Skill: fly5661/ccdb-factor-search (version 0.1.5)

--- name: ccdb-factor-search description: CCDB碳因子查询与匹配。Find and select the best-fit CCDB carbon / emission factor for PCF, LCA, carbon accounting, ESG, and supply-chain work — not just a raw search result list. Supports 碳因子 / 排放因子 / 碳足迹因子 / emission factor / carbon factor / ecoinvent / ecoinvent factor / ecoinvent 因子 / 因子匹配 / 选因子 / LCA因子 / PCF因子 / scope 3 因子 / BOM因子匹配. Covers a broad range of factor sources across China and international datasets, including ecoinvent-related records and other mainstream public or institutional sources. Also activate proactively when 产品碳足迹计算 / LCA建模 / 碳核算 / 供应链排放测算 clearly need factor data even if the user did not explicitly say "查因子". --- # CCDB Factor Search Find the **best-fit CCDB emission factor** for carbon footprint, PCF, LCA, and carbon accounting work — **not just a raw result list**. This skill is built for the real business question: > **Which factor should I actually use?** It searches in **Chinese + English**, compares candidates, filters weak matches, explains risks, and returns a **recommended factor with direct-use guidance**. It is especially useful when the database is broad and the user needs help choosing from **domestic + international sources**, including **ecoinvent** and other mainstream institutional datasets. --- ## Why this is better than plain search - **plain search** → returns a list of possible factors - **this skill** → recommends the best-fit factor for the actual use case - **plain search** → easy to mis-pick wrong region / wrong unit / wrong factor type - **this skill** → filters weak matches and explains risks before use - **plain search** → stops at retrieval - **this skill** → supports real carbon-accounting decisions --- ## What this skill does It can: - search CCDB factors in **Chinese + English** - compare multiple candidates and select the **best-fit** one - distinguish **carbon footprint factor** vs **emission factor** - reject weak matches such as wrong-region, wrong-unit, or spend-based factors - explain whether a result is safe to use directly, review-first, estimate-only, or not suitable - work across a **broad factor data range**, including **China + international sources** and **ecoinvent-related records where available in CCDB** --- ## Best-fit use cases Use this skill when you need to: - 查碳因子 / 排放因子 / 碳足迹因子 - 因子匹配 / 选因子 / 因子筛选 - LCA 因子 / PCF 因子 / scope 3 因子 - BOM 因子匹配 / 供应商材料因子匹配 - 判断某个因子能不能直接用于正式报告 - 区分 **carbon footprint factor** vs **emission factor** - 避免选错 **地区 / 单位 / 因子类型 / 金额口径因子** Also activate proactively when: - user requests 产品碳足迹建模 / LCA建模 / 排放清单核算 / 情景测算 and factor data is clearly needed - user provides BOM or material list and asks for carbon footprint calculation - task involves 供应链碳排放 / scope 3 核算 and activity data is present but no factor has been supplied --- ## Before vs after ### User asks > 帮我找中国最新全国电力因子。 ### Plain search might return - multiple electricity-related candidates - mixed carbon-footprint vs emission-factor results - unclear region / year / direct-use suitability ### This skill returns - one recommended candidate - why it was selected - risk notes - alternatives considered - whether it is safe for direct use or should be reviewed first --- ## Very short examples - 查询最新中国全国电力因子 - 帮我找聚酯切片的碳因子 - 这个因子能不能直接用于正式报告？ - Compare carbon footprint factor vs emission factor for electricity - Find the best CCDB factor for primary aluminium --- ## How to invoke ### Natural-language examples - `查询最新中国全国电力因子` - `帮我找聚酯切片的碳因子，如果中文结果不好就切英文继续找` - `这个因子能不能直接用于正式报告？` - `Compare carbon footprint factor vs emission factor for electricity` - `Find the best CCDB factor for primary aluminium, prefer physical-unit factor` ### Script examples ```bash python3 scripts/query_ccdb.py --auto --user-request "查询最新的中国全国电力因子，单位最好是 kgCO2e/kWh。" python3 scripts/query_ccdb.py --query "electricity" --lang en --top 5 ``` --- ## Typical example prompts ### Example 1 > 查询最新的中国全国电力因子，单位最好是 kgCO2e/kWh。 Expected behavior: - prioritize China electricity candidates - prefer recent applicable years - distinguish carbon footprint factor vs emission factor - return direct-use guidance ### Example 2 > 帮我找聚酯切片的碳因子，如果中文结果不好就切英文继续找。 Expected behavior: - derive PET / polyester synonyms - search bilingually - compare candidates across rounds - return one recommended factor plus alternatives ### Example 3 > 请帮我找原铝的排放因子，优先物理量单位，不要误选成按金额计算的因子。 Expected behavior: - reject or downgrade spend-based factors - prefer physical-unit candidates - explain why the chosen factor is safer ### Example 4 > 这个因子能不能直接用于正式报告？ Expected behavior: - explain whether it is direct-use / needs review / estimate-only / not suitable --- ## Standard output example ```yaml 推荐结果: 匹配等级: close_match 因子名称: 电力 因子值: 0.5777 单位: kgCO2e/kWh 适用地区: 中国 适用年份开始: 2024 适用年份结束: 2024 发布年份: 2024 来源机构: 生态环境部 来源级别: 国家排放因子 使用建议: 建议人工复核后使用 风险与注意事项: - 这是碳足迹因子，不等同于 CO2 排放因子 - 若用于正式核算或核查，请先确认适用口径 ``` --- ## Key fields to return when possible A good result should explain these fields clearly: - 因子名称 / name - 因子值 / factor value - 单位 / unit - 适用地区 / countries - 适用年份开始 / 结束 / applyYear ~ applyYearEnd - 发布年份 / year - 来源机构 / institution - 来源级别 / sourceLevel - 来源说明 / source - 使用建议 / direct-use guidance --- ## Match classes - `direct_match` → highly aligned, usually safe to use after quick sanity check - `close_match` → mostly aligned, should usually be reviewed before formal reporting - `fallback_generic` → usable only as rough estimate / placeholder - `not_suitable` → should not be used directly - `api_unavailable` → no recommendation; retry later --- ## What this skill must do ### 1. Parse the real search intent Identify as much as possible from the request: - material / process / activity - region - year - unit - use purpose - whether the user wants 碳足迹因子 or 排放因子 ### 2. Search bilingually For non-trivial factor matching, do not search in only one language. Always try: - Chinese core term - English equivalent - a few nearby synonyms where needed ### 3. Rank candidates instead of trusting the first hit Do not judge a factor from one field only. Key ranking dimensions include: - semantic fit (`name`, `description`, `specification`) - region fit (`countries`) - unit fit (`unit`) - applicability time (`applyYear` ~ `applyYearEnd`) - publication year (`year`) - authority (`institution`, `sourceLevel`) - factor-type fit (碳足迹因子 vs 排放因子) ### 4. Be conservative Do not force a recommendation when evidence is weak. Prefer: - `not_suitable` - `api_unavailable` over a misleading confident answer. ### 5. Explain the choice The final answer should explain: - what was selected - why it was selected - what risks remain - what alternatives were considered - whether the result can be used directly or only as reference --- ## Key working rules ### Carbon footprint factor vs emission factor These are not always interchangeable. - If the user explicitly asks for **碳足迹 / carbon footprint / PCF**, prefer carbon footprint factors. - If the user explicitly asks for **排放因子 / CO2 emission factor / emissions accounting**, prefer emission factors. - If the user only says something vague like “电力因子”, warn that multiple factor types may exist and should not be mixed directly. ### China-first bias for Chinese requests If: - the request is in Chinese - no explicit region is given - the query is geo-sensitive (especially 电力 / 蒸汽 / 天然气) then Chinese candidates should be preferred by default. ### Region warning for geo-sensitive factors For electricity / steam / natural gas queries, if region is missing, surface that clearly as a risk. ### Latest-factor requests If the user asks for “最新 / latest”, ranking should prefer more recent `applyYear`, not only lexical similarity. ### No spend-based mismatch If the user wants a physical activity factor, do not recommend spend-based / monetary-unit factors as if they were equivalent. --- ## Implementation notes - Main script: `scripts/query_ccdb.py` - API contract: `references/api-contract.md` - Matching logic notes: `references/matching-strategy.md` - Output template: `references/output-template.md` If the API contract changes, update the script and `references/api-contract.md` together. Keep scoring / filtering logic in code rather than overloading SKILL.md with implementation detail. --- ## Advanced fallback / debugging ### Script unavailable fallback If `scripts/query_ccdb.py` is missing or fails to run, fall back to a direct API call: ```bash curl -s -X POST https://gateway.carbonstop.com/management/system/website/searchFactorDataMcp \ -H 'Content-Type: application/json' \ -d '{"sign":"<md5(\"mcp_ccdb_search\"+keyword)>","name":"<keyword>","lang":"zh"}' ``` The sign is `md5("mcp_ccdb_search" + keyword)`. In Python: ```python import hashlib, requests keyword = "电力" sign = hashlib.md5(("mcp_ccdb_search" + keyword).encode()).hexdigest() resp = requests.post("https://gateway.carbonstop.com/management/system/website/searchFactorDataMcp", json={"sign": sign, "name": keyword, "lang": "zh"}) print(resp.json()) ``` Even in fallback mode, apply the same ranking, matching, and output rules defined above. Do not return raw API results without analysis. --- ## Packaging guidance For public packaging, keep the skill folder lean. Recommended public package contents: - `SKILL.md` - `README.md` - `_meta.json` - `CHANGELOG.md` - `scripts/query_ccdb.py` - `references/api-contract.md` - `references/matching-strategy.md` - `references/output-template.md` - `evals/evals.json` Draft notes and publishing scratch files should not be included in the final public package.

