feat(release): make the signing-free install work with nothing but Windows
Some checks failed
deploy-site / deploy (push) Failing after 3m26s
Some checks failed
deploy-site / deploy (push) Failing after 3m26s
The manual install path still needed 7-Zip, which the target machine does not have, so "installable without a certificate" was not yet true. The channel now also publishes the app as byte-split zip parts, and the install script joins them and extracts with the built-in Windows Expand-Archive after verifying every part and the joined archive. Version 1.3.1 republishes the channel from a single build, because a version's artifacts can only match one build and published volumes are never overwritten.
This commit is contained in:
parent
c35c6f3e95
commit
0411f389d9
30 changed files with 234 additions and 118 deletions
|
|
@ -13,6 +13,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- Cloud-optional backup (encrypted, opt-in)
|
- Cloud-optional backup (encrypted, opt-in)
|
||||||
- Plugin system for custom pipelines
|
- Plugin system for custom pipelines
|
||||||
|
|
||||||
|
## [1.3.1] - 2026-09-18
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Manual install no longer needs 7-Zip**: the signing-free channel now also publishes the
|
||||||
|
app as byte-split `zip` parts. The install script joins them and extracts with the built-in
|
||||||
|
Windows `Expand-Archive`, so a user with nothing but Windows can install (the 7z volumes
|
||||||
|
remain the smaller Scoop path). Split volumes from a different build are never mixed:
|
||||||
|
every artifact of a release comes from one build, and a published version is not overwritten.
|
||||||
|
|
||||||
## [1.3.0] - 2026-09-18
|
## [1.3.0] - 2026-09-18
|
||||||
|
|
||||||
> Published from an annotated tag through CI. Installer and update metadata are
|
> Published from an annotated tag through CI. Installer and update metadata are
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
"info": {
|
"info": {
|
||||||
"title": "D3RO-VOICE Admin API",
|
"title": "D3RO-VOICE Admin API",
|
||||||
"description": "Admin CRM Edge Functions for user management, subscription CRUD, payment history, and audit logs.",
|
"description": "Admin CRM Edge Functions for user management, subscription CRUD, payment history, and audit logs.",
|
||||||
"version": "1.3.0"
|
"version": "1.3.1"
|
||||||
},
|
},
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@d3ro/admin",
|
"name": "@d3ro/admin",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "D3RO Voice Admin CRM — SaaS 관리 도구",
|
"description": "D3RO Voice Admin CRM — SaaS 관리 도구",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Version>1.3.0</Version>
|
<Version>1.3.1</Version>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@d3ro/desktop",
|
"name": "@d3ro/desktop",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"productName": "d3ro-voice",
|
"productName": "d3ro-voice",
|
||||||
"description": "로컬 AI 음성 어시스턴트 (Electron)",
|
"description": "로컬 AI 음성 어시스턴트 (Electron)",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
|
|
|
||||||
|
|
@ -151,8 +151,8 @@ def versionSettingsValid = configuredVersionName != null &&
|
||||||
configuredVersionName ==~ strictSemver &&
|
configuredVersionName ==~ strictSemver &&
|
||||||
configuredVersionCodeValue != null &&
|
configuredVersionCodeValue != null &&
|
||||||
configuredVersionCodeValue <= 2100000000L
|
configuredVersionCodeValue <= 2100000000L
|
||||||
def resolvedVersionName = versionSettingsValid ? configuredVersionName : "1.3.0"
|
def resolvedVersionName = versionSettingsValid ? configuredVersionName : "1.3.1"
|
||||||
def resolvedVersionCode = versionSettingsValid ? configuredVersionCodeValue.toInteger() : 1030001
|
def resolvedVersionCode = versionSettingsValid ? configuredVersionCodeValue.toInteger() : 1031001
|
||||||
|
|
||||||
def requiredReleaseSettings = [
|
def requiredReleaseSettings = [
|
||||||
D3RO_RELEASE_STORE_FILE: releaseStoreFilePath,
|
D3RO_RELEASE_STORE_FILE: releaseStoreFilePath,
|
||||||
|
|
|
||||||
|
|
@ -257,7 +257,7 @@
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1030001;
|
CURRENT_PROJECT_VERSION = 1031001;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = D3ROVoice/Info.plist;
|
INFOPLIST_FILE = D3ROVoice/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
|
|
@ -265,7 +265,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.3.0;
|
MARKETING_VERSION = 1.3.1;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
|
|
@ -287,14 +287,14 @@
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1030001;
|
CURRENT_PROJECT_VERSION = 1031001;
|
||||||
INFOPLIST_FILE = D3ROVoice/Info.plist;
|
INFOPLIST_FILE = D3ROVoice/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.3.0;
|
MARKETING_VERSION = 1.3.1;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Added a signing-free install path: the download is served in verifiable parts and installed after checking them.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
서명 인증서 없이도 설치할 수 있는 배포 경로를 추가했습니다. 설치 파일이 나뉘어 제공되고 검증 후에 설치됩니다.
|
||||||
14
apps/mobile-rn/package-lock.json
generated
14
apps/mobile-rn/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@d3ro/mobile-rn",
|
"name": "@d3ro/mobile-rn",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@d3ro/mobile-rn",
|
"name": "@d3ro/mobile-rn",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@d3ro/api-client": "file:../../packages/api-client",
|
"@d3ro/api-client": "file:../../packages/api-client",
|
||||||
"@d3ro/core": "file:../../packages/core",
|
"@d3ro/core": "file:../../packages/core",
|
||||||
|
|
@ -62,7 +62,7 @@
|
||||||
},
|
},
|
||||||
"../..": {
|
"../..": {
|
||||||
"name": "d3ro-voice-monorepo",
|
"name": "d3ro-voice-monorepo",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/desktop",
|
"apps/desktop",
|
||||||
|
|
@ -81,7 +81,7 @@
|
||||||
},
|
},
|
||||||
"../../packages/api-client": {
|
"../../packages/api-client": {
|
||||||
"name": "@d3ro/api-client",
|
"name": "@d3ro/api-client",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@d3ro/core": "*",
|
"@d3ro/core": "*",
|
||||||
|
|
@ -98,7 +98,7 @@
|
||||||
},
|
},
|
||||||
"../../packages/core": {
|
"../../packages/core": {
|
||||||
"name": "@d3ro/core",
|
"name": "@d3ro/core",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"docx": "^9.6.1"
|
"docx": "^9.6.1"
|
||||||
|
|
@ -109,7 +109,7 @@
|
||||||
},
|
},
|
||||||
"../../packages/i18n": {
|
"../../packages/i18n": {
|
||||||
"name": "@d3ro/i18n",
|
"name": "@d3ro/i18n",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
|
|
@ -120,7 +120,7 @@
|
||||||
},
|
},
|
||||||
"../../packages/ui-native": {
|
"../../packages/ui-native": {
|
||||||
"name": "@d3ro/ui-native",
|
"name": "@d3ro/ui-native",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@d3ro/mobile-rn",
|
"name": "@d3ro/mobile-rn",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@d3ro/web",
|
"name": "@d3ro/web",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "D3RO Voice 웹 앱 — Next.js 기반 SaaS 인터페이스",
|
"description": "D3RO Voice 웹 앱 — Next.js 기반 SaaS 인터페이스",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ export function Sidebar(): React.ReactElement {
|
||||||
</PhosphorText>
|
</PhosphorText>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ fontSize: '10px', fontFamily: 'ui-monospace, monospace', color: d3roPalette.text.muted }}>
|
<Box sx={{ fontSize: '10px', fontFamily: 'ui-monospace, monospace', color: d3roPalette.text.muted }}>
|
||||||
v1.3.0
|
v1.3.1
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
{
|
{
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"description": "로컬 AI 음성 어시스턴트 (faster-whisper + Ollama, 100% 오프라인 지원)",
|
"description": "로컬 AI 음성 어시스턴트 (faster-whisper + Ollama, 100% 오프라인 지원)",
|
||||||
"homepage": "https://d3ro.chanpaca.net",
|
"homepage": "https://d3ro.chanpaca.net",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"architecture": {
|
"architecture": {
|
||||||
"64bit": {
|
"64bit": {
|
||||||
"url": [
|
"url": [
|
||||||
"https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-1.3.0/D3RO-Voice-1.3.0-x64-portable.7z.001",
|
"https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-1.3.1/D3RO-Voice-1.3.1-x64-portable.7z.001",
|
||||||
"https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-1.3.0/D3RO-Voice-1.3.0-x64-portable.7z.002"
|
"https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-1.3.1/D3RO-Voice-1.3.1-x64-portable.7z.002"
|
||||||
],
|
],
|
||||||
"hash": [
|
"hash": [
|
||||||
"c4824bd625a2f0fd685f6fd3dd8521f4ced7d46954b59f0c10efcd432f82bd3f",
|
"917f9d6e078517b6c924c30d26b59cf530d8c69e4adbdf679d2196bc645fa993",
|
||||||
"187f4917e53525e736df838b15323e840691d602eadcfd9024407498316932ce"
|
"06f32ca809fed42cda9900e04a8e50da42506ed71944462314ecef40773a3197"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -29,8 +29,8 @@
|
||||||
"architecture": {
|
"architecture": {
|
||||||
"64bit": {
|
"64bit": {
|
||||||
"url": [
|
"url": [
|
||||||
"https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-$version/D3RO-Voice-1.3.0-x64-portable.7z.001",
|
"https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-$version/D3RO-Voice-1.3.1-x64-portable.7z.001",
|
||||||
"https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-$version/D3RO-Voice-1.3.0-x64-portable.7z.002"
|
"https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-$version/D3RO-Voice-1.3.1-x64-portable.7z.002"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,13 @@ npm run release:portable # Forgejo portable 채널 게시
|
||||||
- 자동 업데이트 피드(`latest.yml`)는 **건드리지 않는다** — 서명된 릴리스 전용이다.
|
- 자동 업데이트 피드(`latest.yml`)는 **건드리지 않는다** — 서명된 릴리스 전용이다.
|
||||||
- 볼륨은 불변이다: 같은 버전 경로에 다른 바이트가 있으면 게시가 중단된다.
|
- 볼륨은 불변이다: 같은 버전 경로에 다른 바이트가 있으면 게시가 중단된다.
|
||||||
- 태그/수동 실행 워크플로: `.forgejo/workflows/portable.yml` (필요 시크릿: `FORGEJO_TOKEN`).
|
- 태그/수동 실행 워크플로: `.forgejo/workflows/portable.yml` (필요 시크릿: `FORGEJO_TOKEN`).
|
||||||
|
- 산출물은 7z 분할 볼륨(Scoop용, 162MiB)과 zip 분할 부품(수동 설치용, 243MiB) 두 가지다.
|
||||||
|
수동 설치 스크립트는 Windows 내장 `Expand-Archive`만 쓰므로 7-Zip이 필요 없다.
|
||||||
- 사용자 설치: Scoop 버킷(`bucket/`) 또는 `install-d3ro-voice.ps1`.
|
- 사용자 설치: Scoop 버킷(`bucket/`) 또는 `install-d3ro-voice.ps1`.
|
||||||
|
- 한 버전의 산출물은 한 번의 빌드에서만 나온다(볼륨은 불변). CI 태그 파이프라인이 최초 게시자가 되게 하고,
|
||||||
|
이미 게시된 버전을 같은 번호로 다시 게시하지 않는다 — 필요하면 버전을 올린다.
|
||||||
|
- 로컬에서 게시할 때는 `npm run release:portable:build`가 out/와 sidecar-dist/를 먼저 요구한다
|
||||||
|
(`npm run build --workspace=@d3ro/desktop`, `npm run sidecar:build --workspace=@d3ro/desktop`).
|
||||||
|
|
||||||
## `1.2.0` 릴리스 절차 (기록)
|
## `1.2.0` 릴리스 절차 (기록)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,10 @@
|
||||||
|
|
||||||
### 3-1. 서명 없이 지금 배포되는 것 (portable 채널)
|
### 3-1. 서명 없이 지금 배포되는 것 (portable 채널)
|
||||||
|
|
||||||
- 산출물: 95MiB 단위 **7z 분할 볼륨**(`D3RO-Voice-<v>x64-portable.7z.001/.002`) — LZMA2로
|
- 산출물 두 가지(같은 버라도 목적이 다름):
|
||||||
688MB → 162MiB까지 줄어 각 볼륨이 Cloudflare 한도 안에 들어간다.
|
- **7z 분할 볼륨** `D3RO-Voice-<v>-x64-portable.7z.001/.002` — LZMA2로 688MB → 162MiB. Scoop 전용.
|
||||||
|
- **zip 분할 부품** `...zip.001/.002/.003` — 243MiB. 수동 설치 스크립트용. Windows 내장
|
||||||
|
`Expand-Archive`만으로 풀 수 있어 사용자가 7-Zip을 설치할 필요가 없다.
|
||||||
- 게시 위치: `.../generic/d3ro-voice/portable-<version>/` 와 `.../portable-latest/`
|
- 게시 위치: `.../generic/d3ro-voice/portable-<version>/` 와 `.../portable-latest/`
|
||||||
(**자동 업데이트 피드 `latest.yml`과 완전히 분리**).
|
(**자동 업데이트 피드 `latest.yml`과 완전히 분리**).
|
||||||
- 설치 방법 두 가지:
|
- 설치 방법 두 가지:
|
||||||
|
|
@ -58,8 +60,9 @@
|
||||||
scoop install d3ro/d3ro-voice
|
scoop install d3ro/d3ro-voice
|
||||||
```
|
```
|
||||||
2. **수동 설치 스크립트** — `scripts/install/install-d3ro-voice.ps1` (피드에도 게시됨).
|
2. **수동 설치 스크립트** — `scripts/install/install-d3ro-voice.ps1` (피드에도 게시됨).
|
||||||
볼륨을 내려받아 SHA-256 검증 → 결합 → 7-Zip으로 해제 → `%LOCALAPPDATA%\Programs\D3RO Voice`
|
zip 부품을 내려받아 부품별 SHA-256 검증 → 결합 → 결합본 SHA-256 재검증 →
|
||||||
에 설치 + 시작 메뉴 바로가기. 관리자 권한 불필요, 7-Zip 필요.
|
`Expand-Archive`로 해제 → `%LOCALAPPDATA%\Programs\D3RO Voice`에 설치 + 시작 메뉴 바로가기.
|
||||||
|
관리자 권한 불필요, 추가 도구 불필요(Windows 10/11 기본).
|
||||||
```powershell
|
```powershell
|
||||||
irm https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-latest/install-d3ro-voice.ps1 | iex
|
irm https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-latest/install-d3ro-voice.ps1 | iex
|
||||||
```
|
```
|
||||||
|
|
@ -69,6 +72,10 @@
|
||||||
**안전 규칙(구현에 반영)**:
|
**안전 규칙(구현에 반영)**:
|
||||||
- 볼륨은 불변 — 같은 버전 경로에 다른 바이트가 있으면 게시 중단.
|
- 볼륨은 불변 — 같은 버전 경로에 다른 바이트가 있으면 게시 중단.
|
||||||
- 메타데이터(`portable.json`, 설치 스크립트)만 갱신 허용.
|
- 메타데이터(`portable.json`, 설치 스크립트)만 갱신 허용.
|
||||||
|
- 같은 버전을 다시 빌드하면 7z/zip 바이트가 달라져(내부 타임스탬프) 게시가 중단된다.
|
||||||
|
즉 **한 버전의 portable 산출물은 한 번의 빌드에서만 나온다**. 태그 파이프라인(CI)이 최초
|
||||||
|
게시자가 되도록 하고, 로컬 재게시로 채널을 덮어쓰지 않는다. 이미 게시된 세트를 갱신해야 하면
|
||||||
|
버전을 올린다(이 문서 작성 시 `1.3.0` 부분 게시분을 `1.3.1`로 대체).
|
||||||
- 파일명에 `-portable`을 넣어 서명된 릴리스 자산과 혼동되지 않게 한다.
|
- 파일명에 `-portable`을 넣어 서명된 릴리스 자산과 혼동되지 않게 한다.
|
||||||
- `latest.yml`/`update-policy.json`은 절대 건드리지 않는다(자동 업데이트는 서명 릴리스 전용).
|
- `latest.yml`/`update-policy.json`은 절대 건드리지 않는다(자동 업데이트는 서명 릴리스 전용).
|
||||||
|
|
||||||
|
|
@ -95,11 +102,21 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 검증 기록 (2026-09-18)
|
## 5. 현재 상태 (2026-09-18)
|
||||||
|
|
||||||
|
- 게시된 채널: `portable-1.3.1` + `portable-latest` (7z 볼륨 2개, zip 부품 3개, 인덱스, 설치 스크립트).
|
||||||
|
- `portable-1.3.0`에는 7z 볼륨 2개만 있다(부분 게시, zip 부품 없음) — `1.3.1`이 대체한다.
|
||||||
|
- updater feed(`latest.yml`)는 여전히 `1.1.0`이며 **이 채널은 그것을 건드리지 않는다**.
|
||||||
|
|
||||||
|
## 6. 검증 기록 (2026-09-18)
|
||||||
|
|
||||||
- 7z 볼륨 게시 후 **무인증 공개 GET**으로 인덱스/볼륨/스크립트 제공 확인.
|
- 7z 볼륨 게시 후 **무인증 공개 GET**으로 인덱스/볼륨/스크립트 제공 확인.
|
||||||
- 설치 스크립트 end-to-end 실행: 볼륨 2개 다운로드 → SHA-256 검증 → 결합(162.1MiB) →
|
- 설치 스크립트 end-to-end 실행: 볼륨 2개 다운로드 → SHA-256 검증 → 결합(162.1MiB) →
|
||||||
7-Zip 해제 → 설치 디렉토리에 `D3RO Voice.exe`, `resources/sidecar/sidecar.exe`,
|
7-Zip 해제 → 설치 디렉토리에 `D3RO Voice.exe`, `resources/sidecar/sidecar.exe`,
|
||||||
`resources/sidecar/_internal/faster_whisper/assets/silero_vad_v6.onnx`, `resources/sox/sox.exe`
|
`resources/sidecar/_internal/faster_whisper/assets/silero_vad_v6.onnx`, `resources/sox/sox.exe`
|
||||||
존재 확인 → 시작 메뉴 바로가기 생성. (검증 후 테스트 설치/바로가기는 제거)
|
존재 확인 → 시작 메뉴 바로가기 생성. (검증 후 테스트 설치/바로가기는 제거)
|
||||||
- 재게시 시도 시 동일 볼륨은 "이미 동일한 파일" 로 건너뛰는 것 확인(불변 가드 동작).
|
- 재게시 시도 시 동일 볼륨은 "이미 동일한 파일" 로 건너뛰는 것 확인(불변 가드 동작).
|
||||||
|
- 1.3.1 수동 설치 스크립트를 **7-Zip 없이** end-to-end 실행: zip 부품 3개 다운로드 →
|
||||||
|
부품별 SHA-256 → 결합(242.9MiB) → 결합본 SHA-256 → Expand-Archive → 설치 디렉토리에
|
||||||
|
`D3RO Voice.exe`, `resources/sidecar/sidecar.exe`, VAD `silero_vad_v6.onnx`, `resources/sox/sox.exe`
|
||||||
|
확인 → 시작 메뉴 바로가기 생성. (검증 후 테스트 설치/바로가기 제거)
|
||||||
|
|
@ -193,7 +193,7 @@ Status quick-reference: `[x]` done+verified · `[~]` partial/unverified · `[ ]`
|
||||||
| INFRA-13 | Site deploy (Cloudflare Pages + GitHub Pages) | [x] | `.forgejo/workflows/deploy-site.yml`, `.github/workflows/deploy-site.yml` |
|
| INFRA-13 | Site deploy (Cloudflare Pages + GitHub Pages) | [x] | `.forgejo/workflows/deploy-site.yml`, `.github/workflows/deploy-site.yml` |
|
||||||
| INFRA-15 | Update & release system | [x] | Canonical Forgejo feed + channels/policy (`release/update-policy.json`, `src/main/update-policy.ts`), canonical publisher `scripts/ci/publish-forgejo-release.mjs`, legacy GitLab mirror; `npm run release:metadata:test`. v1.1.0 was published to Forgejo on 2026-09-15; product version moved to `1.2.0` as a forward-fix with CI-only publication, a same-version re-release guard, and download centers that link the feed instead of repository paths. `1.3.0` (2026-09-18) carries the local-STT fixes; Windows publication still needs the CI signing secrets (`11` GAP-REL-02). |
|
| INFRA-15 | Update & release system | [x] | Canonical Forgejo feed + channels/policy (`release/update-policy.json`, `src/main/update-policy.ts`), canonical publisher `scripts/ci/publish-forgejo-release.mjs`, legacy GitLab mirror; `npm run release:metadata:test`. v1.1.0 was published to Forgejo on 2026-09-15; product version moved to `1.2.0` as a forward-fix with CI-only publication, a same-version re-release guard, and download centers that link the feed instead of repository paths. `1.3.0` (2026-09-18) carries the local-STT fixes; Windows publication still needs the CI signing secrets (`11` GAP-REL-02). |
|
||||||
| INFRA-16 | Desktop STT engine packaging | [x] | `apps/desktop/scripts/setup-sidecar.mjs` + `build-sidecar.mjs`, `electron-builder.yml` `extraResources` (`sidecar-dist/sidecar` → `resources/sidecar`, `resources/ffmpeg`), and `scripts/ci/verify-sidecar-bundle.mjs` run in `package-windows`/`package-macos` before electron-builder. Verified on the real bundle: `sidecar.exe` + `_internal` including `faster_whisper/assets/silero_vad_v6.onnx`, plus a packaged-engine transcription round-trip on GPU. |
|
| INFRA-16 | Desktop STT engine packaging | [x] | `apps/desktop/scripts/setup-sidecar.mjs` + `build-sidecar.mjs`, `electron-builder.yml` `extraResources` (`sidecar-dist/sidecar` → `resources/sidecar`, `resources/ffmpeg`), and `scripts/ci/verify-sidecar-bundle.mjs` run in `package-windows`/`package-macos` before electron-builder. Verified on the real bundle: `sidecar.exe` + `_internal` including `faster_whisper/assets/silero_vad_v6.onnx`, plus a packaged-engine transcription round-trip on GPU. |
|
||||||
| INFRA-17 | 서명 없는 배포 채널 (portable + Scoop) | [x] | `scripts/ci/build-portable.mjs` (95MiB 7z 분할 볼륨 + Scoop 매니페스트), `scripts/ci/publish-portable-release.mjs`, `scripts/local/install-d3ro-voice.ps1`, `bucket/` 버킷, `.forgejo/workflows/portable.yml`; updater feed와 분리. 2026-09-18 게시 + 실제 설치 검증. |
|
| INFRA-17 | 서명 없는 배포 채널 (portable + Scoop) | [x] | `scripts/ci/build-portable.mjs` (95MiB 7z 분할 볼륨 + Scoop 매니페스트), `scripts/ci/publish-portable-release.mjs`, `scripts/local/install-d3ro-voice.ps1`, `bucket/` 버킷, `.forgejo/workflows/portable.yml`; 7z 분할 볼륨(Scoop, 162MiB) + zip 분할 부품(수동 설치, 243MiB, 7-Zip 불필요); updater feed와 분리. 2026-09-18 `portable-1.3.1` 게시 + 실제 설치 검증. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
20
package-lock.json
generated
20
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "d3ro-voice-monorepo",
|
"name": "d3ro-voice-monorepo",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "d3ro-voice-monorepo",
|
"name": "d3ro-voice-monorepo",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/desktop",
|
"apps/desktop",
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
},
|
},
|
||||||
"apps/admin": {
|
"apps/admin": {
|
||||||
"name": "@d3ro/admin",
|
"name": "@d3ro/admin",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@d3ro/api-client": "*",
|
"@d3ro/api-client": "*",
|
||||||
"@d3ro/core": "*",
|
"@d3ro/core": "*",
|
||||||
|
|
@ -109,7 +109,7 @@
|
||||||
},
|
},
|
||||||
"apps/desktop": {
|
"apps/desktop": {
|
||||||
"name": "@d3ro/desktop",
|
"name": "@d3ro/desktop",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@d3ro/core": "*",
|
"@d3ro/core": "*",
|
||||||
|
|
@ -157,7 +157,7 @@
|
||||||
},
|
},
|
||||||
"apps/web": {
|
"apps/web": {
|
||||||
"name": "@d3ro/web",
|
"name": "@d3ro/web",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@d3ro/api-client": "*",
|
"@d3ro/api-client": "*",
|
||||||
"@d3ro/core": "*",
|
"@d3ro/core": "*",
|
||||||
|
|
@ -16866,7 +16866,7 @@
|
||||||
},
|
},
|
||||||
"packages/api-client": {
|
"packages/api-client": {
|
||||||
"name": "@d3ro/api-client",
|
"name": "@d3ro/api-client",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@d3ro/core": "*",
|
"@d3ro/core": "*",
|
||||||
|
|
@ -16883,7 +16883,7 @@
|
||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@d3ro/core",
|
"name": "@d3ro/core",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"docx": "^9.6.1"
|
"docx": "^9.6.1"
|
||||||
|
|
@ -16894,7 +16894,7 @@
|
||||||
},
|
},
|
||||||
"packages/i18n": {
|
"packages/i18n": {
|
||||||
"name": "@d3ro/i18n",
|
"name": "@d3ro/i18n",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
|
|
@ -16905,7 +16905,7 @@
|
||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@d3ro/ui",
|
"name": "@d3ro/ui",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@d3ro/core": "*"
|
"@d3ro/core": "*"
|
||||||
|
|
@ -16925,7 +16925,7 @@
|
||||||
},
|
},
|
||||||
"packages/ui-native": {
|
"packages/ui-native": {
|
||||||
"name": "@d3ro/ui-native",
|
"name": "@d3ro/ui-native",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "d3ro-voice-monorepo",
|
"name": "d3ro-voice-monorepo",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "D3RO Voice — 멀티플랫폼 AI 음성 어시스턴트 (Monorepo)",
|
"description": "D3RO Voice — 멀티플랫폼 AI 음성 어시스턴트 (Monorepo)",
|
||||||
"author": "D3RO",
|
"author": "D3RO",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@d3ro/api-client",
|
"name": "@d3ro/api-client",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "D3RO Voice API 클라이언트 — Supabase 래퍼 (web/desktop/mobile 공유)",
|
"description": "D3RO Voice API 클라이언트 — Supabase 래퍼 (web/desktop/mobile 공유)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@d3ro/core",
|
"name": "@d3ro/core",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "D3RO Voice 공유 비즈니스 로직 — 타입, 에러, IPC 채널, 상수, 유틸",
|
"description": "D3RO Voice 공유 비즈니스 로직 — 타입, 에러, IPC 채널, 상수, 유틸",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@d3ro/i18n",
|
"name": "@d3ro/i18n",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "D3RO Voice 공유 i18n — 12개 locale, 타입 안전 키, Context, 포맷 유틸",
|
"description": "D3RO Voice 공유 i18n — 12개 locale, 타입 안전 키, Context, 포맷 유틸",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@d3ro/ui-native",
|
"name": "@d3ro/ui-native",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "D3RO Voice React Native DS — MetalCard/PhosphorText/Led/WaveBars etc.",
|
"description": "D3RO Voice React Native DS — MetalCard/PhosphorText/Led/WaveBars etc.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@d3ro/ui",
|
"name": "@d3ro/ui",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "D3RO Voice 공유 UI — 디자인 시스템 컴포넌트 + 테마 토큰 + CSS 변수 맵",
|
"description": "D3RO Voice 공유 UI — 디자인 시스템 컴포넌트 + 테마 토큰 + CSS 변수 맵",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"androidVersionCode": 1030001,
|
"androidVersionCode": 1031001,
|
||||||
"iosBuildNumber": 1030001,
|
"iosBuildNumber": 1031001,
|
||||||
"releaseDate": "2026-09-18",
|
"releaseDate": "2026-09-18",
|
||||||
"desktopLicensePublicKeyId": "5c52b765135ee2531c681b53cd4dc0e96fff8737f496c0b04ba8334fc81a887f"
|
"desktopLicensePublicKeyId": "5c52b765135ee2531c681b53cd4dc0e96fff8737f496c0b04ba8334fc81a887f"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@
|
||||||
// apps/desktop/release/<version>/portable.json (볼륨 인덱스: 이름/크기/sha256)
|
// apps/desktop/release/<version>/portable.json (볼륨 인덱스: 이름/크기/sha256)
|
||||||
// bucket/d3ro-voice.json (Scoop 매니페스트, 커밋 대상)
|
// bucket/d3ro-voice.json (Scoop 매니페스트, 커밋 대상)
|
||||||
//
|
//
|
||||||
|
// zip 분할 부품(수동 설치용)도 함께 만든다 — Windows 내장 Expand-Archive로 해제할 수 있어
|
||||||
|
// 사용자에게 7-Zip 설치를 요구하지 않는다. 7z 볼륨은 Scoop 전용으로 더 작다(162MiB vs 243MiB).
|
||||||
|
//
|
||||||
// 사용:
|
// 사용:
|
||||||
// npm run build --workspace=@d3ro/desktop
|
// npm run build --workspace=@d3ro/desktop
|
||||||
// node scripts/ci/build-portable.mjs # 7z 분할 볼륨
|
// node scripts/ci/build-portable.mjs # 7z 분할 볼륨
|
||||||
|
|
@ -18,7 +21,15 @@
|
||||||
|
|
||||||
import { spawnSync } from 'node:child_process'
|
import { spawnSync } from 'node:child_process'
|
||||||
import { createHash } from 'node:crypto'
|
import { createHash } from 'node:crypto'
|
||||||
import { existsSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
import {
|
||||||
|
existsSync,
|
||||||
|
readFileSync,
|
||||||
|
readdirSync,
|
||||||
|
renameSync,
|
||||||
|
rmSync,
|
||||||
|
statSync,
|
||||||
|
writeFileSync,
|
||||||
|
} from 'node:fs'
|
||||||
import { createRequire } from 'node:module'
|
import { createRequire } from 'node:module'
|
||||||
import { dirname, join } from 'node:path'
|
import { dirname, join } from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
@ -34,9 +45,12 @@ const FEED = 'https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice'
|
||||||
const PORTABLE_VERSION_PATH = `${FEED}/portable-${version}`
|
const PORTABLE_VERSION_PATH = `${FEED}/portable-${version}`
|
||||||
const PORTABLE_LATEST_PATH = `${FEED}/portable-latest`
|
const PORTABLE_LATEST_PATH = `${FEED}/portable-latest`
|
||||||
const ARCHIVE_BASE = `D3RO-Voice-${version}-x64-portable`
|
const ARCHIVE_BASE = `D3RO-Voice-${version}-x64-portable`
|
||||||
/** Cloudflare 본문 한도(100MiB)보다 여유를 둔 볼륨 크기 */
|
/** Cloudflare 본문 한도(100MiB)보다 여유를 둔 7z 볼륨 크기 (Scoop 경로) */
|
||||||
const VOLUME_SIZE = '95m'
|
const VOLUME_SIZE = '95m'
|
||||||
const MAX_VOLUME_BYTES = 95 * 1024 * 1024
|
const MAX_VOLUME_BYTES = 95 * 1024 * 1024
|
||||||
|
/** 수동 설치 스크립트용 zip 분할 부품 크기 */
|
||||||
|
const ZIP_PART_SIZE = '90m'
|
||||||
|
const MAX_ZIP_PART_BYTES = 95 * 1024 * 1024
|
||||||
|
|
||||||
const wantZip = process.argv.includes('--zip')
|
const wantZip = process.argv.includes('--zip')
|
||||||
const releaseDir = join(desktopDir, 'release', version)
|
const releaseDir = join(desktopDir, 'release', version)
|
||||||
|
|
@ -225,6 +239,73 @@ writeFileSync(
|
||||||
'utf8',
|
'utf8',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ── 수동 설치용 zip 분할 부품 ─────────────────────────────
|
||||||
|
const zipTarget = join(releaseDir, `${ARCHIVE_BASE}.zip`)
|
||||||
|
console.log(`[portable] 수동 설치용 zip 생성 (분할 ${ZIP_PART_SIZE})`)
|
||||||
|
const zipBuild = spawnSync(
|
||||||
|
'npx',
|
||||||
|
[
|
||||||
|
'electron-builder',
|
||||||
|
'--win',
|
||||||
|
'zip',
|
||||||
|
'--x64',
|
||||||
|
'--config',
|
||||||
|
'electron-builder.yml',
|
||||||
|
'--publish',
|
||||||
|
'never',
|
||||||
|
'-c.win.forceCodeSigning=false',
|
||||||
|
'-c.npmRebuild=false',
|
||||||
|
],
|
||||||
|
{ cwd: desktopDir, stdio: 'inherit', shell: process.platform === 'win32' },
|
||||||
|
)
|
||||||
|
if (zipBuild.status !== 0) {
|
||||||
|
console.error(`[portable] zip 빌드 실패 (exit ${zipBuild.status ?? 'null'})`)
|
||||||
|
process.exit(zipBuild.status ?? 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const producedZip = readdirSync(releaseDir).find((name) =>
|
||||||
|
name.endsWith('.zip') && name.includes(version) && !name.includes('.part'),
|
||||||
|
)
|
||||||
|
if (!producedZip) {
|
||||||
|
console.error('[portable] zip 산출물을 찾을 수 없습니다.')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
if (join(releaseDir, producedZip) !== zipTarget) {
|
||||||
|
renameSync(join(releaseDir, producedZip), zipTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
// zip을 90MiB 단위로 바이트 분할한다 (사용자가 이어 붙여 Expand-Archive로 해제)
|
||||||
|
const zipBytes = readFileSync(zipTarget)
|
||||||
|
const partSize = 90 * 1024 * 1024
|
||||||
|
const zipParts = []
|
||||||
|
for (let offset = 0, index = 1; offset < zipBytes.length; offset += partSize, index += 1) {
|
||||||
|
const slice = zipBytes.subarray(offset, Math.min(offset + partSize, zipBytes.length))
|
||||||
|
const name = `${ARCHIVE_BASE}.zip.${String(index).padStart(3, '0')}`
|
||||||
|
if (slice.length > MAX_ZIP_PART_BYTES) {
|
||||||
|
console.error(`[portable] zip 부품이 너무 큽니다: ${name}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
writeFileSync(join(releaseDir, name), slice)
|
||||||
|
zipParts.push({
|
||||||
|
name,
|
||||||
|
size: slice.length,
|
||||||
|
sha256: createHash('sha256').update(slice).digest('hex'),
|
||||||
|
url: `${PORTABLE_VERSION_PATH}/${name}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const zipSha256 = createHash('sha256').update(zipBytes).digest('hex')
|
||||||
|
|
||||||
|
// 인덱스에 zip 부품 정보를 추가한다 (설치 스크립트가 사용)
|
||||||
|
const indexJson = JSON.parse(readFileSync(join(releaseDir, 'portable.json'), 'utf8'))
|
||||||
|
indexJson.zipArchive = `${ARCHIVE_BASE}.zip`
|
||||||
|
indexJson.zipSize = zipBytes.length
|
||||||
|
indexJson.zipSha256 = zipSha256
|
||||||
|
indexJson.zipParts = zipParts
|
||||||
|
writeFileSync(
|
||||||
|
join(releaseDir, 'portable.json'),
|
||||||
|
`${JSON.stringify(indexJson, null, 2)}\n`,
|
||||||
|
'utf8',
|
||||||
|
)
|
||||||
console.log(
|
console.log(
|
||||||
[
|
[
|
||||||
'[portable] 완료',
|
'[portable] 완료',
|
||||||
|
|
@ -232,6 +313,7 @@ console.log(
|
||||||
...volumeEntries.map(
|
...volumeEntries.map(
|
||||||
(entry) => ` ${entry.name} (${(entry.size / 1048576).toFixed(1)}MiB)`,
|
(entry) => ` ${entry.name} (${(entry.size / 1048576).toFixed(1)}MiB)`,
|
||||||
),
|
),
|
||||||
|
` zip : ${zipParts.length}개 부품 / 합계 ${(zipBytes.length / 1048576).toFixed(1)}MiB`,
|
||||||
` 인덱스 : ${join(releaseDir, 'portable.json')}`,
|
` 인덱스 : ${join(releaseDir, 'portable.json')}`,
|
||||||
` scoop : ${join(root, 'bucket', 'd3ro-voice.json')}`,
|
` scoop : ${join(root, 'bucket', 'd3ro-voice.json')}`,
|
||||||
` 게시 : node scripts/ci/publish-portable-release.mjs`,
|
` 게시 : node scripts/ci/publish-portable-release.mjs`,
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,21 @@ for (const volume of index.volumes) {
|
||||||
payloads.push({ name: volume.name, bytes, contentType: 'application/octet-stream' })
|
payloads.push({ name: volume.name, bytes, contentType: 'application/octet-stream' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 수동 설치용 zip 분할 부품 (Windows 내장 Expand-Archive로 해제 — 7-Zip 불필요)
|
||||||
|
for (const part of index.zipParts ?? []) {
|
||||||
|
const partPath = join(releaseDir, part.name)
|
||||||
|
if (!existsSync(partPath)) {
|
||||||
|
console.error(`[portable] zip 부품이 없습니다: ${partPath}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
const partBytes = await readFile(partPath)
|
||||||
|
const partSha = createHash('sha256').update(partBytes).digest('hex')
|
||||||
|
if (partSha !== part.sha256) {
|
||||||
|
console.error(`[portable] zip 부품 sha256 불일치 (${part.name})`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
payloads.push({ name: part.name, bytes: partBytes, contentType: 'application/octet-stream' })
|
||||||
|
}
|
||||||
payloads.push({
|
payloads.push({
|
||||||
name: 'portable.json',
|
name: 'portable.json',
|
||||||
bytes: Buffer.from(`${JSON.stringify(index, null, 2)}\n`, 'utf8'),
|
bytes: Buffer.from(`${JSON.stringify(index, null, 2)}\n`, 'utf8'),
|
||||||
|
|
@ -145,7 +160,7 @@ console.log(
|
||||||
' scoop bucket add d3ro https://git.chanpaca.net/yunchan/d3ro-voice.git',
|
' scoop bucket add d3ro https://git.chanpaca.net/yunchan/d3ro-voice.git',
|
||||||
' scoop install d3ro/d3ro-voice',
|
' scoop install d3ro/d3ro-voice',
|
||||||
'',
|
'',
|
||||||
' 수동 설치(7-Zip 필요):',
|
' 수동 설치(추가 도구 불필요):',
|
||||||
` irm ${FEED}/portable-latest/install-d3ro-voice.ps1 | iex`,
|
` irm ${FEED}/portable-latest/install-d3ro-voice.ps1 | iex`,
|
||||||
'',
|
'',
|
||||||
' 참고: 이 채널은 서명이 없어 자동 업데이트 피드를 갱신하지 않습니다.',
|
' 참고: 이 채널은 서명이 없어 자동 업데이트 피드를 갱신하지 않습니다.',
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,24 @@
|
||||||
# scripts/local/install-d3ro-voice.ps1
|
# scripts/install/install-d3ro-voice.ps1
|
||||||
# 서명 없이 D3RO Voice를 설치하는 수동 설치 스크립트.
|
# 서명 없이 D3RO Voice를 설치하는 수동 설치 스크립트.
|
||||||
#
|
#
|
||||||
# 왜 스크립트인가: canonical feed는 Cloudflare 뒤에 있어 업로드 본문이 100MiB를 넘으면
|
# 왜 스크립트인가: canonical feed는 Cloudflare 뒤에 있어 업로드 본문이 100MiB를 넘으면
|
||||||
# 거부된다. 사이드카(faster-whisper)를 포함한 앱은 95MiB 단위 7z 볼으로 나뉘어 있고,
|
# 거부된다. 사이드카(faster-whisper)를 포함한 앱은 그보다 크므로 zip을 90MiB 단위로
|
||||||
# 이 스크립트가 볼륨을 이어 붙여 해제한다. Scoop을 쓰면 Scoop이 같은 일을 자동으로 한다.
|
# 나누어 게시하고, 이 스크립트가 부품을 이어 붙여 설치한다. Windows 내장
|
||||||
|
# Expand-Archive만 사용하므로 7-Zip 같은 추가 도구가 필요 없고 관리자 권한도 필요 없다.
|
||||||
#
|
#
|
||||||
# 사용:
|
# 사용:
|
||||||
# irm https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-latest/install-d3ro-voice.ps1 | iex
|
# irm https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-latest/install-d3ro-voice.ps1 | iex
|
||||||
# 또는 저장 후:
|
# 또는 저장 후:
|
||||||
# powershell -ExecutionPolicy Bypass -File install-d3ro-voice.ps1
|
# powershell -ExecutionPolicy Bypass -File install-d3ro-voice.ps1
|
||||||
#
|
#
|
||||||
# 요구 사항: Windows 10/11 x64, 7-Zip(없으면 Scoop 사용을 권장).
|
# Scoop을 쓸 수 있으면 그쪽이 더 작고(7z 162MiB) 업데이트도 자동이다:
|
||||||
# 관리자 권한 불필요 — %LOCALAPPDATA%\Programs 아래에 설치한다.
|
# scoop bucket add d3ro https://git.chanpaca.net/yunchan/d3ro-voice.git
|
||||||
|
# scoop install d3ro/d3ro-voice
|
||||||
|
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[string]$FeedBase = 'https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-latest',
|
[string]$FeedBase = 'https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-latest',
|
||||||
[string]$InstallDir = (Join-Path $env:LOCALAPPDATA 'Programs\D3RO Voice'),
|
[string]$InstallDir = (Join-Path $env:LOCALAPPDATA 'Programs\D3RO Voice'),
|
||||||
[string]$SevenZipPath = '',
|
|
||||||
[switch]$Force
|
[switch]$Force
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -30,98 +31,81 @@ function Get-Sha256($path) {
|
||||||
$sha = [System.Security.Cryptography.SHA256]::Create()
|
$sha = [System.Security.Cryptography.SHA256]::Create()
|
||||||
try {
|
try {
|
||||||
$stream = [System.IO.File]::OpenRead($path)
|
$stream = [System.IO.File]::OpenRead($path)
|
||||||
try {
|
try { $bytes = $sha.ComputeHash($stream) } finally { $stream.Dispose() }
|
||||||
$bytes = $sha.ComputeHash($stream)
|
|
||||||
} finally { $stream.Dispose() }
|
|
||||||
} finally { $sha.Dispose() }
|
} finally { $sha.Dispose() }
|
||||||
return ($bytes | ForEach-Object { $_.ToString("x2") }) -join ''
|
return ($bytes | ForEach-Object { $_.ToString('x2') }) -join ''
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Step 'D3RO Voice 휴대용 배포본 설치를 시작합니다 (서명되지 않은 빌드).'
|
Write-Step 'D3RO Voice 설치를 시작합니다 (서명되지 않은 빌드).'
|
||||||
|
|
||||||
# 1. 인덱스 내려받기
|
# 1. 인덱스 내려받기
|
||||||
$indexUrl = "$FeedBase/portable.json"
|
$indexUrl = "$FeedBase/portable.json"
|
||||||
Write-Step "인덱스: $indexUrl"
|
Write-Step "인덱스: $indexUrl"
|
||||||
$index = Invoke-RestMethod -Uri $indexUrl -UseBasicParsing
|
$index = Invoke-RestMethod -Uri $indexUrl -UseBasicParsing
|
||||||
$version = $index.version
|
$version = $index.version
|
||||||
Write-Step "버전 $version, 볼륨 $($index.volumeCount)개 (합계 $([math]::Round($index.totalSize / 1MB, 1)) MB)"
|
|
||||||
|
|
||||||
# 2. 임시 디렉터리에 볼 내려받기 + 해시 검증
|
if (-not $index.zipParts -or $index.zipParts.Count -eq 0) {
|
||||||
|
throw '인덱스에 zip 부품 정보가 없습니다. 이 스크립트는 zipParts가 있는 버전(1.3.0+)을 지원합니다.'
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Step "버전 $version, 부품 $($index.zipParts.Count)개 (합계 $([math]::Round($index.zipSize / 1MB, 1)) MB)"
|
||||||
|
|
||||||
|
# 2. 임시 디렉터리에 부품 내려받기 + 해시 검증
|
||||||
$tempRoot = [System.IO.Path]::GetTempPath()
|
$tempRoot = [System.IO.Path]::GetTempPath()
|
||||||
if ($env:TEMP) { $tempRoot = $env:TEMP }
|
if ($env:TEMP) { $tempRoot = $env:TEMP }
|
||||||
elseif ($env:TMP) { $tempRoot = $env:TMP }
|
elseif ($env:TMP) { $tempRoot = $env:TMP }
|
||||||
|
|
||||||
$workDir = Join-Path $tempRoot "d3ro-voice-$version-portable"
|
$workDir = Join-Path $tempRoot "d3ro-voice-$version-portable"
|
||||||
if (Test-Path $workDir) { Remove-Item -Recurse -Force $workDir }
|
if (Test-Path $workDir) { Remove-Item -Recurse -Force $workDir }
|
||||||
New-Item -ItemType Directory -Path $workDir | Out-Null
|
New-Item -ItemType Directory -Path $workDir | Out-Null
|
||||||
|
|
||||||
foreach ($volume in $index.volumes) {
|
foreach ($part in $index.zipParts) {
|
||||||
$dest = Join-Path $workDir $volume.name
|
$dest = Join-Path $workDir $part.name
|
||||||
$url = "$FeedBase/$($volume.name)"
|
Write-Step "내려받기: $($part.name) ($([math]::Round($part.size / 1MB, 1)) MB)"
|
||||||
Write-Step "내려받기: $($volume.name) ($([math]::Round($volume.size / 1MB, 1)) MB)"
|
Invoke-WebRequest -Uri "$FeedBase/$($part.name)" -OutFile $dest -UseBasicParsing
|
||||||
Invoke-WebRequest -Uri $url -OutFile $dest -UseBasicParsing
|
|
||||||
|
|
||||||
$hash = Get-Sha256 $dest
|
$hash = Get-Sha256 $dest
|
||||||
if ($hash -ne $volume.sha256) {
|
if ($hash -ne $part.sha256) {
|
||||||
throw "해시가 일치하지 않습니다: $($volume.name)`n 기대: $($volume.sha256)`n 실제: $hash"
|
throw "해시가 일치하지 않습니다: $($part.name)`n 기대: $($part.sha256)`n 실제: $hash"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Write-Step '모든 볼륨의 SHA-256 검증 완료'
|
Write-Step '모든 부품의 SHA-256 검증 완료'
|
||||||
|
|
||||||
# 3. 볼륨 이어 붙이기
|
# 3. 부품 이어 붙이기
|
||||||
$archive = Join-Path $workDir "$($index.archive)"
|
$archive = Join-Path $workDir $index.zipArchive
|
||||||
$stream = [System.IO.File]::Create($archive)
|
$stream = [System.IO.File]::Create($archive)
|
||||||
try {
|
try {
|
||||||
foreach ($volume in $index.volumes) {
|
foreach ($part in $index.zipParts) {
|
||||||
$part = [System.IO.File]::OpenRead((Join-Path $workDir $volume.name))
|
$piece = [System.IO.File]::OpenRead((Join-Path $workDir $part.name))
|
||||||
try { $part.CopyTo($stream) } finally { $part.Dispose() }
|
try { $piece.CopyTo($stream) } finally { $piece.Dispose() }
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
$stream.Dispose()
|
$stream.Dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$archiveHash = Get-Sha256 $archive
|
||||||
|
if ($index.zipSha256 -and $archiveHash -ne $index.zipSha256) {
|
||||||
|
throw "결합한 아카이브의 해시가 인덱스와 다릅니다.`n 기대: $($index.zipSha256)`n 실제: $archiveHash"
|
||||||
|
}
|
||||||
Write-Step "아카이브 결합 완료: $([math]::Round((Get-Item $archive).Length / 1MB, 1)) MB"
|
Write-Step "아카이브 결합 완료: $([math]::Round((Get-Item $archive).Length / 1MB, 1)) MB"
|
||||||
|
|
||||||
# 4. 해제 (7-Zip 필요; 없으면 안내)
|
# 4. 압축 해제 (Windows 내장 Expand-Archive — 추가 도구 불필요)
|
||||||
$sevenZipCandidates = @()
|
|
||||||
foreach ($base in @($env:ProgramFiles, ${env:ProgramFiles(x86)})) {
|
|
||||||
if ($base) { $sevenZipCandidates += (Join-Path $base '7-Zip\7z.exe') }
|
|
||||||
}
|
|
||||||
$sevenZip = $sevenZipCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
|
|
||||||
|
|
||||||
if ($SevenZipPath) { $sevenZip = $SevenZipPath }
|
|
||||||
|
|
||||||
if (-not $sevenZip) {
|
|
||||||
# Windows PowerShell 5.1 호환 (?. 연산자는 PowerShell 7 전용)
|
|
||||||
$sevenZipCommand = Get-Command 7z -ErrorAction SilentlyContinue
|
|
||||||
if ($sevenZipCommand) { $sevenZip = $sevenZipCommand.Source }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $sevenZip) {
|
|
||||||
throw @'
|
|
||||||
7-Zip을 찾을 수 없습니다. 두 가지 방법이 있습니다.
|
|
||||||
1) Scoop 사용(권장, 7-Zip 자동 준비):
|
|
||||||
scoop bucket add d3ro https://git.chanpaca.net/yunchan/d3ro-voice.git
|
|
||||||
scoop install d3ro/d3ro-voice
|
|
||||||
2) 7-Zip 설치 후 이 스크립트를 다시 실행: https://www.7-zip.org/
|
|
||||||
'@
|
|
||||||
}
|
|
||||||
|
|
||||||
$extractDir = Join-Path $workDir 'extract'
|
$extractDir = Join-Path $workDir 'extract'
|
||||||
|
Write-Step '압축 해제 중 (수백 MB, 시간이 걸릴 수 있습니다)'
|
||||||
|
Expand-Archive -LiteralPath $archive -DestinationPath $extractDir -Force
|
||||||
|
|
||||||
|
# 5. 설치 디렉터리로 배치
|
||||||
if (Test-Path $InstallDir) {
|
if (Test-Path $InstallDir) {
|
||||||
if (-not $Force) {
|
if (-not $Force) {
|
||||||
throw "설치 경로가 이미 있습니다: $InstallDir`n 다시 설치하려면 -Force 붙이세요."
|
throw "설치 경로가 이미 있습니다: $InstallDir`n 다시 설치하려면 -Force 를 붙이세요."
|
||||||
}
|
}
|
||||||
Write-Step "기존 설치를 교체합니다: $InstallDir"
|
Write-Step "기존 설치를 교체합니다: $InstallDir"
|
||||||
Remove-Item -Recurse -Force $InstallDir
|
Remove-Item -Recurse -Force $InstallDir
|
||||||
}
|
}
|
||||||
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
|
||||||
|
|
||||||
Write-Step '압축 해제 중 (수백 MB, 시간이 걸릴 수 있습니다)'
|
|
||||||
& $sevenZip x $archive "-o$extractDir" -y | Out-Null
|
|
||||||
if ($LASTEXITCODE -ne 0) { throw "압축 해제 실패 (7-Zip exit $LASTEXITCODE)" }
|
|
||||||
|
|
||||||
Copy-Item -Path (Join-Path $extractDir '*') -Destination $InstallDir -Recurse -Force
|
Copy-Item -Path (Join-Path $extractDir '*') -Destination $InstallDir -Recurse -Force
|
||||||
|
|
||||||
# 5. 시작 메뉴 바로가기
|
# 6. 시작 메뉴 바로가기
|
||||||
$exe = Join-Path $InstallDir 'D3RO Voice.exe'
|
$exe = Join-Path $InstallDir 'D3RO Voice.exe'
|
||||||
if (-not (Test-Path $exe)) { throw "실행 파일을 찾을 수 없습니다: $exe" }
|
if (-not (Test-Path $exe)) { throw "실행 파일을 찾을 수 없습니다: $exe" }
|
||||||
|
|
||||||
|
|
@ -139,8 +123,9 @@ Write-Step "설치 완료: $InstallDir"
|
||||||
Write-Step "시작 메뉴 바로가기: $shortcutPath"
|
Write-Step "시작 메뉴 바로가기: $shortcutPath"
|
||||||
Write-Host ''
|
Write-Host ''
|
||||||
Write-Host '참고:' -ForegroundColor Yellow
|
Write-Host '참고:' -ForegroundColor Yellow
|
||||||
Write-Host ' - 이 빌드는 Authenticode 서명이 없어 첫 실행 시 SmartScreen 경고가 뜰 수 있습니다.'
|
Write-Host ' - 이 빌드는 Authenticode 서명이 없어 SmartScreen 경고가 뜰 수 있습니다("추가 정보 -> 실행").'
|
||||||
Write-Host ' - 자동 업데이트는 서명된 릴리스가 게시된 뒤부터 동작합니다(현재 설치본은 그 피드를 봅니다).'
|
Write-Host ' - 자동 업데이트는 서명된 릴리스가 게시된 뒤부터 동작합니다(현재 설치본은 그 피드를 봅니다).'
|
||||||
Write-Host ' - 설정/모델/기록은 %APPDATA%\d3ro-voice 를 공유하므로 기존 설치와 동일하게 유지됩니다.'
|
Write-Host ' - 설정/모델/기록은 %APPDATA%\d3ro-voice 를 공유하므로 기존 설치와 동일하게 유지됩니다.'
|
||||||
|
Write-Host ' - Scoop 사용자는 scoop update d3ro-voice 로 갱신할 수 있습니다(7z 162MiB로 더 작음).'
|
||||||
Write-Host ''
|
Write-Host ''
|
||||||
Write-Host "실행: `"$exe`"" -ForegroundColor Green
|
Write-Host "실행: `"$exe`"" -ForegroundColor Green
|
||||||
4
site/package-lock.json
generated
4
site/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "d3ro-voice-site",
|
"name": "d3ro-voice-site",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "d3ro-voice-site",
|
"name": "d3ro-voice-site",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "d3ro-voice-site",
|
"name": "d3ro-voice-site",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --port 5199 --host",
|
"dev": "vite --port 5199 --host",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue