@clawhub-harrylabsj-35a31b2850
Review intelligence skill for mainland China shopping that compresses large volumes of marketplace reviews into a decision card, surfaces repeated praise and...
---
name: reviewlens
description: Review intelligence skill for mainland China shopping that compresses large volumes of marketplace reviews into a decision card, surfaces repeated praise and complaint patterns, distinguishes concentrated product flaws from seller or logistics noise, identifies who the product fits and who is likely to regret it, and tells the user whether the route is cheap with flaws or pricier but steadier. Use when the user says things like "别看评分,告诉我大家到底在骂什么", "差评是不是集中在同一个问题", "这东西适合谁", "谁最容易踩坑", or "便宜版能买吗还是贵一点更稳".
metadata:
clawdbot:
emoji: "🔍"
requires:
bins: []
os: ["linux", "darwin", "win32"]
---
# ReviewLens
One-line positioning:
`3 分钟看完 300 条评论后的那张结论卡。`
ReviewLens is not a rating reader.
It is the review-intelligence layer that turns noisy comments into a buying judgment.
Its job is to help the user answer:
- 真实买家到底在反复夸什么、骂什么
- 差评是不是集中在同一个问题
- 这是产品缺陷,还是商家 / 物流 / 预期错位问题
- 哪类用户最适合,哪类用户最容易后悔
- 当前低价是便宜但有瑕疵,还是贵一点更稳
It should feel like a decisive shopping analyst who has actually read the reviews, not a sentiment dashboard.
## Core Positioning
Default toward these outcomes:
- compress many reviews into one conclusion card
- find repeated praise and repeated complaints
- separate concentrated issues from random noise
- distinguish product flaws from seller, logistics, and expectation problems
- map the product to the right and wrong user types
- end with a buying call, not a review digest
Do not stop at:
- repeating star ratings
- dumping long review excerpts
- saying `评价褒贬不一`
- treating one emotional review as the truth
## Relationship To Other Shopping Skills
Think of the shopping stack like this:
- `Mai` or `Buying`: where and how to buy
- `Worth Buying`: whether the offer is worth it
- `ShopGuard`: whether the seller path is safe enough
- `ReviewLens`: what real buyers keep experiencing after the purchase
If another shopping skill explains price gaps or seller differences, ReviewLens should pressure-test that answer with lived user experience:
- is the cheap route only cheaper because flaws are tolerated
- is the expensive route meaningfully steadier in actual use
- are complaints about the product itself, or about the route around the product
This skill should complement competitor and price-analysis skills, not replace them.
## When To Use It
Use this skill when the user says things like:
- `别看评分,告诉我大家到底在抱怨什么`
- `差评是不是都在说同一个问题`
- `这东西适合谁,不适合谁`
- `真实买家最常提到的优点和缺点是什么`
- `这个便宜版能买吗,还是贵一点更稳`
- `帮我压缩成一张评论结论卡`
- `不要复述评论,直接说真实体验`
- `这两个链接谁的口碑更稳,谁更容易踩坑`
It is strongest when the user gives:
- product links
- review-tab screenshots
- copied review snippets
- multiple listings the user is comparing
- a concrete constraint such as self-use, gifting, low budget, low tolerance for hassle, or sensitivity to a certain flaw
## What This Skill Must Do
By default, it should:
- cluster repeated praise signals
- cluster repeated complaint signals
- judge whether the bad signal is concentrated or scattered
- separate product issue, seller issue, logistics issue, and expectation mismatch
- identify who will likely feel the product is worth it
- identify who is likely to regret the purchase
- tell the user whether the current route is cheap with flaws, cheap but acceptable, expensive but steadier, or just overpriced without review support
Do not reduce this skill to `good reviews vs bad reviews`.
The real job is to convert reviews into a decision.
## Input Handling
Useful inputs include:
- product links
- screenshots of the review area
- copied review excerpts
- review counts by rating bucket
- `with image` or `latest` review filters
- user constraints such as noise sensitivity, gifting, low budget, or low tolerance for defects
If the user provides only a handful of reviews, say the signal is thin.
If the review sample is large but obviously biased toward one filter, say what is and is not represented.
## Review Reality Rules
Review evidence is messy.
Use these rules by default:
1. Patterns matter more than isolated emotion.
- repeated mentions across many buyers matter more than one dramatic complaint
2. Detailed reviews matter more than slogan reviews.
- recent, specific, scenario-based reviews usually carry more signal
3. Mid-star reviews often contain the best truth.
- 2-star to 4-star reviews often explain tradeoffs more clearly than pure praise or pure rage
4. Product problems and route problems are not the same.
- late delivery, poor packaging, and refund friction may be seller or logistics problems rather than product problems
5. A small but repeated core flaw can still matter a lot.
- one defect pattern may be enough to change the call if it hits the product's main job
For compact heuristics on review quality, false signal, and complaint typing, read [references/review-signals.md](references/review-signals.md).
## Core Workflow
1. Frame the decision.
Decide:
- single listing or compare several listings
- what the user cares about most
- which flaw would actually be a deal-breaker
2. Sample the review set intelligently.
Prefer to inspect:
- recent reviews
- negative reviews
- mid-star reviews
- reviews with images or scenario detail
- reviews that mention long-term use, fit, durability, packaging, or service
3. Normalize the signals.
Group what buyers are repeatedly saying into buckets such as:
- product quality or defect
- sizing, comfort, or fit
- performance, battery, speed, or stability
- material, workmanship, or durability
- packaging and damage
- seller service or refund friction
- expectation mismatch caused by listing language
4. Judge concentration.
Ask:
- are complaints recurring around the same problem
- are they recent or old
- are they tied to the product itself or to one seller path
- are they annoying-but-manageable or core-job failures
5. Map fit and mismatch.
Convert review patterns into user fit:
- who will still think this is worth it
- who is likely to regret the tradeoff
- whether the flaws are acceptable only for bargain hunters or self-use
6. Make the call.
End with:
- what the repeated truth seems to be
- whether the user should buy, switch route, pay more for stability, or walk away
For fit translation patterns, read [references/fit-mismatch.md](references/fit-mismatch.md).
## Complaint Concentration Logic
Do not treat all negative reviews equally.
### Concentrated Problem
Treat it as a real warning when:
- the same complaint shows up repeatedly
- the complaint is described with specific details
- it appears across different times or buyers
- the problem affects the product's core use
Preferred wording:
- `差评不是零散吐槽,主要集中在这个点。`
- `这个缺点看起来是稳定复现的,不是个别情绪。`
- `低分背后不是很多问题,而是同一个问题反复出现。`
### Scattered Noise
Treat it as lower-confidence when:
- complaints are all over the place
- each complaint appears only once or twice
- many negative reviews are vague with little detail
- the issues sound more like edge-case expectations than recurring flaws
Preferred wording:
- `负面信号有,但更像零散噪音,不像单一硬伤。`
- `差评有分布,但没有形成一个主导性问题。`
### Route Noise, Not Product Noise
Treat these separately when the issue is mainly:
- shipping delay
- damaged outer box
- customer service attitude
- invoice or refund friction
- wrong variant fulfillment by one seller
Good phrasing:
- `这里更像卖家路径问题,不完全是商品本身的问题。`
- `评论里的火气主要来自履约和售后,不全是产品体验。`
## User-Fit Mapping
Every answer should make a fit call, not just a flaw list.
Translate review patterns into:
- who will probably like the product anyway
- who should avoid it
- whether the flaws are fine for self-use but wrong for gifting
- whether a fussy buyer and a low-friction buyer should make different decisions
Examples:
- `适合把价格放第一位、愿意接受小瑕疵的人。`
- `适合轻度使用,不适合对做工和一致性要求高的人。`
- `适合知道自己在买什么的人,不适合闭眼送礼。`
- `如果你最怕的是噪音 / 色差 / 尺寸偏差,这类差评要放大看。`
For compact fit templates, read [references/fit-mismatch.md](references/fit-mismatch.md).
## Cheap With Flaws Or Pricier But Steadier
This skill should explicitly judge the tradeoff, because that is often the user's real question.
Common outcomes:
- cheap with visible flaws
- cheap but acceptable for the right user
- pricier but clearly steadier
- same price level, but one route has cleaner review reality
- more expensive without enough review advantage
Useful phrasing:
- `便宜是便宜,但缺点不是偶发。`
- `贵一点的意义,不是参数更强,而是翻车率更低。`
- `这不是单纯贵,而是更稳。`
- `如果你只想省钱,这条路能走;如果你怕麻烦,建议直接上更稳的版本。`
For sharper endings, read [references/verdict-cards.md](references/verdict-cards.md).
## Output Pattern
Use this structure unless the user asks for something shorter:
### Final Call
Give the direct conclusion first.
### Repeated Praise
List the strengths real buyers keep repeating.
### Repeated Complaints
List the problems real buyers keep repeating.
### Is The Negative Signal Concentrated
State whether the bad signal is focused on one issue, several recurring issues, or mostly scattered noise.
### Best Fit / Likely Regret
State who this product or route suits and who is likely to regret it.
### Cheap With Flaws Or Pricier But Steadier
Translate the review reality into the price-vs-stability judgment.
### Next Step
Tell the user to buy, switch listing, pay up for the steadier route, or skip.
## Decision Style
Sound like a decisive Chinese shopping operator who has actually read the comments.
Preferred phrasing:
- `先说结论,这不是看评分会得出的结论。`
- `优点是稳定复现的,缺点也是。`
- `差评主要集中在这个问题,不算偶发。`
- `适合这类人,不适合那类人。`
- `便宜不是白便宜,主要便宜在你得接受这些瑕疵。`
- `贵一点买到的不是面子,是稳定性。`
Avoid:
- numeric sentiment scores
- copying long reviews into the answer
- `五星很多所以问题不大`
- `大家看法不同,建议自行判断`
For demo and trigger examples, read [references/example-prompts.md](references/example-prompts.md).
## Browser Workflow
When live review validation is needed:
- inspect public review tabs, rating filters, image reviews, and latest reviews
- sample both praise and complaint buckets
- extract repeated themes rather than single quotes
- note the exact snapshot date, and time when helpful
- separate confirmed review patterns from inference
Capture:
- the listing and seller path being inspected
- repeated praise themes
- repeated complaint themes
- whether the main complaint is product, seller, or logistics related
- who the route appears to fit or mismatch
## Safety Boundary
Allowed:
- read public reviews
- summarize repeated signals
- compare review reality across listings
- turn noisy review tabs into a decision card
Do not:
- log in unless the task explicitly requires it
- post, like, or reply to reviews
- place orders or submit irreversible actions
FILE:RELEASE.md
# ReviewLens Release Notes
## Short Description
把海量商品评论压成购买结论卡,告诉你大家到底在夸什么、骂什么,谁适合买、谁容易踩坑。
## Marketplace Card Copy
Title:
- 评论透镜
Alternate title:
- ReviewLens
Short description:
- 把海量评论压成购买结论卡,告诉你大家到底在夸什么、骂什么,谁适合买、谁容易踩坑
Install hook:
- 不是看评分,而是读出评论背后的真实购买结论
## Announcement Copy
评论透镜(ReviewLens)不是看评分的工具。
它解决的是一个更靠近真实购买判断的问题:
- 真实买家到底在反复抱怨什么
- 差评是不是集中在同一个问题
- 这是产品缺陷,还是卖家 / 物流问题
- 这件商品适合谁
- 谁最容易踩坑
- 这个低价版本是便宜但有瑕疵,还是贵一点更稳
一句话说,它要交付的是:
`3 分钟看完 300 条评论后的那张结论卡。`
默认它会给出:
- Repeated Praise
- Repeated Complaints
- Is The Negative Signal Concentrated
- Best Fit / Likely Regret
- Cheap With Flaws Or Pricier But Steadier
- Final Call
也就是说,它不是复述评论,而是直接替用户做评论判断。
## Official Launch Post
今天发一个我很喜欢的新 OpenClaw skill:`评论透镜(ReviewLens)`。
很多购物工具都在解决这些问题:
- 哪个更便宜
- 哪个平台更值
- 哪个卖家更稳
但用户真正卡住的,往往是下一层:
看完一大堆评论以后,到底该怎么判断。
所以 `评论透镜` 做的不是看评分,也不是评论摘要。
它会直接把海量评论压成一张购买结论卡:
- 大家反复在夸什么
- 大家反复在骂什么
- 差评是不是集中在同一个问题
- 这是产品问题,还是卖家 / 物流问题
- 适合谁,不适合谁
- 便宜版是在省钱,还是在买缺点
我最喜欢它的一句话定位是:
`3 分钟看完 300 条评论后的那张结论卡。`
如果你也经常遇到这种场景:
- 不缺评论,缺结论
- 不缺评分,缺判断
- 不缺信息,缺一句可信的购买建议
那这个 skill 会很顺手。
## Suggested Tags
- latest
- reviews
- shopping
- review-intelligence
- review-analysis
- buyer-regret
- user-fit
- ecommerce
- taobao
- tmall
- jd
- pdd
## Suggested Repo Name
- `openclaw-skill-reviewlens`
## Preflight
```bash
cd /absolute/path/to/reviewlens
clawhub whoami
bash /absolute/path/to/codex/tmp/validate_clawhub_skill_dir.sh .
```
## Publish Command
### One command
```bash
cd /absolute/path/to/reviewlens
sh scripts/publish.sh
```
### Preview command
```bash
cd /absolute/path/to/reviewlens
sh scripts/publish.sh --print
```
### Manual command
```bash
clawhub publish /absolute/path/to/reviewlens \
--slug reviewlens \
--name "评论透镜" \
--version "1.0.0" \
--changelog "Launch 评论透镜 (ReviewLens), a review-intelligence skill that compresses marketplace reviews into a decision card with repeated praise, repeated complaints, fit, regret risk, and cheap-versus-steady judgment." \
--tags "latest,reviews,shopping,review-intelligence,review-analysis,buyer-regret,user-fit,ecommerce,taobao,tmall,jd,pdd"
```
FILE:agents/openai.yaml
interface:
display_name: "评论透镜"
short_description: "把海量评论压成购买结论卡,告诉你谁适合买、谁容易踩坑"
default_prompt: "Use $reviewlens to compress marketplace reviews into a decision card that surfaces repeated praise, repeated complaints, concentrated bad-review issues, who the product fits, who is likely to regret it, and whether the cheaper option is flawed or the pricier option is steadier, and answer in Chinese."
FILE:clawhub.json
{
"name": "reviewlens",
"version": "1.0.0",
"description": "评论透镜 - 把海量商品评论压成结论卡,告诉你真实买家反复在夸什么、骂什么,谁适合买,谁容易踩坑",
"keywords": [
"reviewlens",
"review-intelligence",
"shopping-reviews",
"review-analysis",
"review-summary",
"user-fit",
"buyer-regret",
"comment-clustering",
"shopping-decision",
"taobao",
"tmall",
"jd",
"pdd",
"ecommerce"
],
"author": "openclaw",
"license": "MIT"
}
FILE:references/example-prompts.md
# Example Prompts
Use this file for demos, trigger tuning, and realistic marketplace phrasing.
## Repeated Complaints
- 别看评分,告诉我大家到底在骂什么。
- 差评是不是都在说同一个问题?
- 帮我看下这 300 条评论,负面点到底集中在哪里?
- 这款东西的真实槽点是什么,不要给我空话。
## Fit And Regret
- 这东西最适合哪类人?谁买了最容易后悔?
- 我是怕麻烦的人,这类评论对我来说算大问题吗?
- 适合自用吗?适合送人吗?
- 如果我最在意噪音 / 做工 / 尺寸稳定性,这款还能买吗?
## Cheap With Flaws Or Pricier But Steadier
- 便宜版能买吗,还是贵一点更稳?
- 这个低价是不是便宜但有瑕疵?
- 贵的那个到底稳在哪里,还是只是品牌溢价?
- 别给我评分,直接说是便宜能忍还是加钱更省心。
## Compare Several Listings
- 这两个链接谁的评论更稳?谁更容易踩坑?
- 同款不同店,评论里最大的区别是什么?
- 这两个版本哪个是真优点更多,哪个只是差评更少?
- 帮我把这几个链接的评论压成一张对比结论卡。
## High-Hit Short Prompts
- 大家到底在抱怨什么?
- 差评集中吗?
- 适合谁?
- 谁容易踩坑?
- 便宜但有瑕疵吗?
- 贵一点更稳吗?
- 给我评论结论卡。
FILE:references/fit-mismatch.md
# Fit And Mismatch
Read this file when you need to convert review patterns into a judgment about who should buy and who is likely to regret it.
## Core Rule
Do not stop at `pros` and `cons`.
Translate the review reality into:
- best-fit user
- acceptable-but-not-perfect user
- likely-regret user
## Common Fit Patterns
### Cheap And Good Enough
Use when:
- flaws are real but manageable
- the user clearly values price over polish
- the product still seems usable for its main job
Common phrasing:
- `适合把价格放第一位、愿意接受小瑕疵的人。`
- `适合自用,不适合对细节零容忍的人。`
### Fine For Light Use, Wrong For Heavy Use
Use when:
- performance or durability complaints exist
- but the product still sounds acceptable for lighter or simpler scenarios
Common phrasing:
- `轻度使用问题不大,重度使用容易放大缺点。`
- `适合偶尔用,不适合每天高频折腾。`
### Fine If You Know The Tradeoff
Use when:
- the main problem is expectation mismatch
- informed buyers may still be satisfied
- uninformed buyers are more likely to feel cheated
Common phrasing:
- `适合知道自己在买什么的人,不适合闭眼下单的人。`
- `你如果接受这个 tradeoff,会觉得还行;如果默认它是更高标准,就容易失望。`
### Fine For Self-Use, Bad For Gifting
Use when:
- workmanship, packaging, consistency, or prestige complaints show up
- the route is still workable for personal use
- but it feels risky for gifting
Common phrasing:
- `自用能买,送人不够稳。`
- `问题不一定大,但送礼容错率太低。`
### Wrong For Low-Hassle Buyers
Use when:
- assembly, setup, sizing, return friction, or inconsistency is a repeated theme
- the product may still work for patient users
Common phrasing:
- `适合愿意折腾的人,不适合怕麻烦的人。`
- `如果你最怕返工,这类评论要放大看。`
## What To Avoid
Avoid vague fit claims like:
- `看个人需求`
- `因人而异`
Instead, name the person:
- `适合预算敏感的自用买家`
- `不适合对噪音敏感的人`
- `适合能接受尺寸偏差的人`
- `不适合急用和送礼场景`
FILE:references/review-signals.md
# Review Signals
Read this file when you need a compact lens for deciding which reviews carry real signal and which ones are mostly noise.
## High-Signal Review Traits
Bias toward reviews that are:
- recent
- specific about the scenario of use
- detailed about the flaw or benefit
- written in a way that explains tradeoffs instead of only venting
- supported by images, measurements, or long-term use comments
Useful phrasing:
- `这类评论的价值不在情绪,而在细节。`
- `越具体,越像真实可复现体验。`
## Lower-Signal Review Traits
Be more cautious when reviews are:
- generic praise with little detail
- repeated with suspiciously similar wording
- clearly reacting to delivery timing rather than product quality
- too short to explain what actually happened
- purely emotional without describing the trigger
Useful phrasing:
- `这更像情绪表达,不足以单独改判。`
- `评分低不等于信息量高。`
## Mid-Star Truth
2-star to 4-star reviews often carry the best signal because they tend to say:
- what is actually good
- what is annoying but tolerable
- what kind of buyer should or should not accept the tradeoff
Useful phrasing:
- `中间分段评论更容易看出真实 tradeoff。`
- `最有用的评论,往往不是最夸也不是最骂。`
## Complaint Buckets
Try to classify each repeated complaint into one of these groups:
- product core defect
- consistency or quality-control problem
- sizing, fit, comfort, or color mismatch
- performance, battery, heat, lag, or stability issue
- material, workmanship, smell, or durability issue
- packaging and shipping damage
- seller communication or refund-friction issue
- expectation mismatch caused by listing language or buyer assumption
This distinction matters because:
- product defects should change the buy/no-buy call
- seller or logistics issues may change the route, not the product verdict
- expectation mismatch may mainly affect uninformed buyers
## Concentration Check
Treat the signal as concentrated when:
- the same complaint appears repeatedly
- the same complaint appears in both recent and older reviews
- several buyers describe similar failure modes
- the complaint hits the product's main use case
Treat the signal as scattered when:
- complaints are diverse and unrelated
- no theme keeps recurring
- many negative reviews lack detail
Useful phrasing:
- `不是所有差评都重要,重要的是它们是否在说同一件事。`
- `一个问题反复出现,权重大于十个零散抱怨。`
## Do Not Overreact To Small Counts Blindly
A small number of complaints can still matter a lot when:
- the total review count is not huge
- the flaw is severe
- it affects the product's core job
- the users describing it sound credible and specific
At the same time, do not inflate weak evidence into certainty.
Say when the signal is directional rather than conclusive.
FILE:references/verdict-cards.md
# Verdict Cards
Read this file when the answer needs to land as a sharp conclusion card instead of a long review summary.
## Core Rule
Do not end with a score.
End with a sentence the user can act on.
## Strong Verdict Patterns
### The Strengths Are Stable, And So Are The Flaws
Use when:
- praise is repeated
- complaints are also repeated
- the product has a clear tradeoff profile
Common phrasing:
- `优点是稳定复现的,缺点也是。`
- `这不是翻车玄学,是一款优缺点都很固定的东西。`
### Cheap, But The Flaws Are Not Random
Use when:
- the low price is real
- the complaints are also real
- the user is effectively paying less to accept a recurring defect
Common phrasing:
- `便宜是便宜,但缺点不是偶发。`
- `这条低价路不是白赚,是拿稳定性换来的。`
### Bad Reviews Are Concentrated On One Problem
Use when:
- one complaint keeps recurring
- that complaint changes the buy/no-buy call
Common phrasing:
- `差评主要集中在同一个问题,不算零散吐槽。`
- `低分背后不是很多问题,而是这个问题被反复提到。`
### Pay More For Stability
Use when:
- the pricier route has meaningfully cleaner review reality
- the premium buys lower regret rather than only better branding
Common phrasing:
- `贵一点的意义,是更稳,不是更花哨。`
- `多花的钱主要买到更低的翻车率。`
### Right For This Buyer, Wrong For That Buyer
Use when:
- the product is not universally good or bad
- the fit depends on the user's tolerance and use case
Common phrasing:
- `适合这类人,不适合那类人。`
- `如果你最在意的是价格,可以买;如果你最怕的是这个问题,建议绕开。`
## Short-Form Template
When the user wants a short answer, compress it to:
`结论:这不是评分问题,而是 tradeoff 问题。优点主要在 X,差评主要集中在 Y。适合 A,不适合 B。便宜版 / 贵版的差异主要体现在稳定性。`
FILE:scripts/publish.sh
#!/usr/bin/env sh
set -eu
ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
WORKSPACE_ROOT="$(CDPATH= cd -- "$ROOT/../.." && pwd)"
CLAWHUB_JSON="$ROOT/clawhub.json"
VALIDATOR="$WORKSPACE_ROOT/tmp/validate_clawhub_skill_dir.sh"
VERSION="$(node -e "process.stdout.write(require(process.argv[1]).version)" "$CLAWHUB_JSON")"
SLUG="reviewlens"
NAME="评论透镜"
CHANGELOG="Launch 评论透镜 (ReviewLens), a review-intelligence skill that compresses marketplace reviews into a decision card with repeated praise, repeated complaints, fit, regret risk, and cheap-versus-steady judgment."
TAGS="latest,reviews,shopping,review-intelligence,review-analysis,buyer-regret,user-fit,ecommerce,taobao,tmall,jd,pdd"
PUBLISH_ROOT="$ROOT"
TMP_DIR=""
if ! command -v clawhub >/dev/null 2>&1; then
echo "clawhub CLI not found in PATH" >&2
exit 1
fi
if ! command -v node >/dev/null 2>&1; then
echo "node is required to read version metadata from clawhub.json" >&2
exit 1
fi
if [ -d "$ROOT/.git" ]; then
TMP_DIR="$(mktemp -d)"
PUBLISH_ROOT="$TMP_DIR/$SLUG"
mkdir -p "$PUBLISH_ROOT"
cp "$ROOT/SKILL.md" "$PUBLISH_ROOT/SKILL.md"
cp "$ROOT/RELEASE.md" "$PUBLISH_ROOT/RELEASE.md"
cp "$ROOT/clawhub.json" "$PUBLISH_ROOT/clawhub.json"
cp -R "$ROOT/agents" "$PUBLISH_ROOT/agents"
cp -R "$ROOT/references" "$PUBLISH_ROOT/references"
cp -R "$ROOT/scripts" "$PUBLISH_ROOT/scripts"
trap 'rm -rf "$TMP_DIR"' EXIT HUP INT TERM
fi
if [ "-" = "--print" ]; then
cat <<EOF
clawhub publish "$PUBLISH_ROOT" \\
--slug "$SLUG" \\
--name "$NAME" \\
--version "$VERSION" \\
--changelog "$CHANGELOG" \\
--tags "$TAGS"
EOF
exit 0
fi
if [ -f "$VALIDATOR" ]; then
bash "$VALIDATOR" "$PUBLISH_ROOT"
fi
echo "Publishing ReviewLens from: $PUBLISH_ROOT"
echo "Version: $VERSION"
echo "Name: $NAME"
echo "Tags: $TAGS"
clawhub publish "$PUBLISH_ROOT" \
--slug "$SLUG" \
--name "$NAME" \
--version "$VERSION" \
--changelog "$CHANGELOG" \
--tags "$TAGS"
Inspired by a public workflow shared by Andrej Karpathy (@karpathy). From raw research to a living Markdown knowledge base that compounds with every question...
---
name: llm-knowledge-bases
description: Inspired by a public workflow shared by Andrej Karpathy (@karpathy). Use when the user wants to check, repair, or grow a Markdown wiki backed by the LLM Knowledge Bases runtime: inspect wiki health, compile missing source notes, clean placeholder pages, add concept/entity/synthesis pages, answer questions from the wiki and file the result back, or run deterministic multimodal wiki lint.
---
# LLM Knowledge Bases
Use this skill to operate a Vault that is managed by the `LLM Knowledge Bases` runtime.
The operating model is:
- `raw/` stores captured source material from the outside world
- `wiki/sources/` stores compiled source notes
- `wiki/outputs/` stores archived answer notes
- `wiki/concepts/` stores durable concept pages
- `wiki/entities/` stores durable entity pages
- `wiki/syntheses/` stores cross-source synthesis pages
- `wiki/_indexes/` stores generated collection indexes
- `wiki/index.md` stores the generated home index
- `wiki/log.md` stores the generated run log page
- `.llm-kb/` stores runtime state
- `.llm-kb/representations/` stores runtime-managed OCR, vision, metadata, and profiling artifacts for non-text raw assets
The runtime owns Vault I/O.
The agent owns understanding, synthesis, linking, and deciding which pages the wiki should gain or improve.
## Important Model
- Treat the Vault as runtime-managed.
- Use `kb_*` tools for all Vault reads and writes.
- Never modify files under `raw/`.
- Never write directly into `wiki/` or `.llm-kb/representations/` with generic file tools.
- Never fabricate IDs, canonical paths, raw hashes, representation paths, or `source_refs`.
- For text and structured-data raw files, use the direct source compile path.
- For PDF and image raw files, use the representation-first path: bundle the asset context, create any missing representations, then compile the source note.
- When a source note summarizes a PDF or image, keep `asset_paths` accurate and include visible review notes, usually `# Visual Notes`, whenever the source depends on a human or model review pass outside the stored representation files.
- The goal is not only to answer questions.
- The goal is to leave the wiki better structured after each meaningful interaction.
## Required Tools
- `kb_status`
- `kb_list_raw`
- `kb_read_raw`
- `kb_get_raw_asset`
- `kb_prepare_source`
- `kb_prepare_source_bundle`
- `kb_prepare_representation`
- `kb_upsert_representation`
- `kb_read_representations`
- `kb_upsert_source_note`
- `kb_prepare_output`
- `kb_upsert_output`
- `kb_prepare_derived_note`
- `kb_upsert_derived_note`
- `kb_search`
- `kb_read_notes`
- `kb_map_gaps`
- `kb_promote_gap`
- `kb_repair_source_ids`
- `kb_rebuild_indexes`
- `kb_lint`
If these tools are unavailable, say so clearly instead of pretending the workflow can proceed.
## Natural-Language Entry Points
Users do not need to name canonical actions.
Map short natural-language requests to the closest workflow.
The explicit prefix `$llm-knowledge-bases` is recommended for maximum routing certainty, but it is not required when the request already clearly matches this skill.
- `check my wiki`, `look over my knowledge base`, `检查我的 wiki`, `总览检查`:
run `kb_status` plus `kb_lint`, report counts, the top issues, and the best next step
- `fill missing source notes`, `continue the backlog`, `补缺失 source notes`, `继续推进这份库`:
use `ingest-source`, call `kb_status`, then `kb_list_raw(changed_only=true)` with an explicit `limit` large enough for the batch, prioritize `missing_source_note`, honor directory/topic filters, compile in small batches, then `kb_rebuild_indexes`
- `clean up these pages`, `fix placeholder titles`, `只修 AI 相关内容`, `做一次维护清理`:
use `maintain-wiki`, start with `kb_lint`, then use `kb_search` plus `kb_read_notes` to target placeholder titles, placeholder summaries, placeholder open questions, placeholder related links, stale navigation, and missing high-value derived pages
- `add concept pages`, `add entity pages`, `add synthesis pages`, `what pages are missing?`, `补 concept/entity/synthesis`:
use `map-gaps`, search first, read the evidence, then call `kb_map_gaps`; use `kb_promote_gap` when a candidate can land directly, otherwise refine it through `kb_prepare_derived_note` plus `kb_upsert_derived_note`
- `repair source ids`, `repair manifest drift`, `修 source id / manifest 漂移`:
run `kb_repair_source_ids` as a dry run first, explain the plan, apply only if the repair set looks correct, then `kb_rebuild_indexes`
- `answer this from the wiki and save it back`, `问答并沉淀成页面`:
use `ask-and-file`, read the evidence first, then choose `output` for query-specific archives or `concept/entity/synthesis` for reusable knowledge
Important:
- honor explicit scope like `top 5`, `first 10`, `only AI-related`, `only raw/书评 1/`, or `do not modify yet`
- prefer small, high-confidence batches over giant rewrites
- for concept/entity/synthesis work, never create a derived page without first reading the supporting notes
## Chinese Intent Lexicon
Treat the following Chinese phrases as strong routing hints, even when the user does not mention tools or page types explicitly.
- `看一下`, `检查一下`, `盘一下`, `总览`, `扫一眼`, `先看看`:
default to inspection-first behavior with `kb_status` plus `kb_lint`
- `先别改`, `先不要修改`, `只看不改`, `先给我报告`:
do not call write tools; stay read-only unless the user later approves changes
- `补缺失`, `补 source`, `补书评`, `编译缺的`, `继续补 source note`:
treat this as `ingest-source` focused on `missing_source_note`
- `整理一下`, `清理一下`, `修一下占位内容`, `只修 AI 相关内容`, `把这部分弄干净`:
treat this as `maintain-wiki` with topic filters and placeholder cleanup
- `补概念页`, `补 entity`, `补 synthesis`, `沉淀成页面`, `把这个主题写成 page`:
treat this as derived-page work; search first, read evidence, then use `kb_map_gaps` or direct derived-note creation
- `修漂移`, `修 source id`, `修 manifest`, `先 dry run`:
route to `kb_repair_source_ids` with a dry run before any apply step
- `继续推进这份库`, `继续往前做`, `接着跑一轮`, `往前推进一批`:
treat this as an incremental maintenance batch, not as an open-ended rewrite
## Continuation Defaults
When the user says `继续推进这份库` or an equivalent continuation request without enough scope detail, run a conservative default batch:
1. call `kb_status`
2. call `kb_list_raw` with `changed_only=true` and an explicit `limit`
3. call `kb_lint`
4. if a dominant topic filter is obvious from the user request, run `kb_search` on that topic and read the top evidence notes
5. choose one primary batch only:
- compile a small batch of `missing_source_note` text raw files, or
- repair a small batch of placeholder-heavy source notes
6. optionally add one high-confidence `concept`, `entity`, or `synthesis` page if the evidence is already strong
7. call `kb_rebuild_indexes`
8. report what changed, what still looks weak, and the next best batch
Do not silently expand an underspecified continuation request into a large multi-hour rewrite.
## Scenario Presets
Use these presets when a short request clearly matches one of the common recurring workflows.
### `ai-topic-cleanup`
Strong triggers:
- `整理一下 AI 相关内容`
- `只修 AI 相关`
- `补 AI 概念页`
- `把 AI 这部分做扎实`
- `继续推进 AI 主题`
Default topic cluster:
- `AI`
- `AIGC`
- `ChatGPT`
- `prompt engineering`
- `AGI`
- `AI hype vs real value`
Default batch:
1. call `kb_lint`
2. run topic-focused `kb_search`
3. read the top evidence notes with `kb_read_notes`
4. repair a small batch of placeholder-heavy source notes first
5. if evidence is already strong, add one high-confidence `concept`, `entity`, or `synthesis` page
6. call `kb_rebuild_indexes`
Priority order:
- placeholder source titles
- placeholder open questions
- placeholder related links
- missing high-value concept/entity/synthesis pages
### `book-review-batch`
Strong triggers:
- `补书评`
- `继续编译书评`
- `把书评编进 wiki`
- `书评批处理`
- `补书评 source notes`
Default scope:
- only `raw/书评 1/`
- text-readable raw files only
- prioritize `missing_source_note`
Default batch:
1. call `kb_status`
2. call `kb_list_raw(changed_only=true)` with an explicit `limit`
3. filter to `raw/书评 1/`
4. compile the first 10 eligible text raw files through `kb_prepare_source`, `kb_read_raw`, and `kb_upsert_source_note`
5. call `kb_rebuild_indexes`
Writing expectations:
- human-facing title, never `元数据`
- real `Summary`, `Key Points`, `Evidence`, `Open Questions`, and `Related Links`
- grounded only in the raw content actually read
### `continue-this-library`
Strong triggers:
- `继续推进我的这份库`
- `接着跑一轮`
- `再往前推进一批`
- `今天继续做这个库`
Default decision rule:
- if `missing_source_note` backlog is obviously dominant, prefer a small ingest batch
- otherwise if placeholder-heavy source notes are clearly dominant, prefer a small cleanup batch
- otherwise read evidence and add one high-confidence derived page
Default batch cap:
- up to 10 source-note compiles, or
- up to 5 placeholder repairs, or
- 1 derived page
Do not mix all three into one large batch unless the user explicitly asks for a broader sweep.
## One-Line Shortcuts
These are valid compact requests.
Treat them as sufficient instructions unless the user adds more scope.
- `用 $llm-knowledge-bases 检查一下我的 wiki,先别改。`
- `用 $llm-knowledge-bases 补书评前 10 个。`
- `用 $llm-knowledge-bases 整理一下 AI 相关内容。`
- `用 $llm-knowledge-bases 补 3 个 concept pages。`
- `用 $llm-knowledge-bases 修一下 source id 漂移,先 dry run。`
- `用 $llm-knowledge-bases 继续推进我的这份库。`
## Canonical Actions
Treat the following as the four canonical high-level actions for this skill.
### `ingest-source`
Use this when the user wants to ingest, compile, or refresh changed raw material.
Sequence:
1. call `kb_status`
2. call `kb_list_raw` with `changed_only=true`
3. for each changed raw file:
- if the raw file is text or structured data:
- call `kb_prepare_source`
- call `kb_read_raw`
- compile the raw content into one grounded source note
- if the raw file is a PDF or image:
- call `kb_prepare_source_bundle`
- call `kb_get_raw_asset`
- if `compile_readiness` is not `ready`, create the missing representation trail with:
- `kb_prepare_representation`
- `kb_upsert_representation`
- call `kb_read_representations`
- compile the source note from the raw metadata plus the reviewed representations
- call `kb_upsert_source_note`
4. after the batch, call `kb_rebuild_indexes`
5. report what was compiled, what remains partial, and which raw assets still need representation work
Important:
- the actual LLM compile happens between the read/bundle steps and `kb_upsert_source_note`
- `kb_read_raw` is only for text-readable raw files
- PDFs usually become compile-ready once there is `native_text`, `ocr_text`, or `page_notes`
- images usually become compile-ready once there is `vision_notes`
- structured data is already text-readable, but `metadata` or `data_profile` can still improve later maintenance
- one raw file may later justify new `concept`, `entity`, or `synthesis` pages
- prefer incremental passes over giant rewrites
### `ask-and-file`
Use this when the user wants a grounded answer and the answer may deserve a durable artifact.
Sequence:
1. call `kb_search`
2. read only the most relevant notes with `kb_read_notes`
3. answer only from retrieved notes
4. decide the best write-back target:
- use `kb_prepare_output` + `kb_upsert_output` for question-specific answer archives
- use `kb_prepare_derived_note` + `kb_upsert_derived_note` when the answer should become a durable `concept`, `entity`, or `synthesis` page
5. call `kb_rebuild_indexes` if the wiki changed materially
Important:
- search before answering
- do not cite notes you did not read
- do not answer directly from non-text raw assets when the runtime already expects the grounded source note or stored representation trail
- choose `output` when preserving the exact query matters
- choose `concept/entity/synthesis` when the answer is reusable beyond the original query
### `maintain-wiki`
Use this when the user wants cleanup, organization, or a quality pass.
Sequence:
1. call `kb_lint`
2. inspect `wiki/index.md`, `wiki/log.md`, and the most relevant collection indexes with `kb_read_notes`
3. identify weak pages, missing derived pages, stale navigation, or grounding gaps
4. if source note ids, manifest entries, source note paths, or stored raw hashes have drifted, call `kb_repair_source_ids` first as a dry run and only apply it when the plan is correct
5. if the user wants fixes, repair narrowly through the appropriate `kb_*` write tools
6. call `kb_rebuild_indexes`
Important:
- prefer small targeted improvements
- keep source grounding visible
- use `concept/entity/synthesis` pages to absorb recurring structure instead of repeating the same reasoning in outputs
- treat `kb_lint` warnings as signals about wiki health, not only schema correctness
- pay special attention when `kb_lint` surfaces `missing_representation`, `representation_stale`, `unreviewed_asset_source`, stale source coverage, unresolved research gaps, unsupported claims, contradiction candidates, or missing high-value pages
- use `kb_repair_source_ids` for deterministic source-note/manifest repair instead of hand-editing ids, paths, or raw hashes
### `map-gaps`
Use this when the user wants to know what the wiki is missing.
Sequence:
1. call `kb_search`
2. read the relevant source, output, and derived pages
3. call `kb_map_gaps`
4. identify:
- repeated ideas that deserve a `concept` page
- repeated named items that deserve an `entity` page
- cross-source themes that deserve a `synthesis` page
5. if the user wants the page landed immediately, call `kb_promote_gap` with the candidate `note_id`
6. otherwise propose the best next pages in priority order
Important:
- prefer candidates with stronger `source_refs` coverage
- use `kb_promote_gap` when a current candidate should be landed as-is into the wiki
- use the returned draft template as the starting point when you want to refine the page before calling `kb_upsert_derived_note`
- treat the returned suggested opening and evidence summary as scaffolding, not final prose
## Writing Rules
### Source Notes
Required frontmatter fields:
- `id`
- `type: source`
- `title`
- `raw_path`
- `raw_hash`
- `source_kind`
- `tags`
- `created_at`
- `updated_at`
- `status`
Strongly recommended frontmatter fields:
- `raw_kind`
- `mime_type`
- `asset_paths`
Required headings:
- `# Summary`
- `# Key Points`
- `# Evidence`
- `# Open Questions`
- `# Related Links`
Multimodal guidance:
- for PDF and image source notes, `asset_paths` should include the primary reviewed raw asset
- add `# Visual Notes` when the note depends on multimodal review details that are not already obvious from the stored representation files
- do not claim a non-text asset was reviewed if neither the representation trail nor the visible review notes support that claim
### Output Notes
Required frontmatter fields:
- `id`
- `type: output`
- `title`
- `query`
- `source_refs`
- `created_at`
- `updated_at`
Required headings:
- `# Answer`
- `# Sources Used`
- `# Follow-up Questions`
### Derived Notes
Derived pages are for durable wiki structure, not ephemeral chat residue.
Supported kinds:
- `concept`
- `entity`
- `synthesis`
Shared frontmatter fields:
- `id`
- `type`
- `title`
- `aliases`
- `source_refs`
- `tags`
- `created_at`
- `updated_at`
- `status`
Required headings by kind:
- `concept`: `# Summary`, `# Definition`, `# Key Points`, `# Evidence`, `# Open Questions`, `# Related Notes`
- `entity`: `# Summary`, `# Who or What`, `# Key Facts`, `# Evidence`, `# Open Questions`, `# Related Notes`
- `synthesis`: `# Summary`, `# Thesis`, `# Supporting Evidence`, `# Tensions`, `# Open Questions`, `# Related Notes`
Guidance:
- `concept` pages capture reusable ideas or frames
- `entity` pages capture named things that recur
- `synthesis` pages capture higher-level cross-source conclusions that should survive beyond any one query
- `synthesis` pages capture multi-source analysis, tradeoffs, or contested views
- keep `source_refs` aligned with real source notes
## Safety Boundaries
- Never modify files under `raw/`.
- Never use generic file-writing tools to modify `wiki/` directly.
- Never invent note IDs, output IDs, or paths.
- Never claim a write succeeded unless the runtime confirms it.
- Never bypass the runtime after a tool failure.
## Failure Handling
If a runtime tool fails:
- explain the failure plainly
- keep the user context intact
- propose the next executable step
- do not work around the error by writing directly to the Vault
## Language Policy
- Write outward-facing artifacts in English by default.
- Preserve original-language titles or quotes only when they materially matter.
- If the user explicitly wants another language, follow that request for the final artifact.
## Finish Standard
When you finish a task with this skill, report:
- what was ingested, answered, maintained, or mapped
- which `kb_*` tools were used
- which files were created or updated
- any unresolved ambiguity or weak evidence
- the best next one to three follow-up steps
FILE:CHANGELOG.md
# Changelog
## 1.2.2 - 2026-04-06
- Add stronger natural-language routing and compact Chinese one-line prompts for common wiki workflows.
## 1.2.1 - 2026-04-05
- Document and ship deterministic repair for legacy src-untitled source ids.
## 1.2.0 - 2026-04-05
- Updated the skill docs for runtime `0.4.0` and its representation-first multimodal ingest model
- Added `kb_get_raw_asset`, `kb_prepare_source_bundle`, `kb_prepare_representation`, `kb_upsert_representation`, and `kb_read_representations` to the documented tool surface
- Added `kb_repair_source_ids` to the documented runtime and maintenance surface so source-id drift can be repaired deterministically
- Clarified the split between direct text/data ingest and PDF/image ingest that must go through stored representations first
- Documented multimodal grounding expectations around `raw_kind`, `mime_type`, `asset_paths`, visible review notes, and the new lint warnings for missing or stale representation trails
## 1.1.4 - 2026-04-05
- Added `kb_promote_gap` so a current gap candidate can be promoted straight into a real derived note
- Refactored the docs so map-gaps can either return refined drafts or land the best current candidate immediately
## 1.1.3 - 2026-04-05
- Upgraded `kb_map_gaps` again so each candidate now includes a suggested opening and evidence summary inside the draft payload
- Clarified in the docs that map-gaps outputs are meant to be refined into near-ready derived notes
## 1.1.2 - 2026-04-05
- Upgraded `kb_map_gaps` from a plain candidate report into a curation assistant with priorities and ready-to-fill Markdown drafts
- Clarified in the skill docs that map-gaps should feed directly into `kb_upsert_derived_note`
## 1.1.1 - 2026-04-05
- Added `kb_map_gaps` to the documented runtime surface
- Updated the skill and agent prompts to treat gap mapping as a first-class workflow
- Clarified how the wiki should identify the next missing concept, entity, and synthesis pages
## 1.1.0 - 2026-04-05
- Repositioned the skill around a wiki-first operating model instead of a runtime-first three-action story
- Added first-class support in the docs and agent prompts for `concept`, `entity`, and `synthesis` pages
- Expanded the recommended vault layout to include `wiki/concepts/`, `wiki/entities/`, `wiki/syntheses/`, `wiki/index.md`, and `wiki/log.md`
- Reframed the canonical workflows as `ingest-source`, `ask-and-file`, `maintain-wiki`, and `map-gaps`
- Updated the scaffold script to create the new wiki directories and generated navigation placeholders
## 1.0.9 - 2026-04-05
- Repositioned the skill around the standalone CLI/MCP runtime instead of describing the product as OpenClaw-first
- Updated README, release notes, and operator prompts to treat OpenClaw as one compatible host alongside Claude Code, Codex, Cursor, and Gemini CLI
- Bumped ClawHub publish metadata to match the new standalone runtime story
## 1.0.8 - 2026-04-04
- Added a Claude Code MCP install path so the same `kb_*` contract can be used outside the OpenClaw host
- Added a reusable Claude Code subagent prompt that carries over the skill's compile, ask, and lint workflow rules
- Added a Codex `AGENTS.md` template plus a portable operator guide for other MCP-capable agents
- Added a multi-agent config helper path for Codex, Cursor, and Gemini CLI
- Updated the README and package metadata to document the cross-agent installation path
FILE:README.md
# LLM Knowledge Bases
Inspired by a public workflow shared by Andrej Karpathy (@karpathy).
From raw text, PDFs, images, and structured data to a living Markdown wiki that compounds with every question.
## Product Model
`LLM Knowledge Bases` is best understood as a local-first wiki operating system for research.
The core model is:
- `raw/` holds outside-world source material
- `wiki/` holds the persistent knowledge layer the agent maintains
- `schema` defines what kinds of pages belong in the wiki and how they relate
- every ingest, query, and maintenance pass should improve the wiki itself
The runtime provides deterministic guardrails for paths, IDs, validation, and writes.
The skill tells the agent how to grow the wiki into something durable and navigable.
## What 1.2.1 Changes
The old mental model was mostly text-first:
- compile raw notes into `wiki/sources/`
- archive answers into `wiki/outputs/`
- lint the structure
The upgraded multimodal model is:
- ingest text and structured data directly into source pages
- inspect PDFs and images through deterministic raw asset metadata
- store OCR, vision, page-note, metadata, and profiling artifacts under `.llm-kb/representations/`
- compile non-text source pages from the full source bundle instead of pretending `kb_read_raw` can read binary inputs
- repair stale legacy `src-untitled-*` source ids deterministically before maintenance or compile flows keep spreading them
- promote recurring ideas into `concept` and `entity` pages
- write cross-source analysis into `synthesis` pages
- keep `wiki/index.md` as a master catalog, `wiki/log.md` as a readable activity log, and `_indexes/` current
- let important answers write back into the wiki instead of disappearing into chat history
This is closer to the original Karpathy-style wiki workflow: the wiki is the product, not just a side effect of a runtime.
## Supported Raw Inputs
The runtime now recognizes:
- text: `.md`, `.txt`
- PDFs: `.pdf`
- images: `.png`, `.jpg`, `.jpeg`, `.webp`, `.gif`, `.svg`
- structured data: `.csv`, `.tsv`, `.json`, `.html`
## Current Page Types
Runtime-backed page kinds now include:
- `source` for grounded source notes compiled from `raw/`
- `output` for archived answers tied to a specific question
- `concept` for reusable ideas that recur across notes
- `entity` for named systems, people, products, orgs, methods, or datasets
- `synthesis` for higher-level cross-source thinking
- `index` for generated navigation pages
- `log` for the generated run log page
## Default Repository Shape
```text
<vault>/
raw/
inbox/
web/
notes/
papers/
repos/
datasets/
images/
wiki/
sources/
outputs/
concepts/
entities/
syntheses/
_indexes/
index.md
log.md
.llm-kb/
manifest.json
runs.jsonl
representations/
```
## High-Level Actions
The skill should think in terms of four high-level actions:
- `ingest-source`: compile changed raw files into `source` pages, using the direct path for text/data and the representation-first path for PDFs/images
- `ask-and-file`: answer from retrieved notes and archive the result as an `output` or promote it into a richer wiki page
- `maintain-wiki`: improve navigation, derived pages, consistency, wiki-health signals, and repair stale source ids or manifest/source-note drift when needed
- `map-gaps`: identify missing concept/entity/synthesis pages, produce prioritized draft templates, and optionally promote the best current candidate straight into a real derived page
## Runtime-Backed Tools
The runtime now supports the core wiki-maintenance surface:
- `kb_status`
- `kb_list_raw`
- `kb_read_raw`
- `kb_get_raw_asset`
- `kb_prepare_source`
- `kb_prepare_source_bundle`
- `kb_prepare_representation`
- `kb_upsert_representation`
- `kb_read_representations`
- `kb_upsert_source_note`
- `kb_prepare_output`
- `kb_upsert_output`
- `kb_prepare_derived_note`
- `kb_upsert_derived_note`
- `kb_search`
- `kb_read_notes`
- `kb_map_gaps`
- `kb_promote_gap`
- `kb_repair_source_ids`
- `kb_rebuild_indexes`
- `kb_lint`
## Install
```bash
clawhub install llm-knowledge-bases
```
For Claude Code:
```bash
claude mcp add llm-knowledge-bases -- \
npx -y --package @harrylabs/llm-knowledge-bases@latest \
llm-knowledge-bases-mcp \
--vault-root /absolute/path/to/your/obsidian-vault
```
For Codex, copy [`agents/codex-AGENTS.md`](agents/codex-AGENTS.md) into your project root as `AGENTS.md`.
For other MCP-capable agents:
```bash
npx -y --package @harrylabs/llm-knowledge-bases@latest \
llm-knowledge-bases-configs --vault-root /absolute/path/to/your/obsidian-vault
```
To scaffold a new vault:
```bash
bash scripts/init_llm_kb_repo.sh my-knowledge-base
```
## Example Prompts
Users do not need to name canonical actions.
Short natural-language requests should still map onto the runtime workflow cleanly.
The explicit prefix `$llm-knowledge-bases` is optional; use it when you want the clearest possible routing.
```text
Use $llm-knowledge-bases to ingest changed text notes and refresh the wiki indexes.
```
```text
Use $llm-knowledge-bases to inspect changed PDFs, store any missing representations, compile grounded source pages, and refresh the indexes.
```
```text
Use $llm-knowledge-bases to answer "What are the tradeoffs between retrieval and memory finetuning?" and file the result back into the wiki.
```
```text
Use $llm-knowledge-bases to inspect the wiki for missing concept or entity pages around agent memory systems.
```
```text
Use $llm-knowledge-bases to run map-gaps and tell me the next five pages this wiki should gain.
```
```text
Use $llm-knowledge-bases to run map-gaps and immediately promote the top synthesis candidate into the wiki.
```
```text
Use $llm-knowledge-bases to run a wiki maintenance pass and tell me which pages are stale, missing, or weakly grounded.
```
```text
Use $llm-knowledge-bases to check my wiki, run kb_status and kb_lint, tell me the current counts, the top 5 issues, and the single best next action. Do not modify anything yet.
```
```text
Use $llm-knowledge-bases to fill missing source notes under raw/书评 1/. Run kb_list_raw with changed_only=true and limit=200, prioritize missing_source_note, compile the first 10 text raw files, rebuild indexes, and tell me what remains missing.
```
```text
Use $llm-knowledge-bases to clean up AI-related pages. Focus on AI, AIGC, ChatGPT, prompt engineering, and AGI; fix placeholder titles, open questions, and related links on 5 notes, then rebuild indexes.
```
```text
Use $llm-knowledge-bases to add concept pages around agent memory. Search first, read the supporting notes, run kb_map_gaps, land the top 3 concept pages, and rebuild indexes.
```
```text
Use $llm-knowledge-bases to repair source-id and manifest drift. Dry-run kb_repair_source_ids first, explain the repair plan, apply it if the plan is sound, then rebuild indexes and report the repair counts.
```
```text
Use $llm-knowledge-bases to answer "What recurring judgments show up across my AI-related book reviews?" Read only the most relevant notes first, then write the durable result back as a concept or synthesis page if it is reusable.
```
## Chinese Compact Prompts
```text
用 $llm-knowledge-bases 检查一下我的 wiki,先别改,告诉我当前数量、最重要的 5 个问题、以及最值得做的下一步。
```
```text
用 $llm-knowledge-bases 补一下缺失的 source notes,只处理 raw/书评 1/,先做前 10 个,再刷新索引。
```
```text
用 $llm-knowledge-bases 整理一下 AI 相关页面,优先修标题像占位词、Open Questions 还是待整理、Related Links 还是待补充的 source notes。
```
```text
用 $llm-knowledge-bases 补 3 个最值得做的 concept pages,先搜索和读证据,再落地页面。
```
```text
用 $llm-knowledge-bases 继续推进这份库:保守地跑一轮,小批量补缺失、修占位页,并在证据足够时补 1 个 derived page。
```
## Scenario Shortcuts
```text
用 $llm-knowledge-bases 整理一下 AI 相关内容。
```
```text
用 $llm-knowledge-bases 补书评前 10 个。
```
```text
用 $llm-knowledge-bases 继续推进我的这份库。
```
## One-Line Chinese Pack
直接复制下面任意一句就能用:
```text
用 $llm-knowledge-bases 检查一下我的 wiki,先别改。
```
```text
用 $llm-knowledge-bases 补书评前 10 个。
```
```text
用 $llm-knowledge-bases 整理一下 AI 相关内容。
```
```text
用 $llm-knowledge-bases 补 3 个 concept pages。
```
```text
用 $llm-knowledge-bases 修一下 source id 漂移,先 dry run。
```
```text
用 $llm-knowledge-bases 继续推进我的这份库。
```
如果上下文已经明确是在操作这个知识库,通常也可以直接省略 `$llm-knowledge-bases`,例如:
```text
检查一下我的 wiki,先别改。
```
```text
补书评前 10 个。
```
```text
整理一下 AI 相关内容。
```
## Daily 3-Pack For This Vault
如果只保留 3 句,优先保留这 3 句:
```text
用 $llm-knowledge-bases 检查一下我的 wiki,先别改,告诉我当前数量、最重要的 5 个问题、以及今天最值得做的一步。
```
```text
用 $llm-knowledge-bases 继续推进我的这份库:只处理 raw/书评 1/,优先补前 10 个 missing source notes,然后刷新索引。
```
```text
用 $llm-knowledge-bases 整理一下 AI 相关内容:先修一小批占位感强的 source notes,再补 1 个最值得做的 concept 或 synthesis page。
```
## Writing Style
This skill prefers durable wiki artifacts over chat-only answers:
- source-grounded pages
- explicit multimodal review trails through `raw_kind`, `mime_type`, `asset_paths`, and `# Visual Notes` when needed
- explicit `source_refs`
- linked Markdown pages that humans can browse
- generated indexes and logs that keep the vault navigable
- warnings from `kb_lint` that highlight missing or stale representations, inconsistent `asset_paths`, isolated draft pages, missing cross-links, stale source coverage, unresolved research gaps, unsupported claims, contradiction candidates, placeholder content, and medium/high-value missing pages before those problems spread
- `kb_repair_source_ids` when source note ids, paths, manifest entries, or stored raw hashes have drifted and need a deterministic repair pass
Outward-facing artifacts default to English unless the user explicitly asks otherwise.
FILE:RELEASE.md
# LLM Knowledge Bases Release Notes
## Short Description
Inspired by a public workflow shared by Andrej Karpathy (@karpathy).
From raw text, PDFs, images, and structured data to a living Markdown wiki that compounds with every question.
## Marketplace Card Copy
Title:
- LLM Knowledge Bases
Slug:
- llm-knowledge-bases
Short description:
- Inspired by a public workflow shared by Andrej Karpathy (@karpathy).
Install hook:
- From raw text, PDFs, images, and structured data to a living Markdown wiki that compounds with every question
## Announcement Copy
`LLM Knowledge Bases` skill `1.2.2` stays on top of runtime `0.4.1`, but makes the workflow much easier to invoke through short natural-language requests:
- inspect a wiki with short prompts like "检查一下我的 wiki,先别改"
- continue book-review ingestion with prompts like "补书评前 10 个"
- run focused cleanup passes with prompts like "整理一下 AI 相关内容"
- add durable derived pages with prompts like "补 3 个 concept pages"
- repair source-id drift with prompts like "修一下 source id 漂移,先 dry run"
- continue a living wiki with prompts like "继续推进我的这份库"
The workflow remains local-first and Markdown-first. This release improves the skill layer, not the runtime contract: it teaches the agent how to map compact English and Chinese requests onto the existing `kb_*` workflows without making users spell out the full operator prompt every time.
This release is still explicitly inspired by a public workflow shared by Andrej Karpathy ([@karpathy](https://x.com/karpathy)) around using LLMs to maintain personal knowledge bases built from Markdown, images, and accumulated outputs. That attribution helps position the skill in a recognizable lineage without implying endorsement.
The current release focuses on:
- stronger natural-language routing in `SKILL.md`, `agents/openai.yaml`, and `agents/codex-AGENTS.md`
- explicit Chinese intent hints for inspection, cleanup, gap mapping, source-id repair, and continuation requests
- scenario presets for AI-topic cleanup, book-review batch ingest, and continuing this specific library incrementally
- compact one-line prompt packs so users can omit long operator instructions and often omit the explicit `$llm-knowledge-bases` prefix too
- release metadata that matches skill `1.2.2` and runtime `0.4.1`
It works especially well with Obsidian, while staying portable because everything remains plain Markdown.
## Suggested Tags
- knowledge-base
- research
- markdown
- wiki
- obsidian
- multimodal
- pdf
- image
- data
- local-first
## Suggested Repo Name
- `openclaw-skill-llm-knowledge-bases`
## Publish Command
```bash
clawhub publish /absolute/path/to/llm-knowledge-bases \
--slug llm-knowledge-bases \
--name "LLM Knowledge Bases" \
--version "1.2.2" \
--changelog "Add stronger natural-language routing and compact Chinese one-line prompts for common wiki workflows." \
--tags "knowledge-base,research,markdown,wiki,obsidian,multimodal,pdf,image,data,local-first"
```
FILE:agents/claude-code-subagent.md
---
name: llm-knowledge-bases
description: Use this subagent when the user wants to ingest text, PDF, image, or structured-data raw material into the wiki, answer from the knowledge base with file-back support, maintain concept/entity/synthesis pages, or run deterministic multimodal wiki lint.
---
You operate a Vault that is managed by the `LLM Knowledge Bases` MCP server.
Operating model:
- `raw/` stores captured source material
- `wiki/sources/` stores compiled source pages
- `wiki/outputs/` stores archived answers
- `wiki/concepts/`, `wiki/entities/`, and `wiki/syntheses/` store durable derived pages
- `wiki/_indexes/`, `wiki/index.md`, and `wiki/log.md` store generated navigation
- `.llm-kb/` stores runtime state
- `.llm-kb/representations/` stores runtime-managed OCR, vision, metadata, and profiling artifacts for non-text assets
Boundaries:
- use MCP tools for all Vault reads and writes
- do not modify `raw/`
- do not write directly into `wiki/` or `.llm-kb/representations/` with generic file tools
- do not invent IDs, paths, hashes, representation paths, or `source_refs`
- use `kb_read_raw` only for text-readable raw files
- use the representation-first path for PDFs and images
Required MCP tools:
- `kb_status`
- `kb_list_raw`
- `kb_read_raw`
- `kb_get_raw_asset`
- `kb_prepare_source`
- `kb_prepare_source_bundle`
- `kb_prepare_representation`
- `kb_upsert_representation`
- `kb_read_representations`
- `kb_upsert_source_note`
- `kb_prepare_output`
- `kb_upsert_output`
- `kb_prepare_derived_note`
- `kb_upsert_derived_note`
- `kb_search`
- `kb_read_notes`
- `kb_map_gaps`
- `kb_promote_gap`
- `kb_repair_source_ids`
- `kb_rebuild_indexes`
- `kb_lint`
Preferred actions:
1. `ingest-source`
- text/data: `kb_prepare_source` -> `kb_read_raw`
- PDFs/images: `kb_prepare_source_bundle` -> `kb_get_raw_asset` -> representation tools -> `kb_read_representations`
2. `ask-and-file`
3. `maintain-wiki`
- use `kb_lint`, then `kb_repair_source_ids` when source ids, manifest entries, source note paths, or raw hashes have drifted
4. `map-gaps`
- `kb_search`
- `kb_read_notes`
- `kb_map_gaps`
- optionally `kb_promote_gap` to land a current candidate immediately
Use `output` notes for query-specific archives.
Use `concept`, `entity`, and `synthesis` notes when the result should become durable wiki structure.
For PDF/image source notes, keep `asset_paths` aligned with the reviewed assets and add `Visual Notes` when the source depends on multimodal review details that are not already captured by stored representations.
FILE:agents/codex-AGENTS.md
# LLM Knowledge Bases
Use this guide when Codex should operate a Vault managed by the `LLM Knowledge Bases` MCP server.
Core model:
- `raw/` stores captured source material
- `wiki/` stores the persistent knowledge layer
- `wiki/sources/` stores compiled source pages
- `wiki/outputs/` stores archived answers
- `wiki/concepts/`, `wiki/entities/`, and `wiki/syntheses/` store durable derived pages
- `wiki/_indexes/`, `wiki/index.md`, and `wiki/log.md` keep the vault navigable
- `.llm-kb/representations/` stores runtime-managed OCR, vision, metadata, and profiling artifacts for non-text assets
Boundaries:
- treat the Vault as runtime-managed
- use MCP tools for all Vault reads and writes
- never modify `raw/`
- never write directly into `wiki/` or `.llm-kb/representations/` with generic file tools
- never invent IDs, hashes, representation paths, or `source_refs`
- use `kb_read_raw` only for text-readable raw files
- use the representation-first path for PDFs and images
Required MCP tools:
- `kb_status`
- `kb_list_raw`
- `kb_read_raw`
- `kb_get_raw_asset`
- `kb_prepare_source`
- `kb_prepare_source_bundle`
- `kb_prepare_representation`
- `kb_upsert_representation`
- `kb_read_representations`
- `kb_upsert_source_note`
- `kb_prepare_output`
- `kb_upsert_output`
- `kb_prepare_derived_note`
- `kb_upsert_derived_note`
- `kb_search`
- `kb_read_notes`
- `kb_map_gaps`
- `kb_promote_gap`
- `kb_repair_source_ids`
- `kb_rebuild_indexes`
- `kb_lint`
Canonical actions:
1. `ingest-source`
- `kb_status`
- `kb_list_raw(changed_only=true)`
- for text/data: `kb_prepare_source` -> `kb_read_raw`
- for PDFs/images: `kb_prepare_source_bundle` -> `kb_get_raw_asset` -> representation tools -> `kb_read_representations`
- write one grounded source page
- `kb_upsert_source_note`
- `kb_rebuild_indexes`
2. `ask-and-file`
- `kb_search`
- `kb_read_notes`
- answer only from retrieved notes
- use `kb_upsert_output` for query-specific archives
- use `kb_upsert_derived_note` for durable concept/entity/synthesis pages
3. `maintain-wiki`
- `kb_lint`
- inspect indexes and relevant pages
- use `kb_repair_source_ids` first when source ids, manifest entries, source note paths, or raw hashes have drifted
- repair narrowly through runtime tools
- `kb_rebuild_indexes`
4. `map-gaps`
- `kb_search`
- `kb_read_notes`
- `kb_map_gaps`
- optionally `kb_promote_gap` when the best current candidate should be landed immediately
Natural-language triggers:
The explicit prefix `$llm-knowledge-bases` is optional.
Use it when you want maximum routing certainty, but short natural-language requests should still trigger this guide when the intent is clear.
- `check my wiki`, `总览检查`: run `kb_status` plus `kb_lint`, report counts, top issues, and the best next action; do not write unless asked
- `fill missing source notes`, `继续推进这份库`: use `kb_status` plus `kb_list_raw(changed_only=true)` with an explicit `limit`, prioritize `missing_source_note`, compile in small batches, then rebuild indexes
- `clean up these pages`, `fix placeholder titles`, `做一次维护清理`: start with `kb_lint`, then use `kb_search` plus `kb_read_notes` to target placeholder titles, open questions, related links, stale navigation, and other high-value health issues
- `add concept/entity/synthesis pages`, `what pages are missing?`: search first, read evidence, then run `kb_map_gaps`; use `kb_promote_gap` when the candidate can be landed directly
- `repair source ids`, `repair manifest drift`: run `kb_repair_source_ids` as a dry run first, explain the plan, then apply only if it looks correct
- `answer this from the wiki and save it back`: use `ask-and-file`; prefer `concept/entity/synthesis` over `output` when the result is reusable beyond the current query
Chinese routing hints:
- `看一下`, `检查一下`, `盘一下`, `总览`, `先看看`: inspection-first, usually `kb_status` plus `kb_lint`
- `先别改`, `只看不改`, `先给我报告`: read-only mode unless the user later asks for writes
- `补缺失`, `补书评`, `编译缺的 source`: `ingest-source` focused on `missing_source_note`
- `整理一下`, `清理一下`, `修一下占位内容`: `maintain-wiki` with placeholder cleanup and topic filters
- `补概念页`, `补 entity`, `补 synthesis`, `沉淀成页面`: derived-page flow; always read evidence before writing
- `修漂移`, `修 source id`, `修 manifest`, `先 dry run`: `kb_repair_source_ids` dry run first
- `继续推进这份库`, `接着跑一轮`: conservative continuation batch, not a giant rewrite
Continuation default when the request is underspecified:
1. `kb_status`
2. `kb_list_raw(changed_only=true)` with an explicit `limit`
3. `kb_lint`
4. optional topic-targeted `kb_search` plus `kb_read_notes`
5. choose one primary batch: either a small `missing_source_note` compile batch or a small placeholder-repair batch
6. optionally add one high-confidence derived page
7. `kb_rebuild_indexes`
Scenario presets:
- `整理一下 AI 相关内容`, `只修 AI 相关`, `补 AI 概念页`
run `kb_lint`, then topic-focused `kb_search` plus `kb_read_notes`; repair a small batch of placeholder-heavy notes first, then optionally land one high-confidence derived page
- `补书评`, `继续编译书评`, `书评批处理`
scope to `raw/书评 1/` text raw files, prioritize `missing_source_note`, compile up to 10 notes, then rebuild indexes
- `继续推进我的这份库`, `接着跑一轮`
choose one primary batch only: either up to 10 source-note compiles, up to 5 placeholder repairs, or 1 derived page
When `继续推进这份库` is underspecified, prefer whichever of these is most clearly dominant in `kb_status` plus `kb_lint`.
One-line shortcuts that should be treated as complete enough requests:
- `用 $llm-knowledge-bases 检查一下我的 wiki,先别改`
- `用 $llm-knowledge-bases 补书评前 10 个`
- `用 $llm-knowledge-bases 整理一下 AI 相关内容`
- `用 $llm-knowledge-bases 补 3 个 concept pages`
- `用 $llm-knowledge-bases 修一下 source id 漂移,先 dry run`
- `用 $llm-knowledge-bases 继续推进我的这份库`
The same one-line requests should also work without the `$llm-knowledge-bases` prefix when the surrounding context is clearly about this wiki.
Always honor explicit scope like `top 5`, `first 10`, `only AI-related`, `only raw/书评 1/`, or `do not modify yet`.
Writing rules:
- source pages need `Summary`, `Key Points`, `Evidence`, `Open Questions`, `Related Links`
- multimodal source pages should keep `raw_kind`, `mime_type`, and `asset_paths` aligned with the reviewed asset trail
- PDF/image source pages should usually include `Visual Notes` when the review evidence is not already obvious from stored representations
- output pages need `Answer`, `Sources Used`, `Follow-up Questions`
- concept pages need `Summary`, `Definition`, `Key Points`, `Evidence`, `Open Questions`, `Related Notes`
- entity pages need `Summary`, `Who or What`, `Key Facts`, `Evidence`, `Open Questions`, `Related Notes`
- synthesis pages need `Summary`, `Thesis`, `Supporting Evidence`, `Tensions`, `Open Questions`, `Related Notes`
Finish by stating:
- what was ingested, answered, or maintained
- which MCP tools were used
- which pages were created or updated
- any unresolved ambiguity or weak evidence
FILE:agents/openai.yaml
interface:
display_name: "LLM Knowledge Bases"
short_description: "From raw text, PDFs, images, and data to a compounding Markdown wiki"
default_prompt: "Use this skill through one-line requests like '检查一下我的 wiki,先别改', '补书评前 10 个', '整理一下 AI 相关内容', '补 3 个 concept pages', '修一下 source id 漂移,先 dry run', or '继续推进我的这份库'. Adding `$llm-knowledge-bases` is optional but gives the most explicit routing."
FILE:agents/portable-operator.md
# Portable Operator Instructions
Use these instructions for any MCP-capable agent that should operate an `LLM Knowledge Bases` vault.
Core model:
- the runtime owns Vault I/O, IDs, paths, validation, state, and generated navigation
- the agent owns understanding, synthesis, Q&A, and deciding which wiki pages should exist
- every meaningful interaction should improve the wiki, not only produce a chat answer
Preferred actions:
- `ingest-source`
- `ask-and-file`
- `maintain-wiki`
- `map-gaps`
Non-negotiable boundaries:
- never modify files under `raw/`
- never write directly into `wiki/` with generic file tools
- never fabricate note ids, hashes, or `source_refs`
- never claim a write succeeded unless the MCP tool confirmed it
Preferred tool order:
- ingest (text/data): `kb_status` -> `kb_list_raw` -> `kb_prepare_source` -> `kb_read_raw` -> `kb_upsert_source_note` -> `kb_rebuild_indexes`
- ingest (pdf/image): `kb_status` -> `kb_list_raw` -> `kb_prepare_source_bundle` -> `kb_get_raw_asset` -> `kb_prepare_representation` -> `kb_upsert_representation` -> `kb_read_representations` -> `kb_upsert_source_note` -> `kb_rebuild_indexes`
- answer: `kb_search` -> `kb_read_notes` -> answer -> `kb_upsert_output` or `kb_upsert_derived_note`
- maintain: `kb_lint` -> `kb_read_notes` -> optional `kb_repair_source_ids` when source ids or manifest/source-note metadata drifted -> targeted fixes -> `kb_rebuild_indexes`
- gap mapping: `kb_search` -> `kb_read_notes` -> `kb_map_gaps` -> optional `kb_promote_gap`
FILE:clawhub.json
{
"name": "llm-knowledge-bases",
"version": "1.2.2",
"description": "Inspired by a public workflow shared by Andrej Karpathy (@karpathy). From raw text, PDFs, images, and structured data to a living Markdown wiki that compounds with every question.",
"keywords": [
"llm-knowledge-bases",
"knowledge-base",
"research",
"markdown",
"wiki",
"obsidian",
"multimodal",
"pdf",
"image",
"data",
"local-first"
]
}
FILE:references/maintenance-playbook.md
# Maintenance Playbook
Use this playbook when the wiki has grown enough that structure drift starts to hurt trust or navigation.
## When To Run A Maintenance Pass
Run a pass when:
- a large batch of raw sources landed
- the wiki is answering well but feels hard to navigate
- the same ideas keep appearing in outputs without dedicated pages
- users report stale, missing, or contradictory notes
- `wiki/index.md` or collection indexes no longer help orient the corpus
## Core Checks
### 1. Source Coverage
Check whether new files in `raw/` are missing corresponding pages in `wiki/sources/`.
Look for:
- unprocessed raw items
- raw files with no manifest record
- raw files with no source page
- source pages that are too shallow to support reuse
### 2. Derived Page Gaps
Check whether the wiki is missing obvious durable pages.
Look for:
- repeated ideas that deserve a `concept` page
- repeated named items that deserve an `entity` page
- repeated cross-source tradeoff or comparison work that deserves a `synthesis` page
### 3. Grounding Quality
Check whether pages remain source-grounded.
Look for:
- weak `Evidence` sections
- `source_refs` that do not correspond to real source pages
- outputs that overstate confidence
- derived pages that sound plausible but cannot be traced back to sources
### 4. Navigation Quality
Check whether generated and human-facing navigation still works.
Look for:
- stale `wiki/index.md`
- missing collection indexes
- pages with no meaningful `Related Notes`
- large clusters of pages that are hard to discover from search or indexes
### 5. Promotion Quality
Check whether the wiki is still treating outputs appropriately.
Look for:
- valuable answers trapped only in `wiki/outputs/`
- recurring question-specific notes that should become durable wiki pages
- concepts or entities repeatedly recreated in multiple outputs
## Cleanup Strategy
Prefer small targeted passes over dramatic rewrites.
Recommended order:
1. run `kb_lint`
2. restore missing source pages and missing generated indexes
3. repair broken `source_refs` or bad canonical paths
4. promote the highest-value repeated ideas into `concept/entity/synthesis` pages
5. rebuild indexes and re-check search quality
## Improvement Ideas
Once the basics are healthy, useful next upgrades include:
- backlink-aware orphan reports
- contradiction or tension candidate reports
- stale-page reports based on update history
- stronger prompts for promoting outputs into durable pages
- workflow wrappers for wiki curation, not only raw-source compilation
Only build tools that remove repeated wiki-maintenance work for the agent.
## Deliverable Format
When reporting a maintenance pass, include:
- the highest-signal issues found
- the pages created or updated
- which problems remain unresolved
- the next one to three highest-value pages or fixes
FILE:references/repo-layout.md
# Repository Layout
This skill works best when the knowledge base stays easy to browse for both humans and agents, and when the runtime owns the canonical write boundaries.
## Default Layout
```text
<vault>/
raw/
inbox/
web/
notes/
papers/
repos/
datasets/
images/
wiki/
sources/
outputs/
concepts/
entities/
syntheses/
_indexes/
index.md
log.md
.llm-kb/
manifest.json
runs.jsonl
```
## Folder Contract
- `raw/`: source-of-truth artifacts captured from the outside world. Preserve filenames, provenance, and local assets. Runtime support is currently centered on `.md` and `.txt` raw files.
- `wiki/sources/`: one source page per raw source, written only through the runtime.
- `wiki/outputs/`: archived answers that preserve the original question context.
- `wiki/concepts/`: durable concept pages for reusable ideas or frameworks.
- `wiki/entities/`: durable pages for named systems, people, organizations, tools, methods, or datasets.
- `wiki/syntheses/`: cross-source pages for tradeoffs, comparisons, theses, and higher-order analysis.
- `wiki/_indexes/`: generated collection indexes such as `sources.md`, `outputs.md`, `concepts.md`, `entities.md`, and `syntheses.md`.
- `wiki/index.md`: generated wiki home page.
- `wiki/log.md`: generated run log page.
- `.llm-kb/manifest.json`: canonical mapping of raw files to source notes and hashes.
- `.llm-kb/runs.jsonl`: append-only run log for audit and generated `wiki/log.md`.
## Recommended Page Shapes
### Source Page
Store source pages in `wiki/sources/` with the runtime-issued `doc_id`.
Required headings:
- `Summary`
- `Key Points`
- `Evidence`
- `Open Questions`
- `Related Links`
Required frontmatter:
- `id`
- `type: source`
- `title`
- `raw_path`
- `raw_hash`
- `source_kind`
- `tags`
- `created_at`
- `updated_at`
- `status`
### Output Page
Store question-specific answer archives in `wiki/outputs/`.
Required headings:
- `Answer`
- `Sources Used`
- `Follow-up Questions`
Required frontmatter:
- `id`
- `type: output`
- `title`
- `query`
- `source_refs`
- `created_at`
- `updated_at`
### Concept Page
Use `wiki/concepts/` for reusable ideas that recur across source notes and answers.
Required headings:
- `Summary`
- `Definition`
- `Key Points`
- `Evidence`
- `Open Questions`
- `Related Notes`
### Entity Page
Use `wiki/entities/` for named things that deserve durable context.
Required headings:
- `Summary`
- `Who or What`
- `Key Facts`
- `Evidence`
- `Open Questions`
- `Related Notes`
### Synthesis Page
Use `wiki/syntheses/` for cross-source analysis and tradeoff pages.
Required headings:
- `Summary`
- `Thesis`
- `Supporting Evidence`
- `Tensions`
- `Open Questions`
- `Related Notes`
### Derived Frontmatter
For `concept`, `entity`, and `synthesis`, include:
- `id`
- `type`
- `title`
- `aliases`
- `source_refs`
- `tags`
- `created_at`
- `updated_at`
- `status`
## Naming
- source notes: `wiki/sources/src-<slug>.md`
- outputs: `wiki/outputs/YYYY-MM-DD-<slug>.md`
- concepts: `wiki/concepts/concept-<slug>.md`
- entities: `wiki/entities/entity-<slug>.md`
- syntheses: `wiki/syntheses/synthesis-<slug>.md`
## Growth Rule
Start with these page kinds and let the wiki earn more structure gradually.
Good growth:
- promote repeated ideas into `concept` pages
- promote repeated named items into `entity` pages
- promote recurring cross-source reasoning into `synthesis` pages
Bad growth:
- inventing a huge taxonomy before the wiki needs it
- creating many directories with no durable workflow behind them
- archiving every thought as an `output` when it should really become a reusable wiki page
FILE:scripts/init_llm_kb_repo.sh
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 1 ]]; then
echo "Usage: bash scripts/init_llm_kb_repo.sh <target-dir>" >&2
exit 1
fi
target_dir="$1"
mkdir -p \
"$target_dir/raw/inbox" \
"$target_dir/raw/web" \
"$target_dir/raw/notes" \
"$target_dir/raw/papers" \
"$target_dir/raw/repos" \
"$target_dir/raw/datasets" \
"$target_dir/raw/images" \
"$target_dir/wiki/sources" \
"$target_dir/wiki/outputs" \
"$target_dir/wiki/concepts" \
"$target_dir/wiki/entities" \
"$target_dir/wiki/syntheses" \
"$target_dir/wiki/_indexes" \
"$target_dir/.llm-kb"
write_if_missing() {
local path="$1"
local content="$2"
if [[ -e "$path" ]]; then
return 0
fi
printf '%s\n' "$content" > "$path"
}
write_if_missing "$target_dir/wiki/_indexes/sources.md" "# Sources Index
Generated by the runtime. Rebuild with \`kb_rebuild_indexes\` after compiling source notes."
write_if_missing "$target_dir/wiki/_indexes/outputs.md" "# Outputs Index
Generated by the runtime. Rebuild with \`kb_rebuild_indexes\` after archiving output notes."
write_if_missing "$target_dir/wiki/_indexes/concepts.md" "# Concepts Index
Generated by the runtime. Rebuild with \`kb_rebuild_indexes\` after writing concept notes."
write_if_missing "$target_dir/wiki/_indexes/entities.md" "# Entities Index
Generated by the runtime. Rebuild with \`kb_rebuild_indexes\` after writing entity notes."
write_if_missing "$target_dir/wiki/_indexes/syntheses.md" "# Syntheses Index
Generated by the runtime. Rebuild with \`kb_rebuild_indexes\` after writing synthesis notes."
write_if_missing "$target_dir/wiki/index.md" "# Knowledge Base Index
Generated by the runtime. Rebuild with \`kb_rebuild_indexes\` after changing the wiki."
write_if_missing "$target_dir/wiki/log.md" "# Knowledge Base Log
Generated by the runtime from \`.llm-kb/runs.jsonl\`."
write_if_missing "$target_dir/.llm-kb/manifest.json" '{
"schema_version": 1,
"vault_root": "REPLACE_WITH_ABSOLUTE_VAULT_PATH",
"sources": {}
}'
write_if_missing "$target_dir/.llm-kb/runs.jsonl" ""
echo "Initialized knowledge base scaffold at: $target_dir"
FILE:scripts/publish.sh
#!/usr/bin/env bash
set -euo pipefail
skill_dir="$(cd "$(dirname "$0")/.." && pwd)"
version="$(node -e 'const fs=require("node:fs"); const pkg=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); console.log(pkg.version);' "$skill_dir/clawhub.json")"
default_changelog="$(node - "$skill_dir/CHANGELOG.md" <<'EOF'
const fs = require("node:fs");
const changelogPath = process.argv[2];
const text = fs.readFileSync(changelogPath, "utf8");
const match = text.match(/^##\s+[^\n]+\n\n- ([^\n]+)/m);
if (!match) {
process.exit(1);
}
process.stdout.write(match[1]);
EOF
)"
changelog="-$default_changelog"
clawhub publish "$skill_dir" \
--slug llm-knowledge-bases \
--name "LLM Knowledge Bases" \
--version "$version" \
--changelog "$changelog" \
--tags "knowledge-base,research,markdown,wiki,obsidian,multimodal,pdf,image,data,local-first"
Decision briefing skill that turns notes, research, proposals, meeting summaries, documents, and connector outputs into a one-page decision brief that clarif...
---
name: DecisionDeck
slug: decisiondeck
version: 1.0.0
description: Decision briefing skill that turns notes, research, proposals, meeting summaries, documents, and connector outputs into a one-page decision brief that clarifies the decision to make, compares options, surfaces conflicts, marks weak evidence, and recommends the next move. Use when the user says "make me a brief", "summarize this for my boss/client", "these documents disagree", "help me choose", or "turn this material into a kickoff or go/no-go brief".
metadata:
clawdbot:
emoji: "🗂️"
requires:
bins: []
os: ["linux", "darwin", "win32"]
---
# DecisionDeck
DecisionDeck is not another summarizer.
It is the decision-brief layer after information has already been gathered.
Its job is to help the user answer:
- 基于这些资料,现在到底要做什么决定
- 真正的选项有哪些
- 哪些文档其实在互相冲突
- 每个判断后面到底有什么证据
- 哪些地方证据不足,而且这些不足会不会影响拍板
- 最后应该给老板、客户、团队负责人什么一页纸结论
This skill should feel like a calm chief of staff or briefing officer: concise, evidence-aware, and willing to make the call.
## Core Positioning
Default toward these outcomes:
- turn many documents into one decision-ready brief
- separate facts, interpretations, assumptions, and missing evidence
- surface disagreement instead of smoothing it away
- compress noise while preserving decision signal
- end with a recommendation and a concrete next step
Do not stop at:
- long summaries
- theme clustering with no call
- "both sides make sense" style non-answers
- hiding uncertainty to make the brief look cleaner
## Relationship To Knowledge Skills
Think of the product line like this:
- Knowledge Connector: connect, import, search, and relate knowledge
- DecisionDeck: compress that knowledge into a one-page decision brief
- NextFromKnowledge: turn the knowledge into the next action or short execution plan
Use this boundary:
- if the user needs cross-document retrieval or relationship discovery, use Knowledge Connector first
- if the user needs a boss-ready, client-ready, or kickoff-ready one-pager, use DecisionDeck
- if the user mainly wants "what should I do next", prefer NextFromKnowledge
Knowledge Connector helps the user bring knowledge in.
DecisionDeck helps the user take that knowledge into a decision room.
## When To Use It
Use this skill when the user says things like:
- "读了一堆资料,帮我做决策摘要"
- "几篇文档观点不一致,帮我梳理"
- "把这些材料整理成一页 brief"
- "我要启动一个项目,帮我整理成 kickoff brief"
- "给老板出一页纸结论"
- "给客户出一个 one-pager"
- "帮我做 go / no-go 简报"
- "不要长文,总结成能拍板的版本"
It is especially strong when the user already has:
- research notes
- strategy docs
- proposals
- meeting summaries
- interview notes
- connector outputs
- internal memos
- messy copied bullets from several sources
## What This Skill Must Do
Default to these jobs:
- identify the real decision, not just the topic
- normalize the actual options on the table
- extract decision-relevant evidence from multiple materials
- mark where the materials agree and where they conflict
- flag weak evidence, assumptions, and unknowns
- produce a concise recommendation with next-step guidance
Good output should feel ready for review, forwarding, or discussion.
## Modes
1. decision memo mode
- choose between several options and justify the recommendation
2. conflict reconciliation mode
- explain where documents or stakeholders disagree and what that means for the decision
3. project kickoff brief mode
- turn scattered material into a startup, initiative, or project brief
4. executive or client one-pager mode
- compress complex material into a quick-read brief for a busy decision-maker
5. go / no-go mode
- decide whether to proceed now, delay, narrow scope, or stop
See [references/brief-frames.md](references/brief-frames.md) when the user needs a more formal frame for executive review, kickoff, or conflict-heavy material.
## Inputs It Can Work From
Common inputs:
- notes and snippets
- research docs
- meeting transcripts or minutes
- product or strategy memos
- proposals and RFP responses
- customer interview summaries
- project docs
- connector search results
- user-written rough bullets
Do not pretend the source material is cleaner than it is.
When the input is messy, normalize it and still drive toward a decision-ready brief.
## Core Workflow
1. Identify the decision target and audience.
Decide:
- what decision is actually being made
- who the brief is for
- whether the user needs a recommendation, a neutral brief, or a go / no-go framing
2. Distill decision-relevant signal.
Separate:
- facts
- repeated signals
- constraints
- stakeholder positions
- assumptions
- unknowns
3. Normalize the options.
Make the options comparable.
If the input mixes goals, approaches, and implementation details, rewrite them into clean option paths.
4. Surface conflict.
Ask:
- where do documents disagree
- whether the disagreement is factual, interpretive, or goal-based
- whether the conflict actually changes the recommendation
5. Judge evidence quality.
Mark whether each important point is:
- directly supported
- inferred from several signals
- weakly supported
- missing support
6. Compress into one page.
Prioritize only what changes the decision.
Prefer a tight brief over an exhaustive memo.
7. Make the call.
End with:
- recommended option
- why it wins now
- what would change the call
- what happens next
## Decision Rules
### One Page Is A Discipline
Do not try to preserve every detail.
Keep what changes:
- the recommendation
- the order of options
- the level of confidence
- the next action
Cut what is merely interesting but not decision-relevant.
### Separate Facts From Interpretation
Always distinguish:
- what the source directly says
- what several sources imply
- what is still an assumption
Do not let opinions wear the clothes of evidence.
### Surface Conflict Explicitly
When documents disagree, say so plainly.
Good wording:
- "资料之间的分歧主要在需求规模判断,不在问题是否存在。"
- "冲突点不是方向,而是投入节奏。"
- "A 文档更乐观,B 文档更保守,背后是不同的成功标准。"
Avoid merging opposing views into fake consensus.
### Mark Evidence Weakness Honestly
If the recommendation depends on thin evidence, say that.
Preferred phrasing:
- "当前建议成立,但证据强度一般。"
- "这个判断更多基于重复信号,不是强证据定论。"
- "这里是方向性建议,不是高置信结论。"
### Recommend When The Material Already Supports A Call
Do not hide behind "need more research" when the current material is enough for a reasonable decision.
If the evidence is not strong enough for a full decision, recommend the smallest decision that can be made now, plus the single best follow-up that would reduce uncertainty.
### Audience Matters
Briefs for busy decision-makers should:
- start with the call
- minimize jargon
- show only the top conflicts and risks
- keep the next step obvious
If the audience is not specified, default to:
- a manager, founder, client, or project owner with limited time
## Output Pattern
Use this structure unless the user wants something shorter:
### Decision In One Line
State the exact decision or question.
### Recommendation
Give the direct call first.
### Options On The Table
Name the real options and what each one optimizes for.
### What The Evidence Supports
Show the highest-value facts, signals, and constraints.
### Where The Materials Conflict
Summarize the main disagreement and whether it changes the call.
### What Is Still Unclear
List the missing facts or weak evidence that matter.
### Next Step
Give the next move, owner, or discussion direction when possible.
## Mode-Specific Guidance
### Decision Memo Mode
Bias toward:
- clear recommendation
- option comparison
- rationale
- confidence and caveats
### Conflict Reconciliation Mode
Bias toward:
- what exactly conflicts
- which side is better supported
- whether the conflict is blocking
- what decision can still be made today
### Project Kickoff Brief Mode
Bias toward:
- goal
- user or stakeholder need
- option paths
- main constraints
- recommended scope or starting shape
### Executive Or Client One-Pager Mode
Bias toward:
- very fast scanability
- plain language
- short sections
- a clean bottom line
## Tone And Quality Bar
- Sound decisive, but not overconfident.
- Compress aggressively without becoming vague.
- Make disagreement legible.
- Make uncertainty visible.
- Do not turn the answer into a research report unless the user asks.
- Do not sound like a passive summarizer.
- Do not avoid the recommendation just to stay "balanced".
Preferred phrasing:
- "一句话建议:先这样定。"
- "这份材料已经足够支持当前判断。"
- "分歧存在,但还不足以推翻推荐方向。"
- "真正缺的不是更多摘要,而是这一个关键信号。"
- "如果要给老板看,我会把结论压成这一页。"
Avoid:
- "信息很多,暂时无法判断"
- "各有优劣,看你偏好"
- long narrative recap with no decision frame
FILE:CHANGELOG.md
# Changelog
## 1.0.0
Release theme: 把“知识接进来”继续推进到“交付一页可决策结果”。
What changed:
- 初版定义 `DecisionDeck` 为一页式决策简报 skill,而不是长摘要工具
- 明确 5 个核心价值:提炼选项、识别冲突、标出证据不足、给出推荐结论、输出下一步
- 建立和 `Knowledge Connector`、`NextFromKnowledge` 的产品边界
- 补齐 marketplace 文案、agents 元数据和参考模板
- 新增本地可运行的 CLI、决策简报引擎、测试脚本和 publish 脚本
Suggested one-line changelog:
- Initial release of DecisionDeck: turn scattered notes, docs, and connector outputs into a one-page decision brief with options, conflicts, evidence gaps, and a recommended next move.
FILE:README.md
# DecisionDeck
把接进来的资料,直接变成一页能拿去做决定的简报。
`DecisionDeck` 是“决策简报官”这条产品线的第一版可发布仓库。它承接 `Knowledge Connector` 之后的下一步:不是继续接更多来源,而是把已经进来的资料压缩成一个能给老板、客户、团队负责人直接看的 decision brief。
它要解决的问题很直接:
- 读了一堆资料,结论到底是什么
- 几份文档观点打架,冲突点到底在哪
- 现在该选哪个方案
- 项目该不该启动,怎么写成一页 kickoff brief
- 给老板或客户的一页纸,应该怎么写到能直接拍板
## 适合的输入
- research notes
- strategy memo
- 会议纪要
- proposal / 方案文档
- 用户访谈摘要
- 内部对比材料
- Knowledge Connector 的搜索或图谱结果
- 用户自己整理的散乱 bullet points
## 默认输出什么
DecisionDeck 默认不是产出长摘要,而是产出一页能拿来讨论和决策的内容:
- `Decision In One Line`
- `Recommendation`
- `Options On The Table`
- `What The Evidence Supports`
- `Where The Materials Conflict`
- `What Is Still Unclear`
- `Next Step`
## 核心命令
```bash
deck brief --file notes.md
deck brief --text "..."
deck compare --file materials.md --options "方案A,方案B,方案C"
deck conflicts --file materials.md
deck kickoff --file research.md
deck gaps --file research.md
deck analyze --file research.md --json
```
这些命令分别对应:
- 默认一页式决策简报
- 显式选项比较
- 冲突梳理
- kickoff brief
- 证据缺口梳理
- 原始结构化分析结果
## 为什么它贴合 Knowledge Connector
Knowledge Connector 解决的是“把知识接进来、连起来、搜出来”。
DecisionDeck 解决的是更靠近结果的一步:
- 从多个文档提炼选项
- 找出观点冲突
- 标出证据不足的地方
- 生成一页决策简报
- 给出下一步建议
也就是说,不让 Knowledge Connector 停在“接进来”,而是把它推进到“产出可决策结果”。
## 和相邻 skill 的边界
- `Knowledge Connector`:负责导入、连接、搜索、关系理解
- `DecisionDeck`:负责一页式决策简报
- `NextFromKnowledge`:负责把已有知识变成下一步动作或短计划
如果用户要的是“给我一个能拿去开会或发老板的 one-pager”,更适合 `DecisionDeck`。
## 典型问题
- “读了一堆资料,帮我做决策摘要”
- “几篇文档观点不一致,帮我梳理”
- “我要启动一个项目,帮我整理成 brief”
- “给老板出一页纸结论”
- “给客户写一个 one-pager”
- “帮我做一个 go / no-go 简报”
## 建议安装名
```bash
clawhub install decisiondeck
```
## 仓库结构
```text
decisiondeck/
├── SKILL.md
├── README.md
├── CHANGELOG.md
├── RELEASE.md
├── clawhub.json
├── package.json
├── .gitignore
├── agents/openai.yaml
├── references/brief-frames.md
├── bin/cli.js
├── src/index.js
├── test/test.js
└── scripts/publish.sh
```
## 本地验证
```bash
npm test
node bin/cli.js brief --text "方案A 继续扩展连接器来源,但实现更重。方案B 先做一页式决策简报,最快本周能验证。建议先做方案B。"
```
## 发布
```bash
npm run publish:clawhub
```
或直接执行:
```bash
sh ./scripts/publish.sh
```
## 一句话卖点
把多份资料压缩成一页可决策简报:选项、冲突、证据缺口、推荐结论和下一步,一次讲清楚。
FILE:RELEASE.md
# DecisionDeck Release Notes
## Short Description
把多份资料、纪要、调研和连接器结果,直接压成一页可决策简报。
## Marketplace Card Copy
Title:
- DecisionDeck
Short description:
- 决策简报官:把多份资料压成一页能拿去拍板的 decision brief
Install hook:
- 不只是把知识接进来,而是把资料直接推进成可决策结果
## Announcement Copy
DecisionDeck 不是再做一个总结器。
它解决的是一个更靠近结果的问题:
- 资料很多,但要怎么拍板
- 文档不少,但观点互相冲突
- 要给老板、客户、项目 owner 出一页纸,到底怎么写
这一版把事情收敛到 5 个高价值结果:
- 从多个文档提炼真实选项
- 找出观点冲突和分歧来源
- 标出证据不足的地方
- 生成一页式决策简报
- 给出推荐结论和下一步建议
它和 Knowledge Connector 是天然上下游:
- Knowledge Connector 负责把知识接进来、连起来、搜出来
- DecisionDeck 负责把这些材料带进决策场景
## Suggested Tags
- latest
- decision
- brief
- executive-summary
- knowledge
- planning
## Suggested Repo Name
- `openclaw-skill-decisiondeck`
## Publish Command
```bash
clawhub publish /absolute/path/to/decisiondeck \
--slug decisiondeck \
--name "DecisionDeck" \
--version "1.0.0" \
--changelog "Initial release of DecisionDeck: turn scattered notes, docs, and connector outputs into a one-page decision brief with options, conflicts, evidence gaps, and a recommended next move." \
--tags "decision,brief,executive-summary,knowledge,planning"
```
FILE:agents/openai.yaml
interface:
display_name: "DecisionDeck"
short_description: "决策简报官:把多份资料压成一页可决策简报"
default_prompt: "Use $decisiondeck to turn my notes, research, proposals, meeting summaries, documents, or connector outputs into a one-page decision brief with options, conflicts, evidence gaps, a recommendation, and next steps, and answer in Chinese."
FILE:bin/cli.js
#!/usr/bin/env node
const DecisionDeck = require('../src/index.js');
const pkg = require('../package.json');
function parseArgs(argv) {
const options = { _: [] };
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (!arg.startsWith('-')) {
options._.push(arg);
continue;
}
const key = arg.replace(/^-+/, '');
const next = argv[i + 1];
const hasValue = typeof next !== 'undefined' && !next.startsWith('-');
options[key] = hasValue ? next : true;
if (hasValue) {
i += 1;
}
}
return options;
}
function printHelp() {
console.log(`DecisionDeck pkg.version
Usage:
deck brief --file notes.md
deck brief --text "..."
deck compare --file options.md --options "方案A,方案B"
deck conflicts --file materials.md
deck kickoff --file research.md
deck gaps --file research.md
deck analyze --file research.md --json
Commands:
brief Produce the default one-page decision brief
compare Rank explicit or inferred options
conflicts Summarize where materials disagree
kickoff Turn materials into a kickoff-ready brief
gaps Surface the missing evidence that matters
analyze Return the distilled structure as JSON
Options:
--file <path> Read input from a file
--text <text> Read input from inline text
--decision <txt> Override the decision question
--audience <txt> Override the target reader, such as boss or client
--options <csv> Comma-separated option names
--json Print JSON output
--help Show this help
--version Show version
`);
}
function requireInput(options) {
if (!options.file && !options.text) {
throw new Error('Please provide --file or --text');
}
}
function parseOptionList(raw) {
if (!raw) {
return [];
}
return String(raw)
.split(',')
.map((item) => item.trim())
.filter(Boolean);
}
function writeOutput(engine, result, asJson) {
if (asJson) {
console.log(JSON.stringify(result, null, 2));
return;
}
console.log(engine.render(result));
}
async function main() {
const argv = process.argv.slice(2);
const command = argv[0];
if (!command || command === '--help' || command === '-h') {
printHelp();
return;
}
if (command === '--version' || command === '-v') {
console.log(pkg.version);
return;
}
const options = parseArgs(argv.slice(1));
const engine = new DecisionDeck();
const shared = {
decision: options.decision,
audience: options.audience,
options: parseOptionList(options.options)
};
try {
switch (command) {
case 'brief':
requireInput(options);
writeOutput(engine, engine.brief(options, shared), options.json);
return;
case 'compare':
requireInput(options);
writeOutput(engine, engine.compare(options, shared), options.json);
return;
case 'conflicts':
requireInput(options);
writeOutput(engine, engine.conflicts(options, shared), options.json);
return;
case 'kickoff':
requireInput(options);
writeOutput(engine, engine.kickoff(options, shared), options.json);
return;
case 'gaps':
requireInput(options);
writeOutput(engine, engine.gaps(options, shared), options.json);
return;
case 'analyze':
requireInput(options);
console.log(JSON.stringify(engine.analyze(options, shared), null, 2));
return;
default:
throw new Error(`Unknown command: command`);
}
} catch (error) {
console.error(`Error: error.message`);
process.exit(1);
}
}
main();
FILE:clawhub.json
{
"name": "decisiondeck",
"version": "1.0.0",
"description": "决策简报官 - 把多份文档、纪要、调研和连接器结果压成一页可决策简报,梳理选项、冲突、证据缺口和推荐结论",
"keywords": [
"decisiondeck",
"决策简报官",
"decision-brief",
"one-pager",
"executive-brief",
"knowledge-connector",
"decision-support",
"kickoff-brief",
"go-no-go",
"research-synthesis"
],
"author": "openclaw",
"license": "MIT",
"repository": "https://github.com/harrylabsj/openclaw-skill-decisiondeck"
}
FILE:package.json
{
"name": "decisiondeck",
"version": "1.0.0",
"description": "DecisionDeck - turn notes, research, proposals, meeting summaries, and connector outputs into a one-page decision brief",
"main": "src/index.js",
"bin": {
"decisiondeck": "./bin/cli.js",
"deck": "./bin/cli.js"
},
"scripts": {
"start": "node bin/cli.js",
"test": "node test/test.js",
"publish:clawhub": "sh ./scripts/publish.sh"
},
"keywords": [
"decision",
"brief",
"one-pager",
"executive-summary",
"knowledge",
"conflict-reconciliation",
"kickoff-brief",
"go-no-go",
"research-synthesis",
"productivity"
],
"author": "openclaw",
"license": "MIT",
"engines": {
"node": ">=16.0.0"
},
"repository": {
"type": "git",
"url": "https://github.com/harrylabsj/openclaw-skill-decisiondeck"
},
"bugs": {
"url": "https://github.com/harrylabsj/openclaw-skill-decisiondeck/issues"
},
"homepage": "https://github.com/harrylabsj/openclaw-skill-decisiondeck#readme"
}
FILE:references/brief-frames.md
# DecisionDeck Brief Frames
Use these frames when the user wants a more formal or audience-specific one-pager.
Do not load this file unless the user needs a more structured brief format.
## 1. Standard Decision Brief
Use for general option comparison and recommendation.
### Decision In One Line
- What exactly needs to be decided
### Recommendation
- Recommended option
- Confidence level
### Options
- Option A
- Option B
- Option C
### Evidence
- strongest supporting facts
- key constraints
- repeated signals
### Conflict
- where the materials disagree
- whether the disagreement changes the recommendation
### Gaps
- missing evidence
- what would change the call
### Next Step
- immediate follow-up or owner
## 2. Executive Or Boss Brief
Use when the reader is busy and only needs the call plus the few reasons that matter.
### Bottom Line
- recommended call in one or two lines
### Why Now
- why this decision matters now
### Why This Option
- the top reasons this option wins
### Main Risks
- the 1 to 3 risks worth watching
### What Could Change The Call
- only the highest-value uncertainty
### Next Step
- what approval, discussion, or action is needed next
## 3. Project Kickoff Brief
Use when the user wants to turn many materials into a project-start one-pager.
### Project Goal
- what outcome the project is trying to create
### Problem Or Opportunity
- what the inputs say is worth solving
### Options Considered
- possible directions or scope shapes
### Recommended Starting Scope
- what to start with now
### Constraints
- team, time, budget, dependency, or risk constraints
### Open Questions
- questions that still matter for kickoff
### Next Step
- owner and first move
## 4. Conflict Reconciliation Brief
Use when the main value is explaining disagreement across documents or stakeholders.
### Decision At Stake
- what decision the conflict affects
### Where There Is Agreement
- what is already consistent across sources
### Where The Conflict Is
- facts, assumptions, priorities, or success metrics
### Which View Is Better Supported
- stronger evidence or weaker evidence
### What Can Still Be Decided Now
- bounded decision despite imperfect alignment
### Next Step
- what discussion, test, or evidence collection should happen next
## 5. Go / No-Go Brief
Use when the user needs a proceed, delay, narrow, or stop recommendation.
### Recommendation
- go
- no-go
- limited go
- defer
### Why
- strongest evidence for the call
### What Makes It Risky
- key uncertainties or blockers
### Threshold For Reconsideration
- what signal would justify changing the decision
### Next Step
- proceed, narrow scope, or gather one critical missing input
FILE:scripts/publish.sh
#!/usr/bin/env sh
set -eu
ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
VERSION="$(node -e "process.stdout.write(require(process.argv[1]).version)" "$ROOT/package.json")"
CHANGELOG="$(node -e "const fs=require('fs'); const text=fs.readFileSync(process.argv[1], 'utf8'); const match=text.match(/Suggested one-line changelog:\\n- (.+)/); process.stdout.write(match ? match[1] : 'Initial release of DecisionDeck.');" "$ROOT/CHANGELOG.md")"
clawhub publish "$ROOT" \
--slug decisiondeck \
--name "DecisionDeck" \
--version "$VERSION" \
--changelog "$CHANGELOG" \
--tags "decision,brief,executive-summary,knowledge,planning"
FILE:src/index.js
const fs = require('fs');
class DecisionDeck {
readInput(input = {}) {
if (input.file) {
return fs.readFileSync(input.file, 'utf-8');
}
if (typeof input.text === 'string' && input.text.trim()) {
return input.text;
}
throw new Error('No input found. Use --file or --text.');
}
normalizeText(text) {
return String(text || '').replace(/\r\n/g, '\n').trim();
}
unique(items) {
const seen = new Set();
const output = [];
for (const item of items) {
const value = String(item || '').trim();
if (!value || seen.has(value)) {
continue;
}
seen.add(value);
output.push(value);
}
return output;
}
shorten(text, maxLength = 72) {
const value = String(text || '').trim().replace(/\s+/g, ' ');
if (value.length <= maxLength) {
return value;
}
return `value.slice(0, maxLength - 3)...`;
}
cleanLine(line) {
return String(line || '')
.replace(/^\s*[-*•\d.)]+\s*/, '')
.replace(/[。;;]+$/, '')
.trim();
}
collectLines(text) {
const normalized = this.normalizeText(text);
const segments = normalized.match(/[^。\n!??!;;]+[。\n!??!;;]?/g) || [];
return this.unique(
segments
.map((segment) => this.cleanLine(segment))
.filter((segment) => segment.length >= 2)
);
}
extractOptionMentions(line) {
const patterns = [
/(方案\s*[A-Z0-9一二三四五六七八九十]+)/gi,
/(选项\s*[A-Z0-9一二三四五六七八九十]+)/gi,
/(方向\s*[A-Z0-9一二三四五六七八九十]+)/gi,
/(路径\s*[A-Z0-9一二三四五六七八九十]+)/gi,
/(版本\s*[A-Z0-9一二三四五六七八九十]+)/gi,
/(策略\s*[A-Z0-9一二三四五六七八九十]+)/gi,
/(Option\s*[A-Z0-9]+)/gi,
/(Plan\s*[A-Z0-9]+)/gi
];
const matches = [];
for (const pattern of patterns) {
const found = String(line || '').match(pattern) || [];
for (const item of found) {
matches.push(item.replace(/\s+/g, ' ').trim());
}
}
return this.unique(matches);
}
classifyLine(line) {
return {
line,
options: this.extractOptionMentions(line),
evidence: /用户|客户|访谈|调研|数据显示|数据|日志|报告|文档|memo|观察|反馈|信号|指标|来源|证据|research|interview|survey|metric|signal|evidence|source/i.test(line),
conflict: /但是|然而|另一份|另一组|不同|不一致|分歧|冲突|相左|保守|乐观|反对|矛盾|相反|争议|意见不同|另一份.*认为|另一组.*认为|however|while|disagree|conflict/i.test(line),
agreement: /一致|共识|共同|都提到|都认为|都指出|都支持|consensus|agree/i.test(line),
unknown: /[??]|待确认|未知|不清楚|不确定|证据不足|还没验证|需要确认|需要验证|假设|不足|unclear|unknown|assumption|insufficient/i.test(line),
constraint: /资源|预算|时间|截止|排期|依赖|合规|法务|团队|窗口|风险|成本|复杂|blocked|deadline|budget|constraint|approval|dependency|resource/i.test(line),
recommendation: /建议|优先|推荐|应该|先做|先走|更适合|更稳|最快|go|no-go|defer|limited go/i.test(line),
action: /^(先|安排|确认|起草|发送|整理|验证|测试|启动|联系|输出|准备|推进|补齐|访谈|调研|对齐|决策|review|draft|email|plan|decide)/i.test(line)
|| /安排|验证|测试|启动|输出|准备|推进|补齐|访谈|调研|对齐|起草|整理|确认/.test(line)
};
}
distill(text, options = {}) {
const lines = this.collectLines(text);
const evidence = [];
const conflicts = [];
const agreements = [];
const unknowns = [];
const constraints = [];
const recommendations = [];
const actions = [];
const facts = [];
for (const line of lines) {
const flags = this.classifyLine(line);
if (flags.evidence) {
evidence.push(line);
}
if (flags.conflict) {
conflicts.push(line);
}
if (flags.agreement) {
agreements.push(line);
}
if (flags.unknown) {
unknowns.push(line);
}
if (flags.constraint) {
constraints.push(line);
}
if (flags.recommendation) {
recommendations.push(line);
}
if (flags.action) {
actions.push(line);
}
if (
!flags.evidence &&
!flags.conflict &&
!flags.agreement &&
!flags.unknown &&
!flags.constraint &&
!flags.recommendation &&
!flags.action
) {
facts.push(line);
}
}
const explicitOptions = Array.isArray(options.options) ? options.options : [];
const inlineOptions = this.unique(lines.flatMap((line) => this.extractOptionMentions(line)));
const optionList = this.unique([...explicitOptions, ...inlineOptions]);
const scoredOptions = optionList
.map((option) => this.scoreOption(option, lines, recommendations, constraints))
.sort((a, b) => b.score - a.score || a.option.length - b.option.length);
return {
lines,
facts: this.unique(facts),
evidence: this.unique(evidence),
conflicts: this.unique(conflicts),
agreements: this.unique(agreements),
unknowns: this.unique(unknowns),
constraints: this.unique(constraints),
recommendations: this.unique(recommendations),
actions: this.unique(actions),
options: scoredOptions
};
}
scoreOption(option, lines, recommendations, constraints) {
const target = String(option || '').toLowerCase();
const relatedLines = lines.filter((line) => line.toLowerCase().includes(target));
let score = relatedLines.length;
for (const line of relatedLines) {
if (/优先|推荐|建议|更适合|更稳|最快|先做|本周能验证|低风险|更贴近|省事|直接/i.test(line)) {
score += 3;
}
if (/成本高|实现更重|上线更慢|风险|高风险|复杂|依赖|不确定|阻塞|贵|延后|不建议|不要|难/i.test(line)) {
score -= 3;
}
if (/证据主要来自|只有 1 个|只有一个|证据不足|待确认|未知|不清楚/i.test(line)) {
score -= 2;
}
}
if (recommendations.some((line) => line.includes(option))) {
score += 3;
}
if (constraints.some((line) => line.includes(option))) {
score -= 1;
}
return {
option,
score,
relatedLines: relatedLines.slice(0, 3)
};
}
inferDecision(distilled, decisionOverride) {
if (decisionOverride) {
return String(decisionOverride).trim();
}
const questionLine = distilled.lines.find((line) => /是否|要不要|该不该|应该|怎么选|选择|决定|go\s*\/\s*no-go|go\/no-go/i.test(line));
if (questionLine) {
return this.shorten(questionLine, 88);
}
if (distilled.options.length >= 2) {
return `在 distilled.options.map((item) => item.option).join(' / ') 之间做出选择`;
}
const anchor = distilled.evidence[0] || distilled.facts[0] || distilled.actions[0] || '把现有材料压缩成可决策结论';
return this.shorten(anchor, 88);
}
inferAudience(audienceOverride) {
return audienceOverride ? String(audienceOverride).trim() : '老板、客户或项目 owner';
}
summarizeOption(optionScore) {
const headline = optionScore.relatedLines[0]
? this.shorten(optionScore.relatedLines[0], 58)
: `optionScore.option 是当前材料里的一个可行路径`;
return `optionScore.option: headline`;
}
buildEvidenceSupport(distilled) {
return this.unique([
...distilled.evidence.slice(0, 3),
...distilled.constraints.slice(0, 1),
...distilled.facts.slice(0, 1)
]).slice(0, 5);
}
buildConflictSummary(distilled) {
if (distilled.conflicts.length > 0) {
return distilled.conflicts.slice(0, 3);
}
if (distilled.options.length >= 2) {
const top = distilled.options[0];
const next = distilled.options[1];
return [`分歧主要在 top.option 和 next.option 的优先级,以及速度、投入、确定性之间的取舍`];
}
return ['当前材料没有明显正面冲突,但仍要留意证据薄弱处'];
}
buildMissingInfo(distilled) {
if (distilled.unknowns.length > 0) {
return distilled.unknowns.slice(0, 3);
}
if (distilled.conflicts.length > 0) {
return distilled.conflicts.slice(0, 2).map((line) => `需要补证:line`);
}
if (distilled.options.length >= 2 && distilled.options[0].score - distilled.options[1].score <= 1) {
return ['前两条路径拉不开明显差距,需要一个更强的用户或业务信号'];
}
return ['当前信息已经足够支持一个方向性判断'];
}
chooseRecommendation(distilled) {
if (distilled.options.length > 0) {
const winner = distilled.options[0];
return {
label: winner.option,
sentence: `建议先走 winner.option`,
support: winner.relatedLines.length > 0 ? winner.relatedLines : distilled.recommendations.slice(0, 2)
};
}
if (distilled.recommendations.length > 0) {
return {
label: this.shorten(distilled.recommendations[0], 36),
sentence: distilled.recommendations[0],
support: distilled.recommendations.slice(0, 2)
};
}
if (distilled.actions.length > 0) {
return {
label: this.shorten(distilled.actions[0], 36),
sentence: `建议先推动:distilled.actions[0]`,
support: distilled.actions.slice(0, 2)
};
}
return {
label: '先做一页简报',
sentence: '建议先把现有材料压缩成一页决策简报,再进入拍板讨论',
support: this.buildEvidenceSupport(distilled)
};
}
inferConfidence(distilled) {
const unknownPenalty = distilled.unknowns.length;
const conflictPenalty = distilled.conflicts.length > 0 ? 1 : 0;
const optionMargin = distilled.options.length >= 2 ? distilled.options[0].score - distilled.options[1].score : 2;
if (optionMargin >= 3 && unknownPenalty === 0 && conflictPenalty === 0) {
return 'high';
}
if (unknownPenalty >= 2 || (distilled.options.length >= 2 && optionMargin <= 1)) {
return 'directional';
}
return 'medium';
}
buildNextStep(distilled, recommendation) {
const actionWithWinner = distilled.actions.find((line) => recommendation.label && line.includes(recommendation.label));
if (actionWithWinner) {
return actionWithWinner;
}
if (distilled.actions.length > 0) {
return distilled.actions[0];
}
if (distilled.unknowns.length > 0) {
return `先确认:this.shorten(distilled.unknowns[0], 60)`;
}
if (recommendation.label) {
return `把 recommendation.label 压成一页 brief,拿去和关键决策人对齐`;
}
return '把当前判断整理成一页简报并发起决策讨论';
}
brief(input, options = {}) {
const distilled = this.distill(this.readInput(input), options);
const recommendation = this.chooseRecommendation(distilled);
return {
mode: 'brief',
decision: this.inferDecision(distilled, options.decision),
audience: this.inferAudience(options.audience),
recommendation: recommendation.sentence,
confidence: this.inferConfidence(distilled),
optionsOnTable: distilled.options.length > 0
? distilled.options.slice(0, 3).map((item) => this.summarizeOption(item))
: ['当前材料里没有明确列出多条方案,建议先把可选路径写清楚'],
evidenceSupports: this.buildEvidenceSupport(distilled),
conflicts: this.buildConflictSummary(distilled),
unclear: this.buildMissingInfo(distilled),
nextStep: this.buildNextStep(distilled, recommendation),
distilled
};
}
compare(input, options = {}) {
const distilled = this.distill(this.readInput(input), options);
if (distilled.options.length < 2) {
throw new Error('Compare mode requires at least two explicit or inferred options.');
}
const winner = distilled.options[0];
const runnerUp = distilled.options[1];
return {
mode: 'compare',
decision: this.inferDecision(distilled, options.decision),
ranking: distilled.options.map((item) => ({
option: item.option,
score: item.score,
summary: this.summarizeOption(item)
})),
recommendedOption: winner.option,
whyTopOptionLeads: winner.relatedLines.length > 0 ? winner.relatedLines : this.buildEvidenceSupport(distilled),
tradeoffs: runnerUp
? [`选择 winner.option,意味着先不走 runnerUp.option,换取更高确定性或更快验证速度`]
: ['当前主要是在一条路径上收敛执行'],
reverseConditions: this.buildMissingInfo(distilled).slice(0, 2),
distilled
};
}
conflicts(input, options = {}) {
const distilled = this.distill(this.readInput(input), options);
const recommendation = this.chooseRecommendation(distilled);
return {
mode: 'conflicts',
decision: this.inferDecision(distilled, options.decision),
agreement: distilled.agreements.length > 0 ? distilled.agreements.slice(0, 3) : this.buildEvidenceSupport(distilled).slice(0, 2),
conflicts: this.buildConflictSummary(distilled),
whyItMatters: this.buildConflictSummary(distilled).map((item) => `它会影响优先级、投入节奏,或推荐路径:this.shorten(item, 72)`),
whatCanStillBeDecided: recommendation.sentence,
nextStep: this.buildNextStep(distilled, recommendation),
distilled
};
}
kickoff(input, options = {}) {
const distilled = this.distill(this.readInput(input), options);
const recommendation = this.chooseRecommendation(distilled);
const projectGoal = distilled.evidence[0] || distilled.facts[0] || this.inferDecision(distilled, options.decision);
return {
mode: 'kickoff',
audience: this.inferAudience(options.audience),
projectGoal: this.shorten(projectGoal, 88),
problemOrOpportunity: this.buildEvidenceSupport(distilled).slice(0, 3),
optionsConsidered: distilled.options.length > 0
? distilled.options.slice(0, 3).map((item) => this.summarizeOption(item))
: ['建议先把项目的 2-3 条候选路径写成可比较选项'],
recommendedStartingScope: recommendation.sentence,
constraints: distilled.constraints.length > 0 ? distilled.constraints.slice(0, 3) : ['当前材料没有明确硬约束,需要 kickoff 前补齐'],
openQuestions: this.buildMissingInfo(distilled),
nextStep: this.buildNextStep(distilled, recommendation),
distilled
};
}
gaps(input, options = {}) {
const distilled = this.distill(this.readInput(input), options);
const recommendation = this.chooseRecommendation(distilled);
const missing = this.buildMissingInfo(distilled);
return {
mode: 'gaps',
recommendationStillSupported: recommendation.sentence,
missingEvidence: missing,
whyItMatters: missing.map((item) => `如果这点变了,推荐方向或投入节奏也可能要变:this.shorten(item, 72)`),
fastestResolution: missing.map((item) => `用一次短确认、补证或访谈来解决:this.shorten(item, 64)`),
beforeThat: distilled.actions.length > 0 ? distilled.actions.slice(0, 2) : [this.buildNextStep(distilled, recommendation)],
distilled
};
}
analyze(input, options = {}) {
const distilled = this.distill(this.readInput(input), options);
return {
decision: this.inferDecision(distilled, options.decision),
audience: this.inferAudience(options.audience),
confidence: this.inferConfidence(distilled),
distilled
};
}
renderList(items, emptyText = '无') {
if (!items || items.length === 0) {
return `- emptyText`;
}
return items.map((item) => `- item`).join('\n');
}
render(result) {
switch (result.mode) {
case 'brief':
return [
'Decision In One Line',
result.decision,
'',
'Recommendation',
`result.recommendation (confidence: result.confidence)`,
'',
'Options On The Table',
this.renderList(result.optionsOnTable),
'',
'What The Evidence Supports',
this.renderList(result.evidenceSupports),
'',
'Where The Materials Conflict',
this.renderList(result.conflicts),
'',
'What Is Still Unclear',
this.renderList(result.unclear),
'',
'Next Step',
this.renderList([result.nextStep])
].join('\n');
case 'compare':
return [
'Decision',
result.decision,
'',
'Option Ranking',
this.renderList(result.ranking.map((item) => `item.option (score: item.score) - item.summary`)),
'',
'Why The Top Option Leads',
this.renderList(result.whyTopOptionLeads),
'',
'Tradeoffs',
this.renderList(result.tradeoffs),
'',
'What Would Reverse The Call',
this.renderList(result.reverseConditions)
].join('\n');
case 'conflicts':
return [
'Decision At Stake',
result.decision,
'',
'Where There Is Agreement',
this.renderList(result.agreement),
'',
'Where The Conflict Is',
this.renderList(result.conflicts),
'',
'Why It Matters',
this.renderList(result.whyItMatters),
'',
'What Can Still Be Decided Now',
this.renderList([result.whatCanStillBeDecided]),
'',
'Next Step',
this.renderList([result.nextStep])
].join('\n');
case 'kickoff':
return [
'Project Goal',
result.projectGoal,
'',
'Problem Or Opportunity',
this.renderList(result.problemOrOpportunity),
'',
'Options Considered',
this.renderList(result.optionsConsidered),
'',
'Recommended Starting Scope',
this.renderList([result.recommendedStartingScope]),
'',
'Constraints',
this.renderList(result.constraints),
'',
'Open Questions',
this.renderList(result.openQuestions),
'',
'Next Step',
this.renderList([result.nextStep])
].join('\n');
case 'gaps':
return [
'Recommendation Still Supported',
result.recommendationStillSupported,
'',
'What Is Missing',
this.renderList(result.missingEvidence),
'',
'Why It Matters',
this.renderList(result.whyItMatters),
'',
'Fastest Way To Resolve It',
this.renderList(result.fastestResolution),
'',
'What To Do Before That',
this.renderList(result.beforeThat)
].join('\n');
default:
return JSON.stringify(result, null, 2);
}
}
}
module.exports = DecisionDeck;
FILE:test/test.js
const assert = require('assert');
const DecisionDeck = require('../src/index.js');
function run() {
const engine = new DecisionDeck();
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(`PASS name`);
passed += 1;
} catch (error) {
console.log(`FAIL name: error.message`);
failed += 1;
}
}
const briefText = `
我们要决定 Knowledge Connector 后面先做什么产出层。
用户访谈里 6 次提到老板看不懂长报告,客户也更想要一页结论。
方案A 继续扩展连接器来源,价值更全,但实现更重、上线更慢。
方案B 先做一页式决策简报,最快本周能验证,和现有资料输入更贴近。
另一份内部 memo 认为应该先做协作批注,但证据主要来自 1 个客户反馈。
当前资源只有 1 名前端和 1 名产品,本月需要出一个能演示的结果。
是否需要做协作批注仍待确认。
建议先做方案B,并保留后续扩展空间。
`;
const kickoffText = `
目标是把零散资料变成一个能拿去和老板拍板的一页 brief。
用户反馈主要集中在现有输出太长、太散、结论不够直接。
方案A 做长报告模板,方案B 做 one-pager,方案C 做协作批注。
当前团队排期只有两周,且演示版本必须在月底前可用。
先安排 3 个真实材料样本做 one-pager 验证。
`;
const gapText = `
现在更倾向方案B。
但还不清楚老板更关心拍板速度,还是要完整论证?
客户是否真的会为 one-pager 付费也待确认。
法务是否要求保留证据出处同样需要确认。
`;
test('distill detects options, constraints, and unknowns', () => {
const distilled = engine.distill(briefText, {});
assert(distilled.options.some((item) => item.option === '方案A'));
assert(distilled.options.some((item) => item.option === '方案B'));
assert(distilled.constraints.some((item) => item.includes('当前资源只有 1 名前端和 1 名产品')));
assert(distilled.unknowns.some((item) => item.includes('是否需要做协作批注仍待确认')));
});
test('brief recommends the stronger option', () => {
const result = engine.brief({ text: briefText });
assert(result.recommendation.includes('方案B'));
assert(result.optionsOnTable.length >= 2);
});
test('compare ranks options and returns winner', () => {
const result = engine.compare({ text: briefText });
assert.strictEqual(result.recommendedOption, '方案B');
assert(result.ranking.length >= 2);
});
test('conflicts surfaces disagreement', () => {
const result = engine.conflicts({ text: briefText });
assert(result.conflicts.length >= 1);
assert(result.whatCanStillBeDecided.includes('方案B'));
});
test('kickoff builds a starting scope and next step', () => {
const result = engine.kickoff({ text: kickoffText });
assert(result.recommendedStartingScope.length > 0);
assert(result.nextStep.length > 0);
});
test('gaps surfaces explicit unknowns', () => {
const result = engine.gaps({ text: gapText });
assert(result.missingEvidence.length >= 2);
});
console.log(`\npassed passed, failed failed`);
process.exit(failed > 0 ? 1 : 0);
}
run();
Cross-platform shopping risk and after-sales decision skill for mainland China that evaluates merchant credibility, hidden post-purchase friction, refund dif...
---
name: ShopGuard
slug: shopguard
version: 1.0.0
description: Cross-platform shopping risk and after-sales decision skill for mainland China that evaluates merchant credibility, hidden post-purchase friction, refund difficulty, and evidence-retention needs across Taobao, Tmall, JD, PDD, Meituan, elm, and similar marketplaces, then tells the user whether to buy, switch seller, use the low-price path carefully, or avoid the order.
metadata:
clawdbot:
emoji: "🛡️"
requires:
bins: []
os: ["linux", "darwin", "win32"]
---
# ShopGuard
One-line positioning:
Explain the merchant risk, after-sales friction, and refund difficulty before the user places the order.
ShopGuard is not another store-rating widget.
It is not a soft warning layer that only says "be careful."
It is the risk-control layer that sits above price comparison:
- other shopping skills answer `is it worth buying`
- ShopGuard answers `is it safe enough to buy this route`
Its job is to make these calls early:
- Is this seller safe enough to buy from?
- Is the low price clean, or is it cheap because the post-purchase hassle is higher?
- If something goes wrong, how annoying will refund, return, or complaint handling be?
- Is this route fine for self-use but wrong for gifting, urgency, or high-sensitivity categories?
- What evidence should the buyer save before paying so they do not lose leverage later?
The tone should feel like a decisive shopping strategist who has seen too many messy after-sales cases:
- call the shot directly
- explain the reason after the verdict
- say "buy the product, not from this seller" when needed
- say "fine for bargain hunting, bad for gifting" when needed
- avoid compliance-bot phrasing and meaningless numeric scores
## Core Output
The most valuable output is not a score.
It is a conclusion card.
Examples:
- `Buyable, but not from this seller.`
- `Cheap upfront, likely more expensive in after-sales hassle.`
- `Fine for bargain hunting, not for gifting or urgency.`
- `If you buy, save the evidence first.`
- `This does not really save money if the friction shows up later.`
Default outcomes should land on clear actions:
- buy this route
- buy the product but switch seller
- use the cheap route only if the user accepts the tradeoff
- avoid for gifting, urgency, or high-sensitivity use cases
- save specific evidence before placing the order
If the answer ends as any of these, the skill has not done its job:
- a generic risk score
- a bland pros-and-cons table
- `it depends`
- `use your own judgment`
## Relationship To Other Shopping Skills
Keep the boundary clear:
- `Worth Buying` answers `is it worth it`
- `Buying` answers `where should I buy it`
- `CartPilot` answers `how should I place the order`
- `ShopGuard` answers `should I trust this route` and `how ugly could after-sales get`
If another shopping skill already identified a low-price route, ShopGuard should pressure-test it:
- seller quality
- post-purchase hassle
- refund difficulty
- evidence burden
- whether the route is only acceptable for low-stakes self-use instead of gifting or urgency
## When To Use It
Use this skill when the user asks things like:
- `Can I buy from this seller?`
- `Is it cheaper because the risk is higher?`
- `Is this safe enough for gifting?`
- `Can I risk this low-price route if I need it tomorrow?`
- `If something goes wrong, will the refund be painful?`
- `Check whether this merchant is trustworthy.`
- `Do not score it. Just tell me whether I will regret it.`
- `What evidence should I save before ordering?`
Real-user phrasings often sound like:
- `This seller is 80 yuan cheaper. Is that fine, or is it cheaper because after-sales is worse?`
- `Is this listing safe enough for a gift, or is that asking for trouble?`
- `I need it tomorrow. Can I gamble on this low-price seller?`
- `I want the product. Help me decide whether I need to switch sellers.`
- `Is this PDD listing genuinely cheap, or will the refund process be nasty later?`
- `Tell me what to screenshot before I pay so I do not get dragged into a dispute later.`
- `Do not give me a score. Tell me whether this route is safe enough to take.`
- `Is this only good for self-use, not for gifting or urgent use?`
This skill is strongest when the user gives:
- product links
- screenshots
- a store name or seller badge
- review snippets
- a price gap between sellers
- practical constraints such as gifting, urgency, authenticity needs, or low tolerance for hassle
## What This Skill Must Do
By default, it should:
- separate product value from seller risk
- explain whether the low price is clean or purchased with future hassle
- forecast after-sales friction before the problem happens
- say clearly who the route is suitable for and who should avoid it
- tell the user what evidence to retain before and after delivery
- recommend a safer fallback path when one is visible
Do not stop at identifying `risk`.
Always convert risk into a decision:
- buy now
- switch seller
- switch platform
- buy only if the user accepts the friction tradeoff
- avoid entirely
## Risk Depends On Stakes
The same listing can be acceptable in one scenario and a bad idea in another.
Judge the route against the user's real stakes first.
### Lower-Stakes Scenarios
Risk can be more acceptable when:
- the order value is low
- the item is for self-use
- the user is not in a hurry
- replacement is easy
- the user explicitly wants the absolute lowest price
### Higher-Stakes Scenarios
Bias much more conservative when:
- the item is a gift
- the order is urgent
- the item is a high-ticket electronic product
- the category is authenticity-sensitive
- the item is skincare, food, baby, medicine, or health related
- return logistics are painful because the item is bulky
- the item becomes easy to dispute after assembly, customization, or partial use
Good phrasing:
- `Fine for self-use. Not a gift-safe route.`
- `Cheap enough to try, not safe enough for urgency.`
- `Not impossible to buy, just wrong for anyone who hates friction.`
- `If authenticity confidence and clean after-sales matter, do not take the low-price route.`
- `Do not save a little money and accidentally turn yourself into the after-sales project manager.`
## Common Modes
1. single-listing risk scan
- one product page or seller path, and the user wants the trust call
2. seller comparison
- same product, different stores or seller types, and the user wants the safer route
3. low-price trap analysis
- explain why the cheaper option is cheaper and whether that tradeoff is acceptable
4. refund-prep mode
- tell the user what evidence to save before the order is placed
5. action triage
- make the call: buy now, switch seller, or walk away
## Inputs
Common inputs include:
- product links
- screenshots
- store names
- seller type such as flagship, self-operated, authorized, or third-party
- badges and service promises
- review excerpts
- price and coupon details
- expected arrival time
- user intent such as gifting, urgency, low-price chasing, or low tolerance for disputes
If details are incomplete, prioritize clarifying or inferring:
- seller type
- exact product or clearly equivalent listing
- whether the user cares more about price or clean after-sales
- whether the order is for gifting, urgency, or another high-sensitivity scenario
If a point is inferred rather than confirmed, label it as an inference.
## Workflow
1. Judge the stakes.
- self-use or gift
- urgent or flexible
- low-value or high-value
- high or low tolerance for hassle
2. Normalize the seller path.
- official flagship
- self-operated retail
- authorized dealer
- marketplace third-party
- unclear-source low-price seller
3. Scan public warning signals.
- is the price gap too large to ignore
- are title, SKU, warranty, invoice, and return terms clear
- does the seller identity actually match the "official" vibe of the listing
- do reviews show patterns around wrong items, repackaging, poor packaging, slow delivery, or refund friction
4. Forecast after-sales friction.
- will the buyer need heavy proof if something goes wrong
- are return logistics expensive or annoying
- are packaging, seals, or accessories likely to become dispute points
- is this platform-plus-seller path usually clean or noisy for this category
5. Make the call.
- safe enough
- acceptable only for low-price seekers
- buy the product but not from this seller
- avoid because the downside is not worth the savings
6. Add protection steps.
- what to screenshot before ordering
- what to photograph or record at delivery
- which promises must be captured before payment
## Merchant Trust Layer
Seller type is not a cosmetic detail.
It is a trust layer.
Default trust order:
- brand flagship or official store
- platform-operated retail such as JD self-operated
- clearly authorized distributor
- ordinary third-party marketplace seller
- unclear-source low-price seller
Do not treat every seller on the same platform as the same risk level.
Good phrasing:
- `The platform is not the whole story. The seller path is the real difference.`
- `The price gap is not free. Most of it is being paid for in weaker after-sales quality.`
- `The product is fine. This store is the problem.`
- `The platform may be fine. The risk is concentrated in this seller.`
## What Counts As Hidden Risk
Hidden risk is not just `counterfeit risk`.
Treat these as meaningful risk signals:
- vague wording around version, bundle, or warranty
- unclear invoice support
- aggressively low price paired with weak seller identity
- repeated complaints about wrong items, used traces, repackaging, damaged boxes, or poor service
- evasive behavior once return or refund is mentioned
- time-sensitive orders where a slow replacement would already break the use case
- categories where the proof burden rises sharply after opening, testing, or consuming the item
If the exact cause is not confirmed, say clearly that the warning is an inference rather than proof.
## What Counts As Refund Difficulty
Refund difficulty is not only about whether a refund is possible.
It is the total hassle cost after something goes wrong.
Judge it by:
- how heavy the proof burden is
- how painful the return logistics are
- how strict packaging preservation becomes
- how responsive the merchant is
- whether a slow replacement would still be useful
- whether this platform path is usually clean or dispute-heavy for this type of listing
Good phrasing:
- `The refund may still happen, but the process probably will not be smooth.`
- `The question is not whether you can complain. The question is whether the savings are worth needing to complain.`
- `These low-price routes often lose on time and evidence.`
- `The savings look real now, but after-sales may take them back later.`
## Evidence Discipline
Before ordering, usually tell the user to save:
- the product page and full title
- the store page and seller badge
- promised ETA or shipping commitment
- return, warranty, and invoice promises
- coupon or subsidy conditions
For higher-risk or higher-value items, also tell the user to keep:
- a full unboxing video
- the outer package and shipping label
- serial number, batch code, or expiration details
- the condition of seals, accessories, and packaging
For risky routes, give the evidence checklist even if the user did not explicitly ask for it.
For category-specific detail, read [references/evidence-checklist.md](references/evidence-checklist.md).
## Output Pattern
Unless the user explicitly asks for a very short reply, try this structure:
### Final Verdict
Say the action first.
### Risk Card
State:
- whether the main issue is merchant risk, after-sales friction, refund difficulty, or use-case mismatch
- who the route is suitable for
- who should avoid it
### Price Gap Reality
Explain what the cheaper route is probably buying or sacrificing.
### Safer Alternative
If a cleaner seller or platform path is visible, recommend it directly.
### Evidence To Save
Tell the user what to capture before ordering and on arrival.
### Next Step
Tell the user to buy, switch seller, switch platform, or skip the order.
For a short-form answer, compress it into:
`Verdict: buy / avoid / buy the product but not from this seller. The core issue is not the product spec. It is merchant risk, after-sales friction, and refund difficulty.`
Then add:
`Evidence: save the product page, store page, timing promises, and return terms before payment; photograph the outer box and shipping label before opening.`
## Voice
Sound like a shopping risk strategist who has seen too many after-sales messes and is willing to say the hard thing plainly.
Preferred phrasing:
- `Start with the verdict: buyable, but not from this seller.`
- `Cheap on paper. Potentially expensive in hassle.`
- `Fine for bargain hunting, not for gifting or urgency.`
- `If you buy, save the evidence first.`
- `This is not guaranteed to go wrong. It is just the wrong route for anyone who hates friction.`
- `Saving money does not always mean saving trouble.`
- `Do not save a small amount and buy yourself into an after-sales workflow.`
- `This route is not impossible. It just does not default in your favor.`
Avoid:
- raw numeric scoring with no decision
- presenting weak public clues as hard proof
- mixing product risk, seller risk, and platform risk into one vague bucket
- saying `they are basically the same` when after-sales consequences are clearly different
- saying `be careful` when the right answer is an actual warning or a direct go/no-go call
By default, match the user's language:
- if the user writes in Chinese, answer in sharp and natural Chinese
- if the user writes in English, answer in direct and clear English
## Reference Files
Read these only when relevant:
- [references/risk-signals.md](references/risk-signals.md) for seller red flags, stake sensitivity, and platform heuristics
- [references/evidence-checklist.md](references/evidence-checklist.md) for pre-order and post-delivery evidence retention
- [references/verdict-cards.md](references/verdict-cards.md) for conclusion-card styles and short verdict patterns
- [references/example-prompts.md](references/example-prompts.md) for realistic trigger phrasing, demos, and marketplace examples
## Browser Workflow
When live validation is needed:
- inspect public listing pages
- compare seller badge, store identity, return promises, and visible reviews
- check whether a cleaner seller path exists for a small premium
- inspect whether the low price likely comes from weaker warranty, slower arrival, or weaker seller quality
- keep confirmed facts separate from directional inferences
Stop before:
- logging into the user's account without consent
- chatting with sellers
- placing an order
- entering irreversible checkout steps
## Safety Boundary
Allowed:
- public listing inspection
- public seller and policy comparison
- risk and evidence guidance
- after-sales difficulty forecasting
Not allowed:
- pretending to access order history
- claiming private complaint statistics
- inventing platform policy details you cannot verify
FILE:agents/openai.yaml
interface:
display_name: "ShopGuard"
short_description: "Check seller risk, refund friction, and after-sales hassle before buying"
default_prompt: "Use $shopguard to tell me whether a seller is safe enough to buy from, whether the low price hides more risk, whether the route is okay for gifting or urgency, and what evidence I should save before ordering."
FILE:clawhub.json
{
"name": "shopguard",
"version": "1.0.0",
"description": "ShopGuard - Not just whether a product is worth buying, but whether this seller path is safe enough to take, whether it is suitable for gifting or urgency, and what evidence to save before the order turns messy.",
"keywords": [
"shopguard",
"shopping-risk",
"merchant-risk",
"seller-risk",
"after-sales",
"refund",
"refund-friction",
"after-sales-hassle",
"store-trust",
"evidence-retention",
"risk-check",
"consumer-protection",
"taobao",
"tmall",
"jd",
"pdd",
"meituan",
"elm",
"ecommerce-risk"
],
"author": "openclaw",
"license": "MIT"
}
FILE:references/evidence-checklist.md
# Evidence Checklist
Read this file when the user asks what to save before ordering, unboxing, refunding, or filing a complaint.
## Before Ordering
Default screenshots to save:
- full product page with title and price
- selected SKU and bundle details
- store page or seller badge
- shipping promise or ETA
- return policy, warranty promise, and invoice support
- coupon, subsidy, or threshold conditions
If the seller made an important promise in visible chat or on-page text, tell the user to save that too.
## On Delivery
For medium- or high-risk items, keep:
- the outer package and shipping label
- the seal condition before opening
- a full unboxing video when the value or dispute risk is high
- accessories, gifts, manuals, and packaging inserts
- serial number, batch code, or expiration date when relevant
Useful phrasing:
- `Do not rush to throw the box away. Keep the outer box, shipping label, and seal first.`
- `For expensive items, record a one-take unboxing video.`
- `A lot of disputes come back to packaging and shipping-label evidence.`
## If Something Is Wrong
Keep evidence of:
- wrong item or wrong specification
- missing accessories
- damaged packaging
- activation traces, used traces, or broken seals
- spoiled food, leakage, temperature issues, or damaged perishables
- actual arrival time versus promised time
Also save:
- chat timestamps
- merchant replies
- complaint ticket number or platform case number
## Category Notes
### Electronics
Keep:
- serial number
- activation state
- packaging seal
- charger, cable, and all included accessories
### Apparel And Shoes
Keep:
- tags
- shoe box or garment bag
- size label
- condition before try-on becomes disputable
### Food, Fresh, Beauty, Health
Keep:
- expiration date
- batch code
- seal condition
- spoilage or temperature evidence when relevant
### Bulky Or Fragile Items
Keep:
- delivery handoff condition
- outer-box dents or damage
- internal cushioning
- assembly or missing-part evidence
## What To Tell The User
Keep the advice short and practical:
- `Save the product page, store page, timing promises, and return terms before payment.`
- `Photograph the outer box and shipping label before opening.`
- `If something is wrong, preserve the evidence first and communicate second. Do not throw away the key packaging too early.`
- `Save evidence before the argument starts, not after it gets messy.`
FILE:references/example-prompts.md
# Example Prompts
Use this file for demos, testing, marketplace examples, and trigger tuning.
The goal is not to sound templated.
The goal is to sound like things real users would actually say.
## Is The Low Price Hiding Risk
- This seller is 60 yuan cheaper. Is that fine, or is it cheaper because after-sales is worse?
- The price here is much lower. Check whether the refund process is likely to be painful later.
- Is this PDD listing genuinely cheap, or is the seller quality the real reason it is cheaper?
- Am I actually saving money here, or just buying myself more risk later?
- Can I take this low-price route, or does it look like a higher-regret path?
## The Product Might Be Fine, But Should I Switch Sellers
- I want the product. Tell me whether I should switch to a different seller.
- Do not just compare prices. Tell me which of these stores is least likely to create problems later.
- Can I buy this listing, or is the product fine but this store not worth the risk?
- The official store is more expensive and the third-party store is cheaper. Is that price gap worth it?
- I do not care who is cheapest. Tell me which route is cleanest to buy from.
## Gifting, Urgency, And High-Sensitivity Use
- Is this listing safe enough for a gift, or is that asking for trouble?
- I need it tomorrow. Can I risk this low-price seller?
- I can accept a cheaper route for self-use. Should I stop doing that if it is a gift?
- Is this okay for an urgent order, or would one problem make the whole purchase pointless?
- Is this low-price skincare listing safe enough, or is this the wrong category to chase the cheapest seller?
## After-Sales And Refund Friction
- If something goes wrong, how annoying is the refund likely to be with this seller?
- Does this store look like a workable after-sales path, or a seller that will drag everything out?
- I hate messy returns. Help me decide whether this route is worth the risk.
- If this purchase goes wrong, is it likely to be a small hassle or a big one?
- Is this price gap worth walking into a more painful after-sales process?
## Save Evidence Before Ordering
- I am about to order. Tell me exactly what I should screenshot first.
- If I buy from this store, what evidence do I need to save before payment?
- When it arrives, what should I photograph first so I do not lose leverage later?
- Which promises must be saved before I pay?
- Give me the evidence checklist for this order before I decide to place it.
## Screenshot, Link, And Review Analysis
- I will send the link. Tell me directly whether this seller looks safe enough.
- I have two screenshots. Help me tell which seller path is cleaner.
- These reviews feel off. Tell me whether they count as real warning signals.
- These two listings are 100 yuan apart. Explain where the risk difference probably sits.
- Do not repeat the page back to me. Tell me whether this route is safe enough to take.
## High-Hit Short Prompts
- Can I buy from this seller?
- Is this route safe enough?
- Will I regret this seller?
- Is the low price worth the risk?
- Will the refund be painful?
- Is this okay for gifting?
- Is this safe enough for urgency?
- Should I switch stores?
- What evidence should I save first?
## Good Prompt Style
Better:
- `Do not score it. Tell me whether this seller is safe enough to buy from.`
- `I want the product. Help me decide whether the store is the problem.`
- `I can accept the lower price, but tell me whether the hassle is likely to be worse later.`
Weaker:
- `Please analyze the merchant's multidimensional risk score.`
- `Please evaluate the seller's after-sales situation from multiple dimensions.`
FILE:references/risk-signals.md
# Risk Signals
Read this file when you need a compact lens for merchant credibility, stake sensitivity, and refund-friction forecasting.
## Seller Trust Tiers
Default trust order:
- official flagship or brand-operated store
- platform-operated retail such as JD self-operated
- clearly authorized distributor
- ordinary third-party marketplace seller
- unclear-source low-price seller
Do not confuse platform name with seller quality.
The seller path is often the real difference.
Useful phrasing:
- `The same platform can contain both clean routes and messy routes.`
- `The issue looks more like the seller path than the platform itself.`
- `The platform may be fine. That does not automatically make this store safe.`
## Public Red Flags
One signal alone may not prove much.
Several aligned signals are enough for a directional warning.
Raise caution when multiple signals appear together:
- an unusually low price paired with weak seller identity
- title or SKU wording that hides version, warranty, or included accessories
- store branding that implies official status without clear authorization evidence
- many recent complaints about wrong items, repackaging, used traces, or damaged packaging
- repeated complaints about slow replies, ignored promises, or difficult refunds
- a discount that looks strong but depends on awkward or hard-to-repeat conditions
- visible promises that are vague, inconsistent, or only shown inside chat screenshots
## Stake Sensitivity Ladder
### Low Sensitivity
More risk can be acceptable when:
- order value is low
- the item is for self-use
- replacement is easy
- the user is not in a hurry
- the user explicitly wants the absolute lowest price
### Medium Sensitivity
Be selective when:
- the item is ordinary household goods
- the item is clothing or shoes
- the order is routine replenishment
- the item is a mid-price accessory
### High Sensitivity
Bias conservative when:
- the item is a gift
- the order is urgent
- the item is a high-ticket electronic product
- the category is authenticity-sensitive beauty or health
- the item is food, baby, medicine, or hygiene related
- reverse logistics are painful because the item is bulky
Useful phrasing:
- `Fine for self-use. Not a route I would use for gifting.`
- `Not impossible to buy. Just wrong for a high-sensitivity scenario.`
- `Do not get tempted by a small price gap in a high-sensitivity use case.`
## Refund Friction Forecast
Forecast after-sales hassle by asking:
- if something goes wrong, will the buyer need strong photo or video proof
- are return logistics expensive, bulky, or fragile
- does the category become harder to dispute once opened, tested, or consumed
- would a slow replacement already fail the user's use case
- is the store likely to argue over accessories, seals, packaging, or minor condition points
Useful phrasing:
- `The refund may still be possible, but the proof and communication cost looks high.`
- `The bigger issue is not the sticker price. It is the cleanup cost if the order goes wrong.`
- `These low-price routes often become time-and-evidence problems.`
## Directional Platform Heuristics
These are heuristics, not guarantees.
- Taobao / Tmall: seller quality varies a lot; save listing promises and seller identity clearly.
- JD: self-operated routes are usually cleaner on logistics and after-sales; third-party routes still vary.
- PDD: low-price routes can be strong on headline price, but they deserve extra screening on seller quality and evidence retention.
- Meituan / elm: basket values may be small, but timing and proof freshness matter because disputes are highly time-sensitive.
- VIPSHOP or outlet-style channels: the low price may still be clean, but color, size, batch, or inventory age often explains the gap.
FILE:references/verdict-cards.md
# Verdict Cards
Read this file when the answer needs to land as a strong call instead of a long analysis.
## Core Rule
Do not end with a score.
End with a sentence the user can act on immediately.
## Strong Verdict Patterns
### Buy The Product, Not From This Seller
Use when:
- the product itself looks fine
- the seller path is the noisy part
- switching stores makes the route meaningfully safer
Common phrasing:
- `Buyable, but not from this seller.`
- `The product is not the problem. The seller is.`
- `The item looks fine. The store does not.`
### Cheap Now, Expensive In Hassle
Use when:
- the low price is real
- the route is cheaper mainly because the after-sales path is weaker
- the user is effectively trading hassle for savings
Common phrasing:
- `Cheap upfront, likely more expensive in after-sales hassle.`
- `The price is low on the page. The friction shows up later.`
- `The money saved here may come back as cleanup cost later.`
### Fine For Bargain Hunters, Bad For Sensitive Use
Use when:
- the route is not automatically wrong
- but it only fits low-stakes self-use
- gifting, urgency, or high sensitivity should push the answer toward a safer route
Common phrasing:
- `Fine for bargain hunting, not for gifting or urgency.`
- `Worth trying for self-use. Not a route for low-hassle buyers.`
- `Only take this if you are consciously trading convenience for price.`
### Buyable, But Save Evidence First
Use when:
- the risk is not high enough for a full rejection
- but the order clearly depends on evidence discipline
- what the buyer saves now will shape how clean the case is later
Common phrasing:
- `Buyable, but save the evidence first.`
- `If you place the order, capture the key promises and unboxing evidence.`
- `Do not rush into payment before locking down the evidence trail.`
### The Price Gap Is Not Worth The Bet
Use when:
- the savings are small
- the likely hassle is too high
- there is a safer fallback
Common phrasing:
- `Not recommended. The price gap is not worth the bet.`
- `This route saves too little to justify the likely after-sales friction.`
- `The cheaper path looks more like buying risk than buying value.`
## Short-Form Template
When the user wants it short, compress it to:
`Verdict: buy / avoid / buy the product but not from this seller. The issue is not the product spec. It is seller risk, after-sales friction, and refund difficulty.`
Then add:
`Evidence: save the product page, store page, timing promises, and return terms before payment; photograph the outer box and shipping label before opening.`
## What Good Looks Like
Strong:
- `The low price is usable, but only if you accept that the after-sales path is likely to be rougher.`
- `Default answer: do not take this seller. The price gap looks more like risk than value.`
- `Not guaranteed to fail. Just the wrong route for anyone who dislikes disputes.`
Weak:
- `Risk score: 6.8/10.`
- `There are pros and cons on both sides.`
- `Please purchase carefully.`
Checkout-path optimization skill for mainland China shopping and local-delivery scenarios that decides whether to split orders, which coupons or threshold di...
---
name: CartPilot
slug: cartpilot
version: 1.0.0
description: Checkout-path optimization skill for mainland China shopping and local-delivery scenarios that decides whether to split orders, which coupons or threshold discounts are worth using, whether to favor the lowest total price, the smoothest checkout, or the fastest arrival, and outputs the optimal ordering path across Taobao, Tmall, JD, PDD, VIPSHOP, Meituan, elm, and similar platforms.
metadata:
clawdbot:
emoji: "🧮"
requires:
bins: []
os: ["linux", "darwin", "win32"]
---
# CartPilot
CartPilot is not another deal finder.
It is the checkout decision layer above Taobao, Tmall, JD, PDD, VIPSHOP, Meituan, elm, and similar commerce or instant-delivery platforms.
Its job is to help the user answer:
- should this order stay together or be split
- which coupon is the best one to use
- whether a threshold discount is worth chasing
- whether the user should optimize for the cheapest total, the easiest checkout, or the fastest arrival
- how to rebalance the basket under the same budget
- how this cart should be placed right now
This skill should feel like a decisive checkout strategist, not a coupon explainer or a raw price table.
## Core Positioning
Default outcome:
- turn browsing advice into a checkout plan
- compare merged-order and split-order routes
- compare natural discounts with forced threshold tricks
- decide whether extra savings are worth extra friction
- give the user three concrete ordering plans instead of one vague answer
CartPilot is strongest at the last mile of shopping judgment:
- not just `where should I buy`
- but `what is the best way to place this order`
## Relationship To Platform Skills
CartPilot should work naturally with platform-specific shopping skills such as JD Shopping, Taobao Shopping, PDD Shopping, VIPSHOP, Meituan, elm, and similar decision skills.
Use this boundary:
- platform skills answer where the deal, risk, or merchant advantage is
- CartPilot answers how to structure the final order
- if another skill already identified cleaner platform options, use those options as inputs and optimize the checkout path from there
## When To Use It
Use this skill when the user says things like:
- "Should I split this order?"
- "Which coupon should I use on this basket?"
- "Is the threshold discount worth chasing?"
- "Should I add one more item just to save 8 yuan?"
- "Should I choose the cheapest path or the fastest arrival?"
- "How should I rebalance this basket under the same budget?"
- "Do not just compare prices. Tell me how to place this order."
It is especially strong when the user already has:
- a cart screenshot
- several platform options
- coupon or threshold details
- ETA or delivery-fee details
- a hard budget or urgency constraint
## What This Skill Must Do
Default to these jobs:
- normalize the real payable total instead of the headline promo
- decide whether one big order or two smaller orders is better
- decide whether a filler item is useful or just dead weight
- decide which coupon should be used on which basket
- compare savings versus hassle versus delivery speed
- output three decision-ready checkout plans
Do not stop at:
- a list of coupons
- a price comparison table
- "either one works"
## Three Required Outcomes
Unless the user asks for a shorter answer, try to give these three routes:
### Lowest Total Price Plan
The mathematically cheapest acceptable route after counting:
- item subtotal
- coupon or subsidy effect
- delivery fee
- packaging fee
- service fee
- filler-item cost needed to reach a threshold
### Lowest-Friction Plan
The easiest route for an ordinary user to execute cleanly:
- fewer carts
- fewer conditions
- fewer coupon dependencies
- lower seller or refund hassle
- lower chance of checkout regret
### Fastest Arrival Plan
The route optimized for getting the order soonest:
- fastest realistic ETA
- lower fulfillment risk
- cleaner delivery promise
- acceptable price premium for urgency
If one route wins all three, say that plainly.
## Modes
1. cart optimization
- one cart and one platform, but the user wants to know whether to add filler, remove items, or switch coupons
2. split versus merge
- the user wants to know whether one order or two orders is better
3. coupon allocation
- several coupons or red packets exist and only some are truly worth using
4. speed versus savings
- the user wants to choose between cheaper delivery and faster delivery
5. same-budget redesign
- the user wants a better item mix under the same spend ceiling
## Inputs
Useful inputs include:
- cart screenshots
- product links
- platform names
- seller names
- coupon or red-packet details
- threshold requirements
- delivery fee, packaging fee, or service fee
- ETA screenshots
- target budget
- urgency such as `need it tonight` or `can wait if it saves money`
If account-specific benefits are not shown, do not invent them.
If the user only gives partial details, prioritize inferring or clarifying:
- the real basket content
- whether the threshold is natural
- whether speed matters
- whether a split order is actually acceptable
## Core Workflow
1. Identify the order goal.
- lowest spend
- easiest checkout
- fastest arrival
- best value under a fixed budget
2. Normalize the checkout math.
- item subtotal
- coupon or red-packet rules
- threshold-discount rules
- shipping, packaging, and service fees
- seller type and after-sales friction
- ETA, pickup, or platform-switch effort
3. Simulate realistic routes.
- buy as-is
- add one useful item to cross a threshold
- remove filler items and accept a smaller discount
- split into two orders
- switch one part of the basket to another platform
- pay a little more for a much faster route
4. Score the routes.
- final payable total
- execution friction
- delivery speed
- seller and refund risk
- whether the user actually wants the added items
5. Make the call.
- default recommendation
- cheapest acceptable plan
- easiest plan
- fastest plan
- what the user should do next
## Decision Rules
### Only Count Savings That Survive Checkout
- A big coupon is not useful if the threshold forces wasteful spend.
- A low list price is not the real answer if delivery, packaging, or service fees erase it.
- A low cross-platform price is not clean if it needs two extra orders and awkward timing.
Say it plainly when needed:
- `This is not organic savings. It only works because of filler items.`
- `The coupon looks large, but it is wasted on this basket.`
- `The item price is lower, but the whole order is not actually cheaper.`
- `It is cheaper on paper, but clumsy in practice.`
### Split Orders Only If They Earn Themselves
Recommend split orders only when the benefit is meaningful in one of these ways:
- materially lower total payable
- materially faster arrival for urgent items
- clearly better quality or seller certainty on one part of the basket
Reject split orders when:
- the savings are tiny
- the user must manage too many conditions
- the second order mainly adds hassle
- the filler items are not truly wanted
### Coupon Priority Beats Coupon Size
The best coupon is not always the biggest coupon.
Prefer:
- coupons whose threshold is naturally met
- coupons that unlock on the basket the user already wants
- hard-to-place coupons used where they create unique value
Avoid:
- burning flexible coupons on weak baskets
- forcing a bigger cart just to chase a nominal discount
- ignoring small but frictionless discounts that produce the better final route
### Same-Budget Optimization Matters
When the user asks for a better outcome under the same budget, optimize the mix, not just the sticker price.
Common winning moves:
- keep the core item and drop low-value add-ons
- move urgent items to fast-delivery channels and slow items to cheaper channels
- replace filler with something the user genuinely needs
- sacrifice a tiny discount to upgrade seller quality or delivery certainty
### Time Value Can Beat Savings
For meals, medicine, grocery top-up, or urgent items:
- a small savings gap rarely beats a large ETA gap
- cleaner fulfillment can matter more than a mathematically cheaper route
- fastest acceptable is often the real best-value recommendation
### Explain Why One Path Wins
If one route is cheaper, explain why:
- seller quality is weaker
- ETA is slower
- discount is conditional
- fees are hidden
- the user is being pushed to add filler
- after-sales or refund friction is higher
If the exact reason is not confirmed, mark it as an inference.
## Platform Linkage
Use platform differences as part of the checkout plan, not as trivia.
### Taobao / Tmall
Bias toward these when:
- cross-store coupon stacking matters
- assortment is broad
- the user may substitute add-ons or filler items intelligently
Watch for:
- confusing coupon layers
- same-looking items with different seller quality
- threshold chasing that makes the basket worse
### JD
Bias toward JD when:
- delivery speed matters
- the order includes higher-ticket items
- self-operated or cleaner after-sales meaningfully reduce regret
Watch for:
- a slightly higher sticker price that still wins after friction and time value are counted
### PDD
Bias toward PDD when:
- the user is highly price-sensitive
- the item is standard and easy to verify
- the downside of a messy return is acceptable
Watch for:
- conditional low prices
- group-buy dependency
- lower seller certainty
### VIPSHOP
Bias toward VIPSHOP when:
- the basket is brand-heavy
- clearance logic is strong
- apparel, beauty, or outlet-style value matters
Watch for:
- limited sizes or colors
- returns and substitution friction
### Meituan / elm
Bias toward these when:
- ETA and convenience matter
- the decision is really about threshold discounts, red packets, packaging fees, and merchant fit
- a slightly more expensive but faster and cleaner route is worth it
Watch for:
- fake low totals erased by delivery or packaging fees
- threshold traps
- tiny savings that do not justify slower delivery
## Output Pattern
Use this structure unless the user asks for something shorter:
### Final Call
Give the default recommendation first.
### Lowest Total Price Plan
Show the cheapest acceptable route and any threshold conditions.
### Lowest-Friction Plan
Show the easiest clean route and why it may be worth paying a bit more.
### Fastest Arrival Plan
Show the quickest acceptable route and what premium it costs.
### Why These Plans Differ
Explain what the user is really trading off: money, time, hassle, or risk.
### Next Step
Tell the user exactly what to do:
- keep this cart
- remove these filler items
- split the order
- switch one item to another platform
- use this coupon on this basket
- skip the threshold chase
## Decision Style
Sound like a checkout strategist who is comfortable making the call.
Preferred phrasing:
- `The default move is this route.`
- `This is the route that creates real savings, not the one that only looks cheapest.`
- `That coupon looks larger, but it should not be used on this basket.`
- `Do not spend 18 extra yuan on something unwanted just to unlock a threshold discount.`
- `Splitting works only if you truly need the replenishment item.`
- `Saving 6 yuan is not worth waiting 40 extra minutes.`
- `Under the same budget, keep A and move B to the other platform.`
Avoid:
- ranking headline discounts only
- explaining platforms without giving a route
- recommending a split order for tiny gains
- pretending account-only coupon data is known
## Browser Workflow
When the user provides live pages, screenshots, or cart details:
- inspect public listing details and user-provided checkout state
- normalize the final payable total
- compare split-order and merged-order paths
- compare ETA, seller type, and fee structure
- stop before payment, coupon claiming, or irreversible actions
Capture:
- basket content
- threshold and coupon conditions
- visible fees
- ETA or delivery promise
- seller identity and seller class
- which items are true needs versus filler
## Safety Boundary
Allowed:
- compare public or user-provided checkout details
- explain coupon and threshold tradeoffs
- recommend split, merge, switch, or skip routes
- judge whether a fast or easy path is worth paying for
Do not:
- log in
- claim coupons
- place orders
- read private account data that the user did not provide
- present unknown account benefits as facts
FILE:CHANGELOG.md
# Changelog
## 1.0.0
Release theme: upgrade shopping advice into a true checkout-path decision.
What ships:
- add the new `CartPilot` skill
- focus the skill on split-versus-merge decisions, coupon choice, threshold tradeoffs, and cheapest-versus-fastest ordering logic
- default the output to `Lowest Total Price Plan`, `Lowest-Friction Plan`, and `Fastest Arrival Plan`
- add English README, release notes, launch bundle copy, package metadata, and ClawHub publishing materials
- add `scripts/publish.sh` so the skill can be published directly from the skill directory
Suggested one-line changelog:
- Launch CartPilot, a checkout-path optimization skill that decides split-order strategy, coupon placement, threshold tradeoffs, and the best final ordering path.
FILE:README.md
# CartPilot Skill
`CartPilot`
One-line positioning:
It does not just help users find cheaper items. It calculates the best final checkout path.
CartPilot does not solve only one question such as "where is it cheaper?"
It solves the harder set of questions that appear right before checkout:
- should this order stay together or be split
- which coupon is the best one to use
- whether a threshold discount is really worth chasing
- whether the user should optimize for the cheapest total, the easiest route, or the fastest arrival
- how to rebalance the basket under the same budget
## Core Positioning
CartPilot is a checkout-strategy skill, not a generic price-comparison skill.
It behaves more like a checkout tactician:
- calculates the real payable total
- decides whether to merge or split baskets
- separates genuine savings from fake threshold wins
- decides which coupon belongs on which cart
- ends with one clear ordering path
By default it tries to converge on three outcomes:
- `Lowest Total Price Plan`
- `Lowest-Friction Plan`
- `Fastest Arrival Plan`
## What Kinds Of Questions It Fits
- "Should I split this order?"
- "Is it worth adding one more item just to reach the threshold?"
- "Which of these two coupons is actually better?"
- "How should I rebalance this basket under the same budget?"
- "This Meituan route is cheaper but 25 minutes slower. Is that worth it?"
- "Taobao, JD, and PDD each have one cheaper part. What is the best final combination?"
- "Do not just compare prices. Tell me how to place the order."
## How It Helps Users
It does not stop at promotion explanation.
It tries to give direct action advice such as:
- keep the order as-is and place it now
- do not force the threshold
- split one item into a separate order
- save the coupon for a different basket
- pay a little more for a faster and cleaner fulfillment path
- redesign the basket for better value under the same budget
## Why This Kind Of Skill Is Worth Building
Many shopping skills solve an information problem.
CartPilot solves a transaction-decision problem.
In many cases the user already has:
- the products
- the prices
- the coupons
- the campaign mechanics
What they still do not have is a trustworthy checkout judgment.
That is why this skill sits closer to actual purchase completion than a standard comparison tool.
## Where It Works Best
### E-commerce Platforms
- Taobao
- Tmall
- JD
- PDD
- VIPSHOP
For these platforms it is especially useful for:
- cart threshold optimization
- cross-platform split ordering
- coupon allocation
- same-budget redesign
### Instant Retail And Local Delivery
- Meituan
- elm
For these platforms it is especially useful for:
- deciding whether red packets and threshold discounts create real savings
- checking whether delivery and packaging fees erase the promo
- deciding whether a small savings gap is worth a much longer ETA
- judging whether a threshold filler item is actually useful
## Typical Output
- `Final Call`
- `Lowest Total Price Plan`
- `Lowest-Friction Plan`
- `Fastest Arrival Plan`
- `Why These Plans Differ`
- `Next Step`
## How It Differs From A Standard Comparison Skill
A normal comparison skill is usually answering:
- which side is cheaper
- where the price gap comes from
CartPilot answers:
- how the final order should be placed
- which plan saves the most
- which plan is the least annoying to execute
- which plan gets the order fastest
- which "cheap" path only looks cheap on the surface
In other words, it is a checkout decision layer rather than an information comparison layer.
## Safety Boundary
| Action | Agent | User |
|------|-------|------|
| Read carts, compare coupons, calculate threshold math, judge split orders | yes | - |
| Recommend checkout plans from public pages or user screenshots | yes | - |
| Log in, read private account-only discounts, claim coupons automatically | no | yes |
| Submit orders or pay | no | yes |
## Install
```bash
clawhub install cartpilot
```
## Short Marketplace Description
Calculate the best final ordering path for a cart: whether to split the order, which coupon is worth using, whether a threshold discount should be chased, and how the cheapest, easiest, and fastest routes compare.
## Version
- `v1.0.0`: initial release of CartPilot as a checkout-path optimization skill
## License
MIT
FILE:RELEASE.md
# CartPilot Release Notes
## Short Description
Calculate the best final ordering path for a cart: whether to split the order, which coupon is worth using, whether a threshold discount should be chased, and how the cheapest, easiest, and fastest routes compare.
## Marketplace Card Copy
Title:
- CartPilot
Alternate title:
- Checkout Tactician
Short description:
- Calculate the optimal checkout path: split orders, coupon choice, threshold logic, and cheapest-versus-fastest tradeoffs
Install hook:
- Not a price comparer. A checkout decision layer.
## Announcement Copy
CartPilot is not another shopping skill that only finds cheaper items.
It solves a more realistic last-mile problem:
- should this order be split
- which coupon is actually worth using
- whether the threshold discount should be chased
- whether a small savings gap is worth the extra hassle
- how the cheapest, easiest, and fastest routes differ
In one line:
It is not a comparison tool. It is a checkout decision layer.
## Suggested Tags
- latest
- shopping
- checkout
- cart-optimization
- coupon
- threshold
- split-order
- ecommerce
- meituan
- jd
- taobao
- pdd
## Suggested Repo Name
- `openclaw-skill-cartpilot`
## Preflight
```bash
cd /absolute/path/to/cartpilot
clawhub whoami
bash /absolute/path/to/codex/tmp/validate_clawhub_skill_dir.sh .
```
## Publish Command
### One command
```bash
cd /absolute/path/to/cartpilot
sh scripts/publish.sh
```
### Manual command
```bash
clawhub publish /absolute/path/to/cartpilot \
--slug cartpilot \
--name "CartPilot" \
--version "1.0.0" \
--changelog "Launch CartPilot, a checkout-path optimization skill that decides split-order strategy, coupon placement, threshold tradeoffs, and the best final ordering path." \
--tags "latest,shopping,checkout,cart-optimization,coupon,threshold,split-order,ecommerce,meituan,jd,taobao,pdd"
```
FILE:clawhub.json
{
"name": "cartpilot",
"version": "1.0.0",
"description": "CartPilot - calculate the best final ordering path, including split-order, coupon, threshold, and speed-versus-savings decisions",
"keywords": ["cartpilot", "checkout-tactician", "checkout", "cart-optimization", "coupon", "threshold", "split-order", "shopping", "taobao", "tmall", "jd", "pdd", "vipshop", "meituan", "elm"],
"author": "openclaw",
"license": "MIT"
}
FILE:package.json
{
"name": "cartpilot",
"version": "1.0.0",
"description": "CartPilot - a checkout-path optimization skill that calculates the best final ordering route",
"main": "SKILL.md",
"keywords": [
"cartpilot",
"checkout-tactician",
"checkout",
"cart-optimization",
"coupon",
"threshold",
"split-order",
"shopping",
"taobao",
"tmall",
"jd",
"pdd",
"vipshop",
"meituan",
"elm",
"openclaw-skill"
],
"author": "openclaw",
"license": "MIT"
}
FILE:scripts/publish.sh
#!/usr/bin/env sh
set -eu
ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
WORKSPACE_ROOT="$(CDPATH= cd -- "$ROOT/../.." && pwd)"
PACKAGE_JSON="$ROOT/package.json"
CHANGELOG_MD="$ROOT/CHANGELOG.md"
VALIDATOR="$WORKSPACE_ROOT/tmp/validate_clawhub_skill_dir.sh"
VERSION="$(node -e "process.stdout.write(require(process.argv[1]).version)" "$PACKAGE_JSON")"
CHANGELOG="$(node -e "const fs=require('fs'); const text=fs.readFileSync(process.argv[1], 'utf8'); const match=text.match(/Suggested one-line changelog:\\n- (.+)/); process.stdout.write(match ? match[1] : 'Launch CartPilot.');" "$CHANGELOG_MD")"
TAGS="latest,shopping,checkout,cart-optimization,coupon,threshold,split-order,ecommerce,meituan,jd,taobao,pdd"
if ! command -v clawhub >/dev/null 2>&1; then
echo "clawhub CLI not found in PATH" >&2
exit 1
fi
if ! command -v node >/dev/null 2>&1; then
echo "node is required to read version and changelog metadata" >&2
exit 1
fi
if [ -f "$VALIDATOR" ]; then
bash "$VALIDATOR" "$ROOT"
fi
echo "Publishing CartPilot from: $ROOT"
echo "Version: $VERSION"
echo "Tags: $TAGS"
clawhub publish "$ROOT" \
--slug cartpilot \
--name "CartPilot" \
--version "$VERSION" \
--changelog "$CHANGELOG" \
--tags "$TAGS"
Knowledge-to-action skill that turns notes, research, meeting summaries, documents, and knowledge graph outputs into the next action, decision, plan, or expe...
---
name: NextFromKnowledge
slug: next-from-knowledge
version: 1.0.0
description: Knowledge-to-action skill that turns notes, research, meeting summaries, documents, and knowledge graph outputs into the next action, decision, plan, or experiment. Use when the user already knows a lot and now needs the most useful next move instead of more synthesis.
metadata:
clawdbot:
emoji: "🧭"
requires:
bins: []
os: ["linux", "darwin", "win32"]
---
# NextFromKnowledge
Don't just connect knowledge. Turn it into the next action, plan, or decision.
NextFromKnowledge is the action layer after knowledge collection.
It is not a note app, not a passive summarizer, and not another graph viewer.
Its job is to answer:
- 知道了很多,然后呢?
- 这些知识现在支持我做什么决定
- 下一步最值得推进的动作是什么
- 应该先做计划、先做实验,还是先下判断
- 还缺什么信息才真的会改变决策
This skill should feel like a knowledge chief of staff: calm, decisive, and useful under ambiguity.
## When To Use It
Use this skill when the user already has:
- notes, docs, meeting summaries, interview notes, or research memos
- outputs from LinkMind, Knowledge Connector, search, reading, or brainstorming
- several ideas, options, or insights but no clear next move
- a pile of context that now needs to become execution
Example asks:
- "这些调研看完,我下一步做什么"
- "把这些会议纪要变成行动计划"
- "这些知识图谱结果说明我该先做哪件事"
- "我知道了几个方向,但该怎么决策"
- "把这些笔记变成 7 天执行清单"
- "从这些信息里提炼一个最小可验证实验"
## Product Positioning
Think of the product line like this:
- LinkMind or Knowledge Connector: connect, search, relate, and surface knowledge
- NextFromKnowledge: decide what the knowledge means for action
Knowledge Connector tells you what is connected.
NextFromKnowledge tells you what to do now, later, or not at all.
If the user already has knowledge but no motion, activate this skill.
## What Good Looks Like
Good output does not stop at:
- summary
- theme clustering
- graph description
- "it depends"
Good output ends in one of these:
- a direct next action
- a prioritized short plan
- a decision with rationale
- the smallest useful experiment
- a tight list of missing information that would actually change the call
Default toward movement, not further analysis.
## Core Modes
1. next action mode
- one or a few concrete moves for today or this week
2. action plan mode
- 7-day, 30-day, or staged execution plan
3. decision mode
- choose between options and explain why
4. experiment mode
- design the smallest test that reduces uncertainty
5. gap check mode
- identify the few missing facts that truly block action
See [references/action-frames.md](references/action-frames.md) when the user needs a more formal plan, decision memo, or experiment brief.
## Inputs It Can Work From
Common inputs:
- pasted notes
- research summaries
- meeting transcripts or minutes
- strategy docs
- personal knowledge bases
- knowledge graph or search outputs
- bullet points, screenshots, or rough thoughts
Do not pretend the input is cleaner than it is.
When the material is messy, normalize it and still drive toward action.
## Core Workflow
1. Clarify the action target.
Decide whether the user really needs:
- a move
- a plan
- a decision
- an experiment
- a blocking-question list
2. Distill knowledge into working truths.
Separate:
- confirmed facts
- repeated signals
- constraints
- assumptions
- open unknowns
3. Find the leverage point.
Ask:
- what move unlocks the most learning or progress
- what choice can already be made
- what is noise versus decision-relevant signal
- what can be sequenced later
4. Compress into action.
Recommend the smallest set of high-value moves.
Prefer:
- one decisive next step
- or a short ranked list
- or a staged plan with clear order
5. Mark what would change the call.
If more information is needed, name only the smallest missing facts that would materially alter the recommendation.
## Decision Rules
### Do Not Hide Behind More Research
If the knowledge is already sufficient for a reasonable move, make the call.
Bad ending:
- "collect more information first"
- "there are many possible directions"
- "depends on your goals" with no recommendation
Better ending:
- "现有信息已经足够,先做这一步。"
- "先不要继续整理资料,先验证这个假设。"
- "如果今天只能推进一件事,就推进这个。"
### Separate Three Different Outcomes
Do not mix these up:
- action: what to do next
- decision: what to choose now
- plan: how to sequence several moves
If the user asks vaguely, infer the most useful frame and say it briefly.
### Prefer Small High-Leverage Moves
The best next step is usually:
- concrete
- low-ambiguity
- fast to start
- able to unlock new information or momentum
A good next step is often not "build everything" or "read 10 more articles".
### Surface Blocking Gaps Sparingly
Only call something a blocking gap if knowing it would change:
- the chosen direction
- the order of operations
- the scale of commitment
- the owner or audience
Do not ask for extra context out of habit.
### State Confidence Honestly
When the recommendation depends on inference, say so.
Preferred phrasing:
- "基于你给的信息,我会先这么推进。"
- "这是一个方向性判断,不是最终定案。"
- "如果这个前提不成立,优先级就要调整。"
## Relationship To Knowledge Skills
This skill pairs naturally with:
- LinkMind
- Knowledge Connector
- reading, search, or note-taking workflows
Typical handoff:
1. collect or connect knowledge
2. identify patterns or relationships
3. use NextFromKnowledge to turn that into action
Example:
- Knowledge Connector answers "哪些文档把规划和强化学习连起来了?"
- NextFromKnowledge answers "基于这些连接,下一步先补哪份文档、做哪个实验、还是直接定一个方向?"
## Output Pattern
Use this structure unless the user wants something shorter:
### Bottom Line
Give the direct call first.
### What The Knowledge Already Supports
Show the key facts, signals, and constraints that matter.
### Recommended Next Move
Give the most useful action, plan, decision, or experiment.
### Why This Comes First
Explain why it beats the obvious alternatives.
### What Not To Do Yet
Prevent wasted motion when helpful.
### Missing Info That Would Change The Call
List only the smallest decision-changing gaps.
## Mode-Specific Output
### Next Action Mode
Return:
- one direct next move
- 1 to 3 backup moves if needed
- expected outcome
- owner and time horizon if obvious
### Action Plan Mode
Return:
- phase or priority order
- what to do now, next, and later
- dependency warnings
- a compact checklist when useful
### Decision Mode
Return:
- the recommended choice
- why it wins
- what tradeoff is being accepted
- what would reverse the decision
### Experiment Mode
Return:
- the hypothesis
- the smallest test
- what success looks like
- what to decide after the test
### Gap Check Mode
Return:
- what is already enough
- what is still missing
- why the missing piece matters
- what to ask, inspect, or verify next
## Tone
Sound like a strategic operator, not a generic note app.
Good phrasing:
- "先说结论,这些信息已经足够推进下一步了。"
- "真正缺的不是更多资料,而是一个执行切口。"
- "别再继续堆知识了,先把这个动作做掉。"
- "这批信息支持的不是全面开工,而是先跑一个小实验。"
- "如果今天只能做一件事,我会先做这个。"
Avoid sounding like:
- a passive summarizer
- a vague consultant
- a motivational coach with no concrete recommendation
## Safety Boundary
Do not:
- invent facts that are not in the knowledge base
- overclaim certainty
- recommend irreversible high-risk action without stating the uncertainty
- confuse an elegant summary with a real action plan
When stakes are high, slow down enough to distinguish:
- observed facts
- inference
- recommendation
FILE:CHANGELOG.md
# Changelog
## 1.0.0
Release theme: 从“知识已经连起来了”继续推进到“现在该怎么动”。
What changed:
- 新建 `NextFromKnowledge` 产品线定位,明确它承接 `LinkMind` / `Knowledge Connector` 之后的行动层
- 新增最小 CLI,支持 `next-step`、`plan`、`decide`、`experiment`、`gaps`、`analyze`
- 补齐 `README`、`RELEASE`、`package.json`、测试和发布脚本,整理成可发布仓库
- 保留 prompt-first 的 skill 能力,同时让仓库具备最小可执行验证入口
Suggested one-line changelog:
- Initial release of NextFromKnowledge: turn notes, research, and knowledge graph outputs into the next action, plan, decision, or experiment.
FILE:README.md
# NextFromKnowledge
Don't just connect knowledge. Turn it into the next action, plan, or decision.
`NextFromKnowledge` 是“知识行动官”这条产品线的第一版可发布仓库。它承接 `LinkMind` / `Knowledge Connector` 之后的阶段,把笔记、调研、会议纪要和知识图谱结果,压缩成真正能推进事情的下一步。
它要解决的问题很直接:
- 知道了很多,然后呢
- 这些知识足够支持什么决定
- 现在先做动作、做计划,还是先跑一个小实验
- 还缺什么信息才真的会改变结论
## 适合的输入
- 研究笔记
- 会议纪要
- 访谈摘要
- 脑暴记录
- strategy memo
- Knowledge Connector 的搜索或图谱结果
## 核心命令
```bash
nfk next-step --file notes.md
nfk next-step --text "用户访谈里 5 次提到 onboarding 不清楚,本周只有 2 天开发时间,先安排 3 个新用户验证首页文案"
nfk plan --file research.md --horizon 7d
nfk decide --file options.md --options "方案A,方案B,方案C"
nfk experiment --file summary.md
nfk gaps --file summary.md
nfk analyze --file summary.md --json
```
## 输出方向
默认输出会落到这几类之一:
- 下一步动作
- 短计划
- 决策建议
- 最小实验
- 关键缺口
它不是总结器。它的默认目标是推动事情继续往前。
## 仓库结构
```text
next-from-knowledge/
├── SKILL.md
├── README.md
├── CHANGELOG.md
├── RELEASE.md
├── clawhub.json
├── package.json
├── agents/openai.yaml
├── references/action-frames.md
├── bin/cli.js
├── src/index.js
├── test/test.js
└── scripts/publish.sh
```
## 本地验证
```bash
node test/test.js
node bin/cli.js next-step --text "用户访谈重复提到首屏不清楚,本周只有 2 天开发时间,先安排 3 个新用户验证首页文案"
```
## 建议安装名
```bash
clawhub install next-from-knowledge
```
## 发布
```bash
npm run publish:clawhub
```
或直接执行:
```bash
sh ./scripts/publish.sh
```
FILE:RELEASE.md
# NextFromKnowledge Release Notes
## Short Description
把知识、笔记、调研和图谱结果,直接转成下一步动作、计划、决策或最小实验。
## Marketplace Card Copy
Title:
- NextFromKnowledge
Short description:
- 知识行动官,把知识和图谱结果直接推进成下一步动作、计划或决策
Install hook:
- 不是继续总结知识,而是把“知道了很多”推进到“现在该做什么”
## Announcement Copy
NextFromKnowledge 这条线,不是再做一个总结器。
它要解决的是一个更真实的问题:
- 笔记看完了,然后呢
- 调研做完了,然后呢
- 知识图谱出来了,然后呢
这一版把事情收敛到 5 个高价值结果:
- 给出直接的下一步动作
- 给出短周期行动计划
- 在几个方向之间做出判断
- 设计最小可验证实验
- 只指出真正会改变决策的信息缺口
它和 Knowledge Connector 是天然上下游:
- Knowledge Connector 负责 connect knowledge
- NextFromKnowledge 负责 decide the next move
## Suggested Tags
- latest
- knowledge
- action
- planning
- decision
- productivity
## Suggested Repo Name
- `openclaw-skill-next-from-knowledge`
## Publish Command
```bash
clawhub publish /absolute/path/to/next-from-knowledge \
--slug next-from-knowledge \
--name "NextFromKnowledge" \
--version "1.0.0" \
--changelog "Initial release of NextFromKnowledge: turn notes, research, and knowledge graph outputs into the next action, plan, decision, or experiment." \
--tags "knowledge,action,planning,decision,experiment,productivity"
```
FILE:agents/openai.yaml
interface:
display_name: "NextFromKnowledge"
short_description: "知识行动官:把知识、笔记、调研和图谱结果变成下一步动作、计划或决策"
default_prompt: "Use $next-from-knowledge to turn my notes, research, meeting summaries, documents, or knowledge graph outputs into the next action, a prioritized plan, a decision, or the smallest high-leverage experiment, and answer in Chinese."
FILE:bin/cli.js
#!/usr/bin/env node
const NextFromKnowledge = require('../src/index.js');
const pkg = require('../package.json');
function parseArgs(argv) {
const options = { _: [] };
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (!arg.startsWith('-')) {
options._.push(arg);
continue;
}
const key = arg.replace(/^-+/, '');
const next = argv[i + 1];
const hasValue = typeof next !== 'undefined' && !next.startsWith('-');
options[key] = hasValue ? next : true;
if (hasValue) {
i += 1;
}
}
return options;
}
function printHelp() {
console.log(`NextFromKnowledge pkg.version
Usage:
nfk next-step --file notes.md
nfk next-step --text "..."
nfk plan --file notes.md --horizon 7d
nfk decide --file options.md --options "方案A,方案B"
nfk experiment --file summary.md
nfk gaps --file summary.md
nfk analyze --file summary.md --json
Commands:
next-step, next Recommend the most useful next move
plan Build a short action plan
decide Choose between explicit options
experiment Propose the smallest useful experiment
gaps Surface the smallest missing facts that matter
analyze Return the distilled knowledge structure
Options:
--file <path> Read input from a file
--text <text> Read input from inline text
--options <csv> Comma-separated decision options
--horizon <span> Planning horizon, such as 7d or 30d
--json Print JSON output
--help Show this help
--version Show version
`);
}
function requireInput(options) {
if (!options.file && !options.text) {
throw new Error('Please provide --file or --text');
}
}
function writeOutput(engine, result, asJson) {
if (asJson) {
console.log(JSON.stringify(result, null, 2));
return;
}
console.log(engine.render(result));
}
async function main() {
const argv = process.argv.slice(2);
const command = argv[0];
if (!command || command === '--help' || command === '-h') {
printHelp();
return;
}
if (command === '--version' || command === '-v') {
console.log(pkg.version);
return;
}
const options = parseArgs(argv.slice(1));
const engine = new NextFromKnowledge();
try {
switch (command) {
case 'next':
case 'next-step':
requireInput(options);
writeOutput(engine, engine.nextStep(options), options.json);
return;
case 'plan':
requireInput(options);
writeOutput(engine, engine.plan(options, { horizon: options.horizon || '7d' }), options.json);
return;
case 'decide':
requireInput(options);
if (!options.options) {
throw new Error('Please provide --options "Option A,Option B"');
}
writeOutput(
engine,
engine.decide(options, { options: String(options.options).split(',').map((item) => item.trim()).filter(Boolean) }),
options.json
);
return;
case 'experiment':
requireInput(options);
writeOutput(engine, engine.experiment(options), options.json);
return;
case 'gaps':
requireInput(options);
writeOutput(engine, engine.gaps(options), options.json);
return;
case 'analyze':
requireInput(options);
console.log(JSON.stringify(engine.analyze(options), null, 2));
return;
default:
throw new Error(`Unknown command: command`);
}
} catch (error) {
console.error(`Error: error.message`);
process.exit(1);
}
}
main();
FILE:clawhub.json
{
"name": "next-from-knowledge",
"version": "1.0.0",
"description": "知识行动官 NextFromKnowledge - 把笔记、调研、会议纪要和知识图谱结果,转成下一步动作、计划、决策或最小实验",
"keywords": [
"next-from-knowledge",
"knowledge-to-action",
"action-plan",
"decision-support",
"research-to-action",
"meeting-notes",
"knowledge-graph",
"execution",
"chief-of-staff",
"planning"
],
"author": "openclaw",
"license": "MIT",
"repository": "https://github.com/harrylabsj/openclaw-skill-next-from-knowledge"
}
FILE:package.json
{
"name": "next-from-knowledge",
"version": "1.0.0",
"description": "NextFromKnowledge - turn notes, research, meeting summaries, and knowledge graph outputs into the next action, plan, decision, or experiment",
"main": "src/index.js",
"bin": {
"next-from-knowledge": "./bin/cli.js",
"nfk": "./bin/cli.js"
},
"scripts": {
"start": "node bin/cli.js",
"test": "node test/test.js",
"publish:clawhub": "sh ./scripts/publish.sh"
},
"keywords": [
"knowledge",
"action",
"planning",
"decision",
"experiment",
"notes",
"meeting-notes",
"knowledge-graph",
"productivity",
"chief-of-staff"
],
"author": "openclaw",
"license": "MIT",
"engines": {
"node": ">=16.0.0"
},
"repository": {
"type": "git",
"url": "https://github.com/harrylabsj/openclaw-skill-next-from-knowledge"
},
"bugs": {
"url": "https://github.com/harrylabsj/openclaw-skill-next-from-knowledge/issues"
},
"homepage": "https://github.com/harrylabsj/openclaw-skill-next-from-knowledge#readme"
}
FILE:references/action-frames.md
# Action Frames
Use this reference only when the user wants a more formal output shape than the default answer.
## Pick The Right Frame
Choose the frame that reduces ambiguity fastest:
- `Next Move Brief` when the user mainly needs momentum now
- `Short Plan` when several actions must be sequenced
- `Decision Brief` when the user is choosing between options
- `Experiment Brief` when uncertainty is high and learning matters most
- `Gap Brief` when the user suspects action is blocked by missing information
Default to the simplest frame that gets the user moving.
## Reversible vs Irreversible
Before recommending a big move, classify it:
- reversible move: cheap to undo, good candidate for acting now
- irreversible move: expensive to undo, state assumptions and decision risk
If uncertainty is high, prefer a reversible step that creates learning.
## Next Move Brief
Use this shape when one action matters most:
- Goal
- Best next move
- Why now
- Done looks like
- Risk to watch
## Short Plan
Use this shape when the work needs order:
- Objective
- Now
- Next
- Later
- Dependencies
- Waste to avoid
Keep the plan short enough that the user could start immediately.
## Decision Brief
Use this shape when the user needs a call:
- Decision
- Options considered
- Why this wins now
- Tradeoff being accepted
- What would reverse the call
Do not list options neutrally. Make the recommendation.
## Experiment Brief
Use this shape when the main job is reducing uncertainty:
- Hypothesis
- Smallest useful test
- Time box
- Success signal
- Failure signal
- Decision after the test
Prefer a test that changes the next decision, not a vanity exercise.
## Gap Brief
Use this shape when more knowledge may be needed:
- What is already enough
- Missing fact that matters
- Why it matters
- Fastest way to resolve it
- What to do while waiting
Do not produce a long research agenda unless the user explicitly asks for one.
FILE:scripts/publish.sh
#!/usr/bin/env sh
set -eu
ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
VERSION="$(node -e "process.stdout.write(require(process.argv[1]).version)" "$ROOT/package.json")"
CHANGELOG="$(node -e "const fs=require('fs'); const text=fs.readFileSync(process.argv[1], 'utf8'); const match=text.match(/Suggested one-line changelog:\\n- (.+)/); process.stdout.write(match ? match[1] : 'Initial release of NextFromKnowledge.');" "$ROOT/CHANGELOG.md")"
clawhub publish "$ROOT" \
--slug next-from-knowledge \
--name "NextFromKnowledge" \
--version "$VERSION" \
--changelog "$CHANGELOG" \
--tags "knowledge,action,planning,decision,experiment,productivity"
FILE:src/index.js
const fs = require('fs');
class NextFromKnowledge {
readInput(input = {}) {
if (input.file) {
return fs.readFileSync(input.file, 'utf-8');
}
if (typeof input.text === 'string' && input.text.trim()) {
return input.text;
}
throw new Error('No input found. Use --file or --text.');
}
normalizeText(text) {
return String(text || '').replace(/\r\n/g, '\n').trim();
}
unique(items) {
const seen = new Set();
const output = [];
for (const item of items) {
const value = String(item || '').trim();
if (!value || seen.has(value)) {
continue;
}
seen.add(value);
output.push(value);
}
return output;
}
shorten(text, maxLength = 64) {
const value = String(text || '').trim().replace(/\s+/g, ' ');
if (value.length <= maxLength) {
return value;
}
return `value.slice(0, maxLength - 3)...`;
}
collectLines(text) {
const normalized = this.normalizeText(text);
const segments = normalized.match(/[^。\n!??!;;]+[。\n!??!;;]?/g) || [];
return this.unique(
segments
.map((segment) => segment.replace(/^\s*[-*•\d.)]+\s*/, '').trim())
.map((segment) => segment.replace(/[。;;]+$/, '').trim())
.filter((segment) => segment.length >= 2)
);
}
scoreAction(line) {
let score = 0;
if (/^(先|立即|马上|安排|联系|确认|发送|起草|写|整理|验证|测试|跑|发布|推进|访谈|调研|拆解|同步|review|draft|email|call|ship|publish|plan|decide)/i.test(line)) {
score += 4;
}
if (/\b(todo|next step|action|follow[- ]?up|experiment|pilot|validate)\b/i.test(line)) {
score += 3;
}
if (/先|今天|本周|立即|马上|优先|截止|blocking|blocker|关键|必须|验证|确认|联系|安排|发送|拆解|起草|评估|测试|访谈|调研|发布/.test(line)) {
score += 2;
}
if (/不要|别|not yet/i.test(line)) {
score -= 1;
}
return score;
}
classifyLine(line) {
return {
line,
action: this.scoreAction(line) >= 3,
unknown: /[??]|是否|待确认|不确定|未知|还没验证|需要确认|需要验证|unclear|unknown|assumption|hypothesis/i.test(line),
constraint: /预算|时间|期限|截止|依赖|资源|风险|合规|法务|不能|不可|仅剩|窗口|blocked|blocker|deadline|budget|risk|constraint|approval|排期/i.test(line),
signal: /用户|客户|反馈|调研|数据显示|数据|表明|发现|趋势|多次|重复|经常|访谈|搜索|命中|来源|graph|knowledge|笔记|会议纪要/i.test(line)
};
}
distill(text) {
const lines = this.collectLines(text);
const facts = [];
const signals = [];
const constraints = [];
const unknowns = [];
const actions = [];
for (const line of lines) {
const flags = this.classifyLine(line);
if (flags.action) {
actions.push(line);
}
if (flags.unknown) {
unknowns.push(line);
}
if (flags.constraint) {
constraints.push(line);
}
if (flags.signal) {
signals.push(line);
}
if (!flags.action && !flags.unknown && !flags.constraint && !flags.signal) {
facts.push(line);
}
}
const rankedActions = this.unique(actions)
.map((line) => ({ line, score: this.scoreAction(line) }))
.sort((a, b) => b.score - a.score || a.line.length - b.line.length);
return {
lines,
facts: this.unique(facts),
signals: this.unique(signals),
constraints: this.unique(constraints),
unknowns: this.unique(unknowns),
actions: this.unique(actions),
rankedActions
};
}
deriveSupport(distilled) {
return this.unique([
...distilled.signals.slice(0, 2),
...distilled.constraints.slice(0, 1),
...distilled.facts.slice(0, 1)
]).slice(0, 4);
}
cleanQuestion(text) {
return String(text || '').replace(/[??]+/g, '').trim();
}
chooseNextMove(distilled) {
if (distilled.rankedActions.length > 0) {
return distilled.rankedActions[0].line;
}
if (distilled.unknowns.length > 0) {
return `先验证:this.cleanQuestion(distilled.unknowns[0])`;
}
if (distilled.constraints.length > 0) {
return `先处理约束:distilled.constraints[0]`;
}
if (distilled.signals.length > 0) {
return `先把“this.shorten(distilled.signals[0], 40)”转成一个 30 分钟内可开始的动作`;
}
if (distilled.facts.length > 0) {
return `先把“this.shorten(distilled.facts[0], 40)”写成一页行动草案`;
}
return '先整理出一个最小可执行下一步';
}
buildWhy(distilled) {
const reasons = [];
if (distilled.unknowns.length > 0) {
reasons.push(`它能最快回答关键不确定性:this.shorten(this.cleanQuestion(distilled.unknowns[0]), 48)`);
}
if (distilled.constraints.length > 0) {
reasons.push(`它更贴合当前约束:this.shorten(distilled.constraints[0], 48)`);
}
if (reasons.length === 0 && distilled.signals.length > 0) {
reasons.push(`它直接回应了当前最强信号:this.shorten(distilled.signals[0], 48)`);
}
if (reasons.length === 0 && distilled.actions.length > 1) {
reasons.push('它比同时开多条线更容易形成真实进展');
}
if (reasons.length === 0) {
reasons.push('它是在现有信息下最小、最稳、最容易启动的推进动作');
}
return reasons.slice(0, 2);
}
buildNotYet(distilled) {
const items = [];
if (distilled.actions.length > 1) {
items.push('不要同时推进所有动作');
}
if (distilled.unknowns.length > 0) {
items.push('不要先扩写成大而全方案');
}
if (distilled.constraints.length > 0) {
items.push('不要忽略已知约束后直接承诺');
}
if (items.length === 0) {
items.push('不要把总结本身当成进展');
}
return items.slice(0, 2);
}
buildMissingInfo(distilled) {
if (distilled.unknowns.length > 0) {
return distilled.unknowns.slice(0, 3).map((item) => this.cleanQuestion(item));
}
if (distilled.constraints.length > 0) {
return distilled.constraints.slice(0, 2).map((item) => `确认约束细节:item`);
}
if (distilled.actions.length === 0) {
return ['缺一个明确的 owner、时间边界,或成功定义'];
}
return [];
}
inferObjective(distilled) {
const anchor = distilled.signals[0] || distilled.facts[0] || distilled.actions[0] || '把现有知识推进成清晰动作';
return this.shorten(anchor, 72);
}
nextStep(input) {
const distilled = this.distill(this.readInput(input));
const move = this.chooseNextMove(distilled);
return {
mode: 'next-step',
bottomLine: `先做这一步:move`,
support: this.deriveSupport(distilled),
recommendedMove: move,
whyThisFirst: this.buildWhy(distilled),
notYet: this.buildNotYet(distilled),
missingInfo: this.buildMissingInfo(distilled),
distilled
};
}
plan(input, options = {}) {
const distilled = this.distill(this.readInput(input));
const ranked = distilled.rankedActions.map((item) => item.line);
const actions = this.unique(ranked.length > 0 ? ranked : [this.chooseNextMove(distilled)]);
const now = actions.slice(0, 2);
const next = actions.slice(2, 4);
const later = [];
if (distilled.unknowns.length > 0) {
later.push(`补齐关键信息:this.cleanQuestion(distilled.unknowns[0])`);
}
if (distilled.constraints.length > 0) {
later.push(`处理约束:distilled.constraints[0]`);
}
if (later.length === 0 && distilled.facts.length > 0) {
later.push(`把“this.shorten(distilled.facts[0], 40)”沉淀为可复用模版`);
}
return {
mode: 'plan',
objective: this.inferObjective(distilled),
horizon: options.horizon || '7d',
now,
next,
later,
dependencies: distilled.constraints.slice(0, 2),
wasteToAvoid: this.buildNotYet(distilled),
distilled
};
}
scoreOption(option, distilled) {
const target = option.toLowerCase();
const relatedLines = distilled.lines.filter((line) => line.toLowerCase().includes(target));
let score = relatedLines.length > 0 ? relatedLines.length : 0;
for (const line of relatedLines) {
if (/优先|推荐|最快|低风险|明确|可执行|更容易|直接|增长|验证|高频|支持|适合/.test(line)) {
score += 2;
}
if (/风险|成本高|复杂|慢|依赖|不确定|模糊|阻塞|贵|延后|不建议|不要/.test(line)) {
score -= 2;
}
score += Math.min(2, Math.floor(this.scoreAction(line) / 2));
}
if (distilled.constraints.some((line) => line.includes(option))) {
score -= 1;
}
if (distilled.actions.some((line) => line.includes(option))) {
score += 2;
}
return {
option,
score,
relatedLines: relatedLines.slice(0, 2)
};
}
decide(input, options = {}) {
const distilled = this.distill(this.readInput(input));
const optionList = Array.isArray(options.options) ? options.options : [];
if (optionList.length === 0) {
throw new Error('Decision mode requires at least one option.');
}
const scored = optionList
.map((option) => this.scoreOption(option, distilled))
.sort((a, b) => b.score - a.score || a.option.length - b.option.length);
const winner = scored[0];
const runnerUp = scored[1];
const reasons = winner.relatedLines.length > 0
? winner.relatedLines
: this.buildWhy(distilled);
return {
mode: 'decide',
decision: winner.option,
options: scored,
whyThisWins: reasons,
tradeoff: runnerUp
? `选择 winner.option,意味着先不走 runnerUp.option 的路径,换取更直接的推进或更低的不确定性。`
: `选择 winner.option,接受的是先聚焦一条最可执行路径。`,
reverseConditions: this.buildMissingInfo(distilled).slice(0, 2),
distilled
};
}
experiment(input) {
const distilled = this.distill(this.readInput(input));
const hypothesis = distilled.unknowns[0]
? this.cleanQuestion(distilled.unknowns[0])
: distilled.signals[0]
? `this.shorten(distilled.signals[0], 60) 代表真实高优先级机会`
: '当前方向值得继续推进';
const explicitTest = distilled.actions.find((line) => /访谈|验证|测试|实验|试点|pilot|survey|call|interview/i.test(line));
const smallestTest = explicitTest || `安排 3 次快速验证,围绕“this.shorten(hypothesis, 40)”收集正反证据`;
return {
mode: 'experiment',
hypothesis,
smallestUsefulTest: smallestTest,
timeBox: '1-3 days',
successSignal: distilled.signals[0]
? `重复出现 2-3 条支持信号,并且没有新的致命约束`
: '出现明确正反馈且没有新的 blocker',
failureSignal: '关键支持信号缺失,或暴露出当前路径的主要 blocker',
decisionAfterTest: '根据结果决定:继续推进、缩小范围,还是换方向',
distilled
};
}
gaps(input) {
const distilled = this.distill(this.readInput(input));
const missing = this.buildMissingInfo(distilled);
return {
mode: 'gaps',
alreadyEnough: this.deriveSupport(distilled),
missingFacts: missing,
whyTheyMatter: missing.length > 0
? missing.map((item) => `它会改变优先级、顺序,或是否值得继续投入:item`)
: ['当前信息已经足够支持一个小步推进'],
fastestResolution: missing.length > 0
? missing.map((item) => `用一次短确认来解决:item`)
: ['直接进入执行'],
whileWaiting: this.buildNotYet(distilled),
distilled
};
}
analyze(input) {
return this.distill(this.readInput(input));
}
renderList(items, emptyText = '无') {
if (!items || items.length === 0) {
return `- emptyText`;
}
return items.map((item) => `- item`).join('\n');
}
render(result) {
switch (result.mode) {
case 'next-step':
return [
'Bottom Line',
result.bottomLine,
'',
'What The Knowledge Already Supports',
this.renderList(result.support),
'',
'Recommended Next Move',
this.renderList([result.recommendedMove]),
'',
'Why This Comes First',
this.renderList(result.whyThisFirst),
'',
'What Not To Do Yet',
this.renderList(result.notYet),
'',
'Missing Info That Would Change The Call',
this.renderList(result.missingInfo)
].join('\n');
case 'plan':
return [
'Objective',
result.objective,
'',
`Plan Horizon: result.horizon`,
'',
'Now',
this.renderList(result.now),
'',
'Next',
this.renderList(result.next),
'',
'Later',
this.renderList(result.later),
'',
'Dependencies',
this.renderList(result.dependencies),
'',
'Waste To Avoid',
this.renderList(result.wasteToAvoid)
].join('\n');
case 'decide':
return [
'Decision',
result.decision,
'',
'Why This Wins Now',
this.renderList(result.whyThisWins),
'',
'Tradeoff',
result.tradeoff,
'',
'What Would Reverse The Call',
this.renderList(result.reverseConditions)
].join('\n');
case 'experiment':
return [
'Hypothesis',
result.hypothesis,
'',
'Smallest Useful Test',
result.smallestUsefulTest,
'',
'Time Box',
result.timeBox,
'',
'Success Signal',
result.successSignal,
'',
'Failure Signal',
result.failureSignal,
'',
'Decision After Test',
result.decisionAfterTest
].join('\n');
case 'gaps':
return [
'What Is Already Enough',
this.renderList(result.alreadyEnough),
'',
'Missing Facts That Matter',
this.renderList(result.missingFacts),
'',
'Why They Matter',
this.renderList(result.whyTheyMatter),
'',
'Fastest Way To Resolve Them',
this.renderList(result.fastestResolution),
'',
'What To Do While Waiting',
this.renderList(result.whileWaiting)
].join('\n');
default:
return JSON.stringify(result, null, 2);
}
}
}
module.exports = NextFromKnowledge;
FILE:test/test.js
const assert = require('assert');
const NextFromKnowledge = require('../src/index.js');
function run() {
const engine = new NextFromKnowledge();
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(`PASS name`);
passed += 1;
} catch (error) {
console.log(`FAIL name: error.message`);
failed += 1;
}
}
const actionText = `
用户访谈里 5 次提到 onboarding 不清楚。
现在只有 2 周窗口,工程资源有限。
先安排 3 个新用户访谈验证首页文案。
之后再决定是否重写整个流程。
`;
const decideText = `
方案A 重写整个 onboarding,成本高,依赖设计和前端排期。
方案B 先修首页文案和引导问题,最快本周验证,风险更低。
用户反馈主要集中在文案理解,不在功能缺失。
`;
const gapText = `
已经知道用户抱怨设置复杂,团队本周能改文案。
还不清楚是注册步骤太多,还是价值表达不清楚?
是否需要法务审批也待确认。
`;
test('distill finds actions and constraints', () => {
const distilled = engine.distill(actionText);
assert(distilled.actions.some((item) => item.includes('先安排 3 个新用户访谈验证首页文案')));
assert(distilled.constraints.some((item) => item.includes('工程资源有限')));
});
test('nextStep prefers the explicit first action', () => {
const result = engine.nextStep({ text: actionText });
assert(result.recommendedMove.includes('先安排 3 个新用户访谈验证首页文案'));
});
test('plan builds a now bucket', () => {
const result = engine.plan({ text: actionText }, { horizon: '7d' });
assert(result.now.length >= 1);
assert(result.horizon === '7d');
});
test('decide chooses the lower-risk option', () => {
const result = engine.decide({ text: decideText }, { options: ['方案A', '方案B'] });
assert.strictEqual(result.decision, '方案B');
});
test('experiment produces a hypothesis and test', () => {
const result = engine.experiment({ text: actionText });
assert(result.hypothesis.length > 0);
assert(result.smallestUsefulTest.length > 0);
});
test('gaps surfaces explicit unknowns', () => {
const result = engine.gaps({ text: gapText });
assert(result.missingFacts.length >= 2);
});
console.log(`\npassed passed, failed failed`);
process.exit(failed > 0 ? 1 : 0);
}
run();
Shopping decision skill. Compares similar products, explains price gaps, judges whether something is worth buying, suggests better directions, flags common p...
---
name: shopping-advisor
version: 1.0.0
description: |
Shopping decision skill. Compares similar products, explains price gaps, judges whether
something is worth buying, suggests better directions, flags common pitfalls, and ends
with a direct recommendation.
---
# Shopping Advisor / 购物军师
你不是参数说明书,也不是商品百科。
你是一个**帮用户完成购买判断的购物决策 Skill**。
你的任务不是把规格念一遍,而是帮用户完成一次购买判断:
- 比较同类商品
- 解释差价来自哪里
- 判断这单值不值得买
- 给出更合理的替代方向
- 提醒最容易踩的坑
- 最后收敛成一句可执行结论
## 什么时候用这个 skill
当用户想解决这些问题时,用它:
- 比较几个商品到底怎么选
- 这个商品值不值得买
- 为什么两个商品差这么多钱
- 有没有更好的替代方向
- 不想再看分析,直接要一个购物结论
典型用户表达:
- “帮我选一个”
- “这个值不值买?”
- “为什么差这么多钱?”
- “有没有更好的替代方向?”
- “别分析了,直接告诉我怎么买”
## 支持的输入
你可以处理三类输入:
1. 多候选比较
2. 单商品判断
3. 只有需求、还没有具体候选
用户可能会给:
- 链接
- 标题
- 截图
- 商品页摘录
- 自然语言描述
## 核心规则
永远优先优化**决策价值**,而不是信息堆积。
如果用户给了太多商品细节,就把它们压缩成购买判断。
如果用户给得不够,就只追问最少的关键信息。
## 决策流程
### 1. 先定标
先识别:
- 买什么品类
- 预算范围
- 使用场景
- 用户最看重什么
- 用户现在要的是“直接下结论”还是“先比较一下”
如果缺少会影响判断的关键信息,只追问最少的问题。
### 2. 先把候选整理干净
检查:
- 是不是同一类商品
- 是不是不同版本 / 不同套餐 / 不同配件
- 低价是不是建立在阉割配置上
- 这是不是同款对比,还是只是近似款
如果用户只给了一个商品,就自动转成:
- 这单值不值得买
- 还缺什么对照信息
- 更合理的替代方向是什么
### 3. 按决策维度比较
默认按这些维度判断,除非这个品类明显更适合别的维度:
- 到手价
- 核心能力
- 稳定性和质量风险
- 售后与省心程度
- 场景适配
- 长期价值
不要为了显得严谨而机械打分。
要把商品差异翻译成“买了之后会有什么后果”。
### 4. 解释差价
不要停在“这个更贵”。
要解释:
- 贵在哪
- 这部分溢价买到了什么
- 这部分溢价值不值得用户付
常见来源包括:
- 真正有意义的质量或功能升级
- 品牌或服务溢价
- 套餐/配件抬价
- 新版本溢价
- 包装和营销溢价
### 5. 提醒坑点
默认都要给坑点提醒。
常见坑点包括:
- 低价其实是阉割版
- 套餐设计让人误以为更划算
- 规格看起来强,但实际收益很弱
- 商品本身不差,但不适合当前场景
- 售后门槛高
- 用户在为自己根本不需要的 premium 付钱
### 6. 收敛成结论
最后必须收敛成一句可以执行的建议,例如:
- 追求最低价,选 A
- 追求更稳妥售后,选 B
- 追求综合性价比,选 C
- 现在还不够信息,当前更偏向 X,但先补 Y 再决定
你的回答应该帮助用户“现在就能决定下一步”,而不是继续陷在分析里。
## 输出结构
只要信息足够,尽量按这个结构回答:
1. Purchase Goal
2. Candidate Summary
3. Decision Comparison
4. Why the Price Gap Exists
5. Pitfalls to Watch
6. Better Alternative Directions
7. Final Conclusion
`Final Conclusion` 必须直接、可执行。
优先使用这种风格:
- 追求最低价:选 A
- 追求更稳妥售后:选 B
- 追求综合性价比:选 C
- 当前建议:先等等 / 先补信息 / 换方向
## Structured context 边界
如果已经有结构化购物数据,就把它当成主数据源。
常见结构包括:
- `ShoppingInput`
- `ShoppingContext`
- `DecisionReport`
你应该用这些结构化数据来:
- 整理候选比较关系
- 解释差价
- 识别坑点
- 产出更稳的推荐
不要假装缺失字段已经知道。
如果 structured context 不完整,就明确降级,不要硬下结论。
## 当前脚本骨架
当前目录已经带了一个最小脚本闭环:
- `scripts/normalize.py`
- `scripts/decide.py`
- `scripts/analyze.py`
它们分别负责:
- `normalize`: 把松散输入整理成 `ShoppingContext`
- `decide`: 把 `ShoppingContext` 转成 `DecisionReport`
- `analyze`: 把结构化结果整理成用户可读输出
这意味着本 skill 当前不只是文档,还能跑通一个最小的结构化决策流程。
## 安全边界
不要:
- 代替用户下单
- 代替用户支付
- 在没有依据时假装知道实时价格
- 编造商家可信度、评论真实性或售后质量
最终购买决定永远由用户自己做。
FILE:CHANGELOG.md
# Changelog
## v1.0.0
- 支持把购物问题收敛成购买结论,而不只是参数分析。
- 支持多候选比较、单商品判断、只有需求时的购买方向建议。
- 支持解释价格差异、识别坑点,并区分同款、近似款和不可直接比较的候选。
- 提供最小可执行 demo 闭环,方便快速演示和回归。
FILE:README.md
# Shopping Advisor / 购物军师
这不是一个“商品分析器”。
它是一个**帮用户完成购买判断的购物决策 Skill**。
它不想让用户继续在参数、套餐、差价、店铺和版本里打转。
它要做的是把一次购买判断直接收敛清楚:
- 比较同类商品
- 解释差价来自哪里
- 判断这单值不值得买
- 给出更合理的替代方向
- 提醒最容易踩的坑
- 最后给一句可执行结论
## 适合什么场景
适合这些用户表达:
- “这几个商品帮我选一个”
- “这个值不值得买?”
- “为什么贵这么多?”
- “有没有更好的替代方向?”
- “别分析了,直接告诉我怎么买”
## 它会给出什么样的答案
默认不是泛泛分析,而是尽量直接收敛到一个动作:
- 现在买
- 先等等
- 换另一个候选
- 补充关键信息再决定
- 换一个更合理的购买方向
典型输出会包含:
- `Purchase Goal`
- `Candidate Summary`
- `Decision Comparison`
- `Why the Price Gap Exists`
- `Pitfalls to Watch`
- `Better Alternative Directions`
- `Final Conclusion`
最后通常会落成这种一句话:
- 追求最低价,选 A
- 追求更稳妥售后,选 B
- 追求综合性价比,选 C
## 为什么用户愿意装
因为用户真正需要的通常不是“参数说明”,而是“这一单到底怎么买更对”。
它特别适合这种犹豫时刻:
- 好像都差不多,但不知道该选哪个
- 便宜是真的便宜,但怕有坑
- 贵的看起来更稳,但不知道值不值那部分溢价
- 不想再看一堆参数,只想知道怎么买更合理
它是 `taobao-competitor-analyzer` 的上位版:
- 不只分析一个商品
- 不只比较价格
- 不只做竞品分析
- 而是直接升级成购买决策官
## 安全边界
| 操作 | Agent | 用户 |
|------|-------|------|
| 搜索/浏览商品信息 | ✅ | - |
| 比较候选、解释差价、提示风险 | ✅ | - |
| 给出推荐购买方向 | ✅ | - |
| 最终支付/提交订单 | ❌ | ✅ |
它会在支付前停下,把最终交易决定交还给用户。
## 安装
```bash
clawhub install shopping-advisor
```
## Demo / 快速体验
最快可以先从这里开始:
```bash
python3 scripts/normalize.py < references/demo-input-same-item.json | python3 scripts/decide.py
```
更多演示场景见:`references/demo-scenarios.md`
## 当前骨架怎么跑
目前这个 skill 已经带了一个最小可执行脚本骨架:
- `scripts/normalize.py`
- `scripts/decide.py`
- `scripts/analyze.py`
### 1. 先做归一化
```bash
python3 scripts/normalize.py <<'EOF'
{"category":"投影仪","scenario":"卧室","priorities":["性价比"],"items":[{"title":"A 标准版","source":"taobao","price_hint":2399},{"title":"B 旗舰版","source":"jd","price_hint":2999}]}
EOF
```
### 2. 再做决策
```bash
python3 scripts/normalize.py <<'EOF' | python3 scripts/decide.py
{"category":"投影仪","scenario":"卧室","priorities":["性价比"],"items":[{"title":"A 标准版","source":"taobao","price_hint":2399},{"title":"B 旗舰版","source":"jd","price_hint":2999}]}
EOF
```
### 3. 输出用户可读结果
```bash
python3 scripts/analyze.py <<'EOF'
{"shopping_context":{"query":{"category":"投影仪","scenario":"卧室","priorities":["性价比"],"decision_mode":"compare"},"candidates":[{"id":"c1","title":"A 标准版","source":"taobao","price":{"final_price":2399}},{"id":"c2","title":"B 旗舰版","source":"jd","price":{"final_price":2999}}],"meta":{"data_source_mode":"user_only","missing_fields":[],"warnings":[]}},"decision_report":{"summary":{"decision":"如果你就是追求到手最低价,优先选 c1;如果你愿意多花一点换更稳妥的体验,优先选 c2。","recommended_action":"compare_more"},"rankings":{"lowest_price":"c1","best_after_sales":"c2","best_value":"c2"},"pitfalls":["低价不一定等于同款同配置。","套餐和版本差异可能会放大表面价差。"],"alternative_directions":["确认是否同款同配置后,再决定是否为更低价切换。"],"missing_info":[]}}
EOF
```
当前实现仍是第一版骨架:
- 已能跑通 `normalize -> decide -> analyze`
- 还没有接真实抓取、评论分析和平台数据
## 一句话卖点
不是告诉你商品是什么,而是告诉你这单怎么买更值。
## 适合放在技能列表页的短介绍
帮你比较候选、解释差价、识别坑点,最后直接给出“买哪个、为什么、现在要不要下单”的购物结论。
FILE:clawhub.json
{
"name": "shopping-advisor",
"title": "Shopping Advisor / 购物军师",
"version": "1.0.0",
"summary": "不是告诉你商品是什么,而是告诉你这单怎么买更值。",
"category": "shopping",
"entry": "SKILL.md"
}
FILE:package.json
{
"name": "shopping-advisor",
"version": "1.0.0",
"description": "不是告诉你商品是什么,而是告诉你这单怎么买更值。",
"main": "SKILL.md",
"keywords": [
"shopping",
"decision",
"ecommerce",
"taobao",
"jd",
"pdd",
"comparison",
"price-gap",
"openclaw-skill"
],
"author": "openclaw",
"license": "MIT"
}
FILE:references/decision-lens.md
# Decision Lens
Use this lens when turning product information into a buying recommendation.
## What to compare
- effective final price
- what the extra money actually buys
- whether the lower-priced option is fully comparable
- quality and stability tradeoffs
- after-sales confidence
- scenario fit
- long-term value
## What to explain
- where the price gap comes from
- whether the premium is worth paying
- what the user gives up if they pick the cheaper option
## What to avoid
- raw parameter dumping
- fake certainty
- pretending near-matches are identical
- making the user do the final synthesis alone
FILE:references/demo-input-need-driven.json
{
"category": "投影仪",
"budget": { "max": 3000 },
"scenario": "卧室夜间看电影",
"priorities": ["性价比", "噪音低"],
"decision_mode": "need_driven",
"notes": "我还没有具体候选,先给我选购方向。",
"items": []
}
FILE:references/demo-input-not-directly-comparable.json
{
"category": "手机",
"budget": { "max": 6000 },
"scenario": "日常使用",
"priorities": ["性价比"],
"decision_mode": "compare",
"notes": "先判断能不能直接按价格比较。",
"items": [
{
"title": "iPhone 15 128G 标准版",
"source": "taobao",
"price_hint": 4999
},
{
"title": "iPhone 15 256G 标准版",
"source": "jd",
"price_hint": 5599
}
]
}
FILE:references/demo-input-over-budget.json
{
"category": "耳机",
"budget": { "max": 200 },
"scenario": "通勤",
"priorities": ["性价比"],
"decision_mode": "compare",
"notes": "如果都超预算,就直接告诉我该不该换方向。",
"items": [
{
"title": "耳机 A 标准版",
"source": "taobao",
"price_hint": 299
},
{
"title": "耳机 A 标准版",
"source": "jd",
"price_hint": 399
}
]
}
FILE:references/demo-input-same-item.json
{
"category": "手机",
"budget": { "max": 5500 },
"scenario": "日常使用",
"priorities": ["性价比"],
"decision_mode": "compare",
"notes": "同款不同平台,想直接看买哪个更值。",
"items": [
{
"title": "iPhone 15 128G 标准版",
"source": "taobao",
"price_hint": 4999
},
{
"title": "iPhone 15 128G 标准版",
"source": "jd",
"price_hint": 5199
}
]
}
FILE:references/demo-input-similar-item.json
{
"category": "手机",
"budget": { "max": 6000 },
"scenario": "日常使用",
"priorities": ["省心", "性价比"],
"decision_mode": "compare",
"notes": "这两个看起来接近,但我不确定是不是完全同款。",
"items": [
{
"title": "iPhone 15 标准版",
"source": "taobao",
"price_hint": 4999
},
{
"title": "iPhone 15 升级款",
"source": "jd",
"price_hint": 5299
}
]
}
FILE:references/demo-scenarios.md
# Demo Scenarios
推荐第一次先跑:`demo-input-same-item.json`
这组 demo 用来稳定演示当前 `shopping-advisor` 的最小可执行闭环:
```bash
python3 scripts/normalize.py < references/demo-input-same-item.json
python3 scripts/normalize.py < references/demo-input-same-item.json | python3 scripts/decide.py
```
如果你想看完整用户可读输出,可以先准备 `shopping_context + decision_report`,再喂给 `scripts/analyze.py`。
## 1. same_item
- 文件:`demo-input-same-item.json`
- 场景:同款不同平台
- 目标:验证 `same_item` 可直接比价
## 2. similar_item
- 文件:`demo-input-similar-item.json`
- 场景:升级款 / 近似款
- 目标:验证 `similar_item` 只作参考,不应直接把最低价当结论
## 3. not_directly_comparable
- 文件:`demo-input-not-directly-comparable.json`
- 场景:规格冲突
- 目标:验证 `not_directly_comparable`
## 4. over_budget
- 文件:`demo-input-over-budget.json`
- 场景:候选可比,但全部超预算
- 目标:验证 `change_direction`
## 5. need_driven
- 文件:`demo-input-need-driven.json`
- 场景:没有候选,只有需求
- 目标:验证 guidance / gather_more_info 路径
FILE:references/platform-notes.md
# Platform Notes
These notes help frame shopping tradeoffs across Chinese ecommerce platforms.
## Taobao / Tmall
- broad selection
- higher variation in sellers and bundles
- requires careful SKU and shop comparison
## JD
- often stronger logistics and after-sales confidence
- price may be higher but peace of mind is often part of the premium
## PDD
- strong headline prices and subsidy stories
- need to inspect conditions, store quality, and refund friction carefully
## General rule
A lower visible price is not automatically a better buy.
Always compare:
- actual comparable SKU
- seller quality
- shipping and returns
- subsidy / coupon conditions
- whether the product fits the user’s real scenario
FILE:schema.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openclaw.dev/schemas/shopping-advisor.schema.json",
"title": "ShoppingAdvisorContracts",
"type": "object",
"additionalProperties": false,
"properties": {
"shopping_context": {
"$ref": "#/$defs/ShoppingContext"
},
"decision_report": {
"$ref": "#/$defs/DecisionReport"
}
},
"$defs": {
"ShoppingContext": {
"type": "object",
"additionalProperties": false,
"required": ["query", "candidates", "meta"],
"properties": {
"query": {
"$ref": "#/$defs/Query"
},
"candidates": {
"type": "array",
"minItems": 0,
"maxItems": 20,
"items": {
"$ref": "#/$defs/Candidate"
}
},
"market_context": {
"$ref": "#/$defs/MarketContext"
},
"meta": {
"$ref": "#/$defs/Meta"
}
}
},
"Query": {
"type": "object",
"additionalProperties": false,
"required": ["decision_mode"],
"properties": {
"category": {
"type": "string",
"minLength": 1
},
"budget": {
"$ref": "#/$defs/Budget"
},
"scenario": {
"type": "string"
},
"priorities": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 10
},
"decision_mode": {
"type": "string",
"enum": ["compare", "single_judgement", "need_driven"]
},
"user_intent": {
"type": "string",
"enum": ["buy_now", "compare_only", "worth_it", "find_direction", "undecided"]
},
"notes": {
"type": "string"
}
}
},
"Budget": {
"type": "object",
"additionalProperties": false,
"properties": {
"min": {
"type": "number",
"minimum": 0
},
"max": {
"type": "number",
"minimum": 0
},
"currency": {
"type": "string",
"default": "CNY"
},
"flexible": {
"type": "boolean",
"default": false
}
}
},
"Candidate": {
"type": "object",
"additionalProperties": false,
"required": ["id", "title", "source"],
"properties": {
"id": {
"type": "string",
"minLength": 1
},
"title": {
"type": "string",
"minLength": 1
},
"source": {
"type": "string",
"enum": ["taobao", "tmall", "jd", "pdd", "manual", "other"]
},
"url": {
"type": "string",
"format": "uri"
},
"brand": {
"type": "string"
},
"model": {
"type": "string"
},
"variant": {
"type": "string"
},
"price": {
"$ref": "#/$defs/Price"
},
"seller": {
"$ref": "#/$defs/Seller"
},
"specs": {
"type": "object",
"additionalProperties": {
"type": ["string", "number", "boolean", "null"]
}
},
"bundle": {
"$ref": "#/$defs/Bundle"
},
"strengths": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 20
},
"risks": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 20
},
"fit_tags": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 20
},
"evidence": {
"type": "array",
"items": {
"$ref": "#/$defs/Evidence"
},
"maxItems": 50
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
},
"Price": {
"type": "object",
"additionalProperties": false,
"properties": {
"list_price": {
"type": "number",
"minimum": 0
},
"final_price": {
"type": "number",
"minimum": 0
},
"currency": {
"type": "string",
"default": "CNY"
},
"price_label": {
"type": "string",
"enum": ["list_price", "discounted", "coupon_price", "bundle_price", "estimated", "unknown"]
},
"captured_at": {
"type": "string",
"format": "date-time"
}
}
},
"Seller": {
"type": "object",
"additionalProperties": false,
"properties": {
"shop_name": {
"type": "string"
},
"shop_type": {
"type": "string",
"enum": ["flagship", "official", "authorized", "marketplace", "individual", "unknown"]
},
"platform_flags": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 20
},
"after_sales_summary": {
"type": "string"
}
}
},
"Bundle": {
"type": "object",
"additionalProperties": false,
"properties": {
"includes": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 30
},
"notes": {
"type": "string"
},
"bundle_risk": {
"type": "string"
}
}
},
"Evidence": {
"type": "object",
"additionalProperties": false,
"required": ["type", "value"],
"properties": {
"type": {
"type": "string",
"enum": ["user_input", "screenshot_ocr", "page_parse", "manual_note", "review_summary", "other"]
},
"value": {
"type": "string"
},
"source_ref": {
"type": "string"
}
}
},
"MarketContext": {
"type": "object",
"additionalProperties": false,
"properties": {
"category_baseline": {
"$ref": "#/$defs/CategoryBaseline"
},
"timing": {
"$ref": "#/$defs/Timing"
}
}
},
"CategoryBaseline": {
"type": "object",
"additionalProperties": false,
"properties": {
"typical_price_band": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": {
"type": "number",
"minimum": 0
}
},
"common_tradeoffs": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 20
}
}
},
"Timing": {
"type": "object",
"additionalProperties": false,
"properties": {
"promotion_sensitive": {
"type": "boolean"
},
"note": {
"type": "string"
}
}
},
"Meta": {
"type": "object",
"additionalProperties": false,
"required": ["data_source_mode"],
"properties": {
"data_source_mode": {
"type": "string",
"enum": ["user_only", "partial_enriched", "fully_enriched"]
},
"adapter": {
"type": "string"
},
"generated_at": {
"type": "string",
"format": "date-time"
},
"missing_fields": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 50
},
"warnings": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 50
}
}
},
"DecisionReport": {
"type": "object",
"additionalProperties": false,
"required": ["summary"],
"properties": {
"summary": {
"$ref": "#/$defs/Summary"
},
"rankings": {
"$ref": "#/$defs/Rankings"
},
"price_explanations": {
"type": "array",
"items": {
"$ref": "#/$defs/PriceExplanation"
},
"maxItems": 20
},
"pitfalls": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 30
},
"alternative_directions": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 20
},
"missing_info": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 30
},
"candidate_summaries": {
"type": "array",
"items": {
"$ref": "#/$defs/CandidateSummary"
},
"maxItems": 20
}
}
},
"Summary": {
"type": "object",
"additionalProperties": false,
"required": ["decision"],
"properties": {
"worth_buying": {
"type": ["boolean", "null"]
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"decision": {
"type": "string",
"minLength": 1
},
"recommended_action": {
"type": "string",
"enum": ["buy_now", "wait", "gather_more_info", "change_direction", "compare_more"]
}
}
},
"Rankings": {
"type": "object",
"additionalProperties": false,
"properties": {
"lowest_price": {
"type": "string"
},
"best_after_sales": {
"type": "string"
},
"best_value": {
"type": "string"
},
"best_fit": {
"type": "string"
}
}
},
"PriceExplanation": {
"type": "object",
"additionalProperties": false,
"required": ["pair", "reason"],
"properties": {
"pair": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": {
"type": "string"
}
},
"reason": {
"type": "string"
},
"worth_paying": {
"type": ["boolean", "null"]
}
}
},
"CandidateSummary": {
"type": "object",
"additionalProperties": false,
"required": ["candidate_id"],
"properties": {
"candidate_id": {
"type": "string"
},
"pros": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 10
},
"cons": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 10
},
"best_for": {
"type": "string"
},
"not_ideal_for": {
"type": "string"
}
}
}
}
}
FILE:scripts/analyze.py
#!/usr/bin/env python3
from common import read_stdin_json
def _candidate_by_id(candidates, candidate_id):
for candidate in candidates:
if candidate.get("id") == candidate_id:
return candidate
return None
def _candidate_label(candidates, candidate_id):
candidate = _candidate_by_id(candidates, candidate_id)
if not candidate:
return candidate_id or "未知候选"
title = candidate.get("title") or candidate_id
return f"{candidate_id} ({title})"
def main() -> None:
payload = read_stdin_json()
context = payload.get("shopping_context") or {}
report = payload.get("decision_report") or {}
query = context.get("query") or {}
candidates = context.get("candidates") or []
summary = report.get("summary") or {}
pitfalls = report.get("pitfalls") or []
directions = report.get("alternative_directions") or []
rankings = report.get("rankings") or {}
missing_info = report.get("missing_info") or []
print("## Purchase Goal")
print(f"- Category: {query.get('category') or '未提供'}")
print(f"- Scenario: {query.get('scenario') or '未提供'}")
print(f"- Priorities: {', '.join(query.get('priorities') or []) or '未提供'}")
print()
print("## Candidate Summary")
if not candidates:
print("- No candidates yet")
else:
relation_labels = {
"same_item": "可直接比价",
"similar_item": "近似款,仅参考",
"not_directly_comparable": "不可直接比价",
}
for candidate in candidates:
price = ((candidate.get("price") or {}).get("final_price"))
source = candidate.get("source") or "unknown"
relation = ((candidate.get("comparison") or {}).get("relation")) or "same_item"
line = f"- {candidate.get('id')}: {candidate.get('title')}"
if price is not None:
line += f" (¥{price})"
line += f" [{source}] [{relation_labels.get(relation, relation)}]"
print(line)
print()
print("## Decision Comparison")
if rankings:
lowest = rankings.get("lowest_price")
after_sales = rankings.get("best_after_sales")
best_value = rankings.get("best_value")
if lowest:
print(f"- Lowest price: {_candidate_label(candidates, lowest)}")
if after_sales:
print(f"- After-sales tilt: {_candidate_label(candidates, after_sales)}")
if best_value:
print(f"- Best value: {_candidate_label(candidates, best_value)}")
else:
print("- 信息不足,暂时无法形成稳定比较。")
print()
print("## Why the Price Gap Exists")
lowest = rankings.get("lowest_price")
best_value = rankings.get("best_value")
non_comparable = [candidate for candidate in candidates if ((candidate.get("comparison") or {}).get("relation")) == "not_directly_comparable"]
if non_comparable:
print("- 本次价格比较只应参考可直接比较的候选;有些候选存在规格或套餐差异。")
if lowest and best_value and lowest != best_value:
print(f"- 账面最低价是 {_candidate_label(candidates, lowest)},但更综合的默认选择更偏向 {_candidate_label(candidates, best_value)}。")
print("- 这通常意味着价差不大,但省心程度或稳妥度更值得考虑。")
elif lowest:
print(f"- 当前看 {_candidate_label(candidates, lowest)} 同时占到价格优势,暂时更像默认优先项。")
else:
print("- 目前还缺少足够价格信息,无法稳定解释差价。")
print()
print("## Pitfalls to Watch")
if pitfalls:
for item in pitfalls:
print(f"- {item}")
else:
print("- 暂无")
print()
print("## Better Alternative Directions")
if directions:
for item in directions:
print(f"- {item}")
else:
print("- 暂无")
print()
if missing_info:
print("## Missing Info")
for item in missing_info:
print(f"- {item}")
print()
print("## Final Conclusion")
print(f"- {summary.get('decision') or '先补信息,再决定是否下单。'}")
similar_candidates = [candidate for candidate in candidates if ((candidate.get("comparison") or {}).get("relation")) == "similar_item"]
non_comparable_candidates = [candidate for candidate in candidates if ((candidate.get("comparison") or {}).get("relation")) == "not_directly_comparable"]
if similar_candidates:
print("- 注意:本次结论里包含近似款参考,适合帮助你缩小范围,但不适合直接把最低价当成最终答案。")
if non_comparable_candidates:
print("- 注意:有些候选存在明显规格或套餐差异,应先确认是否同款同配置,再决定要不要按价格比较。")
if __name__ == "__main__":
main()
FILE:scripts/common.py
#!/usr/bin/env python3
import json
import re
import sys
from typing import Any, Dict, List
from urllib.parse import urlparse
def read_stdin_json() -> Dict[str, Any]:
raw = sys.stdin.read().strip()
if not raw:
raise ValueError("No JSON input provided on stdin")
try:
data = json.loads(raw)
except json.JSONDecodeError as exc:
raise ValueError(f"Invalid JSON input: {exc}") from exc
if not isinstance(data, dict):
raise ValueError("Top-level JSON input must be an object")
return data
def write_json(data: Dict[str, Any]) -> None:
sys.stdout.write(json.dumps(data, ensure_ascii=False, indent=2))
sys.stdout.write("\n")
def write_error(message: str) -> None:
write_json({"error": message})
def make_candidate_id(index: int) -> str:
return f"c{index + 1}"
def clean_text(value: Any) -> str:
if not isinstance(value, str):
return ""
return re.sub(r"\s+", " ", value).strip()
def merge_notes(*values: Any) -> str | None:
parts = []
seen = set()
for value in values:
text = clean_text(value)
if text and text not in seen:
parts.append(text)
seen.add(text)
return " | ".join(parts) if parts else None
def infer_source_from_url(url: str | None) -> str | None:
if not url:
return None
host = urlparse(url).netloc.lower()
if "jd.com" in host:
return "jd"
if "tmall.com" in host:
return "tmall"
if "taobao.com" in host:
return "taobao"
if "pinduoduo.com" in host or "yangkeduo.com" in host or "pdd" in host:
return "pdd"
return "other" if host else None
def infer_decision_mode(items: List[Dict[str, Any]], text: str | None = None) -> str:
normalized = clean_text(text).lower()
if normalized:
if any(token in normalized for token in ["值不值", "能买吗", "值得买吗", "worth it"]):
return "single_judgement"
if any(token in normalized for token in ["对比", "哪个好", "选哪个", "compare"]):
return "compare"
if any(token in normalized for token in ["想买", "求推荐", "买什么", "recommend"]):
return "need_driven"
if len(items) >= 2:
return "compare"
if len(items) == 1:
return "single_judgement"
return "need_driven"
def extract_title(item: Dict[str, Any], index: int) -> tuple[str, bool]:
explicit = clean_text(item.get("title"))
if explicit:
return explicit, False
raw_text = clean_text(item.get("raw_text"))
if raw_text:
return raw_text[:80], False
url = clean_text(item.get("url"))
if url:
host = urlparse(url).netloc or url
return host[:80], True
return f"Candidate {index + 1}", True
def extract_price_value(item: Dict[str, Any]) -> tuple[float | None, str]:
for key in ["price_hint", "final_price", "price", "amount"]:
value = item.get(key)
if isinstance(value, (int, float)):
return float(value), "estimated" if key == "price_hint" else "discounted"
if isinstance(value, str):
text = clean_text(value).replace("¥", "")
try:
return float(text), "estimated" if key == "price_hint" else "discounted"
except ValueError:
pass
text = merge_notes(item.get("raw_text"), item.get("title")) or ""
match = re.search(r"(?:¥|¥)?\s*(\d{2,6}(?:\.\d{1,2})?)", text)
if match:
return float(match.group(1)), "estimated"
return None, "unknown"
def get_final_price(item: Dict[str, Any]) -> Dict[str, Any] | None:
value, label = extract_price_value(item)
if value is None:
return None
return {"final_price": value, "currency": clean_text(item.get("currency")) or "CNY", "price_label": label}
def _comparison_tokens(text: str) -> set[str]:
normalized = clean_text(text).lower()
if not normalized:
return set()
pattern = r"[a-z0-9]+(?:g|gb|tb|寸|mm|ml|w|k)?|标准版|pro|plus|max|ultra|套装|礼盒|单机|裸机|赠品|升级款|升级版|同系列|类似款|相近款"
return set(re.findall(pattern, normalized))
def classify_comparison(base_text: str, other_text: str) -> Dict[str, Any]:
base_clean = clean_text(base_text)
other_clean = clean_text(other_text)
base_tokens = _comparison_tokens(base_clean)
other_tokens = _comparison_tokens(other_clean)
reason_tags: list[str] = []
spec_pairs = [("128g", "256g"), ("256g", "512g"), ("标准版", "pro"), ("plus", "max")]
for left, right in spec_pairs:
if left in base_tokens and right in other_tokens or right in base_tokens and left in other_tokens:
reason_tags.append("spec_conflict")
bundle_tokens = {"套装", "礼盒", "赠品", "单机", "裸机"}
if base_tokens.intersection(bundle_tokens) != other_tokens.intersection(bundle_tokens):
if base_tokens.intersection(bundle_tokens) or other_tokens.intersection(bundle_tokens):
reason_tags.append("bundle_conflict")
uncertainty_tokens = {"升级款", "升级版", "同系列", "类似款", "相近款"}
if any(token in base_clean or token in other_clean for token in uncertainty_tokens):
relation = "similar_item"
reason_tags.append("weak_match")
elif reason_tags:
relation = "not_directly_comparable"
else:
overlap = len(base_tokens.intersection(other_tokens))
relation = "same_item" if overlap >= 2 else "similar_item"
if relation == "similar_item":
reason_tags.append("weak_match")
if reason_tags and "spec_conflict" in reason_tags or "bundle_conflict" in reason_tags:
relation = "not_directly_comparable"
return {
"relation": relation,
"reason_tags": sorted(set(reason_tags)),
}
FILE:scripts/decide.py
#!/usr/bin/env python3
from common import read_stdin_json, write_json, write_error
AFTER_SALES_PRIORITY = {
"jd": 3,
"tmall": 2,
"taobao": 1,
"pdd": 1,
"manual": 0,
"other": 0,
}
def _priced_candidates(candidates):
priced = []
for candidate in candidates:
price = ((candidate.get("price") or {}).get("final_price"))
if isinstance(price, (int, float)):
priced.append((candidate, price))
return priced
def _comparison_relation(candidate):
return ((candidate.get("comparison") or {}).get("relation")) or "same_item"
def _confidence_from_missing(missing_fields, base):
penalty = min(len(missing_fields) * 0.08, 0.3)
return max(0.25, round(base - penalty, 2))
def _best_after_sales_candidate(priced):
ranked = sorted(
priced,
key=lambda item: (
-AFTER_SALES_PRIORITY.get(item[0].get("source") or "other", 0),
item[1],
),
)
return ranked[0][0] if ranked else None
def main() -> None:
try:
context = read_stdin_json()
candidates = context.get("candidates") or []
query = context.get("query") or {}
budget = (query.get("budget") or {}).get("max")
priorities = query.get("priorities") or []
missing_fields = context.get("meta", {}).get("missing_fields", [])
if not candidates:
report = {
"summary": {
"worth_buying": None,
"confidence": 0.3,
"decision": "当前还没有足够候选,先补商品或关键信息,再做购买判断。",
"recommended_action": "gather_more_info"
},
"pitfalls": ["没有候选时,不要直接下单。"],
"alternative_directions": ["先明确预算、场景和至少一个候选。"],
"missing_info": ["candidates"]
}
write_json(report)
return
if len(candidates) == 1:
candidate = candidates[0]
price = ((candidate.get("price") or {}).get("final_price"))
over_budget = budget is not None and price is not None and price > budget
report = {
"summary": {
"worth_buying": False if over_budget else None,
"confidence": _confidence_from_missing(missing_fields, 0.55),
"decision": "这单可以继续看,但还需要对照项或更多关键信息,才能更稳地下结论。",
"recommended_action": "gather_more_info" if not over_budget else "change_direction"
},
"pitfalls": ["只有单个候选时,容易因为缺少对照而高估性价比。"],
"alternative_directions": ["补一个更低价候选和一个更稳妥候选再比较。"],
"missing_info": ["comparison_baseline"]
}
write_json(report)
return
comparison_pool = [candidate for candidate in candidates if _comparison_relation(candidate) != "not_directly_comparable"]
priced = _priced_candidates(comparison_pool or candidates)
if not priced:
report = {
"summary": {
"worth_buying": None,
"confidence": _confidence_from_missing(missing_fields, 0.4),
"decision": "当前候选缺少价格信息,暂时不建议直接下单,先补价格再比较。",
"recommended_action": "gather_more_info"
},
"pitfalls": ["没有价格就无法判断低价和溢价是否成立。"],
"alternative_directions": ["先补每个候选的实际到手价。"],
"missing_info": [*missing_fields, "price"]
}
write_json(report)
return
priced_sorted = sorted(priced, key=lambda item: item[1])
lowest_candidate, lowest_price = priced_sorted[0]
second_price = priced_sorted[1][1] if len(priced_sorted) > 1 else None
within_budget = [item for item in priced_sorted if budget is None or item[1] <= budget]
after_sales_candidate = _best_after_sales_candidate(within_budget or priced_sorted)
rankings = {
"lowest_price": lowest_candidate.get("id"),
}
if after_sales_candidate:
rankings["best_after_sales"] = after_sales_candidate.get("id")
value_candidate = lowest_candidate
if after_sales_candidate and after_sales_candidate.get("id") != lowest_candidate.get("id"):
after_sales_price = next(price for candidate, price in priced_sorted if candidate.get("id") == after_sales_candidate.get("id"))
if lowest_price > 0 and (after_sales_price - lowest_price) / lowest_price <= 0.1:
value_candidate = after_sales_candidate
rankings["best_value"] = value_candidate.get("id")
non_comparable_count = len([candidate for candidate in candidates if _comparison_relation(candidate) == "not_directly_comparable"])
similar_count = len([candidate for candidate in candidates if _comparison_relation(candidate) == "similar_item"])
all_over_budget = budget is not None and not within_budget
confidence = _confidence_from_missing(missing_fields, 0.72)
if non_comparable_count:
confidence = max(0.3, round(confidence - 0.1, 2))
elif similar_count:
confidence = max(0.35, round(confidence - 0.05, 2))
if all_over_budget:
decision = f"按你的预算,当前候选都偏贵,暂时不建议直接下单。账面最低价是 {lowest_candidate.get('id')},但也超出预算,更建议先换方向或降规格。"
action = "change_direction"
worth_buying = False
else:
if non_comparable_count >= max(1, len(candidates) - 1):
decision = "当前候选里有明显规格或套餐差异,暂时不建议直接按最低价下结论,先确认是不是同款同配置。"
action = "gather_more_info"
elif similar_count:
decision = f"当前候选里包含近似款,价格只能作参考,不能直接把最低价当最终结论。当前更偏向先看 {value_candidate.get('id')},但最好先确认版本和系列差异。"
action = "gather_more_info"
elif value_candidate.get("id") == lowest_candidate.get("id"):
decision = f"如果你就是追求到手最低价,优先选 {lowest_candidate.get('id')};目前它也更像默认的性价比选择。"
action = "compare_more"
else:
decision = f"如果你追求到手最低价,优先选 {lowest_candidate.get('id')};如果你愿意多花一点换更稳妥的体验,优先选 {value_candidate.get('id')}。"
action = "compare_more"
if budget is not None and within_budget:
budget_ids = ", ".join(candidate.get("id") for candidate, _ in within_budget)
decision += f" 按你的预算,当前真正可选的是 {budget_ids}。"
if missing_fields:
decision += " 但因为还缺少部分关键信息,这个结论更适合作为初步购买建议。"
action = "gather_more_info"
worth_buying = None
pitfalls = [
"低价不一定等于同款同配置。",
"套餐和版本差异可能会放大表面价差。"
]
if non_comparable_count:
pitfalls.append("部分候选存在明显规格或套餐冲突,不适合直接按价格硬比较。")
elif similar_count:
pitfalls.append("当前候选里包含近似款,低价不一定代表真正更值。")
if second_price is not None and abs(second_price - lowest_price) / max(lowest_price, 1) <= 0.05:
pitfalls.append("当前候选价差很小,不要只因为便宜一点就忽略售后和适配性。")
if missing_fields:
pitfalls.append("当前还缺少关键信息,结论可能会随着补充信息而变化。")
directions = [
"确认是否同款同配置后,再决定是否为更低价切换。"
]
if all_over_budget:
directions.append("优先看更低价位版本,或重新收窄预算与核心需求。")
elif "性价比" not in priorities and missing_fields:
directions.append("补充你的优先级后,结论会更贴近你的真实购买标准。")
report = {
"summary": {
"worth_buying": worth_buying,
"confidence": confidence,
"decision": decision,
"recommended_action": action
},
"rankings": rankings,
"pitfalls": pitfalls,
"alternative_directions": directions,
"missing_info": missing_fields
}
write_json(report)
except Exception as exc:
write_error(str(exc))
if __name__ == "__main__":
main()
FILE:scripts/normalize.py
#!/usr/bin/env python3
from common import (
classify_comparison,
clean_text,
extract_title,
get_final_price,
infer_decision_mode,
infer_source_from_url,
make_candidate_id,
merge_notes,
read_stdin_json,
write_error,
write_json,
)
def main() -> None:
try:
payload = read_stdin_json()
items = payload.get("items") or []
if not isinstance(items, list):
raise ValueError("items must be an array when provided")
notes_text = merge_notes(payload.get("notes"), payload.get("query"))
explicit_decision_mode = payload.get("decision_mode")
inferred_mode = infer_decision_mode(items, notes_text)
decision_mode = explicit_decision_mode or inferred_mode
candidates = []
warnings = []
missing_fields = []
candidate_missing_price = False
if explicit_decision_mode and explicit_decision_mode != inferred_mode:
warnings.append("Explicit decision_mode conflicts with item count or user wording; keeping explicit value.")
title_texts = []
for index, item in enumerate(items):
if not isinstance(item, dict):
continue
title, weak_title = extract_title(item, index)
title_texts.append(merge_notes(title, item.get("raw_text")) or title)
explicit_source = clean_text(item.get("source")) or None
inferred_source = infer_source_from_url(clean_text(item.get("url")) or None)
source = explicit_source or inferred_source or "manual"
if explicit_source and inferred_source and explicit_source != inferred_source:
warnings.append(f"Candidate {index + 1}: source conflicts with URL; keeping explicit source '{explicit_source}'.")
if weak_title:
warnings.append(f"Candidate {index + 1}: title was weak and had to be generated from limited input.")
if item.get("screenshot_path") and not item.get("raw_text"):
warnings.append(f"Candidate {index + 1}: screenshot path provided without readable extracted text.")
price = get_final_price(item)
if price is None:
candidate_missing_price = True
warnings.append(f"Candidate {index + 1}: missing price information.")
evidence = []
if clean_text(item.get("title")):
evidence.append({"type": "user_input", "value": clean_text(item.get("title"))})
if clean_text(item.get("raw_text")):
evidence.append({"type": "manual_note", "value": clean_text(item.get("raw_text"))[:200]})
if clean_text(item.get("url")):
evidence.append({"type": "other", "value": clean_text(item.get("url"))})
if clean_text(item.get("screenshot_path")):
evidence.append({"type": "other", "value": clean_text(item.get("screenshot_path"))})
if notes_text:
evidence.append({"type": "manual_note", "value": notes_text[:200]})
candidate = {
"id": make_candidate_id(index),
"title": title,
"source": source,
"url": clean_text(item.get("url")) or None,
"price": price,
"evidence": evidence or [{"type": "user_input", "value": title}],
}
candidates.append({k: v for k, v in candidate.items() if v is not None})
if candidates:
base_text = title_texts[0]
candidates[0]["comparison"] = {"relation": "same_item", "reason_tags": []}
for index in range(1, len(candidates)):
candidates[index]["comparison"] = classify_comparison(base_text, title_texts[index])
if not clean_text(payload.get("category")):
missing_fields.append("category")
if not clean_text(payload.get("scenario")):
missing_fields.append("scenario")
priorities = payload.get("priorities")
if not priorities:
missing_fields.append("priorities")
if not payload.get("budget"):
missing_fields.append("budget")
if not candidates:
missing_fields.append("candidates")
warnings.append("No candidate items provided; decision will stay in guidance mode.")
elif all((candidate.get("price") or {}).get("final_price") is None for candidate in candidates):
missing_fields.append("price")
elif candidate_missing_price:
warnings.append("Some candidates are missing price information, so later comparisons may be weaker.")
context = {
"query": {
"category": clean_text(payload.get("category")) or None,
"budget": payload.get("budget"),
"scenario": clean_text(payload.get("scenario")) or None,
"priorities": priorities,
"decision_mode": decision_mode,
"notes": notes_text,
},
"candidates": candidates,
"meta": {
"data_source_mode": "user_only",
"missing_fields": missing_fields,
"warnings": warnings,
},
}
write_json(context)
except Exception as exc:
write_error(str(exc))
if __name__ == "__main__":
main()
FILE:types.ts
export type DataSourceMode = 'user_only' | 'partial_enriched' | 'fully_enriched';
export type DecisionMode = 'compare' | 'single_judgement' | 'need_driven';
export type UserIntent =
| 'buy_now'
| 'compare_only'
| 'worth_it'
| 'find_direction'
| 'undecided';
export type CandidateSource =
| 'taobao'
| 'tmall'
| 'jd'
| 'pdd'
| 'manual'
| 'other';
export type ShopType =
| 'flagship'
| 'official'
| 'authorized'
| 'marketplace'
| 'individual'
| 'unknown';
export type PriceLabel =
| 'list_price'
| 'discounted'
| 'coupon_price'
| 'bundle_price'
| 'estimated'
| 'unknown';
export type EvidenceType =
| 'user_input'
| 'screenshot_ocr'
| 'page_parse'
| 'manual_note'
| 'review_summary'
| 'other';
export type RecommendedAction =
| 'buy_now'
| 'wait'
| 'gather_more_info'
| 'change_direction'
| 'compare_more';
export interface ShoppingInput {
query?: string;
category?: string;
budget?: {
min?: number;
max?: number;
currency?: string;
};
scenario?: string;
priorities?: string[];
decision_mode?: DecisionMode;
items?: InputItem[];
notes?: string;
}
export interface InputItem {
url?: string;
title?: string;
source?: CandidateSource;
screenshot_path?: string;
raw_text?: string;
price_hint?: number;
}
export interface ShoppingContext {
query: Query;
candidates: Candidate[];
market_context?: MarketContext;
meta: Meta;
}
export interface Query {
category?: string;
budget?: Budget;
scenario?: string;
priorities?: string[];
decision_mode: DecisionMode;
user_intent?: UserIntent;
notes?: string;
}
export interface Budget {
min?: number;
max?: number;
currency?: string;
flexible?: boolean;
}
export interface Candidate {
id: string;
title: string;
source: CandidateSource;
url?: string;
brand?: string;
model?: string;
variant?: string;
price?: Price;
seller?: Seller;
specs?: Record<string, string | number | boolean | null>;
bundle?: Bundle;
strengths?: string[];
risks?: string[];
fit_tags?: string[];
evidence?: Evidence[];
confidence?: number;
}
export interface Price {
list_price?: number;
final_price?: number;
currency?: string;
price_label?: PriceLabel;
captured_at?: string;
}
export interface Seller {
shop_name?: string;
shop_type?: ShopType;
platform_flags?: string[];
after_sales_summary?: string;
}
export interface Bundle {
includes?: string[];
notes?: string;
bundle_risk?: string;
}
export interface Evidence {
type: EvidenceType;
value: string;
source_ref?: string;
}
export interface MarketContext {
category_baseline?: CategoryBaseline;
timing?: Timing;
}
export interface CategoryBaseline {
typical_price_band?: [number, number];
common_tradeoffs?: string[];
}
export interface Timing {
promotion_sensitive?: boolean;
note?: string;
}
export interface Meta {
data_source_mode: DataSourceMode;
adapter?: string;
generated_at?: string;
missing_fields?: string[];
warnings?: string[];
}
export interface DecisionReport {
summary: Summary;
rankings?: Rankings;
price_explanations?: PriceExplanation[];
pitfalls?: string[];
alternative_directions?: string[];
missing_info?: string[];
candidate_summaries?: CandidateSummary[];
}
export interface Summary {
worth_buying?: boolean | null;
confidence?: number;
decision: string;
recommended_action?: RecommendedAction;
}
export interface Rankings {
lowest_price?: string;
best_after_sales?: string;
best_value?: string;
best_fit?: string;
}
export interface PriceExplanation {
pair: [string, string];
reason: string;
worth_paying?: boolean | null;
}
export interface CandidateSummary {
candidate_id: string;
pros?: string[];
cons?: string[];
best_for?: string;
not_ideal_for?: string;
}
Generate secure random passwords with customizable length, symbols, and numbers using Python's cryptographically strong secrets module.
# password-generator-pro
Generate secure random passwords using Python's secrets module.
## Description
A secure password generator that uses Python's cryptographically secure `secrets` module to generate random passwords. Supports customizable length and character set options.
## Usage
```bash
# Generate a 16-character password with all character types
python ~/.openclaw/skills/password-generator-pro/password_generator.py --length 16
# Generate password without symbols
python ~/.openclaw/skills/password-generator-pro/password_generator.py --length 12 --no-symbols
# Generate password without numbers
python ~/.openclaw/skills/password-generator-pro/password_generator.py --length 12 --no-numbers
# Generate multiple passwords at once
python ~/.openclaw/skills/password-generator-pro/password_generator.py --length 20 --count 5
```
## Examples
```bash
# Default 16-character password
python ~/.openclaw/skills/password-generator-pro/password_generator.py
# Short password for simple use
python ~/.openclaw/skills/password-generator-pro/password_generator.py --length 8
# Long password with letters only
python ~/.openclaw/skills/password-generator-pro/password_generator.py --length 32 --no-symbols --no-numbers
# Generate 3 passwords
python ~/.openclaw/skills/password-generator-pro/password_generator.py --count 3
```
## Options
- `--length`: Password length (default: 16)
- `--no-symbols`: Exclude special symbols
- `--no-numbers`: Exclude numbers
- `--count`: Number of passwords to generate (default: 1)
## Security Note
This tool uses Python's `secrets` module, which is designed for cryptographic applications and provides secure random number generation suitable for passwords and authentication tokens.
FILE:README.md
# Password Generator
Generate secure random passwords using Python's secrets module.
## Description
A secure password generator that uses Python's cryptographically secure `secrets` module to generate random passwords. Supports customizable length and character set options.
## Usage
```bash
# Generate a 16-character password with all character types
python password_generator.py --length 16
# Generate password without symbols
python password_generator.py --length 12 --no-symbols
# Generate password without numbers
python password_generator.py --length 12 --no-numbers
# Generate multiple passwords at once
python password_generator.py --length 20 --count 5
```
## Examples
```bash
# Default 16-character password
python password_generator.py
# Short password for simple use
python password_generator.py --length 8
# Long password with letters only
python password_generator.py --length 32 --no-symbols --no-numbers
# Generate 3 passwords
python password_generator.py --count 3
```
## Options
- `--length`: Password length (default: 16)
- `--no-symbols`: Exclude special symbols
- `--no-numbers`: Exclude numbers
- `--count`: Number of passwords to generate (default: 1)
## Security Note
This tool uses Python's `secrets` module, which is designed for cryptographic applications and provides secure random number generation suitable for passwords and authentication tokens.
FILE:password_generator.py
#!/usr/bin/env python3
"""Password Generator - Secure password generator using Python's secrets module."""
import argparse
import secrets
import string
def generate_password(length: int = 16, include_symbols: bool = True, include_numbers: bool = True) -> str:
"""Generate a secure random password."""
chars = string.ascii_letters
if include_numbers:
chars += string.digits
if include_symbols:
chars += string.punctuation
password = []
password.append(secrets.choice(string.ascii_lowercase))
password.append(secrets.choice(string.ascii_uppercase))
if include_numbers:
password.append(secrets.choice(string.digits))
if include_symbols:
password.append(secrets.choice(string.punctuation))
remaining = length - len(password)
if remaining > 0:
password.extend(secrets.choice(chars) for _ in range(remaining))
secrets.SystemRandom().shuffle(password)
return "".join(password[:length])
def main():
parser = argparse.ArgumentParser(description="Generate secure random passwords")
parser.add_argument("--length", "-l", type=int, default=16, help="Password length (default: 16)")
parser.add_argument("--no-symbols", action="store_true", help="Exclude special symbols")
parser.add_argument("--no-numbers", action="store_true", help="Exclude numbers")
parser.add_argument("--count", "-c", type=int, default=1, help="Number of passwords to generate")
args = parser.parse_args()
min_length = 2 + (0 if args.no_numbers else 1) + (0 if args.no_symbols else 1)
if args.length < min_length:
print(f"Error: Minimum length for selected options is {min_length}")
return 1
for i in range(args.count):
password = generate_password(
length=args.length,
include_symbols=not args.no_symbols,
include_numbers=not args.no_numbers,
)
if args.count > 1:
print(f"{i + 1}: {password}")
else:
print(password)
return 0
if __name__ == "__main__":
exit(main())
Browser-first grocery price finder for mainland China that compares vegetables and everyday fresh groceries across Duoduo Maicai, Meituan Maicai, 7FRESH, Hem...
---
name: Maicai
slug: maicai
version: 1.0.0
description: Browser-first grocery price finder for mainland China that compares vegetables and everyday fresh groceries across Duoduo Maicai, Meituan Maicai, 7FRESH, Hema Fresh, Dingdong Maicai, and similar platforms, normalizes city-specific basket cost, delivery or pickup friction, freshness tradeoffs, and coupon thresholds, and tells the user where to buy the cheapest useful basket right now.
metadata:
clawdbot:
emoji: "🥬"
requires:
bins: []
os: ["linux", "darwin", "win32"]
---
# Maicai
Maicai is not a generic grocery browser.
It is a cross-platform cheap-vegetable decision skill for mainland China grocery and instant-retail platforms.
Its job is to help the user answer:
- 这几样菜现在全网哪里更便宜
- 哪个平台今天更适合先买菜
- 便宜的是单品,还是整篮菜真的更省
- 自提、配送、起送门槛、包装费算进去后谁更划算
- 这波低价是不是靠次品、规格缩水、时段限制或高门槛做出来的
This skill should feel like a Chinese grocery deal operator, not a supermarket flyer.
## When To Use It
Use this skill when the user says things like:
- "帮我找最便宜的青菜、西红柿、土豆"
- "多多买菜、美团买菜、盒马、7fresh 哪家今天更便宜"
- "我想买一篮家常菜,先在哪个平台下单"
- "这几个平台买菜到底谁更省"
- "不要只看单品价,帮我看整单到手价"
- "帮我找便宜菜,但别只给我 headline 价格"
This skill is strongest when the user is comparing daily grocery baskets, vegetable staples, or same-day fresh purchases across platforms.
## Core Positioning
Default outcomes:
- cheapest useful basket
- cheapest single ingredient
- lowest-friction grocery route
- best platform by city or category
- fake-cheap or high-friction offers to skip
Do not stop at a price sheet.
Always end with an action:
- 先去哪个平台看
- 哪些菜适合在那里买
- 哪些活动值得凑
- 哪些门槛别追
- 现在下单还是再等等
## What Counts As Cheap
Never use sticker price alone.
Judge real cheapness by:
- final payable price after coupons, subsidies, memberships, and threshold reductions
- delivery fee, packaging fee, cold-chain fee, and pickup inconvenience
- quantity normalization such as per 500g, per kg, or per item
- freshness grade and whether the product is loose, pre-packed, trimmed, or small-spec
- whether the low price forces a larger basket or awkward add-ons
If a price is low only because of tricky conditions, say it plainly:
- `这是门槛价,不是自然到手价。`
- `便宜是便宜,但得靠凑单。`
- `单品看着低,整篮菜不一定省。`
- `这价做得出来,但不够顺手。`
## Modes
1. single ingredient
- one vegetable, fruit, egg, meat, or staple item
2. basket mode
- a shopping basket such as `青菜 + 西红柿 + 鸡蛋 + 土豆`
3. platform compare
- compare several grocery platforms in one city
4. city-first scan
- tell the user where to start for cheap groceries in a specific city
## City And Time Discipline
Cheap grocery prices are highly local.
Always time-stamp live judgments with an exact date, and mention city when known.
Preferred phrasing:
- `截至 2026-04-01 18:00(中国标准时间)`
- `以下判断以杭州当前公开价签为准`
- `如果你所在城市不同,这个结论可能只保留方向性`
- `这更像今天这个仓的价格,不一定全国通用`
If city is not provided:
- infer cautiously from context when possible
- otherwise say the answer is directional and city-dependent
## Inputs
Useful inputs include:
- platform names
- city name
- screenshots
- cart screenshots
- copied product titles
- a shopping list
- target budget
- urgency such as `今晚做饭` or `周末囤菜`
If the user does not provide enough detail, prioritize:
- city
- basket contents
- preference: lowest total price, fastest delivery, or easiest checkout
## Core Workflow
1. Identify the comparison unit.
- same ingredient, same basket, or same city-platform comparison
2. Normalize the item.
- same vegetable type, same grade, same weight unit, same packaging style when possible
3. Normalize the total cost.
- listed price
- coupon-adjusted price
- threshold and add-on requirements
- delivery fee, packaging fee, or pickup cost
4. Judge the tradeoff.
- freshness confidence
- city/warehouse variance
- delivery slot certainty
- whether cheaper means more hassle
5. Make the call.
- cheapest basket
- best low-friction option
- category split recommendation
- skip-the-noise warning
## Basket Logic
For baskets, optimize for the order the user will actually place.
A basket winner should consider:
- whether the user naturally crosses the threshold
- whether a platform wins on one SKU but loses the full basket
- whether splitting across two platforms is realistic or not worth the extra effort
- whether same-day cooking urgency makes pickup or longer ETA unattractive
Good decision style:
- `单看番茄是 A 更便宜,但整篮菜还是 B 更省。`
- `你今晚就做饭,别为了省 3 块分两单。`
- `叶菜走美团买菜,重货和耐放菜走多多买菜更顺。`
- `7FRESH 适合补高品质菜,不适合拿来拼最低总价。`
## Platform Lens
Read [references/platform-lenses.md](references/platform-lenses.md) when platform-specific nuance matters.
Default platform set:
- Duoduo Maicai
- Meituan Maicai / Xiaoxiang Supermarket style grocery entry points
- 7FRESH
- Hema Fresh
- Dingdong Maicai
- other local or instant-retail grocery channels the user provides
## Normalization Rules
Read [references/normalization-rules.md](references/normalization-rules.md) when you need help comparing weight, basket value, or freshness-adjusted price.
## Output Pattern
Use this structure unless the user asks for something shorter:
### Final Verdict
Give the direct answer first.
### Cheapest Basket Right Now
Name the best whole-order route and any threshold conditions.
### Cheapest By Ingredient
Call out platform winners when different ingredients should be bought in different places.
### Why The Prices Differ
Explain whether the gap comes from city warehouse pricing, subsidy mechanics, weaker freshness, or higher friction.
### Risk Warnings
Point out threshold traps, pickup inconvenience, freshness uncertainty, and packaging or delivery fees.
### Next Step
Tell the user to order now, switch platform, change basket, or wait.
## Decision Style
Sound like a Chinese grocery shopper who knows the platform套路 and is willing to make the call.
Preferred phrasing:
- `先说结论,今天这篮菜先去这家。`
- `单品最低价在 A,但整单最省是 B。`
- `这不是白菜真便宜,是靠门槛券做出来的。`
- `你要的是便宜又省事,不是数学最低价。`
- `这波能冲,但只适合自提顺路的人。`
- `看着便宜,实际上输在配送和包装费。`
Avoid:
- dry supermarket tables
- ranking by poster price only
- pretending city differences do not matter
- giving `都差不多` without resolving the decision
## Browser Workflow
When live validation is needed:
- inspect public grocery listing pages, public activity pages, and user-provided screenshots
- compare visible price, weight, freshness wording, and fee structure
- inspect whether the low price is broad or only a teaser SKU
- stop before login, payment, coupon claiming, or irreversible cart submission
Capture:
- city
- platform
- ingredient or basket
- visible weight or spec
- visible coupon or threshold condition
- delivery or pickup mode
- visible fees and ETA when available
## Safety Boundary
Allowed:
- compare public grocery prices
- explain basket math
- judge coupon thresholds
- rank platforms and routes
Do not:
- log in
- access private account pricing
- claim coupons
- place orders
- fake a city-specific result when only national or stale information is available
FILE:agents/openai.yaml
interface:
display_name: "Maicai"
short_description: "跨平台找便宜菜,比较整篮到手价和买菜门槛"
default_prompt: "Use $maicai to compare cheap vegetables and grocery baskets across Duoduo Maicai, Meituan Maicai, 7FRESH, Hema Fresh, Dingdong Maicai, and similar mainland China grocery platforms, normalize the real basket cost, and tell me where to buy the cheapest useful basket right now."
FILE:clawhub.json
{
"name": "maicai",
"version": "1.0.0",
"description": "便宜买菜雷达 - 帮用户比较多多买菜、美团买菜、7FRESH、盒马鲜生等平台的蔬菜和菜篮子到手价,找到真正更省的买菜路径",
"keywords": [
"maicai",
"grocery",
"vegetables",
"duoduomaicai",
"meituan-maicai",
"7fresh",
"hema",
"dingdong-maicai",
"cheap-groceries",
"basket-compare",
"fresh-food",
"china-ecommerce"
],
"author": "harrylabsj",
"license": "MIT",
"repository": "https://github.com/harrylabsj/maicai"
}
FILE:references/normalization-rules.md
# Normalization Rules
Use this reference when you need to compare vegetables or grocery baskets fairly across platforms.
## Unit Normalization
Always normalize vegetables and fresh groceries by a meaningful unit:
- leafy greens: per 250g or 500g when listings vary a lot
- root vegetables: per 500g or 1kg
- tomatoes, cucumbers, peppers: per 500g or by clear pack weight
- eggs: per piece and per 10 or 12 pack
- meat: per 500g or per kg, and note chilled vs frozen when relevant
If two listings are not the same weight, do not compare sticker price directly.
## Packaging Adjustment
Check whether the item is:
- loose selection
- pre-packed
- trimmed or cut
- premium selected grade
- bundled with other items
If one item is cleaner, larger, or more premium, say the comparison is directional rather than exact.
## Basket Math
For basket comparisons, include:
- sum of item prices
- coupon-adjusted total
- threshold required to unlock savings
- extra filler items needed
- delivery fee
- packaging fee
- pickup inconvenience if applicable
A basket is not truly cheaper if:
- it only wins after buying items the user does not need
- it forces an inconvenient second step such as distant pickup
- the savings are wiped out by fees or time cost
## Freshness Adjustment
Cheaper is not always better if:
- leafy vegetables appear older or smaller
- fruit and fragile produce have weaker quality confidence
- the low price comes from leftover or mixed-grade inventory
Use wording like:
- `低价成立,但可能便宜在规格和鲜度。`
- `这更像价格优先,不像品质优先。`
- `如果你更在意做出来的口感,这个低价不一定真值。`
## Delivery Versus Pickup
Normalize these as part of cost:
- home delivery fee
- packaging fee
- ETA reliability
- pickup distance or inconvenience
If the user is cooking soon, treat long pickup or unstable ETA as a real cost.
## City Variance
Never present a city-specific grocery answer as national truth.
If only partial location data is available, say:
- `这个比较更像你当前城市的方向性判断`
- `换城市后,仓和价签可能明显不同`
- `如果你给我城市和购物清单,我可以把判断压得更准`
FILE:references/platform-lenses.md
# Platform Lenses
Use this reference when the user asks which grocery platform is stronger for a given city, basket type, or urgency profile.
## Duoduo Maicai
Typical strengths:
- low headline price on standard vegetables and staples
- strong value on heavier or less perishable basket items
- useful for budget-first household baskets
Watch for:
- community pickup friction
- stock or assortment varying by pickup point
- low prices that are strongest on a subset of staples, not the whole basket
- freshness and spec variance across pickup points
Bias toward this platform when:
- the user is highly price-sensitive
- pickup is convenient
- the basket is mostly standard home-cooking ingredients
## Meituan Maicai
Typical strengths:
- faster fulfillment
- better for same-day or urgent cooking
- often cleaner for topping up missing ingredients quickly
Watch for:
- delivery and packaging fees eroding the advantage
- attractive single-item prices that lose on total basket cost
- rush-hour or weather-sensitive ETA issues
Bias toward this platform when:
- the user needs the groceries soon
- the basket is small to medium
- convenience matters almost as much as price
## 7FRESH
Typical strengths:
- stronger quality and brand perception on selected categories
- good for premium produce, fruit, imported items, and cleaner packaging
- better fit for users who will pay a little more for predictability
Watch for:
- not always competitive on cheap-vegetable baskets
- premium assortment making the average basket look more expensive
- promotions that are narrower than the homepage heat suggests
Bias toward this platform when:
- the user cares about quality or premium categories
- the basket is not purely budget-first
## Hema Fresh
Typical strengths:
- broad assortment
- strong same-day cooking convenience
- often good for mixed baskets that combine vegetables, proteins, and prepared items
Watch for:
- packaging and delivery structure affecting total cost
- quality positioning making some cheap-staple categories less competitive
- city and warehouse differences being large
Bias toward this platform when:
- the user wants one-stop mixed-basket convenience
- freshness confidence matters
- the user is okay paying a little for lower friction
## Dingdong Maicai
Typical strengths:
- fast grocery delivery
- strong for home-cooking refill scenarios
- often competitive on fresh, daily-use vegetables in supported cities
Watch for:
- city coverage variance
- small-basket fees and thresholds
- delivery-slot dependence at peak times
Bias toward this platform when:
- the user wants fast home delivery
- the city is well covered
- the basket is dinner-oriented and time-sensitive
## Platform Tie-Breakers
Use these shortcuts:
- cheapest total basket: prefer the platform that wins after threshold and fees, not the lowest-looking single SKU
- fastest useful order: prefer lower friction and reliable ETA over saving a few yuan
- best quality-to-price mix: prefer platforms with cleaner freshness confidence on fragile produce
- best heavy staples route: prefer channels strong in root vegetables, eggs, and household basics
## Fake Cheap Signals
Be skeptical when:
- the lowest price is on a teaser SKU with awkward weight
- the order only becomes cheap after excessive threshold chasing
- pickup is inconvenient enough to erase the savings
- premium-category platforms are compared against budget baskets without adjustment
Cross-platform promotion radar for mainland China shopping platforms that tracks the hottest current campaigns across Taobao, Tmall, JD, PDD, VIPSHOP, and si...
---
name: Platform Promo Radar
slug: promotion
version: 1.0.1
description: Cross-platform promotion radar for mainland China shopping platforms that tracks the hottest current campaigns across Taobao, Tmall, JD, PDD, VIPSHOP, and similar marketplaces, ranks them by real savings, participation friction, category fit, freshness, and urgency, and tells the user which promotions are actually worth chasing right now.
metadata:
clawdbot:
emoji: "🔥"
requires:
bins: []
os: ["linux", "darwin", "win32"]
---
# Platform Promo Radar
Platform Promo Radar is not a SKU comparison skill.
It is the promotion intelligence layer above Taobao, Tmall, JD, PDD, VIPSHOP, and similar mainland China shopping channels.
Its job is to help the user answer:
- 现在各平台最热的大促是什么
- 哪些活动是真优惠,哪些只是 banner 很响
- 哪个平台此刻最值得先逛
- 某个品类现在先去哪里看活动更高效
- 这个活动是现在冲、先观察,还是再等等更合适
This skill should feel like a Chinese e-commerce campaign operator and deal hunter, not a neutral calendar.
## When To Use It
Use this skill when the user says things like:
- "最近淘宝、京东、拼多多、唯品会有什么热活动"
- "现在哪个平台促销最猛"
- "618 / 双11 前哨期该先看哪边"
- "我想买护肤 / 家电 / 运动鞋,现在先蹲哪个平台"
- "帮我整理今天 / 本周值得关注的促销"
- "不要只讲官方口号,直接告诉我哪些活动真能省钱"
It is strongest when the user wants a promotion map before choosing specific products.
## Core Positioning
Default outcomes:
- hottest promotions worth checking now
- best platform by category
- easiest promotion for ordinary users to actually use
- strongest `can buy now` window
- fake-heat or high-friction campaigns to ignore
Do not collapse into a raw event list.
Always end with an action:
- 先逛哪家
- 先看哪个频道
- 哪类用户适合冲
- 哪些活动暂时别追
- 如果值得等,等到什么阶段更合理
## Modes
1. market scan
- the user wants the current hot promotion landscape
2. platform compare
- the user wants Taobao vs JD vs PDD vs VIPSHOP promotion strength
3. category-first
- the user gives a category and asks where promotions are strongest
4. event prep
- the user wants to prepare for an upcoming mega-sale window such as 618 or 双11
## Hotness Standard
Do not equate heat with banner volume.
Rank promotions by:
- freshness: active now, imminent, or already fading
- real savings: payable price versus normal sell price, not poster price
- participation friction: coupon stacking, membership, threshold, invite, or group-buy steps
- coverage: how many brands, categories, and normal users can actually benefit
- category fit: where the promotion is genuinely strongest
- inventory and fulfillment confidence
- after-sales confidence
- urgency: whether waiting risks missing the value window
A promotion can be hot but not worth it if it has:
- loud branding but weak final savings
- heavy threshold or stacking friction
- limited stock that ordinary users rarely get
- value concentrated in only a few SKUs
- low-trust sellers or awkward bundles hiding underneath
## Core Workflow
1. Classify the request.
- all-platform scan, category scan, or specific event watch
2. Gather public signals.
- official campaign pages
- platform homepages or channel pages
- public media or shopping-community mentions when useful
- category pages and visible subsidy labels
3. Normalize each promotion.
For each activity capture:
- platform
- campaign name
- exact date stamp for the snapshot
- active window or urgency
- promotion mechanic
- best-fit categories
- real savings pattern
- participation difficulty
- seller limitations
- fulfillment and after-sales caveats
4. Rank and compress.
- top actions now
- category-specific winners
- overhyped items to skip
5. Make the call.
- which platform to check first
- which activity type to prioritize
- who should act now and who should wait
## Time Discipline
Promotion intelligence expires quickly.
When describing live activity status:
- time-stamp the snapshot with an exact date, and time when helpful
- say whether a judgment is confirmed or inferred
- separate `active now`, `preheat`, and `historical pattern`
Preferred phrasing:
- `截至 2026-04-01 14:00(中国标准时间)`
- `这是基于当前公开页面的判断`
- `活动看起来已经进入预热,不等于最低价窗口`
- `这更像平台声量活动,不一定是最佳成交点`
## Platform Lens
For platform-specific mechanics and common traps, read [references/promo-patterns.md](references/promo-patterns.md) when platform detail matters.
Default platform lens:
- Taobao / Tmall: broad participation, many brand campaigns, rich coupon stacking, but mechanics can be complex
- JD: clearer official and self-operated campaigns, stronger logistics and appliance/3C trust
- PDD: aggressive subsidy and low-price mindshare, but confirm seller quality and after-sales friction
- VIPSHOP: strong on branded clearance, apparel, beauty, and outlet-style value, but color, size, and seasonality matter
## Category Guidance
Default to this kind of judgment:
- 3C / appliances: prefer activity windows with stronger authenticity, invoice, and after-sales confidence
- apparel / shoes / beauty clearance: weigh VIP-style outlet deals and brand flash sales higher
- daily essentials / standard consumer goods: low-friction subsidy channels often matter more than loud mega-sale branding
- gift / high-value items: a slightly weaker discount can still be the better recommendation if seller certainty is much stronger
If a category is not obviously strong on one platform, say so instead of forcing a winner.
## Output Pattern
Use this structure unless the user asks for something shorter:
### Final Call
Give the direct verdict first.
### Hottest Promotions Right Now
List the top promotions in ranked order with one-line reasons.
### What Is Actually Worth Chasing
Separate real value from pure hype.
### Best Platform By Goal
Give routes for:
- lowest-friction savings
- biggest headline deal
- safest high-ticket route
- best clearance-hunting route
### Category Shortcut
Tell the user where to start for the category they care about.
### Next Step
Tell the user exactly what to do now:
- open this platform first
- watch this channel
- wait for the next phase
- skip this noisy campaign
## Decision Style
Sound like a Chinese deal editor making the call.
Preferred phrasing:
- `先说结论,今天先看这两个活动。`
- `热度最高的不一定最值,这波真正能省钱的是这个。`
- `如果你只想少折腾,优先走这个活动入口。`
- `这波更像预热,不像真正放价。`
- `声量很大,但普通用户未必拿得到那个价格。`
- `能冲,但只适合这类品类和这类用户。`
Avoid:
- copying slogans from campaign pages
- ranking by headline discount only
- ending with `都可以看看`
- pretending an unverified poster price is final payable price
## Browser Workflow
When live validation is needed:
- inspect public campaign pages, landing pages, and visible subsidy labels
- compare activity mechanics across platforms
- look for threshold, membership, seller-type, and inventory constraints
- stop before login, coupon claiming, cart submission, or any irreversible action
Capture:
- campaign name and platform
- visible active window
- main discount mechanic
- category focus
- obvious access constraints
- whether the value looks broad or niche
## Safety Boundary
Allowed:
- summarize public promotion pages
- compare promotion mechanics
- explain thresholds and tradeoffs
- rank which activities are worth paying attention to
Do not:
- log in
- read private account state
- claim coupons
- place orders
- imitate guaranteed real-time coverage when the data is stale or unverified
FILE:agents/openai.yaml
interface:
display_name: "Promotion"
short_description: "追踪主流电商平台热门促销,判断哪些活动真值得追"
default_prompt: "Use $promotion to scan Taobao, Tmall, JD, PDD, VIPSHOP, and similar mainland China shopping platforms, rank the hottest current promotions by real savings and friction, and tell me which campaigns are actually worth chasing right now."
FILE:clawhub.json
{
"name": "promotion",
"version": "1.0.1",
"description": "电商促销雷达 - 追踪淘宝、天猫、京东、拼多多、唯品会等中国大陆平台的热门促销活动,识别真优惠和假热闹,并给出现在最值得先看的活动入口",
"keywords": [
"promotion",
"promotion-radar",
"platform-promo-radar",
"shopping-deals",
"china-ecommerce",
"taobao",
"tmall",
"jd",
"pdd",
"vipshop",
"discount",
"deals",
"618",
"double11",
"hot-sale"
],
"author": "openclaw",
"license": "MIT",
"repository": "https://github.com/harrylabsj/promotion"
}
FILE:references/promo-patterns.md
# Platform Promo Patterns
Use this reference when the user asks for platform-specific nuance, category guidance, or help judging whether a promotion is truly worth chasing.
## Taobao / Tmall
Typical high-heat mechanics:
- platform-wide mega-sale phases such as preheat, deposit, opening-night release, and return window
- brand flagship coupons
- cross-store full reduction
- category-day campaigns
- membership layers such as 88VIP-style savings
Usually strongest for:
- brand flagship browsing
- cosmetics and personal care
- home goods
- fashion and seasonal trends
Watch for:
- poster discounts that still need multiple coupons to become useful
- same-looking listings with different seller quality
- complicated stacking that normal users may not complete cleanly
- preheat messaging that sounds bigger than the actual low-price window
Good heat signals:
- broad brand participation
- visible category landing pages
- repeated banner placement over several surfaces
- official store support rather than only small sellers
## JD
Typical high-heat mechanics:
- self-operated campaign pages
- category subsidies for 3C and appliances
- time-limited flash sales
- member or PLUS-oriented discounts
- invoice- and delivery-friendly bundles
Usually strongest for:
- appliances
- phones, laptops, and accessories
- high-ticket goods where authenticity and after-sales matter
- gift purchases with delivery certainty requirements
Watch for:
- headline prices that look slightly higher but hide better fulfillment and easier returns
- third-party sellers mixed into the same search area
- bundles that appear cheaper only because accessories are weaker
Good heat signals:
- self-operated coverage
- direct subsidy labels
- strong warehouse and delivery promise
- price stability across multiple official surfaces
## PDD
Typical high-heat mechanics:
- 百亿补贴-like subsidy zones
- limited-time low-price channels
- group-buy or team-buy paths
- channel-specific store subsidies
Usually strongest for:
- standard consumer goods
- fast-moving daily items
- price-sensitive commodity products
- mainstream electronics when seller quality is still strong enough
Watch for:
- group-buy dependency
- confusing final-price conditions
- seller-quality variance
- aggressive headline price with weaker after-sales
Good heat signals:
- subsidy labels attached to mainstream, easy-to-verify SKUs
- broad access without special invite flow
- official or higher-trust merchant participation
## VIPSHOP
Typical high-heat mechanics:
- branded clearance events
- outlet-style limited-time specials
- seasonal apparel and footwear markdowns
- cosmetics and lifestyle flash-sale windows
Usually strongest for:
- apparel
- sportswear
- beauty and personal care
- branded overstock or seasonal clearance
Watch for:
- color and size leftovers driving the price
- end-of-season inventory that is cheap for a reason
- fewer universally strong deals outside its core categories
Good heat signals:
- deep cuts on recognizable brands
- broad size coverage instead of only leftovers
- clear seasonality logic that matches the user's need
## Mega-Sale Phase Cheat Sheet
Use this when the user asks whether to buy now or wait.
- preheat: high noise, useful for watchlists, not automatically the best buy window
- deposit / presale: best for locking high-ticket official goods when stock is tight
- opening release: broadest comparison point and easiest moment for a general audience
- category-day / channel burst: often better for specific verticals than the platform-wide slogan phase
- return / tail phase: lower noise, occasionally strong cleanup prices, but stock becomes patchy
## Fake Heat Signals
Be skeptical when:
- the campaign language is huge but the product coverage is tiny
- the lowest price depends on multiple hard-to-combine conditions
- the promotion is visible everywhere but only a few teaser SKUs are good
- the platform pushes urgency without showing broad, reproducible value
- the cheapest route relies on low-trust sellers or awkward bundles
## Real Value Signals
Treat a campaign as truly actionable when most of these are true:
- normal users can access the price without heroic stacking
- the value applies across multiple brands or a meaningful category
- seller quality is acceptable
- shipping and after-sales are not obviously sacrificed
- the time window is real, not vague preheat language
## Quick Recommendation Shortcuts
Use these as tie-breakers:
- user wants low friction: favor simpler mechanics over the mathematically lowest poster price
- user wants high-ticket safety: favor official, self-operated, or higher-trust seller paths
- user wants clearance hunting: favor outlet-style and seasonal campaigns
- user wants everyday savings: favor low-threshold subsidy channels over loud mega-sale branding
会议运营 Copilot — 支持会前 briefing 生成、会中结构化纪要整理、会后待办追踪与 follow-up 草稿。面向需要系统性管理会议全生命周期的职场人士。提供 boss mode(向上汇报视角)和 executor mode(向下追踪视角)两种输出格式。⚠️ 不提供实时语音转录、日历集成或自动提醒。
---
name: meeting-copilot
description: 会议运营 Copilot — 支持会前 briefing 生成、会中结构化纪要整理、会后待办追踪与 follow-up 草稿。面向需要系统性管理会议全生命周期的职场人士。提供 boss mode(向上汇报视角)和 executor mode(向下追踪视角)两种输出格式。⚠️ 不提供实时语音转录、日历集成或自动提醒。
---
# Meeting Ops Copilot
会议全生命周期辅助,覆盖:会前 briefing → 会中纪要结构化整理 → 会后待办追踪与 follow-up 草稿。支持 boss / executor 两种模式输出。
## Overview
本 skill 帮助用户系统性管理会议全生命周期:
- **会前**:基于议程和背景生成 briefing 材料,供主持或主讲人快速进入状态
- **会中**:将散乱的讨论内容整理为结构化纪要(讨论要点 / 决议 / 待办含负责人)
- **会后**:从纪要中提取待办清单和 follow-up 草稿邮件/消息
**⚠️ 免责声明**:本工具不提供实时语音转录、不接入日历、不发送通知。其输出依赖用户输入的原始信息,质量受限于输入完整性。
## When to Use This Skill
- 需要在会议前快速准备 briefing
- 会议中或会议后需要整理出结构化纪要
- 需要从纪要中生成待办追踪列表
- 需要向老板(boss mode)或下属(executor mode)发送 follow-up 邮件/消息草稿
## Workflow
### Mode 1: boss mode(向上汇报视角)
输出重点:结论先行、决策清晰、风险可见。
```
输入:会议主题 + 议程 + 背景要点(或原始讨论文本)
↓ 识别关键决议、支撑理由、潜在风险
↓ 输出:Boss-friendly Briefing(结论 + 要点 + 风险 + 建议行动)
```
### Mode 2: executor mode(向下追踪视角)
输出重点:任务明确、责任到人、时间清楚。
```
输入:会议纪要 / 讨论要点 / 决议
↓ 提取待办事项(含负责人 / 截止 / 优先级)
↓ 输出:结构化待办列表 + follow-up 草稿
```
## Input
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `task` | string | ✅ | 任务类型:`briefing` / `minutes` / `followup` |
| `meeting_topic` | string | ✅ | 会议主题 |
| `meeting_date` | string | ✅ | 会议日期 YYYY-MM-DD |
| `mode` | string | ✅ | `boss` 或 `executor` |
| `agenda` | string | 条件必填 | 议程要点,多条用 `\|` 分隔(briefing 必填) |
| `raw_text` | string | 条件必填 | 原始讨论文本(minutes 必填) |
| `decisions` | string | 可选 | 已决事项,多条用 `\|` 分隔 |
| `participants` | string | 可选 | 参与者列表,多人用 `\|` 分隔 |
## Output
### briefing(boss mode)
```json
{
"status": "success",
"task": "briefing",
"mode": "boss",
"meeting_topic": "...",
"meeting_date": "...",
"sections": {
"conclusion": "一句话结论",
"key_points": ["要点1", "要点2"],
"decisions_needed": ["待决策事项"],
"risks": ["风险1", "风险2"],
"suggested_actions": ["建议行动1"]
},
"briefing_text": "格式化 briefing 文本"
}
```
### minutes(executor mode)
```json
{
"status": "success",
"task": "minutes",
"mode": "executor",
"meeting_topic": "...",
"meeting_date": "...",
"minutes": {
"discussion_points": ["讨论点1", "讨论点2"],
"decisions": ["决议1"],
"action_items": [
{"task": "任务描述", "owner": "负责人", "deadline": "YYYY-MM-DD", "priority": "high|medium|low"}
]
},
"minutes_text": "格式化纪要文本"
}
```
### followup
```json
{
"status": "success",
"task": "followup",
"mode": "boss|executor",
"meeting_topic": "...",
"followup_items": [...],
"draft_email": "邮件草稿文本",
"draft_message": "简短消息草稿"
}
```
## Safety & Disclaimer
- ⚠️ 本 skill 不接入日历系统,不发送通知,不进行实时语音转录
- ⚠️ 输出的完整性和准确性依赖用户输入的原始信息,不做信息真实性担保
- ⚠️ 本工具是辅助参考,不替代专业会议记录人员或法律/合规审查
- 如涉及重大决策,请结合实际情况自行判断或咨询专业人士
## Constraints
- ❌ 不提供实时语音转录
- ❌ 不接入日历(Google Calendar、钉钉日历等)
- ❌ 不自动发送通知或邮件(仅生成草稿)
- ❌ 不做会议录音存储
## Examples
### 会前 Briefing(boss mode)
**输入**:
```
task: briefing
meeting_topic: Q2 产品规划评审
meeting_date: 2026-04-05
mode: boss
agenda: Q2目标对齐|技术方案选型|资源评估
participants: 产品经理|研发负责人|设计负责人
```
**输出**:Boss-friendly briefing,包含结论先行的核心要点 + 风险 + 建议行动。
### 会中纪要整理(executor mode)
**输入**:
```
task: minutes
meeting_topic: 周例会
meeting_date: 2026-03-31
mode: executor
raw_text: 讨论了A功能延期的风险;决定启用备选方案;张三分头对接供应商;李四负责测试
decisions: 启用备选方案
participants: 张三|李四|王五
```
**输出**:结构化纪要 + 提取的待办(含负责人)。
### 会后 Follow-up(executor mode)
**输入**:
```
task: followup
meeting_topic: 周例会
mode: executor
decisions: 启用备选方案
(action_items from minutes output)
```
**输出**:待办追踪列表 + 邮件/消息草稿。
## Acceptance Criteria
- [ ] 支持 `briefing` / `minutes` / `followup` 三种 task
- [ ] 支持 `boss` / `executor` 两种 mode
- [ ] briefing 输出包含 conclusion / key_points / risks / suggested_actions
- [ ] minutes 输出包含 discussion_points / decisions / action_items(含 owner/deadline/priority)
- [ ] followup 输出包含结构化待办列表 + 邮件草稿 + 消息草稿
- [ ] handler.py 带 `if __name__ == "__main__":` 自测分支,直接 `python3 handler.py` 可跑
- [ ] skill.json / .claw/identity.json / tests/test_handler.py 齐全
- [ ] 免责声明完整呈现于输出和文档中
FILE:handler.py
#!/usr/bin/env python3
"""
Meeting Ops Copilot - handler.py
会议全生命周期辅助:会前 briefing / 会中纪要结构化整理 / 会后待办追踪与 follow-up 草稿
支持 boss mode(向上汇报视角)和 executor mode(向下追踪视角)
"""
import sys
import json
import re
from datetime import datetime, timedelta
from typing import Optional, List, Dict, Any
# ─── 常量 ────────────────────────────────────────────────────────────────────
DISCLAIMER = """
⚠️ **免责声明**
本工具的输出依赖用户输入的原始信息,不做信息真实性担保。
本 skill 不提供实时语音转录、不接入日历系统、不自动发送通知或邮件(仅生成草稿)。
涉及重大决策时,请结合实际情况自行判断或咨询专业人士。
"""
BOSS_BRIEFING_TEMPLATE = """
═══════════════════════════════════════════════════════
📋 会议 Briefing({meeting_topic})
📅 {meeting_date} | 视角:Boss(向上汇报)
═══════════════════════════════════════════════════════
🎯 一句话结论
───────────────────────────────────────────────────────
{conclusion}
📌 核心要点
───────────────────────────────────────────────────────
{key_points}
🔍 待决策事项
───────────────────────────────────────────────────────
{decisions_needed}
⚠️ 风险提示
───────────────────────────────────────────────────────
{risks}
✅ 建议行动
───────────────────────────────────────────────────────
{suggested_actions}
{DISCLAIMER}
"""
EXECUTOR_MINUTES_TEMPLATE = """
═══════════════════════════════════════════════════════
📋 会议纪要({meeting_topic})
📅 {meeting_date} | 视角:Executor(向下追踪)
═══════════════════════════════════════════════════════
💬 讨论要点
───────────────────────────────────────────────────────
{discussion_points}
✅ 决议事项
───────────────────────────────────────────────────────
{decisions}
📌 待办追踪
───────────────────────────────────────────────────────
{action_items}
{DISCLAIMER}
"""
EXECUTOR_FOLLOWUP_TEMPLATE = """
═══════════════════════════════════════════════════════
📤 Meeting Follow-up({meeting_topic})
📅 {meeting_date} | 视角:Executor(向下追踪)
═══════════════════════════════════════════════════════
📌 待办追踪列表
───────────────────────────────────────────────────────
{followup_items}
📧 邮件草稿
───────────────────────────────────────────────────────
{draft_email}
💬 简短消息草稿
───────────────────────────────────────────────────────
{draft_message}
{DISCLAIMER}
"""
BOSS_FOLLOWUP_TEMPLATE = """
═══════════════════════════════════════════════════════
📤 Meeting Follow-up({meeting_topic})
📅 {meeting_date} | 视角:Boss(向上汇报)
═══════════════════════════════════════════════════════
🎯 会议结论摘要
───────────────────────────────────────────────────────
{summary}
📌 待办追踪
───────────────────────────────────────────────────────
{followup_items}
📧 邮件草稿
───────────────────────────────────────────────────────
{draft_email}
💬 简短消息草稿
───────────────────────────────────────────────────────
{draft_message}
{DISCLAIMER}
"""
# ─── 工具函数 ────────────────────────────────────────────────────────────────
def split_list(text: str, sep: str = "|") -> List[str]:
"""将分隔字符串拆分为列表,过滤空项"""
return [s.strip() for s in text.split(sep) if s.strip()]
def parse_raw_text_to_points(raw_text: str) -> List[str]:
"""将原始讨论文本拆分为讨论要点列表"""
raw_text = raw_text.strip()
# 按常见分隔符拆分:句号/分号/换行
points = re.split(r'[;;;\n]+', raw_text)
result = []
for p in points:
p = p.strip()
if p:
# 去掉句末标点
p = re.sub(r'[。..,,]$', '', p)
if p:
result.append(p)
return result
def extract_action_items(raw_text: str, decisions: str, participants: str) -> List[Dict[str, str]]:
"""
从原始文本中提取待办事项。
策略:
1. 切分为多个短句后识别 "X负责Y" / "X做Y" / "X跟进Y" 等模式
2. 识别 "截止.../deadline..." 等时间线索
3. 基于关键词判断优先级
"""
# 纯空格字符串校验(DEF-001 修复)
if not raw_text or not raw_text.strip():
return []
action_items = []
person_names = split_list(participants, "|") if participants else []
decision_list = split_list(decisions, "|") if decisions else []
# 将原文和决议拼接后按句切分,减少跨句误匹配
text_for_search = raw_text
clauses = re.split(r'[;;\n\r]+', text_for_search)
# 常见动作关键词(优先级顺序)
action_patterns = [
r'(?P<owner>[\u4e00-\u9fff]{2,5})(?:分头)?(?P<verb>负责)(?P<task>[^,。,;;\n\r]+)',
r'(?P<owner>[\u4e00-\u9fff]{2,5})(?:分头)?(?P<verb>做)(?P<task>[^,。,;;\n\r]+)',
r'(?P<owner>[\u4e00-\u9fff]{2,5})(?:分头)?(?P<verb>跟进)(?P<task>[^,。,;;\n\r]+)',
r'(?P<owner>[\u4e00-\u9fff]{2,5})(?:分头)?(?P<verb>完成)(?P<task>[^,。,;;\n\r]+)',
r'(?P<owner>[\u4e00-\u9fff]{2,5})(?:分头)?(?P<verb>提交)(?P<task>[^,。,;;\n\r]+)',
r'(?P<owner>[\u4e00-\u9fff]{2,5})(?:分头)?(?P<verb>对接)(?P<task>[^,。,;;\n\r]+)',
r'(?P<owner>[\u4e00-\u9fff]{2,5})(?:分头)?(?P<verb>协调)(?P<task>[^,。,;;\n\r]+)',
r'(?P<owner>[\u4e00-\u9fff]{2,5})(?:分头)?(?P<verb>安排)(?P<task>[^,。,;;\n\r]+)',
]
# 英文名兜底
action_patterns.append(
r'(?P<owner>[A-Za-z][A-Za-z0-9_\-]{1,20})(?:\s*\(?(?P<modifier>[^))\s]{0,6})\)?)?(?P<verb>负责|做|跟进|完成|提交|对接|协调|安排)(?P<task>[^,。,;;\n\r]+)'
)
for clause in clauses:
clause = clause.strip()
if not clause:
continue
found_one = False
for pattern in action_patterns:
# 每个短句优先取第一条可用匹配,避免一个句子内重复匹配造成 owner 误切
match = re.search(pattern, clause)
if not match:
continue
owner = match.group("owner").strip()
task = match.group("task").strip()
# 去掉末尾标点
task = re.sub(r'[。..,,;;]+$', '', task)
if len(task) < 2:
continue
# "分头" 可能被误入 owner 时去掉
owner = owner.replace("分头", "")
if not owner:
continue
# 与参与者名单对齐,增强名字可信度
if person_names and owner not in person_names:
# 有参与者信息但不匹配时,仍然保留,避免遗漏;给默认 TBD 标记
if len(owner) > 2:
owner = owner
else:
owner = "TBD"
# 判断优先级
priority = "medium"
text_around = clause[max(0, match.start()-10):match.end()+20].lower()
if any(kw in text_around for kw in ["紧急", "优先", "重要", "马上", "尽快"]):
priority = "high"
elif any(kw in text_around for kw in ["不急", "缓", "以后", "有空"]):
priority = "low"
# 判断截止时间
deadline = ""
deadline_patterns = [
r'截止[:::]?(\d{4}[-/年]\d{1,2}[-/月]\d{1,2}[日]?)',
r'截止[:::]?(\d{1,2}[-/月]\d{1,2}[日]?)',
r'deadline[:::]?(\d{4}[-/]\d{1,2}[-/]\d{1,2})',
r'最迟(\d{1,2}日)',
r'下周|下个月|本周|本月|明天|今天|本周内',
]
for dp in deadline_patterns:
dm = re.search(dp, clause)
if dm:
if dm.lastindex:
deadline = dm.group(1)
else:
deadline = dm.group(0)
break
# 去掉 "X负责" 本身
task_clean = re.sub(r'^(负责|做|完成|对接|协调|安排|提交|跟进)\s*', '', task)
if task_clean:
action_items.append({
"task": task_clean if len(task_clean) > 3 else task,
"owner": owner if owner not in ["负责", "做", "完成", "对接", "协调", "安排", "提交", "跟进", "TBD", ""] else "TBD",
"deadline": deadline,
"priority": priority
})
found_one = True
break
if not found_one:
continue
# 决议文本中若有明确人名 + 任务,补充一轮兜底匹配
for clause in decision_list:
clause = clause.strip()
if not clause:
continue
for pattern in action_patterns[:8]:
match = re.search(pattern, clause)
if match:
owner = match.group("owner").strip().replace("分头", "")
task = re.sub(r'[。..,,;;]+$', '', match.group("task").strip())
if task and owner and len(task) >= 2:
action_items.append({
"task": re.sub(r'^(负责|做|完成|对接|协调|安排|提交|跟进)\s*', '', task),
"owner": owner,
"deadline": "",
"priority": "medium"
})
break
# 去重:按 (task, owner) 去重
seen = set()
deduped = []
for item in action_items:
key = (item["task"], item["owner"])
if key not in seen:
seen.add(key)
deduped.append(item)
return deduped
def format_action_items(action_items: List[Dict]) -> str:
if not action_items:
return " (暂无明确待办)"
priority_icon = {"high": "🔴", "medium": "🟡", "low": "🟢"}
lines = []
for i, item in enumerate(action_items, 1):
icon = priority_icon.get(item["priority"], "⚪")
deadline_str = f" | 截止:{item['deadline']}" if item["deadline"] else ""
lines.append(f" {i}. {icon} [{item['priority'].upper()}] {item['task']}")
lines.append(f" 负责人:{item['owner']}{deadline_str}")
return "\n".join(lines)
def generate_followup_draft(meeting_topic: str, meeting_date: str,
action_items: List[Dict],
mode: str,
decisions: str = "") -> Dict[str, str]:
"""生成 follow-up 邮件草稿和消息草稿"""
decisions_list = split_list(decisions, "|") if decisions else []
summary_text = ""
if decisions_list:
summary_text = ";".join(decisions_list)
elif action_items:
summary_text = f"共 {len(action_items)} 项待办待落实"
else:
summary_text = "详见下文"
# 邮件草稿
if mode == "executor":
action_lines = []
for item in action_items:
dl = f"(截止:{item['deadline']})" if item["deadline"] else ""
action_lines.append(f"- {item['task']} — {item['owner']} {dl} [优先级:{item['priority']}]")
action_text = "\n".join(action_lines) if action_lines else "暂无明确待办"
draft_email = f"""Subject: 【会议纪要】{meeting_topic} — 待办确认 ({meeting_date})
您好,
{meeting_topic} 会议已结束,以下为待办事项,请相关同事确认:
{action_text}
如有变化或疑问,请随时沟通。
谢谢!
"""
else: # boss mode
draft_email = f"""Subject: 【会议汇报】{meeting_topic} — 结论与待办 ({meeting_date})
领导,
{meeting_topic} 已完成,核心结论:{summary_text}。
待办事项共 {len(action_items)} 项,主要负责人已确认,后续进展将定期汇报。
详细纪要见附件或后续整理版本。
谢谢!
"""
# 消息草稿(简短)
if mode == "executor":
item_count = len(action_items)
owners = ", ".join(dict.fromkeys(item["owner"] for item in action_items if item["owner"] != "TBD"))
draft_message = f"""【{meeting_topic}】已整理纪要,共 {item_count} 项待办。负责人:{owners if owners else '待定'}。详情见邮件。"""
else:
draft_message = f"""【{meeting_topic}】会议已完成,核心结论:{summary_text}。共 {len(action_items)} 项待办推进中。"""
return {
"draft_email": draft_email.strip(),
"draft_message": draft_message.strip()
}
# ─── 核心处理函数 ─────────────────────────────────────────────────────────────
def build_briefing(meeting_topic: str, meeting_date: str,
agenda: str, participants: str) -> Dict[str, Any]:
"""
生成 boss mode briefing。
策略:基于议程结构化分析,提取核心结论要点、待决策项、风险和建议行动。
"""
agenda_items = split_list(agenda, "|") if agenda else []
participant_list = split_list(participants, "|") if participants else []
# 从议程项推断可能的核心要点和决策需求
key_points = []
decisions_needed = []
risks = []
suggested_actions = []
agenda_keywords_map = {
"规划": ("制定明确目标和里程碑", "目标是否符合战略方向"),
"评审": ("评审现有方案或进度", "方案是否通过评审"),
"对齐": ("团队或部门间目标一致", "各方是否达成一致"),
"选型": ("技术方案或产品选型决策", "选型标准是否清晰"),
"资源": ("资源(人力/预算)分配评估", "资源是否满足需求"),
"进度": ("项目或任务进展回顾", "进度是否正常"),
"风险": ("风险识别和应对讨论", "风险是否在可控范围"),
"决策": ("重大决策事项", "决策依据是否充分"),
"复盘": ("项目或阶段复盘总结", "经验教训是否沉淀"),
"预算": ("预算审核或调整", "预算分配是否合理"),
}
for item in agenda_items:
item_stripped = item.strip()
key_point = f"议题:{item_stripped}"
key_points.append(key_point)
for kw, (pt, dc) in agenda_keywords_map.items():
if kw in item_stripped:
decisions_needed.append(f"「{item_stripped}」相关:{dc}")
suggested_actions.append(f"准备{item_stripped}的支撑数据和方案建议")
break
else:
decisions_needed.append(f"「{item_stripped}」需要明确方向和结论")
# 基于参与者推断行动建议
if participant_list:
suggested_actions.append(f"确认{participant_list[0]}的主持/主讲角色")
# 通用风险
risks.append("议程时间可能不足,部分议题需提前预判优先级")
risks.append("决策所需数据或授权信息可能不完整")
# 生成一句话结论
conclusion = f"本次会议聚焦{meeting_topic},共{len(agenda_items)}个议题,需形成明确结论并落实行动"
if agenda_items:
conclusion = f"围绕「{'」「'.join(agenda_items[:2])}」等议题,需对齐方向并落实{len(agenda_items)}项行动计划"
# 格式化
key_points_str = "\n".join(f" • {kp}" for kp in key_points) if key_points else " (议程为空)"
decisions_str = "\n".join(f" • {d}" for d in decisions_needed[:5]) if decisions_needed else " (暂无)"
risks_str = "\n".join(f" • {r}" for r in risks[:3]) if risks else " (暂无明显风险)"
actions_str = "\n".join(f" • {a}" for a in suggested_actions[:5]) if suggested_actions else " (暂无)"
briefing_text = BOSS_BRIEFING_TEMPLATE.format(
meeting_topic=meeting_topic,
meeting_date=meeting_date,
conclusion=conclusion,
key_points=key_points_str,
decisions_needed=decisions_str,
risks=risks_str,
suggested_actions=actions_str,
DISCLAIMER=disclaimer_block()
)
return {
"status": "success",
"task": "briefing",
"mode": "boss",
"meeting_topic": meeting_topic,
"meeting_date": meeting_date,
"participants": participant_list,
"agenda_items": agenda_items,
"sections": {
"conclusion": conclusion,
"key_points": key_points,
"decisions_needed": decisions_needed[:5],
"risks": risks[:3],
"suggested_actions": suggested_actions[:5]
},
"briefing_text": briefing_text.strip()
}
def build_minutes(meeting_topic: str, meeting_date: str,
raw_text: str, decisions: str,
participants: str, mode: str) -> Dict[str, Any]:
"""
生成结构化会议纪要。
支持 boss / executor 两种 mode(目前 executor 模式输出更详细的待办提取)。
"""
# 纯空格 / 空字符串校验(DEF-001 修复)
if not raw_text or not raw_text.strip():
return {
"status": "error",
"message": "raw_text 不能为空或纯空格",
}
discussion_points = parse_raw_text_to_points(raw_text)
decision_list = split_list(decisions, "|") if decisions else []
participant_list = split_list(participants, "|") if participants else []
# 提取待办事项
action_items = extract_action_items(raw_text, decisions, participants)
# 格式化讨论要点(boss mode 精简,executor mode 保留更多细节)
if mode == "boss":
dp_display = "\n".join(f" • {p}" for p in discussion_points) if discussion_points else " (无记录)"
else:
dp_display = "\n".join(f" {i}. {p}" for i, p in enumerate(discussion_points, 1)) if discussion_points else " (无记录)"
decisions_display = "\n".join(f" ✅ {d}" for d in decision_list) if decision_list else " (暂无明确决议)"
action_items_display = format_action_items(action_items)
minutes_text = EXECUTOR_MINUTES_TEMPLATE.format(
meeting_topic=meeting_topic,
meeting_date=meeting_date,
discussion_points=dp_display,
decisions=decisions_display,
action_items=action_items_display,
DISCLAIMER=disclaimer_block()
)
return {
"status": "success",
"task": "minutes",
"mode": mode,
"meeting_topic": meeting_topic,
"meeting_date": meeting_date,
"participants": participant_list,
"minutes": {
"discussion_points": discussion_points,
"decisions": decision_list,
"action_items": action_items
},
"minutes_text": minutes_text.strip()
}
def build_followup(meeting_topic: str, meeting_date: str,
decisions: str, action_items: List[Dict],
mode: str) -> Dict[str, Any]:
"""
生成会后 follow-up 草稿(待办追踪 + 邮件草稿 + 消息草稿)。
"""
decision_list = split_list(decisions, "|") if decisions else []
followup_items_list = []
for i, item in enumerate(action_items, 1):
dl = f"截止:{item['deadline']}" if item.get("deadline") else "截止:待定"
followup_items_list.append(f" {i}. [{item['priority'].upper()}] {item['task']} — {item['owner']}({dl})")
followup_items_str = "\n".join(followup_items_list) if followup_items_list else " (暂无明确待办)"
drafts = generate_followup_draft(meeting_topic, meeting_date, action_items, mode, decisions)
if mode == "executor":
followup_text = EXECUTOR_FOLLOWUP_TEMPLATE.format(
meeting_topic=meeting_topic,
meeting_date=meeting_date,
followup_items=followup_items_str,
draft_email=drafts["draft_email"],
draft_message=drafts["draft_message"],
DISCLAIMER=disclaimer_block()
)
else:
summary = ";".join(decision_list) if decision_list else f"共 {len(action_items)} 项待办待落实"
followup_text = BOSS_FOLLOWUP_TEMPLATE.format(
meeting_topic=meeting_topic,
meeting_date=meeting_date,
summary=summary,
followup_items=followup_items_str,
draft_email=drafts["draft_email"],
draft_message=drafts["draft_message"],
DISCLAIMER=disclaimer_block()
)
return {
"status": "success",
"task": "followup",
"mode": mode,
"meeting_topic": meeting_topic,
"meeting_date": meeting_date,
"decisions": decision_list,
"followup_items": action_items,
"draft_email": drafts["draft_email"],
"draft_message": drafts["draft_message"],
"followup_text": followup_text.strip()
}
def disclaimer_block() -> str:
"""提取免责声明纯文本部分"""
return "\n".join(line.strip() for line in DISCLAIMER.strip().split("\n") if line.strip())
# ─── 主入口 ──────────────────────────────────────────────────────────────────
def handle(topic: Optional[str], user_input: Optional[str],
history: Optional[List] = None, args: Optional[Dict] = None) -> Dict[str, Any]:
"""
Meeting Ops Copilot 主处理函数。
args 字典支持以下字段:
task : "briefing" | "minutes" | "followup"
meeting_topic : string
meeting_date : string (YYYY-MM-DD)
mode : "boss" | "executor"
agenda : string (| 分隔,briefing 必填)
raw_text : string (minutes 必填)
decisions : string (| 分隔)
participants : string (| 分隔)
action_items : list (followup 必填,从 minutes 输出获取)
"""
if args is None:
args = {}
# 兼容旧接口:topic 可能携带 JSON
if topic and not args:
try:
args_candidate = json.loads(topic)
if isinstance(args_candidate, dict):
args = args_candidate
topic = args.get("meeting_topic", "")
except (json.JSONDecodeError, TypeError):
# 尝试从 user_input 中解析
if user_input:
try:
args_candidate = json.loads(user_input)
if isinstance(args_candidate, dict):
args = args_candidate
user_input = ""
except (json.JSONDecodeError, TypeError):
pass
task = args.get("task", "")
meeting_topic = args.get("meeting_topic", topic or "")
meeting_date = args.get("meeting_date", datetime.now().strftime("%Y-%m-%d"))
mode = args.get("mode", "executor")
agenda = args.get("agenda", "")
raw_text = args.get("raw_text", user_input or "")
decisions = args.get("decisions", "")
participants = args.get("participants", "")
action_items = args.get("action_items", [])
# 参数校验
if task not in ("briefing", "minutes", "followup"):
return {
"status": "error",
"message": "不支持的任务类型。请指定 task:briefing / minutes / followup",
"disclaimer": DISCLAIMER
}
if not meeting_topic:
return {
"status": "error",
"message": "缺少 meeting_topic(会议主题)",
"disclaimer": DISCLAIMER
}
if task == "briefing" and not agenda:
return {
"status": "error",
"message": "briefing 任务需要提供 agenda(议程,用 | 分隔多条)",
"disclaimer": DISCLAIMER
}
if task == "minutes" and (not raw_text or not raw_text.strip()):
return {
"status": "error",
"message": "minutes 任务需要提供 raw_text(原始讨论文本)",
"disclaimer": DISCLAIMER
}
# 执行
if task == "briefing":
return build_briefing(meeting_topic, meeting_date, agenda, participants)
elif task == "minutes":
return build_minutes(meeting_topic, meeting_date, raw_text, decisions, participants, mode)
elif task == "followup":
if not action_items and not decisions:
return {
"status": "error",
"message": "followup 任务需要提供 action_items(从 minutes 输出获取)或 decisions",
"disclaimer": DISCLAIMER
}
return build_followup(meeting_topic, meeting_date, decisions, action_items, mode)
# ─── 自测分支 ────────────────────────────────────────────────────────────────
if __name__ == "__main__":
import json
print("=" * 60)
print("🧪 Meeting Ops Copilot — 自测")
print("=" * 60)
# ── Test 1: Briefing ─────────────────────────────────────────────
print("\n📋 Test 1: Briefing(boss mode)")
result1 = handle(
topic="Q2 产品规划评审",
user_input="",
args={
"task": "briefing",
"meeting_topic": "Q2 产品规划评审",
"meeting_date": "2026-04-05",
"mode": "boss",
"agenda": "Q2目标对齐|技术方案选型|资源评估",
"participants": "产品经理|研发负责人|设计负责人"
}
)
print(f" status: {result1['status']}")
print(f" sections keys: {list(result1.get('sections', {}).keys())}")
assert result1["status"] == "success", "briefing failed"
assert "conclusion" in result1["sections"]
assert "risks" in result1["sections"]
print(" ✅ PASS")
# ── Test 2: Minutes ──────────────────────────────────────────────
print("\n📋 Test 2: Minutes(executor mode)")
result2 = handle(
topic="周例会",
user_input="",
args={
"task": "minutes",
"meeting_topic": "周例会",
"meeting_date": "2026-03-31",
"mode": "executor",
"raw_text": "讨论了A功能延期的风险;决定启用备选方案;张三分头对接供应商;李四负责测试并提交报告;王五协调测试环境",
"decisions": "启用备选方案",
"participants": "张三|李四|王五"
}
)
print(f" status: {result2['status']}")
assert result2["status"] == "success"
assert len(result2["minutes"]["action_items"]) >= 3, f"Expected >=3 action items, got {len(result2['minutes']['action_items'])}"
print(f" action_items extracted: {len(result2['minutes']['action_items'])}")
for item in result2["minutes"]["action_items"]:
print(f" - {item['task']} | owner={item['owner']} | priority={item['priority']}")
print(" ✅ PASS")
# ── Test 3: Follow-up ────────────────────────────────────────────
print("\n📋 Test 3: Follow-up(executor mode)")
result3 = handle(
topic="周例会",
user_input="",
args={
"task": "followup",
"meeting_topic": "周例会",
"meeting_date": "2026-03-31",
"mode": "executor",
"decisions": "启用备选方案",
"action_items": result2["minutes"]["action_items"]
}
)
print(f" status: {result3['status']}")
assert result3["status"] == "success"
assert result3["draft_email"]
assert result3["draft_message"]
print(f" draft_email length: {len(result3['draft_email'])}")
print(f" draft_message: {result3['draft_message']}")
print(" ✅ PASS")
# ── Test 4: Boss Mode Minutes ────────────────────────────────────
print("\n📋 Test 4: Minutes(boss mode)")
result4 = handle(
topic="Q2规划评审",
user_input="",
args={
"task": "minutes",
"meeting_topic": "Q2规划评审",
"meeting_date": "2026-04-05",
"mode": "boss",
"raw_text": "确认Q2目标为DAU增长30%;技术方案采用微服务架构;需要增编2人",
"decisions": "Q2目标确认|采用微服务架构",
"participants": "产品|研发|设计"
}
)
print(f" status: {result4['status']}")
assert result4["status"] == "success"
print(" ✅ PASS")
# ── Test 5: Validation — missing params ──────────────────────────
print("\n📋 Test 5: 参数校验(缺少 agenda)")
result5 = handle(
topic="测试会议",
user_input="",
args={
"task": "briefing",
"meeting_topic": "测试会议",
"meeting_date": "2026-03-31",
"mode": "boss",
"agenda": "" # 空
}
)
assert result5["status"] == "error"
print(" ✅ PASS(正确拒绝)")
# ── Test 6: Follow-up from scratch ────────────────────────────────
print("\n📋 Test 6: Follow-up 仅凭 decisions(无 action_items)")
result6 = handle(
topic="测试会议",
user_input="",
args={
"task": "followup",
"meeting_topic": "测试会议",
"meeting_date": "2026-03-31",
"mode": "boss",
"decisions": "Q2目标确认为DAU+30%",
"action_items": []
}
)
print(f" status: {result6['status']}")
assert result6["status"] == "success"
print(" ✅ PASS")
print("\n" + "=" * 60)
print("✅ 所有自测用例通过!")
print("=" * 60)
print("\n使用说明:")
print(" python3 handler.py")
print(" 或通过 skill 框架调用 handle(topic, user_input, history, args)")
FILE:skill.json
{
"name": "meeting-copilot",
"description": "会议运营 Copilot — 支持会前 briefing 生成、会中结构化纪要整理、会后待办追踪与 follow-up 草稿。提供 boss mode(向上汇报视角)和 executor mode(向下追踪视角)两种输出格式。⚠️ 不提供实时语音转录、日历集成或自动提醒。",
"version": "1.0.3",
"author": "Harry",
"tags": [
"meeting",
"ops",
"minutes",
"briefing",
"followup",
"workplace"
],
"safety": {
"no_diagnosis": false,
"no_treatment": false,
"disclaimer_required": true,
"high_risk_keyword_detection": false
},
"modules": {
"handler": "handler.py"
},
"entry": "handler.py"
}
FILE:tests/test_handler.py
#!/usr/bin/env python3
"""
test_handler.py — Meeting Ops Copilot 测试套件
运行方式:python3 tests/test_handler.py
"""
import sys
import json
from pathlib import Path
# 确保 handler.py 在路径中
sys.path.insert(0, str(Path(__file__).parent.parent))
from handler import handle, build_briefing, build_minutes, build_followup, split_list, parse_raw_text_to_points, extract_action_items
def test_split_list():
assert split_list("a|b|c") == ["a", "b", "c"]
assert split_list("a| b | c") == ["a", "b", "c"]
assert split_list("") == []
assert split_list("单条") == ["单条"]
print(" ✅ split_list PASS")
def test_parse_raw_text():
text = "讨论了A功能延期的风险;决定启用备选方案;张三分头对接供应商"
points = parse_raw_text_to_points(text)
assert len(points) >= 2
assert "A功能" in points[0]
print(" ✅ parse_raw_text_to_points PASS")
def test_extract_action_items():
raw = "张三分头对接供应商;李四负责测试并提交报告;王五协调测试环境"
items = extract_action_items(raw, "", "")
assert len(items) >= 2
owners = [item["owner"] for item in items]
assert "张三" in owners
assert "李四" in owners
print(" ✅ extract_action_items PASS")
def test_briefing_boss_mode():
result = build_briefing(
meeting_topic="Q2 产品规划评审",
meeting_date="2026-04-05",
agenda="Q2目标对齐|技术方案选型|资源评估",
participants="产品经理|研发负责人|设计负责人"
)
assert result["status"] == "success"
assert result["task"] == "briefing"
assert result["mode"] == "boss"
sections = result["sections"]
assert "conclusion" in sections
assert "key_points" in sections
assert "risks" in sections
assert "suggested_actions" in sections
assert len(sections["key_points"]) == 3
assert len(sections["decisions_needed"]) == 3
assert result["briefing_text"]
print(" ✅ briefing_boss_mode PASS")
def test_minutes_executor_mode():
result = build_minutes(
meeting_topic="周例会",
meeting_date="2026-03-31",
raw_text="讨论了A功能延期的风险;决定启用备选方案;张三分头对接供应商;李四负责测试",
decisions="启用备选方案",
participants="张三|李四|王五",
mode="executor"
)
assert result["status"] == "success"
assert result["task"] == "minutes"
assert "discussion_points" in result["minutes"]
assert "decisions" in result["minutes"]
assert "action_items" in result["minutes"]
assert len(result["minutes"]["action_items"]) >= 2
assert result["minutes_text"]
# 检查待办提取
items = result["minutes"]["action_items"]
owners = [item["owner"] for item in items]
assert "张三" in owners
print(" ✅ minutes_executor_mode PASS")
def test_minutes_boss_mode():
result = build_minutes(
meeting_topic="Q2规划评审",
meeting_date="2026-04-05",
raw_text="确认Q2目标为DAU增长30%;技术方案采用微服务架构",
decisions="Q2目标确认|采用微服务架构",
participants="产品|研发",
mode="boss"
)
assert result["status"] == "success"
assert result["mode"] == "boss"
assert result["minutes"]["decisions"] == ["Q2目标确认", "采用微服务架构"]
print(" ✅ minutes_boss_mode PASS")
def test_followup_executor_mode():
action_items = [
{"task": "对接供应商", "owner": "张三", "deadline": "2026-04-10", "priority": "high"},
{"task": "测试并提交报告", "owner": "李四", "deadline": "2026-04-12", "priority": "high"},
]
result = build_followup(
meeting_topic="周例会",
meeting_date="2026-03-31",
decisions="启用备选方案",
action_items=action_items,
mode="executor"
)
assert result["status"] == "success"
assert result["task"] == "followup"
assert result["draft_email"]
assert result["draft_message"]
assert "Subject:" in result["draft_email"]
assert "对接供应商" in result["draft_email"]
print(" ✅ followup_executor_mode PASS")
def test_followup_boss_mode():
action_items = [
{"task": "确认Q2目标", "owner": "产品", "deadline": "2026-04-07", "priority": "high"},
]
result = build_followup(
meeting_topic="Q2规划评审",
meeting_date="2026-04-05",
decisions="Q2目标确认为DAU+30%",
action_items=action_items,
mode="boss"
)
assert result["status"] == "success"
assert result["mode"] == "boss"
assert "DAU" in result["draft_email"]
print(" ✅ followup_boss_mode PASS")
def test_handle_main_entry():
"""通过 handle() 主入口调用,测试完整流程"""
result = handle(
topic="",
user_input="",
args={
"task": "briefing",
"meeting_topic": "Q2 产品规划评审",
"meeting_date": "2026-04-05",
"mode": "boss",
"agenda": "Q2目标对齐|技术方案选型",
"participants": "产品经理|研发负责人"
}
)
assert result["status"] == "success"
assert result["sections"]["conclusion"]
print(" ✅ handle_main_entry PASS")
def test_validation_missing_agenda():
result = handle(
topic="测试会议",
user_input="",
args={
"task": "briefing",
"meeting_topic": "测试会议",
"meeting_date": "2026-03-31",
"mode": "boss",
"agenda": ""
}
)
assert result["status"] == "error"
assert "需要提供 agenda" in result["message"]
print(" ✅ validation_missing_agenda PASS")
def test_validation_missing_topic():
result = handle(
topic="",
user_input="",
args={
"task": "minutes",
"meeting_topic": "",
"meeting_date": "2026-03-31",
"mode": "executor",
"raw_text": "some discussion text"
}
)
assert result["status"] == "error"
print(" ✅ validation_missing_topic PASS")
def test_validation_unknown_task():
result = handle(
topic="测试",
user_input="",
args={
"task": "unknown_task",
"meeting_topic": "测试",
"meeting_date": "2026-03-31",
"mode": "boss"
}
)
assert result["status"] == "error"
assert "不支持的任务类型" in result["message"]
print(" ✅ validation_unknown_task PASS")
def test_validation_whitespace_raw_text():
# DEF-001: 纯空格字符串应被正确拒绝
result = handle(
topic="",
user_input="",
args={
"task": "minutes",
"meeting_topic": "测试会议",
"meeting_date": "2026-03-31",
"mode": "executor",
"raw_text": " " # 纯空格
}
)
assert result["status"] == "error", "纯空格 raw_text 应返回 error"
assert "raw_text" in result["message"]
print(" ✅ test_validation_whitespace_raw_text PASS")
def test_validation_empty_raw_text():
# DEF-001: 空字符串应被正确拒绝
result = handle(
topic="",
user_input="",
args={
"task": "minutes",
"meeting_topic": "测试会议",
"meeting_date": "2026-03-31",
"mode": "executor",
"raw_text": ""
}
)
assert result["status"] == "error", "空 raw_text 应返回 error"
assert "raw_text" in result["message"]
print(" ✅ test_validation_empty_raw_text PASS")
def run_all_tests():
print("=" * 60)
print("🧪 Meeting Ops Copilot — 测试套件")
print("=" * 60)
tests = [
("split_list", test_split_list),
("parse_raw_text", test_parse_raw_text),
("extract_action_items", test_extract_action_items),
("briefing_boss_mode", test_briefing_boss_mode),
("minutes_executor_mode", test_minutes_executor_mode),
("minutes_boss_mode", test_minutes_boss_mode),
("followup_executor_mode", test_followup_executor_mode),
("followup_boss_mode", test_followup_boss_mode),
("handle_main_entry", test_handle_main_entry),
("validation_missing_agenda", test_validation_missing_agenda),
("validation_missing_topic", test_validation_missing_topic),
("validation_unknown_task", test_validation_unknown_task),
("validation_whitespace_raw_text", test_validation_whitespace_raw_text),
("validation_empty_raw_text", test_validation_empty_raw_text),
]
passed = 0
failed = 0
for name, fn in tests:
try:
print(f"\n▶ {name}")
fn()
passed += 1
except AssertionError as e:
print(f" ❌ FAIL: {e}")
failed += 1
except Exception as e:
print(f" ❌ ERROR: {e}")
failed += 1
print("\n" + "=" * 60)
print(f"结果:{passed} 通过,{failed} 失败")
print("=" * 60)
return failed == 0
if __name__ == "__main__":
ok = run_all_tests()
sys.exit(0 if ok else 1)
Sales pipeline operating skill that watches CRM, spreadsheets, and follow-up queues to identify stale deals, overdue next steps, stage bottlenecks, and close...
---
name: Pipeline Keeper
slug: pipeline-keeper
version: 1.0.0
description: Sales pipeline operating skill that watches CRM, spreadsheets, and follow-up queues to identify stale deals, overdue next steps, stage bottlenecks, and close-risk accounts, then recommends exactly who to push today and drafts the next message.
metadata:
clawdbot:
emoji: "📈"
requires:
bins: []
os: ["linux", "darwin", "win32"]
---
# Pipeline Keeper
Pipeline Keeper is not a CRM connector. It is the sales judgment layer on top of CRM, ERP, spreadsheets, inbox notes, and deal history.
Use this skill when the user wants answers like:
- 哪些商机快冷了
- 哪些客户今天必须跟进
- 哪些机会卡在某个阶段太久
- 今天最该推进哪 5 个对象
- 帮我写不同语气的 follow-up
- 做日报、周检、丢单预警、漏斗巡检
This skill should feel like a sharp sales ops lead or founder's pipeline chief of staff.
## What Good Looks Like
Do not stop at "here are the records".
Good output ends in action:
- follow up today
- push for a meeting
- confirm buying process
- revive with a value-add note
- escalate internally
- recycle or disqualify
The goal is not documentation. The goal is forward motion.
## Trigger Conditions
Activate when the user asks for pipeline review, follow-up prioritization, deal risk detection, stuck-stage analysis, or outreach drafting, especially around:
- CRM and ERP exports
- spreadsheets or CSV deal trackers
- account notes, meeting notes, and inbox threads
- daily or weekly revenue review
- founder-led sales follow-up
- B2B, agency, consulting, SaaS, or high-ticket sales motions
Example asks:
- "看下哪些客户该跟进了"
- "帮我排今天最该推进的商机"
- "哪些单子快黄了"
- "哪些机会卡在 proposal 太久"
- "给我写 3 个不同语气的跟进消息"
- "做一份今天的 pipeline 巡检"
## Source Handling
Prefer this source order:
1. CRM or ERP for stage, owner, amount, close date, and last activity
2. Spreadsheet tracker for custom fields and manual annotations
3. Email, chat, and meeting notes for context and message drafting
Do not pretend you have access to systems you cannot read.
If the user has not provided a source yet, ask for one compactly or work from pasted data.
## Minimum Useful Deal Schema
Try to build a working table with as many of these fields as available:
- account or opportunity name
- contact name
- stage
- amount or expected value
- owner
- last meaningful touch date
- next step
- next step due date
- estimated close date
- latest reply status
- key blockers or objections
Derived fields to compute when possible:
- days since last touch
- days in current stage
- days overdue on next step
- close-window urgency
- cold-risk level
- recommended action
If important fields are missing, keep working and mark confidence as directional.
## Core Workflow
1. Normalize the pipeline.
- deduplicate the same account or opportunity if it appears across multiple sources
- distinguish real customer activity from internal note churn
- prefer the latest verified stage instead of the most optimistic one
2. Detect urgency and risk.
- identify overdue follow-ups
- identify opportunities with no clear next step
- identify deals stuck in stage beyond a reasonable dwell time
- identify late-stage deals with weak evidence of momentum
3. Rank what matters today.
- balance revenue impact, urgency, controllability, and momentum
- avoid ranking only by deal size
- today’s top list should usually be short and pushable
4. Recommend the next move.
- each priority account should end with one concrete action
- if the correct move is to pause, recycle, or disqualify, say so plainly
5. Draft outreach when useful.
- tailor tone to stage, relationship warmth, and urgency
- give the user something sendable after light editing
## Decision Rules
### Overdue Follow-Up
Treat a deal as overdue when:
- a promised next step date has passed
- there has been no meaningful customer touch for longer than the stage supports
- the customer owes a response and no follow-up is queued
Do not call a deal overdue just because internal notes are old.
### Cold Risk
Cold risk rises when several of these stack together:
- no reply after a clear ask
- no next meeting or next step on calendar
- stage age is high for the motion
- close date keeps slipping
- buyer process is vague
- champion went quiet
- objections are repeating without movement
Be explicit about whether risk is observed or inferred.
### Stage Bottlenecks
Call out stage-level issues when many opportunities are lingering in the same stage beyond normal dwell time.
Examples:
- too many discovery calls with no next meeting
- demos completed but no proposal request
- proposals sent with no mutual close plan
- procurement or legal marked as active but no real owner or deadline
### Priority Ranking
For "today's top 5", prefer deals that combine:
- meaningful upside
- a clear action the user can take today
- evidence that a nudge can change the outcome
- near-term timing pressure
Deprioritize:
- giant deals with no buying signal
- deals already waiting on a dated future step
- dead opportunities disguised as pipeline
## Messaging Rules
When drafting follow-up messages:
- do not write "just checking in"
- do not fake urgency or scarcity
- do not guilt-trip the buyer
- do not over-write; keep messages short and useful
- include a clear next step or decision request
- use the customer's context, not generic filler
Offer 2 to 3 tone options when the user asks for copy:
- warm and helpful
- direct and commercial
- value-add or insight-led
Use breakup or close-the-loop messaging only when the motion is clearly stale.
## Output Pattern
Use this structure unless the user asks for something else:
### Today To Push
List the highest-leverage deals first.
### Risk Watch
Call out deals likely to slip, stall, or go dark.
### Stuck Stage Scan
Identify stage bottlenecks and what they imply.
### Recommended Moves
Give one direct next move per important deal.
### Drafted Follow-Up
Provide sendable copy when useful.
## Tone
Sound revenue-minded, practical, and decisive.
Good phrasing:
- "先说今天最该推的 5 个"
- "这单不是没希望,是缺一个明确 next step"
- "风险不是金额小,而是已经失去成交节奏"
- "现在该推的是决策,不是再发一轮泛泛资料"
- "如果今天只能做 3 件事,我会先动这 3 个对象"
Avoid sounding like:
- a passive report generator
- a neutral CRM export
- a vague management consultant
## Safety And Boundaries
- This skill can read from CRM-like systems if such tools are available, but it is not the system of record itself.
- Do not silently rewrite CRM data without user intent.
- Do not send emails, IMs, or reminders automatically without explicit confirmation.
- Do not inflate confidence or fabricate sales signals.
## Reference Files
Load only the references that match the task:
- `references/pipeline-signals.md` for dwell-time heuristics, scoring, and risk signals
- `references/follow-up-playbook.md` for tone choices and scenario-specific outreach patterns
- `references/output-modes.md` for daily brief, weekly review, and audit output formats
FILE:agents/openai.yaml
interface:
display_name: "商机跟进官"
short_description: "盯 CRM 和跟进节奏,找出快冷商机、卡阶段机会和今天最该推进的客户"
default_prompt: "Use $pipeline-keeper to inspect my CRM, spreadsheet, or deal notes, identify overdue follow-ups, stale opportunities, stage bottlenecks, and the top accounts to push today, then draft the next-best outreach in the right tone."
FILE:clawhub.json
{
"name": "pipeline-keeper",
"version": "1.0.0",
"description": "商机跟进官 Pipeline Keeper - 盯销售漏斗、识别卡单和丢单风险,给出今天最该推进的客户与跟进文案",
"keywords": [
"pipeline-keeper",
"sales",
"crm",
"pipeline",
"follow-up",
"sales-ops",
"deal-risk",
"revenue-ops",
"b2b",
"followup-copy"
],
"author": "openclaw",
"license": "MIT"
}
FILE:references/follow-up-playbook.md
# Follow-Up Playbook
Use this file when the user wants outreach copy, tone choices, or stage-specific follow-up tactics.
## Messaging Objectives
Every follow-up should do one of these:
- confirm a next step
- unblock a blocker
- recover momentum
- ask for a decision
- close the loop cleanly
If a message does none of these, it is probably filler.
## Tone Modes
### Warm And Helpful
Use when the relationship is positive and the deal is still active.
Pattern:
- reference the last conversation
- add one useful angle
- propose one easy next step
### Direct And Commercial
Use when timing matters and the buyer already knows the value.
Pattern:
- restate the pending decision
- ask for a clear yes, no, or timing answer
- keep it short
### Value-Add
Use when the buyer has gone quiet and you need a reason to re-engage.
Pattern:
- share a relevant insight, example, or customer proof
- connect it to their stated problem
- end with one low-friction question
### Breakup / Close-The-Loop
Use when the motion is stale and the user wants clarity.
Pattern:
- acknowledge timing may have changed
- release pressure
- invite a clean close or a later restart
## Scenario Patterns
### No Reply After Demo
Best move:
- summarize the business problem discussed
- ask whether the priority is still active
- offer one concrete next step
### Proposal Sent, Then Silence
Best move:
- avoid resending the same deck blindly
- ask if they want to review commercial terms together
- push for the actual decision process and date
### "Need Internal Alignment"
Best move:
- ask who else is involved
- offer a short summary or one-pager for internal forwarding
- request a date for the next decision checkpoint
### Legal / Procurement Drag
Best move:
- identify exact blocker and owner
- separate process delay from deal risk
- ask for the one document, answer, or approval needed next
### Budget Pushback
Best move:
- return to value before discount
- narrow scope only if it preserves a winnable entry
- do not lead with discounting if urgency is weak
## Copy Rules
Do:
- keep the first sentence grounded in context
- ask for one next move
- make replying easy
Do not:
- write "just bumping this"
- stack three asks in one note
- sound needy or passive-aggressive
- create fake deadline pressure
## Useful Output Format
When drafting copy, provide:
- `Goal`: why this message exists
- `Tone`: warm, direct, value-add, or breakup
- `Message`: a sendable draft
If useful, include 2 variants:
- safer version
- stronger version
FILE:references/output-modes.md
# Output Modes
Use this file when the user asks for a specific reporting format or recurring pipeline review style.
## Daily Push List
Best for:
- "今天最该跟谁推进"
- founder morning review
- SDR or AE start-of-day prioritization
Recommended structure:
- `Today To Push`: top 3 to 5 deals with one-line why
- `Best Next Move`: one action per deal
- `Drafted Follow-Up`: include copy only for the top priorities
## Risk Audit
Best for:
- "哪些单快黄了"
- weekly pipeline hygiene
- late-stage slip detection
Recommended structure:
- `High Risk`
- `Medium Risk`
- `Why Risk Is Rising`
- `Save Or Remove`
## Stage Bottleneck Review
Best for:
- "卡在哪个阶段"
- manager or founder pipeline inspection
Recommended structure:
- `Stuck Stage`
- `What The Pattern Suggests`
- `Operational Fix`
Example operational fixes:
- discovery qualification too weak
- demos happen before stakeholder map is clear
- proposals are sent without a mutual action plan
## Account Push Plan
Best for:
- one important account
- pre-call or pre-follow-up prep
Recommended structure:
- `Current Read`
- `Likely Blocker`
- `Best Next Move`
- `Message Draft`
## Weekly Pipeline Brief
Best for:
- Monday review
- revenue check-in
- founder summary
Recommended structure:
- `What Moved`
- `What Stalled`
- `Top Risks`
- `Top Bets For This Week`
## Confidence Labeling
When source quality is partial, label your read:
- `High confidence`: stage, dates, and recent activity are visible
- `Medium confidence`: some dates or notes are missing
- `Directional`: the judgment is based on partial exports or pasted summaries
FILE:references/pipeline-signals.md
# Pipeline Signals
Use this file when the task needs scoring logic, dwell-time heuristics, or stage-risk reasoning.
## Minimum Scoring Model
A practical priority model can combine four dimensions:
- impact: deal size, strategic value, or expansion value
- urgency: overdue next step, close date pressure, procurement deadline
- momentum: recent reply, meeting booked, stage progression, clear champion
- risk: silence, no next step, repeated objection loops, stage stagnation
Simple rule:
- high impact + high urgency + still controllable = today priority
- high impact + low signal + long stagnation = manager attention, not always top outreach priority
## Derived Field Heuristics
Useful derived fields:
- `days_since_last_touch`
- `days_in_stage`
- `days_overdue_next_step`
- `reply_gap_days`
- `close_date_slip_count`
- `has_named_decision_process`
- `has_next_meeting`
- `champion_strength`
If the source does not provide these directly, infer them from available timestamps and notes.
## Stage Dwell Guidelines
These are default heuristics, not laws. Adjust if the user's motion is enterprise, SMB, agency, or founder-led.
### New Lead / First Contact
- healthy: 1 to 3 business days
- warning: 4 to 7 business days with no real touch
- action: send the first concrete ask, not a generic hello
### Discovery
- healthy: 5 to 10 business days
- warning: discovery completed but no next meeting, no problem statement, or no buyer map
- action: confirm pain, stakeholders, and next milestone
### Demo / Trial
- healthy: 7 to 14 business days
- warning: demo happened but no usage, no evaluation plan, or no technical owner
- action: anchor on success criteria and decision path
### Proposal / Quote
- healthy: 5 to 10 business days
- warning: proposal sent with no response, no review date, or no decision process
- action: ask for review call or explicit decision timing
### Procurement / Security / Legal
- healthy: 10 to 20 business days
- warning: labeled as "legal" or "procurement" but no owner, no ticket, no target date
- action: identify blocker owner and unblock sequence
### Verbal Commit / Close Plan
- healthy: 3 to 7 business days
- warning: verbal yes but no paper process, no signature path, or no implementation handoff
- action: convert intent into dated commitments
## Risk Signals
Strong risk signals:
- customer stopped replying after a proposal or pricing discussion
- close date moved more than once without a clear reason
- the deal has no next step owner
- multiple internal notes exist but no external progress
- champion left, changed role, or lost urgency
- repeated requests for materials without decision movement
- competitor appears late and urgency drops
- discount requests appear before value is accepted
Moderate risk signals:
- long gaps between touches
- stage name looks advanced but notes are still early
- all activity is seller-side follow-up
- next step exists but is vague
## Ranking Guidance For "Top 5 Today"
Promote deals when:
- there is a believable path to movement today
- a message, call, or escalation can change the outcome
- the next action is obvious
Demote deals when:
- no one knows the buying process
- the account is not responding and there is no new angle
- the stage is inflated
- the only reason it ranks high is amount
## Disqualify Or Recycle Guidance
Recommend recycle or disqualification when:
- there is no active pain and no timeline
- follow-up has become repetitive with no new information
- the opportunity lives in pipeline for optics only
- the buyer repeatedly misses agreed next steps with no recovery signal
Say this directly. Healthy pipeline hygiene is part of revenue discipline.
Cross-platform buying decision skill that compares the same product across Taobao, Tmall, JD, PDD, VIPSHOP, and similar marketplaces, distinguishes flagship,...
---
name: Buying
slug: buying
version: 2.0.0
description: Cross-platform buying decision skill that compares the same product across Taobao, Tmall, JD, PDD, VIPSHOP, and similar marketplaces, distinguishes flagship, self-operated, and third-party sellers, normalizes coupon-adjusted price and fulfillment tradeoffs, and outputs the optimal purchase path instead of a raw price table.
metadata:
clawdbot:
emoji: "🛒"
requires:
bins: []
os: ["linux", "darwin", "win32"]
---
# Buying
Buying is not just a shopping helper.
It is the cross-platform judgment layer above Taobao, Tmall, JD, PDD, VIPSHOP, and similar shopping channels.
Its job is to help the user answer:
- 同一商品到底该在哪个平台买
- 这家便宜是不是因为风险更高
- 旗舰店、自营、第三方差别到底值多少钱
- 券后价、配送时效、售后难度一起算,最优购买路径是什么
- 现在该下单、换平台、换店铺,还是再等等
It should feel like a decisive shopping router, not a comparison spreadsheet.
## Core Positioning
Do not treat platform skills as isolated islands.
Buying should unify them and produce one clear recommendation:
- best price path
- safest buy path
- fastest arrival path
- best value path
- avoid-buy path
The output should tell the user what to do next, not just where the prices are.
## Triggers
Activate when the user asks things like:
- "淘宝、拼多多、京东、唯品会到底买哪边"
- "这两个链接是同款吗,差价为什么这么大"
- "旗舰店、自营、第三方哪个更值"
- "这个便宜是不是因为售后更差"
- "帮我给一个最优购买路径"
- "不要只比价,直接告诉我在哪里买最合适"
This skill is strongest when the user is already deciding across several platforms or wondering whether the cheapest route is actually worth it.
## Before Acting
Clarify or infer these if they matter:
- exact SKU or equivalent variant
- budget: hard cap or flexible
- priority: lowest price, lowest risk, fastest delivery, or best value
- urgency: need now or can wait
- seller tolerance: official channel only or acceptable third-party risk
If the user does not provide enough detail, make a practical assumption and state it.
## What This Skill Must Do
Default to these outcomes:
- compare the same or equivalent product across platforms
- distinguish flagship, self-operated, authorized, and generic third-party sellers
- normalize final payable price instead of sticker price
- weigh delivery certainty and after-sales friction
- explain why one option is cheaper
- output one or more purchase paths for different priorities
Do not stop at a comparison table.
## Input Handling
Useful inputs include:
- product links
- screenshots
- copied titles
- SKU names and variants
- price and coupon details
- seller or store names
Before comparing, normalize:
- exact product or equivalent variant
- capacity, color, model year, bundle, and gift differences
- seller type
- payment conditions
If the offers are not actually comparable, say so plainly before recommending anything.
## Core Flow
1. Normalize the item.
- confirm same SKU or clearly label near-equivalent substitutions
- separate official listings from lookalikes or weaker bundles
2. Normalize the real price.
- listed price
- coupon-adjusted price
- subsidy or flash-sale conditions
- shipping and packaging
- membership, threshold, or group-buy constraints
3. Classify the seller path.
- flagship store
- JD self-operated
- authorized distributor
- marketplace third-party
- outlet or flash-sale inventory
4. Evaluate tradeoffs.
- authenticity confidence
- shipping speed and reliability
- return and warranty friction
- whether the cheaper price is caused by higher risk
5. Output the optimal purchase path.
- best overall
- cheapest acceptable
- safest default
- optional faster or lower-risk alternative
## Core Questions To Answer
Every recommendation should answer these:
- Which platform should the user buy from?
- Which seller type should they prefer there?
- What is the real final price?
- Why is another option cheaper or more expensive?
- Is the cheaper path still worth it after risk adjustment?
- What should the user do right now?
## Seller-Type Rules
Always distinguish seller type, not just platform.
Treat these as different trust layers:
- brand flagship or official store
- JD self-operated
- authorized chain or verified distributor
- marketplace third-party
- unclear source or low-trust seller
The same platform can contain both clean and risky paths.
## Price Research Rules
A lower displayed price is not enough.
Normalize for:
- platform coupons
- store coupons
- membership or threshold gates
- cross-store full reduction
- shipping fees
- packaging or service fees
- bundle requirements
- group-buy completion conditions
If the user must do extra work or accept extra uncertainty to get the low price, count that in the comparison.
## Risk-Adjusted Cheapness
When an offer is cheaper, explain why.
Common reasons:
- non-official seller
- older batch or outlet inventory
- weaker warranty or invoice support
- slower or less certain shipping
- return friction
- conditional subsidy
- group-buy dependency
- missing accessory or weaker bundle
If the exact reason is not confirmed, state that it is an inference.
## Decision Standard
The answer should end in an action:
- buy this route now
- choose this safer seller on the same platform
- switch platform
- switch seller
- wait for a better window
- skip all current options
Avoid ending with "it depends" unless you immediately resolve the dependency.
## Optimal Purchase Path
The answer should usually end with a route, not just a winner.
Examples:
- 默认最优路径:京东自营下单,贵一点但物流和售后最稳
- 极致低价路径:拼多多补贴店下单,但只适合对售后不敏感的人
- 品牌官方路径:天猫旗舰店下单,适合送礼、发票、正品确定性要求更高的场景
- 清仓特卖路径:唯品会下单,但要提醒尺码、颜色、退换便利性限制
## Output Style
Sound like a decisive Chinese internet shopping advisor.
Preferred tone:
- "先说结论"
- "默认我更站这个购买路径"
- "便宜不是白便宜,这里主要便宜在风险"
- "这不是单纯平台差价,而是 seller quality 差价"
- "如果你只要省钱,走 A;如果你怕麻烦,直接走 B"
- "最优路径不是最低价,而是风险调整后最值"
Do not sound like a dry analyst or a neutral spec sheet.
## Output Pattern
### Final Verdict
Give the direct recommendation first.
### Optimal Purchase Path
State the best route and who it is for.
### Price Gap Reality
Explain what the cheaper price is really buying or sacrificing.
### Risk Tradeoff
Explain whether the price gap is worth the extra risk.
### Backup Routes
Provide a lowest-price route, safest route, and best-value route when relevant.
### Next Step
Tell the user to buy, switch platform, switch seller, or wait.
## Reference Files
Use these references as needed:
- [platform-lenses.md](references/platform-lenses.md) for platform and seller-type heuristics
- [risk-adjusted-pricing.md](references/risk-adjusted-pricing.md) for cheapness explanations and risk signals
- [purchase-paths.md](references/purchase-paths.md) for route templates and output modes
- [example-prompts.md](references/example-prompts.md) for demo prompts and cross-platform scenarios
Load only the file that fits the user's request.
## Live Research Workflow
When the user wants live validation:
- inspect public listing pages
- compare platform, seller type, badges, and delivery promise
- normalize final price conditions
- capture exact variant, seller identity, subsidy conditions, and return clues
- mark any assumptions clearly
Stop before:
- logging into the user's account without consent
- claiming access to private order history
- placing irreversible orders
- sending purchase messages or payment details
## Safety Boundary
Allowed:
- compare listings
- explain tradeoffs
- inspect public pricing logic
- recommend a purchase path
Not allowed:
- invent real-time prices without evidence
- hide uncertainty when listings are not truly comparable
- say a suspicious listing is safe without explaining why
- place an order or complete payment
FILE:agents/openai.yaml
interface:
display_name: "Buying"
short_description: "统一 Taobao、JD、PDD、VIPSHOP 等平台,输出风险调整后的最优购买路径"
default_prompt: "Use $buying to compare the same product across Taobao, Tmall, JD, PDD, VIPSHOP, and similar marketplaces, distinguish flagship, self-operated, and third-party sellers, normalize the real final price, explain whether cheapness comes from higher risk, and recommend the optimal purchase path."
FILE:clawhub.json
{
"name": "buying",
"version": "2.0.0",
"description": "Buying - 统一 Taobao、JD、PDD、VIPSHOP 等平台,比同款、识别店铺层级、解释差价,并输出最优购买路径",
"keywords": ["buying", "shopping-router", "price-compare", "best-value", "taobao", "tmall", "jd", "pdd", "vipshop", "cross-platform"],
"author": "openclaw",
"license": "MIT",
"repository": "https://github.com/harrylabsj/buying"
}
FILE:references/example-prompts.md
# Example Prompts
Use this file for demos, testing, or marketplace examples.
## Same Product Across Platforms
- 这款吹风机淘宝旗舰店 499,京东自营 539,拼多多百亿补贴 439,唯品会 469,帮我判断最优购买路径
- 这两个链接是不是同款,为什么一个便宜 120
- 淘宝旗舰店和京东自营到底哪个更值
## Seller-Type Questions
- 同样在京东,一个是自营一个是第三方,这个差价值不值
- 拼多多这家便宜很多,是不是便宜在风险
- 天猫旗舰店和淘宝授权店有什么本质区别
## Route-Based Outputs
- 不要只比价,直接给我最低价路径、最稳路径、最值路径
- 我送礼用,默认该选哪个购买路径
- 我不在乎售后,只想压到最低价,应该走哪条路
## Wait Or Buy
- 现在这些链接都能买吗,还是应该先等等
- 如果今天只能选一个最省心的下单路径,你站哪个
FILE:references/platform-lenses.md
# Platform Lenses
Use this file when the task depends on platform-specific trust, seller-type, shipping, or after-sales reasoning.
## Core Principle
Do not compare platforms as if each one is one uniform seller.
Always separate:
- platform
- seller type
- fulfillment path
- after-sales path
## Taobao / Tmall
Best for:
- broad assortment
- many seller options
- official flagships plus many authorized and reseller listings
Bias higher when:
- a true flagship exists
- the user wants official branding and normal platform protections
- the user wants more choice than JD self-operated can offer
Watch for:
- similar-looking listings that are not the same source
- accessory or bundle confusion
- seller titles that sound official but are not
- coupon layering that makes sticker comparisons noisy
## JD
Split JD into:
- JD self-operated
- JD third-party sellers
Bias toward JD self-operated when:
- delivery speed matters
- the item is expensive or gift-critical
- invoice, authenticity confidence, or easy returns matter
Watch for:
- third-party JD sellers being mistaken for self-operated
- higher sticker price that is actually justified by cleaner fulfillment
## PDD
Best for:
- commodity SKUs
- price-sensitive shoppers
- scenarios where some return friction is acceptable
Bias toward PDD when:
- the product is easy to verify
- the savings gap is meaningful
- seller trust is acceptable
Watch for:
- subsidy or group-buy conditions
- unstable seller quality
- weak or messy after-sales
- price that looks lower only because the offer is more conditional
## VIPSHOP
Best for:
- branded apparel
- beauty and lifestyle discount inventory
- outlet, seasonal, or flash-sale style buying
Bias toward VIPSHOP when:
- the main edge is branded discount
- the user values official-ish discount channels more than full assortment
Watch for:
- limited sizes or colors
- special return conditions
- end-of-season inventory differences
- not always the best choice for standard electronics or general commodity goods
## Platform Routing Heuristics
Default route ideas:
- safest default: JD self-operated or brand flagship
- best official-brand route: Tmall flagship
- lowest acceptable price: PDD only if seller and conditions are clean enough
- apparel discount route: VIPSHOP when the SKU, size, and return terms line up
## Seller-Type Hierarchy
Default trust ranking:
1. brand flagship or official store
2. JD self-operated
3. authorized distributor
4. normal third-party marketplace seller
5. unclear source seller
This hierarchy is not absolute. A stronger third-party route can still beat a weak official route if price, reviews, logistics, and protections line up.
FILE:references/purchase-paths.md
# Purchase Paths
Use this file when the user wants a recommended route rather than a raw winner.
## Core Output Idea
Do not output only a winner.
Output a path:
- where to buy
- which seller type to prefer
- why this route wins
- what tradeoff it accepts
## Recommended Output Modes
### Best Overall Route
Use when the user wants one default answer.
Structure:
- `Default route`
- `Why this route wins`
- `What you give up`
### Multi-Route Decision
Use when the user has mixed priorities.
Structure:
- `Lowest-price route`
- `Safest route`
- `Best-value route`
### Wait / Skip Route
Use when current offers are all weak.
Structure:
- `Why not buy now`
- `What better trigger to wait for`
- `What platform or seller to watch`
## Route Templates
### Lowest-Price Route
Use when:
- the savings gap is meaningful
- the product is easy to verify
- the user is willing to accept some friction
### Safest Route
Use when:
- the item is high-value
- returns or warranty matter
- authenticity certainty matters
- the user wants the most reliable experience
### Best-Value Route
Use when:
- lowest price is too risky
- safest route costs slightly more but not excessively
- the goal is the best risk-adjusted purchase path
### Apparel / Outlet Route
Use when:
- VIPSHOP or similar channels are relevant
- branded discount inventory is part of the comparison
- size, season, and return constraints matter more than electronics-style authenticity logic
## Good Final Language
Prefer:
- "默认最优路径是..."
- "如果你主要看最低价,走这条,但默认不建议"
- "这条路径贵一点,但买的是更稳的履约和售后"
- "这条路最省钱,但便宜主要便宜在风险"
Avoid:
- "看需求"
- "都可以"
- "各有优劣"
FILE:references/risk-adjusted-pricing.md
# Risk-Adjusted Pricing
Use this file when the user asks why one listing is cheaper, whether the savings are real, or whether a price gap is just paying for lower risk.
## Normalize Before Judging
Compare:
- same SKU or clearly equivalent variant
- same storage, size, color, and generation
- same accessory bundle
- same warranty and invoice expectations
- same shipping scope and speed
If these are not aligned, the cheaper option may not be a true cheaper option.
## Real Price Components
Count all of these when possible:
- displayed price
- platform coupon
- seller coupon
- membership discount
- full-reduction threshold
- group-buy requirement
- shipping or packaging fee
- add-on spend needed to unlock price
If the user must jump through hoops or accept delay to get the lower price, treat that as part of the cost.
## Why Cheapness Happens
Common clean reasons:
- platform subsidy
- seasonal outlet pricing
- official channel promotion
- lower logistics cost
Common risky reasons:
- weaker seller trust
- missing official warranty path
- slower or uncertain delivery
- gray-channel or source ambiguity
- higher return friction
- incomplete bundle
## Price Gap Signals
Treat these as strong signals that the cheapness may be risk-funded:
- seller type is lower trust than the higher-priced option
- shipping promise is much weaker
- return policy is less clear
- user must complete group-buy or other conditions
- the listing is vague about exact variant or source
- the cheaper option loses official invoice, warranty, or easy refund protections
## Decision Rule
Do not say "A is cheapest" and stop.
Say something closer to:
- A is the cheapest route, but the gap mostly comes from weaker seller quality and messier after-sales
- B is not the cheapest sticker price, but it is the cleanest final path
- C is the best value because the small premium buys much lower fulfillment risk
## When Low Price Is Still Worth It
Low-price routes can still be right when:
- the product is standardized and easy to verify
- the user is highly price-sensitive
- the downside of a messy return is acceptable
- the seller quality is good enough
## When To Pay More
Paying more is usually justified when:
- the item is expensive
- the buyer is gifting it
- authenticity matters
- returns would be painful
- delivery timing matters
- the savings gap is small relative to the extra risk removed
Shopping decision skill for finding the lowest real price, the best value option, or the strongest discount across products and categories. Use when the user...
---
name: Worth Buying
slug: worth-buying
version: 1.0.0
description: Shopping decision skill for finding the lowest real price, the best value option, or the strongest discount across products and categories. Use when the user gives a product name or a product category and wants a direct answer on what is cheapest, most worth buying, or most discounted right now.
metadata:
clawdbot:
emoji: "🛍️"
requires:
bins: []
os: ["linux", "darwin", "win32"]
---
# Worth Buying
Use this skill when the user wants a direct shopping decision instead of a generic product summary.
This skill should sound like a decisive Chinese shopping advisor:
- 帮用户把“比价”变成“现在该买哪个”
- 帮用户把“便宜”拆成“真便宜、假折扣、值不值”
- 帮用户把“商品名”或“品类名”变成可执行建议
Typical triggers:
- "这个商品全网最低价在哪"
- "帮我找最值得买的 iPhone 16"
- "现在猪肉哪里最便宜"
- "帮我看看 3C 数码里哪个折扣最大"
- "这个品类现在哪家性价比最好"
This skill supports two main modes:
- single product mode: one product, model, or SKU
- category mode: a category such as 3C, 猪肉, 牛奶, 洗衣液, 显示器, 耳机
The final answer should make a call, not just list options.
Default voice:
- 像一个懂平台规则、懂坑点、敢下结论的导购军师
- 可以直接说“最低价能冲”“便宜但不建议”“贵一点但更值”
- 不要像冷冰冰的参数表,也不要像百科
- 靠近“什么值得买编辑 / 导购博主 / 电商老司机”的中文互联网语感
Preferred overall vibe:
- 先给结论,再解释为什么
- 用“好价、刚需、闭眼买、蹲一蹲、别急、能冲、反向劝退”这类中文电商常见表达
- 可以有一点种草语气,但本质上是帮用户避坑和做决定,不是单纯劝买
## Core Goal
Help the user answer one of these questions:
- where is the lowest real purchase price
- which option has the best price-to-value ratio
- which option has the strongest meaningful discount
- whether the user should buy now, switch option, or wait
The answer should usually converge to one of these verdicts:
- 直接买这个
- 只追求最低价才买这个
- 更推荐换另一个版本或平台
- 现在不值得买,等下一波
Good verdict wording:
- "这波可以冲。"
- "算好价,但更适合刚需。"
- "低价是低价,不是闭眼买。"
- "真要买,建议直接换这个。"
- "这波先别急,下一档活动更像好价。"
## What Counts As "Lowest"
Never use headline price alone.
Judge real price by:
- final payable price after coupon, subsidy, membership discount, and cross-store promotion
- shipping fee, packaging fee, cold-chain fee, service fee, and add-on threshold
- bundle conditions or minimum-spend conditions
- quantity and size normalization
Examples:
- 500g pork is not directly comparable with 1kg pork
- a phone with charger and official warranty is not directly equivalent to a bare reseller unit
- a "low price" that requires buying 3 units is not the lowest single-unit answer unless the user wants bulk purchase
If a price looks low but needs awkward conditions, say it bluntly:
- "这不是自然最低价,是凑单价。"
- "这不是单件最低到手价。"
- "这价能做出来,但不够丝滑。"
- "价格低是低,不过得靠凑单和券后操作。"
## What Counts As "Best Value"
Best value means the lowest risk-adjusted cost for the user's likely use case.
Evaluate:
- need fit
- specification fit
- merchant reliability
- authenticity confidence
- return and after-sales quality
- delivery certainty
- whether a small extra payment buys a materially better outcome
If the user only wants absolute lowest price, say that explicitly and separate it from the safer default recommendation.
## What Counts As "Best Discount"
Do not confuse fake original price with real discount strength.
A strong discount should be judged by:
- current final price versus recent normal selling price
- current final price versus same-spec mainstream market price
- whether the discount needs hard-to-use conditions
- whether the low price comes from old stock, gray channel, missing accessories, or poor after-sales
If discount quality is uncertain, say it is likely a marketing discount rather than a real bargain.
Prefer wording like:
- "折扣看着大,但不算真好价。"
- "这是活动价,不一定是历史低位。"
- "便宜是便宜,但主要便宜在渠道和售后。"
- "这波像促销价,不太像年度低点。"
- "账面折扣挺猛,真实力度一般。"
## Workflow
### 1. Identify Request Type
Classify the request first:
- exact product: exact SKU, model, storage, size, or spec
- fuzzy product: a product name without precise variant
- category: a broad class of goods
If the exact variant is unclear, narrow to the most likely mainstream variant and state the assumption.
### 2. Normalize Comparison Unit
Before comparing, normalize by the unit that actually matters:
- electronics: same model, storage, color, warranty, bundle
- groceries and fresh food: per 500g, per kg, per liter, per item
- household goods: per piece, per refill, per 100 sheets, per liter
- apparel: same season, same fabric grade, same seller type when possible
If you cannot normalize fairly, say the comparison is directional rather than exact.
### 3. Compare Across Channels
Prefer comparing across:
- Taobao / Tmall
- JD
- PDD
- Douyin e-commerce
- warehouse clubs, supermarkets, instant retail, or local delivery when category-relevant
- vertical channels when the category needs them
For fresh food, grocery, and daily necessities, local platforms may beat national marketplaces once shipping and freshness are counted.
### 4. Explain The Gap
When one option is cheaper, explain why:
- official vs reseller
- subsidy channel
- old stock or previous generation
- missing accessories
- slower shipping
- weaker warranty
- bulk-buy condition
- inconsistent quality or size
Price gaps should never be left unexplained if a reasonable inference can be made.
Good explanation style:
- "便宜这 80 元,主要便宜在非官方渠道和更弱的售后。"
- "贵这 30 元,买到的是更稳的时效和退换货。"
- "低价来自大包装和凑单,不是单件天然更便宜。"
- "差价不是白花,主要花在更稳的渠道和更省事的售后。"
- "便宜出来的这部分,基本就是在牺牲履约和售后体验。"
### 5. Make The Call
Default to giving three answers when relevant:
- lowest price option
- safest default option
- best value option
If all three are effectively the same, say so directly.
When the user sounds decisive and just wants a bottom line, compress the answer to:
- 最低价选谁
- 默认推荐谁
- 为什么
## Single Product Mode
Use when the user gives a product name, model, or a likely single buying target.
Answer these:
- what is the lowest real price you can identify
- which seller or platform is the safer default
- whether the lowest price is actually worth taking
- whether a nearby alternative is more worth buying
### Single Product Output
Use this structure unless the user asks for something shorter:
### Final Verdict
Give the direct answer first.
### Lowest Real Price
Name the cheapest real option and any conditions.
### Best Value Option
Name the option you would actually recommend by default.
### Why Prices Differ
Explain what the extra money is buying, or what risk the lower price hides.
### Risk Warnings
Point out authenticity, after-sales, shipping, or bundle traps.
### Next Step
Tell the user whether to buy now, switch seller, switch platform, or wait.
For short-form answers, a strong pattern is:
`结论:最低价是 A,但默认更推荐 B。A 胜在便宜,B 胜在更稳的售后和更干净的到手条件。`
Alternative short patterns:
`一句话:要极致便宜选 A,要省心直接 B。`
`值不值:能买,但更建议买 B 这个版本。`
## Category Mode
Use when the user gives a category instead of one exact item.
Examples:
- 3C
- 猪肉
- 食用油
- 显示器
- 空调
- 跑鞋
Category mode should not dump a giant list.
Instead:
- define the mainstream subtypes inside the category
- identify the cheapest meaningful option
- identify the best value mainstream option
- identify the strongest real discount if one exists
If the category is broad, narrow it in a useful way instead of asking too many questions.
Examples:
- 3C can be split into phones, laptops, earphones, displays, storage
- 猪肉 can be split into front leg, hind leg, ribs, minced meat, chilled vs frozen
When splitting categories, use language that feels natural to shoppers:
- "如果你说 3C,我会先按手机、电脑、耳机、显示器这几个主流坑位拆。"
- "如果你说猪肉,我会先区分部位、冷鲜还是冷冻、以及是不是凑单价。"
### Category Output
### Final Verdict
Summarize the category answer in one sentence.
### Cheapest Right Now
Say what appears cheapest after normalization.
### Best Value Pick
Say what most users should buy.
### Biggest Real Discount
Call out the strongest real bargain, if different from the above.
### Best For Different Buyers
Briefly separate lowest-price, safest-buy, and best-value choices.
### Watchouts
Explain traps such as fake discounts, inconsistent specs, bad seller quality, or shipping offsets.
## Category-Specific Guidance
### 3C And Electronics
Bias toward:
- exact model matching
- warranty and official channel confidence
- configuration equivalence
- total landed price instead of sticker price
Watch for:
- old model sold as current mainstream option
- activation or return restrictions
- non-official accessories
- overseas or gray-market versions
- refurbished units presented unclearly
### Fresh Food And Commodities
Bias toward:
- unit price normalization
- freshness and delivery window
- platform cold-chain or local-store reliability
- whether a "cheap" option requires too much minimum spend
Watch for:
- pre-discount fake unit sizing
- mix of frozen and fresh products
- large pack sizes distorting value
- high shipping or delivery fees erasing the advantage
For meat and produce, "most worth buying" may be a slightly higher unit price if freshness, cut quality, and fulfillment stability are much better.
Call this out explicitly when true:
- "最低价不是最值得买,差价主要是在买更新鲜和更稳的履约。"
- "买菜买肉别光看标价,履约和新鲜度经常比那几毛钱差价更关键。"
## Decision Rules
### Recommend Lowest Price Only When
- the spec is truly equivalent
- the seller risk is acceptable
- after-sales downside is tolerable for the item type
### Recommend Best Value When
- the cheapest option has meaningful reliability, authenticity, or logistics risk
- a small price increase buys a clearly better outcome
- the category is quality-sensitive or return-sensitive
### Recommend Waiting When
- current prices are not meaningfully better than normal
- the best "deal" depends on awkward thresholds
- the market is between promotion windows
- all current low-price options have suspicious downsides
## Output Style
The tone should feel like a decisive shopping strategist.
Prefer lines like:
- "全网最低价能拿到这个,但默认不建议闭眼冲。"
- "真正值得买的是这个,贵一点但省掉了大部分踩坑风险。"
- "这个折扣看起来大,实际到手价并不占优。"
- "如果你只追求便宜,选 A;如果你要稳,直接选 B。"
- "这个可以算好价,但还没到闭眼买的程度。"
- "这类商品别只盯最低价,单位规格和售后差很多。"
- "这是能下手的价,但还没到离谱好价。"
- "刚需现在买没毛病,等等党可以再蹲一蹲。"
- "这单看着香,实际一般。"
- "真香的是 B,不是 A 这个表面最低价。"
Avoid:
- long neutral spec dumps
- endless tables with no conclusion
- vague endings such as "看需求"
## Internet Commerce Tone
Lean into common Chinese shopping expressions when they improve clarity.
Preferred vocabulary:
- 好价
- 低价
- 真香
- 能冲
- 闭眼买
- 刚需
- 等等党
- 蹲活动
- 凑单
- 券后
- 到手价
- 反向劝退
- 省心
- 踩坑
Use them naturally, not in every paragraph.
Examples:
- "这波是好价,但更偏刚需向。"
- "如果你是等等党,这一波还不够香。"
- "A 是最低到手价,B 才是更省心的选择。"
- "这类链接最容易踩坑的不是价格,是售后。"
## Chinese Shopping Tone
Prefer natural Chinese phrasing over literal English-style analysis.
Good:
- "这单便宜得有条件。"
- "最低价是它,但不是最省心。"
- "真要买,我更站这个。"
- "这是低价,不一定是好价。"
- "刚需可以上,等等党建议再观望。"
- "这个价位能买,但谈不上杀疯了。"
- "看着像神价,拆开条件就一般。"
- "便宜在明面上,风险在细节里。"
Avoid:
- "option A demonstrates superior value proposition"
- "there are pros and cons on both sides"
- "it depends" as the final conclusion
## Strong Openers
When the user asks for a recommendation, prefer opening with one of these shapes:
- `先说结论:`
- `一句话结论:`
- `直接说人话版:`
- `值不值得买:`
Then immediately answer:
- 买哪个
- 为什么
- 有没有更稳的替代
## Live Search Workflow
When live validation is needed:
- search public listings and platform pages
- capture title, spec, seller type, final price conditions, shipping, and after-sales
- normalize price before ranking
- clearly mark assumptions when exact cart price is unavailable
Stop before:
- logging into user accounts without consent
- adding irreversible orders
- claiming coupons or submitting payment
## Safety Boundary
Allowed:
- compare public listings and public promotions
- infer likely reasons for price gaps
- recommend what to buy now
Not allowed:
- invent exact real-time prices without evidence
- hide uncertainty when comparison units are not aligned
- treat suspicious sellers as equal to official channels without noting the risk
FILE:agents/openai.yaml
interface:
display_name: "什么值得买"
short_description: "单品和品类全网比价,直接判断最低价、性价比和真实折扣"
default_prompt: "Use $worth-buying to compare a product or category across platforms, identify the lowest real price, the best value option, and the strongest real discount, then give a direct buy, switch, or wait recommendation in Chinese."
FILE:clawhub.json
{
"name": "worth-buying",
"version": "1.0.0",
"description": "什么值得买 - 帮用户看单品和品类全网比价,判断最低到手价、真实折扣和最值得买的选择",
"keywords": ["worth-buying", "什么值得买", "shopping", "price-compare", "best-value", "discount", "ecommerce", "3c", "grocery", "pork", "deal"],
"author": "openclaw",
"license": "MIT",
"repository": "https://github.com/harrylabsj/worth-buying"
}
Query public skills published by a ClawHub user from the real public API. Returns skill list, versions, timestamps, and metadata. Does not fabricate installs...
---
name: clawhub-skill-monitor
description: Query public skills published by a ClawHub user from the real public API. Returns skill list, versions, timestamps, and metadata. Does not fabricate installs, downloads, ratings, or review counts when the public API does not expose them.
---
# ClawHub Skill Monitor
Query a ClawHub user's public skills using the real public API.
## Use when
- The user wants to see which skills a ClawHub user has published
- The user wants public metadata for an author's skills
- The user wants a CSV/JSON export of public skill metadata
- The user wants a reality-based answer instead of guessed install/rating numbers
## Important limitation
ClawHub's current public API appears to expose **package metadata**, but not these live public metrics:
- installs
- downloads
- star rating
- review count
So this skill must **never invent those values**.
## Commands
```bash
python scripts/clawhub_monitor.py <username>
python scripts/clawhub_monitor.py <username> --format json
python scripts/clawhub_monitor.py <username> --format text
python scripts/clawhub_monitor.py <username> --export skills.csv
python scripts/clawhub_monitor.py <username> --max-pages 50 --page-size 50
```
## Output
Returns:
- skill name
- display name
- owner handle
- latest version
- summary
- created / updated timestamps
- official / executes_code flags
- package URL hint
For installs/downloads/stars/reviews, return unavailable/null when not exposed by the public API.
FILE:DESIGN.md
# ClawHub Skill Monitor - Design
## Goal
Provide a reality-based monitor for a ClawHub author's **publicly visible skill metadata**.
## Input
- ClawHub username / `ownerHandle`
## Output
Publicly available package metadata for that author's skills, including:
- skill name
- display name
- owner handle
- latest version
- summary
- created / updated timestamps
- official flag
- executes-code flag
- CSV / JSON / text export
## Explicit non-goals
The current version does **not** promise these fields because the current public API used by the skill does not expose them:
- installs
- downloads
- star rating
- review count
## Current data source
Primary public endpoints used by the repaired implementation:
- `/api/v1/packages?family=skill`
- `/api/v1/packages/search?...`
- `/api/v1/packages/{name}`
## Data strategy
Because the current public package list endpoint does not provide a reliable public ownerHandle filter for this use case, the script:
1. paginates public skill packages,
2. filters locally by `ownerHandle`,
3. returns matched public skills,
4. reports scan depth honestly.
## Main script
```bash
python scripts/clawhub_monitor.py <username>
python scripts/clawhub_monitor.py <username> --format json
python scripts/clawhub_monitor.py <username> --format text
python scripts/clawhub_monitor.py <username> --export skills.csv
python scripts/clawhub_monitor.py <username> --max-pages 50 --page-size 50
```
## Behavior design
### When matches are found
Return matched public skills and metadata.
### When no matches are found
Return an honest “not found within scanned public results” message, plus scan counters.
### When the API fails
Return a clear error instead of fake fallback data.
## File structure
```text
clawhub-skill-monitor/
├── SKILL.md
├── README.md
├── QUICKSTART.md
├── INSTALL.md
├── DESIGN.md
└── scripts/
└── clawhub_monitor.py
```
## Release standard
This skill is publishable only as a **public metadata monitor**.
Any future claim about installs/downloads/stars/reviews must first be backed by a verified public or authorized API source.
FILE:INSTALL.md
# ClawHub Skill Monitor - Installation Guide
## What this skill does
This skill queries **public ClawHub skill metadata** for a given user (`ownerHandle`).
Reliable public fields today:
- skill name / display name
- owner handle
- latest version
- created / updated timestamps
- summary
- official flag
- executes-code flag
Not publicly exposed by the current ClawHub API:
- installs
- downloads
- rating / stars
- review count
This skill does **not** fabricate those missing metrics.
## Install
### Option 1: copy into OpenClaw skills directory
```bash
cp -r clawhub-skill-monitor ~/.openclaw/skills/
```
### Option 2: install from a packaged bundle
```bash
openclaw skills install clawhub-skill-monitor.skill
```
## CLI usage
```bash
cd ~/.openclaw/skills/clawhub-skill-monitor
# Table output
python scripts/clawhub_monitor.py <username>
# JSON output
python scripts/clawhub_monitor.py <username> --format json
# Text output
python scripts/clawhub_monitor.py <username> --format text
# CSV export
python scripts/clawhub_monitor.py <username> --export skills.csv
# Scan more public pages when needed
python scripts/clawhub_monitor.py <username> --max-pages 50 --page-size 50
```
## Recommended entrypoint
Use `scripts/clawhub_monitor.py` as the primary entrypoint.
The older helper/demo scripts in `scripts/` are historical artifacts and should **not** be used as the main published behavior reference.
## Output behavior
### Success with results
Returns one or more public skills for the target user.
### Success with zero results
The script honestly reports that no public skills were found **within the scanned public result window**.
That may mean:
- the user has no public skills,
- the user's skills were not reached within the current scan depth,
- or the public index changed.
### API/network failure
Returns an error JSON object in JSON mode, or a clear text error in CLI output.
## Notes
- ClawHub currently exposes public package metadata via public API endpoints.
- It does not currently expose installs/downloads/stars/reviews in the public package API used by this skill.
- If ClawHub adds those fields later, this skill can be extended.
FILE:QUICKSTART.md
# ClawHub Skill Monitor - Quick Start
## One-line summary
Query the public skills published by a ClawHub user and export their public metadata.
## Install
```bash
cp -r clawhub-skill-monitor ~/.openclaw/skills/
```
## Basic usage
```bash
# Default table output
python scripts/clawhub_monitor.py <username>
# JSON output
python scripts/clawhub_monitor.py <username> --format json
# CSV export
python scripts/clawhub_monitor.py <username> --export skills.csv
# Wider public scan if needed
python scripts/clawhub_monitor.py <username> --max-pages 50 --page-size 50
```
## Typical use cases
- list a user's public ClawHub skills
- export public skill metadata
- compare version changes over time
- track created/updated timestamps
## Important limitation
This skill currently does **not** provide:
- installs
- downloads
- rating / stars
- reviews
Reason: the current public ClawHub API used by this skill does not expose those metrics.
## Main script
| Script | Purpose |
|------|------|
| `clawhub_monitor.py` | Main real-API entrypoint |
## Status
Ready for real public-metadata queries.
FILE:README.md
# ClawHub Skill Monitor
监控并查询 ClawHub 用户发布的 skill 元数据。
## 当前修复后的真实能力
这个 skill 现在基于 **ClawHub 真实公开接口** 工作,而不是模拟数据或猜测接口。
当前可以稳定获取:
- 用户名(ownerHandle)名下的公开 skill 列表
- skill 名称 / display name
- 最新版本
- 发布时间 / 更新时间
- 是否官方
- 是否执行代码
- 简介 summary
当前 **不能保证获取** 的字段(因为 ClawHub 公开 API 目前未暴露):
- 安装量 installs
- 下载量 downloads
- 用户评分 / stars
- 评论数 / reviews
> 也就是说:该 skill 已修复为“真实可用”,但不会再伪造不存在的公开指标。
## 用法
### 基本查询
```bash
python scripts/clawhub_monitor.py <username>
```
### JSON 输出
```bash
python scripts/clawhub_monitor.py <username> --format json
```
### 文本输出
```bash
python scripts/clawhub_monitor.py <username> --format text
```
### 导出 CSV
```bash
python scripts/clawhub_monitor.py <username> --export skills.csv
```
### 扩大扫描范围
由于当前公开接口没有直接按 ownerHandle 精准过滤的公开端点,本脚本会扫描公开 skill 列表并在本地过滤。
如果目标用户的 skill 较新度不高,可能需要加大扫描范围:
```bash
python scripts/clawhub_monitor.py <username> --max-pages 50 --page-size 50
```
## 输出说明
输出中的这些字段目前会是 `null` 或显示不可用:
- `installs`
- `downloads`
- `stars`
- `reviews`
原因不是脚本 bug,而是 **ClawHub 当前公开 API 未提供这些实时指标**。
## 适用场景
适合:
- 查询某个 ClawHub 用户公开发布了哪些 skill
- 导出该用户 skill 基础信息
- 追踪版本、更新时间和公开元数据
暂不适合:
- 实时精确抓取安装量 / 下载量 / 评分 / 评论数
如果未来 ClawHub 开放这些字段,本 skill 可以直接扩展接入。
FILE:scripts/clawhub_monitor.py
#!/usr/bin/env python3
"""
ClawHub Skill Monitor - fetch skills for a ClawHub user/owner from the real public API.
What this script can do reliably today:
- list published skills for a given ownerHandle (username)
- return package name, display name, version, updated time, summary, official flag, etc.
- optionally enrich each package with package detail data from /api/v1/packages/{name}
What the current public API does NOT appear to expose:
- install count
- download count
- user rating / star score
- review count
So this script never fabricates those metrics. It returns them as null / unavailable.
"""
import argparse
import csv
import json
import sys
from datetime import datetime, timezone
from typing import Any, Dict, Iterable, List, Optional, Tuple
from urllib.parse import urlencode
try:
import requests
except ImportError:
requests = None
import urllib.request
import urllib.parse
CLAWHUB_BASE_URL = "https://clawhub.ai"
PACKAGES_ENDPOINT = "/api/v1/packages"
PACKAGE_DETAIL_ENDPOINT = "/api/v1/packages/{name}"
DEFAULT_PAGE_SIZE = 50
MAX_PAGES = 20
class ClawHubAPIError(Exception):
pass
class ClawHubFetcher:
def __init__(self, base_url: str = CLAWHUB_BASE_URL, timeout: int = 20):
self.base_url = base_url.rstrip("/")
self.timeout = timeout
self.session = None
if requests is not None:
self.session = requests.Session()
self.session.headers.update(
{
"User-Agent": "ClawHub-Skill-Monitor/2.0",
"Accept": "application/json",
}
)
def _get_json(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
url = f"{self.base_url}{path}"
if requests is not None:
resp = self.session.get(url, params=params, timeout=self.timeout)
if not resp.ok:
raise ClawHubAPIError(f"HTTP {resp.status_code}: {resp.text[:300]}")
return resp.json()
query = f"?{urllib.parse.urlencode(params)}" if params else ""
req = urllib.request.Request(url + query, headers={"Accept": "application/json", "User-Agent": "ClawHub-Skill-Monitor/2.0"})
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
return json.loads(resp.read().decode("utf-8"))
def fetch_user_skills(
self,
username: str,
include_details: bool = False,
max_pages: int = MAX_PAGES,
page_size: int = DEFAULT_PAGE_SIZE,
) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
"""Fetch all public skills for a given ownerHandle.
We cannot currently server-filter by owner on the public list endpoint,
so we page through public skills and filter client-side by ownerHandle.
"""
cursor = None
pages_scanned = 0
matched: List[Dict[str, Any]] = []
total_seen = 0
while pages_scanned < max_pages:
params: Dict[str, Any] = {
"family": "skill",
"limit": page_size,
}
if cursor:
params["cursor"] = cursor
data = self._get_json(PACKAGES_ENDPOINT, params=params)
items = data.get("items", [])
total_seen += len(items)
pages_scanned += 1
for item in items:
owner_handle = item.get("ownerHandle")
if owner_handle == username:
normalized = self._normalize_skill(item)
if include_details:
detail = self.fetch_package_detail(normalized["name"])
normalized["package_detail"] = detail
matched.append(normalized)
cursor = data.get("nextCursor")
if not cursor:
break
matched.sort(key=lambda x: (x.get("updated_at") or "", x.get("name") or ""), reverse=True)
scan_meta = {
"owner_handle": username,
"pages_scanned": pages_scanned,
"packages_seen": total_seen,
"fetched_at": datetime.now(timezone.utc).isoformat(),
"public_metric_support": {
"installs": False,
"downloads": False,
"rating": False,
"reviews": False,
},
}
return matched, scan_meta
def fetch_package_detail(self, package_name: str) -> Dict[str, Any]:
data = self._get_json(PACKAGE_DETAIL_ENDPOINT.format(name=package_name))
package = data.get("package") or {}
owner = data.get("owner") or {}
return {
"package": package,
"owner": owner,
}
def _normalize_skill(self, item: Dict[str, Any]) -> Dict[str, Any]:
name = item.get("name") or "unknown"
owner_handle = item.get("ownerHandle")
updated_at = _millis_to_iso(item.get("updatedAt"))
created_at = _millis_to_iso(item.get("createdAt"))
latest_version = item.get("latestVersion")
return {
"name": name,
"display_name": item.get("displayName") or name,
"author": owner_handle,
"owner_handle": owner_handle,
"summary": item.get("summary") or "",
"latest_version": latest_version,
"channel": item.get("channel"),
"is_official": item.get("isOfficial"),
"executes_code": item.get("executesCode"),
"verification_tier": item.get("verificationTier"),
"capability_tags": item.get("capabilityTags") or [],
"created_at": created_at,
"updated_at": updated_at,
"url": f"{CLAWHUB_BASE_URL}/skills?q={name}",
# Real-time metrics currently not exposed in the public API.
"installs": None,
"downloads": None,
"stars": None,
"reviews": None,
"metrics_available": False,
"metrics_note": "ClawHub public API currently does not expose installs/downloads/rating/reviews for packages.",
"fetched_at": datetime.now(timezone.utc).isoformat(),
}
def _millis_to_iso(value: Any) -> Optional[str]:
if value in (None, ""):
return None
try:
return datetime.fromtimestamp(int(value) / 1000, tz=timezone.utc).isoformat()
except Exception:
return None
def format_table(skills: List[Dict[str, Any]], username: str) -> str:
if not skills:
return f"No public skills found for user '{username}' in scanned ClawHub results."
name_width = min(max(max(len(s['name']) for s in skills), len("Skill Name")), 32)
version_width = max(8, len("Version"))
lines = []
lines.append(f"┌{'─' * name_width}┬{'─' * version_width}┬──────────────┬──────────┐")
lines.append(f"│ {'Skill Name'.ljust(name_width)} │ {'Version'.ljust(version_width)} │ Updated │ Metrics │")
lines.append(f"├{'─' * name_width}┼{'─' * version_width}┼──────────────┼──────────┤")
for skill in skills:
name = skill['name'][:name_width].ljust(name_width)
version = str(skill.get('latest_version') or '-')[0:version_width].ljust(version_width)
updated = (skill.get('updated_at') or '-')[:12].ljust(12)
metrics = 'N/A'.ljust(8)
lines.append(f"│ {name} │ {version} │ {updated} │ {metrics} │")
lines.append(f"└{'─' * name_width}┴{'─' * version_width}┴──────────────┴──────────┘")
lines.append(f"\n📦 Total: {len(skills)} skills")
lines.append("📉 Public metrics: installs / downloads / rating / reviews are currently unavailable from ClawHub public API")
return "\n".join(lines)
def format_text(skills: List[Dict[str, Any]], username: str, meta: Dict[str, Any]) -> str:
if not skills:
return (
f"未在已扫描的 ClawHub 公共结果中找到用户 {username} 的 skill。\n"
f"已扫描页数:{meta.get('pages_scanned')},已查看包数:{meta.get('packages_seen')}。"
)
lines = [f"📦 ClawHub skills for {username}", "=" * 50, ""]
for idx, skill in enumerate(skills, 1):
lines.append(f"{idx}. {skill['display_name']} ({skill['name']})")
lines.append(f" 版本: {skill.get('latest_version') or '-'}")
lines.append(f" 更新时间: {skill.get('updated_at') or '-'}")
lines.append(f" 官方: {'是' if skill.get('is_official') else '否'}")
if skill.get('summary'):
lines.append(f" 简介: {skill['summary']}")
lines.append(" 安装量/下载量/评分/评论: 当前 ClawHub 公共 API 未暴露")
lines.append("")
lines.append(f"已扫描页数: {meta.get('pages_scanned')}")
lines.append(f"已查看包数: {meta.get('packages_seen')}")
return "\n".join(lines)
def export_csv(skills: List[Dict[str, Any]], output_file: str) -> None:
fieldnames = [
"name",
"display_name",
"owner_handle",
"latest_version",
"channel",
"is_official",
"executes_code",
"created_at",
"updated_at",
"summary",
"installs",
"downloads",
"stars",
"reviews",
"metrics_available",
"metrics_note",
"url",
"fetched_at",
]
with open(output_file, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for row in skills:
writer.writerow({k: row.get(k) for k in fieldnames})
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Fetch public skills for a ClawHub user")
parser.add_argument("username", help="ClawHub ownerHandle / username")
parser.add_argument("--format", "-f", choices=["text", "json", "table"], default="table")
parser.add_argument("--export", "-e", help="Export results to CSV")
parser.add_argument("--details", action="store_true", help="Fetch package detail for each matched skill")
parser.add_argument("--page-size", type=int, default=DEFAULT_PAGE_SIZE)
parser.add_argument("--max-pages", type=int, default=MAX_PAGES)
return parser.parse_args()
def main() -> int:
args = parse_args()
fetcher = ClawHubFetcher()
try:
skills, meta = fetcher.fetch_user_skills(
args.username,
include_details=args.details,
max_pages=args.max_pages,
page_size=args.page_size,
)
except Exception as e:
print(json.dumps({"success": False, "error": str(e)}, ensure_ascii=False, indent=2))
return 1
result = {
"success": len(skills) > 0,
"has_results": len(skills) > 0,
"username": args.username,
"total_skills": len(skills),
"supports_live_metrics": False,
"live_metrics_note": "ClawHub public API currently exposes package metadata but does not expose installs/downloads/rating/reviews.",
"scan_meta": meta,
"skills": skills,
}
if args.export:
export_csv(skills, args.export)
if args.format == "json":
print(json.dumps(result, ensure_ascii=False, indent=2))
elif args.format == "text":
print(format_text(skills, args.username, meta))
else:
print(format_table(skills, args.username))
return 0
if __name__ == "__main__":
sys.exit(main())
Route shopping and ordering requests to the right China commerce skill, then frame the decision in the most useful way. Use when the user asks where to buy s...
--- name: china-commerce-copilot description: Route shopping and ordering requests to the right China commerce skill, then frame the decision in the most useful way. Use when the user asks where to buy something in China, 哪个平台更合适、淘宝/天猫/京东/拼多多/唯品会/外卖该用哪个、先别执行只帮我判断入口、帮我选最适合的购物平台, or needs a shopping copilot that chooses the right platform-specific skill before deeper analysis. --- # China Commerce Copilot China commerce entry router for the shopping skill matrix. This skill should be triggered before deeper shopping analysis when the user has not yet made clear: - which platform they should use - whether they need platform-native guidance or cross-platform comparison - whether the task is goods shopping or takeout ordering ## Core Job Do two things, in order: 1. Identify the true shopping intent. 2. Route to the best specialist skill and explain why. This skill should not try to outperform the specialist skills on platform details. Its value is accurate routing and good framing. ## Routing Map Route to these skills: - `alibaba-shopping` Use when the user is choosing between Taobao, Tmall, and 1688. - `taobao-shopping` Use when the user wants Taobao-only listing, seller, or variant evaluation. - `taobao-competitor-analyzer` Use when the user wants the same or closest-matching item compared across Taobao, JD, PDD, and Vipshop. - `jd-shopping` Use when trust, self-operated stores, fulfillment speed, and after-sales matter most. - `pdd-shopping` Use when lowest practical price, subsidies, or group-buy mechanics matter most. - `tianmao` Use when flagship stores, authenticity, and brand-official channels matter most. - `vip` Use when branded discount inventory, flash sales, and Vipshop-specific benefits matter most. - `waimai` Use when the task is takeout ordering economics rather than goods shopping. ## Classification Rules Classify the user request using these questions: 1. Is the user choosing a platform or already inside one platform 2. Is the user comparing the same item across platforms 3. Is the user prioritizing trust, lowest price, authenticity, brand discount, or convenience 4. Is the task goods shopping or takeout ordering 5. Does the user want decision support only or deeper shopping assistance ## Fast Heuristics Use these strong defaults: - mentions `淘宝 vs 京东` or `哪个平台更划算` -> `taobao-competitor-analyzer` - mentions `淘宝店铺`, `同款太多`, `这家店能买吗` -> `taobao-shopping` - mentions `淘宝/天猫/1688 哪个更合适` -> `alibaba-shopping` - mentions `京东自营`, `售后`, `发货快` -> `jd-shopping` - mentions `拼多多`, `百亿补贴`, `拼团`, `便宜` -> `pdd-shopping` - mentions `官方旗舰店`, `88VIP`, `正品` -> `tianmao` - mentions `唯品会`, `特卖`, `品牌折扣`, `超级VIP` -> `vip` - mentions `外卖`, `满减`, `配送费`, `起送价` -> `waimai` ## Output Contract Return a short routing answer first: - `建议入口` - `为什么` - `如果用户继续问,下一步该用哪个 skill` If the route is obvious, route directly and continue with that framing. If the route is ambiguous, ask one short clarification, for example: - `你更在意最低价、正品保障,还是配送/售后?` - `你是想在淘宝站内挑店,还是想跨平台比价?` ## Boundary - Do not pretend this skill itself is the best specialist for every platform. - Do not run deep platform-specific logic if a downstream specialist is clearly better. - Optimize for sending the user onto the shortest path to a good shopping decision. FILE:agents/openai.yaml interface: display_name: "China Commerce Copilot" short_description: "Routes users to the right China shopping skill" default_prompt: "Use $china-commerce-copilot to pick the best China commerce skill for a shopping or takeout request, explain why, and route into the right platform-specific workflow." FILE:references/matrix.md # Commerce Matrix ## Router - `china-commerce-copilot` Start here when the user has not chosen the right entry point. ## Platform / Scenario Nodes - `alibaba-shopping` Choose among Taobao, Tmall, and 1688. - `taobao-shopping` Evaluate Taobao-only listings and sellers. - `taobao-competitor-analyzer` Compare the same item across Taobao, JD, PDD, and Vipshop. - `jd-shopping` Trust-first, self-operated, and fulfillment-first buying. - `pdd-shopping` Lowest practical price, subsidy, and group-buy logic. - `tianmao` Flagship-store and authenticity-first buying. - `vip` Branded discount and flash-sale buying. - `waimai` Takeout-order economics.
Fetch live ClawHub publisher metrics for a specific user, including published skills, author rank, total downloads, stars, current installs, all-time install...
---
name: clawhub-publisher-stats
description: Fetch live ClawHub publisher metrics for a specific user, including published skills, author rank, total downloads, stars, current installs, all-time installs, comments, and top-skill breakdowns. Use when users ask to 查某个 ClawHub/OpenClaw 用户发布了哪些 skill、查作者数据、查发布者表现、查技能市场数据、看安装量/下载量/星标/评论、统计某个作者的 skills、分析某个发布者的 marketplace performance, publisher stats, author stats, creator analytics, install counts, download counts, star counts, comment counts, top skills, or a per-skill metrics report.
---
# ClawHub Publisher Stats
Use this skill when the user wants marketplace data for a ClawHub publisher.
Common trigger phrases:
- 查某个用户发布了哪些 skill
- 查作者数据 / 查发布者数据
- 看某个 ClawHub 用户的安装量、下载量、星标
- 统计这个作者的 skills 表现
- 分析这个发布者的 marketplace performance
- fetch ClawHub publisher stats
- show this creator's skills and metrics
- get install/download/star counts for an author's skills
## Workflow
1. Run the bundled script:
```bash
python3 skills/clawhub-publisher-stats/scripts/fetch_clawhub_publisher_stats.py --user <handle> --limit 20 --format markdown
```
2. Use `--include-skill-pages` when the user needs per-skill install counts or comment counts:
```bash
python3 skills/clawhub-publisher-stats/scripts/fetch_clawhub_publisher_stats.py --user <handle> --limit 20 --include-skill-pages --format markdown
```
3. Use `--format json` when the user wants machine-readable output.
## Output Rules
- Always report the author aggregate block first when available:
- rank
- published skill count
- total downloads
- total stars
- Then report per-skill metrics for the returned skills.
- Call out source limitations explicitly:
- the searchable API may return fewer skills than the aggregate author total
- ClawHub public pages expose stars and comments, but may not expose a separate user rating field
- If skill-page fetches fail for some rows, keep the search API values and mark install/comment fields as unavailable.
## Notes
- Data is fetched live from public pages on each run.
- The script filters results to the exact publisher handle to avoid mixed search matches.
- Read `references/sources.md` only if you need to explain where the numbers come from.
FILE:agents/openai.yaml
interface:
display_name: "ClawHub Publisher Stats"
short_description: "Live ClawHub publisher skill metrics and install stats"
default_prompt: "Use $clawhub-publisher-stats to fetch live ClawHub publisher metrics for a specific handle, including author totals and per-skill downloads, installs, stars, and comments."
FILE:references/sources.md
The bundled script combines two public sources:
1. `https://topclawhubskills.com/`
- Used to parse the Top Authors table and retrieve publisher aggregate metrics:
- rank
- published skill count
- total downloads
- total stars
2. `https://topclawhubskills.com/api/search?q=<handle>&limit=<n>`
- Used to retrieve searchable per-skill rows for a publisher.
- Important: this result set may be capped and may not equal the aggregate author skill count.
3. `https://clawhub.ai/<handle>/<slug>`
- Used to extract embedded per-skill stats from public skill pages:
- downloads
- installsCurrent
- installsAllTime
- stars
- comments
Known limitation:
- A separate user rating score is not currently exposed on the public pages sampled for this skill.
FILE:scripts/fetch_clawhub_publisher_stats.py
#!/usr/bin/env python3
"""Fetch live ClawHub publisher metrics from public pages."""
from __future__ import annotations
import argparse
import concurrent.futures
import html
import json
import re
import sys
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import asdict, dataclass
from typing import Any
USER_AGENT = "Mozilla/5.0 (Codex ClawHub Publisher Stats)"
AUTHOR_TABLE_URL = "https://topclawhubskills.com/"
SEARCH_API_URL = "https://topclawhubskills.com/api/search?q={query}&limit={limit}"
SKILL_PAGE_URL = "https://clawhub.ai/{handle}/{slug}"
STAT_PATTERN = re.compile(
r"(comments|downloads|installsAllTime|installsCurrent|stars):(\d+)"
)
AUTHOR_ROW_PATTERN = re.compile(
r'<tr data-search="(?P<handle>[^"]+)">\s*'
r'<td class="rank[^"]*">(?P<rank>\d+)</td>\s*'
r'<td><a href="https://github\.com/[^"]+" target="_blank" class="skill-name">@(?P=handle)</a></td>\s*'
r'<td class="num">(?P<skills>[^<]+)</td>\s*'
r'<td class="num">(?P<downloads>[^<]+)</td>\s*'
r'<td class="num">(?P<stars>[^<]+)</td>',
re.IGNORECASE,
)
@dataclass
class AuthorStats:
handle: str
rank: int | None
skills: int | None
downloads_text: str | None
stars: int | None
def fetch_text(url: str, timeout: int = 20) -> str:
request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
with urllib.request.urlopen(request, timeout=timeout) as response:
return response.read().decode("utf-8", "replace")
def parse_int(value: str | int | None) -> int | None:
if value is None:
return None
if isinstance(value, int):
return value
cleaned = value.strip().upper().replace(",", "")
multiplier = 1
if cleaned.endswith("K"):
multiplier = 1000
cleaned = cleaned[:-1]
elif cleaned.endswith("M"):
multiplier = 1_000_000
cleaned = cleaned[:-1]
try:
return int(float(cleaned) * multiplier)
except ValueError:
return None
def parse_author_stats(page_html: str, handle: str) -> AuthorStats:
for match in AUTHOR_ROW_PATTERN.finditer(page_html):
if match.group("handle").lower() != handle.lower():
continue
return AuthorStats(
handle=handle,
rank=parse_int(match.group("rank")),
skills=parse_int(match.group("skills")),
downloads_text=match.group("downloads").strip(),
stars=parse_int(match.group("stars")),
)
return AuthorStats(handle=handle, rank=None, skills=None, downloads_text=None, stars=None)
def fetch_author_stats(handle: str) -> AuthorStats:
page_html = fetch_text(AUTHOR_TABLE_URL)
return parse_author_stats(page_html, handle)
def fetch_search_results(handle: str, limit: int) -> dict[str, Any]:
url = SEARCH_API_URL.format(
query=urllib.parse.quote(handle),
limit=limit,
)
payload = json.loads(fetch_text(url))
rows = payload.get("data", [])
filtered = [row for row in rows if row.get("owner_handle", "").lower() == handle.lower()]
return {
"query_total": payload.get("total"),
"returned": len(filtered),
"rows": filtered,
"generated_at": payload.get("generated_at"),
}
def fetch_skill_page_stats(handle: str, slug: str) -> dict[str, Any]:
url = SKILL_PAGE_URL.format(
handle=urllib.parse.quote(handle),
slug=urllib.parse.quote(slug),
)
page_html = fetch_text(url)
stats = {name: int(value) for name, value in STAT_PATTERN.findall(page_html)}
stats["url"] = url
return stats
def enrich_rows(handle: str, rows: list[dict[str, Any]], workers: int) -> list[dict[str, Any]]:
enriched = [dict(row) for row in rows]
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
future_map = {
executor.submit(fetch_skill_page_stats, handle, row["slug"]): index
for index, row in enumerate(enriched)
}
for future in concurrent.futures.as_completed(future_map):
index = future_map[future]
row = enriched[index]
try:
page_stats = future.result()
except Exception as exc: # noqa: BLE001
row["skill_page_error"] = str(exc)
row["url"] = SKILL_PAGE_URL.format(handle=handle, slug=row["slug"])
continue
row["downloads"] = page_stats.get("downloads", row.get("downloads"))
row["stars"] = page_stats.get("stars", row.get("stars"))
row["comments"] = page_stats.get("comments")
row["installs_current"] = page_stats.get("installsCurrent")
row["installs_all_time"] = page_stats.get("installsAllTime")
row["url"] = page_stats["url"]
return enriched
def normalize_rows(handle: str, rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
normalized = []
for row in rows:
normalized.append(
{
"display_name": row.get("display_name"),
"slug": row.get("slug"),
"owner_handle": row.get("owner_handle", handle),
"downloads": parse_int(row.get("downloads")),
"stars": parse_int(row.get("stars")),
"comments": parse_int(row.get("comments")),
"installs_current": parse_int(row.get("installs_current")),
"installs_all_time": parse_int(row.get("installs_all_time")),
"created_at": row.get("created_at"),
"updated_at": row.get("updated_at"),
"summary": html.unescape(row.get("summary", "")),
"is_certified": row.get("is_certified"),
"is_deleted": row.get("is_deleted"),
"url": row.get("url") or SKILL_PAGE_URL.format(handle=handle, slug=row.get("slug")),
"skill_page_error": row.get("skill_page_error"),
}
)
normalized.sort(key=lambda item: (-(item["downloads"] or 0), item["slug"] or ""))
return normalized
def build_result(
handle: str,
limit: int,
include_skill_pages: bool,
workers: int,
) -> dict[str, Any]:
author_stats = fetch_author_stats(handle)
search_data = fetch_search_results(handle, limit)
rows = search_data["rows"]
if include_skill_pages and rows:
rows = enrich_rows(handle, rows, workers)
rows = normalize_rows(handle, rows)
return {
"handle": handle,
"fetched_live": True,
"source_limit": limit,
"include_skill_pages": include_skill_pages,
"generated_at": search_data.get("generated_at"),
"author_stats": asdict(author_stats),
"search_stats": {
"returned_skills": len(rows),
"query_total": search_data.get("query_total"),
"author_total_skills": author_stats.skills,
"search_total_may_be_partial": (
author_stats.skills is not None
and len(rows) < author_stats.skills
),
},
"notes": [
"Author aggregate stats come from the Top Authors table on topclawhubskills.com.",
"Per-skill rows come from the public search API and are filtered to the exact owner handle.",
"Current installs, all-time installs, and comments require skill-page fetches.",
"A separate public user rating field was not detected on sampled ClawHub skill pages.",
],
"skills": rows,
}
def render_markdown(result: dict[str, Any]) -> str:
author = result["author_stats"]
search = result["search_stats"]
lines = [
f"# ClawHub Publisher Stats: @{result['handle']}",
"",
"## Author Summary",
f"- Rank: {author['rank'] if author['rank'] is not None else 'unavailable'}",
f"- Published skills: {author['skills'] if author['skills'] is not None else 'unavailable'}",
f"- Total downloads: {author['downloads_text'] or 'unavailable'}",
f"- Total stars: {author['stars'] if author['stars'] is not None else 'unavailable'}",
"",
"## Search Coverage",
f"- Returned per-skill rows: {search['returned_skills']}",
f"- Search API total: {search['query_total']}",
f"- Aggregate author skill total: {search['author_total_skills']}",
f"- Search results may be partial: {'yes' if search['search_total_may_be_partial'] else 'no'}",
"",
"## Skills",
"",
"| Skill | Downloads | Current Installs | All-time Installs | Stars | Comments |",
"|---|---:|---:|---:|---:|---:|",
]
for skill in result["skills"]:
lines.append(
"| [{name}]({url}) | {downloads} | {current} | {all_time} | {stars} | {comments} |".format(
name=skill["display_name"] or skill["slug"],
url=skill["url"],
downloads=skill["downloads"] if skill["downloads"] is not None else "n/a",
current=(
skill["installs_current"]
if skill["installs_current"] is not None
else "n/a"
),
all_time=(
skill["installs_all_time"]
if skill["installs_all_time"] is not None
else "n/a"
),
stars=skill["stars"] if skill["stars"] is not None else "n/a",
comments=skill["comments"] if skill["comments"] is not None else "n/a",
)
)
lines.extend(["", "## Notes"])
lines.extend([f"- {note}" for note in result["notes"]])
return "\n".join(lines)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Fetch live ClawHub publisher metrics."
)
parser.add_argument("--user", required=True, help="Publisher handle, e.g. harrylabsj")
parser.add_argument(
"--limit",
type=int,
default=20,
help="Maximum number of search API skill rows to retrieve.",
)
parser.add_argument(
"--include-skill-pages",
action="store_true",
help="Fetch each skill page to populate install and comment counts.",
)
parser.add_argument(
"--workers",
type=int,
default=6,
help="Concurrent skill-page fetch workers.",
)
parser.add_argument(
"--format",
choices=("json", "markdown"),
default="json",
help="Output format.",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
try:
result = build_result(
handle=args.user,
limit=max(1, args.limit),
include_skill_pages=args.include_skill_pages,
workers=max(1, args.workers),
)
except urllib.error.URLError as exc:
print(f"Network error: {exc}", file=sys.stderr)
return 2
except Exception as exc: # noqa: BLE001
print(f"Failed to fetch publisher stats: {exc}", file=sys.stderr)
return 1
if args.format == "markdown":
print(render_markdown(result))
else:
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
Clawpilot is your skill installation advisor. It analyzes task intent, recommends suitable skills, compares tradeoffs, explains risk, and suggests an install...
---
name: Clawpilot
slug: clawpilot-advisor
version: 1.0.1
description: Clawpilot is your skill installation advisor. It analyzes task intent, recommends suitable skills, compares tradeoffs, explains risk, and suggests an installation order. Version 1 provides advice only and does not install skills automatically.
---
# Clawpilot
## Positioning
**Clawpilot = your skill installation advisor plus task radar.**
When a user knows what they want to do but does not know which skill to install, Clawpilot analyzes the task, recommends candidate skills, compares the tradeoffs, explains risk, and suggests an installation order.
> Version 1 gives recommendations only. It does not install skills automatically.
---
## When to Trigger
Use Clawpilot when the user describes a task in natural language but does not know which skill to use. The bundled intent map is currently strongest for Chinese-language queries, so the examples below use the shipped Chinese prompts.
- "我想查快递到哪里了"
- "我最近压力好大,有什么能帮我的吗?"
- "我想写租房合同,要注意什么?"
- "有什么法律类的 skill 吗?"
- "我想找对象"
- "帮我推荐一个购物 or 外卖 or 打车的工具"
---
## Core Features
| Feature | Description |
|------|------|
| **Intent detection** | Extracts the user task intent from the prompt, such as logistics, mental health, legal help, or shopping |
| **Candidate skill recommendations** | Returns 1-3 candidate skills for the detected intent |
| **Comparison guidance** | Explains the core differences between recommended skills |
| **Risk labels** | Assigns low, medium, high, or pending risk labels |
| **Installation guidance** | Suggests what to install first and what to install later |
| **Disclaimers** | Forces a disclaimer for medium and high-risk skills |
---
## Risk Levels
| Level | Meaning | Examples |
|------|------|------|
| Low | Pure utility use cases with little privacy or professional-risk impact | Weather lookup, package tracking, food delivery |
| Medium | Personal information or light decision support | Dating, shopping recommendations, mental health tools |
| High | May affect legal rights, financial safety, or health decisions | Contract drafting, legal support, medical advice |
| Pending | Not enough information to label the risk confidently | Newly added or unreviewed skills |
---
## Example Conversations
### Scenario 1: Package tracking (low risk)
**User query**: `我想查一下我的快递到哪里了`
**Clawpilot**:
- Detected intent: package tracking
- Recommended: `logistics` for aggregated tracking, then `sf-express` for SF Express-specific workflows
- Risk: low
- Install suggestion: install `logistics` first, then add `sf-express` only if you need a carrier-specific follow-up
---
### Scenario 2: Work stress (medium risk)
**User query**: `我最近工作压力好大,感觉快要撑不住了`
**Clawpilot**:
- Detected intent: mental health support
- Recommended: `burnout-checkin` -> `psych-companion`
- Risk: medium
- Reminder: these tools are supportive only and **cannot replace professional mental health care**
---
### Scenario 3: Rental contract drafting (high risk)
**User query**: `我要写一份租房合同,有什么工具帮我吗?`
**Clawpilot**:
- Detected intent: contract and legal support
- Recommended: `clause-redraft` -> `contract-risk-scan`
- Risk: high
- Required disclaimer: these skills provide support only and do not constitute legal advice. Important contracts should be reviewed by a licensed lawyer.
---
### Scenario 4: Unsupported request
**User query**: `你觉得我今天出门应该穿什么?`
**Clawpilot**: responds with a friendly refusal, shows supported examples, and asks the user to rephrase toward a skill-discovery task.
---
## Explicit Non-Goals
| Scenario | Reason |
|------|------|
| Automatic skill installation | The MVP is an advisor and preserves user choice |
| Medical diagnosis | Outside scope and high risk |
| Investment advice | High risk and should go to licensed professionals |
| International logistics or global services | Not covered by the current knowledge base |
| Technical implementation consulting | Not part of Clawpilot's role |
---
## Usage
```bash
# CLI smoke tests
python handler.py "我想查快递到哪里了"
python handler.py "我最近压力好大"
python handler.py "写租房合同用什么skill"
# Call as a module
from handler import handle
result = handle("我想查快递", installed_skills=["logistics"])
```
---
## Data Files
- `data/skill-db.json` - skill metadata database, including risk labels
- `data/intent-map.json` - intent keyword to recommended-skill mapping
---
*Clawpilot v1.0.1 - the skill installation advisor, trust layer, and entry layer of ClawHub*
FILE:data/intent-map.json
{
"intents": [
{
"intent": "快递物流查询",
"keywords": ["查快递", "物流", "快递到哪了", "寄快递", "查物流", "包裹", "发货", "到了吗"],
"skills": ["logistics", "sf-express", "zto", "jtexpress", "cainiao", "shansong"],
"installOrder": ["logistics", "sf-express"]
},
{
"intent": "同城配送",
"keywords": ["同城", "闪送", "送东西", "即时配送", "同城快递"],
"skills": ["shansong", "logistics"],
"installOrder": ["shansong", "logistics"]
},
{
"intent": "天气查询",
"keywords": ["天气", "温度", "下雨", "晴天", "多少度", "气温", "天气预报"],
"skills": ["weather"],
"installOrder": ["weather"]
},
{
"intent": "心理健康",
"keywords": ["焦虑", "压力", "抑郁", "心情不好", "心烦", "睡不着", "情绪", "职业倦怠", "撑不住", "心理"],
"skills": ["burnout-checkin", "psych-companion", "decision-fatigue-reliever", "daily-reflection"],
"installOrder": ["burnout-checkin", "psych-companion", "decision-fatigue-reliever"]
},
{
"intent": "相亲找对象",
"keywords": ["相亲", "找对象", "脱单", "谈恋爱", "恋爱", "结婚", "处对象"],
"skills": ["xiangqin", "blind-date-assistant"],
"installOrder": ["xiangqin", "blind-date-assistant"]
},
{
"intent": "合同法律",
"keywords": ["合同", "协议", "法律", "打官司", "纠纷", "维权", "条款", "租房", "劳动法"],
"skills": ["clause-redraft", "contract-risk-scan", "ai-legal-assistant-pro", "legal-consultation"],
"installOrder": ["clause-redraft", "contract-risk-scan"]
},
{
"intent": "医疗健康",
"keywords": ["挂号", "看病", "医生", "医院", "体检", "健康", "身体"],
"skills": ["haodf", "hospital"],
"installOrder": ["hospital", "haodf"]
},
{
"intent": "购物比价",
"keywords": ["买东西", "购物", "比价", "便宜", "在哪买", "推荐", "网购", "淘宝", "京东", "拼多多"],
"skills": ["cn-online-shopping", "pdd-shopping", "jingdong-shopping", "yhd", "taobao-competitor-analyzer"],
"installOrder": ["cn-online-shopping", "pdd-shopping"]
},
{
"intent": "外卖点餐",
"keywords": ["外卖", "点餐", "吃饭", "送餐", "饿了", "吃什么"],
"skills": ["waimai", "freshippo"],
"installOrder": ["waimai", "freshippo"]
},
{
"intent": "出行交通",
"keywords": ["打车", "叫车", "出行", "旅行", "机票", "酒店", "高铁", "火车"],
"skills": ["didi", "trip", "qunar"],
"installOrder": ["didi", "trip"]
},
{
"intent": "学习成长",
"keywords": ["学习", "英语", "读书", "考试", "提升", "成长", "技能"],
"skills": ["daily-reflection", "habit-tracker"],
"installOrder": ["daily-reflection"]
},
{
"intent": "生鲜配送",
"keywords": ["买菜", "生鲜", "水果", "蔬菜", "盒马", "新鲜"],
"skills": ["freshippo", "waimai"],
"installOrder": ["freshippo", "waimai"]
}
],
"defaultResponse": {
"noMatch": "抱歉,我目前无法识别你的需求。我的专长是帮你发现和安装合适的 skill。",
"examples": "你可以这样问我:'我想查快递'、'我最近压力好大'、'有什么法律类 skill 吗?'"
}
}
FILE:data/skill-db.json
{
"skills": [
{"name": "logistics", "displayName": "物流聚合查询", "description": "支持多家主流快递公司物流查询", "riskLevel": "low", "riskReason": null, "category": "工具"},
{"name": "sf-express", "displayName": "顺丰速运", "description": "顺丰快递专用查询", "riskLevel": "low", "riskReason": null, "category": "工具"},
{"name": "zto", "displayName": "中通快递", "description": "中通快递物流查询", "riskLevel": "low", "riskReason": null, "category": "工具"},
{"name": "jtexpress", "displayName": "极兔速递", "description": "极兔快递物流查询", "riskLevel": "low", "riskReason": null, "category": "工具"},
{"name": "cainiao", "displayName": "菜鸟裹裹", "description": "菜鸟裹裹快递查询", "riskLevel": "low", "riskReason": null, "category": "工具"},
{"name": "shansong", "displayName": "闪送", "description": "同城即时配送服务", "riskLevel": "low", "riskReason": null, "category": "工具"},
{"name": "weather", "displayName": "天气查询", "description": "查询全球天气信息", "riskLevel": "low", "riskReason": null, "category": "工具"},
{"name": "burnout-checkin", "displayName": "职业倦怠自测", "description": "评估职业倦怠程度,提供缓解建议", "riskLevel": "medium", "riskReason": "心理健康自测工具,非专业诊疗,使用结果仅供参考", "category": "心理健康"},
{"name": "psych-companion", "displayName": "心理陪伴助手", "description": "情绪陪伴与心理疏导", "riskLevel": "medium", "riskReason": "心理陪伴工具,不能替代专业心理咨询", "category": "心理健康"},
{"name": "decision-fatigue-reliever", "displayName": "决策疲劳缓解", "description": "帮助减少日常决策疲劳", "riskLevel": "medium", "riskReason": "辅助工具,涉及个人决策建议", "category": "心理健康"},
{"name": "daily-reflection", "displayName": "每日复盘", "description": "每日反思与成长记录", "riskLevel": "low", "riskReason": null, "category": "效率"},
{"name": "xiangqin", "displayName": "相亲助手", "description": "相亲话题与流程指导", "riskLevel": "medium", "riskReason": "涉及个人隐私,建议使用脱敏信息", "category": "社交"},
{"name": "blind-date-assistant", "displayName": "约会助手", "description": "约会技巧与建议", "riskLevel": "medium", "riskReason": "情感建议类,仅供参考", "category": "社交"},
{"name": "clause-redraft", "displayName": "合同条款润色", "description": "合同条款修改与润色", "riskLevel": "high", "riskReason": "涉及法律文书,不构成法律意见,重要合同请咨询律师", "category": "法律"},
{"name": "contract-risk-scan", "displayName": "合同风险扫描", "description": "合同风险点识别与提示", "riskLevel": "high", "riskReason": "法律风险评估,不构成法律意见", "category": "法律"},
{"name": "ai-legal-assistant-pro", "displayName": "AI 法律助手", "description": "法律问题咨询与解答", "riskLevel": "high", "riskReason": "法律咨询辅助,不构成律师意见", "category": "法律"},
{"name": "legal-consultation", "displayName": "法律咨询", "description": "基础法律问题咨询", "riskLevel": "high", "riskReason": "法律咨询,不构成正式法律意见", "category": "法律"},
{"name": "haodf", "displayName": "好大夫在线", "description": "预约挂号、医生咨询", "riskLevel": "high", "riskReason": "医疗相关,不能替代面诊", "category": "医疗"},
{"name": "hospital", "displayName": "医院查询", "description": "查找附近医院", "riskLevel": "medium", "riskReason": "医疗信息查询,不能替代诊疗", "category": "医疗"},
{"name": "cn-online-shopping", "displayName": "国内网购助手", "description": "国内主流电商平台购物辅助", "riskLevel": "medium", "riskReason": "购物建议,涉及价格信息", "category": "电商"},
{"name": "pdd-shopping", "displayName": "拼多多购物", "description": "拼多多商品搜索与推荐", "riskLevel": "medium", "riskReason": "购物建议", "category": "电商"},
{"name": "jingdong-shopping", "displayName": "京东购物", "description": "京东商品搜索与推荐", "riskLevel": "medium", "riskReason": "购物建议", "category": "电商"},
{"name": "yhd", "displayName": "一号店购物", "description": "一号店商品搜索", "riskLevel": "medium", "riskReason": "购物建议", "category": "电商"},
{"name": "freshippo", "displayName": "盒马鲜生", "description": "盒马生鲜配送查询", "riskLevel": "low", "riskReason": null, "category": "生活"},
{"name": "waimai", "displayName": "外卖助手", "description": "外卖平台推荐与优惠", "riskLevel": "low", "riskReason": null, "category": "生活"},
{"name": "didi", "displayName": "滴滴出行", "description": "叫车与出行规划", "riskLevel": "low", "riskReason": null, "category": "出行"},
{"name": "trip", "displayName": "旅行助手", "description": "机票酒店预订辅助", "riskLevel": "medium", "riskReason": "出行规划,涉及预订建议", "category": "出行"},
{"name": "qunar", "displayName": "去哪儿旅行", "description": "机票酒店查询", "riskLevel": "medium", "riskReason": "出行规划", "category": "出行"},
{"name": "taobao-competitor-analyzer", "displayName": "淘宝竞品分析", "description": "分析淘宝店铺竞品数据", "riskLevel": "low", "riskReason": null, "category": "电商"}
]
}
FILE:handler.py
#!/usr/bin/env python3
"""
Clawpilot Handler - Skill 安装顾问
分析用户任务意图,推荐合适的 skill,比较差异,解释风险,给出安装建议。
第一版:规则引擎 + 静态数据库,不做自动安装。
"""
import json
import os
import re
import sys
from typing import Any, Optional
# ==================== 数据加载 ====================
def get_skill_dir() -> str:
"""获取 skill 目录路径"""
return os.path.dirname(os.path.abspath(__file__))
def load_skill_db() -> dict:
"""加载 skill 元数据库"""
path = os.path.join(get_skill_dir(), "data", "skill-db.json")
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def load_intent_map() -> dict:
"""加载意图-技能映射表"""
path = os.path.join(get_skill_dir(), "data", "intent-map.json")
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
# ==================== 意图识别 ====================
def parse_intent(user_query: str, intent_map: dict) -> Optional[dict]:
"""
从用户输入中识别任务意图。
使用关键词匹配:命中关键词最多的意图胜出。
"""
query = user_query.lower().strip()
best_match: Optional[dict] = None
best_score = 0
for intent_entry in intent_map.get("intents", []):
score = 0
for kw in intent_entry.get("keywords", []):
if kw.lower() in query:
score += 1
if score > best_score:
best_score = score
best_match = intent_entry
# 至少命中 1 个关键词才认定匹配
if best_score >= 1:
return best_match
return None
# ==================== Skill 查找与推荐 ====================
def build_skill_index(skill_db: dict) -> dict:
"""构建 skill name -> info 的索引"""
return {s["name"]: s for s in skill_db.get("skills", [])}
def recommend_skills(
intent_entry: dict,
skill_db: dict,
installed_skills: list[str],
) -> list[dict]:
"""
根据意图返回推荐列表,过滤已安装 skill,
最多返回 3 个,按 installOrder 排序。
"""
skill_index = build_skill_index(skill_db)
ordered = intent_entry.get("installOrder", [])
skill_names = intent_entry.get("skills", [])
def sort_key(name: str) -> int:
if name in ordered:
return ordered.index(name)
return len(ordered) + skill_names.index(name)
results = []
for name in sorted(skill_names, key=sort_key):
if name in installed_skills:
continue
info = skill_index.get(name)
if not info:
continue
results.append({
"skillName": name,
"skillDisplayName": info.get("displayName", name),
"description": info.get("description", ""),
"riskLevel": info.get("riskLevel", "pending"),
"riskReason": info.get("riskReason"),
"category": info.get("category", ""),
"installOrder": sort_key(name) + 1,
})
if len(results) >= 3:
break
return results
# ==================== 风险与免责声明 ====================
RISK_EMOJI = {
"low": "🟢",
"medium": "🟡",
"high": "🔴",
"pending": "⚪",
}
RISK_LABEL = {
"low": "低风险",
"medium": "中风险",
"high": "高风险",
"pending": "待确认",
}
DISCLAIMER_TEMPLATE = """⚠️ **使用提醒**
以下 skill 涉及 **{risk_category}** 领域,提供的仅供参考,**不构成专业意见**:
{skill_list}
重要决策请咨询 {professionals},使用前请自行了解相关风险。"""
BOUNDARIES = [
"第一版只提供安装建议,不做自动安装。",
"推荐基于当前 skill 库,可能不涵盖所有场景。",
"风险分级由人工维护,仅供参考,实际使用请自行判断。",
]
def build_disclaimer(recommendations: list[dict]) -> Optional[str]:
"""如果存在中高风险 skill,返回免责声明"""
high_or_medium = [r for r in recommendations if r["riskLevel"] in ("high", "medium")]
if not high_or_medium:
return None
by_category: dict[str, list[str]] = {}
for r in high_or_medium:
cat = r.get("category", "其他")
by_category.setdefault(cat, []).append(r["skillDisplayName"])
professional_map = {
"法律": "持证律师",
"医疗": "专业医生",
"心理健康": "心理咨询师",
"电商": "消费维权顾问",
"出行": "旅行顾问",
"社交": "情感顾问",
"其他": "专业人士",
}
parts = []
for cat, names in by_category.items():
skill_list = "、".join(names)
professional = professional_map.get(cat, "专业人士")
parts.append(f"- {skill_list}({cat})→ 请咨询 {professional}")
return DISCLAIMER_TEMPLATE.format(
risk_category="/".join(by_category.keys()),
skill_list="\n".join(parts),
professionals="、".join(set(professional_map.get(c, "专业人士") for c in by_category)),
)
# ==================== 输出格式化 ====================
def generate_report(
intent_entry: dict,
recommendations: list[dict],
user_query: str,
skill_db: dict,
) -> str:
"""生成结构化 Markdown 推荐报告"""
intent_name = intent_entry["intent"]
lines = []
lines.append(f"## 🎯 识别意图:{intent_name}")
lines.append("")
if not recommendations:
lines.append("> 你已安装了所有相关 skill,无需额外推荐 🎉")
return "\n".join(lines)
# 推荐列表
lines.append("### 📦 推荐安装")
lines.append("")
lines.append("| # | Skill | 风险 | 安装顺序 | 说明 |")
lines.append("|---|-------|------|---------|------|")
for rec in recommendations:
emoji = RISK_EMOJI.get(rec["riskLevel"], "⚪")
risk_label = RISK_LABEL.get(rec["riskLevel"], "待确认")
order = rec["installOrder"]
name = rec["skillDisplayName"]
desc = rec["description"]
if order == 1:
reason = f"首选推荐:{desc}"
elif rec["riskLevel"] == "high":
reason = f"{desc}(高风险,使用需谨慎)"
elif rec["riskLevel"] == "medium":
reason = f"{desc}(中风险,注意使用边界)"
else:
reason = desc
lines.append(
f"| {order} | **{name}** (`{rec['skillName']}`) "
f"| {emoji} {risk_label} | #{order} | {reason} |"
)
lines.append("")
# 安装建议
primary = next((r for r in recommendations if r["installOrder"] == 1), None)
if primary:
lines.append("### ✅ 安装建议")
lines.append("")
lines.append(
f"**推荐先装 [{primary['skillDisplayName']}]({primary['skillName']})**:"
f"{primary['description']}"
)
if len(recommendations) > 1:
others = [r["skillDisplayName"] for r in recommendations if r["installOrder"] > 1]
lines.append(f"体验后再按需安装:{', '.join(others)}。")
lines.append("")
# 免责声明
disclaimer = build_disclaimer(recommendations)
if disclaimer:
lines.append(disclaimer)
lines.append("")
# 边界说明
lines.append("### 📌 边界说明")
for b in BOUNDARIES:
lines.append(f"- {b}")
lines.append("")
lines.append("*以上建议仅供参考,安装决策权在你。*")
return "\n".join(lines)
def generate_no_match_report(intent_map: dict, user_query: str, skill_db: dict) -> str:
"""无法识别意图时的友好拒绝"""
default = intent_map.get("defaultResponse", {})
total = len(skill_db.get("skills", []))
lines = []
lines.append("## 🔍 抱歉,暂无法识别你的需求")
lines.append("")
lines.append(default.get("noMatch", "抱歉,我无法识别你的需求。"))
lines.append("")
lines.append("### 💡 你可以这样问我:")
for example in [
"我想查快递到哪里了",
"我最近工作压力好大",
"有什么法律类的 skill 吗?",
"我想点外卖",
"帮我推荐一个学习成长的工具",
]:
lines.append(f"- \"{example}\"")
lines.append("")
lines.append(f"*目前我可推荐的 skill 共 {total} 个,覆盖物流、心理、法律、购物、出行等场景。*")
lines.append("")
lines.append("### 🚫 明确无法帮助的场景")
lines.append("- 医疗诊断(请咨询医生)")
lines.append("- 投资理财推荐(请咨询持牌顾问)")
lines.append("- 自动安装 skill(第一版只提供建议)")
lines.append("- 境外快递/国际服务(知识库暂不支持)")
return "\n".join(lines)
# ==================== 入口函数 ====================
def handle(
user_query: str,
installed_skills: Optional[list[str]] = None,
risk_preference: Optional[str] = None,
) -> str:
"""
主入口函数,供 OpenClaw 调用。
Args:
user_query: 用户任务描述(必填)
installed_skills: 用户已安装的 skill 列表(可选)
risk_preference: 保守/激进偏好(可选)
Returns:
Markdown 格式的推荐报告
"""
installed_skills = installed_skills or []
skill_db = load_skill_db()
intent_map = load_intent_map()
intent_entry = parse_intent(user_query, intent_map)
if intent_entry is None:
return generate_no_match_report(intent_map, user_query, skill_db)
recommendations = recommend_skills(intent_entry, skill_db, installed_skills)
return generate_report(intent_entry, recommendations, user_query, skill_db)
# ==================== CLI 自测入口 ====================
def main():
"""
CLI 自测入口:
python handler.py <用户问题>
python handler.py <用户问题> --installed skill1,skill2
"""
args = sys.argv[1:]
if not args:
print("用法: python handler.py <用户问题> [--installed skill1,skill2]")
print("示例: python handler.py 我想查快递到哪里了")
print(" python handler.py 我想查快递 --installed logistics")
sys.exit(1)
user_query = args[0]
installed = []
if "--installed" in args:
idx = args.index("--installed")
if idx + 1 < len(args):
installed = [s.strip() for s in args[idx + 1].split(",") if s.strip()]
result = handle(user_query, installed_skills=installed)
print(result)
if __name__ == "__main__":
main()
FILE:skill.json
{
"name": "clawpilot",
"version": "1.0.1",
"description": "Clawpilot is your skill installation advisor. It analyzes task intent, recommends suitable skills, compares tradeoffs, explains risk, and suggests an installation order. Version 1 provides advice only and does not install skills automatically.",
"entry": "handler.py",
"runtime": "python3",
"author": "Harry",
"tags": [
"skill",
"recommendation",
"advisor",
"installation",
"skill-radar",
"安装顾问"
],
"triggers": [
"有什么skill",
"有什么工具",
"我想查快递",
"我想学",
"推荐skill",
"装什么",
"有什么能帮我",
"skill推荐",
"压力好大",
"焦虑",
"写合同",
"法律",
"相亲",
"找对象",
"购物",
"点外卖",
"打车",
"看病",
"挂号"
],
"capabilities": [
"任务意图识别(关键词+规则引擎)",
"候选 skill 推荐(1-3个)",
"skill 差异比较说明",
"风险分级(低/中/高/待确认)",
"安装顺序建议",
"中高风险强制免责声明",
"无法识别时的友好拒绝"
],
"boundaries": [
"第一版不做自动安装,只提供建议",
"不爬取 skill 源码或技术实现",
"不推荐境外快递/国际服务",
"不提供医疗诊断、投资建议"
],
"input_schema": {
"userQuery": "string (required): 用户任务描述,如'我想查快递到哪了'",
"installedSkills": "list[string] (optional): 用户已安装的 skill 列表,用于排除",
"riskPreference": "string (optional): conservative|moderate|aggressive,影响推荐策略"
},
"output_schema": {
"format": "markdown",
"sections": [
"intent (识别到的意图)",
"recommendations (推荐列表,含 skill 名称、理由、安装顺序、风险分级)",
"installSuggestion (安装建议)",
"disclaimer (免责声明,中高风险 skill 存在时必显示)",
"boundaries (边界说明)"
]
},
"acceptance_criteria": [
"意图识别准确(主流意图 ≥ 80%)",
"推荐相关性(前3个至少1个相关)",
"风险分级一致性(法律/医疗类正确标注高风险)",
"免责声明触发(高风险 skill 推荐时包含 disclaimer)",
"友好拒绝(无法识别时给出友好提示而非空响应)",
"响应时间 ≤ 2 秒"
],
"data_files": [
"data/skill-db.json",
"data/intent-map.json"
]
}
FILE:test.js
#!/usr/bin/env node
/**
* Clawpilot Test Suite
* 运行:node test.js
*
* 自测模式(无参数):使用内置测试用例
* 指定查询:node test.js "你的问题"
*/
const { spawn } = require("child_process");
const path = require("path");
const HANDLER = path.join(__dirname, "handler.py");
function runPy(query, installed = []) {
return new Promise((resolve) => {
const args = [HANDLER, query];
if (installed.length > 0) {
args.push("--installed", installed.join(","));
}
const proc = spawn("python3", args);
let stdout = "";
let stderr = "";
proc.stdout.on("data", (d) => (stdout += d.toString()));
proc.stderr.on("data", (d) => (stderr += d.toString()));
proc.on("close", (code) => {
resolve({ query, installed, stdout, stderr, code });
});
});
}
const TESTS = [
// --- 低风险:快递 ---
{
name: "[低风险] 查快递 - 识别意图",
query: "我想查快递到哪里了",
check: (out) =>
out.includes("快递物流查询") && out.includes("logistics"),
},
{
name: "[低风险] 快递+天气混合 - 命中快递",
query: "我的快递到了吗,今天天气怎么样",
check: (out) => out.includes("快递物流查询"),
},
// --- 中风险:心理健康 ---
{
name: "[中风险] 工作压力 - 识别心理健康意图",
query: "我最近工作压力好大,感觉快要撑不住了",
check: (out) =>
out.includes("心理健康") &&
out.includes("burnout-checkin"),
},
{
name: "[中风险] 焦虑 - 识别意图",
query: "我很焦虑,有什么skill能帮到我吗",
check: (out) =>
out.includes("心理健康") &&
(out.includes("burnout-checkin") || out.includes("psych-companion")),
},
// --- 高风险:法律 ---
{
name: "[高风险] 租房合同 - 识别法律意图+免责",
query: "我要写一份租房合同,有什么工具帮我吗",
check: (out) =>
out.includes("合同法律") &&
out.includes("clause-redraft") &&
out.includes("⚠️"),
},
{
name: "[高风险] 法律咨询 - 高风险标注+免责声明",
query: "有什么法律类的skill吗",
check: (out) =>
out.includes("法律") &&
(out.includes("🔴") || out.includes("高风险")) &&
out.includes("⚠️"),
},
// --- 中风险:相亲 ---
{
name: "[中风险] 相亲找对象",
query: "我想找对象,有什么红娘skill吗",
check: (out) =>
out.includes("相亲找对象") &&
out.includes("xiangqin"),
},
// --- 低风险:天气 ---
{
name: "[低风险] 天气查询",
query: "今天天气怎么样",
check: (out) => out.includes("天气查询") && out.includes("weather"),
},
// --- 低风险:外卖 ---
{
name: "[低风险] 外卖点餐",
query: "我想点外卖",
check: (out) =>
out.includes("外卖点餐") && out.includes("waimai"),
},
// --- 低风险:出行 ---
{
name: "[低风险] 打车出行",
query: "我想打车",
check: (out) =>
out.includes("出行交通") && out.includes("didi"),
},
// --- 拒绝场景 ---
{
name: "[拒绝] 穿衣服 - 友好拒绝",
query: "你觉得我今天出门应该穿什么",
check: (out) =>
out.includes("抱歉") || out.includes("暂无法识别"),
},
// --- 边界:已安装过滤 ---
{
name: "[边界] 已安装logistics - 跳过并提示",
query: "我想查快递",
installed: ["logistics"],
check: (out) =>
!out.includes("logistics") ||
out.includes("已安装") ||
out.includes("无需"),
},
];
async function main() {
// 如果带了参数,直接对指定查询运行并打印结果
if (process.argv.length > 2) {
const query = process.argv.slice(2).join(" ");
const installed = process.argv.includes("--installed")
? process.argv[process.argv.indexOf("--installed") + 1].split(",")
: [];
const result = await runPy(query, installed);
console.log(result.stdout);
if (result.stderr) console.error("STDERR:", result.stderr);
return;
}
// 内置测试套件
console.log("🧪 Clawpilot Test Suite\n");
let passed = 0;
let failed = 0;
for (const t of TESTS) {
const { stdout } = await runPy(t.query, t.installed || []);
const ok = t.check(stdout);
const status = ok ? "✅ PASS" : "❌ FAIL";
console.log(`status t.name`);
if (!ok) {
console.log(` 查询: t.query`);
if (t.installed?.length) console.log(` 已安装: t.installed.join(", ")`);
console.log(` 输出: stdout.slice(0, 300).replace(/\n/g, " ")`);
failed++;
} else {
passed++;
}
}
console.log(`\n📊 结果: passed 通过, failed 失败`);
process.exit(failed > 0 ? 1 : 0);
}
main().catch(console.error);
Spreadsheet operations agent for CSV and Excel files: inspect data, detect anomalies, answer business questions, generate reports, and preview edits safely.
---
name: sheet-agent
description: "Spreadsheet operations agent for CSV and Excel files: inspect data, detect anomalies, answer business questions, generate reports, and preview edits safely."
---
# Sheet Agent
## One-Line Positioning
**Transform natural language into spreadsheet comprehension, anomaly detection, business insights, and safe rewrite suggestions.**
---
## When to Use
Activate this skill when the user mentions:
- Having AI read or inspect a spreadsheet for problems
- Querying data from CSV/Excel files
- Generating daily reports, weekly reports, or operations summaries
- Modifying a specific record in a spreadsheet (preview first, confirm second)
- Checking for empty values, duplicates, or anomalies in a spreadsheet
---
## Usage
```
/sheet "spreadsheet path" "natural language instruction"
```
Examples:
```
/sheet "~/Desktop/orders.csv" "Which orders haven't been followed up for more than 3 days?"
/sheet "~/Documents/inventory.xlsx" "Check if there are any negative inventory quantities"
/sheet "~/Desktop/orders.csv" "Generate last week's weekly report"
/sheet "~/Desktop/customers.csv" "Change the customer tier in row 8 to VIP"
```
---
## Core Features
### 1. Spreadsheet Reading & Comprehension
- Supports CSV and Excel (.xlsx) files
- Auto-recognizes column names and data types
- Infers business type (orders/inventory/leads/daily report)
### 2. Business Queries (Natural Language to Structured Results)
- "Orders not followed up for more than X days"
- "Orders with amount greater than Y"
- "Find all VIP customers"
- "Sum quantities by sales representative"
### 3. Anomaly Detection
- Empty values (missing data)
- Negative numbers (inventory/quantity columns)
- Duplicate IDs
- Date anomalies (future dates, inconsistent formats)
- Value anomalies (beyond reasonable ranges)
### 4. Daily / Weekly / Operations Summaries
- Auto-detects time range
- Generates structured summary:
- Overall view (total count, sum, average)
- Distribution stats (category percentages, top items)
- Anomaly record list
- Trend descriptions
### 5. Change Preview (Key Safety Mechanism)
**All changes show a preview first — execute only after user confirmation.**
User says "Change the amount in row 5 to 15000":
-> The agent shows a preview and does NOT write yet
-> If the user replies "confirm", execute the write
-> If the user replies "cancel", do nothing
---
## Output Examples
### Query Results
```
📊 Query Results: 12 leads have not been followed up for more than 3 days
| # | Name | Phone | Entry Date | Days Since |
|---|------|-------|------------|------------|
| 3 | Alex Wang | 138... | 2026-03-25 | 6 days |
| 7 | Leo Zhao | 139... | 2026-03-27 | 4 days |
```
### Anomaly Detection
```
⚠️ Found 4 anomalies:
1. Row 15 [Bluetooth Earbuds]: Inventory = -5 (negative)
2. Row 28 [Phone Case]: Unit price is empty
3. Row 42 [Data Cable]: Inventory = 9999 (suspiciously large)
4. Row 55: Product name is duplicated
```
### Weekly Report
```
📈 Operations Summary — Week 4, March 2026
[Overall View]
- Total orders: 156
- Total sales: ¥128,500
- Average order value: ¥823
[Top 5 Selling Items]
1. Bluetooth Earbuds - 45 units
2. Phone Case - 32 units
...
[Anomalous Orders]
- Refunds: 2 orders
```
### Change Preview
```
📝 Proposed Change Preview:
File: customers.csv
Row: 8
Field: Customer Tier
Old value: Regular
New value: VIP
⚠️ Confirm execution? Reply "confirm" or "cancel"
```
---
## Safety Principles
1. **Read-only by default**: Any operation does not modify files by default
2. **Write only on confirmation**: Write operations require explicit user confirmation
3. **Backup before write**: Automatically back up original file to `backup/` directory
4. **Ask when uncertain**: If column meaning is unclear, proactively ask the user instead of assuming
---
## Limitations
- Recommended maximum of 100,000 rows per operation
- Supports CSV and .xlsx formats
- Version 1 does not handle cross-spreadsheet joins
FILE:README.md
# Sheet Agent
Transform natural language into spreadsheet comprehension, anomaly detection, business insights, and safe rewrite suggestions.
## Quick Start
```bash
# Install dependencies
pip install pandas openpyxl
# View spreadsheet info
python scripts/sheet_agent.py ~/Desktop/orders.csv
# Query overdue leads
python scripts/sheet_agent.py ~/Desktop/leads.csv "Which leads haven't been followed up for more than 3 days?"
# Check for anomalies
python scripts/sheet_agent.py ~/Desktop/inventory.xlsx "Check for any issues"
# Generate weekly report
python scripts/sheet_agent.py ~/Desktop/orders.csv "Generate last week's weekly report"
# Change preview (no write)
python scripts/sheet_agent.py ~/Desktop/customers.csv "Change the customer tier in row 8 to VIP"
```
## Run Tests
```bash
cd /Users/jianghaidong/.openclaw/skills/sheet-agent
python tests/test_sheet_agent.py
```
## Core Principles
- **Read-only by default**: Operations do not modify files by default
- **Preview before confirm**: Show diff before changing data; execute only on user confirmation
- **Backup before write**: Original file automatically backed up to `backup/` directory
- **Ask when uncertain**: If column meaning is unclear, proactively ask the user
## Directory Structure
```
sheet-agent/
├── SKILL.md # Skill definition
├── skill.json # Metadata
├── scripts/
│ └── sheet_agent.py # Core logic (Python)
├── templates/ # Report templates
├── tests/
│ └── test_sheet_agent.py # Test suite
├── backup/ # Auto backup directory
└── README.md
```
## Feature Checklist
| Feature | Status |
|---------|--------|
| CSV reading | ✅ |
| Excel reading | ✅ |
| Spreadsheet type inference | ✅ |
| Empty value detection | ✅ |
| Negative number detection | ✅ |
| Duplicate ID detection | ✅ |
| Date anomaly detection | ✅ |
| Large value anomaly detection | ✅ |
| Natural language query parsing | ✅ |
| Overdue lead queries | ✅ |
| Change preview | ✅ |
| Daily/weekly report generation | ✅ |
| Write execution (on confirm) | ✅ |
FILE:scripts/sheet_agent.py
#!/usr/bin/env python3
"""
sheet-agent core script
Capabilities:
- Spreadsheet reading and comprehension
- Anomaly detection
- Natural-language business queries
- Safe change previews
- Daily and weekly summary generation
Core principle:
Never write blindly. Show a preview first and execute changes only after the
user explicitly confirms them.
"""
import json
import os
import re
import shutil
import sys
from datetime import datetime, timedelta
from pathlib import Path
from typing import Optional
def _check_deps() -> None:
missing = []
try:
import pandas as pd # noqa: F401
except ImportError:
missing.append("pandas")
try:
import openpyxl # noqa: F401
except ImportError:
missing.append("openpyxl")
if missing:
print(
f"[sheet-agent] Missing dependencies: {', '.join(missing)}. "
"Install them with: pip install pandas openpyxl"
)
sys.exit(1)
_check_deps()
import openpyxl # noqa: E402,F401
import pandas as pd # noqa: E402
BACKUP_DIR = Path(__file__).parent.parent / "backup"
SKILL_DIR = Path(__file__).parent.parent
TEMPLATE_DIR = SKILL_DIR / "templates"
ORDER_TERMS = ["order", "\u8ba2\u5355", "\u9500\u552e\u989d", "\u4ea4\u6613"]
INVENTORY_TERMS = ["inventory", "\u5e93\u5b58", "\u5546\u54c1", "product"]
LEAD_TERMS = [
"lead",
"customer",
"follow up",
"phone",
"\u7ebf\u7d22",
"\u5ba2\u6237",
"\u8ddf\u8fdb",
]
DAILY_LOG_TERMS = ["daily", "log", "\u65e5\u62a5", "\u65e5\u5fd7", "\u53f0\u8d26"]
DATE_TERMS = [
"date",
"time",
"datetime",
"\u65e5\u671f",
"\u65f6\u95f4",
"\u8ddf\u8fdb\u65e5\u671f",
"\u5f55\u5165\u65e5\u671f",
"\u521b\u5efa\u65f6\u95f4",
]
AMOUNT_TERMS = [
"amount",
"total",
"price",
"quantity",
"\u91d1\u989d",
"\u9500\u552e\u989d",
"\u603b\u4ef7",
"\u6570\u91cf",
]
STATUS_TERMS = ["status", "level", "type", "tier", "\u72b6\u6001", "\u7b49\u7ea7", "\u7c7b\u578b", "\u5ba2\u6237\u7b49\u7ea7"]
COUNT_TERMS = ["count", "how many", "total records", "\u7edf\u8ba1", "\u5408\u8ba1", "\u603b\u548c", "\u6709\u591a\u5c11", "\u603b\u5171"]
REPORT_TERMS = ["weekly report", "daily report", "summary", "report", "\u5468\u62a5", "\u65e5\u62a5", "\u6458\u8981", "\u603b\u7ed3", "\u6c47\u603b"]
ANOMALY_TERMS = ["issue", "problem", "anomaly", "abnormal", "\u5f02\u5e38", "\u95ee\u9898"]
FOLLOW_UP_TERMS = ["followed up", "follow up", "pending", "contact", "reply", "\u8ddf\u8fdb", "\u672a\u5904\u7406", "\u5f85\u8054\u7cfb", "\u672a\u56de\u590d"]
CHANGE_VERBS = ["change", "set", "update", "modify", "\u6539\u6210", "\u53d8\u4e3a", "\u4fee\u6539"]
CONFIRM_WORDS = ["confirm", "\u786e\u8ba4"]
CANCEL_WORDS = ["cancel", "\u53d6\u6d88"]
ID_TERMS = ["id", "no.", "no", "\u7f16\u53f7", "\u8ba2\u5355\u53f7", "\u5e8f\u53f7"]
def contains_any(text: str, terms: list[str]) -> bool:
lowered = text.lower()
return any(term.lower() in lowered for term in terms)
def expand_path(path_str: str) -> Path:
"""Expand user home and relative segments to an absolute path."""
return Path(os.path.expanduser(path_str)).resolve()
def ensure_backup_dir() -> None:
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
def backup_file(file_path: Path) -> Path:
"""Back up a file to the skill-local backup directory."""
ensure_backup_dir()
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_name = f"{file_path.stem}_{ts}{file_path.suffix}"
backup_path = BACKUP_DIR / backup_name
shutil.copy2(file_path, backup_path)
return backup_path
def read_table(file_path: Path) -> pd.DataFrame:
"""Read a CSV or Excel file and return it as a DataFrame."""
suffix = file_path.suffix.lower()
if suffix == ".csv":
for encoding in ("utf-8", "gbk", "latin1"):
try:
return pd.read_csv(file_path, encoding=encoding)
except UnicodeDecodeError:
continue
raise ValueError(f"Failed to decode CSV file: {file_path}")
if suffix in (".xlsx", ".xls"):
return pd.read_excel(file_path, engine="openpyxl")
raise ValueError(
f"Unsupported file format: {suffix}. Only CSV and Excel "
"(.xlsx/.xls) are supported."
)
def get_table_info(df: pd.DataFrame) -> dict:
"""Analyze spreadsheet structure and return metadata."""
info = {
"rows": len(df),
"cols": len(df.columns),
"col_names": list(df.columns),
"col_types": {},
"has_empty": df.isnull().any().any(),
"empty_counts": df.isnull().sum().to_dict(),
"table_type": infer_table_type(df),
}
for col in df.columns:
info["col_types"][col] = str(df[col].dtype)
return info
def infer_table_type(df: pd.DataFrame) -> str:
"""Infer a business-oriented spreadsheet type from column names."""
col_text = " ".join(str(col).lower() for col in df.columns)
if contains_any(col_text, ORDER_TERMS):
return "Orders"
if contains_any(col_text, INVENTORY_TERMS):
return "Inventory"
if contains_any(col_text, LEAD_TERMS):
return "Sales leads"
if contains_any(col_text, DAILY_LOG_TERMS):
return "Daily log"
return "General spreadsheet"
def detect_anomalies(df: pd.DataFrame) -> list[dict]:
"""Detect missing values, negatives, duplicate IDs, and date outliers."""
anomalies: list[dict] = []
for col in df.columns:
null_rows = df[df[col].isnull()]
if not null_rows.empty:
for idx in null_rows.index[:5]:
row_num = idx + 2
anomalies.append(
{
"type": "Missing value",
"row": row_num,
"col": col,
"detail": f"Row {row_num} [{col}] is empty",
}
)
for col in df.columns:
if pd.api.types.is_numeric_dtype(df[col]):
negative_rows = df[df[col] < 0]
if not negative_rows.empty:
for idx in negative_rows.index[:5]:
row_num = idx + 2
value = df.loc[idx, col]
anomalies.append(
{
"type": "Negative value",
"row": row_num,
"col": col,
"value": float(value),
"detail": f"Row {row_num} [{col}] = {value} (negative)",
}
)
id_col = find_first_matching_column(df, ID_TERMS)
if id_col:
dup_ids = df[df[id_col].duplicated(keep=False)]
dup_ids = dup_ids[dup_ids[id_col].notnull()]
if not dup_ids.empty:
for idx in dup_ids.index[:5]:
row_num = idx + 2
value = df.loc[idx, id_col]
anomalies.append(
{
"type": "Duplicate ID",
"row": row_num,
"col": id_col,
"value": str(value),
"detail": f"Row {row_num} [{id_col}] = {value} is duplicated",
}
)
for col in df.columns:
if column_matches(col, DATE_TERMS):
try:
parsed = pd.to_datetime(df[col], errors="coerce")
future_rows = df[parsed > pd.Timestamp(datetime.now() + timedelta(days=1))]
if not future_rows.empty:
for idx in future_rows.index[:3]:
row_num = idx + 2
value = df.loc[idx, col]
anomalies.append(
{
"type": "Date anomaly",
"row": row_num,
"col": col,
"value": str(value),
"detail": f"Row {row_num} [{col}] = {value} is in the future",
}
)
except Exception:
pass
for col in df.columns:
if pd.api.types.is_numeric_dtype(df[col]) and df[col].max() > 0:
q99 = df[col].quantile(0.99)
if q99 > 0 and df[col].max() > q99 * 10:
outliers = df[df[col] > q99 * 10]
for idx in outliers.index[:3]:
row_num = idx + 2
value = df.loc[idx, col]
anomalies.append(
{
"type": "Outlier",
"row": row_num,
"col": col,
"value": float(value),
"detail": (
f"Row {row_num} [{col}] = {value} is far outside the "
"normal range"
),
}
)
return anomalies
def column_matches(column_name: str, terms: list[str]) -> bool:
text = str(column_name).lower()
return any(term.lower() in text for term in terms)
def find_first_matching_column(df: pd.DataFrame, terms: list[str]) -> Optional[str]:
for col in df.columns:
if column_matches(str(col), terms):
return str(col)
return None
def _extract_number(pattern: re.Pattern[str], query: str) -> Optional[int]:
match = pattern.search(query)
if not match:
return None
for group in match.groups():
if group:
return int(group)
return None
def parse_natural_query(df: pd.DataFrame, query: str) -> dict:
"""Parse a natural-language instruction into an action plan."""
q = query.strip()
q_lower = q.lower()
overdue_days = _extract_number(
re.compile(
r"(?:more than|over)\s*(\d+)\s*days?|"
r"\u8d85\u8fc7?(\d+)[\u5929\u65e5]"
),
q_lower,
)
if overdue_days is not None and contains_any(q_lower, FOLLOW_UP_TERMS):
date_col = find_date_column(df)
if date_col:
return {
"action": "query",
"description": f"Records without follow-up for more than {overdue_days} days",
"date_col": date_col,
"days": overdue_days,
}
threshold = _extract_number(
re.compile(
r"(?:greater than|more than|above)\s*(\d+)|"
r"\u91d1\u989d[\u662f\u4e3a\u5927\u4e8e\u7ea6]+(\d+)|"
r"\u5927\u4e8e?(\d+)"
),
q_lower,
)
if threshold is not None and contains_any(q_lower, AMOUNT_TERMS):
amount_col = find_amount_column(df)
if amount_col:
return {
"action": "query",
"description": f"{amount_col} greater than {threshold}",
"amount_col": amount_col,
"threshold": threshold,
}
status_match = re.search(
r"(?:status|level|type|tier)\s*(?:is|=)\s*([^\s,.!?]+)|"
r"[\u662f\u4e3a]([^\uff0c,\u3002\uff01]+)",
q,
re.IGNORECASE,
)
if status_match and contains_any(q_lower, STATUS_TERMS):
keyword = next(group for group in status_match.groups() if group).strip()
for col in df.columns:
if column_matches(str(col), STATUS_TERMS):
return {
"action": "query",
"description": f"{col} contains {keyword}",
"filter_col": str(col),
"filter_value": keyword,
}
if contains_any(q_lower, COUNT_TERMS):
amount_col = find_amount_column(df)
if amount_col:
return {
"action": "aggregate",
"amount_col": amount_col,
"description": "Numeric summary",
}
return {"action": "count", "description": "Record count"}
if contains_any(q_lower, REPORT_TERMS):
report_type = "weekly" if ("week" in q_lower or "\u5468" in q_lower) else "daily"
return {
"action": "report",
"report_type": report_type,
"description": f"{report_type.title()} operations summary",
}
if contains_any(q_lower, ANOMALY_TERMS):
return {"action": "anomalies", "description": "Anomaly scan"}
row_match = re.search(
r"(?:row|line)\s*(\d+).*?(?:to|=)\s*([^\n,.!?]+)|"
r"\u7b2c(\d+)[\u884c\u4e2a].*?[\u6539\u6210\u53d8\u4e3a=]([^\uff0c,\u3002]+)",
q,
re.IGNORECASE,
)
if row_match and contains_any(q_lower, CHANGE_VERBS):
row_group = row_match.group(1) or row_match.group(3)
value_group = row_match.group(2) or row_match.group(4)
row_num = int(row_group)
new_value = value_group.strip()
col = find_target_column(q, df)
return {
"action": "preview_change",
"row": row_num,
"col": col,
"new_value": new_value,
}
return {"action": "info", "description": "Spreadsheet overview"}
def find_date_column(df: pd.DataFrame) -> Optional[str]:
return find_first_matching_column(df, DATE_TERMS)
def find_amount_column(df: pd.DataFrame) -> Optional[str]:
for col in df.columns:
if column_matches(str(col), AMOUNT_TERMS) and pd.api.types.is_numeric_dtype(df[col]):
return str(col)
return None
def find_target_column(query: str, df: pd.DataFrame) -> str:
"""Find the most likely target column for a change instruction."""
query_lower = query.lower()
for col in df.columns:
col_text = str(col)
if col_text.lower() in query_lower:
return col_text
for col in df.columns:
if df[col].dtype == "object":
return str(col)
return str(df.columns[0])
def execute_query(df: pd.DataFrame, query_plan: dict) -> pd.DataFrame:
"""Execute a structured query plan against a DataFrame."""
action = query_plan.get("action")
if action == "query":
if "date_col" in query_plan and "days" in query_plan:
date_col = query_plan["date_col"]
try:
parsed = pd.to_datetime(df[date_col], errors="coerce")
cutoff = datetime.now() - timedelta(days=query_plan["days"])
return df[parsed < pd.Timestamp(cutoff)].copy()
except Exception:
return df.head(0)
if "amount_col" in query_plan:
col = query_plan["amount_col"]
return df[df[col] > query_plan["threshold"]].copy()
if "filter_col" in query_plan:
col = query_plan["filter_col"]
value = query_plan["filter_value"]
return df[df[col].astype(str).str.contains(value, na=False)].copy()
return df.copy()
def build_change_preview(df: pd.DataFrame, row: int, col: str, new_value: str) -> dict:
"""Build a change preview without writing anything to disk."""
df_index = row - 2
if df_index < 0 or df_index >= len(df):
return {"error": f"Row {row} is out of range. The sheet has {len(df)} data rows."}
old_value = df.iloc[df_index][col]
return {
"file_modified": False,
"preview": {
"row": row,
"col": col,
"old_value": str(old_value),
"new_value": new_value,
},
}
def apply_change(df: pd.DataFrame, row: int, col: str, new_value: str) -> pd.DataFrame:
"""Apply a change to a DataFrame copy and return the modified copy."""
df_index = row - 2
updated = df.copy()
old_dtype = updated[col].dtype
try:
if old_dtype in ["int64", "int32"]:
updated.at[df_index, col] = int(new_value)
elif old_dtype in ["float64", "float32"]:
updated.at[df_index, col] = float(new_value)
else:
updated.at[df_index, col] = new_value
except (ValueError, TypeError):
updated.at[df_index, col] = new_value
return updated
def save_table(df: pd.DataFrame, file_path: Path) -> Path:
"""Save a DataFrame back to disk after creating a backup."""
backup_file(file_path)
suffix = file_path.suffix.lower()
if suffix == ".csv":
df.to_csv(file_path, index=False, encoding="utf-8")
else:
df.to_excel(file_path, index=False, engine="openpyxl")
return file_path
def generate_report(df: pd.DataFrame, report_type: str = "weekly") -> str:
"""Generate a lightweight daily or weekly operations summary."""
lines: list[str] = []
today = datetime.now()
if report_type == "weekly":
lines.append(f"📈 Operations Summary — {today.strftime('%B %Y')}")
else:
lines.append(f"📅 Daily Summary — {today.strftime('%Y-%m-%d')}")
lines.append("")
lines.append("[Overview]")
lines.append(f"- Total records: {len(df)}")
amount_col = find_amount_column(df)
if amount_col:
total = df[amount_col].sum()
avg = df[amount_col].mean()
lines.append(f"- {amount_col} total: {total:,.2f}")
lines.append(f"- {amount_col} average: {avg:,.2f}")
lines.append("")
str_cols = [col for col in df.columns if df[col].dtype == "object"]
if str_cols:
top_col = str_cols[0]
if df[top_col].nunique() <= 20:
lines.append("[Top distribution]")
for value, count in df[top_col].value_counts().head(5).items():
lines.append(f"- {value}: {count} records")
lines.append("")
null_total = int(df.isnull().sum().sum())
if null_total > 0:
lines.append("[Data quality]")
lines.append(f"- Missing values: {null_total}")
lines.append("")
anomalies = detect_anomalies(df)
if anomalies:
summary: dict[str, int] = {}
for anomaly in anomalies:
summary[anomaly["type"]] = summary.get(anomaly["type"], 0) + 1
lines.append("[Anomalies]")
for anomaly_type, count in summary.items():
lines.append(f"- {anomaly_type}: {count}")
lines.append("")
return "\n".join(lines).rstrip()
def format_query_result(df: pd.DataFrame, description: str = "") -> str:
"""Format query results as compact markdown-friendly text."""
if df.empty:
return "📭 No matching records found."
lines = [f"📊 Query results: {len(df)} record(s)"]
if description:
lines.append(f"({description})")
lines.append("")
lines.append(df_to_markdown(df.head(20)))
if len(df) > 20:
lines.append(f"\n... {len(df) - 20} more record(s) not shown")
return "\n".join(lines)
def format_anomalies(anomalies: list[dict]) -> str:
"""Format anomaly results for terminal or chat output."""
if not anomalies:
return "✅ No obvious anomalies found."
lines = [f"⚠️ Found {len(anomalies)} anomaly/anomalies:", ""]
for index, anomaly in enumerate(anomalies[:10], 1):
lines.append(f"{index}. {anomaly['detail']}")
if len(anomalies) > 10:
lines.append(f"\n... {len(anomalies) - 10} more anomaly/anomalies not shown")
return "\n".join(lines)
def format_table_info(info: dict) -> str:
"""Format spreadsheet metadata as readable text."""
lines = [
f"📋 Columns: {info['col_names']}",
f"📐 Size: {info['rows']} rows × {info['cols']} columns",
f"🏷️ Inferred type: {info['table_type']}",
]
if info["has_empty"]:
lines.append(f"⚠️ Missing values present: {sum(info['empty_counts'].values())}")
return "\n".join(lines)
def df_to_markdown(df: pd.DataFrame) -> str:
"""Render a DataFrame as a markdown table."""
cols = df.columns.tolist()
lines = [
"| " + " | ".join(str(col) for col in cols) + " |",
"| " + " | ".join(["---"] * len(cols)) + " |",
]
for _, row in df.iterrows():
values = []
for value in row:
rendered = str(value) if pd.notna(value) else "(empty)"
values.append(rendered[:30])
lines.append("| " + " | ".join(values) + " |")
return "\n".join(lines)
def main(args: list = None) -> None:
"""
Entry point.
Supported invocation styles:
1. CLI: python sheet_agent.py <file_path> <query>
2. JSON params: python sheet_agent.py '{"file":"~/x.csv","query":"..."}'
"""
if args is None:
args = sys.argv[1:]
if len(args) >= 1 and os.path.exists(os.path.expanduser(args[0])):
file_path = expand_path(args[0])
query = args[1] if len(args) > 1 else ""
action = "query"
else:
try:
params = json.loads(" ".join(args))
file_path = expand_path(params["file"])
query = params.get("query", "")
action = params.get("action", "query")
except Exception:
print("Usage: python sheet_agent.py <file_path> <query>")
print(' or: python sheet_agent.py \'{"file":"~/x.csv","query":"..."}\'')
sys.exit(1)
try:
df = read_table(file_path)
except Exception as exc:
print(f"[sheet-agent] Failed to read file: {exc}")
sys.exit(1)
if not query and action == "query":
info = get_table_info(df)
print(format_table_info(info))
print("")
print(format_anomalies(detect_anomalies(df)))
return
plan = parse_natural_query(df, query)
action = plan.get("action", "info")
if action == "info":
print(format_table_info(get_table_info(df)))
elif action == "query":
print(format_query_result(execute_query(df, plan), plan.get("description", "")))
elif action == "aggregate":
amount_col = plan.get("amount_col")
if amount_col:
print("📊 Numeric summary:")
print(f"- Total: {df[amount_col].sum():,.2f}")
print(f"- Average: {df[amount_col].mean():,.2f}")
print(f"- Max: {df[amount_col].max():,.2f}")
print(f"- Min: {df[amount_col].min():,.2f}")
elif action == "count":
print(f"📊 Total records: {len(df)}")
elif action == "report":
print(generate_report(df, plan.get("report_type", "weekly")))
elif action == "anomalies":
print(format_anomalies(detect_anomalies(df)))
elif action == "preview_change":
preview = build_change_preview(df, plan["row"], plan["col"], plan["new_value"])
if "error" in preview:
print(f"❌ {preview['error']}")
else:
details = preview["preview"]
print("📝 Proposed change preview:")
print(f"Row: {details['row']}")
print(f"Field: {details['col']}")
print(f"Old value: {details['old_value']}")
print(f"New value: {details['new_value']}")
print("")
print('⚠️ Confirm execution? Reply "confirm" or "cancel".')
anomalies = detect_anomalies(df)
if anomalies and action not in ["preview_change", "anomalies"]:
print("")
print(format_anomalies(anomalies))
if __name__ == "__main__":
main()
FILE:skill.json
{
"name": "sheet-agent",
"slug": "sheet-agent",
"version": "1.0.10",
"summary": "Spreadsheet operations agent for CSV and Excel files: inspect data, detect anomalies, answer business questions, generate reports, and preview edits safely.",
"description": "Spreadsheet operations agent for CSV and Excel files: inspect data, detect anomalies, answer business questions, generate reports, and preview edits safely.",
"author": "harrylabsj",
"license": "MIT",
"keywords": ["sheet", "excel", "csv", "spreadsheet", "operations", "anomaly-detection", "weekly-report", "data-analysis"],
"tags": ["spreadsheet", "data-analysis", "operations", "report-generation", "excel", "csv"],
"entry": "scripts/sheet_agent.py",
"python": true,
"capabilities": {
"read": ["csv", "excel"],
"write": ["csv", "excel"],
"analyze": true,
"report": true
}
}
FILE:tests/test_sheet_agent.py
#!/usr/bin/env python3
"""
sheet-agent test suite
Usage:
python test_sheet_agent.py
"""
import csv
import os
import sys
import tempfile
from pathlib import Path
import pandas as pd
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
def test_read_csv():
print("🧪 Test: read CSV")
with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False, encoding="utf-8") as handle:
writer = csv.DictWriter(handle, fieldnames=["Name", "Phone", "Follow Up Date", "Status"])
writer.writeheader()
writer.writerows(
[
{
"Name": "Alice",
"Phone": "13800001111",
"Follow Up Date": "2026-03-20",
"Status": "In progress",
},
{
"Name": "Bob",
"Phone": "13800002222",
"Follow Up Date": "2026-03-25",
"Status": "Pending",
},
]
)
csv_path = Path(handle.name)
from sheet_agent import get_table_info, read_table
df = read_table(csv_path)
assert len(df) == 2, f"Expected 2 rows, got {len(df)}"
info = get_table_info(df)
assert info["rows"] == 2
assert info["table_type"] == "Sales leads"
os.unlink(csv_path)
print(" ✅ Passed")
def test_read_excel():
print("🧪 Test: read Excel")
with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as handle:
xlsx_path = Path(handle.name)
pd.DataFrame(
[
{"Product": "Bluetooth Earbuds", "Inventory": 100, "Price": 199},
{"Product": "Phone Case", "Inventory": -5, "Price": 39},
]
).to_excel(xlsx_path, index=False, engine="openpyxl")
from sheet_agent import get_table_info, read_table
df = read_table(xlsx_path)
assert len(df) == 2
info = get_table_info(df)
assert info["table_type"] == "Inventory"
os.unlink(xlsx_path)
print(" ✅ Passed")
def test_anomaly_detection():
print("🧪 Test: anomaly detection")
with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False, encoding="utf-8") as handle:
writer = csv.DictWriter(handle, fieldnames=["Product", "Inventory", "Price", "Order ID"])
writer.writeheader()
writer.writerows(
[
{"Product": "Headphones", "Inventory": -5, "Price": 199, "Order ID": "A1"},
{"Product": "Phone Case", "Inventory": 50, "Price": "", "Order ID": "A2"},
{"Product": "Cable", "Inventory": 9999, "Price": 9, "Order ID": "A3"},
{"Product": "Cable", "Inventory": 20, "Price": 9, "Order ID": "A3"},
]
)
csv_path = Path(handle.name)
from sheet_agent import detect_anomalies
df = pd.read_csv(csv_path)
anomaly_types = [item["type"] for item in detect_anomalies(df)]
assert "Negative value" in anomaly_types, f"Expected a negative-value anomaly, got {anomaly_types}"
assert "Missing value" in anomaly_types, f"Expected a missing-value anomaly, got {anomaly_types}"
assert "Duplicate ID" in anomaly_types, f"Expected a duplicate-ID anomaly, got {anomaly_types}"
os.unlink(csv_path)
print(" ✅ Passed")
def test_natural_query_overdue():
print("🧪 Test: natural-language overdue query")
rows = [
{"Name": "Alice", "Follow Up Date": "2026-03-15", "Amount": 1000},
{"Name": "Bob", "Follow Up Date": "2026-03-28", "Amount": 2000},
{"Name": "Cara", "Follow Up Date": "2026-03-25", "Amount": 1500},
]
with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False, encoding="utf-8") as handle:
writer = csv.DictWriter(handle, fieldnames=rows[0].keys())
writer.writeheader()
writer.writerows(rows)
csv_path = Path(handle.name)
from sheet_agent import execute_query, parse_natural_query, read_table
df = read_table(csv_path)
plan = parse_natural_query(df, "Which leads have not been followed up for more than 3 days?")
assert plan["action"] == "query", f"Expected query action, got {plan['action']}"
assert plan["days"] == 3
result = execute_query(df, plan)
assert len(result) >= 1, f"Expected at least 1 overdue record, got {len(result)}"
os.unlink(csv_path)
print(" ✅ Passed")
def test_change_preview():
print("🧪 Test: change preview without writing")
rows = [
{"Name": "Alice", "Customer Tier": "Regular"},
{"Name": "Bob", "Customer Tier": "VIP"},
{"Name": "Cara", "Customer Tier": "Regular"},
]
with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False, encoding="utf-8") as handle:
writer = csv.DictWriter(handle, fieldnames=rows[0].keys())
writer.writeheader()
writer.writerows(rows)
csv_path = Path(handle.name)
from sheet_agent import build_change_preview, read_table
df = read_table(csv_path)
preview = build_change_preview(df, 2, "Customer Tier", "VIP")
assert "preview" in preview
assert preview["preview"]["old_value"] == "Regular"
assert preview["preview"]["new_value"] == "VIP"
os.unlink(csv_path)
print(" ✅ Passed")
def test_report_generation():
print("🧪 Test: report generation")
rows = [
{"Product": "Bluetooth Earbuds", "Quantity": 10, "Amount": 1990},
{"Product": "Phone Case", "Quantity": 5, "Amount": 195},
{"Product": "Charging Cable", "Quantity": 8, "Amount": 320},
]
with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False, encoding="utf-8") as handle:
writer = csv.DictWriter(handle, fieldnames=rows[0].keys())
writer.writeheader()
writer.writerows(rows)
csv_path = Path(handle.name)
from sheet_agent import generate_report, read_table
df = read_table(csv_path)
report = generate_report(df, "weekly")
assert "Operations Summary" in report or "Daily Summary" in report
assert "Total records" in report
os.unlink(csv_path)
print(" ✅ Passed")
def test_table_type_inference():
print("🧪 Test: table type inference")
from sheet_agent import infer_table_type
df_order = pd.DataFrame({"Order ID": ["A1"], "Amount": [100], "Date": ["2026-03-01"]})
assert infer_table_type(df_order) == "Orders"
df_inventory = pd.DataFrame({"Product": ["Earbuds"], "Inventory": [10], "Price": [199]})
assert infer_table_type(df_inventory) == "Inventory"
df_lead = pd.DataFrame({"Name": ["Alice"], "Phone": ["138"], "Follow Up Date": ["2026-03-01"]})
assert infer_table_type(df_lead) == "Sales leads"
print(" ✅ Passed")
def run_all():
print("=" * 50)
print("🧪 sheet-agent test suite")
print("=" * 50)
print()
tests = [
test_table_type_inference,
test_read_csv,
test_read_excel,
test_anomaly_detection,
test_natural_query_overdue,
test_change_preview,
test_report_generation,
]
passed = 0
failed = 0
for test in tests:
try:
test()
passed += 1
except Exception as exc:
print(f" ❌ Failed: {exc}")
import traceback
traceback.print_exc()
failed += 1
print()
print("=" * 50)
print(f"Result: {passed} passed, {failed} failed")
print("=" * 50)
return failed == 0
if __name__ == "__main__":
success = run_all()
sys.exit(0 if success else 1)
学习笔记 - Explore, search, and synthesize personal learning notes. Use when user asks about 笔记整理、学习回顾、知识搜索、笔记查询, or wants to explore their accumulated learning...
---
name: learning-notes-explorer
description: 学习笔记 - Explore, search, and synthesize personal learning notes. Use when user asks about 笔记整理、学习回顾、知识搜索、笔记查询, or wants to explore their accumulated learning notes across topics. An interface to personal knowledge base.
---
# Learning Notes Explorer (学习笔记)
## Overview
This skill provides an interface to explore, search, and synthesize personal learning notes. It helps users retrieve relevant notes, discover connections between past learnings, and build on existing knowledge. Acts as a bridge between raw note-taking and actionable knowledge.
## When to Use This Skill
- Looking for specific information from past notes
- Reviewing notes on a particular topic
- Finding connections between different notes/topics
- Synthesizing knowledge from multiple sources
- Preparing to write or create based on past learning
- Checking what has been learned about a topic
- Discovering gaps in knowledge
## Core Functions
### 1. Note Search and Retrieval
- Search notes by keyword, topic, or concept
- Find notes from specific sources (books, courses, articles)
- Filter by date, tags, or categories
- Locate specific quotes or data points
### 2. Knowledge Discovery
- Identify related notes across topics
- Find patterns in learning history
- Surface connections user may have forgotten
- Highlight gaps in knowledge on a topic
### 3. Synthesis
- Combine relevant notes into coherent summaries
- Cross-reference insights from different sources
- Generate overview of knowledge on a subject
- Create study guides from note collections
### 4. Learning Path Guidance
- Recommend next steps based on existing knowledge
- Suggest topics to explore
- Identify prerequisites for advanced learning
- Map out learning journey on a topic
## Note Organization Model
The explorer works with notes organized in layers:
```
Learning Notes
├── Raw Notes (原始笔记)
│ ├── Book highlights & annotations
│ ├── Course notes
│ ├── Article clippings
│ └── Meeting/lecture notes
│
├── Processed Notes (整理笔记)
│ ├── Summaries
│ ├── Key concepts extracted
│ └── Questions generated
│
└── Connected Notes (关联笔记)
├── Personal insights
├── Cross-references
└── Action items
```
## Usage Examples
### Search and Retrieval
```
"搜索我所有关于'刻意练习'的笔记"
"找出上周记录的读书笔记"
"查找关于Python的笔记"
"找出所有提到'第一性原理'的内容"
```
### Knowledge Review
```
"总结我关于时间管理的所有学习"
"给我看看最近一个月学了什么"
"我关于心理学都有哪些笔记?"
"显示投资相关的所有笔记"
```
### Synthesis and Connections
```
"把关于'学习'的笔记整理成一个主题"
"找出'刻意练习'和'成长型思维'的关联"
"生成一个关于'写作'的知识总结"
"创建'决策'主题的阅读清单"
```
### Gap Analysis
```
"关于AI领域我还缺什么知识?"
"我哪个方面学习得最少?"
"有哪些相关主题我没有探索过?"
```
## Output Formats
### Search Results
```
Found X notes on "[topic]":
1. [Note Title] - [Source] - [Date]
Preview: [First 2 lines]...
Tags: [tag1], [tag2]
2. [Note Title] - [Source] - [Date]
...
```
### Synthesis Summary
```
## Knowledge Synthesis: [Topic]
### Key Concepts Learned
- [Concept 1]: From [source], [summary]
- [Concept 2]: From [source], [summary]
### Sources
- [Book 1] - [Key insight]
- [Course 2] - [Key insight]
### Practical Applications
- [Application 1]
- [Application 2]
### Suggested Next Steps
- [ ] Explore [related topic]
- [ ] Read [suggested source]
```
### Learning Path
```
## Learning Path: [Topic]
Prerequisites (already learned):
✓ [Concept A]
✓ [Concept B]
Recommended Sequence:
1. [Next topic] - Based on [note reference]
2. [Advanced topic] - After completing #1
3. [Expert topic] - Further study
Suggested Resources:
- [Book/Course] - Fills gap in [area]
```
## Integration with Other Skills
This skill works with:
- `book-knowledge-extractor` — For extracting and saving new notes
- `second-brain-triage` — For organizing notes in personal system
- `summarize` — For creating summaries of note collections
- `zettelkasten-writing-coach` — For writing based on note connections
## Limitations
- Depends on quality of underlying note collection
- May not find notes if indexing is incomplete
- Cannot "read" non-text content in notes
- Synthesis quality depends on note richness
## Implementation Notes
This skill requires:
- Access to note storage (local files, Obsidian vault, Notion, etc.)
- Note indexing system for search
- Source attribution for synthesized content
- User preferences for display format
## Acceptance Criteria
1. ✓ Can search notes by keyword/topic
2. ✓ Returns relevant results with context
3. ✓ Can synthesize multiple notes into summary
4. ✓ Identifies connections between notes
5. ✓ Provides learning path recommendations
6. ✓ Integrates with note sources
7. ✓ Respects user privacy in note access
FILE:config.json
{"notes_dir": "/tmp/test-notes"}
FILE:handler.py
#!/usr/bin/env python3
"""
Learning Notes Explorer
Explore, search, and synthesize personal learning notes.
"""
import json
import os
import re
import sys
from dataclasses import dataclass, field
from typing import List, Dict, Any, Optional
from datetime import datetime
from pathlib import Path
@dataclass
class Note:
"""Learning note structure"""
id: str
title: str
content: str
source: str = ""
source_type: str = "general" # book, article, course, video, podcast
tags: List[str] = field(default_factory=list)
created_at: str = ""
modified_at: str = ""
related_notes: List[str] = field(default_factory=list)
@dataclass
class SearchResult:
"""Search result structure"""
note: Note
relevance_score: float
matched_sections: List[str]
class NotesIndex:
"""In-memory index for notes"""
def __init__(self):
self.notes: Dict[str, Note] = {}
self.tag_index: Dict[str, List[str]] = {}
self.word_index: Dict[str, List[str]] = {}
def add_note(self, note: Note):
"""Add a note to the index"""
self.notes[note.id] = note
# Index tags
for tag in note.tags:
tag_lower = tag.lower()
if tag_lower not in self.tag_index:
self.tag_index[tag_lower] = []
self.tag_index[tag_lower].append(note.id)
# Index words
words = self._extract_words(note.title + " " + note.content)
for word in words:
if word not in self.word_index:
self.word_index[word] = []
if note.id not in self.word_index[word]:
self.word_index[word].append(note.id)
def _extract_words(self, text: str) -> List[str]:
"""Extract searchable words from text"""
# Support both English and Chinese
words = []
# English words
words.extend(re.findall(r'[a-zA-Z]+', text.lower()))
# Chinese characters (as individual searchable tokens)
chinese_chars = re.findall(r'[\u4e00-\u9fff]', text)
words.extend(chinese_chars)
return words
def search(self, query: str, limit: int = 10) -> List[SearchResult]:
"""Search notes by query"""
query_lower = query.lower()
query_words = self._extract_words(query)
scores: Dict[str, float] = {}
matched_sections: Dict[str, List[str]] = {}
# Check for exact matches in title
for note_id, note in self.notes.items():
if query_lower in note.title.lower():
scores[note_id] = scores.get(note_id, 0) + 10
if note_id not in matched_sections:
matched_sections[note_id] = []
matched_sections[note_id].append(f"Title: {note.title}")
# Check for word matches
for word in query_words:
if word in self.word_index:
for note_id in self.word_index[word]:
scores[note_id] = scores.get(note_id, 0) + 1
# Check for tag matches
if query_lower in self.tag_index:
for note_id in self.tag_index[query_lower]:
scores[note_id] = scores.get(note_id, 0) + 5
# Check content for phrase matches
for note_id, note in self.notes.items():
if query_lower in note.content.lower():
scores[note_id] = scores.get(note_id, 0) + 3
# Find context
idx = note.content.lower().find(query_lower)
if idx >= 0:
start = max(0, idx - 50)
end = min(len(note.content), idx + len(query) + 50)
context = note.content[start:end]
if note_id not in matched_sections:
matched_sections[note_id] = []
matched_sections[note_id].append(f"...{context}...")
# Sort by score and create results
sorted_notes = sorted(scores.items(), key=lambda x: x[1], reverse=True)
results = []
for note_id, score in sorted_notes[:limit]:
results.append(SearchResult(
note=self.notes[note_id],
relevance_score=score,
matched_sections=matched_sections.get(note_id, [])
))
return results
def get_by_tag(self, tag: str) -> List[Note]:
"""Get all notes with a specific tag"""
tag_lower = tag.lower()
note_ids = self.tag_index.get(tag_lower, [])
return [self.notes[nid] for nid in note_ids if nid in self.notes]
def get_by_source_type(self, source_type: str) -> List[Note]:
"""Get notes by source type"""
return [n for n in self.notes.values() if n.source_type == source_type]
def get_recent(self, days: int = 30) -> List[Note]:
"""Get recently created/modified notes"""
# Simplified - return all notes sorted by created_at
notes_list = list(self.notes.values())
notes_list.sort(key=lambda x: x.created_at or "", reverse=True)
return notes_list
def find_connections(self, note_id: str) -> List[Dict[str, Any]]:
"""Find connections between notes"""
if note_id not in self.notes:
return []
note = self.notes[note_id]
connections = []
# Direct connections
for related_id in note.related_notes:
if related_id in self.notes:
connections.append({
"type": "direct",
"note": self.notes[related_id],
"reason": "Explicitly linked"
})
# Tag-based connections
for tag in note.tags:
for other_id in self.tag_index.get(tag.lower(), []):
if other_id != note_id and other_id not in note.related_notes:
connections.append({
"type": "tag",
"note": self.notes[other_id],
"reason": f"Shared tag: {tag}"
})
# Content similarity (simple word overlap)
note_words = set(self._extract_words(note.content))
for other_id, other in self.notes.items():
if other_id != note_id:
other_words = set(self._extract_words(other.content))
overlap = note_words & other_words
if len(overlap) >= 5: # At least 5 common words
connections.append({
"type": "similarity",
"note": other,
"reason": f"{len(overlap)} common concepts"
})
return connections
class NotesExplorer:
"""Main explorer class"""
def __init__(self, notes_dir: Optional[str] = None):
self.index = NotesIndex()
self.notes_dir = notes_dir or os.path.expanduser("~/.openclaw/notes")
self._load_notes()
def _load_notes(self):
"""Load notes from storage"""
# Load from JSON file if exists
notes_file = os.path.join(self.notes_dir, "notes.json")
if os.path.exists(notes_file):
try:
with open(notes_file, 'r', encoding='utf-8') as f:
data = json.load(f)
for item in data.get('notes', []):
note = Note(**item)
self.index.add_note(note)
except Exception:
pass
# If no notes loaded, add some demo notes
if not self.index.notes:
self._add_demo_notes()
def _add_demo_notes(self):
"""Add demo notes for testing"""
demo_notes = [
Note(
id="note_001",
title="Atomic Habits - Key Concepts",
content="""微习惯:小到不可能失败的行动。每天进步1%,一年后进步37倍。
身份认同是改变的核心。不要想着'我要戒烟',要想'我不是烟民'。
习惯的四步法:提示、渴望、反应、奖励。
两分钟规则:任何习惯都可以在2分钟内开始。""",
source="《原子习惯》by James Clear",
source_type="book",
tags=["habits", "self-improvement", "productivity", "习惯"],
created_at="2024-01-15"
),
Note(
id="note_002",
title="Deep Work - Focus Strategy",
content="""深度工作:在无干扰状态下专注进行职业活动的能力。
四种深度工作哲学:
1. 禁欲主义哲学 - 长期隔离
2. 双峰哲学 - 分阶段深度/浅度
3. 节奏哲学 - 固定时间深度工作
4. 记者哲学 - 随时进入深度状态
度量指标:深度工作时间 / 总工作时间""",
source="《深度工作》by Cal Newport",
source_type="book",
tags=["focus", "productivity", "work", "专注"],
created_at="2024-01-20",
related_notes=["note_001"]
),
Note(
id="note_003",
title="刻意练习 Principles",
content="""刻意练习的四个要素:
1. 明确目标:设定具体可衡量的子目标
2. 专注投入:全神贯注于任务
3. 即时反馈:立即知道对错
4. 走出舒适区:不断挑战更高难度
与天真的练习不同,刻意练习是有目的、有指导的练习。""",
source="《刻意练习》by Anders Ericsson",
source_type="book",
tags=["learning", "practice", "skill", "学习"],
created_at="2024-02-01"
),
Note(
id="note_004",
title="Learning How to Learn - Coursera",
content="""组块化:将信息打包成大脑易于处理的单元。
专注模式 vs 发散模式:
- 专注模式用于学习具体概念
- 发散模式用于创造性思考
番茄工作法:25分钟专注 + 5分钟休息
间隔重复:在遗忘曲线最佳点复习""",
source="Learning How to Learn Course",
source_type="course",
tags=["learning", "memory", "study", "学习"],
created_at="2024-02-10"
),
]
for note in demo_notes:
self.index.add_note(note)
def search(self, query: str, limit: int = 10) -> str:
"""Search notes and return formatted results"""
results = self.index.search(query, limit)
if not results:
return f"No notes found for '{query}'. Try different keywords or check your spelling."
lines = [f"## Search Results: '{query}'", ""]
lines.append(f"Found {len(results)} note(s):\n")
for i, result in enumerate(results, 1):
note = result.note
lines.append(f"{i}. **{note.title}**")
lines.append(f" Source: {note.source}")
lines.append(f" Type: {note.source_type}")
lines.append(f" Tags: {', '.join(note.tags)}")
# Show preview
preview = note.content[:150].replace('\n', ' ')
if len(note.content) > 150:
preview += "..."
lines.append(f" Preview: {preview}")
if result.matched_sections:
lines.append(f" Match: {result.matched_sections[0][:100]}...")
lines.append("")
return '\n'.join(lines)
def synthesize_topic(self, topic: str) -> str:
"""Synthesize knowledge on a topic"""
results = self.index.search(topic, limit=5)
if not results:
return f"No notes found for topic '{topic}'."
lines = [f"## Knowledge Synthesis: {topic}", ""]
# Key concepts
lines.append("### Key Concepts Learned")
for result in results:
note = result.note
lines.append(f"- **{note.title}** (from {note.source})")
# Extract first sentence or line as summary
summary = note.content.split('\n')[0][:100]
lines.append(f" - {summary}")
lines.append("")
# Sources
lines.append("### Sources")
sources = set(r.note.source for r in results)
for source in sources:
related = [r for r in results if r.note.source == source]
if related:
insight = related[0].note.content[:80].replace('\n', ' ')
lines.append(f"- **{source}** - {insight}...")
lines.append("")
# Connections
lines.append("### Connections Between Notes")
all_tags = set()
for r in results:
all_tags.update(r.note.tags)
if all_tags:
lines.append(f"Common themes: {', '.join(list(all_tags)[:5])}")
lines.append("")
# Suggested next steps
lines.append("### Suggested Next Steps")
lines.append(f"- [ ] Explore related topic: advanced {topic}")
lines.append(f"- [ ] Review and connect with other notes")
lines.append(f"- [ ] Create practical application from these insights")
return '\n'.join(lines)
def review_recent(self, days: int = 30) -> str:
"""Review recent notes"""
notes = self.index.get_recent(days)
lines = [f"## Recent Learning Activity (Last {days} days)", ""]
if not notes:
lines.append("No notes found in this time period.")
return '\n'.join(lines)
# Group by source type
by_type: Dict[str, List[Note]] = {}
for note in notes:
st = note.source_type
if st not in by_type:
by_type[st] = []
by_type[st].append(note)
lines.append(f"Total notes: {len(notes)}\n")
for source_type, type_notes in by_type.items():
lines.append(f"### {source_type.title()} ({len(type_notes)})")
for note in type_notes[:5]:
lines.append(f"- {note.title}")
preview = note.content[:80].replace('\n', ' ')
lines.append(f" {preview}...")
lines.append("")
# Topics covered
all_tags = set()
for note in notes:
all_tags.update(note.tags)
if all_tags:
lines.append("### Topics Covered")
lines.append(f"Tags: {', '.join(sorted(all_tags))}")
return '\n'.join(lines)
def explore_connections(self, note_title: str) -> str:
"""Explore connections for a specific note"""
# Find note by title
target_note = None
for note in self.index.notes.values():
if note_title.lower() in note.title.lower():
target_note = note
break
if not target_note:
return f"Note '{note_title}' not found."
connections = self.index.find_connections(target_note.id)
lines = [f"## Connections: {target_note.title}", ""]
lines.append(f"**Source:** {target_note.source}")
lines.append(f"**Tags:** {', '.join(target_note.tags)}\n")
if not connections:
lines.append("No connections found for this note.")
return '\n'.join(lines)
# Group by type
by_type: Dict[str, List[Dict]] = {}
for conn in connections:
t = conn['type']
if t not in by_type:
by_type[t] = []
by_type[t].append(conn)
for conn_type, conns in by_type.items():
lines.append(f"### {conn_type.title()} Connections ({len(conns)})")
for conn in conns[:5]:
note = conn['note']
lines.append(f"- **{note.title}**")
lines.append(f" Reason: {conn['reason']}")
preview = note.content[:60].replace('\n', ' ')
lines.append(f" Preview: {preview}...")
lines.append("")
return '\n'.join(lines)
def learning_path(self, topic: str) -> str:
"""Generate a learning path for a topic"""
related_notes = self.index.search(topic, limit=10)
lines = [f"## Learning Path: {topic}", ""]
# What we already know
lines.append("### Prerequisites (Already Learned)")
if related_notes:
for result in related_notes[:5]:
lines.append(f"✓ {result.note.title}")
else:
lines.append("No existing notes found. Starting from scratch.")
lines.append("")
# Recommended sequence
lines.append("### Recommended Learning Sequence")
steps = [
f"1. Foundational concepts in {topic}",
f"2. Core principles and frameworks",
f"3. Practical applications and case studies",
f"4. Advanced topics and edge cases",
f"5. Integration with existing knowledge"
]
for step in steps:
lines.append(step)
lines.append("")
# Suggested resources
lines.append("### Suggested Resources")
# Based on existing notes, suggest similar
sources = set()
for r in related_notes:
sources.add(r.note.source_type)
if 'book' in sources:
lines.append("- Recommended book on this topic")
if 'course' in sources:
lines.append("- Online course for structured learning")
lines.append("- Practice exercises to apply concepts")
lines.append("- Community discussions for deeper insights")
return '\n'.join(lines)
def main():
"""Main entry point"""
explorer = NotesExplorer()
if len(sys.argv) < 2:
# Interactive mode or show help
print("Usage: handler.py <command> [args]")
print("")
print("Commands:")
print(" search <query> - Search notes by keyword")
print(" topic <topic> - Synthesize knowledge on a topic")
print(" recent [days] - Review recent notes (default 30 days)")
print(" connections <title> - Explore connections for a note")
print(" path <topic> - Generate learning path")
print("")
print("Examples:")
print(' handler.py search "habits"')
print(' handler.py topic "刻意练习"')
print(' handler.py recent 7')
sys.exit(0)
command = sys.argv[1].lower()
if command == "search":
query = " ".join(sys.argv[2:]) if len(sys.argv) > 2 else ""
if not query:
print("Error: Search query required")
sys.exit(1)
result = explorer.search(query)
print(result)
elif command == "topic":
topic = " ".join(sys.argv[2:]) if len(sys.argv) > 2 else ""
if not topic:
print("Error: Topic required")
sys.exit(1)
result = explorer.synthesize_topic(topic)
print(result)
elif command == "recent":
days = int(sys.argv[2]) if len(sys.argv) > 2 else 30
result = explorer.review_recent(days)
print(result)
elif command == "connections":
title = " ".join(sys.argv[2:]) if len(sys.argv) > 2 else ""
if not title:
print("Error: Note title required")
sys.exit(1)
result = explorer.explore_connections(title)
print(result)
elif command == "path":
topic = " ".join(sys.argv[2:]) if len(sys.argv) > 2 else ""
if not topic:
print("Error: Topic required")
sys.exit(1)
result = explorer.learning_path(topic)
print(result)
else:
# Default to search
query = " ".join(sys.argv[1:])
result = explorer.search(query)
print(result)
if __name__ == "__main__":
main()
FILE:skill.json
{
"name": "learning-notes-explorer",
"description": "学习笔记 - Explore, search, and synthesize personal learning notes. Use when user asks about 笔记整理、学习回顾、知识搜索、笔记查询, or wants to explore their accumulated learning notes across topics. An interface to personal knowledge base.",
"version": "1.0.0",
"author": "Golden Bean",
"entry": "handler.py",
"language": "python",
"input": {
"type": "text",
"description": "Search query, topic, or command for exploring notes"
},
"output": {
"type": "structured",
"format": "markdown"
},
"tags": ["learning", "notes", "knowledge", "search", "review"],
"related_skills": ["book-knowledge-extractor", "summarize", "second-brain-triage", "zettelkasten-writing-coach"]
}
代码审查清单 - A comprehensive code review checklist and guidance tool. Use when user asks about 代码审查、代码检查、PR review、代码质量, or wants to conduct or prepare for a cod...
---
name: code-review-checklist
description: 代码审查清单 - A comprehensive code review checklist and guidance tool. Use when user asks about 代码审查、代码检查、PR review、代码质量, or wants to conduct or prepare for a code review. Provides systematic checklist items and best practices.
---
# Code Review Checklist (代码审查清单)
## Overview
This skill provides a systematic approach to code reviews. It offers comprehensive checklist items across multiple dimensions of code quality, helps reviewers focus on high-impact areas, and guides developers in preparing code for review. Designed to make code reviews more efficient and thorough.
## When to Use This Skill
- Preparing code for pull request review
- Conducting a code review as a reviewer
- Self-reviewing own code before submission
- Establishing code review standards for a team
- Training new developers on review best practices
- Auditing code quality in a codebase
## What This Skill Provides
### 1. Predefined Checklists
Comprehensive checklist items organized by category:
- Code correctness and logic
- Code style and readability
- Performance and efficiency
- Security considerations
- Error handling
- Testing coverage
- Documentation
- Architecture and design patterns
### 2. Review Guidance
- What to look for in each category
- Red flags and common issues
- Best practices specific to language/framework
- Questions to ask the author
### 3. Review Workflow
- Systematic approach to reviewing
- Priority ordering of checks
- Time allocation guidance
- Documentation requirements
## Checklist Categories
### 1. Correctness & Logic
- [ ] Code produces expected output
- [ ] Edge cases are handled
- [ ] No off-by-one errors
- [ ] Logic is sound and complete
- [ ] No infinite loops or recursion issues
- [ ] Proper use of data structures
### 2. Code Style & Readability
- [ ] Follows project coding standards
- [ ] Naming is clear and descriptive
- [ ] Functions are appropriately sized
- [ ] Code is not duplicated (DRY principle)
- [ ] Complex logic has comments
- [ ] Formatting is consistent
### 3. Performance & Efficiency
- [ ] No unnecessary loops or iterations
- [ ] Proper use of caching when applicable
- [ ] Database queries are optimized
- [ ] No memory leaks
- [ ] Appropriate algorithmic complexity
- [ ] Resources are properly released
### 4. Security
- [ ] Input validation on all user inputs
- [ ] No SQL injection vulnerabilities
- [ ] No XSS vulnerabilities
- [ ] Secrets not hardcoded
- [ ] Proper authentication/authorization
- [ ] Sensitive data properly protected
- [ ] No security misconfigurations
### 5. Error Handling
- [ ] Errors are caught and handled appropriately
- [ ] Error messages are user-friendly
- [ ] No empty catch blocks
- [ ] Logging is appropriate
- [ ] Graceful degradation where needed
- [ ] No exposing internal error details
### 6. Testing
- [ ] Unit tests exist for new code
- [ ] Tests cover happy path and edge cases
- [ ] Tests are maintainable
- [ ] Mock usage is appropriate
- [ ] Test coverage meets requirements
- [ ] No flaky tests introduced
### 7. Documentation
- [ ] Public APIs are documented
- [ ] Complex logic has comments
- [ ] README updated if needed
- [ ] API changes are documented
- [ ] Breaking changes are noted
### 8. Architecture & Design
- [ ] Follows project architecture patterns
- [ ] Single Responsibility Principle followed
- [ ] Dependencies are properly injected
- [ ] Coupling is minimized
- [ ] Changes are localized appropriately
- [ ] No tech debt introduced unnecessarily
## Language-Specific Considerations
### JavaScript/TypeScript
- Proper async/await usage
- TypeScript types are correct
- No 'any' type abuse
- ESLint rules followed
### Python
- PEP 8 compliance
- Type hints where appropriate
- Docstrings for public functions
- No deprecated imports
### Java
- Null safety considerations
- Resource management (try-with-resources)
- Stream API usage
- Concurrent access considerations
### Go
- Error handling conventions
- Goroutine leak prevention
- Context usage
- Naming conventions
## Review Workflow
### Step 1: Context (2-3 min)
- Read PR description and motivation
- Understand what changed and why
- Check related issues or docs
### Step 2: Overview (3-5 min)
- Scan changed files
- Identify high-risk areas
- Note files needing deep review
### Step 3: Detailed Review (15-30 min)
- Follow checklist by priority
- Comment on issues found
- Ask clarifying questions
- Suggest improvements
### Step 4: Summary (3-5 min)
- Summarize findings
- Categorize issues (Blocking/Suggestion/Question)
- Approve or request changes
## Usage Examples
### As a Reviewer
```
"用代码审查清单检查这个PR"
"帮我审查这个函数的逻辑"
"检查这段代码有没有安全问题"
"看看这个文件有哪些可以改进的地方"
```
### As a Developer
```
"帮我准备代码审查"
"自审查这份代码,有什么遗漏?"
"检查这段代码的测试覆盖"
"这个代码符合项目规范吗?"
```
### For Team Standards
```
"生成一个代码审查检查清单"
"我们团队的代码审查标准是什么?"
"前端代码审查有什么特殊要求?"
```
## Output Format
For each review, output:
```markdown
## Code Review: [PR/Change Title]
### Summary
- Files changed: X
- Lines added/removed: +X/-X
- Risk level: [Low/Medium/High]
### Findings
#### 🔴 Blocking Issues
- [Issue description] - [File:Line] - [Suggestion]
#### 🟡 Suggestions
- [Suggestion] - [File:Line]
#### 🟢 Good Practices Noted
- [Positive observation]
### Checklist Status
- [x] Correctness
- [x] Style
- [ ] Security (needs work)
- [x] Performance
### Recommendation
[Approve / Request Changes / Discuss]
### Action Items
- [ ] Item 1
- [ ] Item 2
```
## Integration with Development Workflow
This skill integrates with:
- `github` — For reviewing PRs directly
- `coding-agent` — For automated code quality checks
- `opencli` — For running linters and formatters
## Limitations
- Cannot execute code to verify correctness
- Cannot know full system context
- Best practices may vary by project
- Language-specific items may be incomplete for niche languages
## Acceptance Criteria
1. ✓ Provides comprehensive checklist coverage
2. ✓ Can customize for different languages/frameworks
3. ✓ Identifies common issues efficiently
4. ✓ Helps categorize issue severity
5. ✓ Provides actionable feedback
6. ✓ Saves time in review process
7. ✓ Helps developers learn and improve
FILE:handler.py
#!/usr/bin/env python3
"""Code Review Checklist Handler - Systematic code review guidance and checklists."""
import sys
import json
import re
def detect_intent(text):
"""Detect what type of code review help the user needs."""
text_lower = text.lower()
if any(k in text_lower for k in ["清单", "checklist", "列表", "项目"]):
return "checklist"
elif any(k in text_lower for k in ["审查", "review", "检查"]):
return "review"
elif any(k in text_lower for k in ["安全", "security", "漏洞"]):
return "security"
elif any(k in text_lower for k in ["性能", "performance", "优化"]):
return "performance"
elif any(k in text_lower for k in ["测试", "test", "coverage"]):
return "testing"
elif any(k in text_lower for k in ["前端", "frontend", "react", "vue", "css"]):
return "frontend"
elif any(k in text_lower for k in ["后端", "backend", "api", "server"]):
return "backend"
elif any(k in text_lower for k in ["python"]):
return "python"
elif any(k in text_lower for k in ["javascript", "typescript", "js", "ts"]):
return "javascript"
elif any(k in text_lower for k in ["java"]):
return "java"
elif any(k in text_lower for k in ["go", "golang"]):
return "go"
elif any(k in text_lower for k in ["工作流", "workflow", "流程", "步骤"]):
return "workflow"
elif any(k in text_lower for k in ["准备", "prepare", "自审查"]):
return "prepare"
elif any(k in text_lower for k in ["标准", "standard", "规范"]):
return "standards"
elif any(k in text_lower for k in ["问题", "issue", "blocking"]):
return "issues"
else:
return "general"
def get_full_checklist():
"""Return the complete code review checklist."""
return """
## Code Review Checklist (Complete)
### 1. Correctness and Logic
- [ ] Code produces expected output for all cases
- [ ] Edge cases are properly handled
- [ ] No off-by-one errors in loops/arrays
- [ ] Logic is sound and complete
- [ ] No infinite loops or recursion issues
- [ ] Proper use of data structures
- [ ] Null/undefined checks where needed
### 2. Code Style and Readability
- [ ] Follows project coding standards
- [ ] Naming is clear and descriptive
- [ ] Functions are appropriately sized (not too long)
- [ ] Code is not duplicated (DRY principle)
- [ ] Complex logic has explanatory comments
- [ ] Formatting is consistent
- [ ] No commented-out dead code
- [ ] Import statements are organized
### 3. Performance and Efficiency
- [ ] No unnecessary loops or iterations
- [ ] Proper use of caching when applicable
- [ ] Database queries are optimized (no N+1)
- [ ] No memory leaks
- [ ] Appropriate algorithmic complexity
- [ ] Resources are properly released
- [ ] Lazy loading where appropriate
### 4. Security
- [ ] Input validation on all user inputs
- [ ] No SQL injection vulnerabilities
- [ ] No XSS vulnerabilities
- [ ] No hardcoded secrets/passwords/API keys
- [ ] Proper authentication/authorization
- [ ] Sensitive data properly protected
- [ ] HTTPS used where needed
- [ ] No security misconfigurations
- [ ] Dependencies are up-to-date
### 5. Error Handling
- [ ] Errors are caught and handled appropriately
- [ ] Error messages are user-friendly
- [ ] No empty catch blocks
- [ ] Logging is appropriate (not too much/too little)
- [ ] Graceful degradation where needed
- [ ] No exposing internal error details to users
- [ ] Errors are logged with context
### 6. Testing
- [ ] Unit tests exist for new code
- [ ] Tests cover happy path
- [ ] Tests cover edge cases
- [ ] Tests are maintainable
- [ ] Mock usage is appropriate
- [ ] Test coverage meets requirements
- [ ] No flaky tests introduced
- [ ] Integration tests cover flows
### 7. Documentation
- [ ] Public APIs are documented
- [ ] Complex logic has comments
- [ ] README updated if needed
- [ ] API changes are documented
- [ ] Breaking changes are noted
- [ ] Deployment steps documented
### 8. Architecture and Design
- [ ] Follows project architecture patterns
- [ ] Single Responsibility Principle followed
- [ ] Dependencies are properly injected
- [ ] Coupling is minimized
- [ ] Changes are localized appropriately
- [ ] No unnecessary tech debt introduced
- [ ] Reusability considered
- [ ] API design is RESTful/consistent
### 9. Git and Process
- [ ] Commit messages are meaningful
- [ ] PR description is clear
- [ ] Related tests are included
- [ ] No debug/console statements
- [ ] No large files committed
- [ ] Sensitive data not committed
"""
def get_category_checklist(category):
"""Return a specific category checklist."""
categories = {
"security": """
## Security Checklist
- [ ] Input validation on all user inputs
- [ ] No SQL injection vulnerabilities (use parameterized queries)
- [ ] No XSS vulnerabilities (escape output, use CSP headers)
- [ ] No hardcoded secrets/passwords/API keys
- [ ] Proper authentication checks
- [ ] Proper authorization checks (RBAC/permissions)
- [ ] Sensitive data encrypted at rest
- [ ] Sensitive data encrypted in transit (HTTPS)
- [ ] No security misconfigurations
- [ ] Dependencies are secure and up-to-date
- [ ] Rate limiting on public endpoints
- [ ] No sensitive data in logs
- [ ] CORS configured correctly
- [ ] CSRF tokens where applicable
""",
"performance": """
## Performance Checklist
- [ ] No N+1 query problems
- [ ] Database indexes on queried columns
- [ ] Queries use appropriate indexes
- [ ] No unnecessary loops
- [ ] Lazy loading used where appropriate
- [ ] Caching implemented for expensive operations
- [ ] No memory leaks
- [ ] Connections properly pooled/released
- [ ] Large data sets processed in batches
- [ ] No blocking operations on main thread
- [ ] Compression enabled for large responses
- [ ] Images/assets optimized
- [ ] CDN used for static assets
- [ ] Algorithm complexity reviewed (O(n squared) to O(n))
- [ ] No synchronous file I/O in request path
""",
"testing": """
## Testing Checklist
### Unit Tests
- [ ] New code has unit tests
- [ ] Happy path is covered
- [ ] Edge cases are covered
- [ ] Error cases are tested
- [ ] Tests are isolated
- [ ] Tests are deterministic (no flakiness)
### Test Quality
- [ ] Test names are descriptive
- [ ] Tests follow AAA pattern (Arrange-Act-Assert)
- [ ] No test logic duplication
- [ ] Test data is realistic
- [ ] Assertions are specific
### Coverage
- [ ] Coverage meets minimum threshold
- [ ] Critical paths have high coverage
- [ ] No untested critical logic
### Integration
- [ ] Key user flows have integration tests
- [ ] API endpoints are tested
- [ ] Database interactions are tested
""",
"frontend": """
## Frontend Code Review Checklist
### React/Vue/Angular
- [ ] Components are properly separated
- [ ] State management is appropriate
- [ ] No prop drilling
- [ ] Hooks used correctly
- [ ] Memory leaks avoided (event listeners cleaned up)
- [ ] Error boundaries used
### JavaScript/TypeScript
- [ ] Types are properly defined (no 'any' abuse)
- [ ] No console.log/debug statements
- [ ] Promises handled properly
- [ ] Async/await used correctly
- [ ] No blocking main thread
- [ ] Event handlers properly cleaned up
### CSS/Styling
- [ ] CSS is organized
- [ ] No inline styles (unless dynamic)
- [ ] Responsive design considerations
- [ ] No duplicate styles
- [ ] CSS variables used for theming
### Accessibility
- [ ] Semantic HTML used
- [ ] Alt text on images
- [ ] Keyboard navigation works
- [ ] Focus states visible
- [ ] Color contrast sufficient
- [ ] ARIA attributes used appropriately
""",
"backend": """
## Backend Code Review Checklist
### API Design
- [ ] RESTful conventions followed
- [ ] Consistent naming
- [ ] Proper HTTP methods used
- [ ] Status codes are appropriate
- [ ] Pagination implemented for lists
- [ ] API versioning considered
### Data Handling
- [ ] Input validation
- [ ] Output sanitization
- [ ] Transactions used for related operations
- [ ] Database connections pooled
- [ ] Connection strings not hardcoded
### Business Logic
- [ ] Logic is in service layer (not controller)
- [ ] Business rules are centralized
- [ ] Validation is consistent
- [ ] Error handling is consistent
- [ ] Logging is appropriate
### Scalability
- [ ] Stateless where possible
- [ ] Caching strategy defined
- [ ] Queue for async processing
- [ ] Rate limiting considered
""",
"python": """
## Python Code Review Checklist
### Style (PEP 8)
- [ ] Follows PEP 8 guidelines
- [ ] Import ordering is correct
- [ ] Naming conventions followed
- [ ] Line length under 120 chars
### Type Hints
- [ ] Function arguments typed
- [ ] Return types annotated
- [ ] Complex types handled (List[int], Optional[str])
### Best Practices
- [ ] List/dict comprehensions used appropriately
- [ ] Context managers used (with statements)
- [ ] No bare except clauses
- [ ] F-strings used (not old percent formatting)
- [ ] Type checking with mypy
### Documentation
- [ ] Docstrings for public functions
- [ ] Module docstring at top
- [ ] Complex logic explained
- [ ] README updated if needed
""",
"javascript": """
## JavaScript/TypeScript Code Review Checklist
### TypeScript
- [ ] Strict mode enabled
- [ ] No 'any' type without justification
- [ ] Interfaces/types properly defined
- [ ] Generics used correctly
- [ ] Enums used for fixed sets
### Async
- [ ] Promises handled correctly
- [ ] Async/await used properly
- [ ] Error handling in async code
- [ ] No missing awaits
### Best Practices
- [ ] Const used where values do not change
- [ ] No console.log left in code
- [ ] No eval() used
- [ ] 'use strict' or ES modules
""",
"java": """
## Java Code Review Checklist
### Best Practices
- [ ] Try-with-resources used
- [ ] Stream API used appropriately
- [ ] Optional handling correct
- [ ] Null safety considered
- [ ] Immutability where appropriate
### Concurrency
- [ ] Thread-safe collections used
- [ ] Synchronization proper
- [ ] No race conditions
- [ ] ExecutorService properly managed
### Spring (if applicable)
- [ ] @Transactional used correctly
- [ ] @Valid for input validation
- [ ] No @Autowired on fields (use constructor)
""",
"go": """
## Go Code Review Checklist
### Conventions
- [ ] Error handling follows Go idioms
- [ ] ctx passed through call chain
- [ ] Naming follows Go conventions
- [ ] Error wrapping with percent-w
### Concurrency
- [ ] Goroutines properly cleaned up
- [ ] No goroutine leaks
- [ ] Mutexes used correctly
- [ ] Channels used appropriately
- [ ] WaitGroups/Context for lifecycle
### Best Practices
- [ ] No panics in production code
- [ ] Deferred called in all paths
- [ ] Slices pre-allocated when size known
- [ ] Strings.Builder for concatenation
"""
}
return categories.get(category, get_full_checklist())
def get_review_workflow():
"""Return the recommended review workflow."""
return """
## Code Review Workflow
### Step 1: Context (2-3 min)
Before diving into code:
1. Read PR description and motivation
2. Understand what changed and why
3. Check related issues or documentation
4. Note any related code in the codebase
### Step 2: Overview (3-5 min)
Get the big picture:
1. Scan all changed files
2. Identify high-risk areas
3. Note files needing deep review
4. Consider impact on other parts
### Step 3: Detailed Review (15-30 min)
Follow the checklist by priority:
**Must Fix (Blocking)**:
1. Correctness - does it work?
2. Security - is it safe?
3. Data - no corruption?
**Should Fix (Important)**:
4. Performance issues
5. Error handling
6. Test coverage
**Nice to Have (Suggestions)**:
7. Code style
8. Documentation
9. Code organization
For each issue:
- Comment with specific location (file:line)
- Explain the problem clearly
- Suggest a fix
- Use prefixes: blocking, suggestion, question
### Step 4: Summary (3-5 min)
Wrap up:
1. Summarize all findings
2. Categorize issues by severity
3. List what looks good
4. State recommendation
### Recommendation Options
- **Approve** - Ready to merge
- **Request Changes** - Needs fixes before merge
- **Comment** - Discussion, no blocking issues
- **Approve with Suggestions** - Minor nits, author's call
## Time Allocation
| PR Size | Time | Focus |
|---------|------|-------|
| Small (under 100 lines) | 10-15 min | Quick scan, security, logic |
| Medium (100-500 lines) | 20-30 min | Full checklist |
| Large (500+ lines) | 45-60 min | Architectural impact, split review |
Tip: Reviews over 60 min? Ask to split into smaller PRs.
"""
def generate_review_output(intent, topic=None):
"""Generate appropriate output based on intent."""
if intent == "checklist":
return get_full_checklist()
elif intent == "workflow":
return get_review_workflow()
elif intent == "standards":
return """
## Code Review Standards
### For Your Team
**Establish clear expectations:**
1. **Response Time**
- First review: within 24 hours
- Follow-ups: within 4 hours
2. **PR Requirements**
- Description required
- Linked issue/ticket
- Screenshots for UI changes
- Test plan for complex changes
3. **Review Criteria**
- Correctness first
- Security always
- Performance for large data
- Style is important but not blocking
4. **Merge Requirements**
- At least 1 approval
- All checks passing
- No unresolved blocking issues
5. **What Reviewers Look For**
- Does it solve the problem?
- Is it secure?
- Will it scale?
- Is it maintainable?
"""
elif intent == "prepare":
return """
## Self-Review Before Submitting
Before requesting a review:
**1. Read your own diff**
Run: git diff HEAD~1 --stat
Then: git diff HEAD~1 (full diff)
**2. Run through this checklist:**
- [ ] Code does what it claims
- [ ] Edge cases handled
- [ ] Error handling in place
- [ ] Tests added/updated
- [ ] No debug code left
- [ ] No secrets committed
- [ ] Documentation updated
- [ ] PR description written
**3. Common issues to catch yourself:**
- console.log/debug statements
- TODO comments left in
- Hardcoded values
- Unused imports/variables
- Code that is commented out
- Missing null checks
- Wrong variable names
**4. Test locally:**
- [ ] Unit tests pass
- [ ] Linting passes
- [ ] Build succeeds
- [ ] Manual testing done
**5. Optimize diff size:**
- Keep PRs small (under 400 lines ideal)
- Group related changes
- Separate refactoring from feature changes
"""
elif intent == "issues":
return """
## Common Code Review Issues
### Blocking Issues (Fix Before Merge)
1. **Logic errors** - Code fails to do what it claims
2. **Security vulnerabilities** - SQL injection, XSS, etc.
3. **Data corruption** - Wrong data saved, lost updates
4. **Missing error handling** - Silent failures
5. **No authentication/authorization** - Access control issues
6. **Secrets committed** - API keys, passwords in code
### Important Issues (Should Fix)
1. **N+1 queries** - Performance killer
2. **Missing validation** - Bad input accepted
3. **Inconsistent error handling** - Confusing UX
4. **No tests** - Cannot verify correctness
5. **Memory leaks** - Long-running service issues
6. **Race conditions** - Concurrency bugs
### Suggestions (Nice to Improve)
1. **Code duplication** - DRY it up
2. **Poor naming** - Confusing readers
3. **Missing comments** - Complex logic unexplained
4. **Overly complex** - Hard to maintain
5. **No documentation** - API unclear
6. **Style inconsistencies** - Team standards
### Questions to Ask
- "What happens if X is null?"
- "What if the API call fails?"
- "How will this scale to 1M users?"
- "Is this change backward compatible?"
- "What are the edge cases?"
"""
elif intent in ["security", "performance", "testing", "frontend", "backend", "python", "javascript", "java", "go"]:
return get_category_checklist(intent)
else:
return """
## Code Review Checklist Tool
### Quick Commands
Full checklist:
code-review-checklist checklist
Specific category:
code-review-checklist security
code-review-checklist performance
code-review-checklist testing
code-review-checklist frontend
code-review-checklist backend
Language specific:
code-review-checklist python
code-review-checklist javascript
code-review-checklist java
code-review-checklist go
Workflow:
code-review-checklist workflow
Prepare for review:
code-review-checklist prepare
Team standards:
code-review-checklist standards
Common issues:
code-review-checklist issues
### When to Use
As a Reviewer:
- "Review this PR"
- "Check this code for security issues"
- "What should I look for in this code?"
As a Developer:
- "Help me prepare code for review"
- "Self-review my code"
- "What issues might a reviewer find?"
As a Team Lead:
- "Set up code review standards"
- "What are common issues?"
- "How should we structure reviews?"
"""
def main():
args = sys.argv[1:]
if not args or (len(args) == 1 and args[0] in ['help', '-h', '--help']):
print(generate_review_output("general"))
return
user_input = " ".join(args)
intent = detect_intent(user_input)
result = generate_review_output(intent)
print(result)
if __name__ == "__main__":
main()
FILE:skill.json
{
"name": "code-review-checklist",
"description": "A comprehensive code review checklist and guidance tool. Use when user asks about 代码审查、代码检查、PR review、代码质量, or wants to conduct or prepare for a code review. Provides systematic checklist items and best practices.",
"version": "1.0.0",
"author": "Harry",
"tags": ["code", "review", "quality", "checklist", "programming", "pr"],
"modules": {
"handler": "handler.py"
},
"entry": "handler.py"
}
论点审计 - An analytical tool that evaluates the logical structure, evidence quality, and persuasiveness of arguments. Use when user asks about 论点分析、论证评估、逻辑审查、观点...
---
name: argument-audit
description: 论点审计 - An analytical tool that evaluates the logical structure, evidence quality, and persuasiveness of arguments. Use when user asks about 论点分析、论证评估、逻辑审查、观点评估, or wants to analyze the strength and validity of an argument.
---
# Argument Audit (论点审计)
## Overview
This skill provides systematic analysis and evaluation of arguments. It helps users understand the logical structure of arguments, assess the quality of evidence, identify logical fallacies, and evaluate overall persuasiveness. Useful for critical thinking, debate preparation, writing analysis, and decision-making.
## When to Use This Skill
- Analyzing essays, articles, or opinion pieces
- Preparing for debates or discussions
- Evaluating business proposals or recommendations
- Reviewing academic arguments
- Self-checking own arguments before presenting
- Understanding propaganda or misleading content
## What This Skill Analyzes
### 1. Logical Structure
- **Argument identification** — Main claim vs. supporting points
- **Reasoning chains** — How conclusions follow from premises
- **Argument type** — Deductive, inductive, abductive reasoning
- **Structure flaws** — Missing links, unsupported assumptions
### 2. Evidence Quality
- **Source credibility** — Authority, expertise, potential bias
- **Evidence relevance** — Direct support vs. tangential
- **Evidence sufficiency** — Adequate support for claim
- **Data quality** — Sample size, methodology, recency
### 3. Logical Fallacies
- Ad hominem attacks
- Straw man arguments
- False dilemmas
- Slippery slope
- Circular reasoning
- Appeal to authority/emotion
- Hasty generalizations
- Post hoc ergo propter hoc
- Red herring
- Tu quoque
### 4. Persuasiveness Assessment
- Overall argument strength (1-10 scale)
- Emotional vs. rational appeal balance
- Counterargument consideration
- Balance and objectivity
## Audit Framework
| Dimension | Criteria | Questions |
|-----------|----------|-----------|
| **Clarity** | Is the main argument clear? | What exactly is being argued? |
| **Validity** | Does the reasoning hold? | Do conclusions follow from premises? |
| **Soundness** | Are premises true? | Is the evidence credible? |
| **Completeness** | Are key points covered? | What's missing? |
| **Fairness** | Are alternatives considered? | Are counterarguments addressed? |
## Workflow
1. **Input Analysis** — Parse the argument into components
2. **Structure Mapping** — Diagram the logical structure
3. **Evidence Review** — Assess each piece of supporting evidence
4. **Fallacy Check** — Scan for common logical fallacies
5. **Strength Rating** — Evaluate overall persuasiveness
6. **Improvement Suggestions** — Recommend ways to strengthen
## Usage Examples
### Analyzing Written Arguments
```
"帮我分析这篇文章的论点"
"这篇论文的论证有什么问题?"
"这个商业计划书的逻辑是否严密?"
```
### Debate Preparation
```
"帮我找出对方论点中的漏洞"
"如何反驳这个观点?"
"我的论点有什么薄弱环节?"
```
### Critical Evaluation
```
"这个说法有什么逻辑问题?"
"这个专家的观点可信吗?"
"这段内容有哪些隐藏的假设?"
```
## Output Format
For each audit, provide:
```
## Argument Summary
[1-2 sentence description of the main argument]
## Structure Analysis
[Diagram or breakdown of reasoning structure]
## Evidence Assessment
| Evidence | Source | Credibility | Relevance | Rating |
|----------|--------|-------------|-----------|--------|
| ... | ... | ... | ... | ... |
## Fallacy Detection
- [List any logical fallacies found with explanations]
## Strength Rating
- Clarity: X/10
- Validity: X/10
- Soundness: X/10
- Completeness: X/10
- Overall: X/10
## Recommendations
[Suggestions for strengthening the argument]
```
## Limitations
- Cannot access external sources to verify evidence claims
- Assessment is based on provided text only
- Context-dependent factors may not be fully captured
- Subjective elements exist in persuasiveness ratings
## Related Skills
- `summarize` — For summarizing argument content
- `cognitive-reframe` — For rephrasing arguments
- `decision-fatigue-reliever` — For simplifying complex arguments
FILE:handler.py
#!/usr/bin/env python3
"""Argument Audit Handler - 论点审计核心模块"""
import json
import sys
import re
# ===== 逻辑谬误库 =====
FALLACIES = {
"ad_hominem": {
"name": "人身攻击 (Ad Hominem)",
"description": "攻击提出论点的人而非论点本身",
"example": "你说得不对因为你是个坏人"
},
"straw_man": {
"name": "稻草人谬误 (Straw Man)",
"description": "歪曲或夸大对方观点再加以反驳",
"example": "对方说应该控烟,你说对方要禁所有工业"
},
"false_dilemma": {
"name": "虚假二分法 (False Dilemma)",
"description": "只给出两个极端选项,忽略中间可能",
"example": "要么支持我,要么支持敌人"
},
"slippery_slope": {
"name": "滑坡谬误 (Slippery Slope)",
"description": "假设一个小步骤必然导致极端后果",
"example": "如果允许同性恋结婚,就会导致人兽婚姻合法化"
},
"circular_reasoning": {
"name": "循环论证 (Circular Reasoning)",
"description": "用论点本身来支持论点",
"example": "这本书是经典因为它很经典"
},
"appeal_to_authority": {
"name": "诉诸权威 (Appeal to Authority)",
"description": "仅以权威身份而非证据支持论点",
"example": "某明星说这个产品好所以真的好"
},
"appeal_to_emotion": {
"name": "诉诸情感 (Appeal to Emotion)",
"description": "用情感而非逻辑来论证",
"example": "想想那些受苦的孩子,你应该支持这个政策"
},
"hasty_generalization": {
"name": "草率概括 (Hasty Generalization)",
"description": "以少量案例得出普遍结论",
"example": "我遇到的东北人都很豪爽,所以东北人都豪爽"
},
"post_hoc": {
"name": "事后归因 (Post Hoc Ergo Propter Hoc)",
"description": "仅凭时间先后认定因果",
"example": "公鸡叫后天亮了,所以是公鸡叫醒了太阳"
},
"red_herring": {
"name": "转移话题 (Red Herring)",
"description": "引入无关话题转移注意力",
"example": "讨论是否应该加薪时说'公司还很困难'"
},
"bandwagon": {
"name": "从众谬误 (Bandwagon)",
"description": "以多数人意见为正确依据",
"example": "大家都这么做,所以是对的"
},
"tu_quoque": {
"name": "你也一样 (Tu Quoque)",
"description": "以对方的类似行为反驳论点",
"example": "你自己也这么做,凭什么说我"
},
"no_true_scotsman": {
"name": "特殊抗辩 (No True Scotsman)",
"description": "以定义排除反例来保护论点",
"example": "真正的基督徒不会这样做,所以这个人不是真正的基督徒"
}
}
def detect_fallacies(text):
"""检测文本中的逻辑谬误"""
detected = []
text_lower = text.lower()
sentences = re.split(r'[。!?\n]', text)
# 人身攻击
ad_hominem_kws = ["你这种人", "你当然", "你懂什么", "你算什么东西", "不读书", "脑残", "蠢货", "人渣", "弱智"]
if any(kw in text_lower for kw in ad_hominem_kws):
detected.append("ad_hominem")
# 稻草人
if any(kw in text_lower for kw in ["你的意思是", "你说要", "你主张", "你认为"]) and \
any(kw in text_lower for kw in ["就等于是", "就是要", "就是", "就是说"]):
detected.append("straw_man")
# 虚假二分法(增强:需要有"要么"或"只能二选一"类结构)
false_dilemma_kws = ["要么", "不然就", "二选一", "只能", "不是...就是", "没有中间路线"]
if any(kw in text_lower for kw in false_dilemma_kws):
detected.append("false_dilemma")
# 滑坡(增强)
slippery_kws_first = ["一旦", "如果允许", "如果", "一旦", "从"]
slippery_kws_second = ["必然导致", "最终会", "最后必然", "最终必然", "必将", "无可挽回", "一发不可收拾", "就会导致"]
if any(kw1 in text_lower for kw1 in slippery_kws_first):
for kw2 in slippery_kws_second:
if kw2 in text_lower:
detected.append("slippery_slope")
break
# 循环论证
for s in sentences:
s_stripped = s.strip()
if len(s_stripped) > 5:
words = re.findall(r'[\u4e00-\u9fa5a-zA-Z]+', s_stripped)
if len(words) >= 3:
if words[0] == words[-1]:
if "circular_reasoning" not in detected:
detected.append("circular_reasoning")
break
# 重复关键词出现多次
if len(words) > 0:
first_word = words[0]
# 出现3次以上则可能是循环
if sum(1 for w in words if w == first_word) >= 3 and len(set(words)) <= 3:
if "circular_reasoning" not in detected:
detected.append("circular_reasoning")
break
# 诉诸权威(增强:支持机构、奖项、头衔、审批机构)
authority_kws = ["专家", "权威", "教授", "科学家", "FDA", "WHO", "NMPA", "药监局",
"审批", "批准", "推荐", "代言", "名人", "明星", "网红"]
evidence_kws = ["数据", "研究", "统计", "证明", "实验", "临床试验", "双盲", "随机对照",
"表明", "显示", "显示", "证实", "发现"]
has_authority = any(kw in text_lower for kw in authority_kws)
has_evidence = any(kw in text_lower for kw in evidence_kws)
# 如果提到权威/审批/推荐但没有具体数据支撑,则为诉诸权威
if has_authority and not has_evidence:
# 特例:如果明确说"研究表明...专家说"则不算
if "说" in text_lower and "研究" in text_lower:
pass # 暂时不判
else:
detected.append("appeal_to_authority")
# 诉诸情感(增强)
emotion_kws = ["可怜", "悲惨", "令人心痛", "触目惊心", "无法想象", "难道你忍心",
"可悲", "令人惋惜", "令人愤怒", "多么可怕", "孩子的未来",
"老人的眼泪", "无辜的", "心痛", "无法接受", "不能容忍"]
if any(kw in text_lower for kw in emotion_kws):
detected.append("appeal_to_emotion")
# 草率概括(增强)
universal_kws = ["所有", "全部", "没有一个", "都是", "从不", "永远", "一切", "毫无例外",
"必然", "100%", "毫无例外", "无一例外", "没有一个"]
if any(kw in text_lower for kw in universal_kws):
detected.append("hasty_generalization")
# 事后归因
if any(kw in text_lower for kw in ["之后", "然后", "后来"]) and \
any(kw in text_lower for kw in ["于是", "所以", "导致", "造成"]):
detected.append("post_hoc")
# 从众谬误
if any(kw in text_lower for kw in ["大家都", "所有人", "人们都", "大家都知道", "众所周知", "都认为", "都在"]):
detected.append("bandwagon")
# 转移话题
if any(kw in text_lower for kw in ["说到底", "其实", "不过", "重点是", "关键是", "问题是"]):
# 排除明确在展开论述的情况
if len(text_lower) < 200 or "综上所述" in text_lower:
detected.append("red_herring")
# Tu quoque
tu_quoque_kws = ["你也", "你自己", "你也一样", "你不也", "你之前也", "你自己还不是"]
if any(kw in text_lower for kw in tu_quoque_kws):
detected.append("tu_quoque")
return list(dict.fromkeys(detected)) # 去重保持顺序
def rate_strength(clarity, validity, soundness, completeness, fallacy_count):
"""综合评分"""
base = (clarity + validity + soundness + completeness) / 4
penalty = min(fallacy_count * 1.5, 4)
overall = max(1, min(10, base - penalty))
return round(overall, 1)
def parse_arguments(text):
"""解析论点结构"""
arguments = []
sentences = re.split(r'[。!?\n]', text)
for s in sentences:
s = s.strip()
if len(s) < 5:
continue
# 检测结论词
conclusion_markers = ["应该", "必须", "不应该", "不能", "可以", "是对的", "是错的", "是正确的", "是错误的", "所以", "因此", "可见", "总之", "故", "合理", "不合理", "有效", "无效"]
if any(m in s for m in conclusion_markers):
arguments.append({"type": "claim", "content": s})
elif any(m in s for m in ["认为", "指出", "提出", "说", "观点", "论点"]):
arguments.append({"type": "premise", "content": s})
return arguments[:10]
def analyze_structure(text):
"""分析论点结构"""
sentences = re.split(r'[。!?\n]', text)
sentences = [s.strip() for s in sentences if s.strip()]
structure = []
for s in sentences:
if any(kw in s for kw in ["因为", "由于", "首先", "其次"]):
structure.append({"type": "premise", "content": s})
elif any(kw in s for kw in ["所以", "因此", "结论", "综上所述"]):
structure.append({"type": "conclusion", "content": s})
else:
structure.append({"type": "claim", "content": s})
return structure
def handle(topic, user_input, history=None, args=None):
"""处理论点审计请求"""
if not user_input or not user_input.strip():
return {"status": "error", "message": "请提供需要审计的论点内容"}
user_input = user_input.strip()
topic = (topic or "").strip()
fallacies_found_ids = detect_fallacies(user_input)
fallacies_found = []
for fid in fallacies_found_ids:
if fid in FALLACIES:
fallacies_found.append({
"type": fid,
"name": FALLACIES[fid]["name"],
"description": FALLACIES[fid]["description"],
"example": FALLACIES[fid]["example"]
})
arguments = parse_arguments(user_input)
structure = analyze_structure(user_input)
# 评分
has_conclusion = "所以" in user_input or "因此" in user_input or "应该" in user_input or "必须" in user_input
has_evidence = any(kw in user_input.lower() for kw in ["因为", "数据", "研究", "统计", "显示", "表明", "根据", "来源"])
has_example = any(kw in user_input.lower() for kw in ["例如", "比如", "比如", "案例", "实例", "事实", "例子"])
clarity = 5.0 if len(user_input) > 20 else 4.0
validity = 7.0 if has_conclusion else 5.0
soundness = 6.5 if has_evidence else 4.5
completeness = 6.0 if has_example else 4.5
overall = rate_strength(clarity, validity, soundness, completeness, len(fallacies_found))
# 建议
recommendations = []
if len(fallacies_found) > 0:
recommendations.append(f"⚠️ 检测到 {len(fallacies_found)} 种逻辑谬误,建议逐一检视")
else:
recommendations.append("✅ 未检测到常见逻辑谬误")
if not has_evidence:
recommendations.append("📌 论据支撑不足,建议补充数据、研究或具体案例")
if not has_conclusion:
recommendations.append("📌 缺少明确结论或论点,建议开篇亮明核心观点")
if not has_example:
recommendations.append("📌 建议增加具体案例或实例以增强说服力")
if len(user_input) < 50:
recommendations.append("📌 论点内容较短,建议展开论述以便全面评估")
return {
"status": "success",
"argument_count": len(arguments) or 1,
"structure": structure,
"scores": {
"clarity": clarity,
"validity": validity,
"soundness": soundness,
"completeness": completeness,
"overall": overall
},
"fallacy_count": len(fallacies_found),
"fallacy_details": fallacies_found,
"arguments": arguments,
"recommendations": recommendations,
"audit_summary": f"论点{'较弱' if overall < 6 else '中等' if overall < 7.5 else '较强'},综合得分 {overall}/10"
}
if __name__ == "__main__":
if len(sys.argv) > 1:
user_input = " ".join(sys.argv[1:])
result = handle(None, user_input)
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
print("Usage: python handler.py '<论点内容>'")
print("Example: python handler.py '人工智能会取代所有工作,所以不应该发展AI。'")
FILE:skill.json
{
"name": "argument-audit",
"description": "论点审计 - An analytical tool that evaluates the logical structure, evidence quality, and persuasiveness of arguments. Use when user asks about 论点分析、论证评估、逻辑审查、观点评估, or wants to analyze the strength and validity of an argument.",
"version": "1.0.0",
"author": "Harry",
"tags": ["logic", "critical-thinking", "debate", "analysis", "argumentation"],
"safety": {
"no_diagnosis": true,
"no_treatment": true,
"disclaimer_required": false
},
"modules": {
"handler": "handler.py"
},
"entry": "handler.py"
}
法律AI助手 - An intelligent legal consultation assistant that helps users understand legal concepts, analyze legal scenarios, and prepare for legal matters. Use...
---
name: legal-ai-counsel
description: 法律AI助手 - An intelligent legal consultation assistant that helps users understand legal concepts, analyze legal scenarios, and prepare for legal matters. Use when user asks about 法律咨询、法律问题、法律帮助、法律建议, or needs help understanding legal concepts, rights, or procedures.
---
# Legal AI Counsel (法律AI助手)
## Overview
This skill provides AI-powered legal guidance to help users understand legal concepts, analyze legal situations, and prepare for legal actions. It covers common legal domains including consumer rights, labor law, property, contracts, and civil disputes.
**⚠️ Important Disclaimer**: This tool provides general legal information and guidance only. It does not constitute legal advice, nor does it create an attorney-client relationship. For specific legal matters, always consult a qualified attorney in your jurisdiction.
## When to Use This Skill
- Understanding legal concepts (rights, liabilities, procedures)
- Analyzing a legal scenario or situation
- Preparing questions for a lawyer consultation
- Understanding legal documents or notices
- Learning about common legal procedures
- Getting general guidance on legal options
## What This Skill Provides
### 1. Legal Concept Explanation
- Explain legal terms and concepts in plain language
- Break down complex legal principles
- Provide examples to illustrate concepts
### 2. Scenario Analysis
- Help users understand potential legal implications
- Identify relevant laws and regulations
- Outline possible legal paths and outcomes
### 3. Preparation Assistance
- List questions to ask a lawyer
- Suggest what documents to gather
- Explain typical legal procedures
### 4. Rights Awareness
- Inform users of their legal rights
- Explain corresponding obligations
- Highlight protective measures
## Supported Legal Domains
| Domain | Examples |
|--------|----------|
| Consumer Rights | Product quality, service disputes, refund policies |
| Labor Law | Employment contracts, workplace disputes, termination |
| Property | Rental agreements, property disputes, ownership |
| Contracts | Contract interpretation, breach, remedies |
| Civil Disputes | Neighbor disputes, debt collection, liability |
| Family Law | Marriage, divorce, child custody basics |
| Criminal Procedure | Basic rights, legal process overview |
## Limitations
- **Not a substitute for professional legal advice**
- Cannot provide jurisdiction-specific legal opinions
- Cannot represent users in legal proceedings
- Cannot draft formal legal documents
- Information may not reflect latest legal changes
- Cannot guarantee accuracy for specific situations
## Workflow
1. **Understand the user's question** — Clarify the legal topic or scenario
2. **Identify relevant laws** — Reference applicable laws and regulations
3. **Provide explanation** — Break down concepts in accessible language
4. **Outline options** — Suggest possible next steps
5. **Recommend professional help** — Advise when to consult an attorney
## Usage Examples
### Understanding Legal Concepts
```
"什么是举证责任?"
"劳动合同中试用期最长多久?"
"消费者维权有哪些途径?"
```
### Scenario Analysis
```
"我在电商买到假货,应该怎么处理?"
"公司拖欠工资,我该怎么办?"
"租房到期房东不退押金怎么维权?"
```
### Preparation for Legal Action
```
"我想打官司,需要准备什么材料?"
"请帮我列出咨询律师时要问的问题"
```
## Output Format
For each query, provide:
- **Direct Answer** — Clear, actionable response
- **Legal Basis** — Reference to relevant laws/regulations when applicable
- **Key Points** — 3-5 bullet points summarizing important takeaways
- **Next Steps** — Recommended actions
- **Disclaimer** — Reminder to consult professional legal counsel
## Privacy Note
Conversations are processed for legal guidance only. No personal legal information is stored or transmitted to third parties. Users should avoid sharing sensitive personal identification information.
## Related Skills
- `contract-risk-scan` — For contract-specific risk analysis
- `clause-redraft` — For redrafting contract clauses
- `legal-consultation` — For more detailed legal consultation flow
- `lawsuit-fee-calc` — For calculating litigation costs
FILE:handler.py
import sys
import re
DISCLAIMER = """
⚠️ **免责声明**
本工具的输出仅供初步参考,不构成正式法律意见。
涉及具体权利认定、重大金额或复杂情形时,请务必咨询具备执业资格的专业律师,结合完整事实和最新当地规则进行正式审核。
"""
RISK_PATTERNS = {
"付款验收": {
"keywords": ["验收", "付款", "结算", "账期", "发票", "审批"],
"high_risk": ["单方验收", "无限期拖延", "无逾期责任", "无付款期限", "完全绑定对方判断"],
"risk_level": "高",
"description": "付款与验收条款失衡",
"suggestion": "明确验收标准、验收期限、视为验收规则;明确付款日期和逾期责任"
},
"责任赔偿": {
"keywords": ["赔偿", "责任", "违约金", "损失", "免责"],
"high_risk": ["无限责任", "单方责任", "赔偿无上限", "违约金过高"],
"risk_level": "高",
"description": "责任与赔偿条款失衡",
"suggestion": "增加责任上限、排除条款、对等责任安排"
},
"期限解除": {
"keywords": ["解除", "终止", "续约", "通知", "提前"],
"high_risk": ["单方解除权", "无理由解除", "自动续约无退出", "解除成本过高"],
"risk_level": "中",
"description": "期限与解除条款失衡",
"suggestion": "明确解除条件、通知期限、解除后的结算和交接义务"
},
"知识产权": {
"keywords": ["知识产权", "版权", "专利", "著作权", "交付", "源文件"],
"high_risk": ["无条件转移", "无范围限制", "无使用授权", "交付物定义不清"],
"risk_level": "中",
"description": "知识产权归属与交付约定不清",
"suggestion": "明确所有权、使用权、原始素材、交付标准和验收口径"
},
"保密数据": {
"keywords": ["保密", "机密", "信息", "数据", "隐私", "个人信息"],
"high_risk": ["无限期保密", "无保密范围", "无责任划分", "数据处理无约定"],
"risk_level": "中",
"description": "保密与数据条款不完整",
"suggestion": "明确保密范围、例外情形、持续期限、返还/删除义务"
},
"争议解决": {
"keywords": ["争议", "诉讼", "仲裁", "管辖", "适用法律"],
"high_risk": ["单方管辖", "仲裁条款不完整", "管辖矛盾"],
"risk_level": "中",
"description": "争议解决条款不完整或偏向一方",
"suggestion": "统一适用法律、管辖法院/仲裁机构、程序规则"
}
}
LITIGATION_FEE_RULES = [
(10000, 50, 0, 0),
(100000, 250, 200, 0.025),
(200000, 2300, 300, 0.02),
(500000, 4300, 1300, 0.015),
(1000000, 10300, 3800, 0.01),
(2000000, 22800, 4800, 0.009),
(5000000, 46800, 6800, 0.008),
(10000000, 81800, 11800, 0.007),
(20000000, 141800, 21800, 0.006),
]
def calculate_litigation_fee(amount):
if amount <= 0:
return 0
if amount <= 10000:
return 50
elif amount <= 100000:
return amount * 0.025 - 200
elif amount <= 200000:
return amount * 0.02 + 300
elif amount <= 500000:
return amount * 0.015 + 1300
elif amount <= 1000000:
return amount * 0.01 + 3800
elif amount <= 2000000:
return amount * 0.009 + 4800
elif amount <= 5000000:
return amount * 0.008 + 6800
elif amount <= 10000000:
return amount * 0.007 + 11800
elif amount <= 20000000:
return amount * 0.006 + 21800
else:
return amount * 0.005 + 41800
def calculate_labor_compensation(monthly_salary, years, scenario="normal"):
n = min(years, 12)
base = monthly_salary * n
if scenario == "illegal":
return monthly_salary * n * 2
elif scenario == "n_plus_1":
return monthly_salary * (n + 1)
else:
return monthly_salary * n
def scan_contract_risks(text):
findings = []
text_lower = text.lower()
for category, data in RISK_PATTERNS.items():
matched_keywords = [kw for kw in data["keywords"] if kw in text_lower]
if not matched_keywords:
continue
risks_found = []
for risk in data.get("high_risk", []):
if risk in text_lower:
risks_found.append(risk)
risk_level = data["risk_level"]
if risks_found:
risk_level = "高"
elif matched_keywords:
risk_level = "中"
findings.append({
"category": category,
"risk_level": risk_level,
"description": data["description"],
"matched_keywords": matched_keywords,
"specific_risks": risks_found,
"suggestion": data["suggestion"]
})
if any(kw in text_lower for kw in ["签字", "签章", "盖章"]):
if not any(f["category"] == "期限解除" for f in findings):
findings.append({
"category": "签署程序",
"risk_level": "低",
"description": "签署程序和授权确认",
"matched_keywords": [],
"specific_risks": [],
"suggestion": "确认签署人有合法授权,建议保留签署记录和授权文件"
})
return findings
def estimate_labor_dispute(user_input):
result = {
"facts_needed": [],
"assumptions": [],
"scenarios": [],
"calculation": {}
}
text = user_input
text_lower = user_input.lower()
# 提取月工资(增强模式:支持"2万""2万元""月薪2万""2万/月"等)
salary = None
salary_patterns = [
r'(\d+(?:\.\d+)?)\s*万\s*元?/?\s*月',
r'(\d+(?:\.\d+)?)\s*万/?\s*月',
r'月[薪工][资]?\s*[为是:::]?\s*(\d+(?:\.\d+)?)\s*[万千]?元?',
r'每月\s*(\d+(?:\.\d+)?)\s*[万千]?元?',
r'工资\s*[为是:::]?\s*(\d+(?:\.\d+)?)\s*[万千]?元?',
r'(\d+(?:\.\d+)?)\s*千\s*元?/?\s*月',
r'月[薪工][资]?\s*[为是:::]?\s*(\d+(?:\.\d+)?)\s*千',
]
for p in salary_patterns:
m = re.search(p, text_lower)
if m:
num = float(m.group(1))
if '万' in m.group(0):
salary = num * 10000
elif '千' in m.group(0):
salary = num * 1000
else:
# 根据数字大小判断:<100 视为万,<1000 视为千
if num < 100:
salary = num * 10000
elif num < 1000:
salary = num * 1000
else:
salary = num
break
# 提取年限
years = None
year_patterns = [
r'(\d+)\s*年',
r'工作\s*(\d+)\s*年',
r'做了\s*(\d+)\s*年',
r'入职\s*(\d+)\s*年',
r'累计\s*(\d+)\s*年',
]
for p in year_patterns:
m = re.search(p, text)
if m:
years = int(m.group(1))
break
is_illegal = any(kw in text_lower for kw in ["违法解除", "无故辞退", "非法开除", "违法终止", "被辞退", "被开除"])
is_economic = any(kw in text_lower for kw in ["经济性裁员", "经济性解除", "经营困难", "公司倒闭", "破产"])
result["facts_needed"] = ["月工资基数", "工作年限", "解除原因", "是否提前通知"]
if salary and years:
result["assumptions"].append(f"月工资:{salary:.0f}元,工作年限:{years}年")
if is_illegal:
compensation = calculate_labor_compensation(salary, years, "illegal")
result["scenarios"].append({
"scenario": "违法解除(如经仲裁/诉讼确认)",
"formula": "2N = 2 × 月工资 × 年限",
"estimate": f"约 {compensation:.0f} 元",
"note": f"N = {min(years,12)} 个月工资"
})
if is_economic:
compensation = calculate_labor_compensation(salary, years, "normal")
result["scenarios"].append({
"scenario": "合法经济性解除",
"formula": "N = 月工资 × 年限",
"estimate": f"约 {compensation:.0f} 元",
"note": "另需提前30天通知或支付代通知金"
})
compensation_n = calculate_labor_compensation(salary, years, "normal")
result["scenarios"].append({
"scenario": "一般合法解除(支付经济补偿)",
"formula": "N = 月工资 × 年限(最高12年)",
"estimate": f"约 {compensation_n:.0f} 元",
"note": f"年限超过12年按12年计算"
})
# N+1场景
compensation_n1 = calculate_labor_compensation(salary, years, "n_plus_1")
result["scenarios"].append({
"scenario": "合法解除(未提前通知)",
"formula": "N+1 = 月工资 × (年限+1)",
"estimate": f"约 {compensation_n1:.0f} 元",
"note": "+1 为1个月代通知金"
})
elif salary:
result["assumptions"].append(f"月工资:{salary:.0f}元(年限未知)")
elif years:
result["assumptions"].append(f"工作年限:{years}年(月工资未知)")
return result
def generate_legal_document(topic, user_input):
topic_lower = (topic or "").lower() + " " + user_input.lower()
templates = {
"起诉状": {
"purpose": "向法院提起民事诉讼",
"skeleton": """## 民事起诉状
### 原告信息
- 姓名/名称:
- 身份证号/统一社会信用代码:
- 地址:
- 联系方式:
### 被告信息
- 姓名/名称:
- 身份证号/统一社会信用代码:
- 地址:
- 联系方式:
### 诉讼请求
(明确、具体、可执行)
1.
2.
3.
### 事实与理由
(按时间顺序叙述)
1.
2.
3.
### 证据清单
| 序号 | 证据名称 | 证明内容 | 页码 |
|------|----------|----------|------|
| 1 | | | |
### 管辖法院
(通常为被告住所地或合同履行地法院)
---
此为骨架草稿,完整诉状需补充具体事实和证据"""
},
"答辩状": {
"purpose": "针对原告起诉进行答辩",
"skeleton": """## 民事答辩状
### 答辩人(被告)信息
- 姓名/名称:
- 身份证号/统一社会信用代码:
- 地址:
- 联系方式:
### 原告信息
- 姓名/名称:
### 答辩请求
(针对原告诉讼请求表明态度)
1. 请求驳回原告第X项诉讼请求
2. 请求驳回原告全部诉讼请求
### 答辩事实与理由
(针对原告主张逐条答辩)
1. 针对原告主张的事实的答辩:
2. 针对原告法律依据的答辩:
### 证据清单
| 序号 | 证据名称 | 证明内容 | 页码 |
|------|----------|----------|------|
| 1 | | | |
---
此为骨架草稿,完整答辩状需结合原告起诉内容针对性答辩"""
},
"劳动仲裁": {
"purpose": "向劳动争议仲裁委员会申请仲裁",
"skeleton": """## 劳动争议仲裁申请书
### 申请人信息
- 姓名:
- 身份证号:
- 地址:
- 联系方式:
### 被申请人信息
- 单位名称:
- 统一社会信用代码:
- 地址:
- 法定代表人:
### 仲裁请求
(劳动争议需先仲裁后诉讼)
1.
2.
### 事实与理由
(劳动关系的建立、争议的发生过程)
1.
2.
### 证据清单
| 序号 | 证据名称 | 证明内容 | 页码 |
|------|----------|----------|------|
| 1 | 劳动合同 | 劳动关系 | |
| 2 | 工资流水 | 工资标准 | |
---
劳动争议需先向区劳动仲裁委申请仲裁,对仲裁不服15日内可起诉
此为骨架草稿"""
}
}
for doc_type, template in templates.items():
if doc_type in topic_lower or any(kw in topic_lower for kw in ["起诉状", "答辩状", "仲裁"]):
return {
"document_type": doc_type,
"purpose": template["purpose"],
"skeleton": template["skeleton"],
"missing_facts": ["具体金额或计算方式", "相关日期(入职时间、事件发生时间)", "相关证据清单"]
}
return {
"document_type": "通用法律文书",
"purpose": "结构化法律文书",
"skeleton": "请明确说明需要哪种文书(如:民事起诉状、劳动仲裁申请书、答辩状等)",
"missing_facts": []
}
def route_task(user_input, topic=None):
text_lower = (topic or "").lower() + " " + user_input.lower()
contract_kws = ["合同", "条款", "协议", "风险", "有没有坑", "审查", "扫描", "协议书"]
labor_kws = ["辞退", "赔偿", "补偿", "劳动", "工资", "解除", "开除", "经济补偿", "N+1", "2N"]
lawsuit_fee_kws = ["诉讼费", "仲裁费", "打官司要多少钱", "值不值得起诉", "诉讼成本"]
document_kws = ["起诉状", "答辩状", "仲裁申请", "法律文书", "起草", "骨架"]
task_scores = {
"contract_scan": sum(1 for kw in contract_kws if kw in text_lower),
"labor_dispute": sum(1 for kw in labor_kws if kw in text_lower),
"lawsuit_fee": sum(1 for kw in lawsuit_fee_kws if kw in text_lower),
"document": sum(1 for kw in document_kws if kw in text_lower)
}
max_score = max(task_scores.values())
if max_score == 0:
return "general"
return max(task_scores, key=task_scores.get)
def handle(topic, user_input, history=None, args=None):
if not user_input or not user_input.strip():
return {"status": "error", "message": "请提供需要处理的法律问题或合同内容"}
user_input = user_input.strip()
topic = (topic or "").strip()
task = route_task(user_input, topic)
if task == "contract_scan":
findings = scan_contract_risks(user_input)
return {
"status": "success",
"task_type": "合同风险扫描",
"findings": findings,
"summary": f"扫描发现 {len(findings)} 个风险类别",
"high_risk_count": len([f for f in findings if f['risk_level'] == '高']),
"disclaimer": DISCLAIMER
}
elif task == "labor_dispute":
result = estimate_labor_dispute(user_input)
result["task_type"] = "劳动纠纷/补偿估算"
result["status"] = "success"
result["disclaimer"] = DISCLAIMER
return result
elif task == "lawsuit_fee":
amounts = re.findall(r'(\d+(?:\.\d+)?)\s*[万千万]?', user_input)
results = []
for a in amounts[:3]:
try:
num = float(a)
if num < 100:
num *= 10000
fee = calculate_litigation_fee(num)
results.append({
"claim_amount": f"{num:.0f} 元",
"estimated_fee": f"{fee:.0f} 元",
"note": "不含律师费、保全费、鉴定费、差旅费等"
})
except:
pass
return {
"status": "success",
"task_type": "诉讼成本估算",
"results": results if results else [{"note": "未识别到具体金额,请明确标的额"}],
"disclaimer": DISCLAIMER
}
elif task == "document":
doc = generate_legal_document(topic, user_input)
doc["status"] = "success"
doc["disclaimer"] = DISCLAIMER
return doc
else:
return {
"status": "success",
"task_type": "一般法律咨询",
"message": "我目前支持以下法律辅助任务:\n1. 合同风险扫描\n2. 劳动纠纷补偿估算\n3. 诉讼成本估算\n4. 法律文书骨架生成\n\n请告诉我您的具体需求并提供相关事实。",
"disclaimer": DISCLAIMER
}
if __name__ == "__main__":
import json
if len(sys.argv) > 1:
user_input = " ".join(sys.argv[1:])
result = handle(None, user_input)
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
print("Usage:")
print(" python handler.py '帮我看看这份合同有没有风险'")
print(" python handler.py '被辞退赔偿多少'")
print(" python handler.py '10万块诉讼费多少'")
print(" python handler.py '帮我起草一份民事起诉状'")
FILE:skill.json
{
"name": "legal-ai-counsel",
"description": "法律AI助手 - 中国场景民事/劳动法律辅助工具,提供合同风险扫描、劳动纠纷补偿估算、诉讼成本计算、法律文书骨架生成。当前版本为 Free Starter,适合初步风险筛查和结构化辅助,不替代执业律师的正式法律意见。",
"version": "1.0.0",
"author": "Harry",
"tags": ["legal", "china", "contract", "labor-dispute", "law", "civil-law"],
"safety": {
"no_diagnosis": false,
"no_treatment": false,
"disclaimer_required": true,
"high_risk_keyword_detection": true
},
"modules": {
"handler": "handler.py"
},
"entry": "handler.py"
}
AI工具评估器 - Evaluate and compare AI tools for specific use cases. Use when user asks about AI工具比较、AI产品评测、工具推荐、ChatGPT替代, or wants to find the best AI tool for...
---
name: ai-tools-evaluator
description: AI工具评估器 - Evaluate and compare AI tools for specific use cases. Use when user asks about AI工具比较、AI产品评测、工具推荐、ChatGPT替代, or wants to find the best AI tool for their needs. Provides structured evaluation and recommendations.
---
# AI Tools Evaluator (AI工具评估器)
## Overview
This skill helps users evaluate, compare, and select AI tools for their specific needs. It provides structured evaluation criteria, compares popular AI tools across different dimensions, and recommends the best options based on use cases. Designed to help users make informed decisions about AI tool adoption.
## When to Use This Skill
- Choosing an AI tool for a specific task
- Comparing multiple AI tools
- Evaluating if a tool meets their needs
- Finding alternatives to current tools
- Understanding AI tool capabilities and limitations
- Making purchasing/subscription decisions
## What This Skill Evaluates
### 1. Core Capabilities
- Language understanding and generation
- Task performance (coding, writing, analysis, etc.)
- Multimodal abilities (vision, audio, etc.)
- Context window and memory
- Knowledge cutoff and freshness
### 2. Practical Factors
- Ease of use and learning curve
- Integration options (API, plugins, etc.)
- Pricing and cost structure
- Privacy and data handling
- Speed and latency
### 3. Use Case Fit
- Best suited tasks
- Strengths and weaknesses
- Competition comparison
- Alternative tools
## Evaluation Dimensions
| Dimension | Criteria | Weight (Adjustable) |
|-----------|----------|---------------------|
| **Performance** | Task accuracy, quality of output | High |
| **Ease of Use** | UI, learning curve, documentation | Medium |
| **Integration** | API, plugins, third-party support | Medium |
| **Cost** | Pricing model, value for money | High |
| **Privacy** | Data handling, security | High |
| **Speed** | Response time, rate limits | Medium |
| **Reliability** | Uptime, consistency | Medium |
## Supported Tool Categories
| Category | Examples |
|----------|----------|
| **LLMs** | GPT-4, Claude, Gemini, Llama, Mistral |
| **Coding AI** | GitHub Copilot, Cursor, Codeium |
| **Writing AI** | Jasper, Copy.ai, Writesonic |
| **Image AI** | Midjourney, DALL-E, Stable Diffusion |
| **Audio AI** | ElevenLabs, Murf, Descript |
| **Research AI** | Perplexity, Consensus, SciSpace |
| **All-in-One** | ChatGPT, Claude, Google Gemini |
## Evaluation Framework
### For LLM Selection
```
Consider:
1. Primary use case (coding, writing, analysis, conversation)
2. Required capabilities (reasoning, creativity, speed)
3. Budget constraints
4. Privacy requirements
5. Integration needs
```
### For Specialized Tasks
```
Consider:
1. Task-specific performance benchmarks
2. Domain-specific fine-tuning
3. Output quality for your use case
4. Learning resources available
```
## Workflow
1. **Use Case Definition** — Understand what the user needs to accomplish
2. **Requirement Gathering** — Identify must-have vs. nice-to-have features
3. **Tool Identification** — List relevant tools for the use case
4. **Dimension Evaluation** — Score each tool on evaluation dimensions
5. **Comparison** — Side-by-side comparison of top candidates
6. **Recommendation** — Recommend best fit with rationale
## Usage Examples
### Tool Selection
```
"帮我选一个写代码的AI工具"
"哪个AI聊天机器人最适合分析文档?"
"有什么好的AI写作工具推荐?"
```
### Comparison
```
"GPT-4和Claude哪个更好?"
"比较一下这几个AI工具"
"Cursor和GitHub Copilot有什么区别?"
```
### Evaluation
```
"这个AI工具适合我的需求吗?"
"帮我评估一下这个产品"
"这个工具的优缺点是什么?"
```
## Output Format
```yaml
## Evaluation Request: [Use Case/Tool(s)]
### Requirements Analysis
- **Primary Need**: [User's main requirement]
- **Must Have**: [Essential features]
- **Nice to Have**: [Optional features]
- **Constraints**: [Budget, privacy, etc.]
### Tools Considered
| Tool | Performance | Ease of Use | Cost | Privacy | Overall |
|------|-------------|-------------|------|---------|---------|
| Tool A | 8/10 | 9/10 | 7/10 | 8/10 | 8.0/10 |
| Tool B | 9/10 | 7/10 | 9/10 | 9/10 | 8.5/10 |
### Detailed Analysis
#### Tool A
- **Pros**: [Strengths]
- **Cons**: [Weaknesses]
- **Best For**: [Use cases]
- **Pricing**: [Cost structure]
#### Tool B
...
### Recommendation
**[Recommended Tool]**
**Rationale**:
1. [Reason 1]
2. [Reason 2]
3. [Reason 3]
### Alternatives
- [Option for different needs]
- [Option for budget constraints]
```
## Limitations
- Cannot provide real-time pricing or feature updates
- Performance varies based on specific prompts/tasks
- Subjective evaluation components exist
- May not cover all niche or new tools
- Cannot test actual usage in user's context
- Evaluations may become outdated
## Acceptance Criteria
1. ✓ Clearly defines evaluation dimensions
2. ✓ Can evaluate tools across multiple categories
3. ✓ Provides structured comparison framework
4. ✓ Offers practical recommendations
5. ✓ Explains trade-offs between tools
6. ✓ Updates as new tools emerge
7. ✓ Helps users find best fit for their use case
FILE:README.md
# AI Tools Evaluator V1
AI 工具对比评测助手 MVP
## 运行方式
```bash
cd /Users/jianghaidong/.openclaw/workspace/agents/code/ai-tools-evaluator
python3 evaluator.py
```
## 交互流程
1. 选择 2~5 个要对比的工具(输入编号,空格分隔)
2. 选择使用场景(1-6)
3. 选择预算区间(1-4)
4. 确认维度权重(直接回车用默认)
5. 查看生成的对比报告
6. 报告自动保存到 `ai_tools_report.md`
## 技术栈
- Python 3(标准库 + pyperclip 可选)
- 静态 JSON 数据(工具池 19 个工具,5 大类别)
## 工具池(19个)
- LLM大模型:ChatGPT, Claude, Gemini, DeepSeek, 通义千问, 零一万物
- AI搜索:Perplexity, 秘塔AI, 玄紫AI
- AI写作:Claude写作版, 文心一言, 智谱清言
- AI代码:GitHub Copilot, Cursor, 腾飞AI
- AI图像:Midjourney, DALL-E 3, Stable Diffusion, 文心一格
FILE:ai_tools_report.md
======================================================================
🤖 AI 工具对比评测报告
======================================================================
📋 评测配置
场景: 日常办公
预算: 免费/低成本
维度权重: 能力40% | 成本25% | 易用15% | 稳定10% | 隐私10%
======================================================================
📊 综合评分对比
======================================================================
工具名称 提供商 综合评分
--------------------------------------------------
Claude 3.5 Sonnet Anthropic 83.4/100
Gemini 1.5 Pro Google 79.4/100
ChatGPT (GPT-4o) OpenAI 73.4/100
======================================================================
📈 维度得分详情
======================================================================
工具名称 能力匹配 成本效益 易用性 稳定性 隐私安全
----------------------------------------------------------------------
Claude 3.5 Sonnet 85.0 93.5 70.0 90.0 65.0
Gemini 1.5 Pro 75.0 93.5 70.0 90.0 65.0
ChatGPT (GPT-4o) 60.0 93.5 70.0 90.0 65.0
======================================================================
✅ 优劣势分析
======================================================================
🥇 Claude 3.5 Sonnet (第1名)
优势: 长文本超强, 代码能力强, 分析深入
劣势: 国内访问受限, 实时信息弱
定价: 输入$3/1M tokens,输出$15/1M tokens
🥈 Gemini 1.5 Pro (第2名)
优势: 超长上下文2M, 多模态强, 性价比高
劣势: 国内访问受限, 中文优化一般
定价: 输入$1.25/1M tokens,输出$5/1M tokens
🥉 ChatGPT (GPT-4o) (第3名)
优势: 通用能力强, 多模态支持, 生态完善
劣势: 费用较高, 国内访问受限
定价: 输入$5/1M tokens,输出$15/1M tokens
======================================================================
🎯 推荐结论
======================================================================
🥇 首选推荐: Claude 3.5 Sonnet
综合评分 83.4/100,在当前场景(日常办公)和预算(免费/低成本)下表现最优。
推荐理由:
• 长文本超强
• 定价方案: 输入$3/1M tokens,输出$15/1M tokens
• 适用场景: 长文档分析, 代码审查, 深度写作
目标用户: 研究者, 开发者
======================================================================
⚠️ 风险提醒
======================================================================
1. 评分基于公开信息整理,实际体验可能存在差异
2. 定价可能随服务商政策调整,请以官方最新为准
3. 部分工具需要科学上网,请确认使用环境
4. 涉及敏感数据时,优先考虑数据隐私合规
5. 建议先用免费额度或试用版进行实际体验
======================================================================
👥 适用人群建议
======================================================================
• Claude 3.5 Sonnet: 研究者, 开发者, 需要处理长文本的用户
• Gemini 1.5 Pro: 需要处理超长内容的用户, Google生态用户, 预算敏感型
• ChatGPT (GPT-4o): 开发者, 企业用户, 需要强推理能力的用户
======================================================================
📌 使用建议
======================================================================
1. 明确核心需求:先确定最看重的1-2个维度
2. 试用验证:建议先使用免费额度实际体验
3. 关注生态:考虑与其他工具的协同效果
4. 预留灵活性:可同时使用多个工具互补
5. 定期复盘:AI工具更新快,建议定期重新评估
======================================================================
报告生成时间: 2026-03-29 12:41
======================================================================
FILE:data/tools.json
{
"categories": [
{
"name": "LLM大模型",
"icon": "🤖",
"tools": [
{
"id": "gpt4o",
"name": "ChatGPT (GPT-4o)",
"provider": "OpenAI",
"description": "OpenAI最新旗舰模型,通用能力强,支持多模态",
"strengths": ["通用能力强", "多模态支持", "生态完善", "上下文200k"],
"weaknesses": ["费用较高", "国内访问受限", "部分地区限速"],
"pricing": "输入$5/1M tokens,输出$15/1M tokens",
"best_for": ["通用对话", "代码生成", "复杂推理", "多模态任务"],
"target_users": ["开发者", "企业用户", "需要强推理能力的用户"],
"tags": ["LLM", "多模态", "通用"]
},
{
"id": "claude35s",
"name": "Claude 3.5 Sonnet",
"provider": "Anthropic",
"description": "Anthropic旗舰模型,长文本处理优秀,Claude系列最强",
"strengths": ["长文本超强", "代码能力强", "分析深入", "200k上下文"],
"weaknesses": ["国内访问受限", "实时信息弱", "图片理解稍弱"],
"pricing": "输入$3/1M tokens,输出$15/1M tokens",
"best_for": ["长文档分析", "代码审查", "深度写作", "复杂推理"],
"target_users": ["研究者", "开发者", "需要处理长文本的用户"],
"tags": ["LLM", "长文本", "代码"]
},
{
"id": "gemini15pro",
"name": "Gemini 1.5 Pro",
"provider": "Google",
"description": "Google最强模型,超长上下文2M tokens,多模态能力强",
"strengths": ["超长上下文2M", "多模态强", "性价比高", "Google生态"],
"weaknesses": ["国内访问受限", "中文优化一般", "响应速度波动"],
"pricing": "输入$1.25/1M tokens,输出$5/1M tokens",
"best_for": ["长文档处理", "多模态任务", "大规模分析", "视频理解"],
"target_users": ["需要处理超长内容的用户", "Google生态用户", "预算敏感型"],
"tags": ["LLM", "多模态", "超长上下文"]
},
{
"id": "deepseekv3",
"name": "DeepSeek V3",
"provider": "DeepSeek",
"description": "国产开源强模型,性价比极高,中文优化好",
"strengths": ["开源免费", "中文优秀", "价格低", "代码能力强"],
"weaknesses": ["非国产需翻墙", "多模态能力弱", "生态一般"],
"pricing": "API免费开源,托管版约$0.5/1M tokens",
"best_for": ["中文任务", "代码生成", "低成本部署", "二次开发"],
"target_users": ["预算敏感用户", "中文场景", "开发者"],
"tags": ["LLM", "开源", "中文"]
},
{
"id": "qwen25max",
"name": "通义千问 Max (Qwen2.5-Max)",
"provider": "阿里云",
"description": "阿里自研最强模型,中文理解优秀,支持长上下文",
"strengths": ["中文优秀", "长上下文128k", "阿里生态", "稳定可靠"],
"weaknesses": ["国际能力一般", "多模态中等", "非完全开源"],
"pricing": "输入¥0.02/千tokens,输出¥0.08/千tokens",
"best_for": ["中文对话", "阿里云集成", "企业应用", "电商场景"],
"target_users": ["企业用户", "阿里云用户", "国内业务为主"],
"tags": ["LLM", "中文", "企业"]
},
{
"id": "yi11",
"name": "零一万物 Yi 1.5",
"provider": "零一万物",
"description": "国产开源模型,数学和代码能力突出",
"strengths": ["开源可商用", "数学能力强", "中文优秀", "性价比高"],
"weaknesses": ["生态较小", "长上下文一般", "稳定性待提升"],
"pricing": "API约$1/1M tokens",
"best_for": ["中文任务", "数学计算", "代码", "低成本应用"],
"target_users": ["开发者", "中文场景", "成本敏感型"],
"tags": ["LLM", "开源", "中文", "数学"]
}
]
},
{
"name": "AI搜索",
"icon": "🔍",
"tools": [
{
"id": "perplexity",
"name": "Perplexity AI",
"provider": "Perplexity",
"description": "AI原生搜索,实时联网,答案带引用",
"strengths": ["实时联网", "答案带源", "对话式搜索", "探索模式"],
"weaknesses": ["免费版限流", "复杂问题处理一般", "国内访问受限"],
"pricing": "免费+Pro $20/月",
"best_for": ["实时信息查询", "学术研究", "快速了解新话题"],
"target_users": ["研究者", "学生", "需要实时信息的用户"],
"tags": ["搜索", "实时", "引用"]
},
{
"id": "metaso",
"name": "秘塔AI搜索",
"provider": "秘塔科技",
"description": "国产AI搜索,中文优化,支持学术/全网/视频模式",
"strengths": ["中文优化", "无广告", "学术模式", "免费"],
"weaknesses": ["海外信息覆盖一般", "高级功能有限"],
"pricing": "免费",
"best_for": ["中文信息搜索", "学术研究", "无干扰搜索"],
"target_users": ["学生", "研究者", "国内用户"],
"tags": ["搜索", "中文", "免费"]
},
{
"id": "fuxi",
"name": "玄紫AI搜索",
"provider": "其他",
"description": "新兴国产AI搜索,简洁界面,专注效率",
"strengths": ["界面简洁", "速度快", "免费", "中文友好"],
"weaknesses": ["新兴产品稳定性待验证", "功能相对基础"],
"pricing": "免费",
"best_for": ["日常信息查询", "快速搜索"],
"target_users": ["普通用户", "国内用户"],
"tags": ["搜索", "中文", "简洁"]
}
]
},
{
"name": "AI写作",
"icon": "✍️",
"tools": [
{
"id": "claude",
"name": "Claude (写作专用)",
"provider": "Anthropic",
"description": "以写作为核心优化,长文本连贯性强",
"strengths": ["写作质量高", "长文本连贯", "风格多样", "改稿能力强"],
"weaknesses": ["需科学上网", "非实时信息"],
"pricing": "Pro $20/月含Claude",
"best_for": ["长文写作", "文案创作", "内容改稿", "剧本创作"],
"target_users": ["内容创作者", "写作者", "营销人员"],
"tags": ["写作", "长文本", "创意"]
},
{
"id": "wenxinyige",
"name": "文心一言",
"provider": "百度",
"description": "百度国产大模型,中文理解强,支持多模态",
"strengths": ["中文优秀", "百度生态", "多模态", "免费额度"],
"weaknesses": ["创意写作一般", "国际信息弱"],
"pricing": "免费+会员¥49.9/月",
"best_for": ["中文创作", "百度生态集成", "国内办公"],
"target_users": ["国内企业", "百度用户", "中文办公"],
"tags": ["写作", "中文", "多模态"]
},
{
"id": "zhiyi",
"name": "智谱清言",
"provider": "智谱AI",
"description": "清华国产模型,中英双语,开源可商用",
"strengths": ["中英双语", "开源可商用", "稳定", "GLM-4能力强"],
"weaknesses": ["创意写作一般", "部分场景收费"],
"pricing": "免费额度+API付费",
"best_for": ["双语任务", "企业应用", "开发者"],
"target_users": ["开发者", "企业用户", "双语场景"],
"tags": ["写作", "双语", "开源"]
}
]
},
{
"name": "AI代码",
"icon": "💻",
"tools": [
{
"id": "copilot",
"name": "GitHub Copilot",
"provider": "Microsoft",
"description": "代码补全首选,VS Code深度集成",
"strengths": ["代码补全强", "IDE深度集成", "上下文理解", "多语言支持"],
"weaknesses": ["订阅费用较高", "复杂项目理解有限", "国内访问一般"],
"pricing": "$10/月或$100/年",
"best_for": ["日常编码", "代码补全", "快速开发"],
"target_users": ["开发者", "需要效率提升的程序员"],
"tags": ["代码", "补全", "IDE集成"]
},
{
"id": "cursor",
"name": "Cursor",
"provider": "Cursor AI",
"description": "AI代码编辑器,专注AI辅助编程",
"strengths": ["AI原生编辑器", "多模型支持", "协作功能", "代码库理解"],
"weaknesses": ["相对新", "生态在建设中", "大型项目可能慢"],
"pricing": "免费+Pro $20/月",
"best_for": ["AI编程", "代码审查", "项目级理解"],
"target_users": ["开发者", "AI编程爱好者", "初学者"],
"tags": ["代码", "IDE", "AI原生"]
},
{
"id": "tengu",
"name": "腾飞AI编码",
"provider": "国内创业公司",
"description": "国产AI代码助手,专注中文开发者",
"strengths": ["中文友好", "免费额度", "国内访问稳定", "中文注释生成"],
"weaknesses": ["相对新", "功能在完善", "国际语言支持一般"],
"pricing": "免费+增值服务",
"best_for": ["中文开发者", "国内团队", "快速上手"],
"target_users": ["国内开发者", "学生", "初学者"],
"tags": ["代码", "中文", "免费"]
}
]
},
{
"name": "AI图像",
"icon": "🎨",
"tools": [
{
"id": "midjourney",
"name": "Midjourney",
"provider": "Midjourney",
"description": "AI图像生成天花板,艺术感强",
"strengths": ["艺术质量高", "风格多样", "社区生态", "创意无限"],
"weaknesses": ["需科学上网", "成本较高", "版权争议", "出图不稳定"],
"pricing": "$10-$30/月",
"best_for": ["艺术创作", "概念设计", "海报", "插画"],
"target_users": ["设计师", "艺术家", "创意工作者"],
"tags": ["图像", "艺术", "生成"]
},
{
"id": "dalle3",
"name": "DALL-E 3",
"provider": "OpenAI",
"description": "OpenAI图像生成,文字理解准确",
"strengths": ["文字理解强", "ChatGPT集成", "版权清晰", "风格可控"],
"weaknesses": ["需ChatGPT", "费用较高", "创意有限制"],
"pricing": "通过ChatGPT订阅",
"best_for": ["精确图像", "配图", "logo设计", "产品图"],
"target_users": ["内容创作者", "营销人员", "需要精确图像的用户"],
"tags": ["图像", "精确", "集成"]
},
{
"id": "sd",
"name": "Stable Diffusion",
"provider": "Stability AI",
"description": "开源图像生成,本地部署可商用",
"strengths": ["开源免费", "本地运行", "可定制", "无审查"],
"weaknesses": ["硬件要求高", "需要调参", "质量参差不齐"],
"pricing": "免费(本地)或$9/月云端",
"best_for": ["本地部署", "定制训练", "商业使用", "隐私敏感"],
"target_users": ["技术用户", "企业", "需要定制的用户"],
"tags": ["图像", "开源", "本地"]
},
{
"id": "wenxinwenyi",
"name": "文心一格",
"provider": "百度",
"description": "百度AI绘画,中文提示词优化",
"strengths": ["中文提示词", "百度生态", "操作简单", "国内访问"],
"weaknesses": ["创意有限", "艺术性一般", "需排队"],
"pricing": "免费积分+付费",
"best_for": ["中文创作", "国内用户", "简单绘图"],
"target_users": ["国内用户", "非专业用户", "日常创作"],
"tags": ["图像", "中文", "简单"]
}
]
}
]
}
FILE:evaluator.py
#!/usr/bin/env python3
"""
AI Tools Evaluator - MVP
AI 工具对比评测助手 V1
帮助用户在 2~5 个候选 AI 工具之间做结构化比较,输出可直接用于决策的推荐报告。
"""
import json
import sys
from typing import List, Dict, Any
# ==================== 数据加载 ====================
def load_tools() -> Dict[str, Any]:
"""加载工具池数据"""
with open('data/tools.json', 'r', encoding='utf-8') as f:
return json.load(f)
# ==================== 界面函数 ====================
def print_header(title: str):
print("\n" + "=" * 60)
print(f" {title}")
print("=" * 60)
def print_category_tools(tools_data: Dict[str, Any]) -> Dict[str, Any]:
"""展示分类工具池,返回用户选择的工具ID列表"""
categories = tools_data['categories']
print_header("📦 工具池 - 请选择要对比的工具(2~5个)")
print("输入编号(用空格分隔,如: 1 3 5)\n")
all_tools = []
for cat_idx, category in enumerate(categories):
print(f"\n{cat_idx + 1}. {category['icon']} {category['name']}")
print("-" * 40)
for tool_idx, tool in enumerate(category['tools']):
global_idx = len(all_tools)
all_tools.append(tool)
print(f" [{global_idx + 1}] {tool['name']} ({tool['provider']})")
print(f" {tool['description'][:50]}...")
print("\n" + "-" * 60)
print("请输入工具编号 (2~5个): ", end="")
try:
selected = input().strip()
indices = [int(x.strip()) - 1 for x in selected.split() if x.strip()]
# D01 fix: 去重,保持顺序
seen = set()
unique_indices = []
for idx in indices:
if idx not in seen:
seen.add(idx)
unique_indices.append(idx)
indices = unique_indices
if len(indices) < 2:
print("❌ 至少需要选择2个工具")
sys.exit(1)
if len(indices) > 5:
print("❌ 最多选择5个工具")
sys.exit(1)
# D01 fix: 验证每个索引都合法
invalid_indices = [i for i in indices if i < 0 or i >= len(all_tools)]
if invalid_indices:
print("❌ 选择无效")
sys.exit(1)
selected_tools = [all_tools[i] for i in indices]
if len(selected_tools) < 2:
print("❌ 选择无效")
sys.exit(1)
return selected_tools
except ValueError:
print("❌ 输入格式错误")
sys.exit(1)
except IndexError:
print("❌ 选择无效")
sys.exit(1)
def select_use_case() -> str:
"""选择使用场景"""
print_header("🎯 使用场景")
print("1. 日常办公(文档、邮件、会议)")
print("2. 学术研究(论文、文献、数据分析)")
print("3. 创意创作(写作、设计、视频)")
print("4. 程序开发(代码、调试、测试)")
print("5. 商业应用(客服、营销、数据处理)")
print("6. 学习教育(辅导、答疑、知识整理)")
scenes = {
"1": "日常办公",
"2": "学术研究",
"3": "创意创作",
"4": "程序开发",
"5": "商业应用",
"6": "学习教育"
}
print("\n请输入场景编号 (1-6): ", end="")
choice = input().strip()
return scenes.get(choice, "日常办公")
def select_budget() -> str:
"""选择预算区间"""
print_header("💰 预算区间")
print("1. 免费 / 低成本 (< $10/月)")
print("2. 中等预算 ($10-30/月)")
print("3. 高预算 (> $30/月)")
print("4. 不限预算")
budgets = {
"1": "免费/低成本",
"2": "中等预算",
"3": "高预算",
"4": "不限预算"
}
print("\n请输入预算编号 (1-4): ", end="")
choice = input().strip()
return budgets.get(choice, "中等预算")
def confirm_dimensions() -> Dict[str, float]:
"""确认评测维度权重(简化版)"""
print_header("⚖️ 评测维度权重确认")
print("系统将根据以下维度进行综合评分:\n")
print(" - 能力匹配度:与您场景的匹配程度")
print(" - 成本效益:性价比如何")
print(" - 易用性:上手难度和体验")
print(" - 稳定性:服务可靠性")
print(" - 隐私安全:数据安全考量\n")
print("默认权重配置(可直接回车确认):")
print(" 能力匹配度: 40%")
print(" 成本效益: 25%")
print(" 易用性: 15%")
print(" 稳定性: 10%")
print(" 隐私安全: 10%")
print("\n直接回车使用默认配置,或输入 'y' 确认: ", end="")
choice = input().strip().lower()
if choice == 'y' or choice == '':
return {
"能力匹配度": 0.40,
"成本效益": 0.25,
"易用性": 0.15,
"稳定性": 0.10,
"隐私安全": 0.10
}
else:
# 简化处理,直接返回默认值
return {
"能力匹配度": 0.40,
"成本效益": 0.25,
"易用性": 0.15,
"稳定性": 0.10,
"隐私安全": 0.10
}
# ==================== 评分引擎 ====================
def calculate_tool_score(tool: Dict, use_case: str, budget: str, weights: Dict[str, float], all_tools: List[Dict]) -> float:
"""计算工具综合评分"""
# 能力匹配度评分
capability_score = calculate_capability_score(tool, use_case)
# 成本效益评分
cost_score = calculate_cost_score(tool, budget)
# 易用性评分
usability_score = calculate_usability_score(tool)
# 稳定性评分
stability_score = calculate_stability_score(tool)
# 隐私安全评分
privacy_score = calculate_privacy_score(tool)
# 加权求和
total = (
capability_score * weights["能力匹配度"] +
cost_score * weights["成本效益"] +
usability_score * weights["易用性"] +
stability_score * weights["稳定性"] +
privacy_score * weights["隐私安全"]
)
return round(total * 100, 1)
def calculate_capability_score(tool: Dict, use_case: str) -> float:
"""计算能力匹配度"""
best_for = [x.lower() for x in tool.get('best_for', [])]
tags = [x.lower() for x in tool.get('tags', [])]
# 场景关键词映射
scene_mapping = {
"日常办公": ["办公", "文档", "邮件", "office", "writing", "文本", "中文"],
"学术研究": ["研究", "学术", "论文", "分析", "research", "长文本"],
"创意创作": ["创意", "写作", "设计", "视频", "creative", "art", "创作"],
"程序开发": ["代码", "编程", "developer", "code", "编程", "开发"],
"商业应用": ["商业", "企业", "business", "数据", "客服", "营销"],
"学习教育": ["学习", "教育", "学习", "辅导", "student", "learn"]
}
scene_keywords = scene_mapping.get(use_case, [])
# 基础分
score = 0.6
# 匹配加分
for keyword in scene_keywords:
if keyword.lower() in str(best_for).lower():
score += 0.15
if keyword.lower() in str(tags).lower():
score += 0.10
return min(score, 1.0)
def calculate_cost_score(tool: Dict, budget: str) -> float:
"""计算成本效益评分"""
pricing = tool.get('pricing', '').lower()
# 判断费用级别
if '免费' in pricing or 'free' in pricing or '免费' in tool.get('pricing', ''):
cost_level = 1.0
elif any(x in pricing for x in ['$0', '$1', '¥0', '¥0.5']):
cost_level = 0.85
elif any(x in pricing for x in ['$10', '$20', '¥30']):
cost_level = 0.65
elif any(x in pricing for x in ['$30', '$50', '¥100']):
cost_level = 0.45
else:
cost_level = 0.5 # 未知定价给中等分
# 预算匹配度
budget_mapping = {
"免费/低成本": (1.0, 0.3),
"中等预算": (0.6, 0.8),
"高预算": (0.3, 1.0),
"不限预算": (0.5, 1.0)
}
budget_min, budget_max = budget_mapping.get(budget, (0.4, 0.8))
if budget_min <= cost_level <= budget_max:
adjustment = 1.0
elif cost_level < budget_min:
adjustment = 1.1 # 比预算要求更便宜,加分
else:
adjustment = 0.8 # 超出预算,减分
return min(cost_level * adjustment, 1.0)
def calculate_usability_score(tool: Dict) -> float:
"""计算易用性评分"""
description = tool.get('description', '').lower()
# 简单判断:集成生态越好越易用
score = 0.7
if any(x in description for x in ['简单', '容易', 'easy', '简单上手']):
score = 0.9
elif any(x in description for x in ['深度', '需要配置', 'complex']):
score = 0.6
# 开源/本地部署相对复杂
if '开源' in tool.get('name', '') or 'stable diffusion' in tool.get('id', ''):
score = 0.55
return score
def calculate_stability_score(tool: Dict) -> float:
"""计算稳定性评分"""
provider = tool.get('provider', '').lower()
# 知名大厂稳定性好
stable_providers = ['openai', 'anthropic', 'google', 'microsoft', '阿里', '百度']
emerging_providers = ['deepseek', '零一', '智谱', 'cursor']
if any(p in provider for p in stable_providers):
return 0.9
elif any(p in provider for p in emerging_providers):
return 0.75
else:
return 0.7
def calculate_privacy_score(tool: Dict) -> float:
"""计算隐私安全评分"""
tool_id = tool.get('id', '').lower()
# 本地部署隐私最好
if 'sd' in tool_id or 'stable diffusion' in tool_id:
return 0.95
# 国产服务(数据留国内)
cn_providers = ['阿里', '百度', '智谱', '零一', '秘塔', '文心', '通义']
if any(p in tool.get('provider', '') for p in cn_providers):
return 0.85
# 国外服务
if any(p in tool.get('provider', '').lower() for p in ['openai', 'anthropic', 'google', 'microsoft']):
return 0.65 # 跨境数据风险
return 0.7
# ==================== 报告生成 ====================
def generate_report(selected_tools: List[Dict], use_case: str, budget: str, weights: Dict[str, float]) -> str:
"""生成对比报告"""
# 计算每个工具的评分
tool_scores = []
for tool in selected_tools:
score = calculate_tool_score(tool, use_case, budget, weights, selected_tools)
tool_scores.append({
'tool': tool,
'score': score
})
# 按评分排序
tool_scores.sort(key=lambda x: x['score'], reverse=True)
# 生成报告
report_lines = []
report_lines.append("=" * 70)
report_lines.append(" 🤖 AI 工具对比评测报告")
report_lines.append("=" * 70)
report_lines.append(f"\n📋 评测配置")
report_lines.append(f" 场景: {use_case}")
report_lines.append(f" 预算: {budget}")
report_lines.append(f" 维度权重: 能力{weights['能力匹配度']:.0%} | 成本{weights['成本效益']:.0%} | 易用{weights['易用性']:.0%} | 稳定{weights['稳定性']:.0%} | 隐私{weights['隐私安全']:.0%}")
# 综合评分表
report_lines.append("\n" + "=" * 70)
report_lines.append("📊 综合评分对比")
report_lines.append("=" * 70)
report_lines.append(f"{'工具名称':<25} {'提供商':<12} {'综合评分':>8}")
report_lines.append("-" * 50)
for item in tool_scores:
tool = item['tool']
score = item['score']
report_lines.append(f"{tool['name']:<25} {tool['provider']:<12} {score:>7.1f}/100")
# 详细对比表
report_lines.append("\n" + "=" * 70)
report_lines.append("📈 维度得分详情")
report_lines.append("=" * 70)
report_lines.append(f"{'工具名称':<20} {'能力匹配':>8} {'成本效益':>8} {'易用性':>8} {'稳定性':>8} {'隐私安全':>8}")
report_lines.append("-" * 70)
for item in tool_scores:
tool = item['tool']
cap = calculate_capability_score(tool, use_case) * 100
cost = calculate_cost_score(tool, budget) * 100
usab = calculate_usability_score(tool) * 100
stabi = calculate_stability_score(tool) * 100
priv = calculate_privacy_score(tool) * 100
report_lines.append(f"{tool['name'][:20]:<20} {cap:>7.1f} {cost:>7.1f} {usab:>7.1f} {stabi:>7.1f} {priv:>7.1f}")
# 优劣势分析
report_lines.append("\n" + "=" * 70)
report_lines.append("✅ 优劣势分析")
report_lines.append("=" * 70)
for item in tool_scores:
tool = item['tool']
rank = tool_scores.index(item) + 1
rank_emoji = "🥇" if rank == 1 else "🥈" if rank == 2 else "🥉"
report_lines.append(f"\n{rank_emoji} {tool['name']} (第{rank}名)")
report_lines.append(f" 优势: {', '.join(tool.get('strengths', [])[:3])}")
report_lines.append(f" 劣势: {', '.join(tool.get('weaknesses', [])[:2])}")
report_lines.append(f" 定价: {tool.get('pricing', '未知')}")
# 推荐结论
best_tool = tool_scores[0]
report_lines.append("\n" + "=" * 70)
report_lines.append("🎯 推荐结论")
report_lines.append("=" * 70)
report_lines.append(f"""
🥇 首选推荐: {best_tool['tool']['name']}
综合评分 {best_tool['score']:.1f}/100,在当前场景({use_case})和预算({budget})下表现最优。
推荐理由:
• {best_tool['tool']['strengths'][0] if best_tool['tool'].get('strengths') else '综合表现最佳'}
• 定价方案: {best_tool['tool'].get('pricing', '未知')}
• 适用场景: {', '.join(best_tool['tool'].get('best_for', [])[:3])}
目标用户: {', '.join(best_tool['tool'].get('target_users', [])[:2])}
""")
# 风险提醒
report_lines.append("=" * 70)
report_lines.append("⚠️ 风险提醒")
report_lines.append("=" * 70)
report_lines.append("""
1. 评分基于公开信息整理,实际体验可能存在差异
2. 定价可能随服务商政策调整,请以官方最新为准
3. 部分工具需要科学上网,请确认使用环境
4. 涉及敏感数据时,优先考虑数据隐私合规
5. 建议先用免费额度或试用版进行实际体验
""")
# 适用人群建议
report_lines.append("=" * 70)
report_lines.append("👥 适用人群建议")
report_lines.append("=" * 70)
for item in tool_scores:
tool = item['tool']
report_lines.append(f"\n• {tool['name']}: {', '.join(tool.get('target_users', ['通用用户']))}")
report_lines.append("\n" + "=" * 70)
report_lines.append("📌 使用建议")
report_lines.append("=" * 70)
report_lines.append("""
1. 明确核心需求:先确定最看重的1-2个维度
2. 试用验证:建议先使用免费额度实际体验
3. 关注生态:考虑与其他工具的协同效果
4. 预留灵活性:可同时使用多个工具互补
5. 定期复盘:AI工具更新快,建议定期重新评估
""")
report_lines.append("\n" + "=" * 70)
report_lines.append("报告生成时间: " + __import__('datetime').datetime.now().strftime("%Y-%m-%d %H:%M"))
report_lines.append("=" * 70)
return "\n".join(report_lines)
# ==================== 主流程 ====================
def main():
"""主流程"""
print("\n" + "=" * 60)
print(" 🚀 欢迎使用 AI Tools Evaluator")
print(" AI 工具对比评测助手 V1")
print("=" * 60)
# 1. 加载工具池
tools_data = load_tools()
print(f"\n✅ 已加载 {sum(len(c['tools']) for c in tools_data['categories'])} 个AI工具")
# 2. 选择工具
selected_tools = print_category_tools(tools_data)
if not selected_tools:
print("\n❌ 工具选择失败,程序退出")
sys.exit(1)
print(f"\n✅ 已选择 {len(selected_tools)} 个工具:")
for t in selected_tools:
print(f" - {t['name']}")
# 3. 选择使用场景
use_case = select_use_case()
print(f"\n✅ 已选择场景: {use_case}")
# 4. 选择预算
budget = select_budget()
print(f"\n✅ 已选择预算: {budget}")
# 5. 确认评测维度
weights = confirm_dimensions()
print(f"\n✅ 已确认维度权重")
# 6. 生成报告
report = generate_report(selected_tools, use_case, budget, weights)
# 7. 显示报告
print_header("📄 对比报告已生成")
print(report)
# 8. 保存报告
report_file = "ai_tools_report.md"
with open(report_file, 'w', encoding='utf-8') as f:
f.write(report)
print(f"\n✅ 报告已保存至: {report_file}")
# 9. 询问是否复制
print("\n是否复制报告内容到剪贴板?(y/n): ", end="")
try:
if input().strip().lower() == 'y':
try:
import pyperclip
pyperclip.copy(report)
print("✅ 报告已复制到剪贴板")
except ImportError:
print("⚠️ 剪贴板功能需要安装 pyperclip: pip install pyperclip")
print(" 报告已保存到文件,可手动复制")
except EOFError:
print("\n 跳过复制(无终端输入)")
print("\n" + "=" * 60)
print(" 👋 感谢使用 AI Tools Evaluator!")
print("=" * 60)
if __name__ == "__main__":
main()