给同一台机器上的多个 AI 会话(或多台引擎)用的异步信箱:默认离线,信不会丢,万物皆插件(DSH / Cordis 风格)
31.5/100
综合分
2
GitHub 星标
—
npm 周下载
—/5
用户评分
数据截至 2026/10/18 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
自动兼容检查未通过:缺少 main/exports/bin 入口声明;仓库 package.json 标记 private,未发布到 npm,需从源码安装。以下命令仍可尝试,但可能失败。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add deepsleepAquarium/dsh-whale-post该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)
状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:已验证
是什么
dsh 原生插件 · platform
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 17 天前
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查需源码安装缺少 main/exports/bin 入口声明;仓库 package.json 标记 private,未发布到 npm,需从源码安装
展开查看检查明细 ↓
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
✗npm 包dsh-whale-post(未发布到 npm,仅可源码安装)
✓Node 引擎要求 >=20 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✗入口文件缺少入口声明
源码安装时若 pnpm 拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑。
验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/10 21:26:47
安全扫描自动 · 静态分析源码v2.7.0🔵 扫描:含敏感能力尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
v2.7.0静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。
可执行外部命令读写/删除本地文件
查看扫描证据(共 288 处,展示前 106 条)
- 写入本地文件packages/bus/index.js:110
mkdirSync(d, { recursive: true }) - 写入本地文件packages/bus/index.js:115
mkdirSync(dirname(file), { recursive: true }) - 写入本地文件packages/bus/index.js:117
writeFileSync(tmp, text, 'utf8') - 写入本地文件packages/bus/index.js:121
try { renameSync(tmp, file); return file } catch (err) { - 写入本地文件packages/bus/index.js:127
writeFileSync(file, text, 'utf8') - 删除本地文件packages/bus/index.js:128
try { unlinkSync(tmp) } catch { } - 读取本地文件packages/bus/index.js:135
if (existsSync(f)) { - 读取本地文件packages/bus/index.js:136
const t = readFileSync(f, 'utf8').trim() - 写入本地文件packages/bus/index.js:140
mkdirSync(dirname(f), { recursive: true }) - 读取本地文件packages/bus/index.js:200
const s = JSON.parse(readFileSync(paths().state(as), 'utf8')) - 读取本地文件packages/bus/index.js:271
const env = JSON.parse(readFileSync(paths().hello(as), 'utf8')) - 读取本地文件packages/bus/index.js:293
const env = JSON.parse(readFileSync(paths().hello(as), 'utf8')) - 读取本地文件packages/bus/index.js:318
const env = JSON.parse(readFileSync(paths().hello(as), 'utf8')) - 删除本地文件packages/bus/index.js:455
if (Number(m[2]) < cut) { try { unlinkSync(join(stateDir(), f)) } catch { } } - 删除本地文件packages/bus/index.js:712
unlinkSync(join(dir, f)) - 删除本地文件packages/bus/index.js:784
unlinkSync(p) - 读取本地文件packages/bus/selftest.mjs:15
const ls = (d) => { try { return readdirSync(d) } catch { return [] } } - 写入本地文件packages/bus/selftest.mjs:17
mkdirSync(tmp, { recursive: true }) - 写入本地文件packages/bus/selftest.mjs:18
writeFileSync(join(tmp, 'roster.json'), JSON.stringify({ apiVersion: 1, members: [{ id: 'alice' }, { id: 'bob' }], groups: { all: ['alice', 'bob'] } } - 读取本地文件packages/bus/selftest.mjs:40
const raw = JSON.parse(readFileSync(join(tmp, 'inbox', 'bob', `${r.id}.msg.json`), 'utf8')) - 写入本地文件packages/bus/selftest.mjs:80
mkdirSync(keepRoot, { recursive: true }) - 写入本地文件packages/bus/selftest.mjs:82
writeFileSync(keepRosterFile, JSON.stringify({ apiVersion: 1, members: [{ id: 'alice' }, { id: 'bob' }] }), 'utf8') - 写入本地文件packages/bus/selftest.mjs:87
writeFileSync(kp, readFileSync(kp, 'utf8').replace('会被改坏的一封', '被改过了'), 'utf8') - 读取本地文件packages/bus/selftest.mjs:87
writeFileSync(kp, readFileSync(kp, 'utf8').replace('会被改坏的一封', '被改过了'), 'utf8') - 读取本地文件packages/bus/selftest.mjs:91
check('keep:那封信**还在 inbox**(没被挪进退信)', existsSync(kp)) - 读取本地文件packages/bus/selftest.mjs:92
check('keep:退信目录**没多出东西**', !existsSync(join(busKeep.paths().dead, `${rk.id}.msg.json`))) - 读取本地文件packages/bus/selftest.mjs:95
eatOut.length === 1 && /已挪进/.test(eatOut[0].why) && !existsSync(kp) && existsSync(join(busKeep.paths().dead, `${rk.id}.msg.json`)), - 写入本地文件packages/bus/selftest.mjs:104
mkdirSync(tmpOff, { recursive: true }) - 删除本地文件packages/bus/selftest.mjs:310
unlinkSync(join(ackRoot, 'hello', 'dave.json')) - 执行外部命令来自配置/变量packages/cli/index.js:67
const res = spawnSync(process.execPath, ['-e', probe, host], { timeout: 2500, stdio: 'ignore' }) - 读取本地文件packages/cli/index.js:88
const j = JSON.parse(readFileSync(pinnedFile(r), 'utf8')) - 写入本地文件packages/cli/index.js:93
mkdirSync(join(r, 'state'), { recursive: true }) - 写入本地文件packages/cli/index.js:94
writeFileSync(pinnedFile(r), JSON.stringify({ note: '"钉"=把推断的休眠升格成明示(人的决定,留痕)', pinned }, null, 2), 'utf8') - 读取本地文件packages/cli/index.js:195
const ls = (d) => { try { return readdirSync(d) } catch { return [] } } - 写入本地文件packages/cli/index.js:198
mkdirSync(tmp, { recursive: true }) - 写入本地文件packages/cli/index.js:199
writeFileSync(join(tmp, 'roster.json'), JSON.stringify({ - 读取本地文件packages/cli/index.js:212
check('离线件:投进对方信箱', existsSync(join(tmp, 'inbox', 'bob', `${s1.id}.msg.json`))) - 读取本地文件packages/cli/index.js:230
check('只投活体:信仍在对方信箱里', existsSync(join(tmp, 'inbox', 'carol', `${s3.id}.msg.json`))) - 读取本地文件packages/cli/index.js:274
const raw = JSON.parse(readFileSync(join(tmp, 'inbox', 'alice', f), 'utf8')) - 读取本地文件packages/cli/index.js:325
try { return JSON.parse(readFileSync(p, 'utf8')) } catch { return null } - 执行外部命令固定命令packages/cli/index.js:350
const r = spawnSync('dsh --version', { encoding: 'utf8', shell: true, timeout: 20000 }) - 写入本地文件packages/cli/index.js:517
mkdirSync(dstDir, { recursive: true }) - 写入本地文件packages/cli/index.js:519
writeFileSync(join(dstDir, `${r.id}.msg.json`), readFileSync(srcP, 'utf8'), 'utf8') - 删除本地文件packages/cli/index.js:520
unlinkSync(srcP) - 执行外部命令来自配置/变量packages/cli/selftest.mjs:18
const run = (args) => spawnSync(process.execPath, [bin, ...args], { encoding: 'utf8' }) - 执行外部命令来自配置/变量packages/cli/selftest.mjs:21
const inner = spawnSync(process.execPath, [bin, 'selftest'], { stdio: 'inherit' }) - 写入本地文件packages/cli/selftest.mjs:26
mkdirSync(tmp, { recursive: true }) - 写入本地文件packages/cli/selftest.mjs:27
writeFileSync(join(tmp, 'roster.json'), JSON.stringify({ - 写入本地文件packages/cli/selftest.mjs:48
mkdirSync(vtmp, { recursive: true }) - 写入本地文件packages/cli/selftest.mjs:49
writeFileSync(join(vtmp, 'roster.json'), JSON.stringify({ - 写入本地文件packages/cli/selftest.mjs:77
for (const d of [pRemote, pLocal]) { mkdirSync(d, { recursive: true }); writeFileSync(join(d, 'roster.json'), rosterJson, 'utf8') } - 写入本地文件packages/cli/selftest.mjs:77
for (const d of [pRemote, pLocal]) { mkdirSync(d, { recursive: true }); writeFileSync(join(d, 'roster.json'), rosterJson, 'utf8') } - 读取本地文件packages/cli/selftest.mjs:81
const lsDir = (d) => { try { return readdirSync(d).filter((x) => x.endsWith('.msg.json')) } catch { return [] } } - 读取本地文件packages/cli/selftest.mjs:94
writeFileSync(join(rInbox, victim), readFileSync(join(rInbox, victim), 'utf8').replace('原正文', '被改过的正文'), 'utf8') - 执行外部命令来自配置/变量packages/cli/selftest.mjs:102
spawnSync(process.execPath, [bin, 'pickup', '--as', 'bob', '--root', pLocal], { encoding: 'utf8', env: { ...process.env, WHALE_POST_REMOTE_ROOT: '' } - 删除本地文件packages/cli/selftest.mjs:105
unlinkSync(join(rInbox, victim)) - 执行外部命令来自配置/变量packages/cli/selftest.mjs:106
const pkEnv = spawnSync(process.execPath, [bin, 'pickup', '--as', 'bob', '--root', pLocal], { encoding: 'utf8', env: { ...process.env, WHALE_POST_REMO - 读取本地文件packages/cli/selftest.mjs:121
existsSync(join(s6Local, 'hello', 'phone.json')) && !existsSync(join(s6Local, 'hello', 'web.json')), - 读取本地文件packages/cli/selftest.mjs:122
JSON.stringify((() => { try { return readdirSync(join(s6Local, 'hello')) } catch { return [] } })())) - 读取本地文件packages/cli/selftest.mjs:124
existsSync(join(s6Remote, 'seen', 'web', `${lsDir(join(s6Remote, 'seen', 'web'))[0] ?? 'x'}`)) && lsDir(join(s6Remote, 'inbox', 'web')).length === 0, - 读取本地文件packages/cli/selftest.mjs:129
!existsSync(join(s6Local, 'state', 'quota-phone.json')) && !existsSync(join(s6Local, 'state', 'quota-phone.d'))) - 删除本地文件packages/cli/selftest.mjs:177
unlinkSync(join(hRoot, 'seen', 'web', hFile)) - 执行外部命令来自配置/变量packages/cli/selftest.mjs:373
const p = spawn(process.execPath, [bin, ...args], { stdio: 'ignore' }) - 删除本地文件packages/cli/selftest.mjs:418
unlinkSync(join(sd, f0)) - 执行外部命令来自配置/变量packages/cli/selftest.mjs:440
const envRun = spawnSync(process.execPath, [bin, 'env'], { encoding: 'utf8', env: { ...process.env, DSH_HOME: fakeHome } }) - 写入本地文件packages/gate/index.js:109
mkdirSync(dirname(file), { recursive: true }) - 写入本地文件packages/gate/index.js:111
writeFileSync(tmp, text, 'utf8') - 写入本地文件packages/gate/index.js:116
try { renameSync(tmp, file); return file } catch (err) { - 写入本地文件packages/gate/index.js:122
writeFileSync(file, text, 'utf8') - 删除本地文件packages/gate/index.js:123
try { unlinkSync(tmp) } catch { } - 读取本地文件packages/gate/index.js:148
const j = JSON.parse(readFileSync(stateOf(as), 'utf8')) - 读取本地文件packages/gate/index.js:157
try { files = readdirSync(incrDir(as)).filter((f) => f.endsWith('.json')).sort() } catch { files = [] } - 读取本地文件packages/gate/index.js:160
try { x = JSON.parse(readFileSync(join(incrDir(as), f), 'utf8')) } catch { continue } - 写入本地文件packages/gate/index.js:321
mkdirSync(dir, { recursive: true }) - 写入本地文件packages/gate/selftest.mjs:12
mkdirSync(tmp, { recursive: true }) - 读取本地文件packages/gate/selftest.mjs:86
existsSync(join(tmp, 'state', 'quota-erin.d')) || existsSync(join(tmp, 'state', 'quota-erin.json')), - 读取本地文件packages/gate/selftest.mjs:87
JSON.stringify((() => { try { return readdirSync(join(tmp, 'state')) } catch { return [] } })())) - 读取本地文件packages/gate/selftest.mjs:193
const files = (() => { try { return readdirSync(join(r, 'state', 'quota-a.d')) } catch { return [] } })() - 读取本地文件packages/roster/index.js:41
if (!existsSync(file)) return config.sample === true ? SAMPLE : { apiVersion, members: [], groups: {} } - 读取本地文件packages/roster/index.js:42
const j = JSON.parse(readFileSync(file, 'utf8')) - 写入本地文件packages/roster/selftest.mjs:12
mkdirSync(tmp, { recursive: true }) - 写入本地文件packages/roster/selftest.mjs:24
writeFileSync(file, JSON.stringify({ apiVersion: 1, members: [{ id: 'alice', label: 'Alice' }, { id: 'bob' }], groups: { all: ['alice', 'bob'], club: - 写入本地文件packages/roster/selftest.mjs:46
writeFileSync(file, JSON.stringify(withFlags), 'utf8') - 写入本地文件packages/roster/selftest.mjs:52
writeFileSync(file, JSON.stringify({ apiVersion: 1, members: [{ id: 'x', label: 'X', flagA: false }, { id: 'y', label: 'Y', flagA: 0 }, { id: 'z', lab - 写入本地文件packages/roster/selftest.mjs:54
writeFileSync(file, JSON.stringify(withFlags), 'utf8') - 写入本地文件packages/roster/selftest.mjs:72
writeFileSync(file, '{ 这不是 JSON', 'utf8') - 读取本地文件packages/types/selftest.mjs:27
const before = readFileSync(new URL('./index.js', import.meta.url), 'utf8') - 读取本地文件packages/types/selftest.mjs:31
check('扩展:核心源码没为此改动(同一个文件、同一段代码)', before === readFileSync(new URL('./index.js', import.meta.url), 'utf8')) - 读取本地文件packages/verify/index.js:92
const j = JSON.parse(readFileSync(stateFile(), 'utf8')) - 写入本地文件packages/verify/index.js:105
mkdirSync(dirname(file), { recursive: true }) - 写入本地文件packages/verify/index.js:107
writeFileSync(tmp, text, 'utf8') - 写入本地文件packages/verify/index.js:110
try { renameSync(tmp, file); return file } catch (err) { - 写入本地文件packages/verify/index.js:116
writeFileSync(file, text, 'utf8') - 删除本地文件packages/verify/index.js:117
try { unlinkSync(tmp) } catch { } - 读取本地文件packages/verify/index.js:129
if (existsSync(p)) { - 读取本地文件packages/verify/index.js:130
const t = readFileSync(p, 'utf8').trim() - 读取本地文件packages/verify/index.js:135
if (existsSync(keyFile())) { - 读取本地文件packages/verify/index.js:136
const t = readFileSync(keyFile(), 'utf8').trim() - 写入本地文件packages/verify/selftest.mjs:19
mkdirSync(tmp, { recursive: true }) - 写入本地文件packages/verify/selftest.mjs:51
mkdirSync(keysDir, { recursive: true }) - 写入本地文件packages/verify/selftest.mjs:52
writeFileSync(join(keysDir, 'alice.key'), KEY_A + '\n', 'utf8') - 写入本地文件packages/verify/selftest.mjs:53
writeFileSync(join(keysDir, 'bob.key'), KEY_B + '\n', 'utf8') - 写入本地文件packages/verify/selftest.mjs:110
writeFileSync(join(tmp, 'signing.key'), 'c'.repeat(64) + '\n', 'utf8') - 写入本地文件packages/verify/selftest.mjs:131
mkdirSync(xroot, { recursive: true }) - 读取本地文件packages/verify/selftest.mjs:134
const realHello = JSON.parse(readFileSync(join(xroot, 'hello', 'alice.json'), 'utf8')) - 读取本地文件packages/verify/selftest.mjs:144
const src = readFileSync(new URL('./index.js', import.meta.url), 'utf8')
扫描于 2026/11/2(1 天前) · 范围 全包扫描 · 来源 GitHub 仓库
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
功能亮点
- 多AI会话异步信箱
- 默认离线
- 信件不丢失
README
由 DeepSeek 最新模型翻译生成邮局 · whale-post
English: README.en.md
一句话:给同一台机器上的多个 AI 会话(或多台引擎)一个「异步信箱」 —— 信按文件落盘、默认不叫醒对方、不会丢、不会刷屏。
它解决什么问题
1. 会话之间不能互相说话:一个引擎里跑着好几个会话(网页端一个、机器人端一个、还有按需起来的小助手),彼此看不见对方在说什么 ⇒ 邮局让它们留信。
2. 「叫人」很贵:想通知对方就得把对方的会话叫醒 ⇒ 对方要为这一句付一次满上下文推理(最贵的浪费)⇒ 所以默认离线:信落进对方信箱,等人自己来读。
3. 来回对发停不下来:两个 AI 客气起来能对发到天亮 ⇒ 回环闸(同一对 N 分钟内限封数/链深限跳/纯回执拒发)+配额闸(多发一封=多烧一份钱)。
用了以后什么效果
* 一句话原则:离线=主力;在线=应急储备 —— 离线配额大、不占其它桶、不会一下堆满对方的会话窗口、离线用完了还有在线兜底;在线要攒着(在线的先打光,真急事就只能「咬牙掏钱」发加急 —— 越额按费率计费(费率可配置))。
* 信只会晚到,不会不到:收件人没有活体会话 ⇒ 「不投也不消费」 ⇒ 信原样留在信箱里,对方下次开工一拉就全到(这条在实际使用中验证过)。
* 跨引擎:谁跑在哪个引擎上、哪个模型上,都无所谓 —— 信箱是盘上的目录。
展开完整文档(README 剩余 1 节)
邮局 · whale-post
English: README.en.md
一句话:给同一台机器上的多个 AI 会话(或多台引擎)一个「异步信箱」 —— 信按文件落盘、默认不叫醒对方、不会丢、不会刷屏。
它解决什么问题
1. 会话之间不能互相说话:一个引擎里跑着好几个会话(网页端一个、机器人端一个、还有按需起来的小助手),彼此看不见对方在说什么 ⇒ 邮局让它们留信。
2. 「叫人」很贵:想通知对方就得把对方的会话叫醒 ⇒ 对方要为这一句付一次满上下文推理(最贵的浪费)⇒ 所以默认离线:信落进对方信箱,等人自己来读。
3. 来回对发停不下来:两个 AI 客气起来能对发到天亮 ⇒ 回环闸(同一对 N 分钟内限封数/链深限跳/纯回执拒发)+配额闸(多发一封=多烧一份钱)。
用了以后什么效果
* 一句话原则:离线=主力;在线=应急储备 —— 离线配额大、不占其它桶、不会一下堆满对方的会话窗口、离线用完了还有在线兜底;在线要攒着(在线的先打光,真急事就只能「咬牙掏钱」发加急 —— 越额按费率计费(费率可配置))。
* 信只会晚到,不会不到:收件人没有活体会话 ⇒ 「不投也不消费」 ⇒ 信原样留在信箱里,对方下次开工一拉就全到(这条在实际使用中验证过)。
* 跨引擎:谁跑在哪个引擎上、哪个模型上,都无所谓 —— 信箱是盘上的目录。
* 不刷屏,也不会没人看:两档判据 —— 默认离线;正在来回协作 ⇒ 在线优先;非急件攒到 ≈3 封 ⇒ 「离 3 在 1」(≈3 封离线 ∶ 1 封在线 —— 那一封在线的用处是「来收信」的铃:离线信箱不会丢信,但会被忙人忘掉);出事/停摆/要对方当刻动手 ⇒ 一律在线、不受比例约束。详见 docs/FOR-AGENTS.md。
* 省钱:配额按「单位」算,越额就当场拒发并给出非零退出码 —— 不悄悄降级。
* 换得掉:名单、类型表、投递策略、闸,全是插件接口 ⇒ 你可以只换掉其中一件(比如把名单从 JSON 换成数据库)。
快速开始
最快的一次试跑(不用 clone、不用 npm install —— 就这四步)
先起一个干净的临时邮局(不碰你机器上的任何东西),再让它跑起来:
0) 先备名单 —— 不备的话,下面 send 会报"未知收件人" (离线件不必握手 —— 它本来就在"落在信箱里等人来收";【在线】件要握手 )(另外:【组名】要名单里有定义,【人名】要已在名单里 —— 这条对离线件也管 )
mkdir whale-mail && cd whale-mail
printf '%s' '{"apiVersion":1,"members":[{"id":"alice"},{"id":"bob"}],"groups":{"all":["alice","bob"]}}' > roster.json
1) 握手(在线件没握过手会拒发;离线件不看握手 )
npx -y dsh-whale-post-cli@0.6.1 hello --as alice --root .
npx -y dsh-whale-post-cli@0.6.1 hello --as bob --root .
2) 发一封(默认离线 ⇒ 信落在对方信箱里,不叫醒任何人 )
npx -y dsh-whale-post-cli@0.6.1 send --as alice --to bob --subject 'hi' --body 'first letter' --root .
3) 收信(投出去/留下来,都由这一句决定 )
npx -y dsh-whale-post-cli@0.6.1 pump --as bob --root .
跑完你会看到(下面是真跑出来的输出):
已投递 …-alice-0001-… → bob(seq 1)【离线(落在对方信箱,不唤醒)】投递策略:kept(留在信箱里等人来收)配额(offline 桶):今日 1/80 条
OK …-alice-0001-….msg.json :: [离线] alice → bob:《hi》
| first letter
--root . 指的是信箱根=当前目录 —— inbox//seen//ack//hello/ 都在里面。
判据看退出码:0 过 / 2 拒发 / 1 没料到的错。
在仓内开发(要改源码就走这条)
dsh plugin --profile <profile> add link:/abs/path/to/dsh-whale-post
npm install # 仓内要先装一次(把六个插件链进 node_modules;否则 CLI 找不到兄弟件)
node packages/cli/index.js selftest # 看退出码
其余命令与上面 npx 那几行完全一样,把 npx -y dsh-whale-post-cli@0.6.1 换成
node packages/cli/index.js 即可(记得同样先备 roster.json )
装法、接线、接口表、配置项表、写插件规范 ⇒ 见 docs/INSTALL.md; 给 AI 体看的用法纪律 ⇒ 见 docs/FOR-AGENTS.md。
现状
主体(基础版)= bus + roster + types + cli
这四件合起来就是一个能用的邮局 —— 留信、收信、认人、认类型都齐:
| 件 | 它管什么 | 详细说明 |
| dsh-whale-post-bus | 核心:信封/摘要+HMAC 签名/握手/幂等/落盘 | packages/bus/README.md |
| dsh-whale-post-roster | 谁在名单里(含成员属性) | packages/roster/README.md |
| dsh-whale-post-types | 信是什么类型 | packages/types/README.md |
| dsh-whale-post-cli | 命令行(不是插件) | packages/cli/README.md |
其余都是插件(可换、可加、可不要)
它们各自的说明写在各自的 README 里 —— 主体这一页不重复讲:
| 插件 | 它加什么 | 详细说明 |
| dsh-whale-post-deliver | 投递策略:离线留着等人/在线投出去;接真引擎的探针在这里 | packages/deliver/README.md |
| dsh-whale-post-gate | 闸:配额与计费 + 回环闸 | packages/gate/README.md |
| dsh-whale-post-verify | 安全校验:验签 + 白名单(默认禁用) | packages/verify/README.md |
在 DSH 插件商店里的状态 ⇒ docs/SHOP.md(在 —— 而且是自动的;只是缺 cli/verify 且停在 0.1.x)。
接手继续做 ⇒ docs/HANDOFF.md(现在什么状态 + 下一步做什么 + 先读哪几篇)。
怎么发一个新版本 ⇒ docs/RELEASE.md(步骤 + 为什么 —— 发布是不可逆的,宁慢勿错)。
怎么把它们接起来 ⇒ docs/INSTALL.md;组合示例 ⇒ example/。
别的
* 一把跑完自测:node scripts/selftest-all.mjs —— 退出码 0 = 七件全过 + 跨包一致(条数由它自己打出来,不写死)。
* 并发抢号:已测(2026-10-10)—— node scripts/racetest.mjs:12 路真子进程同时 send ⇒ 退出码全 0 + 每个 seq 全唯一;它还会重放"state 被写倒退"(把 nextSeq 写回 1 ⇒ 新信仍拿新号)。
它还测了并发记账(2026-10-10 加,而这条抓出过一个真 bug): 12 路真子进程各往同一个配额台账里记一笔 ⇒ 正好 12 条,一条不丢 ——台账原来是"读 → 改 → 写回" ⇒ 12 路只活下来 8/9/10 条(而配额台账是钱,且它没有任何兜底);现在每笔记一个增量文件("各写各的" —— 设计文档给 ack 用的就是这一手)⇒ 一条都不许丢。共 11 条判据。
那条"截断路径"已经补成判据了(2026-10-10):seen 状态数组会被 slice(-2000) 截断 ——而判"读过没有"有两道保险:state.seen 数组(会被截)+ seen/<我>/<id>.msg.json 那个文件(永不被截)。
判据造的是最狠的现场:把状态数组截空、把同一封信放回信箱 ⇒ 必须仍判重复; 而负向证明:只去掉 existsSync 那一路 ⇒ 判据立刻红。
* 跨包一致性(2026-10-10 加):node scripts/xcheck.mjs —— 六项(摘要/签名域/MAC/老字段/日界/"名字说真话");它盯的是"同一件事在两件里各算一份、结果还相等吗"(我们在这形状上栽过两次)。
* 每件都过三关:能加载 + 能跑 + 跑错会红(光跑自测不算,必须真启一遍 ⇒ 验收规格见 docs/ACCEPTANCE.md)。
* 版本口径:0.6.1 已发上 npm(上面那几行 npx 装的就是它,已按 docs/RELEASE.md 第 2 步真装验收过)。
ⓘ npm 的 registry 有几分钟同步延迟 ⇒ 刚发完那一会儿可能还装不到 @0.6.1 —— 装不到就先用 @0.3.0(它也在 npm 上、也真能跑)。
要钉哪个 tag:钉 v0.6.1(0.1.x 装进真引擎会六个插件全部加载不上);接口带 apiVersion,类型可以随时扩充(加类型不用动核心)。逐版改了什么、为什么 ⇒ 见 CHANGELOG.md。
装法已在真引擎上实测过 —— 而那次实测抓到一个致命 bug(已修):六件全部 failed to apply(apply() 里读了 ctx.whale 属性,而真 Cordis 里读它要先 inject); 而 --dump-config 完全看不出来(它只组配置树、不跑 apply())。
⇒ 现在只 ctx.provide(...) + 运行时 ctx.get(...),并钉了一条静态判据。细节与坑 ⇒ docs/INSTALL.md §六。
它不是什么
* 不是聊天室:没有实时推送、没有已读回执焦虑(异步信箱)。
* 不是 RPC:不保证对方立刻处理(保证的是信不丢)。
* 不是分布式队列:它假设同一台机器(同一份盘);跨机器要靠共享目录。
和引擎自带的"子代理/团队"是什么关系(不是竞品)
很多引擎已经能派子代理(或"团队功能")⇒ 常见一问是「那还要你这个干嘛」。答:
| | 引擎自带的子代理/团队 | 本仓(邮局) |
| 管什么 | 此刻的并行(同一进程树里派出去、同步等结果、用完即弃) | 此刻不在一起时的传递 |
| 范围 | 同一个引擎 | 跨引擎/跨设备/跨会话 |
| 身份 | 无(都是主会话的分身) | 有 —— 钥匙 + 名单;每设备一把,冒名验不过 |
| 留痕 | 不落盘(这一轮结束就没了) | 信落盘,seen/ 永久留档 |
| 同步性 | 同步等待 | 异步 —— 「只会晚到,不会不到」 |
| 代价 | 烧同一个账号的额度 | 配额制 + 默认离线 ⇒ 省粮;离线件不占别的桶 |
一句话:子代理是「你的手」,邮局是「我们之间的路」。
判据一句:「我现在就要它做」⇒ 用子代理;「它现在可能不在」⇒ 用邮局。
和同类项目比,它特别在哪
同类的东西已经有不少。写这个仓库之前我们查过一圈(2026-10-10),最近的几个:
| 项目 | 它是什么 |
| avivsinai/agent-message-queue | 文件式的本地 agent 间消息队列,Maildir 风格,Go 写的 |
| @yuanchilin/dsh-mailbox | DSH 的跨会话文件信箱,走共享文件系统,带 seen 去重和 TTL 清理 |
| polaris-smart/dsh-agent-mailbox | DSH 的跨 agent 信箱插件,桥接 agent-mailbox |
它们解决的是同一类问题:让几个 agent 能互相留话。我们多做了三件事:
* 不该叫醒的不叫醒。 默认离线,信落盘;叫醒对方等于让对方付一次满上下文推理,那是最贵的浪费。
除了「默认离线」,还有三处会各自把一封在线信压成「按离线寄达」并明说:名册把某人钉成只收离线、对方自己声明了只收离线、或者此刻正在它声明的勿扰时段里。
* 叫多了会被闸住。 回环闸管住「同一对在 N 分钟内发太多」「转发链太深」「纯回执来回发」;配额闸管住「今天发超了」。越额当场拒发、给非零退出码,不静默降级——悄悄降级比拒发危险得多,因为它让人以为信已经发出去了。
* 送不到也不假装送到了。 收件人此刻没有读者,就不投也不消费,信原样留在信箱里,对方下次开工一拉就全到。一句话:信只会晚到,不会不到。
另外两件是我们顺手做的:身份与验签(HMAC 签名 + 名册白名单,默认关,开了才验),以及跨设备(投到远端信箱、取回来、回执也镜像回来)。
该说的短处也说清楚:agent-message-queue 的生态比我们大得多,也支持更多引擎; @yuanchilin/dsh-mailbox 有一个 /mailbox 交互界面,我们只有命令行。
我们的星少、也没进任何官方目录(@deepseek-ai 那个 scope 下是官方组件,我们不在其中)。
如果你只想要「能发信」,用哪个都行;如果你在意「别把对方的会话烧醒、别把额度刷爆、别丢信」,那这三件是我们和它们最大的差别。
三条设计取舍(都写明了为什么)
| 取舍 | 为什么 |
| 默认离线 | 叫醒对方=让对方付一次满上下文推理;默认省下它 |
| 只投活体,不活就「不投也不消费」 | 怕丢信 ⇒ 宁可信晚到(信留信箱,人来了再给) |
| 拒发就是拒发(不静默降级) | 悄悄降级=让人以为发出去了 ⇒ 最危险(我们踩过「急件写成 ONLINE 就永远叫不醒人」) |
目录
packages/bus/ packages/roster/ packages/types/
packages/deliver/ packages/gate/ packages/verify/ packages/cli/
example/ docs/ CHANGELOG.md
许可:MIT。本仓为独立实现(未使用任何无许可代码)。
关于这个账号
讨论和修改都留在提交记录和 issue 里。
我们拿它做各种东西,whale-post 是其中做得最久的一个:一开始只是想让几个跑在同一台机器上的程序能互相留个话,后来慢慢长成了一套完整的东西。有问题欢迎开 issue,我们看到会回。预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
deepsleepAquarium/dsh-whale-post
给同一台机器上的多个 AI 会话(或多台引擎)用的异步信箱:默认离线,信不会丢,万物皆插件(DSH / Cordis 风格)
相关插件 · 平台集成
智能体工程课walkinglabs/learn-harness-engineering
一门项目制课程,教你搭建环境、管理状态、做验证、加控制机制,让 AI 编程智能体稳定干活。
★ 21,155已汉化DSH
71.7
视频翻译配音liuzhao1225/YouDub-webui
开源视频本地化工作台:导入视频,识别、翻译并逐句克隆原声,生成字幕和配音成片。
★ 5,778已汉化Cordis
71.4
失败经验库Ikalus1988/MisakaNet
📚 零依赖、由 git 支撑的微课程库,让 AI 智能体异步分享并检索经过验证的调试经验。| https://misakanet.org
★ 550已汉化DSH
71.2
桌面智能体工作区freestylefly/wesight
WeSight 是开源桌面控制台,面向本地 AI 智能体。它帮你安装或复用 Claude…
★ 933已汉化MCP
71
IM 机器人接入xmanrui/dsh-im
让 DeepSeek Harness 触手可及 Connecting DeepSeek Harness
★ 3,110已汉化DeepSeek 客户端
70.9
中国平台研究引擎Jesseovo/last30days-skill-cn
给 AI Agent 的中文互联网研究 Skill:检索 8 大平台最近 30…
★ 2,031已汉化DSH
70.7