初始化
This commit is contained in:
1 parent
34474bd5fc
commit
b638b541d0
3008 files changed
+837699
No files matched your search
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Authentication helper for xdfx skill scripts.
|
||||
|
||||
The caller should not pass tokens through command-line arguments. This module
|
||||
owns the fixed admin login flow and returns a Bearer token before business
|
||||
interfaces are called.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import ssl
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class AuthResult:
|
||||
token: str
|
||||
url: str
|
||||
status: int | None
|
||||
raw: str
|
||||
|
||||
|
||||
class AuthError(RuntimeError):
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
*,
|
||||
url: str,
|
||||
status: int | None = None,
|
||||
response: str = "",
|
||||
exception: BaseException | None = None,
|
||||
) -> None:
|
||||
super().__init__(message)
|
||||
self.url = url
|
||||
self.status = status
|
||||
self.response = response
|
||||
self.exception = exception
|
||||
|
||||
|
||||
def auth_header_value(token: str) -> str:
|
||||
token = token.strip()
|
||||
return token if token.lower().startswith("bearer ") else f"Bearer {token}"
|
||||
|
||||
|
||||
def redact_url(url: str) -> str:
|
||||
return re.sub(r"([?&](?:password|token|authToken|access_token)=)[^&]+", r"\1***", url, flags=re.IGNORECASE)
|
||||
|
||||
|
||||
def ssl_context(*, verify_ssl: bool) -> ssl.SSLContext | None:
|
||||
if verify_ssl:
|
||||
return None
|
||||
return ssl._create_unverified_context()
|
||||
|
||||
|
||||
def build_login_url(api_base: str, path: str) -> str:
|
||||
return path if re.match(r"^https?://", path, flags=re.IGNORECASE) else api_base.rstrip("/") + "/" + path.lstrip("/")
|
||||
|
||||
|
||||
def extract_access_token(data: Any) -> str | None:
|
||||
if isinstance(data, dict):
|
||||
for key in ("access_token", "accessToken", "token", "jwt", "id_token", "authorization", "Authorization"):
|
||||
value = data.get(key)
|
||||
if isinstance(value, str) and value.strip():
|
||||
return value.strip()
|
||||
for value in data.values():
|
||||
nested = extract_access_token(value)
|
||||
if nested:
|
||||
return nested
|
||||
elif isinstance(data, list):
|
||||
for value in data:
|
||||
nested = extract_access_token(value)
|
||||
if nested:
|
||||
return nested
|
||||
return None
|
||||
|
||||
|
||||
def parse_json_loose(text: str) -> Any:
|
||||
try:
|
||||
return json.loads(text)
|
||||
except Exception:
|
||||
pass
|
||||
match = re.search(r"[\[{]", text or "")
|
||||
if not match:
|
||||
return {}
|
||||
start = match.start()
|
||||
for end in range(len(text), start, -1):
|
||||
try:
|
||||
return json.loads(text[start:end])
|
||||
except Exception:
|
||||
continue
|
||||
return {}
|
||||
|
||||
|
||||
def fetch_access_token(
|
||||
*,
|
||||
api_base: str,
|
||||
username: str = "admin",
|
||||
password: str = "ch@user",
|
||||
login_path: str = "/api/auth/login",
|
||||
timeout: float = 90,
|
||||
verify_ssl: bool = False,
|
||||
) -> AuthResult:
|
||||
"""Login once and return the extracted access token."""
|
||||
url = build_login_url(api_base, login_path)
|
||||
payload = {"username": username, "password": password}
|
||||
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout, context=ssl_context(verify_ssl=verify_ssl)) as resp:
|
||||
raw = resp.read().decode("utf-8", errors="replace")
|
||||
parsed = parse_json_loose(raw)
|
||||
token = extract_access_token(parsed)
|
||||
if not token:
|
||||
raise AuthError(
|
||||
"登录接口已响应但未找到 access_token",
|
||||
url=redact_url(url),
|
||||
status=getattr(resp, "status", None),
|
||||
response=raw,
|
||||
)
|
||||
return AuthResult(token=token, url=redact_url(url), status=getattr(resp, "status", None), raw=raw)
|
||||
except urllib.error.HTTPError as exc:
|
||||
body = exc.read().decode("utf-8", errors="replace") if exc.fp else ""
|
||||
raise AuthError(
|
||||
"登录接口返回错误",
|
||||
url=redact_url(url),
|
||||
status=exc.code,
|
||||
response=body,
|
||||
exception=exc,
|
||||
) from exc
|
||||
except AuthError:
|
||||
raise
|
||||
except Exception as exc: # noqa: BLE001 - auth errors must preserve detail for user feedback.
|
||||
raise AuthError(
|
||||
"登录接口调用异常",
|
||||
url=redact_url(url),
|
||||
response=str(exc),
|
||||
exception=exc,
|
||||
) from exc
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the xdfx flow one step at a time.
|
||||
|
||||
The heavy business logic intentionally stays in xdfx_flow.py. This wrapper only
|
||||
keeps "full flow" orchestration readable and preserves per-step feedback: every
|
||||
subprocess line is flushed immediately, so OP/SAVE/ERROR/STEP feedback remains
|
||||
visible to the calling agent.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
if hasattr(sys.stderr, "reconfigure"):
|
||||
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
||||
|
||||
STEPS = ("step1", "step2", "step3", "step4", "step5", "step6")
|
||||
|
||||
STEP_LABELS = {
|
||||
"step1": "受众目标分析与入库",
|
||||
"step2": "关键传播节点分析与入库",
|
||||
"step3": "受众脆弱点分析与入库",
|
||||
"step4": "传播节点脆弱点分析与入库",
|
||||
"step5": "受众策略分析与入库",
|
||||
"step6": "传播节点策略分析与入库",
|
||||
}
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
"""Parse only wrapper-owned flags; pass all other flags to xdfx_flow.py."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="按六个步骤运行 xdfx_flow.py,并实时转发中文反馈日志。",
|
||||
add_help=True,
|
||||
)
|
||||
parser.add_argument("--from-step", choices=STEPS, default="step1")
|
||||
parser.add_argument("--to-step", choices=STEPS, default="step6")
|
||||
parser.add_argument(
|
||||
"--continue-on-error",
|
||||
action="store_true",
|
||||
help="某一步返回非零退出码时仍继续执行后续步骤。",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def selected_steps(first: str, last: str) -> list[str]:
|
||||
"""Return an inclusive step slice for resume/partial reruns."""
|
||||
start = STEPS.index(first)
|
||||
end = STEPS.index(last)
|
||||
if start > end:
|
||||
raise SystemExit("起始步骤必须小于或等于结束步骤")
|
||||
return list(STEPS[start : end + 1])
|
||||
|
||||
|
||||
def stream_command(step: str, cmd: list[str]) -> tuple[int, bool]:
|
||||
"""Run one step and forward stdout/stderr lines as they are produced."""
|
||||
print(f"运行步骤:{STEP_LABELS.get(step, step)};执行命令:" + " ".join(cmd), flush=True)
|
||||
env = os.environ.copy()
|
||||
env["PYTHONIOENCODING"] = "utf-8"
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
env=env,
|
||||
)
|
||||
assert process.stdout is not None
|
||||
waiting_user = False
|
||||
for line in process.stdout:
|
||||
if line.startswith("用户协助 "):
|
||||
waiting_user = True
|
||||
sys.stdout.buffer.write(line.encode("utf-8", errors="replace"))
|
||||
sys.stdout.buffer.flush()
|
||||
return process.wait(), waiting_user
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = build_parser()
|
||||
args, passthrough = parser.parse_known_args(argv)
|
||||
flow_script = Path(__file__).with_name("xdfx_flow.py")
|
||||
exit_code = 0
|
||||
for step in selected_steps(args.from_step, args.to_step):
|
||||
# Keep all business flags unchanged so the wrapper behaves like a thin
|
||||
# streaming version of repeated xdfx_flow.py --mode stepN calls.
|
||||
cmd = [sys.executable, str(flow_script), "--mode", step, *passthrough]
|
||||
code, waiting_user = stream_command(step, cmd)
|
||||
if waiting_user:
|
||||
print("已发起用户协助,暂停后续步骤;请根据用户选择重跑当前步骤。", flush=True)
|
||||
return code
|
||||
if code:
|
||||
exit_code = code
|
||||
if not args.continue_on_error:
|
||||
return code
|
||||
return exit_code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in new issue
Block a user