优化后端鉴权与日志安全
This commit is contained in:
@@ -6,6 +6,7 @@ CREATE TABLE IF NOT EXISTS admin_users (
|
|||||||
password_hash TEXT NOT NULL,
|
password_hash TEXT NOT NULL,
|
||||||
role TEXT NOT NULL,
|
role TEXT NOT NULL,
|
||||||
status TEXT NOT NULL,
|
status TEXT NOT NULL,
|
||||||
|
session_version INTEGER NOT NULL DEFAULT 1,
|
||||||
created_at TIMESTAMPTZ NOT NULL,
|
created_at TIMESTAMPTZ NOT NULL,
|
||||||
updated_at TIMESTAMPTZ NOT NULL
|
updated_at TIMESTAMPTZ NOT NULL
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE admin_users
|
||||||
|
ADD COLUMN IF NOT EXISTS session_version INTEGER NOT NULL DEFAULT 1;
|
||||||
@@ -6,6 +6,7 @@ type AdminUserRow = {
|
|||||||
password_hash: string
|
password_hash: string
|
||||||
role: string
|
role: string
|
||||||
status: string
|
status: string
|
||||||
|
session_version: number
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
}
|
}
|
||||||
@@ -21,7 +22,7 @@ type AdminUserCreateInput = {
|
|||||||
|
|
||||||
type AdminUserPatch = Partial<Pick<
|
type AdminUserPatch = Partial<Pick<
|
||||||
AdminUserRow,
|
AdminUserRow,
|
||||||
'username' | 'password_hash' | 'role' | 'status' | 'updated_at'
|
'username' | 'password_hash' | 'role' | 'status' | 'session_version' | 'updated_at'
|
||||||
>>
|
>>
|
||||||
|
|
||||||
type AdminUserListInput = {
|
type AdminUserListInput = {
|
||||||
@@ -66,9 +67,10 @@ export async function createAdminUser(input: AdminUserCreateInput): Promise<Admi
|
|||||||
password_hash,
|
password_hash,
|
||||||
role,
|
role,
|
||||||
status,
|
status,
|
||||||
|
session_version,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
) VALUES ($1, $2, $3, $4, $5, $6)
|
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
@@ -76,6 +78,7 @@ export async function createAdminUser(input: AdminUserCreateInput): Promise<Admi
|
|||||||
input.passwordHash,
|
input.passwordHash,
|
||||||
input.role,
|
input.role,
|
||||||
input.status,
|
input.status,
|
||||||
|
1,
|
||||||
input.createdAt,
|
input.createdAt,
|
||||||
input.updatedAt,
|
input.updatedAt,
|
||||||
],
|
],
|
||||||
@@ -102,8 +105,9 @@ export async function updateAdminUser(
|
|||||||
password_hash = $2,
|
password_hash = $2,
|
||||||
role = $3,
|
role = $3,
|
||||||
status = $4,
|
status = $4,
|
||||||
updated_at = $5
|
session_version = $5,
|
||||||
WHERE id = $6
|
updated_at = $6
|
||||||
|
WHERE id = $7
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
@@ -111,6 +115,7 @@ export async function updateAdminUser(
|
|||||||
next.password_hash,
|
next.password_hash,
|
||||||
next.role,
|
next.role,
|
||||||
next.status,
|
next.status,
|
||||||
|
Number(next.session_version || 1),
|
||||||
next.updated_at,
|
next.updated_at,
|
||||||
Number(userId),
|
Number(userId),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ type FulfillmentProfileUpsertInput = {
|
|||||||
updatedAt: string
|
updatedAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type FulfillmentProfileRequirementInput = {
|
export type FulfillmentProfileRequirementInput = {
|
||||||
roleKey: string
|
roleKey: string
|
||||||
credentialType: string
|
credentialType: string
|
||||||
quantityPerUnit?: number | string
|
quantityPerUnit?: number | string
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export type AdminSession = {
|
|||||||
username: string
|
username: string
|
||||||
role: AdminRole
|
role: AdminRole
|
||||||
expiresAt: string
|
expiresAt: string
|
||||||
|
sessionVersion: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type AdminRole = 'admin' | 'operator' | 'support'
|
type AdminRole = 'admin' | 'operator' | 'support'
|
||||||
@@ -137,12 +138,22 @@ export async function verifyAdminSessionToken(token: unknown): Promise<AdminSess
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tokenSessionVersion = Number(payload?.ver || 0)
|
||||||
|
const currentSessionVersion = normalizeAdminSessionVersion(user.session_version)
|
||||||
|
if (tokenSessionVersion !== currentSessionVersion) {
|
||||||
|
throw createHttpError('后台登录态已失效,请重新登录', {
|
||||||
|
statusCode: 401,
|
||||||
|
errorCode: 'admin_auth_stale',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sessionId: String(payload?.sid || '').trim(),
|
sessionId: String(payload?.sid || '').trim(),
|
||||||
userId: Number(user.id),
|
userId: Number(user.id),
|
||||||
username: String(user.username || ''),
|
username: String(user.username || ''),
|
||||||
role: normalizeAdminRole(user.role),
|
role: normalizeAdminRole(user.role),
|
||||||
expiresAt,
|
expiresAt,
|
||||||
|
sessionVersion: currentSessionVersion,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,6 +259,7 @@ export async function updateManagedAdminUserRole(
|
|||||||
await ensureAdminUserChangeAllowed(user, { nextRole: role }, session)
|
await ensureAdminUserChangeAllowed(user, { nextRole: role }, session)
|
||||||
const updated = await updateAdminUser(user.id, {
|
const updated = await updateAdminUser(user.id, {
|
||||||
role,
|
role,
|
||||||
|
session_version: nextAdminSessionVersion(user),
|
||||||
updated_at: nowIso(),
|
updated_at: nowIso(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -273,6 +285,7 @@ export async function updateManagedAdminUserStatus(
|
|||||||
await ensureAdminUserChangeAllowed(user, { nextStatus: status }, session)
|
await ensureAdminUserChangeAllowed(user, { nextStatus: status }, session)
|
||||||
const updated = await updateAdminUser(user.id, {
|
const updated = await updateAdminUser(user.id, {
|
||||||
status,
|
status,
|
||||||
|
session_version: nextAdminSessionVersion(user),
|
||||||
updated_at: nowIso(),
|
updated_at: nowIso(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -295,6 +308,7 @@ export async function resetManagedAdminUserPassword(userId: number | string, pay
|
|||||||
validatePassword(password)
|
validatePassword(password)
|
||||||
const updated = await updateAdminUser(user.id, {
|
const updated = await updateAdminUser(user.id, {
|
||||||
password_hash: hashAdminPassword(password),
|
password_hash: hashAdminPassword(password),
|
||||||
|
session_version: nextAdminSessionVersion(user),
|
||||||
updated_at: nowIso(),
|
updated_at: nowIso(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -325,6 +339,7 @@ function createAdminSession(user: AdminUserRow): JsonObject {
|
|||||||
uid: Number(user.id),
|
uid: Number(user.id),
|
||||||
usr: String(user.username || ''),
|
usr: String(user.username || ''),
|
||||||
role: normalizeAdminRole(user.role),
|
role: normalizeAdminRole(user.role),
|
||||||
|
ver: normalizeAdminSessionVersion(user.session_version),
|
||||||
iat: issuedAt,
|
iat: issuedAt,
|
||||||
exp: expiresAt,
|
exp: expiresAt,
|
||||||
}
|
}
|
||||||
@@ -430,6 +445,15 @@ function validatePassword(password: string): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeAdminSessionVersion(value: unknown): number {
|
||||||
|
const parsed = Number(value)
|
||||||
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextAdminSessionVersion(user: AdminUserRow): number {
|
||||||
|
return normalizeAdminSessionVersion(user.session_version) + 1
|
||||||
|
}
|
||||||
|
|
||||||
async function getRequiredAdminUser(userId: number | string): Promise<AdminUserRow> {
|
async function getRequiredAdminUser(userId: number | string): Promise<AdminUserRow> {
|
||||||
const user = await getAdminUserById(Number(userId))
|
const user = await getAdminUserById(Number(userId))
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export function mapKuaishouCloudFulfillmentContext(value: unknown): JsonRecord |
|
|||||||
prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending',
|
prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending',
|
||||||
cloudSourceKey: String(binding.cloudSourceKey || '').trim(),
|
cloudSourceKey: String(binding.cloudSourceKey || '').trim(),
|
||||||
cloudSourceKeyFallbacks: Array.isArray(binding.cloudSourceKeyFallbacks)
|
cloudSourceKeyFallbacks: Array.isArray(binding.cloudSourceKeyFallbacks)
|
||||||
? binding.cloudSourceKeyFallbacks.map((value) => String(value || '').trim()).filter(Boolean)
|
? binding.cloudSourceKeyFallbacks.map((value: unknown) => String(value || '').trim()).filter(Boolean)
|
||||||
: [],
|
: [],
|
||||||
resolvedSourceKey: String(binding.resolvedSourceKey || '').trim(),
|
resolvedSourceKey: String(binding.resolvedSourceKey || '').trim(),
|
||||||
skuId: Number(binding.skuId || 0) || 0,
|
skuId: Number(binding.skuId || 0) || 0,
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ function mapAdminNotificationConfig(config: JsonObject = {}) {
|
|||||||
bark: {
|
bark: {
|
||||||
enabled: bark.enabled !== false,
|
enabled: bark.enabled !== false,
|
||||||
serverUrl: String(bark.serverUrl || 'https://api.day.app').trim(),
|
serverUrl: String(bark.serverUrl || 'https://api.day.app').trim(),
|
||||||
recipients: (Array.isArray(bark.recipients) ? bark.recipients : []).map((item) => ({
|
recipients: (Array.isArray(bark.recipients) ? bark.recipients : []).map((item: JsonObject) => ({
|
||||||
id: String(item.id || '').trim(),
|
id: String(item.id || '').trim(),
|
||||||
name: String(item.name || '').trim(),
|
name: String(item.name || '').trim(),
|
||||||
deviceKey: String(item.deviceKey || '').trim(),
|
deviceKey: String(item.deviceKey || '').trim(),
|
||||||
@@ -105,7 +105,7 @@ function mapAdminNotificationConfig(config: JsonObject = {}) {
|
|||||||
},
|
},
|
||||||
wpush: {
|
wpush: {
|
||||||
enabled: wpush.enabled !== false,
|
enabled: wpush.enabled !== false,
|
||||||
recipients: (Array.isArray(wpush.recipients) ? wpush.recipients : []).map((item) => ({
|
recipients: (Array.isArray(wpush.recipients) ? wpush.recipients : []).map((item: JsonObject) => ({
|
||||||
id: String(item.id || '').trim(),
|
id: String(item.id || '').trim(),
|
||||||
name: String(item.name || '').trim(),
|
name: String(item.name || '').trim(),
|
||||||
apiKey: String(item.apiKey || item.apikey || '').trim(),
|
apiKey: String(item.apiKey || item.apikey || '').trim(),
|
||||||
@@ -146,7 +146,7 @@ function mapAdminScheduledJobsConfig(config: JsonObject = {}) {
|
|||||||
function listAdminCloudtentaclesMonitorAccounts() {
|
function listAdminCloudtentaclesMonitorAccounts() {
|
||||||
const sourcesConfig = listCloudtentaclesSources()
|
const sourcesConfig = listCloudtentaclesSources()
|
||||||
const sessionsConfig = getAllCloudtentaclesSessionStates()
|
const sessionsConfig = getAllCloudtentaclesSessionStates()
|
||||||
const sessions = sessionsConfig.sessions || {}
|
const sessions: Record<string, JsonObject> = sessionsConfig.sessions || {}
|
||||||
|
|
||||||
return (Array.isArray(sourcesConfig.sources) ? sourcesConfig.sources : []).map((source) => {
|
return (Array.isArray(sourcesConfig.sources) ? sourcesConfig.sources : []).map((source) => {
|
||||||
const sourceKey = String(source.key || '').trim()
|
const sourceKey = String(source.key || '').trim()
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ export async function prepareAdminTaskKuaishouCloudFulfillment(
|
|||||||
bindUrl: preparedBinding.bindUrl,
|
bindUrl: preparedBinding.bindUrl,
|
||||||
bindPreparedAt: now,
|
bindPreparedAt: now,
|
||||||
bindExpiresAt: resolveKuaishouCloudBindUrlExpiresAt(now),
|
bindExpiresAt: resolveKuaishouCloudBindUrlExpiresAt(now),
|
||||||
bindProbeAt: null,
|
bindProbeAt: null as null,
|
||||||
bindProbeStatus: 'pending',
|
bindProbeStatus: 'pending',
|
||||||
bindProbeMessage: '',
|
bindProbeMessage: '',
|
||||||
roleName: '',
|
roleName: '',
|
||||||
@@ -181,9 +181,9 @@ export async function prepareAdminTaskKuaishouCloudFulfillment(
|
|||||||
status: 'pending',
|
status: 'pending',
|
||||||
name: '',
|
name: '',
|
||||||
rid: '',
|
rid: '',
|
||||||
refreshedAt: null,
|
refreshedAt: null as null,
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
rawInfo: null,
|
rawInfo: null as null,
|
||||||
},
|
},
|
||||||
purchase: {
|
purchase: {
|
||||||
...flowWithResolvedBinding.purchase,
|
...flowWithResolvedBinding.purchase,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
replaceFulfillmentProfileRequirements,
|
replaceFulfillmentProfileRequirements,
|
||||||
upsertFulfillmentProfile,
|
upsertFulfillmentProfile,
|
||||||
upsertSkuFulfillmentBinding,
|
upsertSkuFulfillmentBinding,
|
||||||
|
type FulfillmentProfileRequirementInput,
|
||||||
} from '../../repositories/fulfillment-profile-repo.js'
|
} from '../../repositories/fulfillment-profile-repo.js'
|
||||||
import { upsertProductMatchRule } from '../../repositories/product-match-rule-repo.js'
|
import { upsertProductMatchRule } from '../../repositories/product-match-rule-repo.js'
|
||||||
import { normalizeProductName } from '../order/product-match-service.js'
|
import { normalizeProductName } from '../order/product-match-service.js'
|
||||||
@@ -14,7 +15,18 @@ import {
|
|||||||
} from '../order/kuaishou-cloud-fulfillment-config-service.js'
|
} from '../order/kuaishou-cloud-fulfillment-config-service.js'
|
||||||
import { nowIso } from '../../utils/time.js'
|
import { nowIso } from '../../utils/time.js'
|
||||||
|
|
||||||
const CORE_PROFILES = [
|
type JsonObject = Record<string, any>
|
||||||
|
type CoreProfile = {
|
||||||
|
profileKey: string
|
||||||
|
name: string
|
||||||
|
executorKey: string
|
||||||
|
requiresClaim: boolean
|
||||||
|
autoDispatch: boolean
|
||||||
|
inventoryStrategy: string
|
||||||
|
requirements: FulfillmentProfileRequirementInput[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const CORE_PROFILES: CoreProfile[] = [
|
||||||
{
|
{
|
||||||
profileKey: 'manual_review',
|
profileKey: 'manual_review',
|
||||||
name: '人工发货',
|
name: '人工发货',
|
||||||
@@ -35,8 +47,6 @@ const CORE_PROFILES = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
type JsonObject = Record<string, any>
|
|
||||||
|
|
||||||
export async function ensureFulfillmentCatalogBootstrapped() {
|
export async function ensureFulfillmentCatalogBootstrapped() {
|
||||||
const profileMap = await ensureCoreProfiles()
|
const profileMap = await ensureCoreProfiles()
|
||||||
await syncConfiguredFulfillmentBindings(profileMap)
|
await syncConfiguredFulfillmentBindings(profileMap)
|
||||||
@@ -44,7 +54,7 @@ export async function ensureFulfillmentCatalogBootstrapped() {
|
|||||||
|
|
||||||
async function ensureCoreProfiles() {
|
async function ensureCoreProfiles() {
|
||||||
const timestamp = nowIso()
|
const timestamp = nowIso()
|
||||||
const profileMap = {}
|
const profileMap: JsonObject = {}
|
||||||
|
|
||||||
for (const profile of CORE_PROFILES) {
|
for (const profile of CORE_PROFILES) {
|
||||||
const saved = await upsertFulfillmentProfile({
|
const saved = await upsertFulfillmentProfile({
|
||||||
@@ -148,6 +158,6 @@ function resolveBindingRuntimeConfig(binding: JsonObject = {}) {
|
|||||||
return baseConfig
|
return baseConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPlainObject(value) {
|
function isPlainObject(value: unknown): value is JsonObject {
|
||||||
return Object.prototype.toString.call(value) === '[object Object]'
|
return Object.prototype.toString.call(value) === '[object Object]'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,19 @@ import { formatFenToAmount, normalizeFen } from '../../utils/money.js'
|
|||||||
import { parseTaskContext as parseTaskContextValue } from '../../utils/task-json.js'
|
import { parseTaskContext as parseTaskContextValue } from '../../utils/task-json.js'
|
||||||
import { nowIso } from '../../utils/time.js'
|
import { nowIso } from '../../utils/time.js'
|
||||||
import { buildClaimUrl } from './claim-service.js'
|
import { buildClaimUrl } from './claim-service.js'
|
||||||
|
import type { ClaimTokenRow, OrderItemRow, OrderRow, TaskRow } from '../../types/repository-rows.js'
|
||||||
|
|
||||||
export const CLAIM_TERMINAL_STATUSES = new Set(['expired', 'closed'])
|
export const CLAIM_TERMINAL_STATUSES = new Set(['expired', 'closed'])
|
||||||
export const KUAISHOU_CLOUD_CLAIM_GUIDE_BASE_PATH = '/kuaishou-cloud-guide'
|
export const KUAISHOU_CLOUD_CLAIM_GUIDE_BASE_PATH = '/kuaishou-cloud-guide'
|
||||||
|
|
||||||
|
type JsonObject = Record<string, any>
|
||||||
|
type ClaimContext = {
|
||||||
|
claimToken: ClaimTokenRow
|
||||||
|
task: TaskRow
|
||||||
|
order: OrderRow
|
||||||
|
orderItem: OrderItemRow
|
||||||
|
}
|
||||||
|
|
||||||
export async function getClaimContext(token: unknown) {
|
export async function getClaimContext(token: unknown) {
|
||||||
const normalized = String(token || '').trim()
|
const normalized = String(token || '').trim()
|
||||||
|
|
||||||
@@ -76,7 +85,7 @@ export async function getClaimContext(token: unknown) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildClaimDetailPayload({ claimToken, task, order, orderItem }) {
|
export function buildClaimDetailPayload({ claimToken, task, order, orderItem }: ClaimContext) {
|
||||||
const kuaishouCloudFulfillment = mapClaimKuaishouCloudFulfillment(task, order)
|
const kuaishouCloudFulfillment = mapClaimKuaishouCloudFulfillment(task, order)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -113,7 +122,7 @@ export function buildClaimDetailPayload({ claimToken, task, order, orderItem })
|
|||||||
skuName: orderItem.sku_name,
|
skuName: orderItem.sku_name,
|
||||||
quantity: orderItem.quantity,
|
quantity: orderItem.quantity,
|
||||||
},
|
},
|
||||||
session: null,
|
session: null as null,
|
||||||
kuaishouCloudFulfillment,
|
kuaishouCloudFulfillment,
|
||||||
result: task.redeemed_at
|
result: task.redeemed_at
|
||||||
? {
|
? {
|
||||||
@@ -126,7 +135,7 @@ export function buildClaimDetailPayload({ claimToken, task, order, orderItem })
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapClaimKuaishouCloudFulfillment(task, order) {
|
export function mapClaimKuaishouCloudFulfillment(task: TaskRow, order: OrderRow) {
|
||||||
if (String(task?.executor_key || '').trim() !== 'kuaishou_ct_assisted') {
|
if (String(task?.executor_key || '').trim() !== 'kuaishou_ct_assisted') {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -213,11 +222,11 @@ export function mapClaimKuaishouCloudFulfillment(task, order) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTaskContext(task) {
|
function parseTaskContext(task: TaskRow): JsonObject {
|
||||||
return parseTaskContextValue(task)
|
return parseTaskContextValue(task)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function expireClaimContext(claimToken, task) {
|
async function expireClaimContext(claimToken: ClaimTokenRow, task: TaskRow) {
|
||||||
const now = nowIso()
|
const now = nowIso()
|
||||||
const nextClaimToken = await updateClaimToken(claimToken.id, {
|
const nextClaimToken = await updateClaimToken(claimToken.id, {
|
||||||
status: 'expired',
|
status: 'expired',
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from '../fulfillment/kuaishou-cloud-task-service.js'
|
} from '../fulfillment/kuaishou-cloud-task-service.js'
|
||||||
import { buildClaimDetailPayload, getClaimContext } from './kuaishou-cloud-claim-context.js'
|
import { buildClaimDetailPayload, getClaimContext } from './kuaishou-cloud-claim-context.js'
|
||||||
import { syncKuaishouCloudRoleInfo } from './kuaishou-cloud-sync-service.js'
|
import { syncKuaishouCloudRoleInfo } from './kuaishou-cloud-sync-service.js'
|
||||||
|
import type { TaskRow } from '../../types/repository-rows.js'
|
||||||
|
|
||||||
const KUAISHOU_CLOUD_GUIDE_DIR = path.resolve(PROJECT_ROOT, '../../tems/imgs')
|
const KUAISHOU_CLOUD_GUIDE_DIR = path.resolve(PROJECT_ROOT, '../../tems/imgs')
|
||||||
const ALLOWED_GUIDE_FILES = new Set(['1.png', '2.png', '3.png'])
|
const ALLOWED_GUIDE_FILES = new Set(['1.png', '2.png', '3.png'])
|
||||||
@@ -136,7 +137,7 @@ export async function verifyKuaishouCloudClaimTicket(token: unknown, payload: Js
|
|||||||
return getKuaishouCloudClaimDetail(token)
|
return getKuaishouCloudClaimDetail(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getKuaishouCloudClaimGuideAssetPath(filename) {
|
export async function getKuaishouCloudClaimGuideAssetPath(filename: unknown) {
|
||||||
const normalized = String(filename || '').trim()
|
const normalized = String(filename || '').trim()
|
||||||
if (!ALLOWED_GUIDE_FILES.has(normalized)) {
|
if (!ALLOWED_GUIDE_FILES.has(normalized)) {
|
||||||
throw createHttpError('指引图片不存在', {
|
throw createHttpError('指引图片不存在', {
|
||||||
@@ -172,7 +173,7 @@ export async function getKuaishouCloudClaimDetail(token: unknown) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTaskContext(task) {
|
function parseTaskContext(task: Partial<TaskRow> | null | undefined): JsonObject {
|
||||||
const rawValue = task?.context_json
|
const rawValue = task?.context_json
|
||||||
if (!rawValue) {
|
if (!rawValue) {
|
||||||
return {}
|
return {}
|
||||||
@@ -189,7 +190,7 @@ function parseTaskContext(task) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function confirmKuaishouCloudClaimRole(token) {
|
export async function confirmKuaishouCloudClaimRole(token: unknown) {
|
||||||
const context = await getClaimContext(token)
|
const context = await getClaimContext(token)
|
||||||
const now = nowIso()
|
const now = nowIso()
|
||||||
|
|
||||||
@@ -245,7 +246,7 @@ export async function confirmKuaishouCloudClaimRole(token) {
|
|||||||
return getKuaishouCloudClaimDetail(token)
|
return getKuaishouCloudClaimDetail(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function redeemKuaishouCloudClaim(token) {
|
export async function redeemKuaishouCloudClaim(token: unknown) {
|
||||||
const context = await getClaimContext(token)
|
const context = await getClaimContext(token)
|
||||||
const now = nowIso()
|
const now = nowIso()
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import {
|
|||||||
probeKuaishouCloudTaskBindUrl,
|
probeKuaishouCloudTaskBindUrl,
|
||||||
refreshKuaishouCloudTaskRoleInfo,
|
refreshKuaishouCloudTaskRoleInfo,
|
||||||
} from '../fulfillment/kuaishou-cloud-task-service.js'
|
} from '../fulfillment/kuaishou-cloud-task-service.js'
|
||||||
|
import type { TaskRow } from '../../types/repository-rows.js'
|
||||||
|
|
||||||
export async function syncKuaishouCloudRoleInfo(task) {
|
export async function syncKuaishouCloudRoleInfo(task: TaskRow) {
|
||||||
const flow = normalizeKuaishouCloudFlow(parseTaskContext(task).kuaishouCloudFulfillment)
|
const flow = normalizeKuaishouCloudFlow(parseTaskContext(task).kuaishouCloudFulfillment)
|
||||||
|
|
||||||
if (!flow.binding.vnId || !flow.binding.vnKey || !flow.binding.vnPhone) {
|
if (!flow.binding.vnId || !flow.binding.vnKey || !flow.binding.vnPhone) {
|
||||||
@@ -42,7 +43,7 @@ export async function syncKuaishouCloudRoleInfo(task) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTaskContext(task) {
|
function parseTaskContext(task: Partial<TaskRow> | null | undefined): Record<string, any> {
|
||||||
const rawValue = task?.context_json
|
const rawValue = task?.context_json
|
||||||
|
|
||||||
if (!rawValue) {
|
if (!rawValue) {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
normalizeActor,
|
normalizeActor,
|
||||||
parseTaskContext,
|
parseTaskContext,
|
||||||
} from "./kuaishou-cloud/task-context.js";
|
} from "./kuaishou-cloud/task-context.js";
|
||||||
|
import type { TaskRow } from "../../types/repository-rows.js";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
isKuaishouCloudBindUrlFresh,
|
isKuaishouCloudBindUrlFresh,
|
||||||
@@ -58,7 +59,7 @@ export {
|
|||||||
resolvePersistedCloudtentaclesContextWithFallback,
|
resolvePersistedCloudtentaclesContextWithFallback,
|
||||||
} from "./kuaishou-cloud/cloudtentacles-context.js";
|
} from "./kuaishou-cloud/cloudtentacles-context.js";
|
||||||
|
|
||||||
export async function ensureTaskClaimLink(task) {
|
export async function ensureTaskClaimLink(task: TaskRow) {
|
||||||
const tokenStatus = String(task?.primary_claim_token_status || "").trim();
|
const tokenStatus = String(task?.primary_claim_token_status || "").trim();
|
||||||
const token = String(
|
const token = String(
|
||||||
task?.primary_claim_token || task?.claim_token || ""
|
task?.primary_claim_token || task?.claim_token || ""
|
||||||
@@ -82,7 +83,7 @@ export async function ensureTaskClaimLink(task) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function prepareKuaishouCloudFulfillmentTask(
|
export async function prepareKuaishouCloudFulfillmentTask(
|
||||||
task,
|
task: TaskRow,
|
||||||
options: JsonObject = {}
|
options: JsonObject = {}
|
||||||
) {
|
) {
|
||||||
if (!isKuaishouCloudTask(task)) {
|
if (!isKuaishouCloudTask(task)) {
|
||||||
@@ -246,7 +247,7 @@ export async function prepareKuaishouCloudFulfillmentTask(
|
|||||||
bindUrl: preparedBinding.bindUrl,
|
bindUrl: preparedBinding.bindUrl,
|
||||||
bindPreparedAt: now,
|
bindPreparedAt: now,
|
||||||
bindExpiresAt: resolveKuaishouCloudBindUrlExpiresAt(now),
|
bindExpiresAt: resolveKuaishouCloudBindUrlExpiresAt(now),
|
||||||
bindProbeAt: null,
|
bindProbeAt: null as null,
|
||||||
bindProbeStatus: "pending",
|
bindProbeStatus: "pending",
|
||||||
bindProbeMessage: "",
|
bindProbeMessage: "",
|
||||||
roleName: "",
|
roleName: "",
|
||||||
@@ -256,9 +257,9 @@ export async function prepareKuaishouCloudFulfillmentTask(
|
|||||||
status: "pending",
|
status: "pending",
|
||||||
name: "",
|
name: "",
|
||||||
rid: "",
|
rid: "",
|
||||||
refreshedAt: null,
|
refreshedAt: null as null,
|
||||||
errorMessage: "",
|
errorMessage: "",
|
||||||
rawInfo: null,
|
rawInfo: null as null,
|
||||||
},
|
},
|
||||||
purchase: {
|
purchase: {
|
||||||
...flowWithResolvedBinding.purchase,
|
...flowWithResolvedBinding.purchase,
|
||||||
@@ -312,7 +313,7 @@ export async function prepareKuaishouCloudFulfillmentTask(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function refreshKuaishouCloudTaskBindUrl(
|
export async function refreshKuaishouCloudTaskBindUrl(
|
||||||
task,
|
task: TaskRow,
|
||||||
options: JsonObject = {}
|
options: JsonObject = {}
|
||||||
) {
|
) {
|
||||||
if (!isKuaishouCloudTask(task)) {
|
if (!isKuaishouCloudTask(task)) {
|
||||||
@@ -413,7 +414,7 @@ export async function refreshKuaishouCloudTaskBindUrl(
|
|||||||
bindUrl: preparedBinding.bindUrl,
|
bindUrl: preparedBinding.bindUrl,
|
||||||
bindPreparedAt: now,
|
bindPreparedAt: now,
|
||||||
bindExpiresAt: resolveKuaishouCloudBindUrlExpiresAt(now),
|
bindExpiresAt: resolveKuaishouCloudBindUrlExpiresAt(now),
|
||||||
bindProbeAt: null,
|
bindProbeAt: null as null,
|
||||||
bindProbeStatus: "pending",
|
bindProbeStatus: "pending",
|
||||||
bindProbeMessage: "",
|
bindProbeMessage: "",
|
||||||
roleName:
|
roleName:
|
||||||
@@ -485,7 +486,7 @@ export async function refreshKuaishouCloudTaskBindUrl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function probeKuaishouCloudTaskBindUrl(
|
export async function probeKuaishouCloudTaskBindUrl(
|
||||||
task,
|
task: TaskRow,
|
||||||
options: JsonObject = {}
|
options: JsonObject = {}
|
||||||
) {
|
) {
|
||||||
if (!isKuaishouCloudTask(task)) {
|
if (!isKuaishouCloudTask(task)) {
|
||||||
@@ -503,7 +504,7 @@ export async function probeKuaishouCloudTaskBindUrl(
|
|||||||
return {
|
return {
|
||||||
task,
|
task,
|
||||||
flow,
|
flow,
|
||||||
probe: null,
|
probe: null as null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -518,7 +519,7 @@ export async function probeKuaishouCloudTaskBindUrl(
|
|||||||
return {
|
return {
|
||||||
task,
|
task,
|
||||||
flow,
|
flow,
|
||||||
probe: null,
|
probe: null as null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,7 +583,7 @@ export async function probeKuaishouCloudTaskBindUrl(
|
|||||||
* @param {{ taskContext?: any, flow?: any, now?: string, actor?: any, error?: unknown }} [input]
|
* @param {{ taskContext?: any, flow?: any, now?: string, actor?: any, error?: unknown }} [input]
|
||||||
*/
|
*/
|
||||||
async function markKuaishouCloudBindUrlRefreshFailed(
|
async function markKuaishouCloudBindUrlRefreshFailed(
|
||||||
task,
|
task: TaskRow,
|
||||||
{ taskContext, flow, now, actor, error }: JsonObject = {}
|
{ taskContext, flow, now, actor, error }: JsonObject = {}
|
||||||
) {
|
) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
@@ -648,7 +649,7 @@ async function markKuaishouCloudBindUrlRefreshFailed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function refreshKuaishouCloudTaskRoleInfo(
|
export async function refreshKuaishouCloudTaskRoleInfo(
|
||||||
task,
|
task: TaskRow,
|
||||||
options: JsonObject = {}
|
options: JsonObject = {}
|
||||||
) {
|
) {
|
||||||
if (!isKuaishouCloudTask(task)) {
|
if (!isKuaishouCloudTask(task)) {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import {
|
|||||||
import { KUAISHOU_CLOUD_FIXED_VN_KEY, type JsonObject } from "./domain.js";
|
import { KUAISHOU_CLOUD_FIXED_VN_KEY, type JsonObject } from "./domain.js";
|
||||||
|
|
||||||
export function resolveKuaishouCloudBindingResources(
|
export function resolveKuaishouCloudBindingResources(
|
||||||
flow,
|
flow: JsonObject,
|
||||||
{ skuItems = [], knapsackItems = [] } = {}
|
{ skuItems = [], knapsackItems = [] }: { skuItems?: unknown[], knapsackItems?: unknown[] } = {}
|
||||||
) {
|
) {
|
||||||
const normalizedSkuItems = Array.isArray(skuItems)
|
const normalizedSkuItems = Array.isArray(skuItems)
|
||||||
? skuItems.filter(isCloudSkuLikeItem)
|
? skuItems.filter(isCloudSkuLikeItem)
|
||||||
@@ -70,7 +70,7 @@ export function resolveKuaishouCloudBindingResources(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveKuaishouCloudVnKeyCandidates(_input = {}) {
|
export function resolveKuaishouCloudVnKeyCandidates(_input: JsonObject = {}) {
|
||||||
return [KUAISHOU_CLOUD_FIXED_VN_KEY];
|
return [KUAISHOU_CLOUD_FIXED_VN_KEY];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ export async function prepareKuaishouCloudBindResourceWithFallback(input: JsonOb
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectKuaishouCloudNameCandidates(flow) {
|
function collectKuaishouCloudNameCandidates(flow: JsonObject) {
|
||||||
return Array.from(
|
return Array.from(
|
||||||
new Set(
|
new Set(
|
||||||
[
|
[
|
||||||
@@ -171,10 +171,10 @@ function collectKuaishouCloudNameCandidates(flow) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function findCloudItemByNames(items, nameCandidates, preferredId = 0) {
|
function findCloudItemByNames(items: JsonObject[], nameCandidates: string[], preferredId = 0) {
|
||||||
const normalizedItems = Array.isArray(items) ? items : [];
|
const normalizedItems = Array.isArray(items) ? items : [];
|
||||||
const normalizedNames = nameCandidates
|
const normalizedNames = nameCandidates
|
||||||
.map((item) => ({
|
.map((item: string) => ({
|
||||||
raw: String(item || "").trim(),
|
raw: String(item || "").trim(),
|
||||||
normalized: normalizeProductName(item),
|
normalized: normalizeProductName(item),
|
||||||
}))
|
}))
|
||||||
@@ -182,34 +182,34 @@ function findCloudItemByNames(items, nameCandidates, preferredId = 0) {
|
|||||||
|
|
||||||
if (normalizedNames.length === 0 || normalizedItems.length === 0) {
|
if (normalizedNames.length === 0 || normalizedItems.length === 0) {
|
||||||
return preferredId > 0
|
return preferredId > 0
|
||||||
? normalizedItems.find((item) => Number(item.id || 0) === preferredId) ||
|
? normalizedItems.find((item: JsonObject) => Number(item.id || 0) === preferredId) ||
|
||||||
null
|
null
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preferredId > 0) {
|
if (preferredId > 0) {
|
||||||
const preferred =
|
const preferred =
|
||||||
normalizedItems.find((item) => Number(item.id || 0) === preferredId) ||
|
normalizedItems.find((item: JsonObject) => Number(item.id || 0) === preferredId) ||
|
||||||
null;
|
null;
|
||||||
if (preferred) {
|
if (preferred) {
|
||||||
return preferred;
|
return preferred;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const exactMatches = normalizedItems.filter((item) => {
|
const exactMatches = normalizedItems.filter((item: JsonObject) => {
|
||||||
const itemName = normalizeProductName(item.name);
|
const itemName = normalizeProductName(item.name);
|
||||||
return normalizedNames.some(
|
return normalizedNames.some(
|
||||||
(candidate) => candidate.normalized === itemName
|
(candidate: { normalized: string }) => candidate.normalized === itemName
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
if (exactMatches.length > 0) {
|
if (exactMatches.length > 0) {
|
||||||
return exactMatches[0];
|
return exactMatches[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const partialMatches = normalizedItems.filter((item) => {
|
const partialMatches = normalizedItems.filter((item: JsonObject) => {
|
||||||
const itemName = normalizeProductName(item.name);
|
const itemName = normalizeProductName(item.name);
|
||||||
return normalizedNames.some(
|
return normalizedNames.some(
|
||||||
(candidate) =>
|
(candidate: { normalized: string }) =>
|
||||||
itemName.includes(candidate.normalized) ||
|
itemName.includes(candidate.normalized) ||
|
||||||
candidate.normalized.includes(itemName)
|
candidate.normalized.includes(itemName)
|
||||||
);
|
);
|
||||||
@@ -224,13 +224,15 @@ function findCloudItemByNames(items, nameCandidates, preferredId = 0) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCloudSkuLikeItem(item) {
|
function isCloudSkuLikeItem(item: unknown): item is JsonObject {
|
||||||
return Boolean(item) && typeof item === "object" && Number(item.id || 0) > 0;
|
const current = item && typeof item === "object" ? item as JsonObject : {};
|
||||||
|
return Number(current.id || 0) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRecoverableKuaishouCloudVnKeyError(error) {
|
function isRecoverableKuaishouCloudVnKeyError(error: unknown) {
|
||||||
const errorCode = String(error?.errorCode || error?.code || "").trim();
|
const current = error && typeof error === "object" ? error as JsonObject : {};
|
||||||
const errorMessage = String(error?.message || "").trim();
|
const errorCode = String(current.errorCode || current.code || "").trim();
|
||||||
|
const errorMessage = String(current.message || "").trim();
|
||||||
return (
|
return (
|
||||||
errorCode === "cloudtentacles_vn_bind_url_failed" &&
|
errorCode === "cloudtentacles_vn_bind_url_failed" &&
|
||||||
errorMessage.includes("不支持的游戏类型")
|
errorMessage.includes("不支持的游戏类型")
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export function resolvePersistedCloudtentaclesContext(sourceKey = "default") {
|
|||||||
*/
|
*/
|
||||||
export function resolvePersistedCloudtentaclesContextWithFallback(
|
export function resolvePersistedCloudtentaclesContextWithFallback(
|
||||||
primarySourceKey = "default",
|
primarySourceKey = "default",
|
||||||
fallbacks = []
|
fallbacks: unknown[] = []
|
||||||
) {
|
) {
|
||||||
const candidates = [
|
const candidates = [
|
||||||
String(primarySourceKey || "default").trim() || "default",
|
String(primarySourceKey || "default").trim() || "default",
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ export const KUAISHOU_CLOUD_FIXED_VN_KEY = "1";
|
|||||||
|
|
||||||
export type JsonObject = Record<string, any>;
|
export type JsonObject = Record<string, any>;
|
||||||
|
|
||||||
export function isKuaishouCloudTask(task) {
|
export function isKuaishouCloudTask(task: unknown) {
|
||||||
return String(task?.executor_key || "").trim() === "kuaishou_ct_assisted";
|
const source = task && typeof task === "object" ? task as JsonObject : {};
|
||||||
|
return String(source.executor_key || "").trim() === "kuaishou_ct_assisted";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeKuaishouCloudFlow(value) {
|
export function normalizeKuaishouCloudFlow(value: unknown) {
|
||||||
const source = value && typeof value === "object" ? value : {};
|
const source: JsonObject = value && typeof value === "object" ? value as JsonObject : {};
|
||||||
const binding =
|
const binding =
|
||||||
source.binding && typeof source.binding === "object" ? source.binding : {};
|
source.binding && typeof source.binding === "object" ? source.binding : {};
|
||||||
const role =
|
const role =
|
||||||
@@ -129,8 +130,8 @@ export function normalizeKuaishouCloudFlow(value) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeKuaishouCloudRoleInfo(value) {
|
export function normalizeKuaishouCloudRoleInfo(value: unknown) {
|
||||||
const rawInfo = value && typeof value === "object" ? value : null;
|
const rawInfo: JsonObject | null = value && typeof value === "object" ? value as JsonObject : null;
|
||||||
const nestedBindInfo =
|
const nestedBindInfo =
|
||||||
rawInfo?.sBindInfo && typeof rawInfo.sBindInfo === "object"
|
rawInfo?.sBindInfo && typeof rawInfo.sBindInfo === "object"
|
||||||
? rawInfo.sBindInfo
|
? rawInfo.sBindInfo
|
||||||
@@ -157,15 +158,15 @@ export function normalizeKuaishouCloudRoleInfo(value) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function maskPhone(value) {
|
export function maskPhone(value: unknown) {
|
||||||
return maskPhoneValue(value);
|
return maskPhoneValue(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function maskCode(value) {
|
export function maskCode(value: unknown) {
|
||||||
return maskCodeValue(value);
|
return maskCodeValue(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveKuaishouCloudBindUrlExpiresAt(preparedAt) {
|
export function resolveKuaishouCloudBindUrlExpiresAt(preparedAt: unknown) {
|
||||||
const preparedTime = Date.parse(String(preparedAt || ""));
|
const preparedTime = Date.parse(String(preparedAt || ""));
|
||||||
if (!Number.isFinite(preparedTime)) {
|
if (!Number.isFinite(preparedTime)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -176,7 +177,7 @@ export function resolveKuaishouCloudBindUrlExpiresAt(preparedAt) {
|
|||||||
return new Date(preparedTime + Math.max(1, ttlSeconds) * 1000).toISOString();
|
return new Date(preparedTime + Math.max(1, ttlSeconds) * 1000).toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isKuaishouCloudBindUrlFresh(flow, now = new Date()) {
|
export function isKuaishouCloudBindUrlFresh(flow: unknown, now = new Date()) {
|
||||||
const normalizedFlow = normalizeKuaishouCloudFlow(flow);
|
const normalizedFlow = normalizeKuaishouCloudFlow(flow);
|
||||||
if (!normalizedFlow.binding.bindUrl) {
|
if (!normalizedFlow.binding.bindUrl) {
|
||||||
return false;
|
return false;
|
||||||
@@ -195,7 +196,7 @@ export function isKuaishouCloudBindUrlFresh(flow, now = new Date()) {
|
|||||||
return expiresTime > now.getTime();
|
return expiresTime > now.getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeStringArray(value) {
|
export function normalizeStringArray(value: unknown) {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return value.map((v) => String(v || "").trim()).filter(Boolean);
|
return value.map((v) => String(v || "").trim()).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
import { parseTaskContext as parseTaskContextValue } from "../../../utils/task-json.js";
|
import { parseTaskContext as parseTaskContextValue } from "../../../utils/task-json.js";
|
||||||
|
import type { TaskRow } from "../../../types/repository-rows.js";
|
||||||
|
|
||||||
export function normalizeActor(actor) {
|
type JsonObject = Record<string, any>;
|
||||||
|
|
||||||
|
export function normalizeActor(actor: unknown) {
|
||||||
if (!actor || typeof actor !== "object") {
|
if (!actor || typeof actor !== "object") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const source = String(actor.source || "").trim();
|
const current = actor as JsonObject;
|
||||||
const userId = Number(actor.userId || 0) || 0;
|
const source = String(current.source || "").trim();
|
||||||
const username = String(actor.username || "").trim();
|
const userId = Number(current.userId || 0) || 0;
|
||||||
const role = String(actor.role || "").trim();
|
const username = String(current.username || "").trim();
|
||||||
|
const role = String(current.role || "").trim();
|
||||||
|
|
||||||
if (!source && !userId && !username && !role) {
|
if (!source && !userId && !username && !role) {
|
||||||
return null;
|
return null;
|
||||||
@@ -22,19 +26,19 @@ export function normalizeActor(actor) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseTaskContext(task) {
|
export function parseTaskContext(task: Partial<TaskRow> | null | undefined) {
|
||||||
return parseTaskContextValue(task);
|
return parseTaskContextValue(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTaskClaimExpiresAt(task) {
|
export function getTaskClaimExpiresAt(task: Partial<TaskRow> | null | undefined) {
|
||||||
return task?.claim_expires_at || task?.primary_claim_expires_at || null;
|
return task?.claim_expires_at || task?.primary_claim_expires_at || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isClaimExpired(expiredAt) {
|
export function isClaimExpired(expiredAt: unknown) {
|
||||||
if (!expiredAt) {
|
if (!expiredAt) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timestamp = new Date(expiredAt).getTime();
|
const timestamp = new Date(expiredAt instanceof Date ? expiredAt : String(expiredAt || "")).getTime();
|
||||||
return Number.isFinite(timestamp) && timestamp <= Date.now();
|
return Number.isFinite(timestamp) && timestamp <= Date.now();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ import {
|
|||||||
} from "./domain.js";
|
} from "./domain.js";
|
||||||
import { resolvePersistedCloudtentaclesContextWithFallback } from "./cloudtentacles-context.js";
|
import { resolvePersistedCloudtentaclesContextWithFallback } from "./cloudtentacles-context.js";
|
||||||
import { normalizeActor, parseTaskContext } from "./task-context.js";
|
import { normalizeActor, parseTaskContext } from "./task-context.js";
|
||||||
|
import type { TaskRow } from "../../../types/repository-rows.js";
|
||||||
|
|
||||||
export async function dispatchKuaishouCloudFulfillmentTask(
|
export async function dispatchKuaishouCloudFulfillmentTask(
|
||||||
task,
|
task: TaskRow,
|
||||||
options: JsonObject = {}
|
options: JsonObject = {}
|
||||||
) {
|
) {
|
||||||
if (!isKuaishouCloudTask(task)) {
|
if (!isKuaishouCloudTask(task)) {
|
||||||
@@ -157,7 +158,7 @@ export async function dispatchKuaishouCloudFulfillmentTask(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function returnKuaishouCloudFulfillmentTask(
|
export async function returnKuaishouCloudFulfillmentTask(
|
||||||
task,
|
task: TaskRow,
|
||||||
options: JsonObject = {}
|
options: JsonObject = {}
|
||||||
) {
|
) {
|
||||||
if (!isKuaishouCloudTask(task)) {
|
if (!isKuaishouCloudTask(task)) {
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ const NOTIFICATION_CONFIG_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'notificat
|
|||||||
const DEFAULT_BARK_SERVER_URL = 'https://api.day.app'
|
const DEFAULT_BARK_SERVER_URL = 'https://api.day.app'
|
||||||
|
|
||||||
type JsonObject = Record<string, any>
|
type JsonObject = Record<string, any>
|
||||||
|
type NotificationRecipient = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
deviceKey?: string
|
||||||
|
apiKey?: string
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export function getNotificationConfigFilePath() {
|
export function getNotificationConfigFilePath() {
|
||||||
return NOTIFICATION_CONFIG_FILE_PATH
|
return NOTIFICATION_CONFIG_FILE_PATH
|
||||||
@@ -26,7 +33,7 @@ export function listEnabledBarkRecipients(config: JsonObject = getNotificationCo
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (Array.isArray(config.channels?.bark?.recipients) ? config.channels.bark.recipients : [])
|
return (Array.isArray(config.channels?.bark?.recipients) ? config.channels.bark.recipients : [])
|
||||||
.filter((item) => item.enabled !== false && String(item.deviceKey || '').trim())
|
.filter((item: JsonObject) => item.enabled !== false && String(item.deviceKey || '').trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listEnabledWpushRecipients(config: JsonObject = getNotificationConfig()) {
|
export function listEnabledWpushRecipients(config: JsonObject = getNotificationConfig()) {
|
||||||
@@ -35,7 +42,7 @@ export function listEnabledWpushRecipients(config: JsonObject = getNotificationC
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (Array.isArray(config.channels?.wpush?.recipients) ? config.channels.wpush.recipients : [])
|
return (Array.isArray(config.channels?.wpush?.recipients) ? config.channels.wpush.recipients : [])
|
||||||
.filter((item) => item.enabled !== false && String(item.apiKey || item.apikey || '').trim())
|
.filter((item: JsonObject) => item.enabled !== false && String(item.apiKey || item.apikey || '').trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadNotificationConfigFromFile() {
|
function loadNotificationConfigFromFile() {
|
||||||
@@ -58,20 +65,20 @@ export function normalizeNotificationConfig(rawValue: unknown) {
|
|||||||
enabled: typeof bark.enabled === 'boolean' ? bark.enabled : true,
|
enabled: typeof bark.enabled === 'boolean' ? bark.enabled : true,
|
||||||
serverUrl: normalizeBarkServerUrl(bark.serverUrl),
|
serverUrl: normalizeBarkServerUrl(bark.serverUrl),
|
||||||
recipients: (Array.isArray(bark.recipients) ? bark.recipients : [])
|
recipients: (Array.isArray(bark.recipients) ? bark.recipients : [])
|
||||||
.map((item) => normalizeBarkRecipient(item))
|
.map((item: unknown) => normalizeBarkRecipient(item))
|
||||||
.filter(Boolean),
|
.filter(Boolean),
|
||||||
},
|
},
|
||||||
wpush: {
|
wpush: {
|
||||||
enabled: typeof wpush.enabled === 'boolean' ? wpush.enabled : true,
|
enabled: typeof wpush.enabled === 'boolean' ? wpush.enabled : true,
|
||||||
recipients: (Array.isArray(wpush.recipients) ? wpush.recipients : [])
|
recipients: (Array.isArray(wpush.recipients) ? wpush.recipients : [])
|
||||||
.map((item) => normalizeWpushRecipient(item))
|
.map((item: unknown) => normalizeWpushRecipient(item))
|
||||||
.filter(Boolean),
|
.filter(Boolean),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeBarkRecipient(rawValue: unknown) {
|
function normalizeBarkRecipient(rawValue: unknown): NotificationRecipient | null {
|
||||||
if (!isPlainObject(rawValue)) {
|
if (!isPlainObject(rawValue)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -97,7 +104,7 @@ function normalizeBarkServerUrl(value: unknown) {
|
|||||||
return normalized.replace(/\/+$/, '')
|
return normalized.replace(/\/+$/, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeWpushRecipient(rawValue: unknown) {
|
function normalizeWpushRecipient(rawValue: unknown): NotificationRecipient | null {
|
||||||
if (!isPlainObject(rawValue)) {
|
if (!isPlainObject(rawValue)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -125,11 +132,11 @@ export function createDefaultNotificationConfig() {
|
|||||||
bark: {
|
bark: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
serverUrl: DEFAULT_BARK_SERVER_URL,
|
serverUrl: DEFAULT_BARK_SERVER_URL,
|
||||||
recipients: [],
|
recipients: [] as NotificationRecipient[],
|
||||||
},
|
},
|
||||||
wpush: {
|
wpush: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
recipients: [],
|
recipients: [] as NotificationRecipient[],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { sendBarkNotification } from './bark-service.js'
|
|||||||
import { sendWpushNotification } from './wpush-service.js'
|
import { sendWpushNotification } from './wpush-service.js'
|
||||||
|
|
||||||
type JsonObject = Record<string, any>
|
type JsonObject = Record<string, any>
|
||||||
|
type NotificationResult = ReturnType<typeof mapNotificationResult>
|
||||||
type NotificationInput = {
|
type NotificationInput = {
|
||||||
title?: string
|
title?: string
|
||||||
body?: string
|
body?: string
|
||||||
@@ -31,12 +32,12 @@ export async function sendInternalNotification(input: NotificationInput = {}) {
|
|||||||
successCount: 0,
|
successCount: 0,
|
||||||
failedCount: 0,
|
failedCount: 0,
|
||||||
skippedCount: barkRecipients.length + wpushRecipients.length,
|
skippedCount: barkRecipients.length + wpushRecipients.length,
|
||||||
results: [],
|
results: [] as NotificationResult[],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = [
|
const results = [
|
||||||
...(await Promise.all(barkRecipients.map(async (recipient) => {
|
...(await Promise.all(barkRecipients.map(async (recipient: JsonObject) => {
|
||||||
try {
|
try {
|
||||||
const result = await sendBarkNotification({
|
const result = await sendBarkNotification({
|
||||||
serverUrl: bark.serverUrl,
|
serverUrl: bark.serverUrl,
|
||||||
@@ -64,7 +65,7 @@ export async function sendInternalNotification(input: NotificationInput = {}) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}))),
|
}))),
|
||||||
...(await Promise.all(wpushRecipients.map(async (recipient) => {
|
...(await Promise.all(wpushRecipients.map(async (recipient: JsonObject) => {
|
||||||
try {
|
try {
|
||||||
const result = await sendWpushNotification({
|
const result = await sendWpushNotification({
|
||||||
recipient,
|
recipient,
|
||||||
|
|||||||
@@ -23,8 +23,11 @@ import {
|
|||||||
OPEN_91_PROVIDER,
|
OPEN_91_PROVIDER,
|
||||||
resolveOpen91QueryState,
|
resolveOpen91QueryState,
|
||||||
} from './shared.js'
|
} from './shared.js'
|
||||||
|
import type { OrderRow } from '../../types/repository-rows.js'
|
||||||
|
|
||||||
export async function queryOpen91Order(payload = {}, { requestId = '' } = {}) {
|
type JsonObject = Record<string, any>
|
||||||
|
|
||||||
|
export async function queryOpen91Order(payload: JsonObject = {}, { requestId = '' } = {}) {
|
||||||
const config = assertOpen91Config()
|
const config = assertOpen91Config()
|
||||||
const normalized = normalizeOpen91QueryPayload(payload)
|
const normalized = normalizeOpen91QueryPayload(payload)
|
||||||
|
|
||||||
@@ -168,12 +171,12 @@ export async function queryOpen91Order(payload = {}, { requestId = '' } = {}) {
|
|||||||
return buildOpen91SuccessResponse(responseData)
|
return buildOpen91SuccessResponse(responseData)
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveOpen91OrderFailReason(order) {
|
function resolveOpen91OrderFailReason(order: OrderRow) {
|
||||||
const payload = parseJsonObject(order?.raw_payload_json)
|
const payload = parseJsonObject(order?.raw_payload_json)
|
||||||
return String(payload.manualFailedReason || '').trim()
|
return String(payload.manualFailedReason || '').trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseJsonObject(value) {
|
function parseJsonObject(value: unknown): JsonObject {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export function assertOpen91Config() {
|
|||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeOpen91String(value) {
|
export function normalizeOpen91String(value: unknown) {
|
||||||
return String(value || '').trim()
|
return String(value || '').trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +187,7 @@ export function assertOpen91Signature(params: JsonObject = {}, config = assertOp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function encryptOpen91Cards(cards = [], secret = assertOpen91Config().secret) {
|
export function encryptOpen91Cards(cards: unknown[] = [], secret = assertOpen91Config().secret) {
|
||||||
const normalizedSecret = normalizeOpen91String(secret)
|
const normalizedSecret = normalizeOpen91String(secret)
|
||||||
if (normalizedSecret.length !== 32) {
|
if (normalizedSecret.length !== 32) {
|
||||||
throw createHttpError('91卡券 cards 加密密钥长度必须为 32 个字符', {
|
throw createHttpError('91卡券 cards 加密密钥长度必须为 32 个字符', {
|
||||||
@@ -227,7 +227,7 @@ export function buildOpen91ErrorResponse(message: unknown, code = 500) {
|
|||||||
return {
|
return {
|
||||||
code,
|
code,
|
||||||
message: normalizeOpen91String(message) || '系统错误',
|
message: normalizeOpen91String(message) || '系统错误',
|
||||||
data: null,
|
data: null as null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,7 +323,7 @@ export function resolveOpen91QueryState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stringifyOpen91SignValue(value) {
|
export function stringifyOpen91SignValue(value: unknown) {
|
||||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||||
return String(value)
|
return String(value)
|
||||||
}
|
}
|
||||||
@@ -339,12 +339,12 @@ export function stringifyOpen91SignValue(value) {
|
|||||||
return String(value)
|
return String(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeOpen91BuyNum(value) {
|
function normalizeOpen91BuyNum(value: unknown) {
|
||||||
const parsed = Number(value)
|
const parsed = Number(value)
|
||||||
return Number.isInteger(parsed) ? parsed : 0
|
return Number.isInteger(parsed) ? parsed : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeOpen91OptionalAmount(value) {
|
function normalizeOpen91OptionalAmount(value: unknown) {
|
||||||
const normalized = normalizeOpen91String(value)
|
const normalized = normalizeOpen91String(value)
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return ''
|
return ''
|
||||||
@@ -360,7 +360,7 @@ function normalizeOpen91OptionalAmount(value) {
|
|||||||
return normalized
|
return normalized
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeOpen91Timestamp(value) {
|
function normalizeOpen91Timestamp(value: unknown) {
|
||||||
const normalized = normalizeOpen91String(value)
|
const normalized = normalizeOpen91String(value)
|
||||||
if (!/^\d{10}$/.test(normalized)) {
|
if (!/^\d{10}$/.test(normalized)) {
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function getKuaishouCloudFulfillmentConfig() {
|
|||||||
return loadKuaishouCloudFulfillmentConfigFromFile();
|
return loadKuaishouCloudFulfillmentConfigFromFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveKuaishouCloudFulfillmentConfig(rawValue) {
|
export function saveKuaishouCloudFulfillmentConfig(rawValue: unknown) {
|
||||||
return writeJsonFile(
|
return writeJsonFile(
|
||||||
KUAISHOU_CLOUD_FULFILLMENT_FILE_PATH,
|
KUAISHOU_CLOUD_FULFILLMENT_FILE_PATH,
|
||||||
rawValue,
|
rawValue,
|
||||||
@@ -94,19 +94,19 @@ function loadKuaishouCloudFulfillmentConfigFromFile() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeKuaishouCloudFulfillmentConfig(rawValue) {
|
function normalizeKuaishouCloudFulfillmentConfig(rawValue: unknown) {
|
||||||
const source = isPlainObject(rawValue) ? rawValue : {};
|
const source = isPlainObject(rawValue) ? rawValue : {};
|
||||||
return {
|
return {
|
||||||
enabled: source.enabled !== false,
|
enabled: source.enabled !== false,
|
||||||
items: Array.isArray(source.items)
|
items: Array.isArray(source.items)
|
||||||
? source.items
|
? source.items
|
||||||
.map((item) => normalizeKuaishouCloudFulfillmentItem(item))
|
.map((item: unknown) => normalizeKuaishouCloudFulfillmentItem(item))
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
: [],
|
: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeKuaishouCloudFulfillmentItem(rawValue) {
|
function normalizeKuaishouCloudFulfillmentItem(rawValue: unknown) {
|
||||||
if (!isPlainObject(rawValue)) {
|
if (!isPlainObject(rawValue)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -178,17 +178,17 @@ function normalizeKuaishouCloudFulfillmentItem(rawValue) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePositiveInteger(value) {
|
function normalizePositiveInteger(value: unknown) {
|
||||||
const parsed = Number(value);
|
const parsed = Number(value);
|
||||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : 0;
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeNonNegativeInteger(value, fallback) {
|
function normalizeNonNegativeInteger(value: unknown, fallback: number) {
|
||||||
const parsed = Number(value);
|
const parsed = Number(value);
|
||||||
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePriority(value) {
|
function normalizePriority(value: unknown) {
|
||||||
const parsed = Number(value);
|
const parsed = Number(value);
|
||||||
if (!Number.isFinite(parsed)) {
|
if (!Number.isFinite(parsed)) {
|
||||||
return 100;
|
return 100;
|
||||||
@@ -196,19 +196,19 @@ function normalizePriority(value) {
|
|||||||
|
|
||||||
return Math.max(1, Math.round(parsed));
|
return Math.max(1, Math.round(parsed));
|
||||||
}
|
}
|
||||||
function isPlainObject(value) {
|
function isPlainObject(value: unknown): value is JsonObject {
|
||||||
return Object.prototype.toString.call(value) === "[object Object]";
|
return Object.prototype.toString.call(value) === "[object Object]";
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeStringArray(value) {
|
function normalizeStringArray(value: unknown) {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return value.map((v) => String(v || "").trim()).filter(Boolean);
|
return value.map((v: unknown) => String(v || "").trim()).filter(Boolean);
|
||||||
}
|
}
|
||||||
// 兼容旧格式:字符串用逗号分隔
|
// 兼容旧格式:字符串用逗号分隔
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
return value
|
return value
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((v) => v.trim())
|
.map((v: string) => v.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export async function runCloudtentaclesFullDebugFlow(payload: JsonObject = {}) {
|
|||||||
const beforeAsset = await runFlowStep('查询购买前余额', () => getCloudtentaclesAsset(payload))
|
const beforeAsset = await runFlowStep('查询购买前余额', () => getCloudtentaclesAsset(payload))
|
||||||
await sleep(350)
|
await sleep(350)
|
||||||
const skuList = await runFlowStep('查询 SKU 列表', () => listCloudtentaclesSku(payload))
|
const skuList = await runFlowStep('查询 SKU 列表', () => listCloudtentaclesSku(payload))
|
||||||
const targetSku = skuList.items.find((item) => Number(item.id || 0) === skuId) || null
|
const targetSku = skuList.items.find((item: JsonObject) => Number(item.id || 0) === skuId) || null
|
||||||
|
|
||||||
if (!targetSku) {
|
if (!targetSku) {
|
||||||
throw createHttpError(`cloudtentacles 未找到 SKU ${skuId}`, {
|
throw createHttpError(`cloudtentacles 未找到 SKU ${skuId}`, {
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ export async function validateCloudtentaclesSession(payload: JsonObject = {}) {
|
|||||||
userInfo: isPlainObject(userInfoResult.payload?.data) ? userInfoResult.payload.data : {},
|
userInfo: isPlainObject(userInfoResult.payload?.data) ? userInfoResult.payload.data : {},
|
||||||
asset: Number(assetResult.payload?.data || 0),
|
asset: Number(assetResult.payload?.data || 0),
|
||||||
permissions: Array.isArray(permissionResult.payload?.data)
|
permissions: Array.isArray(permissionResult.payload?.data)
|
||||||
? permissionResult.payload.data.map((item) => String(item || '').trim()).filter(Boolean)
|
? permissionResult.payload.data.map((item: unknown) => String(item || '').trim()).filter(Boolean)
|
||||||
: [],
|
: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import { PROJECT_ROOT } from '../../../config/runtime.js'
|
|||||||
const CLOUDTENTACLES_SESSION_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'cloudtentacles-session.json')
|
const CLOUDTENTACLES_SESSION_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'cloudtentacles-session.json')
|
||||||
|
|
||||||
type JsonObject = Record<string, any>
|
type JsonObject = Record<string, any>
|
||||||
|
type CloudtentaclesSessionState = ReturnType<typeof createDefaultCloudtentaclesSessionState>
|
||||||
|
type CloudtentaclesSessionStatesFile = {
|
||||||
|
sessions: Record<string, CloudtentaclesSessionState>
|
||||||
|
}
|
||||||
|
|
||||||
export function getCloudtentaclesSessionFilePath() {
|
export function getCloudtentaclesSessionFilePath() {
|
||||||
return CLOUDTENTACLES_SESSION_FILE_PATH
|
return CLOUDTENTACLES_SESSION_FILE_PATH
|
||||||
@@ -127,7 +131,7 @@ export function pruneCloudtentaclesSessionStates(sourceKeys: unknown[] = []) {
|
|||||||
// Internal helpers
|
// Internal helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function loadCloudtentaclesSessionStatesFromFile() {
|
function loadCloudtentaclesSessionStatesFromFile(): CloudtentaclesSessionStatesFile {
|
||||||
if (!fs.existsSync(CLOUDTENTACLES_SESSION_FILE_PATH)) {
|
if (!fs.existsSync(CLOUDTENTACLES_SESSION_FILE_PATH)) {
|
||||||
return createDefaultCloudtentaclesSessionStates()
|
return createDefaultCloudtentaclesSessionStates()
|
||||||
}
|
}
|
||||||
@@ -162,7 +166,7 @@ function normalizeCloudtentaclesSessionState(rawValue: unknown) {
|
|||||||
* Handles old format (single object without sessions key) by auto-wrapping
|
* Handles old format (single object without sessions key) by auto-wrapping
|
||||||
* into { sessions: { 'default': ... } }.
|
* into { sessions: { 'default': ... } }.
|
||||||
*/
|
*/
|
||||||
function normalizeSessionStatesFile(rawValue: unknown) {
|
function normalizeSessionStatesFile(rawValue: unknown): CloudtentaclesSessionStatesFile {
|
||||||
// Old format: { token: 'xxx', ... } (single object, no sessions key)
|
// Old format: { token: 'xxx', ... } (single object, no sessions key)
|
||||||
if (isPlainObject(rawValue) && !rawValue.sessions) {
|
if (isPlainObject(rawValue) && !rawValue.sessions) {
|
||||||
return {
|
return {
|
||||||
@@ -194,7 +198,7 @@ function createDefaultCloudtentaclesSessionState() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDefaultCloudtentaclesSessionStates() {
|
function createDefaultCloudtentaclesSessionStates(): CloudtentaclesSessionStatesFile {
|
||||||
return {
|
return {
|
||||||
sessions: {
|
sessions: {
|
||||||
'default': createDefaultCloudtentaclesSessionState(),
|
'default': createDefaultCloudtentaclesSessionState(),
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ export async function probeCloudtentaclesBindUrl(payload: JsonObject = {}) {
|
|||||||
finalUrl: bindUrl,
|
finalUrl: bindUrl,
|
||||||
reason: 'missing_signature_params',
|
reason: 'missing_signature_params',
|
||||||
roleInfo: normalizeAmsBindRoleInfo(null),
|
roleInfo: normalizeAmsBindRoleInfo(null),
|
||||||
raw: null,
|
raw: null as null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +247,7 @@ export async function probeCloudtentaclesBindUrl(payload: JsonObject = {}) {
|
|||||||
finalUrl: bindUrl,
|
finalUrl: bindUrl,
|
||||||
reason: error instanceof Error ? error.message : String(error || 'probe_failed'),
|
reason: error instanceof Error ? error.message : String(error || 'probe_failed'),
|
||||||
roleInfo: normalizeAmsBindRoleInfo(null),
|
roleInfo: normalizeAmsBindRoleInfo(null),
|
||||||
raw: null,
|
raw: null as null,
|
||||||
durationMs: Date.now() - startedAt,
|
durationMs: Date.now() - startedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -269,7 +269,7 @@ export async function getCloudtentaclesBindInfo(payload: JsonObject = {}) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const items = Array.isArray(result.payload?.data) ? result.payload.data : []
|
const items = Array.isArray(result.payload?.data) ? result.payload.data : []
|
||||||
const matchedItem = items.find((item) => Number(item?.id || 0) === id) || items[0] || {}
|
const matchedItem = items.find((item: JsonObject) => Number(item?.id || 0) === id) || items[0] || {}
|
||||||
const bindInfo = parseBindInfo(matchedItem?.bind_info)
|
const bindInfo = parseBindInfo(matchedItem?.bind_info)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from '../../open-91/shared.js'
|
} from '../../open-91/shared.js'
|
||||||
import { createHttpError } from '../../../utils/http.js'
|
import { createHttpError } from '../../../utils/http.js'
|
||||||
import { nowIso } from '../../../utils/time.js'
|
import { nowIso } from '../../../utils/time.js'
|
||||||
|
import type { OrderItemRow, OrderRow } from '../../../types/repository-rows.js'
|
||||||
|
|
||||||
export const OPEN_91_PENDING_CONFIG_STATUS = 'pending_config'
|
export const OPEN_91_PENDING_CONFIG_STATUS = 'pending_config'
|
||||||
export const OPEN_91_MANUAL_FAILED_STATUS = 'manual_failed'
|
export const OPEN_91_MANUAL_FAILED_STATUS = 'manual_failed'
|
||||||
@@ -68,7 +69,7 @@ export function buildOpen91SourceEvent(payload: JsonObject = {}, config: JsonObj
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function upsertOpen91PendingOrder(payload = {}, config = {}) {
|
export async function upsertOpen91PendingOrder(payload: JsonObject = {}, config: JsonObject = {}) {
|
||||||
const event = buildOpen91SourceEvent(payload, config)
|
const event = buildOpen91SourceEvent(payload, config)
|
||||||
const now = nowIso()
|
const now = nowIso()
|
||||||
const existing = await findOrderByPlatformOrderId({
|
const existing = await findOrderByPlatformOrderId({
|
||||||
@@ -127,11 +128,11 @@ export async function upsertOpen91PendingOrder(payload = {}, config = {}) {
|
|||||||
return {
|
return {
|
||||||
order,
|
order,
|
||||||
orderItems,
|
orderItems,
|
||||||
tasks: [],
|
tasks: [] as JsonObject[],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function retryOpen91Order(orderId) {
|
export async function retryOpen91Order(orderId: string | number) {
|
||||||
const order = await getRequiredOpen91Order(orderId)
|
const order = await getRequiredOpen91Order(orderId)
|
||||||
const items = await listOrderItemsByOrderId(order.id)
|
const items = await listOrderItemsByOrderId(order.id)
|
||||||
const event = buildOpen91SourceEventFromOrder(order, items)
|
const event = buildOpen91SourceEventFromOrder(order, items)
|
||||||
@@ -154,7 +155,7 @@ export async function retryOpen91Order(orderId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function failOpen91Order(orderId, reason = '') {
|
export async function failOpen91Order(orderId: string | number, reason = '') {
|
||||||
const order = await getRequiredOpen91Order(orderId)
|
const order = await getRequiredOpen91Order(orderId)
|
||||||
const now = nowIso()
|
const now = nowIso()
|
||||||
const rawPayload = parseJsonObject(order.raw_payload_json)
|
const rawPayload = parseJsonObject(order.raw_payload_json)
|
||||||
@@ -245,7 +246,7 @@ export async function listOpen91Orders({ page = 1, pageSize = 20, status = 'pend
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getRequiredOpen91Order(orderId) {
|
export async function getRequiredOpen91Order(orderId: string | number) {
|
||||||
const order = await getOrderById(Number(orderId))
|
const order = await getOrderById(Number(orderId))
|
||||||
|
|
||||||
if (!order || order.provider !== OPEN_91_PROVIDER || order.platform !== OPEN_91_PLATFORM) {
|
if (!order || order.provider !== OPEN_91_PROVIDER || order.platform !== OPEN_91_PLATFORM) {
|
||||||
@@ -258,11 +259,11 @@ export async function getRequiredOpen91Order(orderId) {
|
|||||||
return order
|
return order
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildOpen91SourceEventFromOrder(order, items = []) {
|
function buildOpen91SourceEventFromOrder(order: OrderRow, items: OrderItemRow[] = []) {
|
||||||
const rawPayload = parseJsonObject(order.raw_payload_json)
|
const rawPayload = parseJsonObject(order.raw_payload_json)
|
||||||
const body = parseJsonObject(rawPayload.body)
|
const body = parseJsonObject(rawPayload.body)
|
||||||
const normalizedItems = Array.isArray(items) ? items : []
|
const normalizedItems = Array.isArray(items) ? items : []
|
||||||
const eventItems = normalizedItems.map((item) => {
|
const eventItems = normalizedItems.map((item: OrderItemRow) => {
|
||||||
const snapshot = parseJsonObject(item.item_snapshot_json)
|
const snapshot = parseJsonObject(item.item_snapshot_json)
|
||||||
const productNo = String(snapshot.productNo || snapshot.externalSkuCode || item.sku_code || '').trim()
|
const productNo = String(snapshot.productNo || snapshot.externalSkuCode || item.sku_code || '').trim()
|
||||||
|
|
||||||
@@ -301,7 +302,7 @@ function buildOpen91SourceEventFromOrder(order, items = []) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapOpen91AdminOrderRow(row) {
|
function mapOpen91AdminOrderRow(row: JsonObject) {
|
||||||
const rawPayload = parseJsonObject(row.raw_payload_json)
|
const rawPayload = parseJsonObject(row.raw_payload_json)
|
||||||
const items = Array.isArray(row.items_json) ? row.items_json : []
|
const items = Array.isArray(row.items_json) ? row.items_json : []
|
||||||
const firstItem = items[0] || {}
|
const firstItem = items[0] || {}
|
||||||
@@ -327,7 +328,7 @@ function mapOpen91AdminOrderRow(row) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseJsonObject(value) {
|
function parseJsonObject(value: unknown): JsonObject {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ async function runCloudtentaclesHealthAccount(
|
|||||||
): Promise<CloudtentaclesHealthAccountResult> {
|
): Promise<CloudtentaclesHealthAccountResult> {
|
||||||
const sourceKey = String(account.sourceKey || '').trim() || 'default'
|
const sourceKey = String(account.sourceKey || '').trim() || 'default'
|
||||||
const source = getCloudtentaclesSourceByKey(sourceKey)
|
const source = getCloudtentaclesSourceByKey(sourceKey)
|
||||||
const session = getCloudtentaclesSessionStateByKey(sourceKey) || {}
|
const session: JsonObject = getCloudtentaclesSessionStateByKey(sourceKey) || {}
|
||||||
const label = String(account.label || source?.label || source?.username || sourceKey).trim() || sourceKey
|
const label = String(account.label || source?.label || source?.username || sourceKey).trim() || sourceKey
|
||||||
const threshold = normalizeNonNegativeInteger(account.assetThreshold, 500)
|
const threshold = normalizeNonNegativeInteger(account.assetThreshold, 500)
|
||||||
const checkedAt = new Date().toISOString()
|
const checkedAt = new Date().toISOString()
|
||||||
|
|||||||
@@ -1,6 +1,26 @@
|
|||||||
export type StartupState = ReturnType<typeof createStartupState>;
|
type StartupProcessError = {
|
||||||
|
time: string;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
export function createStartupState() {
|
export type StartupState = {
|
||||||
|
phase: string;
|
||||||
|
startedAt: string;
|
||||||
|
core: {
|
||||||
|
ready: boolean;
|
||||||
|
running: boolean;
|
||||||
|
attemptCount: number;
|
||||||
|
readyAt: string;
|
||||||
|
lastAttemptAt: string;
|
||||||
|
lastError: string;
|
||||||
|
};
|
||||||
|
process: {
|
||||||
|
lastUnhandledRejection: StartupProcessError | null;
|
||||||
|
lastUncaughtException: StartupProcessError | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createStartupState(): StartupState {
|
||||||
return {
|
return {
|
||||||
phase: "starting",
|
phase: "starting",
|
||||||
startedAt: new Date().toISOString(),
|
startedAt: new Date().toISOString(),
|
||||||
|
|||||||
@@ -29,13 +29,16 @@ export function buildSuccessPayload(data: unknown, msg: string = "ok") {
|
|||||||
|
|
||||||
export function buildErrorPayload(error: unknown, fallbackMessage: string) {
|
export function buildErrorPayload(error: unknown, fallbackMessage: string) {
|
||||||
const classification = classifyRouteError(error);
|
const classification = classifyRouteError(error);
|
||||||
|
const message = classification.statusCode >= 500
|
||||||
|
? fallbackMessage || "服务内部错误"
|
||||||
|
: error instanceof Error ? error.message : fallbackMessage;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code: 1,
|
code: 1,
|
||||||
msg: error instanceof Error ? error.message : fallbackMessage,
|
msg: message,
|
||||||
errorCode: classification.errorCode,
|
errorCode: classification.errorCode,
|
||||||
time: Math.floor(Date.now() / 1000),
|
time: Math.floor(Date.now() / 1000),
|
||||||
data: null,
|
data: null as null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +65,7 @@ export function buildNotFoundPayload(req: Request) {
|
|||||||
code: 1,
|
code: 1,
|
||||||
msg: `未实现接口: ${req.method} ${req.originalUrl}`,
|
msg: `未实现接口: ${req.method} ${req.originalUrl}`,
|
||||||
time: Math.floor(Date.now() / 1000),
|
time: Math.floor(Date.now() / 1000),
|
||||||
data: null,
|
data: null as null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,3 +102,27 @@ test('formatLogEntry prints nested detail blocks without ansi colors in file mod
|
|||||||
assert.match(text, /未登录或登录已失效/)
|
assert.match(text, /未登录或登录已失效/)
|
||||||
assert.doesNotMatch(text, /\u001B\[/)
|
assert.doesNotMatch(text, /\u001B\[/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('formatLogEntry masks sensitive fields in inline and nested details', () => {
|
||||||
|
const text = formatLogEntry({
|
||||||
|
time: '2026-04-14T10:36:24.974Z',
|
||||||
|
level: 'info',
|
||||||
|
scope: '[security]',
|
||||||
|
message: 'masked',
|
||||||
|
pid: 4671,
|
||||||
|
detail: {
|
||||||
|
token: 'abcdef1234567890',
|
||||||
|
authorization: 'Bearer abcdef1234567890',
|
||||||
|
nested: {
|
||||||
|
cookie: 'sessionid=abcdef1234567890',
|
||||||
|
note: 'password=super-secret-value',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, { color: false })
|
||||||
|
|
||||||
|
assert.doesNotMatch(text, /abcdef1234567890/)
|
||||||
|
assert.doesNotMatch(text, /super-secret-value/)
|
||||||
|
assert.match(text, /abcdef\*\*\*\*567890/)
|
||||||
|
assert.match(text, /Bearer\*\*\*\*567890/)
|
||||||
|
assert.match(text, /password=super-\*\*\*\*-value/)
|
||||||
|
})
|
||||||
|
|||||||
@@ -5,15 +5,29 @@ import util from 'node:util'
|
|||||||
|
|
||||||
import { PROJECT_ROOT, runtimeConfig } from '../config/runtime.js'
|
import { PROJECT_ROOT, runtimeConfig } from '../config/runtime.js'
|
||||||
import type { HttpErrorLike } from './http.js'
|
import type { HttpErrorLike } from './http.js'
|
||||||
|
import { maskSecret } from './masking.js'
|
||||||
|
|
||||||
const LOG_LEVEL_PRIORITY = {
|
type LogLevel = 'debug' | 'info' | 'warn' | 'error'
|
||||||
|
type LogChannel = 'app' | 'integration'
|
||||||
|
type LogDetail = unknown
|
||||||
|
type InlineFields = Record<string, string | number | boolean | null | undefined>
|
||||||
|
type LogEntry = {
|
||||||
|
time: string
|
||||||
|
level: LogLevel
|
||||||
|
scope: string
|
||||||
|
message: string
|
||||||
|
pid: number
|
||||||
|
detail?: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
|
||||||
debug: 10,
|
debug: 10,
|
||||||
info: 20,
|
info: 20,
|
||||||
warn: 30,
|
warn: 30,
|
||||||
error: 40,
|
error: 40,
|
||||||
}
|
}
|
||||||
|
|
||||||
const LEVEL_LABELS = {
|
const LEVEL_LABELS: Record<LogLevel, string> = {
|
||||||
debug: 'DEBUG',
|
debug: 'DEBUG',
|
||||||
info: 'INFO ',
|
info: 'INFO ',
|
||||||
warn: 'WARN ',
|
warn: 'WARN ',
|
||||||
@@ -36,35 +50,48 @@ const LOG_DIR = path.join(DATA_ROOT, 'logs')
|
|||||||
const ACTIVE_LOG_LEVEL = normalizeLogLevel(runtimeConfig.logging?.level)
|
const ACTIVE_LOG_LEVEL = normalizeLogLevel(runtimeConfig.logging?.level)
|
||||||
const LOG_RETENTION_DAYS = 7
|
const LOG_RETENTION_DAYS = 7
|
||||||
const LEGACY_LOG_FILES = new Set(['app.log', 'integration.log'])
|
const LEGACY_LOG_FILES = new Set(['app.log', 'integration.log'])
|
||||||
|
const SENSITIVE_KEY_PATTERN = /token|secret|password|passwd|pwd|cookie|authorization|credential|card|cards|cardno|cardpwd|apikey|api_key|devicekey|session/i
|
||||||
|
const MAX_SANITIZE_DEPTH = 8
|
||||||
|
|
||||||
let writeQueue = Promise.resolve()
|
let writeQueue = Promise.resolve()
|
||||||
let lastCleanupDateKey = ''
|
let lastCleanupDateKey = ''
|
||||||
|
|
||||||
export function createRequestId(prefix = 'req') {
|
export function createRequestId(prefix = 'req'): string {
|
||||||
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
|
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logDebug(scope, message, detail = undefined) {
|
export function logDebug(scope: unknown, message: unknown, detail: LogDetail = undefined) {
|
||||||
return writeLog('debug', scope, message, detail)
|
return writeLog('debug', scope, message, detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logInfo(scope, message, detail = undefined) {
|
export function logInfo(scope: unknown, message: unknown, detail: LogDetail = undefined) {
|
||||||
return writeLog('info', scope, message, detail)
|
return writeLog('info', scope, message, detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logWarn(scope, message, detail = undefined) {
|
export function logWarn(scope: unknown, message: unknown, detail: LogDetail = undefined) {
|
||||||
return writeLog('warn', scope, message, detail)
|
return writeLog('warn', scope, message, detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logError(scope, message, detail = undefined) {
|
export function logError(scope: unknown, message: unknown, detail: LogDetail = undefined) {
|
||||||
return writeLog('error', scope, message, detail)
|
return writeLog('error', scope, message, detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logIntegration(scope, message, detail = undefined, { level = 'info' } = {}) {
|
export function logIntegration(
|
||||||
|
scope: unknown,
|
||||||
|
message: unknown,
|
||||||
|
detail: LogDetail = undefined,
|
||||||
|
{ level = 'info' }: { level?: LogLevel } = {},
|
||||||
|
) {
|
||||||
return writeLog(level, scope, message, detail, { channel: 'integration' })
|
return writeLog(level, scope, message, detail, { channel: 'integration' })
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeLog(level, scope, message, detail, { channel = 'app' } = {}) {
|
function writeLog(
|
||||||
|
level: LogLevel,
|
||||||
|
scope: unknown,
|
||||||
|
message: unknown,
|
||||||
|
detail: LogDetail,
|
||||||
|
{ channel = 'app' }: { channel?: LogChannel } = {},
|
||||||
|
): LogEntry | null {
|
||||||
if (!shouldWriteLog(level, ACTIVE_LOG_LEVEL)) {
|
if (!shouldWriteLog(level, ACTIVE_LOG_LEVEL)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -83,7 +110,7 @@ function writeLog(level, scope, message, detail, { channel = 'app' } = {}) {
|
|||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
function enqueueFileWrite(entry, filePath) {
|
function enqueueFileWrite(entry: LogEntry, filePath: string): void {
|
||||||
const line = formatLogEntry(entry, { color: false })
|
const line = formatLogEntry(entry, { color: false })
|
||||||
const dateKey = extractLogDateKey(entry.time)
|
const dateKey = extractLogDateKey(entry.time)
|
||||||
|
|
||||||
@@ -99,12 +126,12 @@ function enqueueFileWrite(entry, filePath) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeConsole(entry) {
|
function writeConsole(entry: LogEntry): void {
|
||||||
const logger = resolveConsoleMethod(entry.level)
|
const logger = resolveConsoleMethod(entry.level)
|
||||||
logger(formatLogEntry(entry, { color: supportsAnsiColor() }))
|
logger(formatLogEntry(entry, { color: supportsAnsiColor() }))
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveConsoleMethod(level) {
|
function resolveConsoleMethod(level: unknown) {
|
||||||
const normalized = normalizeLogLevel(level)
|
const normalized = normalizeLogLevel(level)
|
||||||
|
|
||||||
if (normalized === 'error') {
|
if (normalized === 'error') {
|
||||||
@@ -118,26 +145,26 @@ function resolveConsoleMethod(level) {
|
|||||||
return console.log
|
return console.log
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeLogLevel(level) {
|
export function normalizeLogLevel(level: unknown): LogLevel {
|
||||||
const normalized = String(level || '').trim().toLowerCase()
|
const normalized = String(level || '').trim().toLowerCase()
|
||||||
return LOG_LEVEL_PRIORITY[normalized] != null ? normalized : 'info'
|
return isLogLevel(normalized) ? normalized : 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldWriteLog(level, configuredLevel = ACTIVE_LOG_LEVEL) {
|
export function shouldWriteLog(level: unknown, configuredLevel: unknown = ACTIVE_LOG_LEVEL): boolean {
|
||||||
const normalizedLevel = normalizeLogLevel(level)
|
const normalizedLevel = normalizeLogLevel(level)
|
||||||
const normalizedConfigured = normalizeLogLevel(configuredLevel)
|
const normalizedConfigured = normalizeLogLevel(configuredLevel)
|
||||||
|
|
||||||
return LOG_LEVEL_PRIORITY[normalizedLevel] >= LOG_LEVEL_PRIORITY[normalizedConfigured]
|
return LOG_LEVEL_PRIORITY[normalizedLevel] >= LOG_LEVEL_PRIORITY[normalizedConfigured]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatLogEntry(entry, { color = false } = {}) {
|
export function formatLogEntry(entry: Partial<LogEntry>, { color = false }: { color?: boolean } = {}): string {
|
||||||
const normalizedEntry = {
|
const normalizedEntry = {
|
||||||
time: String(entry?.time || new Date().toISOString()),
|
time: String(entry?.time || new Date().toISOString()),
|
||||||
level: normalizeLogLevel(entry?.level),
|
level: normalizeLogLevel(entry?.level),
|
||||||
scope: String(entry?.scope || 'app').trim() || 'app',
|
scope: String(entry?.scope || 'app').trim() || 'app',
|
||||||
message: String(entry?.message || '').trim() || '-',
|
message: String(entry?.message || '').trim() || '-',
|
||||||
pid: Number(entry?.pid || process.pid),
|
pid: Number(entry?.pid || process.pid),
|
||||||
detail: entry?.detail,
|
detail: sanitizeLogValue(entry?.detail),
|
||||||
}
|
}
|
||||||
const timestamp = colorize(formatLogTimestamp(normalizedEntry.time), ANSI.gray, color)
|
const timestamp = colorize(formatLogTimestamp(normalizedEntry.time), ANSI.gray, color)
|
||||||
const level = colorize(LEVEL_LABELS[normalizedEntry.level], resolveLevelColor(normalizedEntry.level), color)
|
const level = colorize(LEVEL_LABELS[normalizedEntry.level], resolveLevelColor(normalizedEntry.level), color)
|
||||||
@@ -166,16 +193,16 @@ export function formatLogEntry(entry, { color = false } = {}) {
|
|||||||
return `${firstLine}\n${indentBlock(inspected, ' ')}`
|
return `${firstLine}\n${indentBlock(inspected, ' ')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveLogFilePath(channel = 'app', time = new Date().toISOString()) {
|
export function resolveLogFilePath(channel = 'app', time = new Date().toISOString()): string {
|
||||||
const normalizedChannel = String(channel || '').trim() === 'integration' ? 'integration' : 'app'
|
const normalizedChannel = String(channel || '').trim() === 'integration' ? 'integration' : 'app'
|
||||||
return path.join(LOG_DIR, `${normalizedChannel}-${extractLogDateKey(time)}.log`)
|
return path.join(LOG_DIR, `${normalizedChannel}-${extractLogDateKey(time)}.log`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveExpiredLogFilenames(
|
export function resolveExpiredLogFilenames(
|
||||||
fileNames = [],
|
fileNames: string[] = [],
|
||||||
referenceTime = new Date().toISOString(),
|
referenceTime = new Date().toISOString(),
|
||||||
retentionDays = LOG_RETENTION_DAYS,
|
retentionDays = LOG_RETENTION_DAYS,
|
||||||
) {
|
): string[] {
|
||||||
const normalizedRetentionDays = Math.max(1, Number(retentionDays) || LOG_RETENTION_DAYS)
|
const normalizedRetentionDays = Math.max(1, Number(retentionDays) || LOG_RETENTION_DAYS)
|
||||||
const cutoffDateKey = toDateKey(offsetDate(referenceTime, -(normalizedRetentionDays - 1)))
|
const cutoffDateKey = toDateKey(offsetDate(referenceTime, -(normalizedRetentionDays - 1)))
|
||||||
|
|
||||||
@@ -183,8 +210,8 @@ export function resolveExpiredLogFilenames(
|
|||||||
.filter((fileName) => shouldDeleteLogFileByDate(fileName, cutoffDateKey))
|
.filter((fileName) => shouldDeleteLogFileByDate(fileName, cutoffDateKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatLogTimestamp(value) {
|
export function formatLogTimestamp(value: unknown): string {
|
||||||
const date = new Date(value)
|
const date = new Date(value instanceof Date ? value : String(value || ''))
|
||||||
|
|
||||||
if (Number.isNaN(date.getTime())) {
|
if (Number.isNaN(date.getTime())) {
|
||||||
return String(value || '').trim() || new Date().toISOString()
|
return String(value || '').trim() || new Date().toISOString()
|
||||||
@@ -193,27 +220,28 @@ export function formatLogTimestamp(value) {
|
|||||||
return `${formatLocalDate(date)} ${formatLocalTime(date)}.${String(date.getMilliseconds()).padStart(3, '0')} ${formatTimezoneOffset(date)}`
|
return `${formatLocalDate(date)} ${formatLocalTime(date)}.${String(date.getMilliseconds()).padStart(3, '0')} ${formatTimezoneOffset(date)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveDataRoot() {
|
function resolveDataRoot(): string {
|
||||||
const configured = String(runtimeConfig.data?.root || '').trim()
|
const configured = String(runtimeConfig.data?.root || '').trim()
|
||||||
return configured ? path.resolve(configured) : path.resolve(PROJECT_ROOT, 'data')
|
return configured ? path.resolve(configured) : path.resolve(PROJECT_ROOT, 'data')
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeLogValue(value) {
|
function normalizeLogValue(value: unknown): unknown {
|
||||||
if (typeof value === 'undefined') {
|
if (typeof value === 'undefined') {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return JSON.parse(
|
return sanitizeLogValue(JSON.parse(
|
||||||
JSON.stringify(value, (_key, current) => {
|
JSON.stringify(value, (_key, current) => {
|
||||||
if (current instanceof Error) {
|
if (current instanceof Error) {
|
||||||
const currentError = current as Error & HttpErrorLike
|
const currentError = current as Error & HttpErrorLike
|
||||||
return {
|
return {
|
||||||
name: current.name,
|
name: current.name,
|
||||||
message: current.message,
|
message: sanitizeLogString(current.message),
|
||||||
stack: current.stack,
|
stack: sanitizeLogString(current.stack),
|
||||||
statusCode: currentError.statusCode,
|
statusCode: currentError.statusCode,
|
||||||
errorCode: currentError.errorCode,
|
errorCode: currentError.errorCode,
|
||||||
|
context: sanitizeLogValue(currentError.context),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,13 +251,71 @@ function normalizeLogValue(value) {
|
|||||||
|
|
||||||
return current
|
return current
|
||||||
}),
|
}),
|
||||||
)
|
))
|
||||||
} catch {
|
} catch {
|
||||||
return String(value)
|
return sanitizeLogString(String(value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function splitDetailPayload(detail) {
|
function sanitizeLogValue(value: unknown, key = '', depth = 0): unknown {
|
||||||
|
if (typeof value === 'undefined') {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSensitiveLogKey(key)) {
|
||||||
|
return maskSecret(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return sanitizeLogString(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'number' || typeof value === 'boolean') {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'bigint') {
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depth >= MAX_SANITIZE_DEPTH) {
|
||||||
|
return '[MaxDepth]'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((item) => sanitizeLogValue(item, key, depth + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(value).map(([currentKey, currentValue]) => [
|
||||||
|
currentKey,
|
||||||
|
sanitizeLogValue(currentValue, currentKey, depth + 1),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitizeLogString(String(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeLogString(value: unknown): string {
|
||||||
|
return String(value || '')
|
||||||
|
.replace(/(Bearer\s+)([A-Za-z0-9._~+/=-]+)/gi, (_matched, prefix, secret) => `${prefix}${maskSecret(secret)}`)
|
||||||
|
.replace(
|
||||||
|
/\b((?:token|secret|password|passwd|pwd|cookie|authorization|api[_-]?key|device[_-]?key)=)([^&\s,;]+)/gi,
|
||||||
|
(_matched, prefix, secret) => `${prefix}${maskSecret(secret)}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSensitiveLogKey(key: unknown): boolean {
|
||||||
|
return SENSITIVE_KEY_PATTERN.test(String(key || '').trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitDetailPayload(detail: unknown): { inline: InlineFields, block: unknown | null } {
|
||||||
if (typeof detail === 'undefined') {
|
if (typeof detail === 'undefined') {
|
||||||
return {
|
return {
|
||||||
inline: {},
|
inline: {},
|
||||||
@@ -252,8 +338,8 @@ function splitDetailPayload(detail) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (detail && typeof detail === 'object') {
|
if (detail && typeof detail === 'object') {
|
||||||
const inline = {}
|
const inline: InlineFields = {}
|
||||||
const block = {}
|
const block: Record<string, unknown> = {}
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(detail)) {
|
for (const [key, value] of Object.entries(detail)) {
|
||||||
if (isScalarLogValue(value)) {
|
if (isScalarLogValue(value)) {
|
||||||
@@ -276,14 +362,14 @@ function splitDetailPayload(detail) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatInlineFields(fields) {
|
function formatInlineFields(fields: InlineFields): string {
|
||||||
return Object.entries(fields)
|
return Object.entries(fields)
|
||||||
.filter(([, value]) => typeof value !== 'undefined' && value !== '')
|
.filter(([, value]) => typeof value !== 'undefined' && value !== '')
|
||||||
.map(([key, value]) => `${key}=${formatInlineValue(value)}`)
|
.map(([key, value]) => `${key}=${formatInlineValue(value)}`)
|
||||||
.join(' ')
|
.join(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatInlineValue(value) {
|
function formatInlineValue(value: unknown): string {
|
||||||
if (value === null) {
|
if (value === null) {
|
||||||
return 'null'
|
return 'null'
|
||||||
}
|
}
|
||||||
@@ -296,22 +382,22 @@ function formatInlineValue(value) {
|
|||||||
return /^[A-Za-z0-9_./:@-]+$/.test(text) ? text : JSON.stringify(text)
|
return /^[A-Za-z0-9_./:@-]+$/.test(text) ? text : JSON.stringify(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isScalarLogValue(value) {
|
function isScalarLogValue(value: unknown): value is string | number | boolean | null {
|
||||||
return value === null || ['string', 'number', 'boolean'].includes(typeof value)
|
return value === null || ['string', 'number', 'boolean'].includes(typeof value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function indentBlock(text, indent) {
|
function indentBlock(text: unknown, indent: string): string {
|
||||||
return String(text || '')
|
return String(text || '')
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map((line) => `${indent}${line}`)
|
.map((line) => `${indent}${line}`)
|
||||||
.join('\n')
|
.join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
function supportsAnsiColor() {
|
function supportsAnsiColor(): boolean {
|
||||||
return process.env.NO_COLOR !== '1' && Boolean(process.stdout.isTTY || process.stderr.isTTY)
|
return process.env.NO_COLOR !== '1' && Boolean(process.stdout.isTTY || process.stderr.isTTY)
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveLevelColor(level) {
|
function resolveLevelColor(level: unknown): string {
|
||||||
switch (normalizeLogLevel(level)) {
|
switch (normalizeLogLevel(level)) {
|
||||||
case 'debug':
|
case 'debug':
|
||||||
return ANSI.blue
|
return ANSI.blue
|
||||||
@@ -324,7 +410,7 @@ function resolveLevelColor(level) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function colorize(text, ansiCode, enabled) {
|
function colorize(text: string, ansiCode: string, enabled: boolean): string {
|
||||||
if (!enabled || !ansiCode) {
|
if (!enabled || !ansiCode) {
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
@@ -332,15 +418,15 @@ function colorize(text, ansiCode, enabled) {
|
|||||||
return `${ansiCode}${text}${ANSI.reset}`
|
return `${ansiCode}${text}${ANSI.reset}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatLocalDate(date) {
|
function formatLocalDate(date: Date): string {
|
||||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatLocalTime(date) {
|
function formatLocalTime(date: Date): string {
|
||||||
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
|
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTimezoneOffset(date) {
|
function formatTimezoneOffset(date: Date): string {
|
||||||
const totalMinutes = -date.getTimezoneOffset()
|
const totalMinutes = -date.getTimezoneOffset()
|
||||||
const sign = totalMinutes >= 0 ? '+' : '-'
|
const sign = totalMinutes >= 0 ? '+' : '-'
|
||||||
const absoluteMinutes = Math.abs(totalMinutes)
|
const absoluteMinutes = Math.abs(totalMinutes)
|
||||||
@@ -349,7 +435,7 @@ function formatTimezoneOffset(date) {
|
|||||||
return `${sign}${hours}:${minutes}`
|
return `${sign}${hours}:${minutes}`
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cleanupExpiredLogsIfNeeded(dateKey) {
|
async function cleanupExpiredLogsIfNeeded(dateKey: string): Promise<void> {
|
||||||
if (!dateKey || dateKey === lastCleanupDateKey) {
|
if (!dateKey || dateKey === lastCleanupDateKey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -366,13 +452,13 @@ async function cleanupExpiredLogsIfNeeded(dateKey) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractLogDateKey(time) {
|
function extractLogDateKey(time: unknown): string {
|
||||||
const normalized = String(time || '').trim()
|
const normalized = String(time || '').trim()
|
||||||
return /^\d{4}-\d{2}-\d{2}/.test(normalized) ? normalized.slice(0, 10) : toDateKey(normalized)
|
return /^\d{4}-\d{2}-\d{2}/.test(normalized) ? normalized.slice(0, 10) : toDateKey(normalized)
|
||||||
}
|
}
|
||||||
|
|
||||||
function toDateKey(value) {
|
function toDateKey(value: unknown): string {
|
||||||
const date = new Date(value)
|
const date = new Date(value instanceof Date ? value : String(value || ''))
|
||||||
|
|
||||||
if (Number.isNaN(date.getTime())) {
|
if (Number.isNaN(date.getTime())) {
|
||||||
return new Date().toISOString().slice(0, 10)
|
return new Date().toISOString().slice(0, 10)
|
||||||
@@ -381,13 +467,13 @@ function toDateKey(value) {
|
|||||||
return date.toISOString().slice(0, 10)
|
return date.toISOString().slice(0, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
function offsetDate(value, offsetDays) {
|
function offsetDate(value: unknown, offsetDays: unknown): Date {
|
||||||
const date = new Date(value)
|
const date = new Date(value instanceof Date ? value : String(value || ''))
|
||||||
date.setUTCDate(date.getUTCDate() + Number(offsetDays || 0))
|
date.setUTCDate(date.getUTCDate() + Number(offsetDays || 0))
|
||||||
return date
|
return date
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldDeleteLogFileByDate(fileName, cutoffDateKey) {
|
function shouldDeleteLogFileByDate(fileName: unknown, cutoffDateKey: string): boolean {
|
||||||
if (LEGACY_LOG_FILES.has(String(fileName || '').trim())) {
|
if (LEGACY_LOG_FILES.has(String(fileName || '').trim())) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -398,5 +484,9 @@ function shouldDeleteLogFileByDate(fileName, cutoffDateKey) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return matched[2] < cutoffDateKey
|
return String(matched[2] || '') < cutoffDateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLogLevel(value: string): value is LogLevel {
|
||||||
|
return ['debug', 'info', 'warn', 'error'].includes(value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"checkJs": true,
|
"checkJs": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"strict": false,
|
"strict": false,
|
||||||
|
"noImplicitAny": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"types": ["node"],
|
"types": ["node"],
|
||||||
"lib": ["ES2022"]
|
"lib": ["ES2022"]
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ function shouldClearAdminSession(errorCode: string) {
|
|||||||
'admin_auth_invalid',
|
'admin_auth_invalid',
|
||||||
'admin_auth_expired',
|
'admin_auth_expired',
|
||||||
'admin_auth_user_invalid',
|
'admin_auth_user_invalid',
|
||||||
|
'admin_auth_stale',
|
||||||
].includes(errorCode)
|
].includes(errorCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user