chatablex-web-sdk 1.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.
- package/README.md +199 -0
- package/README.zh-CN.md +199 -0
- package/dist/index.d.mts +280 -0
- package/dist/index.d.ts +280 -0
- package/dist/index.js +366 -0
- package/dist/index.mjs +337 -0
- package/package.json +50 -0
- package/src/bridge.ts +162 -0
- package/src/index.ts +115 -0
- package/src/modules/ai.ts +18 -0
- package/src/modules/events.ts +23 -0
- package/src/modules/skills.ts +14 -0
- package/src/modules/storage.ts +18 -0
- package/src/modules/tool.ts +54 -0
- package/src/modules/tools.ts +18 -0
- package/src/modules/ui.ts +26 -0
- package/src/types.ts +270 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
// src/bridge.ts
|
|
2
|
+
var Bridge = class {
|
|
3
|
+
constructor(debug = false) {
|
|
4
|
+
this._msgId = 0;
|
|
5
|
+
this._pending = /* @__PURE__ */ new Map();
|
|
6
|
+
this._listeners = /* @__PURE__ */ new Map();
|
|
7
|
+
this._debug = debug;
|
|
8
|
+
}
|
|
9
|
+
// -------------------------------------------------------------------------
|
|
10
|
+
// Lifecycle
|
|
11
|
+
// -------------------------------------------------------------------------
|
|
12
|
+
/** Install the global ChatableXReceive handler so Flutter can push data in. */
|
|
13
|
+
install() {
|
|
14
|
+
window.ChatableXReceive = (jsonStr) => {
|
|
15
|
+
try {
|
|
16
|
+
const data = JSON.parse(jsonStr);
|
|
17
|
+
if (data.type === "response") {
|
|
18
|
+
this._handleResponse(data);
|
|
19
|
+
} else if (data.type === "event") {
|
|
20
|
+
this._handleEvent(data);
|
|
21
|
+
}
|
|
22
|
+
} catch (e) {
|
|
23
|
+
console.error("[ChatableX Bridge] receive parse error:", e);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
this._log("ChatableXReceive installed");
|
|
27
|
+
}
|
|
28
|
+
/** Wait for ChatableXBridge (set by Flutter) to become available. */
|
|
29
|
+
waitForBridge(timeoutMs) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
if (window.ChatableXBridge) {
|
|
32
|
+
resolve();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const start = Date.now();
|
|
36
|
+
const check = setInterval(() => {
|
|
37
|
+
if (window.ChatableXBridge) {
|
|
38
|
+
clearInterval(check);
|
|
39
|
+
resolve();
|
|
40
|
+
} else if (Date.now() - start > timeoutMs) {
|
|
41
|
+
clearInterval(check);
|
|
42
|
+
reject(new Error(`ChatableXBridge not available after ${timeoutMs}ms`));
|
|
43
|
+
}
|
|
44
|
+
}, 50);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// -------------------------------------------------------------------------
|
|
48
|
+
// Request / Response
|
|
49
|
+
// -------------------------------------------------------------------------
|
|
50
|
+
/** Send a request to Flutter and wait for a response. */
|
|
51
|
+
sendMessage(method, params = {}, requestTimeoutMs = 3e4) {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const id = this._nextId();
|
|
54
|
+
const message = { id, method, params, timestamp: Date.now() };
|
|
55
|
+
const timer = setTimeout(() => {
|
|
56
|
+
if (this._pending.has(id)) {
|
|
57
|
+
this._pending.delete(id);
|
|
58
|
+
reject(new Error(`Request timeout: ${method}`));
|
|
59
|
+
}
|
|
60
|
+
}, requestTimeoutMs);
|
|
61
|
+
this._pending.set(id, { resolve, reject, timer });
|
|
62
|
+
if (window.ChatableXBridge) {
|
|
63
|
+
window.ChatableXBridge.postMessage(JSON.stringify(message));
|
|
64
|
+
} else {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
this._pending.delete(id);
|
|
67
|
+
reject(new Error("ChatableXBridge not available"));
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
_handleResponse(data) {
|
|
72
|
+
const pending = this._pending.get(data.id);
|
|
73
|
+
if (!pending) return;
|
|
74
|
+
this._pending.delete(data.id);
|
|
75
|
+
clearTimeout(pending.timer);
|
|
76
|
+
if (data.success) {
|
|
77
|
+
pending.resolve(data.data);
|
|
78
|
+
} else {
|
|
79
|
+
pending.reject(new Error(data.error ?? "Unknown error"));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// -------------------------------------------------------------------------
|
|
83
|
+
// Events
|
|
84
|
+
// -------------------------------------------------------------------------
|
|
85
|
+
_handleEvent(data) {
|
|
86
|
+
const handlers = this._listeners.get(data.eventType);
|
|
87
|
+
if (handlers) {
|
|
88
|
+
for (const fn of handlers) {
|
|
89
|
+
try {
|
|
90
|
+
fn(data.data);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
console.error("[ChatableX] event handler error:", e);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
addEventListener(eventType, handler) {
|
|
98
|
+
if (!this._listeners.has(eventType)) {
|
|
99
|
+
this._listeners.set(eventType, /* @__PURE__ */ new Set());
|
|
100
|
+
}
|
|
101
|
+
this._listeners.get(eventType).add(handler);
|
|
102
|
+
return () => {
|
|
103
|
+
const set = this._listeners.get(eventType);
|
|
104
|
+
if (set) {
|
|
105
|
+
set.delete(handler);
|
|
106
|
+
if (set.size === 0) this._listeners.delete(eventType);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/** Dispatch a synthetic event (used internally). */
|
|
111
|
+
dispatchEvent(eventType, data) {
|
|
112
|
+
this._handleEvent({ eventType, data });
|
|
113
|
+
}
|
|
114
|
+
// -------------------------------------------------------------------------
|
|
115
|
+
// Internals
|
|
116
|
+
// -------------------------------------------------------------------------
|
|
117
|
+
_nextId() {
|
|
118
|
+
return `ctx_${++this._msgId}_${Date.now()}`;
|
|
119
|
+
}
|
|
120
|
+
_log(...args) {
|
|
121
|
+
if (this._debug) console.log("[ChatableX Bridge]", ...args);
|
|
122
|
+
}
|
|
123
|
+
destroy() {
|
|
124
|
+
for (const [, p] of this._pending) {
|
|
125
|
+
clearTimeout(p.timer);
|
|
126
|
+
p.reject(new Error("Bridge destroyed"));
|
|
127
|
+
}
|
|
128
|
+
this._pending.clear();
|
|
129
|
+
this._listeners.clear();
|
|
130
|
+
window.ChatableXReceive = void 0;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// src/modules/tool.ts
|
|
135
|
+
function createToolModule(bridge, appId) {
|
|
136
|
+
let _info = { id: appId, name: appId, version: "1.0.0", description: "" };
|
|
137
|
+
let _handler = null;
|
|
138
|
+
const dispatch = async (params) => {
|
|
139
|
+
if (!_handler) {
|
|
140
|
+
return { success: false, error: "No execute handler registered" };
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
return await _handler(params);
|
|
144
|
+
} catch (e) {
|
|
145
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
146
|
+
return { success: false, error: msg };
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
window.__CHATABLEX_DISPATCH__ = dispatch;
|
|
150
|
+
bridge.addEventListener("toolExecution", async (data) => {
|
|
151
|
+
const params = data;
|
|
152
|
+
const requestId = params._requestId;
|
|
153
|
+
const result = await dispatch(params);
|
|
154
|
+
if (requestId && window.ChatableXBridge) {
|
|
155
|
+
window.ChatableXBridge.postMessage(JSON.stringify({
|
|
156
|
+
method: "tool.executeResult",
|
|
157
|
+
params: { _requestId: requestId, ...result }
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
return {
|
|
162
|
+
getInfo() {
|
|
163
|
+
return { ..._info };
|
|
164
|
+
},
|
|
165
|
+
onExecute(handler) {
|
|
166
|
+
_handler = handler;
|
|
167
|
+
},
|
|
168
|
+
/** @internal — called by SDK after handshake to fill in tool metadata */
|
|
169
|
+
_setInfo(info) {
|
|
170
|
+
_info = { ..._info, ...info };
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/modules/events.ts
|
|
176
|
+
function createEventsModule(bridge) {
|
|
177
|
+
return {
|
|
178
|
+
on(eventType, callback) {
|
|
179
|
+
bridge.sendMessage("events.subscribe", { eventType }).catch(() => {
|
|
180
|
+
});
|
|
181
|
+
return bridge.addEventListener(eventType, callback);
|
|
182
|
+
},
|
|
183
|
+
onAiResponse(callback) {
|
|
184
|
+
return this.on("aiResponse", callback);
|
|
185
|
+
},
|
|
186
|
+
onToolExecution(callback) {
|
|
187
|
+
return this.on("toolExecution", callback);
|
|
188
|
+
},
|
|
189
|
+
onUserMessage(callback) {
|
|
190
|
+
return this.on("userMessage", callback);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/modules/ai.ts
|
|
196
|
+
function createAIModule(bridge) {
|
|
197
|
+
return {
|
|
198
|
+
chat(message, options) {
|
|
199
|
+
return bridge.sendMessage("ai.chat", { message, ...options });
|
|
200
|
+
},
|
|
201
|
+
chatStream(message, options) {
|
|
202
|
+
return bridge.sendMessage("ai.chatStream", { message, ...options });
|
|
203
|
+
},
|
|
204
|
+
getContext() {
|
|
205
|
+
return bridge.sendMessage("ai.getContext", {});
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/modules/ui.ts
|
|
211
|
+
function createUIModule(bridge) {
|
|
212
|
+
return {
|
|
213
|
+
showNotification(message, type = "info") {
|
|
214
|
+
return bridge.sendMessage("ui.showNotification", { message, type });
|
|
215
|
+
},
|
|
216
|
+
showConfirm(title, message) {
|
|
217
|
+
return bridge.sendMessage("ui.showConfirm", { title, message });
|
|
218
|
+
},
|
|
219
|
+
pickFile(options) {
|
|
220
|
+
return bridge.sendMessage("ui.pickFile", options ?? {});
|
|
221
|
+
},
|
|
222
|
+
openTab(config) {
|
|
223
|
+
return bridge.sendMessage("ui.openTab", config);
|
|
224
|
+
},
|
|
225
|
+
updateState(state) {
|
|
226
|
+
return bridge.sendMessage("ui.updateState", state);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/modules/storage.ts
|
|
232
|
+
function createStorageModule(bridge) {
|
|
233
|
+
return {
|
|
234
|
+
get(key) {
|
|
235
|
+
return bridge.sendMessage("storage.get", { key });
|
|
236
|
+
},
|
|
237
|
+
set(key, value) {
|
|
238
|
+
return bridge.sendMessage("storage.set", { key, value });
|
|
239
|
+
},
|
|
240
|
+
delete(key) {
|
|
241
|
+
return bridge.sendMessage("storage.delete", { key });
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/modules/tools.ts
|
|
247
|
+
function createToolsModule(bridge) {
|
|
248
|
+
return {
|
|
249
|
+
list() {
|
|
250
|
+
return bridge.sendMessage("tools.list", {});
|
|
251
|
+
},
|
|
252
|
+
execute(toolId, params) {
|
|
253
|
+
return bridge.sendMessage("tools.execute", { toolId, params });
|
|
254
|
+
},
|
|
255
|
+
executeWithConfirm(toolId, params) {
|
|
256
|
+
return bridge.sendMessage("tools.executeWithConfirm", { toolId, params });
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/modules/skills.ts
|
|
262
|
+
function createSkillsModule(bridge) {
|
|
263
|
+
return {
|
|
264
|
+
list() {
|
|
265
|
+
return bridge.sendMessage("skills.list", {});
|
|
266
|
+
},
|
|
267
|
+
execute(skillId, variables) {
|
|
268
|
+
return bridge.sendMessage("skills.execute", { skillId, variables });
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/index.ts
|
|
274
|
+
var SDK_VERSION = "1.0.0";
|
|
275
|
+
var _instance = null;
|
|
276
|
+
var ChatableX = {
|
|
277
|
+
/**
|
|
278
|
+
* Initialize the SDK and establish the bridge with the Flutter host.
|
|
279
|
+
*
|
|
280
|
+
* 1. Sets up `window.ChatableXReceive` (Flutter → JS message handler).
|
|
281
|
+
* 2. Waits for `window.ChatableXBridge` (Flutter's JavaScriptChannel).
|
|
282
|
+
* 3. Sends `sdk_init` handshake and receives tool config from Flutter.
|
|
283
|
+
* 4. Returns the fully-initialised SDK instance.
|
|
284
|
+
*/
|
|
285
|
+
async init(config) {
|
|
286
|
+
if (_instance) return _instance;
|
|
287
|
+
const debug = config.debug ?? false;
|
|
288
|
+
const timeout = config.timeout ?? 1e4;
|
|
289
|
+
const bridge = new Bridge(debug);
|
|
290
|
+
bridge.install();
|
|
291
|
+
await bridge.waitForBridge(timeout);
|
|
292
|
+
if (debug) console.log("[ChatableX] Bridge connected, sending sdk_init");
|
|
293
|
+
let toolConfig = {};
|
|
294
|
+
try {
|
|
295
|
+
const resp = await bridge.sendMessage("sdk_init", {
|
|
296
|
+
appId: config.appId,
|
|
297
|
+
sdkVersion: SDK_VERSION
|
|
298
|
+
});
|
|
299
|
+
if (resp && typeof resp === "object") {
|
|
300
|
+
toolConfig = resp;
|
|
301
|
+
}
|
|
302
|
+
} catch {
|
|
303
|
+
if (debug) console.warn("[ChatableX] sdk_init handshake failed, continuing with defaults");
|
|
304
|
+
}
|
|
305
|
+
const toolModule = createToolModule(bridge, config.appId);
|
|
306
|
+
if (toolConfig) toolModule._setInfo(toolConfig);
|
|
307
|
+
const sdk = {
|
|
308
|
+
ai: createAIModule(bridge),
|
|
309
|
+
tools: createToolsModule(bridge),
|
|
310
|
+
skills: createSkillsModule(bridge),
|
|
311
|
+
ui: createUIModule(bridge),
|
|
312
|
+
events: createEventsModule(bridge),
|
|
313
|
+
storage: createStorageModule(bridge),
|
|
314
|
+
tool: toolModule
|
|
315
|
+
};
|
|
316
|
+
window.ChatableX = sdk;
|
|
317
|
+
_instance = sdk;
|
|
318
|
+
if (debug) console.log(`[ChatableX] SDK v${SDK_VERSION} ready for: ${config.appId}`);
|
|
319
|
+
return sdk;
|
|
320
|
+
},
|
|
321
|
+
/** Get the current SDK instance (throws if not initialised). */
|
|
322
|
+
getInstance() {
|
|
323
|
+
if (!_instance) throw new Error("ChatableX SDK not initialised. Call ChatableX.init() first.");
|
|
324
|
+
return _instance;
|
|
325
|
+
},
|
|
326
|
+
/** Check whether the SDK has been initialised. */
|
|
327
|
+
isReady() {
|
|
328
|
+
return _instance !== null;
|
|
329
|
+
},
|
|
330
|
+
/** SDK version */
|
|
331
|
+
version: SDK_VERSION
|
|
332
|
+
};
|
|
333
|
+
export {
|
|
334
|
+
Bridge,
|
|
335
|
+
ChatableX,
|
|
336
|
+
SDK_VERSION
|
|
337
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "chatablex-web-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ChatableX Web SDK for AI App WebUI development. Provides bridge communication with the ChatableX Flutter client.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src",
|
|
18
|
+
"README.md",
|
|
19
|
+
"README.zh-CN.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
23
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"prepublishOnly": "npm run build"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"chatablex",
|
|
29
|
+
"sdk",
|
|
30
|
+
"webui",
|
|
31
|
+
"ai",
|
|
32
|
+
"flutter",
|
|
33
|
+
"webview",
|
|
34
|
+
"bridge"
|
|
35
|
+
],
|
|
36
|
+
"author": "ChatableX Team",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/ersa-org/co-work.git",
|
|
41
|
+
"directory": "chatablex-web-sdk"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"tsup": "^8.0.1",
|
|
45
|
+
"typescript": "^5.3.0"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=16.0.0"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/bridge.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Low-level WebView bridge between the Web SDK and the Flutter host.
|
|
3
|
+
*
|
|
4
|
+
* Communication:
|
|
5
|
+
* JS → Flutter : window.ChatableXBridge.postMessage(JSON.stringify(msg))
|
|
6
|
+
* Flutter → JS : controller.runJavaScript("window.ChatableXReceive('...')")
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
type PendingRequest = {
|
|
10
|
+
resolve: (value: unknown) => void;
|
|
11
|
+
reject: (reason: Error) => void;
|
|
12
|
+
timer: ReturnType<typeof setTimeout>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type EventHandler = (data: unknown) => void;
|
|
16
|
+
|
|
17
|
+
export class Bridge {
|
|
18
|
+
private _msgId = 0;
|
|
19
|
+
private _pending = new Map<string, PendingRequest>();
|
|
20
|
+
private _listeners = new Map<string, Set<EventHandler>>();
|
|
21
|
+
private _debug: boolean;
|
|
22
|
+
|
|
23
|
+
constructor(debug = false) {
|
|
24
|
+
this._debug = debug;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// -------------------------------------------------------------------------
|
|
28
|
+
// Lifecycle
|
|
29
|
+
// -------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/** Install the global ChatableXReceive handler so Flutter can push data in. */
|
|
32
|
+
install(): void {
|
|
33
|
+
window.ChatableXReceive = (jsonStr: string) => {
|
|
34
|
+
try {
|
|
35
|
+
const data = JSON.parse(jsonStr);
|
|
36
|
+
if (data.type === 'response') {
|
|
37
|
+
this._handleResponse(data);
|
|
38
|
+
} else if (data.type === 'event') {
|
|
39
|
+
this._handleEvent(data);
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.error('[ChatableX Bridge] receive parse error:', e);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
this._log('ChatableXReceive installed');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Wait for ChatableXBridge (set by Flutter) to become available. */
|
|
49
|
+
waitForBridge(timeoutMs: number): Promise<void> {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
if (window.ChatableXBridge) {
|
|
52
|
+
resolve();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const start = Date.now();
|
|
56
|
+
const check = setInterval(() => {
|
|
57
|
+
if (window.ChatableXBridge) {
|
|
58
|
+
clearInterval(check);
|
|
59
|
+
resolve();
|
|
60
|
+
} else if (Date.now() - start > timeoutMs) {
|
|
61
|
+
clearInterval(check);
|
|
62
|
+
reject(new Error(`ChatableXBridge not available after ${timeoutMs}ms`));
|
|
63
|
+
}
|
|
64
|
+
}, 50);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// -------------------------------------------------------------------------
|
|
69
|
+
// Request / Response
|
|
70
|
+
// -------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
/** Send a request to Flutter and wait for a response. */
|
|
73
|
+
sendMessage(method: string, params: Record<string, unknown> = {}, requestTimeoutMs = 30_000): Promise<unknown> {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const id = this._nextId();
|
|
76
|
+
const message = { id, method, params, timestamp: Date.now() };
|
|
77
|
+
|
|
78
|
+
const timer = setTimeout(() => {
|
|
79
|
+
if (this._pending.has(id)) {
|
|
80
|
+
this._pending.delete(id);
|
|
81
|
+
reject(new Error(`Request timeout: ${method}`));
|
|
82
|
+
}
|
|
83
|
+
}, requestTimeoutMs);
|
|
84
|
+
|
|
85
|
+
this._pending.set(id, { resolve, reject, timer });
|
|
86
|
+
|
|
87
|
+
if (window.ChatableXBridge) {
|
|
88
|
+
window.ChatableXBridge.postMessage(JSON.stringify(message));
|
|
89
|
+
} else {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
this._pending.delete(id);
|
|
92
|
+
reject(new Error('ChatableXBridge not available'));
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private _handleResponse(data: { id: string; success: boolean; data?: unknown; error?: string }): void {
|
|
98
|
+
const pending = this._pending.get(data.id);
|
|
99
|
+
if (!pending) return;
|
|
100
|
+
this._pending.delete(data.id);
|
|
101
|
+
clearTimeout(pending.timer);
|
|
102
|
+
if (data.success) {
|
|
103
|
+
pending.resolve(data.data);
|
|
104
|
+
} else {
|
|
105
|
+
pending.reject(new Error(data.error ?? 'Unknown error'));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// -------------------------------------------------------------------------
|
|
110
|
+
// Events
|
|
111
|
+
// -------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
private _handleEvent(data: { eventType: string; data: unknown }): void {
|
|
114
|
+
const handlers = this._listeners.get(data.eventType);
|
|
115
|
+
if (handlers) {
|
|
116
|
+
for (const fn of handlers) {
|
|
117
|
+
try { fn(data.data); } catch (e) { console.error('[ChatableX] event handler error:', e); }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
addEventListener(eventType: string, handler: EventHandler): () => void {
|
|
123
|
+
if (!this._listeners.has(eventType)) {
|
|
124
|
+
this._listeners.set(eventType, new Set());
|
|
125
|
+
}
|
|
126
|
+
this._listeners.get(eventType)!.add(handler);
|
|
127
|
+
return () => {
|
|
128
|
+
const set = this._listeners.get(eventType);
|
|
129
|
+
if (set) {
|
|
130
|
+
set.delete(handler);
|
|
131
|
+
if (set.size === 0) this._listeners.delete(eventType);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Dispatch a synthetic event (used internally). */
|
|
137
|
+
dispatchEvent(eventType: string, data: unknown): void {
|
|
138
|
+
this._handleEvent({ eventType, data });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// -------------------------------------------------------------------------
|
|
142
|
+
// Internals
|
|
143
|
+
// -------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
private _nextId(): string {
|
|
146
|
+
return `ctx_${++this._msgId}_${Date.now()}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private _log(...args: unknown[]): void {
|
|
150
|
+
if (this._debug) console.log('[ChatableX Bridge]', ...args);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
destroy(): void {
|
|
154
|
+
for (const [, p] of this._pending) {
|
|
155
|
+
clearTimeout(p.timer);
|
|
156
|
+
p.reject(new Error('Bridge destroyed'));
|
|
157
|
+
}
|
|
158
|
+
this._pending.clear();
|
|
159
|
+
this._listeners.clear();
|
|
160
|
+
window.ChatableXReceive = undefined;
|
|
161
|
+
}
|
|
162
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chatablex-web-sdk
|
|
3
|
+
*
|
|
4
|
+
* Runtime SDK for ChatableX AI App (WebUI) development.
|
|
5
|
+
* Developers install this package and call `ChatableX.init()` to connect
|
|
6
|
+
* their web app to the ChatableX Flutter host.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { ChatableX } from 'chatablex-web-sdk';
|
|
11
|
+
*
|
|
12
|
+
* const sdk = await ChatableX.init({ appId: 'counter-app' });
|
|
13
|
+
*
|
|
14
|
+
* sdk.tool.onExecute(async (params) => {
|
|
15
|
+
* // handle LLM-driven tool calls
|
|
16
|
+
* return { success: true, data: 'done' };
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { Bridge } from './bridge';
|
|
22
|
+
import { createToolModule } from './modules/tool';
|
|
23
|
+
import { createEventsModule } from './modules/events';
|
|
24
|
+
import { createAIModule } from './modules/ai';
|
|
25
|
+
import { createUIModule } from './modules/ui';
|
|
26
|
+
import { createStorageModule } from './modules/storage';
|
|
27
|
+
import { createToolsModule } from './modules/tools';
|
|
28
|
+
import { createSkillsModule } from './modules/skills';
|
|
29
|
+
import type { ChatableXSDK, ChatableXInitConfig, ToolInfo } from './types';
|
|
30
|
+
|
|
31
|
+
export const SDK_VERSION = '1.0.0';
|
|
32
|
+
|
|
33
|
+
let _instance: ChatableXSDK | null = null;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Main entry point. Provides `ChatableX.init()` to bootstrap the SDK.
|
|
37
|
+
*/
|
|
38
|
+
export const ChatableX = {
|
|
39
|
+
/**
|
|
40
|
+
* Initialize the SDK and establish the bridge with the Flutter host.
|
|
41
|
+
*
|
|
42
|
+
* 1. Sets up `window.ChatableXReceive` (Flutter → JS message handler).
|
|
43
|
+
* 2. Waits for `window.ChatableXBridge` (Flutter's JavaScriptChannel).
|
|
44
|
+
* 3. Sends `sdk_init` handshake and receives tool config from Flutter.
|
|
45
|
+
* 4. Returns the fully-initialised SDK instance.
|
|
46
|
+
*/
|
|
47
|
+
async init(config: ChatableXInitConfig): Promise<ChatableXSDK> {
|
|
48
|
+
if (_instance) return _instance;
|
|
49
|
+
|
|
50
|
+
const debug = config.debug ?? false;
|
|
51
|
+
const timeout = config.timeout ?? 10_000;
|
|
52
|
+
const bridge = new Bridge(debug);
|
|
53
|
+
|
|
54
|
+
// 1. Install the global receiver first
|
|
55
|
+
bridge.install();
|
|
56
|
+
|
|
57
|
+
// 2. Wait for Flutter to set up the channel
|
|
58
|
+
await bridge.waitForBridge(timeout);
|
|
59
|
+
|
|
60
|
+
if (debug) console.log('[ChatableX] Bridge connected, sending sdk_init');
|
|
61
|
+
|
|
62
|
+
// 3. Handshake — tell Flutter we're ready and get tool config back
|
|
63
|
+
let toolConfig: Partial<ToolInfo> = {};
|
|
64
|
+
try {
|
|
65
|
+
const resp = await bridge.sendMessage('sdk_init', {
|
|
66
|
+
appId: config.appId,
|
|
67
|
+
sdkVersion: SDK_VERSION,
|
|
68
|
+
});
|
|
69
|
+
if (resp && typeof resp === 'object') {
|
|
70
|
+
toolConfig = resp as Partial<ToolInfo>;
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
if (debug) console.warn('[ChatableX] sdk_init handshake failed, continuing with defaults');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 4. Create modules
|
|
77
|
+
const toolModule = createToolModule(bridge, config.appId);
|
|
78
|
+
if (toolConfig) toolModule._setInfo(toolConfig);
|
|
79
|
+
|
|
80
|
+
const sdk: ChatableXSDK = {
|
|
81
|
+
ai: createAIModule(bridge),
|
|
82
|
+
tools: createToolsModule(bridge),
|
|
83
|
+
skills: createSkillsModule(bridge),
|
|
84
|
+
ui: createUIModule(bridge),
|
|
85
|
+
events: createEventsModule(bridge),
|
|
86
|
+
storage: createStorageModule(bridge),
|
|
87
|
+
tool: toolModule,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Expose on window for debugging / Flutter interop
|
|
91
|
+
window.ChatableX = sdk;
|
|
92
|
+
|
|
93
|
+
_instance = sdk;
|
|
94
|
+
if (debug) console.log(`[ChatableX] SDK v${SDK_VERSION} ready for: ${config.appId}`);
|
|
95
|
+
return sdk;
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
/** Get the current SDK instance (throws if not initialised). */
|
|
99
|
+
getInstance(): ChatableXSDK {
|
|
100
|
+
if (!_instance) throw new Error('ChatableX SDK not initialised. Call ChatableX.init() first.');
|
|
101
|
+
return _instance;
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
/** Check whether the SDK has been initialised. */
|
|
105
|
+
isReady(): boolean {
|
|
106
|
+
return _instance !== null;
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
/** SDK version */
|
|
110
|
+
version: SDK_VERSION,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Re-export all types
|
|
114
|
+
export * from './types';
|
|
115
|
+
export { Bridge } from './bridge';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Bridge } from '../bridge';
|
|
2
|
+
import type { ChatOptions, ChatResponse, SessionContext, ChatableXAI } from '../types';
|
|
3
|
+
|
|
4
|
+
export function createAIModule(bridge: Bridge): ChatableXAI {
|
|
5
|
+
return {
|
|
6
|
+
chat(message: string, options?: ChatOptions): Promise<ChatResponse> {
|
|
7
|
+
return bridge.sendMessage('ai.chat', { message, ...options }) as Promise<ChatResponse>;
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
chatStream(message: string, options?: ChatOptions): Promise<unknown> {
|
|
11
|
+
return bridge.sendMessage('ai.chatStream', { message, ...options });
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
getContext(): Promise<SessionContext> {
|
|
15
|
+
return bridge.sendMessage('ai.getContext', {}) as Promise<SessionContext>;
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Bridge } from '../bridge';
|
|
2
|
+
import type { EventType, EventCallbackMap, Unsubscribe, ChatableXEvents } from '../types';
|
|
3
|
+
|
|
4
|
+
export function createEventsModule(bridge: Bridge): ChatableXEvents {
|
|
5
|
+
return {
|
|
6
|
+
on<T extends EventType>(eventType: T, callback: EventCallbackMap[T]): Unsubscribe {
|
|
7
|
+
bridge.sendMessage('events.subscribe', { eventType }).catch(() => {});
|
|
8
|
+
return bridge.addEventListener(eventType, callback as (data: unknown) => void);
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
onAiResponse(callback) {
|
|
12
|
+
return this.on('aiResponse', callback);
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
onToolExecution(callback) {
|
|
16
|
+
return this.on('toolExecution', callback);
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
onUserMessage(callback) {
|
|
20
|
+
return this.on('userMessage', callback);
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|