fix: harden compression boundaries and target search

This commit is contained in:
237899745
2026-07-25 12:23:35 +08:00
parent 0ff9eae56d
commit 61fa9cb820
9 changed files with 787 additions and 169 deletions

View File

@@ -19,9 +19,10 @@
<ul class="mt-2 list-disc space-y-1 pl-5">
<li>Base URL<code>https://ys.workyai.cn/api/v1</code></li>
<li>认证方式<code>X-API-Key</code>推荐 <code>Authorization: Bearer &lt;token&gt;</code></li>
<li>支持格式PNG / JPG / JPEG / WebP / AVIF / GIF静态/ BMP / TIFF / ICO支持 output_format 转码</li>
<li>支持格式PNG / JPG / JPEG / WebP / AVIF / GIF / BMP / TIFF / ICO仅静态图片支持 output_format 转码</li>
<li>压缩率<code>compression_rate</code> 1-100JPEG/WebP/AVIF 以该比例为体积上限无损格式为尽力优化</li>
<li>计量成功压缩 1 个文件计 1 若体积未变小或压缩率为 100则不扣额度</li>
<li>目标体积<code>target_size_bytes</code> 仅支持 JPEG/WebP/AVIF且不能与 <code>compression_rate</code> 同时提交</li>
<li>计量输出体积变小时计 1 仅同格式无缩放的 <code>compression_rate=100</code> 原样请求免计量</li>
</ul>
</div>
@@ -40,6 +41,7 @@
https://ys.workyai.cn/api/v1/compress</code></pre>
<div class="mt-3 text-xs text-slate-500">
返回字段包含 <code>download_url</code><code>saved_percent</code> <code>billing.units_charged</code>
目标小于清晰度保护边界时返回 <code>INVALID_REQUEST</code>不会返回超过目标的文件
</div>
</div>
@@ -95,6 +97,7 @@ curl -H "X-API-Key: if_live_xxx" \
<div class="font-medium text-slate-900">错误处理与常见问题</div>
<ul class="mt-2 list-disc space-y-1 pl-5">
<li><code>INVALID_REQUEST</code>参数或文件不合法检查格式与字段</li>
<li><code>UNSUPPORTED_FORMAT</code>不支持该格式或输入为动画图片</li>
<li><code>QUOTA_EXCEEDED</code>额度不足升级套餐或等待周期重置</li>
<li><code>FORBIDDEN</code>无权限下载该任务/文件认证不一致</li>
<li><code>EMAIL_NOT_VERIFIED</code>登录用户需先完成邮箱验证</li>

View File

