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.
- package/LICENSE +21 -0
- package/README.md +458 -2
- package/agents/rules.md +52 -0
- package/agents/runtimes/codex.md +14 -0
- package/bin/bridge +3 -0
- package/bin/runner +3 -0
- package/cli/lib/agents.js +97 -0
- package/cli/lib/docs.js +151 -0
- package/cli/lib/local.js +41 -0
- package/cli/lib/server.js +52 -0
- package/cli/lib/util.js +107 -0
- package/cli/team-chat.js +383 -0
- package/host/bridge/attempts.js +43 -0
- package/host/bridge/bridge.js +326 -0
- package/host/bridge/config.js +105 -0
- package/host/bridge/knowledge-outbox.js +96 -0
- package/host/bridge/launcher.js +126 -0
- package/host/bridge/main.js +87 -0
- package/host/bridge/progress.js +74 -0
- package/host/bridge/prompt.js +97 -0
- package/host/bridge/run-archive.js +200 -0
- package/host/bridge/thread.js +44 -0
- package/host/bridge/triage.js +97 -0
- package/host/bridge/workdir.js +41 -0
- package/host/knowledge/card.js +43 -0
- package/host/knowledge/errors.js +50 -0
- package/host/knowledge/lesson.js +153 -0
- package/host/knowledge/process.js +160 -0
- package/host/knowledge/scrub.js +29 -0
- package/host/knowledge/shell.js +74 -0
- package/host/knowledge/stack.js +229 -0
- package/host/knowledge/transcript.js +82 -0
- package/host/lib/chat-api.js +120 -0
- package/host/lib/log.js +6 -0
- package/host/lib/pid-lock.js +65 -0
- package/host/lib/shell-words.js +45 -0
- package/host/runner/bridge-process.js +82 -0
- package/host/runner/config.js +81 -0
- package/host/runner/main.js +69 -0
- package/host/runner/retention.js +40 -0
- package/host/runner/runner.js +161 -0
- package/package.json +67 -5
- package/server/dist/src/app.js +25 -0
- package/server/dist/src/app.module.js +50 -0
- package/server/dist/src/attachments/attachment-store.js +111 -0
- package/server/dist/src/attachments/attachments.controller.js +67 -0
- package/server/dist/src/attachments/mime.js +84 -0
- package/server/dist/src/attachments/uploads.interceptor.js +72 -0
- package/server/dist/src/auth/participant.guard.js +53 -0
- package/server/dist/src/bridges/bridge-restart.service.js +80 -0
- package/server/dist/src/bridges/bridges.controller.js +47 -0
- package/server/dist/src/chats/chats.controller.js +83 -0
- package/server/dist/src/chats/chats.service.js +128 -0
- package/server/dist/src/collector/collector.controller.js +71 -0
- package/server/dist/src/collector/collector.module.js +23 -0
- package/server/dist/src/collector/collector.service.js +111 -0
- package/server/dist/src/common/clock.js +20 -0
- package/server/dist/src/common/errors.js +37 -0
- package/server/dist/src/common/format.js +14 -0
- package/server/dist/src/common/http.js +17 -0
- package/server/dist/src/common/slug.js +28 -0
- package/server/dist/src/common/tokens.js +19 -0
- package/server/dist/src/common/validation.js +41 -0
- package/server/dist/src/config/config.js +39 -0
- package/server/dist/src/conversation.module.js +41 -0
- package/server/dist/src/db/database.js +73 -0
- package/server/dist/src/db/database.module.js +51 -0
- package/server/dist/src/db/schema.js +212 -0
- package/server/dist/src/decisions/decisions.controller.js +55 -0
- package/server/dist/src/decisions/decisions.service.js +109 -0
- package/server/dist/src/documents/documents.controller.js +87 -0
- package/server/dist/src/documents/documents.service.js +189 -0
- package/server/dist/src/health/health.controller.js +29 -0
- package/server/dist/src/knowledge/knowledge.controller.js +83 -0
- package/server/dist/src/knowledge/knowledge.module.js +25 -0
- package/server/dist/src/knowledge/knowledge.service.js +189 -0
- package/server/dist/src/local/docs-sync.service.js +97 -0
- package/server/dist/src/local/local.controller.js +197 -0
- package/server/dist/src/local/local.guard.js +55 -0
- package/server/dist/src/local/local.module.js +30 -0
- package/server/dist/src/local/setup.service.js +154 -0
- package/server/dist/src/local/transfer.service.js +151 -0
- package/server/dist/src/main.js +33 -0
- package/server/dist/src/mcp/chat-tools.js +282 -0
- package/server/dist/src/mcp/mcp.controller.js +58 -0
- package/server/dist/src/mcp/mcp.module.js +24 -0
- package/server/dist/src/mentions/mention-state.js +80 -0
- package/server/dist/src/mentions/mentions.controller.js +100 -0
- package/server/dist/src/mentions/mentions.service.js +203 -0
- package/server/dist/src/messages/message.presenter.js +102 -0
- package/server/dist/src/messages/messages.service.js +149 -0
- package/server/dist/src/messages/thread.controller.js +179 -0
- package/server/dist/src/participants/directory.service.js +68 -0
- package/server/dist/src/participants/participants.module.js +24 -0
- package/server/dist/src/participants/participants.service.js +84 -0
- package/server/dist/src/settings/settings.service.js +42 -0
- package/server/dist/src/sharing/consent.js +16 -0
- package/server/dist/src/sharing/sharing.module.js +22 -0
- package/server/dist/src/sharing/sharing.service.js +175 -0
- package/server/dist/src/team/runner.service.js +75 -0
- package/server/dist/src/team/team.controller.js +178 -0
- package/server/dist/src/team/team.module.js +25 -0
- package/server/dist/src/team/team.service.js +280 -0
- package/server/dist/src/web/template.js +57 -0
- package/server/dist/src/web/web-login.service.js +74 -0
- package/server/dist/src/web/web.controller.js +203 -0
- package/server/dist/src/web/web.module.js +25 -0
- package/server/drizzle/0000_init.sql +169 -0
- package/server/drizzle/0001_seed_roles.sql +25 -0
- package/server/drizzle/0002_knowledge.sql +17 -0
- package/server/drizzle/0003_sharing.sql +30 -0
- package/server/drizzle/meta/0000_snapshot.json +1224 -0
- package/server/drizzle/meta/0001_snapshot.json +1224 -0
- package/server/drizzle/meta/0002_snapshot.json +1359 -0
- package/server/drizzle/meta/0003_snapshot.json +1545 -0
- package/server/drizzle/meta/_journal.json +34 -0
- package/server/package.json +1 -0
- package/server/views/chat.html +865 -0
- package/server/views/documents.html +204 -0
- package/server/views/login.html +31 -0
- package/server/views/setup.html +60 -0
- package/server/views/team.html +369 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Схема чата. Таблицы те же, что были в PHP (database/migrations): перенос данных —
|
|
3
|
+
// построчный, а мост, раннер и страницы видят прежние поля.
|
|
4
|
+
//
|
|
5
|
+
// Миграции генерируются из этого файла: npm run db:generate.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.loginCodes = exports.collectedCards = exports.collectorInstallations = exports.settings = exports.knowledgeCards = exports.documentVersions = exports.documents = exports.decisions = exports.mentions = exports.attachments = exports.messages = exports.chatAgentRoles = exports.chats = exports.roles = exports.participantTokens = exports.participants = void 0;
|
|
8
|
+
const drizzle_orm_1 = require("drizzle-orm");
|
|
9
|
+
const pg_core_1 = require("drizzle-orm/pg-core");
|
|
10
|
+
const clock_1 = require("../common/clock");
|
|
11
|
+
const at = (name) => (0, pg_core_1.timestamp)(name, { withTimezone: true, mode: 'date' });
|
|
12
|
+
// Время ставит приложение по своим часам (common/clock): тесты сдвигают их, и сроки
|
|
13
|
+
// годности сигналов проверяются без ожидания. SQL-умолчание — для прямых вставок.
|
|
14
|
+
const timestamps = {
|
|
15
|
+
createdAt: at('created_at').notNull().defaultNow().$defaultFn(clock_1.now),
|
|
16
|
+
updatedAt: at('updated_at').notNull().defaultNow().$defaultFn(clock_1.now).$onUpdate(clock_1.now),
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Участники: люди, агенты и раннеры машин. Токен — только хешем (sha256): показывается
|
|
20
|
+
* один раз при выпуске. Удалить участника нельзя — на него ссылаются сообщения;
|
|
21
|
+
* агента отключают (disabled_at).
|
|
22
|
+
*/
|
|
23
|
+
exports.participants = (0, pg_core_1.pgTable)('participants', {
|
|
24
|
+
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
25
|
+
name: (0, pg_core_1.text)('name').notNull(),
|
|
26
|
+
handle: (0, pg_core_1.text)('handle').notNull().unique(), // @ник для упоминаний
|
|
27
|
+
kind: (0, pg_core_1.text)('kind').notNull(), // human | ai | host
|
|
28
|
+
role: (0, pg_core_1.text)('role').notNull(), // owner | host | слаг роли из roles
|
|
29
|
+
accent: (0, pg_core_1.varchar)('accent', { length: 16 }).notNull().default('#0057c2'),
|
|
30
|
+
tokenHash: (0, pg_core_1.varchar)('token_hash', { length: 64 }).unique(),
|
|
31
|
+
lastSeenAt: at('last_seen_at'),
|
|
32
|
+
// Агент: движок, на котором его запускает раннер, и машина-раннер, которой он принадлежит.
|
|
33
|
+
runtime: (0, pg_core_1.varchar)('runtime', { length: 40 }),
|
|
34
|
+
disabledAt: at('disabled_at'),
|
|
35
|
+
hostId: (0, pg_core_1.integer)('host_id').references(() => exports.participants.id, { onDelete: 'set null' }),
|
|
36
|
+
// Раннер: какие движки есть на его машине — сообщает сам на каждом проходе.
|
|
37
|
+
runtimes: (0, pg_core_1.jsonb)('runtimes').$type(),
|
|
38
|
+
...timestamps,
|
|
39
|
+
});
|
|
40
|
+
/**
|
|
41
|
+
* Дополнительные токены участника. Ими раннер запускает агентов: постоянный токен агента
|
|
42
|
+
* сервер знает только хешем, а перевыпуск сломал бы тем, кто им пользуется. Ими же
|
|
43
|
+
* входит браузер — свой токен на каждый вход.
|
|
44
|
+
*/
|
|
45
|
+
exports.participantTokens = (0, pg_core_1.pgTable)('participant_tokens', {
|
|
46
|
+
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
47
|
+
participantId: (0, pg_core_1.integer)('participant_id').notNull().references(() => exports.participants.id, { onDelete: 'cascade' }),
|
|
48
|
+
tokenHash: (0, pg_core_1.varchar)('token_hash', { length: 64 }).notNull().unique(),
|
|
49
|
+
label: (0, pg_core_1.text)('label').notNull(), // кто и зачем выпустил: «runner», «web:…»
|
|
50
|
+
...timestamps,
|
|
51
|
+
});
|
|
52
|
+
/** Роль агента — инструкция в markdown: уходит агенту в каждое задание. */
|
|
53
|
+
exports.roles = (0, pg_core_1.pgTable)('roles', {
|
|
54
|
+
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
55
|
+
slug: (0, pg_core_1.text)('slug').notNull().unique(),
|
|
56
|
+
name: (0, pg_core_1.text)('name').notNull(),
|
|
57
|
+
instructions: (0, pg_core_1.text)('instructions').notNull(),
|
|
58
|
+
...timestamps,
|
|
59
|
+
});
|
|
60
|
+
/** Чат — проект: свой тред, журнал решений, документы и контекст агента. */
|
|
61
|
+
exports.chats = (0, pg_core_1.pgTable)('chats', {
|
|
62
|
+
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
63
|
+
slug: (0, pg_core_1.text)('slug').notNull().unique(), // в адресе: /c/qbazar
|
|
64
|
+
name: (0, pg_core_1.text)('name').notNull(),
|
|
65
|
+
description: (0, pg_core_1.text)('description'), // уходит агенту в каждое задание
|
|
66
|
+
workdir: (0, pg_core_1.text)('workdir'), // каталог кода на машине с мостами — агент стартует в нём
|
|
67
|
+
...timestamps,
|
|
68
|
+
});
|
|
69
|
+
/** Замена роли агента в конкретном чате. */
|
|
70
|
+
exports.chatAgentRoles = (0, pg_core_1.pgTable)('chat_agent_roles', {
|
|
71
|
+
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
72
|
+
chatId: (0, pg_core_1.integer)('chat_id').notNull().references(() => exports.chats.id, { onDelete: 'cascade' }),
|
|
73
|
+
participantId: (0, pg_core_1.integer)('participant_id').notNull().references(() => exports.participants.id, { onDelete: 'cascade' }),
|
|
74
|
+
role: (0, pg_core_1.text)('role').notNull(),
|
|
75
|
+
...timestamps,
|
|
76
|
+
}, t => [(0, pg_core_1.unique)().on(t.chatId, t.participantId)]);
|
|
77
|
+
/** Тред — разговор. Здесь можно сомневаться, спорить и передумывать. */
|
|
78
|
+
exports.messages = (0, pg_core_1.pgTable)('messages', {
|
|
79
|
+
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
80
|
+
chatId: (0, pg_core_1.integer)('chat_id').notNull().references(() => exports.chats.id, { onDelete: 'cascade' }),
|
|
81
|
+
participantId: (0, pg_core_1.integer)('participant_id').notNull().references(() => exports.participants.id, { onDelete: 'cascade' }),
|
|
82
|
+
body: (0, pg_core_1.text)('body').notNull(),
|
|
83
|
+
replyToId: (0, pg_core_1.integer)('reply_to_id').references(() => exports.messages.id, { onDelete: 'set null' }),
|
|
84
|
+
// Вопрос владельцу, без ответа на который работа не пойдёт. Помечает автор явно:
|
|
85
|
+
// упоминание владельца в тексте вопросом не считается.
|
|
86
|
+
asksOwner: (0, pg_core_1.boolean)('asks_owner').notNull().default(false),
|
|
87
|
+
...timestamps,
|
|
88
|
+
}, t => [(0, pg_core_1.index)().on(t.chatId, t.id), (0, pg_core_1.index)().on(t.createdAt)]);
|
|
89
|
+
/** Файлы сообщения. Живут только вместе с ним; сам файл — в каталоге данных. */
|
|
90
|
+
exports.attachments = (0, pg_core_1.pgTable)('attachments', {
|
|
91
|
+
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
92
|
+
messageId: (0, pg_core_1.integer)('message_id').notNull().references(() => exports.messages.id, { onDelete: 'cascade' }),
|
|
93
|
+
name: (0, pg_core_1.text)('name').notNull(), // как назвал файл автор
|
|
94
|
+
mime: (0, pg_core_1.varchar)('mime', { length: 191 }).notNull(),
|
|
95
|
+
size: (0, pg_core_1.bigint)('size', { mode: 'number' }).notNull(),
|
|
96
|
+
path: (0, pg_core_1.text)('path').notNull(), // относительно каталога вложений
|
|
97
|
+
...timestamps,
|
|
98
|
+
});
|
|
99
|
+
/**
|
|
100
|
+
* Обращения: @handle в тексте. Гасит обращение не чтение, а подтверждение после
|
|
101
|
+
* опубликованного ответа. Ход обработки — факты от моста (запуск, сигнал жизни, исход);
|
|
102
|
+
* состояние выводится из них при чтении, готовой надписи в базе нет.
|
|
103
|
+
*/
|
|
104
|
+
exports.mentions = (0, pg_core_1.pgTable)('mentions', {
|
|
105
|
+
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
106
|
+
messageId: (0, pg_core_1.integer)('message_id').notNull().references(() => exports.messages.id, { onDelete: 'cascade' }),
|
|
107
|
+
participantId: (0, pg_core_1.integer)('participant_id').notNull().references(() => exports.participants.id, { onDelete: 'cascade' }),
|
|
108
|
+
handledAt: at('handled_at'),
|
|
109
|
+
runStartedAt: at('run_started_at'),
|
|
110
|
+
beatAt: at('beat_at'),
|
|
111
|
+
beatTtl: (0, pg_core_1.integer)('beat_ttl'),
|
|
112
|
+
outcome: (0, pg_core_1.varchar)('outcome', { length: 16 }), // done | failed | deferred | paused
|
|
113
|
+
outcomeAt: at('outcome_at'),
|
|
114
|
+
note: (0, pg_core_1.text)('note'),
|
|
115
|
+
attempts: (0, pg_core_1.integer)('attempts').notNull().default(0),
|
|
116
|
+
...timestamps,
|
|
117
|
+
}, t => [(0, pg_core_1.unique)().on(t.messageId, t.participantId), (0, pg_core_1.index)().on(t.participantId, t.handledAt)]);
|
|
118
|
+
/**
|
|
119
|
+
* Журнал решений — только утверждённое владельцем. Правке не подлежит: решение
|
|
120
|
+
* отменяется выпуском нового со ссылкой supersedes_id. Нумерация — своя в каждом чате.
|
|
121
|
+
*/
|
|
122
|
+
exports.decisions = (0, pg_core_1.pgTable)('decisions', {
|
|
123
|
+
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
124
|
+
chatId: (0, pg_core_1.integer)('chat_id').notNull().references(() => exports.chats.id, { onDelete: 'cascade' }),
|
|
125
|
+
number: (0, pg_core_1.integer)('number').notNull(),
|
|
126
|
+
participantId: (0, pg_core_1.integer)('participant_id').notNull().references(() => exports.participants.id, { onDelete: 'cascade' }),
|
|
127
|
+
title: (0, pg_core_1.text)('title').notNull(),
|
|
128
|
+
body: (0, pg_core_1.text)('body'),
|
|
129
|
+
messageId: (0, pg_core_1.integer)('message_id').references(() => exports.messages.id, { onDelete: 'set null' }),
|
|
130
|
+
supersedesId: (0, pg_core_1.integer)('supersedes_id').references(() => exports.decisions.id, { onDelete: 'set null' }),
|
|
131
|
+
...timestamps,
|
|
132
|
+
}, t => [(0, pg_core_1.unique)().on(t.chatId, t.number)]);
|
|
133
|
+
/** Документы проекта: планы, аудиты, хендоффы. Ключевое поле — status. */
|
|
134
|
+
exports.documents = (0, pg_core_1.pgTable)('documents', {
|
|
135
|
+
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
136
|
+
chatId: (0, pg_core_1.integer)('chat_id').notNull().references(() => exports.chats.id, { onDelete: 'cascade' }),
|
|
137
|
+
slug: (0, pg_core_1.text)('slug').notNull(),
|
|
138
|
+
title: (0, pg_core_1.text)('title').notNull(),
|
|
139
|
+
kind: (0, pg_core_1.text)('kind').notNull(), // plan | audit | handoff | report | reference
|
|
140
|
+
status: (0, pg_core_1.text)('status').notNull().default('draft'), // draft | current | archived
|
|
141
|
+
origin: (0, pg_core_1.text)('origin'), // путь в репозитории для выгрузки
|
|
142
|
+
participantId: (0, pg_core_1.integer)('participant_id').references(() => exports.participants.id, { onDelete: 'set null' }),
|
|
143
|
+
...timestamps,
|
|
144
|
+
}, t => [(0, pg_core_1.unique)().on(t.chatId, t.slug), (0, pg_core_1.index)().on(t.status, t.kind)]);
|
|
145
|
+
/** Редакции документа: каждая правка — новая, прошлые не переписываются. */
|
|
146
|
+
exports.documentVersions = (0, pg_core_1.pgTable)('document_versions', {
|
|
147
|
+
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
148
|
+
documentId: (0, pg_core_1.integer)('document_id').notNull().references(() => exports.documents.id, { onDelete: 'cascade' }),
|
|
149
|
+
revision: (0, pg_core_1.integer)('revision').notNull(),
|
|
150
|
+
body: (0, pg_core_1.text)('body').notNull(),
|
|
151
|
+
participantId: (0, pg_core_1.integer)('participant_id').references(() => exports.participants.id, { onDelete: 'set null' }),
|
|
152
|
+
note: (0, pg_core_1.text)('note'), // чем правка вызвана
|
|
153
|
+
...timestamps,
|
|
154
|
+
}, t => [(0, pg_core_1.unique)().on(t.documentId, t.revision)]);
|
|
155
|
+
/**
|
|
156
|
+
* Карточки опыта: что агент делал в запуске и какой урок из этого следует (host/knowledge,
|
|
157
|
+
* docs/plan-2026-10-24-knowledge.md). Кода проекта в карточке нет — стек, ход работы в
|
|
158
|
+
* числах и обобщённый урок. key — запись запуска на машине моста: повторная отправка и урок,
|
|
159
|
+
* дописанный позже, обновляют ту же карточку.
|
|
160
|
+
*/
|
|
161
|
+
exports.knowledgeCards = (0, pg_core_1.pgTable)('knowledge_cards', {
|
|
162
|
+
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
163
|
+
chatId: (0, pg_core_1.integer)('chat_id').notNull().references(() => exports.chats.id, { onDelete: 'cascade' }),
|
|
164
|
+
participantId: (0, pg_core_1.integer)('participant_id').references(() => exports.participants.id, { onDelete: 'set null' }),
|
|
165
|
+
key: (0, pg_core_1.text)('key').notNull().unique(),
|
|
166
|
+
format: (0, pg_core_1.integer)('format').notNull(),
|
|
167
|
+
card: (0, pg_core_1.jsonb)('card').$type().notNull(),
|
|
168
|
+
// Текст для поиска: урок и стек. null — урока нет: карточка хранится, но в поиск не идёт.
|
|
169
|
+
search: (0, pg_core_1.text)('search'),
|
|
170
|
+
// Отправка автору пакета (с согласия): каждая правка карточки — новая ревизия, ушедшая —
|
|
171
|
+
// shared_revision. Урок, дописанный после отправки, уйдёт ещё раз.
|
|
172
|
+
revision: (0, pg_core_1.integer)('revision').notNull().default(1),
|
|
173
|
+
sharedRevision: (0, pg_core_1.integer)('shared_revision').notNull().default(0),
|
|
174
|
+
...timestamps,
|
|
175
|
+
}, t => [
|
|
176
|
+
(0, pg_core_1.index)('knowledge_cards_search_idx').using('gin', (0, drizzle_orm_1.sql) `to_tsvector('russian', ${t.search})`),
|
|
177
|
+
(0, pg_core_1.index)().on(t.chatId),
|
|
178
|
+
]);
|
|
179
|
+
/**
|
|
180
|
+
* Настройки этой установки: согласие на отправку опыта, её адрес у сборщика. Не переносятся
|
|
181
|
+
* дампом — это свойство машины, а не чата.
|
|
182
|
+
*/
|
|
183
|
+
exports.settings = (0, pg_core_1.pgTable)('settings', {
|
|
184
|
+
key: (0, pg_core_1.text)('key').primaryKey(),
|
|
185
|
+
value: (0, pg_core_1.jsonb)('value').notNull(),
|
|
186
|
+
...timestamps,
|
|
187
|
+
});
|
|
188
|
+
/**
|
|
189
|
+
* Сборщик опыта (CHAT_COLLECTOR=1) — сервер автора пакета. Установки регистрируются сами и
|
|
190
|
+
* присылают обезличенные карточки: без чата, участников и ключей запусков.
|
|
191
|
+
*/
|
|
192
|
+
exports.collectorInstallations = (0, pg_core_1.pgTable)('collector_installations', {
|
|
193
|
+
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
194
|
+
tokenHash: (0, pg_core_1.varchar)('token_hash', { length: 64 }).notNull().unique(),
|
|
195
|
+
lastSeenAt: at('last_seen_at'),
|
|
196
|
+
...timestamps,
|
|
197
|
+
});
|
|
198
|
+
exports.collectedCards = (0, pg_core_1.pgTable)('collected_cards', {
|
|
199
|
+
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
200
|
+
installationId: (0, pg_core_1.integer)('installation_id').notNull().references(() => exports.collectorInstallations.id, { onDelete: 'cascade' }),
|
|
201
|
+
ref: (0, pg_core_1.varchar)('ref', { length: 64 }).notNull(), // хеш записи запуска: урок, пришедший позже, обновляет карточку
|
|
202
|
+
format: (0, pg_core_1.integer)('format').notNull(),
|
|
203
|
+
card: (0, pg_core_1.jsonb)('card').$type().notNull(),
|
|
204
|
+
...timestamps,
|
|
205
|
+
}, t => [(0, pg_core_1.unique)().on(t.installationId, t.ref)]);
|
|
206
|
+
/** Одноразовые ссылки входа в браузер. Хранится хеш кода: сама ссылка — только у того, кто её запросил. */
|
|
207
|
+
exports.loginCodes = (0, pg_core_1.pgTable)('login_codes', {
|
|
208
|
+
codeHash: (0, pg_core_1.varchar)('code_hash', { length: 64 }).primaryKey(),
|
|
209
|
+
participantId: (0, pg_core_1.integer)('participant_id').notNull().references(() => exports.participants.id, { onDelete: 'cascade' }),
|
|
210
|
+
expiresAt: at('expires_at').notNull(),
|
|
211
|
+
createdAt: at('created_at').notNull().defaultNow().$defaultFn(clock_1.now),
|
|
212
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
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.DecisionsController = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const participant_guard_1 = require("../auth/participant.guard");
|
|
18
|
+
const chats_service_1 = require("../chats/chats.service");
|
|
19
|
+
const decisions_service_1 = require("./decisions.service");
|
|
20
|
+
let DecisionsController = class DecisionsController {
|
|
21
|
+
chats;
|
|
22
|
+
decisions;
|
|
23
|
+
constructor(chats, decisions) {
|
|
24
|
+
this.chats = chats;
|
|
25
|
+
this.decisions = decisions;
|
|
26
|
+
}
|
|
27
|
+
async index(slug) {
|
|
28
|
+
return { decisions: await this.decisions.list(await this.chats.resolve(slug)) };
|
|
29
|
+
}
|
|
30
|
+
async decide(me, slug, body) {
|
|
31
|
+
return this.decisions.decide(await this.chats.resolve(slug), me, body);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
exports.DecisionsController = DecisionsController;
|
|
35
|
+
__decorate([
|
|
36
|
+
(0, common_1.Get)(['chats/:chat/decisions', 'decisions']),
|
|
37
|
+
__param(0, (0, common_1.Param)('chat')),
|
|
38
|
+
__metadata("design:type", Function),
|
|
39
|
+
__metadata("design:paramtypes", [Object]),
|
|
40
|
+
__metadata("design:returntype", Promise)
|
|
41
|
+
], DecisionsController.prototype, "index", null);
|
|
42
|
+
__decorate([
|
|
43
|
+
(0, common_1.Post)(['chats/:chat/decisions', 'decisions']),
|
|
44
|
+
__param(0, (0, participant_guard_1.Me)()),
|
|
45
|
+
__param(1, (0, common_1.Param)('chat')),
|
|
46
|
+
__param(2, (0, common_1.Body)()),
|
|
47
|
+
__metadata("design:type", Function),
|
|
48
|
+
__metadata("design:paramtypes", [Object, Object, Object]),
|
|
49
|
+
__metadata("design:returntype", Promise)
|
|
50
|
+
], DecisionsController.prototype, "decide", null);
|
|
51
|
+
exports.DecisionsController = DecisionsController = __decorate([
|
|
52
|
+
(0, common_1.Controller)('api'),
|
|
53
|
+
(0, common_1.UseGuards)(participant_guard_1.ParticipantGuard),
|
|
54
|
+
__metadata("design:paramtypes", [chats_service_1.ChatsService, decisions_service_1.DecisionsService])
|
|
55
|
+
], DecisionsController);
|
|
@@ -0,0 +1,109 @@
|
|
|
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.DecisionsService = 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 errors_1 = require("../common/errors");
|
|
20
|
+
const format_1 = require("../common/format");
|
|
21
|
+
const validation_1 = require("../common/validation");
|
|
22
|
+
const database_module_1 = require("../db/database.module");
|
|
23
|
+
const schema_1 = require("../db/schema");
|
|
24
|
+
const participants_service_1 = require("../participants/participants.service");
|
|
25
|
+
const decideSchema = zod_1.z.object({
|
|
26
|
+
title: zod_1.z.string().max(300),
|
|
27
|
+
body: (0, validation_1.optionalText)(20000),
|
|
28
|
+
message_id: (0, validation_1.int)().nullish(),
|
|
29
|
+
supersedes_id: (0, validation_1.int)().nullish(),
|
|
30
|
+
});
|
|
31
|
+
/**
|
|
32
|
+
* Журнал решений — только утверждённое. Неизменяем: правок нет ни в API, ни в
|
|
33
|
+
* интерфейсе. Передумали — выпускается новое решение со ссылкой на старое.
|
|
34
|
+
*/
|
|
35
|
+
let DecisionsService = class DecisionsService {
|
|
36
|
+
db;
|
|
37
|
+
constructor(db) {
|
|
38
|
+
this.db = db;
|
|
39
|
+
}
|
|
40
|
+
async list(chat) {
|
|
41
|
+
const rows = await this.db.select().from(schema_1.decisions).where((0, drizzle_orm_1.eq)(schema_1.decisions.chatId, chat.id)).orderBy((0, drizzle_orm_1.asc)(schema_1.decisions.number));
|
|
42
|
+
return this.present(rows);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Зафиксировать решение — ТОЛЬКО владельцу: агенты предлагают, утверждает человек.
|
|
46
|
+
* Иначе журнал перестаёт быть списком принятого и снова становится свалкой мнений.
|
|
47
|
+
*/
|
|
48
|
+
async decide(chat, me, input) {
|
|
49
|
+
if (!(0, participants_service_1.isOwner)(me))
|
|
50
|
+
throw (0, errors_1.forbidden)('Решение фиксирует только владелец. Предложение оставьте сообщением в треде.');
|
|
51
|
+
const data = (0, validation_1.validate)(decideSchema, input);
|
|
52
|
+
if (data.message_id != null && !(await this.messageInChat(chat, data.message_id))) {
|
|
53
|
+
throw errors_1.ValidationFailed.field('message_id', 'Сообщения с таким id в этом чате нет.');
|
|
54
|
+
}
|
|
55
|
+
if (data.supersedes_id != null && !(await this.decisionInChat(chat, data.supersedes_id))) {
|
|
56
|
+
throw errors_1.ValidationFailed.field('supersedes_id', 'Решения с таким id в этом чате нет.');
|
|
57
|
+
}
|
|
58
|
+
// Нумерация своя у каждого чата: «решение №3» в двух проектах — разные решения.
|
|
59
|
+
const [{ last }] = await this.db.select({ last: (0, drizzle_orm_1.max)(schema_1.decisions.number) }).from(schema_1.decisions).where((0, drizzle_orm_1.eq)(schema_1.decisions.chatId, chat.id));
|
|
60
|
+
const [decision] = await this.db.insert(schema_1.decisions).values({
|
|
61
|
+
chatId: chat.id,
|
|
62
|
+
number: (last ?? 0) + 1,
|
|
63
|
+
participantId: me.id,
|
|
64
|
+
title: data.title,
|
|
65
|
+
body: data.body ?? null,
|
|
66
|
+
messageId: data.message_id ?? null,
|
|
67
|
+
supersedesId: data.supersedes_id ?? null,
|
|
68
|
+
}).returning();
|
|
69
|
+
return (await this.present([decision]))[0];
|
|
70
|
+
}
|
|
71
|
+
async present(rows) {
|
|
72
|
+
if (rows.length === 0)
|
|
73
|
+
return [];
|
|
74
|
+
const ids = rows.map(d => d.id);
|
|
75
|
+
const [people, superseding, superseded] = await Promise.all([
|
|
76
|
+
this.db.select({ id: schema_1.participants.id, handle: schema_1.participants.handle }).from(schema_1.participants),
|
|
77
|
+
this.db.select({ supersedesId: schema_1.decisions.supersedesId }).from(schema_1.decisions).where((0, drizzle_orm_1.inArray)(schema_1.decisions.supersedesId, ids)),
|
|
78
|
+
this.db.select({ id: schema_1.decisions.id, number: schema_1.decisions.number }).from(schema_1.decisions)
|
|
79
|
+
.where((0, drizzle_orm_1.inArray)(schema_1.decisions.id, rows.map(d => d.supersedesId).filter((id) => id !== null))),
|
|
80
|
+
]);
|
|
81
|
+
const handle = new Map(people.map(p => [p.id, p.handle]));
|
|
82
|
+
const isSuperseded = new Set(superseding.map(d => d.supersedesId));
|
|
83
|
+
const numberOf = new Map(superseded.map(d => [d.id, d.number]));
|
|
84
|
+
return rows.map(d => ({
|
|
85
|
+
number: d.number,
|
|
86
|
+
title: d.title,
|
|
87
|
+
body: d.body,
|
|
88
|
+
decided_by: handle.get(d.participantId),
|
|
89
|
+
decided_at: (0, format_1.iso)(d.createdAt),
|
|
90
|
+
from_message: d.messageId,
|
|
91
|
+
supersedes: d.supersedesId === null ? null : numberOf.get(d.supersedesId) ?? null,
|
|
92
|
+
superseded: isSuperseded.has(d.id),
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
async messageInChat(chat, id) {
|
|
96
|
+
const [row] = await this.db.select({ id: schema_1.messages.id }).from(schema_1.messages).where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.messages.id, id), (0, drizzle_orm_1.eq)(schema_1.messages.chatId, chat.id)));
|
|
97
|
+
return Boolean(row);
|
|
98
|
+
}
|
|
99
|
+
async decisionInChat(chat, id) {
|
|
100
|
+
const [row] = await this.db.select({ id: schema_1.decisions.id }).from(schema_1.decisions).where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.decisions.id, id), (0, drizzle_orm_1.eq)(schema_1.decisions.chatId, chat.id)));
|
|
101
|
+
return Boolean(row);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
exports.DecisionsService = DecisionsService;
|
|
105
|
+
exports.DecisionsService = DecisionsService = __decorate([
|
|
106
|
+
(0, common_1.Injectable)(),
|
|
107
|
+
__param(0, (0, common_1.Inject)(database_module_1.DATABASE)),
|
|
108
|
+
__metadata("design:paramtypes", [Object])
|
|
109
|
+
], DecisionsService);
|
|
@@ -0,0 +1,87 @@
|
|
|
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.DocumentsController = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const participant_guard_1 = require("../auth/participant.guard");
|
|
18
|
+
const chats_service_1 = require("../chats/chats.service");
|
|
19
|
+
const documents_service_1 = require("./documents.service");
|
|
20
|
+
/** Документы — бывшие .md. Подтверждает актуальность только владелец. */
|
|
21
|
+
let DocumentsController = class DocumentsController {
|
|
22
|
+
chats;
|
|
23
|
+
documents;
|
|
24
|
+
constructor(chats, documents) {
|
|
25
|
+
this.chats = chats;
|
|
26
|
+
this.documents = documents;
|
|
27
|
+
}
|
|
28
|
+
async index(slug, status, kind) {
|
|
29
|
+
const chat = await this.chats.resolve(slug);
|
|
30
|
+
return { documents: await this.documents.list(chat, { status, kind }), kinds: documents_service_1.KINDS, statuses: documents_service_1.STATUSES };
|
|
31
|
+
}
|
|
32
|
+
async show(chat, slug) {
|
|
33
|
+
return this.documents.show(await this.chats.resolve(chat), slug);
|
|
34
|
+
}
|
|
35
|
+
/** Новый документ — 201, новая редакция существующего — 200. */
|
|
36
|
+
async store(me, slug, body, res) {
|
|
37
|
+
const { document, created } = await this.documents.write(await this.chats.resolve(slug), me, body);
|
|
38
|
+
res.status(created ? 201 : 200);
|
|
39
|
+
return document;
|
|
40
|
+
}
|
|
41
|
+
async update(me, chat, slug, body) {
|
|
42
|
+
return this.documents.update(await this.chats.resolve(chat), slug, me, body);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
exports.DocumentsController = DocumentsController;
|
|
46
|
+
__decorate([
|
|
47
|
+
(0, common_1.Get)(['chats/:chat/documents', 'documents']),
|
|
48
|
+
__param(0, (0, common_1.Param)('chat')),
|
|
49
|
+
__param(1, (0, common_1.Query)('status')),
|
|
50
|
+
__param(2, (0, common_1.Query)('kind')),
|
|
51
|
+
__metadata("design:type", Function),
|
|
52
|
+
__metadata("design:paramtypes", [Object, String, String]),
|
|
53
|
+
__metadata("design:returntype", Promise)
|
|
54
|
+
], DocumentsController.prototype, "index", null);
|
|
55
|
+
__decorate([
|
|
56
|
+
(0, common_1.Get)(['chats/:chat/documents/:slug', 'documents/:slug']),
|
|
57
|
+
__param(0, (0, common_1.Param)('chat')),
|
|
58
|
+
__param(1, (0, common_1.Param)('slug')),
|
|
59
|
+
__metadata("design:type", Function),
|
|
60
|
+
__metadata("design:paramtypes", [Object, String]),
|
|
61
|
+
__metadata("design:returntype", Promise)
|
|
62
|
+
], DocumentsController.prototype, "show", null);
|
|
63
|
+
__decorate([
|
|
64
|
+
(0, common_1.Post)(['chats/:chat/documents', 'documents']),
|
|
65
|
+
__param(0, (0, participant_guard_1.Me)()),
|
|
66
|
+
__param(1, (0, common_1.Param)('chat')),
|
|
67
|
+
__param(2, (0, common_1.Body)()),
|
|
68
|
+
__param(3, (0, common_1.Res)({ passthrough: true })),
|
|
69
|
+
__metadata("design:type", Function),
|
|
70
|
+
__metadata("design:paramtypes", [Object, Object, Object, Object]),
|
|
71
|
+
__metadata("design:returntype", Promise)
|
|
72
|
+
], DocumentsController.prototype, "store", null);
|
|
73
|
+
__decorate([
|
|
74
|
+
(0, common_1.Patch)(['chats/:chat/documents/:slug', 'documents/:slug']),
|
|
75
|
+
__param(0, (0, participant_guard_1.Me)()),
|
|
76
|
+
__param(1, (0, common_1.Param)('chat')),
|
|
77
|
+
__param(2, (0, common_1.Param)('slug')),
|
|
78
|
+
__param(3, (0, common_1.Body)()),
|
|
79
|
+
__metadata("design:type", Function),
|
|
80
|
+
__metadata("design:paramtypes", [Object, Object, String, Object]),
|
|
81
|
+
__metadata("design:returntype", Promise)
|
|
82
|
+
], DocumentsController.prototype, "update", null);
|
|
83
|
+
exports.DocumentsController = DocumentsController = __decorate([
|
|
84
|
+
(0, common_1.Controller)('api'),
|
|
85
|
+
(0, common_1.UseGuards)(participant_guard_1.ParticipantGuard),
|
|
86
|
+
__metadata("design:paramtypes", [chats_service_1.ChatsService, documents_service_1.DocumentsService])
|
|
87
|
+
], DocumentsController);
|