Files
dsh-custom a44227b69a feat: 个人定制版 —— API Key 模式显示剩余积分 + 悬浮卡片
基于 @axiaohungry/dsh-llm-workbuddy 1.3.21 (MIT)。
详见 README.CUSTOM.md。
2026-10-10 13:59:09 +08:00

316 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
import { copyFileSync, existsSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import { createRequire } from "node:module";
import { delimiter, dirname, join, resolve } from "node:path";
import { spawnSync } from "node:child_process";
import { parseDocument } from "yaml";
import {
WORKBUDDY_SESSION_REF,
WORKBUDDY_SESSIONS_REF,
LEGACY_SESSION_REF,
LEGACY_SESSIONS_REF,
createWorkBuddySessionStore,
loginWorkBuddy,
parseWorkBuddySession,
parseWorkBuddySessions,
serializeWorkBuddySession,
serializeWorkBuddySessions,
upsertWorkBuddySession,
} from "./workbuddy-auth.js";
const PACKAGE = "@axiaohungry/dsh-llm-workbuddy";
const LEGACY_PACKAGES = ["dsh-llm-workbuddy", "dsh-llm-codebuddy"];
const PACKAGE_VERSION = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf8")).version;
const PACKAGE_SPEC = `${PACKAGE}@${PACKAGE_VERSION}`;
const PROVIDER_PATH = ["llm-pi-ai", "providers", "workbuddy-cn"];
const LEGACY_PROVIDER_PATH = ["llm-pi-ai", "providers", "codebuddy-cn"];
const IGNORED_BUILDS = ["@google/genai", "protobufjs"];
const require = createRequire(import.meta.url);
function dshHome() {
return resolve(process.env.DSH_HOME || join(homedir(), ".dsh"));
}
function profileHasPackage(home, profile, packageName) {
const file = join(home, "profiles", profile, "package.json");
if (!existsSync(file)) return false;
const json = JSON.parse(readFileSync(file, "utf8"));
return Boolean(json.dependencies?.[packageName] || json.devDependencies?.[packageName]);
}
function profileHasPlugin(home, profile) {
return profileHasPackage(home, profile, PACKAGE);
}
function dshEnv() {
const pnpmPackageDir = dirname(require.resolve("pnpm"));
const pnpmBinDir = join(dirname(pnpmPackageDir), ".bin");
const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path") || "PATH";
return {
...process.env,
[pathKey]: `${pnpmBinDir}${delimiter}${process.env[pathKey] || ""}`,
npm_config_ignore_workspace_root_check: "true",
};
}
function runDsh(args) {
const result = spawnSync(process.platform === "win32" ? "dsh.cmd" : "dsh", args, {
stdio: "inherit",
shell: process.platform === "win32",
env: dshEnv(),
});
if (result.error) throw result.error;
if (result.status !== 0) throw new Error(`dsh ${args.join(" ")} 执行失败(退出码 ${result.status})`);
}
function entrySettingsVersion(output) {
const version = String(output).match(/\b(\d+)\.(\d+)\.(\d+)(?:[-.]|\b)/);
if (!version) return undefined;
const [, major, minor, patch] = version.map(Number);
return major > 0 || minor > 1 || (minor === 1 && patch >= 7);
}
function usesEntrySettings() {
const result = spawnSync(process.platform === "win32" ? "dsh.cmd" : "dsh", ["--version"], {
encoding: "utf8",
shell: process.platform === "win32",
env: dshEnv(),
});
if (result.error || result.status !== 0) throw new Error("无法检测 DSH 版本,请确认 dsh 命令可用");
const modern = entrySettingsVersion(result.stdout);
if (modern === undefined) throw new Error("无法识别 DSH 版本,令牌已保存;请在模型设置中手动选择令牌登录");
return modern;
}
function writeYamlDocument(file, document) {
const temporary = join(dirname(file), `.workbuddy-${process.pid}.tmp`);
writeFileSync(temporary, String(document), "utf8");
renameSync(temporary, file);
}
function withPnpmBuildPolicy(file, action) {
const document = parseDocument(readFileSync(file, "utf8"));
if (document.errors.length) throw new Error(`无法解析 ${file}:${document.errors[0].message}`);
const changes = [];
for (const packageName of IGNORED_BUILDS) {
const path = ["allowBuilds", packageName];
if (typeof document.getIn(path) !== "boolean") {
changes.push({ packageName, existed: document.hasIn(path), value: document.getIn(path) });
document.setIn(path, false);
}
}
if (changes.length) writeYamlDocument(file, document);
try {
return action();
} finally {
if (changes.length) {
const current = parseDocument(readFileSync(file, "utf8"));
for (const change of changes) {
const path = ["allowBuilds", change.packageName];
if (change.existed) current.setIn(path, change.value);
else current.deleteIn(path);
}
if (current.getIn(["allowBuilds"])?.items?.length === 0) current.deleteIn(["allowBuilds"]);
writeYamlDocument(file, current);
}
}
}
function cleanPnpmWorkspace(file) {
if (!existsSync(file)) return;
const document = parseDocument(readFileSync(file, "utf8"));
if (document.errors.length) throw new Error(`无法解析 ${file}:${document.errors[0].message}`);
const entries = document.getIn(["minimumReleaseAgeExclude"])?.items;
if (!entries) return;
const packageNames = [PACKAGE, ...LEGACY_PACKAGES];
const remaining = entries.map((entry) => entry.value).filter((entry) => !packageNames.some((packageName) => String(entry).startsWith(`${packageName}@`)));
if (remaining.length === entries.length) return;
if (remaining.length) document.setIn(["minimumReleaseAgeExclude"], remaining);
else document.deleteIn(["minimumReleaseAgeExclude"]);
writeYamlDocument(file, document);
}
function cleanSettings(file) {
if (!existsSync(file)) return undefined;
const source = readFileSync(file, "utf8");
const document = parseDocument(source);
if (document.errors.length) throw new Error(`无法解析 ${file}:${document.errors[0].message}`);
const paths = [PROVIDER_PATH, LEGACY_PROVIDER_PATH].filter((path) => document.hasIn(path));
if (paths.length === 0) return undefined;
for (const path of paths) document.deleteIn(path);
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const backup = `${file}.workbuddy-backup-${stamp}`;
const temporary = join(dirname(file), `.settings-workbuddy-${process.pid}.tmp`);
copyFileSync(file, backup);
writeFileSync(temporary, String(document), "utf8");
renameSync(temporary, file);
return backup;
}
function enableTokenLogin(home = dshHome()) {
const file = join(home, "settings.yaml");
const source = existsSync(file) ? readFileSync(file, "utf8") : "{}\n";
const document = parseDocument(source);
if (document.errors.length) throw new Error(`无法解析 ${file}:${document.errors[0].message}`);
if (document.hasIn(PROVIDER_PATH)) document.deleteIn([...PROVIDER_PATH, "apiKeyEnv"]);
else if (document.hasIn(LEGACY_PROVIDER_PATH)) {
const legacy = document.getIn(LEGACY_PROVIDER_PATH);
const value = legacy && typeof legacy.toJSON === "function" ? legacy.toJSON() : legacy;
document.setIn(PROVIDER_PATH, document.createNode(value && typeof value === "object" ? value : {}));
document.deleteIn([...PROVIDER_PATH, "apiKeyEnv"]);
document.deleteIn(LEGACY_PROVIDER_PATH);
}
else document.setIn(PROVIDER_PATH, {});
if (existsSync(file)) {
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
copyFileSync(file, `${file}.workbuddy-backup-${stamp}`);
}
writeYamlDocument(file, document);
}
function storeLoginSession(session, home = dshHome()) {
const file = join(home, ".credentials.yaml");
const document = parseDocument(existsSync(file) ? readFileSync(file, "utf8") : "{}\n");
if (document.errors.length) throw new Error(`无法解析 ${file}:${document.errors[0].message}`);
const stored = document.get(WORKBUDDY_SESSIONS_REF)
?? document.get(LEGACY_SESSIONS_REF)
?? document.get(WORKBUDDY_SESSION_REF)
?? document.get(LEGACY_SESSION_REF);
const current = stored ? parseWorkBuddySessions(stored) : createWorkBuddySessionStore();
const next = upsertWorkBuddySession(current, session);
document.set(WORKBUDDY_SESSIONS_REF, serializeWorkBuddySessions(next));
document.set(WORKBUDDY_SESSION_REF, serializeWorkBuddySession(next.sessions.find((entry) => entry.id === next.activeId)));
document.delete(LEGACY_SESSIONS_REF);
document.delete(LEGACY_SESSION_REF);
if (existsSync(file)) {
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
copyFileSync(file, `${file}.workbuddy-backup-${stamp}`);
}
writeYamlDocument(file, document);
}
async function login() {
console.log("正在打开 WorkBuddy 中国站网页登录(无需安装 WorkBuddy CLI)……");
const session = await loginWorkBuddy((url, opened) => {
if (opened) console.log("浏览器登录页已打开,请在浏览器中完成登录。");
else console.log(`无法自动打开浏览器,请手动访问:${url}`);
});
storeLoginSession(session);
if (usesEntrySettings()) {
console.log("WorkBuddy 令牌登录成功。新版 DSH 按插件条目保存配置,请在模型设置的 WorkBuddy 卡片中选择“令牌登录”;无需安装 WorkBuddy CLI。");
} else {
enableTokenLogin();
console.log("WorkBuddy 令牌登录成功,Provider 已切换为令牌模式。请重启 DSH。");
}
}
function install() {
for (const profile of ["web", "headless"]) {
runDsh(["plugin", "--profile", profile, "list", "--depth", "0"]);
const workspace = join(dshHome(), "profiles", profile, "pnpm-workspace.yaml");
withPnpmBuildPolicy(workspace, () => {
for (const legacyPackage of LEGACY_PACKAGES) {
if (profileHasPackage(dshHome(), profile, legacyPackage)) {
runDsh(["plugin", "--profile", profile, "remove", legacyPackage]);
}
}
runDsh(["plugin", "--profile", profile, "add", PACKAGE_SPEC]);
});
}
console.log("WorkBuddy Provider 已安装。请重启 DSH 后进行配置。");
}
function uninstall(home = dshHome()) {
const backup = cleanSettings(join(home, "settings.yaml"));
for (const profile of ["web", "headless"]) {
const workspace = join(home, "profiles", profile, "pnpm-workspace.yaml");
const installedPackages = [PACKAGE, ...LEGACY_PACKAGES].filter((packageName) => profileHasPackage(home, profile, packageName));
if (installedPackages.length) {
withPnpmBuildPolicy(workspace, () => {
for (const packageName of installedPackages) {
runDsh(["plugin", "--profile", profile, "remove", packageName]);
}
});
}
cleanPnpmWorkspace(workspace);
}
console.log(backup ? `WorkBuddy 配置已清理,备份:${backup}` : "未发现 WorkBuddy Provider 配置。");
console.log("插件已卸载,API Key 和登录令牌凭据保持不变。请重启 DSH。");
}
function selfTest() {
const root = mkdtempSync(join(tmpdir(), "dsh-workbuddy-cli-"));
try {
if (entrySettingsVersion("dsh 0.1.6") !== false || entrySettingsVersion("dsh 0.1.7-rc.1") !== true || entrySettingsVersion("dsh 0.2.0-rc.2") !== true) {
throw new Error("DSH version detection self-test failed");
}
const file = join(root, "settings.yaml");
writeFileSync(file, "llm-pi-ai:\n providers:\n opencode-go:\n apiKeyEnv: OPENCODE_GO_API_KEY\n codebuddy-cn:\n apiKeyEnv: WORKBUDDY_CN_API_KEY\n models:\n - id: legacy-model\n", "utf8");
enableTokenLogin(root);
const tokenMode = parseDocument(readFileSync(file, "utf8"));
if (tokenMode.hasIn([...PROVIDER_PATH, "apiKeyEnv"]) || !tokenMode.hasIn(PROVIDER_PATH) || tokenMode.hasIn(LEGACY_PROVIDER_PATH) || !tokenMode.hasIn([...PROVIDER_PATH, "models", 0, "id"])) {
throw new Error("token login settings self-test failed");
}
const backup = cleanSettings(file);
const result = parseDocument(readFileSync(file, "utf8"));
if (!backup || !existsSync(backup) || result.hasIn(PROVIDER_PATH) || !result.hasIn(["llm-pi-ai", "providers", "opencode-go"])) {
throw new Error("uninstall settings cleanup self-test failed");
}
enableTokenLogin(root);
if (!parseDocument(readFileSync(file, "utf8")).hasIn(PROVIDER_PATH)) {
throw new Error("token login provider creation self-test failed");
}
const sampleSession = {
auth: { accessToken: "test-access-token", refreshToken: "test-refresh-token", expiresAt: Date.now() + 60_000 },
account: { userId: "test-user" },
};
storeLoginSession(sampleSession, root);
const storedSession = parseDocument(readFileSync(join(root, ".credentials.yaml"), "utf8")).get(WORKBUDDY_SESSION_REF);
if (parseWorkBuddySession(storedSession).account.userId !== "test-user") {
throw new Error("token credential storage self-test failed");
}
const storedSessions = parseDocument(readFileSync(join(root, ".credentials.yaml"), "utf8")).get(WORKBUDDY_SESSIONS_REF);
const sessionStore = parseWorkBuddySessions(storedSessions);
if (sessionStore.sessions.length !== 1 || sessionStore.activeId !== sessionStore.sessions[0].id) {
throw new Error("token account list self-test failed");
}
const workspace = join(root, "pnpm-workspace.yaml");
writeFileSync(workspace, "packages:\n - .\nallowBuilds:\n '@google/genai': true\n protobufjs: pending\n", "utf8");
withPnpmBuildPolicy(workspace, () => {
const active = parseDocument(readFileSync(workspace, "utf8"));
if (active.getIn(["allowBuilds", "@google/genai"]) !== true || active.getIn(["allowBuilds", "protobufjs"]) !== false) {
throw new Error("pnpm build policy activation self-test failed");
}
});
const restored = parseDocument(readFileSync(workspace, "utf8"));
if (restored.getIn(["allowBuilds", "@google/genai"]) !== true || restored.getIn(["allowBuilds", "protobufjs"]) !== "pending") {
throw new Error("pnpm build policy self-test failed");
}
restored.setIn(["minimumReleaseAgeExclude"], ["other@1.0.0", `${PACKAGE}@1.3.1`]);
writeYamlDocument(workspace, restored);
cleanPnpmWorkspace(workspace);
const cleaned = parseDocument(readFileSync(workspace, "utf8")).getIn(["minimumReleaseAgeExclude"])?.items?.map((entry) => entry.value);
if (cleaned?.join(",") !== "other@1.0.0") throw new Error("pnpm workspace cleanup self-test failed");
const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path") || "PATH";
if (!dshEnv()[pathKey].split(delimiter)[0].endsWith(join("node_modules", ".bin"))) {
throw new Error("bundled pnpm PATH self-test failed");
}
console.log("CLI-SELF-TEST-OK");
} finally {
rmSync(root, { recursive: true, force: true });
}
}
const command = process.argv[2];
if (command === "install") install();
else if (command === "uninstall") uninstall();
else if (command === "login") await login();
else if (command === "--self-test") selfTest();
else {
console.log("用法:dsh-llm-workbuddy <install|login|uninstall>");
process.exitCode = 1;
}