Files
oracle-jump-query-public/scripts/oracle_skill.py
T

3827 lines
171 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Oracle Jump Query - AI Skill (Improved Version)
通过中转服务查询远程 Oracle 存储过程元数据
改进点:
1. 增加重试机制(可配置重试次数)
2. 增加详细日志输出(方便诊断问题)
3. 超时时间可配置
4. 增加 Agent 状态检查
5. 增加请求超时分级(快速命令 vs 慢速命令)
"""
import json
import os
import sys
import time
import logging
import io
import re
import html
import requests
import base64
import hashlib
import platform
import uuid
import secrets
from typing import Optional, Dict, Any, List, Tuple
import argparse
from datetime import datetime, timezone
# 修复 Windows 控制台 UTF-8 输出
if sys.platform == 'win32':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
# ============================================================
# 配置(修改为你的中转服务地址和认证信息)
# ============================================================
TRANSIT_URL = "https://ts.henlo.net"
AUTH_TOKEN = ""
DEFAULT_SERVER_ID = "server-001"
DEFAULT_TIMEOUT = 60 # 默认超时(秒)
MAX_RETRIES = 3 # 最大重试次数
RETRY_DELAY = 2 # 重试延迟(秒)
BOS_API_URL = "http://bos.henlo.net/servlet/doserverdata" # BOS 统一接口地址
MAX_AWR_DOWNLOAD_BYTES = 50 * 1024 * 1024
AWR_DATE_PATTERN = re.compile(r"^\d{8}$")
# ============================================================
# 版本号
# ============================================================
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
SKILL_DIR = os.path.dirname(SCRIPT_DIR)
VERSION_FILE = os.path.join(SKILL_DIR, "VERSION")
NO_PROXY_HOSTS = "bos.henlo.net,124.220.155.147,127.0.0.1,localhost"
for _proxy_key in ("NO_PROXY", "no_proxy"):
_existing_no_proxy = os.environ.get(_proxy_key, "")
_items = [item.strip() for item in _existing_no_proxy.split(",") if item.strip()]
for _host in NO_PROXY_HOSTS.split(","):
if _host not in _items:
_items.append(_host)
os.environ[_proxy_key] = ",".join(_items)
def get_version():
"""读取 VERSION 文件中的版本号"""
try:
with open(VERSION_FILE, "r", encoding="utf-8") as f:
return f.read().strip()
except Exception:
return "0.0.0"
VERSION = get_version()
# 表空间查询 SQL(需要 SELECT_CATALOG_ROLE 或 DBA 权限,建议 DBA 授权)
TABLESPACE_SQL_DBA = r"""
SELECT
df.tablespace_name AS "表空间名称",
ROUND(df.total_bytes / 1048576, 1) AS "总大小MB",
ROUND((df.total_bytes - NVL(fs.free_bytes, 0)) / 1048576, 1) AS "已用MB",
ROUND(NVL(fs.free_bytes, 0) / 1048576, 1) AS "剩余MB",
ROUND((df.total_bytes - NVL(fs.free_bytes, 0)) / df.total_bytes * 100, 1) AS "使用率%",
df.autoextensible AS "自动拓展",
ROUND(df.max_bytes / 1048576, 1) AS "最大可拓展MB",
ROUND((df.total_bytes - NVL(fs.free_bytes, 0)) / NULLIF(df.max_bytes, 0) * 100, 1) AS "最大使用率%"
FROM (
SELECT
tablespace_name,
SUM(bytes) AS total_bytes,
SUM(CASE WHEN autoextensible = 'YES' THEN maxbytes ELSE bytes END) AS max_bytes,
MAX(CASE WHEN autoextensible = 'YES' THEN 'YES' ELSE 'NO' END) AS autoextensible
FROM dba_data_files
GROUP BY tablespace_name
) df
LEFT JOIN (
SELECT tablespace_name, SUM(bytes) AS free_bytes
FROM dba_free_space
GROUP BY tablespace_name
) fs ON df.tablespace_name = fs.tablespace_name
ORDER BY 6 DESC
"""
# 表空间查询回退 SQL(仅 USER_FREE_SPACE,始终可用)
TABLESPACE_SQL_FREE = """
SELECT tablespace_name AS "表空间名称",
ROUND(SUM(bytes) / 1048576, 2) AS "剩余MB"
FROM user_free_space
GROUP BY tablespace_name
ORDER BY tablespace_name
"""
# DBA 授权 VIEW 名称:DBA 执行一次授权 SQL 后即可使用
DBA_TS_VIEW = "HENLO_TS_USAGE"
DBA_TS_VIEW_FULL = f"sys.{DBA_TS_VIEW}"
TABLESPACE_SQL_DBA_VIEW = f"""SELECT * FROM {DBA_TS_VIEW}"""
TABLESPACE_SQL_DBA_VIEW_FULL = f"""SELECT * FROM {DBA_TS_VIEW_FULL}"""
# 尝试使用 DBA 创建的专用函数(返回 SYS_REFCURSOR)
TABLESPACE_SQL_DBA_FUNC = """SELECT sys.get_tablespace_usage FROM DUAL"""
_TS_FUNC_COLUMNS = ["表空间名称", "总大小MB", "已用MB", "剩余MB", "使用率%"]
_TS_FUNC_RE = re.compile(r'\[(\w+)\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)\]')
# 表空间使用率字段索引(完整 8 列表)
_TS_COL_NAMES = 0
_TS_COL_TOTAL = 1
_TS_COL_USED = 2
_TS_COL_FREE = 3
_TS_COL_PCT = 4
_TS_COL_AUTO = 5
_TS_COL_MAX = 6
_TS_COL_MAXPCT = 7
def _check_ts_warnings(data_str: str) -> bool:
"""
智能检查表空间告警:
- 优先按表解析( | ... | 格式的 markdown 行)
- 自动扩展(YES)→ 看最大使用率%列
- 非自动扩展(NO)→ 看当前使用率%列
- 若解析失败则回退到字符串模糊匹配
- 回退路径忽略最大使用率%列,仅匹配当前使用率%列
- 函数查询路径(5 列)一律匹配第 5 列(使用率%)
"""
has_warning = False
records = [] # (名称, 有效使用率)
for line in data_str.split("\n"):
line = line.strip()
if not line.startswith("|") or line.startswith("|---"):
continue
cells = [c.strip() for c in line.split("|")]
cells = [c for c in cells if c] # 去掉首尾空单元格
if len(cells) >= 5:
name = cells[_TS_COL_NAMES]
if len(cells) >= 8:
# 完整 8 列表(VIEW / DBA 路径)
if cells[_TS_COL_AUTO] == "YES":
try:
rate = float(cells[_TS_COL_MAXPCT])
records.append((name, rate))
if rate >= 95:
has_warning = True
except ValueError:
pass
else:
try:
rate = float(cells[_TS_COL_PCT])
records.append((name, rate))
if rate >= 95:
has_warning = True
except ValueError:
pass
elif len(cells) == 5:
# 函数查询 5 列表
try:
rate = float(cells[_TS_COL_PCT])
records.append((name, rate))
if rate >= 95:
has_warning = True
except ValueError:
pass
if has_warning:
print("⚠️ 警告:以下表空间使用率过高,请尽快扩容!")
# 对已触发告警的记录排序输出
warned = [(n, r) for n, r in records if r >= 95]
warned.sort(key=lambda x: -x[1])
for n, r in warned:
print(f" 🔴 {n}: {r}%")
return has_warning
# 给 DBA 的授权 SQL(仅在无权限时输出)
DBA_GRANT_SQL = f'''-- ════════════════════════════════════════════════════════
-- 以下 SQL 请交给 DBA 在 Oracle 中执行一次
-- 作用:创建一个授权视图,让普通用户 bosnds3 也能查询表空间完整信息
-- 此视图使用定义者权限(默认),以 DBA 身份运行,不会暴露其他数据
-- ════════════════════════════════════════════════════════
CREATE OR REPLACE VIEW {DBA_TS_VIEW} AS
SELECT
df.tablespace_name AS "表空间名称",
ROUND(df.total_bytes / 1048576, 1) AS "总大小MB",
ROUND((df.total_bytes - NVL(fs.free_bytes, 0)) / 1048576, 1) AS "已用MB",
ROUND(NVL(fs.free_bytes, 0) / 1048576, 1) AS "剩余MB",
ROUND((df.total_bytes - NVL(fs.free_bytes, 0)) / df.total_bytes * 100, 1) AS "使用率%",
df.autoextensible AS "自动拓展",
ROUND(df.max_bytes / 1048576, 1) AS "最大可拓展MB",
ROUND((df.total_bytes - NVL(fs.free_bytes, 0)) / NULLIF(df.max_bytes, 0) * 100, 1) AS "最大使用率%"
FROM (
SELECT
tablespace_name,
SUM(bytes) AS total_bytes,
SUM(CASE WHEN autoextensible = 'YES' THEN maxbytes ELSE bytes END) AS max_bytes,
MAX(CASE WHEN autoextensible = 'YES' THEN 'YES' ELSE 'NO' END) AS autoextensible
FROM dba_data_files
GROUP BY tablespace_name
) df
LEFT JOIN (
SELECT tablespace_name, SUM(bytes) AS free_bytes
FROM dba_free_space
GROUP BY tablespace_name
) fs ON df.tablespace_name = fs.tablespace_name
ORDER BY 6 DESC;
GRANT SELECT ON {DBA_TS_VIEW_FULL} TO bosnds3;
'''
def _parse_func_result(data_str):
"""从 SYS_REFCURSOR 返回的 Go 结构体中提取表格数据"""
matches = _TS_FUNC_RE.findall(data_str)
if not matches:
return None
# 构建 Markdown 表格
header = "| " + " | ".join(_TS_FUNC_COLUMNS) + " |"
sep = "|---" * len(_TS_FUNC_COLUMNS) + "|"
rows = []
for m in matches:
row = "| " + " | ".join(m) + " |"
rows.append(row)
return header + "\n" + sep + "\n" + "\n".join(rows)
def query_tablespace(server_id):
"""
查询表空间使用情况,按优先级尝试四种方式:
1. DBA_DATA_FILES 直接查询(需 SELECT_CATALOG_ROLE)
2. DBA 创建的专用函数 get_tablespace_usage(返回 SYS_REFCURSOR)
3. DBA 授权的专用 VIEW HENLO_TS_USAGE
4. USER_FREE_SPACE 简单查询(始终可用,仅剩余空间)
"""
schema = "BOSNDS3"
# 第 1 步:尝试 DBA 查询
result = query(server_id, "execute_query", schema, "",
timeout=DEFAULT_TIMEOUT, max_retries=1,
sql=TABLESPACE_SQL_DBA)
if result.get("success"):
return result
err = (result.get("error") or "").lower()
is_perm_error = "ora-00942" in err or "insufficient privileges" in err
if not is_perm_error:
return result
# 第 2 步:尝试 DBA 创建的专用函数(返回 SYS_REFCURSOR)
logger.info("DBA 视图不可用,尝试 DBA 专用函数...")
result_func = query(server_id, "execute_query", schema, "",
timeout=DEFAULT_TIMEOUT, max_retries=1,
sql=TABLESPACE_SQL_DBA_FUNC)
if result_func.get("success"):
raw_data = result_func.get("data", "")
parsed = _parse_func_result(raw_data)
if parsed:
result_func["_via_func"] = True
result_func["data"] = parsed
return result_func
else:
# 解析失败但数据可用,保留原始数据
logger.info("函数返回了数据但格式无法解析,保留原始输出")
result_func["_via_func"] = True
result_func["_parse_failed"] = True
return result_func
# 第 3 步:尝试 DBA 授权 VIEW(无 schema 前缀)
logger.info("DBA 函数不可用,尝试 DBA 授权 VIEW...")
result2 = query(server_id, "execute_query", schema, "",
timeout=DEFAULT_TIMEOUT, max_retries=1,
sql=TABLESPACE_SQL_DBA_VIEW)
if result2.get("success"):
result2["_via_view"] = True
return result2
# 第 3b 步:尝试 DBA 授权 VIEW(带 sys. 前缀,某些库可能创建在 SYS 下)
logger.info("VIEW 无前缀不可用,尝试 sys. 前缀...")
result2b = query(server_id, "execute_query", schema, "",
timeout=DEFAULT_TIMEOUT, max_retries=1,
sql=TABLESPACE_SQL_DBA_VIEW_FULL)
if result2b.get("success"):
result2b["_via_view"] = True
return result2b
# 第 4 步:全部失败,输出授权 SQL + 回退到 USER_FREE_SPACE
logger.info("所有 DBA 方案不可用,回退到 USER_FREE_SPACE")
_print_dba_grant_sql()
result3 = query(server_id, "execute_query", schema, "",
timeout=DEFAULT_TIMEOUT, max_retries=MAX_RETRIES,
sql=TABLESPACE_SQL_FREE)
if result3.get("success"):
result3["_fallback"] = True
return result3
def _print_dba_grant_sql():
"""打印给 DBA 执行的授权 SQL 和说明"""
print("\n" + "═" * 70)
print("⚠️ 当前 Oracle 用户无 DBA 系统视图权限,无法获取完整表空间信息。")
print("\n请将以下 SQL 交给 DBA 执行一次:")
print("─" * 70)
print(DBA_GRANT_SQL)
print("─" * 70)
print("📋 DBA 执行授权后,再次查询即可获得完整信息。")
print("=" * 70)
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def get_script_dir():
"""获取脚本自身所在目录"""
return os.path.dirname(os.path.abspath(__file__))
def get_config():
"""Load local runtime config, creating it from the checked-in template when absent."""
cfg_path = os.path.join(get_script_dir(), "config.json")
template_path = os.path.join(get_script_dir(), "config.template.json")
for path in (cfg_path, "config.json"):
try:
with open(path, "r", encoding="utf-8-sig") as f:
return json.load(f)
except FileNotFoundError:
continue
except json.JSONDecodeError as exc:
logger.warning("Invalid local config %s: %s", path, exc)
break
cfg = {"transit_url": TRANSIT_URL, "server_id": "", "access_token": "", "expires_at": "", "user_name": "", "client_code": "", "client_title": "", "client_list": []}
try:
with open(template_path, "r", encoding="utf-8-sig") as f:
cfg.update(json.load(f))
except (FileNotFoundError, json.JSONDecodeError):
pass
save_config(cfg)
return cfg
def save_config(cfg: dict):
"""保存配置到脚本同目录的 config.json"""
cfg_path = os.path.join(get_script_dir(), "config.json")
with open(cfg_path, "w", encoding="utf-8") as f:
json.dump(cfg, f, ensure_ascii=False, indent=2)
logger.info(f"配置已保存到 {cfg_path}")
def get_client_code(client: Dict[str, Any]) -> str:
"""Return normalized client code from server response."""
return str(client.get("code") or client.get("clientCode") or "").strip()
def get_client_title(client: Dict[str, Any]) -> str:
"""Return normalized client title/name from server response."""
return str(client.get("title") or client.get("name") or "").strip()
def normalize_client(client: Dict[str, Any]) -> Dict[str, Any]:
"""Normalize client response while preserving status fields."""
normalized = dict(client)
normalized["code"] = get_client_code(client)
normalized["title"] = get_client_title(client)
return normalized
def normalize_client_list(clients: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Normalize client list and drop entries without code."""
result = []
seen = set()
for client in clients or []:
normalized = normalize_client(client)
code = normalized.get("code", "")
if not code or code in seen:
continue
seen.add(code)
result.append(normalized)
return result
def merge_client_lists(base: List[Dict[str, Any]], updates: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Merge client metadata by code, preserving title from either source."""
merged: Dict[str, Dict[str, Any]] = {}
order: List[str] = []
for client in normalize_client_list(base) + normalize_client_list(updates):
code = client.get("code", "")
if not code:
continue
if code not in merged:
merged[code] = {}
order.append(code)
merged[code].update(client)
if not merged[code].get("title"):
merged[code]["title"] = get_client_title(client)
return [merged[code] for code in order]
def sync_client_cache(cfg: Dict[str, Any], clients: List[Dict[str, Any]]) -> bool:
"""Write refreshed clients into config and refresh current client title."""
normalized = normalize_client_list(clients)
cfg["client_list"] = normalized
current_code = cfg.get("client_code") or cfg.get("server_id")
if current_code:
for client in normalized:
if client.get("code") == current_code:
cfg["client_code"] = client.get("code", current_code)
cfg["client_title"] = client.get("title", cfg.get("client_title", ""))
cfg["server_id"] = cfg["client_code"]
break
return True
def refresh_client_cache(cfg: Optional[Dict[str, Any]] = None, quiet: bool = False) -> List[Dict[str, Any]]:
"""Refresh the authorized client list from transit server and persist it locally."""
cfg = cfg or get_config() or {}
transit_url = cfg.get("transit_url", TRANSIT_URL)
if not ensure_logged_in(cfg):
return []
clients: List[Dict[str, Any]] = normalize_client_list(cfg.get("client_list", []))
try:
resp = requests.get(f"{transit_url}/api/me", headers=make_headers(), timeout=10)
if resp.status_code == 401:
if not quiet:
print("❌ 中转机登录态已失效,请重新登录")
return clients
resp.raise_for_status()
me = resp.json()
if me.get("expires_at"):
cfg["expires_at"] = me.get("expires_at")
if me.get("user", {}).get("name"):
cfg["user_name"] = me.get("user", {}).get("name")
except requests.exceptions.RequestException as e:
if not quiet:
print(f"⚠️ 从 /api/me 刷新 client 失败: {e}")
try:
resp = requests.get(f"{transit_url}/api/clients", headers=make_headers(), timeout=10)
if resp.status_code == 401:
if not quiet:
print("❌ 中转机登录态已失效,请重新登录")
return clients
resp.raise_for_status()
data = resp.json()
# /api/clients is authoritative for the current authorization snapshot.
# Replace instead of merging so revoked clients disappear from local cache.
clients = normalize_client_list(data.get("clients", []))
except requests.exceptions.RequestException as e:
if not quiet:
print(f"⚠️ 从 /api/clients 刷新 client 失败: {e}")
return clients
if sync_client_cache(cfg, clients):
save_config(cfg)
return clients
def find_client_matches(clients: List[Dict[str, Any]], selector: str) -> Tuple[List[Dict[str, Any]], str]:
"""Find client by code/title/name. Exact matches win over fuzzy matches."""
selector = (selector or "").strip()
if not selector:
return [], "empty"
normalized = normalize_client_list(clients)
needle = selector.lower()
exact_code = [c for c in normalized if c.get("code", "").lower() == needle]
if exact_code:
return exact_code, "code"
exact_title = [c for c in normalized if c.get("title", "").lower() == needle]
if exact_title:
return exact_title, "title"
fuzzy = []
seen = set()
for client in normalized:
code = client.get("code", "")
title = client.get("title", "")
if needle in code.lower() or needle in title.lower():
if code not in seen:
seen.add(code)
fuzzy.append(client)
return fuzzy, "fuzzy"
def print_client_candidates(clients: List[Dict[str, Any]]):
"""Print selectable client candidates."""
for client in normalize_client_list(clients):
code = client.get("code", "")
title = client.get("title", "")
online = client.get("online")
authorized = client.get("authorized")
status = ""
if online is not None or authorized is not None:
online_text = "在线" if online else "离线"
auth_text = "已授权" if authorized else "未授权"
status = f" ({online_text}, {auth_text})"
print(f" - {code} ({title}){status}")
def ts_api_call(method: str, params: dict, timeout: int = 15) -> Dict[str, Any]:
"""
调用 BOS 统一接口
Args:
method: 接口方法名(如 ts_login, ts_client_check)
params: 请求参数
timeout: 超时时间(秒)
Returns:
dict: {"code": 0/1, "success": True/False, "message": ..., "data": ...}
"""
payload = {"method": method, "params": params}
try:
logger.info(f"调用 BOS 接口: {method}")
resp = requests.post(BOS_API_URL, json=payload, timeout=timeout)
resp.raise_for_status()
result = resp.json()
logger.info(f"BOS 接口响应: code={result.get('code')}, success={result.get('success')}")
return result
except requests.exceptions.ConnectionError as e:
return {"code": 1, "success": False, "message": f"无法连接 BOS 接口: {e}", "data": None}
except requests.exceptions.Timeout:
return {"code": 1, "success": False, "message": f"BOS 接口超时({timeout}秒)", "data": None}
except Exception as e:
return {"code": 1, "success": False, "message": f"BOS 接口错误: {e}", "data": None}
def cmd_clients():
"""获取当前用户可访问 client 列表及在线状态(通过中转服务的 /api/clients 接口)"""
config = get_config() or {}
if not ensure_logged_in(config):
return
clients = refresh_client_cache(config)
if not clients:
print("没有可用的 client")
return
print(f"共 {len(clients)} 个 client:")
print("-" * 60)
for cl in clients:
code = cl.get("code", "?")
title = cl.get("title", "?")
online = cl.get("online", False)
authorized = cl.get("authorized", False)
status_icon = "[OK]" if online and authorized else ("[?]" if online else "[X]")
auth_text = "已授权" if authorized else "未授权"
online_text = "在线" if online else "离线"
print(f" {status_icon} {code} - {title} ({online_text}, {auth_text})")
print("-" * 60)
def get_server_status(client_code: str = "") -> Dict[str, Any]:
"""Get server health status from transit server."""
config = get_config() or {}
transit_url = config.get("transit_url", TRANSIT_URL)
if not ensure_logged_in(config):
return {"success": False, "error": "login required"}
if not client_code:
client_code = config.get("client_code", "")
if not client_code:
return {"success": False, "error": "clientCode is required"}
try:
resp = requests.get(
f"{transit_url}/api/server_status",
params={"client_code": client_code},
headers=make_headers(),
timeout=DEFAULT_TIMEOUT + 5,
)
if resp.status_code == 401:
return {"success": False, "error": "login required or token expired"}
if resp.status_code == 403:
try:
return {"success": False, "error": resp.json().get("error", resp.text)}
except Exception:
return {"success": False, "error": resp.text}
resp.raise_for_status()
return resp.json()
except requests.exceptions.RequestException as e:
return {"success": False, "error": str(e)}
def cmd_checkup(client_code: str = ""):
"""Print server health report as a Markdown table."""
result = get_server_status(client_code)
if not result.get("success"):
print(f"Error: {result.get('error', 'unknown error')}")
return
data = result.get("data", {}) or {}
fields = [
"clientCode",
"info_cpu_rate",
"info_memory_rate",
"info_memory",
"bak_path",
"bak_lastdate",
"computer",
]
print("| " + " | ".join(fields) + " |")
print("| " + " | ".join(["---"] * len(fields)) + " |")
print("| " + " | ".join(str(data.get(f, "") or "") for f in fields) + " |")
if data.get("log_saved") is False:
print(f"\nNote: server log was not saved to BOS: {data.get('log_error', '')}")
def _load_sys_function_bundle(schema: str = "<AGENT_SCHEMA>") -> str:
schema = (schema or "<AGENT_SCHEMA>").strip()
if schema != "<AGENT_SCHEMA>" and not re.fullmatch(r"[A-Za-z][A-Za-z0-9_$#]*", schema):
raise ValueError("schema must be a valid Oracle identifier")
root = os.path.join(SKILL_DIR, "references", "sys-functions")
sections = []
for name in sorted(f for f in os.listdir(root) if f.lower().endswith(".sql")):
with open(os.path.join(root, name), "r", encoding="utf-8-sig") as f:
sections.append(f"-- ===== {name} =====\n" + f.read().replace("<AGENT_SCHEMA>", schema).rstrip())
return "\n\n".join(sections) + "\n"
def _load_sys_function_sql(filename: str, schema: str) -> str:
schema = (schema or "").strip().upper()
if not re.fullmatch(r"[A-Za-z][A-Za-z0-9_$#]*", schema):
raise ValueError("schema must be a valid Oracle identifier")
if filename != os.path.basename(filename) or not filename.lower().endswith(".sql"):
raise ValueError("invalid SYS function filename")
path = os.path.join(SKILL_DIR, "references", "sys-functions", filename)
with open(path, "r", encoding="utf-8-sig") as f:
return f.read().replace("<AGENT_SCHEMA>", schema)
def _resolve_agent_schema(client_code: str) -> str:
version_result = query(client_code, "version", timeout=15)
version_data = version_result.get("data") or {}
if isinstance(version_data, str):
try:
version_data = json.loads(version_data)
except json.JSONDecodeError:
version_data = {}
schema = (version_data.get("schema") or "").strip().upper()
if not re.fullmatch(r"[A-Za-z][A-Za-z0-9_$#]*", schema):
return ""
return schema
def check_inspection_sys_functions(client_code: str) -> Dict[str, Any]:
result = query(client_code, "sys_function_check", timeout=30)
if not result.get("success"):
return {"success": False, "error": result.get("error", "SYS function check failed")}
data = result.get("data") or {}
if isinstance(data, str):
try: data = json.loads(data)
except json.JSONDecodeError: data = {}
if not data.get("success"):
schema = (data.get("schema") or "").strip()
if not schema:
schema = _resolve_agent_schema(client_code)
return {
"success": False,
"error": "SYS.HENLO_ORA_MONITOR is missing, incompatible, or not executable",
"agent_schema": schema,
"sys_function_check": data,
}
return {"success": True, "data": data}
def _inspection_grant_result(result: Dict[str, Any]) -> Dict[str, Any]:
schema = (result.get("agent_schema") or "").strip()
if not schema or not re.fullmatch(r"[A-Za-z][A-Za-z0-9_$#]*", schema):
result["grant_sql_required"] = True
result["grant_sql"] = ""
result["error"] = "SYS function preflight failed and the Agent Oracle schema could not be determined"
result["next_step"] = "请检查 Agent oracle.schema 配置;Skill 不会输出包含 <AGENT_SCHEMA> 占位符的授权 SQL"
return result
result["grant_sql_required"] = True
result["agent_schema"] = schema.upper()
result["grant_sql"] = _load_sys_function_bundle(schema.upper())
result["next_step"] = f"请由客户 DBA 使用 SYS 执行 grant_sql(授权账号 {schema.upper()}),完成后重新生成巡检报告"
return result
def inspection_report(client_code: str = "", timeout: int = 90, report_type: str = "manual") -> Dict[str, Any]:
"""Generate and archive a server inspection report through transit server."""
config = get_config() or {}
transit_url = config.get("transit_url", TRANSIT_URL)
if not ensure_logged_in(config):
return {"success": False, "error": "login required"}
if not client_code:
client_code = config.get("client_code", "")
if not client_code:
return {"success": False, "error": "clientCode is required"}
sys_check = check_inspection_sys_functions(client_code)
if not sys_check.get("success"):
return _inspection_grant_result(sys_check)
payload = {
"client_code": client_code,
"report_type": report_type,
"timeout": timeout,
}
try:
resp = requests.post(
f"{transit_url}/api/inspection_report",
json=payload,
headers=make_headers(),
timeout=max(30, timeout + 15),
)
if resp.status_code in (400, 401, 403, 404, 502, 503, 504):
try:
return {"success": False, "error": resp.json().get("error", resp.text), "status_code": resp.status_code}
except Exception:
return {"success": False, "error": resp.text, "status_code": resp.status_code}
resp.raise_for_status()
return resp.json()
except requests.exceptions.RequestException as e:
return {"success": False, "error": str(e)}
def inspection_report_latest(client_code: str = "", refresh: bool = False) -> Dict[str, Any]:
"""Read the latest archived inspection report, optionally refreshing it first."""
config = get_config() or {}
transit_url = config.get("transit_url", TRANSIT_URL)
if not ensure_logged_in(config):
return {"success": False, "error": "login required"}
if not client_code:
client_code = config.get("client_code", "")
if not client_code:
return {"success": False, "error": "clientCode is required"}
if refresh:
sys_check = check_inspection_sys_functions(client_code)
if not sys_check.get("success"):
return _inspection_grant_result(sys_check)
params = {"client_code": client_code}
if refresh:
params["refresh"] = "true"
try:
resp = requests.get(
f"{transit_url}/api/inspection_report/latest",
params=params,
headers=make_headers(),
timeout=120 if refresh else DEFAULT_TIMEOUT + 5,
)
if resp.status_code in (400, 401, 403, 404, 502, 503, 504):
try:
return {"success": False, "error": resp.json().get("error", resp.text), "status_code": resp.status_code}
except Exception:
return {"success": False, "error": resp.text, "status_code": resp.status_code}
resp.raise_for_status()
return resp.json()
except requests.exceptions.RequestException as e:
return {"success": False, "error": str(e)}
def inspection_report_get(report_id: str) -> Dict[str, Any]:
"""Read an archived inspection report by ID."""
config = get_config() or {}
transit_url = config.get("transit_url", TRANSIT_URL)
if not ensure_logged_in(config):
return {"success": False, "error": "login required"}
report_id = str(report_id or "").strip()
if not report_id:
return {"success": False, "error": "report id is required"}
try:
resp = requests.get(
f"{transit_url}/api/inspection_report/{report_id}",
headers=make_headers(),
timeout=DEFAULT_TIMEOUT + 5,
)
if resp.status_code in (400, 401, 403, 404, 502, 503, 504):
try:
return {"success": False, "error": resp.json().get("error", resp.text), "status_code": resp.status_code}
except Exception:
return {"success": False, "error": resp.text, "status_code": resp.status_code}
resp.raise_for_status()
return resp.json()
except requests.exceptions.RequestException as e:
return {"success": False, "error": str(e)}
def _awr_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 awr_status(client_code: str = "") -> Dict[str, Any]:
"""Read AWR readiness without triggering report generation."""
cfg = get_config() or {}
if not ensure_logged_in(cfg):
return {"success": False, "error": "login required"}
client_code = _awr_client_code(client_code)
if not client_code:
return {"success": False, "error": "clientCode is required"}
try:
resp = requests.get(
f"{str(cfg.get('transit_url') or TRANSIT_URL).rstrip('/')}/api/awr/status",
params={"client_code": client_code},
headers=make_headers(),
timeout=DEFAULT_TIMEOUT,
)
if not resp.ok:
try:
return {"success": False, "error": resp.json().get("error", resp.text), "status_code": resp.status_code}
except Exception:
return {"success": False, "error": resp.text, "status_code": resp.status_code}
result = resp.json()
data = result.get("data") or {}
if isinstance(data, dict) and data.get("status") == "permission_required":
result["dba_sql_required"] = True
schema = _resolve_agent_schema(client_code)
if schema:
result["agent_schema"] = schema
result["dba_sql"] = _load_sys_function_sql("henlo_awr_export.sql", schema)
result["next_step"] = f"请由客户 DBA 在确认 Diagnostics Pack 授权后执行 dba_sql(授权账号 {schema}),Skill 不会自动执行"
else:
result["agent_schema"] = ""
result["dba_sql"] = ""
result["next_step"] = "请检查 Agent oracle.schema 配置;Skill 不会输出包含 <AGENT_SCHEMA> 占位符的 SQL"
return result
except requests.exceptions.RequestException as e:
return {"success": False, "error": str(e)}
def awr_list(client_code: str = "") -> Dict[str, Any]:
"""List Agent-generated AWR reports without downloading their HTML."""
cfg = get_config() or {}
if not ensure_logged_in(cfg):
return {"success": False, "error": "login required"}
client_code = _awr_client_code(client_code)
if not client_code:
return {"success": False, "error": "clientCode is required"}
try:
resp = requests.get(
f"{str(cfg.get('transit_url') or TRANSIT_URL).rstrip('/')}/api/awr/list",
params={"client_code": client_code},
headers=make_headers(),
timeout=DEFAULT_TIMEOUT,
)
if not resp.ok:
try:
return {"success": False, "error": resp.json().get("error", resp.text), "status_code": resp.status_code}
except Exception:
return {"success": False, "error": resp.text, "status_code": resp.status_code}
return resp.json()
except requests.exceptions.RequestException as e:
return {"success": False, "error": str(e)}
def _safe_awr_client_part(client_code: str) -> str:
value = re.sub(r"[^A-Za-z0-9_-]+", "_", client_code).strip("_")
return value or "CLIENT"
def download_awr_report(client_code: str, date: str, output_path: str = "") -> Dict[str, Any]:
"""Download the exact AWR HTML supplied by Agent through transit-server."""
cfg = get_config() or {}
if not ensure_logged_in(cfg):
return {"success": False, "error": "login required"}
client_code = _awr_client_code(client_code)
date = (date or "").strip()
if not client_code:
return {"success": False, "error": "clientCode is required"}
if not AWR_DATE_PATTERN.fullmatch(date):
return {"success": False, "error": "date must use yyyyMMdd format"}
if not output_path:
out_dir = os.path.join(SKILL_DIR, "outputs")
os.makedirs(out_dir, exist_ok=True)
output_path = os.path.join(out_dir, f"AWR-{_safe_awr_client_part(client_code)}-{date}.html")
output_path = os.path.abspath(output_path)
parent = os.path.dirname(output_path)
if parent:
os.makedirs(parent, exist_ok=True)
temp_path = output_path + ".part"
try:
if os.path.exists(temp_path):
os.remove(temp_path)
except OSError as e:
return {"success": False, "error": str(e)}
total = 0
try:
with requests.get(
f"{str(cfg.get('transit_url') or TRANSIT_URL).rstrip('/')}/api/awr/download",
params={"client_code": client_code, "date": date},
headers=make_headers(),
timeout=120,
stream=True,
) as resp:
if not resp.ok:
try:
error = resp.json().get("error", resp.text)
except Exception:
error = resp.text
return {"success": False, "error": error, "status_code": resp.status_code}
content_length = resp.headers.get("Content-Length", "")
if content_length.isdigit() and int(content_length) > MAX_AWR_DOWNLOAD_BYTES:
return {"success": False, "error": f"AWR report exceeds {MAX_AWR_DOWNLOAD_BYTES} bytes"}
with open(temp_path, "wb") as f:
for chunk in resp.iter_content(chunk_size=64 * 1024):
if not chunk:
continue
total += len(chunk)
if total > MAX_AWR_DOWNLOAD_BYTES:
raise ValueError(f"AWR report exceeds {MAX_AWR_DOWNLOAD_BYTES} bytes")
f.write(chunk)
os.replace(temp_path, output_path)
return {"success": True, "client_code": client_code, "date": date, "path": output_path, "size": total}
except (requests.exceptions.RequestException, OSError, ValueError) as e:
try:
if os.path.exists(temp_path):
os.remove(temp_path)
except OSError:
pass
return {"success": False, "error": str(e)}
def cmd_awr_status(client_code: str = "", as_json: bool = False):
result = awr_status(client_code)
if as_json or not result.get("success"):
print(json.dumps(result, ensure_ascii=False, indent=2))
return
data = result.get("data") or {}
print(f"AWR status: {data.get('status', '-')}")
print(f"Client: {result.get('client_code') or _awr_client_code(client_code)}")
print(f"Available: {data.get('available', False)}")
print(f"Enabled: {data.get('enabled', False)}")
print(f"Diagnostics Pack confirmed: {data.get('license_confirmed', False)}")
if data.get("latest_date"):
print(f"Latest date: {data.get('latest_date')}")
if data.get("reason"):
print(f"Reason: {data.get('reason')}")
if result.get("dba_sql_required"):
if result.get("dba_sql"):
print(f"Agent schema: {result.get('agent_schema')}")
print("DBA SQL (manual execution only; Diagnostics Pack authorization must be confirmed first):")
print(result.get("dba_sql"))
print(f"Next step: {result.get('next_step', '')}")
def cmd_awr_list(client_code: str = "", as_json: bool = False):
result = awr_list(client_code)
if as_json or not result.get("success"):
print(json.dumps(result, ensure_ascii=False, indent=2))
return
rows = result.get("data") or []
if not rows:
print("No AWR reports are available.")
return
print("DATE STATUS BEGIN_SNAP END_SNAP GENERATED_AT REASON")
for row in rows:
print(f"{row.get('date', '-'):<9} {row.get('status', '-'):<8} {str(row.get('begin_snap', '-')):<10} {str(row.get('end_snap', '-')):<8} {row.get('generated_at', '-')} {row.get('reason', '')}")
def cmd_awr_download(client_code: str, date: str, output_path: str = "", as_json: bool = False):
result = download_awr_report(client_code, date, output_path)
if as_json or not result.get("success"):
print(json.dumps(result, ensure_ascii=False, indent=2))
return
print(f"AWR report: {result['path']}")
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]:
data = result.get("data") or {}
if isinstance(data, dict) and data.get("report_json"):
report_json = data.get("report_json")
if isinstance(report_json, str):
try:
return json.loads(report_json)
except Exception:
return {}
if isinstance(report_json, dict):
return report_json
if isinstance(data, dict) and ("memory" in data or "disks" in data or "alerts" in data):
return data
return {}
def _fmt_report_number(value):
if value is None or value == "":
return ""
if isinstance(value, float) and value.is_integer():
return str(int(value))
return str(value)
def _html_escape(value) -> str:
if value is None:
return ""
return html.escape(str(value), quote=True)
def _pct(value) -> str:
if value is None or value == "":
return ""
try:
return f"{float(value):.2f}%"
except Exception:
return _html_escape(value)
def _gb(value) -> str:
if value is None or value == "":
return ""
try:
return f"{float(value):.2f}G"
except Exception:
return _html_escape(value)
def _tag(level: str, text: str = "") -> str:
level = str(level or "").lower()
if level in ("critical", "bad", "error", "failed"):
cls, label = "bad", text or "重大异常"
elif level in ("warning", "warn", "partial"):
cls, label = "warn", text or "普通异常"
elif level in ("pending", "unknown", "not_checked"):
cls, label = "pending", text or "待接入"
else:
cls, label = "ok", text or "正常"
return f'<span class="tag {cls}">{_html_escape(label)}</span>'
def _status_level_from_percent(value, warn=80, bad=90):
try:
n = float(value)
except Exception:
return "pending"
if n >= bad:
return "critical"
if n >= warn:
return "warning"
return "ok"
def _cache_hit_level(value):
try:
n = float(value)
except Exception:
return "pending"
if n < 80:
return "critical"
if n < 90:
return "warning"
return "ok"
def _render_process_summary(processes):
if not processes:
return "未返回关键进程状态", "pending"
rows = []
missing = False
for process in processes:
if isinstance(process, str):
rows.append(_html_escape(process))
continue
name = process.get("name") or "unknown"
running = bool(process.get("running"))
count = process.get("count", 0)
missing = missing or not running
label = f"{name}:{'运行中' if running else '未运行'}"
if running:
label += f"({count} 个)"
rows.append(_html_escape(label))
return "<br>".join(rows), "warning" if missing else "ok"
def _find_ops(payload: Dict[str, Any], item_name: str) -> Dict[str, Any]:
for item in payload.get("oracle_ops") or []:
if isinstance(item, dict) and item.get("item") == item_name:
return item
return {}
def _ops_count(item: Dict[str, Any]) -> int:
rows = item.get("rows") if isinstance(item, dict) else []
return len(rows or [])
def _render_ops_rows(item: Dict[str, Any], empty_text: str, max_rows: int = 6) -> str:
rows = item.get("rows") if isinstance(item, dict) else []
if not rows:
return f'<tr><td colspan="6">{_html_escape(empty_text)}</td></tr>'
columns = item.get("columns") or []
html_rows = []
for row in rows[:max_rows]:
data = {str(columns[i]).upper(): row[i] for i in range(min(len(columns), len(row)))}
session = f"{data.get('USERNAME', '')} / {data.get('SID', '')},{data.get('SERIAL_NO', '')}".strip(" /,")
detail = data.get("SQL_TEXT", "")
source = " / ".join(value for value in (data.get("MACHINE", ""), data.get("PROGRAM", "")) if value)
html_rows.append(
"<tr>"
f"<td>{_html_escape(data.get('ELAPSED_SEC', ''))} 秒</td>"
f"<td>{_html_escape(session)}</td>"
f"<td>{_html_escape(data.get('SQL_ID', ''))}</td>"
f"<td>{_html_escape(data.get('EVENT', ''))}</td>"
f"<td>{_tag('warning' if item.get('status') == 'warning' else 'ok')}</td>"
f"<td>{_html_escape(source)}<div class=\"sql-text\">{_html_escape(detail)}</div></td>"
"</tr>"
)
return "\n".join(html_rows)
def _render_tablespace_cards(payload: Dict[str, Any]) -> str:
cards = []
tablespace_item = _find_ops(payload, "tablespace")
datafile_item = _find_ops(payload, "datafiles")
datafile_columns = [str(value).upper() for value in (datafile_item.get("columns") or [])]
datafiles = {}
for row in datafile_item.get("rows") or []:
data = {datafile_columns[i]: row[i] for i in range(min(len(datafile_columns), len(row)))}
name = data.get("TABLESPACE_NAME", "")
if name and (name not in datafiles or float(data.get("MAX_GB") or 0) > float(datafiles[name].get("MAX_GB") or 0)):
datafiles[name] = data
columns = [str(value).upper() for value in (tablespace_item.get("columns") or [])]
for row in tablespace_item.get("rows") or []:
data = {columns[i]: row[i] for i in range(min(len(columns), len(row)))}
name = data.get("TABLESPACE_NAME") or (row[0] if row else "")
datafile = datafiles.get(name, {})
used = data.get("USED_PCT", "")
max_gb = datafile.get("MAX_GB", "")
current_gb = data.get("TOTAL_GB", "")
used_gb = data.get("USED_GB", "")
free_gb = data.get("FREE_GB", "")
max_used = datafile.get("FILE_USED_PCT", "")
try:
extend_free = round(float(max_gb) - float(used_gb), 2) if max_gb != "" else ""
except Exception:
extend_free = ""
level_value = max_used if max_used != "" else used
level = _status_level_from_percent(level_value, 85, 95)
metrics = [("当前大小", f"{current_gb}G"), ("已用", f"{used_gb}G"), ("当前剩余", f"{free_gb}G"),
("自动扩展上限", f"{max_gb}G" if max_gb != "" else "未返回"),
("可扩展剩余", f"{extend_free}G" if extend_free != "" else "未返回"),
("最大空间使用率", f"{level_value}%" if level_value != "" else "未返回")]
metric_html = "".join(f'<div class="space-metric"><div class="space-label">{_html_escape(k)}</div><div class="space-value">{_html_escape(v)}</div></div>' for k, v in metrics)
advice = "当前未达到重大阈值,建议持续观察。" if level == "ok" else "空间使用率较高,建议评估扩容、清理历史数据或确认自动扩展上限。"
cards.append('<div class="tablespace-card">' f'<div class="tablespace-head"><div class="tablespace-name">{_html_escape(name)}</div>{_tag(level)}</div>' f'<div class="tablespace-metrics">{metric_html}</div>' f'<div class="tablespace-advice">{_html_escape(advice)}</div></div>')
if not cards:
return '<div class="tablespace-card"><div class="tablespace-advice">未返回表空间明细。</div></div>'
return "\n".join(cards)
def _render_alert_rows(payload: Dict[str, Any]) -> str:
alerts = payload.get("alerts") or []
failed = payload.get("failed_items") or []
rows = []
for alert in alerts:
level = alert.get("level", "warning") if isinstance(alert, dict) else "warning"
title = alert.get("title", "异常") if isinstance(alert, dict) else str(alert)
desc = alert.get("desc", "") if isinstance(alert, dict) else ""
rows.append(f"<tr><td>{_tag(level)}</td><td>{_html_escape(title)}</td><td>{_html_escape(desc)}</td><td>按运维规范排查并持续观察。</td></tr>")
for item in failed:
name = item.get("item", "巡检项") if isinstance(item, dict) else "巡检项"
err = item.get("error", "") if isinstance(item, dict) else str(item)
rows.append(f"<tr><td>{_tag('warning')}</td><td>{_html_escape(name)}</td><td>{_html_escape(err)}</td><td>检查授权、网络或 Agent 日志后重试。</td></tr>")
if not rows:
rows.append(f"<tr><td>{_tag('ok')}</td><td>未发现异常</td><td>本次巡检未产生告警。</td><td>保持观察。</td></tr>")
return "\n".join(rows)
def render_inspection_report_html(result: Dict[str, Any]) -> str:
payload = _inspection_report_payload(result)
record = result.get("data") if isinstance(result.get("data"), dict) else {}
client = result.get("client_code") or payload.get("client_code") or record.get("client_code") or get_server_id()
report_no = result.get("report_no") or record.get("report_no") or f"CHECK-{client}"
collected_at = payload.get("collected_at") or record.get("collected_at") or ""
server = payload.get("server") or {}
memory = payload.get("memory") or {}
disks = payload.get("disks") or []
oracle = payload.get("oracle") or {}
connections = server.get("connections") or {}
process_summary, process_level = _render_process_summary(server.get("processes"))
cache_hit = oracle.get("cache_hit") or {}
buffer_cache = cache_hit.get("buffer_cache_percent")
library_cache = cache_hit.get("library_cache_percent")
alerts = payload.get("alerts") or []
overall = "warning" if alerts or payload.get("failed_items") else "ok"
cpu_level = _status_level_from_percent(server.get("cpu_percent"), 80, 90)
mem_level = _status_level_from_percent(memory.get("used_percent"), 80, 90)
oracle_level = "ok" if oracle.get("online") else "critical"
blocking = _find_ops(payload, "blocking_locks")
long_tx = _find_ops(payload, "long_transactions")
slow = _find_ops(payload, "active_slow_sql")
ip_addresses = ", ".join(server.get("ip_addresses") or []) or "待采集"
os_text = " ".join(str(value) for value in (server.get("os"), server.get("os_version")) if value) or "待采集"
connection_text = "采集失败"
connection_level = "pending"
if connections.get("available"):
connection_text = "总计 {total};已建立 {established};监听 {listening};TIME_WAIT {time_wait};CLOSE_WAIT {close_wait}".format(
total=connections.get("total", 0), established=connections.get("established", 0),
listening=connections.get("listening", 0), time_wait=connections.get("time_wait", 0),
close_wait=connections.get("close_wait", 0))
connection_level = "ok"
disk_rows = []
disk_burst = "未爆满"
for disk in disks:
level = disk.get("status") or _status_level_from_percent(disk.get("used_percent"), 85, 95)
if level in ("warning", "critical", "bad"):
disk_burst = "存在风险"
disk_rows.append(
"<tr>"
f"<td>{_html_escape(disk.get('drive'))}</td>"
f"<td>{_pct(disk.get('used_percent'))}</td>"
f"<td>{_gb(disk.get('used_gb'))} / {_gb(disk.get('free_gb'))}</td>"
f"<td>读 {_html_escape(disk.get('read_mb_s', 0))} MB/s<br>写 {_html_escape(disk.get('write_mb_s', 0))} MB/s</td>"
f"<td>{_tag(level)}</td>"
f"<td>24h 使用率峰值 {_pct(disk.get('used_peak_24h_percent'))}。</td>"
"</tr>"
)
if not disk_rows:
disk_rows.append(f"<tr><td colspan=\"6\">未返回磁盘明细。</td></tr>")
disk_rows.append(f"<tr><td>磁盘爆满检测</td><td>{_html_escape(disk_burst)}</td><td>普通:≥ 85%<br>重大:≥ 95%</td><td>-</td><td>{_tag('warning' if disk_burst != '未爆满' else 'ok')}</td><td>按各分区当前使用率和 24h 峰值判断。</td></tr>")
conclusion = "本次巡检未发现明显异常。"
if alerts:
conclusion = "本次巡检发现:" + ";".join(_html_escape(a.get("title", "")) for a in alerts if isinstance(a, dict)) + "。建议按异常明细处理。"
return f"""<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>服务器巡检报告</title>
<style>
:root {{--text:#182230;--muted:#667085;--line:#d0d5dd;--head:#f3f6fa;--blue:#1f5f8b;--green:#18794e;--green-bg:#e8f6ef;--yellow:#a15c00;--yellow-bg:#fff3d6;--red:#b42318;--red-bg:#fde8e5;}}
*{{box-sizing:border-box}} body{{margin:0;background:#e9edf2;color:var(--text);font-family:"Microsoft YaHei",SimSun,Arial,sans-serif;font-size:14px;line-height:1.55;letter-spacing:0}}
.page{{width:min(960px,calc(100% - 32px));margin:24px auto;background:#fff;border:1px solid var(--line);box-shadow:0 12px 30px rgba(16,24,40,.12)}} .cover{{display:grid;grid-template-columns:1fr 280px;gap:20px;padding:28px 32px;border-bottom:3px solid var(--blue);background:#f7fbff}}
h1{{margin:0 0 12px;font-size:28px;line-height:36px}} .subtitle{{color:var(--muted);font-size:14px}} .meta-box{{border:1px solid var(--line);background:#fff;padding:12px 14px;display:grid;gap:6px;align-self:start}}
.section{{padding:22px 32px;border-bottom:1px solid var(--line);overflow-x:auto}} h2{{margin:0 0 14px;font-size:18px;line-height:26px;color:#111827}} table{{width:100%;border-collapse:collapse;table-layout:fixed;min-width:760px}} th,td{{border:1px solid var(--line);padding:9px 10px;text-align:left;vertical-align:top;word-break:break-word}} th{{background:var(--head);color:#344054;font-weight:700}}
.tag{{display:inline-block;min-width:64px;padding:3px 8px;border-radius:999px;font-size:12px;font-weight:700;text-align:center}} .ok{{color:var(--green);background:var(--green-bg)}} .warn{{color:var(--yellow);background:var(--yellow-bg)}} .bad{{color:var(--red);background:var(--red-bg)}} .pending{{color:#475467;background:#eef2f6}}
.sql-text{{font-family:Consolas,"Courier New",monospace;font-size:12px;line-height:1.45;white-space:pre-wrap;word-break:break-word;background:#f8fafc;border:1px solid #e4e7ec;padding:8px;margin-top:6px}} .note{{margin:8px 0 12px;color:var(--muted);font-size:13px}} .subsection-title{{margin:18px 0 8px;font-size:15px;font-weight:700;color:#263548}} .subsection-title:first-of-type{{margin-top:0}}
.tablespace-list{{display:grid;gap:12px}} .tablespace-card{{border:1px solid var(--line);background:#fff}} .tablespace-head{{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 12px;background:var(--head);border-bottom:1px solid var(--line)}} .tablespace-name{{font-weight:700;font-size:15px}} .tablespace-metrics{{display:grid;grid-template-columns:repeat(6,1fr);gap:0;border-bottom:1px solid var(--line)}} .space-metric{{min-height:62px;padding:9px 10px;border-right:1px solid var(--line)}} .space-metric:last-child{{border-right:0}} .space-label{{color:var(--muted);font-size:12px;line-height:18px}} .space-value{{margin-top:4px;font-size:16px;line-height:22px;font-weight:700}} .tablespace-advice{{padding:10px 12px;color:#344054}}
.conclusion{{display:grid;grid-template-columns:120px 1fr;gap:14px;align-items:start}} .level{{padding:12px;text-align:center;font-size:16px;font-weight:700;border-radius:4px}} .footer{{padding:14px 32px 24px;color:var(--muted);font-size:12px;text-align:right}} @media print{{body{{background:#fff}}.page{{width:auto;margin:0;border:0;box-shadow:none}}.section{{overflow:visible}}}}
</style>
</head>
<body><main class="page">
<header class="cover"><div><h1>服务器巡检报告</h1><div class="subtitle">本报告由 Agent 自动采集服务器与 Oracle 数据库巡检结果后生成,用于运维归档、工单报备和客户沟通。</div></div><div class="meta-box"><div><strong>报告编号:</strong>{_html_escape(report_no)}</div><div><strong>巡检时间:</strong>{_html_escape(collected_at)}</div><div><strong>巡检方式:</strong>手动巡检</div><div><strong>总体状态:</strong>{_tag(overall)}</div></div></header>
<section class="section"><h2>一、基础信息</h2><table><tbody><tr><th style="width:140px;">客户名称</th><td>{_html_escape(client)}</td><th style="width:140px;">服务器编号</th><td>{_html_escape(client)}</td></tr><tr><th>服务器 IP</th><td>{_html_escape(ip_addresses)}</td><th>计算机名</th><td>{_html_escape(server.get('computer') or '待采集')}</td></tr><tr><th>操作系统</th><td>{_html_escape(os_text)}</td><th>数据库状态</th><td>{'在线' if oracle.get('online') else '异常'}</td></tr><tr><th>启动时间</th><td>{_html_escape(server.get('boot_time') or '待采集')}</td><th>运行时长</th><td>{_html_escape(server.get('uptime') or '待采集')}</td></tr></tbody></table></section>
<section class="section"><h2>二、服务器资源巡检</h2><div class="subsection-title">2.1 基础运行状态</div><table><tbody><tr><th style="width:160px;">巡检项</th><th style="width:210px;">结果值</th><th style="width:150px;">预警规则</th><th style="width:110px;">状态</th><th>说明</th></tr><tr><td>服务器在线状态</td><td>{'在线' if server.get('online') else '离线'}</td><td>离线即重大异常</td><td>{_tag('ok' if server.get('online') else 'critical')}</td><td>Agent WebSocket 在线,最近心跳正常。</td></tr><tr><td>CPU 使用率</td><td>{_pct(server.get('cpu_percent'))}</td><td>普通:≥ 80%<br>重大:≥ 90%</td><td>{_tag(cpu_level)}</td><td>当前 CPU 使用率按 Agent 实时采集值判断。</td></tr><tr><td>运行时长</td><td>{_html_escape(server.get('uptime') or '待采集')}</td><td>采集失败时标记待接入</td><td>{_tag('ok' if server.get('uptime') else 'pending')}</td><td>启动时间:{_html_escape(server.get('boot_time') or '待采集')}。</td></tr><tr><td>关键进程</td><td>{process_summary}</td><td>任一配置进程未运行即异常</td><td>{_tag(process_level)}</td><td>检查项由 Agent 配置 inspection_processes 定义。</td></tr><tr><td>TCP 连接数</td><td>{_html_escape(connection_text)}</td><td>CLOSE_WAIT 持续增长需关注</td><td>{_tag(connection_level)}</td><td>{_html_escape(connections.get('error') or '来自 netstat -ano -p tcp 实时统计。')}</td></tr></tbody></table>
<div class="subsection-title">2.2 内存状态</div><table><tbody><tr><th style="width:160px;">巡检项</th><th style="width:150px;">结果值</th><th style="width:150px;">预警规则</th><th style="width:110px;">状态</th><th>说明</th></tr><tr><td>实时内存使用率</td><td>{_pct(memory.get('used_percent'))}</td><td>普通:≥ 80%<br>重大:≥ 90%</td><td>{_tag(mem_level)}</td><td>当前内存使用率按 Agent 实时采集值判断。</td></tr><tr><td>已用 / 剩余内存</td><td>{_gb(memory.get('used_gb'))} / {_gb(memory.get('free_gb'))}</td><td>剩余低于 10% 为重大异常</td><td>{_tag(mem_level)}</td><td>总内存 {_gb(memory.get('total_gb'))}。</td></tr><tr><td>内存峰值</td><td>1h {_pct(memory.get('peak_1h_percent'))}<br>24h {_pct(memory.get('peak_24h_percent'))}</td><td>普通:≥ 85%<br>重大:≥ 95%</td><td>{_tag(_status_level_from_percent(memory.get('peak_24h_percent'),85,95))}</td><td>来自 Agent 本地滚动指标缓存。</td></tr><tr><td>内存溢出检测</td><td>{len(memory.get('oom_events') or [])} 条事件</td><td>发现 OOM / 相关错误即异常</td><td>{_tag('warning' if memory.get('oom_events') else 'ok')}</td><td>按采集周期内内存异常事件汇总。</td></tr></tbody></table>
<div class="subsection-title">2.3 磁盘状态</div><table><tbody><tr><th style="width:120px;">分区</th><th style="width:120px;">使用率</th><th style="width:150px;">已用 / 剩余空间</th><th style="width:130px;">读写速率</th><th style="width:110px;">状态</th><th>说明</th></tr>{''.join(disk_rows)}</tbody></table></section>
<section class="section"><h2>三、数据库巡检结果</h2><div class="subsection-title">3.1 基础状态</div><table><tbody><tr><th style="width:160px;">巡检项</th><th style="width:180px;">结果值</th><th style="width:160px;">预警阈值</th><th style="width:110px;">状态</th><th>说明</th></tr><tr><td>数据库服务在线状态</td><td>{'在线' if oracle.get('online') else '异常'}</td><td>不可连接即重大异常</td><td>{_tag(oracle_level)}</td><td>{_html_escape(oracle.get('error') or 'Agent 可正常连接 Oracle,基础查询成功。')}</td></tr><tr><td>数据库会话数</td><td>{_html_escape(oracle.get('active_sessions')) if oracle.get('active_sessions') is not None else '采集失败'} 个 ACTIVE<br>{_html_escape(oracle.get('total_sessions')) if oracle.get('total_sessions') is not None else '-'} 个用户会话</td><td>ACTIVE 普通:≥ 50<br>重大:≥ 100</td><td>{_tag(_status_level_from_percent(oracle.get('active_sessions'),50,100))}</td><td>来源:{_html_escape(oracle.get('session_count_source') or '未采集')}。</td></tr><tr><td>Buffer Cache 命中率</td><td>{_pct(buffer_cache) if buffer_cache is not None else '采集失败'}</td><td>普通:&lt; 90%<br>重大:&lt; 80%</td><td>{_tag(_cache_hit_level(buffer_cache))}</td><td>来自 v$sysstat。</td></tr><tr><td>Library Cache 命中率</td><td>{_pct(library_cache) if library_cache is not None else '采集失败'}</td><td>普通:&lt; 90%<br>重大:&lt; 80%</td><td>{_tag(_cache_hit_level(library_cache))}</td><td>来自 v$librarycache。</td></tr><tr><td>事务运行状态</td><td>长事务 {_ops_count(long_tx)} 条</td><td>普通:≥ 1 条<br>重大:≥ 3 条</td><td>{_tag('warning' if _ops_count(long_tx) else 'ok')}</td><td>按 Oracle 运维监控函数返回结果统计。</td></tr><tr><td>阻塞会话</td><td>{_ops_count(blocking)} 条</td><td>普通:≥ 1 条<br>重大:≥ 3 条</td><td>{_tag('warning' if _ops_count(blocking) else 'ok')}</td><td>未返回记录时视为未发现阻塞。</td></tr></tbody></table>
<div class="subsection-title">3.2 慢 SQL 检测明细</div><div class="note">慢 SQL 不只展示数量,还应保留 SQL_ID、会话、耗时、等待事件和 SQL 文本,便于后续定位来源对象或业务过程。</div><table><tbody><tr><th style="width:160px;">巡检项</th><th style="width:150px;">用户/会话</th><th style="width:150px;">SQL_ID</th><th style="width:130px;">等待事件</th><th style="width:110px;">状态</th><th>SQL 文本 / 溯源</th></tr>{_render_ops_rows(slow, '未发现当前慢 SQL。')}</tbody></table>
<div class="subsection-title">3.3 表空间检测明细</div><div class="note">表空间报告应列出当前空间和自动扩展上限。若数据文件允许自动扩展,异常判断优先参考“最大空间使用率”和“剩余可扩展空间”,避免只按当前大小误报。</div><div class="tablespace-list">{_render_tablespace_cards(payload)}</div>
<div class="subsection-title">3.4 存储过程 / 自定义 SQL 巡检明细</div><div class="note">如果巡检项来自自定义 SQL 或存储过程,需要展示执行对象、SQL/过程内容、返回字段和异常判定依据。</div><table><tbody><tr><th style="width:160px;">巡检名称</th><th style="width:150px;">执行对象</th><th style="width:130px;">返回结果</th><th style="width:110px;">状态</th><th>SQL / 存储过程内容</th></tr><tr><td>Oracle 运维监控函数</td><td>SYS.HENLO_ORA_MONITOR</td><td>{'可执行' if payload.get('oracle_ops') else '未返回'}</td><td>{_tag('ok' if payload.get('oracle_ops') else 'warning')}</td><td>Agent 优先调用 SYS 下的巡检函数,失败时回退到当前 schema。<div class="sql-text">SELECT SYS.HENLO_ORA_MONITOR(:item, :minutes, :top_n) FROM DUAL</div></td></tr></tbody></table></section>
<section class="section"><h2>四、异常明细</h2><table><tbody><tr><th style="width:120px;">异常等级</th><th style="width:170px;">异常类型</th><th>异常说明</th><th style="width:160px;">处理建议</th></tr>{_render_alert_rows(payload)}</tbody></table></section>
<section class="section"><h2>五、巡检结论</h2><div class="conclusion"><div class="level {'warn' if overall == 'warning' else 'ok'}">{'普通异常' if overall == 'warning' else '正常'}</div><div>{conclusion}</div></div></section>
<div class="footer">生成来源:Oracle Jump Query Agent / 固定格式 HTML 巡检报告 Demo</div>
</main></body></html>"""
def write_inspection_report_html(result: Dict[str, Any], path: str = "") -> str:
payload = _inspection_report_payload(result)
client = result.get("client_code") or payload.get("client_code") or get_server_id()
report_no = result.get("report_no") or f"inspection-report-{client}"
if not path:
out_dir = os.path.join(SKILL_DIR, "outputs")
os.makedirs(out_dir, exist_ok=True)
safe_name = re.sub(r"[^A-Za-z0-9_.-]+", "_", f"{report_no}-{client}")
path = os.path.join(out_dir, f"{safe_name}.html")
else:
parent = os.path.dirname(os.path.abspath(path))
if parent:
os.makedirs(parent, exist_ok=True)
html_text = render_inspection_report_html(result)
with open(path, "w", encoding="utf-8") as f:
f.write(html_text)
return os.path.abspath(path)
def _markdown_escape(value) -> str:
if value is None:
return ""
return str(value).replace("\\", "\\\\").replace("|", "\\|").replace("\r\n", "<br>").replace("\n", "<br>")
def render_inspection_report_markdown(result: Dict[str, Any]) -> str:
payload = _inspection_report_payload(result)
record = result.get("data") if isinstance(result.get("data"), dict) else {}
client = result.get("client_code") or payload.get("client_code") or record.get("client_code") or get_server_id()
report_no = result.get("report_no") or record.get("report_no") or f"CHECK-{client}"
collected_at = payload.get("collected_at") or record.get("collected_at") or ""
server = payload.get("server") or {}
memory = payload.get("memory") or {}
disks = payload.get("disks") or []
oracle = payload.get("oracle") or {}
alerts = payload.get("alerts") or []
failed = payload.get("failed_items") or []
missing = payload.get("missing_permissions") or payload.get("missing_grants") or []
connections = server.get("connections") or {}
cache_hit = oracle.get("cache_hit") or {}
process_items = []
for process in server.get("processes") or []:
if isinstance(process, str):
process_items.append(process)
else:
process_items.append(f"{process.get('name', 'unknown')}={'运行中' if process.get('running') else '未运行'}({process.get('count', 0)})")
process_text = ";".join(process_items) or "未返回关键进程状态"
stale = result.get("stale")
overall = "普通异常" if alerts or failed else "正常"
lines = [
f"# 服务器巡检报告 - {_markdown_escape(client)}",
"",
f"- 报告编号:{_markdown_escape(report_no)}",
f"- 巡检时间:{_markdown_escape(collected_at)}",
f"- 报告类型:{'历史快照' if stale else '实时报告'}",
f"- 总体状态:{overall}",
"",
"## 一、基础信息",
"",
"| 项目 | 内容 |",
"| --- | --- |",
f"| 客户/服务器编号 | {_markdown_escape(client)} |",
f"| 计算机名 | {_markdown_escape(server.get('computer') or '待确认')} |",
f"| 服务器 IP | {_markdown_escape(', '.join(server.get('ip_addresses') or []) or '待采集')} |",
f"| 操作系统 | {_markdown_escape(' '.join(str(value) for value in (server.get('os'), server.get('os_version')) if value) or '待采集')} |",
f"| 启动时间 | {_markdown_escape(server.get('boot_time') or '待采集')} |",
f"| 服务器状态 | {'在线' if server.get('online') else '离线'} |",
f"| Oracle 状态 | {'在线' if oracle.get('online') else '异常'} |",
"",
"## 二、服务器资源巡检",
"",
"### 2.1 基础运行状态",
"",
"| 巡检项 | 结果值 | 状态 |",
"| --- | --- | --- |",
f"| 服务器在线状态 | {'在线' if server.get('online') else '离线'} | {'正常' if server.get('online') else '重大异常'} |",
f"| CPU 使用率 | {_markdown_escape(_pct(server.get('cpu_percent')))} | {_markdown_escape(_status_level_from_percent(server.get('cpu_percent'), 80, 90))} |",
f"| 运行时长 | {_markdown_escape(server.get('uptime') or '待采集')} | {'正常' if server.get('uptime') else '待接入'} |",
f"| 关键进程 | {_markdown_escape(process_text)} | {'正常' if process_items and all((p if isinstance(p, str) else p.get('running')) for p in (server.get('processes') or [])) else '异常或待采集'} |",
f"| TCP 连接数 | 总计 {_markdown_escape(connections.get('total', '-'))};已建立 {_markdown_escape(connections.get('established', '-'))};监听 {_markdown_escape(connections.get('listening', '-'))};TIME_WAIT {_markdown_escape(connections.get('time_wait', '-'))};CLOSE_WAIT {_markdown_escape(connections.get('close_wait', '-'))} | {'正常' if connections.get('available') else '采集失败'} |",
"",
"### 2.2 内存状态",
"",
"| 总量 | 已用 | 剩余 | 使用率 | 1h 峰值 | 24h 峰值 | OOM 事件 |",
"| --- | --- | --- | --- | --- | --- | --- |",
"| {total} | {used} | {free} | {used_pct} | {peak1} | {peak24} | {oom} |".format(
total=_markdown_escape(_gb(memory.get("total_gb"))),
used=_markdown_escape(_gb(memory.get("used_gb"))),
free=_markdown_escape(_gb(memory.get("free_gb"))),
used_pct=_markdown_escape(_pct(memory.get("used_percent"))),
peak1=_markdown_escape(_pct(memory.get("peak_1h_percent"))),
peak24=_markdown_escape(_pct(memory.get("peak_24h_percent"))),
oom=len(memory.get("oom_events") or []),
),
"",
"### 2.3 磁盘状态",
"",
"| 分区 | 总量 | 已用 | 剩余 | 使用率 | 24h 峰值 | 读速率 MB/s | 写速率 MB/s | 状态 |",
"| --- | --- | --- | --- | --- | --- | --- | --- | --- |",
]
if disks:
for disk in disks:
lines.append(
"| {drive} | {total} | {used} | {free} | {used_pct} | {peak24} | {read} | {write} | {status} |".format(
drive=_markdown_escape(disk.get("drive")),
total=_markdown_escape(_gb(disk.get("total_gb"))),
used=_markdown_escape(_gb(disk.get("used_gb"))),
free=_markdown_escape(_gb(disk.get("free_gb"))),
used_pct=_markdown_escape(_pct(disk.get("used_percent"))),
peak24=_markdown_escape(_pct(disk.get("used_peak_24h_percent"))),
read=_markdown_escape(disk.get("read_mb_s", 0)),
write=_markdown_escape(disk.get("write_mb_s", 0)),
status=_markdown_escape(disk.get("status") or "unknown"),
)
)
else:
lines.append("| - | - | - | - | - | - | - | - | 未返回磁盘明细 |")
lines.extend([
"",
"## 三、数据库巡检结果",
"",
"| 巡检项 | 结果值 | 状态 |",
"| --- | --- | --- |",
f"| 数据库服务 | {'在线' if oracle.get('online') else '异常'} | {'正常' if oracle.get('online') else '重大异常'} |",
f"| 活跃会话数 | {_markdown_escape(oracle.get('active_sessions') if oracle.get('active_sessions') is not None else '采集失败')} | {_markdown_escape(_status_level_from_percent(oracle.get('active_sessions'), 50, 100))} |",
f"| 用户会话总数 | {_markdown_escape(oracle.get('total_sessions') if oracle.get('total_sessions') is not None else '采集失败')} | {_markdown_escape(oracle.get('session_count_source') or '未采集')} |",
f"| Buffer Cache 命中率 | {_markdown_escape(_pct(cache_hit.get('buffer_cache_percent')) if cache_hit.get('buffer_cache_percent') is not None else '采集失败')} | {_markdown_escape(_cache_hit_level(cache_hit.get('buffer_cache_percent')))} |",
f"| Library Cache 命中率 | {_markdown_escape(_pct(cache_hit.get('library_cache_percent')) if cache_hit.get('library_cache_percent') is not None else '采集失败')} | {_markdown_escape(_cache_hit_level(cache_hit.get('library_cache_percent')))} |",
f"| 长事务 | {_ops_count(_find_ops(payload, 'long_transactions'))} 条 | {'普通异常' if _ops_count(_find_ops(payload, 'long_transactions')) else '正常'} |",
f"| 阻塞会话 | {_ops_count(_find_ops(payload, 'blocking_locks'))} 条 | {'普通异常' if _ops_count(_find_ops(payload, 'blocking_locks')) else '正常'} |",
f"| 当前慢 SQL | {_ops_count(_find_ops(payload, 'active_slow_sql'))} 条 | {'普通异常' if _ops_count(_find_ops(payload, 'active_slow_sql')) else '正常'} |",
"",
"## 四、异常明细",
"",
"| 等级 | 类型 | 说明 |",
"| --- | --- | --- |",
])
if alerts or failed:
for alert in alerts:
if isinstance(alert, dict):
lines.append(f"| {_markdown_escape(alert.get('level', 'warning'))} | {_markdown_escape(alert.get('title') or alert.get('type') or '异常')} | {_markdown_escape(alert.get('desc') or alert.get('value') or '')} |")
else:
lines.append(f"| warning | 异常 | {_markdown_escape(alert)} |")
for item in failed:
if isinstance(item, dict):
lines.append(f"| warning | {_markdown_escape(item.get('item') or '巡检项')} | {_markdown_escape(item.get('error') or item.get('reason') or '')} |")
else:
lines.append(f"| warning | 巡检项 | {_markdown_escape(item)} |")
else:
lines.append("| normal | 未发现异常 | 本次巡检未产生告警。 |")
lines.extend(["", "## 五、巡检结论", "", f"本次巡检总体状态:**{overall}**。"])
if missing:
lines.extend(["", "### 缺失授权", ""])
lines.extend(f"- {_markdown_escape(item)}" for item in missing)
lines.extend(["", "---", "", "生成来源:Oracle Jump Query Agent / 固定格式 Markdown 巡检报告", ""])
return "\n".join(lines)
def write_inspection_report_markdown(result: Dict[str, Any], path: str = "") -> str:
payload = _inspection_report_payload(result)
client = result.get("client_code") or payload.get("client_code") or get_server_id()
report_no = result.get("report_no") or f"inspection-report-{client}"
if not path:
out_dir = os.path.join(SKILL_DIR, "outputs")
os.makedirs(out_dir, exist_ok=True)
safe_name = re.sub(r"[^A-Za-z0-9_.-]+", "_", f"{report_no}-{client}")
path = os.path.join(out_dir, f"{safe_name}.md")
else:
parent = os.path.dirname(os.path.abspath(path))
if parent:
os.makedirs(parent, exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
f.write(render_inspection_report_markdown(result))
return os.path.abspath(path)
def _parse_report_cli_args(args):
as_json = "--json" in args
html_path = ""
markdown_path = None
clean = []
i = 0
while i < len(args):
arg = args[i]
if arg == "--json":
i += 1
continue
if arg == "--html":
html_path = ""
if i + 1 < len(args) and not args[i + 1].startswith("--"):
html_path = args[i + 1]
i += 2
continue
i += 1
continue
if arg in ("--markdown", "--md"):
markdown_path = ""
if i + 1 < len(args) and not args[i + 1].startswith("--"):
markdown_path = args[i + 1]
i += 2
continue
i += 1
continue
clean.append(arg)
i += 1
return clean, as_json, html_path, markdown_path
def _print_inspection_report(result: Dict[str, Any]):
if not result.get("success"):
print(f"Error: {result.get('error', 'unknown error')}")
if result.get("status_code"):
print(f"HTTP status: {result.get('status_code')}")
return
payload = _inspection_report_payload(result)
record = result.get("data") if isinstance(result.get("data"), dict) else {}
client = result.get("client_code") or payload.get("client_code") or record.get("client_code") or get_server_id()
stale = result.get("stale")
title_suffix = "历史快照" if stale else "实时报告"
print(f"# 服务器巡检报告 - {client} ({title_suffix})")
print()
if result.get("report_no"):
print(f"- 报告号: {result.get('report_no')}")
if record.get("id"):
print(f"- 归档 ID: {_fmt_report_number(record.get('id'))}")
if payload.get("collected_at"):
print(f"- 采集时间: {payload.get('collected_at')}")
elif record.get("collected_at"):
print(f"- 采集时间: {record.get('collected_at')}")
if payload.get("duration_ms") is not None:
print(f"- 耗时: {payload.get('duration_ms')} ms")
if result.get("archive_saved") is not None:
print(f"- 已归档: {result.get('archive_saved')}")
if stale is not None:
print(f"- 历史快照: {stale}")
memory = payload.get("memory") or {}
if memory:
print()
print("## 内存")
print("| 总量GB | 已用GB | 剩余GB | 使用率 | 1h峰值 | 24h峰值 |")
print("| --- | --- | --- | --- | --- | --- |")
print("| {total} | {used} | {free} | {used_pct}% | {peak1}% | {peak24}% |".format(
total=memory.get("total_gb", ""),
used=memory.get("used_gb", ""),
free=memory.get("free_gb", ""),
used_pct=memory.get("used_percent", ""),
peak1=memory.get("peak_1h_percent", ""),
peak24=memory.get("peak_24h_percent", ""),
))
disks = payload.get("disks") or []
if disks:
print()
print("## 磁盘")
print("| 分区 | 总量GB | 已用GB | 剩余GB | 使用率 | 24h峰值 | 状态 |")
print("| --- | --- | --- | --- | --- | --- | --- |")
for disk in disks:
print("| {drive} | {total} | {used} | {free} | {used_pct}% | {peak24}% | {status} |".format(
drive=disk.get("drive", ""),
total=disk.get("total_gb", ""),
used=disk.get("used_gb", ""),
free=disk.get("free_gb", ""),
used_pct=disk.get("used_percent", ""),
peak24=disk.get("used_peak_24h_percent", ""),
status=disk.get("status", ""),
))
alerts = payload.get("alerts") or []
if alerts:
print()
print("## 告警")
for alert in alerts:
title = alert.get("title") or alert.get("type") or "告警"
desc = alert.get("desc") or alert.get("value") or ""
level = alert.get("level") or ""
print(f"- [{level}] {title}: {desc}")
failed = payload.get("failed_items") or []
if failed:
print()
print("## 未完成项目")
for item in failed:
if isinstance(item, dict):
print(f"- {item.get('item', '')}: {item.get('error') or item.get('reason') or ''}")
else:
print(f"- {item}")
missing = payload.get("missing_permissions") or payload.get("missing_grants") or []
if missing:
print()
print("## 缺失授权")
for item in missing:
print(f"- {item}")
oracle = payload.get("oracle") or {}
oracle_text = json.dumps(oracle, ensure_ascii=False).lower()
needs_sys = any(token in oracle_text for token in ("unknown item", "ora-00942", "henlo_ora_monitor", "instance status"))
if needs_sys:
_print_inspection_sys_guidance()
def _print_inspection_sys_guidance():
"""Show the DBA-applied SYS function artifacts when inspection grants are missing."""
skill_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys_dir = os.path.join(skill_root, "references", "sys-functions")
files = ["henlo_ora_monitor.sql", "henlo_awr_export.sql"]
print()
print("## 需要 SYS 授权")
print("当前巡检检测到 Oracle SYS 函数或动态性能视图授权尚未完成。请由 DBA/SYS 手工执行以下留档 SQL,先将 <AGENT_SCHEMA> 替换为 Agent 实际登录账号。Skill 不会自动执行授权。")
print(f"SQL 留档目录: {sys_dir}")
for name in files:
path = os.path.join(sys_dir, name)
print(f"- {path}")
print("执行顺序:henlo_ora_monitor.sql;确认 Diagnostics Pack 授权后再执行 henlo_awr_export.sql。授权完成后重新生成巡检报告。")
def download_inspection_report_html(result: Dict[str, Any], path: str = "") -> str:
"""Download the fixed HTML rendered by transit-server; Skill only stores it locally."""
cfg = get_config() or {}
transit_url = str(cfg.get("transit_url") or get_transit_url() or TRANSIT_URL).rstrip("/")
html_url = str(result.get("html_url") or "")
if not html_url:
report_no = str(result.get("report_no") or result.get("data", {}).get("report_no") or "")
if report_no:
html_url = f"{transit_url}/api/inspection_report/{report_no}/html"
if html_url.startswith("/"):
html_url = transit_url + html_url
if not html_url:
raise RuntimeError("transit-server did not return html_url")
if result.get("archive_saved") is False:
raise RuntimeError("巡检报告尚未完成归档,暂时无法下载 HTML;请稍后重试")
response = None
last_error = ""
for attempt in range(5):
try:
response = requests.get(html_url, headers=make_headers(), timeout=30)
if response.status_code != 404:
response.raise_for_status()
break
last_error = "HTML archive is still pending"
except requests.RequestException as exc:
last_error = str(exc)
if attempt < 4:
time.sleep(1)
if response is None or response.status_code == 404:
raise RuntimeError(f"巡检报告 HTML 下载失败:{last_error},请确认 transit-server 已完成归档")
if not path:
out_dir = os.path.join(SKILL_DIR, "outputs")
os.makedirs(out_dir, exist_ok=True)
report_no = str(result.get("report_no") or "inspection-report")
path = os.path.join(out_dir, report_no + ".html")
else:
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
with open(path, "wb") as f:
f.write(response.content)
return path
def cmd_agent_versions(client_codes=None, all_clients=False, as_json=False):
cfg = get_config() or {}
clients = refresh_client_cache(cfg, quiet=True)
if not clients:
result = {"success": False, "error": "无法获取客户列表:登录已失效或本地没有客户缓存,请先执行 login"}
print(json.dumps(result, ensure_ascii=False, indent=2) if as_json else result["error"])
return
wanted = {str(c).upper() for c in (client_codes or [])}
rows = []
for client in clients:
code = get_client_code(client)
if not all_clients and code.upper() not in wanted:
continue
row = {"client_code": code, "title": get_client_title(client), "version": client.get("version", ""), "status": "offline"}
try:
result = query(code, "version", "", "", timeout=15, max_retries=0)
if result.get("success"):
data = result.get("data")
if isinstance(data, str):
data = json.loads(data)
if isinstance(data, dict) and data.get("version"):
row["version"] = data["version"]
row["status"] = "online"
except Exception as exc:
row["error"] = str(exc)
rows.append(row)
if as_json:
print(json.dumps({"success": True, "servers": rows}, ensure_ascii=False, indent=2))
else:
for row in rows:
print(f"{row['client_code']}: {row.get('version') or '-'} ({row['status']})")
def _output_inspection_report(result: Dict[str, Any], as_json: bool, html_path="", markdown_path=None):
if not result.get("success"):
if as_json:
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
_print_inspection_report(result)
return
artifacts = {"html": download_inspection_report_html(result, html_path or "")}
if markdown_path is not None:
artifacts["markdown"] = write_inspection_report_markdown(result, markdown_path)
if as_json:
output = dict(result)
output["artifacts"] = artifacts
print(json.dumps(output, ensure_ascii=False, indent=2))
return
print(f"HTML report: {artifacts['html']}")
if artifacts.get("markdown"):
print(f"Markdown report: {artifacts['markdown']}")
print()
_print_inspection_report(result)
def cmd_inspection_report(client_code: str = "", as_json: bool = False, timeout: int = 90, html_path="", markdown_path=None):
result = inspection_report(client_code, timeout=timeout)
_output_inspection_report(result, as_json, html_path, markdown_path)
def cmd_inspection_latest(client_code: str = "", as_json: bool = False, refresh: bool = False, html_path="", markdown_path=None):
result = inspection_report_latest(client_code, refresh=refresh)
_output_inspection_report(result, as_json, html_path, markdown_path)
def cmd_inspection_get(report_id: str, as_json: bool = False, html_path="", markdown_path=None):
result = inspection_report_get(report_id)
_output_inspection_report(result, as_json, html_path, markdown_path)
def get_upgrade_admin_token(cfg: Optional[Dict[str, Any]] = None) -> str:
"""Get transit-server agent update admin token."""
token = os.environ.get("ORACLE_JUMP_UPGRADE_ADMIN_TOKEN", "").strip()
if token:
return token
cfg = cfg or get_config() or {}
token = str(cfg.get("upgrade_admin_token", "") or "").strip()
if token:
return token
return ""
def agent_update(client_code: str = "", timeout: int = 300) -> Dict[str, Any]:
"""Trigger a remote agent self-update through transit-server."""
config = get_config() or {}
transit_url = config.get("transit_url", TRANSIT_URL)
if not ensure_logged_in(config):
return {"success": False, "error": "login required"}
if not client_code:
client_code = config.get("client_code") or config.get("server_id", "")
if not client_code:
return {"success": False, "error": "clientCode is required"}
if timeout <= 0 or timeout > 600:
timeout = 300
upgrade_token = get_upgrade_admin_token(config)
if not upgrade_token:
return {
"success": False,
"error": "upgrade admin token is not configured; set ORACLE_JUMP_UPGRADE_ADMIN_TOKEN or upgrade_admin_token",
}
payload = {"server_id": client_code, "timeout": timeout}
headers = make_headers()
headers["X-Upgrade-Admin-Token"] = upgrade_token
try:
resp = requests.post(
f"{transit_url}/api/admin/agent_update/trigger",
json=payload,
headers=headers,
timeout=timeout + 15,
)
if resp.status_code in (400, 401, 403, 404, 502, 503, 504):
try:
return {"success": False, "error": resp.json().get("error", resp.text), "status_code": resp.status_code}
except Exception:
return {"success": False, "error": resp.text, "status_code": resp.status_code}
resp.raise_for_status()
return resp.json()
except requests.exceptions.RequestException as e:
return {"success": False, "error": str(e)}
def cmd_agent_update(client_code: str = "", timeout: int = 300):
"""Print remote agent update trigger result."""
result = agent_update(client_code, timeout)
if not result.get("success"):
print(f"Error: {result.get('error', 'unknown error')}")
if result.get("status_code"):
print(f"HTTP status: {result.get('status_code')}")
return
print("Agent update triggered.")
print(json.dumps(result, ensure_ascii=False, indent=2))
DEFAULT_OPS_REPORT_ITEMS = [
"active_slow_sql",
"blocking_locks",
"long_transactions",
"tablespace",
"datafiles",
"undo",
"memory",
"io_waits",
"ora_errors",
"invalid_objects",
]
def oracle_ops(item: str, client_code: str = "", minutes: int = 60, top_n: int = 20, timeout: int = 30) -> Dict[str, Any]:
"""Call transit server oracle ops endpoint."""
config = get_config() or {}
transit_url = config.get("transit_url", TRANSIT_URL)
if not ensure_logged_in(config):
return {"success": False, "error": "login required"}
if not client_code:
client_code = config.get("client_code", "")
payload = {
"client_code": client_code,
"item": item,
"minutes": minutes,
"top_n": top_n,
"timeout": timeout,
}
try:
resp = requests.post(f"{transit_url}/api/oracle_ops", json=payload, headers=make_headers(), timeout=timeout + 5)
if resp.status_code in (400, 401, 403, 503):
try:
return {"success": False, "error": resp.json().get("error", resp.text)}
except Exception:
return {"success": False, "error": resp.text}
resp.raise_for_status()
return resp.json()
except requests.exceptions.RequestException as e:
return {"success": False, "error": str(e)}
def oracle_ops_report(client_code: str = "", items=None, minutes: int = 60, top_n: int = 20, per_item_timeout: int = 30) -> Dict[str, Any]:
"""Call transit server oracle ops report endpoint."""
config = get_config() or {}
transit_url = config.get("transit_url", TRANSIT_URL)
if not ensure_logged_in(config):
return {"success": False, "error": "login required"}
if not client_code:
client_code = config.get("client_code", "")
payload = {
"client_code": client_code,
"items": items or DEFAULT_OPS_REPORT_ITEMS,
"minutes": minutes,
"top_n": top_n,
"per_item_timeout": per_item_timeout,
}
try:
resp = requests.post(
f"{transit_url}/api/oracle_ops_report",
json=payload,
headers=make_headers(),
timeout=max(30, per_item_timeout * len(payload["items"]) + 10),
)
if resp.status_code in (400, 401, 403, 503):
try:
return {"success": False, "error": resp.json().get("error", resp.text)}
except Exception:
return {"success": False, "error": resp.text}
resp.raise_for_status()
return resp.json()
except requests.exceptions.RequestException as e:
return {"success": False, "error": str(e)}
def _markdown_table(columns, rows):
if not columns:
return "未发现异常"
lines = []
lines.append("| " + " | ".join(str(c) for c in columns) + " |")
lines.append("| " + " | ".join(["---"] * len(columns)) + " |")
for row in rows or []:
values = [str(v) if v is not None else "" for v in row]
while len(values) < len(columns):
values.append("")
lines.append("| " + " | ".join(values[:len(columns)]) + " |")
if len(lines) == 2:
lines.append("| " + " | ".join([""] * len(columns)) + " |")
return "\n".join(lines)
def _print_ops_item(data: Dict[str, Any]):
title = data.get("title") or data.get("item") or "Oracle 运维监控"
status = data.get("status", "")
print(f"## {title}")
if status:
print(f"状态: {status}")
if data.get("warnings"):
print("\n告警:")
for warning in data.get("warnings", []):
print(f"- {warning}")
if data.get("error"):
print(f"\n错误: {data.get('error')}")
print()
print(_markdown_table(data.get("columns", []), data.get("rows", [])))
if data.get("grant_sql_required") and data.get("grant_sql"):
print("\n### SYS 授权函数安装 SQL")
print("```sql")
print(data.get("grant_sql"))
print("```")
def cmd_ops(item: str, client_code: str = ""):
result = oracle_ops(item, client_code, timeout=60 if item == "history_top_sql" else 30)
if not result.get("success"):
print(f"Error: {result.get('error', 'unknown error')}")
return
_print_ops_item(result.get("data", {}) or {})
def cmd_ops_report(client_code: str = ""):
result = oracle_ops_report(client_code)
if not result.get("success"):
print(f"Error: {result.get('error', 'unknown error')}")
return
client = result.get("client_code", client_code or get_server_id())
summary = result.get("summary", {}) or {}
print(f"# Oracle 运维日报 - {client}")
print()
print("## 总览")
print(f"- 高风险: {summary.get('high', 0)}")
print(f"- 警告: {summary.get('warning', 0)}")
print(f"- 正常: {summary.get('normal', 0)}")
print(f"- 未完成: {summary.get('failed', 0)}")
for item in result.get("items", []) or []:
print()
_print_ops_item(item)
failed = result.get("failed_items", []) or []
if failed:
print("\n## 未完成项目")
print("| item | reason |")
print("| --- | --- |")
for f in failed:
print(f"| {f.get('item', '')} | {f.get('error', '')} |")
if result.get("grant_sql_required") and result.get("grant_sql"):
print("\n## SYS 授权函数")
print("当前库缺少或无法执行 `SYS.HENLO_ORA_MONITOR` / 当前账号 `HENLO_ORA_MONITOR`。")
print("如果有 SYS 账号,优先按 Option A 安装;如果当前 Agent 登录账号已有 v$/dba_/AWR 查询权限,可按 Option B 安装到当前账号。")
print("```sql")
print(result.get("grant_sql"))
print("```")
def cmd_sys_functions(schema: str = "", as_json: bool = False):
"""Print every SYS function SQL currently required by the skill."""
schema = (schema or "<AGENT_SCHEMA>").strip()
if schema != "<AGENT_SCHEMA>" and not re.fullmatch(r"[A-Za-z][A-Za-z0-9_$#]*", schema):
raise ValueError("schema must be a valid Oracle identifier")
root = os.path.join(SKILL_DIR, "references", "sys-functions")
files = sorted(f for f in os.listdir(root) if f.lower().endswith(".sql"))
items = []
for name in files:
with open(os.path.join(root, name), "r", encoding="utf-8-sig") as f:
items.append({"file": name, "sql": f.read().replace("<AGENT_SCHEMA>", schema)})
if as_json:
print(json.dumps({"success": True, "schema": schema, "files": items}, ensure_ascii=False, indent=2))
return
print("-- Oracle Jump Query SYS function deployment bundle")
print(f"-- Grant target schema: {schema}")
print("-- Execute as SYS. This command only generates SQL.\n")
for item in items:
print(f"-- ===== {item['file']} =====")
print(item["sql"].rstrip() + "\n")
def cmd_login(secret_key: str, client_code: str = ""):
"""
登录中转机并选择 client
流程:
1. 调中转机 /api/login,由中转机调用 BOS ts_login
2. 中转机验证 clientCode(客户服务器编号,可选)并签发 access_token
3. 保存到 config.json
"""
cfg = get_config() or {}
transit_url = cfg.get("transit_url", TRANSIT_URL)
try:
resp = requests.post(
f"{transit_url}/api/login",
json={"secret_key": secret_key, "client_code": client_code},
timeout=20,
)
if resp.status_code in (401, 403):
try:
err = resp.json().get("error", resp.text)
except Exception:
err = resp.text
print(f"❌ 登录失败: {err}")
return
resp.raise_for_status()
result = resp.json()
except requests.exceptions.RequestException as e:
print(f"❌ 登录失败,无法连接中转机: {e}")
return
if not result.get("success"):
print(f"❌ 登录失败: {result.get('error', '未知错误')}")
return
current_client = result.get("current_client", {})
client_list = result.get("client_list", [])
access_token = result.get("access_token", "")
if not access_token:
print("❌ 登录失败: 中转机未返回 access_token")
return
# 3. 保存配置
cfg.pop("secret_key", None)
cfg["access_token"] = access_token
cfg["expires_at"] = result.get("expires_at", "")
cfg["user_name"] = result.get("user", {}).get("name", "")
cfg["client_code"] = current_client.get("code", client_code)
cfg["client_title"] = current_client.get("title", "")
cfg["client_list"] = client_list
cfg["server_id"] = cfg["client_code"]
save_config(cfg)
print(f"✅ 登录成功")
print(f" 用户: {cfg.get('user_name', '')}")
print(f" 指向 client: {cfg['client_code']} ({cfg.get('client_title', '')})")
print(f" 过期时间: {cfg.get('expires_at', '')}")
if len(client_list) > 1:
print(f" 可切换 client: {', '.join(c['code'] for c in client_list)}")
def _device_key_path() -> str:
return os.path.join(os.path.expanduser("~"), ".oracle-jump-query", "device-key.json")
def _load_device_key() -> Dict[str, Any]:
with open(_device_key_path(), "r", encoding="utf-8") as f:
return json.load(f)
def _save_device_key(item: Dict[str, Any]) -> None:
path = _device_key_path()
os.makedirs(os.path.dirname(path), mode=0o700, exist_ok=True)
with open(path, "x", encoding="utf-8") as f:
json.dump(item, f, ensure_ascii=False, indent=2)
try:
os.chmod(path, 0o600)
except OSError:
pass
def _device_identity() -> Tuple[str, str]:
mac = ":".join(f"{(uuid.getnode() >> shift) & 0xff:02X}" for shift in range(40, -1, -8))
return hashlib.sha256(mac.encode("ascii")).hexdigest(), f"{mac[:8]}****{mac[-5:]}"
def cmd_device_register(device_name: str = ""):
"""Generate an Ed25519 key locally and register this terminal as PENDING."""
try:
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
except ImportError:
print("❌ 需要安装 cryptography: python -m pip install cryptography")
return
cfg = get_config() or {}
if not ensure_logged_in(cfg):
print("❌ 当前登录态已失效,请先执行 login")
return
try:
key = _load_device_key()
private = Ed25519PrivateKey.from_private_bytes(base64.b64decode(key["private_key"]))
except (OSError, KeyError, ValueError, TypeError):
private = Ed25519PrivateKey.generate()
public = private.public_key().public_bytes_raw()
key = {"device_id": "device-" + secrets.token_hex(12), "private_key": base64.b64encode(private.private_bytes_raw()).decode("ascii"), "public_key": base64.b64encode(public).decode("ascii"), "key_fingerprint": hashlib.sha256(public).hexdigest()}
try:
_save_device_key(key)
except FileExistsError:
# Another Skill process created the stable key first; never overwrite it.
key = _load_device_key()
mac_hash, mac_masked = _device_identity()
body = {"device_id": key["device_id"], "device_name": device_name or platform.node() or "Trusted device", "computer_name": platform.node(), "os_name": platform.platform(), "key_algorithm": "ED25519", "public_key": key["public_key"], "key_fingerprint": key["key_fingerprint"], "mac_hash": mac_hash, "mac_masked": mac_masked, "description": "Registered by oracle-jump-query skill"}
try:
resp = requests.post(f"{cfg.get('transit_url', TRANSIT_URL)}/api/device/register", json=body, headers=make_headers(), timeout=20)
data = resp.json()
except (requests.RequestException, ValueError) as exc:
print(f"❌ 可信设备注册失败: {exc}")
return
if not data.get("success"):
print(f"❌ 可信设备注册失败: {data.get('error', resp.text)}")
return
print("✅ 可信设备注册请求已提交,当前状态为 PENDING")
print(f" device_id: {key['device_id']}")
print(f" fingerprint: {key['key_fingerprint']}")
print(" 请在后台表单审批为 APPROVED 后执行 device_login")
def cmd_device_login(client_code: str = ""):
"""Use the locally stored Ed25519 key to perform trusted-device login."""
try:
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
key = _load_device_key()
private = Ed25519PrivateKey.from_private_bytes(base64.b64decode(key["private_key"]))
except (ImportError, OSError, KeyError, ValueError, TypeError) as exc:
print(f"❌ 本机可信设备密钥不可用: {exc}")
return
transit_url = (get_config() or {}).get("transit_url", TRANSIT_URL)
identity = {"device_id": key["device_id"], "key_fingerprint": key["key_fingerprint"]}
try:
challenge_resp = requests.post(f"{transit_url}/api/device/challenge", json=identity, timeout=20)
challenge_data = challenge_resp.json()
if not challenge_data.get("success"):
print(f"❌ 获取可信设备挑战失败: {challenge_data.get('error', challenge_resp.text)}")
return
challenge = challenge_data["challenge"]
signature = base64.b64encode(private.sign(challenge.encode("utf-8"))).decode("ascii")
resp = requests.post(f"{transit_url}/api/device/login", json={**identity, "challenge": challenge, "signature": signature, "client_code": client_code}, timeout=20)
data = resp.json()
except (requests.RequestException, ValueError) as exc:
print(f"❌ 可信设备登录失败: {exc}")
return
if not data.get("success"):
print(f"❌ 可信设备登录失败: {data.get('error', resp.text)}")
return
cfg = get_config() or {}
cfg.update({"access_token": data.get("access_token", ""), "expires_at": data.get("expires_at", ""), "user_name": data.get("user", {}).get("name", ""), "client_code": data.get("current_client", {}).get("code", client_code), "client_title": data.get("current_client", {}).get("title", "")})
cfg["server_id"] = cfg.get("client_code", "")
cfg["client_list"] = data.get("client_list", [])
save_config(cfg)
print("✅ 可信设备登录成功")
print(f" 用户: {cfg.get('user_name', '')}")
print(f" 指向 client: {cfg.get('client_code', '')}")
def cmd_logout():
"""登出,清除登录信息"""
cfg = get_config() or {}
token = cfg.get("access_token", "")
transit_url = cfg.get("transit_url", TRANSIT_URL)
if token:
try:
requests.post(f"{transit_url}/api/logout", headers=make_headers(), timeout=10)
except Exception:
pass
# 保留 transit_url 和 server_id,清除登录相关
keep_keys = {"transit_url", "server_id", "default_schema"}
new_cfg = {k: v for k, v in cfg.items() if k in keep_keys}
save_config(new_cfg)
print("✅ 已登出")
def cmd_status():
"""显示当前登录状态和选中的 client"""
config = get_config() or {}
transit_url = config.get("transit_url", TRANSIT_URL)
print("当前状态:")
print("-" * 40)
if not config.get("access_token"):
print(" 未登录,请使用 login <secretKey> [clientCode] 登录")
print("-" * 40)
return
if is_token_expired(config):
print(" 登录已过期,请重新执行 login <secretKey> [clientCode]")
print(f" 过期时间:{config.get('expires_at', '')}")
print("-" * 40)
return
try:
resp = requests.get(f"{transit_url}/api/me", headers=make_headers(), timeout=5)
if resp.status_code == 401:
print(" 中转机登录态已失效,请重新登录")
print("-" * 40)
return
resp.raise_for_status()
me = resp.json()
user = me.get("user", {})
client_list = me.get("client_list", [])
if sync_client_cache(config, client_list):
if me.get("expires_at"):
config["expires_at"] = me.get("expires_at")
if user.get("name"):
config["user_name"] = user.get("name")
save_config(config)
print(f" 用户:{user.get('name', config.get('user_name', '未知'))}")
print(f" client:{config.get('client_code', '未选择')} ({config.get('client_title', '')})")
print(f" 过期时间:{me.get('expires_at', config.get('expires_at', ''))}")
if client_list:
print(f" 可用 client:")
for cl in client_list:
marker = " <--" if cl["code"] == config.get("client_code") else ""
print(f" - {cl['code']} ({cl.get('title', '')}){marker}")
except Exception as e:
print(f" 无法从中转机获取状态: {e}")
# 显示 Agent 在线状态
try:
resp = requests.get(f"{transit_url}/api/clients", headers=make_headers(), timeout=5)
if resp.status_code == 200:
data = resp.json()
if sync_client_cache(config, data.get("clients", [])):
save_config(config)
current_code = config.get("client_code", "")
for cl in data.get("clients", []):
if cl.get("code") == current_code:
online = "在线" if cl.get("online") else "离线"
auth = "已授权" if cl.get("authorized") else "未授权"
print(f" Agent 状态:{online} | {auth}")
break
except Exception:
pass # 静默失败
print("-" * 40)
def cmd_switch(client_selector: str):
"""切换当前 client,支持 code 或 title/name 匹配。"""
cfg = get_config()
if not cfg:
print("❌ 未登录,请先使用 login 登录")
return
if not ensure_logged_in(cfg):
return
client_list = cfg.get("client_list", [])
matches, match_type = find_client_matches(client_list, client_selector)
if not matches:
print(f"未在本地缓存中找到 client: {client_selector},正在刷新 client 列表...")
client_list = refresh_client_cache(cfg)
matches, match_type = find_client_matches(client_list, client_selector)
if not matches:
print(f"❌ client '{client_selector}' 不在可用列表中")
print("可用 client:")
print_client_candidates(client_list)
return
if len(matches) > 1:
print(f"⚠️ client '{client_selector}' 匹配到多个结果,请指定更准确的 client code:")
print_client_candidates(matches)
return
matched_client = normalize_client(matches[0])
cfg["client_code"] = matched_client["code"]
cfg["client_title"] = matched_client.get("title", "")
cfg["server_id"] = matched_client["code"]
sync_client_cache(cfg, client_list)
save_config(cfg)
print(f"✅ 已切换到 client: {matched_client['code']} ({matched_client.get('title', '')})")
def make_headers():
"""构建 HTTP 请求头"""
headers = {"Content-Type": "application/json"}
token = ""
cfg = get_config()
if cfg:
token = cfg.get("access_token", "")
if not token:
token = AUTH_TOKEN
if token:
headers["Authorization"] = f"Bearer {token}"
return headers
def parse_expires_at(value: str) -> Optional[datetime]:
if not value:
return None
try:
return datetime.fromisoformat(value.replace("Z", "+00:00"))
except Exception:
return None
def is_token_expired(cfg: dict) -> bool:
expires_at = parse_expires_at(cfg.get("expires_at", ""))
if not expires_at:
return False
now = datetime.now(expires_at.tzinfo or timezone.utc)
return now >= expires_at
def _auto_device_login(cfg: Dict[str, Any]) -> bool:
"""Refresh the transit session with the locally approved trusted device."""
try:
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
key = _load_device_key()
private = Ed25519PrivateKey.from_private_bytes(base64.b64decode(key["private_key"]))
identity = {"device_id": key["device_id"], "key_fingerprint": key["key_fingerprint"]}
transit_url = cfg.get("transit_url", TRANSIT_URL)
challenge_data = requests.post(f"{transit_url}/api/device/challenge", json=identity, timeout=20).json()
if not challenge_data.get("success"):
return False
challenge = challenge_data["challenge"]
signature = base64.b64encode(private.sign(challenge.encode("utf-8"))).decode("ascii")
data = requests.post(f"{transit_url}/api/device/login", json={**identity, "challenge": challenge, "signature": signature, "client_code": cfg.get("client_code", "")}, timeout=20).json()
if not data.get("success") or not data.get("access_token"):
return False
current = data.get("current_client", {})
cfg.update({"access_token": data["access_token"], "expires_at": data.get("expires_at", ""), "user_name": data.get("user", {}).get("name", cfg.get("user_name", "")), "client_code": current.get("code", cfg.get("client_code", "")), "client_title": current.get("title", cfg.get("client_title", "")), "client_list": data.get("client_list", cfg.get("client_list", []))})
cfg["server_id"] = cfg.get("client_code", cfg.get("server_id", ""))
save_config(cfg)
print("✅ 中转 token 已通过可信设备自动续取")
return True
except (ImportError, OSError, KeyError, ValueError, TypeError, requests.RequestException):
return False
def ensure_logged_in(cfg: Optional[dict] = None) -> bool:
cfg = cfg or get_config() or {}
if not cfg.get("access_token") or is_token_expired(cfg):
if _auto_device_login(cfg):
return True
if not cfg.get("access_token"):
print("❌ 未登录,请先执行: python oracle_skill.py login <secretKey> [clientCode]")
return False
if is_token_expired(cfg):
print("❌ 登录已过期,请重新执行: python oracle_skill.py login <secretKey> [clientCode]")
print(" 注意:同一个 secretKey 在其他设备重新登录后,本设备也需要重新登录。")
return False
return True
def get_transit_url():
"""获取中转服务 URL"""
cfg = get_config()
if cfg and "transit_url" in cfg:
return cfg["transit_url"]
return TRANSIT_URL
def get_server_id():
"""获取默认服务器 ID"""
cfg = get_config()
if cfg and "server_id" in cfg:
return cfg["server_id"]
return DEFAULT_SERVER_ID
def query(server_id: str, action: str, schema: str = "", name: str = "",
timeout: int = DEFAULT_TIMEOUT, max_retries: int = MAX_RETRIES,
sql: str = "") -> Dict[str, Any]:
"""
发送查询请求到中转服务(带重试机制)
Args:
server_id: 服务器 ID
action: 操作类型(analyze_procedure, list_procedures, 等等)
schema: Schema 名称
name: 存储过程名称
timeout: 超时时间(秒)
max_retries: 最大重试次数
sql: SQL 语句(execute_query 用)
Returns:
dict: {"success": True/False, "data": ..., "error": ...}
"""
if not ensure_logged_in():
return {"success": False, "error": "login required"}
url = f"{get_transit_url()}/api/query"
payload = {
"server_id": server_id,
"action": action,
"schema": schema,
"name": name,
"timeout": timeout,
}
if sql:
payload["sql"] = sql
last_error = None
for attempt in range(max_retries + 1):
try:
logger.info(f"尝试 {attempt + 1}/{max_retries + 1}: POST {url}")
logger.debug(f"Payload: {payload}")
resp = requests.post(
url,
json=payload,
headers=make_headers(),
timeout=timeout + 3 # 额外 3 秒用于网络延迟(中转在 timeout 秒后即返回 504)
)
resp.raise_for_status()
result = resp.json()
logger.info(f"请求成功: {result.get('success', 'unknown')}")
return result
except requests.exceptions.ConnectionError as e:
last_error = f"无法连接中转服务 {url},请检查地址和端口: {e}"
logger.error(last_error)
except requests.exceptions.Timeout as e:
last_error = f"请求超时({timeout}秒),Agent 可能未响应或处理时间过长: {e}"
logger.error(last_error)
except requests.exceptions.HTTPError as e:
last_error = f"HTTP 错误: {e}"
logger.error(last_error)
except Exception as e:
last_error = f"未知错误: {e}"
logger.error(last_error)
# 如果不是最后一次尝试,则等待后重试
if attempt < max_retries:
logger.info(f"等待 {RETRY_DELAY} 秒后重试...")
time.sleep(RETRY_DELAY)
# 所有重试都失败了
return {"success": False, "error": last_error}
def list_servers(max_retries: int = MAX_RETRIES):
"""列出所有在线的 Agent 服务器(带重试)"""
if not ensure_logged_in():
return {"error": "login required"}
url = f"{get_transit_url()}/api/servers"
for attempt in range(max_retries + 1):
try:
logger.info(f"获取服务器列表: {url}")
resp = requests.get(url, headers=make_headers(), timeout=10)
resp.raise_for_status()
return resp.json()
except Exception as e:
if attempt < max_retries:
logger.warning(f"获取服务器列表失败,重试中... ({e})")
time.sleep(RETRY_DELAY)
else:
logger.error(f"获取服务器列表失败: {e}")
return {"error": str(e)}
def health_check(max_retries: int = MAX_RETRIES):
"""检查中转服务健康状态(带重试)"""
url = f"{get_transit_url()}/api/health"
for attempt in range(max_retries + 1):
try:
logger.info(f"健康检查: {url}")
resp = requests.get(url, timeout=5)
resp.raise_for_status()
return resp.json()
except Exception as e:
if attempt < max_retries:
logger.warning(f"健康检查失败,重试中... ({e})")
time.sleep(RETRY_DELAY)
else:
logger.error(f"健康检查失败: {e}")
return {"error": str(e)}
def check_agent_status(server_id: str) -> Dict[str, Any]:
"""
检查 Agent 状态
Returns:
dict: {"online": True/False, "server_id": ..., "error": ...}
"""
logger.info(f"检查 Agent 状态: {server_id}")
result = list_servers()
if "error" in result:
return {"online": False, "error": result["error"]}
servers = result.get("servers", [])
for s in servers:
if s.get("server_id") == server_id:
return {"online": True, "server_id": server_id, "info": s}
return {"online": False, "error": f"Agent {server_id} 不在线"}
def query_with_permission(user_id: str, table_id: str, sql: str,
timeout: int = DEFAULT_TIMEOUT) -> Dict[str, Any]:
"""
执行带权限过滤的查询
流程:
1. 调用 get_user_perm 获取用户权限
2. 解析权限SQL
3. 拼接到原SQL的WHERE条件
4. 执行最终SQL
Args:
user_id: 用户ID
table_id: 表ID (AD_TABLE.ID)
sql: 原始SQL查询
timeout: 超时时间
Returns:
dict: 查询结果
"""
logger.info(f"查询用户 {user_id} 对表 {table_id} 的权限...")
# 1. 获取权限
param = f"{user_id},{table_id}"
perm_result = query(get_server_id(), "get_user_perm", "BOSNDS3", param,
timeout=30, max_retries=MAX_RETRIES)
if not perm_result.get("success"):
return perm_result
# 2. 解析权限SQL
try:
perm_data = json.loads(perm_result.get("data", "{}"))
except json.JSONDecodeError:
return {"success": False, "error": "权限数据解析失败"}
perm_sql = perm_data.get("perm_sql", "")
has_restriction = perm_data.get("has_restriction", False)
logger.info(f"权限结果: has_restriction={has_restriction}")
# 3. 拼接SQL
if has_restriction and perm_sql:
# 改进的 WHERE 拼接:找到最外层 WHERE 的位置
# 1. 去除注释
import re
sql_no_comments = re.sub(r'--.*?$', '', sql, flags=re.MULTILINE)
sql_no_comments = re.sub(r'/\*.*?\*/', '', sql_no_comments, flags=re.DOTALL)
# 2. 找到最外层 WHERE(忽略子查询中的 WHERE)
# 简单方法:找到最后一个 FROM 后的 WHERE(如果存在)
# 更健壮的方法:使用 SQL 解析器,这里简化处理
sql_upper = sql_no_comments.upper()
# 检查是否有 WHERE
if "WHERE" in sql_upper:
# 找到 WHERE 位置
where_pos = sql_upper.rfind("WHERE")
# 检查 WHERE 是否在括号内(子查询)
# 统计 WHERE 前的 '(' 和 ')' 数量
before_where = sql_no_comments[:where_pos]
open_paren = before_where.count('(')
close_paren = before_where.count(')')
if open_paren == close_paren:
# 最外层 WHERE,追加 AND
final_sql = f"{sql} AND {perm_sql}"
else:
# WHERE 在子查询中,在末尾添加 WHERE
# 检查是否已有 GROUP BY / ORDER BY / LIMIT
if any(kw in sql_upper for kw in ["GROUP BY", "ORDER BY", "LIMIT", "FETCH"]):
# 在这些关键字前插入 WHERE
for kw in ["GROUP BY", "ORDER BY", "LIMIT", "FETCH"]:
if kw in sql_upper:
kw_pos = sql_upper.find(kw)
final_sql = f"{sql[:kw_pos]} WHERE {perm_sql} {sql[kw_pos:]}"
break
else:
# 在末尾添加 WHERE
final_sql = f"{sql} WHERE {perm_sql}"
else:
# 无 WHERE,添加 WHERE
# 检查是否已有 GROUP BY / ORDER BY / LIMIT
if any(kw in sql_upper for kw in ["GROUP BY", "ORDER BY", "LIMIT", "FETCH"]):
for kw in ["GROUP BY", "ORDER BY", "LIMIT", "FETCH"]:
if kw in sql_upper:
kw_pos = sql_upper.find(kw)
final_sql = f"{sql[:kw_pos]} WHERE {perm_sql} {sql[kw_pos:]}"
break
else:
# 在末尾添加 WHERE
final_sql = f"{sql} WHERE {perm_sql}"
logger.info(f"拼接权限条件: {perm_sql[:100]}...")
else:
# 全权限或无限制
final_sql = sql
logger.info("用户无权限限制,直接执行原SQL")
# 4. 执行最终SQL
logger.info(f"执行最终SQL: {final_sql[:100]}...")
return query(get_server_id(), "execute_query", "BOSNDS3", "",
timeout=timeout, max_retries=MAX_RETRIES,
sql=final_sql)
def print_result(result: Dict[str, Any]):
"""格式化输出结果"""
if result.get("success"):
data = result.get("data", "")
print(data)
else:
error = result.get("error", "未知错误")
print(f"❌ 错误: {error}", file=sys.stderr)
logger.error(f"查询失败: {error}")
def interactive_mode():
"""交互式模式"""
# Global variable declarations (must be at the beginning of the function)
global DEFAULT_TIMEOUT, MAX_RETRIES, DEFAULT_SERVER_ID
print("=" * 50)
print(" Oracle Jump Query - AI Skill (Improved)")
print(" 输入 help 查看命令,exit 退出")
print("=" * 50)
# Check transit server
health = health_check()
if "error" in health:
print(f"⚠️ 中转服务不可用: {health['error']}")
print("请检查 config.json 中的 transit_url 配置")
else:
agents = health.get('agents', 0)
print(f"✅ 中转服务正常 (在线 Agent: {agents})")
# 检查默认 Agent 是否在线
agent_status = check_agent_status(get_server_id())
if agent_status["online"]:
print(f"✅ Agent {get_server_id()} 在线")
else:
print(f"⚠️ Agent {get_server_id()} 不在线: {agent_status.get('error', '')}")
print()
while True:
try:
line = input("> ").strip()
except (EOFError, KeyboardInterrupt):
print("\n再见!")
break
if not line:
continue
parts = line.split()
cmd = parts[0].lower()
if cmd == "exit" or cmd == "quit":
print("再见!")
break
elif cmd == "help":
print("""
可用命令:
analyze <schema> <procedure> - 分析存储过程(源码+依赖+表+触发器)
list <schema> - 列出 schema 下的所有存储过程
source <schema> <procedure> - 获取存储过程源码
deps <schema> <procedure> - 获取存储过程依赖
tables <schema> <procedure> - 获取相关表结构
describe <schema> <table> - 查询表结构(列、索引、行数)
discover [schema] [domain] - 发现核心业务域(NL2SQL前置)
nl2sql [schema] [domain] - 生成 NL2SQL Schema 字典
query <SQL> - 执行 SELECT 查询(生产表必须带 WHERE 条件和行数限制)
perm <userId> <tableId> [col] - 查询用户数据权限
qperm <userId> <tableId> <SQL>- 带权限过滤的查询
login <secretKey> [clientCode]- 登录中转机 + 选择客户服务器
logout - 登出
status - 查看登录状态 + 当前 client
switch <clientCode|clientName>- 按 code 或名称切换 client
servers - 列出在线 Agent
health - 检查中转服务状态
tablespace / tablespaces - 查询表空间使用情况
inspection_report [client] - 生成并归档巡检报告,默认导出 HTML(可加 --markdown)
inspection_latest [client] - 查看最近报告并导出 HTML(可加 --refresh / --markdown)
inspection_get <id> - 查看指定归档报告并导出 HTML(可加 --markdown)
awr_status [client] - 查看 AWR 授权、权限和生成状态
awr_list [client] - 列出 Agent 已生成的 AWR 报告
awr_download <client> <date> - 下载指定日期的 AWR HTML(yyyyMMdd)
agent <server_id> - 检查指定 Agent 状态
server <server_id> - 切换目标服务器
timeout <seconds> - 设置默认超时时间
retry <count> - 设置重试次数
debug - 切换调试模式
version - 查看 Skill 版本号
exit - 退出
""")
elif cmd == "version":
print(f"Oracle Jump Query Skill v{VERSION}")
print(f"脚本路径: {__file__}")
print(f"Skill 目录: {SKILL_DIR}")
elif cmd == "login":
if len(parts) < 2:
print("用法: login <secretKey> [clientCode]")
continue
cmd_login(parts[1], parts[2] if len(parts) >= 3 else "")
elif cmd == "logout":
cmd_logout()
elif cmd == "status":
cmd_status()
elif cmd == "ops":
if len(parts) < 2:
print("用法: ops <item> [clientCode]")
continue
cmd_ops(parts[1], parts[2] if len(parts) >= 3 else "")
elif cmd == "ops_report":
cmd_ops_report(parts[1] if len(parts) >= 2 else "")
elif cmd == "sys_functions":
args = [p for p in parts[1:] if not p.startswith("--")]
cmd_sys_functions(args[0] if args else "", "--json" in parts)
elif cmd == "inspection_report":
args, as_json, html_path, markdown_path = _parse_report_cli_args(parts[1:])
cmd_inspection_report(args[0] if args else "", as_json=as_json, html_path=html_path, markdown_path=markdown_path)
elif cmd == "inspection_latest":
refresh = "--refresh" in parts
args, as_json, html_path, markdown_path = _parse_report_cli_args([p for p in parts[1:] if p != "--refresh"])
cmd_inspection_latest(args[0] if args else "", as_json=as_json, refresh=refresh, html_path=html_path, markdown_path=markdown_path)
elif cmd == "inspection_get":
if len(parts) < 2:
print("用法: inspection_get <id> [--json] [--html [path]] [--markdown [path]]")
continue
args, as_json, html_path, markdown_path = _parse_report_cli_args(parts[1:])
cmd_inspection_get(args[0], as_json=as_json, html_path=html_path, markdown_path=markdown_path)
elif cmd == "awr_status":
args = [p for p in parts[1:] if p != "--json"]
cmd_awr_status(args[0] if args else "", as_json="--json" in parts)
elif cmd == "awr_list":
args = [p for p in parts[1:] if p != "--json"]
cmd_awr_list(args[0] if args else "", as_json="--json" in parts)
elif cmd == "awr_download":
args = [p for p in parts[1:] if not p.startswith("--")]
if len(args) < 2:
print("用法: awr_download <clientCode> <yyyyMMdd> [--output <path>] [--json]")
continue
output_path = ""
if "--output" in parts:
output_index = parts.index("--output")
if output_index + 1 < len(parts):
output_path = parts[output_index + 1]
cmd_awr_download(args[0], args[1], output_path=output_path, as_json="--json" in parts)
elif cmd == "agent_update":
timeout = int(parts[2]) if len(parts) >= 3 and parts[2].isdigit() else 300
cmd_agent_update(parts[1] if len(parts) >= 2 else "", timeout)
elif cmd == "switch":
if len(parts) < 2:
print("用法: switch <clientCode|clientName>")
continue
cmd_switch(" ".join(parts[1:]))
elif cmd == "servers":
result = list_servers()
if "error" in result:
print(f"❌ {result['error']}")
else:
servers = result.get("servers", [])
if not servers:
print("没有在线的 Agent")
else:
for s in servers:
print(f" 🟢 {s['server_id']}")
elif cmd == "health":
result = health_check()
if "error" in result:
print(f"❌ {result['error']}")
else:
print(f"✅ 中转服务正常 (在线 Agent: {result.get('agents', 0)})")
elif cmd == "agent":
if len(parts) < 2:
status = check_agent_status(get_server_id())
else:
status = check_agent_status(parts[1])
if status["online"]:
print(f"✅ Agent {status['server_id']} 在线")
print(f" 信息: {status.get('info', {})}")
else:
print(f"❌ Agent 不在线: {status.get('error', '')}")
elif cmd == "server":
if len(parts) < 2:
print(f"当前服务器: {get_server_id()}")
else:
DEFAULT_SERVER_ID = parts[1]
print(f"切换到服务器: {parts[1]}")
elif cmd == "timeout":
if len(parts) < 2:
print(f"当前超时时间: {DEFAULT_TIMEOUT} 秒")
else:
try:
DEFAULT_TIMEOUT = int(parts[1])
print(f"超时时间设置为: {DEFAULT_TIMEOUT} 秒")
except ValueError:
print("用法: timeout <秒数>")
elif cmd == "retry":
if len(parts) < 2:
print(f"当前重试次数: {MAX_RETRIES}")
else:
try:
MAX_RETRIES = int(parts[1])
print(f"重试次数设置为: {MAX_RETRIES}")
except ValueError:
print("用法: retry <次数>")
elif cmd == "debug":
# 切换调试模式
if logger.level == logging.DEBUG:
logger.setLevel(logging.INFO)
print("调试模式: 关闭")
else:
logger.setLevel(logging.DEBUG)
print("调试模式: 开启")
elif cmd == "analyze":
if len(parts) < 3:
print("用法: analyze <schema> <procedure>")
continue
print(f"⏳ 正在分析 {parts[1]}.{parts[2]} (超时 {DEFAULT_TIMEOUT * 2} 秒)...")
result = query(get_server_id(), "analyze_procedure", parts[1], parts[2],
timeout=DEFAULT_TIMEOUT * 2, max_retries=MAX_RETRIES)
print_result(result)
elif cmd == "list":
if len(parts) < 2:
print("用法: list <schema>")
continue
print(f"⏳ 正在获取存储过程列表...")
result = query(get_server_id(), "list_procedures", parts[1],
timeout=30, max_retries=MAX_RETRIES)
print_result(result)
elif cmd == "source":
if len(parts) < 3:
print("用法: source <schema> <procedure>")
continue
print(f"⏳ 正在获取源码...")
result = query(get_server_id(), "get_source", parts[1], parts[2],
timeout=DEFAULT_TIMEOUT, max_retries=MAX_RETRIES)
print_result(result)
elif cmd == "deps":
if len(parts) < 3:
print("用法: deps <schema> <procedure>")
continue
print(f"⏳ 正在获取依赖...")
result = query(get_server_id(), "get_dependencies", parts[1], parts[2],
timeout=DEFAULT_TIMEOUT, max_retries=MAX_RETRIES)
print_result(result)
elif cmd == "tables":
if len(parts) < 3:
print("用法: tables <schema> <procedure>")
continue
print(f"⏳ 正在获取表结构...")
result = query(get_server_id(), "get_tables", parts[1], parts[2],
timeout=DEFAULT_TIMEOUT, max_retries=MAX_RETRIES)
print_result(result)
elif cmd == "describe":
if len(parts) < 3:
print("用法: describe <schema> <table>")
continue
print(f"⏳ 正在查询表 {parts[1]}.{parts[2]} ...")
result = query(get_server_id(), "describe_table", parts[1], parts[2],
timeout=DEFAULT_TIMEOUT, max_retries=MAX_RETRIES)
print_result(result)
elif cmd == "discover":
# NL2SQL: 发现核心业务域
schema = parts[1] if len(parts) > 1 else get_config().get("default_schema", "BOSNDS3")
domain = parts[2] if len(parts) > 2 else ""
print(f"⏳ 正在发现业务域 (schema={schema}, domain={domain or 'ALL'})...")
result = query(get_server_id(), "schema_discovery", schema, domain,
timeout=DEFAULT_TIMEOUT * 3, max_retries=MAX_RETRIES)
print_result(result)
elif cmd == "nl2sql":
# NL2SQL: 生成完整 Schema
schema = parts[1] if len(parts) > 1 else get_config().get("default_schema", "BOSNDS3")
domain = parts[2] if len(parts) > 2 else "RETAIL"
print(f"⏳ 正在生成 NL2SQL Schema (schema={schema}, domain={domain})...")
result = query(get_server_id(), "generate_nl2sql_schema", schema, domain,
timeout=DEFAULT_TIMEOUT * 3, max_retries=MAX_RETRIES)
print_result(result)
elif cmd == "query":
# execute_query: 执行任意 SELECT
if len(parts) < 2:
print("用法: query <SQL>")
print(" 示例: query SELECT * FROM M_RETAIL WHERE BILLDATE = 20260501 AND ROWNUM <= 20")
continue
# 支持可选 server_id 前缀:query henlo "SELECT ..."
known_servers = ["henlo", "renben", "HENLO", "RENBEN"]
sql_parts = parts[1:]
server_override = None
if parts[1].lower() in known_servers:
server_override = parts[1].upper()
sql_parts = parts[2:]
if not sql_parts:
print("用法: query <SQL>")
continue
sql = " ".join(sql_parts)
server_id = server_override if server_override else get_server_id()
print(f"⏳ 执行查询 [{server_id}]: {sql[:80]}...")
result = query(server_id, "execute_query", "BOSNDS3", "",
timeout=DEFAULT_TIMEOUT, max_retries=MAX_RETRIES,
sql=sql)
print_result(result)
elif cmd == "perm":
# get_user_perm: 获取用户权限
if len(parts) < 3:
print("用法: perm <userId> <tableId> [columnName]")
print(" 示例: perm 1015 12964")
print(" 示例: perm 940 12983 C_STORE_ID")
continue
user_id = parts[1]
table_id = parts[2]
col_name = parts[3] if len(parts) > 3 else ""
param = f"{user_id},{table_id}" if not col_name else f"{user_id},{table_id},{col_name}"
print(f"⏳ 查询用户 {user_id} 对表 {table_id} 的权限...")
result = query(get_server_id(), "get_user_perm", "BOSNDS3", param,
timeout=30, max_retries=MAX_RETRIES)
print_result(result)
elif cmd == "qperm":
# query_with_perm: 带权限过滤的查询
if len(parts) < 4:
print("用法: qperm <userId> <tableId> <SQL>")
print(" 示例: qperm 1015 12964 SELECT * FROM M_RETAIL WHERE BILLDATE=20260501")
continue
user_id = parts[1]
table_id = parts[2]
sql = " ".join(parts[3:])
print(f"⏳ 执行带权限过滤的查询 (用户={user_id}, 表={table_id})...")
result = query_with_permission(user_id, table_id, sql)
print_result(result)
elif cmd == "clients":
cmd_clients()
elif cmd in ("tablespace", "tablespaces"):
print(f"⏳ 正在查询表空间使用情况...")
result = query_tablespace(get_server_id())
if result.get("success"):
via_func = result.get("_via_func", False)
via_view = result.get("_via_view", False)
is_fallback = result.get("_fallback", False)
parse_failed = result.get("_parse_failed", False)
if via_func and parse_failed:
print("\n📊 表空间使用情况一览(通过 DBA 专用函数,原始格式):")
elif via_func:
print("\n📊 表空间使用情况一览(通过 DBA 专用函数):")
elif via_view:
print("\n📊 表空间使用情况一览(通过 DBA 授权 VIEW):")
elif is_fallback:
print("\n📊 表空间使用情况(用户视图 — 无DBA权限,仅显示名称和剩余空间):")
print("─" * 65)
else:
print("\n📊 表空间使用情况一览:")
print("─" * 110)
print(result.get("data", ""))
print("─" * 65 if is_fallback else "─" * 110)
if not is_fallback:
_check_ts_warnings(result.get("data", ""))
else:
print_result(result)
else:
print(f"未知命令: {cmd},输入 help 查看帮助")
# === capabilities command (version + command discovery) ===
_cap_parser = argparse.ArgumentParser(prog="oracle_skill", add_help=False)
_cap_subparsers = _cap_parser.add_subparsers(dest="subcmd")
p = _cap_subparsers.add_parser('capabilities')
p.add_argument('--json', action='store_true', help='Output pure JSON')
def cmd_capabilities(args):
"""
Return Skill version and all available command definitions (JSON format)
Args:
args: Command line arguments, including --json flag
"""
# Define all available commands
commands = [
{
"name": "analyze",
"description": "Analyze stored procedure: source + dependency tables + nested procedures + triggers",
"parameters": {
"type": "object",
"properties": {
"schema": {
"type": "string",
"description": "Database schema name (e.g. bosnds3)"
},
"procedure": {
"type": "string",
"description": "Stored procedure name (e.g. M_RETAIL_SUBMIT)"
}
},
"required": ["schema", "procedure"]
}
},
{
"name": "list",
"description": "List all stored procedures in specified schema",
"parameters": {
"type": "object",
"properties": {
"schema": {
"type": "string",
"description": "Database schema name"
}
},
"required": ["schema"]
}
},
{
"name": "source",
"description": "Get stored procedure source code",
"parameters": {
"type": "object",
"properties": {
"schema": {
"type": "string",
"description": "Database schema name"
},
"procedure": {
"type": "string",
"description": "Stored procedure name"
}
},
"required": ["schema", "procedure"]
}
},
{
"name": "describe",
"description": "Query table structure (field names, types, comments)",
"parameters": {
"type": "object",
"properties": {
"schema": {
"type": "string",
"description": "Database schema name"
},
"table": {
"type": "string",
"description": "Table name"
}
},
"required": ["schema", "table"]
}
},
{
"name": "search",
"description": "Search tables or stored procedures (supports fuzzy search)",
"parameters": {
"type": "object",
"properties": {
"schema": {
"type": "string",
"description": "Database schema name"
},
"keyword": {
"type": "string",
"description": "Search keyword"
}
},
"required": ["schema", "keyword"]
}
},
{
"name": "query",
"description": "Execute SELECT query (only allows SELECT statements; production table queries must include WHERE filters and row limits)",
"parameters": {
"type": "object",
"properties": {
"schema": {
"type": "string",
"description": "Database schema name"
},
"sql": {
"type": "string",
"description": "SQL SELECT query statement. For production tables, include explicit WHERE filters and ROWNUM/page limits; do not query whole tables."
}
},
"required": ["schema", "sql"]
}
},
{
"name": "discover",
"description": "Schema exploration: list all tables or search specific tables",
"parameters": {
"type": "object",
"properties": {
"schema": {
"type": "string",
"description": "Database schema name"
},
"filter": {
"type": "string",
"description": "Optional, table name filter condition (e.g. M_%)"
}
},
"required": ["schema"]
}
},
{
"name": "nl2sql",
"description": "Natural language to SQL (requires DeepSeek API)",
"parameters": {
"type": "object",
"properties": {
"schema": {
"type": "string",
"description": "Database schema name"
},
"question": {
"type": "string",
"description": "Natural language question (e.g. 'Query sales of Huadu Store 2 in May')"
}
},
"required": ["schema", "question"]
}
},
{
"name": "perm",
"description": "Get user data permission SQL (via stored procedure get_userspermsql)",
"parameters": {
"type": "object",
"properties": {
"schema": {
"type": "string",
"description": "Database schema name"
},
"userid": {
"type": "string",
"description": "User ID"
},
"table": {
"type": "string",
"description": "Table name (e.g. M_RETAIL)"
}
},
"required": ["schema", "userid", "table"]
}
},
{
"name": "qperm",
"description": "Quick permission SQL retrieval (simplified version)",
"parameters": {
"type": "object",
"properties": {
"schema": {
"type": "string",
"description": "Database schema name"
},
"userid": {
"type": "string",
"description": "User ID"
},
"table": {
"type": "string",
"description": "Table name"
}
},
"required": ["schema", "userid", "table"]
}
},
{
"name": "login",
"description": "Login to transit server and select a client",
"parameters": {
"type": "object",
"properties": {
"secretKey": {
"type": "string",
"description": "BOS secret key for authentication"
},
"clientCode": {
"type": "string",
"description": "Optional client/server code to select (must be in user's clientList). If omitted, the transit server selects the first available client."
}
},
"required": ["secretKey"]
}
},
{
"name": "logout",
"description": "Logout and clear login credentials",
"parameters": {
"type": "object",
"properties": {}
}
},
{
"name": "status",
"description": "Show current login status and selected client",
"parameters": {
"type": "object",
"properties": {}
}
},
{
"name": "clients",
"description": "Refresh the current user's authorized client list and online status using the existing transit-server access token.",
"parameters": {
"type": "object",
"properties": {}
}
},
{
"name": "ops",
"description": "Run one Oracle daily operations monitor item through transit server and agent.",
"parameters": {
"type": "object",
"properties": {
"item": {
"type": "string",
"description": "Monitor item, such as active_slow_sql, blocking_locks, tablespace, memory, io_waits, ora_errors"
},
"clientCode": {
"type": "string",
"description": "Optional client/server code. If omitted, the current client is used."
}
},
"required": ["item"]
}
},
{
"name": "ops_report",
"description": "Generate an Oracle daily operations report for a client.",
"parameters": {
"type": "object",
"properties": {
"clientCode": {
"type": "string",
"description": "Optional client/server code. If omitted, the current client is used."
}
},
"required": []
}
},
{
"name": "inspection_report",
"description": "Generate and archive a server inspection report for a client.",
"parameters": {
"type": "object",
"properties": {
"clientCode": {
"type": "string",
"description": "Optional client/server code. If omitted, the current client is used."
},
"json": {
"type": "boolean",
"description": "Output raw JSON instead of Markdown summary."
},
"html": {
"type": "string",
"description": "HTML output path. HTML is generated by default; use an empty value to write to the default outputs directory."
},
"markdown": {
"type": "string",
"description": "Optional Markdown output path. Use an empty value to write to the default outputs directory."
}
},
"required": []
}
},
{
"name": "inspection_latest",
"description": "Read the latest archived server inspection report for a client.",
"parameters": {
"type": "object",
"properties": {
"clientCode": {
"type": "string",
"description": "Optional client/server code. If omitted, the current client is used."
},
"refresh": {
"type": "boolean",
"description": "Generate a fresh report before returning it."
},
"json": {
"type": "boolean",
"description": "Output raw JSON instead of Markdown summary."
},
"html": {
"type": "string",
"description": "HTML output path. HTML is generated by default; use an empty value to write to the default outputs directory."
},
"markdown": {
"type": "string",
"description": "Optional Markdown output path. Use an empty value to write to the default outputs directory."
}
},
"required": []
}
},
{
"name": "inspection_get",
"description": "Read an archived server inspection report by report ID.",
"parameters": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "TS_INSPECTION_REPORT.ID"
},
"json": {
"type": "boolean",
"description": "Output raw JSON instead of Markdown summary."
},
"html": {
"type": "string",
"description": "HTML output path. HTML is generated by default; use an empty value to write to the default outputs directory."
},
"markdown": {
"type": "string",
"description": "Optional Markdown output path. Use an empty value to write to the default outputs directory."
}
},
"required": ["id"]
}
},
{
"name": "agent_update",
"description": "Trigger a remote Agent self-update through transit-server. The Agent downloads the upgrade package from OSS.",
"parameters": {
"type": "object",
"properties": {
"clientCode": {
"type": "string",
"description": "Optional client/server code. If omitted, the current client is used."
},
"timeout": {
"type": "integer",
"description": "Optional timeout seconds, default 300, max 600."
}
},
"required": []
}
},
{
"name": "device_register",
"description": "Generate a local Ed25519 device key and submit a trusted-device registration request.",
"parameters": {"type": "object", "properties": {"deviceName": {"type": "string", "description": "Optional device display name."}}},
"required": []
},
{
"name": "device_login",
"description": "Authenticate through an approved trusted device using a local Ed25519 private key.",
"parameters": {"type": "object", "properties": {"clientCode": {"type": "string", "description": "Optional client/server code."}}},
"required": []
},
{
"name": "awr_status",
"description": "Read AWR authorization, readiness and latest generated report status without generating a report.",
"parameters": {
"type": "object",
"properties": {
"clientCode": {"type": "string", "description": "Optional client/server code."},
"json": {"type": "boolean", "description": "Output machine-readable JSON."}
},
"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",
"description": "List AWR reports already generated and stored by the target Agent.",
"parameters": {
"type": "object",
"properties": {
"clientCode": {"type": "string", "description": "Optional client/server code."},
"json": {"type": "boolean", "description": "Output machine-readable JSON."}
},
"required": []
}
},
{
"name": "awr_download",
"description": "Download an Agent-generated AWR HTML report through transit-server without local rendering.",
"parameters": {
"type": "object",
"properties": {
"clientCode": {"type": "string", "description": "Client/server code."},
"date": {"type": "string", "description": "AWR report date in yyyyMMdd format."},
"output": {"type": "string", "description": "Optional local output path."}
},
"required": ["clientCode", "date"]
}
},
{
"name": "switch",
"description": "Switch to a different client by code or title/name",
"parameters": {
"type": "object",
"properties": {
"clientCode": {
"type": "string",
"description": "Client code or title/name to switch to"
}
},
"required": ["clientCode"]
}
},
{
"name": "capabilities",
"description": "Show Skill version and all available command definitions",
"parameters": {
"type": "object",
"properties": {
"json": {
"type": "boolean",
"description": "Whether to output pure JSON format"
}
}
}
}
]
# Build return result
result = {
"version": VERSION,
"skill_name": "oracle-jump-query",
"description": "Oracle Jump Query Skill - Query remote Oracle database via transit server",
"transit_server": "https://ts.henlo.net",
"default_schema": "bosnds3",
"commands": commands,
"metadata": {
"total_commands": len(commands),
"supported_actions": [
"analyze", "list", "source", "describe",
"search", "query", "discover", "nl2sql",
"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", "log_info", "log_tail", "log_search", "log_enable", "log_disable", "agent_update", "capabilities"
],
"python_version": "3.6+",
"dependencies": ["requests"]
}
}
# Output format
if hasattr(args, 'json') and args.json:
# Pure JSON output (no log interference)
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
# Friendly format output
print("Oracle Jump Query Skill - Capabilities")
print("=" * 60)
print("Version: " + result['version'])
print("Transit Server: " + result['transit_server'])
print("Default Schema: " + result['default_schema'])
print("\nAvailable Commands (" + str(result['metadata']['total_commands']) + "):")
print("-" * 60)
for i, cmd in enumerate(commands, 1):
print(str(i) + ". " + cmd['name'])
print(" Description: " + cmd['description'])
if 'parameters' in cmd and 'properties' in cmd['parameters']:
params = cmd['parameters']['properties']
if params:
print(" Parameters: " + ', '.join(params.keys()))
print()
print("=" * 60)
print("For machine-readable output, use: --json")
def main():
# Global variable declarations for main function
global DEFAULT_TIMEOUT, MAX_RETRIES, DEFAULT_SERVER_ID
if len(sys.argv) == 1:
interactive_mode()
return
cmd = sys.argv[1].lower()
if cmd == "servers":
result = list_servers()
print(json.dumps(result, indent=2, ensure_ascii=False))
elif cmd == "health":
result = health_check()
print(json.dumps(result, indent=2, ensure_ascii=False))
elif cmd == "version":
print(f"Oracle Jump Query Skill v{VERSION}")
if len(sys.argv) >= 3 and sys.argv[2] == "agent":
args = [a for a in sys.argv[3:] if a not in ("--all", "--json")]
cmd_agent_versions(args, all_clients="--all" in sys.argv[3:], as_json="--json" in sys.argv[3:])
return
elif cmd == "agent":
if len(sys.argv) >= 3:
status = check_agent_status(sys.argv[2])
else:
status = check_agent_status(get_server_id())
print(json.dumps(status, indent=2, ensure_ascii=False))
elif cmd == "analyze" and len(sys.argv) >= 4:
result = query(get_server_id(), "analyze_procedure", sys.argv[2], sys.argv[3],
timeout=DEFAULT_TIMEOUT * 2, max_retries=MAX_RETRIES)
print_result(result)
elif cmd == "list" and len(sys.argv) >= 3:
result = query(get_server_id(), "list_procedures", sys.argv[2],
timeout=30, max_retries=MAX_RETRIES)
print_result(result)
elif cmd == "source" and len(sys.argv) >= 4:
result = query(get_server_id(), "get_source", sys.argv[2], sys.argv[3],
timeout=DEFAULT_TIMEOUT, max_retries=MAX_RETRIES)
print_result(result)
elif cmd == "deps" and len(sys.argv) >= 4:
result = query(get_server_id(), "get_dependencies", sys.argv[2], sys.argv[3],
timeout=DEFAULT_TIMEOUT, max_retries=MAX_RETRIES)
print_result(result)
elif cmd == "tables" and len(sys.argv) >= 4:
result = query(get_server_id(), "get_tables", sys.argv[2], sys.argv[3],
timeout=DEFAULT_TIMEOUT, max_retries=MAX_RETRIES)
print_result(result)
elif cmd == "describe" and len(sys.argv) >= 4:
result = query(get_server_id(), "describe_table", sys.argv[2], sys.argv[3],
timeout=DEFAULT_TIMEOUT, max_retries=MAX_RETRIES)
print_result(result)
elif cmd == "discover" and len(sys.argv) >= 2:
schema = sys.argv[2] if len(sys.argv) > 2 else "BOSNDS3"
domain = sys.argv[3] if len(sys.argv) > 3 else ""
result = query(get_server_id(), "schema_discovery", schema, domain,
timeout=DEFAULT_TIMEOUT * 3, max_retries=MAX_RETRIES)
print_result(result)
elif cmd == "nl2sql" and len(sys.argv) >= 2:
schema = sys.argv[2] if len(sys.argv) > 2 else "BOSNDS3"
domain = sys.argv[3] if len(sys.argv) > 3 else "RETAIL"
result = query(get_server_id(), "generate_nl2sql_schema", schema, domain,
timeout=DEFAULT_TIMEOUT * 3, max_retries=MAX_RETRIES)
print_result(result)
elif cmd == "query" and len(sys.argv) >= 2:
sql = " ".join(sys.argv[2:])
result = query(get_server_id(), "execute_query", "BOSNDS3", "",
timeout=DEFAULT_TIMEOUT, max_retries=MAX_RETRIES,
sql=sql)
print_result(result)
elif cmd == "perm" and len(sys.argv) >= 4:
user_id = sys.argv[2]
table_id = sys.argv[3]
col_name = sys.argv[4] if len(sys.argv) > 4 else ""
param = f"{user_id},{table_id}" if not col_name else f"{user_id},{table_id},{col_name}"
result = query(get_server_id(), "get_user_perm", "BOSNDS3", param,
timeout=30, max_retries=MAX_RETRIES)
print_result(result)
elif cmd == "qperm" and len(sys.argv) >= 5:
user_id = sys.argv[2]
table_id = sys.argv[3]
sql = " ".join(sys.argv[4:])
result = query_with_permission(user_id, table_id, sql)
print_result(result)
elif cmd == "login":
if len(sys.argv) >= 3:
cmd_login(sys.argv[2], sys.argv[3] if len(sys.argv) >= 4 else "")
else:
cfg = get_config() or {}
if not ensure_logged_in(cfg):
print("❌ 未检测到已授权可信设备,请执行 login <secretKey> [clientCode]")
elif cmd == "logout":
cmd_logout()
elif cmd == "device_register":
cmd_device_register(" ".join(sys.argv[2:]) if len(sys.argv) >= 3 else "")
elif cmd == "device_login":
cmd_device_login(sys.argv[2] if len(sys.argv) >= 3 else "")
elif cmd == "status":
cmd_status()
elif cmd == "ops" and len(sys.argv) >= 3:
cmd_ops(sys.argv[2], sys.argv[3] if len(sys.argv) >= 4 else "")
elif cmd == "ops_report":
cmd_ops_report(sys.argv[2] if len(sys.argv) >= 3 else "")
elif cmd == "sys_functions":
args = [p for p in sys.argv[2:] if not p.startswith("--")]
cmd_sys_functions(args[0] if args else "", "--json" in sys.argv[2:])
elif cmd == "inspection_report":
args, as_json, html_path, markdown_path = _parse_report_cli_args(sys.argv[2:])
cmd_inspection_report(args[0] if args else "", as_json=as_json, html_path=html_path, markdown_path=markdown_path)
elif cmd == "inspection_latest":
refresh = "--refresh" in sys.argv[2:]
args, as_json, html_path, markdown_path = _parse_report_cli_args([p for p in sys.argv[2:] if p != "--refresh"])
cmd_inspection_latest(args[0] if args else "", as_json=as_json, refresh=refresh, html_path=html_path, markdown_path=markdown_path)
elif cmd == "inspection_get" and len(sys.argv) >= 3:
args, as_json, html_path, markdown_path = _parse_report_cli_args(sys.argv[2:])
cmd_inspection_get(args[0], as_json=as_json, html_path=html_path, markdown_path=markdown_path)
elif cmd == "awr_status":
args = [p for p in sys.argv[2:] if p != "--json"]
cmd_awr_status(args[0] if args else "", as_json="--json" in sys.argv[2:])
elif cmd == "awr_list":
args = [p for p in sys.argv[2:] if p != "--json"]
cmd_awr_list(args[0] if args else "", as_json="--json" in sys.argv[2:])
elif cmd == "awr_download" and len(sys.argv) >= 4:
output_path = ""
if "--output" in sys.argv[4:]:
output_index = sys.argv.index("--output")
if output_index + 1 < len(sys.argv):
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:])
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":
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)
elif cmd == "switch" and len(sys.argv) >= 3:
cmd_switch(" ".join(sys.argv[2:]))
elif cmd == "clients":
cmd_clients()
elif cmd in ("tablespace", "tablespaces"):
print(f"⏳ 正在查询表空间使用情况...")
result = query_tablespace(get_server_id())
if result.get("success"):
via_func = result.get("_via_func", False)
via_view = result.get("_via_view", False)
is_fallback = result.get("_fallback", False)
parse_failed = result.get("_parse_failed", False)
if via_func and parse_failed:
print("\n📊 表空间使用情况一览(通过 DBA 专用函数,原始格式):")
elif via_func:
print("\n📊 表空间使用情况一览(通过 DBA 专用函数):")
elif via_view:
print("\n📊 表空间使用情况一览(通过 DBA 授权 VIEW):")
elif is_fallback:
print("\n📊 表空间使用情况(用户视图 — 无DBA权限,仅显示名称和剩余空间):")
print("─" * 65)
else:
print("\n📊 表空间使用情况一览:")
print("─" * 110)
print(result.get("data", ""))
print("─" * 65 if is_fallback else "─" * 110)
if not is_fallback:
_check_ts_warnings(result.get("data", ""))
else:
print_result(result)
elif cmd == "capabilities":
args = _cap_parser.parse_args(["capabilities"] + sys.argv[2:])
cmd_capabilities(args)
return
else:
print(__doc__)
if __name__ == "__main__":
main()