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-navigate
description: Use when the user wants to open, switch, or navigate to a Zhihu page, tab, menu, profile area, notification area, message area, or creator area through browser actions.
version: 0.1.0
author: sgclaw
tags:
- zhihu
- browser
- navigation
---
# Zhihu Navigate
Open or switch to known Zhihu destinations through browser navigation steps. Use this skill for page routing, menu opening, profile-area switching, and creator-center entry, not for article authoring or hotlist data extraction.
## When to Use
- The user asks to open a Zhihu page such as home, hot list, notifications, messages, search, creator center, or write article.
- The user asks to switch to a specific Zhihu tab, section, or profile sub-area.
- The user asks to open a Zhihu menu such as the avatar menu or notifications menu.
- The task is browser navigation inside Zhihu and the desired destination is one of the known catalog targets.
Do not use this skill for:
- writing or publishing Zhihu articles
- collecting hotlist data or producing reports
- arbitrary web browsing outside the known Zhihu catalog
## Workflow
1. Classify the request as one of these destination types:
- route: direct page URL
- component: clickable entry or tab
- flow: multi-step navigation such as open menu, then open a nested destination
2. Match the requested destination against the known catalog in [routes-and-targets.md](references/routes-and-targets.md).
3. Prefer the most explicit target name available in the request.
4. If the request maps to a known ambiguous alias, stop and ask for clarification instead of guessing.
5. Use semantic selectors first and revalidate brittle selectors using [selector-strategy.md](references/selector-strategy.md) before relying on them.
6. In the SuperRPA browser host, use the packaged browser-script tool `zhihu-navigate.open_creator_entry` before generic probing when the target is creator center or article entry.
7. After navigation, verify the destination using URL, domain, or visible text whenever the page exposes a stable signal.
8. If Zhihu redirects to login, captcha, or verification state, report that block explicitly instead of pretending the target page was reached.
9. Once the target page is verified, stop exploratory click probing and hand off to the next skill if collection or editing is required.
## SuperRPA Interface Contract
- Inside the sgClaw browser host, prefer `superrpa_browser` for Zhihu routing and DOM interactions. `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 tool over ad-hoc probing for creator/article entry.
- Prefer direct route navigation to known canonical URLs before brittle click chains.
- Do not cycle through multiple weak selectors when a canonical route is available.
## Ambiguity Rules
- Treat `关注分栏` as ambiguous until the user says whether they mean home feed following or profile following.
- Treat `回答排序菜单` as ambiguous until the user says whether they mean question page sorting or answer page sorting.
- If two targets score equally, ask a direct clarification question instead of falling back to the first match.
## Output
Return a concise result with:
- requested destination
- resolved target key
- navigation type: route, component, or flow
- expected domain
- final URL when available
- verification result or uncertainty
- whether a login/verification redirect blocked the intended destination
## References
- Use [routes-and-targets.md](references/routes-and-targets.md) to identify the intended target and flow shape.
- Use [selector-strategy.md](references/selector-strategy.md) when a selector looks weak, overly generic, or DOM-version-dependent.
- Use the preserved source catalog in `assets/zhihu_navigation_pages.source.json` only when a reference file does not contain enough detail.
## Common Mistakes
- Confusing page navigation with article-writing actions.
- Treating menu opening and page opening as the same kind of operation.
- Ignoring alias collisions and silently picking one target.
- Trusting a generic selector without checking whether it still maps to the intended control.

View File

@@ -0,0 +1,22 @@
[skill]
name = "zhihu-navigate"
description = "Use when the user wants to open, switch, or navigate to a Zhihu page, tab, menu, profile area, notification area, message area, or creator area through browser actions."
version = "0.1.0"
author = "sgclaw"
tags = ["zhihu", "browser", "navigation"]
prompts = [
"For Zhihu creator-center or write-article navigation inside the SuperRPA browser host, call zhihu-navigate.open_creator_entry before any generic browser getText, click, or selector probing.",
"zhihu-navigate.open_creator_entry is the deterministic path for detecting login blocks, creator-home state, and editor-ready state on www.zhihu.com.",
"Do not use zhuanlan.zhihu.com inside this BrowserAttached host unless the host policy explicitly allows it. Prefer canonical www.zhihu.com creator routes.",
"Never generate jQuery-style :contains() selectors. If the packaged browser script is available, use it before any generic browser probing."
]
[[tools]]
name = "open_creator_entry"
description = "Inspect the current Zhihu page, detect login or creator/editor state, and resolve the stable creator/article-entry path on www.zhihu.com before generic probing."
kind = "browser_script"
command = "scripts/open_creator_entry.js"
[tools.args]
desired_target = "Requested target such as creator_home or article_editor."

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,75 @@
# Routes And Targets
This skill is derived from the remote navigation catalog in `assets/zhihu_navigation_pages.source.json`.
## Current Scope
- Routes: 13
- Components: 53
- Flows: 16
- Targets: 69
## Model
- `route`: direct page destination with a known URL
- `component`: clickable entry, tab, button, or menu control
- `flow`: multi-step sequence composed from routes and components
- `target`: user-facing entry point that resolves to exactly one route, component, or flow
## Representative Routes
- `home`
- `hot_list`
- `notifications_page`
- `messages_page`
- `search_results_page`
- `creator_home`
- `write_article`
## Representative Components
- `top_nav_home`
- `top_nav_hot`
- `top_nav_creator`
- `top_nav_notifications`
- `top_nav_avatar_menu`
- `notifications_tab_replies`
- `messages_all_tab`
## Representative Flows
- `open_avatar_menu`
- `open_notifications_menu`
- `open_creator_from_home`
- `open_profile_from_avatar_menu`
- `open_profile_answers_tab`
- `open_profile_articles_tab`
- `open_security_settings_from_avatar_menu`
## Confirmed Alias Conflicts
These aliases currently resolve to more than one target and must not be guessed:
| Alias | Conflicting targets |
| --- | --- |
| `关注分栏` | `home_feed_following_tab`, `profile_following_tab` |
| `回答排序菜单` | `answer_sort_menu`, `question_sort_menu` |
## Preferred Disambiguation Wording
When the user uses an ambiguous alias, ask for the missing context directly:
- `你说的“关注分栏”是首页关注流,还是个人主页里的关注分栏?`
- `你说的“回答排序菜单”是问题页的排序菜单,还是回答列表的排序菜单?`
## Practical Routing Rule
Prefer the most explicit phrase in this order:
1. exact target name
2. exact alias
3. explicit page + area combination
4. generic area noun only
If step 3 or 4 still matches more than one target, ask before acting.

View File

@@ -0,0 +1,59 @@
# Selector Strategy
The source catalog already mixes stable and brittle selectors. Use this order when validating or refreshing selectors.
## Preferred Order
1. Stable `href` selectors for direct links
2. `aria-label` and `role` selectors for tabs, menus, and buttons
3. `data-testid` selectors when available
4. Stable semantic class names tied to product structure
5. Generic class selectors only as a last resort
6. CSS hash classes only when no better hook exists
## Good Patterns In The Current Catalog
- `a[href='/']`
- `a[href='/hot']`
- `a[href='/creator']`
- `button[aria-label='通知']`
- `[role='tab'][aria-label*='回复']`
- `[data-testid='sort-button']`
These are relatively resilient because they describe user-facing semantics instead of transient layout implementation.
## Known Brittle Or Weak Patterns
- `div.css-1q62b6s > div.css-byu4by`
- `button:has(img)`
- `.MoreButton`
- `.Popover`
- `.Tooltip`
- `.floating-menu`
- `.Modal`
- `.Dialog`
Risks:
- hash classes can change on any frontend build
- generic popup selectors can match the wrong layer
- image-based button matching is vulnerable to layout and icon changes
## Revalidation Rule
Before relying on a weak selector:
1. Check whether an `href`, `aria-label`, `role`, or `data-testid` selector now exists.
2. Confirm the selector matches exactly one intended element.
3. Confirm the element is visible and actionable in the current page state.
4. If the selector is still generic, pair it with a stronger page-context check before acting.
## Failure Handling
If a weak selector stops working:
- do not silently substitute another generic selector
- report which selector failed
- describe the page context where it failed
- request a selector refresh or DOM inspection before retrying

View File

@@ -0,0 +1,137 @@
function cleanText(value) {
return String(value || '')
.replace(/\s+/g, ' ')
.replace(/\u200b/g, '')
.trim();
}
function bodyText() {
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 hasEditorSignals() {
return !!document.querySelector(
"textarea[placeholder*='标题'], input[placeholder*='标题'], div[contenteditable='true'][role='textbox']"
);
}
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 isWriteEntryText(text) {
return !!text && (text.includes('写文章') || text.includes('发文章'));
}
function extractHref(node) {
if (!node) {
return '';
}
if (typeof node.href === 'string' && node.href) {
return node.href;
}
if (typeof node.getAttribute === 'function') {
return node.getAttribute('href') || '';
}
return '';
}
function findClickableAncestor(node) {
let current = node;
while (current) {
const tagName = String(current.tagName || '').toUpperCase();
const role = typeof current.getAttribute === 'function' ? cleanText(current.getAttribute('role')) : '';
const tabindex = typeof current.getAttribute === 'function' ? current.getAttribute('tabindex') : null;
const href = extractHref(current);
if (
tagName === 'A' ||
tagName === 'BUTTON' ||
role === 'button' ||
href ||
(tabindex !== null && tabindex !== '')
) {
return current;
}
current = current.parentElement || null;
}
return null;
}
function findWriteEntry() {
const directCandidates = Array.from(
document.querySelectorAll("a[href], button, [role='button']")
);
const directMatch = directCandidates.find((node) => {
const text = cleanText(node.textContent);
if (!isWriteEntryText(text)) {
return false;
}
return isVisible(node);
});
if (directMatch) {
return directMatch;
}
const textNodes = Array.from(
document.querySelectorAll("div, span, [tabindex]")
);
for (const node of textNodes) {
if (!isVisible(node)) {
continue;
}
const text = cleanText(node.textContent);
if (!isWriteEntryText(text)) {
continue;
}
const clickableAncestor = findClickableAncestor(node);
if (clickableAncestor && isVisible(clickableAncestor)) {
return clickableAncestor;
}
}
return null;
}
const currentUrl = location.href;
const text = bodyText();
if (isLoginBlocked(currentUrl, text)) {
return {
status: 'login_required',
current_url: currentUrl,
};
}
if (hasEditorSignals()) {
return {
status: 'editor_ready',
current_url: currentUrl,
};
}
const writeEntry = findWriteEntry();
if (writeEntry) {
writeEntry.click();
const href = extractHref(writeEntry);
return {
status: 'creator_entry_clicked',
current_url: currentUrl,
next_url: href || undefined,
};
}
return {
status: 'creator_home',
current_url: currentUrl,
desired_target: String(args.desired_target || 'creator_home'),
};