path: CHANGELOG.md
content: # Changelog

## 0.1.4

- added Chinese keywords to description frontmatter for better ClawHub search discoverability (`CCDB碳因子查询与匹配`)
- extended description to include proactive activation rule: activate when LCA / PCF / 碳核算 tasks need factor data even if user did not explicitly say "查因子"
- added proactive trigger section in "When to use this skill" for BOM/LCA/scope-3 implicit invocation scenarios
- added script-unavailable fallback section with direct API call examples (curl + Python)
- fixed `skill_name` typo in `evals/evals.json` (`ccdb-carbon-factor-search` → `ccdb-factor-search`)
- bumped version to 0.1.4

## 0.1.3

- upgraded README into a fuller public-facing skill page with capabilities, use cases, output fields, factor-type guidance, and API field checklist
- updated `references/api-contract.md` with confirmed field meanings (`year`, `applyYear`, `applyYearEnd`, `countries`, `sourceLevel`)
- fixed English raw-term truncation to avoid mid-word clipping
- added China-first bias for Chinese requests without explicit region on geo-sensitive factors
- added missing-region warning for electricity / steam / natural gas queries
- added recency bonus for latest-factor requests based on applicability year
- strengthened authority weighting for stronger official / international sources
- distinguished carbon-footprint factors vs emission factors in ranking and risk output
- added direct-use guidance in final recommended result

## 0.1.2

- rewrote README to improve ClawHub presentation and conversion
- clarified business use cases, examples, and value proposition
- made conservative matching / no-guessing behavior more explicit
- added clearer output structure and positioning vs plain search

## 0.1.1

- added `_meta.json`
- improved conservative API error handling
- expanded eval coverage
- clarified README and publishing notes

## 0.1.0

- initial beta release


path: CLAWHUB_PUBLISH_COMMAND.md
content: # ClawHub Publish Command

## 1. Login first

```bash
clawhub login
clawhub whoami
```

## 2. Publish command

```bash
clawhub publish /root/.openclaw/workspace/skills/ccdb-factor-search \
  --slug ccdb-factor-search \
  --name "CCDB Factor Search" \
  --version 0.1.3 \
  --tags carbon,emission-factor,ccdb,sustainability,lca,search \
  --changelog "0.1.3 improves best-fit factor selection with richer README/docs, confirmed API field meanings, better geo-sensitive matching, latest-factor recency handling, stronger authority weighting, clearer carbon-footprint vs emission-factor distinction, and safer direct-use guidance."
```

## 3. Suggested follow-up checks

After publishing, check:

```bash
clawhub whoami
clawhub inspect ccdb-factor-search
```

If later you update the skill, bump the version and publish again.


path: CLAWHUB_PUBLISH_COPY.md
content: # ClawHub Publish Copy

## Suggested title
CCDB Factor Search

## Suggested slug
ccdb-factor-search

## Suggested release stage
Beta / Preview

## Short summary
Search and select the best-fit CCDB emission factor from Carbonstop APIs using bilingual query expansion, smallest-core-term search, multi-round candidate comparison, suitability filtering, and structured result explanations.

## Full description
`ccdb-factor-search` helps find the most suitable CCDB / Carbonstop emission factor for a user's scenario instead of simply returning a raw query result list.

It works by extracting a minimal core search term from the user's request, searching in both Chinese and English, expanding only when necessary, comparing candidates across multiple rounds, rejecting obviously unsuitable matches, and returning a reasoned recommendation with risks, alternatives, and search trace.

This skill is especially useful when users want to:
- 查找或匹配排放因子 / 碳因子
- 从 CCDB / Carbonstop API 中找到“最合适”的因子
- 对多个候选因子做适配性判断
- 在中英文之间切换检索词
- 在结果不理想时继续优化搜索词
- 区分 direct match / close match / fallback / not suitable

## Suggested tags
carbon, emission-factor, ccdb, sustainability, lca, search

## Changelog
0.1.3 improves best-fit factor selection with richer README/docs, confirmed API field meanings, better geo-sensitive matching, latest-factor recency handling, stronger authority weighting, clearer carbon-footprint vs emission-factor distinction, and safer direct-use guidance.

## Known limitations
- This skill depends on the Carbonstop CCDB API.
- Some factor values are encrypted in the API response.
- Some scenarios may only yield close/fallback matches rather than direct matches.
- Steam-related queries may still return broader heat/steam fallback candidates depending on data coverage.

## Recommended publish note
This is a beta release focused on best-fit factor retrieval and suitability judgment. It is intentionally conservative: when no reliable candidate is found, it should prefer `not_suitable` over misleading recommendations.


path: CLAWHUB_RELEASE.md
content: # ClawHub Release Material

## Suggested title
ccdb-factor-search

## Suggested short summary
Search and select the best-fit CCDB emission factor from Carbonstop APIs using bilingual query expansion, multi-round candidate comparison, suitability filtering, and structured result explanations.

## Suggested version
0.1.3

## Suggested full description
`ccdb-factor-search` helps find the most suitable CCDB / Carbonstop emission factor for a user's scenario instead of just returning a raw query result list. It extracts a minimal core search term, searches in both Chinese and English, expands only when necessary, compares candidates across multiple rounds, rejects obviously unsuitable results, and returns a reasoned recommendation with risks, alternatives, and search trace.

It is especially useful when users ask to:
- 查找或匹配排放因子 / 碳因子
- 比较多个候选因子是否适合当前业务场景
- 根据地区、单位、年份、工艺或场景筛选更合适的 CCDB 因子
- 在中英文之间切换检索词
- 对检索结果做“direct / close / fallback / not suitable”判断

## Suggested release stage
Beta / Preview

Reason:
- electricity, natural gas, polyester-related, and aluminium-related cases already show useful behavior
- some steam scenarios still rely on fallback behavior depending on data coverage
- real-world usage feedback will further improve vocabulary and matching rules

## Suggested tags / positioning
- carbon
- emission-factor
- ccdb
- search
- sustainability
- lca
- factor-matching

