@twinsearth/w2m-dsh-plugin 1.2.0 → 1.2.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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +9165 -9079
- package/README.md +3 -2
- package/codex/mcp-servers.toml +1 -1
- package/lib/client.js +29 -3
- package/package.json +98 -93
- package/plugin.json +1 -1
- package/src/plugin/council-fleet.mjs +221 -0
- package/src/plugin/dataflow.mjs +72 -0
- package/src/plugin/fleet-chat-route.mjs +58 -2
- package/src/plugin/intents.mjs +137 -0
- package/src/plugin/memory-view.mjs +60 -0
- package/src/plugin/tools.mjs +193 -8
- package/src/relay/fleet-memory.mjs +85 -0
- package/src/relay/fleet-messages.mjs +33 -5
- package/src/relay/server.mjs +42 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ English | [中文](#中文说明)
|
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
17
|
-
Five routes in. They all end with **
|
|
17
|
+
Five routes in. They all end with **13 tools registered** in your agent. Pick one.
|
|
18
18
|
|
|
19
19
|
### 1. DSH plugin (recommended — zero configuration)
|
|
20
20
|
|
|
@@ -137,6 +137,7 @@ hand-maintained host list.
|
|
|
137
137
|
|
|
138
138
|
`w2m_status` · `w2m_devices` · `w2m_run` · `w2m_wait` · `w2m_history` · `w2m_stats` · `w2m_report` ·
|
|
139
139
|
`w2m_memory` · `w2m_approvals` · `w2m_council` · `w2m_chat` · `w2m_update`
|
|
140
|
+
- `w2m_intent` — map a structured intent (run-tests, node-version, git-status, git-head, fleet-facts, shard-probe) onto an allow-listed command; unknown intents are refused by name, never guessed.
|
|
140
141
|
|
|
141
142
|
### Environment a dispatched command receives (1.1.15+)
|
|
142
143
|
|
|
@@ -260,7 +261,7 @@ MIT.
|
|
|
260
261
|
|
|
261
262
|
### 安装
|
|
262
263
|
|
|
263
|
-
五种方式,最终都会在你的 agent 里注册 **
|
|
264
|
+
五种方式,最终都会在你的 agent 里注册 **13 tools registered**。任选其一。
|
|
264
265
|
|
|
265
266
|
#### 1. DSH 插件(推荐,零配置)
|
|
266
267
|
|
package/codex/mcp-servers.toml
CHANGED
package/lib/client.js
CHANGED
|
@@ -295,6 +295,11 @@ window.__ModuleLoader__.load({
|
|
|
295
295
|
'.w2mChat_empty{color:var(--dsw-alias-label-tertiary)}',
|
|
296
296
|
'.w2mChat_msg{display:flex;flex-direction:column;gap:2px}',
|
|
297
297
|
'.w2mChat_meta{color:var(--dsw-alias-label-tertiary);font-size:11px}',
|
|
298
|
+
'.w2mChat_to{font:inherit;font-size:11px;max-width:120px;border-radius:6px;border:.5px solid var(--dsw-alias-border-default);background:var(--dsw-alias-surface-default);color:var(--dsw-alias-label-primary)}',
|
|
299
|
+
'.w2mChat_reply{display:flex;align-items:center;gap:6px;padding:4px 10px;font-size:11px;color:var(--dsw-alias-label-secondary);border-top:.5px solid var(--dsw-alias-border-default)}',
|
|
300
|
+
'.w2mChat_replyCancel{margin-left:auto;border:none;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer}',
|
|
301
|
+
'.w2mChat_msg{cursor:pointer}',
|
|
302
|
+
'.w2mChat_thread{margin-left:6px;opacity:.7}',
|
|
298
303
|
'.w2mChat_text{white-space:pre-wrap;word-break:break-word}',
|
|
299
304
|
'.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}',
|
|
300
305
|
'.w2mChat_compose{display:flex;gap:8px;padding:8px 10px;border-top:.5px solid var(--dsw-alias-border-l2);align-items:flex-end}',
|
|
@@ -614,6 +619,8 @@ window.__ModuleLoader__.load({
|
|
|
614
619
|
const [usage, setUsage] = React.useState(null);
|
|
615
620
|
const [localMachineName, setLocalMachineName] = React.useState(null);
|
|
616
621
|
const [roster, setRoster] = React.useState(null);
|
|
622
|
+
const [toRecipient, setToRecipient] = React.useState('all');
|
|
623
|
+
const [replyTo, setReplyTo] = React.useState(null);
|
|
617
624
|
const [text, setText] = React.useState('');
|
|
618
625
|
const [error, setError] = React.useState(null);
|
|
619
626
|
const [busy, setBusy] = React.useState(false);
|
|
@@ -670,12 +677,18 @@ window.__ModuleLoader__.load({
|
|
|
670
677
|
method: 'POST',
|
|
671
678
|
credentials: 'same-origin',
|
|
672
679
|
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
673
|
-
body: JSON.stringify({
|
|
680
|
+
body: JSON.stringify({
|
|
681
|
+
session_id: sessionId,
|
|
682
|
+
text: value,
|
|
683
|
+
...(toRecipient === 'all' ? {} : { to: toRecipient }),
|
|
684
|
+
...(replyTo === null ? {} : { reply_to: replyTo.id, ...(replyTo.threadId === null ? {} : { thread_id: replyTo.threadId }) }),
|
|
685
|
+
}),
|
|
674
686
|
},
|
|
675
687
|
});
|
|
676
688
|
setBusy(false);
|
|
677
689
|
if (result.ok) {
|
|
678
690
|
setText('');
|
|
691
|
+
setReplyTo(null);
|
|
679
692
|
await load();
|
|
680
693
|
} else {
|
|
681
694
|
setError(result.error);
|
|
@@ -713,6 +726,15 @@ window.__ModuleLoader__.load({
|
|
|
713
726
|
open
|
|
714
727
|
? h('div', { className: 'w2mChat_panel', role: 'dialog', 'aria-label': '编队聊天' },
|
|
715
728
|
h('div', { className: 'w2mChat_head' },
|
|
729
|
+
h('select', {
|
|
730
|
+
className: 'w2mChat_to',
|
|
731
|
+
value: toRecipient,
|
|
732
|
+
title: '收件人',
|
|
733
|
+
onChange: (event) => setToRecipient(event.target.value),
|
|
734
|
+
}, [
|
|
735
|
+
h('option', { key: '__all__', value: 'all' }, '全部'),
|
|
736
|
+
...(Array.isArray(roster?.machines) ? roster.machines.filter((machine) => machine.online === true && typeof machine.machine_id === 'string').map((machine) => h('option', { key: machine.machine_id, value: machine.machine_id }, machine.name ?? machine.machine_id)) : []),
|
|
737
|
+
]),
|
|
716
738
|
h('span', null, '编队聊天'),
|
|
717
739
|
usageText === null ? null : h('span', { className: 'w2mChat_usage' }, usageText),
|
|
718
740
|
h('button', { type: 'button', className: 'w2mChat_close', title: '关闭', onClick: () => setOpen(false) }, '×')),
|
|
@@ -722,13 +744,17 @@ window.__ModuleLoader__.load({
|
|
|
722
744
|
? (error === null ? h('div', { className: 'w2mChat_empty' }, '本会话还没有消息') : null)
|
|
723
745
|
: messages.map((message) => {
|
|
724
746
|
var kind = messageKind(message, localMachineId);
|
|
725
|
-
return h('div', { key: message?.message_id ?? `${message?.machine_id}:${message?.at_ms}`, className: 'w2mChat_msg is-' + kind },
|
|
726
|
-
h('div', { className: 'w2mChat_meta' }, h(WhaleAvatar, { machineId: message?.machine_id }), `${machineLabel(message)} · ${timeLabel(message)}`, kind === 'session' ? ' · 来自会话' : ''),
|
|
747
|
+
return h('div', { key: message?.message_id ?? `${message?.machine_id}:${message?.at_ms}`, className: 'w2mChat_msg is-' + kind, onClick: () => setReplyTo({ id: message?.message_id ?? null, threadId: message?.thread_id ?? null, text: typeof message?.text === 'string' ? message.text : '' }) },
|
|
748
|
+
h('div', { className: 'w2mChat_meta' }, h(WhaleAvatar, { machineId: message?.machine_id }), `${machineLabel(message)} · ${timeLabel(message)}`, kind === 'session' ? ' · 来自会话' : '', message?.thread_id ? h('span', { className: 'w2mChat_thread' }, '#' + message.thread_id) : null),
|
|
727
749
|
h('div', { className: 'w2mChat_text' }, message?.text ?? ''));
|
|
728
750
|
})),
|
|
729
751
|
|
|
730
752
|
|
|
731
753
|
h('div', { className: 'w2mChat_compose' },
|
|
754
|
+
replyTo === null ? null : h('div', { className: 'w2mChat_reply' }, [
|
|
755
|
+
h('span', { key: 'label' }, '回复:' + (replyTo.text.length > 40 ? replyTo.text.slice(0, 40) + '…' : replyTo.text)),
|
|
756
|
+
h('button', { key: 'cancel', type: 'button', className: 'w2mChat_replyCancel', title: '取消回复', onClick: () => setReplyTo(null) }, '×'),
|
|
757
|
+
]),
|
|
732
758
|
h('textarea', {
|
|
733
759
|
className: 'w2mChat_input',
|
|
734
760
|
value: text,
|
package/package.json
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.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
|
-
"main": "lib/index.js",
|
|
7
|
-
"bin": {
|
|
8
|
-
"w2m-localside": "bin/w2m-localside.mjs",
|
|
9
|
-
"w2m-mcp": "bin/w2m-mcp.mjs",
|
|
10
|
-
"w2m-rabbit": "bin/w2m-rabbit.mjs",
|
|
11
|
-
"w2m-stun": "bin/w2m-stun.mjs"
|
|
12
|
-
},
|
|
13
6
|
"exports": {
|
|
14
7
|
".": "./lib/index.js",
|
|
15
8
|
"./tools": "./lib/tools.js",
|
|
@@ -20,6 +13,16 @@
|
|
|
20
13
|
"./package.json": "./package.json",
|
|
21
14
|
"./gpw": "./lib/gpw.js"
|
|
22
15
|
},
|
|
16
|
+
"dsh": {
|
|
17
|
+
"plugin": true,
|
|
18
|
+
"kind": "server",
|
|
19
|
+
"bundle": {
|
|
20
|
+
"patch": "cordis.patch.yml"
|
|
21
|
+
},
|
|
22
|
+
"client": {
|
|
23
|
+
"platform": "web"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
23
26
|
"files": [
|
|
24
27
|
"mcp.json",
|
|
25
28
|
"plugin.json",
|
|
@@ -47,80 +50,10 @@
|
|
|
47
50
|
".claude-plugin",
|
|
48
51
|
"codex"
|
|
49
52
|
],
|
|
50
|
-
"dsh": {
|
|
51
|
-
"plugin": true,
|
|
52
|
-
"kind": "server",
|
|
53
|
-
"bundle": {
|
|
54
|
-
"patch": "cordis.patch.yml"
|
|
55
|
-
},
|
|
56
|
-
"client": {
|
|
57
|
-
"platform": "web"
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
"disclosure": {
|
|
61
|
-
"cloud": true,
|
|
62
|
-
"network": [
|
|
63
|
-
"http://202.182.123.154:8787",
|
|
64
|
-
"202.182.123.154:3478",
|
|
65
|
-
"stun.cloudflare.com:3478",
|
|
66
|
-
"stun.l.google.com:19302",
|
|
67
|
-
"stun.nextcloud.com:443",
|
|
68
|
-
"ilinkai.weixin.qq.com:443",
|
|
69
|
-
"weixin.qq.com:443",
|
|
70
|
-
"api.weixin.qq.com:443"
|
|
71
|
-
],
|
|
72
|
-
"offlineMode": false,
|
|
73
|
-
"apiKeys": [
|
|
74
|
-
{
|
|
75
|
-
"env": "W2M_OPERATOR_TOKEN",
|
|
76
|
-
"storage": "profile-config"
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
"env": null,
|
|
80
|
-
"storage": "file-0600"
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
"env": null,
|
|
84
|
-
"storage": "profile-config"
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
"env": null,
|
|
88
|
-
"storage": "im-bot-token"
|
|
89
|
-
}
|
|
90
|
-
],
|
|
91
|
-
"permissions": {
|
|
92
|
-
"network": "the configured relay (default: the shared server above), STUN servers, and UDP to the peer machines. The optional chatops channels reach one instant-messaging platform each, and only when that channel is named in `channels` and the host has supplied the transport: the WeChat channel reaches ilinkai.weixin.qq.com and sends the bot token as a bearer credential, and both that host and the base url in a login response are checked against an allow-list before a token is sent anywhere",
|
|
93
|
-
"filesystem": "read/write inside the project directory and DSH_HOME; the agent writes its spool and state under --state; the chatops file fence reads inside `projectRoot` only, after resolving symlinks",
|
|
94
|
-
"env": [
|
|
95
|
-
"W2M_*"
|
|
96
|
-
]
|
|
97
|
-
},
|
|
98
|
-
"jurisdiction": [
|
|
99
|
-
"CN",
|
|
100
|
-
"JP"
|
|
101
|
-
],
|
|
102
|
-
"retention": "server"
|
|
103
|
-
},
|
|
104
53
|
"engines": {
|
|
105
54
|
"node": "\u003e=20.19.0"
|
|
106
55
|
},
|
|
107
|
-
"
|
|
108
|
-
|
|
109
|
-
},
|
|
110
|
-
"devDependencies": {
|
|
111
|
-
"@eslint/js": "^10.0.1",
|
|
112
|
-
"eslint": "^10.12.0",
|
|
113
|
-
"globals": "^17.13.0",
|
|
114
|
-
"prettier": "^3.9.9"
|
|
115
|
-
},
|
|
116
|
-
"peerDependencies": {
|
|
117
|
-
"@deepseek-ai/dsh-tools": "*"
|
|
118
|
-
},
|
|
119
|
-
"peerDependenciesMeta": {
|
|
120
|
-
"@deepseek-ai/dsh-tools": {
|
|
121
|
-
"optional": true
|
|
122
|
-
}
|
|
123
|
-
},
|
|
56
|
+
"license": "MIT",
|
|
124
57
|
"scripts": {
|
|
125
58
|
"format": "prettier --write .",
|
|
126
59
|
"format:check": "prettier --check .",
|
|
@@ -131,14 +64,14 @@
|
|
|
131
64
|
"pack": "node scripts/pack.mjs",
|
|
132
65
|
"reachability": "node scripts/reachability-check.mjs",
|
|
133
66
|
"reachability:classify": "node scripts/reachability-classify.mjs",
|
|
134
|
-
"test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs test/update-channels.test.mjs test/update-prerelease.test.mjs test/session-watch.test.mjs test/session-readonly.test.mjs test/session-sync.test.mjs test/fleet-chat-identity.test.mjs test/identity-reader.test.mjs",
|
|
67
|
+
"test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs test/update-channels.test.mjs test/update-prerelease.test.mjs test/session-watch.test.mjs test/session-readonly.test.mjs test/session-sync.test.mjs test/fleet-chat-identity.test.mjs test/identity-reader.test.mjs test/intents.test.mjs test/dataflow.test.mjs test/council-fleet.test.mjs test/fleet-memory.test.mjs test/memory-view.test.mjs",
|
|
135
68
|
"test:acceptance-doc": "node --test --test-force-exit test/acceptance-doc.test.mjs",
|
|
136
69
|
"test:account-identity": "node --test --test-force-exit test/account-identity.test.mjs",
|
|
137
70
|
"test:account-join": "node --test --test-force-exit test/account-join.test.mjs",
|
|
138
71
|
"test:admission": "node --test --test-force-exit test/admission.test.mjs",
|
|
139
72
|
"test:admission-routes": "node --test --test-force-exit test/admission-routes.test.mjs",
|
|
140
73
|
"test:agent": "node --test --test-force-exit test/agent.test.mjs test/memory.test.mjs",
|
|
141
|
-
"test:all": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/p2p-crypto.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs test/update-channels.test.mjs test/update-prerelease.test.mjs test/session-watch.test.mjs test/session-readonly.test.mjs test/session-sync.test.mjs test/fleet-chat-identity.test.mjs test/identity-reader.test.mjs",
|
|
74
|
+
"test:all": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/p2p-crypto.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs test/update-channels.test.mjs test/update-prerelease.test.mjs test/session-watch.test.mjs test/session-readonly.test.mjs test/session-sync.test.mjs test/fleet-chat-identity.test.mjs test/identity-reader.test.mjs test/intents.test.mjs test/dataflow.test.mjs test/council-fleet.test.mjs test/fleet-memory.test.mjs test/memory-view.test.mjs",
|
|
142
75
|
"test:approval-gate": "node --test --test-force-exit test/approval-gate.test.mjs",
|
|
143
76
|
"test:approval-index": "node --test --test-force-exit test/approval-index.test.mjs",
|
|
144
77
|
"test:authority": "node --test --test-force-exit test/authority.test.mjs",
|
|
@@ -188,8 +121,30 @@
|
|
|
188
121
|
"test:update-channels": "node --test --test-force-exit test/update-channels.test.mjs",
|
|
189
122
|
"test:update-prerelease": "node --test --test-force-exit test/update-prerelease.test.mjs",
|
|
190
123
|
"verify": "node scripts/check-entrypoints.mjs \u0026\u0026 node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/tools.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/p2p-node.test.mjs test/p2p-agent.test.mjs test/client-bundle.test.mjs test/loader-entry.test.mjs",
|
|
191
|
-
"verify:join": "node scripts/verify-join.mjs"
|
|
124
|
+
"verify:join": "node scripts/verify-join.mjs",
|
|
125
|
+
"test:intents": "node --test --test-force-exit test/intents.test.mjs",
|
|
126
|
+
"test:dataflow": "node --test --test-force-exit test/dataflow.test.mjs",
|
|
127
|
+
"test:council-fleet": "node --test --test-force-exit test/council-fleet.test.mjs",
|
|
128
|
+
"test:fleet-memory": "node --test --test-force-exit test/fleet-memory.test.mjs",
|
|
129
|
+
"test:memory-view": "node --test --test-force-exit test/memory-view.test.mjs"
|
|
192
130
|
},
|
|
131
|
+
"devDependencies": {
|
|
132
|
+
"@eslint/js": "^10.0.1",
|
|
133
|
+
"eslint": "^10.12.0",
|
|
134
|
+
"globals": "^17.13.0",
|
|
135
|
+
"prettier": "^3.9.9"
|
|
136
|
+
},
|
|
137
|
+
"publishConfig": {
|
|
138
|
+
"access": "public"
|
|
139
|
+
},
|
|
140
|
+
"repository": {
|
|
141
|
+
"type": "git",
|
|
142
|
+
"url": "git+https://github.com/TwinsEarth/dsh-windows2macos.git"
|
|
143
|
+
},
|
|
144
|
+
"homepage": "https://github.com/TwinsEarth/dsh-windows2macos",
|
|
145
|
+
"bugs": {
|
|
146
|
+
"url": "https://github.com/TwinsEarth/dsh-windows2macos/issues"
|
|
147
|
+
},
|
|
193
148
|
"keywords": [
|
|
194
149
|
"dsh-bundle",
|
|
195
150
|
"dsh-plugin",
|
|
@@ -203,16 +158,66 @@
|
|
|
203
158
|
"replicate",
|
|
204
159
|
"sharding"
|
|
205
160
|
],
|
|
206
|
-
"
|
|
207
|
-
|
|
208
|
-
|
|
161
|
+
"main": "lib/index.js",
|
|
162
|
+
"bin": {
|
|
163
|
+
"w2m-localside": "bin/w2m-localside.mjs",
|
|
164
|
+
"w2m-mcp": "bin/w2m-mcp.mjs",
|
|
165
|
+
"w2m-rabbit": "bin/w2m-rabbit.mjs",
|
|
166
|
+
"w2m-stun": "bin/w2m-stun.mjs"
|
|
167
|
+
},
|
|
168
|
+
"disclosure": {
|
|
169
|
+
"cloud": true,
|
|
170
|
+
"network": [
|
|
171
|
+
"http://202.182.123.154:8787",
|
|
172
|
+
"202.182.123.154:3478",
|
|
173
|
+
"stun.cloudflare.com:3478",
|
|
174
|
+
"stun.l.google.com:19302",
|
|
175
|
+
"stun.nextcloud.com:443",
|
|
176
|
+
"ilinkai.weixin.qq.com:443",
|
|
177
|
+
"weixin.qq.com:443",
|
|
178
|
+
"api.weixin.qq.com:443"
|
|
179
|
+
],
|
|
180
|
+
"offlineMode": false,
|
|
181
|
+
"apiKeys": [
|
|
182
|
+
{
|
|
183
|
+
"env": "W2M_OPERATOR_TOKEN",
|
|
184
|
+
"storage": "profile-config"
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"env": null,
|
|
188
|
+
"storage": "file-0600"
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"env": null,
|
|
192
|
+
"storage": "profile-config"
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"env": null,
|
|
196
|
+
"storage": "im-bot-token"
|
|
197
|
+
}
|
|
198
|
+
],
|
|
199
|
+
"permissions": {
|
|
200
|
+
"network": "the configured relay (default: the shared server above), STUN servers, and UDP to the peer machines. The optional chatops channels reach one instant-messaging platform each, and only when that channel is named in `channels` and the host has supplied the transport: the WeChat channel reaches ilinkai.weixin.qq.com and sends the bot token as a bearer credential, and both that host and the base url in a login response are checked against an allow-list before a token is sent anywhere",
|
|
201
|
+
"filesystem": "read/write inside the project directory and DSH_HOME; the agent writes its spool and state under --state; the chatops file fence reads inside `projectRoot` only, after resolving symlinks",
|
|
202
|
+
"env": [
|
|
203
|
+
"W2M_*"
|
|
204
|
+
]
|
|
205
|
+
},
|
|
206
|
+
"jurisdiction": [
|
|
207
|
+
"CN",
|
|
208
|
+
"JP"
|
|
209
|
+
],
|
|
210
|
+
"retention": "server"
|
|
209
211
|
},
|
|
210
|
-
"
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
212
|
+
"dependencies": {
|
|
213
|
+
|
|
214
|
+
},
|
|
215
|
+
"peerDependencies": {
|
|
216
|
+
"@deepseek-ai/dsh-tools": "*"
|
|
217
|
+
},
|
|
218
|
+
"peerDependenciesMeta": {
|
|
219
|
+
"@deepseek-ai/dsh-tools": {
|
|
220
|
+
"optional": true
|
|
221
|
+
}
|
|
222
|
+
}
|
|
218
223
|
}
|
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.2.
|
|
4
|
+
"version": "1.2.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": [
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A council whose members are machines, not callers in one process.
|
|
3
|
+
*
|
|
4
|
+
* ## What is shared with the single-machine council, and what is not
|
|
5
|
+
*
|
|
6
|
+
* The RULE is shared: a vote's weight is a pure function of the numbers on its ballot (authority and reputation), so any
|
|
7
|
+
* machine that holds the same ballots computes the same outcome; and a tie is a REFUSAL, not a coin toss, because a split
|
|
8
|
+
* council is exactly the case a person has to look at.
|
|
9
|
+
*
|
|
10
|
+
* The TRANSPORT is different. A single-machine council holds its ballots in memory; a fleet council's ballots arrive as
|
|
11
|
+
* chat messages on a thread (\`council-<id>\`), which is what the 1.2.1 relay already supports. So this module is the state
|
|
12
|
+
* and the arithmetic, and {@link fleetCouncilRequest} / {@link fleetCouncilBallot} are the two messages it exchanges --
|
|
13
|
+
* plain JSON in text, because the fleet chat contract is "plain text" and inventing a binary frame would need a relay
|
|
14
|
+
* change for no benefit.
|
|
15
|
+
*
|
|
16
|
+
* ## Why late ballots are refused rather than ignored
|
|
17
|
+
*
|
|
18
|
+
* Silently dropping a ballot makes a voter believe their vote counted. Refusing it by name can be told to them.
|
|
19
|
+
*/
|
|
20
|
+
export const FLEET_COUNCIL_REFUSAL = Object.freeze({
|
|
21
|
+
UNKNOWN: 'FLEET_COUNCIL_UNKNOWN',
|
|
22
|
+
ALREADY_OPEN: 'FLEET_COUNCIL_ALREADY_OPEN',
|
|
23
|
+
ALREADY_DECIDED: 'FLEET_COUNCIL_ALREADY_DECIDED',
|
|
24
|
+
NO_BALLOTS: 'FLEET_COUNCIL_NO_BALLOTS',
|
|
25
|
+
BAD_BALLOT: 'FLEET_COUNCIL_BAD_BALLOT',
|
|
26
|
+
DUPLICATE_VOTER: 'FLEET_COUNCIL_DUPLICATE_VOTER',
|
|
27
|
+
LATE_BALLOT: 'FLEET_COUNCIL_LATE_BALLOT',
|
|
28
|
+
TIED: 'FLEET_COUNCIL_TIED',
|
|
29
|
+
BAD_MESSAGE: 'FLEET_COUNCIL_BAD_MESSAGE',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/** The weight of one ballot. Deliberately the same formula as \`agent/council.mjs#weightOf\`, restated here because importing
|
|
33
|
+
* the single-machine module would drag its phase machine into a fleet decision that does not have phases. */
|
|
34
|
+
export function fleetWeightOf({ authority, reputation } = {}) {
|
|
35
|
+
const level = Number.isFinite(authority) ? authority : null;
|
|
36
|
+
if (level === null || level < 0 || level > 100) return null;
|
|
37
|
+
const rep = Number.isFinite(reputation) ? reputation : 0;
|
|
38
|
+
return level * (1 + rep / 100);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Count the stored ballots. Pure: same ballots, same outcome, on any machine.
|
|
43
|
+
*
|
|
44
|
+
* @param {Array<{choice: string, authority: number, reputation: number, machine_id?: string}>} ballots
|
|
45
|
+
* @returns {{ok: true, choice: string, weight: number, dissent: object[]} | {ok: false, code: string, detail?: object}}
|
|
46
|
+
*/
|
|
47
|
+
export function tallyFleetBallots(ballots) {
|
|
48
|
+
const list = Array.isArray(ballots) ? ballots : [];
|
|
49
|
+
if (list.length === 0) return { ok: false, code: FLEET_COUNCIL_REFUSAL.NO_BALLOTS };
|
|
50
|
+
const totals = new Map();
|
|
51
|
+
for (const ballot of list) {
|
|
52
|
+
const choice = typeof ballot?.choice === 'string' ? ballot.choice.trim() : '';
|
|
53
|
+
if (choice === '') return { ok: false, code: FLEET_COUNCIL_REFUSAL.BAD_BALLOT, detail: { ballot } };
|
|
54
|
+
const weight = fleetWeightOf(ballot);
|
|
55
|
+
if (weight === null) return { ok: false, code: FLEET_COUNCIL_REFUSAL.BAD_BALLOT, detail: { authority: ballot?.authority } };
|
|
56
|
+
totals.set(choice, (totals.get(choice) ?? 0) + weight);
|
|
57
|
+
}
|
|
58
|
+
const ranked = [...totals.entries()].sort((a, b) => b[1] - a[1]);
|
|
59
|
+
const [topChoice, topWeight] = ranked[0];
|
|
60
|
+
const tied = ranked.slice(1).filter(([, weight]) => weight === topWeight);
|
|
61
|
+
if (tied.length > 0) {
|
|
62
|
+
return {
|
|
63
|
+
ok: false,
|
|
64
|
+
code: FLEET_COUNCIL_REFUSAL.TIED,
|
|
65
|
+
detail: { tied: [topChoice, ...tied.map(([choice]) => choice)], weight: topWeight },
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
ok: true,
|
|
70
|
+
choice: topChoice,
|
|
71
|
+
weight: topWeight,
|
|
72
|
+
dissent: list.filter((ballot) => ballot.choice !== topChoice).map((ballot) => ({ ...ballot })),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {{now?: () => number}} [options]
|
|
78
|
+
*/
|
|
79
|
+
export function createFleetCouncil(options = {}) {
|
|
80
|
+
const now = typeof options.now === 'function' ? options.now : () => Date.now();
|
|
81
|
+
/** councilId -> { id, question, openedBy, atMs, decidedAtMs, ballots, decision } */
|
|
82
|
+
const councils = new Map();
|
|
83
|
+
|
|
84
|
+
function open({ councilId, question, openedByMachineId } = {}) {
|
|
85
|
+
const id = typeof councilId === 'string' && councilId.trim() !== '' ? councilId.trim() : null;
|
|
86
|
+
if (id === null) return { ok: false, code: FLEET_COUNCIL_REFUSAL.BAD_BALLOT, detail: { field: 'councilId' } };
|
|
87
|
+
const questionText = typeof question === 'string' ? question.trim() : '';
|
|
88
|
+
if (questionText === '') return { ok: false, code: FLEET_COUNCIL_REFUSAL.BAD_BALLOT, detail: { field: 'question' } };
|
|
89
|
+
const existing = councils.get(id);
|
|
90
|
+
if (existing !== undefined && existing.decision === null) return { ok: false, code: FLEET_COUNCIL_REFUSAL.ALREADY_OPEN };
|
|
91
|
+
const council = {
|
|
92
|
+
id,
|
|
93
|
+
question: questionText,
|
|
94
|
+
openedBy: typeof openedByMachineId === 'string' ? openedByMachineId : null,
|
|
95
|
+
atMs: now(),
|
|
96
|
+
decidedAtMs: null,
|
|
97
|
+
ballots: [],
|
|
98
|
+
decision: null,
|
|
99
|
+
};
|
|
100
|
+
councils.set(id, council);
|
|
101
|
+
return { ok: true, council: snapshot(council) };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function vote({ councilId, machineId, choice, reason = null, authority = null, reputation = null } = {}) {
|
|
105
|
+
const council = councils.get(typeof councilId === 'string' ? councilId.trim() : '');
|
|
106
|
+
if (council === undefined) return { ok: false, code: FLEET_COUNCIL_REFUSAL.UNKNOWN, detail: { councilId } };
|
|
107
|
+
if (council.decision !== null) return { ok: false, code: FLEET_COUNCIL_REFUSAL.LATE_BALLOT, detail: { decidedAtMs: council.decidedAtMs } };
|
|
108
|
+
const voter = typeof machineId === 'string' && machineId !== '' ? machineId : null;
|
|
109
|
+
if (voter === null) return { ok: false, code: FLEET_COUNCIL_REFUSAL.BAD_BALLOT, detail: { field: 'machineId' } };
|
|
110
|
+
if (council.ballots.some((ballot) => ballot.machine_id === voter)) {
|
|
111
|
+
return { ok: false, code: FLEET_COUNCIL_REFUSAL.DUPLICATE_VOTER, detail: { machineId: voter } };
|
|
112
|
+
}
|
|
113
|
+
const ballot = {
|
|
114
|
+
machine_id: voter,
|
|
115
|
+
choice: typeof choice === 'string' ? choice.trim() : '',
|
|
116
|
+
reason: typeof reason === 'string' ? reason : null,
|
|
117
|
+
authority: Number.isInteger(authority) && authority >= 0 && authority <= 100 ? authority : null,
|
|
118
|
+
reputation: Number.isFinite(reputation) ? reputation : null,
|
|
119
|
+
at_ms: now(),
|
|
120
|
+
};
|
|
121
|
+
if (ballot.choice === '' || ballot.authority === null) {
|
|
122
|
+
return { ok: false, code: FLEET_COUNCIL_REFUSAL.BAD_BALLOT, detail: { ballot } };
|
|
123
|
+
}
|
|
124
|
+
council.ballots.push(ballot);
|
|
125
|
+
return { ok: true, council: snapshot(council) };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function decide({ councilId } = {}) {
|
|
129
|
+
const council = councils.get(typeof councilId === 'string' ? councilId.trim() : '');
|
|
130
|
+
if (council === undefined) return { ok: false, code: FLEET_COUNCIL_REFUSAL.UNKNOWN, detail: { councilId } };
|
|
131
|
+
if (council.decision !== null) return { ok: true, council: snapshot(council), alreadyDecided: true };
|
|
132
|
+
const counted = tallyFleetBallots(council.ballots);
|
|
133
|
+
if (counted.ok !== true) return counted;
|
|
134
|
+
council.decision = {
|
|
135
|
+
choice: counted.choice,
|
|
136
|
+
weight: counted.weight,
|
|
137
|
+
dissent: counted.dissent,
|
|
138
|
+
ballots: council.ballots.length,
|
|
139
|
+
};
|
|
140
|
+
council.decidedAtMs = now();
|
|
141
|
+
return { ok: true, council: snapshot(council) };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function state({ councilId } = {}) {
|
|
145
|
+
const council = councils.get(typeof councilId === 'string' ? councilId.trim() : '');
|
|
146
|
+
if (council === undefined) return { ok: false, code: FLEET_COUNCIL_REFUSAL.UNKNOWN, detail: { councilId } };
|
|
147
|
+
return { ok: true, council: snapshot(council) };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function list() {
|
|
151
|
+
return { ok: true, councils: [...councils.values()].map(snapshot) };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function snapshot(council) {
|
|
155
|
+
return {
|
|
156
|
+
id: council.id,
|
|
157
|
+
question: council.question,
|
|
158
|
+
opened_by: council.openedBy,
|
|
159
|
+
open: council.decision === null,
|
|
160
|
+
at_ms: council.atMs,
|
|
161
|
+
decided_at_ms: council.decidedAtMs,
|
|
162
|
+
ballots: council.ballots.map((ballot) => ({ ...ballot })),
|
|
163
|
+
decision: council.decision === null ? null : { ...council.decision, dissent: council.decision.dissent.map((b) => ({ ...b })) },
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { open, vote, decide, state, list, _councils: councils };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The thread every message about one council travels on. */
|
|
171
|
+
export function fleetCouncilThread(councilId) {
|
|
172
|
+
return 'council-' + String(councilId ?? '').trim();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The message that opens a vote on the fleet chat. */
|
|
176
|
+
export function fleetCouncilRequest({ councilId, question, openedByMachineId = null } = {}) {
|
|
177
|
+
return JSON.stringify({
|
|
178
|
+
v: 1,
|
|
179
|
+
kind: 'council-request',
|
|
180
|
+
council_id: String(councilId ?? ''),
|
|
181
|
+
question: String(question ?? ''),
|
|
182
|
+
opened_by: openedByMachineId,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** The message that carries one ballot. */
|
|
187
|
+
export function fleetCouncilBallot({ councilId, machineId, choice, reason = null, authority = null, reputation = null } = {}) {
|
|
188
|
+
return JSON.stringify({
|
|
189
|
+
v: 1,
|
|
190
|
+
kind: 'council-ballot',
|
|
191
|
+
council_id: String(councilId ?? ''),
|
|
192
|
+
machine_id: String(machineId ?? ''),
|
|
193
|
+
choice: String(choice ?? ''),
|
|
194
|
+
reason,
|
|
195
|
+
authority,
|
|
196
|
+
reputation,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Read one fleet-chat message: is it a council request, a ballot, or neither?
|
|
202
|
+
*
|
|
203
|
+
* @param {string} text
|
|
204
|
+
* @returns {{kind: 'request'|'ballot', payload: object} | {kind: 'none'} | {kind: 'bad', reason: string}}
|
|
205
|
+
*/
|
|
206
|
+
export function parseFleetCouncilMessage(text) {
|
|
207
|
+
if (typeof text !== 'string' || text.trim() === '') return { kind: 'none' };
|
|
208
|
+
let parsed;
|
|
209
|
+
try { parsed = JSON.parse(text); } catch { return { kind: 'none' }; }
|
|
210
|
+
if (parsed === null || typeof parsed !== 'object' || parsed.v !== 1) return { kind: 'none' };
|
|
211
|
+
if (parsed.kind === 'council-request') {
|
|
212
|
+
if (typeof parsed.council_id !== 'string' || parsed.council_id.trim() === '') return { kind: 'bad', reason: 'council_id' };
|
|
213
|
+
return { kind: 'request', payload: parsed };
|
|
214
|
+
}
|
|
215
|
+
if (parsed.kind === 'council-ballot') {
|
|
216
|
+
if (typeof parsed.council_id !== 'string' || parsed.council_id.trim() === '') return { kind: 'bad', reason: 'council_id' };
|
|
217
|
+
if (typeof parsed.machine_id !== 'string' || parsed.machine_id.trim() === '') return { kind: 'bad', reason: 'machine_id' };
|
|
218
|
+
return { kind: 'ballot', payload: parsed };
|
|
219
|
+
}
|
|
220
|
+
return { kind: 'none' };
|
|
221
|
+
}
|