sophhub 0.4.28 → 0.4.30

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.
@@ -15,19 +15,16 @@ from dataclasses import dataclass, field
15
15
  from datetime import datetime
16
16
  from pathlib import Path
17
17
  from typing import Any, Optional
18
-
18
+ import sophnet_tools
19
19
 
20
20
  DEFAULT_TIMEOUT = 30
21
21
  DEFAULT_API_BASE_URL = "https://yagent.sophnet.com/api"
22
22
  DEFAULT_JWT_PATH = Path("/home/node/.openclaw/jwt.json")
23
23
  DEFAULT_OPENCLAW_BASE_PATH = Path("/home/node/.openclaw/.base.json")
24
- DEFAULT_SOURCE_LABEL = "虾友 DM"
24
+ OSS_UPLOAD_TIMEOUT = 60
25
25
 
26
26
  CONFIG_ALIASES: dict[str, tuple[str, ...]] = {
27
27
  "dev_friend_id": ("dev_friend_id", "DEV_FRIEND_ID", "friend_id"),
28
- "dev_friend_name": ("dev_friend_name", "DEV_FRIEND_NAME", "friend_name"),
29
- "terminal_label": ("terminal_label", "TERMINAL_LABEL"),
30
- "terminal_url": ("terminal_url", "TERMINAL_URL"),
31
28
  "session_url_template": (
32
29
  "session_url_template",
33
30
  "SESSION_URL_TEMPLATE",
@@ -39,10 +36,16 @@ CONFIG_ALIASES: dict[str, tuple[str, ...]] = {
39
36
  "OPENCLAW_BASE_URL",
40
37
  "sophclaw_base_url",
41
38
  ),
42
- "default_model": ("default_model", "DEFAULT_MODEL", "llm", "model"),
43
39
  }
44
40
 
45
41
 
42
+ def get_sessions_dir(agent_id: str) -> Path:
43
+ """根据 agentId 构造 sessions 目录路径。"""
44
+ if not agent_id.strip():
45
+ raise AppError("agent-id 不能为空")
46
+ return Path("/home/node/.openclaw/agents") / agent_id.strip() / "sessions"
47
+
48
+
46
49
  class AppError(RuntimeError):
47
50
  pass
48
51
 
@@ -161,12 +164,12 @@ class BugReport:
161
164
  sender_name: str
162
165
  user_description: str
163
166
  openclaw_base_url: Optional[str] = None
164
- terminal_label: str = DEFAULT_SOURCE_LABEL
165
- terminal_url: Optional[str] = None
166
167
  session_key: Optional[str] = None
167
168
  session_url: Optional[str] = None
168
- context: Optional[str] = None
169
169
  model: Optional[str] = None
170
+ model_provider: Optional[str] = None
171
+ context_tokens: Optional[int] = None
172
+ total_tokens: Optional[int] = None
170
173
  oss_urls: list[str] = field(default_factory=list)
171
174
  reported_at: Optional[str] = None
172
175
 
@@ -185,44 +188,29 @@ def format_bug_message(report: BugReport) -> str:
185
188
  ts = report.reported_at or datetime.now().strftime("%Y-%m-%d %H:%M:%S")
186
189
  lines = [
187
190
  f"【在线 Bug】{ts}",
188
- "",
189
- "▎发件人",
190
- f"- 用户: {report.sender_name.strip()} ({report.sender_id.strip()})",
191
- f"- 来源: {report.terminal_label.strip() or DEFAULT_SOURCE_LABEL}",
192
- "",
193
- "▎用户描述",
194
- report.user_description.strip(),
195
- "",
196
- "▎环境",
191
+ f"▎发件人:{report.sender_name.strip()}",
192
+ f"▎ID: {report.sender_id.strip()}",
193
+ f"▎会话:{report.session_key.strip() if report.session_key else '(无)'}",
197
194
  ]
198
195
  if report.openclaw_base_url:
199
- lines.append(f"- OpenClaw Base URL: {report.openclaw_base_url}")
200
- else:
201
- lines.append("- OpenClaw Base URL: (未配置)")
202
- terminal_line = f"- 终端: {report.terminal_label.strip() or DEFAULT_SOURCE_LABEL}"
203
- if report.terminal_url and report.terminal_url.strip():
204
- terminal_line += f" ({report.terminal_url.strip()})"
205
- lines.append(terminal_line)
196
+ lines.append(f"Base URL: {report.openclaw_base_url}")
206
197
  if report.model and report.model.strip():
207
- lines.append(f"- 模型: {report.model.strip()}")
208
- else:
209
- lines.append("- 模型: (未知)")
210
-
211
- lines.extend(["", "▎会话"])
212
- if report.session_key and report.session_key.strip():
213
- lines.append(f"- sessionKey: {report.session_key.strip()}")
214
- else:
215
- lines.append("- sessionKey: (无)")
216
- if report.session_url and report.session_url.strip():
217
- lines.append(f"- Session 链接: {report.session_url.strip()}")
218
- if report.context and report.context.strip():
219
- lines.extend(["- 近期上下文:", report.context.strip()])
198
+ model_line = report.model.strip()
199
+ if report.model_provider and report.model_provider.strip():
200
+ model_line = f"{report.model_provider.strip()}/{report.model.strip()}"
201
+ lines.append(f"▎模型: {model_line}")
202
+ if report.context_tokens is not None:
203
+ lines.append(f"▎上下文大小: {report.context_tokens}")
204
+ if report.total_tokens is not None:
205
+ lines.append(f"▎当前上下文占用: {report.total_tokens}")
220
206
 
221
- lines.extend(["", "▎附件"])
222
207
  if report.oss_urls:
223
- lines.extend(report.oss_urls)
224
- else:
225
- lines.append("(无)")
208
+ lines.append(f"▎附件:{report.oss_urls[0]}")
209
+ for u in report.oss_urls[1:]:
210
+ lines.append(f" {u}")
211
+
212
+ lines.append(f"▎用户描述:")
213
+ lines.append(f" {report.user_description.strip()}")
226
214
 
227
215
  return "\n".join(lines).rstrip() + "\n"
228
216
 
@@ -349,7 +337,7 @@ def load_friends(api_base: str, token: str, timeout: int) -> list[dict[str, Any]
349
337
  return out
350
338
 
351
339
 
352
- def find_nearest_agent_config(start_dir: Path | None = None) -> Path | None:
340
+ def find_nearest_agent_config(start_dir: Optional[Path] = None) -> Optional[Path]:
353
341
  current = (start_dir or Path.cwd()).resolve()
354
342
  for candidate in [current, *current.parents]:
355
343
  config_path = candidate / ".config.json"
@@ -358,7 +346,7 @@ def find_nearest_agent_config(start_dir: Path | None = None) -> Path | None:
358
346
  return None
359
347
 
360
348
 
361
- def read_model_from_agent_config(config_path: Path | None = None) -> Optional[str]:
349
+ def read_model_from_agent_config(config_path: Optional[Path] = None) -> Optional[str]:
362
350
  path = config_path or find_nearest_agent_config()
363
351
  if path is None:
364
352
  return None
@@ -375,39 +363,105 @@ def read_model_from_agent_config(config_path: Path | None = None) -> Optional[st
375
363
  return None
376
364
 
377
365
 
378
- def read_context_file(path: Optional[str]) -> Optional[str]:
379
- if not path:
380
- return None
381
- file_path = Path(path).expanduser()
366
+ def resolve_sender_from_jwt(jwt_path: Path = DEFAULT_JWT_PATH) -> tuple[str, str]:
367
+ """从 JWT payload 中解析 userId 和 name 作为 sender_id / sender_name。"""
368
+ data = load_json_file(jwt_path)
369
+ token = data.get("web_jwt")
370
+ if not isinstance(token, str) or not token.strip():
371
+ raise AppError(f"JWT 文件缺少有效 web_jwt: {jwt_path}")
372
+ token = token.strip()
373
+ if token.lower().startswith("bearer "):
374
+ token = token[7:].strip()
375
+ parts = token.split(".")
376
+ if len(parts) != 3:
377
+ raise AppError("web_jwt 不是有效的 JWT 格式")
378
+ payload_b64 = parts[1]
379
+ remainder = len(payload_b64) % 4
380
+ if remainder:
381
+ payload_b64 += "=" * (4 - remainder)
382
382
  try:
383
- text = file_path.read_text(encoding="utf-8")
384
- except OSError as exc:
385
- raise AppError(f"无法读取 context 文件 {file_path}: {exc}") from exc
386
- text = text.strip()
387
- return text or None
383
+ raw = base64.urlsafe_b64decode(payload_b64.encode("ascii"))
384
+ payload = json.loads(raw)
385
+ except (ValueError, UnicodeDecodeError, json.JSONDecodeError) as exc:
386
+ raise AppError(f"JWT payload 解码失败: {exc}") from exc
387
+ user_id = payload.get("userId")
388
+ if user_id is None:
389
+ raise AppError("JWT payload 中未找到 userId 字段")
390
+ sender_id = str(int(user_id))
391
+ sender_name = str(payload.get("name") or "")
392
+ if not sender_name.strip():
393
+ sender_name = sender_id
394
+ return sender_id, sender_name
395
+
396
+
397
+ def resolve_session_file(session_key: str, agent_id: str) -> tuple[Path, dict[str, Any]]:
398
+ """根据 sessionKey 查找 .jsonl 会话文件,同时返回 session 元信息。"""
399
+ if not session_key.strip():
400
+ raise AppError("session-key 不能为空")
401
+ if not agent_id.strip():
402
+ raise AppError("agent-id 不能为空")
403
+ sessions_dir = get_sessions_dir(agent_id)
404
+ sessions_json = sessions_dir / "sessions.json"
405
+ if not sessions_json.is_file():
406
+ raise AppError(f"sessions.json 不存在: {sessions_json}")
407
+ data = load_json_file(sessions_json)
408
+ session_entry = data.get(session_key.strip())
409
+ if not isinstance(session_entry, dict):
410
+ raise AppError(f"sessions.json 中未找到 sessionKey={session_key} 的记录")
411
+ session_file = Path(session_entry.get("sessionFile") or "")
412
+ if not session_file.is_file():
413
+ raise AppError(f"session 文件不存在: {session_file}")
414
+ meta = {
415
+ "modelProvider": session_entry.get("modelProvider"),
416
+ "model": session_entry.get("model"),
417
+ "contextTokens": session_entry.get("contextTokens"),
418
+ "totalTokens": session_entry.get("totalTokens"),
419
+ }
420
+ return session_file, meta
421
+
422
+
423
+ def upload_session_to_oss(session_file: Path, timeout: int = OSS_UPLOAD_TIMEOUT) -> str:
424
+ """上传 session 文件到 OSS,返回签名下载 URL。"""
425
+ signed_url = sophnet_tools.upload_oss(str(session_file), timeout=timeout)
426
+ if not signed_url:
427
+ raise AppError("OSS 上传失败:未返回下载 URL")
428
+ return signed_url
388
429
 
389
430
 
390
431
  def build_report_from_args(args: argparse.Namespace) -> BugReport:
391
432
  cred = load_cred_file(args.cred_file)
392
433
 
434
+ # sender: 显式传入优先,否则从 JWT 自动解析
435
+ sender_id = (args.sender_id or "").strip()
436
+ sender_name = (args.sender_name or "").strip()
437
+ if not sender_id or not sender_name:
438
+ jwt_id, jwt_name = resolve_sender_from_jwt()
439
+ if not sender_id:
440
+ sender_id = jwt_id
441
+ if not sender_name:
442
+ sender_name = jwt_name
443
+
393
444
  openclaw_base = (
394
445
  (args.openclaw_base_url or "").strip()
395
446
  or cfg_pick(cred, "openclaw_base_url", "OPENCLAW_BASE_URL")
396
447
  or read_openclaw_base_url(Path(args.openclaw_base_path).expanduser())
397
448
  )
398
449
 
399
- terminal_label = (
400
- (args.terminal_label or "").strip()
401
- or cfg_pick(cred, "terminal_label", "BUG_TERMINAL_LABEL")
402
- or DEFAULT_SOURCE_LABEL
403
- )
404
- terminal_url = (
405
- (args.terminal_url or "").strip()
406
- or cfg_pick(cred, "terminal_url", "BUG_TERMINAL_URL")
407
- or None
408
- )
409
-
410
450
  session_key = (args.session_key or "").strip() or None
451
+ agent_id = (args.agent_id or "").strip() or None
452
+
453
+ # OSS URL: 显式传入优先,否则根据 session_key 自动上传
454
+ explicit_oss_urls = [normalize_http_url(u, field_name="oss-url") for u in (args.oss_url or [])]
455
+ session_meta: dict[str, Any] = {}
456
+ if explicit_oss_urls:
457
+ oss_urls = explicit_oss_urls
458
+ elif session_key and agent_id:
459
+ session_file, session_meta = resolve_session_file(session_key, agent_id)
460
+ oss_url = upload_session_to_oss(session_file)
461
+ oss_urls = [oss_url]
462
+ else:
463
+ oss_urls = []
464
+
411
465
  template = (args.session_url_template or "").strip() or cfg_pick(
412
466
  cred, "session_url_template", "BUG_SESSION_URL_TEMPLATE"
413
467
  )
@@ -419,33 +473,38 @@ def build_report_from_args(args: argparse.Namespace) -> BugReport:
419
473
  openclaw_base,
420
474
  )
421
475
 
422
- context = (args.context or "").strip() or read_context_file(args.context_file)
423
-
424
- oss_urls = [normalize_http_url(u, field_name="oss-url") for u in (args.oss_url or [])]
425
-
426
476
  model = (
427
477
  (args.model or "").strip()
478
+ or session_meta.get("model")
428
479
  or cfg_pick(cred, "default_model", "BUG_MODEL")
429
480
  or read_model_from_agent_config()
430
481
  ) or None
482
+ model_provider = (
483
+ session_meta.get("modelProvider")
484
+ or cfg_pick(cred, "model_provider")
485
+ ) or None
486
+
487
+ def _session_int(key: str) -> Optional[int]:
488
+ v = session_meta.get(key)
489
+ return int(v) if isinstance(v, (int, float)) else None
431
490
 
432
491
  return BugReport(
433
- sender_id=args.sender_id,
434
- sender_name=args.sender_name,
492
+ sender_id=sender_id,
493
+ sender_name=sender_name,
435
494
  user_description=args.description,
436
495
  openclaw_base_url=openclaw_base,
437
- terminal_label=terminal_label,
438
- terminal_url=terminal_url,
439
496
  session_key=session_key,
440
497
  session_url=session_url,
441
- context=context,
442
498
  model=model,
499
+ model_provider=model_provider,
500
+ context_tokens=_session_int("contextTokens"),
501
+ total_tokens=_session_int("totalTokens"),
443
502
  oss_urls=oss_urls,
444
503
  reported_at=(args.reported_at or "").strip() or None,
445
504
  )
446
505
 
447
506
 
448
- def resolve_dev_friend(args: argparse.Namespace, cred: dict[str, Any]) -> tuple[int, str]:
507
+ def resolve_dev_friend(args: argparse.Namespace, cred: dict[str, Any]) -> int:
449
508
  if args.friend_id is not None and args.friend_id > 0:
450
509
  friend_id = int(args.friend_id)
451
510
  else:
@@ -458,12 +517,52 @@ def resolve_dev_friend(args: argparse.Namespace, cred: dict[str, Any]) -> tuple[
458
517
  raise AppError(f"dev_friend_id 不是有效整数: {raw}") from exc
459
518
  if friend_id <= 0:
460
519
  raise AppError("friend-id 必须为正整数")
461
- friend_name = (
462
- (args.friend_name or "").strip()
463
- or cfg_pick(cred, "dev_friend_name", "BUG_DEV_FRIEND_NAME")
464
- or "研发值班"
520
+ return friend_id
521
+
522
+
523
+ def request_friend(api_base: str, token: str, friend_id: int, timeout: int) -> dict[str, Any]:
524
+ """发起好友请求。"""
525
+ url = f"{api_base}/sys/openclaw/friend/request"
526
+ return request_json("POST", url, token, timeout, payload={"friendId": friend_id})
527
+
528
+
529
+ def resolve_api_base(
530
+ args: argparse.Namespace,
531
+ cred: dict[str, Any],
532
+ ) -> str:
533
+ """从参数和凭证中解析 api_base_url,三处调用共用。"""
534
+ return normalize_http_url(
535
+ (args.api_base_url or "").strip()
536
+ or cfg_pick(cred, "api_base_url", "BUG_API_BASE_URL")
537
+ or DEFAULT_API_BASE_URL,
538
+ field_name="api-base-url",
465
539
  )
466
- return friend_id, friend_name
540
+
541
+
542
+ def cmd_ensure_friend(args: argparse.Namespace) -> int:
543
+ """ensure-friend 子命令:检查 dev_friend_id 是否已是好友,否则发起好友请求。
544
+
545
+ 退出码: 0=已是好友, 2=刚发起好友请求, 1=异常
546
+ """
547
+ cred = load_cred_file(getattr(args, "cred_file", None), required=True)
548
+ friend_id = resolve_dev_friend(args, cred)
549
+ api_base = resolve_api_base(args, cred)
550
+ jwt_path = Path(args.jwt_path).expanduser()
551
+ token = read_bearer_token(jwt_path)
552
+ ensure_jwt_valid(token, jwt_path, args.allow_expired_jwt)
553
+
554
+ friends = load_friends(api_base, token, args.timeout)
555
+ friend_ids = {f.get("friendId") for f in friends}
556
+
557
+ if friend_id in friend_ids:
558
+ result = {"status": "already_friend", "friendId": friend_id}
559
+ print_json(result)
560
+ return 0
561
+
562
+ request_friend(api_base, token, friend_id, args.timeout)
563
+ result = {"status": "request_sent", "friendId": friend_id}
564
+ print_json(result)
565
+ return 2
467
566
 
468
567
 
469
568
  def add_report_args(parser: argparse.ArgumentParser) -> None:
@@ -472,10 +571,11 @@ def add_report_args(parser: argparse.ArgumentParser) -> None:
472
571
  "-c",
473
572
  help="凭证 JSON 路径(默认 src/secrets/bug-report.json)",
474
573
  )
475
- parser.add_argument("--sender-id", required=True, help="发件人 ID(虾友/Bot senderId)")
476
- parser.add_argument("--sender-name", required=True, help="发件人展示名")
574
+ parser.add_argument("--sender-id", default="", help="发件人 ID(默认从 JWT 自动解析)")
575
+ parser.add_argument("--sender-name", default="", help="发件人展示名(默认从 JWT 自动解析)")
477
576
  parser.add_argument("--description", required=True, help="用户 Bug 描述")
478
577
  parser.add_argument("--session-key", default="", help="当前会话 sessionKey")
578
+ parser.add_argument("--agent-id", default="", help="当前 Agent ID(用于定位 sessions 目录)")
479
579
  parser.add_argument(
480
580
  "--session-url",
481
581
  default="",
@@ -496,15 +596,10 @@ def add_report_args(parser: argparse.ArgumentParser) -> None:
496
596
  default=str(DEFAULT_OPENCLAW_BASE_PATH),
497
597
  help="base_url JSON 路径",
498
598
  )
499
- parser.add_argument("--terminal-label", default="", help="终端名称,默认「虾友 DM」")
500
- parser.add_argument("--terminal-url", default="", help="终端入口 URL(可选)")
501
- parser.add_argument(
502
- "--model",
599
+ parser.add_argument("--model",
503
600
  default="",
504
601
  help="用户/客服会话使用的模型(默认读凭证 default_model 或就近 .config.json 的 llm)",
505
602
  )
506
- parser.add_argument("--context", default="", help="近期对话上下文(多行文本)")
507
- parser.add_argument("--context-file", help="从文件读取近期上下文")
508
603
  parser.add_argument(
509
604
  "--oss-url",
510
605
  action="append",
@@ -524,7 +619,6 @@ def add_http_args(parser: argparse.ArgumentParser) -> None:
524
619
  parser.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT)
525
620
  parser.add_argument("--allow-expired-jwt", action="store_true")
526
621
  parser.add_argument("--friend-id", type=int, help="研发值班虾友 friendId")
527
- parser.add_argument("--friend-name", default="", help="研发虾友展示名(日志用)")
528
622
 
529
623
 
530
624
  def build_parser() -> argparse.ArgumentParser:
@@ -540,6 +634,15 @@ def build_parser() -> argparse.ArgumentParser:
540
634
  add_http_args(p_send)
541
635
  p_send.add_argument("--json", action="store_true", help="JSON 输出")
542
636
 
637
+ p_ensure = sub.add_parser("ensure-friend", help="校验研发虾友是否为好友,否则发起好友请求")
638
+ p_ensure.add_argument(
639
+ "--cred-file",
640
+ "-c",
641
+ help="凭证 JSON 路径(默认 src/secrets/bug-report.json)",
642
+ )
643
+ add_http_args(p_ensure)
644
+ p_ensure.add_argument("--json", action="store_true", help="JSON 输出")
645
+
543
646
  p_friends = sub.add_parser("list-friends", help="列出虾友(配置 dev_friend_id 时用)")
544
647
  p_friends.add_argument(
545
648
  "--cred-file",
@@ -558,14 +661,12 @@ def main(argv: Optional[list[str]] = None) -> int:
558
661
  if getattr(args, "timeout", DEFAULT_TIMEOUT) <= 0:
559
662
  raise AppError("timeout 必须为正整数")
560
663
 
664
+ if args.command == "ensure-friend":
665
+ return cmd_ensure_friend(args)
666
+
561
667
  if args.command == "list-friends":
562
668
  cred = load_cred_file(getattr(args, "cred_file", None))
563
- api_base = normalize_http_url(
564
- (args.api_base_url or "").strip()
565
- or cfg_pick(cred, "api_base_url", "BUG_API_BASE_URL")
566
- or DEFAULT_API_BASE_URL,
567
- field_name="api-base-url",
568
- )
669
+ api_base = resolve_api_base(args, cred)
569
670
  token = read_bearer_token(Path(args.jwt_path).expanduser())
570
671
  ensure_jwt_valid(token, Path(args.jwt_path).expanduser(), args.allow_expired_jwt)
571
672
  friends = load_friends(api_base, token, args.timeout)
@@ -583,6 +684,9 @@ def main(argv: Optional[list[str]] = None) -> int:
583
684
  "senderId": report.sender_id,
584
685
  "senderName": report.sender_name,
585
686
  "model": report.model,
687
+ "modelProvider": report.model_provider,
688
+ "contextTokens": report.context_tokens,
689
+ "totalTokens": report.total_tokens,
586
690
  "sessionKey": report.session_key,
587
691
  "sessionUrl": report.session_url,
588
692
  "ossUrls": report.oss_urls,
@@ -595,13 +699,8 @@ def main(argv: Optional[list[str]] = None) -> int:
595
699
  return 0
596
700
 
597
701
  cred = load_cred_file(args.cred_file, required=True)
598
- friend_id, friend_name = resolve_dev_friend(args, cred)
599
- api_base = normalize_http_url(
600
- (args.api_base_url or "").strip()
601
- or cfg_pick(cred, "api_base_url", "BUG_API_BASE_URL")
602
- or DEFAULT_API_BASE_URL,
603
- field_name="api-base-url",
604
- )
702
+ friend_id = resolve_dev_friend(args, cred)
703
+ api_base = resolve_api_base(args, cred)
605
704
  jwt_path = Path(args.jwt_path).expanduser()
606
705
  token = read_bearer_token(jwt_path)
607
706
  ensure_jwt_valid(token, jwt_path, args.allow_expired_jwt)
@@ -609,7 +708,7 @@ def main(argv: Optional[list[str]] = None) -> int:
609
708
  sent = send_im_message(api_base, token, rid, message, args.timeout)
610
709
  result = {
611
710
  "success": True,
612
- "friend": {"friendId": friend_id, "displayName": friend_name},
711
+ "friend": {"friendId": friend_id},
613
712
  "rid": rid,
614
713
  "messageId": sent.get("_id"),
615
714
  "message": message,
@@ -618,7 +717,7 @@ def main(argv: Optional[list[str]] = None) -> int:
618
717
  print_json(result)
619
718
  else:
620
719
  print("✅ Bug 工单已发送给研发虾友。")
621
- print(f"收件人: {friend_name} (friendId={friend_id})")
720
+ print(f"收件人 friendId={friend_id}")
622
721
  if sent.get("_id"):
623
722
  print(f"messageId: {sent.get('_id')}")
624
723
  return 0
@@ -1,6 +1,3 @@
1
1
  {
2
- "dev_friend_id": 55039,
3
- "dev_friend_name": "用户2199",
4
- "terminal_label": "虾友 DM",
5
- "default_model": "Qwen3.5-122B-A10B"
2
+ "dev_friend_id": 23858
6
3
  }
@@ -1,12 +1,24 @@
1
1
  {
2
2
  "name": "sophnet-image-ocr",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "types": [
5
5
  "builtin"
6
6
  ],
7
7
  "displayName": "",
8
- "description": "",
8
+ "description": "Use when the user needs to extract text, tables, or structured content from images or PDF files. Supports local files and URLs via Sophnet OCR API.",
9
9
  "changelog": [
10
+ {
11
+ "version": "1.1.0",
12
+ "date": "2026-05-26",
13
+ "changes": [
14
+ "SKILL.md 精简优化(168行→69行),description 改为触发条件式",
15
+ "pymupdf 改为懒加载,首次 PDF 使用时从阿里云镜像自动安装,图片 OCR 不再受 pymupdf 下载超时影响",
16
+ "OCR 模型升级:PaddleOCR-VL-0.9B → PaddleOCR-VL-1.5",
17
+ "新增 design.md 设计文档和 references/api-details.md",
18
+ "删除 src/uv.lock",
19
+ "pyproject.toml 配置阿里云镜像源"
20
+ ]
21
+ },
10
22
  {
11
23
  "version": "1.0.0",
12
24
  "date": "2026-04-09",
@@ -16,5 +28,5 @@
16
28
  }
17
29
  ],
18
30
  "createdAt": "2026-04-09",
19
- "updatedAt": "2026-04-09"
31
+ "updatedAt": "2026-05-26"
20
32
  }