This commit is contained in:
张成
2026-03-17 16:40:51 +08:00
commit db1d70a46e
13 changed files with 915 additions and 0 deletions

40
1.md Normal file
View File

@@ -0,0 +1,40 @@
核心目标: 通过监控全平台Amazon, TikTok, Temu, Shopee的核心数据利用实时曲线和趋势预测模型实现“发现爆款 -> 数据验证 -> 决策跟卖”的闭环。
---
1. 业务需求范围
1.1 覆盖平台与站点
- Amazon: 全球主流站US, UK, DE, JP
- TikTok Shop: 东南亚及美区(侧重短视频带货趋势)。
- Temu: 全球站(侧重价格战与低价爆款)。
- Shopee: 东南亚市场(侧重品类精细化运营)。
1.2 监控核心类目(户外生活/Outdoor Lifestyle
- 核心类目: 野餐包、帐篷、地垫、户外产品、防水包。
- 扩展逻辑: 支持基于关键词和类目 ID 的自定义扩展。
---
2. 核心功能需求
2.1 多维数据抓取模块 (Data Scraper)
- TOP榜单抓取 每日/每小时抓取 Bestsellers、New Releases、Movers & Shakers 榜单。
- List/品类详情: 提取产品标题、价格、SKU、品牌、变体、上架时间、物流方式。
- 舆情监控: 抓取售后留言和评论Reviews需支持提取“差评关键词”以发现现有产品的痛点。
- 流量监控: TikTok 关联视频点赞数、热度趋势Amazon 搜索排名BSR波动。
2.2 BI 数据处理与分析 (Processing & BI)
- 实时曲线绘制: * BSR 曲线: 监控产品排名的历史波动,判断是“昙花一现”还是“稳步上升”。
- 价格曲线: 监控价格变动Temu 价格挤压或 Amazon 季节性调价)。
- 销量预测曲线: 基于排名和库存变动,拟合预测未来 7-30 天的销量。
- BI 嗅探逻辑:
- 竞品对标: 自动匹配相似款,计算同质化程度。
- 舆情聚合: 利用 NLP 聚类,提炼该品类下消费者最不满意的 3 个点(如:防水包不防漏、帐篷难搭建),作为跟卖后的迭代改进方向。
2.3 趋势预测与决策辅助 (Trend Engine)
- 趋势判定算法:
- 潜伏期识别: 识别刚上架且排名异常快速上升的产品Potential Winners
- 爆发力模型: 结合 TikTok 热度指数 + 平台搜索量增长率。
- 决策建议:
- 跟卖指数: 综合竞争程度、利润空间和供应链难度,给出品类跟卖分数。
- 生命周期预警: 提示该品类是否已进入红海或衰退期(如户外产品明显的季节性)。
---
3. 预期成果 (Outcome)
- 可视化看板: 每天自动推送一份“户外品类异动日报”。
- 爆款预警: 当监测到“防水包”类目下某新品 3 天内排名上升超过 50% 时,系统触发钉钉/邮件预警。
- 决策支持: 提供该款产品的“避坑指南”(基于竞品售后差评分析)。

View File

