Files
my-vault/.obsidian/plugins/note-buddy/main.js
T

693 lines
23 KiB
JavaScript
Raw Normal View History

2026-01-31 08:25:19 +08:00
/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/main.ts
var main_exports = {};
__export(main_exports, {
default: () => NoteBuddyPlugin
});
module.exports = __toCommonJS(main_exports);
var import_obsidian4 = require("obsidian");
// src/chat-view.ts
var import_obsidian2 = require("obsidian");
// src/service.ts
var import_obsidian = require("obsidian");
// src/models.ts
var defaultSettings = {
serviceUrl: "http://127.0.0.1:4096"
};
var MODELS_CACHE_TTL = 5 * 60 * 1e3;
// src/service.ts
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;
}
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 ${timeoutMs / 1e3} seconds`)), timeoutMs)
)
]);
}
async healthCheck() {
const url = `${this.serviceUrl}/global/health`;
try {
const response = await this.request({
url,
method: "GET"
});
if (response.status === 200 && response.json.healthy === true) {
return {
status: "connected" /* Connected */,
lastTestTime: Date.now()
};
} else {
return {
status: "disconnected" /* Disconnected */,
lastTestTime: Date.now(),
lastError: `Service unhealthy or unreachable`
};
}
} catch (error) {
const err = error;
return {
status: "disconnected" /* Disconnected */,
lastTestTime: Date.now(),
lastError: err.message || "Unknown error"
};
}
}
async createSession() {
const url = `${this.serviceUrl}/session`;
try {
const response = await this.request({
url,
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({})
});
if (response.status === 200) {
const data = response.json;
return {
sessionID: data.id,
createTime: data.time.created,
title: data.title
};
}
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"
},
body: JSON.stringify(message)
},
_OpenCodeClient.MESSAGE_REQUEST_TIMEOUT_MS
);
if (response.status === 200) {
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}`);
}
}
async sendMessage(input) {
if (!this.session) {
this.session = await this.createSession();
}
const message = {
parts: [{ type: "text", text: input }]
};
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
var VIEW_TYPE_CHAT = "note-buddy-chat-view";
var ChatView = class extends import_obsidian2.ItemView {
constructor(leaf, plugin) {
super(leaf);
this.plugin = plugin;
this.messages = [];
this.client = new OpenCodeClient(
plugin.settings.serviceUrl,
plugin.settings.defaultModelId,
plugin.onModelUnavailable?.bind(plugin)
);
}
getViewType() {
return VIEW_TYPE_CHAT;
}
getDisplayText() {
return "NoteBuddy Chat";
}
async onOpen() {
const container = this.containerEl.children[1];
container.empty();
const chatContainer = container.createDiv({ cls: "nb-chat-container" });
chatContainer.style.cssText = `
display: flex;
flex-direction: column;
height: 100%;
gap: 1rem;
`;
const messagesContainer = chatContainer.createDiv({ cls: "nb-messages-container" });
this.messagesContainer = messagesContainer;
messagesContainer.style.cssText = `
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1rem;
`;
this.renderMessages();
const welcomeMessage = messagesContainer.createDiv({ cls: "nb-message nb-message-system" });
welcomeMessage.createSpan({ text: "Welcome to NoteBuddy! Chat with your notes here." });
welcomeMessage.style.cssText = `
padding: 0.75rem;
background-color: var(--background-secondary);
border-radius: 0.5rem;
font-size: 0.9rem;
color: var(--text-muted);
`;
const inputContainer = chatContainer.createDiv({ cls: "nb-input-container" });
inputContainer.style.cssText = `
display: flex;
gap: 0.5rem;
padding: 1rem;
border-top: 1px solid var(--background-modifier-border);
`;
const textarea = inputContainer.createEl("textarea", {
cls: "nb-chat-input",
attr: { placeholder: "Type your message..." }
});
textarea.style.cssText = `
flex: 1;
min-height: 60px;
max-height: 150px;
padding: 0.5rem;
border: 1px solid var(--background-modifier-border);
border-radius: 0.375rem;
background-color: var(--background-primary);
color: var(--text-normal);
resize: vertical;
font-family: inherit;
`;
const sendButton = inputContainer.createEl("button", {
cls: "nb-send-button",
text: "Send"
});
sendButton.style.cssText = `
padding: 0.5rem 1rem;
background-color: var(--interactive-accent);
color: var(--text-on-accent);
border: none;
border-radius: 0.375rem;
cursor: pointer;
font-weight: 500;
`;
const sendMessage = async () => {
const message = textarea.value.trim();
if (!message) return;
this.messages.push({ kind: "userText", text: message });
this.renderMessages();
textarea.value = "";
try {
if (!this.plugin.sessionState) {
this.plugin.sessionState = await this.client.createSession();
}
const sendData = {
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")) {
this.plugin.sessionState = await this.client.createSession();
response = await this.client.sendMessageToSession(this.plugin.sessionState.sessionID, sendData);
} else {
throw sendError;
}
}
for (const part of response.parts) {
this.messages.push({ kind: "assistantPart", part });
}
this.renderMessages();
this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight;
} catch (error) {
console.error("[NoteBuddy] Send failed:", error);
new import_obsidian2.Notice(`Failed to send message: ${error.message}`);
}
};
sendButton.onclick = sendMessage;
textarea.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
}
async onClose() {
console.log("[NoteBuddy] Chat view closed");
}
renderMessages() {
this.messagesContainer.empty();
if (this.messages.length === 0) {
const welcomeMessage = this.messagesContainer.createDiv({ cls: "nb-message nb-message-system" });
welcomeMessage.createSpan({ text: "Welcome to NoteBuddy! Chat with your notes here." });
welcomeMessage.style.cssText = `
padding: 0.75rem;
background-color: var(--background-secondary);
border-radius: 0.5rem;
font-size: 0.9rem;
color: var(--text-muted);
`;
return;
}
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: var(--background-secondary);
border-radius: 0.5rem;
font-size: 0.9rem;
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;
}
}
}
};
// src/settings.ts
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;
}
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://")) {
new import_obsidian3.Notice("Service URL must start with http:// or https://");
return;
}
try {
const url = new URL(value);
if (!url.hostname || url.hostname === "") {
new import_obsidian3.Notice("Service URL must include a valid hostname");
return;
}
if (!url.port || url.port === "") {
new import_obsidian3.Notice("Service URL must include a port number");
return;
}
} catch (e) {
new import_obsidian3.Notice("Service URL must be a valid URL format");
return;
}
this.plugin.settings.serviceUrl = value;
await this.plugin.saveSettings();
})
);
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, this.plugin.settings.defaultModelId);
const result = await client.healthCheck();
if (result.status === "connected") {
new import_obsidian3.Notice("Connection successful!");
} else {
new import_obsidian3.Notice(`Connection failed: ${result.lastError}`);
}
})
);
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);
}
})
);
}
};
// src/main.ts
var DEFAULT_SETTINGS = defaultSettings;
var NoteBuddyPlugin = class extends import_obsidian4.Plugin {
constructor() {
super(...arguments);
this.ribbonIconEl = null;
}
async onload() {
console.log("Loading NoteBuddy plugin...");
await this.loadSettings();
this.registerView(
VIEW_TYPE_CHAT,
(leaf) => new ChatView(leaf, this)
);
this.addRibbonIcon("bot", "NoteBuddy", () => {
this.activateView();
});
this.addCommand({
id: "open-chat-view",
name: "Open NoteBuddy Chat",
callback: () => {
this.activateView();
}
});
this.app.workspace.onLayoutReady(() => {
this.activateView();
});
this.addSettingTab(new NoteBuddySettingTab(this.app, this));
}
onunload() {
console.log("Unloading NoteBuddy plugin...");
}
async activateView() {
const { workspace } = this.app;
let leaf = null;
const leaves = workspace.getLeavesOfType(VIEW_TYPE_CHAT);
if (leaves.length > 0) {
leaf = leaves[0];
} else {
leaf = workspace.getRightLeaf(false);
}
if (leaf) {
await workspace.setActiveLeaf(leaf);
await leaf.setViewState({ type: VIEW_TYPE_CHAT, active: true });
}
}
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);
}
};