chore: 更新公共镜像至 1.5.43
This commit is contained in:
@@ -1,5 +1,15 @@
|
|||||||
# oracle-jump-query Skill Release Standard
|
# oracle-jump-query Skill Release Standard
|
||||||
|
|
||||||
|
## Git 提交说明
|
||||||
|
|
||||||
|
- 提交信息和发布记录使用简洁、准确的中文。
|
||||||
|
- 按可独立验证的小功能节点分别提交,不混入无关修改。
|
||||||
|
- Bug 修复通常只说明修复的功能点和结果,不展开代码缺陷细节;排障或防复发所必需的信息除外。
|
||||||
|
|
||||||
|
Release 1.5.43: 统一 Git 提交与发布记录规范,明确使用简洁中文、按小功能节点提交,并精简 Bug 修复说明。提交 `176bd4594779a8489ad25ab828380b2b7ab3354b` 已于 2026-08-27 10:18 +08:00 推送到私有 Gitea,并同步安装目录;`capabilities --json` 验证版本为 `1.5.43`。
|
||||||
|
|
||||||
|
Release 1.5.42: added `log_info`, `log_tail`, `log_search`, `log_enable`, and `log_disable`; uses authenticated dedicated transit endpoints and preserves structured log errors. Commit `e90cbc9ce3525970b2f07885d52c4f0172bf9d2f` was pushed to private Gitea at 2026-08-27 09:51 +08:00, pulled into the installed Skill, and verified by `capabilities --json` as 1.5.42. Log content is not stored in Skill config, output archives, or release history.
|
||||||
|
|
||||||
A published `VERSION` is frozen. Increment it for every later change, update `CHANGELOG.md`, commit and push to Gitea. Skill updates use Git only: pull the commit into the Codex Skill directory with `git pull --ff-only origin main`, then run `capabilities --json`. Do not publish or update this Skill through OSS.
|
A published `VERSION` is frozen. Increment it for every later change, update `CHANGELOG.md`, commit and push to Gitea. Skill updates use Git only: pull the commit into the Codex Skill directory with `git pull --ff-only origin main`, then run `capabilities --json`. Do not publish or update this Skill through OSS.
|
||||||
|
|
||||||
`inspection_report` and `inspection_latest --refresh` must run `sys_function_check` before Agent collection. Missing SYS/current-schema functions stop generation and return the complete SQL bundle. Historical report reads do not run this preflight.
|
`inspection_report` and `inspection_latest --refresh` must run `sys_function_check` before Agent collection. Missing SYS/current-schema functions stop generation and return the complete SQL bundle. Historical report reads do not run this preflight.
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
|
## 1.5.43 (2026-08-27)
|
||||||
|
|
||||||
|
- 统一 Git 提交与发布记录规范:使用简洁、准确的中文,通常按小功能节点分别提交。
|
||||||
|
- Bug 修复通常只描述修复的功能点和结果,不展开代码缺陷细节。
|
||||||
|
|
||||||
|
## 1.5.42 (2026-08-27)
|
||||||
|
|
||||||
|
- Added authenticated `log_info`, `log_tail`, `log_search`, `log_enable`, and `log_disable` commands and capability definitions.
|
||||||
|
- Added dedicated transit endpoint calls, paths containing spaces, keyword/regex context search, recent/full scan selection, and structured error handling.
|
||||||
|
- Log excerpts remain read-only and are analyzed by the Skill/model; the Skill does not execute server commands or write customer files.
|
||||||
|
|
||||||
## 1.5.37 (2026-08-14)
|
## 1.5.37 (2026-08-14)
|
||||||
|
|
||||||
### Public mirror documentation
|
### Public mirror documentation
|
||||||
|
|||||||
@@ -101,6 +101,10 @@ python oracle_skill.py perm 940 12983
|
|||||||
| `inspection_latest [clientCode] [--refresh] [--json] [--html [path]] [--markdown [path]]` | 查看最近归档巡检报告并默认导出 HTML;加 `--refresh` 时重新生成 | `inspection_latest HENLO --refresh --markdown` |
|
| `inspection_latest [clientCode] [--refresh] [--json] [--html [path]] [--markdown [path]]` | 查看最近归档巡检报告并默认导出 HTML;加 `--refresh` 时重新生成 | `inspection_latest HENLO --refresh --markdown` |
|
||||||
| `inspection_get <id> [--json] [--html [path]] [--markdown [path]]` | 按归档 ID 查看历史巡检报告并默认导出 HTML | `inspection_get 123 --markdown` |
|
| `inspection_get <id> [--json] [--html [path]] [--markdown [path]]` | 按归档 ID 查看历史巡检报告并默认导出 HTML | `inspection_get 123 --markdown` |
|
||||||
| `agent_update [clientCode] [timeout]` | 触发客户 Agent 自动更新,升级包由 Agent 从 OSS 下载 | `agent_update HENLO 300` |
|
| `agent_update [clientCode] [timeout]` | 触发客户 Agent 自动更新,升级包由 Agent 从 OSS 下载 | `agent_update HENLO 300` |
|
||||||
|
| `log_info <path> [--client code]` | 验证服务器绝对日志路径和文件元数据 | `log_info "D:\logs\app.log" --client AHMW --json` |
|
||||||
|
| `log_tail <path> [--client code]` | 有界读取日志末尾内容 | `log_tail "D:\logs\app.log" --client AHMW --lines 200` |
|
||||||
|
| `log_search <path> <pattern...>` | 在 Agent 本机按关键词或正则流式搜索日志 | `log_search "D:\logs\app.log" ERROR --client AHMW --json` |
|
||||||
|
| `log_enable [clientCode]` / `log_disable [clientCode]` | 持久化开启或关闭目标客户日志分析 | `log_enable AHMW` |
|
||||||
| `discover [schema] [filter]` | 发现核心业务表 | `discover bosnds3 M_%` |
|
| `discover [schema] [filter]` | 发现核心业务表 | `discover bosnds3 M_%` |
|
||||||
| `nl2sql <schema> <question>` | 自然语言转 SQL | `nl2sql bosnds3 查询花都二店5月销售额` |
|
| `nl2sql <schema> <question>` | 自然语言转 SQL | `nl2sql bosnds3 查询花都二店5月销售额` |
|
||||||
| `servers` | 列出在线 Agent | `servers` |
|
| `servers` | 列出在线 Agent | `servers` |
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ description: Oracle 跳板查询技能。通过中转服务查询远程 Oracle
|
|||||||
|
|
||||||
# Oracle 跳板查询
|
# Oracle 跳板查询
|
||||||
|
|
||||||
> **版本:v1.5.37** · [更新日志](./CHANGELOG.md)
|
> **版本:v1.5.43** · [更新日志](./CHANGELOG.md)
|
||||||
|
|
||||||
## 使用原则
|
## 使用原则
|
||||||
|
|
||||||
@@ -78,6 +78,10 @@ python scripts/oracle_skill.py login <secretKey> [clientCode]
|
|||||||
| `awr_status [clientCode] [--json]` | 查看 AWR 授权确认、权限、快照和最近生成状态,不触发生成 |
|
| `awr_status [clientCode] [--json]` | 查看 AWR 授权确认、权限、快照和最近生成状态,不触发生成 |
|
||||||
| `awr_list [clientCode] [--json]` | 列出 Agent 已生成并保留的 AWR 报告 |
|
| `awr_list [clientCode] [--json]` | 列出 Agent 已生成并保留的 AWR 报告 |
|
||||||
| `awr_download <clientCode> <yyyyMMdd> [--output path]` | 经中转机下载 Agent 原始 AWR HTML,不在本地渲染 |
|
| `awr_download <clientCode> <yyyyMMdd> [--output path]` | 经中转机下载 Agent 原始 AWR HTML,不在本地渲染 |
|
||||||
|
| `log_info <path> [--client code] [--json]` | 验证服务器绝对日志路径并返回文件元数据 |
|
||||||
|
| `log_tail <path> [--client code] [--lines N] [--json]` | 有界读取服务器日志末尾内容 |
|
||||||
|
| `log_search <path> <pattern...> [--client code] [--regex] [--scope recent|full] [--json]` | 在 Agent 本机流式筛选日志并返回有限上下文 |
|
||||||
|
| `log_enable [clientCode]` / `log_disable [clientCode]` | 为有权限的客户持久化开启或关闭日志分析 |
|
||||||
| `agent_update [clientCode] [timeout]` | 触发客户 Agent 自动更新;由中转机下发更新动作,Agent 按 OSS latest.json 返回的包地址下载升级 |
|
| `agent_update [clientCode] [timeout]` | 触发客户 Agent 自动更新;由中转机下发更新动作,Agent 按 OSS latest.json 返回的包地址下载升级 |
|
||||||
|
|
||||||
## BOS 分析优先路径
|
## BOS 分析优先路径
|
||||||
@@ -162,8 +166,18 @@ python scripts/oracle_skill.py awr_list WEIRUI
|
|||||||
python scripts/oracle_skill.py awr_download WEIRUI 20260720
|
python scripts/oracle_skill.py awr_download WEIRUI 20260720
|
||||||
```
|
```
|
||||||
|
|
||||||
|
日志分析默认关闭。用户提供服务器上的单个绝对文本日志路径后,先执行 `log_info`,再按问题选择 `log_tail` 或 `log_search`;默认只扫描末尾 100 MB。日志正文原样返回给当前 Skill 会话但不写入中转审计库:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python scripts/oracle_skill.py log_enable AHMW
|
||||||
|
python scripts/oracle_skill.py log_info "D:\logs\app.log" --client AHMW --json
|
||||||
|
python scripts/oracle_skill.py log_search "D:\logs\app.log" ERROR ORA- --client AHMW --before 3 --after 3 --json
|
||||||
|
```
|
||||||
|
|
||||||
## 内容维护
|
## 内容维护
|
||||||
|
|
||||||
|
Git 提交和发布记录使用简洁、准确的中文描述,并按可独立验证的小功能节点分别提交。Bug 修复通常只说明修复的功能点和用户可见结果,不展开代码层缺陷细节;只有排障或防止复发确有需要时才补充技术原因。每次提交避免混入无关修改。
|
||||||
|
|
||||||
本 skill 的 Git 源地址分为私有主库和公开镜像:
|
本 skill 的 Git 源地址分为私有主库和公开镜像:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
@@ -73,6 +73,10 @@ python scripts/oracle_skill.py <command> [args]
|
|||||||
| `awr_status [clientCode]` | 查看 AWR 授权和可用状态,不生成报告 | `python oracle_skill.py awr_status WEIRUI` |
|
| `awr_status [clientCode]` | 查看 AWR 授权和可用状态,不生成报告 | `python oracle_skill.py awr_status WEIRUI` |
|
||||||
| `awr_list [clientCode]` | 列出 Agent 已生成的 AWR 报告 | `python oracle_skill.py awr_list WEIRUI` |
|
| `awr_list [clientCode]` | 列出 Agent 已生成的 AWR 报告 | `python oracle_skill.py awr_list WEIRUI` |
|
||||||
| `awr_download <clientCode> <yyyyMMdd>` | 下载 Agent 原始 AWR HTML | `python oracle_skill.py awr_download WEIRUI 20260720` |
|
| `awr_download <clientCode> <yyyyMMdd>` | 下载 Agent 原始 AWR HTML | `python oracle_skill.py awr_download WEIRUI 20260720` |
|
||||||
|
| `log_info <path> [--client code]` | 验证单个绝对日志路径和文件元数据 | `python oracle_skill.py log_info "D:\logs\app.log" --client AHMW --json` |
|
||||||
|
| `log_tail <path> [--client code]` | 有界读取日志末尾内容 | `python oracle_skill.py log_tail "D:\logs\app.log" --client AHMW --lines 200` |
|
||||||
|
| `log_search <path> <pattern...>` | 关键词/RE2 正则流式搜索并返回有限上下文 | `python oracle_skill.py log_search "D:\logs\app.log" ERROR --client AHMW --json` |
|
||||||
|
| `log_enable [clientCode]` / `log_disable [clientCode]` | 开启或关闭目标客户日志分析 | `python oracle_skill.py log_enable AHMW` |
|
||||||
| `servers` | 列出在线 Agent | `python oracle_skill.py servers` |
|
| `servers` | 列出在线 Agent | `python oracle_skill.py servers` |
|
||||||
| `health` | 检查中转服务状态 | `python oracle_skill.py health` |
|
| `health` | 检查中转服务状态 | `python oracle_skill.py health` |
|
||||||
| `tablespace / tablespaces` | 查询表空间使用情况 | `python oracle_skill.py tablespace` |
|
| `tablespace / tablespaces` | 查询表空间使用情况 | `python oracle_skill.py tablespace` |
|
||||||
@@ -139,6 +143,10 @@ curl -X POST https://ts.henlo.net/api/query \
|
|||||||
| `execute_query` | 执行 SELECT 查询(只读)—— 表空间查询也复用此 action |
|
| `execute_query` | 执行 SELECT 查询(只读)—— 表空间查询也复用此 action |
|
||||||
| `schema_discovery` | 发现核心业务域 |
|
| `schema_discovery` | 发现核心业务域 |
|
||||||
| `generate_nl2sql_schema` | 生成 NL2SQL Schema 字典 |
|
| `generate_nl2sql_schema` | 生成 NL2SQL Schema 字典 |
|
||||||
|
| `log_info` | 本机日志文件元数据 |
|
||||||
|
| `log_tail` | 本机日志有界尾部读取 |
|
||||||
|
| `log_search` | 本机日志有界流式筛选 |
|
||||||
|
| `log_config_set` | 只修改日志分析启用开关 |
|
||||||
|
|
||||||
|
|
||||||
## 故障排查
|
## 故障排查
|
||||||
|
|||||||
+164
-1
@@ -978,6 +978,83 @@ def cmd_awr_download(client_code: str, date: str, output_path: str = "", as_json
|
|||||||
print(f"Size: {result['size']} bytes")
|
print(f"Size: {result['size']} bytes")
|
||||||
|
|
||||||
|
|
||||||
|
def _log_client_code(client_code: str = "") -> str:
|
||||||
|
cfg = get_config() or {}
|
||||||
|
return (client_code or cfg.get("client_code") or cfg.get("server_id") or "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _log_api(endpoint: str, payload: Dict[str, Any], timeout: int = 120) -> Dict[str, Any]:
|
||||||
|
cfg = get_config() or {}
|
||||||
|
if not ensure_logged_in(cfg):
|
||||||
|
return {"success": False, "error": "login required", "error_code": "login_required"}
|
||||||
|
client_code = _log_client_code(str(payload.get("client_code") or ""))
|
||||||
|
if not client_code:
|
||||||
|
return {"success": False, "error": "clientCode is required", "error_code": "invalid_request"}
|
||||||
|
body = dict(payload)
|
||||||
|
body["client_code"] = client_code
|
||||||
|
body["timeout"] = max(1, min(int(timeout or 120), 300))
|
||||||
|
try:
|
||||||
|
resp = requests.post(
|
||||||
|
f"{str(cfg.get('transit_url') or TRANSIT_URL).rstrip('/')}/api/log/{endpoint}",
|
||||||
|
json=body,
|
||||||
|
headers=make_headers(),
|
||||||
|
timeout=body["timeout"] + 5,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
result = resp.json()
|
||||||
|
except Exception:
|
||||||
|
result = {"success": False, "error": resp.text or f"HTTP {resp.status_code}"}
|
||||||
|
if not resp.ok:
|
||||||
|
result.setdefault("success", False)
|
||||||
|
result.setdefault("status_code", resp.status_code)
|
||||||
|
return result
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return {"success": False, "error": str(e), "error_code": "request_failed"}
|
||||||
|
|
||||||
|
|
||||||
|
def log_info(path: str, client_code: str = "", timeout: int = 120) -> Dict[str, Any]:
|
||||||
|
return _log_api("info", {"client_code": client_code, "path": path}, timeout)
|
||||||
|
|
||||||
|
|
||||||
|
def log_tail(path: str, client_code: str = "", lines: int = 200,
|
||||||
|
max_bytes: int = 256 * 1024, timeout: int = 120) -> Dict[str, Any]:
|
||||||
|
return _log_api("tail", {
|
||||||
|
"client_code": client_code, "path": path, "lines": lines, "max_bytes": max_bytes,
|
||||||
|
}, timeout)
|
||||||
|
|
||||||
|
|
||||||
|
def log_search(path: str, patterns: List[str], client_code: str = "", regex: bool = False,
|
||||||
|
case_sensitive: bool = False, before_lines: int = 3, after_lines: int = 3,
|
||||||
|
max_matches: int = 100, scan_scope: str = "recent",
|
||||||
|
recent_bytes: int = 100 * 1024 * 1024, timeout: int = 120) -> Dict[str, Any]:
|
||||||
|
return _log_api("search", {
|
||||||
|
"client_code": client_code, "path": path, "patterns": patterns, "regex": regex,
|
||||||
|
"case_sensitive": case_sensitive, "before_lines": before_lines, "after_lines": after_lines,
|
||||||
|
"max_matches": max_matches, "scan_scope": scan_scope, "recent_bytes": recent_bytes,
|
||||||
|
}, timeout)
|
||||||
|
|
||||||
|
|
||||||
|
def set_log_analysis_enabled(enabled: bool, client_code: str = "", timeout: int = 30) -> Dict[str, Any]:
|
||||||
|
return _log_api("config", {"client_code": client_code, "enabled": bool(enabled)}, timeout)
|
||||||
|
|
||||||
|
|
||||||
|
def _print_log_result(result: Dict[str, Any], as_json: bool = False):
|
||||||
|
if as_json or not result.get("success"):
|
||||||
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||||
|
return
|
||||||
|
data = result.get("data") or {}
|
||||||
|
if isinstance(data.get("lines"), list):
|
||||||
|
for entry in data["lines"]:
|
||||||
|
if isinstance(entry, dict):
|
||||||
|
marker = ">" if entry.get("match") else " "
|
||||||
|
print(f"{marker} {entry.get('scan_line', '-')}: {entry.get('text', '')}")
|
||||||
|
else:
|
||||||
|
print(entry)
|
||||||
|
print(f"matches={data.get('matches', '-')} complete={data.get('complete', True)} stop_reason={data.get('stop_reason', '')}")
|
||||||
|
else:
|
||||||
|
print(json.dumps(data, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
|
||||||
def _inspection_report_payload(result: Dict[str, Any]) -> Dict[str, Any]:
|
def _inspection_report_payload(result: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
data = result.get("data") or {}
|
data = result.get("data") or {}
|
||||||
if isinstance(data, dict) and data.get("report_json"):
|
if isinstance(data, dict) and data.get("report_json"):
|
||||||
@@ -3351,6 +3428,45 @@ def cmd_capabilities(args):
|
|||||||
"required": []
|
"required": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "log_info",
|
||||||
|
"description": "Validate one absolute server log path and return bounded file metadata without Oracle access.",
|
||||||
|
"parameters": {"type": "object", "properties": {
|
||||||
|
"path": {"type": "string", "description": "Absolute log file path on the target server."},
|
||||||
|
"clientCode": {"type": "string", "description": "Optional client/server code."}
|
||||||
|
}, "required": ["path"]}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_tail",
|
||||||
|
"description": "Read a bounded tail from one server log file.",
|
||||||
|
"parameters": {"type": "object", "properties": {
|
||||||
|
"path": {"type": "string"}, "clientCode": {"type": "string"},
|
||||||
|
"lines": {"type": "integer", "description": "Default 200, maximum 2000."},
|
||||||
|
"maxBytes": {"type": "integer", "description": "Default 262144, maximum 1048576."}
|
||||||
|
}, "required": ["path"]}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_search",
|
||||||
|
"description": "Stream-search a server log with keywords or RE2 regular expressions and bounded context.",
|
||||||
|
"parameters": {"type": "object", "properties": {
|
||||||
|
"path": {"type": "string"}, "patterns": {"type": "array", "items": {"type": "string"}},
|
||||||
|
"clientCode": {"type": "string"}, "regex": {"type": "boolean"},
|
||||||
|
"caseSensitive": {"type": "boolean"}, "beforeLines": {"type": "integer"},
|
||||||
|
"afterLines": {"type": "integer"}, "maxMatches": {"type": "integer"},
|
||||||
|
"scanScope": {"type": "string", "enum": ["recent", "full"]},
|
||||||
|
"recentBytes": {"type": "integer"}, "timeout": {"type": "integer"}
|
||||||
|
}, "required": ["path", "patterns"]}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_enable",
|
||||||
|
"description": "Enable persisted log analysis for an authorized client.",
|
||||||
|
"parameters": {"type": "object", "properties": {"clientCode": {"type": "string"}}, "required": []}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_disable",
|
||||||
|
"description": "Disable persisted log analysis for an authorized client.",
|
||||||
|
"parameters": {"type": "object", "properties": {"clientCode": {"type": "string"}}, "required": []}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "awr_list",
|
"name": "awr_list",
|
||||||
"description": "List AWR reports already generated and stored by the target Agent.",
|
"description": "List AWR reports already generated and stored by the target Agent.",
|
||||||
@@ -3419,7 +3535,7 @@ def cmd_capabilities(args):
|
|||||||
"analyze", "list", "source", "describe",
|
"analyze", "list", "source", "describe",
|
||||||
"search", "query", "discover", "nl2sql",
|
"search", "query", "discover", "nl2sql",
|
||||||
"perm", "qperm", "login", "logout",
|
"perm", "qperm", "login", "logout",
|
||||||
"status", "switch", "clients", "device_register", "device_login", "ops", "ops_report", "inspection_report", "inspection_latest", "inspection_get", "awr_status", "awr_list", "awr_download", "agent_update", "capabilities"
|
"status", "switch", "clients", "device_register", "device_login", "ops", "ops_report", "inspection_report", "inspection_latest", "inspection_get", "awr_status", "awr_list", "awr_download", "log_info", "log_tail", "log_search", "log_enable", "log_disable", "agent_update", "capabilities"
|
||||||
],
|
],
|
||||||
"python_version": "3.6+",
|
"python_version": "3.6+",
|
||||||
"dependencies": ["requests"]
|
"dependencies": ["requests"]
|
||||||
@@ -3612,6 +3728,53 @@ def main():
|
|||||||
output_path = sys.argv[output_index + 1]
|
output_path = sys.argv[output_index + 1]
|
||||||
cmd_awr_download(sys.argv[2], sys.argv[3], output_path=output_path, as_json="--json" in sys.argv[4:])
|
cmd_awr_download(sys.argv[2], sys.argv[3], output_path=output_path, as_json="--json" in sys.argv[4:])
|
||||||
|
|
||||||
|
elif cmd == "log_info":
|
||||||
|
parser = argparse.ArgumentParser(prog="oracle_skill.py log_info")
|
||||||
|
parser.add_argument("path")
|
||||||
|
parser.add_argument("--client", default="")
|
||||||
|
parser.add_argument("--timeout", type=int, default=120)
|
||||||
|
parser.add_argument("--json", action="store_true")
|
||||||
|
args = parser.parse_args(sys.argv[2:])
|
||||||
|
_print_log_result(log_info(args.path, args.client, args.timeout), args.json)
|
||||||
|
|
||||||
|
elif cmd == "log_tail":
|
||||||
|
parser = argparse.ArgumentParser(prog="oracle_skill.py log_tail")
|
||||||
|
parser.add_argument("path")
|
||||||
|
parser.add_argument("--client", default="")
|
||||||
|
parser.add_argument("--lines", type=int, default=200)
|
||||||
|
parser.add_argument("--max-bytes", type=int, default=256 * 1024)
|
||||||
|
parser.add_argument("--timeout", type=int, default=120)
|
||||||
|
parser.add_argument("--json", action="store_true")
|
||||||
|
args = parser.parse_args(sys.argv[2:])
|
||||||
|
_print_log_result(log_tail(args.path, args.client, args.lines, args.max_bytes, args.timeout), args.json)
|
||||||
|
|
||||||
|
elif cmd == "log_search":
|
||||||
|
parser = argparse.ArgumentParser(prog="oracle_skill.py log_search")
|
||||||
|
parser.add_argument("path")
|
||||||
|
parser.add_argument("patterns", nargs="+")
|
||||||
|
parser.add_argument("--client", default="")
|
||||||
|
parser.add_argument("--regex", action="store_true")
|
||||||
|
parser.add_argument("--case-sensitive", action="store_true")
|
||||||
|
parser.add_argument("--before", type=int, default=3)
|
||||||
|
parser.add_argument("--after", type=int, default=3)
|
||||||
|
parser.add_argument("--max-matches", type=int, default=100)
|
||||||
|
parser.add_argument("--scope", choices=("recent", "full"), default="recent")
|
||||||
|
parser.add_argument("--recent-bytes", type=int, default=100 * 1024 * 1024)
|
||||||
|
parser.add_argument("--timeout", type=int, default=120)
|
||||||
|
parser.add_argument("--json", action="store_true")
|
||||||
|
args = parser.parse_args(sys.argv[2:])
|
||||||
|
_print_log_result(log_search(
|
||||||
|
args.path, args.patterns, args.client, args.regex, args.case_sensitive,
|
||||||
|
args.before, args.after, args.max_matches, args.scope, args.recent_bytes, args.timeout,
|
||||||
|
), args.json)
|
||||||
|
|
||||||
|
elif cmd in ("log_enable", "log_disable"):
|
||||||
|
parser = argparse.ArgumentParser(prog=f"oracle_skill.py {cmd}")
|
||||||
|
parser.add_argument("client", nargs="?", default="")
|
||||||
|
parser.add_argument("--json", action="store_true")
|
||||||
|
args = parser.parse_args(sys.argv[2:])
|
||||||
|
_print_log_result(set_log_analysis_enabled(cmd == "log_enable", args.client), args.json)
|
||||||
|
|
||||||
elif cmd == "agent_update":
|
elif cmd == "agent_update":
|
||||||
timeout = int(sys.argv[3]) if len(sys.argv) >= 4 and sys.argv[3].isdigit() else 300
|
timeout = int(sys.argv[3]) if len(sys.argv) >= 4 and sys.argv[3].isdigit() else 300
|
||||||
cmd_agent_update(sys.argv[2] if len(sys.argv) >= 3 else "", timeout)
|
cmd_agent_update(sys.argv[2] if len(sys.argv) >= 3 else "", timeout)
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import io
|
||||||
|
import json
|
||||||
|
import unittest
|
||||||
|
from contextlib import redirect_stdout
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from scripts import oracle_skill as skill
|
||||||
|
|
||||||
|
|
||||||
|
class FakeResponse:
|
||||||
|
def __init__(self, payload, ok=True, status_code=200):
|
||||||
|
self._payload = payload
|
||||||
|
self.ok = ok
|
||||||
|
self.status_code = status_code
|
||||||
|
self.text = ""
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return self._payload
|
||||||
|
|
||||||
|
|
||||||
|
class LogAnalysisSkillTests(unittest.TestCase):
|
||||||
|
def config(self):
|
||||||
|
return {
|
||||||
|
"access_token": "token",
|
||||||
|
"expires_at": "2999-01-01T00:00:00+08:00",
|
||||||
|
"transit_url": "http://transit.example",
|
||||||
|
"client_code": "AHMW",
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_log_info_preserves_path_with_spaces(self):
|
||||||
|
response = FakeResponse({"success": True, "data": {"size": 12}})
|
||||||
|
path = r"D:\Program Files\BOS\logs\app log.txt"
|
||||||
|
with mock.patch.object(skill, "get_config", return_value=self.config()), mock.patch.object(
|
||||||
|
skill.requests, "post", return_value=response
|
||||||
|
) as post:
|
||||||
|
result = skill.log_info(path, "AHMW")
|
||||||
|
self.assertTrue(result["success"])
|
||||||
|
self.assertEqual(post.call_args.args[0], "http://transit.example/api/log/info")
|
||||||
|
self.assertEqual(post.call_args.kwargs["json"]["path"], path)
|
||||||
|
self.assertEqual(post.call_args.kwargs["json"]["client_code"], "AHMW")
|
||||||
|
self.assertEqual(post.call_args.kwargs["headers"]["Authorization"], "Bearer token")
|
||||||
|
|
||||||
|
def test_log_search_forwards_filters_and_full_scope(self):
|
||||||
|
response = FakeResponse({"success": True, "data": {"matches": 1, "lines": []}})
|
||||||
|
with mock.patch.object(skill, "get_config", return_value=self.config()), mock.patch.object(
|
||||||
|
skill.requests, "post", return_value=response
|
||||||
|
) as post:
|
||||||
|
result = skill.log_search(
|
||||||
|
r"D:\logs\app.log", ["ERROR", r"ORA-\d+"], "AHMW", regex=True,
|
||||||
|
case_sensitive=True, before_lines=5, after_lines=7, max_matches=20,
|
||||||
|
scan_scope="full", timeout=300,
|
||||||
|
)
|
||||||
|
self.assertTrue(result["success"])
|
||||||
|
payload = post.call_args.kwargs["json"]
|
||||||
|
self.assertEqual(payload["patterns"], ["ERROR", r"ORA-\d+"])
|
||||||
|
self.assertTrue(payload["regex"])
|
||||||
|
self.assertEqual(payload["scan_scope"], "full")
|
||||||
|
self.assertEqual(payload["timeout"], 300)
|
||||||
|
|
||||||
|
def test_log_enable_uses_authenticated_customer_endpoint(self):
|
||||||
|
response = FakeResponse({"success": True, "data": {"enabled": True, "persisted": True}})
|
||||||
|
with mock.patch.object(skill, "get_config", return_value=self.config()), mock.patch.object(
|
||||||
|
skill.requests, "post", return_value=response
|
||||||
|
) as post:
|
||||||
|
result = skill.set_log_analysis_enabled(True, "AHMW")
|
||||||
|
self.assertTrue(result["success"])
|
||||||
|
self.assertEqual(post.call_args.args[0], "http://transit.example/api/log/config")
|
||||||
|
self.assertEqual(post.call_args.kwargs["json"]["enabled"], True)
|
||||||
|
|
||||||
|
def test_log_error_code_is_preserved(self):
|
||||||
|
response = FakeResponse(
|
||||||
|
{"success": False, "error": "log file not found", "error_code": "file_not_found"},
|
||||||
|
ok=False, status_code=404,
|
||||||
|
)
|
||||||
|
with mock.patch.object(skill, "get_config", return_value=self.config()), mock.patch.object(
|
||||||
|
skill.requests, "post", return_value=response
|
||||||
|
):
|
||||||
|
result = skill.log_info(r"D:\missing.log", "AHMW")
|
||||||
|
self.assertFalse(result["success"])
|
||||||
|
self.assertEqual(result["error_code"], "file_not_found")
|
||||||
|
self.assertEqual(result["status_code"], 404)
|
||||||
|
|
||||||
|
def test_capabilities_lists_all_log_commands(self):
|
||||||
|
output = io.StringIO()
|
||||||
|
with redirect_stdout(output):
|
||||||
|
skill.cmd_capabilities(type("Args", (), {"json": True})())
|
||||||
|
payload = json.loads(output.getvalue())
|
||||||
|
names = {item["name"] for item in payload["commands"]}
|
||||||
|
self.assertTrue({"log_info", "log_tail", "log_search", "log_enable", "log_disable"}.issubset(names))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user