@core-workspace/infoflow-openclaw-plugin 2026.3.9 → 2026.3.27-beta.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/CHANGELOG.md +91 -0
- package/CLAUDE.md +135 -0
- package/COLLABORATION_REPORT.md +209 -0
- package/PROJECT_GUIDE.md +355 -0
- package/README.md +158 -66
- package/docs/dev-guide.md +63 -50
- package/docs/qa-feature-list.md +452 -0
- package/docs/webhook-guide.md +178 -0
- package/index.ts +28 -2
- package/openclaw.plugin.json +131 -21
- package/package.json +16 -3
- package/scripts/deploy.sh +66 -7
- package/scripts/postinstall.cjs +80 -0
- package/skills/infoflow-dev/SKILL.md +2 -2
- package/skills/infoflow-dev/references/api.md +1 -1
- package/src/adapter/inbound/webhook-parser.ts +27 -5
- package/src/adapter/inbound/ws-receiver.ts +304 -43
- package/src/adapter/outbound/markdown-local-images.ts +80 -0
- package/src/adapter/outbound/reply-dispatcher.ts +146 -65
- package/src/adapter/outbound/target-resolver.ts +4 -3
- package/src/channel/accounts.ts +97 -22
- package/src/channel/channel.ts +456 -12
- package/src/channel/media.ts +20 -6
- package/src/channel/monitor.ts +8 -3
- package/src/channel/outbound.ts +358 -21
- package/src/channel/streaming.ts +740 -0
- package/src/commands/changelog.ts +80 -0
- package/src/commands/doctor.ts +545 -0
- package/src/commands/logs.ts +449 -0
- package/src/commands/version.ts +20 -0
- package/src/compat/openclaw-sdk.ts +218 -0
- package/src/handler/message-handler.ts +673 -166
- package/src/logging.ts +1 -1
- package/src/runtime.ts +1 -1
- package/src/security/dm-policy.ts +1 -4
- package/src/security/group-policy.ts +174 -51
- package/src/tools/actions/index.ts +15 -13
- package/src/tools/cron/relay.ts +1154 -0
- package/src/tools/hooks/index.ts +13 -1
- package/src/tools/index.ts +714 -32
- package/src/types.ts +144 -25
- package/src/utils/audio/g722/dct_tables.ts +381 -0
- package/src/utils/audio/g722/decoder.ts +919 -0
- package/src/utils/audio/g722/defs.ts +105 -0
- package/src/utils/audio/g722/hd-parser.ts +247 -0
- package/src/utils/audio/g722/huff_tables.ts +240 -0
- package/src/utils/audio/g722/index.ts +78 -0
- package/src/utils/audio/g722/output_decoded.pcm +0 -0
- package/src/utils/audio/g722/output_decoded.wav +0 -0
- package/src/utils/audio/g722/tables.ts +173 -0
- package/src/utils/audio/g722/test_api.ts +31 -0
- package/src/utils/audio/g722/test_voice.hd +0 -0
- package/src/utils/bos/im-bos-client.ts +219 -0
- package/src/utils/group-agent-cache.ts +142 -0
- package/src/utils/token-adapter.ts +120 -51
|
@@ -1,35 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Each appKey gets its own adapter instance (multi-account isolation).
|
|
2
|
+
* Token management: fetches and caches Infoflow app_access_token.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* - Plugin needs raw response text for large integer precision (>2^53)
|
|
8
|
-
* - Plugin needs manual JSON construction for group recall
|
|
4
|
+
* Replaces SDK TokenManager with a self-contained implementation for
|
|
5
|
+
* full visibility into token requests and easier debugging.
|
|
9
6
|
*
|
|
10
|
-
*
|
|
7
|
+
* API: POST {apiHost}/auth/app_access_token
|
|
8
|
+
* Body: { app_key, app_secret: md5(appSecret).toLowerCase() }
|
|
9
|
+
* Response: { code: "ok", data: { app_access_token, expire } }
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - Per-appKey instance cache (multi-account isolation)
|
|
13
|
+
* - In-memory token cache with 5-minute early expiry buffer
|
|
14
|
+
* - Concurrent request deduplication (Promise lock)
|
|
15
|
+
* - Full logging at every step
|
|
11
16
|
*/
|
|
12
17
|
|
|
13
|
-
import {
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
import { ensureHttps } from "../channel/outbound.js";
|
|
20
|
+
import { getInfoflowSendLog } from "../logging.js";
|
|
14
21
|
|
|
15
|
-
|
|
16
|
-
*
|
|
17
|
-
|
|
18
|
-
* (Inlined from send.ts to avoid circular dependency)
|
|
19
|
-
*/
|
|
20
|
-
function ensureHttps(apiHost: string): string {
|
|
21
|
-
if (apiHost.startsWith("http://")) {
|
|
22
|
-
const url = new URL(apiHost);
|
|
23
|
-
const isLocal = url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
24
|
-
if (!isLocal) {
|
|
25
|
-
return apiHost.replace(/^http:/, "https:");
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return apiHost;
|
|
29
|
-
}
|
|
22
|
+
const TOKEN_PATH = "/api/v1/auth/app_access_token";
|
|
23
|
+
const EXPIRE_BUFFER_MS = 5 * 60 * 1000; // refresh 5 minutes before expiry
|
|
24
|
+
const FETCH_TIMEOUT_MS = 15_000;
|
|
30
25
|
|
|
31
26
|
// Module-level instance cache (keyed by appKey for multi-account isolation)
|
|
32
|
-
const adapterCache = new Map<string,
|
|
27
|
+
const adapterCache = new Map<string, InfoflowTokenManager>();
|
|
33
28
|
|
|
34
29
|
export type SDKAdapterParams = {
|
|
35
30
|
apiHost: string;
|
|
@@ -38,49 +33,123 @@ export type SDKAdapterParams = {
|
|
|
38
33
|
};
|
|
39
34
|
|
|
40
35
|
/**
|
|
41
|
-
* Gets or creates
|
|
42
|
-
* Adapter instances are cached per appKey to reuse SDK TokenManager's
|
|
43
|
-
* internal token cache and Promise lock.
|
|
36
|
+
* Gets or creates a token manager for the given appKey.
|
|
44
37
|
*/
|
|
45
|
-
export function getOrCreateAdapter(params: SDKAdapterParams):
|
|
38
|
+
export function getOrCreateAdapter(params: SDKAdapterParams): InfoflowTokenManager {
|
|
46
39
|
const existing = adapterCache.get(params.appKey);
|
|
47
40
|
if (existing) {
|
|
48
41
|
return existing;
|
|
49
42
|
}
|
|
50
|
-
|
|
51
|
-
const adapter = new InfoflowSDKAdapter(params);
|
|
43
|
+
const adapter = new InfoflowTokenManager(params);
|
|
52
44
|
adapterCache.set(params.appKey, adapter);
|
|
53
45
|
return adapter;
|
|
54
46
|
}
|
|
55
47
|
|
|
48
|
+
type TokenCache = {
|
|
49
|
+
token: string;
|
|
50
|
+
expireAt: number; // ms timestamp
|
|
51
|
+
};
|
|
52
|
+
|
|
56
53
|
/**
|
|
57
|
-
*
|
|
54
|
+
* Self-contained token manager for a single appKey.
|
|
58
55
|
*/
|
|
59
|
-
export class
|
|
60
|
-
private
|
|
56
|
+
export class InfoflowTokenManager {
|
|
57
|
+
private readonly apiHost: string;
|
|
58
|
+
private readonly appKey: string;
|
|
59
|
+
private readonly appSecret: string;
|
|
60
|
+
private cache: TokenCache | null = null;
|
|
61
|
+
private refreshPromise: Promise<string> | null = null;
|
|
61
62
|
|
|
62
63
|
constructor(params: SDKAdapterParams) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const configManager = new ConfigManager({
|
|
70
|
-
appId: params.appKey,
|
|
71
|
-
appSecret: params.appSecret,
|
|
72
|
-
apiDomain: domain,
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
this.tokenManager = new TokenManager(configManager);
|
|
64
|
+
this.apiHost = ensureHttps(params.apiHost);
|
|
65
|
+
this.appKey = params.appKey;
|
|
66
|
+
this.appSecret = params.appSecret;
|
|
67
|
+
getInfoflowSendLog().info(
|
|
68
|
+
`[token] init: appKey=${params.appKey.slice(0, 4)}***, apiHost=${this.apiHost}`,
|
|
69
|
+
);
|
|
76
70
|
}
|
|
77
71
|
|
|
78
|
-
/**
|
|
79
|
-
* Gets an access token via SDK TokenManager.
|
|
80
|
-
* Handles caching, MD5 signing, concurrency safety, and early refresh internally.
|
|
81
|
-
*/
|
|
82
72
|
async getToken(): Promise<string> {
|
|
83
|
-
|
|
73
|
+
const log = getInfoflowSendLog();
|
|
74
|
+
|
|
75
|
+
// Deduplicate concurrent requests
|
|
76
|
+
if (this.refreshPromise) {
|
|
77
|
+
log.info(`[token] waiting for in-flight refresh: appKey=${this.appKey.slice(0, 4)}***`);
|
|
78
|
+
return this.refreshPromise;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Return cached token if still valid
|
|
82
|
+
if (this.cache && Date.now() < this.cache.expireAt - EXPIRE_BUFFER_MS) {
|
|
83
|
+
log.info(
|
|
84
|
+
`[token] cache hit: appKey=${this.appKey.slice(0, 4)}***, expiresIn=${Math.round((this.cache.expireAt - Date.now()) / 1000)}s`,
|
|
85
|
+
);
|
|
86
|
+
return this.cache.token;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
log.info(`[token] cache miss, fetching new token: appKey=${this.appKey.slice(0, 4)}***`);
|
|
90
|
+
this.refreshPromise = this.fetchToken();
|
|
91
|
+
try {
|
|
92
|
+
const token = await this.refreshPromise;
|
|
93
|
+
return token;
|
|
94
|
+
} finally {
|
|
95
|
+
this.refreshPromise = null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private async fetchToken(): Promise<string> {
|
|
100
|
+
const log = getInfoflowSendLog();
|
|
101
|
+
const url = `${this.apiHost}${TOKEN_PATH}`;
|
|
102
|
+
const signedSecret = createHash("md5").update(this.appSecret).digest("hex").toLowerCase();
|
|
103
|
+
|
|
104
|
+
log.info(`[token] POST ${url}, app_key=${this.appKey.slice(0, 4)}***`);
|
|
105
|
+
|
|
106
|
+
const controller = new AbortController();
|
|
107
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
108
|
+
|
|
109
|
+
let responseText = "";
|
|
110
|
+
try {
|
|
111
|
+
const res = await fetch(url, {
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: { "Content-Type": "application/json" },
|
|
114
|
+
body: JSON.stringify({ app_key: this.appKey, app_secret: signedSecret }),
|
|
115
|
+
signal: controller.signal,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
responseText = await res.text();
|
|
119
|
+
log.info(`[token] response: status=${res.status}, body=${responseText.slice(0, 200)}`);
|
|
120
|
+
|
|
121
|
+
const data = JSON.parse(responseText) as Record<string, unknown>;
|
|
122
|
+
const inner = data.data as Record<string, unknown> | undefined;
|
|
123
|
+
|
|
124
|
+
if (data.code !== "ok" || !inner?.app_access_token) {
|
|
125
|
+
const errMsg = String(data.message ?? data.errmsg ?? `code=${data.code ?? "unknown"}`);
|
|
126
|
+
log.error(`[token] fetch failed: appKey=${this.appKey.slice(0, 4)}***, error=${errMsg}`);
|
|
127
|
+
throw new Error(`Failed to get token: ${errMsg}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const token = String(inner.app_access_token);
|
|
131
|
+
const expireSeconds = typeof inner.expire === "number" ? inner.expire : 7200;
|
|
132
|
+
this.cache = { token, expireAt: Date.now() + expireSeconds * 1000 };
|
|
133
|
+
|
|
134
|
+
log.info(
|
|
135
|
+
`[token] fetch ok: appKey=${this.appKey.slice(0, 4)}***, tokenLen=${token.length}, expireIn=${expireSeconds}s`,
|
|
136
|
+
);
|
|
137
|
+
return token;
|
|
138
|
+
} catch (err) {
|
|
139
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
140
|
+
if (msg.includes("abort") || msg.includes("signal")) {
|
|
141
|
+
log.error(
|
|
142
|
+
`[token] fetch timeout after ${FETCH_TIMEOUT_MS}ms: appKey=${this.appKey.slice(0, 4)}***`,
|
|
143
|
+
);
|
|
144
|
+
throw new Error(`Token fetch timed out after ${FETCH_TIMEOUT_MS}ms`);
|
|
145
|
+
}
|
|
146
|
+
log.error(
|
|
147
|
+
`[token] fetch exception: appKey=${this.appKey.slice(0, 4)}***, error=${msg}, responseText=${responseText.slice(0, 200)}`,
|
|
148
|
+
);
|
|
149
|
+
throw err;
|
|
150
|
+
} finally {
|
|
151
|
+
clearTimeout(timer);
|
|
152
|
+
}
|
|
84
153
|
}
|
|
85
154
|
}
|
|
86
155
|
|