@twinsearth/w2m-dsh-plugin 1.6.1 → 1.6.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.
@@ -5,14 +5,14 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "The w2m fleet plugins: one account, many machines, any of Windows, macOS or Linux.",
8
- "version": "1.6.1"
8
+ "version": "1.6.2"
9
9
  },
10
10
  "plugins": [
11
11
  {
12
12
  "name": "w2m-fleet",
13
13
  "source": "./",
14
14
  "description": "Read fleet state and dispatch commands across the machines registered to one account. Read-only MCP surface: the five exposed tools cannot change anything.",
15
- "version": "1.6.1",
15
+ "version": "1.6.2",
16
16
  "author": {
17
17
  "name": "TwinsEarth"
18
18
  },
@@ -26,5 +26,5 @@
26
26
  ]
27
27
  }
28
28
  ],
29
- "version": "1.6.1"
29
+ "version": "1.6.2"
30
30
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "w2m-fleet",
3
3
  "description": "Drive the same project on online Windows, macOS and Linux machines from one account: read fleet state, dispatch commands and read results. The MCP surface is deliberately read-only.",
4
- "version": "1.6.1",
4
+ "version": "1.6.2",
5
5
  "author": {
6
6
  "name": "TwinsEarth"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@
7
7
 
8
8
  All notable changes to this project are documented here.
9
9
 
10
+ ## [1.6.2] — 2026-10-24
11
+
12
+ **relay 全服概览:多少个编队、多少台机器在线(healthz + operator /v1/fleets + 面板一行显示)。**
13
+
14
+ - `state.fleetSummary()`:按 fleet 聚合设备数/在线数,返回 `fleets_total`、`devices_online`、
15
+ `devices_total`、`unfleeted_devices` 与 per-fleet 明细。
16
+ - `GET /healthz`(公开)新增上述**聚合计数**(不暴露 per-fleet 明细);
17
+ - `GET /v1/fleets`(operator token):返回 per-fleet 明细(其他账号的编队名不向普通设备暴露)。
18
+ - 面板 relay 折叠区新增一行:`全服:N 个编队 · M/T 台在线`(fleet-target probe 读取 healthz 计数)。
19
+ - relay 主机将升级到 1.6.2(包含 1.6.1 的 Node 执行仅人工/内存态与 allow-list 解释器身份修复)。
20
+ - 测试:relay 200/200、fleet-target 49/49、client-bundle 26/26;证据
21
+ `docs/acceptance/1.6.2/RELAY-FLEET-OVERVIEW.json`(fleet task `01M5ANYMVLRYXSN`)。
10
22
  ## [1.6.1] — 2026-10-24
11
23
 
12
24
  **Node 执行开关改为「仅人工 UI 手动、进程内存态」:文件/远程/自动化一律无法授权。**
@@ -12,6 +12,6 @@
12
12
 
13
13
  [mcp_servers.w2m-fleet]
14
14
  command = "npx"
15
- args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@1.6.1", "w2m-mcp"]
15
+ args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@1.6.2", "w2m-mcp"]
16
16
  startup_timeout_sec = 20
17
17
  tool_timeout_sec = 60
@@ -0,0 +1,22 @@
1
+ {
2
+ "version": "1.6.2",
3
+ "case": "RELAY-FLEET-OVERVIEW",
4
+ "kind": "positive",
5
+ "machines": [
6
+ "b53a37f4-1464-4512-9259-33a7df091e3e"
7
+ ],
8
+ "executed_at": "2026-10-24T02:23:58.829Z",
9
+ "executor": "lead",
10
+ "requirement": "the relay reports how many fleets it serves and how many machines are online: aggregate counts on the public health probe, per-fleet rows only for the operator",
11
+ "task_id": "01M5ANYMVLRYXSN",
12
+ "result": {
13
+ "outcome": "ok",
14
+ "assertions": {
15
+ "relay_suite": "200/200 pass",
16
+ "fleet_summary": "fleets_total/devices_online/devices_total",
17
+ "operator_route": "GET /v1/fleets",
18
+ "panel_line": "globalSummary rendered"
19
+ }
20
+ },
21
+ "note": "relay and fleet-target suites ran as a real fleet task"
22
+ }
package/lib/client.js CHANGED
@@ -112,6 +112,7 @@ window.__ModuleLoader__.load({
112
112
  relayFold: '另有 {count} 个已折叠,点击展开',
113
113
  relayUnfold: '收起折叠的 relay',
114
114
  relayTemporary: '临时 Relay 节点(回退配置)',
115
+ globalSummary: '全服:{fleets} 个编队 · {online}/{total} 台在线',
115
116
  capLabel: 'Node 执行:{state}',
116
117
  capDenied: '拒绝',
117
118
  capTemporary: '临时 24 小时',
@@ -143,6 +144,7 @@ window.__ModuleLoader__.load({
143
144
  relayFold: '{count} more folded — expand',
144
145
  relayUnfold: 'collapse the folded relays',
145
146
  relayTemporary: 'temporary relay nodes (fallback config)',
147
+ globalSummary: '{fleets} fleets · {online}/{total} machines online',
146
148
  capLabel: 'Node exec: {state}',
147
149
  capDenied: 'denied',
148
150
  capTemporary: 'temporary 24 h',
@@ -631,6 +633,11 @@ window.__ModuleLoader__.load({
631
633
  h('span', { key: 'url', className: cls('relayUrl') }, String(item.url ?? '').replace(/^https?:\/\//, '').replace(/\/$/, '')),
632
634
  h('span', { key: 'rtt', className: cls('relayRtt') }, item.rtt_ms === null || item.rtt_ms === undefined ? '—' : `${item.rtt_ms} ms`),
633
635
  ])),
636
+ relayFold.server === null || relayFold.server === undefined ? null : h('div', { key: 'global', className: cls('relayHint') }, t('globalSummary', {
637
+ fleets: relayFold.server.fleets_total ?? '—',
638
+ online: relayFold.server.devices_online ?? '—',
639
+ total: relayFold.server.devices_total ?? '—',
640
+ })),
634
641
  relayFold.hidden_count > 0 && relayOpen === false
635
642
  ? h('div', { key: 'hint', className: cls('relayHint') }, t('relayFold', { count: relayFold.hidden_count }))
636
643
  : null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinsearth/w2m-dsh-plugin",
3
- "version": "1.6.1",
3
+ "version": "1.6.2",
4
4
  "description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 11 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
5
5
  "type": "module",
6
6
  "exports": {
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "twinsearth-w2m-dsh",
4
- "version": "1.6.1",
4
+ "version": "1.6.2",
5
5
  "description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 12 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -503,7 +503,16 @@ export async function probeRelays(urls, options = {}) {
503
503
  if (!response.ok || body?.ok !== true) {
504
504
  return { url, ok: false, rtt_ms: null, error: `HTTP ${response.status}` };
505
505
  }
506
- return { url, ok: true, rtt_ms: Date.now() - started, relay_id: body.relay_id ?? null };
506
+ return {
507
+ url, ok: true, rtt_ms: Date.now() - started, relay_id: body.relay_id ?? null,
508
+ // v1.6.2: Whole-relay totals, shown in the panel as "N fleets · M machines online".
509
+ // Counts only: per-fleet membership stays behind the fleet-scoped endpoints.
510
+ stats: {
511
+ fleets_total: Number.isInteger(body.fleets_total) ? body.fleets_total : null,
512
+ devices_online: Number.isInteger(body.devices_online) ? body.devices_online : null,
513
+ devices_total: Number.isInteger(body.devices_total) ? body.devices_total : null,
514
+ },
515
+ };
507
516
  } catch (error) {
508
517
  return { url, ok: false, rtt_ms: null, error: error instanceof Error ? error.message : String(error) };
509
518
  }
@@ -573,6 +582,8 @@ export function buildUiState(deps) {
573
582
  primary_url: relays.shown.find((item) => item.ok === true)?.url ?? null,
574
583
  // v1.5.2: a fallback relay list is never presented as settled; the panel renders it red.
575
584
  temporary: relays.temporary === true,
585
+ // v1.6.2: whole-relay overview (fleets + online machines) for the panel header line.
586
+ server: relays.server ?? null,
576
587
  },
577
588
  };
578
589
  }
@@ -795,8 +806,10 @@ function mountTargetRoutes(connection, deps, status, logger) {
795
806
  return { ...item, role: reachableSeen === 1 ? 'primary' : 'standby' };
796
807
  });
797
808
  const folded = foldRelayItems(items, 3);
809
+ // v1.6.2: the primary relay's own view of the whole server, if it reported one.
810
+ const primaryStats = items.find((item) => item.ok === true)?.stats ?? null;
798
811
  // v1.5.2: a fallback list is marked so the panel can render it in red, not as a settled config.
799
- const withMark = { ...folded, temporary: deps.cfg()?.relayTemporary === true };
812
+ const withMark = { ...folded, temporary: deps.cfg()?.relayTemporary === true, server: primaryStats };
800
813
  relayCache = { at: now, value: withMark };
801
814
  return withMark;
802
815
  };
@@ -277,7 +277,7 @@ const MAX_STAGES = 16;
277
277
  * `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
278
278
  * overrides it at runtime for tests and packagers.
279
279
  */
280
- const PLUGIN_VERSION = '1.6.1';
280
+ const PLUGIN_VERSION = '1.6.2';
281
281
 
282
282
  /** How long any single HTTP call may take before it is aborted, in milliseconds. */
283
283
  const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
@@ -853,6 +853,17 @@ export class RelayServer {
853
853
  * every task that carried a grant, with the argv hash and key id it was authorised under -- and deliberately returns
854
854
  * only operator-readable data, never result bodies.
855
855
  */
856
+ /**
857
+ * v1.6.2: whole-relay fleet overview for an operator. Aggregate counts are public on /healthz;
858
+ * the per-fleet rows are not, because they name other accounts fleets.
859
+ */
860
+ if (method === 'GET' && path === '/v1/fleets') {
861
+ const auth = this.authorizeTaskDispatch(req);
862
+ if (auth.role !== 'operator') throw new ProtocolError('OPERATOR_REQUIRED', 'listing fleets is an operator view');
863
+ const summary = this.state.fleetSummary();
864
+ return sendJson(res, 200, { protocol_version: PROTOCOL_VERSION, rabbit_time: this.state.nowIso(), ok: true, ...summary });
865
+ }
866
+
856
867
  if (path === '/v1/grants/revoke' || path === '/v1/grants') {
857
868
  const auth = this.authorizeTaskDispatch(req);
858
869
  if (auth.role !== 'operator') throw new ProtocolError('OPERATOR_REQUIRED', 'only the operator may administer grants');
@@ -3245,12 +3245,40 @@ export class RabbitState {
3245
3245
 
3246
3246
  /* ---------------- serialization ---------------- */
3247
3247
 
3248
+ /**
3249
+ * v1.6.2: the whole-relay view: how many fleets this relay serves and how many machines are
3250
+ * online. Counts only — the per-fleet member lists stay behind the fleet-scoped endpoints, so
3251
+ * this is safe to expose on the public health probe.
3252
+ */
3253
+ fleetSummary(nowMs = this.nowMs()) {
3254
+ const byFleet = new Map();
3255
+ let online = 0;
3256
+ for (const device of this.devices.values()) {
3257
+ const key = typeof device.fleet_id === 'string' && device.fleet_id !== '' ? device.fleet_id : null;
3258
+ const entry = byFleet.get(key) ?? { fleet_id: key, devices: 0, online: 0 };
3259
+ entry.devices += 1;
3260
+ if (this.isOnlineDevice(device, nowMs)) { entry.online += 1; online += 1; }
3261
+ byFleet.set(key, entry);
3262
+ }
3263
+ const fleets = [...byFleet.values()].sort((a, b) => b.devices - a.devices);
3264
+ return {
3265
+ fleets_total: fleets.filter((f) => f.fleet_id !== null).length,
3266
+ unfleeted_devices: byFleet.get(null)?.devices ?? 0,
3267
+ devices_total: this.devices.size,
3268
+ devices_online: online,
3269
+ fleets,
3270
+ };
3271
+ }
3248
3272
  stats() {
3249
3273
  return {
3250
3274
  protocol_version: PROTOCOL_VERSION,
3251
3275
  seq: this.seq,
3252
3276
  buffered_events: this.buffer.length,
3253
3277
  devices: this.devices.size,
3278
+ // v1.6.2: whole-relay totals on the public health probe.
3279
+ fleets_total: this.fleetSummary().fleets_total,
3280
+ devices_online: this.fleetSummary().devices_online,
3281
+ devices_total: this.devices.size,
3254
3282
  tasks: this.tasks.size,
3255
3283
  results: this.results.size,
3256
3284
  pairing_codes: [...this.pairingCodes.values()].filter((c) => !c.used && this.nowMs() < c.expires_at_ms).length,