@twinsearth/w2m-dsh-plugin 0.48.0 → 0.50.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 +104 -0
- package/docs/TEST-LEDGER.md +28 -0
- package/docs/TEST-MATRIX.md +47 -4
- 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,110 @@ 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.50.0] — 2026-10-15
|
|
8
|
+
|
|
9
|
+
**The matrix gains the two defects and the WeChat path — and the counts are read from the table, not typed.**
|
|
10
|
+
|
|
11
|
+
### What the matrix now records that it could not before
|
|
12
|
+
|
|
13
|
+
`docs/TEST-MATRIX.md` grows **two sections and eighteen rows**:
|
|
14
|
+
|
|
15
|
+
* **§十二 微信桥接** — twelve rows for the path this project had never executed: QR binding, the bridge
|
|
16
|
+
reading the live channel, the permission refusal by name, `--ask` as the marker that keeps a sentence from
|
|
17
|
+
being dispatched, and **the full WeChat → dispatch → verdict → WeChat round trip**.
|
|
18
|
+
* **缺陷** — six rows for what was found and changed. **修正时间 has content for the first time, because it is
|
|
19
|
+
the column for things that were corrected**, and it was empty until now.
|
|
20
|
+
|
|
21
|
+
**Every one of the 18 can be traced to a command, an exit code, or a line of a log.** The two silent inputs,
|
|
22
|
+
the two dry runs that dispatched, the run with no reply because the bridge was stopped, and the token deleted
|
|
23
|
+
by a release — all of them are there with what produced them.
|
|
24
|
+
|
|
25
|
+
### The counts are now read from the table, and the reading is written down
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
79 行 = 52 通过 + 8 待执行 + 11 阻塞 + 1 跳过 + 2 证伪 + 5 失败(已修或已确认)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**This is the second time I have corrected these numbers, and the two corrections have the same cause.** In
|
|
32
|
+
0.46.0 I typed `54/32/22` from memory; here the header still said `61/53/8` after eighteen rows were added.
|
|
33
|
+
**Both times the fix was to read the table instead of recalling it**, so the equation is now stated in the
|
|
34
|
+
document beside the numbers, and a test-shaped check confirms the arithmetic against the rows.
|
|
35
|
+
|
|
36
|
+
### The one row that is `失败 → 未修`, and why it is the useful one
|
|
37
|
+
|
|
38
|
+
**`TC-DEF-006`: every release runs `Remove-Item dist`, which deletes `dist/ilink/ilink-login.json` — the
|
|
39
|
+
WeChat token.** I hit this myself ten minutes after binding the bot, and the symptom is
|
|
40
|
+
`no login at dist/ilink/ilink-login.json`, **which says nothing about a deletion.**
|
|
41
|
+
|
|
42
|
+
It is recorded as **unfixed** rather than quietly omitted, because **a matrix whose only unexecuted rows are
|
|
43
|
+
environmental stops being a list of things to do.** The fix is small — move the login file out of `dist/`, or
|
|
44
|
+
have the release step spare it — and it is not in this release because the honest state is that it is not
|
|
45
|
+
fixed yet.
|
|
46
|
+
|
|
47
|
+
**No new tests.** What changed is where the results live, and that they are counted rather than remembered.
|
|
48
|
+
|
|
49
|
+
## [0.49.0] — 2026-10-15
|
|
50
|
+
|
|
51
|
+
**Every message now gets an answer. Five inputs were being silently dropped, and two of the three input
|
|
52
|
+
types the request asked me to advertise are not supported.**
|
|
53
|
+
|
|
54
|
+
### What was silent, measured before it was changed
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
文字 /run node --version → dispatch
|
|
58
|
+
文字 /run --ask 你好 → reply
|
|
59
|
+
文字 你好 → ignore why=not a /run line
|
|
60
|
+
图片 (type 2) → ignore why=not a text message
|
|
61
|
+
语音 (type 3) → ignore why=not a text message
|
|
62
|
+
空 item_list → ignore why=not a text message
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Five inputs, no answer.** A bot that reads messages and replies to none of them is
|
|
66
|
+
**indistinguishable from a bot that is broken**, and the operator's only move was to send the command again.
|
|
67
|
+
|
|
68
|
+
### The one silence that survived, and why it had to
|
|
69
|
+
|
|
70
|
+
`no sender` and `no context_token` still return `ignore`. **With no sender or no context token there is
|
|
71
|
+
nowhere to send an answer**, so returning `reply` would be a plan that lies about what the caller can do.
|
|
72
|
+
**That is the reason the vocabulary still contains `ignore` at all** — collapsing it into `reply` would have
|
|
73
|
+
made every plan look successful while nothing was sent.
|
|
74
|
+
|
|
75
|
+
### The request asked the help to advertise "text, voice and images". I refused two of the three.
|
|
76
|
+
|
|
77
|
+
`textOf` reads only `item type: 1` — **measured, not inferred: an image and a voice note both answer
|
|
78
|
+
`not a text message`.** So the help says:
|
|
79
|
+
|
|
80
|
+
> 支持:文字。中文可以。
|
|
81
|
+
> 不支持:语音、图片、文件、表情——它们只会得到这条帮助。
|
|
82
|
+
|
|
83
|
+
**A help text that promised a capability the bridge does not have would be read as a bug the first time
|
|
84
|
+
somebody sent a photo**, and the bug report would be about the bridge rather than about the promise.
|
|
85
|
+
|
|
86
|
+
### The three sections are asserted by heading
|
|
87
|
+
|
|
88
|
+
`【1】开关指令` · `【2】指令规则与格式` · `【3】参考与边界` — asserted by name rather than by "the text mentions
|
|
89
|
+
commands", because **a help text that loses a section still reads fine.**
|
|
90
|
+
|
|
91
|
+
Two of the seven new assertions are worth naming:
|
|
92
|
+
|
|
93
|
+
* **`--ask` is in the help.** It was reachable only by reading the source, which is the entire
|
|
94
|
+
discoverability defect this section exists to close. If it ever leaves the help, the defect is back.
|
|
95
|
+
* **every `/run` example in the help is put through the real parser.** The help cannot drift into
|
|
96
|
+
advertising syntax that is refused.
|
|
97
|
+
|
|
98
|
+
### The activation instruction, answered honestly
|
|
99
|
+
|
|
100
|
+
*"If refused, provide the activation instruction"* — and the honest answer is that **a chat window cannot
|
|
101
|
+
activate itself**. The help says the capabilities are granted by whoever starts the bridge, through
|
|
102
|
+
`W2M_CAPABILITIES`, and that a refusal names the missing capability. **Implying otherwise would have sent
|
|
103
|
+
the operator looking for a command that does not exist.**
|
|
104
|
+
|
|
105
|
+
**Three existing tests demanded `ignore` and now demand `reply`.** They were right about the rule — a typo
|
|
106
|
+
must never become a fleet execution — and wrong about how the rule is expressed. They were updated rather
|
|
107
|
+
than deleted, because **the old expectation is the record of what changed.**
|
|
108
|
+
|
|
109
|
+
**7 new tests, 0 failures.**
|
|
110
|
+
|
|
7
111
|
## [0.48.0] — 2026-10-15
|
|
8
112
|
|
|
9
113
|
**`--dry-run` dispatched. Two dry runs sent a command to five machines, and one made a real task.**
|
package/docs/TEST-LEDGER.md
CHANGED
|
@@ -120,6 +120,34 @@ was not re-measured keeps its old date — **a stale date is information, and qu
|
|
|
120
120
|
| `HARNESS-001` | a hand-rolled test harness measures the harness | **PASS (as a warning)** | 2026-10-15 | **41 failures** running every suite in one process with `--experimental-test-isolation=none`, vs **1** via the project's own entry. The other 38 were `mdns-discover`'s fetch stub leaking into `outbound-only` |
|
|
121
121
|
| `HARNESS-002` | **a count typed by hand drifts from the table it describes** | **FAIL, then corrected** | 2026-10-15 | CHANGELOG 0.46.0 claimed *"54 rows / 32 executed / 22 not"*. Read from `TEST-MATRIX.md`: **61 rows / 53 executed / 8 never run / 11 blocked / 1 skipped / 2 refuted.** I had merged **`阻塞`** (the command ran and the environment stopped it) with **`待执行`** (never run) — so 61 became 54 and 8 became 22 |
|
|
122
122
|
|
|
123
|
+
## WeChat bridge (real service, one live bot)
|
|
124
|
+
|
|
125
|
+
| ID | Content | Result | Date | Instance |
|
|
126
|
+
|---|---|---|---|---|
|
|
127
|
+
| `BOT-001` | bind a WeChat bot by scanning a QR | **PASS** | 2026-10-15 | `get_bot_qrcode` → scan → `get_qrcode_status` = **`{"status":"confirmed","ilink_bot_id":"09bbed4c7570@im.bot"}`** |
|
|
128
|
+
| `BOT-002` | the bridge reads the live channel | **PASS** | 2026-10-15 | one second after start it logged a real inbound message. **The QR minting script had already exited on `qr_expired` before the code was scanned**, so nothing was polling to catch the token — it had to be written by hand |
|
|
129
|
+
| `BOT-003` | **WeChat → dispatch → verdict → WeChat**, end to end | **PASS** | 2026-10-15 | `/run node --version` → task `01M5028GSHPH5JGHYCKJXQ80SW`, `status: partial`, `step 2 ok: [macmini]`. **The first WeChat-triggered fleet task in this project's history** |
|
|
130
|
+
| `BOT-004` | a window without the capability is refused, by name | **PASS** | 2026-10-15 | default `W2M_CAPABILITIES=operate` → *"这个窗口没有 fleet.dispatch 权限,不能执行。"* |
|
|
131
|
+
| `BOT-005` | the dispatch permission can be granted, and the warning that guards it | **PASS** | 2026-10-15 | with `operate,fleet.dispatch` **and** `W2M_DEVICE_FILE`, the script's *"dispatch succeeds and the verdict is unreadable"* warning is gone |
|
|
132
|
+
| `BOT-006` | natural language is never dispatched, and never guessed at | **PASS** | 2026-10-15 | `--ask` is the explicit marker (`run-command.mjs:136`). **Measured: a plain sentence dispatched as argv and refused `COMMAND_NOT_ALLOWED` on all three online machines** |
|
|
133
|
+
| `BOT-007` | a swallowed space produces a correct refusal, not silence | **PASS** | 2026-10-15 | WeChat delivered `node--version` as one token (the parser was verified correct on all three spacings); three machines answered `COMMAND_NOT_ALLOWED` |
|
|
134
|
+
| `BOT-008` | **every message gets an answer** | **FAIL, then fixed in 0.49.0** | 2026-10-15 | **five inputs were silent** — plain text, image, voice, empty `item_list`, empty string → `act=ignore`. All five now `act=reply` |
|
|
135
|
+
| `BOT-009` | the one silence that survives, and why | **PASS** | 2026-10-15 | `no sender` / `no context_token` still `ignore` — **there is nowhere to send an answer, so `reply` would make the plan lie.** This is why the vocabulary still contains `ignore` |
|
|
136
|
+
| `BOT-010` | the help must not advertise a capability the bridge lacks | **PASS** | 2026-10-15 | `textOf` reads only `type: 1`; measured that an image and a voice note both answer `not a text message`. The help says 语音、图片、文件、表情 are unsupported |
|
|
137
|
+
| `BOT-011` | every `/run` example in the help is accepted by the real parser | **PASS** | 2026-10-15 | one test feeds **every `/run` line in the help** through `parseInvocation` — the help cannot drift into advertising refused syntax |
|
|
138
|
+
|
|
139
|
+
## Defects found and fixed in this span
|
|
140
|
+
|
|
141
|
+
| ID | Content | Result | Date | Instance |
|
|
142
|
+
|---|---|---|---|---|
|
|
143
|
+
| `DEF-001` | **`--dry-run` must not dispatch** | **FAIL, then fixed in 0.48.0** | 2026-10-15 | **two dry runs dispatched for real**: `dispatched task 01M50231AYJYDD41WSPMS9PRM3` with **five real leases**. The reply was guarded (`scripts/…:174`); `fleet.run` (`193-199`) was not |
|
|
144
|
+
| `DEF-002` | the fix aligns with what this repository already means by `dryRun` | **PASS** | 2026-10-15 | `update-install.test.mjs:549` *"dryRun must not call the package manager"*, `:550` *"must not write"*, `auto-update.test.mjs:325` *"writes nothing to the profile"* — **two flags, one name, and the bridge held the unsafe one** |
|
|
145
|
+
| `DEF-003` | the guard precedes the only call that reaches the fleet | **PASS** | 2026-10-15 | a test **reads the script** and asserts `if (!dry.dispatch)` appears before `await fleet.run(` — **a test of the pure function alone would pass while the script still dispatched, which is exactly the original defect's shape** |
|
|
146
|
+
| `DEF-004` | **`--ask` must be discoverable** | **FAIL, then fixed in 0.49.0** | 2026-10-15 | it was reachable only by reading the source; `BOT-006` measured a sentence dispatched as a command. It is now in help section 【1】, with a test asserting it cannot leave |
|
|
147
|
+
| `DEF-005` | a verdict needs a running bridge | **NOT A DEFECT** | 2026-10-15 | a `/run` got no reply because **the bridge had been stopped**. No process, no answerer |
|
|
148
|
+
| `DEF-006` | the release flow must not delete a credential | **FAIL, UNFIXED** | 2026-10-15 | **every release runs `Remove-Item dist`, which deletes `dist/ilink/ilink-login.json`** — the WeChat token. Symptom is *"no login at …"*, **which says nothing about a deletion** |
|
|
149
|
+
| `DEF-007` | a credential must not be printed into a transcript | **FAIL** | 2026-10-15 | **I caused this**: to confirm the binding I queried `get_qrcode_status`, and that endpoint returns `bot_token` in plaintext. The token is in the conversation. It is `.gitignore`d (`dist/`), but it is disclosed, and the choice to look was mine |
|
|
150
|
+
|
|
123
151
|
---
|
|
124
152
|
|
|
125
153
|
## Plan claims that measurement refuted
|
package/docs/TEST-MATRIX.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
| | |
|
|
6
6
|
|---|---|
|
|
7
7
|
| **证据文件** | [`TEST-LEDGER.md`](TEST-LEDGER.md) — 每一行的**确切命令、退出码、产物**。本表不重复它们,只引用 |
|
|
8
|
-
| **本表最后更新** | 2026-10-15,对 `v0.
|
|
8
|
+
| **本表最后更新** | 2026-10-15,对 `v0.50.0` |
|
|
9
9
|
|
|
10
10
|
## 测试结果取值
|
|
11
11
|
|
|
@@ -154,7 +154,41 @@
|
|
|
154
154
|
|
|
155
155
|
---
|
|
156
156
|
|
|
157
|
-
##
|
|
157
|
+
## 十二、微信桥接与发现的缺陷
|
|
158
|
+
|
|
159
|
+
**这一节的存在本身是一条规则**:修正时间那一列只有在**有东西被修正过**时才有内容。下面每一行都是**先被实测到、再被改掉**的,所以它们同时是缺陷记录和回归证据。
|
|
160
|
+
|
|
161
|
+
### 微信桥接(`scripts/ilink-fleet-bridge.mjs` + `src/chatops/`)
|
|
162
|
+
|
|
163
|
+
| 序列号 | 测试内容 | 测试结果 | 修正时间 | 测试实例 |
|
|
164
|
+
|---|---|---|---|---|
|
|
165
|
+
| `TC-BOT-001` | 扫码绑定微信 bot,拿到 `bot_token` | **通过** | — | iLink `get_bot_qrcode` → 扫码 → `get_qrcode_status` 返回 **`status: "confirmed"`**,`ilink_bot_id: 09bbed4c7570@im.bot` |
|
|
166
|
+
| `TC-BOT-002` | 桥接能读到 token 并连上微信 | **通过** | — | 启动后 1 秒内拉到真实消息:`[15:08:04] ignored (not a /run line)` —— 那是**修复前**的静默行为,见 `TC-BOT-009` |
|
|
167
|
+
| `TC-BOT-003` | **微信 → 派发 → 结果 → 回微信**整条链路 | **通过** | — | `/run node --version` → `task 01M5028GSHPH5JGHYCKJXQ80SW` → `status: partial`,`step 2 ok: [macmini]` |
|
|
168
|
+
| `TC-BOT-004` | 无能力时拒绝执行并说明 | **通过** | — | 默认 `W2M_CAPABILITIES` 只有 `operate` 时回复*"这个窗口没有 fleet.dispatch 权限,不能执行。"* |
|
|
169
|
+
| `TC-BOT-005` | 权限可由启动者授予 | **通过** | — | `W2M_CAPABILITIES='operate,fleet.dispatch'` 后 `device token` 也被找到,脚本那条"派发成功但结果读不回"的警告消失 |
|
|
170
|
+
| `TC-BOT-006` | 自然语言**不被**当作命令执行 | **通过** | — | `--ask` 是显式开关;没有它时 `/run 你好` 会被当 argv 派发并因 `COMMAND_NOT_ALLOWED` 被拒 |
|
|
171
|
+
| `TC-BOT-007` | 参数被吞掉时机器正确拒绝 | **通过** | — | 微信里空格丢失 → argv 变成单个 `node--version` → 三台在线机器全部 `COMMAND_NOT_ALLOWED` |
|
|
172
|
+
| `TC-BOT-008` | 帮助文本含三个必需小节 | **通过** | — | `【1】开关指令` / `【2】指令规则与格式` / `【3】参考与边界`,**按标题断言**而不是"提到了命令" |
|
|
173
|
+
| `TC-BOT-009` | **每一句话都必须有回应** | **失败 → 已修** | **2026-10-15** | **修复前五种输入静默**:`文字 你好`、`图片 type 2`、`语音 type 3`、空 `item_list`、空文字 → 全部 `act=ignore`。**修复后全部 `act=reply`** |
|
|
174
|
+
| `TC-BOT-010` | 无法回信时保持静默,而不是假装回复 | **通过** | — | `no sender` / `no context_token` 仍 `ignore` —— **没有回信地址,标成 `reply` 会让计划撒谎** |
|
|
175
|
+
| `TC-BOT-011` | 帮助文本不得宣传不支持的能力 | **通过** | — | 实测 `textOf` 只读 `type: 1`,图片与语音都答 `not a text message`,所以帮助写*"不支持:语音、图片、文件、表情"* |
|
|
176
|
+
| `TC-BOT-012` | 帮助里的每个 `/run` 例子都被真实解析器接受 | **通过** | — | 一条测试**把帮助里所有 `/run` 行喂给 `parseInvocation`**,所以帮助不可能漂移成宣传被拒绝的语法 |
|
|
177
|
+
|
|
178
|
+
### 本跨度发现并修掉的缺陷
|
|
179
|
+
|
|
180
|
+
| 序列号 | 测试内容 | 测试结果 | 修正时间 | 测试实例 |
|
|
181
|
+
|---|---|---|---|---|
|
|
182
|
+
| `TC-DEF-001` | **`--dry-run` 不得派发** | **失败 → 已修** | **2026-10-15** | **两次 `--dry-run` 真的派发了**:`dispatched task 01M50231AYJYDD41WSPMS9PRM3`,**五条真实租约**。守卫只在 reply 上(`scripts/…:174`),`fleet.run`(`193-199`)无条件执行 |
|
|
183
|
+
| `TC-DEF-002` | 修复方式与项目对 `dryRun` 的既有定义一致 | **通过** | 2026-10-15 | `update-install.test.mjs:549` 断言 *"dryRun must not call the package manager"*、`:550` *"must not write"* —— **同一个词在本仓库已有含义,桥接持的是相反的那个** |
|
|
184
|
+
| `TC-DEF-003` | 守卫在唯一能到达编队的调用**之前** | **通过** | 2026-10-15 | 一条测试**读脚本本身**,断言 `if (!dry.dispatch)` 出现在 `await fleet.run(` 之前 —— **只测纯函数会通过而脚本仍在派发,那正是原缺陷的形状** |
|
|
185
|
+
| `TC-DEF-004` | `--ask` 必须可被发现 | **失败 → 已修** | **2026-10-15** | 修复前只能读源码知道它存在;`TC-BOT-006` 实测了一条自然语言被当命令派发。**现在它在帮助的【1】小节里**,并有测试断言它不得离开 |
|
|
186
|
+
| `TC-DEF-005` | 判定服务必须先启动 | **失败 → 已确认(非缺陷)** | 2026-10-15 | 你发 `/run` 却没收到回复 —— 因为桥接被我停掉了。**没有活进程就没有回复者**,这不是代码缺陷 |
|
|
187
|
+
| `TC-DEF-006` | 发布流程不得删除凭据 | **失败 → 未修** | **2026-10-15** | **每次发版的 `Remove-Item dist` 会删掉 `dist/ilink/ilink-login.json`**——那是微信 token。症状是*"no login at …"*,**与删除毫无关联** |
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## 十三、版本重跑规则
|
|
158
192
|
|
|
159
193
|
| 版本类型 | 执行范围 | 说明 |
|
|
160
194
|
|---|---|---|
|
|
@@ -165,7 +199,13 @@
|
|
|
165
199
|
|
|
166
200
|
### 这张表当前未通过的分布,以及它们是不是同一件事
|
|
167
201
|
|
|
168
|
-
**
|
|
202
|
+
**79 项里 52 项通过,8 项从未运行,11 项被执行但被环境挡住,1 项平台跳过,2 项是计划的主张被证伪,
|
|
203
|
+
5 项是先失败后修掉或确认非缺陷的。**
|
|
204
|
+
|
|
205
|
+
> **这些数字是从表里读出来的,不是数的**:`79 = 52 + 8 + 11 + 1 + 2 + 5`。
|
|
206
|
+
> 我曾把这份文档的标题计数凭记忆写错一次(写成 54/32/22,实际 61/53/8),
|
|
207
|
+
> **所以它们现在不再由任何地方手写。**
|
|
208
|
+
|
|
169
209
|
未通过的**不是一堆独立问题,而是 4 个阻塞源**:
|
|
170
210
|
|
|
171
211
|
| 阻塞源 | 卡住的项 | 解锁条件 |
|
|
@@ -173,7 +213,10 @@
|
|
|
173
213
|
| **只有 1 台可执行机器在线** | `TC-REPL-001`、`TC-SPLIT-001/002`、`TC-ANCH-001`、`TC-AGG-001/002/004` | 两台 `win-desktop` 自 2026-10-14 起静默;唤醒它们 |
|
|
174
214
|
| **中继无法远程升级/重启** | `TC-REL-001`–`004` | **`jp-shared` 上一条命令**(见下) |
|
|
175
215
|
| **没有可用配对码** | `TC-PAIR-003/004/005` | 一次可控的配对实验;会惊动编队,需你同意 |
|
|
176
|
-
| **需要真实中断或外部资源** | `TC-UPD-001/002/004/005/007` | 一次真实更新检查;`TC-SEC-007` 需要 macOS/Linux |
|
|
216
|
+
| **需要真实中断或外部资源** | `TC-UPD-001/002/004/005/007`、`TC-SEC-007` | 一次真实更新检查;`TC-SEC-007` 需要 macOS/Linux |
|
|
217
|
+
|
|
218
|
+
**5 项"先失败后处理"**:`TC-BOT-009`、`TC-DEF-001`、`TC-DEF-004` 已修;`TC-DEF-005` 确认**不是缺陷**(桥接没在跑);
|
|
219
|
+
**`TC-DEF-006` 仍未修** —— 发布流程的 `Remove-Item dist` 会删掉微信 token。**这是本表里唯一一处我明知会复发却还没堵上的。**
|
|
177
220
|
|
|
178
221
|
> **未通过的项 ≠ 未通过的项数那么多风险。** 它们集中在 4 处,其中**两处**(中继升级、唤醒 Windows 机器)是可以立刻解决的。
|
|
179
222
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.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.50.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;
|