feat: switch to plugin

This commit is contained in:
mgt
2025-11-21 21:25:27 +08:00
parent 48eae6658c
commit 5961387d90
11 changed files with 370 additions and 266 deletions

View File

@@ -1,7 +1,7 @@
---
title: Overview
createdAt: 2025-07-20T22:35:03+08:00
lastModified: 2025-11-20T23:41:32+08:00
lastModified: 2025-11-21T21:25:29+08:00
---
# Overview
@@ -128,12 +128,13 @@ This will automatically pull resources from the remote repository, avoiding rebu
- `digestSuffixLength`: Digest suffix length for deterministic IDs
- `supportedFormats`: Optional allowlist of file extensions to process
#### Geocoding (`user.geocoding`)
#### Geocoding (via `geocodingPlugin`)
- `enableGeocoding`: Enable reverse geocoding from GPS coordinates (default: `false`)
- `geocodingProvider`: Geocoding service provider (`'mapbox'` | `'nominatim'` | `'auto'`, default: `'auto'`)
- `enable`: Enable reverse geocoding from GPS coordinates (default: `false`)
- `provider`: Geocoding service provider (`'mapbox'` | `'nominatim'` | `'auto'`, default: `'auto'`)
- `mapboxToken`: Mapbox access token (required when using Mapbox provider)
- `nominatimBaseUrl`: Custom Nominatim base URL (optional, defaults to OpenStreetMap's public instance)
- `cachePrecision`: Coordinate cache precision (decimals, default: `4`)
#### System Observability (`system.observability`)
@@ -148,34 +149,40 @@ This will automatically pull resources from the remote repository, avoiding rebu
#### Geocoding Configuration Example
To enable reverse geocoding, configure the geocoding provider under `user.geocoding` in your `builder.config.ts`:
Enable reverse geocoding by adding the geocoding plugin in `builder.config.ts`:
**Using Nominatim (Free, but rate-limited):**
```typescript
{
user: {
geocoding: {
enableGeocoding: true,
import { defineBuilderConfig, geocodingPlugin } from '@afilmory/builder'
export default defineBuilderConfig(() => ({
plugins: [
geocodingPlugin({
enable: true,
provider: 'nominatim',
// Optional: use custom Nominatim instance
// nominatimBaseUrl: 'https://your-nominatim-instance.com'
},
},
}
}),
],
}))
```
**Using Mapbox:**
```typescript
{
user: {
geocoding: {
enableGeocoding: true,
import { defineBuilderConfig, geocodingPlugin } from '@afilmory/builder'
export default defineBuilderConfig(() => ({
plugins: [
geocodingPlugin({
enable: true,
provider: 'mapbox',
mapboxToken: process.env.MAPBOX_TOKEN,
},
},
}
}),
],
}))
```
**Notes:**
@@ -183,6 +190,7 @@ To enable reverse geocoding, configure the geocoding provider under `user.geocod
- Nominatim is free but has strict rate limits (1 request/second)
- Both providers support intelligent caching to minimize API calls
- Location data includes country, city, and location name extracted from GPS coordinates
- Reverse geocoding is handled by the built-in geocoding plugin and runs only when the plugin is added to `plugins` with `enable: true`
## 📋 CLI Commands

View File

@@ -55,12 +55,6 @@ export default defineBuilderConfig(() => ({
},
},
},
user: {
geocoding: {
enableGeocoding: false,
geocodingProvider: 'auto',
},
},
// plugins: [thumbnailStoragePlugin()],
plugins: [
githubRepoSyncPlugin({

View File

@@ -4,9 +4,6 @@ import { thumbnailExists } from '../image/thumbnail.js'
import { logger } from '../logger/index.js'
import { handleDeletedPhotos, loadExistingManifest, needsUpdate, saveManifest } from '../manifest/manager.js'
import { CURRENT_MANIFEST_VERSION } from '../manifest/version.js'
import { createStorageKeyNormalizer, runWithPhotoExecutionContext } from '../photo/execution-context.js'
import { createGeocodingProvider, parseGPSCoordinates } from '../photo/geocoding.js'
import { createPhotoProcessingLoggers } from '../photo/logger-adapter.js'
import type { PhotoProcessorOptions } from '../photo/processor.js'
import { processPhoto } from '../photo/processor.js'
import type { PluginRunState } from '../plugins/manager.js'
@@ -21,7 +18,7 @@ import type { StorageConfig } from '../storage/index.js'
import { StorageFactory, StorageManager } from '../storage/index.js'
import type { BuilderConfig, UserBuilderSettings } from '../types/config.js'
import type { AfilmoryManifest, CameraInfo, LensInfo } from '../types/manifest.js'
import type { LocationInfo, PhotoManifestItem, ProcessPhotoResult } from '../types/photo.js'
import type { PhotoManifestItem, ProcessPhotoResult } from '../types/photo.js'
import { ClusterPool } from '../worker/cluster-pool.js'
import type { TaskCompletedPayload } from '../worker/pool.js'
import { WorkerPool } from '../worker/pool.js'
@@ -420,13 +417,6 @@ export class AfilmoryBuilder {
})
}
const locationRetryStats = await this.retryMissingLocations(manifest)
if (locationRetryStats.attempted > 0) {
logger.main.info(
`📍 为 ${locationRetryStats.attempted} 张缺失位置信息的照片尝试补全,成功 ${locationRetryStats.updated}`,
)
}
await this.emitPluginEvent(runState, 'afterProcessTasks', {
options,
tasks: tasksToProcess,
@@ -688,78 +678,6 @@ export class AfilmoryBuilder {
return this.storageManager
}
private async retryMissingLocations(manifest: PhotoManifestItem[]): Promise<{ attempted: number; updated: number }> {
const geocodingSettings = this.getUserSettings().geocoding
if (!geocodingSettings.enableGeocoding) {
return { attempted: 0, updated: 0 }
}
const provider = createGeocodingProvider(
geocodingSettings.geocodingProvider || 'auto',
geocodingSettings.mapboxToken,
geocodingSettings.nominatimBaseUrl,
)
if (!provider) {
return { attempted: 0, updated: 0 }
}
const hasCandidate = manifest.some(
(item) =>
!item.location && item.exif && item.exif.GPSLatitude !== undefined && item.exif.GPSLongitude !== undefined,
)
if (!hasCandidate) {
return { attempted: 0, updated: 0 }
}
const storageManager = this.ensureStorageManager()
const storageConfig = this.getStorageConfig()
const normalizeStorageKey = createStorageKeyNormalizer(storageConfig)
const loggers = createPhotoProcessingLoggers(0, logger)
return await runWithPhotoExecutionContext(
{
builder: this,
storageManager,
storageConfig,
normalizeStorageKey,
loggers,
},
async () => {
const coordinateCache = new Map<string, LocationInfo | null>()
let attempted = 0
let updated = 0
for (const item of manifest) {
if (item.location || !item.exif) {
continue
}
const { latitude, longitude } = parseGPSCoordinates(item.exif)
if (latitude === undefined || longitude === undefined) {
continue
}
const cacheKey = `${latitude.toFixed(4)},${longitude.toFixed(4)}`
let locationInfo = coordinateCache.get(cacheKey)
if (locationInfo === undefined) {
locationInfo = await provider.reverseGeocode(latitude, longitude)
coordinateCache.set(cacheKey, locationInfo ?? null)
}
attempted++
if (locationInfo) {
item.location = locationInfo
updated++
}
}
return { attempted, updated }
},
)
}
private getUserSettings(): UserBuilderSettings {
if (!this.config.user) {
throw new Error('User configuration is missing. 请配置 system/user 设置。')

View File

@@ -28,14 +28,7 @@ export function createDefaultBuilderConfig(): BuilderConfig {
},
},
},
user: {
storage: null,
geocoding: {
// 地理编码默认配置
enableGeocoding: false, // 默认关闭,需要用户主动启用
geocodingProvider: 'auto',
},
},
user: null!,
plugins: [],
}
}

View File

@@ -57,21 +57,8 @@ function ensureUserSettings(target: BuilderConfig): UserBuilderSettings {
if (!target.user) {
target.user = {
storage: null,
geocoding: {
enableGeocoding: false,
geocodingProvider: 'auto',
},
}
}
// 兼容旧的配置对象缺少 geocoding 字段的情况
if (!target.user.geocoding) {
target.user.geocoding = {
enableGeocoding: false,
geocodingProvider: 'auto',
}
}
return target.user
}
@@ -82,13 +69,6 @@ function applyUserOverrides(target: BuilderConfig, overrides?: BuilderConfigInpu
if (overrides.storage !== undefined) {
user.storage = overrides.storage as StorageConfig | null
}
if (overrides.geocoding) {
user.geocoding = {
...user.geocoding,
...overrides.geocoding,
}
}
}
function normalizeBuilderConfig(defaults: BuilderConfig, input: BuilderConfigInput): BuilderConfig {

View File

@@ -13,6 +13,8 @@ export {
processPhotoWithPipeline,
} from './photo/image-pipeline.js'
export type { PhotoProcessorOptions } from './photo/processor.js'
export type { GeocodingPluginOptions } from './plugins/geocoding.js'
export { default as geocodingPlugin } from './plugins/geocoding.js'
export type { GitHubRepoSyncPluginOptions } from './plugins/github-repo-sync.js'
export { createGitHubRepoSyncPlugin, default as githubRepoSyncPlugin } from './plugins/github-repo-sync.js'
export type { B2StoragePluginOptions } from './plugins/storage/b2.js'

View File

@@ -12,7 +12,7 @@
- **`live-photo-handler.ts`** - Live Photo 检测和处理
- **`logger-adapter.ts`** - Logger 适配器,实现适配器模式
- **`info-extractor.ts`** - 照片信息提取
- **`geocoding.ts`** - 反向地理编码,从 GPS 坐标提取位置信息
- **`geocoding.ts`** - 反向地理编码提供者定义(通过 geocoding 插件调用)
### 设计模式
@@ -43,11 +43,10 @@ class CompatibleLoggerAdapter implements PhotoLogger {
2. 创建 Sharp 实例
3. 处理缩略图和 blurhash
4. 处理 EXIF 数据
5. 处理影调分析
6. 处理地理编码(从 GPS 坐标反向解析位置信息)
5. HDR / Motion Photo / Live Photo 检测
6. 处理影调分析
7. 提取照片信息
8. 处理 Live Photo
9. 构建照片清单项
8. 构建照片清单项
### 主要改进
@@ -55,7 +54,7 @@ class CompatibleLoggerAdapter implements PhotoLogger {
2. **Logger 适配器**: 使用异步执行上下文管理 logger避免全局状态污染
3. **缓存管理**: 统一管理各种数据的缓存和复用逻辑
4. **Live Photo 处理**: 专门的模块处理 Live Photo 检测和匹配
5. **反向地理编码**: 支持从 GPS 坐标提取位置信息,支持多个地理编码提供商
5. **反向地理编码插件**: 通过 geocoding 插件在构建生命周期中写入位置信息,支持多个地理编码提供商
6. **类型安全**: 完善的 TypeScript 类型定义
### 使用方法
@@ -83,8 +82,7 @@ const result = await processPhoto(
#### 单独使用各个模块
```typescript
import { processLivePhoto, processThumbnailAndBlurhash, processExifData, processLocationData } from './index.js'
import { createGeocodingProvider } from './geocoding.js'
import { processLivePhoto, processThumbnailAndBlurhash, processExifData } from './index.js'
// Live Photo 处理
const livePhotoResult = processLivePhoto(photoKey, livePhotoMap, builder.getStorageManager())
@@ -94,9 +92,25 @@ const thumbnailResult = await processThumbnailAndBlurhash(imageBuffer, photoId,
// EXIF 处理
const exifData = await processExifData(imageBuffer, rawImageBuffer, photoKey, existingItem, options)
```
// 地理编码处理
const locationInfo = await processLocationData(exifData, photoKey, existingItem, options)
#### 启用反向地理编码
`builder.config.ts` 中通过插件开启:
```typescript
import { defineBuilderConfig, geocodingPlugin } from '@afilmory/builder'
export default defineBuilderConfig(() => ({
plugins: [
geocodingPlugin({
enable: true,
provider: 'auto',
mapboxToken: process.env.MAPBOX_TOKEN,
// nominatimBaseUrl: 'https://your-nominatim-instance.com',
}),
],
}))
```
### 扩展性

View File

@@ -9,10 +9,7 @@ import { extractExifData } from '../image/exif.js'
import { calculateHistogramAndAnalyzeTone } from '../image/histogram.js'
import { generateThumbnailAndBlurhash, thumbnailExists } from '../image/thumbnail.js'
import { workdir } from '../path.js'
import type { LocationInfo, PhotoManifestItem, PickedExif, ToneAnalysis } from '../types/photo.js'
import { getPhotoExecutionContext } from './execution-context.js'
import type { GeocodingProvider } from './geocoding.js'
import { createGeocodingProvider, extractLocationFromGPS, parseGPSCoordinates } from './geocoding.js'
import type { PhotoManifestItem, PickedExif, ToneAnalysis } from '../types/photo.js'
import { getGlobalLoggers } from './logger-adapter.js'
import type { PhotoProcessorOptions } from './processor.js'
@@ -123,100 +120,3 @@ export async function processToneAnalysis(
// 计算新的影调分析
return await calculateHistogramAndAnalyzeTone(sharpInstance)
}
// ============ 地理编码相关 ============
// 坐标缓存(避免重复 API 调用)
// Key 格式:"{lat},{lon}" 精确到小数点后4位约10米精度
const locationCache = new Map<string, LocationInfo | null>()
// 单例提供者(避免重复创建)
let cachedProvider: GeocodingProvider | null = null
let lastProviderConfig: string | null = null
/**
* 处理位置数据(反向地理编码)
* 优先复用现有数据,如果不存在或需要强制更新则进行地理编码
*/
export async function processLocationData(
exifData: PickedExif | null,
photoKey: string,
existingItem: PhotoManifestItem | undefined,
options: PhotoProcessorOptions,
): Promise<LocationInfo | null> {
const loggers = getGlobalLoggers()
try {
// 获取配置
const context = getPhotoExecutionContext()
const config = context.builder.getConfig()
const geocodingSettings = config.user?.geocoding ?? {
enableGeocoding: false,
geocodingProvider: 'auto',
}
// 检查是否启用地理编码
if (!geocodingSettings.enableGeocoding) {
return null
}
// 检查是否可以复用现有数据
if (!options.isForceMode && !options.isForceManifest && existingItem?.location) {
const photoId = path.basename(photoKey, path.extname(photoKey))
loggers.location.info(`复用现有位置数据:${photoId}`)
return existingItem.location
}
// 检查 EXIF 是否包含 GPS 数据
if (!exifData) {
return null
}
// 解析 GPS 坐标
const { latitude, longitude } = parseGPSCoordinates(exifData)
if (latitude === undefined || longitude === undefined) {
return null
}
// 生成缓存 key精确到小数点后4位
const cacheKey = `${latitude.toFixed(4)},${longitude.toFixed(4)}`
// 检查缓存
if (locationCache.has(cacheKey)) {
const cached = locationCache.get(cacheKey)
const photoId = path.basename(photoKey, path.extname(photoKey))
loggers.location.info(`使用缓存的位置数据:${photoId} (${cacheKey})`)
return cached ?? null
}
// 创建或复用地理编码提供者
const providerType = geocodingSettings.geocodingProvider || 'auto'
const providerConfigKey = `${providerType}:${geocodingSettings.mapboxToken || ''}:${geocodingSettings.nominatimBaseUrl || ''}`
if (!cachedProvider || lastProviderConfig !== providerConfigKey) {
cachedProvider = createGeocodingProvider(
providerType,
geocodingSettings.mapboxToken,
geocodingSettings.nominatimBaseUrl,
)
lastProviderConfig = providerConfigKey
}
if (!cachedProvider) {
loggers.location.warn('无法创建地理编码提供者')
return null
}
// 调用反向地理编码 API
const locationInfo = await extractLocationFromGPS(latitude, longitude, cachedProvider)
// 缓存结果(包括 null
locationCache.set(cacheKey, locationInfo)
return locationInfo
} catch (error) {
loggers.location.error('处理位置数据失败:', error)
return null
}
}

View File

@@ -16,12 +16,7 @@ import { THUMBNAIL_PLUGIN_DATA_KEY } from '../plugins/thumbnail-storage/shared.j
import type { PhotoManifestItem, ProcessPhotoResult } from '../types/photo.js'
import type { S3ObjectLike } from '../types/s3.js'
import { shouldProcessPhoto } from './cache-manager.js'
import {
processExifData,
processLocationData,
processThumbnailAndBlurhash,
processToneAnalysis,
} from './data-processors.js'
import { processExifData, processThumbnailAndBlurhash, processToneAnalysis } from './data-processors.js'
import { getPhotoExecutionContext } from './execution-context.js'
import { detectGainMap } from './gainmap-detector.js'
import { extractPhotoInfo } from './info-extractor.js'
@@ -206,12 +201,9 @@ export async function executePhotoProcessingPipeline(
throw new Error(errorMsg)
}
// 7. 处理影调分析
// 8. 处理影调分析
const toneAnalysis = await processToneAnalysis(sharpInstance, photoKey, existingItem, options)
// 8. 处理位置数据(反向地理编码)
const location = await processLocationData(exifData, photoKey, existingItem, options)
// 9. 提取照片信息
const photoInfo = extractPhotoInfo(photoKey, exifData)
@@ -235,7 +227,7 @@ export async function executePhotoProcessingPipeline(
digest: contentDigest,
exif: exifData,
toneAnalysis,
location,
location: existingItem?.location ?? null,
// Video source (Motion Photo or Live Photo)
video:
motionPhotoMetadata?.isMotionPhoto && motionPhotoMetadata.motionPhotoOffset !== undefined

View File

@@ -0,0 +1,311 @@
import type { AfilmoryBuilder } from '../builder/builder.js'
import type { Logger } from '../logger/index.js'
import {
createStorageKeyNormalizer,
getPhotoExecutionContext,
runWithPhotoExecutionContext,
} from '../photo/execution-context.js'
import type { GeocodingProvider } from '../photo/geocoding.js'
import { createGeocodingProvider, extractLocationFromGPS, parseGPSCoordinates } from '../photo/geocoding.js'
import { createPhotoProcessingLoggers } from '../photo/logger-adapter.js'
import type { LocationInfo, PhotoManifestItem, PickedExif } from '../types/photo.js'
import type { BuilderPlugin } from './types.js'
const PLUGIN_NAME = 'afilmory:geocoding'
const RUN_STATE_KEY = 'geocodingState'
const DEFAULT_CACHE_PRECISION = 4
interface GeocodingPluginOptions {
enable?: boolean
provider?: 'mapbox' | 'nominatim' | 'auto'
mapboxToken?: string
nominatimBaseUrl?: string
cachePrecision?: number
}
type GeocodingPluginOptionsResolved = Required<Pick<GeocodingPluginOptions, 'enable' | 'provider'>> &
Pick<GeocodingPluginOptions, 'mapboxToken' | 'nominatimBaseUrl' | 'cachePrecision'> & {
language: string | null
}
interface ResolvedGeocodingSettings {
provider: 'mapbox' | 'nominatim' | 'auto'
mapboxToken?: string
nominatimBaseUrl?: string
cachePrecision: number
}
interface GeocodingState {
provider: GeocodingProvider | null
providerKey: string | null
cache: Map<string, LocationInfo | null>
}
interface LocationResolutionResult {
attempted: boolean
updated: boolean
}
type LocationLogger = Logger['main']
function normalizeCachePrecision(value: number | undefined): number {
if (typeof value !== 'number' || Number.isNaN(value)) {
return DEFAULT_CACHE_PRECISION
}
const rounded = Math.round(value)
return Math.max(0, Math.min(10, rounded))
}
function normalizeLanguage(value: string | string[] | undefined): string | null {
if (!value) return null
const parts = Array.isArray(value) ? value : String(value).split(',')
const normalized = parts.map((v) => v.trim()).filter(Boolean)
return normalized.length > 0 ? normalized.join(',') : null
}
function resolveSettings(options: GeocodingPluginOptions): GeocodingPluginOptionsResolved {
return {
enable: options.enable ?? false,
provider: options.provider ?? 'auto',
mapboxToken: options.mapboxToken,
nominatimBaseUrl: options.nominatimBaseUrl,
cachePrecision: normalizeCachePrecision(options.cachePrecision ?? DEFAULT_CACHE_PRECISION),
}
}
function getOrCreateState(runShared: Map<string, unknown>): GeocodingState {
const existing = runShared.get(RUN_STATE_KEY) as GeocodingState | undefined
if (existing) {
return existing
}
const next: GeocodingState = {
provider: null,
providerKey: null,
cache: new Map(),
}
runShared.set(RUN_STATE_KEY, next)
return next
}
function buildProviderKey(settings: ResolvedGeocodingSettings): string {
return `${settings.provider}:${settings.mapboxToken ?? ''}:${settings.nominatimBaseUrl ?? ''}`
}
function ensureProvider(
state: GeocodingState,
settings: ResolvedGeocodingSettings,
logger: LocationLogger,
): GeocodingProvider | null {
const providerKey = buildProviderKey(settings)
if (state.provider && state.providerKey === providerKey) {
return state.provider
}
const provider = createGeocodingProvider(settings.provider, settings.mapboxToken, settings.nominatimBaseUrl)
if (!provider) {
logger.warn('无法创建地理编码提供者,请检查 geocoding 配置和 Token')
state.provider = null
state.providerKey = null
return null
}
state.provider = provider
state.providerKey = providerKey
return provider
}
async function ensurePhotoContext<T>(builder: AfilmoryBuilder, logger: Logger, fn: () => Promise<T>): Promise<T> {
try {
getPhotoExecutionContext()
return await fn()
} catch {
const storageConfig = builder.getStorageConfig()
const storageManager = builder.getStorageManager()
const normalizeStorageKey = createStorageKeyNormalizer(storageConfig)
const loggers = createPhotoProcessingLoggers(0, logger)
return await runWithPhotoExecutionContext(
{
builder,
storageManager,
storageConfig,
normalizeStorageKey,
loggers,
},
fn,
)
}
}
function buildCacheKey(latitude: number, longitude: number, precision: number): string {
return `${latitude.toFixed(precision)},${longitude.toFixed(precision)}`
}
async function resolveLocationForItem(
item: PhotoManifestItem,
exif: PickedExif | null | undefined,
state: GeocodingState,
settings: ResolvedGeocodingSettings,
provider: GeocodingProvider,
shouldOverwriteExisting: boolean,
): Promise<LocationResolutionResult> {
if (item.location && !shouldOverwriteExisting) {
return { attempted: false, updated: false }
}
if (!exif) {
if (shouldOverwriteExisting) {
item.location = null
}
return { attempted: false, updated: false }
}
const { latitude, longitude } = parseGPSCoordinates(exif)
if (latitude === undefined || longitude === undefined) {
if (shouldOverwriteExisting) {
item.location = null
}
return { attempted: false, updated: false }
}
const cacheKey = buildCacheKey(latitude, longitude, settings.cachePrecision)
const cached = state.cache.get(cacheKey)
if (cached !== undefined) {
if (cached) {
item.location = cached
return { attempted: true, updated: true }
}
if (shouldOverwriteExisting) {
item.location = null
}
return { attempted: true, updated: false }
}
const location = await extractLocationFromGPS(latitude, longitude, provider)
state.cache.set(cacheKey, location ?? null)
if (location) {
item.location = location
return { attempted: true, updated: true }
}
if (shouldOverwriteExisting) {
item.location = null
}
return { attempted: true, updated: false }
}
export default function geocodingPlugin(options: GeocodingPluginOptions = {}): BuilderPlugin {
const normalizedOptions = resolveSettings(options)
let settings: ResolvedGeocodingSettings | null = null
return {
name: PLUGIN_NAME,
hooks: {
onInit: () => {
settings = {
provider: normalizedOptions.provider,
mapboxToken: normalizedOptions.mapboxToken,
nominatimBaseUrl: normalizedOptions.nominatimBaseUrl,
cachePrecision: normalizedOptions.cachePrecision ?? DEFAULT_CACHE_PRECISION,
language: normalizedOptions.language,
}
},
afterPhotoProcess: async ({ builder, payload, runShared, logger }) => {
if (!settings) return
const { item } = payload.result
if (!item) return
const shouldOverwriteExisting = payload.options.isForceMode || payload.options.isForceManifest
if (!normalizedOptions.enable) return
// 当已有位置信息且未强制刷新时,不重复调用地理编码
if (item.location && !shouldOverwriteExisting) {
return
}
const currentSettings = settings
await ensurePhotoContext(builder, logger, async () => {
const state = getOrCreateState(runShared)
const locationLogger = logger.main.withTag('LOCATION')
const provider = ensureProvider(state, currentSettings, locationLogger)
if (!provider) {
if (shouldOverwriteExisting) {
item.location = null
}
return
}
const exif = item.exif ?? payload.context.existingItem?.exif ?? null
await resolveLocationForItem(item, exif, state, currentSettings, provider, shouldOverwriteExisting)
})
},
afterProcessTasks: async ({ builder, payload, runShared, logger }) => {
if (!settings || !normalizedOptions.enable) {
return
}
const currentSettings = settings
const state = getOrCreateState(runShared)
const locationLogger = logger.main.withTag('LOCATION')
const provider = ensureProvider(state, currentSettings, locationLogger)
if (!provider) {
return
}
const storageConfig = builder.getStorageConfig()
const storageManager = builder.getStorageManager()
const normalizeStorageKey = createStorageKeyNormalizer(storageConfig)
const loggers = createPhotoProcessingLoggers(0, logger)
await runWithPhotoExecutionContext(
{
builder,
storageManager,
storageConfig,
normalizeStorageKey,
loggers,
},
async () => {
let attempted = 0
let updated = 0
const shouldOverwriteExisting = payload.options.isForceMode || payload.options.isForceManifest
for (const item of payload.manifest) {
if (!item) continue
if (item.location) continue
const { attempted: didAttempt, updated: didUpdate } = await resolveLocationForItem(
item,
item.exif,
state,
currentSettings,
provider,
shouldOverwriteExisting,
)
if (didAttempt) {
attempted++
if (didUpdate) {
updated++
}
}
}
if (attempted > 0) {
locationLogger.info(`📍 为 ${attempted} 张缺失位置信息的照片尝试补全,成功 ${updated}`)
}
},
)
},
},
}
}
export type { GeocodingPluginOptions }

View File

@@ -36,16 +36,8 @@ export interface SystemBuilderSettings {
observability: SystemObservabilitySettings
}
export interface UserGeocodingSettings {
enableGeocoding: boolean
geocodingProvider: 'mapbox' | 'nominatim' | 'auto'
mapboxToken?: string
nominatimBaseUrl?: string
}
export interface UserBuilderSettings {
storage: StorageConfig | null
geocoding: UserGeocodingSettings
}
export interface BuilderConfig {