@mrrisega/dsh-remote 0.6.4-beta.10 → 0.6.4-beta.11

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/dsh-setup.mjs CHANGED
@@ -921,10 +921,26 @@ function writePatchAtomic(patchFile, text) {
921
921
  /**
922
922
  * 原子地写入插件 patch 激活行;写后**重新解析校验**,不合法立即回滚。
923
923
  * 返回 {ok, changed, reason?, basePatch}
924
+ *
925
+ * ⚠️ 硬约束:**本函数绝不能在插件已在 dsh.profile.bundles 时写入 patch 行**。
926
+ * 两个激活点同时存在时,dsh web 启动即 `duplicate loader entry id: dsh-remote-web`
927
+ * 并导致「plugin tree failed to load」——整个插件树加载失败(实测日志)。
928
+ * 因此所有 return 之前都要保证:要么只有 patch 行、要么只有 bundles 条目。
924
929
  */
925
930
  function ensurePatchActivation(patchFile, pluginLocalDir, pkgFile) {
926
931
  const original = fs.readFileSync(patchFile, "utf8");
927
- // 幂等:已经是唯一激活点就什么都不做(但要确保 bundles 里没有重复激活)
932
+ const pkgBundles = () => {
933
+ try {
934
+ const pkg = JSON.parse(fs.readFileSync(pkgFile, "utf8"));
935
+ return (pkg.dsh && pkg.dsh.profile && Array.isArray(pkg.dsh.profile.bundles))
936
+ ? pkg.dsh.profile.bundles.filter((b) => PLUGIN_ALL_IDS.includes(b)) : [];
937
+ } catch { return []; }
938
+ };
939
+ // 基座就是「插件已在 bundles」→ 保持 bundles 形态(不写 patch 行),避免两个激活点并存。
940
+ // 这一路等价于插件市场安装形态:需要重启 dsh web 才装载。
941
+ if (pkgBundles().length) {
942
+ return { ok: false, reason: "插件已由 dsh.profile.bundles 声明(保持单一激活点,不写 patch 行)", changed: false, bundleOnly: true, basePatch: original };
943
+ }
928
944
  const base = validatePatchBase(original);
929
945
  if (!base.ok) return { ok: false, reason: base.reason, changed: false, basePatch: original };
930
946
  const hasOurRow = base.insertedIds.some((id) => PLUGIN_ALL_IDS.includes(id));
@@ -972,6 +988,12 @@ function ensurePatchActivation(patchFile, pluginLocalDir, pkgFile) {
972
988
  return { ok: false, reason: "写入后校验失败,已回滚", changed: false, basePatch: original };
973
989
  }
974
990
  removeBundleEntry(pkgFile); // 单一激活点:bundles 里不能再有本插件
991
+ // 双保险:上面那次移除若没生效(并发写 package.json / 解析失败),**回滚本次 patch 行**,
992
+ // 宁可退回 bundles 形态(需重启)也绝不留"两处激活"→ dsh web 启动会 duplicate id 崩掉。
993
+ if (pkgBundles().length) {
994
+ writePatchAtomic(patchFile, original);
995
+ return { ok: false, reason: "bundles 条目未能移除,已回滚 patch 行以免重复激活", changed: false, bundleOnly: true, basePatch: original };
996
+ }
975
997
  return { ok: true, changed: true, basePatch: original };
976
998
  }
977
999
 
@@ -1137,10 +1159,14 @@ function convergePluginActivation(profileDir, pkgFile, patchFile, pluginDir, pat
1137
1159
  console.log(`✅ 插件已就绪: ${pluginLocalDir}`);
1138
1160
  return { hotPatch: true, changed: r.changed, basePatch: r.basePatch };
1139
1161
  }
1140
- // patch 行写不进去(文件形态异常)→ 回退 bundles(需重启),保证功能可用
1141
- console.warn(`⚠️ 无法写入 patch 激活行(${r.reason}),改为 bundles 形态(需重启 dsh web 生效)`);
1142
- ensureBundleEntry(pkgFile);
1143
- console.log(`✅ 插件已就绪: ${pluginLocalDir}(bundles 形态)`);
1162
+ // patch 行不能写(文件形态异常,或插件已由 bundles 声明)→ 回退 bundles(需重启),保证功能可用。
1163
+ // 这条路的输出要如实说明"装完要重启一次",不能让人以为已经热加载了。
1164
+ if (r.bundleOnly) {
1165
+ console.log("ℹ 该插件已由 dsh.profile.bundles 声明(保持单一激活点,不重复写入 patch 行)");
1166
+ } else {
1167
+ console.warn(`⚠️ 无法写入 patch 激活行(${r.reason}),改为 bundles 形态(需重启 dsh web 生效)`);
1168
+ ensureBundleEntry(pkgFile); // 回退激活点:插件仍可用,代价是必须重启一次
1169
+ }
1144
1170
  return { hotPatch: false, changed: true, basePatch: patch };
1145
1171
  }
