@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.
- package/LICENSE +1 -1
- package/README.en.md +424 -0
- package/README.md +365 -164
- package/index.ts +28 -0
- package/openclaw.plugin.json +12 -41
- package/package.json +69 -40
- package/src/channel.ts +557 -0
- package/src/config/accounts.ts +230 -0
- package/src/config/schema.ts +144 -0
- package/src/core/connection.ts +733 -0
- package/src/core/message-handler.ts +1268 -0
- package/src/core/provider.ts +106 -0
- package/src/core/state.ts +54 -0
- package/src/directory.ts +95 -0
- package/src/gateway-methods.ts +237 -0
- package/src/onboarding.ts +387 -0
- package/src/policy.ts +19 -0
- package/src/probe.ts +213 -0
- package/src/reply-dispatcher.ts +674 -0
- package/src/runtime.ts +7 -0
- package/src/sdk/helpers.ts +317 -0
- package/src/sdk/types.ts +515 -0
- package/src/secret-input.ts +19 -0
- package/src/services/media/audio.ts +54 -0
- package/src/services/media/chunk-upload.ts +293 -0
- package/src/services/media/common.ts +154 -0
- package/src/services/media/file.ts +70 -0
- package/src/services/media/image.ts +67 -0
- package/src/services/media/index.ts +10 -0
- package/src/services/media/video.ts +162 -0
- package/src/services/media.ts +1134 -0
- package/src/services/messaging/index.ts +16 -0
- package/src/services/messaging/send.ts +137 -0
- package/src/services/messaging.ts +800 -0
- package/src/targets.ts +45 -0
- package/src/types/index.ts +52 -0
- package/src/utils/agent.ts +63 -0
- package/src/utils/async.ts +51 -0
- package/src/utils/constants.ts +9 -0
- package/src/utils/http-client.ts +84 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/logger.ts +78 -0
- package/src/utils/session.ts +118 -0
- package/src/utils/token.ts +94 -0
- package/src/utils/utils-legacy.ts +506 -0
- package/.env.example +0 -11
- package/skills/intclaw_matrix/SKILL.md +0 -20
- package/src/channel/intclaw_channel.js +0 -155
- 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,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,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
|
+
}
|