diff --git a/.gitea/workflows/publish-dlp-io.yaml b/.gitea/workflows/publish-dlp-io.yaml
index 44329bd..ed59957 100644
--- a/.gitea/workflows/publish-dlp-io.yaml
+++ b/.gitea/workflows/publish-dlp-io.yaml
@@ -341,3 +341,51 @@ jobs:
throw "release asset mismatch: $($publishedNames -join ', ')"
}
Write-Host "release verified: $tag ($($localNames.Count) files)"
+
+ - name: Publish packages to Gitea PyPI
+ shell: pwsh
+ env:
+ TWINE_USERNAME: antior
+ TWINE_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
+ TWINE_REPOSITORY_URL: ${{ gitea.server_url }}/api/packages/antior/pypi
+ run: |
+ $ErrorActionPreference = "Stop"
+ if ([string]::IsNullOrWhiteSpace($env:TWINE_PASSWORD)) {
+ throw "GITHUB_TOKEN is required to publish packages"
+ }
+ $headers = @{ Authorization = "token $env:TWINE_PASSWORD" }
+ $simpleUrl = "$env:TWINE_REPOSITORY_URL/simple/dlp-io/"
+
+ function Get-RegistryFileNames {
+ try {
+ $response = Invoke-WebRequest -Headers $headers -Uri $simpleUrl
+ } catch {
+ if ([int]$_.Exception.Response.StatusCode -eq 404) { return @() }
+ throw
+ }
+ return @([regex]::Matches($response.Content, ']*>(?[^<]+)', "IgnoreCase") |
+ ForEach-Object { [Net.WebUtility]::HtmlDecode($_.Groups["name"].Value) })
+ }
+
+ $published = @(Get-RegistryFileNames)
+ $missing = @(Get-ChildItem -LiteralPath dist -File | Where-Object { $published -notcontains $_.Name })
+ if ($missing.Count -gt 0) {
+ $uploadArgs = @("-3.13", "-m", "twine", "upload", "--non-interactive", "--disable-progress-bar") +
+ @($missing | ForEach-Object FullName)
+ & py @uploadArgs
+ if ($LASTEXITCODE -ne 0) { throw "twine upload failed" }
+ } else {
+ Write-Host "all registry artifacts already published"
+ }
+
+ $expected = @(Get-ChildItem -LiteralPath dist -File | ForEach-Object Name)
+ for ($attempt = 1; $attempt -le 20; $attempt++) {
+ $published = @(Get-RegistryFileNames)
+ $unpublished = @($expected | Where-Object { $published -notcontains $_ })
+ if ($unpublished.Count -eq 0) { break }
+ if ($attempt -lt 20) { Start-Sleep -Seconds 3 }
+ }
+ if ($unpublished.Count -ne 0) {
+ throw "registry package files missing after upload: $($unpublished -join ', ')"
+ }
+ Write-Host "registry verified: dlp-io==$($env:DLP_IO_PACKAGE_VERSION) ($($expected.Count) files)"
diff --git a/README.md b/README.md
index 89c1582..c4e6801 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,8 @@ pytest
- `dlp_io-X.Y.Z.tar.gz`(sdist)
- `dlp_io.py` 和 5 个裸 `dlp_io.cp3XX-win_amd64.pyd`(免安装单文件,放进项目目录即可 import)
+同一批 wheel/sdist 还会同步发布到 Gitea PyPI registry(`https://gitea.docker.antior.cn/api/packages/antior/pypi/simple`),可直接 `pip install --index-url ... dlp-io`。
+
安装方式见 [docs/DLP_IO_LIBRARY.md](docs/DLP_IO_LIBRARY.md)。
本地只构建纯 Python 版:
diff --git a/dlp_io.py b/dlp_io.py
index b61c9aa..d33aa4c 100644
--- a/dlp_io.py
+++ b/dlp_io.py
@@ -17,7 +17,7 @@ from enum import Enum
from multiprocessing.context import AuthenticationError
from multiprocessing.connection import Client
-__version__ = "0.1.0"
+__version__ = "0.1.1"
__all__ = [
"DlpConfig",
diff --git a/docs/DLP_IO_LIBRARY.md b/docs/DLP_IO_LIBRARY.md
index 7118094..ed8ff85 100644
--- a/docs/DLP_IO_LIBRARY.md
+++ b/docs/DLP_IO_LIBRARY.md
@@ -2,11 +2,23 @@
`dlp-io` 为 Windows DLP 透明加密环境提供接近 `io.open` 的 Python 文件 API。非白名单进程负责业务逻辑和写入;文件读取由已获 DLP 白名单授权的 Python helper 完成,再通过经过认证的 Windows Named Pipe 返回明文字节。
-当前版本为 `dlp-io==0.1.0`,distribution 名是 `dlp-io`,import 名是 `dlp_io`。
+当前版本为 `dlp-io==0.1.1`,distribution 名是 `dlp-io`,import 名是 `dlp_io`。
## 安装
-发布物托管在 Gitea Release 页面,由 `.gitea/workflows/publish-dlp-io.yaml` 从 immutable tag `dlp-io-vX.Y.Z` 自动构建并上传,全部产物在 CI runner 上编译,不依赖本地环境。
+### 方式一:Gitea PyPI registry(推荐)
+
+registry 已开放匿名下载,一条命令即可安装;pip 会自动选择最匹配的 wheel:Windows 上 CPython 3.10–3.14 会得到对应的 pyd wheel(整个库编译为单个 `.pyd`),其余环境得到纯 Python wheel。
+
+```powershell
+py -m pip install --index-url https://gitea.docker.antior.cn/api/packages/antior/pypi/simple dlp-io==0.1.1
+```
+
+Package 页面:
+
+### 方式二:Gitea Release 页面
+
+发布物由 `.gitea/workflows/publish-dlp-io.yaml` 从 immutable tag `dlp-io-vX.Y.Z` 自动构建并上传,全部产物在 CI runner 上编译,不依赖本地环境。
Release 页面:
@@ -21,13 +33,13 @@ Release 页面:
```powershell
# 纯 Python 版(任意平台)
-py -m pip install https://gitea.docker.antior.cn/antior/dlp-io/releases/download/dlp-io-v0.1.0/dlp_io-0.1.0-py3-none-any.whl
+py -m pip install https://gitea.docker.antior.cn/antior/dlp-io/releases/download/dlp-io-v0.1.1/dlp_io-0.1.1-py3-none-any.whl
# pyd 版(示例为 CPython 3.13,其它版本替换文件名中的 cp313)
-py -3.13 -m pip install https://gitea.docker.antior.cn/antior/dlp-io/releases/download/dlp-io-v0.1.0/dlp_io-0.1.0-cp313-cp313-win_amd64.whl
+py -3.13 -m pip install https://gitea.docker.antior.cn/antior/dlp-io/releases/download/dlp-io-v0.1.1/dlp_io-0.1.1-cp313-cp313-win_amd64.whl
```
-也可以先从 Release 页面下载 wheel,再 `py -m pip install <文件路径>`。下载 Release 资产不需要 token;发布只能在 CI 中通过注入的 `GITHUB_TOKEN` 完成,发布凭据不得写入命令行、URL 或 tracked 文件。
+也可以先从 Release 页面下载 wheel,再 `py -m pip install <文件路径>`。下载 Release 资产和 registry 包都不需要 token;发布只能在 CI 中通过注入的 `GITHUB_TOKEN` 完成,发布凭据不得写入命令行、URL 或 tracked 文件。
调用方需要安装 `dlp-io`。白名单 Python 无需安装该包:helper 源码由调用方以 UTF-8 通过 stdin 注入,且只依赖 Python 标准库,不会在磁盘创建临时 `.py` 文件。
diff --git a/tests/test_dlp_io_packaging.py b/tests/test_dlp_io_packaging.py
index e92df39..4151727 100644
--- a/tests/test_dlp_io_packaging.py
+++ b/tests/test_dlp_io_packaging.py
@@ -46,6 +46,7 @@ class WorkflowStep(Enum):
BUILD = "Build and check distribution"
SMOKE = "Smoke test release artifacts"
RELEASE = "Create Gitea release with wheels"
+ PYPI = "Publish packages to Gitea PyPI"
@dataclass(frozen=True)