work-license-server 1.0.2 → 1.0.4

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/bin/server.mjs CHANGED
@@ -2,13 +2,16 @@
2
2
  /**
3
3
  * Точка запуска лицензионного сервера и лендинга ПАК WORK (work.odant.org).
4
4
  */
5
+ import { fileURLToPath } from 'node:url';
5
6
  import { createLicenseServerApp } from '../src/server.js';
6
7
 
7
8
  const port = Number(process.env.PORT || process.env.WORK_LICENSE_PORT || 8020);
8
- const dataDir = process.env.WORK_LICENSE_DATA_DIR || './data';
9
+ const defaultDataDir = fileURLToPath(new URL('../data', import.meta.url));
10
+ const dataDir = process.env.WORK_LICENSE_DATA_DIR || defaultDataDir;
9
11
 
10
12
  const app = createLicenseServerApp({ dataDir });
11
13
  const server = await app.listen(port);
12
14
  const addr = server.address();
13
15
  const actualPort = typeof addr === 'object' && addr ? addr.port : port;
14
- console.log(`[work.odant.org] Лицензионный сервер и лендинг ПАК WORK запущен: http://localhost:${actualPort}/`);
16
+ console.log(`[work.odant.org] Витрина ODANT-WORK: http://localhost:${actualPort}/`);
17
+ console.log(`[work.odant.org] Панель администратора (отдельная страница): http://localhost:${actualPort}/admin (логин: ${app.store.adminLogin})`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "work-license-server",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Лицензионный сервер, биллинг ЮKassa, боты поддержки (MAX, Telegram, VK) и лендинг ПАК WORK (work.odant.org)",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/admin.js ADDED
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Отдельная защищённая страница панели администратора центра лицензирования и биллинга ODANT-WORK (/admin).
3
+ *
4
+ * Не связана ссылками с публичной витриной (/).
5
+ * Требует обязательной авторизации администратора (POST /api/admin/login -> Bearer adm_...).
6
+ */
7
+ import {
8
+ PAK_PRODUCT,
9
+ SLA_TIERS,
10
+ FRANCHISE_PROGRAM,
11
+ REGIONS_CATALOG,
12
+ PARTNER_TRAINING_TRACKS,
13
+ VENDOR_CONTACTS,
14
+ } from './catalog.js';
15
+ import { icon } from './landing.js';
16
+
17
+ const esc = (s) => String(s ?? '').replace(/[&<>"']/g, ch => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', '\'': '&#39;' }[ch]));
18
+ const rub = (n) => Number(n || 0).toLocaleString('ru-RU').replace(/\u00a0/g, ' ') + ' ₽';
19
+ const jsonForScript = (v) => JSON.stringify(v).replace(/</g, '\\u003c').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
20
+
21
+ export function renderAdminPage({ publicKey = '', assetVersion = '' } = {}) {
22
+ const v = assetVersion ? '?v=' + encodeURIComponent(assetVersion) : '';
23
+ const P = PAK_PRODUCT;
24
+ const visibleRegions = REGIONS_CATALOG.filter(r => !r.hidden);
25
+
26
+ const adminData = {
27
+ product: P,
28
+ tiers: Object.values(SLA_TIERS),
29
+ franchise: FRANCHISE_PROGRAM,
30
+ regions: visibleRegions,
31
+ tracks: PARTNER_TRAINING_TRACKS,
32
+ vendor: VENDOR_CONTACTS,
33
+ publicKey,
34
+ };
35
+
36
+ return /* html */`<!DOCTYPE html>
37
+ <html lang="ru" data-theme="dark">
38
+ <head>
39
+ <meta charset="utf-8">
40
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
41
+ <title>ODANT-WORK · Консоль администратора центра лицензирования</title>
42
+ <meta name="robots" content="noindex, nofollow">
43
+ <meta name="theme-color" content="#04060b">
44
+ <link rel="icon" type="image/png" href="/favicon.ico">
45
+ <link rel="preconnect" href="https://fonts.googleapis.com">
46
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
47
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Manrope:wght@600;700;800&display=swap">
48
+ <link rel="stylesheet" href="/assets/landing.css${v}">
49
+ <script>try{var t=localStorage.getItem('odant_work_theme');if(t)document.documentElement.setAttribute('data-theme',t)}catch(e){}</script>
50
+ </head>
51
+ <body data-stage="admin">
52
+ <div class="bg-aurora" aria-hidden="true"><i></i><i></i><i></i></div>
53
+ <div class="bg-grid" aria-hidden="true"></div>
54
+ <div class="bg-vignette" aria-hidden="true"></div>
55
+
56
+ <!-- Экран защищённого входа администратора -->
57
+ <div class="admin-auth-gate" id="adminLoginScreen">
58
+ <div class="card card-glow admin-login-card">
59
+ <div class="modal-head" style="margin-bottom:18px">
60
+ <img src="/assets/odant-logo.png" alt="ODANT">
61
+ <div>
62
+ <span class="kicker">Защищённый контур</span>
63
+ <h1 class="h3" style="margin:4px 0 2px;font-size:21px">Админ-панель ODANT-WORK</h1>
64
+ <div class="small muted">Центр сертификации Ed25519 и биллинга ЮKassa</div>
65
+ </div>
66
+ </div>
67
+ <form id="adminLoginForm" onsubmit="return false;">
68
+ <div class="field">
69
+ <label for="admLoginUser">Логин администратора</label>
70
+ <input type="text" id="admLoginUser" value="admin" autocomplete="username" required>
71
+ </div>
72
+ <div class="field">
73
+ <label for="admLoginPass">Пароль / Ключ администратора (WORK_ADMIN_PASSWORD)</label>
74
+ <input type="password" id="admLoginPass" placeholder="Введите ключ администратора…" autocomplete="current-password" required>
75
+ </div>
76
+ <div class="status" id="admLoginError" style="color:var(--danger);margin:6px 0 12px;min-height:18px"></div>
77
+ <button type="submit" class="btn btn-primary btn-lg" style="width:100%" id="btnAdminLoginSubmit">
78
+ ${icon('lock')} Войти в панель администратора
79
+ </button>
80
+ </form>
81
+ <div class="small muted" style="margin-top:14px;padding-top:12px;border-top:1px solid var(--line)">
82
+ Ключ ЦС Ed25519: <code>${esc(publicKey.slice(0, 26))}…</code>
83
+ </div>
84
+ </div>
85
+ </div>
86
+
87
+ <!-- Основная консоль администратора (скрыта до успешной авторизации) -->
88
+ <div class="shell" id="adminAppShell" style="display:none">
89
+ <header class="topbar" style="grid-template-columns: auto 1fr auto">
90
+ <div class="brand" style="cursor:default">
91
+ <img src="/assets/odant-logo.png" alt="ODANT">
92
+ <span>
93
+ <span class="brand-name">ODANT<b>·</b>WORK <span class="tag" style="margin-left:6px;vertical-align:middle">ADMIN</span></span><br>
94
+ <span class="brand-sub">центр сертификации Ed25519 · биллинг ЮKassa · франшизы · SLA</span>
95
+ </span>
96
+ </div>
97
+ <div></div>
98
+ <div class="top-actions">
99
+ <button class="icon-btn" id="btnTheme" title="Светлая / тёмная тема" aria-label="Переключить тему">${icon('sun')}</button>
100
+ <button class="btn btn-ghost btn-sm" data-action="registry">${icon('server')}<span>Обновить реестр</span></button>
101
+ <button class="btn btn-ok btn-sm" data-action="demo-pak">+ Демо-лицензия ПАК</button>
102
+ <button class="btn btn-ghost btn-sm" data-action="admin-logout" title="Завершить сессию администратора">${icon('lock')}<span id="admUserLabel">Выйти</span></button>
103
+ </div>
104
+ </header>
105
+
106
+ <section class="stage active" style="padding-top:26px">
107
+ <div class="stats" style="grid-template-columns:repeat(6,1fr)">
108
+ <div class="stat"><b id="admStatLic">0</b><span id="admStatLicSub">лицензий ПАК · 0 мест</span></div>
109
+ <div class="stat"><b id="admStatPaid">0 ₽</b><span id="admStatPaidSub">оплачено заказов: 0</span></div>
110
+ <div class="stat"><b id="admStatPending">0 ₽</b><span id="admStatPendingSub">ожидают оплаты: 0</span></div>
111
+ <div class="stat"><b id="admStatPartners">0 / 0</b><span>партнёров / резервов франшиз</span></div>
112
+ <div class="stat"><b id="admStatUsers">0</b><span id="admStatUsersSub">клиентов (оферт ПЭП: 0)</span></div>
113
+ <div class="stat"><b id="admStatTickets">0</b><span id="admStatTicketsSub">открытых тикетов SLA</span></div>
114
+ </div>
115
+
116
+ <div class="admin-workspace" id="adminWorkspace">
117
+ <!-- Левая панель навигации и ИИ-агента администратора -->
118
+ <aside class="admin-left">
119
+ <nav class="admin-nav" aria-label="Вкладки админ-панели">
120
+ <button class="adm-tab on" data-adm-tab="all">${icon('ai')}<span>ИИ-агент + Все разделы</span><i class="live-dot"></i></button>
121
+ <button class="adm-tab" data-adm-tab="agent">${icon('agents')}<span>Только ИИ-агент</span></button>
122
+ <button class="adm-tab" data-adm-tab="licenses">${icon('key')}<span>Лицензии (${P.baseSeats} вечн. + доп.)</span></button>
123
+ <button class="adm-tab" data-adm-tab="orders">${icon('cart')}<span>Заказы ЮKassa</span></button>
124
+ <button class="adm-tab" data-adm-tab="partners">${icon('map')}<span>Франшизы и партнёры</span></button>
125
+ <button class="adm-tab" data-adm-tab="tickets">${icon('chat')}<span>Тикеты SLA (MAX·TG·VK)</span></button>
126
+ <button class="adm-tab" data-adm-tab="users" style="grid-column:1 / -1">${icon('user')}<span>Зарегистрированные клиенты и подписанные оферты (ПЭП)</span></button>
127
+ </nav>
128
+
129
+ <div class="card adm-agent-card" id="admAgentCard">
130
+ <div class="ai-head" style="padding:0 0 12px;margin-bottom:10px">
131
+ <img src="/assets/odant-logo.png" alt="">
132
+ <div>
133
+ <b>ИИ-агент администратора</b>
134
+ <span>● управление реестром · защищённый контур</span>
135
+ </div>
136
+ </div>
137
+ <div class="ai-msgs adm-ai-msgs" id="admAiMessages">
138
+ <div class="msg bot">Здравствуйте! Я ИИ-агент консоли администратора ODANT-WORK. Помогу проанализировать реестр лицензий (${P.baseSeats} вечных мест + 1 год Enterprise), выпустить сертификат Ed25519, провести оплату заказов ЮKassa, активировать резервы франшиз или проверить квоты по регионам РФ.</div>
139
+ </div>
140
+ <div class="ai-sugg" style="padding:8px 0;border-top:1px solid var(--line)">
141
+ <button data-adm-ask="Сводка по серверу лицензий">Сводка реестра</button>
142
+ <button data-adm-ask="Выпусти базовую лицензию для ООО «АгроТех» ИНН 6234001122">+ Выпустить лицензию</button>
143
+ <button data-adm-ask="Проведи оплату ожидающих заказов">Провести оплаты</button>
144
+ <button data-adm-ask="Активируй резервы франшиз">Активировать резервы</button>
145
+ <button data-adm-ask="Сколько вакантных франшиз в Татарстане?">Квота Татарстана</button>
146
+ </div>
147
+ <div class="ai-input" style="padding:10px 0 0">
148
+ <input type="text" id="admAiInput" placeholder="Команда или вопрос ИИ-агенту админки…" autocomplete="off">
149
+ <button class="btn btn-primary" data-action="adm-ai-send" aria-label="Отправить">${icon('send')}</button>
150
+ </div>
151
+ </div>
152
+ </aside>
153
+
154
+ <!-- Правая область разделов админки -->
155
+ <div class="admin-main" id="adminMain">
156
+ <!-- 1. ЛИЦЕНЗИИ -->
157
+ <div class="card" data-adm-sec="licenses">
158
+ <div class="sec-head" style="margin-bottom:14px">
159
+ <div>
160
+ <span class="kicker">Выпуск сертификата Ed25519</span>
161
+ <h3 class="h3" style="margin:4px 0 0">Выпустить базовую лицензию комплекса (${P.baseSeats} вечных лицензий + 1 год поддержки Enterprise)</h3>
162
+ </div>
163
+ </div>
164
+ <div class="grid g4" style="align-items:end">
165
+ <div class="field" style="margin:0"><label for="admLicOrg">Организация</label><input type="text" id="admLicOrg" value="АО «РязаньАгроПром»"></div>
166
+ <div class="field" style="margin:0"><label for="admLicInn">ИНН</label><input type="text" id="admLicInn" value="6234109876"></div>
167
+ <div class="field" style="margin:0"><label for="admLicEmail">Email клиента</label><input type="email" id="admLicEmail" value="it@ryazan-agro.ru"></div>
168
+ <div class="field" style="margin:0"><label for="admLicHwid">HWID комплекса (необязательно)</label><input type="text" id="admLicHwid" placeholder="PAK-..."></div>
169
+ </div>
170
+ <div style="display:flex;align-items:center;gap:14px;margin-top:14px;flex-wrap:wrap">
171
+ <button class="btn btn-primary btn-sm" data-action="adm-issue-lic">${icon('key')} Выпустить и подписать сертификат</button>
172
+ <span class="small muted" id="admIssueStatus"></span>
173
+ </div>
174
+ </div>
175
+
176
+ <div class="card" data-adm-sec="licenses">
177
+ <h3 class="h3" style="margin:0 0 12px">Выпущенные лицензии комплексов ODANT-WORK</h3>
178
+ <div class="table-wrap">
179
+ <table class="t">
180
+ <thead><tr>
181
+ <th>Лицензия / Код бота</th>
182
+ <th>Организация / ИНН</th>
183
+ <th>Привязка оборудования / Партнёр</th>
184
+ <th class="num">Сессии (вечные + доп.)</th>
185
+ <th>SLA и срок</th>
186
+ <th>Сертификат</th>
187
+ </tr></thead>
188
+ <tbody id="admLicensesBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
189
+ </table>
190
+ </div>
191
+ </div>
192
+
193
+ <!-- 2. ЗАКАЗЫ ЮKASSA -->
194
+ <div class="card" data-adm-sec="orders">
195
+ <h3 class="h3" style="margin:0 0 12px">Заказы и платежи ЮKassa</h3>
196
+ <div class="table-wrap">
197
+ <table class="t">
198
+ <thead><tr>
199
+ <th>Заказ / Платёж</th>
200
+ <th>Назначение</th>
201
+ <th>Плательщик</th>
202
+ <th class="num">Сумма</th>
203
+ <th>Статус</th>
204
+ <th>Действия</th>
205
+ </tr></thead>
206
+ <tbody id="admOrdersBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
207
+ </table>
208
+ </div>
209
+ </div>
210
+
211
+ <!-- 3. ФРАНШИЗЫ И ПАРТНЁРЫ -->
212
+ <div class="card" data-adm-sec="partners">
213
+ <div class="sec-head" style="margin-bottom:14px">
214
+ <div>
215
+ <span class="kicker">Региональная партнёрская сеть</span>
216
+ <h3 class="h3" style="margin:4px 0 0">Зарегистрировать партнёра-франчайзи вручную</h3>
217
+ </div>
218
+ </div>
219
+ <div class="grid g4" style="align-items:end">
220
+ <div class="field" style="margin:0">
221
+ <label for="admPartRegion">Субъект РФ</label>
222
+ <select id="admPartRegion">
223
+ ${visibleRegions.map(r => `<option value="${esc(r.code)}" ${r.code === 'RU-RYA' ? 'selected' : ''}>${esc(r.name)} (${esc(r.district)})</option>`).join('')}
224
+ </select>
225
+ </div>
226
+ <div class="field" style="margin:0"><label for="admPartCompany">Компания-партнёр</label><input type="text" id="admPartCompany" value="ООО «ЦентрИнтеграция»"></div>
227
+ <div class="field" style="margin:0"><label for="admPartInn">ИНН</label><input type="text" id="admPartInn" value="6234556677"></div>
228
+ <div class="field" style="margin:0"><label for="admPartEmail">Email партнёра</label><input type="email" id="admPartEmail" value="partner@center-it.ru"></div>
229
+ </div>
230
+ <div style="display:flex;align-items:center;gap:14px;margin-top:14px;flex-wrap:wrap">
231
+ <button class="btn btn-primary btn-sm" data-action="adm-register-partner">${icon('map')} Зарегистрировать партнёра</button>
232
+ <span class="small muted" id="admPartnerStatus"></span>
233
+ </div>
234
+ </div>
235
+
236
+ <div class="card" data-adm-sec="partners">
237
+ <h3 class="h3" style="margin:0 0 12px">Резервы региональных франшиз и действующие партнёры</h3>
238
+ <div class="table-wrap">
239
+ <table class="t">
240
+ <thead><tr>
241
+ <th>ID / Тип</th>
242
+ <th>Субъект РФ</th>
243
+ <th>Компания / ИНН</th>
244
+ <th>Условия / Скидка</th>
245
+ <th>Статус и аттестация</th>
246
+ <th>Действия</th>
247
+ </tr></thead>
248
+ <tbody id="admPartnersBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
249
+ </table>
250
+ </div>
251
+ </div>
252
+
253
+ <!-- 4. ТИКЕТЫ SLA -->
254
+ <div class="card" data-adm-sec="tickets">
255
+ <h3 class="h3" style="margin:0 0 12px">Обращения техподдержки (MAX · Telegram · VK)</h3>
256
+ <div class="table-wrap">
257
+ <table class="t">
258
+ <thead><tr>
259
+ <th>Тикет / Канал</th>
260
+ <th>Клиент / Лицензия</th>
261
+ <th>SLA</th>
262
+ <th>Переписка</th>
263
+ <th>Ответ инженера</th>
264
+ </tr></thead>
265
+ <tbody id="admTicketsBody"><tr><td colspan="5" class="muted">Загрузка…</td></tr></tbody>
266
+ </table>
267
+ </div>
268
+ </div>
269
+
270
+ <!-- 5. КЛИЕНТЫ И ОФЕРТЫ ПЭП -->
271
+ <div class="card" data-adm-sec="users">
272
+ <h3 class="h3" style="margin:0 0 12px">Зарегистрированные клиенты и подписанные договоры-оферты (ПЭП)</h3>
273
+ <div class="table-wrap">
274
+ <table class="t">
275
+ <thead><tr>
276
+ <th>ID / Email</th>
277
+ <th>Организация / ИНН</th>
278
+ <th>Представитель</th>
279
+ <th>Лицензия комплекса</th>
280
+ <th>Статус оферты (ПЭП)</th>
281
+ <th>Дата акцепта</th>
282
+ </tr></thead>
283
+ <tbody id="admUsersBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
284
+ </table>
285
+ </div>
286
+ </div>
287
+ </div>
288
+ </div>
289
+ </section>
290
+
291
+ <footer>
292
+ <div class="footer-bottom">
293
+ <span>© ODANT · Консоль администратора центра лицензирования ${esc(P.domain)}</span>
294
+ <span>Ключ центра сертификации Ed25519: <code>${esc(publicKey)}</code></span>
295
+ </div>
296
+ </footer>
297
+ </div>
298
+
299
+ <!-- Просмотр подписанного сертификата Ed25519 -->
300
+ <div class="modal" id="certModal" role="dialog" aria-modal="true" aria-labelledby="certTitle">
301
+ <div class="card" style="max-width:640px">
302
+ <div class="modal-head">
303
+ <img src="/assets/odant-logo.png" alt="">
304
+ <div><h3 class="h3" id="certTitle" style="margin:0">Сертификат лицензии Ed25519</h3><div class="small muted" id="certSub">WORK-LIC-v1</div></div>
305
+ </div>
306
+ <pre class="console" id="certJsonBox" style="max-height:360px;margin:0"></pre>
307
+ <div style="display:flex;gap:10px;margin-top:14px;flex-wrap:wrap">
308
+ <button class="btn btn-primary" data-action="cert-copy">Скопировать JSON</button>
309
+ <button class="btn btn-ok" data-action="cert-download">Скачать .json</button>
310
+ <button class="btn btn-ghost" data-action="cert-close">Закрыть</button>
311
+ </div>
312
+ </div>
313
+ </div>
314
+
315
+ <script>window.__ODANT_ADMIN__ = ${jsonForScript(adminData)};</script>
316
+ <script src="/assets/admin.js${v}" defer></script>
317
+ </body>
318
+ </html>`;
319
+ }
package/src/agent.js CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  FRANCHISE_PROGRAM,
16
16
  REGIONS_CATALOG,
17
17
  calculateOrderQuote,
18
+ PUBLIC_OFERTA,
18
19
  } from './catalog.js';
19
20
 
20
21
  const GENERIC_WORDS = new Set([
@@ -58,16 +59,158 @@ export class LicenseServerAiAgent {
58
59
  this.fetchImpl = fetchImpl;
59
60
  }
60
61
 
61
- async chat({ message = '', user = null } = {}) {
62
+ async chat({ message = '', user = null, isAdmin = false } = {}) {
62
63
  const text = String(message || '').trim();
63
64
  if (!text) {
64
65
  return {
65
- reply: 'Здравствуйте! Я ИИ-агент лицензионного центра ODANT-WORK. Спросите о комплексе, ИИ-агентах платформы, расчёте лицензий со скидкой или вакантных франшизах в вашем регионе.',
66
+ reply: isAdmin
67
+ ? 'Здравствуйте! Я ИИ-агент консоли администратора ODANT-WORK. Спросите сводку по реестру, выпустите сертификат Ed25519, проведите оплату заказов ЮKassa или активируйте резервы франшиз.'
68
+ : 'Здравствуйте! Я ИИ-агент ODANT-WORK. Спросите о комплексе, ИИ-агентах платформы, расчёте лицензий со скидкой, условиях публичной оферты или вакантных франшизах в вашем регионе.',
66
69
  actions: [],
67
70
  };
68
71
  }
69
72
  const lower = text.toLowerCase();
70
73
 
74
+ // 0.1 Команда администратора: выпуск новой лицензии ПАК
75
+ if (/(выпуст|создай|оформ)\S*\s+(?:базов\S*\s+|демо[-\s]*)?лиценз/i.test(lower)) {
76
+ if (!isAdmin) {
77
+ return {
78
+ reply: 'Прямой выпуск сертификатов лицензий доступен только авторизованному администратору центра сертификации. Для покупки комплекса или докупки мест зарегистрируйтесь в личном кабинете и подпишите договор-оферту.',
79
+ actions: [{ type: 'open_auth' }],
80
+ };
81
+ }
82
+ const orgMatch = text.match(/(?:для\s+)?((?:ООО|АО|ПАО|ЗАО|ИП)\s*«[^»]+»)/i);
83
+ const innMatch = text.match(/ИНН\s*(\d{10,12})/i) || text.match(/\b(\d{10,12})\b/);
84
+ const emailMatch = text.match(/([a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,})/i);
85
+ const lic = this.store.issueOrUpdateLicense({
86
+ customer: {
87
+ name: orgMatch ? orgMatch[1] : 'ООО «ТехноПром-Интеграция»',
88
+ inn: innMatch ? innMatch[1] : '6234109876',
89
+ email: emailMatch ? emailMatch[1] : 'it@technoprom.ru',
90
+ },
91
+ });
92
+ return {
93
+ reply: [
94
+ `Выпущена и подписана сертификатом **Ed25519** базовая лицензия **${lic.licenseId}**:`,
95
+ `• Организация: **${lic.customer?.name || '—'}** (ИНН ${lic.customer?.inn || '—'})`,
96
+ `• Рабочие места: **${lic.totalSeats}** (${lic.baseSeats} вечных лицензий в комплекте)`,
97
+ `• Поддержка: **1 год ${lic.support?.tierName || 'Enterprise'}** (${lic.support?.reactionLabel || 'до 15 минут'}, далее — по желанию)`,
98
+ `• Код привязки бота MAX/TG/VK: \`${lic.support?.bindCode || lic.licenseId}\``,
99
+ ].join('\n'),
100
+ actions: [{ type: 'refresh_admin' }, { type: 'admin_tab', tab: 'licenses' }],
101
+ data: { license: lic },
102
+ };
103
+ }
104
+
105
+ // 0.2 Команда администратора: проведение оплаты ожидающих заказов ЮKassa
106
+ if (/(провед|подтверд)\S*\s+оплат/i.test(lower)) {
107
+ if (!isAdmin) {
108
+ return {
109
+ reply: 'Подтверждение платежей вручную доступно только администратору центра биллинга.',
110
+ actions: [],
111
+ };
112
+ }
113
+ const pending = Object.values(this.store.state.orders || {}).filter(o => o.status === 'pending');
114
+ if (!pending.length) {
115
+ return {
116
+ reply: 'Все выставленные заказы ЮKassa уже оплачены — ожидающих платежей сейчас нет.',
117
+ actions: [{ type: 'refresh_admin' }],
118
+ };
119
+ }
120
+ const completed = [];
121
+ for (const o of pending) {
122
+ const done = this.store.completePaidOrder(o.orderId, { paymentId: o.paymentId || ('yk_' + o.orderId) });
123
+ if (done?.order) completed.push(done.order);
124
+ }
125
+ return {
126
+ reply: [
127
+ `Проведена оплата по **${completed.length}** заказам ЮKassa:`,
128
+ ...completed.map(ord => `• **${ord.orderId}** — ${ord.customer?.name || 'Клиент'} · **${fmt(ord.amountRub)} ₽** (статус: оплачен)`),
129
+ 'Сертификаты лицензий и резервы франшиз автоматически обновлены.',
130
+ ].join('\n'),
131
+ actions: [{ type: 'refresh_admin' }, { type: 'admin_tab', tab: 'orders' }],
132
+ };
133
+ }
134
+
135
+ // 0.25 Команда администратора: активация резервов франшиз в действующих партнёров
136
+ if (isAdmin && /(активир|конвертир|перевед)\S*\s+(?:резерв|франш|партн)/i.test(lower)) {
137
+ const pendingRes = Object.values(this.store.state.reservations || {}).filter(
138
+ r => r.status !== 'canceled' && !r.convertedToPartnerId
139
+ );
140
+ if (!pendingRes.length) {
141
+ return {
142
+ reply: 'Сейчас нет неактивированных резервов франшиз.',
143
+ actions: [{ type: 'refresh_admin' }],
144
+ };
145
+ }
146
+ const activated = [];
147
+ for (const r of pendingRes) {
148
+ const p = this.store.registerPartner({ reservationId: r.reservationId });
149
+ activated.push(p);
150
+ }
151
+ return {
152
+ reply: [
153
+ `Активировано партнёров-франчайзи из резервов: **${activated.length}**:`,
154
+ ...activated.map(p => `• **${p.partnerId}** — ${p.companyName} (${p.regionName}, скидка ${p.discountPercent}%)`),
155
+ ].join('\n'),
156
+ actions: [{ type: 'refresh_admin' }, { type: 'admin_tab', tab: 'partners' }],
157
+ };
158
+ }
159
+
160
+ // 0.3 Команда администратора: сводка по реестру сервера лицензий / заказам / тикетам
161
+ if (/(сводк|реестр|статус сервер|обзор сервер|неоплачен\S*\s+заказ|покажи заказ|открыт\S*\s+тикет|покажи тикет|резерв\S*\s+франш)/i.test(lower)) {
162
+ if (!isAdmin) {
163
+ return {
164
+ reply: 'Внутренняя сводка по реестру сервера доступна только в консоли администратора. В личном кабинете на сайте вы можете посмотреть статус своей лицензии и подписанной оферты.',
165
+ actions: user ? [{ type: 'switch_stage', stage: 'addons' }] : [{ type: 'open_auth' }],
166
+ };
167
+ }
168
+ const licenses = this.store.listLicenses();
169
+ const orders = Object.values(this.store.state.orders || {});
170
+ const partners = this.store.listPartners();
171
+ const reservations = Object.values(this.store.state.reservations || {}).filter(
172
+ r => r.status !== 'canceled' && !r.convertedToPartnerId
173
+ );
174
+ const tickets = this.store.listTickets();
175
+ const users = this.store.listUsers();
176
+ const signedUsers = users.filter(u => u.ofertaAccepted);
177
+ const totalSeats = licenses.reduce((s, l) => s + (l.totalSeats || 0), 0);
178
+ const paidOrders = orders.filter(o => o.status === 'paid');
179
+ const pendingOrders = orders.filter(o => o.status === 'pending');
180
+ const paidSum = paidOrders.reduce((s, o) => s + (Number(o.amountRub) || 0), 0);
181
+ const pendingSum = pendingOrders.reduce((s, o) => s + (Number(o.amountRub) || 0), 0);
182
+ const openTickets = tickets.filter(t => t.status !== 'answered' && t.status !== 'closed');
183
+
184
+ return {
185
+ reply: [
186
+ '**Сводка центра лицензирования и биллинга ODANT-WORK:**',
187
+ `• Выпущено лицензий ПАК: **${licenses.length}** (суммарно **${totalSeats}** активных мест, по ${PAK_PRODUCT.baseSeats} вечных в базе)`,
188
+ `• Оплачено заказов ЮKassa: **${paidOrders.length}** на сумму **${fmt(paidSum)} ₽**`,
189
+ `• Ожидают оплаты: **${pendingOrders.length}** на сумму **${fmt(pendingSum)} ₽**`,
190
+ `• Франшизы: **${partners.length}** действующих партнёров и **${reservations.length}** активных резервов`,
191
+ `• Клиенты и оферты: **${users.length}** зарегистрировано (оферта ПЭП подписана у **${signedUsers.length}**)`,
192
+ `• Техподдержка (MAX · TG · VK): **${openTickets.length}** открытых из **${tickets.length}** обращений`,
193
+ pendingOrders.length > 0 ? '\nНапишите «Проведи оплату заказов», чтобы подтвердить ожидающие платежи.' : '',
194
+ ].filter(Boolean).join('\n'),
195
+ actions: [{ type: 'refresh_admin' }],
196
+ };
197
+ }
198
+
199
+ // 0.4 Вопросы по публичной оферте и порядку регистрации
200
+ if (/(оферт|договор|пэп|подпис\S*\s+оферт|регистрац|акцепт)/i.test(lower)) {
201
+ return {
202
+ reply: [
203
+ `**${PUBLIC_OFERTA.title}** (редакция \`${PUBLIC_OFERTA.version}\` от ${PUBLIC_OFERTA.updatedAt}):`,
204
+ '• Любые действия по заказу комплекса, докупке годовых лицензий, оплате через ЮKassa и резервированию региональной франшизы выполняются **только после регистрации в личном кабинете и подписания оферты простой электронной подписью (ПЭП)**.',
205
+ '• При подписании фиксируются наименование организации, ИНН, корпоративный email, ФИО представителя и уникальный идентификатор подписи `PEP-...`.',
206
+ user?.ofertaAccepted
207
+ ? `• У вашей организации (**${user.companyName}**) оферта уже подписана: \`${user.ofertaSignatureId}\`.`
208
+ : '• Нажмите «Войти / Регистрация», чтобы ознакомиться с текстом оферты и подписать её.',
209
+ ].join('\n'),
210
+ actions: [{ type: 'open_oferta' }],
211
+ };
212
+ }
213
+
71
214
  // 1. Франшиза в конкретном субъекте РФ
72
215
  const region = findRegionInText(text);
73
216
  if (region && /(франш|паушал|взнос|регион|вакант|квот|резерв|залог|партн|сколько|област|край|республ)/i.test(lower)) {
@@ -146,7 +289,7 @@ export class LicenseServerAiAgent {
146
289
  }
147
290
 
148
291
  // 5. Комплекс и аппаратная платформа
149
- if (/(ryzen|процессор|желез|характерист|radeon|8065|памят|192|модел|комплекс|продукт|сервер|(^|[^а-яё])ии([^а-яё]|$))/i.test(lower)) {
292
+ if (/(ryzen|процессор|желез|характерист|radeon|8065|памят|192|модел|комплекс|продукт|сервер|api|источник|(^|[^а-яё])ии([^а-яё]|$))/i.test(lower)) {
150
293
  const H = PAK_PRODUCT.hardware;
151
294
  return {
152
295
  reply: [
@@ -156,8 +299,9 @@ export class LicenseServerAiAgent {
156
299
  `• ИИ-ускорение: ${H.aiEngine}`,
157
300
  `• Память и диск: ${H.ram} · ${H.storage}`,
158
301
  `• Защита: ${H.security}`,
159
- '• Внутри: система учёта, документооборот, управление процессами, ИИ-консультант, мультиагентная система и 4 типа локальных моделей (языковая, голосовая, генерация изображений, векторный поиск).',
160
- `• В комплекте: вечная лицензия на ${PAK_PRODUCT.baseSeats} одновременных сессий + ${PAK_PRODUCT.includedSupportYears} год поддержки.`,
302
+ '• Внутри: система учёта, документооборот, управление процессами, ИИ-консультант, мультиагентная система и 4 типа интегрированных локальных моделей (языковая, голосовая, генерация изображений, векторный поиск).',
303
+ '• **Гибридный контур моделей:** интегрированные локальные модели предназначены для базовых ежедневных операций; если требуются более мощные модели ИИ, их можно подключить через API из любого источника.',
304
+ `• **В комплекте:** вечная лицензия на **${PAK_PRODUCT.baseSeats} одновременных сессий** + **${PAK_PRODUCT.includedSupportYears} год поддержки уровня Enterprise** (24/7, реакция ≤ 15 мин). Дальнейшее продление поддержки и докупка мест — **по желанию**.`,
161
305
  ].join('\n'),
162
306
  actions: [{ type: 'switch_stage', stage: 'product' }],
163
307
  };
@@ -174,7 +318,7 @@ export class LicenseServerAiAgent {
174
318
  `• Резерв субъекта: невозвратный залог **${fmt(FRANCHISE_PROGRAM.reservationDepositRub)} ₽** через ЮKassa, полная оплата паушального взноса — **в течение 1 года**`,
175
319
  `• Сейчас свободно **${totalVacant}** франшиз в **${all.length}** субъектах РФ (квота пропорциональна населению)`,
176
320
  `• Скидка партнёра: от ${FRANCHISE_PROGRAM.minDiscountPercent}% до ${FRANCHISE_PROGRAM.maxDiscountPercent}% от годового объёма продаж`,
177
- '• 100% дохода партнёра: оборудование и гарантия на него, NAS для резервных копий, серверы инференса, собственный API к моделям, поддержка, дообучение агентов и навыков.',
321
+ '• 100% дохода партнёра: оборудование и гарантия на него, NAS для резервных копий, серверы инференса, собственный API к мощным моделям, поддержка, дообучение агентов и навыков.',
178
322
  ].join('\n'),
179
323
  actions: [{ type: 'switch_stage', stage: 'franchise' }],
180
324
  };
@@ -184,10 +328,10 @@ export class LicenseServerAiAgent {
184
328
  return {
185
329
  reply: [
186
330
  'Я ИИ-агент лицензионного центра **ODANT-WORK**. Коротко об условиях:',
187
- `1. **Комплекс** — ${fmt(PAK_PRODUCT.basePriceRub)} ₽: AMD Ryzen™ AI Max+ PRO 495, Radeon™ 8065S, 192 ГБ, 4 ТБ, ${PAK_PRODUCT.baseSeats} вечных мест и год поддержки.`,
331
+ `1. **Комплекс** — ${fmt(PAK_PRODUCT.basePriceRub)} ₽: AMD Ryzen™ AI Max+ PRO 495, Radeon™ 8065S, 192 ГБ, 4 ТБ, **${PAK_PRODUCT.baseSeats} вечных лицензий** и **1 год поддержки Enterprise** в комплекте (дальше — по желанию). Интегрированные модели — для базовых операций, более мощные ИИ подключаются через API из любого источника.`,
188
332
  '2. **Доп. места на год:** Start 10 000 ₽ · Business 15 000 ₽ · Enterprise 20 000 ₽, скидка за количество до −25%.',
189
333
  `3. **Франшиза:** паушальный взнос ${fmt(FRANCHISE_PROGRAM.baseFranchiseFeeRub)} ₽ × индекс субъекта, залог за резерв ${fmt(FRANCHISE_PROGRAM.reservationDepositRub)} ₽, полная оплата в течение 1 года.`,
190
- 'Спросите, например: «Рассчитай 25 мест Business», «Франшиза в Рязанской области» или «Что умеют ИИ-агенты?»',
334
+ 'Спросите, например: «Рассчитай 25 мест Business», «Франшиза в Рязанской области», «Сводка по серверу» или «Выпусти лицензию».',
191
335
  ].join('\n'),
192
336
  actions: [],
193
337
  };
package/src/bots.js CHANGED
@@ -56,7 +56,7 @@ export class SupportBotsHub {
56
56
  '',
57
57
  'Доступные команды:',
58
58
  '• /bind <КОД> — привязать ПАК WORK к этому чату',
59
- '• /status — проверить рабочие места (5 вечных + годовые), SLA и остаток обращений',
59
+ '• /status — проверить рабочие места (10 вечных + годовые), SLA и остаток обращений',
60
60
  '• /buy <start|business|enterprise> <кол-во_мест> — докупить годовые лицензии через ЮKassa',
61
61
  '• Любое другое сообщение — создать обращение в инженерную поддержку по вашему SLA.',
62
62
  ].join('\n');