feat: 个人定制版 —— API Key 模式显示剩余积分 + 悬浮卡片
基于 @axiaohungry/dsh-llm-workbuddy 1.3.21 (MIT)。 详见 README.CUSTOM.md。
This commit is contained in:
commit
a44227b69a
14 files changed
+5517
No files matched your search
@@ -0,0 +1,315 @@
|
||||
#!/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;
|
||||
}
|
||||
Reference in new issue
Block a user