opc-agent 1.3.2 → 2.0.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 (226) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +20 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +14 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +13 -0
  4. package/.github/workflows/ci.yml +24 -0
  5. package/CHANGELOG.md +48 -63
  6. package/CONTRIBUTING.md +21 -60
  7. package/README.md +284 -348
  8. package/README.zh-CN.md +415 -415
  9. package/dist/channels/slack.js +93 -10
  10. package/dist/channels/telegram.d.ts +30 -9
  11. package/dist/channels/telegram.js +125 -33
  12. package/dist/channels/web.d.ts +10 -0
  13. package/dist/channels/web.js +33 -2
  14. package/dist/cli.js +667 -65
  15. package/dist/core/agent.d.ts +23 -0
  16. package/dist/core/agent.js +120 -3
  17. package/dist/core/runtime.d.ts +5 -0
  18. package/dist/core/runtime.js +71 -0
  19. package/dist/core/scheduler.d.ts +52 -0
  20. package/dist/core/scheduler.js +168 -0
  21. package/dist/core/subagent.d.ts +28 -0
  22. package/dist/core/subagent.js +65 -0
  23. package/dist/daemon.d.ts +3 -0
  24. package/dist/daemon.js +134 -0
  25. package/dist/deploy/hermes.js +22 -22
  26. package/dist/deploy/openclaw.js +31 -40
  27. package/dist/index.d.ts +10 -10
  28. package/dist/index.js +22 -15
  29. package/dist/providers/index.d.ts +6 -2
  30. package/dist/providers/index.js +22 -9
  31. package/dist/schema/oad.d.ts +180 -6
  32. package/dist/schema/oad.js +12 -1
  33. package/dist/skills/auto-learn.d.ts +28 -0
  34. package/dist/skills/auto-learn.js +257 -0
  35. package/dist/templates/code-reviewer.d.ts +0 -8
  36. package/dist/templates/code-reviewer.js +5 -9
  37. package/dist/templates/customer-service.d.ts +0 -8
  38. package/dist/templates/customer-service.js +2 -6
  39. package/dist/templates/data-analyst.d.ts +0 -8
  40. package/dist/templates/data-analyst.js +5 -9
  41. package/dist/templates/knowledge-base.d.ts +0 -8
  42. package/dist/templates/knowledge-base.js +2 -6
  43. package/dist/templates/sales-assistant.d.ts +0 -8
  44. package/dist/templates/sales-assistant.js +4 -8
  45. package/dist/templates/teacher.d.ts +0 -8
  46. package/dist/templates/teacher.js +6 -10
  47. package/dist/tools/builtin/datetime.d.ts +3 -0
  48. package/dist/tools/builtin/datetime.js +44 -0
  49. package/dist/tools/builtin/file.d.ts +3 -0
  50. package/dist/tools/builtin/file.js +151 -0
  51. package/dist/tools/builtin/index.d.ts +15 -0
  52. package/dist/tools/builtin/index.js +30 -0
  53. package/dist/tools/builtin/shell.d.ts +3 -0
  54. package/dist/tools/builtin/shell.js +43 -0
  55. package/dist/tools/builtin/web.d.ts +3 -0
  56. package/dist/tools/builtin/web.js +37 -0
  57. package/dist/tools/mcp-client.d.ts +24 -0
  58. package/dist/tools/mcp-client.js +119 -0
  59. package/dist/traces/index.d.ts +49 -0
  60. package/dist/traces/index.js +102 -0
  61. package/docs/.vitepress/config.ts +103 -103
  62. package/docs/api/cli.md +48 -48
  63. package/docs/api/oad-schema.md +64 -64
  64. package/docs/api/sdk.md +80 -80
  65. package/docs/guide/concepts.md +51 -51
  66. package/docs/guide/configuration.md +79 -79
  67. package/docs/guide/deployment.md +42 -42
  68. package/docs/guide/getting-started.md +44 -44
  69. package/docs/guide/templates.md +28 -28
  70. package/docs/guide/testing.md +84 -84
  71. package/docs/index.md +27 -27
  72. package/docs/zh/api/cli.md +54 -54
  73. package/docs/zh/api/oad-schema.md +87 -87
  74. package/docs/zh/api/sdk.md +102 -102
  75. package/docs/zh/guide/concepts.md +104 -104
  76. package/docs/zh/guide/configuration.md +135 -135
  77. package/docs/zh/guide/deployment.md +81 -81
  78. package/docs/zh/guide/getting-started.md +82 -82
  79. package/docs/zh/guide/templates.md +84 -84
  80. package/docs/zh/guide/testing.md +88 -88
  81. package/docs/zh/index.md +27 -27
  82. package/examples/README.md +22 -0
  83. package/examples/basic-agent.ts +90 -0
  84. package/examples/brain-integration.ts +71 -0
  85. package/examples/customer-service-demo/README.md +90 -90
  86. package/examples/customer-service-demo/oad.yaml +107 -107
  87. package/examples/multi-channel.ts +74 -0
  88. package/package.json +1 -1
  89. package/src/analytics/index.ts +66 -66
  90. package/src/channels/discord.ts +192 -192
  91. package/src/channels/email.ts +177 -177
  92. package/src/channels/feishu.ts +236 -236
  93. package/src/channels/index.ts +15 -15
  94. package/src/channels/slack.ts +217 -160
  95. package/src/channels/telegram.ts +155 -33
  96. package/src/channels/voice.ts +106 -106
  97. package/src/channels/web.ts +38 -2
  98. package/src/channels/webhook.ts +199 -199
  99. package/src/channels/websocket.ts +87 -87
  100. package/src/channels/wechat.ts +149 -149
  101. package/src/cli.ts +697 -63
  102. package/src/core/a2a.ts +143 -143
  103. package/src/core/agent.ts +146 -3
  104. package/src/core/analytics-engine.ts +186 -186
  105. package/src/core/auth.ts +57 -57
  106. package/src/core/cache.ts +141 -141
  107. package/src/core/compose.ts +77 -77
  108. package/src/core/config.ts +14 -14
  109. package/src/core/errors.ts +148 -148
  110. package/src/core/hitl.ts +138 -138
  111. package/src/core/logger.ts +57 -57
  112. package/src/core/orchestrator.ts +215 -215
  113. package/src/core/performance.ts +187 -187
  114. package/src/core/rate-limiter.ts +128 -128
  115. package/src/core/room.ts +109 -109
  116. package/src/core/runtime.ts +230 -152
  117. package/src/core/sandbox.ts +101 -101
  118. package/src/core/scheduler.ts +187 -0
  119. package/src/core/security.ts +171 -171
  120. package/src/core/subagent.ts +98 -0
  121. package/src/core/types.ts +68 -68
  122. package/src/core/versioning.ts +106 -106
  123. package/src/core/watch.ts +178 -178
  124. package/src/core/workflow.ts +235 -235
  125. package/src/daemon.ts +96 -0
  126. package/src/deploy/hermes.ts +156 -156
  127. package/src/deploy/openclaw.ts +190 -200
  128. package/src/i18n/index.ts +216 -216
  129. package/src/index.ts +14 -10
  130. package/src/memory/deepbrain.ts +108 -108
  131. package/src/memory/index.ts +34 -34
  132. package/src/plugins/index.ts +208 -208
  133. package/src/providers/index.ts +354 -331
  134. package/src/schema/oad.ts +14 -2
  135. package/src/skills/auto-learn.ts +262 -0
  136. package/src/skills/base.ts +16 -16
  137. package/src/skills/document.ts +100 -100
  138. package/src/skills/http.ts +35 -35
  139. package/src/skills/index.ts +27 -27
  140. package/src/skills/scheduler.ts +80 -80
  141. package/src/skills/webhook-trigger.ts +59 -59
  142. package/src/templates/code-reviewer.ts +30 -34
  143. package/src/templates/customer-service.ts +76 -80
  144. package/src/templates/data-analyst.ts +66 -70
  145. package/src/templates/executive-assistant.ts +71 -71
  146. package/src/templates/financial-advisor.ts +60 -60
  147. package/src/templates/knowledge-base.ts +27 -31
  148. package/src/templates/legal-assistant.ts +71 -71
  149. package/src/templates/sales-assistant.ts +75 -79
  150. package/src/templates/teacher.ts +75 -79
  151. package/src/testing/index.ts +181 -181
  152. package/src/tools/builtin/datetime.ts +41 -0
  153. package/src/tools/builtin/file.ts +107 -0
  154. package/src/tools/builtin/index.ts +28 -0
  155. package/src/tools/builtin/shell.ts +43 -0
  156. package/src/tools/builtin/web.ts +35 -0
  157. package/src/tools/calculator.ts +73 -73
  158. package/src/tools/datetime.ts +149 -149
  159. package/src/tools/json-transform.ts +187 -187
  160. package/src/tools/mcp-client.ts +131 -0
  161. package/src/tools/mcp.ts +76 -76
  162. package/src/tools/text-analysis.ts +116 -116
  163. package/src/traces/index.ts +132 -0
  164. package/templates/Dockerfile +15 -15
  165. package/templates/code-reviewer/README.md +27 -27
  166. package/templates/code-reviewer/oad.yaml +41 -41
  167. package/templates/customer-service/README.md +22 -22
  168. package/templates/customer-service/oad.yaml +36 -36
  169. package/templates/docker-compose.yml +21 -21
  170. package/templates/ecommerce-assistant/README.md +45 -45
  171. package/templates/ecommerce-assistant/oad.yaml +47 -47
  172. package/templates/knowledge-base/README.md +28 -28
  173. package/templates/knowledge-base/oad.yaml +38 -38
  174. package/templates/sales-assistant/README.md +26 -26
  175. package/templates/sales-assistant/oad.yaml +43 -43
  176. package/templates/tech-support/README.md +43 -43
  177. package/templates/tech-support/oad.yaml +45 -45
  178. package/test-agent/Dockerfile +9 -0
  179. package/test-agent/README.md +50 -0
  180. package/test-agent/agent.yaml +23 -0
  181. package/test-agent/docker-compose.yml +11 -0
  182. package/test-agent/oad.yaml +31 -0
  183. package/test-agent/package-lock.json +1492 -0
  184. package/test-agent/package.json +18 -0
  185. package/test-agent/src/index.ts +24 -0
  186. package/test-agent/src/skills/echo.ts +15 -0
  187. package/test-agent/tsconfig.json +25 -0
  188. package/tests/a2a.test.ts +66 -66
  189. package/tests/agent.test.ts +72 -72
  190. package/tests/analytics.test.ts +50 -50
  191. package/tests/auto-learn.test.ts +105 -0
  192. package/tests/builtin-tools.test.ts +83 -0
  193. package/tests/channel.test.ts +39 -39
  194. package/tests/cli.test.ts +46 -0
  195. package/tests/e2e.test.ts +134 -134
  196. package/tests/errors.test.ts +83 -83
  197. package/tests/hitl.test.ts +71 -71
  198. package/tests/i18n.test.ts +41 -41
  199. package/tests/mcp.test.ts +54 -54
  200. package/tests/oad.test.ts +68 -68
  201. package/tests/performance.test.ts +115 -115
  202. package/tests/plugin.test.ts +74 -74
  203. package/tests/room.test.ts +106 -106
  204. package/tests/runtime.test.ts +42 -42
  205. package/tests/sandbox.test.ts +46 -46
  206. package/tests/security.test.ts +60 -60
  207. package/tests/subagent.test.ts +130 -0
  208. package/tests/telegram-discord.test.ts +60 -0
  209. package/tests/templates.test.ts +77 -77
  210. package/tests/v070.test.ts +76 -76
  211. package/tests/versioning.test.ts +75 -75
  212. package/tests/voice.test.ts +61 -61
  213. package/tests/webhook.test.ts +29 -29
  214. package/tests/workflow.test.ts +143 -143
  215. package/tsconfig.json +19 -19
  216. package/vitest.config.ts +9 -9
  217. package/dist/core/dashboard.d.ts +0 -35
  218. package/dist/core/dashboard.js +0 -157
  219. package/dist/core/priority.d.ts +0 -52
  220. package/dist/core/priority.js +0 -102
  221. package/src/core/dashboard.ts +0 -219
  222. package/src/core/priority.ts +0 -140
  223. package/src/dtv/data.ts +0 -29
  224. package/src/dtv/trust.ts +0 -43
  225. package/src/dtv/value.ts +0 -47
  226. package/src/marketplace/index.ts +0 -223