@@ -0,0 +1,138 @@
// zhipu_query_position_page按你原项目的回调风格实现参考 @zhipu.js 15-29
import { openTab } from '../libs/tabs.js';
import { execute_script } from '../libs/inject.js';
function injected_zhipu_query_position_page() {
// 运行在 content 隔离环境:监听页面派发的 __REQUEST_DONE
// 命中接口后用 chrome.runtime.sendMessage 回传给 background
const action = 'zhipu_query_position_page';
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
function send(action_name, data) {
try {
chrome.runtime.sendMessage({ type: 'push', action: action_name, data });
} catch (e) {
// ignore
}
}
if (!window.__zhipu_job_posts_listener__) {
window.__zhipu_job_posts_listener__ = true;
window.addEventListener('__REQUEST_DONE', (event) => {
const detail = event && event.detail ? event.detail : {};
const url = detail.url || '';
if (typeof url === 'string' && url.includes('/api/v1/search/job/posts')) {
send('zhipu_job_posts', detail.json);
}
});
}
async function auto_flip_pages() {
await sleep(1500);
while (true) {
const next_li = document.querySelector('.atsx-pagination-next');
if (!next_li) {
send(action, { is_end: true });
return;
}
const disabled =
next_li.getAttribute('aria-disabled') === 'true' ||
next_li.classList.contains('atsx-pagination-disabled');
if (disabled) {
send(action, { is_end: true });
return;
}
const next_btn = next_li.querySelector('.atsx-pagination-item-link');
if (!next_btn) {
send(action, { is_end: true });
return;
}
next_btn.click();
await sleep(2000);
}
}
if (!location.href.startsWith('https://zhipu-ai.jobs.feishu.cn/referral/position')) {
send(action, { error: '未定位到智谱职位列表页', url: location.href });
return;
}
auto_flip_pages().catch((err) => {
send(action, { error: (err && err.message) || String(err) });
});
}
export function zhipu_query_position_page(data, sendResponse) {
return new Promise((resolve, reject) => {
const url = 'https://zhipu-ai.jobs.feishu.cn/referral/position';
let times = 0;
openTab({
url,
latest: false,
top: 20,
left: 20,
width: 1440,
height: 900,
target: '__zhipu_query_position_page',
tabError(tab, details) {
const result = {
code: 30,
status: false,
message: details && (details.message || details.statusLine || details.error) || 'tab error',
data: null,
documentURI: details && details.url,
};
sendResponse && sendResponse({ action: 'zhipu_query_position_page', data: result });
sendResponse && sendResponse.log && sendResponse.log(result);
if (tab && tab.remove) {
tab.remove(1500);
}
reject(new Error(result.message));
},
tabUpdated(tab, details) {
if (times > 0) return;
times += 1;
const tab_id = tab && tab.id;
if (!tab_id) {
return reject(new Error('tab.id 为空'));
}
execute_script(tab_id, injected_zhipu_query_position_page, 'document_idle')
.then(() => {
const result = { code: 0, status: true, message: 'ok', data: { tab_id, url } };
sendResponse && sendResponse({ action: 'zhipu_query_position_page', data: result });
sendResponse && sendResponse.log && sendResponse.log(result);
// 这里不自动关 tab方便你调试要关就打开下一行
// tab.remove(3500);
resolve({ tab_id, url });
})
.catch((err) => {
if (tab && tab.remove) {
tab.remove(500);
}
reject(err);
});
},
});
});
}

View File

@@ -0,0 +1,10 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>mv2_simple_crx background</title>
</head>
<body>
<script type="module" src="index.js"></script>
</body>
</html>

View File

@@ -0,0 +1,88 @@
import { zhipu_query_position_page } from '../actions/zhipu.js';
const actions = {
zhipu_query_position_page,
};
function create_action_send_response(sender) {
const fn = (payload) => {
emit_ui_event('push', { type: 'reply', ...payload, sender });
};
fn.log = (payload) => {
emit_ui_event('push', { type: 'log', action: 'log', data: payload, sender });
};
return fn;
}
const ui_page_url = chrome.runtime.getURL('ui/index.html');
function log() {
// eslint-disable-next-line no-console
console.log.apply(console, ['[mv2_simple_crx]'].concat([].slice.call(arguments)));
}
function emit_ui_event(event_name, payload) {
chrome.runtime.sendMessage({
channel: 'ui_event',
event_name,
payload,
ts: Date.now(),
});
}
chrome.browserAction.onClicked.addListener(() => {
chrome.tabs.create({ url: ui_page_url, active: true });
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (!message) {
return;
}
// UI 自己发出来的事件background 不处理
if (message.channel === 'ui_event') {
return;
}
// content -> background 的推送消息(通用)
if (message.type === 'push') {
emit_ui_event('push', {
type: 'push',
action: message.action,
data: message.data,
sender,
});
return;
}
// UI -> background 的 action 调用
if (!message.action) {
sendResponse && sendResponse({ ok: false, error: '缺少 action' });
return;
}
const fn = actions[message.action];
if (!fn) {
sendResponse({ ok: false, error: '未知 action: ' + message.action });
return;
}
const request_id = `${Date.now()}_${Math.random().toString().slice(2)}`;
emit_ui_event('request', { type: 'request', request_id, action: message.action, data: message.data || {}, sender });
const action_send_response = create_action_send_response(sender);
Promise.resolve()
.then(() => fn(message.data || {}, action_send_response))
.then((res) => {
emit_ui_event('response', { type: 'response', request_id, ok: true, data: res, sender });
sendResponse({ ok: true, data: res, request_id });
})
.catch((err) => {
const error = (err && err.message) || String(err);
emit_ui_event('response', { type: 'response', request_id, ok: false, error, sender });
sendResponse({ ok: false, error, request_id });
});
return true;
});

