deepseek-harness
deepseek-ai
DeepSeek Harness: Everything is a Plugin.
PROJECT TOPICS
PROJECT README
Bridges a local BlueBubbles server (the macOS iMessage bridge) into DeepSeek Harness.
Design principle (Unix philosophy): the host plugin keeps only passive capabilities (webhook receive + binding resolution + message injection) and two high-frequency model tools (send text / send attachment); everything else converges into the bb-channel CLI — agents call it via bash, humans and automation scripts use it directly.
| Component | Location | Role |
|---|---|---|
bluebubbles-bridge |
src/index.ts |
webhook route + message injection + 2 send tools + the bluebubbles service |
dsh-heartbeat |
src/heartbeat.ts |
generic periodic wake-up (reads heartbeat-targets.json) |
dsh-cron |
src/cron.ts |
cron-time tasks (reads cron-jobs.json) |
bb-channel |
bin/bb-channel.mjs |
CLI: chats/messages/send/bind/contacts/webhook/configure… |
| Tool | Role |
|---|---|
bluebubbles_send_text |
send a text message |
bluebubbles_send_attachment |
send an attachment (image/file) |
Everything else goes through the CLI (agents call it via bash — equivalent capability):
~/.local/bin/bb-channel chats [--limit N] [--all] # list chats (placeholder/pairing-code noise hidden by default)
~/.local/bin/bb-channel messages <chatGuid> [--limit N] # read history (with sender display names)
~/.local/bin/bb-channel send <chatGuid> <text...> # send text
~/.local/bin/bb-channel send-attachment <chatGuid> <file>
~/.local/bin/bb-channel attachment <guid> [--dir D] # download an attachment
~/.local/bin/bb-channel bind <chatGuid> (--workspace PATH | --session ID)
~/.local/bin/bb-channel unbind <chatGuid>
~/.local/bin/bb-channel bindings # show the binding table
~/.local/bin/bb-channel contacts / set-contact <address> <name>
~/.local/bin/bb-channel webhook [--url URL] # check/self-register the webhook
~/.local/bin/bb-channel ping / configure # connectivity / write ~/.dsh/.env
process.env → ~/.dsh/.env → ~/.zshenv; nothing to fill in by hand.~/.dsh/bluebubbles-bindings.json / bluebubbles-contacts.json directly; the plugin hot re-reads both files before every inbound message — edits take effect immediately, no reload.BlueBubbles server (new message)
│ POST {type:"new-message", data:{...}}
▼
DSH webServer route /bluebubbles/webhook (loopback only)
│ hot re-read bindings/contacts → look up chatGuid → workspacePath/sessionId
▼
workspace.sessionIds[0] → agents.get(sessionId).send(userMessage, 'next-step', true)
▼
The workspace's model wakes up and sees a message annotated
"📱 iMessage · <MM-DD HH:mm UTC±N> · <chat name> · 来自 <name> (<number>)"
The header stamp comes from the message's dateCreated (BlueBubbles hands back either Apple epoch ms
or Unix ms — disambiguated by magnitude), rendered in the host timezone and always suffixed with the
UTC offset. BLUEBUBBLES_TZ overrides the zone (e.g. BLUEBUBBLES_TZ=Asia/Shanghai); the year is
prepended only when the message isn't from the current year. An unknown/unsupported BLUEBUBBLES_TZ
falls back to the host zone, and if Intl rejects the zone at format time the stamp degrades to a
plain host-local stamp — the header is decoration and never breaks inbound delivery. Messages
without dateCreated keep the old header unchanged.
Anti-loop (two layers, v22+):
pendingSent queue: the plugin records (chatGuid, normalized text) before every send; webhook echoes with isFromMe=true matching an entry are dropped (60s TTL, unicode NFC-normalized comparison);seenGuids: after a successful send the real guid returned by the API joins a dedup set (BlueBubbles occasionally pushes the same event twice — second-layer backstop).A blanket isFromMe drop is not an option: phones on the same Apple ID also produce isFromMe=true in a self-chat DM, and dropping all of them would kill real user messages.
Sender display name: payload.handle.displayName → ~/.dsh/bluebubbles-contacts.json (address→name, maintained via bb-channel set-contact) → bare number.
With relay: true on a binding, the bridge registers a reply trigger for the woken session (persisted to bluebubbles-relay-state.json, 10min TTL, cleared on turn/end; an exact NO_REPLY reply suppresses delivery). During that turn, every assistant message containing text parts is sent back to the chat immediately — thinking and tool results are never delivered.
When dsh-cron fires a job whose target session has a relay: true binding, it arms the same mechanism through the bluebubbles service's armRelay. Cron task prompts must not tell the model to call send tools itself — that would double-send.
A session driven purely over iMessage has nobody to click the web UI's interactive cards, and both cards would otherwise park the turn forever. For any session named by the bindings table (including subagent children of one), the bridge registers prepend listeners that run ahead of the web answerers:
ask_user_question is denied with a corrective message telling the model to ask in plain text instead — the reply is relayed to iMessage automatically, then the turn ends and the user's next iMessage answers it;reject (fail closed: sandbox escalations are denied and the model is told to continue without them). Set BLUEBUBBLES_GUARD_APPROVAL=allow to auto-approve instead, only for trusted setups.Sessions not in the bindings table fall through to the ordinary web flow unchanged. Disable the guard entirely with BLUEBUBBLES_GUARD=0.
| Method | Takes effect |
|---|---|
bb-channel configure --password <pw> (writes ~/.dsh/.env) |
after DSH restart or bridge reload |
env var BLUEBUBBLES_PASSWORD (optionally BLUEBUBBLES_BASE_URL) |
after DSH restart |
Never put DSH_-prefixed variables in .env — the DSH bootstrap refuses to start. That is why heartbeat/cron config keys are HEARTBEAT_INTERVAL / HEARTBEAT_TARGETS / CRON_JOBS.
$DSH_HOME, default ~/.dsh)| Path | Content | Writer |
|---|---|---|
bluebubbles-bindings.json |
{ "chat:<guid>": { workspacePath \| sessionId } } |
bb-channel bind/unbind |
bluebubbles-contacts.json |
{ "address": "display name" } |
bb-channel set-contact |
bluebubbles-media/ |
inbound attachments (<guid>-<filename>) |
bridge auto-download |
heartbeat-targets.json |
heartbeat targets | hand-edited |
cron-jobs.json |
cron jobs | hand-edited |
Session resolution chain: sessionId direct → otherwise workspacePath → that workspace's sessionIds[0] (most recent session) → live agent. With no live agent, the bridge resumes the persisted session on demand (folding its stored preset, same as the web UI's attach path) — bound chats survive DSH restarts without anyone reopening them. A message is dropped and logged only when the resume itself fails (unknown session, subagent-owned session, missing preset).
src/*.ts → npm run typecheck → git commit~/.dsh/profiles/web/cordis.patch.yml, bump the corresponding row's ?v=N by 1 and savecurl -X POST -d '{}' http://127.0.0.1:3080/bluebubbles/webhook (ok-vN).With BLUEBUBBLES_DEBUG=1 (env or .env), inbound events and drop reasons go to ~/.dsh/bluebubbles-debug.log (serialized appends, no lost lines).
As a profile bundle (consumers):
dsh plugin --profile web add github:vINyLogY/dsh-bluebubbles
dsh plugin add forwards to pnpm and, because this package declares
dsh.bundle.patch, automatically joins the profile's bundle stack — the
shipped cordis.patch.yml inserts rows for all three plugins
(bluebubbles-bridge, dsh-heartbeat, dsh-cron, resolved through the
package exports map). A DSH restart loads them. The bb-channel CLI lands on
the profile's node_modules/.bin via the package bin entry.
For local development, insert absolute-path rows with a ?v=N cache-buster
into the profile's own cordis.patch.yml instead (see below).
@deepseek-ai/dsh-* devDeps (^0.1.1-rc.2 / cordis ^4.0.1 — see the badge above for the exact CI-verified CLI version).src/index.ts.bin/bb-channel.mjs), zero dependencies, global fetch/FormData — deliberately .mjs so it runs on any modern Node and stays ESM wherever it is symlinked.npm install --cache ./.npm-cache && npm run typecheck.CLASSIFICATION EVIDENCE
系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。当前命中: 无有效分类标签。