@insta-dev01/insta-plugin-openclaw 1.0.0 → 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 (127) hide show
  1. package/dist/index.d.ts +9 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +171 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/src/channel/config.d.ts +9 -0
  6. package/dist/src/channel/config.d.ts.map +1 -0
  7. package/dist/src/channel/config.js +10 -0
  8. package/dist/src/channel/config.js.map +1 -0
  9. package/dist/src/channel/connection.d.ts +34 -0
  10. package/dist/src/channel/connection.d.ts.map +1 -0
  11. package/dist/src/channel/connection.js +281 -0
  12. package/dist/src/channel/connection.js.map +1 -0
  13. package/dist/src/channel/dispatcher.d.ts +43 -0
  14. package/dist/src/channel/dispatcher.d.ts.map +1 -0
  15. package/dist/src/channel/dispatcher.js +324 -0
  16. package/dist/src/channel/dispatcher.js.map +1 -0
  17. package/dist/src/channel/index.d.ts +5 -0
  18. package/dist/src/channel/index.d.ts.map +1 -0
  19. package/dist/src/channel/index.js +135 -0
  20. package/dist/src/channel/index.js.map +1 -0
  21. package/dist/src/channel/logger.d.ts +10 -0
  22. package/dist/src/channel/logger.d.ts.map +1 -0
  23. package/dist/src/channel/logger.js +30 -0
  24. package/dist/src/channel/logger.js.map +1 -0
  25. package/dist/src/channel/protocol.d.ts +15 -0
  26. package/dist/src/channel/protocol.d.ts.map +1 -0
  27. package/dist/src/channel/protocol.js +204 -0
  28. package/dist/src/channel/protocol.js.map +1 -0
  29. package/dist/src/channel/registrar.d.ts +21 -0
  30. package/dist/src/channel/registrar.d.ts.map +1 -0
  31. package/dist/src/channel/registrar.js +115 -0
  32. package/dist/src/channel/registrar.js.map +1 -0
  33. package/dist/src/channel/registration-store.d.ts +21 -0
  34. package/dist/src/channel/registration-store.d.ts.map +1 -0
  35. package/{src/channel/registration-store.ts → dist/src/channel/registration-store.js} +21 -46
  36. package/dist/src/channel/registration-store.js.map +1 -0
  37. package/dist/src/channel/types.d.ts +80 -0
  38. package/dist/src/channel/types.d.ts.map +1 -0
  39. package/dist/src/channel/types.js +3 -0
  40. package/dist/src/channel/types.js.map +1 -0
  41. package/dist/src/core/index.d.ts +5 -0
  42. package/dist/src/core/index.d.ts.map +1 -0
  43. package/dist/src/core/index.js +3 -0
  44. package/dist/src/core/index.js.map +1 -0
  45. package/dist/src/core/register-identity.d.ts +58 -0
  46. package/dist/src/core/register-identity.d.ts.map +1 -0
  47. package/dist/src/core/register-identity.js +251 -0
  48. package/dist/src/core/register-identity.js.map +1 -0
  49. package/dist/src/core/task-api.d.ts +31 -0
  50. package/dist/src/core/task-api.d.ts.map +1 -0
  51. package/dist/src/core/task-api.js +116 -0
  52. package/dist/src/core/task-api.js.map +1 -0
  53. package/dist/src/core/urls.d.ts +33 -0
  54. package/dist/src/core/urls.d.ts.map +1 -0
  55. package/dist/src/core/urls.js +40 -0
  56. package/dist/src/core/urls.js.map +1 -0
  57. package/dist/src/tools/get-plugin-profile.d.ts +7 -0
  58. package/dist/src/tools/get-plugin-profile.d.ts.map +1 -0
  59. package/dist/src/tools/get-plugin-profile.js +132 -0
  60. package/dist/src/tools/get-plugin-profile.js.map +1 -0
  61. package/dist/src/tools/grab-task.d.ts +7 -0
  62. package/dist/src/tools/grab-task.d.ts.map +1 -0
  63. package/dist/src/tools/grab-task.js +100 -0
  64. package/dist/src/tools/grab-task.js.map +1 -0
  65. package/dist/src/tools/list-tasks.d.ts +7 -0
  66. package/dist/src/tools/list-tasks.d.ts.map +1 -0
  67. package/dist/src/tools/list-tasks.js +92 -0
  68. package/dist/src/tools/list-tasks.js.map +1 -0
  69. package/dist/src/tools/propose-registration.d.ts +14 -0
  70. package/dist/src/tools/propose-registration.d.ts.map +1 -0
  71. package/dist/src/tools/propose-registration.js +103 -0
  72. package/dist/src/tools/propose-registration.js.map +1 -0
  73. package/dist/src/tools/register-identity.d.ts +11 -0
  74. package/dist/src/tools/register-identity.d.ts.map +1 -0
  75. package/dist/src/tools/register-identity.js +101 -0
  76. package/dist/src/tools/register-identity.js.map +1 -0
  77. package/dist/src/tools/submit-deliverable.d.ts +17 -0
  78. package/dist/src/tools/submit-deliverable.d.ts.map +1 -0
  79. package/dist/src/tools/submit-deliverable.js +215 -0
  80. package/dist/src/tools/submit-deliverable.js.map +1 -0
  81. package/dist/src/tools/upload-artifact.d.ts +14 -0
  82. package/dist/src/tools/upload-artifact.d.ts.map +1 -0
  83. package/dist/src/tools/upload-artifact.js +166 -0
  84. package/dist/src/tools/upload-artifact.js.map +1 -0
  85. package/dist/src/utils/file-lock.d.ts +4 -0
  86. package/dist/src/utils/file-lock.d.ts.map +1 -0
  87. package/dist/src/utils/file-lock.js +43 -0
  88. package/dist/src/utils/file-lock.js.map +1 -0
  89. package/dist/src/utils/profile.d.ts +17 -0
  90. package/dist/src/utils/profile.d.ts.map +1 -0
  91. package/dist/src/utils/profile.js +26 -0
  92. package/dist/src/utils/profile.js.map +1 -0
  93. package/dist/src/utils/session.d.ts +3 -0
  94. package/dist/src/utils/session.d.ts.map +1 -0
  95. package/dist/src/utils/session.js +26 -0
  96. package/dist/src/utils/session.js.map +1 -0
  97. package/package.json +17 -5
  98. package/.env.example +0 -23
  99. package/channel/346/265/201/347/250/213/345/233/276.md +0 -477
  100. package/index.ts +0 -198
  101. package/src/channel/config.ts +0 -27
  102. package/src/channel/connection.ts +0 -341
  103. package/src/channel/dispatcher.ts +0 -374
  104. package/src/channel/index.ts +0 -173
  105. package/src/channel/logger.ts +0 -36
  106. package/src/channel/protocol.ts +0 -265
  107. package/src/channel/registrar.ts +0 -172
  108. package/src/channel/types.ts +0 -102
  109. package/src/core/index.ts +0 -13
  110. package/src/core/register-identity.ts +0 -326
  111. package/src/core/task-api.ts +0 -168
  112. package/src/core/urls.ts +0 -52
  113. package/src/prompt/job.md +0 -21
  114. package/src/tools/get-plugin-profile.ts +0 -152
  115. package/src/tools/grab-task.ts +0 -133
  116. package/src/tools/list-tasks.ts +0 -135
  117. package/src/tools/propose-registration.ts +0 -116
  118. package/src/tools/register-identity.ts +0 -121
  119. package/src/tools/submit-deliverable.ts +0 -268
  120. package/src/tools/upload-artifact.ts +0 -222
  121. package/src/utils/file-lock.ts +0 -43
  122. package/src/utils/profile.ts +0 -45
  123. package/src/utils/session.ts +0 -30
  124. package/tests/profile.test.ts +0 -70
  125. package/tests/session.test.ts +0 -53
  126. package/tsconfig.json +0 -49
  127. package/vitest.config.ts +0 -26
