feat(ai-roles): импортируемый мультиязычный каталог ролей агента #222
Reference in New Issue
Block a user
Delete Branch "feature/agent-roles-catalog"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Что это
Импорт проверенных ролей агента из каталога: админ листает каталог, затягивает роли/комплекты в workspace и может обновить уже импортированную роль, когда в каталоге вышла новая версия. Каталог мультиязычный — язык выбирается при импорте.
Каталог
index.json(манифест — комплекты, языки, версии ролей) +bundles/<id>/<lang>.json(контент).index.json→ «есть обновление» проверяется одним запросом.AI_AGENT_ROLES_CATALOG_URL:http(s)://...→ удалённый fetch; пусто/путь → локальная папкаagent-roles-catalog/(дефолт для dev).Сервер
sourceнаai_agent_roles({ slug, language, version };null— роль создана вручную).^[a-z0-9-]+$до построения пути).catalog,catalog/bundle,import,update-from-catalog.slug+language; обновление перезаписывает контент, но не трогаетenabledи аккуратно обходит коллизию имён.Клиент
Как включить GitHub-источник
Выложить папку
agent-roles-catalog/в репозиторий и задатьAI_AGENT_ROLES_CATALOG_URLна raw-базу. Для dev ничего настраивать не нужно — читается локальная папка.Проверка
tscчисто; jest поai-agent-roles— все тесты зелёные (сервис + репозиторий + провайдер + контроллер), включая path-traversal-гард, стриминговый кап, конфликты импорта (skip/rename/already-installed) и сценарии обновления (up-to-date / not-in-catalog / language-unavailable / коллизия имени).tscчисто; обаtranslation.jsonвалидны.agent-roles-catalog/scripts/check.mjs→ OK (уникальность slug по всему каталогу, соответствие index ↔ языковым файлам).Замечание по миграции
Нужно прогнать новую миграцию (
20260626T120000-ai-agent-roles-catalog-source) — добавляет колонкуsource.🤖 Generated with Claude Code
Admins can browse a curated catalog of agent roles, import roles/bundles into a workspace, and update an imported role when the catalog ships a newer version. Catalog: a set of JSON files (index.json manifest + bundles/<id>/<lang>.json) served from a local folder (dev) or a remote http(s) base URL via AI_AGENT_ROLES_CATALOG_URL. Seeded with the existing 7 RU roles (editorial + research bundles) plus EN translations. Server: - migration: nullable jsonb `source` column on ai_agent_roles ({ slug, language, version }; null => manually created) - catalog provider: remote fetch with timeout + streaming size cap, or local read; ^[a-z0-9-]+$ segment guard against path-traversal/SSRF - admin endpoints: catalog, catalog/bundle, import, update-from-catalog - import/update match by slug+language; update preserves `enabled` Client: - catalog modal with language selector and Import/Installed/Update states - "Import from catalog" button + empty-state CTA in the roles settings panel - en-US/ru-RU strings Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>- Rename catalog-source migration 20260626T120000 -> T150000 so it sorts after develop's latest migration (T140000-page-temporary-notes); the old timestamp predated ai-chat-message-status/share-aliases and tripped Kysely's #ensureMigrationsInOrder, aborting server boot. - Provider: inject a Nest Logger and log the real cause (incl. response status) in the parseJson / readLocal / fetchRemote catch blocks, and propagate a useful cause into the BadGatewayException message; add a shortError helper (robust to jest's realm-shifted Error-likes). - Provider: replace the manual Uint8Array assembly with Buffer.concat(chunks).toString('utf8'); keep the streaming size cap. - Controller spec: add admin-gate coverage for the 4 catalog routes (catalog/catalogBundle/import/updateFromCatalog) - non-admin Forbidden + service untouched, admin delegates with the right args. - Service spec: add getCatalog/getCatalogBundle tests covering the localized() three-tier fallback, the sorted language union, the missing-bundle BadGateway, and the role-version default. - Provider spec: add remote fetch-rejects and non-ok (503) error branches. - Service: drop the dead Date.now() tail in freeName (now an explicit unreachable throw) and extract a shared isUniqueViolation() predicate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>ef678e34e1to8be8279809Item 1 (concurrency-safe import): add a partial UNIQUE index on (workspace_id, source->>'slug', source->>'language') WHERE source IS NOT NULL AND deleted_at IS NULL, so two concurrent imports of the same bundle can no longer create duplicate roles for one catalog slug+language. The in-memory installedKeys snapshot cannot see a sibling request's writes; the index is the backstop. importFromCatalog now catches the 23505 from THIS index (keyed off the constraint name) and treats it as "already installed" -> skip, batch continues. A 23505 from the name-uniqueness index keeps its existing friendly per-role error behavior (distinguished by constraint name; an indeterminate 23505 falls back to that path, so no regression). Item 2 (single source validator): strengthen parseSource into THE single form validator for the source jsonb column -> returns a fully-valid RoleSource | null (slug/language non-empty strings, version a number). The service's weaker roleSource is removed and both layers share the RoleSource type (defined in the db entity.types module both already import AiAgentRole from, so no import cycle). normalizeRow / the read path now only ever yield a valid RoleSource or null; a malformed stored source normalizes to null (tolerated by the service). Tests: parseSource null for {} / {slug:123} / {slug:'a'} / empty-string keys / string version, typed value for a full valid shape; service test that a source-uniqueness 23505 is skipped (not errored) and the batch continues. Verified the partial index rejects a duplicate source-not-null row but allows two source-NULL rows, and the migration up/down run cleanly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Ghost referenced this pull request2026-06-28 04:23:39 +03:00
Ghost referenced this pull request2026-06-28 22:18:11 +03:00