🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

手机端代理遥控 0x5446/rowel

DeepSeek Harness
0x544615 天前有提交已提供中文文档扫描:含敏感能力

专为 DeepSeek Harness 打造的 iPhone 应用。

37.8/100
综合分
3
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/10/21 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

自动兼容检查未通过:缺少 main/exports/bin 入口声明;仓库 package.json 标记 private,未发布到 npm,需从源码安装。以下命令仍可尝试,但可能失败。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add 0x5446/rowel
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:需留意
是什么
dsh 原生插件 · ui
装得上吗
作者未把包发布到 npm,只能用源码安装
插件本身没问题,是分发方式不同
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 15 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查需源码安装

缺少 main/exports/bin 入口声明;仓库 package.json 标记 private,未发布到 npm,需从源码安装

展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✗npm 包rowel-workspace(未发布到 npm,仅可源码安装)
✓Node 引擎要求 >=22 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✗入口文件缺少入口声明

源码安装时若 pnpm 拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑。

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/11/4 08:25:07

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 85 处,展示前 85 条)
  • 写入本地文件
    bridle/src/cli.ts:563
    writeFileSync(target, exportIdentity(state, passphrase, deviceId), { mode: 0o600 })
  • 读取本地文件
    bridle/src/cli.ts:590
    archive = readFileSync(source, 'utf8')
  • 读取本地文件
    bridle/src/cli.ts:650
    for (const entry of readdirSync(dir, { withFileTypes: true })) {
  • 读取本地文件
    bridle/src/cli.ts:653
    else if (entry.name.endsWith('.js')) newest = Math.max(newest, statSync(full).mtimeMs)
  • 读取本地文件
    bridle/src/cli.ts:775
    statSync(modulePath)
  • 删除本地文件递归
    bridle/src/cli.ts:880
    rmSync(join(home, entry), { recursive: true, force: true })
  • 发起网络请求
    bridle/src/cli.ts:898
    const response = await fetch(new URL('/healthz', toHttpUrl(state.relayUrl)), { signal: AbortSignal.timeout(5_000) })
  • 写入本地文件
    bridle/src/identity.ts:172
    mkdirSync(home, { recursive: true, mode: 0o700 })
  • 读取本地文件
    bridle/src/identity.ts:176
    raw = readFileSync(path, 'utf8')
  • 读取本地文件
    bridle/src/identity.ts:322
    return JSON.parse(readFileSync(statePath(), 'utf8')) as BridleState
  • 写入本地文件
    bridle/src/identity.ts:348
    mkdirSync(rowelHome(), { recursive: true, mode: 0o700 })
  • 写入本地文件
    bridle/src/identity.ts:353
    writeFileSync(lock, String(process.pid), { flag: 'wx', mode: 0o600 })
  • 读取本地文件
    bridle/src/identity.ts:368
    if (readFileSync(lock, 'utf8') === String(process.pid)) unlinkSync(lock)
  • 删除本地文件
    bridle/src/identity.ts:368
    if (readFileSync(lock, 'utf8') === String(process.pid)) unlinkSync(lock)
  • 读取本地文件
    bridle/src/identity.ts:395
    holder = readFileSync(lock, 'utf8')
  • 读取本地文件
    bridle/src/identity.ts:396
    age = Date.now() - statSync(lock).mtimeMs
  • 读取本地文件
    bridle/src/identity.ts:402
    if (readFileSync(lock, 'utf8') === holder) unlinkSync(lock)
  • 删除本地文件
    bridle/src/identity.ts:402
    if (readFileSync(lock, 'utf8') === holder) unlinkSync(lock)
  • 写入本地文件
    bridle/src/identity.ts:412
    writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 })
  • 写入本地文件
    bridle/src/identity.ts:413
    renameSync(temporary, path)
  • 写入本地文件
    bridle/src/identity.ts:414
    chmodSync(path, 0o600)
  • 写入本地文件
    bridle/src/instances.ts:70
    mkdirSync(join(path, '..'), { recursive: true, mode: 0o700 })
  • 写入本地文件
    bridle/src/instances.ts:72
    writeFileSync(temporary, `${JSON.stringify(homes, null, 2)}\n`, { mode: 0o600 })
  • 写入本地文件
    bridle/src/instances.ts:73
    renameSync(temporary, path)
  • 读取本地文件
    bridle/src/instances.ts:103
    const parsed: unknown = JSON.parse(readFileSync(indexPath(), 'utf8'))
  • 读取本地文件
    bridle/src/instances.ts:114
    state = JSON.parse(readFileSync(join(home, 'bridle.json'), 'utf8')) as typeof state
  • 读取本地文件
    bridle/src/instances.ts:147
    info = JSON.parse(readFileSync(join(home, 'runtime.json'), 'utf8')) as RuntimeInfo
  • 读取本地文件
    bridle/src/instances.ts:161
    return existsSync(join(home, 'bridle.json'))
  • 发起网络请求
    bridle/src/pair.ts:75
    const response = await fetch(new URL('/v1/pair/offer', toHttpUrl(state.relayUrl)), {
  • 写入本地文件
    bridle/src/runtime.ts:48
    mkdirSync(rowelHome(), { recursive: true, mode: 0o700 })
  • 写入本地文件
    bridle/src/runtime.ts:51
    writeFileSync(temporary, `${JSON.stringify(info, null, 2)}\n`, { mode: 0o600 })
  • 写入本地文件
    bridle/src/runtime.ts:52
    renameSync(temporary, path)
  • 删除本地文件
    bridle/src/runtime.ts:58
    rmSync(runtimePath())
  • 读取本地文件
    bridle/src/runtime.ts:96
    info = JSON.parse(readFileSync(runtimePath(), 'utf8')) as RuntimeInfo
  • 写入本地文件
    bridle/src/service.ts:57
    mkdirSync(rowelHome(), { recursive: true, mode: 0o700 })
  • 写入本地文件
    bridle/src/service.ts:60
    mkdirSync(join(homedir(), 'Library', 'LaunchAgents'), { recursive: true })
  • 写入本地文件
    bridle/src/service.ts:61
    writeFileSync(path, launchdPlist(command, args, log), { mode: 0o644 })
  • 执行外部命令固定命令
    bridle/src/service.ts:63
    execFileSync('launchctl', ['load', '-w', path], { stdio: 'ignore' })
  • 写入本地文件
    bridle/src/service.ts:68
    mkdirSync(join(homedir(), '.config', 'systemd', 'user'), { recursive: true })
  • 写入本地文件
    bridle/src/service.ts:69
    writeFileSync(path, systemdUnit(command, args), { mode: 0o644 })
  • 执行外部命令固定命令
    bridle/src/service.ts:70
    execFileSync('systemctl', ['--user', 'daemon-reload'], { stdio: 'ignore' })
  • 执行外部命令固定命令
    bridle/src/service.ts:71
    execFileSync('systemctl', ['--user', 'enable', '--now', `${SERVICE_LABEL}.service`], { stdio: 'ignore' })
  • 删除本地文件
    bridle/src/service.ts:86
    rmSync(path, { force: true })
  • 删除本地文件
    bridle/src/service.ts:92
    rmSync(path, { force: true })
  • 执行外部命令来自配置/变量
    bridle/src/service.ts:101
    execFileSync(command, args, { stdio: 'ignore' })
  • 读取本地文件
    bridle/src/version.ts:18
    const manifest = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8')) as { version?: string }
  • 读取本地文件
    dsh-plugin/src/index.ts:73
    const manifest = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8')) as { name?: unknown; version?: unknown }
  • 写入本地文件
    e2e/src/dsh.ts:81
    mkdirSync(userHome, { recursive: true })
  • 读取本地文件
    e2e/src/dsh.ts:84
    ? modelRows(readFileSync(settings, 'utf8'))
  • 写入本地文件
    e2e/src/dsh.ts:91
    mkdirSync(join(home, 'profiles', 'web'), { recursive: true })
  • 写入本地文件
    e2e/src/dsh.ts:92
    writeFileSync(join(home, 'profiles', 'web', 'cordis.patch.yml'), patch)
  • 执行外部命令来自配置/变量
    e2e/src/dsh.ts:99
    const child = spawn(bin, ['web', '--host', '127.0.0.1', '--port', String(port), '--no-open'], {
  • 删除本地文件递归
    e2e/src/dsh.ts:171
    rmSync(home, { recursive: true, force: true })
  • 删除本地文件递归
    e2e/src/stack.ts:153
    rmSync(home, { recursive: true, force: true })
  • 读取本地文件
    ios/asc.mjs:59
    { key: readFileSync(KEY_PATH, 'utf8'), dsaEncoding: 'ieee-p1363' })
  • 请求外部主机api.appstoreconnect.apple.com
    ios/asc.mjs:71
    const response = await fetch(`https://api.appstoreconnect.apple.com${path}`, {
  • 发起网络请求
    ios/asc.mjs:71
    const response = await fetch(`https://api.appstoreconnect.apple.com${path}`, {
  • 读取本地文件
    ios/asc.mjs:263
    bytes = readFileSync(path)
  • 发起网络请求
    ios/asc.mjs:301
    const put = await fetch(operation.url, {
  • 读取本地文件
    marketing/video/render.mjs:28
    const beats = JSON.parse(readFileSync(join(raw, 'beats.json'), 'utf8'))
  • 执行外部命令来自配置/变量
    marketing/video/render.mjs:48
    const run = (file, args) => execFileSync(file, args, { stdio: 'inherit', cwd: here })
  • 写入本地文件
    marketing/video/render.mjs:64
    mkdirSync(join(here, 'public'), { recursive: true })
  • 写入本地文件
    marketing/video/render.mjs:101
    writeFileSync(list, parts.map(p => `file '${p}'`).join('\n') + '\n')
  • 删除本地文件
    marketing/video/render.mjs:102
    rmSync(join(here, 'public/phone.mp4'), { force: true })
  • 删除本地文件
    marketing/video/render.mjs:107
    for (const part of [...parts, list, source]) rmSync(part, { force: true })
  • 写入本地文件
    marketing/video/render.mjs:110
    writeFileSync(join(here, 'public/beats.json'), `${JSON.stringify(timeline, null, 2)}\n`)
  • 写入本地文件
    marketing/video/render.mjs:112
    mkdirSync(join(here, 'out'), { recursive: true })
  • 读取本地文件
    marketing/video/review.mjs:21
    const beats = JSON.parse(readFileSync(join(here, 'raw/review-beats.json'), 'utf8'))
  • 执行外部命令来自配置/变量
    marketing/video/review.mjs:22
    const run = (file, args) => execFileSync(file, args, { stdio: 'inherit', cwd: here })
  • 写入本地文件
    marketing/video/review.mjs:31
    mkdirSync(join(here, 'public'), { recursive: true })
  • 删除本地文件
    marketing/video/review.mjs:44
    rmSync(source, { force: true })
  • 写入本地文件
    marketing/video/review.mjs:50
    writeFileSync(join(here, 'public/review-beats.json'), `${JSON.stringify(timeline, null, 2)}\n`)
  • 写入本地文件
    marketing/video/review.mjs:52
    mkdirSync(join(here, 'out'), { recursive: true })
  • 发起网络请求
    marketing/video/src/timeline.js:38
    const timeline = await (await fetch(staticFile(file))).json()
  • 读取本地文件
    relay/src/server.ts:444
    return readFileSync(configured, 'utf8')
  • 发起网络请求
    relay-worker/src/apns.ts:167
    response = await fetch(`${host}/3/device/${target.token}`, { method: 'POST', headers, body })
  • 发起网络请求
    relay-worker/src/index.ts:43
    async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
  • 请求外部主机switchboard
    relay-worker/src/index.ts:176
    return await env.SWITCHBOARD.get(id).fetch(new Request('https://switchboard/bridle', request))
  • 发起网络请求
    relay-worker/src/index.ts:176
    return await env.SWITCHBOARD.get(id).fetch(new Request('https://switchboard/bridle', request))
  • 请求外部主机switchboard
    relay-worker/src/index.ts:194
    return await target.fetch(new Request(`https://switchboard/app?device=${encodeURIComponent(deviceId)}`, request))
  • 发起网络请求
    relay-worker/src/index.ts:194
    return await target.fetch(new Request(`https://switchboard/app?device=${encodeURIComponent(deviceId)}`, request))
  • 发起网络请求
    relay-worker/src/switchboard.ts:63
    override async fetch(request: Request): Promise<Response> {
  • 发起网络请求
    site/worker.js:57
    async fetch(request, env) {
  • 发起网络请求
    site/worker.js:59
    if (!url.pathname.startsWith('/ingest/')) return env.ASSETS.fetch(request)
  • 发起网络请求
    site/worker.js:78
    const response = await fetch(new URL(`https://${host}${path}${url.search}`), init)
扫描于 2026/11/4(0 天前) · 范围 全包扫描 · 来源 GitHub 仓库
涉及外部主机:api.appstoreconnect.apple.com、switchboard

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

界面预览

3 张 · 点击查看大图

功能亮点

  • 原生iOS客户端
  • 手机回复智能体
  • 中继仅见密文

README

由 DeepSeek 最新模型翻译生成
Rowel 专为 DeepSeek Harness 打造的 iPhone 应用。 你的智能体正等着你——随时随地回应。 dsh 停下来请求权限时,你的手机会震动,你能看到它想运行的命令或 diff,轻点一下就能让 Mac 继续。查看智能体做了什么,回答它卡住的那个问题,在火车上启动下一件事。端到端加密;中继读不到你的流量。 rowel.novabox.ai ·加入 TestFlight · MIT,全部开源:应用、Bridle、中继、协议。 只为 dsh 打造。 Rowel 说的是 DeepSeek Harness API——会话、审批、模型、技能。它不是 Claude Code、Codex、Cursor 或其他智能体的遥控器。Harness 本身能跑很多模型(DeepSeek、GLM、Qwen 等等),Rowel 让你在它们之间挑选。
展开完整文档(README 剩余 1 节)
Rowel

专为 DeepSeek Harness 打造的 iPhone 应用。
你的智能体正等着你——随时随地回应。

dsh 停下来请求权限时,你的手机会震动,你能看到它想运行的命令或 diff,轻点一下就能让 Mac 继续。查看智能体做了什么,回答它卡住的那个问题,在火车上启动下一件事。端到端加密;中继读不到你的流量。

rowel.novabox.ai ·加入 TestFlight · MIT,全部开源:应用、Bridle、中继、协议。

只为 dsh 打造。 Rowel 说的是 DeepSeek Harness API——会话、审批、模型、技能。它不是 Claude Code、Codex、Cursor 或其他智能体的遥控器。Harness 本身能跑很多模型(DeepSeek、GLM、Qwen 等等),Rowel 让你在它们之间挑选。

| 组件 | 是什么 | 运行在哪 |
| Rowel | iOS 应用 | 你的 iPhone |
| Bridle | 一个伴随进程,替 Harness 说话 | 运行 dsh 的 Mac |
| Relay | 一个看不到内容的交换机 | 公网——我们的,或你自己的 |

iPhone internet your Mac
┌────────┐ ┌──────────┐ ┌──────────────────┐
│ Rowel │◄──wss───►│ Relay │◄──wss───►│ Bridle ──► dsh │
└────────┘ sealed └──────────┘ sealed └──────────────────┘
└────────── Noise_IK_25519_ChaChaPoly_SHA256 ─────────┘

Bridle 主动拨出。不用端口转发,不用公网 IP,路由器上什么都不用改。 手机和 Mac 在同一网络时,应用会先尝试直连 Mac,四分之一秒后再拨中继;本地路径通常胜出,之后中继就不再承载这个会话。

没有账号。配对就是终端里的一个二维码。

目前进展

Mac 端已完成,日常在用。中继已部署。Rowel 1.1 需要 dsh 0.2 或更新版本——dsh 0.2 换掉了整套 API,应用和 Bridle 现在直接说新 API。应用在 TestFlight 上,直到通过 App Store 审核。你也可以用 Xcode 自己构建,免费,但有个七天的坑。

还在用 dsh 0.1?Bridle 0.1.x 和应用 1.0 继续可用;用 ROWEL_REF=v0.1.7 安装。升到 dsh 0.2 是单向的——先读从 dsh 0.1 升级。

跑起来

1. 在 Mac 上安装 Bridle

curl -fsSL https://rowel.novabox.ai/install | sh
需要 Node 22+ 和 git —— 脚本不会安装这两样;缺什么它就停下来告诉你 —— 还需要 dsh 0.2,脚本会检查并给出警告。它安装的所有东西都在 ~/.rowel 里,另外在 PATH 上放一个符号链接(以及 npm 的常规缓存)。它一共 144 行,其中超过三分之一是注释,如果你想先读一遍再运行,可以看看。

2. 配对

bridle pair

如果 Bridle 没在运行,它会启动 Bridle,打印一个二维码,然后保持运行。在 App 里点 Connect a Mac,再点 I've run it — scan the code。如果 Bridle 已经在运行(作为服务,或作为 dsh 的插件),它只会打印一个新码。--code 作用相同,用于手动输入而不是扫码。

如果 dsh 已经在运行 —— 手动启动的,或者由登录项保持运行 —— Bridle 无法从外部登录它:dsh 0.2 只把登录 token 交给自己的插件。改为在 dsh 内部运行 Bridle,然后重启 dsh:

bridle plugin install

bridle status 会告诉你当前是哪种情况,bridle doctor 会在两者都不是时告诉你该怎么做。如果放任不管,且没有 dsh 在运行,Bridle 会自己启动一个,并用它打印的 token 登录。

通过 SSH 连接时,终端可能画不出二维码,加上 --link 可以打印原始配对链接。当无法使用摄像头时,bridle pair --code 会打印一个 8 字符的码供你输入,然后等待:Mac 随后会显示手机的公钥并请你确认,你拿它和手机上显示的公钥核对。SECURITY.md 说明了这一步为什么存在。

3. 保持运行

bridle service install

登录后自动启动,关闭终端也不受影响。bridle service uninstall 可以把它卸掉。

bridle 命令

bridle start (and pair, on the first run)
bridle pair a pairing QR, starting bridle if none runs (--code: a code to type)
bridle status machine, relay, harness, paired devices
bridle devices list paired devices
bridle revoke <prefix> remove one
bridle backup <file> save this machine's identity, encrypted
bridle restore <file> put a saved identity back
bridle service install keep it running after login
bridle plugin install run bridle inside dsh instead (how it signs in to a dsh already running)
bridle doctor check this machine's setup

bridle 本身可用的参数:

--relay <url> a different relay (default: the public one)
--dsh <url> the harness, if it is not on a usual port
--advertise <url> an extra address to put in the pairing code — a tunnel hostname the machine cannot discover for itself. LAN and Tailscale addresses are found automatically. --direct-port <n> fix the local-network port --no-direct do not listen on the local network at all --no-auto-start never launch the harness --link also print the raw pairing link

状态保存在 ~/.rowel/bridle.json,权限 0600,里面存着这台机器的私钥。ROWEL_HOME 可以把它挪走。

一台 Mac 上跑两个 harness

一个 Bridle 只服务一个 dsh。放任不管时,它会探测常见端口(3080–3083、8080、8791),并沿用上次应答的那个 dsh——只有一个 harness 时没问题,有两个就是掷硬币。--dsh 终结这种猜测:

bridle --dsh http://127.0.0.1:3081

如果那里没有应答,Bridle 会在该端口上启动一个 harness——启动行会显示 (started by bridle),表示是它启动的。--no-auto-start 让它改为拒绝启动。

同一个身份只能运行一个 Bridle;第二次启动会停下来,并说出第一个的名字。所以要把现有配对挪到别的端口,先停掉正在运行的 Bridle,再用 --dsh 启动。要让第二个 harness 与第一个并存,给它一个自己的身份:

ROWEL_HOME=~/.rowel-3081 bridle --dsh http://127.0.0.1:3081

它首次运行会打印自己的二维码,应用里会显示第二台机器——名字相同,靠指纹后缀区分。bridle pair 不需要这些:它什么都不启动,所以能愉快地跑在已经启动的 Bridle 旁边。

从 dsh 0.1 升级

dsh 0.2 首次启动时会重写 ~/.dsh——settings.yaml 变成 settings.yaml.imported,其中的各个 section 移入 profile——而 dsh 0.1 读不了 0.2 写入的对话。所以:

1. 升级前完整备份 ~/.dsh。想回退,就要恢复那份副本,并重新安装 @deepseek-ai/dsh@0.1.1-rc.2,而不只是降级。
2. 把 dsh、Bridle(curl -fsSL https://rowel.novabox.ai/install | sh)和应用一起升到 1.1。应用 1.0 无法与 Bridle 0.2 通信,Bridle 0.2 也无法与 dsh 0.1 通信。
3. 如果你的模型来自网关——Command Code、OpenRouter,任何配置在 llm-pi-ai 下的东西——升级后就没了:dsh 0.2 的默认 profile 不加载那个 adapter,导入时也会丢掉这个 section。把它放回 ~/.dsh/profiles/web/cordis.patch.yml,从 settings.yaml.imported 复制 providers 块(以及你的默认模型):

- insert:
- id: llm-pi-ai
name: '@deepseek-ai/dsh-llm-pi-ai'
config:
providers:
commandcode: # as it was under llm-pi-ai.providers
apiKeyEnv: COMMANDCODE_API_KEY
api: openai-completions
baseURL: https://api.commandcode.ai/provider/v1
… its models list, unchanged

- id: agent-default-model
config:
provider: commandcode
model: deepseek/deepseek-v4.1-flash

然后重启 dsh。密钥留在原处:路由通过环境变量来引用它们。
4. 如果 Bridle 之前作为 dsh 的插件运行,它现在仍然是;否则运行 bridle plugin install 并重启 dsh(见 Pair)。

它保护什么,不保护什么

中继按电路编号在两个 socket 之间转发密封帧。它没有密钥材料,也打不开这些帧,这是形态本身的属性,而不是不承诺任何人的行为——端到端测试会通过中继抓取手机流量,只要其中出现方法名、会话 id、机器名或对话标题,测试就失败。(中继确实会从 Bridle 自己的注册信息里获知机器的显示名称;SECURITY.md 列出了它获知的全部内容。)隧道的两套实现都只用各自平台自带的原语,Node 的 node:crypto 和 Swift 的 CryptoKit。代码树里没有任何第三方密码学依赖。

人们低估的那部分: dsh 向浏览器索取的,不过是它打印出来的那个 token,而 Bridle 持有这次登录,所以配对后的手机对那台 Mac 拥有的权限,和它自己的终端一样——可以运行命令,读写文件。bridle revoke 是收回权限的唯一办法。应用里没有任何一项权限比这更小;应用只是少画了几个按钮。

SECURITY.md 是完整的威胁模型,包括中继确实会获知什么、手机解锁意味着什么,以及已知的弱点。在配对任何你在意的东西之前,值得先读一读。

从源码构建

npm install
npm test # build, docs check, unit tests. Seconds.
npm run test:e2e # the whole stack; ROWEL_E2E_DSH_BIN=<dsh 0.2> for the parts that need dsh,
ROWEL_E2E_MODEL=1 for the few that spend model turns
npm run test:ios # needs Xcode and brew install xcodegen
npm run vectors # regenerate the cross-language test vectors

protocol/ Noise, frames, pairing, relay wire format (TypeScript)
bridle/ the companion process and its CLI
dsh-plugin/ the same core, mounted inside the harness instead of beside it
relay/ the Node relay
relay-worker/ the same relay on Cloudflare Workers and Durable Objects
e2e/ tests that span all three
ios/ the app (Swift, SwiftUI, XcodeGen)

两套实现如何保持同一套协议

Noise 握手和帧编码写了两遍,一遍 TypeScript,一遍 Swift。「我的服务端和我的客户端能通话」在两端都是我自己写的时候证明不了什么,所以 npm run vectors 用固定的密钥和固定的临时密钥跑一遍握手,写出确定性的测试夹具;Swift 那边逐字节比对——握手消息、握手哈希、确认号、传输密文、配对链接、帧编码。

这里失败意味着协议分叉,不是测试不稳定。它已经抓到过一次:Foundation 的 JSONEncoder 不保证键的顺序,两端都能正常解析,自己跟自己对话再怎么聊也发现不了。

运行你自己的中继

bridle --relay wss://your.host 就是客户端侧的全部。relay/ 是单个 Node 进程,没有数据库,也没有配置文件;relay-worker/ 是同一个交换机跑在 Cloudflare Workers 上,公开的那个就跑在这上面。docs/deployment.md 里有 DNS 和 TLS 的细节。

文档

docs/ 是中文写的——它是给维护这个项目的人看的,翻译它比让它保持正确还费劲。英文读者并不是看不了关键部分:SECURITY.md 讲了威胁模型,CONTRIBUTING.md 讲了构建方式和规则,代码注释和提交信息也全程是英文。

| docs/architecture.md | 为什么是这个形态,新功能该放哪 |
| docs/protocol.md | 线上传输的确切字节,够你写第三个客户端 |
| docs/fold.md | 事件日志怎么变成屏幕画面,一条条规则讲 |
| docs/dsh-api-inventory.md | 应用用到的 dsh 0.2 端点和流,以及用在哪 |
| docs/deployment.md | 怎么跑 relay、DNS,以及上线还差什么 |

docs/README.md 说明了哪些章节是规范级、哪些是设计级。想照着设计级的章节重新实现,先来问一声。

许可

MIT。见 LICENSE。
预览分享卡片长什么样 ↓0x5446/rowel
分享 · DeepSeek Harness Hub
0x5446/rowel

专为 DeepSeek Harness 打造的 iPhone 应用。

这是你的仓库?认领条目

回到顶部