feat: add initial skill authoring workspace

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
木炎
2026-04-02 18:34:56 +08:00
parent a461b0734e
commit 51913555ad
30 changed files with 7114 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
---
name: zhihu-write
description: Use when the user wants to draft, fill, or publish a Zhihu article through browser actions in the Zhihu editor or creator center.
version: 0.1.0
author: sgclaw
tags:
- zhihu
- browser
- writing
---
# Zhihu Write
Draft or publish a Zhihu article through the creator center and editor flow. Use this skill for filling article title and body, entering the Zhihu editor, and confirming the publish sequence. Do not use it for generic page navigation or hotlist extraction.
## When to Use
- The user asks to write, draft, fill, edit, or publish a Zhihu article.
- The task requires entering the Zhihu editor from creator center or the direct write page.
- The task requires browser-side verification that the article title or publish URL is correct.
Do not use this skill for:
- opening ordinary Zhihu pages without editing content
- collecting hotlist or comment metrics
- bulk content generation without a clear article title and body
## Workflow
1. Confirm the article inputs exist and are non-empty: title and body are both required.
2. Decide whether the run is draft-only or publish mode.
3. In the SuperRPA browser host, call the packaged browser-script tools before any generic browser probing:
- `zhihu-write.prepare_article_editor`
- `zhihu-write.fill_article_draft`
4. Enter the editor flow described in [editor-flow.md](references/editor-flow.md).
5. Fill the title first, then the body.
6. If the task is publish mode, require explicit human confirmation before clicking publish.
7. After publish, verify the final state using title text and published URL when available.
8. If a brittle selector is involved, revalidate it using [publish-safety.md](references/publish-safety.md) before acting.
9. If the editor is blocked by login, verification, or missing permissions, report that explicit state instead of continuing generic probing.
10. Once draft or publish verification succeeds, stop exploratory browser work.
## SuperRPA Interface Contract
- Inside the sgClaw browser host, prefer `superrpa_browser` for Zhihu editor actions. `browser_action` is only the compatibility alias.
- Always pass `expected_domain` as the bare hostname only, for example `www.zhihu.com`.
- All selectors must be valid CSS selectors because the host executes `document.querySelector(...)`.
- Never use XPath or jQuery-style pseudo-selectors such as `:contains(...)`.
- Prefer the packaged browser-script tools over ad-hoc `getText` or `click` probing.
- In the BrowserAttached host, use canonical `www.zhihu.com` creator routes first.
- Do not navigate to `zhuanlan.zhihu.com` unless the host policy explicitly allows that domain.
- Do not retry multiple weak editor selectors when a canonical creator/editor route is available.
## Confirmation Rule
- Draft-only mode does not require a publish confirmation gate.
- Publish mode always requires an explicit confirmation from the human operator in the current session.
- If the users wording is ambiguous between draft and publish, default to draft and ask before publishing.
## Output
Return a concise result with:
- article title
- mode: `draft` or `publish`
- editor entry path used
- final URL if one was captured
- verification result
- unresolved issues or brittle points encountered
- whether login/verification/permission state blocked the requested action
## References
- Use [editor-flow.md](references/editor-flow.md) for the action sequence and verification steps.
- Use [publish-safety.md](references/publish-safety.md) before any live publish click.
- Use `assets/zhihu_write_flow.source.json` when the references need exact selector or step names from the source flow.
## Common Mistakes
- Publishing when the user only asked for a draft.
- Treating a captured edit URL as proof of successful publication.
- Ignoring title verification after the publish confirmation flow.
- Reusing brittle selectors without checking whether a better semantic selector now exists.

View File

@@ -0,0 +1,34 @@
[skill]
name = "zhihu-write"
description = "Use when the user wants to draft, fill, or publish a Zhihu article through browser actions in the Zhihu editor or creator center."
version = "0.1.0"
author = "sgclaw"
tags = ["zhihu", "browser", "writing"]
prompts = [
"For Zhihu article drafting or publishing inside the SuperRPA browser host, call zhihu-write.prepare_article_editor before any generic browser getText, click, or selector probing.",
"If zhihu-write.prepare_article_editor reports editor_ready, call zhihu-write.fill_article_draft with the title, body, and publish_mode arguments instead of generating ad-hoc browser selectors.",
"Do not use zhuanlan.zhihu.com inside this BrowserAttached host unless the host policy explicitly allows it. Prefer canonical www.zhihu.com creator routes.",
"If the user asked to publish but has not explicitly confirmed publishing in the current conversation, stop and ask for confirmation before any publish click.",
"Never generate jQuery-style :contains() selectors. Use the packaged browser scripts before any generic browser probing."
]
[[tools]]
name = "prepare_article_editor"
description = "Detect whether the current Zhihu page is login-blocked, creator-home, or editor-ready on www.zhihu.com before article drafting or publishing."
kind = "browser_script"
command = "scripts/prepare_article_editor.js"
[tools.args]
desired_mode = "Requested mode such as draft or publish."
[[tools]]
name = "fill_article_draft"
description = "Fill the current Zhihu article editor with title and body, and optionally continue the publish flow when explicit confirmation is already present."
kind = "browser_script"
command = "scripts/fill_article_draft.js"
[tools.args]
title = "Article title to write into the Zhihu editor."
body = "Article body to write into the Zhihu editor."
publish_mode = "Use true only when explicit human confirmation to publish is already present in the current conversation."

