topfans/frontend/utils/laser-card/segmentationCloud.js

321 lines
9.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
ALIYUN_ACCESS_KEY_ID,
ALIYUN_ACCESS_KEY_SECRET,
ALIYUN_SECURITY_TOKEN,
ALIYUN_STS_URL,
IMM_REGION,
OSS_BUCKET,
OSS_KEY_PREFIX,
OSS_REGION,
SEGMENT_API_BASE,
SEGMENT_API_TOKEN,
SEGMENT_TRANSPORT
} from './segmentApi.js'
import {
ossPostObjectFromLocalFilePath,
ossPresignedObjectUrl,
rpcIvpdSegmentImage,
looksLikeBareOssEndpointUrl
} from './aliyunPortraitUni.js'
function uniRequest(options) {
return new Promise((resolve, reject) => {
uni.request({
...options,
success: (r) => {
if (r.statusCode >= 200 && r.statusCode < 300) {
resolve(r)
} else {
const msg =
(typeof r.data === 'object' && r.data && (r.data.Message || r.data.message)) ||
(typeof r.data === 'string' ? r.data : '') ||
`HTTP ${r.statusCode}`
reject(new Error(msg))
}
},
fail: (e) => reject(new Error(e.errMsg || '网络请求失败'))
})
})
}
/** 内层 Promise 长期无回调时强制失败(避免一直「正在识别」) */
function raceWithTimeout(promise, ms, errMsg) {
return new Promise((resolve, reject) => {
const t = setTimeout(() => reject(new Error(errMsg)), ms)
Promise.resolve(promise).then(
(v) => {
clearTimeout(t)
resolve(v)
},
(e) => {
clearTimeout(t)
reject(e)
}
)
})
}
function normalizeStsPayload(raw) {
if (!raw || typeof raw !== 'object') {
return null
}
if (raw.Credentials) {
const c = raw.Credentials
return {
accessKeyId: c.AccessKeyId,
accessKeySecret: c.AccessKeySecret,
securityToken: c.SecurityToken || undefined,
expiration: c.Expiration
}
}
if (raw.accessKeyId && raw.accessKeySecret) {
return {
accessKeyId: raw.accessKeyId,
accessKeySecret: raw.accessKeySecret,
securityToken: raw.securityToken || undefined,
expiration: raw.expiration
}
}
return null
}
async function fetchStsCredentials() {
const url = (ALIYUN_STS_URL || '').trim()
if (!url) {
return null
}
const res = await uniRequest({ url, method: 'GET', timeout: 30000 })
const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
const cred = normalizeStsPayload(data)
if (!cred || !cred.accessKeyId) {
throw new Error('STS 地址返回格式不正确(需 Credentials 或 accessKeyId/accessKeySecret')
}
return cred
}
/** 开发直配 AK或 STS 拉取 */
function getCredentialsFromConfig() {
const id = (ALIYUN_ACCESS_KEY_ID || '').trim()
const sec = (ALIYUN_ACCESS_KEY_SECRET || '').trim()
const token = (ALIYUN_SECURITY_TOKEN || '').trim()
if (id && sec) {
return {
accessKeyId: id,
accessKeySecret: sec,
securityToken: token || undefined
}
}
return null
}
async function resolveCredentials() {
const fromFile = getCredentialsFromConfig()
if (fromFile) {
return fromFile
}
const url = (ALIYUN_STS_URL || '').trim()
if (!url) {
throw new Error('请配置 ALIYUN_ACCESS_KEY_ID/SECRET开发或 ALIYUN_STS_URL生产')
}
return fetchStsCredentials()
}
function canUseDirectAliyunClient() {
const bucket = (OSS_BUCKET || '').trim()
const hasAk =
!!(ALIYUN_ACCESS_KEY_ID || '').trim() && !!(ALIYUN_ACCESS_KEY_SECRET || '').trim()
const hasStsUrl = !!(ALIYUN_STS_URL || '').trim()
return !!(bucket && (hasAk || hasStsUrl))
}
/**
* @returns {'direct' | 'backend'}
*/
function resolveSegmentTransport() {
const raw = String(SEGMENT_TRANSPORT == null ? 'auto' : SEGMENT_TRANSPORT)
.trim()
.toLowerCase()
if (raw === 'backend' || raw === 'proxy' || raw === 'server') {
return 'backend'
}
if (raw === 'direct' || raw === 'aliyun' || raw === 'client' || raw === 'oss') {
if (!canUseDirectAliyunClient()) {
throw new Error(
'SEGMENT_TRANSPORT 为 direct/aliyun/client/oss 时需配置 OSS_BUCKET 以及ALIYUN_ACCESS_KEY_ID+SECRET 或 ALIYUN_STS_URL'
)
}
return 'direct'
}
return canUseDirectAliyunClient() ? 'direct' : 'backend'
}
/**
* 凭证 → OSS PostObject → 签名 GET URL → IVPD SegmentImageUrl→ downloadFile
*/
async function segmentViaOssIvpd(filePath) {
const bucket = (OSS_BUCKET || '').trim()
if (!bucket) {
throw new Error('请在 config/segmentApi.js 配置 OSS_BUCKET')
}
const cred = await resolveCredentials()
const ext = /\.png$/i.test(String(filePath || '')) ? 'png' : 'jpg'
const base = `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`
const prefix = OSS_KEY_PREFIX.replace(/^\/+/, '')
const objectKeyIn = `${prefix}${base}.${ext}`
await ossPostObjectFromLocalFilePath({
bucket,
ossRegionId: OSS_REGION,
accessKeyId: cred.accessKeyId,
accessKeySecret: cred.accessKeySecret,
securityToken: cred.securityToken,
objectKey: objectKeyIn,
filePath
})
const imageUrlForSeg = await ossPresignedObjectUrl({
bucket,
ossRegionId: OSS_REGION,
accessKeyId: cred.accessKeyId,
accessKeySecret: cred.accessKeySecret,
securityToken: cred.securityToken,
objectKey: objectKeyIn,
expiresSec: 3600
})
const ivpdRegion = (IMM_REGION || '').trim() || String(OSS_REGION || '').replace(/^oss-/i, '')
const outUrl = await rpcIvpdSegmentImage({
imageUrl: imageUrlForSeg,
accessKeyId: cred.accessKeyId,
accessKeySecret: cred.accessKeySecret,
securityToken: cred.securityToken,
regionId: ivpdRegion
})
return raceWithTimeout(
new Promise((res, rej) => {
uni.downloadFile({
url: outUrl,
timeout: 120000,
success: (dr) => {
if (dr.statusCode !== 200 || !dr.tempFilePath) {
rej(new Error('下载分割结果失败'))
return
}
res({ kind: 'cutout', localPath: dr.tempFilePath })
},
fail: (e) => {
const raw = e.errMsg || '未知错误'
const needDomainHint =
looksLikeBareOssEndpointUrl(raw) ||
(/^https?:\/\//i.test(String(raw).trim()) && String(raw).includes('aliyuncs'))
const extra = needDomainHint
? '(若仅为 URL请在 manifest → networkSecurity.domainWhiteList 中加入下载域名并重编译。)'
: ''
rej(new Error(`下载分割结果失败:${raw}${extra}`))
}
})
}),
125000,
'下载分割结果超时'
)
}
/**
* 方案二:自建 Node 代理server/index.js
*/
function segmentViaProxy(filePath) {
const base = (SEGMENT_API_BASE || '').trim().replace(/\/+$/, '')
if (!base) {
return Promise.reject(
new Error(
'缺少 SEGMENT_API_BASEHTTP/后端抠图需要填写根地址POST {BASE}/segment。若使用客户端直连阿里云请配置 OSS_BUCKET 与 AK 或 STS并将 SEGMENT_TRANSPORT 设为 auto 或 direct。'
)
)
}
const url = `${base}/segment`
return new Promise((resolve, reject) => {
uni.uploadFile({
url,
filePath,
name: 'image',
timeout: 120000,
header: SEGMENT_API_TOKEN ? { 'x-laser-token': SEGMENT_API_TOKEN } : {},
success: (res) => {
if (res.statusCode !== 200) {
reject(new Error(typeof res.data === 'string' ? res.data : `HTTP ${res.statusCode}`))
return
}
let body
try {
body = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
} catch (e) {
reject(new Error('代理返回非 JSON'))
return
}
if (!body || body.ok !== true || !body.imageUrl) {
reject(new Error((body && body.error) || '抠图失败'))
return
}
uni.downloadFile({
url: body.imageUrl,
timeout: 120000,
success: (dr) => {
if (dr.statusCode !== 200 || !dr.tempFilePath) {
reject(new Error('下载分割结果失败'))
return
}
resolve({ kind: 'cutout', localPath: dr.tempFilePath })
},
fail: (e) => reject(new Error(e.errMsg || 'downloadFile 失败'))
})
},
fail: (e) => reject(new Error(e.errMsg || 'uploadFile 失败'))
})
})
}
/** 防止某一步网络/原生无回调导致一直转圈 */
function withTimeout(promise, ms, message) {
let timer = null
return new Promise((resolve, reject) => {
timer = setTimeout(() => {
timer = null
reject(new Error(message))
}, ms)
promise.then(
(v) => {
if (timer != null) {
clearTimeout(timer)
}
resolve(v)
},
(e) => {
if (timer != null) {
clearTimeout(timer)
}
reject(e)
}
)
})
}
/**
* 上传本地图并获取抠图结果本地路径。
* 链路由 config/segmentApi.js 的 SEGMENT_TRANSPORT 决定direct=客户端 OSS + IVPD SegmentImagebackend=HTTP 后端(契约同 server/index.jsauto=能直连则直连否则走后端。
*/
export function segmentPortraitToLocal(filePath) {
const transport = resolveSegmentTransport()
const inner =
transport === 'direct' ? segmentViaOssIvpd(filePath) : segmentViaProxy(filePath)
return withTimeout(
inner,
240000,
'智能抠图超时请检查手机网络、OSS、IVPD 地域IMM_REGION如 cn-shanghai、RAM 权限及 manifest 网络权限'
)
}