balance-dsh 2.1.1 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -5910,6 +5910,11 @@ function UsageCostLine({ useProjection, sessionId }) {
5910
5910
  kind: "installed",
5911
5911
  to: data.to || "",
5912
5912
  failed: Array.isArray(data.failed) ? data.failed : [],
5913
+ // 服务端会判断这次更新是否「持久」:按 npm 版本装进 profile 的,只覆盖文件
5914
+ // 会被重启时的声明版本盖回去(用户 2026-10-31 报的版本回归)。
5915
+ persisted: data.persisted !== false,
5916
+ warning: typeof data.warning === "string" ? data.warning : "",
5917
+ command: typeof data.command === "string" ? data.command : "",
5913
5918
  });
5914
5919
  })
5915
5920
  .catch((error) => {
@@ -5938,6 +5943,12 @@ function UsageCostLine({ useProjection, sessionId }) {
5938
5943
  if (failed.length > 0) {
5939
5944
  return "已更新到 " + updateCheck.to + ",但有 " + failed.length + " 个文件没覆盖成功(" + failed.join(";") + ")。重启后如仍异常,请先从插件市场重装。";
5940
5945
  }
5946
+ // 不持久的那一次必须说清楚:重启会退回旧号,并给出能真正生效的命令。
5947
+ if (updateCheck.persisted === false) {
5948
+ return "已就地更新到 " + updateCheck.to + ",但本机是按 npm 版本装进 profile 的 —— 重启后可能退回旧号。"
5949
+ + "请执行 " + (updateCheck.command || "dsh plugin install balance-dsh@" + updateCheck.to)
5950
+ + "(或 设置 → 插件 里的更新)后再重启。";
5951
+ }
5941
5952
  return "已更新到 " + updateCheck.to + ",重启 DeepSeek Harness 后生效。";
5942
5953
  }
5943
5954
  if (updateCheck.kind === "error") return "检查失败:" + updateCheck.error + "(可稍后重试)";
package/lib/index.js CHANGED
@@ -758,7 +758,7 @@ function formatBalance(balance) {
758
758
  // | { status:"failed" } (网络失败;官方明确“失败绝不变成 0 余额”)
759
759
  // client 是 AccountClientMetadata(version / locale / timezoneOffsetSeconds),
760
760
  // 缺字段会在平台侧组装请求头时抛错,所以三个字段都要给。
761
- const PLUGIN_VERSION = "2.1.1";
761
+ const PLUGIN_VERSION = "2.1.2";
762
762
 
763
763
  /** 磁盘上**实际安装**的版本号。
764
764
  *
@@ -862,6 +862,188 @@ function updateTarballUrl(version) {
862
862
  return "https://registry.npmjs.org/balance-dsh/-/balance-dsh-" + version + ".tgz";
863
863
  }
864
864
 
865
+ /** 插件目录所在的 profile 根(.../profiles/<名字>/node_modules/balance-dsh → .../profiles/<名字>)。
866
+ *
867
+ * 用户反馈(2026-10-31):「在插件设置里更新之后重启会导致版本回归,更新完是 2.1.0,
868
+ * 重启了之后又变成 2.0.8」。根因就是这里 —— 就地覆盖文件只改了 node_modules 里的内容,
869
+ * profile 的 package.json / pnpm-lock.yaml 里锁的还是旧版本;DSH 重启时按声明版本重新
870
+ * 落地那一份,于是又退回 2.0.8。所以必须同时把「声明的版本」也更新掉,见下面
871
+ * installViaPackageManager。 */
872
+ async function profileDirOf(pluginDir) {
873
+ if (typeof pluginDir !== "string" || pluginDir === "") return "";
874
+ const { dirname, basename, join } = await import("node:path");
875
+ const fs = await import("node:fs/promises");
876
+ /** 这一层是不是「声明了 balance-dsh 的 profile 根」。 */
877
+ const declares = async (dir) => {
878
+ try {
879
+ const raw = JSON.parse(await fs.readFile(join(dir, "package.json"), "utf8"));
880
+ return raw != null && typeof raw === "object" && raw.dependencies != null && Object.hasOwn(raw.dependencies, "balance-dsh");
881
+ } catch {
882
+ return false;
883
+ }
884
+ };
885
+ // 常规布局:<profile>/node_modules/<包>
886
+ const nodeModules = dirname(pluginDir);
887
+ if (basename(nodeModules) === "node_modules") {
888
+ const profile = dirname(nodeModules);
889
+ if (await declares(profile)) return profile;
890
+ }
891
+ // 兜底:pnpm 的虚拟仓是 <profile>/node_modules/.pnpm/<包>@<版本>/node_modules/<包>,
892
+ // 上一级不是 profile —— 往上找「package.json 里声明了 balance-dsh」的那一层,最多 6 级。
893
+ // 认不出来返回 ""(开发目录),调用方按「本地装法」处理,就地覆盖就是最终状态。
894
+ let dir = dirname(pluginDir);
895
+ for (let i = 0; i < 6 && dir !== ""; i += 1) {
896
+ if (await declares(dir)) return dir;
897
+ const parent = dirname(dir);
898
+ if (parent === dir) break;
899
+ dir = parent;
900
+ }
901
+ return "";
902
+ }
903
+
904
+ /** profile 里 balance-dsh 的依赖声明。只有**registry 版本号**才需要走包管理器:
905
+ * `file:` / `link:` / `git` 这类指向本地目录的装法,就地覆盖文件就是最终状态,不会被回退。 */
906
+ async function profileSpecOf(profileDir) {
907
+ if (profileDir === "") return "";
908
+ try {
909
+ const { readFile } = await import("node:fs/promises");
910
+ const { join } = await import("node:path");
911
+ const raw = JSON.parse(await readFile(join(profileDir, "package.json"), "utf8"));
912
+ const spec = raw?.dependencies?.["balance-dsh"];
913
+ if (typeof spec !== "string") return "";
914
+ const trimmed = spec.trim();
915
+ if (trimmed === "") return "";
916
+ // file:/link:/portal:/workspace:/git+https: 这些都不指向 registry 版本号。
917
+ // ⚠️ git 要写成 `git(?:\+[a-z]+)?`:实参是 `git+https://…`,光写 `git\+` 后面
918
+ // 跟的是 `https:` 而不是冒号,会漏过去被当成 registry 号(探针 2026-10-31 抓到)。
919
+ if (/^(file|link|portal|workspace|git(?:\+[a-z]+)?|https?):/i.test(trimmed)) return "";
920
+ if (trimmed.startsWith(".") || trimmed.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(trimmed)) return "";
921
+ return trimmed;
922
+ } catch {
923
+ return "";
924
+ }
925
+ }
926
+
927
+ /** 用 DSH 自带的 pnpm 真正装一遍:它会把 profile 的 package.json + pnpm-lock.yaml + node_modules
928
+ * 一起改成新版本,重启不会再退回旧号。失败时把两个文件按原样恢复(对齐 DSH 插件管理器自己的做法)。 */
929
+ async function installViaPackageManager(profileDir, version) {
930
+ const result = { ok: false, error: "", output: "" };
931
+ if (profileDir === "" || version === "") {
932
+ result.error = "没有 profile 目录";
933
+ return result;
934
+ }
935
+ const path = await import("node:path");
936
+ const fs = await import("node:fs/promises");
937
+ const roots = [];
938
+ const res = typeof process !== "undefined" && typeof process.resourcesPath === "string" ? process.resourcesPath : "";
939
+ if (res !== "") {
940
+ roots.push(path.join(res, "runtime", "primary-runtime", "dependencies"));
941
+ roots.push(path.join(res, "app", "runtime", "primary-runtime", "dependencies"));
942
+ }
943
+ const exe = typeof process !== "undefined" && typeof process.execPath === "string" ? process.execPath : "";
944
+ if (exe !== "") {
945
+ const base = path.dirname(exe);
946
+ roots.push(path.join(base, "runtime", "primary-runtime", "dependencies"));
947
+ roots.push(path.join(base, "resources", "runtime", "primary-runtime", "dependencies"));
948
+ roots.push(path.join(base, "..", "resources", "runtime", "primary-runtime", "dependencies"));
949
+ }
950
+ let nodeExe = "";
951
+ let pnpmMjs = "";
952
+ for (const root of roots) {
953
+ const nodeTry = process.platform === "win32"
954
+ ? path.join(root, "node", "bin", "node.exe")
955
+ : path.join(root, "node", "bin", "node");
956
+ const pnpmTry = path.join(root, "pnpm", "bin", "pnpm.mjs");
957
+ try {
958
+ await fs.access(nodeTry);
959
+ await fs.access(pnpmTry);
960
+ nodeExe = nodeTry;
961
+ pnpmMjs = pnpmTry;
962
+ break;
963
+ } catch {
964
+ // 继续试下一个候选根
965
+ }
966
+ }
967
+ if (nodeExe === "" || pnpmMjs === "") {
968
+ result.error = "找不到 DSH 自带的 pnpm";
969
+ return result;
970
+ }
971
+
972
+ // 备份两个会被 pnpm 改写的文件:失败要原样恢复,绝不能让 profile 停在半改状态
973
+ const saved = [];
974
+ for (const name of ["package.json", "pnpm-lock.yaml"]) {
975
+ const file = path.join(profileDir, name);
976
+ try {
977
+ saved.push({ file, text: await fs.readFile(file, "utf8") });
978
+ } catch {
979
+ saved.push({ file, text: null });
980
+ }
981
+ }
982
+ const restore = async () => {
983
+ for (const item of saved) {
984
+ try {
985
+ if (item.text === null) await fs.rm(item.file, { force: true });
986
+ else await fs.writeFile(item.file, item.text, "utf8");
987
+ } catch {
988
+ // 恢复失败也只能作罢,至少已经报错给用户
989
+ }
990
+ }
991
+ };
992
+
993
+ const { spawn } = await import("node:child_process");
994
+ const args = [
995
+ pnpmMjs,
996
+ "add",
997
+ "balance-dsh@" + version,
998
+ "--config.minimum-release-age=0",
999
+ "--config.confirmModulesPurge=false",
1000
+ "--reporter=append-only",
1001
+ "--registry",
1002
+ "https://registry.npmjs.org/",
1003
+ ];
1004
+ const env = { ...process.env };
1005
+ delete env.ELECTRON_RUN_AS_NODE;
1006
+ delete env.ELECTRON_NO_ATTACH_CONSOLE;
1007
+ const child = spawn(nodeExe, args, { cwd: profileDir, env, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
1008
+ let out = "";
1009
+ child.stdout?.on("data", (chunk) => {
1010
+ out += String(chunk);
1011
+ if (out.length > 20000) out = out.slice(-20000);
1012
+ });
1013
+ child.stderr?.on("data", (chunk) => {
1014
+ out += String(chunk);
1015
+ if (out.length > 20000) out = out.slice(-20000);
1016
+ });
1017
+ const done = await new Promise((resolve) => {
1018
+ const timer = setTimeout(() => {
1019
+ try {
1020
+ child.kill();
1021
+ } catch {
1022
+ // 进程可能已经退出
1023
+ }
1024
+ resolve({ code: -1, timedOut: true });
1025
+ }, 240000);
1026
+ child.on("error", (error) => {
1027
+ clearTimeout(timer);
1028
+ resolve({ code: -1, error: error instanceof Error ? error.message : String(error) });
1029
+ });
1030
+ child.on("close", (code) => {
1031
+ clearTimeout(timer);
1032
+ resolve({ code: typeof code === "number" ? code : -1, timedOut: false });
1033
+ });
1034
+ });
1035
+ result.output = out.trim().split(/\r?\n/).slice(-6).join("\n");
1036
+ if (done.code === 0) {
1037
+ result.ok = true;
1038
+ return result;
1039
+ }
1040
+ await restore();
1041
+ result.error = done.timedOut
1042
+ ? "pnpm 超过 4 分钟没有结束(已中止,profile 已恢复原状)"
1043
+ : (done.error || ("pnpm 退出码 " + String(done.code)));
1044
+ return result;
1045
+ }
1046
+
865
1047
  /** 就地更新本插件:下载 npm 上的最新 tarball → 解包 → 逐文件覆盖自己的安装目录。
866
1048
  *
867
1049
  * 几个刻意的取舍:
@@ -887,6 +1069,37 @@ async function installLatestVersion(ctx) {
887
1069
  const root = await installedPluginDir();
888
1070
  if (root === "") return { ok: false, error: "定位不到插件安装目录" };
889
1071
 
1072
+ // ★ 先看这一份装法是不是「按 registry 版本装进 profile 的」。
1073
+ // 是的话,光覆盖 node_modules 里的文件没用 —— profile 的 package.json / pnpm-lock.yaml
1074
+ // 还锁着旧版本,DSH 重启时按声明版本重新落地,版本就退回旧号(用户 2026-10-31 报的现象)。
1075
+ // 所以这种情况先让 DSH 自带的 pnpm 真正装一遍,声明与文件一起变,重启才不丢。
1076
+ const profileDir = await profileDirOf(root);
1077
+ const registrySpec = await profileSpecOf(profileDir);
1078
+ const manualCommand = "dsh plugin install balance-dsh@" + latest;
1079
+ let pnpmFailure = "";
1080
+ let pnpmOutput = "";
1081
+ if (registrySpec !== "") {
1082
+ const viaPnpm = await installViaPackageManager(profileDir, latest);
1083
+ if (viaPnpm.ok) {
1084
+ updateCache = { at: 0, value: null };
1085
+ return {
1086
+ ok: true,
1087
+ installed: true,
1088
+ from: currentVer,
1089
+ to: latest,
1090
+ via: "pnpm",
1091
+ persisted: true,
1092
+ files: 0,
1093
+ failed: [],
1094
+ restart: true,
1095
+ };
1096
+ }
1097
+ // pnpm 跑不起来(没有自带 runtime / 断网 / 被策略拦):继续走就地覆盖,
1098
+ // 但必须如实告诉用户「这次不持久」,并给出能真正生效的命令。
1099
+ pnpmFailure = viaPnpm.error;
1100
+ pnpmOutput = viaPnpm.output;
1101
+ }
1102
+
890
1103
  let gz;
891
1104
  try {
892
1105
  const signal =
@@ -972,6 +1185,17 @@ async function installLatestVersion(ctx) {
972
1185
  installed: true,
973
1186
  from: currentVer,
974
1187
  to: latest,
1188
+ via: "files",
1189
+ // registry 装法只覆盖文件 = 重启会被 profile 里锁的旧版本覆盖回去,必须如实标出来
1190
+ persisted: registrySpec === "",
1191
+ warning: registrySpec === ""
1192
+ ? ""
1193
+ : "文件已覆盖到 " + latest + ",但本机是按 npm 版本装进 profile 的,profile 里锁定的还是旧版本;"
1194
+ + "重启后可能退回旧号(" + (pnpmFailure || "自带 pnpm 不可用") + ")。"
1195
+ + "请执行 " + manualCommand + " 后再重启,或用 设置 → 插件 里的更新。",
1196
+ command: manualCommand,
1197
+ pnpmError: pnpmFailure,
1198
+ pnpmOutput,
975
1199
  files: written.length,
976
1200
  failed,
977
1201
  backup: backupRoot,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "balance-dsh",
3
- "version": "2.1.1",
3
+ "version": "2.1.2",
4
4
  "description": "在 DeepSeek Harness 中显示 DeepSeek 账户余额和当前会话 token/费用消耗(含高峰/空闲峰谷计费)。仅支持 DeepSeek 官方模型与账号,暂不支持其他大模型。",
5
5
  "author": "Rannist",
6
6
  "type": "module",