View File

@@ -0,0 +1,126 @@
{
"entry_url": "https://www.zhihu.com/creator",
"editor_url": "https://zhuanlan.zhihu.com/write",
"domains": {
"creator": "www.zhihu.com",
"editor": "zhuanlan.zhihu.com"
},
"literals": {
"write_entry_text": "写文章",
"title_placeholder": "请输入标题(最多 100 个字)",
"body_role": "textbox",
"publish_text": "发布",
"publish_confirm_text": "确认发布"
},
"selectors": {
"creator_write_panel": "div.css-1q62b6s",
"creator_write_entry": "div.css-1q62b6s > div.css-byu4by",
"title_input": "textarea[placeholder='请输入标题(最多 100 个字)']",
"body_editor": "div.notranslate.public-DraftEditor-content[contenteditable='true'][role='textbox']",
"publish_button": "button.Button--primary.Button--blue",
"publish_confirm_dialog": "div[role='dialog']",
"publish_confirm_button": "div[role='dialog'] button.Button--primary.Button--blue",
"published_title": "h1"
},
"steps": [
{
"name": "navigate_creator",
"action": "navigate",
"expected_domain": "creator",
"url_ref": "entry_url",
"log_message": "navigate https://www.zhihu.com/creator"
},
{
"name": "click_write_article",
"action": "click",
"expected_domain": "creator",
"selector_ref": "creator_write_entry",
"wait_after_ms": 1500,
"log_message": "click 写文章"
},
{
"name": "wait_editor_ready",
"action": "waitForSelector",
"expected_domain": "editor",
"selector_ref": "title_input",
"timeout_ms": 8000,
"log_message": "wait for editor title input"
},
{
"name": "type_title",
"action": "type",
"expected_domain": "editor",
"selector_ref": "title_input",
"text_source": "title",
"clear_first": true,
"log_message": "type article title into 请输入标题(最多 100 个字)"
},
{
"name": "type_body",
"action": "type",
"expected_domain": "editor",
"selector_ref": "body_editor",
"text_source": "body",
"clear_first": true,
"log_message": "type article body into editor textbox"
},
{
"name": "scroll_publish_button",
"action": "scrollTo",
"expected_domain": "editor",
"selector_ref": "publish_button",
"only_when_publish": true,
"log_message": "scroll to 发布"
},
{
"name": "click_publish",
"action": "click",
"expected_domain": "editor",
"selector_ref": "publish_button",
"wait_after_ms": 800,
"only_when_publish": true,
"capture_url": true,
"log_message": "click 发布"
},
{
"name": "wait_publish_confirm_dialog",
"action": "waitForSelector",
"expected_domain": "editor",
"selector_ref": "publish_confirm_dialog",
"timeout_ms": 8000,
"only_when_publish": true,
"log_message": "wait for publish confirm dialog"
},
{
"name": "click_publish_confirm",
"action": "click",
"expected_domain": "editor",
"selector_ref": "publish_confirm_button",
"wait_after_ms": 1500,
"only_when_publish": true,
"capture_url": true,
"log_message": "click 确认发布"
},
{
"name": "wait_published_title",
"action": "waitForSelector",
"expected_domain": "editor",
"selector_ref": "published_title",
"timeout_ms": 15000,
"only_when_publish": true,
"capture_url": true,
"log_message": "wait for published article title"
},
{
"name": "confirm_published_title",
"action": "getText",
"expected_domain": "editor",
"selector_ref": "published_title",
"only_when_publish": true,
"expect_text_source": "title",
"allow_empty_text": true,
"capture_url": true,
"log_message": "verify published article title"
}
]
}

View File

