├── .github
├── FUNDING.yml
└── workflows
│ └── main.yml
├── .gitignore
├── .vscode
└── settings.json
├── README.en.md
├── README.md
├── README.zh.md
├── cc-plugin.config.ts
├── cc-plugin.json
├── crx
├── crx-key.pem
└── crx.js
├── doc
├── Marquee.png
├── chrome-message.png
├── icon128.png
├── prop
│ └── v2.png
├── scene1.png
├── scene2.png
├── scene3.png
├── scene4.png
├── scene5.png
├── small.png
└── v2.png
├── hello-chrome
├── .vscode
│ └── settings.json
├── background.js
├── content.js
├── devtools.html
├── devtools.js
├── icons
│ └── 48.png
├── index.html
├── index.js
├── inject.js
├── manifest.json
├── options.html
├── options.js
├── popup.html
├── popup.js
└── readme.md
├── icons
└── 48.png
├── others.md
├── package.json
├── src
├── core
│ ├── types.ts
│ └── util.ts
├── ga
│ ├── index.ts
│ └── type.ts
├── i18n
│ ├── en.ts
│ └── zh.ts
├── main.ts
├── panel
│ ├── index.ts
│ ├── index.vue
│ └── res
│ │ ├── chrome.png
│ │ └── edge.png
├── scripts
│ ├── background
│ │ ├── content.ts
│ │ ├── devtools.ts
│ │ ├── index.ts
│ │ ├── notify.ts
│ │ ├── tabInfo.ts
│ │ └── tabMgr.ts
│ ├── const.ts
│ ├── content
│ │ └── index.ts
│ ├── inject-view
│ │ ├── ad.vue
│ │ ├── app.vue
│ │ ├── banner.vue
│ │ ├── const.ts
│ │ ├── github.ts
│ │ ├── loader.ts
│ │ ├── memory-draw.ts
│ │ ├── memory.vue
│ │ ├── res
│ │ │ ├── close.svg
│ │ │ ├── left.svg
│ │ │ └── right.svg
│ │ ├── shortkeys.vue
│ │ ├── store.ts
│ │ ├── util.ts
│ │ └── web-test.ts
│ ├── inject
│ │ ├── break.ts
│ │ ├── code-hint.ts
│ │ ├── connect-me.ts
│ │ ├── enumConfig.ts
│ │ ├── event.ts
│ │ ├── everything.ts
│ │ ├── hint
│ │ │ ├── adapter.ts
│ │ │ ├── hint-v2.ts
│ │ │ ├── hint-v3.ts
│ │ │ └── index.ts
│ │ ├── index.ts
│ │ ├── inject-view.ts
│ │ ├── inspect-list.ts
│ │ ├── inspector.ts
│ │ ├── setValue.ts
│ │ ├── types.ts
│ │ └── util.ts
│ └── terminal.ts
└── views
│ ├── devtools
│ ├── bridge.ts
│ ├── bus.ts
│ ├── comp
│ │ └── index.ts
│ ├── const.ts
│ ├── contextMenu.ts
│ ├── data.ts
│ ├── everything.vue
│ ├── find.vue
│ ├── game-info.vue
│ ├── hierarchy.vue
│ ├── index.ts
│ ├── index.vue
│ ├── inspector.vue
│ ├── refresh.vue
│ ├── register-panel.ts
│ ├── store.ts
│ ├── timer.ts
│ ├── ui
│ │ ├── property-engine.vue
│ │ ├── property-group.vue
│ │ ├── property-image.vue
│ │ ├── propertys.vue
│ │ ├── settings.vue
│ │ └── ui-prop.vue
│ └── util.ts
│ ├── global.less
│ ├── options
│ ├── index.ts
│ ├── index.vue
│ └── res
│ │ └── money.png
│ └── popup
│ ├── index.ts
│ ├── index.vue
│ ├── res
│ ├── 30.jpg
│ ├── friend.png
│ ├── github.png
│ ├── qq.png
│ ├── star.png
│ └── tiezi.png
│ ├── tool-item.vue
│ ├── tool.vue
│ └── type.ts
├── tsconfig.json
└── yarn.lock
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
14 | thanks_dev: # Replace with a single thanks.dev username
15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
16 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: Deploy static content to Pages
4 |
5 | # Controls when the action will run.
6 | on:
7 | push:
8 | branches: [ main ]
9 |
10 | # Allows you to run this workflow manually from the Actions tab
11 | workflow_dispatch:
12 |
13 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
14 | permissions:
15 | contents: read
16 | pages: write
17 | id-token: write
18 |
19 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
20 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
21 | concurrency:
22 | group: "pages"
23 | cancel-in-progress: false
24 |
25 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
26 | jobs:
27 | # Single deploy job since we're just deploying
28 | deploy:
29 | environment:
30 | name: github-pages
31 | url: ${{ steps.deployment.outputs.page_url }}
32 | runs-on: ubuntu-22.04
33 |
34 | # Steps represent a sequence of tasks that will be executed as part of the job
35 | steps:
36 | - name: Checkout
37 | uses: actions/checkout@v3
38 | - name: install
39 | run: npm run installForce
40 | - name: pack web
41 | run: npm run ccp-pack-web
42 | - name: DeployWithDeleteLatestResult
43 | uses: peaceiris/actions-gh-pages@v3
44 | with:
45 | personal_token: ${{ secrets.PERSONAL_TOKEN }}
46 | publish_dir: ./web
47 | external_repository: tidys/cc-inspector-chrome
48 | publish_branch: gh-pages
49 | keep_files: true
50 | exclude_assets: '*.js,*.css'
51 | - name: DeployJsCss
52 | uses: peaceiris/actions-gh-pages@v3
53 | with:
54 | personal_token: ${{ secrets.PERSONAL_TOKEN }}
55 | publish_dir: ./web
56 | external_repository: tidys/cc-inspector-chrome
57 | publish_branch: gh-pages
58 | keep_files: true
59 | exclude_assets: ''
60 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | test/
3 | node_modules/
4 | web/
5 | .yalc/
6 | yalc.lock
7 | chrome/
8 | dist/
9 | yarn-error.log
10 | chrome.zip
11 | chrome.crx
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "editor.formatOnSaveMode": "modifications",
4 | "files.autoSave": "onFocusChange",
5 | "editor.codeActionsOnSave": {
6 | "source.organizeImports": "explicit"
7 | },
8 | "[typescript]": {
9 | "editor.tabSize": 2,
10 | "editor.formatOnSave": true,
11 | "editor.defaultFormatter": "esbenp.prettier-vscode",
12 | "prettier.printWidth": 1000
13 | },
14 | "[json]": {
15 | "editor.quickSuggestions": {
16 | "strings": true
17 | },
18 | "editor.suggest.insertMode": "replace",
19 | "editor.tabSize": 2
20 | },
21 | "[vue]": {
22 | "editor.defaultFormatter": "esbenp.prettier-vscode",
23 | "editor.tabSize": 2,
24 | "prettier.printWidth": 1000
25 | },
26 | "files.refactoring.autoSave": false,
27 | "search.exclude": {
28 | "**/node_modules": true,
29 | "**/bower_components": true,
30 | "**/*.code-search": true,
31 | "chrome": true,
32 | "yarn-error.log": true,
33 | "yarn.lock": true,
34 | ".yalc/": true
35 | },
36 | "files.exclude": {
37 | ".yalc/": false,
38 | "**/.git": true,
39 | "**/.svn": true,
40 | "**/.hg": true,
41 | "**/CVS": true,
42 | "**/.DS_Store": true,
43 | "**/Thumbs.db": true
44 | },
45 | "prettier.printWidth": 1000
46 | }
47 |
--------------------------------------------------------------------------------
/README.en.md:
--------------------------------------------------------------------------------
1 | This is a free extension that works out of the box without any additional installation. It can help you debug games made by CocosCreator, display the node tree of the game, and the component properties of the node tree, which is very helpful when troubleshooting game problems.
2 |
3 | On the left side of the debug panel is the hierarchy manager, which is used to display the node tree of the game, and on the right side is the property inspector, which is used to display the detailed properties of the node. The width of the two panels can be flexibly adjusted.
4 |
5 | ## Features:
6 | - The extension automatically retrieves and refreshes the node tree and its status
7 | - The game made is nested in iframe, and debugging is also supported, and the debugging target can be switched freely.
8 | - It also supports debugging Cocos games made by Creator2.x and Creator3.x.
9 | - In the hierarchy manager, you can use the up, down, left, and right arrow keys to quickly fold, expand, and select the node tree.
10 | - In the hierarchy manager, you can use the space shortcut key to control the display and hiding of nodes.
11 | - In the hierarchy manager, you can right-click and select `destroy` to destroy a node.
12 | - In the property inspector, if the attribute references a node, the reference can be highlighted in the node tree.
13 | - The property inspector supports simplified and full display of properties, which is convenient for observing properties in different situations.
14 | - The properties support adjusting the value by dragging the mouse, which is very convenient when adjusting the coordinates.
15 | - Supports printing components in the console, which is convenient for observing the whole picture.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # cc-inspector-chrome
2 |
3 | 在浏览器中查看CocosCreator游戏的节点树、节点属性。
4 |
5 | 
6 |
7 | - [youtube](https://www.youtube.com/watch?v=ajMz3zEFTA8)
8 | - [chrome](https://chromewebstore.google.com/detail/cc-inspector/hejbkamkfnkifppoaljcidepkhgaahcj?hl=zh-CN&utm_source=ext_sidebar)
9 | - [bilibili](https://www.bilibili.com/video/BV1jzcHeSEh3/)
10 | - [cocos store](https://store.cocos.com/app/detail/2002)
11 | - [github](https://github.com/tidys/cc-inspector-chrome)
12 |
13 |
14 | ### 论坛
15 | - http://forum.cocos.com/t/chrome-creator/55669
16 | - https://forum.cocos.org/t/topic/164888
17 |
18 |
19 | ## 开发中使用到的技术
20 | - [cc-plugin](https://www.npmjs.com/package/cc-plugin)
21 | - [cc-ui](https://www.npmjs.com/package/@xuyanfeng/cc-ui)
22 | - chrome 插件开发
23 | - vue3、webpack
24 | - cocos creator
25 |
26 | ## Cocos Creator Test Cases
27 | - [cocos test cases](https://tidys.github.io/creator-test-cases/)
28 |
29 | ## TODO
30 | - 目前开发过程中无法实现HMR,参考[extensions-reloader](https://chrome.google.com/webstore/detail/extensions-reloader/fimgfedafeadlieiabdeeaodndnlbhid?utm_source=chrome-ntp-icon)
31 |
--------------------------------------------------------------------------------
/README.zh.md:
--------------------------------------------------------------------------------
1 | 这是一个免费的扩展,开箱即用,无须任何额外的安装,可以帮助你调试🦆CocosCreator🐤制作的游戏,显示游戏运行的节点树,以及节点树的组件属性,在排查游戏问题时,非常有帮助。
2 |
3 | 调试面板左侧为层级管理器,用于展示游戏的节点树,右侧为属性检查器,用于展示节点的详细属性,可以灵活调整2个面板的宽度。
4 |
5 | ## 为什么选择该插件?
6 |
7 | 1️⃣😍免费开源
8 | 2️⃣💯支持所有CocosCreator版本
9 | 3️⃣🤟功能丰富强大,满足开发游戏的常用Inspect需求
10 | 4️⃣💻与Chrome浏览器深度融合,开箱即用
11 | 5️⃣💁♂️作者本人也在使用,迭代更新频繁
12 |
13 | ## 🍉特点:
14 | 1️⃣同时支持调试Creator2.x和Creator3.x制作的Cocos游戏。
15 | 2️⃣层级管理器、属性检查器支持自动/手动刷新。
16 | 3️⃣制作的游戏在iframe嵌套,同样也支持调试,并且支持自由切换调试目标。
17 |
18 | 🍎层级管理器:展示游戏的节点树
19 | 1️⃣滑过、选中节点时,游戏内高亮显示节点位置,便于观察节点位置。
20 | 2️⃣支持使用上下左右方向键快速折叠、展开、选中节点树。
21 | 3️⃣支持使用空格快捷键,控制节点的显示隐藏。
22 | 4️⃣右键选择`destroy`销毁某个节点。
23 | 5️⃣🔍支持模糊搜索节点,按照路径搜索节点,查找更方便。
24 | 6️⃣显示Sprite、Label、Button等类型节点,预制节点绿色标记,观察节点结构非常方便。
25 |
26 | 🍏属性检查器:展示节点的详细属性
27 | 1️⃣如果属性引用了节点,支持在节点树中高亮显示引用。
28 | 2️⃣支持属性精简显示和全部显示,方便在不同情况下观察属性。
29 | 3️⃣支持鼠标拖拉调整数值,在调整坐标时,非常方便。
30 | 4️⃣支持在控制台打印组件,方便观察全貌。
31 |
32 | 🗡️调试小助手:在游戏的网页中显示增强功能的入口
33 | 1️⃣支持fps显隐,游戏暂停步进等控制。
34 | 2️⃣支持拖拽调整位置、自动隐藏,不干扰用户。
35 | 3️⃣支持在游戏界面拾取节点,节点信息会同步选中到层级管理器和属性检查器。
36 | 4️⃣支持按照类型拾取节点,支持展示鼠标位置的所有节点。
37 | 5️⃣支持快捷键一键拾取节点,使用起来极度丝滑。
38 |
39 | 🚀更多完整功能动态图演示,请查看: https://juejin.cn/post/7463836172559024179
40 |
41 | 🧙♂️Cocos论坛: https://forum.cocos.org/t/topic/164888
42 | 🏠插件官网: https://tidys.github.io/cc-inspector-chrome/inspector.html
43 | 🌈github开源地址: https://github.com/tidys/cc-inspector-chrome
44 | 📺BiliBili视频介绍: https://www.bilibili.com/video/BV1jzcHeSEh3/
45 | 📺YouTube视频介绍: https://www.youtube.com/watch?v=ajMz3zEFTA8
46 |
47 | ❤️如果你喜欢该插件,请分享给你的朋友。
48 |
49 | 🎁如果可以,在CocosStore支持我一下: https://store.cocos.com/app/detail/2002
--------------------------------------------------------------------------------
/cc-plugin.config.ts:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import { CocosPluginManifest, CocosPluginOptions, Panel } from "cc-plugin/src/declare";
3 |
4 | const pkgName = "cc-inspector";
5 |
6 | function i18n(key: string) {
7 | return `i18n:${pkgName}.${key}`;
8 | }
9 |
10 | const manifest: CocosPluginManifest = {
11 | name: pkgName,
12 | version: "2.2.0",
13 | description: "Debug games made with CocosCreator and display node trees and node properties",
14 | store: "https://store.cocos.com/app/detail/2002",
15 | author: "xu_yanfeng",
16 | main: "./src/main.ts",
17 | panels: [
18 | {
19 | name: "inspector",
20 | type: Panel.Type.DockAble,
21 | main: "./src/panel/index.ts",
22 | title: "Cocos Inspector",
23 | width: 500,
24 | height: 400,
25 | minWidth: 50,
26 | minHeight: 400,
27 | },
28 | ],
29 | menus: [
30 | {
31 | path: `cc-inspector/${i18n("title")}`,
32 | message: {
33 | name: "showPanel",
34 | },
35 | },
36 | ],
37 | i18n_en: "./src/i18n/en.ts",
38 | i18n_zh: "./src/i18n/zh.ts",
39 | icon: {
40 | "48": "./icons/48.png",
41 | },
42 | analysis: {
43 | // googleAnalytics: "G-0S2X4Z1FE7",
44 | },
45 | chrome: {
46 | permissions: ["storage", "notifications"],
47 | url: "https://chromewebstore.google.com/detail/cc-inspector/hejbkamkfnkifppoaljcidepkhgaahcj?authuser=0&hl=en",
48 | version: 3,
49 | pem: "./crx-key.pem",
50 | view_devtools: "src/views/devtools/index.ts",
51 | view_options: "src/views/options/index.ts",
52 | view_popup: "src/views/popup/index.ts",
53 | script_background: "src/scripts/background/index.ts",
54 | script_content: "src/scripts/content/index.ts",
55 | script_inject: "src/scripts/inject/index.ts",
56 | script_inject_view: "src/scripts/inject-view/web-test.ts",
57 | },
58 | };
59 | const options: CocosPluginOptions = {
60 | obscure: false,
61 | server: {
62 | enabled: false,
63 | port: 2022,
64 | https: true,
65 | writeToDisk: true,
66 | },
67 | watchBuild: true,
68 | outputProject: {
69 | web: "./web",
70 | chrome: "./chrome",
71 | },
72 | };
73 | export default { manifest, options };
74 |
--------------------------------------------------------------------------------
/cc-plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "v2": "",
3 | "v3": ""
4 | }
--------------------------------------------------------------------------------
/crx/crx-key.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDKA+h2e8dPlzt3
3 | RRXtRZuY3s/dhaxIz7TbiUQymqwi8vhUx3p9LzBbQSK3qOjJGifsrkf2C6TpmvWZ
4 | lpTbDdl4hz5Z5Ljrt2GtehEGlb/IxbiuEuAKfznrljMKwDHdp809hmc6XtwZdvW0
5 | B/0yfRtUKZ+9HH6rvQBtcDaZ4fAkX2BIcqw/j5CIzW+IyziswnFoMQkC68Cu2vS1
6 | mNSU8foYlxQAsmttbzr/PGZkrOL92oeVoJJfPfR7eBP8JhSbVJ6GGdhw72SQTByY
7 | 5e9Pl94veUdhwryH/xtsb27YrNO8EPbwMyJvu+LYp2Im5IOC3HvChB/XpfnnqfgZ
8 | F0aCvOIVAgMBAAECggEAIwlre4U7S8AQhb6bL3RHqMtYJPUGrPgtR3g2tkoiOcuH
9 | K3kPcv/ItMMGyPDIMvzQ2NlfTLq3RvbjytgViCqOQbu/IisrsWI0VDicFFbLlQBJ
10 | 6BMI/XkUbBo4TxCHdYihbZ7Ob469QMNjmO5byTJul3nCpTN4esPEe7afGbBYjI4Z
11 | SffhXI/eF5CaBEqsEOAwDunzFh3E7bE6E/JkGOU29xLQy/eryhmD1O3Cnf2i3q29
12 | TooHe0hgh9lkVGt3sZJLp/sH2EYda5W95/Ji1yWaJ9p4M016UPLDGFn4hJvmdGfY
13 | VVfQ47sCvvJ26zHt/+kZzreXt6YpAlKGmb6ibjVT0QKBgQDmvZtLgwmgvqvJNgx/
14 | /b+NDq0mIWX60ASVT3wYiqidtu7xmUU6hUWFumtJB8MWELRUJ7kkzd3PfbEVzX9O
15 | wIo4ifYWrhxxMh8kmoGmpKWZ/MUtH7RN0NXkmcPAKaBftzjooI0VB97KArSuBGeX
16 | 2Tphd+VHMPiKMaI3STa2xKo5eQKBgQDgIUlg4lVATvPWY2Vg6b5qEtzLCiIwgNZD
17 | 5Swx8wlYJApm94OJ5dMzjYFt/5vdeSJfV3ESnZ6gKSsW3vX1dGeFLvIiA7v5OBv5
18 | 0FO2VAXR+iLJje4ZWKTwhzgoM4EYSjS62bUGQ90tJt20YP2nXgYEoQ1agmWTSRPn
19 | 3BFXRXLifQKBgQDMnO8Nc1IiXXLwpyFGjrpCV/VrjspkM/scfLPK/4qu8P3K+OPP
20 | FUelYr6osF1rP7zps6AW5wf/a7KRZv2x1EO+B3lWe2d1acD8MJcwM2k2uFQRw7+c
21 | EcjbQw+3ZDJ3Ln8kqtrw/12tPeEDP5ytp1CCBlQnYWHFCmaTKDWAtb1N4QKBgFfo
22 | lmhavcdYcElReQz3AUmHlnRIyDov/lppA9mfkrWwhSf3wu8OZrVctjxXumG2xmWQ
23 | 3XfIvNPi8dSppN0eSBAz5qKyxkKs4EQukvb7o8DFFGnrskzcuOzijIMwGF1XlbEH
24 | /Pm1GoZALUs3k6XWuhOMu7kZVg/b5OPXLDIHulTdAoGBAIhFZhcl1/W7ANlFvYy/
25 | 6OuxjHYCqFgOa4qYz9ia2X9UnK+SOqTLplZtbbYag4OIxvGZAQ3+Ys6Tm0Y4IySk
26 | qB8d1aSzJelu8feqJtWf9QZFbHvdN+orRQkhPwydC0m/ujefy/+v/edCXv9kWCdj
27 | mw7DaZnzhqap9CnBXnYXFP+5
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/crx/crx.js:
--------------------------------------------------------------------------------
1 | let gulp = require('gulp');
2 | let path = require('path');
3 | let fse = require('fs-extra');
4 | let fs = require('fs');
5 | let shell = require('gulp-shell');
6 |
7 | gulp.task("packageCrx", function () {
8 | let pem = path.join(__dirname, "bin/dist.pem");
9 | if (!fs.existsSync(pem)) {
10 | console.log("签名文件不存在:" + pem);
11 | return;
12 | }
13 | let dist = path.join(__dirname, "dist");
14 | if (!fs.existsSync(dist)) {
15 | console.log("打包目录不存在: " + dist);
16 | console.log("发布失败!");
17 | return;
18 | }
19 |
20 | let exec = require('child_process').exec;
21 | let packageCmd = "chrome.exe --pack-extension=" + dist + " --pack-extension-key=" + pem;
22 | console.log("------------------------------------------------");
23 | console.log("执行打包命令:\n " + packageCmd);
24 | console.log("------------------------------------------------");
25 | let ret = exec(packageCmd);
26 | ret.stdout.on('data', function () {
27 | console.log("data");
28 | });
29 | ret.stdout.on('close', function (err, a, b, c) {
30 |
31 | let file = path.join(__dirname, "dist.crx");
32 | if (fs.existsSync(file)) {
33 | let desFile = path.join(__dirname, "bin/cc-inspector.crx");
34 | if (fs.existsSync(desFile)) {
35 | fse.removeSync(desFile);
36 | }
37 | fse.move(file, desFile, function (err) {
38 | if (!err) {
39 | console.log("发布插件安装包文件成功!");
40 | console.log("存放路径: " + desFile);
41 | } else {
42 | console.log(err);
43 | }
44 | })
45 | } else {
46 | console.log("文件不存在: " + file);
47 | console.log("发布失败!");
48 | }
49 | });
50 |
51 | });
52 | gulp.task("default", function () {
53 | console.log("hello gulp!");
54 | });
55 |
--------------------------------------------------------------------------------
/doc/Marquee.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tidys/cc-inspector-chrome/a0329f741412c121ffc949daafbd5d86bf2f38cf/doc/Marquee.png
--------------------------------------------------------------------------------
/doc/chrome-message.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tidys/cc-inspector-chrome/a0329f741412c121ffc949daafbd5d86bf2f38cf/doc/chrome-message.png
--------------------------------------------------------------------------------
/doc/icon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tidys/cc-inspector-chrome/a0329f741412c121ffc949daafbd5d86bf2f38cf/doc/icon128.png
--------------------------------------------------------------------------------
/doc/prop/v2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tidys/cc-inspector-chrome/a0329f741412c121ffc949daafbd5d86bf2f38cf/doc/prop/v2.png
--------------------------------------------------------------------------------
/doc/scene1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tidys/cc-inspector-chrome/a0329f741412c121ffc949daafbd5d86bf2f38cf/doc/scene1.png
--------------------------------------------------------------------------------
/doc/scene2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tidys/cc-inspector-chrome/a0329f741412c121ffc949daafbd5d86bf2f38cf/doc/scene2.png
--------------------------------------------------------------------------------
/doc/scene3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tidys/cc-inspector-chrome/a0329f741412c121ffc949daafbd5d86bf2f38cf/doc/scene3.png
--------------------------------------------------------------------------------
/doc/scene4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tidys/cc-inspector-chrome/a0329f741412c121ffc949daafbd5d86bf2f38cf/doc/scene4.png
--------------------------------------------------------------------------------
/doc/scene5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tidys/cc-inspector-chrome/a0329f741412c121ffc949daafbd5d86bf2f38cf/doc/scene5.png
--------------------------------------------------------------------------------
/doc/small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tidys/cc-inspector-chrome/a0329f741412c121ffc949daafbd5d86bf2f38cf/doc/small.png
--------------------------------------------------------------------------------
/doc/v2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tidys/cc-inspector-chrome/a0329f741412c121ffc949daafbd5d86bf2f38cf/doc/v2.png
--------------------------------------------------------------------------------
/hello-chrome/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.autoSave": "onFocusChange",
3 | "[html]": {
4 | "editor.suggest.insertMode": "replace",
5 | "editor.formatOnSave": true,
6 | "editor.formatOnType": true,
7 | "editor.defaultFormatter": "esbenp.prettier-vscode"
8 | }
9 | }
--------------------------------------------------------------------------------
/hello-chrome/background.js:
--------------------------------------------------------------------------------
1 | console.log("background.js")
2 | chrome.runtime.onConnect.addListener((port) => {
3 | console.log("runtime onConnect: ", port)
4 | port.onMessage.addListener((message) => {
5 | console.log("runtime port onMessage: ", message)
6 | port.postMessage(message + "*bg")
7 | })
8 | port.onDisconnect.addListener(() => {
9 | console.log("runtime port onDisconnect")
10 | })
11 | })
12 | chrome.runtime.onMessage.addListener((message) => {
13 | console.log("runtime onMessage: ", message)
14 | })
15 |
16 | chrome.debugger.onEvent.addListener((source, method, params) => {
17 | // console.log("debugger onEvent: ", source, method, params)
18 | })
19 | chrome.action.onClicked.addListener((tab) => {
20 | debugger
21 | // 使用devtools协议链接到页面
22 | chrome.debugger.attach({ tabId: tab.id }, "1.0", () => {
23 | chrome.debugger.sendCommand({ tabId: tab.id }, "Debugger.enable", {}, () => { });
24 | return;
25 | debugger;
26 | // 启动调试器
27 | chrome.debugger.sendCommand({ tabId: tab.id }, "Debugger.enable", {}, () => {
28 | debugger;
29 | // chrome.debugger.sendCommand({ tabId: tab.id }, "Debugger.getScriptSource", (scripts) => {
30 | // console.log(scripts);
31 | // debugger
32 | // })
33 | chrome.debugger.sendCommand(
34 | { tabId: tab.id, },
35 | "Runtime.evaluate",
36 | { expression: "window.abc=()=>{console.log(1)}" },
37 | (result) => {
38 | debugger
39 | if (result && result.result && result.result.objectId) {
40 | console.log(result.result);
41 |
42 | chrome.debugger.sendCommand(
43 | {
44 | tabId: tab.id,
45 | method: "Debugger.searchInContentScripts",
46 | params: {
47 | query: "abc",
48 | }
49 | }, (result) => {
50 | debugger
51 | })
52 | return;
53 | // 获取func
54 | chrome.debugger.sendCommand(
55 | {
56 | tabId: tab.id,
57 | method: "Debugger.searchInContentScripts",
58 | params: {
59 | query: "window.abc",
60 | }
61 | },
62 | "Debugger.searchInContentScripts",
63 | { functionId: result.result.objectId },
64 | (location) => {
65 | debugger
66 | console.log(location);
67 | if (location) {
68 | // 调整到函数
69 | chrome.debugger.sendCommand(
70 | { tabId: tab.id, },
71 | "Debugger.setBreakpoint",
72 | {
73 | location: location.locations[0],
74 | },
75 | (brk) => {
76 | debugger
77 | console.log(brk);
78 | }
79 | )
80 |
81 | }
82 |
83 | }
84 | )
85 | }
86 | }
87 | );
88 |
89 |
90 | });
91 |
92 |
93 | });
94 | })
95 | // chrome.tabs.onConnect.addListener((port) => {
96 | // console.log("tabs onConnect: ", port)
97 | // port.onMessage.addListener((message) => {
98 | // console.log("port onMessage: ", message)
99 | // })
100 | // port.onDisconnect.addListener(() => {
101 | // console.log("port onDisconnect")
102 | // })
103 | // })
104 | // chrome.tabs.onMessage.addListener((message) => {
105 | // console.log("tabs onMessage: ", message)
106 | // })
--------------------------------------------------------------------------------
/hello-chrome/content.js:
--------------------------------------------------------------------------------
1 | console.log("content.js")
--------------------------------------------------------------------------------
/hello-chrome/devtools.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | devtools
5 |
6 |
7 |
8 | devtools
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/hello-chrome/devtools.js:
--------------------------------------------------------------------------------
1 | console.log("devtools.js");
2 |
3 | // const tabsConnect = chrome.tabs.connect({ name: "devtoos" });
4 | // tabsConnect.onMessage.addListener((message) => {
5 | // console.log("tabsConnect Message: ", message)
6 | // })
7 | // tabsConnect.onDisconnect.addListener(() => {
8 | // console.log("tabsConnect disconnect")
9 | // })
10 | const runtimeConnect = chrome.runtime.connect({ name: "devtools" })
11 | runtimeConnect.onDisconnect.addListener(() => {
12 | console.log(`runtimeConnect disconnect`,)
13 | });
14 | runtimeConnect.onMessage.addListener((message) => {
15 | console.log('runtimeConnect Message: ', message)
16 | text.innerText = message;
17 | });
18 | // view
19 | const text = document.getElementById('text')
20 | const send2bg = document.getElementById('send2bg')
21 | if (send2bg) {
22 | send2bg.addEventListener('click', () => {
23 | console.log(document.flag);
24 | const message = ("devtools send to background")
25 | runtimeConnect.postMessage(message)
26 | // tabsConnect.sendMessage(message);
27 | // chrome.runtime.sendMessage(message);
28 | })
29 | }
30 |
31 | console.log('href: ', window.location.href);
32 | console.log(chrome.devtools);
33 | console.log(chrome.devtools.inspectedWindow.tabId);
34 | chrome.devtools.panels.setOpenResourceHandler((res) => {
35 | debugger;
36 | console.log(res);
37 | })
38 | chrome.devtools.panels.create("Hello World", "icon.png", "devtools.html", (panel) => {
39 | console.log("panel created");
40 | panel.onShown.addListener((win, b) => {
41 | console.log("panel shown", win, b);
42 | console.log(win.document.body)
43 | console.log(` doc: `, win.document === document);
44 | console.log(win.document);
45 | console.log(window.document);
46 | win.document.flag = "devtools_panel";
47 | win.document.body.addEventListener('contextmenu', (e) => {
48 | console.log(e);
49 | })
50 | document.body.addEventListener('keydown', (e) => {
51 | console.log(e);
52 | })
53 | });
54 | // panel.createStatusBarButton({})
55 | // panel.show();
56 |
57 | panel.onHidden.addListener((a, b) => {
58 | console.log("panel hidden", a, b);
59 | // setTimeout(() => {
60 | // debugger;
61 |
62 | // panel.show();
63 | // }, 3 * 1000);
64 | });
65 | panel.onSearch.addListener((query) => {
66 | console.log("panel search", query);
67 | });
68 | });
--------------------------------------------------------------------------------
/hello-chrome/icons/48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tidys/cc-inspector-chrome/a0329f741412c121ffc949daafbd5d86bf2f38cf/hello-chrome/icons/48.png
--------------------------------------------------------------------------------
/hello-chrome/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | index
8 |
9 |
10 |
11 |
12 |
13 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/hello-chrome/index.js:
--------------------------------------------------------------------------------
1 | console.log("index.js")
--------------------------------------------------------------------------------
/hello-chrome/inject.js:
--------------------------------------------------------------------------------
1 | console.log("inject")
--------------------------------------------------------------------------------
/hello-chrome/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "description": "hello world",
4 | "icons": {
5 | "48": "./icons/48.png"
6 | },
7 | "devtools_page": "devtools.html",
8 | "content_scripts": [
9 | {
10 | "matches": [
11 | ""
12 | ],
13 | "js": [
14 | "content.js"
15 | ],
16 | "run_at": "document_end",
17 | "all_frames": true
18 | }
19 | ],
20 | "options_ui": {
21 | "page": "options.html",
22 | "browser_style": true
23 | },
24 | "name": "helloworld",
25 | "version": "2.1.0",
26 | "permissions": [
27 | "tabs",
28 | "debugger"
29 | ],
30 | "host_permissions": [
31 | "wss://*/*",
32 | "ws://*/*",
33 | "",
34 | "*://*/*",
35 | "tabs",
36 | "http://*/*",
37 | "https://*/*",
38 | "audio",
39 | "system.cpu",
40 | "clipboardRead",
41 | "clipboardWrite",
42 | "system.memory",
43 | "processes",
44 | "tabs",
45 | "storage",
46 | "nativeMessaging",
47 | "contextMenus",
48 | "notifications"
49 | ],
50 | "action": {
51 | "default_icon": {
52 | "48": "./icons/48.png"
53 | },
54 | "default_title": "hello-world"
55 | },
56 | "background": {
57 | "service_worker": "background.js",
58 | "type": "module"
59 | }
60 | }
--------------------------------------------------------------------------------
/hello-chrome/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | options
5 |
6 |
7 |
8 | options
9 |
10 |
11 |
--------------------------------------------------------------------------------
/hello-chrome/options.js:
--------------------------------------------------------------------------------
1 | console.log('options.js')
--------------------------------------------------------------------------------
/hello-chrome/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | popup
5 |
6 |
7 |
8 | popup
9 |
10 |
11 |
--------------------------------------------------------------------------------
/hello-chrome/popup.js:
--------------------------------------------------------------------------------
1 | console.log("popup")
--------------------------------------------------------------------------------
/hello-chrome/readme.md:
--------------------------------------------------------------------------------
1 | 使用原生js编写的Chrome插件,方便我理解一些逻辑
--------------------------------------------------------------------------------
/icons/48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tidys/cc-inspector-chrome/a0329f741412c121ffc949daafbd5d86bf2f38cf/icons/48.png
--------------------------------------------------------------------------------
/others.md:
--------------------------------------------------------------------------------
1 |
2 | # 说明
3 |
4 | inject在development模式下无法正常使用,暂时的解决办法,注释掉`vue-cli-plugin-browser-extension/index.js`代码中的124行:
5 | ```
6 | webpackConfig.plugin('extension-reloader').use(ExtensionReloader, [{ entries, ...extensionReloaderOptions }])
7 | ```
8 | 详细原因参考:[issues](https://github.com/adambullmer/vue-cli-plugin-browser-extension/issues/120)
9 |
10 | # 后续工作
11 |
12 | popup界面增加联系方式。
13 |
14 | 开发一个独立的electron桌面版本,使用socket调试app,解决排查app问题的痛点。
15 |
16 |
17 | # 后续工作
18 |
19 | 后续打算使用vue3重写,并且完全使用webpack进行打包配置,以灵活应对打包配置,关联项目
20 |
21 | https://github.com/tidys/project-tool
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "cc-plugin",
3 | "description": "cocos creator plugin",
4 | "devDependencies": {
5 | "@types/chrome": "0.0.293",
6 | "@types/fs-extra": "9.0.1",
7 | "@types/kind-of": "^6.0.0",
8 | "@types/lodash": "^4.14.176",
9 | "@types/node": "16.11.12",
10 | "@types/uuid": "^8.3.1",
11 | "@xuyanfeng/cc-ui": "0.2.37",
12 | "cc-plugin": "2.2.2"
13 | },
14 | "name": "cc-plugin-demo",
15 | "scripts": {
16 | "installForce": "yarn install --force",
17 | "ccp-pack-v2": "cc-plugin pack cp-v2",
18 | "ccp-pack-v3": "cc-plugin pack cp-v3",
19 | "ccp-pack-web": "cc-plugin pack web",
20 | "ccp-pack-chrome": "cc-plugin pack chrome",
21 | "ccp-serve-v2": "cc-plugin serve cp-v2",
22 | "ccp-serve-v3": "cc-plugin serve cp-v3",
23 | "ccp-serve-web": "cc-plugin serve web",
24 | "ccp-serve-chrome": "cc-plugin serve chrome"
25 | },
26 | "version": "1.0.0",
27 | "dependencies": {
28 | "@tsparticles/slim": "^3.8.1",
29 | "@tsparticles/vue3": "^3.0.1",
30 | "lodash": "^4.17.21",
31 | "tiny-emitter": "2.1.0",
32 | "uuid": "^8.3.2"
33 | }
34 | }
--------------------------------------------------------------------------------
/src/core/types.ts:
--------------------------------------------------------------------------------
1 | import { ShowCode } from "../scripts/inject/types";
2 | import { Chunk } from "../scripts/terminal";
3 | import { FrameDetails, Info, NodeInfoData, TreeData } from "../views/devtools/data";
4 |
5 | export enum Page {
6 | None = "None",
7 | Inject = "Inject",
8 | Devtools = "Devtools",
9 | Background = "Background",
10 | Content = "Content",
11 | Popup = "Popup",
12 | Options = "Options",
13 | }
14 |
15 | // #region 定义接受发送的数据声明,方便定位
16 | export interface RequestTreeInfoData {
17 | /**
18 | * 当前正在使用的frameID
19 | */
20 | frameID: number;
21 | }
22 | export type ResponseTreeInfoData = TreeData;
23 |
24 | export interface RequestNodeInfoData {
25 | /**
26 | * 节点的UUID
27 | */
28 | uuid: string;
29 | }
30 | export type ResponseNodeInfoData = NodeInfoData;
31 |
32 | export interface RequestSupportData {}
33 | export interface ResponseSupportData {
34 | /**
35 | * 是否支持
36 | */
37 | support: boolean;
38 | /**
39 | * 消息
40 | */
41 | msg: string;
42 | /**
43 | * engine版本
44 | */
45 | version: string;
46 | }
47 | export class DynamicAtlas {
48 | /**
49 | * 是否启用动态图集
50 | */
51 | enable: boolean = false;
52 | atlasCount: number = 0;
53 | maxAtlasCount: number = 0;
54 | maxFrameSize: number = 0;
55 | textureSize: number = 0;
56 | textureBleeding: boolean = true;
57 | /**
58 | * 是否支持在游戏中查看
59 | */
60 | supportView: boolean = false;
61 | }
62 | export class ResponseGameInfoData {
63 | public dynamicAtals = new DynamicAtlas();
64 | }
65 | export type ResponseUpdateFramesData = FrameDetails[];
66 |
67 | export interface RequestUseFrameData {
68 | id: number;
69 | }
70 | export interface ResponseUseFrameData {
71 | id: number;
72 | }
73 | export type RequestSetPropertyData = Info;
74 | export type ResponseSetPropertyData = Info;
75 | export type RequestLogData = string[];
76 | export type RequestOpenNodeTouchFuntionData = { uuid: string; code: ShowCode; index: number };
77 | export type RequestOpenScriptData = {
78 | /**节点或者组件的UUID */
79 | uuid: string;
80 | /**节点的脚本名字 */
81 | script: string;
82 | };
83 | export type ResponseErrorData = string;
84 | export type RequestBreakOnData = { uuid: string; type: BreakOnType };
85 | export enum Msg {
86 | None = "None",
87 | /**
88 | * 具体的节点信息
89 | */
90 | RequestNodeInfo = "request-node-info",
91 | ResponseNodeInfo = "response-node-info",
92 | /**
93 | * 节点树信息
94 | */
95 | RequstTreeInfo = "request-tree-info",
96 | ResponseTreeInfo = "response-tree-info",
97 | /**
98 | * 游戏支持信息
99 | */
100 | RequestSupport = "request-support",
101 | ResponseSupport = "response-support",
102 |
103 | ResponseMemoryInfo = "response-memory-info",
104 | VisibleFPS = "visible-fps",
105 | ResponseBuyEverything = "ResponseBuyEverything",
106 | /**
107 | * 当前页面信息
108 | */
109 | TabsInfo = "tabs_info",
110 | /**
111 | * 获取页面ID
112 | */
113 | GetTabID = "GetTabID",
114 | AddOpactiy = "add-opacity",
115 | /**
116 | * 用户主动选中的节点
117 | */
118 | InspectNode = "inspect-node",
119 | /**
120 | * 鼠标滑过节点
121 | */
122 | HoverNode = "hover-node",
123 | /**
124 | * 选中节点
125 | */
126 | SelectNode = "select-node",
127 | /**
128 | * 更新页面的frame
129 | */
130 | ResponseUpdateFrames = "response-update-frames",
131 | RequestUseFrame = "request-use-frame",
132 | ResponseUseFrame = "response-use-frame",
133 | /**
134 | * 测试从background层主动断开devtools的链接
135 | */
136 | RequestDisconnectDevtools = "request-disconnect-devtools",
137 | DevtoolConnectError = "devtool-connect-error",
138 | RequestLogData = "request-log-data",
139 | RequestLogCustom = "request-log-custom",
140 | ReqWriteClipboard = "request-write-clipboard",
141 | RequestSetProperty = "request-set-property",
142 | ResponseSetProperty = "response-set-property",
143 | RequestVisible = "request-visible",
144 | RequestDestroy = "request-destroy",
145 |
146 | ResponseError = "response-error",
147 |
148 | RequestGameInfo = "request-game-info",
149 | ResponseGameInfo = "response-game-info",
150 |
151 | RequestDynamicAtlasView = "request-dynamic-atlas-view",
152 | ResponseDynamicAtlasView = "response-dynamic-atlas-view",
153 |
154 | /**
155 | * 请求在source面板打开节点的touch函数
156 | */
157 | RequestOpenNodeTouchFuntion = "request-open-node-touch-funtion",
158 | RequestOpenScript = "request-open-script",
159 | RequestBreakOn = "request-break-on",
160 | RequestBreakClean = "request-break-clean",
161 | RequestOpenInCocos = "request-open-in-cocos",
162 | }
163 | export enum BreakOnType {
164 | SizeChanged = "size changed",
165 | TransformChanged = "transform changed",
166 | ColorChanged = "color changed",
167 | LayerChanged = "layer changed",
168 | SiblingOrderChanged = "sibling order changed",
169 | ActiveChanged = "active changed",
170 | Destroyed = "destroyed",
171 | ParentChanged = "parent changed",
172 | ChildAdded = "child added",
173 | ChildRemoved = "child removed",
174 | CompAdded = "component added",
175 | CompRemoved = "component removed",
176 | }
177 | export class PluginEvent {
178 | public static FLAG = "cc-inspector";
179 | /**
180 | * 增加一个消息的标记位,方便知道是自己插件的消息
181 | */
182 | flag: string = PluginEvent.FLAG;
183 | /**
184 | * 消息是否有效
185 | */
186 | valid: boolean = false;
187 | /**
188 | * 消息的类型
189 | */
190 | msg: Msg | null = null;
191 | /**
192 | * 携带的数据
193 | */
194 | data: any = null;
195 |
196 | /**
197 | * 事件发送的源头
198 | */
199 | source: Page | null = null;
200 | /**
201 | * 事件要发送的目标
202 | */
203 | target: Page | null = null;
204 | isTargetDevtools() {
205 | return this.target === Page.Devtools;
206 | }
207 | isTargetBackground() {
208 | return this.target === Page.Background;
209 | }
210 | isTargetContent() {
211 | return this.target === Page.Content;
212 | }
213 | /**
214 | * 将addListener监听的数据转换为类
215 | */
216 | static create(data: any): PluginEvent {
217 | let obj = data;
218 | if (typeof data === "string") {
219 | obj = JSON.stringify(data);
220 | } else if (typeof data === "object") {
221 | obj = data;
222 | } else {
223 | debugger;
224 | }
225 |
226 | const ret = new PluginEvent(Page.None, Page.None, Msg.None, null);
227 | if (obj.flag !== PluginEvent.FLAG) {
228 | ret.valid = false;
229 | } else {
230 | const cls = data as PluginEvent;
231 | ret.source = cls.source;
232 | ret.target = cls.target;
233 | ret.msg = cls.msg;
234 | ret.data = cls.data;
235 | ret.valid = true;
236 | }
237 | return ret;
238 | }
239 | check(source: Page, target: Page) {
240 | return source && target && this.source === source && this.target === target;
241 | }
242 |
243 | reset(source: Page | null, target: Page | null) {
244 | if (source && target) {
245 | this.source = source;
246 | this.target = target;
247 | }
248 | }
249 |
250 | /**
251 | *
252 | */
253 | static finish(event: PluginEvent) {
254 | event.source = event.target = null;
255 | }
256 | toChunk(): Chunk[] {
257 | return [new Chunk(new Date().toLocaleString()).color("white").background("black").padding("0 4px"), new Chunk(this.source).color("white").background("red").padding("0 4px").margin("0 0 0 5px"), new Chunk("=>").color("black").background("yellow").bold(), new Chunk(this.target, false).color("white").background("green").padding("0 4px"), new Chunk(this.msg, true).color("white").background("black").margin("0 0 0 5px").padding("0 6px"), new Chunk(JSON.stringify(this.data))];
258 | }
259 | constructor(source: Page, target: Page, msg: Msg, data?: any) {
260 | if (PageInclude(target)) {
261 | this.source = source;
262 | this.target = target;
263 | this.msg = msg;
264 | this.data = data;
265 | } else {
266 | console.warn(`无效的target: ${target}`);
267 | }
268 | }
269 | }
270 |
271 | function inEnum(enumValues: any, value: Page | Msg) {
272 | for (let key in enumValues) {
273 | if (enumValues.hasOwnProperty(key)) {
274 | //@ts-ignore
275 | let itemEnum = enumValues[key] as string;
276 | if (itemEnum === value) {
277 | return true;
278 | }
279 | }
280 | }
281 | return false;
282 | }
283 |
284 | export function PageInclude(page: Page) {
285 | return inEnum(Page, page);
286 | }
287 |
288 | export function MsgInclude(msg: Msg) {
289 | return inEnum(Msg, msg);
290 | }
291 | export const debugLog: boolean = false;
292 |
--------------------------------------------------------------------------------
/src/core/util.ts:
--------------------------------------------------------------------------------
1 | import { Page } from "./types";
2 |
3 | interface LogOptions {
4 | data: any;
5 | flag?: string;
6 | color?: "red" | "blue";
7 | }
8 |
9 | export function log(options: LogOptions) {
10 | const data: any = options.data;
11 | const time = new Date().toLocaleString();
12 | let log = "";
13 | if (typeof data === "string") {
14 | log = data;
15 | } else if (typeof data === "object") {
16 | log = JSON.stringify(data);
17 | }
18 |
19 | let str = "";
20 | if (options.flag) {
21 | str = `[${time}][${options.flag}]: ${log} `;
22 | } else {
23 | str = `[${time}]: ${log} `;
24 | }
25 | if (options.color) {
26 | console.log(`%c${str}`, `color:${options.color};`);
27 | } else {
28 | console.log(str);
29 | }
30 | }
31 |
32 | export function assembleDevToolsName(id: number) {
33 | return `${Page.Devtools}-${id}`;
34 | }
35 | export function getDevToolsInspectorId(name: string) {
36 | const id = name.split("-")[1];
37 | return id ? Number(id) : 0;
38 | }
39 |
--------------------------------------------------------------------------------
/src/ga/index.ts:
--------------------------------------------------------------------------------
1 | import { GA_Button, GA_EventName, MeasurementBody } from "./type";
2 |
3 | const API_SECRET = "_yU7eNTgT4Khe2Jo22Ki_g";
4 | const MEASUREMENT_ID = "G-RW7J0JZ6T5";
5 | const GA_ENDPOINT = "https://www.google-analytics.com/mp/collect";
6 |
7 | export class GoogleAnalytics {
8 | async getOrCreateSessionId() {
9 | const result = await chrome.storage.local.get("clientId");
10 | let clientId = result.clientId;
11 | if (!clientId) {
12 | clientId = self.crypto.randomUUID();
13 | await chrome.storage.local.set({ clientId });
14 | }
15 | return clientId;
16 | }
17 | private isChromeEnv() {
18 | return !!chrome?.storage?.local?.get;
19 | }
20 | public async fireEventWithParam(name: GA_EventName, param: string) {
21 | if (!this.isChromeEnv()) {
22 | return;
23 | }
24 | const time = Date.now();
25 | const id = await this.getOrCreateSessionId();
26 | fetch(`${GA_ENDPOINT}?measurement_id=${MEASUREMENT_ID}&api_secret=${API_SECRET}`, {
27 | method: "POST",
28 | body: JSON.stringify({
29 | client_id: id,
30 | events: [
31 | {
32 | name: name,
33 | params: {
34 | id: param,
35 | session_id: time.toString(),
36 | engagement_time_msec: time.toString(),
37 | },
38 | },
39 | ],
40 | } as MeasurementBody),
41 | })
42 | .then(() => {})
43 | .catch((e) => {
44 | console.error(e);
45 | });
46 | }
47 | public async fireEvent(name: string) {
48 | if (!this.isChromeEnv()) {
49 | return;
50 | }
51 | const time = Date.now();
52 | const id = await this.getOrCreateSessionId();
53 | fetch(`${GA_ENDPOINT}?measurement_id=${MEASUREMENT_ID}&api_secret=${API_SECRET}`, {
54 | method: "POST",
55 | body: JSON.stringify({
56 | client_id: id,
57 | events: [
58 | {
59 | name: name,
60 | params: {
61 | session_id: time.toString(),
62 | engagement_time_msec: time.toString(),
63 | },
64 | },
65 | ],
66 | } as MeasurementBody),
67 | })
68 | .then(() => {})
69 | .catch((e) => {
70 | console.error(e);
71 | });
72 | }
73 | async clickButton(btn: GA_Button) {
74 | await this.fireEventWithParam(GA_EventName.ButtonClicked, btn);
75 | }
76 | async openView(view: string) {
77 | await this.fireEventWithParam(GA_EventName.PageView, view);
78 | }
79 | }
80 | export const ga = new GoogleAnalytics();
81 |
--------------------------------------------------------------------------------
/src/ga/type.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 发送的消息数据结构
3 | *
4 | * @doc https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?hl=zh-cn&client_type=gtag#payload_post_body
5 | * @github https://github.dev/GoogleChrome/chrome-extensions-samples/blob/main/functional-samples/tutorial.google-analytics/scripts/google-analytics.js#L69
6 | */
7 | export interface MeasurementBody {
8 | /**
9 | * 用户的ID,用于标识用户
10 | */
11 | client_id: string;
12 | /**
13 | * 用户的唯一标识,只能包含 utf-8 字符。
14 | */
15 | user_id?: string;
16 | /**
17 | * 事件相关联的时间的 UNIX 时间戳,此值应仅设置为记录过去发生的事件。
18 | */
19 | timestamp_micros?: number;
20 | /**
21 | * 用户属性用于描述用户群细分,例如语言偏好设置或地理位置。
22 | *
23 | * @doc https://developers.google.com/analytics/devguides/collection/protocol/ga4/user-properties?hl=zh-cn&client_type=gtag
24 | */
25 | user_properties?: Object;
26 | /**
27 | * 用户提供的数据。
28 | *
29 | *@doc https://developers.google.com/analytics/devguides/collection/ga4/uid-data?hl=zh-cn
30 | */
31 | user_data?: Object;
32 | /**
33 | * 设置请求的用户意见征求设置。
34 | * @doc https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?hl=zh-cn&client_type=gtag#payload_consent
35 | */
36 | consent?: Object;
37 | /**
38 | * 每个请求最多可以发送 25 个事件
39 | */
40 | events?: MeasurementEvent[];
41 | }
42 | export interface MeasurementEvent {
43 | /**
44 | * 事件的名称。
45 | *
46 | * Google提供的事件: https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference/events?hl=zh-cn#add_payment_info
47 | * 预留的事件名:https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?hl=zh-cn&client_type=gtag#reserved_event_names
48 | */
49 | name: string;
50 | /**
51 | * 预留的参数名:https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?hl=zh-cn&client_type=gtag#reserved_parameter_names
52 | */
53 | params?: {
54 | [key: string]: any;
55 | /**
56 | * 在实时报告中查看事件,需要该参数
57 | */
58 | session_id?: string;
59 | /**
60 | * 事件的互动时长(以毫秒为单位)
61 | */
62 | engagement_time_msec?: string;
63 | };
64 | }
65 | export interface GA_Event_PageView extends MeasurementEvent {
66 | name: "page_view";
67 | params: {
68 | page_title: string;
69 | page_location: string;
70 | };
71 | }
72 |
73 | export enum GA_EventName {
74 | ButtonClicked = "button_clicked",
75 | PageView = "page_view",
76 | SpaceVisible = "space_visible",
77 | MouseMenu = "mouse_menu",
78 | Hierarchy = "hierarchy",
79 | Inspector = "Inspector",
80 | EngineVersion = "engine_version",
81 | AppVersion = "app_version",
82 | Popup = "popup",
83 | Rate = "rate",
84 | GameUrl = "game_url",
85 | GamePlayer = "game_player",
86 | GamePause = "game_pause",
87 | GameStep = "game_step",
88 | TreeSearch = "tree-search",
89 | /**
90 | * 用户点击store广告链接
91 | */
92 | ClickPluginLink = "click_plugin_link",
93 | /**
94 | * 用户主动关闭store广告
95 | */
96 | CloseAd = "close_ad",
97 | OpenDoc = "open_doc",
98 | /**
99 | * 展示广告
100 | */
101 | ShowAd = "show_ad",
102 | /**
103 | * 用户主动使用inspector检查游戏节点
104 | */
105 | GameInspector = "game_inspector",
106 | }
107 | export enum GA_Button {
108 | Github = "github",
109 | Issues = "issues",
110 | Docs = "docs",
111 | QQ = "qq",
112 | /**
113 | * 当页面不支持cocos时,用户手动点击了刷新
114 | */
115 | FreshManual = "fresh-manual",
116 | }
117 |
--------------------------------------------------------------------------------
/src/i18n/en.ts:
--------------------------------------------------------------------------------
1 | export const title = "CCInspector";
2 |
--------------------------------------------------------------------------------
/src/i18n/zh.ts:
--------------------------------------------------------------------------------
1 | export const title = "CCInspector";
2 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import CCP from "cc-plugin/src/ccp/entry-main";
2 | import { BuilderOptions } from "cc-plugin/src/declare";
3 | import pluginConfig from "../cc-plugin.config";
4 |
5 | CCP.init(pluginConfig, {
6 | load: () => {
7 | console.log("plugin load");
8 | },
9 | builder: {
10 | onAfterBuild(target: BuilderOptions) {},
11 | },
12 | messages: {
13 | showPanel() {
14 | CCP.Adaptation.Panel.open("self.main");
15 | },
16 | },
17 | });
18 |
--------------------------------------------------------------------------------
/src/panel/index.ts:
--------------------------------------------------------------------------------
1 | import { loadSlim } from "@tsparticles/slim";
2 | import Particles from "@tsparticles/vue3";
3 | import ccui from "@xuyanfeng/cc-ui";
4 | import "@xuyanfeng/cc-ui/dist/ccui.css";
5 | import "@xuyanfeng/cc-ui/iconfont/iconfont.css";
6 | import CCP from "cc-plugin/src/ccp/entry-render";
7 | import { createApp } from "vue";
8 | import pluginConfig from "../../cc-plugin.config";
9 | import App from "./index.vue";
10 | export default CCP.init(pluginConfig, {
11 | ready: function (rootElement: any, args: any) {
12 | const app = createApp(App);
13 | app.use(ccui);
14 | //@ts-ignore
15 | app.use(Particles, {
16 | init: async (engine) => {
17 | await loadSlim(engine);
18 | },
19 | });
20 | app.mount(rootElement);
21 | },
22 | });
23 |
--------------------------------------------------------------------------------
/src/panel/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
38 |
39 |
157 |
158 |
262 |
--------------------------------------------------------------------------------
/src/panel/res/chrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tidys/cc-inspector-chrome/a0329f741412c121ffc949daafbd5d86bf2f38cf/src/panel/res/chrome.png
--------------------------------------------------------------------------------
/src/panel/res/edge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tidys/cc-inspector-chrome/a0329f741412c121ffc949daafbd5d86bf2f38cf/src/panel/res/edge.png
--------------------------------------------------------------------------------
/src/scripts/background/content.ts:
--------------------------------------------------------------------------------
1 | import { debugLog, Page, PluginEvent } from "../../core/types";
2 | import { FrameDetails } from "../../views/devtools/data";
3 | import { Terminal } from "../terminal";
4 | import { TabInfo } from "./tabInfo";
5 |
6 | export class Content {
7 | public frameID: number = 0;
8 | /**
9 | * port的名字标识
10 | */
11 | public name: string = Page.None;
12 | /**
13 | * tab.id作为唯一标识
14 | */
15 | public tabID: number | null = null;
16 | public title: string = "";
17 | public url: string = "";
18 | protected port: chrome.runtime.Port | null = null;
19 | public tab: chrome.tabs.Tab | null = null;
20 | public terminal: Terminal = null;
21 | /**
22 | * 是否正在使用
23 | */
24 | public using: boolean = false;
25 | private tabInfo: TabInfo | null = null;
26 | constructor(tab: chrome.tabs.Tab, port: chrome.runtime.Port, tabInfo: TabInfo) {
27 | this.tabInfo = tabInfo;
28 | this.port = port;
29 | this.tab = tab;
30 | this.name = port.name;
31 | this.tabID = tab.id;
32 | this.url = port.sender.url;
33 | this.title = tab.title;
34 | this.terminal = new Terminal(`Port-${this.name}`);
35 | port.onMessage.addListener((data: any, port: chrome.runtime.Port) => {
36 | const event = PluginEvent.create(data);
37 | debugLog && console.log(...this.terminal.chunkMessage(event.toChunk()));
38 | if (event.valid && this.onMessage) {
39 | this.onMessage(event);
40 | } else {
41 | debugLog && console.log(...this.terminal.log(JSON.stringify(data)));
42 | }
43 | });
44 | port.onDisconnect.addListener((port: chrome.runtime.Port) => {
45 | const ret = ["localhost", "127.0.0.1"].find((el) => port.sender.url.includes(el));
46 | if (ret) {
47 | console.log("local port disconnect");
48 | // debugger;
49 | }
50 | debugLog && console.log(...this.terminal.disconnect(""));
51 | this.onDisconnect(port);
52 | });
53 | this.frameID = port.sender.frameId || 0;
54 | }
55 | getFrameDetais(): FrameDetails {
56 | return {
57 | tabID: this.tabID,
58 | url: this.url,
59 | frameID: this.frameID,
60 | };
61 | }
62 | private onDisconnect(disPort: chrome.runtime.Port) {
63 | this.tabInfo.removePort(this);
64 | }
65 |
66 | public onMessage(data: PluginEvent) {
67 | // content的数据一般都是要同步到devtools
68 | if (data.isTargetDevtools()) {
69 | if (this.tabInfo.devtool) {
70 | this.tabInfo.devtool.send(data);
71 | } else {
72 | debugger;
73 | }
74 | } else {
75 | debugger;
76 | }
77 | }
78 |
79 | send(data: PluginEvent) {
80 | if (this.port) {
81 | this.port.postMessage(data);
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/scripts/background/devtools.ts:
--------------------------------------------------------------------------------
1 | import { debugLog, Msg, Page, PluginEvent, RequestUseFrameData } from "../../core/types";
2 | import { Terminal } from "../terminal";
3 | import { TabInfo } from "./tabInfo";
4 |
5 | export class Devtools {
6 | /**
7 | * port的名字标识
8 | */
9 | public name: string = Page.None;
10 | /**
11 | * tab.id作为唯一标识
12 | */
13 | public tabID: number | null = null;
14 | public title: string = "";
15 | public url: string = "";
16 | protected port: chrome.runtime.Port | null = null;
17 | public tab: chrome.tabs.Tab | null = null;
18 |
19 | public terminal: Terminal = null;
20 | public tabInfo: TabInfo | null = null;
21 | constructor(port: chrome.runtime.Port, tabInfo: TabInfo) {
22 | this.tabInfo = tabInfo;
23 | this.port = port;
24 | this.name = port.name;
25 | this.url = port.sender.url;
26 | this.terminal = new Terminal(`Port-${this.name}`);
27 | port.onMessage.addListener((data: any, port: chrome.runtime.Port) => {
28 | const event = PluginEvent.create(data);
29 | debugLog && console.log(...this.terminal.chunkMessage(event.toChunk()));
30 | if (event.valid && this.onMessage) {
31 | this.onMessage(event);
32 | } else {
33 | debugLog && console.log(...this.terminal.log(JSON.stringify(data)));
34 | }
35 | });
36 | port.onDisconnect.addListener((port: chrome.runtime.Port) => {
37 | const ret = ["localhost", "127.0.0.1"].find((el) => port.sender.url.includes(el));
38 | if (ret) {
39 | debugger;
40 | }
41 | debugLog && console.log(...this.terminal.disconnect(""));
42 | this.onDisconnect(port);
43 | });
44 | }
45 | public onDisconnect(port: chrome.runtime.Port) {
46 | this.tabInfo.removeDevtools(this);
47 | }
48 | public onMessage(data: PluginEvent) {
49 | if (data.msg === Msg.RequestUseFrame) {
50 | // 因为devtool是定时器驱动,这里改变了content,后续就会将数据派发到对应的content中去
51 | this.tabInfo.useFrame((data.data as RequestUseFrameData).id);
52 | } else if (data.msg === Msg.RequestDisconnectDevtools) {
53 | if (this.port) {
54 | this.port.disconnect();
55 | this.tabInfo.devtool = null;
56 | }
57 | } else {
58 | // 从devtools过来的消息统一派发到目标content中
59 | if (data.check(Page.Devtools, Page.Background)) {
60 | data.reset(Page.Background, Page.Content);
61 | this.tabInfo.sendMsgToContent(data);
62 | }
63 | }
64 | }
65 | send(data: PluginEvent) {
66 | if (this.port) {
67 | this.port.postMessage(data);
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/scripts/background/index.ts:
--------------------------------------------------------------------------------
1 | import { debugLog, Msg, Page, PluginEvent } from "../../core/types";
2 | import { getDevToolsInspectorId } from "../../core/util";
3 | import { Terminal } from "../terminal";
4 | import "./notify";
5 | import { tabMgr } from "./tabMgr";
6 | const terminal = new Terminal(Page.Background);
7 | debugLog && console.log(...terminal.init());
8 |
9 | chrome.runtime.onConnect.addListener((port: chrome.runtime.Port) => {
10 | if (port.name === Page.Content) {
11 | const tab: chrome.tabs.Tab | undefined = port.sender?.tab;
12 | const tabID = tab.id;
13 | if (tabID === undefined || tabID <= 0) {
14 | return;
15 | }
16 | tabMgr.addTab(tab, port);
17 | } else if (port.name.startsWith(Page.Devtools)) {
18 | const id = getDevToolsInspectorId(port.name);
19 | console.log(`devtools tab id is ${id}`);
20 | const tab = tabMgr.findTab(id);
21 | if (tab) {
22 | tab.addDevtools(port);
23 | } else {
24 | // 没有发现与之对应的调试content,主动断开链接
25 | const event = new PluginEvent(Page.Background, Page.Devtools, Msg.DevtoolConnectError, "missing content");
26 | port.postMessage(event);
27 | port.disconnect();
28 | }
29 | }
30 | });
31 | chrome.runtime.onMessage.addListener((request: PluginEvent, sender: any, sendResponse: any) => {
32 | const event = PluginEvent.create(request);
33 | const tabID = sender.tab.id;
34 | const tabInfo = tabMgr.findTab(tabID);
35 | if (tabInfo) {
36 | if (event.check(Page.Content, Page.Background)) {
37 | // 监听来自content.js发来的事件,将消息转发到devtools
38 | event.reset(Page.Background, Page.Devtools);
39 | console.log(`%c[Message]url:${sender.url}]\n${JSON.stringify(request)}`, "color:green");
40 | if (tabInfo.devtool) {
41 | tabInfo.devtool.send(request);
42 | }
43 | }
44 | }
45 | });
46 | chrome.tabs.onActivated.addListener(({ tabId, windowId }) => {});
47 | chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
48 | //
49 | });
50 | chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
51 | // 页面发生刷新,通知重新生成数据
52 | if (changeInfo.status === "complete") {
53 | const { id } = tab;
54 | // -1为自己
55 | if (id >= 0) {
56 | const tabInfo = tabMgr.findTab(id);
57 | if (tabInfo) {
58 | tabInfo.useFrame(0);
59 | }
60 | }
61 | }
62 | });
63 |
--------------------------------------------------------------------------------
/src/scripts/background/notify.ts:
--------------------------------------------------------------------------------
1 | import { gt } from "semver";
2 | import PKG from "../../../cc-plugin.config";
3 | import { ga } from "../../ga";
4 | import { GA_EventName } from "../../ga/type";
5 | import { githubMirrorMgr } from "../inject-view/github";
6 | import { getAdData, NotifyButton } from "../inject-view/loader";
7 |
8 | export const TipUpdate = "tip-update";
9 | (async () => {
10 | interface ConfigItem {
11 | id: string;
12 | click?: Function;
13 | closed?: Function;
14 | title: string;
15 | message: string;
16 | /**
17 | * 检查是否可以创建通知
18 | * @returns 返回true,则会创建通知
19 | */
20 | check: (item: ConfigItem) => Promise;
21 | buttons?: Array<{ title: string; click?: Function }>;
22 | }
23 | function goRate() {
24 | const url = PKG.manifest.chrome.url;
25 | if (url) {
26 | // chrome.windows.create({
27 | // url: url,
28 | // type: "normal",
29 | // });
30 | chrome.tabs.create({ url: url });
31 | ga.fireEventWithParam(GA_EventName.Rate, "go");
32 | }
33 | }
34 | const KeyHasRate = "has-rate";
35 | const config: ConfigItem[] = [
36 | {
37 | id: "rate",
38 | title: "Hi",
39 | message: "如果不是真爱,你也不会使用这么长时间,求五星好评!",
40 | click: () => {
41 | goRate();
42 | },
43 | closed: () => {
44 | console.log("closed");
45 | },
46 | check: async (cfg: ConfigItem) => {
47 | const result = await chrome.storage.local.get(KeyHasRate);
48 | if (result[KeyHasRate]) {
49 | // 已经评价过了
50 | return false;
51 | }
52 | const KeyInstallTime = "install-time";
53 | const KeyLatestShowTime = "latest-show-time";
54 |
55 | let res1 = await chrome.storage.local.get(KeyInstallTime);
56 | const time1 = res1[KeyInstallTime];
57 | if (!time1) {
58 | // 首次安装
59 | chrome.storage.local.set({ [KeyInstallTime]: new Date().getTime() });
60 | return false;
61 | }
62 |
63 | const diff = (new Date().getTime() - time1) / 1000;
64 | const afterInstall = 60 * 60 * 24 * 3; // 安装3天后
65 | if (diff <= afterInstall) {
66 | // 安装后一段时间不显示
67 | return false;
68 | }
69 | let canShow = false;
70 | const res = await chrome.storage.local.get(KeyLatestShowTime);
71 | const time = res[KeyLatestShowTime];
72 | if (time) {
73 | // 检查距离上次弹出是否超过指定时间
74 | const diff = (new Date().getTime() - time) / 1000;
75 | const afterLatestShow = 60 * 60 * 24 * 1; // 一天一次
76 | canShow = diff > afterLatestShow;
77 | } else {
78 | // 首次弹出
79 | canShow = true;
80 | }
81 | if (!canShow) {
82 | return false;
83 | }
84 | chrome.storage.local.set({ [KeyLatestShowTime]: new Date().getTime() });
85 | return true;
86 | },
87 | buttons: [
88 | {
89 | title: "我已评价",
90 | click: () => {
91 | chrome.storage.local.set({ [KeyHasRate]: true });
92 | ga.fireEventWithParam(GA_EventName.Rate, "has rate");
93 | },
94 | },
95 | {
96 | title: "前往评价",
97 | click: () => {
98 | goRate();
99 | },
100 | },
101 | ],
102 | },
103 | ];
104 | try {
105 | await githubMirrorMgr.init();
106 | // 版本检查
107 | const data = await githubMirrorMgr.getData("version.json");
108 | if (data) {
109 | const info = data as { ver: string };
110 | const b = gt(info.ver || "0.0.0", PKG.manifest.version);
111 | if (info.ver && b) {
112 | config.push({
113 | id: "update",
114 | title: `${PKG.manifest.name}发现新版本${info.ver || ""}`,
115 | message: `点击查看`,
116 | click: () => {
117 | goRate();
118 | },
119 | check: async () => {
120 | return true;
121 | },
122 | });
123 | }
124 | }
125 | // 广告分析
126 | const adData = await getAdData();
127 | if (adData && adData.notify.length) {
128 | adData.notify.forEach((el) => {
129 | const KeyIKnow = `i-know-${el.id}`;
130 | config.push({
131 | id: el.id,
132 | title: el.title,
133 | message: el.msg,
134 | buttons: el.buttons.map((btn) => {
135 | const map = {};
136 | map[NotifyButton.IKnow] = "我知道了";
137 | map[NotifyButton.Go] = "前往";
138 | return {
139 | title: map[btn],
140 | click: () => {
141 | if (btn === NotifyButton.Go) {
142 | chrome.tabs.create({ url: el.url });
143 | } else if (btn === NotifyButton.IKnow) {
144 | chrome.storage.local.set({ [`${KeyIKnow}`]: true });
145 | }
146 | },
147 | };
148 | }),
149 | click: () => {
150 | chrome.tabs.create({ url: el.url });
151 | },
152 | check: async () => {
153 | // 检查是否已经过期
154 | const now = new Date().getTime();
155 | if (el.deadTime && now > new Date(el.deadTime).getTime()) {
156 | return false;
157 | }
158 | // 检查是否已经取消了
159 | const result = await chrome.storage.local.get(KeyIKnow);
160 | if (result[KeyIKnow]) {
161 | return false;
162 | }
163 | // 距离上次是否已经过去了指定的时间间隔
164 | const KeyLatestShowTime = `latest-show-${el.id}`;
165 | const res = await chrome.storage.local.get(KeyLatestShowTime);
166 | const time = res[KeyLatestShowTime];
167 | if (time) {
168 | const diff = (new Date().getTime() - time) / 1000 / 60; // 过去了多少分钟
169 | if (diff <= el.duration) {
170 | return false;
171 | }
172 | }
173 | chrome.storage.local.set({ [KeyLatestShowTime]: new Date().getTime() });
174 | return true;
175 | },
176 | });
177 | });
178 | }
179 | } catch (e) {
180 | console.error(e);
181 | }
182 | chrome.notifications.onClicked.addListener((id) => {
183 | const ret = config.find((el) => el.id === id);
184 | if (ret) {
185 | ret.click && ret.click();
186 | }
187 | });
188 | chrome.notifications.onClosed.addListener((id) => {
189 | const ret = config.find((el) => el.id === id);
190 | if (ret) {
191 | ret.closed && ret.closed();
192 | }
193 | });
194 | chrome.notifications.onButtonClicked.addListener((notificationId, buttonIndex) => {
195 | const ret = config.find((el) => el.id === notificationId);
196 | if (!ret.buttons) {
197 | return;
198 | }
199 | const btn = ret.buttons[buttonIndex];
200 | if (!btn) {
201 | return;
202 | }
203 | btn.click && btn.click();
204 | });
205 |
206 | for (let i = 0; i < config.length; i++) {
207 | await createNotification(config[i]);
208 | }
209 |
210 | async function createNotification(config: ConfigItem) {
211 | const b = await config.check(config);
212 | if (!b) {
213 | return;
214 | }
215 | const { title, buttons, message, id } = config;
216 | chrome.notifications.create(
217 | id,
218 | {
219 | type: "basic",
220 | iconUrl: "icons/48.png",
221 | title: title,
222 | message: message,
223 | buttons: buttons ? buttons.map((el) => ({ title: el.title })) : [],
224 | },
225 | (id: string) => {}
226 | );
227 | }
228 | })();
229 |
--------------------------------------------------------------------------------
/src/scripts/background/tabInfo.ts:
--------------------------------------------------------------------------------
1 | import { Msg, Page, PluginEvent, ResponseUpdateFramesData, ResponseUseFrameData } from "../../core/types";
2 | import { FrameDetails } from "../../views/devtools/data";
3 | import { Content } from "./content";
4 | import { Devtools } from "./devtools";
5 |
6 | export class TabInfo {
7 | /**
8 | * 标签的ID
9 | */
10 | public tabID: number;
11 | constructor(tabID: number) {
12 | this.tabID = tabID;
13 | }
14 | /**
15 | * 因为iframe的原因,可能对应多个,主iframe的id是0
16 | */
17 | public contentArray: Array = [];
18 | addContent(tab: chrome.tabs.Tab, port: chrome.runtime.Port) {
19 | // 新的content连上来,需要更新devtools
20 | let portContent: Content = new Content(tab, port, this);
21 | this.contentArray.push(portContent);
22 | this.updateFrames();
23 | // 之前持有的链接会断开,content会重新发起链接,重新连上之后,如果只有一个人链接,需要设置using状态,否则devtools找不到content
24 | if (this.contentArray.length === 1) {
25 | const id = this.contentArray[0].frameID;
26 | this.useFrame(id);
27 | }
28 | }
29 | public removePort(item: Content) {
30 | let index = this.contentArray.findIndex((el) => el === item);
31 | if (index > -1) {
32 | this.contentArray.splice(index, 1);
33 | this.updateFrames();
34 |
35 | // 使用第一个frame
36 | if (this.contentArray.length) {
37 | const id = this.contentArray[0].frameID;
38 | this.useFrame(id);
39 | }
40 | }
41 | }
42 | public removeDevtools(item: Devtools) {
43 | this.devtool = null;
44 | }
45 | useFrame(id: number) {
46 | this.contentArray.map((content) => {
47 | content.using = content.frameID === id;
48 | });
49 | this.sendMsgToDevtool(Msg.ResponseUseFrame, { id } as ResponseUseFrameData);
50 | }
51 | /**
52 | * 通知devtools更新
53 | */
54 | private updateFrames() {
55 | const data: FrameDetails[] = [];
56 | this.contentArray.forEach((item) => {
57 | const frame = (item as Content).getFrameDetais();
58 | data.push(frame);
59 | });
60 | this.sendMsgToDevtool(Msg.ResponseUpdateFrames, data as ResponseUpdateFramesData);
61 | }
62 | private sendMsgToDevtool(msg: Msg, data: any) {
63 | if (this.devtool) {
64 | const event = new PluginEvent(Page.Background, Page.Devtools, msg, data);
65 | this.devtool.send(event);
66 | }
67 | }
68 | public sendMsgToContent(data: PluginEvent) {
69 | const content = this.contentArray.find((el) => el.using);
70 | if (content) {
71 | content.send(data);
72 | } else {
73 | // 当页面没有完成刷新状态时,conent并没有using,就会触发此处逻辑
74 | // 在页面完成刷新后,会主动设置为using
75 | }
76 | }
77 | public devtool: Devtools | null = null;
78 | addDevtools(port: chrome.runtime.Port) {
79 | if (this.devtool === null) {
80 | this.devtool = new Devtools(port, this);
81 | } else {
82 | debugger;
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/scripts/background/tabMgr.ts:
--------------------------------------------------------------------------------
1 | import { TabInfo } from "./tabInfo";
2 |
3 | export class TabMgr {
4 | constructor() {
5 | this.tabArray = [];
6 | }
7 | /**
8 | * chrome打开的所有标签页面
9 | */
10 | public tabArray: TabInfo[] = [];
11 | public findTab(id: number): TabInfo | null {
12 | return this.tabArray.find((el) => el.tabID === id) || null;
13 | }
14 |
15 | addTab(tab: chrome.tabs.Tab, port: chrome.runtime.Port) {
16 | let tabInfo = this.findTab(tab.id);
17 | if (!tabInfo) {
18 | tabInfo = new TabInfo(tab.id);
19 | this.tabArray.push(tabInfo);
20 | }
21 | tabInfo.addContent(tab, port);
22 | }
23 | }
24 | export const tabMgr = new TabMgr();
25 |
--------------------------------------------------------------------------------
/src/scripts/const.ts:
--------------------------------------------------------------------------------
1 | import { GA_EventName } from "../ga/type";
2 |
3 | export enum DocumentEvent {
4 | /**
5 | * 从inject到content的事件
6 | */
7 | Inject2Content = "inject2content",
8 | /**
9 | * 从content到inject的事件
10 | */
11 | Content2Inject = "content2inject",
12 | EngineVersion = "engineVersion",
13 | GoogleAnalytics = "googleAnalytics",
14 | LoadInjectCss = "load-inject-css",
15 | InspectorClear = "inspector_clear",
16 | GameInspectorBegan = "GameInspectorBegan",
17 | GameInspectorEnd = "GameInspectorEnd",
18 | }
19 | export interface GoogleAnalyticsData {
20 | event: GA_EventName;
21 | params: string;
22 | }
23 |
--------------------------------------------------------------------------------
/src/scripts/content/index.ts:
--------------------------------------------------------------------------------
1 | // content.js 和原始界面共享DOM,具有操作dom的能力
2 | // 但是不共享js,要想访问页面js,只能通过注入的方式
3 | import { ChromeConst } from "cc-plugin/src/chrome/const";
4 | import { debugLog, Page, PluginEvent } from "../../core/types";
5 | import { ga } from "../../ga";
6 | import { GA_EventName } from "../../ga/type";
7 | import { DocumentEvent, GoogleAnalyticsData } from "../const";
8 | import { Terminal } from "../terminal";
9 |
10 | const terminal = new Terminal(Page.Content);
11 | debugLog && console.log(...terminal.init());
12 |
13 | // #region 注入脚本
14 | export function injectScript(url: string) {
15 | if (chrome && chrome.runtime && chrome.runtime.getURL) {
16 | let content = chrome.runtime.getURL(url);
17 | const script = document.createElement("script");
18 | script.setAttribute("type", "text/javascript");
19 | script.setAttribute("src", content);
20 | script.onload = function () {
21 | // 加载注入脚本界面的css
22 | let css = chrome.runtime.getURL(ChromeConst.css.inject_view);
23 | const event = new CustomEvent(DocumentEvent.LoadInjectCss, { detail: [css] });
24 | document.dispatchEvent(event);
25 | document.head.removeChild(script);
26 | };
27 | document.head.appendChild(script);
28 | debugLog && console.log(...terminal.green(`inject script success: ${content}`));
29 | } else {
30 | debugLog && console.log(...terminal.red("inject script failed"));
31 | }
32 | }
33 |
34 | document.addEventListener(DocumentEvent.EngineVersion, async (event: CustomEvent) => {
35 | const version: string = event.detail;
36 | if (version) {
37 | ga.fireEventWithParam(GA_EventName.EngineVersion, version);
38 | }
39 | });
40 | document.addEventListener(DocumentEvent.GoogleAnalytics, (event: CustomEvent) => {
41 | const data: GoogleAnalyticsData = event.detail;
42 | if (data && data.event) {
43 | if (data.params) {
44 | ga.fireEventWithParam(data.event, data.params);
45 | } else {
46 | ga.fireEvent(data.event);
47 | }
48 | }
49 | });
50 | // #region 和Inject通讯
51 | document.addEventListener(DocumentEvent.Inject2Content, (event: CustomEvent) => {
52 | let data: PluginEvent = PluginEvent.create(event.detail);
53 | if (data.valid && data.check(Page.Inject, Page.Content)) {
54 | debugLog && console.log(...terminal.chunkMessage(data.toChunk()));
55 | data.reset(Page.Content, Page.Devtools);
56 | if (connect) {
57 | // 接受来自inject.js的消息数据,然后中转到background.js
58 | connect.postMessage(data);
59 | } else {
60 | debugLog && console.log(...terminal.log(`connect is null`));
61 | console.log("connect is null");
62 | }
63 | } else {
64 | throw new Error(`invalid data: ${event.detail}`);
65 | }
66 | });
67 | // #region 和background通讯
68 | let connect: chrome.runtime.Port = null;
69 | function doConnect() {
70 | connect = chrome.runtime.connect({ name: Page.Content });
71 | connect.onDisconnect.addListener(() => {
72 | debugLog && console.log(...terminal.disconnect(""));
73 | connect = null;
74 | doConnect();
75 | });
76 | connect.onMessage.addListener((data: PluginEvent, sender: chrome.runtime.Port) => {
77 | const event = PluginEvent.create(data);
78 | if (event.valid && event.check(Page.Background, Page.Content)) {
79 | debugLog && console.log(...terminal.chunkMessage(event.toChunk()));
80 | event.reset(Page.Content, Page.Inject);
81 | const e = new CustomEvent(DocumentEvent.Content2Inject, { detail: event });
82 | debugLog && console.log(...terminal.chunkSend(event.toChunk()));
83 | document.dispatchEvent(e);
84 | } else {
85 | throw new Error(`invalid data: ${data}`);
86 | }
87 | });
88 | }
89 | doConnect();
90 | injectScript(ChromeConst.script.inject);
91 |
--------------------------------------------------------------------------------
/src/scripts/inject-view/ad.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
60 |
81 |
--------------------------------------------------------------------------------
/src/scripts/inject-view/banner.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ data.name }}
6 |
7 |
8 |
9 |
10 |
57 |
58 |
123 |
--------------------------------------------------------------------------------
/src/scripts/inject-view/const.ts:
--------------------------------------------------------------------------------
1 | import { TinyEmitter } from "tiny-emitter";
2 | export const Msg = {
3 | ChangeAd: "ChangeAd",
4 | };
5 | export const emitter = new TinyEmitter();
6 |
--------------------------------------------------------------------------------
/src/scripts/inject-view/github.ts:
--------------------------------------------------------------------------------
1 | export interface MirrorInfo {
2 | /**
3 | * 请求的url
4 | */
5 | name: string;
6 | /**
7 | * 上次请求成功的时间
8 | */
9 | time: number;
10 | }
11 | class Config {
12 | private key = "cc-inspector-ad-config";
13 | private data: MirrorInfo[] = [];
14 | async init() {
15 | try {
16 | let str: string = "";
17 | if (this.isNormalWebEnv()) {
18 | str = localStorage.getItem(this.key);
19 | } else if (this.isChromeBackgroundEnv()) {
20 | const ret = await chrome.storage.local.get(this.key);
21 | if (ret && ret[this.key]) {
22 | str = ret[this.key] as string;
23 | }
24 | }
25 | if (str) {
26 | const ret = JSON.parse(str) as MirrorInfo[];
27 | if (ret) {
28 | this.data = [];
29 | ret.forEach((el) => {
30 | this.data.push({ name: el.name, time: el.time });
31 | });
32 | }
33 | }
34 | } catch {
35 | debugger;
36 | }
37 | }
38 | private isChromeBackgroundEnv() {
39 | return typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.local !== "undefined";
40 | }
41 | private isNormalWebEnv() {
42 | return typeof localStorage !== "undefined";
43 | }
44 | async save(name: string, time: number) {
45 | const ret = this.data.find((el) => el.name === name);
46 | if (ret) {
47 | ret.time = time;
48 | } else {
49 | this.data.push({ name: name, time: time } as MirrorInfo);
50 | }
51 | const saveString = JSON.stringify(this.data);
52 |
53 | if (this.isNormalWebEnv()) {
54 | localStorage.setItem(this.key, saveString);
55 | } else if (this.isChromeBackgroundEnv()) {
56 | await chrome.storage.local.set({ [this.key]: saveString });
57 | }
58 | }
59 | getTime(url: string) {
60 | const ret = this.data.find((el) => el.name === url);
61 | if (ret) {
62 | return ret.time;
63 | }
64 | return 0;
65 | }
66 | }
67 | export class GithubMirror {
68 | owner: string = "tidys";
69 | repo: string = "cc-inspector-ad";
70 | branch: string = "main";
71 | /**
72 | * 上次请求成功的时间
73 | */
74 | time: number = 0;
75 | /**
76 | * 镜像的名字
77 | */
78 | name: string = "";
79 | private calcUrl: Function;
80 | constructor(name: string, cb: Function) {
81 | this.name = name;
82 | this.time = cfg.getTime(name);
83 | this.calcUrl = cb;
84 | }
85 | public getUrl(file: string) {
86 | if (!file) {
87 | return "";
88 | }
89 | if (this.calcUrl) {
90 | return this.calcUrl(this.owner, this.repo, this.branch, file);
91 | } else {
92 | return "";
93 | }
94 | }
95 | public async getData(file: string) {
96 | const url = this.getUrl(file);
97 | if (url) {
98 | const data = await this.reqFecth(url);
99 | return data;
100 | }
101 | return null;
102 | }
103 | private reqFecth(url: string): Promise