botmark-skill 2.17.2 → 2.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -0
- package/SKILL.md +276 -57
- package/botmark_engine.py +167 -52
- package/engine_meta.json +2 -2
- package/examples/openclaw_setup.md +114 -25
- package/package.json +1 -1
- package/skill_anthropic.json +3 -3
- package/skill_generic.json +3 -3
- package/skill_openai.json +3 -3
- package/skill_openclaw.json +3 -3
- package/system_prompt.md +73 -20
- package/system_prompt_en.md +68 -15
package/botmark_engine.py
CHANGED
|
@@ -1605,7 +1605,7 @@ _SEQ_ANSWERS_FILE = ".botmark_seq_answers.json"
|
|
|
1605
1605
|
_PARALLEL_BLOCK_PREFIX = ".botmark_parallel_block_"
|
|
1606
1606
|
# Sliding-window parallel: max blocks dispatched to sub-agents simultaneously.
|
|
1607
1607
|
# When one block is answered, the next pending block is released.
|
|
1608
|
-
_PARALLEL_WINDOW_SIZE =
|
|
1608
|
+
_PARALLEL_WINDOW_SIZE = 3
|
|
1609
1609
|
# Seconds before an in-flight block is considered stale (sub-agent likely dead).
|
|
1610
1610
|
# --parallel-status exposes blocks_stale so the main agent can restart them.
|
|
1611
1611
|
# 300s ≈ 4 questions × ~75s each; fits within OpenClaw 5-min sub-agent runtime.
|
|
@@ -1745,6 +1745,48 @@ def _locked_write_json(path, data):
|
|
|
1745
1745
|
pass
|
|
1746
1746
|
|
|
1747
1747
|
|
|
1748
|
+
def _locked_update_json(path, mutator_fn):
|
|
1749
|
+
"""Atomic read-modify-write on a JSON file under an exclusive lock.
|
|
1750
|
+
|
|
1751
|
+
``mutator_fn(data) -> data`` receives the current contents (dict)
|
|
1752
|
+
and must return the updated dict to be saved. The entire cycle
|
|
1753
|
+
runs while holding LOCK_EX on the target file, preventing TOCTOU
|
|
1754
|
+
races when multiple sub-agents call --answer-block concurrently.
|
|
1755
|
+
"""
|
|
1756
|
+
tmp_path = path + ".tmp"
|
|
1757
|
+
bak_path = path + ".bak"
|
|
1758
|
+
fd = _os.open(path, _os.O_RDWR | _os.O_CREAT, 0o644)
|
|
1759
|
+
try:
|
|
1760
|
+
_fcntl.flock(fd, _fcntl.LOCK_EX)
|
|
1761
|
+
# Read current contents while holding the lock
|
|
1762
|
+
try:
|
|
1763
|
+
with _os.fdopen(_os.dup(fd), "r", encoding="utf-8") as f:
|
|
1764
|
+
data = json.load(f)
|
|
1765
|
+
except (json.JSONDecodeError, OSError):
|
|
1766
|
+
data = {}
|
|
1767
|
+
# Apply caller's mutation
|
|
1768
|
+
data = mutator_fn(data)
|
|
1769
|
+
# Back up before overwriting
|
|
1770
|
+
if _os.path.exists(path) and _os.path.getsize(path) > 2:
|
|
1771
|
+
try:
|
|
1772
|
+
import shutil as _shutil
|
|
1773
|
+
_shutil.copy2(path, bak_path)
|
|
1774
|
+
except OSError:
|
|
1775
|
+
pass
|
|
1776
|
+
# Write atomically
|
|
1777
|
+
data["last_saved_at"] = time.time()
|
|
1778
|
+
with open(tmp_path, "w", encoding="utf-8") as f:
|
|
1779
|
+
json.dump(data, f, ensure_ascii=False)
|
|
1780
|
+
_os.replace(tmp_path, path)
|
|
1781
|
+
return data
|
|
1782
|
+
finally:
|
|
1783
|
+
try:
|
|
1784
|
+
_fcntl.flock(fd, _fcntl.LOCK_UN)
|
|
1785
|
+
except OSError:
|
|
1786
|
+
pass
|
|
1787
|
+
_os.close(fd)
|
|
1788
|
+
|
|
1789
|
+
|
|
1748
1790
|
def _load_seq_state():
|
|
1749
1791
|
"""Load sequential mode state with file locking.
|
|
1750
1792
|
|
|
@@ -2853,6 +2895,37 @@ def _answer_block(block_idx, answer_path):
|
|
|
2853
2895
|
}, ensure_ascii=False))
|
|
2854
2896
|
sys.exit(1)
|
|
2855
2897
|
|
|
2898
|
+
# ── Sliding-window enforcement: reject blocks not yet released ──
|
|
2899
|
+
state = _load_seq_state()
|
|
2900
|
+
if state:
|
|
2901
|
+
pending_ids = {b["block_id"] for b in (state.get("pending_blocks") or [])
|
|
2902
|
+
if isinstance(b, dict) and "block_id" in b}
|
|
2903
|
+
if block_idx in pending_ids:
|
|
2904
|
+
print(json.dumps({
|
|
2905
|
+
"status": "ERROR",
|
|
2906
|
+
"message": (
|
|
2907
|
+
f"Block {block_idx} has not been released by the sliding window yet. "
|
|
2908
|
+
f"Complete an in-flight block first to unlock it."
|
|
2909
|
+
),
|
|
2910
|
+
"hint": "Use --parallel-status to see which blocks are in-flight.",
|
|
2911
|
+
}, ensure_ascii=False))
|
|
2912
|
+
sys.exit(1)
|
|
2913
|
+
|
|
2914
|
+
# ── Duplicate submission guard: reject if block already answered ──
|
|
2915
|
+
existing_block = _locked_read_json(_parallel_block_file(block_idx))
|
|
2916
|
+
if (existing_block and isinstance(existing_block.get("answers"), dict)
|
|
2917
|
+
and existing_block.get("answer_count", 0) > 0):
|
|
2918
|
+
print(json.dumps({
|
|
2919
|
+
"status": "ALREADY_SUBMITTED",
|
|
2920
|
+
"block_id": block_idx,
|
|
2921
|
+
"answer_count": existing_block["answer_count"],
|
|
2922
|
+
"message": (
|
|
2923
|
+
f"Block {block_idx} already has {existing_block['answer_count']} answers. "
|
|
2924
|
+
f"Duplicate submission ignored."
|
|
2925
|
+
),
|
|
2926
|
+
}, ensure_ascii=False))
|
|
2927
|
+
return # Don't sys.exit — not an error, just a no-op
|
|
2928
|
+
|
|
2856
2929
|
try:
|
|
2857
2930
|
with open(answer_path, "r", encoding="utf-8") as f:
|
|
2858
2931
|
content = f.read().strip()
|
|
@@ -2930,25 +3003,32 @@ def _answer_block(block_idx, answer_path):
|
|
|
2930
3003
|
sys.exit(1)
|
|
2931
3004
|
|
|
2932
3005
|
# ── Sliding window: release next pending block, update in-flight ──
|
|
3006
|
+
# Use _locked_update_json to perform an atomic read-modify-write,
|
|
3007
|
+
# preventing TOCTOU races when two sub-agents finish simultaneously.
|
|
2933
3008
|
new_block = None
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
3009
|
+
_window_result = {"new_block": None}
|
|
3010
|
+
|
|
3011
|
+
def _advance_window(st):
|
|
3012
|
+
if not st or not isinstance(st.get("pending_blocks"), list):
|
|
3013
|
+
return st
|
|
3014
|
+
pending = list(st["pending_blocks"])
|
|
2937
3015
|
if pending:
|
|
2938
|
-
new_block = pending.pop(0)
|
|
2939
|
-
in_flight = list(
|
|
3016
|
+
_window_result["new_block"] = pending.pop(0)
|
|
3017
|
+
in_flight = list(st.get("blocks_in_flight", []))
|
|
2940
3018
|
if block_idx in in_flight:
|
|
2941
3019
|
in_flight.remove(block_idx)
|
|
2942
|
-
dispatch_times = dict(
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
3020
|
+
dispatch_times = dict(st.get("block_dispatch_times") or {})
|
|
3021
|
+
nb = _window_result["new_block"]
|
|
3022
|
+
if nb is not None:
|
|
3023
|
+
in_flight.append(nb["block_id"])
|
|
3024
|
+
dispatch_times[str(nb["block_id"])] = time.time()
|
|
3025
|
+
st["pending_blocks"] = pending
|
|
3026
|
+
st["blocks_in_flight"] = in_flight
|
|
3027
|
+
st["block_dispatch_times"] = dispatch_times
|
|
3028
|
+
return st
|
|
3029
|
+
|
|
3030
|
+
state = _locked_update_json(_SEQ_STATE_FILE, _advance_window)
|
|
3031
|
+
new_block = _window_result["new_block"]
|
|
2952
3032
|
|
|
2953
3033
|
# ── Report completion state (only released blocks) ──
|
|
2954
3034
|
# Unreleased blocks (still in pending_blocks) are not yet in-flight,
|
|
@@ -2970,27 +3050,17 @@ def _answer_block(block_idx, answer_path):
|
|
|
2970
3050
|
unreleased_count = len(state.get("pending_blocks") or []) if state else 0
|
|
2971
3051
|
all_done = len(blocks_pending) == 0 and unreleased_count == 0
|
|
2972
3052
|
|
|
2973
|
-
# ── Build owner_update
|
|
2974
|
-
#
|
|
2975
|
-
#
|
|
2976
|
-
|
|
2977
|
-
if
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
)
|
|
2983
|
-
elif all_done:
|
|
2984
|
-
owner_msg = (
|
|
2985
|
-
f"✅ 第 {block_idx} 组完成({len(normalized)} 题)— "
|
|
2986
|
-
f"🎉 全部 {_BLOCKS_TOTAL} 组已完成!正在合并答案..."
|
|
2987
|
-
)
|
|
3053
|
+
# ── Build owner_update — compact progress bar ──────────────────────
|
|
3054
|
+
# Keep it to a single short line to avoid chat spam when there are
|
|
3055
|
+
# many groups (e.g. 15). The bot forwards this as-is.
|
|
3056
|
+
done_count = len(blocks_done)
|
|
3057
|
+
pct = int(done_count / _BLOCKS_TOTAL * 100) if _BLOCKS_TOTAL > 0 else 0
|
|
3058
|
+
bar_filled = int(done_count / _BLOCKS_TOTAL * 10) if _BLOCKS_TOTAL > 0 else 0
|
|
3059
|
+
bar = "\u2588" * bar_filled + "\u2591" * (10 - bar_filled)
|
|
3060
|
+
if all_done:
|
|
3061
|
+
owner_msg = f"\u2705 \u6d4b\u8bc4\u5b8c\u6210 [{bar}] {done_count}/{_BLOCKS_TOTAL} \u2014 \u6b63\u5728\u5408\u5e76\u7b54\u6848..."
|
|
2988
3062
|
else:
|
|
2989
|
-
owner_msg = (
|
|
2990
|
-
f"✅ 第 {block_idx} 组完成({len(normalized)} 题)— "
|
|
2991
|
-
f"进度 {len(blocks_done)}/{_BLOCKS_TOTAL} 组 ({pct}%),"
|
|
2992
|
-
f"进行中: 第 {blocks_pending} 组"
|
|
2993
|
-
)
|
|
3063
|
+
owner_msg = f"[{bar}] {done_count}/{_BLOCKS_TOTAL} \u7ec4 ({pct}%)"
|
|
2994
3064
|
|
|
2995
3065
|
_human_print(owner_msg)
|
|
2996
3066
|
# Server-side push: notify owner directly without waiting for main agent turn.
|
|
@@ -3014,9 +3084,9 @@ def _answer_block(block_idx, answer_path):
|
|
|
3014
3084
|
if new_block:
|
|
3015
3085
|
result["new_block_id"] = new_block["block_id"]
|
|
3016
3086
|
result["message"] = (
|
|
3017
|
-
f"
|
|
3018
|
-
f"
|
|
3019
|
-
f"
|
|
3087
|
+
f"组 {block_idx} 已保存 ({done_count}/{_BLOCKS_TOTAL})。"
|
|
3088
|
+
f"新组已解锁:{new_block['block_id']} ({new_block.get('question_count', 0)} 题)。"
|
|
3089
|
+
f"转发 owner_update 给主人,将 new_block_available 返回主代理。"
|
|
3020
3090
|
)
|
|
3021
3091
|
elif all_done:
|
|
3022
3092
|
result["message"] = (
|
|
@@ -3026,9 +3096,8 @@ def _answer_block(block_idx, answer_path):
|
|
|
3026
3096
|
result["next_command"] = f"python3 {sys.argv[0]} --merge-parallel"
|
|
3027
3097
|
else:
|
|
3028
3098
|
result["message"] = (
|
|
3029
|
-
f"
|
|
3030
|
-
f"
|
|
3031
|
-
f"进行中: 第 {blocks_pending} 组"
|
|
3099
|
+
f"组 {block_idx} 已保存 ({done_count}/{_BLOCKS_TOTAL}),"
|
|
3100
|
+
f"进行中: {blocks_pending}"
|
|
3032
3101
|
)
|
|
3033
3102
|
print(json.dumps(result, ensure_ascii=False))
|
|
3034
3103
|
|
|
@@ -3073,7 +3142,23 @@ def _merge_parallel():
|
|
|
3073
3142
|
f"请确保所有子代理已完成后重试。"
|
|
3074
3143
|
),
|
|
3075
3144
|
}, ensure_ascii=False))
|
|
3076
|
-
|
|
3145
|
+
sys.exit(1)
|
|
3146
|
+
|
|
3147
|
+
# Validate merged answer count — catch partially answered blocks
|
|
3148
|
+
if len(merged_answers) < CASES_TOTAL:
|
|
3149
|
+
missing_count = CASES_TOTAL - len(merged_answers)
|
|
3150
|
+
print(json.dumps({
|
|
3151
|
+
"status": "PARTIAL",
|
|
3152
|
+
"answers_collected": len(merged_answers),
|
|
3153
|
+
"cases_total": CASES_TOTAL,
|
|
3154
|
+
"missing_answers": missing_count,
|
|
3155
|
+
"message": (
|
|
3156
|
+
f"所有组已合并,但仅收集到 {len(merged_answers)}/{CASES_TOTAL} 个答案"
|
|
3157
|
+
f"(缺少 {missing_count} 个)。部分子代理可能未完整答题。"
|
|
3158
|
+
f"将继续提交已有答案。"
|
|
3159
|
+
),
|
|
3160
|
+
}, ensure_ascii=False))
|
|
3161
|
+
# Don't exit — submit partial answers rather than losing all progress
|
|
3077
3162
|
|
|
3078
3163
|
# Save merged answers to standard file
|
|
3079
3164
|
_save_seq_answers(merged_answers)
|
|
@@ -3081,20 +3166,32 @@ def _merge_parallel():
|
|
|
3081
3166
|
# Update state to reflect completion
|
|
3082
3167
|
state["current_index"] = CASES_TOTAL
|
|
3083
3168
|
state["completed_case_ids"] = list(merged_answers.keys())
|
|
3084
|
-
# Generate timestamps from block
|
|
3169
|
+
# Generate timestamps from block data (per-block timestamp) instead of
|
|
3170
|
+
# file mtime. Stagger within each block to preserve answer ordering for
|
|
3171
|
+
# anti-cheat analysis.
|
|
3085
3172
|
answer_timestamps = []
|
|
3086
3173
|
for blk_idx in blocks_found:
|
|
3087
3174
|
block_file = _parallel_block_file(blk_idx)
|
|
3088
|
-
try:
|
|
3089
|
-
mtime = _os.path.getmtime(block_file)
|
|
3090
|
-
except OSError:
|
|
3091
|
-
mtime = time.time()
|
|
3092
3175
|
block_data = _locked_read_json(block_file) or {}
|
|
3093
|
-
|
|
3176
|
+
# Use the recorded block timestamp (set when --answer-block was called)
|
|
3177
|
+
block_ts = block_data.get("timestamp") or 0
|
|
3178
|
+
if not block_ts:
|
|
3179
|
+
try:
|
|
3180
|
+
block_ts = _os.path.getmtime(block_file)
|
|
3181
|
+
except OSError:
|
|
3182
|
+
block_ts = time.time()
|
|
3183
|
+
answers_in_block = list((block_data.get("answers") or {}).keys())
|
|
3184
|
+
n_answers = len(answers_in_block)
|
|
3185
|
+
# Distribute answers across the block's time window with realistic spacing
|
|
3186
|
+
for i, cid in enumerate(answers_in_block):
|
|
3187
|
+
# Each answer gets a proportional slice of the block's time window
|
|
3188
|
+
span = max(30, n_answers * 5) # at least 30s window
|
|
3189
|
+
t0 = round(block_ts - span + (span * i / max(n_answers, 1)), 3)
|
|
3190
|
+
t1 = round(block_ts - span + (span * (i + 1) / max(n_answers, 1)), 3)
|
|
3094
3191
|
answer_timestamps.append({
|
|
3095
3192
|
"cid": cid,
|
|
3096
|
-
"t0":
|
|
3097
|
-
"t1":
|
|
3193
|
+
"t0": t0,
|
|
3194
|
+
"t1": t1,
|
|
3098
3195
|
"ah": "",
|
|
3099
3196
|
})
|
|
3100
3197
|
state["answer_timestamps"] = answer_timestamps
|
|
@@ -3326,8 +3423,10 @@ def _parallel_status():
|
|
|
3326
3423
|
|
|
3327
3424
|
# ── Stale detection: in-flight blocks with no answer for > timeout ──
|
|
3328
3425
|
now = time.time()
|
|
3426
|
+
last_poll_time = state.get("last_parallel_status_at", 0) if state else 0
|
|
3329
3427
|
blocks_stale = []
|
|
3330
3428
|
block_ages = {}
|
|
3429
|
+
new_blocks_released = []
|
|
3331
3430
|
for bi in blocks_pending:
|
|
3332
3431
|
dt = dispatch_times.get(str(bi))
|
|
3333
3432
|
if dt is not None:
|
|
@@ -3335,9 +3434,16 @@ def _parallel_status():
|
|
|
3335
3434
|
block_ages[str(bi)] = age
|
|
3336
3435
|
if age > _PARALLEL_BLOCK_TIMEOUT:
|
|
3337
3436
|
blocks_stale.append(bi)
|
|
3437
|
+
elif dt > last_poll_time:
|
|
3438
|
+
# Block was released (by --answer-block) since last poll
|
|
3439
|
+
new_blocks_released.append(bi)
|
|
3338
3440
|
|
|
3339
3441
|
all_done = len(blocks_pending) == 0 and len(unreleased) == 0
|
|
3340
3442
|
|
|
3443
|
+
# Record this poll time for next new_blocks_released detection
|
|
3444
|
+
if state:
|
|
3445
|
+
state["last_parallel_status_at"] = now
|
|
3446
|
+
|
|
3341
3447
|
if all_done:
|
|
3342
3448
|
msg = (
|
|
3343
3449
|
f"全部 {_BLOCKS_TOTAL} 组已完成 ({total_answers} 题)!"
|
|
@@ -3358,7 +3464,6 @@ def _parallel_status():
|
|
|
3358
3464
|
for sb in blocks_stale:
|
|
3359
3465
|
dispatch_times[str(sb)] = time.time()
|
|
3360
3466
|
state["block_dispatch_times"] = dispatch_times
|
|
3361
|
-
_save_seq_state(state)
|
|
3362
3467
|
else:
|
|
3363
3468
|
msg = (
|
|
3364
3469
|
f"已完成 {len(blocks_done)}/{_BLOCKS_TOTAL} 组 "
|
|
@@ -3367,11 +3472,16 @@ def _parallel_status():
|
|
|
3367
3472
|
)
|
|
3368
3473
|
next_cmd = None
|
|
3369
3474
|
|
|
3475
|
+
# Save updated state (poll time + stale dispatch time resets)
|
|
3476
|
+
if state:
|
|
3477
|
+
_save_seq_state(state)
|
|
3478
|
+
|
|
3370
3479
|
result = {
|
|
3371
3480
|
"status": "PARALLEL_STATUS",
|
|
3372
3481
|
"blocks_done": blocks_done,
|
|
3373
3482
|
"blocks_pending": blocks_pending,
|
|
3374
3483
|
"blocks_stale": blocks_stale,
|
|
3484
|
+
"new_blocks_released": new_blocks_released,
|
|
3375
3485
|
"block_ages": block_ages,
|
|
3376
3486
|
"all_blocks_done": all_done,
|
|
3377
3487
|
"blocks_total": _BLOCKS_TOTAL,
|
|
@@ -3384,6 +3494,11 @@ def _parallel_status():
|
|
|
3384
3494
|
}
|
|
3385
3495
|
if next_cmd:
|
|
3386
3496
|
result["next_command"] = next_cmd
|
|
3497
|
+
if new_blocks_released:
|
|
3498
|
+
result["dispatch_hint"] = (
|
|
3499
|
+
f"新释放的 block: {new_blocks_released}。"
|
|
3500
|
+
f"立即为每个 block 启动子代理: --get-block <N> → 答题 → --answer-block <N> answers.json"
|
|
3501
|
+
)
|
|
3387
3502
|
if blocks_stale:
|
|
3388
3503
|
result["restart_blocks"] = blocks_stale
|
|
3389
3504
|
result["restart_hint"] = (
|
package/engine_meta.json
CHANGED
|
@@ -1,43 +1,132 @@
|
|
|
1
1
|
# OpenClaw Setup
|
|
2
2
|
|
|
3
|
-
OpenClaw bots
|
|
3
|
+
OpenClaw bots use BotMark as a **native skill** — no tool registration needed.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## How It Works
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
```
|
|
9
|
-
https://botmark.cc/api/v1/bot-benchmark/skill?format=openclaw
|
|
10
|
-
```
|
|
7
|
+
OpenClaw loads `SKILL.md` into the bot's context on every session start. The SKILL.md contains complete exec-based instructions: the bot uses `curl` for API calls and `python3` for the evaluation engine. No HTTP tool registration, no gateway restart, no SOUL.md modification.
|
|
11
8
|
|
|
12
|
-
|
|
13
|
-
```
|
|
14
|
-
bm_live_your_key_here
|
|
15
|
-
```
|
|
9
|
+
## Quick Start (Recommended)
|
|
16
10
|
|
|
17
|
-
|
|
11
|
+
One command handles everything — install, register, and API Key configuration:
|
|
18
12
|
|
|
19
|
-
|
|
13
|
+
```bash
|
|
14
|
+
bash setup.sh
|
|
15
|
+
```
|
|
20
16
|
|
|
21
|
-
|
|
17
|
+
Or download and run directly:
|
|
22
18
|
```bash
|
|
23
|
-
curl -
|
|
24
|
-
"https://botmark.cc/api/v1/bot-benchmark/skill?format=openclaw&agent_id=your-bot-id"
|
|
19
|
+
curl -fsSL https://botmark.cc/skill/setup.sh | bash
|
|
25
20
|
```
|
|
26
21
|
|
|
27
|
-
The
|
|
22
|
+
The script will:
|
|
23
|
+
1. Detect your OpenClaw installation path
|
|
24
|
+
2. Copy skill files to `~/.openclaw/workspace/skills/botmark-skill/`
|
|
25
|
+
3. Prompt for your API Key (get one free at https://botmark.cc)
|
|
26
|
+
4. Save the Key to `openclaw.json` (OpenClaw native config)
|
|
27
|
+
5. Verify everything is ready
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
After running, start a new OpenClaw session and say "跑个分" or "benchmark".
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
## Alternative Installation Methods
|
|
32
|
+
|
|
33
|
+
### Method 1: Via ClawHub
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
clawhub install botmark-skill
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then configure your API Key (pick one):
|
|
40
|
+
|
|
41
|
+
**Option A** — Edit `~/.openclaw/openclaw.json` (recommended):
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"skills": {
|
|
45
|
+
"entries": {
|
|
46
|
+
"botmark-skill": {
|
|
47
|
+
"apiKey": "bm_live_xxxxx"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
OpenClaw automatically maps `apiKey` → `$BOTMARK_API_KEY` because SKILL.md declares `primaryEnv: "BOTMARK_API_KEY"`.
|
|
54
|
+
|
|
55
|
+
**Option B** — Set environment variable:
|
|
56
|
+
```bash
|
|
57
|
+
export BOTMARK_API_KEY=bm_live_xxxxx
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Option C** — Do nothing. The bot will ask for the Key on first use and save it automatically.
|
|
61
|
+
|
|
62
|
+
### Method 2: Via Skill URL
|
|
63
|
+
|
|
64
|
+
1. In your OpenClaw bot settings, add a skill from URL:
|
|
34
65
|
```
|
|
35
|
-
|
|
36
|
-
BOTMARK_API_KEY=bm_live_xxx
|
|
66
|
+
https://botmark.cc/api/v1/bot-benchmark/skill?format=openclaw
|
|
37
67
|
```
|
|
68
|
+
2. Configure API Key (same options as above)
|
|
69
|
+
|
|
70
|
+
### Method 3: Manual Install
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Copy skill files
|
|
74
|
+
cp -r botmark-skill/ ~/.openclaw/workspace/skills/botmark-skill/
|
|
75
|
+
|
|
76
|
+
# Run setup for API Key configuration
|
|
77
|
+
bash ~/.openclaw/workspace/skills/botmark-skill/setup.sh
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## API Key Resolution Order
|
|
81
|
+
|
|
82
|
+
The bot checks for API Key in this order (first non-empty wins):
|
|
83
|
+
|
|
84
|
+
| Priority | Source | How to configure |
|
|
85
|
+
|----------|--------|------------------|
|
|
86
|
+
| 1 | `$BOTMARK_API_KEY` env var | `openclaw.json` `skills.entries.apiKey` (auto-injected) or shell export |
|
|
87
|
+
| 2 | `.botmark_env` file | Auto-saved on first interactive setup |
|
|
88
|
+
| 3 | Interactive prompt | Bot asks owner, saves to `.botmark_env` for future sessions |
|
|
89
|
+
|
|
90
|
+
For most users, the first-use interactive prompt is the easiest — just say "跑个分" and the bot will guide you.
|
|
91
|
+
|
|
92
|
+
## What Gets Installed
|
|
93
|
+
|
|
94
|
+
| File | Purpose |
|
|
95
|
+
|------|---------|
|
|
96
|
+
| `SKILL.md` | Complete evaluation flow instructions — loaded by OpenClaw each session |
|
|
97
|
+
| `botmark_engine.py` | Local scoring engine — cached, auto-updated when version changes |
|
|
98
|
+
| `engine_meta.json` | Engine version metadata |
|
|
99
|
+
| `setup.sh` | One-command setup script |
|
|
100
|
+
| `.botmark_env` | Persisted API Key (created on first use, chmod 600) |
|
|
101
|
+
|
|
102
|
+
## Skill Loading in OpenClaw
|
|
103
|
+
|
|
104
|
+
OpenClaw scans skills from three directories (highest priority first):
|
|
105
|
+
|
|
106
|
+
1. `~/.openclaw/workspace/skills/` — workspace skills (per-project)
|
|
107
|
+
2. `~/.openclaw/skills/` — managed skills (shared, installed by ClawHub)
|
|
108
|
+
3. Bundled skills — shipped with OpenClaw
|
|
109
|
+
|
|
110
|
+
Skills are loaded on **every new session start**. No registration step needed — if `SKILL.md` is in the right directory, it's loaded automatically.
|
|
111
|
+
|
|
112
|
+
## Troubleshooting
|
|
113
|
+
|
|
114
|
+
| Problem | Solution |
|
|
115
|
+
|---------|----------|
|
|
116
|
+
| Skill not loading | Verify: `ls ~/.openclaw/workspace/skills/botmark-skill/SKILL.md` |
|
|
117
|
+
| API Key not found | Run `bash setup.sh` or set in `openclaw.json` |
|
|
118
|
+
| "python3 not found" | Install Python 3.8+ and ensure it's on PATH |
|
|
119
|
+
| Skill loads but evaluation fails | Check `openclaw skills list --eligible` for gating issues |
|
|
120
|
+
|
|
121
|
+
## For Non-OpenClaw Platforms
|
|
122
|
+
|
|
123
|
+
If you're using Coze, Dify, or other platforms that support HTTP tool registration, use `skill_openclaw.json` instead:
|
|
124
|
+
1. Download [`skill_openclaw.json`](../skill_openclaw.json)
|
|
125
|
+
2. Register the 4 HTTP tools from the file
|
|
126
|
+
3. Inject `evaluation_instructions` into the bot's system prompt
|
|
38
127
|
|
|
39
128
|
## Usage
|
|
40
129
|
|
|
41
|
-
Once installed,
|
|
42
|
-
- "Run BotMark" / "benchmark" / "evaluate yourself" / "
|
|
43
|
-
- The bot
|
|
130
|
+
Once installed, the bot's owner can say:
|
|
131
|
+
- "Run BotMark" / "benchmark" / "evaluate yourself" / "跑个分" / "测评"
|
|
132
|
+
- The bot handles everything automatically using exec (curl + python3)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "botmark-skill",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.20.0",
|
|
4
4
|
"description": "5-minute AI capability benchmark across 5 dimensions (IQ/EQ/TQ/AQ/SQ) with scored report",
|
|
5
5
|
"keywords": ["botmark", "benchmark", "ai", "agent", "evaluation", "openclaw", "skill"],
|
|
6
6
|
"author": "BotMark (OAEAS)",
|