@ykurilov/agent-chat-skills 0.0.0-stage → 0.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +81 -2
- package/dist/chunk-AFEASUO5.js +9 -0
- package/dist/chunk-AFEASUO5.js.map +1 -0
- package/dist/chunk-T5CV2V6H.js +48 -0
- package/dist/chunk-T5CV2V6H.js.map +1 -0
- package/dist/chunk-X36FOCSA.js +58 -0
- package/dist/chunk-X36FOCSA.js.map +1 -0
- package/dist/index.js +452 -0
- package/dist/index.js.map +1 -0
- package/dist/labels.js +20 -0
- package/dist/labels.js.map +1 -0
- package/dist/postgres.js +194 -0
- package/dist/postgres.js.map +1 -0
- package/dist/react.js +543 -0
- package/dist/react.js.map +1 -0
- package/dist/sqlite.js +167 -0
- package/dist/sqlite.js.map +1 -0
- package/dist/types/create-skills.d.ts +36 -0
- package/dist/types/index.d.ts +15 -0
- package/dist/types/labels.d.ts +14 -0
- package/dist/types/loaded-skills.d.ts +16 -0
- package/dist/types/postgres.d.ts +9 -0
- package/dist/types/react/line-diff.d.ts +10 -0
- package/dist/types/react/skills-admin.d.ts +15 -0
- package/dist/types/react/skills-client.d.ts +42 -0
- package/dist/types/react/skills-labels.d.ts +68 -0
- package/dist/types/react/skills-skin.d.ts +126 -0
- package/dist/types/react.d.ts +11 -0
- package/dist/types/skill-rules.d.ts +22 -0
- package/dist/types/skill-texts.d.ts +19 -0
- package/dist/types/skill-visibility.d.ts +16 -0
- package/dist/types/skills-handler.d.ts +21 -0
- package/dist/types/skills-manager.d.ts +36 -0
- package/dist/types/sqlite.d.ts +8 -0
- package/dist/types/store/memory.d.ts +3 -0
- package/dist/types/store/migration-settings.d.ts +4 -0
- package/dist/types/store/postgres-schema.d.ts +398 -0
- package/dist/types/store/postgres.d.ts +12 -0
- package/dist/types/store/skill-store.d.ts +22 -0
- package/dist/types/store/sqlite-schema.d.ts +425 -0
- package/dist/types/store/sqlite.d.ts +6 -0
- package/dist/types/types.d.ts +103 -0
- package/dist/types/with-skills.d.ts +7 -0
- package/drizzle/postgres/0000_init.sql +33 -0
- package/drizzle/postgres/meta/0000_snapshot.json +220 -0
- package/drizzle/postgres/meta/_journal.json +13 -0
- package/drizzle/sqlite/0000_init.sql +32 -0
- package/drizzle/sqlite/meta/0000_snapshot.json +219 -0
- package/drizzle/sqlite/meta/_journal.json +13 -0
- package/package.json +88 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# @ykurilov/agent-chat-skills
|
|
2
|
+
|
|
3
|
+
## 0.57.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [ade99cc]
|
|
8
|
+
- Updated dependencies [86afa20]
|
|
9
|
+
- Updated dependencies [624c10c]
|
|
10
|
+
- @ykurilov/agent-chat-core@0.57.0
|
|
11
|
+
|
|
12
|
+
## 0.56.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- 949d3b2: Навыки агента: новый пакет `@ykurilov/agent-chat-skills` (список включённых навыков в блоке контекста, загрузка указаний
|
|
17
|
+
инструментом `use_skill`, версии, учёт загрузок, API и экран для админа) и скин экрана `@ykurilov/agent-chat-skin-tandem/skills`.
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- Updated dependencies [24c313e]
|
|
22
|
+
- @ykurilov/agent-chat-core@0.56.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yury Kurilov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,82 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ykurilov/agent-chat-skills
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Навыки агента: готовые указания для отдельных задач («разработка игр», «оформление договора»), которые админ пишет
|
|
4
|
+
на экране приложения, а агент загружает сам, когда задача подходит. Инструкция агента не растёт, кэш модели не ломается.
|
|
5
|
+
|
|
6
|
+
Пример. Навык `game-dev`: «Когда просят сделать игру» и две страницы указаний. Человек пишет «сделай змейку» — агент
|
|
7
|
+
зовёт `use_skill("game-dev")`, получает указания и работает по ним. Человек пишет «поправь цвет кнопки» — навык не
|
|
8
|
+
грузится, запрос короткий.
|
|
9
|
+
|
|
10
|
+
`react`, `better-sqlite3` и `pg` — необязательные зависимости (peer): ставьте те, что нужны.
|
|
11
|
+
|
|
12
|
+
## Сервер
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { createSkills, createSkillsHandler, withSkills } from "@ykurilov/agent-chat-skills";
|
|
16
|
+
import { sqliteSkillStore } from "@ykurilov/agent-chat-skills/sqlite"; // или postgresSkillStore(pool) из …/postgres
|
|
17
|
+
import { mountChat } from "@ykurilov/agent-chat-server/hono";
|
|
18
|
+
|
|
19
|
+
const skills = createSkills({
|
|
20
|
+
store: sqliteSkillStore("./data/chat.db"), // тот же файл, что у чатов, или свой
|
|
21
|
+
isAdmin: (user) => adminIds.has(user.id), // доступ к экрану навыков; можно async
|
|
22
|
+
chatStorage: storage, // хранилище чатов: по истории ветки видно, загружен ли навык
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const handler = createChatHandler({
|
|
26
|
+
…,
|
|
27
|
+
// Права на ход: один раз в начале хода, из базы приложения. Нет viewer — навыки «только админам» не видит никто.
|
|
28
|
+
// По ownerId: в копии чата человека userId — сотрудник, а права нужны человека.
|
|
29
|
+
agent: withSkills({ instructions, tools, viewer: async ({ ownerId }) => ({ isAdmin: await users.isAdmin(ownerId) }) }, skills),
|
|
30
|
+
});
|
|
31
|
+
mountChat(app, handler);
|
|
32
|
+
mountChat(app, createSkillsHandler({ skills, resolveUser })); // адреса экрана, по умолчанию /api/skills
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`withSkills` добавляет блок контекста `skills` после блоков приложения и инструмент `use_skill` к инструментам
|
|
36
|
+
приложения. Совпадение имени — ошибка при запуске. Next.js: `createNextRouteHandlers(skillsHandler)` в
|
|
37
|
+
`app/api/skills/[[...path]]/route.ts`.
|
|
38
|
+
|
|
39
|
+
Настройки `createSkills`: `maxBodyChars` (по умолчанию 50 000), `modelTexts` (`englishSkillsModelTexts` для
|
|
40
|
+
англоязычного агента), `log` и `logError` (по умолчанию `console`).
|
|
41
|
+
|
|
42
|
+
## Экран
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { SkillsAdmin } from "@ykurilov/agent-chat-skills/react";
|
|
46
|
+
import { tandemSkillsSkin } from "@ykurilov/agent-chat-skin-tandem/skills";
|
|
47
|
+
|
|
48
|
+
<SkillsAdmin skin={tandemSkillsSkin} />
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Список со статусом и числом загрузок, создание и правка с предпросмотром markdown, статус, история версий с разницей
|
|
52
|
+
и откатом, удаление, последние чаты, где навык загружали. Подписи — `SkillsLabelsProvider` (`englishSkillsLabels`).
|
|
53
|
+
Подписи шагов в ленте чата — `skillsStepPhrases` и `skillsTitles` из `@ykurilov/agent-chat-skills/labels`.
|
|
54
|
+
|
|
55
|
+
## Что важно знать
|
|
56
|
+
|
|
57
|
+
- **Навык.** `name` — строчные латинские или русские буквы, цифры, дефис, 2–48 знаков, не меняется. `whenToUse` — одна
|
|
58
|
+
строка до 300 знаков. `body` — markdown. Статус: `off`, `admins`, `everyone`. Новый навык выключен.
|
|
59
|
+
- **Кэш модели.** Список навыков (`- name: whenToUse`, по имени) лежит в блоке контекста хода, а не в инструкции и не
|
|
60
|
+
в описании инструмента. Инструкция и инструменты одни на все чаты. Пока навыки не правят, запрос к модели от хода к
|
|
61
|
+
ходу начинается одинаково. Правка навыка меняет только новый вопрос: старая часть запроса остаётся байт в байт.
|
|
62
|
+
- **Повторная загрузка.** Тело приходит как `<skill name="…" version="N" hash="…">…</skill>`; `hash` — отпечаток тела,
|
|
63
|
+
он отличает навык, созданный заново с тем же именем. То же тело ещё видно модели в ветке или уже отдано в этом ходе
|
|
64
|
+
(два вызова в одном шаге) — ответ «уже загружен выше» без тела. Новая версия, сжатие истории или другая ветка — тело
|
|
65
|
+
заново.
|
|
66
|
+
- **Видимость.** `everyone` — всем; `admins` — когда у хода `viewer.isAdmin` (`agent.viewer` ядра, права из базы
|
|
67
|
+
приложения, один раз в начале хода); `off` — никому. Список и все вызовы `use_skill` хода решают по одним правам.
|
|
68
|
+
Групповой ход видит только `everyone`. В копии чата человека у сотрудника права человека, если `viewer` считает
|
|
69
|
+
права по `ownerId` (в копии `userId` — сотрудник): он видит то же, что увидел бы человек. Нет `agent.viewer` —
|
|
70
|
+
`admins` не видит никто, в журнал одна подсказка. Невидимый навык инструмент не отдаёт: ответ как про несуществующий.
|
|
71
|
+
- **Доступ к экрану.** `resolveUser` → `isAdmin` пакета на каждом запросе. Не админ получает 404 на любом адресе, экран
|
|
72
|
+
показывает «не найдено». Правки — только со своего сайта и сайтов из `allowedOrigins`.
|
|
73
|
+
- **Одновременная правка.** Правка и откат несут `expectedVersion`; навык успели изменить — 409, экран предлагает
|
|
74
|
+
загрузить свежую версию. Откат проверяется нынешними правилами: старая версия длиннее нынешнего `maxBodyChars` — 400.
|
|
75
|
+
- **Черновик.** Уход к другому навыку, «Новый» или откат при несохранённых правках сперва спрашивает
|
|
76
|
+
(`discardingDraft` в `SkillEditor`); закрытие вкладки предупреждает браузером.
|
|
77
|
+
- **Предел запроса.** Считается от `maxBodyChars` с запасом, не меньше 256 КБ; больше — 413.
|
|
78
|
+
- **Учёт.** Каждая загрузка: навык, версия, чат, человек, ход, время; строка `skills: loaded "…" vN chat=… owner=… turn=…`
|
|
79
|
+
в журнал сервера. Сбой записи не ломает ход.
|
|
80
|
+
- **Хранилища.** Память (`memorySkillStore`, тесты), SQLite, PostgreSQL (схема `agent_chat`). Таблицы `skills`,
|
|
81
|
+
`skill_versions`, `skill_loads`, миграции сами при открытии. Список для вопроса модели и экрана — `listBrief`, без
|
|
82
|
+
тел. Символ NUL все хранилища убирают при записи (PostgreSQL его не хранит), проверка «не пусто» — после чистки.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/store/migration-settings.ts"],"sourcesContent":["/** Своя таблица учёта: хранилище чатов, песочница и фон живут в той же базе и ведут свои. */\nexport const SKILLS_MIGRATIONS_TABLE = \"__skills_migrations\";\n\n/** Постоянное число для блокировки базы миграциями навыков; не совпадает с ключами ядра и других пакетов (…442–447). */\nexport const SKILLS_MIGRATION_LOCK_KEY = 7_311_900_448;\n"],"mappings":";AACO,IAAM,0BAA0B;AAGhC,IAAM,4BAA4B;","names":[]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SKILL_STATUSES,
|
|
3
|
+
withoutNul
|
|
4
|
+
} from "./chunk-X36FOCSA.js";
|
|
5
|
+
|
|
6
|
+
// src/skill-rules.ts
|
|
7
|
+
var SKILL_NAME_PATTERN = /^[a-zа-яё0-9][a-zа-яё0-9-]{1,47}$/;
|
|
8
|
+
var MAX_WHEN_TO_USE_CHARS = 300;
|
|
9
|
+
var DEFAULT_MAX_BODY_CHARS = 5e4;
|
|
10
|
+
var InvalidSkillError = class extends Error {
|
|
11
|
+
constructor(field, reason) {
|
|
12
|
+
super(`Invalid skill ${field}: ${reason}`);
|
|
13
|
+
this.field = field;
|
|
14
|
+
this.reason = reason;
|
|
15
|
+
this.name = "InvalidSkillError";
|
|
16
|
+
}
|
|
17
|
+
field;
|
|
18
|
+
reason;
|
|
19
|
+
};
|
|
20
|
+
function checkSkillName(name) {
|
|
21
|
+
if (!SKILL_NAME_PATTERN.test(name)) throw new InvalidSkillError("name", "name_format");
|
|
22
|
+
}
|
|
23
|
+
var LINE_BREAK = /[\n\v\f\r\u0085\u2028\u2029]/;
|
|
24
|
+
function checkSkillText(text, maxBodyChars) {
|
|
25
|
+
const whenToUse = withoutNul(text.whenToUse).trim();
|
|
26
|
+
const body = withoutNul(text.body);
|
|
27
|
+
if (whenToUse === "") throw new InvalidSkillError("whenToUse", "when_to_use_empty");
|
|
28
|
+
if (LINE_BREAK.test(whenToUse)) throw new InvalidSkillError("whenToUse", "when_to_use_multiline");
|
|
29
|
+
if (whenToUse.length > MAX_WHEN_TO_USE_CHARS) throw new InvalidSkillError("whenToUse", "when_to_use_too_long");
|
|
30
|
+
if (body.trim() === "") throw new InvalidSkillError("body", "body_empty");
|
|
31
|
+
if (body.length > maxBodyChars) throw new InvalidSkillError("body", "body_too_long");
|
|
32
|
+
return { whenToUse, body };
|
|
33
|
+
}
|
|
34
|
+
function checkSkillStatus(status) {
|
|
35
|
+
if (!SKILL_STATUSES.includes(status)) throw new InvalidSkillError("status", "status_unknown");
|
|
36
|
+
return status;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
SKILL_NAME_PATTERN,
|
|
41
|
+
MAX_WHEN_TO_USE_CHARS,
|
|
42
|
+
DEFAULT_MAX_BODY_CHARS,
|
|
43
|
+
InvalidSkillError,
|
|
44
|
+
checkSkillName,
|
|
45
|
+
checkSkillText,
|
|
46
|
+
checkSkillStatus
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=chunk-T5CV2V6H.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/skill-rules.ts"],"sourcesContent":["import { SKILL_STATUSES } from \"./types\";\nimport type { SkillStatus } from \"./types\";\nimport { withoutNul } from \"./store/skill-store\";\n\n/** Имя: строчные латинские или русские буквы, цифры, дефис; 2–48 знаков; первый знак — не дефис. */\nexport const SKILL_NAME_PATTERN = /^[a-zа-яё0-9][a-zа-яё0-9-]{1,47}$/;\nexport const MAX_WHEN_TO_USE_CHARS = 300;\nexport const DEFAULT_MAX_BODY_CHARS = 50_000;\n\nexport type InvalidSkillReason =\n | \"name_format\"\n | \"when_to_use_empty\"\n | \"when_to_use_too_long\"\n | \"when_to_use_multiline\"\n | \"body_empty\"\n | \"body_too_long\"\n | \"status_unknown\";\n\n/** Навык не прошёл проверку: какое поле и почему. */\nexport class InvalidSkillError extends Error {\n constructor(\n readonly field: \"name\" | \"whenToUse\" | \"body\" | \"status\",\n readonly reason: InvalidSkillReason,\n ) {\n super(`Invalid skill ${field}: ${reason}`);\n this.name = \"InvalidSkillError\";\n }\n}\n\nexport function checkSkillName(name: string): void {\n if (!SKILL_NAME_PATTERN.test(name)) throw new InvalidSkillError(\"name\", \"name_format\");\n}\n\n/** Любой перевод строки, включая юникодные: `whenToUse` — одна строка списка в вопросе модели. */\nconst LINE_BREAK = /[\\n\\v\\f\\r\\u0085\\u2028\\u2029]/;\n\n/** Проверяет и чистит текст: без символа NUL (его не хранит ни одно хранилище), `whenToUse` без пробелов по краям. */\nexport function checkSkillText(text: { whenToUse: string; body: string }, maxBodyChars: number): { whenToUse: string; body: string } {\n const whenToUse = withoutNul(text.whenToUse).trim();\n const body = withoutNul(text.body);\n if (whenToUse === \"\") throw new InvalidSkillError(\"whenToUse\", \"when_to_use_empty\");\n if (LINE_BREAK.test(whenToUse)) throw new InvalidSkillError(\"whenToUse\", \"when_to_use_multiline\");\n if (whenToUse.length > MAX_WHEN_TO_USE_CHARS) throw new InvalidSkillError(\"whenToUse\", \"when_to_use_too_long\");\n if (body.trim() === \"\") throw new InvalidSkillError(\"body\", \"body_empty\");\n if (body.length > maxBodyChars) throw new InvalidSkillError(\"body\", \"body_too_long\");\n return { whenToUse, body };\n}\n\nexport function checkSkillStatus(status: string): SkillStatus {\n if (!(SKILL_STATUSES as readonly string[]).includes(status)) throw new InvalidSkillError(\"status\", \"status_unknown\");\n return status as SkillStatus;\n}\n"],"mappings":";;;;;;AAKO,IAAM,qBAAqB;AAC3B,IAAM,wBAAwB;AAC9B,IAAM,yBAAyB;AAY/B,IAAM,oBAAN,cAAgC,MAAM;AAAA,EAC3C,YACW,OACA,QACT;AACA,UAAM,iBAAiB,KAAK,KAAK,MAAM,EAAE;AAHhC;AACA;AAGT,SAAK,OAAO;AAAA,EACd;AAAA,EALW;AAAA,EACA;AAKb;AAEO,SAAS,eAAe,MAAoB;AACjD,MAAI,CAAC,mBAAmB,KAAK,IAAI,EAAG,OAAM,IAAI,kBAAkB,QAAQ,aAAa;AACvF;AAGA,IAAM,aAAa;AAGZ,SAAS,eAAe,MAA2C,cAA2D;AACnI,QAAM,YAAY,WAAW,KAAK,SAAS,EAAE,KAAK;AAClD,QAAM,OAAO,WAAW,KAAK,IAAI;AACjC,MAAI,cAAc,GAAI,OAAM,IAAI,kBAAkB,aAAa,mBAAmB;AAClF,MAAI,WAAW,KAAK,SAAS,EAAG,OAAM,IAAI,kBAAkB,aAAa,uBAAuB;AAChG,MAAI,UAAU,SAAS,sBAAuB,OAAM,IAAI,kBAAkB,aAAa,sBAAsB;AAC7G,MAAI,KAAK,KAAK,MAAM,GAAI,OAAM,IAAI,kBAAkB,QAAQ,YAAY;AACxE,MAAI,KAAK,SAAS,aAAc,OAAM,IAAI,kBAAkB,QAAQ,eAAe;AACnF,SAAO,EAAE,WAAW,KAAK;AAC3B;AAEO,SAAS,iBAAiB,QAA6B;AAC5D,MAAI,CAAE,eAAqC,SAAS,MAAM,EAAG,OAAM,IAAI,kBAAkB,UAAU,gBAAgB;AACnH,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var SKILL_STATUSES = ["off", "admins", "everyone"];
|
|
3
|
+
var SkillExistsError = class extends Error {
|
|
4
|
+
constructor(skillName) {
|
|
5
|
+
super(`Skill "${skillName}" already exists`);
|
|
6
|
+
this.skillName = skillName;
|
|
7
|
+
this.name = "SkillExistsError";
|
|
8
|
+
}
|
|
9
|
+
skillName;
|
|
10
|
+
};
|
|
11
|
+
var SkillNotFoundError = class extends Error {
|
|
12
|
+
constructor(skillName) {
|
|
13
|
+
super(`Skill "${skillName}" not found`);
|
|
14
|
+
this.skillName = skillName;
|
|
15
|
+
this.name = "SkillNotFoundError";
|
|
16
|
+
}
|
|
17
|
+
skillName;
|
|
18
|
+
};
|
|
19
|
+
var SkillVersionConflictError = class extends Error {
|
|
20
|
+
constructor(skillName, currentVersion) {
|
|
21
|
+
super(`Skill "${skillName}" changed: current version is ${currentVersion}`);
|
|
22
|
+
this.skillName = skillName;
|
|
23
|
+
this.currentVersion = currentVersion;
|
|
24
|
+
this.name = "SkillVersionConflictError";
|
|
25
|
+
}
|
|
26
|
+
skillName;
|
|
27
|
+
currentVersion;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// src/store/skill-store.ts
|
|
31
|
+
function byName(left, right) {
|
|
32
|
+
return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
|
|
33
|
+
}
|
|
34
|
+
function sameText(skill, edit) {
|
|
35
|
+
return skill.whenToUse === edit.whenToUse && skill.body === edit.body;
|
|
36
|
+
}
|
|
37
|
+
function withoutNul(text) {
|
|
38
|
+
return text.replaceAll("\0", "");
|
|
39
|
+
}
|
|
40
|
+
function storedText(text) {
|
|
41
|
+
return { whenToUse: withoutNul(text.whenToUse), body: withoutNul(text.body) };
|
|
42
|
+
}
|
|
43
|
+
function brief({ body: _body, ...skill }) {
|
|
44
|
+
return skill;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
SKILL_STATUSES,
|
|
49
|
+
SkillExistsError,
|
|
50
|
+
SkillNotFoundError,
|
|
51
|
+
SkillVersionConflictError,
|
|
52
|
+
byName,
|
|
53
|
+
sameText,
|
|
54
|
+
withoutNul,
|
|
55
|
+
storedText,
|
|
56
|
+
brief
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=chunk-X36FOCSA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/store/skill-store.ts"],"sourcesContent":["/** Кому виден навык: никому, только админам, всем. */\nexport type SkillStatus = \"off\" | \"admins\" | \"everyone\";\n\nexport const SKILL_STATUSES: readonly SkillStatus[] = [\"off\", \"admins\", \"everyone\"];\n\n/** Навык: текущая версия и статус. */\nexport interface Skill {\n /** Короткое уникальное имя, не меняется. */\n name: string;\n /** Когда брать навык: 1–2 фразы, одна строка. */\n whenToUse: string;\n /** Указания в markdown. */\n body: string;\n status: SkillStatus;\n /** Номер текущей версии текста, с 1. */\n version: number;\n createdBy: string;\n createdAt: string;\n updatedBy: string;\n updatedAt: string;\n}\n\n/** Навык без тела: для списка в вопросе модели и на экране, тела не читаются из базы. */\nexport type SkillBrief = Omit<Skill, \"body\">;\n\n/** Версия текста навыка: что было после правки. Статус версий не создаёт. */\nexport interface SkillVersion {\n name: string;\n version: number;\n whenToUse: string;\n body: string;\n author: string;\n createdAt: string;\n}\n\n/** Загрузка тела навыка агентом. */\nexport interface SkillLoad {\n name: string;\n version: number;\n chatId: string;\n ownerId: string;\n /** Ход — id ответа агента. */\n turnId: string;\n loadedAt: string;\n}\n\n/** Учёт загрузок навыка: всего и последние. */\nexport interface SkillLoadStats {\n total: number;\n /** Новые сверху. */\n recent: SkillLoad[];\n}\n\nexport interface NewSkill {\n name: string;\n whenToUse: string;\n body: string;\n status: SkillStatus;\n author: string;\n}\n\nexport interface SkillTextEdit {\n whenToUse: string;\n body: string;\n author: string;\n /** Версия, которую правили: другая в базе — `SkillVersionConflictError`. */\n expectedVersion: number;\n}\n\n/**\n * Хранилище навыков: память, SQLite, PostgreSQL ведут себя одинаково (общий контрактный тест). Время передаёт\n * вызывающий (`at`, ISO): часы — у службы навыков. Символ NUL в тексте не хранится: запись его убирает.\n */\nexport interface SkillStore {\n /** Все навыки по имени. */\n list(): Promise<Skill[]>;\n /** Все навыки по имени, без тел. */\n listBrief(): Promise<SkillBrief[]>;\n get(name: string): Promise<Skill | null>;\n /** Новый навык с версией 1. Имя занято — `SkillExistsError`. */\n create(skill: NewSkill, at: string): Promise<Skill>;\n /**\n * Правка текста: новая версия, если текст другой, иначе навык как есть. Нет навыка — `SkillNotFoundError`;\n * версия не та — `SkillVersionConflictError`. Проверка и запись — одним действием базы.\n */\n updateText(name: string, edit: SkillTextEdit, at: string): Promise<Skill>;\n /** Смена статуса без новой версии. Нет навыка — `SkillNotFoundError`. */\n setStatus(name: string, status: SkillStatus, author: string, at: string): Promise<Skill>;\n /** Удаляет навык, его версии и учёт загрузок. Был — `true`. */\n delete(name: string): Promise<boolean>;\n /** Версии навыка, новые сверху. */\n listVersions(name: string): Promise<SkillVersion[]>;\n getVersion(name: string, version: number): Promise<SkillVersion | null>;\n recordLoad(load: SkillLoad): Promise<void>;\n /** Учёт загрузок: всего и `limit` последних. */\n loadStats(name: string, limit: number): Promise<SkillLoadStats>;\n /** Сколько раз загружали каждый навык: имя → число. Навыков без загрузок в ответе нет. */\n loadCounts(): Promise<Record<string, number>>;\n}\n\nexport class SkillExistsError extends Error {\n constructor(readonly skillName: string) {\n super(`Skill \"${skillName}\" already exists`);\n this.name = \"SkillExistsError\";\n }\n}\n\nexport class SkillNotFoundError extends Error {\n constructor(readonly skillName: string) {\n super(`Skill \"${skillName}\" not found`);\n this.name = \"SkillNotFoundError\";\n }\n}\n\nexport class SkillVersionConflictError extends Error {\n constructor(\n readonly skillName: string,\n readonly currentVersion: number,\n ) {\n super(`Skill \"${skillName}\" changed: current version is ${currentVersion}`);\n this.name = \"SkillVersionConflictError\";\n }\n}\n","import type { Skill, SkillBrief } from \"../types\";\n\n/** Порядок по имени — по кодам знаков: одинаковый в памяти, SQLite и PostgreSQL при любой локали базы. */\nexport function byName<T extends { name: string }>(left: T, right: T): number {\n return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;\n}\n\n/** Текст навыка не изменился: новая версия не нужна. */\nexport function sameText(skill: Skill, edit: { whenToUse: string; body: string }): boolean {\n return skill.whenToUse === edit.whenToUse && skill.body === edit.body;\n}\n\n/** PostgreSQL не хранит символ NUL в тексте; все хранилища убирают его до записи, чтобы вести себя одинаково. */\nexport function withoutNul(text: string): string {\n return text.replaceAll(\"\\u0000\", \"\");\n}\n\n/** Текст навыка без символа NUL. */\nexport function storedText(text: { whenToUse: string; body: string }): { whenToUse: string; body: string } {\n return { whenToUse: withoutNul(text.whenToUse), body: withoutNul(text.body) };\n}\n\n/** Навык без тела. */\nexport function brief({ body: _body, ...skill }: Skill): SkillBrief {\n return skill;\n}\n"],"mappings":";AAGO,IAAM,iBAAyC,CAAC,OAAO,UAAU,UAAU;AAiG3E,IAAM,mBAAN,cAA+B,MAAM;AAAA,EAC1C,YAAqB,WAAmB;AACtC,UAAM,UAAU,SAAS,kBAAkB;AADxB;AAEnB,SAAK,OAAO;AAAA,EACd;AAAA,EAHqB;AAIvB;AAEO,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAC5C,YAAqB,WAAmB;AACtC,UAAM,UAAU,SAAS,aAAa;AADnB;AAEnB,SAAK,OAAO;AAAA,EACd;AAAA,EAHqB;AAIvB;AAEO,IAAM,4BAAN,cAAwC,MAAM;AAAA,EACnD,YACW,WACA,gBACT;AACA,UAAM,UAAU,SAAS,iCAAiC,cAAc,EAAE;AAHjE;AACA;AAGT,SAAK,OAAO;AAAA,EACd;AAAA,EALW;AAAA,EACA;AAKb;;;ACvHO,SAAS,OAAmC,MAAS,OAAkB;AAC5E,SAAO,KAAK,OAAO,MAAM,OAAO,KAAK,KAAK,OAAO,MAAM,OAAO,IAAI;AACpE;AAGO,SAAS,SAAS,OAAc,MAAoD;AACzF,SAAO,MAAM,cAAc,KAAK,aAAa,MAAM,SAAS,KAAK;AACnE;AAGO,SAAS,WAAW,MAAsB;AAC/C,SAAO,KAAK,WAAW,MAAU,EAAE;AACrC;AAGO,SAAS,WAAW,MAAgF;AACzG,SAAO,EAAE,WAAW,WAAW,KAAK,SAAS,GAAG,MAAM,WAAW,KAAK,IAAI,EAAE;AAC9E;AAGO,SAAS,MAAM,EAAE,MAAM,OAAO,GAAG,MAAM,GAAsB;AAClE,SAAO;AACT;","names":[]}
|