## Known limitations to disclose
- API-dependent skill
- encrypted factor values may prevent direct numeric output in some cases
- steam scenarios may still return broader heat/steam matches
- data quality and coverage affect final matching quality

## Recommended publish note
This is a 0.1.3 beta release focused on best-fit factor retrieval and suitability judgment. It is designed to be conservative: when no reliable candidate is found, it should prefer `not_suitable` over misleading recommendations.


path: PUBLISHING_NOTES.md
content: # Publishing Notes

## Intended use

This skill is for CCDB / Carbonstop carbon-factor lookup where the user needs the most suitable factor, not just a raw result list.

## Differentiators

- extracts search intent from natural-language requests
- searches in both Chinese and English
- iteratively refines search terms
- compares candidates across multiple rounds
- downgrades unsuitable factors (wrong region, wrong unit, spend-based mismatch, etc.)
- returns a reasoned recommendation with risks and alternatives

## Runtime dependency

This skill depends on access to the Carbonstop CCDB API.

Current default endpoint:
- `https://gateway.carbonstop.com/management/system/website/queryFactorListClaw`

The endpoint can still be overridden with `CCDB_API_BASE_URL` when needed.

## Environment variables

- `CCDB_API_BASE_URL` (optional; defaults to the production endpoint above)
- `CCDB_SIGN_PREFIX` (optional; defaults to `openclaw_ccdb`)

## Pre-publish checklist

- confirm production API endpoint
- confirm rate limits / auth behavior
- verify 5+ representative evals
- review term lexicon with domain experts
- confirm output wording is suitable for end users

## Suggested publish summary

A CCDB carbon-factor matching skill for Carbonstop APIs. It extracts keywords from user requests, searches both Chinese and English terms, iteratively refines search keywords, ranks candidates, rejects unsuitable matches, and returns the best-fit emission factor with reasoning and risk notes.


path: README.md
content: # CCDB Factor Search

Find the **best-fit CCDB emission factor** for carbon footprint, PCF, LCA, carbon accounting, ESG, and supply-chain work — **not just a raw result list**.

This skill is built for the real business question:

> **Which factor should I actually use?**

It searches in **Chinese + English**, compares candidates, filters weak matches, explains risks, and returns a **recommended factor with direct-use guidance**.

It is especially useful when the database is broad and the user needs help choosing from **domestic + international sources**, including **ecoinvent** and other mainstream institutional datasets.

---

## Why this is better than plain search

- **plain search** → returns a list of possible factors
- **this skill** → recommends the best-fit factor for the actual use case
- **plain search** → easy to mis-pick wrong region / wrong unit / wrong factor type
- **this skill** → filters weak matches and explains risks before use
- **plain search** → stops at retrieval
- **this skill** → supports real carbon-accounting decisions

In short:
- plain search → “here are 10 factors”
- this skill → “this is the best candidate, here’s why, here are the risks, and here are the backups”

---

## Best-fit use cases

Use this skill when you need to:

- 查碳因子 / 排放因子 / 碳足迹因子
- 因子匹配 / 选因子 / 因子筛选
- LCA 因子 / PCF 因子 / scope 3 因子
- BOM 因子匹配 / 供应商材料因子匹配
- 判断某个因子能不能直接用于正式报告
- 区分 **carbon footprint factor** vs **emission factor**
- 避免选错 **地区 / 单位 / 因子类型 / 金额口径因子**

Also activate proactively when:
- the task clearly needs factor data for 产品碳足迹建模 / LCA建模 / 排放清单核算 / 情景测算
- the user provides BOM or material list and asks for carbon footprint calculation
- the task involves 供应链碳排放 / scope 3 核算 and activity data is present but no factor has been supplied

---

## Broad factor coverage

This skill is designed for **broad factor retrieval + selection**, not just narrow single-source lookup.

It can help the user search and choose across:
- **China domestic factor sources**
- **international factor sources**
- **institutional / published datasets**
- **ecoinvent-related records available in CCDB**

This matters because many real-world carbon-accounting tasks do not fail at "finding any factor" — they fail at **choosing the right one from a wide set of possible sources**.

---

## Before vs after

### User asks
> 帮我找中国最新全国电力因子。

### Plain search might return
- multiple electricity-related candidates
- mixed carbon-footprint vs emission-factor results
- unclear region / year / direct-use suitability

### This skill returns
- one recommended candidate
- why it was selected
- risk notes
- alternatives considered
- whether it is safe for direct use or should be reviewed first

---

## What this skill does

It can:
- search CCDB factors in **Chinese + English**
- compare multiple candidates and select the **best-fit** one
- distinguish **carbon footprint factor** vs **emission factor**
- reject weak matches such as wrong-region, wrong-unit, or spend-based factors
- explain whether a result is safe to use directly, review-first, estimate-only, or not suitable
- work across a **broad factor data range**, including **China + international sources** and **ecoinvent-related records where available in CCDB**

---

## Very short examples

- 查询最新中国全国电力因子
- 帮我找聚酯切片的碳因子
- 这个因子能不能直接用于正式报告？
- Compare carbon footprint factor vs emission factor for electricity
- Find the best CCDB factor for primary aluminium
- Search ecoinvent-related factors available in CCDB for resin materials

---

## How to invoke

### Natural-language examples
- `查询最新中国全国电力因子`
- `帮我找聚酯切片的碳因子，如果中文结果不好就切英文继续找`
- `这个因子能不能直接用于正式报告？`
- `Compare carbon footprint factor vs emission factor for electricity`
- `Find the best CCDB factor for primary aluminium, prefer physical-unit factor`
- `Search ecoinvent-related factors available in CCDB for polyester resin`

### Script examples
```bash
python3 scripts/query_ccdb.py --auto --user-request "查询最新的中国全国电力因子，单位最好是 kgCO2e/kWh。"
python3 scripts/query_ccdb.py --query "electricity" --lang en --top 5
```

---

## Typical example prompts

### Example 1 — latest China electricity factor
> 查询最新的中国全国电力因子，单位最好是 kgCO2e/kWh。

Expected behavior:
- prioritize China electricity candidates
- prefer recent applicable years
- distinguish carbon footprint factor vs emission factor
- return direct-use guidance

### Example 2 — bilingual material lookup
> 帮我找聚酯切片的碳因子，如果中文结果不好就切英文继续找。

Expected behavior:
- derive PET / polyester synonyms
- search bilingually
- compare candidates across rounds
- return one recommended factor plus alternatives

### Example 3 — conservative screening
> 请帮我找原铝的排放因子，优先物理量单位，不要误选成按金额计算的因子。

Expected behavior:
- reject or downgrade spend-based factors
- prefer physical-unit candidates
- explain why the chosen factor is safer

### Example 4 — ecoinvent-related lookup
> 帮我找 CCDB 里和 ecoinvent 相关的聚合物因子，优先能直接用于 LCA 建模的。

Expected behavior:
- search broad source candidates
- surface ecoinvent-related records where available in CCDB
- compare candidates by unit / region / suitability
- explain whether the result is safe for direct use or needs review

### Example 5 — suitability review
> 这个因子能不能直接用于正式报告？

Expected behavior:
- explain whether it is direct-use / needs review / estimate-only / not suitable

---

## Standard output example

```yaml
推荐结果:
  匹配等级: close_match
  因子名称: 电力
  因子值: 0.5777
  单位: kgCO2e/kWh
  适用地区: 中国
  适用年份开始: 2024
  适用年份结束: 2024
  发布年份: 2024
  来源机构: 生态环境部
  来源级别: 国家排放因子
  使用建议: 建议人工复核后使用

风险与注意事项:
  - 这是碳足迹因子，不等同于 CO2 排放因子
  - 若用于正式核算或核查，请先确认适用口径
```

---

## Key fields to return when possible

