@insta-dev01/intclaw 1.0.10 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +1 -1
  2. package/README.en.md +424 -0
  3. package/README.md +365 -164
  4. package/index.ts +28 -0
  5. package/openclaw.plugin.json +12 -41
  6. package/package.json +69 -40
  7. package/src/channel.ts +557 -0
  8. package/src/config/accounts.ts +230 -0
  9. package/src/config/schema.ts +144 -0
  10. package/src/core/connection.ts +733 -0
  11. package/src/core/message-handler.ts +1268 -0
  12. package/src/core/provider.ts +106 -0
  13. package/src/core/state.ts +54 -0
  14. package/src/directory.ts +95 -0
  15. package/src/gateway-methods.ts +237 -0
  16. package/src/onboarding.ts +387 -0
  17. package/src/policy.ts +19 -0
  18. package/src/probe.ts +213 -0
  19. package/src/reply-dispatcher.ts +674 -0
  20. package/src/runtime.ts +7 -0
  21. package/src/sdk/helpers.ts +317 -0
  22. package/src/sdk/types.ts +515 -0
  23. package/src/secret-input.ts +19 -0
  24. package/src/services/media/audio.ts +54 -0
  25. package/src/services/media/chunk-upload.ts +293 -0
  26. package/src/services/media/common.ts +154 -0
  27. package/src/services/media/file.ts +70 -0
  28. package/src/services/media/image.ts +67 -0
  29. package/src/services/media/index.ts +10 -0
  30. package/src/services/media/video.ts +162 -0
  31. package/src/services/media.ts +1134 -0
  32. package/src/services/messaging/index.ts +16 -0
  33. package/src/services/messaging/send.ts +137 -0
  34. package/src/services/messaging.ts +800 -0
  35. package/src/targets.ts +45 -0
  36. package/src/types/index.ts +52 -0
  37. package/src/utils/agent.ts +63 -0
  38. package/src/utils/async.ts +51 -0
  39. package/src/utils/constants.ts +9 -0
  40. package/src/utils/http-client.ts +84 -0
  41. package/src/utils/index.ts +8 -0
  42. package/src/utils/logger.ts +78 -0
  43. package/src/utils/session.ts +118 -0
  44. package/src/utils/token.ts +94 -0
  45. package/src/utils/utils-legacy.ts +506 -0
  46. package/.env.example +0 -11
  47. package/skills/intclaw_matrix/SKILL.md +0 -20
  48. package/src/channel/intclaw_channel.js +0 -155
  49. package/src/index.js +0 -23