View File

@@ -0,0 +1,166 @@
// content script在 document_start inline 注入 RequestWatcher
// 目标:页面里触发 XHR/fetch 时派发 __REQUEST_DONE
(() => {
function inject_inline(fn) {
const el = document.createElement('script');
el.type = 'text/javascript';
el.textContent = `(${fn.toString()})();`;
(document.head || document.documentElement).appendChild(el);
el.parentNode && el.parentNode.removeChild(el);
}
function request_watcher() {
const F = window.Function.__proto__;
if (F.__RequestWatcher) return;
const __addEventListener = F.__addEventListener = F.__addEventListener || window.addEventListener;
const __xhr_open = F.__xhr_open = F.__xhr_open || window.XMLHttpRequest.prototype.open;
const __xhr_send = F.__xhr_send = F.__xhr_send || window.XMLHttpRequest.prototype.send;
const __fetch = F.__fetch = F.__fetch || window.fetch;
if (F.__fetch) {
F.__fetch = F.__fetch.bind(window);
}
const tagList = [];
__addEventListener('__REQUEST_TAG', function (event) {
if (event && event.detail && event.detail.TAG) {
tagList.push(event.detail.TAG);
}
});
function onOpen(method, url, async, user, password) {
this.__detail = {};
if (tagList.length) this.__detail.TAG = tagList.pop();
if (method !== undefined) this.__detail.method = method;
if (url !== undefined) this.__detail.url = url;
if (async !== undefined) this.__detail.async = async;
if (user !== undefined) this.__detail.user = user;
if (password !== undefined) this.__detail.password = password;
return __xhr_open.apply(this, arguments);
}
function onSend(data) {
this.__detail = this.__detail || {};
this.__detail.data = data;
this.addEventListener('readystatechange', onReadyStateChange);
return __xhr_send.apply(this, arguments);
}
function onReadyStateChange() {
if (this.readyState === 4) {
let detail = { ...this.__detail };
delete this.__detail;
detail.url = this.responseURL || detail.url || null;
detail.readyState = this.readyState;
detail.status = this.status;
detail.statusText = this.statusText;
detail.ok = this.status >= 200 && this.status < 300;
detail.text = null;
detail.json = null;
detail.responseContentType = null;
try {
detail.text = this.responseText || null;
detail.responseContentType = this.getResponseHeader('content-type')?.toLowerCase() || null;
if (
detail.responseContentType &&
(detail.responseContentType.includes('application/json') || detail.responseContentType.includes('text/json'))
) {
detail.json = JSON.parse(detail.text || 'null');
}
} catch (ex) {
// ignore
}
// 关键:只派发纯数据,避免跨上下文被裁剪导致“看起来不一样”
const payload = {
TAG: detail.TAG,
method: detail.method,
url: detail.url,
status: detail.status,
statusText: detail.statusText,
ok: detail.ok,
responseContentType: detail.responseContentType,
text: detail.text,
json: detail.json,
data: detail.data,
};
window.dispatchEvent(new CustomEvent('__REQUEST_DONE', { detail: payload }));
detail = null;
}
}
window.XMLHttpRequest.prototype.open = onOpen;
window.XMLHttpRequest.prototype.send = onSend;
// fetch用 Proxy避免 this/参数形态问题)
if (typeof __fetch === 'function') {
window.fetch = new Proxy(__fetch, {
apply(target, thisArg, args) {
const [url, options] = args;
let detail = {};
if (tagList.length) detail.TAG = tagList.pop();
if (url !== undefined) detail.url = url;
if (options && typeof options === 'object') {
detail.options = options;
if (options.method !== undefined) detail.method = options.method;
if (options.body !== undefined) detail.data = options.body;
}
return target(...args)
.then(async (response) => {
detail.ok = true;
detail.message = 'ok';
const cloneResponse = response.clone();
detail.status = cloneResponse.status;
detail.statusText = cloneResponse.statusText;
detail.redirected = cloneResponse.redirected;
detail.url = cloneResponse.url;
detail.text = await cloneResponse.text();
detail.response = response.clone();
detail.responseContentType = response.headers.get('Content-Type')?.toLowerCase();
if (
detail.responseContentType &&
(detail.responseContentType.includes('application/json') || detail.responseContentType.includes('text/json'))
) {
detail.json = await response.clone().json();
}
return response;
})
.catch((error) => {
detail.ok = false;
detail.error = error;
detail.message = error.message;
throw error;
})
.finally(() => {
const payload = {
TAG: detail.TAG,
method: detail.method,
url: detail.url,
status: detail.status,
statusText: detail.statusText,
ok: detail.ok,
responseContentType: detail.responseContentType,
text: detail.text,
json: detail.json,
data: detail.data,
};
window.dispatchEvent(new CustomEvent('__REQUEST_DONE', { detail: payload }));
detail = null;
});
},
});
}
F.__RequestWatcher = true;
}
inject_inline(request_watcher);
})();

