Chrome extension

Mình từng nhìn thấy một video trên Tiktok hay là Facebook gì đó đại loại nội dung video là trong 1 phòng thi thì có một sinh viên sử dụng một chương trình nào đó mình không rõ sử dụng chức năng chụp ảnh màn hình phần câu hỏi và ngay sau đó có luôn đáp án hiển thị và người đó nhanh chóng hoàn thành bài thi. Trông rất là tà đạo và ma giáo nhưng mình cũng khá thích ý tưởng đó nên mình cũng muốn làm 1 cái cho riêng mình. Sau đây thì mình sẽ kể lại quá trình phát triển 1 extension nhỏ như cái mắt muỗi mà gian nan hết nấc.
Mình cũng từng cùng 1 người anh làm 1 ứng dụng có sử dụng AI bằng 1 cái api chọc lậu nhưng giờ không dùng được nữa vì nó đã được làm bảo mật hơn, nó cũng không hẳn là vấn đề vì định hướng của ông ấy là không bỏ ra 1 đồng nào còn mình thì cũng đã tìm ra cách để bypass api đó lần 2 rồi, chỉ là mất hơi nhiều thời gian của mình và nó vẫn không khả thi trong định hướng của ông ý nên là mình dùng cho extension lần này của mình.
Mình dù đã từng làm chơi chơi được 1 vài cái extension nhưng không hề tìm hiểu chuyên sâu về nó, chỉ cố gắng code cho xong và chạy được thôi cho nên lần này mình muốn code 1 cách cẩn thận và tử tế hơn nên chắc chắn phải code bằng Typescript, mình đã lên Youtube search cách tạo Chrome Extension bằng typescript thì đương nhiên là cũng có kết quả, họ hướng dẫn tạo bằng Vite, demo bằng cách sửa trang defaut popup thêm 1 cái button click bắn ra thông báo, code thì typescript khá đúng ý mình rồi nên mình bắt đầu bắt tay vào project github của dự án đó vào xem code, nói chung tưởng chừng thuận lợi nhưng mà vẫn chẳng như mơ, rắc rối kéo đến ngay sau đó.
Giới thiệu qua 1 chút thì 1 project chrome extension nó có cấu trúc gồm các file
- manifest.json: Đây là file cấu hình chính của extension, nơi chứa các thông tin cần thiết như tên, phiên bản, quyền truy cập, và các thành phần khác của extension.
- content script: Đây là các script được gắn vào trang web mà người dùng truy cập. Chúng cho phép bạn tương tác với nội dung của trang và thực hiện các thay đổi cần thiết.
- background script: Đây là các script chạy ngầm, không liên kết trực tiếp với giao diện người dùng. Chúng có thể xử lý các tác vụ như quản lý trạng thái, lắng nghe các sự kiện, và thực hiện các chức năng mà không cần sự tương tác trực tiếp từ người dùng.
- default_popup: Đây là giao diện hiển thị khi người dùng nhấp vào biểu tượng của extension trên thanh công cụ của trình duyệt. Nó thường chứa thông tin hoặc các tùy chọn mà người dùng có thể tương tác.
Ý tưởng của cái extension mình sắp làm cùng đơn giản thôi, chọn 1 phần text trên trang web sau đó sẽ hiện lên 1 toolbox nhỏ có chứa vài chức năng như dịch, tóm tắt, trả lời câu hỏi và 1 chức năng cho nhập promt tự do. Sau khi chọn xong chức năng sẽ call api đến cái api lậu mà mình có, sau đó nó sẽ thực hiện yêu cầu và trả về kết quả cho mình.
Cái vấn đề mình gặp phải đầu tiên muốn nói chính là cái ví dụ mà người ta hướng dẫn cho mình thì nó chỉ là phần defaut_popup thôi, nhưng tiện ích của mình làm thì 90% nó đều xử lý hết trên content script, không lẽ mình lại phải viết hết code bằng javascript ư? may quá không phải khổ thế vì mình thấy trong project github kia họ có cấu hình phần vite.config có phần build
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
input:{
background: resolve(__dirname, 'background.ts'),
popup: resolve(__dirname, "index.html"),
content: resolve(__dirname, "content.ts"),
},
output:{
entryFileNames:'[name].js',
assetFileNames: 'assets/[name].[ext]' // Xuất file CSS riêng
}
}
}
})
ít nhất thì không còn phải viết code thuần js nữa vì mình rất sợ type của js nó tùm lum fix rất mệt, chỉ có vấn đề là mình không làm được bằng React Component mà phải viết DOM query hơi khổ.
Mình đã thực sự làm xong hết toàn bộ giao diện trong 1 buổi chiều, tưởng ngon ăn lắm rồi cho đến khi ghép api vào thì mới nhận ra 1 vấn đề nghiêm trọng nữa là chrome manifest v3 nó hoàn toàn chặn hàm eval, hàm đó là hàm mà mình thực sự cần để giải mã script function mà api trả về, thử rất nhiều cách như dùng iframe vì mình search bằng Chatgpt nó kêu thực thi trong sandbox được nên là mình lại phải viết lại code gửi mã từ background ra đến iframe, nói thì bằng 1 câu nhưng nếu ai thực sự làm rồi thì sẽ hiểu nó rất là lòng vòng..... Mất công mất sức chán chê đủ các kiểu cuối cùng Chatgpt nó kêu mình là không có cách nào -.- nghe mà muốn đấm cho vài cái vì cách nó đưa ra, làm xong rồi không được thì mới kêu không có cách nào khác, tưởng chừng dự án kết thúc thì mình phát hiện ra manifest v2 có hỗ trợ hàm eval và phải xin quyền.