@@ -1,160 +1,217 @@
1
- import { BaseChannel } from './index';
2
- import type { Message } from '../core/types';
3
-
4
- /**
5
- * Slack Channel — v0.8.0
6
- * Slack Bot with Socket Mode / Events API support, threads, and slash commands.
7
- */
8
-
9
- export interface SlackChannelConfig {
10
- /** Bot token (xoxb-...) */
11
- botToken: string;
12
- /** App-level token for Socket Mode (xapp-...) */
13
- appToken?: string;
14
- /** Signing secret for Events API verification */
15
- signingSecret?: string;
16
- /** Use Socket Mode (true) or Events API (false) */
17
- socketMode?: boolean;
18
- /** Port for Events API webhook server (default: 3001) */
19
- port?: number;
20
- /** Slash commands to register */
21
- slashCommands?: SlashCommandConfig[];
22
- }
23
-
24
- export interface SlashCommandConfig {
25
- command: string;
26
- description: string;
27
- handler?: (payload: SlashCommandPayload) => Promise<string>;
28
- }
29
-
30
- export interface SlashCommandPayload {
31
- command: string;
32
- text: string;
33
- userId: string;
34
- channelId: string;
35
- triggerId: string;
36
- }
37
-
38
- export interface SlackMessageEvent {
39
- type: string;
40
- channel: string;
41
- user: string;
42
- text: string;
43
- ts: string;
44
- threadTs?: string;
45
- botId?: string;
46
- }
47
-
48
- export class SlackChannel extends BaseChannel {
49
- type = 'slack';
50
- private config: SlackChannelConfig;
51
- private running = false;
52
- private slashHandlers: Map<string, SlashCommandConfig> = new Map();
53
-
54
- constructor(config: SlackChannelConfig) {
55
- super();
56
- this.config = config;
57
-
58
- for (const cmd of config.slashCommands ?? []) {
59
- this.slashHandlers.set(cmd.command, cmd);
60
- }
61
- }
62
-
63
- async start(): Promise<void> {
64
- this.running = true;
65
-
66
- if (this.config.socketMode && this.config.appToken) {
67
- await this.startSocketMode();
68
- } else {
69
- await this.startEventsAPI();
70
- }
71
- }
72
-
73
- async stop(): Promise<void> {
74
- this.running = false;
75
- // Cleanup connections
76
- }
77
-
78
- /** Start Socket Mode connection */
79
- private async startSocketMode(): Promise<void> {
80
- // TODO: Implement with @slack/socket-mode
81
- // const { SocketModeClient } = await import('@slack/socket-mode');
82
- // const client = new SocketModeClient({ appToken: this.config.appToken! });
83
- // client.on('message', (event) => this.handleMessage(event));
84
- // await client.start();
85
- }
86
-
87
- /** Start Events API HTTP server */
88
- private async startEventsAPI(): Promise<void> {
89
- // TODO: Implement with express or http
90
- // const port = this.config.port ?? 3001;
91
- // Listen for POST /slack/events and /slack/commands
92
- }
93
-
94
- /** Handle incoming Slack message */
95
- async handleMessage(event: SlackMessageEvent): Promise<void> {
96
- // Ignore bot messages
97
- if (event.botId) return;
98
-
99
- const message = this.slackToMessage(event);
100
- if (this.handler) {
101
- const reply = await this.handler(message);
102
- await this.sendMessage(event.channel, reply.content, event.threadTs ?? event.ts);
103
- }
104
- }
105
-
106
- /** Handle slash command */
107
- async handleSlashCommand(payload: SlashCommandPayload): Promise<string> {
108
- const cmd = this.slashHandlers.get(payload.command);
109
- if (cmd?.handler) {
110
- return cmd.handler(payload);
111
- }
112
-
113
- // Default: pass to message handler
114
- const message: Message = {
115
- id: `slack-cmd-${Date.now()}`,
116
- role: 'user',
117
- content: `${payload.command} ${payload.text}`.trim(),
118
- timestamp: Date.now(),
119
- metadata: {
120
- channel: 'slack',
121
- channelId: payload.channelId,
122
- userId: payload.userId,
123
- isSlashCommand: true,
124
- },
125
- };
126
-
127
- if (this.handler) {
128
- const reply = await this.handler(message);
129
- return reply.content;
130
- }
131
- return 'Command received.';
132
- }
133
-
134
- /** Convert Slack event to Message */
135
- private slackToMessage(event: SlackMessageEvent): Message {
136
- return {
137
- id: event.ts,
138
- role: 'user',
139
- content: event.text,
140
- timestamp: parseFloat(event.ts) * 1000,
141
- metadata: {
142
- channel: 'slack',
143
- channelId: event.channel,
144
- userId: event.user,
145
- threadTs: event.threadTs,
146
- },
147
- };
148
- }
149
-
150
- /** Send a message to a Slack channel */
151
- async sendMessage(channel: string, text: string, threadTs?: string): Promise<void> {
152
- // TODO: Implement with @slack/web-api
153
- // const { WebClient } = await import('@slack/web-api');
154
- // const client = new WebClient(this.config.botToken);
155
- // await client.chat.postMessage({ channel, text, thread_ts: threadTs });
156
- void channel;
157
- void text;
158
- void threadTs;
159
- }
160
- }
1
+ import { BaseChannel } from './index';
2
+ import type { Message } from '../core/types';
3
+
4
+ /**
5
+ * Slack Channel — v0.8.0
6
+ * Slack Bot with Socket Mode / Events API support, threads, and slash commands.
7
+ */
8
+
9
+ export interface SlackChannelConfig {
10
+ /** Bot token (xoxb-...) */
11
+ botToken: string;
12
+ /** App-level token for Socket Mode (xapp-...) */
13
+ appToken?: string;
14
+ /** Signing secret for Events API verification */
15
+ signingSecret?: string;
16
+ /** Use Socket Mode (true) or Events API (false) */
17
+ socketMode?: boolean;
18
+ /** Port for Events API webhook server (default: 3001) */
19
+ port?: number;
20
+ /** Slash commands to register */
21
+ slashCommands?: SlashCommandConfig[];
22
+ }
23
+
24
+ export interface SlashCommandConfig {
25
+ command: string;
26
+ description: string;
27
+ handler?: (payload: SlashCommandPayload) => Promise<string>;
28
+ }
29
+
30
+ export interface SlashCommandPayload {
31
+ command: string;
32
+ text: string;
33
+ userId: string;
34
+ channelId: string;
35
+ triggerId: string;
36
+ }
37
+
38
+ export interface SlackMessageEvent {
39
+ type: string;
40
+ channel: string;
41
+ user: string;
42
+ text: string;
43
+ ts: string;
44
+ threadTs?: string;
45
+ botId?: string;
46
+ }
47
+
48
+ export class SlackChannel extends BaseChannel {
49
+ type = 'slack';
50
+ private config: SlackChannelConfig;
51
+ private running = false;
52
+ private slashHandlers: Map<string, SlashCommandConfig> = new Map();
53
+
54
+ constructor(config: SlackChannelConfig) {
55
+ super();
56
+ this.config = config;
57
+
58
+ for (const cmd of config.slashCommands ?? []) {
59
+ this.slashHandlers.set(cmd.command, cmd);
60
+ }
61
+ }
62
+
63
+ async start(): Promise<void> {
64
+ this.running = true;
65
+
66
+ if (this.config.socketMode && this.config.appToken) {
67
+ await this.startSocketMode();
68
+ } else {
69
+ await this.startEventsAPI();
70
+ }
71
+ }
72
+
73
+ async stop(): Promise<void> {
74
+ this.running = false;
75
+ // Cleanup connections
76
+ }
77
+
78
+ /** Start Socket Mode connection */
79
+ private async startSocketMode(): Promise<void> {
80
+ // TODO: Implement with @slack/socket-mode
81
+ // const { SocketModeClient } = await import('@slack/socket-mode');
82
+ // const client = new SocketModeClient({ appToken: this.config.appToken! });
83
+ // client.on('message', (event) => this.handleMessage(event));
84
+ // await client.start();
85
+ }
86
+
87
+ /** Start Events API HTTP server */
88
+ private async startEventsAPI(): Promise<void> {
89
+ const http = await import('http');
90
+ const port = this.config.port ?? 3001;
91
+
92
+ const server = http.createServer(async (req, res) => {
93
+ if (req.method !== 'POST') {
94
+ res.writeHead(404);
95
+ res.end();
96
+ return;
97
+ }
98
+
99
+ const chunks: Buffer[] = [];
100
+ for await (const chunk of req) chunks.push(chunk as Buffer);
101
+ const body = JSON.parse(Buffer.concat(chunks).toString());
102
+
103
+ // URL verification challenge
104
+ if (body.type === 'url_verification') {
105
+ res.writeHead(200, { 'Content-Type': 'application/json' });
106
+ res.end(JSON.stringify({ challenge: body.challenge }));
107
+ return;
108
+ }
109
+
110
+ // Event callback
111
+ if (body.type === 'event_callback' && body.event) {
112
+ const event = body.event as SlackMessageEvent;
113
+ if (event.type === 'message' || event.type === 'app_mention') {
114
+ // Don't block the HTTP response
115
+ this.handleMessage(event).catch(() => {});
116
+ }
117
+ }
118
+
119
+ // Slash commands (form-urlencoded, but we handle JSON for simplicity)
120
+ if (req.url === '/slack/commands' && body.command) {
121
+ const reply = await this.handleSlashCommand({
122
+ command: body.command,
123
+ text: body.text ?? '',
124
+ userId: body.user_id,
125
+ channelId: body.channel_id,
126
+ triggerId: body.trigger_id,
127
+ });
128
+ res.writeHead(200, { 'Content-Type': 'application/json' });
129
+ res.end(JSON.stringify({ response_type: 'ephemeral', text: reply }));
130
+ return;
131
+ }
132
+
133
+ res.writeHead(200);
134
+ res.end('ok');
135
+ });
136
+
137
+ server.listen(port, () => {
138
+ console.log(`[SlackChannel] Events API listening on port ${port}`);
139
+ });
140
+ }
141
+
142
+ /** Handle incoming Slack message */
143
+ async handleMessage(event: SlackMessageEvent): Promise<void> {
144
+ // Ignore bot messages
145
+ if (event.botId) return;
146
+
147
+ const message = this.slackToMessage(event);
148
+ if (this.handler) {
149
+ const reply = await this.handler(message);
150
+ await this.sendMessage(event.channel, reply.content, event.threadTs ?? event.ts);
151
+ }
152
+ }
153
+
154
+ /** Handle slash command */
155
+ async handleSlashCommand(payload: SlashCommandPayload): Promise<string> {
156
+ const cmd = this.slashHandlers.get(payload.command);
157
+ if (cmd?.handler) {
158
+ return cmd.handler(payload);
159
+ }
160
+
161
+ // Default: pass to message handler
162
+ const message: Message = {
163
+ id: `slack-cmd-${Date.now()}`,
164
+ role: 'user',
165
+ content: `${payload.command} ${payload.text}`.trim(),
166
+ timestamp: Date.now(),
167
+ metadata: {
168
+ channel: 'slack',
169
+ channelId: payload.channelId,
170
+ userId: payload.userId,
171
+ isSlashCommand: true,
172
+ },
173
+ };
174
+
175
+ if (this.handler) {
176
+ const reply = await this.handler(message);
177
+ return reply.content;
178
+ }
179
+ return 'Command received.';
180
+ }
181
+
182
+ /** Convert Slack event to Message */
183
+ private slackToMessage(event: SlackMessageEvent): Message {
184
+ return {
185
+ id: event.ts,
186
+ role: 'user',
187
+ content: event.text,
188
+ timestamp: parseFloat(event.ts) * 1000,
189
+ metadata: {
190
+ channel: 'slack',
191
+ channelId: event.channel,
192
+ userId: event.user,
193
+ threadTs: event.threadTs,
194
+ },
195
+ };
196
+ }
197
+
198
+ /** Send a message to a Slack channel */
199
+ async sendMessage(channel: string, text: string, threadTs?: string): Promise<void> {
200
+ const body: Record<string, string> = { channel, text };
201
+ if (threadTs) body.thread_ts = threadTs;
202
+
203
+ const res = await fetch('https://slack.com/api/chat.postMessage', {
204
+ method: 'POST',
205
+ headers: {
206
+ 'Authorization': `Bearer ${this.config.botToken}`,
207
+ 'Content-Type': 'application/json',
208
+ },
209
+ body: JSON.stringify(body),
210
+ });
211
+
212
+ const data = await res.json() as { ok: boolean; error?: string };
213
+ if (!data.ok) {
214
+ console.error(`[SlackChannel] chat.postMessage failed: ${data.error}`);
215
+ }
216
+ }
217
+ }
@@ -2,21 +2,45 @@ import type { Message } from '../core/types';
2
2
  import { BaseChannel } from './index';