View File

@@ -0,0 +1,49 @@
// executeScriptMV2 使用 chrome.tabs.executeScript
function normalize_code(code) {
if (typeof code === 'function') {
return `(${code.toString()})();`;
}
return code;
}
export function execute_script(tab_id, code, run_at) {
run_at = run_at || 'document_idle';
code = normalize_code(code);
return new Promise((resolve, reject) => {
chrome.tabs.executeScript(
tab_id,
{
code,
runAt: run_at,
},
(result) => {
if (chrome.runtime.lastError) {
return reject(new Error(chrome.runtime.lastError.message));
}
resolve(result);
},
);
});
}
export function inject_file(tab_id, file, run_at) {
run_at = run_at || 'document_idle';
return new Promise((resolve, reject) => {
chrome.tabs.executeScript(
tab_id,
{
file,
runAt: run_at,
},
() => {
if (chrome.runtime.lastError) {
return reject(new Error(chrome.runtime.lastError.message));
}
resolve(true);
},
);
});
}

View File

@@ -0,0 +1,78 @@
// openTabMV2 版本(极简 + 回调风格)
function attach_tab_helpers(tab) {
if (!tab) return tab;
tab.remove = function remove(delay_ms) {
delay_ms = Number.isFinite(delay_ms) ? delay_ms : 0;
setTimeout(() => {
chrome.tabs.remove(tab.id, () => void 0);
}, Math.max(0, delay_ms));
};
return tab;
}
export function open_tab(url, options) {
// 保留原本 Promise 版本(内部复用)
options = options && typeof options === 'object' ? options : {};
return new Promise((resolve, reject) => {
chrome.tabs.create(
{
url: 'about:blank',
active: options.active !== false,
},
(tab) => {
if (chrome.runtime.lastError) {
return reject(new Error(chrome.runtime.lastError.message));
}
if (!tab || !tab.id) {
return reject(new Error('tab 创建失败'));
}
const tab_id = tab.id;
const on_updated = (updated_tab_id, change_info, updated_tab) => {
if (updated_tab_id !== tab_id) return;
if (change_info.status !== 'complete') return;
chrome.tabs.onUpdated.removeListener(on_updated);
resolve({ tab_id, tab: attach_tab_helpers(updated_tab) });
};
chrome.tabs.onUpdated.addListener(on_updated);
chrome.tabs.update(tab_id, { url });
},
);
});
}
export function close_tab(tab_id, delay_ms) {
delay_ms = Number.isFinite(delay_ms) ? delay_ms : 0;
setTimeout(() => {
chrome.tabs.remove(tab_id, () => void 0);
}, Math.max(0, delay_ms));
}
// 兼容你原来的调用风格openTab({ url, ..., tabError(tab, details), tabUpdated(tab, details) })
export function openTab(options) {
options = options && typeof options === 'object' ? options : {};
const url = options.url;
const tabError = typeof options.tabError === 'function' ? options.tabError : () => void 0;
const tabUpdated = typeof options.tabUpdated === 'function' ? options.tabUpdated : () => void 0;
if (!url) {
tabError(null, { error: 'url 不能为空' });
return;
}
open_tab(url, { active: options.active !== false })
.then(({ tab }) => {
tabUpdated(tab, { status: 'complete' });
})
.catch((err) => {
tabError(null, { error: err.message || String(err) });
});
}

