dsh-wechat-remote 1.7.12-rc.9 → 1.7.12

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Agent远程管理助手 · DSH 插件安装器
2
2
 
3
- 命令行入口用于 DSH Web;安装包内置配套插件。1.7.12-rc.9 为 Desktop 适配预览测试版,不建议用于日常节点;正式推荐版本仍为 1.7.10,以下 `latest` 命令不会安装本预览版。
3
+ 命令行入口用于 DSH Web;同一个安装包也可在 Desktop 原生插件管理器中安装。当前正式版为 1.7.12。
4
4
 
5
5
  首次安装和升级旧插件使用同一个命令:
6
6
 
@@ -24,7 +24,11 @@ DSH 的新版本、预览版本或未登记的主机架构不会仅因缺少验
24
24
 
25
25
  默认使用 `web` 配置和原来的 DSH 数据目录。自定义数据目录时,在新终端设置相同的 `DSH_HOME`,或附加 `--home "数据目录"`;使用其他配置时附加 `--profile 配置名称`。WSL、容器和其他系统用户内的 DSH,需要在对应环境中执行安装,不能跨环境替换。
26
26
 
27
- Desktop 由桌面应用的原生插件管理器负责安装和更新,不支持用 `--profile desktop` 绕过它。本预览版正在验证两端联动更新:发现另一端可更新时先提示,确认后等待空闲再处理,也可选择稍后。仅处理同一系统用户、同一数据目录下已经安装并启用的插件;需要另一端原生更新能力配合,可能需要重启。未完成联动时保留另一端原状并提示,不代表两端已更新。当前 npm `latest` 不因本预览版而获得 Desktop 支持。
27
+ Desktop 在原生插件管理器中添加 `dsh-wechat-remote`,不支持用 `--profile desktop` 绕过它。启用后可在本插件详情或“设置 → 微信连接”中检查更新;Desktop 的一键更新使用宿主原生安装流程。若更新后提示待生效,请在任务结束时完全退出并重新打开 Desktop,无需重新配对。
28
+
29
+ 发现另一端可更新时先提示,确认后等待空闲再处理,也可选择稍后。仅处理同一系统用户、同一数据目录下已经安装并启用的插件;需要另一端原生更新能力配合,可能需要重启。未完成联动时保留另一端原状并提示,不代表两端已更新。
30
+
31
+ 现有小程序的模型菜单可能未及时反映电脑上的切换;发送前请重新选择所需模型,并确认电脑已同步该选择,避免选择后立即发送。同一会话仍受 DSH 原生占用规则约束,不能由 Web 和 Desktop 同时操作。
28
32
 
29
33
  同一个 Web 节点请勿重复添加两种微信连接插件包。若原先通过原生插件管理页安装了 `dsh-wechat-remote`,仍回到该页面更新;一行安装器会提示安装来源不匹配,不会再添加另一份。
30
34
 
package/assets/plugin.tgz CHANGED
Binary file
@@ -1,22 +1,37 @@
1
1
  {
2
- "version": "1.7.12-rc.9",
2
+ "version": "1.7.12",
3
3
  "catalog": {
4
4
  "schemaVersion": 1,
5
- "revision": "installer-1.7.12-rc.9",
6
- "issuedAt": 1790574684248,
7
- "expiresAt": 1792993884248,
5
+ "revision": "installer-1.7.12",
6
+ "issuedAt": 1790610305428,
7
+ "expiresAt": 1793029505428,
8
8
  "releases": [
9
9
  {
10
- "version": "1.7.12-rc.9",
11
- "channel": "preview",
12
- "dsh": [],
13
- "platforms": [],
14
- "architectures": [],
10
+ "version": "1.7.12",
11
+ "channel": "stable",
12
+ "dsh": [
13
+ "0.1.7-rc.1"
14
+ ],
15
+ "platforms": [
16
+ "windows"
17
+ ],
18
+ "architectures": [
19
+ "x64"
20
+ ],
15
21
  "asset": {
16
- "url": "https://github.com/martinbear1/dsh-wechat-remote/releases/download/v1.7.12-rc.9/harness-remote-dsh-wechat-remote-1.7.12-rc.9.tgz",
17
- "sha256": "7983d5dd34ccde43f952b6a65def8180f4b0951c412dfb6e10d3a7ca000083b5",
18
- "bytes": 10415425
19
- }
22
+ "url": "https://github.com/martinbear1/dsh-wechat-remote/releases/download/v1.7.12/harness-remote-dsh-wechat-remote-1.7.12.tgz",
23
+ "sha256": "10781ff17bbc9e780391f2f753525240406926cbb2097ce641cf3eaf07809254",
24
+ "bytes": 10421020
25
+ },
26
+ "targets": [
27
+ {
28
+ "platform": "windows",
29
+ "arch": "x64",
30
+ "dsh": [
31
+ "0.1.7-rc.1"
32
+ ]
33
+ }
34
+ ]
20
35
  }
21
36
  ],
22
37
  "blocked": [],
@@ -710,10 +710,29 @@ function presentationProjection(key, raw) {
710
710
  }
711
711
  return { key: `agent.${name}.v1`, value };
712
712
  }
