技能修改

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 并重试原接口一次。 默认关闭 HTTPS 证书校验,用于兼容 UAT/内网自签证书;如果部署环境证书链正常,可以追加 `--verify-ssl`。接口返回 401 时仍由 `xdfx_auth.py` 刷新 token 并重试原接口一次。
@@ -31,8 +31,8 @@ scripts/xdfx_flow.py
默认调用: 默认调用:
```text ```text
POST /api/v1/auth/login/username?password=ch%40user POST /api/auth/login
body: {"username":"admin"} body: {"username":"admin","password":"ch@user"}
``` ```
拿到 `access_token` 后会写入内部 `Authorization: Bearer ...`。不要通过任何命令行参数传 token;脚本会忽略外部 Authorization,统一由 `xdfx_auth.py` 获取和刷新。登录接口和 401 重试都会输出 `操作反馈` / `错误反馈`,但反馈中的 `password`、`token` 会脱敏。默认关闭 HTTPS 证书校验,适配 UAT/内网自签证书;如果环境证书链正常,可以追加 `--verify-ssl`。 拿到 `access_token` 后会写入内部 `Authorization: Bearer ...`。不要通过任何命令行参数传 token;脚本会忽略外部 Authorization,统一由 `xdfx_auth.py` 获取和刷新。登录接口和 401 重试都会输出 `操作反馈` / `错误反馈`,但反馈中的 `password`、`token` 会脱敏。默认关闭 HTTPS 证书校验,适配 UAT/内网自签证书;如果环境证书链正常,可以追加 `--verify-ssl`。
@@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Authentication helper for xdfx skill scripts. """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 owns the fixed admin login flow and returns a Bearer token before business
interfaces are called. interfaces are called.
""" """
@@ -12,7 +12,6 @@ import json
import re import re
import ssl import ssl
import urllib.error import urllib.error
import urllib.parse
import urllib.request import urllib.request
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
@@ -58,12 +57,8 @@ def ssl_context(*, verify_ssl: bool) -> ssl.SSLContext | None:
return ssl._create_unverified_context() return ssl._create_unverified_context()
def build_login_url(api_base: str, path: str, password: str) -> str: def build_login_url(api_base: str, path: str) -> str:
base = path if re.match(r"^https?://", path, flags=re.IGNORECASE) else api_base.rstrip("/") + "/" + path.lstrip("/") return 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 extract_access_token(data: Any) -> str | None: def extract_access_token(data: Any) -> str | None:
@@ -106,13 +101,13 @@ def fetch_access_token(
api_base: str, api_base: str,
username: str = "admin", username: str = "admin",
password: str = "ch@user", password: str = "ch@user",
login_path: str = "/api/v1/auth/login/username", login_path: str = "/api/auth/login",
timeout: float = 90, timeout: float = 90,
verify_ssl: bool = False, verify_ssl: bool = False,
) -> AuthResult: ) -> AuthResult:
"""Login once and return the extracted access token.""" """Login once and return the extracted access token."""
url = build_login_url(api_base, login_path, password) url = build_login_url(api_base, login_path)
payload = {"username": username} payload = {"username": username, "password": password}
data = json.dumps(payload, ensure_ascii=False).encode("utf-8") data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
headers = {"Content-Type": "application/json", "Accept": "application/json"} headers = {"Content-Type": "application/json", "Accept": "application/json"}
req = urllib.request.Request(url, data=data, headers=headers, method="POST") req = urllib.request.Request(url, data=data, headers=headers, method="POST")
@@ -122,6 +122,8 @@ REQUEST_FIELD_LABELS = {
"temperature": "温度", "temperature": "温度",
"usermessage": "用户消息", "usermessage": "用户消息",
"user_message": "用户消息", "user_message": "用户消息",
"username": "登录用户名",
"password": "登录密码",
"mubiao": "行动目标", "mubiao": "行动目标",
"zhanghao": "账号列表", "zhanghao": "账号列表",
"taskId": "任务ID", "taskId": "任务ID",
@@ -148,6 +150,8 @@ REQUEST_FIELD_LABELS = {
"step": "步骤状态", "step": "步骤状态",
} }
SENSITIVE_KEY_PARTS = ("password", "token", "authorization", "access_token", "accesstoken", "authtoken")
# Platform values mirror the frontend dictionaries used by xdfx strategy pages. # Platform values mirror the frontend dictionaries used by xdfx strategy pages.
PLATFORM_MAP = { PLATFORM_MAP = {
"facebook": 1, "facebook": 1,
@@ -228,7 +232,7 @@ class FlowContext:
auto_login: bool = True auto_login: bool = True
login_username: str = "admin" login_username: str = "admin"
login_password: str = "ch@user" login_password: str = "ch@user"
login_path: str = "/api/v1/auth/login/username" login_path: str = "/api/auth/login"
login_attempt_count: int = 0 login_attempt_count: int = 0
@@ -249,6 +253,37 @@ def compact(value: Any, limit: int = 1400) -> str:
return text[:limit] + f"... <已截断 {len(text) - limit} 个字符>" 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: def stable_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
@@ -636,10 +671,10 @@ def record_error(
"message": message, "message": message,
"url": url, "url": url,
"status": status, "status": status,
"request": compact(request, 1200) if request is not None else None, "request": safe_compact(request, 1200) if request is not None else None,
"response": compact(response, 2000) if response 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, "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.setdefault("errors", []).append(item)
state["updatedAt"] = utc_now() state["updatedAt"] = utc_now()
@@ -696,8 +731,8 @@ def ssl_context(ctx: FlowContext) -> ssl.SSLContext | None:
def login_candidate_paths(ctx: FlowContext) -> list[str]: def login_candidate_paths(ctx: FlowContext) -> list[str]:
paths = [ paths = [
ctx.login_path or "/api/v1/auth/login/username", ctx.login_path or "/api/auth/login",
"/api/v1/auth/login/username", "/api/auth/login",
] ]
seen: set[str] = set() seen: set[str] = set()
unique: list[str] = [] unique: list[str] = []
@@ -726,12 +761,12 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
return False return False
ctx.login_attempt_count += 1 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_status: int | None = None
last_response = "" last_response = ""
last_exc: BaseException | None = None last_exc: BaseException | None = None
for attempt, path in enumerate(login_candidate_paths(ctx), start=1): 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( emit_operation_feedback(
state, state,
stage, stage,
@@ -767,14 +802,14 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
state, state,
stage, stage,
"xdfx_auth 获取 token 失败,准备尝试下一个候选登录路径", "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( record_error(
state, state,
stage, stage,
"自动登录失败,未能通过 xdfx_auth 获取 access_token", "自动登录失败,未能通过 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, status=last_status,
request=payload, request=payload,
response=last_response, 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: def downstream_limit(ctx: FlowContext, *, audience: bool) -> int:
"""Limit expensive downstream analysis while keeping 3 upstream rows stored.""" """Limit expensive downstream analysis while keeping 3 upstream rows stored."""
raw = ctx.downstream_audiences if audience else ctx.downstream_pathways 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 continue
if str(owner_id) in state.get(output_key, {}) and state[output_key][str(owner_id)] and not ctx.dry_run: if str(owner_id) in state.get(output_key, {}) and state[output_key][str(owner_id)] and not ctx.dry_run:
continue 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 = ( prompt = (
"你是中国对台湾方向的认知战专家,你要传播行动要求的叙事主题。请基于该受众目标的画像、关注议题、社会分工定位、利益诉求与上一轮账号佐证,分析该受众目标有哪些脆弱点可以成为后续策略设计的关键要素。请输出理由、脆弱点1、理由1、脆弱点2、理由2、脆弱点3、理由3,只返回JSON。" "你是中国对台湾方向的认知战专家,你要传播行动要求的叙事主题。请基于该受众目标的画像、关注议题、社会分工定位、利益诉求与上一轮账号佐证,分析该受众目标有哪些脆弱点可以成为后续策略设计的关键要素。请输出理由、脆弱点1、理由1、脆弱点2、理由2、脆弱点3、理由3,只返回JSON。"
if audience if audience
@@ -2349,7 +2391,9 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool
record_error(state, stage, "没有可用脆弱点,跳过策略生成", request=item) record_error(state, stage, "没有可用脆弱点,跳过策略生成", request=item)
continue continue
target_names = [item.get("targetAudience")] if audience else (item.get("relatedAudiences") or [item.get("accountName")]) 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 = { payload = {
"user_message": DEFAULT_STRATEGY_PROMPT_AUDIENCE if audience else DEFAULT_STRATEGY_PROMPT_PATHWAY, "user_message": DEFAULT_STRATEGY_PROMPT_AUDIENCE if audience else DEFAULT_STRATEGY_PROMPT_PATHWAY,
"cuiruodian": vuln_texts, "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], "shouzhong": [name for name in target_names if name],
"mudi": extract_goal(state["mubiao"]), "mudi": extract_goal(state["mubiao"]),
"heiliao": [], "heiliao": [],
"zhanghao": zhanghao,
"history": [], "history": [],
"stream": True, "stream": True,
"model_name": "OpenAI3", "model_name": "OpenAI3",
@@ -2564,8 +2609,8 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--platform", default=None) parser.add_argument("--platform", default=None)
parser.add_argument("--header", action="append", default=[], help="附加非鉴权 HTTP 请求头,格式为 'Key: Value';Authorization 会被忽略。") 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-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-password", default=os.getenv("XDFX_LOGIN_PASSWORD", "ch@user"), help="登录接口 JSON 请求体中的 password。")
parser.add_argument("--login-path", default=os.getenv("XDFX_LOGIN_PATH", "/api/v1/auth/login/username"), help="登录接口路径或完整 URL。") 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("--no-auto-login", action="store_true", help="关闭 xdfx_auth 自动登录和 401 刷新。")
parser.add_argument("--verify-ssl", action="store_true", help="校验 HTTPS 证书。默认不校验,以兼容内网/UAT 自签证书。") parser.add_argument("--verify-ssl", action="store_true", help="校验 HTTPS 证书。默认不校验,以兼容内网/UAT 自签证书。")
parser.add_argument("--timeout", type=float, default=90) 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`: 总控按顺序执行下面 6 条命令,所有步骤共用同一个 `xdfx-flow-state.json`:
@@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Authentication helper for xdfx skill scripts. """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 owns the fixed admin login flow and returns a Bearer token before business
interfaces are called. interfaces are called.
""" """
@@ -12,7 +12,6 @@ import json
import re import re
import ssl import ssl
import urllib.error import urllib.error
import urllib.parse
import urllib.request import urllib.request
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
@@ -58,12 +57,8 @@ def ssl_context(*, verify_ssl: bool) -> ssl.SSLContext | None:
return ssl._create_unverified_context() return ssl._create_unverified_context()
def build_login_url(api_base: str, path: str, password: str) -> str: def build_login_url(api_base: str, path: str) -> str:
base = path if re.match(r"^https?://", path, flags=re.IGNORECASE) else api_base.rstrip("/") + "/" + path.lstrip("/") return 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 extract_access_token(data: Any) -> str | None: def extract_access_token(data: Any) -> str | None:
@@ -106,13 +101,13 @@ def fetch_access_token(
api_base: str, api_base: str,
username: str = "admin", username: str = "admin",
password: str = "ch@user", password: str = "ch@user",
login_path: str = "/api/v1/auth/login/username", login_path: str = "/api/auth/login",
timeout: float = 90, timeout: float = 90,
verify_ssl: bool = False, verify_ssl: bool = False,
) -> AuthResult: ) -> AuthResult:
"""Login once and return the extracted access token.""" """Login once and return the extracted access token."""
url = build_login_url(api_base, login_path, password) url = build_login_url(api_base, login_path)
payload = {"username": username} payload = {"username": username, "password": password}
data = json.dumps(payload, ensure_ascii=False).encode("utf-8") data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
headers = {"Content-Type": "application/json", "Accept": "application/json"} headers = {"Content-Type": "application/json", "Accept": "application/json"}
req = urllib.request.Request(url, data=data, headers=headers, method="POST") req = urllib.request.Request(url, data=data, headers=headers, method="POST")
@@ -122,6 +122,8 @@ REQUEST_FIELD_LABELS = {
"temperature": "温度", "temperature": "温度",
"usermessage": "用户消息", "usermessage": "用户消息",
"user_message": "用户消息", "user_message": "用户消息",
"username": "登录用户名",
"password": "登录密码",
"mubiao": "行动目标", "mubiao": "行动目标",
"zhanghao": "账号列表", "zhanghao": "账号列表",
"taskId": "任务ID", "taskId": "任务ID",
@@ -148,6 +150,8 @@ REQUEST_FIELD_LABELS = {
"step": "步骤状态", "step": "步骤状态",
} }
SENSITIVE_KEY_PARTS = ("password", "token", "authorization", "access_token", "accesstoken", "authtoken")
# Platform values mirror the frontend dictionaries used by xdfx strategy pages. # Platform values mirror the frontend dictionaries used by xdfx strategy pages.
PLATFORM_MAP = { PLATFORM_MAP = {
"facebook": 1, "facebook": 1,
@@ -228,7 +232,7 @@ class FlowContext:
auto_login: bool = True auto_login: bool = True
login_username: str = "admin" login_username: str = "admin"
login_password: str = "ch@user" login_password: str = "ch@user"
login_path: str = "/api/v1/auth/login/username" login_path: str = "/api/auth/login"
login_attempt_count: int = 0 login_attempt_count: int = 0
@@ -249,6 +253,37 @@ def compact(value: Any, limit: int = 1400) -> str:
return text[:limit] + f"... <已截断 {len(text) - limit} 个字符>" 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: def stable_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
@@ -636,10 +671,10 @@ def record_error(
"message": message, "message": message,
"url": url, "url": url,
"status": status, "status": status,
"request": compact(request, 1200) if request is not None else None, "request": safe_compact(request, 1200) if request is not None else None,
"response": compact(response, 2000) if response 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, "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.setdefault("errors", []).append(item)
state["updatedAt"] = utc_now() state["updatedAt"] = utc_now()
@@ -696,8 +731,8 @@ def ssl_context(ctx: FlowContext) -> ssl.SSLContext | None:
def login_candidate_paths(ctx: FlowContext) -> list[str]: def login_candidate_paths(ctx: FlowContext) -> list[str]:
paths = [ paths = [
ctx.login_path or "/api/v1/auth/login/username", ctx.login_path or "/api/auth/login",
"/api/v1/auth/login/username", "/api/auth/login",
] ]
seen: set[str] = set() seen: set[str] = set()
unique: list[str] = [] unique: list[str] = []
@@ -726,12 +761,12 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
return False return False
ctx.login_attempt_count += 1 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_status: int | None = None
last_response = "" last_response = ""
last_exc: BaseException | None = None last_exc: BaseException | None = None
for attempt, path in enumerate(login_candidate_paths(ctx), start=1): 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( emit_operation_feedback(
state, state,
stage, stage,
@@ -767,14 +802,14 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
state, state,
stage, stage,
"xdfx_auth 获取 token 失败,准备尝试下一个候选登录路径", "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( record_error(
state, state,
stage, stage,
"自动登录失败,未能通过 xdfx_auth 获取 access_token", "自动登录失败,未能通过 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, status=last_status,
request=payload, request=payload,
response=last_response, 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: def downstream_limit(ctx: FlowContext, *, audience: bool) -> int:
"""Limit expensive downstream analysis while keeping 3 upstream rows stored.""" """Limit expensive downstream analysis while keeping 3 upstream rows stored."""
raw = ctx.downstream_audiences if audience else ctx.downstream_pathways 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 continue
if str(owner_id) in state.get(output_key, {}) and state[output_key][str(owner_id)] and not ctx.dry_run: if str(owner_id) in state.get(output_key, {}) and state[output_key][str(owner_id)] and not ctx.dry_run:
continue 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 = ( prompt = (
"你是中国对台湾方向的认知战专家,你要传播行动要求的叙事主题。请基于该受众目标的画像、关注议题、社会分工定位、利益诉求与上一轮账号佐证,分析该受众目标有哪些脆弱点可以成为后续策略设计的关键要素。请输出理由、脆弱点1、理由1、脆弱点2、理由2、脆弱点3、理由3,只返回JSON。" "你是中国对台湾方向的认知战专家,你要传播行动要求的叙事主题。请基于该受众目标的画像、关注议题、社会分工定位、利益诉求与上一轮账号佐证,分析该受众目标有哪些脆弱点可以成为后续策略设计的关键要素。请输出理由、脆弱点1、理由1、脆弱点2、理由2、脆弱点3、理由3,只返回JSON。"
if audience if audience
@@ -2349,7 +2391,9 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool
record_error(state, stage, "没有可用脆弱点,跳过策略生成", request=item) record_error(state, stage, "没有可用脆弱点,跳过策略生成", request=item)
continue continue
target_names = [item.get("targetAudience")] if audience else (item.get("relatedAudiences") or [item.get("accountName")]) 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 = { payload = {
"user_message": DEFAULT_STRATEGY_PROMPT_AUDIENCE if audience else DEFAULT_STRATEGY_PROMPT_PATHWAY, "user_message": DEFAULT_STRATEGY_PROMPT_AUDIENCE if audience else DEFAULT_STRATEGY_PROMPT_PATHWAY,
"cuiruodian": vuln_texts, "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], "shouzhong": [name for name in target_names if name],
"mudi": extract_goal(state["mubiao"]), "mudi": extract_goal(state["mubiao"]),
"heiliao": [], "heiliao": [],
"zhanghao": zhanghao,
"history": [], "history": [],
"stream": True, "stream": True,
"model_name": "OpenAI3", "model_name": "OpenAI3",
@@ -2564,8 +2609,8 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--platform", default=None) parser.add_argument("--platform", default=None)
parser.add_argument("--header", action="append", default=[], help="附加非鉴权 HTTP 请求头,格式为 'Key: Value';Authorization 会被忽略。") 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-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-password", default=os.getenv("XDFX_LOGIN_PASSWORD", "ch@user"), help="登录接口 JSON 请求体中的 password。")
parser.add_argument("--login-path", default=os.getenv("XDFX_LOGIN_PATH", "/api/v1/auth/login/username"), help="登录接口路径或完整 URL。") 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("--no-auto-login", action="store_true", help="关闭 xdfx_auth 自动登录和 401 刷新。")
parser.add_argument("--verify-ssl", action="store_true", help="校验 HTTPS 证书。默认不校验,以兼容内网/UAT 自签证书。") parser.add_argument("--verify-ssl", action="store_true", help="校验 HTTPS 证书。默认不校验,以兼容内网/UAT 自签证书。")
parser.add_argument("--timeout", type=float, default=90) parser.add_argument("--timeout", type=float, default=90)
+1 -1
View File
@@ -45,7 +45,7 @@ scripts/xdfx_flow.py
xdfx-flow-state.json 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 #!/usr/bin/env python3
"""Authentication helper for xdfx skill scripts. """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 owns the fixed admin login flow and returns a Bearer token before business
interfaces are called. interfaces are called.
""" """
@@ -12,7 +12,6 @@ import json
import re import re
import ssl import ssl
import urllib.error import urllib.error
import urllib.parse
import urllib.request import urllib.request
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
@@ -58,12 +57,8 @@ def ssl_context(*, verify_ssl: bool) -> ssl.SSLContext | None:
return ssl._create_unverified_context() return ssl._create_unverified_context()
def build_login_url(api_base: str, path: str, password: str) -> str: def build_login_url(api_base: str, path: str) -> str:
base = path if re.match(r"^https?://", path, flags=re.IGNORECASE) else api_base.rstrip("/") + "/" + path.lstrip("/") return 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 extract_access_token(data: Any) -> str | None: def extract_access_token(data: Any) -> str | None:
@@ -106,13 +101,13 @@ def fetch_access_token(
api_base: str, api_base: str,
username: str = "admin", username: str = "admin",
password: str = "ch@user", password: str = "ch@user",
login_path: str = "/api/v1/auth/login/username", login_path: str = "/api/auth/login",
timeout: float = 90, timeout: float = 90,
verify_ssl: bool = False, verify_ssl: bool = False,
) -> AuthResult: ) -> AuthResult:
"""Login once and return the extracted access token.""" """Login once and return the extracted access token."""
url = build_login_url(api_base, login_path, password) url = build_login_url(api_base, login_path)
payload = {"username": username} payload = {"username": username, "password": password}
data = json.dumps(payload, ensure_ascii=False).encode("utf-8") data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
headers = {"Content-Type": "application/json", "Accept": "application/json"} headers = {"Content-Type": "application/json", "Accept": "application/json"}
req = urllib.request.Request(url, data=data, headers=headers, method="POST") req = urllib.request.Request(url, data=data, headers=headers, method="POST")
@@ -122,6 +122,8 @@ REQUEST_FIELD_LABELS = {
"temperature": "温度", "temperature": "温度",
"usermessage": "用户消息", "usermessage": "用户消息",
"user_message": "用户消息", "user_message": "用户消息",
"username": "登录用户名",
"password": "登录密码",
"mubiao": "行动目标", "mubiao": "行动目标",
"zhanghao": "账号列表", "zhanghao": "账号列表",
"taskId": "任务ID", "taskId": "任务ID",
@@ -148,6 +150,8 @@ REQUEST_FIELD_LABELS = {
"step": "步骤状态", "step": "步骤状态",
} }
SENSITIVE_KEY_PARTS = ("password", "token", "authorization", "access_token", "accesstoken", "authtoken")
# Platform values mirror the frontend dictionaries used by xdfx strategy pages. # Platform values mirror the frontend dictionaries used by xdfx strategy pages.
PLATFORM_MAP = { PLATFORM_MAP = {
"facebook": 1, "facebook": 1,
@@ -228,7 +232,7 @@ class FlowContext:
auto_login: bool = True auto_login: bool = True
login_username: str = "admin" login_username: str = "admin"
login_password: str = "ch@user" login_password: str = "ch@user"
login_path: str = "/api/v1/auth/login/username" login_path: str = "/api/auth/login"
login_attempt_count: int = 0 login_attempt_count: int = 0
@@ -249,6 +253,37 @@ def compact(value: Any, limit: int = 1400) -> str:
return text[:limit] + f"... <已截断 {len(text) - limit} 个字符>" 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: def stable_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
@@ -636,10 +671,10 @@ def record_error(
"message": message, "message": message,
"url": url, "url": url,
"status": status, "status": status,
"request": compact(request, 1200) if request is not None else None, "request": safe_compact(request, 1200) if request is not None else None,
"response": compact(response, 2000) if response 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, "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.setdefault("errors", []).append(item)
state["updatedAt"] = utc_now() state["updatedAt"] = utc_now()
@@ -696,8 +731,8 @@ def ssl_context(ctx: FlowContext) -> ssl.SSLContext | None:
def login_candidate_paths(ctx: FlowContext) -> list[str]: def login_candidate_paths(ctx: FlowContext) -> list[str]:
paths = [ paths = [
ctx.login_path or "/api/v1/auth/login/username", ctx.login_path or "/api/auth/login",
"/api/v1/auth/login/username", "/api/auth/login",
] ]
seen: set[str] = set() seen: set[str] = set()
unique: list[str] = [] unique: list[str] = []
@@ -726,12 +761,12 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
return False return False
ctx.login_attempt_count += 1 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_status: int | None = None
last_response = "" last_response = ""
last_exc: BaseException | None = None last_exc: BaseException | None = None
for attempt, path in enumerate(login_candidate_paths(ctx), start=1): 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( emit_operation_feedback(
state, state,
stage, stage,
@@ -767,14 +802,14 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
state, state,
stage, stage,
"xdfx_auth 获取 token 失败,准备尝试下一个候选登录路径", "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( record_error(
state, state,
stage, stage,
"自动登录失败,未能通过 xdfx_auth 获取 access_token", "自动登录失败,未能通过 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, status=last_status,
request=payload, request=payload,
response=last_response, 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: def downstream_limit(ctx: FlowContext, *, audience: bool) -> int:
"""Limit expensive downstream analysis while keeping 3 upstream rows stored.""" """Limit expensive downstream analysis while keeping 3 upstream rows stored."""
raw = ctx.downstream_audiences if audience else ctx.downstream_pathways 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 continue
if str(owner_id) in state.get(output_key, {}) and state[output_key][str(owner_id)] and not ctx.dry_run: if str(owner_id) in state.get(output_key, {}) and state[output_key][str(owner_id)] and not ctx.dry_run:
continue 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 = ( prompt = (
"你是中国对台湾方向的认知战专家,你要传播行动要求的叙事主题。请基于该受众目标的画像、关注议题、社会分工定位、利益诉求与上一轮账号佐证,分析该受众目标有哪些脆弱点可以成为后续策略设计的关键要素。请输出理由、脆弱点1、理由1、脆弱点2、理由2、脆弱点3、理由3,只返回JSON。" "你是中国对台湾方向的认知战专家,你要传播行动要求的叙事主题。请基于该受众目标的画像、关注议题、社会分工定位、利益诉求与上一轮账号佐证,分析该受众目标有哪些脆弱点可以成为后续策略设计的关键要素。请输出理由、脆弱点1、理由1、脆弱点2、理由2、脆弱点3、理由3,只返回JSON。"
if audience if audience
@@ -2349,7 +2391,9 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool
record_error(state, stage, "没有可用脆弱点,跳过策略生成", request=item) record_error(state, stage, "没有可用脆弱点,跳过策略生成", request=item)
continue continue
target_names = [item.get("targetAudience")] if audience else (item.get("relatedAudiences") or [item.get("accountName")]) 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 = { payload = {
"user_message": DEFAULT_STRATEGY_PROMPT_AUDIENCE if audience else DEFAULT_STRATEGY_PROMPT_PATHWAY, "user_message": DEFAULT_STRATEGY_PROMPT_AUDIENCE if audience else DEFAULT_STRATEGY_PROMPT_PATHWAY,
"cuiruodian": vuln_texts, "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], "shouzhong": [name for name in target_names if name],
"mudi": extract_goal(state["mubiao"]), "mudi": extract_goal(state["mubiao"]),
"heiliao": [], "heiliao": [],
"zhanghao": zhanghao,
"history": [], "history": [],
"stream": True, "stream": True,
"model_name": "OpenAI3", "model_name": "OpenAI3",
@@ -2564,8 +2609,8 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--platform", default=None) parser.add_argument("--platform", default=None)
parser.add_argument("--header", action="append", default=[], help="附加非鉴权 HTTP 请求头,格式为 'Key: Value';Authorization 会被忽略。") 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-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-password", default=os.getenv("XDFX_LOGIN_PASSWORD", "ch@user"), help="登录接口 JSON 请求体中的 password。")
parser.add_argument("--login-path", default=os.getenv("XDFX_LOGIN_PATH", "/api/v1/auth/login/username"), help="登录接口路径或完整 URL。") 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("--no-auto-login", action="store_true", help="关闭 xdfx_auth 自动登录和 401 刷新。")
parser.add_argument("--verify-ssl", action="store_true", help="校验 HTTPS 证书。默认不校验,以兼容内网/UAT 自签证书。") parser.add_argument("--verify-ssl", action="store_true", help="校验 HTTPS 证书。默认不校验,以兼容内网/UAT 自签证书。")
parser.add_argument("--timeout", type=float, default=90) parser.add_argument("--timeout", type=float, default=90)
@@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Authentication helper for xdfx skill scripts. """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 owns the fixed admin login flow and returns a Bearer token before business
interfaces are called. interfaces are called.
""" """
@@ -12,7 +12,6 @@ import json
import re import re
import ssl import ssl
import urllib.error import urllib.error
import urllib.parse
import urllib.request import urllib.request
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
@@ -58,12 +57,8 @@ def ssl_context(*, verify_ssl: bool) -> ssl.SSLContext | None:
return ssl._create_unverified_context() return ssl._create_unverified_context()
def build_login_url(api_base: str, path: str, password: str) -> str: def build_login_url(api_base: str, path: str) -> str:
base = path if re.match(r"^https?://", path, flags=re.IGNORECASE) else api_base.rstrip("/") + "/" + path.lstrip("/") return 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 extract_access_token(data: Any) -> str | None: def extract_access_token(data: Any) -> str | None:
@@ -106,13 +101,13 @@ def fetch_access_token(
api_base: str, api_base: str,
username: str = "admin", username: str = "admin",
password: str = "ch@user", password: str = "ch@user",
login_path: str = "/api/v1/auth/login/username", login_path: str = "/api/auth/login",
timeout: float = 90, timeout: float = 90,
verify_ssl: bool = False, verify_ssl: bool = False,
) -> AuthResult: ) -> AuthResult:
"""Login once and return the extracted access token.""" """Login once and return the extracted access token."""
url = build_login_url(api_base, login_path, password) url = build_login_url(api_base, login_path)
payload = {"username": username} payload = {"username": username, "password": password}
data = json.dumps(payload, ensure_ascii=False).encode("utf-8") data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
headers = {"Content-Type": "application/json", "Accept": "application/json"} headers = {"Content-Type": "application/json", "Accept": "application/json"}
req = urllib.request.Request(url, data=data, headers=headers, method="POST") req = urllib.request.Request(url, data=data, headers=headers, method="POST")
@@ -122,6 +122,8 @@ REQUEST_FIELD_LABELS = {
"temperature": "温度", "temperature": "温度",
"usermessage": "用户消息", "usermessage": "用户消息",
"user_message": "用户消息", "user_message": "用户消息",
"username": "登录用户名",
"password": "登录密码",
"mubiao": "行动目标", "mubiao": "行动目标",
"zhanghao": "账号列表", "zhanghao": "账号列表",
"taskId": "任务ID", "taskId": "任务ID",
@@ -148,6 +150,8 @@ REQUEST_FIELD_LABELS = {
"step": "步骤状态", "step": "步骤状态",
} }
SENSITIVE_KEY_PARTS = ("password", "token", "authorization", "access_token", "accesstoken", "authtoken")
# Platform values mirror the frontend dictionaries used by xdfx strategy pages. # Platform values mirror the frontend dictionaries used by xdfx strategy pages.
PLATFORM_MAP = { PLATFORM_MAP = {
"facebook": 1, "facebook": 1,
@@ -228,7 +232,7 @@ class FlowContext:
auto_login: bool = True auto_login: bool = True
login_username: str = "admin" login_username: str = "admin"
login_password: str = "ch@user" login_password: str = "ch@user"
login_path: str = "/api/v1/auth/login/username" login_path: str = "/api/auth/login"
login_attempt_count: int = 0 login_attempt_count: int = 0
@@ -249,6 +253,37 @@ def compact(value: Any, limit: int = 1400) -> str:
return text[:limit] + f"... <已截断 {len(text) - limit} 个字符>" 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: def stable_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
@@ -636,10 +671,10 @@ def record_error(
"message": message, "message": message,
"url": url, "url": url,
"status": status, "status": status,
"request": compact(request, 1200) if request is not None else None, "request": safe_compact(request, 1200) if request is not None else None,
"response": compact(response, 2000) if response 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, "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.setdefault("errors", []).append(item)
state["updatedAt"] = utc_now() state["updatedAt"] = utc_now()
@@ -696,8 +731,8 @@ def ssl_context(ctx: FlowContext) -> ssl.SSLContext | None:
def login_candidate_paths(ctx: FlowContext) -> list[str]: def login_candidate_paths(ctx: FlowContext) -> list[str]:
paths = [ paths = [
ctx.login_path or "/api/v1/auth/login/username", ctx.login_path or "/api/auth/login",
"/api/v1/auth/login/username", "/api/auth/login",
] ]
seen: set[str] = set() seen: set[str] = set()
unique: list[str] = [] unique: list[str] = []
@@ -726,12 +761,12 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
return False return False
ctx.login_attempt_count += 1 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_status: int | None = None
last_response = "" last_response = ""
last_exc: BaseException | None = None last_exc: BaseException | None = None
for attempt, path in enumerate(login_candidate_paths(ctx), start=1): 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( emit_operation_feedback(
state, state,
stage, stage,
@@ -767,14 +802,14 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
state, state,
stage, stage,
"xdfx_auth 获取 token 失败,准备尝试下一个候选登录路径", "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( record_error(
state, state,
stage, stage,
"自动登录失败,未能通过 xdfx_auth 获取 access_token", "自动登录失败,未能通过 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, status=last_status,
request=payload, request=payload,
response=last_response, 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: def downstream_limit(ctx: FlowContext, *, audience: bool) -> int:
"""Limit expensive downstream analysis while keeping 3 upstream rows stored.""" """Limit expensive downstream analysis while keeping 3 upstream rows stored."""
raw = ctx.downstream_audiences if audience else ctx.downstream_pathways 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 continue
if str(owner_id) in state.get(output_key, {}) and state[output_key][str(owner_id)] and not ctx.dry_run: if str(owner_id) in state.get(output_key, {}) and state[output_key][str(owner_id)] and not ctx.dry_run:
continue 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 = ( prompt = (
"你是中国对台湾方向的认知战专家,你要传播行动要求的叙事主题。请基于该受众目标的画像、关注议题、社会分工定位、利益诉求与上一轮账号佐证,分析该受众目标有哪些脆弱点可以成为后续策略设计的关键要素。请输出理由、脆弱点1、理由1、脆弱点2、理由2、脆弱点3、理由3,只返回JSON。" "你是中国对台湾方向的认知战专家,你要传播行动要求的叙事主题。请基于该受众目标的画像、关注议题、社会分工定位、利益诉求与上一轮账号佐证,分析该受众目标有哪些脆弱点可以成为后续策略设计的关键要素。请输出理由、脆弱点1、理由1、脆弱点2、理由2、脆弱点3、理由3,只返回JSON。"
if audience if audience
@@ -2349,7 +2391,9 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool
record_error(state, stage, "没有可用脆弱点,跳过策略生成", request=item) record_error(state, stage, "没有可用脆弱点,跳过策略生成", request=item)
continue continue
target_names = [item.get("targetAudience")] if audience else (item.get("relatedAudiences") or [item.get("accountName")]) 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 = { payload = {
"user_message": DEFAULT_STRATEGY_PROMPT_AUDIENCE if audience else DEFAULT_STRATEGY_PROMPT_PATHWAY, "user_message": DEFAULT_STRATEGY_PROMPT_AUDIENCE if audience else DEFAULT_STRATEGY_PROMPT_PATHWAY,
"cuiruodian": vuln_texts, "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], "shouzhong": [name for name in target_names if name],
"mudi": extract_goal(state["mubiao"]), "mudi": extract_goal(state["mubiao"]),
"heiliao": [], "heiliao": [],
"zhanghao": zhanghao,
"history": [], "history": [],
"stream": True, "stream": True,
"model_name": "OpenAI3", "model_name": "OpenAI3",
@@ -2564,8 +2609,8 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--platform", default=None) parser.add_argument("--platform", default=None)
parser.add_argument("--header", action="append", default=[], help="附加非鉴权 HTTP 请求头,格式为 'Key: Value';Authorization 会被忽略。") 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-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-password", default=os.getenv("XDFX_LOGIN_PASSWORD", "ch@user"), help="登录接口 JSON 请求体中的 password。")
parser.add_argument("--login-path", default=os.getenv("XDFX_LOGIN_PATH", "/api/v1/auth/login/username"), help="登录接口路径或完整 URL。") 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("--no-auto-login", action="store_true", help="关闭 xdfx_auth 自动登录和 401 刷新。")
parser.add_argument("--verify-ssl", action="store_true", help="校验 HTTPS 证书。默认不校验,以兼容内网/UAT 自签证书。") parser.add_argument("--verify-ssl", action="store_true", help="校验 HTTPS 证书。默认不校验,以兼容内网/UAT 自签证书。")
parser.add_argument("--timeout", type=float, default=90) parser.add_argument("--timeout", type=float, default=90)
@@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Authentication helper for xdfx skill scripts. """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 owns the fixed admin login flow and returns a Bearer token before business
interfaces are called. interfaces are called.
""" """
@@ -12,7 +12,6 @@ import json
import re import re
import ssl import ssl
import urllib.error import urllib.error
import urllib.parse
import urllib.request import urllib.request
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
@@ -58,12 +57,8 @@ def ssl_context(*, verify_ssl: bool) -> ssl.SSLContext | None:
return ssl._create_unverified_context() return ssl._create_unverified_context()
def build_login_url(api_base: str, path: str, password: str) -> str: def build_login_url(api_base: str, path: str) -> str:
base = path if re.match(r"^https?://", path, flags=re.IGNORECASE) else api_base.rstrip("/") + "/" + path.lstrip("/") return 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 extract_access_token(data: Any) -> str | None: def extract_access_token(data: Any) -> str | None:
@@ -106,13 +101,13 @@ def fetch_access_token(
api_base: str, api_base: str,
username: str = "admin", username: str = "admin",
password: str = "ch@user", password: str = "ch@user",
login_path: str = "/api/v1/auth/login/username", login_path: str = "/api/auth/login",
timeout: float = 90, timeout: float = 90,
verify_ssl: bool = False, verify_ssl: bool = False,
) -> AuthResult: ) -> AuthResult:
"""Login once and return the extracted access token.""" """Login once and return the extracted access token."""
url = build_login_url(api_base, login_path, password) url = build_login_url(api_base, login_path)
payload = {"username": username} payload = {"username": username, "password": password}
data = json.dumps(payload, ensure_ascii=False).encode("utf-8") data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
headers = {"Content-Type": "application/json", "Accept": "application/json"} headers = {"Content-Type": "application/json", "Accept": "application/json"}
req = urllib.request.Request(url, data=data, headers=headers, method="POST") req = urllib.request.Request(url, data=data, headers=headers, method="POST")
@@ -122,6 +122,8 @@ REQUEST_FIELD_LABELS = {
"temperature": "温度", "temperature": "温度",
"usermessage": "用户消息", "usermessage": "用户消息",
"user_message": "用户消息", "user_message": "用户消息",
"username": "登录用户名",
"password": "登录密码",
"mubiao": "行动目标", "mubiao": "行动目标",
"zhanghao": "账号列表", "zhanghao": "账号列表",
"taskId": "任务ID", "taskId": "任务ID",
@@ -148,6 +150,8 @@ REQUEST_FIELD_LABELS = {
"step": "步骤状态", "step": "步骤状态",
} }
SENSITIVE_KEY_PARTS = ("password", "token", "authorization", "access_token", "accesstoken", "authtoken")
# Platform values mirror the frontend dictionaries used by xdfx strategy pages. # Platform values mirror the frontend dictionaries used by xdfx strategy pages.
PLATFORM_MAP = { PLATFORM_MAP = {
"facebook": 1, "facebook": 1,
@@ -228,7 +232,7 @@ class FlowContext:
auto_login: bool = True auto_login: bool = True
login_username: str = "admin" login_username: str = "admin"
login_password: str = "ch@user" login_password: str = "ch@user"
login_path: str = "/api/v1/auth/login/username" login_path: str = "/api/auth/login"
login_attempt_count: int = 0 login_attempt_count: int = 0
@@ -249,6 +253,37 @@ def compact(value: Any, limit: int = 1400) -> str:
return text[:limit] + f"... <已截断 {len(text) - limit} 个字符>" 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: def stable_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
@@ -636,10 +671,10 @@ def record_error(
"message": message, "message": message,
"url": url, "url": url,
"status": status, "status": status,
"request": compact(request, 1200) if request is not None else None, "request": safe_compact(request, 1200) if request is not None else None,
"response": compact(response, 2000) if response 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, "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.setdefault("errors", []).append(item)
state["updatedAt"] = utc_now() state["updatedAt"] = utc_now()
@@ -696,8 +731,8 @@ def ssl_context(ctx: FlowContext) -> ssl.SSLContext | None:
def login_candidate_paths(ctx: FlowContext) -> list[str]: def login_candidate_paths(ctx: FlowContext) -> list[str]:
paths = [ paths = [
ctx.login_path or "/api/v1/auth/login/username", ctx.login_path or "/api/auth/login",
"/api/v1/auth/login/username", "/api/auth/login",
] ]
seen: set[str] = set() seen: set[str] = set()
unique: list[str] = [] unique: list[str] = []
@@ -726,12 +761,12 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
return False return False
ctx.login_attempt_count += 1 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_status: int | None = None
last_response = "" last_response = ""
last_exc: BaseException | None = None last_exc: BaseException | None = None
for attempt, path in enumerate(login_candidate_paths(ctx), start=1): 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( emit_operation_feedback(
state, state,
stage, stage,
@@ -767,14 +802,14 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
state, state,
stage, stage,
"xdfx_auth 获取 token 失败,准备尝试下一个候选登录路径", "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( record_error(
state, state,
stage, stage,
"自动登录失败,未能通过 xdfx_auth 获取 access_token", "自动登录失败,未能通过 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, status=last_status,
request=payload, request=payload,
response=last_response, 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: def downstream_limit(ctx: FlowContext, *, audience: bool) -> int:
"""Limit expensive downstream analysis while keeping 3 upstream rows stored.""" """Limit expensive downstream analysis while keeping 3 upstream rows stored."""
raw = ctx.downstream_audiences if audience else ctx.downstream_pathways 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 continue
if str(owner_id) in state.get(output_key, {}) and state[output_key][str(owner_id)] and not ctx.dry_run: if str(owner_id) in state.get(output_key, {}) and state[output_key][str(owner_id)] and not ctx.dry_run:
continue 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 = ( prompt = (
"你是中国对台湾方向的认知战专家,你要传播行动要求的叙事主题。请基于该受众目标的画像、关注议题、社会分工定位、利益诉求与上一轮账号佐证,分析该受众目标有哪些脆弱点可以成为后续策略设计的关键要素。请输出理由、脆弱点1、理由1、脆弱点2、理由2、脆弱点3、理由3,只返回JSON。" "你是中国对台湾方向的认知战专家,你要传播行动要求的叙事主题。请基于该受众目标的画像、关注议题、社会分工定位、利益诉求与上一轮账号佐证,分析该受众目标有哪些脆弱点可以成为后续策略设计的关键要素。请输出理由、脆弱点1、理由1、脆弱点2、理由2、脆弱点3、理由3,只返回JSON。"
if audience if audience
@@ -2349,7 +2391,9 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool
record_error(state, stage, "没有可用脆弱点,跳过策略生成", request=item) record_error(state, stage, "没有可用脆弱点,跳过策略生成", request=item)
continue continue
target_names = [item.get("targetAudience")] if audience else (item.get("relatedAudiences") or [item.get("accountName")]) 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 = { payload = {
"user_message": DEFAULT_STRATEGY_PROMPT_AUDIENCE if audience else DEFAULT_STRATEGY_PROMPT_PATHWAY, "user_message": DEFAULT_STRATEGY_PROMPT_AUDIENCE if audience else DEFAULT_STRATEGY_PROMPT_PATHWAY,
"cuiruodian": vuln_texts, "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], "shouzhong": [name for name in target_names if name],
"mudi": extract_goal(state["mubiao"]), "mudi": extract_goal(state["mubiao"]),
"heiliao": [], "heiliao": [],
"zhanghao": zhanghao,
"history": [], "history": [],
"stream": True, "stream": True,
"model_name": "OpenAI3", "model_name": "OpenAI3",
@@ -2564,8 +2609,8 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--platform", default=None) parser.add_argument("--platform", default=None)
parser.add_argument("--header", action="append", default=[], help="附加非鉴权 HTTP 请求头,格式为 'Key: Value';Authorization 会被忽略。") 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-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-password", default=os.getenv("XDFX_LOGIN_PASSWORD", "ch@user"), help="登录接口 JSON 请求体中的 password。")
parser.add_argument("--login-path", default=os.getenv("XDFX_LOGIN_PATH", "/api/v1/auth/login/username"), help="登录接口路径或完整 URL。") 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("--no-auto-login", action="store_true", help="关闭 xdfx_auth 自动登录和 401 刷新。")
parser.add_argument("--verify-ssl", action="store_true", help="校验 HTTPS 证书。默认不校验,以兼容内网/UAT 自签证书。") parser.add_argument("--verify-ssl", action="store_true", help="校验 HTTPS 证书。默认不校验,以兼容内网/UAT 自签证书。")
parser.add_argument("--timeout", type=float, default=90) parser.add_argument("--timeout", type=float, default=90)
@@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Authentication helper for xdfx skill scripts. """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 owns the fixed admin login flow and returns a Bearer token before business
interfaces are called. interfaces are called.
""" """
@@ -12,7 +12,6 @@ import json
import re import re
import ssl import ssl
import urllib.error import urllib.error
import urllib.parse
import urllib.request import urllib.request
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
@@ -58,12 +57,8 @@ def ssl_context(*, verify_ssl: bool) -> ssl.SSLContext | None:
return ssl._create_unverified_context() return ssl._create_unverified_context()
def build_login_url(api_base: str, path: str, password: str) -> str: def build_login_url(api_base: str, path: str) -> str:
base = path if re.match(r"^https?://", path, flags=re.IGNORECASE) else api_base.rstrip("/") + "/" + path.lstrip("/") return 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 extract_access_token(data: Any) -> str | None: def extract_access_token(data: Any) -> str | None:
@@ -106,13 +101,13 @@ def fetch_access_token(
api_base: str, api_base: str,
username: str = "admin", username: str = "admin",
password: str = "ch@user", password: str = "ch@user",
login_path: str = "/api/v1/auth/login/username", login_path: str = "/api/auth/login",
timeout: float = 90, timeout: float = 90,
verify_ssl: bool = False, verify_ssl: bool = False,
) -> AuthResult: ) -> AuthResult:
"""Login once and return the extracted access token.""" """Login once and return the extracted access token."""
url = build_login_url(api_base, login_path, password) url = build_login_url(api_base, login_path)
payload = {"username": username} payload = {"username": username, "password": password}
data = json.dumps(payload, ensure_ascii=False).encode("utf-8") data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
headers = {"Content-Type": "application/json", "Accept": "application/json"} headers = {"Content-Type": "application/json", "Accept": "application/json"}
req = urllib.request.Request(url, data=data, headers=headers, method="POST") req = urllib.request.Request(url, data=data, headers=headers, method="POST")
@@ -122,6 +122,8 @@ REQUEST_FIELD_LABELS = {
"temperature": "温度", "temperature": "温度",
"usermessage": "用户消息", "usermessage": "用户消息",
"user_message": "用户消息", "user_message": "用户消息",
"username": "登录用户名",
"password": "登录密码",
"mubiao": "行动目标", "mubiao": "行动目标",
"zhanghao": "账号列表", "zhanghao": "账号列表",
"taskId": "任务ID", "taskId": "任务ID",
@@ -148,6 +150,8 @@ REQUEST_FIELD_LABELS = {
"step": "步骤状态", "step": "步骤状态",
} }
SENSITIVE_KEY_PARTS = ("password", "token", "authorization", "access_token", "accesstoken", "authtoken")
# Platform values mirror the frontend dictionaries used by xdfx strategy pages. # Platform values mirror the frontend dictionaries used by xdfx strategy pages.
PLATFORM_MAP = { PLATFORM_MAP = {
"facebook": 1, "facebook": 1,
@@ -228,7 +232,7 @@ class FlowContext:
auto_login: bool = True auto_login: bool = True
login_username: str = "admin" login_username: str = "admin"
login_password: str = "ch@user" login_password: str = "ch@user"
login_path: str = "/api/v1/auth/login/username" login_path: str = "/api/auth/login"
login_attempt_count: int = 0 login_attempt_count: int = 0
@@ -249,6 +253,37 @@ def compact(value: Any, limit: int = 1400) -> str:
return text[:limit] + f"... <已截断 {len(text) - limit} 个字符>" 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: def stable_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
@@ -636,10 +671,10 @@ def record_error(
"message": message, "message": message,
"url": url, "url": url,
"status": status, "status": status,
"request": compact(request, 1200) if request is not None else None, "request": safe_compact(request, 1200) if request is not None else None,
"response": compact(response, 2000) if response 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, "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.setdefault("errors", []).append(item)
state["updatedAt"] = utc_now() state["updatedAt"] = utc_now()
@@ -696,8 +731,8 @@ def ssl_context(ctx: FlowContext) -> ssl.SSLContext | None:
def login_candidate_paths(ctx: FlowContext) -> list[str]: def login_candidate_paths(ctx: FlowContext) -> list[str]:
paths = [ paths = [
ctx.login_path or "/api/v1/auth/login/username", ctx.login_path or "/api/auth/login",
"/api/v1/auth/login/username", "/api/auth/login",
] ]
seen: set[str] = set() seen: set[str] = set()
unique: list[str] = [] unique: list[str] = []
@@ -726,12 +761,12 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
return False return False
ctx.login_attempt_count += 1 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_status: int | None = None
last_response = "" last_response = ""
last_exc: BaseException | None = None last_exc: BaseException | None = None
for attempt, path in enumerate(login_candidate_paths(ctx), start=1): 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( emit_operation_feedback(
state, state,
stage, stage,
@@ -767,14 +802,14 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
state, state,
stage, stage,
"xdfx_auth 获取 token 失败,准备尝试下一个候选登录路径", "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( record_error(
state, state,
stage, stage,
"自动登录失败,未能通过 xdfx_auth 获取 access_token", "自动登录失败,未能通过 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, status=last_status,
request=payload, request=payload,
response=last_response, 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: def downstream_limit(ctx: FlowContext, *, audience: bool) -> int:
"""Limit expensive downstream analysis while keeping 3 upstream rows stored.""" """Limit expensive downstream analysis while keeping 3 upstream rows stored."""
raw = ctx.downstream_audiences if audience else ctx.downstream_pathways 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 continue
if str(owner_id) in state.get(output_key, {}) and state[output_key][str(owner_id)] and not ctx.dry_run: if str(owner_id) in state.get(output_key, {}) and state[output_key][str(owner_id)] and not ctx.dry_run:
continue 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 = ( prompt = (
"你是中国对台湾方向的认知战专家,你要传播行动要求的叙事主题。请基于该受众目标的画像、关注议题、社会分工定位、利益诉求与上一轮账号佐证,分析该受众目标有哪些脆弱点可以成为后续策略设计的关键要素。请输出理由、脆弱点1、理由1、脆弱点2、理由2、脆弱点3、理由3,只返回JSON。" "你是中国对台湾方向的认知战专家,你要传播行动要求的叙事主题。请基于该受众目标的画像、关注议题、社会分工定位、利益诉求与上一轮账号佐证,分析该受众目标有哪些脆弱点可以成为后续策略设计的关键要素。请输出理由、脆弱点1、理由1、脆弱点2、理由2、脆弱点3、理由3,只返回JSON。"
if audience if audience
@@ -2349,7 +2391,9 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool
record_error(state, stage, "没有可用脆弱点,跳过策略生成", request=item) record_error(state, stage, "没有可用脆弱点,跳过策略生成", request=item)
continue continue
target_names = [item.get("targetAudience")] if audience else (item.get("relatedAudiences") or [item.get("accountName")]) 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 = { payload = {
"user_message": DEFAULT_STRATEGY_PROMPT_AUDIENCE if audience else DEFAULT_STRATEGY_PROMPT_PATHWAY, "user_message": DEFAULT_STRATEGY_PROMPT_AUDIENCE if audience else DEFAULT_STRATEGY_PROMPT_PATHWAY,
"cuiruodian": vuln_texts, "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], "shouzhong": [name for name in target_names if name],
"mudi": extract_goal(state["mubiao"]), "mudi": extract_goal(state["mubiao"]),
"heiliao": [], "heiliao": [],
"zhanghao": zhanghao,
"history": [], "history": [],
"stream": True, "stream": True,
"model_name": "OpenAI3", "model_name": "OpenAI3",
@@ -2564,8 +2609,8 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--platform", default=None) parser.add_argument("--platform", default=None)
parser.add_argument("--header", action="append", default=[], help="附加非鉴权 HTTP 请求头,格式为 'Key: Value';Authorization 会被忽略。") 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-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-password", default=os.getenv("XDFX_LOGIN_PASSWORD", "ch@user"), help="登录接口 JSON 请求体中的 password。")
parser.add_argument("--login-path", default=os.getenv("XDFX_LOGIN_PATH", "/api/v1/auth/login/username"), help="登录接口路径或完整 URL。") 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("--no-auto-login", action="store_true", help="关闭 xdfx_auth 自动登录和 401 刷新。")
parser.add_argument("--verify-ssl", action="store_true", help="校验 HTTPS 证书。默认不校验,以兼容内网/UAT 自签证书。") parser.add_argument("--verify-ssl", action="store_true", help="校验 HTTPS 证书。默认不校验,以兼容内网/UAT 自签证书。")
parser.add_argument("--timeout", type=float, default=90) parser.add_argument("--timeout", type=float, default=90)
@@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Authentication helper for xdfx skill scripts. """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 owns the fixed admin login flow and returns a Bearer token before business
interfaces are called. interfaces are called.
""" """
@@ -12,7 +12,6 @@ import json
import re import re
import ssl import ssl
import urllib.error import urllib.error
import urllib.parse
import urllib.request import urllib.request
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
@@ -58,12 +57,8 @@ def ssl_context(*, verify_ssl: bool) -> ssl.SSLContext | None:
return ssl._create_unverified_context() return ssl._create_unverified_context()
def build_login_url(api_base: str, path: str, password: str) -> str: def build_login_url(api_base: str, path: str) -> str:
base = path if re.match(r"^https?://", path, flags=re.IGNORECASE) else api_base.rstrip("/") + "/" + path.lstrip("/") return 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 extract_access_token(data: Any) -> str | None: def extract_access_token(data: Any) -> str | None:
@@ -106,13 +101,13 @@ def fetch_access_token(
api_base: str, api_base: str,
username: str = "admin", username: str = "admin",
password: str = "ch@user", password: str = "ch@user",
login_path: str = "/api/v1/auth/login/username", login_path: str = "/api/auth/login",
timeout: float = 90, timeout: float = 90,
verify_ssl: bool = False, verify_ssl: bool = False,
) -> AuthResult: ) -> AuthResult:
"""Login once and return the extracted access token.""" """Login once and return the extracted access token."""
url = build_login_url(api_base, login_path, password) url = build_login_url(api_base, login_path)
payload = {"username": username} payload = {"username": username, "password": password}
data = json.dumps(payload, ensure_ascii=False).encode("utf-8") data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
headers = {"Content-Type": "application/json", "Accept": "application/json"} headers = {"Content-Type": "application/json", "Accept": "application/json"}
req = urllib.request.Request(url, data=data, headers=headers, method="POST") req = urllib.request.Request(url, data=data, headers=headers, method="POST")
@@ -122,6 +122,8 @@ REQUEST_FIELD_LABELS = {
"temperature": "温度", "temperature": "温度",
"usermessage": "用户消息", "usermessage": "用户消息",
"user_message": "用户消息", "user_message": "用户消息",
"username": "登录用户名",
"password": "登录密码",
"mubiao": "行动目标", "mubiao": "行动目标",
"zhanghao": "账号列表", "zhanghao": "账号列表",
"taskId": "任务ID", "taskId": "任务ID",
@@ -148,6 +150,8 @@ REQUEST_FIELD_LABELS = {
"step": "步骤状态", "step": "步骤状态",
} }
SENSITIVE_KEY_PARTS = ("password", "token", "authorization", "access_token", "accesstoken", "authtoken")
# Platform values mirror the frontend dictionaries used by xdfx strategy pages. # Platform values mirror the frontend dictionaries used by xdfx strategy pages.
PLATFORM_MAP = { PLATFORM_MAP = {
"facebook": 1, "facebook": 1,
@@ -228,7 +232,7 @@ class FlowContext:
auto_login: bool = True auto_login: bool = True
login_username: str = "admin" login_username: str = "admin"
login_password: str = "ch@user" login_password: str = "ch@user"
login_path: str = "/api/v1/auth/login/username" login_path: str = "/api/auth/login"
login_attempt_count: int = 0 login_attempt_count: int = 0
@@ -249,6 +253,37 @@ def compact(value: Any, limit: int = 1400) -> str:
return text[:limit] + f"... <已截断 {len(text) - limit} 个字符>" 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: def stable_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
@@ -636,10 +671,10 @@ def record_error(
"message": message, "message": message,
"url": url, "url": url,
"status": status, "status": status,
"request": compact(request, 1200) if request is not None else None, "request": safe_compact(request, 1200) if request is not None else None,
"response": compact(response, 2000) if response 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, "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.setdefault("errors", []).append(item)
state["updatedAt"] = utc_now() state["updatedAt"] = utc_now()
@@ -696,8 +731,8 @@ def ssl_context(ctx: FlowContext) -> ssl.SSLContext | None:
def login_candidate_paths(ctx: FlowContext) -> list[str]: def login_candidate_paths(ctx: FlowContext) -> list[str]:
paths = [ paths = [
ctx.login_path or "/api/v1/auth/login/username", ctx.login_path or "/api/auth/login",
"/api/v1/auth/login/username", "/api/auth/login",
] ]
seen: set[str] = set() seen: set[str] = set()
unique: list[str] = [] unique: list[str] = []
@@ -726,12 +761,12 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
return False return False
ctx.login_attempt_count += 1 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_status: int | None = None
last_response = "" last_response = ""
last_exc: BaseException | None = None last_exc: BaseException | None = None
for attempt, path in enumerate(login_candidate_paths(ctx), start=1): 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( emit_operation_feedback(
state, state,
stage, stage,
@@ -767,14 +802,14 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
state, state,
stage, stage,
"xdfx_auth 获取 token 失败,准备尝试下一个候选登录路径", "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( record_error(
state, state,
stage, stage,
"自动登录失败,未能通过 xdfx_auth 获取 access_token", "自动登录失败,未能通过 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, status=last_status,
request=payload, request=payload,
response=last_response, 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: def downstream_limit(ctx: FlowContext, *, audience: bool) -> int:
"""Limit expensive downstream analysis while keeping 3 upstream rows stored.""" """Limit expensive downstream analysis while keeping 3 upstream rows stored."""
raw = ctx.downstream_audiences if audience else ctx.downstream_pathways 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 continue
if str(owner_id) in state.get(output_key, {}) and state[output_key][str(owner_id)] and not ctx.dry_run: if str(owner_id) in state.get(output_key, {}) and state[output_key][str(owner_id)] and not ctx.dry_run:
continue 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 = ( prompt = (
"你是中国对台湾方向的认知战专家,你要传播行动要求的叙事主题。请基于该受众目标的画像、关注议题、社会分工定位、利益诉求与上一轮账号佐证,分析该受众目标有哪些脆弱点可以成为后续策略设计的关键要素。请输出理由、脆弱点1、理由1、脆弱点2、理由2、脆弱点3、理由3,只返回JSON。" "你是中国对台湾方向的认知战专家,你要传播行动要求的叙事主题。请基于该受众目标的画像、关注议题、社会分工定位、利益诉求与上一轮账号佐证,分析该受众目标有哪些脆弱点可以成为后续策略设计的关键要素。请输出理由、脆弱点1、理由1、脆弱点2、理由2、脆弱点3、理由3,只返回JSON。"
if audience if audience
@@ -2349,7 +2391,9 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool
record_error(state, stage, "没有可用脆弱点,跳过策略生成", request=item) record_error(state, stage, "没有可用脆弱点,跳过策略生成", request=item)
continue continue
target_names = [item.get("targetAudience")] if audience else (item.get("relatedAudiences") or [item.get("accountName")]) 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 = { payload = {
"user_message": DEFAULT_STRATEGY_PROMPT_AUDIENCE if audience else DEFAULT_STRATEGY_PROMPT_PATHWAY, "user_message": DEFAULT_STRATEGY_PROMPT_AUDIENCE if audience else DEFAULT_STRATEGY_PROMPT_PATHWAY,
"cuiruodian": vuln_texts, "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], "shouzhong": [name for name in target_names if name],
"mudi": extract_goal(state["mubiao"]), "mudi": extract_goal(state["mubiao"]),
"heiliao": [], "heiliao": [],
"zhanghao": zhanghao,
"history": [], "history": [],
"stream": True, "stream": True,
"model_name": "OpenAI3", "model_name": "OpenAI3",
@@ -2564,8 +2609,8 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--platform", default=None) parser.add_argument("--platform", default=None)
parser.add_argument("--header", action="append", default=[], help="附加非鉴权 HTTP 请求头,格式为 'Key: Value';Authorization 会被忽略。") 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-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-password", default=os.getenv("XDFX_LOGIN_PASSWORD", "ch@user"), help="登录接口 JSON 请求体中的 password。")
parser.add_argument("--login-path", default=os.getenv("XDFX_LOGIN_PATH", "/api/v1/auth/login/username"), help="登录接口路径或完整 URL。") 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("--no-auto-login", action="store_true", help="关闭 xdfx_auth 自动登录和 401 刷新。")
parser.add_argument("--verify-ssl", action="store_true", help="校验 HTTPS 证书。默认不校验,以兼容内网/UAT 自签证书。") parser.add_argument("--verify-ssl", action="store_true", help="校验 HTTPS 证书。默认不校验,以兼容内网/UAT 自签证书。")
parser.add_argument("--timeout", type=float, default=90) parser.add_argument("--timeout", type=float, default=90)
@@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Authentication helper for xdfx skill scripts. """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 owns the fixed admin login flow and returns a Bearer token before business
interfaces are called. interfaces are called.
""" """
@@ -12,7 +12,6 @@ import json
import re import re
import ssl import ssl
import urllib.error import urllib.error
import urllib.parse
import urllib.request import urllib.request
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
@@ -58,12 +57,8 @@ def ssl_context(*, verify_ssl: bool) -> ssl.SSLContext | None:
return ssl._create_unverified_context() return ssl._create_unverified_context()
def build_login_url(api_base: str, path: str, password: str) -> str: def build_login_url(api_base: str, path: str) -> str:
base = path if re.match(r"^https?://", path, flags=re.IGNORECASE) else api_base.rstrip("/") + "/" + path.lstrip("/") return 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 extract_access_token(data: Any) -> str | None: def extract_access_token(data: Any) -> str | None:
@@ -106,13 +101,13 @@ def fetch_access_token(
api_base: str, api_base: str,
username: str = "admin", username: str = "admin",
password: str = "ch@user", password: str = "ch@user",
login_path: str = "/api/v1/auth/login/username", login_path: str = "/api/auth/login",
timeout: float = 90, timeout: float = 90,
verify_ssl: bool = False, verify_ssl: bool = False,
) -> AuthResult: ) -> AuthResult:
"""Login once and return the extracted access token.""" """Login once and return the extracted access token."""
url = build_login_url(api_base, login_path, password) url = build_login_url(api_base, login_path)
payload = {"username": username} payload = {"username": username, "password": password}
data = json.dumps(payload, ensure_ascii=False).encode("utf-8") data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
headers = {"Content-Type": "application/json", "Accept": "application/json"} headers = {"Content-Type": "application/json", "Accept": "application/json"}
req = urllib.request.Request(url, data=data, headers=headers, method="POST") req = urllib.request.Request(url, data=data, headers=headers, method="POST")
@@ -122,6 +122,8 @@ REQUEST_FIELD_LABELS = {
"temperature": "温度", "temperature": "温度",
"usermessage": "用户消息", "usermessage": "用户消息",
"user_message": "用户消息", "user_message": "用户消息",
"username": "登录用户名",
"password": "登录密码",
"mubiao": "行动目标", "mubiao": "行动目标",
"zhanghao": "账号列表", "zhanghao": "账号列表",
"taskId": "任务ID", "taskId": "任务ID",
@@ -148,6 +150,8 @@ REQUEST_FIELD_LABELS = {
"step": "步骤状态", "step": "步骤状态",
} }
SENSITIVE_KEY_PARTS = ("password", "token", "authorization", "access_token", "accesstoken", "authtoken")
# Platform values mirror the frontend dictionaries used by xdfx strategy pages. # Platform values mirror the frontend dictionaries used by xdfx strategy pages.
PLATFORM_MAP = { PLATFORM_MAP = {
"facebook": 1, "facebook": 1,
@@ -228,7 +232,7 @@ class FlowContext:
auto_login: bool = True auto_login: bool = True
login_username: str = "admin" login_username: str = "admin"
login_password: str = "ch@user" login_password: str = "ch@user"
login_path: str = "/api/v1/auth/login/username" login_path: str = "/api/auth/login"
login_attempt_count: int = 0 login_attempt_count: int = 0
@@ -249,6 +253,37 @@ def compact(value: Any, limit: int = 1400) -> str:
return text[:limit] + f"... <已截断 {len(text) - limit} 个字符>" 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: def stable_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
@@ -636,10 +671,10 @@ def record_error(
"message": message, "message": message,
"url": url, "url": url,
"status": status, "status": status,
"request": compact(request, 1200) if request is not None else None, "request": safe_compact(request, 1200) if request is not None else None,
"response": compact(response, 2000) if response 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, "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.setdefault("errors", []).append(item)
state["updatedAt"] = utc_now() state["updatedAt"] = utc_now()
@@ -696,8 +731,8 @@ def ssl_context(ctx: FlowContext) -> ssl.SSLContext | None:
def login_candidate_paths(ctx: FlowContext) -> list[str]: def login_candidate_paths(ctx: FlowContext) -> list[str]:
paths = [ paths = [
ctx.login_path or "/api/v1/auth/login/username", ctx.login_path or "/api/auth/login",
"/api/v1/auth/login/username", "/api/auth/login",
] ]
seen: set[str] = set() seen: set[str] = set()
unique: list[str] = [] unique: list[str] = []
@@ -726,12 +761,12 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
return False return False
ctx.login_attempt_count += 1 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_status: int | None = None
last_response = "" last_response = ""
last_exc: BaseException | None = None last_exc: BaseException | None = None
for attempt, path in enumerate(login_candidate_paths(ctx), start=1): 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( emit_operation_feedback(
state, state,
stage, stage,
@@ -767,14 +802,14 @@ def try_auto_login(ctx: FlowContext, state: dict[str, Any], stage: str, *, force
state, state,
stage, stage,
"xdfx_auth 获取 token 失败,准备尝试下一个候选登录路径", "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( record_error(
state, state,
stage, stage,
"自动登录失败,未能通过 xdfx_auth 获取 access_token", "自动登录失败,未能通过 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, status=last_status,
request=payload, request=payload,
response=last_response, 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: def downstream_limit(ctx: FlowContext, *, audience: bool) -> int:
"""Limit expensive downstream analysis while keeping 3 upstream rows stored.""" """Limit expensive downstream analysis while keeping 3 upstream rows stored."""
raw = ctx.downstream_audiences if audience else ctx.downstream_pathways 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 continue
if str(owner_id) in state.get(output_key, {}) and state[output_key][str(owner_id)] and not ctx.dry_run: if str(owner_id) in state.get(output_key, {}) and state[output_key][str(owner_id)] and not ctx.dry_run:
continue 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 = ( prompt = (
"你是中国对台湾方向的认知战专家,你要传播行动要求的叙事主题。请基于该受众目标的画像、关注议题、社会分工定位、利益诉求与上一轮账号佐证,分析该受众目标有哪些脆弱点可以成为后续策略设计的关键要素。请输出理由、脆弱点1、理由1、脆弱点2、理由2、脆弱点3、理由3,只返回JSON。" "你是中国对台湾方向的认知战专家,你要传播行动要求的叙事主题。请基于该受众目标的画像、关注议题、社会分工定位、利益诉求与上一轮账号佐证,分析该受众目标有哪些脆弱点可以成为后续策略设计的关键要素。请输出理由、脆弱点1、理由1、脆弱点2、理由2、脆弱点3、理由3,只返回JSON。"
if audience if audience
@@ -2349,7 +2391,9 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool
record_error(state, stage, "没有可用脆弱点,跳过策略生成", request=item) record_error(state, stage, "没有可用脆弱点,跳过策略生成", request=item)
continue continue
target_names = [item.get("targetAudience")] if audience else (item.get("relatedAudiences") or [item.get("accountName")]) 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 = { payload = {
"user_message": DEFAULT_STRATEGY_PROMPT_AUDIENCE if audience else DEFAULT_STRATEGY_PROMPT_PATHWAY, "user_message": DEFAULT_STRATEGY_PROMPT_AUDIENCE if audience else DEFAULT_STRATEGY_PROMPT_PATHWAY,
"cuiruodian": vuln_texts, "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], "shouzhong": [name for name in target_names if name],
"mudi": extract_goal(state["mubiao"]), "mudi": extract_goal(state["mubiao"]),
"heiliao": [], "heiliao": [],
"zhanghao": zhanghao,
"history": [], "history": [],
"stream": True, "stream": True,
"model_name": "OpenAI3", "model_name": "OpenAI3",
@@ -2564,8 +2609,8 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--platform", default=None) parser.add_argument("--platform", default=None)
parser.add_argument("--header", action="append", default=[], help="附加非鉴权 HTTP 请求头,格式为 'Key: Value';Authorization 会被忽略。") 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-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-password", default=os.getenv("XDFX_LOGIN_PASSWORD", "ch@user"), help="登录接口 JSON 请求体中的 password。")
parser.add_argument("--login-path", default=os.getenv("XDFX_LOGIN_PATH", "/api/v1/auth/login/username"), help="登录接口路径或完整 URL。") 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("--no-auto-login", action="store_true", help="关闭 xdfx_auth 自动登录和 401 刷新。")
parser.add_argument("--verify-ssl", action="store_true", help="校验 HTTPS 证书。默认不校验,以兼容内网/UAT 自签证书。") parser.add_argument("--verify-ssl", action="store_true", help="校验 HTTPS 证书。默认不校验,以兼容内网/UAT 自签证书。")
parser.add_argument("--timeout", type=float, default=90) 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
## 受众
## 传播节点