@miraiva_test/miravia-order-report 0.1.13
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/bin/cli.js +61 -0
- package/dist/.keypair.json +5 -0
- package/dist/README.md +249 -0
- package/dist/SKILL.md +329 -0
- package/dist/VERSION +1 -0
- package/dist/executive_summary_prompt_v2.md +574 -0
- package/dist/reference.md +409 -0
- package/dist/requirements.txt +6 -0
- package/dist/scripts/auth_cli.py +1442 -0
- package/dist/scripts/config.py +216 -0
- package/dist/scripts/cred_crypto.py +235 -0
- package/dist/scripts/estimate_time.py +244 -0
- package/dist/scripts/export_orders.py +1549 -0
- package/dist/scripts/i18n.py +1162 -0
- package/dist/scripts/inject_insights.py +96 -0
- package/dist/scripts/login.bat +14 -0
- package/dist/scripts/miravia_client.py +468 -0
- package/dist/scripts/report_v4.py +1694 -0
- package/dist/scripts/run.bat +15 -0
- package/dist/scripts/skill_crypto.py +86 -0
- package/dist/scripts/skill_keypair.py +86 -0
- package/dist/scripts/skill_logger.py +262 -0
- package/dist/scripts/test_v4_render.py +322 -0
- package/dist/scripts/trace_logger.py +97 -0
- package/dist/scripts/validate_render.py +101 -0
- package/dist/static/chart.umd.min.js +20 -0
- package/dist/static/chartjs-plugin-datalabels.min.js +7 -0
- package/dist/static/miravia-logo.svg +61 -0
- package/dist/templates/report_v4.html +1005 -0
- package/lib/build-dist.js +86 -0
- package/lib/clean-dist.js +18 -0
- package/lib/installer.js +444 -0
- package/package.json +34 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
REM ──────────────────────────────────────────────────────────────────────
|
|
3
|
+
REM run.bat — Windows CMD wrapper for running any Python script
|
|
4
|
+
REM
|
|
5
|
+
REM Purpose: Bypass PowerShell Constrained Language Mode that blocks
|
|
6
|
+
REM .NET method calls (System.Convert, System.Text.Encoding) in
|
|
7
|
+
REM enterprise-managed Windows environments.
|
|
8
|
+
REM
|
|
9
|
+
REM Usage: scripts\run.bat <script.py> [arguments...]
|
|
10
|
+
REM Example: scripts\run.bat scripts/export_orders.py --agent-type cursor --ping
|
|
11
|
+
REM scripts\run.bat scripts/estimate_time.py --agent-type cursor --last-days 30
|
|
12
|
+
REM ──────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
cd /d "%~dp0.."
|
|
15
|
+
python %*
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Skill Hub encrypted payload decryption.
|
|
5
|
+
|
|
6
|
+
Aligned with server-side SkillSignatureUtil:
|
|
7
|
+
- Cipher: RSA/ECB/PKCS1Padding (signMethod=SHA256withRSA / SHA1withRSA default)
|
|
8
|
+
or RSA/ECB/OAEPWithSHA-512AndMGF1Padding (signMethod=SHA512withRSA)
|
|
9
|
+
- Encoding: Base64URL (no padding)
|
|
10
|
+
- Chunking: each ciphertext block = keySize / 8 (RSA-2048 -> 256 bytes)
|
|
11
|
+
|
|
12
|
+
Dependency: cryptography>=41
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import base64
|
|
18
|
+
import json
|
|
19
|
+
from typing import Dict
|
|
20
|
+
|
|
21
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
22
|
+
from cryptography.hazmat.primitives.asymmetric import padding as asym_padding
|
|
23
|
+
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SkillAuthDecryptError(RuntimeError):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _b64url_decode(s: str) -> bytes:
|
|
31
|
+
if not s:
|
|
32
|
+
raise SkillAuthDecryptError("cipher is empty")
|
|
33
|
+
s = s.strip()
|
|
34
|
+
pad = "=" * (-len(s) % 4)
|
|
35
|
+
try:
|
|
36
|
+
return base64.urlsafe_b64decode(s + pad)
|
|
37
|
+
except Exception as e: # noqa: BLE001
|
|
38
|
+
raise SkillAuthDecryptError(f"cipher Base64URL decode failed: {e}") from e
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _build_padding(sign_method: str):
|
|
42
|
+
sm = (sign_method or "").upper()
|
|
43
|
+
if sm == "SHA512WITHRSA":
|
|
44
|
+
return asym_padding.OAEP(
|
|
45
|
+
mgf=asym_padding.MGF1(algorithm=hashes.SHA512()),
|
|
46
|
+
algorithm=hashes.SHA512(),
|
|
47
|
+
label=None,
|
|
48
|
+
)
|
|
49
|
+
# SHA256withRSA / SHA1withRSA defaults to PKCS1v15, aligned with server-side default
|
|
50
|
+
return asym_padding.PKCS1v15()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def decrypt_payload(
|
|
54
|
+
cipher_b64url: str,
|
|
55
|
+
private_key_pem: bytes,
|
|
56
|
+
sign_method: str = "SHA256withRSA",
|
|
57
|
+
) -> Dict:
|
|
58
|
+
"""RSA chunked decryption of encryptedPayload, returns parsed JSON dict."""
|
|
59
|
+
raw = _b64url_decode(cipher_b64url)
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
sk = serialization.load_pem_private_key(private_key_pem, password=None)
|
|
63
|
+
except Exception as e: # noqa: BLE001
|
|
64
|
+
raise SkillAuthDecryptError(f"Failed to load private key: {e}") from e
|
|
65
|
+
if not isinstance(sk, RSAPrivateKey):
|
|
66
|
+
raise SkillAuthDecryptError("Private key is not RSA type")
|
|
67
|
+
|
|
68
|
+
block_size = sk.key_size // 8 # RSA-2048 → 256
|
|
69
|
+
if len(raw) == 0 or len(raw) % block_size != 0:
|
|
70
|
+
raise SkillAuthDecryptError(
|
|
71
|
+
f"cipher length {len(raw)} is not a multiple of block_size={block_size}"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
pad = _build_padding(sign_method)
|
|
75
|
+
parts = []
|
|
76
|
+
try:
|
|
77
|
+
for i in range(0, len(raw), block_size):
|
|
78
|
+
parts.append(sk.decrypt(raw[i : i + block_size], pad))
|
|
79
|
+
except Exception as e: # noqa: BLE001
|
|
80
|
+
raise SkillAuthDecryptError(f"RSA chunked decryption failed: {e}") from e
|
|
81
|
+
|
|
82
|
+
plaintext = b"".join(parts)
|
|
83
|
+
try:
|
|
84
|
+
return json.loads(plaintext.decode("utf-8"))
|
|
85
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as e:
|
|
86
|
+
raise SkillAuthDecryptError(f"Plaintext is not valid JSON: {e}") from e
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Skill keypair management (consumer side only)
|
|
5
|
+
|
|
6
|
+
The private key is generated by the backend during Skill registration;
|
|
7
|
+
the seller persists it to .keypair.json during initialization.
|
|
8
|
+
This module only reads and normalizes the format (raw Base64 -> PEM).
|
|
9
|
+
|
|
10
|
+
File location: <skill_dir>/.keypair.json
|
|
11
|
+
File format (example):
|
|
12
|
+
{
|
|
13
|
+
"skill_code": "<32-char skillCode>",
|
|
14
|
+
"private_key": "-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----",
|
|
15
|
+
"sign_method": "SHA256withRSA"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
private_key can also be raw Base64 (PKCS#8 single-line); this module auto-wraps it into PEM.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import textwrap
|
|
26
|
+
from typing import Dict
|
|
27
|
+
|
|
28
|
+
import config
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SkillKeypairError(RuntimeError):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _to_pem(private_key_str: str) -> bytes:
|
|
36
|
+
"""Normalize a private key string into PEM bytes.
|
|
37
|
+
|
|
38
|
+
Supports two input formats:
|
|
39
|
+
- Already PEM text (with -----BEGIN/-----END headers);
|
|
40
|
+
- Raw Base64 PKCS#8 single-line (no headers), auto-wrapped into PEM.
|
|
41
|
+
"""
|
|
42
|
+
s = private_key_str.strip()
|
|
43
|
+
if not s:
|
|
44
|
+
raise SkillKeypairError("private_key is empty")
|
|
45
|
+
if "BEGIN" in s and "END" in s:
|
|
46
|
+
# Already PEM; normalize line endings
|
|
47
|
+
return s.replace("\r\n", "\n").encode("utf-8")
|
|
48
|
+
# Raw Base64 -> wrap as PEM; fold at 64 chars
|
|
49
|
+
body = "\n".join(textwrap.wrap(s, 64))
|
|
50
|
+
pem = f"-----BEGIN PRIVATE KEY-----\n{body}\n-----END PRIVATE KEY-----\n"
|
|
51
|
+
return pem.encode("utf-8")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def load_keypair(path: str = None) -> Dict:
|
|
55
|
+
"""Read .keypair.json and return a dict.
|
|
56
|
+
|
|
57
|
+
Returned fields:
|
|
58
|
+
- skill_code: str
|
|
59
|
+
- private_key_pem: bytes (PEM format, can be fed directly to cryptography.serialization.load_pem_private_key)
|
|
60
|
+
- sign_method: str (default SHA256withRSA)
|
|
61
|
+
"""
|
|
62
|
+
path = path or config.KEYPAIR_FILE
|
|
63
|
+
if not os.path.exists(path):
|
|
64
|
+
raise SkillKeypairError(
|
|
65
|
+
f".keypair.json not found: {path}\n"
|
|
66
|
+
f"Please persist the Skill-registered private key and skillCode to this file.\n"
|
|
67
|
+
f"Template: {{\"skill_code\":\"...\",\"private_key\":\"-----BEGIN PRIVATE KEY-----...\",\"sign_method\":\"SHA256withRSA\"}}"
|
|
68
|
+
)
|
|
69
|
+
try:
|
|
70
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
71
|
+
raw = json.load(f)
|
|
72
|
+
except json.JSONDecodeError as e:
|
|
73
|
+
raise SkillKeypairError(f".keypair.json parse failed: {e}") from e
|
|
74
|
+
|
|
75
|
+
skill_code = (raw.get("skill_code") or "").strip()
|
|
76
|
+
private_key = raw.get("private_key") or ""
|
|
77
|
+
if not skill_code:
|
|
78
|
+
raise SkillKeypairError(".keypair.json missing skill_code")
|
|
79
|
+
if not private_key:
|
|
80
|
+
raise SkillKeypairError(".keypair.json missing private_key")
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
"skill_code": skill_code,
|
|
84
|
+
"private_key_pem": _to_pem(private_key),
|
|
85
|
+
"sign_method": (raw.get("sign_method") or "SHA256withRSA").strip(),
|
|
86
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Skill invocation logging module
|
|
5
|
+
|
|
6
|
+
Log file naming: logs/skill-{date}.log
|
|
7
|
+
Appends one JSON line per skill invocation with structured call details.
|
|
8
|
+
|
|
9
|
+
Field descriptions (aligned with backend reporting protocol):
|
|
10
|
+
- SellerID : Current authorized seller ID (from credentials.json, empty if missing)
|
|
11
|
+
- SkillName : Fixed "miravia_order_reports"
|
|
12
|
+
- Action : Invocation scenario/action (export / refund / business / inspect / ping)
|
|
13
|
+
- AgentType : Agent type (empty if not Agent-driven)
|
|
14
|
+
- ModelName : Model name (empty if no LLM invoked)
|
|
15
|
+
- Duration : Human-readable duration (e.g. "12.3s")
|
|
16
|
+
- Status : Invocation result "success" / "fail"
|
|
17
|
+
- ErrorCode : Error code (empty on success)
|
|
18
|
+
- apiCallUrl : API URLs involved in this call (deduplicated)
|
|
19
|
+
- apiCallCount : Total API call count
|
|
20
|
+
- apiSuccessCount : Successful API call count
|
|
21
|
+
- dataCount : Number of data rows fetched
|
|
22
|
+
- durationMs : Duration in milliseconds
|
|
23
|
+
- envData : Environment info object
|
|
24
|
+
- os : Operating system name
|
|
25
|
+
- os_version : Operating system version
|
|
26
|
+
- node_version : Node.js version (empty for Python skill)
|
|
27
|
+
- agent_version : Agent/Skill version
|
|
28
|
+
- locale : System locale
|
|
29
|
+
- remark : Remarks
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
import logging
|
|
34
|
+
import os
|
|
35
|
+
import platform
|
|
36
|
+
import locale
|
|
37
|
+
import time
|
|
38
|
+
from datetime import datetime
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import Any, Dict, List, Optional
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Skill root directory / logs
|
|
44
|
+
_SKILL_DIR = Path(__file__).resolve().parent.parent
|
|
45
|
+
_LOGS_DIR = _SKILL_DIR / "logs"
|
|
46
|
+
|
|
47
|
+
SKILL_NAME = "miravia_order_reports"
|
|
48
|
+
|
|
49
|
+
# Version: read from npm-package/package.json as single source of truth
|
|
50
|
+
def _read_version() -> str:
|
|
51
|
+
"""Read version from npm-package/package.json; fallback to hardcoded if unavailable."""
|
|
52
|
+
try:
|
|
53
|
+
pkg_path = _SKILL_DIR / "npm-package" / "package.json"
|
|
54
|
+
if pkg_path.exists():
|
|
55
|
+
data = json.loads(pkg_path.read_text(encoding="utf-8"))
|
|
56
|
+
return str(data.get("version", "0.0.0"))
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
return "0.0.0"
|
|
60
|
+
|
|
61
|
+
SKILL_VERSION = _read_version()
|
|
62
|
+
|
|
63
|
+
# ---- agent_type global state (CLI required input, no auto-detection) ----
|
|
64
|
+
_agent_type: str = ""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def set_agent_type(agent_type: str) -> None:
|
|
68
|
+
"""Called by CLI entry at startup to set global agent_type.
|
|
69
|
+
|
|
70
|
+
agent_type must be explicitly passed by the calling Agent via --agent-type CLI parameter;
|
|
71
|
+
no auto-detection or inference is performed.
|
|
72
|
+
"""
|
|
73
|
+
global _agent_type
|
|
74
|
+
_agent_type = agent_type.strip()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_agent_type() -> str:
|
|
78
|
+
"""Get current global agent_type.
|
|
79
|
+
|
|
80
|
+
Returns empty string if set_agent_type() has not been called.
|
|
81
|
+
"""
|
|
82
|
+
return _agent_type
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def detect_model_name() -> str:
|
|
86
|
+
"""Detect the LLM model name used by the current Agent.
|
|
87
|
+
|
|
88
|
+
Priority:
|
|
89
|
+
1. Environment variable SKILL_MODEL_NAME (explicit override, highest priority)
|
|
90
|
+
2. Match model env var by AgentType:
|
|
91
|
+
- claude -> ANTHROPIC_MODEL
|
|
92
|
+
- qoder -> QODER_MODEL (not exposed yet, empty)
|
|
93
|
+
- cursor -> CURSOR_MODEL
|
|
94
|
+
3. Fallback to empty string (undetectable)
|
|
95
|
+
"""
|
|
96
|
+
# 1. Explicit override
|
|
97
|
+
explicit = os.environ.get("SKILL_MODEL_NAME", "").strip()
|
|
98
|
+
if explicit:
|
|
99
|
+
return explicit
|
|
100
|
+
|
|
101
|
+
agent = get_agent_type()
|
|
102
|
+
|
|
103
|
+
# 2. Claude Code: ANTHROPIC_MODEL is its standard config
|
|
104
|
+
if agent == "claude":
|
|
105
|
+
val = os.environ.get("ANTHROPIC_MODEL", "").strip()
|
|
106
|
+
if val:
|
|
107
|
+
return val
|
|
108
|
+
|
|
109
|
+
# 3. Qoder: does not expose model name env var yet; expects QODER_MODEL later
|
|
110
|
+
if agent == "qoder":
|
|
111
|
+
val = os.environ.get("QODER_MODEL", "").strip()
|
|
112
|
+
if val:
|
|
113
|
+
return val
|
|
114
|
+
|
|
115
|
+
# 4. Cursor
|
|
116
|
+
if agent == "cursor":
|
|
117
|
+
for key in ("CURSOR_MODEL", "OPENAI_MODEL"):
|
|
118
|
+
val = os.environ.get(key, "").strip()
|
|
119
|
+
if val:
|
|
120
|
+
return val
|
|
121
|
+
|
|
122
|
+
return ""
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ApiCallTracker:
|
|
126
|
+
"""Track API call count and results for final log entry."""
|
|
127
|
+
|
|
128
|
+
def __init__(self):
|
|
129
|
+
self.call_count: int = 0
|
|
130
|
+
self.success_count: int = 0
|
|
131
|
+
self.urls: List[str] = []
|
|
132
|
+
self._url_set: set = set()
|
|
133
|
+
|
|
134
|
+
def record(self, url: str, success: bool) -> None:
|
|
135
|
+
self.call_count += 1
|
|
136
|
+
if success:
|
|
137
|
+
self.success_count += 1
|
|
138
|
+
if url not in self._url_set:
|
|
139
|
+
self._url_set.add(url)
|
|
140
|
+
self.urls.append(url)
|
|
141
|
+
|
|
142
|
+
def reset(self) -> None:
|
|
143
|
+
self.call_count = 0
|
|
144
|
+
self.success_count = 0
|
|
145
|
+
self.urls.clear()
|
|
146
|
+
self._url_set.clear()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# Global tracker instance (referenced by miravia_client.py)
|
|
150
|
+
api_tracker = ApiCallTracker()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _get_seller_id() -> str:
|
|
154
|
+
"""Read seller_id from credentials.json if available, otherwise return empty string."""
|
|
155
|
+
cred_file = _SKILL_DIR / "credentials.json"
|
|
156
|
+
if not cred_file.exists():
|
|
157
|
+
return ""
|
|
158
|
+
try:
|
|
159
|
+
data = json.loads(cred_file.read_text(encoding="utf-8"))
|
|
160
|
+
return str(data.get("seller_id", "") or "")
|
|
161
|
+
except (OSError, json.JSONDecodeError):
|
|
162
|
+
return ""
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _get_env_data() -> Dict[str, str]:
|
|
166
|
+
"""Collect runtime environment info."""
|
|
167
|
+
sys_name = platform.system() # e.g. "Darwin", "Linux", "Windows"
|
|
168
|
+
sys_version = platform.release() # e.g. "26.3.1"
|
|
169
|
+
try:
|
|
170
|
+
sys_locale = locale.getdefaultlocale()[0] or ""
|
|
171
|
+
except Exception:
|
|
172
|
+
sys_locale = ""
|
|
173
|
+
return {
|
|
174
|
+
"os": sys_name,
|
|
175
|
+
"os_version": sys_version,
|
|
176
|
+
"node_version": "", # Python skill, not applicable
|
|
177
|
+
"agent_version": SKILL_VERSION,
|
|
178
|
+
"locale": sys_locale,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# Log retention days
|
|
183
|
+
_LOG_RETENTION_DAYS = 5
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _log_file_path() -> Path:
|
|
187
|
+
"""Return today's log file path: logs/skill-{YYYY-MM-DD}.log"""
|
|
188
|
+
_LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
|
189
|
+
date_str = datetime.now().strftime("%Y-%m-%d")
|
|
190
|
+
return _LOGS_DIR / f"skill-{date_str}.log"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _cleanup_old_logs() -> None:
|
|
194
|
+
"""Delete skill log files older than _LOG_RETENTION_DAYS days."""
|
|
195
|
+
if not _LOGS_DIR.exists():
|
|
196
|
+
return
|
|
197
|
+
from datetime import timedelta
|
|
198
|
+
cutoff = datetime.now() - timedelta(days=_LOG_RETENTION_DAYS)
|
|
199
|
+
for f in _LOGS_DIR.glob("skill-*.log"):
|
|
200
|
+
# Parse date from filename: skill-YYYY-MM-DD.log
|
|
201
|
+
try:
|
|
202
|
+
date_part = f.stem.replace("skill-", "")
|
|
203
|
+
file_date = datetime.strptime(date_part, "%Y-%m-%d")
|
|
204
|
+
if file_date < cutoff:
|
|
205
|
+
f.unlink(missing_ok=True)
|
|
206
|
+
except (ValueError, OSError):
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def write_skill_log(
|
|
211
|
+
action: str,
|
|
212
|
+
status: str,
|
|
213
|
+
duration_ms: int,
|
|
214
|
+
data_count: int = 0,
|
|
215
|
+
error_code: str = "",
|
|
216
|
+
remark: str = "",
|
|
217
|
+
seller_id: Optional[str] = None,
|
|
218
|
+
) -> None:
|
|
219
|
+
"""Write one skill invocation log entry to logs/skill-{date}.log.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
action: Scenario/action name (export / refund / business / inspect / ping)
|
|
223
|
+
status: "success" / "fail"
|
|
224
|
+
duration_ms: Duration in milliseconds
|
|
225
|
+
data_count: Number of data rows fetched
|
|
226
|
+
error_code: Error code (empty on success)
|
|
227
|
+
remark: Remarks
|
|
228
|
+
seller_id: Optional seller_id override
|
|
229
|
+
"""
|
|
230
|
+
record = {
|
|
231
|
+
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3],
|
|
232
|
+
"SellerID": seller_id if seller_id is not None else _get_seller_id(),
|
|
233
|
+
"SkillName": SKILL_NAME,
|
|
234
|
+
"Action": action,
|
|
235
|
+
"AgentType": get_agent_type(),
|
|
236
|
+
"ModelName": detect_model_name(),
|
|
237
|
+
"Duration": f"{duration_ms / 1000:.1f}s" if duration_ms >= 1000 else f"{duration_ms}ms",
|
|
238
|
+
"Status": status,
|
|
239
|
+
"ErrorCode": error_code,
|
|
240
|
+
"apiCallUrl": api_tracker.urls,
|
|
241
|
+
"apiCallCount": api_tracker.call_count,
|
|
242
|
+
"apiSuccessCount": api_tracker.success_count,
|
|
243
|
+
"dataCount": data_count,
|
|
244
|
+
"durationMs": duration_ms,
|
|
245
|
+
"envData": _get_env_data(),
|
|
246
|
+
"remark": remark,
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
log_path = _log_file_path()
|
|
250
|
+
try:
|
|
251
|
+
with open(log_path, "a", encoding="utf-8") as f:
|
|
252
|
+
f.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
253
|
+
except OSError as e:
|
|
254
|
+
logging.getLogger("miravia.skill_logger").warning(
|
|
255
|
+
"Failed to write skill log: %s", e
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Attempt to clean up expired logs after each write
|
|
259
|
+
try:
|
|
260
|
+
_cleanup_old_logs()
|
|
261
|
+
except Exception:
|
|
262
|
+
pass
|