package/index.ts DELETED
@@ -1,198 +0,0 @@
1
- import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
2
- import { plugin } from "./src/channel/index.js";
3
- import { getPluginProfileToolFactory } from "./src/tools/get-plugin-profile.js";
4
- import { uploadArtifactToolFactory } from "./src/tools/upload-artifact.js";
5
- import { registerIdentityToolFactory } from "./src/tools/register-identity.js";
6
- import { proposeRegistrationToolFactory } from "./src/tools/propose-registration.js";
7
- import { listTasksToolFactory } from "./src/tools/list-tasks.js";
8
- import { grabTaskToolFactory } from "./src/tools/grab-task.js";
9
- import { submitDeliverableToolFactory } from "./src/tools/submit-deliverable.js";
10
- import { getPendingRegistration, clearPendingRegistration, getBaseDir } from "./src/channel/registration-store.js";
11
- import { registerIdentity, type RegisterIdentityParams } from "./src/core/register-identity.js";
12
-
13
- // ── Registration confirmation control UI schema ───────────────────────────────
14
-
15
- const REGISTRATION_FORM_SCHEMA = {
16
- type: "object",
17
- description: "InstaClaw 服务注册参数(可在此修改后点击确认注册)",
18
- properties: {
19
- name: {
20
- type: "string",
21
- description: "档案名称",
22
- },
23
- avatar: {
24
- type: "string",
25
- description: "头像图片 URL",
26
- },
27
- description: {
28
- type: "string",
29
- description: "服务描述",
30
- },
31
- hourly_rate: {
32
- type: "number",
33
- description: "时薪(元,必须 > 0)",
34
- minimum: 0.01,
35
- },
36
- scene_tags: {
37
- type: "array",
38
- items: { type: "string" },
39
- description: "场景标签 ID 数组(1-3 个)",
40
- minItems: 1,
41
- maxItems: 3,
42
- },
43
- custom_tags: {
44
- type: "array",
45
- items: { type: "string" },
46
- description: "自定义标签(可选)",
47
- },
48
- instance_type: {
49
- type: "number",
50
- enum: [0, 1],
51
- description: "实例类型:0 = 本地实例,1 = 影子实例",
52
- },
53
- },
54
- required: ["name", "avatar", "description", "hourly_rate", "scene_tags", "instance_type"],
55
- };
56
-
57
- // ── Plugin entry ──────────────────────────────────────────────────────────────
58
-
59
- export default definePluginEntry({
60
- id: "insta-plugin-openclaw",
61
- name: "insta-plugin-openclaw",
62
- description: "Instagram Claw Connector",
63
- register(api) {
64
- // ── Tools ────────────────────────────────────────────────────────────────
65
-
66
- // 注册档案(首次启动时由注册向导自动调用)
67
- api.registerTool(registerIdentityToolFactory);
68
-
69
- // AI 提案工具:生成注册参数并暂存,等待用户在控制面板确认
70
- api.registerTool(proposeRegistrationToolFactory);
71
-
72
- // 注册插件配置/注册状态查询工具
73
- api.registerTool(getPluginProfileToolFactory);
74
-
75
- // 注册制品文件上传工具
76
- api.registerTool(uploadArtifactToolFactory);
77
-
78
- // 注册任务池查询工具
79
- api.registerTool(listTasksToolFactory);
80
-
81
- // 注册任务抢单工具
82
- api.registerTool(grabTaskToolFactory);
83
-
84
- // 注册任务产物打包提交工具
85
- api.registerTool(submitDeliverableToolFactory);
86
-
87
- // ── Session state extension ──────────────────────────────────────────────
88
- //
89
- // Projects the in-memory pending registration record into the Gateway
90
- // session snapshot so clients can read it via session.pluginExtensions.
91
- // The control UI descriptor below reads this projection to pre-fill the
92
- // confirmation form.
93
-
94
- api.session.state.registerSessionExtension({
95
- namespace: "instaclaw_registration",
96
- description: "InstaClaw 待确认注册信息。当 AI 生成注册提案后此字段会被填充,用户确认后清除。",
97
- project: (_ctx) => {
98
- const pending = getPendingRegistration();
99
- if (!pending) return undefined;
100
- // Expose status + params so the client UI can pre-fill the form fields.
101
- // Cast through `unknown` to satisfy the recursive PluginJsonValue constraint.
102
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
- return {
104
- status: pending.status as string,
105
- createdAt: pending.createdAt,
106
- params: pending.params as unknown as Record<string, string | number | boolean | null>,
107
- } as unknown as any; // PluginJsonValue is a recursive type; `any` avoids deep mismatch
108
- },
109
- });
110
-
111
- // ── Control UI descriptor ────────────────────────────────────────────────
112
- //
113
- // Declares a settings-surface widget that the Gateway client renders when
114
- // `instaclaw_registration.status === "pending_confirmation"`.
115
- // The user can edit the pre-filled fields before submitting.
116
-
117
- api.session.controls.registerControlUiDescriptor({
118
- id: "instaclaw-registration-confirm",
119
- surface: "settings",
120
- label: "InstaClaw 服务注册",
121
- description: "AI 已根据当前 Agent 上下文推荐了一套注册参数,请确认或修改后点击「确认注册」完成注册。",
122
- schema: REGISTRATION_FORM_SCHEMA,
123
- });
124
-
125
- // ── Gateway method: confirm registration ─────────────────────────────────
126
- //
127
- // Called by the client UI when the user clicks "确认注册".
128
- // `params` contains the (possibly edited) registration fields from the form.
129
- // Falls back to the in-memory pending params for any missing fields so the
130
- // form can send only the fields the user changed.
131
-
132
- api.registerGatewayMethod(
133
- "instaclaw.registration.submit",
134
- async (opts) => {
135
- const { params, respond } = opts;
136
-
137
- const pending = getPendingRegistration();
138
-
139
- // Merge: form-provided values take precedence over the stored proposal.
140
- const merged: RegisterIdentityParams = {
141
- name: (params["name"] as string | undefined) ?? pending?.params.name ?? "",
142
- avatar: (params["avatar"] as string | undefined) ?? pending?.params.avatar ?? "",
143
- description: (params["description"] as string | undefined) ?? pending?.params.description ?? "",
144
- hourly_rate: (params["hourly_rate"] as number | undefined) ?? pending?.params.hourly_rate ?? 0,
145
- scene_tags: (params["scene_tags"] as string[] | undefined) ?? pending?.params.scene_tags ?? [],
146
- custom_tags: (params["custom_tags"] as string[] | undefined) ?? pending?.params.custom_tags,
147
- instance_type: ((params["instance_type"] as number | undefined) ?? pending?.params.instance_type ?? 0) as 0 | 1,
148
- };
149
-
150
- // Basic guard: require at least the mandatory fields to be non-empty.
151
- if (!merged.name || !merged.avatar || !merged.description || merged.hourly_rate <= 0 || merged.scene_tags.length === 0) {
152
- respond(false, undefined, {
153
- code: "INVALID_PARAMS",
154
- message: "注册参数不完整,name / avatar / description / hourly_rate / scene_tags 均为必填项。",
155
- });
156
- return;
157
- }
158
-
159
- // Bug 1 fix: use the baseDir cached by startAccount instead of
160
- // process.cwd(), which may point to a different directory.
161
- const baseDir = getBaseDir();
162
- let result: Awaited<ReturnType<typeof registerIdentity>>;
163
-
164
- try {
165
- result = await registerIdentity(merged, baseDir);
166
- } catch (err) {
167
- respond(false, undefined, {
168
- code: "REGISTRATION_EXCEPTION",
169
- message: err instanceof Error ? err.message : String(err),
170
- });
171
- return;
172
- }
173
-
174
- if (!result.success) {
175
- respond(false, undefined, {
176
- code: "REGISTRATION_FAILED",
177
- message: result.error ?? "注册失败,请重试。",
178
- });
179
- return;
180
- }
181
-
182
- // Success: clear pending state so the confirmation UI disappears.
183
- clearPendingRegistration();
184
-
185
- respond(true, {
186
- app_key: result.data?.app_key,
187
- claw_id: result.data?.claw_id,
188
- message: "注册成功 ✓ InstaClaw channel 将在 10 秒内自动建立 WebSocket 连接。",
189
- });
190
- },
191
- );
192
-
193
- // ── Channel ──────────────────────────────────────────────────────────────
194
-
195
- // 注册引态平台 WebSocket 通道
196
- api.registerChannel({ plugin });
197
- },
198
- });
@@ -1,27 +0,0 @@
1
- export const CHANNEL_ID = "insta-connector" as const;
2
-
3
- export const HEARTBEAT_INTERVAL = parseInt(
4
- process.env["INSTACLAW_HEARTBEAT_INTERVAL"] ?? "30000",
5
- 10,
6
- );
7
-
8
- export const MAX_RECONNECT_ATTEMPTS = 0; // 0 = infinite
9
-
10
- export const SDK_REQUEST_TIMEOUT = parseInt(
11
- process.env["SDK_REQUEST_TIMEOUT"] ?? "43200000", // 12 hours
12
- 10,
13
- );
14
-
15
- export const MAX_CONCURRENT_REQUESTS = parseInt(
16
- process.env["MAX_CONCURRENT_REQUESTS"] ?? "10",
17
- 10,
18
- );
19
-
20
- export const TEXT_CHUNK_SIZE = 50;
21
-
22
- export const DEBUG_ENABLED = process.env["INSTA_DEBUG"] === "true";
23
-
24
- export const POLL_INTERVAL_MS = parseInt(
25
- process.env["INSTA_POLL_INTERVAL_MS"] ?? "10000",
26
- 10,
27
- );
@@ -1,341 +0,0 @@
1
- import WebSocket from "ws";
2
- import type { ConnectionConfig, ConnectionState } from "./types.js";
3
- import type { DebugLogger } from "./logger.js";
4
-
5
- // ── WebSocketConnection ───────────────────────────────────────────────────────
6
-
7
- export class WebSocketConnection {
8
- private ws: WebSocket | null = null;
9
- private state: ConnectionState = "disconnected";
10
- private reconnectAttempts = 0;
11
- private heartbeatTimer: NodeJS.Timeout | null = null;
12
- private lastPongTime = 0;
13
- private isStopped = false;
14
- private isReconnecting = false;
15
-
16
- constructor(
17
- private config: ConnectionConfig,
18
- private logger: DebugLogger,
19
- private onMessage: (data: string) => void,
20
- private onStateChange: (state: ConnectionState) => void,
21
- ) {}
22
-
23
- async connect(accountId?: string): Promise<void> {
24
- this.updateState("connecting");
25
- this.logger.info("Establishing WebSocket connection", { url: this.config.wsUrl });
26
-
27
- try {
28
- this.ws = new WebSocket(this.config.wsUrl, {
29
- headers: {
30
- "x-app-key": this.config.clientId,
31
- "x-app-secret": this.config.clientSecret,
32
- },
33
- });
34
-
35
- this.setupEventListeners();
36
- await this.waitForOpen();
37
-
38
- this.updateState("connected");
39
- this.reconnectAttempts = 0;
40
- this.logger.info("WebSocket connected");
41
-
42
- if (accountId) {
43
- const { registerConnection } = await import("./index.js");
44
- registerConnection(accountId, this.ws);
45
- }
46
-
47
- this.startHeartbeat();
48
- } catch (err) {
49
- this.logger.error("Failed to connect", err as Error, { url: this.config.wsUrl });
50
- this.handleReconnect();
51
- throw err;
52
- }
53
- }
54
-
55
- private waitForOpen(): Promise<void> {
56
- return new Promise((resolve, reject) => {
57
- if (!this.ws) return reject(new Error("WebSocket not created"));
58
-
59
- const timeout = setTimeout(() => reject(new Error("Connection timeout")), 10_000);
60
-
61
- this.ws.once("open", () => { clearTimeout(timeout); resolve(); });
62
- this.ws.once("error", (err) => { clearTimeout(timeout); reject(err); });
63
- });
64
- }
65
-
66
- private setupEventListeners(): void {
67
- if (!this.ws) return;
68
-
69
- this.ws.on("message", (data: WebSocket.Data) => {
70
- try {
71
- this.onMessage(data.toString());
72
- } catch (err) {
73
- this.logger.error("Error processing message", err as Error);
74
- }
75
- });
76
-
77
- this.ws.on("pong", () => { this.lastPongTime = Date.now(); });
78
-
79
- this.ws.on("close", (code, reason) => {
80
- this.logger.info("WebSocket closed", { code, reason: reason.toString(), intentional: this.isStopped });
81
- this.stopHeartbeat();
82
- this.updateState("disconnected");
83
- if (!this.isStopped) this.handleReconnect();
84
- });
85
-
86
- this.ws.on("error", (err) => {
87
- this.logger.error("WebSocket error", err, { state: this.state });
88
- });
89
- }
90
-
91
- async disconnect(accountId?: string): Promise<void> {
92
- this.isStopped = true;
93
- this.stopHeartbeat();
94
-
95
- if (accountId) {
96
- try {
97
- const { unregisterConnection } = await import("./index.js");
98
- unregisterConnection(accountId);
99
- } catch (err) {
100
- this.logger.warn("Failed to unregister connection", { accountId, error: (err as Error).message });
101
- }
102
- }
103
-
104
- if (this.ws) {
105
- this.ws.removeAllListeners();
106
- if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
107
- this.ws.close();
108
- }
109
- this.ws = null;
110
- }
111
-
112
- this.updateState("disconnected");
113
- this.logger.info("WebSocket disconnected");
114
- }
115
-
116
- // ── Heartbeat ───────────────────────────────────────────────────────────────
117
-
118
- private startHeartbeat(): void {
119
- this.stopHeartbeat();
120
- this.lastPongTime = Date.now();
121
-
122
- this.heartbeatTimer = setInterval(() => {
123
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
124
-
125
- const timeSinceLastPong = Date.now() - this.lastPongTime;
126
- const threshold = this.config.heartbeatInterval * 3;
127
-
128
- if (timeSinceLastPong > threshold) {
129
- this.logger.warn("Heartbeat timeout, reconnecting", { timeSinceLastPong, threshold });
130
- this.stopHeartbeat();
131
- this.handleReconnect();
132
- return;
133
- }
134
-
135
- try {
136
- this.ws.ping();
137
- } catch (err) {
138
- this.logger.error("Ping failed", err as Error);
139
- this.stopHeartbeat();
140
- this.handleReconnect();
141
- }
142
- }, this.config.heartbeatInterval);
143
- }
144
-
145
- private stopHeartbeat(): void {
146
- if (this.heartbeatTimer) {
147
- clearInterval(this.heartbeatTimer);
148
- this.heartbeatTimer = null;
149
- }
150
- }
151
-
152
- // ── Reconnection ────────────────────────────────────────────────────────────
153
-
154
- private async handleReconnect(): Promise<void> {
155
- if (this.isReconnecting || this.isStopped) return;
156
-
157
- const max = this.config.reconnectMaxAttempts;
158
- if (max && max > 0 && this.reconnectAttempts >= max) {
159
- this.logger.warn("Max reconnect attempts reached", { attempts: this.reconnectAttempts });
160
- return;
161
- }
162
-
163
- this.isReconnecting = true;
164
- this.updateState("reconnecting");
165
-
166
- const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30_000) + Math.random() * 1000;
167
- this.reconnectAttempts++;
168
- this.logger.info("Reconnecting", { attempt: this.reconnectAttempts, delay: Math.round(delay) });
169
-
170
- await new Promise((r) => setTimeout(r, delay));
171
-
172
- if (this.ws) {
173
- this.ws.removeAllListeners();
174
- if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
175
- this.ws.close();
176
- }
177
- this.ws = null;
178
- }
179
-
180
- this.isReconnecting = false;
181
-
182
- try {
183
- await this.connect();
184
- } catch {
185
- // connect() will schedule another reconnect internally
186
- }
187
- }
188
-
189
- // ── Send / State ────────────────────────────────────────────────────────────
190
-
191
- send(data: string): void {
192
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
193
- throw new Error("WebSocket is not connected");
194
- }
195
- this.ws.send(data);
196
- }
197
-
198
- getState(): ConnectionState { return this.state; }
199
- isConnected(): boolean { return this.state === "connected" && this.ws?.readyState === WebSocket.OPEN; }
200
- getWebSocket(): WebSocket | null { return this.ws; }
201
-
202
- private updateState(newState: ConnectionState): void {
203
- if (this.state !== newState) {
204
- this.state = newState;
205
- this.onStateChange(newState);
206
- }
207
- }
208
- }
209
-
210
- // ── monitorProvider ───────────────────────────────────────────────────────────
211
-
212
- export async function monitorProvider(
213
- credentials: { clientId: string; clientSecret: string },
214
- accountId: string,
215
- abortSignal: AbortSignal,
216
- channelRuntime?: unknown,
217
- ): Promise<void> {
218
- const { DebugLogger } = await import("./logger.js");
219
- const { SDKDispatcher } = await import("./dispatcher.js");
220
- const {
221
- HEARTBEAT_INTERVAL,
222
- MAX_RECONNECT_ATTEMPTS,
223
- SDK_REQUEST_TIMEOUT,
224
- MAX_CONCURRENT_REQUESTS,
225
- DEBUG_ENABLED,
226
- } = await import("./config.js");
227
- const { parseRequest, parseEnvelope, TOPIC_USER_MESSAGES } = await import("./protocol.js");
228
- const { getInstaUrl } = await import("../core/urls.js");
229
-
230
- const logger = new DebugLogger(DEBUG_ENABLED, `[InstaPlugin:${accountId}]`);
231
-
232
- logger.info("Starting provider monitor", { accountId, wsUrl: getInstaUrl("wsChat") });
233
-
234
- const dispatcher = new SDKDispatcher(
235
- {
236
- requestTimeout: SDK_REQUEST_TIMEOUT,
237
- maxConcurrentRequests: MAX_CONCURRENT_REQUESTS,
238
- debug: DEBUG_ENABLED,
239
- },
240
- logger,
241
- accountId,
242
- channelRuntime,
243
- );
244
-
245
- async function handleMessage(raw: string): Promise<void> {
246
- try {
247
- let topic: string | undefined;
248
- let envelopeType: string | undefined;
249
-
250
- try {
251
- const peek = JSON.parse(raw) as Record<string, unknown>;
252
- if (peek && Array.isArray(peek["input"]) && peek["metadata"] && !peek["headers"]) {
253
- topic = TOPIC_USER_MESSAGES;
254
- } else {
255
- topic = (peek?.["headers"] as Record<string, string> | undefined)?.["topic"];
256
- envelopeType = peek?.["type"] as string | undefined;
257
- }
258
- } catch {
259
- logger.error("Failed to peek message", undefined, { preview: raw.substring(0, 100) });
260
- return;
261
- }
262
-
263
- if (topic === TOPIC_USER_MESSAGES) {
264
- let request;
265
- try {
266
- request = parseRequest(raw);
267
- } catch (err) {
268
- logger.error("Failed to parse request", err as Error);
269
- return;
270
- }
271
-
272
- logger.info("Received user request", {
273
- messageId: request.messageId,
274
- sessionId: request.sessionId,
275
- contentLength: request.content.length,
276
- });
277
-
278
- const ws = connection.getWebSocket();
279
- if (ws) {
280
- await dispatcher.dispatchRequest(request, ws);
281
- } else {
282
- logger.error("Cannot dispatch: WebSocket unavailable");
283
- }
284
- return;
285
- }
286
-
287
- if (envelopeType !== undefined && envelopeType !== "MESSAGE") {
288
- logger.info("Ignoring non-MESSAGE frame", { type: envelopeType });
289
- return;
290
- }
291
-
292
- // Bot-messages echo path — audit log only, no re-dispatch
293
- try {
294
- const event = parseEnvelope(raw);
295
- logger.debug("Received Open Responses event (audit)", { type: event.type, response_id: event.response_id });
296
- } catch (err) {
297
- logger.error("Failed to parse envelope", err as Error);
298
- }
299
- } catch (err) {
300
- logger.error("Unhandled error in handleMessage", err as Error);
301
- }
302
- }
303
-
304
- const connConfig = {
305
- wsUrl: getInstaUrl("wsChat"),
306
- clientId: credentials.clientId,
307
- clientSecret: credentials.clientSecret,
308
- heartbeatInterval: HEARTBEAT_INTERVAL,
309
- reconnectMaxAttempts: MAX_RECONNECT_ATTEMPTS,
310
- };
311
-
312
- const connection = new WebSocketConnection(
313
- connConfig,
314
- logger,
315
- handleMessage,
316
- (state) => logger.info("Connection state", { state }),
317
- );
318
-
319
- const abortHandler = async () => {
320
- logger.info("Abort received, disconnecting");
321
- await connection.disconnect(accountId);
322
- logger.info("Provider monitor stopped");
323
- };
324
-
325
- if (abortSignal.aborted) {
326
- await abortHandler();
327
- return;
328
- }
329
-
330
- abortSignal.addEventListener("abort", abortHandler, { once: true });
331
-
332
- try {
333
- await connection.connect(accountId);
334
- return new Promise<void>((resolve) => {
335
- abortSignal.addEventListener("abort", () => resolve(), { once: true });
336
- });
337
- } catch (err) {
338
- logger.error("Failed to start provider monitor", err as Error);
339
- throw err;
340
- }
341
- }