更新记录

1.1.0(2026-08-20)

  • iOS 新增 Swift 原生混编核心,使用 PhotoKit 读取真实相册,并通过 Vision 在本地检测人脸。
  • Android 新增 Kotlin 原生混编核心,使用 MediaStore 读取真实相册,并通过随包 ML Kit Face Detection 在本地检测人脸。
  • App 端活动、任务、候选和资源状态改由原生层保存;UTS 桥只传递基础标量和 JSON 字符串。
  • 修复 iOS UTS number 生成 NSNumber 后直接传入 Swift Double 参数导致插件整体编译失败的问题;原生层现显式接收并归一化 NSNumber
  • 新增 pauseScanDirectresumeScanDirectcancelScanDirectgetScanStateDirectgetCandidatesDirectprepareAssetsDirectmarkAssetsProcessedDirect
  • demo 改为轮询原生任务状态,显示真实缩略图,不再注册跨桥复杂回调。
  • 时间、地点和人脸筛选均在设备本地执行,不请求后台、不上传照片;不做人脸身份或儿童年龄判断。

平台兼容性

uni-app(5.24)

Vue2 Vue3 Chrome Safari app-vue app-nvue Android iOS 鸿蒙
- - - - - - - - -
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
- - - - - - - - - - - -

uni-app x(5.11)

Chrome Safari Android iOS 鸿蒙 微信小程序

i-photo-filter 照片智能筛选

当前版本定位

本 README 以已发布的 1.1.0 为基础,说明当前开发中、尚未发布的 UniAppX App 能力:在用户授权后真实读取手机相册,并在设备侧按活动时间、活动地点和人脸检测结果筛选照片。

插件不请求后台、不上传照片、不建立人脸库,也不保存人脸特征;筛选处理和缓存均在设备侧完成。真实系统相册扫描只在 iOS 和 Android App 提供;H5、微信小程序和 HarmonyOS 的兼容导出不提供真实自动相册扫描。

App 原生实现

  • iOS:Swift 调用 PhotoKit 读取授权范围内的最近照片,使用 Vision 检测人脸。
  • Android:Kotlin 调用 MediaStore 读取最近照片,使用随包 ML Kit Face Detection 检测人脸。
  • 活动、任务、候选和资源状态保存在 Swift/Kotlin 原生层。
  • App 的 UTS 桥只传递 stringnumberboolean;任务状态、候选和资源结果通过原生 JSON 字符串返回。
  • demo 每 400ms 轮询任务状态,不跨 iOS/Android 桥传递 Options、候选数组或 callback 对象。
  • 候选缩略图写入应用缓存目录;只有用户确认候选后,prepareAssetsDirect 才按需准备原图临时文件。

平台能力

平台 权限与实现 当前能力
iOS App PhotoKit + Vision 真实相册读取、时间/地点筛选、本地人脸检测、缩略图缓存、按需准备原图。
Android App MediaStore + ML Kit 真实相册读取、时间/可用位置筛选、本地人脸检测、缩略图缓存、按需准备原图。
H5 unsupported 不读取系统相册;兼容导出仅用于模拟和接口调试,不提供真实自动扫描。
微信小程序 unsupported 兼容导出不支持自动读取系统相册。
HarmonyOS unsupported 兼容导出当前未接入原生相册实现,不提供真实自动扫描。

iOS 的 limited 权限只扫描用户授权的照片。照片没有位置数据、或 Android 未授予 ACCESS_MEDIA_LOCATION 时,locationMatcheddistanceMetersnull:这表示位置未知。启用地点筛选时,locationMatched: null 不会直接使照片筛选失败,只有 locationMatched: false 表示实际地点不匹配并使地点条件失败。

ActivityProfile 示例

完整兼容画像仍使用 IPhotoFilterActivityProfile

import { IPhotoFilterActivityProfile } from '@/uni_modules/i-photo-filter/interface.uts'