View File

@@ -0,0 +1,28 @@
{
"manifest_version": 2,
"name": "mv2_simple_crx",
"version": "0.1.0",
"description": "MV2 极简骨架openTab + executeScript + __REQUEST_DONE 监听",
"permissions": [
"tabs",
"<all_urls>"
],
"background": {
"page": "background/index.html",
"persistent": true
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content/content.js"],
"run_at": "document_start",
"all_frames": false
}
],
"web_accessible_resources": [
"content/request_watcher.js"
],
"browser_action": {
"default_title": "mv2_simple_crx"
}
}

View File

@@ -0,0 +1,18 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>mv2_simple_crx</title>
<style>
body { font-family: Arial, "Microsoft YaHei"; margin: 12px; }
button { width: 100%; padding: 10px 12px; cursor: pointer; }
pre { white-space: pre-wrap; word-break: break-word; background: #f6f6f6; padding: 8px; }
</style>
</head>
<body>
<button id="btn">打开智谱职位页并监听</button>
<pre id="out"></pre>
<script src="popup.js"></script>
</body>
</html>

View File

@@ -0,0 +1,16 @@
const out = document.getElementById('out');
const btn = document.getElementById('btn');
function set_out(obj) {
out.textContent = typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2);
}
btn.addEventListener('click', () => {
set_out('执行中...');
chrome.runtime.sendMessage(
{ action: 'zhipu_query_position_page', data: {} },
(res) => {
set_out(res);
},
);
});

View File

@@ -0,0 +1,158 @@
:root {
--bg: #0b1220;
--card: #121b2d;
--card2: #0f1728;
--text: #e7eefc;
--muted: #9db0d1;
--border: rgba(255, 255, 255, 0.08);
--primary: #4f8cff;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", Arial, sans-serif;
background: radial-gradient(1200px 700px at 20% 10%, rgba(79, 140, 255, 0.25), transparent 60%),
radial-gradient(900px 600px at 90% 20%, rgba(167, 139, 250, 0.18), transparent 55%),
var(--bg);
color: var(--text);
}
.app {
max-width: 1200px;
margin: 0 auto;
padding: 18px;
}
.header {
padding: 14px 14px 10px;
}
.title {
font-size: 20px;
font-weight: 700;
}
.sub {
margin-top: 6px;
color: var(--muted);
font-size: 12px;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
.card {
border: 1px solid var(--border);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.02));
backdrop-filter: blur(8px);
border-radius: 12px;
overflow: hidden;
}
.card_title {
padding: 12px 14px;
border-bottom: 1px solid var(--border);
background: rgba(0, 0, 0, 0.15);
font-weight: 600;
font-size: 13px;
}
.form {
padding: 14px;
}
.label {
display: block;
margin: 10px 0 6px;
color: var(--muted);
font-size: 12px;
}
.input,
.textarea {
width: 100%;
border: 1px solid var(--border);
background: rgba(0, 0, 0, 0.25);
color: var(--text);
border-radius: 10px;
padding: 10px 10px;
outline: none;
}
.textarea {
min-height: 160px;
resize: vertical;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 12px;
line-height: 1.5;
}
.row {
display: flex;
gap: 10px;
margin-top: 12px;
}
.btn {
border: 1px solid var(--border);
background: rgba(0, 0, 0, 0.25);
color: var(--text);
border-radius: 10px;
padding: 10px 12px;
cursor: pointer;
font-weight: 600;
}
.btn.primary {
border-color: rgba(79, 140, 255, 0.45);
background: rgba(79, 140, 255, 0.18);
}
.btn:hover {
border-color: rgba(255, 255, 255, 0.18);
}
.hint {
margin-top: 12px;
color: var(--muted);
font-size: 12px;
line-height: 1.5;
}
.hint code {
color: #c7d2fe;
}
.pre {
margin: 0;
padding: 14px;
background: rgba(0, 0, 0, 0.22);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 12px;
line-height: 1.5;
min-height: 260px;
color: #dbeafe;
}
.pre_scroll {
max-height: 520px;
overflow: auto;
}
.span2 {
grid-column: 1 / span 2;
}
@media (max-width: 980px) {
.grid {
grid-template-columns: 1fr;
}
.span2 {
grid-column: 1 / span 1;
}
}

