ZhaoAndy821/dsh-zcode-connector ↗★ 2

dsh-zcode-connect

通过CDP将ZCode桌面应用接入DSH。 将ZCode客户端作为模型通道接入DSH。适合已有ZCode桌面端的用户。

套件
dsh-zcode-connect
相容性
待驗證
Harness 依賴範圍
^0.1.5-rc.1
Cordis 依賴範圍
^4.0.2
版本
0.1.0
授權
MIT
最近更新
2026年9月26日

同名套件的其他儲存庫

安裝

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:ZhaoAndy821/dsh-zcode-connector

DSH ZCode Connector

Use the ZCode (Z.ai) desktop app as a model channel from DeepSeek Harness (DSH) — by driving the app's own GUI over Chrome DevTools Protocol, not by pretending it is an API key.

It ships three things:

PieceWhat it does
bridge/zcode-bridge.mjsLocal OpenAI-compatible endpoint (POST /v1/chat/completions) that types a prompt into the running ZCode window, waits for the turn to finish, and returns the answer; the plugin ships this same file and starts it on demand
plugin dsh-zcode-connectHost half with three loopback routes (status JSON, the HTML panel, and an on-demand bridge start) plus a client half that puts the panel in DSH's right sidebar as a tab — account, plan, live quota, loaded capabilities, what this connector installed, and what the client is running in the background right now
mcp/server.mjsMCP stdio server exposing status, background runs, ask, and provisioning tools to any MCP client (DSH consumes it through @deepseek-ai/dsh-mcp-client)

Why drive the GUI instead of using an API key

Measured on a real install, not inferred:

  • ZCode's promo grants (for example ZCode Weekend Build, 300,000,000 GLM-5.3-Flash tokens) are declared in the client's own catalog as account-bound: "access": { "type": "zhipu-account", "mode": "start-plan" }. There is no API key to extract.
  • The only inference route for such a grant, POST https://zcode.z.ai/api/v1/zcode-plan/anthropic/v1/messages, accepts the account token but answers {"code":3007,"msg":"captcha verify failed"} without X-Aliyun-Captcha-Verify-Param — an Aliyun traceless-captcha proof minted per request inside ZCode's renderer.
  • The documented platform routes do not carry the grant: with the same account, https://open.bigmodel.cn/api/anthropic and /api/coding/paas/v4 answer {"code":"1113","message":"余额不足或无可用资源包,请充值。"}, and the ZCode account token is 401 on https://api.z.ai/api/anthropic.

So the way to reach that quota from your own tooling is to let the first-party client make the call and drive its UI. That is what this project does — and it is the reason the grant is reachable here at all.

The client does the real work

  • The client signs every request. ZCode's own renderer mints the per-request attestation, so the call path is the same one the app uses when you press Send by hand.
  • Same widgets, same events, same channels. The bridge attaches over the public Chromium DevTools Protocol and drives the real controls — nothing internal is reimplemented, and no hidden endpoint is called behind the app's back.
  • The credential is read-only, and only for display. The account token is used for exactly one thing: asking the client's own plan API how much of the grant is left, so the panel can show it. It never leaves in a response, never reaches a log, and the self-check asserts that.
  • One account, one session, one conversation at a time. The bridge is serialised by design: no parallel-call path, no retry storming.
  • The client stays the vendor's build. No proxy, no certificate injection, no patched binary.
  • The prompt is verified before it is sent. The composer is cleared first and the text is checked character-for-character, so what arrives is exactly what the caller wrote.
  • Answers are read from the client's own transcript, not scraped from a screen — so long, tool-heavy turns come back complete.

It behaves exactly like a normal user

The window does not have to be visible, focused, on top, or even restored. Input goes to the renderer over CDP, which is independent of window stacking and of OS focus, so ZCode can sit minimised behind your other work — it just has to be running. (Measured: the app was launched minimised and every click and keystroke below still landed.)

Window position and size are not part of the contract. Two different things are easy to confuse here, so the connector keeps them apart:

What it isDoes the bridge touch it?
the windowthe OS window you open from the dock or taskbar — a display surfaceno: never read, never moved, never resized
the renderer pagethe Chromium page inside that window (file://…/renderer/index.html) — the only CDP target the bridge attaches toonly its layout viewport, and only for the duration of one request

There is exactly one page target and it is the renderer of the very window you use; no second, hidden window is opened. The status/panel half touches no window at all — it reads files.

The page's viewport is frozen for the length of a request because a turn can run for minutes: a target's centre is measured and then clicked, and if the window were resized in between, the target would have moved. The freeze uses the size the window already has, plus its device pixel ratio, so nothing you see changes, and it is released the moment the request ends. Verified: with the fallback deliberately set to 1000×700 the bridge logs viewport frozen at 1280x900 @1.5x (source: window), and an override placed before a call is gone after it — the page returns to its own 1280×900 at dpr 1.5.

The app lays out against the viewport it is told it has, anchoring its composer 73 px above the bottom edge (measured at 1244×802, 1100×650 and 1280×900). Announcing a size larger than the window is therefore what pushes the composer off the bottom of the window — which is why the bridge announces the window's own size; ZCODE_VIEWPORT_W / ZCODE_VIEWPORT_H are only a fallback for a window reporting no usable size (below 640×480).

Apart from that, nothing is unusual: the same widgets, the same events, the same request path.

A person doesThe bridge does
clicks New task in the sidebardispatches a real mouse click at that element's centre (Input.dispatchMouseEvent)
clicks the model selector, picks the plan channelsame, after reading the composer's label and verifying the hit landed on the intended element
types the prompt into the composerfocuses it, replaces the selection, sends Input.insertText, then verifies the text landed character-for-character
presses EnterInput.dispatchKeyEvent with the Enter keycode
reads the answerreads the client's own transcript (querySource === "main_turn") — the same record the app writes for its own history

There is no hidden channel and no injected business logic: the automation layer only clicks, types, and reads what the client already displays or persists.

Watching what the client runs in the background

When ZCode works, it works inside its own process: the primary agent spawns subagents, runs tool calls, and keeps going on its own. Nothing about that reaches the harness that asked for it — DSH's own sidebar lists DSH's subagents and jobs, and it cannot see the ones running in another application. The connector therefore reads the client's own state files and renders them into its panel (and over MCP), so the run is visible while it happens:

ShownMeaning
running subagentsrole, the task it was given, the tool it is executing right now (with its target path), elapsed time, turns, tool calls, tokens
finished subagentsoutcome (done / partial / blocked), duration, tokens, tool count, the CHANGED: / FAILED: item list from its own report
recent tasksthe client's task list with per-task subagent count, total tokens, and last activity
generating indicatorwhether inference is open right now, and for how long
scheduled workAutomations, their runs, and idle-time (off-peak) tasks

It is passive: three reads of files the client already writes, no injection, no polling of the GUI, no extra quota.

SourceWhat it yields
~/.zcode/cli/agents/sess_*/agent_*/metadata.jsonone directory per subagent run: role, task, status, tokens, tool count, duration
~/.zcode/cli/agents/sess_*/agent_*/output.txtthe worker's own report, parsed into outcome + counts + item list
~/.zcode/cli/rollout/model-io-*.jsonlone record per model round trip, appended as the run proceeds (live turn count)
~/.zcode/cli/log/zcode-.jsonlthe client's turn lifecycle: open inference requests, every tool call, per-session phases
~/.zcode/v2/tasks-index.sqlitetask titles, per-task status, Automations and off-peak runs

The same view is available to an agent as the zcode_runs MCP tool, which is the practical way to watch a long handoff: a single call reports what is running, what it is doing, and what finished. The panel refreshes every 15 s while it is open.

This is also what makes long runs safe to hand over. A thinking-heavy turn can outlast a caller's own wait budget — the call returns a timeout, the client keeps working — and the run stays observable instead of being lost with the call.

Where it appears: a tab in the right sidebar

The panel is not a page you have to go and open. The plugin ships a client half that registers it as a tab next to Subagents and Tasks in DSH's right sidebar, and Settings → Plugins → ZCode carries the switch that adds or removes it (a per-browser preference, on by default).

Three extension points make that tab, all of them DSH's own:

WhatRegistered intoNotes
the tab typectx.sidebarRightTabs.register({ id, kind, priority, title, guide })a kind of its own, so it cannot collide with Files/Tasks; the guide capsule is how it shows up in the sidebar's list
its bodyslot sidebar.right.pane.tab, keyed by that idan iframe of the host route below, so the view has one implementation, not two
its chip titleslot sidebar.right.pane.tab.title, keyed by the same idglyph + the title the frame hands in

The same shape works for any DSH plugin that wants a right-sidebar panel; lib/client.js is a prebuilt bundle (no bundler — it is already in the __ModuleLoader__.load envelope).

Extensibility: turn ZCode into a grunt-work executor

ZCode is an extension platform in its own right, and this connector exposes that surface over MCP, so a caller can reshape the client instead of merely talking to it:

ZCode extension pointWhere it livesInstalled by
Skills~/.zcode/skills//SKILL.mdzcode_install_skill
Custom subagents (roles)~/.zcode/agents/.mdzcode_install_agent
MCP servers~/.zcode/cli/config.json → mcp.serverszcode_install_mcp
Plugins & marketplacesthe client's own plugin systemthe client UI
Automations, idle-time tasks, cron toolsthe client's scheduling surfacethe client, or a prompt

That composition is what makes it useful for tedious volume rather than reasoning: skills teach the contract (what to return, in what format), subagent roles narrow a small model's job so it cannot wander, fan-out splits a batch into chunks the model can actually finish, MCP servers hand it tools it did not have, and automations keep it working on a cadence without a driver.

Measured example: 12 files, primary agent split into two chunks and ran two dsh-batch-worker subagents in parallel, each returning a machine-readable CHANGED: report — 1 m 50 s end to end.

None of this is ZCode-specific plumbing. The same shape — attach over CDP, install skills and subagent roles, expose the lot over MCP — applies to any Electron client that has its own extension points, which is why the pattern is worth publishing as a template rather than a one-off script.

Requirements

  • ZCode desktop, launched with a debug port: ZCode.exe --remote-debugging-port=9333 (it must be running, but it does not need to be visible or in the foreground — minimised, covered, or on another virtual desktop all work; only closing it breaks the bridge)
  • Node.js 20+ (the bridge and MCP server use the built-in fetch)
  • DeepSeek Harness, if you want the plugin half

Install

1. The bridge (shipped here; the plugin can start it for you)

node bridge/zcode-bridge.mjs           # listens on 127.0.0.1:9444

POST /v1/chat/completions (OpenAI-shaped, non-streaming). GET /healthz reports the queue depth.

The plugin ships its own copy of that file and owns the process, so you do not have to start it by hand. bridgeMode decides who does:

bridgeModeWho starts it
on-demand (default)nobody at boot — the first thing that needs it does: an MCP zcode_ask, or the panel's 「start the bridge」 button
autothe plugin starts it while loading and stops it again when it unloads
offthe plugin never touches the process; it only reports whether the port answers

bridgeScript overrides which file gets spawned; empty means the copy bundled with the plugin.

2. The plugin (DSH)

Copy this directory somewhere stable, then in your profile:

// 
/package.json
"dependencies": { "dsh-zcode-connect": "link:/absolute/path/to/dsh-zcode-connector" },
"dsh": { "profile": { "bundles": [ /* … */ "dsh-zcode-connect" ] } }

The plugin imports @deepseek-ai/schemastery, so a package outside the profile tree needs a dependency bridge: /node_modules/@deepseek-ai → /profiles/node_modules/@deepseek-ai.

# 
/cordis.patch.yml
- id: zcode-connect
  name: dsh-zcode-connect
  config:
    debugPort: 9333          # ZCode's --remote-debugging-port
    bridgePort: 9444         # the bridge above
    bridgeMode: on-demand    # on-demand | auto | off — see the table above

The client half is fail-closed — do not half-declare it. package.json declares exports["./client"] and dsh.client, and lib/client.js must therefore exist. DSH composes client bundles at startup and refuses to boot when a declaration resolves to no file (ClientPackageCompositionError; observed as the launcher bringing dsh up and it exiting a few seconds later). Shipping both together is the only valid state; if you ever remove lib/client.js, remove those two declarations in the same commit.

3. The MCP server (optional, but this is the convenient surface)

- id: mcp-zcode
  name: '@deepseek-ai/dsh-mcp-client'
  config:
    transport: stdio
    serverName: zcode
    command: node
    args: ['/absolute/path/to/dsh-zcode-connector/mcp/server.mjs']
    toolCallTimeoutMs: 300000
    failOnStartupError: false

Tools then appear as mcp__zcode__:

ToolPurposeSpends quota
zcode_statusaccount, plan, live remaining quota, loaded channelsno
zcode_runswhat the client is running in the background right now, plus what it just finishedno
zcode_askone prompt → GLM-5.3-Flash → the replyyes
zcode_install_skillinstall a skill into the client (~/.zcode/skills//SKILL.md)no
zcode_install_agentinstall a user-level subagent role (~/.zcode/agents/.md)no
zcode_install_mcpinstall an MCP server for the client (~/.zcode/cli/config.json → mcp.servers)no
zcode_capabilitieslist what this connector installedno
zcode_uninstallremove an installed skill / agent / MCP serverno

Where the connector writes things

Locations are taken from the client's own zcode-guide:diagnosing-mcp material, not guessed:

ScopeFileField
Skills~/.zcode/skills//SKILL.mdfrontmatter name + description
Subagents~/.zcode/agents/.mdfrontmatter name, description, tools, model
MCP (user)~/.zcode/cli/config.jsonmcp.servers
MCP (workspace)/.zcode/config.json or /zcode.jsonmcp.servers
MCP (plugin)`
/.mcp.json`namespaced `plugin:
:`

Two rules the client states explicitly and this