All files / dingtalk-openclaw-connector/src/utils token.ts

10.2% Statements 5/49
100% Branches 0/0
0% Functions 0/3
10.2% Lines 5/49

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 851x                   1x 1x                       1x 1x                                                                                                                        
/**
 * Access Token 管理模块
 * 支持钉钉 API 和 OAPI 的 Token 获取和缓存
 */
 
import axios from 'axios';
import type { DingtalkConfig } from '../types/index.ts';
 
// ============ 常量 ============
 
export const DINGTALK_API = 'https://api.dingtalk.com';
export const DINGTALK_OAPI = 'https://oapi.dingtalk.com';
 
// ============ Access Token 缓存 ============
 
type CachedToken = {
  token: string;
  expiryMs: number;
};
 
/**
 * 按 clientId 分桶缓存,避免多账号串 token。
 */
const apiTokenCache = new Map<string, CachedToken>();
const oapiTokenCache = new Map<string, CachedToken>();
 
function cacheKey(config: DingtalkConfig): string {
  // clientId 可能来自多账号合并配置,理论上必填;这里做兜底避免 Map key 为 undefined
  return String((config as any)?.clientId ?? '').trim();
}
 
/**
 * 获取钉钉 Access Token(新版 API)
 */
export async function getAccessToken(config: DingtalkConfig): Promise<string> {
  const now = Date.now();
  const key = cacheKey(config);
  const cached = apiTokenCache.get(key);
  if (cached && cached.expiryMs > now + 60_000) {
    return cached.token;
  }
 
  const response = await axios.post(`${DINGTALK_API}/v1.0/oauth2/accessToken`, {
    appKey: config.clientId,
    appSecret: config.clientSecret,
  });
 
  const token = response.data.accessToken as string;
  const expireInSec = Number(response.data.expireIn ?? 0);
  apiTokenCache.set(key, {
    token,
    expiryMs: now + expireInSec * 1000,
  });
  return token;
}
 
/**
 * 获取钉钉 OAPI Access Token(旧版 API,用于媒体上传等)
 */
export async function getOapiAccessToken(config: DingtalkConfig): Promise<string | null> {
  try {
    const now = Date.now();
    const key = cacheKey(config);
    const cached = oapiTokenCache.get(key);
    if (cached && cached.expiryMs > now + 60_000) {
      return cached.token;
    }
 
    const resp = await axios.get(`${DINGTALK_OAPI}/gettoken`, {
      params: { appkey: config.clientId, appsecret: config.clientSecret },
    });
 
    if (resp.data?.errcode === 0 && resp.data?.access_token) {
      const token = String(resp.data.access_token);
      // 钉钉返回 expires_in(秒),拿不到就按 2 小时兜底
      const expiresInSec = Number(resp.data.expires_in ?? 7200);
      oapiTokenCache.set(key, { token, expiryMs: now + expiresInSec * 1000 });
      return token;
    }
    return null;
  } catch {
    return null;
  }
}