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.
- package/package.json +1 -1
- package/skills/agent-install/skill.json +9 -2
- package/skills/agent-install/src/scripts/update_openclaw.py +62 -0
- package/skills/online-bug-report/skill.json +27 -3
- package/skills/online-bug-report/src/SKILL.md +26 -102
- package/skills/online-bug-report/src/pyproject.toml +10 -2
- package/skills/online-bug-report/src/references/config.example.json +1 -4
- package/skills/online-bug-report/src/scripts/report_bug.py +202 -103
- package/skills/online-bug-report/src/secrets/bug-report.json +1 -4
- package/skills/sophnet-image-ocr/skill.json +15 -3
- package/skills/sophnet-image-ocr/src/SKILL.md +28 -125
- package/skills/sophnet-image-ocr/src/pyproject.toml +8 -2
- package/skills/sophnet-image-ocr/src/references/api-details.md +36 -0
- package/skills/sophnet-image-ocr/src/scripts/ocr.py +26 -2
- package/skills/sophnet-image-ocr/src/uv.lock +0 -234
|
@@ -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
|
-
|
|
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"
|
|
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"
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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.
|
|
224
|
-
|
|
225
|
-
|
|
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
|
|
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
|
|
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
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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=
|
|
434
|
-
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]) ->
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
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",
|
|
476
|
-
parser.add_argument("--sender-name",
|
|
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("--
|
|
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 =
|
|
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
|
|
599
|
-
api_base =
|
|
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
|
|
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"
|
|
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,12 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sophnet-image-ocr",
|
|
3
|
-
"version": "1.
|
|
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-
|
|
31
|
+
"updatedAt": "2026-05-26"
|
|
20
32
|
}
|