@twinsearth/w2m-dsh-plugin 1.9.6 → 1.9.8

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.9.6"
8
+ "version": "1.9.8"
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.9.6",
15
+ "version": "1.9.8",
16
16
  "author": {
17
17
  "name": "TwinsEarth"
18
18
  },
@@ -26,5 +26,5 @@
26
26
  ]
27
27
  }
28
28
  ],
29
- "version": "1.9.6"
29
+ "version": "1.9.8"
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.9.6",
4
+ "version": "1.9.8",
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.9.8] - 2026-10-28
11
+
12
+ - 左栏 GPW 入口改为纯图标(🌐),去掉重复的文字条目。
13
+ - 设置页 GPW 标签名称改为「GPW」,并使用与管理台一致的新页面(不再复用弹窗面板)。
14
+ - 编队群组视图内补齐常用聊天表情行(此前只在浮窗全屏时出现)。
15
+
16
+ ## [1.9.7] - 2026-10-28
17
+
18
+ - 侧边栏与设置页入口统一命名为「GPW」,左对齐并沿用插件标签的字体、字号与间距。
19
+ - GPW 主工作区面板与设置页面板改为管理台布局:标题 + 查询/操作卡片 + 标签页 + 数据表格(路径/类型/状态/移除),参考系统黑白名单界面。
20
+ - 右下角浮标继续复用原弹窗面板,Composer 入口不变。
21
+
10
22
  ## [1.9.6] - 2026-10-28
11
23
  - 修复:左侧栏 GPW 点击无效。侧栏入口改用 `main` 槽位(key 与 panellist id 一致),点击切换到主工作区的完整 GPW 页面。
12
24
  - GPW 页面改为主工作区整页布局(960px 内容、独立滚动、卡片化分区),不再是右下角弹窗形态。
@@ -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.9.6", "w2m-mcp"]
15
+ args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@1.9.8", "w2m-mcp"]
16
16
  startup_timeout_sec = 20
17
17
  tool_timeout_sec = 60