1146
1172
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrisega/dsh-remote",
3
- "version": "0.6.4-beta.10",
3
+ "version": "0.6.4-beta.11",
4
4
  "description": "手机远程控制 DeepSeek Harness · Remote control DeepSeek Harness (dsh web) from any phone browser — 100% 全功能 App 级体验:发消息、看工具执行、审批权限、改设置、管凭据,含特权操作,免内网穿透。一条命令安装 npx @mrrisega/dsh-remote。Mobile remote control for DSH, self-host or SaaS, no server needed on LAN.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -1753,7 +1753,7 @@ const PLUGIN_ID = "dsh-remote-web";
1753
1753
  const PLUGIN_LEGACY_IDS = ["dsh-remote-ui"];
1754
1754
  const PLUGIN_ALL_IDS = [PLUGIN_ID, ...PLUGIN_LEGACY_IDS];
1755
1755
  /** 插件自身发布版本(与 dsh-remote 根包同步递增)。 */
1756
- const PLUGIN_VERSION = "0.6.4-beta.10";
1756
+ const PLUGIN_VERSION = "0.6.4-beta.11";
1757
1757
  const UPDATE_LOG = ".dsh-update.log";
1758
1758
  const UPDATE_MARKER = ".dsh-update-running";
1759
1759
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-web",
3
- "version": "0.6.4-beta.10",
3
+ "version": "0.6.4-beta.11",
4
4
  "description": "公网远程控制 DeepSeek Harness(dsh web):安装即得专属加密地址,人在外面也能用手机访问电脑上的 dsh——无需同一局域网/WiFi、无需公网 IP 与内网穿透,全程加密;手机端 100% 还原电脑体验(对话/工具/审批/设置)。技术用户可选自建服务,流量走自己的服务器。Remote control DeepSeek Harness (dsh web) from anywhere over the public internet — install, get an encrypted URL, use it from your phone on any network (dual-half cordis plugin: Settings panel + same-origin /dsh-remote routes). (2026-09 由 dsh-remote-ui 更名 / renamed from dsh-remote-ui; dsh-remote 的 dsh web 插件半,不是纯 UI/皮肤插件)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -181,3 +181,30 @@ test("回归护栏:plugin 子命令必须把激活结果 return 给 setup(
181
181
  assert.match(src, /pluginResult = await pluginCmd\(\[\]\)/, "setup 应保存 pluginCmd 的返回值");
182
182
  assert.match(src, /pluginResult\.hotPatch/, "应据 hotPatch 判断是否走热挂载形态");
183
183
  });
184
+
185
+ test("硬约束:插件已在 bundles 时**绝不**写 patch 行(两个激活点 = duplicate id 崩溃)", () => {
186
+ // 事故复盘:patch 行与插件自带 bundle patch 同时生效时,dsh web 启动即
187
+ // TypeError: duplicate loader entry id: dsh-remote-web
188
+ // Error: dsh: plugin tree failed to load: failed to apply loader entry include (cordis:include)
189
+ // 整个插件树加载失败(实测生产日志)。所以这两条激活路径必须互斥。
190
+ const profile = makeProfile({ bundles: [...PLUGIN_BUNDLES, "dsh-remote-web"] });
191
+ const relay = join(profile, "relay");
192
+ try {
193
+ const out = installPlugin(profile, relay);
194
+ assert.match(out, /已由 dsh\.profile\.bundles 声明/, "应识别并保持 bundles 形态");
195
+ assert.equal((readPatch(profile).match(/- id: dsh-remote-web/g) || []).length, 0,
196
+ "bundles 已声明时不得再写 patch 行(否则重复 ID)");
197
+ assert.ok(readBundles(profile).includes("dsh-remote-web"), "bundles 形态应保持");
198
+ } finally { cleanup(profile); }
199
+ });
200
+
201
+ test("硬约束:写入 patch 行后 bundles 必须被清空(单一激活点双向收口)", () => {
202
+ const profile = makeProfile({ bundles: [...PLUGIN_BUNDLES, "dsh-remote-web"] });
203
+ const relay = join(profile, "relay");
204
+ try {
205
+ installPlugin(profile, relay);
206
+ const patchRows = (readPatch(profile).match(/- id: dsh-remote-web/g) || []).length;
207
+ const inBundles = readBundles(profile).filter((b) => b === "dsh-remote-web").length;
208
+ assert.equal(patchRows + inBundles, 1, `激活点必须恰好一个:patch=${patchRows} bundles=${inBundles}`);
209
+ } finally { cleanup(profile); }
210
+ });