Präzises Routing für jeden Befehl: Praxis der Multi-Skill-Unterstützung in HagiCode Preset Task
Präzises Routing für jeden Befehl: Praxis der Multi-Skill-Unterstützung in HagiCode Preset Task
Ein preset mit mehreren Befehlen, die aber nur eine einzige Skill-Anforderung teilen müssen? Dieses Refactoring ermöglicht es jedem Befehl, seinen abhängigen skill unabhängig zu deklarieren und zeigt diese Bindung im Visualisierungs-Panel an – mit Badge, Zusammenfassung und One-Click-Installation, alles in einem.
Hintergrund
Zuerst etwas Hintergrundinformation.
HagiCodes preset task ist ein System von plugin-basierten kleinen Tools. Benutzer müssen keine Befehle manuell eingeben, sondern können einfach einige Felder im Visualisierungs-Panel ausfüllen, klicken und eine automatische Aufgaben-Sitzung erstellen. Jedes preset ist im Grunde ein Verzeichnis, das normalerweise so aussieht:
manifest.json:Identitätsinformationen des presetspanel.json:Formulardefinition des Visualisierungs-Panelscommands.json:Liste der tatsächlich auszuführenden Befehletask-preset.jsonoderprompts.json:Aufgabenparameter und Skill-Anforderungen
Dieses System ist bequem zu verwenden, aber wir haben bald auf eine umständliche Stelle gestoßen.
In den frühen Versionen konnten skills nur im requirements-Array auf preset-Ebene deklariert werden. Was bedeutet das? Alle Befehle innerhalb desselben presets teilen dieselben Skill-Anforderungen. Klingt harmlos, aber in der Praxis sieht es so aus:
Ein preset hat fünf Befehle, wobei der erste den last30days skill verwenden soll, der dritte den ui-master skill, und die restlichen drei benötigen keinen skill. Mit dem alten Design ist das unmöglich. Um verschiedene Befehle zu verschiedenen skills zu routen, müssten Sie diese Befehle in mehrere presets aufteilen, was die Konfiguration aufbläht.
Genau dieses Problem soll der Vorschlag extend-preset-task-multiple-skills-support lösen: Jeder Befehl kann unabhängig seinen abhängigen skill deklarieren, und diese Bindung wird im UI visualisiert.
Über HagiCode
Die in diesem Artikel vorgestellte Lösung stammt aus unserer praktischen Erfahrung im HagiCode Projekt. HagiCode ist ein AI-Code-Assistent-Projekt, und das preset task-System ist genau der Schnelleinstieg für Benutzer. Jede Änderung, die wir vorstellen, ist das Ergebnis unserer praktischen Erfahrungen und Optimierungen – schließlich ist Theorie nicht alles. Der Quellcode des Projekts ist unter HagiCode-org/site verfügbar. Wenn Sie interessiert sind, können Sie dem Projekt einen Star geben.
Das Problem klar verstehen: Warum keine Zuordnungstabelle?
Bevor wir loslegen, ist die einfachste Lösung, die einem in den Sinn kommt: eine zusätzliche commandSkillMappings-Zuordnungstabelle erstellen und die Beziehung “Befehls-ID → skill” separat zu speichern. Klingt sauber, Verantwortlichkeiten getrennt.
Aber bei genauerer Betrachtung merkt man, dass das nicht stimmt.
Jeder Befehl in commands.json hat bereits eine ID, und diese ID muss in der Zuordnungstabelle erneut kopiert werden. Zwei Dateien, dieselbe ID – sobald jemand den Befehl ändert und vergisst, die Zuordnungstabelle zu synchronisieren, driftet die Daten auseinander. Dieses Design “Trennung um der Trennung willen” hat langfristig höhere Wartungskosten als den Nutzen der Ordnung. Am Ende ist es nur unnötiger Ärger.
Deshalb haben wir uns für einen direkteren Weg entschieden: das optionale skill-Feld direkt in die Befehlsdefinition einzufügen. Ein Befehl deklariert selbst, welchen skill er bindet, wird in der Nähe gewartet, und niemand verliert den Kontakt.
Hinter dieser Entscheidung steht ein wichtigeres Designprinzip, das separat erwähnt werden sollte.
Kernpunkt 1: Trennung der Verantwortlichkeiten auf zwei Datenebenen
Das ist die wichtigste Erkenntnis in diesem gesamten Refactoring.
Viele Leute reagieren sofort: Wenn Befehle ein skill-Feld haben, sollten wir beim requirement check (Skill-Zugangsprüfung) nicht das skill-Feld jedes Befehls durchsuchen?
Nein.
Wir haben dies absichtlich in zwei Ebenen aufgeteilt:
- Das
skill-Feld incommands.json: Verantwortlich nur für die Deklaration der Bindung. Es sagt dem System “dieser Befehl soll welchen skill binden” und wird zum Rendern von prompt-Vorläufen und UI-Anzeigen verwendet. - Das
requirements-Array intask-preset.json: Das ist die autoritative Enumeration. Es ist der echte Zugang, der entscheidet, welche skills ein preset erfüllen muss, um ausgeführt zu werden.
Mit anderen Worten, skill beantwortet “welcher skill, was gerendert wird”, requirements beantwortet “wird es überhaupt erlaubt ausgeführt zu werden”. Zwei verschiedene Dinge, nicht vermischen.
Der Vorteil dieser Trennung ist, dass die check-Logik natürlich einfach bleibt. Da der Zugang immer auf dem requirements-Array auf preset-Ebene basiert, werden nach CacheKey dedupliziert, auch wenn mehrere Befehle denselben skill binden, wird nur einmal überprüft, und es gibt keine wiederholten Punktzugriffe. Befehls-level skills führen keine zusätzlichen Überprüfungskosten ein.
Dieses Prinzip ist auch der Hauptgrund, warum wir die Zuordnungstabellenlösung abgelehnt haben – die Zuordnungstabelle könnte den Eindruck erwecken, dass “Bindung gleich Zugang ist”, was die beiden Verantwortlichkeiten wieder vermengt. Klugheit wird zur Dummheit.
Kernpunkt 2: Wie sieht die Befehlsdefinition aus?
Die refactored Befehlsdefinition fügt einfach ein optionales skill-Feld zur ursprünglichen Basis hinzu. Nehmen wir das last30days bundled preset als Beispiel, seine commands.json sieht ungefähr so aus:
{ "$schema": "../../schemas/commands.schema.json", "version": "1.1", "commands": [ { "id": "research", "skill": "last30days", "prompt": "Untersuche, wie die Leute in den letzten 30 Tagen über {topic} diskutiert haben" }, { "id": "summarize", "prompt": "Fassen Sie die obigen Untersuchungsergebnisse zusammen" } ]}Einige wichtige Punkte:
versionwurde auf1.1erhöht, und das entsprechende schema fügt auch das optionaleskill-Feld hinzu.- Der erste Befehl
researchbindet denlast30daysskill und wird bei Ausführung zu diesem skill geroutet. - Der zweite Befehl
summarizebindet keinen skill, er ist nur ein normaler Befehl und nutzt den Standardpfad. - Beachten Sie, dass hier keine requirement im Befehl geschrieben wird. Der echte Zugang befindet sich im
requirements-Array intask-preset.json:
{ "requirements": [ { "key": "last30days", "cacheKey": "skill:last30days" } ]}Der last30days skill, den research bindet, muss in diesem requirements-Array erscheinen, sonst gibt es ein Problem – genau das ist die harte Einschränkung, die im nächsten Abschnitt besprochen wird. Zwangsmaßnahmen bringen nichts.
Kernpunkt 3: Kreuzvalidierung während des Ladens
Nur die Bindung in den Daten zu deklarieren reicht nicht, es muss jemand als Backup fungieren, um zu verhindern, dass “ein Befehl einen skill bindet, aber requirements hat ihn nicht deklariert” – solche verwaisten Bindungen in die Produktion gelangen.
Dieser Backup ist ValidateCommandSkills. Er wird während des Ladens des preset-Pakets ausgeführt, überprüft jeden Befehl einzeln, ob sein skill eine Entsprechung im requirements-Array auf preset-Ebene findet. Wird nicht gefunden, wird das Paket als illegal deklariert, das gesamte preset deaktiviert und der Diagnosecode command-skill-not-in-requirements geworfen.
Warum das gesamte Paket deaktivieren und nicht nur den Befehl überspringen? Weil preset ein Ganzes ist, und Befehle haben oft Abhängigkeiten (die Ausgabe des vorherigen wird dem nächsten gefüttert). Wenn leise ein Befehl übersprungen wird, erhalten die folgenden Befehle leere Eingaben, und das Verhalten ist völlig unvorhersehbar. Schließlich sind Menschen undurchsichtig, und auch Code. Besser lassen Benutzer einen klaren Fehler sehen, als Aufgaben unverständlich abweichen zu lassen. Das darf nicht sorglos sein.
Diese Validierung wird während des Ladens abgeschlossen, das heißt, das Problem wird entdeckt, sobald das preset registriert wird, nicht erst, wenn Benutzer tatsächlich “ausführen” klicken und dann platzt. Für die Benutzererfahrung ist ein früher Fehler immer besser als ein späterer Fehler.
Kernpunkt 4: Idempotente Konkatenierung von prompt-Vorläufen
Als nächstes der empfindlichste Teil der Ausführungskette.
Wenn ein Befehl einen skill bindet, wie z.B. last30days, muss das System vor der eigentlichen Ausführung diese skill-Information “davorstellen”, um einen vollständigen Einzeiler-Befehl an den Executor zu übergeben. Dieser Prozess wird von CombineCommandSkillPrelude erledigt.
Ein konkretes Beispiel. Der prompt des research-Befehls ist “Untersuche, wie die Leute in den letzten 30 Tagen über {topic} diskutiert haben”, der gebundene skill ist last30days, dann ist der Befehl, der dem Executor übergeben wird, ungefähr:
/last30days Untersuche, wie die Leute in den letzten 30 Tagen über {topic} diskutiert habenDas heißt, der Vorläufer /last30days wurde vor dem prompt hinzugefügt. Wenn der Executor diesen Vorläufer sieht, weiß er, dass er den Kontext zuerst auf den last30days skill umschalten muss.
Hier gibt es eine häufige Falle: Idempotenz.
Warum betonen Idempotenz? Weil in manchen Szenarien der prompt bereits diesen skill-Vorläufer enthalten könnte (z.B. Benutzer haben manuell die Hälfte geschrieben oder aus einer anderen Stelle kopiert). Wenn das System dumm nochmal zusammenfügt, wird es zu /last30days /last30days Untersuche..., und der Either wirft einen Fehler oder verhält sich abnormal.
Deshalb prüft CombineCommandSkillPrelude vor der Konkatenierung, ob der Präfix bereits vorhanden ist, und fügt ihn nicht doppelt hinzu. Dieser Schritt wirkt unbedeutend, kann aber eine Art sehr versteckter bug verhindern.
Es ist erwähnenswert, dass diese gesamte Vorläufer-Injektionslogik auf preset-Definitionsebene (BuildCommandPrelude in PresetTaskCatalogProvider) abgeschlossen wird, und der Sitzungserstellungscode von SessionsController völlig nicht geändert werden muss. Das ist auch ein Vorteil der Verantwortungstrennung – der Ausführungspunkt bleibt stabil, und die Komplexität der Skill-Routing wird innerhalb der Definitionsebene begrenzt.
Kernpunkt 5: Wie zeigt das Frontend diese Bindung an?
Das Backend hat das Datenmodell und die Ausführungskette ordentlich gemacht, und der letzte Schritt ist, dass Benutzer diese Bindung auf der Benutzeroberfläche “sehen” können. Schließlich, wenn Benutzer ein Feature nicht wahrnehmen können, ist es so gut wie nicht gemacht.
Das Frontend hat drei Dinge getan.
Erstens, Badges im Befehlsauswähler. Im command-picker wird neben jedem Befehl, der einen skill bindet, ein kleiner Badge angezeigt, der angibt, welchen skill er benötigt. Auf einen Blick wissen Benutzer, welcher Befehl “mit skill” und welcher ein normaler Befehl ist.
Zweitens, Zusammenfassungsblock für requirement-check. Auf dem Panel gibt es einen speziellen Zusammenfassungsbereich, der alle skill-Anforderungen auflistet, die das aktuelle preset erfüllen muss, und welchen skill jeder Befehl bindet. Die Daten dieses Blocks stammen aus der Zuordnung commandSkillsByRequirementKey – Befehle werden nach dem requirement key, den sie binden, gruppiert und aggregiert, damit Benutzer auf einen Blick erkennen können, ob “Anforderungen” und “tatsächliche Bindungen” übereinstimmen. Das ist genau das Problem, wenn man versucht, einen Tiger zu malen und stattdessen einen Hund bekommt – deshalb muss die Aggregationslogik direkt und nicht verspielt sein.
Drittens, One-Click-Installations-Deep-Link bei Fehlern. Wenn requirement check feststellt, dass ein skill nicht installiert ist, müssen Benutzer nicht selbst in der Dokumentation suchen, um die Installation zu finden. Die Benutzeroberfläche gibt direkt einen Deep-Link-Button, klicken Sie einmal, um zum entsprechenden Installationsprozess zu springen. Dieser Schritt reduziert die Distanz zwischen “Problem entdecken” und “Problem lösen” auf das Minimum.
Auf Frontend-Typenebene ist auch sehr zurückhaltend: Der Befehlstyp fügt nur ein skill?: string hinzu und macht eine Normalisierung (|| undefined), um zu vermeiden, dass leere Zeichenfolgen in späteren Urteilen Probleme verursachen.
Praxis: Fünf Schritte für das komplette Refactoring
Wenn wir die oben genannten Punkte zusammenbinden, ist das gesamte Refactoring eigentlich fünf Schritte:
- Schema erweitern:
commands.schema.jsonfügt das optionaleskill-Feld hinzu, Versionsnummer auf1.1erhöht. - Analyse + Validierung:
NormalizeCommandsist verantwortlich für die Analyse von Befehlsdefinitionen,ValidateCommandSkillsmacht Kreuzvalidierung, Befehls-skill muss im requirements-Array auf preset-Ebene gefunden werden. - Vorläufer injizieren:
BuildCommandPreludefügt vor der Ausführung idempotent den/skill-Vorläufer vor den Befehl,SessionsControllermuss nicht geändert werden. - Bundled preset migrieren: Die
commands.jsonder zwei eingebauten presetslast30daysundui-masterändern, um den entsprechenden Befehlen dasskill-Feld zu ergänzen. Die Migration ändert nur commands.json, keine anderen Dateien. - Frontend-Visualisierung: Typenfeld ergänzen, command-picker Badges hinzufügen, requirement-check Zusammenfassungsblock hinzufügen, bei Fehlern One-Click-Installations-Deep-Link geben.
Einige praktische Hinweise, separat aufgeführt:
- Ein Befehl kann nur einen skill binden. Das ist die aktuelle Einschränkung. Wenn ein Szenario wirklich benötigt, dass ein Befehl mehrere skills auslöst, ist der Ausweg, mehrere skills im
requirements-Array auf preset-Ebene zu deklarieren, damit sie auf preset-Ebene koexistieren. - Diagnosecode bei Validierungsfehlern ist
command-skill-not-in-requirements, bei Problemen suchen Sie direkt diesen Code. - Frontend-Normalisierung erinnern Sie
|| undefined, lassen Sie nicht leere Zeichenfolgen in die Urteilslogik mischen. - Bei Migration nur commands.json ändern, requirements-Bereich unverändert lassen, um unbeabsichtigte Änderungen zu vermeiden.
- Backend-Tests decken drei Szenarien ab: Befehls-skill in requirements (durchläuft), nicht in (Paket deaktiviert), mehrere Befehle binden denselben skill (Deduplizierung normal).
Zusammenfassung
Dieses Refactoring der Multi-Skill-Unterstützung von preset task fügt auf der Oberfläche nur ein skill-Feld zu Befehlen hinzu, aber dahinter verbirgt sich ein sehr würdig zu überdenkendes Designproblem: Sollten Bindung und Zugang getrennt werden?
Unsere Antwort ist ja. Das skill-Feld kümmert sich nur um “welcher skill, was gerendert wird”, requirements kümmert sich um “ob es erlaubt ist auszuführen zu werden”. Sobald diese zwei Verantwortlichkeiten vermischt sind, egal ob durch Zuordnungstabelle oder andere Formen, machen nachfolgende Validierung, Deduplizierung und UI-Anzeige alles umständlicher. Nach der Trennung wird jede Ebene einfach: Der Zugang basiert immer auf einer autoritativen Enumeration, Bindungen werden in der Nähe gewartet und driften nicht, Vorläufer-Konkatenierung ist idempotent und kontrollierbar, UI zeigt nur die bereits klaren Daten an.
Zurückblickend hat das gesamte Refactoring keine ausgefallenen Techniken verwendet, es basiert nur auf sauberer Trennung von Verantwortlichkeiten und dann jedem Layer, was er tun soll. Nach dieser Runde des Polierens kann das preset task-System von HagiCode jeden Befehl präzise zu dem skill routen, zu dem er gehört. Im Grunde sollte es so einfach sein…
Referenzen
- HagiCode-org/site:Projektquellcode, die komplette Implementierung des preset task-Systems ist hier.
- HagiCode offizielle Website:Erfahren Sie mehr über die Gesamtfähigkeiten von HagiCode.
- OpenSpec-Vorschlag
extend-preset-task-multiple-skills-support:Das ursprüngliche Designdokument für dieses Refactoring, enthält proposal, design und tasks.
开始使用 HagiCode
一次安装,几分钟上手
HagiCode for Windows 在 Microsoft Store 免费提供。打开商店即可安装并保持更新;也可以先对比各版本与定价,再决定从哪个渠道开始。