View File

@@ -0,0 +1,54 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>mv2_simple_crx 控制台</title>
<link rel="stylesheet" href="index.css" />
</head>
<body>
<div class="app">
<div class="header">
<div class="title">mv2_simple_crx 控制台</div>
<div class="sub">选择 action填参数执行并查看响应/推送事件</div>
</div>
<div class="grid">
<div class="card">
<div class="card_title">调用</div>
<div class="form">
<label class="label">方法名action</label>
<select id="action_name" class="input">
<option value="zhipu_query_position_page">zhipu_query_position_page</option>
</select>
<label class="label">参数JSON</label>
<textarea id="action_params" class="textarea" spellcheck="false">{}</textarea>
<div class="row">
<button id="btn_run" class="btn primary">执行</button>
<button id="btn_clear" class="btn">清空日志</button>
</div>
<div class="hint">
说明:执行后会显示「响应」,同时如果页面内监听到 <code>__REQUEST_DONE</code> 命中接口,会在「推送事件」里持续追加。
</div>
</div>
</div>
<div class="card">
<div class="card_title">响应</div>
<pre id="last_response" class="pre"></pre>
</div>
<div class="card span2">
<div class="card_title">推送事件(实时)</div>
<pre id="event_log" class="pre pre_scroll"></pre>
</div>
</div>
</div>
<script src="index.js"></script>
</body>
</html>

View File

@@ -0,0 +1,72 @@
const action_name_el = document.getElementById('action_name');
const action_params_el = document.getElementById('action_params');
const btn_run_el = document.getElementById('btn_run');
const btn_clear_el = document.getElementById('btn_clear');
const last_response_el = document.getElementById('last_response');
const event_log_el = document.getElementById('event_log');
function now_time() {
const d = new Date();
const pad = (n) => String(n).padStart(2, '0');
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
function safe_json_parse(text) {
try {
return JSON.parse(text);
} catch (e) {
return { __parse_error: e.message, __raw: text };
}
}
function set_last_response(obj) {
last_response_el.textContent = JSON.stringify(obj, null, 2);
}
function append_event_line(obj) {
const line = `[${now_time()}] ${JSON.stringify(obj)}`;
event_log_el.textContent = (event_log_el.textContent ? event_log_el.textContent + '\n' : '') + line;
event_log_el.scrollTop = event_log_el.scrollHeight;
}
btn_run_el.addEventListener('click', () => {
const action = action_name_el.value;
const params = safe_json_parse(action_params_el.value || '{}');
append_event_line({ type: 'call', action, params });
set_last_response({ running: true, action, params });
chrome.runtime.sendMessage({ action, data: params }, (res) => {
set_last_response(res);
append_event_line({ type: 'response', action, res });
});
});
btn_clear_el.addEventListener('click', () => {
last_response_el.textContent = '';
event_log_el.textContent = '';
});
// background -> UI 的推送
chrome.runtime.onMessage.addListener((message) => {
if (!message || message.channel !== 'ui_event') {
return;
}
if (message.event_name === 'push') {
append_event_line({ type: 'push', payload: message.payload });
return;
}
if (message.event_name === 'request') {
append_event_line({ type: 'request', payload: message.payload });
return;
}
if (message.event_name === 'response') {
append_event_line({ type: 'bg_response', payload: message.payload });
return;
}
});
append_event_line({ type: 'ready', hint: '点击右侧/上方执行按钮开始' });