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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.0",
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
- # HEARTBEAT.md
1
+ # Heartbeat checklist
2
2
 
3
- # Keep this file empty (or with only comments) to skip heartbeat API calls.
4
-
5
- # Add tasks below when you want the agent to check something periodically.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sophhub",
3
- "version": "0.4.22",
3
+ "version": "0.4.24",
4
4
  "description": "SophHub CLI - Manage and download AI Agent skills and agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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",
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.3",
12
- "date": "2026-04-15",
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-04-15"
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.7)控制人脸检测置信度阈值,`search_similarity_threshold`(默认 0.3)控制搜索匹配的最低相似度。
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 未单独提供改绑子命令)。旧键名仍兼容读取。
@@ -1,4 +1,4 @@
1
1
  {
2
- "query_threshold": 0.7,
2
+ "query_threshold": 0.5,
3
3
  "search_similarity_threshold": 0.3
4
4
  }
@@ -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
+ }