openclaw-openagent 1.0.1 → 1.0.2

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 (187) hide show
  1. package/dist/index.d.ts +25 -0
  2. package/dist/index.js +105 -0
  3. package/dist/src/app/channel-tools.d.ts +28 -0
  4. package/dist/src/app/channel-tools.js +251 -0
  5. package/dist/src/app/discovery-tools.d.ts +35 -0
  6. package/dist/src/app/discovery-tools.js +243 -0
  7. package/dist/src/app/download-file-tool.d.ts +10 -0
  8. package/dist/src/app/download-file-tool.js +104 -0
  9. package/dist/src/app/hooks.d.ts +14 -0
  10. package/dist/src/app/hooks.js +118 -0
  11. package/dist/src/app/index.d.ts +13 -0
  12. package/dist/src/app/index.js +44 -0
  13. package/dist/src/app/messaging-tools.d.ts +11 -0
  14. package/dist/src/app/messaging-tools.js +79 -0
  15. package/dist/src/app/ops-tools.d.ts +21 -0
  16. package/dist/src/app/ops-tools.js +158 -0
  17. package/dist/src/app/remote-agent-tool.d.ts +27 -0
  18. package/dist/src/app/remote-agent-tool.js +461 -0
  19. package/dist/src/app/types.d.ts +61 -0
  20. package/dist/src/app/types.js +11 -0
  21. package/dist/src/app/upload-file-tool.d.ts +16 -0
  22. package/dist/src/app/upload-file-tool.js +353 -0
  23. package/dist/src/app/verbose-preflight.d.ts +2 -0
  24. package/dist/src/app/verbose-preflight.js +145 -0
  25. package/dist/src/auth/config.d.ts +79 -0
  26. package/dist/src/auth/config.js +133 -0
  27. package/dist/src/auth/credential-manager.d.ts +65 -0
  28. package/dist/src/auth/credential-manager.js +122 -0
  29. package/dist/src/auth/index.d.ts +6 -0
  30. package/dist/src/auth/index.js +6 -0
  31. package/dist/src/auth/verify.d.ts +42 -0
  32. package/dist/src/auth/verify.js +60 -0
  33. package/dist/src/channel.d.ts +269 -0
  34. package/dist/src/channel.js +488 -0
  35. package/dist/src/compat.d.ts +37 -0
  36. package/dist/src/compat.js +70 -0
  37. package/dist/src/config/config-schema.d.ts +56 -0
  38. package/dist/src/config/config-schema.js +34 -0
  39. package/dist/src/messaging/aggregator.d.ts +25 -0
  40. package/dist/src/messaging/aggregator.js +90 -0
  41. package/dist/src/messaging/collector.d.ts +27 -0
  42. package/dist/src/messaging/collector.js +76 -0
  43. package/dist/src/messaging/executor.d.ts +14 -0
  44. package/dist/src/messaging/executor.js +59 -0
  45. package/dist/src/messaging/inbound.d.ts +97 -0
  46. package/dist/src/messaging/inbound.js +63 -0
  47. package/dist/src/messaging/index.d.ts +10 -0
  48. package/dist/src/messaging/index.js +9 -0
  49. package/dist/src/messaging/mention-protocol.d.ts +42 -0
  50. package/dist/src/messaging/mention-protocol.js +74 -0
  51. package/dist/src/messaging/process-c2c-request.d.ts +55 -0
  52. package/dist/src/messaging/process-c2c-request.js +445 -0
  53. package/dist/src/messaging/process-message.d.ts +62 -0
  54. package/dist/src/messaging/process-message.js +282 -0
  55. package/dist/src/messaging/scheduler.d.ts +17 -0
  56. package/dist/src/messaging/scheduler.js +47 -0
  57. package/dist/src/messaging/types.d.ts +34 -0
  58. package/dist/src/messaging/types.js +4 -0
  59. package/dist/src/plugin-ui/assets/openagent-override.js +9267 -0
  60. package/dist/src/plugin-ui/index.d.ts +13 -0
  61. package/dist/src/plugin-ui/index.js +16 -0
  62. package/dist/src/plugin-ui/ui-extension-loader/backup.d.ts +23 -0
  63. package/dist/src/plugin-ui/ui-extension-loader/backup.js +82 -0
  64. package/dist/src/plugin-ui/ui-extension-loader/index.d.ts +28 -0
  65. package/dist/src/plugin-ui/ui-extension-loader/index.js +240 -0
  66. package/dist/src/plugin-ui/ui-extension-loader/locator.d.ts +35 -0
  67. package/dist/src/plugin-ui/ui-extension-loader/locator.js +129 -0
  68. package/dist/src/plugin-ui/ui-extension-loader/manifest.d.ts +26 -0
  69. package/dist/src/plugin-ui/ui-extension-loader/manifest.js +45 -0
  70. package/dist/src/plugin-ui/ui-extension-loader/registry-regex.d.ts +9 -0
  71. package/dist/src/plugin-ui/ui-extension-loader/registry-regex.js +701 -0
  72. package/dist/src/plugin-ui/ui-extension-loader/removed-extensions.d.ts +8 -0
  73. package/dist/src/plugin-ui/ui-extension-loader/removed-extensions.js +58 -0
  74. package/dist/src/plugin-ui/ui-extension-loader/types.d.ts +55 -0
  75. package/dist/src/plugin-ui/ui-extension-loader/types.js +23 -0
  76. package/dist/src/proxy/auth-proxy.d.ts +20 -0
  77. package/dist/src/proxy/auth-proxy.js +337 -0
  78. package/dist/src/runtime/account.d.ts +156 -0
  79. package/dist/src/runtime/account.js +491 -0
  80. package/dist/src/runtime/index.d.ts +5 -0
  81. package/dist/src/runtime/index.js +6 -0
  82. package/dist/src/runtime/plugin-runtime.d.ts +40 -0
  83. package/dist/src/runtime/plugin-runtime.js +72 -0
  84. package/dist/src/runtime/registry.d.ts +41 -0
  85. package/dist/src/runtime/registry.js +60 -0
  86. package/dist/src/sdk/CLASS_MAP.md +143 -0
  87. package/dist/src/sdk/index.d.ts +126 -0
  88. package/dist/src/sdk/index.js +23990 -0
  89. package/dist/src/sdk/modules/cloud-search-module.js +1117 -0
  90. package/dist/src/sdk/modules/follow-module.js +1069 -0
  91. package/dist/src/sdk/modules/group-module.js +7397 -0
  92. package/dist/src/sdk/modules/relationship-module.js +2269 -0
  93. package/dist/src/sdk/modules/signaling-module.js +1468 -0
  94. package/dist/src/sdk/modules/tim-upload-plugin.js +730 -0
  95. package/dist/src/sdk/node-env/http-request.js +90 -0
  96. package/dist/src/sdk/node-env/index.js +57 -0
  97. package/dist/src/sdk/node-env/storage.js +114 -0
  98. package/dist/src/sdk/package.json +10 -0
  99. package/dist/src/sdk/sdk/CLASS_MAP.md +143 -0
  100. package/dist/src/sdk/sdk/index.d.ts +126 -0
  101. package/dist/src/sdk/sdk/index.js +23990 -0
  102. package/dist/src/sdk/sdk/modules/cloud-search-module.js +1117 -0
  103. package/dist/src/sdk/sdk/modules/follow-module.js +1069 -0
  104. package/dist/src/sdk/sdk/modules/group-module.js +7397 -0
  105. package/dist/src/sdk/sdk/modules/relationship-module.js +2269 -0
  106. package/dist/src/sdk/sdk/modules/signaling-module.js +1468 -0
  107. package/dist/src/sdk/sdk/modules/tim-upload-plugin.js +730 -0
  108. package/dist/src/sdk/sdk/node-env/http-request.js +90 -0
  109. package/dist/src/sdk/sdk/node-env/index.js +57 -0
  110. package/dist/src/sdk/sdk/node-env/storage.js +114 -0
  111. package/dist/src/sdk/sdk/package.json +10 -0
  112. package/dist/src/sdk/sdk/tsconfig.json +16 -0
  113. package/dist/src/sdk/tsconfig.json +16 -0
  114. package/dist/src/state/pending-invocation-store.d.ts +26 -0
  115. package/dist/src/state/pending-invocation-store.js +39 -0
  116. package/dist/src/state/store.d.ts +165 -0
  117. package/dist/src/state/store.js +535 -0
  118. package/dist/src/tim/c2c.d.ts +83 -0
  119. package/dist/src/tim/c2c.js +318 -0
  120. package/dist/src/tim/channels.d.ts +95 -0
  121. package/dist/src/tim/channels.js +279 -0
  122. package/dist/src/tim/client.d.ts +54 -0
  123. package/dist/src/tim/client.js +268 -0
  124. package/dist/src/tim/index.d.ts +6 -0
  125. package/dist/src/tim/index.js +6 -0
  126. package/dist/src/tim/messages.d.ts +50 -0
  127. package/dist/src/tim/messages.js +104 -0
  128. package/dist/src/tim/sdk-logger-init.d.ts +13 -0
  129. package/dist/src/tim/sdk-logger-init.js +46 -0
  130. package/dist/src/tools.d.ts +9 -0
  131. package/dist/src/tools.js +8 -0
  132. package/dist/src/transport/factory.d.ts +63 -0
  133. package/dist/src/transport/factory.js +54 -0
  134. package/dist/src/transport/oasn/index.d.ts +12 -0
  135. package/dist/src/transport/oasn/index.js +9 -0
  136. package/dist/src/transport/oasn/oasn-agent-card.d.ts +38 -0
  137. package/dist/src/transport/oasn/oasn-agent-card.js +102 -0
  138. package/dist/src/transport/oasn/oasn-discovery.d.ts +33 -0
  139. package/dist/src/transport/oasn/oasn-discovery.js +97 -0
  140. package/dist/src/transport/oasn/oasn-files.d.ts +64 -0
  141. package/dist/src/transport/oasn/oasn-files.js +174 -0
  142. package/dist/src/transport/oasn/oasn-http.d.ts +98 -0
  143. package/dist/src/transport/oasn/oasn-http.js +362 -0
  144. package/dist/src/transport/oasn/oasn-invocation.d.ts +154 -0
  145. package/dist/src/transport/oasn/oasn-invocation.js +432 -0
  146. package/dist/src/transport/oasn/oasn-normalize.d.ts +6 -0
  147. package/dist/src/transport/oasn/oasn-normalize.js +112 -0
  148. package/dist/src/transport/oasn/oasn-register.d.ts +19 -0
  149. package/dist/src/transport/oasn/oasn-register.js +24 -0
  150. package/dist/src/transport/oasn/oasn-transport.d.ts +114 -0
  151. package/dist/src/transport/oasn/oasn-transport.js +230 -0
  152. package/dist/src/transport/oasn/oasn-types.d.ts +331 -0
  153. package/dist/src/transport/oasn/oasn-types.js +44 -0
  154. package/dist/src/transport/tim/index.d.ts +7 -0
  155. package/dist/src/transport/tim/index.js +6 -0
  156. package/dist/src/transport/tim/tim-transport.d.ts +122 -0
  157. package/dist/src/transport/tim/tim-transport.js +402 -0
  158. package/dist/src/transport/types.d.ts +450 -0
  159. package/dist/src/transport/types.js +38 -0
  160. package/dist/src/util/http.d.ts +21 -0
  161. package/dist/src/util/http.js +93 -0
  162. package/dist/src/util/logger.d.ts +20 -0
  163. package/dist/src/util/logger.js +100 -0
  164. package/dist/src/util/url-resolver.d.ts +7 -0
  165. package/dist/src/util/url-resolver.js +20 -0
  166. package/index.ts +11 -0
  167. package/openclaw.plugin.json +9 -0
  168. package/package.json +7 -4
  169. package/src/app/download-file-tool.ts +133 -0
  170. package/src/app/hooks.ts +89 -5
  171. package/src/app/index.ts +6 -0
  172. package/src/app/remote-agent-tool.ts +46 -0
  173. package/src/app/types.ts +1 -0
  174. package/src/app/upload-file-tool.ts +411 -0
  175. package/src/plugin-ui/assets/openagent-override.js +1 -63
  176. package/src/plugin-ui/modules/agent-book/panel/agent-book.js +0 -61
  177. package/src/plugin-ui/modules/agent-book/panel/agent-data.js +0 -1
  178. package/src/plugin-ui/ui-extension-loader/registry-regex.ts +30 -13
  179. package/src/runtime/account.ts +0 -23
  180. package/src/transport/oasn/oasn-files.ts +5 -5
  181. package/src/transport/oasn/oasn-invocation.ts +44 -2
  182. package/src/transport/oasn/oasn-normalize.ts +0 -26
  183. package/src/transport/oasn/oasn-register.ts +8 -81
  184. package/src/transport/oasn/oasn-transport.ts +9 -32
  185. package/src/transport/oasn/oasn-types.ts +53 -6
  186. package/src/transport/types.ts +33 -36
  187. package/src/util/url-resolver.ts +17 -0
