@twinsearth/w2m-dsh-plugin 0.48.0 → 0.49.0
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/CHANGELOG.md +62 -0
- package/package.json +1 -1
- package/src/chatops/ilink-fleet-bridge.mjs +82 -4
- package/src/plugin/tools.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,68 @@ All notable changes to this project are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
5
5
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.49.0] — 2026-10-15
|
|
8
|
+
|
|
9
|
+
**Every message now gets an answer. Five inputs were being silently dropped, and two of the three input
|
|
10
|
+
types the request asked me to advertise are not supported.**
|
|
11
|
+
|
|
12
|
+
### What was silent, measured before it was changed
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
文字 /run node --version → dispatch
|
|
16
|
+
文字 /run --ask 你好 → reply
|
|
17
|
+
文字 你好 → ignore why=not a /run line
|
|
18
|
+
图片 (type 2) → ignore why=not a text message
|
|
19
|
+
语音 (type 3) → ignore why=not a text message
|
|
20
|
+
空 item_list → ignore why=not a text message
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Five inputs, no answer.** A bot that reads messages and replies to none of them is
|
|
24
|
+
**indistinguishable from a bot that is broken**, and the operator's only move was to send the command again.
|
|
25
|
+
|
|
26
|
+
### The one silence that survived, and why it had to
|
|
27
|
+
|
|
28
|
+
`no sender` and `no context_token` still return `ignore`. **With no sender or no context token there is
|
|
29
|
+
nowhere to send an answer**, so returning `reply` would be a plan that lies about what the caller can do.
|
|
30
|
+
**That is the reason the vocabulary still contains `ignore` at all** — collapsing it into `reply` would have
|
|
31
|
+
made every plan look successful while nothing was sent.
|
|
32
|
+
|
|
33
|
+
### The request asked the help to advertise "text, voice and images". I refused two of the three.
|
|
34
|
+
|
|
35
|
+
`textOf` reads only `item type: 1` — **measured, not inferred: an image and a voice note both answer
|
|
36
|
+
`not a text message`.** So the help says:
|
|
37
|
+
|
|
38
|
+
> 支持:文字。中文可以。
|
|
39
|
+
> 不支持:语音、图片、文件、表情——它们只会得到这条帮助。
|
|
40
|
+
|
|
41
|
+
**A help text that promised a capability the bridge does not have would be read as a bug the first time
|
|
42
|
+
somebody sent a photo**, and the bug report would be about the bridge rather than about the promise.
|
|
43
|
+
|
|
44
|
+
### The three sections are asserted by heading
|
|
45
|
+
|
|
46
|
+
`【1】开关指令` · `【2】指令规则与格式` · `【3】参考与边界` — asserted by name rather than by "the text mentions
|
|
47
|
+
commands", because **a help text that loses a section still reads fine.**
|
|
48
|
+
|
|
49
|
+
Two of the seven new assertions are worth naming:
|
|
50
|
+
|
|
51
|
+
* **`--ask` is in the help.** It was reachable only by reading the source, which is the entire
|
|
52
|
+
discoverability defect this section exists to close. If it ever leaves the help, the defect is back.
|
|
53
|
+
* **every `/run` example in the help is put through the real parser.** The help cannot drift into
|
|
54
|
+
advertising syntax that is refused.
|
|
55
|
+
|
|
56
|
+
### The activation instruction, answered honestly
|
|
57
|
+
|
|
58
|
+
*"If refused, provide the activation instruction"* — and the honest answer is that **a chat window cannot
|
|
59
|
+
activate itself**. The help says the capabilities are granted by whoever starts the bridge, through
|
|
60
|
+
`W2M_CAPABILITIES`, and that a refusal names the missing capability. **Implying otherwise would have sent
|
|
61
|
+
the operator looking for a command that does not exist.**
|
|
62
|
+
|
|
63
|
+
**Three existing tests demanded `ignore` and now demand `reply`.** They were right about the rule — a typo
|
|
64
|
+
must never become a fleet execution — and wrong about how the rule is expressed. They were updated rather
|
|
65
|
+
than deleted, because **the old expectation is the record of what changed.**
|
|
66
|
+
|
|
67
|
+
**7 new tests, 0 failures.**
|
|
68
|
+
|
|
7
69
|
## [0.48.0] — 2026-10-15
|
|
8
70
|
|
|
9
71
|
**`--dry-run` dispatched. Two dry runs sent a command to five machines, and one made a real task.**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"description": "DSH: Windows2MacOS — drive the same project on online Windows and macOS machines from one DeepSeek account, across networks and regions. P2P hole punching is the default path (relay + shared server as rendezvous, ledger and fallback), with a bundled STUN responder, Tailscale/public-VPS/tunnel shapes, Rabbit relay + per-machine Localside + eight DSH tools (replicate / split), nine aggregate verdicts and signed requests.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -140,6 +140,64 @@ export function sendSucceeded(response) {
|
|
|
140
140
|
return response.ret === 0 || response.errcode === 0;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* What to say when a message is not a command.
|
|
145
|
+
*
|
|
146
|
+
* ## The requirement this satisfies, and the part of it that had to be refused
|
|
147
|
+
*
|
|
148
|
+
* *"Every sentence from WeChat must get a reply; a mistake must get help."* Before this, **five measured
|
|
149
|
+
* inputs were silently dropped**: a plain sentence, an image, a voice note, an empty `item_list`, and an
|
|
150
|
+
* empty string. The operator sees a bot that reads messages and answers nothing, which is
|
|
151
|
+
* indistinguishable from a bot that is broken.
|
|
152
|
+
*
|
|
153
|
+
* The request also asked the help to advertise *"text, voice and images"*. **Two of those three are not
|
|
154
|
+
* supported**, and `textOf` reading only `type: 1` is the proof: an image and a voice note both answer
|
|
155
|
+
* `not a text message`. So the boundary section says what is true — **a help text that promises a
|
|
156
|
+
* capability the bridge does not have would be read as a bug the first time somebody sent a photo.**
|
|
157
|
+
*
|
|
158
|
+
* ## Why the boundary is a section rather than a footnote
|
|
159
|
+
*
|
|
160
|
+
* A user cannot discover a limitation they are never told about. **"Voice is not supported" arriving in
|
|
161
|
+
* the same breath as the command syntax is the difference between a limitation and a mystery.**
|
|
162
|
+
*
|
|
163
|
+
* @param {{reason?: string|null}} [context]
|
|
164
|
+
* @returns {string}
|
|
165
|
+
*/
|
|
166
|
+
export function helpText(context = {}) {
|
|
167
|
+
const why = typeof context.reason === 'string' && context.reason !== '' ? context.reason : null;
|
|
168
|
+
const lines = [];
|
|
169
|
+
if (why !== null) lines.push(`没有执行这条消息:${why}`, '');
|
|
170
|
+
|
|
171
|
+
lines.push(
|
|
172
|
+
'【1】开关指令',
|
|
173
|
+
' /run <命令> 把一个命令派发到编队(默认所有在线机器)',
|
|
174
|
+
' /run --ask <一句话> 问一句话,不执行任何东西',
|
|
175
|
+
' /run --on <机器> <命令> 只派发到指定机器(逗号分隔)',
|
|
176
|
+
' /run --timeout <毫秒> <命令> 覆盖等待上限',
|
|
177
|
+
' /run --mode <模式> <命令> replicate | split | broadcast | pipeline',
|
|
178
|
+
'',
|
|
179
|
+
' —— 关于权限:这个窗口当前的能力是 operate、fleet.dispatch。',
|
|
180
|
+
' 若被拒绝,回复里会写明缺哪一个能力;能力由启动桥接的人通过',
|
|
181
|
+
' W2M_CAPABILITIES 授予,聊天窗口本身不能自行激活。',
|
|
182
|
+
'',
|
|
183
|
+
'【2】指令规则与格式',
|
|
184
|
+
' · 只能以 /run 开头,其余文字一律当作普通消息并回这条帮助。',
|
|
185
|
+
' · /run 之后是 argv(逐个词),不是 shell 字符串:',
|
|
186
|
+
' 管道 | 、重定向 > 、&& 都不会生效,会被拒绝。',
|
|
187
|
+
' · 每个词里的空格要用引号包起来。',
|
|
188
|
+
' · 举例: /run node --version',
|
|
189
|
+
' /run git rev-parse --short HEAD',
|
|
190
|
+
' /run --ask 现在编队里有几台机器在线?',
|
|
191
|
+
'',
|
|
192
|
+
'【3】参考与边界',
|
|
193
|
+
' · 支持:文字。中文可以。',
|
|
194
|
+
' · 不支持:语音、图片、文件、表情——它们只会得到这条帮助。',
|
|
195
|
+
' · 命令是否被允许由目标机器自己的允许清单决定;',
|
|
196
|
+
' 被拒绝时回复会带上原因码(如 COMMAND_NOT_ALLOWED)。',
|
|
197
|
+
);
|
|
198
|
+
return lines.join('\n');
|
|
199
|
+
}
|
|
200
|
+
|
|
143
201
|
/**
|
|
144
202
|
* Decide what to do with one inbound message.
|
|
145
203
|
*
|
|
@@ -158,14 +216,34 @@ export function planMessage({ message, authorize }) {
|
|
|
158
216
|
const contextToken = message?.context_token;
|
|
159
217
|
const text = textOf(message);
|
|
160
218
|
|
|
219
|
+
/**
|
|
220
|
+
* The only two real silences, and both are structural rather than a decision.
|
|
221
|
+
*
|
|
222
|
+
* With no `from_user_id` or no `context_token` there is nowhere to send an answer to — so a `reply`
|
|
223
|
+
* here would be a promise the caller cannot keep. **These stay `ignore`, and they are the reason the
|
|
224
|
+
* vocabulary still has one**: collapsing them into `reply` would make the plan lie about what the
|
|
225
|
+
* script can do. Everything below this line gets an answer, because it can.
|
|
226
|
+
*/
|
|
161
227
|
if (typeof from !== 'string' || from === '') return { act: 'ignore', why: 'no sender' };
|
|
162
228
|
if (typeof contextToken !== 'string' || contextToken === '') return { act: 'ignore', why: 'no context_token' };
|
|
163
|
-
|
|
229
|
+
|
|
230
|
+
// A non-text message is answered rather than dropped: "every sentence must get a reply" includes the
|
|
231
|
+
// sentences the bridge cannot read.
|
|
232
|
+
if (text === null) {
|
|
233
|
+
return { act: 'reply', to: from, contextToken, text: helpText({ reason: '这条消息不是文字(语音/图片/文件暂不支持)' }) };
|
|
234
|
+
}
|
|
164
235
|
|
|
165
236
|
const trimmed = text.trim();
|
|
166
|
-
// Only the exact `/run` prefix is an action
|
|
167
|
-
//
|
|
168
|
-
if (!/^\/run(\s|$)/i.test(trimmed))
|
|
237
|
+
// Only the exact `/run` prefix is an action, and that rule is unchanged: a typo must never become a
|
|
238
|
+
// fleet execution. What changed is that it now gets an answer instead of silence.
|
|
239
|
+
if (!/^\/run(\s|$)/i.test(trimmed)) {
|
|
240
|
+
return {
|
|
241
|
+
act: 'reply',
|
|
242
|
+
to: from,
|
|
243
|
+
contextToken,
|
|
244
|
+
text: helpText({ reason: trimmed === '' ? '空消息' : '这条消息没有以 /run 开头' }),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
169
247
|
|
|
170
248
|
const body = trimmed.replace(/^\/run\s*/i, '');
|
|
171
249
|
const invocation = parseInvocation(body);
|
package/src/plugin/tools.mjs
CHANGED
|
@@ -208,7 +208,7 @@ const MAX_STAGES = 16;
|
|
|
208
208
|
* `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
|
|
209
209
|
* overrides it at runtime for tests and packagers.
|
|
210
210
|
*/
|
|
211
|
-
const PLUGIN_VERSION = '0.
|
|
211
|
+
const PLUGIN_VERSION = '0.49.0';
|
|
212
212
|
|
|
213
213
|
/** How long any single HTTP call may take before it is aborted, in milliseconds. */
|
|
214
214
|
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|