713
- function withPresentationProjections(block) {
713
+ function legacyPermissionValue(value, catalog) {
714
+ const selection = object(value), source = object(catalog);
715
+ if (Array.isArray(selection.options)) return value;
716
+ if (typeof selection.currentValue !== "string" || !Array.isArray(source.options)) return value;
717
+ const seen = /* @__PURE__ */ new Set();
718
+ const options = [];
719
+ for (const raw of source.options) {
720
+ const option = object(raw);
721
+ if (typeof option.value !== "string" || !option.value || seen.has(option.value) || typeof option.name !== "string") return value;
722
+ seen.add(option.value);
723
+ options.push({
724
+ value: option.value,
725
+ name: option.name,
726
+ ...typeof option.description === "string" ? { description: option.description } : {}
727
+ });
728
+ }
729
+ return { ...selection, options };
730
+ }
731
+ function withPresentationProjections(block, permissionCatalog) {
714
732
  const source = object(block);
715
733
  if (!source.values) return block;
716
734
  const values = { ...object(source.values) };
735
+ if (Object.hasOwn(values, "permissions")) values.permissions = legacyPermissionValue(values.permissions, permissionCatalog);
717
736
  for (const [key, value] of Object.entries(values)) {
718
737
  const projected = presentationProjection(key, value);
719
738
  if (projected) values[projected.key] = projected.value;
@@ -800,6 +819,94 @@ async function invokeHostRemote(ctx, gateway, request) {
800
819
  return projectedSubagentCatalog(parent, projection, listing?.items);
801
820
  }
802
821
 
822
+ // src/legacy-model-identity.ts
823
+ var LEGACY_MODEL_FORMAT = "legacy-provider-model-v1";
824
+ var NATIVE_MODEL_FORMAT = "native-v1";
825
+ var separator = " \xB7 ";
826
+ function record2(value) {
827
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
828
+ }
829
+ function failure(message) {
830
+ throw Object.assign(new Error(message), { code: "adapter/model-identity-invalid" });
831
+ }
832
+ function modelIdentityFormat(request) {
833
+ const format = request.modelIdentity ?? LEGACY_MODEL_FORMAT;
834
+ if (format !== LEGACY_MODEL_FORMAT && format !== NATIVE_MODEL_FORMAT) {
835
+ failure("\u4E0D\u652F\u6301\u6B64\u6A21\u578B\u6807\u8BC6\u683C\u5F0F\uFF1B\u672A\u66F4\u6539\u6A21\u578B");
836
+ }
837
+ return format;
838
+ }
839
+ function component(value) {
840
+ return value.replace(/%/g, "%25").replace(/·/g, "%B7").replace(/[\u0000-\u001f\u007f]/g, (char) => "%" + char.charCodeAt(0).toString(16).padStart(2, "0").toUpperCase());
841
+ }
842
+ function legacyModelAlias(provider, model) {
843
+ return component(provider) + separator + component(model);
844
+ }
845
+ function groupsOf(catalog) {
846
+ const groups = record2(catalog)?.groups;
847
+ if (!Array.isArray(groups)) failure("\u4E3B\u673A\u6A21\u578B\u76EE\u5F55\u65E0\u6548\uFF1B\u8BF7\u5237\u65B0\u6A21\u578B\u5217\u8868");
848
+ const providers = /* @__PURE__ */ new Set();
849
+ for (const candidate of groups) {
850
+ const group = record2(candidate);
851
+ if (!group || typeof group.id !== "string" || !group.id || providers.has(group.id) || !Array.isArray(group.models)) {
852
+ failure("\u4E3B\u673A\u63D0\u4F9B\u65B9\u76EE\u5F55\u65E0\u6548\uFF1B\u8BF7\u5237\u65B0\u6A21\u578B\u5217\u8868");
853
+ }
854
+ providers.add(group.id);
855
+ const models = /* @__PURE__ */ new Set();
856
+ for (const candidate2 of group.models) {
857
+ const model = record2(candidate2);
858
+ if (!model || typeof model.id !== "string" || !model.id || models.has(model.id)) {
859
+ failure("\u4E3B\u673A\u6A21\u578B\u76EE\u5F55\u5305\u542B\u65E0\u6548\u6216\u91CD\u590D\u6807\u8BC6\uFF1B\u672A\u731C\u6D4B\u6A21\u578B");
860
+ }
861
+ models.add(model.id);
862
+ }
863
+ }
864
+ return groups;
865
+ }
866
+ function legacyModelSelection(value) {
867
+ const selection = record2(value);
868
+ if (!selection || typeof selection.provider !== "string" || typeof selection.model !== "string") return value;
869
+ return { ...selection, model: legacyModelAlias(selection.provider, selection.model), nativeModel: selection.model };
870
+ }
871
+ function presentModelCatalog(catalog, current, format) {
872
+ const groups = groupsOf(catalog);
873
+ const aliases = format === LEGACY_MODEL_FORMAT && groups.length > 1;
874
+ const selection = (value) => {
875
+ const selected = record2(value);
876
+ const missingProvider = groups.length === 1 && selected && selected.provider !== groups[0].id;
877
+ return format === LEGACY_MODEL_FORMAT && (aliases || missingProvider) ? legacyModelSelection(value) : value;
878
+ };
879
+ return {
880
+ ...catalog,
881
+ groups: aliases ? groups.map((group) => ({ ...group, models: group.models.map((model) => ({
882
+ ...model,
883
+ id: legacyModelAlias(group.id, model.id),
884
+ name: `${group.name || group.id}${separator}${model.name || model.id}`,
885
+ nativeModel: model.id
886
+ })) })) : groups,
887
+ default: selection(catalog.default),
888
+ current: selection(current),
889
+ // Per-response negotiation (not OS/version guessing or global state).
890
+ // A future Mini requests native-v1 and MUST verify this acknowledgement.
891
+ modelIdentity: { version: 1, format, aliases, supportedFormats: [LEGACY_MODEL_FORMAT, NATIVE_MODEL_FORMAT] }
892
+ };
893
+ }
894
+ function needsModelAliasResolution(request) {
895
+ return modelIdentityFormat(request) === LEGACY_MODEL_FORMAT && typeof request.model === "string" && request.model.includes(separator);
896
+ }
897
+ function resolveModelAlias(catalog, request) {
898
+ const groups = groupsOf(catalog);
899
+ const group = groups.find((group2) => group2.id === request.provider);
900
+ if (!group || typeof request.model !== "string") failure("\u8BE5\u63D0\u4F9B\u65B9\u5DF2\u4E0D\u53EF\u7528\uFF1B\u8BF7\u5237\u65B0\u6A21\u578B\u5217\u8868\u540E\u91CD\u65B0\u9009\u62E9");
901
+ const models = group.models;
902
+ const raw = models.find((model) => model.id === request.model);
903
+ const aliased = models.find((model) => legacyModelAlias(group.id, model.id) === request.model);
904
+ if (raw && aliased && raw.id !== aliased.id) failure("\u6A21\u578B\u6807\u8BC6\u6709\u6B67\u4E49\uFF1B\u8BF7\u5237\u65B0\u6A21\u578B\u5217\u8868\u540E\u91CD\u65B0\u9009\u62E9");
905
+ if (aliased) return { model: aliased.id, aliased: true };
906
+ if (raw) return { model: raw.id, aliased: false };
907
+ failure("\u6A21\u578B\u6216\u63D0\u4F9B\u65B9\u5DF2\u53D8\u5316\uFF1B\u8BF7\u5237\u65B0\u6A21\u578B\u5217\u8868\u540E\u91CD\u65B0\u9009\u62E9\uFF0C\u672A\u66F4\u6539\u6A21\u578B");
908
+ }
909
+
803
910
  // src/dsh-protocol-compat.ts
804
911
  var SINGLE_REQUEST_METHODS = /* @__PURE__ */ new Set([
805
912
  "session.attachment",
@@ -809,7 +916,6 @@ var SINGLE_REQUEST_METHODS = /* @__PURE__ */ new Set([
809
916
  "session.openWorkspacePath",
810
917
  "session.rename",
811
918
  "session.search",
812
- "session.selectModel",
813
919
  "session.updateQueue",
814
920
  "workspace.archiveSession",
815
921
  "workspace.create",
@@ -878,6 +984,7 @@ function planLegacyRpc(request) {
878
984
  if (method === "session.models") {
879
985
  return { kind: "session-models", request: payload };
880
986
  }
987
+ if (method === "session.selectModel") return { kind: "session-select-model", request: payload };
881
988
  if (method === "session.prompt") {
882
989
  const parts = Array.isArray(payload.content) ? payload.content : [];
883
990
  const first = recordOf(parts[0]);
@@ -1002,6 +1109,14 @@ function resolveTypertGateway(ctx) {
1002
1109
  const query = ctx.get("sessionQuery");
1003
1110
  const agents = ctx.get("agents");
1004
1111
  return {
1112
+ permissionCatalog: () => {
1113
+ try {
1114
+ const service = ctx.get("permissionPresets");
1115
+ return typeof service?.catalog === "function" ? service.catalog() : void 0;
1116
+ } catch {
1117
+ return void 0;
1118
+ }
1119
+ },
1005
1120
  ...typeof agents?.get === "function" ? { canFollowSession: (id) => Boolean(agents.get(id)) } : {},
1006
1121
  ...typeof query?.observeSession === "function" ? { readSnapshot: async (address, maxMessages, signal) => {
1007
1122
  const id = address.kind === "session" ? address.sessionId : address.childSessionId;
@@ -1072,8 +1187,8 @@ function errorResult(error) {
1072
1187
  }
1073
1188
  function historyEvents(records) {
1074
1189
  if (!Array.isArray(records)) return [];
1075
- return records.flatMap((record2) => {
1076
- const row = recordOf(record2);
1190
+ return records.flatMap((record3) => {
1191
+ const row = recordOf(record3);
1077
1192
  const event = recordOf(row?.event);
1078
1193
  if (row?.type === "event" && event) return [{ event }];
1079
1194
  if (row?.type !== "chunks" || !event) return [];
@@ -1181,7 +1296,7 @@ function createHistoryPageReader(gateway, sessionId, signal) {
1181
1296
  return {
1182
1297
  events: historyEvents(first.records),
1183
1298
  hasMore: first.hasMore === true,
1184
- projections: withPresentationProjections(first.projections),
1299
+ projections: withPresentationProjections(first.projections, gateway.permissionCatalog?.()),
1185
1300
  historyEndSeq: first.cursor
1186
1301
  };
1187
1302
  }
@@ -1189,7 +1304,7 @@ function createHistoryPageReader(gateway, sessionId, signal) {
1189
1304
  return {
1190
1305
  ...page,
1191
1306
  ...latest ? {
1192
- projections: withPresentationProjections(first.projections),
1307
+ projections: withPresentationProjections(first.projections, gateway.permissionCatalog?.()),
1193
1308
  historyEndSeq: first.cursor
1194
1309
  } : {}
1195
1310
  };
@@ -1233,6 +1348,7 @@ async function permissionCommandValue(gateway, plan, signal, readHistory, flushP
1233
1348
  return plan.nativeReceipt ? command : { accepted: true, command: true, permission: current, commandId: command?.commandId };
1234
1349
  }
1235
1350
  async function sessionModelsValue(gateway, request, signal) {
1351
+ const format = modelIdentityFormat(request);
1236
1352
  const sessionId = typeof request.sessionId === "string" ? request.sessionId : "";
1237
1353
  if (!sessionId) throw new Error("session.models requires sessionId");
1238
1354
  const address = await resolveDshSessionAddress(gateway, sessionId, signal);
@@ -1251,7 +1367,23 @@ async function sessionModelsValue(gateway, request, signal) {
1251
1367
  }
1252
1368
  const current = selection?.next ?? selection?.lastUsed ?? catalog.default;
1253
1369
  if (!recordOf(current)) throw new Error("DSH returned no usable model selection");
1254
- return { ...catalog, current };
1370
+ return presentModelCatalog(catalog, current, format);
1371
+ }
1372
+ async function selectModelValue(gateway, request, signal) {
1373
+ signal.throwIfAborted();
1374
+ modelIdentityFormat(request);
1375
+ const { modelIdentity: _format, ...native } = request;
1376
+ let aliased = false;
1377
+ if (needsModelAliasResolution(request)) {
1378
+ const catalog = await gateway.invoke({ namespace: "session", method: "modelCatalog", args: {}, signal });
1379
+ const resolved = resolveModelAlias(catalog, request);
1380
+ native.model = resolved.model;
1381
+ aliased = resolved.aliased;
1382
+ }
1383
+ signal.throwIfAborted();
1384
+ const result = await gateway.invoke({ namespace: "session", method: "selectModel", args: { request: native }, signal });
1385
+ const receipt = recordOf(result);
1386
+ return aliased && receipt && recordOf(receipt.selected) ? { ...receipt, selected: legacyModelSelection(receipt.selected) } : result;
1255
1387
  }
1256
1388
  async function invokeLegacyRpc(gateway, request, options) {
1257
1389
  let result;
@@ -1262,6 +1394,7 @@ async function invokeLegacyRpc(gateway, request, options) {
1262
1394
  else if (plan.kind === "workspace-list") value = await workspaceValue(gateway, options.signal);
1263
1395
  else if (plan.kind === "session-history") value = await historyValue(gateway, plan.request, options.signal);
1264
1396
  else if (plan.kind === "session-models") value = await sessionModelsValue(gateway, plan.request, options.signal);
1397
+ else if (plan.kind === "session-select-model") value = await selectModelValue(gateway, plan.request, options.signal);
1265
1398
  else if (plan.kind === "permission-command") value = await permissionCommandValue(
1266
1399
  gateway,
1267
1400
  plan,
@@ -1281,8 +1414,8 @@ async function invokeLegacyRpc(gateway, request, options) {
1281
1414
  });
1282
1415
  break;
1283
1416
  } catch (error) {
1284
- const failure = recordOf(error);
1285
- if (request.method !== "session.list" || attempt >= 2 || failure?.code !== "SESSION_QUERY_PERSISTENCE_FAILED" || typeof failure.message !== "string" || !/ENOENT.*scandir.*[\\/]\.dsh-mkdir-[^\\/]+$/.test(failure.message)) throw error;
1417
+ const failure2 = recordOf(error);
1418
+ if (request.method !== "session.list" || attempt >= 2 || failure2?.code !== "SESSION_QUERY_PERSISTENCE_FAILED" || typeof failure2.message !== "string" || !/ENOENT.*scandir.*[\\/]\.dsh-mkdir-[^\\/]+$/.test(failure2.message)) throw error;
1286
1419
  await delay(25 * (attempt + 1), void 0, { signal: options.signal });
1287
1420
  }
1288
1421
  }
@@ -2,6 +2,8 @@ import type { Context } from '@deepseek-ai/cordis';
2
2
  import { resolveDshSessionAddress } from './dsh-session-address.js';
3
3
  type JsonRecord = Record<string, unknown>;
4
4
  export interface TypertGatewayLike {
5
+ /** Public synchronous native metadata, not a session write or a guessed list. */
6
+ readonly permissionCatalog?: () => unknown;
5
7
  /** Native query snapshots do not promote a cold session into a writer. */
6
8
  readonly readSnapshot?: (address: Awaited<ReturnType<typeof resolveDshSessionAddress>>, maxMessages: number, signal: AbortSignal) => Promise<unknown>;
7
9
  readonly canFollowSession?: (sessionId: string) => boolean;
@@ -58,6 +60,9 @@ type InvocationPlan = {
58
60
  } | {
59
61
  readonly kind: 'session-models';
60
62
  readonly request: JsonRecord;
63
+ } | {
64
+ readonly kind: 'session-select-model';
65
+ readonly request: JsonRecord;
61
66
  } | {
62
67
  readonly kind: 'session-history';
63
68
  readonly request: JsonRecord;
@@ -2,6 +2,7 @@ import { setTimeout as delay } from 'node:timers/promises';
2
2
  import { resolveDshSessionAddress } from './dsh-session-address.js';
3
3
  import { withPresentationProjections } from './session-presentation.js';
4
4
  import { openHostEvents, invokeHostRemote } from './dsh-host-contract.js';
5
+ import { modelIdentityFormat, presentModelCatalog, needsModelAliasResolution, resolveModelAlias, legacyModelSelection } from './legacy-model-identity.js';
5
6
  const SINGLE_REQUEST_METHODS = new Set([
6
7
  'session.attachment',
7
8
  'session.cancel',
@@ -10,7 +11,6 @@ const SINGLE_REQUEST_METHODS = new Set([
10
11
  'session.openWorkspacePath',
11
12
  'session.rename',
12
13
  'session.search',
13
- 'session.selectModel',
14
14
  'session.updateQueue',
15
15
  'workspace.archiveSession',
16
16
  'workspace.create',
@@ -88,6 +88,8 @@ export function planLegacyRpc(request) {
88
88
  if (method === 'session.models') {
89
89
  return { kind: 'session-models', request: payload };
90
90
  }
91
+ if (method === 'session.selectModel')
92
+ return { kind: 'session-select-model', request: payload };
91
93
  if (method === 'session.prompt') {
92
94
  // Released clients carry the permission picker through session.prompt.
93
95
  // DSH's native UI uses commands.execute: sending this to the model neither
@@ -217,6 +219,17 @@ export function resolveTypertGateway(ctx) {
217
219
  const query = ctx.get('sessionQuery');
218
220
  const agents = ctx.get('agents');
219
221
  return {
222
+ permissionCatalog: () => {
223
+ // Read on demand so hot-loaded/withdrawn catalog contributions cannot
224
+ // leave stale choices cached in the adapter. No selection is performed.
225
+ try {
226
+ const service = ctx.get('permissionPresets');
227
+ return typeof service?.catalog === 'function' ? service.catalog() : undefined;
228
+ }
229
+ catch {
230
+ return undefined;
231
+ } // Catalog failure must not make chat/history unreadable.
232
+ },
220
233
  ...(typeof agents?.get === 'function' ? { canFollowSession: (id) => Boolean(agents.get(id)) } : {}),
221
234
  ...(typeof query?.observeSession === 'function' ? { readSnapshot: async (address, maxMessages, signal) => {
222
235
  const id = address.kind === 'session' ? address.sessionId : address.childSessionId;
@@ -429,7 +442,7 @@ export function createHistoryPageReader(gateway, sessionId, signal) {
429
442
  return {
430
443
  events: historyEvents(first.records),
431
444
  hasMore: first.hasMore === true,
432
- projections: withPresentationProjections(first.projections),
445
+ projections: withPresentationProjections(first.projections, gateway.permissionCatalog?.()),
433
446
  historyEndSeq: first.cursor,
434
447
  };
435
448
  }
@@ -437,7 +450,7 @@ export function createHistoryPageReader(gateway, sessionId, signal) {
437
450
  return {
438
451
  ...page,
439
452
  ...(latest ? {
440
- projections: withPresentationProjections(first.projections),
453
+ projections: withPresentationProjections(first.projections, gateway.permissionCatalog?.()),
441
454
  historyEndSeq: first.cursor,
442
455
  } : {}),
443
456
  };
@@ -492,6 +505,7 @@ export async function invokeLegacyPermissionRpc(gateway, request, signal, readHi
492
505
  return { type: 'server-response', rpcId: request.rpcId, result };
493
506
  }
494
507
  async function sessionModelsValue(gateway, request, signal) {
508
+ const format = modelIdentityFormat(request);
495
509
  const sessionId = typeof request.sessionId === 'string' ? request.sessionId : '';
496
510
  if (!sessionId)
497
511
  throw new Error('session.models requires sessionId');
@@ -514,7 +528,26 @@ async function sessionModelsValue(gateway, request, signal) {
514
528
  const current = selection?.next ?? selection?.lastUsed ?? catalog.default;
515
529
  if (!recordOf(current))
516
530
  throw new Error('DSH returned no usable model selection');
517
- return { ...catalog, current };
531
+ return presentModelCatalog(catalog, current, format);
532
+ }
533
+ async function selectModelValue(gateway, request, signal) {
534
+ signal.throwIfAborted();
535
+ modelIdentityFormat(request);
536
+ const { modelIdentity: _format, ...native } = request;
537
+ let aliased = false;
538
+ if (needsModelAliasResolution(request)) {
539
+ const catalog = await gateway.invoke({ namespace: 'session', method: 'modelCatalog', args: {}, signal });
540
+ const resolved = resolveModelAlias(catalog, request);
541
+ native.model = resolved.model;
542
+ aliased = resolved.aliased;
543
+ }
544
+ signal.throwIfAborted();
545
+ // Exactly one native mutation. DSH still validates provider, reasoning effort,
546
+ // session ownership and permissions. A receipt is not permission to replay.
547
+ const result = await gateway.invoke({ namespace: 'session', method: 'selectModel', args: { request: native }, signal });
548
+ const receipt = recordOf(result);
549
+ return aliased && receipt && recordOf(receipt.selected)
550
+ ? { ...receipt, selected: legacyModelSelection(receipt.selected) } : result;
518
551
  }
519
552
  /** Execute one stable request and restore the pre-0.1.2 HTTP envelope. */
520
553
  export async function invokeLegacyRpc(gateway, request, options) {
@@ -530,6 +563,8 @@ export async function invokeLegacyRpc(gateway, request, options) {
530
563
  value = await historyValue(gateway, plan.request, options.signal);
531
564
  else if (plan.kind === 'session-models')
532
565
  value = await sessionModelsValue(gateway, plan.request, options.signal);
566
+ else if (plan.kind === 'session-select-model')
567
+ value = await selectModelValue(gateway, plan.request, options.signal);
533
568
  else if (plan.kind === 'permission-command')
534
569
  value = await permissionCommandValue(gateway, plan, options.signal, (sessionId, signal) => historyValue(gateway, { sessionId, maxMessages: 1 }, signal), options.flushPermission);
535
570
  else {
@@ -1,6 +1,6 @@
1
1
  import { resolveTypertGateway } from './dsh-protocol-compat.js';
2
2
  import { resolveDshSessionAddress, isSessionReadError } from './dsh-session-address.js';
3
- import { presentationProjection } from './session-presentation.js';
3
+ import { presentationProjection, legacyPermissionValue } from './session-presentation.js';
4
4
  import { AssistantStreamCompatibility, assistantRecordPresentation } from './assistant-stream-compat.js';
5
5
  import { resourcePresentation } from './agent-resources.js';
6
6
  import { TurnActivityCompatibility } from './turn-activity.js';
@@ -160,6 +160,7 @@ export class DshRealtimeCompatibility {
160
160
  kind: path.endsWith('.mux') ? 'mux' : 'host',
161
161
  lifetime: new AbortController(),
162
162
  sessionLifetimes: new Map(),
163
+ permissionProjections: new Map(),
163
164
  };
164
165
  this.sockets.add(state);
165
166
  if (state.kind === 'host')
@@ -314,6 +315,7 @@ export class DshRealtimeCompatibility {
314
315
  controller.abort(new Error('socket closed'));
315
316
  }
316
317
  state.sessionLifetimes.clear();
318
+ state.permissionProjections.clear();
317
319
  if (state.clientId) {
318
320
  for (const [eventId, values] of this.pending) {
319
321
  const next = values.filter(value => value.clientId !== state.clientId);
@@ -336,6 +338,17 @@ export class DshRealtimeCompatibility {
336
338
  state.socket.close(1009, 'realtime consumer is too slow');
337
339
  return;
338
340
  }
341
+ if (payload.type === 'session/projection' && payload.key === 'permissions' && typeof payload.sessionId === 'string') {
342
+ const previous = state.permissionProjections.get(payload.sessionId);
343
+ if (!previous || Number(payload.seq) >= Number(previous.seq)) {
344
+ state.permissionProjections.delete(payload.sessionId);
345
+ state.permissionProjections.set(payload.sessionId, payload);
346
+ if (state.permissionProjections.size > MAX_SESSION_SUBSCRIPTIONS) {
347
+ state.permissionProjections.delete(state.permissionProjections.keys().next().value);
348
+ }
349
+ }
350
+ payload = { ...payload, value: legacyPermissionValue(payload.value, this.gateway().permissionCatalog?.()) };
351
+ }
339
352
  state.socket.send(JSON.stringify({
340
353
  type: 'server-event',
341
354
  ...(rpcId ? { rpcId } : {}),
@@ -376,6 +389,7 @@ export class DshRealtimeCompatibility {
376
389
  if (!frame)
377
390
  return;
378
391
  if (frame.type === 'baseline') {
392
+ state.permissionProjections.clear();
379
393
  const value = recordOf(frame.value);
380
394
  const queues = recordOf(value?.queues) ?? {};
381
395
  for (const [sessionId, items] of Object.entries(queues)) {
@@ -531,10 +545,25 @@ export class DshRealtimeCompatibility {
531
545
  const frame = recordOf(raw);
532
546
  if (!frame)
533
547
  continue;
548
+ if (frame.type === 'emit' && frame.event === 'permission-presets/catalog-changed') {
549
+ // The shipped phone has no catalog listener. Re-present its known
550
+ // selections at their original sequence, with the new live choices.
551
+ // It accepts equal-seq presentation refreshes; never invent a native
552
+ // permission change or acquire a session merely to refresh the menu.
553
+ for (const target of this.sockets) {
554
+ for (const payload of [...target.permissionProjections.values()])
555
+ this.send(target, payload);
556
+ }
557
+ continue;
558
+ }
534
559
  if (frame.type === 'ready') {
535
560
  state.clientId = stringOf(frame.clientId);
536
561
  continue;
537
562
  }
563
+ if (frame.type === 'emit' && frame.event === 'api-session/removed' && Array.isArray(frame.args)) {
564
+ for (const target of this.sockets)
565
+ target.permissionProjections.delete(stringOf(frame.args[0]));
566
+ }
538
567
  const conflict = this.readonlyConflict(frame);
539
568
  // Temporary phone-notification mitigation only; see readonlyConflict.
540
569
  // The original frame and native Desktop activation/lock stay untouched.
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Compatibility ONLY for the released Mini Program's model-only equality check.
3
+ * Native IDs, projections, defaults on disk, and slash-style Remote RPCs stay native.
4
+ * No mutable/global alias table: Web/Desktop, reconnect and hot reload cannot share
5
+ * stale identity state. Labels never determine routing; exact native IDs do.
6
+ */
7
+ type RecordValue = Record<string, unknown>;
8
+ export declare const LEGACY_MODEL_FORMAT = "legacy-provider-model-v1";
9
+ export declare const NATIVE_MODEL_FORMAT = "native-v1";
10
+ export declare function modelIdentityFormat(request: RecordValue): string;
11
+ export declare function legacyModelAlias(provider: string, model: string): string;
12
+ export declare function legacyModelSelection(value: unknown): unknown;
13
+ export declare function presentModelCatalog(catalog: RecordValue, current: unknown, format: string): RecordValue;
14
+ export declare function needsModelAliasResolution(request: RecordValue): boolean;
15
+ export declare function resolveModelAlias(catalog: unknown, request: RecordValue): {
16
+ model: string;
17
+ aliased: boolean;
18
+ };
19
+ export {};
@@ -0,0 +1,100 @@
1
+ export const LEGACY_MODEL_FORMAT = 'legacy-provider-model-v1';
2
+ export const NATIVE_MODEL_FORMAT = 'native-v1';
3
+ const separator = ' · ';
4
+ function record(value) {
5
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null;
6
+ }
7
+ function failure(message) {
8
+ throw Object.assign(new Error(message), { code: 'adapter/model-identity-invalid' });
9
+ }
10
+ export function modelIdentityFormat(request) {
11
+ const format = request.modelIdentity ?? LEGACY_MODEL_FORMAT;
12
+ if (format !== LEGACY_MODEL_FORMAT && format !== NATIVE_MODEL_FORMAT) {
13
+ failure('不支持此模型标识格式;未更改模型');
14
+ }
15
+ return format;
16
+ }
17
+ function component(value) {
18
+ // Escape the separator character and escape character themselves, retaining
19
+ // readable Unicode. Thus IDs containing punctuation cannot collide.
20
+ return value.replace(/%/g, '%25').replace(/·/g, '%B7')
21
+ .replace(/[\u0000-\u001f\u007f]/g, char => '%' + char.charCodeAt(0).toString(16).padStart(2, '0').toUpperCase());
22
+ }
23
+ export function legacyModelAlias(provider, model) {
24
+ return component(provider) + separator + component(model);
25
+ }
26
+ function groupsOf(catalog) {
27
+ const groups = record(catalog)?.groups;
28
+ if (!Array.isArray(groups))
29
+ failure('主机模型目录无效;请刷新模型列表');
30
+ const providers = new Set();
31
+ for (const candidate of groups) {
32
+ const group = record(candidate);
33
+ if (!group || typeof group.id !== 'string' || !group.id || providers.has(group.id) || !Array.isArray(group.models)) {
34
+ failure('主机提供方目录无效;请刷新模型列表');
35
+ }
36
+ providers.add(group.id);
37
+ const models = new Set();
38
+ for (const candidate of group.models) {
39
+ const model = record(candidate);
40
+ if (!model || typeof model.id !== 'string' || !model.id || models.has(model.id)) {
41
+ failure('主机模型目录包含无效或重复标识;未猜测模型');
42
+ }
43
+ models.add(model.id);
44
+ }
45
+ }
46
+ return groups;
47
+ }
48
+ export function legacyModelSelection(value) {
49
+ const selection = record(value);
50
+ if (!selection || typeof selection.provider !== 'string' || typeof selection.model !== 'string')
51
+ return value;
52
+ return { ...selection, model: legacyModelAlias(selection.provider, selection.model), nativeModel: selection.model };
53
+ }
54
+ export function presentModelCatalog(catalog, current, format) {
55
+ const groups = groupsOf(catalog);
56
+ const aliases = format === LEGACY_MODEL_FORMAT && groups.length > 1;
57
+ const selection = (value) => {
58
+ const selected = record(value);
59
+ // A removed provider must not accidentally check the sole remaining
60
+ // provider's same-named model. Keep the unavailable native intent visible.
61
+ const missingProvider = groups.length === 1 && selected && selected.provider !== groups[0].id;
62
+ return format === LEGACY_MODEL_FORMAT && (aliases || missingProvider) ? legacyModelSelection(value) : value;
63
+ };
64
+ return {
65
+ ...catalog,
66
+ groups: aliases ? groups.map(group => ({ ...group, models: group.models.map(model => ({
67
+ ...model,
68
+ id: legacyModelAlias(group.id, model.id),
69
+ name: `${group.name || group.id}${separator}${model.name || model.id}`,
70
+ nativeModel: model.id,
71
+ })) })) : groups,
72
+ default: selection(catalog.default),
73
+ current: selection(current),
74
+ // Per-response negotiation (not OS/version guessing or global state).
75
+ // A future Mini requests native-v1 and MUST verify this acknowledgement.
76
+ modelIdentity: { version: 1, format, aliases, supportedFormats: [LEGACY_MODEL_FORMAT, NATIVE_MODEL_FORMAT] },
77
+ };
78
+ }
79
+ export function needsModelAliasResolution(request) {
80
+ return modelIdentityFormat(request) === LEGACY_MODEL_FORMAT
81
+ && typeof request.model === 'string' && request.model.includes(separator);
82
+ }
83
+ export function resolveModelAlias(catalog, request) {
84
+ const groups = groupsOf(catalog);
85
+ const group = groups.find(group => group.id === request.provider);
86
+ if (!group || typeof request.model !== 'string')
87
+ failure('该提供方已不可用;请刷新模型列表后重新选择');
88
+ const models = group.models;
89
+ const raw = models.find(model => model.id === request.model);
90
+ const aliased = models.find(model => legacyModelAlias(group.id, model.id) === request.model);
91
+ // Old clients can retain raw IDs over a plugin upgrade. If a real raw ID is
92
+ // also another model's alias, neither interpretation is safe. Require refresh.
93
+ if (raw && aliased && raw.id !== aliased.id)
94
+ failure('模型标识有歧义;请刷新模型列表后重新选择');
95
+ if (aliased)
96
+ return { model: aliased.id, aliased: true };
97
+ if (raw)
98
+ return { model: raw.id, aliased: false };
99
+ failure('模型或提供方已变化;请刷新模型列表后重新选择,未更改模型');
100
+ }
@@ -1,2 +1,2 @@
1
1
  /** Build identity stays with loaded code, even after package.json is replaced. */
2
- export declare const PLUGIN_VERSION = "1.7.12-rc.9";
2
+ export declare const PLUGIN_VERSION = "1.7.12";
@@ -1,2 +1,2 @@
1
1
  /** Build identity stays with loaded code, even after package.json is replaced. */
2
- export const PLUGIN_VERSION = '1.7.12-rc.9';
2
+ export const PLUGIN_VERSION = '1.7.12';
@@ -2,4 +2,11 @@ export declare function presentationProjection(key: string, raw: unknown): {
2
2
  key: string;
3
3
  value: unknown;
4
4
  } | null;
5
- export declare function withPresentationProjections(block: unknown): unknown;
5
+ /** DSH 0.1.7 moved selectable permissions out of the Session projection into
6
+ * permissionPresets.catalog(). Released phones still read permissions.options.
7
+ * Join that LIVE process catalog only at our legacy transport boundary: never
8
+ * persist options into native history, invent default choices, or change the
9
+ * current permission. Older hosts already supplying options are unchanged.
10
+ * Retire this bridge when supported phone clients read the native catalog. */
11
+ export declare function legacyPermissionValue(value: unknown, catalog: unknown): unknown;
12
+ export declare function withPresentationProjections(block: unknown, permissionCatalog?: unknown): unknown;
@@ -40,11 +40,40 @@ export function presentationProjection(key, raw) {
40
40
  }
41
41
  return { key: `agent.${name}.v1`, value };
42
42
  }
43
- export function withPresentationProjections(block) {
43
+ /** DSH 0.1.7 moved selectable permissions out of the Session projection into
44
+ * permissionPresets.catalog(). Released phones still read permissions.options.
45
+ * Join that LIVE process catalog only at our legacy transport boundary: never
46
+ * persist options into native history, invent default choices, or change the
47
+ * current permission. Older hosts already supplying options are unchanged.
48
+ * Retire this bridge when supported phone clients read the native catalog. */
49
+ export function legacyPermissionValue(value, catalog) {
50
+ const selection = object(value), source = object(catalog);
51
+ // A legacy Session may carry its own complete choices. They remain the
52
+ // authority on that host; this bridge only supplies the missing modern list.
53
+ if (Array.isArray(selection.options))
54
+ return value;
55
+ if (typeof selection.currentValue !== 'string' || !Array.isArray(source.options))
56
+ return value;
57
+ const seen = new Set();
58
+ const options = [];
59
+ for (const raw of source.options) {
60
+ const option = object(raw);
61
+ if (typeof option.value !== 'string' || !option.value || seen.has(option.value)
62
+ || typeof option.name !== 'string')
63
+ return value;
64
+ seen.add(option.value);
65
+ options.push({ value: option.value, name: option.name,
66
+ ...(typeof option.description === 'string' ? { description: option.description } : {}) });
67
+ }
68
+ return { ...selection, options };
69
+ }
70
+ export function withPresentationProjections(block, permissionCatalog) {
44
71
  const source = object(block);
45
72
  if (!source.values)
46
73
  return block;
47
74
  const values = { ...object(source.values) };
75
+ if (Object.hasOwn(values, 'permissions'))
76
+ values.permissions = legacyPermissionValue(values.permissions, permissionCatalog);
48
77
  for (const [key, value] of Object.entries(values)) {
49
78
  const projected = presentationProjection(key, value);
50
79
  if (projected)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-remote/dsh-wechat-remote",
3
- "version": "1.7.12-rc.9",
3
+ "version": "1.7.12",
4
4
  "description": "Agent远程管理助手:支持 Windows、macOS 与 Linux 上的 DeepSeek Harness,提供微信配对、安全局域网直连及端到端加密公网连接",
5
5
  "private": true,
6
6
  "license": "MIT",
@@ -107,7 +107,7 @@
107
107
  "test:attachment": "node scripts/test-attachment.mjs && node scripts/test-public-object-client.mjs && node scripts/test-agent-inputs.mjs && node --test scripts/test-object-transfer-lifecycle.mjs scripts/test-object-entry-recovery.mjs",
108
108
  "test:gate-ports": "node scripts/test-gate-ports.mjs",
109
109
  "test:dsh-runtime": "node scripts/test-dsh-runtime.mjs && node --test scripts/test-independent-hosts.mjs scripts/test-node-storage.mjs",
110
- "test:dsh-protocol-compat": "node scripts/test-dsh-protocol-compat.mjs && node scripts/test-dsh-session-address.mjs && node scripts/test-session-presentation.mjs && node --test scripts/test-preset-settings.mjs",
110
+ "test:dsh-protocol-compat": "node scripts/test-dsh-protocol-compat.mjs && node scripts/test-dsh-session-address.mjs && node scripts/test-session-presentation.mjs && node --test scripts/test-preset-settings.mjs scripts/test-session-controls-compat.mjs scripts/test-legacy-model-identity.mjs",
111
111
  "test:dsh-realtime-compat": "node scripts/test-dsh-realtime-compat.mjs && node scripts/test-assistant-stream.mjs && node scripts/test-agent-resources.mjs && node --test scripts/test-session-export.mjs scripts/test-session-directory-realtime.mjs scripts/test-cold-session-readonly.mjs scripts/test-passive-writer-notification.mjs",
112
112
  "test:dsh-compatibility-api": "node scripts/test-dsh-compatibility-api.mjs && node --test scripts/test-host-compatibility.mjs scripts/test-installer-native-restore.mjs",
113
113
  "test:public-relay": "node --test scripts/test-tunnel-send-queue.mjs scripts/test-tunnel-request-lifecycle.mjs && node scripts/test-public-relay.mjs && node scripts/test-relay-liveness.mjs",
@@ -115,6 +115,8 @@
115
115
  "test:native-pairing": "node scripts/test-native-pairing.mjs",
116
116
  "test:native-preset-settings": "node scripts/test-native-preset-settings.mjs",
117
117
  "test:native-control-contract": "node scripts/test-native-control-contract.mjs",
118
+ "test:native-session-controls": "node scripts/test-native-session-controls.mjs",
119
+ "test:released-mini-models": "node scripts/test-released-mini-models.mjs",
118
120
  "test:independent-transports": "node scripts/test-independent-transports.mjs",
119
121
  "verify:installer": "node scripts/test-hybrid-package.mjs"
120
122
  },
@@ -226,7 +228,9 @@
226
228
  "lib/typert-schema-compat.js",
227
229
  "lib/typert-schema-compat.d.ts",
228
230
  "lib/dsh-protocol-compat.js",
229
- "lib/dsh-protocol-compat.d.ts",
231
+ "lib/dsh-protocol-compat.d.ts",
232
+ "lib/legacy-model-identity.js",
233
+ "lib/legacy-model-identity.d.ts",
230
234
  "lib/dsh-session-address.js",
231
235
  "lib/dsh-session-address.d.ts",
232
236
  "lib/session-presentation.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-wechat-remote",
3
- "version": "1.7.12-rc.9",
3
+ "version": "1.7.12",
4
4
  "private": false,
5
5
  "description": "Agent远程管理助手:DSH 微信连接插件的一键安装与升级",
6
6
  "type": "module",