const profile : IPhotoFilterActivityProfile = {
  activityId: 'children-sports-day-20260726',
  name: '儿童夏日运动会',
  startAt: 1785031200000,
  endAt: 1785045600000,
  timePaddingMinutes: 30,
  location: {
    latitude: 32.0603,
    longitude: 118.7969,
    radiusMeters: 1200
  },
  isChildrenOnly: true,
  referenceImages: [
    '/static/logo.png',
    '/static/logo.png',
    '/static/logo.png',
    '/static/logo.png',
    '/static/logo.png'
  ],
  referenceTags: ['运动场', '彩色背心', '跑道', '儿童活动'],
  highConfidenceThreshold: 78,
  mediumConfidenceThreshold: 60,
  profileVersion: 'demo-profile-1',
  modelVersion: 'native-local-1'
}

referenceImages 是兼容契约字段,App 原生直连接口不会传输这些图片路径,不参与筛选。referenceTags 同样是兼容契约字段,不参与原生筛选。当前原生 MVP 只使用时间、地点、人脸及置信度阈值。

App API 使用示例

App 端使用 *Direct 接口。结构化返回值都是包含 okcodemessagedata 的 JSON 字符串:

import {
  prepareActivityDirect,
  startScanDirect,
  getScanStateDirect,
  getCandidatesDirect,
  getScannedPhotosDirect,
  prepareAssetsDirect,
  markAssetsProcessedDirect
} from '@/uni_modules/i-photo-filter'

const activityId = 'children-sports-day-20260726'

const prepared = prepareActivityDirect(
  activityId,
  '儿童夏日运动会',
  1785031200000,
  1785045600000,
  30,
  32.0603,
  118.7969,
  1200,
  true,
  78,
  60,
  'demo-profile-1',
  'native-local-1'
)

if (prepared) {
  const taskId = startScanDirect(
    activityId,
    'album',
    10,
    true,
    true,
    true,
    4,
    120,
    1,
    20260726,
    0
  )

  const stateJson = getScanStateDirect(taskId)
  const candidatesJson = getCandidatesDirect(
    activityId,
    taskId,
    'high,medium,low',
    false,
    1,
    50
  )
  const scannedPhotosJson = getScannedPhotosDirect(taskId, 1, 50)
  const assetIdsJson = '[]'
  const assetsJson = prepareAssetsDirect(taskId, assetIdsJson)
  const marked = markAssetsProcessedDirect(taskId, assetIdsJson)
  console.log(stateJson, candidatesJson, scannedPhotosJson, assetsJson, marked)
}

业务页面应轮询 getScanStateDirect,在状态变为 completedcancelledfailed 时停止轮询。

API 表

App 原生直连接口

API 返回值 行为
prepareActivityDirect(...) boolean 用基础标量在原生层保存活动画像。
clearActivityCache(activityId) void 清除活动及其原生任务缓存。
getPhotoPermissionStatus() PhotoPermissionStatus 读取系统相册权限状态。
requestPhotoPermissionDirect() void 请求系统相册权限。
startScanDirect(...) string 启动真实相册扫描,返回 taskId;失败返回空字符串。
pauseScanDirect(taskId) string 返回状态 JSON。
resumeScanDirect(taskId) string 返回状态 JSON。
cancelScanDirect(taskId) string 返回状态 JSON。
getScanStateDirect(taskId) string 返回状态 JSON。
getCandidatesDirect(...) string 返回分页候选 JSON,保留用于既有候选筛选流程。
getScannedPhotosDirect(taskId, page, pageSize) string 返回当前任务的全部已枚举照片分页结果;包括命中、未命中及缩略图读取失败记录。iOS/Android 返回原生 JSON envelope。
prepareAssetsDirect(taskId, assetIdsJson) string 返回已准备原图临时文件 JSON。
markAssetsProcessedDirect(taskId, assetIdsJson) boolean 在原生任务中标记照片已处理。

