Hướng dẫn gửi hành động thông qua socket của EzyChat đến chrome extension để thực thi một nhiệm vụ bất kỳ
Back to ezychatEzyChat có thể sử dụng kết nối socket để chuyển một workflow từ backend tới Chrome extension EzyConnector. Extension nhận workflow, thực thi tuần tự các thao tác trên trình duyệt rồi gửi kết quả ngược về backend.
Một workflow có thể bao gồm các hành động như:
- Mở hoặc tìm một tab.
- Chuyển tab hiện tại tới URL khác.
- Chờ một phần tử xuất hiện.
- Điền dữ liệu vào input hoặc textarea.
- Nhấn nút hoặc gửi phím Enter.
- Tải tệp từ URL rồi gán vào file input.
- Chờ một khoảng thời gian.
- Dừng workflow khi phát hiện trạng thái lỗi trên trang.
Cần lưu ý rằng “nhiệm vụ bất kỳ” ở đây không có nghĩa extension thực thi JavaScript tùy ý do server gửi xuống. EzyConnector chỉ thực thi các command đã được đăng ký sẵn. Đây là ranh giới an toàn quan trọng của thiết kế.
Kiến trúc tổng quát
Luồng xử lý gồm ba thành phần:
sequenceDiagram
participant Backend as EzyChat backend
participant Socket as EzyChat socket application
participant Extension as EzyConnector
participant Browser as Trang web trên Chrome
Extension->>Socket: Kết nối và đăng nhập bằng access token
Backend->>Socket: pushConnectorRequest
Socket->>Extension: connectorRequest
Extension->>Browser: Thực thi các command
Browser-->>Extension: Kết quả từng command
Extension->>Socket: connectorResponse
Socket-->>Backend: Kết quả workflow
EzyChat backend không kết nối trực tiếp với Chrome extension. Backend gửi yêu cầu tới socket application, còn socket application định tuyến yêu cầu tới phiên đăng nhập của quản trị viên tương ứng.
Kết quả cũng đi theo chiều ngược lại qua socket application.
requestId được dùng để ghép kết quả với request đang chờ ở backend.Chuẩn bị EzyConnector
EzyConnector là Chrome extension sử dụng Manifest V3. Sau khi cài extension, người dùng đăng nhập bằng:
- URL trang quản trị.
- Tài khoản quản trị.
- Mật khẩu.
Extension thực hiện đăng nhập HTTP để lấy access token, sau đó lấy WebSocket URL từ hệ thống và mở kết nối đến:
- Zone:
chat - Application:
ezychat
Trong bước đăng nhập socket, access token được gửi qua trường
adminAccessToken.
Khi truy cập application thành công, extension chuyển sang trạng thái
connected và có thể nhận workflow.Do service worker của Manifest V3 có thể bị Chrome tạm dừng khi không hoạt động, extension sử dụng alarm định kỳ để thử kết nối lại. Socket client cũng được cấu hình reconnect khi kết nối bị gián đoạn.
Trước khi gửi workflow, backend có thể kiểm tra xem quản trị viên đích có phiên socket đang hoạt động hay không. Nếu không có extension tương ứng đang trực tuyến, workflow không thể được chuyển tới trình duyệt và lời gọi phía backend có thể hết thời gian chờ.
Cấu trúc một workflow
Dữ liệu nghiệp vụ mà EzyChat backend gửi tới socket có dạng:
{
"adminId": 123,
"type": "workflow.execute",
"payload": {
"version": 1,
"commands": [
{
"name": "tab.ensure",
"args": {
"url": "https://example.com/tasks",
"urlPattern": "https://example.com/*",
"active": true
},
"saveAs": "taskTab"
},
{
"name": "dom.fill",
"args": {
"tabId": "",
"selector": "textarea[name="content"]",
"value": "Nội dung cần gửi"
}
},
{
"name": "dom.click",
"args": {
"tabId": "",
"selector": "button[type="submit"]"
}
}
]
}
}
Các trường chính:
| Trường | Ý nghĩa |
|---|---|
adminId | Quản trị viên đang chạy EzyConnector và sẽ nhận workflow |
type | Loại nhiệm vụ; hiện extension hỗ trợ workflow.execute |
payload.version | Phiên bản giao thức workflow; hiện tại là 1 |
payload.commands | Danh sách command được thực thi tuần tự |
name | Tên command |
args | Tham số của command |
saveAs | Tên dùng để lưu kết quả command và tham chiếu ở bước sau |
Khi chuyển tiếp xuống extension, socket application tạo thông điệp:
{
"id": "1a",
"type": "workflow.execute",
"payload": {
"version": 1,
"commands": []
}
}
Trong đó
id là mã request do backend sinh ra. Extension phải giữ nguyên mã này khi trả kết quả.Gửi workflow từ EzyChat backend
Ở phía Java, nên đóng gói workflow thành một
EzyObject, sau đó gửi qua connector manager của EzyChat.Ví dụ dưới đây mở một trang, nhập dữ liệu rồi bấm nút gửi:
EzyArray commands = EzyEntityFactory.newArray();
commands.add(
EzyEntityFactory.newObjectBuilder()
.append("name", "tab.ensure")
.append(
"args",
EzyEntityFactory.newObjectBuilder()
.append("url", "https://example.com/tasks")
.append("urlPattern", "https://example.com/*")
.append("active", true)
.build()
)
.append("saveAs", "taskTab")
.build()
);
commands.add(
EzyEntityFactory.newObjectBuilder()
.append("name", "dom.wait")
.append(
"args",
EzyEntityFactory.newObjectBuilder()
.append("tabId", "")
.append("selector", "textarea[name="content"]")
.append("timeoutMs", 10000)
.build()
)
.build()
);
commands.add(
EzyEntityFactory.newObjectBuilder()
.append("name", "dom.fill")
.append(
"args",
EzyEntityFactory.newObjectBuilder()
.append("tabId", "")
.append("selector", "textarea[name="content"]")
.append("value", "Nội dung cần gửi")
.build()
)
.build()
);
commands.add(
EzyEntityFactory.newObjectBuilder()
.append("name", "dom.click")
.append(
"args",
EzyEntityFactory.newObjectBuilder()
.append("tabId", "")
.append("selector", "button[type="submit"]")
.build()
)
.build()
);
EzyObject connectorRequest =
EzyEntityFactory.newObjectBuilder()
.append("adminId", adminId)
.append("type", "workflow.execute")
.append(
"payload",
EzyEntityFactory.newObjectBuilder()
.append("version", 1)
.append("commands", commands)
.build()
)
.build();
Map<String, Object> result =
connectorManager.pushConnectorRequest(
connectorRequest,
true,
15
);
Ba tham số của lời gọi cuối có ý nghĩa:
-
connectorRequest: nội dung workflow. -
true: cho phép chia request thành nhiều fragment. -
15: thời gian tối đa chờ extension phản hồi, tính bằng giây.
Nên dùng chế độ fragment khi workflow có nội dung dài, nhiều selector, nhiều command hoặc danh sách URL tệp. Backend hiện chia dữ liệu đã serialize thành các fragment 16 KiB trước khi gửi.
Nếu payload nhỏ, có thể gửi không phân mảnh:
Map<String, Object> result =
connectorManager.pushConnectorRequest(
connectorRequest,
15
);
Định tuyến request qua socket
Backend gửi request bằng command socket:
pushConnectorRequest
Socket application thực hiện các bước:
- Xác thực người gửi là tài khoản connector được phép phát workflow.
- Kiểm tra
requestId. - Ghép các fragment nếu request được gửi phân mảnh.
- Kiểm tra
adminIdvàtype. - Tìm phiên socket của quản trị viên tương ứng.
- Chuyển yêu cầu tới extension bằng command:
connectorRequest
Thông điệp nhận bởi extension có dạng:
{
"id": "1a",
"type": "workflow.execute",
"payload": {
"version": 1,
"commands": [...]
}
}
Việc tách command phía backend và command phía extension giúp socket application đóng vai trò router. Extension không cần biết request ban đầu được tạo bởi API, scheduled job hay một service nghiệp vụ khác.
Extension thực thi workflow như thế nào?
Service worker của EzyConnector đăng ký handler cho
connectorRequest.Khi nhận được thông điệp hợp lệ, extension:
- Kiểm tra request có
idvàtype. - Chuyển request tới dispatcher.
- Dispatcher chỉ chấp nhận
type = workflow.execute. - Workflow executor kiểm tra
versionvà danh sách command. - Các command được thực thi tuần tự theo đúng thứ tự.
- Kết quả từng command được lưu lại.
- Nếu tất cả command thành công, extension trả kết quả thành công.
- Nếu một command phát sinh lỗi, workflow dừng ngay và trả thông báo lỗi.
Extension giới hạn tối đa 100 command trong một workflow.
Các command hiện được hỗ trợ
Quản lý tab
tab.ensure tìm một tab phù hợp, tạo tab nếu chưa tồn tại hoặc chuyển tab hiện tại tới URL yêu cầu.
{
"name": "tab.ensure",
"args": {
"url": "https://example.com/tasks",
"urlPattern": "https://example.com/*",
"active": true,
"timeoutMs": 15000
},
"saveAs": "page"
}
Kết quả chứa thông tin tương tự:
{
"id": 42,
"windowId": 7,
"url": "https://example.com/tasks"
}
Các command quản lý tab khác:
-
tab.create -
tab.update -
tab.reload -
tab.remove
Chờ phần tử
{
"name": "dom.wait",
"args": {
"tabId": "",
"selector": "#task-form",
"timeoutMs": 10000
}
}
Command này lặp lại việc tìm selector cho đến khi phần tử xuất hiện hoặc hết thời gian chờ.
Nhập dữ liệu
{
"name": "dom.fill",
"args": {
"tabId": "",
"selector": "textarea[name="content"]",
"value": "Xin chào",
"minBeforeTypeDelayMs": 100,
"maxBeforeTypeDelayMs": 300,
"minCharacterDelayMs": 40,
"maxCharacterDelayMs": 100
}
}
Ngoài việc gán giá trị, extension phát các DOM event như
input, change, keydown và keyup. Điều này giúp thao tác tương thích tốt hơn với các trang sử dụng framework frontend.Nhấn phần tử hoặc gửi phím
{
"name": "dom.click",
"args": {
"tabId": "",
"selector": "button[type="submit"]"
}
}
Hoặc:
{
"name": "dom.keypress",
"args": {
"tabId": "",
"selector": "textarea[name="content"]",
"key": "Enter"
}
}
Chèn độ trễ
{
"name": "delay",
"args": {
"tabId": "",
"minDurationMs": 350,
"maxDurationMs": 900
}
}
Có thể sử dụng
durationMs cho thời gian cố định hoặc cặp minDurationMs và maxDurationMs cho thời gian ngẫu nhiên.Nên truyền
tabId để timer chạy trong tab. Timer của service worker có thể bị Chrome trì hoãn khi worker không hoạt động.Tải và upload tệp
{
"name": "dom.uploadRemoteFiles",
"args": {
"tabId": "",
"selector": "input[type="file"]",
"triggerSelector": ".upload-button",
"urls": [
"https://cdn.example.com/image.jpg"
],
"timeoutMs": 10000
}
}
Extension tải tệp, chuyển tệp thành đối tượng
File, gán vào file input rồi phát input và change.Các giới hạn hiện tại:
- URL tệp phải dùng HTTPS.
- HTTP chỉ được chấp nhận với localhost.
- Mỗi tệp không được vượt quá 10 MiB.
Chờ một trong nhiều trạng thái
dom.waitAny phù hợp khi trang có thể trả về trạng thái thành công hoặc lỗi:
{
"name": "dom.waitAny",
"args": {
"tabId": "",
"timeoutMs": 15000,
"waitForChangeMs": 500,
"stableMs": 300,
"conditions": [
{
"selector": ".notification-success"
},
{
"selector": ".notification-error",
"errorMessage": "Trang web từ chối yêu cầu"
}
]
}
}
Nếu điều kiện có
errorMessage được thỏa mãn, workflow kết thúc với lỗi.Ngoài ra,
dom.assertTextAbsent có thể dùng để dừng workflow nếu một vùng trên trang chứa nội dung không mong muốn.Tham chiếu kết quả giữa các command
Khi một command có:
{
"saveAs": "page"
}
kết quả của command được lưu dưới tên
page. Command tiếp theo có thể tham chiếu một trường bằng cú pháp:Ví dụ:
{
"name": "dom.click",
"args": {
"tabId": "",
"selector": "#submit"
}
}
Tham chiếu có thể là toàn bộ giá trị:
{
"tabId": ""
}
hoặc được chèn vào một chuỗi:
{
"value": "Tab hiện tại là "
}
Tên
saveAs phải bắt đầu bằng chữ cái và chỉ chứa chữ cái, chữ số hoặc dấu gạch dưới.Nếu không khai báo
saveAs, kết quả vẫn được lưu bằng chỉ số của command, nhưng đặt tên rõ ràng giúp workflow dễ đọc và ít phụ thuộc vào thứ tự hơn.Nhận kết quả tại backend
Khi workflow hoàn thành, extension gửi:
connectorResponse
Kết quả thành công:
{
"id": "1a",
"ok": true,
"data": {
"page": {
"id": 42,
"windowId": 7,
"url": "https://example.com/tasks"
},
"1": {
"found": true
},
"2": {
"filled": true
},
"3": {
"clicked": true
}
}
}
Kết quả thất bại:
{
"id": "1a",
"ok": false,
"error": "Element not found: button[type="submit"]"
}
Socket application sử dụng
id để chuyển kết quả về đúng request backend đang chờ. Phía Java nhận kết quả dưới dạng Map<String, Object>:
Map<String, Object> result =
connectorManager.pushConnectorRequest(
connectorRequest,
true,
15
);
boolean successful =
Boolean.TRUE.equals(result.get("ok"));
if (!successful) {
String error = String.valueOf(result.get("error"));
// Xử lý lỗi nghiệp vụ hoặc ghi log.
}
ok = true chỉ cho biết extension đã thực thi toàn bộ workflow mà không phát sinh exception. Nó không tự động chứng minh nghiệp vụ trên website đã thành công.Ví dụ, thao tác bấm nút có thể hoàn thành nhưng website vẫn trả về lỗi. Vì vậy workflow quan trọng nên có thêm bước
dom.waitAny hoặc một command xác minh trạng thái cuối.Ví dụ workflow hoàn chỉnh
Workflow sau mở một trang quản trị công việc, nhập tiêu đề và nội dung, gửi biểu mẫu rồi chờ kết quả:
{
"adminId": 123,
"type": "workflow.execute",
"payload": {
"version": 1,
"commands": [
{
"name": "tab.ensure",
"args": {
"url": "https://example.com/tasks/new",
"urlPattern": "https://example.com/tasks/*",
"active": true
},
"saveAs": "taskPage"
},
{
"name": "dom.wait",
"args": {
"tabId": "",
"selector": "form#task-form",
"timeoutMs": 10000
}
},
{
"name": "dom.fill",
"args": {
"tabId": "",
"selector": "input[name="title"]",
"value": "Kiểm tra dữ liệu"
}
},
{
"name": "dom.fill",
"args": {
"tabId": "",
"selector": "textarea[name="description"]",
"value": "Nhiệm vụ được gửi từ EzyChat"
}
},
{
"name": "dom.click",
"args": {
"tabId": "",
"selector": "button[type="submit"]"
}
},
{
"name": "dom.waitAny",
"args": {
"tabId": "",
"timeoutMs": 15000,
"waitForChangeMs": 500,
"stableMs": 300,
"conditions": [
{
"selector": ".alert-success"
},
{
"selector": ".alert-danger",
"errorMessage": "Không thể tạo nhiệm vụ"
}
]
},
"saveAs": "submission"
}
]
}
}
Bảo mật
Cơ chế này cho phép extension thao tác trực tiếp trên các website mà người dùng đang đăng nhập, vì vậy cần xem workflow là dữ liệu có đặc quyền cao.
Các biện pháp quan trọng gồm:
- Chỉ cho tài khoản connector chuyên dụng gửi workflow.
- Định tuyến workflow theo
adminId, không nhận danh tính đích từ extension. - Chỉ hỗ trợ các command nằm trong registry.
- Không cung cấp command thực thi JavaScript tùy ý.
- Luôn sử dụng HTTPS và WSS ngoài môi trường localhost.
- Kiểm tra URL và selector ở backend trước khi tạo workflow.
- Không đưa access token, mật khẩu hoặc dữ liệu nhạy cảm vào log.
- Giới hạn số command, timeout, kích thước fragment và kích thước tệp.
- Chỉ cài extension từ nguồn tin cậy.
- Thu hẹp
host_permissionsnếu môi trường triển khai chỉ làm việc với một số domain cố định.
Extension hiện chạy các thao tác DOM trong
MAIN world. Vì vậy command có thể tương tác với DOM và JavaScript context của trang. Khả năng này cần thiết cho automation nhưng cũng làm tăng mức độ đặc quyền của extension.
Xử lý lỗi và timeout
Một workflow có thể thất bại tại nhiều lớp:
| Lớp | Ví dụ |
|---|---|
| Backend | Dữ liệu đầu vào không hợp lệ |
| Socket application | Người gửi không phải connector hợp lệ |
| Định tuyến | Không tìm thấy phiên extension của adminId |
| Kết nối | Extension mất kết nối trước khi nhận request |
| Dispatcher | type không được hỗ trợ |
| Workflow | Sai version hoặc quá 100 command |
| Command | Sai tên command hoặc thiếu tham số |
| Trình duyệt | Không tìm thấy tab hoặc phần tử |
| Website đích | DOM thay đổi, phiên đăng nhập hết hạn |
| Phản hồi | Backend hết timeout trước khi workflow hoàn thành |
Timeout phía backend nên lớn hơn tổng thời gian tối đa của workflow. Ví dụ, nếu workflow có hai bước chờ 10 giây và một bước chờ kết quả 15 giây, timeout 15 giây ở backend là không đủ trong trường hợp xấu nhất.
Nên tính timeout theo công thức gần đúng:
timeout backend > tổng timeout của các command + tổng delay + thời gian tải trang + khoảng dự phòng cho mạng và socket
Workflow nên được thiết kế có giới hạn rõ ràng. Tránh các bước chờ vô hạn hoặc chuỗi delay dài vì backend đang giữ một future để chờ phản hồi.
Mở rộng để thực thi nhiệm vụ mới
Nếu nhiệm vụ mới có thể biểu diễn bằng các command hiện có, chỉ cần tạo workflow mới ở backend. Không cần phát hành lại extension.
Ví dụ:
- Gửi biểu mẫu:
tab.ensure→dom.fill→dom.click. - Gửi tin nhắn:
tab.ensure→dom.wait→dom.fill→dom.keypress. - Upload ảnh:
tab.ensure→dom.uploadRemoteFiles. - Kiểm tra trạng thái:
dom.waitAnyhoặcdom.assertTextAbsent.
Nếu nhiệm vụ cần một khả năng hoàn toàn mới, cần bổ sung command handler vào extension. Quy trình phù hợp là:
- Định nghĩa tên command và cấu trúc
args. - Thêm handler vào command registry.
- Kiểm tra chặt chẽ kiểu dữ liệu và giới hạn tham số.
- Trả về kết quả có cấu trúc.
- Bảo đảm handler phát sinh exception khi thao tác thất bại.
- Chỉ sau đó mới tạo workflow sử dụng command mới ở backend.
Không nên thêm một command tổng quát kiểu
javascript.execute. Các command hẹp như dom.scroll, tab.capture hoặc cookie.read dễ kiểm soát quyền, kiểm tra dữ liệu và ghi log an toàn hơn.
Kết luận
EzyChat và EzyConnector sử dụng mô hình request–response có định danh để biến socket thành một kênh điều khiển trình duyệt:
- Backend mô tả nhiệm vụ bằng workflow.
- Socket application xác thực và định tuyến theo quản trị viên.
- Extension thực thi các command tuần tự.
- Kết quả hoặc lỗi được trả về đúng request ban đầu.
Ưu điểm chính của thiết kế này là logic nghiệp vụ và selector nằm ở backend, trong khi extension giữ vai trò một runtime automation tổng quát. Phần lớn nhiệm vụ mới có thể được triển khai bằng cách tạo workflow mới mà không cần sửa extension, miễn là nhiệm vụ đó có thể ghép từ các command đã được đăng ký.