Haga que cada comando se enrute con precisión: Soporte de múltiples habilidades en HagiCode Preset Task en la práctica
Haga que cada comando se enrute con precisión: Soporte de múltiples habilidades en HagiCode Preset Task en la práctica
Un preset con múltiples comandos, pero solo un conjunto de requisitos de habilidades compartidos? Esta actualización permite que cada comando declare independientemente la habilidad de la que depende, y muestre este enlace en el panel visual: insignias, resumen, instalación con un solo clic, todo en un solo flujo.
Antecedentes
Primero, un poco de contexto.
El preset task de HagiCode es un sistema de herramientas modulares. Los usuarios no tienen que escribir comandos manualmente, solo llenan algunos campos en el panel visual, hacen clic y pueden crear una sesión de tarea automática. Cada preset es esencialmente un directorio, que generalmente se ve así:
manifest.json: Información de identidad del presetpanel.json: Definición del formulario del panel visualcommands.json: Lista de comandos que se ejecutarántask-preset.jsonoprompts.json: Parámetros de tarea y requisitos de habilidades
Este sistema es conveniente de usar, pero pronto encontramos un problema incómodo.
En las primeras versiones, las habilidades solo podían declararse en la matriz requirements a nivel de preset. ¿Qué significa esto? Que todos los comandos dentro del mismo preset compartían el mismo conjunto de requisitos de habilidades. Puede que no parezca gran cosa, pero en la práctica el escenario es así:
Un preset tiene cinco comandos, el primero quiere usar la habilidad last30days, el tercero quiere usar ui-master, y los otros tres no necesitan ninguna habilidad. En el diseño antiguo no se podía hacer. Si querías enrutar diferentes comandos a diferentes habilidades, tenías que separar estos comandos en varios presets, la configuración se inflaba.
Eso es lo que la propuesta extend-preset-task-multiple-skills-support quiere resolver: permitir que cada comando declare independientemente la habilidad de la que depende, y visualizar este enlace en la UI.
Sobre HagiCode
La solución que comparto en este artículo proviene de nuestra experiencia práctica en el proyecto HagiCode. HagiCode es un proyecto de asistente de código con IA, y el sistema preset task es precisamente su entrada de operaciones rápidas para los usuarios. Cada cambio mencionado a continuación es algo que hemos optimizado a partir de problemas reales: al fin y al cabo, el papel es superficial. El código fuente del proyecto está en HagiCode-org/site, si estás interesado puedes ir y darle una Star.
Primero aclara el problema: ¿Por qué no una tabla de mapeo?
Antes de empezar, la solución más fácil de pensar es: crear otra tabla de mapeo commandSkillMappings, y almacenar por separado la relación “ID de comando → skill”. Suena limpio, separación de responsabilidades.
Pero si piensas bien, te das cuenta de que no tiene sentido.
Cada comando en commands.json ya tiene un ID, y en la tabla de mapeo tienes que copiar este ID de nuevo. Dos archivos, el mismo ID, si algún día alguien cambia un comando y olvida sincronizar la tabla de mapeo, los datos se derivan. Este diseño de “separación por separación” tiene un costo de mantenimiento mucho mayor que la limpieza que aporta. Al final, solo añade problemas.
Así que finalmente elegimos un camino más directo: poner el campo opcional skill directamente en la definición del comando. Un comando declara por sí mismo a qué skill está vinculado, mantenimiento cercano, nadie perderá la conexión con nadie.
Detrás de esta decisión, hay un principio de diseño más importante que vale la pena mencionar por separado.
Núcleo uno: Separación de responsabilidades de datos de dos capas
Esta es la cognición más crítica de toda la actualización.
La primera reacción de mucha gente es: dado que el comando tiene skill, ¿no debería escanear el campo skill de cada comando al hacer el check de requisitos?
No.
Hemos separado deliberadamente esto en dos capas:
- Campo
skillencommands.json: Solo es responsable de declarar el enlace. Le dice al sistema “este comando está vinculado a qué skill”, se usa para renderizar el prefijo del prompt y mostrar en la UI. - Matriz
requirementsentask-preset.json: Es la enumeración autorizada. Es el verdadero control de acceso, decide qué habilidades debe cumplir un preset para poder ejecutarse.
En otras palabras, skill responde “a qué está vinculado, qué renderizar”, requirements responde “si realmente permite ejecutarse”. Dos cosas, no las mezcles.
El beneficio de esta separación es que la lógica de verificación es naturalmente simple. Porque el control de acceso siempre se basa en requirements a nivel de preset, se deduplica por CacheKey, múltiples comandos vinculados al mismo skill solo se detectarán una vez, no se realizarán múltiples puntos de control. El skill a nivel de comando no introduce ningún sobrecarga de detección adicional.
Este principio también es la razón fundamental por la que rechazamos la solución de tabla de mapeo: la tabla de mapeo haría que la gente piense que “enlace es control de acceso”, mezclando las dos responsabilidades de nuevo. La inteligencia se vuelve estupidez, no más que eso.
Núcleo dos: ¿Cómo se ve la definición de comando?
La definición de comando después de la actualización, simplemente añade un campo opcional skill sobre la base original. Tomando el bundled preset last30days como ejemplo, su commands.json se ve aproximadamente así:
{ "$schema": "../../schemas/commands.schema.json", "version": "1.1", "commands": [ { "id": "research", "skill": "last30days", "prompt": "Investiga las discusiones reales de los últimos 30 días sobre {topic}" }, { "id": "summarize", "prompt": "Organiza los resultados de investigación anteriores en un resumen" } ]}Algunos puntos clave:
versionse actualizó a1.1, el esquema correspondiente también añadió el campo opcionalskill.- El primer comando
researchestá vinculado al skilllast30days, al ejecutarse se enruta a esta habilidad. - El segundo comando
summarizeno está vinculado a ningún skill, es solo un comando normal, sigue la ruta predeterminada. - Observa que aquí no se escribe ningún requirement en el comando. El verdadero control de acceso está en
requirementsentask-preset.json:
{ "requirements": [ { "key": "last30days", "cacheKey": "skill:last30days" } ]}El last30days vinculado por el comando research debe aparecer en este requirements, de lo contrario habrá problemas: esto es exactamente la restricción estricta que se discutirá en la siguiente sección. No puedes obligar a un fruto verde a ser dulce.
Núcleo tres: Validación cruzada en tiempo de carga
Solo declarar el enlace en los datos no es suficiente, alguien tiene que garantizar, para evitar que “un comando está vinculado a un skill, pero en requirements no se declara” este enlace huérfano se escape a producción.
Esta garantía es ValidateCommandSkills. Se ejecuta cuando se carga el paquete preset, verifica cada comando para ver si su skill puede encontrar el elemento correspondiente en requirements a nivel de preset. Si no lo encuentra, se determina como paquete ilegal, deshabilita directamente todo el preset, y lanza el código de diagnóstico command-skill-not-in-requirements.
¿Por qué deshabilitar todo el paquete en lugar de solo saltar ese comando? Porque el preset es un todo, a menudo hay relaciones de dependencia entre los comandos (la salida del anterior alimenta al siguiente). Si saltas uno silenciosamente, los comandos siguientes recibirán entrada vacía, el comportamiento será completamente incontrolable. Después de todo, hay que ser cuidadoso. Es mejor que los usuarios vean un error explícito, en lugar de dejar que la tarea se desvíe misteriosamente a medio camino. Este punto, no se puede ser negligente.
Esta validación se completa en tiempo de carga, es decir, el problema se descubrirá en el momento en que el preset se registre, no esperará hasta que el usuario haga clic en “ejecutar” para explotar. Para la experiencia del usuario, un error temprano siempre es mejor que un error tardío.
Núcleo cuatro: Concatenación idempotente del prefijo del prompt
A continuación, es el eslabón más sutil en la cadena de ejecución.
Cuando un comando está vinculado a un skill, por ejemplo last30days, el sistema antes de ejecutar realmente, necesita “pegar” esta información de skill al frente del comando, formando una instrucción de línea única completa para el ejecutor. Este proceso es responsabilidad de CombineCommandSkillPrelude.
Tomemos un ejemplo concreto. El prompt del comando research es “Investiga las discusiones reales de los últimos 30 días sobre {topic}”, el skill vinculado es last30days, entonces la instrucción final entregada al ejecutor es aproximadamente:
/last30days Investiga las discusiones reales de los últimos 30 días sobre {topic}Es decir, se añadió el prefijo /last30days antes del prompt. El ejecutor al ver este prefijo, sabe que primero debe cambiar el contexto al skill last30days.
Hay un pozo fácil de pisar aquí: idempotencia.
¿Por qué enfatizar idempotencia? Porque en algunos escenarios, el prompt en sí ya puede tener este prefijo de skill (por ejemplo, el usuario escribió la mitad manualmente, o lo copió de otro lugar). Si el sistema lo pega tontamente de nuevo, se convertirá en /last30days /last30days Investiga..., el ejecutor either dará error o tendrá un comportamiento anormal.
Por eso CombineCommandSkillPrelude antes de concatenar detectará primero, si el prefijo ya existe, no lo añadirá de nuevo. Este paso parece insignificante, pero puede bloquear una clase de bugs muy sutiles.
Cabe mencionar que toda la lógica de inyección de prefijo se completa en la capa de definición del preset (BuildCommandPrelude en PresetTaskCatalogProvider), el código de creación de sesión en SessionsController no necesita cambios en absoluto. Este es también el beneficio de la separación de responsabilidades: la entrada de ejecución permanece estable, la complejidad del enrutamiento de habilidades se recoge dentro de la capa de definición.
Núcleo cinco: Cómo muestra el frontend el enlace
El backend ha ordenado el modelo de datos y la cadena de ejecución, el último paso es permitir que los usuarios “vean” este enlace en la interfaz. Después de todo, si un usuario no puede percibir una función, entonces es aproximadamente equivalente a no hacerla.
El frontend hizo tres cosas.
Primero, insignias en el selector de comandos. En el command-picker, junto a cada comando vinculado a un skill se mostrará una pequeña insignia, indicando de qué skill depende. Los usuarios de un vistazo saben qué comando es “con habilidad”, cuál es un comando normal.
Segundo, bloque de resumen de requirement-check. En el panel hay un área de resumen dedicada, que lista todos los requisitos de skill que el preset actual necesita cumplir, y a qué skill está vinculado cada comando respectivamente. Los datos de este bloque provienen del mapeo commandSkillsByRequirementKey: agrupa los comandos por su requirement key vinculado, conveniente para que los usuarios vean de un vistazo si los “requisitos” y el “enlace real” coinciden. Dibujar un tigre sin lograr que se parezca, aproximadamente es esto: por eso la lógica de agregación debe ser directa, no complicada.
Tercero, enlace profundo de instalación con un solo clic en caso de fallo. Si el requirement-check descubre que algún skill no está instalado, los usuarios no tienen que buscar la entrada de instalación en la documentación ellos mismos. La interfaz da directamente un botón de enlace profundo, al hacer clic salta al flujo de instalación correspondiente. Este paso comprime al mínimo la distancia entre “descubrir el problema” y “resolver el problema”.
En cuanto a los tipos de frontend, también son muy moderados, el tipo de comando simplemente añadió un skill?: string, y se hizo un procesamiento de normalización (|| undefined), para evitar que cadenas vacías u otros valores de frontera causen problemas en juicios posteriores.
Práctica: Cinco pasos para completar toda la actualización
Conectando los puntos dispersos anteriores, toda la actualización es en realidad cinco pasos:
- Extender el esquema:
commands.schema.jsonañade el campo opcionalskill, número de versión actualizado a1.1. - Análisis + validación:
NormalizeCommandses responsable de analizar la definición de comandos,ValidateCommandSkillshace la validación cruzada, el skill del comando debe poder encontrarlo en los requirements a nivel de preset. - Inyectar prefijo:
BuildCommandPreludeantes de la ejecución concatena idempotentemente el prefijo/skillfrente al comando, no necesita modificarSessionsController. - Migrar bundled preset: Modificar
commands.jsonde los dos preset integradoslast30daysyui-master, añadir el camposkilla los comandos correspondientes. La migración solo toca commands.json, no toca otros archivos. - Visualización del frontend: tipos complementan campos, command-picker añade insignias, requirement-check añade bloque de resumen, en caso de fallo da enlace profundo de instalación con un solo clic.
Algunas notas en la práctica, se listan por separado:
- Un comando solo puede vincularse a un skill. Esta es la restricción actual. Si un escenario realmente necesita que un comando active múltiples habilidades, la salida de emergencia es declarar múltiples skills en
requirementsa nivel de preset, permitiendo que coexistan a nivel de preset. - Código de diagnóstico para fallos de validación es
command-skill-not-in-requirements, al investigar problemas busca directamente este código. - Normalización del frontend recuerda
|| undefined, no dejes que cadenas vacías se mezclen en la lógica de juicio. - Al migrar solo toca commands.json, mantén requirements sin cambios, para evitar introducir cambios inesperados.
- Pruebas del backend cubren tres escenarios: comando skill en requirements (pasa), no en (deshabilita paquete), múltiples comandos vinculan el mismo skill (deduplicación normal).
Resumen
Esta actualización de soporte de múltiples habilidades en preset task,表面上 es solo añadir un campo skill a los comandos, pero detrás conlleva un problema de diseño que vale la pena considerar: enlace y control de acceso, ¿deberían separarse o no?
Nuestra respuesta es separar. El campo skill solo se encarga de “a qué está vinculado, qué renderizar”, requirements se encarga de “si permite ejecutar”. Una vez que estas dos capas de responsabilidades se mezclan, ya sea usando una tabla de mapeo u otra forma, hará que la validación posterior, deduplicación, visualización de la UI se vuelvan incómodas. Después de separar, cada capa se vuelve simple: el control de acceso siempre se basa en una enumeración autorizada, el enlace se mantiene cercano y no se deriva, la concatenación del prefijo es idempotente y controlable, la UI solo muestra los datos ya claros.
Mirando hacia atrás, toda la actualización no usó ninguna tecnología elegante, se basó simplemente en cortar las responsabilidades limpiamente, y luego garantizar lo que cada capa debe respaldar. El sistema preset task de HagiCode después de este pulido, finalmente puede permitir que cada comando se enroute con precisión al skill que debe ir. Al fin y al cabo, las cosas deberían ser así de simples…
Materiales de referencia
- HagiCode-org/site: Código fuente del proyecto, la implementación completa del sistema preset task está aquí.
- Sitio web oficial de HagiCode: Conoce las capacidades generales de HagiCode.
- Propuesta de OpenSpec
extend-preset-task-multiple-skills-support: Documento de diseño original de esta actualización, incluye proposal, design y tasks.
开始使用 HagiCode
一次安装,几分钟上手
HagiCode for Windows 在 Microsoft Store 免费提供。打开商店即可安装并保持更新;也可以先对比各版本与定价,再决定从哪个渠道开始。