@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "maxun-core",
|
"name": "maxun-core",
|
||||||
"version": "0.0.11",
|
"version": "0.0.12",
|
||||||
"description": "Core package for Maxun, responsible for data extraction",
|
"description": "Core package for Maxun, responsible for data extraction",
|
||||||
"main": "build/index.js",
|
"main": "build/index.js",
|
||||||
"typings": "build/index.d.ts",
|
"typings": "build/index.d.ts",
|
||||||
|
|||||||
@@ -286,6 +286,12 @@ export default class Interpreter extends EventEmitter {
|
|||||||
? arrayToObject(<any>superset[key])
|
? arrayToObject(<any>superset[key])
|
||||||
: superset[key];
|
: superset[key];
|
||||||
|
|
||||||
|
if (key === 'selectors' && Array.isArray(value) && Array.isArray(superset[key])) {
|
||||||
|
return value.some(selector =>
|
||||||
|
(superset[key] as any[]).includes(selector)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Every `subset` key must exist in the `superset` and
|
// Every `subset` key must exist in the `superset` and
|
||||||
// have the same value (strict equality), or subset[key] <= superset[key]
|
// have the same value (strict equality), or subset[key] <= superset[key]
|
||||||
return parsedSuperset[key]
|
return parsedSuperset[key]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "maxun",
|
"name": "maxun",
|
||||||
"version": "0.0.9",
|
"version": "0.0.10",
|
||||||
"author": "Maxun",
|
"author": "Maxun",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"loglevel": "^1.8.0",
|
"loglevel": "^1.8.0",
|
||||||
"loglevel-plugin-remote": "^0.6.8",
|
"loglevel-plugin-remote": "^0.6.8",
|
||||||
"maxun-core": "^0.0.11",
|
"maxun-core": "^0.0.12",
|
||||||
"minio": "^8.0.1",
|
"minio": "^8.0.1",
|
||||||
"moment-timezone": "^0.5.45",
|
"moment-timezone": "^0.5.45",
|
||||||
"node-cron": "^3.0.3",
|
"node-cron": "^3.0.3",
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
"new": "Roboter erstellen",
|
"new": "Roboter erstellen",
|
||||||
"modal": {
|
"modal": {
|
||||||
"title": "Geben Sie die URL ein",
|
"title": "Geben Sie die URL ein",
|
||||||
"login_title": "Erfordert diese Seite eine Anmeldung?",
|
"login_title": "Ist für diese Website eine Anmeldung erforderlich?",
|
||||||
"label": "URL",
|
"label": "URL",
|
||||||
"button": "Aufnahme starten"
|
"button": "Aufnahme starten"
|
||||||
},
|
},
|
||||||
@@ -139,12 +139,41 @@
|
|||||||
"no_key_message": "Sie haben noch keinen API-Schlüssel generiert.",
|
"no_key_message": "Sie haben noch keinen API-Schlüssel generiert.",
|
||||||
"generate_button": "API-Schlüssel generieren",
|
"generate_button": "API-Schlüssel generieren",
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"fetch_error": "API-Schlüssel konnte nicht abgerufen werden - ${error}",
|
"errors": {
|
||||||
"generate_success": "API-Schlüssel erfolgreich generiert",
|
"fetch": {
|
||||||
"generate_error": "API-Schlüssel konnte nicht generiert werden - ${error}",
|
"network": "Netzwerkfehler beim Abrufen des API-Schlüssels: ${error}",
|
||||||
"delete_success": "API-Schlüssel erfolgreich gelöscht",
|
"unauthorized": "Sie müssen angemeldet sein, um auf den API-Schlüssel zuzugreifen",
|
||||||
"delete_error": "API-Schlüssel konnte nicht gelöscht werden - ${error}",
|
"not_found": "API-Schlüssel für Ihr Konto wurde nicht gefunden",
|
||||||
"copy_success": "API-Schlüssel erfolgreich kopiert"
|
"server": "Serverfehler beim Abrufen des API-Schlüssels. Bitte versuchen Sie es später erneut",
|
||||||
|
"unknown": "Unbekannter Fehler beim Abrufen des API-Schlüssels: ${error}"
|
||||||
|
},
|
||||||
|
"generate": {
|
||||||
|
"network": "Netzwerkfehler bei der Generierung des API-Schlüssels: ${error}",
|
||||||
|
"unauthorized": "Sie müssen angemeldet sein, um einen API-Schlüssel zu generieren",
|
||||||
|
"key_exists": "Sie haben bereits einen API-Schlüssel. Bitte löschen Sie zuerst den vorhandenen",
|
||||||
|
"not_found": "Benutzerkonto nicht gefunden",
|
||||||
|
"server": "Serverfehler bei der Generierung des API-Schlüssels. Bitte versuchen Sie es später erneut",
|
||||||
|
"unknown": "Unbekannter Fehler bei der Generierung des API-Schlüssels: ${error}"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"network": "Netzwerkfehler beim Löschen des API-Schlüssels: ${error}",
|
||||||
|
"unauthorized": "Sie müssen angemeldet sein, um den API-Schlüssel zu löschen",
|
||||||
|
"not_found": "Benutzerkonto nicht gefunden",
|
||||||
|
"key_not_found": "Kein API-Schlüssel zum Löschen gefunden",
|
||||||
|
"server": "Serverfehler beim Löschen des API-Schlüssels. Bitte versuchen Sie es später erneut",
|
||||||
|
"unknown": "Unbekannter Fehler beim Löschen des API-Schlüssels: ${error}"
|
||||||
|
},
|
||||||
|
"copy": {
|
||||||
|
"failed": "Fehler beim Kopieren des API-Schlüssels in die Zwischenablage",
|
||||||
|
"no_key": "Kein API-Schlüssel zum Kopieren verfügbar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"fetch": "API-Schlüssel erfolgreich abgerufen",
|
||||||
|
"generate": "Neuer API-Schlüssel erfolgreich generiert",
|
||||||
|
"delete": "API-Schlüssel erfolgreich gelöscht",
|
||||||
|
"copy": "API-Schlüssel in die Zwischenablage kopiert"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"action_description": {
|
"action_description": {
|
||||||
@@ -487,6 +516,19 @@
|
|||||||
},
|
},
|
||||||
"navbar": {
|
"navbar": {
|
||||||
"project_name": "Maxun",
|
"project_name": "Maxun",
|
||||||
|
"notifications": {
|
||||||
|
"success": {
|
||||||
|
"logout": "Erfolgreich abgemeldet"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"logout": {
|
||||||
|
"unauthorized": "Sie sind nicht berechtigt, diese Aktion durchzuführen",
|
||||||
|
"server": "Serverfehler beim Abmelden",
|
||||||
|
"network": "Netzwerkfehler beim Abmelden",
|
||||||
|
"unknown": "Ein unerwarteter Fehler ist beim Abmelden aufgetreten"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"upgrade": {
|
"upgrade": {
|
||||||
"button": "Upgrade",
|
"button": "Upgrade",
|
||||||
"modal": {
|
"modal": {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"register": {
|
"register": {
|
||||||
"title": "Register Account",
|
"title": "Create An Account",
|
||||||
"email": "Enter Work Email",
|
"email": "Enter Work Email",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"button": "Register",
|
"button": "Register",
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
"new":"Create Robot",
|
"new":"Create Robot",
|
||||||
"modal":{
|
"modal":{
|
||||||
"title":"Enter the URL",
|
"title":"Enter the URL",
|
||||||
"login_title": "Does this website require login?",
|
"login_title": "Does this website require logging in?",
|
||||||
"label":"URL",
|
"label":"URL",
|
||||||
"button":"Start Recording"
|
"button":"Start Recording"
|
||||||
},
|
},
|
||||||
@@ -140,12 +140,41 @@
|
|||||||
"no_key_message": "You haven't generated an API key yet.",
|
"no_key_message": "You haven't generated an API key yet.",
|
||||||
"generate_button": "Generate API Key",
|
"generate_button": "Generate API Key",
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"fetch_error": "Failed to fetch API Key - ${error}",
|
"errors": {
|
||||||
"generate_success": "Generated API Key successfully",
|
"fetch": {
|
||||||
"generate_error": "Failed to generate API Key - ${error}",
|
"network": "Network error while fetching API key: ${error}",
|
||||||
"delete_success": "API Key deleted successfully",
|
"unauthorized": "You must be logged in to access API key",
|
||||||
"delete_error": "Failed to delete API Key - ${error}",
|
"not_found": "Unable to find API key for your account",
|
||||||
"copy_success": "Copied API Key successfully"
|
"server": "Server error while fetching API key. Please try again later",
|
||||||
|
"unknown": "Unknown error occurred while fetching API key: ${error}"
|
||||||
|
},
|
||||||
|
"generate": {
|
||||||
|
"network": "Network error while generating API key: ${error}",
|
||||||
|
"unauthorized": "You must be logged in to generate an API key",
|
||||||
|
"key_exists": "You already have an API key. Please delete the existing one first",
|
||||||
|
"not_found": "User account not found",
|
||||||
|
"server": "Server error while generating API key. Please try again later",
|
||||||
|
"unknown": "Unknown error occurred while generating API key: ${error}"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"network": "Network error while deleting API key: ${error}",
|
||||||
|
"unauthorized": "You must be logged in to delete API key",
|
||||||
|
"not_found": "User account not found",
|
||||||
|
"key_not_found": "No API key found to delete",
|
||||||
|
"server": "Server error while deleting API key. Please try again later",
|
||||||
|
"unknown": "Unknown error occurred while deleting API key: ${error}"
|
||||||
|
},
|
||||||
|
"copy": {
|
||||||
|
"failed": "Failed to copy API key to clipboard",
|
||||||
|
"no_key": "No API key available to copy"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"fetch": "API key retrieved successfully",
|
||||||
|
"generate": "New API key generated successfully",
|
||||||
|
"delete": "API key deleted successfully",
|
||||||
|
"copy": "API key copied to clipboard"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"action_description": {
|
"action_description": {
|
||||||
@@ -498,6 +527,19 @@
|
|||||||
},
|
},
|
||||||
"navbar": {
|
"navbar": {
|
||||||
"project_name": "Maxun",
|
"project_name": "Maxun",
|
||||||
|
"notifications": {
|
||||||
|
"success": {
|
||||||
|
"logout": "Logged out successfully"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"logout": {
|
||||||
|
"unauthorized": "You are not authorized to perform this action",
|
||||||
|
"server": "Server error occurred during logout",
|
||||||
|
"network": "Network error while logging out",
|
||||||
|
"unknown": "An unexpected error occurred during logout"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"upgrade": {
|
"upgrade": {
|
||||||
"button": "Upgrade",
|
"button": "Upgrade",
|
||||||
"modal": {
|
"modal": {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
"new": "Crear Robot",
|
"new": "Crear Robot",
|
||||||
"modal": {
|
"modal": {
|
||||||
"title": "Ingresa la URL",
|
"title": "Ingresa la URL",
|
||||||
"login_title": "¿Este sitio requiere inicio de sesión?",
|
"login_title": "¿Este sitio web requiere iniciar sesión?",
|
||||||
"label": "URL",
|
"label": "URL",
|
||||||
"button": "Comenzar grabación"
|
"button": "Comenzar grabación"
|
||||||
},
|
},
|
||||||
@@ -140,12 +140,41 @@
|
|||||||
"no_key_message": "Aún no has generado una clave API.",
|
"no_key_message": "Aún no has generado una clave API.",
|
||||||
"generate_button": "Generar Clave API",
|
"generate_button": "Generar Clave API",
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"fetch_error": "Error al obtener la clave API - ${error}",
|
"errors": {
|
||||||
"generate_success": "Clave API generada con éxito",
|
"fetch": {
|
||||||
"generate_error": "Error al generar la clave API - ${error}",
|
"network": "Error de red al obtener la clave API: ${error}",
|
||||||
"delete_success": "Clave API eliminada con éxito",
|
"unauthorized": "Debes iniciar sesión para acceder a la clave API",
|
||||||
"delete_error": "Error al eliminar la clave API - ${error}",
|
"not_found": "No se pudo encontrar la clave API para tu cuenta",
|
||||||
"copy_success": "Clave API copiada con éxito"
|
"server": "Error del servidor al obtener la clave API. Por favor, inténtalo más tarde",
|
||||||
|
"unknown": "Error desconocido al obtener la clave API: ${error}"
|
||||||
|
},
|
||||||
|
"generate": {
|
||||||
|
"network": "Error de red al generar la clave API: ${error}",
|
||||||
|
"unauthorized": "Debes iniciar sesión para generar una clave API",
|
||||||
|
"key_exists": "Ya tienes una clave API. Por favor, elimina la existente primero",
|
||||||
|
"not_found": "Cuenta de usuario no encontrada",
|
||||||
|
"server": "Error del servidor al generar la clave API. Por favor, inténtalo más tarde",
|
||||||
|
"unknown": "Error desconocido al generar la clave API: ${error}"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"network": "Error de red al eliminar la clave API: ${error}",
|
||||||
|
"unauthorized": "Debes iniciar sesión para eliminar la clave API",
|
||||||
|
"not_found": "Cuenta de usuario no encontrada",
|
||||||
|
"key_not_found": "No se encontró ninguna clave API para eliminar",
|
||||||
|
"server": "Error del servidor al eliminar la clave API. Por favor, inténtalo más tarde",
|
||||||
|
"unknown": "Error desconocido al eliminar la clave API: ${error}"
|
||||||
|
},
|
||||||
|
"copy": {
|
||||||
|
"failed": "Error al copiar la clave API al portapapeles",
|
||||||
|
"no_key": "No hay clave API disponible para copiar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"fetch": "Clave API obtenida exitosamente",
|
||||||
|
"generate": "Nueva clave API generada exitosamente",
|
||||||
|
"delete": "Clave API eliminada exitosamente",
|
||||||
|
"copy": "Clave API copiada al portapapeles"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"action_description": {
|
"action_description": {
|
||||||
@@ -488,6 +517,19 @@
|
|||||||
},
|
},
|
||||||
"navbar": {
|
"navbar": {
|
||||||
"project_name": "Maxun",
|
"project_name": "Maxun",
|
||||||
|
"notifications": {
|
||||||
|
"success": {
|
||||||
|
"logout": "Sesión cerrada exitosamente"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"logout": {
|
||||||
|
"unauthorized": "No estás autorizado para realizar esta acción",
|
||||||
|
"server": "Error del servidor durante el cierre de sesión",
|
||||||
|
"network": "Error de red al cerrar sesión",
|
||||||
|
"unknown": "Ocurrió un error inesperado al cerrar sesión"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"upgrade": {
|
"upgrade": {
|
||||||
"button": "Actualizar",
|
"button": "Actualizar",
|
||||||
"modal": {
|
"modal": {
|
||||||
|
|||||||
@@ -140,12 +140,41 @@
|
|||||||
"no_key_message": "APIキーはまだ生成されていません。",
|
"no_key_message": "APIキーはまだ生成されていません。",
|
||||||
"generate_button": "APIキーを生成",
|
"generate_button": "APIキーを生成",
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"fetch_error": "APIキーの取得に失敗しました - ${error}",
|
"errors": {
|
||||||
"generate_success": "APIキーが正常に生成されました",
|
"fetch": {
|
||||||
"generate_error": "APIキーの生成に失敗しました - ${error}",
|
"network": "APIキーの取得中にネットワークエラーが発生しました:${error}",
|
||||||
"delete_success": "APIキーが正常に削除されました",
|
"unauthorized": "APIキーにアクセスするにはログインが必要です",
|
||||||
"delete_error": "APIキーの削除に失敗しました - ${error}",
|
"not_found": "アカウントのAPIキーが見つかりません",
|
||||||
"copy_success": "APIキーがコピーされました"
|
"server": "APIキーの取得中にサーバーエラーが発生しました。後でもう一度お試しください",
|
||||||
|
"unknown": "APIキーの取得中に不明なエラーが発生しました:${error}"
|
||||||
|
},
|
||||||
|
"generate": {
|
||||||
|
"network": "APIキーの生成中にネットワークエラーが発生しました:${error}",
|
||||||
|
"unauthorized": "APIキーを生成するにはログインが必要です",
|
||||||
|
"key_exists": "APIキーが既に存在します。既存のキーを先に削除してください",
|
||||||
|
"not_found": "ユーザーアカウントが見つかりません",
|
||||||
|
"server": "APIキーの生成中にサーバーエラーが発生しました。後でもう一度お試しください",
|
||||||
|
"unknown": "APIキーの生成中に不明なエラーが発生しました:${error}"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"network": "APIキーの削除中にネットワークエラーが発生しました:${error}",
|
||||||
|
"unauthorized": "APIキーを削除するにはログインが必要です",
|
||||||
|
"not_found": "ユーザーアカウントが見つかりません",
|
||||||
|
"key_not_found": "削除するAPIキーが見つかりません",
|
||||||
|
"server": "APIキーの削除中にサーバーエラーが発生しました。後でもう一度お試しください",
|
||||||
|
"unknown": "APIキーの削除中に不明なエラーが発生しました:${error}"
|
||||||
|
},
|
||||||
|
"copy": {
|
||||||
|
"failed": "APIキーのクリップボードへのコピーに失敗しました",
|
||||||
|
"no_key": "コピーできるAPIキーがありません"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"fetch": "APIキーの取得に成功しました",
|
||||||
|
"generate": "新しいAPIキーの生成に成功しました",
|
||||||
|
"delete": "APIキーの削除に成功しました",
|
||||||
|
"copy": "APIキーをクリップボードにコピーしました"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"action_description": {
|
"action_description": {
|
||||||
@@ -488,6 +517,19 @@
|
|||||||
},
|
},
|
||||||
"navbar": {
|
"navbar": {
|
||||||
"project_name": "Maxun",
|
"project_name": "Maxun",
|
||||||
|
"notifications": {
|
||||||
|
"success": {
|
||||||
|
"logout": "ログアウトに成功しました"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"logout": {
|
||||||
|
"unauthorized": "この操作を実行する権限がありません",
|
||||||
|
"server": "ログアウト中にサーバーエラーが発生しました",
|
||||||
|
"network": "ログアウト中にネットワークエラーが発生しました",
|
||||||
|
"unknown": "ログアウト中に予期せぬエラーが発生しました"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"upgrade": {
|
"upgrade": {
|
||||||
"button": "アップグレード",
|
"button": "アップグレード",
|
||||||
"modal": {
|
"modal": {
|
||||||
|
|||||||
@@ -140,12 +140,41 @@
|
|||||||
"no_key_message": "您还未生成API密钥。",
|
"no_key_message": "您还未生成API密钥。",
|
||||||
"generate_button": "生成API密钥",
|
"generate_button": "生成API密钥",
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"fetch_error": "获取API密钥失败 - ${error}",
|
"errors": {
|
||||||
"generate_success": "API密钥生成成功",
|
"fetch": {
|
||||||
"generate_error": "生成API密钥失败 - ${error}",
|
"network": "获取API密钥时发生网络错误:${error}",
|
||||||
"delete_success": "API密钥删除成功",
|
"unauthorized": "您必须登录才能访问API密钥",
|
||||||
"delete_error": "删除API密钥失败 - ${error}",
|
"not_found": "找不到您账户的API密钥",
|
||||||
"copy_success": "API密钥复制成功"
|
"server": "获取API密钥时发生服务器错误。请稍后重试",
|
||||||
|
"unknown": "获取API密钥时发生未知错误:${error}"
|
||||||
|
},
|
||||||
|
"generate": {
|
||||||
|
"network": "生成API密钥时发生网络错误:${error}",
|
||||||
|
"unauthorized": "您必须登录才能生成API密钥",
|
||||||
|
"key_exists": "您已经有一个API密钥。请先删除现有的密钥",
|
||||||
|
"not_found": "找不到用户账户",
|
||||||
|
"server": "生成API密钥时发生服务器错误。请稍后重试",
|
||||||
|
"unknown": "生成API密钥时发生未知错误:${error}"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"network": "删除API密钥时发生网络错误:${error}",
|
||||||
|
"unauthorized": "您必须登录才能删除API密钥",
|
||||||
|
"not_found": "找不到用户账户",
|
||||||
|
"key_not_found": "找不到要删除的API密钥",
|
||||||
|
"server": "删除API密钥时发生服务器错误。请稍后重试",
|
||||||
|
"unknown": "删除API密钥时发生未知错误:${error}"
|
||||||
|
},
|
||||||
|
"copy": {
|
||||||
|
"failed": "复制API密钥到剪贴板失败",
|
||||||
|
"no_key": "没有可复制的API密钥"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"fetch": "成功获取API密钥",
|
||||||
|
"generate": "成功生成新的API密钥",
|
||||||
|
"delete": "成功删除API密钥",
|
||||||
|
"copy": "已将API密钥复制到剪贴板"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"action_description": {
|
"action_description": {
|
||||||
@@ -488,6 +517,19 @@
|
|||||||
},
|
},
|
||||||
"navbar": {
|
"navbar": {
|
||||||
"project_name": "Maxun",
|
"project_name": "Maxun",
|
||||||
|
"notifications": {
|
||||||
|
"success": {
|
||||||
|
"logout": "退出登录成功"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"logout": {
|
||||||
|
"unauthorized": "您没有执行此操作的权限",
|
||||||
|
"server": "退出登录时发生服务器错误",
|
||||||
|
"network": "退出登录时发生网络错误",
|
||||||
|
"unknown": "退出登录时发生未知错误"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"upgrade": {
|
"upgrade": {
|
||||||
"button": "升级",
|
"button": "升级",
|
||||||
"modal": {
|
"modal": {
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ router.post("/register", async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { email, password } = req.body;
|
const { email, password } = req.body;
|
||||||
|
|
||||||
// Validation checks with translation codes
|
|
||||||
if (!email) {
|
if (!email) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
error: "VALIDATION_ERROR",
|
error: "VALIDATION_ERROR",
|
||||||
@@ -32,7 +31,6 @@ router.post("/register", async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user exists
|
|
||||||
let userExist = await User.findOne({ raw: true, where: { email } });
|
let userExist = await User.findOne({ raw: true, where: { email } });
|
||||||
if (userExist) {
|
if (userExist) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
@@ -43,7 +41,6 @@ router.post("/register", async (req, res) => {
|
|||||||
|
|
||||||
const hashedPassword = await hashPassword(password);
|
const hashedPassword = await hashPassword(password);
|
||||||
|
|
||||||
// Create user
|
|
||||||
let user: any;
|
let user: any;
|
||||||
try {
|
try {
|
||||||
user = await User.create({ email, password: hashedPassword });
|
user = await User.create({ email, password: hashedPassword });
|
||||||
@@ -55,7 +52,6 @@ router.post("/register", async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check JWT secret
|
|
||||||
if (!process.env.JWT_SECRET) {
|
if (!process.env.JWT_SECRET) {
|
||||||
console.log("JWT_SECRET is not defined in the environment");
|
console.log("JWT_SECRET is not defined in the environment");
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
@@ -64,7 +60,6 @@ router.post("/register", async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success path
|
|
||||||
const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET as string);
|
const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET as string);
|
||||||
user.password = undefined as unknown as string;
|
user.password = undefined as unknown as string;
|
||||||
res.cookie("token", token, {
|
res.cookie("token", token, {
|
||||||
@@ -124,7 +119,6 @@ router.post("/login", async (req, res) => {
|
|||||||
|
|
||||||
const token = jwt.sign({ id: user?.id }, process.env.JWT_SECRET as string);
|
const token = jwt.sign({ id: user?.id }, process.env.JWT_SECRET as string);
|
||||||
|
|
||||||
// return user and token to client, exclude hashed password
|
|
||||||
if (user) {
|
if (user) {
|
||||||
user.password = undefined as unknown as string;
|
user.password = undefined as unknown as string;
|
||||||
}
|
}
|
||||||
@@ -147,24 +141,34 @@ router.post("/login", async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.get("/logout", async (req, res) => {
|
router.get("/logout", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
res.clearCookie("token");
|
res.clearCookie("token");
|
||||||
return res.json({ message: "Logout successful" });
|
return res.status(200).json({
|
||||||
} catch (error: any) {
|
ok: true,
|
||||||
res.status(500).send(`Could not logout user - ${error.message}`);
|
message: "Logged out successfully",
|
||||||
|
code: "success"
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout error:', error);
|
||||||
|
return res.status(500).json({
|
||||||
|
ok: false,
|
||||||
|
message: "Error during logout",
|
||||||
|
code: "server",
|
||||||
|
error: process.env.NODE_ENV === 'development' ? error : undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
"/current-user",
|
"/current-user",
|
||||||
requireSignIn,
|
requireSignIn,
|
||||||
async (req: Request, res) => {
|
async (req: AuthenticatedRequest, res) => {
|
||||||
const authenticatedReq = req as AuthenticatedRequest;
|
|
||||||
try {
|
try {
|
||||||
if (!authenticatedReq.user) {
|
if (!req.user) {
|
||||||
return res.status(401).json({ ok: false, error: "Unauthorized" });
|
return res.status(401).json({ ok: false, error: "Unauthorized" });
|
||||||
}
|
}
|
||||||
const user = await User.findByPk(authenticatedReq.user.id, {
|
const user = await User.findByPk(req.user.id, {
|
||||||
attributes: { exclude: ["password"] },
|
attributes: { exclude: ["password"] },
|
||||||
});
|
});
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -187,7 +191,7 @@ router.get(
|
|||||||
router.get(
|
router.get(
|
||||||
"/user/:id",
|
"/user/:id",
|
||||||
requireSignIn,
|
requireSignIn,
|
||||||
async (req: Request, res) => {
|
async (req: AuthenticatedRequest, res) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
@@ -216,13 +220,12 @@ router.get(
|
|||||||
router.post(
|
router.post(
|
||||||
"/generate-api-key",
|
"/generate-api-key",
|
||||||
requireSignIn,
|
requireSignIn,
|
||||||
async (req: Request, res) => {
|
async (req: AuthenticatedRequest, res) => {
|
||||||
const authenticatedReq = req as AuthenticatedRequest;
|
|
||||||
try {
|
try {
|
||||||
if (!authenticatedReq.user) {
|
if (!req.user) {
|
||||||
return res.status(401).json({ ok: false, error: "Unauthorized" });
|
return res.status(401).json({ ok: false, error: "Unauthorized" });
|
||||||
}
|
}
|
||||||
const user = await User.findByPk(authenticatedReq.user.id, {
|
const user = await User.findByPk(req.user.id, {
|
||||||
attributes: { exclude: ["password"] },
|
attributes: { exclude: ["password"] },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -257,14 +260,13 @@ router.post(
|
|||||||
router.get(
|
router.get(
|
||||||
"/api-key",
|
"/api-key",
|
||||||
requireSignIn,
|
requireSignIn,
|
||||||
async (req: Request, res) => {
|
async (req: AuthenticatedRequest, res) => {
|
||||||
const authenticatedReq = req as AuthenticatedRequest;
|
|
||||||
try {
|
try {
|
||||||
if (!authenticatedReq.user) {
|
if (!req.user) {
|
||||||
return res.status(401).json({ ok: false, error: "Unauthorized" });
|
return res.status(401).json({ ok: false, error: "Unauthorized" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await User.findByPk(authenticatedReq.user.id, {
|
const user = await User.findByPk(req.user.id, {
|
||||||
raw: true,
|
raw: true,
|
||||||
attributes: ["api_key"],
|
attributes: ["api_key"],
|
||||||
});
|
});
|
||||||
@@ -286,14 +288,13 @@ router.get(
|
|||||||
router.delete(
|
router.delete(
|
||||||
"/delete-api-key",
|
"/delete-api-key",
|
||||||
requireSignIn,
|
requireSignIn,
|
||||||
async (req: Request, res) => {
|
async (req: AuthenticatedRequest, res) => {
|
||||||
const authenticatedReq = req as AuthenticatedRequest;
|
if (!req.user) {
|
||||||
if (!authenticatedReq.user) {
|
|
||||||
return res.status(401).send({ error: "Unauthorized" });
|
return res.status(401).send({ error: "Unauthorized" });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await User.findByPk(authenticatedReq.user.id, { raw: true });
|
const user = await User.findByPk(req.user.id, { raw: true });
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(404).json({ message: "User not found" });
|
return res.status(404).json({ message: "User not found" });
|
||||||
@@ -303,7 +304,7 @@ router.delete(
|
|||||||
return res.status(404).json({ message: "API Key not found" });
|
return res.status(404).json({ message: "API Key not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
await User.update({ api_key: null }, { where: { id: authenticatedReq.user.id } });
|
await User.update({ api_key: null }, { where: { id: req.user.id } });
|
||||||
|
|
||||||
capture("maxun-oss-api-key-deleted", {
|
capture("maxun-oss-api-key-deleted", {
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
@@ -349,8 +350,7 @@ router.get("/google", (req, res) => {
|
|||||||
router.get(
|
router.get(
|
||||||
"/google/callback",
|
"/google/callback",
|
||||||
requireSignIn,
|
requireSignIn,
|
||||||
async (req: Request, res) => {
|
async (req: AuthenticatedRequest, res) => {
|
||||||
const authenticatedReq = req as AuthenticatedRequest;
|
|
||||||
const { code, state } = req.query;
|
const { code, state } = req.query;
|
||||||
try {
|
try {
|
||||||
if (!state) {
|
if (!state) {
|
||||||
@@ -376,12 +376,12 @@ router.get(
|
|||||||
return res.status(400).json({ message: "Email not found" });
|
return res.status(400).json({ message: "Email not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!authenticatedReq.user) {
|
if (!req.user) {
|
||||||
return res.status(401).send({ error: "Unauthorized" });
|
return res.status(401).send({ error: "Unauthorized" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the currently authenticated user (from `requireSignIn`)
|
// Get the currently authenticated user (from `requireSignIn`)
|
||||||
let user = await User.findOne({ where: { id: authenticatedReq.user.id } });
|
let user = await User.findOne({ where: { id: req.user.id } });
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(400).json({ message: "User not found" });
|
return res.status(400).json({ message: "User not found" });
|
||||||
@@ -459,13 +459,12 @@ router.get(
|
|||||||
router.post(
|
router.post(
|
||||||
"/gsheets/data",
|
"/gsheets/data",
|
||||||
requireSignIn,
|
requireSignIn,
|
||||||
async (req: Request, res) => {
|
async (req: AuthenticatedRequest, res) => {
|
||||||
const authenticatedReq = req as AuthenticatedRequest;
|
|
||||||
const { spreadsheetId, robotId } = req.body;
|
const { spreadsheetId, robotId } = req.body;
|
||||||
if (!authenticatedReq.user) {
|
if (!req.user) {
|
||||||
return res.status(401).send({ error: "Unauthorized" });
|
return res.status(401).send({ error: "Unauthorized" });
|
||||||
}
|
}
|
||||||
const user = await User.findByPk(authenticatedReq.user.id, { raw: true });
|
const user = await User.findByPk(req.user.id, { raw: true });
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(400).json({ message: "User not found" });
|
return res.status(400).json({ message: "User not found" });
|
||||||
@@ -577,14 +576,13 @@ router.post("/gsheets/update", requireSignIn, async (req, res) => {
|
|||||||
router.post(
|
router.post(
|
||||||
"/gsheets/remove",
|
"/gsheets/remove",
|
||||||
requireSignIn,
|
requireSignIn,
|
||||||
async (req: Request, res) => {
|
async (req: AuthenticatedRequest, res) => {
|
||||||
const authenticatedReq = req as AuthenticatedRequest;
|
|
||||||
const { robotId } = req.body;
|
const { robotId } = req.body;
|
||||||
if (!robotId) {
|
if (!robotId) {
|
||||||
return res.status(400).json({ message: "Robot ID is required" });
|
return res.status(400).json({ message: "Robot ID is required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!authenticatedReq.user) {
|
if (!req.user) {
|
||||||
return res.status(401).send({ error: "Unauthorized" });
|
return res.status(401).send({ error: "Unauthorized" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -606,7 +604,7 @@ router.post(
|
|||||||
});
|
});
|
||||||
|
|
||||||
capture("maxun-oss-google-sheet-integration-removed", {
|
capture("maxun-oss-google-sheet-integration-removed", {
|
||||||
user_id: authenticatedReq.user.id,
|
user_id: req.user.id,
|
||||||
robot_id: robotId,
|
robot_id: robotId,
|
||||||
deleted_at: new Date().toISOString(),
|
deleted_at: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
@@ -620,4 +618,4 @@ router.post(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -25,35 +25,43 @@ chromium.use(stealthPlugin());
|
|||||||
|
|
||||||
export const router = Router();
|
export const router = Router();
|
||||||
|
|
||||||
export const decryptWorkflowActions = async (workflow: any[],): Promise<any[]> => {
|
export const processWorkflowActions = async (workflow: any[], checkLimit: boolean = false): Promise<any[]> => {
|
||||||
// Create a deep copy to avoid mutating the original workflow
|
const processedWorkflow = JSON.parse(JSON.stringify(workflow));
|
||||||
const processedWorkflow = JSON.parse(JSON.stringify(workflow));
|
|
||||||
|
|
||||||
// Process each step in the workflow
|
processedWorkflow.workflow.forEach((pair: any) => {
|
||||||
for (const step of processedWorkflow) {
|
pair.what.forEach((action: any) => {
|
||||||
if (!step.what) continue;
|
// Handle limit validation for scrapeList action
|
||||||
|
if (action.action === 'scrapeList' && checkLimit && Array.isArray(action.args) && action.args.length > 0) {
|
||||||
// Process each action in the step
|
const scrapeConfig = action.args[0];
|
||||||
for (const action of step.what) {
|
if (scrapeConfig && typeof scrapeConfig === 'object' && 'limit' in scrapeConfig) {
|
||||||
// Only process type and press actions
|
if (typeof scrapeConfig.limit === 'number' && scrapeConfig.limit > 5) {
|
||||||
if ((action.action === 'type' || action.action === 'press') && Array.isArray(action.args) && action.args.length > 1) {
|
scrapeConfig.limit = 5;
|
||||||
// The second argument contains the encrypted value
|
|
||||||
const encryptedValue = action.args[1];
|
|
||||||
if (typeof encryptedValue === 'string') {
|
|
||||||
try {
|
|
||||||
// Decrypt the value and update the args array
|
|
||||||
action.args[1] = await decrypt(encryptedValue);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to decrypt value:', error);
|
|
||||||
// Keep the encrypted value if decryption fails
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
// Handle decryption for type and press actions
|
||||||
|
if ((action.action === 'type' || action.action === 'press') && Array.isArray(action.args) && action.args.length > 1) {
|
||||||
|
try {
|
||||||
|
const encryptedValue = action.args[1];
|
||||||
|
if (typeof encryptedValue === 'string') {
|
||||||
|
const decryptedValue = decrypt(encryptedValue);
|
||||||
|
action.args[1] = decryptedValue;
|
||||||
|
} else {
|
||||||
|
logger.log('error', 'Encrypted value is not a string');
|
||||||
|
action.args[1] = '';
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
logger.log('error', `Failed to decrypt input value: ${errorMessage}`);
|
||||||
|
action.args[1] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return processedWorkflow;
|
return processedWorkflow;
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs information about recordings API.
|
* Logs information about recordings API.
|
||||||
@@ -88,7 +96,7 @@ router.get('/recordings/:id', requireSignIn, async (req, res) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (data?.recording?.workflow) {
|
if (data?.recording?.workflow) {
|
||||||
data.recording.workflow = await decryptWorkflowActions(
|
data.recording.workflow = await processWorkflowActions(
|
||||||
data.recording.workflow,
|
data.recording.workflow,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -163,54 +171,82 @@ interface Credentials {
|
|||||||
[key: string]: CredentialInfo;
|
[key: string]: CredentialInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTypeActionsInWorkflow(workflow: any[], credentials: Credentials) {
|
function handleWorkflowActions(workflow: any[], credentials: Credentials) {
|
||||||
return workflow.map(step => {
|
return workflow.map(step => {
|
||||||
if (!step.what) return step;
|
if (!step.what) return step;
|
||||||
|
|
||||||
const indicesToRemove = new Set<number>();
|
const newWhat: any[] = [];
|
||||||
step.what.forEach((action: any, index: number) => {
|
const processedSelectors = new Set<string>();
|
||||||
if (!action.action || !action.args?.[0]) return;
|
|
||||||
|
for (let i = 0; i < step.what.length; i++) {
|
||||||
|
const action = step.what[i];
|
||||||
|
|
||||||
if ((action.action === 'type' || action.action === 'press') && credentials[action.args[0]]) {
|
if (!action?.action || !action?.args?.[0]) {
|
||||||
indicesToRemove.add(index);
|
newWhat.push(action);
|
||||||
|
continue;
|
||||||
if (step.what[index + 1]?.action === 'waitForLoadState') {
|
|
||||||
indicesToRemove.add(index + 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const filteredWhat = step.what.filter((_: any, index: number) => !indicesToRemove.has(index));
|
const selector = action.args[0];
|
||||||
|
const credential = credentials[selector];
|
||||||
|
|
||||||
Object.entries(credentials).forEach(([selector, credentialInfo]) => {
|
if (!credential) {
|
||||||
const clickIndex = filteredWhat.findIndex((action: any) =>
|
newWhat.push(action);
|
||||||
action.action === 'click' && action.args?.[0] === selector
|
continue;
|
||||||
);
|
}
|
||||||
|
|
||||||
if (clickIndex !== -1) {
|
if (action.action === 'click') {
|
||||||
const chars = credentialInfo.value.split('');
|
newWhat.push(action);
|
||||||
|
|
||||||
chars.forEach((char, i) => {
|
if (!processedSelectors.has(selector) &&
|
||||||
filteredWhat.splice(clickIndex + 1 + (i * 2), 0, {
|
i + 1 < step.what.length &&
|
||||||
|
(step.what[i + 1].action === 'type' || step.what[i + 1].action === 'press')) {
|
||||||
|
|
||||||
|
newWhat.push({
|
||||||
action: 'type',
|
action: 'type',
|
||||||
args: [
|
args: [selector, encrypt(credential.value), credential.type]
|
||||||
selector,
|
|
||||||
encrypt(char),
|
|
||||||
credentialInfo.type
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
filteredWhat.splice(clickIndex + 2 + (i * 2), 0, {
|
newWhat.push({
|
||||||
action: 'waitForLoadState',
|
action: 'waitForLoadState',
|
||||||
args: ['networkidle']
|
args: ['networkidle']
|
||||||
});
|
});
|
||||||
|
|
||||||
|
processedSelectors.add(selector);
|
||||||
|
|
||||||
|
while (i + 1 < step.what.length &&
|
||||||
|
(step.what[i + 1].action === 'type' ||
|
||||||
|
step.what[i + 1].action === 'press' ||
|
||||||
|
step.what[i + 1].action === 'waitForLoadState')) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ((action.action === 'type' || action.action === 'press') &&
|
||||||
|
!processedSelectors.has(selector)) {
|
||||||
|
newWhat.push({
|
||||||
|
action: 'type',
|
||||||
|
args: [selector, encrypt(credential.value), credential.type]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
newWhat.push({
|
||||||
|
action: 'waitForLoadState',
|
||||||
|
args: ['networkidle']
|
||||||
|
});
|
||||||
|
|
||||||
|
processedSelectors.add(selector);
|
||||||
|
|
||||||
|
// Skip subsequent type/press/waitForLoadState actions for this selector
|
||||||
|
while (i + 1 < step.what.length &&
|
||||||
|
(step.what[i + 1].action === 'type' ||
|
||||||
|
step.what[i + 1].action === 'press' ||
|
||||||
|
step.what[i + 1].action === 'waitForLoadState')) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...step,
|
...step,
|
||||||
what: filteredWhat
|
what: newWhat
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -243,7 +279,7 @@ router.put('/recordings/:id', requireSignIn, async (req: AuthenticatedRequest, r
|
|||||||
let workflow = [...robot.recording.workflow]; // Create a copy of the workflow
|
let workflow = [...robot.recording.workflow]; // Create a copy of the workflow
|
||||||
|
|
||||||
if (credentials) {
|
if (credentials) {
|
||||||
workflow = updateTypeActionsInWorkflow(workflow, credentials);
|
workflow = handleWorkflowActions(workflow, credentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the limit
|
// Update the limit
|
||||||
@@ -281,9 +317,23 @@ router.put('/recordings/:id', requireSignIn, async (req: AuthenticatedRequest, r
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
robot.set('recording', { ...robot.recording, workflow });
|
const updates: any = {
|
||||||
|
recording: {
|
||||||
|
...robot.recording,
|
||||||
|
workflow
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
await robot.save();
|
if (name) {
|
||||||
|
updates.recording_meta = {
|
||||||
|
...robot.recording_meta,
|
||||||
|
name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await Robot.update(updates, {
|
||||||
|
where: { 'recording_meta.id': id }
|
||||||
|
});
|
||||||
|
|
||||||
const updatedRobot = await Robot.findOne({ where: { 'recording_meta.id': id } });
|
const updatedRobot = await Robot.findOne({ where: { 'recording_meta.id': id } });
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { getBestSelectorForAction } from "../utils";
|
|||||||
import { browserPool } from "../../server";
|
import { browserPool } from "../../server";
|
||||||
import { uuid } from "uuidv4";
|
import { uuid } from "uuidv4";
|
||||||
import { capture } from "../../utils/analytics"
|
import { capture } from "../../utils/analytics"
|
||||||
import { encrypt } from "../../utils/auth";
|
import { decrypt, encrypt } from "../../utils/auth";
|
||||||
|
|
||||||
interface PersistedGeneratedData {
|
interface PersistedGeneratedData {
|
||||||
lastUsedSelector: string;
|
lastUsedSelector: string;
|
||||||
@@ -42,6 +42,13 @@ interface MetaData {
|
|||||||
isLogin?: boolean;
|
isLogin?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface InputState {
|
||||||
|
selector: string;
|
||||||
|
value: string;
|
||||||
|
type: string;
|
||||||
|
cursorPosition: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Workflow generator is used to transform the user's interactions into an automatically
|
* Workflow generator is used to transform the user's interactions into an automatically
|
||||||
* generated correct workflows, using the ability of internal state persistence and
|
* generated correct workflows, using the ability of internal state persistence and
|
||||||
@@ -428,26 +435,86 @@ export class WorkflowGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ((elementInfo?.tagName === 'INPUT' || elementInfo?.tagName === 'TEXTAREA') && selector) {
|
if ((elementInfo?.tagName === 'INPUT' || elementInfo?.tagName === 'TEXTAREA') && selector) {
|
||||||
// Calculate the exact position within the element
|
const positionAndCursor = await page.evaluate(
|
||||||
const elementPos = await page.evaluate((selector) => {
|
({ selector, coords }) => {
|
||||||
const element = document.querySelector(selector);
|
const getCursorPosition = (element: any, clickX: any) => {
|
||||||
if (!element) return null;
|
const text = element.value;
|
||||||
const rect = element.getBoundingClientRect();
|
|
||||||
return {
|
const mirror = document.createElement('div');
|
||||||
x: rect.left,
|
|
||||||
y: rect.top
|
const style = window.getComputedStyle(element);
|
||||||
};
|
mirror.style.cssText = `
|
||||||
}, selector);
|
font: ${style.font};
|
||||||
|
line-height: ${style.lineHeight};
|
||||||
|
padding: ${style.padding};
|
||||||
|
border: ${style.border};
|
||||||
|
box-sizing: ${style.boxSizing};
|
||||||
|
white-space: ${style.whiteSpace};
|
||||||
|
overflow-wrap: ${style.overflowWrap};
|
||||||
|
position: absolute;
|
||||||
|
top: -9999px;
|
||||||
|
left: -9999px;
|
||||||
|
width: ${element.offsetWidth}px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(mirror);
|
||||||
|
|
||||||
|
const paddingLeft = parseFloat(style.paddingLeft);
|
||||||
|
const borderLeft = parseFloat(style.borderLeftWidth);
|
||||||
|
|
||||||
|
const adjustedClickX = clickX - (paddingLeft + borderLeft);
|
||||||
|
|
||||||
|
let bestIndex = 0;
|
||||||
|
let bestDiff = Infinity;
|
||||||
|
|
||||||
|
for (let i = 0; i <= text.length; i++) {
|
||||||
|
const textBeforeCursor = text.substring(0, i);
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.textContent = textBeforeCursor;
|
||||||
|
mirror.innerHTML = '';
|
||||||
|
mirror.appendChild(span);
|
||||||
|
|
||||||
|
const textWidth = span.getBoundingClientRect().width;
|
||||||
|
|
||||||
|
const diff = Math.abs(adjustedClickX - textWidth);
|
||||||
|
|
||||||
|
if (diff < bestDiff) {
|
||||||
|
bestIndex = i;
|
||||||
|
bestDiff = diff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.removeChild(mirror);
|
||||||
|
|
||||||
|
return bestIndex;
|
||||||
|
};
|
||||||
|
|
||||||
if (elementPos) {
|
const element = document.querySelector(selector) as HTMLInputElement | HTMLTextAreaElement;
|
||||||
const relativeX = coordinates.x - elementPos.x;
|
if (!element) return null;
|
||||||
const relativeY = coordinates.y - elementPos.y;
|
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const relativeX = coords.x - rect.left;
|
||||||
|
|
||||||
|
return {
|
||||||
|
rect: {
|
||||||
|
x: rect.left,
|
||||||
|
y: rect.top
|
||||||
|
},
|
||||||
|
cursorIndex: getCursorPosition(element, relativeX)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ selector, coords: coordinates }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (positionAndCursor) {
|
||||||
|
const relativeX = coordinates.x - positionAndCursor.rect.x;
|
||||||
|
const relativeY = coordinates.y - positionAndCursor.rect.y;
|
||||||
|
|
||||||
const pair: WhereWhatPair = {
|
const pair: WhereWhatPair = {
|
||||||
where,
|
where,
|
||||||
what: [{
|
what: [{
|
||||||
action: 'click',
|
action: 'click',
|
||||||
args: [selector, { position: { x: relativeX, y: relativeY } }]
|
args: [selector, { position: { x: relativeX, y: relativeY } }, { cursorIndex: positionAndCursor.cursorIndex }],
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1026,77 +1093,107 @@ export class WorkflowGenerator {
|
|||||||
* @param workflow The workflow to be optimized.
|
* @param workflow The workflow to be optimized.
|
||||||
*/
|
*/
|
||||||
private optimizeWorkflow = (workflow: WorkflowFile) => {
|
private optimizeWorkflow = (workflow: WorkflowFile) => {
|
||||||
|
const inputStates = new Map<string, InputState>();
|
||||||
// replace a sequence of press actions by a single fill action
|
|
||||||
let input = {
|
for (const pair of workflow.workflow) {
|
||||||
selector: '',
|
let currentIndex = 0;
|
||||||
value: '',
|
|
||||||
type: '',
|
while (currentIndex < pair.what.length) {
|
||||||
actionCounter: 0,
|
const condition = pair.what[currentIndex];
|
||||||
};
|
|
||||||
|
if (condition.action === 'click' && condition.args?.[2]?.cursorIndex !== undefined) {
|
||||||
const pushTheOptimizedAction = (pair: WhereWhatPair, index: number) => {
|
const selector = condition.args[0];
|
||||||
if (input.value.length === 1) {
|
const cursorIndex = condition.args[2].cursorIndex;
|
||||||
// when only one press action is present, keep it and add a waitForLoadState action
|
|
||||||
pair.what.splice(index + 1, 0, {
|
let state = inputStates.get(selector) || {
|
||||||
action: 'waitForLoadState',
|
selector,
|
||||||
args: ['networkidle'],
|
value: '',
|
||||||
})
|
type: 'text',
|
||||||
} else {
|
cursorPosition: -1
|
||||||
// when more than one press action is present, add a type action
|
};
|
||||||
pair.what.splice(index - input.actionCounter, input.actionCounter, {
|
|
||||||
action: 'type',
|
state.cursorPosition = cursorIndex;
|
||||||
args: [input.selector, encrypt(input.value), input.type],
|
inputStates.set(selector, state);
|
||||||
}, {
|
|
||||||
action: 'waitForLoadState',
|
pair.what.splice(currentIndex, 1);
|
||||||
args: ['networkidle'],
|
continue;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
if (condition.action === 'press' && condition.args?.[1]) {
|
||||||
|
const [selector, encryptedKey, type] = condition.args;
|
||||||
|
const key = decrypt(encryptedKey);
|
||||||
|
|
||||||
|
let state = inputStates.get(selector);
|
||||||
|
if (!state) {
|
||||||
|
state = {
|
||||||
|
selector,
|
||||||
|
value: '',
|
||||||
|
type: type || 'text',
|
||||||
|
cursorPosition: -1
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
state.type = type || state.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.length === 1) {
|
||||||
|
if (state.cursorPosition === -1) {
|
||||||
|
state.value += key;
|
||||||
|
} else {
|
||||||
|
state.value =
|
||||||
|
state.value.slice(0, state.cursorPosition) +
|
||||||
|
key +
|
||||||
|
state.value.slice(state.cursorPosition);
|
||||||
|
state.cursorPosition++;
|
||||||
|
}
|
||||||
|
} else if (key === 'Backspace') {
|
||||||
|
if (state.cursorPosition > 0) {
|
||||||
|
state.value =
|
||||||
|
state.value.slice(0, state.cursorPosition - 1) +
|
||||||
|
state.value.slice(state.cursorPosition);
|
||||||
|
state.cursorPosition--;
|
||||||
|
} else if (state.cursorPosition === -1 && state.value.length > 0) {
|
||||||
|
state.value = state.value.slice(0, -1);
|
||||||
|
}
|
||||||
|
} else if (key === 'Delete') {
|
||||||
|
if (state.cursorPosition >= 0 && state.cursorPosition < state.value.length) {
|
||||||
|
state.value =
|
||||||
|
state.value.slice(0, state.cursorPosition) +
|
||||||
|
state.value.slice(state.cursorPosition + 1);
|
||||||
|
} else if (state.cursorPosition === -1 && state.value.length > 0) {
|
||||||
|
state.value = state.value.slice(0, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inputStates.set(selector, state);
|
||||||
|
|
||||||
|
pair.what.splice(currentIndex, 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentIndex++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const [selector, state] of inputStates.entries()) {
|
||||||
for (const pair of workflow.workflow) {
|
if (state.value) {
|
||||||
pair.what.forEach((condition, index) => {
|
for (let i = workflow.workflow.length - 1; i >= 0; i--) {
|
||||||
if (condition.action === 'press') {
|
const pair = workflow.workflow[i];
|
||||||
if (condition.args && condition.args[1]) {
|
|
||||||
if (!input.selector) {
|
pair.what.push({
|
||||||
input.selector = condition.args[0];
|
action: 'type',
|
||||||
}
|
args: [selector, encrypt(state.value), state.type]
|
||||||
if (input.selector === condition.args[0]) {
|
}, {
|
||||||
input.actionCounter++;
|
action: 'waitForLoadState',
|
||||||
if (condition.args[1].length === 1) {
|
args: ['networkidle']
|
||||||
input.value = input.value + condition.args[1];
|
});
|
||||||
} else if (condition.args[1] === 'Backspace') {
|
|
||||||
input.value = input.value.slice(0, -1);
|
break;
|
||||||
} else if (condition.args[1] !== 'Shift') {
|
|
||||||
pushTheOptimizedAction(pair, index);
|
|
||||||
pair.what.splice(index + 1, 0, {
|
|
||||||
action: 'waitForLoadState',
|
|
||||||
args: ['networkidle'],
|
|
||||||
})
|
|
||||||
input = { selector: '', value: '', type: '', actionCounter: 0 };
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pushTheOptimizedAction(pair, index);
|
|
||||||
input = {
|
|
||||||
selector: condition.args[0],
|
|
||||||
value: condition.args[1],
|
|
||||||
type: condition.args[2],
|
|
||||||
actionCounter: 1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (input.value.length !== 0) {
|
|
||||||
pushTheOptimizedAction(pair, index);
|
|
||||||
// clear the input
|
|
||||||
input = { selector: '', value: '', type: '', actionCounter: 0 };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return workflow;
|
return workflow;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns workflow params from the stored metadata.
|
* Returns workflow params from the stored metadata.
|
||||||
|
|||||||
@@ -1092,12 +1092,16 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => {
|
|||||||
newPath.splice(i, 1);
|
newPath.splice(i, 1);
|
||||||
const newPathKey = selector(newPath);
|
const newPathKey = selector(newPath);
|
||||||
if (scope.visited.has(newPathKey)) {
|
if (scope.visited.has(newPathKey)) {
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
if (unique(newPath) && same(newPath, input)) {
|
try {
|
||||||
yield newPath;
|
if (unique(newPath) && same(newPath, input)) {
|
||||||
scope.visited.set(newPathKey, true);
|
yield newPath;
|
||||||
yield* optimize(newPath, input, scope);
|
scope.visited.set(newPathKey, true);
|
||||||
|
yield* optimize(newPath, input, scope);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Paper,
|
Paper,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { ContentCopy, Visibility, Delete } from '@mui/icons-material';
|
import { ContentCopy, Visibility, VisibilityOff, Delete } from '@mui/icons-material';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useGlobalInfoStore } from '../../context/globalInfo';
|
import { useGlobalInfoStore } from '../../context/globalInfo';
|
||||||
@@ -43,8 +43,33 @@ const ApiKeyManager = () => {
|
|||||||
try {
|
try {
|
||||||
const { data } = await axios.get(`${apiUrl}/auth/api-key`);
|
const { data } = await axios.get(`${apiUrl}/auth/api-key`);
|
||||||
setApiKey(data.api_key);
|
setApiKey(data.api_key);
|
||||||
|
notify('success', t('apikey.notifications.success.fetch'));
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
notify('error', t('apikey.notifications.fetch_error', { error: error.message }));
|
const status = error.response?.status;
|
||||||
|
let errorKey = 'unknown';
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 401:
|
||||||
|
errorKey = 'unauthorized';
|
||||||
|
break;
|
||||||
|
case 404:
|
||||||
|
errorKey = 'not_found';
|
||||||
|
break;
|
||||||
|
case 500:
|
||||||
|
errorKey = 'server';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (error.message?.includes('Network Error')) {
|
||||||
|
errorKey = 'network';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notify(
|
||||||
|
'error',
|
||||||
|
t(`apikey.notifications.errors.fetch.${errorKey}`, {
|
||||||
|
error: error.response?.data?.message || error.message
|
||||||
|
})
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -58,11 +83,36 @@ const ApiKeyManager = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.post(`${apiUrl}/auth/generate-api-key`);
|
const { data } = await axios.post(`${apiUrl}/auth/generate-api-key`);
|
||||||
setApiKey(data.api_key);
|
if (data.ok && data.api_key) {
|
||||||
|
setApiKey(data.api_key);
|
||||||
notify('success', t('apikey.notifications.generate_success'));
|
notify('success', t('apikey.notifications.success.generate'));
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
notify('error', t('apikey.notifications.generate_error', { error: error.message }));
|
const status = error.response?.status;
|
||||||
|
let errorKey = 'unknown';
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 401:
|
||||||
|
errorKey = 'unauthorized';
|
||||||
|
break;
|
||||||
|
case 403:
|
||||||
|
errorKey = 'limit_reached';
|
||||||
|
break;
|
||||||
|
case 500:
|
||||||
|
errorKey = 'server';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (error.message?.includes('Network Error')) {
|
||||||
|
errorKey = 'network';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notify(
|
||||||
|
'error',
|
||||||
|
t(`apikey.notifications.errors.generate.${errorKey}`, {
|
||||||
|
error: error.response?.data?.message || error.message
|
||||||
|
})
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -71,22 +121,54 @@ const ApiKeyManager = () => {
|
|||||||
const deleteApiKey = async () => {
|
const deleteApiKey = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await axios.delete(`${apiUrl}/auth/delete-api-key`);
|
const response = await axios.delete(`${apiUrl}/auth/delete-api-key`);
|
||||||
setApiKey(null);
|
if (response.data.ok) {
|
||||||
notify('success', t('apikey.notifications.delete_success'));
|
setApiKey(null);
|
||||||
|
notify('success', t('apikey.notifications.success.delete'));
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
notify('error', t('apikey.notifications.delete_error', { error: error.message }));
|
const status = error.response?.status;
|
||||||
|
let errorKey = 'unknown';
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 401:
|
||||||
|
errorKey = 'unauthorized';
|
||||||
|
break;
|
||||||
|
case 404:
|
||||||
|
errorKey = 'not_found';
|
||||||
|
break;
|
||||||
|
case 500:
|
||||||
|
errorKey = 'server';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (error.message?.includes('Network Error')) {
|
||||||
|
errorKey = 'network';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notify(
|
||||||
|
'error',
|
||||||
|
t(`apikey.notifications.errors.delete.${errorKey}`, {
|
||||||
|
error: error.response?.data?.message || error.message
|
||||||
|
})
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyToClipboard = () => {
|
const copyToClipboard = async () => {
|
||||||
if (apiKey) {
|
if (!apiKey) return;
|
||||||
navigator.clipboard.writeText(apiKey);
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(apiKey);
|
||||||
setCopySuccess(true);
|
setCopySuccess(true);
|
||||||
|
notify('success', t('apikey.notifications.success.copy'));
|
||||||
|
|
||||||
|
// Reset copy success state after 2 seconds
|
||||||
setTimeout(() => setCopySuccess(false), 2000);
|
setTimeout(() => setCopySuccess(false), 2000);
|
||||||
notify('info', t('apikey.notifications.copy_success'));
|
} catch (error) {
|
||||||
|
notify('error', t('apikey.notifications.errors.copy.failed'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -137,7 +219,7 @@ const ApiKeyManager = () => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={showKey ? t('apikey.actions.hide') : t('apikey.actions.show')}>
|
<Tooltip title={showKey ? t('apikey.actions.hide') : t('apikey.actions.show')}>
|
||||||
<IconButton onClick={() => setShowKey(!showKey)}>
|
<IconButton onClick={() => setShowKey(!showKey)}>
|
||||||
<Visibility />
|
{showKey ? <VisibilityOff /> : <Visibility />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t('apikey.actions.delete')}>
|
<Tooltip title={t('apikey.actions.delete')}>
|
||||||
|
|||||||
@@ -108,11 +108,39 @@ export const NavBar: React.FC<NavBarProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
dispatch({ type: "LOGOUT" });
|
try {
|
||||||
window.localStorage.removeItem("user");
|
const { data } = await axios.get(`${apiUrl}/auth/logout`);
|
||||||
const { data } = await axios.get(`${apiUrl}/auth/logout`);
|
if (data.ok) {
|
||||||
notify("success", data.message);
|
dispatch({ type: "LOGOUT" });
|
||||||
navigate("/login");
|
window.localStorage.removeItem("user");
|
||||||
|
notify('success', t('navbar.notifications.success.logout'));
|
||||||
|
navigate("/login");
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
const status = error.response?.status;
|
||||||
|
let errorKey = 'unknown';
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 401:
|
||||||
|
errorKey = 'unauthorized';
|
||||||
|
break;
|
||||||
|
case 500:
|
||||||
|
errorKey = 'server';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (error.message?.includes('Network Error')) {
|
||||||
|
errorKey = 'network';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notify(
|
||||||
|
'error',
|
||||||
|
t(`navbar.notifications.errors.logout.${errorKey}`, {
|
||||||
|
error: error.response?.data?.message || error.message
|
||||||
|
})
|
||||||
|
);
|
||||||
|
navigate("/login");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const goToMainMenu = async () => {
|
const goToMainMenu = async () => {
|
||||||
@@ -205,7 +233,9 @@ export const NavBar: React.FC<NavBarProps> = ({
|
|||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'flex-start',
|
justifyContent: 'flex-start',
|
||||||
}}>
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
onClick={() => navigate('/')}>
|
||||||
<img src={MaxunLogo} width={45} height={40} style={{ borderRadius: '5px', margin: '5px 0px 5px 15px' }} />
|
<img src={MaxunLogo} width={45} height={40} style={{ borderRadius: '5px', margin: '5px 0px 5px 15px' }} />
|
||||||
<div style={{ padding: '11px' }}><ProjectName mode={darkMode ? 'dark' : 'light'}>{t('navbar.project_name')}</ProjectName></div>
|
<div style={{ padding: '11px' }}><ProjectName mode={darkMode ? 'dark' : 'light'}>{t('navbar.project_name')}</ProjectName></div>
|
||||||
<Chip
|
<Chip
|
||||||
@@ -220,14 +250,16 @@ export const NavBar: React.FC<NavBarProps> = ({
|
|||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
|
||||||
{!isRecording ? (
|
{!isRecording ? (
|
||||||
<>
|
<>
|
||||||
<Button variant="outlined" onClick={handleUpdateOpen} sx={{
|
<IconButton onClick={handleUpdateOpen} sx={{
|
||||||
marginRight: '25px',
|
display: 'flex',
|
||||||
color: "#00000099",
|
alignItems: 'center',
|
||||||
border: "#00000099 1px solid",
|
borderRadius: '5px',
|
||||||
'&:hover': { color: '#ff00c3', border: '#ff00c3 1px solid' }
|
padding: '8px',
|
||||||
|
marginRight: '20px',
|
||||||
}}>
|
}}>
|
||||||
<Update sx={{ marginRight: '5px' }} /> {t('navbar.upgrade.button')}
|
<Update sx={{ marginRight: '5px' }} />
|
||||||
</Button>
|
<Typography variant="body1">{t('navbar.upgrade.button')}</Typography>
|
||||||
|
</IconButton>
|
||||||
<Modal open={open} onClose={handleUpdateClose}>
|
<Modal open={open} onClose={handleUpdateClose}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@@ -124,81 +124,113 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin
|
|||||||
}
|
}
|
||||||
}, [robot]);
|
}, [robot]);
|
||||||
|
|
||||||
const extractInitialCredentials = (workflow: any[]): Credentials => {
|
function extractInitialCredentials(workflow: any[]): Credentials {
|
||||||
const credentials: Credentials = {};
|
const credentials: Credentials = {};
|
||||||
|
|
||||||
// Helper function to check if a character is printable
|
|
||||||
const isPrintableCharacter = (char: string): boolean => {
|
const isPrintableCharacter = (char: string): boolean => {
|
||||||
return char.length === 1 && !!char.match(/^[\x20-\x7E]$/);
|
return char.length === 1 && !!char.match(/^[\x20-\x7E]$/);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Process each step in the workflow
|
|
||||||
workflow.forEach(step => {
|
workflow.forEach(step => {
|
||||||
if (!step.what) return;
|
if (!step.what) return;
|
||||||
|
|
||||||
// Keep track of the current input field being processed
|
|
||||||
let currentSelector = '';
|
let currentSelector = '';
|
||||||
let currentValue = '';
|
let currentValue = '';
|
||||||
let currentType = '';
|
let currentType = '';
|
||||||
|
let i = 0;
|
||||||
// Process actions in sequence to maintain correct text state
|
|
||||||
step.what.forEach((action: any) => {
|
while (i < step.what.length) {
|
||||||
if (
|
const action = step.what[i];
|
||||||
(action.action === 'type' || action.action === 'press') &&
|
|
||||||
action.args?.length >= 2 &&
|
if (!action.action || !action.args?.[0]) {
|
||||||
typeof action.args[1] === 'string'
|
i++;
|
||||||
) {
|
continue;
|
||||||
const selector: string = action.args[0];
|
}
|
||||||
const character: string = action.args[1];
|
|
||||||
const inputType: string = action.args[2] || '';
|
const selector = action.args[0];
|
||||||
|
|
||||||
// Detect `input[type="password"]`
|
// Handle full word type actions first
|
||||||
if (!currentType && inputType.toLowerCase() === 'password') {
|
if (action.action === 'type' &&
|
||||||
currentType = 'password';
|
action.args?.length >= 2 &&
|
||||||
|
typeof action.args[1] === 'string' &&
|
||||||
|
action.args[1].length > 1) {
|
||||||
|
|
||||||
|
if (!credentials[selector]) {
|
||||||
|
credentials[selector] = {
|
||||||
|
value: action.args[1],
|
||||||
|
type: action.args[2] || 'text'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
i++;
|
||||||
// If we're dealing with a new selector, store the previous one
|
continue;
|
||||||
if (currentSelector && selector !== currentSelector) {
|
}
|
||||||
if (!credentials[currentSelector]) {
|
|
||||||
|
// Handle character-by-character sequences (both type and press)
|
||||||
|
if ((action.action === 'type' || action.action === 'press') &&
|
||||||
|
action.args?.length >= 2 &&
|
||||||
|
typeof action.args[1] === 'string') {
|
||||||
|
|
||||||
|
if (selector !== currentSelector) {
|
||||||
|
if (currentSelector && currentValue) {
|
||||||
credentials[currentSelector] = {
|
credentials[currentSelector] = {
|
||||||
value: currentValue,
|
value: currentValue,
|
||||||
type: currentType
|
type: currentType || 'text'
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
credentials[currentSelector].value = currentValue;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Update current tracking variables
|
|
||||||
if (selector !== currentSelector) {
|
|
||||||
currentSelector = selector;
|
currentSelector = selector;
|
||||||
currentValue = credentials[selector]?.value || '';
|
currentValue = credentials[selector]?.value || '';
|
||||||
currentType = inputType || credentials[selector]?.type || '';
|
currentType = action.args[2] || credentials[selector]?.type || 'text';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle different types of key actions
|
const character = action.args[1];
|
||||||
if (character === 'Backspace') {
|
|
||||||
// Remove the last character when backspace is pressed
|
if (isPrintableCharacter(character)) {
|
||||||
currentValue = currentValue.slice(0, -1);
|
|
||||||
} else if (isPrintableCharacter(character)) {
|
|
||||||
// Add the character to the current value
|
|
||||||
currentValue += character;
|
currentValue += character;
|
||||||
|
} else if (character === 'Backspace') {
|
||||||
|
currentValue = currentValue.slice(0, -1);
|
||||||
}
|
}
|
||||||
// Note: We ignore other special keys like 'Shift', 'Enter', etc.
|
|
||||||
|
if (!currentType && action.args[2]?.toLowerCase() === 'password') {
|
||||||
|
currentType = 'password';
|
||||||
|
}
|
||||||
|
|
||||||
|
let j = i + 1;
|
||||||
|
while (j < step.what.length) {
|
||||||
|
const nextAction = step.what[j];
|
||||||
|
if (!nextAction.action || !nextAction.args?.[0] ||
|
||||||
|
nextAction.args[0] !== selector ||
|
||||||
|
(nextAction.action !== 'type' && nextAction.action !== 'press')) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (nextAction.args[1] === 'Backspace') {
|
||||||
|
currentValue = currentValue.slice(0, -1);
|
||||||
|
} else if (isPrintableCharacter(nextAction.args[1])) {
|
||||||
|
currentValue += nextAction.args[1];
|
||||||
|
}
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
|
||||||
|
credentials[currentSelector] = {
|
||||||
|
value: currentValue,
|
||||||
|
type: currentType
|
||||||
|
};
|
||||||
|
|
||||||
|
i = j;
|
||||||
|
} else {
|
||||||
|
i++;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
// Store the final state of the last processed selector
|
if (currentSelector && currentValue) {
|
||||||
if (currentSelector) {
|
|
||||||
credentials[currentSelector] = {
|
credentials[currentSelector] = {
|
||||||
value: currentValue,
|
value: currentValue,
|
||||||
type: currentType
|
type: currentType || 'text'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return credentials;
|
return credentials;
|
||||||
};
|
}
|
||||||
|
|
||||||
const groupCredentialsByType = (credentials: Credentials): GroupedCredentials => {
|
const groupCredentialsByType = (credentials: Credentials): GroupedCredentials => {
|
||||||
return Object.entries(credentials).reduce((acc: GroupedCredentials, [selector, info]) => {
|
return Object.entries(credentials).reduce((acc: GroupedCredentials, [selector, info]) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user