diff --git a/locales/en/nodeDefs.json b/locales/en/nodeDefs.json index 3c08286..febc459 100644 --- a/locales/en/nodeDefs.json +++ b/locales/en/nodeDefs.json @@ -26,7 +26,8 @@ "display_name": "OpenAI Text Skill Options", "description": "Load a local SKILL.md package for progressive use by OpenAI Text API.", "inputs": { - "skill_name": {"name": "skill_name", "tooltip": "Select a configured local Skill"} + "skill_name": {"name": "skill_name", "tooltip": "Select a configured local Skill"}, + "description": {"name": "Skill description", "tooltip": "Read-only description of the selected Skill"} }, "outputs": {"0": {"name": "skill_options"}} }, diff --git a/locales/zh/nodeDefs.json b/locales/zh/nodeDefs.json index f04cb30..e63f33d 100644 --- a/locales/zh/nodeDefs.json +++ b/locales/zh/nodeDefs.json @@ -26,7 +26,8 @@ "display_name": "OpenAI 文本 Skill 选项", "description": "加载本地 SKILL.md,并供 OpenAI 文本 API 渐进式读取引用资料。", "inputs": { - "skill_name": {"name": "Skill 名称", "tooltip": "选择配置中发现的本地 Skill"} + "skill_name": {"name": "Skill 名称", "tooltip": "选择配置中发现的本地 Skill"}, + "description": {"name": "Skill 描述", "tooltip": "当前所选 Skill 的只读描述"} }, "outputs": {"0": {"name": "Skill 选项"}} }, diff --git a/options/openai_text_skill_options_node.py b/options/openai_text_skill_options_node.py index 4df83de..b2b5f3b 100644 --- a/options/openai_text_skill_options_node.py +++ b/options/openai_text_skill_options_node.py @@ -1,6 +1,16 @@ +from aiohttp import web +from server import PromptServer from comfy_api.latest import io -from ..utils.skill_utils import create_skill_options, discover_skills +from ..utils.skill_utils import create_skill_options, get_skill_summaries + + +@PromptServer.instance.routes.get("/ycyy/openai/skills/all") +async def get_all_openai_skills(request): + try: + return web.json_response(get_skill_summaries()) + except Exception as exc: + return web.json_response({"error": str(exc)}, status=500) class OpenAITextSkillOptions(io.ComfyNode): @@ -8,8 +18,9 @@ class OpenAITextSkillOptions(io.ComfyNode): @classmethod def define_schema(cls) -> io.Schema: - skills, _ = discover_skills(strict=True) + skills = get_skill_summaries() names = [item["name"] for item in skills] or [""] + description = skills[0]["description"] if skills else "" return io.Schema( node_id="YCYY_OpenAI_Text_Skill_Options", display_name="OpenAI Text Skill Options", @@ -20,7 +31,14 @@ class OpenAITextSkillOptions(io.ComfyNode): options=names, default=names[0], tooltip="Select a configured local SKILL.md package.", - ) + ), + io.String.Input( + id="description", + default=description, + multiline=True, + socketless=True, + tooltip="Description of the selected Skill (read-only).", + ), ], outputs=[ io.AnyType.Output( @@ -33,7 +51,8 @@ class OpenAITextSkillOptions(io.ComfyNode): ) @classmethod - def execute(cls, skill_name) -> io.NodeOutput: + def execute(cls, skill_name, description="") -> io.NodeOutput: if not skill_name: raise ValueError("No valid local Skills were found") + # Description is display-only. Always rebuild options from the selected Skill. return io.NodeOutput(create_skill_options(skill_name)) diff --git a/utils/skill_utils.py b/utils/skill_utils.py index f93ca55..d35d63a 100644 --- a/utils/skill_utils.py +++ b/utils/skill_utils.py @@ -279,6 +279,15 @@ def discover_skills(strict=True): return result, errors +def get_skill_summaries(): + """Return only the metadata needed to render the Skill selector.""" + skills, _ = discover_skills(strict=True) + return [ + {"name": skill["name"], "description": skill["description"]} + for skill in skills + ] + + def get_skill_snapshot(skill_name): skills, _ = discover_skills(strict=True) diff --git a/web/js/openai_text_skill_options.js b/web/js/openai_text_skill_options.js new file mode 100644 index 0000000..5442224 --- /dev/null +++ b/web/js/openai_text_skill_options.js @@ -0,0 +1,98 @@ +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; + +const NODE_CLASS = "YCYY_OpenAI_Text_Skill_Options"; +let skillMap = new Map(); + +function refreshSkillNodes() { + for (const node of app.graph?._nodes ?? []) { + if (node.comfyClass === NODE_CLASS || node.type === NODE_CLASS) syncNode(node); + } +} + +async function loadSkills(retries = 1) { + try { + const response = await api.fetchApi("/ycyy/openai/skills/all"); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const data = await response.json(); + skillMap = new Map( + (Array.isArray(data) ? data : []).map(skill => [skill.name, skill]) + ); + refreshSkillNodes(); + } catch (error) { + console.error("[YCYY] Failed to load Skill descriptions:", error); + if (retries > 0) setTimeout(() => loadSkills(retries - 1), 2000); + } +} + +function makeReadOnly(widget) { + if (!widget) return; + widget.options = { ...(widget.options ?? {}), readOnly: true }; + if (widget.inputEl) { + widget.inputEl.readOnly = true; + widget.inputEl.setAttribute("aria-readonly", "true"); + widget.inputEl.spellcheck = false; + } +} + +function applyDescription(node, skillName) { + const descriptionWidget = node.widgets?.find(widget => widget.name === "description"); + if (!descriptionWidget) return; + const skill = skillMap.get(skillName); + descriptionWidget.value = typeof skill?.description === "string" + ? skill.description + : ""; + makeReadOnly(descriptionWidget); + app.canvas?.draw(true, true); +} + +function syncNode(node) { + const skillWidget = node.widgets?.find(widget => widget.name === "skill_name"); + const descriptionWidget = node.widgets?.find(widget => widget.name === "description"); + if (!skillWidget || !descriptionWidget) return; + + const values = skillWidget.options?.values ?? []; + if (values.length && !values.includes(skillWidget.value)) { + const fallback = values[0] ?? ""; + console.warn( + `[YCYY] Skill "${skillWidget.value}" no longer exists; using "${fallback}"` + ); + skillWidget.value = fallback; + } + makeReadOnly(descriptionWidget); + applyDescription(node, skillWidget.value); +} + +app.registerExtension({ + name: "YCYY.OpenAI.Text.SkillOptions", + async setup() { + await loadSkills(); + }, + async beforeRegisterNodeDef(nodeType) { + if (nodeType.comfyClass !== NODE_CLASS) return; + + const originalCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + const result = originalCreated?.apply(this, arguments); + const skillWidget = this.widgets?.find(widget => widget.name === "skill_name"); + const descriptionWidget = this.widgets?.find(widget => widget.name === "description"); + if (skillWidget && descriptionWidget) { + makeReadOnly(descriptionWidget); + const originalCallback = skillWidget.callback; + skillWidget.callback = value => { + applyDescription(this, value); + originalCallback?.call(this, value); + }; + setTimeout(() => syncNode(this), 0); + } + return result; + }; + + const originalConfigure = nodeType.prototype.onConfigure; + nodeType.prototype.onConfigure = function () { + const result = originalConfigure?.apply(this, arguments); + setTimeout(() => syncNode(this), 0); + return result; + }; + }, +});