技能修改

This commit is contained in:
fengxiangdebaozi committed 2026-06-29 18:08:09 +08:00
1 parent 45ce14e15a
commit 29082b349d
22 files changed
+623 -205

No files matched your search

+1 -1
View File
@@ -41,7 +41,7 @@ skills-data/xdfx-single-agent-full-flow/scripts/xdfx_flow.py
}
```
默认会自动处理登录态:脚本启动时先 `import scripts/xdfx_auth.py`,用固定账号 `admin`、口令 `ch@user` 调用 `POST /api/v1/auth/login/username?password=ch%40user`,请求体为 `{"username":"admin"}`。拿到 `access_token` 后写入内部 `Authorization: Bearer ...`,再调用第一步业务接口;不要通过任何命令行参数传 token。
默认会自动处理登录态:脚本启动时先 `import scripts/xdfx_auth.py`,用固定账号 `admin`、口令 `ch@user` 调用 `POST /api/auth/login`,请求体为 `{"username":"admin","password":"ch@user"}`。拿到 `access_token` 后写入内部 `Authorization: Bearer ...`,再调用第一步业务接口;不要通过任何命令行参数传 token。
默认关闭 HTTPS 证书校验,用于兼容 UAT/内网自签证书;如果部署环境证书链正常,可以追加 `--verify-ssl`。接口返回 401 时仍由 `xdfx_auth.py` 刷新 token 并重试原接口一次。
@@ -31,8 +31,8 @@ scripts/xdfx_flow.py
默认调用:
```text
POST /api/v1/auth/login/username?password=ch%40user
body: {"username":"admin"}
POST /api/auth/login
body: {"username":"admin","password":"ch@user"}
```
拿到 `access_token` 后会写入内部 `Authorization: Bearer ...`。不要通过任何命令行参数传 token;脚本会忽略外部 Authorization,统一由 `xdfx_auth.py` 获取和刷新。登录接口和 401 重试都会输出 `操作反馈` / `错误反馈`,但反馈中的 `password`、`token` 会脱敏。默认关闭 HTTPS 证书校验,适配 UAT/内网自签证书;如果环境证书链正常,可以追加 `--verify-ssl`。
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""Authentication helper for xdfx skill scripts.
The caller should not pass tokens through command-line arguments. This module
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.
"""
@@ -12,7 +12,6 @@ import json
import re
import ssl
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass
from typing import Any
@@ -58,12 +57,8 @@ def ssl_context(*, verify_ssl: bool) -> ssl.SSLContext | None:
return ssl._create_unverified_context()
def build_login_url(api_base: str, path: str, password: str) -> str:
base = path if re.match(r"^https?://", path, flags=re.IGNORECASE) else api_base.rstrip("/") + "/" + path.lstrip("/")
if password:
separator = "&" if "?" in base else "?"
base = f"{base}{separator}password={urllib.parse.quote(password, safe='')}"
return base
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:
@@ -106,13 +101,13 @@ def fetch_access_token(
api_base: str,
username: str = "admin",
password: str = "ch@user",
login_path: str = "/api/v1/auth/login/username",
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, password)
payload = {"username": username}
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")
@@ -122,6 +122,8 @@ REQUEST_FIELD_LABELS = {
"temperature": "温度",
"usermessage": "用户消息",
"user_message": "用户消息",
"username": "登录用户名",
"password": "登录密码",
"mubiao": "行动目标",
"zhanghao": "账号列表",
"taskId": "任务ID",
@@ -148,6 +150,8 @@ REQUEST_FIELD_LABELS = {
"step": "步骤状态",
}
SENSITIVE_KEY_PARTS = ("password", "token", "authorization", "access_token", "accesstoken", "authtoken")
# Platform values mirror the frontend dictionaries used by xdfx strategy pages.
PLATFORM_MAP = {
"facebook": 1,
@@ -228,7 +232,7 @@ class FlowContext:
auto_login: bool = True
login_username: str = "admin"
login_password: str = "ch@user"
login_path: str = "/api/v1/auth/login/username"
login_path: str = "/api/auth/login"
login_attempt_count: int = 0
@@ -249,6 +253,37 @@ def compact(value: Any, limit: int = 1400) -> str:
return text[:limit] + f"... <已截断 {len(text) - limit} 个字符>"
def is_sensitive_key(key: Any) -> bool:
normalized = re.sub(r"[^a-z0-9_]", "", str(key or "").lower())
return any(part in normalized for part in SENSITIVE_KEY_PARTS)
def redact_sensitive(value: Any) -> Any:
if isinstance(value, dict):
return {key: "***" if is_sensitive_key(key) else redact_sensitive(item) for key, item in value.items()}
if isinstance(value, list):
return [redact_sensitive(item) for item in value]
if isinstance(value, tuple):
return tuple(redact_sensitive(item) for item in value)
if isinstance(value, str):
text = re.sub(r"(?i)(Bearer\s+)[^\s\",}]+", r"\1***", value)
text = re.sub(
r"(?i)(\"(?:password|token|authToken|access_token|accessToken|authorization)\"\s*:\s*)\"[^\"]*\"",
r'\1"***"',
text,
)
return re.sub(
r"(?i)((?:password|token|authToken|access_token|accessToken|authorization)=)[^&\s]+",
r"\1***",
text,
)
return value
def safe_compact(value: Any, limit: int = 1400) -> str:
return compact(redact_sensitive(value), limit)
def stable_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
@@ -636,10 +671,10 @@ def record_error(
"message": message,
"url": url,
"status": status,
"request": compact(request, 1200) if request is not None else None,
"response": compact(response, 2000) if response is not None else None,
"request": safe_compact(request, 1200) if request is not None else None,
"response": safe_compact(response, 2000) if response is not None else None,
"exceptionType": type(exception).__name__ if exception else None,
"exception": str(exception) if exception else None,
"exception": safe_compact(str(exception), 1000) if exception else None,
}
state.setdefault("errors", []).append(item)
state["updatedAt"] = utc_now()
@@ -696,8 +731,8 @@ def ssl_context(ctx: FlowContext) -> ssl.SSLContext | None:
def login_candidate_paths(ctx: FlowContext) -> list[str]:
paths = [
ctx.login_path or "/api/v1/auth/login/username",
"/api/v1/auth/login/username",
ctx.login_path or "/api/auth/login",
"/api/auth/login",
]
seen: set[str] = set()
unique: list[str] = []
@@ -726,12 +761,12 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
return False
ctx.login_attempt_count += 1
payload = {"username": ctx.login_username}
payload = {"username": ctx.login_username, "password": ctx.login_password}
last_status: int | None = None
last_response = ""
last_exc: BaseException | None = None
for attempt, path in enumerate(login_candidate_paths(ctx), start=1):
display_url = xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, path, ctx.login_password))
display_url = xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, path))
emit_operation_feedback(
state,
stage,
@@ -767,14 +802,14 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
state,
stage,
"xdfx_auth 获取 token 失败,准备尝试下一个候选登录路径",
{"url": exc.url, "status": exc.status, "response": compact(exc.response, 1000), "exception": str(exc)},
{"url": exc.url, "status": exc.status, "response": safe_compact(exc.response, 1000), "exception": str(exc)},
)
record_error(
state,
stage,
"自动登录失败,未能通过 xdfx_auth 获取 access_token",
url=xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, ctx.login_path or "/api/v1/auth/login/username", ctx.login_password)),
url=xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, ctx.login_path or "/api/auth/login")),
status=last_status,
request=payload,
response=last_response,
@@ -2013,6 +2048,13 @@ def object_for_audience(audience: dict[str, Any]) -> dict[str, Any]:
}
def selected_accounts_for_item(item: dict[str, Any], *, audience: bool) -> list[Any]:
if audience:
accounts = item.get("accounts") or []
return accounts or [object_for_audience(item)]
return [item.get("account") or item]
def downstream_limit(ctx: FlowContext, *, audience: bool) -> int:
"""Limit expensive downstream analysis while keeping 3 upstream rows stored."""
raw = ctx.downstream_audiences if audience else ctx.downstream_pathways
@@ -2263,7 +2305,7 @@ def run_vuln_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool) ->
continue
if str(owner_id) in state.get(output_key, {}) and state[output_key][str(owner_id)] and not ctx.dry_run:
continue
zhanghao = item.get("accounts") or [object_for_audience(item)] if audience else [item.get("account") or item]
zhanghao = selected_accounts_for_item(item, audience=audience)
prompt = (
"你是中国对台湾方向的认知战专家,你要传播行动要求的叙事主题。请基于该受众目标的画像、关注议题、社会分工定位、利益诉求与上一轮账号佐证,分析该受众目标有哪些脆弱点可以成为后续策略设计的关键要素。请输出理由、脆弱点1、理由1、脆弱点2、理由2、脆弱点3、理由3,只返回JSON。"
if audience
@@ -2349,7 +2391,9 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool
record_error(state, stage, "没有可用脆弱点,跳过策略生成", request=item)
continue
target_names = [item.get("targetAudience")] if audience else (item.get("relatedAudiences") or [item.get("accountName")])
pingtai = state.get("platform") or (platform_label(item.get("account") or {}) if not audience else "")
zhanghao = selected_accounts_for_item(item, audience=audience)
first_account = zhanghao[0] if zhanghao and isinstance(zhanghao[0], dict) else {}
pingtai = state.get("platform") or platform_label(first_account)
payload = {
"user_message": DEFAULT_STRATEGY_PROMPT_AUDIENCE if audience else DEFAULT_STRATEGY_PROMPT_PATHWAY,
"cuiruodian": vuln_texts,
@@ -2357,6 +2401,7 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool
"shouzhong": [name for name in target_names if name],
"mudi": extract_goal(state["mubiao"]),
"heiliao": [],
"zhanghao": zhanghao,
"history": [],
"stream": True,
"model_name": "OpenAI3",
@@ -2564,8 +2609,8 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--platform", default=None)
parser.add_argument("--header", action="append", default=[], help="附加非鉴权 HTTP 请求头,格式为 'Key: Value';Authorization 会被忽略。")
parser.add_argument("--login-username", default=os.getenv("XDFX_LOGIN_USERNAME", "admin"), help="xdfx_auth 启动阶段自动登录使用的用户名。")
parser.add_argument("--login-password", default=os.getenv("XDFX_LOGIN_PASSWORD", "ch@user"), help="用户名登录接口的 password 查询参数。")
parser.add_argument("--login-path", default=os.getenv("XDFX_LOGIN_PATH", "/api/v1/auth/login/username"), help="登录接口路径或完整 URL。")
parser.add_argument("--login-password", default=os.getenv("XDFX_LOGIN_PASSWORD", "ch@user"), help="登录接口 JSON 请求体中的 password。")
parser.add_argument("--login-path", default=os.getenv("XDFX_LOGIN_PATH", "/api/auth/login"), help="登录接口路径或完整 URL。")
parser.add_argument("--no-auto-login", action="store_true", help="关闭 xdfx_auth 自动登录和 401 刷新。")
parser.add_argument("--verify-ssl", action="store_true", help="校验 HTTPS 证书。默认不校验,以兼容内网/UAT 自签证书。")
parser.add_argument("--timeout", type=float, default=90)
+1 -1
View File
@@ -28,7 +28,7 @@ scripts/xdfx_flow.py
## 推荐执行顺序
默认会自动处理登录态:脚本启动时先 `import scripts/xdfx_auth.py`,用固定账号 `admin`、口令 `ch@user` 调用 `POST /api/v1/auth/login/username?password=ch%40user`,请求体为 `{"username":"admin"}`。拿到 `access_token` 后写入内部 `Authorization: Bearer ...`,再调用第一步业务接口;不要通过任何命令行参数传 token。401 时仍由 `xdfx_auth.py` 刷新 token 并重试原接口一次。默认关闭 HTTPS 证书校验以兼容 UAT/内网自签证书;如果环境证书链正常,可以追加 `--verify-ssl`。
默认会自动处理登录态:脚本启动时先 `import scripts/xdfx_auth.py`,用固定账号 `admin`、口令 `ch@user` 调用 `POST /api/auth/login`,请求体为 `{"username":"admin","password":"ch@user"}`。拿到 `access_token` 后写入内部 `Authorization: Bearer ...`,再调用第一步业务接口;不要通过任何命令行参数传 token。401 时仍由 `xdfx_auth.py` 刷新 token 并重试原接口一次。默认关闭 HTTPS 证书校验以兼容 UAT/内网自签证书;如果环境证书链正常,可以追加 `--verify-ssl`。
总控按顺序执行下面 6 条命令,所有步骤共用同一个 `xdfx-flow-state.json`:
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""Authentication helper for xdfx skill scripts.
The caller should not pass tokens through command-line arguments. This module
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.
"""
@@ -12,7 +12,6 @@ import json
import re
import ssl
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass
from typing import Any
@@ -58,12 +57,8 @@ def ssl_context(*, verify_ssl: bool) -> ssl.SSLContext | None:
return ssl._create_unverified_context()
def build_login_url(api_base: str, path: str, password: str) -> str:
base = path if re.match(r"^https?://", path, flags=re.IGNORECASE) else api_base.rstrip("/") + "/" + path.lstrip("/")
if password:
separator = "&" if "?" in base else "?"
base = f"{base}{separator}password={urllib.parse.quote(password, safe='')}"
return base
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:
@@ -106,13 +101,13 @@ def fetch_access_token(
api_base: str,
username: str = "admin",
password: str = "ch@user",
login_path: str = "/api/v1/auth/login/username",
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, password)
payload = {"username": username}
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")
@@ -122,6 +122,8 @@ REQUEST_FIELD_LABELS = {
"temperature": "温度",
"usermessage": "用户消息",
"user_message": "用户消息",
"username": "登录用户名",
"password": "登录密码",
"mubiao": "行动目标",
"zhanghao": "账号列表",
"taskId": "任务ID",
@@ -148,6 +150,8 @@ REQUEST_FIELD_LABELS = {
"step": "步骤状态",
}
SENSITIVE_KEY_PARTS = ("password", "token", "authorization", "access_token", "accesstoken", "authtoken")
# Platform values mirror the frontend dictionaries used by xdfx strategy pages.
PLATFORM_MAP = {
"facebook": 1,
@@ -228,7 +232,7 @@ class FlowContext:
auto_login: bool = True
login_username: str = "admin"
login_password: str = "ch@user"
login_path: str = "/api/v1/auth/login/username"
login_path: str = "/api/auth/login"
login_attempt_count: int = 0
@@ -249,6 +253,37 @@ def compact(value: Any, limit: int = 1400) -> str:
return text[:limit] + f"... <已截断 {len(text) - limit} 个字符>"
def is_sensitive_key(key: Any) -> bool:
normalized = re.sub(r"[^a-z0-9_]", "", str(key or "").lower())
return any(part in normalized for part in SENSITIVE_KEY_PARTS)
def redact_sensitive(value: Any) -> Any:
if isinstance(value, dict):
return {key: "***" if is_sensitive_key(key) else redact_sensitive(item) for key, item in value.items()}
if isinstance(value, list):
return [redact_sensitive(item) for item in value]
if isinstance(value, tuple):
return tuple(redact_sensitive(item) for item in value)
if isinstance(value, str):
text = re.sub(r"(?i)(Bearer\s+)[^\s\",}]+", r"\1***", value)
text = re.sub(
r"(?i)(\"(?:password|token|authToken|access_token|accessToken|authorization)\"\s*:\s*)\"[^\"]*\"",
r'\1"***"',
text,
)
return re.sub(
r"(?i)((?:password|token|authToken|access_token|accessToken|authorization)=)[^&\s]+",
r"\1***",
text,
)
return value
def safe_compact(value: Any, limit: int = 1400) -> str:
return compact(redact_sensitive(value), limit)
def stable_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
@@ -636,10 +671,10 @@ def record_error(
"message": message,
"url": url,
"status": status,
"request": compact(request, 1200) if request is not None else None,
"response": compact(response, 2000) if response is not None else None,
"request": safe_compact(request, 1200) if request is not None else None,
"response": safe_compact(response, 2000) if response is not None else None,
"exceptionType": type(exception).__name__ if exception else None,
"exception": str(exception) if exception else None,
"exception": safe_compact(str(exception), 1000) if exception else None,
}
state.setdefault("errors", []).append(item)
state["updatedAt"] = utc_now()
@@ -696,8 +731,8 @@ def ssl_context(ctx: FlowContext) -> ssl.SSLContext | None:
def login_candidate_paths(ctx: FlowContext) -> list[str]:
paths = [
ctx.login_path or "/api/v1/auth/login/username",
"/api/v1/auth/login/username",
ctx.login_path or "/api/auth/login",
"/api/auth/login",
]
seen: set[str] = set()
unique: list[str] = []
@@ -726,12 +761,12 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
return False
ctx.login_attempt_count += 1
payload = {"username": ctx.login_username}
payload = {"username": ctx.login_username, "password": ctx.login_password}
last_status: int | None = None
last_response = ""
last_exc: BaseException | None = None
for attempt, path in enumerate(login_candidate_paths(ctx), start=1):
display_url = xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, path, ctx.login_password))
display_url = xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, path))
emit_operation_feedback(
state,
stage,
@@ -767,14 +802,14 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
state,
stage,
"xdfx_auth 获取 token 失败,准备尝试下一个候选登录路径",
{"url": exc.url, "status": exc.status, "response": compact(exc.response, 1000), "exception": str(exc)},
{"url": exc.url, "status": exc.status, "response": safe_compact(exc.response, 1000), "exception": str(exc)},
)
record_error(
state,
stage,
"自动登录失败,未能通过 xdfx_auth 获取 access_token",
url=xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, ctx.login_path or "/api/v1/auth/login/username", ctx.login_password)),
url=xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, ctx.login_path or "/api/auth/login")),
status=last_status,
request=payload,
response=last_response,
@@ -2013,6 +2048,13 @@ def object_for_audience(audience: dict[str, Any]) -> dict[str, Any]:
}
def selected_accounts_for_item(item: dict[str, Any], *, audience: bool) -> list[Any]:
if audience:
accounts = item.get("accounts") or []
return accounts or [object_for_audience(item)]
return [item.get("account") or item]
def downstream_limit(ctx: FlowContext, *, audience: bool) -> int:
"""Limit expensive downstream analysis while keeping 3 upstream rows stored."""
raw = ctx.downstream_audiences if audience else ctx.downstream_pathways
@@ -2263,7 +2305,7 @@ def run_vuln_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool) ->
continue
if str(owner_id) in state.get(output_key, {}) and state[output_key][str(owner_id)] and not ctx.dry_run:
continue
zhanghao = item.get("accounts") or [object_for_audience(item)] if audience else [item.get("account") or item]
zhanghao = selected_accounts_for_item(item, audience=audience)
prompt = (
"你是中国对台湾方向的认知战专家,你要传播行动要求的叙事主题。请基于该受众目标的画像、关注议题、社会分工定位、利益诉求与上一轮账号佐证,分析该受众目标有哪些脆弱点可以成为后续策略设计的关键要素。请输出理由、脆弱点1、理由1、脆弱点2、理由2、脆弱点3、理由3,只返回JSON。"
if audience
@@ -2349,7 +2391,9 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool
record_error(state, stage, "没有可用脆弱点,跳过策略生成", request=item)
continue
target_names = [item.get("targetAudience")] if audience else (item.get("relatedAudiences") or [item.get("accountName")])
pingtai = state.get("platform") or (platform_label(item.get("account") or {}) if not audience else "")
zhanghao = selected_accounts_for_item(item, audience=audience)
first_account = zhanghao[0] if zhanghao and isinstance(zhanghao[0], dict) else {}
pingtai = state.get("platform") or platform_label(first_account)
payload = {
"user_message": DEFAULT_STRATEGY_PROMPT_AUDIENCE if audience else DEFAULT_STRATEGY_PROMPT_PATHWAY,
"cuiruodian": vuln_texts,
@@ -2357,6 +2401,7 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool
"shouzhong": [name for name in target_names if name],
"mudi": extract_goal(state["mubiao"]),
"heiliao": [],
"zhanghao": zhanghao,
"history": [],
"stream": True,
"model_name": "OpenAI3",
@@ -2564,8 +2609,8 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--platform", default=None)
parser.add_argument("--header", action="append", default=[], help="附加非鉴权 HTTP 请求头,格式为 'Key: Value';Authorization 会被忽略。")
parser.add_argument("--login-username", default=os.getenv("XDFX_LOGIN_USERNAME", "admin"), help="xdfx_auth 启动阶段自动登录使用的用户名。")
parser.add_argument("--login-password", default=os.getenv("XDFX_LOGIN_PASSWORD", "ch@user"), help="用户名登录接口的 password 查询参数。")
parser.add_argument("--login-path", default=os.getenv("XDFX_LOGIN_PATH", "/api/v1/auth/login/username"), help="登录接口路径或完整 URL。")
parser.add_argument("--login-password", default=os.getenv("XDFX_LOGIN_PASSWORD", "ch@user"), help="登录接口 JSON 请求体中的 password。")
parser.add_argument("--login-path", default=os.getenv("XDFX_LOGIN_PATH", "/api/auth/login"), help="登录接口路径或完整 URL。")
parser.add_argument("--no-auto-login", action="store_true", help="关闭 xdfx_auth 自动登录和 401 刷新。")
parser.add_argument("--verify-ssl", action="store_true", help="校验 HTTPS 证书。默认不校验,以兼容内网/UAT 自签证书。")
parser.add_argument("--timeout", type=float, default=90)
+1 -1
View File
@@ -45,7 +45,7 @@ scripts/xdfx_flow.py
xdfx-flow-state.json
```
默认会自动处理登录态:脚本启动时先 `import scripts/xdfx_auth.py`,用固定账号 `admin`、口令 `ch@user` 调用 `POST /api/v1/auth/login/username?password=ch%40user`,请求体为 `{"username":"admin"}`,拿到 `access_token` 后写入内部 `Authorization: Bearer ...`,再调用第一步业务接口;不要通过任何命令行参数传 token。默认关闭 HTTPS 证书校验,用于兼容 UAT/内网自签证书;需要严格校验证书时追加 `--verify-ssl`。接口返回 401 时仍由 `xdfx_auth.py` 刷新 token 并重试原接口一次。
默认会自动处理登录态:脚本启动时先 `import scripts/xdfx_auth.py`,用固定账号 `admin`、口令 `ch@user` 调用 `POST /api/auth/login`,请求体为 `{"username":"admin","password":"ch@user"}`,拿到 `access_token` 后写入内部 `Authorization: Bearer ...`,再调用第一步业务接口;不要通过任何命令行参数传 token。默认关闭 HTTPS 证书校验,用于兼容 UAT/内网自签证书;需要严格校验证书时追加 `--verify-ssl`。接口返回 401 时仍由 `xdfx_auth.py` 刷新 token 并重试原接口一次。
每一步命令结束都会输出一行:
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""Authentication helper for xdfx skill scripts.
The caller should not pass tokens through command-line arguments. This module
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.
"""
@@ -12,7 +12,6 @@ import json
import re
import ssl
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass
from typing import Any
@@ -58,12 +57,8 @@ def ssl_context(*, verify_ssl: bool) -> ssl.SSLContext | None:
return ssl._create_unverified_context()
def build_login_url(api_base: str, path: str, password: str) -> str:
base = path if re.match(r"^https?://", path, flags=re.IGNORECASE) else api_base.rstrip("/") + "/" + path.lstrip("/")
if password:
separator = "&" if "?" in base else "?"
base = f"{base}{separator}password={urllib.parse.quote(password, safe='')}"
return base
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:
@@ -106,13 +101,13 @@ def fetch_access_token(
api_base: str,
username: str = "admin",
password: str = "ch@user",
login_path: str = "/api/v1/auth/login/username",
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, password)
payload = {"username": username}
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")
@@ -122,6 +122,8 @@ REQUEST_FIELD_LABELS = {
"temperature": "温度",
"usermessage": "用户消息",
"user_message": "用户消息",
"username": "登录用户名",
"password": "登录密码",
"mubiao": "行动目标",
"zhanghao": "账号列表",
"taskId": "任务ID",
@@ -148,6 +150,8 @@ REQUEST_FIELD_LABELS = {
"step": "步骤状态",
}
SENSITIVE_KEY_PARTS = ("password", "token", "authorization", "access_token", "accesstoken", "authtoken")
# Platform values mirror the frontend dictionaries used by xdfx strategy pages.
PLATFORM_MAP = {
"facebook": 1,
@@ -228,7 +232,7 @@ class FlowContext:
auto_login: bool = True
login_username: str = "admin"
login_password: str = "ch@user"
login_path: str = "/api/v1/auth/login/username"
login_path: str = "/api/auth/login"
login_attempt_count: int = 0
@@ -249,6 +253,37 @@ def compact(value: Any, limit: int = 1400) -> str:
return text[:limit] + f"... <已截断 {len(text) - limit} 个字符>"
def is_sensitive_key(key: Any) -> bool:
normalized = re.sub(r"[^a-z0-9_]", "", str(key or "").lower())
return any(part in normalized for part in SENSITIVE_KEY_PARTS)
def redact_sensitive(value: Any) -> Any:
if isinstance(value, dict):
return {key: "***" if is_sensitive_key(key) else redact_sensitive(item) for key, item in value.items()}
if isinstance(value, list):
return [redact_sensitive(item) for item in value]
if isinstance(value, tuple):
return tuple(redact_sensitive(item) for item in value)
if isinstance(value, str):
text = re.sub(r"(?i)(Bearer\s+)[^\s\",}]+", r"\1***", value)
text = re.sub(
r"(?i)(\"(?:password|token|authToken|access_token|accessToken|authorization)\"\s*:\s*)\"[^\"]*\"",
r'\1"***"',
text,
)
return re.sub(
r"(?i)((?:password|token|authToken|access_token|accessToken|authorization)=)[^&\s]+",
r"\1***",
text,
)
return value
def safe_compact(value: Any, limit: int = 1400) -> str:
return compact(redact_sensitive(value), limit)
def stable_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
@@ -636,10 +671,10 @@ def record_error(
"message": message,
"url": url,
"status": status,
"request": compact(request, 1200) if request is not None else None,
"response": compact(response, 2000) if response is not None else None,
"request": safe_compact(request, 1200) if request is not None else None,
"response": safe_compact(response, 2000) if response is not None else None,
"exceptionType": type(exception).__name__ if exception else None,
"exception": str(exception) if exception else None,
"exception": safe_compact(str(exception), 1000) if exception else None,
}
state.setdefault("errors", []).append(item)
state["updatedAt"] = utc_now()
@@ -696,8 +731,8 @@ def ssl_context(ctx: FlowContext) -> ssl.SSLContext | None:
def login_candidate_paths(ctx: FlowContext) -> list[str]:
paths = [
ctx.login_path or "/api/v1/auth/login/username",
"/api/v1/auth/login/username",
ctx.login_path or "/api/auth/login",
"/api/auth/login",
]
seen: set[str] = set()
unique: list[str] = []
@@ -726,12 +761,12 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
return False
ctx.login_attempt_count += 1
payload = {"username": ctx.login_username}
payload = {"username": ctx.login_username, "password": ctx.login_password}
last_status: int | None = None
last_response = ""
last_exc: BaseException | None = None
for attempt, path in enumerate(login_candidate_paths(ctx), start=1):
display_url = xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, path, ctx.login_password))
display_url = xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, path))
emit_operation_feedback(
state,
stage,
@@ -767,14 +802,14 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
state,
stage,
"xdfx_auth 获取 token 失败,准备尝试下一个候选登录路径",
{"url": exc.url, "status": exc.status, "response": compact(exc.response, 1000), "exception": str(exc)},
{"url": exc.url, "status": exc.status, "response": safe_compact(exc.response, 1000), "exception": str(exc)},
)
record_error(
state,
stage,
"自动登录失败,未能通过 xdfx_auth 获取 access_token",
url=xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, ctx.login_path or "/api/v1/auth/login/username", ctx.login_password)),
url=xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, ctx.login_path or "/api/auth/login")),
status=last_status,
request=payload,
response=last_response,
@@ -2013,6 +2048,13 @@ def object_for_audience(audience: dict[str, Any]) -> dict[str, Any]:
}
def selected_accounts_for_item(item: dict[str, Any], *, audience: bool) -> list[Any]:
if audience:
accounts = item.get("accounts") or []
return accounts or [object_for_audience(item)]
return [item.get("account") or item]
def downstream_limit(ctx: FlowContext, *, audience: bool) -> int:
"""Limit expensive downstream analysis while keeping 3 upstream rows stored."""
raw = ctx.downstream_audiences if audience else ctx.downstream_pathways
@@ -2263,7 +2305,7 @@ def run_vuln_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool) ->
continue
if str(owner_id) in state.get(output_key, {}) and state[output_key][str(owner_id)] and not ctx.dry_run:
continue
zhanghao = item.get("accounts") or [object_for_audience(item)] if audience else [item.get("account") or item]
zhanghao = selected_accounts_for_item(item, audience=audience)
prompt = (
"你是中国对台湾方向的认知战专家,你要传播行动要求的叙事主题。请基于该受众目标的画像、关注议题、社会分工定位、利益诉求与上一轮账号佐证,分析该受众目标有哪些脆弱点可以成为后续策略设计的关键要素。请输出理由、脆弱点1、理由1、脆弱点2、理由2、脆弱点3、理由3,只返回JSON。"
if audience
@@ -2349,7 +2391,9 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool
record_error(state, stage, "没有可用脆弱点,跳过策略生成", request=item)
continue
target_names = [item.get("targetAudience")] if audience else (item.get("relatedAudiences") or [item.get("accountName")])
pingtai = state.get("platform") or (platform_label(item.get("account") or {}) if not audience else "")
zhanghao = selected_accounts_for_item(item, audience=audience)
first_account = zhanghao[0] if zhanghao and isinstance(zhanghao[0], dict) else {}
pingtai = state.get("platform") or platform_label(first_account)
payload = {
"user_message": DEFAULT_STRATEGY_PROMPT_AUDIENCE if audience else DEFAULT_STRATEGY_PROMPT_PATHWAY,
"cuiruodian": vuln_texts,
@@ -2357,6 +2401,7 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool
"shouzhong": [name for name in target_names if name],
"mudi": extract_goal(state["mubiao"]),
"heiliao": [],
"zhanghao": zhanghao,
"history": [],
"stream": True,
"model_name": "OpenAI3",
@@ -2564,8 +2609,8 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--platform", default=None)
parser.add_argument("--header", action="append", default=[], help="附加非鉴权 HTTP 请求头,格式为 'Key: Value';Authorization 会被忽略。")
parser.add_argument("--login-username", default=os.getenv("XDFX_LOGIN_USERNAME", "admin"), help="xdfx_auth 启动阶段自动登录使用的用户名。")
parser.add_argument("--login-password", default=os.getenv("XDFX_LOGIN_PASSWORD", "ch@user"), help="用户名登录接口的 password 查询参数。")
parser.add_argument("--login-path", default=os.getenv("XDFX_LOGIN_PATH", "/api/v1/auth/login/username"), help="登录接口路径或完整 URL。")
parser.add_argument("--login-password", default=os.getenv("XDFX_LOGIN_PASSWORD", "ch@user"), help="登录接口 JSON 请求体中的 password。")
parser.add_argument("--login-path", default=os.getenv("XDFX_LOGIN_PATH", "/api/auth/login"), help="登录接口路径或完整 URL。")
parser.add_argument("--no-auto-login", action="store_true", help="关闭 xdfx_auth 自动登录和 401 刷新。")
parser.add_argument("--verify-ssl", action="store_true", help="校验 HTTPS 证书。默认不校验,以兼容内网/UAT 自签证书。")
parser.add_argument("--timeout", type=float, default=90)
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""Authentication helper for xdfx skill scripts.
The caller should not pass tokens through command-line arguments. This module
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.
"""
@@ -12,7 +12,6 @@ import json
import re
import ssl
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass
from typing import Any
@@ -58,12 +57,8 @@ def ssl_context(*, verify_ssl: bool) -> ssl.SSLContext | None:
return ssl._create_unverified_context()
def build_login_url(api_base: str, path: str, password: str) -> str:
base = path if re.match(r"^https?://", path, flags=re.IGNORECASE) else api_base.rstrip("/") + "/" + path.lstrip("/")
if password:
separator = "&" if "?" in base else "?"
base = f"{base}{separator}password={urllib.parse.quote(password, safe='')}"
return base
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:
@@ -106,13 +101,13 @@ def fetch_access_token(
api_base: str,
username: str = "admin",
password: str = "ch@user",
login_path: str = "/api/v1/auth/login/username",
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, password)
payload = {"username": username}
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")
@@ -122,6 +122,8 @@ REQUEST_FIELD_LABELS = {
"temperature": "温度",
"usermessage": "用户消息",
"user_message": "用户消息",
"username": "登录用户名",
"password": "登录密码",
"mubiao": "行动目标",
"zhanghao": "账号列表",
"taskId": "任务ID",
@@ -148,6 +150,8 @@ REQUEST_FIELD_LABELS = {
"step": "步骤状态",
}
SENSITIVE_KEY_PARTS = ("password", "token", "authorization", "access_token", "accesstoken", "authtoken")
# Platform values mirror the frontend dictionaries used by xdfx strategy pages.
PLATFORM_MAP = {
"facebook": 1,
@@ -228,7 +232,7 @@ class FlowContext:
auto_login: bool = True
login_username: str = "admin"
login_password: str = "ch@user"
login_path: str = "/api/v1/auth/login/username"
login_path: str = "/api/auth/login"
login_attempt_count: int = 0
@@ -249,6 +253,37 @@ def compact(value: Any, limit: int = 1400) -> str:
return text[:limit] + f"... <已截断 {len(text) - limit} 个字符>"
def is_sensitive_key(key: Any) -> bool:
normalized = re.sub(r"[^a-z0-9_]", "", str(key or "").lower())
return any(part in normalized for part in SENSITIVE_KEY_PARTS)
def redact_sensitive(value: Any) -> Any:
if isinstance(value, dict):
return {key: "***" if is_sensitive_key(key) else redact_sensitive(item) for key, item in value.items()}
if isinstance(value, list):
return [redact_sensitive(item) for item in value]
if isinstance(value, tuple):
return tuple(redact_sensitive(item) for item in value)
if isinstance(value, str):
text = re.sub(r"(?i)(Bearer\s+)[^\s\",}]+", r"\1***", value)
text = re.sub(
r"(?i)(\"(?:password|token|authToken|access_token|accessToken|authorization)\"\s*:\s*)\"[^\"]*\"",
r'\1"***"',
text,
)
return re.sub(
r"(?i)((?:password|token|authToken|access_token|accessToken|authorization)=)[^&\s]+",
r"\1***",
text,
)
return value
def safe_compact(value: Any, limit: int = 1400) -> str:
return compact(redact_sensitive(value), limit)
def stable_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
@@ -636,10 +671,10 @@ def record_error(
"message": message,
"url": url,
"status": status,
"request": compact(request, 1200) if request is not None else None,
"response": compact(response, 2000) if response is not None else None,
"request": safe_compact(request, 1200) if request is not None else None,
"response": safe_compact(response, 2000) if response is not None else None,
"exceptionType": type(exception).__name__ if exception else None,
"exception": str(exception) if exception else None,
"exception": safe_compact(str(exception), 1000) if exception else None,
}
state.setdefault("errors", []).append(item)
state["updatedAt"] = utc_now()
@@ -696,8 +731,8 @@ def ssl_context(ctx: FlowContext) -> ssl.SSLContext | None:
def login_candidate_paths(ctx: FlowContext) -> list[str]:
paths = [
ctx.login_path or "/api/v1/auth/login/username",
"/api/v1/auth/login/username",
ctx.login_path or "/api/auth/login",
"/api/auth/login",
]
seen: set[str] = set()
unique: list[str] = []
@@ -726,12 +761,12 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
return False
ctx.login_attempt_count += 1
payload = {"username": ctx.login_username}
payload = {"username": ctx.login_username, "password": ctx.login_password}
last_status: int | None = None
last_response = ""
last_exc: BaseException | None = None
for attempt, path in enumerate(login_candidate_paths(ctx), start=1):
display_url = xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, path, ctx.login_password))
display_url = xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, path))
emit_operation_feedback(
state,
stage,
@@ -767,14 +802,14 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
state,
stage,
"xdfx_auth 获取 token 失败,准备尝试下一个候选登录路径",
{"url": exc.url, "status": exc.status, "response": compact(exc.response, 1000), "exception": str(exc)},
{"url": exc.url, "status": exc.status, "response": safe_compact(exc.response, 1000), "exception": str(exc)},
)
record_error(
state,
stage,
"自动登录失败,未能通过 xdfx_auth 获取 access_token",
url=xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, ctx.login_path or "/api/v1/auth/login/username", ctx.login_password)),
url=xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, ctx.login_path or "/api/auth/login")),
status=last_status,
request=payload,
response=last_response,
@@ -2013,6 +2048,13 @@ def object_for_audience(audience: dict[str, Any]) -> dict[str, Any]:
}
def selected_accounts_for_item(item: dict[str, Any], *, audience: bool) -> list[Any]:
if audience:
accounts = item.get("accounts") or []
return accounts or [object_for_audience(item)]
return [item.get("account") or item]
def downstream_limit(ctx: FlowContext, *, audience: bool) -> int:
"""Limit expensive downstream analysis while keeping 3 upstream rows stored."""
raw = ctx.downstream_audiences if audience else ctx.downstream_pathways
@@ -2263,7 +2305,7 @@ def run_vuln_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool) ->
continue
if str(owner_id) in state.get(output_key, {}) and state[output_key][str(owner_id)] and not ctx.dry_run:
continue
zhanghao = item.get("accounts") or [object_for_audience(item)] if audience else [item.get("account") or item]
zhanghao = selected_accounts_for_item(item, audience=audience)
prompt = (
"你是中国对台湾方向的认知战专家,你要传播行动要求的叙事主题。请基于该受众目标的画像、关注议题、社会分工定位、利益诉求与上一轮账号佐证,分析该受众目标有哪些脆弱点可以成为后续策略设计的关键要素。请输出理由、脆弱点1、理由1、脆弱点2、理由2、脆弱点3、理由3,只返回JSON。"
if audience
@@ -2349,7 +2391,9 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool
record_error(state, stage, "没有可用脆弱点,跳过策略生成", request=item)
continue
target_names = [item.get("targetAudience")] if audience else (item.get("relatedAudiences") or [item.get("accountName")])
pingtai = state.get("platform") or (platform_label(item.get("account") or {}) if not audience else "")
zhanghao = selected_accounts_for_item(item, audience=audience)
first_account = zhanghao[0] if zhanghao and isinstance(zhanghao[0], dict) else {}
pingtai = state.get("platform") or platform_label(first_account)
payload = {
"user_message": DEFAULT_STRATEGY_PROMPT_AUDIENCE if audience else DEFAULT_STRATEGY_PROMPT_PATHWAY,
"cuiruodian": vuln_texts,
@@ -2357,6 +2401,7 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool
"shouzhong": [name for name in target_names if name],
"mudi": extract_goal(state["mubiao"]),
"heiliao": [],
"zhanghao": zhanghao,
"history": [],
"stream": True,
"model_name": "OpenAI3",
@@ -2564,8 +2609,8 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--platform", default=None)
parser.add_argument("--header", action="append", default=[], help="附加非鉴权 HTTP 请求头,格式为 'Key: Value';Authorization 会被忽略。")
parser.add_argument("--login-username", default=os.getenv("XDFX_LOGIN_USERNAME", "admin"), help="xdfx_auth 启动阶段自动登录使用的用户名。")
parser.add_argument("--login-password", default=os.getenv("XDFX_LOGIN_PASSWORD", "ch@user"), help="用户名登录接口的 password 查询参数。")
parser.add_argument("--login-path", default=os.getenv("XDFX_LOGIN_PATH", "/api/v1/auth/login/username"), help="登录接口路径或完整 URL。")
parser.add_argument("--login-password", default=os.getenv("XDFX_LOGIN_PASSWORD", "ch@user"), help="登录接口 JSON 请求体中的 password。")
parser.add_argument("--login-path", default=os.getenv("XDFX_LOGIN_PATH", "/api/auth/login"), help="登录接口路径或完整 URL。")
parser.add_argument("--no-auto-login", action="store_true", help="关闭 xdfx_auth 自动登录和 401 刷新。")
parser.add_argument("--verify-ssl", action="store_true", help="校验 HTTPS 证书。默认不校验,以兼容内网/UAT 自签证书。")
parser.add_argument("--timeout", type=float, default=90)
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""Authentication helper for xdfx skill scripts.
The caller should not pass tokens through command-line arguments. This module
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.
"""
@@ -12,7 +12,6 @@ import json
import re
import ssl
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass
from typing import Any
@@ -58,12 +57,8 @@ def ssl_context(*, verify_ssl: bool) -> ssl.SSLContext | None:
return ssl._create_unverified_context()
def build_login_url(api_base: str, path: str, password: str) -> str:
base = path if re.match(r"^https?://", path, flags=re.IGNORECASE) else api_base.rstrip("/") + "/" + path.lstrip("/")
if password:
separator = "&" if "?" in base else "?"
base = f"{base}{separator}password={urllib.parse.quote(password, safe='')}"
return base
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:
@@ -106,13 +101,13 @@ def fetch_access_token(
api_base: str,
username: str = "admin",
password: str = "ch@user",
login_path: str = "/api/v1/auth/login/username",
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, password)
payload = {"username": username}
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")
@@ -122,6 +122,8 @@ REQUEST_FIELD_LABELS = {
"temperature": "温度",
"usermessage": "用户消息",
"user_message": "用户消息",
"username": "登录用户名",
"password": "登录密码",
"mubiao": "行动目标",
"zhanghao": "账号列表",
"taskId": "任务ID",
@@ -148,6 +150,8 @@ REQUEST_FIELD_LABELS = {
"step": "步骤状态",
}
SENSITIVE_KEY_PARTS = ("password", "token", "authorization", "access_token", "accesstoken", "authtoken")
# Platform values mirror the frontend dictionaries used by xdfx strategy pages.
PLATFORM_MAP = {
"facebook": 1,
@@ -228,7 +232,7 @@ class FlowContext:
auto_login: bool = True
login_username: str = "admin"
login_password: str = "ch@user"
login_path: str = "/api/v1/auth/login/username"
login_path: str = "/api/auth/login"
login_attempt_count: int = 0
@@ -249,6 +253,37 @@ def compact(value: Any, limit: int = 1400) -> str:
return text[:limit] + f"... <已截断 {len(text) - limit} 个字符>"
def is_sensitive_key(key: Any) -> bool:
normalized = re.sub(r"[^a-z0-9_]", "", str(key or "").lower())
return any(part in normalized for part in SENSITIVE_KEY_PARTS)
def redact_sensitive(value: Any) -> Any:
if isinstance(value, dict):
return {key: "***" if is_sensitive_key(key) else redact_sensitive(item) for key, item in value.items()}
if isinstance(value, list):
return [redact_sensitive(item) for item in value]
if isinstance(value, tuple):
return tuple(redact_sensitive(item) for item in value)
if isinstance(value, str):
text = re.sub(r"(?i)(Bearer\s+)[^\s\",}]+", r"\1***", value)
text = re.sub(
r"(?i)(\"(?:password|token|authToken|access_token|accessToken|authorization)\"\s*:\s*)\"[^\"]*\"",
r'\1"***"',
text,
)
return re.sub(
r"(?i)((?:password|token|authToken|access_token|accessToken|authorization)=)[^&\s]+",
r"\1***",
text,
)
return value
def safe_compact(value: Any, limit: int = 1400) -> str:
return compact(redact_sensitive(value), limit)
def stable_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
@@ -636,10 +671,10 @@ def record_error(
"message": message,
"url": url,
"status": status,
"request": compact(request, 1200) if request is not None else None,
"response": compact(response, 2000) if response is not None else None,
"request": safe_compact(request, 1200) if request is not None else None,
"response": safe_compact(response, 2000) if response is not None else None,
"exceptionType": type(exception).__name__ if exception else None,
"exception": str(exception) if exception else None,
"exception": safe_compact(str(exception), 1000) if exception else None,
}
state.setdefault("errors", []).append(item)
state["updatedAt"] = utc_now()
@@ -696,8 +731,8 @@ def ssl_context(ctx: FlowContext) -> ssl.SSLContext | None:
def login_candidate_paths(ctx: FlowContext) -> list[str]:
paths = [
ctx.login_path or "/api/v1/auth/login/username",
"/api/v1/auth/login/username",
ctx.login_path or "/api/auth/login",
"/api/auth/login",
]
seen: set[str] = set()
unique: list[str] = []
@@ -726,12 +761,12 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
return False
ctx.login_attempt_count += 1
payload = {"username": ctx.login_username}
payload = {"username": ctx.login_username, "password": ctx.login_password}
last_status: int | None = None
last_response = ""
last_exc: BaseException | None = None
for attempt, path in enumerate(login_candidate_paths(ctx), start=1):
display_url = xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, path, ctx.login_password))
display_url = xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, path))
emit_operation_feedback(
state,
stage,
@@ -767,14 +802,14 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
state,
stage,
"xdfx_auth 获取 token 失败,准备尝试下一个候选登录路径",
{"url": exc.url, "status": exc.status, "response": compact(exc.response, 1000), "exception": str(exc)},
{"url": exc.url, "status": exc.status, "response": safe_compact(exc.response, 1000), "exception": str(exc)},
)
record_error(
state,
stage,
"自动登录失败,未能通过 xdfx_auth 获取 access_token",
url=xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, ctx.login_path or "/api/v1/auth/login/username", ctx.login_password)),
url=xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, ctx.login_path or "/api/auth/login")),
status=last_status,
request=payload,
response=last_response,
@@ -2013,6 +2048,13 @@ def object_for_audience(audience: dict[str, Any]) -> dict[str, Any]:
}
def selected_accounts_for_item(item: dict[str, Any], *, audience: bool) -> list[Any]:
if audience:
accounts = item.get("accounts") or []
return accounts or [object_for_audience(item)]
return [item.get("account") or item]
def downstream_limit(ctx: FlowContext, *, audience: bool) -> int:
"""Limit expensive downstream analysis while keeping 3 upstream rows stored."""
raw = ctx.downstream_audiences if audience else ctx.downstream_pathways
@@ -2263,7 +2305,7 @@ def run_vuln_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool) ->
continue
if str(owner_id) in state.get(output_key, {}) and state[output_key][str(owner_id)] and not ctx.dry_run:
continue
zhanghao = item.get("accounts") or [object_for_audience(item)] if audience else [item.get("account") or item]
zhanghao = selected_accounts_for_item(item, audience=audience)
prompt = (
"你是中国对台湾方向的认知战专家,你要传播行动要求的叙事主题。请基于该受众目标的画像、关注议题、社会分工定位、利益诉求与上一轮账号佐证,分析该受众目标有哪些脆弱点可以成为后续策略设计的关键要素。请输出理由、脆弱点1、理由1、脆弱点2、理由2、脆弱点3、理由3,只返回JSON。"
if audience
@@ -2349,7 +2391,9 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool
record_error(state, stage, "没有可用脆弱点,跳过策略生成", request=item)
continue
target_names = [item.get("targetAudience")] if audience else (item.get("relatedAudiences") or [item.get("accountName")])
pingtai = state.get("platform") or (platform_label(item.get("account") or {}) if not audience else "")
zhanghao = selected_accounts_for_item(item, audience=audience)
first_account = zhanghao[0] if zhanghao and isinstance(zhanghao[0], dict) else {}
pingtai = state.get("platform") or platform_label(first_account)
payload = {
"user_message": DEFAULT_STRATEGY_PROMPT_AUDIENCE if audience else DEFAULT_STRATEGY_PROMPT_PATHWAY,
"cuiruodian": vuln_texts,
@@ -2357,6 +2401,7 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool
"shouzhong": [name for name in target_names if name],
"mudi": extract_goal(state["mubiao"]),
"heiliao": [],
"zhanghao": zhanghao,
"history": [],
"stream": True,
"model_name": "OpenAI3",
@@ -2564,8 +2609,8 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--platform", default=None)
parser.add_argument("--header", action="append", default=[], help="附加非鉴权 HTTP 请求头,格式为 'Key: Value';Authorization 会被忽略。")
parser.add_argument("--login-username", default=os.getenv("XDFX_LOGIN_USERNAME", "admin"), help="xdfx_auth 启动阶段自动登录使用的用户名。")
parser.add_argument("--login-password", default=os.getenv("XDFX_LOGIN_PASSWORD", "ch@user"), help="用户名登录接口的 password 查询参数。")
parser.add_argument("--login-path", default=os.getenv("XDFX_LOGIN_PATH", "/api/v1/auth/login/username"), help="登录接口路径或完整 URL。")
parser.add_argument("--login-password", default=os.getenv("XDFX_LOGIN_PASSWORD", "ch@user"), help="登录接口 JSON 请求体中的 password。")
parser.add_argument("--login-path", default=os.getenv("XDFX_LOGIN_PATH", "/api/auth/login"), help="登录接口路径或完整 URL。")
parser.add_argument("--no-auto-login", action="store_true", help="关闭 xdfx_auth 自动登录和 401 刷新。")
parser.add_argument("--verify-ssl", action="store_true", help="校验 HTTPS 证书。默认不校验,以兼容内网/UAT 自签证书。")
parser.add_argument("--timeout", type=float, default=90)
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""Authentication helper for xdfx skill scripts.
The caller should not pass tokens through command-line arguments. This module
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.
"""
@@ -12,7 +12,6 @@ import json
import re
import ssl
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass
from typing import Any
@@ -58,12 +57,8 @@ def ssl_context(*, verify_ssl: bool) -> ssl.SSLContext | None:
return ssl._create_unverified_context()
def build_login_url(api_base: str, path: str, password: str) -> str:
base = path if re.match(r"^https?://", path, flags=re.IGNORECASE) else api_base.rstrip("/") + "/" + path.lstrip("/")
if password:
separator = "&" if "?" in base else "?"
base = f"{base}{separator}password={urllib.parse.quote(password, safe='')}"
return base
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:
@@ -106,13 +101,13 @@ def fetch_access_token(
api_base: str,
username: str = "admin",
password: str = "ch@user",
login_path: str = "/api/v1/auth/login/username",
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, password)
payload = {"username": username}
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")
@@ -122,6 +122,8 @@ REQUEST_FIELD_LABELS = {
"temperature": "温度",
"usermessage": "用户消息",
"user_message": "用户消息",
"username": "登录用户名",
"password": "登录密码",
"mubiao": "行动目标",
"zhanghao": "账号列表",
"taskId": "任务ID",
@@ -148,6 +150,8 @@ REQUEST_FIELD_LABELS = {
"step": "步骤状态",
}
SENSITIVE_KEY_PARTS = ("password", "token", "authorization", "access_token", "accesstoken", "authtoken")
# Platform values mirror the frontend dictionaries used by xdfx strategy pages.
PLATFORM_MAP = {
"facebook": 1,
@@ -228,7 +232,7 @@ class FlowContext:
auto_login: bool = True
login_username: str = "admin"
login_password: str = "ch@user"
login_path: str = "/api/v1/auth/login/username"
login_path: str = "/api/auth/login"
login_attempt_count: int = 0
@@ -249,6 +253,37 @@ def compact(value: Any, limit: int = 1400) -> str:
return text[:limit] + f"... <已截断 {len(text) - limit} 个字符>"
def is_sensitive_key(key: Any) -> bool:
normalized = re.sub(r"[^a-z0-9_]", "", str(key or "").lower())
return any(part in normalized for part in SENSITIVE_KEY_PARTS)
def redact_sensitive(value: Any) -> Any:
if isinstance(value, dict):
return {key: "***" if is_sensitive_key(key) else redact_sensitive(item) for key, item in value.items()}
if isinstance(value, list):
return [redact_sensitive(item) for item in value]
if isinstance(value, tuple):
return tuple(redact_sensitive(item) for item in value)
if isinstance(value, str):
text = re.sub(r"(?i)(Bearer\s+)[^\s\",}]+", r"\1***", value)
text = re.sub(
r"(?i)(\"(?:password|token|authToken|access_token|accessToken|authorization)\"\s*:\s*)\"[^\"]*\"",
r'\1"***"',
text,
)
return re.sub(
r"(?i)((?:password|token|authToken|access_token|accessToken|authorization)=)[^&\s]+",
r"\1***",
text,
)
return value
def safe_compact(value: Any, limit: int = 1400) -> str:
return compact(redact_sensitive(value), limit)
def stable_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
@@ -636,10 +671,10 @@ def record_error(
"message": message,
"url": url,
"status": status,
"request": compact(request, 1200) if request is not None else None,
"response": compact(response, 2000) if response is not None else None,
"request": safe_compact(request, 1200) if request is not None else None,
"response": safe_compact(response, 2000) if response is not None else None,
"exceptionType": type(exception).__name__ if exception else None,
"exception": str(exception) if exception else None,
"exception": safe_compact(str(exception), 1000) if exception else None,
}
state.setdefault("errors", []).append(item)
state["updatedAt"] = utc_now()
@@ -696,8 +731,8 @@ def ssl_context(ctx: FlowContext) -> ssl.SSLContext | None:
def login_candidate_paths(ctx: FlowContext) -> list[str]:
paths = [
ctx.login_path or "/api/v1/auth/login/username",
"/api/v1/auth/login/username",
ctx.login_path or "/api/auth/login",
"/api/auth/login",
]
seen: set[str] = set()
unique: list[str] = []
@@ -726,12 +761,12 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
return False
ctx.login_attempt_count += 1
payload = {"username": ctx.login_username}
payload = {"username": ctx.login_username, "password": ctx.login_password}
last_status: int | None = None
last_response = ""
last_exc: BaseException | None = None
for attempt, path in enumerate(login_candidate_paths(ctx), start=1):
display_url = xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, path, ctx.login_password))
display_url = xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, path))
emit_operation_feedback(
state,
stage,
@@ -767,14 +802,14 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
state,
stage,
"xdfx_auth 获取 token 失败,准备尝试下一个候选登录路径",
{"url": exc.url, "status": exc.status, "response": compact(exc.response, 1000), "exception": str(exc)},
{"url": exc.url, "status": exc.status, "response": safe_compact(exc.response, 1000), "exception": str(exc)},
)
record_error(
state,
stage,
"自动登录失败,未能通过 xdfx_auth 获取 access_token",
url=xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, ctx.login_path or "/api/v1/auth/login/username", ctx.login_password)),
url=xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, ctx.login_path or "/api/auth/login")),
status=last_status,
request=payload,
response=last_response,
@@ -2013,6 +2048,13 @@ def object_for_audience(audience: dict[str, Any]) -> dict[str, Any]:
}
def selected_accounts_for_item(item: dict[str, Any], *, audience: bool) -> list[Any]:
if audience:
accounts = item.get("accounts") or []
return accounts or [object_for_audience(item)]
return [item.get("account") or item]
def downstream_limit(ctx: FlowContext, *, audience: bool) -> int:
"""Limit expensive downstream analysis while keeping 3 upstream rows stored."""
raw = ctx.downstream_audiences if audience else ctx.downstream_pathways
@@ -2263,7 +2305,7 @@ def run_vuln_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool) ->
continue
if str(owner_id) in state.get(output_key, {}) and state[output_key][str(owner_id)] and not ctx.dry_run:
continue
zhanghao = item.get("accounts") or [object_for_audience(item)] if audience else [item.get("account") or item]
zhanghao = selected_accounts_for_item(item, audience=audience)
prompt = (
"你是中国对台湾方向的认知战专家,你要传播行动要求的叙事主题。请基于该受众目标的画像、关注议题、社会分工定位、利益诉求与上一轮账号佐证,分析该受众目标有哪些脆弱点可以成为后续策略设计的关键要素。请输出理由、脆弱点1、理由1、脆弱点2、理由2、脆弱点3、理由3,只返回JSON。"
if audience
@@ -2349,7 +2391,9 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool
record_error(state, stage, "没有可用脆弱点,跳过策略生成", request=item)
continue
target_names = [item.get("targetAudience")] if audience else (item.get("relatedAudiences") or [item.get("accountName")])
pingtai = state.get("platform") or (platform_label(item.get("account") or {}) if not audience else "")
zhanghao = selected_accounts_for_item(item, audience=audience)
first_account = zhanghao[0] if zhanghao and isinstance(zhanghao[0], dict) else {}
pingtai = state.get("platform") or platform_label(first_account)
payload = {
"user_message": DEFAULT_STRATEGY_PROMPT_AUDIENCE if audience else DEFAULT_STRATEGY_PROMPT_PATHWAY,
"cuiruodian": vuln_texts,
@@ -2357,6 +2401,7 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool
"shouzhong": [name for name in target_names if name],
"mudi": extract_goal(state["mubiao"]),
"heiliao": [],
"zhanghao": zhanghao,
"history": [],
"stream": True,
"model_name": "OpenAI3",
@@ -2564,8 +2609,8 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--platform", default=None)
parser.add_argument("--header", action="append", default=[], help="附加非鉴权 HTTP 请求头,格式为 'Key: Value';Authorization 会被忽略。")
parser.add_argument("--login-username", default=os.getenv("XDFX_LOGIN_USERNAME", "admin"), help="xdfx_auth 启动阶段自动登录使用的用户名。")
parser.add_argument("--login-password", default=os.getenv("XDFX_LOGIN_PASSWORD", "ch@user"), help="用户名登录接口的 password 查询参数。")
parser.add_argument("--login-path", default=os.getenv("XDFX_LOGIN_PATH", "/api/v1/auth/login/username"), help="登录接口路径或完整 URL。")
parser.add_argument("--login-password", default=os.getenv("XDFX_LOGIN_PASSWORD", "ch@user"), help="登录接口 JSON 请求体中的 password。")
parser.add_argument("--login-path", default=os.getenv("XDFX_LOGIN_PATH", "/api/auth/login"), help="登录接口路径或完整 URL。")
parser.add_argument("--no-auto-login", action="store_true", help="关闭 xdfx_auth 自动登录和 401 刷新。")
parser.add_argument("--verify-ssl", action="store_true", help="校验 HTTPS 证书。默认不校验,以兼容内网/UAT 自签证书。")
parser.add_argument("--timeout", type=float, default=90)
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""Authentication helper for xdfx skill scripts.
The caller should not pass tokens through command-line arguments. This module
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.
"""
@@ -12,7 +12,6 @@ import json
import re
import ssl
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass
from typing import Any
@@ -58,12 +57,8 @@ def ssl_context(*, verify_ssl: bool) -> ssl.SSLContext | None:
return ssl._create_unverified_context()
def build_login_url(api_base: str, path: str, password: str) -> str:
base = path if re.match(r"^https?://", path, flags=re.IGNORECASE) else api_base.rstrip("/") + "/" + path.lstrip("/")
if password:
separator = "&" if "?" in base else "?"
base = f"{base}{separator}password={urllib.parse.quote(password, safe='')}"
return base
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:
@@ -106,13 +101,13 @@ def fetch_access_token(
api_base: str,
username: str = "admin",
password: str = "ch@user",
login_path: str = "/api/v1/auth/login/username",
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, password)
payload = {"username": username}
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")
@@ -122,6 +122,8 @@ REQUEST_FIELD_LABELS = {
"temperature": "温度",
"usermessage": "用户消息",
"user_message": "用户消息",
"username": "登录用户名",
"password": "登录密码",
"mubiao": "行动目标",
"zhanghao": "账号列表",
"taskId": "任务ID",
@@ -148,6 +150,8 @@ REQUEST_FIELD_LABELS = {
"step": "步骤状态",
}
SENSITIVE_KEY_PARTS = ("password", "token", "authorization", "access_token", "accesstoken", "authtoken")
# Platform values mirror the frontend dictionaries used by xdfx strategy pages.
PLATFORM_MAP = {
"facebook": 1,
@@ -228,7 +232,7 @@ class FlowContext:
auto_login: bool = True
login_username: str = "admin"
login_password: str = "ch@user"
login_path: str = "/api/v1/auth/login/username"
login_path: str = "/api/auth/login"
login_attempt_count: int = 0
@@ -249,6 +253,37 @@ def compact(value: Any, limit: int = 1400) -> str:
return text[:limit] + f"... <已截断 {len(text) - limit} 个字符>"
def is_sensitive_key(key: Any) -> bool:
normalized = re.sub(r"[^a-z0-9_]", "", str(key or "").lower())
return any(part in normalized for part in SENSITIVE_KEY_PARTS)
def redact_sensitive(value: Any) -> Any:
if isinstance(value, dict):
return {key: "***" if is_sensitive_key(key) else redact_sensitive(item) for key, item in value.items()}
if isinstance(value, list):
return [redact_sensitive(item) for item in value]
if isinstance(value, tuple):
return tuple(redact_sensitive(item) for item in value)
if isinstance(value, str):
text = re.sub(r"(?i)(Bearer\s+)[^\s\",}]+", r"\1***", value)
text = re.sub(
r"(?i)(\"(?:password|token|authToken|access_token|accessToken|authorization)\"\s*:\s*)\"[^\"]*\"",
r'\1"***"',
text,
)
return re.sub(
r"(?i)((?:password|token|authToken|access_token|accessToken|authorization)=)[^&\s]+",
r"\1***",
text,
)
return value
def safe_compact(value: Any, limit: int = 1400) -> str:
return compact(redact_sensitive(value), limit)
def stable_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
@@ -636,10 +671,10 @@ def record_error(
"message": message,
"url": url,
"status": status,
"request": compact(request, 1200) if request is not None else None,
"response": compact(response, 2000) if response is not None else None,
"request": safe_compact(request, 1200) if request is not None else None,
"response": safe_compact(response, 2000) if response is not None else None,
"exceptionType": type(exception).__name__ if exception else None,
"exception": str(exception) if exception else None,
"exception": safe_compact(str(exception), 1000) if exception else None,
}
state.setdefault("errors", []).append(item)
state["updatedAt"] = utc_now()
@@ -696,8 +731,8 @@ def ssl_context(ctx: FlowContext) -> ssl.SSLContext | None:
def login_candidate_paths(ctx: FlowContext) -> list[str]:
paths = [
ctx.login_path or "/api/v1/auth/login/username",
"/api/v1/auth/login/username",
ctx.login_path or "/api/auth/login",
"/api/auth/login",
]
seen: set[str] = set()
unique: list[str] = []
@@ -726,12 +761,12 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
return False
ctx.login_attempt_count += 1
payload = {"username": ctx.login_username}
payload = {"username": ctx.login_username, "password": ctx.login_password}
last_status: int | None = None
last_response = ""
last_exc: BaseException | None = None
for attempt, path in enumerate(login_candidate_paths(ctx), start=1):
display_url = xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, path, ctx.login_password))
display_url = xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, path))
emit_operation_feedback(
state,
stage,
@@ -767,14 +802,14 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
state,
stage,
"xdfx_auth 获取 token 失败,准备尝试下一个候选登录路径",
{"url": exc.url, "status": exc.status, "response": compact(exc.response, 1000), "exception": str(exc)},
{"url": exc.url, "status": exc.status, "response": safe_compact(exc.response, 1000), "exception": str(exc)},
)
record_error(
state,
stage,
"自动登录失败,未能通过 xdfx_auth 获取 access_token",
url=xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, ctx.login_path or "/api/v1/auth/login/username", ctx.login_password)),
url=xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, ctx.login_path or "/api/auth/login")),
status=last_status,
request=payload,
response=last_response,
@@ -2013,6 +2048,13 @@ def object_for_audience(audience: dict[str, Any]) -> dict[str, Any]:
}
def selected_accounts_for_item(item: dict[str, Any], *, audience: bool) -> list[Any]:
if audience:
accounts = item.get("accounts") or []
return accounts or [object_for_audience(item)]
return [item.get("account") or item]
def downstream_limit(ctx: FlowContext, *, audience: bool) -> int:
"""Limit expensive downstream analysis while keeping 3 upstream rows stored."""
raw = ctx.downstream_audiences if audience else ctx.downstream_pathways
@@ -2263,7 +2305,7 @@ def run_vuln_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool) ->
continue
if str(owner_id) in state.get(output_key, {}) and state[output_key][str(owner_id)] and not ctx.dry_run:
continue
zhanghao = item.get("accounts") or [object_for_audience(item)] if audience else [item.get("account") or item]
zhanghao = selected_accounts_for_item(item, audience=audience)
prompt = (
"你是中国对台湾方向的认知战专家,你要传播行动要求的叙事主题。请基于该受众目标的画像、关注议题、社会分工定位、利益诉求与上一轮账号佐证,分析该受众目标有哪些脆弱点可以成为后续策略设计的关键要素。请输出理由、脆弱点1、理由1、脆弱点2、理由2、脆弱点3、理由3,只返回JSON。"
if audience
@@ -2349,7 +2391,9 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool
record_error(state, stage, "没有可用脆弱点,跳过策略生成", request=item)
continue
target_names = [item.get("targetAudience")] if audience else (item.get("relatedAudiences") or [item.get("accountName")])
pingtai = state.get("platform") or (platform_label(item.get("account") or {}) if not audience else "")
zhanghao = selected_accounts_for_item(item, audience=audience)
first_account = zhanghao[0] if zhanghao and isinstance(zhanghao[0], dict) else {}
pingtai = state.get("platform") or platform_label(first_account)
payload = {
"user_message": DEFAULT_STRATEGY_PROMPT_AUDIENCE if audience else DEFAULT_STRATEGY_PROMPT_PATHWAY,
"cuiruodian": vuln_texts,
@@ -2357,6 +2401,7 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool
"shouzhong": [name for name in target_names if name],
"mudi": extract_goal(state["mubiao"]),
"heiliao": [],
"zhanghao": zhanghao,
"history": [],
"stream": True,
"model_name": "OpenAI3",
@@ -2564,8 +2609,8 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--platform", default=None)
parser.add_argument("--header", action="append", default=[], help="附加非鉴权 HTTP 请求头,格式为 'Key: Value';Authorization 会被忽略。")
parser.add_argument("--login-username", default=os.getenv("XDFX_LOGIN_USERNAME", "admin"), help="xdfx_auth 启动阶段自动登录使用的用户名。")
parser.add_argument("--login-password", default=os.getenv("XDFX_LOGIN_PASSWORD", "ch@user"), help="用户名登录接口的 password 查询参数。")
parser.add_argument("--login-path", default=os.getenv("XDFX_LOGIN_PATH", "/api/v1/auth/login/username"), help="登录接口路径或完整 URL。")
parser.add_argument("--login-password", default=os.getenv("XDFX_LOGIN_PASSWORD", "ch@user"), help="登录接口 JSON 请求体中的 password。")
parser.add_argument("--login-path", default=os.getenv("XDFX_LOGIN_PATH", "/api/auth/login"), help="登录接口路径或完整 URL。")
parser.add_argument("--no-auto-login", action="store_true", help="关闭 xdfx_auth 自动登录和 401 刷新。")
parser.add_argument("--verify-ssl", action="store_true", help="校验 HTTPS 证书。默认不校验,以兼容内网/UAT 自签证书。")
parser.add_argument("--timeout", type=float, default=90)
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""Authentication helper for xdfx skill scripts.
The caller should not pass tokens through command-line arguments. This module
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.
"""
@@ -12,7 +12,6 @@ import json
import re
import ssl
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass
from typing import Any
@@ -58,12 +57,8 @@ def ssl_context(*, verify_ssl: bool) -> ssl.SSLContext | None:
return ssl._create_unverified_context()
def build_login_url(api_base: str, path: str, password: str) -> str:
base = path if re.match(r"^https?://", path, flags=re.IGNORECASE) else api_base.rstrip("/") + "/" + path.lstrip("/")
if password:
separator = "&" if "?" in base else "?"
base = f"{base}{separator}password={urllib.parse.quote(password, safe='')}"
return base
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:
@@ -106,13 +101,13 @@ def fetch_access_token(
api_base: str,
username: str = "admin",
password: str = "ch@user",
login_path: str = "/api/v1/auth/login/username",
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, password)
payload = {"username": username}
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")
@@ -122,6 +122,8 @@ REQUEST_FIELD_LABELS = {
"temperature": "温度",
"usermessage": "用户消息",
"user_message": "用户消息",
"username": "登录用户名",
"password": "登录密码",
"mubiao": "行动目标",
"zhanghao": "账号列表",
"taskId": "任务ID",
@@ -148,6 +150,8 @@ REQUEST_FIELD_LABELS = {
"step": "步骤状态",
}
SENSITIVE_KEY_PARTS = ("password", "token", "authorization", "access_token", "accesstoken", "authtoken")
# Platform values mirror the frontend dictionaries used by xdfx strategy pages.
PLATFORM_MAP = {
"facebook": 1,
@@ -228,7 +232,7 @@ class FlowContext:
auto_login: bool = True
login_username: str = "admin"
login_password: str = "ch@user"
login_path: str = "/api/v1/auth/login/username"
login_path: str = "/api/auth/login"
login_attempt_count: int = 0
@@ -249,6 +253,37 @@ def compact(value: Any, limit: int = 1400) -> str:
return text[:limit] + f"... <已截断 {len(text) - limit} 个字符>"
def is_sensitive_key(key: Any) -> bool:
normalized = re.sub(r"[^a-z0-9_]", "", str(key or "").lower())
return any(part in normalized for part in SENSITIVE_KEY_PARTS)
def redact_sensitive(value: Any) -> Any:
if isinstance(value, dict):
return {key: "***" if is_sensitive_key(key) else redact_sensitive(item) for key, item in value.items()}
if isinstance(value, list):
return [redact_sensitive(item) for item in value]
if isinstance(value, tuple):
return tuple(redact_sensitive(item) for item in value)
if isinstance(value, str):
text = re.sub(r"(?i)(Bearer\s+)[^\s\",}]+", r"\1***", value)
text = re.sub(
r"(?i)(\"(?:password|token|authToken|access_token|accessToken|authorization)\"\s*:\s*)\"[^\"]*\"",
r'\1"***"',
text,
)
return re.sub(
r"(?i)((?:password|token|authToken|access_token|accessToken|authorization)=)[^&\s]+",
r"\1***",
text,
)
return value
def safe_compact(value: Any, limit: int = 1400) -> str:
return compact(redact_sensitive(value), limit)
def stable_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
@@ -636,10 +671,10 @@ def record_error(
"message": message,
"url": url,
"status": status,
"request": compact(request, 1200) if request is not None else None,
"response": compact(response, 2000) if response is not None else None,
"request": safe_compact(request, 1200) if request is not None else None,
"response": safe_compact(response, 2000) if response is not None else None,
"exceptionType": type(exception).__name__ if exception else None,
"exception": str(exception) if exception else None,
"exception": safe_compact(str(exception), 1000) if exception else None,
}
state.setdefault("errors", []).append(item)
state["updatedAt"] = utc_now()
@@ -696,8 +731,8 @@ def ssl_context(ctx: FlowContext) -> ssl.SSLContext | None:
def login_candidate_paths(ctx: FlowContext) -> list[str]:
paths = [
ctx.login_path or "/api/v1/auth/login/username",
"/api/v1/auth/login/username",
ctx.login_path or "/api/auth/login",
"/api/auth/login",
]
seen: set[str] = set()
unique: list[str] = []
@@ -726,12 +761,12 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
return False
ctx.login_attempt_count += 1
payload = {"username": ctx.login_username}
payload = {"username": ctx.login_username, "password": ctx.login_password}
last_status: int | None = None
last_response = ""
last_exc: BaseException | None = None
for attempt, path in enumerate(login_candidate_paths(ctx), start=1):
display_url = xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, path, ctx.login_password))
display_url = xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, path))
emit_operation_feedback(
state,
stage,
@@ -767,14 +802,14 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
state,
stage,
"xdfx_auth 获取 token 失败,准备尝试下一个候选登录路径",
{"url": exc.url, "status": exc.status, "response": compact(exc.response, 1000), "exception": str(exc)},
{"url": exc.url, "status": exc.status, "response": safe_compact(exc.response, 1000), "exception": str(exc)},
)
record_error(
state,
stage,
"自动登录失败,未能通过 xdfx_auth 获取 access_token",
url=xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, ctx.login_path or "/api/v1/auth/login/username", ctx.login_password)),
url=xdfx_auth.redact_url(xdfx_auth.build_login_url(ctx.api_base, ctx.login_path or "/api/auth/login")),
status=last_status,
request=payload,
response=last_response,
@@ -2013,6 +2048,13 @@ def object_for_audience(audience: dict[str, Any]) -> dict[str, Any]:
}
def selected_accounts_for_item(item: dict[str, Any], *, audience: bool) -> list[Any]:
if audience:
accounts = item.get("accounts") or []
return accounts or [object_for_audience(item)]
return [item.get("account") or item]
def downstream_limit(ctx: FlowContext, *, audience: bool) -> int:
"""Limit expensive downstream analysis while keeping 3 upstream rows stored."""
raw = ctx.downstream_audiences if audience else ctx.downstream_pathways
@@ -2263,7 +2305,7 @@ def run_vuln_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool) ->
continue
if str(owner_id) in state.get(output_key, {}) and state[output_key][str(owner_id)] and not ctx.dry_run:
continue
zhanghao = item.get("accounts") or [object_for_audience(item)] if audience else [item.get("account") or item]
zhanghao = selected_accounts_for_item(item, audience=audience)
prompt = (
"你是中国对台湾方向的认知战专家,你要传播行动要求的叙事主题。请基于该受众目标的画像、关注议题、社会分工定位、利益诉求与上一轮账号佐证,分析该受众目标有哪些脆弱点可以成为后续策略设计的关键要素。请输出理由、脆弱点1、理由1、脆弱点2、理由2、脆弱点3、理由3,只返回JSON。"
if audience
@@ -2349,7 +2391,9 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool
record_error(state, stage, "没有可用脆弱点,跳过策略生成", request=item)
continue
target_names = [item.get("targetAudience")] if audience else (item.get("relatedAudiences") or [item.get("accountName")])
pingtai = state.get("platform") or (platform_label(item.get("account") or {}) if not audience else "")
zhanghao = selected_accounts_for_item(item, audience=audience)
first_account = zhanghao[0] if zhanghao and isinstance(zhanghao[0], dict) else {}
pingtai = state.get("platform") or platform_label(first_account)
payload = {
"user_message": DEFAULT_STRATEGY_PROMPT_AUDIENCE if audience else DEFAULT_STRATEGY_PROMPT_PATHWAY,
"cuiruodian": vuln_texts,
@@ -2357,6 +2401,7 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool
"shouzhong": [name for name in target_names if name],
"mudi": extract_goal(state["mubiao"]),
"heiliao": [],
"zhanghao": zhanghao,
"history": [],
"stream": True,
"model_name": "OpenAI3",
@@ -2564,8 +2609,8 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--platform", default=None)
parser.add_argument("--header", action="append", default=[], help="附加非鉴权 HTTP 请求头,格式为 'Key: Value';Authorization 会被忽略。")
parser.add_argument("--login-username", default=os.getenv("XDFX_LOGIN_USERNAME", "admin"), help="xdfx_auth 启动阶段自动登录使用的用户名。")
parser.add_argument("--login-password", default=os.getenv("XDFX_LOGIN_PASSWORD", "ch@user"), help="用户名登录接口的 password 查询参数。")
parser.add_argument("--login-path", default=os.getenv("XDFX_LOGIN_PATH", "/api/v1/auth/login/username"), help="登录接口路径或完整 URL。")
parser.add_argument("--login-password", default=os.getenv("XDFX_LOGIN_PASSWORD", "ch@user"), help="登录接口 JSON 请求体中的 password。")
parser.add_argument("--login-path", default=os.getenv("XDFX_LOGIN_PATH", "/api/auth/login"), help="登录接口路径或完整 URL。")
parser.add_argument("--no-auto-login", action="store_true", help="关闭 xdfx_auth 自动登录和 401 刷新。")
parser.add_argument("--verify-ssl", action="store_true", help="校验 HTTPS 证书。默认不校验,以兼容内网/UAT 自签证书。")
parser.add_argument("--timeout", type=float, default=90)
+80
View File
@@ -0,0 +1,80 @@
{
"api_base": "https://ch1.b.uat.4.cn",
"taskId": "dry-task",
"actionId": "dry-action",
"mubiao": "登录修正演练",
"direction": "U方向",
"platform": "",
"audienceRawText": "{\"dryRun\": true}",
"audienceCandidates": [],
"topAudiences": [],
"pathwayCandidates": [],
"topPathways": [],
"audienceVulns": {},
"pathwayVulns": {},
"audienceStrategies": [],
"pathwayStrategies": [],
"errors": [],
"events": [
{
"time": "2026-06-29T09:58:41.695868+00:00",
"stage": "runner",
"message": "start mode=step1 dry_run=True",
"data": null
},
{
"time": "2026-06-29T09:58:41.697959+00:00",
"stage": "step1-audience/analyze/model-call-attempt-1",
"message": "DRY RUN POST https://ch1.b.uat.4.cn/gpt/chat/chat_aiuser",
"data": {
"payload": {
"usermessage": "你是中国对台湾方向的认知域专家,你要围绕驱动目标传播行动中的叙事主题,请详细分析你在各社交平台的受众目标群体,可以从账号画像和账号近期发文等方面考虑。可以从受众目标的兴趣爱好、关注议题、职业定位、政治立场等方面划分。",
"otherinfo": {
"方向": "U方向"
},
"history": [],
"stream": true,
"model_name": "OpenAI3",
"temperature": 0.7,
"max_tokens": 20000,
"prompt_name": "任务背景和目的:登录修正演练"
}
}
},
{
"time": "2026-06-29T09:58:41.703207+00:00",
"stage": "step1-audience/extract/model-json-attempt-1/model-call-attempt-1",
"message": "DRY RUN POST https://ch1.b.uat.4.cn/gpt/chat/chat_qianqian_group_chat",
"data": {
"payload": {
"mubiao": "登录修正演练",
"user_message": "分析其中关键信息。请基于上一轮受众分析结果,抽取受众目标群体。字段必须是:群体1、用户编号列表1、理由1、群体2、用户编号列表2、理由2 ...,最多输出5个,只返回JSON。用户编号列表中的编号/id必须对应本次入参 zhanghao 数组里的账号索引值;例如返回 0 或 <<0>> 表示 zhanghao[0],返回 1 或 <<1>> 表示 zhanghao[1]。不要生成不存在于 zhanghao 的编号。",
"zhanghao": [],
"history": [],
"stream": true,
"model_name": "OpenAI3",
"temperature": 0.7,
"max_tokens": 20000,
"prompt_name": "登录修正演练"
}
}
},
{
"time": "2026-06-29T09:58:41.703207+00:00",
"stage": "step1-audience",
"message": "完成,保存受众 0 个",
"data": null
}
],
"startedAt": "2026-06-29T09:58:41.694826+00:00",
"updatedAt": "2026-06-29T09:58:41.709191+00:00",
"_statePath": "C:\\tmp\\xdfx-login-fix-state.json",
"_summaryPath": "C:\\tmp\\xdfx-login-fix-summary.md",
"_autoLogin": true,
"_verifySsl": false,
"_downstreamLimits": {
"audiences": 2,
"pathways": 2
},
"audienceAccounts": []
}
+18
View File
@@ -0,0 +1,18 @@
# xdfx 分析入库执行摘要
- 更新时间:2026-06-29T09:58:41.709191+00:00
- taskId:dry-task
- actionId:dry-action
- 受众入库:0
- 传播节点入库:0
- 下游受众选择:0
- 下游传播节点选择:0
- 受众脆弱点入库:0
- 传播节点脆弱点入库:0
- 受众策略入库:0
- 传播节点策略入库:0
- 错误数:0
## 受众
## 传播节点