package/src/targets.ts ADDED
@@ -0,0 +1,45 @@
1
+ import type { IntclawMessageContext } from "./types/index.ts";
2
+
3
+ function stripProviderPrefix(raw: string): string {
4
+ return raw.replace(/^(intclaw|dd|ding):/i, "").trim();
5
+ }
6
+
7
+ export function normalizeIntclawTarget(raw: string): string | null {
8
+ const trimmed = raw.trim();
9
+ if (!trimmed) {
10
+ return null;
11
+ }
12
+
13
+ const withoutProvider = stripProviderPrefix(trimmed);
14
+ const lowered = withoutProvider.toLowerCase();
15
+ if (lowered.startsWith("user:")) {
16
+ return withoutProvider.slice("user:".length).trim() || null;
17
+ }
18
+ if (lowered.startsWith("group:")) {
19
+ return withoutProvider.slice("group:".length).trim() || null;
20
+ }
21
+
22
+ return withoutProvider;
23
+ }
24
+
25
+ export function formatIntclawTarget(id: string, type?: "user" | "group"): string {
26
+ const trimmed = id.trim();
27
+ if (type === "group") {
28
+ return `group:${trimmed}`;
29
+ }
30
+ if (type === "user") {
31
+ return `user:${trimmed}`;
32
+ }
33
+ return trimmed;
34
+ }
35
+
36
+ export function looksLikeIntclawId(raw: string): boolean {
37
+ const trimmed = stripProviderPrefix(raw.trim());
38
+ if (!trimmed) {
39
+ return false;
40
+ }
41
+ if (/^(user|group):/i.test(trimmed)) {
42
+ return true;
43
+ }
44
+ return true;
45
+ }
@@ -0,0 +1,52 @@
1
+ import type { BaseProbeResult } from "openclaw/plugin-sdk";
2
+ import type {
3
+ IntclawConfigSchema,
4
+ IntclawGroupSchema,
5
+ IntclawAccountConfigSchema,
6
+ z,
7
+ } from "../config/schema.ts";
8
+
9
+ export type IntclawConfig = z.infer<typeof IntclawConfigSchema>;
10
+ export type IntclawGroupConfig = z.infer<typeof IntclawGroupSchema>;
11
+ export type IntclawAccountConfig = z.infer<typeof IntclawAccountConfigSchema>;
12
+
13
+ export type IntclawConnectionMode = "stream";
14
+
15
+ export type IntclawDefaultAccountSelectionSource =
16
+ | "explicit-default"
17
+ | "mapped-default"
18
+ | "fallback";
19
+ export type IntclawAccountSelectionSource = "explicit" | IntclawDefaultAccountSelectionSource;
20
+
21
+ export type ResolvedIntclawAccount = {
22
+ accountId: string;
23
+ selectionSource: IntclawAccountSelectionSource;
24
+ enabled: boolean;
25
+ configured: boolean;
26
+ name?: string;
27
+ clientId?: string;
28
+ clientSecret?: string;
29
+ /** Merged config (top-level defaults + account-specific overrides) */
30
+ config: IntclawConfig;
31
+ };
32
+
33
+ export type IntclawMessageContext = {
34
+ conversationId: string;
35
+ messageId: string;
36
+ senderId: string;
37
+ senderName?: string;
38
+ conversationType: "1" | "2"; // 1=单聊, 2=群聊
39
+ content: string;
40
+ contentType: string;
41
+ groupSubject?: string;
42
+ };
43
+
44
+ export type IntclawSendResult = {
45
+ messageId: string;
46
+ conversationId: string;
47
+ };
48
+
49
+ export type IntclawProbeResult = BaseProbeResult<string> & {
50
+ clientId?: string;
51
+ botName?: string;
52
+ };
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Agent 相关工具函数
3
+ *
4
+ * 提供 Agent 配置解析、工作空间路径解析等功能
5
+ */
6
+ import * as os from "node:os";
7
+ import * as path from "node:path";
8
+ import type { ClawdbotConfig } from "openclaw/plugin-sdk";
9
+
10
+ /**
11
+ * 解析 Agent 工作空间路径
12
+ *
13
+ * 参考 OpenClaw SDK 的 resolveAgentWorkspaceDir 实现逻辑:
14
+ * 1. 优先从 agents.list 中查找用户配置的 workspace
15
+ * 2. 如果没有配置,使用默认路径规则:
16
+ * - 默认 Agent (main): ~/.openclaw/workspace
17
+ * - 其他 Agent: ~/.openclaw/workspace-{agentId}
18
+ *
19
+ * @param cfg - OpenClaw 配置对象
20
+ * @param agentId - Agent ID
21
+ * @returns Agent 工作空间的绝对路径
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * // 用户自定义工作空间
26
+ * const cfg = {
27
+ * agents: {
28
+ * list: [{ id: 'bot1', workspace: '~/my-workspace' }]
29
+ * }
30
+ * };
31
+ * resolveAgentWorkspaceDir(cfg, 'bot1'); // => '/Users/xxx/my-workspace'
32
+ *
33
+ * // 默认 Agent
34
+ * resolveAgentWorkspaceDir(cfg, 'main'); // => '/Users/xxx/.openclaw/workspace'
35
+ *
36
+ * // 其他 Agent
37
+ * resolveAgentWorkspaceDir(cfg, 'bot2'); // => '/Users/xxx/.openclaw/workspace-bot2'
38
+ * ```
39
+ */
40
+ export function resolveAgentWorkspaceDir(
41
+ cfg: ClawdbotConfig,
42
+ agentId: string,
43
+ ): string {
44
+ // 1. 先从 agents.list 中查找配置的 workspace
45
+ const agentConfig = cfg.agents?.list?.find((a: any) => a.id === agentId);
46
+
47
+ if (agentConfig?.workspace) {
48
+ // 用户配置了自定义工作空间路径
49
+ // 支持 ~ 路径展开
50
+ return agentConfig.workspace.startsWith('~')
51
+ ? path.join(os.homedir(), agentConfig.workspace.slice(1))
52
+ : agentConfig.workspace;
53
+ }
54
+
55
+ // 2. 使用默认路径规则
56
+ if (agentId === 'main' || agentId === cfg.defaultAgent) {
57
+ // 默认 Agent 使用 ~/.openclaw/workspace
58
+ return path.join(os.homedir(), '.openclaw', 'workspace');
59
+ }
60
+
61
+ // 其他 Agent 使用 ~/.openclaw/workspace-{agentId}
62
+ return path.join(os.homedir(), '.openclaw', `workspace-${agentId}`);
63
+ }
@@ -0,0 +1,51 @@
1
+ export type RaceResult<T> =
2
+ | { status: "success"; value: T }
3
+ | { status: "timeout" }
4
+ | { status: "aborted" };
5
+
6
+ export async function raceWithTimeoutAndAbort<T>(
7
+ promise: Promise<T>,
8
+ opts: { timeoutMs: number; abortSignal?: AbortSignal },
9
+ ): Promise<RaceResult<T>> {
10
+ const { timeoutMs, abortSignal } = opts;
11
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
12
+ let abortHandler: (() => void) | undefined;
13
+
14
+ const timeoutOutcome = new Promise<{ kind: "timeout" }>((resolve) => {
15
+ timeoutId = setTimeout(() => resolve({ kind: "timeout" }), timeoutMs);
16
+ });
17
+
18
+ const abortOutcome: Promise<{ kind: "aborted" }> | Promise<never> = abortSignal
19
+ ? new Promise<{ kind: "aborted" }>((resolve) => {
20
+ if (abortSignal.aborted) {
21
+ resolve({ kind: "aborted" });
22
+ return;
23
+ }
24
+ abortHandler = () => resolve({ kind: "aborted" });
25
+ abortSignal.addEventListener("abort", abortHandler, { once: true });
26
+ })
27
+ : new Promise<never>(() => {});
28
+
29
+ try {
30
+ const winner = await Promise.race([
31
+ promise.then((value) => ({ kind: "success" as const, value })),
32
+ timeoutOutcome,
33
+ abortOutcome,
34
+ ]);
35
+
36
+ if (winner.kind === "success") {
37
+ return { status: "success", value: winner.value };
38
+ }
39
+ if (winner.kind === "timeout") {
40
+ return { status: "timeout" };
41
+ }
42
+ return { status: "aborted" };
43
+ } finally {
44
+ if (timeoutId) {
45
+ clearTimeout(timeoutId);
46
+ }
47
+ if (abortSignal && abortHandler) {
48
+ abortSignal.removeEventListener("abort", abortHandler);
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 常量定义模块
3
+ */
4
+
5
+ /** 默认账号 ID,用于标记单账号模式(无 accounts 配置)时的内部标识 */
6
+ export const DEFAULT_ACCOUNT_ID = '__default__';
7
+
8
+ /** 新会话触发命令 */
9
+ export const NEW_SESSION_COMMANDS = ['/new', '/reset', '/clear', '新会话', '重新开始', '清空对话'];
@@ -0,0 +1,84 @@
1
+ /**
2
+ * HTTP 客户端配置模块
3
+ *
4
+ * 提供统一的 axios 实例,禁用代理以避免系统 PAC 文件影响
5
+ *
6
+ * 问题背景:
7
+ * - 阿里巴巴内网 PAC 文件会将 *.intclaw.com 路由到内网代理(如 192.168.1.176:443)
8
+ * - 当不在内网环境时,会导致连接超时
9
+ *
10
+ * 解决方案:
11
+ * - 创建专用的 axios 实例,禁用代理
12
+ * - 仅影响IntClaw插件,不影响 OpenClaw Gateway 和其他插件
13
+ *
14
+ * 使用方式:
15
+ * ```typescript
16
+ * import { intclawHttp } from './utils/http-client.ts';
17
+ *
18
+ * const response = await intclawHttp.post('/api/endpoint', data);
19
+ * ```
20
+ */
21
+
22
+ import axios, { type AxiosInstance, type CreateAxiosDefaults } from 'axios';
23
+
24
+ /**
25
+ * 获取代理配置
26
+ *
27
+ * 策略:
28
+ * 1. 如果设置了 INTCLAW_FORCE_PROXY=true,使用环境变量中的代理
29
+ * 2. 否则禁用代理(避免被系统 PAC 影响)
30
+ */
31
+ function getProxyConfig(): CreateAxiosDefaults['proxy'] {
32
+ // 如果强制启用代理
33
+ if (process.env.INTCLAW_FORCE_PROXY === 'true') {
34
+ const proxyUrl =
35
+ process.env.https_proxy ||
36
+ process.env.HTTPS_PROXY ||
37
+ process.env.http_proxy ||
38
+ process.env.HTTP_PROXY;
39
+
40
+ if (proxyUrl) {
41
+ return proxyUrl as any;
42
+ }
43
+ }
44
+
45
+ // 默认禁用代理
46
+ return false;
47
+ }
48
+
49
+ /**
50
+ * IntClaw专用 HTTP 客户端
51
+ *
52
+ * 特性:
53
+ * - 禁用代理(避免 PAC 文件影响)
54
+ * - 30 秒超时
55
+ * - 仅影响IntClaw插件的请求
56
+ */
57
+ export const intclawHttp: AxiosInstance = axios.create({
58
+ proxy: getProxyConfig(),
59
+ timeout: 30000,
60
+ headers: {
61
+ 'Content-Type': 'application/json',
62
+ },
63
+ });
64
+
65
+ /**
66
+ * IntClaw OAPI 专用 HTTP 客户端(用于媒体上传等)
67
+ */
68
+ export const intclawOapiHttp: AxiosInstance = axios.create({
69
+ proxy: getProxyConfig(),
70
+ timeout: 60000, // 媒体上传可能需要更长时间
71
+ headers: {
72
+ 'Content-Type': 'application/json',
73
+ },
74
+ });
75
+
76
+ /**
77
+ * 用于文件上传的 HTTP 客户端(支持 multipart/form-data)
78
+ */
79
+ export const intclawUploadHttp: AxiosInstance = axios.create({
80
+ proxy: getProxyConfig(),
81
+ timeout: 120000, // 文件上传需要更长时间
82
+ maxContentLength: Infinity,
83
+ maxBodyLength: Infinity,
84
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * 工具函数模块统一导出
3
+ */
4
+
5
+ export * from './constants.ts';
6
+ export * from './token.ts';
7
+ export * from './session.ts';
8
+ export * from './logger.ts';
@@ -0,0 +1,78 @@
1
+ /**
2
+ * 日志工具模块
3
+ * 根据 debug 配置控制日志输出
4
+ */
5
+
6
+ /**
7
+ * 创建日志记录器
8
+ * @param debug - 是否启用 debug 模式
9
+ * @param prefix - 日志前缀
10
+ * @returns 日志记录器对象
11
+ */
12
+ export function createLogger(debug: boolean = false, prefix: string = '') {
13
+ const logger = {
14
+ /**
15
+ * 打印 info 级别日志
16
+ * 仅在 debug 模式下输出
17
+ */
18
+ info(...args: any[]) {
19
+ if (debug) {
20
+ if (prefix) {
21
+ console.log(`[${prefix}]`, ...args);
22
+ } else {
23
+ console.log(...args);
24
+ }
25
+ }
26
+ },
27
+
28
+ /**
29
+ * 打印 warn 级别日志
30
+ * 始终输出
31
+ */
32
+ warn(...args: any[]) {
33
+ if (prefix) {
34
+ console.warn(`[${prefix}]`, ...args);
35
+ } else {
36
+ console.warn(...args);
37
+ }
38
+ },
39
+
40
+ /**
41
+ * 打印 error 级别日志
42
+ * 始终输出
43
+ */
44
+ error(...args: any[]) {
45
+ if (prefix) {
46
+ console.error(`[${prefix}]`, ...args);
47
+ } else {
48
+ console.error(...args);
49
+ }
50
+ },
51
+
52
+ /**
53
+ * 打印 debug 级别日志
54
+ * 仅在 debug 模式下输出
55
+ */
56
+ debug(...args: any[]) {
57
+ if (debug) {
58
+ if (prefix) {
59
+ console.log(`[DEBUG][${prefix}]`, ...args);
60
+ } else {
61
+ console.log('[DEBUG]', ...args);
62
+ }
63
+ }
64
+ },
65
+ };
66
+
67
+ return logger;
68
+ }
69
+
70
+ /**
71
+ * 从配置中创建日志记录器
72
+ * @param config - 包含 debug 配置的对象
73
+ * @param prefix - 日志前缀
74
+ * @returns 日志记录器对象
75
+ */
76
+ export function createLoggerFromConfig(config: { debug?: boolean }, prefix: string = '') {
77
+ return createLogger(!!config.debug, prefix);
78
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * 会话管理模块
3
+ * 构建 OpenClaw 标准会话上下文
4
+ */
5
+
6
+ import { NEW_SESSION_COMMANDS } from './constants.ts';
7
+
8
+ /** OpenClaw 标准会话上下文 */
9
+ export interface SessionContext {
10
+ channel: 'intclaw-connector';
11
+ accountId: string;
12
+ chatType: 'direct' | 'group';
13
+ peerId: string;
14
+ conversationId?: string;
15
+ senderName?: string;
16
+ groupSubject?: string;
17
+ }
18
+
19
+ /**
20
+ * 构建 OpenClaw 标准会话上下文
21
+ * 遵循 OpenClaw session.dmScope 机制,让 Gateway 根据配置自动处理会话隔离
22
+ *
23
+ * @param sharedMemoryAcrossConversations - 是否在不同会话间共享记忆(默认 false)
24
+ * - true: 所有会话共享记忆,使用 accountId 作为记忆标识
25
+ * - false: 不同会话独立记忆,使用完整的 sessionContext 作为记忆标识
26
+ */
27
+ export function buildSessionContext(params: {
28
+ accountId: string;
29
+ senderId: string;
30
+ senderName?: string;
31
+ conversationType: string;
32
+ conversationId?: string;
33
+ groupSubject?: string;
34
+ separateSessionByConversation?: boolean;
35
+ groupSessionScope?: 'group' | 'group_sender';
36
+ sharedMemoryAcrossConversations?: boolean;
37
+ }): SessionContext {
38
+ const {
39
+ accountId,
40
+ senderId,
41
+ senderName,
42
+ conversationType,
43
+ conversationId,
44
+ groupSubject,
45
+ separateSessionByConversation,
46
+ groupSessionScope,
47
+ sharedMemoryAcrossConversations,
48
+ } = params;
49
+ const isDirect = conversationType === '1';
50
+
51
+ // sharedMemoryAcrossConversations=true 时,所有会话共享记忆
52
+ // 通过将 peerId 设置为 accountId 来实现跨会话记忆共享
53
+ if (sharedMemoryAcrossConversations === true) {
54
+ return {
55
+ channel: 'intclaw-connector',
56
+ accountId,
57
+ chatType: isDirect ? 'direct' : 'group',
58
+ peerId: accountId, // 使用 accountId 作为 peerId,实现跨会话记忆共享
59
+ conversationId: isDirect ? undefined : conversationId,
60
+ senderName,
61
+ groupSubject: isDirect ? undefined : groupSubject,
62
+ };
63
+ }
64
+
65
+ if (separateSessionByConversation === false) {
66
+ return {
67
+ channel: 'intclaw-connector',
68
+ accountId,
69
+ chatType: isDirect ? 'direct' : 'group',
70
+ peerId: senderId,
71
+ senderName,
72
+ };
73
+ }
74
+
75
+ if (isDirect) {
76
+ return {
77
+ channel: 'intclaw-connector',
78
+ accountId,
79
+ chatType: 'direct',
80
+ peerId: senderId,
81
+ senderName,
82
+ };
83
+ }
84
+
85
+ if (groupSessionScope === 'group_sender') {
86
+ return {
87
+ channel: 'intclaw-connector',
88
+ accountId,
89
+ chatType: 'group',
90
+ peerId: `${conversationId}:${senderId}`,
91
+ conversationId,
92
+ senderName,
93
+ groupSubject,
94
+ };
95
+ }
96
+
97
+ return {
98
+ channel: 'intclaw-connector',
99
+ accountId,
100
+ chatType: 'group',
101
+ peerId: conversationId || senderId,
102
+ conversationId,
103
+ senderName,
104
+ groupSubject,
105
+ };
106
+ }
107
+
108
+ /**
109
+ * 检查消息是否是新会话命令
110
+ */
111
+ export function normalizeSlashCommand(text: string): string {
112
+ const trimmed = text.trim();
113
+ const lower = trimmed.toLowerCase();
114
+ if (NEW_SESSION_COMMANDS.some((cmd) => lower === cmd.toLowerCase())) {
115
+ return '/new';
116
+ }
117
+ return text;
118
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Access Token 管理模块
3
+ * 支持IntClaw API 和 OAPI 的 Token 获取和缓存
4
+ */
5
+
6
+ import type { IntclawConfig } from '../types/index.ts';
7
+ import { intclawHttp, intclawOapiHttp } from './http-client.ts';
8
+ import { INTCLAW_CONFIG } from '../../config.ts';
9
+
10
+ // ============ 常量 ============
11
+
12
+ export const INTCLAW_API = INTCLAW_CONFIG.API_BASE_URL;
13
+ export const INTCLAW_OAPI = INTCLAW_CONFIG.OAPI_BASE_URL;
14
+
15
+ // ============ Access Token 缓存 ============
16
+
17
+ type CachedToken = {
18
+ token: string;
19
+ expiryMs: number;
20
+ };
21
+
22
+ /**
23
+ * 按 clientId 分桶缓存,避免多账号串 token。
24
+ */
25
+ const apiTokenCache = new Map<string, CachedToken>();
26
+ const oapiTokenCache = new Map<string, CachedToken>();
27
+
28
+ function cacheKey(config: IntclawConfig): string {
29
+ const clientId = String((config as any)?.clientId ?? '').trim();
30
+
31
+ // 添加校验
32
+ if (!clientId) {
33
+ throw new Error(
34
+ 'Invalid IntclawConfig: clientId is required for token caching. ' +
35
+ 'Please ensure your configuration includes a valid clientId.'
36
+ );
37
+ }
38
+
39
+ return clientId;
40
+ }
41
+
42
+ /**
43
+ * 获取IntClaw Access Token(新版 API)
44
+ */
45
+ export async function getAccessToken(config: IntclawConfig): Promise<string> {
46
+ const now = Date.now();
47
+ const key = cacheKey(config);
48
+ const cached = apiTokenCache.get(key);
49
+ if (cached && cached.expiryMs > now + 60_000) {
50
+ return cached.token;
51
+ }
52
+
53
+ const response = await intclawHttp.post(`${INTCLAW_API}/v1.0/oauth2/accessToken`, {
54
+ appKey: config.clientId,
55
+ appSecret: config.clientSecret,
56
+ });
57
+
58
+ const token = response.data.accessToken as string;
59
+ const expireInSec = Number(response.data.expireIn ?? 0);
60
+ apiTokenCache.set(key, {
61
+ token,
62
+ expiryMs: now + expireInSec * 1000,
63
+ });
64
+ return token;
65
+ }
66
+
67
+ /**
68
+ * 获取IntClaw OAPI Access Token(旧版 API,用于媒体上传等)
69
+ */
70
+ export async function getOapiAccessToken(config: IntclawConfig): Promise<string | null> {
71
+ try {
72
+ const now = Date.now();
73
+ const key = cacheKey(config);
74
+ const cached = oapiTokenCache.get(key);
75
+ if (cached && cached.expiryMs > now + 60_000) {
76
+ return cached.token;
77
+ }
78
+
79
+ const resp = await intclawOapiHttp.get(`${INTCLAW_OAPI}/gettoken`, {
80
+ params: { appkey: config.clientId, appsecret: config.clientSecret },
81
+ });
82
+
83
+ if (resp.data?.errcode === 0 && resp.data?.access_token) {
84
+ const token = String(resp.data.access_token);
85
+ // IntClaw返回 expires_in(秒),拿不到就按 2 小时兜底
86
+ const expiresInSec = Number(resp.data.expires_in ?? 7200);
87
+ oapiTokenCache.set(key, { token, expiryMs: now + expiresInSec * 1000 });
88
+ return token;
89
+ }
90
+ return null;
91
+ } catch {
92
+ return null;
93
+ }
94
+ }