@@ -0,0 +1,318 @@
1
+ /**
2
+ * TIM C2C — Agent-to-Agent direct messaging
3
+ *
4
+ * Provides C2C (peer-to-peer) communication for remote agent orchestration.
5
+ * Used by openagent_call_remote_agent tool (T2) to send requests and await replies.
6
+ *
7
+ * Architecture:
8
+ * - sendC2CCustomMessage: sends a TIM CustomMessage via CONV_C2C
9
+ * - waitForC2CReply: registers a pending request, resolves when reply arrives
10
+ * - initC2CHandler: listens for client 'c2c_message' events, dispatches to pending requests
11
+ *
12
+ * @see docs/products/orchestrator/specs/T1-c2c-infra.md
13
+ * @see docs/products/orchestrator/openagent-teams-architecture.md §3.3.3
14
+ */
15
+ import { logger } from '../util/logger.js';
16
+ import { registry } from '../runtime/registry.js';
17
+ /**
18
+ * All pending requests waiting for C2C replies.
19
+ * Key: requestId (UUID), Value: callbacks + timeout handle.
20
+ *
21
+ * Memory safety: every entry is guaranteed to be deleted via one of three paths:
22
+ * 1. Normal completion (task_result received) → clearTimeout + delete + resolve
23
+ * 2. Timeout (timeoutMs expired) → delete + reject
24
+ * 3. Abort (signal.abort) → clearTimeout + delete + reject
25
+ */
26
+ const pendingRequests = new Map();
27
+ const pendingFiles = new Map();
28
+ const fileWaiters = new Map();
29
+ /** Max time to wait for late-arriving TIMFileElem messages (10 min). */
30
+ const FILE_WAIT_TIMEOUT_MS = 10 * 60_000;
31
+ // ── sendC2CCustomMessage ──
32
+ /**
33
+ * Send a C2C custom message to a remote agent.
34
+ *
35
+ * Uses TIM SDK createCustomMessage + CONV_C2C.
36
+ * Protocol payload is JSON-serialized into the custom message `data` field.
37
+ *
38
+ * Timeout pattern matches messages.ts sendMessage (10s SDK-call guard).
39
+ * @see src/tim/messages.ts L61-87
40
+ *
41
+ * @param userId - Target agent's TIM userId
42
+ * @param payload - Structured message payload (request/progress/result)
43
+ */
44
+ export async function sendC2CCustomMessage(userId, payload) {
45
+ const rt = registry.getDefault();
46
+ if (!rt || !rt.isRunning || !rt.client) {
47
+ throw new Error('OpenAgent not connected');
48
+ }
49
+ const chat = rt.client._chat;
50
+ const types = rt.client._types;
51
+ if (!chat || !rt.client.isReady) {
52
+ throw new Error('TIM SDK not initialized');
53
+ }
54
+ const message = chat.createCustomMessage({
55
+ to: userId,
56
+ conversationType: types.CONV_C2C,
57
+ payload: {
58
+ data: JSON.stringify(payload),
59
+ description: payload.type,
60
+ extension: '',
61
+ },
62
+ });
63
+ logger.info(`[tim/c2c] Sending C2C: to=${userId} type=${payload.type}`);
64
+ // 10s SDK-call guard — same pattern as messages.ts L64-69
65
+ // If SDK hangs (dead WebSocket), this prevents the Tool from blocking forever.
66
+ let timeoutHandle;
67
+ const timeoutPromise = new Promise((_, reject) => {
68
+ timeoutHandle = setTimeout(() => reject(new Error(`sendC2CCustomMessage timeout (10s) to=${userId}`)), 10_000);
69
+ });
70
+ try {
71
+ await Promise.race([chat.sendMessage(message), timeoutPromise]);
72
+ logger.info(`[tim/c2c] C2C sent OK: to=${userId} type=${payload.type}`);
73
+ }
74
+ catch (err) {
75
+ const code = err?.code;
76
+ logger.error(`[tim/c2c] C2C send failed: code=${code} msg=${err.message} to=${userId}`);
77
+ throw err;
78
+ }
79
+ finally {
80
+ clearTimeout(timeoutHandle);
81
+ }
82
+ }
83
+ // ── waitForC2CReply ──
84
+ /**
85
+ * Wait for a C2C reply matching a specific requestId.
86
+ *
87
+ * Registers a pending entry in the registry. When c2cMessageHandler receives
88
+ * a matching message, it dispatches to the entry's callbacks:
89
+ * - task_progress → onProgress(content)
90
+ * - task_result → resolve({ status, content })
91
+ *
92
+ * @param requestId - UUID to match replies against
93
+ * @param options - Timeout, abort signal, and progress callback
94
+ */
95
+ export function waitForC2CReply(requestId, options) {
96
+ return new Promise((resolve, reject) => {
97
+ // Timeout: auto-reject + cleanup after timeoutMs
98
+ const timeoutHandle = setTimeout(() => {
99
+ pendingRequests.delete(requestId);
100
+ reject(new Error(`C2C reply timeout (${options.timeoutMs}ms) requestId=${requestId}`));
101
+ }, options.timeoutMs);
102
+ // Abort signal: reject + cleanup on user cancel
103
+ if (options.signal) {
104
+ options.signal.addEventListener('abort', () => {
105
+ clearTimeout(timeoutHandle);
106
+ pendingRequests.delete(requestId);
107
+ reject(new Error(`C2C reply aborted requestId=${requestId}`));
108
+ }, { once: true });
109
+ }
110
+ // Register pending entry
111
+ pendingRequests.set(requestId, {
112
+ onProgress: options.onProgress,
113
+ resolve,
114
+ reject,
115
+ timeoutHandle,
116
+ });
117
+ logger.info(`[tim/c2c] Registered pending request: requestId=${requestId} timeout=${options.timeoutMs}ms`);
118
+ });
119
+ }
120
+ // ── C2C Message Handler ──
121
+ /**
122
+ * Parse a raw C2C message payload into a OpenAgent protocol message.
123
+ * Returns null if the message is not a OpenAgent protocol message.
124
+ */
125
+ function parseC2CPayload(raw) {
126
+ // TIM CustomMessage payload: { data: string, description: string, extension: string }
127
+ const p = raw.payload;
128
+ if (!p?.data)
129
+ return null;
130
+ try {
131
+ const parsed = JSON.parse(p.data);
132
+ // Validate: must be a OpenAgent protocol message with request_id
133
+ if (typeof parsed.type !== 'string' || !parsed.type.startsWith('openagent_task_')) {
134
+ return null;
135
+ }
136
+ if (typeof parsed.request_id !== 'string') {
137
+ logger.warn(`[tim/c2c] C2C message missing request_id: type=${parsed.type}`);
138
+ return null;
139
+ }
140
+ return parsed;
141
+ }
142
+ catch {
143
+ // Not a JSON payload or parse error → not a OpenAgent message
144
+ return null;
145
+ }
146
+ }
147
+ /**
148
+ * Initialize the C2C message handler.
149
+ *
150
+ * Listens for 'c2c_message' events from TIMClient and dispatches to:
151
+ * - pendingRequests registry (for progress/result → T2's waitForC2CReply)
152
+ * - 'c2c_request' event (for incoming task requests → T3's handler)
153
+ *
154
+ * Must be called after TIMClient.connect() succeeds.
155
+ *
156
+ * @param client - Connected TIMClient instance
157
+ */
158
+ export function initC2CHandler(client) {
159
+ client.on('c2c_message', async (raw) => {
160
+ try {
161
+ // T4: Check for TIMFileElem before parsing as CustomMessage
162
+ // MSG_FILE messages have a different payload structure (fileUrl, fileName, fileSize)
163
+ // and won't pass parseC2CPayload. We accumulate them for later retrieval.
164
+ if (raw.type === 'TIMFileElem') {
165
+ handleC2CFileMessage(raw);
166
+ return;
167
+ }
168
+ const payload = parseC2CPayload(raw);
169
+ if (!payload)
170
+ return; // Not a OpenAgent protocol message
171
+ const requestId = payload.request_id;
172
+ switch (payload.type) {
173
+ case 'openagent_task_progress': {
174
+ const pending = pendingRequests.get(requestId);
175
+ if (!pending) {
176
+ logger.debug(`[tim/c2c] No pending request for progress requestId=${requestId}`);
177
+ return;
178
+ }
179
+ logger.debug(`[tim/c2c] Progress for requestId=${requestId}: ${payload.content.slice(0, 80)}`);
180
+ pending.onProgress?.(payload.content);
181
+ break;
182
+ }
183
+ case 'openagent_task_result': {
184
+ const pending = pendingRequests.get(requestId);
185
+ if (!pending) {
186
+ logger.debug(`[tim/c2c] No pending request for result requestId=${requestId}`);
187
+ return;
188
+ }
189
+ logger.info(`[tim/c2c] Result for requestId=${requestId}: status=${payload.status}`);
190
+ clearTimeout(pending.timeoutHandle);
191
+ pendingRequests.delete(requestId);
192
+ // T4: Collect pending files from this sender.
193
+ // Files may arrive before or after task_result (race condition).
194
+ // If files are already here → drain immediately.
195
+ // If not → register a fileWaiter and wait up to FILE_WAIT_TIMEOUT_MS.
196
+ const fileCount = payload.file_count ?? 0;
197
+ let mediaUrls;
198
+ if (fileCount > 0) {
199
+ const senderFiles = pendingFiles.get(raw.from) ?? [];
200
+ logger.info(`[tim/c2c] Result has file_count=${fileCount}, pending files from sender: ${senderFiles.length}`);
201
+ let filesToDrain;
202
+ if (senderFiles.length >= fileCount) {
203
+ // Files already arrived → drain immediately
204
+ filesToDrain = senderFiles;
205
+ pendingFiles.delete(raw.from);
206
+ }
207
+ else {
208
+ // Files haven't arrived yet → wait for them
209
+ logger.info(`[tim/c2c] Waiting for ${fileCount} file(s) from ${raw.from} (have ${senderFiles.length})...`);
210
+ filesToDrain = await new Promise((resolveFiles) => {
211
+ const timeoutHandle = setTimeout(() => {
212
+ fileWaiters.delete(raw.from);
213
+ const partial = pendingFiles.get(raw.from) ?? [];
214
+ pendingFiles.delete(raw.from);
215
+ logger.warn(`[tim/c2c] File wait timeout (${FILE_WAIT_TIMEOUT_MS}ms) for ${raw.from}, got ${partial.length}/${fileCount} file(s)`);
216
+ resolveFiles(partial);
217
+ }, FILE_WAIT_TIMEOUT_MS);
218
+ fileWaiters.set(raw.from, {
219
+ expectedCount: fileCount,
220
+ onFilesReady: (files) => {
221
+ clearTimeout(timeoutHandle);
222
+ fileWaiters.delete(raw.from);
223
+ resolveFiles(files);
224
+ },
225
+ timeoutHandle,
226
+ });
227
+ });
228
+ }
229
+ if (filesToDrain.length > 0) {
230
+ mediaUrls = [];
231
+ for (const fileInfo of filesToDrain) {
232
+ try {
233
+ logger.info(`[tim/c2c] Downloading file: ${fileInfo.fileName} (${fileInfo.fileSize} bytes)`);
234
+ const resp = await fetch(fileInfo.fileUrl);
235
+ if (!resp.ok) {
236
+ logger.error(`[tim/c2c] File download failed: HTTP ${resp.status} for ${fileInfo.fileName}`);
237
+ continue;
238
+ }
239
+ const buffer = Buffer.from(await resp.arrayBuffer());
240
+ // Save to temp dir with original filename preserved
241
+ // Use /tmp/openclaw/ prefix — framework's default localRoots whitelist.
242
+ const fs = await import('node:fs');
243
+ const nodePath = await import('node:path');
244
+ const tmpDir = nodePath.join('/tmp/openclaw', 'openagent-media');
245
+ fs.mkdirSync(tmpDir, { recursive: true });
246
+ const tmpPath = nodePath.join(tmpDir, `${Date.now()}-${fileInfo.fileName}`);
247
+ fs.writeFileSync(tmpPath, buffer);
248
+ mediaUrls.push(tmpPath);
249
+ logger.info(`[tim/c2c] File saved: ${fileInfo.fileName} → ${tmpPath} (${buffer.byteLength} bytes)`);
250
+ }
251
+ catch (err) {
252
+ logger.error(`[tim/c2c] File download/save error: ${err.message} for ${fileInfo.fileName}`);
253
+ }
254
+ }
255
+ if (mediaUrls.length === 0)
256
+ mediaUrls = undefined;
257
+ }
258
+ }
259
+ pending.resolve({ status: payload.status, content: payload.content, mediaUrls });
260
+ break;
261
+ }
262
+ case 'openagent_task_request': {
263
+ // Incoming task request from another agent → forward to T3 handler
264
+ // T1 does not process requests, only replies (progress + result)
265
+ logger.info(`[tim/c2c] Incoming task request from=${raw.from} requestId=${requestId}`);
266
+ client.emit('c2c_request', {
267
+ from: raw.from,
268
+ payload,
269
+ time: raw.time,
270
+ });
271
+ break;
272
+ }
273
+ default:
274
+ logger.debug(`[tim/c2c] Unknown C2C payload type: ${payload.type}`);
275
+ }
276
+ }
277
+ catch (err) {
278
+ logger.error(`[tim/c2c] Error handling C2C message: ${err.message}`);
279
+ }
280
+ });
281
+ logger.info('[tim/c2c] C2C message handler initialized');
282
+ }
283
+ /**
284
+ * Handle a raw C2C message that might be a TIMFileElem.
285
+ * Called from initC2CHandler for non-CustomMessage types.
286
+ * Accumulates file metadata in pendingFiles for later retrieval.
287
+ *
288
+ * @see docs/audit/023-tim-upload-plugin-v1.md §4.4
289
+ */
290
+ function handleC2CFileMessage(raw) {
291
+ // TIMFileElem payload structure (from TIM SDK):
292
+ // { fileUrl: string, fileName: string, fileSize: number, uuid: string }
293
+ const payload = raw.payload;
294
+ if (!payload?.fileUrl || !payload?.fileName) {
295
+ logger.debug(`[tim/c2c] MSG_FILE from=${raw.from} but missing fileUrl/fileName, skip`);
296
+ return;
297
+ }
298
+ const fileInfo = {
299
+ fileUrl: payload.fileUrl,
300
+ fileName: payload.fileName,
301
+ fileSize: payload.fileSize ?? 0,
302
+ };
303
+ if (!pendingFiles.has(raw.from)) {
304
+ pendingFiles.set(raw.from, []);
305
+ }
306
+ pendingFiles.get(raw.from).push(fileInfo);
307
+ const totalFiles = pendingFiles.get(raw.from).length;
308
+ logger.info(`[tim/c2c] Pending file from=${raw.from}: ${fileInfo.fileName} (${fileInfo.fileSize} bytes) total=${totalFiles}`);
309
+ // Check if a fileWaiter is waiting for files from this sender.
310
+ // This handles the race condition where task_result arrived before the file.
311
+ const waiter = fileWaiters.get(raw.from);
312
+ if (waiter && totalFiles >= waiter.expectedCount) {
313
+ logger.info(`[tim/c2c] File waiter satisfied for ${raw.from}: ${totalFiles}/${waiter.expectedCount} file(s)`);
314
+ const files = pendingFiles.get(raw.from) ?? [];
315
+ pendingFiles.delete(raw.from);
316
+ waiter.onFilesReady(files);
317
+ }
318
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * TIM Channels — Channel management operations via SDK
3
+ *
4
+ * All operations use the TIM SDK (not REST API).
5
+ * listChannels uses the auth service endpoint.
6
+ */
7
+ import type { TIMClient } from './client.js';
8
+ export interface ChannelInfo {
9
+ id: string;
10
+ name: string;
11
+ desc: string;
12
+ memberCount?: number;
13
+ }
14
+ export interface MemberInfo {
15
+ id: string;
16
+ nick: string;
17
+ role: string;
18
+ avatar?: string;
19
+ }
20
+ /**
21
+ * List all available channels from the auth service.
22
+ */
23
+ export declare function listChannels(client: TIMClient): Promise<ChannelInfo[]>;
24
+ /**
25
+ * Search channels by keyword (client-side filter of listChannels).
26
+ */
27
+ export declare function searchChannels(client: TIMClient, keyword: string): Promise<ChannelInfo[]>;
28
+ /**
29
+ * Join a group channel.
30
+ *
31
+ * Per TIM Web SDK docs, joinGroup may:
32
+ * - Resolve with status JOIN_STATUS_SUCCESS / ALREADY_IN_GROUP / WAIT_APPROVAL
33
+ * - Reject with error code 10013 ("already group member")
34
+ *
35
+ * We handle both paths and always refresh the SDK conversation cache
36
+ * afterwards to prevent sendMessage error 2801.
37
+ *
38
+ * @see https://cloud.tencent.com/document/product/269/75395
39
+ */
40
+ export declare function joinChannel(client: TIMClient, channelId: string): Promise<{
41
+ success: boolean;
42
+ message: string;
43
+ }>;
44
+ /**
45
+ * Leave a group channel.
46
+ */
47
+ export declare function leaveChannel(client: TIMClient, channelId: string): Promise<{
48
+ success: boolean;
49
+ message: string;
50
+ }>;
51
+ /**
52
+ * Get members of a group channel.
53
+ */
54
+ export declare function getMembers(client: TIMClient, channelId: string): Promise<MemberInfo[]>;
55
+ /**
56
+ * Create a new Community group channel.
57
+ *
58
+ * Uses GRP_COMMUNITY (社群) — supports large-scale membership, topics,
59
+ * and free join/leave. Requires Tencent IM flagship plan with Community
60
+ * feature enabled in console.
61
+ *
62
+ * groupID is always auto-generated by the SDK with @TGS#_ prefix.
63
+ * Community groups reject custom groupIDs that don't start with @TGS#_
64
+ * (SDK error ILLEGAL_GRP_ID / 2602).
65
+ *
66
+ * @see https://cloud.tencent.com/document/product/269/75394
67
+ * @see https://cloud.tencent.com/document/product/269/1502 (群组类型)
68
+ */
69
+ export declare function createChannel(client: TIMClient, name: string, description?: string, skill?: string): Promise<{
70
+ success: boolean;
71
+ channelId: string;
72
+ }>;
73
+ /**
74
+ * Get channel skill (stored in group notification field).
75
+ */
76
+ export declare function getChannelSkill(client: TIMClient, channelId: string): Promise<string | null>;
77
+ /**
78
+ * Set channel skill (stored in group notification field).
79
+ */
80
+ export declare function setChannelSkill(client: TIMClient, channelId: string, skill: string): Promise<{
81
+ success: boolean;
82
+ }>;
83
+ /**
84
+ * Get channel display name from group profile.
85
+ */
86
+ export declare function getGroupName(client: TIMClient, channelId: string): Promise<string | null>;
87
+ /**
88
+ * Read a single group-level custom field (AppDefinedData).
89
+ *
90
+ * Uses getGroupProfile with groupCustomFieldFilter to fetch only the
91
+ * requested field. Returns null if field not set or fetch fails.
92
+ *
93
+ * @param fieldKey - The custom field key (e.g. 'gm_req_mention')
94
+ */
95
+ export declare function getGroupCustomField(client: TIMClient, channelId: string, fieldKey: string): Promise<string | null>;
@@ -0,0 +1,279 @@
1
+ /**
2
+ * TIM Channels — Channel management operations via SDK
3
+ *
4
+ * All operations use the TIM SDK (not REST API).
5
+ * listChannels uses the auth service endpoint.
6
+ */
7
+ import { logger } from '../util/logger.js';
8
+ import { httpGet } from '../util/http.js';
9
+ const AUTH_BASE = 'https://auth.ai-talk.live';
10
+ // ── Functions ──
11
+ /**
12
+ * List all available channels from the auth service.
13
+ */
14
+ export async function listChannels(client) {
15
+ logger.info('[tim/channels] listChannels: fetching from auth service');
16
+ try {
17
+ const headers = {};
18
+ if (client.userSig) {
19
+ headers['Authorization'] = `Bearer ${client.userSig}`;
20
+ }
21
+ const data = await httpGet(`${AUTH_BASE}/api/channels`, { headers });
22
+ const rawList = Array.isArray(data) ? data : (data.data
23
+ || data.channels
24
+ || []);
25
+ logger.info(`[tim/channels] listChannels: found ${rawList.length} channels`);
26
+ return rawList.map((item) => {
27
+ const ch = item;
28
+ return {
29
+ id: (ch['channel_id'] || ch['id'] || ch['channelId'] || ch['GroupId'] || ''),
30
+ name: (ch['name'] || ch['Name'] || ''),
31
+ desc: (ch['description'] || ch['desc'] || ch['Introduction'] || ''),
32
+ memberCount: (ch['member_count'] || ch['memberCount'] || 0),
33
+ };
34
+ });
35
+ }
36
+ catch (err) {
37
+ logger.error(`[tim/channels] listChannels error: ${err.message}`);
38
+ throw err;
39
+ }
40
+ }
41
+ /**
42
+ * Search channels by keyword (client-side filter of listChannels).
43
+ */
44
+ export async function searchChannels(client, keyword) {
45
+ logger.info(`[tim/channels] searchChannels: keyword="${keyword}"`);
46
+ const all = await listChannels(client);
47
+ const lower = keyword.toLowerCase();
48
+ return all.filter(ch => ch.name.toLowerCase().includes(lower) ||
49
+ ch.desc.toLowerCase().includes(lower) ||
50
+ ch.id.toLowerCase().includes(lower));
51
+ }
52
+ /**
53
+ * TIM SDK error code for "already a group member".
54
+ * @see https://cloud.tencent.com/document/product/269/75394
55
+ */
56
+ const ERR_ALREADY_GROUP_MEMBER = 10013;
57
+ /**
58
+ * Join a group channel.
59
+ *
60
+ * Per TIM Web SDK docs, joinGroup may:
61
+ * - Resolve with status JOIN_STATUS_SUCCESS / ALREADY_IN_GROUP / WAIT_APPROVAL
62
+ * - Reject with error code 10013 ("already group member")
63
+ *
64
+ * We handle both paths and always refresh the SDK conversation cache
65
+ * afterwards to prevent sendMessage error 2801.
66
+ *
67
+ * @see https://cloud.tencent.com/document/product/269/75395
68
+ */
69
+ export async function joinChannel(client, channelId) {
70
+ const chat = client._chat;
71
+ if (!chat || !client.isReady)
72
+ throw new Error('Not connected');
73
+ logger.info(`[tim/channels] joinChannel: ${channelId}`);
74
+ try {
75
+ await chat.joinGroup({ groupID: channelId });
76
+ logger.info(`[tim/channels] joinChannel: joined ${channelId}`);
77
+ }
78
+ catch (err) {
79
+ const code = err?.code;
80
+ if (code === ERR_ALREADY_GROUP_MEMBER) {
81
+ logger.info(`[tim/channels] joinChannel: already a member of ${channelId} (code=${code})`);
82
+ }
83
+ else {
84
+ logger.error(`[tim/channels] joinChannel failed: code=${code} msg=${err.message}`);
85
+ throw err;
86
+ }
87
+ }
88
+ // Force SDK to cache group conversation profile.
89
+ // Required because: (1) ALREADY_IN_GROUP does not refresh cache,
90
+ // and (2) even new joins may not populate the conversation list.
91
+ // Without this, sendMessage fails with error 2801 (group profile cache miss).
92
+ try {
93
+ await chat.getConversationProfile(`GROUP${channelId}`);
94
+ logger.info(`[tim/channels] joinChannel: conversation profile cached for ${channelId}`);
95
+ }
96
+ catch (cacheErr) {
97
+ const cacheCode = cacheErr?.code;
98
+ logger.warn(`[tim/channels] joinChannel: failed to cache conversation profile for ${channelId} (code=${cacheCode})`);
99
+ }
100
+ return { success: true, message: `Joined ${channelId}` };
101
+ }
102
+ /**
103
+ * Leave a group channel.
104
+ */
105
+ export async function leaveChannel(client, channelId) {
106
+ const chat = client._chat;
107
+ if (!chat || !client.isReady)
108
+ throw new Error('Not connected');
109
+ logger.info(`[tim/channels] leaveChannel: ${channelId}`);
110
+ try {
111
+ await chat.quitGroup(channelId);
112
+ return { success: true, message: `Left ${channelId}` };
113
+ }
114
+ catch (err) {
115
+ const code = err?.code;
116
+ logger.error(`[tim/channels] leaveChannel failed: code=${code} msg=${err.message} ch=${channelId}`);
117
+ throw err;
118
+ }
119
+ }
120
+ /**
121
+ * Get members of a group channel.
122
+ */
123
+ export async function getMembers(client, channelId) {
124
+ const chat = client._chat;
125
+ if (!chat || !client.isReady)
126
+ throw new Error('Not connected');
127
+ logger.info(`[tim/channels] getMembers: ${channelId}`);
128
+ try {
129
+ const res = await chat.getGroupMemberList({
130
+ groupID: channelId,
131
+ count: 100,
132
+ offset: 0,
133
+ });
134
+ const members = (res.data?.memberList || []).map(m => ({
135
+ id: m.userID,
136
+ nick: m.nick || m.userID,
137
+ role: m.role || 'Member',
138
+ avatar: m.avatar || '',
139
+ }));
140
+ logger.info(`[tim/channels] getMembers: ${channelId} found ${members.length} members`);
141
+ return members;
142
+ }
143
+ catch (err) {
144
+ const code = err?.code;
145
+ logger.error(`[tim/channels] getMembers failed: code=${code} msg=${err.message} ch=${channelId}`);
146
+ throw err;
147
+ }
148
+ }
149
+ /**
150
+ * Create a new Community group channel.
151
+ *
152
+ * Uses GRP_COMMUNITY (社群) — supports large-scale membership, topics,
153
+ * and free join/leave. Requires Tencent IM flagship plan with Community
154
+ * feature enabled in console.
155
+ *
156
+ * groupID is always auto-generated by the SDK with @TGS#_ prefix.
157
+ * Community groups reject custom groupIDs that don't start with @TGS#_
158
+ * (SDK error ILLEGAL_GRP_ID / 2602).
159
+ *
160
+ * @see https://cloud.tencent.com/document/product/269/75394
161
+ * @see https://cloud.tencent.com/document/product/269/1502 (群组类型)
162
+ */
163
+ export async function createChannel(client, name, description = '', skill = '') {
164
+ const chat = client._chat;
165
+ if (!chat || !client.isReady)
166
+ throw new Error('Not connected');
167
+ logger.info(`[tim/channels] createChannel: type=Community name="${name}"`);
168
+ try {
169
+ // Community groups require groupID to start with @TGS#_ or be empty.
170
+ // SDK auto-generates @TGS#_ prefixed ID when groupID is omitted.
171
+ const result = await chat.createGroup({
172
+ type: client._types.GRP_COMMUNITY,
173
+ name,
174
+ // groupID intentionally omitted — SDK auto-generates @TGS#_ prefixed ID
175
+ introduction: description,
176
+ notification: skill,
177
+ isSupportTopic: false,
178
+ });
179
+ const actualId = result?.data?.group?.groupID;
180
+ if (!actualId)
181
+ throw new Error('SDK returned no groupID');
182
+ logger.info(`[tim/channels] Channel created: ${actualId} (type=Community)`);
183
+ return { success: true, channelId: actualId };
184
+ }
185
+ catch (err) {
186
+ const code = err?.code;
187
+ const msg = err?.message || String(err);
188
+ logger.error(`[tim/channels] createChannel failed: code=${code} msg=${msg}`);
189
+ throw err;
190
+ }
191
+ }
192
+ /**
193
+ * Get channel skill (stored in group notification field).
194
+ */
195
+ export async function getChannelSkill(client, channelId) {
196
+ const chat = client._chat;
197
+ if (!chat || !client.isReady)
198
+ return null;
199
+ try {
200
+ const res = await chat.getGroupProfile({
201
+ groupID: channelId,
202
+ groupCustomFieldFilter: [],
203
+ });
204
+ return res.data?.group?.notification || null;
205
+ }
206
+ catch (err) {
207
+ const code = err?.code;
208
+ logger.warn(`[tim/channels] getChannelSkill failed: code=${code} msg=${err.message} ch=${channelId}`);
209
+ return null;
210
+ }
211
+ }
212
+ /**
213
+ * Set channel skill (stored in group notification field).
214
+ */
215
+ export async function setChannelSkill(client, channelId, skill) {
216
+ const chat = client._chat;
217
+ if (!chat || !client.isReady)
218
+ throw new Error('Not connected');
219
+ logger.info(`[tim/channels] setChannelSkill: ${channelId}`);
220
+ try {
221
+ await chat.updateGroupProfile({
222
+ groupID: channelId,
223
+ notification: skill,
224
+ });
225
+ return { success: true };
226
+ }
227
+ catch (err) {
228
+ const code = err?.code;
229
+ logger.error(`[tim/channels] setChannelSkill failed: code=${code} msg=${err.message} ch=${channelId}`);
230
+ throw err;
231
+ }
232
+ }
233
+ /**
234
+ * Get channel display name from group profile.
235
+ */
236
+ export async function getGroupName(client, channelId) {
237
+ const chat = client._chat;
238
+ if (!chat || !client.isReady)
239
+ return null;
240
+ try {
241
+ const res = await chat.getGroupProfile({
242
+ groupID: channelId,
243
+ groupCustomFieldFilter: [],
244
+ });
245
+ return res.data?.group?.name || null;
246
+ }
247
+ catch (err) {
248
+ const code = err?.code;
249
+ logger.warn(`[tim/channels] getGroupName failed: code=${code} msg=${err.message} ch=${channelId}`);
250
+ return null;
251
+ }
252
+ }
253
+ /**
254
+ * Read a single group-level custom field (AppDefinedData).
255
+ *
256
+ * Uses getGroupProfile with groupCustomFieldFilter to fetch only the
257
+ * requested field. Returns null if field not set or fetch fails.
258
+ *
259
+ * @param fieldKey - The custom field key (e.g. 'gm_req_mention')
260
+ */
261
+ export async function getGroupCustomField(client, channelId, fieldKey) {
262
+ const chat = client._chat;
263
+ if (!chat || !client.isReady)
264
+ return null;
265
+ try {
266
+ const res = await chat.getGroupProfile({
267
+ groupID: channelId,
268
+ groupCustomFieldFilter: [fieldKey],
269
+ });
270
+ const fields = res.data?.group?.groupCustomField ?? [];
271
+ const target = fields.find((f) => f.key === fieldKey);
272
+ return target?.value ?? null;
273
+ }
274
+ catch (err) {
275
+ const code = err?.code;
276
+ logger.warn(`[tim/channels] getGroupCustomField(${fieldKey}) failed: code=${code} ch=${channelId}`);
277
+ return null;
278
+ }
279
+ }