balance-dsh 2.1.1 → 2.1.3
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/README.md +4 -2
- package/lib/client.js +11 -0
- package/lib/index.js +242 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -90,9 +90,11 @@ token 数用官方文档站「图片 Token 计算器」的同一套算法估算
|
|
|
90
90
|
|
|
91
91
|
整张卡片可以收起(点标题一行展开 / 折叠)。**没有保存按钮**,改动后约 0.35 秒自动保存,左下角依次显示「修改中… → 保存中… → 已保存」;点「恢复默认」立刻写回全部默认值。设置存放在你的 `.dsh` 目录下的 `balance-dsh.json`。
|
|
92
92
|
|
|
93
|
-
### `/balance` 命令
|
|
93
|
+
### `/balance-dsh-rannist` 命令
|
|
94
94
|
|
|
95
|
-
在对话里输入 `/balance` 查询余额明细和当前会话消耗。
|
|
95
|
+
在对话里输入 `/balance-dsh-rannist` 查询余额明细和当前会话消耗。
|
|
96
|
+
|
|
97
|
+
命令名带作者后缀是刻意的:DSH 的命令名全局唯一,`/balance` 这类通用名会和别的余额插件撞车(先注册的赢、后来的整条命令消失),所以用全名、不设备用名。注意 DSH 只接受**小写**命令名,写成大写会在注册时被判非法。
|
|
96
98
|
|
|
97
99
|
## 使用统计口径
|
|
98
100
|
|
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,13 @@ function formatBalance(balance) {
|
|
|
758
758
|
// | { status:"failed" } (网络失败;官方明确“失败绝不变成 0 余额”)
|
|
759
759
|
// client 是 AccountClientMetadata(version / locale / timezoneOffsetSeconds),
|
|
760
760
|
// 缺字段会在平台侧组装请求头时抛错,所以三个字段都要给。
|
|
761
|
-
const PLUGIN_VERSION = "2.1.
|
|
761
|
+
const PLUGIN_VERSION = "2.1.3";
|
|
762
|
+
|
|
763
|
+
/** 斜杠命令名(2026-10-31 定案:用带作者后缀的全名,避免和别的余额插件撞车,也不设备用名)。
|
|
764
|
+
* ⚠️ DSH 的 COMMAND_NAME = /^[a-z][a-z0-9_-]*$/u —— **只允许小写**。
|
|
765
|
+
* 写成 balance-dsh-Rannist 会在 normalizeDefinition 里抛 TypeError,整条命令直接消失,
|
|
766
|
+
* 所以这里落成小写;界面里输入 `/balance-dsh-rannist` 即可。 */
|
|
767
|
+
const COMMAND_NAME = "balance-dsh-rannist";
|
|
762
768
|
|
|
763
769
|
/** 磁盘上**实际安装**的版本号。
|
|
764
770
|
*
|
|
@@ -862,6 +868,188 @@ function updateTarballUrl(version) {
|
|
|
862
868
|
return "https://registry.npmjs.org/balance-dsh/-/balance-dsh-" + version + ".tgz";
|
|
863
869
|
}
|
|
864
870
|
|
|
871
|
+
/** 插件目录所在的 profile 根(.../profiles/<名字>/node_modules/balance-dsh → .../profiles/<名字>)。
|
|
872
|
+
*
|
|
873
|
+
* 用户反馈(2026-10-31):「在插件设置里更新之后重启会导致版本回归,更新完是 2.1.0,
|
|
874
|
+
* 重启了之后又变成 2.0.8」。根因就是这里 —— 就地覆盖文件只改了 node_modules 里的内容,
|
|
875
|
+
* profile 的 package.json / pnpm-lock.yaml 里锁的还是旧版本;DSH 重启时按声明版本重新
|
|
876
|
+
* 落地那一份,于是又退回 2.0.8。所以必须同时把「声明的版本」也更新掉,见下面
|
|
877
|
+
* installViaPackageManager。 */
|
|
878
|
+
async function profileDirOf(pluginDir) {
|
|
879
|
+
if (typeof pluginDir !== "string" || pluginDir === "") return "";
|
|
880
|
+
const { dirname, basename, join } = await import("node:path");
|
|
881
|
+
const fs = await import("node:fs/promises");
|
|
882
|
+
/** 这一层是不是「声明了 balance-dsh 的 profile 根」。 */
|
|
883
|
+
const declares = async (dir) => {
|
|
884
|
+
try {
|
|
885
|
+
const raw = JSON.parse(await fs.readFile(join(dir, "package.json"), "utf8"));
|
|
886
|
+
return raw != null && typeof raw === "object" && raw.dependencies != null && Object.hasOwn(raw.dependencies, "balance-dsh");
|
|
887
|
+
} catch {
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
// 常规布局:<profile>/node_modules/<包>
|
|
892
|
+
const nodeModules = dirname(pluginDir);
|
|
893
|
+
if (basename(nodeModules) === "node_modules") {
|
|
894
|
+
const profile = dirname(nodeModules);
|
|
895
|
+
if (await declares(profile)) return profile;
|
|
896
|
+
}
|
|
897
|
+
// 兜底:pnpm 的虚拟仓是 <profile>/node_modules/.pnpm/<包>@<版本>/node_modules/<包>,
|
|
898
|
+
// 上一级不是 profile —— 往上找「package.json 里声明了 balance-dsh」的那一层,最多 6 级。
|
|
899
|
+
// 认不出来返回 ""(开发目录),调用方按「本地装法」处理,就地覆盖就是最终状态。
|
|
900
|
+
let dir = dirname(pluginDir);
|
|
901
|
+
for (let i = 0; i < 6 && dir !== ""; i += 1) {
|
|
902
|
+
if (await declares(dir)) return dir;
|
|
903
|
+
const parent = dirname(dir);
|
|
904
|
+
if (parent === dir) break;
|
|
905
|
+
dir = parent;
|
|
906
|
+
}
|
|
907
|
+
return "";
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/** profile 里 balance-dsh 的依赖声明。只有**registry 版本号**才需要走包管理器:
|
|
911
|
+
* `file:` / `link:` / `git` 这类指向本地目录的装法,就地覆盖文件就是最终状态,不会被回退。 */
|
|
912
|
+
async function profileSpecOf(profileDir) {
|
|
913
|
+
if (profileDir === "") return "";
|
|
914
|
+
try {
|
|
915
|
+
const { readFile } = await import("node:fs/promises");
|
|
916
|
+
const { join } = await import("node:path");
|
|
917
|
+
const raw = JSON.parse(await readFile(join(profileDir, "package.json"), "utf8"));
|
|
918
|
+
const spec = raw?.dependencies?.["balance-dsh"];
|
|
919
|
+
if (typeof spec !== "string") return "";
|
|
920
|
+
const trimmed = spec.trim();
|
|
921
|
+
if (trimmed === "") return "";
|
|
922
|
+
// file:/link:/portal:/workspace:/git+https: 这些都不指向 registry 版本号。
|
|
923
|
+
// ⚠️ git 要写成 `git(?:\+[a-z]+)?`:实参是 `git+https://…`,光写 `git\+` 后面
|
|
924
|
+
// 跟的是 `https:` 而不是冒号,会漏过去被当成 registry 号(探针 2026-10-31 抓到)。
|
|
925
|
+
if (/^(file|link|portal|workspace|git(?:\+[a-z]+)?|https?):/i.test(trimmed)) return "";
|
|
926
|
+
if (trimmed.startsWith(".") || trimmed.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(trimmed)) return "";
|
|
927
|
+
return trimmed;
|
|
928
|
+
} catch {
|
|
929
|
+
return "";
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/** 用 DSH 自带的 pnpm 真正装一遍:它会把 profile 的 package.json + pnpm-lock.yaml + node_modules
|
|
934
|
+
* 一起改成新版本,重启不会再退回旧号。失败时把两个文件按原样恢复(对齐 DSH 插件管理器自己的做法)。 */
|
|
935
|
+
async function installViaPackageManager(profileDir, version) {
|
|
936
|
+
const result = { ok: false, error: "", output: "" };
|
|
937
|
+
if (profileDir === "" || version === "") {
|
|
938
|
+
result.error = "没有 profile 目录";
|
|
939
|
+
return result;
|
|
940
|
+
}
|
|
941
|
+
const path = await import("node:path");
|
|
942
|
+
const fs = await import("node:fs/promises");
|
|
943
|
+
const roots = [];
|
|
944
|
+
const res = typeof process !== "undefined" && typeof process.resourcesPath === "string" ? process.resourcesPath : "";
|
|
945
|
+
if (res !== "") {
|
|
946
|
+
roots.push(path.join(res, "runtime", "primary-runtime", "dependencies"));
|
|
947
|
+
roots.push(path.join(res, "app", "runtime", "primary-runtime", "dependencies"));
|
|
948
|
+
}
|
|
949
|
+
const exe = typeof process !== "undefined" && typeof process.execPath === "string" ? process.execPath : "";
|
|
950
|
+
if (exe !== "") {
|
|
951
|
+
const base = path.dirname(exe);
|
|
952
|
+
roots.push(path.join(base, "runtime", "primary-runtime", "dependencies"));
|
|
953
|
+
roots.push(path.join(base, "resources", "runtime", "primary-runtime", "dependencies"));
|
|
954
|
+
roots.push(path.join(base, "..", "resources", "runtime", "primary-runtime", "dependencies"));
|
|
955
|
+
}
|
|
956
|
+
let nodeExe = "";
|
|
957
|
+
let pnpmMjs = "";
|
|
958
|
+
for (const root of roots) {
|
|
959
|
+
const nodeTry = process.platform === "win32"
|
|
960
|
+
? path.join(root, "node", "bin", "node.exe")
|
|
961
|
+
: path.join(root, "node", "bin", "node");
|
|
962
|
+
const pnpmTry = path.join(root, "pnpm", "bin", "pnpm.mjs");
|
|
963
|
+
try {
|
|
964
|
+
await fs.access(nodeTry);
|
|
965
|
+
await fs.access(pnpmTry);
|
|
966
|
+
nodeExe = nodeTry;
|
|
967
|
+
pnpmMjs = pnpmTry;
|
|
968
|
+
break;
|
|
969
|
+
} catch {
|
|
970
|
+
// 继续试下一个候选根
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
if (nodeExe === "" || pnpmMjs === "") {
|
|
974
|
+
result.error = "找不到 DSH 自带的 pnpm";
|
|
975
|
+
return result;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// 备份两个会被 pnpm 改写的文件:失败要原样恢复,绝不能让 profile 停在半改状态
|
|
979
|
+
const saved = [];
|
|
980
|
+
for (const name of ["package.json", "pnpm-lock.yaml"]) {
|
|
981
|
+
const file = path.join(profileDir, name);
|
|
982
|
+
try {
|
|
983
|
+
saved.push({ file, text: await fs.readFile(file, "utf8") });
|
|
984
|
+
} catch {
|
|
985
|
+
saved.push({ file, text: null });
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
const restore = async () => {
|
|
989
|
+
for (const item of saved) {
|
|
990
|
+
try {
|
|
991
|
+
if (item.text === null) await fs.rm(item.file, { force: true });
|
|
992
|
+
else await fs.writeFile(item.file, item.text, "utf8");
|
|
993
|
+
} catch {
|
|
994
|
+
// 恢复失败也只能作罢,至少已经报错给用户
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
const { spawn } = await import("node:child_process");
|
|
1000
|
+
const args = [
|
|
1001
|
+
pnpmMjs,
|
|
1002
|
+
"add",
|
|
1003
|
+
"balance-dsh@" + version,
|
|
1004
|
+
"--config.minimum-release-age=0",
|
|
1005
|
+
"--config.confirmModulesPurge=false",
|
|
1006
|
+
"--reporter=append-only",
|
|
1007
|
+
"--registry",
|
|
1008
|
+
"https://registry.npmjs.org/",
|
|
1009
|
+
];
|
|
1010
|
+
const env = { ...process.env };
|
|
1011
|
+
delete env.ELECTRON_RUN_AS_NODE;
|
|
1012
|
+
delete env.ELECTRON_NO_ATTACH_CONSOLE;
|
|
1013
|
+
const child = spawn(nodeExe, args, { cwd: profileDir, env, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
1014
|
+
let out = "";
|
|
1015
|
+
child.stdout?.on("data", (chunk) => {
|
|
1016
|
+
out += String(chunk);
|
|
1017
|
+
if (out.length > 20000) out = out.slice(-20000);
|
|
1018
|
+
});
|
|
1019
|
+
child.stderr?.on("data", (chunk) => {
|
|
1020
|
+
out += String(chunk);
|
|
1021
|
+
if (out.length > 20000) out = out.slice(-20000);
|
|
1022
|
+
});
|
|
1023
|
+
const done = await new Promise((resolve) => {
|
|
1024
|
+
const timer = setTimeout(() => {
|
|
1025
|
+
try {
|
|
1026
|
+
child.kill();
|
|
1027
|
+
} catch {
|
|
1028
|
+
// 进程可能已经退出
|
|
1029
|
+
}
|
|
1030
|
+
resolve({ code: -1, timedOut: true });
|
|
1031
|
+
}, 240000);
|
|
1032
|
+
child.on("error", (error) => {
|
|
1033
|
+
clearTimeout(timer);
|
|
1034
|
+
resolve({ code: -1, error: error instanceof Error ? error.message : String(error) });
|
|
1035
|
+
});
|
|
1036
|
+
child.on("close", (code) => {
|
|
1037
|
+
clearTimeout(timer);
|
|
1038
|
+
resolve({ code: typeof code === "number" ? code : -1, timedOut: false });
|
|
1039
|
+
});
|
|
1040
|
+
});
|
|
1041
|
+
result.output = out.trim().split(/\r?\n/).slice(-6).join("\n");
|
|
1042
|
+
if (done.code === 0) {
|
|
1043
|
+
result.ok = true;
|
|
1044
|
+
return result;
|
|
1045
|
+
}
|
|
1046
|
+
await restore();
|
|
1047
|
+
result.error = done.timedOut
|
|
1048
|
+
? "pnpm 超过 4 分钟没有结束(已中止,profile 已恢复原状)"
|
|
1049
|
+
: (done.error || ("pnpm 退出码 " + String(done.code)));
|
|
1050
|
+
return result;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
865
1053
|
/** 就地更新本插件:下载 npm 上的最新 tarball → 解包 → 逐文件覆盖自己的安装目录。
|
|
866
1054
|
*
|
|
867
1055
|
* 几个刻意的取舍:
|
|
@@ -887,6 +1075,37 @@ async function installLatestVersion(ctx) {
|
|
|
887
1075
|
const root = await installedPluginDir();
|
|
888
1076
|
if (root === "") return { ok: false, error: "定位不到插件安装目录" };
|
|
889
1077
|
|
|
1078
|
+
// ★ 先看这一份装法是不是「按 registry 版本装进 profile 的」。
|
|
1079
|
+
// 是的话,光覆盖 node_modules 里的文件没用 —— profile 的 package.json / pnpm-lock.yaml
|
|
1080
|
+
// 还锁着旧版本,DSH 重启时按声明版本重新落地,版本就退回旧号(用户 2026-10-31 报的现象)。
|
|
1081
|
+
// 所以这种情况先让 DSH 自带的 pnpm 真正装一遍,声明与文件一起变,重启才不丢。
|
|
1082
|
+
const profileDir = await profileDirOf(root);
|
|
1083
|
+
const registrySpec = await profileSpecOf(profileDir);
|
|
1084
|
+
const manualCommand = "dsh plugin install balance-dsh@" + latest;
|
|
1085
|
+
let pnpmFailure = "";
|
|
1086
|
+
let pnpmOutput = "";
|
|
1087
|
+
if (registrySpec !== "") {
|
|
1088
|
+
const viaPnpm = await installViaPackageManager(profileDir, latest);
|
|
1089
|
+
if (viaPnpm.ok) {
|
|
1090
|
+
updateCache = { at: 0, value: null };
|
|
1091
|
+
return {
|
|
1092
|
+
ok: true,
|
|
1093
|
+
installed: true,
|
|
1094
|
+
from: currentVer,
|
|
1095
|
+
to: latest,
|
|
1096
|
+
via: "pnpm",
|
|
1097
|
+
persisted: true,
|
|
1098
|
+
files: 0,
|
|
1099
|
+
failed: [],
|
|
1100
|
+
restart: true,
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
// pnpm 跑不起来(没有自带 runtime / 断网 / 被策略拦):继续走就地覆盖,
|
|
1104
|
+
// 但必须如实告诉用户「这次不持久」,并给出能真正生效的命令。
|
|
1105
|
+
pnpmFailure = viaPnpm.error;
|
|
1106
|
+
pnpmOutput = viaPnpm.output;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
890
1109
|
let gz;
|
|
891
1110
|
try {
|
|
892
1111
|
const signal =
|
|
@@ -972,6 +1191,17 @@ async function installLatestVersion(ctx) {
|
|
|
972
1191
|
installed: true,
|
|
973
1192
|
from: currentVer,
|
|
974
1193
|
to: latest,
|
|
1194
|
+
via: "files",
|
|
1195
|
+
// registry 装法只覆盖文件 = 重启会被 profile 里锁的旧版本覆盖回去,必须如实标出来
|
|
1196
|
+
persisted: registrySpec === "",
|
|
1197
|
+
warning: registrySpec === ""
|
|
1198
|
+
? ""
|
|
1199
|
+
: "文件已覆盖到 " + latest + ",但本机是按 npm 版本装进 profile 的,profile 里锁定的还是旧版本;"
|
|
1200
|
+
+ "重启后可能退回旧号(" + (pnpmFailure || "自带 pnpm 不可用") + ")。"
|
|
1201
|
+
+ "请执行 " + manualCommand + " 后再重启,或用 设置 → 插件 里的更新。",
|
|
1202
|
+
command: manualCommand,
|
|
1203
|
+
pnpmError: pnpmFailure,
|
|
1204
|
+
pnpmOutput,
|
|
975
1205
|
files: written.length,
|
|
976
1206
|
failed,
|
|
977
1207
|
backup: backupRoot,
|
|
@@ -2887,7 +3117,7 @@ async function sumSubagentUsage(ctx, sessionId, config) {
|
|
|
2887
3117
|
function formatUsage(usage, pricing, multiplier = 2, model = "") {
|
|
2888
3118
|
// ⚠️ usage 为 null 时绝不许直接解引用(2026-10-31 全量审查)。
|
|
2889
3119
|
// sumSessionUsage 在会话还在装载 / 事件为空时会返回 null,
|
|
2890
|
-
//
|
|
3120
|
+
// 而命令处理把 usage 直接传进来 —— 下面一行 usage.byModel 就是 TypeError,
|
|
2891
3121
|
// 整个命令面板报错。命令只是查看入口,拿不到数字就说一句话。
|
|
2892
3122
|
if (usage == null || typeof usage !== "object") {
|
|
2893
3123
|
return "当前会话消耗:暂时无数据(会话可能还在加载,稍后再试)";
|
|
@@ -3077,13 +3307,17 @@ let statsInFlight = null;
|
|
|
3077
3307
|
await refreshCnCalendar(ctx, false);
|
|
3078
3308
|
});
|
|
3079
3309
|
|
|
3080
|
-
// 1)
|
|
3310
|
+
// 1) 命令行查询入口。命令名**必须是插件专属的**(2026-10-31 用户反馈:
|
|
3311
|
+
// 「这个指令跟好多插件冲突了」)—— DSH 的命令名是全局唯一的,注册重名会抛
|
|
3312
|
+
// `command "x" is already registered`,先注册的那个赢、后来的整条命令消失。
|
|
3313
|
+
// 所以直接用带作者后缀的全名,不设备用名(用户 2026-10-31 定案)。
|
|
3314
|
+
// ⚠️ DSH 的命令名规则是 COMMAND_NAME = /^[a-z][a-z0-9_-]*$/u,**只允许小写**,
|
|
3315
|
+
// 写成大写 Rannist 会在 normalizeDefinition 里抛 TypeError、整条命令消失,
|
|
3316
|
+
// 所以这里落成小写的 balance-dsh-rannist。
|
|
3081
3317
|
ctx.inject(["commands"], (commandCtx) => {
|
|
3082
|
-
// register 抛错(命令名冲突 / API 变更)只影响 /balance 命令本身,
|
|
3083
|
-
// 不能让它中止 bootstrap —— 余额行和消耗行都不走命令通道。
|
|
3084
3318
|
try {
|
|
3085
3319
|
commandCtx.commands.register({
|
|
3086
|
-
name:
|
|
3320
|
+
name: COMMAND_NAME,
|
|
3087
3321
|
description: "查询 DeepSeek 余额与当前会话消耗",
|
|
3088
3322
|
handler: async ({ agent, signal }) => {
|
|
3089
3323
|
const usage = sumSessionUsage(agent.session, effectiveConfig);
|
|
@@ -3110,7 +3344,7 @@ let statsInFlight = null;
|
|
|
3110
3344
|
},
|
|
3111
3345
|
});
|
|
3112
3346
|
} catch (error) {
|
|
3113
|
-
runQuietly("注册 /
|
|
3347
|
+
runQuietly("注册 /" + COMMAND_NAME + " 命令", () => {
|
|
3114
3348
|
throw error;
|
|
3115
3349
|
});
|
|
3116
3350
|
}
|
|
@@ -3199,8 +3433,7 @@ let statsInFlight = null;
|
|
|
3199
3433
|
wantUpdate = false;
|
|
3200
3434
|
}
|
|
3201
3435
|
if (wantUpdate) {
|
|
3202
|
-
if (req.method === "POST") {
|
|
3203
|
-
// ⚠️ 必须和设置写入同一道同源校验(2026-10-31 第二十二轮审计):
|
|
3436
|
+
if (req.method === "POST") { // ⚠️ 必须和设置写入同一道同源校验(2026-10-31 第二十二轮审计):
|
|
3204
3437
|
// 这条分支会真的下载 npm 包并覆盖插件安装目录。少了校验,本机任意网页
|
|
3205
3438
|
// 用一个 text/plain 的简单请求就能触发写盘(CSRF)。
|
|
3206
3439
|
if (!isSameOriginJsonRequest(req)) {
|