初始化
This commit is contained in:
1 parent
34474bd5fc
commit
b638b541d0
3008 files changed
+837699
No files matched your search
@@ -0,0 +1,50 @@
|
||||
---
|
||||
name: xdfx-step2-pathway
|
||||
description: xdfx 拆分流程 step2,基于受众分析关键传播节点,发起用户协助选择节点,保存选中的节点并建立受众关系。
|
||||
---
|
||||
|
||||
# Step2 关键传播节点分析与入库
|
||||
|
||||
前置条件:`xdfx-flow-state.json` 中已有 Step1 写入的 `topAudiences[*].designId`。
|
||||
|
||||
不要手写接口调用,直接运行:
|
||||
|
||||
```bash
|
||||
python scripts/xdfx_flow.py --mode step2 --input xdfx-input.json --state xdfx-flow-state.json --summary xdfx-flow-summary.md
|
||||
```
|
||||
|
||||
鉴权和证书由脚本兜底处理:脚本启动时先 `import scripts/xdfx_auth.py`,自动用 `admin / ch@user` 获取 `access_token`,并在第一次业务接口调用前写入内部 Authorization;不要通过命令行传 token。接口返回 401 时仍由 `xdfx_auth.py` 刷新 token 并重试原接口一次;HTTPS 默认不校验证书,需校验证书时追加 `--verify-ssl`。
|
||||
|
||||
本步骤由脚本完成:
|
||||
|
||||
- 对已入库受众逐个分析关键传播节点。
|
||||
- 自动解析候选账号和传播节点 JSON。
|
||||
- 流式模型接口首次空响应、模型无响应或超时类返回会自动再调用一次。
|
||||
- 跨受众合并去重后输出 `用户协助`,等待用户选择关键传播节点;用户选择后带 `--select-pathways 1,2,3` 重跑 Step2,才保存选中的节点。
|
||||
- 保存传播节点。
|
||||
- 保存传播节点与受众关系。
|
||||
- 同步前端步骤状态 `step-analysis-keypoint` 与 `step-analysis-weakpoint`。
|
||||
- 把 `pathwayAccountId`、关联 `designIds`、错误明细写入 state。
|
||||
|
||||
关系保存失败会写入错误;智能体必须按失败重试规则先重跑 Step2,不能直接跳到 Step3。
|
||||
|
||||
## 失败重试规则
|
||||
|
||||
- 出现 `错误反馈`、`JSON解析复核`,或 `步骤反馈` 中 `状态` 为 `部分完成` / `已跳过` / `本步骤新增错误数` 大于 0 时,必须先重跑当前 Step2 命令。
|
||||
- 重试前必须告诉用户失败阶段、接口 URL、状态码、响应摘要和“正在重试 Step2”,不能静默重试。
|
||||
- Step2 未重试成功前,不允许继续 Step3;不要因为 Step2 失败就跳过关键传播节点分析。
|
||||
- Step2 重试后仍失败时,停止自动推进,读取 `xdfx-flow-state.json.errors` 汇报失败细节,并等待用户处理或确认。
|
||||
|
||||
## 步骤反馈
|
||||
|
||||
命令结束会输出 `步骤反馈 {...}`。必须先根据这行反馈告诉用户 Step2 的完成状态、当前传播节点入库数量和新增错误数,再决定是否继续下一步。
|
||||
|
||||
如果输出 `用户协助 {...}`,必须暂停流程,把关键传播节点选项发给用户;用户选择后重跑 Step2 并追加 `--select-pathways ...`。
|
||||
|
||||
执行过程中会输出 `操作反馈 {...}`。每出现一次都要告诉用户当前操作和接口 URL。
|
||||
|
||||
保存成功会输出 `入库反馈 {...}`。每出现一次都要告诉用户已入库的传播节点和返回 ID。
|
||||
|
||||
如果出现 `错误反馈 {...}`,必须立即告诉用户失败阶段、接口 URL、状态码、请求摘要和响应摘要。
|
||||
|
||||
如果出现 `JSON解析复核 {...}`,必须查看 `响应片段`,判断是否能识别传播节点 JSON;无法确认时重跑 Step2。
|
||||
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Authentication helper for xdfx skill scripts.
|
||||
|
||||
The caller should not pass tokens through command-line arguments. This module
|
||||
owns the fixed admin login flow and returns a Bearer token before business
|
||||
interfaces are called.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import ssl
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class AuthResult:
|
||||
token: str
|
||||
url: str
|
||||
status: int | None
|
||||
raw: str
|
||||
|
||||
|
||||
class AuthError(RuntimeError):
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
*,
|
||||
url: str,
|
||||
status: int | None = None,
|
||||
response: str = "",
|
||||
exception: BaseException | None = None,
|
||||
) -> None:
|
||||
super().__init__(message)
|
||||
self.url = url
|
||||
self.status = status
|
||||
self.response = response
|
||||
self.exception = exception
|
||||
|
||||
|
||||
def auth_header_value(token: str) -> str:
|
||||
token = token.strip()
|
||||
return token if token.lower().startswith("bearer ") else f"Bearer {token}"
|
||||
|
||||
|
||||
def redact_url(url: str) -> str:
|
||||
return re.sub(r"([?&](?:password|token|authToken|access_token)=)[^&]+", r"\1***", url, flags=re.IGNORECASE)
|
||||
|
||||
|
||||
def ssl_context(*, verify_ssl: bool) -> ssl.SSLContext | None:
|
||||
if verify_ssl:
|
||||
return None
|
||||
return ssl._create_unverified_context()
|
||||
|
||||
|
||||
def build_login_url(api_base: str, path: str) -> str:
|
||||
return path if re.match(r"^https?://", path, flags=re.IGNORECASE) else api_base.rstrip("/") + "/" + path.lstrip("/")
|
||||
|
||||
|
||||
def extract_access_token(data: Any) -> str | None:
|
||||
if isinstance(data, dict):
|
||||
for key in ("access_token", "accessToken", "token", "jwt", "id_token", "authorization", "Authorization"):
|
||||
value = data.get(key)
|
||||
if isinstance(value, str) and value.strip():
|
||||
return value.strip()
|
||||
for value in data.values():
|
||||
nested = extract_access_token(value)
|
||||
if nested:
|
||||
return nested
|
||||
elif isinstance(data, list):
|
||||
for value in data:
|
||||
nested = extract_access_token(value)
|
||||
if nested:
|
||||
return nested
|
||||
return None
|
||||
|
||||
|
||||
def parse_json_loose(text: str) -> Any:
|
||||
try:
|
||||
return json.loads(text)
|
||||
except Exception:
|
||||
pass
|
||||
match = re.search(r"[\[{]", text or "")
|
||||
if not match:
|
||||
return {}
|
||||
start = match.start()
|
||||
for end in range(len(text), start, -1):
|
||||
try:
|
||||
return json.loads(text[start:end])
|
||||
except Exception:
|
||||
continue
|
||||
return {}
|
||||
|
||||
|
||||
def fetch_access_token(
|
||||
*,
|
||||
api_base: str,
|
||||
username: str = "admin",
|
||||
password: str = "ch@user",
|
||||
login_path: str = "/api/auth/login",
|
||||
timeout: float = 90,
|
||||
verify_ssl: bool = False,
|
||||
) -> AuthResult:
|
||||
"""Login once and return the extracted access token."""
|
||||
url = build_login_url(api_base, login_path)
|
||||
payload = {"username": username, "password": password}
|
||||
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout, context=ssl_context(verify_ssl=verify_ssl)) as resp:
|
||||
raw = resp.read().decode("utf-8", errors="replace")
|
||||
parsed = parse_json_loose(raw)
|
||||
token = extract_access_token(parsed)
|
||||
if not token:
|
||||
raise AuthError(
|
||||
"登录接口已响应但未找到 access_token",
|
||||
url=redact_url(url),
|
||||
status=getattr(resp, "status", None),
|
||||
response=raw,
|
||||
)
|
||||
return AuthResult(token=token, url=redact_url(url), status=getattr(resp, "status", None), raw=raw)
|
||||
except urllib.error.HTTPError as exc:
|
||||
body = exc.read().decode("utf-8", errors="replace") if exc.fp else ""
|
||||
raise AuthError(
|
||||
"登录接口返回错误",
|
||||
url=redact_url(url),
|
||||
status=exc.code,
|
||||
response=body,
|
||||
exception=exc,
|
||||
) from exc
|
||||
except AuthError:
|
||||
raise
|
||||
except Exception as exc: # noqa: BLE001 - auth errors must preserve detail for user feedback.
|
||||
raise AuthError(
|
||||
"登录接口调用异常",
|
||||
url=redact_url(url),
|
||||
response=str(exc),
|
||||
exception=exc,
|
||||
) from exc
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user