LLM action 结果分析不再传 state_summary
调整了 agent.py 和 LLM client 协议/实现。 现在只传当前 action 的结构化结果和必要诊断日志,避免历史运行态影响判断。 提示词和文档也已同步说明。 verify-ip 增加健康检查重试 默认 VERIFY_INTERVAL_SEC=10、VERIFY_MAX_ATTEMPTS=12,约 2 分钟。 verify-ip 未通过但未达到最大次数时,会播报进度、保存 checkpoint,并继续从当前 verify-ip 重试,不会进入 download-log。 参数已加入 config.txt.example、脚本配置读取、README、打包 README、Skill 文档和流程图。
This commit is contained in:
parent
e572a26e6f
commit
4250a7b221
@ -91,7 +91,7 @@ packaging/
|
|||||||
- 支持通过 `--llm-action-analysis-prompt-file`、`PAM_LLM_ACTION_ANALYSIS_PROMPT_FILE` 或 chat 内 `llm config action_analysis_prompt_file=...` 自定义 action 审核提示词。
|
- 支持通过 `--llm-action-analysis-prompt-file`、`PAM_LLM_ACTION_ANALYSIS_PROMPT_FILE` 或 chat 内 `llm config action_analysis_prompt_file=...` 自定义 action 审核提示词。
|
||||||
- 增加统一运行日志,默认写入 `logs/pam_deploy_agent.log`,覆盖 CLI/chat、LLM 调用、action 路由、脚本/MCP 调用、LangGraph、checkpoint 等关键流程。
|
- 增加统一运行日志,默认写入 `logs/pam_deploy_agent.log`,覆盖 CLI/chat、LLM 调用、action 路由、脚本/MCP 调用、LangGraph、checkpoint 等关键流程。
|
||||||
- chat 支持 `llm test [文本]`,可用当前 LLM client 做一次轻量调用,确认真实 LLM 或规则 fallback 是否正常加载。
|
- chat 支持 `llm test [文本]`,可用当前 LLM client 做一次轻量调用,确认真实 LLM 或规则 fallback 是否正常加载。
|
||||||
- 添加基础测试,当前本地结果为 `66 passed, 3 skipped`。
|
- 添加基础测试,当前本地结果为 `67 passed, 3 skipped`。
|
||||||
|
|
||||||
未完成:
|
未完成:
|
||||||
|
|
||||||
@ -300,13 +300,15 @@ PAM> resume
|
|||||||
PAM> exit
|
PAM> exit
|
||||||
```
|
```
|
||||||
|
|
||||||
`chat` 默认仍要求在会话内显式输入 `run`,并确认参数、目标 IP 范围和最终执行后才会执行 action。输入 `你好`、`hello` 这类问候不会触发 LLM/结构化分析;需要分析部署需求时可直接描述部署任务,或显式使用 `analyze <需求>`。每个 action 完成后都会自动进入一次 LLM/规则审核,并播报审核开始/结束;只有审核通过才会把 action 记为 completed;如果审核建议停止或审核本身失败,流程会暂停并输出建议,等待用户决定是否 `resume` 重试当前 action。`poll-download-progress` 和 `poll-upgrade-progress` 每次只查询一次进度,workflow 会按 `POLL_INTERVAL_SEC`、`DOWNLOAD_POLL_MAX_ATTEMPTS`、`UPGRADE_POLL_MAX_ATTEMPTS` 重复调用,并在每次返回后让 LLM/规则判断是否完成、播报进度;未完成时不会跳到下一个 action。逐 IP action 失败时也会暂停,修复外部环境后输入 `resume` 会从当前 action 重试;如果确实需要回滚,使用 `rollback [IP]` 显式执行。`llm test [文本]` 可测试当前 LLM client 是否可用。`--analyze-actions` 仅控制详细审核结果是否写入 `events`。执行中可按 `Ctrl+C` 中断,chat 会保存当前 checkpoint 并把流程标记为 `user_interrupted`。`set KEY=VALUE` 和 `load params <路径>` 会把更新同步到当前运行 state、`config.txt` 和 checkpoint。`chat` 也支持 `--llm-base-url` / `--llm-api-key` / `--llm-model` / `--llm-action-analysis-prompt-file`、`--mcp-config` 和 `--analyze-actions`。
|
`chat` 默认仍要求在会话内显式输入 `run`,并确认参数、目标 IP 范围和最终执行后才会执行 action。输入 `你好`、`hello` 这类问候不会触发 LLM/结构化分析;需要分析部署需求时可直接描述部署任务,或显式使用 `analyze <需求>`。每个 action 完成后都会自动进入一次 LLM/规则审核,并播报审核开始/结束;审核输入只包含当前 action 的结构化结果和必要诊断日志,不会把完整运行态 `state_summary` 交给大模型,避免跨步骤状态干扰判断;只有审核通过才会把 action 记为 completed;如果审核建议停止或审核本身失败,流程会暂停并输出建议,等待用户决定是否 `resume` 重试当前 action。`poll-download-progress` 和 `poll-upgrade-progress` 每次只查询一次进度,workflow 会按 `POLL_INTERVAL_SEC`、`DOWNLOAD_POLL_MAX_ATTEMPTS`、`UPGRADE_POLL_MAX_ATTEMPTS` 重复调用,并在每次返回后让 LLM/规则判断是否完成、播报进度;未完成时不会跳到下一个 action。`verify-ip` 用于应用启动后的健康检查,失败时 workflow 会按 `VERIFY_INTERVAL_SEC` 重试,最多 `VERIFY_MAX_ATTEMPTS` 次;默认约每 10 秒一次、最多 12 次,仍未通过才暂停。逐 IP action 失败时也会暂停,修复外部环境后输入 `resume` 会从当前 action 重试;如果确实需要回滚,使用 `rollback [IP]` 显式执行。`llm test [文本]` 可测试当前 LLM client 是否可用。`--analyze-actions` 仅控制详细审核结果是否写入 `events`。执行中可按 `Ctrl+C` 中断,chat 会保存当前 checkpoint 并把流程标记为 `user_interrupted`。`set KEY=VALUE` 和 `load params <路径>` 会把更新同步到当前运行 state、`config.txt` 和 checkpoint。`chat` 也支持 `--llm-base-url` / `--llm-api-key` / `--llm-model` / `--llm-action-analysis-prompt-file`、`--mcp-config` 和 `--analyze-actions`。
|
||||||
|
|
||||||
进度查询相关参数:
|
重试和进度查询相关参数:
|
||||||
|
|
||||||
- `POLL_INTERVAL_SEC`:两次进度查询之间的等待秒数,默认 `2`。
|
- `POLL_INTERVAL_SEC`:两次进度查询之间的等待秒数,默认 `2`。
|
||||||
- `DOWNLOAD_POLL_MAX_ATTEMPTS`:云下载进度最大查询次数,默认 `60`。
|
- `DOWNLOAD_POLL_MAX_ATTEMPTS`:云下载进度最大查询次数,默认 `60`。
|
||||||
- `UPGRADE_POLL_MAX_ATTEMPTS`:单 IP 推送进度最大查询次数,默认 `600`。
|
- `UPGRADE_POLL_MAX_ATTEMPTS`:单 IP 推送进度最大查询次数,默认 `600`。
|
||||||
|
- `VERIFY_INTERVAL_SEC`:`verify-ip` 健康检查失败后的重试间隔秒数,默认 `10`。
|
||||||
|
- `VERIFY_MAX_ATTEMPTS`:`verify-ip` 健康检查最大尝试次数,默认 `12`。
|
||||||
|
|
||||||
## 日志
|
## 日志
|
||||||
|
|
||||||
|
|||||||
@ -71,6 +71,8 @@ description: 面向 PAM HOME/NODE 的智能部署 Skill。由 Skill 负责理解
|
|||||||
| `pollIntervalSec` | `POLL_INTERVAL_SEC` | 否 | 两次进度查询间隔,默认 `2` 秒 |
|
| `pollIntervalSec` | `POLL_INTERVAL_SEC` | 否 | 两次进度查询间隔,默认 `2` 秒 |
|
||||||
| `downloadPollMaxAttempts` | `DOWNLOAD_POLL_MAX_ATTEMPTS` | 否 | 云下载进度最大查询次数,默认 `60` |
|
| `downloadPollMaxAttempts` | `DOWNLOAD_POLL_MAX_ATTEMPTS` | 否 | 云下载进度最大查询次数,默认 `60` |
|
||||||
| `upgradePollMaxAttempts` | `UPGRADE_POLL_MAX_ATTEMPTS` | 否 | 单 IP 推送进度最大查询次数,默认 `600` |
|
| `upgradePollMaxAttempts` | `UPGRADE_POLL_MAX_ATTEMPTS` | 否 | 单 IP 推送进度最大查询次数,默认 `600` |
|
||||||
|
| `verifyIntervalSec` | `VERIFY_INTERVAL_SEC` | 否 | `verify-ip` 健康检查失败后的重试间隔,默认 `10` 秒 |
|
||||||
|
| `verifyMaxAttempts` | `VERIFY_MAX_ATTEMPTS` | 否 | `verify-ip` 健康检查最大尝试次数,默认 `12` |
|
||||||
|
|
||||||
### 3.2 运行控制参数
|
### 3.2 运行控制参数
|
||||||
|
|
||||||
@ -164,6 +166,8 @@ description: 面向 PAM HOME/NODE 的智能部署 Skill。由 Skill 负责理解
|
|||||||
- `POLL_INTERVAL_SEC`
|
- `POLL_INTERVAL_SEC`
|
||||||
- `DOWNLOAD_POLL_MAX_ATTEMPTS`
|
- `DOWNLOAD_POLL_MAX_ATTEMPTS`
|
||||||
- `UPGRADE_POLL_MAX_ATTEMPTS`
|
- `UPGRADE_POLL_MAX_ATTEMPTS`
|
||||||
|
- `VERIFY_INTERVAL_SEC`
|
||||||
|
- `VERIFY_MAX_ATTEMPTS`
|
||||||
- 命令行只传 action 级控制参数:
|
- 命令行只传 action 级控制参数:
|
||||||
- `--action` / `-Action`
|
- `--action` / `-Action`
|
||||||
- `--ip` / `-Ip`
|
- `--ip` / `-Ip`
|
||||||
@ -173,7 +177,7 @@ description: 面向 PAM HOME/NODE 的智能部署 Skill。由 Skill 负责理解
|
|||||||
- `client_secret` 等敏感字段不得通过命令行透传。
|
- `client_secret` 等敏感字段不得通过命令行透传。
|
||||||
- 如果用户明确要求“不落地配置文件”,则本 Skill 不执行真实部署,只说明限制和原因。
|
- 如果用户明确要求“不落地配置文件”,则本 Skill 不执行真实部署,只说明限制和原因。
|
||||||
- `traceFilePath` 不写入 `config.txt`,由 Agent 在运行时持有并应用。
|
- `traceFilePath` 不写入 `config.txt`,由 Agent 在运行时持有并应用。
|
||||||
- 进度查询间隔和最大次数写入 `config.txt`,由 Agent workflow 和脚本调试流程共同读取。
|
- 进度查询和健康检查重试参数写入 `config.txt`,由 Agent workflow 和脚本调试流程共同读取。
|
||||||
|
|
||||||
## 4. 主流程(硬约束)
|
## 4. 主流程(硬约束)
|
||||||
|
|
||||||
@ -204,7 +208,7 @@ description: 面向 PAM HOME/NODE 的智能部署 Skill。由 Skill 负责理解
|
|||||||
- `upgrade-ip`
|
- `upgrade-ip`
|
||||||
- 重复调用 `poll-upgrade-progress` 单次查询进度;每次返回后交给 LLM/规则判断,直到推送完成、失败或达到最大查询次数
|
- 重复调用 `poll-upgrade-progress` 单次查询进度;每次返回后交给 LLM/规则判断,直到推送完成、失败或达到最大查询次数
|
||||||
- `start-ip`
|
- `start-ip`
|
||||||
- `verify-ip`
|
- 重复调用 `verify-ip` 健康检查;`SUCCESS=false` 时按 `VERIFY_INTERVAL_SEC` 等待后重试,直到成功或达到 `VERIFY_MAX_ATTEMPTS`
|
||||||
- `download-log`
|
- `download-log`
|
||||||
17. 汇总每台 IP 的结果。
|
17. 汇总每台 IP 的结果。
|
||||||
18. 若 action 失败、LLM/规则审核要求停止,或出现 legacy `PENDING_AGENT_CONFIRMATION(...)`,暂停在当前 action 并输出建议。
|
18. 若 action 失败、LLM/规则审核要求停止,或出现 legacy `PENDING_AGENT_CONFIRMATION(...)`,暂停在当前 action 并输出建议。
|
||||||
@ -225,6 +229,7 @@ description: 面向 PAM HOME/NODE 的智能部署 Skill。由 Skill 负责理解
|
|||||||
6. 若某步骤失败后需要进入提示、确认或分支流程,可按 `failurePauseSec` 等待。
|
6. 若某步骤失败后需要进入提示、确认或分支流程,可按 `failurePauseSec` 等待。
|
||||||
7. 若某个间隔值为 `0`,表示该层级不等待,直接进入下一动作。
|
7. 若某个间隔值为 `0`,表示该层级不等待,直接进入下一动作。
|
||||||
8. `poll-download-progress` 和 `poll-upgrade-progress` 的脚本 action 只执行一次进度查询;正式 workflow 的循环、checkpoint、LLM 判断和进度播报由 Agent Runtime 负责。
|
8. `poll-download-progress` 和 `poll-upgrade-progress` 的脚本 action 只执行一次进度查询;正式 workflow 的循环、checkpoint、LLM 判断和进度播报由 Agent Runtime 负责。
|
||||||
|
9. `verify-ip` 失败但未达到 `VERIFY_MAX_ATTEMPTS` 时,不进入 `download-log`,也不把当前 action 记为 completed;正式 workflow 会播报健康检查进度、保存 checkpoint,并按 `VERIFY_INTERVAL_SEC` 重试当前 action。
|
||||||
|
|
||||||
### 4.2 主流程中的强制确认点
|
### 4.2 主流程中的强制确认点
|
||||||
|
|
||||||
@ -245,13 +250,14 @@ description: 面向 PAM HOME/NODE 的智能部署 Skill。由 Skill 负责理解
|
|||||||
4. 在每个全局步骤失败后,立即告知用户失败阶段、失败原因和后续处理。
|
4. 在每个全局步骤失败后,立即告知用户失败阶段、失败原因和后续处理。
|
||||||
5. 在逐台 IP 处理时,必须告知当前正在处理哪一台 IP。
|
5. 在逐台 IP 处理时,必须告知当前正在处理哪一台 IP。
|
||||||
6. 在云下载和单 IP 推送进度查询阶段,每次 `poll-*` 返回后都必须汇报当前进度,不能静默等待完成。
|
6. 在云下载和单 IP 推送进度查询阶段,每次 `poll-*` 返回后都必须汇报当前进度,不能静默等待完成。
|
||||||
7. 若执行耗时较长,必须按阶段持续播报,不能等全部结束后一次性汇总。
|
7. 在 `verify-ip` 健康检查阶段,每次未通过都必须播报当前检查次数、最大次数和返回信息,不能静默等待应用启动。
|
||||||
8. 若失败后建议回滚,必须明确告诉用户:
|
8. 若执行耗时较长,必须按阶段持续播报,不能等全部结束后一次性汇总。
|
||||||
|
9. 若失败后建议回滚,必须明确告诉用户:
|
||||||
- 哪一台 IP 失败
|
- 哪一台 IP 失败
|
||||||
- 失败阶段
|
- 失败阶段
|
||||||
- 建议是否回滚
|
- 建议是否回滚
|
||||||
- 是否需要 `stopFirst`
|
- 是否需要 `stopFirst`
|
||||||
9. 若当前处于 action 间隔等待中,也必须告诉用户等待时长和下一步动作。
|
10. 若当前处于 action 间隔等待中,也必须告诉用户等待时长和下一步动作。
|
||||||
|
|
||||||
建议的阶段播报格式:
|
建议的阶段播报格式:
|
||||||
|
|
||||||
@ -441,7 +447,7 @@ description: 面向 PAM HOME/NODE 的智能部署 Skill。由 Skill 负责理解
|
|||||||
| 16.1 | 创建单 IP 推送任务 | `upgrade-ip --ip ...` | 返回 `RESULT=TASK_CREATED` | 暂停在当前 action,修复后 `resume` 重试;需要回滚时显式执行 rollback |
|
| 16.1 | 创建单 IP 推送任务 | `upgrade-ip --ip ...` | 返回 `RESULT=TASK_CREATED` | 暂停在当前 action,修复后 `resume` 重试;需要回滚时显式执行 rollback |
|
||||||
| 16.2 | 查询单 IP 推送进度 | 重复调用单次 `poll-upgrade-progress --ip ...` | LLM/规则判断 `progress_complete=true`;或 `STEP=DONE` / `FINISH=true` / `MSG=success` 且 `RATE_OF_PROGRESS=100` | 暂停在当前 action,修复后 `resume` 重试;需要回滚时显式执行 rollback |
|
| 16.2 | 查询单 IP 推送进度 | 重复调用单次 `poll-upgrade-progress --ip ...` | LLM/规则判断 `progress_complete=true`;或 `STEP=DONE` / `FINISH=true` / `MSG=success` 且 `RATE_OF_PROGRESS=100` | 暂停在当前 action,修复后 `resume` 重试;需要回滚时显式执行 rollback |
|
||||||
| 16.3 | 启动单 IP | `start-ip --ip ...` | action 成功返回 | 暂停在当前 action,修复后 `resume` 重试;需要回滚时显式执行 rollback |
|
| 16.3 | 启动单 IP | `start-ip --ip ...` | action 成功返回 | 暂停在当前 action,修复后 `resume` 重试;需要回滚时显式执行 rollback |
|
||||||
| 16.4 | 校验单 IP | `verify-ip --ip ...` | 返回 `SUCCESS=true` | 暂停在当前 action,修复后 `resume` 重试;需要回滚时显式执行 rollback |
|
| 16.4 | 校验单 IP | 重复调用单次 `verify-ip --ip ...` | 返回 `SUCCESS=true` | 按 `VERIFY_INTERVAL_SEC` 重试,达到 `VERIFY_MAX_ATTEMPTS` 后仍失败才暂停在当前 action;需要回滚时显式执行 rollback |
|
||||||
| 16.5 | 下载日志 | `download-log --ip ...` | 返回 `LOG_FILE=...` | 记录日志下载失败,但不覆盖原主失败原因 |
|
| 16.5 | 下载日志 | `download-log --ip ...` | 返回 `LOG_FILE=...` | 记录日志下载失败,但不覆盖原主失败原因 |
|
||||||
| 17 | 汇总结果 | 汇总每台 IP 的阶段、失败原因、回滚状态、日志路径 | 报告内容完整 | 若汇总失败,至少保留原始 action 输出 |
|
| 17 | 汇总结果 | 汇总每台 IP 的阶段、失败原因、回滚状态、日志路径 | 报告内容完整 | 若汇总失败,至少保留原始 action 输出 |
|
||||||
| 18 | 失败暂停或显式回滚 | 失败后默认停在当前 action;用户输入 `rollback [IP]` 后才执行回滚 | 用户明确要求回滚或修复后 `resume` | 未显式要求回滚时不自动回滚 |
|
| 18 | 失败暂停或显式回滚 | 失败后默认停在当前 action;用户输入 `rollback [IP]` 后才执行回滚 | 用户明确要求回滚或修复后 `resume` | 未显式要求回滚时不自动回滚 |
|
||||||
|
|||||||
@ -12,3 +12,5 @@ LOG_NAME=app.log
|
|||||||
POLL_INTERVAL_SEC=2
|
POLL_INTERVAL_SEC=2
|
||||||
DOWNLOAD_POLL_MAX_ATTEMPTS=60
|
DOWNLOAD_POLL_MAX_ATTEMPTS=60
|
||||||
UPGRADE_POLL_MAX_ATTEMPTS=600
|
UPGRADE_POLL_MAX_ATTEMPTS=600
|
||||||
|
VERIFY_INTERVAL_SEC=10
|
||||||
|
VERIFY_MAX_ATTEMPTS=12
|
||||||
|
|||||||
@ -371,6 +371,8 @@ function Get-PamConfig {
|
|||||||
'POLL_INTERVAL_SEC' { $config[$key] = $value }
|
'POLL_INTERVAL_SEC' { $config[$key] = $value }
|
||||||
'DOWNLOAD_POLL_MAX_ATTEMPTS' { $config[$key] = $value }
|
'DOWNLOAD_POLL_MAX_ATTEMPTS' { $config[$key] = $value }
|
||||||
'UPGRADE_POLL_MAX_ATTEMPTS' { $config[$key] = $value }
|
'UPGRADE_POLL_MAX_ATTEMPTS' { $config[$key] = $value }
|
||||||
|
'VERIFY_INTERVAL_SEC' { $config[$key] = $value }
|
||||||
|
'VERIFY_MAX_ATTEMPTS' { $config[$key] = $value }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -392,6 +394,8 @@ function Get-PamConfig {
|
|||||||
POLL_INTERVAL_SEC = '2'
|
POLL_INTERVAL_SEC = '2'
|
||||||
DOWNLOAD_POLL_MAX_ATTEMPTS = '60'
|
DOWNLOAD_POLL_MAX_ATTEMPTS = '60'
|
||||||
UPGRADE_POLL_MAX_ATTEMPTS = '600'
|
UPGRADE_POLL_MAX_ATTEMPTS = '600'
|
||||||
|
VERIFY_INTERVAL_SEC = '10'
|
||||||
|
VERIFY_MAX_ATTEMPTS = '12'
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($name in $defaults.Keys) {
|
foreach ($name in $defaults.Keys) {
|
||||||
|
|||||||
@ -60,6 +60,8 @@ usage() {
|
|||||||
POLL_INTERVAL_SEC
|
POLL_INTERVAL_SEC
|
||||||
DOWNLOAD_POLL_MAX_ATTEMPTS
|
DOWNLOAD_POLL_MAX_ATTEMPTS
|
||||||
UPGRADE_POLL_MAX_ATTEMPTS
|
UPGRADE_POLL_MAX_ATTEMPTS
|
||||||
|
VERIFY_INTERVAL_SEC
|
||||||
|
VERIFY_MAX_ATTEMPTS
|
||||||
|
|
||||||
说明:
|
说明:
|
||||||
--action poll-download-progress 和 poll-upgrade-progress 只执行一次进度查询。
|
--action poll-download-progress 和 poll-upgrade-progress 只执行一次进度查询。
|
||||||
@ -352,6 +354,8 @@ set_defaults() {
|
|||||||
: "${POLL_INTERVAL_SEC:=2}"
|
: "${POLL_INTERVAL_SEC:=2}"
|
||||||
: "${DOWNLOAD_POLL_MAX_ATTEMPTS:=60}"
|
: "${DOWNLOAD_POLL_MAX_ATTEMPTS:=60}"
|
||||||
: "${UPGRADE_POLL_MAX_ATTEMPTS:=600}"
|
: "${UPGRADE_POLL_MAX_ATTEMPTS:=600}"
|
||||||
|
: "${VERIFY_INTERVAL_SEC:=10}"
|
||||||
|
: "${VERIFY_MAX_ATTEMPTS:=12}"
|
||||||
}
|
}
|
||||||
|
|
||||||
load_config() {
|
load_config() {
|
||||||
@ -376,7 +380,7 @@ load_config() {
|
|||||||
value="$(strip_inline_comment "$value")"
|
value="$(strip_inline_comment "$value")"
|
||||||
|
|
||||||
case "$key" in
|
case "$key" in
|
||||||
HOME_BASE_URL|CLIENT_ID|CLIENT_SECRET|AIRPORT_CODE|APP_NAME|MODULE_NAME|VERSION_NUMBER|ZIP_FILE_PATH|ACTION_TYPE|TIMEOUT|LOG_NAME|POLL_INTERVAL_SEC|DOWNLOAD_POLL_MAX_ATTEMPTS|UPGRADE_POLL_MAX_ATTEMPTS)
|
HOME_BASE_URL|CLIENT_ID|CLIENT_SECRET|AIRPORT_CODE|APP_NAME|MODULE_NAME|VERSION_NUMBER|ZIP_FILE_PATH|ACTION_TYPE|TIMEOUT|LOG_NAME|POLL_INTERVAL_SEC|DOWNLOAD_POLL_MAX_ATTEMPTS|UPGRADE_POLL_MAX_ATTEMPTS|VERIFY_INTERVAL_SEC|VERIFY_MAX_ATTEMPTS)
|
||||||
printf -v "$key" '%s' "$value"
|
printf -v "$key" '%s' "$value"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@ -93,7 +93,10 @@ flowchart TD
|
|||||||
K1 -- 已完成 --> L[ip_action 节点执行 start-ip]
|
K1 -- 已完成 --> L[ip_action 节点执行 start-ip]
|
||||||
K1 -- 异常或超时 --> R
|
K1 -- 异常或超时 --> R
|
||||||
L --> M[ip_action 节点执行 verify-ip]
|
L --> M[ip_action 节点执行 verify-ip]
|
||||||
M --> N[ip_action 节点执行 download-log]
|
M --> M1{健康检查通过或达到最大次数}
|
||||||
|
M1 -- 未通过且未超时 --> M
|
||||||
|
M1 -- 已通过 --> N[ip_action 节点执行 download-log]
|
||||||
|
M1 -- 仍未通过且超时 --> R
|
||||||
N --> O{还有下一个 IP}
|
N --> O{还有下一个 IP}
|
||||||
O -- 是 --> J
|
O -- 是 --> J
|
||||||
O -- 否 --> R[render_report 输出报告]
|
O -- 否 --> R[render_report 输出报告]
|
||||||
@ -115,8 +118,8 @@ flowchart LR
|
|||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
A[action 执行完成] --> C[整理 ActionResult 和 AgentState 摘要]
|
A[action 执行完成] --> C[整理当前 ActionResult]
|
||||||
C --> D[敏感字段脱敏并截断长日志]
|
C --> D[敏感字段脱敏;仅在异常时附带必要诊断日志]
|
||||||
D --> E{真实 LLM 是否配置}
|
D --> E{真实 LLM 是否配置}
|
||||||
E -- 是 --> F[OpenAICompatibleLlmClient 输出结构化审核]
|
E -- 是 --> F[OpenAICompatibleLlmClient 输出结构化审核]
|
||||||
E -- 否 --> G[RuleBasedLlmClient 本地规则审核]
|
E -- 否 --> G[RuleBasedLlmClient 本地规则审核]
|
||||||
@ -134,11 +137,32 @@ flowchart TD
|
|||||||
说明:
|
说明:
|
||||||
|
|
||||||
- 每个 action 完成后都会进入一次审核,不再依赖 `--analyze-actions` 开关。
|
- 每个 action 完成后都会进入一次审核,不再依赖 `--analyze-actions` 开关。
|
||||||
|
- 审核输入只包含当前 action 的结构化结果和必要诊断日志,不再传入完整运行态 `state_summary`,避免历史状态干扰大模型判断。
|
||||||
- `--analyze-actions` 或 `llm action-analysis on` 只控制是否把详细审核结果写入 `events`。
|
- `--analyze-actions` 或 `llm action-analysis on` 只控制是否把详细审核结果写入 `events`。
|
||||||
- 只有 action 执行成功且审核允许继续时,才会写入 `completed_global_steps` 或 `ip_states[ip].completed_steps`。
|
- 只有 action 执行成功且审核允许继续时,才会写入 `completed_global_steps` 或 `ip_states[ip].completed_steps`。
|
||||||
- 如果审核建议停止或审核本身失败,当前 action 不会计入 completed,`resume` 会重试当前 action。
|
- 如果审核建议停止或审核本身失败,当前 action 不会计入 completed,`resume` 会重试当前 action。
|
||||||
- 如果审核本身失败,也会生成“停止继续”的审核结果并暂停流程,避免黑盒继续执行。
|
- 如果审核本身失败,也会生成“停止继续”的审核结果并暂停流程,避免黑盒继续执行。
|
||||||
|
|
||||||
|
## verify-ip 健康检查重试
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[执行 verify-ip] --> B[LLM/规则审核单次返回]
|
||||||
|
B --> C{SUCCESS 是否为 true}
|
||||||
|
C -- 是 --> D[清理重试计数,标记 verify-ip completed]
|
||||||
|
C -- 否 --> E{是否达到 VERIFY_MAX_ATTEMPTS}
|
||||||
|
E -- 否 --> F[播报 ACTION_PROGRESS 并保存 checkpoint]
|
||||||
|
F --> G[等待 VERIFY_INTERVAL_SEC]
|
||||||
|
G --> A
|
||||||
|
E -- 是 --> H[暂停在 verify-ip,写入 review_context]
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- `verify-ip` 用于应用启动后的健康检查,失败时默认每 `10` 秒重试一次,最多 `12` 次,约两分钟。
|
||||||
|
- 重试参数来自 `VERIFY_INTERVAL_SEC` 和 `VERIFY_MAX_ATTEMPTS`,支持通过 `config.txt`、chat `set` 或 `load params` 热更新。
|
||||||
|
- 未达到最大次数时不会把 `verify-ip` 写入 completed,也不会进入 `download-log`;中断或失败后 `resume` 仍从 `verify-ip` 继续。
|
||||||
|
|
||||||
## 进度查询 action 语义
|
## 进度查询 action 语义
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
|
|||||||
@ -71,13 +71,15 @@ cd pam-deploy-agent-linux-x86_64
|
|||||||
本次发布包对应的运行时行为也已同步到包内 `README.md`:
|
本次发布包对应的运行时行为也已同步到包内 `README.md`:
|
||||||
|
|
||||||
- 每个 action 完成后都会自动执行一次 LLM/规则审核,只有审核通过才会把 action 记为 completed。
|
- 每个 action 完成后都会自动执行一次 LLM/规则审核,只有审核通过才会把 action 记为 completed。
|
||||||
|
- action 审核输入不包含完整运行态 `state_summary`,只包含当前 action 的结构化结果和必要诊断日志,避免历史状态干扰大模型判断。
|
||||||
- `poll-download-progress` 和 `poll-upgrade-progress` 是单次进度查询 action;Agent workflow 会按配置重复调用,每次返回后交给 LLM/规则判断是否完成并播报进度。
|
- `poll-download-progress` 和 `poll-upgrade-progress` 是单次进度查询 action;Agent workflow 会按配置重复调用,每次返回后交给 LLM/规则判断是否完成并播报进度。
|
||||||
|
- `verify-ip` 会按 `VERIFY_INTERVAL_SEC` / `VERIFY_MAX_ATTEMPTS` 做应用健康检查重试,默认每 10 秒一次、最多 12 次,仍未通过才暂停。
|
||||||
- `--analyze-actions` 只控制是否把详细审核结果写入 `events`。
|
- `--analyze-actions` 只控制是否把详细审核结果写入 `events`。
|
||||||
- action 失败或审核阻断后会保存 checkpoint 并暂停;修复外部环境后通过 `resume` 从当前 action 重试。
|
- action 失败或审核阻断后会保存 checkpoint 并暂停;修复外部环境后通过 `resume` 从当前 action 重试。
|
||||||
- 回滚不再属于主 workflow 自动分支;需要时使用 chat 内 `rollback [IP]` 或 CLI `rollback --checkpoint ...` 显式执行。
|
- 回滚不再属于主 workflow 自动分支;需要时使用 chat 内 `rollback [IP]` 或 CLI `rollback --checkpoint ...` 显式执行。
|
||||||
- chat 支持执行中 `Ctrl+C` 中断后保存 checkpoint,再通过 `resume` 重试当前 action。
|
- chat 支持执行中 `Ctrl+C` 中断后保存 checkpoint,再通过 `resume` 重试当前 action。
|
||||||
- chat 支持 `set KEY=VALUE` 和 `load params <路径>` 热更新当前运行任务参数。
|
- chat 支持 `set KEY=VALUE` 和 `load params <路径>` 热更新当前运行任务参数。
|
||||||
- 进度查询间隔和最大次数可通过 `POLL_INTERVAL_SEC`、`DOWNLOAD_POLL_MAX_ATTEMPTS`、`UPGRADE_POLL_MAX_ATTEMPTS` 配置。
|
- 进度查询和健康检查重试参数可通过 `POLL_INTERVAL_SEC`、`DOWNLOAD_POLL_MAX_ATTEMPTS`、`UPGRADE_POLL_MAX_ATTEMPTS`、`VERIFY_INTERVAL_SEC`、`VERIFY_MAX_ATTEMPTS` 配置。
|
||||||
- 支持通过 `--llm-action-analysis-prompt-file` 或 chat 内 `llm config action_analysis_prompt_file=...` 自定义 action 审核提示词。
|
- 支持通过 `--llm-action-analysis-prompt-file` 或 chat 内 `llm config action_analysis_prompt_file=...` 自定义 action 审核提示词。
|
||||||
- chat 支持 `llm test [文本]` 测试当前 LLM client 是否正常加载。
|
- chat 支持 `llm test [文本]` 测试当前 LLM client 是否正常加载。
|
||||||
- 默认运行日志写入 `logs/pam_deploy_agent.log`,可通过 `PAM_AGENT_LOG_FILE` 和 `PAM_AGENT_LOG_LEVEL` 调整。
|
- 默认运行日志写入 `logs/pam_deploy_agent.log`,可通过 `PAM_AGENT_LOG_FILE` 和 `PAM_AGENT_LOG_LEVEL` 调整。
|
||||||
|
|||||||
@ -36,8 +36,8 @@ pam-deploy-agent-linux-x86_64/
|
|||||||
|
|
||||||
发布包默认会优先使用 `prompt_toolkit` 增强输入,支持更稳定的退格、历史记录和补全;如果增强输入初始化失败,会自动降级到普通 `input()`。输出仍会在可用时使用 `rich` 做更清晰的文本展示。
|
发布包默认会优先使用 `prompt_toolkit` 增强输入,支持更稳定的退格、历史记录和补全;如果增强输入初始化失败,会自动降级到普通 `input()`。输出仍会在可用时使用 `rich` 做更清晰的文本展示。
|
||||||
action 失败或审核阻断后会保存 checkpoint 并暂停;修复外部环境后输入 `resume` 会从当前 action 重试。回滚不再属于主 workflow 自动分支,需要时在 chat 内输入 `rollback [IP]` 显式执行。
|
action 失败或审核阻断后会保存 checkpoint 并暂停;修复外部环境后输入 `resume` 会从当前 action 重试。回滚不再属于主 workflow 自动分支,需要时在 chat 内输入 `rollback [IP]` 显式执行。
|
||||||
chat 会在执行前归一化并展示实际写入脚本配置的参数;`script_only` / `hybrid_node_mcp` 会先检查 `ZIP_FILE_PATH` 是否存在,避免脚本运行后才用默认路径失败。执行过程中每个 action 都会输出开始、完成或失败状态;每个 action 完成后还会自动进入一次 LLM/规则审核,并播报审核开始和审核结果;只有审核通过才会把 action 记为 completed。
|
chat 会在执行前归一化并展示实际写入脚本配置的参数;`script_only` / `hybrid_node_mcp` 会先检查 `ZIP_FILE_PATH` 是否存在,避免脚本运行后才用默认路径失败。执行过程中每个 action 都会输出开始、完成或失败状态;每个 action 完成后还会自动进入一次 LLM/规则审核,并播报审核开始和审核结果;审核输入只包含当前 action 的结构化结果和必要诊断日志,不会把完整运行态 `state_summary` 交给大模型;只有审核通过才会把 action 记为 completed。
|
||||||
`poll-download-progress` 和 `poll-upgrade-progress` 每次只查询一次进度,Agent workflow 会按 `POLL_INTERVAL_SEC`、`DOWNLOAD_POLL_MAX_ATTEMPTS`、`UPGRADE_POLL_MAX_ATTEMPTS` 重复调用,并在每次返回后交给 LLM/规则判断是否完成、向 chat 播报进度。
|
`poll-download-progress` 和 `poll-upgrade-progress` 每次只查询一次进度,Agent workflow 会按 `POLL_INTERVAL_SEC`、`DOWNLOAD_POLL_MAX_ATTEMPTS`、`UPGRADE_POLL_MAX_ATTEMPTS` 重复调用,并在每次返回后交给 LLM/规则判断是否完成、向 chat 播报进度。`verify-ip` 健康检查失败时,Agent workflow 会按 `VERIFY_INTERVAL_SEC` 重试,最多 `VERIFY_MAX_ATTEMPTS` 次;默认每 10 秒一次、最多 12 次,仍未通过才暂停。
|
||||||
|
|
||||||
## 交互式使用
|
## 交互式使用
|
||||||
|
|
||||||
@ -244,7 +244,9 @@ MCP token 获取方式与 HOME 一致,默认按 `client_credentials` POST 到
|
|||||||
- 执行真实 action 前请确认配置文件中的 `HOME_BASE_URL`、`CLIENT_ID`、`CLIENT_SECRET`、`AIRPORT_CODE`、`APP_NAME`、`MODULE_NAME`、`VERSION_NUMBER`、`ZIP_FILE_PATH`。
|
- 执行真实 action 前请确认配置文件中的 `HOME_BASE_URL`、`CLIENT_ID`、`CLIENT_SECRET`、`AIRPORT_CODE`、`APP_NAME`、`MODULE_NAME`、`VERSION_NUMBER`、`ZIP_FILE_PATH`。
|
||||||
- `chat` 中输入 `你好`、`hello` 这类问候不会触发 LLM/结构化分析;需要分析部署需求时请直接描述部署任务,或显式使用 `analyze <需求>`。
|
- `chat` 中输入 `你好`、`hello` 这类问候不会触发 LLM/结构化分析;需要分析部署需求时请直接描述部署任务,或显式使用 `analyze <需求>`。
|
||||||
- 每个 action 完成后都会自动执行一次 LLM/规则审核;`--analyze-actions` 和 `llm action-analysis on` 只控制是否把详细审核结果写入 `events`。
|
- 每个 action 完成后都会自动执行一次 LLM/规则审核;`--analyze-actions` 和 `llm action-analysis on` 只控制是否把详细审核结果写入 `events`。
|
||||||
|
- action 审核输入不包含完整运行态 `state_summary`,只包含当前 action 的结构化结果和必要诊断日志。
|
||||||
- `poll-download-progress` 和 `poll-upgrade-progress` 是单次进度查询 action,未完成时不会进入下一个 action;最大查询次数和间隔可通过 `config.txt` 或 chat `set` 热更新。
|
- `poll-download-progress` 和 `poll-upgrade-progress` 是单次进度查询 action,未完成时不会进入下一个 action;最大查询次数和间隔可通过 `config.txt` 或 chat `set` 热更新。
|
||||||
|
- `verify-ip` 会按 `VERIFY_INTERVAL_SEC` / `VERIFY_MAX_ATTEMPTS` 做健康检查重试,默认每 10 秒一次、最多 12 次。
|
||||||
- `llm test [文本]` 可测试当前 LLM client 是否可用。
|
- `llm test [文本]` 可测试当前 LLM client 是否可用。
|
||||||
- 如果审核建议停止、审核本身失败,或用户在执行中按下 `Ctrl+C`,流程都会保存 checkpoint 并进入暂停状态;后续可使用 `resume` 重试当前 action。
|
- 如果审核建议停止、审核本身失败,或用户在执行中按下 `Ctrl+C`,流程都会保存 checkpoint 并进入暂停状态;后续可使用 `resume` 重试当前 action。
|
||||||
- `set KEY=VALUE` 和 `load params <路径>` 会热更新当前运行任务的参数,并回写运行中的 `config.txt` 和 checkpoint。
|
- `set KEY=VALUE` 和 `load params <路径>` 会热更新当前运行任务的参数,并回写运行中的 `config.txt` 和 checkpoint。
|
||||||
|
|||||||
@ -33,6 +33,7 @@ REQUIRED_ACTION_VALUES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PROGRESS_ACTIONS = {"poll-download-progress", "poll-upgrade-progress"}
|
PROGRESS_ACTIONS = {"poll-download-progress", "poll-upgrade-progress"}
|
||||||
|
VERIFY_ACTION = "verify-ip"
|
||||||
|
|
||||||
|
|
||||||
class PamDeployAgent:
|
class PamDeployAgent:
|
||||||
@ -557,6 +558,10 @@ class PamDeployAgent:
|
|||||||
)
|
)
|
||||||
analysis = self._append_action_analysis(state, action, result, ip=ip)
|
analysis = self._append_action_analysis(state, action, result, ip=ip)
|
||||||
|
|
||||||
|
if self._handle_verify_retry(state, ip, action, result, analysis, failed):
|
||||||
|
ip_state["status"] = "RUNNING"
|
||||||
|
return state
|
||||||
|
|
||||||
if failed:
|
if failed:
|
||||||
fail_event = {
|
fail_event = {
|
||||||
"type": "ACTION_FAIL",
|
"type": "ACTION_FAIL",
|
||||||
@ -1022,6 +1027,94 @@ class PamDeployAgent:
|
|||||||
)
|
)
|
||||||
return max(max_attempts, 1), max(interval_sec, 0.0)
|
return max(max_attempts, 1), max(interval_sec, 0.0)
|
||||||
|
|
||||||
|
def _handle_verify_retry(
|
||||||
|
self,
|
||||||
|
state: AgentState,
|
||||||
|
ip: str,
|
||||||
|
action: str,
|
||||||
|
result: ActionResult,
|
||||||
|
analysis: LlmActionAnalysis | None,
|
||||||
|
failed: bool,
|
||||||
|
) -> bool:
|
||||||
|
"""处理 verify-ip 的应用启动等待;未通过但未超时则保留当前 action 重试。"""
|
||||||
|
if action != VERIFY_ACTION:
|
||||||
|
return False
|
||||||
|
|
||||||
|
key = self._poll_attempt_key(action, ip=ip)
|
||||||
|
if not failed:
|
||||||
|
state.poll_attempts.pop(key, None)
|
||||||
|
return False
|
||||||
|
|
||||||
|
max_attempts, interval_sec = self._verify_limits(state)
|
||||||
|
attempt = state.poll_attempts.get(key, 0) + 1
|
||||||
|
state.poll_attempts[key] = attempt
|
||||||
|
message = self._verify_message(result, ip=ip, attempt=attempt, max_attempts=max_attempts)
|
||||||
|
progress_event = {
|
||||||
|
"type": "ACTION_PROGRESS",
|
||||||
|
"stage": action,
|
||||||
|
"backend": result.backend,
|
||||||
|
"ip": ip,
|
||||||
|
"message": message,
|
||||||
|
"attempt": attempt,
|
||||||
|
"max_attempts": max_attempts,
|
||||||
|
"values": dict(result.values),
|
||||||
|
}
|
||||||
|
state.events.append(progress_event)
|
||||||
|
self._emit_progress(progress_event)
|
||||||
|
|
||||||
|
if attempt >= max_attempts:
|
||||||
|
logger.warning(
|
||||||
|
"verify-ip 达到最大检查次数 run_id=%s ip=%s attempt=%s max=%s result=%s analysis=%s",
|
||||||
|
state.run_id,
|
||||||
|
ip,
|
||||||
|
attempt,
|
||||||
|
max_attempts,
|
||||||
|
_action_result_for_log(result),
|
||||||
|
json_for_log(asdict(analysis)) if analysis else "",
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._save_checkpoint(state)
|
||||||
|
logger.info(
|
||||||
|
"verify-ip 未通过,等待后重试 run_id=%s ip=%s attempt=%s max=%s interval=%s message=%s",
|
||||||
|
state.run_id,
|
||||||
|
ip,
|
||||||
|
attempt,
|
||||||
|
max_attempts,
|
||||||
|
interval_sec,
|
||||||
|
message,
|
||||||
|
)
|
||||||
|
if interval_sec > 0:
|
||||||
|
time.sleep(interval_sec)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _verify_limits(self, state: AgentState) -> tuple[int, float]:
|
||||||
|
"""从运行参数读取 verify-ip 健康检查最大次数和间隔。"""
|
||||||
|
interval_sec = _safe_float(state.params.get("VERIFY_INTERVAL_SEC"), float(DEFAULT_PARAMS["VERIFY_INTERVAL_SEC"]))
|
||||||
|
max_attempts = _safe_int(
|
||||||
|
state.params.get("VERIFY_MAX_ATTEMPTS"),
|
||||||
|
int(DEFAULT_PARAMS["VERIFY_MAX_ATTEMPTS"]),
|
||||||
|
)
|
||||||
|
return max(max_attempts, 1), max(interval_sec, 0.0)
|
||||||
|
|
||||||
|
def _verify_message(
|
||||||
|
self,
|
||||||
|
result: ActionResult,
|
||||||
|
*,
|
||||||
|
ip: str,
|
||||||
|
attempt: int,
|
||||||
|
max_attempts: int,
|
||||||
|
) -> str:
|
||||||
|
"""格式化 verify-ip 重试播报。"""
|
||||||
|
parts = [f"IP={ip}", f"第 {attempt}/{max_attempts} 次健康检查"]
|
||||||
|
for key in ("SUCCESS", "MESSAGE", "STATUS", "CODE"):
|
||||||
|
value = result.values.get(key)
|
||||||
|
if value not in (None, ""):
|
||||||
|
parts.append(f"{key}={value}")
|
||||||
|
if result.error_summary:
|
||||||
|
parts.append(f"error_summary={result.error_summary}")
|
||||||
|
return ",".join(parts)
|
||||||
|
|
||||||
def _progress_complete(
|
def _progress_complete(
|
||||||
self,
|
self,
|
||||||
action: str,
|
action: str,
|
||||||
@ -1145,7 +1238,6 @@ class PamDeployAgent:
|
|||||||
analysis = self.llm_client.analyze_action_result(
|
analysis = self.llm_client.analyze_action_result(
|
||||||
action=action,
|
action=action,
|
||||||
result=result,
|
result=result,
|
||||||
state_summary=self._state_summary_for_llm(state, ip=ip),
|
|
||||||
)
|
)
|
||||||
except Exception as exc: # pragma: no cover - 审核失败时也要显式暂停,避免黑盒继续执行
|
except Exception as exc: # pragma: no cover - 审核失败时也要显式暂停,避免黑盒继续执行
|
||||||
logger.exception("LLM action 审核失败 run_id=%s action=%s ip=%s", state.run_id, action, ip or "")
|
logger.exception("LLM action 审核失败 run_id=%s action=%s ip=%s", state.run_id, action, ip or "")
|
||||||
@ -1202,24 +1294,6 @@ class PamDeployAgent:
|
|||||||
)
|
)
|
||||||
return analysis
|
return analysis
|
||||||
|
|
||||||
def _state_summary_for_llm(self, state: AgentState, *, ip: str | None = None) -> dict[str, Any]:
|
|
||||||
"""生成给 LLM action 分析使用的脱敏状态摘要。"""
|
|
||||||
return {
|
|
||||||
"run_id": state.run_id,
|
|
||||||
"execution_strategy": state.execution_strategy,
|
|
||||||
"completed_global_steps": state.completed_global_steps,
|
|
||||||
"online_ip_count": len(state.online_ips),
|
|
||||||
"target_ips": state.target_ips,
|
|
||||||
"current_ip": ip or "",
|
|
||||||
"current_ip_state": state.ip_states.get(ip, {}) if ip else {},
|
|
||||||
"pending_confirmation": state.pending_confirmation,
|
|
||||||
"paused": state.paused,
|
|
||||||
"pause_reason": state.pause_reason,
|
|
||||||
"last_success_step": state.last_success_step,
|
|
||||||
"last_failed_step": state.last_failed_step,
|
|
||||||
"poll_attempts": state.poll_attempts,
|
|
||||||
}
|
|
||||||
|
|
||||||
def _review_context(
|
def _review_context(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@ -20,6 +20,8 @@ CONFIG_KEYS = (
|
|||||||
"POLL_INTERVAL_SEC",
|
"POLL_INTERVAL_SEC",
|
||||||
"DOWNLOAD_POLL_MAX_ATTEMPTS",
|
"DOWNLOAD_POLL_MAX_ATTEMPTS",
|
||||||
"UPGRADE_POLL_MAX_ATTEMPTS",
|
"UPGRADE_POLL_MAX_ATTEMPTS",
|
||||||
|
"VERIFY_INTERVAL_SEC",
|
||||||
|
"VERIFY_MAX_ATTEMPTS",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -67,6 +67,8 @@ DEFAULT_PARAMS = {
|
|||||||
"POLL_INTERVAL_SEC": 2,
|
"POLL_INTERVAL_SEC": 2,
|
||||||
"DOWNLOAD_POLL_MAX_ATTEMPTS": 60,
|
"DOWNLOAD_POLL_MAX_ATTEMPTS": 60,
|
||||||
"UPGRADE_POLL_MAX_ATTEMPTS": 600,
|
"UPGRADE_POLL_MAX_ATTEMPTS": 600,
|
||||||
|
"VERIFY_INTERVAL_SEC": 10,
|
||||||
|
"VERIFY_MAX_ATTEMPTS": 12,
|
||||||
}
|
}
|
||||||
|
|
||||||
# 日志、报告和 LLM 输入中需要脱敏的字段。
|
# 日志、报告和 LLM 输入中需要脱敏的字段。
|
||||||
|
|||||||
@ -40,7 +40,6 @@ class LlmClient(Protocol):
|
|||||||
*,
|
*,
|
||||||
action: str,
|
action: str,
|
||||||
result: ActionResult,
|
result: ActionResult,
|
||||||
state_summary: dict[str, Any],
|
|
||||||
) -> LlmActionAnalysis:
|
) -> LlmActionAnalysis:
|
||||||
"""分析 action 执行结果,并给出是否允许继续执行的建议。"""
|
"""分析 action 执行结果,并给出是否允许继续执行的建议。"""
|
||||||
...
|
...
|
||||||
|
|||||||
@ -150,7 +150,6 @@ class OpenAICompatibleLlmClient:
|
|||||||
*,
|
*,
|
||||||
action: str,
|
action: str,
|
||||||
result: ActionResult,
|
result: ActionResult,
|
||||||
state_summary: dict[str, Any],
|
|
||||||
) -> LlmActionAnalysis:
|
) -> LlmActionAnalysis:
|
||||||
"""调用 LLM 分析 action 结果,返回结构化诊断建议。"""
|
"""调用 LLM 分析 action 结果,返回结构化诊断建议。"""
|
||||||
payload = self._complete_json(
|
payload = self._complete_json(
|
||||||
@ -159,7 +158,6 @@ class OpenAICompatibleLlmClient:
|
|||||||
{
|
{
|
||||||
"action": action,
|
"action": action,
|
||||||
"result": _action_review_result_payload(action, result),
|
"result": _action_review_result_payload(action, result),
|
||||||
"state_summary": _redact_sensitive(state_summary),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return LlmActionAnalysis(
|
return LlmActionAnalysis(
|
||||||
|
|||||||
@ -41,7 +41,9 @@ PARAM_PROMPT = """从用户输入中抽取 PAM 部署参数和控制信息。
|
|||||||
"LOG_NAME": "...",
|
"LOG_NAME": "...",
|
||||||
"POLL_INTERVAL_SEC": "...",
|
"POLL_INTERVAL_SEC": "...",
|
||||||
"DOWNLOAD_POLL_MAX_ATTEMPTS": "...",
|
"DOWNLOAD_POLL_MAX_ATTEMPTS": "...",
|
||||||
"UPGRADE_POLL_MAX_ATTEMPTS": "..."
|
"UPGRADE_POLL_MAX_ATTEMPTS": "...",
|
||||||
|
"VERIFY_INTERVAL_SEC": "...",
|
||||||
|
"VERIFY_MAX_ATTEMPTS": "..."
|
||||||
},
|
},
|
||||||
"extracted_control": {
|
"extracted_control": {
|
||||||
"user_specified_ips": ["..."]
|
"user_specified_ips": ["..."]
|
||||||
@ -91,7 +93,9 @@ ACTION_ANALYSIS_PROMPT = """分析一次 PAM action 执行结果。
|
|||||||
- 进度 action 未完成但正常时,`has_anomaly=false`、`should_continue=true`、`progress_complete=false`,建议继续查询进度。
|
- 进度 action 未完成但正常时,`has_anomaly=false`、`should_continue=true`、`progress_complete=false`,建议继续查询进度。
|
||||||
- 进度 action 完成条件优先看 `STEP=DONE`、`STATUS=completed/done/success`、`SUCCESS=true`、`FINISH=true`,或 `MSG=success` 且 `RATE_OF_PROGRESS=100` 且 `CODE` 为空或 0。
|
- 进度 action 完成条件优先看 `STEP=DONE`、`STATUS=completed/done/success`、`SUCCESS=true`、`FINISH=true`,或 `MSG=success` 且 `RATE_OF_PROGRESS=100` 且 `CODE` 为空或 0。
|
||||||
- 进度 action 出现 `CODE` 非 0,或 `STEP/MSG/STATUS/MESSAGE` 含 fail/error,应标记异常并 `should_continue=false`。
|
- 进度 action 出现 `CODE` 非 0,或 `STEP/MSG/STATUS/MESSAGE` 含 fail/error,应标记异常并 `should_continue=false`。
|
||||||
- 主要依据结构化字段 `ok`、`exit_code`、`values`、`error_summary` 判断;只有输入里存在 `diagnostic_log` 时,才把它当作异常诊断上下文。
|
- 主要依据结构化字段 `ok`、`exit_code`、`values`、`error_summary` 判断;不会提供完整运行态摘要,避免被历史状态误导。
|
||||||
|
- `verify-ip SUCCESS=false` 由 runtime 按配置重复检查;单次审核仍应说明当前健康检查未通过。
|
||||||
|
- 只有输入里存在 `diagnostic_log` 时,才把它当作异常诊断上下文。
|
||||||
- 脚本正常过程日志不会作为错误依据,不能因为日志来自 stderr 就判定异常。
|
- 脚本正常过程日志不会作为错误依据,不能因为日志来自 stderr 就判定异常。
|
||||||
- 不要输出密钥、token、Authorization 或完整日志原文。
|
- 不要输出密钥、token、Authorization 或完整日志原文。
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -166,11 +166,10 @@ class RuleBasedLlmClient:
|
|||||||
*,
|
*,
|
||||||
action: str,
|
action: str,
|
||||||
result: ActionResult,
|
result: ActionResult,
|
||||||
state_summary: dict[str, Any],
|
|
||||||
) -> LlmActionAnalysis:
|
) -> LlmActionAnalysis:
|
||||||
"""用本地规则分析 action 结果,作为真实 LLM 不可用时的兜底。"""
|
"""用本地规则分析 action 结果,作为真实 LLM 不可用时的兜底。"""
|
||||||
logger.info(
|
logger.info(
|
||||||
"规则 LLM action 审核开始 action=%s result=%s state_summary=%s",
|
"规则 LLM action 审核开始 action=%s result=%s",
|
||||||
action,
|
action,
|
||||||
json_for_log(
|
json_for_log(
|
||||||
{
|
{
|
||||||
@ -183,7 +182,6 @@ class RuleBasedLlmClient:
|
|||||||
},
|
},
|
||||||
max_text_len=1000,
|
max_text_len=1000,
|
||||||
),
|
),
|
||||||
json_for_log(state_summary),
|
|
||||||
)
|
)
|
||||||
notes: list[str] = []
|
notes: list[str] = []
|
||||||
has_anomaly = not result.ok
|
has_anomaly = not result.ok
|
||||||
|
|||||||
@ -20,6 +20,8 @@
|
|||||||
- 进度 action 未完成但正常时,`has_anomaly=false`、`should_continue=true`、`progress_complete=false`,建议继续查询进度。
|
- 进度 action 未完成但正常时,`has_anomaly=false`、`should_continue=true`、`progress_complete=false`,建议继续查询进度。
|
||||||
- 进度 action 完成条件优先看 `STEP=DONE`、`STATUS=completed/done/success`、`SUCCESS=true`、`FINISH=true`,或 `MSG=success` 且 `RATE_OF_PROGRESS=100` 且 `CODE` 为空或 0。
|
- 进度 action 完成条件优先看 `STEP=DONE`、`STATUS=completed/done/success`、`SUCCESS=true`、`FINISH=true`,或 `MSG=success` 且 `RATE_OF_PROGRESS=100` 且 `CODE` 为空或 0。
|
||||||
- 进度 action 出现 `CODE` 非 0,或 `STEP/MSG/STATUS/MESSAGE` 含 fail/error,应标记异常并 `should_continue=false`。
|
- 进度 action 出现 `CODE` 非 0,或 `STEP/MSG/STATUS/MESSAGE` 含 fail/error,应标记异常并 `should_continue=false`。
|
||||||
- 主要依据结构化字段 `ok`、`exit_code`、`values`、`error_summary` 判断;只有输入里存在 `diagnostic_log` 时,才把它当作异常诊断上下文。
|
- 主要依据结构化字段 `ok`、`exit_code`、`values`、`error_summary` 判断;不会提供完整运行态摘要,避免被历史状态误导。
|
||||||
|
- `verify-ip SUCCESS=false` 由 runtime 按配置重复检查;单次审核仍应说明当前健康检查未通过。
|
||||||
|
- 只有输入里存在 `diagnostic_log` 时,才把它当作异常诊断上下文。
|
||||||
- 脚本正常过程日志不会作为错误依据,不能因为日志来自 stderr 就判定异常。
|
- 脚本正常过程日志不会作为错误依据,不能因为日志来自 stderr 就判定异常。
|
||||||
- 不要输出密钥、token、Authorization 或完整日志原文。
|
- 不要输出密钥、token、Authorization 或完整日志原文。
|
||||||
|
|||||||
@ -18,11 +18,13 @@ PARAMS = {
|
|||||||
"MODULE_NAME": "Node",
|
"MODULE_NAME": "Node",
|
||||||
"VERSION_NUMBER": "2.0.5",
|
"VERSION_NUMBER": "2.0.5",
|
||||||
"ZIP_FILE_PATH": "C:/pkg.zip",
|
"ZIP_FILE_PATH": "C:/pkg.zip",
|
||||||
|
"VERIFY_INTERVAL_SEC": 0,
|
||||||
|
"VERIFY_MAX_ATTEMPTS": 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class BlockingReviewLlmClient:
|
class BlockingReviewLlmClient:
|
||||||
def analyze_action_result(self, *, action, result, state_summary):
|
def analyze_action_result(self, *, action, result):
|
||||||
return LlmActionAnalysis(
|
return LlmActionAnalysis(
|
||||||
action=action,
|
action=action,
|
||||||
has_anomaly=True,
|
has_anomaly=True,
|
||||||
@ -40,7 +42,7 @@ class BlockingOnceReviewLlmClient:
|
|||||||
self.blocked_action = blocked_action
|
self.blocked_action = blocked_action
|
||||||
self.blocked = False
|
self.blocked = False
|
||||||
|
|
||||||
def analyze_action_result(self, *, action, result, state_summary):
|
def analyze_action_result(self, *, action, result):
|
||||||
if action == self.blocked_action and not self.blocked:
|
if action == self.blocked_action and not self.blocked:
|
||||||
self.blocked = True
|
self.blocked = True
|
||||||
return LlmActionAnalysis(
|
return LlmActionAnalysis(
|
||||||
@ -56,7 +58,7 @@ class BlockingOnceReviewLlmClient:
|
|||||||
|
|
||||||
|
|
||||||
class BrokenReviewLlmClient:
|
class BrokenReviewLlmClient:
|
||||||
def analyze_action_result(self, *, action, result, state_summary):
|
def analyze_action_result(self, *, action, result):
|
||||||
raise RuntimeError("review transport failed")
|
raise RuntimeError("review transport failed")
|
||||||
|
|
||||||
|
|
||||||
@ -93,6 +95,26 @@ class ProgressivePollRunner(FakeActionRunner):
|
|||||||
return super()._fixture_for(action, kwargs)
|
return super()._fixture_for(action, kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class FlakyVerifyRunner(FakeActionRunner):
|
||||||
|
"""模拟应用启动后第二次健康检查通过。"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.verify_calls = 0
|
||||||
|
|
||||||
|
def _fixture_for(self, action, kwargs):
|
||||||
|
if action == "verify-ip" and kwargs.get("ip") == "192.168.1.10":
|
||||||
|
self.verify_calls += 1
|
||||||
|
if self.verify_calls == 1:
|
||||||
|
return {
|
||||||
|
"ACTION": action,
|
||||||
|
"IP": "192.168.1.10",
|
||||||
|
"SUCCESS": "false",
|
||||||
|
"MESSAGE": "application is starting",
|
||||||
|
}
|
||||||
|
return super()._fixture_for(action, kwargs)
|
||||||
|
|
||||||
|
|
||||||
def test_run_deploy_flow_success(tmp_path: Path):
|
def test_run_deploy_flow_success(tmp_path: Path):
|
||||||
agent = PamDeployAgent(fake_runner=FakeActionRunner())
|
agent = PamDeployAgent(fake_runner=FakeActionRunner())
|
||||||
state = agent.create_state(
|
state = agent.create_state(
|
||||||
@ -161,6 +183,30 @@ def test_progress_timeout_pauses_on_current_action(tmp_path: Path):
|
|||||||
assert state.poll_attempts["global:poll-download-progress"] == 2
|
assert state.poll_attempts["global:poll-download-progress"] == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_verify_ip_retries_until_success_before_marking_failed(tmp_path: Path):
|
||||||
|
fake = FlakyVerifyRunner()
|
||||||
|
agent = PamDeployAgent(fake_runner=fake)
|
||||||
|
state = agent.create_state(
|
||||||
|
params=PARAMS,
|
||||||
|
execution_strategy="fake",
|
||||||
|
config_path=str(tmp_path / "config.txt"),
|
||||||
|
checkpoint_path=str(tmp_path / "checkpoint.json"),
|
||||||
|
)
|
||||||
|
|
||||||
|
agent.run_deploy_flow(state)
|
||||||
|
|
||||||
|
assert fake.verify_calls == 2
|
||||||
|
assert state.paused is False
|
||||||
|
assert state.poll_attempts == {}
|
||||||
|
assert state.ip_states["192.168.1.10"]["status"] == "SUCCESS"
|
||||||
|
assert any(
|
||||||
|
event["type"] == "ACTION_PROGRESS"
|
||||||
|
and event["stage"] == "verify-ip"
|
||||||
|
and event["ip"] == "192.168.1.10"
|
||||||
|
for event in state.events
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_create_state_writes_absolute_script_config_path_and_normalized_zip(tmp_path: Path):
|
def test_create_state_writes_absolute_script_config_path_and_normalized_zip(tmp_path: Path):
|
||||||
package_path = tmp_path / "pkg.zip"
|
package_path = tmp_path / "pkg.zip"
|
||||||
params = {**PARAMS, "ZIP_FILE_PATH": str(package_path)}
|
params = {**PARAMS, "ZIP_FILE_PATH": str(package_path)}
|
||||||
@ -216,6 +262,8 @@ def test_run_deploy_flow_stops_on_verify_failure(tmp_path: Path):
|
|||||||
|
|
||||||
agent.run_deploy_flow(state)
|
agent.run_deploy_flow(state)
|
||||||
|
|
||||||
|
verify_calls = [call for call in fake.calls if call[0] == "verify-ip" and call[1].get("ip") == "192.168.1.10"]
|
||||||
|
assert len(verify_calls) == 2
|
||||||
assert state.pending_confirmation == ""
|
assert state.pending_confirmation == ""
|
||||||
assert state.paused is True
|
assert state.paused is True
|
||||||
assert state.pause_reason == "action_failed"
|
assert state.pause_reason == "action_failed"
|
||||||
|
|||||||
@ -19,11 +19,13 @@ PARAMS = {
|
|||||||
"MODULE_NAME": "Node",
|
"MODULE_NAME": "Node",
|
||||||
"VERSION_NUMBER": "2.0.5",
|
"VERSION_NUMBER": "2.0.5",
|
||||||
"ZIP_FILE_PATH": "C:/pkg.zip",
|
"ZIP_FILE_PATH": "C:/pkg.zip",
|
||||||
|
"VERIFY_INTERVAL_SEC": 0,
|
||||||
|
"VERIFY_MAX_ATTEMPTS": 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class BlockingReviewLlmClient:
|
class BlockingReviewLlmClient:
|
||||||
def analyze_action_result(self, *, action, result, state_summary):
|
def analyze_action_result(self, *, action, result):
|
||||||
return LlmActionAnalysis(
|
return LlmActionAnalysis(
|
||||||
action=action,
|
action=action,
|
||||||
has_anomaly=True,
|
has_anomaly=True,
|
||||||
@ -56,7 +58,7 @@ class FakeTestableLlmClient:
|
|||||||
def generate_plan(self, *, params, intent, strategy):
|
def generate_plan(self, *, params, intent, strategy):
|
||||||
raise AssertionError("llm test should only call understand_request")
|
raise AssertionError("llm test should only call understand_request")
|
||||||
|
|
||||||
def analyze_action_result(self, *, action, result, state_summary):
|
def analyze_action_result(self, *, action, result):
|
||||||
return LlmActionAnalysis(action=action)
|
return LlmActionAnalysis(action=action)
|
||||||
|
|
||||||
|
|
||||||
@ -232,7 +234,7 @@ def test_chat_resume_retries_failed_ip_without_rollback(tmp_path: Path):
|
|||||||
checkpoint_path=str(tmp_path / "checkpoint.json"),
|
checkpoint_path=str(tmp_path / "checkpoint.json"),
|
||||||
)
|
)
|
||||||
|
|
||||||
output = run_session(session, ["run", "yes", "yes", "yes", "resume", "exit"])
|
output = run_session(session, ["run", "yes", "yes", "yes", "exit"])
|
||||||
|
|
||||||
assert session.state is not None
|
assert session.state is not None
|
||||||
assert session.state.pending_confirmation == ""
|
assert session.state.pending_confirmation == ""
|
||||||
@ -241,7 +243,7 @@ def test_chat_resume_retries_failed_ip_without_rollback(tmp_path: Path):
|
|||||||
assert session.state.ip_states["192.168.1.10"]["status"] == "SUCCESS"
|
assert session.state.ip_states["192.168.1.10"]["status"] == "SUCCESS"
|
||||||
assert session.state.ip_states["192.168.1.11"]["status"] == "SUCCESS"
|
assert session.state.ip_states["192.168.1.11"]["status"] == "SUCCESS"
|
||||||
assert not any(call[0] == "rollback-ip" for call in fake.calls)
|
assert not any(call[0] == "rollback-ip" for call in fake.calls)
|
||||||
assert any("如需回滚,输入 rollback 192.168.1.10" in item for item in output)
|
assert any("进度更新: verify-ip" in item for item in output)
|
||||||
|
|
||||||
|
|
||||||
def test_chat_explicit_rollback_command_rolls_back_failed_ip(tmp_path: Path):
|
def test_chat_explicit_rollback_command_rolls_back_failed_ip(tmp_path: Path):
|
||||||
|
|||||||
@ -14,6 +14,8 @@ PARAMS = {
|
|||||||
"MODULE_NAME": "Node",
|
"MODULE_NAME": "Node",
|
||||||
"VERSION_NUMBER": "2.0.5",
|
"VERSION_NUMBER": "2.0.5",
|
||||||
"ZIP_FILE_PATH": "C:/pkg.zip",
|
"ZIP_FILE_PATH": "C:/pkg.zip",
|
||||||
|
"VERIFY_INTERVAL_SEC": 0,
|
||||||
|
"VERIFY_MAX_ATTEMPTS": 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -221,7 +221,6 @@ def test_openai_compatible_client_analyzes_action_result_with_redaction():
|
|||||||
values={"CLIENT_SECRET": "real-secret", "SUCCESS": "false"},
|
values={"CLIENT_SECRET": "real-secret", "SUCCESS": "false"},
|
||||||
stderr="x" * 1200,
|
stderr="x" * 1200,
|
||||||
),
|
),
|
||||||
state_summary={"params": {"CLIENT_SECRET": "real-secret"}},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
serialized_prompt = str(calls[0])
|
serialized_prompt = str(calls[0])
|
||||||
@ -229,6 +228,7 @@ def test_openai_compatible_client_analyzes_action_result_with_redaction():
|
|||||||
assert analysis.has_anomaly is True
|
assert analysis.has_anomaly is True
|
||||||
assert analysis.severity == "high"
|
assert analysis.severity == "high"
|
||||||
assert "real-secret" not in serialized_prompt
|
assert "real-secret" not in serialized_prompt
|
||||||
|
assert "state_summary" not in input_payload
|
||||||
assert input_payload["result"]["diagnostic_log"].startswith("[已截断]...")
|
assert input_payload["result"]["diagnostic_log"].startswith("[已截断]...")
|
||||||
|
|
||||||
|
|
||||||
@ -268,7 +268,6 @@ def test_openai_compatible_client_omits_success_script_logs_from_action_review()
|
|||||||
stdout="ACTION=get-online-ips\nCOUNT=1\nIP=10.4.1.1\n",
|
stdout="ACTION=get-online-ips\nCOUNT=1\nIP=10.4.1.1\n",
|
||||||
stderr="[INFO] [FLOW][START] get_token\n[INFO] [FLOW][DONE] get_online_ips\n",
|
stderr="[INFO] [FLOW][START] get_token\n[INFO] [FLOW][DONE] get_online_ips\n",
|
||||||
),
|
),
|
||||||
state_summary={},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
input_payload = _llm_input_payload(calls[0])
|
input_payload = _llm_input_payload(calls[0])
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user