3
3
 
4
4
  /**
5
- * Telegram channel — basic webhook handler for Telegram Bot API.
6
- * Set TELEGRAM_BOT_TOKEN env var or pass in config.
5
+ * Telegram channel — supports both long-polling and webhook modes.
6
+ *
7
+ * Config:
8
+ * token: bot token (or TELEGRAM_BOT_TOKEN env var)
9
+ * mode: 'polling' | 'webhook' (default: 'polling')
10
+ * webhookUrl: required for webhook mode
11
+ * port: webhook server port (default: 3001)
12
+ *
13
+ * Polling mode requires no public URL — ideal for dev/local.
14
+ * Webhook mode is more efficient for production.
7
15
  */
16
+
17
+ export interface TelegramChannelConfig {
18
+ token?: string;
19
+ mode?: 'polling' | 'webhook';
20
+ webhookUrl?: string;
21
+ port?: number;
22
+ }
23
+
8
24
  export class TelegramChannel extends BaseChannel {
9
25
  readonly type = 'telegram';
10
26
  private token: string;
27
+ private mode: 'polling' | 'webhook';
11
28
  private webhookUrl?: string;
12
- private server: import('http').Server | null = null;
13
29
  private port: number;
14
30
 
15
- constructor(options: { token?: string; webhookUrl?: string; port?: number } = {}) {
31
+ // Polling state
32
+ private offset: number = 0;
33
+ private polling: boolean = false;
34
+
35
+ // Webhook state
36
+ private server: import('http').Server | null = null;
37
+
38
+ constructor(config: TelegramChannelConfig = {}) {
16
39
  super();
17
- this.token = options.token ?? process.env.TELEGRAM_BOT_TOKEN ?? '';
18
- this.webhookUrl = options.webhookUrl;
19
- this.port = options.port ?? 3001;
40
+ this.token = config.token ?? process.env.TELEGRAM_BOT_TOKEN ?? '';
41
+ this.mode = config.mode ?? 'polling';
42
+ this.webhookUrl = config.webhookUrl;
43
+ this.port = config.port ?? 3001;
20
44
  }
21
45
 
22
46
  async start(): Promise<void> {
@@ -25,39 +49,88 @@ export class TelegramChannel extends BaseChannel {
25
49
  return;
26
50
  }
27
51
 
52
+ if (this.mode === 'webhook') {
53
+ await this.startWebhook();
54
+ } else {
55
+ await this.startPolling();
56
+ }
57
+ }
58
+
59
+ async stop(): Promise<void> {
60
+ if (this.mode === 'webhook') {
61
+ await this.stopWebhook();
62
+ } else {
63
+ this.polling = false;
64
+ }
65
+ }
66
+
67
+ // ─── Polling Mode ────────────────────────────────────────
68
+
69
+ private async startPolling(): Promise<void> {
70
+ // Delete any existing webhook so polling works
71
+ await this.apiCall('deleteWebhook');
72
+ console.log(`[TelegramChannel] Started long-polling mode`);
73
+ this.polling = true;
74
+ this.poll();
75
+ }
76
+
77
+ private async poll(): Promise<void> {
78
+ while (this.polling) {
79
+ try {
80
+ const updates = await this.getUpdates();
81
+ for (const update of updates) {
82
+ await this.processUpdate(update);
83
+ }
84
+ } catch (err) {
85
+ console.error('[TelegramChannel] Polling error:', err);
86
+ // Back off on error
87
+ if (this.polling) {
88
+ await new Promise((r) => setTimeout(r, 5000));
89
+ }
90
+ }
91
+ }
92
+ }
93
+
94
+ private async getUpdates(): Promise<any[]> {
95
+ const url = `https://api.telegram.org/bot${this.token}/getUpdates?offset=${this.offset}&timeout=30&allowed_updates=["message"]`;
96
+ const controller = new AbortController();
97
+ const timeout = setTimeout(() => controller.abort(), 40000); // 30s long-poll + 10s buffer
98
+
99
+ try {
100
+ const res = await fetch(url, { signal: controller.signal });
101
+ const data = (await res.json()) as { ok: boolean; result: any[] };
102
+ if (data.ok && data.result.length > 0) {
103
+ this.offset = data.result[data.result.length - 1].update_id + 1;
104
+ }
105
+ return data.result || [];
106
+ } finally {
107
+ clearTimeout(timeout);
108
+ }
109
+ }
110
+
111
+ // ─── Webhook Mode ────────────────────────────────────────
112
+
113
+ private async startWebhook(): Promise<void> {
114
+ if (this.webhookUrl) {
115
+ await this.apiCall('setWebhook', { url: `${this.webhookUrl}/webhook/${this.token}` });
116
+ }
117
+
28
118
  const express = (await import('express')).default;
29
119
  const app = express();
30
120
  app.use(express.json());
31
121
 
32
122
  app.post(`/webhook/${this.token}`, async (req, res) => {
33
123
  try {
34
- const update = req.body;
35
- if (update.message?.text && this.handler) {
36
- const msg: Message = {
37
- id: `tg_${update.message.message_id}`,
38
- role: 'user',
39
- content: update.message.text,
40
- timestamp: update.message.date * 1000,
41
- metadata: {
42
- sessionId: `tg_${update.message.chat.id}`,
43
- chatId: update.message.chat.id,
44
- userId: update.message.from?.id,
45
- platform: 'telegram',
46
- },
47
- };
48
-
49
- const response = await this.handler(msg);
50
- await this.sendMessage(update.message.chat.id, response.content);
51
- }
124
+ await this.processUpdate(req.body);
52
125
  res.json({ ok: true });
53
126
  } catch (err) {
54
- console.error('[TelegramChannel] Error handling update:', err);
127
+ console.error('[TelegramChannel] Webhook error:', err);
55
128
  res.status(500).json({ error: 'Internal error' });
56
129
  }
57
130
  });
58
131
 
59
132
  app.get('/health', (_req, res) => {
60
- res.json({ status: 'ok', channel: 'telegram' });
133
+ res.json({ status: 'ok', channel: 'telegram', mode: 'webhook' });
61
134
  });
62
135
 
63
136
  return new Promise((resolve) => {
@@ -68,23 +141,72 @@ export class TelegramChannel extends BaseChannel {
68
141
  });
69
142
  }
70
143
 
71
- async stop(): Promise<void> {
144
+ private async stopWebhook(): Promise<void> {
72
145
  return new Promise((resolve, reject) => {
73
146
  if (!this.server) return resolve();
74
147
  this.server.close((err) => (err ? reject(err) : resolve()));
75
148
  });
76
149
  }
77
150
 
78
- private async sendMessage(chatId: number, text: string): Promise<void> {
79
- const url = `https://api.telegram.org/bot${this.token}/sendMessage`;
151
+ // ─── Shared ──────────────────────────────────────────────
152
+
153
+ private async processUpdate(update: any): Promise<void> {
154
+ const message = update.message || update.edited_message;
155
+ if (!message?.text || !this.handler) return;
156
+
157
+ const msg: Message = {
158
+ id: `tg_${message.message_id}`,
159
+ role: 'user',
160
+ content: message.text,
161
+ timestamp: message.date * 1000,
162
+ metadata: {
163
+ sessionId: `tg_${message.chat.id}`,
164
+ chatId: message.chat.id,
165
+ userId: message.from?.id,
166
+ username: message.from?.username,
167
+ firstName: message.from?.first_name,
168
+ platform: 'telegram',
169
+ chatType: message.chat.type,
170
+ },
171
+ };
172
+
173
+ const response = await this.handler(msg);
174
+ await this.sendMessage(message.chat.id, response.content);
175
+ }
176
+
177
+ async sendMessage(chatId: number | string, text: string): Promise<void> {
178
+ // Telegram max message length is 4096
179
+ const chunks = this.splitText(text, 4096);
180
+ for (const chunk of chunks) {
181
+ await this.apiCall('sendMessage', {
182
+ chat_id: chatId,
183
+ text: chunk,
184
+ parse_mode: 'Markdown',
185
+ });
186
+ }
187
+ }
188
+
189
+ private async apiCall(method: string, body?: Record<string, unknown>): Promise<any> {
190
+ const url = `https://api.telegram.org/bot${this.token}/${method}`;
80
191
  try {
81
- await fetch(url, {
192
+ const res = await fetch(url, {
82
193
  method: 'POST',
83
194
  headers: { 'Content-Type': 'application/json' },
84
- body: JSON.stringify({ chat_id: chatId, text, parse_mode: 'Markdown' }),
195
+ body: body ? JSON.stringify(body) : undefined,
85
196
  });
197
+ return await res.json();
86
198
  } catch (err) {
87
- console.error('[TelegramChannel] Failed to send message:', err);
199
+ console.error(`[TelegramChannel] API call ${method} failed:`, err);
200
+ throw err;
201
+ }
202
+ }
203
+
204
+ private splitText(text: string, maxLen: number): string[] {
205
+ if (text.length <= maxLen) return [text];
206
+ const parts: string[] = [];
207
+ for (let i = 0; i < text.length; i += maxLen) {
208
+ parts.push(text.slice(i, i + maxLen));
88
209
  }
210
+ return parts;
89
211
  }
90
212
  }