A good result should explain these fields clearly:
- 因子名称 / name
- 因子值 / factor value
- 单位 / unit
- 适用地区 / countries
- 适用年份开始 / 结束 / applyYear ~ applyYearEnd
- 发布年份 / year
- 来源机构 / institution
- 来源级别 / sourceLevel
- 来源说明 / source
- 使用建议 / direct-use guidance

---

## Match classes

- `direct_match` → highly aligned, usually safe to use after quick sanity check
- `close_match` → mostly aligned, should usually be reviewed before formal reporting
- `fallback_generic` → usable only as rough estimate / placeholder
- `not_suitable` → should not be used directly
- `api_unavailable` → no recommendation; retry later

---

## Carbon footprint factor vs emission factor

These are not always interchangeable.

For some categories — especially electricity — available results may include both:
- **碳足迹因子 / carbon footprint factor**
- **二氧化碳排放因子 / CO2 emission factor**

Recommended handling:
- if the user explicitly asks for **碳足迹 / carbon footprint / PCF**, prefer carbon footprint factors
- if the user explicitly asks for **排放因子 / CO2 emission factor / emissions accounting**, prefer emission factors
- if the user only says something vague like “电力因子”, warn that multiple factor types may exist and should not be mixed directly

---

## Key working rules

### Search bilingually
For non-trivial factor matching, do not search in only one language.
Always try:
- Chinese core term
- English equivalent
- nearby synonyms where needed

### Rank candidates instead of trusting the first hit
Do not judge a factor from one field only.
Key ranking dimensions include:
- semantic fit (`name`, `description`, `specification`)
- region fit (`countries`)
- unit fit (`unit`)
- applicability time (`applyYear` ~ `applyYearEnd`)
- publication year (`year`)
- authority (`institution`, `sourceLevel`)
- factor-type fit (碳足迹因子 vs 排放因子)

### Be conservative
Do not force a recommendation when evidence is weak.
Prefer:
- `not_suitable`
- `api_unavailable`

over a misleading confident answer.

### China-first bias for Chinese requests
If:
- the request is in Chinese
- no explicit region is given
- the query is geo-sensitive (especially 电力 / 蒸汽 / 天然气)

then Chinese candidates should be preferred by default.

### Region warning for geo-sensitive factors
For electricity / steam / natural gas queries, if region is missing, surface that clearly as a risk.

### Latest-factor requests
If the user asks for “最新 / latest”, ranking should prefer more recent `applyYear`, not only lexical similarity.

### No spend-based mismatch
If the user wants a physical activity factor, do not recommend spend-based / monetary-unit factors as if they were equivalent.

---

## Implementation notes

- Main script: `scripts/query_ccdb.py`
- API contract: `references/api-contract.md`
- Matching logic notes: `references/matching-strategy.md`
- Output template: `references/output-template.md`

If the API contract changes, update the script and `references/api-contract.md` together.

Keep scoring / filtering logic in code rather than overloading the skill page with implementation detail.

---

## Advanced fallback / debugging

### Script unavailable fallback

If `scripts/query_ccdb.py` is missing or fails to run, fall back to a direct API call:

```bash
curl -s -X POST https://gateway.carbonstop.com/management/system/website/searchFactorDataMcp \
  -H 'Content-Type: application/json' \
  -d '{"sign":"<md5(\"mcp_ccdb_search\"+keyword)>","name":"<keyword>","lang":"zh"}'
```

The sign is `md5("mcp_ccdb_search" + keyword)`. In Python:
```python
import hashlib, requests
keyword = "电力"
sign = hashlib.md5(("mcp_ccdb_search" + keyword).encode()).hexdigest()
resp = requests.post("https://gateway.carbonstop.com/management/system/website/searchFactorDataMcp",
    json={"sign": sign, "name": keyword, "lang": "zh"})
print(resp.json())
```

Even in fallback mode, apply the same ranking, matching, and output rules defined above. Do not return raw API results without analysis.

---

## Packaging guidance

For public packaging, keep the skill folder lean.
Recommended public package contents:
- `SKILL.md`
- `README.md`
- `_meta.json`
- `CHANGELOG.md`
- `scripts/query_ccdb.py`
- `references/api-contract.md`
- `references/matching-strategy.md`
- `references/output-template.md`
- `evals/evals.json`

Draft notes and publishing scratch files should not be included in the final public package.


path: _meta.json
content: ownerId: fly5661
slug: ccdb-factor-search
version: 0.1.4
publishedAt: 0

path: evals/evals.json
content: {
  "skill_name": "ccdb-factor-search",
  "evals": [
    {
      "id": 1,
      "prompt": "请帮我找中国全国电网电力因子，单位最好是tCO₂e/MWh。",
      "expected_output": "Should find electricity-related candidates and rank the China/tCO₂e/MWh candidate highest.",
      "files": [],
      "expectations": [
        "The result includes electricity-related candidates",
        "The best match explains why the China candidate is preferred",
        "The output records attempted Chinese and English search terms"
      ]
    },
    {
      "id": 2,
      "prompt": "帮我检索聚酯切片的碳因子，如果中文结果不好就切英文继续找。",
      "expected_output": "Should derive Chinese and English search terms and attempt iterative search.",
      "files": [],
      "expectations": [
        "The output contains both Chinese and English search terms",
        "The output shows iterative search behavior when needed",
        "The output gives a suitability classification"
      ]
    },
    {
      "id": 3,
      "prompt": "我要找外购蒸汽的排放因子，看看返回的因子是否适合，不适合就继续调整搜索词。",
      "expected_output": "Should analyze suitability and either choose a close factor or explain why additional refinement is needed.",
      "files": [],
      "expectations": [
        "The output includes suitability judgment",
        "The output explains mismatch risk or fallback reasoning"
      ]
    },
    {
      "id": 4,
      "prompt": "请帮我找原铝的排放因子，优先物理量单位，不要误选成按金额计算的因子。",
      "expected_output": "Should reject or downgrade spend-based or monetary-unit candidates if physical-activity factors are expected.",
      "files": [],
      "expectations": [
        "The output flags spend-based or monetary-unit mismatch when present",
        "The output prefers physical activity-based units"
      ]
    },
    {
      "id": 5,
      "prompt": "帮我找中国的天然气因子，若没有完全匹配就给最接近的候选并说明风险。",
      "expected_output": "Should return the best available candidate with explicit risk explanation if there is no direct match.",
      "files": [],
      "expectations": [
        "The output contains a best-match explanation",
        "The output contains risk notes when only close/fallback matches exist"
      ]
    },
    {
      "id": 6,
      "prompt": "请帮我找江苏地区电力排放因子，单位最好是kgCO2e/kWh，没有就明确告诉我缺什么约束。",
      "expected_output": "Should incorporate region and unit constraints, and explicitly surface missing constraints or mismatches when exact fit is weak.",
      "files": [],
      "expectations": [
        "The output reflects region and unit constraints",
        "The output explains whether the region and unit are matched, missing, or mismatched"
      ]
    },
    {
      "id": 7,
      "prompt": "我要找英国蒸汽因子，如果 API 没返回可用结果，不要猜，直接说明查不到。",
      "expected_output": "Should fail conservatively when API results are unavailable or unsuitable, instead of inventing a recommendation.",
      "files": [],
      "expectations": [
        "The output can return not_suitable or api_unavailable conservatively",
        "The output does not fabricate a recommended factor when evidence is weak"
      ]
    },
    {
      "id": 8,
      "prompt": "请比较 PET resin 和 polyester chip 相关因子，给我最合适的一个，并附上备选项。",
      "expected_output": "Should search bilingual PET/polyester terms, compare candidates, and return one recommendation with alternatives.",
      "files": [],
      "expectations": [
        "The output contains bilingual or cross-term search behavior",
        "The output includes one selected factor and alternative candidates"
      ]
    }
  ]
}


path: references/api-contract.md
content: # CCDB API Contract

## Current endpoint

`POST https://gateway.carbonstop.com/management/system/website/queryFactorListClaw`

