@elizaos/capacitor-agent 1.0.0 → 2.0.11-beta.7
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/ElizaosCapacitorAgent.podspec +17 -0
- package/LICENSE +21 -0
- package/README.md +145 -0
- package/android/build.gradle +44 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/ai/eliza/plugins/agent/AgentPlugin.kt +308 -0
- package/dist/esm/definitions.d.ts +87 -2
- package/dist/esm/definitions.d.ts.map +1 -1
- package/dist/esm/definitions.js +4 -1
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +5 -2
- package/dist/esm/web.d.ts.map +1 -1
- package/dist/esm/web.js +108 -2
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +109 -3
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +109 -3
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/AgentPlugin/AgentPlugin.swift +917 -0
- package/package.json +20 -10
|
@@ -0,0 +1,917 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
import WebKit
|
|
4
|
+
|
|
5
|
+
private let maxRequestBodyBytes = 10 * 1024 * 1024
|
|
6
|
+
private let maxResponseBodyBytes = 10 * 1024 * 1024
|
|
7
|
+
private let localAgentPort = 31337
|
|
8
|
+
private let localAgentIpcScheme = "eliza-local-agent"
|
|
9
|
+
private let localAgentIpcHost = "ipc"
|
|
10
|
+
|
|
11
|
+
private struct AgentEndpoint {
|
|
12
|
+
let baseURL: URL
|
|
13
|
+
let token: String?
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private struct AgentHTTPResponse {
|
|
17
|
+
let status: Int
|
|
18
|
+
let statusText: String
|
|
19
|
+
let headers: [String: String]
|
|
20
|
+
let body: String
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// Eliza Agent Plugin — iOS bridge.
|
|
24
|
+
///
|
|
25
|
+
/// Remote/cloud modes bridge the Capacitor Agent API to an explicitly
|
|
26
|
+
/// configured HTTP agent endpoint, such as a local Mac dev server or a remote
|
|
27
|
+
/// Eliza agent. Local dev/sideload mode uses a path-only in-app identity; full
|
|
28
|
+
/// Bun foreground traffic goes through the ElizaBunRuntime Capacitor bridge,
|
|
29
|
+
/// while this plugin keeps the foreground WebView ITTP route kernel as a
|
|
30
|
+
/// compatibility path. It never starts an iOS local TCP listener.
|
|
31
|
+
@objc(AgentPlugin)
|
|
32
|
+
public class AgentPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
33
|
+
public let identifier = "AgentPlugin"
|
|
34
|
+
public let jsName = "Agent"
|
|
35
|
+
public let pluginMethods: [CAPPluginMethod] = [
|
|
36
|
+
CAPPluginMethod(name: "start", returnType: CAPPluginReturnPromise),
|
|
37
|
+
CAPPluginMethod(name: "stop", returnType: CAPPluginReturnPromise),
|
|
38
|
+
CAPPluginMethod(name: "getStatus", returnType: CAPPluginReturnPromise),
|
|
39
|
+
CAPPluginMethod(name: "chat", returnType: CAPPluginReturnPromise),
|
|
40
|
+
CAPPluginMethod(name: "getLocalAgentToken", returnType: CAPPluginReturnPromise),
|
|
41
|
+
CAPPluginMethod(name: "request", returnType: CAPPluginReturnPromise),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
private static var conversationIdByBaseURL: [String: String] = [:]
|
|
45
|
+
private static var localStartedAt: Date?
|
|
46
|
+
private static let apiBaseConfigKeys = [
|
|
47
|
+
"apiBase",
|
|
48
|
+
"baseUrl",
|
|
49
|
+
"baseURL",
|
|
50
|
+
"agentApiBase",
|
|
51
|
+
"ELIZA_AGENT_API_BASE",
|
|
52
|
+
"ELIZA_API_BASE",
|
|
53
|
+
"ELIZA_IOS_API_BASE",
|
|
54
|
+
"ELIZA_IOS_REMOTE_API_BASE",
|
|
55
|
+
"ELIZA_MOBILE_API_BASE",
|
|
56
|
+
"VITE_ELIZA_IOS_API_BASE",
|
|
57
|
+
"VITE_ELIZA_MOBILE_API_BASE",
|
|
58
|
+
"VITE_ELIZA_IOS_API_BASE",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
@objc func start(_ call: CAPPluginCall) {
|
|
62
|
+
if isLocalAgentMode(call: call) {
|
|
63
|
+
Self.localStartedAt = Self.localStartedAt ?? Date()
|
|
64
|
+
call.resolve(localAgentStatus(state: "running", error: nil))
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
guard let endpoint = resolveEndpoint(call: call) else {
|
|
69
|
+
call.reject(missingEndpointMessage())
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
sendJSON(endpoint: endpoint, path: "/api/agent/start", method: "POST", timeoutMs: timeoutMs(from: call)) { result in
|
|
74
|
+
switch result {
|
|
75
|
+
case .success(let response):
|
|
76
|
+
guard self.isHTTPSuccess(response.status) else {
|
|
77
|
+
call.reject(self.httpErrorMessage(prefix: "Agent start failed", response: response))
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
let payload = self.parseJSONObject(response.body)
|
|
81
|
+
let statusPayload = (payload?["status"] as? JSObject) ?? payload ?? [:]
|
|
82
|
+
call.resolve(self.normalizedStatus(statusPayload, fallbackState: "running", endpoint: endpoint, error: nil))
|
|
83
|
+
case .failure(let error):
|
|
84
|
+
call.reject("Agent start failed: \(error.localizedDescription)")
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@objc func stop(_ call: CAPPluginCall) {
|
|
90
|
+
if isLocalAgentMode(call: call) {
|
|
91
|
+
Self.localStartedAt = nil
|
|
92
|
+
call.resolve(["ok": true])
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
guard let endpoint = resolveEndpoint(call: call) else {
|
|
97
|
+
call.reject(missingEndpointMessage())
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
sendJSON(endpoint: endpoint, path: "/api/agent/stop", method: "POST", timeoutMs: timeoutMs(from: call)) { result in
|
|
102
|
+
switch result {
|
|
103
|
+
case .success(let response):
|
|
104
|
+
guard self.isHTTPSuccess(response.status) else {
|
|
105
|
+
call.reject(self.httpErrorMessage(prefix: "Agent stop failed", response: response))
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
let payload = self.parseJSONObject(response.body)
|
|
109
|
+
let ok = (payload?["ok"] as? Bool) ?? true
|
|
110
|
+
call.resolve(["ok": ok])
|
|
111
|
+
case .failure(let error):
|
|
112
|
+
call.reject("Agent stop failed: \(error.localizedDescription)")
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@objc func getStatus(_ call: CAPPluginCall) {
|
|
118
|
+
if isLocalAgentMode(call: call) {
|
|
119
|
+
Self.localStartedAt = Self.localStartedAt ?? Date()
|
|
120
|
+
call.resolve(localAgentStatus(state: "running", error: nil))
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
guard let endpoint = resolveEndpoint(call: call) else {
|
|
125
|
+
call.resolve(status(state: "error", agentName: nil, port: nil, startedAt: nil, error: missingEndpointMessage()))
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
sendJSON(endpoint: endpoint, path: "/api/status", method: "GET", timeoutMs: timeoutMs(from: call, defaultValue: 1_500)) { result in
|
|
130
|
+
switch result {
|
|
131
|
+
case .success(let response):
|
|
132
|
+
guard self.isHTTPSuccess(response.status) else {
|
|
133
|
+
call.resolve(self.status(
|
|
134
|
+
state: "error",
|
|
135
|
+
agentName: nil,
|
|
136
|
+
port: self.port(from: endpoint.baseURL),
|
|
137
|
+
startedAt: nil,
|
|
138
|
+
error: self.httpErrorMessage(prefix: "Agent status unavailable", response: response)
|
|
139
|
+
))
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
let payload = self.parseJSONObject(response.body) ?? [:]
|
|
143
|
+
call.resolve(self.normalizedStatus(payload, fallbackState: "running", endpoint: endpoint, error: nil))
|
|
144
|
+
case .failure(let error):
|
|
145
|
+
call.resolve(self.status(
|
|
146
|
+
state: "error",
|
|
147
|
+
agentName: nil,
|
|
148
|
+
port: self.port(from: endpoint.baseURL),
|
|
149
|
+
startedAt: nil,
|
|
150
|
+
error: "Agent status unavailable: \(error.localizedDescription)"
|
|
151
|
+
))
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@objc func chat(_ call: CAPPluginCall) {
|
|
157
|
+
guard let text = call.getString("text")?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty else {
|
|
158
|
+
call.reject("Agent.chat requires non-empty text")
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
if isLocalAgentMode(call: call) {
|
|
162
|
+
let timeout = timeoutMs(from: call)
|
|
163
|
+
ensureLocalConversation(timeoutMs: timeout) { conversationResult in
|
|
164
|
+
switch conversationResult {
|
|
165
|
+
case .success(let conversationId):
|
|
166
|
+
self.sendLocalChatMessage(conversationId: conversationId, text: text, timeoutMs: timeout, retryOnMissingConversation: true) { result in
|
|
167
|
+
switch result {
|
|
168
|
+
case .success(let payload):
|
|
169
|
+
call.resolve(payload)
|
|
170
|
+
case .failure(let error):
|
|
171
|
+
call.reject(error.localizedDescription)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
case .failure(let error):
|
|
175
|
+
call.reject(error.localizedDescription)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
guard let endpoint = resolveEndpoint(call: call) else {
|
|
181
|
+
call.reject(missingEndpointMessage())
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
ensureConversation(endpoint: endpoint, timeoutMs: timeoutMs(from: call)) { conversationResult in
|
|
186
|
+
switch conversationResult {
|
|
187
|
+
case .success(let conversationId):
|
|
188
|
+
self.sendChatMessage(endpoint: endpoint, conversationId: conversationId, text: text, timeoutMs: self.timeoutMs(from: call), retryOnMissingConversation: true) { result in
|
|
189
|
+
switch result {
|
|
190
|
+
case .success(let payload):
|
|
191
|
+
call.resolve(payload)
|
|
192
|
+
case .failure(let error):
|
|
193
|
+
call.reject(error.localizedDescription)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
case .failure(let error):
|
|
197
|
+
call.reject(error.localizedDescription)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
@objc func getLocalAgentToken(_ call: CAPPluginCall) {
|
|
203
|
+
if isLocalAgentMode(call: call) {
|
|
204
|
+
call.resolve([
|
|
205
|
+
"available": false,
|
|
206
|
+
"token": NSNull(),
|
|
207
|
+
])
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let token = resolveEndpoint(call: call)?.token
|
|
212
|
+
call.resolve([
|
|
213
|
+
"available": token != nil,
|
|
214
|
+
"token": token ?? NSNull(),
|
|
215
|
+
])
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
@objc func request(_ call: CAPPluginCall) {
|
|
219
|
+
guard let path = call.getString("path")?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
220
|
+
isSafeLocalPath(path) else {
|
|
221
|
+
call.reject("Agent.request requires a local path that starts with / and is not an absolute URL")
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
let method = (call.getString("method") ?? "GET").trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
|
|
225
|
+
guard method.range(of: "^[A-Z]{1,16}$", options: .regularExpression) != nil else {
|
|
226
|
+
call.reject("Unsupported HTTP method")
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
let body = call.getString("body")
|
|
230
|
+
if let bodyBytes = body?.data(using: .utf8), bodyBytes.count > maxRequestBodyBytes {
|
|
231
|
+
call.reject("Request body is too large")
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let headers = call.getObject("headers") ?? [:]
|
|
236
|
+
if isLocalAgentMode(call: call) {
|
|
237
|
+
sendLocalIttpRequest(
|
|
238
|
+
path: path,
|
|
239
|
+
method: method,
|
|
240
|
+
headers: headers,
|
|
241
|
+
body: body,
|
|
242
|
+
timeoutMs: timeoutMs(from: call)
|
|
243
|
+
) { result in
|
|
244
|
+
switch result {
|
|
245
|
+
case .success(let response):
|
|
246
|
+
call.resolve(self.agentHTTPResponseObject(response))
|
|
247
|
+
case .failure(let error):
|
|
248
|
+
call.reject("iOS local agent request failed: \(error.localizedDescription)")
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
guard let endpoint = resolveEndpoint(call: call) else {
|
|
255
|
+
call.reject(missingEndpointMessage())
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
sendJSON(
|
|
259
|
+
endpoint: endpoint,
|
|
260
|
+
path: path,
|
|
261
|
+
method: method,
|
|
262
|
+
headers: headers,
|
|
263
|
+
body: body,
|
|
264
|
+
timeoutMs: timeoutMs(from: call)
|
|
265
|
+
) { result in
|
|
266
|
+
switch result {
|
|
267
|
+
case .success(let response):
|
|
268
|
+
call.resolve([
|
|
269
|
+
"status": response.status,
|
|
270
|
+
"statusText": response.statusText,
|
|
271
|
+
"headers": response.headers,
|
|
272
|
+
"body": response.body,
|
|
273
|
+
])
|
|
274
|
+
case .failure(let error):
|
|
275
|
+
call.reject("Local agent request failed: \(error.localizedDescription)")
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private func ensureLocalConversation(
|
|
281
|
+
timeoutMs: Int,
|
|
282
|
+
completion: @escaping (Result<String, Error>) -> Void
|
|
283
|
+
) {
|
|
284
|
+
let baseKey = "ios-local-ittp"
|
|
285
|
+
if let existing = Self.conversationIdByBaseURL[baseKey], !existing.isEmpty {
|
|
286
|
+
completion(.success(existing))
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
sendLocalIttpRequest(
|
|
291
|
+
path: "/api/conversations",
|
|
292
|
+
method: "POST",
|
|
293
|
+
headers: ["Content-Type": "application/json"],
|
|
294
|
+
body: "{\"title\":\"Quick Chat\"}",
|
|
295
|
+
timeoutMs: timeoutMs
|
|
296
|
+
) { result in
|
|
297
|
+
switch result {
|
|
298
|
+
case .success(let response):
|
|
299
|
+
guard self.isHTTPSuccess(response.status) else {
|
|
300
|
+
completion(.failure(self.pluginError(self.httpErrorMessage(prefix: "Failed to create local conversation", response: response))))
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
guard let payload = self.parseJSONObject(response.body),
|
|
304
|
+
let conversation = payload["conversation"] as? JSObject,
|
|
305
|
+
let id = conversation["id"] as? String,
|
|
306
|
+
!id.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
307
|
+
completion(.failure(self.pluginError("Local conversation create response missing id")))
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
Self.conversationIdByBaseURL[baseKey] = id
|
|
311
|
+
completion(.success(id))
|
|
312
|
+
case .failure(let error):
|
|
313
|
+
completion(.failure(self.pluginError("Failed to create local conversation: \(error.localizedDescription)")))
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private func sendLocalChatMessage(
|
|
319
|
+
conversationId: String,
|
|
320
|
+
text: String,
|
|
321
|
+
timeoutMs: Int,
|
|
322
|
+
retryOnMissingConversation: Bool,
|
|
323
|
+
completion: @escaping (Result<JSObject, Error>) -> Void
|
|
324
|
+
) {
|
|
325
|
+
let path = "/api/conversations/\(urlEncode(conversationId))/messages"
|
|
326
|
+
let bodyObject: JSObject = [
|
|
327
|
+
"text": text,
|
|
328
|
+
"channelType": "DM",
|
|
329
|
+
]
|
|
330
|
+
guard let bodyData = try? JSONSerialization.data(withJSONObject: bodyObject, options: []),
|
|
331
|
+
let body = String(data: bodyData, encoding: .utf8) else {
|
|
332
|
+
completion(.failure(pluginError("Failed to encode local chat request")))
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
sendLocalIttpRequest(
|
|
337
|
+
path: path,
|
|
338
|
+
method: "POST",
|
|
339
|
+
headers: ["Content-Type": "application/json"],
|
|
340
|
+
body: body,
|
|
341
|
+
timeoutMs: timeoutMs
|
|
342
|
+
) { result in
|
|
343
|
+
switch result {
|
|
344
|
+
case .success(let response):
|
|
345
|
+
if response.status == 404 && retryOnMissingConversation {
|
|
346
|
+
Self.conversationIdByBaseURL.removeValue(forKey: "ios-local-ittp")
|
|
347
|
+
self.ensureLocalConversation(timeoutMs: timeoutMs) { nextConversation in
|
|
348
|
+
switch nextConversation {
|
|
349
|
+
case .success(let nextId):
|
|
350
|
+
self.sendLocalChatMessage(
|
|
351
|
+
conversationId: nextId,
|
|
352
|
+
text: text,
|
|
353
|
+
timeoutMs: timeoutMs,
|
|
354
|
+
retryOnMissingConversation: false,
|
|
355
|
+
completion: completion
|
|
356
|
+
)
|
|
357
|
+
case .failure(let error):
|
|
358
|
+
completion(.failure(error))
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
guard self.isHTTPSuccess(response.status) else {
|
|
364
|
+
completion(.failure(self.pluginError(self.httpErrorMessage(prefix: "Local chat request failed", response: response))))
|
|
365
|
+
return
|
|
366
|
+
}
|
|
367
|
+
let payload = self.parseJSONObject(response.body) ?? [:]
|
|
368
|
+
let responseText = (payload["text"] as? String) ?? ""
|
|
369
|
+
let agentName = (payload["agentName"] as? String) ?? "Agent"
|
|
370
|
+
completion(.success([
|
|
371
|
+
"text": responseText,
|
|
372
|
+
"agentName": agentName,
|
|
373
|
+
]))
|
|
374
|
+
case .failure(let error):
|
|
375
|
+
completion(.failure(self.pluginError("Local chat request failed: \(error.localizedDescription)")))
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private func sendLocalIttpRequest(
|
|
381
|
+
path: String,
|
|
382
|
+
method: String,
|
|
383
|
+
headers: JSObject = [:],
|
|
384
|
+
body: String? = nil,
|
|
385
|
+
timeoutMs: Int,
|
|
386
|
+
completion: @escaping (Result<AgentHTTPResponse, Error>) -> Void
|
|
387
|
+
) {
|
|
388
|
+
guard let webView = bridge?.webView else {
|
|
389
|
+
completion(.success(localIttpUnavailableResponse()))
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
let payload: JSObject = [
|
|
394
|
+
"path": path,
|
|
395
|
+
"method": method,
|
|
396
|
+
"headers": headers,
|
|
397
|
+
"body": body ?? NSNull(),
|
|
398
|
+
"timeoutMs": timeoutMs,
|
|
399
|
+
]
|
|
400
|
+
|
|
401
|
+
let source = """
|
|
402
|
+
const handler = window.__ELIZA_IOS_LOCAL_AGENT_REQUEST__;
|
|
403
|
+
if (typeof handler !== "function") {
|
|
404
|
+
return {
|
|
405
|
+
status: 503,
|
|
406
|
+
statusText: "Service Unavailable",
|
|
407
|
+
headers: { "content-type": "application/json" },
|
|
408
|
+
body: JSON.stringify({
|
|
409
|
+
ok: false,
|
|
410
|
+
error: "ios_ittp_handler_unavailable",
|
|
411
|
+
reason: "The WebView ITTP local-agent request bridge is not installed yet."
|
|
412
|
+
})
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
return await handler(options);
|
|
416
|
+
"""
|
|
417
|
+
|
|
418
|
+
if #available(iOS 14.0, *) {
|
|
419
|
+
DispatchQueue.main.async {
|
|
420
|
+
Task { @MainActor in
|
|
421
|
+
do {
|
|
422
|
+
let value = try await webView.callAsyncJavaScript(
|
|
423
|
+
source,
|
|
424
|
+
arguments: ["options": payload],
|
|
425
|
+
in: nil,
|
|
426
|
+
contentWorld: .page
|
|
427
|
+
)
|
|
428
|
+
completion(self.parseAgentHTTPResponse(value))
|
|
429
|
+
} catch {
|
|
430
|
+
completion(.failure(error))
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
} else {
|
|
435
|
+
completion(.success(localIttpUnavailableResponse()))
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private func ensureConversation(
|
|
440
|
+
endpoint: AgentEndpoint,
|
|
441
|
+
timeoutMs: Int,
|
|
442
|
+
completion: @escaping (Result<String, Error>) -> Void
|
|
443
|
+
) {
|
|
444
|
+
let baseKey = endpoint.baseURL.absoluteString
|
|
445
|
+
if let existing = Self.conversationIdByBaseURL[baseKey], !existing.isEmpty {
|
|
446
|
+
completion(.success(existing))
|
|
447
|
+
return
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
sendJSON(
|
|
451
|
+
endpoint: endpoint,
|
|
452
|
+
path: "/api/conversations",
|
|
453
|
+
method: "POST",
|
|
454
|
+
headers: ["Content-Type": "application/json"],
|
|
455
|
+
body: "{\"title\":\"Quick Chat\"}",
|
|
456
|
+
timeoutMs: timeoutMs
|
|
457
|
+
) { result in
|
|
458
|
+
switch result {
|
|
459
|
+
case .success(let response):
|
|
460
|
+
guard self.isHTTPSuccess(response.status) else {
|
|
461
|
+
completion(.failure(self.pluginError(self.httpErrorMessage(prefix: "Failed to create conversation", response: response))))
|
|
462
|
+
return
|
|
463
|
+
}
|
|
464
|
+
guard let payload = self.parseJSONObject(response.body),
|
|
465
|
+
let conversation = payload["conversation"] as? JSObject,
|
|
466
|
+
let id = conversation["id"] as? String,
|
|
467
|
+
!id.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
468
|
+
completion(.failure(self.pluginError("Conversation create response missing id")))
|
|
469
|
+
return
|
|
470
|
+
}
|
|
471
|
+
Self.conversationIdByBaseURL[baseKey] = id
|
|
472
|
+
completion(.success(id))
|
|
473
|
+
case .failure(let error):
|
|
474
|
+
completion(.failure(self.pluginError("Failed to create conversation: \(error.localizedDescription)")))
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
private func sendChatMessage(
|
|
480
|
+
endpoint: AgentEndpoint,
|
|
481
|
+
conversationId: String,
|
|
482
|
+
text: String,
|
|
483
|
+
timeoutMs: Int,
|
|
484
|
+
retryOnMissingConversation: Bool,
|
|
485
|
+
completion: @escaping (Result<JSObject, Error>) -> Void
|
|
486
|
+
) {
|
|
487
|
+
let path = "/api/conversations/\(urlEncode(conversationId))/messages"
|
|
488
|
+
let bodyObject: JSObject = [
|
|
489
|
+
"text": text,
|
|
490
|
+
"channelType": "DM",
|
|
491
|
+
]
|
|
492
|
+
guard let bodyData = try? JSONSerialization.data(withJSONObject: bodyObject, options: []),
|
|
493
|
+
let body = String(data: bodyData, encoding: .utf8) else {
|
|
494
|
+
completion(.failure(pluginError("Failed to encode chat request")))
|
|
495
|
+
return
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
sendJSON(
|
|
499
|
+
endpoint: endpoint,
|
|
500
|
+
path: path,
|
|
501
|
+
method: "POST",
|
|
502
|
+
headers: ["Content-Type": "application/json"],
|
|
503
|
+
body: body,
|
|
504
|
+
timeoutMs: timeoutMs
|
|
505
|
+
) { result in
|
|
506
|
+
switch result {
|
|
507
|
+
case .success(let response):
|
|
508
|
+
if response.status == 404 && retryOnMissingConversation {
|
|
509
|
+
Self.conversationIdByBaseURL.removeValue(forKey: endpoint.baseURL.absoluteString)
|
|
510
|
+
self.ensureConversation(endpoint: endpoint, timeoutMs: timeoutMs) { nextConversation in
|
|
511
|
+
switch nextConversation {
|
|
512
|
+
case .success(let nextId):
|
|
513
|
+
self.sendChatMessage(
|
|
514
|
+
endpoint: endpoint,
|
|
515
|
+
conversationId: nextId,
|
|
516
|
+
text: text,
|
|
517
|
+
timeoutMs: timeoutMs,
|
|
518
|
+
retryOnMissingConversation: false,
|
|
519
|
+
completion: completion
|
|
520
|
+
)
|
|
521
|
+
case .failure(let error):
|
|
522
|
+
completion(.failure(error))
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return
|
|
526
|
+
}
|
|
527
|
+
guard self.isHTTPSuccess(response.status) else {
|
|
528
|
+
completion(.failure(self.pluginError(self.httpErrorMessage(prefix: "Chat request failed", response: response))))
|
|
529
|
+
return
|
|
530
|
+
}
|
|
531
|
+
let payload = self.parseJSONObject(response.body) ?? [:]
|
|
532
|
+
let responseText = (payload["text"] as? String) ?? ""
|
|
533
|
+
let agentName = (payload["agentName"] as? String) ?? "Agent"
|
|
534
|
+
completion(.success([
|
|
535
|
+
"text": responseText,
|
|
536
|
+
"agentName": agentName,
|
|
537
|
+
]))
|
|
538
|
+
case .failure(let error):
|
|
539
|
+
completion(.failure(self.pluginError("Chat request failed: \(error.localizedDescription)")))
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private func sendJSON(
|
|
545
|
+
endpoint: AgentEndpoint,
|
|
546
|
+
path: String,
|
|
547
|
+
method: String,
|
|
548
|
+
headers: JSObject = [:],
|
|
549
|
+
body: String? = nil,
|
|
550
|
+
timeoutMs: Int,
|
|
551
|
+
completion: @escaping (Result<AgentHTTPResponse, Error>) -> Void
|
|
552
|
+
) {
|
|
553
|
+
guard let url = URL(string: path, relativeTo: endpoint.baseURL)?.absoluteURL else {
|
|
554
|
+
completion(.failure(pluginError("Invalid local agent request path")))
|
|
555
|
+
return
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
var request = URLRequest(url: url)
|
|
559
|
+
request.httpMethod = method
|
|
560
|
+
request.timeoutInterval = TimeInterval(timeoutMs) / 1_000
|
|
561
|
+
request.cachePolicy = .reloadIgnoringLocalCacheData
|
|
562
|
+
|
|
563
|
+
for (key, value) in headers {
|
|
564
|
+
let normalizedKey = key.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
565
|
+
guard !normalizedKey.isEmpty,
|
|
566
|
+
!isBlockedHeader(normalizedKey),
|
|
567
|
+
let stringValue = value as? String,
|
|
568
|
+
!stringValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
569
|
+
continue
|
|
570
|
+
}
|
|
571
|
+
request.setValue(stringValue, forHTTPHeaderField: normalizedKey)
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if let token = endpoint.token, request.value(forHTTPHeaderField: "Authorization") == nil {
|
|
575
|
+
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if let body = body, method != "GET", method != "HEAD" {
|
|
579
|
+
let bodyData = body.data(using: .utf8) ?? Data()
|
|
580
|
+
if bodyData.count > maxRequestBodyBytes {
|
|
581
|
+
completion(.failure(pluginError("Request body is too large")))
|
|
582
|
+
return
|
|
583
|
+
}
|
|
584
|
+
request.httpBody = bodyData
|
|
585
|
+
if request.value(forHTTPHeaderField: "Content-Type") == nil {
|
|
586
|
+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
URLSession.shared.dataTask(with: request) { data, response, error in
|
|
591
|
+
if let error = error {
|
|
592
|
+
completion(.failure(error))
|
|
593
|
+
return
|
|
594
|
+
}
|
|
595
|
+
guard let httpResponse = response as? HTTPURLResponse else {
|
|
596
|
+
completion(.failure(self.pluginError("Agent endpoint returned a non-HTTP response")))
|
|
597
|
+
return
|
|
598
|
+
}
|
|
599
|
+
let responseData = data ?? Data()
|
|
600
|
+
if responseData.count > maxResponseBodyBytes {
|
|
601
|
+
completion(.failure(self.pluginError("Response body is too large")))
|
|
602
|
+
return
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
var responseHeaders: [String: String] = [:]
|
|
606
|
+
for (key, value) in httpResponse.allHeaderFields {
|
|
607
|
+
guard let headerKey = key as? String else { continue }
|
|
608
|
+
responseHeaders[headerKey.lowercased()] = String(describing: value)
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
completion(.success(AgentHTTPResponse(
|
|
612
|
+
status: httpResponse.statusCode,
|
|
613
|
+
statusText: HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode),
|
|
614
|
+
headers: responseHeaders,
|
|
615
|
+
body: String(data: responseData, encoding: .utf8) ?? ""
|
|
616
|
+
)))
|
|
617
|
+
}.resume()
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
private func resolveEndpoint(call: CAPPluginCall? = nil) -> AgentEndpoint? {
|
|
621
|
+
guard let rawBaseURL = readConfiguredString(
|
|
622
|
+
call: call,
|
|
623
|
+
keys: Self.apiBaseConfigKeys
|
|
624
|
+
) else {
|
|
625
|
+
return nil
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
let trimmed = rawBaseURL.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
629
|
+
.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
|
630
|
+
guard let baseURL = URL(string: trimmed),
|
|
631
|
+
let scheme = baseURL.scheme?.lowercased(),
|
|
632
|
+
scheme == "http" || scheme == "https",
|
|
633
|
+
baseURL.host != nil else {
|
|
634
|
+
return nil
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
let token = readConfiguredString(
|
|
638
|
+
call: call,
|
|
639
|
+
keys: [
|
|
640
|
+
"apiToken",
|
|
641
|
+
"token",
|
|
642
|
+
"agentApiToken",
|
|
643
|
+
"ELIZA_AGENT_API_TOKEN",
|
|
644
|
+
"ELIZA_API_TOKEN",
|
|
645
|
+
"ELIZA_IOS_API_TOKEN",
|
|
646
|
+
"ELIZA_IOS_REMOTE_API_TOKEN",
|
|
647
|
+
"ELIZA_MOBILE_API_TOKEN",
|
|
648
|
+
"VITE_ELIZA_IOS_API_TOKEN",
|
|
649
|
+
"VITE_ELIZA_MOBILE_API_TOKEN",
|
|
650
|
+
"VITE_ELIZA_IOS_API_TOKEN",
|
|
651
|
+
]
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
return AgentEndpoint(baseURL: baseURL, token: token)
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
private func readConfiguredString(call: CAPPluginCall?, keys: [String]) -> String? {
|
|
658
|
+
for key in keys {
|
|
659
|
+
if let value = call?.getString(key)?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty {
|
|
660
|
+
return value
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
let pluginConfig = getConfig()
|
|
665
|
+
for key in keys {
|
|
666
|
+
if let value = pluginConfig.getString(key)?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty {
|
|
667
|
+
return value
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
for key in keys {
|
|
672
|
+
if let value = Bundle.main.object(forInfoDictionaryKey: key) as? String {
|
|
673
|
+
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
674
|
+
if !trimmed.isEmpty { return trimmed }
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
let environment = ProcessInfo.processInfo.environment
|
|
679
|
+
for key in keys {
|
|
680
|
+
if let value = environment[key]?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty {
|
|
681
|
+
return value
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
let defaults = UserDefaults.standard
|
|
686
|
+
for key in keys {
|
|
687
|
+
if let value = defaults.string(forKey: key)?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty {
|
|
688
|
+
return value
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return nil
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
private func isLocalAgentMode(call: CAPPluginCall? = nil) -> Bool {
|
|
696
|
+
if let endpoint = resolveEndpoint(call: call),
|
|
697
|
+
isLocalAgentEndpoint(endpoint.baseURL) {
|
|
698
|
+
return true
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if let rawBaseURL = readConfiguredString(
|
|
702
|
+
call: call,
|
|
703
|
+
keys: Self.apiBaseConfigKeys
|
|
704
|
+
), isLocalAgentIdentity(rawBaseURL) {
|
|
705
|
+
return true
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
guard let rawMode = readConfiguredString(
|
|
709
|
+
call: call,
|
|
710
|
+
keys: [
|
|
711
|
+
"mode",
|
|
712
|
+
"runtimeMode",
|
|
713
|
+
"agentRuntimeMode",
|
|
714
|
+
"ELIZA_IOS_RUNTIME_MODE",
|
|
715
|
+
"ELIZA_MOBILE_RUNTIME_MODE",
|
|
716
|
+
"VITE_ELIZA_IOS_RUNTIME_MODE",
|
|
717
|
+
"VITE_ELIZA_MOBILE_RUNTIME_MODE",
|
|
718
|
+
]
|
|
719
|
+
) else {
|
|
720
|
+
return false
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
switch rawMode.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
|
|
724
|
+
case "local", "ios-local", "sideload-local", "dev-local":
|
|
725
|
+
return true
|
|
726
|
+
default:
|
|
727
|
+
return false
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
private func isLocalAgentEndpoint(_ url: URL) -> Bool {
|
|
732
|
+
guard let scheme = url.scheme?.lowercased(),
|
|
733
|
+
let host = url.host?.lowercased() else { return false }
|
|
734
|
+
if scheme == localAgentIpcScheme && host == localAgentIpcHost {
|
|
735
|
+
return true
|
|
736
|
+
}
|
|
737
|
+
guard scheme == "http" else { return false }
|
|
738
|
+
return (host == "127.0.0.1" || host == "localhost") && (url.port ?? 80) == localAgentPort
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
private func isLocalAgentIdentity(_ value: String) -> Bool {
|
|
742
|
+
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
743
|
+
guard let url = URL(string: trimmed) else { return false }
|
|
744
|
+
return isLocalAgentEndpoint(url)
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
private func localAgentStatus(state: String, error: String?) -> JSObject {
|
|
748
|
+
let startedAt = Self.localStartedAt.map { $0.timeIntervalSince1970 * 1000 }
|
|
749
|
+
return status(
|
|
750
|
+
state: state,
|
|
751
|
+
agentName: "Eliza",
|
|
752
|
+
port: nil,
|
|
753
|
+
startedAt: startedAt,
|
|
754
|
+
error: error
|
|
755
|
+
)
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
private func normalizedStatus(
|
|
759
|
+
_ payload: JSObject,
|
|
760
|
+
fallbackState: String,
|
|
761
|
+
endpoint: AgentEndpoint,
|
|
762
|
+
error: String?
|
|
763
|
+
) -> JSObject {
|
|
764
|
+
let state = (payload["state"] as? String) ?? fallbackState
|
|
765
|
+
let agentName = payload["agentName"] as? String
|
|
766
|
+
let startedAt = payload["startedAt"] as? Double
|
|
767
|
+
?? (payload["startedAt"] as? NSNumber)?.doubleValue
|
|
768
|
+
?? (payload["started_at"] as? NSNumber)?.doubleValue
|
|
769
|
+
let rawError = error ?? payload["error"] as? String
|
|
770
|
+
return status(
|
|
771
|
+
state: state,
|
|
772
|
+
agentName: agentName,
|
|
773
|
+
port: (payload["port"] as? Int)
|
|
774
|
+
?? (payload["port"] as? NSNumber)?.intValue
|
|
775
|
+
?? port(from: endpoint.baseURL),
|
|
776
|
+
startedAt: startedAt,
|
|
777
|
+
error: rawError
|
|
778
|
+
)
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
private func status(
|
|
782
|
+
state: String,
|
|
783
|
+
agentName: String?,
|
|
784
|
+
port: Int?,
|
|
785
|
+
startedAt: Double?,
|
|
786
|
+
error: String?
|
|
787
|
+
) -> JSObject {
|
|
788
|
+
return [
|
|
789
|
+
"state": state,
|
|
790
|
+
"agentName": agentName ?? NSNull(),
|
|
791
|
+
"port": port ?? NSNull(),
|
|
792
|
+
"startedAt": startedAt ?? NSNull(),
|
|
793
|
+
"error": error ?? NSNull(),
|
|
794
|
+
]
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
private func timeoutMs(from call: CAPPluginCall, defaultValue: Int = 10_000) -> Int {
|
|
798
|
+
let value = call.getInt("timeoutMs") ?? defaultValue
|
|
799
|
+
return min(120_000, max(1_000, value))
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
private func port(from url: URL) -> Int? {
|
|
803
|
+
if let port = url.port { return port }
|
|
804
|
+
if url.scheme?.lowercased() == "http" { return 80 }
|
|
805
|
+
if url.scheme?.lowercased() == "https" { return 443 }
|
|
806
|
+
return nil
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
private func isSafeLocalPath(_ path: String) -> Bool {
|
|
810
|
+
if !path.hasPrefix("/") || path.hasPrefix("//") { return false }
|
|
811
|
+
if path.range(of: "^[a-zA-Z][a-zA-Z0-9+.-]*://", options: .regularExpression) != nil {
|
|
812
|
+
return false
|
|
813
|
+
}
|
|
814
|
+
return true
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
private func isBlockedHeader(_ key: String) -> Bool {
|
|
818
|
+
switch key.lowercased() {
|
|
819
|
+
case "host", "connection", "content-length":
|
|
820
|
+
return true
|
|
821
|
+
default:
|
|
822
|
+
return false
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
private func isHTTPSuccess(_ status: Int) -> Bool {
|
|
827
|
+
return status >= 200 && status < 300
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
private func parseJSONObject(_ body: String) -> JSObject? {
|
|
831
|
+
guard let data = body.data(using: .utf8),
|
|
832
|
+
let object = try? JSONSerialization.jsonObject(with: data, options: []),
|
|
833
|
+
let json = object as? JSObject else {
|
|
834
|
+
return nil
|
|
835
|
+
}
|
|
836
|
+
return json
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
private func parseAgentHTTPResponse(_ value: Any?) -> Result<AgentHTTPResponse, Error> {
|
|
840
|
+
guard let payload = value as? JSObject else {
|
|
841
|
+
return .failure(pluginError("iOS local ITTP bridge returned a non-object response"))
|
|
842
|
+
}
|
|
843
|
+
let status = (payload["status"] as? Int)
|
|
844
|
+
?? (payload["status"] as? NSNumber)?.intValue
|
|
845
|
+
?? 500
|
|
846
|
+
let statusText = payload["statusText"] as? String
|
|
847
|
+
?? HTTPURLResponse.localizedString(forStatusCode: status)
|
|
848
|
+
let rawHeaders = payload["headers"] as? JSObject ?? [:]
|
|
849
|
+
var headers: [String: String] = [:]
|
|
850
|
+
for (key, value) in rawHeaders {
|
|
851
|
+
headers[key.lowercased()] = String(describing: value)
|
|
852
|
+
}
|
|
853
|
+
let body = payload["body"] as? String ?? ""
|
|
854
|
+
if let bodyBytes = body.data(using: .utf8), bodyBytes.count > maxResponseBodyBytes {
|
|
855
|
+
return .failure(pluginError("Response body is too large"))
|
|
856
|
+
}
|
|
857
|
+
return .success(AgentHTTPResponse(
|
|
858
|
+
status: status,
|
|
859
|
+
statusText: statusText,
|
|
860
|
+
headers: headers,
|
|
861
|
+
body: body
|
|
862
|
+
))
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
private func agentHTTPResponseObject(_ response: AgentHTTPResponse) -> JSObject {
|
|
866
|
+
var headers: JSObject = [:]
|
|
867
|
+
for (key, value) in response.headers {
|
|
868
|
+
headers[key] = value
|
|
869
|
+
}
|
|
870
|
+
return [
|
|
871
|
+
"status": response.status,
|
|
872
|
+
"statusText": response.statusText,
|
|
873
|
+
"headers": headers,
|
|
874
|
+
"body": response.body,
|
|
875
|
+
]
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
private func localIttpUnavailableResponse() -> AgentHTTPResponse {
|
|
879
|
+
let bodyObject: JSObject = [
|
|
880
|
+
"ok": false,
|
|
881
|
+
"error": "ios_ittp_handler_unavailable",
|
|
882
|
+
"reason": localIttpOnlyMessage(),
|
|
883
|
+
]
|
|
884
|
+
let bodyData = try? JSONSerialization.data(withJSONObject: bodyObject, options: [])
|
|
885
|
+
let body = bodyData.flatMap { String(data: $0, encoding: .utf8) } ?? "{\"ok\":false,\"error\":\"ios_ittp_handler_unavailable\"}"
|
|
886
|
+
return AgentHTTPResponse(
|
|
887
|
+
status: 503,
|
|
888
|
+
statusText: HTTPURLResponse.localizedString(forStatusCode: 503),
|
|
889
|
+
headers: ["content-type": "application/json"],
|
|
890
|
+
body: body
|
|
891
|
+
)
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
private func httpErrorMessage(prefix: String, response: AgentHTTPResponse) -> String {
|
|
895
|
+
let body = response.body.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
896
|
+
if body.isEmpty {
|
|
897
|
+
return "\(prefix): HTTP \(response.status)"
|
|
898
|
+
}
|
|
899
|
+
return "\(prefix): HTTP \(response.status): \(body)"
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
private func missingEndpointMessage() -> String {
|
|
903
|
+
return "iOS Agent requires a configured HTTP endpoint for remote/cloud mode, or runtimeMode=local for dev/sideload local mode. Set Agent.apiBase in capacitor.config, an Info.plist/UserDefaults key such as ELIZA_IOS_API_BASE or ELIZA_AGENT_API_BASE, or a simulator environment variable."
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
private func localIttpOnlyMessage() -> String {
|
|
907
|
+
return "iOS local agent requests require the WebView ITTP route kernel bridge. Start the app WebView before calling native Agent.request or Agent.chat in local mode."
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
private func urlEncode(_ value: String) -> String {
|
|
911
|
+
return value.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? value
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
private func pluginError(_ message: String) -> NSError {
|
|
915
|
+
return NSError(domain: "AgentPlugin", code: 1, userInfo: [NSLocalizedDescriptionKey: message])
|
|
916
|
+
}
|
|
917
|
+
}
|