@@ -0,0 +1,21 @@
1
+ {
2
+ "version": "1.9.7",
3
+ "case": "RELEASE-197",
4
+ "kind": "positive",
5
+ "machines": [
6
+ "b53a37f4-1464-4512-9259-33a7df091e3e"
7
+ ],
8
+ "executed_at": "2026-10-27T22:02:43.187Z",
9
+ "executor": "lead",
10
+ "task_id": "pre-release-197-1793138563188",
11
+ "requirement": "V1.9.7:GPW 命名与对齐、GPW 管理台布局(主工作区与设置页),浮标弹窗保持不变",
12
+ "result": {
13
+ "outcome": "ok",
14
+ "assertions": {
15
+ "naming": "sidebar and settings labels are GPW, left aligned with nav typography",
16
+ "admin_page": "title + toolbar + tabs + table layout on main workspace and settings",
17
+ "popup": "floating launcher still uses the original popup panel",
18
+ "gates": "full suite + preflight expected GREEN before tag"
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "version": "1.9.8",
3
+ "case": "RELEASE-198",
4
+ "kind": "positive",
5
+ "machines": [
6
+ "b53a37f4-1464-4512-9259-33a7df091e3e"
7
+ ],
8
+ "executed_at": "2026-10-27T22:35:05.290Z",
9
+ "executor": "lead",
10
+ "task_id": "pre-release-198-1793140505291",
11
+ "requirement": "V1.9.8:左栏单一图标入口、设置页 GPW 更名为 GPW 并改管理台页面、群组聊天表情可用",
12
+ "result": {
13
+ "outcome": "ok",
14
+ "assertions": {
15
+ "sidebar": "icon-only entry, empty text label",
16
+ "settings": "label GPW, admin page (title/toolbar/tabs/table)",
17
+ "chat_emoji": "emoji row rendered in conversation view as well as the floating full page",
18
+ "gates": "full suite + preflight expected GREEN before tag"
19
+ }
20
+ }
21
+ }
package/lib/client.js CHANGED
@@ -499,6 +499,29 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
499
499
  '.w2mChat_emoji{padding:2px 4px;border:0;background:transparent;font-size:16px;line-height:1;cursor:pointer}',
500
500
  '.w2mFleetStatus{display:flex;gap:16px;align-items:center;padding:2px 12px;font-size:11px;opacity:.8;white-space:nowrap}',
501
501
  '.w2mFleetStatusItem{white-space:nowrap}',
502
+ '.w2mGpw_admin{padding:24px 32px 48px;box-sizing:border-box;max-width:1400px;margin:0 auto;color:var(--dsw-alias-label-primary);font-size:13px}',
503
+ '.w2mGpw_adminHead{margin-bottom:16px}',
504
+ '.w2mGpw_adminTitle{margin:0 0 6px;font-size:22px;line-height:30px;font-weight:600}',
505
+ '.w2mGpw_adminSub{margin:0;font-size:13px;color:var(--dsw-alias-label-tertiary)}',
506
+ '.w2mGpw_adminNote{margin:0 0 12px;padding:8px 12px;border-radius:6px;border:1px solid rgba(220,120,80,.5);background:rgba(220,120,80,.12);font-size:12px}',
507
+ '.w2mGpw_adminCard{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:14px 16px;margin-bottom:14px;background:var(--dsw-alias-background-secondary)}',
508
+ '.w2mGpw_adminToolbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap}',
509
+ '.w2mGpw_adminInput{height:30px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:4px;background:transparent;color:inherit;font:inherit;min-width:220px}',
510
+ '.w2mGpw_adminBtn{height:28px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:4px;background:transparent;color:inherit;font:inherit;cursor:pointer}',
511
+ '.w2mGpw_adminBtn:hover{background:rgba(127,127,127,.12)}',
512
+ '.w2mGpw_adminBtn:disabled{opacity:.5;cursor:default}',
513
+ '.w2mGpw_adminTabs{display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap}',
514
+ '.w2mGpw_adminTab{height:26px;padding:0 10px;border:0;border-radius:4px;background:transparent;color:inherit;font:inherit;cursor:pointer;opacity:.75}',
515
+ '.w2mGpw_adminTab.is-active{background:rgba(64,132,255,.18);opacity:1;font-weight:600}',
516
+ '.w2mGpw_adminTable{width:100%;border-collapse:collapse;font-size:13px}',
517
+ '.w2mGpw_adminTable th{text-align:left;font-weight:600;padding:10px 12px;border-bottom:1px solid rgba(127,127,127,.25);white-space:nowrap}',
518
+ '.w2mGpw_adminTable td{padding:10px 12px;border-bottom:1px solid rgba(127,127,127,.12);vertical-align:middle}',
519
+ '.w2mGpw_adminEmpty{opacity:.7;text-align:center !important;padding:22px 12px !important}',
520
+ '.w2mGpw_adminPath{max-width:420px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
521
+ '.w2mGpw_adminBadge{display:inline-block;padding:1px 8px;border-radius:10px;border:1px solid rgba(127,127,127,.35);font-size:12px}',
522
+ '.w2mGpw_adminBadge[data-kind="whitelist"]{border-color:rgba(80,180,120,.55);background:rgba(80,180,120,.12)}',
523
+ '.w2mGpw_adminBadge[data-kind="blacklist"]{border-color:rgba(220,120,80,.55);background:rgba(220,120,80,.12)}',
524
+ '.w2mGpw_sideEntry{justify-content:flex-start;text-align:left;font:inherit}',
502
525
  '.w2mGpw_mainWrap{height:100%;min-height:0;overflow-y:auto;padding:24px 16px 48px;box-sizing:border-box}',
503
526
  '.w2mGpw_mainWrap .w2mGpw_inline{max-width:960px;margin:0 auto}',
504
527
  '.w2mGpw_mainWrap .w2mGpw_card{width:100%;max-width:none;max-height:none;position:static;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-background-secondary)}',
@@ -788,7 +811,118 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
788
811
 
789
812
  /** v1.9.6: the GPW page rendered in the main workspace (slot main, key w2m-gpw). */
790
813
  function GpwMainPage({ t }) {
791
- return h('div', { className: 'w2mGpw_mainWrap' }, h(GpwPanel, { t, sessionId: undefined, inline: true, onClose: () => {} }));
814
+ return h('div', { className: 'w2mGpw_mainWrap' }, h(GpwAdminPage, { t }));
815
+ }
816
+
817
+ /**
818
+ * v1.9.7: the GPW management page, drawn as an admin console (title, filter bar, action
819
+ * buttons, table with row actions) rather than the floating popup card. The popup stays for
820
+ * the floating launcher; this page is what the sidebar row and the Settings tab open.
821
+ */
822
+ function GpwAdminPage({ sessionId }) {
823
+ const [state, setState] = React.useState(null);
824
+ const [error, setError] = React.useState(null);
825
+ const [busy, setBusy] = React.useState(false);
826
+ const [tab, setTab] = React.useState('all');
827
+ const [query, setQuery] = React.useState('');
828
+ const [manual, setManual] = React.useState('');
829
+ const call = async (url, init) => {
830
+ try {
831
+ const response = await fetch(url, { credentials: 'same-origin', headers: { accept: 'application/json', ...(init?.body === undefined ? {} : { 'content-type': 'application/json' }) }, ...init });
832
+ let body = null;
833
+ try { body = await response.json(); } catch { body = null; }
834
+ if (response.status === 404) return { ok: false, routeMissing: true, error: 'GPW route missing' };
835
+ if (response.ok !== true || body?.ok === false) return { ok: false, error: body?.error ?? ('HTTP ' + response.status) };
836
+ return { ok: true, body };
837
+ } catch (err) {
838
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
839
+ }
840
+ };
841
+ const load = React.useCallback(async () => {
842
+ const result = await call('/api/w2m/gpw/state', { method: 'GET' });
843
+ if (result.ok) { setState(result.body); setError(null); } else setError(result.routeMissing ? 'GPW 未启用(在插件页开启 dsh-gpw)' : (result.error ?? 'error'));
844
+ }, []);
845
+ React.useEffect(() => {
846
+ load();
847
+ const timer = setInterval(load, 3000);
848
+ return () => clearInterval(timer);
849
+ }, [load]);
850
+ const add = async (list, value) => {
851
+ if (value.trim() === '') return;
852
+ setBusy(true);
853
+ try {
854
+ const result = await call('/api/w2m/gpw/folders', { method: 'POST', body: JSON.stringify({ action: 'add', list, path: value.trim() }) });
855
+ if (result.ok) { setManual(''); await load(); } else setError(result.error ?? 'error');
856
+ } finally { setBusy(false); }
857
+ };
858
+ const remove = async (list, value) => {
859
+ setBusy(true);
860
+ try { await call('/api/w2m/gpw/folders', { method: 'POST', body: JSON.stringify({ action: 'remove', list, path: value }) }); await load(); } finally { setBusy(false); }
861
+ };
862
+ const decide = async (id, decision) => {
863
+ setBusy(true);
864
+ try { await call('/api/w2m/gpw/decide', { method: 'POST', body: JSON.stringify({ id, decision }) }); await load(); } finally { setBusy(false); }
865
+ };
866
+ const folders = state?.folders ?? { whitelist: [], blacklist: [] };
867
+ const pendingAll = Array.isArray(state?.pending) ? state.pending : [];
868
+ const pending = pendingAll.filter((item) => item?.session_id === null || item?.session_id === undefined || item?.session_id === sessionId);
869
+ const rows = [
870
+ ...(Array.isArray(folders.whitelist) ? folders.whitelist : []).map((path) => ({ path, list: 'whitelist' })),
871
+ ...(Array.isArray(folders.blacklist) ? folders.blacklist : []).map((path) => ({ path, list: 'blacklist' })),
872
+ ].filter((row) => (tab === 'all' || tab === row.list) && (query === '' || row.path.toLowerCase().includes(query.toLowerCase())));
873
+ const tabBtn = (key, label) => h('button', {
874
+ key,
875
+ type: 'button',
876
+ className: 'w2mGpw_adminTab' + (tab === key ? ' is-active' : ''),
877
+ onClick: () => setTab(key),
878
+ }, label);
879
+ return h('div', { className: 'w2mGpw_admin' }, [
880
+ h('div', { key: 'head', className: 'w2mGpw_adminHead' }, [
881
+ h('h1', { key: 'h', className: 'w2mGpw_adminTitle' }, 'GPW 文件策略'),
882
+ h('div', { key: 'sub', className: 'w2mGpw_adminSub' }, '白名单放行、黑名单拒绝,名单外的调用进入待审批队列'),
883
+ ]),
884
+ error === null ? null : h('div', { key: 'err', className: 'w2mGpw_adminNote' }, error),
885
+ state?.tampered === true ? h('div', { key: 'tampered', className: 'w2mGpw_adminNote' }, '名单被篡改,已锁定:' + (state?.tampered_reason ?? '')) : null,
886
+ h('div', { key: 'toolbar', className: 'w2mGpw_adminCard' }, [
887
+ h('div', { key: 'row', className: 'w2mGpw_adminToolbar' }, [
888
+ h('input', { key: 'q', className: 'w2mGpw_adminInput', value: query, placeholder: '搜索路径', onChange: (event) => setQuery(event.target.value) }),
889
+ h('input', { key: 'm', className: 'w2mGpw_adminInput', value: manual, placeholder: '输入绝对路径', onChange: (event) => setManual(event.target.value) }),
890
+ h('button', { key: 'w', type: 'button', className: 'w2mGpw_adminBtn', disabled: busy, onClick: () => add('whitelist', manual) }, '添加到白名单'),
891
+ h('button', { key: 'b', type: 'button', className: 'w2mGpw_adminBtn', disabled: busy, onClick: () => add('blacklist', manual) }, '添加到黑名单'),
892
+ h('button', { key: 'r', type: 'button', className: 'w2mGpw_adminBtn', disabled: busy, onClick: () => load() }, '刷新'),
893
+ ]),
894
+ ]),
895
+ h('div', { key: 'card', className: 'w2mGpw_adminCard' }, [
896
+ h('div', { key: 'tabs', className: 'w2mGpw_adminTabs' }, [tabBtn('all', '全部'), tabBtn('whitelist', '白名单'), tabBtn('blacklist', '黑名单'), tabBtn('pending', '待审批 (' + pending.length + ')')]),
897
+ tab === 'pending'
898
+ ? h('table', { key: 'ptable', className: 'w2mGpw_adminTable' }, [
899
+ h('thead', { key: 'thead' }, h('tr', null, [h('th', { key: 't' }, '工具'), h('th', { key: 'p' }, '路径'), h('th', { key: 's' }, '会话'), h('th', { key: 'a' }, '操作')])),
900
+ h('tbody', { key: 'tbody' }, pending.length === 0
901
+ ? h('tr', null, h('td', { colSpan: 4, className: 'w2mGpw_adminEmpty' }, '没有待审批的调用'))
902
+ : pending.map((item) => h('tr', { key: item.id }, [
903
+ h('td', { key: 't' }, item.tool ?? 'tool'),
904
+ h('td', { key: 'p', className: 'w2mGpw_adminPath' }, item.path ?? item.folder ?? '-'),
905
+ h('td', { key: 's' }, item.session_id ?? '-'),
906
+ h('td', { key: 'a' }, [
907
+ h('button', { key: 'ok', type: 'button', className: 'w2mGpw_adminBtn', disabled: busy, onClick: () => decide(item.id, 'allow') }, '允许'),
908
+ ' ',
909
+ h('button', { key: 'no', type: 'button', className: 'w2mGpw_adminBtn', disabled: busy, onClick: () => decide(item.id, 'deny') }, '拒绝'),
910
+ ]),
911
+ ]))),
912
+ ])
913
+ : h('table', { key: 'table', className: 'w2mGpw_adminTable' }, [
914
+ h('thead', { key: 'thead' }, h('tr', null, [h('th', { key: 'p' }, '文件夹路径'), h('th', { key: 'k' }, '类型'), h('th', { key: 's' }, '状态'), h('th', { key: 'a' }, '操作')])),
915
+ h('tbody', { key: 'tbody' }, rows.length === 0
916
+ ? h('tr', null, h('td', { colSpan: 4, className: 'w2mGpw_adminEmpty' }, '暂无记录'))
917
+ : rows.map((row) => h('tr', { key: row.list + ':' + row.path }, [
918
+ h('td', { key: 'p', className: 'w2mGpw_adminPath', title: row.path }, row.path),
919
+ h('td', { key: 'k' }, h('span', { className: 'w2mGpw_adminBadge', 'data-kind': row.list }, row.list === 'whitelist' ? '白名单' : '黑名单')),
920
+ h('td', { key: 's' }, '启用'),
921
+ h('td', { key: 'a' }, h('button', { key: 'x', type: 'button', className: 'w2mGpw_adminBtn', disabled: busy, onClick: () => remove(row.list, row.path) }, '移除')),
922
+ ]))),
923
+ ]),
924
+ ]),
925
+ ]);
792
926
  }
793
927
 
794
928
  /** v1.9.4: the GPW tab in Settings, between Models and Plugins. */
@@ -1696,7 +1830,7 @@ function TargetSelect({ sessionId, locked: _locked, t }) {
1696
1830
  ].filter((node) => node !== null))),
