#!/usr/bin/env python3 # -*- coding: utf-8 -*- """修复:OpenAI 插件升级到 1.0.4 后 schema 补丁丢失,导致 qwen 不可用。""" from __future__ import annotations import sys import time from pathlib import Path ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "scripts_cursor")) sys.path.insert(0, str(ROOT / "scripts_cursor" / "作业7_故障自愈工作流")) import _bootstrap # noqa: F401 import _agent_cfg as AC import _config as C import _plugin_util as PU from dify_api import Dify PREFERRED = ( "detect_vllm_anomaly", "restart_vllm_container", "send_recovery_notification", ) SYSTEM_PROMPT = """你是 vLLM 故障自愈运维 Agent。当用户报告 vLLM OOM、崩溃、不可用或要求自愈时,必须严格按顺序执行: 1. detect_vllm_anomaly — 检测是否 OOM 或容器异常 2. 若 anomaly=true 且 recommendation=restart_vllm_container,调用 restart_vllm_container 重启容器 3. 重启后再次 detect_vllm_anomaly 确认恢复 4. send_recovery_notification — 向钉钉发送恢复通知(message 写结论,detail 写证据与重启结果) 禁止跳过检测直接重启。禁止编造未发生的重启。 通知示例 message:「vLLM OOM 已自愈,容器已重启恢复服务」。""" def list_openai_llm_py(d: Dify) -> list[str]: out, _ = d.sh( "docker exec %s sh -c " "'ls -d /app/storage/cwd/langgenius/openai-*@*/models/llm/llm.py 2>/dev/null'" % C.PLUGIN_CONTAINER ) return [x.strip() for x in (out or "").splitlines() if x.strip()] def patch_all_openai_llm(d: Dify) -> list[str]: paths = list_openai_llm_py(d) if not paths: raise RuntimeError("找不到任何 openai*/models/llm/llm.py") patched = [] for path in paths: content = PU.container_read(d, C.PLUGIN_CONTAINER, path) has = PU.LLM_PATCH_MARKER in content print("[INFO]", path, "patched=" + str(has)) if has: continue d.sh( "docker exec %s cp %s %s.bak_qwenfix104" % (C.PLUGIN_CONTAINER, path, path) ) new_content = PU.patch_llm_schema(content) PU.container_write(d, C.PLUGIN_CONTAINER, path, new_content) out, err = d.sh( "docker exec %s python -m py_compile %s" % (C.PLUGIN_CONTAINER, path) ) print("[INFO] py_compile", path, out or err or "ok") patched.append(path) return patched def restore_hw7_agent(d: Dify) -> None: agent_id = C.HOMEWORK7_AGENT_ID sql = ( "SELECT a.app_id, a.id, a.active_config_snapshot_id " "FROM agents a WHERE a.id='%s' LIMIT 1;" % agent_id ) out, err = d.sh( 'docker exec %s psql -U postgres -d dify -t -A -F "|" -c "%s"' % (C.PG_CONTAINER, sql) ) line = (out or "").strip().splitlines() if not line: raise RuntimeError(err or "hw7 agent missing") app_id, agent_id, snap_id = line[0].split("|") out, _ = d.sh( 'docker exec %s psql -U postgres -d dify -t -A -c ' '"SELECT id FROM agent_config_drafts WHERE agent_id=\'%s\' ' 'ORDER BY updated_at DESC NULLS LAST LIMIT 1;"' % (C.PG_CONTAINER, agent_id) ) draft_id = ((out or "").strip().splitlines() or [C.HOMEWORK7_DRAFT_ID])[0].strip() out, _ = d.sh( "docker exec %s psql -U postgres -d dify -t -A -c " "\"SELECT id FROM tool_api_providers WHERE name='%s' LIMIT 1;\"" % (C.PG_CONTAINER, C.SELF_HEAL_PROVIDER) ) provider_uuid = (out or "").strip() r = d.get( "/workspaces/current/tool-provider/api/tools?provider=%s" % C.SELF_HEAL_PROVIDER ) data = r.get("json") or [] by_name = {} for tool in data: name = tool.get("name") or tool.get("tool_name") or "" by_name[name] = { "enabled": True, "provider_type": "api", "provider_id": provider_uuid, "tool_name": name, "credential_type": "unauthorized", "credential_ref": None, "runtime_parameters": {}, } tool_entries = [by_name[n] for n in PREFERRED if n in by_name] if len(tool_entries) != len(PREFERRED): raise RuntimeError("tools incomplete: %s" % list(by_name)) def patch(cfg: dict) -> dict: prompt = cfg.setdefault("prompt", {}) or {} cfg["prompt"] = prompt prompt["system_prompt"] = SYSTEM_PROMPT tools = cfg.get("tools") if isinstance(cfg.get("tools"), dict) else {} cfg["tools"] = tools tools["dify_tools"] = tool_entries tools["cli_tools"] = tools.get("cli_tools") or [] model = cfg.get("model") if isinstance(cfg.get("model"), dict) else {} cfg["model"] = model model["model"] = C.QWEN_MODEL model["plugin_id"] = "langgenius/openai" model["model_provider"] = C.OPENAI_PROVIDER model["credential_ref"] = None ms = model.get("model_settings") if isinstance(model.get("model_settings"), dict) else {} model["model_settings"] = ms ms["max_tokens"] = ms.get("max_tokens") or 4096 for k in ( "stop", "top_p", "temperature", "response_format", "presence_penalty", "frequency_penalty", ): ms.setdefault(k, None) return cfg snap = AC.load_cfg(d, "agent_config_snapshots", snap_id) draft = AC.load_cfg(d, "agent_config_drafts", draft_id) AC.save_cfg(d, "agent_config_snapshots", snap_id, patch(snap), "hw7fix2s") AC.save_cfg(d, "agent_config_drafts", draft_id, patch(draft), "hw7fix2d") print("[INFO] restored hw7 agent", app_id, agent_id, "model=", C.QWEN_MODEL) def main() -> int: if sys.platform == "win32": try: sys.stdout.reconfigure(encoding="utf-8", errors="replace") except Exception: pass d = Dify(C.DIFY_EMAIL, C.DIFY_PASSWORD) try: d.login() patched = patch_all_openai_llm(d) print("[INFO] newly patched files:", patched or "(none, checking 1.0.4)") # 强制确认 1.0.4 有 marker paths = list_openai_llm_py(d) for p in paths: c = PU.container_read(d, C.PLUGIN_CONTAINER, p) print("[CHECK]", "1.0.4" in p, p.split("/")[-5] if "/" in p else p, "marker=", PU.LLM_PATCH_MARKER in c) print("[INFO] restart plugin_daemon ...") PU.restart_plugin_daemon(d) time.sleep(12) r = d.get("/workspaces/current/models/model-types/llm") data = (r.get("json") or {}).get("data") or [] names = [] for m in data: if isinstance(m, dict): names.append( "%s/%s" % (m.get("status"), m.get("model") or m.get("model_name")) ) print("[INFO] llm count=", len(data), "sample=", names[:15]) if not data: print("[FAIL] LLM 列表仍为空") return 2 restore_hw7_agent(d) print("[PASS] OpenAI 插件 schema 已修复,作业7 Agent 已恢复 qwen3.6-27b") return 0 finally: d.close() if __name__ == "__main__": sys.exit(main())