@twinsearth/w2m-dsh-plugin 1.1.18 → 1.1.19
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 +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +30 -0
- package/README.md +8 -8
- package/codex/mcp-servers.toml +1 -1
- package/lib/client.js +179 -0
- package/lib/client.js.bak2 +640 -0
- package/package.json +1 -1
- package/plugin.json +2 -2
- package/src/plugin/fleet-chat-route.mjs +120 -0
- package/src/plugin/tools.mjs +24 -3
- package/src/plugin/tools.mjs.bak-c7 +0 -7029
|
@@ -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.1.
|
|
8
|
+
"version": "1.1.19"
|
|
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.1.
|
|
15
|
+
"version": "1.1.19",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "TwinsEarth"
|
|
18
18
|
},
|
|
@@ -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.1.
|
|
4
|
+
"version": "1.1.19",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TwinsEarth"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented here.
|
|
4
4
|
|
|
5
|
+
## [1.1.19] — 2026-10-18
|
|
6
|
+
|
|
7
|
+
**The chat panel exists in the browser.**
|
|
8
|
+
|
|
9
|
+
Fleet chat shipped as a tool in 1.1.17 and as a relay API in the same release. What was missing was the half a user actually
|
|
10
|
+
touches: a control in the composer. This release adds it.
|
|
11
|
+
|
|
12
|
+
- **The panel**: a "fleet chat" control beside the target-machine control opens a floating panel listing this session's
|
|
13
|
+
messages, with the relay usage shown next to them, a multi-line input, and a send button. It refreshes while open.
|
|
14
|
+
- **The host route**: the browser never holds a device token. It calls `/api/w2m/chat` on the host, which proxies to the
|
|
15
|
+
relay's device-gated `/v1/fleet/messages` — so the token stays in the host process.
|
|
16
|
+
- **Tool bug fixed**: `w2m_chat` resolved its token through `readDevice(cfg)`, which fails when `stateDir` is unset even
|
|
17
|
+
though a `device.json` exists elsewhere; it now resolves exactly like every other tool (`resolveToken(cfg)`).
|
|
18
|
+
- **Theme discipline**: the panel carries no literal colours, only `--dsw-*` tokens, because a literal is wrong in one of
|
|
19
|
+
the two themes. The stylesheet test caught the one literal (`#fff` on the send button).
|
|
20
|
+
|
|
21
|
+
The relay on the fleet host was upgraded to this line of code in the same step: it had been running 1.0.14, which has no
|
|
22
|
+
`/v1/fleet/messages` route at all, so the panel would have shown "no route" forever. Verified after the restart: an
|
|
23
|
+
authenticated GET returns 200 (it returned 404 before), unauthenticated returns 401, and the fleet came back with the same
|
|
24
|
+
eight devices and no re-pairing.
|
|
25
|
+
|
|
26
|
+
### Not in this release
|
|
27
|
+
|
|
28
|
+
The coloured per-machine avatars for chat messages are designed and not built. The panel shows one style for every
|
|
29
|
+
machine today.
|
|
30
|
+
|
|
31
|
+
### Verified
|
|
32
|
+
|
|
33
|
+
Gate GREEN, 15 of 15. 2489 tests / 2471+ pass. client-bundle 25, tools 122, claims, portable-plugins 10.
|
|
34
|
+
|
|
5
35
|
## [1.1.18] — 2026-10-18
|
|
6
36
|
|
|
7
37
|
**The developer declares this a STABLE release.** It ships the three-channel updater and the release plumbing that makes
|
package/README.md
CHANGED
|
@@ -66,18 +66,18 @@ The manifests live in [`.claude-plugin/`](.claude-plugin/) and are version-locke
|
|
|
66
66
|
# copy this block into your own codex/mcp-servers.toml
|
|
67
67
|
[mcp_servers.w2m]
|
|
68
68
|
command = "npx"
|
|
69
|
-
args = ["-y", "@twinsearth/w2m-dsh-plugin@1.1.
|
|
69
|
+
args = ["-y", "@twinsearth/w2m-dsh-plugin@1.1.19", "w2m-mcp"]
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
### 5. Verified release tarball (offline)
|
|
73
73
|
|
|
74
|
-
From the `v1.1.
|
|
74
|
+
From the `v1.1.19` release page download **both** files — `twinsearth-w2m-dsh-plugin-1.1.19.tgz` and `SHA256SUMS` —
|
|
75
75
|
then:
|
|
76
76
|
|
|
77
77
|
```bash
|
|
78
|
-
curl -LO https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v1.1.
|
|
78
|
+
curl -LO https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v1.1.19/twinsearth-w2m-dsh-plugin-1.1.19.tgz
|
|
79
79
|
sha256sum -c SHA256SUMS # verify before installing, always
|
|
80
|
-
npm install -g ./twinsearth-w2m-dsh-plugin-1.1.
|
|
80
|
+
npm install -g ./twinsearth-w2m-dsh-plugin-1.1.19.tgz
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
Every release carries `SHA256SUMS` for exactly this reason. If a release is missing it, do not install that release.
|
|
@@ -288,17 +288,17 @@ w2m-mcp --help
|
|
|
288
288
|
# 把这段复制进你自己的 codex/mcp-servers.toml
|
|
289
289
|
[mcp_servers.w2m]
|
|
290
290
|
command = "npx"
|
|
291
|
-
args = ["-y", "@twinsearth/w2m-dsh-plugin@1.1.
|
|
291
|
+
args = ["-y", "@twinsearth/w2m-dsh-plugin@1.1.19", "w2m-mcp"]
|
|
292
292
|
```
|
|
293
293
|
|
|
294
294
|
#### 5. 校验过的发布包(离线)
|
|
295
295
|
|
|
296
|
-
从 `v1.1.
|
|
296
|
+
从 `v1.1.19` 的 release 页面下载**两个**文件 —— `twinsearth-w2m-dsh-plugin-1.1.19.tgz` 与 `SHA256SUMS` —— 然后:
|
|
297
297
|
|
|
298
298
|
```bash
|
|
299
|
-
curl -LO https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v1.1.
|
|
299
|
+
curl -LO https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v1.1.19/twinsearth-w2m-dsh-plugin-1.1.19.tgz
|
|
300
300
|
sha256sum -c SHA256SUMS # 安装前务必校验
|
|
301
|
-
npm install -g ./twinsearth-w2m-dsh-plugin-1.1.
|
|
301
|
+
npm install -g ./twinsearth-w2m-dsh-plugin-1.1.19.tgz
|
|
302
302
|
```
|
|
303
303
|
|
|
304
304
|
每个 release 都带 `SHA256SUMS` 正是为了这一步。若某个 release 缺少它,**不要安装该版本**。
|
package/codex/mcp-servers.toml
CHANGED
package/lib/client.js
CHANGED
|
@@ -283,6 +283,24 @@ window.__ModuleLoader__.load({
|
|
|
283
283
|
'.w2mTarget_name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
284
284
|
'.w2mTarget_detail{margin-left:auto;flex:none;color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:400}',
|
|
285
285
|
'.w2mTarget_option:disabled .w2mTarget_detail{color:var(--dsw-alias-label-caption)}',
|
|
286
|
+
'.w2mChat{position:relative;display:inline-flex;align-items:center}',
|
|
287
|
+
'.w2mChat_trigger{display:inline-flex;align-items:center;gap:4px;height:28px;padding:0 8px;border:none;border-radius:var(--dsw-radius-sm);background:transparent;color:var(--dsw-alias-label-secondary);font-size:13px;font-weight:500;line-height:20px;cursor:pointer;outline:none}',
|
|
288
|
+
'.w2mChat_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}',
|
|
289
|
+
'.w2mChat_trigger:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}',
|
|
290
|
+
'.w2mChat_panel{position:fixed;right:16px;bottom:88px;z-index:60;display:flex;flex-direction:column;width:340px;max-height:420px;border-radius:var(--dsw-radius-lg);border:.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);box-shadow:var(--dsw-elevation-soft);font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary);overflow:hidden}',
|
|
291
|
+
'.w2mChat_head{display:flex;align-items:center;gap:8px;padding:8px 10px;border-bottom:.5px solid var(--dsw-alias-border-l2);font-weight:600}',
|
|
292
|
+
'.w2mChat_usage{margin-left:auto;font-weight:400;color:var(--dsw-alias-label-tertiary);font-size:11px}',
|
|
293
|
+
'.w2mChat_close{border:none;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:16px;line-height:16px;padding:0 2px}',
|
|
294
|
+
'.w2mChat_list{flex:1;overflow-y:auto;padding:8px 10px;display:flex;flex-direction:column;gap:8px;min-height:120px}',
|
|
295
|
+
'.w2mChat_empty{color:var(--dsw-alias-label-tertiary)}',
|
|
296
|
+
'.w2mChat_msg{display:flex;flex-direction:column;gap:2px}',
|
|
297
|
+
'.w2mChat_meta{color:var(--dsw-alias-label-tertiary);font-size:11px}',
|
|
298
|
+
'.w2mChat_text{white-space:pre-wrap;word-break:break-word}',
|
|
299
|
+
'.w2mChat_compose{display:flex;gap:8px;padding:8px 10px;border-top:.5px solid var(--dsw-alias-border-l2);align-items:flex-end}',
|
|
300
|
+
'.w2mChat_input{flex:1;resize:none;box-sizing:border-box;font:inherit;color:inherit;background:var(--dsw-alias-bg-layer-2);border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm);padding:6px 8px;outline:none;min-height:38px;max-height:96px}',
|
|
301
|
+
'.w2mChat_send{flex:none;height:28px;padding:0 10px;border:none;border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-text-on-business);font:inherit;font-weight:500;cursor:pointer}',
|
|
302
|
+
'.w2mChat_send:disabled{opacity:.5;cursor:default}',
|
|
303
|
+
'.w2mChat_error{padding:0 10px 8px;color:var(--dsw-alias-state-error-primary);font-size:11px}',
|
|
286
304
|
].join('');
|
|
287
305
|
|
|
288
306
|
/**
|
|
@@ -530,6 +548,148 @@ window.__ModuleLoader__.load({
|
|
|
530
548
|
]);
|
|
531
549
|
}
|
|
532
550
|
|
|
551
|
+
// -------------------------------------------------------------------------------------------
|
|
552
|
+
// Fleet chat panel (v1.1.18 addition: the missing browser half of `w2m_chat`)
|
|
553
|
+
// -------------------------------------------------------------------------------------------
|
|
554
|
+
//
|
|
555
|
+
// v1.1.17 shipped fleet chat as a tool only — its own CHANGELOG records "no client UI panel
|
|
556
|
+
// beyond the generic tool card". This is that panel: a session-scoped broadcast board the
|
|
557
|
+
// operator and the agent share. Every read and write goes through the host route
|
|
558
|
+
// `/api/w2m/chat` (host-side `fleet-chat-route.mjs`), so the device token stays in the host
|
|
559
|
+
// process and this file never holds a credential.
|
|
560
|
+
const CHAT_ROUTE = '/api/w2m/chat';
|
|
561
|
+
|
|
562
|
+
/** One JSON call to the host chat route; a refusal is a value, not an exception. */
|
|
563
|
+
async function chatCall(options) {
|
|
564
|
+
let response;
|
|
565
|
+
try {
|
|
566
|
+
response = await fetch(options.url, options.init);
|
|
567
|
+
} catch (error) {
|
|
568
|
+
return { ok: false, error: `the plugin could not be reached: ${error instanceof Error ? error.message : String(error)}` };
|
|
569
|
+
}
|
|
570
|
+
let body = null;
|
|
571
|
+
try {
|
|
572
|
+
body = await response.json();
|
|
573
|
+
} catch {
|
|
574
|
+
return { ok: false, error: `the plugin answered HTTP ${response.status} with a body that is not JSON` };
|
|
575
|
+
}
|
|
576
|
+
if (response.status === 404) return { ok: false, error: 'this host does not serve the W2M chat route' };
|
|
577
|
+
if (response.ok !== true || body?.ok === false) {
|
|
578
|
+
return { ok: false, error: typeof body?.error === 'string' ? body.error : `HTTP ${response.status}` };
|
|
579
|
+
}
|
|
580
|
+
return { ok: true, state: body };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* The fleet-chat control: opens a panel with the session's messages, a compose box and the
|
|
585
|
+
* relay's current usage. The text stays on the relay; this component keeps only the last view.
|
|
586
|
+
*/
|
|
587
|
+
function FleetChat({ sessionId }) {
|
|
588
|
+
const [open, setOpen] = React.useState(false);
|
|
589
|
+
const [messages, setMessages] = React.useState([]);
|
|
590
|
+
const [usage, setUsage] = React.useState(null);
|
|
591
|
+
const [text, setText] = React.useState('');
|
|
592
|
+
const [error, setError] = React.useState(null);
|
|
593
|
+
const [busy, setBusy] = React.useState(false);
|
|
594
|
+
|
|
595
|
+
const load = React.useCallback(async (signal) => {
|
|
596
|
+
if (typeof sessionId !== 'string' || sessionId === '') return;
|
|
597
|
+
const result = await chatCall({
|
|
598
|
+
url: `${CHAT_ROUTE}?session_id=${encodeURIComponent(sessionId)}&limit=50`,
|
|
599
|
+
init: { method: 'GET', credentials: 'same-origin', headers: { accept: 'application/json' }, signal },
|
|
600
|
+
});
|
|
601
|
+
if (result.ok) {
|
|
602
|
+
setMessages(Array.isArray(result.state.messages) ? result.state.messages : []);
|
|
603
|
+
setUsage(result.state.usage ?? null);
|
|
604
|
+
setError(null);
|
|
605
|
+
} else {
|
|
606
|
+
setError(result.error);
|
|
607
|
+
}
|
|
608
|
+
}, [sessionId]);
|
|
609
|
+
|
|
610
|
+
React.useEffect(() => {
|
|
611
|
+
if (!open) return undefined;
|
|
612
|
+
const controller = new AbortController();
|
|
613
|
+
load(controller.signal);
|
|
614
|
+
const timer = setInterval(() => load(controller.signal), 3000);
|
|
615
|
+
return () => {
|
|
616
|
+
controller.abort();
|
|
617
|
+
clearInterval(timer);
|
|
618
|
+
};
|
|
619
|
+
}, [open, load]);
|
|
620
|
+
|
|
621
|
+
const send = React.useCallback(async () => {
|
|
622
|
+
const value = text.trim();
|
|
623
|
+
if (value === '' || busy) return undefined;
|
|
624
|
+
setBusy(true);
|
|
625
|
+
const result = await chatCall({
|
|
626
|
+
url: CHAT_ROUTE,
|
|
627
|
+
init: {
|
|
628
|
+
method: 'POST',
|
|
629
|
+
credentials: 'same-origin',
|
|
630
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
631
|
+
body: JSON.stringify({ session_id: sessionId, text: value }),
|
|
632
|
+
},
|
|
633
|
+
});
|
|
634
|
+
setBusy(false);
|
|
635
|
+
if (result.ok) {
|
|
636
|
+
setText('');
|
|
637
|
+
await load();
|
|
638
|
+
} else {
|
|
639
|
+
setError(result.error);
|
|
640
|
+
}
|
|
641
|
+
return undefined;
|
|
642
|
+
}, [busy, load, sessionId, text]);
|
|
643
|
+
|
|
644
|
+
const disabled = typeof sessionId !== 'string' || sessionId === '';
|
|
645
|
+
const usageText = usage === null ? null : `${Math.round((usage.used_24h_bytes ?? 0) / 1024)} KiB/24h · ${usage.messages ?? 0} 条`;
|
|
646
|
+
const machineLabel = (message) => {
|
|
647
|
+
const id = typeof message?.machine_id === 'string' ? message.machine_id : '?';
|
|
648
|
+
return id.length > 12 ? `${id.slice(0, 8)}…` : id;
|
|
649
|
+
};
|
|
650
|
+
const timeLabel = (message) => (Number.isFinite(message?.at_ms) ? new Date(message.at_ms).toLocaleTimeString() : '');
|
|
651
|
+
|
|
652
|
+
return h('span', { className: 'w2mChat' },
|
|
653
|
+
h('button', {
|
|
654
|
+
type: 'button',
|
|
655
|
+
className: 'w2mChat_trigger',
|
|
656
|
+
title: '编队聊天(本会话广播)',
|
|
657
|
+
'aria-expanded': open,
|
|
658
|
+
disabled,
|
|
659
|
+
onClick: () => setOpen((value) => !value),
|
|
660
|
+
}, '💬', h('span', { className: 'w2mChat_label' }, '编队聊天')),
|
|
661
|
+
open
|
|
662
|
+
? h('div', { className: 'w2mChat_panel', role: 'dialog', 'aria-label': '编队聊天' },
|
|
663
|
+
h('div', { className: 'w2mChat_head' },
|
|
664
|
+
h('span', null, '编队聊天'),
|
|
665
|
+
usageText === null ? null : h('span', { className: 'w2mChat_usage' }, usageText),
|
|
666
|
+
h('button', { type: 'button', className: 'w2mChat_close', title: '关闭', onClick: () => setOpen(false) }, '×')),
|
|
667
|
+
h('div', { className: 'w2mChat_list' },
|
|
668
|
+
error !== null ? h('div', { className: 'w2mChat_error' }, error) : null,
|
|
669
|
+
messages.length === 0
|
|
670
|
+
? (error === null ? h('div', { className: 'w2mChat_empty' }, '本会话还没有消息') : null)
|
|
671
|
+
: messages.map((message) => h('div', { key: message?.message_id ?? `${message?.machine_id}:${message?.at_ms}`, className: 'w2mChat_msg' },
|
|
672
|
+
h('div', { className: 'w2mChat_meta' }, `${machineLabel(message)} · ${timeLabel(message)}`),
|
|
673
|
+
h('div', { className: 'w2mChat_text' }, message?.text ?? '')))),
|
|
674
|
+
h('div', { className: 'w2mChat_compose' },
|
|
675
|
+
h('textarea', {
|
|
676
|
+
className: 'w2mChat_input',
|
|
677
|
+
value: text,
|
|
678
|
+
rows: 2,
|
|
679
|
+
placeholder: '说点什么,发给本会话的机器…',
|
|
680
|
+
disabled: busy,
|
|
681
|
+
onChange: (event) => setText(event.target.value),
|
|
682
|
+
onKeyDown: (event) => {
|
|
683
|
+
if (event.key === 'Enter' && event.shiftKey !== true) {
|
|
684
|
+
event.preventDefault();
|
|
685
|
+
send();
|
|
686
|
+
}
|
|
687
|
+
},
|
|
688
|
+
}),
|
|
689
|
+
h('button', { type: 'button', className: 'w2mChat_send', disabled: busy || text.trim() === '', onClick: send }, busy ? '发送中…' : '发送')))
|
|
690
|
+
: null);
|
|
691
|
+
}
|
|
692
|
+
|
|
533
693
|
/**
|
|
534
694
|
* Client plugin body: install the stylesheet, register the dictionaries, take the seat.
|
|
535
695
|
*
|
|
@@ -607,6 +767,24 @@ window.__ModuleLoader__.load({
|
|
|
607
767
|
t,
|
|
608
768
|
}),
|
|
609
769
|
}, TargetSelect));
|
|
770
|
+
|
|
771
|
+
// The fleet-chat panel: the same session-scoped seat, one slot after the target control.
|
|
772
|
+
// Wrapped in its own try/catch on purpose — the target control is the primary half of this
|
|
773
|
+
// bundle, and a chat panel that cannot mount must not take the page (or the app) down with it.
|
|
774
|
+
try {
|
|
775
|
+
slots.inject('conversation.input.left', () => slots.register({
|
|
776
|
+
name: 'conversation.input.left',
|
|
777
|
+
id: 'w2m-chat',
|
|
778
|
+
order: 41,
|
|
779
|
+
label: 'fleet chat',
|
|
780
|
+
inject: (sessionId) => ({ sessionId }),
|
|
781
|
+
}, FleetChat));
|
|
782
|
+
} catch (error) {
|
|
783
|
+
console.error(
|
|
784
|
+
'@twinsearth/w2m-dsh-plugin: the fleet chat panel was not mounted; the rest of the page is unaffected',
|
|
785
|
+
error,
|
|
786
|
+
);
|
|
787
|
+
}
|
|
610
788
|
}
|
|
611
789
|
|
|
612
790
|
/**
|
|
@@ -633,6 +811,7 @@ window.__ModuleLoader__.load({
|
|
|
633
811
|
exports.apply = apply;
|
|
634
812
|
exports.inject = inject;
|
|
635
813
|
exports.TargetSelect = TargetSelect;
|
|
814
|
+
exports.FleetChat = FleetChat;
|
|
636
815
|
exports.injectStyle = injectStyle;
|
|
637
816
|
exports.makeTranslator = makeTranslator;
|
|
638
817
|
return module.exports;
|