zhihu — search, read & publish on Zhihu
Two authentication layers, two scripts:
| Script | Auth | Capabilities |
|---|---|---|
scripts/search.py |
ZHIHU_DEVELOPER_TOKEN (Bearer) |
Search Zhihu, search the web, get hot topics |
scripts/blog.py |
ZHIHU_COOKIES (login cookie) |
Read/write own articles & answers |
No browser, no third-party deps — just urllib.
The connector injects credentials as env vars:
ZHIHU_DEVELOPER_TOKEN— Zhihu Developer Platform access secret (Bearer token). Used for search and hot-list queries. Secret — never echo or print it.ZHIHU_COOKIES— a JSON array of{name, value, domain, path, ...}cookies. Used for reading/writing the user's own content. Secret — never echo or print it.
Locate the scripts first (every Bash block)
The connector sets $SKILL_DIR to this skill's directory, so the scripts live at
$SKILL_DIR/scripts/. Do NOT hard-code python3 $SKILL_DIR/scripts/… directly.
If more than one skill was loaded in the same turn, $SKILL_DIR can point at the
other skill and the call fails with No such file or directory (this is the #1
cause of a Zhihu run silently not finishing). Resolve the path defensively at the
top of every Bash block — each Bash call is a fresh shell, so the variable does
not carry over:
# blog.py is unique to this skill, so it anchors zhihu's dir even if $SKILL_DIR is wrong.
ZDIR="$SKILL_DIR"; [ -f "$ZDIR/scripts/blog.py" ] || ZDIR=$(find /tmp -maxdepth 8 -path '*/skills/*/scripts/blog.py' 2>/dev/null | head -1 | sed 's#/scripts/blog.py##')
[ -f "$ZDIR/scripts/blog.py" ] || { echo "zhihu scripts not found (SKILL_DIR=$SKILL_DIR) — is the skill loaded?" >&2; exit 1; }
SEARCH="$ZDIR/scripts/search.py"; BLOG="$ZDIR/scripts/blog.py"
Then use "$SEARCH" / "$BLOG" (always quoted) as shown below.
Search CLI (search.py)
scripts/search.py — search Zhihu and the web. Requires
only ZHIHU_DEVELOPER_TOKEN (no cookies needed).
# Resolve the scripts first (see "Locate the scripts first" above) — robust to $SKILL_DIR.
ZDIR="$SKILL_DIR"; [ -f "$ZDIR/scripts/blog.py" ] || ZDIR=$(find /tmp -maxdepth 8 -path '*/skills/*/scripts/blog.py' 2>/dev/null | head -1 | sed 's#/scripts/blog.py##')
SEARCH="$ZDIR/scripts/search.py"
# Search Zhihu content (站内搜索) — questions, answers, articles
python3 "$SEARCH" search "Python 爬虫"
python3 "$SEARCH" search "Python 爬虫" --count 5
# Search the entire web (全网搜索) — all indexed sites
python3 "$SEARCH" global "AI Agent"
python3 "$SEARCH" global "AI Agent" --count 15
# Filter by site or time
python3 "$SEARCH" global "React" --filter 'host=="github.com"'
python3 "$SEARCH" global "新闻" --filter 'publish_time>=1720000000'
python3 "$SEARCH" global "技术" --filter 'host=="github.com" AND publish_time>=1720000000'
python3 "$SEARCH" global "实时新闻" --db realtime
# Get Zhihu trending topics (热榜)
python3 "$SEARCH" hot
python3 "$SEARCH" hot --limit 10
Search commands
| Goal | Command |
|---|---|
| Search Zhihu (max 10 results) | python3 "$SEARCH" search "<query>" --count N |
| Search entire web (max 20) | python3 "$SEARCH" global "<query>" --count N |
| Filter by site | --filter 'host=="example.com"' |
| Filter by time | --filter 'publish_time>=<unix_ts>' |
| Search only realtime/static index | --db realtime or --db static |
| Zhihu trending topics (max 30) | python3 "$SEARCH" hot --limit N |
Search result fields
zhihu_search returns: title, type (Article/Answer), content_id, url, excerpt, vote_up (赞同), comments, author, authority level, edit_time.
global_search returns the same fields plus has_more indicator. The url
includes utm tracking params from Zhihu's platform.
hot_list returns: rank, title, url, summary, thumbnail.
global_search Filter syntax
host=="example.com"— filter by domain (note:host=="zhihu.com"not supported — usesearchcommand instead)publish_time>=1720000000— filter by publish time (unix seconds)- Logical operators:
AND,OR(must be uppercase) - Parentheses for grouping:
(host=="a.com" OR host=="b.com") AND publish_time>=T
Blog CLI (blog.py)
The skill ships scripts/blog.py — self-contained, stdlib only.
Requires ZHIHU_COOKIES (login cookie).
# Resolve the scripts first (see "Locate the scripts first" above) — robust to $SKILL_DIR.
ZDIR="$SKILL_DIR"; [ -f "$ZDIR/scripts/blog.py" ] || ZDIR=$(find /tmp -maxdepth 8 -path '*/skills/*/scripts/blog.py' 2>/dev/null | head -1 | sed 's#/scripts/blog.py##')
BLOG="$ZDIR/scripts/blog.py"
# Read (run directly)
python3 "$BLOG" whoami # who is logged in
python3 "$BLOG" articles --limit 20 # my published articles + stats
python3 "$BLOG" article <article-id> # one article's details + stats
python3 "$BLOG" answers --limit 20 # my published answers + stats
python3 "$BLOG" answer <answer-id> # one answer's details + stats
python3 "$BLOG" question <question-id> # a question's info + whether I answered it
Verify the connection first
ZDIR="$SKILL_DIR"; [ -f "$ZDIR/scripts/blog.py" ] || ZDIR=$(find /tmp -maxdepth 8 -path '*/skills/*/scripts/blog.py' 2>/dev/null | head -1 | sed 's#/scripts/blog.py##')
python3 "$ZDIR/scripts/blog.py" whoami
# → {"id": "...", "name": "<the connected display name>", "url_token": "<the connected handle>", ...}
On a 401/403 the cookie is expired — tell the user to reconnect at
https://studio.acedata.cloud/console/connectors (re-capture with the ACE
extension). Do not retry in a loop.
Reading recipes
| Goal | Command |
|---|---|
| Who am I | python3 "$BLOG" whoami |
| My latest articles + vote/comment counts | python3 "$BLOG" articles --limit 20 |
| My latest answers + like/favorite/comment counts | python3 "$BLOG" answers --limit 20 |
| Next page (any list) | add --offset 20 |
| One article's stats | python3 "$BLOG" article <id> |
| One answer's stats (incl. 赞同 voteup) | python3 "$BLOG" answer <id> |
| A question's info + my answer id (if any) | python3 "$BLOG" question <id> |
Article stats: voteup_count (赞同), comment_count (评论). Zhihu does not
expose per-article read counts on these endpoints.
Answer stat caveat: the answers list endpoint does not return
voteup_count (赞同) — it only exposes like_count (喜欢), favorite_count
(收藏) and comment_count. For the authoritative 赞同 count of an answer, call
answer <id> (the single-answer endpoint returns voteup_count). question <id>
reports already_answered + my_answer_id so you know whether to use
answer-question (new) or edit-answer (update).
Publishing — GATED (dry-run unless trailing --confirm)
publish writes to the user's real account. Without a trailing --confirm it
dry-runs (prints what it would do, changes nothing). --confirm is honored
only as the last argument, so a title/content containing "--confirm" can
never silently go live. Always show the dry-run to the user, get an explicit
"yes", then re-run with --confirm last.
# Content is HTML. For Markdown, convert to HTML first (e.g. `pandoc -f gfm -t html`).
python3 "$BLOG" publish --title "标题" --content-file article.html # dry-run
python3 "$BLOG" publish --title "标题" --content-file article.html --draft-only --confirm # save a private draft
python3 "$BLOG" publish --title "标题" --content-file article.html --confirm # PUBLIC, goes live
--draft-onlystops after saving a private draft (safe — nothing public).- Without
--draft-only, the article is published publicly under the user's name. Default to--draft-onlyunless the user clearly asked to go live. - Images are auto-hosted. Zhihu strips any
<img>whosesrcis not on its own CDN, so on--confirmthe CLI re-uploads every external image (HTML<img src>and Markdown, plusdata:URIs) to Zhihu's image service and rewrites the URLs first — images already on*.zhimg.comare left untouched. The result reportsimages: {found, rehosted, failed}; the dry-run reportsimages_found. Pass--no-imagesto skip this. So you can hand the CLI HTML/Markdown with normal public image URLs and the pictures survive.
Answering questions — GATED (dry-run unless trailing --confirm)
Two write commands cover the question/answer side. Both gate exactly like
publish: no trailing --confirm → dry-run; --confirm is honored only
as the last argument. Always show the dry-run, get an explicit "yes", then
re-run with --confirm last.
# Content is HTML (same as articles). For Markdown, convert to HTML first.
# Post a NEW answer to a question
python3 "$BLOG" answer-question --question <qid> --content-file ans.html # dry-run
python3 "$BLOG" answer-question --question <qid> --content-file ans.html --draft-only --confirm # PRIVATE draft (safe)
python3 "$BLOG" answer-question --question <qid> --content-file ans.html --confirm # PUBLIC, goes live
# Edit an EXISTING answer (replaces its live, public content)
python3 "$BLOG" edit-answer --id <answer-id> --content-file ans.html # dry-run
python3 "$BLOG" edit-answer --id <answer-id> --content-file ans.html --confirm # overwrites live answer
- One answer per question. Zhihu allows a single answer per user per
question. If the user already answered,
answer-question --confirmreturns a clear error telling you to useedit-answerinstead — find the existing answer id withanswersorquestion <qid>(which reportsmy_answer_id). --draft-onlyis the safe path for new answers — it saves a private draft on the question (nothing public). The user reviews it on Zhihu, then you re-run without--draft-only(with--confirm) to publish. Prefer this unless the user clearly asked to go live immediately.edit-answerhas no private mode — the answer is already public, so any--confirmedit is live immediately. It preserves the answer's current repost setting unless you override with--repost allowed|disallowed.- 转载授权 (
--repost allowed|disallowed): new answers default todisallowed(don't grant repost rights); editing keeps the current setting. - Images are auto-hosted for answers too — same behavior and
--no-imagesflag aspublish.
Gotchas — surface before the user is surprised
- This is the user's real Zhihu account. Confirm before any publish / answer / edit; reading exposes their own private drafts.
- Cookie expiry: Zhihu cookies are short-lived. A
401/403means reconnect at studio.acedata.cloud/console/connectors — never loop-retry. - ToS: cookie automation is against most platforms' terms. This only ever acts on the user's own account with their own captured cookie; the user owns that risk. Never use it to scrape other people's content at scale.
- Never print
ZHIHU_COOKIES— it is full account access. - Scope: Zhihu only. Other Chinese platforms (掘金 / CSDN / …) ship as their
own per-platform skills (e.g.
csdn,juejin), each with its own connector — not a--platformswitch here.
Record the output
After you successfully publish and obtain the live result URL, call the built-in
publish_artifact tool ONCE so the user can track this deliverable in My Outputs:
publish_artifact(kind="article", channel="zhihu", title="<title>", url="<the REAL returned URL>", status="delivered")
Use the real returned URL — never fabricate one. Call it once per published item,
only after delivery is confirmed; skip it (or use status="failed") if publishing failed.
See _shared/artifacts.md.