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/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 = 4
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
- state = _load_seq_state()
2935
- if state and isinstance(state.get("pending_blocks"), list):
2936
- pending = list(state["pending_blocks"])
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(state.get("blocks_in_flight", []))
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(state.get("block_dispatch_times") or {})
2943
- if new_block is not None:
2944
- in_flight.append(new_block["block_id"])
2945
- # Record when this new block is dispatched so --parallel-status
2946
- # can detect a stale/dead sub-agent after _PARALLEL_BLOCK_TIMEOUT.
2947
- dispatch_times[str(new_block["block_id"])] = time.time()
2948
- state["pending_blocks"] = pending
2949
- state["blocks_in_flight"] = in_flight
2950
- state["block_dispatch_times"] = dispatch_times
2951
- _save_seq_state(state)
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 so sub-agent can forward progress immediately ──
2974
- # Sub-agent includes this in its final message to the main agent so the
2975
- # owner sees each block completion as it happens, not batched at the end.
2976
- pct = int(len(blocks_done) / _BLOCKS_TOTAL * 100) if _BLOCKS_TOTAL > 0 else 0
2977
- if new_block:
2978
- owner_msg = (
2979
- f" {block_idx} 组完成({len(normalized)} 题)— "
2980
- f"进度 {len(blocks_done)}/{_BLOCKS_TOTAL} 组 ({pct}%),"
2981
- f"🔓 已解锁第 {new_block['block_id']} "
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" {block_idx} 组已保存。已完成 {len(blocks_done)}/{_BLOCKS_TOTAL}。"
3018
- f"🔓 新一组已解锁:第 {new_block['block_id']} ({new_block.get('question_count', 0)} 题)"
3019
- f"\n⚠️ 请立即将 owner_update 转发给主人,然后将 new_block_available 返回主代理。"
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" {block_idx} 组已保存。"
3030
- f"已完成 {len(blocks_done)}/{_BLOCKS_TOTAL},"
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
- return
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 file mtimes (for anti-cheat compatibility)
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
- for cid in (block_data.get("answers") or {}):
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": round(mtime - 30, 3), # approximate start
3097
- "t1": round(mtime, 3),
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,6 +1,6 @@
1
1
  {
2
- "engine_version": "3.13.1",
2
+ "engine_version": "3.15.0",
3
3
  "engine_checksum": "",
4
- "skill_version": "2.17.2",
4
+ "skill_version": "2.20.0",
5
5
  "usage": "python3 botmark_engine.py --config session_config.json"
6
6
  }
@@ -1,43 +1,132 @@
1
1
  # OpenClaw Setup
2
2
 
3
- OpenClaw bots have native skill support with persistent configuration.
3
+ OpenClaw bots use BotMark as a **native skill** no tool registration needed.
4
4
 
5
- ## Method 1: Install via Skill URL (Recommended)
5
+ ## How It Works
6
6
 
7
- 1. In your OpenClaw bot settings, add a new skill with this URL:
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
- 2. Configure the skill's `BOTMARK_API_KEY` setting with your API Key:
13
- ```
14
- bm_live_your_key_here
15
- ```
9
+ ## Quick Start (Recommended)
16
10
 
17
- 3. The skill will be automatically registered with all 5 tools.
11
+ One command handles everything install, register, and API Key configuration:
18
12
 
19
- ## Method 2: One-step Install + Bind
13
+ ```bash
14
+ bash setup.sh
15
+ ```
20
16
 
21
- Fetch the skill with your API Key to auto-bind (pass the key in the Authorization header, not as a query parameter):
17
+ Or download and run directly:
22
18
  ```bash
23
- curl -H "Authorization: Bearer bm_live_xxx" \
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 response will include a `binding` object with your `binding_id`. Store the `binding_id` in the `BOTMARK_BINDING_ID` environment variable.
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
- ## Method 3: Manual Install
29
+ After running, start a new OpenClaw session and say "跑个分" or "benchmark".
30
30
 
31
- 1. Download [`skill_openclaw.json`](../skill_openclaw.json)
32
- 2. Import it into your OpenClaw bot's skill registry
33
- 3. Set environment variables:
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
- BOTMARK_SERVER_URL=https://botmark.cc
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, your bot's owner can say:
42
- - "Run BotMark" / "benchmark" / "evaluate yourself" / "test yourself"
43
- - The bot will handle everything automatically
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.17.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)",