ben4202121_buddybridge/main.js
buddybridge 2bdd9ad41a fix: restore revealLeaf and bump minAppVersion to 1.7.2
- revealLeaf is required to show the chat panel on fresh Obsidian installs
- Bump minAppVersion from 1.5.0 to 1.7.2 to match revealLeaf API requirement
- Update README prerequisites to state Obsidian 1.7.2+ requirement
2026-06-29 20:41:47 +08:00

1067 lines
39 KiB
JavaScript

/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/main.ts
var main_exports = {};
__export(main_exports, {
default: () => BuddyBridgePlugin
});
module.exports = __toCommonJS(main_exports);
var import_obsidian3 = require("obsidian");
// src/api.ts
var import_child_process = require("child_process");
var path = __toESM(require("path"));
var fs = __toESM(require("fs"));
// src/types.ts
var CURRENT_SETTINGS_VERSION = 3;
var DEFAULT_SETTINGS = {
codebuddyPath: "",
maxConversations: 20,
version: CURRENT_SETTINGS_VERSION
};
function isObject(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function getString(data, key) {
const value = data[key];
return typeof value === "string" ? value : void 0;
}
function getNumber(data, key) {
const value = data[key];
return typeof value === "number" ? value : void 0;
}
function getErrorMessage(error) {
if (error instanceof Error) {
return error.message;
}
if (typeof error === "string") {
return error;
}
return "\u672A\u77E5\u9519\u8BEF";
}
function migrateSettings(stored) {
var _a;
if (!isObject(stored)) {
return { ...DEFAULT_SETTINGS };
}
const maxConversations = getNumber(stored, "maxConversations");
return {
codebuddyPath: (_a = getString(stored, "codebuddyPath")) != null ? _a : DEFAULT_SETTINGS.codebuddyPath,
maxConversations: typeof maxConversations === "number" && maxConversations > 0 ? maxConversations : DEFAULT_SETTINGS.maxConversations,
version: CURRENT_SETTINGS_VERSION
};
}
function generateId() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = Math.random() * 16 | 0;
const v = c === "x" ? r : r & 3 | 8;
return v.toString(16);
});
}
function normalizePersistedData(raw) {
const result = {};
if (!isObject(raw)) {
return result;
}
if (Array.isArray(raw.conversations)) {
result.conversations = raw.conversations;
}
if (isObject(raw.settings)) {
result.settings = migrateSettings(raw.settings);
}
return result;
}
// src/api.ts
var TIMEOUT = 3e5;
var NODE_EXECUTABLE = process.platform === "win32" ? "node.exe" : "node";
function findNodeExecutable() {
const home = process.env.HOME || process.env.USERPROFILE || "";
const nodeDirs = [];
if (process.platform === "win32") {
nodeDirs.push(path.dirname(process.execPath));
const appData = process.env.APPDATA || "";
if (appData) {
nodeDirs.push(appData);
nodeDirs.push(path.join(appData, "npm"));
}
const programFiles = process.env.ProgramFiles || "C:\\Program Files";
const programFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
const localAppData = process.env.LOCALAPPDATA || "";
nodeDirs.push(
path.join(programFiles, "nodejs"),
path.join(programFilesX86, "nodejs")
);
if (localAppData) {
nodeDirs.push(path.join(localAppData, "Programs", "nodejs"));
}
const nvmSymlink = process.env.NVM_SYMLINK;
if (nvmSymlink) {
nodeDirs.push(nvmSymlink);
}
if (home) {
const wbNodeVersionsDir = path.join(home, ".workbuddy", "binaries", "node", "versions");
try {
const versions = fs.readdirSync(wbNodeVersionsDir);
for (const v of versions) {
nodeDirs.push(path.join(wbNodeVersionsDir, v));
}
} catch (e) {
}
}
for (const drive of ["C:", "D:", "E:"]) {
if (drive + "\\" !== path.parse(programFiles).root.toUpperCase()) {
nodeDirs.push(path.join(drive + "\\Program Files", "nodejs"));
}
}
} else {
nodeDirs.push(
path.join(home, ".local", "bin"),
path.join(home, ".npm-global", "bin"),
path.join(home, ".volta", "bin"),
path.join(home, "bin", "codebuddy"),
"/usr/local/bin/codebuddy",
"/opt/homebrew/bin/codebuddy"
);
const nvmBin = process.env.NVM_BIN;
if (nvmBin) {
nodeDirs.push(nvmBin);
}
}
for (const dir of nodeDirs) {
if (!dir)
continue;
try {
const nodePath = path.join(dir, NODE_EXECUTABLE);
if (fs.existsSync(nodePath) && fs.statSync(nodePath).isFile()) {
console.log("[BB] found node at:", nodePath);
return nodePath;
}
} catch (e) {
}
}
console.log("[BB] WARNING: node not found in any search path, falling back to 'node'");
return "node";
}
function resolveCodebuddyPath(customPath) {
if (customPath && fs.existsSync(customPath)) {
return customPath;
}
if (process.env.CODEBUDDY_PATH && fs.existsSync(process.env.CODEBUDDY_PATH)) {
return process.env.CODEBUDDY_PATH;
}
const home = process.env.HOME || process.env.USERPROFILE || "";
const localAppData = process.env.LOCALAPPDATA || "";
const appData = process.env.APPDATA || "";
const programFiles = process.env.ProgramFiles || "C:\\Program Files";
const programFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
const candidates = [];
if (process.platform === "win32") {
candidates.push(
path.join(localAppData, "Programs", "WorkBuddy", "resources", "app.asar.unpacked", "cli", "bin", "codebuddy.exe"),
path.join(localAppData, "Programs", "WorkBuddy", "resources", "app.asar.unpacked", "cli", "bin", "codebuddy.cmd"),
path.join(localAppData, "Programs", "WorkBuddy", "resources", "app.asar.unpacked", "cli", "bin", "codebuddy")
);
if (appData) {
candidates.push(path.join(appData, "npm", "codebuddy.cmd"));
candidates.push(path.join(appData, "npm", "codebuddy"));
}
candidates.push(
path.join(programFiles, "nodejs", "codebuddy.cmd"),
path.join(programFiles, "nodejs", "node_modules", ".bin", "codebuddy.cmd"),
path.join(programFilesX86, "nodejs", "node_modules", ".bin", "codebuddy.cmd"),
path.join(programFiles, "WorkBuddy", "resources", "app.asar.unpacked", "cli", "bin", "codebuddy.exe"),
path.join(programFiles, "WorkBuddy", "resources", "app.asar.unpacked", "cli", "bin", "codebuddy.cmd"),
path.join(programFiles, "WorkBuddy", "resources", "app.asar.unpacked", "cli", "bin", "codebuddy"),
path.join(programFilesX86, "WorkBuddy", "resources", "app.asar.unpacked", "cli", "bin", "codebuddy.exe"),
path.join(programFilesX86, "WorkBuddy", "resources", "app.asar.unpacked", "cli", "bin", "codebuddy.cmd"),
path.join(programFilesX86, "WorkBuddy", "resources", "app.asar.unpacked", "cli", "bin", "codebuddy")
);
for (const drive of ["C:", "D:", "E:"]) {
candidates.push(
path.join(drive + "\\Program Files", "WorkBuddy", "resources", "app.asar.unpacked", "cli", "bin", "codebuddy.exe"),
path.join(drive + "\\Program Files", "WorkBuddy", "resources", "app.asar.unpacked", "cli", "bin", "codebuddy.cmd"),
path.join(drive + "\\Program Files", "WorkBuddy", "resources", "app.asar.unpacked", "cli", "bin", "codebuddy")
);
}
} else {
candidates.push(
path.join(home, ".local", "bin", "codebuddy"),
path.join(home, ".npm-global", "bin", "codebuddy"),
path.join(home, ".volta", "bin", "codebuddy"),
path.join(home, "bin", "codebuddy"),
"/usr/local/bin/codebuddy",
"/opt/homebrew/bin/codebuddy"
);
}
const nvmBin = process.env.NVM_BIN;
if (nvmBin)
candidates.push(path.join(nvmBin, "codebuddy"));
const npmPrefix = process.env.npm_config_prefix;
if (npmPrefix)
candidates.push(path.join(npmPrefix, "bin", "codebuddy"));
for (const p of candidates) {
if (fs.existsSync(p)) {
console.log("[BB] resolved codebuddy path:", p);
return p;
}
}
const envPath = process.env.PATH || "";
const pathSep = process.platform === "win32" ? ";" : ":";
const exeNames = process.platform === "win32" ? ["codebuddy.exe", "codebuddy.cmd", "codebuddy"] : ["codebuddy"];
for (const dir of envPath.split(pathSep)) {
if (!dir)
continue;
for (const name of exeNames) {
try {
const p = path.join(dir, name);
if (fs.existsSync(p))
return p;
} catch (e) {
}
}
}
return "codebuddy";
}
function parseMessageBlock(block) {
if (!isObject(block))
return null;
const type = getString(block, "type");
if (type !== "thinking" && type !== "text" && type !== "tool_call")
return null;
return {
type,
thinking: getString(block, "thinking"),
text: getString(block, "text"),
name: getString(block, "name"),
input: block.input
};
}
function blockToChunk(block) {
if (block.type === "thinking") {
return { type: "thinking", content: block.thinking || "" };
}
if (block.type === "text") {
return { type: "text", content: block.text || "" };
}
const input = block.input;
return {
type: "tool",
content: "",
toolName: block.name || "unknown",
toolDetail: typeof input === "string" ? input : JSON.stringify(input != null ? input : {})
};
}
function parseStreamEvent(raw) {
if (!isObject(raw))
return null;
const event = isObject(raw.event) ? raw.event : raw;
if (!isObject(event))
return null;
return {
type: getString(event, "type") || "",
thinking: getString(event, "thinking"),
text: getString(event, "text"),
name: getString(event, "name"),
input: event.input,
result: getString(event, "result"),
error: getString(event, "error"),
message: getString(event, "message"),
content: getString(event, "content")
};
}
function parseStreamLine(line) {
if (!line.trim())
return null;
try {
const raw = JSON.parse(line);
if (isObject(raw) && (raw.type === "assistant" || raw.type === "user")) {
const message = isObject(raw.message) ? raw.message : null;
const content = Array.isArray(message == null ? void 0 : message.content) ? message.content : [];
for (const item of content) {
const block = parseMessageBlock(item);
if (block) {
const chunk = blockToChunk(block);
if (chunk)
return chunk;
}
}
return null;
}
const event = parseStreamEvent(raw);
if (!event)
return null;
if (event.type === "thinking") {
return { type: "thinking", content: event.thinking || "" };
}
if (event.type === "message_delta") {
return { type: "text", content: event.text || "" };
}
if (event.type === "tool_call") {
const input = event.input;
return {
type: "tool",
content: "",
toolName: event.name || "unknown",
toolDetail: typeof input === "string" ? input : JSON.stringify(input != null ? input : {})
};
}
if (event.type === "result") {
return { type: "done", content: event.result || "" };
}
if (event.type === "error") {
return { type: "error", content: event.error || event.message || "\u672A\u77E5\u9519\u8BEF" };
}
console.log("[BB] unknown event:", line.substring(0, 200));
const fallbackText = event.text || event.content || event.message || "";
if (fallbackText) {
return { type: "text", content: fallbackText };
}
return null;
} catch (e) {
return { type: "text", content: line };
}
}
function isWindowsWrapper(scriptPath) {
return scriptPath.endsWith(".cmd") || scriptPath.endsWith(".exe") || scriptPath.endsWith(".bat");
}
function isBareFallback(scriptPath) {
return scriptPath === "codebuddy" || !path.isAbsolute(scriptPath);
}
function needsWindowsShell(scriptPath) {
return process.platform === "win32" && (scriptPath.endsWith(".cmd") || scriptPath.endsWith(".bat"));
}
var BuddyBridgeAPI = class {
constructor(timeout = TIMEOUT) {
this.timeout = timeout;
this.scriptPath = resolveCodebuddyPath("");
}
setCodebuddyPath(p) {
this.scriptPath = resolveCodebuddyPath(p);
}
generateId() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = Math.random() * 16 | 0;
const v = c === "x" ? r : r & 3 | 8;
return v.toString(16);
});
}
async *sendMessage(sessionId, text, vaultPath) {
var _a;
const scriptPath = this.scriptPath;
const procOptions = {
timeout: this.timeout,
stdio: ["ignore", "pipe", "pipe"]
};
if (vaultPath) {
procOptions.cwd = vaultPath;
}
const cliArgs = ["--print", "--output-format", "stream-json", "--session-id", sessionId, text];
if (needsWindowsShell(scriptPath)) {
procOptions.shell = true;
}
let proc;
if (isWindowsWrapper(scriptPath) || isBareFallback(scriptPath)) {
proc = (0, import_child_process.spawn)(scriptPath, cliArgs, procOptions);
} else {
const nodeBin = findNodeExecutable() || "node";
proc = (0, import_child_process.spawn)(nodeBin, [scriptPath, ...cliArgs], procOptions);
}
let buffer = "";
let errOut = "";
let hasOutput = false;
const chunkQueue = [];
let resolveQueue = null;
let closed = false;
proc.stdout.on("data", (d) => {
buffer += d.toString();
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
const chunk = parseStreamLine(line);
if (chunk) {
hasOutput = true;
const preview = typeof chunk.content === "string" ? chunk.content.substring(0, 80) : JSON.stringify(chunk.content).substring(0, 80);
console.log("[BB] chunk:", chunk.type, preview);
if (resolveQueue) {
resolveQueue({ value: chunk, done: false });
resolveQueue = null;
} else {
chunkQueue.push(chunk);
}
}
}
});
proc.stderr.on("data", (d) => {
errOut += d.toString();
console.log("[BB] stderr:", errOut);
});
proc.on("close", (code, signal) => {
console.log("[BB] exit:", code, signal ? "signal:" + signal : "", "| err:", errOut.substring(0, 200));
closed = true;
if (resolveQueue) {
if (errOut && !hasOutput) {
resolveQueue({ value: { type: "error", content: errOut }, done: true });
} else {
resolveQueue({ value: { type: "done", content: "" }, done: true });
}
resolveQueue = null;
}
});
proc.on("error", (e) => {
console.log("[BB] spawn err:", e.message, "| scriptPath:", scriptPath);
closed = true;
if (resolveQueue) {
let hint = e.message;
if (e.message.includes("ENOENT")) {
if (scriptPath === "codebuddy") {
hint = "\u627E\u4E0D\u5230 codebuddy CLI\u3002\u8BF7\u786E\u8BA4\u5DF2\u5B89\u88C5 WorkBuddy \u684C\u9762\u7248\uFF0C\u6216\u5728\u63D2\u4EF6\u8BBE\u7F6E\u4E2D\u6307\u5B9A codebuddy \u8DEF\u5F84\u3002";
} else if (!isWindowsWrapper(scriptPath) && !isBareFallback(scriptPath)) {
hint = `\u627E\u4E0D\u5230 Node.js \u6765\u8FD0\u884C codebuddy (\u8DEF\u5F84: ${scriptPath})\u3002\u8BF7\u786E\u8BA4\u5DF2\u5B89\u88C5 Node.js\u3002`;
}
}
resolveQueue({ value: { type: "error", content: hint }, done: true });
resolveQueue = null;
}
});
while (true) {
if (chunkQueue.length > 0) {
const nextChunk = chunkQueue.shift();
if (nextChunk) {
yield nextChunk;
continue;
}
}
if (closed) {
if (buffer.trim()) {
const chunk = parseStreamLine(buffer);
if (chunk)
yield chunk;
}
break;
}
const next = await new Promise((r) => {
resolveQueue = r;
});
if (next.done) {
if (((_a = next.value) == null ? void 0 : _a.type) === "error")
throw new Error(next.value.content);
break;
}
yield next.value;
}
}
cancel() {
}
};
// src/views/chat.ts
var import_obsidian = require("obsidian");
// src/chat/manager.ts
var ConversationManager = class {
constructor() {
this.conversations = /* @__PURE__ */ new Map();
this.activeId = null;
this.persistCallback = null;
}
setPersistCallback(callback) {
this.persistCallback = callback;
}
async persist() {
if (this.persistCallback) {
await this.persistCallback(this.getAll());
}
}
handlePersistError(error) {
console.error("[BB] persist failed:", getErrorMessage(error));
}
/** 显式触发持久化(流式结束后调用) */
async flush() {
await this.persist();
}
/** 从持久化数据加载对话 */
load(conversations) {
if (!conversations || conversations.length === 0) {
this.createConversation();
return;
}
for (const conv of conversations) {
this.conversations.set(conv.id, { ...conv });
}
this.activeId = conversations[0].id;
}
/** 创建新对话 */
createConversation(title) {
const id = generateId();
const conv = {
id,
title: title || "\u65B0\u5BF9\u8BDD",
sessionId: "",
// 首次发送消息时由 Gateway 分配
messages: [],
createdAt: Date.now(),
updatedAt: Date.now()
};
this.conversations.set(id, conv);
this.activeId = id;
this.persist().catch((err) => this.handlePersistError(err));
return conv;
}
/** 删除对话 */
deleteConversation(id) {
if (!this.conversations.has(id))
return false;
this.conversations.delete(id);
if (this.activeId === id) {
const remaining = this.getAll();
this.activeId = remaining.length > 0 ? remaining[0].id : null;
}
this.persist().catch((err) => this.handlePersistError(err));
return true;
}
/** 切换到指定对话 */
switchTo(id) {
const conv = this.conversations.get(id);
if (!conv)
return null;
this.activeId = id;
return conv;
}
/** 获取当前活跃对话 */
getActive() {
if (!this.activeId)
return null;
return this.conversations.get(this.activeId) || null;
}
/** 获取所有对话(按更新时间倒序) */
getAll() {
return Array.from(this.conversations.values()).sort((a, b) => b.updatedAt - a.updatedAt);
}
/** 添加消息到当前活跃对话 */
addMessage(convId, role, content) {
const conv = this.conversations.get(convId);
if (!conv)
return null;
const msg = {
id: generateId(),
role,
content,
timestamp: Date.now()
};
conv.messages.push(msg);
conv.updatedAt = Date.now();
if (conv.title === "\u65B0\u5BF9\u8BDD" && role === "user" && content.trim()) {
conv.title = content.substring(0, 30) + (content.length > 30 ? "..." : "");
}
this.persist().catch((err) => this.handlePersistError(err));
return msg;
}
/** 更新指定消息内容(用于流式追加) */
updateMessage(convId, msgId, content, skipSave = false) {
const conv = this.conversations.get(convId);
if (!conv)
return false;
const msg = conv.messages.find((m) => m.id === msgId);
if (!msg)
return false;
msg.content = content;
conv.updatedAt = Date.now();
if (!skipSave) {
this.persist().catch((err) => this.handlePersistError(err));
}
return true;
}
/** 设置对话的 Gateway sessionId */
setSessionId(convId, sessionId) {
const conv = this.conversations.get(convId);
if (!conv)
return false;
conv.sessionId = sessionId;
return true;
}
};
// src/views/chat.ts
var VIEW_TYPE_CHAT = "buddybridge-panel";
var BuddyBridgeChatView = class extends import_obsidian.ItemView {
constructor(leaf, api, loadDataCallback) {
super(leaf);
this.isStreaming = false;
this.streamingMsgId = null;
this.api = api;
this.loadDataCallback = loadDataCallback;
this.manager = new ConversationManager();
this.markdownComponent = new import_obsidian.Component();
this.markdownComponent.load();
}
get vaultPath() {
const adapter = this.app.vault.adapter;
return adapter.basePath;
}
getViewType() {
return VIEW_TYPE_CHAT;
}
getDisplayText() {
return "BuddyBridge \u804A\u5929";
}
getIcon() {
return "bot";
}
getManager() {
return this.manager;
}
async onOpen() {
const container = this.contentEl;
container.empty();
container.addClass("buddybridge-chat-container");
this.tabBar = container.createDiv({ cls: "buddybridge-tab-bar" });
const newBtn = this.tabBar.createEl("button", {
text: "",
cls: "buddybridge-new-chat-btn",
attr: { title: "\u65B0\u5EFA\u5BF9\u8BDD", "aria-label": "\u65B0\u5EFA\u5BF9\u8BDD" }
});
(0, import_obsidian.setIcon)(newBtn, "plus");
newBtn.onclick = () => this.createNewChat();
this.messageContainer = container.createDiv({ cls: "buddybridge-messages" });
const inputArea = container.createDiv({ cls: "buddybridge-input-area" });
this.inputEl = inputArea.createEl("textarea", {
cls: "buddybridge-input",
attr: { placeholder: "\u8F93\u5165\u6D88\u606F... (Shift+Enter \u6362\u884C\uFF0CEnter \u53D1\u9001)", rows: "2" }
});
this.inputEl.onkeydown = (e) => this.handleKeydown(e);
this.inputEl.oninput = () => this.adjustTextareaHeight();
const sendBtn = inputArea.createEl("button", {
text: "\u53D1\u9001",
cls: "buddybridge-send-btn",
attr: { "aria-label": "\u53D1\u9001" }
});
sendBtn.onclick = () => this.sendMessage();
try {
const conversations = await this.loadDataCallback();
await this.loadConversations(conversations);
} catch (e) {
console.error("[BB] \u52A0\u8F7D\u5386\u53F2\u5BF9\u8BDD\u5931\u8D25:", e);
}
}
async onClose() {
this.markdownComponent.unload();
}
async loadConversations(conversations) {
this.manager.load(conversations);
this.renderTabs();
await this.renderMessages();
}
async createNewChat() {
this.manager.createConversation();
this.renderTabs();
await this.renderMessages();
}
async switchToChat(id) {
this.manager.switchTo(id);
this.renderTabs();
await this.renderMessages();
}
async deleteChat(id, e) {
e.stopPropagation();
this.manager.deleteConversation(id);
this.renderTabs();
await this.renderMessages();
}
/** 渲染标签栏 */
renderTabs() {
var _a;
const newBtn = this.tabBar.querySelector(".buddybridge-new-chat-btn");
const oldTabs = this.tabBar.querySelectorAll(".buddybridge-tab");
oldTabs.forEach((t) => t.remove());
const conversations = this.manager.getAll();
const activeId = (_a = this.manager.getActive()) == null ? void 0 : _a.id;
for (const conv of conversations) {
const tab = this.tabBar.createDiv({ cls: "buddybridge-tab" });
if (conv.id === activeId) {
tab.addClass("buddybridge-tab-active");
}
tab.createSpan({ text: conv.title, cls: "buddybridge-tab-title" });
const closeBtn = tab.createSpan({
cls: "buddybridge-tab-close",
attr: { title: "\u5173\u95ED\u5BF9\u8BDD", "aria-label": "\u5173\u95ED\u5BF9\u8BDD", role: "button", tabindex: "0" }
});
(0, import_obsidian.setIcon)(closeBtn, "x");
closeBtn.onclick = (e) => this.deleteChat(conv.id, e);
closeBtn.onkeydown = (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
void this.deleteChat(conv.id, e);
}
};
tab.onclick = () => this.switchToChat(conv.id);
if (newBtn) {
tab.after(newBtn);
}
}
}
async renderMessages() {
this.messageContainer.empty();
const conv = this.manager.getActive();
if (!conv) {
const empty = this.messageContainer.createDiv({ cls: "buddybridge-empty-chat" });
const icon = empty.createDiv({ cls: "buddybridge-empty-chat-icon" });
(0, import_obsidian.setIcon)(icon, "message-square");
empty.createDiv({ cls: "buddybridge-empty-chat-title", text: "\u5F00\u59CB\u65B0\u5BF9\u8BDD" });
empty.createDiv({ cls: "buddybridge-empty-chat-subtitle", text: "\u70B9\u51FB\u4E0A\u65B9 + \u6309\u94AE\u6216\u8F93\u5165\u6D88\u606F\u5F00\u59CB\u804A\u5929" });
return;
}
for (const msg of conv.messages) {
await this.renderMessage(msg);
}
this.scrollToBottom();
}
async renderMessage(msg) {
const row = this.messageContainer.createDiv({
cls: `buddybridge-message-row buddybridge-message-${msg.role}`
});
const bubble = row.createDiv({ cls: "buddybridge-bubble" });
const isWaiting = msg.role === "assistant" && msg.content === "" && msg.id === this.streamingMsgId;
if (isWaiting) {
this.renderThinkingIndicator(bubble);
} else if (msg.role === "assistant") {
await this.renderMarkdownContent(bubble, msg.content);
} else {
bubble.createSpan({ text: msg.content });
}
return row;
}
renderThinkingIndicator(bubble) {
const thinking = bubble.createDiv({ cls: "buddybridge-thinking" });
thinking.createSpan({ cls: "buddybridge-thinking-text", text: "\u601D\u8003\u4E2D" });
const dots = thinking.createDiv({ cls: "buddybridge-thinking-dots" });
for (let i = 0; i < 3; i++) {
dots.createSpan({ cls: "buddybridge-dot" });
}
}
async renderMarkdownContent(bubble, content) {
if (!content)
return;
const thinkingBlock = bubble.querySelector(".buddybridge-thinking-block");
const toolsBlock = bubble.querySelector(".buddybridge-tools-block");
let markdownContainer = bubble.querySelector(".buddybridge-markdown-content");
if (!(markdownContainer instanceof HTMLElement)) {
markdownContainer = bubble.createDiv({ cls: "buddybridge-markdown-content" });
if (thinkingBlock instanceof HTMLElement) {
bubble.insertBefore(markdownContainer, thinkingBlock);
} else if (toolsBlock instanceof HTMLElement) {
bubble.insertBefore(markdownContainer, toolsBlock);
}
}
if (!(markdownContainer instanceof HTMLElement))
return;
markdownContainer.empty();
await import_obsidian.MarkdownRenderer.render(
this.app,
content,
markdownContainer,
"",
this.markdownComponent
);
}
adjustTextareaHeight() {
this.inputEl.style.setProperty("--buddybridge-input-height", `${this.inputEl.scrollHeight}px`);
}
async handleKeydown(e) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
await this.sendMessage();
}
}
async sendMessage() {
if (this.isStreaming)
return;
const text = this.inputEl.value.trim();
if (!text)
return;
let conv = this.manager.getActive();
if (!conv) {
conv = this.manager.createConversation();
this.renderTabs();
}
if (!conv.sessionId) {
conv.sessionId = this.api.generateId();
}
const convId = conv.id;
this.manager.addMessage(convId, "user", text);
this.inputEl.value = "";
this.adjustTextareaHeight();
await this.renderMessages();
const aiMsg = this.manager.addMessage(convId, "assistant", "");
if (!aiMsg)
return;
this.streamingMsgId = aiMsg.id;
this.isStreaming = true;
await this.renderMessages();
let firstChunk = true;
let thinkingContent = "";
let textContent = "";
try {
const contextText = this.vaultPath ? `\u5F53\u524D Obsidian Vault \u8DEF\u5F84: ${this.vaultPath}
\u5DE5\u4F5C\u76EE\u5F55\u5373 vault \u6839\u76EE\u5F55\uFF0C\u8BF7\u57FA\u4E8E vault \u4E2D\u7684\u6587\u4EF6\u56DE\u7B54\u95EE\u9898\u3002
---
${text}` : text;
const streamingBubble = this.messageContainer.querySelector(
`.buddybridge-message-assistant:last-child .buddybridge-bubble`
);
if (!(streamingBubble instanceof HTMLElement)) {
throw new Error("\u627E\u4E0D\u5230 Assistant \u6D88\u606F\u6C14\u6CE1");
}
for await (const chunk of this.api.sendMessage(conv.sessionId, contextText, this.vaultPath)) {
const bubble = streamingBubble;
if (firstChunk) {
firstChunk = false;
const thinking = bubble.querySelector(".buddybridge-thinking");
if (thinking instanceof HTMLElement) {
thinking.addClass("buddybridge-thinking-fadeout");
await new Promise((r) => window.setTimeout(r, 200));
thinking.remove();
}
}
if (chunk.type === "thinking") {
thinkingContent += chunk.content;
let block = bubble.querySelector(".buddybridge-thinking-block");
if (!(block instanceof HTMLElement)) {
block = bubble.createDiv({ cls: "buddybridge-thinking-block" });
const header = block.createDiv({ cls: "buddybridge-thinking-header" });
const icon = header.createSpan({ cls: "buddybridge-thinking-header-icon" });
(0, import_obsidian.setIcon)(icon, "sparkles");
header.createSpan({ cls: "buddybridge-thinking-header-text", text: "\u601D\u8003\u4E2D..." });
const chevron = header.createSpan({ cls: "buddybridge-thinking-header-chevron", text: "\u25BE" });
const bodyDiv = block.createDiv({ cls: "buddybridge-thinking-body buddybridge-hidden" });
header.addEventListener("click", () => {
const hidden = bodyDiv.hasClass("buddybridge-hidden");
bodyDiv.toggleClass("buddybridge-hidden", !hidden);
chevron.textContent = hidden ? "\u25BE" : "\u25B8";
});
}
const body = block.querySelector(".buddybridge-thinking-body");
if (body instanceof HTMLElement) {
body.setText(thinkingContent);
}
} else if (chunk.type === "tool") {
let toolsBlock = bubble.querySelector(".buddybridge-tools-block");
if (!(toolsBlock instanceof HTMLElement)) {
toolsBlock = bubble.createDiv({ cls: "buddybridge-tools-block" });
const hdr = toolsBlock.createDiv({ cls: "buddybridge-tools-header" });
const icon = hdr.createSpan({ cls: "buddybridge-tools-header-icon" });
(0, import_obsidian.setIcon)(icon, "wrench");
hdr.createSpan({ cls: "buddybridge-tools-header-text", text: "\u5DE5\u5177\u8C03\u7528" });
const chevron = hdr.createSpan({ cls: "buddybridge-tools-header-chevron", text: "\u25BE" });
hdr.addEventListener("click", () => {
const list2 = toolsBlock.querySelector(".buddybridge-tools-list");
if (list2 instanceof HTMLElement) {
const hidden = list2.hasClass("buddybridge-hidden");
list2.toggleClass("buddybridge-hidden", !hidden);
chevron.textContent = hidden ? "\u25BE" : "\u25B8";
}
});
toolsBlock.createDiv({ cls: "buddybridge-tools-list buddybridge-hidden" });
}
const list = toolsBlock.querySelector(".buddybridge-tools-list");
if (list instanceof HTMLElement) {
const toolName = chunk.toolName || "";
const toolDetail = chunk.toolDetail || "";
let iconName = "wrench";
if (toolName.includes("read") || toolName.includes("\u67E5\u770B") || toolName.includes("\u8BFB\u53D6")) {
iconName = "file-text";
} else if (toolName.includes("write") || toolName.includes("\u7F16\u8F91") || toolName.includes("\u5199\u5165")) {
iconName = "pencil";
} else if (toolName.includes("search") || toolName.includes("\u641C\u7D22") || toolName.includes("\u67E5\u627E")) {
iconName = "search";
}
const row = list.createDiv({ cls: "buddybridge-tool-call" });
const icon = row.createSpan({ cls: "buddybridge-tool-call-icon" });
(0, import_obsidian.setIcon)(icon, iconName);
row.createSpan({
cls: "buddybridge-tool-call-text",
text: `${toolName} ${toolDetail}`.trim()
});
}
} else if (chunk.type === "text") {
textContent += chunk.content;
this.manager.updateMessage(convId, aiMsg.id, textContent, true);
await this.renderMarkdownContent(bubble, textContent);
} else if (chunk.type === "error") {
this.manager.updateMessage(convId, aiMsg.id, `\u9519\u8BEF: ${chunk.content}`, true);
new import_obsidian.Notice(`\u8BF7\u6C42\u5931\u8D25: ${chunk.content}`);
}
}
const finalContent = textContent || thinkingContent;
this.manager.updateMessage(convId, aiMsg.id, finalContent);
if (!finalContent) {
this.manager.updateMessage(convId, aiMsg.id, "\uFF08\u65E0\u54CD\u5E94\uFF0C\u8BF7\u91CD\u8BD5\uFF09");
}
const thinkingLabel = streamingBubble.querySelector(".buddybridge-thinking-header-text");
if (thinkingLabel instanceof HTMLElement) {
thinkingLabel.setText("\u5DF2\u601D\u8003");
}
await this.renderMessages();
await this.manager.flush();
} catch (error) {
const message = getErrorMessage(error);
this.manager.updateMessage(convId, aiMsg.id, `\u9519\u8BEF: ${message}`);
new import_obsidian.Notice(`\u8BF7\u6C42\u5931\u8D25: ${message}`);
await this.renderMessages();
} finally {
this.isStreaming = false;
this.streamingMsgId = null;
}
}
scrollToBottom() {
this.messageContainer.scrollTop = this.messageContainer.scrollHeight;
}
};
// src/settings/tab.ts
var import_obsidian2 = require("obsidian");
var BuddyBridgeSettingTab = class extends import_obsidian2.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
const { containerEl } = this;
containerEl.empty();
new import_obsidian2.Setting(containerEl).setName("Configuration").setHeading();
new import_obsidian2.Setting(containerEl).setName("CodeBuddy \u8DEF\u5F84").setDesc("codebuddy \u53EF\u6267\u884C\u6587\u4EF6\u8DEF\u5F84\u3002\u5982 WorkBuddy \u81EA\u5B9A\u4E49\u5B89\u88C5\uFF0C\u8DEF\u5F84\u901A\u5E38\u4E3A\uFF1A\u5B89\u88C5\u76EE\u5F55\\resources\\app.asar.unpacked\\cli\\bin\\codebuddy\uFF08\u53F3\u952E WorkBuddy \u5FEB\u6377\u65B9\u5F0F \u2192 \u6253\u5F00\u6587\u4EF6\u4F4D\u7F6E \u53EF\u627E\u5230\u5B89\u88C5\u76EE\u5F55\uFF09").addText((text) => text.setPlaceholder("WorkBuddy\u5B89\u88C5\u76EE\u5F55\\resources\\app.asar.unpacked\\cli\\bin\\codebuddy").setValue(this.plugin.settings.codebuddyPath).onChange(async (value) => {
this.plugin.settings.codebuddyPath = value;
this.plugin.api.setCodebuddyPath(value);
await this.plugin.saveSettings();
}));
new import_obsidian2.Setting(containerEl).setName("\u6700\u5927\u5BF9\u8BDD\u6570").setDesc("\u6700\u591A\u4FDD\u7559\u591A\u5C11\u4E2A\u5BF9\u8BDD\uFF08\u65E7\u5BF9\u8BDD\u5C06\u88AB\u81EA\u52A8\u5220\u9664\uFF09").addText((text) => text.setPlaceholder("20").setValue(String(this.plugin.settings.maxConversations)).onChange(async (value) => {
const num = parseInt(value);
if (!isNaN(num) && num > 0) {
this.plugin.settings.maxConversations = num;
await this.plugin.saveSettings();
}
}));
}
};
// src/main.ts
var BuddyBridgePlugin = class extends import_obsidian3.Plugin {
constructor() {
super(...arguments);
this.chatView = null;
}
async onload() {
try {
await this.loadSettings();
this.api = new BuddyBridgeAPI();
this.api.setCodebuddyPath(this.settings.codebuddyPath);
this.registerView(
VIEW_TYPE_CHAT,
(leaf) => {
const view = new BuddyBridgeChatView(leaf, this.api, async () => {
const data = normalizePersistedData(await this.loadData());
return data.conversations || [];
});
this.chatView = view;
view.getManager().setPersistCallback(async (conversations) => {
const data = normalizePersistedData(await this.loadData());
data.conversations = conversations;
await this.saveData(data);
});
return view;
}
);
this.addRibbonIcon("bot", "BuddyBridge \u804A\u5929", async () => {
await this.activateView();
});
this.addCommand({
id: "open-chat",
name: "\u6253\u5F00\u804A\u5929\u9762\u677F",
callback: async () => {
await this.activateView();
}
});
this.addSettingTab(new BuddyBridgeSettingTab(this.app, this));
} catch (e) {
console.error("[BB] \u63D2\u4EF6\u52A0\u8F7D\u5931\u8D25:", e);
new import_obsidian3.Notice("BuddyBridge \u52A0\u8F7D\u5931\u8D25\uFF0C\u8BF7\u67E5\u770B Console");
}
}
onunload() {
this.api.cancel();
}
async activateView() {
try {
const { workspace } = this.app;
let leaf = workspace.getLeavesOfType(VIEW_TYPE_CHAT)[0];
if (!leaf) {
leaf = workspace.getRightLeaf(true);
if (!leaf) {
leaf = workspace.getLeaf(true);
}
if (leaf) {
await leaf.setViewState({ type: VIEW_TYPE_CHAT, active: true });
}
}
if (leaf) {
await workspace.revealLeaf(leaf);
workspace.setActiveLeaf(leaf, { focus: true });
} else {
new import_obsidian3.Notice("BuddyBridge\uFF1A\u65E0\u6CD5\u521B\u5EFA\u804A\u5929\u9762\u677F");
}
} catch (e) {
console.error("[BB] \u6253\u5F00\u804A\u5929\u9762\u677F\u5931\u8D25:", e);
new import_obsidian3.Notice("BuddyBridge\uFF1A\u6253\u5F00\u9762\u677F\u5931\u8D25\uFF0C\u8BF7\u67E5\u770B Console");
}
}
async loadPersistedConversations() {
const data = normalizePersistedData(await this.loadData());
if (this.chatView) {
await this.chatView.loadConversations(data.conversations || []);
}
}
async loadSettings() {
const data = normalizePersistedData(await this.loadData());
this.settings = migrateSettings(data.settings);
}
async saveSettings() {
const existingData = normalizePersistedData(await this.loadData());
const merged = { ...existingData, settings: this.settings };
await this.saveData(merged);
this.api.setCodebuddyPath(this.settings.codebuddyPath);
}
};