feat: add initial skill authoring workspace
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
83
skills/zhihu-navigate/SKILL.md
Normal file
83
skills/zhihu-navigate/SKILL.md
Normal 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.
|
||||
22
skills/zhihu-navigate/SKILL.toml
Normal file
22
skills/zhihu-navigate/SKILL.toml
Normal 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."
|
||||
2481
skills/zhihu-navigate/assets/zhihu_navigation_pages.source.json
Normal file
2481
skills/zhihu-navigate/assets/zhihu_navigation_pages.source.json
Normal file
File diff suppressed because it is too large
Load Diff
75
skills/zhihu-navigate/references/routes-and-targets.md
Normal file
75
skills/zhihu-navigate/references/routes-and-targets.md
Normal 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.
|
||||
|
||||
59
skills/zhihu-navigate/references/selector-strategy.md
Normal file
59
skills/zhihu-navigate/references/selector-strategy.md
Normal 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
|
||||
|
||||
137
skills/zhihu-navigate/scripts/open_creator_entry.js
Normal file
137
skills/zhihu-navigate/scripts/open_creator_entry.js
Normal 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'),
|
||||
};
|
||||
Reference in New Issue
Block a user