fix error and new docs
This commit is contained in:
@@ -3,11 +3,12 @@
|
|||||||
## Сборка на Mac
|
## Сборка на Mac
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm ci
|
FFMPEG_BINARIES_URL=https://cdn.npmmirror.com/binaries/ffmpeg-static npm ci
|
||||||
npm run pack:mac
|
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`.
|
`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/` (имена **без версии**):
|
В `release/` (имена **без версии**):
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -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()
|
||||||
@@ -53,7 +53,8 @@ def build_document() -> Document:
|
|||||||
add_step(
|
add_step(
|
||||||
doc,
|
doc,
|
||||||
"Шаг 2. Собрать macOS (только на Mac)",
|
"Шаг 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(
|
add_step(
|
||||||
doc,
|
doc,
|
||||||
@@ -159,6 +160,10 @@ def build_document() -> Document:
|
|||||||
"WSL Linux build / Node 18",
|
"WSL Linux build / Node 18",
|
||||||
"Нужен nvm и Node 22 (scripts/wsl-pack-linux.sh).",
|
"Нужен nvm и Node 22 (scripts/wsl-pack-linux.sh).",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"npm ci / ffmpeg-static / GitHub 5xx",
|
||||||
|
"Win-скрипт повторит npm ci через FFMPEG_BINARIES_URL; на Mac используйте эту переменную вручную.",
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"AfterMac: version mismatch",
|
"AfterMac: version mismatch",
|
||||||
"Версии в package.json и latest-mac.yml должны совпадать до запуска release-all.",
|
"Версии в package.json и latest-mac.yml должны совпадать до запуска release-all.",
|
||||||
|
|||||||
@@ -45,6 +45,44 @@ function Write-Fail([string]$text) {
|
|||||||
Write-Host " [!!] $text" -ForegroundColor Red
|
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 {
|
function Invoke-Npm {
|
||||||
param(
|
param(
|
||||||
[string]$Label,
|
[string]$Label,
|
||||||
@@ -52,14 +90,13 @@ function Invoke-Npm {
|
|||||||
[string]$WorkingDirectory
|
[string]$WorkingDirectory
|
||||||
)
|
)
|
||||||
Write-Host " > $Label"
|
Write-Host " > $Label"
|
||||||
Push-Location $WorkingDirectory
|
$exitCode = Invoke-NpmRaw -NpmArgs $NpmArgs -WorkingDirectory $WorkingDirectory
|
||||||
try {
|
if ($exitCode -ne 0 -and (Test-IsNpmCi $NpmArgs)) {
|
||||||
& npm @NpmArgs
|
Write-Host " [--] npm ci failed (exit $exitCode). Retrying via ffmpeg-static mirror..." -ForegroundColor Yellow
|
||||||
if ($LASTEXITCODE -ne 0) {
|
$exitCode = Invoke-NpmCiWithFfmpegMirror $WorkingDirectory
|
||||||
throw "$Label failed (exit $LASTEXITCODE)"
|
|
||||||
}
|
}
|
||||||
} finally {
|
if ($exitCode -ne 0) {
|
||||||
Pop-Location
|
throw "$Label failed (exit $exitCode)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,5 +15,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Using $(node -v) ($(command -v node))"
|
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 ci
|
||||||
npm run pack:linux
|
npm run pack:linux
|
||||||
|
|||||||
Reference in New Issue
Block a user