Chạy thành công mĩ mãn luôn, nhưng vì chuyển về v2 nên mình lại tự tạo cho mình thêm 1 vấn đề khác đó chính là Google Chrome nó không support manifest v2 nữa cho nên nó luôn cảnh báo, tự động tắt extension của mình đi thực sự rất phiền, đồng thời mình cũng đối mặt với 1 cái vấn đề khác nữa, vì mình inject css và html vào trang web nên nó đồng nghĩa với việc global css của trang cũng sẽ ảnh hưởng tới các phần của mình, ví dụ font chữ, font size to nhỏ, màu nền đủ các vấn đề...
Lại hỏi Chatgpt nó đưa ra 2 giải pháp 1 là dùng iframe, 2 là dùng shadow root, mình lại viết lại code của mình nhưng làm xong thì nhận ra việc gửi và nhận dữ liệu giữa extension và iframe nó rất khó, rất là cồng kềnh, sau đó lại phải sửa lại code về dùng shadow, vẫn là lại nói mồm thì nhanh chứ sửa đi sửa lại mệt bở hơi tai. Đến tận thời điểm này thì code của mình mới đem ra cho tester đi kiểm thử phiên bản đầu tiên.

Vậy đó, từ ngày 13/4 (sinh nhật mình :V) đến ngày 22/4 mình mới hoàn thành được giai đoạn 1 đó mới là gồm làm cho api hoạt động và chỉnh sửa giao diện để hoàn thiện ở mức chạy được
Sau đó mình chuyển sang giai đoạn 2, ở giai đoạn này mình muốn làm thêm chức năng chụp ảnh màn hình thì thì nó hoàn toàn không thể thực hiện đơn thuần trên trình duyệt được nữa mà cần có 1 backend để thực hiện OCR, mình chuyển hết request về BE của mình và mình sẽ dùng BE này để gọi api đến AI, đồng nghĩa với việc giải quyết được vấn đề eval ở trình duyệt và nâng lại manifest version lên 3. Xử lý BE này thì đơn giản hơn xíu nên không gặp vấn đề gì lắm, cái vấn đề thực sự mà mình gặp phải là việc cắt ảnh ở phía extension thôi.
Tại phiên bản cut ảnh đầu tiên nó cũng hoạt động khá là ổn, khá là ổn vì lúc ý mình cũng chưa nhận ra được vấn đề thôi, mình gửi ảnh về BE, cắt ảnh theo vùng chọn, OCR đọc chữ trong ảnh, trả chữ về client, dùng kết quả đó gửi đến BE tiếp để cho thực thi yêu cầu tới AI. vấn đề nằm ở chỗ buổi sáng mình code trên máy công ty độn phân giải full hd, tối về dùng máy của mình độ phân giải 2k, scale 200%, cái bước cắt ảnh sai tùm lum, sửa công thức đi đi lại lại bao nhiêu lần mới đúng.
Mình nhận ra được là ở FE dùng canvas cut ảnh luôn được không cần phải dùng đến BE nên mình sửa lại luôn vì nếu cắt ở BE thì sẽ phải gửi full ảnh gốc lên kèm toạ độ bắt đầu, width, height phần cut, độ phân giải màn hình cao thì phần gửi lên BE sẽ rất nặng, cắt trước mới gửi thì sẽ nhẹ hơn nhiều, sau đó gửi ảnh lên BE thì chỉ còn mỗi bước OCR nữa là xong.

