work-license-server 0.0.0-stage → 1.0.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/README.md +17 -2
- package/bin/server.mjs +14 -0
- package/package.json +29 -4
- package/src/bots.js +311 -0
- package/src/catalog.js +451 -0
- package/src/crypto.js +120 -0
- package/src/index.js +17 -0
- package/src/landing.js +981 -0
- package/src/server.js +290 -0
- package/src/store.js +758 -0
- package/src/yookassa.js +184 -0
package/src/catalog.js
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Каталог комплектации ПАК WORK, тарифов годовых пользовательских лицензий и SLA поддержки
|
|
3
|
+
* для лицензионного сервера и лендинга https://work.odant.org.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const PAK_PRODUCT = Object.freeze({
|
|
7
|
+
sku: 'WORK-PAK-495-192-4T',
|
|
8
|
+
name: 'Программно-аппаратный комплекс WORK',
|
|
9
|
+
domain: 'work.odant.org',
|
|
10
|
+
basePriceRub: 5_000_000,
|
|
11
|
+
baseSeats: 5,
|
|
12
|
+
perpetualBase: true,
|
|
13
|
+
includedSupportYears: 1,
|
|
14
|
+
includedSupportTier: 'business',
|
|
15
|
+
hardware: Object.freeze({
|
|
16
|
+
cpu: 'AMD Ryzen 495 8085',
|
|
17
|
+
ram: '192 ГБ Unified RAM',
|
|
18
|
+
ramGb: 192,
|
|
19
|
+
storage: 'SSD 4 ТБ NVMe PCIe Gen4',
|
|
20
|
+
ssdTb: 4,
|
|
21
|
+
}),
|
|
22
|
+
software: Object.freeze({
|
|
23
|
+
accounting: 'Предустановленная базовая типовая конфигурация (аналог 1С: Предприятие — справочники, план счетов, двойная запись, оборотно-сальдовая ведомость, анализ счёта, первичные документы, импорт баз 1С)',
|
|
24
|
+
seatModel: 'По одновременным активным сессиям (5 вечных рабочих мест в базовом комплекте + докупка срочных годовых лицензий)',
|
|
25
|
+
hardwareBinding: 'Привязка вечной лицензии к Hardware ID материнской платы/CPU AMD Ryzen + криптографическому ключу узла (#system/node-key.json)',
|
|
26
|
+
}),
|
|
27
|
+
preinstalledModels: Object.freeze([
|
|
28
|
+
{
|
|
29
|
+
id: 'Qwen3-30B-A3B',
|
|
30
|
+
title: 'Qwen 3 30B',
|
|
31
|
+
badge: 'LLM · Учёт · Код · Агент',
|
|
32
|
+
description: 'Основная языковая модель для живого диалога, проведения хозяйственных операций, анализа баланса, формирования отчётов и разработки.',
|
|
33
|
+
ramGb: 18.5,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'Qwen3-Voice',
|
|
37
|
+
title: 'Голосовая модель (ASR + TTS)',
|
|
38
|
+
badge: 'Полнодуплексный голос',
|
|
39
|
+
description: 'Локальное распознавание и синтез естественной речи с мгновенным откликом и взаимным перебиванием собеседников.',
|
|
40
|
+
ramGb: 4.2,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'Flux-Image',
|
|
44
|
+
title: 'Генерация изображений',
|
|
45
|
+
badge: 'Дизайн · Карточки · Схемы',
|
|
46
|
+
description: 'Локальный синтез иллюстраций, визуализации товаров, презентационных материалов и графики внутри контура компании.',
|
|
47
|
+
ramGb: 12.0,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'Qwen3-Embedding',
|
|
51
|
+
title: 'Embedding + RAG',
|
|
52
|
+
badge: 'Семантический поиск',
|
|
53
|
+
description: 'Векторная индексация всех договоров, первичных документов, переписки и регламентов предприятия.',
|
|
54
|
+
ramGb: 2.5,
|
|
55
|
+
},
|
|
56
|
+
]),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/** Тарифы годовой лицензии на 1 дополнительного пользователя (зависят от уровня поддержки SLA). */
|
|
60
|
+
export const SLA_TIERS = Object.freeze({
|
|
61
|
+
start: Object.freeze({
|
|
62
|
+
id: 'start',
|
|
63
|
+
name: 'Start',
|
|
64
|
+
badge: 'Базовый SLA',
|
|
65
|
+
pricePerUserYearRub: 15_000,
|
|
66
|
+
ticketsLimitPerSeatYear: 12,
|
|
67
|
+
reactionMinutes: 480,
|
|
68
|
+
reactionLabel: 'до 8 рабочих часов',
|
|
69
|
+
scheduleLabel: 'Пн–Пт, 09:00–18:00 МСК',
|
|
70
|
+
channels: ['MAX', 'Telegram', 'VK'],
|
|
71
|
+
features: [
|
|
72
|
+
'1 дополнительное конкурентное рабочее место на 1 год',
|
|
73
|
+
'До 12 обращений в техподдержку в год на каждое место',
|
|
74
|
+
'Время реакции инженера — до 8 рабочих часов',
|
|
75
|
+
'Поддержка через боты MAX, Telegram, VK',
|
|
76
|
+
'Доступ ко всем обновлениям ядра WORK и типовой конфигурации',
|
|
77
|
+
],
|
|
78
|
+
}),
|
|
79
|
+
business: Object.freeze({
|
|
80
|
+
id: 'business',
|
|
81
|
+
name: 'Business',
|
|
82
|
+
badge: 'Рекомендуемый',
|
|
83
|
+
pricePerUserYearRub: 40_000,
|
|
84
|
+
ticketsLimitPerSeatYear: 48,
|
|
85
|
+
reactionMinutes: 120,
|
|
86
|
+
reactionLabel: 'до 2 часов',
|
|
87
|
+
scheduleLabel: 'Ежедневно, 08:00–22:00 МСК',
|
|
88
|
+
channels: ['MAX', 'Telegram', 'VK'],
|
|
89
|
+
features: [
|
|
90
|
+
'1 дополнительное конкурентное рабочее место на 1 год',
|
|
91
|
+
'До 48 обращений в техподдержку в год на каждое место',
|
|
92
|
+
'Приоритетное время реакции — до 2 часов',
|
|
93
|
+
'Консультации по настройке учётных схем и отчётов',
|
|
94
|
+
'Помощь в администрировании предустановленных локальных моделей ИИ',
|
|
95
|
+
],
|
|
96
|
+
}),
|
|
97
|
+
enterprise: Object.freeze({
|
|
98
|
+
id: 'enterprise',
|
|
99
|
+
name: 'Enterprise',
|
|
100
|
+
badge: 'Максимальный 24/7',
|
|
101
|
+
pricePerUserYearRub: 100_000,
|
|
102
|
+
ticketsLimitPerSeatYear: 9999,
|
|
103
|
+
reactionMinutes: 15,
|
|
104
|
+
reactionLabel: 'до 15 минут (24/7)',
|
|
105
|
+
scheduleLabel: 'Круглосуточно 24/7/365',
|
|
106
|
+
channels: ['MAX', 'Telegram', 'VK', 'Выделенный инженер'],
|
|
107
|
+
features: [
|
|
108
|
+
'1 дополнительное конкурентное рабочее место на 1 год',
|
|
109
|
+
'Безлимитное количество обращений в поддержку',
|
|
110
|
+
'Критическое время реакции — до 15 минут 24/7',
|
|
111
|
+
'Выделенный инженер и прямая линия сопровождения',
|
|
112
|
+
'Помощь с дообучением моделей ИИ (LoRA) под специфику предприятия',
|
|
113
|
+
],
|
|
114
|
+
}),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Рассчитать стоимость заказа (покупка базового ПАК и/или докупка годовых пользовательских мест).
|
|
119
|
+
*/
|
|
120
|
+
export function calculateOrderQuote({
|
|
121
|
+
includeBasePak = false,
|
|
122
|
+
tier = 'business',
|
|
123
|
+
addonSeats = 0,
|
|
124
|
+
years = 1,
|
|
125
|
+
} = {}) {
|
|
126
|
+
const tierKey = String(tier || 'business').toLowerCase();
|
|
127
|
+
const tierSpec = SLA_TIERS[tierKey] || SLA_TIERS.business;
|
|
128
|
+
const seats = Math.max(0, Math.floor(Number(addonSeats || 0)));
|
|
129
|
+
const periodYears = Math.max(1, Math.min(5, Math.floor(Number(years || 1))));
|
|
130
|
+
|
|
131
|
+
const baseAmountRub = includeBasePak ? PAK_PRODUCT.basePriceRub : 0;
|
|
132
|
+
const addonsAmountRub = seats * tierSpec.pricePerUserYearRub * periodYears;
|
|
133
|
+
const totalAmountRub = baseAmountRub + addonsAmountRub;
|
|
134
|
+
|
|
135
|
+
const items = [];
|
|
136
|
+
if (includeBasePak) {
|
|
137
|
+
items.push({
|
|
138
|
+
title: `${PAK_PRODUCT.name} (${PAK_PRODUCT.hardware.cpu}, ${PAK_PRODUCT.hardware.ramGb} ГБ RAM, SSD ${PAK_PRODUCT.hardware.ssdTb} ТБ, вечная лицензия на 5 мест + 1 год поддержки)`,
|
|
139
|
+
quantity: 1,
|
|
140
|
+
unitPriceRub: PAK_PRODUCT.basePriceRub,
|
|
141
|
+
totalRub: PAK_PRODUCT.basePriceRub,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
if (seats > 0) {
|
|
145
|
+
items.push({
|
|
146
|
+
title: `Годовая лицензия WORK на доп. рабочее место (уровень поддержки ${tierSpec.name}, ${periodYears} г.)`,
|
|
147
|
+
quantity: seats,
|
|
148
|
+
unitPriceRub: tierSpec.pricePerUserYearRub * periodYears,
|
|
149
|
+
totalRub: addonsAmountRub,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
includeBasePak: !!includeBasePak,
|
|
155
|
+
basePakPriceRub: baseAmountRub,
|
|
156
|
+
baseSeats: includeBasePak ? PAK_PRODUCT.baseSeats : 0,
|
|
157
|
+
includedSupportYears: includeBasePak ? PAK_PRODUCT.includedSupportYears : 0,
|
|
158
|
+
tier: tierSpec.id,
|
|
159
|
+
tierName: tierSpec.name,
|
|
160
|
+
pricePerUserYearRub: tierSpec.pricePerUserYearRub,
|
|
161
|
+
addonSeats: seats,
|
|
162
|
+
years: periodYears,
|
|
163
|
+
addonsAmountRub,
|
|
164
|
+
totalAmountRub,
|
|
165
|
+
reactionMinutes: tierSpec.reactionMinutes,
|
|
166
|
+
reactionLabel: tierSpec.reactionLabel,
|
|
167
|
+
ticketsAdded: seats * tierSpec.ticketsLimitPerSeatYear * periodYears,
|
|
168
|
+
items,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Параметры партнёрской программы и региональной франшизы WORK:
|
|
174
|
+
* - Фиксированная стоимость франшизы пропорциональна экономическому индексу региона (baseFranchiseRub * economicIndex).
|
|
175
|
+
* - Максимальное количество партнёров в регионе пропорционально населению региона (partnersPerMillion * populationMillions, мин. 1).
|
|
176
|
+
* - Партнёрская скидка растёт пропорционально годовому объёму продаж от начальных 10% до максимальных 50%.
|
|
177
|
+
*/
|
|
178
|
+
export const FRANCHISE_PROGRAM = Object.freeze({
|
|
179
|
+
baseFranchiseFeeRub: 1_000_000, // Базовый эталон стоимости франшизы (при экономическом индексе региона = 1.0)
|
|
180
|
+
partnersPerMillionPopulation: 1, // Норматив квоты партнёров: 1 партнёр на каждый 1 млн населения региона (пропорционально населению)
|
|
181
|
+
minDiscountPercent: 10, // Начальная партнёрская скидка (10%)
|
|
182
|
+
maxDiscountPercent: 50, // Максимальная партнёрская скидка (50%)
|
|
183
|
+
maxDiscountAnnualSalesRub: 100_000_000, // Годовой объём продаж (100 млн ₽ = ~20 ПАК), при котором достигается максимальная скидка 50%
|
|
184
|
+
hardwareWarrantyYears: 1, // Обязательная годовая гарантия на оборудование ПАК, предоставляемая партнёром
|
|
185
|
+
partnerOperationsModel: Object.freeze([
|
|
186
|
+
'Партнёр самостоятельно закупает аппаратную платформу (AMD Ryzen 495 8085 · 192 ГБ RAM · SSD 4 ТБ) у дистрибьюторов',
|
|
187
|
+
'Партнёр предустанавливает WORK, типовую учётную конфигурацию и локальные модели ИИ (Qwen 3 30B, Голос, Изображения, Embedding)',
|
|
188
|
+
'Партнёр самостоятельно привязывает оборудование (HWID + #system/node-key.json) и выдаёт лицензии клиентам через партнёрский шлюз',
|
|
189
|
+
'Партнёр предоставляет клиенту годовую гарантию (1 год) на поставленное оборудование ПАК',
|
|
190
|
+
]),
|
|
191
|
+
partnerRevenueStreams: Object.freeze([
|
|
192
|
+
{
|
|
193
|
+
id: 'pak-license-margin',
|
|
194
|
+
title: 'Продажа ПАК WORK и лицензий (скидка 10% → 50%)',
|
|
195
|
+
share: 'Скидка партнёра от 10% до 50% пропорционально годовому объёму продаж + аппаратная наценка при сборке ПАК',
|
|
196
|
+
recommendedAvgRub: 1_500_000,
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: 'nas-backup-hardware',
|
|
200
|
+
title: 'Поставка NAS-систем для резервного копирования',
|
|
201
|
+
share: '100% доход партнёра',
|
|
202
|
+
description: 'Поставка и настройка сетевых хранилищ NAS для автоматической архивации и снапшотов баз организаций WORK',
|
|
203
|
+
recommendedAvgRub: 350_000,
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: 'inference-hardware',
|
|
207
|
+
title: 'Оборудование для инференса и ИИ-кластеров',
|
|
208
|
+
share: '100% доход партнёра',
|
|
209
|
+
description: 'Поставка дополнительных GPU-серверов, нейроускорителей и вычислительных узлов для масштабирования инференса',
|
|
210
|
+
recommendedAvgRub: 1_200_000,
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
id: 'partner-model-api',
|
|
214
|
+
title: 'Собственный платный API партнёра к моделям ИИ',
|
|
215
|
+
share: '100% регулярный (ARR) доход партнёра',
|
|
216
|
+
description: 'Подключение клиентов к собственному модельному кластеру партнёра по платной подписке (провайдер в /MODELS)',
|
|
217
|
+
recommendedAvgRub: 360_000,
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: 'partner-tech-support',
|
|
221
|
+
title: 'Техническая поддержка и сервисное сопровождение',
|
|
222
|
+
share: 'Доход партнёра по договорам SLA и выездному обслуживанию',
|
|
223
|
+
description: 'Первая и вторая линия техподдержки предприятий региона, администрирование серверов и регламентное ТО',
|
|
224
|
+
recommendedAvgRub: 240_000,
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: 'agent-training-skills',
|
|
228
|
+
title: 'Дообучение ИИ-агентов (LoRA) и разработка навыков (skills)',
|
|
229
|
+
share: '100% проектный доход партнёра',
|
|
230
|
+
description: 'Сборка датасетов предприятия, дообучение моделей под отраслевую специфику, создание навыков (skills) и учётных форм',
|
|
231
|
+
recommendedAvgRub: 600_000,
|
|
232
|
+
},
|
|
233
|
+
]),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Справочник регионов РФ с экономическими характеристиками и численностью населения:
|
|
238
|
+
* - Стоимость региональной франшизы пропорциональна экономическим характеристикам региона (economicIndex).
|
|
239
|
+
* - Квота (максимальное число партнёров в регионе) пропорциональна населению региона (populationMillions).
|
|
240
|
+
*/
|
|
241
|
+
export const REGIONS_CATALOG = Object.freeze([
|
|
242
|
+
{ code: 'RU-MOW', name: 'Москва', district: 'ЦФО', populationMillions: 13.1, grpTrillionRub: 28.5, activeEnterprises: 620_000, economicIndex: 3.5 },
|
|
243
|
+
{ code: 'RU-MOS', name: 'Московская область', district: 'ЦФО', populationMillions: 8.6, grpTrillionRub: 8.4, activeEnterprises: 185_000, economicIndex: 2.0 },
|
|
244
|
+
{ code: 'RU-SPE', name: 'Санкт-Петербург', district: 'СЗФО', populationMillions: 5.6, grpTrillionRub: 11.2, activeEnterprises: 245_000, economicIndex: 2.2 },
|
|
245
|
+
{ code: 'RU-KDA', name: 'Краснодарский край', district: 'ЮФО', populationMillions: 5.8, grpTrillionRub: 4.3, activeEnterprises: 120_000, economicIndex: 1.4 },
|
|
246
|
+
{ code: 'RU-SVE', name: 'Свердловская область', district: 'УФО', populationMillions: 4.2, grpTrillionRub: 4.2, activeEnterprises: 112_000, economicIndex: 1.4 },
|
|
247
|
+
{ code: 'RU-ROS', name: 'Ростовская область', district: 'ЮФО', populationMillions: 4.2, grpTrillionRub: 2.6, activeEnterprises: 76_000, economicIndex: 1.0 },
|
|
248
|
+
{ code: 'RU-BA', name: 'Республика Башкортостан', district: 'ПФО', populationMillions: 4.1, grpTrillionRub: 2.4, activeEnterprises: 65_000, economicIndex: 1.0 },
|
|
249
|
+
{ code: 'RU-TA', name: 'Республика Татарстан', district: 'ПФО', populationMillions: 4.0, grpTrillionRub: 4.6, activeEnterprises: 98_000, economicIndex: 1.5 },
|
|
250
|
+
{ code: 'RU-CHE', name: 'Челябинская область', district: 'УФО', populationMillions: 3.4, grpTrillionRub: 2.5, activeEnterprises: 68_000, economicIndex: 1.0 },
|
|
251
|
+
{ code: 'RU-SAM', name: 'Самарская область', district: 'ПФО', populationMillions: 3.1, grpTrillionRub: 2.6, activeEnterprises: 74_000, economicIndex: 1.1 },
|
|
252
|
+
{ code: 'RU-NIZ', name: 'Нижегородская область', district: 'ПФО', populationMillions: 3.1, grpTrillionRub: 2.5, activeEnterprises: 72_000, economicIndex: 1.1 },
|
|
253
|
+
{ code: 'RU-KYA', name: 'Красноярский край', district: 'СФО', populationMillions: 2.9, grpTrillionRub: 3.5, activeEnterprises: 58_000, economicIndex: 1.2 },
|
|
254
|
+
{ code: 'RU-NVS', name: 'Новосибирская область', district: 'СФО', populationMillions: 2.8, grpTrillionRub: 2.3, activeEnterprises: 82_000, economicIndex: 1.1 },
|
|
255
|
+
{ code: 'RU-VOR', name: 'Воронежская область', district: 'ЦФО', populationMillions: 2.3, grpTrillionRub: 1.5, activeEnterprises: 44_000, economicIndex: 0.8 },
|
|
256
|
+
{ code: 'RU-PRI', name: 'Приморский край', district: 'ДФО', populationMillions: 1.8, grpTrillionRub: 1.7, activeEnterprises: 48_000, economicIndex: 0.9 },
|
|
257
|
+
{ code: 'RU-REG', name: 'Стандартный регион РФ (до 1 млн чел.)', district: 'РФ', populationMillions: 1.0, grpTrillionRub: 0.9, activeEnterprises: 25_000, economicIndex: 0.5 },
|
|
258
|
+
]);
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Программа обучения и сертификации специалистов партнёров WORK.
|
|
262
|
+
*/
|
|
263
|
+
export const PARTNER_TRAINING_TRACKS = Object.freeze([
|
|
264
|
+
{
|
|
265
|
+
id: 'pak-ai-engineer',
|
|
266
|
+
title: 'Сертифицированный инженер ПАК и локального ИИ WORK',
|
|
267
|
+
code: 'WORK-CERT-ENG',
|
|
268
|
+
hours: 40,
|
|
269
|
+
passScore: 80,
|
|
270
|
+
requiredForFranchise: true,
|
|
271
|
+
topics: [
|
|
272
|
+
'Архитектура ПАК AMD Ryzen 495 8085 (192 ГБ Unified RAM, SSD 4 ТБ) и профилирование памяти',
|
|
273
|
+
'Администрирование локальных моделей: Qwen 3 30B, Qwen3-Voice (ASR/TTS), Flux-Image, Qwen3-Embedding',
|
|
274
|
+
'Сборка корпоративных датасетов ChatML и дообучение адаптеров LoRA / SFT / GRPO в контуре предприятия',
|
|
275
|
+
'Аппаратная привязка лицензий (HWID + #system/node-key.json) и закрытый контур безопасности',
|
|
276
|
+
],
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
id: 'accounting-architect',
|
|
280
|
+
title: 'Сертифицированный архитектор учётных систем WORK (1С-класс)',
|
|
281
|
+
code: 'WORK-CERT-ACC',
|
|
282
|
+
hours: 48,
|
|
283
|
+
passScore: 80,
|
|
284
|
+
requiredForFranchise: true,
|
|
285
|
+
topics: [
|
|
286
|
+
'Типовая учётная конфигурация (/MODULES/Register): справочники $catalog, виртуальные справочники $virtual, план счетов $account',
|
|
287
|
+
'Явная двойная запись, проведение операций $operation, сторно и двухэтапные субаналитики',
|
|
288
|
+
'Отчётность ODANT-стандарта: Оборотно-сальдовая ведомость, Анализ счёта, Обороты счёта, Журнал проводок',
|
|
289
|
+
'Миграция исторических баз и остатков из 1С:Предприятие в изолированные БД организаций WORK',
|
|
290
|
+
],
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
id: 'platform-developer',
|
|
294
|
+
title: 'Сертифицированный разработчик модулей и ИИ-агентов WORK',
|
|
295
|
+
code: 'WORK-CERT-DEV',
|
|
296
|
+
hours: 36,
|
|
297
|
+
passScore: 75,
|
|
298
|
+
requiredForFranchise: false,
|
|
299
|
+
topics: [
|
|
300
|
+
'Трёхосевое наследование метаданных WORK (Types → Ancestor → Parent → Class)',
|
|
301
|
+
'Разработка представлений work-form на компонентах ODA и серверных методов шлюза',
|
|
302
|
+
'Создание прикладных навыков (skills), инструментов и триггеров для ИИ-консультанта',
|
|
303
|
+
'Федерация серверов NODES и подключение периферии через LAN / MCP',
|
|
304
|
+
],
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
id: 'sales-sla-manager',
|
|
308
|
+
title: 'Сертифицированный эксперт по продажам и сопровождению ПАК WORK',
|
|
309
|
+
code: 'WORK-CERT-SLS',
|
|
310
|
+
hours: 24,
|
|
311
|
+
passScore: 75,
|
|
312
|
+
requiredForFranchise: true,
|
|
313
|
+
topics: [
|
|
314
|
+
'Экономическое обоснование внедрения ПАК WORK (5 вечных мест + локальный ИИ против облачных подписок)',
|
|
315
|
+
'Лицензионная политика: расчёт конкурентных сессий и подбор тарифов SLA (Start / Business / Enterprise)',
|
|
316
|
+
'Регламент первой и второй линии поддержки через боты MAX, Telegram и VK',
|
|
317
|
+
],
|
|
318
|
+
},
|
|
319
|
+
]);
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Рассчитать максимальное число партнёров (квоту) для региона пропорционально численности населения.
|
|
323
|
+
*/
|
|
324
|
+
export function calculateRegionPartnerQuota(populationMillions = 1.0) {
|
|
325
|
+
const pop = Math.max(0.1, Number(populationMillions || 1.0));
|
|
326
|
+
return Math.max(1, Math.round(pop * FRANCHISE_PROGRAM.partnersPerMillionPopulation));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Рассчитать фиксированную стоимость региональной франшизы пропорционально экономическим характеристикам региона
|
|
331
|
+
* и квоту партнёров пропорционально населению региона.
|
|
332
|
+
*/
|
|
333
|
+
export function calculateFranchiseFee(regionCodeOrIndex = 'RU-SVE', customPopulationMillions = null) {
|
|
334
|
+
let region = null;
|
|
335
|
+
let economicIndex = 1.0;
|
|
336
|
+
let populationMillions = 1.0;
|
|
337
|
+
|
|
338
|
+
if (typeof regionCodeOrIndex === 'number') {
|
|
339
|
+
economicIndex = Math.max(0.2, regionCodeOrIndex);
|
|
340
|
+
populationMillions = customPopulationMillions != null ? Math.max(0.1, Number(customPopulationMillions)) : 1.0;
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
region = REGIONS_CATALOG.find(r => r.code === regionCodeOrIndex || r.name.toLowerCase() === String(regionCodeOrIndex).toLowerCase())
|
|
344
|
+
|| REGIONS_CATALOG[0];
|
|
345
|
+
economicIndex = region.economicIndex;
|
|
346
|
+
populationMillions = customPopulationMillions != null ? Math.max(0.1, Number(customPopulationMillions)) : region.populationMillions;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const franchiseFeeRub = Math.round(FRANCHISE_PROGRAM.baseFranchiseFeeRub * economicIndex);
|
|
350
|
+
const maxPartnersQuota = calculateRegionPartnerQuota(populationMillions);
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
regionCode: region?.code || 'CUSTOM',
|
|
354
|
+
regionName: region?.name || 'Выбранный регион',
|
|
355
|
+
district: region?.district || 'РФ',
|
|
356
|
+
populationMillions,
|
|
357
|
+
maxPartnersQuota,
|
|
358
|
+
grpTrillionRub: region?.grpTrillionRub ?? null,
|
|
359
|
+
activeEnterprises: region?.activeEnterprises ?? null,
|
|
360
|
+
economicIndex,
|
|
361
|
+
baseFranchiseFeeRub: FRANCHISE_PROGRAM.baseFranchiseFeeRub,
|
|
362
|
+
franchiseFeeRub,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Рассчитать партнёрскую скидку пропорционально годовому объёму продаж:
|
|
368
|
+
* - Начальная скидка (при 0 ₽ продаж) = 10%
|
|
369
|
+
* - Максимальная скидка (при >= maxDiscountAnnualSalesRub) = 50%
|
|
370
|
+
* - Между ними — прямая пропорция от годового объёма продаж.
|
|
371
|
+
*/
|
|
372
|
+
export function calculatePartnerDiscount(annualSalesRub = 0) {
|
|
373
|
+
const sales = Math.max(0, Number(annualSalesRub || 0));
|
|
374
|
+
const { minDiscountPercent, maxDiscountPercent, maxDiscountAnnualSalesRub } = FRANCHISE_PROGRAM;
|
|
375
|
+
const ratio = Math.min(1, sales / maxDiscountAnnualSalesRub);
|
|
376
|
+
const rawDiscount = minDiscountPercent + ratio * (maxDiscountPercent - minDiscountPercent);
|
|
377
|
+
const discountPercent = Math.round(rawDiscount * 10) / 10;
|
|
378
|
+
|
|
379
|
+
let statusLevel = 'Start Partner (10%+)';
|
|
380
|
+
if (discountPercent >= 50)
|
|
381
|
+
statusLevel = 'Diamond Franchise (50%)';
|
|
382
|
+
else if (discountPercent >= 40)
|
|
383
|
+
statusLevel = 'Platinum Franchise (40%+)';
|
|
384
|
+
else if (discountPercent >= 30)
|
|
385
|
+
statusLevel = 'Gold Franchise (30%+)';
|
|
386
|
+
else if (discountPercent >= 20)
|
|
387
|
+
statusLevel = 'Silver Franchise (20%+)';
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
annualSalesRub: sales,
|
|
391
|
+
discountPercent,
|
|
392
|
+
statusLevel,
|
|
393
|
+
minDiscountPercent,
|
|
394
|
+
maxDiscountPercent,
|
|
395
|
+
maxDiscountAnnualSalesRub,
|
|
396
|
+
nextTargetSalesRub: sales >= maxDiscountAnnualSalesRub ? 0 : maxDiscountAnnualSalesRub - sales,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Полный расчёт экономики партнёра-франчайзи:
|
|
402
|
+
* - фиксированный региональный взнос (по экономике региона) и квота партнёров (по населению);
|
|
403
|
+
* - скидка партнёра (10% → 50%) от годового объёма продаж;
|
|
404
|
+
* - дополнительный 100% доход партнёра: поставка NAS, серверов инференса, платный API к моделям,
|
|
405
|
+
* техподдержка, дообучение ИИ-агентов (LoRA) и разработка навыков (skills).
|
|
406
|
+
*/
|
|
407
|
+
export function calculatePartnerEconomics({
|
|
408
|
+
regionCode = 'RU-SVE',
|
|
409
|
+
annualSalesRub = 0,
|
|
410
|
+
dealRetailAmountRub = PAK_PRODUCT.basePriceRub,
|
|
411
|
+
extraServices = {},
|
|
412
|
+
} = {}) {
|
|
413
|
+
const franchise = calculateFranchiseFee(regionCode);
|
|
414
|
+
const discount = calculatePartnerDiscount(annualSalesRub);
|
|
415
|
+
const retailRub = Math.max(0, Number(dealRetailAmountRub || 0));
|
|
416
|
+
const partnerMarginRub = Math.round(retailRub * (discount.discountPercent / 100));
|
|
417
|
+
const netToVendorRub = retailRub - partnerMarginRub;
|
|
418
|
+
|
|
419
|
+
// Расчёт дополнительных источников дохода партнёра (100% выручка партнёра)
|
|
420
|
+
const nasBackupRub = Math.max(0, Number(extraServices.nasBackupRub ?? 350_000));
|
|
421
|
+
const inferenceHardwareRub = Math.max(0, Number(extraServices.inferenceHardwareRub ?? 0));
|
|
422
|
+
const modelApiAnnualRub = Math.max(0, Number(extraServices.modelApiAnnualRub ?? 360_000));
|
|
423
|
+
const techSupportRub = Math.max(0, Number(extraServices.techSupportRub ?? 240_000));
|
|
424
|
+
const agentTrainingSkillsRub = Math.max(0, Number(extraServices.agentTrainingSkillsRub ?? 600_000));
|
|
425
|
+
|
|
426
|
+
const totalExtraRevenueRub = nasBackupRub + inferenceHardwareRub + modelApiAnnualRub + techSupportRub + agentTrainingSkillsRub;
|
|
427
|
+
const totalPartnerIncomePerClientRub = partnerMarginRub + totalExtraRevenueRub;
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
franchise,
|
|
431
|
+
discount,
|
|
432
|
+
deal: {
|
|
433
|
+
retailAmountRub: retailRub,
|
|
434
|
+
discountPercent: discount.discountPercent,
|
|
435
|
+
partnerMarginRub,
|
|
436
|
+
netToVendorRub,
|
|
437
|
+
},
|
|
438
|
+
extraRevenue: {
|
|
439
|
+
nasBackupRub,
|
|
440
|
+
inferenceHardwareRub,
|
|
441
|
+
modelApiAnnualRub,
|
|
442
|
+
techSupportRub,
|
|
443
|
+
agentTrainingSkillsRub,
|
|
444
|
+
totalExtraRevenueRub,
|
|
445
|
+
},
|
|
446
|
+
totalPartnerIncomePerClientRub,
|
|
447
|
+
revenueStreamsCatalog: FRANCHISE_PROGRAM.partnerRevenueStreams,
|
|
448
|
+
operationsModel: FRANCHISE_PROGRAM.partnerOperationsModel,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
package/src/crypto.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Криптографическое ядро центра лицензирования work.odant.org (Ed25519).
|
|
3
|
+
* Подписывает и проверяет сертификаты лицензий ПАК WORK (WORK-LIC-v1),
|
|
4
|
+
* привязанные к Hardware ID (AMD Ryzen CPU/MB) и ключу узла (#system/node-key.json).
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import crypto from 'node:crypto';
|
|
9
|
+
import { PAK_PRODUCT } from './catalog.js';
|
|
10
|
+
|
|
11
|
+
export const LICENSE_PROTOCOL = 'WORK-LIC-v1';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Загрузить или сгенерировать ключевую пару Ed25519 центра лицензирования.
|
|
15
|
+
*/
|
|
16
|
+
export function loadOrCreateAuthorityKeys(keyFilePath) {
|
|
17
|
+
if (keyFilePath && fs.existsSync(keyFilePath)) {
|
|
18
|
+
try {
|
|
19
|
+
const raw = JSON.parse(fs.readFileSync(keyFilePath, 'utf8'));
|
|
20
|
+
if (raw?.publicKey && raw?.privateKey)
|
|
21
|
+
return raw;
|
|
22
|
+
}
|
|
23
|
+
catch { /* создадим заново */ }
|
|
24
|
+
}
|
|
25
|
+
const pair = crypto.generateKeyPairSync('ed25519');
|
|
26
|
+
const keys = {
|
|
27
|
+
publicKey: pair.publicKey.export({ type: 'spki', format: 'der' }).toString('base64'),
|
|
28
|
+
privateKey: pair.privateKey.export({ type: 'pkcs8', format: 'der' }).toString('base64'),
|
|
29
|
+
created: Date.now(),
|
|
30
|
+
};
|
|
31
|
+
if (keyFilePath) {
|
|
32
|
+
fs.mkdirSync(path.dirname(keyFilePath), { recursive: true });
|
|
33
|
+
fs.writeFileSync(keyFilePath, JSON.stringify(keys, null, 2), { mode: 0o600 });
|
|
34
|
+
}
|
|
35
|
+
return keys;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Каноническая строка сертификата лицензии (полностью совместима с клиентским модулем WORK).
|
|
40
|
+
*/
|
|
41
|
+
export function canonicalLicenseString(lic = {}) {
|
|
42
|
+
const clean = {
|
|
43
|
+
protocol: lic.protocol || LICENSE_PROTOCOL,
|
|
44
|
+
licenseId: String(lic.licenseId || ''),
|
|
45
|
+
issuedAt: Number(lic.issuedAt || 0),
|
|
46
|
+
issuer: String(lic.issuer || 'work.odant.org'),
|
|
47
|
+
customer: {
|
|
48
|
+
name: String(lic.customer?.name || ''),
|
|
49
|
+
inn: String(lic.customer?.inn || ''),
|
|
50
|
+
email: String(lic.customer?.email || ''),
|
|
51
|
+
},
|
|
52
|
+
hardware: {
|
|
53
|
+
hwid: String(lic.hardware?.hwid || ''),
|
|
54
|
+
nodeFingerprint: String(lic.hardware?.nodeFingerprint || ''),
|
|
55
|
+
sku: String(lic.hardware?.sku || PAK_PRODUCT.sku),
|
|
56
|
+
},
|
|
57
|
+
baseSeats: Number(lic.baseSeats ?? PAK_PRODUCT.baseSeats),
|
|
58
|
+
perpetualBase: lic.perpetualBase !== false,
|
|
59
|
+
addonSubscriptions: Array.isArray(lic.addonSubscriptions)
|
|
60
|
+
? lic.addonSubscriptions.map(sub => ({
|
|
61
|
+
id: String(sub.id || ''),
|
|
62
|
+
tier: String(sub.tier || 'start').toLowerCase(),
|
|
63
|
+
seats: Number(sub.seats || 0),
|
|
64
|
+
issuedAt: Number(sub.issuedAt || 0),
|
|
65
|
+
expiresAt: Number(sub.expiresAt || 0),
|
|
66
|
+
orderId: String(sub.orderId || ''),
|
|
67
|
+
status: String(sub.status || 'active'),
|
|
68
|
+
}))
|
|
69
|
+
: [],
|
|
70
|
+
};
|
|
71
|
+
return JSON.stringify(clean);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Подписать сертификат лицензии закрытым ключом центра сертификации.
|
|
76
|
+
*/
|
|
77
|
+
export function signCertificate(lic, authorityKeys) {
|
|
78
|
+
const priv = crypto.createPrivateKey({
|
|
79
|
+
key: Buffer.from(String(authorityKeys.privateKey), 'base64'),
|
|
80
|
+
format: 'der',
|
|
81
|
+
type: 'pkcs8',
|
|
82
|
+
});
|
|
83
|
+
const payload = canonicalLicenseString(lic);
|
|
84
|
+
const signature = crypto.sign(null, Buffer.from(payload, 'utf8'), priv).toString('base64');
|
|
85
|
+
return {
|
|
86
|
+
...lic,
|
|
87
|
+
protocol: lic.protocol || LICENSE_PROTOCOL,
|
|
88
|
+
issuer: lic.issuer || 'work.odant.org',
|
|
89
|
+
issuerPublicKey: authorityKeys.publicKey,
|
|
90
|
+
signature,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Проверить подпись сертификата лицензии.
|
|
96
|
+
*/
|
|
97
|
+
export function verifyCertificate(lic, publicKeyB64 = '') {
|
|
98
|
+
if (!lic || typeof lic !== 'object' || !lic.signature)
|
|
99
|
+
return false;
|
|
100
|
+
const pubKey = publicKeyB64 || lic.issuerPublicKey;
|
|
101
|
+
if (!pubKey)
|
|
102
|
+
return false;
|
|
103
|
+
try {
|
|
104
|
+
const pub = crypto.createPublicKey({
|
|
105
|
+
key: Buffer.from(String(pubKey), 'base64'),
|
|
106
|
+
format: 'der',
|
|
107
|
+
type: 'spki',
|
|
108
|
+
});
|
|
109
|
+
const payload = canonicalLicenseString(lic);
|
|
110
|
+
return crypto.verify(
|
|
111
|
+
null,
|
|
112
|
+
Buffer.from(payload, 'utf8'),
|
|
113
|
+
pub,
|
|
114
|
+
Buffer.from(String(lic.signature), 'base64')
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export {
|
|
2
|
+
PAK_PRODUCT,
|
|
3
|
+
SLA_TIERS,
|
|
4
|
+
calculateOrderQuote,
|
|
5
|
+
FRANCHISE_PROGRAM,
|
|
6
|
+
REGIONS_CATALOG,
|
|
7
|
+
PARTNER_TRAINING_TRACKS,
|
|
8
|
+
calculateRegionPartnerQuota,
|
|
9
|
+
calculateFranchiseFee,
|
|
10
|
+
calculatePartnerDiscount,
|
|
11
|
+
calculatePartnerEconomics,
|
|
12
|
+
} from './catalog.js';
|
|
13
|
+
export { LICENSE_PROTOCOL, loadOrCreateAuthorityKeys, canonicalLicenseString, signCertificate, verifyCertificate } from './crypto.js';
|
|
14
|
+
export { LicenseStore } from './store.js';
|
|
15
|
+
export { YooKassaService } from './yookassa.js';
|
|
16
|
+
export { SupportBotsHub } from './bots.js';
|
|
17
|
+
export { createLicenseServerApp } from './server.js';
|