fix error and new docs

This commit is contained in:
Ivan Fontosh
2026-05-18 10:06:47 +08:00
parent 0ae3c39333
commit 10de99bb06
7 changed files with 353 additions and 10 deletions
+2 -1
View File
@@ -3,11 +3,12 @@
## Сборка на Mac
```bash
npm ci
FFMPEG_BINARIES_URL=https://cdn.npmmirror.com/binaries/ffmpeg-static npm ci
npm run pack:mac
```
`pack:mac` сам вызывает `build` и `scripts/release-mac-prep.mjs` — подтягивает **оба** набора нативных бинарников sharp (x64 и arm64). Без этого x64-сборка с Apple Silicon падает при старте с ошибкой `Could not load the "sharp" module using the darwin-x64 runtime`.
Переменная `FFMPEG_BINARIES_URL` нужна только чтобы `ffmpeg-static` не падал из-за временных 5xx на GitHub при `npm ci`.
В `release/` (имена **без версии**):
Binary file not shown.
Binary file not shown.
+298
View File
@@ -0,0 +1,298 @@
# -*- coding: utf-8 -*-
"""Generate TTRPG-License-Key-Instructions.docx (run: python scripts/generate-license-key-docx.py)."""
from __future__ import annotations
from pathlib import Path
from docx import Document
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.shared import Pt
ROOT = Path(__file__).resolve().parents[1]
OUT_PATHS = [
ROOT / "docs" / "TTRPG-License-Key-Instructions.docx",
Path(r"D:\TTRPG-Release\TTRPG-License-Key-Instructions.docx"),
]
def add_bullet(doc: Document, text: str, level: int = 0) -> None:
style = "List Bullet" if level == 0 else "List Bullet 2"
doc.add_paragraph(text, style=style)
def add_step(doc: Document, n: int, title: str, body: str) -> None:
p = doc.add_paragraph()
p.add_run(f"Шаг {n}. {title}. ").bold = True
p.add_run(body)
def add_code(doc: Document, text: str) -> None:
for line in text.strip().splitlines():
p = doc.add_paragraph(line)
p.style = "No Spacing"
for run in p.runs:
run.font.name = "Consolas"
run.font.size = Pt(9)
def build_document() -> Document:
doc = Document()
normal = doc.styles["Normal"]
normal.font.name = "Calibri"
normal.font.size = Pt(11)
title = doc.add_heading("TTRPG Player — выдача нового лицензионного ключа", level=0)
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
doc.add_paragraph(
"Инструкция для администратора: добавление нового продуктового ключа в базу "
"сервера лицензий. Пользователь вводит этот ключ в приложении; сервер выдаёт "
"подписанный лицензионный токен при активации."
)
doc.add_heading("Термины", level=1)
doc_terms = [
(
"Продуктовый ключ",
"строка вида TTRPG-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX — выдаётся покупателю, "
"хранится в data.json на сервере.",
),
(
"Лицензионный токен",
"выдаётся автоматически при активации в приложении; вручную создавать не нужно.",
),
(
"sub",
"внутренний идентификатор лицензии на сервере (отзыв, учёт устройств).",
),
]
for term, desc in doc_terms:
p = doc.add_paragraph()
p.add_run(f"{term}").bold = True
p.add_run(desc)
doc.add_heading("Где лежит сервер (продакшен)", level=1)
srv = doc.add_table(rows=6, cols=2)
srv.style = "Table Grid"
srv.rows[0].cells[0].text = "Параметр"
srv.rows[0].cells[1].text = "Значение"
server_rows = [
("Домен", "https://license.mailib.ru/"),
("VPS", "185.173.94.234"),
("Папка проекта", "/var/www/license_mailib_ru"),
("Файл ключей", "/var/www/license_mailib_ru/data.json"),
("Пользователь Linux", "dndlicense"),
]
for i, (k, v) in enumerate(server_rows, start=1):
srv.rows[i].cells[0].text = k
srv.rows[i].cells[1].text = v
doc.add_heading("Редактирование data.json (удобный редактор)", level=1)
doc.add_paragraph(
"Не используйте nano, если нужны выделение мышью и привычное копирование. "
"Рекомендуется Cursor или VS Code с расширением Remote - SSH."
)
add_step(
doc,
1,
"Настроить SSH",
"В %USERPROFILE%\\.ssh\\config добавьте хост (ключ ttrpg_updates_root — тот же, "
"что для публикации обновлений):",
)
add_code(
doc,
"""Host mailib-vps
HostName 185.173.94.234
User root
IdentityFile C:\\Users\\Administrator\\.ssh\\ttrpg_updates_root""",
)
add_step(
doc,
2,
"Подключиться",
"F1 → Remote-SSH: Connect to Host… → mailib-vps.",
)
add_step(
doc,
3,
"Открыть папку",
"File → Open Folder → /var/www/license_mailib_ru → открыть data.json.",
)
add_bullet(doc, "Альтернатива: WinSCP (SFTP) → правый клик по data.json → Edit.")
add_bullet(
doc,
"Альтернатива: scp скачать файл на ПК, править в Cursor, scp залить обратно.",
)
doc.add_heading("Выдача нового ключа", level=1)
add_step(
doc,
1,
"Резервная копия",
"На сервере (SSH или терминал в Remote SSH):",
)
add_code(
doc,
"cd /var/www/license_mailib_ru\n"
"cp data.json \"data.json.bak.$(date +%F-%H%M%S)\"",
)
add_step(
doc,
2,
"Сгенерировать запись (Node.js на сервере)",
"Выполнить в каталоге /var/www/license_mailib_ru. Скрипт добавит запись в "
"productKeys и выведет новый ключ в консоль. Срок и лимит устройств — в начале скрипта.",
)
add_code(
doc,
r"""node -e "
const fs = require('node:fs');
const crypto = require('node:crypto');
const dataPath = process.env.DND_LICENSE_DATA_PATH || './data.json';
const data = JSON.parse(fs.readFileSync(dataPath, 'utf8'));
const maxDevices = 3;
const expiresAtSec = Math.floor(new Date('2027-12-31T23:59:59Z').getTime() / 1000);
const key = 'TTRPG-' + crypto.randomUUID().toUpperCase();
const sub = 'lic_' + crypto.randomUUID().replace(/-/g, '');
data.productKeys ??= [];
data.productKeys.push({
key,
sub,
pid: 'dnd_player',
maxDevices,
expiresAtSec
});
fs.writeFileSync(dataPath, JSON.stringify(data, null, 2) + '\n');
console.log('NEW PRODUCT KEY:', key);
console.log('sub:', sub);
console.log('expiresAtSec:', expiresAtSec);
console.log('maxDevices:', maxDevices);
"
""",
)
doc.add_paragraph(
"Сохраните выведенный NEW PRODUCT KEY — его передаёте пользователю. "
"Формат ключа должен соответствовать шаблону TTRPG-… (заглавные hex-символы)."
)
add_step(
doc,
3,
"Или добавить вручную в data.json",
"В массив productKeys добавьте объект (пример):",
)
add_code(
doc,
"""{
"key": "TTRPG-12345678-1234-1234-1234-123456789ABC",
"sub": "lic_unique_id",
"pid": "dnd_player",
"maxDevices": 3,
"expiresAtSec": 1830268799
}""",
)
fields = doc.add_table(rows=6, cols=2)
fields.style = "Table Grid"
fields.rows[0].cells[0].text = "Поле"
fields.rows[0].cells[1].text = "Описание"
field_rows = [
("key", "Продуктовый ключ для пользователя (уникальный)"),
("sub", "Уникальный ID лицензии на сервере"),
("pid", "Обычно dnd_player"),
("maxDevices", "Сколько разных deviceId можно активировать"),
("expiresAtSec", "Срок действия (Unix time, секунды)"),
]
for i, (k, v) in enumerate(field_rows, start=1):
fields.rows[i].cells[0].text = k
fields.rows[i].cells[1].text = v
add_step(
doc,
4,
"Права на файл (если правили от root)",
"chown dndlicense:dndlicense /var/www/license_mailib_ru/data.json",
)
add_step(
doc,
5,
"Перезапуск сервиса",
"После изменения data.json:",
)
add_code(doc, "sudo -u dndlicense pm2 restart dnd-license")
doc.add_heading("Проверка", level=1)
add_bullet(doc, "Сервер жив: curl https://license.mailib.ru/health → {\"ok\":true}")
add_bullet(
doc,
"В приложении TTRPG Player: «Указать ключ» → вставить продуктовый ключ → активация.",
)
add_bullet(
doc,
"При ошибке unknown_product_key — ключ не в data.json или опечатка; "
"too_many_devices — превышен maxDevices.",
)
doc.add_heading("Отзыв лицензии", level=1)
doc.add_paragraph(
"Чтобы отозвать лицензию по sub, добавьте sub в массив revokedSubs в data.json "
"и перезапустите pm2. Либо POST /v1/admin/revoke с Bearer-токеном администратора "
"(токен хранится только на сервере, в эту инструкцию не включён)."
)
doc.add_heading("Локальная разработка", level=1)
add_bullet(
doc,
"Исходники сервера на ПК: D:\\Work\\my_projects\\dnd_project\\DndGamePlayerLicenseServer",
)
add_bullet(doc, "Локальный data.json: скопировать из data.example.json")
add_bullet(doc, "Запуск: npm start (нужен LICENSE_PRIVATE_KEY_PEM)")
add_bullet(doc, "Порт по умолчанию: 3847")
doc.add_heading("Частые проблемы", level=1)
problems = [
(
"Ключ не принимается в приложении",
"Проверьте формат TTRPG-… (8-4-4-4-12 hex), что запись есть в productKeys, "
"сервер перезапущен после правки data.json.",
),
(
"JSON сломан после правки",
"Восстановите из data.json.bak.*; проверьте запятые и кавычки.",
),
(
"Нет прав на запись",
"Правьте от root или chown на dndlicense; файл не должен быть только для чтения.",
),
]
for prob, fix in problems:
p = doc.add_paragraph()
p.add_run(f"{prob}. ").bold = True
p.add_run(fix)
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p.add_run(
"Репозиторий сервера: git.mailib.ru/ifontosh/DndGamePlayerLicenseServer"
).italic = True
return doc
def main() -> None:
doc = build_document()
for path in OUT_PATHS:
path.parent.mkdir(parents=True, exist_ok=True)
doc.save(str(path))
print(f"Wrote {path}")
if __name__ == "__main__":
main()
+6 -1
View File
@@ -53,7 +53,8 @@ def build_document() -> Document:
add_step(
doc,
"Шаг 2. Собрать macOS (только на Mac)",
"npm ci && npm run build && npm run pack:mac",
"FFMPEG_BINARIES_URL=https://cdn.npmmirror.com/binaries/ffmpeg-static npm ci && "
"npm run build && npm run pack:mac",
)
add_step(
doc,
@@ -159,6 +160,10 @@ def build_document() -> Document:
"WSL Linux build / Node 18",
"Нужен nvm и Node 22 (scripts/wsl-pack-linux.sh).",
),
(
"npm ci / ffmpeg-static / GitHub 5xx",
"Win-скрипт повторит npm ci через FFMPEG_BINARIES_URL; на Mac используйте эту переменную вручную.",
),
(
"AfterMac: version mismatch",
"Версии в package.json и latest-mac.yml должны совпадать до запуска release-all.",
+45 -8
View File
@@ -45,6 +45,44 @@ function Write-Fail([string]$text) {
Write-Host " [!!] $text" -ForegroundColor Red
}
$FfmpegStaticMirror = 'https://cdn.npmmirror.com/binaries/ffmpeg-static'
function Test-IsNpmCi([string[]]$NpmArgs) {
return ($NpmArgs.Count -eq 1 -and $NpmArgs[0] -eq 'ci')
}
function Invoke-NpmRaw {
param(
[string[]]$NpmArgs,
[string]$WorkingDirectory
)
Push-Location $WorkingDirectory
try {
& npm @NpmArgs
return $LASTEXITCODE
} finally {
Pop-Location
}
}
function Invoke-NpmCiWithFfmpegMirror {
param(
[string]$WorkingDirectory
)
$prevMirror = $env:FFMPEG_BINARIES_URL
$env:FFMPEG_BINARIES_URL = $FfmpegStaticMirror
try {
Write-Host " > npm ci (retry with FFMPEG_BINARIES_URL=$FfmpegStaticMirror)"
return (Invoke-NpmRaw -NpmArgs @('ci') -WorkingDirectory $WorkingDirectory)
} finally {
if ($null -eq $prevMirror) {
Remove-Item Env:\FFMPEG_BINARIES_URL -ErrorAction SilentlyContinue
} else {
$env:FFMPEG_BINARIES_URL = $prevMirror
}
}
}
function Invoke-Npm {
param(
[string]$Label,
@@ -52,14 +90,13 @@ function Invoke-Npm {
[string]$WorkingDirectory
)
Write-Host " > $Label"
Push-Location $WorkingDirectory
try {
& npm @NpmArgs
if ($LASTEXITCODE -ne 0) {
throw "$Label failed (exit $LASTEXITCODE)"
}
} finally {
Pop-Location
$exitCode = Invoke-NpmRaw -NpmArgs $NpmArgs -WorkingDirectory $WorkingDirectory
if ($exitCode -ne 0 -and (Test-IsNpmCi $NpmArgs)) {
Write-Host " [--] npm ci failed (exit $exitCode). Retrying via ffmpeg-static mirror..." -ForegroundColor Yellow
$exitCode = Invoke-NpmCiWithFfmpegMirror $WorkingDirectory
}
if ($exitCode -ne 0) {
throw "$Label failed (exit $exitCode)"
}
}
+2
View File
@@ -15,5 +15,7 @@ else
fi
echo "Using $(node -v) ($(command -v node))"
export FFMPEG_BINARIES_URL="${FFMPEG_BINARIES_URL:-https://cdn.npmmirror.com/binaries/ffmpeg-static}"
echo "Using ffmpeg-static mirror: ${FFMPEG_BINARIES_URL}"
npm ci
npm run pack:linux