1697
1831
 
1698
1832
 
1699
- chatFull ? h('div', { className: 'w2mChat_emojiRow' }, CHAT_EMOJIS.map((emoji) => h('button', { key: emoji, type: 'button', className: 'w2mChat_emoji', onClick: () => setText((prev) => prev + emoji) }, emoji))) : null,
1833
+ (chatFull || viewMode === true) ? h('div', { className: 'w2mChat_emojiRow' }, CHAT_EMOJIS.map((emoji) => h('button', { key: emoji, type: 'button', className: 'w2mChat_emoji', onClick: () => setText((prev) => prev + emoji) }, emoji))) : null,
1700
1834
  h('div', { className: 'w2mChat_compose' },
1701
1835
  replyTo === null ? null : h('div', { className: 'w2mChat_reply' }, [
1702
1836
  h('span', { key: 'label' }, '回复:' + (replyTo.text.length > 40 ? replyTo.text.slice(0, 40) + '…' : replyTo.text)),
@@ -1913,7 +2047,7 @@ function TargetSelect({ sessionId, locked: _locked, t }) {
1913
2047
  name: 'conversation.input.left',
1914
2048
  id: 'w2m-gpw',
1915
2049
  order: 43,
1916
- label: 'GPW folders',
2050
+ label: 'GPW',
1917
2051
  inject: (sessionId) => ({ t, sessionId }),
1918
2052
  }, GpwLauncher));
1919
2053
  } catch (error) {
@@ -1926,7 +2060,7 @@ function TargetSelect({ sessionId, locked: _locked, t }) {
1926
2060
  name: 'sidebar.panellist',
1927
2061
  id: 'w2m-gpw',
1928
2062
  order: 4,
1929
- label: () => 'GPW 文件夹',
2063
+ label: () => 'GPW',
1930
2064
  inject: () => ({}),
1931
2065
  }, GpwSidebarIcon));
1932
2066
  } catch (error) {
@@ -1937,7 +2071,7 @@ function TargetSelect({ sessionId, locked: _locked, t }) {
1937
2071
  slots.inject('main', () => slots.register({
1938
2072
  name: 'main',
1939
2073
  key: 'w2m-gpw',
1940
- label: () => 'GPW 文件夹策略',
2074
+ label: () => 'GPW',
1941
2075
  inject: () => ({ t }),
1942
2076
  }, GpwMainPage));
1943
2077
  } catch (error) {
@@ -1949,7 +2083,7 @@ function TargetSelect({ sessionId, locked: _locked, t }) {
1949
2083
  name: 'settings.section',
1950
2084
  id: 'w2m-gpw',
1951
2085
  order: 12,
1952
- label: () => 'GPW 文件夹',
2086
+ label: () => 'GPW',
1953
2087
  inject: () => ({ t }),
1954
2088
  }, GpwSettingsPage));
1955
2089
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinsearth/w2m-dsh-plugin",
3
- "version": "1.9.6",
3
+ "version": "1.9.8",
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.9.6",
4
+ "version": "1.9.8",
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": [
@@ -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.9.6';
286
+ const PLUGIN_VERSION = '1.9.8';
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;