@zhengjunyao/dsh-zhihu
A Zhihu (知乎) plugin for DeepSeek Harness: it wraps the local zhihu CLI — pyzhihu-cli, command name zhihu — as zhihu_* agent tools. Search questions, read the hot list, inspect questions / answers / comments, look up users, browse the recommended feed, topics and notifications. Publishing, voting, following and deleting are off by default and gated behind an explicit readOnly switch.
- Read tools call
zhihu <command> --jsonand normalize the raw Zhihu API payload into compact items (id, type, title, URL, author, upvotes, answer count). - Login rides Zhihu's official QR-code API: the CLI writes the QR image to a PNG and the plugin hands the path to the agent, which can show it to the user. Pasting a cookie string is also supported — the plugin writes the CLI's cookie file directly, so the token never lands in the process list.
- Write tools are not registered at all by default, so nothing can be published or deleted by accident.
Prerequisites
# Install the Zhihu CLI (Python 3.10+; uv keeps it in its own venv)
uv tool install pyzhihu-cli # or: pipx install pyzhihu-cli
zhihu --version # zhihu-cli, version 0.2.4
The plugin resolves zhihu from $PATH, ~/.local/bin, /opt/homebrew/bin and /usr/local/bin; point cliPath at an absolute path if it lives elsewhere.
Important: every pyzhihu-cli command requires a login — even the hot list fails with
Not authenticated. The first run must therefore be azhihu_login.
Tools
Management and auth
| Tool | Purpose |
|---|---|
zhihu_status |
CLI availability and version, login state (cookie has z_c0/_xsrf/d_c0), read-only mode, paths, timeout. Never echoes cookie contents |
zhihu_config |
Configure readOnly / cliPath / timeoutMs / loginWaitMs / proxy / cliHome; reset: true restores defaults. Call with no arguments to read |
zhihu_login |
Log in. mode: qrcode (default) returns the QR PNG path (~/.zhihu-cli/login_qrcode.png) to show the user and can be called again to keep waiting (it never spawns a second QR code); mode: cookie accepts pasted z_c0/_xsrf/d_c0 and verifies online |
zhihu_logout |
Remove the CLI's cookie file |
zhihu_whoami |
Current account profile |
Read
| Tool | Purpose |
|---|---|
zhihu_search |
Search content (type: general|people|topic) |
zhihu_hot |
Zhihu hot list |
zhihu_question |
Question detail; answers: true adds the answer list (limit / sort) |
zhihu_answer |
Answer body; comments: true adds comments |
zhihu_user |
User profile, or their answers / articles / followers / following (include) |
zhihu_feed |
Recommended feed; withComments: true uses the CLI's text-mode "feed + comments" |
zhihu_topic |
Topic detail; hotQuestions: true adds the topic's hot questions |
zhihu_notifications |
Notifications (with offset paging) |
zhihu_collections |
Your collections |
Write (registered only when readOnly: false)
| Tool | Purpose |
|---|---|
zhihu_publish |
Publish a question / pin / article (kind: ask|pin|article, optional topics and local images; dryRun: true only echoes the command) |
zhihu_vote |
Upvote / cancel an upvote |
zhihu_follow_question |
Follow / unfollow a question |
zhihu_delete |
Delete your own question / pin / article (requires explicit confirm: true) |
Web panel
Two entries, one component:
- A bottom-right floating ball (blue 「知」 button) — reachable from any page, opening a 380px popover.
- A 「知乎」 card in the settings page (
settings.section).
The panel covers: login state with an inline QR code (one click, auto-detects the scan), the account summary, the read-only switch, proxy / timeout settings, and quick hot-list / search lookups with clickable links.
It talks to the loopback-only /api/dsh-zhihu/* route family (status, config, login, qrcode, logout, whoami, hot, search); non-loopback peers, foreign Host headers and cross-site Origins get 403. The QR is returned as a PNG data URL — no new files on disk.
Client changes need a browser hard refresh; host changes or
dsh.clientmanifest changes need adsh webrestart.
Read-only switch
The plugin is read-only by default: write tools are never registered, so the model cannot even see them.
Two ways to open them up:
// 1) At install time, seed it in cordis.patch.yml
- insert:
- id: zhihu
name: dsh-zhihu
config:
readOnly: false
2) At runtime via the tool (stored in ~/.dsh/dsh-zhihu.json, remounts immediately):
zhihu_config({ readOnly: false }) # enable write tools
zhihu_config({ readOnly: true }) # revoke them
The store value wins over the install-time seed; with neither set, read-only applies.
Configuration
| Key | Default | Meaning |
|---|---|---|
readOnly |
true |
Only read tools when true; write tools are registered when false |
cliPath |
zhihu |
CLI command name or absolute path |
timeoutMs |
90000 |
Per-invocation timeout |
loginWaitMs |
15000 |
How long zhihu_login waits for a scan before returning; call it again to keep waiting |
proxy |
empty | Empty = inherit the environment; none = force no proxy (Zhihu is a domestic site, a system proxy can actually break it); or http://host:port |
cliHome |
~/.zhihu-cli |
CLI config directory. The CLI hardcodes Path.home()/".zhihu-cli", so the plugin overrides the child process's HOME |
Stored in ~/.dsh/dsh-zhihu.json (mode 0600). Credentials are not stored here — the CLI keeps cookies in <cliHome>/cookies.json (the CLI chmods it 0600 itself).
Logging in
Option 1 — QR code (recommended)
zhihu_login() → returns qrcodePath (~/.zhihu-cli/login_qrcode.png)
↓ the agent shows it with read_image / present
the user scans it in the Zhihu app
zhihu_login() → keeps waiting; returns authenticated: true
Option 2 — paste a cookie
zhihu_login({ mode: "cookie", cookie: "z_c0=...; _xsrf=...; d_c0=..." })
The plugin writes the cookie straight into the CLI's cookies.json (byte-compatible with the CLI's own save_cookies) and then verifies it online via zhihu whoami; a failed check removes the file and reports why. Unlike zhihu login --cookie, this path never exposes the token in the process list.
Compatibility
Requires DeepSeek Harness ≥ 0.1.5-rc.1 (declared in dsh.engines.dsh, which the plugin market reads) and is tested on 0.1.5-rc.1. This build follows the DSH 0.1.5 strict tool-result contract (lossless-JSON snapshots, additionalProperties: false schema validation, output.render returning ContentBlock[]) and resolves the executable independently of the host PATH (a launchd-managed host only has /usr/bin:/bin).
Known limitations
- The CLI's
--jsonmode does not cover comments, the "feed + comments" command, or a topic's hot questions. Those three fall back to text mode and land in the result'stextfield (ANSI colour codes stripped). zhihu_search/zhihu_hotaccept-a/--answersonly in the CLI's human-readable mode; it is ignored under--json, so the plugin does not expose it.- Zhihu applies risk control. This plugin is a thin wrapper — request volume and compliance are the caller's responsibility; do not crawl aggressively.
- Zhihu's public API can change; the real command surface is whatever
zhihu <cmd> --helpreports locally.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
Every command fails with Not authenticated |
pyzhihu-cli needs a session for all commands, the hot list included. Run zhihu_login first. |
403 with code 40352, a redirect to /account/unhuman, or "系统监测到您的网络环境存在异常" |
Zhihu risk control rejects the request fingerprint, not just missing cookies — so re-pasting a cookie may not help. Use zhihu_login({ mode: "browser" }) to import the real session from local Chrome, then retry. QR-code polling is anonymous and gets 403 on every poll, which is why scanning appears to do nothing. |
403 with code 10003 mentioning x-zse-96 |
The endpoint requires Zhihu's request signature. Topic detail is unavailable; zhihu_question automatically falls back to the answer list (the embedded question.title carries the title). |
zhihu_status reports the CLI unavailable |
Install it (uv tool install pyzhihu-cli) or point cliPath at an absolute path. |
| The panel's QR code never completes the login | Expected under risk control, for the reason above. Use the 「从 Chrome 导入登录态」 button in the panel instead. |
Every failure now names which of these it is: failureReason classifies the CLI output instead of returning a bare timeout.
Development
pnpm install
pnpm typecheck # tsc --noEmit (host + client)
pnpm build # tsc for .d.ts + tsdown for lib/index.js (host) and lib/client.js (browser)
pnpm test # smoke tests (real CLI calls; all writes land in a temp dir)
pnpm test:e2e # drive the plugin's own tools against the real CLI (no host restart)
node tests/routes.mjs # hit /api/dsh-zhihu/* with synthetic req/res
node tests/client.mjs # execute lib/client.js under a synthetic __ModuleLoader__
pnpm verify:full # portability check in an isolated DSH_HOME (tarball install + stability watch)
Portability verification follows PORTABILITY-SOP.md and must print ✅ 通过 before a release.
Artifacts: lib/index.js (host, ESM) + lib/client.js (browser closure-factory) + lib/types/ (.d.ts).
Install into a profile
dsh plugin --profile web add @zhengjunyao/dsh-zhihu
dsh plugin --profile web add link:/path/to/dsh-zhihu
# or, once published
dsh plugin --profile web add github:zhengjy01/dsh-zhihu
A DSH web restart is required afterwards (there is no hot reload).
License
MIT. The Zhihu CLI itself (pyzhihu-cli) is Apache-2.0; this project only invokes it as a subprocess and contains none of its code.
No comments yet. Be the first to write one.