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
package/cli/team-chat.js
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// team-chat — командный чат владельца и ИИ-агентов.
|
|
3
|
+
//
|
|
4
|
+
// team-chat start свой чат на localhost: сервер, раннер агентов, ссылка для входа
|
|
5
|
+
// team-chat login новая одноразовая ссылка для входа в браузер
|
|
6
|
+
// team-chat connect подключить агентов этой машины к чужому чату
|
|
7
|
+
// team-chat participant участники и их токены
|
|
8
|
+
// team-chat dump|restore перенос чата на другую машину
|
|
9
|
+
// team-chat docs документы ↔ репозитории
|
|
10
|
+
// team-chat consent согласие на отправку опыта агентов автору пакета
|
|
11
|
+
// team-chat status что запущено
|
|
12
|
+
//
|
|
13
|
+
// Данные — в ~/.team-chat (или TEAM_CHAT_HOME): база, вложения, логи, конфиг раннера.
|
|
14
|
+
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import os from 'node:os';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import readline from 'node:readline/promises';
|
|
19
|
+
import { detectRuntimes, registerCodexMcp, startRunner, writeRunnerConfig } from './lib/agents.js';
|
|
20
|
+
import { exportDocs, findDocs } from './lib/docs.js';
|
|
21
|
+
import { LocalApi } from './lib/local.js';
|
|
22
|
+
import { defaultHome, legacyData, newSecret, startServer, VERSION } from './lib/server.js';
|
|
23
|
+
import { alive, die, openBrowser, portFree, readJson, say, stop, waitHttp, warn, writeJson } from './lib/util.js';
|
|
24
|
+
|
|
25
|
+
const HELP = `team-chat ${VERSION} — командный чат владельца и ИИ-агентов
|
|
26
|
+
|
|
27
|
+
team-chat start [--port 8600] [--no-open]
|
|
28
|
+
Поднять свой чат на localhost: сервер, раннер агентов (claude, codex — какие
|
|
29
|
+
найдутся), ссылку для входа. Работает, пока не нажмёшь Ctrl+C.
|
|
30
|
+
|
|
31
|
+
team-chat login [@ник]
|
|
32
|
+
Новая одноразовая ссылка для входа в браузер (живёт 15 минут).
|
|
33
|
+
|
|
34
|
+
team-chat connect <адрес чата> --token <токен раннера>
|
|
35
|
+
Подключить агентов этой машины к чужому чату. Токен машины выдаёт владелец того
|
|
36
|
+
чата: «Команда» → «+ машина».
|
|
37
|
+
|
|
38
|
+
team-chat participant [--list] | <ник> [--name …] [--role …] [--kind human|ai] [--reissue]
|
|
39
|
+
Участники: список, завести, перевыпустить токен (показывается один раз).
|
|
40
|
+
|
|
41
|
+
team-chat dump [--file chat-dump.json]
|
|
42
|
+
team-chat restore <файл> [--files <каталог вложений>] [--force]
|
|
43
|
+
Перенос чата: снимок в файл и восстановление из него (заменяет всё). Дамп PHP-версии
|
|
44
|
+
(php artisan chat:dump) тоже подходит.
|
|
45
|
+
|
|
46
|
+
team-chat docs export|import [--chat <slug>] [--root <каталог>] [--dry-run]
|
|
47
|
+
Подтверждённые документы и журнал решений — в репозитории; переписка из .md
|
|
48
|
+
репозиториев — в чат черновиками. Каталог по умолчанию — каталог проекта чата.
|
|
49
|
+
|
|
50
|
+
team-chat consent [on|off]
|
|
51
|
+
Согласие на отправку обезличенного опыта агентов автору пакета: без аргумента —
|
|
52
|
+
текст согласия и что сейчас; on — помогать, off — не отправлять.
|
|
53
|
+
|
|
54
|
+
team-chat status
|
|
55
|
+
Что запущено и где лежат данные.
|
|
56
|
+
|
|
57
|
+
Данные: ${defaultHome()} (переопределить — TEAM_CHAT_HOME).`;
|
|
58
|
+
|
|
59
|
+
/** Флаги без значения; остальные --ключ берут значение (--port 8600 или --port=8600). */
|
|
60
|
+
const SWITCHES = new Set(['no-open', 'reissue', 'list', 'force', 'dry-run']);
|
|
61
|
+
|
|
62
|
+
function parseArgs(argv) {
|
|
63
|
+
const opts = { _: [] };
|
|
64
|
+
for (let i = 0; i < argv.length; i++) {
|
|
65
|
+
const a = argv[i];
|
|
66
|
+
if (!a.startsWith('--')) opts._.push(a);
|
|
67
|
+
else {
|
|
68
|
+
const [k, v] = a.slice(2).split('=', 2);
|
|
69
|
+
opts[k] = SWITCHES.has(k) ? true : v ?? argv[++i];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return opts;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const home = defaultHome();
|
|
76
|
+
const configFile = path.join(home, 'config.json');
|
|
77
|
+
const lockFile = path.join(home, 'team-chat.pid');
|
|
78
|
+
|
|
79
|
+
function loadConfig() {
|
|
80
|
+
return readJson(configFile, {});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function running() {
|
|
84
|
+
const pid = Number(fs.existsSync(lockFile) ? fs.readFileSync(lockFile, 'utf8') : 0);
|
|
85
|
+
return pid && alive(pid) ? pid : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Служебные пути своего запущенного сервера. */
|
|
89
|
+
function local() {
|
|
90
|
+
const cfg = loadConfig();
|
|
91
|
+
if (!cfg.localSecret || !running()) die('чат не запущен на этой машине — сначала team-chat start');
|
|
92
|
+
return new LocalApi(cfg);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------- start
|
|
96
|
+
|
|
97
|
+
async function cmdStart(opts) {
|
|
98
|
+
if (running()) die(`уже запущен (pid ${running()}). Ссылка для входа: team-chat login`);
|
|
99
|
+
fs.mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
100
|
+
if (legacyData(home)) {
|
|
101
|
+
warn('в каталоге данных — база прежней, PHP-версии. Новый сервер её не читает: перенеси её');
|
|
102
|
+
warn(` дампом — в ${path.join(home, 'app')}: php artisan chat:dump, затем team-chat restore <файл>.`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const cfg = loadConfig();
|
|
106
|
+
cfg.port = Number(opts.port ?? cfg.port ?? 8600);
|
|
107
|
+
cfg.localSecret ??= newSecret();
|
|
108
|
+
// Порт занят — дальше нельзя: ответ чужого сервера сошёл бы за наш, и ссылка для
|
|
109
|
+
// входа вела бы в чужой чат.
|
|
110
|
+
if (!(await portFree(cfg.port))) {
|
|
111
|
+
die(`порт ${cfg.port} уже занят другой программой. Запусти на другом: team-chat start --port ${cfg.port + 100}`);
|
|
112
|
+
}
|
|
113
|
+
writeJson(configFile, cfg);
|
|
114
|
+
say(`team-chat ${VERSION}: данные — ${home}`);
|
|
115
|
+
|
|
116
|
+
fs.writeFileSync(lockFile, String(process.pid));
|
|
117
|
+
const server = startServer(home, cfg);
|
|
118
|
+
let runner = null;
|
|
119
|
+
let stopping = false;
|
|
120
|
+
|
|
121
|
+
const shutdown = code => {
|
|
122
|
+
if (stopping) return;
|
|
123
|
+
stopping = true;
|
|
124
|
+
say('\nостанавливаю…');
|
|
125
|
+
stop(runner);
|
|
126
|
+
stop(server);
|
|
127
|
+
fs.rmSync(lockFile, { force: true });
|
|
128
|
+
setTimeout(() => process.exit(code), 500);
|
|
129
|
+
};
|
|
130
|
+
process.on('SIGINT', () => shutdown(0));
|
|
131
|
+
process.on('SIGTERM', () => shutdown(0));
|
|
132
|
+
server.on('exit', code => {
|
|
133
|
+
if (!stopping) { warn(`сервер остановился (код ${code}) — см. ${path.join(home, 'logs', 'server.log')}`); shutdown(1); }
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const url = `http://localhost:${cfg.port}`;
|
|
137
|
+
if (!(await waitHttp(`${url}/up`, 60000, server))) {
|
|
138
|
+
warn(`сервер не поднялся — см. ${path.join(home, 'logs', 'server.log')}`);
|
|
139
|
+
return shutdown(1);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Владелец, раннер и агенты под найденные движки. Повторный запуск ничего не ломает.
|
|
143
|
+
const runtimes = detectRuntimes();
|
|
144
|
+
const api = new LocalApi(cfg);
|
|
145
|
+
const setup = await api.post('/setup', {
|
|
146
|
+
name: os.userInfo().username,
|
|
147
|
+
agents: Object.entries(runtimes).map(([name, r]) => `${name}:${name}:${r.role}`),
|
|
148
|
+
// Токена раннера у нас нет — нужен новый.
|
|
149
|
+
rotate_host: !cfg.hostToken,
|
|
150
|
+
});
|
|
151
|
+
if (setup.host_token) {
|
|
152
|
+
cfg.hostToken = setup.host_token;
|
|
153
|
+
writeJson(configFile, cfg);
|
|
154
|
+
}
|
|
155
|
+
if (setup.agents.length) say(`→ агенты: ${setup.agents.map(a => '@' + a).join(', ')}`);
|
|
156
|
+
if (!Object.keys(runtimes).length) warn('не нашёл ни claude, ни codex — чат работает, но отвечать в нём некому.');
|
|
157
|
+
|
|
158
|
+
writeRunnerConfig(home, runtimes);
|
|
159
|
+
registerCodexMcp(url);
|
|
160
|
+
await askConsentOnce(api);
|
|
161
|
+
|
|
162
|
+
// Раннер упал — поднимаем снова, но не чаще раза в 10 с.
|
|
163
|
+
const launchRunner = () => {
|
|
164
|
+
if (stopping) return;
|
|
165
|
+
runner = startRunner(home, { api: `http://127.0.0.1:${cfg.port}/api`, token: cfg.hostToken });
|
|
166
|
+
runner.on('exit', code => {
|
|
167
|
+
if (stopping) return;
|
|
168
|
+
warn(`раннер остановился (код ${code}) — перезапуск через 10 с, см. ${path.join(home, 'logs', 'runner.log')}`);
|
|
169
|
+
setTimeout(launchRunner, 10000);
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
launchRunner();
|
|
173
|
+
|
|
174
|
+
say(`\n✓ чат работает: ${url}`);
|
|
175
|
+
say(` вход (одноразовая ссылка, 15 мин): ${setup.login_url}`);
|
|
176
|
+
say(` логи: ${path.join(home, 'logs')} остановить: Ctrl+C\n`);
|
|
177
|
+
if (!opts['no-open']) openBrowser(setup.login_url);
|
|
178
|
+
|
|
179
|
+
// Держим процесс: сервер и раннер — наши дети.
|
|
180
|
+
await new Promise(() => {});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------- login
|
|
184
|
+
|
|
185
|
+
async function cmdLogin(opts) {
|
|
186
|
+
const { url } = await local().post('/login', { handle: opts._[1] ?? null });
|
|
187
|
+
say(url);
|
|
188
|
+
openBrowser(url);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------- connect
|
|
192
|
+
|
|
193
|
+
async function cmdConnect(opts) {
|
|
194
|
+
const url = (opts._[1] || '').replace(/\/+$/, '');
|
|
195
|
+
if (!/^https?:\/\//.test(url) || !opts.token) die('нужно: team-chat connect <адрес чата> --token <токен раннера>');
|
|
196
|
+
if (running()) die('на этой машине уже запущен team-chat start — раннер у него свой.');
|
|
197
|
+
fs.mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
198
|
+
|
|
199
|
+
const ok = await fetch(`${url}/api/runner/agents`, { headers: { Authorization: `Bearer ${opts.token}`, Accept: 'application/json' } })
|
|
200
|
+
.then(r => r.status).catch(() => 0);
|
|
201
|
+
if (ok === 401 || ok === 403) die('чат не принял токен: нужен токен машины (участник host) этого чата.');
|
|
202
|
+
if (ok !== 200) die(`чат по адресу ${url} не отвечает.`);
|
|
203
|
+
|
|
204
|
+
const cfg = loadConfig();
|
|
205
|
+
cfg.remote = { url, token: opts.token };
|
|
206
|
+
writeJson(configFile, cfg);
|
|
207
|
+
|
|
208
|
+
const runtimes = detectRuntimes();
|
|
209
|
+
if (!Object.keys(runtimes).length) die('на этой машине нет ни claude, ни codex — подключать некого.');
|
|
210
|
+
writeRunnerConfig(home, runtimes);
|
|
211
|
+
registerCodexMcp(url);
|
|
212
|
+
|
|
213
|
+
fs.writeFileSync(lockFile, String(process.pid));
|
|
214
|
+
let runner = null;
|
|
215
|
+
let stopping = false;
|
|
216
|
+
const shutdown = () => { stopping = true; stop(runner); fs.rmSync(lockFile, { force: true }); setTimeout(() => process.exit(0), 300); };
|
|
217
|
+
process.on('SIGINT', shutdown);
|
|
218
|
+
process.on('SIGTERM', shutdown);
|
|
219
|
+
|
|
220
|
+
const launch = () => {
|
|
221
|
+
if (stopping) return;
|
|
222
|
+
runner = startRunner(home, { api: `${url}/api`, token: opts.token });
|
|
223
|
+
runner.on('exit', code => { if (!stopping) { warn(`раннер остановился (код ${code}) — перезапуск через 10 с`); setTimeout(launch, 10000); } });
|
|
224
|
+
};
|
|
225
|
+
launch();
|
|
226
|
+
|
|
227
|
+
say(`✓ агенты этой машины (${Object.keys(runtimes).join(', ')}) подключены к ${url}`);
|
|
228
|
+
say(' каких агентов запускать, решает владелец того чата: движок агента должен быть здесь.');
|
|
229
|
+
say(` логи: ${path.join(home, 'logs')} остановить: Ctrl+C`);
|
|
230
|
+
await new Promise(() => {});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---------------------------------------------------------------- participant
|
|
234
|
+
|
|
235
|
+
async function cmdParticipant(opts) {
|
|
236
|
+
const api = local();
|
|
237
|
+
const handle = opts._[1]?.replace(/^@/, '');
|
|
238
|
+
if (opts.list || !handle) {
|
|
239
|
+
for (const p of (await api.get('/participants')).participants) {
|
|
240
|
+
say(`${p.handle.padEnd(16)} ${p.name.padEnd(16)} ${p.role.padEnd(12)} ${p.kind.padEnd(6)} токен: ${p.token ? 'есть' : '—'} был: ${p.last_seen_at ?? 'не заходил'}`);
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const r = await api.post('/participants', { handle, name: opts.name, role: opts.role, kind: opts.kind, accent: opts.accent, reissue: !!opts.reissue });
|
|
245
|
+
if (!r.token) return say(`@${r.handle} обновлён. Токен не тронут (нужен --reissue).`);
|
|
246
|
+
say(r.created ? `Участник @${r.handle} создан. Токен:` : `Токен перевыпущен для @${r.handle}:`);
|
|
247
|
+
say(` ${r.token}`);
|
|
248
|
+
say(' Показывается один раз — у нас хранится только хеш.');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ---------------------------------------------------------------- dump / restore
|
|
252
|
+
|
|
253
|
+
async function cmdDump(opts) {
|
|
254
|
+
const file = path.resolve(opts.file ?? 'chat-dump.json');
|
|
255
|
+
const dump = await local().get('/dump');
|
|
256
|
+
fs.writeFileSync(file, JSON.stringify(dump, null, 2) + '\n', { mode: 0o600 });
|
|
257
|
+
for (const [table, rows] of Object.entries(dump.tables)) say(` ${table.padEnd(18)} ${rows.length}`);
|
|
258
|
+
say(`Выгружено в ${file}. Вложения — файлы в ${path.join(home, 'files')}, переносятся отдельно.`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function cmdRestore(opts) {
|
|
262
|
+
const file = opts._[1];
|
|
263
|
+
if (!file || !fs.existsSync(file)) die('нужно: team-chat restore <файл дампа> [--files <каталог вложений>]');
|
|
264
|
+
const dump = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
265
|
+
if (!dump.tables) die('файл не похож на дамп чата.');
|
|
266
|
+
|
|
267
|
+
say(`Снимок от ${dump.exported_at ?? 'неизвестно'}:`);
|
|
268
|
+
for (const [table, rows] of Object.entries(dump.tables)) say(` ${table.padEnd(18)} ${rows.length}`);
|
|
269
|
+
if (!opts.force) die('текущее содержимое чата будет заменено целиком. Если согласен — повтори с --force.');
|
|
270
|
+
|
|
271
|
+
const { restored } = await local().post('/restore', dump);
|
|
272
|
+
say(`✓ восстановлено: ${Object.values(restored).reduce((a, b) => a + b, 0)} строк`);
|
|
273
|
+
// Раннер этой машины пришёл из дампа со своим токеном — наш сохранённый больше не годится:
|
|
274
|
+
// следующий start выпустит новый.
|
|
275
|
+
const cfg = loadConfig();
|
|
276
|
+
delete cfg.hostToken;
|
|
277
|
+
writeJson(configFile, cfg);
|
|
278
|
+
|
|
279
|
+
// Файлы вложений: у PHP-версии — storage/app/private/attachments, здесь — files/attachments.
|
|
280
|
+
if (opts.files) {
|
|
281
|
+
const from = path.resolve(opts.files);
|
|
282
|
+
const to = path.join(home, 'files', 'attachments');
|
|
283
|
+
if (!fs.existsSync(from)) die(`каталога ${from} нет.`);
|
|
284
|
+
fs.mkdirSync(to, { recursive: true, mode: 0o700 });
|
|
285
|
+
fs.cpSync(from, to, { recursive: true });
|
|
286
|
+
say(`✓ вложения скопированы в ${to}`);
|
|
287
|
+
}
|
|
288
|
+
say('Токены раннеров и браузеров не переносятся: перезапусти team-chat start — раннер возьмёт новый токен;');
|
|
289
|
+
say('в браузер — войти заново (team-chat login).');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ---------------------------------------------------------------- docs
|
|
293
|
+
|
|
294
|
+
async function cmdDocs(opts) {
|
|
295
|
+
const action = opts._[1];
|
|
296
|
+
if (!['export', 'import'].includes(action)) die('нужно: team-chat docs export|import [--chat <slug>] [--root <каталог>] [--dry-run]');
|
|
297
|
+
const api = local();
|
|
298
|
+
const data = await api.get(`/export${opts.chat ? `?chat=${encodeURIComponent(opts.chat)}` : ''}`);
|
|
299
|
+
const root = path.resolve(opts.root ?? data.chat.workdir ?? die(`у чата «${data.chat.slug}» не задан каталог проекта — укажи --root.`));
|
|
300
|
+
const dryRun = Boolean(opts['dry-run']);
|
|
301
|
+
|
|
302
|
+
if (action === 'export') {
|
|
303
|
+
const written = exportDocs(data, root, dryRun);
|
|
304
|
+
return say(dryRun ? `${written} файл(ов) было бы записано (--dry-run).` : `${written} файл(ов) записано. Не забудь закоммитить в репозиториях.`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const documents = findDocs(root);
|
|
308
|
+
for (const d of documents) say(` ${d.origin.padEnd(50)} ${d.kind.padEnd(10)} ${d.slug}`);
|
|
309
|
+
if (documents.length === 0) return warn(`в ${root} не нашлось документов для переноса.`);
|
|
310
|
+
if (dryRun) return say(`${documents.length} документ(ов) — ничего не записано (--dry-run).`);
|
|
311
|
+
const { imported } = await api.post('/documents', { chat: data.chat.slug, documents });
|
|
312
|
+
say(`${imported} документ(ов) перенесено черновиками. Файлы в репозиториях НЕ удалены — удаляй сам, когда сверишь.`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ---------------------------------------------------------------- consent
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Спросить согласие, если его не давали (или текст с тех пор изменился). Без терминала
|
|
319
|
+
* спросить некого — не спрашиваем и ничего не отправляем.
|
|
320
|
+
*/
|
|
321
|
+
async function askConsentOnce(api) {
|
|
322
|
+
const status = await api.get('/consent');
|
|
323
|
+
if (status.answer !== null) return;
|
|
324
|
+
if (!process.stdin.isTTY) return say('опыт агентов автору пакета не отправляется — согласие: team-chat consent on');
|
|
325
|
+
|
|
326
|
+
say(`\n${status.text}\n`);
|
|
327
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
328
|
+
let answer = null;
|
|
329
|
+
while (answer === null) {
|
|
330
|
+
const reply = (await rl.question('Помогать? (да/нет) ')).trim().toLowerCase();
|
|
331
|
+
answer = ['да', 'д', 'yes', 'y'].includes(reply) ? true : ['нет', 'н', 'no', 'n'].includes(reply) ? false : null;
|
|
332
|
+
}
|
|
333
|
+
rl.close();
|
|
334
|
+
await api.post('/consent', { answer });
|
|
335
|
+
say(answer ? '✓ спасибо! Передумаешь — team-chat consent off\n' : '✓ ничего не отправляется. Передумаешь — team-chat consent on\n');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function cmdConsent(opts) {
|
|
339
|
+
const api = local();
|
|
340
|
+
const action = opts._[1];
|
|
341
|
+
if (action && !['on', 'off'].includes(action)) die('нужно: team-chat consent [on|off]');
|
|
342
|
+
|
|
343
|
+
const status = action ? await api.post('/consent', { answer: action === 'on' }) : await api.get('/consent');
|
|
344
|
+
if (!action) say(`${status.text}\n`);
|
|
345
|
+
say(`согласие: ${status.answer === null ? 'не спрашивали' : status.answer ? `да (${status.answered_at})` : `нет (${status.answered_at})`}`);
|
|
346
|
+
say(`карточек опыта ждут отправки: ${status.pending}`);
|
|
347
|
+
if (!status.answer) return;
|
|
348
|
+
if (!status.collector) return say('сборщика у автора пакета пока нет — карточки уйдут, когда он появится.');
|
|
349
|
+
if (action !== 'on') return say('отправка — сама, раз в 5 минут.');
|
|
350
|
+
const { sent } = await api.post('/share');
|
|
351
|
+
say(`отправлено сейчас: ${sent}; дальше — само, раз в 5 минут.`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ---------------------------------------------------------------- status
|
|
355
|
+
|
|
356
|
+
async function cmdStatus() {
|
|
357
|
+
const cfg = loadConfig();
|
|
358
|
+
const pid = running();
|
|
359
|
+
say(`данные: ${home}`);
|
|
360
|
+
if (!cfg.port && !cfg.remote) return say('ещё не запускался: team-chat start');
|
|
361
|
+
say(pid ? `запущен (pid ${pid})` : 'не запущен');
|
|
362
|
+
if (cfg.remote) say(`подключён к: ${cfg.remote.url}`);
|
|
363
|
+
if (cfg.port) {
|
|
364
|
+
const up = await waitHttp(`http://localhost:${cfg.port}/up`, 1500);
|
|
365
|
+
say(`свой чат: http://localhost:${cfg.port} — ${up ? 'отвечает' : 'не отвечает'}`);
|
|
366
|
+
}
|
|
367
|
+
const runtimes = Object.keys(readJson(path.join(home, 'runner.json'), { runtimes: {} }).runtimes);
|
|
368
|
+
say(`движки агентов: ${runtimes.join(', ') || 'нет'} (${path.join(home, 'runner.json')})`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ----------------------------------------------------------------
|
|
372
|
+
|
|
373
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
374
|
+
const commands = {
|
|
375
|
+
start: cmdStart, login: cmdLogin, connect: cmdConnect, participant: cmdParticipant,
|
|
376
|
+
dump: cmdDump, restore: cmdRestore, docs: cmdDocs, consent: cmdConsent, status: cmdStatus,
|
|
377
|
+
};
|
|
378
|
+
const command = commands[opts._[0]];
|
|
379
|
+
if (!command) {
|
|
380
|
+
say(HELP);
|
|
381
|
+
process.exit(opts._[0] && !['help', '--help', '-h'].includes(opts._[0]) ? 1 : 0);
|
|
382
|
+
}
|
|
383
|
+
await command(opts);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Счётчики неудачных попыток по обращениям — файл .bridge-state-<handle>.json.
|
|
2
|
+
//
|
|
3
|
+
// Без них дежурный цикл перезапускал бы агента на стабильно падающем обращении каждые
|
|
4
|
+
// десять секунд и молча жёг лимиты. Файл читается заново на каждом проходе: снять
|
|
5
|
+
// отложенное обращение можно, удалив его ключ руками.
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
|
|
9
|
+
export class AttemptStore {
|
|
10
|
+
#file;
|
|
11
|
+
#counts;
|
|
12
|
+
|
|
13
|
+
constructor(file, counts = {}) {
|
|
14
|
+
this.#file = file;
|
|
15
|
+
this.#counts = counts;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static load(file) {
|
|
19
|
+
try {
|
|
20
|
+
return new AttemptStore(file, JSON.parse(fs.readFileSync(file, 'utf8')));
|
|
21
|
+
} catch {
|
|
22
|
+
return new AttemptStore(file);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
count(mentionId) {
|
|
27
|
+
return this.#counts[mentionId] ?? 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Ещё одна неудача. → сколько их теперь. */
|
|
31
|
+
fail(mentionId) {
|
|
32
|
+
this.#counts[mentionId] = this.count(mentionId) + 1;
|
|
33
|
+
return this.#counts[mentionId];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
forget(mentionId) {
|
|
37
|
+
delete this.#counts[mentionId];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
save() {
|
|
41
|
+
fs.writeFileSync(this.#file, JSON.stringify(this.#counts, null, 1));
|
|
42
|
+
}
|
|
43
|
+
}
|