/**
* FX-CRM 人員驗證服務
* 用 FSUID 查詢 PersonnelObj 取得用戶身份和權限
*/
import type { UserIdentity } from '@mcp-internal/shared'
import { getAppSecrets } from '../../utils/secrets.js'
/** FX-CRM Access Token 快取 */
let accessTokenCache: { token: string; expiresAt: number } | null = null
/** 用戶快取(FSUID → UserIdentity) */
const userCache = new Map<string, { user: UserIdentity; expiresAt: number }>()
const USER_CACHE_TTL_MS = 5 * 60 * 1000 // 5 分鐘
/**
* 取得 FX-CRM Corp Access Token
*/
async function getCorpAccessToken(): Promise<string> {
// 檢查快取
if (accessTokenCache && accessTokenCache.expiresAt > Date.now()) {
return accessTokenCache.token
}
const secrets = await getAppSecrets()
const response = await fetch(
`https://open.fxiaoke.com/cgi/corpAccessToken/get/V2`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
appId: secrets.fxcrmAppId,
appSecret: secrets.fxcrmAppSecret,
permanentCode: secrets.fxcrmPermanentCode,
}),
}
)
const data = await response.json() as any
if (data.errorCode !== 0) {
throw new Error(`FX-CRM 取得 Access Token 失敗: ${data.errorMessage}`)
}
// 快取 token(提前 5 分鐘過期)
accessTokenCache = {
token: data.corpAccessToken,
expiresAt: Date.now() + (data.expiresIn - 300) * 1000,
}
return data.corpAccessToken
}
/**
* 查詢 FX-CRM 資料
*/
async function queryFxcrm(
objectApiName: string,
filters: Array<{ field_name: string; field_values: string[]; operator: string }>
): Promise<any[]> {
const secrets = await getAppSecrets()
const accessToken = await getCorpAccessToken()
const response = await fetch(
`https://open.fxiaoke.com/cgi/crm/v2/data/query`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
corpAccessToken: accessToken,
corpId: secrets.fxcrmCorpId,
currentOpenUserId: secrets.fxcrmDefaultUserId, // 管理員帳號
data: {
dataObjectApiName: objectApiName,
search_query_info: {
limit: 1,
offset: 0,
filters,
},
},
}),
}
)
const data = await response.json() as any
if (data.errorCode !== 0) {
throw new Error(`FX-CRM 查詢失敗: ${data.errorMessage}`)
}
return data.data?.dataList || []
}
/**
* 用 FSUID 驗證用戶身份
* @param fsuid FX-CRM 用戶 ID(格式:FSUID_xxxxx)
* @returns 用戶身份,如果無效則返回 null
*/
export async function validateFsuid(fsuid: string): Promise<UserIdentity | null> {
// 格式驗證
if (!fsuid || !fsuid.startsWith('FSUID_')) {
return null
}
// 檢查快取
const cached = userCache.get(fsuid)
if (cached && cached.expiresAt > Date.now()) {
return cached.user
}
try {
// 查詢 PersonnelObj,用 user_id 欄位匹配
const results = await queryFxcrm('PersonnelObj', [
{
field_name: 'user_id',
field_values: [fsuid],
operator: 'EQ',
},
])
if (results.length === 0) {
return null
}
const personnel = results[0]
// 檢查人員是否有效
if (personnel.is_deleted || personnel.life_status !== 'normal') {
return null
}
const user: UserIdentity = {
fsuid: personnel.user_id,
name: personnel.name,
permission: personnel.mcp_permission__c || 'viewer',
authenticatedAt: new Date(),
}
// 存入快取
userCache.set(fsuid, {
user,
expiresAt: Date.now() + USER_CACHE_TTL_MS,
})
return user
} catch (error) {
console.error('FX-CRM FSUID 驗證失敗:', error)
return null
}
}
/**
* 清除用戶快取
*/
export function clearUserCache(mcpToken?: string): void {
if (mcpToken) {
userCache.delete(mcpToken)
} else {
userCache.clear()
}
}
/**
* 取得快取統計
*/
export function getCacheStats(): { userCacheSize: number; hasAccessToken: boolean } {
return {
userCacheSize: userCache.size,
hasAccessToken: accessTokenCache !== null && accessTokenCache.expiresAt > Date.now(),
}
}