Trong giai đoạn này mình hoàn thành thêm cả move và resize popup
Sang giai đoạn 3, mình nhận ra mình hoàn toàn có thể code file content.ts, background.ts bằng react và thậm chí còn chả cần thiết phải dùng đến vite, đầu óc quay cuồng choáng váng luôn, làm bao nhiêu lâu thì cũng nhận ra được cái sự thật nhảm nhí này.
Mình cấu trúc lại dự án viết 1 file script để copy những file tĩnh trong thư mục public gồm file manifest.json và hình ảnh vào thư mục dist, build file scss sang css và đưa vào dist, build file ts sang js đại loại như sau
const sourceDir = path.join(__dirname , "src", 'public');
const destDir = path.join(__dirname, 'dist');
const cssAssetDir = path.join(__dirname, 'dist', 'assets');
const popupScss = path.join(__dirname, 'src', 'css', 'popup.scss');
const contentScss = path.join(__dirname, 'src', 'css', 'content.scss');
// Sao chép thư mục tĩnh
function copyDirectory(source: string, destination: string) {
if (!fs.existsSync(destination)) {
fs.mkdirSync(destination, { recursive: true });
}
const files = fs.readdirSync(source);
files.forEach(file => {
const curSource = path.join(source, file);
const curDest = path.join(destination, file);
if (fs.lstatSync(curSource).isDirectory()) {
copyDirectory(curSource, curDest); // Đệ quy sao chép thư mục
} else {
fs.copyFileSync(curSource, curDest); // Sao chép file
console.log("Copied file:", curDest);
}
});
}
function buildSass(cssPath: string, fileName: string) {
if (!fs.existsSync(cssAssetDir)) {
fs.mkdirSync(cssAssetDir, { recursive: true });
}
const result = sass.compile(cssPath);
fs.writeFileSync(path.join(__dirname, 'dist', 'assets', `${fileName}.css`), result.css);
console.log("Builded css:", path.join(__dirname, 'dist', 'assets', `${fileName}.css`));
}
copyDirectory(sourceDir, destDir);
buildSass(popupScss, 'popup');
buildSass(contentScss, 'content');
await build({
entrypoints: ['src/popup.tsx', 'src/background.ts', "src/content.tsx"],
outdir: 'dist',
target: 'browser',
minify: true,
});

Phần build ra rất gọn gàng mà giờ không phải phụ thuộc vào cái vite nữa, cần gì tự build đó luôn.
content script hiện tại được viết lại bằng react, chia hết ra các component nhỏ xíu để mình dễ quản lý code hơn
return (
<>
<link rel="stylesheet" href={chrome.runtime.getURL("assets/content.css")}/>
<PopupMenu runCustomPrompt={runCustomPrompt}/>
<IconBox handleIconClick={handleIconClick}/>
<ScreenshotCanvas />
</>
);

Trong phần này mình chọn bun vì mình lười cài thêm package thôi chứ muốn build thì cài thêm esbuild cũng được, hơn nữa do ban đầu mình định bundle sang file exe để giấu mã nguồn backend của mình nữa.
Sau khoảng 1 tháng từ 13/4 đến 13/5 tính cả nghỉ lễ :V, cuối cùng mình cũng hoàn thiện được extension AI của mình, kể ra cũng học được rất nhiều thứ dù chả phải là dự án gì ghê gớm lắm nhưng nói chung khá là đáng nhớ.
Đây là demo AI, tuy không đúng 100 % nhưng mà nó cũng giúp được trả lời nhanh :D