## Request headers

Minimum required headers for script usage:
- `Content-Type: application/json`
- `Accept: application/json, text/plain, */*`

## Request body

```json
{
  "sign": "<md5 businessLabel+name>",
  "name": "电力",
  "lang": "zh"
}
```

## Signing rule

- `businessLabel` is currently `openclaw_ccdb`
- `sign = md5("openclaw_ccdb" + name)`

## Response shape

Top-level fields:
- `code`
- `msg`
- `total`
- `rows`

Common candidate fields observed:
- `id`
- `name`
- `nameEn`
- `description`
- `specification`
- `unit`
- `cValue`
- `countries`
- `area`
- `year`
- `applyYear`
- `applyYearEnd`
- `institution`
- `source`
- `sourceLevel`
- `documentType`
- `factorClassify`

## Field interpretation checklist

- `year` → 发布年份
- `applyYear` → 适用年份开始时间
- `applyYearEnd` → 适用年份结束时间
- `countries` → 发布国家 / 地区
- `area` → 可忽略
- `sourceLevel` → 来源级别

## Matching guidance

When ranking candidates, consider together:
1. semantic fit in `name` / `description` / `specification`
2. region fit in `countries`
3. unit fit in `unit`
4. applicability time in `applyYear` ~ `applyYearEnd`
5. publication recency in `year`
6. authority in `institution` / `sourceLevel`
7. factor-type fit: 碳足迹因子 vs 排放因子

## Caveats

- `cValue` may be encrypted / unavailable for unpaid data
- the current interface exposes a lightweight query body; precision depends heavily on post-query ranking
- suitability ranking should not rely on a single field


path: references/domain-lexicon.md
content: # Domain Lexicon

Use this lexicon to expand search terms before or during iterative search.

## Energy
- 电力 → electricity, grid electricity, purchased electricity
- 外购电 → purchased electricity, outsourced electricity, grid electricity
- 电网电力 → grid electricity
- 蒸汽 → steam
- 外购蒸汽 → purchased steam, imported steam, steam
- 天然气 → natural gas
- 柴油 → diesel
- 汽油 → gasoline, petrol
- 煤 → coal

## Materials
- 聚酯切片 → polyester chip, PET chip, PET resin
- PET树脂 → PET resin
- 原铝 → primary aluminium, primary aluminum
- 铝锭 → aluminium ingot, aluminum ingot
- 钢材 → steel
- 铜 → copper
- 纸箱 → corrugated box, carton board, cardboard box
- 塑料包装 → plastic packaging

## Logistics / Transport
- 公路运输 → road transport, trucking
- 海运 → ocean freight, sea freight
- 空运 → air freight
- 铁路运输 → rail freight

## Lifecycle / Scenario cues
- cradle-to-gate → 从摇篮到大门, 原材料到出厂
- gate-to-gate → 厂内生产阶段, 从门到门
- 范畴3 → scope 3
- 生产资料 → purchased goods, capital goods, upstream materials

## Use guidance

When the first query is weak:
1. switch between Chinese and English equivalents
2. try broader parent terms
3. try more process-specific terms
4. use lifecycle or scenario cues only when the user explicitly needs them


path: references/matching-strategy.md
content: # Matching Strategy

Use this strategy to select the most suitable CCDB factor, not merely the first returned factor.

## Mandatory search policy

For non-trivial requests, do all of the following unless the user explicitly narrows scope:
1. search with the strongest Chinese term
2. search with at least one Chinese synonym or broader/narrower Chinese variant
3. search with the strongest English equivalent
4. search with at least one English synonym or alternative wording
5. compare all high-scoring candidates across all runs before selecting a winner

## Constraints to extract and preserve

Always try to preserve these constraints from the user request:
- target object or activity
- process / stage / scenario
- geography / grid / country / region
- unit
- year / applicability period
- source preference if the user implies it

## Candidate selection hierarchy

Prefer candidates in this order:
1. direct semantic match + matching region + matching unit
2. direct semantic match + matching region + compatible unit
3. direct semantic match + weaker region match + compatible unit
4. broader parent-category fallback with explicit warning

## Red flags

Downgrade or reject candidates when:
- country/region conflicts with the user requirement
- unit is clearly incompatible with the intended use
- the result refers to a different lifecycle stage or category
- the result is too generic while a more specific candidate exists
- the result is economically-based or scope-3-spend-based when the user wanted physical activity-based factors

## Output classification

Classify the chosen result as one of:
- direct_match
- close_match
- fallback_generic
- not_suitable

## Multi-round refinement guidance

If the first search set does not yield a reliable result, refine in this order:
1. remove modifiers and test the core noun
2. add process keyword
3. add or remove geography keyword
4. switch Chinese ↔ English
5. broaden to parent category
6. try common domain synonyms

## Required explanation in final answer

The final answer should explain:
- why the chosen factor is the best available match
- what other candidates were considered
- what mismatch risks remain
- what further clarification would improve confidence if needed


path: references/output-template.md
content: # Output Template

Use this structure when returning the final result to the user.

## 推荐结果
- 匹配等级：`direct_match | close_match | fallback_generic | not_suitable`
- 因子名称：
- 英文名称：
- 因子值：
- 单位：
- 适用地区：
- 适用年份：
- 来源机构：
- 来源说明：

## 选择原因
- 为什么它是当前最合适的结果
- 哪些字段与用户需求直接匹配
- 哪些字段只是近似匹配

## 风险与注意事项
- 地域风险
- 单位风险
- 生命周期/场景风险
- 数据是否加密 / 是否仅返回元数据

## 检索路径
- 中文词：
- 英文词：
- 调整过的词：

## 候选备选
- 候选 1：
- 候选 2：
- 候选 3：

## 结论建议
- 是否建议直接使用
- 是否建议用户补充信息后再查


path: references/workflow.md
content: # CCDB Carbon Factor Search Workflow

## Goal

Find the most suitable carbon factor from the CCDB API for the user's intended object or activity.

## Step 1: Normalize the request

Extract as many of these fields as possible:
- material / product / fuel / energy type
- process or activity
- lifecycle stage
- geography / country / region
- time period if relevant
- unit requirements
- industry context
- any user-provided benchmark or expected match

## Step 2: Build search terms

Create a ranked list of search terms:
1. exact Chinese term from user
2. normalized Chinese synonym
3. exact English translation
4. normalized English synonym
5. broader parent category
6. narrower process-specific term if available

Examples:
- “聚酯切片” → “聚酯切片”, “PET切片”, “polyester chip”, “PET resin”
- “外购电（华东电网）” → “外购电 华东电网”, “区域电网电力”, “purchased electricity East China grid”, “grid electricity East China”

## Step 3: Query iteratively

Start with the strongest term. If the result quality is weak:
- add or remove geography
- add or remove lifecycle stage
- switch Chinese ↔ English
- broaden to parent category
- narrow to process-specific term

Keep a search log.

Suggested OpenClaw helper usage:
- `python3 scripts/query_ccdb.py --query "电力" --lang zh`
- `python3 scripts/query_ccdb.py --query "electricity" --lang en`
- add `--region`, `--unit`, `--year` when the user provided those constraints

## Step 4: Evaluate suitability

Check whether returned factors match the user's need on:
- semantic object match
- lifecycle stage match
- region match
- unit match
- recency / source reliability
- whether the factor is generic vs specific

Reject factors that are clearly about a different object, stage, or region unless only fallback factors are available.

## Step 5: Return the best factor

When returning the chosen factor, explain:
- why it is the best available match
- what search path was used
- what tradeoffs remain
- whether the result is direct match / close substitute / fallback generic factor

## If nothing suitable is found

Return:
- all key search terms attempted
- why results were unsuitable
- what clarification would improve matching


path: scripts/query_ccdb.py
content: #!/usr/bin/env python3
"""CCDB factor query helper for Carbonstop website API.

Search strategy: prefer the smallest core term first, then use bilingual equivalents,
then expand only if recall is weak.
"""

