chrome-devtools-skill
npx skills add https://github.com/steelan9199/wechat-publisher --skill chrome-devtools-skill
Agent 安装分布
Skill 文档
Chrome DevTools Skill
ä½¿ç¨ Chrome DevTools Protocol è¿è¡ç½é¡µæ°æ®è·åååæã
工使µç¨ï¼AI èªå¨æ§è¡ï¼
å½ç¨æ·ä½¿ç¨æ¤ skill æ¶ï¼AI åºè¯¥èªå¨å®æä»¥ä¸æææ¥éª¤ï¼è䏿¯è®©ç¨æ·æå¨æ§è¡ï¼
1. èªå¨å¯å¨ Chrome è¿ç¨è°è¯
AI èªå¨æ£æµæä½ç³»ç»å¹¶å¯å¨ Chromeï¼
Windows:
start chrome --remote-debugging-port=9222 --user-data-dir="%TEMP%\chrome-devtools-profile" [URL]
éè¦æç¤º: 使ç¨åºå®ç
--user-data-dirï¼å¦chrome-devtools-profileï¼ï¼ä¸è¦æ¯æ¬¡ä½¿ç¨ä¸åçæä»¶å¤¹ãè¿æ · Chrome ä¼è®°ä½ç»å½ç¶æãCookie åç¼åï¼ä¸æ¬¡å¯å¨æ´å¿«ã
macOS:
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --user-data-dir="$TMPDIR/chrome-profile-stable" [URL]
Linux:
google-chrome --remote-debugging-port=9222 --user-data-dir="/tmp/chrome-profile-stable" [URL]
2. èªå¨å¯å¨ MCP æå¡å¨
AI èªå¨å¯å¨ MCP æå¡å¨ï¼å¨åå°è¿è¡ï¼ï¼
npx -y chrome-devtools-mcp@latest --browser-url=http://127.0.0.1:9222
3. æ§è¡ç¨æ·è¯·æ±çæä½
使ç¨ä»¥ä¸ MCP å·¥å ·å®æç¨æ·ä»»å¡ï¼
mcp__chrome-devtools__navigate_page– 导èªå°é¡µé¢mcp__chrome-devtools__wait_for– çå¾ é¡µé¢å è½½mcp__chrome-devtools__take_snapshot– è·å页é¢å¿«ç §mcp__chrome-devtools__evaluate_script– æ§è¡ JavaScript æåæ°æ®mcp__chrome-devtools__screenshot– æªåå±å¹æªå¾mcp__chrome-devtools__list_network_requests– ååºç½ç»è¯·æ±
æ åæä½æµç¨ï¼AI 使ç¨ï¼
å½ç¨æ·è¦æ±æå¼/åæ/æåæä¸ªç½é¡µæ¶ï¼AI æä»¥ä¸é¡ºåºæ§è¡ï¼
1. å¯å¨ Chromeï¼è°è¯æ¨¡å¼ï¼
2. å¯å¨ MCP æå¡å¨
3. 导èªå°ç®æ URL
4. çå¾
页é¢å è½½
5. æ§è¡ç¨æ·è¯·æ±çæä½ï¼æªå¾/æåæ°æ®/çæ§ç½ç»çï¼
6. è¿åç»æç»ç¨æ·
æ°æ®æåæå·§
æååè¡¨æ°æ®:
() => {
const items = document.querySelectorAll(".item");
return Array.from(items).map((item) => ({
title: item.querySelector(".title")?.innerText,
link: item.querySelector("a")?.href,
}));
};
æåè¡¨æ ¼æ°æ®:
() => {
const rows = document.querySelectorAll("table tr");
return Array.from(rows).map((row) =>
Array.from(row.querySelectorAll("td, th")).map((cell) => cell.innerText)
);
};
常è§ç¨ä¾
- æå¼ç½é¡µå¹¶æªå¾: å¯å¨ Chrome â å¯¼èª â æªå¾ â è¿åç»æ
- ååä¿¡æ¯æå: å¯¼èª â æ»å¨å è½½ â æåå表
- 表åèªå¨å: å¯¼èª â fill_form â click â wait_for
- API çæ§: å¯¼èª â list_network_requests â get_network_request
- æ§è½åæ: performance_start_trace â æä½ â performance_stop_trace
æä½³å®è·µ
- èªå¨å¤çææåç½®æ¥éª¤ – ä¸è¦è¦æ±ç¨æ·æå¨å¯å¨ Chrome æ MCP æå¡å¨
- 使ç¨åºå®çç¨æ·æ°æ®ç®å½ – ä¿æ
--user-data-dirä¸è´ï¼é¿å æ¯æ¬¡é½éæ°å è½½ - å
ç¨
take_snapshotäºè§£é¡µé¢ç»æ - 夿æåç¨
evaluate_scriptæ§è¡ JavaScript - ç¡®ä¿è¿å JSON å¯åºååçæ°æ®
- å¤ç卿å
容æ¶ä½¿ç¨
wait_for
ææ¯åç
Chrome DevTools Protocol (CDP) éä¿¡æ¹å¼
| æ¹å¼ | åè®® | ç¨é |
|---|---|---|
| HTTP | http://127.0.0.1:9222/json/list |
è·å页é¢å表ãåºæ¬ä¿¡æ¯ |
| WebSocket | ws://127.0.0.1:9222/devtools/page/{pageId} |
宿¶æ§å¶é¡µé¢ãæ§è¡èæ¬ |
WebSocket éä¿¡æµç¨
import asyncio
import websockets
import json
async def cdp_example():
# 1. è¿æ¥å° Chrome DevTools WebSocket
ws_url = "ws://127.0.0.1:9222/devtools/page/{pageId}"
async with websockets.connect(ws_url) as ws:
# 2. å¯ç¨ Runtime å
await ws.send(json.dumps({
"id": 1,
"method": "Runtime.enable"
}))
# 3. æ§è¡ JavaScript
await ws.send(json.dumps({
"id": 2,
"method": "Runtime.evaluate",
"params": {
"expression": "document.title",
"returnByValue": True
}
}))
# 4. å¾ªç¯æ¥æ¶ååºï¼å¹é
id
while True:
msg = await ws.recv()
data = json.loads(msg)
if data.get('id') == 2: # å¹é
è¯·æ± id
return data['result']['result']['value']
å ³é®æ³¨æäºé¡¹
- ååºå¹é
: WebSocket 伿¶å°å¤ç§æ¶æ¯ï¼
consoleAPICalledãexecutionContextCreatedçï¼ï¼å¿ é¡»éè¿idå¹é 请æ±åååº - çå¾
页é¢å è½½: æ§è¡æä½å使ç¨
await asyncio.sleep(2)æçå¾ ç¹å®å ç´ - ç¼ç é®é¢: Windows ç»ç«¯éè¦
sys.stdout.reconfigure(encoding='utf-8')æ¥æ£ç¡®æ¾ç¤ºä¸æ
宿ç»éªä¸è¸©åè®°å½
é®é¢ 1: Windows 䏿è¾åºä¹±ç
ç°è±¡: æåç䏿å
容æ¾ç¤ºä¸ºä¹±ç ææåº UnicodeEncodeError: 'gbk' codec can't encode character
åå : Windows ç»ç«¯é»è®¤ä½¿ç¨ GBK ç¼ç ï¼è Python è¾åº UTF-8 䏿æ¶ä¼å²çª
è§£å³: å¨ Python èæ¬å¼å¤´æ·»å ç¼ç é
ç½®ï¼
import sys
sys.stdout.reconfigure(encoding='utf-8')
éè¦: ææç¤ºä¾èæ¬ï¼baidu_search_example.pyãcdp_helper.pyï¼é½å·²æ·»å æ¤ç¼ç é
ç½®ï¼ç´æ¥è¿è¡ä¸ä¼äº§çä¹±ç ã
é®é¢ 2: HTML å符å®ä½è½¬ä¹
ç°è±¡: JavaScript 代ç ä¸ç => ç®å¤´å½æ°å¨éè¿ JSON ä¼ è¾æ¶è¢«è½¬ä¹ä¸º =>
åå : JSON æ åä¼å¯¹æäºå符è¿è¡ Unicode 转ä¹
è§£å³: è¿æ¯æ£å¸¸ç°è±¡ï¼ä¸å½±åå®é
æ§è¡ã妿éè¦å¨ Python ä¸å¤çï¼å¯ä»¥ä½¿ç¨ï¼
import codecs
decoded = codecs.decode(string, 'unicode_escape')
é®é¢ 3: é¡µé¢ ID è·å
ç°è±¡: ä¸ç¥éå½å页é¢ç WebSocket URL è§£å³: éè¿ HTTP æ¥å£è·å页é¢å表ï¼
curl -s http://127.0.0.1:9222/json/list
ç¶åæå webSocketDebuggerUrl åæ®µ
注æ: é¡µé¢ ID æ¯å¨æçæçï¼æ¯æ¬¡å¯å¨ Chrome é½ä¼ååãåºè¯¥å¨ä»£ç ä¸å¨æè·åè䏿¯ç¡¬ç¼ç ï¼
import urllib.request
import json
def get_ws_url():
req = urllib.request.Request('http://127.0.0.1:9222/json/list')
with urllib.request.urlopen(req) as response:
pages = json.loads(response.read().decode())
return pages[0]['webSocketDebuggerUrl'] if pages else None
é®é¢ 4: Windows çå¾ å½ä»¤è·¨å¹³å°å ¼å®¹æ§é®é¢
ç°è±¡: å¨ Git Bash ä¸ä½¿ç¨ timeout /t 3 /nobreak æ¥é invalid time interval
åå : timeout æ¯ Windows CMD å½ä»¤ï¼å¨ Bash ä¸è¯æ³ä¸å
¼å®¹
è§£å³: 使ç¨è·¨å¹³å°ççå¾
æ¹æ¡ï¼
# Windows CMD
timeout /t 3 /nobreak >nul
# Git Bash / Linux
sleep 3
# 跨平å°éç¨ï¼Windowsï¼
ping -n 4 127.0.0.1 > nul
é®é¢ 5: Claude Code æä»¶åå ¥éå¶
ç°è±¡: ç´æ¥ä½¿ç¨ Write å·¥å ·åå»ºæ°æä»¶æ¶æç¤º “File has not been read yet” åå : Claude Code è¦æ±å¿ é¡»å 读åæä»¶æè½åå ¥ï¼å®å ¨æºå¶ï¼ è§£å³:
- å ä½¿ç¨ Read å·¥å ·è¯»åæä»¶ï¼å¦æä¸åå¨ä¼æ¥éï¼éè¦ç¨ Bash å建空æä»¶ï¼
- æè
ä½¿ç¨ Bash ç
echoæcatå½ä»¤ç´æ¥åå ¥ - æè ä½¿ç¨ Python èæ¬å¨æçæå¹¶æ§è¡ä»£ç
é®é¢ 6: 卿å 容æåä¸å®æ´
ç°è±¡: æåçæç´¢ç»æä¸æäºå段ï¼å¦æè¦ï¼ä¸ºç©º åå :
- 页é¢ä½¿ç¨äºå¨æå è½½ï¼éè¦æ»å¨æè½æ¾ç¤ºå ¨é¨å 容
- ä¸åç½ç«ä½¿ç¨ä¸åç class åç§°
- æäºå 容æ¯å¼æ¥å è½½ç
è§£å³:
- 使ç¨å¤ä¸ªéæ©å¨å¹é ï¼
const abstract = item.querySelector('.c-abstract, .abstract, [class*="abstract"]')?.innerText;
- æ»å¨é¡µé¢å è½½æ´å¤å 容ï¼
window.scrollTo(0, document.body.scrollHeight);
- å¢å çå¾ æ¶é´ç¡®ä¿å¼æ¥å 容å è½½å®æ
é®é¢ 7: åå° vs å尿使µè§å¨
CDP æ¯æä¸¤ç§æä½æ¨¡å¼ï¼æ ¹æ®ç¨æ·éæ±éæ©ï¼
åå°æä½ï¼é»è®¤ï¼ï¼
- éè¿ WebSocket åé JavaScript å½ä»¤ç´æ¥æä½ DOM
- æµè§å¨çé¢ä¸ä¼æ¾ç¤ºæä½è¿ç¨ï¼ç¨æ·çä¸å°è¾å ¥ãç¹å»çå¨ä½ï¼
- éåæ°æ®æåãèªå¨åæµè¯çä¸éè¦å¯è§ååé¦çåºæ¯
- 代ç 示ä¾ï¼
# ç´æ¥è®¾ç½®è¾å
¥æ¡çå¼ï¼ç¨æ·çä¸å°è¾å
¥è¿ç¨ï¼
await ws.send(json.dumps({
"method": "Runtime.evaluate",
"params": {
"expression": "document.querySelector('#kw').value = 'ç¾å¥³';"
}
}))
åå°æä½ï¼å¯è§åï¼ï¼
- ä½¿ç¨ CDP ç Input 忍¡æçå®ç¨æ·è¾å ¥
- æµè§å¨ä¼æ¾ç¤ºå®æ´çæä½è¿ç¨ï¼é®çè¾å ¥ãé¼ æ ç¹å»çï¼
- éåæ¼ç¤ºãæå¦ãéè¦ç¨æ·è§å¯æä½è¿ç¨çåºæ¯
- 代ç 示ä¾ï¼
# 1. å
èç¦è¾å
¥æ¡
await ws.send(json.dumps({
"method": "Runtime.evaluate",
"params": {
"expression": "document.querySelector('#kw').focus();"
}
}))
# 2. 模æé®çè¾å
¥ï¼ç¨æ·è½çå°éåè¾å
¥ï¼
for char in "ç¾å¥³":
await ws.send(json.dumps({
"method": "Input.dispatchKeyEvent",
"params": {
"type": "char",
"text": char
}
}))
await asyncio.sleep(0.1) # 模æç宿åé´é
# 3. 模æç¹å»æç´¢æé®ï¼ç¨æ·è½çå°ç¹å»ææï¼
await ws.send(json.dumps({
"method": "Input.dispatchMouseEvent",
"params": {
"type": "mousePressed",
"x": 100,
"y": 200,
"button": "left"
}
}))
éæ©å»ºè®®ï¼
| åºæ¯ | æ¨èæ¨¡å¼ | åå |
|---|---|---|
| æ°æ®æå | åå° | é度快ï¼ä¸éè¦å¯è§å |
| èªå¨åæµè¯ | åå° | 稳å®å¯é ï¼ä¸åUIå½±å |
| æç´¢ç±»ä»»å¡ | åå° | ç´æ¥æé URLæ´ç®åå¯é |
| æä½æ¼ç¤º | åå° | ç¨æ·è½çå°å®æ´è¿ç¨ |
| æå¦æ¼ç¤º | åå° | 便äºè§å¯åçè§£ |
| 表åå¡«å | 两è çå¯ | åå°æ´å¿«ï¼åå°æ´ç´è§ |
æç´¢ç±»ä»»å¡æ¨èæ¹æ¡ï¼
- åå°æ¹å¼ï¼æ¨èï¼ï¼ç´æ¥æé æç´¢URLï¼å¦
https://www.baidu.com/s?wd=å ³é®è¯ - åå°æ¹å¼ï¼ä» å½ç¨æ·æç¡®è¦æ±”å¯è§å”ã”è½çå°æä½è¿ç¨”æ¶ä½¿ç¨
é®é¢ 8: æç´¢ç±»ä»»å¡çæä½³å®è·µ
åºæ¯: ç¨æ·è¦æ±”å¨ç¾åº¦/è°·ææç´¢ææå ³é®è¯å¹¶æåç»æ”
æ¹æ¡å¯¹æ¯:
| æ¹æ¡ | å®ç°æ¹å¼ | ä¼ç¹ | ç¼ºç¹ | æ¨è度 |
|---|---|---|---|---|
| A | åå°ç´æ¥è®¿é®æç´¢URL | ç®åãå¿«éãç¨³å® | ç¨æ·çä¸å°è¿ç¨ | âââââ |
| B | åå°æ¨¡æè¾å ¥ç¹å» | å¯è§åãæåé¦ | å¤æãæ ¢ãéå¤çåæ | âââ |
æ¨èæ¹æ¡ Aï¼åå°æ¹å¼ï¼ï¼
# ç´æ¥æé æç´¢URLï¼ä¸æ¥å°ä½
url = f"https://www.baidu.com/s?wd={urllib.parse.quote(keyword)}"
await client.navigate(url)
await asyncio.sleep(3) # çå¾
å è½½
results = await client.evaluate(extract_script)
使¶ä½¿ç¨æ¹æ¡ Bï¼åå°æ¹å¼ï¼ï¼
- ç¨æ·æç¡®è¦æ±”å¯è§åæä½”ã”让æçå°è¿ç¨”
- æå¦æ¼ç¤ºåºæ¯
- éè¦å±ç¤ºèªå¨åæä½è¿ç¨
åå°æ¹å¼æ³¨æäºé¡¹ï¼
- å¿
é¡»å
focus()è¾å ¥æ¡ - æåé´é 0.1-0.15 ç§æåé
- ç¾åº¦æç´ æé®åæ 约 x:650, y:230ï¼1920×1080å辨çï¼
- éè¦å¯ç¨ Input åï¼
Input.enable
åå°è¾å ¥æåè¯¦ç»æ¥éª¤ï¼ä»¥ç¾åº¦æç´¢æ¡ä¸ºä¾ï¼ï¼
# æ¥éª¤ 1: å¯ç¨ Input åï¼å¿
é¡»å
å¯ç¨æè½åéé®çäºä»¶ï¼
await ws.send(json.dumps({
"id": 1,
"method": "Input.enable"
}))
# æ¥éª¤ 2: èç¦è¾å
¥æ¡ï¼å
³é®æ¥éª¤ï¼å¦åé®çäºä»¶å¯è½æ æï¼
await ws.send(json.dumps({
"id": 2,
"method": "Runtime.evaluate",
"params": {
"expression": "document.querySelector('#kw').focus();", # '#kw' æ¯ç¾åº¦æç´¢æ¡ID
"returnByValue": True
}
}))
await asyncio.sleep(0.5) # çå¾
èç¦å®æ
# æ¥éª¤ 3: æ¸
空è¾å
¥æ¡ï¼å¯éï¼ç¡®ä¿è¾å
¥æ¡ä¸ºç©ºï¼
await ws.send(json.dumps({
"id": 3,
"method": "Runtime.evaluate",
"params": {
"expression": "document.querySelector('#kw').value = '';",
"returnByValue": True
}
}))
# æ¥éª¤ 4: éåè¾å
¥æåï¼ç¨æ·è½çå°éåè¾å
¥å¨ç»ï¼
text = "hello ä½ å¥½å"
for i, char in enumerate(text):
await ws.send(json.dumps({
"id": 10 + i,
"method": "Input.dispatchKeyEvent",
"params": {
"type": "char", # è¾å
¥å符
"text": char # è¦è¾å
¥çå符
}
}))
await asyncio.sleep(0.15) # æåé´éï¼è®©ç¨æ·è½çå°å¨ç»
å ³é®è¦ç¹ï¼
type: "char"ç¨äºè¾å ¥æ®éå符- å¿
é¡»å
focus()è¾å ¥æ¡ï¼å¦åè¾å ¥å¯è½æ æ - é´éæ¶é´å»ºè®® 0.1-0.2 ç§ï¼å¤ªçå¯è½æ¼åï¼å¤ªé¿ç¨æ·çå¾ ä¹
- æ¯æä¸æè¾å ¥ï¼æ éé¢å¤ç¼ç å¤ç
é®é¢ 9: 表åèªå¨åæ§è¡é¡ºåº
æ£ç¡®é¡ºåº:
- å¯ç¨ Runtime â 2. å¡«å è¾å ¥æ¡ â 3. ç¹å»æé® â 4. çå¾ é¡µé¢å è½½ â 5. æåæ°æ®
示ä¾ä»£ç ï¼ç¾åº¦æç´¢åå°æ¹å¼ï¼ï¼
# 1. å¡«å
æç´¢è¯
await ws.send(json.dumps({
"id": 2,
"method": "Runtime.evaluate",
"params": {
"expression": "document.querySelector('#kw').value = 'æç´¢è¯';",
"returnByValue": True
}
}))
# 2. ç¹å»æç´¢æé®
await ws.send(json.dumps({
"id": 3,
"method": "Runtime.evaluate",
"params": {
"expression": "document.querySelector('#su').click();",
"returnByValue": True
}
}))
# 3. çå¾
å è½½
await asyncio.sleep(3)
# 4. æåç»æ
await ws.send(json.dumps({
"id": 4,
"method": "Runtime.evaluate",
"params": {
"expression": """
(() => {
const items = document.querySelectorAll('.result');
return Array.from(items).map(item => ({
title: item.querySelector('h3')?.innerText,
link: item.querySelector('a')?.href
}));
})()
""",
"returnByValue": True
}
}))
ç¨æ·ä½¿ç¨æå
å¦ä½ä½¿ç¨æ¤ Skill
ç¨æ·å¯ä»¥éè¿ä»¥ä¸æ¹å¼è§¦åæ¤ skillï¼
- ç´æ¥è¯´æéæ± – æè¿°ä½ æ³å¯¹ç½é¡µåä»ä¹
- 使ç¨
/chrome-devtools-skillå½ä»¤ – æ¾å¼è°ç¨ skill - æåç½é¡µæä½ – 任使¶å”æå¼ç½é¡µãæåæ°æ®ãæªå¾”çå ³é®è¯
æç¤ºè¯ç¤ºä¾
以䏿¯ä¸äºç¨æ·å¯ä»¥ä½¿ç¨çæç¤ºè¯æ¨¡æ¿ï¼
åºç¡æä½
æå¼ https://www.example.com å¹¶æªå¾
æå https://www.example.com ç页é¢å
容
åæä¸ä¸è¿ä¸ªç½é¡µï¼https://github.com/xxx/xxx
æ°æ®æå
æå¼ https://www.jd.comï¼æåé¦é¡µææååæ é¢åä»·æ ¼
è®¿é® https://www.zhihu.com/exploreï¼è·åçé¨é®é¢å表
æå https://www.example.com 页é¢ä¸çææé¾æ¥
çæ§ååæ
æå¼ https://www.example.comï¼çæ§é¡µé¢å è½½çææ API 请æ±
åæ https://www.example.com ç页颿§è½
è®¿é® https://www.example.comï¼æå页é¢ä¸çææå¾çå°å
èªå¨åæä½
æå¼ https://www.example.comï¼å¨æç´¢æ¡è¾å
¥"å
³é®è¯"å¹¶æç´¢
è®¿é® https://www.example.comï¼ç¹å»"å è½½æ´å¤"æé®ï¼æåææåè¡¨æ°æ®
æç¤ºè¯æå·§
- æç¡® URL – æä¾å®æ´çç½é¡µå°å
- 说æç®æ – æ¸ æ¥æè¿°ä½ æ³è·åä»ä¹æ°æ®ææ§è¡ä»ä¹æä½
- æå®æ ¼å¼ – 妿éè¦ç¹å®æ ¼å¼ï¼å¯ä»¥è¯´æï¼å¦”ä»¥è¡¨æ ¼å½¢å¼è¿å”ï¼
- 夿任å¡åæ¥ – 对äºå¤æ¥éª¤ä»»å¡ï¼å¯ä»¥åæ¥æè¿°
示ä¾å¯¹è¯
ç¨æ·: 帮ææå¼ç¾åº¦ï¼çççææ¦æä»ä¹å 容
AI: [èªå¨å¯å¨ Chrome â æå¼ç¾åº¦ â æåçææ°æ® â è¿åç»æ]
ç¨æ·: æå https://news.ycombinator.com çå 10 æ¡æ°é»æ é¢å龿¥
AI: [èªå¨å¯å¨ Chrome â æå¼ Hacker News â æåæ°é»æ°æ® â è¿åè¡¨æ ¼]
ç¨æ·: åæä¸ä¸æ·å®é¦é¡µé½å è½½äºåªäºæ¥å£
AI: [èªå¨å¯å¨ Chrome â æå¼æ·å® â çæ§ç½ç»è¯·æ± â è¿å API å表]
åèèµæº
- 示ä¾èæ¬: è§
scripts/ç®å½baidu_search_example.py– ç¾åº¦æç´¢å®æ´ç¤ºä¾cdp_helper.py– CDP 客æ·ç«¯å°è£ ç±»README.md– å¿«éå¼å§æå
Chrome DevTools MCP é 置信æ¯
å¦æç¨æ·è¯¢é®chrome-devtools MCPæä¹é
ç½®ï¼æä¾ä»¥ä¸ JSON é
置信æ¯ï¼
{
"mcpServers": {
"chrome-devtools": {
"command": "npx",
"args": [
"chrome-devtools-mcp@latest",
"--browser-url=http://127.0.0.1:9222"
]
}
}
}
// æµè¯ä¿®æ¹ // å¦ä¸ä¸ªæµè¯ä¿®æ¹ // æµè¯ä¿®æ¹ pre-push æ¹æ¡