sophhub 0.4.22 → 0.4.24
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/agents/parent-toddler/.config.json +7 -1
- package/agents/parent-toddler/HEARTBEAT.md +4 -4
- package/agents/parent-toddler/TOOLS.md +9 -0
- package/agents/parent-toddler/scripts/compact_sessions_over_threshold.py +368 -0
- package/package.json +1 -1
- package/skills/agent-install/src/SKILL.md +2 -0
- package/skills/agent-install/src/scripts/common.py +20 -0
- package/skills/agent-install/src/scripts/update_openclaw.py +5 -0
- package/skills/image-classify/skill.json +12 -5
- package/skills/image-classify/src/SKILL.md +1 -1
- package/skills/image-classify/src/references/config.json +1 -1
- package/skills/online-bug-report/skill.json +55 -0
- package/skills/online-bug-report/src/SKILL.md +131 -0
- package/skills/online-bug-report/src/pyproject.toml +5 -0
- package/skills/online-bug-report/src/references/config.example.json +11 -0
- package/skills/online-bug-report/src/scripts/report_bug.py +631 -0
- package/skills/online-bug-report/src/secrets/bug-report.json +6 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.1.
|
|
2
|
+
"version": "1.1.1",
|
|
3
3
|
"agent_id": "parent-toddler",
|
|
4
4
|
"agent_label": "幼儿家长助手",
|
|
5
5
|
"description": "面向幼儿家长的成长陪伴助手:亲子互动、图片与多媒体创作、日常记录与轻量咨询",
|
|
@@ -21,6 +21,12 @@
|
|
|
21
21
|
"read"
|
|
22
22
|
]
|
|
23
23
|
},
|
|
24
|
+
"heartbeat": {
|
|
25
|
+
"every": "2h",
|
|
26
|
+
"target": "none",
|
|
27
|
+
"lightContext": true,
|
|
28
|
+
"isolatedSession": true
|
|
29
|
+
},
|
|
24
30
|
"skills": [
|
|
25
31
|
{ "name": "image-classify", "builtin": false, "auto_install": true },
|
|
26
32
|
{ "name": "image-identify-world", "builtin": false, "auto_install": true },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Heartbeat checklist
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
- 解析当前 Agent ID:从 SessionKey(形如 `agent:<AgentID>:...`)取第二段;隔离 Heartbeat 会话示例 `agent:<AgentID>:main:heartbeat`,`<AgentID>` 即所需 id。亦可与用户问「获取当前 Agent ID」时给出的 id 交叉确认,二者须一致。
|
|
4
|
+
- 静默执行:`python3 scripts/compact_sessions_over_threshold.py --agent-id <AgentID> --json`(不向用户展示命令、路径、日志或脚本输出;脚本局部失败亦不影响 Heartbeat,仍只回复 `HEARTBEAT_OK`)
|
|
5
|
+
- 完成后只回复 `HEARTBEAT_OK`,不要输出其它内容
|
|
@@ -10,6 +10,15 @@
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## 运维备忘
|
|
14
|
+
|
|
15
|
+
- Session store:`/home/node/.openclaw/agents/<AgentID>/sessions/sessions.json`(`<AgentID>` 为 openclaw 运行时 id,由 Heartbeat 通过脚本参数 `--agent-id` 传入)
|
|
16
|
+
- 定时压缩:见 `HEARTBEAT.md`;间隔见 `openclaw.json` 中本 agent 的 `heartbeat.every`;通过 gateway `chat.send` 向各 session 发送 `/compact` 命令
|
|
17
|
+
- 须启用 `heartbeat.isolatedSession`(见 `.config.json`),否则 Heartbeat 执行期间主会话为 running,无法被选中压缩
|
|
18
|
+
- **勿**在 `tools.deny` 中禁用 `exec`(Heartbeat 依赖 exec 运行压缩脚本)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
13
22
|
## 可选备忘
|
|
14
23
|
|
|
15
24
|
以下为占位,按需填写:
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
轮询指定 agent 的 sessions.json,对「超阈值且非 running」的会话执行 /compact。
|
|
4
|
+
通过 openclaw gateway call chat.send 向目标 session 发送 /compact 控制命令。
|
|
5
|
+
|
|
6
|
+
Heartbeat 须在独立会话中运行(heartbeat.isolatedSession=true),否则执行脚本的
|
|
7
|
+
会话在 store 里为 running,主会话无法被选中。脚本会跳过键名以 :heartbeat 结尾的
|
|
8
|
+
会话,以及环境变量标明的当前执行会话。
|
|
9
|
+
|
|
10
|
+
局部 compact 失败仅写入 JSON 汇总,进程仍以 exit 0 结束,便于 Heartbeat 稳定回复 HEARTBEAT_OK。
|
|
11
|
+
|
|
12
|
+
默认状态目录: /home/node/.openclaw
|
|
13
|
+
--agent-id 必填(由调用方传入当前 openclaw Agent ID)
|
|
14
|
+
可选环境变量 OPENCLAW_STATE_DIR 覆盖状态目录
|
|
15
|
+
|
|
16
|
+
用法:
|
|
17
|
+
python3 compact_sessions_over_threshold.py --agent-id parent-toddler --json
|
|
18
|
+
python3 compact_sessions_over_threshold.py --agent-id parent-toddler --dry-run
|
|
19
|
+
python3 compact_sessions_over_threshold.py --agent-id parent-toddler --threshold 100000
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import subprocess
|
|
28
|
+
import sys
|
|
29
|
+
import uuid
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Any, Iterable, Optional
|
|
32
|
+
|
|
33
|
+
DEFAULT_STATE_DIR = Path("/home/node/.openclaw")
|
|
34
|
+
DEFAULT_TOKEN_THRESHOLD = 80_000
|
|
35
|
+
COMPACT_MESSAGE = "/compact"
|
|
36
|
+
DEFAULT_AGENT_TIMEOUT_SEC = 600
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _as_int_or_none(v: Any) -> Optional[int]:
|
|
40
|
+
if v is None or isinstance(v, bool):
|
|
41
|
+
return None
|
|
42
|
+
if isinstance(v, int):
|
|
43
|
+
return v
|
|
44
|
+
if isinstance(v, float) and v.is_integer():
|
|
45
|
+
return int(v)
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def iter_session_rows(store: dict[str, Any]) -> Iterable[dict[str, Any]]:
|
|
50
|
+
for key, entry in store.items():
|
|
51
|
+
if not isinstance(entry, dict):
|
|
52
|
+
continue
|
|
53
|
+
st = entry.get("status")
|
|
54
|
+
yield {
|
|
55
|
+
"key": key,
|
|
56
|
+
"sessionId": entry.get("sessionId"),
|
|
57
|
+
"totalTokens": _as_int_or_none(entry.get("totalTokens")),
|
|
58
|
+
"statusRunning": st == "running",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def list_sessions_from_store(path: Path) -> list[dict[str, Any]]:
|
|
63
|
+
raw = path.read_text(encoding="utf-8")
|
|
64
|
+
data = json.loads(raw)
|
|
65
|
+
if not isinstance(data, dict):
|
|
66
|
+
raise ValueError("根节点必须是 JSON 对象(键为 session key)")
|
|
67
|
+
return list(iter_session_rows(data))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def filter_sessions_by_agent(rows: list[dict[str, Any]], agent_id: str) -> list[dict[str, Any]]:
|
|
71
|
+
aid = agent_id.strip()
|
|
72
|
+
if not aid:
|
|
73
|
+
raise ValueError("agent_id 不能为空")
|
|
74
|
+
prefix = f"agent:{aid}:"
|
|
75
|
+
return [row for row in rows if str(row.get("key", "")).startswith(prefix)]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def filter_sessions_over_threshold(
|
|
79
|
+
rows: list[dict[str, Any]],
|
|
80
|
+
threshold: int,
|
|
81
|
+
) -> list[dict[str, Any]]:
|
|
82
|
+
if threshold < 0:
|
|
83
|
+
raise ValueError("threshold 不能为负数")
|
|
84
|
+
out: list[dict[str, Any]] = []
|
|
85
|
+
for row in rows:
|
|
86
|
+
total = row.get("totalTokens")
|
|
87
|
+
if total is None:
|
|
88
|
+
continue
|
|
89
|
+
if total > threshold:
|
|
90
|
+
out.append(row)
|
|
91
|
+
return out
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def filter_sessions_idle(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
95
|
+
return [row for row in rows if row.get("statusRunning") is not True]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _resolve_executor_session_key() -> Optional[str]:
|
|
99
|
+
for name in ("OPENCLAW_TASK_SESSION_KEY", "OPENCLAW_SESSION_KEY"):
|
|
100
|
+
value = os.environ.get(name, "").strip()
|
|
101
|
+
if value:
|
|
102
|
+
return value
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _resolve_executor_session_id() -> Optional[str]:
|
|
107
|
+
value = os.environ.get("OPENCLAW_TASK_SESSION_ID", "").strip()
|
|
108
|
+
return value or None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def filter_sessions_excluded_from_compact(
|
|
112
|
+
rows: list[dict[str, Any]],
|
|
113
|
+
*,
|
|
114
|
+
executor_session_key: Optional[str] = None,
|
|
115
|
+
executor_session_id: Optional[str] = None,
|
|
116
|
+
) -> list[dict[str, Any]]:
|
|
117
|
+
"""跳过 Heartbeat 隔离会话及当前正在执行本脚本的会话。"""
|
|
118
|
+
key = (executor_session_key or "").strip()
|
|
119
|
+
sid = (executor_session_id or "").strip()
|
|
120
|
+
out: list[dict[str, Any]] = []
|
|
121
|
+
for row in rows:
|
|
122
|
+
row_key = str(row.get("key", ""))
|
|
123
|
+
if row_key.endswith(":heartbeat"):
|
|
124
|
+
continue
|
|
125
|
+
if key and row_key == key:
|
|
126
|
+
continue
|
|
127
|
+
row_sid = row.get("sessionId")
|
|
128
|
+
if sid and row_sid is not None and str(row_sid) == sid:
|
|
129
|
+
continue
|
|
130
|
+
out.append(row)
|
|
131
|
+
return out
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def filter_sessions_compactable(
|
|
135
|
+
rows: list[dict[str, Any]],
|
|
136
|
+
*,
|
|
137
|
+
executor_session_key: Optional[str] = None,
|
|
138
|
+
executor_session_id: Optional[str] = None,
|
|
139
|
+
) -> list[dict[str, Any]]:
|
|
140
|
+
rows = filter_sessions_excluded_from_compact(
|
|
141
|
+
rows,
|
|
142
|
+
executor_session_key=executor_session_key,
|
|
143
|
+
executor_session_id=executor_session_id,
|
|
144
|
+
)
|
|
145
|
+
return filter_sessions_idle(rows)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def resolve_state_dir() -> Path:
|
|
149
|
+
override = os.environ.get("OPENCLAW_STATE_DIR", "").strip()
|
|
150
|
+
if override:
|
|
151
|
+
return Path(override).expanduser()
|
|
152
|
+
return DEFAULT_STATE_DIR
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def resolve_sessions_store_path(agent_id: str, explicit: Optional[Path]) -> Path:
|
|
156
|
+
if explicit is not None:
|
|
157
|
+
return explicit.expanduser().resolve()
|
|
158
|
+
return resolve_state_dir() / "agents" / agent_id / "sessions" / "sessions.json"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def find_compact_targets(
|
|
162
|
+
store_path: Path,
|
|
163
|
+
agent_id: str,
|
|
164
|
+
threshold: int,
|
|
165
|
+
*,
|
|
166
|
+
executor_session_key: Optional[str] = None,
|
|
167
|
+
executor_session_id: Optional[str] = None,
|
|
168
|
+
) -> list[dict[str, Any]]:
|
|
169
|
+
rows = list_sessions_from_store(store_path)
|
|
170
|
+
rows = filter_sessions_by_agent(rows, agent_id)
|
|
171
|
+
rows = filter_sessions_over_threshold(rows, threshold)
|
|
172
|
+
return filter_sessions_compactable(
|
|
173
|
+
rows,
|
|
174
|
+
executor_session_key=executor_session_key or _resolve_executor_session_key(),
|
|
175
|
+
executor_session_id=executor_session_id or _resolve_executor_session_id(),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def run_compact_for_session(
|
|
180
|
+
session_key: str,
|
|
181
|
+
*,
|
|
182
|
+
timeout_sec: int,
|
|
183
|
+
dry_run: bool,
|
|
184
|
+
) -> tuple[bool, str]:
|
|
185
|
+
key = session_key.strip()
|
|
186
|
+
if not key:
|
|
187
|
+
return False, "missing sessionKey"
|
|
188
|
+
if dry_run:
|
|
189
|
+
return True, "dry-run"
|
|
190
|
+
|
|
191
|
+
params = {
|
|
192
|
+
"sessionKey": key,
|
|
193
|
+
"message": COMPACT_MESSAGE,
|
|
194
|
+
"idempotencyKey": str(uuid.uuid4()),
|
|
195
|
+
"timeoutMs": timeout_sec * 1000,
|
|
196
|
+
}
|
|
197
|
+
# CLI --timeout 为毫秒,略大于 params.timeoutMs 以等待 gateway 收尾。
|
|
198
|
+
cli_timeout_ms = (timeout_sec + 15) * 1000
|
|
199
|
+
cmd = [
|
|
200
|
+
"openclaw",
|
|
201
|
+
"gateway",
|
|
202
|
+
"call",
|
|
203
|
+
"chat.send",
|
|
204
|
+
"--params",
|
|
205
|
+
json.dumps(params, ensure_ascii=False),
|
|
206
|
+
"--expect-final",
|
|
207
|
+
"--timeout",
|
|
208
|
+
str(cli_timeout_ms),
|
|
209
|
+
]
|
|
210
|
+
try:
|
|
211
|
+
result = subprocess.run(
|
|
212
|
+
cmd,
|
|
213
|
+
capture_output=True,
|
|
214
|
+
text=True,
|
|
215
|
+
timeout=(timeout_sec + 30),
|
|
216
|
+
check=False,
|
|
217
|
+
)
|
|
218
|
+
except FileNotFoundError:
|
|
219
|
+
return False, "openclaw 命令未找到"
|
|
220
|
+
except subprocess.TimeoutExpired:
|
|
221
|
+
return False, f"超时(>{timeout_sec}s)"
|
|
222
|
+
|
|
223
|
+
if result.returncode != 0:
|
|
224
|
+
detail = (result.stderr or result.stdout or "").strip()
|
|
225
|
+
return False, detail or f"exit {result.returncode}"
|
|
226
|
+
return True, "ok"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def poll_and_compact(
|
|
230
|
+
*,
|
|
231
|
+
store_path: Path,
|
|
232
|
+
agent_id: str,
|
|
233
|
+
threshold: int,
|
|
234
|
+
dry_run: bool,
|
|
235
|
+
timeout_sec: int,
|
|
236
|
+
) -> dict[str, Any]:
|
|
237
|
+
targets = find_compact_targets(store_path, agent_id, threshold)
|
|
238
|
+
results: list[dict[str, Any]] = []
|
|
239
|
+
ok_count = 0
|
|
240
|
+
fail_count = 0
|
|
241
|
+
|
|
242
|
+
for row in targets:
|
|
243
|
+
key = row.get("key")
|
|
244
|
+
session_id = row.get("sessionId")
|
|
245
|
+
total = row.get("totalTokens")
|
|
246
|
+
|
|
247
|
+
success, detail = run_compact_for_session(
|
|
248
|
+
str(key) if key else "",
|
|
249
|
+
timeout_sec=timeout_sec,
|
|
250
|
+
dry_run=dry_run,
|
|
251
|
+
)
|
|
252
|
+
if success:
|
|
253
|
+
ok_count += 1
|
|
254
|
+
status = "dry-run" if dry_run else "compacted"
|
|
255
|
+
else:
|
|
256
|
+
fail_count += 1
|
|
257
|
+
status = "failed"
|
|
258
|
+
results.append(
|
|
259
|
+
{
|
|
260
|
+
"key": key,
|
|
261
|
+
"sessionId": session_id,
|
|
262
|
+
"totalTokens": total,
|
|
263
|
+
"statusRunning": False,
|
|
264
|
+
"status": status,
|
|
265
|
+
"detail": detail,
|
|
266
|
+
}
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
"agentId": agent_id,
|
|
271
|
+
"storePath": str(store_path),
|
|
272
|
+
"threshold": threshold,
|
|
273
|
+
"dryRun": dry_run,
|
|
274
|
+
"targetCount": len(targets),
|
|
275
|
+
"okCount": ok_count,
|
|
276
|
+
"failCount": fail_count,
|
|
277
|
+
"results": results,
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def main() -> None:
|
|
282
|
+
p = argparse.ArgumentParser(
|
|
283
|
+
description=(
|
|
284
|
+
"对 totalTokens 超过阈值、非 running、且非 Heartbeat/执行器会话的条目执行 /compact"
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
p.add_argument(
|
|
288
|
+
"--store",
|
|
289
|
+
type=Path,
|
|
290
|
+
default=None,
|
|
291
|
+
help="sessions.json 路径(默认 /home/node/.openclaw/agents/<agent>/sessions/sessions.json)",
|
|
292
|
+
)
|
|
293
|
+
p.add_argument(
|
|
294
|
+
"--agent-id",
|
|
295
|
+
type=str,
|
|
296
|
+
required=True,
|
|
297
|
+
help="当前 openclaw Agent ID(必填,由 Heartbeat 等调用方传入)",
|
|
298
|
+
)
|
|
299
|
+
p.add_argument(
|
|
300
|
+
"--threshold",
|
|
301
|
+
type=int,
|
|
302
|
+
default=DEFAULT_TOKEN_THRESHOLD,
|
|
303
|
+
help=f"totalTokens 阈值,严格大于该值才处理(默认 {DEFAULT_TOKEN_THRESHOLD})",
|
|
304
|
+
)
|
|
305
|
+
p.add_argument(
|
|
306
|
+
"--dry-run",
|
|
307
|
+
action="store_true",
|
|
308
|
+
help="只列出将压缩的会话(已排除 running),不调用 gateway chat.send",
|
|
309
|
+
)
|
|
310
|
+
p.add_argument(
|
|
311
|
+
"--timeout",
|
|
312
|
+
type=int,
|
|
313
|
+
default=DEFAULT_AGENT_TIMEOUT_SEC,
|
|
314
|
+
help=f"单次 chat.send /compact 超时秒数(默认 {DEFAULT_AGENT_TIMEOUT_SEC})",
|
|
315
|
+
)
|
|
316
|
+
p.add_argument(
|
|
317
|
+
"--json",
|
|
318
|
+
action="store_true",
|
|
319
|
+
help="以 JSON 输出汇总(便于日志/监控)",
|
|
320
|
+
)
|
|
321
|
+
args = p.parse_args()
|
|
322
|
+
|
|
323
|
+
agent_id = args.agent_id.strip()
|
|
324
|
+
if not agent_id:
|
|
325
|
+
print("错误:--agent-id 不能为空", file=sys.stderr)
|
|
326
|
+
sys.exit(1)
|
|
327
|
+
store_path = resolve_sessions_store_path(agent_id, args.store)
|
|
328
|
+
|
|
329
|
+
if not store_path.is_file():
|
|
330
|
+
print(f"错误:找不到 session store: {store_path}", file=sys.stderr)
|
|
331
|
+
sys.exit(1)
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
summary = poll_and_compact(
|
|
335
|
+
store_path=store_path,
|
|
336
|
+
agent_id=agent_id,
|
|
337
|
+
threshold=args.threshold,
|
|
338
|
+
dry_run=args.dry_run,
|
|
339
|
+
timeout_sec=max(30, args.timeout),
|
|
340
|
+
)
|
|
341
|
+
except json.JSONDecodeError as e:
|
|
342
|
+
print(f"错误:JSON 解析失败: {e}", file=sys.stderr)
|
|
343
|
+
sys.exit(1)
|
|
344
|
+
except ValueError as e:
|
|
345
|
+
print(f"错误:{e}", file=sys.stderr)
|
|
346
|
+
sys.exit(1)
|
|
347
|
+
|
|
348
|
+
if args.json:
|
|
349
|
+
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
|
350
|
+
else:
|
|
351
|
+
print(
|
|
352
|
+
f"agent={summary['agentId']} store={summary['storePath']} "
|
|
353
|
+
f"threshold>{summary['threshold']} targets={summary['targetCount']} "
|
|
354
|
+
f"ok={summary['okCount']} failed={summary['failCount']}"
|
|
355
|
+
)
|
|
356
|
+
for item in summary["results"]:
|
|
357
|
+
print(
|
|
358
|
+
f" [{item['status']}] {item.get('key')} "
|
|
359
|
+
f"tokens={item.get('totalTokens')} "
|
|
360
|
+
f"sessionId={item.get('sessionId')} "
|
|
361
|
+
f"{item.get('detail', '')}"
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
sys.exit(0)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
if __name__ == "__main__":
|
|
368
|
+
main()
|
package/package.json
CHANGED
|
@@ -167,6 +167,8 @@ uv run {baseDir}/scripts/update_openclaw.py \
|
|
|
167
167
|
|
|
168
168
|
**自动生成图片描述**:Agent 包 `.config.json` 顶层可选 **`auto_generate_image_description`**(布尔);不写或与 `true` 为开启,`false` 为关闭。与 **`update_openclaw.py`** 一并生效;具体写入 **`openclaw.json`** 的规则以实现为准,脚本标准输出中含 **`auto_generate_image_description`** 字段便于核对。
|
|
169
169
|
|
|
170
|
+
**Heartbeat(可选,默认不配置)**:`.config.json` **可以不写** `heartbeat`。此时 `update_openclaw.py` **不会**向 `openclaw.json` 写入或合并 `heartbeat` 字段(沿用该 agent 在 openclaw 中的已有配置,新装则无 heartbeat)。仅当显式提供带有效 **`every`** 的 `heartbeat` 对象时才会合并,例如 `every: "2h"`、`target: "none"`、`lightContext: true`。脚本输出含 `heartbeat_configured` / `heartbeat` 便于核对。
|
|
171
|
+
|
|
170
172
|
`NOT_INSTALLED` 无备份,直接 2)。`UPDATABLE` / `SAME_VERSION_REINSTALL` 须已完成 1)。
|
|
171
173
|
|
|
172
174
|
**输入模版(仅要备份时)**
|
|
@@ -222,6 +222,22 @@ def normalize_llm_primary(llm: Any) -> str | None:
|
|
|
222
222
|
return f"sophnet/{stripped}"
|
|
223
223
|
|
|
224
224
|
|
|
225
|
+
def normalize_heartbeat_for_openclaw(agent_def: dict[str, Any]) -> dict[str, Any] | None:
|
|
226
|
+
"""
|
|
227
|
+
仅当 .config.json 显式配置 heartbeat 且包含有效 every 时返回配置块。
|
|
228
|
+
未配置、空对象、或缺少 every 时返回 None(install 不写入 openclaw.json 的 heartbeat)。
|
|
229
|
+
"""
|
|
230
|
+
raw = agent_def.get("heartbeat")
|
|
231
|
+
if raw is None:
|
|
232
|
+
return None
|
|
233
|
+
if not isinstance(raw, dict) or not raw:
|
|
234
|
+
return None
|
|
235
|
+
every = raw.get("every")
|
|
236
|
+
if not isinstance(every, str) or not every.strip():
|
|
237
|
+
return None
|
|
238
|
+
return raw
|
|
239
|
+
|
|
240
|
+
|
|
225
241
|
def build_agent_entry(
|
|
226
242
|
agent_def: dict[str, Any],
|
|
227
243
|
*,
|
|
@@ -281,6 +297,10 @@ def build_agent_entry(
|
|
|
281
297
|
if install.get("subagents"):
|
|
282
298
|
entry["subagents"] = install["subagents"]
|
|
283
299
|
|
|
300
|
+
heartbeat = normalize_heartbeat_for_openclaw(agent_def)
|
|
301
|
+
if heartbeat is not None:
|
|
302
|
+
entry["heartbeat"] = heartbeat
|
|
303
|
+
|
|
284
304
|
return entry
|
|
285
305
|
|
|
286
306
|
|
|
@@ -16,6 +16,7 @@ from common import (
|
|
|
16
16
|
find_bot_api_accounts_by_agent_id,
|
|
17
17
|
find_agent_by_workspace,
|
|
18
18
|
is_auto_generate_image_description_enabled,
|
|
19
|
+
normalize_heartbeat_for_openclaw,
|
|
19
20
|
load_agent_definition,
|
|
20
21
|
load_openclaw_config,
|
|
21
22
|
resolve_existing_agent_entry,
|
|
@@ -147,6 +148,8 @@ def update_openclaw(
|
|
|
147
148
|
},
|
|
148
149
|
)
|
|
149
150
|
|
|
151
|
+
heartbeat_cfg = normalize_heartbeat_for_openclaw(agent_def)
|
|
152
|
+
|
|
150
153
|
return {
|
|
151
154
|
"agent_id": agent_id,
|
|
152
155
|
"openclaw_id": target_agent_id,
|
|
@@ -159,6 +162,8 @@ def update_openclaw(
|
|
|
159
162
|
"version": agent_def["version"],
|
|
160
163
|
"install_state": str(state_path),
|
|
161
164
|
"auto_generate_image_description": auto_img_desc,
|
|
165
|
+
"heartbeat_configured": heartbeat_cfg is not None,
|
|
166
|
+
"heartbeat": heartbeat_cfg,
|
|
162
167
|
}
|
|
163
168
|
|
|
164
169
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "image-classify",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"types": [
|
|
5
5
|
"store"
|
|
6
6
|
],
|
|
@@ -8,12 +8,19 @@
|
|
|
8
8
|
"description": "通过人脸识别对照片进行分类、搜索和管理",
|
|
9
9
|
"changelog": [
|
|
10
10
|
{
|
|
11
|
-
"version": "1.0.
|
|
12
|
-
"date": "2026-
|
|
11
|
+
"version": "1.0.4",
|
|
12
|
+
"date": "2026-05-18",
|
|
13
13
|
"changes": [
|
|
14
|
-
"
|
|
14
|
+
"人脸检测的阈值从0.7降低到0.5"
|
|
15
15
|
]
|
|
16
16
|
},
|
|
17
|
+
{
|
|
18
|
+
"changes": [
|
|
19
|
+
"照片分类器添加对自动发送虾友的支持"
|
|
20
|
+
],
|
|
21
|
+
"date": "2026-04-15",
|
|
22
|
+
"version": "1.0.3"
|
|
23
|
+
},
|
|
17
24
|
{
|
|
18
25
|
"changes": [
|
|
19
26
|
"配置文件读写统一使用 UTF-8 编码(open 显式指定 encoding)"
|
|
@@ -38,5 +45,5 @@
|
|
|
38
45
|
}
|
|
39
46
|
],
|
|
40
47
|
"createdAt": "2026-04-10",
|
|
41
|
-
"updatedAt": "2026-
|
|
48
|
+
"updatedAt": "2026-05-18"
|
|
42
49
|
}
|
|
@@ -363,6 +363,6 @@ uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json uplo
|
|
|
363
363
|
- `pack` 与 `copy` 均依赖 `config.json` 中保存的搜索结果(`search_result` 或 `quick_search_result` 等),须先执行 `search`、`classify` 或 `quick-search`。默认流程只需 **`pack`**;`copy` 为可选,由用户另行提出时再执行。
|
|
364
364
|
- **`classify`** 会向 `friendId` 发**分类结果摘要**;**`pack`** 会向 `friendId` 发**打包下载链接**(两次私信、用途不同);若只需其一,可后续再改脚本或配置(当前实现为两者都发)。
|
|
365
365
|
- `pack` 的压缩文件存放在系统临时目录中,上传完成后可忽略清理。上传超时默认 120 秒,文件特别大时可通过 `--timeout` 增大。
|
|
366
|
-
- `config.json` 中的 `query_threshold`(默认 0.
|
|
366
|
+
- `config.json` 中的 `query_threshold`(默认 0.5)控制人脸检测置信度阈值,`search_similarity_threshold`(默认 0.3)控制搜索匹配的最低相似度。
|
|
367
367
|
- `{baseDir}` 指本 skill 根目录(如 `skills/image-classify`),调用时替换为实际绝对路径。
|
|
368
368
|
- `friendId`(虾友号)与可选 `friendLabel` 写在用户条目下;**注册时**通过 `add ... --friend-id` / `--friend-label` 写入。后续若需修改可编辑 `config.json`(本 skill 未单独提供改绑子命令)。旧键名仍兼容读取。
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "online-bug-report",
|
|
3
|
+
"version": "1.0.5",
|
|
4
|
+
"types": [
|
|
5
|
+
"store"
|
|
6
|
+
],
|
|
7
|
+
"displayName": "在线提Bug",
|
|
8
|
+
"description": "用户说「在线提交bug」时收集 Bug 并经由虾友 DM 发送工单(含 session 附件)",
|
|
9
|
+
"changelog": [
|
|
10
|
+
{
|
|
11
|
+
"changes": [
|
|
12
|
+
"凭证移至 src/secrets/bug-report.json,与 npm 打包仅含 src/ 对齐"
|
|
13
|
+
],
|
|
14
|
+
"date": "2026-05-19",
|
|
15
|
+
"version": "1.0.5"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"changes": [
|
|
19
|
+
"凭证仅使用 secrets/bug-report.json,移除 .secrets 候选路径"
|
|
20
|
+
],
|
|
21
|
+
"date": "2026-05-19",
|
|
22
|
+
"version": "1.0.4"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"changes": [
|
|
26
|
+
"内置 secrets/bug-report.json 随 skill 下发,下载即用"
|
|
27
|
+
],
|
|
28
|
+
"date": "2026-05-19",
|
|
29
|
+
"version": "1.0.3"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"changes": [
|
|
33
|
+
"凭证改为 .secrets/bug-report.json;明确唯一触发词;移除仓库内嵌凭证"
|
|
34
|
+
],
|
|
35
|
+
"date": "2026-05-19",
|
|
36
|
+
"version": "1.0.2"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"changes": [
|
|
40
|
+
"工单环境区增加「模型」字段;支持 --model 与 default_model 配置"
|
|
41
|
+
],
|
|
42
|
+
"date": "2026-05-18",
|
|
43
|
+
"version": "1.0.1"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"changes": [
|
|
47
|
+
"初次提交:精简工单模板、preview/send、虾友 DM 外推"
|
|
48
|
+
],
|
|
49
|
+
"date": "2026-05-18",
|
|
50
|
+
"version": "1.0.0"
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"createdAt": "2026-05-18",
|
|
54
|
+
"updatedAt": "2026-05-19"
|
|
55
|
+
}
|