@@ -3,7 +3,14 @@ import { computed, onMounted, reactive, ref } from 'vue'
import { zipSync } from 'fflate'
import { useAuthStore } from '@/stores/auth'
import { compressFile, getSubscription, getUsage, sendVerification, type CompressResponse } from '@/services/api'
import {
compressFile,
getSubscription,
getUsage,
sendVerification,
type CompressResponse,
type OutputFormat,
} from '@/services/api'
import { ApiError } from '@/services/http'
import { formatBytes } from '@/utils/format'
@@ -22,6 +29,7 @@ const auth = useAuthStore()
type CompressionMode = 'percent' | 'size'
type OutputFormatOption = 'auto' | 'png' | 'jpeg' | 'webp' | 'avif' | 'gif' | 'bmp' | 'tiff' | 'ico'
const targetSizeFormats = new Set<OutputFormat>(['jpeg', 'webp', 'avif'])
const options = reactive({
mode: 'percent' as CompressionMode, // 压缩模式:百分比 / 目标大小
@@ -121,6 +129,17 @@ function getTargetSizeBytes(): number | undefined {
return Math.round(bytes)
}
function setCompressionMode(mode: CompressionMode) {
options.mode = mode
if (
mode === 'size'
&& options.outputFormat !== 'auto'
&& !targetSizeFormats.has(options.outputFormat)
) {
options.outputFormat = 'webp'
}
}
async function runOne(item: UploadItem) {
item.status = 'compressing'
item.error = undefined
@@ -133,7 +152,15 @@ async function runOne(item: UploadItem) {
return
}
const outputFormat = options.outputFormat === 'auto' ? undefined : options.outputFormat
const outputFormat: OutputFormat | undefined = options.outputFormat === 'auto'
? (options.mode === 'size' ? 'webp' : undefined)
: options.outputFormat
if (options.mode === 'size' && outputFormat && !targetSizeFormats.has(outputFormat)) {
item.status = 'error'
item.error = '按目标大小仅支持 JPEG、WebP 或 AVIF 输出'
return
}
const compressOptions = options.mode === 'percent'
? {
@@ -347,7 +374,7 @@ async function resendVerification() {
<div class="rounded-xl border border-slate-200 bg-white p-5">
<div class="flex items-center justify-between">
<div class="text-sm font-medium text-slate-900">上传图片</div>
<div class="text-xs text-slate-500">支持 PNG / JPG / JPEG / WebP / AVIF / GIF / BMP / TIFF / ICOGIF 仅静态</div>
<div class="text-xs text-slate-500">支持 PNG / JPG / JPEG / WebP / AVIF / GIF / BMP / TIFF / ICO仅静态图片</div>
</div>
<div class="mt-4 flex flex-col gap-3">
@@ -503,7 +530,7 @@ async function resendVerification() {
type="button"
class="flex-1 rounded-md px-3 py-1.5 text-xs font-medium transition"
:class="options.mode === 'percent' ? 'bg-indigo-600 text-white' : 'text-slate-600 hover:bg-slate-100'"
@click="options.mode = 'percent'"
@click="setCompressionMode('percent')"
>
按百分比
</button>
@@ -511,7 +538,7 @@ async function resendVerification() {
type="button"
class="flex-1 rounded-md px-3 py-1.5 text-xs font-medium transition"
:class="options.mode === 'size' ? 'bg-indigo-600 text-white' : 'text-slate-600 hover:bg-slate-100'"
@click="options.mode = 'size'"
@click="setCompressionMode('size')"
>
按目标大小
</button>
@@ -556,7 +583,9 @@ async function resendVerification() {
<option value="MB">MB</option>
</select>
</div>
<div class="text-xs text-slate-500">直接指定压缩后的目标大小系统会自动调整质量以逼近目标 JPEG/WebP/AVIF 支持</div>
<div class="text-xs text-slate-500">
仅支持 JPEG/WebP/AVIF保持原格式时会自动输出 WebP过小且无法保证清晰度的目标会被拒绝
</div>
</div>
<label class="space-y-1">
@@ -565,15 +594,15 @@ async function resendVerification() {
v-model="options.outputFormat"
class="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm text-slate-800"
>
<option value="auto">保持原格式推荐</option>
<option value="auto">{{ options.mode === 'size' ? '自动选择 WebP推荐' : '保持原格式(推荐)' }}</option>
<option value="jpeg">JPEG</option>
<option value="png">PNG</option>
<option value="png" :disabled="options.mode === 'size'">PNG</option>
<option value="webp">WebP</option>
<option value="avif">AVIF</option>
<option value="gif">GIF仅静态</option>
<option value="bmp">BMP</option>
<option value="tiff">TIFF</option>
<option value="ico">ICO</option>
<option value="gif" :disabled="options.mode === 'size'">GIF仅静态</option>
<option value="bmp" :disabled="options.mode === 'size'">BMP</option>
<option value="tiff" :disabled="options.mode === 'size'">TIFF</option>
<option value="ico" :disabled="options.mode === 'size'">ICO</option>
</select>
<div class="text-xs text-slate-500">支持按需转码目标大小模式建议配合 JPEG/WebP/AVIF</div>
</label>