import argparse
import hashlib
import json
import os
import re
import sys
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError

BUSINESS_LABEL = os.environ.get("CCDB_SIGN_PREFIX", "openclaw_ccdb")
DEFAULT_BASE_URL = os.environ.get(
    "CCDB_API_BASE_URL",
    "https://gateway.carbonstop.com/management/system/website/queryFactorListClaw",
)

CN_TO_EN = {
    "电力": ["electricity", "power"],
    "蒸汽": ["steam", "steam supply", "process steam", "industrial steam", "heat"],
    "天然气": ["natural gas", "gas"],
    "原铝": ["primary aluminium", "primary aluminum", "aluminium"],
    "铝": ["aluminium", "aluminum"],
    "聚酯切片": ["polyester chip", "PET resin", "PET chip"],
    "聚酯": ["polyester", "PET"],
    "PET": ["PET resin", "PET chip", "polyester resin"],
}

TERM_SIMPLIFY = {
    "外购电": "电力",
    "电网电力": "电力",
    "外购蒸汽": "蒸汽",
    "外购天然气": "天然气",
    "管道天然气": "天然气",
    "城市燃气": "天然气",
    "燃气": "天然气",
    "电解铝": "原铝",
    "铝锭": "原铝",
    "PET切片": "聚酯切片",
    "PET树脂": "聚酯切片",
    "聚酯树脂": "聚酯切片",
}

REGION_HINTS = [
    "中国", "全国", "华东", "华北", "华南", "华中", "东北", "西北", "西南",
    "北京", "上海", "广东", "江苏", "浙江", "山东", "欧洲", "美国", "英国", "日本", "非洲",
]

UNIT_PATTERNS = [
    r"kgCO₂e/[A-Za-z%\u4e00-\u9fa5]+",
    r"tCO₂e/[A-Za-z%\u4e00-\u9fa5]+",
    r"kgCO2e/[A-Za-z%\u4e00-\u9fa5]+",
    r"tCO2e/[A-Za-z%\u4e00-\u9fa5]+",
]

YEAR_RE = re.compile(r"(20\d{2})")
GEO_SENSITIVE_TERMS = {"电力", "蒸汽", "天然气"}
LATEST_HINTS = ["最新", "latest", "newest", "recent", "最近"]
CHINA_HINTS = ["中国", "全国"]
AUTHORITY_KEYWORDS = {
    "国家级机构": ["生态环境部", "国家", "国务院", "发改委", "工信部", "财政部", "中国"],
    "国际权威": ["ipcc", "iea", "ecoinvent", "defra", "ghg protocol"],
}
CFP_HINTS = ["碳足迹", "carbon footprint", "pcf"]
EMISSION_HINTS = ["排放因子", "二氧化碳排放", "co2 emission", "emission factor", "排放"]


def md5_hex(text: str) -> str:
    return hashlib.md5(text.encode("utf-8")).hexdigest()


def make_sign(name: str) -> str:
    return md5_hex(f"{BUSINESS_LABEL}{name}")


def normalize_text(*parts) -> str:
    return " ".join(str(p or "").strip() for p in parts if str(p or "").strip()).lower()


def contains_chinese(text: str) -> bool:
    return bool(re.search(r"[\u4e00-\u9fff]", text or ""))


def extract_region(text: str) -> str:
    for hint in REGION_HINTS:
        if hint in (text or ""):
            return hint
    return ""


def extract_unit(text: str) -> str:
    for pattern in UNIT_PATTERNS:
        m = re.search(pattern, text or "")
        if m:
            return m.group(0)
    return ""


def extract_year(text: str) -> str:
    m = YEAR_RE.search(text or "")
    return m.group(1) if m else ""


def safe_truncate_english(text: str, limit: int = 50) -> str:
    text = re.sub(r"\s+", " ", (text or "").strip())
    if len(text) <= limit:
        return text
    cut = text[:limit + 1]
    if " " in cut:
        cut = cut.rsplit(" ", 1)[0]
    return cut.strip() or text[:limit].strip()


def infer_raw_term(text: str) -> str:
    text = text or ""
    candidates = sorted(set(list(TERM_SIMPLIFY.keys()) + list(CN_TO_EN.keys())), key=len, reverse=True)
    for term in candidates:
        if term in text or term.lower() in text.lower():
            return term
    cleaned = re.sub(r"我要找|请帮我找|请查询|查询|检索|因子|排放因子|碳因子|全国|中国|单位|适合|是否|给我|一个|最合适|有效的|输出|分析", " ", text)
    cleaned = re.sub(r"\s+", " ", cleaned).strip()
    if contains_chinese(cleaned):
        return cleaned[:20] if cleaned else text[:20]
    return safe_truncate_english(cleaned or text, 50)


def simplify_term(term: str) -> str:
    term = (term or "").strip()
    return TERM_SIMPLIFY.get(term, term)


def english_variants(cn_term: str):
    return CN_TO_EN.get(cn_term, []) + CN_TO_EN.get(cn_term.lower(), [])


def build_terms(user_request: str, explicit_query: str = ""):
    raw = explicit_query.strip() if explicit_query else infer_raw_term(user_request)
    core = simplify_term(raw)
    terms = []

    def add(term: str, lang: str, source: str):
        term = (term or "").strip()
        if not term:
            return
        item = {"term": term, "lang": lang, "source": source}
        if item not in terms:
            terms.append(item)

    if contains_chinese(core):
        add(core, "zh", "core_cn")
        for en in english_variants(core):
            add(en, "en", "core_en")
    else:
        add(core, "en", "core_en")

    if raw != core:
        add(raw, "zh" if contains_chinese(raw) else "en", "raw_term")

    if core == "蒸汽":
        for t in ["steam", "steam supply", "process steam", "industrial steam", "purchased steam", "heat"]:
            add(t, "en", "secondary")
    elif core == "天然气":
        for t in ["natural gas", "pipeline natural gas", "city gas"]:
            add(t, "en", "secondary")
    elif core == "电力":
        for t in ["electricity", "power", "grid electricity"]:
            add(t, "en", "secondary")
    elif core == "原铝":
        for t in ["primary aluminium", "aluminium ingot", "aluminium"]:
            add(t, "en", "secondary")
    elif core == "聚酯切片":
        for t in ["PET resin", "PET chip", "polyester resin"]:
            add(t, "en", "secondary")

    return core, raw, terms


def query_semantic_penalty(core_term: str, row: dict):
    core = normalize_text(core_term)
    text = normalize_text(row.get("name"), row.get("nameEn"), row.get("description"), row.get("specification"))
    penalties = []
    score = 0

    if core in ["聚酯切片", "聚酯", "pet"]:
        if not any(k in text for k in ["polyester", "pet", "resin", "切片", "聚酯"]):
            score -= 18
            penalties.append("material semantics weak for polyester/PET query")
    if core in ["原铝", "铝"]:
        if not any(k in text for k in ["铝", "aluminium", "aluminum"]):
            score -= 15
            penalties.append("material semantics weak for aluminium query")
    if core == "蒸汽":
        if not any(k in text for k in ["steam", "heat", "蒸汽", "thermal"]):
            score -= 12
            penalties.append("steam/heat semantics weak")
        if any(k in text for k in ["steam", "蒸汽"]):
            score += 10
        elif "heat" in text and not any(k in text for k in ["steam", "蒸汽"]):
            score -= 10
            penalties.append("heat may be broader than steam")
    if core == "天然气":
        if not any(k in text for k in ["natural gas", "天然气", "燃气", "gas", "lng"]):
            score -= 15
            penalties.append("natural gas semantics weak")
    if core == "电力":
        if not any(k in text for k in ["electricity", "power", "电力"]):
            score -= 12
            penalties.append("electricity semantics weak")
    return score, penalties


def has_latest_hint(text: str) -> bool:
    t = normalize_text(text)
    return any(h in t for h in LATEST_HINTS)


def detect_factor_preference(text: str) -> str:
    t = normalize_text(text)
    if any(h in t for h in CFP_HINTS):
        return "carbon_footprint"
    if any(h in t for h in EMISSION_HINTS):
        return "emission"
    return "unspecified"


def classify_factor_type(row: dict) -> str:
    text = normalize_text(row.get('name'), row.get('description'), row.get('specification'), row.get('source'))
    if any(h in text for h in CFP_HINTS):
        return 'carbon_footprint'
    if any(h in text for h in EMISSION_HINTS):
        return 'emission'
    return 'unspecified'


def authority_bonus(row: dict):
    inst = normalize_text(row.get('institution'), row.get('source'), row.get('sourceLevel'))
    if any(k.lower() in inst for k in AUTHORITY_KEYWORDS['国家级机构']):
        return 8, 'national-level authority source'
    if any(k.lower() in inst for k in AUTHORITY_KEYWORDS['国际权威']):
        return 5, 'international authority source'
    return 0, ''


def recency_bonus(row: dict, query: str):
    ay = str(row.get('applyYear') or '')
    try:
        year = int(ay)
    except ValueError:
        return 0, ''
    base = {2025: 10, 2024: 8, 2023: 5}.get(year, 0)
    if has_latest_hint(query):
        base = int(base * 1.5) if base else 0
    return base, f'recency bonus for applyYear={year}' if base else ''


def classify_match(score: int, row: dict, region: str, unit: str, risks: list):
    region_text = normalize_text(row.get("area"), row.get("countries"))
    unit_value = str(row.get("unit") or "")
    region_ok = (not region) or (region.lower() in region_text)
    unit_ok = (not unit) or (unit.lower() == unit_value.lower()) or (unit.lower() in unit_value.lower())
    if 'region mismatch' in risks and region:
        return 'not_suitable'
    if 'unit mismatch' in risks and unit:
        return 'not_suitable'
    if 'monetary unit may be unsuitable for physical activity factor matching' in risks:
        return 'not_suitable'
    if score >= 80 and region_ok and unit_ok:
        return 'direct_match'
    if score >= 60 and not any(r in risks for r in ['region mismatch', 'unit mismatch']):
        return 'close_match'
    if score >= 45 and len(risks) <= 2:
        return 'fallback_generic'
    return 'not_suitable'


def score_row(row: dict, query: str, core_term: str, region: str = '', unit: str = '', year: str = ''):
    score = 0
    reasons = []
    risks = []
    q = (query or '').strip().lower()
    q_tokens = [t for t in re.split(r"[\s/,_\-]+", q) if t]

    name = str(row.get('name') or '')
    name_en = str(row.get('nameEn') or '')
    description = str(row.get('description') or '')
    specification = str(row.get('specification') or '')
    area = str(row.get('area') or '')
    countries = str(row.get('countries') or '')
    unit_value = str(row.get('unit') or '')
    institution = str(row.get('institution') or '')
    source_level = str(row.get('sourceLevel') or '')
    document_type = str(row.get('documentType') or '')
    apply_year = str(row.get('applyYear') or '')
    apply_year_end = str(row.get('applyYearEnd') or '')
    c_value = str(row.get('cValue') or '')
    factor_classify = str(row.get('factorClassify') or '')

    combined = normalize_text(name, name_en, description, specification)

    if q and (q == name.lower() or q == name_en.lower()):
        score += 50
        reasons.append('exact name match')
    elif q and q in combined:
        score += 30
        reasons.append('query appears in candidate text')

    token_hits = 0
    for token in q_tokens:
        if token in combined:
            token_hits += 1
    if token_hits:
        score += min(20, token_hits * 5)
        reasons.append(f'{token_hits} token(s) matched')

    semantic_penalty, semantic_penalty_reasons = query_semantic_penalty(core_term, row)
    score += semantic_penalty
    risks.extend(semantic_penalty_reasons)

    if region:
        region_text = normalize_text(area, countries)
        if region.lower() in region_text:
            score += 15
            reasons.append('region matched')
        else:
            score -= 18
            reasons.append('region not matched')
            risks.append('region mismatch')
    else:
        if core_term in GEO_SENSITIVE_TERMS:
            risks.append('region missing for geo-sensitive factor')
        if contains_chinese(query) and core_term in GEO_SENSITIVE_TERMS and ('中国' in countries or '全国' in name or '中国' in name):
            score += 12
            reasons.append('China-biased ranking for Chinese request without explicit region')

    if unit:
        if unit.lower() == unit_value.lower():
            score += 18
            reasons.append('unit matched exactly')
        elif unit.lower() in unit_value.lower() or unit_value.lower() in unit.lower():
            score += 8
            reasons.append('unit partially matched')
        else:
            score -= 18
            reasons.append('unit not matched')
            risks.append('unit mismatch')

    if year:
        if year == apply_year:
            score += 8
            reasons.append('year matched')
        elif apply_year:
            risks.append('year differs')

    rb, rb_reason = recency_bonus(row, query)
    score += rb
    if rb_reason:
        reasons.append(rb_reason)

    pref = detect_factor_preference(query)
    row_type = classify_factor_type(row)
    if pref == 'carbon_footprint' and row_type == 'carbon_footprint':
        score += 10
        reasons.append('factor type matches carbon footprint intent')
    elif pref == 'emission' and row_type == 'emission':
        score += 10
        reasons.append('factor type matches emission-factor intent')
    elif pref != 'unspecified' and row_type != 'unspecified' and pref != row_type:
        score -= 10
        risks.append('factor type may not match intended accounting purpose')
    elif pref == 'unspecified' and core_term == '电力' and row_type != 'unspecified':
        risks.append('factor type ambiguous: carbon footprint vs emission factor')

    ab, ab_reason = authority_bonus(row)
    score += ab
    if ab_reason:
        reasons.append(ab_reason)

    if institution:
        score += 3
    if source_level:
        score += 3
    if document_type:
        score += 1
    if factor_classify:
        score += 1

    if '加密数据' in c_value:
        risks.append('value encrypted')
    if area in {'0', 'null', 'None'}:
        score -= 3
        risks.append('weak area field')
    if any(x in normalize_text(description, specification) for x in ['范畴3', '生产资料', '支出']):
        score -= 8
        risks.append('may be spend-based or scope-3 category factor')
    if any(x in unit_value.lower() for x in ['日元', 'usd', 'eur', '元', 'rmb']) and not any(x in core_term for x in ['金额', '支出', '花费']):
        score -= 18
        risks.append('monetary unit may be unsuitable for physical activity factor matching')
    if apply_year_end and apply_year_end not in {'不限', 'null', 'None'} and year and apply_year_end < year:
        risks.append('applicability period may not cover requested year')

    risks = list(dict.fromkeys(risks))
    return score, reasons, risks


def query_api(base_url: str, name: str, lang: str) -> dict:
    payload = {'sign': make_sign(name), 'name': name, 'lang': lang}
    body = json.dumps(payload, ensure_ascii=False).encode('utf-8')
    headers = {
        'Accept': 'application/json, text/plain, */*',
        'Content-Type': 'application/json',
        'User-Agent': 'OpenClaw-CCDB-Skill/1.0',
    }
    req = Request(base_url, data=body, headers=headers, method='POST')
    try:
        with urlopen(req, timeout=30) as resp:
            raw = resp.read().decode('utf-8', errors='replace')
        data = json.loads(raw)
        return {'request': payload, 'response': data, 'ok': True}
    except HTTPError as e:
        detail = e.read().decode('utf-8', errors='replace') if hasattr(e, 'read') else str(e)
        return {'request': payload, 'response': None, 'ok': False, 'error': {'type': 'http_error', 'status': getattr(e, 'code', None), 'message': str(e), 'detail': detail}}
    except URLError as e:
        return {'request': payload, 'response': None, 'ok': False, 'error': {'type': 'network_error', 'message': str(e)}}
    except json.JSONDecodeError as e:
        return {'request': payload, 'response': None, 'ok': False, 'error': {'type': 'invalid_json', 'message': str(e)}}