兼容模拟接口

以下接口保留在 interface.uts 和非 App 共享实现中:prepareActivityrequestPhotoPermissionstartScanpauseScanresumeScancancelScangetScanStategetCandidatesprepareAssetsmarkAssetsProcessedonScanProgressoffScanProgressonCandidateBatchoffCandidateBatchonScanStateChangeoffScanStateChangeonAssetPrepareProgressoffAssetPrepareProgressonPluginErroroffPluginError

App demo 不调用这些复杂 Options/Result/Event 接口,避免生成的 Swift/Kotlin 桥接复杂对象。

扫描照片分页结果

getScannedPhotosDirect(taskId, page, pageSize) 是新增的扫描结果查询接口,getCandidatesDirect(...) 仍完整保留以兼容已有调用。iOS/Android 成功时返回如下原生 envelope(list 中每项为 IPhotoFilterScannedPhoto):

{
  "ok": true,
  "code": 0,
  "message": "",
  "data": {
    "taskId": "scan-20260727-001",
    "total": 126,
    "page": 1,
    "pageSize": 50,
    "list": []
  }
}

扫描会保留任务中所有已枚举的照片记录,而不只保留候选。原生返回的 selected 是初始默认选择建议:filterMatched: trueconfidenceLevel: "high" 的照片初始为选中。demo 的交互选择由 selectedAssetIds 独立维护,重新查询扫描结果不会回写或改变返回项的 photo.selected;可正常读取但未命中的照片可以手动选择,loadFailed: true 的照片不可选择。将 selectedAssetIds 中最终选中的 assetId 组成 JSON 数组传给 prepareAssetsDirect(taskId, assetIdsJson),即可按需准备原图临时文件。

候选字段表

字段 类型 当前含义
assetId string iOS PhotoKit local identifier 或 Android MediaStore ID。
source album \| mock 候选来源。
thumbnailPath string 应用缓存目录中的真实缩略图路径。
createdAt number 照片拍摄时间,毫秒时间戳。
distanceMeters number \| null 照片位置到活动地点的距离;无位置数据时为 null
locationMatched boolean \| null 是否在活动半径内;无位置数据时为 null
faceCount number 本地检测到的人脸数量。
personDetected boolean 当前等价于 faceCount > 0
personScore number 当前基础人物分数。
childLikelihood number \| null 原生 MVP 固定为 null,不做年龄判断。
subjectScore number 当前基础主体分数。
sceneSimilarity number 当前由时间命中提供的基础分数。
qualityScore number 当前基础质量分数。
duplicateGroupId string 预留重复照片分组字段。
totalScore number 时间、地点和人脸基础分的汇总。
confidenceLevel high \| medium \| low 根据活动阈值计算的等级。
reasonTags Array<string> time_matchedlocation_matchedface_detected
selected boolean high 候选默认选中。

扫描照片字段表

IPhotoFilterScannedPhoto 用于 getScannedPhotosDirectdata.list。它与候选字段不同:它覆盖任务中全部已枚举照片,并明确表达筛选和读取状态。

字段 类型 含义
assetId string iOS PhotoKit local identifier 或 Android MediaStore ID,可作为 prepareAssetsDirect 的选中资源 ID。
source string 照片来源;原生相册扫描为 album
thumbnailPath string 本地缩略图缓存路径;loadFailed: true 时为空字符串。
createdAt number 照片拍摄时间,毫秒时间戳。
distanceMeters number \| null 与活动地点的距离;无位置数据时为 null
locationMatched boolean \| null true 为地点匹配,false 为实际不匹配,null 为位置未知;未知不会直接使已启用的地点筛选失败。
faceCount number 本地检测到的人脸数量。
personDetected boolean 当前等价于 faceCount > 0
filterMatched boolean 时间、地点、人脸等当前筛选条件均通过,且缩略图读取成功。
loadFailed boolean 照片/缩略图读取失败,或缩略图缓存写入失败;失败记录仍保留在列表中,但不可选择。
confidenceLevel string 按当前活动阈值得出的 highmediumlow 等级。
totalScore number 当前时间、地点和人脸基础分的汇总。
reasonTags Array<string> 当前匹配/失败原因标签,例如 time_matchedlocation_matchedface_detectedthumbnail_failed
selected boolean 原生扫描给出的初始默认选择建议;高置信度且命中的照片初始为 true。demo 的后续手动选择由独立的 selectedAssetIds 维护,不会回写此字段。

