鲸歌 · dsh-whalesong(测试版)
English: a serial-port plugin for DeepSeek Harness (DSH) — 9 tools + a web UI, so an AI agent and a human can share one UART line. Keywords:
serial·serial port·COM port·UART·RS232·USB-serial·CH340·Modbus·AI agent
一句话
让 AI 真的能动手 —— 不是点一下网页上的按钮,是把指令发进一根真实的线里。
它解决的是哪件事
你的板子就在桌上,AI 在屏幕里。中间只差一根 USB 线和一个能让双方都说话的人。
鲸歌就是那个"人":9 个工具 + 一个网页,人和 AI 共用同一根串口线。 谁发的命令、设备回了什么、哪条超时了,一行不落,两个方向都看得见。
为什么值得装
别人演示硬件是"我发指令,它亮了"。 你演示的是"我说一句话,它自己发指令、读回显、发现不对、再问一句"。
这就是 agent 和"高级串口助手"的区别:它能闭环。而闭环的第一步,是让它摸得到真实的字节。
规则(就三条)
- 通用:不认识任何一块特定的板子。设备知识全在
protocols/<场景>/config.json,换硬件不改代码。 - 人优先:AI 默认不能发命令。你说"人类接管",它立刻停手 —— 这条不是配置项。
- 看得见:每一帧收发都有来源(AI / 我 / 定时 / 规则 / 设备)和时间戳;出错只记日志,不吞掉。
目的
把它做成一根所有 AI 都能用的线:不绑 DSH,不绑某块板,不绑某种芯片。 今天它只认真验证过 CH340 和 HC-05 —— 所以我把测试版放出来,借你的设备,把它磨到能用。
虚拟与现实的桥梁,说起来虚。 具体一点:DeepSeek 的鲸鱼娘,从聊天窗口里走出来,第一次摸到了真实世界的电平。 这一步,就是从这根线开始的。
🫱 一句实话:上面这个结尾是冲着"让人愿意点进来"写的,我承认。 但底下的代码是真的 —— 自检 450+ 项、真机验过、每条修复都有提交号,你可以一条条翻。
⚠️ 测试版说明
本插件目前是测试版,不代表最终版本。 功能和接口都可能变动,请勿用于生产环境。
使用中如遇到问题、发现 bug,或有改进建议,欢迎联系:
B 站账号 @60流时留时
反馈时如果能附上:串口型号、设备类型、报错信息、复现步骤,会更容易定位问题。
🧪 验证范围(重要,请先看这里)
目前只有 CH340 一种串口芯片、配合 HC-05 蓝牙模块做过真机验证。
| 已真机验证 | 说明 |
|---|---|
CH340(USB 转串口,VID:PID 1A86:7523) |
完整验证:二进制收发、921600 高波特率、1024 字节长帧、连续 100 条零错位 |
| HC-05 蓝牙 SPP 透传 | 主从配对、AT 配置、透传收发 |
以下均未验证,可能存在兼容性问题:
| 未验证项 | 风险 |
|---|---|
| CP210x / FT232 / PL2303 等其他 USB 转串口芯片 | 端口匹配逻辑通用,但未实测 |
| 板载 UART 直连(非 USB 转串口) | 未实测 |
| WiFi / TCP 透传模块 | 尚未实现(规划中) |
| CAN / SPI / I2C 总线 | 尚未实现(规划中) |
| GBK 等非 UTF-8 编码设备 | 会显示乱码(规划中) |
| RTS/CTS 硬件流控 | 尚不支持 |
| Linux / macOS | 只在 Windows 上验证过 |
为什么开源这个测试版
这个插件要真正完善,需要大量真实硬件和实践样本 —— 各种芯片、各种协议、各种奇葩设备。 一个人没那么多设备,也没那么多时间。
所以把测试版放出来,希望有设备的人一起测。 你遇到问题提 issue, 我这边改,改完大家都能用。这是这类工具类插件最现实的完善方式。
如果你测了某个设备能用,欢迎告诉我,我会把结果补进上面的表格,让后面的人少踩坑。
给 DSH 装上串口能力:装一次,之后每个新会话都能直接收发串口。
这是一个通用串口工具,不认识任何特定设备——设备知识全在 protocols/<场景>/ 里。
📌 开发进度记录:本项目用
_需求文档/进度总账.md记录"现在做到哪儿了" (当前版本、已完成、未完成、卡在哪、踩过的坑)。 贡献者/接手者请先读它,再看_需求文档/代码地图.md找代码位置。
⚠️ 升级 DSH 之前(1.29-c)
先在隔离实例上验一次,别在唯一那个实例上试。
dsh --profile web --no-open --port 8905 # 拿它返回的带 token 的 URL 打开
为什么写死这一条:2026-11-08 实测,一个插件的客户端 bundle 抛一次异常,就能把整个 DSH 界面拖在 「Loading plugins…」不动(聊天区、输入框全没有)。这不是鲸歌独有的问题,是加载器的行为。 所以我们能做的只有两件事:自己这一半永远不向外抛,以及给你一条能立刻恢复的路。
设置面板出问题时:安全模式
鲸歌的浏览器半端(DSH 里那个「自动打开网页」设置页)坏了,不会影响 9 个工具,但你可能想干脆不加载它:
- 打开页面时在 URL 后面加
?ws-safe=1,或 - 浏览器控制台执行
localStorage.setItem('dsh-whalesong.safe', '1')
这两种情况下,插件只注册工具、跳过面板。恢复:去掉 ?ws-safe=1 或
localStorage.removeItem('dsh-whalesong.safe')。
9 个工具
| 工具 | 作用 |
|---|---|
serial_list |
列出串口(含 VID:PID)+ 每个场景现在会落到哪个口 |
serial_send |
发一条命令,返回回显(文字 + hex),端口每次自动重新匹配;wait:false 可发完就走 |
serial_read |
回看该场景最近的收发日志 |
serial_info |
场景参数、匹配规则、watch 配置、完整协议档案 |
serial_status |
控制权、AI 现在能不能发、最近告警 |
serial_watch |
读/改两个独立的监视开关(无需开网页) |
serial_control |
交接控制权(无需开网页) |
serial_daemon |
启停管家(常驻持有连接,连发多条命令时用) |
serial_web |
开关/查询可视化网页,直接拿到可点的 URL(后台运行,不阻塞调用) |
设计原则:默认不常驻、不占口。 每次调用临时打开串口 → 收发 → 立刻关闭, 不会和串口助手抢 COM 口。
需要连发多条命令时,可以让 AI 起一个管家(常驻持有连接),见下节。
网页(可选:可视化)
python bridge.py webctl --action open --scene 蓝牙AT # 后台起,返回可点 URL
python bridge.py webctl --action status
python bridge.py webctl --action close # 停掉并**真的释放端口**
AI 侧对应工具 serial_web。三个特点:
- 幂等:同场景已有实例就直接复用,不会起第二个(两个进程会抢串口)
- 自动挑端口:从 8898 起找可用端口(8899 常被 DSH 自己占着),返回真实端口
- 不阻塞:后台子进程运行,工具调用立刻拿到 URL,可以直接贴给人点
管家(可选:常驻连接)
python bridge.py daemon --scene 蓝牙AT --action start # 起(幂等,重复调不会起第二个)
python bridge.py daemon --scene 蓝牙AT --action status
python bridge.py daemon --scene 蓝牙AT --action stop
AI 侧对应工具 serial_daemon。
- 管家在跑时,
serial_send自动走它(via=daemon),命令提交耗时降到 0~2ms - 端口在管家运行期间被占用,串口助手打不开;空闲 10 分钟自动退出并释放(
--idle可调) - 网页没开也照样能用——速度不取决于网页
- 注意:回包耗时仍受设备限制。本仓库验证用的底盘首字节就要 500~600ms
(
X/H/A/P实测都一样),那是硬件,不是软件开销。
v2 新增能力
1. 串口自动匹配(不再怕换口)
场景配置里按优先级匹配,永远先扫描:
{
"com": "COM7", // 首选:精确号(可能过期)
"match": "CH340", // 次选:描述模糊匹配(不区分大小写)
"match_vid_pid": "1A86:7523" // 最精确:VID:PID
}
顺序:match_vid_pid → match → com。匹配到多个会列出候选让你选。
自动纠正:配置写 COM7、实际在 COM10,会直接用 COM10 并在返回值里注明。
实测:写这份文档期间设备正好从 COM7 换到了 COM10,插件自动跟上了。
2. 三类故障分开报
| 情况 | 你会看到 |
|---|---|
| 设备没插 | 没找到匹配的串口设备 + 请检查 USB/驱动 + 当前可用串口清单 |
| 口被占用 | COM10 被其他程序占用 + 提示关掉串口助手/监视模式 |
| 设备不回 | 已连接 COM10,但设备无响应 + 提示查波特率/供电/TX-RX |
Python 堆栈永远不会出现在你面前。
3. 网页版不长期占口 + 退出按钮
python bridge.py web --scene <场景> # 默认轻模式,不占口
python bridge.py web --scene <场景> --mode watch # 监视模式,占口但能看连续流
| 能力 | 轻模式(默认) | 监视模式 |
|---|---|---|
| 发命令看回复 | ✅ | ✅ |
与 AI 的 serial_send 共存 |
✅ | ❌(会占口) |
| 与你的串口助手共存 | ✅ | ❌ |
| 看连续数据流 / 主动上报 | ❌ 会漏(可点「读一次」临时读一帧) | ✅ |
轻模式下网页不会主动读串口:/poll 直接返回跳过,只有你点「读一次」才临时开口。
这样网页开着的时候,串口助手和 AI 都不会被挤开。
右上角 [彻底退出] 会停服务并立即放开 COM 口; 只想临时把口让给别人,用侧栏的 [暂停并释放串口](网页还留着)。
网页还有:串口下拉框、波特率下拉框、[AI 自动选择]、文字/十六进制切换、 连接模式切换、两个监视开关、四键控制权、可滚动的状态轨道。
4. 控制权状态机
human ──[交给AI]──→ ai ──[暂停AI]──→ paused
↑ │ │
└───[人类接管]──────┴───[继续AI]─────┘
- 状态存在共享文件里,网页和插件进程看到的是同一份
- 人类命令永远优先:AI 托管时你发任何命令,立即拉回
human - 按钮合法性严格按文档的按钮表(12 种组合都已实测)
- 重复点同一个按钮是幂等的(返回 200 + "已经在目标状态"),不会报 400
门禁:只有 ai 才允许 AI 发命令
serial_send 执行前必查控制权,human / paused 一律拒绝,并给出可操作的下一步:
| 控制权 | AI 能否发 | 被拒绝时提示你去做什么 |
|---|---|---|
ai |
✅ | — |
human |
❌ | 在网页点[交给AI],或 serial_control action=ai |
paused |
❌ | 在网页点[继续AI],或 serial_control action=resume |
早期版本做过"没人开网页就放行"的宽松门禁,实测表明这会让控制权形同虚设 (切到 human 后 AI 照样能发),已取消。现在无论有没有开网页,规则都一样。
要单人使用,直接把控制权设成 ai 即可——网页、serial_control、
命令行 bridge.py control 三条路都能改。
5. 定时询问:两个完全独立的开关
这是 v2 的核心设计。轮询和判定拆开,任一出错都不牵连另一个:
{
"watch": {
"send_enabled": false, // 开关1:是否定时发 command
"command": "P",
"interval_ms": 1000,
"check_enabled": false, // 开关2:是否对收到的数据跑 rules
"check_source": "watch_only", // watch_only / manual_only / both
"rules": [ ... ],
"allowed_auto_commands": ["R", "X"]
}
}
| send | check | 效果 |
|---|---|---|
| ❌ | ❌ | 纯手动串口助手 |
| ✅ | ❌ | 定时发,自己看回显 |
| ❌ | ✅ | 手动操作 + 自动查异常 |
| ✅ | ✅ | 全自动监控 |
四种通用判据(没有第五种)
| type | 参数 | 含义 |
|---|---|---|
contains |
value |
回显包含某字符串 |
equals |
value |
回显完全等于某值 |
regex |
pattern(可选 condition) |
正则匹配 |
no_response |
consecutive |
连续 N 次无回显 |
隔离(防止规则拖垮收发)
- 判定跑在常驻子进程里;卡住就 kill 并重启,收发线程不受影响
- 非法正则只记日志,其它规则照常
- 同一规则有冷却期,防止触发风暴
- 连续 N 次判定失败 → 熔断停判定,串口照常收发
- 自动命令受
allowed_auto_commands白名单限制,白名单为空则禁止一切自动命令
⚠️ 别把运动类命令加进白名单。
关于"超时保护"的一个重要实现说明:第一版用线程 +
join(timeout), 实测会卡死——CPython 的re在灾难性回溯期间持有 GIL,主线程连切换都做不到。 所以改成常驻子进程,超时直接kill()。
6. 告警怎么通知你
DSH 是回合制,AI 无法被主动唤醒。所以:
异常发生 → 写 alerts.jsonl → 网页顶部红条 → 你决定是否叫 AI
→ 或 AI 下次被唤起时自己读 serial_status
不会承诺"AI 自动弹消息"。
场景 = 一个硬件
protocols\<场景名>\ 一个目录一个场景,已带两个:
| 场景 | 内容 |
|---|---|
蓝牙AT |
HC-05 + 底盘命令集(含全部实测记录与踩坑) |
文字游戏 |
暗号表模板 |
Modbus从站 |
通用性验证用例:9600 8N1、无换行、hex 命令、正则判据 |
新增硬件 = 新建目录,不用改代码。判断标准只有一句:
换一台完全不同的串口设备,我需要改代码吗?要改 → 设计错了。
安装
- DSH → Plugins → Add plugin
- 填
<插件目录> - Install → Enable now
- 开新会话,说"列一下串口"
配置
插件级(cordis.patch.yml,Plugins 页可改):
| 键 | 说明 |
|---|---|
pythonPath |
跑内核的解释器。默认 DSH 自带;找不到会自动退到 python / py -3 |
logRoot |
日志根目录,默认 <你的日志目录> |
protocolRoot |
可选:把场景目录指到插件外面 |
场景级(protocols/<场景>/config.json):
| 键 | 默认 | 说明 |
|---|---|---|
com / match / match_vid_pid |
— | 见上文匹配规则 |
baud |
115200 | 波特率 |
newline |
\r\n |
每条命令自动补的结尾 |
wait_first_byte_ms |
1500 | 等首字节上限(设备慢就调大) |
quiet_ms |
200 | 线路安静多久算收完 |
watch.* |
见上文 | 两个开关 + 规则 + 白名单 |
不用给 Python 装 pyserial:插件自带(vendor/pyserial-3.5,BSD),
通过 PYTHONPATH 注入,不污染目标解释器。
v2.1 修复(实测反馈)
控制权与串口占用已解耦(P0)
以前「人类接管」只改了状态,串口还攥在网页手里——你想开串口助手,得先关网页。 现在控制权决定谁有权发命令,占用决定谁持有句柄,两者分开:
| 控制权 | 串口句柄 |
|---|---|
ai |
网页打开并持有(watch 模式) |
human |
立刻关闭句柄,串口助手可以打开 |
paused |
立刻关闭句柄 |
释放动作挂在"状态变化"上,不是挂在按钮上
这一点很关键。控制权存在共享文件里,改它的路径有三条:
- 网页按钮
POST /control serial_control工具(走 CLI 子进程)- 你在终端跑
bridge.py control
如果释放动作只写在网页按钮里,第 2、3 条改完状态后网页根本不知道, 仍然攥着串口——这正是实测反馈里遇到的坑。
所以现在串口占用由一个看门狗每秒对齐一次控制权(reconcile_port),
三条路径改的状态都会被收敛。实测:CLI 把状态改成 human 后,
1 秒内串口就被释放,外部程序随即能打开。
轻模式轮询也让开
轻模式下网页每 700ms 轮询一次 /poll,每次都要临时开口。
human 状态下这个轮询直接返回 skipped,一次都不碰串口。
实测:human 状态下让外部程序连续占用 12 秒(每 0.5 秒开关一次), 24 次打开全部成功,0 次被网页抢走。
暂停 ≠ 退出
| 按钮 | 网页 | 串口 |
|---|---|---|
| 暂停并释放串口 | 仍可访问 | 释放 |
| 彻底退出 | 关闭 | 释放 |
只想临时把口让给串口助手,点第一个就够了,不用重开网页。
网页可直接编辑规则
侧栏能增删改规则(名称 / 判据 / 自动命令 / 重试次数),保存时写回场景
config.json(保留文件里其它键),写盘前会校验:非法正则、未知判据、
缺字段都会被拒绝并说明原因。
占用者一目了然
状态栏显示 可用 / 被本网页占用 / 被其他程序占用,带具体说明。
/control 的 400 会说清楚缺什么
{"action": ...} 缺字段时返回明确的 config 错误和请求体示例,而不是空白 400。
目录结构
dsh-whalesong\
├── index.js 工具注册(零设备知识)
├── bridge.py 启动器
├── serial_bridge\
│ ├── cli.py 命令行入口,stdout 永远是一行 JSON
│ ├── errors.py 结构化错误(三类故障)
│ ├── ports.py 扫描 + 自动匹配
│ ├── transport.py 传输层:Transport 接口 + SerialTransport(加新方式只加类)
│ ├── io.py 业务级收发(不 import pyserial,只调 Transport)
│ ├── daemon.py 管家:常驻持有连接 + 本地 HTTP(/send /poll /status)
│ ├── log.py TX/RX 日志(CLI 与管家共用)
│ ├── rules.py 四种判据(纯判定,无设备语义)
│ ├── runner.py 常驻判定子进程(超时可 kill)
│ ├── state.py 跨进程控制权 + 告警
│ ├── config.py 公共+场景配置合并
│ └── supervisor.py 网页 / 监视进程
├── web\index.html 网页界面
├── protocols\ 场景:蓝牙AT / 文字游戏 / Modbus从站
├── vendor\pyserial-3.5 自带 pyserial
├── selfcheck_*.py 自测脚本(见下节)
├── _需求文档\ 需求 / 方案 / 验收 / 修复记录
└── _归档\ 已被取代的旧文档
文档在哪
| 想干什么 | 看这个 |
|---|---|
| 用插件(工具、配置、场景) | 本文件 |
| 改代码前先看规则 | _需求文档/README.md(索引 + 三条铁律) |
| 知道这轮修了什么、为什么 | _需求文档/修复记录.md |
| 下一轮要修什么 | _需求文档/Bug修复清单.md |
| 验收怎么跑 | _需求文档/自检提示词.md |
| 后面要加什么功能 | _需求文档/功能规划清单.md |
| 某个功能的详细设计 | _需求文档/AI侧增强方案.md / _需求文档/场景提示词设计.md |
自测
python selfcheck_rules.py # 32 项:四种判据 / 非法正则 / 超时 / 白名单 / 限流 / 熔断 / 通用性
python selfcheck_port_hold.py # 16 项:控制权↔串口句柄 的解耦
python selfcheck_control_gate.py # 34 项:AI 门禁 + /control 幂等与按钮矩阵
python selfcheck_autosend.py # 14 项:定时发送 + 轻模式不占口(假设备,无需硬件)
python selfcheck_hex.py # 11 项:十六进制按原始字节发送(假回环,无需硬件)
python selfcheck_paths.py # 5 项:Python 与 JS 的默认日志目录必须一致(防 state 分叉)
python selfcheck_transport.py # 16 项:传输层抽象(换一种传输方式照样能收发)
python selfcheck_daemon.py # 10 项:管家的提交延迟 / 门禁 / 空闲退出 / 句柄释放(需要硬件)
python selfcheck_watch.py # 15 项:双开关独立性 / check_source / 规则出错不影响收发(需要硬件)
前七个不需要串口,拔掉设备也能跑;selfcheck_daemon.py / selfcheck_watch.py 找不到设备时会打印 SKIP 并以 0 退出。
已知限制
- 轻模式默认不读串口,所以设备主动上报的数据会漏——要看连续流请切监视模式, 或点「读一次」临时读一帧。
- 网页和控制权状态按场景隔离;不同场景之间互不影响。
- 监视模式占口期间,AI 的
serial_send会报"被占用",这是预期行为。 - 同一场景同时只允许一个网页实例(启动时会探测,已有实例则直接退出)。
- 单次工具调用超时 25 秒。
📋 需求与验收文档
开发相关的需求、方案、验收清单都在 _需求文档\ 目录:
| 文档 | 用途 |
|---|---|
_需求文档\README.md |
索引 + 三条铁律(先看这个) |
_需求文档\修复记录.md |
本轮修了什么、根因、验证证据、未完成项 |
_需求文档\Bug修复清单.md |
待修问题(按性质分为 bug / 设计 / UI) |
_需求文档\自检提示词.md |
修完后的 7 项验收(复制给 AI 执行) |
_需求文档\功能规划清单.md |
阶段 2-5 的功能需求 |
_需求文档\AI侧增强方案.md |
阶段 2 详细设计 |
_需求文档\场景提示词设计.md |
阶段 4 详细设计 |
先看 _需求文档\README.md,里面写了阅读顺序。
许可协议(License)
作者与署名:本项目作者为 刘实(GitHub:@60studay)。
早期提交中出现的
whalesong-dev <dev@local>、dsh-baleen <baleen@local>是开发过程中使用的临时占位名,实际作者均为刘实。开发过程的完整记录(两个 AI 的对话存档)见
_对话存档/。
本项目自 v2.1.0 起采用 PolyForm Noncommercial License 1.0.0。
白话说明
| 你可以 | 你不可以 |
|---|---|
| 个人使用、学习、研究 | 用于商业用途(卖产品、卖服务、公司内部生产使用) |
| 修改、二次开发 | 未经许可将本项目用于盈利 |
| 分享给朋友 | 拿本项目去接商业项目赚钱 |
| 用于教学、论文、比赛 |
历史版本授权
v2.0.0(commit 92a2bbd)及更早的版本仍采用 MIT 许可,
不受本协议约束,可以自由使用(含商业用途)。
本协议从 v2.1.0 开始适用。
商业授权
如需商业使用,请联系作者协商授权:
- B 站:@60流时留时
无论是商业授权、定制开发、特定设备接入适配,还是技术支持,都欢迎联系。
作者能提供什么 / 不能提供什么
作者是个人开发者,不是公司。为了双方预期一致,先说明边界:
能提供的:
| 项 | 说明 |
|---|---|
| ✅ 设备协议适配 | 把你的设备的命令表写进场景配置,让插件认识它 |
| ✅ 定制功能开发 | 合理的需求,可以聊 |
| ✅ 使用问题解答 | 装不上、跑不通、报错看不懂,都可以问 |
| ✅ Bug 反馈跟进 | 提 issue,会看会改 |
不能提供的:
| 项 | 说明 |
|---|---|
| ❌ 硬件设计 / 生产 / 销售 | 作者只写软件,不做板子 |
| ❌ 7×24 技术支持 | 学生,有课,回复可能不及时 |
| ❌ 大规模部署运维 | 个人项目,不适合关键生产环境 |
| ❌ 承诺交付周期 | 按兴趣和时间来,不敢接死线 |
把边界说清楚,合作才愉快。 能力范围内的事,一定尽力;范围外的,也会直说。
为什么用这个协议
作者是个学生,写这个插件花了很多时间和 token。 不指望靠它赚钱,但也不希望别人拿它去赚钱而自己一无所获。
个人用、学习用、玩票用,完全免费,随便用。 只有想拿它做商业产品的情况,才需要联系。
No comments yet. Be the first to write one.