diff --git a/docs/MANUAL_MAC_UPDATE_UPLOAD.md b/docs/MANUAL_MAC_UPDATE_UPLOAD.md index f423adc..2bdfd0b 100644 --- a/docs/MANUAL_MAC_UPDATE_UPLOAD.md +++ b/docs/MANUAL_MAC_UPDATE_UPLOAD.md @@ -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/` (имена **без версии**): diff --git a/docs/TTRPG-License-Key-Instructions.docx b/docs/TTRPG-License-Key-Instructions.docx new file mode 100644 index 0000000..17d32aa Binary files /dev/null and b/docs/TTRPG-License-Key-Instructions.docx differ diff --git a/docs/~$RPG-Release-Instructions.docx b/docs/~$RPG-Release-Instructions.docx deleted file mode 100644 index 7dab902..0000000 Binary files a/docs/~$RPG-Release-Instructions.docx and /dev/null differ diff --git a/scripts/generate-license-key-docx.py b/scripts/generate-license-key-docx.py new file mode 100644 index 0000000..fe58dfd --- /dev/null +++ b/scripts/generate-license-key-docx.py @@ -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() diff --git a/scripts/generate-release-docx.py b/scripts/generate-release-docx.py index 0e5686a..e39cc48 100644 --- a/scripts/generate-release-docx.py +++ b/scripts/generate-release-docx.py @@ -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.", diff --git a/scripts/ttrpg-release/prepare-release.ps1 b/scripts/ttrpg-release/prepare-release.ps1 index c16e0ee..9c015f7 100644 --- a/scripts/ttrpg-release/prepare-release.ps1 +++ b/scripts/ttrpg-release/prepare-release.ps1 @@ -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)" } } diff --git a/scripts/wsl-pack-linux.sh b/scripts/wsl-pack-linux.sh index deec4d0..b827d11 100644 --- a/scripts/wsl-pack-linux.sh +++ b/scripts/wsl-pack-linux.sh @@ -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