team-chat 0.0.0-stage → 0.3.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.
Files changed (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +458 -2
  3. package/agents/rules.md +52 -0
  4. package/agents/runtimes/codex.md +14 -0
  5. package/bin/bridge +3 -0
  6. package/bin/runner +3 -0
  7. package/cli/lib/agents.js +97 -0
  8. package/cli/lib/docs.js +151 -0
  9. package/cli/lib/local.js +41 -0
  10. package/cli/lib/server.js +52 -0
  11. package/cli/lib/util.js +107 -0
  12. package/cli/team-chat.js +383 -0
  13. package/host/bridge/attempts.js +43 -0
  14. package/host/bridge/bridge.js +326 -0
  15. package/host/bridge/config.js +105 -0
  16. package/host/bridge/knowledge-outbox.js +96 -0
  17. package/host/bridge/launcher.js +126 -0
  18. package/host/bridge/main.js +87 -0
  19. package/host/bridge/progress.js +74 -0
  20. package/host/bridge/prompt.js +97 -0
  21. package/host/bridge/run-archive.js +200 -0
  22. package/host/bridge/thread.js +44 -0
  23. package/host/bridge/triage.js +97 -0
  24. package/host/bridge/workdir.js +41 -0
  25. package/host/knowledge/card.js +43 -0
  26. package/host/knowledge/errors.js +50 -0
  27. package/host/knowledge/lesson.js +153 -0
  28. package/host/knowledge/process.js +160 -0
  29. package/host/knowledge/scrub.js +29 -0
  30. package/host/knowledge/shell.js +74 -0
  31. package/host/knowledge/stack.js +229 -0
  32. package/host/knowledge/transcript.js +82 -0
  33. package/host/lib/chat-api.js +120 -0
  34. package/host/lib/log.js +6 -0
  35. package/host/lib/pid-lock.js +65 -0
  36. package/host/lib/shell-words.js +45 -0
  37. package/host/runner/bridge-process.js +82 -0
  38. package/host/runner/config.js +81 -0
  39. package/host/runner/main.js +69 -0
  40. package/host/runner/retention.js +40 -0
  41. package/host/runner/runner.js +161 -0
  42. package/package.json +67 -5
  43. package/server/dist/src/app.js +25 -0
  44. package/server/dist/src/app.module.js +50 -0
  45. package/server/dist/src/attachments/attachment-store.js +111 -0
  46. package/server/dist/src/attachments/attachments.controller.js +67 -0
  47. package/server/dist/src/attachments/mime.js +84 -0
  48. package/server/dist/src/attachments/uploads.interceptor.js +72 -0
  49. package/server/dist/src/auth/participant.guard.js +53 -0
  50. package/server/dist/src/bridges/bridge-restart.service.js +80 -0
  51. package/server/dist/src/bridges/bridges.controller.js +47 -0
  52. package/server/dist/src/chats/chats.controller.js +83 -0
  53. package/server/dist/src/chats/chats.service.js +128 -0
  54. package/server/dist/src/collector/collector.controller.js +71 -0
  55. package/server/dist/src/collector/collector.module.js +23 -0
  56. package/server/dist/src/collector/collector.service.js +111 -0
  57. package/server/dist/src/common/clock.js +20 -0
  58. package/server/dist/src/common/errors.js +37 -0
  59. package/server/dist/src/common/format.js +14 -0
  60. package/server/dist/src/common/http.js +17 -0
  61. package/server/dist/src/common/slug.js +28 -0
  62. package/server/dist/src/common/tokens.js +19 -0
  63. package/server/dist/src/common/validation.js +41 -0
  64. package/server/dist/src/config/config.js +39 -0
  65. package/server/dist/src/conversation.module.js +41 -0
  66. package/server/dist/src/db/database.js +73 -0
  67. package/server/dist/src/db/database.module.js +51 -0
  68. package/server/dist/src/db/schema.js +212 -0
  69. package/server/dist/src/decisions/decisions.controller.js +55 -0
  70. package/server/dist/src/decisions/decisions.service.js +109 -0
  71. package/server/dist/src/documents/documents.controller.js +87 -0
  72. package/server/dist/src/documents/documents.service.js +189 -0
  73. package/server/dist/src/health/health.controller.js +29 -0
  74. package/server/dist/src/knowledge/knowledge.controller.js +83 -0
  75. package/server/dist/src/knowledge/knowledge.module.js +25 -0
  76. package/server/dist/src/knowledge/knowledge.service.js +189 -0
  77. package/server/dist/src/local/docs-sync.service.js +97 -0
  78. package/server/dist/src/local/local.controller.js +197 -0
  79. package/server/dist/src/local/local.guard.js +55 -0
  80. package/server/dist/src/local/local.module.js +30 -0
  81. package/server/dist/src/local/setup.service.js +154 -0
  82. package/server/dist/src/local/transfer.service.js +151 -0
  83. package/server/dist/src/main.js +33 -0
  84. package/server/dist/src/mcp/chat-tools.js +282 -0
  85. package/server/dist/src/mcp/mcp.controller.js +58 -0
  86. package/server/dist/src/mcp/mcp.module.js +24 -0
  87. package/server/dist/src/mentions/mention-state.js +80 -0
  88. package/server/dist/src/mentions/mentions.controller.js +100 -0
  89. package/server/dist/src/mentions/mentions.service.js +203 -0
  90. package/server/dist/src/messages/message.presenter.js +102 -0
  91. package/server/dist/src/messages/messages.service.js +149 -0
  92. package/server/dist/src/messages/thread.controller.js +179 -0
  93. package/server/dist/src/participants/directory.service.js +68 -0
  94. package/server/dist/src/participants/participants.module.js +24 -0
  95. package/server/dist/src/participants/participants.service.js +84 -0
  96. package/server/dist/src/settings/settings.service.js +42 -0
  97. package/server/dist/src/sharing/consent.js +16 -0
  98. package/server/dist/src/sharing/sharing.module.js +22 -0
  99. package/server/dist/src/sharing/sharing.service.js +175 -0
  100. package/server/dist/src/team/runner.service.js +75 -0
  101. package/server/dist/src/team/team.controller.js +178 -0
  102. package/server/dist/src/team/team.module.js +25 -0
  103. package/server/dist/src/team/team.service.js +280 -0
  104. package/server/dist/src/web/template.js +57 -0
  105. package/server/dist/src/web/web-login.service.js +74 -0
  106. package/server/dist/src/web/web.controller.js +203 -0
  107. package/server/dist/src/web/web.module.js +25 -0
  108. package/server/drizzle/0000_init.sql +169 -0
  109. package/server/drizzle/0001_seed_roles.sql +25 -0
  110. package/server/drizzle/0002_knowledge.sql +17 -0
  111. package/server/drizzle/0003_sharing.sql +30 -0
  112. package/server/drizzle/meta/0000_snapshot.json +1224 -0
  113. package/server/drizzle/meta/0001_snapshot.json +1224 -0
  114. package/server/drizzle/meta/0002_snapshot.json +1359 -0
  115. package/server/drizzle/meta/0003_snapshot.json +1545 -0
  116. package/server/drizzle/meta/_journal.json +34 -0
  117. package/server/package.json +1 -0
  118. package/server/views/chat.html +865 -0
  119. package/server/views/documents.html +204 -0
  120. package/server/views/login.html +31 -0
  121. package/server/views/setup.html +60 -0
  122. package/server/views/team.html +369 -0
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.DirectoryService = exports.Directory = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const database_module_1 = require("../db/database.module");
18
+ const schema_1 = require("../db/schema");
19
+ const participants_service_1 = require("./participants.service");
20
+ /**
21
+ * Снимок ролей: какие роли есть и кому какая роль дана в каком чате. Роль нужна на
22
+ * каждом сообщении ленты, поэтому снимок берётся одним разом на запрос и дальше
23
+ * читается из памяти.
24
+ */
25
+ class Directory {
26
+ rolesBySlug;
27
+ overrides;
28
+ constructor(rolesBySlug, overrides) {
29
+ this.rolesBySlug = rolesBySlug;
30
+ this.overrides = overrides;
31
+ }
32
+ /** Роль участника в чате: замена для этого чата, иначе основная. */
33
+ roleIn(participant, chatId) {
34
+ return this.overrides.get(`${chatId}:${participant.id}`) ?? participant.role;
35
+ }
36
+ /** Название роли; у владельца — «владелец», у служебных и неизвестных — сам слаг. */
37
+ roleName(slug) {
38
+ if (slug === participants_service_1.ROLE_OWNER)
39
+ return 'владелец';
40
+ return this.rolesBySlug.get(slug)?.name ?? slug;
41
+ }
42
+ roleApi(slug) {
43
+ const role = this.rolesBySlug.get(slug);
44
+ return role
45
+ ? { slug: role.slug, name: role.name, instructions: role.instructions }
46
+ : { slug, name: slug, instructions: null };
47
+ }
48
+ }
49
+ exports.Directory = Directory;
50
+ let DirectoryService = class DirectoryService {
51
+ db;
52
+ constructor(db) {
53
+ this.db = db;
54
+ }
55
+ async load() {
56
+ const [allRoles, allOverrides] = await Promise.all([
57
+ this.db.select().from(schema_1.roles),
58
+ this.db.select().from(schema_1.chatAgentRoles),
59
+ ]);
60
+ return new Directory(new Map(allRoles.map(r => [r.slug, r])), new Map(allOverrides.map(o => [`${o.chatId}:${o.participantId}`, o.role])));
61
+ }
62
+ };
63
+ exports.DirectoryService = DirectoryService;
64
+ exports.DirectoryService = DirectoryService = __decorate([
65
+ (0, common_1.Injectable)(),
66
+ __param(0, (0, common_1.Inject)(database_module_1.DATABASE)),
67
+ __metadata("design:paramtypes", [Object])
68
+ ], DirectoryService);
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.ParticipantsModule = void 0;
10
+ const common_1 = require("@nestjs/common");
11
+ const participant_guard_1 = require("../auth/participant.guard");
12
+ const directory_service_1 = require("./directory.service");
13
+ const participants_service_1 = require("./participants.service");
14
+ /** Участники, их токены и роли — нужны почти каждому модулю, поэтому глобальный. */
15
+ let ParticipantsModule = class ParticipantsModule {
16
+ };
17
+ exports.ParticipantsModule = ParticipantsModule;
18
+ exports.ParticipantsModule = ParticipantsModule = __decorate([
19
+ (0, common_1.Global)(),
20
+ (0, common_1.Module)({
21
+ providers: [participants_service_1.ParticipantsService, directory_service_1.DirectoryService, participant_guard_1.ParticipantGuard],
22
+ exports: [participants_service_1.ParticipantsService, directory_service_1.DirectoryService, participant_guard_1.ParticipantGuard],
23
+ })
24
+ ], ParticipantsModule);
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.ParticipantsService = exports.isMember = exports.isHost = exports.isOwner = exports.HANDLE_PATTERN = exports.KIND_HOST = exports.ROLE_HOST = exports.ROLE_OWNER = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const drizzle_orm_1 = require("drizzle-orm");
18
+ const clock_1 = require("../common/clock");
19
+ const tokens_1 = require("../common/tokens");
20
+ const database_module_1 = require("../db/database.module");
21
+ const schema_1 = require("../db/schema");
22
+ exports.ROLE_OWNER = 'owner';
23
+ /** Раннер на хосте: запускает агентов, сам в разговоре не участвует. */
24
+ exports.ROLE_HOST = 'host';
25
+ exports.KIND_HOST = 'host';
26
+ exports.HANDLE_PATTERN = /^[a-z0-9][a-z0-9_-]{1,39}$/;
27
+ const isOwner = (p) => p.role === exports.ROLE_OWNER;
28
+ exports.isOwner = isOwner;
29
+ const isHost = (p) => p.kind === exports.KIND_HOST;
30
+ exports.isHost = isHost;
31
+ /** Те, кто в разговоре: люди и агенты, без раннеров. */
32
+ const isMember = (p) => p.kind !== exports.KIND_HOST;
33
+ exports.isMember = isMember;
34
+ /** Участники и их токены. */
35
+ let ParticipantsService = class ParticipantsService {
36
+ db;
37
+ constructor(db) {
38
+ this.db = db;
39
+ }
40
+ /** Участник по токену — постоянному или дополнительному (раннер, браузер). */
41
+ async byToken(plain) {
42
+ const hash = (0, tokens_1.hashToken)(plain);
43
+ const [own] = await this.db.select().from(schema_1.participants).where((0, drizzle_orm_1.eq)(schema_1.participants.tokenHash, hash));
44
+ if (own)
45
+ return own;
46
+ const [extra] = await this.db.select({ participant: schema_1.participants })
47
+ .from(schema_1.participantTokens)
48
+ .innerJoin(schema_1.participants, (0, drizzle_orm_1.eq)(schema_1.participants.id, schema_1.participantTokens.participantId))
49
+ .where((0, drizzle_orm_1.eq)(schema_1.participantTokens.tokenHash, hash));
50
+ return extra?.participant ?? null;
51
+ }
52
+ /**
53
+ * Выпустить постоянный токен. Открытое значение показывается ровно один раз — у нас
54
+ * остаётся только хеш. Повторный выпуск отзывает прежний.
55
+ */
56
+ async issueToken(participantId) {
57
+ const plain = (0, tokens_1.randomToken)();
58
+ await this.db.update(schema_1.participants).set({ tokenHash: (0, tokens_1.hashToken)(plain) }).where((0, drizzle_orm_1.eq)(schema_1.participants.id, participantId));
59
+ return plain;
60
+ }
61
+ /**
62
+ * Дополнительный токен с меткой. Прежние с той же меткой отзываются: раннер
63
+ * перезапустился и взял новые — старые больше не нужны никому.
64
+ */
65
+ async issueExtraToken(participantId, label) {
66
+ const plain = (0, tokens_1.randomToken)();
67
+ await this.db.transaction(async (tx) => {
68
+ await tx.delete(schema_1.participantTokens)
69
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.participantTokens.participantId, participantId), (0, drizzle_orm_1.eq)(schema_1.participantTokens.label, label)));
70
+ await tx.insert(schema_1.participantTokens).values({ participantId, tokenHash: (0, tokens_1.hashToken)(plain), label });
71
+ });
72
+ return plain;
73
+ }
74
+ /** «Был на связи»: мост опрашивает чат своим токеном — по этому видно, жив ли он. */
75
+ async touchSeen(participantId) {
76
+ await this.db.update(schema_1.participants).set({ lastSeenAt: (0, clock_1.now)() }).where((0, drizzle_orm_1.eq)(schema_1.participants.id, participantId));
77
+ }
78
+ };
79
+ exports.ParticipantsService = ParticipantsService;
80
+ exports.ParticipantsService = ParticipantsService = __decorate([
81
+ (0, common_1.Injectable)(),
82
+ __param(0, (0, common_1.Inject)(database_module_1.DATABASE)),
83
+ __metadata("design:paramtypes", [Object])
84
+ ], ParticipantsService);
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.SettingsService = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const drizzle_orm_1 = require("drizzle-orm");
18
+ const database_module_1 = require("../db/database.module");
19
+ const schema_1 = require("../db/schema");
20
+ /** Настройки этой установки — ключ и значение JSON. Дампом не переносятся: они про машину. */
21
+ let SettingsService = class SettingsService {
22
+ db;
23
+ constructor(db) {
24
+ this.db = db;
25
+ }
26
+ async get(key) {
27
+ const [row] = await this.db.select().from(schema_1.settings).where((0, drizzle_orm_1.eq)(schema_1.settings.key, key));
28
+ return row?.value ?? null;
29
+ }
30
+ async set(key, value) {
31
+ await this.db.insert(schema_1.settings).values({ key, value }).onConflictDoUpdate({ target: schema_1.settings.key, set: { value } });
32
+ }
33
+ async remove(key) {
34
+ await this.db.delete(schema_1.settings).where((0, drizzle_orm_1.eq)(schema_1.settings.key, key));
35
+ }
36
+ };
37
+ exports.SettingsService = SettingsService;
38
+ exports.SettingsService = SettingsService = __decorate([
39
+ (0, common_1.Injectable)(),
40
+ __param(0, (0, common_1.Inject)(database_module_1.DATABASE)),
41
+ __metadata("design:paramtypes", [Object])
42
+ ], SettingsService);
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ // Согласие на отправку опыта автору пакета. Текст меняется — растёт версия, и согласие
3
+ // спрашивается заново: человек соглашался на то, что прочёл, а не на то, что написали потом.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.consentGiven = exports.CONSENT_TEXT = exports.CONSENT_VERSION = void 0;
6
+ exports.CONSENT_VERSION = 1;
7
+ exports.CONSENT_TEXT = `Помочь развитию team-chat?
8
+ Ваши агенты копят опыт: какие задачи решали, что сработало, где ошибались. Этот опыт
9
+ сохраняется в вашем проекте, и в следующий раз агент возьмёт готовое решение, а не будет
10
+ искать его заново.
11
+ С вашего согласия обезличенная копия этого опыта уйдёт автору пакета — на развитие агента
12
+ team-chat. Код и файлы ваших проектов не отправляются никогда.
13
+ Отключить можно в любой момент: team-chat consent off`;
14
+ /** Согласие действует, только если дано на текущий текст. */
15
+ const consentGiven = (consent) => consent?.answer === true && consent.version === exports.CONSENT_VERSION;
16
+ exports.consentGiven = consentGiven;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.SharingModule = void 0;
10
+ const common_1 = require("@nestjs/common");
11
+ const settings_service_1 = require("../settings/settings.service");
12
+ const sharing_service_1 = require("./sharing.service");
13
+ /** Отправка опыта автору пакета — с согласия владельца этой установки. */
14
+ let SharingModule = class SharingModule {
15
+ };
16
+ exports.SharingModule = SharingModule;
17
+ exports.SharingModule = SharingModule = __decorate([
18
+ (0, common_1.Module)({
19
+ providers: [settings_service_1.SettingsService, sharing_service_1.SharingService],
20
+ exports: [sharing_service_1.SharingService],
21
+ })
22
+ ], SharingModule);
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.SharingService = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const drizzle_orm_1 = require("drizzle-orm");
18
+ const zod_1 = require("zod");
19
+ const clock_1 = require("../common/clock");
20
+ const errors_1 = require("../common/errors");
21
+ const tokens_1 = require("../common/tokens");
22
+ const validation_1 = require("../common/validation");
23
+ const config_1 = require("../config/config");
24
+ const database_module_1 = require("../db/database.module");
25
+ const schema_1 = require("../db/schema");
26
+ const collector_service_1 = require("../collector/collector.service");
27
+ const settings_service_1 = require("../settings/settings.service");
28
+ const consent_1 = require("./consent");
29
+ const CONSENT = 'consent';
30
+ /** Эта установка у сборщика: его адрес, токен и соль, которой обезличиваются ключи запусков. */
31
+ const INSTALLATION = 'collector_installation';
32
+ const INTERVAL_MS = 5 * 60_000;
33
+ const TIMEOUT_MS = 30_000;
34
+ const consentSchema = zod_1.z.object({ answer: zod_1.z.boolean() });
35
+ /** Сборщик не ответил или отказал. upstream — его код ответа, null — не достучались. */
36
+ class CollectorUnavailable extends errors_1.ApiError {
37
+ upstream;
38
+ constructor(upstream, reason) {
39
+ super(common_1.HttpStatus.BAD_GATEWAY, 'collector_unavailable', `Сборщик опыта не принял карточки: ${reason}.`);
40
+ this.upstream = upstream;
41
+ }
42
+ }
43
+ /**
44
+ * Отправка опыта автору пакета — только с согласия владельца установки и только карточки:
45
+ * без чата, участников и ключей запусков (ключ уходит хешем с солью этой установки — по
46
+ * нему сборщик узнаёт ту же карточку, когда дописан урок, но не видит, откуда она).
47
+ * Код проектов в карточке не бывает вовсе.
48
+ */
49
+ let SharingService = class SharingService {
50
+ config;
51
+ db;
52
+ settings;
53
+ logger = new common_1.Logger('Sharing');
54
+ timer;
55
+ current = null;
56
+ constructor(config, db, settings) {
57
+ this.config = config;
58
+ this.db = db;
59
+ this.settings = settings;
60
+ }
61
+ onApplicationBootstrap() {
62
+ if (!this.config.collectorUrl)
63
+ return;
64
+ this.timer = setInterval(() => {
65
+ this.share().catch(e => this.logger.warn(`опыт не отправлен: ${e.message} — повторю позже`));
66
+ }, INTERVAL_MS);
67
+ this.timer.unref();
68
+ }
69
+ onApplicationShutdown() {
70
+ clearInterval(this.timer);
71
+ }
72
+ /** Согласие и что с отправкой: для CLI. answer null — не спрашивали или текст с тех пор изменился. */
73
+ async status() {
74
+ const consent = await this.settings.get(CONSENT);
75
+ const current = consent?.version === consent_1.CONSENT_VERSION ? consent : null;
76
+ const [{ pending }] = await this.db.select({ pending: (0, drizzle_orm_1.count)() }).from(schema_1.knowledgeCards)
77
+ .where((0, drizzle_orm_1.gt)(schema_1.knowledgeCards.revision, schema_1.knowledgeCards.sharedRevision));
78
+ return {
79
+ version: consent_1.CONSENT_VERSION,
80
+ text: consent_1.CONSENT_TEXT,
81
+ answer: current?.answer ?? null,
82
+ answered_at: current?.at ?? null,
83
+ collector: this.config.collectorUrl,
84
+ pending,
85
+ };
86
+ }
87
+ async setConsent(input) {
88
+ const { answer } = (0, validation_1.validate)(consentSchema, input);
89
+ await this.settings.set(CONSENT, { answer, version: consent_1.CONSENT_VERSION, at: (0, clock_1.now)().toISOString() });
90
+ return this.status();
91
+ }
92
+ /** Отправить всё, что не ушло. Одна отправка за раз: таймер и CLI не задвоят карточки. */
93
+ share() {
94
+ this.current ??= this.sendPending().finally(() => { this.current = null; });
95
+ return this.current;
96
+ }
97
+ async sendPending() {
98
+ const url = this.config.collectorUrl;
99
+ if (!url)
100
+ return { sent: 0, skipped: 'no_collector' };
101
+ if (!(0, consent_1.consentGiven)(await this.settings.get(CONSENT)))
102
+ return { sent: 0, skipped: 'no_consent' };
103
+ let sent = 0;
104
+ for (;;) {
105
+ const rows = await this.db.select().from(schema_1.knowledgeCards)
106
+ .where((0, drizzle_orm_1.gt)(schema_1.knowledgeCards.revision, schema_1.knowledgeCards.sharedRevision))
107
+ .orderBy((0, drizzle_orm_1.asc)(schema_1.knowledgeCards.id)).limit(collector_service_1.MAX_BATCH);
108
+ if (rows.length === 0)
109
+ break;
110
+ const installation = await this.installation(url);
111
+ await this.post(installation, '/api/collect/cards', {
112
+ cards: rows.map(r => ({ ref: (0, tokens_1.hashToken)(installation.salt + r.key), card: r.card })),
113
+ });
114
+ // Отмечается отправленная ревизия: урок, дописанный, пока шла отправка, уйдёт следующей.
115
+ for (const row of rows) {
116
+ await this.db.update(schema_1.knowledgeCards).set({ sharedRevision: row.revision }).where((0, drizzle_orm_1.eq)(schema_1.knowledgeCards.id, row.id));
117
+ }
118
+ sent += rows.length;
119
+ if (rows.length < collector_service_1.MAX_BATCH)
120
+ break;
121
+ }
122
+ if (sent)
123
+ this.logger.log(`опыт отправлен автору пакета: карточек ${sent}`);
124
+ return { sent };
125
+ }
126
+ /** Токен этой установки у сборщика; нет или сборщик сменился — регистрируемся. */
127
+ async installation(url) {
128
+ const saved = await this.settings.get(INSTALLATION);
129
+ if (saved?.url === url)
130
+ return saved;
131
+ const { token } = await this.request(url, '/api/collect/installations', {});
132
+ const installation = { url, token, salt: (0, tokens_1.randomToken)() };
133
+ await this.settings.set(INSTALLATION, installation);
134
+ return installation;
135
+ }
136
+ async post(installation, path, body) {
137
+ try {
138
+ return await this.request(installation.url, path, body, installation.token);
139
+ }
140
+ catch (e) {
141
+ // Сборщик забыл установку (её удалил владелец) — в следующий раз зарегистрируемся заново.
142
+ if (e instanceof CollectorUnavailable && e.upstream === 401)
143
+ await this.settings.remove(INSTALLATION);
144
+ throw e;
145
+ }
146
+ }
147
+ async request(url, path, body, token) {
148
+ let res;
149
+ try {
150
+ res = await fetch(url + path, {
151
+ method: 'POST',
152
+ headers: {
153
+ 'Content-Type': 'application/json',
154
+ Accept: 'application/json',
155
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
156
+ },
157
+ body: JSON.stringify(body),
158
+ signal: AbortSignal.timeout(TIMEOUT_MS),
159
+ });
160
+ }
161
+ catch (e) {
162
+ throw new CollectorUnavailable(null, `${url} не отвечает (${e.message})`);
163
+ }
164
+ if (!res.ok)
165
+ throw new CollectorUnavailable(res.status, `ответ ${res.status} на ${path}`);
166
+ return res.json();
167
+ }
168
+ };
169
+ exports.SharingService = SharingService;
170
+ exports.SharingService = SharingService = __decorate([
171
+ (0, common_1.Injectable)(),
172
+ __param(0, (0, common_1.Inject)(config_1.SERVER_CONFIG)),
173
+ __param(1, (0, common_1.Inject)(database_module_1.DATABASE)),
174
+ __metadata("design:paramtypes", [Object, Object, settings_service_1.SettingsService])
175
+ ], SharingService);
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.RunnerService = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const drizzle_orm_1 = require("drizzle-orm");
18
+ const errors_1 = require("../common/errors");
19
+ const database_module_1 = require("../db/database.module");
20
+ const schema_1 = require("../db/schema");
21
+ const participants_service_1 = require("../participants/participants.service");
22
+ const RUNTIME = /^[a-z0-9_-]{1,40}$/;
23
+ /**
24
+ * Раннер на машине с агентами: держит мост для каждого СВОЕГО агента. Спрашивает, кого
25
+ * запускать, и берёт токен каждого. Команду запуска сервер не передаёт: какие движки
26
+ * есть и как их запускать, знает только машина — иначе это было бы удалённым запуском кода.
27
+ *
28
+ * Раннеров может быть несколько; агент принадлежит одному — иначе два раннера выбивали бы
29
+ * друг другу токены, а агент отвечал бы дважды. Свободного забирает первый раннер, у
30
+ * которого есть его движок.
31
+ */
32
+ let RunnerService = class RunnerService {
33
+ db;
34
+ participants;
35
+ constructor(db, participants) {
36
+ this.db = db;
37
+ this.participants = participants;
38
+ }
39
+ async agents(me, runtimesQuery) {
40
+ hostOnly(me);
41
+ const runtimes = (runtimesQuery ?? '').split(',').filter(r => RUNTIME.test(r));
42
+ await this.db.update(schema_1.participants).set({ runtimes }).where((0, drizzle_orm_1.eq)(schema_1.participants.id, me.id));
43
+ // Свободных агентов со своим движком забираю себе — одним запросом: два раннера
44
+ // одновременно одного агента не заберут.
45
+ if (runtimes.length) {
46
+ await this.db.update(schema_1.participants).set({ hostId: me.id }).where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.participants.kind, 'ai'), (0, drizzle_orm_1.isNull)(schema_1.participants.disabledAt), (0, drizzle_orm_1.isNull)(schema_1.participants.hostId), (0, drizzle_orm_1.inArray)(schema_1.participants.runtime, runtimes)));
47
+ }
48
+ const mine = await this.db.select().from(schema_1.participants)
49
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.participants.kind, 'ai'), (0, drizzle_orm_1.isNull)(schema_1.participants.disabledAt), (0, drizzle_orm_1.eq)(schema_1.participants.hostId, me.id)))
50
+ .orderBy((0, drizzle_orm_1.asc)(schema_1.participants.id));
51
+ return { agents: mine.map(a => ({ handle: a.handle, name: a.name, runtime: a.runtime })) };
52
+ }
53
+ /** Токен агента для его моста. Только своего агента; прежний токен раннера отзывается. */
54
+ async token(me, handle) {
55
+ hostOnly(me);
56
+ const [agent] = await this.db.select().from(schema_1.participants).where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.participants.handle, handle), (0, drizzle_orm_1.eq)(schema_1.participants.kind, 'ai')));
57
+ if (!agent)
58
+ throw (0, errors_1.notFound)();
59
+ if (agent.disabledAt !== null)
60
+ throw (0, errors_1.conflict)('not_an_active_agent');
61
+ if (agent.hostId !== me.id)
62
+ throw new errors_1.ApiError(403, 'not_yours', 'Этот агент работает на другой машине.');
63
+ return { handle: agent.handle, token: await this.participants.issueExtraToken(agent.id, 'runner') };
64
+ }
65
+ };
66
+ exports.RunnerService = RunnerService;
67
+ exports.RunnerService = RunnerService = __decorate([
68
+ (0, common_1.Injectable)(),
69
+ __param(0, (0, common_1.Inject)(database_module_1.DATABASE)),
70
+ __metadata("design:paramtypes", [Object, participants_service_1.ParticipantsService])
71
+ ], RunnerService);
72
+ function hostOnly(me) {
73
+ if (!(0, participants_service_1.isHost)(me))
74
+ throw (0, errors_1.forbidden)('Это путь раннера на хосте.');
75
+ }