@yuanchilin/dsh-mailbox 1.0.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/LICENSE +21 -0
- package/README.md +69 -0
- package/bin/mailbox.mjs +183 -0
- package/cordis.patch.yml +12 -0
- package/lib/core.js +186 -0
- package/lib/index.js +199 -0
- package/lib/types/index.d.ts +44 -0
- package/package.json +60 -0
- package/skill/README.md +117 -0
- package/skill/SKILL.md +83 -0
- package/skill/examples/handlers.patch.ps1 +154 -0
- package/skill/legacy-mcp.config.json +14 -0
- package/skill/mailbox.config.json +11 -0
- package/skill/mailbox.mjs +344 -0
- package/skill/mailbox.ps1 +172 -0
- package/skill/mailbox.psm1 +270 -0
- package/skill/self-test.ps1 +105 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// 最小类型声明 (core 与插件导出的形状)
|
|
2
|
+
export interface MailboxConfig {
|
|
3
|
+
identity: string;
|
|
4
|
+
layout: "root" | "dirs";
|
|
5
|
+
root: string;
|
|
6
|
+
dirs: Record<string, string>;
|
|
7
|
+
participants: string[];
|
|
8
|
+
intervalSec: number;
|
|
9
|
+
timeoutSec: number;
|
|
10
|
+
seenFile: string;
|
|
11
|
+
patchRoot: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface MailboxMessage {
|
|
15
|
+
id: string;
|
|
16
|
+
from: string;
|
|
17
|
+
to: string;
|
|
18
|
+
type: "request" | "response" | "notify" | "reply";
|
|
19
|
+
topic: string;
|
|
20
|
+
payload: unknown;
|
|
21
|
+
ts: number;
|
|
22
|
+
reply_to: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface MailboxStatus {
|
|
26
|
+
identity: string;
|
|
27
|
+
layout: string;
|
|
28
|
+
outDir: string;
|
|
29
|
+
outCount: number;
|
|
30
|
+
seen: number;
|
|
31
|
+
inboxes: Array<{ dir: string; msgCount: number }>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveConfig(partial?: Partial<MailboxConfig>, env?: NodeJS.ProcessEnv): MailboxConfig;
|
|
35
|
+
export function resolveDirs(cfg: MailboxConfig): { out: string; in: string[] };
|
|
36
|
+
export function seenFileOf(cfg: MailboxConfig): string;
|
|
37
|
+
export function loadSeen(cfg: MailboxConfig): string[];
|
|
38
|
+
export function saveSeen(cfg: MailboxConfig, seen: string[]): void;
|
|
39
|
+
export function sendMessage(cfg: MailboxConfig, msg: Partial<Pick<MailboxMessage, "to" | "type" | "topic" | "payload" | "reply_to">>): string;
|
|
40
|
+
export function recvNew(cfg: MailboxConfig, markSeen?: boolean): MailboxMessage[];
|
|
41
|
+
export function removeMessage(cfg: MailboxConfig, id: string, inbox?: boolean): boolean;
|
|
42
|
+
export function cleanTTL(cfg: MailboxConfig, opts?: { ttlHours?: number; dryRun?: boolean }): number;
|
|
43
|
+
export function statusOf(cfg: MailboxConfig): MailboxStatus;
|
|
44
|
+
export function assertUsable(cfg: MailboxConfig): void;
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yuanchilin/dsh-mailbox",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "DeepSeek Harness cross-session file mailbox: send/recv/status tools over a shared filesystem mailbox (N participants, directed & broadcast routing, seen dedup, TTL cleanup), plus a zero-dependency node CLI and a packaged DSH skill.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/types/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"default": "./lib/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./core": {
|
|
13
|
+
"default": "./lib/core.js"
|
|
14
|
+
},
|
|
15
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
16
|
+
"./skill/SKILL.md": "./skill/SKILL.md",
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"mailbox": "bin/mailbox.mjs"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"lib/index.js",
|
|
24
|
+
"lib/core.js",
|
|
25
|
+
"lib/types/index.d.ts",
|
|
26
|
+
"bin/mailbox.mjs",
|
|
27
|
+
"cordis.patch.yml",
|
|
28
|
+
"skill/",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"test": "node --test test/"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"deepseek-harness",
|
|
37
|
+
"dsh",
|
|
38
|
+
"mailbox",
|
|
39
|
+
"cross-session",
|
|
40
|
+
"messaging",
|
|
41
|
+
"plugin"
|
|
42
|
+
],
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"dsh": {
|
|
45
|
+
"bundle": {
|
|
46
|
+
"patch": "./cordis.patch.yml"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
54
|
+
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
|
|
55
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=18.0.0"
|
|
59
|
+
}
|
|
60
|
+
}
|
package/skill/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# mailbox — 通用跨会话文件信箱 v1
|
|
2
|
+
|
|
3
|
+
文件信箱,用于 **N 个会话/agent 之间的异步通信**。泛化自 mcp/RP 联调工具,零依赖、配置驱动、双实现(pwsh + Node)可互换混用。
|
|
4
|
+
|
|
5
|
+
## 核心模型
|
|
6
|
+
|
|
7
|
+
- 每个参与者有唯一身份 `id`,各写各的信箱目录,互读对方的
|
|
8
|
+
- **layout=root(标准)**:共享根目录 `<root>/<id>/` 每人一个子目录
|
|
9
|
+
- **layout=dirs(兼容旧双目录)**:`dirs: { id -> 目录 }` 显式映射,旧 mcp/RP 信箱免迁移接入
|
|
10
|
+
- 消息格式:`{ id, from, to, type, topic, payload, ts, reply_to }`,文件 `msg_<id>.json`
|
|
11
|
+
- 路由:`to=<id>` 定向 / `to=all` 广播(只写一份,各人自取)
|
|
12
|
+
- seen 去重:每参与者独立 seen 文件(默认 `<outDir>/.seen.json`),重启不重复处理
|
|
13
|
+
|
|
14
|
+
## 快速开始
|
|
15
|
+
|
|
16
|
+
```powershell
|
|
17
|
+
# 1. 初始化(每个参与者同一 root 下各一个目录)
|
|
18
|
+
.\mailbox.ps1 init -Id agent-a -Root D:/Downloads/Agent/.mailbox
|
|
19
|
+
.\mailbox.ps1 init -Id agent-b -Root D:/Downloads/Agent/.mailbox
|
|
20
|
+
|
|
21
|
+
# 2. A 发消息
|
|
22
|
+
.\mailbox.ps1 send -To agent-b -Topic hello -Payload '{"x":1}'
|
|
23
|
+
|
|
24
|
+
# 3. B 收消息(一次性)
|
|
25
|
+
.\mailbox.ps1 recv -Format table
|
|
26
|
+
|
|
27
|
+
# 4. B 事件监听(新消息 → 打印 → exit 0,DSH 后台 job 完成即唤醒 agent)
|
|
28
|
+
.\mailbox.ps1 wait -Timeout 600
|
|
29
|
+
|
|
30
|
+
# 5. B 常驻轮询(request 自动回 response,可挂 handlers)
|
|
31
|
+
.\mailbox.ps1 poll -Interval 2 -Handlers .\examples\handlers.patch.ps1
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Node 版命令完全一致:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
node mailbox.mjs send --to agent-b --topic hello --payload '{"x":1}'
|
|
38
|
+
node mailbox.mjs recv --format table
|
|
39
|
+
node mailbox.mjs wait --timeout 600
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 命令
|
|
43
|
+
|
|
44
|
+
| 命令 | pwsh | node | 说明 |
|
|
45
|
+
|---|---|---|---|
|
|
46
|
+
| init | `init -Id x -Root y` | `init --id x --root y` | 生成配置文件 |
|
|
47
|
+
| send | `send -To <id\|all> -Type <t> -Topic <t> -Payload <json> -ReplyTo <id>` | 同左 `--to/--type/--topic/--payload/--reply-to` | 发消息 |
|
|
48
|
+
| recv | `recv [-Format table\|json]` | `recv [--format table\|json]` | 读新消息(更新 seen) |
|
|
49
|
+
| wait | `wait [-Timeout sec] [-Interval sec]` | `wait [--timeout sec]` | 新消息即 exit 0(唤醒) |
|
|
50
|
+
| poll | `poll [-Interval sec] [-Handlers <ps1>]` | `poll [--interval sec] [--handlers <mjs>]` | 常驻轮询 |
|
|
51
|
+
| clean | `clean [-TtlHours n] [-DryRun]` | `clean [--ttl-hours n] [--dry-run]` | TTL 清理自己的已发送消息 |
|
|
52
|
+
| status | `status` | `status` | 身份/目录/消息数 |
|
|
53
|
+
|
|
54
|
+
## 配置
|
|
55
|
+
|
|
56
|
+
配置文件默认 `mailbox.config.json`(与工具同目录),可用 `-Config` / `--config` 或环境变量 `MAILBOX_CONFIG` 指定。
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"identity": "agent-a",
|
|
61
|
+
"layout": "root",
|
|
62
|
+
"root": "D:/Downloads/Agent/.mailbox",
|
|
63
|
+
"dirs": {},
|
|
64
|
+
"participants": [],
|
|
65
|
+
"intervalSec": 2,
|
|
66
|
+
"timeoutSec": 0,
|
|
67
|
+
"seenFile": "",
|
|
68
|
+
"patchRoot": ""
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
优先级:**CLI 参数 > 环境变量(`MAILBOX_CONFIG`/`MAILBOX_ID`/`MAILBOX_ROOT`/`MAILBOX_INTERVAL`/`MAILBOX_TIMEOUT`)> 配置文件 > 默认**。
|
|
73
|
+
|
|
74
|
+
- `participants` 留空时自动扫描 `<root>/` 下的子目录作为参与者
|
|
75
|
+
- `seenFile` 留空默认 `<outDir>/.seen.json`
|
|
76
|
+
- `patchRoot` 供示例补丁 handler 使用
|
|
77
|
+
|
|
78
|
+
## 消息类型与默认处理
|
|
79
|
+
|
|
80
|
+
| type | 默认处理(poll) |
|
|
81
|
+
|---|---|
|
|
82
|
+
| request | 自动回 `response`(echo payload) |
|
|
83
|
+
| response / notify / reply | 打印 |
|
|
84
|
+
|
|
85
|
+
**可插拔 handler**:
|
|
86
|
+
- pwsh:`-Handlers <ps1>`,dot-source 后定义 `function Handle-Message($Msg, $Ctx)`,返回 `$false` 回退默认逻辑
|
|
87
|
+
- node:`--handlers <mjs>`,导出 `export async function handle(msg, ctx)`,返回 `false` 回退默认逻辑
|
|
88
|
+
|
|
89
|
+
示例:`examples/handlers.patch.ps1` 补丁自动应用(备份→替换→`node --check`→失败回滚→写 `patches/history.jsonl`→回 response),协议:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{ "type": "request", "topic": "patch_<任意>", "payload": {
|
|
93
|
+
"patch_id": "p1", "note": "说明",
|
|
94
|
+
"edits": [ { "file": "build/x.js", "old": "...", "new": "..." },
|
|
95
|
+
{ "file": "build/y.js", "whole": "完整内容" },
|
|
96
|
+
{ "file": "build/z.js", "old": "...", "new": "...", "replace_all": true } ] } }
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 旧布局兼容(mcp/RP)
|
|
100
|
+
|
|
101
|
+
`legacy-mcp.config.json` 用 `layout:"dirs"` 映射原 mcp/RP 双目录,原轮询/监听脚本改为薄封装:
|
|
102
|
+
|
|
103
|
+
```powershell
|
|
104
|
+
.\mailbox_poll_mcp.ps1 # → mailbox.ps1 poll -Config legacy-mcp.config.json -Handlers examples/handlers.patch.ps1
|
|
105
|
+
.\mailbox_wait_mcp.ps1 # → mailbox.ps1 wait -Config legacy-mcp.config.json
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## 自测
|
|
109
|
+
|
|
110
|
+
```powershell
|
|
111
|
+
.\self-test.ps1 # 覆盖: 往返/广播/wait 唤醒/TTL/双实现互通/旧布局/去重
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## 二期路线(未实现)
|
|
115
|
+
|
|
116
|
+
- HTTP relay(跨机器,CLI 加 `transport` 开关)+ X25519 加密 / Ed25519 签名
|
|
117
|
+
- 独立 npm/pwsh 包发布
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mailbox
|
|
3
|
+
description: 跨会话异步通信的文件信箱。通过共享文件系统在任意会话/agent 之间收发消息,支持 N 参与者、定向/广播、seen 去重、TTL 清理、事件唤醒。当需要与其他会话、后台 agent 或未来会话交换消息、任务结果或指示(而对方不一定在线)时使用。提供 pwsh 与 node 双实现的 CLI(send/recv/wait/poll/clean/status/init)。
|
|
4
|
+
whenToUse: 需要跨会话/跨进程异步传递消息时,例如向另一个会话发起请求、等待对方响应、投递任务结果、广播通知;或需要把当前会话状态交接给后续会话。
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# mailbox — 跨会话文件信箱
|
|
8
|
+
|
|
9
|
+
基于文件系统的异步消息机制:**每个参与者各写各的信箱目录,互读对方的**。消息是文件,所以只要双方共享同一文件系统(同机或网络盘),就能在任意会话/进程/时间点通信,对方不在线也不丢消息。
|
|
10
|
+
|
|
11
|
+
## 脚本位置
|
|
12
|
+
|
|
13
|
+
- **本技能目录**(随技能打包,任何项目可用):`$env:USERPROFILE\.dsh\skills\mailbox\`
|
|
14
|
+
- **项目内开发副本**(如存在,优先用项目内的):`<workspace>/tools/mailbox/`
|
|
15
|
+
- 两个实现完全同协议,可互换混用:
|
|
16
|
+
- pwsh:`mailbox.ps1`(模块 `mailbox.psm1`)
|
|
17
|
+
- node:`mailbox.mjs`(零依赖)
|
|
18
|
+
|
|
19
|
+
## 快速开始
|
|
20
|
+
|
|
21
|
+
```powershell
|
|
22
|
+
$mb = "$env:USERPROFILE\.dsh\skills\mailbox" # 或项目内 tools/mailbox
|
|
23
|
+
|
|
24
|
+
# 1. 初始化身份(同一 root 下每个参与者各初始化一次;root 需双方共享)
|
|
25
|
+
& "$mb\mailbox.ps1" init -Id agent-a -Root D:/Downloads/Agent/.mailbox
|
|
26
|
+
|
|
27
|
+
# 2. 发消息(to=all 广播 / to=<id> 定向)
|
|
28
|
+
& "$mb\mailbox.ps1" send -To agent-b -Topic hello -Payload '{"x":1}'
|
|
29
|
+
|
|
30
|
+
# 3. 收消息(一次性,自动更新 seen)
|
|
31
|
+
& "$mb\mailbox.ps1" recv -Format table
|
|
32
|
+
|
|
33
|
+
# 4. 事件唤醒:新消息到达即打印并 exit 0(后台 job 完成 → agent 被唤醒)
|
|
34
|
+
& "$mb\mailbox.ps1" wait -Timeout 600
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
node 版命令一致:`node "$mb\mailbox.mjs" send --to agent-b --topic hello --payload '{"x":1}'`
|
|
38
|
+
|
|
39
|
+
## 命令
|
|
40
|
+
|
|
41
|
+
| 命令 | pwsh | node | 说明 |
|
|
42
|
+
|---|---|---|---|
|
|
43
|
+
| init | `init -Id <id> -Root <dir>` | `init --id <id> --root <dir>` | 生成配置 |
|
|
44
|
+
| send | `send -To <id\|all> [-Type request\|response\|notify\|reply] [-Topic <t>] [-Payload <json>] [-ReplyTo <id>]` | `--to/--type/--topic/--payload/--reply-to` | 发消息 |
|
|
45
|
+
| recv | `recv [-Format table\|json]` | `recv [--format ...]` | 读新消息(更新 seen) |
|
|
46
|
+
| wait | `wait [-Timeout <sec>]` | `wait [--timeout <sec>]` | 新消息即 exit 0(唤醒) |
|
|
47
|
+
| poll | `poll [-Interval <sec>] [-Handlers <ps1>]` | `poll [--interval <sec>] [--handlers <mjs>]` | 常驻轮询(request 自动回 response) |
|
|
48
|
+
| clean | `clean [-TtlHours <n>] [-DryRun]` | `clean [--ttl-hours <n>] [--dry-run]` | TTL 清理自己发过的旧消息 |
|
|
49
|
+
| status | `status` | `status` | 身份/目录/消息数 |
|
|
50
|
+
|
|
51
|
+
## 配置
|
|
52
|
+
|
|
53
|
+
默认读取工具同目录 `mailbox.config.json`,可用 `-Config` / `--config` 或环境变量 `MAILBOX_CONFIG` 指定。优先级:**CLI 参数 > 环境变量(`MAILBOX_ID`/`MAILBOX_ROOT`/`MAILBOX_CONFIG`/`MAILBOX_INTERVAL`/`MAILBOX_TIMEOUT`)> 配置文件 > 默认**。
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{ "identity": "agent-a", "layout": "root", "root": "D:/Downloads/Agent/.mailbox",
|
|
57
|
+
"participants": [], "intervalSec": 2, "timeoutSec": 0, "seenFile": "", "patchRoot": "" }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- `layout=root`(标准):`<root>/<id>/` 每人一子目录;`participants` 留空自动扫描
|
|
61
|
+
- `layout=dirs`(旧双目录兼容):`dirs: { "<id>": "<目录>" }` 显式映射
|
|
62
|
+
- `seenFile` 留空默认 `<outDir>/.seen.json`(每参与者独立去重)
|
|
63
|
+
|
|
64
|
+
## 协议
|
|
65
|
+
|
|
66
|
+
- 消息文件:`msg_<id>.json`,内容 `{ id, from, to, type, topic, payload, ts, reply_to }`
|
|
67
|
+
- 路由:`to=<id>` 定向 / `to=all` 广播(写一份,各人自取)
|
|
68
|
+
- 消息类型:`request` / `response` / `notify` / `reply`
|
|
69
|
+
- **请求-响应模式**:发 `request` 带 `reply_to`,对方处理完回 `response` 指向原 id;`wait`/`recv` 可按 `reply_to` 关联
|
|
70
|
+
- 已处理消息建议删除(`Remove-MailboxMsg` / 由 poll 自动清理),seen 文件兜底去重
|
|
71
|
+
|
|
72
|
+
## 典型用法
|
|
73
|
+
|
|
74
|
+
1. **跨会话交接**:完成任务后 `send -To <对方> -Type notify -Topic handoff_ready -Payload @{...}`,把上下文文件路径放进 payload
|
|
75
|
+
2. **请求-响应**:`send -To rp -Type request -Topic patch_bug2 -Payload @{edits=...}` → 对方 poll 自动处理并回 `patch_result`
|
|
76
|
+
3. **等待对方**:`wait -Timeout 600` 挂后台 job,新消息到达 job 完成即被唤醒(DSH 机制)
|
|
77
|
+
4. **补丁自动应用**(示例):`poll -Handlers examples/handlers.patch.ps1`(需配置 `patchRoot`),收到 `topic=patch_*` 消息自动备份→替换→node --check→回滚→回 response
|
|
78
|
+
|
|
79
|
+
## 注意事项
|
|
80
|
+
|
|
81
|
+
- 双方必须共享同一文件系统(同机目录或网络盘);广域网需二期 HTTP relay(未实现)
|
|
82
|
+
- `wait` 与 `poll` 共用 seen 文件,同一身份不要同时跑两个
|
|
83
|
+
- 消息明文存储;需要保密时不要写入敏感 payload(二期支持加密)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# handlers.patch.ps1 — 示例 handler: 补丁自动应用
|
|
3
|
+
# (泛化自旧 mailbox_poll_mcp.ps1 的 Invoke-MailboxPatch)
|
|
4
|
+
#
|
|
5
|
+
# 用法:
|
|
6
|
+
# .\mailbox.ps1 poll -Handlers .\examples\handlers.patch.ps1
|
|
7
|
+
# 要求:
|
|
8
|
+
# 配置文件里 patchRoot 指向补丁目标工作区 (如 D:/work/myproject)
|
|
9
|
+
# 协议 (对端发来的消息):
|
|
10
|
+
# { type:"request", topic:"patch_<任意>", payload:{
|
|
11
|
+
# patch_id:"<标识>", note:"<说明>",
|
|
12
|
+
# edits:[
|
|
13
|
+
# { file:"build/x.js", old:"...", new:"..." }, # 片段替换
|
|
14
|
+
# { file:"build/y.js", whole:"完整新内容" }, # 整文件替换
|
|
15
|
+
# { file:"build/z.js", old:"...", new:"...", replace_all:true } # 全部替换
|
|
16
|
+
# ] } }
|
|
17
|
+
# 行为: 备份 → 替换 → node --check 语法检查 → 失败回滚 →
|
|
18
|
+
# 写 <patchRoot>/patches/history.jsonl → 回 response (topic: patch_result)
|
|
19
|
+
#
|
|
20
|
+
# 返回值: $true=已处理; $false=非补丁消息, 交给默认逻辑 (request→echo)
|
|
21
|
+
# ============================================================================
|
|
22
|
+
|
|
23
|
+
function Handle-Message {
|
|
24
|
+
param($Msg, $Ctx)
|
|
25
|
+
|
|
26
|
+
if ("$($Msg.topic)" -notlike "patch_*") { return $false }
|
|
27
|
+
|
|
28
|
+
$cfg = $Ctx.Cfg
|
|
29
|
+
$workRoot = [string]$cfg.patchRoot
|
|
30
|
+
if (-not $workRoot -or -not (Test-Path -LiteralPath $workRoot)) {
|
|
31
|
+
Write-Host " [补丁] patchRoot 未配置或不存在, 跳过 (config.patchRoot=$workRoot)"
|
|
32
|
+
return $true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
$payload = $Msg.payload
|
|
36
|
+
$patchId = if ($payload.patch_id) { [string]$payload.patch_id } else { [string]$Msg.id }
|
|
37
|
+
$edits = @($payload.edits)
|
|
38
|
+
$results = @()
|
|
39
|
+
$changed = @{} # full path -> backup path
|
|
40
|
+
$ok = $true
|
|
41
|
+
|
|
42
|
+
New-Item -ItemType Directory -Force -Path (Join-Path $workRoot "patches") | Out-Null
|
|
43
|
+
$bakDir = Join-Path $workRoot "patches\backup"
|
|
44
|
+
New-Item -ItemType Directory -Force -Path $bakDir | Out-Null
|
|
45
|
+
|
|
46
|
+
Write-Host "`n[补丁应用] patch_id=$patchId edits=$($edits.Count) note=$($payload.note) root=$workRoot"
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
foreach ($e in $edits) {
|
|
50
|
+
$rel = [string]$e.file
|
|
51
|
+
$full = Join-Path $workRoot $rel
|
|
52
|
+
|
|
53
|
+
# 路径安全: 必须落在 patchRoot 内
|
|
54
|
+
if (-not $full.StartsWith($workRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
55
|
+
$results += "SKIP $rel (路径越界, 拒绝)"
|
|
56
|
+
$ok = $false
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
if (-not (Test-Path -LiteralPath $full)) {
|
|
60
|
+
$results += "FAIL $rel (文件不存在)"
|
|
61
|
+
$ok = $false
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# 首次接触该文件时备份
|
|
66
|
+
if (-not $changed.ContainsKey($full)) {
|
|
67
|
+
$bak = Join-Path $bakDir ((Split-Path $rel -Leaf) + ".$patchId.bak")
|
|
68
|
+
Copy-Item -LiteralPath $full -Destination $bak -Force
|
|
69
|
+
$changed[$full] = $bak
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
$content = Get-Content -LiteralPath $full -Raw
|
|
73
|
+
|
|
74
|
+
if ($null -ne $e.whole -and [string]$e.whole -ne "") {
|
|
75
|
+
Set-Content -LiteralPath $full -Value ([string]$e.whole) -Encoding UTF8 -NoNewline
|
|
76
|
+
$results += "OK $rel (整文件替换)"
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
$old = [string]$e.old
|
|
81
|
+
$new = [string]$e.new
|
|
82
|
+
if ($old -eq "") {
|
|
83
|
+
$results += "FAIL $rel (old 为空)"
|
|
84
|
+
$ok = $false
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
$count = ([regex]::Matches($content, [regex]::Escape($old))).Count
|
|
89
|
+
if ($count -eq 0) {
|
|
90
|
+
$results += "FAIL $rel (old 片段未找到)"
|
|
91
|
+
$ok = $false
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
if ($count -gt 1 -and -not $e.replace_all) {
|
|
95
|
+
$results += "FAIL $rel (old 出现 $count 次, 需 replace_all=true)"
|
|
96
|
+
$ok = $false
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if ($e.replace_all) {
|
|
101
|
+
$newContent = $content.Replace($old, $new)
|
|
102
|
+
} else {
|
|
103
|
+
$idx = $content.IndexOf($old, [System.StringComparison]::Ordinal)
|
|
104
|
+
$newContent = $content.Substring(0, $idx) + $new + $content.Substring($idx + $old.Length)
|
|
105
|
+
}
|
|
106
|
+
Set-Content -LiteralPath $full -Value $newContent -Encoding UTF8 -NoNewline
|
|
107
|
+
$results += "OK $rel (替换 x$count)"
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# --- JS 语法检查 (node --check), 失败回滚 ---
|
|
111
|
+
if ($changed.Count -gt 0) {
|
|
112
|
+
$syntaxFail = $false
|
|
113
|
+
foreach ($full in @($changed.Keys)) {
|
|
114
|
+
if ($full -like "*.js") {
|
|
115
|
+
$out = & node --check $full 2>&1
|
|
116
|
+
if ($LASTEXITCODE -ne 0) {
|
|
117
|
+
$syntaxFail = $true
|
|
118
|
+
$results += "SYNTAX-FAIL $full : $out"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if ($syntaxFail) {
|
|
123
|
+
foreach ($full in @($changed.Keys)) {
|
|
124
|
+
Copy-Item -LiteralPath $changed[$full] -Destination $full -Force
|
|
125
|
+
$results += "ROLLED-BACK $full"
|
|
126
|
+
}
|
|
127
|
+
$ok = $false
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
$ok = $false
|
|
132
|
+
$results += "EXCEPTION: $($_.Exception.Message)"
|
|
133
|
+
foreach ($full in @($changed.Keys)) {
|
|
134
|
+
Copy-Item -LiteralPath $changed[$full] -Destination $full -Force
|
|
135
|
+
$results += "ROLLED-BACK $full"
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# --- 历史记录 ---
|
|
140
|
+
$rec = [ordered]@{
|
|
141
|
+
ts = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
|
142
|
+
patch_id = $patchId; from = $Msg.from; ok = $ok
|
|
143
|
+
edits = $edits.Count; results = $results
|
|
144
|
+
}
|
|
145
|
+
($rec | ConvertTo-Json -Depth 5) | Add-Content (Join-Path $workRoot "patches\history.jsonl") -Encoding UTF8
|
|
146
|
+
|
|
147
|
+
# --- 回 response ---
|
|
148
|
+
Send-Mailbox -Cfg $cfg -To $Msg.from -Type "response" -Topic "patch_result" `
|
|
149
|
+
-ReplyTo $Msg.id -Payload @{ patch_id = $patchId; ok = $ok; results = $results }
|
|
150
|
+
|
|
151
|
+
$results | ForEach-Object { Write-Host " $_" }
|
|
152
|
+
Write-Host " → 补丁结果: $(if ($ok) {'成功'} else {'失败(已回滚)'}) (response 已回 $($Msg.from))"
|
|
153
|
+
return $true
|
|
154
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"identity": "mcp",
|
|
3
|
+
"layout": "dirs",
|
|
4
|
+
"root": "",
|
|
5
|
+
"dirs": {
|
|
6
|
+
"mcp": "D:/Downloads/Agent/Soc/tools/mcp-serial/mailbox",
|
|
7
|
+
"rp": "D:/Downloads/Agent/RP/tools/mailbox"
|
|
8
|
+
},
|
|
9
|
+
"participants": [],
|
|
10
|
+
"intervalSec": 2,
|
|
11
|
+
"timeoutSec": 0,
|
|
12
|
+
"seenFile": "D:/Downloads/Agent/Soc/tools/mcp-serial/tools/.seen_mcp.json",
|
|
13
|
+
"patchRoot": "D:/Downloads/Agent/Soc/tools/mcp-serial"
|
|
14
|
+
}
|