技能提交
This commit is contained in:
1 parent
210cb5117f
commit
6851c96a45
38 files changed
+8032
-1892
No files matched your search
@@ -211,6 +211,13 @@ export function AgentModuleSidebar({
|
||||
<Link
|
||||
key={`${keyPrefix}:${agent.id}`}
|
||||
href={href}
|
||||
onClick={() => {
|
||||
try {
|
||||
sessionStorage.setItem(`agent-module:${moduleKey}:last-agent`, agent.id);
|
||||
} catch {
|
||||
// Ignore storage failures; route navigation is the source of truth.
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"group relative flex w-full items-start gap-2.5 rounded-xl border px-2.5 py-2 transition-colors",
|
||||
isActive
|
||||
|
||||
@@ -84,6 +84,7 @@ function writeCachedYUserInfo(yUserId: string, info: Record<string, unknown>): v
|
||||
function buildExternalUserInfo(
|
||||
yUserId: string,
|
||||
result: Record<string, unknown>,
|
||||
access_token: string | null | undefined,
|
||||
previous: Record<string, unknown>,
|
||||
cached: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
@@ -101,6 +102,12 @@ function buildExternalUserInfo(
|
||||
return next;
|
||||
}
|
||||
|
||||
const queryAccessToken = normalizeExternalAccessToken(access_token);
|
||||
if (queryAccessToken) {
|
||||
next.accessToken = queryAccessToken;
|
||||
return next;
|
||||
}
|
||||
|
||||
const previousYUserId = readStoredYUserId(previous);
|
||||
const previousAccessToken =
|
||||
previousYUserId === yUserId ? normalizeExternalAccessToken(previous.accessToken) : "";
|
||||
@@ -121,6 +128,7 @@ function buildExternalUserInfo(
|
||||
export async function loginByYUserId(
|
||||
yUserId: string,
|
||||
token?: string | null,
|
||||
access_token?: string | null,
|
||||
): Promise<void> {
|
||||
if (token) {
|
||||
authStorageSet(TOOLS_TOKEN_STORAGE_KEY, token);
|
||||
@@ -134,12 +142,11 @@ export async function loginByYUserId(
|
||||
// consumer 返回 { accessToken, gateDepartName, typeName, userName }。
|
||||
// 一并存入路由参数 yUserId,使「应用管理」里 source=yUserId 的路由参数能取到(与
|
||||
// source=userName/gateDepartName/typeName 直接取接口值统一走 userInfo[source])。
|
||||
// 注意:接口也会返回 accessToken,但 XK/PG 及 source=accessToken 要用路由透传的
|
||||
// access_token,故登录流程随后会用 persistAccessToken 覆盖 userInfo.accessToken。
|
||||
// 注意:接口返回的 accessToken 优先;登录 URL 的 access_token 只在接口值为空时兜底。
|
||||
const result = res.data ?? {};
|
||||
const previous = readExternalUserInfo();
|
||||
const cached = readCachedYUserInfo(yUserId);
|
||||
const next = buildExternalUserInfo(yUserId, result, previous, cached);
|
||||
const next = buildExternalUserInfo(yUserId, result, access_token, previous, cached);
|
||||
authStorageSet(EXTERNAL_USER_INFO_STORAGE_KEY, JSON.stringify(next));
|
||||
writeCachedYUserInfo(yUserId, next);
|
||||
}
|
||||
@@ -149,7 +156,7 @@ export function persistToolsToken(token: string): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* 把内网登录路由参数 access_token 合并写入 userInfo.accessToken(覆盖接口返回的同名值)。
|
||||
* 把内网登录路由参数 access_token 合并写入 userInfo.accessToken。
|
||||
* 据此拼参的有:XK/PG 中任务 iframe(XK 用 accessToken、PG 用 access_token,二者均读
|
||||
* external-links.getAccessToken() = userInfo.accessToken);以及「应用管理」里
|
||||
* source=accessToken 的轻应用路由参数(resolveRouteParamValue 取 userInfo.accessToken)。
|
||||
|
||||
@@ -75,7 +75,7 @@ export default function LoginPage() {
|
||||
const yUserId = searchParams.get('yUserId')?.trim() ?? '';
|
||||
const toolToken = searchParams.get('token')?.trim() ?? '';
|
||||
// 内网登录透传的访问令牌:存入 userInfo.accessToken,供 XK/PG 中任务 iframe 拼参
|
||||
const accessToken = searchParams.get('access_token')?.trim() ?? '';
|
||||
const access_token = searchParams.get('access_token')?.trim() ?? '';
|
||||
// 用户名直登口令门:开启后端开关时,口令从 ?password= 透传
|
||||
const gatePassword = searchParams.get('password') ?? '';
|
||||
// token 换登录:?authToken= 传上游 token(与 tools_token 的 ?token= 区分开)
|
||||
@@ -182,7 +182,7 @@ export default function LoginPage() {
|
||||
if (onlineTicket) {
|
||||
deerflowUsername = await resolveUsernameByOnlineTicket(onlineTicket);
|
||||
} else if (yUserId) {
|
||||
await loginByYUserId(yUserId, toolToken || null);
|
||||
await loginByYUserId(yUserId, toolToken || null, access_token || null);
|
||||
} else if (toolToken) {
|
||||
persistToolsToken(toolToken);
|
||||
}
|
||||
@@ -212,9 +212,11 @@ export default function LoginPage() {
|
||||
}
|
||||
// 标准登录把用户名写入 userInfo,供顶栏用户区显示(已有则不覆盖)
|
||||
setStoredUserName(loginUserName);
|
||||
// 内网透传的 access_token 合并进 userInfo(在 loginByYUserId 覆盖之后),
|
||||
// XK/PG 中任务 iframe 据此取 accessToken
|
||||
persistAccessToken(accessToken);
|
||||
// 非 yUserId 登录仍可直接持久化 access_token;yUserId 登录已在 loginByYUserId 内按
|
||||
// “接口 accessToken 优先,URL access_token 仅兜底”的规则处理。
|
||||
if (!yUserId) {
|
||||
persistAccessToken(access_token);
|
||||
}
|
||||
// 嵌入模式不写 sidebarEnabled(localStorage 的「简洁/全量」偏好会跨标签污染正常页);
|
||||
// 去 chrome 由按标签页的嵌入会话(isEmbedActive)驱动,见 PageLayout。
|
||||
if (!isEmbed) persistSidebarEnabled(sidebarEnabled);
|
||||
@@ -273,7 +275,7 @@ export default function LoginPage() {
|
||||
sidebarEnabled,
|
||||
yUserId,
|
||||
toolToken,
|
||||
accessToken,
|
||||
access_token,
|
||||
onlineTicket,
|
||||
authToken,
|
||||
gatePassword,
|
||||
|
||||
@@ -11,13 +11,13 @@ import { RouteBaseContext } from "@/core/route-base";
|
||||
export function useRouter() {
|
||||
const navigate = useNavigate();
|
||||
const base = useContext(RouteBaseContext);
|
||||
const resolveHref = (href: string) =>
|
||||
href.startsWith(base) ? href : href.replace("/page/workspace", base);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
push: (href: string) =>
|
||||
navigate(href.replace("/page/workspace", base)),
|
||||
replace: (href: string) =>
|
||||
navigate(href.replace("/page/workspace", base), { replace: true }),
|
||||
push: (href: string) => navigate(resolveHref(href)),
|
||||
replace: (href: string) => navigate(resolveHref(href), { replace: true }),
|
||||
back: () => navigate(-1),
|
||||
forward: () => navigate(1),
|
||||
refresh: () => window.location.reload(),
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
|
||||
1. 受众目标分析,选出最优 3 个受众并入库。
|
||||
2. 关键传播节点分析,跨受众去重后选出最优 3 个节点并入库。
|
||||
3. 受众脆弱点分析并入库。
|
||||
4. 关键传播节点脆弱点分析并入库。
|
||||
5. 受众策略分析并入库。
|
||||
6. 关键传播节点策略分析并入库。
|
||||
3. 从已入库受众中默认选前 2 个继续,每个生成最多 3 个脆弱点并入库。
|
||||
4. 从已入库关键传播节点中默认选前 2 个继续,每个生成最多 3 个脆弱点并入库。
|
||||
5. 对上述选中的每个受众默认生成最多 3 个策略并入库。
|
||||
6. 对上述选中的每个关键传播节点默认生成最多 3 个策略并入库。
|
||||
7. 同步前端步骤状态,保证原 `item-aiceh` 页面步骤条可继续识别。
|
||||
|
||||
## 技能位置
|
||||
@@ -41,9 +41,9 @@ skills-data/xdfx-single-agent-full-flow/scripts/xdfx_flow.py
|
||||
}
|
||||
```
|
||||
|
||||
默认会自动处理登录态:当命令没有传 `Authorization`,或某个接口返回 401 时,脚本会用固定账号 `admin`、口令 `ch@user` 调用 `POST /api/v1/auth/login/username?password=ch%40user`,请求体为 `{"username":"admin"}`。拿到 `access_token` 后写入 `Authorization: Bearer ...` 并重试原接口一次。
|
||||
默认会自动处理登录态:脚本启动时先 `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`。如果已经有可用 token,也可以继续追加 `--auth-token TOKEN` 或 `--header "Authorization: Bearer TOKEN"`,脚本会优先使用传入 token。
|
||||
默认关闭 HTTPS 证书校验,用于兼容 UAT/内网自签证书;如果部署环境证书链正常,可以追加 `--verify-ssl`。接口返回 401 时仍由 `xdfx_auth.py` 刷新 token 并重试原接口一次。
|
||||
|
||||
## 执行命令
|
||||
|
||||
@@ -60,30 +60,44 @@ python scripts/xdfx_flow.py --mode step6 --input xdfx-input.json --state xdfx-fl
|
||||
|
||||
`--mode full` 只作为无人值守或排障兜底,不作为默认执行方式;外层工具通常会等长命令结束后才返回输出,无法保证真实逐步反馈。逐步命令共享同一个 `xdfx-flow-state.json`,不会影响 id 传递、断点续跑和最终入库。
|
||||
|
||||
如果需要一条命令完成逐步编排,可以使用包装脚本。它内部仍按 step1-step6 调用 `xdfx_flow.py`,并逐行转发反馈:
|
||||
|
||||
```bash
|
||||
python scripts/xdfx_run_full_steps.py --input xdfx-input.json --state xdfx-flow-state.json --summary xdfx-flow-summary.md
|
||||
```
|
||||
|
||||
每完成一步都会输出一行:
|
||||
|
||||
```text
|
||||
STEP_FEEDBACK {...}
|
||||
步骤反馈 {...}
|
||||
```
|
||||
|
||||
调用技能的智能体必须在每个 step 命令结束后读取 `STEP_FEEDBACK`,给用户一次反馈,再执行下一条 step 命令。
|
||||
调用技能的智能体必须在每个 step 命令结束后读取 `步骤反馈`,给用户一次反馈,再执行下一条 step 命令。
|
||||
|
||||
每次执行具体操作或调用接口前,脚本会输出:
|
||||
|
||||
```text
|
||||
OP_FEEDBACK {...}
|
||||
操作反馈 {...}
|
||||
```
|
||||
|
||||
调用技能的智能体必须把 `operation`、`method` 和 `url` 转述给用户,例如:
|
||||
调用技能的智能体必须把 `操作`、`请求方式` 和 `接口地址` 转述给用户,例如:
|
||||
|
||||
```text
|
||||
正在保存受众目标,调用 POST https://.../api/task/task-action-design。
|
||||
```
|
||||
|
||||
每次保存接口成功拿到 `id` 后,脚本会输出:
|
||||
|
||||
```text
|
||||
入库反馈 {...}
|
||||
```
|
||||
|
||||
调用技能的智能体必须把 `入库对象类型`、`入库对象名称` 和 `返回ID` 转述给用户,例如“已入库受众目标:xxx,designId=123”或“已入库受众脆弱点:xxx,id=456”。
|
||||
|
||||
如果报错,脚本会立即输出:
|
||||
|
||||
```text
|
||||
ERROR_FEEDBACK {...}
|
||||
错误反馈 {...}
|
||||
```
|
||||
|
||||
调用技能的智能体必须立刻告诉用户失败阶段、接口 URL、状态码、请求摘要和响应摘要。
|
||||
@@ -91,10 +105,10 @@ ERROR_FEEDBACK {...}
|
||||
如果模型 JSON 经过自动提取和修复仍无法解析,脚本会输出:
|
||||
|
||||
```text
|
||||
JSON_PARSE_REVIEW {...}
|
||||
JSON解析复核 {...}
|
||||
```
|
||||
|
||||
调用技能的智能体必须查看 `responseExcerpt`,判断是否能识别真正 JSON;无法确认时重跑当前步骤或重新调用对应模型接口。
|
||||
调用技能的智能体必须查看 `响应片段`,判断是否能识别真正 JSON;无法确认时重跑当前步骤或重新调用对应模型接口。
|
||||
|
||||
只检查脚本与参数、不调用真实接口:
|
||||
|
||||
@@ -130,6 +144,8 @@ python scripts/xdfx_flow.py --mode step6 --input xdfx-input.json --state xdfx-fl
|
||||
- `task-action-design-strategy-relation`
|
||||
- `task/task-action/{actionId}` 的 `step` 字段
|
||||
|
||||
策略信息形式/模态会按前端 `modelType` 字典保存为 value 逗号串,例如模型输出“图片,视频”时,脚本先查 `sys/sys-dict-data` 再写入 `task-action-message.modelType`,不会直接保存中文文字。
|
||||
|
||||
## 输出文件
|
||||
|
||||
执行后查看:
|
||||
@@ -139,27 +155,29 @@ xdfx-flow-state.json
|
||||
xdfx-flow-summary.md
|
||||
```
|
||||
|
||||
`xdfx-flow-state.json` 保存断点状态、接口返回 id、模型解析结果与错误明细。
|
||||
`xdfx-flow-state.json` 保存断点状态、接口返回 ID、模型解析结果与错误明细。
|
||||
|
||||
`xdfx-flow-summary.md` 是给用户看的执行摘要。
|
||||
|
||||
## 每步反馈
|
||||
|
||||
脚本会输出三类反馈行:
|
||||
脚本会输出五类反馈行:
|
||||
|
||||
- `OP_FEEDBACK`:当前正在做什么、调用哪个接口。
|
||||
- `ERROR_FEEDBACK`:当前错误的阶段、接口、状态码、请求摘要和响应摘要。
|
||||
- `JSON_PARSE_REVIEW`:模型返回内容无法稳定解析为 JSON,需要执行智能体复核是否重试。
|
||||
- `STEP_FEEDBACK`:当前步骤最终状态和累计入库数量。
|
||||
- `操作反馈`:当前正在做什么、调用哪个接口。
|
||||
- `入库反馈`:当前已入库的对象类型、名称和返回 ID。
|
||||
- `错误反馈`:当前错误的阶段、接口、状态码、请求摘要和响应摘要。
|
||||
- `JSON解析复核`:模型返回内容无法稳定解析为 JSON,需要执行智能体复核是否重试。
|
||||
- `步骤反馈`:当前步骤最终状态和累计入库数量。
|
||||
|
||||
`STEP_FEEDBACK` 中包含:
|
||||
`步骤反馈` 中包含:
|
||||
|
||||
- `step`:步骤编号。
|
||||
- `label`:步骤名称。
|
||||
- `status`:`ok`、`partial` 或 `skipped`。
|
||||
- `counts`:当前累计入库数量。
|
||||
- `newErrors`:本步骤新增错误数。
|
||||
- `totalErrors`:累计错误数。
|
||||
- `步骤`:步骤编号。
|
||||
- `步骤名称`:步骤名称。
|
||||
- `状态`:`成功`、`部分完成` 或 `已跳过`。
|
||||
- `执行状态`:中文执行状态。
|
||||
- `累计入库数量`:当前累计入库数量。
|
||||
- `本步骤新增错误数`:本步骤新增错误数。
|
||||
- `累计错误数`:累计错误数。
|
||||
|
||||
智能体反馈用户时建议使用这种格式:
|
||||
|
||||
@@ -167,20 +185,23 @@ xdfx-flow-summary.md
|
||||
Step1 受众目标分析完成:当前已入库受众 X 个,本步骤新增错误 Y 个。
|
||||
```
|
||||
|
||||
如果 `status=partial`,要补一句“该步骤部分完成”,并说明稍后最终汇总会列出错误明细。
|
||||
如果 `状态` 为 `部分完成`,要补一句“该步骤部分完成”,并说明稍后最终汇总会列出错误明细。
|
||||
|
||||
如果 `status=skipped`,要说明跳过原因。
|
||||
如果 `状态` 为 `已跳过`,要说明跳过原因。
|
||||
|
||||
## 容错规则
|
||||
|
||||
- 单个受众、账号、传播节点、脆弱点或策略失败,不中断整个流程。
|
||||
- 保存接口必须返回 `id`,否则不会进入下游步骤。
|
||||
- 受众目标默认入库 3 个,关键传播节点默认入库 3 个。
|
||||
- 下游脆弱点/策略默认只选择前 2 个受众和前 2 个传播节点继续,可用 `--downstream-audiences` / `--downstream-pathways` 调成 1 或 2。
|
||||
- 每个被选中的对象默认最多生成 3 个脆弱点、3 个策略。
|
||||
- 模型流式 `answer` 会自动拼接。
|
||||
- 流式模型接口第一次返回空响应、`y模型没有响应`、`模型没有响应` 或超时类字符串时,会自动再调用一次;仍失败才写入 `errors`。
|
||||
- 模型输出会自动去掉 `<think>`,从前后混杂文本、多个 JSON 代码块、无换行流式文本、连续 JSON 对象、非工整 JSON 中提取多个候选,并按当前步骤所需字段选择最像的 JSON。
|
||||
- 模型 JSON 会尝试修复中文引号、全角冒号/逗号、尾随逗号、未加引号的键、Python 风格 `True/False/None`。
|
||||
- 模型 JSON 解析失败会自动重试 1 次。
|
||||
- 每一步完成都会写入 state/summary 并输出 `STEP_FEEDBACK`。
|
||||
- 每一步完成都会写入 state/summary 并输出 `步骤反馈`。
|
||||
- 接口失败会写入 `errors`,包含阶段、URL、状态码、请求摘要、响应正文和异常类型。
|
||||
|
||||
## 回复用户规则
|
||||
@@ -190,3 +211,4 @@ Step1 受众目标分析完成:当前已入库受众 X 个,本步骤新增
|
||||
如果 `errors` 为空,可以说明“全流程完成”。
|
||||
|
||||
如果 `errors` 非空,只能说明“部分完成”,并列出失败阶段、接口 URL、状态码和响应摘要。
|
||||
|
||||
@@ -26,20 +26,20 @@ scripts/xdfx_flow.py
|
||||
}
|
||||
```
|
||||
|
||||
默认会在缺少 `Authorization` 或接口返回 401 时,自动用 `admin / ch@user` 调用:
|
||||
鉴权不能通过命令行传递 token。脚本会在启动阶段先 `import scripts/xdfx_auth.py`,用 `admin / ch@user` 获取访问令牌,并在第一步业务接口调用前写入请求头。
|
||||
|
||||
默认调用:
|
||||
|
||||
```text
|
||||
POST /api/v1/auth/login/username?password=ch%40user
|
||||
body: {"username":"admin"}
|
||||
```
|
||||
|
||||
拿到 `access_token` 后会写入 `Authorization: Bearer ...` 并重试原接口一次。登录接口和 401 重试都会输出 `OP_FEEDBACK` / `ERROR_FEEDBACK`,但反馈中的 `password`、`token` 会脱敏。默认关闭 HTTPS 证书校验,适配 UAT/内网自签证书;如果环境证书链正常,可以追加 `--verify-ssl`。
|
||||
|
||||
如果已经有可用登录态,也可以在命令里追加 `--auth-token TOKEN` 或 `--header "Authorization: Bearer TOKEN"`;此时脚本优先使用传入的 token。
|
||||
拿到 `access_token` 后会写入内部 `Authorization: Bearer ...`。不要通过任何命令行参数传 token;脚本会忽略外部 Authorization,统一由 `xdfx_auth.py` 获取和刷新。登录接口和 401 重试都会输出 `操作反馈` / `错误反馈`,但反馈中的 `password`、`token` 会脱敏。默认关闭 HTTPS 证书校验,适配 UAT/内网自签证书;如果环境证书链正常,可以追加 `--verify-ssl`。
|
||||
|
||||
## 执行命令
|
||||
|
||||
从本 Skill 目录逐步执行。虽然这是“一个技能做完整流程”的版本,但为了给用户真实的逐步反馈,智能体必须按 step1-step6 分六个命令运行,每个命令结束后先把 `STEP_FEEDBACK` / 新增错误反馈给用户,再继续下一步:
|
||||
从本 Skill 目录逐步执行。虽然这是“一个技能做完整流程”的版本,但为了给用户真实的逐步反馈,智能体必须按 step1-step6 分六个命令运行,每个命令结束后先把 `步骤反馈` / 新增错误反馈给用户,再继续下一步:
|
||||
|
||||
```bash
|
||||
python scripts/xdfx_flow.py --mode step1 --input xdfx-input.json --state xdfx-flow-state.json --summary xdfx-flow-summary.md
|
||||
@@ -52,13 +52,29 @@ python scripts/xdfx_flow.py --mode step6 --input xdfx-input.json --state xdfx-fl
|
||||
|
||||
`--mode full` 仅作为无人值守或本地排障兜底,不作为默认执行方式;因为外层工具通常会等长命令结束才返回输出,无法保证用户看到实时反馈。逐步命令共享同一个 `xdfx-flow-state.json`,id 传递和断点续跑不受影响。
|
||||
|
||||
每一步完成后都会输出一行 `STEP_FEEDBACK {...}`。智能体必须在每个 step 命令结束后给用户一次简短反馈,说明该步骤状态、已入库数量和新增错误数,然后再执行下一步。
|
||||
如果确实想用一条命令编排完整流程,优先使用轻量包装脚本,而不是 `--mode full`。它会在内部逐个启动 step 命令并转发每一行反馈:
|
||||
|
||||
脚本每次调用接口前还会输出 `OP_FEEDBACK {...}`,包含当前操作、HTTP 方法和 URL。智能体必须用简短中文告诉用户“正在做什么、调用哪个接口”。
|
||||
```bash
|
||||
python scripts/xdfx_run_full_steps.py --input xdfx-input.json --state xdfx-flow-state.json --summary xdfx-flow-summary.md
|
||||
```
|
||||
|
||||
脚本出现错误时会立即输出 `ERROR_FEEDBACK {...}`,包含阶段、接口 URL、状态码、请求摘要、响应摘要和异常类型。智能体必须立刻告诉用户错误细节,不能等到最后才说。
|
||||
每一步完成后都会输出一行 `步骤反馈 {...}`。智能体必须在每个 step 命令结束后给用户一次简短反馈,说明该步骤状态、已入库数量和新增错误数,然后再执行下一步。
|
||||
|
||||
如果模型返回内容经过多候选 JSON 提取和修复后仍无法解析,脚本会输出 `JSON_PARSE_REVIEW {...}`。智能体必须查看其中的 `responseExcerpt`,判断是否能识别出可用 JSON;无法确认时,重跑当前步骤或重新调用对应接口。
|
||||
脚本每次调用接口前还会输出 `操作反馈 {...}`,包含当前操作、HTTP 方法和 URL。智能体必须用简短中文告诉用户“正在做什么、调用哪个接口”。
|
||||
|
||||
每次保存接口成功拿到返回 ID 后会输出 `入库反馈 {...}`,包含 `入库对象类型`、`入库对象名称`、`返回ID` 和 `接口路径`。智能体必须把它转述成“已入库 xxx 受众 / xxx 脆弱点 / xxx 策略”等明确日志。
|
||||
|
||||
脚本出现错误时会立即输出 `错误反馈 {...}`,包含阶段、接口 URL、状态码、请求摘要、响应摘要和异常类型。智能体必须立刻告诉用户错误细节,不能等到最后才说。
|
||||
|
||||
如果模型返回内容经过多候选 JSON 提取和修复后仍无法解析,脚本会输出 `JSON解析复核 {...}`。智能体必须查看其中的 `响应片段`,判断是否能识别出可用 JSON;无法确认时,重跑当前步骤或重新调用对应接口。
|
||||
|
||||
## 失败重试规则
|
||||
|
||||
- 任意步骤出现 `错误反馈`、`JSON解析复核`,或 `步骤反馈` 中 `状态` 为 `部分完成` / `已跳过` / `本步骤新增错误数` 大于 0 时,必须先重跑当前失败的同一条 `--mode stepN` 命令。
|
||||
- 重试前必须告诉用户失败阶段、接口 URL、状态码、响应摘要和“正在重试当前步骤”,不能静默重试。
|
||||
- 当前步骤未重试成功前,不允许继续执行下一步;不要因为某一步失败就跳过进入后续分析或入库。
|
||||
- 同一步重试后仍失败时,停止自动推进,读取 `xdfx-flow-state.json.errors` 汇报失败细节,并等待用户处理或确认。
|
||||
- 只有用户明确要求忽略该失败,才可以继续后续步骤;回复中必须说明这是用户确认后的带风险继续。
|
||||
|
||||
只做参数和脚本检查、不调用接口:
|
||||
|
||||
@@ -68,36 +84,44 @@ python scripts/xdfx_flow.py --mode full --input xdfx-input.json --dry-run
|
||||
|
||||
## 脚本保证
|
||||
|
||||
- 受众只取最优 3 个继续。
|
||||
- 关键传播节点全局去重后只取最优 3 个继续。
|
||||
- 受众目标默认入库最优 3 个,可用 `--max-audiences` 调整。
|
||||
- 关键传播节点全局去重后默认入库最优 3 个,可用 `--max-pathways` 调整。
|
||||
- 下游脆弱点/策略默认只从已入库结果中选择前 2 个受众、前 2 个传播节点继续,可用 `--downstream-audiences` / `--downstream-pathways` 调成 1 或 2。
|
||||
- 每个被选中的受众/传播节点默认生成最多 3 个脆弱点,可用 `--max-vulns` 调整。
|
||||
- 每个被选中的受众/传播节点默认生成最多 3 个策略,可用 `--max-strategies` 调整。
|
||||
- 每个保存接口必须拿到返回 `id` 才进入下游。
|
||||
- 保存后会同步前端 `task/task-action.step` 步骤状态;如只想入库不更新步骤条,追加 `--no-step-status`。
|
||||
- 策略保存会同步写入 `task-action-channel`、`base-narrate`、`task-action-message`、`task-action-design-strategy-relation`,兼容策略表格/编排页面读取。
|
||||
- 策略 `信息形式/模态` 入库前会读取 `sys/sys-dict-data` 的 `modelType` 字典,把模型返回的“图片,视频/文本,图片”等文字转成前端需要的 value 逗号串;字典读取失败时使用内置兜底映射。
|
||||
- Step1 的受众目标抽取结果里,`用户编号列表` / `编号id` / `id` 必须对应上一轮 `zhanghao` 账号数组的索引值;脚本会按这个索引依次找到账号并保存受众账号关系。
|
||||
- 模型流式 `{"answer":"..."}` 会自动拼接。
|
||||
- 流式模型接口第一次返回空响应、`y模型没有响应`、`模型没有响应` 或超时类字符串时,会自动再调用一次;仍失败才写入 `errors`。
|
||||
- 模型输出会自动去掉 `<think>`,从前后混杂文本、多个 JSON 代码块、无换行流式文本、连续 JSON 对象、非工整 JSON 中提取多个候选,并按当前步骤所需字段选择最像的 JSON。
|
||||
- 模型 JSON 会尝试修复中文引号、全角冒号/逗号、尾随逗号、未加引号的键、Python 风格 `True/False/None`。
|
||||
- 模型 JSON 解析失败会自动重试 1 次。
|
||||
- 单个受众、账号、节点或策略失败不会拖垮全流程。
|
||||
- 每完成一步都会输出 `STEP_FEEDBACK`,包含 `step`、`label`、`status`、累计入库计数和新增错误数。
|
||||
- 每次接口调用前都会输出 `OP_FEEDBACK`,说明当前操作和接口 URL。
|
||||
- 每个错误都会立即输出 `ERROR_FEEDBACK`,说明失败阶段、接口、状态码、请求摘要和响应摘要。
|
||||
- 单个受众、账号、节点或策略失败会被脚本记录;智能体仍必须按“失败重试规则”先重跑当前步骤,不能直接跳到下一步。
|
||||
- 每完成一步都会输出 `步骤反馈`,包含 `步骤`、`步骤名称`、`状态`、`累计入库数量` 和 `本步骤新增错误数`。
|
||||
- 每次接口调用前都会输出 `操作反馈`,说明当前操作和接口 URL。
|
||||
- 每次保存成功都会输出 `入库反馈`,说明已入库对象类型、名称和返回 ID。
|
||||
- 每个错误都会立即输出 `错误反馈`,说明失败阶段、接口、状态码、请求摘要和响应摘要。
|
||||
- 所有接口错误都会写入 `xdfx-flow-state.json.errors`,包含阶段、URL、状态码、请求摘要、响应正文和异常类型。
|
||||
|
||||
## 回复用户
|
||||
|
||||
执行过程中,每出现一行 `STEP_FEEDBACK`,都要立刻向用户反馈一次:
|
||||
执行过程中,每出现一行 `步骤反馈`,都要立刻向用户反馈一次:
|
||||
|
||||
- 当前完成的步骤名称。
|
||||
- 状态:`ok`、`partial` 或 `skipped`。
|
||||
- 状态:`成功`、`部分完成` 或 `已跳过`。
|
||||
- 当前累计入库数量。
|
||||
- 本步骤新增错误数。
|
||||
|
||||
每出现一行 `OP_FEEDBACK`,都要简短告诉用户当前正在执行的操作和接口,例如“正在保存受众目标,调用 POST /api/task/task-action-design”。
|
||||
每出现一行 `操作反馈`,都要简短告诉用户当前正在执行的操作和接口,例如“正在保存受众目标,调用 POST /api/task/task-action-design”。
|
||||
|
||||
每出现一行 `ERROR_FEEDBACK`,都要立刻告诉用户失败阶段、接口 URL、状态码和响应摘要。
|
||||
每出现一行 `入库反馈`,都要简短告诉用户当前已入库的对象,例如“已入库受众目标:xxx,designId=123”或“已入库受众脆弱点:xxx,id=456”。
|
||||
|
||||
每出现一行 `JSON_PARSE_REVIEW`,都要查看 `responseExcerpt`。如果能明显识别出目标 JSON,说明需要人工复核;如果不能识别,重跑当前 step 或重新调用对应模型接口,并告诉用户“模型 JSON 不工整,正在复核并重试”。
|
||||
每出现一行 `错误反馈`,都要立刻告诉用户失败阶段、接口 URL、状态码和响应摘要。
|
||||
|
||||
每出现一行 `JSON解析复核`,都要查看 `响应片段`。如果能明显识别出目标 JSON,说明需要人工复核;如果不能识别,重跑当前 step 或重新调用对应模型接口,并告诉用户“模型 JSON 不工整,正在复核并重试”。
|
||||
|
||||
全部执行结束后再读取 `xdfx-flow-summary.md` 和命令输出,向用户说明:
|
||||
|
||||
@@ -109,3 +133,4 @@ python scripts/xdfx_flow.py --mode full --input xdfx-input.json --dry-run
|
||||
- 失败明细
|
||||
|
||||
只要 `errors` 不为空,就说“部分完成”,不要说“全部完成”。
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/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.parse
|
||||
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, password: str) -> str:
|
||||
base = path if re.match(r"^https?://", path, flags=re.IGNORECASE) else api_base.rstrip("/") + "/" + path.lstrip("/")
|
||||
if password:
|
||||
separator = "&" if "?" in base else "?"
|
||||
base = f"{base}{separator}password={urllib.parse.quote(password, safe='')}"
|
||||
return base
|
||||
|
||||
|
||||
def 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/v1/auth/login/username",
|
||||
timeout: float = 90,
|
||||
verify_ssl: bool = False,
|
||||
) -> AuthResult:
|
||||
"""Login once and return the extracted access token."""
|
||||
url = build_login_url(api_base, login_path, password)
|
||||
payload = {"username": username}
|
||||
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout, context=ssl_context(verify_ssl=verify_ssl)) as resp:
|
||||
raw = resp.read().decode("utf-8", errors="replace")
|
||||
parsed = parse_json_loose(raw)
|
||||
token = extract_access_token(parsed)
|
||||
if not token:
|
||||
raise AuthError(
|
||||
"登录接口已响应但未找到 access_token",
|
||||
url=redact_url(url),
|
||||
status=getattr(resp, "status", None),
|
||||
response=raw,
|
||||
)
|
||||
return AuthResult(token=token, url=redact_url(url), status=getattr(resp, "status", None), raw=raw)
|
||||
except urllib.error.HTTPError as exc:
|
||||
body = exc.read().decode("utf-8", errors="replace") if exc.fp else ""
|
||||
raise AuthError(
|
||||
"登录接口返回错误",
|
||||
url=redact_url(url),
|
||||
status=exc.code,
|
||||
response=body,
|
||||
exception=exc,
|
||||
) from exc
|
||||
except AuthError:
|
||||
raise
|
||||
except Exception as exc: # noqa: BLE001 - auth errors must preserve detail for user feedback.
|
||||
raise AuthError(
|
||||
"登录接口调用异常",
|
||||
url=redact_url(url),
|
||||
response=str(exc),
|
||||
exception=exc,
|
||||
) from exc
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the xdfx flow one step at a time.
|
||||
|
||||
The heavy business logic intentionally stays in xdfx_flow.py. This wrapper only
|
||||
keeps "full flow" orchestration readable and preserves per-step feedback: every
|
||||
subprocess line is flushed immediately, so OP/SAVE/ERROR/STEP feedback remains
|
||||
visible to the calling agent.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
STEPS = ("step1", "step2", "step3", "step4", "step5", "step6")
|
||||
|
||||
STEP_LABELS = {
|
||||
"step1": "受众目标分析与入库",
|
||||
"step2": "关键传播节点分析与入库",
|
||||
"step3": "受众脆弱点分析与入库",
|
||||
"step4": "传播节点脆弱点分析与入库",
|
||||
"step5": "受众策略分析与入库",
|
||||
"step6": "传播节点策略分析与入库",
|
||||
}
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
"""Parse only wrapper-owned flags; pass all other flags to xdfx_flow.py."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="按六个步骤运行 xdfx_flow.py,并实时转发中文反馈日志。",
|
||||
add_help=True,
|
||||
)
|
||||
parser.add_argument("--from-step", choices=STEPS, default="step1")
|
||||
parser.add_argument("--to-step", choices=STEPS, default="step6")
|
||||
parser.add_argument(
|
||||
"--continue-on-error",
|
||||
action="store_true",
|
||||
help="某一步返回非零退出码时仍继续执行后续步骤。",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def selected_steps(first: str, last: str) -> list[str]:
|
||||
"""Return an inclusive step slice for resume/partial reruns."""
|
||||
start = STEPS.index(first)
|
||||
end = STEPS.index(last)
|
||||
if start > end:
|
||||
raise SystemExit("起始步骤必须小于或等于结束步骤")
|
||||
return list(STEPS[start : end + 1])
|
||||
|
||||
|
||||
def stream_command(step: str, cmd: list[str]) -> int:
|
||||
"""Run one step and forward stdout/stderr lines as they are produced."""
|
||||
print(f"运行步骤:{STEP_LABELS.get(step, step)};执行命令:" + " ".join(cmd), flush=True)
|
||||
env = os.environ.copy()
|
||||
env["PYTHONIOENCODING"] = "utf-8"
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
env=env,
|
||||
)
|
||||
assert process.stdout is not None
|
||||
for line in process.stdout:
|
||||
sys.stdout.buffer.write(line.encode("utf-8", errors="replace"))
|
||||
sys.stdout.buffer.flush()
|
||||
return process.wait()
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = build_parser()
|
||||
args, passthrough = parser.parse_known_args(argv)
|
||||
flow_script = Path(__file__).with_name("xdfx_flow.py")
|
||||
exit_code = 0
|
||||
for step in selected_steps(args.from_step, args.to_step):
|
||||
# Keep all business flags unchanged so the wrapper behaves like a thin
|
||||
# streaming version of repeated xdfx_flow.py --mode stepN calls.
|
||||
cmd = [sys.executable, str(flow_script), "--mode", step, *passthrough]
|
||||
code = stream_command(step, cmd)
|
||||
if code:
|
||||
exit_code = code
|
||||
if not args.continue_on_error:
|
||||
return code
|
||||
return exit_code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -28,7 +28,7 @@ scripts/xdfx_flow.py
|
||||
|
||||
## 推荐执行顺序
|
||||
|
||||
默认会自动处理登录态:当命令没有传 `Authorization`,或某个接口返回 401 时,脚本会用固定账号 `admin`、口令 `ch@user` 调用 `POST /api/v1/auth/login/username?password=ch%40user`,请求体为 `{"username":"admin"}`。拿到 `access_token` 后写入 `Authorization: Bearer ...` 并重试原接口一次。默认关闭 HTTPS 证书校验以兼容 UAT/内网自签证书;如果环境证书链正常,可以追加 `--verify-ssl`。如果已有可用 token,可追加 `--auth-token TOKEN` 或 `--header "Authorization: Bearer TOKEN"`。
|
||||
默认会自动处理登录态:脚本启动时先 `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`。
|
||||
|
||||
总控按顺序执行下面 6 条命令,所有步骤共用同一个 `xdfx-flow-state.json`:
|
||||
|
||||
@@ -41,13 +41,23 @@ python scripts/xdfx_flow.py --mode step5 --input xdfx-input.json --state xdfx-fl
|
||||
python scripts/xdfx_flow.py --mode step6 --input xdfx-input.json --state xdfx-flow-state.json --summary xdfx-flow-summary.md
|
||||
```
|
||||
|
||||
每条命令结束都会输出一行 `STEP_FEEDBACK {...}`。总控必须在每个 step 命令结束后立即反馈用户,再继续下一步。
|
||||
每条命令结束都会输出一行 `步骤反馈 {...}`。总控必须在每个 step 命令结束后立即反馈用户,再继续下一步。
|
||||
|
||||
每次接口调用前会输出 `OP_FEEDBACK {...}`。总控必须告诉用户当前正在做什么、调用哪个接口。
|
||||
每次接口调用前会输出 `操作反馈 {...}`。总控必须告诉用户当前正在做什么、调用哪个接口。
|
||||
|
||||
如果接口或模型处理报错,会立即输出 `ERROR_FEEDBACK {...}`。总控必须立刻把失败阶段、URL、状态码、请求摘要和响应摘要告诉用户。
|
||||
每次保存接口成功拿到返回 ID 后会输出 `入库反馈 {...}`。总控必须告诉用户已入库的对象类型、名称和返回 ID,例如“已入库关键传播节点:xxx,pathwayAccountId=123”。
|
||||
|
||||
如果模型返回文本经过自动修复仍无法解析,会输出 `JSON_PARSE_REVIEW {...}`。总控必须查看响应片段,判断是否需要重跑当前 step 或重新调用对应接口。
|
||||
如果接口或模型处理报错,会立即输出 `错误反馈 {...}`。总控必须立刻把失败阶段、URL、状态码、请求摘要和响应摘要告诉用户。
|
||||
|
||||
如果模型返回文本经过自动修复仍无法解析,会输出 `JSON解析复核 {...}`。总控必须查看响应片段,判断是否需要重跑当前 step 或重新调用对应接口。
|
||||
|
||||
## 失败重试规则
|
||||
|
||||
- 任意 step 出现 `错误反馈`、`JSON解析复核`,或 `步骤反馈` 中 `状态` 为 `部分完成` / `已跳过` / `本步骤新增错误数` 大于 0 时,总控必须先重跑当前失败的同一条 `--mode stepN` 命令。
|
||||
- 重试前必须告诉用户失败阶段、接口 URL、状态码、响应摘要和“正在重试当前步骤”,不能静默重试。
|
||||
- 当前 step 未重试成功前,不允许继续执行下一 step;不要因为某一步失败就跳过进入后续分析或入库。
|
||||
- 同一 step 重试后仍失败时,停止自动推进,读取 `xdfx-flow-state.json.errors` 汇报失败细节,并等待用户处理或确认。
|
||||
- 只有用户明确要求忽略该失败,才可以继续后续 step;回复中必须说明这是用户确认后的带风险继续。
|
||||
|
||||
如果只想一条命令跑完,也可以使用下面命令做无人值守兜底,但不建议作为默认方式,因为外层工具通常会等命令结束后才返回输出,无法保证用户看到实时反馈:
|
||||
|
||||
@@ -55,21 +65,32 @@ python scripts/xdfx_flow.py --mode step6 --input xdfx-input.json --state xdfx-fl
|
||||
python scripts/xdfx_flow.py --mode full --input xdfx-input.json --state xdfx-flow-state.json --summary xdfx-flow-summary.md
|
||||
```
|
||||
|
||||
更推荐的一条命令方式是使用包装脚本;它内部仍按 step1-step6 分步调用,并逐行转发反馈:
|
||||
|
||||
```bash
|
||||
python scripts/xdfx_run_full_steps.py --input xdfx-input.json --state xdfx-flow-state.json --summary xdfx-flow-summary.md
|
||||
```
|
||||
|
||||
## 断点续跑
|
||||
|
||||
- Step1 成功后会写入 `topAudiences`。
|
||||
- Step1 的受众目标抽取结果里,`用户编号列表` / `编号id` / `id` 必须对应上一轮 `zhanghao` 账号数组的索引值;脚本会按这个索引依次找到账号并保存受众账号关系。
|
||||
- Step2 成功后会写入 `topPathways`。
|
||||
- Step3/Step4 会写入两类脆弱点。
|
||||
- Step5/Step6 会写入两类策略。
|
||||
- Step1 默认入库最优 3 个受众;Step2 默认入库最优 3 个传播节点。
|
||||
- Step3/Step4 默认只选择前 2 个已入库受众/传播节点继续,每个生成最多 3 个脆弱点;可用 `--downstream-audiences` / `--downstream-pathways` 调成 1 或 2。
|
||||
- Step5/Step6 默认对上述选中的每个受众/传播节点生成最多 3 个策略。
|
||||
- 各步骤会同步前端 `task/task-action.step`;需要禁用时每条命令追加 `--no-step-status`。
|
||||
- 策略步骤会同步写入 channel、narrate、message、策略与受众/传播节点关系表,保证原前端策略表格/编排页可继续读取。
|
||||
- 每个步骤完成都会输出 `STEP_FEEDBACK`,总控必须逐步转述给用户。
|
||||
- 每次操作和接口调用都会输出 `OP_FEEDBACK`,总控必须逐条转述当前操作。
|
||||
- 每次错误都会输出 `ERROR_FEEDBACK`,总控必须立即报告错误细节。
|
||||
- 每次 JSON 彻底解析失败都会输出 `JSON_PARSE_REVIEW`,总控必须复核响应片段并判断是否重试当前 step。
|
||||
- 策略步骤保存 `信息形式/模态` 时,必须使用脚本读取 `sys/sys-dict-data` 的 `modelType` 字典并把文字转成 value 逗号串,不能把“图片,视频”等文字直接写入 `task-action-message.modelType`。
|
||||
- 每个步骤完成都会输出 `步骤反馈`,总控必须逐步转述给用户。
|
||||
- 每次操作和接口调用都会输出 `操作反馈`,总控必须逐条转述当前操作。
|
||||
- 每次保存成功都会输出 `入库反馈`,总控必须逐条转述入库对象。
|
||||
- 每次错误都会输出 `错误反馈`,总控必须立即报告错误细节。
|
||||
- 每次 JSON 彻底解析失败都会输出 `JSON解析复核`,总控必须复核响应片段并判断是否重试当前 step。
|
||||
- 每个步骤里的流式模型接口第一次空响应、返回 `y模型没有响应`/`模型没有响应` 或超时类字符串时,会自动再调用一次;仍失败才写入 `errors`。
|
||||
- 某一步失败后,修正问题再重跑同一个 `--mode stepN`。
|
||||
- 某一步失败后,必须先重跑同一个 `--mode stepN`;重试仍失败再等待用户处理或确认,不能自动跳到后续步骤。
|
||||
|
||||
## 汇报规则
|
||||
|
||||
执行过程中按 `OP_FEEDBACK` 告诉用户当前操作和接口;按 `ERROR_FEEDBACK` 立即报告错误;每个 step 结束后再根据 `STEP_FEEDBACK` 回复用户一次。全部步骤结束后,读取 `xdfx-flow-summary.md` 和 `xdfx-flow-state.json.errors` 再做最终汇总。只要存在错误,就明确说“部分完成”,并列出失败阶段、接口 URL、状态码和响应摘要。
|
||||
执行过程中按 `操作反馈` 告诉用户当前操作和接口;按 `入库反馈` 告诉用户已入库什么对象和返回 ID;按 `错误反馈` 立即报告错误;每个步骤结束后再根据 `步骤反馈` 回复用户一次。全部步骤结束后,读取 `xdfx-flow-summary.md` 和 `xdfx-flow-state.json.errors` 再做最终汇总。只要存在错误,就明确说“部分完成”,并列出失败阶段、接口 URL、状态码和响应摘要。
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/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.parse
|
||||
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, password: str) -> str:
|
||||
base = path if re.match(r"^https?://", path, flags=re.IGNORECASE) else api_base.rstrip("/") + "/" + path.lstrip("/")
|
||||
if password:
|
||||
separator = "&" if "?" in base else "?"
|
||||
base = f"{base}{separator}password={urllib.parse.quote(password, safe='')}"
|
||||
return base
|
||||
|
||||
|
||||
def 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/v1/auth/login/username",
|
||||
timeout: float = 90,
|
||||
verify_ssl: bool = False,
|
||||
) -> AuthResult:
|
||||
"""Login once and return the extracted access token."""
|
||||
url = build_login_url(api_base, login_path, password)
|
||||
payload = {"username": username}
|
||||
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout, context=ssl_context(verify_ssl=verify_ssl)) as resp:
|
||||
raw = resp.read().decode("utf-8", errors="replace")
|
||||
parsed = parse_json_loose(raw)
|
||||
token = extract_access_token(parsed)
|
||||
if not token:
|
||||
raise AuthError(
|
||||
"登录接口已响应但未找到 access_token",
|
||||
url=redact_url(url),
|
||||
status=getattr(resp, "status", None),
|
||||
response=raw,
|
||||
)
|
||||
return AuthResult(token=token, url=redact_url(url), status=getattr(resp, "status", None), raw=raw)
|
||||
except urllib.error.HTTPError as exc:
|
||||
body = exc.read().decode("utf-8", errors="replace") if exc.fp else ""
|
||||
raise AuthError(
|
||||
"登录接口返回错误",
|
||||
url=redact_url(url),
|
||||
status=exc.code,
|
||||
response=body,
|
||||
exception=exc,
|
||||
) from exc
|
||||
except AuthError:
|
||||
raise
|
||||
except Exception as exc: # noqa: BLE001 - auth errors must preserve detail for user feedback.
|
||||
raise AuthError(
|
||||
"登录接口调用异常",
|
||||
url=redact_url(url),
|
||||
response=str(exc),
|
||||
exception=exc,
|
||||
) from exc
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the xdfx flow one step at a time.
|
||||
|
||||
The heavy business logic intentionally stays in xdfx_flow.py. This wrapper only
|
||||
keeps "full flow" orchestration readable and preserves per-step feedback: every
|
||||
subprocess line is flushed immediately, so OP/SAVE/ERROR/STEP feedback remains
|
||||
visible to the calling agent.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
STEPS = ("step1", "step2", "step3", "step4", "step5", "step6")
|
||||
|
||||
STEP_LABELS = {
|
||||
"step1": "受众目标分析与入库",
|
||||
"step2": "关键传播节点分析与入库",
|
||||
"step3": "受众脆弱点分析与入库",
|
||||
"step4": "传播节点脆弱点分析与入库",
|
||||
"step5": "受众策略分析与入库",
|
||||
"step6": "传播节点策略分析与入库",
|
||||
}
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
"""Parse only wrapper-owned flags; pass all other flags to xdfx_flow.py."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="按六个步骤运行 xdfx_flow.py,并实时转发中文反馈日志。",
|
||||
add_help=True,
|
||||
)
|
||||
parser.add_argument("--from-step", choices=STEPS, default="step1")
|
||||
parser.add_argument("--to-step", choices=STEPS, default="step6")
|
||||
parser.add_argument(
|
||||
"--continue-on-error",
|
||||
action="store_true",
|
||||
help="某一步返回非零退出码时仍继续执行后续步骤。",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def selected_steps(first: str, last: str) -> list[str]:
|
||||
"""Return an inclusive step slice for resume/partial reruns."""
|
||||
start = STEPS.index(first)
|
||||
end = STEPS.index(last)
|
||||
if start > end:
|
||||
raise SystemExit("起始步骤必须小于或等于结束步骤")
|
||||
return list(STEPS[start : end + 1])
|
||||
|
||||
|
||||
def stream_command(step: str, cmd: list[str]) -> int:
|
||||
"""Run one step and forward stdout/stderr lines as they are produced."""
|
||||
print(f"运行步骤:{STEP_LABELS.get(step, step)};执行命令:" + " ".join(cmd), flush=True)
|
||||
env = os.environ.copy()
|
||||
env["PYTHONIOENCODING"] = "utf-8"
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
env=env,
|
||||
)
|
||||
assert process.stdout is not None
|
||||
for line in process.stdout:
|
||||
sys.stdout.buffer.write(line.encode("utf-8", errors="replace"))
|
||||
sys.stdout.buffer.flush()
|
||||
return process.wait()
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = build_parser()
|
||||
args, passthrough = parser.parse_known_args(argv)
|
||||
flow_script = Path(__file__).with_name("xdfx_flow.py")
|
||||
exit_code = 0
|
||||
for step in selected_steps(args.from_step, args.to_step):
|
||||
# Keep all business flags unchanged so the wrapper behaves like a thin
|
||||
# streaming version of repeated xdfx_flow.py --mode stepN calls.
|
||||
cmd = [sys.executable, str(flow_script), "--mode", step, *passthrough]
|
||||
code = stream_command(step, cmd)
|
||||
if code:
|
||||
exit_code = code
|
||||
if not args.continue_on_error:
|
||||
return code
|
||||
return exit_code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -45,12 +45,12 @@ scripts/xdfx_flow.py
|
||||
xdfx-flow-state.json
|
||||
```
|
||||
|
||||
默认会自动处理登录态:缺少 `Authorization` 或接口返回 401 时,脚本会用固定账号 `admin`、口令 `ch@user` 调用 `POST /api/v1/auth/login/username?password=ch%40user`,请求体为 `{"username":"admin"}`,拿到 `access_token` 后写入 `Authorization: Bearer ...` 并重试原接口一次。默认关闭 HTTPS 证书校验,用于兼容 UAT/内网自签证书;需要严格校验证书时追加 `--verify-ssl`。已有 token 时可追加 `--auth-token TOKEN` 或 `--header "Authorization: Bearer TOKEN"`。
|
||||
默认会自动处理登录态:脚本启动时先 `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 并重试原接口一次。
|
||||
|
||||
每一步命令结束都会输出一行:
|
||||
|
||||
```text
|
||||
STEP_FEEDBACK {...}
|
||||
步骤反馈 {...}
|
||||
```
|
||||
|
||||
总控智能体必须在每一步结束后立即把这行反馈转述给用户,再执行下一步。
|
||||
@@ -58,15 +58,23 @@ STEP_FEEDBACK {...}
|
||||
每次执行具体操作或调用接口前,脚本会输出:
|
||||
|
||||
```text
|
||||
OP_FEEDBACK {...}
|
||||
操作反馈 {...}
|
||||
```
|
||||
|
||||
总控智能体必须把 `operation`、`method` 和 `url` 转述给用户。
|
||||
总控智能体必须把 `操作`、`请求方式` 和 `接口地址` 转述给用户。
|
||||
|
||||
每次保存接口成功拿到 `id` 后,脚本会输出:
|
||||
|
||||
```text
|
||||
入库反馈 {...}
|
||||
```
|
||||
|
||||
总控智能体必须把 `入库对象类型`、`入库对象名称` 和 `返回ID` 转述给用户,例如“已入库受众目标:xxx,designId=123”或“已入库策略:xxx,id=456”。
|
||||
|
||||
如果报错,脚本会立即输出:
|
||||
|
||||
```text
|
||||
ERROR_FEEDBACK {...}
|
||||
错误反馈 {...}
|
||||
```
|
||||
|
||||
总控智能体必须立刻告诉用户失败阶段、接口 URL、状态码、请求摘要和响应摘要。
|
||||
@@ -74,12 +82,12 @@ ERROR_FEEDBACK {...}
|
||||
如果模型 JSON 经过自动提取和修复仍无法解析,脚本会输出:
|
||||
|
||||
```text
|
||||
JSON_PARSE_REVIEW {...}
|
||||
JSON解析复核 {...}
|
||||
```
|
||||
|
||||
总控智能体必须查看 `responseExcerpt`,判断是否能识别真正 JSON;无法确认时重跑当前 step 或重新调用对应模型接口。
|
||||
总控智能体必须查看 `响应片段`,判断是否能识别真正 JSON;无法确认时重跑当前步骤或重新调用对应模型接口。
|
||||
|
||||
每一步中的流式模型接口第一次返回空响应、`y模型没有响应`、`模型没有响应` 或超时类字符串时,脚本会自动再调用一次;如果第二次仍失败,才写入 `errors` 并在该步骤反馈里体现为 `partial`。
|
||||
每一步中的流式模型接口第一次返回空响应、`y模型没有响应`、`模型没有响应` 或超时类字符串时,脚本会自动再调用一次;如果第二次仍失败,才写入 `errors` 并在该步骤反馈里体现为 `部分完成`。
|
||||
|
||||
## 推荐执行顺序
|
||||
|
||||
@@ -96,23 +104,39 @@ python scripts/xdfx_flow.py --mode step6 --input xdfx-input.json --state xdfx-fl
|
||||
|
||||
如果某一步失败,修复接口、鉴权或输入后,只重跑对应 `stepN` 即可。
|
||||
|
||||
如果需要一条命令完成分步编排,可以在 `xdfx-split-controller` 目录使用包装脚本;它内部仍按 step1-step6 调用执行器,并逐行转发反馈:
|
||||
|
||||
```bash
|
||||
python scripts/xdfx_run_full_steps.py --input xdfx-input.json --state xdfx-flow-state.json --summary xdfx-flow-summary.md
|
||||
```
|
||||
|
||||
## 每步反馈格式
|
||||
|
||||
脚本会输出三类反馈行:
|
||||
脚本会输出五类反馈行:
|
||||
|
||||
- `OP_FEEDBACK`:当前正在做什么、调用哪个接口。
|
||||
- `ERROR_FEEDBACK`:当前错误的阶段、接口、状态码、请求摘要和响应摘要。
|
||||
- `JSON_PARSE_REVIEW`:模型返回内容无法稳定解析为 JSON,需要执行智能体复核是否重试。
|
||||
- `STEP_FEEDBACK`:当前步骤最终状态和累计入库数量。
|
||||
- `操作反馈`:当前正在做什么、调用哪个接口。
|
||||
- `入库反馈`:当前已入库对象的类型、名称和返回 ID。
|
||||
- `错误反馈`:当前错误的阶段、接口、状态码、请求摘要和响应摘要。
|
||||
- `JSON解析复核`:模型返回内容无法稳定解析为 JSON,需要执行智能体复核是否重试。
|
||||
- `步骤反馈`:当前步骤最终状态和累计入库数量。
|
||||
|
||||
`STEP_FEEDBACK` 中包含:
|
||||
`步骤反馈` 中包含:
|
||||
|
||||
- `step`:步骤编号。
|
||||
- `label`:步骤名称。
|
||||
- `status`:`ok`、`partial` 或 `skipped`。
|
||||
- `counts`:当前累计入库数量。
|
||||
- `newErrors`:本步骤新增错误数。
|
||||
- `totalErrors`:累计错误数。
|
||||
- `步骤`:步骤编号。
|
||||
- `步骤名称`:步骤名称。
|
||||
- `状态`:`成功`、`部分完成` 或 `已跳过`。
|
||||
- `执行状态`:中文执行状态。
|
||||
- `累计入库数量`:当前累计入库数量。
|
||||
- `本步骤新增错误数`:本步骤新增错误数。
|
||||
- `累计错误数`:累计错误数。
|
||||
|
||||
默认数量规则:
|
||||
|
||||
- Step1 入库最优 3 个受众。
|
||||
- Step2 入库最优 3 个传播节点。
|
||||
- Step3/Step4 只从已入库结果中默认选择前 2 个受众/传播节点继续,每个生成最多 3 个脆弱点;可用 `--downstream-audiences` / `--downstream-pathways` 调成 1 或 2。
|
||||
- Step5/Step6 对上述选中的每个受众/传播节点默认生成最多 3 个策略。
|
||||
- Step5/Step6 的策略模态必须按前端 `modelType` 字典保存为 value 逗号串;脚本会自动把“图片,视频/文本,图片”等文字转成 `task-action-message.modelType` 需要的值。
|
||||
|
||||
总控反馈用户时建议使用这种格式:
|
||||
|
||||
@@ -120,9 +144,9 @@ python scripts/xdfx_flow.py --mode step6 --input xdfx-input.json --state xdfx-fl
|
||||
Step2 关键传播节点分析完成:当前已入库传播节点 X 个,本步骤新增错误 Y 个。
|
||||
```
|
||||
|
||||
如果 `status=partial`,说明该步骤部分完成,并继续执行后续可执行步骤。
|
||||
如果 `状态` 为 `部分完成`,说明该步骤部分完成,并继续执行后续可执行步骤。
|
||||
|
||||
如果 `status=skipped`,说明跳过原因。
|
||||
如果 `状态` 为 `已跳过`,说明跳过原因。
|
||||
|
||||
## 各步骤职责
|
||||
|
||||
@@ -254,7 +278,7 @@ xdfx-flow-summary.md
|
||||
xdfx-flow-state.json
|
||||
```
|
||||
|
||||
优先依据本步骤的 `STEP_FEEDBACK` 给即时反馈;最终汇总时再读取完整 summary 和 errors。
|
||||
优先依据本步骤的 `步骤反馈` 给即时反馈;最终汇总时再读取完整 summary 和 errors。
|
||||
|
||||
如果 `errors` 非空,必须告诉用户:
|
||||
|
||||
@@ -266,3 +290,4 @@ xdfx-flow-state.json
|
||||
- 哪些数据已成功入库,哪些数据未完成。
|
||||
|
||||
只要存在错误,就说“部分完成”,不要说“全部完成”。
|
||||
|
||||
@@ -24,7 +24,7 @@ python scripts/xdfx_flow.py --mode step1 --input xdfx-input.json --state xdfx-fl
|
||||
}
|
||||
```
|
||||
|
||||
鉴权和证书由脚本兜底处理:缺少 `Authorization` 或接口返回 401 时,自动用 `admin / ch@user` 登录获取 `access_token` 并重试原接口一次;HTTPS 默认不校验证书,需校验证书时追加 `--verify-ssl`。
|
||||
鉴权和证书由脚本兜底处理:脚本启动时先 `import scripts/xdfx_auth.py`,自动用 `admin / ch@user` 获取 `access_token`,并在第一次业务接口调用前写入内部 Authorization;不要通过命令行传 token。接口返回 401 时仍由 `xdfx_auth.py` 刷新 token 并重试原接口一次;HTTPS 默认不校验证书,需校验证书时追加 `--verify-ssl`。
|
||||
|
||||
本步骤由脚本完成:
|
||||
|
||||
@@ -32,7 +32,8 @@ python scripts/xdfx_flow.py --mode step1 --input xdfx-input.json --state xdfx-fl
|
||||
- 自动解析流式 answer 与候选账号。
|
||||
- 流式模型接口首次空响应、模型无响应或超时类返回会自动再调用一次。
|
||||
- 抽取结构化受众。
|
||||
- 选择最优 3 个受众。
|
||||
- 抽取结果中的 `用户编号列表` / `编号id` / `id` 必须对应上一轮 `zhanghao` 账号数组的索引值;脚本按索引依次找到账号并保存。
|
||||
- 选择并入库最优 3 个受众。
|
||||
- 保存受众目标。
|
||||
- 保存受众目标账号。
|
||||
- 同步前端步骤状态 `step-analysis-audience` 与 `step-analysis-keypoint`。
|
||||
@@ -40,12 +41,22 @@ python scripts/xdfx_flow.py --mode step1 --input xdfx-input.json --state xdfx-fl
|
||||
|
||||
执行后检查 `xdfx-flow-summary.md`。如果 `errors` 非空,向用户报告失败接口、状态码、响应正文摘要。
|
||||
|
||||
## 失败重试规则
|
||||
|
||||
- 出现 `错误反馈`、`JSON解析复核`,或 `步骤反馈` 中 `状态` 为 `部分完成` / `已跳过` / `本步骤新增错误数` 大于 0 时,必须先重跑当前 Step1 命令。
|
||||
- 重试前必须告诉用户失败阶段、接口 URL、状态码、响应摘要和“正在重试 Step1”,不能静默重试。
|
||||
- Step1 未重试成功前,不允许继续 Step2;不要因为 Step1 失败就跳过受众分析。
|
||||
- Step1 重试后仍失败时,停止自动推进,读取 `xdfx-flow-state.json.errors` 汇报失败细节,并等待用户处理或确认。
|
||||
|
||||
## 步骤反馈
|
||||
|
||||
命令结束会输出 `STEP_FEEDBACK {...}`。必须先根据这行反馈告诉用户 Step1 的完成状态、当前受众入库数量和新增错误数,再决定是否继续下一步。
|
||||
命令结束会输出 `步骤反馈 {...}`。必须先根据这行反馈告诉用户 Step1 的完成状态、当前受众入库数量和新增错误数,再决定是否继续下一步。
|
||||
|
||||
执行过程中会输出 `OP_FEEDBACK {...}`。每出现一次都要告诉用户当前操作和接口 URL。
|
||||
执行过程中会输出 `操作反馈 {...}`。每出现一次都要告诉用户当前操作和接口 URL。
|
||||
|
||||
如果出现 `ERROR_FEEDBACK {...}`,必须立即告诉用户失败阶段、接口 URL、状态码、请求摘要和响应摘要。
|
||||
保存成功会输出 `入库反馈 {...}`。每出现一次都要告诉用户已入库的受众、受众账号和返回 ID。
|
||||
|
||||
如果出现 `错误反馈 {...}`,必须立即告诉用户失败阶段、接口 URL、状态码、请求摘要和响应摘要。
|
||||
|
||||
如果出现 `JSON解析复核 {...}`,必须查看 `响应片段`,判断是否能识别受众 JSON;无法确认时重跑 Step1。
|
||||
|
||||
如果出现 `JSON_PARSE_REVIEW {...}`,必须查看 `responseExcerpt`,判断是否能识别受众 JSON;无法确认时重跑 Step1。
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/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.parse
|
||||
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, password: str) -> str:
|
||||
base = path if re.match(r"^https?://", path, flags=re.IGNORECASE) else api_base.rstrip("/") + "/" + path.lstrip("/")
|
||||
if password:
|
||||
separator = "&" if "?" in base else "?"
|
||||
base = f"{base}{separator}password={urllib.parse.quote(password, safe='')}"
|
||||
return base
|
||||
|
||||
|
||||
def 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/v1/auth/login/username",
|
||||
timeout: float = 90,
|
||||
verify_ssl: bool = False,
|
||||
) -> AuthResult:
|
||||
"""Login once and return the extracted access token."""
|
||||
url = build_login_url(api_base, login_path, password)
|
||||
payload = {"username": username}
|
||||
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
@@ -13,27 +13,37 @@ description: xdfx 拆分流程 step2,基于受众分析关键传播节点,
|
||||
python scripts/xdfx_flow.py --mode step2 --input xdfx-input.json --state xdfx-flow-state.json --summary xdfx-flow-summary.md
|
||||
```
|
||||
|
||||
鉴权和证书由脚本兜底处理:缺少 `Authorization` 或接口返回 401 时,自动用 `admin / ch@user` 登录获取 `access_token` 并重试原接口一次;HTTPS 默认不校验证书,需校验证书时追加 `--verify-ssl`。
|
||||
鉴权和证书由脚本兜底处理:脚本启动时先 `import scripts/xdfx_auth.py`,自动用 `admin / ch@user` 获取 `access_token`,并在第一次业务接口调用前写入内部 Authorization;不要通过命令行传 token。接口返回 401 时仍由 `xdfx_auth.py` 刷新 token 并重试原接口一次;HTTPS 默认不校验证书,需校验证书时追加 `--verify-ssl`。
|
||||
|
||||
本步骤由脚本完成:
|
||||
|
||||
- 对已入库受众逐个分析关键传播节点。
|
||||
- 自动解析候选账号和传播节点 JSON。
|
||||
- 流式模型接口首次空响应、模型无响应或超时类返回会自动再调用一次。
|
||||
- 跨受众合并去重,选择全局最优 3 个传播节点。
|
||||
- 跨受众合并去重,选择并入库全局最优 3 个传播节点。
|
||||
- 保存传播节点。
|
||||
- 保存传播节点与受众关系。
|
||||
- 同步前端步骤状态 `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` 汇报失败细节,并等待用户处理或确认。
|
||||
|
||||
## 步骤反馈
|
||||
|
||||
命令结束会输出 `STEP_FEEDBACK {...}`。必须先根据这行反馈告诉用户 Step2 的完成状态、当前传播节点入库数量和新增错误数,再决定是否继续下一步。
|
||||
命令结束会输出 `步骤反馈 {...}`。必须先根据这行反馈告诉用户 Step2 的完成状态、当前传播节点入库数量和新增错误数,再决定是否继续下一步。
|
||||
|
||||
执行过程中会输出 `OP_FEEDBACK {...}`。每出现一次都要告诉用户当前操作和接口 URL。
|
||||
执行过程中会输出 `操作反馈 {...}`。每出现一次都要告诉用户当前操作和接口 URL。
|
||||
|
||||
如果出现 `ERROR_FEEDBACK {...}`,必须立即告诉用户失败阶段、接口 URL、状态码、请求摘要和响应摘要。
|
||||
保存成功会输出 `入库反馈 {...}`。每出现一次都要告诉用户已入库的传播节点和返回 ID。
|
||||
|
||||
如果出现 `错误反馈 {...}`,必须立即告诉用户失败阶段、接口 URL、状态码、请求摘要和响应摘要。
|
||||
|
||||
如果出现 `JSON解析复核 {...}`,必须查看 `响应片段`,判断是否能识别传播节点 JSON;无法确认时重跑 Step2。
|
||||
|
||||
如果出现 `JSON_PARSE_REVIEW {...}`,必须查看 `responseExcerpt`,判断是否能识别传播节点 JSON;无法确认时重跑 Step2。
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/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.parse
|
||||
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, password: str) -> str:
|
||||
base = path if re.match(r"^https?://", path, flags=re.IGNORECASE) else api_base.rstrip("/") + "/" + path.lstrip("/")
|
||||
if password:
|
||||
separator = "&" if "?" in base else "?"
|
||||
base = f"{base}{separator}password={urllib.parse.quote(password, safe='')}"
|
||||
return base
|
||||
|
||||
|
||||
def 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/v1/auth/login/username",
|
||||
timeout: float = 90,
|
||||
verify_ssl: bool = False,
|
||||
) -> AuthResult:
|
||||
"""Login once and return the extracted access token."""
|
||||
url = build_login_url(api_base, login_path, password)
|
||||
payload = {"username": username}
|
||||
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
@@ -13,26 +13,37 @@ description: xdfx 拆分流程 step3,为已入库受众抽取并保存最多3
|
||||
python scripts/xdfx_flow.py --mode step3 --input xdfx-input.json --state xdfx-flow-state.json --summary xdfx-flow-summary.md
|
||||
```
|
||||
|
||||
鉴权和证书由脚本兜底处理:缺少 `Authorization` 或接口返回 401 时,自动用 `admin / ch@user` 登录获取 `access_token` 并重试原接口一次;HTTPS 默认不校验证书,需校验证书时追加 `--verify-ssl`。
|
||||
鉴权和证书由脚本兜底处理:脚本启动时先 `import scripts/xdfx_auth.py`,自动用 `admin / ch@user` 获取 `access_token`,并在第一次业务接口调用前写入内部 Authorization;不要通过命令行传 token。接口返回 401 时仍由 `xdfx_auth.py` 刷新 token 并重试原接口一次;HTTPS 默认不校验证书,需校验证书时追加 `--verify-ssl`。
|
||||
|
||||
本步骤由脚本完成:
|
||||
|
||||
- 对每个已入库受众抽取最多 3 个脆弱点。
|
||||
- 从已入库受众中默认选择前 2 个继续分析;可用 `--downstream-audiences 1` 调成只处理 1 个。
|
||||
- 对每个被选中的受众抽取并入库最多 3 个脆弱点。
|
||||
- 流式模型接口首次空响应、模型无响应或超时类返回会自动再调用一次。
|
||||
- 自动修复和解析模型 JSON。
|
||||
- 调用受众脆弱点保存接口。
|
||||
- 只有保存成功并返回 `id` 的脆弱点才进入后续策略阶段。
|
||||
- 同步前端步骤状态 `step-analysis-weakpoint`。
|
||||
- 单个受众失败不影响其他受众。
|
||||
- 单个受众失败会写入错误;智能体必须按失败重试规则先重跑 Step3,不能直接跳到 Step4/Step5。
|
||||
|
||||
完成后查看 `audienceVulns` 和 `errors`。
|
||||
|
||||
## 失败重试规则
|
||||
|
||||
- 出现 `错误反馈`、`JSON解析复核`,或 `步骤反馈` 中 `状态` 为 `部分完成` / `已跳过` / `本步骤新增错误数` 大于 0 时,必须先重跑当前 Step3 命令。
|
||||
- 重试前必须告诉用户失败阶段、接口 URL、状态码、响应摘要和“正在重试 Step3”,不能静默重试。
|
||||
- Step3 未重试成功前,不允许继续 Step4/Step5;不要因为 Step3 失败就跳过受众脆弱点分析。
|
||||
- Step3 重试后仍失败时,停止自动推进,读取 `xdfx-flow-state.json.errors` 汇报失败细节,并等待用户处理或确认。
|
||||
|
||||
## 步骤反馈
|
||||
|
||||
命令结束会输出 `STEP_FEEDBACK {...}`。必须先根据这行反馈告诉用户 Step3 的完成状态、当前受众脆弱点数量和新增错误数,再决定是否继续下一步。
|
||||
命令结束会输出 `步骤反馈 {...}`。必须先根据这行反馈告诉用户 Step3 的完成状态、当前受众脆弱点数量和新增错误数,再决定是否继续下一步。
|
||||
|
||||
执行过程中会输出 `OP_FEEDBACK {...}`。每出现一次都要告诉用户当前操作和接口 URL。
|
||||
执行过程中会输出 `操作反馈 {...}`。每出现一次都要告诉用户当前操作和接口 URL。
|
||||
|
||||
如果出现 `ERROR_FEEDBACK {...}`,必须立即告诉用户失败阶段、接口 URL、状态码、请求摘要和响应摘要。
|
||||
保存成功会输出 `入库反馈 {...}`。每出现一次都要告诉用户已入库的受众脆弱点和返回 ID。
|
||||
|
||||
如果出现 `错误反馈 {...}`,必须立即告诉用户失败阶段、接口 URL、状态码、请求摘要和响应摘要。
|
||||
|
||||
如果出现 `JSON解析复核 {...}`,必须查看 `响应片段`,判断是否能识别受众脆弱点 JSON;无法确认时重跑 Step3。
|
||||
|
||||
如果出现 `JSON_PARSE_REVIEW {...}`,必须查看 `responseExcerpt`,判断是否能识别受众脆弱点 JSON;无法确认时重跑 Step3。
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/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.parse
|
||||
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, password: str) -> str:
|
||||
base = path if re.match(r"^https?://", path, flags=re.IGNORECASE) else api_base.rstrip("/") + "/" + path.lstrip("/")
|
||||
if password:
|
||||
separator = "&" if "?" in base else "?"
|
||||
base = f"{base}{separator}password={urllib.parse.quote(password, safe='')}"
|
||||
return base
|
||||
|
||||
|
||||
def 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/v1/auth/login/username",
|
||||
timeout: float = 90,
|
||||
verify_ssl: bool = False,
|
||||
) -> AuthResult:
|
||||
"""Login once and return the extracted access token."""
|
||||
url = build_login_url(api_base, login_path, password)
|
||||
payload = {"username": username}
|
||||
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
@@ -13,26 +13,37 @@ description: xdfx 拆分流程 step4,为已入库关键传播节点抽取并
|
||||
python scripts/xdfx_flow.py --mode step4 --input xdfx-input.json --state xdfx-flow-state.json --summary xdfx-flow-summary.md
|
||||
```
|
||||
|
||||
鉴权和证书由脚本兜底处理:缺少 `Authorization` 或接口返回 401 时,自动用 `admin / ch@user` 登录获取 `access_token` 并重试原接口一次;HTTPS 默认不校验证书,需校验证书时追加 `--verify-ssl`。
|
||||
鉴权和证书由脚本兜底处理:脚本启动时先 `import scripts/xdfx_auth.py`,自动用 `admin / ch@user` 获取 `access_token`,并在第一次业务接口调用前写入内部 Authorization;不要通过命令行传 token。接口返回 401 时仍由 `xdfx_auth.py` 刷新 token 并重试原接口一次;HTTPS 默认不校验证书,需校验证书时追加 `--verify-ssl`。
|
||||
|
||||
本步骤由脚本完成:
|
||||
|
||||
- 对每个已入库关键传播节点抽取最多 3 个脆弱点。
|
||||
- 从已入库关键传播节点中默认选择前 2 个继续分析;可用 `--downstream-pathways 1` 调成只处理 1 个。
|
||||
- 对每个被选中的关键传播节点抽取并入库最多 3 个脆弱点。
|
||||
- 流式模型接口首次空响应、模型无响应或超时类返回会自动再调用一次。
|
||||
- 自动修复和解析模型 JSON。
|
||||
- 调用传播节点脆弱点保存接口。
|
||||
- 只有保存成功并返回 `id` 的脆弱点才进入后续策略阶段。
|
||||
- 同步前端步骤状态 `step-analysis-weakpoint`。
|
||||
- 单个节点失败不影响其他节点。
|
||||
- 单个节点失败会写入错误;智能体必须按失败重试规则先重跑 Step4,不能直接跳到 Step5/Step6。
|
||||
|
||||
完成后查看 `pathwayVulns` 和 `errors`。
|
||||
|
||||
## 失败重试规则
|
||||
|
||||
- 出现 `错误反馈`、`JSON解析复核`,或 `步骤反馈` 中 `状态` 为 `部分完成` / `已跳过` / `本步骤新增错误数` 大于 0 时,必须先重跑当前 Step4 命令。
|
||||
- 重试前必须告诉用户失败阶段、接口 URL、状态码、响应摘要和“正在重试 Step4”,不能静默重试。
|
||||
- Step4 未重试成功前,不允许继续 Step5/Step6;不要因为 Step4 失败就跳过传播节点脆弱点分析。
|
||||
- Step4 重试后仍失败时,停止自动推进,读取 `xdfx-flow-state.json.errors` 汇报失败细节,并等待用户处理或确认。
|
||||
|
||||
## 步骤反馈
|
||||
|
||||
命令结束会输出 `STEP_FEEDBACK {...}`。必须先根据这行反馈告诉用户 Step4 的完成状态、当前传播节点脆弱点数量和新增错误数,再决定是否继续下一步。
|
||||
命令结束会输出 `步骤反馈 {...}`。必须先根据这行反馈告诉用户 Step4 的完成状态、当前传播节点脆弱点数量和新增错误数,再决定是否继续下一步。
|
||||
|
||||
执行过程中会输出 `OP_FEEDBACK {...}`。每出现一次都要告诉用户当前操作和接口 URL。
|
||||
执行过程中会输出 `操作反馈 {...}`。每出现一次都要告诉用户当前操作和接口 URL。
|
||||
|
||||
如果出现 `ERROR_FEEDBACK {...}`,必须立即告诉用户失败阶段、接口 URL、状态码、请求摘要和响应摘要。
|
||||
保存成功会输出 `入库反馈 {...}`。每出现一次都要告诉用户已入库的传播节点脆弱点和返回 ID。
|
||||
|
||||
如果出现 `错误反馈 {...}`,必须立即告诉用户失败阶段、接口 URL、状态码、请求摘要和响应摘要。
|
||||
|
||||
如果出现 `JSON解析复核 {...}`,必须查看 `响应片段`,判断是否能识别传播节点脆弱点 JSON;无法确认时重跑 Step4。
|
||||
|
||||
如果出现 `JSON_PARSE_REVIEW {...}`,必须查看 `responseExcerpt`,判断是否能识别传播节点脆弱点 JSON;无法确认时重跑 Step4。
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/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.parse
|
||||
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, password: str) -> str:
|
||||
base = path if re.match(r"^https?://", path, flags=re.IGNORECASE) else api_base.rstrip("/") + "/" + path.lstrip("/")
|
||||
if password:
|
||||
separator = "&" if "?" in base else "?"
|
||||
base = f"{base}{separator}password={urllib.parse.quote(password, safe='')}"
|
||||
return base
|
||||
|
||||
|
||||
def 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/v1/auth/login/username",
|
||||
timeout: float = 90,
|
||||
verify_ssl: bool = False,
|
||||
) -> AuthResult:
|
||||
"""Login once and return the extracted access token."""
|
||||
url = build_login_url(api_base, login_path, password)
|
||||
payload = {"username": username}
|
||||
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
@@ -13,27 +13,39 @@ description: xdfx 拆分流程 step5,基于受众目标与受众脆弱点生
|
||||
python scripts/xdfx_flow.py --mode step5 --input xdfx-input.json --state xdfx-flow-state.json --summary xdfx-flow-summary.md
|
||||
```
|
||||
|
||||
鉴权和证书由脚本兜底处理:缺少 `Authorization` 或接口返回 401 时,自动用 `admin / ch@user` 登录获取 `access_token` 并重试原接口一次;HTTPS 默认不校验证书,需校验证书时追加 `--verify-ssl`。
|
||||
鉴权和证书由脚本兜底处理:脚本启动时先 `import scripts/xdfx_auth.py`,自动用 `admin / ch@user` 获取 `access_token`,并在第一次业务接口调用前写入内部 Authorization;不要通过命令行传 token。接口返回 401 时仍由 `xdfx_auth.py` 刷新 token 并重试原接口一次;HTTPS 默认不校验证书,需校验证书时追加 `--verify-ssl`。
|
||||
|
||||
本步骤由脚本完成:
|
||||
|
||||
- 按受众读取已入库脆弱点。
|
||||
- 从已入库受众中默认选择前 2 个继续生成策略;可用 `--downstream-audiences 1` 调成只处理 1 个。
|
||||
- 对每个被选中的受众读取已入库脆弱点并默认生成最多 3 个策略。
|
||||
- 调用策略生成模型接口。
|
||||
- 流式模型接口首次空响应、模型无响应或超时类返回会自动再调用一次。
|
||||
- 自动解析 `发布人设N / 账号类型N / 信息标题N` 等字段。
|
||||
- 调用策略保存接口。
|
||||
- 同步写入 `task-action-channel`、`base-narrate`、`task-action-message`、`task-action-design-strategy-relation`。
|
||||
- 保存 `task-action-message.modelType` 前会读取前端 `modelType` 字典并把“图片,视频/文本,图片”等模态文字转成 value 逗号串,避免策略表格回显为空。
|
||||
- 同步前端步骤状态 `step-analysis-strategy` 与 `step-analysis-strategy-gantt`。
|
||||
- 保存成功后写入 `audienceStrategies`。
|
||||
|
||||
如果某个受众没有可用脆弱点,脚本会跳过并在 `errors` 中记录。
|
||||
如果某个受众没有可用脆弱点,脚本会记录错误;智能体必须先回到 Step3 或重跑 Step5 修复当前策略环节,不能直接跳过。
|
||||
|
||||
## 失败重试规则
|
||||
|
||||
- 出现 `错误反馈`、`JSON解析复核`,或 `步骤反馈` 中 `状态` 为 `部分完成` / `已跳过` / `本步骤新增错误数` 大于 0 时,必须先重跑当前 Step5 命令。
|
||||
- 如果失败原因是缺少受众脆弱点,先重跑 Step3 生成脆弱点,再重跑 Step5;重试前必须告诉用户原因和重试动作。
|
||||
- Step5 未重试成功前,不允许继续 Step6 或做最终汇总;不要因为 Step5 失败就跳过受众策略分析。
|
||||
- Step5 重试后仍失败时,停止自动推进,读取 `xdfx-flow-state.json.errors` 汇报失败细节,并等待用户处理或确认。
|
||||
|
||||
## 步骤反馈
|
||||
|
||||
命令结束会输出 `STEP_FEEDBACK {...}`。必须先根据这行反馈告诉用户 Step5 的完成状态、当前受众策略数量和新增错误数,再决定是否继续下一步。
|
||||
命令结束会输出 `步骤反馈 {...}`。必须先根据这行反馈告诉用户 Step5 的完成状态、当前受众策略数量和新增错误数,再决定是否继续下一步。
|
||||
|
||||
执行过程中会输出 `OP_FEEDBACK {...}`。每出现一次都要告诉用户当前操作和接口 URL。
|
||||
执行过程中会输出 `操作反馈 {...}`。每出现一次都要告诉用户当前操作和接口 URL。
|
||||
|
||||
如果出现 `ERROR_FEEDBACK {...}`,必须立即告诉用户失败阶段、接口 URL、状态码、请求摘要和响应摘要。
|
||||
保存成功会输出 `入库反馈 {...}`。每出现一次都要告诉用户已入库的策略、策略子表和返回 ID。
|
||||
|
||||
如果出现 `错误反馈 {...}`,必须立即告诉用户失败阶段、接口 URL、状态码、请求摘要和响应摘要。
|
||||
|
||||
如果出现 `JSON解析复核 {...}`,必须查看 `响应片段`,判断是否能识别受众策略 JSON;无法确认时重跑 Step5。
|
||||
|
||||
如果出现 `JSON_PARSE_REVIEW {...}`,必须查看 `responseExcerpt`,判断是否能识别受众策略 JSON;无法确认时重跑 Step5。
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/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.parse
|
||||
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, password: str) -> str:
|
||||
base = path if re.match(r"^https?://", path, flags=re.IGNORECASE) else api_base.rstrip("/") + "/" + path.lstrip("/")
|
||||
if password:
|
||||
separator = "&" if "?" in base else "?"
|
||||
base = f"{base}{separator}password={urllib.parse.quote(password, safe='')}"
|
||||
return base
|
||||
|
||||
|
||||
def 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/v1/auth/login/username",
|
||||
timeout: float = 90,
|
||||
verify_ssl: bool = False,
|
||||
) -> AuthResult:
|
||||
"""Login once and return the extracted access token."""
|
||||
url = build_login_url(api_base, login_path, password)
|
||||
payload = {"username": username}
|
||||
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
@@ -13,27 +13,39 @@ description: xdfx 拆分流程 step6,基于关键传播节点与节点脆弱
|
||||
python scripts/xdfx_flow.py --mode step6 --input xdfx-input.json --state xdfx-flow-state.json --summary xdfx-flow-summary.md
|
||||
```
|
||||
|
||||
鉴权和证书由脚本兜底处理:缺少 `Authorization` 或接口返回 401 时,自动用 `admin / ch@user` 登录获取 `access_token` 并重试原接口一次;HTTPS 默认不校验证书,需校验证书时追加 `--verify-ssl`。
|
||||
鉴权和证书由脚本兜底处理:脚本启动时先 `import scripts/xdfx_auth.py`,自动用 `admin / ch@user` 获取 `access_token`,并在第一次业务接口调用前写入内部 Authorization;不要通过命令行传 token。接口返回 401 时仍由 `xdfx_auth.py` 刷新 token 并重试原接口一次;HTTPS 默认不校验证书,需校验证书时追加 `--verify-ssl`。
|
||||
|
||||
本步骤由脚本完成:
|
||||
|
||||
- 按关键传播节点读取已入库脆弱点。
|
||||
- 从已入库关键传播节点中默认选择前 2 个继续生成策略;可用 `--downstream-pathways 1` 调成只处理 1 个。
|
||||
- 对每个被选中的关键传播节点读取已入库脆弱点并默认生成最多 3 个策略。
|
||||
- 调用策略生成模型接口。
|
||||
- 流式模型接口首次空响应、模型无响应或超时类返回会自动再调用一次。
|
||||
- 自动解析 `发布人设N / 账号类型N / 信息标题N` 等字段。
|
||||
- 调用策略保存接口。
|
||||
- 同步写入 `task-action-channel`、`base-narrate`、`task-action-message`、`task-action-design-strategy-relation`。
|
||||
- 保存 `task-action-message.modelType` 前会读取前端 `modelType` 字典并把“图片,视频/文本,图片”等模态文字转成 value 逗号串,避免策略表格回显为空。
|
||||
- 同步前端步骤状态 `step-analysis-strategy` 与 `step-analysis-strategy-gantt`。
|
||||
- 保存成功后写入 `pathwayStrategies`。
|
||||
|
||||
如果某个传播节点没有可用脆弱点,脚本会跳过并在 `errors` 中记录。
|
||||
如果某个传播节点没有可用脆弱点,脚本会记录错误;智能体必须先回到 Step4 或重跑 Step6 修复当前策略环节,不能直接跳过。
|
||||
|
||||
## 失败重试规则
|
||||
|
||||
- 出现 `错误反馈`、`JSON解析复核`,或 `步骤反馈` 中 `状态` 为 `部分完成` / `已跳过` / `本步骤新增错误数` 大于 0 时,必须先重跑当前 Step6 命令。
|
||||
- 如果失败原因是缺少传播节点脆弱点,先重跑 Step4 生成脆弱点,再重跑 Step6;重试前必须告诉用户原因和重试动作。
|
||||
- Step6 未重试成功前,不允许做最终成功汇总;不要因为 Step6 失败就跳过传播节点策略分析。
|
||||
- Step6 重试后仍失败时,停止自动推进,读取 `xdfx-flow-state.json.errors` 汇报失败细节,并等待用户处理或确认。
|
||||
|
||||
## 步骤反馈
|
||||
|
||||
命令结束会输出 `STEP_FEEDBACK {...}`。必须先根据这行反馈告诉用户 Step6 的完成状态、当前传播节点策略数量和新增错误数,再做最终汇总。
|
||||
命令结束会输出 `步骤反馈 {...}`。必须先根据这行反馈告诉用户 Step6 的完成状态、当前传播节点策略数量和新增错误数,再做最终汇总。
|
||||
|
||||
执行过程中会输出 `OP_FEEDBACK {...}`。每出现一次都要告诉用户当前操作和接口 URL。
|
||||
执行过程中会输出 `操作反馈 {...}`。每出现一次都要告诉用户当前操作和接口 URL。
|
||||
|
||||
如果出现 `ERROR_FEEDBACK {...}`,必须立即告诉用户失败阶段、接口 URL、状态码、请求摘要和响应摘要。
|
||||
保存成功会输出 `入库反馈 {...}`。每出现一次都要告诉用户已入库的策略、策略子表和返回 ID。
|
||||
|
||||
如果出现 `错误反馈 {...}`,必须立即告诉用户失败阶段、接口 URL、状态码、请求摘要和响应摘要。
|
||||
|
||||
如果出现 `JSON解析复核 {...}`,必须查看 `响应片段`,判断是否能识别传播节点策略 JSON;无法确认时重跑 Step6。
|
||||
|
||||
如果出现 `JSON_PARSE_REVIEW {...}`,必须查看 `responseExcerpt`,判断是否能识别传播节点策略 JSON;无法确认时重跑 Step6。
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/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.parse
|
||||
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, password: str) -> str:
|
||||
base = path if re.match(r"^https?://", path, flags=re.IGNORECASE) else api_base.rstrip("/") + "/" + path.lstrip("/")
|
||||
if password:
|
||||
separator = "&" if "?" in base else "?"
|
||||
base = f"{base}{separator}password={urllib.parse.quote(password, safe='')}"
|
||||
return base
|
||||
|
||||
|
||||
def 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/v1/auth/login/username",
|
||||
timeout: float = 90,
|
||||
verify_ssl: bool = False,
|
||||
) -> AuthResult:
|
||||
"""Login once and return the extracted access token."""
|
||||
url = build_login_url(api_base, login_path, password)
|
||||
payload = {"username": username}
|
||||
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout, context=ssl_context(verify_ssl=verify_ssl)) as resp:
|
||||
raw = resp.read().decode("utf-8", errors="replace")
|
||||
parsed = parse_json_loose(raw)
|
||||
token = extract_access_token(parsed)
|
||||
if not token:
|
||||
raise AuthError(
|
||||
"登录接口已响应但未找到 access_token",
|
||||
url=redact_url(url),
|
||||
status=getattr(resp, "status", None),
|
||||
response=raw,
|
||||
)
|
||||
return AuthResult(token=token, url=redact_url(url), status=getattr(resp, "status", None), raw=raw)
|
||||
except urllib.error.HTTPError as exc:
|
||||
body = exc.read().decode("utf-8", errors="replace") if exc.fp else ""
|
||||
raise AuthError(
|
||||
"登录接口返回错误",
|
||||
url=redact_url(url),
|
||||
status=exc.code,
|
||||
response=body,
|
||||
exception=exc,
|
||||
) from exc
|
||||
except AuthError:
|
||||
raise
|
||||
except Exception as exc: # noqa: BLE001 - auth errors must preserve detail for user feedback.
|
||||
raise AuthError(
|
||||
"登录接口调用异常",
|
||||
url=redact_url(url),
|
||||
response=str(exc),
|
||||
exception=exc,
|
||||
) from exc
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"api_base": "https://ch1.b.uat.4.cn",
|
||||
"taskId": "1",
|
||||
"actionId": "2",
|
||||
"mubiao": "索引映射验证",
|
||||
"direction": "U方向",
|
||||
"platform": "",
|
||||
"audienceRawText": "{\"dryRun\": true}",
|
||||
"audienceCandidates": [],
|
||||
"topAudiences": [],
|
||||
"pathwayCandidates": [],
|
||||
"topPathways": [],
|
||||
"audienceVulns": {},
|
||||
"pathwayVulns": {},
|
||||
"audienceStrategies": [],
|
||||
"pathwayStrategies": [],
|
||||
"errors": [],
|
||||
"events": [
|
||||
{
|
||||
"time": "2026-06-29T08:49:10.660975+00:00",
|
||||
"stage": "runner",
|
||||
"message": "start mode=step1 dry_run=True",
|
||||
"data": null
|
||||
},
|
||||
{
|
||||
"time": "2026-06-29T08:49:10.662968+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-29T08:49:10.668998+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-29T08:49:10.668998+00:00",
|
||||
"stage": "step1-audience",
|
||||
"message": "完成,保存受众 0 个",
|
||||
"data": null
|
||||
}
|
||||
],
|
||||
"startedAt": "2026-06-29T08:49:10.659972+00:00",
|
||||
"updatedAt": "2026-06-29T08:49:10.674982+00:00",
|
||||
"_statePath": "C:\\tmp\\xdfx-auth-index-check-state.json",
|
||||
"_summaryPath": "C:\\tmp\\xdfx-auth-index-check-summary.md",
|
||||
"_autoLogin": true,
|
||||
"_verifySsl": false,
|
||||
"_downstreamLimits": {
|
||||
"audiences": 2,
|
||||
"pathways": 2
|
||||
},
|
||||
"audienceAccounts": []
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# xdfx 分析入库执行摘要
|
||||
|
||||
- 更新时间:2026-06-29T08:49:10.674982+00:00
|
||||
- taskId:1
|
||||
- actionId:2
|
||||
- 受众入库:0
|
||||
- 传播节点入库:0
|
||||
- 下游受众选择:0
|
||||
- 下游传播节点选择:0
|
||||
- 受众脆弱点入库:0
|
||||
- 传播节点脆弱点入库:0
|
||||
- 受众策略入库:0
|
||||
- 传播节点策略入库:0
|
||||
- 错误数:0
|
||||
|
||||
## 受众
|
||||
|
||||
## 传播节点
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"api_base": "https://ch1.b.uat.4.cn",
|
||||
"taskId": "1",
|
||||
"actionId": "2",
|
||||
"mubiao": "日志中文化验证",
|
||||
"direction": "U方向",
|
||||
"platform": "",
|
||||
"audienceRawText": "{\"dryRun\": true}",
|
||||
"audienceCandidates": [],
|
||||
"topAudiences": [],
|
||||
"pathwayCandidates": [],
|
||||
"topPathways": [],
|
||||
"audienceVulns": {},
|
||||
"pathwayVulns": {},
|
||||
"audienceStrategies": [],
|
||||
"pathwayStrategies": [],
|
||||
"errors": [],
|
||||
"events": [
|
||||
{
|
||||
"time": "2026-06-29T07:16:23.252171+00:00",
|
||||
"stage": "runner",
|
||||
"message": "start mode=step1 dry_run=True",
|
||||
"data": null
|
||||
},
|
||||
{
|
||||
"time": "2026-06-29T07:16:23.252171+00:00",
|
||||
"stage": "runner/auto-login",
|
||||
"message": "dry-run 跳过自动登录",
|
||||
"data": {
|
||||
"username": "admin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"time": "2026-06-29T07:16:23.253180+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-29T07:16:23.261131+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。",
|
||||
"zhanghao": [],
|
||||
"history": [],
|
||||
"stream": true,
|
||||
"model_name": "OpenAI3",
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 20000,
|
||||
"prompt_name": "日志中文化验证"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"time": "2026-06-29T07:16:23.261131+00:00",
|
||||
"stage": "step1-audience",
|
||||
"message": "完成,保存受众 0 个",
|
||||
"data": null
|
||||
}
|
||||
],
|
||||
"startedAt": "2026-06-29T07:16:23.252171+00:00",
|
||||
"updatedAt": "2026-06-29T07:16:23.266115+00:00",
|
||||
"_statePath": "C:\\tmp\\xdfx-log-check-state.json",
|
||||
"_summaryPath": "C:\\tmp\\xdfx-log-check-summary.md",
|
||||
"_autoLogin": true,
|
||||
"_verifySsl": false,
|
||||
"_downstreamLimits": {
|
||||
"audiences": 2,
|
||||
"pathways": 2
|
||||
},
|
||||
"audienceAccounts": []
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# xdfx 分析入库执行摘要
|
||||
|
||||
- 更新时间:2026-06-29T07:16:23.266115+00:00
|
||||
- taskId:1
|
||||
- actionId:2
|
||||
- 受众入库:0
|
||||
- 传播节点入库:0
|
||||
- 下游受众选择:0
|
||||
- 下游传播节点选择:0
|
||||
- 受众脆弱点入库:0
|
||||
- 传播节点脆弱点入库:0
|
||||
- 受众策略入库:0
|
||||
- 传播节点策略入库:0
|
||||
- 错误数:0
|
||||
|
||||
## 受众
|
||||
|
||||
## 传播节点
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"api_base": "https://ch1.b.uat.4.cn",
|
||||
"taskId": "1",
|
||||
"actionId": "2",
|
||||
"mubiao": "dry-run-test",
|
||||
"direction": "U方向",
|
||||
"platform": "fb",
|
||||
"audienceRawText": "{\"dryRun\": true}",
|
||||
"audienceCandidates": [],
|
||||
"topAudiences": [],
|
||||
"pathwayCandidates": [],
|
||||
"topPathways": [],
|
||||
"audienceVulns": {},
|
||||
"pathwayVulns": {},
|
||||
"audienceStrategies": [],
|
||||
"pathwayStrategies": [],
|
||||
"errors": [],
|
||||
"events": [
|
||||
{
|
||||
"time": "2026-06-29T03:43:58.007253+00:00",
|
||||
"stage": "runner",
|
||||
"message": "start mode=step1 dry_run=True",
|
||||
"data": null
|
||||
},
|
||||
{
|
||||
"time": "2026-06-29T03:43:58.007253+00:00",
|
||||
"stage": "runner/auto-login",
|
||||
"message": "dry-run 跳过自动登录",
|
||||
"data": {
|
||||
"username": "admin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"time": "2026-06-29T03:43:58.007253+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": "任务背景和目的:dry-run-test"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"time": "2026-06-29T03:43:58.017225+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": "dry-run-test",
|
||||
"user_message": "分析其中关键信息。请基于上一轮受众分析结果,抽取受众目标群体,字段必须是:群体1、用户编号列表1、理由1、群体2、用户编号列表2、理由2 ...,最多输出5个,只返回JSON。",
|
||||
"zhanghao": [],
|
||||
"history": [],
|
||||
"stream": true,
|
||||
"model_name": "OpenAI3",
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 20000,
|
||||
"prompt_name": "dry-run-test"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"time": "2026-06-29T03:43:58.018223+00:00",
|
||||
"stage": "step1-audience",
|
||||
"message": "完成,保存受众 0 个",
|
||||
"data": null
|
||||
}
|
||||
],
|
||||
"startedAt": "2026-06-29T03:43:58.006257+00:00",
|
||||
"updatedAt": "2026-06-29T03:43:58.021216+00:00",
|
||||
"_statePath": "C:\\tmp\\xdfx-runner-dry-state.json",
|
||||
"_summaryPath": "C:\\tmp\\xdfx-runner-dry-summary.md",
|
||||
"_autoLogin": true,
|
||||
"_verifySsl": false,
|
||||
"_downstreamLimits": {
|
||||
"audiences": 2,
|
||||
"pathways": 2
|
||||
},
|
||||
"audienceAccounts": []
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# xdfx 分析入库执行摘要
|
||||
|
||||
- 更新时间:2026-06-29T03:43:58.021216+00:00
|
||||
- taskId:1
|
||||
- actionId:2
|
||||
- 受众入库:0
|
||||
- 传播节点入库:0
|
||||
- 下游受众选择:0
|
||||
- 下游传播节点选择:0
|
||||
- 受众脆弱点入库:0
|
||||
- 传播节点脆弱点入库:0
|
||||
- 受众策略入库:0
|
||||
- 传播节点策略入库:0
|
||||
- 错误数:0
|
||||
|
||||
## 受众
|
||||
|
||||
## 传播节点
|
||||
Reference in new issue
Block a user