配置改为数据库存储
将运行时 JSON 配置迁入 app_config_entries,启动时从文件导入并删除源文件。
This commit is contained in:
@@ -1,8 +1,12 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { APP_CONFIG_KEYS } from '../../../config/app-config-keys.js'
|
||||
import { PROJECT_ROOT } from '../../../config/runtime.js'
|
||||
import { createHttpError } from '../../../utils/http.js'
|
||||
import {
|
||||
readAppConfigEntry,
|
||||
saveAppConfigEntry,
|
||||
} from '../../config/app-config-store.js'
|
||||
import {
|
||||
DEFAULT_CLOUDTENTACLES_DEVICE_ID,
|
||||
DEFAULT_CLOUDTENTACLES_DEVICE_TYPE,
|
||||
@@ -22,47 +26,38 @@ export function getCloudtentaclesSessionFilePath() {
|
||||
return CLOUDTENTACLES_SESSION_FILE_PATH
|
||||
}
|
||||
|
||||
/**
|
||||
* Backward-compatible: returns the session for key='default'.
|
||||
* Old callers that expect a single session object still work.
|
||||
*/
|
||||
// 兼容旧调用:返回 key='default' 的会话。
|
||||
export function getCloudtentaclesSessionState() {
|
||||
const states = loadCloudtentaclesSessionStatesFromFile()
|
||||
const states = loadCloudtentaclesSessionStates()
|
||||
return states.sessions['default'] || createDefaultCloudtentaclesSessionState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a session by its sourceKey. Returns null if not found.
|
||||
*/
|
||||
// 按 sourceKey 获取会话,未命中时返回 null。
|
||||
export function getCloudtentaclesSessionStateByKey(sourceKey: unknown) {
|
||||
const states = loadCloudtentaclesSessionStatesFromFile()
|
||||
const states = loadCloudtentaclesSessionStates()
|
||||
const key = String(sourceKey || '').trim()
|
||||
if (!key) return null
|
||||
return states.sessions[key] || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the entire session states map: { sessions: { 'default': {...}, ... } }.
|
||||
*/
|
||||
// 返回所有账号的会话状态。
|
||||
export function getAllCloudtentaclesSessionStates() {
|
||||
return loadCloudtentaclesSessionStatesFromFile()
|
||||
return loadCloudtentaclesSessionStates()
|
||||
}
|
||||
|
||||
/**
|
||||
* Backward-compatible save: accepts both old single-object format
|
||||
* and new sessions-map format, normalizes, and persists.
|
||||
*/
|
||||
// 兼容旧保存格式:支持单会话对象和 sessions 映射。
|
||||
export function saveCloudtentaclesSessionState(rawValue: unknown) {
|
||||
const normalized = normalizeSessionStatesFile(rawValue)
|
||||
fs.mkdirSync(path.dirname(CLOUDTENTACLES_SESSION_FILE_PATH), { recursive: true })
|
||||
fs.writeFileSync(CLOUDTENTACLES_SESSION_FILE_PATH, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8')
|
||||
return normalized.sessions['default'] || createDefaultCloudtentaclesSessionState()
|
||||
return saveAppConfigEntry({
|
||||
configKey: APP_CONFIG_KEYS.cloudtentaclesSession,
|
||||
legacyFilePath: CLOUDTENTACLES_SESSION_FILE_PATH,
|
||||
value: normalized,
|
||||
normalize: normalizeSessionStatesFile,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a session for a specific sourceKey.
|
||||
*/
|
||||
export function saveCloudtentaclesSessionStateByKey(sourceKey: unknown, rawValue: unknown) {
|
||||
// 保存指定 sourceKey 的会话。
|
||||
export async function saveCloudtentaclesSessionStateByKey(sourceKey: unknown, rawValue: unknown) {
|
||||
const key = String(sourceKey || '').trim()
|
||||
if (!key) {
|
||||
throw createHttpError('cloudtentacles sourceKey 不能为空', {
|
||||
@@ -71,18 +66,15 @@ export function saveCloudtentaclesSessionStateByKey(sourceKey: unknown, rawValue
|
||||
})
|
||||
}
|
||||
|
||||
const states = loadCloudtentaclesSessionStatesFromFile()
|
||||
const states = loadCloudtentaclesSessionStates()
|
||||
states.sessions[key] = normalizeCloudtentaclesSessionState(rawValue)
|
||||
|
||||
fs.mkdirSync(path.dirname(CLOUDTENTACLES_SESSION_FILE_PATH), { recursive: true })
|
||||
fs.writeFileSync(CLOUDTENTACLES_SESSION_FILE_PATH, `${JSON.stringify(states, null, 2)}\n`, 'utf8')
|
||||
await saveCloudtentaclesSessionState(states)
|
||||
return states.sessions[key]
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a session by sourceKey.
|
||||
*/
|
||||
export function deleteCloudtentaclesSessionStateByKey(sourceKey: unknown) {
|
||||
// 删除指定 sourceKey 的会话。
|
||||
export async function deleteCloudtentaclesSessionStateByKey(sourceKey: unknown) {
|
||||
const key = String(sourceKey || '').trim()
|
||||
if (!key) {
|
||||
throw createHttpError('cloudtentacles sourceKey 不能为空', {
|
||||
@@ -91,25 +83,20 @@ export function deleteCloudtentaclesSessionStateByKey(sourceKey: unknown) {
|
||||
})
|
||||
}
|
||||
|
||||
const states = loadCloudtentaclesSessionStatesFromFile()
|
||||
const states = loadCloudtentaclesSessionStates()
|
||||
delete states.sessions[key]
|
||||
|
||||
fs.mkdirSync(path.dirname(CLOUDTENTACLES_SESSION_FILE_PATH), { recursive: true })
|
||||
fs.writeFileSync(CLOUDTENTACLES_SESSION_FILE_PATH, `${JSON.stringify(states, null, 2)}\n`, 'utf8')
|
||||
await saveCloudtentaclesSessionState(states)
|
||||
return states
|
||||
}
|
||||
|
||||
/**
|
||||
* Backward-compatible clear: clears the 'default' session only.
|
||||
*/
|
||||
// 兼容旧调用:只清理 default 会话。
|
||||
export function clearCloudtentaclesSessionState() {
|
||||
return clearCloudtentaclesSessionStateByKey('default')
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear a session by sourceKey (sets it to default empty state).
|
||||
*/
|
||||
export function clearCloudtentaclesSessionStateByKey(sourceKey: unknown) {
|
||||
// 清理指定 sourceKey 的会话,重置为空状态。
|
||||
export async function clearCloudtentaclesSessionStateByKey(sourceKey: unknown) {
|
||||
const key = String(sourceKey || '').trim()
|
||||
if (!key) {
|
||||
throw createHttpError('cloudtentacles sourceKey 不能为空', {
|
||||
@@ -119,50 +106,37 @@ export function clearCloudtentaclesSessionStateByKey(sourceKey: unknown) {
|
||||
}
|
||||
|
||||
const cleared = createDefaultCloudtentaclesSessionState()
|
||||
saveCloudtentaclesSessionStateByKey(key, cleared)
|
||||
await saveCloudtentaclesSessionStateByKey(key, cleared)
|
||||
return cleared
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep only sessions whose sourceKey still exists in source config.
|
||||
*/
|
||||
export function pruneCloudtentaclesSessionStates(sourceKeys: unknown[] = []) {
|
||||
// 只保留仍存在 source 配置的会话。
|
||||
export async function pruneCloudtentaclesSessionStates(sourceKeys: unknown[] = []) {
|
||||
const allowedKeys = new Set(
|
||||
Array.isArray(sourceKeys)
|
||||
? sourceKeys.map((value) => String(value || '').trim()).filter(Boolean)
|
||||
: []
|
||||
)
|
||||
|
||||
const states = loadCloudtentaclesSessionStatesFromFile()
|
||||
const states = loadCloudtentaclesSessionStates()
|
||||
states.sessions = Object.fromEntries(
|
||||
Object.entries(states.sessions || {}).filter(([key]) => allowedKeys.has(String(key || '').trim()))
|
||||
)
|
||||
|
||||
fs.mkdirSync(path.dirname(CLOUDTENTACLES_SESSION_FILE_PATH), { recursive: true })
|
||||
fs.writeFileSync(CLOUDTENTACLES_SESSION_FILE_PATH, `${JSON.stringify(states, null, 2)}\n`, 'utf8')
|
||||
await saveCloudtentaclesSessionState(states)
|
||||
return states
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function loadCloudtentaclesSessionStatesFromFile(): CloudtentaclesSessionStatesFile {
|
||||
if (!fs.existsSync(CLOUDTENTACLES_SESSION_FILE_PATH)) {
|
||||
return createDefaultCloudtentaclesSessionStates()
|
||||
}
|
||||
|
||||
try {
|
||||
const rawText = fs.readFileSync(CLOUDTENTACLES_SESSION_FILE_PATH, 'utf8')
|
||||
return normalizeSessionStatesFile(JSON.parse(rawText))
|
||||
} catch {
|
||||
return createDefaultCloudtentaclesSessionStates()
|
||||
}
|
||||
function loadCloudtentaclesSessionStates(): CloudtentaclesSessionStatesFile {
|
||||
return readAppConfigEntry({
|
||||
configKey: APP_CONFIG_KEYS.cloudtentaclesSession,
|
||||
legacyFilePath: CLOUDTENTACLES_SESSION_FILE_PATH,
|
||||
fallback: createDefaultCloudtentaclesSessionStates,
|
||||
normalize: normalizeSessionStatesFile,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a single session state item.
|
||||
*/
|
||||
// 标准化单个会话状态。
|
||||
function normalizeCloudtentaclesSessionState(rawValue: unknown) {
|
||||
const source = isPlainObject(rawValue) ? rawValue : {}
|
||||
|
||||
@@ -177,13 +151,8 @@ function normalizeCloudtentaclesSessionState(rawValue: unknown) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the overall session states file format.
|
||||
* Handles old format (single object without sessions key) by auto-wrapping
|
||||
* into { sessions: { 'default': ... } }.
|
||||
*/
|
||||
function normalizeSessionStatesFile(rawValue: unknown): CloudtentaclesSessionStatesFile {
|
||||
// Old format: { token: 'xxx', ... } (single object, no sessions key)
|
||||
// 标准化整体会话状态,同时兼容旧版单会话对象。
|
||||
export function normalizeSessionStatesFile(rawValue: unknown): CloudtentaclesSessionStatesFile {
|
||||
if (isPlainObject(rawValue) && !rawValue.sessions) {
|
||||
return {
|
||||
sessions: {
|
||||
@@ -192,7 +161,6 @@ function normalizeSessionStatesFile(rawValue: unknown): CloudtentaclesSessionSta
|
||||
}
|
||||
}
|
||||
|
||||
// New format: { sessions: { 'default': {...}, ... } }
|
||||
return {
|
||||
sessions: isPlainObject(rawValue) && isPlainObject(rawValue.sessions)
|
||||
? Object.fromEntries(
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { APP_CONFIG_KEYS } from '../../../config/app-config-keys.js'
|
||||
import { PROJECT_ROOT } from '../../../config/runtime.js'
|
||||
import { createHttpError } from '../../../utils/http.js'
|
||||
import {
|
||||
readAppConfigEntry,
|
||||
saveAppConfigEntry,
|
||||
} from '../../config/app-config-store.js'
|
||||
import {
|
||||
normalizeCloudtentaclesDeviceId,
|
||||
normalizeCloudtentaclesDeviceType,
|
||||
@@ -16,56 +20,44 @@ export function getCloudtentaclesSourcesFilePath() {
|
||||
return CLOUDTENTACLES_SOURCES_FILE_PATH
|
||||
}
|
||||
|
||||
/**
|
||||
* Backward-compatible: returns the source with key='default'.
|
||||
* Old callers that expect a single source object still work.
|
||||
*/
|
||||
// 兼容旧调用:返回 key='default' 的单账号配置。
|
||||
export function getCloudtentaclesSourceConfig() {
|
||||
const config = loadCloudtentaclesSourcesConfigFromFile()
|
||||
const config = loadCloudtentaclesSourcesConfig()
|
||||
const defaultSource = config.sources.find(s => s.key === 'default')
|
||||
return defaultSource || normalizeCloudtentaclesSourceItem({ key: 'default' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a source item by its key. Returns null if not found.
|
||||
*/
|
||||
// 按 sourceKey 获取账号配置,未命中时返回 null。
|
||||
export function getCloudtentaclesSourceByKey(sourceKey: unknown) {
|
||||
const config = loadCloudtentaclesSourcesConfigFromFile()
|
||||
const config = loadCloudtentaclesSourcesConfig()
|
||||
const key = String(sourceKey || '').trim()
|
||||
if (!key) return null
|
||||
return config.sources.find(s => s.key === key) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the entire normalized config: { enabled, sources }.
|
||||
*/
|
||||
// 返回完整的多账号配置。
|
||||
export function listCloudtentaclesSources() {
|
||||
return loadCloudtentaclesSourcesConfigFromFile()
|
||||
return loadCloudtentaclesSourcesConfig()
|
||||
}
|
||||
|
||||
/**
|
||||
* Backward-compatible save: accepts both old single-object format
|
||||
* and new list format, normalizes, and persists.
|
||||
*/
|
||||
// 兼容旧保存格式:支持单账号对象和 { enabled, sources } 多账号格式。
|
||||
export function saveCloudtentaclesSourceConfig(rawValue: unknown) {
|
||||
const normalized = normalizeCloudtentaclesSourcesConfig(rawValue)
|
||||
fs.mkdirSync(path.dirname(CLOUDTENTACLES_SOURCES_FILE_PATH), { recursive: true })
|
||||
fs.writeFileSync(CLOUDTENTACLES_SOURCES_FILE_PATH, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8')
|
||||
return normalized
|
||||
return saveAppConfigEntry({
|
||||
configKey: APP_CONFIG_KEYS.cloudtentaclesSources,
|
||||
legacyFilePath: CLOUDTENTACLES_SOURCES_FILE_PATH,
|
||||
value: normalized,
|
||||
normalize: normalizeCloudtentaclesSourcesConfig,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the entire list-format config object: { enabled, sources: [...] }.
|
||||
*/
|
||||
// 保存完整多账号配置。
|
||||
export function saveCloudtentaclesSourcesList(rawValue: unknown) {
|
||||
return saveCloudtentaclesSourceConfig(rawValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Save or update a single source item identified by sourceKey.
|
||||
* If a source with the same key exists, it is replaced; otherwise it is appended.
|
||||
*/
|
||||
export function saveCloudtentaclesSourceByKey(sourceKey: unknown, data: JsonObject = {}) {
|
||||
// 按 sourceKey 新增或替换单个账号配置。
|
||||
export async function saveCloudtentaclesSourceByKey(sourceKey: unknown, data: JsonObject = {}) {
|
||||
const key = String(sourceKey || '').trim()
|
||||
if (!key) {
|
||||
throw createHttpError('cloudtentacles sourceKey 不能为空', {
|
||||
@@ -74,7 +66,7 @@ export function saveCloudtentaclesSourceByKey(sourceKey: unknown, data: JsonObje
|
||||
})
|
||||
}
|
||||
|
||||
const config = loadCloudtentaclesSourcesConfigFromFile()
|
||||
const config = loadCloudtentaclesSourcesConfig()
|
||||
const normalizedItem = normalizeCloudtentaclesSourceItem({ ...data, key })
|
||||
const existingIndex = config.sources.findIndex(s => s.key === key)
|
||||
|
||||
@@ -84,15 +76,12 @@ export function saveCloudtentaclesSourceByKey(sourceKey: unknown, data: JsonObje
|
||||
config.sources.push(normalizedItem)
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(CLOUDTENTACLES_SOURCES_FILE_PATH), { recursive: true })
|
||||
fs.writeFileSync(CLOUDTENTACLES_SOURCES_FILE_PATH, `${JSON.stringify(config, null, 2)}\n`, 'utf8')
|
||||
return config
|
||||
await saveCloudtentaclesSourceConfig(config)
|
||||
return normalizedItem
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a single source by key.
|
||||
*/
|
||||
export function deleteCloudtentaclesSourceByKey(sourceKey: unknown) {
|
||||
// 删除单个账号配置。
|
||||
export async function deleteCloudtentaclesSourceByKey(sourceKey: unknown) {
|
||||
const key = String(sourceKey || '').trim()
|
||||
if (!key) {
|
||||
throw createHttpError('cloudtentacles sourceKey 不能为空', {
|
||||
@@ -101,7 +90,7 @@ export function deleteCloudtentaclesSourceByKey(sourceKey: unknown) {
|
||||
})
|
||||
}
|
||||
|
||||
const config = loadCloudtentaclesSourcesConfigFromFile()
|
||||
const config = loadCloudtentaclesSourcesConfig()
|
||||
const existingIndex = config.sources.findIndex(s => s.key === key)
|
||||
|
||||
if (existingIndex < 0) {
|
||||
@@ -110,31 +99,20 @@ export function deleteCloudtentaclesSourceByKey(sourceKey: unknown) {
|
||||
|
||||
config.sources.splice(existingIndex, 1)
|
||||
|
||||
fs.mkdirSync(path.dirname(CLOUDTENTACLES_SOURCES_FILE_PATH), { recursive: true })
|
||||
fs.writeFileSync(CLOUDTENTACLES_SOURCES_FILE_PATH, `${JSON.stringify(config, null, 2)}\n`, 'utf8')
|
||||
await saveCloudtentaclesSourceConfig(config)
|
||||
return config
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function loadCloudtentaclesSourcesConfigFromFile() {
|
||||
if (!fs.existsSync(CLOUDTENTACLES_SOURCES_FILE_PATH)) {
|
||||
return createDefaultCloudtentaclesSourcesConfig()
|
||||
}
|
||||
|
||||
try {
|
||||
const rawText = fs.readFileSync(CLOUDTENTACLES_SOURCES_FILE_PATH, 'utf8')
|
||||
return normalizeCloudtentaclesSourcesConfig(JSON.parse(rawText))
|
||||
} catch {
|
||||
return createDefaultCloudtentaclesSourcesConfig()
|
||||
}
|
||||
function loadCloudtentaclesSourcesConfig() {
|
||||
return readAppConfigEntry({
|
||||
configKey: APP_CONFIG_KEYS.cloudtentaclesSources,
|
||||
legacyFilePath: CLOUDTENTACLES_SOURCES_FILE_PATH,
|
||||
fallback: createDefaultCloudtentaclesSourcesConfig,
|
||||
normalize: normalizeCloudtentaclesSourcesConfig,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a single source item. Adds key (required) and label (optional).
|
||||
*/
|
||||
// 标准化单个账号配置,补齐 key 和 label。
|
||||
function normalizeCloudtentaclesSourceItem(rawValue: unknown) {
|
||||
const source = isPlainObject(rawValue) ? rawValue : {}
|
||||
|
||||
@@ -151,12 +129,8 @@ function normalizeCloudtentaclesSourceItem(rawValue: unknown) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the overall config. Handles both old single-object format
|
||||
* (auto-migrates to new list format) and new { enabled, sources } format.
|
||||
*/
|
||||
function normalizeCloudtentaclesSourcesConfig(rawValue: unknown) {
|
||||
// Old format: { enabled: true, username: 'xxx', ... } (single object, no sources array)
|
||||
// 标准化整体配置,同时兼容旧版单账号对象。
|
||||
export function normalizeCloudtentaclesSourcesConfig(rawValue: unknown) {
|
||||
if (isPlainObject(rawValue) && !Array.isArray(rawValue.sources)) {
|
||||
return {
|
||||
enabled: rawValue.enabled !== false,
|
||||
@@ -164,13 +138,12 @@ function normalizeCloudtentaclesSourcesConfig(rawValue: unknown) {
|
||||
normalizeCloudtentaclesSourceItem({
|
||||
key: 'default',
|
||||
label: '默认账号',
|
||||
...rawValue, // old fields auto-map to default source
|
||||
...rawValue, // 旧字段自动映射为 default 账号。
|
||||
}),
|
||||
].filter(Boolean),
|
||||
}
|
||||
}
|
||||
|
||||
// New format: { enabled, sources: [...] }
|
||||
return {
|
||||
enabled: isPlainObject(rawValue) ? rawValue.enabled !== false : true,
|
||||
sources: isPlainObject(rawValue) && Array.isArray(rawValue.sources)
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { APP_CONFIG_KEYS } from '../../../config/app-config-keys.js'
|
||||
import { PROJECT_ROOT } from '../../../config/runtime.js'
|
||||
import { readJsonFile, writeJsonFile } from '../../../utils/json-file-store.js'
|
||||
import {
|
||||
hasAppConfigEntry,
|
||||
readAppConfigEntry,
|
||||
saveAppConfigEntry,
|
||||
} from '../../config/app-config-store.js'
|
||||
import { normalizeCloudtentaclesMatchName } from '../../order/cloudtentacles-match-utils.js'
|
||||
import type { KuaishouFeifeiProductRule } from '../../../types/runtime-config.js'
|
||||
|
||||
@@ -29,23 +33,28 @@ export function getKuaishouFeifeiConfigFilePath() {
|
||||
}
|
||||
|
||||
export function hasKuaishouFeifeiConfigFile() {
|
||||
return fs.existsSync(KUAISHOU_FEIFEI_CONFIG_FILE_PATH)
|
||||
return hasAppConfigEntry(
|
||||
APP_CONFIG_KEYS.kuaishouFeifei,
|
||||
KUAISHOU_FEIFEI_CONFIG_FILE_PATH,
|
||||
)
|
||||
}
|
||||
|
||||
export function getKuaishouFeifeiSourceConfig(): KuaishouFeifeiSourceConfig {
|
||||
return readJsonFile(
|
||||
KUAISHOU_FEIFEI_CONFIG_FILE_PATH,
|
||||
createDefaultKuaishouFeifeiSourceConfig,
|
||||
normalizeKuaishouFeifeiSourceConfig,
|
||||
)
|
||||
return readAppConfigEntry({
|
||||
configKey: APP_CONFIG_KEYS.kuaishouFeifei,
|
||||
legacyFilePath: KUAISHOU_FEIFEI_CONFIG_FILE_PATH,
|
||||
fallback: createDefaultKuaishouFeifeiSourceConfig,
|
||||
normalize: normalizeKuaishouFeifeiSourceConfig,
|
||||
})
|
||||
}
|
||||
|
||||
export function saveKuaishouFeifeiSourceConfig(rawValue: unknown): KuaishouFeifeiSourceConfig {
|
||||
return writeJsonFile(
|
||||
KUAISHOU_FEIFEI_CONFIG_FILE_PATH,
|
||||
rawValue,
|
||||
normalizeKuaishouFeifeiSourceConfig,
|
||||
)
|
||||
export function saveKuaishouFeifeiSourceConfig(rawValue: unknown): Promise<KuaishouFeifeiSourceConfig> {
|
||||
return saveAppConfigEntry({
|
||||
configKey: APP_CONFIG_KEYS.kuaishouFeifei,
|
||||
legacyFilePath: KUAISHOU_FEIFEI_CONFIG_FILE_PATH,
|
||||
value: rawValue,
|
||||
normalize: normalizeKuaishouFeifeiSourceConfig,
|
||||
})
|
||||
}
|
||||
|
||||
export function normalizeKuaishouFeifeiSourceConfig(rawValue: unknown): KuaishouFeifeiSourceConfig {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { APP_CONFIG_KEYS } from '../../../config/app-config-keys.js'
|
||||
import { PROJECT_ROOT, runtimeConfig } from '../../../config/runtime.js'
|
||||
import { readJsonFile, writeJsonFile } from '../../../utils/json-file-store.js'
|
||||
import {
|
||||
readAppConfigEntry,
|
||||
saveAppConfigEntry,
|
||||
} from '../../config/app-config-store.js'
|
||||
|
||||
const KUAISHOU_INDUSTRY_SOURCE_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'kuaishou-industry-source.json')
|
||||
const DEFAULT_CALLBACK_BASE_URL = 'https://openapi.kwaixiaodian.com'
|
||||
@@ -62,24 +66,26 @@ export function getKuaishouIndustrySourceFilePath(): string {
|
||||
}
|
||||
|
||||
export function getKuaishouIndustrySourceConfig(): KuaishouIndustrySourceConfig {
|
||||
return readJsonFile(
|
||||
KUAISHOU_INDUSTRY_SOURCE_FILE_PATH,
|
||||
createDefaultKuaishouIndustrySourceConfig,
|
||||
normalizeKuaishouIndustrySourceConfig,
|
||||
)
|
||||
return readAppConfigEntry({
|
||||
configKey: APP_CONFIG_KEYS.kuaishouIndustrySource,
|
||||
legacyFilePath: KUAISHOU_INDUSTRY_SOURCE_FILE_PATH,
|
||||
fallback: createDefaultKuaishouIndustrySourceConfig,
|
||||
normalize: normalizeKuaishouIndustrySourceConfig,
|
||||
})
|
||||
}
|
||||
|
||||
export function saveKuaishouIndustrySourceConfig(rawValue: unknown): KuaishouIndustrySourceConfig {
|
||||
return writeJsonFile(
|
||||
KUAISHOU_INDUSTRY_SOURCE_FILE_PATH,
|
||||
rawValue,
|
||||
normalizeKuaishouIndustrySourceConfig,
|
||||
)
|
||||
export function saveKuaishouIndustrySourceConfig(rawValue: unknown): Promise<KuaishouIndustrySourceConfig> {
|
||||
return saveAppConfigEntry({
|
||||
configKey: APP_CONFIG_KEYS.kuaishouIndustrySource,
|
||||
legacyFilePath: KUAISHOU_INDUSTRY_SOURCE_FILE_PATH,
|
||||
value: rawValue,
|
||||
normalize: normalizeKuaishouIndustrySourceConfig,
|
||||
})
|
||||
}
|
||||
|
||||
export function patchKuaishouIndustrySourceConfig(
|
||||
patch: Partial<KuaishouIndustrySourceConfig>,
|
||||
): KuaishouIndustrySourceConfig {
|
||||
): Promise<KuaishouIndustrySourceConfig> {
|
||||
return saveKuaishouIndustrySourceConfig({
|
||||
...getKuaishouIndustrySourceConfig(),
|
||||
...patch,
|
||||
@@ -108,7 +114,7 @@ export function findKuaishouIndustryShopConfig(
|
||||
export function patchKuaishouIndustryShopConfig(
|
||||
sellerId: unknown,
|
||||
patch: Partial<KuaishouIndustryShopConfig>,
|
||||
): KuaishouIndustrySourceConfig {
|
||||
): Promise<KuaishouIndustrySourceConfig> {
|
||||
const source = getKuaishouIndustrySourceConfig()
|
||||
const normalizedSellerId = String(patch.sellerId || sellerId || '').trim()
|
||||
const existing = findKuaishouIndustryShopConfig(normalizedSellerId, source)
|
||||
@@ -119,7 +125,7 @@ export function patchKuaishouIndustryShopConfig(
|
||||
})
|
||||
|
||||
if (!nextShop) {
|
||||
return source
|
||||
return Promise.resolve(source)
|
||||
}
|
||||
|
||||
const shops = listKuaishouIndustryShopConfigs(source)
|
||||
@@ -131,7 +137,7 @@ export function patchKuaishouIndustryShopConfig(
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeKuaishouIndustrySourceConfig(rawValue: unknown): KuaishouIndustrySourceConfig {
|
||||
export function normalizeKuaishouIndustrySourceConfig(rawValue: unknown): KuaishouIndustrySourceConfig {
|
||||
const fallback = createDefaultKuaishouIndustrySourceConfig()
|
||||
const source = isPlainObject(rawValue) ? rawValue : {}
|
||||
const shops = normalizeKuaishouIndustryShopList(source)
|
||||
|
||||
@@ -76,7 +76,7 @@ export async function refreshKuaishouIndustryAccessToken(
|
||||
failureMessage: '快手 accessToken 刷新失败',
|
||||
errorCode: 'kuaishou_industry_access_token_refresh_failed',
|
||||
})
|
||||
const saved = saveTokenPayload(tokenPayload, config, shop)
|
||||
const saved = await saveTokenPayload(tokenPayload, config, shop)
|
||||
|
||||
logInfo('[kuaishou-industry/token]', 'accessToken 刷新成功', {
|
||||
durationMs: Date.now() - startedAt,
|
||||
@@ -94,10 +94,10 @@ export async function refreshKuaishouIndustryAccessToken(
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
patchKuaishouIndustryShopConfig(shop.sellerId, {
|
||||
await patchKuaishouIndustryShopConfig(shop.sellerId, {
|
||||
lastRefreshError: message,
|
||||
})
|
||||
patchKuaishouIndustrySourceConfig({
|
||||
await patchKuaishouIndustrySourceConfig({
|
||||
lastRefreshError: message,
|
||||
})
|
||||
logWarn('[kuaishou-industry/token]', 'accessToken 刷新失败', resolveRefreshErrorDetail(error))
|
||||
@@ -133,7 +133,7 @@ export async function exchangeKuaishouIndustryAuthorizationCode(
|
||||
failureMessage: '快手授权码换取 accessToken 失败',
|
||||
errorCode: 'kuaishou_industry_authorization_code_exchange_failed',
|
||||
})
|
||||
const saved = saveTokenPayload(tokenPayload, config, {
|
||||
const saved = await saveTokenPayload(tokenPayload, config, {
|
||||
...createEmptyShopConfig(),
|
||||
sellerId: String(options.sellerId || '').trim(),
|
||||
shopId: String(options.sellerId || '').trim(),
|
||||
@@ -157,7 +157,7 @@ export async function exchangeKuaishouIndustryAuthorizationCode(
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
patchKuaishouIndustrySourceConfig({
|
||||
await patchKuaishouIndustrySourceConfig({
|
||||
lastRefreshError: message,
|
||||
})
|
||||
logWarn('[kuaishou-industry/token]', '授权码换取 accessToken 失败', resolveRefreshErrorDetail(error))
|
||||
@@ -314,7 +314,7 @@ async function requestKuaishouIndustryToken({
|
||||
|
||||
if (!response.ok || !isTokenResponseSuccess(json) || !tokenPayload.accessToken) {
|
||||
const message = resolveTokenErrorMessage(json, response.status, failureMessage)
|
||||
patchKuaishouIndustrySourceConfig({
|
||||
await patchKuaishouIndustrySourceConfig({
|
||||
lastRefreshError: message,
|
||||
})
|
||||
throw createHttpError(message, {
|
||||
@@ -326,7 +326,7 @@ async function requestKuaishouIndustryToken({
|
||||
return tokenPayload
|
||||
}
|
||||
|
||||
function saveTokenPayload(
|
||||
async function saveTokenPayload(
|
||||
tokenPayload: ReturnType<typeof normalizeTokenResponse>,
|
||||
config: KuaishouIndustrySourceConfig,
|
||||
shop: KuaishouIndustryShopConfig,
|
||||
@@ -340,7 +340,7 @@ function saveTokenPayload(
|
||||
}
|
||||
|
||||
const nextRefreshToken = tokenPayload.refreshToken || shop.refreshToken
|
||||
const savedConfig = patchKuaishouIndustryShopConfig(nextSellerId, {
|
||||
const savedConfig = await patchKuaishouIndustryShopConfig(nextSellerId, {
|
||||
...shop,
|
||||
enabled: shop.enabled !== false,
|
||||
sellerId: nextSellerId,
|
||||
|
||||
Reference in New Issue
Block a user