@twinsearth/w2m-dsh-plugin 1.9.6 → 1.9.7

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.7"
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.7",
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.7"
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.7",
5
5
  "author": {
6
6
  "name": "TwinsEarth"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@
7
7
 
8
8
  All notable changes to this project are documented here.
9
9
 
10
+ ## [1.9.7] - 2026-10-28
11
+
12
+ - 侧边栏与设置页入口统一命名为「GPW」,左对齐并沿用插件标签的字体、字号与间距。
13
+ - GPW 主工作区面板与设置页面板改为管理台布局:标题 + 查询/操作卡片 + 标签页 + 数据表格(路径/类型/状态/移除),参考系统黑白名单界面。
14
+ - 右下角浮标继续复用原弹窗面板,Composer 入口不变。
15
+
10
16
  ## [1.9.6] - 2026-10-28
11
17
  - 修复:左侧栏 GPW 点击无效。侧栏入口改用 `main` 槽位(key 与 panellist id 一致),点击切换到主工作区的完整 GPW 页面。
12
18
  - 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.7", "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
+ }
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. */
@@ -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) {
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.7",
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.7",
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.7';
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;