def run_single_query(base_url: str, query: str, lang: str, core_term: str, region: str, unit: str, year: str, top: int):
    result = query_api(base_url, query, lang)
    if not result.get('ok'):
        return {'query': query, 'lang': lang, 'request': result['request'], 'code': None, 'msg': 'api_error', 'total': 0, 'ranked_candidates': [], 'error': result.get('error')}

    response = result.get('response') or {}
    rows = response.get('rows') or []
    ranked = []
    for row in rows:
        score, reasons, risks = score_row(row, query, core_term, region, unit, year)
        ranked.append({'score': score, 'classification': classify_match(score, row, region, unit, risks), 'reasons': reasons, 'risks': risks, 'candidate': row})
    ranked.sort(key=lambda x: x['score'], reverse=True)
    return {'query': query, 'lang': lang, 'request': result['request'], 'code': response.get('code'), 'msg': response.get('msg'), 'total': response.get('total'), 'ranked_candidates': ranked[:top], 'error': None}


def dedupe_candidates(search_runs):
    merged = {}
    for run in search_runs:
        if run.get('error'):
            continue
        for cand in run.get('ranked_candidates', []):
            row = cand['candidate']
            key = row.get('id') or f"{row.get('name')}|{row.get('unit')}|{row.get('institution')}"
            enriched = {'from_query': run.get('query'), 'from_lang': run.get('lang'), **cand}
            existing = merged.get(key)
            if existing is None or enriched['score'] > existing['score']:
                merged[key] = enriched
    return sorted(merged.values(), key=lambda x: x['score'], reverse=True)


def direct_use_guidance(classification: str, risks: list):
    if classification == 'direct_match' and not risks:
        return '可直接使用（仍建议做一次口径复核）'
    if classification == 'direct_match':
        return '基本可直接使用，但需先检查风险提示'
    if classification == 'close_match':
        return '建议人工复核后使用'
    if classification == 'fallback_generic':
        return '仅适合估算或临时占位，不建议直接用于正式披露'
    return '不建议直接使用'


def explain_best(best, alternatives, constraints, core_term):
    if not best or best.get('classification') == 'not_suitable':
        suggestions = []
        if core_term == '蒸汽':
            suggestions = ['蒸汽', 'steam', 'steam supply', 'process steam', 'industrial steam', 'purchased steam']
        elif core_term == '天然气':
            suggestions = ['天然气', 'natural gas', 'pipeline natural gas', 'city gas', 'natural gas combustion']
        elif core_term == '聚酯切片':
            suggestions = ['聚酯切片', 'PET resin', 'PET chip', 'polyester resin']
        top_alts = []
        for alt in alternatives[:3]:
            r = alt['candidate']
            top_alts.append({'name': r.get('name'), 'nameEn': r.get('nameEn'), 'unit': r.get('unit'), 'countries': r.get('countries'), 'score': alt.get('score'), 'classification': alt.get('classification'), 'risks': alt.get('risks')})
        return {'selected': None, 'classification': 'not_suitable', 'why': 'No reliable direct/close/fallback candidate met the current constraints after searching from the smallest core term outward.', 'mismatch_risks': ['no suitable candidate found'], 'constraints_used': constraints, 'alternatives_considered': top_alts, 'query_refinement_suggestions': suggestions, 'next_action': 'Retry with a nearby core noun or provide more precise stage/region/unit constraints.'}

    row = best['candidate']
    why_parts = [f"Selected because score={best['score']} and classification={best['classification']}", f"matched via query '{best['from_query']}' ({best['from_lang']}) from core term '{core_term}'"]
    if best.get('reasons'):
        why_parts.append('; '.join(best['reasons']))

    alt_summary = []
    for alt in alternatives[:3]:
        r = alt['candidate']
        alt_summary.append({'name': r.get('name'), 'nameEn': r.get('nameEn'), 'unit': r.get('unit'), 'countries': r.get('countries'), 'score': alt.get('score'), 'classification': alt.get('classification'), 'risks': alt.get('risks')})

    result = {
        '匹配等级': best['classification'],
        '因子名称': row.get('name'),
        '英文名称': row.get('nameEn'),
        '因子值': row.get('cValue'),
        '单位': row.get('unit'),
        '适用地区': row.get('countries') or row.get('area'),
        '适用年份开始': row.get('applyYear'),
        '适用年份结束': row.get('applyYearEnd'),
        '发布年份': row.get('year'),
        '来源机构': row.get('institution'),
        '来源级别': row.get('sourceLevel'),
        '来源说明': row.get('source'),
        '选择原因': ' | '.join(why_parts),
        '风险与注意事项': best.get('risks', []),
        '使用建议': direct_use_guidance(best['classification'], best.get('risks', [])),
    }
    return {'selected': row, 'classification': best['classification'], 'why': ' | '.join(why_parts), 'mismatch_risks': best.get('risks', []), 'constraints_used': constraints, 'alternatives_considered': alt_summary, 'recommended_result': result, 'next_action': 'Use as direct/close match if acceptable, otherwise refine with a nearby synonym or more precise stage/region details.'}


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument('--query', default='')
    parser.add_argument('--user-request', default='')
    parser.add_argument('--lang', default='zh', choices=['zh', 'en'])
    parser.add_argument('--base-url', default=DEFAULT_BASE_URL)
    parser.add_argument('--region', default='')
    parser.add_argument('--unit', default='')
    parser.add_argument('--year', default='')
    parser.add_argument('--top', type=int, default=5)
    parser.add_argument('--auto', action='store_true')
    args = parser.parse_args()

    if args.auto or args.user_request:
        user_request = args.user_request or args.query
        region = args.region or extract_region(user_request)
        unit = args.unit or extract_unit(user_request)
        year = args.year or extract_year(user_request)
        constraints = {'region': region, 'unit': unit, 'year': year}
        core_term, raw_term, terms = build_terms(user_request, args.query)
        runs = []
        for item in terms[:8]:
            runs.append(run_single_query(args.base_url, item['term'], item['lang'], core_term, region, unit, year, args.top))
        api_errors = [r for r in runs if r.get('error')]
        if api_errors and not any(r.get('ranked_candidates') for r in runs):
            print(json.dumps({'ok': False, 'mode': 'auto', 'user_request': user_request, 'core_term': core_term, 'raw_term': raw_term, 'derived_constraints': constraints, 'terms': terms, 'search_runs': runs, 'analysis': {'selected': None, 'classification': 'api_unavailable', 'why': 'CCDB API request failed across all attempted search terms; returning no recommendation to avoid a misleading factor match.', 'mismatch_risks': ['api unavailable'], 'constraints_used': constraints, 'alternatives_considered': [], 'next_action': 'Retry later or verify API endpoint / network / signing configuration.'}}, ensure_ascii=False, indent=2))
            return 1
        merged = dedupe_candidates(runs)
        best = merged[0] if merged else None
        explanation = explain_best(best, merged[1:], constraints, core_term)
        print(json.dumps({'ok': True, 'mode': 'auto', 'user_request': user_request, 'core_term': core_term, 'raw_term': raw_term, 'derived_constraints': constraints, 'terms': terms, 'search_runs': runs, 'merged_candidates': merged[:10], 'best_match': best, 'analysis': explanation}, ensure_ascii=False, indent=2))
        return 0

    core_term = simplify_term(args.query)
    result = run_single_query(args.base_url, args.query, args.lang, core_term, args.region, args.unit, args.year, args.top)
    print(json.dumps({'ok': True, **result}, ensure_ascii=False, indent=2))
    return 0


if __name__ == '__main__':
    sys.exit(main())


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)