@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/CLAUDE.md +135 -0
  3. package/COLLABORATION_REPORT.md +209 -0
  4. package/PROJECT_GUIDE.md +355 -0
  5. package/README.md +158 -66
  6. package/docs/dev-guide.md +63 -50
  7. package/docs/qa-feature-list.md +452 -0
  8. package/docs/webhook-guide.md +178 -0
  9. package/index.ts +28 -2
  10. package/openclaw.plugin.json +131 -21
  11. package/package.json +16 -3
  12. package/scripts/deploy.sh +66 -7
  13. package/scripts/postinstall.cjs +80 -0
  14. package/skills/infoflow-dev/SKILL.md +2 -2
  15. package/skills/infoflow-dev/references/api.md +1 -1
  16. package/src/adapter/inbound/webhook-parser.ts +27 -5
  17. package/src/adapter/inbound/ws-receiver.ts +304 -43
  18. package/src/adapter/outbound/markdown-local-images.ts +80 -0
  19. package/src/adapter/outbound/reply-dispatcher.ts +146 -65
  20. package/src/adapter/outbound/target-resolver.ts +4 -3
  21. package/src/channel/accounts.ts +97 -22
  22. package/src/channel/channel.ts +456 -12
  23. package/src/channel/media.ts +20 -6
  24. package/src/channel/monitor.ts +8 -3
  25. package/src/channel/outbound.ts +358 -21
  26. package/src/channel/streaming.ts +740 -0
  27. package/src/commands/changelog.ts +80 -0
  28. package/src/commands/doctor.ts +545 -0
  29. package/src/commands/logs.ts +449 -0
  30. package/src/commands/version.ts +20 -0
  31. package/src/compat/openclaw-sdk.ts +218 -0
  32. package/src/handler/message-handler.ts +673 -166
  33. package/src/logging.ts +1 -1
  34. package/src/runtime.ts +1 -1
  35. package/src/security/dm-policy.ts +1 -4
  36. package/src/security/group-policy.ts +174 -51
  37. package/src/tools/actions/index.ts +15 -13
  38. package/src/tools/cron/relay.ts +1154 -0
  39. package/src/tools/hooks/index.ts +13 -1
  40. package/src/tools/index.ts +714 -32
  41. package/src/types.ts +144 -25
  42. package/src/utils/audio/g722/dct_tables.ts +381 -0
  43. package/src/utils/audio/g722/decoder.ts +919 -0
  44. package/src/utils/audio/g722/defs.ts +105 -0
  45. package/src/utils/audio/g722/hd-parser.ts +247 -0
  46. package/src/utils/audio/g722/huff_tables.ts +240 -0
  47. package/src/utils/audio/g722/index.ts +78 -0
  48. package/src/utils/audio/g722/output_decoded.pcm +0 -0
  49. package/src/utils/audio/g722/output_decoded.wav +0 -0
  50. package/src/utils/audio/g722/tables.ts +173 -0
  51. package/src/utils/audio/g722/test_api.ts +31 -0
  52. package/src/utils/audio/g722/test_voice.hd +0 -0
  53. package/src/utils/bos/im-bos-client.ts +219 -0
  54. package/src/utils/group-agent-cache.ts +142 -0
  55. package/src/utils/token-adapter.ts +120 -51
@@ -1,35 +1,30 @@
1
1
  /**
2
- * SDK Adapter Layer: wraps SDK TokenManager for token management.
3
- * Each appKey gets its own adapter instance (multi-account isolation).
2
+ * Token management: fetches and caches Infoflow app_access_token.
4
3
  *
5
- * Design decision: Only use SDK TokenManager, NOT HTTPClient, because:
6
- * - SDK HTTPClient uses http:// but plugin requires https://
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
- * HTTP requests continue using fetch in send.ts.
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 { ConfigManager, TokenManager } from "@core-workspace/infoflow-sdk-nodejs";
18
+ import { createHash } from "node:crypto";
19
+ import { ensureHttps } from "../channel/outbound.js";
20
+ import { getInfoflowSendLog } from "../logging.js";
14
21
 
15
- /**
16
- * Ensures apiHost uses HTTPS for security (secrets in transit).
17
- * Allows HTTP only for localhost/127.0.0.1 (local development).
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, InfoflowSDKAdapter>();
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 an SDK adapter for the given appKey.
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): InfoflowSDKAdapter {
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
- * SDK adapter wrapping TokenManager for a single appKey.
54
+ * Self-contained token manager for a single appKey.
58
55
  */
59
- export class InfoflowSDKAdapter {
60
- private tokenManager: TokenManager;
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
- // SDK ConfigManager expects pure domain (e.g. "apiin.im.baidu.com"),
64
- // but plugin's apiHost is a full URL (e.g. "https://apiin.im.baidu.com").
65
- // Extract the host portion.
66
- const apiUrl = ensureHttps(params.apiHost);
67
- const domain = new URL(apiUrl).host;
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
- return this.tokenManager.getAccessToken();
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