mirror of
https://github.com/ben4202121/buddybridge.git
synced 2026-07-22 07:48:01 +00:00
- 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
1067 lines
39 KiB
JavaScript
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);
|
|
}
|
|
};
|