错误码表

错误码 常量 含义
9011001 PHOTO_FILTER_PERMISSION_DENIED 照片访问权限被拒绝。
9011002 PHOTO_FILTER_PERMISSION_RESTRICTED 当前平台限制照片访问。
9012001 PHOTO_FILTER_SCAN_NOT_FOUND 未找到筛选任务。
9012002 PHOTO_FILTER_SCAN_BUSY 筛选任务正在运行或任务不匹配。
9012003 PHOTO_FILTER_SCAN_STATE_INVALID 当前任务状态不支持该操作。
9013001 PHOTO_FILTER_PROFILE_INVALID 活动画像参数无效。
9013002 PHOTO_FILTER_MODEL_VERSION_INVALID 模型版本无效。
9014001 PHOTO_FILTER_ASSET_NOT_FOUND 未找到任务中的照片资源。
9014002 PHOTO_FILTER_ASSET_PREPARE_FAILED 照片资源准备失败。
9015001 PHOTO_FILTER_CACHE_CLEAR_FAILED 缓存或原生 JSON 处理失败。

PHOTO_FILTER_SCAN_NOT_FOUND9012001)和 PHOTO_FILTER_SCAN_BUSY9012002)均为公开常量。为兼容当前原生实现,iOS/Android 的 Direct JSON 方法在任务不存在或活动不匹配时返回 9012002;例如 getScannedPhotosDirect 的未知 taskId 不会返回 9012001

隐私说明

  • 筛选处理和缓存均在设备侧完成,不请求业务后台,也不向业务后台上传照片或人脸特征。
  • 只在扫描时生成缩略图缓存;原图仅在用户确认后按需写入临时目录。
  • 不做人脸身份比对,也不建立长期人脸库。
  • iCloud 照片尚未下载到本机时,PhotoKit 可能通过系统管理的网络获取资源;插件本身不发起业务后台上传。
  • “检测到人脸”不等于“确认是孩子”。当前 childLikelihoodnull
  • 清除活动缓存会移除内存任务;系统缓存文件由应用缓存生命周期管理。

当前边界

  • iCloud 中尚未下载到本机的照片可能需要 PhotoKit 临时下载,速度受系统和网络状态影响,但插件本身不连接业务后台。
  • Android 7.0(API 24)及以上才读取系统 EXIF;Android 6.0(API 23)会跳过 EXIF 位置并按正常朝向处理,避免加载高版本系统类型。更高版本的位置元数据仍受照片 EXIF 和 ACCESS_MEDIA_LOCATION 权限影响。
  • 当前不做儿童年龄判断、身份识别、服装识别、参考图相似度、重复照片聚类或跨进程持久化断点。
  • H5、微信小程序和 HarmonyOS 暂不提供真实自动相册扫描。
  • 必须完成 iOS 与 Android 真机编译和运行验证后,才能把相应平台标记为已验收。

隐私、权限声明

1. 本插件需要申请的系统权限列表:

iOS 相册读取权限;Android READ_MEDIA_IMAGES/READ_EXTERNAL_STORAGE 与可选 ACCESS_MEDIA_LOCATION

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

仅在设备本地读取用户授权的照片、拍摄时间、可用位置元数据并生成缓存缩略图;不上传照片,不保存人脸特征

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

暂无用户评论。