@@ -0,0 +1,53 @@
# Editor Flow
This skill is based on the preserved source flow in `assets/zhihu_write_flow.source.json`.
## Entry Points
- Creator center entry URL: `https://www.zhihu.com/creator`
- BrowserAttached direct editor URL: `https://www.zhihu.com/creator/posts/new`
`https://zhuanlan.zhihu.com/write` exists in the preserved source flow, but it is not the default entry point inside the current SuperRPA browser host because the host domain policy only guarantees `www.zhihu.com`.
The current BrowserAttached flow enters creator center first, then uses the packaged browser-script tools to resolve whether the session is blocked by login, already in the editor, or needs the canonical creator editor route.
## Required Inputs
- `title`
- `body`
Both fields must be non-empty before any browser action starts.
## Core Sequence
1. Navigate to creator center.
2. Click the write-article entry.
3. Wait for the title input in the editor domain.
4. Fill the title with `clear_first = true`.
5. Fill the body editor with `clear_first = true`.
6. If publish mode:
- scroll to the publish button
- click publish
- wait for publish confirmation dialog
- click confirm publish
- wait for published title
- verify published title text
## Readiness Checks
- The editor is considered ready only after the title input appears.
- The publish flow is not complete until at least one post-publish verification succeeds.
## URL Capture Rules
- Pre-publish clicks may return an editor URL.
- A valid published article URL should match the published article prefix and should not end in `/edit`.
- If publish mode finishes without a published article URL, treat the run as unconfirmed even if some clicks succeeded.
## Known Brittle Points
- creator-center article entry selector
- placeholder-based title input selector
- generic primary-button publish selectors
These should be revalidated before any live publish run.

View File

@@ -0,0 +1,49 @@
# Publish Safety
Publishing is the highest-risk action in this skill. Treat it as a gated operation.
## Mandatory Safety Rules
- Do not publish without explicit human confirmation in the current conversation.
- Do not assume `publish: true` in an old request still reflects the users latest intent.
- Do not treat a successful click as proof of publication.
## What Must Be Verified
At least one of these post-publish checks should succeed:
- the final URL is a published Zhihu article URL
- the visible page title matches the requested article title
If both checks fail, report the run as unconfirmed.
## Failure Cases
### Title verification fails
- Stop.
- Report the expected title and the observed title.
- Do not claim the article was published correctly.
### URL remains in edit mode
- Treat the result as draft or unconfirmed publish.
- Report that the browser stayed on an editor-style URL.
- Ask for manual review before any retry.
### Publish dialog does not appear
- Do not retry blindly on generic primary buttons.
- Report that the dialog selector failed.
- Revalidate selectors and page state first.
## Brittle Selectors To Revalidate First
- `div.css-1q62b6s > div.css-byu4by`
- `textarea[placeholder='请输入标题(最多 100 个字)']`
- `button.Button--primary.Button--blue`
- `div[role='dialog'] button.Button--primary.Button--blue`
- `h1`
These are usable as source references, but not trustworthy forever.

View File

@@ -0,0 +1,179 @@
function cleanText(value) {
return String(value || '')
.replace(/\s+/g, ' ')
.replace(/\u200b/g, '')
.trim();
}
function pageText() {
const body = document.body;
return cleanText(body && (body.innerText || body.textContent || ''));
}
function isLoginBlocked(url, text) {
return /\/signin\b|\/signup\b/.test(url) ||
/登录|注册|验证码|安全验证|验证后继续|请先登录/.test(text);
}
function isVisible(node) {
if (!node) {
return false;
}
const rect = typeof node.getBoundingClientRect === 'function' ? node.getBoundingClientRect() : null;
return !rect || rect.width > 0 || rect.height > 0;
}
function attrText(node, name) {
if (!node || typeof node.getAttribute !== 'function') {
return '';
}
return cleanText(node.getAttribute(name) || '');
}
function looksLikeTitleInput(node) {
const signals = [
attrText(node, 'placeholder'),
attrText(node, 'data-placeholder'),
attrText(node, 'aria-label'),
].filter(Boolean);
return signals.some((value) => value.includes('标题'));
}
function dispatchTextInput(node) {
node.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
node.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
}
function fillInput(node, value) {
node.focus();
if ('value' in node) {
node.value = '';
dispatchTextInput(node);
node.value = value;
dispatchTextInput(node);
return;
}
node.textContent = value;
dispatchTextInput(node);
}
function fillEditable(node, value) {
node.focus();
node.innerHTML = '';
const lines = String(value || '').split(/\n/);
lines.forEach((line, index) => {
if (index > 0) {
node.appendChild(document.createElement('br'));
}
node.appendChild(document.createTextNode(line));
});
dispatchTextInput(node);
}
function findVisible(selectors) {
for (const selector of selectors) {
const nodes = Array.from(document.querySelectorAll(selector));
const match = nodes.find((node) => {
return isVisible(node);
});
if (match) {
return match;
}
}
return null;
}
function findBodyEditor(titleInput) {
for (const selector of [
"div[contenteditable='true'][role='textbox']",
"div.public-DraftEditor-content[contenteditable='true']",
"[role='textbox'][contenteditable='true']",
"[contenteditable='true'][data-placeholder]",
"div[contenteditable='true']",
]) {
const nodes = Array.from(document.querySelectorAll(selector));
const match = nodes.find((node) => isVisible(node) && node !== titleInput && !looksLikeTitleInput(node));
if (match) {
return match;
}
}
return null;
}
function findButtonByText(fragment) {
const candidates = Array.from(document.querySelectorAll("button, [role='button'], a"));
const wanted = cleanText(fragment);
return candidates.find((node) => {
const text = cleanText(node.textContent);
if (!text || !text.includes(wanted)) {
return false;
}
return isVisible(node);
}) || null;
}
const currentUrl = location.href;
const text = pageText();
if (isLoginBlocked(currentUrl, text)) {
return {
status: 'login_required',
current_url: currentUrl,
};
}
const titleInput = findVisible([
"textarea[placeholder*='标题']",
"input[placeholder*='标题']",
"textarea[data-placeholder*='标题']",
"input[data-placeholder*='标题']",
"[role='textbox'][aria-label*='标题']",
"[contenteditable='true'][aria-label*='标题']",
"[contenteditable='true'][data-placeholder*='标题']",
]);
const bodyEditor = findBodyEditor(titleInput);
if (!titleInput || !bodyEditor) {
return {
status: 'editor_not_ready',
current_url: currentUrl,
};
}
fillInput(titleInput, String(args.title || ''));
fillEditable(bodyEditor, String(args.body || ''));
const publishMode = String(args.publish_mode || '').toLowerCase() === 'true';
if (!publishMode) {
return {
status: 'draft_ready',
current_url: currentUrl,
title: cleanText(args.title),
};
}
const publishButton = findButtonByText('发布');
if (!publishButton) {
return {
status: 'publish_button_missing',
current_url: currentUrl,
title: cleanText(args.title),
};
}
publishButton.click();
const confirmButton = findButtonByText('确认发布');
if (!confirmButton) {
return {
status: 'publish_clicked',
current_url: currentUrl,
title: cleanText(args.title),
};
}
confirmButton.click();
return {
status: 'publish_submitted',
current_url: currentUrl,
title: cleanText(args.title),
};

