@yuanchilin/dsh-mailbox 0.0.1
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 +89 -0
- package/bin/mailbox.mjs +225 -0
- package/cordis.patch.yml +12 -0
- package/lib/client.js +110 -0
- package/lib/command.js +98 -0
- package/lib/core.js +441 -0
- package/lib/directory-parse.js +37 -0
- package/lib/index.js +319 -0
- package/lib/types/index.d.ts +123 -0
- package/lib/watcher.js +129 -0
- package/package.json +81 -0
- package/skill/README.md +117 -0
- package/skill/SKILL.md +96 -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 +218 -0
- package/skill/mailbox.psm1 +279 -0
- package/skill/self-test.ps1 +105 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 yuanchilin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# @yuanchilin/dsh-mailbox
|
|
2
|
+
|
|
3
|
+
DeepSeek Harness 跨会话文件信箱插件:让多个 DSH 会话 / agent 通过共享文件系统异步通信——**发消息即投递,收消息自动唤醒**,对方离线也不丢信。
|
|
4
|
+
|
|
5
|
+
## 能力一览
|
|
6
|
+
|
|
7
|
+
| 面 | 内容 |
|
|
8
|
+
|---|---|
|
|
9
|
+
| **工具** | `mailbox_send` / `mailbox_recv` / `mailbox_status` / `mailbox_sessions` / `mailbox_alias` / `mailbox_clean` |
|
|
10
|
+
| **命令** | `/mailbox <目标|别名|all> <消息>`(输入框直发);`/mailbox` 空回车列会话目录;`/mailbox recv` 收信;输入 `/mailbox` 弹出**补全选择器** |
|
|
11
|
+
| **内建 watcher** | 新消息 → 为目标会话 agent 创建完成 job → DSH 自动唤醒;随插件启动,**重启自动复活**,无需挂 CLI job |
|
|
12
|
+
| **CLI** | 零依赖 node CLI(`bin/mailbox.mjs`)+ pwsh 双实现(`skill/mailbox.ps1/.psm1`),同协议互通 |
|
|
13
|
+
| **Skill** | `skill/SKILL.md`,复制到 `~/.dsh/skills/mailbox/` 供所有会话加载 |
|
|
14
|
+
|
|
15
|
+
## 核心机制
|
|
16
|
+
|
|
17
|
+
- **文件协议**:每人一个目录 `<root>/<identity>/`,各写各的、互读对方的;消息即 `msg_<id>.json`,定向/广播路由,seen 去重,TTL 清理。
|
|
18
|
+
- **身份自动派生**:默认 `<工作区名>-<会话短id>`(如 `dsh-mailbox-17cbcfa0`),同一工作区多会话互不冲突;配置显式 `identity` 优先。
|
|
19
|
+
- **发现**:`mailbox_sessions` 会话目录(在线/别名/工作区/标题);发送目标未登记时**错误即地址簿**(附完整目录);`to` 支持 identity / 别名 / 完整 sessionId / 工作区路径 / `all`。
|
|
20
|
+
- **默认信箱位置**:`$DSH_HOME/mailbox`(通常 `~/.dsh/mailbox`),无 DSH_HOME 回退 `~/.dsh/mailbox`——可移植,不写死盘符。
|
|
21
|
+
|
|
22
|
+
## 安装(DSH 插件)
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
# 本地源码(开发, 符号链接: 已装包=源码, 改完重启即生效)
|
|
26
|
+
dsh plugin --profile web add link:<本仓库>/packages/mailbox
|
|
27
|
+
|
|
28
|
+
# 或 npm 发布版
|
|
29
|
+
dsh plugin --profile web add @yuanchilin/dsh-mailbox
|
|
30
|
+
|
|
31
|
+
# 然后重启 dsh web
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
配置(可选, 均有默认值):profile 的 `cordis.patch.yml` 覆盖 `root`/`identity` 等:
|
|
35
|
+
|
|
36
|
+
```yaml
|
|
37
|
+
- id: mailbox
|
|
38
|
+
config:
|
|
39
|
+
root: D:/somewhere/shared-mailbox # 默认 ~/.dsh/mailbox
|
|
40
|
+
# identity 留空 → 自动按会话派生; watcher: false 可关闭内建唤醒
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 使用
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
mailbox_send { to: "hub" | "dsh-mailbox-17cbcfa0" | "<sessionId>" | "all",
|
|
47
|
+
type: "notify|request|response|reply", topic: "hello",
|
|
48
|
+
payload: {...}, replyTo: "<msg-id>" }
|
|
49
|
+
mailbox_recv {} → 新消息列表 (自动 seen 去重)
|
|
50
|
+
mailbox_status {} → 身份/目录/会话目录/在线
|
|
51
|
+
mailbox_sessions {} → 会话目录: 找"要对话的会话"
|
|
52
|
+
mailbox_alias { alias: "hub" } → 给本会话设唯一别名
|
|
53
|
+
mailbox_clean { ttlHours: 24, dryRun: false }
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
CLI:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
node packages/mailbox/bin/mailbox.mjs send --identity me --to hub --topic hello --payload '{"text":"hi"}'
|
|
60
|
+
node packages/mailbox/bin/mailbox.mjs recv --identity me
|
|
61
|
+
node packages/mailbox/bin/mailbox.mjs sessions --identity me
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 开发
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
npm install # 仓库根 (npm workspace)
|
|
68
|
+
npm test # node:test, 30 用例
|
|
69
|
+
npm run build # 重建浏览器 bundle lib/client.js (提交入库)
|
|
70
|
+
npm run watch:client # bundle 监视重建
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
结构:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
lib/ 核心逻辑 (纯 JS): core/command/watcher/directory-parse + index(插件) + types
|
|
77
|
+
src/client/ 浏览器插件源码 → lib/client.js (/mailbox 补全弹窗)
|
|
78
|
+
bin/ node CLI
|
|
79
|
+
skill/ SKILL.md + pwsh 双实现 + 示例
|
|
80
|
+
test/ node:test 用例
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## CI
|
|
84
|
+
|
|
85
|
+
`.github/workflows/ci.yml`:Node 22/24,`npm ci` → 测试 → 构建 → 校验 `lib/client.js` 与源码同步。
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT
|
package/bin/mailbox.mjs
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// @yuanchilin/dsh-mailbox — node CLI (薄封装, 复用 lib/core.js)
|
|
4
|
+
//
|
|
5
|
+
// node bin/mailbox.mjs send --to agent-b --topic hello --payload '{"x":1}'
|
|
6
|
+
// node bin/mailbox.mjs recv --format json
|
|
7
|
+
// node bin/mailbox.mjs wait --timeout 600 # 新消息即 exit 0 (唤醒)
|
|
8
|
+
// node bin/mailbox.mjs poll --interval 2 # 常驻 (request→echo)
|
|
9
|
+
// node bin/mailbox.mjs clean --ttl-hours 24 --dry-run
|
|
10
|
+
// node bin/mailbox.mjs status
|
|
11
|
+
// node bin/mailbox.mjs sessions # 会话目录 (注册表: 在线/别名/工作区)
|
|
12
|
+
// node bin/mailbox.mjs init --id agent-a --root D:/Downloads/Agent/.mailbox
|
|
13
|
+
//
|
|
14
|
+
// 配置优先级: 参数 > 环境变量 (MAILBOX_CONFIG/ID/ROOT/INTERVAL/TIMEOUT) > 配置文件 > 默认
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
18
|
+
import { join, dirname } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import * as core from "../lib/core.js";
|
|
21
|
+
|
|
22
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const DEFAULT_CONFIG = join(__dirname, "..", "mailbox.config.json");
|
|
24
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
25
|
+
|
|
26
|
+
function parseArgs(argv) {
|
|
27
|
+
const args = { _: [] };
|
|
28
|
+
for (let i = 0; i < argv.length; i++) {
|
|
29
|
+
const a = argv[i];
|
|
30
|
+
if (a.startsWith("--")) {
|
|
31
|
+
const key = a.slice(2);
|
|
32
|
+
if (i + 1 < argv.length && !argv[i + 1].startsWith("--")) args[key] = argv[++i];
|
|
33
|
+
else args[key] = true;
|
|
34
|
+
} else {
|
|
35
|
+
args._.push(a);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return args;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getConfig(args) {
|
|
42
|
+
let fileCfg = {};
|
|
43
|
+
const configPath = args.config || process.env.MAILBOX_CONFIG || DEFAULT_CONFIG;
|
|
44
|
+
if (existsSync(configPath)) {
|
|
45
|
+
try {
|
|
46
|
+
fileCfg = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.warn(`[mailbox] 读取配置失败: ${configPath} (${e.message})`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const cfg = core.resolveConfig({ ...fileCfg }, process.env);
|
|
52
|
+
if (args.identity) cfg.identity = args.identity;
|
|
53
|
+
if (args.root) { cfg.root = args.root; cfg.layout = "root"; }
|
|
54
|
+
if (args.interval !== undefined) cfg.intervalSec = Number(args.interval);
|
|
55
|
+
if (args.timeout !== undefined) cfg.timeoutSec = Number(args.timeout);
|
|
56
|
+
// 配置文件里 root 为空串时回退到源码默认 (DSH_HOME/mailbox 或 ~/.dsh/mailbox)
|
|
57
|
+
if (!cfg.root && cfg.layout === "root") cfg.root = core.defaultMailboxRoot();
|
|
58
|
+
return { cfg, configPath };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 心跳是 best-effort: 共享根不可写(沙箱)时静默跳过, 不影响主流程 (尤其 wait 监听循环)。 */
|
|
62
|
+
function safeTouchCli(cfg) {
|
|
63
|
+
try {
|
|
64
|
+
safeTouchCli(cfg);
|
|
65
|
+
} catch {
|
|
66
|
+
// 忽略: 心跳失败不应让 send/recv/wait/poll/status 崩溃
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const commands = {
|
|
71
|
+
init(args, cfg, configPath) {
|
|
72
|
+
const out = {
|
|
73
|
+
identity: cfg.identity,
|
|
74
|
+
layout: cfg.layout,
|
|
75
|
+
root: cfg.root,
|
|
76
|
+
dirs: cfg.dirs,
|
|
77
|
+
participants: cfg.participants,
|
|
78
|
+
intervalSec: cfg.intervalSec,
|
|
79
|
+
timeoutSec: cfg.timeoutSec,
|
|
80
|
+
seenFile: cfg.seenFile,
|
|
81
|
+
patchRoot: cfg.patchRoot,
|
|
82
|
+
};
|
|
83
|
+
writeFileSync(configPath, JSON.stringify(out, null, 2) + "\n", "utf-8");
|
|
84
|
+
console.log(`已生成配置: ${configPath}`);
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
send(args, cfg) {
|
|
88
|
+
if (!args.to) throw new Error("send 需要 --to <id|alias|all>");
|
|
89
|
+
let payload = {};
|
|
90
|
+
if (args.payload) {
|
|
91
|
+
try { payload = JSON.parse(args.payload); } catch { throw new Error(`payload 不是合法 JSON: ${args.payload}`); }
|
|
92
|
+
}
|
|
93
|
+
core.assertUsable(cfg);
|
|
94
|
+
safeTouchCli(cfg);
|
|
95
|
+
const target = core.resolveTarget(cfg, args.to);
|
|
96
|
+
const id = core.sendMessage(cfg, {
|
|
97
|
+
to: args.to,
|
|
98
|
+
type: args.type || "notify",
|
|
99
|
+
topic: args.topic || "",
|
|
100
|
+
payload,
|
|
101
|
+
replyTo: args.replyTo || "",
|
|
102
|
+
});
|
|
103
|
+
console.log(`sent ${id} (${new Date().toTimeString().slice(0, 8)})`);
|
|
104
|
+
if (!core.isKnownTarget(cfg, target)) {
|
|
105
|
+
console.log(core.unknownTargetHint(cfg, target));
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
recv(args, cfg) {
|
|
110
|
+
core.assertUsable(cfg);
|
|
111
|
+
safeTouchCli(cfg);
|
|
112
|
+
const msgs = core.recvNew(cfg, true);
|
|
113
|
+
if (msgs.length === 0) { console.log("(无新消息)"); return; }
|
|
114
|
+
if (args.format === "json") {
|
|
115
|
+
for (const m of msgs) console.log(JSON.stringify(m));
|
|
116
|
+
} else {
|
|
117
|
+
for (const m of msgs) {
|
|
118
|
+
const p = m.payload && Object.keys(m.payload).length ? ` payload=${JSON.stringify(m.payload)}` : "";
|
|
119
|
+
console.log(`[${m.from} -> ${m.to}] ${m.type} topic=${m.topic} id=${m.id}${m.reply_to ? ` reply_to=${m.reply_to}` : ""}${p}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
async wait(args, cfg) {
|
|
125
|
+
core.assertUsable(cfg);
|
|
126
|
+
safeTouchCli(cfg);
|
|
127
|
+
const started = Date.now();
|
|
128
|
+
for (;;) {
|
|
129
|
+
const msgs = core.recvNew(cfg, true);
|
|
130
|
+
if (msgs.length > 0) {
|
|
131
|
+
console.log(`=== NEW MESSAGES: ${msgs.length} ===`);
|
|
132
|
+
for (const m of msgs) console.log(JSON.stringify(m));
|
|
133
|
+
console.log("=== WAKE-UP (exit 0) ===");
|
|
134
|
+
process.exit(0);
|
|
135
|
+
}
|
|
136
|
+
if (cfg.timeoutSec > 0 && (Date.now() - started) / 1000 >= cfg.timeoutSec) {
|
|
137
|
+
console.log(`TIMEOUT after ${cfg.timeoutSec}s, no new messages`);
|
|
138
|
+
process.exit(0);
|
|
139
|
+
}
|
|
140
|
+
await sleep(cfg.intervalSec * 1000);
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
async poll(args, cfg) {
|
|
145
|
+
core.assertUsable(cfg);
|
|
146
|
+
safeTouchCli(cfg);
|
|
147
|
+
console.log(`poll 启动 (identity=${cfg.identity} 每 ${cfg.intervalSec}s). Ctrl+C 退出`);
|
|
148
|
+
for (;;) {
|
|
149
|
+
try {
|
|
150
|
+
for (const m of core.recvNew(cfg, true)) {
|
|
151
|
+
console.log(`[收到] from=${m.from} type=${m.type} topic=${m.topic} id=${m.id}`);
|
|
152
|
+
if (m.type === "request") {
|
|
153
|
+
core.sendMessage(cfg, {
|
|
154
|
+
to: m.from,
|
|
155
|
+
type: "response",
|
|
156
|
+
topic: m.topic,
|
|
157
|
+
payload: { echo: m.payload, from: cfg.identity },
|
|
158
|
+
replyTo: m.id,
|
|
159
|
+
});
|
|
160
|
+
console.log(` → 已回 response (reply_to=${m.id})`);
|
|
161
|
+
} else {
|
|
162
|
+
console.log(JSON.stringify(m));
|
|
163
|
+
}
|
|
164
|
+
try { core.removeMessage(cfg, m.id, true); } catch { /* 权限不足则跳过 */ }
|
|
165
|
+
}
|
|
166
|
+
} catch (e) {
|
|
167
|
+
console.warn(`轮询异常: ${e.message}`);
|
|
168
|
+
}
|
|
169
|
+
await sleep(cfg.intervalSec * 1000);
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
clean(args, cfg) {
|
|
174
|
+
core.assertUsable(cfg);
|
|
175
|
+
const removed = core.cleanTTL(cfg, {
|
|
176
|
+
ttlHours: args.ttlHours !== undefined ? Number(args.ttlHours) : 24,
|
|
177
|
+
dryRun: !!args.dryRun,
|
|
178
|
+
});
|
|
179
|
+
console.log(`clean: ${args.dryRun ? "dry-run" : "已删除"} ${removed} 条过期消息`);
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
status(args, cfg) {
|
|
183
|
+
core.assertUsable(cfg);
|
|
184
|
+
safeTouchCli(cfg);
|
|
185
|
+
const s = core.statusOf(cfg);
|
|
186
|
+
console.log(`身份: ${s.identity} layout=${s.layout}`);
|
|
187
|
+
console.log(`写: ${s.outDir} (消息 ${s.outCount})`);
|
|
188
|
+
console.log(`seen: ${s.seen} 条`);
|
|
189
|
+
for (const i of s.inboxes) console.log(`读: ${i.name || i.dir} (消息 ${i.msgCount}, 未读 ${i.unread ?? 0})`);
|
|
190
|
+
if (s.sessions.length > 0) {
|
|
191
|
+
console.log("会话目录:");
|
|
192
|
+
const now = Date.now();
|
|
193
|
+
for (const r of s.sessions) {
|
|
194
|
+
const on = r.online ? "●在线" : "○离线";
|
|
195
|
+
const ago = r.lastSeen ? `${Math.max(0, Math.round((now - r.lastSeen) / 1000))}s前` : "-";
|
|
196
|
+
console.log(` ${on} ${r.identity}${r.alias ? ` (${r.alias})` : ""} ${r.workspace || "?"}${r.title ? ` «${r.title}»` : ""} last=${ago}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
sessions(args, cfg) {
|
|
202
|
+
core.assertUsable(cfg);
|
|
203
|
+
const list = core.listSessions(cfg);
|
|
204
|
+
if (list.length === 0) { console.log("(暂无注册会话: 各会话调用一次 mailbox 工具即自动登记)"); return; }
|
|
205
|
+
const now = Date.now();
|
|
206
|
+
for (const r of list) {
|
|
207
|
+
const on = core.isOnline(r, cfg.presenceWindowSec) ? "●在线" : "○离线";
|
|
208
|
+
const ago = r.lastSeen ? `${Math.max(0, Math.round((now - r.lastSeen) / 1000))}s前` : "-";
|
|
209
|
+
console.log(`${on} ${r.identity}${r.alias ? ` (alias=${r.alias})` : ""} ${r.workspace || "?"}${r.title ? ` «${r.title}»` : ""} last=${ago}`);
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const args = parseArgs(process.argv.slice(2));
|
|
215
|
+
const command = args._[0] || "status";
|
|
216
|
+
const { cfg, configPath } = getConfig(args);
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const fn = commands[command];
|
|
220
|
+
if (!fn) throw new Error(`未知命令: ${command} (可用: init/send/recv/wait/poll/clean/status/sessions)`);
|
|
221
|
+
await fn(args, cfg, configPath);
|
|
222
|
+
} catch (e) {
|
|
223
|
+
console.error(`[mailbox] ${e.message}`);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# The dsh-mailbox bundle patch: registers the mailbox plugin row over the
|
|
2
|
+
# profile root. Override identity/root in the profile's own cordis.patch.yml:
|
|
3
|
+
#
|
|
4
|
+
# - id: mailbox
|
|
5
|
+
# config:
|
|
6
|
+
# identity: agent-a
|
|
7
|
+
# root: D:/Downloads/Agent/.mailbox
|
|
8
|
+
#
|
|
9
|
+
# layout=dirs (legacy dual-directory): provide dirs: { "<id>": "<path>" }.
|
|
10
|
+
- insert:
|
|
11
|
+
- id: mailbox
|
|
12
|
+
name: '@yuanchilin/dsh-mailbox'
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@yuanchilin/dsh-mailbox",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
//#region lib/directory-parse.js
|
|
8
|
+
const DIRECTORY_LINE = /^\s{2}([●○])(在线|离线)\s+(\S+)(?:\s+\(([^()]*)\))?\s+(.*)$/;
|
|
9
|
+
/**
|
|
10
|
+
* 从 /mailbox 目录输出文本解析会话行。
|
|
11
|
+
* @returns { id, label, detail }[] (popupSelect SelectOption 形状)
|
|
12
|
+
*/
|
|
13
|
+
function parseDirectory(text) {
|
|
14
|
+
const rows = [];
|
|
15
|
+
if (typeof text !== "string") return rows;
|
|
16
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
17
|
+
const m = DIRECTORY_LINE.exec(raw);
|
|
18
|
+
if (!m) continue;
|
|
19
|
+
const identity = m[3];
|
|
20
|
+
const alias = m[4] ?? "";
|
|
21
|
+
const rest = (m[5] ?? "").trim();
|
|
22
|
+
rows.push({
|
|
23
|
+
id: identity,
|
|
24
|
+
label: alias ? `${identity} (${alias})` : identity,
|
|
25
|
+
detail: `${m[1] === "●" ? "●在线" : "○离线"} ${rest || "?"}`
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return rows;
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/client/index.ts
|
|
32
|
+
/**
|
|
33
|
+
* Browser client plugin for the mailbox: decorates the HOST `/mailbox`
|
|
34
|
+
* command with a popupSelect — typing `/mailbox` pops the session directory
|
|
35
|
+
* as selectable options, picking one fills the composer with
|
|
36
|
+
* `/mailbox <identity> ` so the user types the message and Enter executes
|
|
37
|
+
* the host command.
|
|
38
|
+
*
|
|
39
|
+
* 低风险设计 (教训沉淀):
|
|
40
|
+
* - 不用自定义 Remote 命名空间 (无挂载/注入死锁)。选项数据走成熟的
|
|
41
|
+
* `remote.commands` 通道: 执行 `/mailbox` 拿目录文本, 解析成选项。
|
|
42
|
+
* - 根级只注入 ui-commands 自己也在用的成熟服务
|
|
43
|
+
* (commandUi / remote / remote.commands / sessions)。
|
|
44
|
+
* - `conversation.input` 不是服务键, 在 onSelect 里经 ctx.conversation
|
|
45
|
+
* 懒取, 取不到静默跳过。
|
|
46
|
+
*
|
|
47
|
+
* @module @yuanchilin/dsh-mailbox/client
|
|
48
|
+
*/
|
|
49
|
+
const name = "mailbox-client";
|
|
50
|
+
const inject = [
|
|
51
|
+
"commandUi",
|
|
52
|
+
"remote",
|
|
53
|
+
"remote.commands",
|
|
54
|
+
"sessions"
|
|
55
|
+
];
|
|
56
|
+
function apply(ctx) {
|
|
57
|
+
ctx.inject([
|
|
58
|
+
"commandUi",
|
|
59
|
+
"remote",
|
|
60
|
+
"remote.commands",
|
|
61
|
+
"sessions"
|
|
62
|
+
], (scope) => {
|
|
63
|
+
const command = scope.get("commandUi");
|
|
64
|
+
const sessions = scope.get("sessions");
|
|
65
|
+
const remote = scope.get("remote");
|
|
66
|
+
scope.effect(() => command.decorate({
|
|
67
|
+
name: "mailbox",
|
|
68
|
+
available: () => true,
|
|
69
|
+
ui: {
|
|
70
|
+
kind: "popupSelect",
|
|
71
|
+
options: async (session) => {
|
|
72
|
+
if (remote.commands === void 0) return [];
|
|
73
|
+
const result = await remote.commands.execute(session.sessionId, "/mailbox");
|
|
74
|
+
if (!result.ok || result.value === void 0) throw new Error(`mailbox 目录获取失败: ${result.error?.message ?? result.error?.code ?? "unknown"}`);
|
|
75
|
+
const rows = parseDirectory(result.value.result?.text ?? "");
|
|
76
|
+
if (rows.length === 0) throw new Error("mailbox 目录为空 (暂无注册会话)");
|
|
77
|
+
return rows;
|
|
78
|
+
},
|
|
79
|
+
onSelect: async (option, session) => {
|
|
80
|
+
const actx = sessions.scope(session.sessionId);
|
|
81
|
+
if (actx === void 0) return;
|
|
82
|
+
const input = scope.get("conversation")?.input?.for(actx);
|
|
83
|
+
if (input === void 0) return;
|
|
84
|
+
input.setDraft(`/mailbox ${option.id} `);
|
|
85
|
+
setTimeout(() => {
|
|
86
|
+
const list = document.querySelectorAll("textarea[data-phase]");
|
|
87
|
+
let target = null;
|
|
88
|
+
for (const el of list) if (el.offsetParent !== null) {
|
|
89
|
+
target = el;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
target ??= list[0] ?? null;
|
|
93
|
+
if (target === null) return;
|
|
94
|
+
target.focus();
|
|
95
|
+
target.setSelectionRange(target.value.length, target.value.length);
|
|
96
|
+
}, 0);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}), "mailbox: /mailbox popup decoration");
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
103
|
+
exports.apply = apply;
|
|
104
|
+
exports.inject = inject;
|
|
105
|
+
exports.name = name;
|
|
106
|
+
return module.exports;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
//# sourceMappingURL=client.js.map
|
package/lib/command.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// @yuanchilin/dsh-mailbox — 宿主 `/mailbox` 命令
|
|
3
|
+
//
|
|
4
|
+
// 在聊天输入框直接使用 (服务端命令注册表, 每个会话独立执行):
|
|
5
|
+
// /mailbox → 用法 + 会话目录 (发现对方)
|
|
6
|
+
// /mailbox <target|alias|all> <message> → 定向/别名/广播发送
|
|
7
|
+
// /mailbox recv → 收取发给我的新消息
|
|
8
|
+
// /mailbox list → 会话目录
|
|
9
|
+
//
|
|
10
|
+
// 执行时带会话上下文 (invocation.agent.session): 身份自动派生 + 心跳登记,
|
|
11
|
+
// 与 mailbox_send 等工具完全同协议。
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
import * as core from "./core.js";
|
|
15
|
+
|
|
16
|
+
export const MAILBOX_USAGE = "Usage: /mailbox <target|alias|all> <message> | /mailbox [list|recv]";
|
|
17
|
+
|
|
18
|
+
/** 解析命令输入 (首 token 为目标, 其余为消息)。 */
|
|
19
|
+
export function parseMailboxCommand(rawInput) {
|
|
20
|
+
const input = String(rawInput ?? "").trim();
|
|
21
|
+
if (input.length === 0) return { kind: "usage" };
|
|
22
|
+
const lower = input.toLowerCase();
|
|
23
|
+
if (lower === "list" || lower === "sessions" || lower === "help" || lower === "?") return { kind: "list" };
|
|
24
|
+
if (lower === "recv") return { kind: "recv" };
|
|
25
|
+
const sp = input.search(/\s/);
|
|
26
|
+
if (sp === -1) return { kind: "no-message", to: input };
|
|
27
|
+
const to = input.slice(0, sp).trim();
|
|
28
|
+
const message = input.slice(sp + 1).trim();
|
|
29
|
+
if (!message) return { kind: "no-message", to };
|
|
30
|
+
return { kind: "send", to, message };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 渲染会话目录 (身份/别名/在线/工作区/标题/未读)。 */
|
|
34
|
+
export function renderDirectory(eff) {
|
|
35
|
+
const sessions = core.listSessions(eff);
|
|
36
|
+
if (sessions.length === 0) return "(暂无注册会话: 各会话调用一次 mailbox 工具/命令即自动登记)";
|
|
37
|
+
return sessions
|
|
38
|
+
.map((s) => {
|
|
39
|
+
const on = core.isOnline(s, eff.presenceWindowSec) ? "●在线" : "○离线";
|
|
40
|
+
const alias = s.alias ? ` (${s.alias})` : "";
|
|
41
|
+
const title = s.title ? ` «${s.title}»` : "";
|
|
42
|
+
const unread = core.unreadFrom(eff, s.identity) ? ` 未读${core.unreadFrom(eff, s.identity)}` : "";
|
|
43
|
+
return ` ${on} ${s.identity}${alias} ${s.workspace || "?"}${title}${unread}`;
|
|
44
|
+
})
|
|
45
|
+
.join("\n");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 执行一次解析后的 /mailbox 命令, 返回 CommandResult ({ kind, text })。 */
|
|
49
|
+
export function executeMailboxCommand(ctx, cfg, invocation) {
|
|
50
|
+
const session = invocation.agent?.session;
|
|
51
|
+
const eff = core.effectiveConfig(cfg, session);
|
|
52
|
+
core.touchRegistry(cfg, session);
|
|
53
|
+
const parsed = parseMailboxCommand(invocation.rawInput);
|
|
54
|
+
switch (parsed.kind) {
|
|
55
|
+
case "usage":
|
|
56
|
+
return { kind: "success", text: `${MAILBOX_USAGE}\n\n会话目录:\n${renderDirectory(eff)}` };
|
|
57
|
+
case "list":
|
|
58
|
+
return { kind: "success", text: `会话目录:\n${renderDirectory(eff)}` };
|
|
59
|
+
case "recv": {
|
|
60
|
+
const msgs = core.recvNew(eff, true);
|
|
61
|
+
if (msgs.length === 0) return { kind: "success", text: "(无新消息)" };
|
|
62
|
+
const lines = msgs.map((m) => {
|
|
63
|
+
const p = m.payload && Object.keys(m.payload).length ? ` payload=${JSON.stringify(m.payload)}` : "";
|
|
64
|
+
return `[${m.from} -> ${m.to}] ${m.type} topic=${m.topic} id=${m.id}${m.reply_to ? ` reply_to=${m.reply_to}` : ""}${p}`;
|
|
65
|
+
});
|
|
66
|
+
return { kind: "success", text: `新消息 ${msgs.length} 条:\n${lines.join("\n")}` };
|
|
67
|
+
}
|
|
68
|
+
case "no-message":
|
|
69
|
+
return { kind: "error", text: `/mailbox ${parsed.to}: 缺少消息内容。${MAILBOX_USAGE}` };
|
|
70
|
+
case "send": {
|
|
71
|
+
const target = core.resolveTarget(eff, parsed.to);
|
|
72
|
+
const id = core.sendMessage(eff, {
|
|
73
|
+
to: parsed.to,
|
|
74
|
+
type: "notify",
|
|
75
|
+
topic: "message",
|
|
76
|
+
payload: { text: parsed.message },
|
|
77
|
+
});
|
|
78
|
+
const rec = core.listSessions(eff).find((s) => s.identity === target);
|
|
79
|
+
const status = rec
|
|
80
|
+
? core.isOnline(rec, eff.presenceWindowSec) ? "目标在线" : "目标离线"
|
|
81
|
+
: parsed.to === "all" ? "" : core.unknownTargetHint(eff, target);
|
|
82
|
+
return {
|
|
83
|
+
kind: "success",
|
|
84
|
+
text: `已发送 ${id} (${eff.identity} → ${target}) ${status}\n内容: ${parsed.message}`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 向宿主命令注册表注册 /mailbox。 */
|
|
91
|
+
export function registerMailboxCommand(ctx, cfg) {
|
|
92
|
+
ctx.commands.register({
|
|
93
|
+
name: "mailbox",
|
|
94
|
+
description: "跨会话发消息: /mailbox <目标|别名|all> <消息>; 单独 /mailbox 查看会话目录; /mailbox recv 收消息",
|
|
95
|
+
input: { hint: "<target|alias|all> <message>" },
|
|
96
|
+
handler: (invocation) => executeMailboxCommand(ctx, cfg, invocation),
|
|
97
|
+
});
|
|
98
|
+
}
|