@twinsearth/w2m-dsh-plugin 1.7.2 → 1.7.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/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +10 -0
- package/codex/mcp-servers.toml +1 -1
- package/docs/acceptance/1.7.3/TOAST-AND-TOTALS.json +29 -0
- package/lib/client.js +28 -8
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/src/plugin/fleet-target.mjs +12 -10
- package/src/plugin/tools.mjs +1 -1
|
@@ -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.7.
|
|
8
|
+
"version": "1.7.3"
|
|
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.7.
|
|
15
|
+
"version": "1.7.3",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "TwinsEarth"
|
|
18
18
|
},
|
|
@@ -26,5 +26,5 @@
|
|
|
26
26
|
]
|
|
27
27
|
}
|
|
28
28
|
],
|
|
29
|
-
"version": "1.7.
|
|
29
|
+
"version": "1.7.3"
|
|
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.7.
|
|
4
|
+
"version": "1.7.3",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TwinsEarth"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@
|
|
|
7
7
|
|
|
8
8
|
All notable changes to this project are documented here.
|
|
9
9
|
|
|
10
|
+
## [1.7.3] — 2026-10-25
|
|
11
|
+
|
|
12
|
+
**截图提示改为顶部浮层(5 秒自动消失、可关闭、不挤压输入框);全服计数标注来源 relay 并单列本编队。**
|
|
13
|
+
|
|
14
|
+
- 截图成功/失败提示不再插在输入区里:改为聊天面板顶部浮层(绝对定位,不占布局;连续截图只替换同一行,
|
|
15
|
+
不会越堆越多),**5 秒自动消失**,右侧 × 可立即关闭。
|
|
16
|
+
- 全服统计行改为「主 relay(地址)全服:X/Y 个编队 · A/B 台在线」,并新增一行「本编队:n/m 台在线」;
|
|
17
|
+
两者语义不同,不再混在一起看。
|
|
18
|
+
- relay 角色判定改为按**配置的主 relay**(不再按响应最快,备用 relay 偶然更快时会被误标成 primary)。
|
|
19
|
+
- 证据:`docs/acceptance/1.7.3/TOAST-AND-TOTALS.json`(fleet task `01M5ANYMVMSGGQV`;宿主截图 141,768 字节)。
|
|
10
20
|
## [1.7.2] — 2026-10-25
|
|
11
21
|
|
|
12
22
|
**按实测反馈调整编队聊天截图/复制:只复制不预览、快捷键 Ctrl+Shift+P、强制可选中。**
|
package/codex/mcp-servers.toml
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.7.3",
|
|
3
|
+
"case": "TOAST-AND-TOTALS",
|
|
4
|
+
"kind": "positive",
|
|
5
|
+
"machines": [
|
|
6
|
+
"b53a37f4-1464-4512-9259-33a7df091e3e"
|
|
7
|
+
],
|
|
8
|
+
"executed_at": "2026-10-24T19:25:35.677Z",
|
|
9
|
+
"executor": "lead",
|
|
10
|
+
"requirement": "screenshot feedback must not squeeze the compose box or stay forever: a top overlay that dismisses after 5s and can be closed; whole-server totals must name the relay they came from and not impersonate our fleet",
|
|
11
|
+
"task_id": "01M5ANYMVMSGGQV",
|
|
12
|
+
"result": {
|
|
13
|
+
"outcome": "ok",
|
|
14
|
+
"host_capture_smoke": {
|
|
15
|
+
"platform": "win32",
|
|
16
|
+
"ok": true,
|
|
17
|
+
"bytes": 141768,
|
|
18
|
+
"copied": true
|
|
19
|
+
},
|
|
20
|
+
"assertions": {
|
|
21
|
+
"fleet_chat_capture": "3/3 pass",
|
|
22
|
+
"client_bundle": "26/26 pass",
|
|
23
|
+
"fleet_target": "76/76 pass",
|
|
24
|
+
"toast": "absolute overlay inside the panel (no layout shift), 5s timer, close button",
|
|
25
|
+
"totals": "labelled \"primary relay (<url>)\"; separate \"this fleet: n/m online\" line",
|
|
26
|
+
"roles": "primary role follows the configured rabbitUrl, not the fastest response"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
package/lib/client.js
CHANGED
|
@@ -115,6 +115,8 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
|
|
|
115
115
|
relayUnfold: '收起折叠的 relay',
|
|
116
116
|
relayTemporary: '临时 Relay 节点(回退配置)',
|
|
117
117
|
globalSummary: '全服:{fleetsOnline}/{fleets} 个编队 · {online}/{total} 台在线',
|
|
118
|
+
globalSummaryWithSource: '主 relay({source})全服:{fleetsOnline}/{fleets} 个编队 · {online}/{total} 台在线',
|
|
119
|
+
fleetSummary: '本编队:{online}/{total} 台在线',
|
|
118
120
|
globalUnreachable: '主 relay 不可达({url})· 全服统计不可用',
|
|
119
121
|
updateButton: '检查插件更新',
|
|
120
122
|
updateChecking: '正在检查 npm…',
|
|
@@ -154,6 +156,8 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
|
|
|
154
156
|
relayUnfold: 'collapse the folded relays',
|
|
155
157
|
relayTemporary: 'temporary relay nodes (fallback config)',
|
|
156
158
|
globalSummary: 'fleets {fleetsOnline}/{fleets} · machines {online}/{total} online',
|
|
159
|
+
globalSummaryWithSource: 'primary relay ({source}) · fleets {fleetsOnline}/{fleets} · machines {online}/{total}',
|
|
160
|
+
fleetSummary: 'this fleet: {online}/{total} online',
|
|
157
161
|
globalUnreachable: 'primary relay unreachable ({url}) · server totals unavailable',
|
|
158
162
|
updateButton: 'check for plugin update',
|
|
159
163
|
updateChecking: 'checking npm…',
|
|
@@ -402,7 +406,8 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
|
|
|
402
406
|
'.w2mChat_copy{margin-left:6px;border:none;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:11px;line-height:14px;padding:0 2px;vertical-align:middle}',
|
|
403
407
|
'.w2mChat_copy:hover{color:var(--dsw-alias-label-primary)}',
|
|
404
408
|
'.w2mChat_shotButton{flex:none;height:28px;padding:0 8px;border:.5px solid var(--dsw-alias-border-default);border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-surface-default);color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer}',
|
|
405
|
-
'.w2mChat_toast{padding:
|
|
409
|
+
'.w2mChat_toast{position:absolute;left:10px;right:10px;top:46px;z-index:6;display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:8px;border:.5px solid var(--dsw-alias-border-default);background:var(--dsw-alias-bg-layer-2);box-shadow:var(--dsw-elevation-soft);font-size:11px;color:var(--dsw-alias-label-secondary)}',
|
|
410
|
+
'.w2mChat_toastClose{margin-left:auto;border:none;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:12px;line-height:12px;padding:0 2px}',
|
|
406
411
|
'.w2mChat_text{white-space:pre-wrap;word-break:break-word}',
|
|
407
412
|
'.w2mChat_msg.is-session .w2mChat_text{color:var(--dsw-danger)}', '.w2mChat_msg.is-chat .w2mChat_text{color:var(--dsw-success)}', '.w2mChat_msg.is-remote .w2mChat_text{color:var(--dsw-text)}', '.w2mChat_avatar{display:inline-flex;width:16px;height:16px;margin-right:5px;vertical-align:-3px}',
|
|
408
413
|
'.w2mChat_compose{display:flex;gap:8px;padding:8px 10px;border-top:.5px solid var(--dsw-alias-border-l2);align-items:flex-end}',
|
|
@@ -697,12 +702,20 @@ function TargetSelect({ sessionId, locked: _locked, t }) {
|
|
|
697
702
|
h('span', { key: 'url', className: cls('relayUrl') }, String(item.url ?? '').replace(/^https?:\/\//, '').replace(/\/$/, '')),
|
|
698
703
|
h('span', { key: 'rtt', className: cls('relayRtt') }, item.rtt_ms === null || item.rtt_ms === undefined ? '—' : `${item.rtt_ms} ms`),
|
|
699
704
|
])),
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
705
|
+
// v1.7.3: name the relay the totals came from; "whole server" and "this fleet" are different
|
|
706
|
+
// numbers, and the panel used to leave the reader to guess which one it was showing.
|
|
707
|
+
relayFold.server === null || relayFold.server === undefined
|
|
708
|
+
? (relayFold.server_reachable === false
|
|
709
|
+
? h('div', { key: 'globalUnreachable', className: cls('relayHint') }, t('globalUnreachable', { url: String(relayFold.server_url ?? '').replace(/^https?:\/\//, '').replace(/\/$/, '') || '?' }))
|
|
710
|
+
: null)
|
|
711
|
+
: h('div', { key: 'global', className: cls('relayHint') }, t('globalSummaryWithSource', {
|
|
712
|
+
source: String(relayFold.server_url ?? '').replace(/^https?:\/\//, '').replace(/\/$/, '') || '?',
|
|
713
|
+
fleetsOnline: relayFold.server.fleets_online ?? '—',
|
|
714
|
+
fleets: relayFold.server.fleets_total ?? '—',
|
|
715
|
+
online: relayFold.server.devices_online ?? '—',
|
|
716
|
+
total: relayFold.server.devices_total ?? '—',
|
|
717
|
+
})),
|
|
718
|
+
h('div', { key: 'fleet', className: cls('relayHint') }, t('fleetSummary', { online: state?.fleet?.online ?? '—', total: state?.fleet?.total ?? '—' })),
|
|
706
719
|
h(PluginUpdate, { key: 'update', t }),
|
|
707
720
|
relayFold.hidden_count > 0 && relayOpen === false
|
|
708
721
|
? h('div', { key: 'hint', className: cls('relayHint') }, t('relayFold', { count: relayFold.hidden_count }))
|
|
@@ -1035,6 +1048,14 @@ function TargetSelect({ sessionId, locked: _locked, t }) {
|
|
|
1035
1048
|
return () => document.removeEventListener('keydown', onKeyDown, true);
|
|
1036
1049
|
}, [open, captureScreenshot]);
|
|
1037
1050
|
|
|
1051
|
+
// v1.7.3: screenshot feedback is ephemeral: it fades after 5s and can be closed. It must not
|
|
1052
|
+
// sit in the compose row (that squeezed the input) and must not accumulate with more captures.
|
|
1053
|
+
React.useEffect(() => {
|
|
1054
|
+
if (toast === null) return undefined;
|
|
1055
|
+
const timer = setTimeout(() => setToast(null), 5000);
|
|
1056
|
+
return () => clearTimeout(timer);
|
|
1057
|
+
}, [toast]);
|
|
1058
|
+
|
|
1038
1059
|
const unseenCount = messages.reduce((count, message) => (Number.isFinite(message?.at_ms) && message.at_ms > lastSeenMs ? count + 1 : count), 0);
|
|
1039
1060
|
const disabled = typeof sessionId !== 'string' || sessionId === '';
|
|
1040
1061
|
const usageText = usage === null ? null : `${Math.round((usage.used_24h_bytes ?? 0) / 1024)} KiB/24h · ${usage.messages ?? 0} 条`;
|
|
@@ -1100,7 +1121,6 @@ function TargetSelect({ sessionId, locked: _locked, t }) {
|
|
|
1100
1121
|
|
|
1101
1122
|
|
|
1102
1123
|
h('div', { className: 'w2mChat_compose' },
|
|
1103
|
-
toast === null ? null : h('div', { key: 'toast', className: 'w2mChat_toast' }, toast),
|
|
1104
1124
|
replyTo === null ? null : h('div', { className: 'w2mChat_reply' }, [
|
|
1105
1125
|
h('span', { key: 'label' }, '回复:' + (replyTo.text.length > 40 ? replyTo.text.slice(0, 40) + '…' : replyTo.text)),
|
|
1106
1126
|
h('button', { key: 'cancel', type: 'button', className: 'w2mChat_replyCancel', title: '取消回复', onClick: () => setReplyTo(null) }, '×'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.3",
|
|
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.7.
|
|
4
|
+
"version": "1.7.3",
|
|
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": [
|
|
@@ -801,22 +801,24 @@ function mountTargetRoutes(connection, deps, status, logger) {
|
|
|
801
801
|
return null;
|
|
802
802
|
}
|
|
803
803
|
const probed = await probeRelays(urls);
|
|
804
|
-
//
|
|
805
|
-
//
|
|
806
|
-
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
804
|
+
// v1.7.3: the role follows the CONFIGURED primary relay, not whichever relay answered fastest.
|
|
805
|
+
// Ranking by latency let the backup masquerade as primary whenever it was quicker -- the panel then
|
|
806
|
+
// explained one relay while showing another relay's whole-server numbers.
|
|
807
|
+
const normalize = (value) => (typeof value === 'string' ? value.trim().replace(/\/+$/, '') : null);
|
|
808
|
+
const configuredPrimary = normalize(deps.cfg()?.rabbitUrl) ?? normalize(urls[0]);
|
|
809
|
+
const ordered = [...probed];
|
|
810
|
+
const primaryIndex = ordered.findIndex((item) => normalize(item.url) === configuredPrimary);
|
|
811
|
+
if (primaryIndex > 0) ordered.unshift(...ordered.splice(primaryIndex, 1));
|
|
812
|
+
const items = ordered.map((item) => ({
|
|
813
|
+
...item,
|
|
814
|
+
role: item.ok !== true ? 'offline' : (normalize(item.url) === configuredPrimary ? 'primary' : 'standby'),
|
|
815
|
+
}));
|
|
812
816
|
const folded = foldRelayItems(items, 3);
|
|
813
817
|
// v1.7.2: whole-server totals come from the CONFIGURED primary relay only. v1.6.2 used "the first
|
|
814
818
|
// relay that answered", so when the primary was unreachable the panel showed the BACKUP relay's
|
|
815
819
|
// numbers (1/1 fleets, 2/5 machines) under the "whole server" label -- visibly wrong. An
|
|
816
820
|
// unreachable primary reports `server: null` + `server_reachable: false`, and the panel says the
|
|
817
821
|
// totals are unavailable instead of presenting another relay's fleet as the server's.
|
|
818
|
-
const normalize = (value) => (typeof value === 'string' ? value.trim().replace(/\/+$/, '') : null);
|
|
819
|
-
const configuredPrimary = normalize(deps.cfg()?.rabbitUrl) ?? normalize(urls[0]);
|
|
820
822
|
const primaryItem = configuredPrimary === null
|
|
821
823
|
? items.find((item) => item.ok === true) ?? null
|
|
822
824
|
: items.find((item) => normalize(item.url) === configuredPrimary) ?? null;
|
package/src/plugin/tools.mjs
CHANGED
|
@@ -283,7 +283,7 @@ const MAX_STAGES = 16;
|
|
|
283
283
|
* `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
|
|
284
284
|
* overrides it at runtime for tests and packagers.
|
|
285
285
|
*/
|
|
286
|
-
const PLUGIN_VERSION = '1.7.
|
|
286
|
+
const PLUGIN_VERSION = '1.7.3';
|
|
287
287
|
|
|
288
288
|
/** How long any single HTTP call may take before it is aborted, in milliseconds. */
|
|
289
289
|
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|