View File

@@ -0,0 +1,106 @@
function cleanText(value) {
return String(value || '')
.replace(/\s+/g, ' ')
.replace(/\u200b/g, '')
.trim();
}
function pageText() {
const body = document.body;
return cleanText(body && (body.innerText || body.textContent || ''));
}
function isLoginBlocked(url, text) {
return /\/signin\b|\/signup\b/.test(url) ||
/登录|注册|验证码|安全验证|验证后继续|请先登录/.test(text);
}
function isVisible(node) {
if (!node) {
return false;
}
const rect = typeof node.getBoundingClientRect === 'function' ? node.getBoundingClientRect() : null;
return !rect || rect.width > 0 || rect.height > 0;
}
function attrText(node, name) {
if (!node || typeof node.getAttribute !== 'function') {
return '';
}
return cleanText(node.getAttribute(name) || '');
}
function looksLikeTitleInput(node) {
const signals = [
attrText(node, 'placeholder'),
attrText(node, 'data-placeholder'),
attrText(node, 'aria-label'),
].filter(Boolean);
return signals.some((value) => value.includes('标题'));
}
function findVisibleTitleInput() {
const selectors = [
"textarea[placeholder*='标题']",
"input[placeholder*='标题']",
"textarea[data-placeholder*='标题']",
"input[data-placeholder*='标题']",
"[role='textbox'][aria-label*='标题']",
"[contenteditable='true'][aria-label*='标题']",
"[contenteditable='true'][data-placeholder*='标题']",
];
for (const selector of selectors) {
const node = document.querySelector(selector);
if (isVisible(node)) {
return node;
}
}
return null;
}
function findBodyEditor(titleInput) {
const selectors = [
"div[contenteditable='true'][role='textbox']",
"div.public-DraftEditor-content[contenteditable='true']",
"[role='textbox'][contenteditable='true']",
"[contenteditable='true'][data-placeholder]",
"div[contenteditable='true']",
];
for (const selector of selectors) {
const nodes = Array.from(document.querySelectorAll(selector));
const visible = nodes.find((node) => {
return isVisible(node) && node !== titleInput && !looksLikeTitleInput(node);
});
if (visible) {
return visible;
}
}
return null;
}
const currentUrl = location.href;
const text = pageText();
if (isLoginBlocked(currentUrl, text)) {
return {
status: 'login_required',
current_url: currentUrl,
};
}
const titleInput = findVisibleTitleInput();
const bodyEditor = findBodyEditor(titleInput);
if (titleInput && bodyEditor) {
return {
status: 'editor_ready',
current_url: currentUrl,
title_placeholder: titleInput.getAttribute('placeholder') || '',
};
}
return {
status: 'editor_unavailable',
current_url: currentUrl,
desired_mode: String(args.desired_mode || 'draft'),
};