├── .all-contributorsrc ├── .dcignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── codeql │ └── codeql-config.yml └── workflows │ ├── ChangeRelease.yml │ ├── DockerHub-Description.yml │ ├── Publish Ja3 Package.yml │ ├── Publish Lite Package.yml │ ├── Publish Package.yml │ ├── codeql-analysis.yml │ └── deploy-webdoc.yml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── Dockerfile.ja3 ├── Dockerfile.lite ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── Procfile ├── README.md ├── SECURITY.md ├── backup.py ├── chrole.py ├── config.py ├── config └── .gitkeep ├── db ├── __init__.py ├── basedb.py ├── db_converter.py ├── notepad.py ├── pubtpl.py ├── push_request.py ├── redisdb.py ├── site.py ├── task.py ├── tasklog.py ├── tpl.py └── user.py ├── docker-compose.yml ├── libs ├── __init__.py ├── convert.py ├── cookie_utils.py ├── fetcher.py ├── funcs.py ├── json_typing.py ├── log.py ├── mcrypto.py ├── parse_url.py ├── safe_eval.py └── utils.py ├── qd.py ├── requirements.txt ├── run.py ├── ssh ├── qd_fetch └── qd_fetch.pub ├── update.sh ├── version.json ├── web.py ├── web ├── .bowerrc ├── Gruntfile.js ├── __init__.py ├── app.py ├── bower.json ├── docs │ ├── .vitepress │ │ ├── config.ts │ │ └── locales │ │ │ ├── en_US.ts │ │ │ ├── index.ts │ │ │ └── zh_CN.ts │ ├── guide │ │ ├── deployment.md │ │ ├── faq.md │ │ ├── how-to-use.md │ │ ├── update.md │ │ └── what-is-qd.md │ ├── index.md │ ├── public │ │ ├── favicon.ico │ │ ├── index.png │ │ ├── login.png │ │ ├── logo.png │ │ ├── preserve_log.png │ │ └── save_har.png │ └── zh_CN │ │ ├── guide │ │ ├── deployment.md │ │ ├── faq.md │ │ ├── how-to-use.md │ │ ├── update.md │ │ └── what-is-qd.md │ │ └── index.md ├── handlers │ ├── __init__.py │ ├── about.py │ ├── base.py │ ├── har.py │ ├── index.py │ ├── login.py │ ├── my.py │ ├── push.py │ ├── site.py │ ├── subscribe.py │ ├── task.py │ ├── task_multi.py │ ├── tpl.py │ ├── user.py │ └── util.py ├── package.json ├── pnpm-lock.yaml ├── static │ ├── coffee │ │ ├── components │ │ │ └── utils.coffee │ │ └── har │ │ │ ├── analysis.coffee │ │ │ ├── contenteditable.coffee │ │ │ ├── editablelist.coffee │ │ │ ├── editor.coffee │ │ │ ├── entry_editor.coffee │ │ │ ├── entry_list.coffee │ │ │ └── upload_ctrl.coffee │ ├── components │ │ ├── Font-Awesome │ │ │ ├── css │ │ │ │ └── all.min.css │ │ │ └── webfonts │ │ │ │ ├── fa-brands-400.ttf │ │ │ │ ├── fa-brands-400.woff2 │ │ │ │ ├── fa-regular-400.ttf │ │ │ │ ├── fa-regular-400.woff2 │ │ │ │ ├── fa-solid-900.ttf │ │ │ │ ├── fa-solid-900.woff2 │ │ │ │ ├── fa-v4compatibility.ttf │ │ │ │ └── fa-v4compatibility.woff2 │ │ ├── angular │ │ │ └── angular.min.js │ │ ├── blueimp-md5 │ │ │ └── js │ │ │ │ └── md5.js │ │ ├── bootstrap │ │ │ └── dist │ │ │ │ ├── css │ │ │ │ ├── bootstrap-theme.css │ │ │ │ ├── bootstrap-theme.css.map │ │ │ │ ├── bootstrap-theme.min.css │ │ │ │ ├── bootstrap.css │ │ │ │ ├── bootstrap.css.map │ │ │ │ └── bootstrap.min.css │ │ │ │ ├── fonts │ │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ │ └── glyphicons-halflings-regular.woff │ │ │ │ └── js │ │ │ │ ├── bootstrap.js │ │ │ │ └── bootstrap.min.js │ │ ├── clipboard │ │ │ └── dist │ │ │ │ └── clipboard.min.js │ │ ├── download.js │ │ ├── draggable-polyfill │ │ │ └── lib │ │ │ │ └── draggable-polyfill.js │ │ ├── editor_js.js │ │ ├── groups_op.js │ │ ├── jquery.tablesorter │ │ │ ├── css │ │ │ │ ├── dragtable.mod.css │ │ │ │ ├── filter.formatter.css │ │ │ │ ├── highlights.css │ │ │ │ ├── theme.black-ice.css │ │ │ │ ├── theme.blue.css │ │ │ │ ├── theme.bootstrap.css │ │ │ │ ├── theme.bootstrap_2.css │ │ │ │ ├── theme.bootstrap_3.css │ │ │ │ ├── theme.bootstrap_4.css │ │ │ │ ├── theme.dark.css │ │ │ │ ├── theme.default.css │ │ │ │ ├── theme.dropbox.css │ │ │ │ ├── theme.green.css │ │ │ │ ├── theme.grey.css │ │ │ │ ├── theme.ice.css │ │ │ │ ├── theme.jui.css │ │ │ │ ├── theme.materialize.css │ │ │ │ ├── theme.metro-dark.css │ │ │ │ └── widget.grouping.css │ │ │ └── dist │ │ │ │ └── js │ │ │ │ ├── jquery.tablesorter.combined.min.js │ │ │ │ ├── jquery.tablesorter.min.js │ │ │ │ └── jquery.tablesorter.widgets.min.js │ │ ├── jquery │ │ │ └── dist │ │ │ │ └── jquery.min.js │ │ ├── js-base64 │ │ │ └── base64.js │ │ ├── lunar-javascript │ │ │ └── lunar.js │ │ ├── misc.js │ │ ├── mutil_op.js │ │ ├── node_components.js │ │ ├── node_components.js.LICENSE.txt │ │ ├── node_components.tpl.js │ │ ├── nprogress │ │ │ ├── nprogress.css │ │ │ └── nprogress.js │ │ ├── seajs │ │ │ └── dist │ │ │ │ └── sea.js │ │ ├── select2 │ │ │ └── dist │ │ │ │ ├── css │ │ │ │ └── select2.min.css │ │ │ │ └── js │ │ │ │ ├── select2.full.min.js │ │ │ │ └── select2.min.js │ │ ├── utils.js │ │ └── webpack.config.js │ ├── css │ │ ├── base.css │ │ ├── index.css │ │ ├── less │ │ │ ├── base.less │ │ │ ├── index.less │ │ │ ├── my.less │ │ │ ├── register.less │ │ │ └── task_new.less │ │ ├── my.css │ │ ├── register.css │ │ └── task_new.css │ ├── cur │ │ ├── default.cur │ │ └── pointer.cur │ ├── har │ │ ├── analysis.js │ │ ├── contenteditable.js │ │ ├── editablelist.html │ │ ├── editablelist.js │ │ ├── editor.css │ │ ├── editor.js │ │ ├── editor.less │ │ ├── entry_editor.js │ │ ├── entry_list.js │ │ ├── partials │ │ │ └── upload.html │ │ └── upload_ctrl.js │ └── img │ │ ├── 120.png │ │ ├── 128.png │ │ ├── 152.png │ │ ├── 16.png │ │ ├── 167.png │ │ ├── 180.png │ │ ├── 192.png │ │ ├── 200.png │ │ ├── 32.png │ │ ├── 96.png │ │ ├── body.jpg │ │ ├── header.jpg │ │ ├── icon.png │ │ ├── icon_old.png │ │ ├── push_pic.png │ │ ├── slide_bg.png │ │ └── slide_target.png └── tpl │ ├── DB_manage.html │ ├── LeftFuncBtn.html │ ├── about.html │ ├── base.html │ ├── har │ ├── editor.html │ └── entry_editor.html │ ├── index.html │ ├── login.html │ ├── my.html │ ├── password_reset.html │ ├── password_reset_email.html │ ├── pubtpl_register.html │ ├── pubtpl_reposinfo.html │ ├── pubtpl_subscribe.html │ ├── pubtpl_unsubscribe.html │ ├── pubtpl_wait.html │ ├── push_list.html │ ├── register.html │ ├── site_manage.html │ ├── task_new.html │ ├── task_new_var.html │ ├── task_setTime.html │ ├── task_setgroup.html │ ├── tasklog.html │ ├── taskmulti.html │ ├── taskmulti_tasksinfo.html │ ├── toolbox-notepad.html │ ├── toolbox.html │ ├── totalLog.html │ ├── tpl_push.html │ ├── tpl_run_failed.html │ ├── tpl_run_success.html │ ├── tpl_setgroup.html │ ├── tpls_public.html │ ├── user_manage.html │ ├── user_register_cus_pusher.html │ ├── user_register_pusher.html │ ├── user_register_pushsw.html │ ├── user_setnewpwd.html │ ├── utils.html │ └── utils_run_result.html └── worker.py /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "a76yyyy", 10 | "name": "a76yyyy", 11 | "avatar_url": "https://avatars.githubusercontent.com/u/56478790?v=4", 12 | "profile": "http://www.a76yyyy.cn", 13 | "contributions": [ 14 | "design", 15 | "code", 16 | "maintenance" 17 | ] 18 | }, 19 | { 20 | "login": "Binux", 21 | "name": "Roy Binux", 22 | "avatar_url": "https://avatars.githubusercontent.com/u/646451?v=4", 23 | "profile": "http://binux.me/", 24 | "contributions": [ 25 | "design", 26 | "code", 27 | "maintenance" 28 | ] 29 | }, 30 | { 31 | "login": "AragonSnow", 32 | "name": "AragonSnow", 33 | "avatar_url": "https://avatars.githubusercontent.com/u/22835918?v=4", 34 | "profile": "https://github.com/AragonSnow", 35 | "contributions": [ 36 | "code", 37 | "design", 38 | "maintenance" 39 | ] 40 | }, 41 | { 42 | "login": "Mark-1215", 43 | "name": "Mark", 44 | "avatar_url": "https://avatars.githubusercontent.com/u/36469805?v=4", 45 | "profile": "https://www.quchao.net", 46 | "contributions": [ 47 | "design", 48 | "blog", 49 | "example", 50 | "doc" 51 | ] 52 | }, 53 | { 54 | "login": "cdpidan", 55 | "name": "pidan", 56 | "avatar_url": "https://avatars.githubusercontent.com/u/8141453?v=4", 57 | "profile": "https://github.com/cdpidan", 58 | "contributions": [ 59 | "design" 60 | ] 61 | }, 62 | { 63 | "login": "buzhibujuelb", 64 | "name": "buzhibujue", 65 | "avatar_url": "https://avatars.githubusercontent.com/u/24644841?v=4", 66 | "profile": "https://buzhibujue.cf", 67 | "contributions": [ 68 | "code" 69 | ] 70 | }, 71 | { 72 | "login": "billypon", 73 | "name": "billypon", 74 | "avatar_url": "https://avatars.githubusercontent.com/u/1763302?v=4", 75 | "profile": "https://github.com/billypon", 76 | "contributions": [ 77 | "code" 78 | ] 79 | }, 80 | { 81 | "login": "acooler15", 82 | "name": "acooler15", 83 | "avatar_url": "https://avatars.githubusercontent.com/u/19186382?v=4", 84 | "profile": "http://www.lingyan8.com", 85 | "contributions": [ 86 | "code", 87 | "maintenance" 88 | ] 89 | }, 90 | { 91 | "login": "aa889788", 92 | "name": "shxyke", 93 | "avatar_url": "https://avatars.githubusercontent.com/u/16019986?v=4", 94 | "profile": "https://github.com/aa889788", 95 | "contributions": [ 96 | "code" 97 | ] 98 | }, 99 | { 100 | "login": "gxitm", 101 | "name": "xiaoxiao", 102 | "avatar_url": "https://avatars.githubusercontent.com/u/2405087?v=4", 103 | "profile": "https://github.com/gxitm", 104 | "contributions": [ 105 | "code" 106 | ] 107 | }, 108 | { 109 | "login": "hiCasper", 110 | "name": "hiCasper", 111 | "avatar_url": "https://avatars.githubusercontent.com/u/25276620?v=4", 112 | "profile": "https://blog.hicasper.com", 113 | "contributions": [ 114 | "code" 115 | ] 116 | }, 117 | { 118 | "login": "ckx000", 119 | "name": "旋子", 120 | "avatar_url": "https://avatars.githubusercontent.com/u/5800591?v=4", 121 | "profile": "https://github.com/ckx000", 122 | "contributions": [ 123 | "code" 124 | ] 125 | }, 126 | { 127 | "login": "chen8945", 128 | "name": "chen8945", 129 | "avatar_url": "https://avatars.githubusercontent.com/u/44148812?v=4", 130 | "profile": "https://github.com/chen8945", 131 | "contributions": [ 132 | "code" 133 | ] 134 | }, 135 | { 136 | "login": "seiuneko", 137 | "name": "seiuneko", 138 | "avatar_url": "https://avatars.githubusercontent.com/u/25706824?v=4", 139 | "profile": "https://github.com/seiuneko", 140 | "contributions": [ 141 | "code" 142 | ] 143 | }, 144 | { 145 | "login": "powersee", 146 | "name": "powersee", 147 | "avatar_url": "https://avatars.githubusercontent.com/u/38074760?v=4", 148 | "profile": "https://github.com/powersee", 149 | "contributions": [ 150 | "code" 151 | ] 152 | } 153 | ], 154 | "contributorsPerLine": 7, 155 | "projectName": "qd", 156 | "projectOwner": "qd-today", 157 | "repoType": "github", 158 | "repoHost": "https://github.com", 159 | "skipCi": true 160 | } 161 | -------------------------------------------------------------------------------- /.dcignore: -------------------------------------------------------------------------------- 1 | # Write glob rules for ignored files. 2 | # Check syntax on https://deepcode.freshdesk.com/support/solutions/articles/60000531055-how-can-i-ignore-files-or-directories- 3 | # Check examples on https://github.com/github/gitignore 4 | 5 | # Hidden directories 6 | .*/ 7 | 8 | node_modules/ 9 | bower_components/ 10 | build/ 11 | dist/ 12 | 13 | # ExtJs 14 | build/ 15 | ext/ 16 | 17 | # Python 18 | __pycache__/ 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | MANIFEST 33 | htmlcov/ 34 | cover/ 35 | instance/ 36 | docs/_build/ 37 | target/ 38 | profile_default/ 39 | __pypackages__/ 40 | celerybeat-schedule 41 | env/ 42 | venv/ 43 | ENV/ 44 | env.bak/ 45 | venv.bak/ 46 | /site 47 | cython_debug/ 48 | 49 | # Node 50 | logs 51 | pids 52 | lib-cov 53 | coverage 54 | bower_components 55 | build/Release 56 | node_modules/ 57 | jspm_packages/ 58 | web_modules/ 59 | out 60 | dist 61 | 62 | # QD 63 | web/static/components/*/ 64 | database.db 65 | config.json 66 | database - 副本.db 67 | *.db-journal 68 | qiandao.rar 69 | qd.rar 70 | 压缩.bat 71 | .ssh/ 72 | database*.db 73 | local_config.py 74 | .virtual_documents/ 75 | .gitkeep 76 | web/static/components/*/package-lock.json 77 | redis 78 | tmp/ 79 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug报告 2 | description: Create a report to help us improve 3 | title: "[Bug] " 4 | labels: ["bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | 如果你可以自己 debug 并解决的话, 建议您直接提交 PR 或 描述解决方法, 非常感谢! 10 | - type: checkboxes 11 | id: verify_step 12 | attributes: 13 | label: Verify steps 14 | description: | 15 | 在提交之前, 请确认 Please verify that you've followed these steps. 16 | options: 17 | - label: Tracker 我已经在 [Issue Tracker](……/) 中找过我要提出的问题 18 | required: true 19 | - label: Latest 我已经使用 最新源码 测试过,问题依旧存在 20 | required: true 21 | - label: Core 这是 QD 框架存在的问题,并非我所使用的 QD 早期版本(如 20210628及之前版号 等)或模板的特定问题 22 | required: true 23 | - label: Meaningful 我提交的不是无意义的 催促更新或修复 请求 24 | required: true 25 | - type: input 26 | id: qd_version 27 | attributes: 28 | label: QD Version 29 | description: | 30 | QD 版本号 31 | placeholder: "20211228" 32 | validations: 33 | required: true 34 | - type: dropdown 35 | id: bug_os 36 | attributes: 37 | label: Bug on OS 38 | description: | 39 | 发现问题所在的系统环境 OS 40 | multiple: true 41 | options: 42 | - macOS 43 | - Windows 44 | - Linux 45 | - OpenBSD/FreeBSD 46 | validations: 47 | required: true 48 | - type: dropdown 49 | id: bug_platform 50 | attributes: 51 | label: Bug on Platform 52 | description: | 53 | 发现问题所在的平台 Platform 54 | multiple: true 55 | options: 56 | - Docker/macOS 57 | - Docker/Windows x86 58 | - Docker/Windows x64 59 | - Docker/Linux 32位 60 | - Docker/Linux 64位 61 | - Code Build/macOS 62 | - Code Build/Windows x86 63 | - Code Build/Windows x64 64 | - Code Build/Linux 32位 65 | - Code Build/Linux 64位 66 | validations: 67 | required: true 68 | - type: textarea 69 | id: reproduce_bug 70 | attributes: 71 | label: To Reproduce 72 | description: | 73 | 复现此Bug的步骤. 74 | validations: 75 | required: true 76 | - type: textarea 77 | id: describe_bug 78 | attributes: 79 | label: Describe the Bug 80 | description: | 81 | 对Bug本身清晰而简洁的描述. 82 | validations: 83 | required: true 84 | - type: textarea 85 | id: qd_config 86 | attributes: 87 | label: QD config 88 | description: | 89 | 在下方附上 QD 脱敏后配置文件 或 创建容器命令及环境变量 的内容. 90 | render: shell 91 | validations: 92 | required: false 93 | - type: textarea 94 | id: qd_log 95 | attributes: 96 | label: QD log 97 | description: | 98 | 在下方附上 QD 后台报错 的日志. 99 | render: shell 100 | validations: 101 | required: true 102 | - type: textarea 103 | id: excepted_behavior 104 | attributes: 105 | label: Expected behavior 106 | description: | 107 | 对预期修复后情况的清晰明了的描述. 108 | validations: 109 | required: true 110 | - type: textarea 111 | id: screenshots 112 | attributes: 113 | label: Screenshots 114 | description: | 115 | 添加屏幕截图以帮助解释您的问题. 116 | validations: 117 | required: false 118 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | 3 | contact_links: 4 | - name: Get help in GitHub Discussions 5 | url: https://github.com/qd-today/qd/discussions 6 | about: Have a question? Not sure if your issue affects everyone reproducibly? The quickest way to get help is on QD's GitHub Discussions! 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 功能请求 2 | description: Suggest an idea for this project 3 | title: "[Feature] " 4 | labels: [enhancement] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | 如果你可以自己 编程实现 的话, 建议您直接提交 PR 或 描述解决方法, 非常感谢! 10 | - type: checkboxes 11 | id: verify_step 12 | attributes: 13 | label: Verify steps 14 | description: "在提交之前,请确认 Please verify that you've followed these steps" 15 | options: 16 | - label: Tracker 我已经在 [Issue Tracker](……/) 中找过我要提出的问题 17 | required: true 18 | - label: Need 当前 QD 框架并不包含该功能特性或者还不完善 19 | required: true 20 | - label: Framework 这是 QD 框架应包含的特性,并非模板特性 21 | required: true 22 | - label: Meaningful 我提交的不是无意义的 催促更新或修复 请求 23 | required: true 24 | - type: textarea 25 | attributes: 26 | label: Describe the Feature 27 | description: | 28 | 对问题本身清晰而简洁的描述.例如这个问题如何影响到你?目前 QD框架 的行为是什麽? 29 | validations: 30 | required: true 31 | - type: textarea 32 | attributes: 33 | label: Describe the solution 34 | description: | 35 | 清晰明了地描述您的解决方案.例如你想实现什么Feature特性或功能?如何实现该功能? 36 | validations: 37 | required: true 38 | - type: textarea 39 | attributes: 40 | label: Describe alternatives 41 | description: | 42 | 对您考虑过的任何替代解决方案或备选功能进行清晰、简洁的描述. 43 | validations: 44 | required: false 45 | - type: textarea 46 | attributes: 47 | label: Additional context 48 | description: | 49 | 在此处添加有关功能请求的任何其他描述或屏幕截图. 50 | validations: 51 | required: false 52 | -------------------------------------------------------------------------------- /.github/codeql/codeql-config.yml: -------------------------------------------------------------------------------- 1 | name: "My CodeQL config" 2 | 3 | paths-ignore: 4 | - 'web/static/components/*/*' 5 | - 'node_modules/*' 6 | - '*.test.js' -------------------------------------------------------------------------------- /.github/workflows/ChangeRelease.yml: -------------------------------------------------------------------------------- 1 | name: ChangeRelease 2 | on: 3 | workflow_dispatch: {} 4 | push: 5 | paths: [CHANGELOG.md] 6 | branches: [master] 7 | tags: ["*"] 8 | 9 | jobs: 10 | sync: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: dropseed/changerelease@v1 14 | with: 15 | github_token: ${{ secrets.GITHUB_TOKEN }} 16 | # optional 17 | tag_prefix: "" 18 | changelog: CHANGELOG.md -------------------------------------------------------------------------------- /.github/workflows/DockerHub-Description.yml: -------------------------------------------------------------------------------- 1 | name: Update Docker Hub Description 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - README.md 8 | - .github/workflows/DockerHub-Description.yml 9 | jobs: 10 | dockerHubDescription: 11 | if: startsWith(github.repository, 'qd-today/qd') 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Docker a76yyyy Hub Description 17 | uses: peter-evans/dockerhub-description@v3 18 | with: 19 | username: ${{ secrets.DOCKER_USERNAME }} 20 | password: ${{ secrets.DOCKER_PASSWORD }} 21 | repository: a76yyyy/qiandao 22 | short-description: ${{ github.event.repository.description }} 23 | 24 | - name: Docker qdtoday Hub Description 25 | uses: peter-evans/dockerhub-description@v3 26 | with: 27 | username: ${{ secrets.QD_DOCKER_USERNAME }} 28 | password: ${{ secrets.QD_DOCKER_PASSWORD }} 29 | repository: qdtoday/qd 30 | short-description: ${{ github.event.repository.description }} -------------------------------------------------------------------------------- /.github/workflows/Publish Ja3 Package.yml: -------------------------------------------------------------------------------- 1 | name: Publish Ja3 Package 2 | on: 3 | workflow_dispatch: {} 4 | push: 5 | paths-ignore: 6 | - 'docs/**' 7 | - '.github/**' 8 | - '**.md' 9 | - '**.png' 10 | - '**.json' 11 | branches: [master] 12 | tags: ["*"] 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | name: Publish Ja3 Package job 18 | if: startsWith(github.repository, 'qd-today/qd') # 仓库名 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v3 23 | - name: Set up QEMU 24 | uses: docker/setup-qemu-action@v2 25 | - name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v2 27 | - name: Login to GitHub Container Registry 28 | uses: docker/login-action@v2 29 | with: 30 | registry: ghcr.io 31 | username: ${{ secrets.DOCKER_USERNAME }} # dockerServer Username 在setting创建secrets name=DOCKER_USERNAME value=dockerid 32 | password: ${{ secrets.GITHUB_TOKEN }} # dockerServer Token 33 | - name: login to qdtoday DockerHub 34 | uses: docker/login-action@v2 35 | with: 36 | registry: docker.io 37 | username: ${{ secrets.QD_DOCKER_USERNAME }} # dockerServer Username 在setting创建secrets name=QD_DOCKER_USERNAME value=dockerid 38 | password: ${{ secrets.QD_DOCKER_PASSWORD }} # dockerServer Token 在setting创建secrets name=QD_DOCKER_PASSWORD value=dockerToken 39 | - name: Publish Ja3 Package 40 | uses: docker/build-push-action@v4 41 | if: github.ref == 'refs/heads/master' 42 | with: 43 | context: . 44 | file: ./Dockerfile.ja3 45 | platforms: linux/386,linux/arm64,linux/amd64,linux/arm/v6,linux/arm/v7 # 你准备构建的镜像平台 46 | push: true 47 | tags: ghcr.io/qd-today/qd:ja3-latest,docker.io/qdtoday/qd:ja3-dev 48 | - name: Login to a76yyyy DockerHub 49 | uses: docker/login-action@v2 50 | if: github.ref == 'refs/heads/master' 51 | with: 52 | registry: docker.io 53 | username: ${{ secrets.DOCKER_USERNAME }} # dockerServer Username 在setting创建secrets name=DOCKER_USERNAME value=dockerid 54 | password: ${{ secrets.DOCKER_PASSWORD }} # dockerServer Token 在setting创建secrets name=DOCKER_PASSWORD value=dockerToken 55 | - name: Push image to a76yyyy DockerHub 56 | if: github.ref == 'refs/heads/master' 57 | run: | 58 | docker buildx imagetools create \ 59 | --tag docker.io/a76yyyy/qiandao:ja3-dev \ 60 | docker.io/qdtoday/qd:ja3-dev 61 | - name: Get version 62 | id: get_version 63 | if: startsWith(github.ref, 'refs/tags/') 64 | run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} 65 | - name: Build Ja3-latest Package 66 | uses: docker/build-push-action@v4 67 | if: startsWith(github.ref, 'refs/tags/') 68 | with: 69 | context: . 70 | file: ./Dockerfile.ja3 71 | platforms: linux/386,linux/arm64,linux/amd64,linux/arm/v6,linux/arm/v7 # 你准备构建的镜像平台 72 | push: true 73 | tags: docker.io/qdtoday/qd:ja3-latest,docker.io/qdtoday/qd:ja3-${{ steps.get_version.outputs.VERSION }} 74 | - name: Login to a76yyyy DockerHub 75 | uses: docker/login-action@v2 76 | if: startsWith(github.ref, 'refs/tags/') 77 | with: 78 | registry: docker.io 79 | username: ${{ secrets.DOCKER_USERNAME }} # dockerServer Username 在setting创建secrets name=DOCKER_USERNAME value=dockerid 80 | password: ${{ secrets.DOCKER_PASSWORD }} # dockerServer Token 在setting创建secrets name=DOCKER_PASSWORD value=dockerToken 81 | - name: Push image to a76yyyy DockerHub 82 | if: startsWith(github.ref, 'refs/tags/') 83 | run: | 84 | docker buildx imagetools create \ 85 | --tag docker.io/a76yyyy/qiandao:ja3-latest \ 86 | --tag docker.io/a76yyyy/qiandao:ja3-${{ steps.get_version.outputs.VERSION }} \ 87 | docker.io/qdtoday/qd:ja3-${{ steps.get_version.outputs.VERSION }} 88 | -------------------------------------------------------------------------------- /.github/workflows/Publish Lite Package.yml: -------------------------------------------------------------------------------- 1 | name: Publish Lite Package 2 | on: 3 | workflow_dispatch: {} 4 | push: 5 | paths-ignore: 6 | - 'docs/**' 7 | - '.github/**' 8 | - '**.md' 9 | - '**.png' 10 | - '**.json' 11 | branches: [master] 12 | tags: ["*"] 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | name: Publish Lite Package job 18 | if: startsWith(github.repository, 'qd-today/qd') # 仓库名 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v3 23 | - name: Set up QEMU 24 | uses: docker/setup-qemu-action@v2 25 | - name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v2 27 | - name: Login to GitHub Container Registry 28 | uses: docker/login-action@v2 29 | with: 30 | registry: ghcr.io 31 | username: ${{ secrets.DOCKER_USERNAME }} # dockerServer Username 在setting创建secrets name=DOCKER_USERNAME value=dockerid 32 | password: ${{ secrets.GITHUB_TOKEN }} # dockerServer Token 33 | - name: login to qdtoday DockerHub 34 | uses: docker/login-action@v2 35 | with: 36 | registry: docker.io 37 | username: ${{ secrets.QD_DOCKER_USERNAME }} # dockerServer Username 在setting创建secrets name=QD_DOCKER_USERNAME value=dockerid 38 | password: ${{ secrets.QD_DOCKER_PASSWORD }} # dockerServer Token 在setting创建secrets name=QD_DOCKER_PASSWORD value=dockerToken 39 | - name: Publish Lite Package 40 | uses: docker/build-push-action@v4 41 | if: github.ref == 'refs/heads/master' 42 | with: 43 | context: . 44 | file: ./Dockerfile.lite 45 | platforms: linux/386,linux/arm64,linux/amd64,linux/arm/v6,linux/arm/v7 # 你准备构建的镜像平台 46 | push: true 47 | tags: ghcr.io/qd-today/qd:lite-latest,docker.io/qdtoday/qd:lite-dev 48 | - name: Login to a76yyyy DockerHub 49 | uses: docker/login-action@v2 50 | if: github.ref == 'refs/heads/master' 51 | with: 52 | registry: docker.io 53 | username: ${{ secrets.DOCKER_USERNAME }} # dockerServer Username 在setting创建secrets name=DOCKER_USERNAME value=dockerid 54 | password: ${{ secrets.DOCKER_PASSWORD }} # dockerServer Token 在setting创建secrets name=DOCKER_PASSWORD value=dockerToken 55 | - name: Push image to a76yyyy DockerHub 56 | if: github.ref == 'refs/heads/master' 57 | run: | 58 | docker buildx imagetools create \ 59 | --tag docker.io/a76yyyy/qiandao:lite-dev \ 60 | docker.io/qdtoday/qd:lite-dev 61 | - name: Get version 62 | id: get_version 63 | if: startsWith(github.ref, 'refs/tags/') 64 | run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} 65 | - name: Build Lite-latest Package 66 | uses: docker/build-push-action@v4 67 | if: startsWith(github.ref, 'refs/tags/') 68 | with: 69 | context: . 70 | file: ./Dockerfile.lite 71 | platforms: linux/386,linux/arm64,linux/amd64,linux/arm/v6,linux/arm/v7 # 你准备构建的镜像平台 72 | push: true 73 | tags: docker.io/qdtoday/qd:lite-latest,docker.io/qdtoday/qd:lite-${{ steps.get_version.outputs.VERSION }} 74 | - name: Login to a76yyyy DockerHub 75 | uses: docker/login-action@v2 76 | if: startsWith(github.ref, 'refs/tags/') 77 | with: 78 | registry: docker.io 79 | username: ${{ secrets.DOCKER_USERNAME }} # dockerServer Username 在setting创建secrets name=DOCKER_USERNAME value=dockerid 80 | password: ${{ secrets.DOCKER_PASSWORD }} # dockerServer Token 在setting创建secrets name=DOCKER_PASSWORD value=dockerToken 81 | - name: Push image to a76yyyy DockerHub 82 | if: startsWith(github.ref, 'refs/tags/') 83 | run: | 84 | docker buildx imagetools create \ 85 | --tag docker.io/a76yyyy/qiandao:lite-latest \ 86 | --tag docker.io/a76yyyy/qiandao:lite-${{ steps.get_version.outputs.VERSION }} \ 87 | docker.io/qdtoday/qd:lite-${{ steps.get_version.outputs.VERSION }} 88 | -------------------------------------------------------------------------------- /.github/workflows/Publish Package.yml: -------------------------------------------------------------------------------- 1 | name: Publish Latest Package 2 | on: 3 | workflow_dispatch: {} 4 | push: 5 | paths-ignore: 6 | - 'docs/**' 7 | - '.github/**' 8 | - '**.md' 9 | - '**.png' 10 | - '**.json' 11 | branches: [master] 12 | tags: ["*"] 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | name: Publish Latest Package job 18 | if: startsWith(github.repository, 'qd-today/qd') # 仓库名 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v3 23 | - name: Set up QEMU 24 | uses: docker/setup-qemu-action@v2 25 | - name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v2 27 | - name: Login to GitHub Container Registry 28 | uses: docker/login-action@v2 29 | with: 30 | registry: ghcr.io 31 | username: ${{ secrets.DOCKER_USERNAME }} # dockerServer Username 在setting创建secrets name=DOCKER_USERNAME value=dockerid 32 | password: ${{ secrets.GITHUB_TOKEN }} # dockerServer Token 33 | - name: login to qdtoday DockerHub 34 | uses: docker/login-action@v2 35 | with: 36 | registry: docker.io 37 | username: ${{ secrets.QD_DOCKER_USERNAME }} # dockerServer Username 在setting创建secrets name=QD_DOCKER_USERNAME value=dockerid 38 | password: ${{ secrets.QD_DOCKER_PASSWORD }} # dockerServer Token 在setting创建secrets name=QD_DOCKER_PASSWORD value=dockerToken 39 | - name: Publish Latest Package 40 | uses: docker/build-push-action@v4 41 | if: github.ref == 'refs/heads/master' 42 | with: 43 | context: . 44 | file: ./Dockerfile 45 | platforms: linux/386,linux/arm64,linux/amd64,linux/arm/v6,linux/arm/v7 # 你准备构建的镜像平台 46 | push: true 47 | tags: ghcr.io/qd-today/qd:latest,docker.io/qdtoday/qd:dev 48 | - name: Login to a76yyyy DockerHub 49 | uses: docker/login-action@v2 50 | if: github.ref == 'refs/heads/master' 51 | with: 52 | registry: docker.io 53 | username: ${{ secrets.DOCKER_USERNAME }} # dockerServer Username 在setting创建secrets name=DOCKER_USERNAME value=dockerid 54 | password: ${{ secrets.DOCKER_PASSWORD }} # dockerServer Token 在setting创建secrets name=DOCKER_PASSWORD value=dockerToken 55 | - name: Push image to a76yyyy DockerHub 56 | if: github.ref == 'refs/heads/master' 57 | run: | 58 | docker buildx imagetools create \ 59 | --tag docker.io/a76yyyy/qiandao:dev \ 60 | docker.io/qdtoday/qd:dev 61 | - name: Get version 62 | id: get_version 63 | if: startsWith(github.ref, 'refs/tags/') 64 | run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} 65 | - name: Build Latest Package 66 | uses: docker/build-push-action@v4 67 | if: startsWith(github.ref, 'refs/tags/') 68 | with: 69 | context: . 70 | file: ./Dockerfile 71 | platforms: linux/386,linux/arm64,linux/amd64,linux/arm/v6,linux/arm/v7 # 你准备构建的镜像平台 72 | push: true 73 | tags: docker.io/qdtoday/qd:latest,docker.io/qdtoday/qd:${{ steps.get_version.outputs.VERSION }} 74 | - name: Login to a76yyyy DockerHub 75 | uses: docker/login-action@v2 76 | if: startsWith(github.ref, 'refs/tags/') 77 | with: 78 | registry: docker.io 79 | username: ${{ secrets.DOCKER_USERNAME }} # dockerServer Username 在setting创建secrets name=DOCKER_USERNAME value=dockerid 80 | password: ${{ secrets.DOCKER_PASSWORD }} # dockerServer Token 在setting创建secrets name=DOCKER_PASSWORD value=dockerToken 81 | - name: Push image to a76yyyy DockerHub 82 | if: startsWith(github.ref, 'refs/tags/') 83 | run: | 84 | docker buildx imagetools create \ 85 | --tag docker.io/a76yyyy/qiandao:latest \ 86 | --tag docker.io/a76yyyy/qiandao:${{ steps.get_version.outputs.VERSION }} \ 87 | docker.io/qdtoday/qd:${{ steps.get_version.outputs.VERSION }} 88 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master, dev ] 17 | paths: 18 | - '**.js' 19 | - '**.py' 20 | pull_request: 21 | # The branches below must be a subset of the branches above 22 | branches: [ master, dev ] 23 | paths: 24 | - '**.js' 25 | - '**.py' 26 | # schedule: 27 | # - cron: '34 4 * * 1' 28 | 29 | jobs: 30 | analyze: 31 | name: Analyze 32 | runs-on: ubuntu-latest 33 | permissions: 34 | actions: read 35 | contents: read 36 | security-events: write 37 | 38 | strategy: 39 | fail-fast: false 40 | matrix: 41 | language: [ 'javascript', 'python' ] 42 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 43 | # Learn more: 44 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 45 | 46 | steps: 47 | - name: Checkout repository 48 | uses: actions/checkout@v2 49 | 50 | # Initializes the CodeQL tools for scanning. 51 | - name: Initialize CodeQL 52 | uses: github/codeql-action/init@v1 53 | with: 54 | languages: ${{ matrix.language }} 55 | config-file: ./.github/codeql/codeql-config.yml 56 | # If you wish to specify custom queries, you can do so here or in a config file. 57 | # By default, queries listed here will override any specified in a config file. 58 | # Prefix the list here with "+" to use these queries and those in the config file. 59 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 60 | 61 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 62 | # If this step fails, then you should remove it and run the build manually (see below) 63 | - name: Autobuild 64 | uses: github/codeql-action/autobuild@v1 65 | 66 | # ℹ️ Command-line programs to run using the OS shell. 67 | # 📚 https://git.io/JvXDl 68 | 69 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 70 | # and modify them (or add more) to build your code if your project 71 | # uses a compiled language 72 | 73 | #- run: | 74 | # make bootstrap 75 | # make release 76 | 77 | - name: Perform CodeQL Analysis 78 | uses: github/codeql-action/analyze@v1 79 | -------------------------------------------------------------------------------- /.github/workflows/deploy-webdoc.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Webdoc 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - webdoc 8 | paths: 9 | - '.github/workflows/deploy-webdoc.yml' 10 | - 'web/**' 11 | 12 | jobs: 13 | deploy: 14 | if: startsWith(github.repository, 'qd-today/qd') 15 | runs-on: ubuntu-latest 16 | defaults: 17 | run: 18 | working-directory: ./web 19 | steps: 20 | - uses: actions/checkout@v3 21 | with: 22 | fetch-depth: 0 23 | - uses: pnpm/action-setup@v2 24 | with: 25 | version: 7.24.2 26 | - uses: actions/setup-node@v3 27 | with: 28 | node-version: 16 29 | cache: 'pnpm' 30 | cache-dependency-path: web/pnpm-lock.yaml 31 | - run: pnpm install --frozen-lockfile 32 | 33 | - name: Build 34 | run: pnpm docs:build 35 | 36 | - name: Deploy 37 | uses: peaceiris/actions-gh-pages@v3 38 | with: 39 | github_token: ${{ secrets.GITHUB_TOKEN }} 40 | publish_dir: web/docs/.vitepress/dist -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # cache 7 | *.dccache 8 | 9 | # C extensions 10 | *.so 11 | 12 | # frontend 13 | node_modules/ 14 | bower_components/ 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # pyenv 83 | .python-version 84 | .idea/ 85 | .vscode 86 | # celery beat schedule file 87 | celerybeat-schedule 88 | workspace.xml 89 | # SageMath parsed files 90 | *.sage.py 91 | .idea/workspace.xml 92 | # Environments 93 | .env 94 | .venv 95 | env/ 96 | venv/ 97 | env27/ 98 | ENV/ 99 | env.bak/ 100 | venv.bak/ 101 | 102 | # Spyder project settings 103 | .spyderproject 104 | .spyproject 105 | 106 | # Rope project settings 107 | .ropeproject 108 | 109 | # mkdocs documentation 110 | /site 111 | 112 | # mypy 113 | .mypy_cache/ 114 | 115 | # history 116 | .history/ 117 | *.un~ 118 | .lh/ 119 | 120 | # backup 121 | *.bak 122 | 123 | # qd 124 | database.db 125 | config.json 126 | database - 副本.db 127 | *.db-journal 128 | qiandao.rar 129 | qd.rar 130 | 压缩.bat 131 | .ssh/ 132 | database*.db 133 | local_config.py 134 | .virtual_documents/ 135 | .gitkeep 136 | web/static/components/*/package-lock.json 137 | redis 138 | tmp/ 139 | 140 | # Logs 141 | logs 142 | *.log 143 | npm-debug.log* 144 | yarn-debug.log* 145 | yarn-error.log* 146 | lerna-debug.log* 147 | .pnpm-debug.log* 148 | 149 | # Diagnostic reports (https://nodejs.org/api/report.html) 150 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 151 | 152 | # Runtime data 153 | pids 154 | *.pid 155 | *.seed 156 | *.pid.lock 157 | 158 | # Directory for instrumented libs generated by jscoverage/JSCover 159 | lib-cov 160 | 161 | # Coverage directory used by tools like istanbul 162 | coverage 163 | *.lcov 164 | 165 | # nyc test coverage 166 | .nyc_output 167 | 168 | # from: https://github.com/vuejs/vitepress/blob/main/.gitignore 169 | /coverage 170 | /src/client/shared.ts 171 | /src/node/shared.ts 172 | *.log 173 | *.tgz 174 | .DS_Store 175 | .idea 176 | .temp 177 | .vite_opt_cache 178 | .vscode 179 | dist 180 | cache 181 | examples-temp 182 | node_modules 183 | pnpm-global 184 | TODOs.md 185 | 186 | cache 187 | 188 | alembic.ini 189 | .hintrc -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 基础镜像 2 | FROM a76yyyy/pycurl:latest 3 | 4 | # 维护者信息 5 | LABEL maintainer "a76yyyy " 6 | LABEL org.opencontainers.image.source=https://github.com/qd-today/qd 7 | 8 | ADD ssh/qd_fetch /root/.ssh/id_rsa 9 | ADD ssh/qd_fetch.pub /root/.ssh/id_rsa.pub 10 | WORKDIR /usr/src/app 11 | 12 | # QD && Pip install modules 13 | RUN sed -i 's/mirrors.ustc.edu.cn/dl-cdn.alpinelinux.org/g' /etc/apk/repositories && \ 14 | apk update && apk add --update --no-cache openssh-client && \ 15 | chmod 600 /root/.ssh/id_rsa && \ 16 | ssh-keyscan gitee.com > /root/.ssh/known_hosts && \ 17 | let num=$RANDOM%100+10 && \ 18 | sleep $num && \ 19 | git clone --depth 1 git@gitee.com:qd-today/qd.git /gitclone_tmp && \ 20 | yes | cp -rf /gitclone_tmp/. /usr/src/app && \ 21 | rm -rf /gitclone_tmp && \ 22 | chmod +x /usr/src/app/update.sh && \ 23 | ln -s /usr/src/app/update.sh /bin/update && \ 24 | apk add --update --no-cache openssh-client python3 py3-six \ 25 | py3-markupsafe py3-pycryptodome py3-tornado py3-wrapt \ 26 | py3-packaging py3-greenlet py3-urllib3 py3-cryptography \ 27 | py3-aiosignal py3-async-timeout py3-attrs py3-frozenlist \ 28 | py3-multidict py3-charset-normalizer py3-aiohttp \ 29 | py3-typing-extensions py3-yarl && \ 30 | [[ $(getconf LONG_BIT) = "32" ]] && \ 31 | echo "Tips: 32-bit systems do not support ddddocr, so there is no need to install numpy and opencv-python" || \ 32 | apk add --update --no-cache py3-numpy-dev py3-opencv py3-pillow && \ 33 | apk add --no-cache --virtual .build_deps cmake make perl \ 34 | autoconf g++ automake py3-pip py3-setuptools py3-wheel python3-dev \ 35 | linux-headers libtool util-linux && \ 36 | sed -i '/ddddocr/d' requirements.txt && \ 37 | sed -i '/packaging/d' requirements.txt && \ 38 | sed -i '/wrapt/d' requirements.txt && \ 39 | sed -i '/pycryptodome/d' requirements.txt && \ 40 | sed -i '/tornado/d' requirements.txt && \ 41 | sed -i '/MarkupSafe/d' requirements.txt && \ 42 | sed -i '/pillow/d' requirements.txt && \ 43 | sed -i '/opencv/d' requirements.txt && \ 44 | sed -i '/numpy/d' requirements.txt && \ 45 | sed -i '/greenlet/d' requirements.txt && \ 46 | sed -i '/urllib3/d' requirements.txt && \ 47 | sed -i '/cryptography/d' requirements.txt && \ 48 | sed -i '/aiosignal/d' requirements.txt && \ 49 | sed -i '/async-timeout/d' requirements.txt && \ 50 | sed -i '/attrs/d' requirements.txt && \ 51 | sed -i '/frozenlist/d' requirements.txt && \ 52 | sed -i '/multidict/d' requirements.txt && \ 53 | sed -i '/charset-normalizer/d' requirements.txt && \ 54 | sed -i '/aiohttp/d' requirements.txt && \ 55 | sed -i '/typing-extensions/d' requirements.txt && \ 56 | sed -i '/yarl/d' requirements.txt && \ 57 | pip install --no-cache-dir -r requirements.txt && \ 58 | apk del .build_deps && \ 59 | sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories && \ 60 | rm -rf /var/cache/apk/* && \ 61 | rm -rf /usr/share/man/* 62 | 63 | ENV PORT 80 64 | EXPOSE $PORT/tcp 65 | 66 | # timezone 67 | ENV TZ=CST-8 68 | 69 | # 添加挂载点 70 | VOLUME ["/usr/src/app/config"] 71 | 72 | CMD ["sh","-c","python /usr/src/app/run.py"] -------------------------------------------------------------------------------- /Dockerfile.ja3: -------------------------------------------------------------------------------- 1 | # 基础镜像 2 | # 集成 curl-impersonate 解决 ja3 指纹被识别为 curl 的问题 3 | # https://github.com/qd-today/qd/issues/289 4 | # 不支持 http3 和 Quic 连接, 如需相关功能请使用 latest 版本 5 | FROM a76yyyy/pycurl:ja3-latest 6 | 7 | # 维护者信息 8 | LABEL maintainer "a76yyyy " 9 | LABEL org.opencontainers.image.source=https://github.com/qd-today/qd 10 | 11 | ADD ssh/qd_fetch /root/.ssh/id_rsa 12 | ADD ssh/qd_fetch.pub /root/.ssh/id_rsa.pub 13 | WORKDIR /usr/src/app 14 | 15 | # QD && Pip install modules 16 | RUN sed -i 's/mirrors.ustc.edu.cn/dl-cdn.alpinelinux.org/g' /etc/apk/repositories && \ 17 | apk update && apk add --update --no-cache openssh-client && \ 18 | chmod 600 /root/.ssh/id_rsa && \ 19 | ssh-keyscan gitee.com > /root/.ssh/known_hosts && \ 20 | let num=$RANDOM%100+10 && \ 21 | sleep $num && \ 22 | git clone --depth 1 git@gitee.com:qd-today/qd.git /gitclone_tmp && \ 23 | yes | cp -rf /gitclone_tmp/. /usr/src/app && \ 24 | rm -rf /gitclone_tmp && \ 25 | chmod +x /usr/src/app/update.sh && \ 26 | ln -s /usr/src/app/update.sh /bin/update && \ 27 | apk add --update --no-cache python3 py3-six py3-markupsafe py3-pycryptodome py3-tornado \ 28 | py3-wrapt py3-packaging py3-greenlet py3-urllib3 py3-cryptography py3-aiosignal \ 29 | py3-async-timeout py3-attrs py3-frozenlist py3-multidict py3-charset-normalizer \ 30 | py3-aiohttp py3-typing-extensions py3-yarl && \ 31 | [[ $(getconf LONG_BIT) = "32" ]] && \ 32 | echo "Tips: 32-bit systems do not support ddddocr, so there is no need to install numpy and opencv-python" || \ 33 | apk add --update --no-cache py3-opencv py3-pillow && \ 34 | apk add --no-cache --virtual .build_deps cmake make perl autoconf g++ automake \ 35 | linux-headers libtool util-linux py3-pip py3-setuptools py3-wheel python3-dev py3-numpy-dev && \ 36 | sed -i '/ddddocr/d' requirements.txt && \ 37 | sed -i '/packaging/d' requirements.txt && \ 38 | sed -i '/wrapt/d' requirements.txt && \ 39 | sed -i '/pycryptodome/d' requirements.txt && \ 40 | sed -i '/tornado/d' requirements.txt && \ 41 | sed -i '/MarkupSafe/d' requirements.txt && \ 42 | sed -i '/pillow/d' requirements.txt && \ 43 | sed -i '/opencv/d' requirements.txt && \ 44 | sed -i '/numpy/d' requirements.txt && \ 45 | sed -i '/greenlet/d' requirements.txt && \ 46 | sed -i '/urllib3/d' requirements.txt && \ 47 | sed -i '/cryptography/d' requirements.txt && \ 48 | sed -i '/aiosignal/d' requirements.txt && \ 49 | sed -i '/async-timeout/d' requirements.txt && \ 50 | sed -i '/attrs/d' requirements.txt && \ 51 | sed -i '/frozenlist/d' requirements.txt && \ 52 | sed -i '/multidict/d' requirements.txt && \ 53 | sed -i '/charset-normalizer/d' requirements.txt && \ 54 | sed -i '/aiohttp/d' requirements.txt && \ 55 | sed -i '/typing-extensions/d' requirements.txt && \ 56 | sed -i '/yarl/d' requirements.txt && \ 57 | pip install --no-cache-dir -r requirements.txt && \ 58 | apk del .build_deps && \ 59 | sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories && \ 60 | rm -rf /var/cache/apk/* && \ 61 | rm -rf /usr/share/man/* 62 | 63 | ENV PORT 80 64 | EXPOSE $PORT/tcp 65 | 66 | # timezone 67 | ENV TZ=CST-8 68 | 69 | # 添加挂载点 70 | VOLUME ["/usr/src/app/config"] 71 | 72 | CMD ["sh","-c","python /usr/src/app/run.py"] -------------------------------------------------------------------------------- /Dockerfile.lite: -------------------------------------------------------------------------------- 1 | # 基础镜像 2 | # 去除了 OCR 相关功能, 其他与 latest 版本保持一致. 3 | # 适用于硬盘空间不大于 600M 的 Docker 构建. 4 | FROM a76yyyy/pycurl:lite-latest 5 | 6 | # 维护者信息 7 | LABEL maintainer "a76yyyy " 8 | LABEL org.opencontainers.image.source=https://github.com/qd-today/qd 9 | 10 | ADD ssh/qd_fetch /root/.ssh/id_rsa 11 | ADD ssh/qd_fetch.pub /root/.ssh/id_rsa.pub 12 | WORKDIR /usr/src/app 13 | 14 | # QD & Pip install modules 15 | RUN sed -i 's/mirrors.ustc.edu.cn/dl-cdn.alpinelinux.org/g' /etc/apk/repositories && \ 16 | # ln -s /usr/bin/python3 /usr/bin/python && \ 17 | apk update && apk add --update --no-cache openssh-client python3 py3-six py3-markupsafe py3-pycryptodome py3-tornado py3-wrapt py3-packaging py3-greenlet py3-urllib3 py3-cryptography py3-aiosignal py3-async-timeout py3-attrs py3-frozenlist py3-multidict py3-charset-normalizer py3-aiohttp py3-typing-extensions py3-yarl && \ 18 | chmod 600 /root/.ssh/id_rsa && \ 19 | ssh-keyscan gitee.com > /root/.ssh/known_hosts && \ 20 | let num=$RANDOM%100+10 && \ 21 | sleep $num && \ 22 | git clone --depth 1 git@gitee.com:qd-today/qd.git /gitclone_tmp && \ 23 | yes | cp -rf /gitclone_tmp/. /usr/src/app && \ 24 | rm -rf /gitclone_tmp && \ 25 | chmod +x /usr/src/app/update.sh && \ 26 | ln -s /usr/src/app/update.sh /bin/update && \ 27 | apk add --no-cache --virtual .build_deps nano cmake make perl autoconf g++ automake \ 28 | linux-headers libtool util-linux python3-dev py3-pip py3-setuptools py3-wheel && \ 29 | sed -i '/ddddocr/d' requirements.txt && \ 30 | sed -i '/packaging/d' requirements.txt && \ 31 | sed -i '/wrapt/d' requirements.txt && \ 32 | sed -i '/pycryptodome/d' requirements.txt && \ 33 | sed -i '/tornado/d' requirements.txt && \ 34 | sed -i '/MarkupSafe/d' requirements.txt && \ 35 | sed -i '/pillow/d' requirements.txt && \ 36 | sed -i '/opencv/d' requirements.txt && \ 37 | sed -i '/numpy/d' requirements.txt && \ 38 | sed -i '/greenlet/d' requirements.txt && \ 39 | sed -i '/urllib3/d' requirements.txt && \ 40 | sed -i '/cryptography/d' requirements.txt && \ 41 | sed -i '/aiosignal/d' requirements.txt && \ 42 | sed -i '/async-timeout/d' requirements.txt && \ 43 | sed -i '/attrs/d' requirements.txt && \ 44 | sed -i '/frozenlist/d' requirements.txt && \ 45 | sed -i '/multidict/d' requirements.txt && \ 46 | sed -i '/charset-normalizer/d' requirements.txt && \ 47 | sed -i '/aiohttp/d' requirements.txt && \ 48 | sed -i '/typing-extensions/d' requirements.txt && \ 49 | sed -i '/yarl/d' requirements.txt && \ 50 | pip install --no-cache-dir -r requirements.txt && \ 51 | apk del .build_deps && \ 52 | sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories && \ 53 | rm -rf /var/cache/apk/* && \ 54 | rm -rf /usr/share/man/* 55 | 56 | ENV PORT=80 57 | EXPOSE $PORT/tcp 58 | ENV QIANDAO_LITE=True 59 | 60 | # timezone 61 | ENV TZ=CST-8 62 | 63 | # 添加挂载点 64 | VOLUME ["/usr/src/app/config"] 65 | 66 | CMD ["sh","-c","python /usr/src/app/run.py"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 QD-Today 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://mirrors.cloud.tencent.com/pypi/simple/" 3 | verify_ssl = true 4 | name = "tencent" 5 | 6 | [[source]] 7 | url = "https://pypi.org/simple" 8 | verify_ssl = true 9 | name = "pypi" 10 | 11 | [pipenv] 12 | install_search_all_sources = true 13 | 14 | [packages] 15 | tornado = "*" 16 | charset-normalizer = "*" 17 | requests = {version = "*", index = "pypi"} 18 | pbkdf2 = "*" 19 | pycryptodome = {version = "*", index = "pypi"} 20 | pytz = "*" 21 | croniter = {version = "*", index = "pypi"} 22 | Jinja2 = "*" 23 | redis = "*" 24 | incremental = "*" 25 | u-msgpack-python = {version = "*", index = "pypi"} 26 | pysocks = "*" 27 | faker = {version = "*", index = "pypi"} 28 | colorama = {version = "*", markers="sys_platform == 'win32'"} 29 | aiofiles = "*" 30 | passlib = "*" 31 | aiomysql = "*" 32 | aiosqlite = "*" 33 | cryptography = "*" 34 | aiohttp = "*" 35 | sqlalchemy = {version = "<2.0", extras = ["asyncio"]} 36 | 37 | [dev-packages] 38 | opencv-python-headless = "*" 39 | ddddocr = "*" 40 | pycurl = {version = "*", markers="sys_platform != 'win32'"} 41 | 42 | [requires] 43 | python_version = "3.11" 44 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: python run.py 2 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # 安全政策 2 | 3 | ## 获得安全更新支持的版本 4 | 5 | 使用这部分来告诉人们 项目的哪些版本 目前得到了安全更新的支持。 6 | 7 | | 版本 | 是否获得支持 | 8 | |------- | ------------------ | 9 | | 202110xx | :white_check_mark: | 10 | | 20230524 | :x: | 11 | 12 | ## 报告漏洞 13 | 14 | 使用本节来告诉人们如何报告漏洞。 15 | 16 | 告诉他们在哪里, 多长时间可以得到一个报告的漏洞的更新; 17 | 18 | 如果漏洞被接受或被拒绝, 会有什么结果, 等等。 19 | 41 | -------------------------------------------------------------------------------- /chrole.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim:fenc=utf-8 3 | # 4 | # Copyright © 2017 Binux 5 | # 6 | # Distributed under terms of the MIT license. 7 | 8 | """ 9 | change the role of user 10 | """ 11 | 12 | import asyncio 13 | import sys 14 | 15 | import db 16 | 17 | 18 | def usage(): 19 | print('Usage: python3 %s [role]' % sys.argv[0]) 20 | print('Example: python3 %s admin@qd.today admin' % sys.argv[0]) 21 | sys.exit(1) 22 | 23 | async def main(): 24 | email = sys.argv[1] 25 | role = sys.argv[2] if len(sys.argv) == 3 else '' 26 | userdb = db.User() 27 | 28 | user = await userdb.get(email=email, fields=['id']) 29 | if not user: 30 | print("Cannot find user: ", email) 31 | sys.exit(1) 32 | rowcount = await userdb.mod(user['id'], role=role) 33 | if rowcount >= 1: 34 | print("role of {} changed to {}".format(email, role or '[empty]')) 35 | else: 36 | print("role of {} not changed".format(email)) 37 | 38 | 39 | if __name__ == '__main__': 40 | if not 2 <= len(sys.argv) <= 3: 41 | usage() 42 | else: 43 | loop = asyncio.new_event_loop() 44 | asyncio.set_event_loop(loop) 45 | run = asyncio.ensure_future(main(), loop=loop) 46 | loop.run_until_complete(run) 47 | -------------------------------------------------------------------------------- /config/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /db/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # vim: set et sw=4 ts=4 sts=4 ff=unix fenc=utf8: 4 | # Author: Binux 5 | # http://binux.me 6 | # Created on 2014-08-08 20:28:15 7 | 8 | import os 9 | import sys 10 | 11 | from db.basedb import AlchemyMixin 12 | 13 | sys.path.append(os.path.abspath(os.path.dirname(os.path.dirname(__file__)))) 14 | from .notepad import Notepad 15 | from .pubtpl import Pubtpl 16 | from .push_request import PushRequest 17 | from .redisdb import RedisDB 18 | from .site import Site 19 | from .task import Task 20 | from .tasklog import Tasklog 21 | from .tpl import Tpl 22 | from .user import User 23 | 24 | 25 | class DB(AlchemyMixin): 26 | def __init__(self) -> None: 27 | self.user = User() 28 | self.tpl = Tpl() 29 | self.task = Task() 30 | self.tasklog = Tasklog() 31 | self.push_request = PushRequest() 32 | self.redis = RedisDB() 33 | self.site = Site() 34 | self.pubtpl = Pubtpl() 35 | self.notepad = Notepad() 36 | 37 | -------------------------------------------------------------------------------- /db/notepad.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # vim: set et sw=4 ts=4 sts=4 ff=unix fenc=utf8: 3 | # Author: a76yyyy 4 | # Created on 2022-08-12 18:41:09 5 | 6 | # import os 7 | # import sys 8 | # sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 9 | from sqlalchemy import Column, Integer, Text, delete, select, update 10 | 11 | from .basedb import AlchemyMixin, BaseDB, logger_DB 12 | 13 | 14 | class Notepad(BaseDB,AlchemyMixin): 15 | ''' 16 | Site db 17 | 18 | regEn 19 | ''' 20 | __tablename__ = 'notepad' 21 | id = Column(Integer, primary_key=True) 22 | userid = Column(Integer, nullable=False) 23 | notepadid = Column(Integer, nullable=False) 24 | content = Column(Text) 25 | 26 | def add(self, insert, sql_session=None): 27 | return self._insert(Notepad(**insert), sql_session=sql_session) 28 | 29 | def mod(self, userid, notepadid, sql_session=None, **kwargs): 30 | assert userid, 'need userid' 31 | assert notepadid, 'need notepadid' 32 | 33 | return self._update(update(Notepad).where(Notepad.userid == userid).where(Notepad.notepadid == notepadid).values(**kwargs), sql_session=sql_session) 34 | 35 | async def get(self, userid, notepadid, fields=None, one_or_none=False, first=True, to_dict=True, sql_session=None): 36 | assert userid, 'need userid' 37 | assert notepadid, 'need notepadid' 38 | 39 | if fields is None: 40 | _fields = Notepad 41 | else: 42 | _fields = (getattr(Notepad, field) for field in fields) 43 | 44 | smtm = select(_fields).where(Notepad.userid == userid).where(Notepad.notepadid == notepadid) 45 | 46 | result = await self._get(smtm, one_or_none=one_or_none, first=first, sql_session=sql_session) 47 | if to_dict and result is not None: 48 | return self.to_dict(result,fields) 49 | return result 50 | 51 | async def list(self, fields=None, limit=1000, to_dict=True, sql_session=None, **kwargs): 52 | if fields is None: 53 | _fields = Notepad 54 | else: 55 | _fields = (getattr(Notepad, field) for field in fields) 56 | 57 | smtm = select(_fields) 58 | 59 | for key, value in kwargs.items(): 60 | smtm = smtm.where(getattr(Notepad, key) == value) 61 | 62 | if limit: 63 | smtm = smtm.limit(limit) 64 | 65 | result = await self._get(smtm, sql_session=sql_session) 66 | if to_dict and result is not None: 67 | return [self.to_dict(row,fields) for row in result] 68 | return result 69 | 70 | def delete(self, userid, notepadid, sql_session=None): 71 | return self._delete(delete(Notepad).where(Notepad.userid == userid).where(Notepad.notepadid == notepadid), sql_session=sql_session) 72 | 73 | if __name__ == '__main__': 74 | import asyncio 75 | async def test(): 76 | notepad = Notepad() 77 | try: 78 | async with notepad.transaction() as sql_session: 79 | await notepad.add({'userid':1,'notepadid':1,'content':'test'}, sql_session=sql_session) 80 | await notepad.add({'userid':1,'notepadid':2}) 81 | await notepad.add({'userid':2,'notepadid':1}) 82 | await notepad.add({'userid':2,'notepadid':2}) 83 | except Exception as e: 84 | print(e) 85 | notepad1 = await notepad.get(1,1) 86 | notepad1_content = await notepad.get(1,1,fields=('content',)) 87 | notepad_list = await notepad.list(userid=1) 88 | notepad_list_content = await notepad.list(userid=1,fields=('content',)) 89 | print('notepad1: ',notepad1) 90 | print('notepad1_content: ',notepad1_content) 91 | print('notepad_list: ',notepad_list) 92 | print('notepad_list_content: ',notepad_list_content) 93 | 94 | await notepad.mod(1,1,content='test1') 95 | notepad1 = await notepad.get(1,1) 96 | print('notepad1 after mod : ',notepad1) 97 | 98 | await notepad.delete(1,1) 99 | await notepad.delete(1,2) 100 | await notepad.delete(2,1) 101 | await notepad.delete(2,2) 102 | return 103 | loop = asyncio.new_event_loop() 104 | asyncio.set_event_loop(loop) 105 | task = asyncio.ensure_future(test(), loop=loop) 106 | loop.run_until_complete(task) 107 | -------------------------------------------------------------------------------- /db/pubtpl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # vim: set et sw=4 ts=4 sts=4 ff=unix fenc=utf8: 4 | # Author: Binux 5 | # http://binux.me 6 | # Created on 2014-08-08 19:53:09 7 | 8 | from sqlalchemy import Column, Integer, Text, delete, select, update 9 | 10 | from .basedb import AlchemyMixin, BaseDB 11 | 12 | 13 | class Pubtpl(BaseDB,AlchemyMixin): 14 | ''' 15 | Site db 16 | 17 | regEn 18 | ''' 19 | __tablename__ = 'pubtpl' 20 | id = Column(Integer, primary_key=True) 21 | name = Column(Text) 22 | author = Column(Text) 23 | comments = Column(Text) 24 | content = Column(Text) 25 | filename = Column(Text) 26 | date = Column(Text) 27 | version = Column(Text) 28 | url = Column(Text) 29 | update = Column(Text) 30 | reponame = Column(Text) 31 | repourl = Column(Text) 32 | repoacc = Column(Text) 33 | repobranch = Column(Text) 34 | commenturl = Column(Text) 35 | 36 | def add(self, insert, sql_session=None): 37 | return self._insert(Pubtpl(**insert), sql_session=sql_session) 38 | 39 | def mod(self, id, sql_session=None, **kwargs): 40 | assert id, 'need id' 41 | return self._update(update(Pubtpl).where(Pubtpl.id == id).values(**kwargs), sql_session=sql_session) 42 | 43 | async def get(self, id, fields=None, one_or_none=False, first=True, to_dict=True, sql_session=None): 44 | assert id, 'need id' 45 | if fields is None: 46 | _fields = Pubtpl 47 | else: 48 | _fields = (getattr(Pubtpl, field) for field in fields) 49 | 50 | smtm = select(_fields).where(Pubtpl.id == id) 51 | 52 | result = await self._get(smtm, one_or_none=one_or_none, first=first, sql_session=sql_session) 53 | if to_dict and result is not None: 54 | return self.to_dict(result,fields) 55 | return result 56 | 57 | async def list(self, fields=None, limit=1000, to_dict=True, sql_session=None, **kwargs): 58 | if fields is None: 59 | _fields = Pubtpl 60 | else: 61 | _fields = (getattr(Pubtpl, field) for field in fields) 62 | 63 | smtm = select(_fields) 64 | 65 | for key, value in kwargs.items(): 66 | smtm = smtm.where(getattr(Pubtpl, key) == value) 67 | 68 | if limit: 69 | smtm = smtm.limit(limit) 70 | 71 | result = await self._get(smtm, sql_session=sql_session) 72 | if to_dict and result is not None: 73 | return [self.to_dict(row,fields) for row in result] 74 | return result 75 | 76 | def delete(self, id, sql_session=None): 77 | return self._delete(delete(Pubtpl).where(Pubtpl.id == id), sql_session=sql_session) 78 | -------------------------------------------------------------------------------- /db/push_request.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # vim: set et sw=4 ts=4 sts=4 ff=unix fenc=utf8: 4 | # Author: Binux 5 | # http://binux.me 6 | # Created on 2014-08-08 20:12:27 7 | 8 | import time 9 | 10 | from sqlalchemy import INTEGER, Column, Integer, String, select, text, update 11 | from sqlalchemy.dialects.mysql import TINYINT 12 | 13 | from .basedb import AlchemyMixin, BaseDB 14 | 15 | 16 | class PushRequest(BaseDB,AlchemyMixin): 17 | ''' 18 | push request db 19 | 20 | id, from_tplid, from_userid, to_tplid, to_userid, status, msg, ctime, mtime, atime 21 | ''' 22 | __tablename__ = 'push_request' 23 | 24 | id = Column(Integer, primary_key=True) 25 | from_tplid = Column(INTEGER, nullable=False) 26 | from_userid = Column(INTEGER, nullable=False) 27 | status = Column(TINYINT, nullable=False, server_default=text("'0'")) 28 | ctime = Column(INTEGER, nullable=False) 29 | mtime = Column(INTEGER, nullable=False) 30 | atime = Column(INTEGER, nullable=False) 31 | to_tplid = Column(INTEGER) 32 | to_userid = Column(INTEGER) 33 | msg = Column(String(1024)) 34 | 35 | 36 | PENDING = 0 37 | CANCEL = 1 38 | REFUSE = 2 39 | ACCEPT = 3 40 | 41 | class NOTSET(object): pass 42 | 43 | def add(self, from_tplid, from_userid, to_tplid, to_userid, msg='', sql_session=None): 44 | now = time.time() 45 | 46 | insert = dict( 47 | from_tplid = from_tplid, 48 | from_userid = from_userid, 49 | to_tplid = to_tplid, 50 | to_userid = to_userid, 51 | status = PushRequest.PENDING, 52 | msg = msg, 53 | ctime = now, 54 | mtime = now, 55 | atime = now, 56 | ) 57 | return self._insert(PushRequest(**insert), sql_session=sql_session) 58 | 59 | def mod(self, id, sql_session=None, **kwargs): 60 | for each in ('id', 'from_tplid', 'from_userid', 'to_userid', 'ctime'): 61 | assert each not in kwargs, '%s not modifiable' % each 62 | 63 | kwargs['mtime'] = time.time() 64 | return self._update(update(PushRequest).where(PushRequest.id == id).values(**kwargs), sql_session=sql_session) 65 | 66 | async def get(self, id, fields=None, one_or_none=False, first=True, to_dict=True, sql_session=None): 67 | assert id, 'need id' 68 | if fields is None: 69 | _fields = PushRequest 70 | else: 71 | _fields = (getattr(PushRequest, field) for field in fields) 72 | 73 | smtm = select(_fields).where(PushRequest.id == id) 74 | 75 | result = await self._get(smtm, one_or_none=one_or_none, first=first, sql_session=sql_session) 76 | if to_dict and result is not None: 77 | return self.to_dict(result,fields) 78 | return result 79 | 80 | async def list(self, fields=None, limit=1000, to_dict=True, sql_session=None, **kwargs): 81 | if fields is None: 82 | _fields = PushRequest 83 | else: 84 | _fields = (getattr(PushRequest, field) for field in fields) 85 | 86 | smtm = select(_fields) 87 | 88 | for key, value in kwargs.items(): 89 | smtm = smtm.where(getattr(PushRequest, key) == value) 90 | 91 | if limit: 92 | smtm = smtm.limit(limit) 93 | 94 | result = await self._get(smtm.order_by(PushRequest.mtime.desc()), sql_session=sql_session) 95 | if to_dict and result is not None: 96 | return [self.to_dict(row,fields) for row in result] 97 | return result 98 | -------------------------------------------------------------------------------- /db/redisdb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # vim: set et sw=4 ts=4 sts=4 ff=unix fenc=utf8: 4 | # Author: Binux 5 | # http://binux.me 6 | # Created on 2014-08-08 20:40:53 7 | 8 | import umsgpack 9 | 10 | import config 11 | from libs.log import Log 12 | from libs.utils import is_lan 13 | 14 | logger_RedisDB = Log('QD.RedisDB').getlogger() 15 | class RedisDB(object): 16 | def __init__(self, host=config.redis.host, port=config.redis.port, password=config.redis.passwd, db=config.redis.db, evil=config.evil): 17 | try: 18 | import redis 19 | except ImportError: 20 | self.client = None 21 | return 22 | 23 | self.evil_limit = evil 24 | try: 25 | self.client = redis.StrictRedis(host=host, port=port, password=password, db=db, socket_timeout=3, socket_connect_timeout=3) 26 | self.client.ping() 27 | except redis.exceptions.ConnectionError as e: 28 | if config.display_import_warning: 29 | logger_RedisDB.warning('Connect Redis falied: \"%s\". \nTips: This warning message is only for prompting, it will not affect running of QD framework. ',e) 30 | self.client = None 31 | 32 | def evil(self, ip, userid, cnt=None): 33 | if not self.client: 34 | return 35 | if cnt == self.client.incrby('ip_%s' % ip, cnt): 36 | self.client.expire('ip_%s' % ip, 3600) 37 | if userid and cnt == self.client.incrby('user_%s' % userid, cnt): 38 | self.client.expire('user_%s' % userid, 3600) 39 | 40 | def is_evil(self, ip, userid=None): 41 | if not self.client: 42 | return False 43 | if config.evil_pass_lan_ip and is_lan(ip): 44 | return False 45 | if userid: 46 | if int(self.client.get('user_%s' % userid) or '0') > self.evil_limit: 47 | return True 48 | else: 49 | return False 50 | if int(self.client.get('ip_%s' % ip) or '0') > self.evil_limit: 51 | return True 52 | return False 53 | 54 | def cache(self, key, _lambda, timeout=60*60): 55 | if not self.client: 56 | return _lambda() 57 | ret = self.client.get('cache_%s' % key) 58 | if ret: 59 | return umsgpack.unpackb(ret) 60 | ret = _lambda() 61 | self.client.set('cache_%s', umsgpack.packb(ret)) 62 | return ret 63 | 64 | def close(self): 65 | if self.client: 66 | self.client.close() 67 | -------------------------------------------------------------------------------- /db/site.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # vim: set et sw=4 ts=4 sts=4 ff=unix fenc=utf8: 4 | # Author: Binux 5 | # http://binux.me 6 | # Created on 2014-08-08 19:53:09 7 | 8 | from sqlalchemy import INTEGER, Column, Integer, Text, select, text, update 9 | 10 | from .basedb import AlchemyMixin, BaseDB 11 | 12 | 13 | class Site(BaseDB,AlchemyMixin): 14 | ''' 15 | Site db 16 | 17 | regEn 18 | ''' 19 | __tablename__ = 'site' 20 | 21 | id = Column(Integer, primary_key=True) 22 | regEn = Column(INTEGER, nullable=False, server_default=text("'1'")) 23 | MustVerifyEmailEn = Column(INTEGER, nullable=False, server_default=text("'0'")) 24 | logDay = Column(INTEGER, nullable=False, server_default=text("'365'")) 25 | repos = Column(Text, nullable=False) 26 | 27 | def add(self, sql_session=None): 28 | insert = dict(regEn = 1) 29 | return self._insert(Site(**insert), sql_session=sql_session) 30 | 31 | def mod(self, id, sql_session=None, **kwargs): 32 | assert id, 'need id' 33 | return self._update(update(Site).where(Site.id == id).values(**kwargs), sql_session=sql_session) 34 | 35 | async def get(self, id, fields=None, one_or_none=False, first=True, to_dict=True, sql_session=None): 36 | assert id, 'need id' 37 | if fields is None: 38 | _fields = Site 39 | else: 40 | _fields = (getattr(Site, field) for field in fields) 41 | 42 | smtm = select(_fields).where(Site.id == id) 43 | 44 | result = await self._get(smtm, one_or_none=one_or_none, first=first, sql_session=sql_session) 45 | if to_dict and result is not None: 46 | return self.to_dict(result,fields) 47 | return result 48 | -------------------------------------------------------------------------------- /db/tasklog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # vim: set et sw=4 ts=4 sts=4 ff=unix fenc=utf8: 4 | # Author: Binux 5 | # http://binux.me 6 | # Created on 2014-08-08 20:18:29 7 | 8 | import time 9 | 10 | from sqlalchemy import INTEGER, Column, Integer, Text, delete, select 11 | from sqlalchemy.dialects.mysql import TINYINT 12 | 13 | from .basedb import AlchemyMixin, BaseDB 14 | 15 | 16 | class Tasklog(BaseDB,AlchemyMixin): 17 | ''' 18 | task log db 19 | 20 | id, taskid, success, ctime, msg 21 | ''' 22 | __tablename__ = 'tasklog' 23 | 24 | id = Column(Integer, primary_key=True) 25 | taskid = Column(INTEGER, nullable=False) 26 | success = Column(TINYINT(1), nullable=False) 27 | ctime = Column(INTEGER, nullable=False) 28 | msg = Column(Text) 29 | 30 | def add(self, taskid, success, msg='', sql_session=None): 31 | now = time.time() 32 | 33 | insert = dict( 34 | taskid = taskid, 35 | success = success, 36 | msg = msg, 37 | ctime = now, 38 | ) 39 | return self._insert(Tasklog(**insert), sql_session=sql_session) 40 | 41 | async def list(self, fields=None, limit=1000, to_dict=True, sql_session=None, **kwargs): 42 | if fields is None: 43 | _fields = Tasklog 44 | else: 45 | _fields = (getattr(Tasklog, field) for field in fields) 46 | 47 | smtm = select(_fields) 48 | 49 | for key, value in kwargs.items(): 50 | smtm = smtm.where(getattr(Tasklog, key) == value) 51 | 52 | if limit: 53 | smtm = smtm.limit(limit) 54 | 55 | result = await self._get(smtm.order_by(Tasklog.ctime.desc()), sql_session=sql_session) 56 | if to_dict and result is not None: 57 | return [self.to_dict(row,fields) for row in result] 58 | return result 59 | 60 | def delete(self, id, sql_session=None): 61 | return self._delete(delete(Tasklog).where(Tasklog.id == id), sql_session=sql_session) 62 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | qd: 5 | image: qdtoday/qd:latest 6 | # image: qdtoday/qd:lite-latest # 精简版 7 | # image: qdtoday/qd:dev # 开发版 8 | container_name: qd 9 | depends_on: 10 | - redis 11 | ports: 12 | - "8923:80" 13 | volumes: 14 | - "./config:/usr/src/app/config" 15 | environment: 16 | # - QIANDAO_DEBUG=False 17 | # - MULTI_PROCESS=False 18 | # - AUTO_RELOAD=False 19 | # - GZIP=True 20 | # - BIND=0.0.0.0 21 | # - PORT=80 22 | # - QUEUE_NUM=50 23 | # - ENABLE_HTTPS=False 24 | # - ACCESS_LOG=True 25 | # - DOMAIN= 26 | # - USE_PYCURL=True 27 | # - ALLOW_RETRY=True 28 | # - DNS_SERVER= 29 | # - CURL_ENCODING=True 30 | # - CURL_CONTENT_LENGTH=True 31 | # - NOT_RETRY_CODE=301|302|303|304|305|307|400|401|403|404|405|407|408|409|410|412|415|413|414|500|501|502|503|504|599 32 | # - EMPTY_RETRY=True 33 | # - COOKIE_DAY=5 34 | # - JAWSDB_MARIA_URL=mysql://user:pass@localhost:3306/dbname?auth_plugin= 35 | - REDISCLOUD_URL=redis://redis:6379 36 | # - TRACEBACK_PRINT=False 37 | # - PUSH_PIC_URL=https://fastly.jsdelivr.net/gh/qd-today/qd@master/web/static/img/push_pic.png 38 | # - PUSH_BATCH_SW=True 39 | # - PUSH_BATCH_DELTA=60 40 | # - WS_PING_INTERVAL=5 41 | # - WS_PING_TIMEOUT=30 42 | # - WS_MAX_MESSAGE_SIZE=10485760 43 | # - WS_MAX_QUEUE_SIZE=100 44 | # - WS_MAX_CONNECTIONS_SUBSCRIBE=30 45 | # - SUBSCRIBE_ACCELERATE_URL=jsdelivr_cdn 46 | # - DB_TYPE=sqlite3 47 | # - QIANDAO_SQL_LOGGING_NAME=qiandao.sql 48 | # - QIANDAO_SQL_LOGGING_LEVEL=WARNING 49 | # - QIANDAO_SQL_POOL_LOGGING_NAME=qiandao.sql.pool 50 | # - QIANDAO_SQL_POOL_LOGGING_LEVEL=WARNING 51 | # - QIANDAO_SQL_POOL_SIZE=5 52 | # - QIANDAO_SQL_MAX_OVERFLOW=10 53 | # - QIANDAO_SQL_POOL_PRE_PING=True 54 | # - QIANDAO_SQL_POOL_RECYCLE=3600 55 | # - QIANDAO_SQL_POOL_TIMEOUT=30 56 | # - QIANDAO_SQL_POOL_USE_LIFO=True 57 | # - REDIS_DB_INDEX=1 58 | # - QIANDAO_EVIL=1000 59 | # - EVIL_PASS_LAN_IP=True 60 | - PBKDF2_ITERATIONS=400 61 | - AES_KEY=binux 62 | - COOKIE_SECRET=binux 63 | # - CHECK_TASK_LOOP=500 64 | # - TASK_MAX_RETRY_COUNT=8 65 | # - DOWNLOAD_SIZE_LIMIT=5242880 66 | # - REQUEST_TIMEOUT=30.0 67 | # - CONNECT_TIMEOUT=30.0 68 | # - DELAY_MAX_TIMEOUT=29.9 69 | # - UNSAFE_EVAL_TIMEOUT=3.0 70 | # - PROXIES= 71 | # - PROXY_DIRECT_MODE=regexp 72 | # - PROXY_DIRECT=(?xi)\A([a-z][a-z0-9+\-.]*://)?(0(.0){3}|127(.0){2}.1|localhost|\[::([\d]+)?\])(:[0-9]+)? 73 | # - NEW_TASK_DELAY=1 74 | # - EXTRA_ONNX_NAME= 75 | # - EXTRA_CHARSETS_NAME= 76 | # - NEW_TASK_DELAY=1 77 | # - MAIL_SMTP= 78 | # - MAIL_PORT=465 79 | # - MAIL_SSL=True 80 | # - MAIL_USER= 81 | # - MAIL_PASSWORD= 82 | # - MAIL_FROM=${MAIL_USER} 83 | # - MAIL_DOMAIN= 84 | # - MAILGUN_KEY= 85 | # - USER0ISADMIN=True 86 | 87 | redis: 88 | image: redis:alpine 89 | container_name: redis 90 | command: ["--loglevel warning"] 91 | # command: redis-server /usr/local/etc/redis/redis.conf 92 | volumes: 93 | # - ./redis/redis.conf:/usr/local/etc/redis/redis.conf:rw 94 | - ./redis/data:/data:rw 95 | -------------------------------------------------------------------------------- /libs/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.append(os.path.abspath(os.path.dirname(os.path.dirname(__file__)))) 5 | -------------------------------------------------------------------------------- /libs/json_typing.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from typing import TypedDict 3 | 4 | 5 | class Env(TypedDict): 6 | variables: typing.Dict[str, str] 7 | "用户设置 Task 变量" 8 | session: typing.List # TODO:暂时还没看到list中是什么东西 9 | 10 | 11 | class _NameVlaue(TypedDict): 12 | name: str 13 | value: str 14 | 15 | 16 | Cookie = _NameVlaue 17 | Header = _NameVlaue 18 | 19 | 20 | class Request(TypedDict): 21 | url: str 22 | method: typing.Literal[ 23 | "GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS" 24 | ] # | str 25 | headers: typing.List[Header] 26 | cookies: typing.List[Cookie] 27 | 28 | 29 | ExtractVariable = TypedDict( # 内含 from 关键字,所以用这个方式声明 30 | "ExtractVariable", 31 | { 32 | "name": str, 33 | "re": str, 34 | "from": typing.Literal["content", "status", "header", "header-location"], 35 | }, 36 | ) 37 | FailedAssert = TypedDict( 38 | "FailedAssert", 39 | { 40 | "re": str, 41 | "from": typing.Literal["content", "status", "header", "header-location"], 42 | }, 43 | ) 44 | SuccessAssert = TypedDict( 45 | "SuccessAssert", 46 | { 47 | "re": str, 48 | "from": typing.Literal["content", "status", "header", "header-location"], 49 | }, 50 | ) 51 | 52 | 53 | class Rule(TypedDict): 54 | extract_variables: typing.List[ExtractVariable] 55 | failed_asserts: typing.List[FailedAssert] 56 | success_asserts: typing.List[SuccessAssert] 57 | 58 | 59 | class HARTest(TypedDict): # 可能也被用到了其它地方,暂时命名为 HARTest 60 | env: Env 61 | request: Request 62 | rule: Rule 63 | -------------------------------------------------------------------------------- /libs/log.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # vim: set et sw=4 ts=4 sts=4 ff=unix fenc=utf8: 4 | # Author: A76yyyy 5 | # http://www.a76yyyy.cn 6 | # Created on 2022-03-14 11:39:57 7 | 8 | import logging 9 | import os 10 | import sys 11 | import time 12 | 13 | import tornado.log 14 | 15 | from config import debug 16 | 17 | default_level = logging.DEBUG if debug else logging.INFO 18 | 19 | class Log(object): 20 | ''' 21 | 封装后的logging 22 | ''' 23 | def __init__(self , logger = None, logger_level = default_level, log_dir_path = None, channel_level = default_level): 24 | ''' 25 | 指定保存日志的文件路径,日志级别,以及调用文件 26 | 将日志存入到指定的文件中 27 | ''' 28 | 29 | # 创建一个logger 30 | logging.basicConfig() 31 | if logger is None or isinstance(logger,str): 32 | self.logger = logging.getLogger(logger) 33 | elif isinstance(logger,logging.Logger): 34 | self.logger = logger 35 | self.logger.setLevel(logger_level) 36 | self.logger.propagate = False 37 | 38 | # 创建一个handler,用于写入日志文件 39 | self.log_time = time.strftime("%Y_%m_%d") 40 | 41 | # 定义handler的输出格式 42 | fmt = "%(color)s[%(levelname)1.1s %(asctime)s %(name)s %(module)s:%(lineno)d]%(end_color)s %(message)s" 43 | formatter = tornado.log.LogFormatter(fmt=fmt) 44 | 45 | self.logger.handlers.clear() 46 | 47 | # 创建一个handler,用于输出到控制台 48 | ch = logging.StreamHandler(sys.stdout) 49 | ch.setFormatter(formatter) 50 | ch.setLevel(channel_level) 51 | 52 | # 给logger添加handler 53 | self.logger.addHandler(ch) 54 | 55 | if log_dir_path: 56 | self.logger.propagate = True 57 | self.log_name = os.path.join(log_dir_path,self.log_time + '.log') 58 | fh = logging.FileHandler(self.log_name, 'a', encoding='utf-8') 59 | fh.setFormatter(formatter) 60 | self.logger.addHandler(fh) 61 | 62 | def getlogger(self): 63 | return self.logger 64 | -------------------------------------------------------------------------------- /libs/parse_url.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # vim: set et sw=4 ts=4 sts=4 ff=unix fenc=utf8: 4 | # Author: A76yyyy 5 | # http://www.a76yyyy.cn 6 | # Created on 2022-03-14 12:00:00 7 | 8 | import re 9 | 10 | 11 | def parse_url(url): 12 | if not url: 13 | return None 14 | result = re.match('((?P(https?|socks5h?)+)://)?((?P[^:@/]+)(:(?P[^@/]+))?@)?(?P[^:@/]+):(?P\d+)', url) 15 | return None if not result else { 16 | 'scheme': result.group('scheme'), 17 | 'host': result.group('host'), 18 | 'port': int(result.group('port')), 19 | 'username': result.group('username'), 20 | 'password': result.group('password'), 21 | 'href': str(url) 22 | } 23 | -------------------------------------------------------------------------------- /qd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # vim: set et sw=4 ts=4 sts=4 ff=unix fenc=utf8: 4 | # Author: Binux 5 | # http://binux.me 6 | # Created on 2014-08-18 12:17:21 7 | 8 | import asyncio 9 | import json 10 | import sys 11 | 12 | import config 13 | 14 | config.display_import_warning = False 15 | from libs.fetcher import Fetcher 16 | from libs.log import Log 17 | from run import start_server 18 | 19 | logger_QD = Log('QD').getlogger() 20 | 21 | def usage(): 22 | print("{} tpl.har [--key=value] [env.json]".format(sys.argv[0])) 23 | sys.exit(1) 24 | 25 | if __name__ == '__main__': 26 | if len(sys.argv) < 3: 27 | usage() 28 | 29 | # load tpl 30 | tpl_file = sys.argv[1] 31 | try: 32 | # deepcode ignore PT: tpl_file is a file 33 | tpl = json.load(open(tpl_file,encoding='utf-8')) 34 | except Exception as e: 35 | logger_QD.error(e) 36 | usage() 37 | 38 | # load env 39 | variables = {} 40 | env = {} 41 | env_file = None 42 | for each in sys.argv[2:]: 43 | if each.startswith('--'): 44 | key, value = each.split('=', 1) 45 | key = key.lstrip('--') 46 | variables[key] = value 47 | else: 48 | env_file = each 49 | if env_file: 50 | try: 51 | # deepcode ignore PT: env_file is a file 52 | env = json.load(open(env_file,encoding='utf-8')) 53 | except Exception as e: 54 | logger_QD.error(e) 55 | usage() 56 | if 'variables' not in env or not isinstance(env['variables'], dict) \ 57 | or 'session' not in env: 58 | env = { 59 | 'variables': env, 60 | 'session': [], 61 | } 62 | env['variables'].update(variables) 63 | 64 | # 判断 端口 是否被占用 65 | import re 66 | import socket 67 | def check_port(port): 68 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 69 | try: 70 | s.connect(('127.0.0.1', port)) 71 | s.shutdown(2) 72 | logger_QD.debug('Port %s is used' % port) 73 | return False 74 | except: 75 | logger_QD.debug('Port %s is available' % port) 76 | return True 77 | manual_start = check_port(config.port) 78 | if manual_start: 79 | logger_QD.info('QD service is not running on port %s' % config.port) 80 | logger_QD.info('QD service will be started on port %s' % config.port) 81 | # 创建新进程, 以执行 run 中的 main 异步函数 82 | import multiprocessing 83 | p = multiprocessing.Process(target=start_server) 84 | p.start() 85 | # 循环检测端口是否被占用, 如果被占用, 则继续下一步 86 | while True: 87 | if not check_port(config.port): 88 | break 89 | else: 90 | import time 91 | time.sleep(1) 92 | else: 93 | logger_QD.info('QD service is running on port %s' % config.port) 94 | 95 | # do fetch 96 | ioloop = asyncio.new_event_loop() 97 | asyncio.set_event_loop(ioloop) 98 | result:asyncio.Task = asyncio.ensure_future(Fetcher().do_fetch(tpl, env), loop=ioloop) 99 | logger_QD.info('QD start to do fetch: %s' % tpl_file) 100 | ioloop.run_until_complete(result) 101 | ioloop.stop() 102 | 103 | try: 104 | result = result.result() 105 | except Exception as e: 106 | print('QD failed!', e) 107 | else: 108 | print('QD success! Results:\n', result.get('variables', {}).get('__log__', '').replace('\\r\\n','\r\n')) 109 | 110 | if manual_start: 111 | p.terminate() 112 | p.join() 113 | logger_QD.info('QD service is ended. ') 114 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # These requirements were autogenerated by pipenv 3 | # To regenerate from the project's Pipfile, run: 4 | # 5 | # pipenv requirements 6 | # 7 | 8 | -i https://mirrors.cloud.tencent.com/pypi/simple/ 9 | --extra-index-url https://pypi.org/simple 10 | aiofiles==23.1.0 11 | aiohttp==3.8.4 12 | aiomysql==0.1.1 13 | aiosignal==1.3.1; python_version >= '3.7' 14 | aiosqlite==0.19.0 15 | async-timeout==4.0.2; python_full_version <= '3.11.2' 16 | attrs==23.1.0; python_version >= '3.7' 17 | certifi==2023.5.7; python_version >= '3.6' 18 | cffi==1.15.1 19 | charset-normalizer==3.1.0 20 | colorama==0.4.6; sys_platform == 'win32' 21 | croniter==1.3.15 22 | cryptography==41.0.1 23 | faker==18.10.1 24 | frozenlist==1.3.3; python_version >= '3.7' 25 | greenlet==2.0.2; python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32'))))) 26 | idna==3.4; python_version >= '3.5' 27 | incremental==22.10.0 28 | jinja2==3.1.2 29 | markupsafe==2.1.3; python_version >= '3.7' 30 | multidict==6.0.4; python_version >= '3.7' 31 | passlib==1.7.4 32 | pbkdf2==1.3 33 | pycparser==2.21 34 | pycryptodome==3.18.0 35 | pymysql==1.0.3; python_version >= '3.7' 36 | pysocks==1.7.1 37 | python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 38 | pytz==2023.3 39 | redis==4.5.5 40 | requests==2.31.0 41 | six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 42 | sqlalchemy[asyncio]==1.4.48 43 | tornado==6.3.2 44 | u-msgpack-python==2.8.0 45 | urllib3==2.0.2; python_version >= '3.7' 46 | yarl==1.9.2; python_version >= '3.7' 47 | # ddddocr==1.4.7 48 | # opencv-python-headless==4.7.0.68 49 | # pycurl==7.45.2 -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim:fenc=utf-8 3 | # 4 | # Copyright © 2016 Binux 5 | 6 | import asyncio 7 | import json 8 | import logging 9 | import os 10 | import platform 11 | import sys 12 | 13 | import tornado.log 14 | from tornado.httpserver import HTTPServer 15 | from tornado.ioloop import IOLoop, PeriodicCallback 16 | 17 | import config 18 | from db import DB, db_converter 19 | from db.basedb import engine 20 | from libs.log import Log 21 | from web.app import Application 22 | from worker import BatchWorker, QueueWorker 23 | 24 | 25 | def start_server(): 26 | if sys.getdefaultencoding() != 'utf-8': 27 | import importlib 28 | importlib.reload(sys) 29 | # init logging 30 | logger = Log().getlogger() 31 | logger_QD = Log('QD.Run').getlogger() 32 | 33 | if config.debug: 34 | channel = logging.StreamHandler(sys.stderr) 35 | channel.setFormatter(tornado.log.LogFormatter()) 36 | channel.setLevel(logging.WARNING) 37 | logger_QD.addHandler(channel) 38 | 39 | if not config.accesslog: 40 | tornado.log.access_log.disabled = True 41 | else: 42 | tornado.log.access_log = Log('tornado.access').getlogger() 43 | # tornado.log.app_log = Log('tornado.application').getlogger() 44 | 45 | if len(sys.argv) > 2 and sys.argv[1] == '-p' and sys.argv[2].isdigit(): 46 | port = int(sys.argv[2]) 47 | else: 48 | port = config.port 49 | 50 | if platform.system() == 'Windows': 51 | config.multiprocess = False 52 | if config.multiprocess and config.autoreload: 53 | config.autoreload = False 54 | 55 | try: 56 | database = DB() 57 | converter = db_converter.DBconverter(database) 58 | loop = asyncio.new_event_loop() 59 | asyncio.set_event_loop(loop) 60 | run = asyncio.ensure_future(converter.ConvertNewType(database) , loop=loop) 61 | loop.run_until_complete(run) 62 | 63 | default_version = json.load(open(os.path.join(os.path.dirname(__file__), 'version.json'),'r', encoding='utf-8'))['version'] 64 | App= Application(database, default_version) 65 | http_server = HTTPServer(App, xheaders=True) 66 | http_server.bind(port, config.bind) 67 | if config.multiprocess: 68 | http_server.start(num_processes=0) 69 | else: 70 | http_server.start() 71 | 72 | io_loop = IOLoop.instance() 73 | try: 74 | if config.worker_method.upper() == 'QUEUE': 75 | worker = QueueWorker(database) 76 | io_loop.add_callback(worker) 77 | elif config.worker_method.upper() == 'BATCH': 78 | worker = BatchWorker(database) 79 | PeriodicCallback(worker, config.check_task_loop).start() 80 | else: 81 | raise Exception('worker_method must be Queue or Batch, please check config!') 82 | except Exception as e: 83 | logger.exception('worker start error!') 84 | raise KeyboardInterrupt() 85 | 86 | logger_QD.info("Http Server started on %s:%s", config.bind, port) 87 | io_loop.start() 88 | except KeyboardInterrupt : 89 | logger_QD.info("Http Server is being manually interrupted... ") 90 | loop = asyncio.new_event_loop() 91 | asyncio.set_event_loop(loop) 92 | run = asyncio.ensure_future(engine.dispose() , loop=loop) 93 | loop.run_until_complete(run) 94 | logger_QD.info("Http Server is ended. ") 95 | 96 | 97 | if __name__ == "__main__": 98 | start_server() 99 | -------------------------------------------------------------------------------- /ssh/qd_fetch: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIG4wIBAAKCAYEA2FD1IBCZcDgu7BjS2iySxTUlYWEWdkVB7xdmKJT4kUTEdEve 3 | 7lvJb6PalpW8k1l1N7r/vFRDvSMe1Rd1Sc9n7dsOacCMsjy8Q5UDPGQ7ccahTDI7 4 | ygSA3igmasn9g5l21Sm04mP0riFzSybP7Xvne6EE5P6/CnO20kk6wWXmohhaIJjV 5 | pEhJ4/fMVWWHPggD90aKCEMKQJAL+FmLTpNl2t9jEUZczzKVlgEQEssnMZERiS4h 6 | wKFMy8qmupjjddGtyjNWR6WEE+Kjj/KexcthFYWNAb2m8K/5jna439sxOe17PUMS 7 | CAbxjr4rRZTWRqk8FTSVwW5EZmBKAJCXMJIM8gaH9vG3VHt8btE0y0w8pgRbQ2bs 8 | TZ/3OTLHKQtPS7QMs4Ko/Z7LWHEzanUgZvWgvuv8SPEtXRVO+BuCsB6BV8RA4JbG 9 | ogEx54oDn3Imo1+nWfrLte2tA+GiQXod9pwm9lVZ0L7oHL+y9OXCPFSPt5G8II+z 10 | tid4yd8+D7PMFUp9AgMBAAECggGAPxbNhz6QIcShGsbaqDsby1yGvHH+hRwJ8NuY 11 | NfnMzUGLMof2SvqGM4NMe2LMjwUmRMzLTXgeEYmusmOXF6HgI+UhvQIlhs5j8ioB 12 | uZlaJQLThE+aVH8fvqtjGP40bC9UEzOIyAZzFF45LA/z0MW0a8A4lzkdKsU46vfk 13 | pQc9TsVfc+ZfWPykYFKjS7zCxvq7fWEbXPGAiff5JcOn95DRwhYo5J7q7ptIOWmx 14 | 8GlD0xcGUEYeHfptVXqTShTRMkmmpuLq/uSYWsfIbwcJYXNx1cqVP8VRvM/Fl3uA 15 | yo+w7BW0JFcP/AKnCvnWAYfY6qjdYqewuUES3p5Boy0KpsBpg50LtWOoM7JXNdnh 16 | 9M0Ahwkwq2uoU+UklHEaM3IxJI6LVKG2Wv4MU/xXqAPYa1P2iG8x7Gx0UoIlI3GT 17 | gS7MdGxWzgC1qL/V+CobEWivjw/DfCturoAyIbirWMK6UK4WEx1f8uyAjeNTMB+i 18 | RMn9J4HRMVK+6fI6AzFHBOi8nFnBAoHBAPHb59bPUJhRggiKhFum8r5u4Ad+EpYb 19 | SAf/eTGLiSBzKVwfQTZVZtbD8aw0Uo4Lja2kGXywz1R0bHo7kYu9fUeqvTYI6uPn 20 | RWC94EFxS+Uhy6kQa+HluSFAlBMpl8/d0wICMhKTovgAMSrLV1/r/MwDnQdNRhn8 21 | qWgP0wNwN6Y8AWhwEVZjtxdAba34eMtZ60RnP4i77kmwfp0uHl1hf7j1cEMgEYis 22 | rWHEPVmpuvTnO2f65b03i/5vA9hcmBCgRQKBwQDk9ruyLMJDjd+fd9kn40uiiny6 23 | 4+wUDuOE5cRM/SJGxAHCj2nPYZ+Jb/BtLlZPnX4/o0xH7i45M8i4F7WwQQRLT+GL 24 | DMdOLXnilg05KThTrfuSWeFQaYcsTuZH4xoMb3JN+bbm6byxpkrPFerKLFuXwaJB 25 | QpqxZb5RrUAmzA18zjK9uP6eH/KoNq9DXYpGSQrPpqKSMeyPdN5MfT6ire7TCf5I 26 | NdCSrtoiWR93K0YZ+jy/fjhi8YhhGtsDkLh2sNkCgcAn+YDIMFvR0Sb84mgd/eoB 27 | L7I3is7noHq3V7JgTf/MLTyYzVGp+VOWC6zWGY/Lin1QRd8wjMi33MZRLadQwXED 28 | d2TumWq5YHBHHRsDtgYlSQCwYNZz79Q5esL+p9E/uho2Ksa760r0UpPNzu4SHJ8b 29 | 2T0LB+qeQPR/Dp6B4vpPr3tMF7nF3ZVVcoCuUHMmqSYJmEcYAnfQFivXLtg19vd1 30 | SpvTfGlpfaDql9TMDRGpiM540S9HpvashwzLpgI8PUECgcBin1iHzbm7qW+qW9BA 31 | 2ynoEGjbg3NbzHH5+5JLazmJa8IqUOXNuwoiBAJ2tN/emUMZHDiuPwzP/Ns3WJcd 32 | HVRBMF5BK7bpmLMIg4OcnIGzoZxcFLT1kw7zaeTNEChXNOXyal57JjDuAGf2vUUN 33 | 4n9tZa2DGLJTOTT6L3sTew9QM+0tRXL/H1nBVRS3GqKF1c5/VRJvkGuJ+1gJcGU7 34 | Y1ocsxBBgFSf6grxgpwIE661X9Es1qxpCgqpcQzngz3BejkCgcEAmq7+WtnKwV1C 35 | 4VcD2Rg8LGks++3DMaPP4LyqDnUjGi3ieJ0bzO1oopCfghfK145FfdlP/XB3m3j9 36 | R0t4qMn01WinaklJz1DXvVDrWqCkLIiYPjCxY4C/NIjRgTqmN8EoYDe5DXcWpVx5 37 | Eo2nkWzJwelT7mpinM2ZLIuNTLgciVTfZ9ZEDcmSd2EqLSuQo40q0CIG22JXWK2x 38 | tK66q5b7fqo1iyS6yWG5hXDEElrVrRJOlmnYSfMLBY96qMjXKqAX 39 | -----END RSA PRIVATE KEY----- -------------------------------------------------------------------------------- /ssh/qd_fetch.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDYUPUgEJlwOC7sGNLaLJLFNSVhYRZ2RUHvF2YolPiRRMR0S97uW8lvo9qWlbyTWXU3uv+8VEO9Ix7VF3VJz2ft2w5pwIyyPLxDlQM8ZDtxxqFMMjvKBIDeKCZqyf2DmXbVKbTiY/SuIXNLJs/te+d7oQTk/r8Kc7bSSTrBZeaiGFogmNWkSEnj98xVZYc+CAP3RooIQwpAkAv4WYtOk2Xa32MRRlzPMpWWARASyycxkRGJLiHAoUzLyqa6mON10a3KM1ZHpYQT4qOP8p7Fy2EVhY0Bvabwr/mOdrjf2zE57Xs9QxIIBvGOvitFlNZGqTwVNJXBbkRmYEoAkJcwkgzyBof28bdUe3xu0TTLTDymBFtDZuxNn/c5MscpC09LtAyzgqj9nstYcTNqdSBm9aC+6/xI8S1dFU74G4KwHoFXxEDglsaiATHnigOfciajX6dZ+su17a0D4aJBeh32nCb2VVnQvugcv7L05cI8VI+3kbwgj7O2J3jJ3z4Ps8wVSn0= public 2 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 20230606, 3 | "Repo": ["https://github.com/qd-today/qd", "https://github.com/a76yyyy/qd", "https://github.com/binux/qiandao"], 4 | "Author&Collaborator": ["binux", "a76yyyy", "AragonSnow", "Mark", "PiDan", "acooler15", "billypon", "liubei121212", "gxitm", "戏如人生", "buzhibujuelb", "powersee", "cxk000"], 5 | "Theme": { 6 | "version": 20191106, 7 | "Pidan": "0.1.2", 8 | "Theme_url": "https://www.quchao.net/QianDao-Theme.html" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /web.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # vim: set et sw=4 ts=4 sts=4 ff=unix fenc=utf8: 4 | # Author: Binux 5 | # http://binux.me 6 | # Created on 2014-07-30 12:38:34 7 | 8 | import sys 9 | 10 | from tornado.httpserver import HTTPServer 11 | from tornado.ioloop import IOLoop 12 | 13 | import config 14 | from db import DB 15 | from libs.log import Log 16 | from web.app import Application 17 | 18 | if __name__ == "__main__": 19 | # init logging 20 | logger_Web = Log('QD.Web').getlogger() 21 | 22 | if not config.debug: 23 | import logging 24 | 25 | import tornado.log 26 | channel = logging.StreamHandler(sys.stderr) 27 | channel.setFormatter(tornado.log.LogFormatter()) 28 | channel.setLevel(logging.WARNING) 29 | logger_Web.addHandler(channel) 30 | 31 | if len(sys.argv) > 2 and sys.argv[1] == '-p' and sys.argv[2].isdigit(): 32 | port = int(sys.argv[2]) 33 | else: 34 | port = config.port 35 | 36 | http_server = HTTPServer(Application(DB()), xheaders=True) 37 | http_server.bind(port, config.bind) 38 | http_server.start() 39 | 40 | logger_Web.info("http server started on %s:%s", config.bind, port) 41 | IOLoop.instance().start() 42 | -------------------------------------------------------------------------------- /web/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /web/Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*global module:false*/ 2 | module.exports = function(grunt) { 3 | 4 | // Project configuration. 5 | grunt.initConfig({ 6 | // Metadata. 7 | pkg: grunt.file.readJSON('package.json'), 8 | banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - ' + 9 | '<%= grunt.template.today("yyyy-mm-dd") %>\n' + 10 | '<%= pkg.homepage ? "* " + pkg.homepage + "\\n" : "" %>' + 11 | '* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' + 12 | ' Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */\n', 13 | // Task configuration. 14 | concat: { 15 | options: { 16 | banner: '<%= banner %>', 17 | stripBanners: true 18 | }, 19 | dist: { 20 | src: ['lib/<%= pkg.name %>.js'], 21 | dest: 'dist/<%= pkg.name %>.js' 22 | } 23 | }, 24 | uglify: { 25 | options: { 26 | banner: '<%= banner %>' 27 | }, 28 | dist: { 29 | src: '<%= concat.dist.dest %>', 30 | dest: 'dist/<%= pkg.name %>.min.js' 31 | } 32 | }, 33 | jshint: { 34 | options: { 35 | curly: true, 36 | eqeqeq: true, 37 | immed: true, 38 | latedef: true, 39 | newcap: true, 40 | noarg: true, 41 | sub: true, 42 | undef: true, 43 | unused: true, 44 | boss: true, 45 | eqnull: true, 46 | globals: { 47 | jQuery: true 48 | } 49 | }, 50 | gruntfile: { 51 | src: 'Gruntfile.js' 52 | }, 53 | lib_test: { 54 | src: ['lib/**/*.js', 'test/**/*.js'] 55 | } 56 | }, 57 | watch: { 58 | gruntfile: { 59 | files: '<%= jshint.gruntfile.src %>', 60 | tasks: ['jshint:gruntfile'] 61 | }, 62 | lib_test: { 63 | files: '<%= jshint.lib_test.src %>', 64 | tasks: ['jshint:lib_test', 'nodeunit'] 65 | } 66 | }, 67 | bower: { 68 | install: { 69 | options: { 70 | targetDir: './static/components', 71 | layout: 'byComponent', 72 | copy: true, 73 | install: true, 74 | verbose: false, 75 | cleanTargetDir: false, 76 | cleanBowerDir: false, 77 | bowerOptions: {} 78 | } 79 | } 80 | } 81 | }); 82 | 83 | // These plugins provide necessary tasks. 84 | grunt.loadNpmTasks('grunt-contrib-concat'); 85 | grunt.loadNpmTasks('grunt-contrib-uglify'); 86 | grunt.loadNpmTasks('grunt-contrib-jshint'); 87 | grunt.loadNpmTasks('grunt-contrib-watch'); 88 | grunt.loadNpmTasks('grunt-bower-task'); 89 | grunt.loadNpmTasks('grunt-contrib-clean'); 90 | // Default task. 91 | grunt.registerTask('default', ['jshint', 'concat', 'uglify','watch']); 92 | 93 | }; 94 | -------------------------------------------------------------------------------- /web/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # vim: set et sw=4 ts=4 sts=4 ff=unix fenc=utf8: 4 | # Author: Binux 5 | # http://binux.me 6 | # Created on 2014-07-30 12:22:47 7 | 8 | import os 9 | import sys 10 | 11 | sys.path.append(os.path.abspath(os.path.dirname(os.path.dirname(__file__)))) 12 | -------------------------------------------------------------------------------- /web/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # vim: set et sw=4 ts=4 sts=4 ff=unix fenc=utf8: 4 | # Author: Binux 5 | # http://binux.me 6 | # Created on 2014-07-30 12:22:52 7 | 8 | import os 9 | 10 | import jinja2 11 | import tornado.web 12 | 13 | import config 14 | from db import DB 15 | from libs import utils 16 | from libs.fetcher import Fetcher 17 | from libs.log import Log 18 | from web.handlers import handlers, ui_methods, ui_modules 19 | 20 | logger_Web = Log('QD.Web').getlogger() 21 | class Application(tornado.web.Application): 22 | def __init__(self, db:DB, default_version=None): 23 | settings = dict( 24 | template_path = os.path.join(os.path.dirname(__file__), "tpl"), 25 | static_path = os.path.join(os.path.dirname(__file__), "static"), 26 | debug = config.debug, 27 | gzip = config.gzip, 28 | autoreload = config.autoreload, 29 | 30 | cookie_secret = config.cookie_secret, 31 | login_url = '/login', 32 | websocket_ping_interval = config.websocket.ping_interval, 33 | websocket_ping_timeout = config.websocket.ping_timeout, 34 | websocket_max_message_size = config.websocket.max_message_size, 35 | ) 36 | 37 | super(Application, self).__init__(handlers, **settings) 38 | 39 | self.jinja_env = jinja2.Environment( 40 | loader=jinja2.FileSystemLoader(settings['template_path']), 41 | extensions=['jinja2.ext.loopcontrols', ], 42 | autoescape=True, 43 | auto_reload=config.autoreload) 44 | 45 | self.db = db 46 | self.version = default_version or 'Debug' 47 | 48 | self.fetcher = Fetcher() 49 | 50 | self.jinja_env.globals.update({ 51 | 'config': config, 52 | 'format_date': utils.format_date, 53 | 'varbinary2ip': utils.varbinary2ip, 54 | 'version': self.version, 55 | }) 56 | self.jinja_env.filters.update(ui_methods) 57 | -------------------------------------------------------------------------------- /web/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qd_for_python3", 3 | "version": "1.0.3", 4 | "authors": [ 5 | "(a76yyyy ", 6 | "AragonSnow", 7 | "binux )" 8 | ], 9 | "description": "QD", 10 | "homepage": "https://github.com/qd-today/qd", 11 | "private": true, 12 | "ignore": [ 13 | "**/.*", 14 | ".bowerrc", 15 | ".gitignore", 16 | ".jshintignore", 17 | ".jshintrc", 18 | "node_modules", 19 | "bower_components", 20 | "static/components", 21 | "test", 22 | "tests", 23 | "src", 24 | "source", 25 | "spec", 26 | "gruntfile.js", 27 | "README.md" 28 | ], 29 | "dependencies": { 30 | "blueimp-md5": "^2.19.0", 31 | "js-base64": "^3.7.5", 32 | "angular": "~1.8.2", 33 | "jquery": "^3.7.0", 34 | "bootstrap": "=3.2.0", 35 | "seajs": "=2.3.0", 36 | "select2": "^4.0.13", 37 | "nprogress": "^0.2.0", 38 | "jquery.tablesorter": "^2.31.3", 39 | "Font-Awesome": "^6.4.0", 40 | "clipboard": "^2.0.11", 41 | "draggable-polyfill": "XboxYan/draggable-polyfill", 42 | "lunar-javascript":"6tail/lunar-javascript" 43 | }, 44 | "main": "", 45 | "license": "MIT", 46 | "keywords": [ 47 | "QD", 48 | "QD-TODAY", 49 | "har", 50 | "http", 51 | "web" 52 | ], 53 | "exportsOverride": { 54 | "blueimp-md5": { 55 | "js": "js/md5.js" 56 | }, 57 | "js-base64": { 58 | "": "base64.js" 59 | }, 60 | "angular": { 61 | "": "*.min.js" 62 | }, 63 | "jquery": { 64 | "dist": "dist/jquery.min.js" 65 | }, 66 | "bootstrap": { 67 | "dist/css": "dist/css/*.*", 68 | "dist/js": "dist/js/*.js", 69 | "dist/fonts": "dist/fonts/*.*" 70 | }, 71 | "seajs": { 72 | "dist": "dist/sea.js" 73 | }, 74 | "select2": { 75 | "dist/js": "dist/js/*.min.js", 76 | "dist/css": "dist/css/*.min.css" 77 | }, 78 | "nprogress": { 79 | "": [ 80 | "nprogress.js", 81 | "nprogress.css" 82 | ] 83 | }, 84 | "jquery.tablesorter": { 85 | "dist/js": "dist/js/*.min.js", 86 | "css": "css/*.css" 87 | }, 88 | "Font-Awesome": { 89 | "css": "css/*.min.css", 90 | "fonts": "fonts/*.*" 91 | }, 92 | "clipboard": { 93 | "dist": "dist/clipboard.min.js" 94 | }, 95 | "draggable-polyfill": { 96 | "lib": "lib/draggable-polyfill.js" 97 | }, 98 | "lunar-javascript": { 99 | "": "lunar.js" 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /web/docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from "module"; 2 | import { defineConfig } from "vitepress"; 3 | import locales from "./locales"; 4 | 5 | const require = createRequire(import.meta.url); 6 | const pkg = require("vitepress/package.json"); 7 | 8 | export default defineConfig({ 9 | title: "QD", 10 | locales: locales.locales, 11 | base: "/qd/", 12 | head: [["link", { rel: "icon", href: "/qd/favicon.ico" }]], 13 | themeConfig: { 14 | search: { 15 | provider: "local", 16 | options: { 17 | locales: { 18 | zh_CN: { 19 | translations: { 20 | button: { 21 | buttonText: "搜索文档", 22 | buttonAriaLabel: "搜索文档", 23 | }, 24 | modal: { 25 | noResultsText: "无法找到相关结果", 26 | resetButtonTitle: "清除查询条件", 27 | footer: { 28 | selectText: "选择", 29 | navigateText: "切换", 30 | }, 31 | }, 32 | }, 33 | }, 34 | }, 35 | }, 36 | }, 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /web/docs/.vitepress/locales/en_US.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'module' 2 | import { defineConfig } from 'vitepress' 3 | 4 | const require = createRequire(import.meta.url) 5 | const pkg = require('vitepress/package.json') 6 | 7 | export default defineConfig({ 8 | lang: 'en-US', 9 | description: 'A web framework for HTTP timed task automation.', 10 | 11 | themeConfig: { 12 | nav: nav(), 13 | 14 | logo: '/logo.png', 15 | 16 | lastUpdatedText: 'last Updated', 17 | 18 | sidebar: { 19 | '/guide/': sidebarGuide() 20 | }, 21 | 22 | socialLinks: [ 23 | { icon: 'github', link: 'https://github.com/qd-today/qd' }, 24 | { 25 | icon: { 26 | svg: '' 27 | }, link: 'https://gitee.com/qd-today/qd' 28 | }, 29 | ], 30 | 31 | footer: { 32 | message: 'Released under the MIT License.', 33 | copyright: 'Copyright © 2023-present QD Developers' 34 | }, 35 | 36 | editLink: { 37 | pattern: 'https://github.com/qd-today/qd/edit/master/web/docs/:path', 38 | text: 'Edit this page on GitHub' 39 | }, 40 | 41 | outline: { 42 | label: 'On this page', 43 | }, 44 | 45 | } 46 | }) 47 | 48 | function nav() { 49 | return [ 50 | { text: 'Guide', link: '/guide/what-is-qd' }, 51 | ] 52 | } 53 | 54 | function sidebarGuide() { 55 | return [ 56 | { 57 | text: 'Guide', 58 | items: [ 59 | { text: 'What is QD?', link: '/guide/what-is-qd' }, 60 | { text: 'Deployment', link: '/guide/deployment' }, 61 | { text: 'How to Use?', link: '/guide/how-to-use' }, 62 | { text: 'Update', link: '/guide/update' }, 63 | { text: 'FAQ', link: '/guide/faq' }, 64 | ] 65 | } 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /web/docs/.vitepress/locales/index.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | import en_US from './en_US' 3 | import zh_CN from './zh_CN' 4 | 5 | export default defineConfig({ 6 | locales: { 7 | root: { 8 | label: 'English', 9 | lang: en_US.lang, 10 | themeConfig: en_US.themeConfig, 11 | description: en_US.description 12 | }, 13 | zh_CN: { 14 | label: '简体中文', 15 | lang: zh_CN.lang, 16 | themeConfig: zh_CN.themeConfig, 17 | description: zh_CN.description 18 | } 19 | } 20 | }) -------------------------------------------------------------------------------- /web/docs/.vitepress/locales/zh_CN.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'module' 2 | import { defineConfig } from 'vitepress' 3 | 4 | const require = createRequire(import.meta.url) 5 | const pkg = require('vitepress/package.json') 6 | 7 | export default defineConfig({ 8 | lang: 'zh-CN', 9 | description: 'HTTP定时任务自动执行Web框架', 10 | 11 | themeConfig: { 12 | nav: nav(), 13 | 14 | logo: '/logo.png', 15 | 16 | lastUpdatedText: '最后更新', 17 | 18 | sidebar: { 19 | '/zh_CN/guide/': sidebarGuide() 20 | }, 21 | 22 | socialLinks: [ 23 | { icon: 'github', link: 'https://github.com/qd-today/qd' }, 24 | { 25 | icon: { 26 | svg: '' 27 | }, link: 'https://gitee.com/qd-today/qd' 28 | }, 29 | ], 30 | 31 | footer: { 32 | message: '基于 MIT 许可证发布.', 33 | copyright: 'Copyright © 2023-当前 QD 开发者' 34 | }, 35 | 36 | editLink: { 37 | pattern: 'https://github.com/qd-today/qd/edit/master/web/docs/:path', 38 | text: '在 GitHub 中编辑此页面' 39 | }, 40 | 41 | outline: { 42 | label: '页面导航', 43 | }, 44 | 45 | } 46 | }) 47 | 48 | function nav() { 49 | return [ 50 | { text: '指南', link: '/zh_CN/guide/what-is-qd' }, 51 | ] 52 | } 53 | 54 | function sidebarGuide() { 55 | return [ 56 | { 57 | text: '指南', 58 | items: [ 59 | { text: '什么是 QD?', link: '/zh_CN/guide/what-is-qd' }, 60 | { text: '部署', link: '/zh_CN/guide/deployment' }, 61 | { text: '如何使用?', link: '/zh_CN/guide/how-to-use' }, 62 | { text: '更新', link: '/zh_CN/guide/update' }, 63 | { text: '常见问题', link: '/zh_CN/guide/faq' }, 64 | ] 65 | } 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /web/docs/guide/how-to-use.md: -------------------------------------------------------------------------------- 1 | # How to Use? 2 | 3 | ## Har Obtain 4 | 5 | ### 1. What is HAR? 6 | 7 | HAR: 8 | 9 | ### 2. Packet capture 10 | 11 | #### 2.1. Chrome or Edge 12 | 13 | 1. Press `F12`, `Ctrl + Shift + I`, or from the Chrome menu select `More tools` > `Developer tools`. 14 | 2. From the panel that opens at the bottom of your screen, select the `Network tab`. 15 | 3. Make sure the `Record` button in the upper left corner of the `Network tab` is shown in **red**. 16 | 4. If it's grey, click it once to start recording. 17 | 5. Check the box next to `Preserve log`. 18 | ![Preserve log](/preserve_log.png) 19 | 6. Click the `Clear` button to clear out any existing logs from the `Network tab`. 20 | 7. Now try to reproduce the task you were trying to do. 21 | 8. Once you have reproduced the task, right-click anywhere on the grid of network requests. 22 | 9. Select `Save as HAR with Content`. 23 | ![Save as HAR with Content](/save_har.png) 24 | 10. Save the file to your computer. 25 | 26 | #### 2.2. Firefox 27 | 28 | 1. Press `F12` ​(or Go to `Tools` > `Web Developer` > `Network`). 29 | 2. Now try to reproduce the task you were trying to do. 30 | 3. Right-click on the loaded results. 31 | 4. Select Save all as har. 32 | 33 | #### 2.3. Windows - Fiddler 34 | 35 | 1. Open Fiddler, open the `Tools` menu, select `Options`. 36 | 2. Select the `HTTPS tab`, check `Capture HTTPS Connects`. 37 | 3. Select the `Connections tab`, check `Decrypt HTTPS traffic`. 38 | 4. Now try to reproduce the task you were trying to do. 39 | 5. export to HAR format - please select HTTPArchive as the export method. 40 | 41 | #### 2.4. IOS - Stream 42 | 43 | 1. Open Stream. 44 | 2. Before capture the HTTPS request, you need to install the CA certificate, `setting` > `General` > `About` > `Certificate Trust Settings` to trust the CA certificate. 45 | 3. Click the start packet capture button, the phone will automatically pop up the VPN configuration window, and then select Allow. 46 | 4. Now try to reproduce the task you were trying to do. 47 | 5. On the app page, click Stop Capture to end this capture. 48 | 6. export the HAR file. 49 | 50 | ### 3. Community HAR 51 | 52 | 1. Click the `Community Template` button to the right of `my template`. 53 | 2. Update the repository to get the latest HAR file list. 54 | 3. Select the HAR file you want to use and click the `Subscribe` button to subscribe to the template. 55 | 4. Modify the template according to your needs. 56 | 5. Then jump to `step 5` of [3. Edit the template](#edit-the-template) to continue editing. 57 | 58 | ## Upload the HAR file 59 | 60 | 1. Access and login to QD framework. 61 | 2. Click the `+` button to the right of `my template`. 62 | 3. Upload the HAR file you just saved. 63 | 4. Click `upload` to continue. 64 | 65 | ## Edit the template 66 | 67 | ::: v-pre 68 | 69 | 1. Replace the username, password, cookie, header and other parts that change according to the user with a template similar to `{{ username }}`. (templates support **jinja2 syntax**) 70 | 2. Use the test panel in request editing to test whether the template is returned correctly, use the correct and wrong user names to test。 71 | 3. Fill in `success/failure assertion`, which helps to detect sign-in failures and template failures. 72 | 4. When some data from the previous request is needed in the request, variable extraction is used to extract the data through regularization and save it in the environment variable. Use `ab(\w+)cd`, the group selector, to select part of it. 73 | 5. Use `__log__` to extract task logs. 74 | 6. When all request edits are complete, use `Test` next to the Download button to test the overall. 75 | 7. The template being edited will be automatically saved in the browser cache, so don't worry about losing it. 76 | 8. Click the `Save` button to save the template. 77 | 9. Click the `Download` button to download the template. 78 | ::: 79 | 80 | ## Create scheduled task 81 | 82 | 1. Click the `+` button to the right of `my task`. 83 | 2. Select the template you just created. 84 | 3. Fill in the task variables, task interval, task group and task execution time. 85 | 4. Click the `Test` button to test the task. 86 | 5. Click the Save button to save the task. 87 | -------------------------------------------------------------------------------- /web/docs/guide/update.md: -------------------------------------------------------------------------------- 1 | # Update 2 | 3 | > Please always remember to back up your database before updating or redeploying. 4 | > 5 | > After the update, please restart the container or clear the browser cache. 6 | 7 | ## Source Code Deployment Update 8 | 9 | ``` sh 10 | # First cd to the directory of source code, execute the command and restart the process 11 | wget https://gitee.com/qd-today/qd/raw/master/update.sh -O ./update.sh && \ 12 | sh ./update.sh 13 | ``` 14 | 15 | ## Docker Compose Deployment Update 16 | 17 | ``` sh 18 | # First cd to the directory of docker-compose.yml, execute the command and restart the container 19 | docker compose pull && \ 20 | docker compose up -d 21 | ``` 22 | 23 | ## Docker Container Deployment Update 24 | 25 | ``` sh 26 | # Enter the container background first, restart the container after executing the command 27 | wget https://gitee.com/qd-today/qd/raw/master/update.sh -O /usr/src/app/update.sh && \ 28 | sh /usr/src/app/update.sh 29 | ``` 30 | 31 | ## Forcibly synchronize the latest source code 32 | 33 | ``` sh 34 | # First cd to the root directory of code, execute the command and restart the process 35 | # docker exec -it "container name" /bin/sh 36 | wget https://gitee.com/qd-today/qd/raw/master/update.sh -O ./update.sh && \ 37 | sh ./update.sh -f 38 | ``` 39 | -------------------------------------------------------------------------------- /web/docs/guide/what-is-qd.md: -------------------------------------------------------------------------------- 1 | # What is QD? 2 | 3 | QD is a web framework for HTTP timed task automation base on HAR Editor and Tornado Server. 4 | 5 | 7 | 8 | ## Features 9 | 10 | - **Har-based**: By simply uploading Har obtained through packet capture, you can create HTTP task template required by the framework. 11 | - **Tornado Server**: Use Tornado as a server to asynchronously respond to front-end and initiate HTTP requests. 12 | - **API & Plugin Support**: Various APIs and filters are built in for template creation, and custom plugins will be provided later. 13 | - **Open source**: QD is an open source project based on the MIT license. 14 | 15 | ## How to deploy 16 | 17 | Please refer: [Deployment](deployment) 18 | 19 | ## How to use 20 | 21 | Please refer: [How to use](how-to-use) 22 | 23 | ## How to Update 24 | 25 | Please refer: [Update](update) 26 | 27 | ## Discussion 28 | 29 | - Github: [Issue](https://github.com/qd-today/qd/issues) 30 | - Github: [Discussion](https://github.com/qd-today/qd/discussions) 31 | -------------------------------------------------------------------------------- /web/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | title: HTTP task automation framework 4 | 5 | hero: 6 | name: QD 7 | text: A web framework for HTTP timed task automation. 8 | tagline: "" 9 | image: 10 | src: /logo.png 11 | alt: QD 12 | actions: 13 | - theme: brand 14 | text: Get Started 15 | link: /guide/what-is-qd 16 | - theme: alt 17 | text: View on GitHub 18 | link: https://github.com/qd-today/qd 19 | 20 | features: 21 | - title: Har-based 22 | details: By simply uploading Har obtained through packet capture, you can create HTTP task template required by the framework. 23 | - title: Tornado Server 24 | details: Use Tornado as a server to asynchronously respond to front-end and initiate HTTP requests. 25 | - title: API & Plugin Support 26 | details: Various APIs and filters are built in for template creation, and custom plugins will be provided later. 27 | - title: Open source 28 | details: QD is an open source project based on the MIT license. 29 | --- -------------------------------------------------------------------------------- /web/docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/docs/public/favicon.ico -------------------------------------------------------------------------------- /web/docs/public/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/docs/public/index.png -------------------------------------------------------------------------------- /web/docs/public/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/docs/public/login.png -------------------------------------------------------------------------------- /web/docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/docs/public/logo.png -------------------------------------------------------------------------------- /web/docs/public/preserve_log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/docs/public/preserve_log.png -------------------------------------------------------------------------------- /web/docs/public/save_har.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/docs/public/save_har.png -------------------------------------------------------------------------------- /web/docs/zh_CN/guide/faq.md: -------------------------------------------------------------------------------- 1 | # 常见问题 2 | 3 | ## 如何备份和恢复数据库? 4 | 5 | QD 默认使用 **sqlite3** 作为框架数据库,`database.db` 文件保存在 `config` 目录下。使用 Docker 容器部署时,可以使用 `docker cp` 命令备份数据库文件,然后使用 `docker cp` 命令在新容器中恢复数据库文件。 6 | 7 | ``` sh 8 | # 数据库备份 9 | docker cp container_name:/usr/src/app/config/database.db . 10 | # 数据库恢复 11 | docker cp database.db container_name:/usr/src/app/config/ 12 | ``` 13 | 14 | ## 如何在 Docker 中配置邮箱服务器? 15 | 16 | ``` sh 17 | docker run -d --name qd -p 8923:80 -v $(pwd)/qd/config:/usr/src/app/config --env MAIL_SMTP=STMP服务器 --env MAIL_PORT=邮箱服务器端口 --env MAIL_USER=用户名 --env MAIL_PASSWORD=密码 --env DOMAIN=域名 qdtoday/qd 18 | ``` 19 | 20 | ## 如何在 Docker 中使用 MySQL? 21 | 22 | ``` sh 23 | docker run -d --name qd -p 8923:80 -v $(pwd)/qd/config:/usr/src/app/config --ENV DB_TYPE=mysql --ENV JAWSDB_MARIA_URL=mysql://用户名:密码@hostname:port/数据库名 qdtoday/qd 24 | ``` 25 | 26 | ## 如何自己搭建 Docker 镜像? 27 | 28 | 请参考此镜像的构建文件 [Dockerfile](https://github.com/qd-today/qd/blob/master/Dockerfile)。 29 | 30 | ## 如何查看当前框架支持的 API 和 Jinja2 模板变量? 31 | 32 | 请进入框架首页,然后点击左上角的 `常用 API/过滤器` 按钮,可以查看当前框架支持的API和Jinja2模板变量。 33 | 34 | ## 如何提交 bug 问题? 35 | 36 | 请在遇到问题后开启 `Debug` 模式,然后将详细的错误日志提交至 [Issue](https://github.com/qd-today/qd/issues)。 37 | 38 | ## QD 模板一般需要哪些请求? 39 | 40 | 根据经验,以下请求是必要的: 41 | 42 | - 登录页面 43 | - 发布到登录页面 44 | - 发起 用户名、密码 请求 45 | - 发送后导致页面跳转的页面 46 | - 翻页前后的第一个网页 47 | 48 | ## 我的用户名和密码会被泄露吗? 49 | 50 | 为了帮助用户发起请求,终究需要记录用户名和密码。这只能靠服务器维护人员的自律来保证后端数据的安全。但在框架设计中,每个用户在存储时都使用安全密钥进行加密。使用密钥对用户数据进行加密,可以保证仅获取数据库就无法解密用户数据。(加密的用户数据包括用户上传的模板、用户为任务设置的变量等) 51 | 52 | 如果还是不放心,可以自己搭建QD框架,下载模板在自己的服务器上运行。 53 | 54 | ## 提示警告信息: `Connect Redis falied: Error 10061` 55 | 56 | QD 使用 `redis` 作为限流工具,如果没有安装 `redis` 服务,框架会提示以下警告信息。 57 | 58 | ``` sh 59 | [W xxxxxx xx:xx:xx QD.RedisDB redisdb:28] Connect Redis falied: Error 10061 connecting to localhost:6379. 由于目标计算机积极拒绝,无法连接。 60 | ``` 61 | 62 | 然而,`redis` 在框架中并不是必须的,如果你不需要使用 `限流` 功能,可以忽略该警告信息。 63 | 64 | > 建议使用 `Docker compose` 部署 QD 框架, Docker compose 配置已默认安装 redis 容器。 65 | 66 | ## 提示警告信息: `Import PyCurl module falied` 67 | 68 | QD 使用 `pycurl` 模块来发送 HTTP Proxy 请求。如果没有安装 `pycurl` 模块,框架会提示以下警告信息。 69 | 70 | ``` sh 71 | [W xxxxxx xx:xx:xx QD.Http.Fetcher fetcher:34] Import PyCurl module falied: No module named 'pycurl' 72 | ``` 73 | 74 | 然而,`pycurl` 在框架中并不是必须的,如果你不需要使用 `Proxy` 功能,可以忽略该警告信息。 75 | 76 | > `pycurl` 模块在 Windows 系统上安装比较麻烦,需要安装 `libcurl` 库,具体安装方法请参考 [pycurl官方文档](http://pycurl.io/docs/latest/install.html)。 77 | > 78 | > 建议使用容器或 linux 系统部署 QD 框架, Docker 容器已预装Curl环境, 默认安装pycurl模组。 79 | 80 | ## 如何注册推送方式 81 | 82 | 你可以在`工具箱`->`推送注册`中注册不同的推送工具,以便在发生特定事件(例如定时任务执行失败)时向你推送通知 83 | 84 | ### TgBot 85 | 86 | 假设你已经创建了一个具有自定义域名的 Telegram bot API: 87 | 88 | `https://tg.mydomain.com/bot1111111111:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/sendMessage?chat_id=222222222&text=HelloWorld` 89 | 90 | 上面这个请求将会向`222222222`这个聊天发送一条`HelloWorld`消息。那么在注册TgBot作为推送方式时: 91 | 92 | - `TG_TOKEN` 应当填写bot的ID以及对应的key的组合,但是不包括`bot`,即申请TgBot时BotFather提供的token:`1111111111:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA` 93 | - `TG_USERID` 应当填写telegram API中的`chat_id`字段,即 `222222222` 94 | - `TG_HOST` 填`tg.mydomain.com`,也可以带上`http://`或者`https://`前缀 95 | 96 | 因此最终填写形式形如: 97 | 98 | `1111111111:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA;222222222;tg.mydomain.com` 99 | 100 | ## 公共模板更新页面提示错误代码为 undefined 101 | 102 | - [issue#423](https://github.com/qd-today/qd/issues/423) 103 | 104 | > 公共模板更新页面提示错误代码为 undefined, 或者控制台显示 WebSocket 连接 failed 但不显示错误原因 105 | 106 | 请检查反向代理相关配置是否正确, 参考 [Nginx反向代理WebSocket服务连接报错](https://blog.csdn.net/tiven_/article/details/126126442) 107 | 108 | > 参考配置如下: 109 | > 110 | > ``` Nginx 111 | > server { 112 | > listen 80; 113 | > # 自行修改 server_name 114 | > server_name qd.example.com; 115 | > location / { 116 | > proxy_pass http://ip:port; 117 | > 118 | > # WebSocket 关键配置 开始 119 | > proxy_http_version 1.1; 120 | > proxy_set_header Upgrade $http_upgrade; 121 | > proxy_set_header Connection "upgrade"; 122 | > # WebSocket 关键配置 结束 123 | > 124 | > # 其他可选配置 开始 125 | > proxy_set_header Host $host; 126 | > proxy_set_header X-Real-IP $remote_addr; 127 | > proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 128 | > proxy_set_header X-Forwarded-Proto $scheme; 129 | > # 其他可选配置 结束 130 | > } 131 | > } 132 | > ``` 133 | -------------------------------------------------------------------------------- /web/docs/zh_CN/guide/how-to-use.md: -------------------------------------------------------------------------------- 1 | # 如何使用? 2 | 3 | ## 获取 HAR 4 | 5 | ### 1.什么是 HAR? 6 | 7 | HAR: 8 | 9 | ### 2.抓包 10 | 11 | #### 2.1. Chrome 或 Edge 12 | 13 | 1. 按 `F12` 、 `Ctrl + Shift + I` ,或从 Chrome 菜单中选择 `更多工具` > `开发者工具` 。 14 | 2. 从屏幕底部或右侧打开的面板中,选择 `网络` 选项卡。 15 | 3. 确保 `网络` 选项卡左上角的 `录制` 按钮显示为 **红色**。 16 | 4. 如果它是灰色的,单击一下开始录制。 17 | 5. 选中 `保留日志` 旁边的框。 18 | ![Preserve log](/preserve_log.png) 19 | 6. 单击 `清除` 按钮以清除 `网络` 选项卡中的所有现有日志。 20 | 7. 现在尝试重现您计划发起的 HTTP 请求。 21 | 8. 重现任务后,右键单击网络请求网格上的任意位置。 22 | 9. 选择 `另存为带内容的 HAR`。 23 | ![Save as HAR with Content](/save_har.png) 24 | 10. 将文件保存到您的计算机。 25 | 26 | #### 2.2. 火狐 27 | 28 | 1. 按 `F12` (或转到 `Tools` > `Web Developer` > `Network` )。 29 | 2. 现在尝试重现您计划发起的 HTTP 请求。 30 | 3. 右键单击​​加载的结果。 31 | 4. 选择全部另存为 har。 32 | 33 | #### 2.3. Windows - Fiddler 34 | 35 | 1. 打开 Fiddler,打开 `Tools` 菜单,选择 `Options`。 36 | 2. 选择 `HTTPS 选项卡` ,勾选 `Capture HTTPS Connects` 。 37 | 3. 选择 `连接` 选项卡,选中 `解密 HTTPS 流量` 。 38 | 4. 现在尝试重现您计划发起的 HTTP 请求。 39 | 5. 导出为 HAR 格式 - 请选择 HTTPArchive 作为导出方式。 40 | 41 | #### 2.4. IOS - Stream 42 | 43 | 1. 打开 Stream。 44 | 2. 抓取HTTPS请求前,需要安装CA证书, `setting` > `General` > `About` > `Certificate Trust Settings` 信任CA证书。 45 | 3. 点击开始抓包按钮,手机会自动弹出VPN配置窗口,然后选择允许。 46 | 4. 现在尝试重现您计划发起的 HTTP 请求。 47 | 5. 在应用页面,点击停止抓拍结束本次抓拍。 48 | 6.导出HAR文件。 49 | 50 | ### 3. 公共模板 51 | 52 | 1. 点击 `我的模板` 右侧的 `社区模板` 按钮。 53 | 2. 更新存储库以获取最新的 HAR 文件列表。 54 | 3. 选择你要使用的HAR文件,点击 `Subscribe` 按钮订阅模板。 55 | 4. 根据需要修改模板。 56 | 5. 然后跳到 [3.编辑模板](#编辑模板) 继续编辑。 57 | 58 | ## 上传 HAR 文件 59 | 60 | 1. 访问并登录 QD 首页。 61 | 2. 点击 `我的模板` 右侧的 `+` 按钮。 62 | 3. 上传刚刚保存的 HAR 文件。 63 | 4. 点击 `上传` 继续。 64 | 65 | ## 编辑模板 66 | 67 | ::: v-pre 68 | 69 | 1. 将用户名、密码、cookie、header 等根据用户变化的部分替换成类似 `{{ username }}` 的模板。(模板支持 **jinja2 语法**) 70 | 2. 在请求编辑中使用测试面板测试模板是否正确返回,使用正确和错误的用户名进行测试。 71 | 3. 填写 `成功/失败断言`,这有助于检测模板任务是否运行失败。 72 | 4. 当请求中需要上一次请求的一些数据时,使用变量抽取,通过正则化抽取数据,保存在环境变量中。使用组选择器 `ab(\w+)cd` 选择其中的一部分。 73 | 5. 使用 `__log__` 提取任务日志。 74 | 6. 当所有请求编辑完成后,使用下载按钮旁边的 `测试` 来测试整体。 75 | 7. 正在编辑的模板会自动保存在浏览器缓存中,不用担心丢失。 76 | 8. 单击 `保存` 按钮保存模板。 77 | 9. 单击 `下载` 按钮下载模板。 78 | ::: 79 | 80 | ## 创建定时任务 81 | 82 | 1. 点击 `我的任务` 右侧的 `+` 按钮。 83 | 2. 选择刚刚创建的模板。 84 | 3. 填写任务变量,任务间隔、任务组和任务执行时间。 85 | 4. 单击 `测试` 按钮测试任务。 86 | 5. 单击保存按钮保存任务。 87 | -------------------------------------------------------------------------------- /web/docs/zh_CN/guide/update.md: -------------------------------------------------------------------------------- 1 | # 更新方法 2 | 3 | > 操作前请一定要记得备份数据库!!! 4 | > 5 | > 更新后请重启容器或清空浏览器缓存。 6 | 7 | ## 源码部署更新 8 | 9 | ``` sh 10 | # 先 cd 到源码所在目录, 执行命令后重启进程 11 | wget https://gitee.com/qd-today/qd/raw/master/update.sh -O ./update.sh && \ 12 | sh ./update.sh 13 | ``` 14 | 15 | ## Docker Compose 部署更新 16 | 17 | ``` sh 18 | # 先 cd 到 docker-compose.yml 所在目录, 执行命令后重启容器 19 | docker compose pull && \ 20 | docker compose up -d 21 | ``` 22 | 23 | ## Docker 容器部署更新 24 | 25 | ``` sh 26 | # 先进入容器后台, 执行命令后重启容器 27 | # docker exec -it 容器名 /bin/sh 28 | wget https://gitee.com/qd-today/qd/raw/master/update.sh -O /usr/src/app/update.sh && \ 29 | sh /usr/src/app/update.sh 30 | ``` 31 | 32 | ## 强制同步最新源码 33 | 34 | ``` sh 35 | # 先 cd 到仓库代码根目录, 执行命令后重启进程 36 | wget https://gitee.com/qd-today/qd/raw/master/update.sh -O ./update.sh && \ 37 | sh ./update.sh -f 38 | ``` 39 | -------------------------------------------------------------------------------- /web/docs/zh_CN/guide/what-is-qd.md: -------------------------------------------------------------------------------- 1 | # 什么是 QD? 2 | 3 | QD 是 一个 基于 HAR 编辑器和 Tornado 服务端的 HTTP 定时任务自动执行 Web 框架。 4 | 5 | 7 | 8 | ## 特性 9 | 10 | - **基于Har**: 仅需上传通过抓包得到的 Har, 即可制作框架所需的 HTTP 任务模板。 11 | - **Tornado 服务端**: 使用 Tornado 作为服务端, 以实现异步响应前端和发起 HTTP 请求。 12 | - **API & 插件支持**: 内置多种 API 和过滤器用于模板制作, 后续将提供自定义插件支持。 13 | - **开源**: QD 是一个基于 MIT 许可证的开源项目。 14 | 15 | ## 如何部署 16 | 17 | 请参考: [部署](deployment) 18 | 19 | ## 如何使用 20 | 21 | 请参考: [如何使用](how-to-use) 22 | 23 | ## 如何更新 24 | 25 | 请参考: [更新](update) 26 | 27 | ## 讨论 28 | 29 | - Github: [问题反馈](https://github.com/qd-today/qd/issues) 30 | - Github: [讨论](https://github.com/qd-today/qd/discussions) 31 | -------------------------------------------------------------------------------- /web/docs/zh_CN/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | title: HTTP定时任务自动执行框架 4 | 5 | hero: 6 | name: QD 7 | text: 一个 HTTP 定时任务自动执行 Web 框架 8 | tagline: "" 9 | image: 10 | src: /logo.png 11 | alt: QD 12 | actions: 13 | - theme: brand 14 | text: 开始了解 15 | link: /zh_CN/guide/what-is-qd 16 | - theme: alt 17 | text: 在 GitHub 中查看 18 | link: https://github.com/qd-today/qd 19 | 20 | features: 21 | - title: 基于Har 22 | details: 仅需上传通过抓包得到的 Har, 即可制作框架所需的 HTTP 任务模板。 23 | - title: Tornado 服务端 24 | details: 使用 Tornado 作为服务端, 以实现异步响应前端请求和发起 HTTP 请求。 25 | - title: API & 插件支持 26 | details: 内置多种 API 和过滤器用于模板制作, 后续将提供自定义插件支持。 27 | - title: 开源项目 28 | details: QD 是一个基于 MIT 许可证的开源项目。 29 | --- -------------------------------------------------------------------------------- /web/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # vim: set et sw=4 ts=4 sts=4 ff=unix fenc=utf8: 4 | # Author: Binux<17175297.hk@gmail.com> 5 | # http://binux.me 6 | # Created on 2012-12-15 16:15:50 7 | 8 | import os 9 | import sys 10 | 11 | sys.path.append(os.path.abspath(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))) 12 | from . import base 13 | 14 | handlers = [] 15 | ui_modules = {} 16 | ui_methods = {} 17 | modules = [] 18 | for file in os.listdir(os.path.dirname(__file__)): 19 | if not file.endswith(".py"): 20 | continue 21 | if file == "__init__.py": 22 | continue 23 | modules.append(file[:-3]) 24 | 25 | for module in modules: 26 | module = __import__('%s.%s' % (__package__, module), fromlist = ["handlers"]) 27 | if hasattr(module, "handlers"): 28 | handlers.extend(module.handlers) 29 | if hasattr(module, "ui_modules"): 30 | ui_modules.update(module.ui_modules) 31 | if hasattr(module, "ui_methods"): 32 | ui_methods.update(module.ui_methods) 33 | -------------------------------------------------------------------------------- /web/handlers/about.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # vim: set et sw=4 ts=4 sts=4 ff=unix fenc=utf8: 4 | # Author: Binux 5 | # http://binux.me 6 | # Created on 2014-08-08 21:06:02 7 | 8 | from .base import * 9 | 10 | 11 | class AboutHandler(BaseHandler): 12 | @tornado.web.addslash 13 | async def get(self): 14 | await self.render('about.html') 15 | return 16 | 17 | handlers = [ 18 | ('/about/?', AboutHandler), 19 | ] 20 | -------------------------------------------------------------------------------- /web/handlers/index.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # vim: set et sw=4 ts=4 sts=4 ff=unix fenc=utf8: 4 | # Author: Binux 5 | # http://binux.me 6 | # Created on 2014-07-30 16:02:08 7 | 8 | import json 9 | 10 | from .base import * 11 | 12 | 13 | class IndexHandlers(BaseHandler): 14 | async def get(self): 15 | if self.current_user: 16 | self.redirect('/my/') 17 | return 18 | 19 | tplid = self.get_argument('tplid', None) 20 | fields = ('id', 'sitename', 'success_count') 21 | tpls = sorted(await self.db.tpl.list(userid=None, fields=fields, limit=None), key=lambda t: -t['success_count']) 22 | if not tpls: 23 | return await self.render('index.html', tpls=[], tplid=0, tpl=None, variables=[]) 24 | 25 | if not tplid: 26 | for tpl in tpls: 27 | if tpl.get('id'): 28 | tplid = tpl['id'] 29 | break 30 | tplid = int(tplid) 31 | tpl = self.check_permission(await self.db.tpl.get(tplid, fields=('id', 'userid', 'sitename', 'siteurl', 'note', 'variables', 'init_env'))) 32 | variables = json.loads(tpl['variables']) 33 | if not tpl['init_env']: 34 | tpl['init_env'] = '{}' 35 | 36 | return await self.render('index.html', tpls=tpls, tplid=tplid, tpl=tpl, variables=variables, init_env=json.loads(tpl['init_env'])) 37 | 38 | handlers = [ 39 | ('/', IndexHandlers), 40 | ] 41 | -------------------------------------------------------------------------------- /web/handlers/my.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # vim: set et sw=4 ts=4 sts=4 ff=unix fenc=utf8: 4 | # Author: Binux 5 | # http://binux.me 6 | # Created on 2014-08-08 21:06:02 7 | 8 | import time 9 | 10 | from .base import * 11 | 12 | 13 | def my_status(task): 14 | if task['disabled']: 15 | return u'停止' 16 | if task['last_failed_count']: 17 | return u'已失败%d次,重试中...' % task['last_failed_count'] 18 | if (task['last_failed'] or 0) > (task['last_success'] or 0): 19 | return u'失败' 20 | if task['success_count'] == 0 and task['failed_count'] == 0 and task['next'] and (task['next'] - time.time() < 60): 21 | return u'正在准备执行任务' 22 | return u'正常' 23 | 24 | class MyHandler(BaseHandler): 25 | @tornado.web.addslash 26 | @tornado.web.authenticated 27 | async def get(self): 28 | user = self.current_user 29 | adminflg = False 30 | # 验证用户是否存在 31 | if (await self.db.user.get(user['id'], fields=('id',))): 32 | if (await self.db.user.get(user['id'], fields=('role',)))['role'] == 'admin': 33 | adminflg = True 34 | 35 | tpls = await self.db.tpl.list(userid=user['id'], fields=('id', 'siteurl', 'sitename', 'banner', 'note', 'disabled', 'lock', 'last_success', 'ctime', 'mtime', 'fork', '_groups', 'updateable', 'tplurl'), limit=None) 36 | 37 | tasks = await self.db.task.list(user['id'], fields=('id', 'tplid', 'note', 'disabled', 'last_success', 'success_count', 'failed_count', 'last_failed', 'next', 'last_failed_count', 'ctime', '_groups'), limit=None) 38 | for task in tasks: 39 | tpl = await self.db.tpl.get(task['tplid'], fields=('id', 'userid', 'sitename', 'siteurl', 'banner', 'note') ) 40 | task['tpl'] = tpl 41 | 42 | _groups = [] 43 | for task in tasks: 44 | if not isinstance(task['_groups'], str): 45 | task['_groups'] = str(task['_groups']) 46 | temp = task['_groups'] 47 | if (temp not in _groups): 48 | _groups.append(temp) 49 | 50 | tplgroups = [] 51 | for tpl in tpls: 52 | temp = tpl['_groups'] 53 | if (temp not in tplgroups): 54 | tplgroups.append(temp) 55 | 56 | await self.render('my.html', tpls=tpls, tasks=tasks, my_status=my_status, userid=user['id'], taskgroups=_groups, tplgroups=tplgroups, adminflg=adminflg) 57 | else: 58 | return self.redirect('/login') 59 | 60 | class CheckUpdateHandler(BaseHandler): 61 | @tornado.web.addslash 62 | @tornado.web.authenticated 63 | async def get(self): 64 | user = self.current_user 65 | async with self.db.transaction() as sql_session: 66 | tpls = await self.db.tpl.list(userid=user['id'], fields=('id', 'mtime', 'tplurl'), limit=None, sql_session=sql_session) 67 | 68 | hjson = {} 69 | for h in await self.db.pubtpl.list(fields=('id', 'filename', 'reponame', 'date', 'update'), sql_session=sql_session): 70 | hjson[f'{h["filename"]}|{h["reponame"]}'] = h 71 | 72 | for tpl in tpls: 73 | if tpl["tplurl"] in hjson and hjson[tpl["tplurl"]]["update"] and tpl['mtime'] < time.mktime(time.strptime(hjson[tpl["tplurl"]]['date'],"%Y-%m-%d %H:%M:%S")): 74 | await self.db.tpl.mod(tpl["id"], updateable=1, sql_session=sql_session) 75 | 76 | self.redirect('/my/') 77 | 78 | handlers = [ 79 | ('/my/?', MyHandler), 80 | ('/my/checkupdate/?', CheckUpdateHandler), 81 | ] 82 | 83 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qd", 3 | "version": "1.0.0", 4 | "description": "QD - 一个 HTTP 请求定时任务自动执行框架 base on HAR Editor and Tornado Server", 5 | "main": "webpack.config.js", 6 | "scripts": { 7 | "docs:dev": "vitepress dev docs", 8 | "docs:build": "vitepress build docs", 9 | "docs:preview": "vitepress preview docs", 10 | "docs:serve": "vitepress serve docs", 11 | "webpack": "cd static/components & webpack --config webpack.config.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/qd-today/qd.git" 16 | }, 17 | "keywords": [ 18 | "QD", 19 | "QD-TODAY", 20 | "har", 21 | "http", 22 | "web" 23 | ], 24 | "author": "a76yyyy", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/qd-today/qd/issues" 28 | }, 29 | "homepage": "https://github.com/qd-today/qd#readme", 30 | "dependencies": { 31 | "curl2har": "^0.0.21", 32 | "qs": "^6.11.2", 33 | "tough-cookie": "^4.1.2", 34 | "url": "github:a76yyyy/node-url#master" 35 | }, 36 | "engines": { 37 | "node": ">= 0.10.0" 38 | }, 39 | "devDependencies": { 40 | "bower": "^1.8.14", 41 | "coffeescript": "^2.7.0", 42 | "grunt": "^1.6.1", 43 | "grunt-bower-task": "^0.6.2", 44 | "grunt-contrib-clean": "^2.0.1", 45 | "grunt-contrib-concat": "^2.1.0", 46 | "grunt-contrib-jshint": "^3.2.0", 47 | "grunt-contrib-uglify": "^5.2.2", 48 | "grunt-contrib-watch": "^1.1.0", 49 | "process": "^0.11.10", 50 | "terser-webpack-plugin": "^5.3.9", 51 | "ts-loader": "^9.4.3", 52 | "tslib": "^2.5.2", 53 | "typescript": "^4.9.5", 54 | "util": "^0.12.5", 55 | "vitepress": "1.0.0-beta.1", 56 | "vue": "^3.3.4", 57 | "webpack": "^5.84.1", 58 | "webpack-cli": "^5.1.1" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /web/static/coffee/har/contenteditable.coffee: -------------------------------------------------------------------------------- 1 | # vim: set et sw=2 ts=2 sts=2 ff=unix fenc=utf8: 2 | # Author: Binux 3 | # http://binux.me 4 | # Created on 2014-08-04 19:34:02 5 | 6 | define (require, exports, module) -> 7 | angular.module('contenteditable', []) 8 | .directive('contenteditable', ['$timeout', ($timeout) -> { 9 | restrict: 'A' 10 | require: '?ngModel' 11 | link: (scope, element, attrs, ngModel) -> 12 | if !ngModel 13 | return 14 | 15 | element.bind('input', (e) -> 16 | scope.$apply -> 17 | text = element.text() 18 | ngModel.$setViewValue(text) 19 | if text == '' 20 | $timeout -> 21 | if element.prev().hasClass('contentedit-wrapper') 22 | element.prev().click() 23 | else 24 | element[0].blur() 25 | element[0].focus() 26 | ) 27 | element.bind('blur', (e) -> 28 | text = element.text() 29 | if ngModel.$viewValue != text 30 | ngModel.$render() 31 | ) 32 | 33 | oldRender = ngModel.$render 34 | ngModel.$render = -> 35 | if !!oldRender 36 | oldRender() 37 | if not element.is(':focus') 38 | element.text(ngModel.$viewValue || '') 39 | }]) 40 | -------------------------------------------------------------------------------- /web/static/coffee/har/editablelist.coffee: -------------------------------------------------------------------------------- 1 | # vim: set et sw=2 ts=2 sts=2 ff=unix fenc=utf8: 2 | # Author: Binux 3 | # http://binux.me 4 | # Created on 2014-08-05 14:47:55 5 | 6 | define (require, exports, module) -> 7 | angular.module('editablelist', []) 8 | .directive('editablelist', () -> { 9 | restrict: 'A' 10 | scope: true 11 | templateUrl: '/static/har/editablelist.html' 12 | link: ($scope, $element, $attr, ctrl, $transclude) -> 13 | $scope.$filter = $scope.$eval($attr.filter) 14 | $scope.$watch($attr.editablelist, (value) -> 15 | $scope.$list = value 16 | ) 17 | }) 18 | -------------------------------------------------------------------------------- /web/static/coffee/har/editor.coffee: -------------------------------------------------------------------------------- 1 | # vim: set et sw=2 ts=2 sts=2 ff=unix fenc=utf8: 2 | # Author: Binux 3 | # http://binux.me 4 | # Created on 2014-08-01 11:02:45 5 | 6 | define (require, exports, module) -> 7 | require '/static/har/contenteditable' 8 | require '/static/har/upload_ctrl' 9 | require '/static/har/entry_list' 10 | require '/static/har/entry_editor' 11 | 12 | # contentedit-wrapper 13 | $(document).on('click', '.contentedit-wrapper', (ev) -> 14 | editable = $(this).hide().next('[contenteditable]').show().focus() 15 | ) 16 | $(document).on('blur', '.contentedit-wrapper + [contenteditable]', (ev) -> 17 | $(this).hide().prev('.contentedit-wrapper').show() 18 | ) 19 | $(document).on('focus', '[contenteditable]', (ev) -> 20 | if this.childNodes[0] 21 | range = document.createRange() 22 | sel = window.getSelection() 23 | range.setStartBefore(this.childNodes[0]) 24 | range.setEndAfter(this) 25 | sel.removeAllRanges() 26 | sel.addRange(range) 27 | ) 28 | 29 | # $(() -> 30 | # if $('body').attr('get-cookie') == 'true' 31 | # $('[data-toggle=get-cookie][disabled]').attr('disabled', false) 32 | # return 33 | # ) 34 | 35 | # get cookie helper 36 | cookie_input = null 37 | $(document).on('click', "[data-toggle=get-cookie]", (ev) -> 38 | $this = $(this) 39 | # if $this.attr('disabled') 40 | # return 41 | cookie_input = angular.element($this.parent().find('input')) 42 | 43 | if $('body').attr('get-cookie') != 'true' 44 | # $this.html('没有插件,详情F12') 45 | # console.log('如需要插件请访问 https://github.com/qd-today/get-cookies/ 下载并手动安装插件') 46 | if $this.attr('getmod') == 1 47 | $this.attr('href', 'https://github.com/qd-today/get-cookies/') 48 | .attr('target', '_blank').html('安装插件后请刷新') 49 | else 50 | $this.attr('getmod', 1) 51 | .html('再次点击前往安装 Get-Cookies 插件') 52 | return 53 | ) 54 | # deepcode ignore InsufficientPostmessageValidation: the event.origin is checked 55 | window.addEventListener("message", (ev) -> 56 | if event.origin != window.location.origin 57 | return 58 | 59 | cookie = ev.data 60 | cookie_str = "" 61 | for key, value of cookie 62 | cookie_str += key + '=' + value + '; ' 63 | if cookie_str == '' 64 | console.log('没有获得cookie, 您是否已经登录?') 65 | return 66 | cookie_input?.val(cookie_str) 67 | cookie_input?.scope().$parent.var.value = cookie_str 68 | ) 69 | 70 | editor = angular.module('HAREditor', [ 71 | 'editablelist' 72 | 'upload_ctrl' 73 | 'entry_list' 74 | 'entry_editor' 75 | ]) 76 | 77 | init: -> angular.bootstrap(document.body, ['HAREditor']) 78 | -------------------------------------------------------------------------------- /web/static/components/Font-Awesome/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/components/Font-Awesome/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /web/static/components/Font-Awesome/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/components/Font-Awesome/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /web/static/components/Font-Awesome/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/components/Font-Awesome/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /web/static/components/Font-Awesome/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/components/Font-Awesome/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /web/static/components/Font-Awesome/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/components/Font-Awesome/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /web/static/components/Font-Awesome/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/components/Font-Awesome/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /web/static/components/Font-Awesome/webfonts/fa-v4compatibility.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/components/Font-Awesome/webfonts/fa-v4compatibility.ttf -------------------------------------------------------------------------------- /web/static/components/Font-Awesome/webfonts/fa-v4compatibility.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/components/Font-Awesome/webfonts/fa-v4compatibility.woff2 -------------------------------------------------------------------------------- /web/static/components/bootstrap/dist/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/components/bootstrap/dist/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /web/static/components/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/components/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /web/static/components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /web/static/components/download.js: -------------------------------------------------------------------------------- 1 | function AjaxPosSaveFile(response, state, xhr) { 2 | //result:请求到的结果数据 3 | //state:请求状态(success) 4 | //xhr:XMLHttpRequest对象 5 | 6 | // 从Response Headers中获取fileName 7 | let fileName = xhr.getResponseHeader('Content-Disposition').split(';')[1].split('=')[1].replace(/\"/g, '') 8 | //获取下载文件的类型 9 | let type = xhr.getResponseHeader("content-type") 10 | //结果数据类型处理 11 | let blob = new Blob([response], { type: type }) 12 | 13 | //对于标签,只有 Firefox 和 Chrome(内核)支持 download 属性 14 | //IE10以上支持blob,但是依然不支持download 15 | if ('download' in document.createElement('a')) {//支持a标签download的浏览器 16 | //通过创建a标签实现 17 | let link = document.createElement("a"); 18 | //文件名 19 | link.download = fileName; 20 | link.style.display = "none" 21 | link.href = URL.createObjectURL(blob); 22 | document.body.appendChild(link); 23 | link.click();//执行下载 24 | URL.revokeObjectURL(link.href);//释放url 25 | document.body.removeChild(link);//释放标签 26 | } else {//不支持 27 | if (window.navigator.msSaveOrOpenBlob) { 28 | window.navigator.msSaveOrOpenBlob(blob, fileName) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /web/static/components/editor_js.js: -------------------------------------------------------------------------------- 1 | function reserve_check() { 2 | document.querySelectorAll('#droplist>a.list-group-item.entry').forEach(function(el){ 3 | var tmp = el.getElementsByClassName('entry-checked')[0].getElementsByTagName('input')[0] 4 | tmp.checked = !tmp.checked 5 | }); 6 | entries = window.global_har.har.log.entries 7 | for (i = 0; i < entries.length;i++) 8 | { 9 | entries[i].checked = !entries[i].checked 10 | } 11 | } -------------------------------------------------------------------------------- /web/static/components/groups_op.js: -------------------------------------------------------------------------------- 1 | function checkboxOnclick(checkbox){ 2 | var tasknodes = document.getElementsByClassName(checkbox.name); 3 | for (i = 0; i < tasknodes.length; i++) { 4 | if (checkbox.checked == true){ 5 | $(tasknodes[i]).hide(); 6 | window.localStorage.Groups = "1" 7 | }else{ 8 | $(tasknodes[i]).show(); 9 | } 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /web/static/components/jquery.tablesorter/css/dragtable.mod.css: -------------------------------------------------------------------------------- 1 | /* 2 | * dragtable 3 | * @Version 2.0.14 MOD 4 | * default css 5 | */ 6 | .dragtable-sortable { 7 | list-style-type: none; 8 | margin: 0; 9 | padding: 0; 10 | -moz-user-select: none; 11 | z-index: 10; 12 | } 13 | .dragtable-sortable li { 14 | margin: 0; 15 | padding: 0; 16 | float: left; 17 | font-size: 1em; 18 | } 19 | .dragtable-sortable table { 20 | margin-top: 0; 21 | } 22 | .dragtable-sortable th, .dragtable-sortable td { 23 | border-left: 0px; 24 | } 25 | .dragtable-sortable li:first-child th, .dragtable-sortable li:first-child td { 26 | border-left: 1px solid #CCC; 27 | } 28 | .dragtable-handle-selected { 29 | /* table-handle class while actively dragging a column */ 30 | } 31 | .ui-sortable-helper { 32 | opacity: 0.7; 33 | filter: alpha(opacity=70); 34 | } 35 | .ui-sortable-placeholder { 36 | -moz-box-shadow: 4px 5px 4px rgba(0,0,0,0.2) inset; 37 | -webkit-box-shadow: 4px 5px 4px rgba(0,0,0,0.2) inset; 38 | box-shadow: 4px 5px 4px rgba(0,0,0,0.2) inset; 39 | border-bottom: 1px solid rgba(0,0,0,0.2); 40 | border-top: 1px solid rgba(0,0,0,0.2); 41 | visibility: visible !important; 42 | /* change the background color here to match the tablesorter theme */ 43 | background: #EFEFEF; 44 | } 45 | .ui-sortable-placeholder * { 46 | opacity: 0.0; 47 | visibility: hidden; 48 | } 49 | .table-handle, .table-handle-disabled { 50 | /* background-image: url(images/dragtable-handle.png); */ 51 | /* background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAANAQMAAAC5Li2yAAAABlBMVEUAAAAzMzPI8eYgAAAAAnRSTlMAzORBQ6MAAAAOSURBVAjXYwABByyYAQAQWgFBLN2RnwAAAABJRU5ErkJggg=='); */ 52 | background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyIiBoZWlnaHQ9IjEzIj48cmVjdCBzdHlsZT0iZmlsbDojMzMzO2ZpbGwtb3BhY2l0eTouODsiIHdpZHRoPSIxIiBoZWlnaHQ9IjEiIHg9IjEiIHk9IjIiLz4JPHJlY3Qgc3R5bGU9ImZpbGw6IzMzMztmaWxsLW9wYWNpdHk6Ljg7IiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4PSIxIiB5PSI0Ii8+CTxyZWN0IHN0eWxlPSJmaWxsOiMzMzM7ZmlsbC1vcGFjaXR5Oi44OyIgd2lkdGg9IjEiIGhlaWdodD0iMSIgeD0iMSIgeT0iNiIvPjxyZWN0IHN0eWxlPSJmaWxsOiMzMzM7ZmlsbC1vcGFjaXR5Oi44OyIgd2lkdGg9IjEiIGhlaWdodD0iMSIgeD0iMSIgeT0iOCIvPjxyZWN0IHN0eWxlPSJmaWxsOiMzMzM7ZmlsbC1vcGFjaXR5Oi44OyIgd2lkdGg9IjEiIGhlaWdodD0iMSIgeD0iMSIgeT0iMTAiLz48L3N2Zz4=); 53 | background-repeat: repeat-x; 54 | height: 13px; 55 | margin: 0 1px; 56 | cursor: move; 57 | } 58 | .table-handle-disabled { 59 | opacity: 0; 60 | cursor: not-allowed; 61 | } 62 | .dragtable-sortable table { 63 | margin-bottom: 0; 64 | } 65 | -------------------------------------------------------------------------------- /web/static/components/jquery.tablesorter/css/highlights.css: -------------------------------------------------------------------------------- 1 | /* CSS Column & Row Hightlights - matches the blue theme 2 | * See https://mottie.github.io/tablesorter/docs/example-css-highlighting.html 3 | */ 4 | table.hover-highlight td:before, 5 | table.focus-highlight td:before { 6 | background: #fff; 7 | } 8 | 9 | /* ODD ZEBRA STRIPE color (needs zebra widget) */ 10 | .hover-highlight .odd td:before, .hover-highlight .odd th:before, 11 | .focus-highlight .odd td:before, .focus-highlight .odd th:before { 12 | background: #ebf2fa; 13 | } 14 | /* EVEN ZEBRA STRIPE color (needs zebra widget) */ 15 | .hover-highlight .even td:before, .hover-highlight .even th:before, 16 | .focus-highlight .even td:before, .focus-highlight .even th:before { 17 | background-color: #fff; 18 | } 19 | 20 | /* FOCUS ROW highlight color (touch devices) */ 21 | .focus-highlight td:focus::before, .focus-highlight th:focus::before { 22 | background-color: lightblue; 23 | } 24 | /* FOCUS COLUMN highlight color (touch devices) */ 25 | .focus-highlight td:focus::after, .focus-highlight th:focus::after { 26 | background-color: lightblue; 27 | } 28 | /* FOCUS CELL highlight color */ 29 | .focus-highlight th:focus, .focus-highlight td:focus, 30 | .focus-highlight .even th:focus, .focus-highlight .even td:focus, 31 | .focus-highlight .odd th:focus, .focus-highlight .odd td:focus { 32 | background-color: #d9d9d9; 33 | color: #333; 34 | } 35 | 36 | /* HOVER ROW highlight colors */ 37 | table.hover-highlight tbody > tr:hover > td, /* override tablesorter theme row hover */ 38 | table.hover-highlight tbody > tr.odd:hover > td, 39 | table.hover-highlight tbody > tr.even:hover > td { 40 | background-color: #ffa; 41 | } 42 | /* HOVER COLUMN highlight colors */ 43 | .hover-highlight tbody tr td:hover::after, 44 | .hover-highlight tbody tr th:hover::after { 45 | background-color: #ffa; 46 | } 47 | 48 | /* ************************************************* */ 49 | /* **** No need to modify the definitions below **** */ 50 | /* ************************************************* */ 51 | .focus-highlight td:focus::after, .focus-highlight th:focus::after, 52 | .hover-highlight td:hover::after, .hover-highlight th:hover::after { 53 | content: ''; 54 | position: absolute; 55 | width: 100%; 56 | height: 999em; 57 | left: 0; 58 | top: -555em; 59 | z-index: -1; 60 | } 61 | .focus-highlight td:focus::before, .focus-highlight th:focus::before { 62 | content: ''; 63 | position: absolute; 64 | width: 999em; 65 | height: 100%; 66 | left: -555em; 67 | top: 0; 68 | z-index: -2; 69 | } 70 | /* required styles */ 71 | .hover-highlight, 72 | .focus-highlight { 73 | overflow: hidden; 74 | } 75 | .hover-highlight td, .hover-highlight th, 76 | .focus-highlight td, .focus-highlight th { 77 | position: relative; 78 | outline: 0; 79 | } 80 | /* override the tablesorter theme styling */ 81 | table.hover-highlight, table.hover-highlight tbody > tr > td, 82 | table.focus-highlight, table.focus-highlight tbody > tr > td, 83 | /* override zebra styling */ 84 | table.hover-highlight tbody tr.even > th, 85 | table.hover-highlight tbody tr.even > td, 86 | table.hover-highlight tbody tr.odd > th, 87 | table.hover-highlight tbody tr.odd > td, 88 | table.focus-highlight tbody tr.even > th, 89 | table.focus-highlight tbody tr.even > td, 90 | table.focus-highlight tbody tr.odd > th, 91 | table.focus-highlight tbody tr.odd > td { 92 | background: transparent; 93 | } 94 | /* table background positioned under the highlight */ 95 | table.hover-highlight td:before, 96 | table.focus-highlight td:before { 97 | content: ''; 98 | position: absolute; 99 | width: 100%; 100 | height: 100%; 101 | left: 0; 102 | top: 0; 103 | z-index: -3; 104 | } -------------------------------------------------------------------------------- /web/static/components/jquery.tablesorter/css/widget.grouping.css: -------------------------------------------------------------------------------- 1 | /* Grouping widget css */ 2 | tr.group-header td { 3 | background: #eee; 4 | } 5 | .group-name { 6 | text-transform: uppercase; 7 | font-weight: bold; 8 | } 9 | .group-count { 10 | color: #999; 11 | } 12 | .group-hidden { 13 | display: none !important; 14 | } 15 | .group-header, .group-header td { 16 | user-select: none; 17 | -moz-user-select: none; 18 | } 19 | /* collapsed arrow */ 20 | tr.group-header td i { 21 | display: inline-block; 22 | width: 0; 23 | height: 0; 24 | border-top: 4px solid transparent; 25 | border-bottom: 4px solid #888; 26 | border-right: 4px solid #888; 27 | border-left: 4px solid transparent; 28 | margin-right: 7px; 29 | user-select: none; 30 | -moz-user-select: none; 31 | } 32 | tr.group-header.collapsed td i { 33 | border-top: 5px solid transparent; 34 | border-bottom: 5px solid transparent; 35 | border-left: 5px solid #888; 36 | border-right: 0; 37 | margin-right: 10px; 38 | } 39 | -------------------------------------------------------------------------------- /web/static/components/misc.js: -------------------------------------------------------------------------------- 1 | function refresh_modal_load(url) { 2 | // 管理用户 3 | let a = document.createElement("a"); 4 | document.body.appendChild(a); 5 | a.style = "display: none"; 6 | a.setAttribute('data-load-method', 'GET'); 7 | a.setAttribute('class', 'modal_load'); 8 | a.setAttribute('href', url); 9 | a.click(); 10 | document.body.removeChild(a); 11 | } 12 | -------------------------------------------------------------------------------- /web/static/components/mutil_op.js: -------------------------------------------------------------------------------- 1 | function del_confirm() { 2 | $('#multi_op-result').html('').show(); 7 | } 8 | 9 | function Null_info() { 10 | $('#multi_op-result').html('').show(); 14 | } 15 | 16 | function Get_Groups() { 17 | var str1 = '\ 22 |
' 23 | var str2 = '
\ 30 | ' 31 | var Indexs = {}; 32 | var es = document.querySelectorAll('tr[class*=taskgroup]'); 33 | var tmp = "" 34 | es.forEach(function(entry){ 35 | var tmp = entry.getElementsByTagName('td')[0].getElementsByTagName('input')[0] 36 | if (tmp.checked){ 37 | Indexs[tmp.name] = '' 38 | } 39 | }); 40 | if (Object.keys(Indexs).length > 0){ 41 | var groupurl = '/getgroups/'+ Object.keys(Indexs)[0] 42 | $.get(groupurl, function(result){ 43 | tmp = JSON.parse(result) 44 | var groupinfo = "" 45 | Object.keys(tmp).forEach(function(key){ 46 | groupinfo = groupinfo + str2 + key + '">' + key + str3; 47 | }); 48 | $("#multi_op-result").html(str1 + groupinfo + str4).show(); 49 | OnlyCheckOne() 50 | }); 51 | } 52 | else{ 53 | Null_info() 54 | } 55 | return false; 56 | } 57 | 58 | function goto_my () { 59 | window.location.replace("/my/") 60 | } 61 | 62 | function OnlyCheckOne() { 63 | var fanxiBox = $("#oneCheck input:checkbox"); 64 | fanxiBox.click(function () { 65 | if(this.checked || this.checked=='checked'){ 66 | fanxiBox.removeAttr("checked"); 67 | $(this).prop("checked", true); 68 | } 69 | }); 70 | }; 71 | 72 | function Del_tasks () { 73 | var Indexs = {}; 74 | var es = document.querySelectorAll('tr[class*=taskgroup]'); 75 | es.forEach(function(entry){ 76 | var tmp = entry.getElementsByTagName('td')[0].getElementsByTagName('input')[0] 77 | if (tmp.checked){ 78 | Indexs[tmp.name] = '' 79 | } 80 | }); 81 | var $this = $(this); 82 | var data = {taskids: JSON.stringify(Indexs), func: "Del"} 83 | $.ajax('/tasks/{{ userid }}', { 84 | type: 'POST', 85 | data: data, 86 | }) 87 | .done(function(data) { 88 | goto_my() 89 | }) 90 | .fail(function(jxhr) { 91 | $("del_confirm-result").html('

失败

').show().find('div').text(jxhr.responseText); 92 | }) 93 | .always(function() { 94 | $this.button('reset');}); 95 | 96 | return false; 97 | } 98 | 99 | function setTasksGroup() { 100 | var Indexs = {}; 101 | var es = document.querySelectorAll('tr[class*=taskgroup]'); 102 | var tmp = "" 103 | var groupValue = '' 104 | es.forEach(function(entry){ 105 | var tmp = entry.getElementsByTagName('td')[0].getElementsByTagName('input')[0] 106 | if (tmp.checked){ 107 | Indexs[tmp.name] = '' 108 | } 109 | }); 110 | 111 | tmp = document.querySelectorAll('#NewGroupValue')[0] 112 | if(tmp.value == ''){ 113 | es = document.querySelectorAll('#oneCheck input[type=checkbox]'); 114 | es.forEach(function (e){ 115 | if (e.checked) { 116 | groupValue = e.name; 117 | } 118 | }); 119 | } 120 | else { 121 | groupValue = tmp.value 122 | } 123 | var $this = $(this); 124 | var data = {taskids: JSON.stringify(Indexs), func: "setGroup", groupValue: groupValue} 125 | $.ajax('/tasks/{{ userid }}', { 126 | type: 'POST', 127 | data: data, 128 | }) 129 | .done(function(data) { 130 | window.location.replace("/my/") 131 | }) 132 | .fail(function(jxhr) { 133 | $('#run-result').html('

失败

').show().find('div').text(jxhr.responseText); 134 | }) 135 | .always(function() { 136 | $this.button('reset');}); 137 | 138 | return false; 139 | } -------------------------------------------------------------------------------- /web/static/components/node_components.tpl.js: -------------------------------------------------------------------------------- 1 | window.node_url = require('url') 2 | window.node_tough = require('tough-cookie') 3 | window.node_querystring = require('qs') 4 | window.node_curl2har = require('curl2har') 5 | -------------------------------------------------------------------------------- /web/static/components/nprogress/nprogress.css: -------------------------------------------------------------------------------- 1 | /* Make clicks pass-through */ 2 | #nprogress { 3 | pointer-events: none; 4 | } 5 | 6 | #nprogress .bar { 7 | background: #29d; 8 | 9 | position: fixed; 10 | z-index: 1031; 11 | top: 0; 12 | left: 0; 13 | 14 | width: 100%; 15 | height: 2px; 16 | } 17 | 18 | /* Fancy blur effect */ 19 | #nprogress .peg { 20 | display: block; 21 | position: absolute; 22 | right: 0px; 23 | width: 100px; 24 | height: 100%; 25 | box-shadow: 0 0 10px #29d, 0 0 5px #29d; 26 | opacity: 1.0; 27 | 28 | -webkit-transform: rotate(3deg) translate(0px, -4px); 29 | -ms-transform: rotate(3deg) translate(0px, -4px); 30 | transform: rotate(3deg) translate(0px, -4px); 31 | } 32 | 33 | /* Remove these to get rid of the spinner */ 34 | #nprogress .spinner { 35 | display: block; 36 | position: fixed; 37 | z-index: 1031; 38 | top: 15px; 39 | right: 15px; 40 | } 41 | 42 | #nprogress .spinner-icon { 43 | width: 18px; 44 | height: 18px; 45 | box-sizing: border-box; 46 | 47 | border: solid 2px transparent; 48 | border-top-color: #29d; 49 | border-left-color: #29d; 50 | border-radius: 50%; 51 | 52 | -webkit-animation: nprogress-spinner 400ms linear infinite; 53 | animation: nprogress-spinner 400ms linear infinite; 54 | } 55 | 56 | .nprogress-custom-parent { 57 | overflow: hidden; 58 | position: relative; 59 | } 60 | 61 | .nprogress-custom-parent #nprogress .spinner, 62 | .nprogress-custom-parent #nprogress .bar { 63 | position: absolute; 64 | } 65 | 66 | @-webkit-keyframes nprogress-spinner { 67 | 0% { -webkit-transform: rotate(0deg); } 68 | 100% { -webkit-transform: rotate(360deg); } 69 | } 70 | @keyframes nprogress-spinner { 71 | 0% { transform: rotate(0deg); } 72 | 100% { transform: rotate(360deg); } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /web/static/components/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack')// webpack needs to be explicitly required 3 | const TerserPlugin = require("terser-webpack-plugin"); 4 | 5 | module.exports = { 6 | mode: 'production', //开发模式development 下打包的bundle不压缩 ,生产环境下打包改为: production 7 | // 要打程序的入口文件 8 | entry: { 9 | node_components:'./node_components.tpl.js' 10 | }, 11 | // 输出配置 12 | output: { 13 | filename: '[name].js', //输出文件名 14 | path: path.resolve(__dirname) //输出文件夹 15 | }, 16 | plugins: [ 17 | // fix "process is not defined" error: 18 | // (do "npm install process" before running the build) 19 | new webpack.ProvidePlugin({ 20 | process: 'process/browser', 21 | }), 22 | ], 23 | optimization: { 24 | minimize: true, 25 | minimizer: [ 26 | new TerserPlugin({ 27 | // extractComments: false,//不将注释提取到单独的文件中 28 | }), 29 | ], 30 | }, 31 | performance: { 32 | hints:false 33 | } 34 | } -------------------------------------------------------------------------------- /web/static/css/base.css: -------------------------------------------------------------------------------- 1 | /* vim: set et sw=2 ts=2 sts=2 ff=unix fenc=utf8: */ 2 | /* Author: Binux */ 3 | /* http://binux.me */ 4 | /* Created on 2014-08-08 23:02:39 */ 5 | html { 6 | position: relative; 7 | min-height: 95%; 8 | } 9 | body { 10 | margin-bottom: 60px; 11 | background: url(../img/body.jpg) repeat-x #FFF;/* 修改-新加-头部图片 */ 12 | } 13 | footer { 14 | position: absolute; 15 | height: 30px; 16 | width: 100%; 17 | bottom: 0; 18 | margin: 10px 0; 19 | } 20 | h1 > a { 21 | color: black; 22 | text-decoration: none; 23 | } 24 | h1 > a:hover, 25 | h1 > a:focus { 26 | color: black; 27 | text-decoration: none; 28 | } 29 | .form-group { 30 | position: relative; 31 | } 32 | .form-group .control-label .control-helper { 33 | position: absolute; 34 | top: 0; 35 | right: 0; 36 | } 37 | .form-group .control-label .control-helper[disabled] { 38 | color: darkgray; 39 | text-decoration: none; 40 | cursor: default; 41 | } 42 | 43 | .autowrap { 44 | word-wrap: break-word; 45 | word-break: break-all; 46 | } 47 | 48 | .showbut { 49 | position: relative; 50 | } 51 | 52 | .showbut:hover .hljs-button { 53 | display: block 54 | } 55 | 56 | .hljs-button { 57 | display: none; 58 | position: absolute; 59 | right: 4px; 60 | top: 4px; 61 | font-size: 12px; 62 | color: #4d4d4d; 63 | background-color: white; 64 | padding: 2px 8px; 65 | /* margin: 8px; */ 66 | border-radius: 4px; 67 | cursor: pointer; 68 | box-shadow: 0 2px 4px rgba(0,0,0,0.2), 0 2px 4px rgba(0,0,0,0.2); 69 | } 70 | 71 | th,.apitable td{ 72 | text-align: center; 73 | vertical-align:middle !important; 74 | } 75 | 76 | .apitable td div{ 77 | max-width: 400px; 78 | max-height: 80px; 79 | overflow: hidden; 80 | text-overflow: ellipsis; 81 | } 82 | -------------------------------------------------------------------------------- /web/static/css/index.css: -------------------------------------------------------------------------------- 1 | /* vim: set et sw=2 ts=2 sts=2 ff=unix fenc=utf8: */ 2 | /* Author: Binux */ 3 | /* http://binux.me */ 4 | /* Created on 2014-08-09 23:10:58 */ 5 | .login { 6 | position: absolute; 7 | right: 0; 8 | top: 0; 9 | margin: 20px; 10 | } 11 | form { 12 | width: 300px; 13 | padding-top: 40px; 14 | margin: 0 auto; 15 | } 16 | form h1 { 17 | text-align: center; 18 | font-size: 52px; 19 | margin-bottom: 30px; 20 | } 21 | form h1 sup { 22 | font-size: 50%; 23 | top: -1em; 24 | } 25 | form div.text-right { 26 | margin-top: 30px; 27 | } 28 | -------------------------------------------------------------------------------- /web/static/css/less/base.less: -------------------------------------------------------------------------------- 1 | /* vim: set et sw=2 ts=2 sts=2 ff=unix fenc=utf8: */ 2 | /* Author: Binux */ 3 | /* http://binux.me */ 4 | /* Created on 2014-08-08 23:02:39 */ 5 | 6 | html { 7 | position: relative; 8 | min-height: 100%; 9 | } 10 | 11 | body { 12 | margin-bottom: 60px; 13 | } 14 | 15 | footer { 16 | position: absolute; 17 | height: 30px; 18 | width: 100%; 19 | bottom: 0; 20 | margin: 10px 0; 21 | } 22 | 23 | h1 > a { 24 | color: black; 25 | text-decoration: none; 26 | &:hover, &:focus { 27 | color: black; 28 | text-decoration: none; 29 | } 30 | } 31 | 32 | .form-group { 33 | position: relative; 34 | .control-label .control-helper { 35 | position: absolute; 36 | top: 0; 37 | right: 0; 38 | 39 | &[disabled] { 40 | color: darkgray; 41 | text-decoration: none; 42 | cursor: default; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /web/static/css/less/index.less: -------------------------------------------------------------------------------- 1 | /* vim: set et sw=2 ts=2 sts=2 ff=unix fenc=utf8: */ 2 | /* Author: Binux */ 3 | /* http://binux.me */ 4 | /* Created on 2014-08-09 23:10:58 */ 5 | 6 | .login { 7 | position: absolute; 8 | right: 0; 9 | top: 0; 10 | margin: 20px; 11 | } 12 | 13 | form { 14 | width: 300px; 15 | padding-top: 40px; 16 | margin: 0 auto; 17 | 18 | h1 { 19 | text-align: center; 20 | font-size: 52px; 21 | margin-bottom: 30px; 22 | 23 | sup { 24 | font-size: 50%; 25 | top: -1em 26 | } 27 | } 28 | 29 | div.text-right { 30 | margin-top: 30px; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /web/static/css/less/my.less: -------------------------------------------------------------------------------- 1 | /* vim: set et sw=2 ts=2 sts=2 ff=unix fenc=utf8: */ 2 | /* Author: Binux */ 3 | /* http://binux.me */ 4 | /* Created on 2014-08-08 22:53:48 */ 5 | 6 | @import "task_new.less"; 7 | 8 | header { 9 | .container { 10 | position: relative; 11 | border-bottom: 1px solid #eee; 12 | } 13 | 14 | .buttons { 15 | position: absolute; 16 | margin-bottom: 10px; 17 | left: -10px; 18 | right: -10px; 19 | bottom: 0; 20 | 21 | a { 22 | margin: 0 10px; 23 | } 24 | } 25 | } 26 | 27 | section { 28 | h2 a.btn { 29 | margin-top: -.2em 30 | } 31 | } 32 | 33 | [data-toggle=popover] { 34 | cursor: pointer; 35 | } 36 | -------------------------------------------------------------------------------- /web/static/css/less/register.less: -------------------------------------------------------------------------------- 1 | /* vim: set et sw=2 ts=2 sts=2 ff=unix fenc=utf8: */ 2 | /* Author: Binux */ 3 | /* http://binux.me */ 4 | /* Created on 2014-08-08 21:42:53 */ 5 | 6 | form { 7 | width: 300px; 8 | padding-top: 40px; 9 | margin: 0 auto; 10 | 11 | @media(min-height: 700px) { 12 | padding-top: 60px; 13 | } 14 | 15 | h1 { 16 | text-align: center; 17 | font-size: 52px; 18 | margin-bottom: 30px; 19 | 20 | sup { 21 | font-size: 50%; 22 | top: -1em 23 | } 24 | } 25 | 26 | button[type=submit] { 27 | margin-left: 10px; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /web/static/css/less/task_new.less: -------------------------------------------------------------------------------- 1 | /* vim: set et sw=2 ts=2 sts=2 ff=unix fenc=utf8: */ 2 | /* Author: Binux */ 3 | /* http://binux.me */ 4 | /* Created on 2014-08-09 11:44:44 */ 5 | 6 | form { 7 | padding: 0 20px 20px 20px; 8 | 9 | button { 10 | margin-left: 10px; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /web/static/css/my.css: -------------------------------------------------------------------------------- 1 | /* vim: set et sw=2 ts=2 sts=2 ff=unix fenc=utf8: */ 2 | /* Author: Binux */ 3 | /* http://binux.me */ 4 | /* Created on 2014-08-08 22:53:48 */ 5 | /* vim: set et sw=2 ts=2 sts=2 ff=unix fenc=utf8: */ 6 | /* Author: Binux */ 7 | /* http://binux.me */ 8 | /* Created on 2014-08-09 11:44:44 */ 9 | form { 10 | padding: 0 20px 20px 20px; 11 | } 12 | form button { 13 | margin-left: 10px; 14 | } 15 | 16 | header .container { 17 | position: relative; 18 | /* 修改-顶部下划线border-bottom: 1px solid #eee */ 19 | /* 修改-新加-下划线阴影-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05) */ 20 | /* 修改-新加-下划线阴影 box-shadow:0 1px 1px rgba(0,0,0,.05); */ 21 | background: url(../img/header.jpg) repeat-x #FFF;/* 修改-新加-头部图片 */ 22 | background-repeat: no-repeat;/* 修改-新加-头部图片 */ 23 | /* background-size:100% 100%; */ 24 | } 25 | header .buttons { 26 | position: absolute; 27 | margin-bottom: 10px; 28 | left: -10px; 29 | right: -10px; 30 | bottom: 0; 31 | } 32 | header .buttons a { 33 | margin: 0 10px; 34 | } 35 | section h2 a.btn { 36 | margin-top: -0.2em; 37 | } 38 | [data-toggle=popover] { 39 | cursor: pointer; 40 | } 41 | -------------------------------------------------------------------------------- /web/static/css/register.css: -------------------------------------------------------------------------------- 1 | /* vim: set et sw=2 ts=2 sts=2 ff=unix fenc=utf8: */ 2 | /* Author: Binux */ 3 | /* http://binux.me */ 4 | /* Created on 2014-08-08 21:42:53 */ 5 | form { 6 | width: 300px; 7 | padding-top: 40px; 8 | margin: 0 auto; 9 | } 10 | @media (min-height: 700px) { 11 | form { 12 | padding-top: 60px; 13 | } 14 | } 15 | form h1 { 16 | text-align: center; 17 | font-size: 52px; 18 | margin-bottom: 30px; 19 | } 20 | form h1 sup { 21 | font-size: 50%; 22 | top: -1em; 23 | } 24 | form button[type=submit] { 25 | margin-left: 10px; 26 | } 27 | -------------------------------------------------------------------------------- /web/static/css/task_new.css: -------------------------------------------------------------------------------- 1 | /* vim: set et sw=2 ts=2 sts=2 ff=unix fenc=utf8: */ 2 | /* Author: Binux */ 3 | /* http://binux.me */ 4 | /* Created on 2014-08-09 11:44:44 */ 5 | form { 6 | padding: 0 20px 20px 20px; 7 | } 8 | form button { 9 | margin-left: 10px; 10 | } 11 | -------------------------------------------------------------------------------- /web/static/cur/default.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/cur/default.cur -------------------------------------------------------------------------------- /web/static/cur/pointer.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/cur/pointer.cur -------------------------------------------------------------------------------- /web/static/har/contenteditable.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | (function() { 3 | // vim: set et sw=2 ts=2 sts=2 ff=unix fenc=utf8: 4 | // Author: Binux 5 | // http://binux.me 6 | // Created on 2014-08-04 19:34:02 7 | define(function(require, exports, module) { 8 | return angular.module('contenteditable', []).directive('contenteditable', [ 9 | '$timeout', 10 | function($timeout) { 11 | return { 12 | restrict: 'A', 13 | require: '?ngModel', 14 | link: function(scope, 15 | element, 16 | attrs, 17 | ngModel) { 18 | var oldRender; 19 | if (!ngModel) { 20 | return; 21 | } 22 | element.bind('input', 23 | function(e) { 24 | return scope.$apply(function() { 25 | var text; 26 | text = element.text(); 27 | ngModel.$setViewValue(text); 28 | if (text === '') { 29 | return $timeout(function() { 30 | if (element.prev().hasClass('contentedit-wrapper')) { 31 | return element.prev().click(); 32 | } else { 33 | element[0].blur(); 34 | return element[0].focus(); 35 | } 36 | }); 37 | } 38 | }); 39 | }); 40 | element.bind('blur', 41 | function(e) { 42 | var text; 43 | text = element.text(); 44 | if (ngModel.$viewValue !== text) { 45 | return ngModel.$render(); 46 | } 47 | }); 48 | oldRender = ngModel.$render; 49 | return ngModel.$render = function() { 50 | if (!!oldRender) { 51 | oldRender(); 52 | } 53 | if (!element.is(':focus')) { 54 | return element.text(ngModel.$viewValue || ''); 55 | } 56 | }; 57 | } 58 | }; 59 | } 60 | ]); 61 | }); 62 | 63 | }).call(this); 64 | -------------------------------------------------------------------------------- /web/static/har/editablelist.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
6 | 9 |
10 |
11 | 12 |
13 |
14 |
15 | 16 | 17 |
18 | 20 |
22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /web/static/har/editablelist.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | (function() { 3 | // vim: set et sw=2 ts=2 sts=2 ff=unix fenc=utf8: 4 | // Author: Binux 5 | // http://binux.me 6 | // Created on 2014-08-05 14:47:55 7 | define(function(require, exports, module) { 8 | return angular.module('editablelist', []).directive('editablelist', function() { 9 | return { 10 | restrict: 'A', 11 | scope: true, 12 | templateUrl: '/static/har/editablelist.html', 13 | link: function($scope, $element, $attr, ctrl, $transclude) { 14 | $scope.$filter = $scope.$eval($attr.filter); 15 | return $scope.$watch($attr.editablelist, function(value) { 16 | return $scope.$list = value; 17 | }); 18 | } 19 | }; 20 | }); 21 | }); 22 | 23 | }).call(this); 24 | -------------------------------------------------------------------------------- /web/static/har/editor.css: -------------------------------------------------------------------------------- 1 | /* vim: set et sw=2 ts=2 sts=2 ff=unix fenc=utf8: */ 2 | /* Author: Binux */ 3 | /* http://binux.me */ 4 | /* Created on 2014-08-01 19:18:00 */ 5 | .container { 6 | margin-top: 10px; 7 | } 8 | .pageref { 9 | color: #3c763d; 10 | background-color: #dff0d8; 11 | } 12 | .entry { 13 | padding-left: 46px; 14 | overflow: hidden; 15 | text-overflow: ellipsis; 16 | white-space: nowrap; 17 | } 18 | .entry-checked { 19 | position: absolute; 20 | border-right: 1px solid #ddd; 21 | padding: 0 12px; 22 | left: 0; 23 | } 24 | .label { 25 | margin: 0 .2em; 26 | } 27 | .btn-xxs { 28 | padding: 1px 3px; 29 | font-size: 8px; 30 | line-height: 1.5; 31 | border-radius: 2px; 32 | } 33 | a.h1-index { 34 | color: black; 35 | } 36 | a.h1-index:hover, 37 | a.h1-index:focus { 38 | text-decoration: none; 39 | } 40 | h1 { 41 | margin-top: 10px; 42 | margin-bottom: 20px; 43 | } 44 | #upload-har #har-guide { 45 | position: absolute; 46 | margin: 10px 10px; 47 | top: 0; 48 | right: 0; 49 | } 50 | #edit-entry .entry-method { 51 | display: inline-block; 52 | } 53 | #edit-entry .entry-url { 54 | padding: .2em .6em .1em; 55 | } 56 | #edit-entry dt .contentedit-wrapper:after { 57 | content: ":"; 58 | } 59 | #edit-entry dt, 60 | #edit-entry dd { 61 | word-break: break-all; 62 | } 63 | #edit-entry details dt .contentedit-wrapper { 64 | overflow: hidden; 65 | text-overflow: ellipsis; 66 | white-space: nowrap; 67 | } 68 | #edit-entry .select .btn { 69 | margin-top: 1px; 70 | margin-right: .5em; 71 | } 72 | #edit-entry .select .btn.active { 73 | color: #fff; 74 | background-color: #449d44; 75 | border-color: #398439; 76 | } 77 | #edit-entry .select .btn:after { 78 | font-family: 'Glyphicons Halflings'; 79 | content: "\2212"; 80 | } 81 | #edit-entry .select .btn.active:after { 82 | content: "\e013"; 83 | } 84 | #edit-entry .remove.btn:after { 85 | line-height: 1.5; 86 | font-family: 'Glyphicons Halflings'; 87 | content: "\e014"; 88 | } 89 | #edit-entry .panel-preview iframe { 90 | border: 0; 91 | width: 100%; 92 | height: 400px; 93 | } 94 | #edit-entry .preview-assert dt { 95 | width: 60%; 96 | text-align: left; 97 | position: relative; 98 | } 99 | #edit-entry .preview-assert dt select { 100 | width: 100px; 101 | } 102 | #edit-entry .preview-assert dt .input-wrapper { 103 | position: absolute; 104 | top: 0; 105 | left: 105px; 106 | right: 40px; 107 | } 108 | #edit-entry .preview-assert dt .input-wrapper input { 109 | width: 100%; 110 | border: 0; 111 | text-align: right; 112 | } 113 | #edit-entry .preview-assert dt .eq { 114 | width: 20px; 115 | float: right; 116 | } 117 | #edit-entry .preview-assert dd { 118 | margin-left: 60%; 119 | padding-left: 20px; 120 | } 121 | #edit-entry .extract-variables dt { 122 | width: 60%; 123 | position: relative; 124 | } 125 | #edit-entry .extract-variables dt .select-wrapper { 126 | width: 100px; 127 | position: absolute; 128 | left: 0; 129 | } 130 | #edit-entry .extract-variables dt .input-wrapper { 131 | width: 50%; 132 | float: left; 133 | padding: 0 20px; 134 | } 135 | #edit-entry .extract-variables dt .input-wrapper.name { 136 | padding-left: 120px; 137 | } 138 | #edit-entry .extract-variables dt .eq { 139 | position: absolute; 140 | left: 50%; 141 | margin-left: -20px; 142 | width: 20px; 143 | float: left; 144 | } 145 | #edit-entry .extract-variables dt .eq:nth-child(2) { 146 | left: 100%; 147 | } 148 | #edit-entry .extract-variables dt input { 149 | border: 0; 150 | width: 100%; 151 | text-align: right; 152 | } 153 | #edit-entry .extract-variables dd { 154 | margin-left: 60%; 155 | padding-left: 20px; 156 | } 157 | .contentedit-wrapper + [contenteditable] { 158 | display: none; 159 | } 160 | [contenteditable]:empty:not(:focus):before { 161 | color: #777; 162 | content: attr(data-ph); 163 | } 164 | .add-request { 165 | float: left; 166 | } 167 | .plaintext_only { 168 | -webkit-user-modify: read-write-plaintext-only; 169 | } -------------------------------------------------------------------------------- /web/static/har/editor.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | (function() { 3 | // vim: set et sw=2 ts=2 sts=2 ff=unix fenc=utf8: 4 | // Author: Binux 5 | // http://binux.me 6 | // Created on 2014-08-01 11:02:45 7 | define(function(require, exports, module) { 8 | var cookie_input, editor; 9 | require('/static/har/contenteditable'); 10 | require('/static/har/upload_ctrl'); 11 | require('/static/har/entry_list'); 12 | require('/static/har/entry_editor'); 13 | // contentedit-wrapper 14 | $(document).on('click', '.contentedit-wrapper', function(ev) { 15 | var editable; 16 | return editable = $(this).hide().next('[contenteditable]').show().focus(); 17 | }); 18 | $(document).on('blur', '.contentedit-wrapper + [contenteditable]', function(ev) { 19 | return $(this).hide().prev('.contentedit-wrapper').show(); 20 | }); 21 | $(document).on('focus', '[contenteditable]', function(ev) { 22 | var range, sel; 23 | if (this.childNodes[0]) { 24 | range = document.createRange(); 25 | sel = window.getSelection(); 26 | range.setStartBefore(this.childNodes[0]); 27 | range.setEndAfter(this); 28 | sel.removeAllRanges(); 29 | return sel.addRange(range); 30 | } 31 | }); 32 | // $(() -> 33 | // if $('body').attr('get-cookie') == 'true' 34 | // $('[data-toggle=get-cookie][disabled]').attr('disabled', false) 35 | // return 36 | // ) 37 | 38 | // get cookie helper 39 | cookie_input = null; 40 | $(document).on('click', "[data-toggle=get-cookie]", function(ev) { 41 | var $this; 42 | $this = $(this); 43 | // if $this.attr('disabled') 44 | // return 45 | cookie_input = angular.element($this.parent().find('input')); 46 | if ($('body').attr('get-cookie') !== 'true') { 47 | // $this.html('没有插件,详情F12') 48 | // console.log('如需要插件请访问 https://github.com/qd-today/get-cookies/ 下载并手动安装插件') 49 | if ($this.attr('getmod') === 1) { 50 | $this.attr('href', 'https://github.com/qd-today/get-cookies/').attr('target', '_blank').html('安装插件后请刷新'); 51 | } else { 52 | $this.attr('getmod', 1).html('再次点击前往安装 Get-Cookies 插件'); 53 | } 54 | } 55 | }); 56 | // deepcode ignore InsufficientPostmessageValidation: the event.origin is checked 57 | window.addEventListener("message", function(ev) { 58 | var cookie, cookie_str, key, value; 59 | if (event.origin !== window.location.origin) { 60 | return; 61 | } 62 | cookie = ev.data; 63 | cookie_str = ""; 64 | for (key in cookie) { 65 | value = cookie[key]; 66 | cookie_str += key + '=' + value + '; '; 67 | } 68 | if (cookie_str === '') { 69 | console.log('没有获得cookie, 您是否已经登录?'); 70 | return; 71 | } 72 | if (cookie_input != null) { 73 | cookie_input.val(cookie_str); 74 | } 75 | return cookie_input != null ? cookie_input.scope().$parent.var.value = cookie_str : void 0; 76 | }); 77 | editor = angular.module('HAREditor', ['editablelist', 'upload_ctrl', 'entry_list', 'entry_editor']); 78 | return { 79 | init: function() { 80 | return angular.bootstrap(document.body, ['HAREditor']); 81 | } 82 | }; 83 | }); 84 | 85 | }).call(this); 86 | -------------------------------------------------------------------------------- /web/static/har/editor.less: -------------------------------------------------------------------------------- 1 | /* vim: set et sw=2 ts=2 sts=2 ff=unix fenc=utf8: */ 2 | /* Author: Binux */ 3 | /* http://binux.me */ 4 | /* Created on 2014-08-01 19:18:00 */ 5 | 6 | 7 | .container { 8 | margin-top: 10px; 9 | } 10 | 11 | .pageref { 12 | color: #3c763d; 13 | background-color: #dff0d8; 14 | } 15 | 16 | .entry { 17 | padding-left: 46px; 18 | overflow: hidden; 19 | text-overflow: ellipsis; 20 | white-space: nowrap; 21 | } 22 | 23 | .entry-checked { 24 | position: absolute; 25 | border-right: 1px solid #ddd; 26 | padding: 0 12px; 27 | left: 0; 28 | } 29 | 30 | .label { 31 | margin: 0 .2em; 32 | } 33 | 34 | .btn-xxs { 35 | padding: 1px 3px; 36 | font-size: 8px; 37 | line-height: 1.5; 38 | border-radius: 2px; 39 | } 40 | 41 | 42 | a.h1-index { 43 | color: black; 44 | &:hover, &:focus { 45 | text-decoration: none; 46 | } 47 | } 48 | 49 | h1 { 50 | margin-top: 10px; 51 | margin-bottom: 20px; 52 | } 53 | 54 | #upload-har { 55 | #har-guide { 56 | position: absolute; 57 | margin: 10px 10px; 58 | top: 0; 59 | right: 0; 60 | } 61 | } 62 | 63 | #edit-entry { 64 | .entry-method { 65 | display: inline-block; 66 | } 67 | 68 | .entry-url { 69 | padding: .2em .6em .1em; 70 | } 71 | 72 | dt .contentedit-wrapper:after { 73 | content: ":"; 74 | } 75 | 76 | dt, dd { 77 | word-break: break-all; 78 | 79 | } 80 | 81 | details dt .contentedit-wrapper { 82 | overflow: hidden; 83 | text-overflow: ellipsis; 84 | white-space: nowrap; 85 | } 86 | 87 | .select .btn { 88 | margin-top: 1px; 89 | margin-right: .5em; 90 | 91 | &.active { 92 | color: #fff; 93 | background-color: #449d44; 94 | border-color: #398439; 95 | } 96 | 97 | &:after { 98 | font-family: 'Glyphicons Halflings'; 99 | content: "\2212"; 100 | } 101 | 102 | &.active:after { 103 | content: "\e013"; 104 | } 105 | } 106 | 107 | .remove.btn:after { 108 | line-height: 1.5; 109 | font-family: 'Glyphicons Halflings'; 110 | content: "\e014"; 111 | } 112 | 113 | .panel-preview iframe { 114 | border: 0; 115 | width: 100%; 116 | height: 400px; 117 | } 118 | 119 | .preview-assert { 120 | dt { 121 | width: 60%; 122 | text-align: left; 123 | position: relative; 124 | 125 | select { 126 | width: 100px; 127 | } 128 | 129 | .input-wrapper { 130 | position: absolute; 131 | top: 0; 132 | left: 105px; 133 | right: 40px; 134 | 135 | input { 136 | width: 100%; 137 | border: 0; 138 | text-align: right; 139 | } 140 | } 141 | 142 | .eq { 143 | width: 20px; 144 | float: right; 145 | } 146 | } 147 | dd { 148 | margin-left: 60%; 149 | padding-left: 20px; 150 | } 151 | } 152 | 153 | .extract-variables { 154 | dt { 155 | width: 60%; 156 | position: relative; 157 | 158 | .select-wrapper { 159 | width: 100px; 160 | position: absolute; 161 | left: 0; 162 | } 163 | 164 | .input-wrapper { 165 | width: 50%; 166 | float: left; 167 | padding: 0 20px; 168 | } 169 | 170 | .input-wrapper.name { 171 | padding-left: 120px; 172 | } 173 | 174 | .eq { 175 | position: absolute; 176 | left: 50%; 177 | margin-left: -20px; 178 | width: 20px; 179 | float: left; 180 | } 181 | 182 | .eq:nth-child(2) { 183 | left: 100%; 184 | } 185 | 186 | 187 | input { 188 | border: 0; 189 | width: 100%; 190 | text-align: right; 191 | } 192 | } 193 | dd { 194 | margin-left: 60%; 195 | padding-left: 20px; 196 | } 197 | } 198 | } 199 | 200 | .contentedit-wrapper + [contenteditable] { 201 | display: none; 202 | } 203 | 204 | [contenteditable]:empty:not(:focus):before { 205 | color: #777; 206 | content:attr(data-ph) 207 | } 208 | 209 | .add-request { 210 | float: left; 211 | } 212 | -------------------------------------------------------------------------------- /web/static/har/partials/upload.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/har/partials/upload.html -------------------------------------------------------------------------------- /web/static/img/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/img/120.png -------------------------------------------------------------------------------- /web/static/img/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/img/128.png -------------------------------------------------------------------------------- /web/static/img/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/img/152.png -------------------------------------------------------------------------------- /web/static/img/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/img/16.png -------------------------------------------------------------------------------- /web/static/img/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/img/167.png -------------------------------------------------------------------------------- /web/static/img/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/img/180.png -------------------------------------------------------------------------------- /web/static/img/192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/img/192.png -------------------------------------------------------------------------------- /web/static/img/200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/img/200.png -------------------------------------------------------------------------------- /web/static/img/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/img/32.png -------------------------------------------------------------------------------- /web/static/img/96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/img/96.png -------------------------------------------------------------------------------- /web/static/img/body.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/img/body.jpg -------------------------------------------------------------------------------- /web/static/img/header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/img/header.jpg -------------------------------------------------------------------------------- /web/static/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/img/icon.png -------------------------------------------------------------------------------- /web/static/img/icon_old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/img/icon_old.png -------------------------------------------------------------------------------- /web/static/img/push_pic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/img/push_pic.png -------------------------------------------------------------------------------- /web/static/img/slide_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/img/slide_bg.png -------------------------------------------------------------------------------- /web/static/img/slide_target.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a76yyyy/qd/b27f0c0253936f7a0628896b371083a9d21c2990/web/static/img/slide_target.png -------------------------------------------------------------------------------- /web/tpl/LeftFuncBtn.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 |
8 | 16 | 17 | 22 | 23 | 28 | 29 | -------------------------------------------------------------------------------- /web/tpl/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block head %} 4 | {{ super() }} 5 | 6 | {% endblock %} 7 | 8 | {% block body %} 9 |
10 | 13 |
14 | {{ utils.qd_title() }} 15 |

16 |
17 | 21 | 26 |
27 | 28 |
29 | {% include "task_new_var.html" %} 30 |
31 | 32 |
33 | 立即执行 34 | 创建任务 35 |
36 | 37 | 52 | {{ utils.task_new_scripts() }} 53 | {{ utils.modal_load() }} 54 | {{ utils.form_select2(id="#tplid") }} 55 |
56 |
57 | {% endblock %} 58 | -------------------------------------------------------------------------------- /web/tpl/login.html: -------------------------------------------------------------------------------- 1 | {% extends "register.html" %} 2 | 3 | {% block title %}QD 登录{% endblock %} 4 | 5 | {% block reg %}{% endblock %} 6 | -------------------------------------------------------------------------------- /web/tpl/password_reset.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}QD 密码重置{% endblock %} 4 | 5 | {% block head %} 6 | {{ super() }} 7 | 8 | {% endblock %} 9 | 10 | {% block body %} 11 |
12 |
13 | {{ utils.qd_title() }} 14 |

密码重置

15 |
16 | 17 | 18 | 19 | 20 | {% if password_error %}

{{ password_error }}

{% endif %} 21 |

请输入新的密码

22 |
23 |
24 | 25 |
26 |
27 |
28 | {{ utils.submit_loading() }} 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /web/tpl/password_reset_email.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}QD 密码重置{% endblock %} 4 | 5 | {% block head %} 6 | {{ super() }} 7 | 8 | {% endblock %} 9 | 10 | {% block body %} 11 |
12 |
13 | {{ utils.qd_title() }} 14 |

密码重置

15 | 21 |
22 | 23 |
24 |
25 |
26 | {{ utils.submit_loading() }} 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /web/tpl/pubtpl_register.html: -------------------------------------------------------------------------------- 1 |
2 |

注册仓库

3 |
4 |
5 | 6 | {% if current_user %} 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 |
21 | {% endif %} 22 | 23 |
24 | 25 |
26 | 27 | 61 |
62 | -------------------------------------------------------------------------------- /web/tpl/pubtpl_reposinfo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% for repo in repos %} 10 | 11 | 14 | 15 | 16 | {% endfor %} 17 | 18 |
仓库名仓库分支
12 | {{ repo.reponame }} 13 | {{ repo.repobranch }}
-------------------------------------------------------------------------------- /web/tpl/pubtpl_unsubscribe.html: -------------------------------------------------------------------------------- 1 |
2 |

退订仓库

3 |
4 |
5 | 31 | {% if current_user %} 32 |
33 |
34 | 36 |
37 | 73 | {% endif %} 74 |
-------------------------------------------------------------------------------- /web/tpl/register.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}QD 注册{% endblock %} 4 | 5 | {% block head %} 6 | {{ super() }} 7 | 8 | {% endblock %} 9 | 10 | {% block body %} 11 |
12 |
13 | {{ utils.qd_title() }} 14 |
15 | {% block reg %}

欢迎注册私有QD平台

{% endblock %} 16 | 23 |
24 | 25 | 26 | 27 | 28 | {% if password_error %}

{{ password_error }}

{% endif %} 29 |
30 |
31 |
32 | 33 | 34 | {% if regFlg %} 35 | 36 | {% endif %} 37 | 38 | 39 |
40 |
41 | {{ utils.submit_loading() }} 42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /web/tpl/site_manage.html: -------------------------------------------------------------------------------- 1 | {% import "utils.html" as utils %} 2 | 3 | 4 |

网站管理

5 |
6 | 7 | {% if adminflg %} 8 |
9 | 12 |
13 |
14 | 17 |
18 |
19 | 20 | 21 |
22 | 23 |
24 | 25 |
26 | 27 |
28 | 29 | 30 | 31 | 32 |
33 | 34 |
35 | 36 |
37 | 38 | {% else %} 39 |
非管理员,不可查看
40 | {% endif %} 41 | 42 | {{ utils.submit_loading() }} 43 | 44 | 45 | 54 | 55 | -------------------------------------------------------------------------------- /web/tpl/task_new_var.html: -------------------------------------------------------------------------------- 1 | {% if tpl.note %} 2 | 3 |
4 | {% autoescape false %} 5 | {{ tpl.note|replace("\r\n","
")|replace("\n","
") }} 6 | {% endautoescape %} 7 |
8 | {% endif %} 9 | 10 | {% macro asurl(siteurl) %}{{ 'http://'+siteurl if not siteurl.startswith('http') else siteurl }}{% endmacro %} 11 | 12 | {% macro varname_format(var) %} 13 | {%- set var_lower = var.lower() %} 14 | {%- if var_lower == 'username' %}用户名 15 | {%- elif var_lower == 'password' %}密码 16 | {%- elif var_lower == 'cookie' or var_lower == 'cookies' %}Cookies{% if tpl.siteurl %}点击获取{% endif %} 17 | {%- else %}{{ var }} 18 | {%- endif -%} 19 | {% endmacro %} 20 | 21 | {% macro var_tips(var) %} 22 | {% endmacro %} 23 | 24 | {%if task and task.init_env %} 25 | {% for env in init_env %} 26 |
27 | 28 | 29 | {% if env.value %} 30 | 前值:
{{ env.value }} 31 |
复制
32 |
33 | {% endif %} 34 | 35 | 36 | {{ var_tips(env.name) }} 37 |
38 | {% endfor %} 39 | {% else %} 40 | {% for var in variables %} 41 |
42 | 43 | {% if init_env.get(var, '') %} 44 | 45 | 默认值:
{{ init_env.get(var, '') }} 46 |
复制
47 |
48 | {% else %} 49 | 50 | {% endif %} 51 | 52 | 53 | {{ var_tips(var) }} 54 |
55 | {% endfor %} 56 | {% endif %} 57 | 60 | -------------------------------------------------------------------------------- /web/tpl/task_setgroup.html: -------------------------------------------------------------------------------- 1 | {% import "utils.html" as utils %} 2 |
3 |

设置分组

4 |
5 | 6 |
7 | {% for group in _groups %} 8 |
9 | 12 |
13 | {% endfor %} 14 |
15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 |
26 | {% if current_user %} 27 | 28 | {% endif %} 29 |
30 | 31 | 43 | 44 | {{ utils.task_new_scripts() }} 45 | {{ utils.submit_loading() }} 46 |
47 | -------------------------------------------------------------------------------- /web/tpl/tasklog.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}QD 日志{% endblock %} 4 | 5 | {% block header %} 6 | {{ super() }} 7 | 8 | {% endblock %} 9 | 10 | {% block body %} 11 | {{ utils.header(current_user, title="QD日志", sup=False) }} 12 | 13 |
14 |
15 |

16 | 任务执行日志 17 | 清空 18 | 19 | 20 |

21 |
22 | 24 | 天的日志 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {% for log in tasklog | sort(reverse=True, attribute="ctime") %} 36 | 37 | 38 | 45 | 55 | 56 | {% endfor %} 57 | 58 |
时间状态日志
{{ format_date(log.ctime) }} 39 | {% if log.success %} 40 | 成功 41 | {% else %} 42 | 失败 43 | {% endif %} 44 | 46 | {% if log.msg %} 47 | {% for _log in log.msg.split('\\r\\n') %} 48 | {{ _log }}
49 | {% endfor %} 50 | 51 | {% else %} 52 | {{ log.msg }} 53 | 54 | {% endif %}
59 |
60 |
61 | 85 | {% endblock %} 86 | -------------------------------------------------------------------------------- /web/tpl/taskmulti_tasksinfo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% for task in tasks %} 10 | 11 | 12 | 23 | 24 | {% endfor %} 25 | 26 |
任务id任务名
{{ task.id }} 13 | {{ task.sitename }} 14 | {% if task.note %} 15 | - 16 | 17 | {% if task.note | length > 15 %} 18 | {{ task.note | truncate(10, True) }} 19 | {% else %} {{ task.note }} {% endif %} 20 | {% endif %} 21 | 22 |
-------------------------------------------------------------------------------- /web/tpl/toolbox.html: -------------------------------------------------------------------------------- 1 | {% import "utils.html" as utils %} 2 |
3 |

工具箱

4 |
5 | {% if current_user %} 6 | 7 | 8 | 9 | 10 | 11 | {% endif %} 12 | 13 | {{ utils.submit_loading() }} 14 |
15 | -------------------------------------------------------------------------------- /web/tpl/tpl_push.html: -------------------------------------------------------------------------------- 1 | {% import "utils.html" as utils %} 2 |
3 |

模板发布

4 |
5 |

发布模板需要经过以下测试

6 |

  • 1. 运行失败 逻辑测试
  • 7 |

  • 2. 运行成功 逻辑测试
  • 8 |

    您可以使用 成功/失败断言 检测任务是否成功

    9 |
    10 |
    11 | 12 | 13 |

    只有勾选的HAR请求会被发布,但依旧不要忘记删除请求中的个人信息

    14 |
    15 |
    16 | 17 | 23 |

    您的模板将会被暂时锁定,直到发布被接受或拒绝。

    24 |
    25 |
    26 | 27 | 28 | 29 | 30 |
    31 |
    32 | 33 |
    34 | {{ utils.form_select2(id="#totpl") }} 35 |
    36 | -------------------------------------------------------------------------------- /web/tpl/tpl_run_failed.html: -------------------------------------------------------------------------------- 1 | 2 |

    运行失败

    3 |
    4 | {% if log %} 5 | 6 | {% for _log in log.split('\\r\\n') %} 7 | {{ _log }}
    8 | {% endfor %} 9 | {% endif%} 10 |
    11 | -------------------------------------------------------------------------------- /web/tpl/tpl_run_success.html: -------------------------------------------------------------------------------- 1 |

    运行成功

    2 | {% if log %} 3 |
    4 | {% for _log in log.split('\\r\\n') %} 5 | {{ _log }}
    6 | {% endfor %} 7 |
    8 | {% endif %} 9 | -------------------------------------------------------------------------------- /web/tpl/tpl_setgroup.html: -------------------------------------------------------------------------------- 1 | {% import "utils.html" as utils %} 2 |
    3 |

    设置分组

    4 |
    5 | 6 |
    7 | {% for group in _groups %} 8 |
    9 | 12 |
    13 | {% endfor %} 14 |
    15 | 16 | 17 |
    18 | 19 | 20 | 21 | 22 | 23 |
    24 | 25 |
    26 | {% if current_user %} 27 | 28 | {% endif %} 29 |
    30 | 31 | 43 | 44 | {{ utils.task_new_scripts() }} 45 | {{ utils.submit_loading() }} 46 |
    47 | -------------------------------------------------------------------------------- /web/tpl/tpls_public.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}QD 公开模板{% endblock %} 4 | 5 | {% block head %} 6 | {{ super() }} 7 | 8 | {% endblock %} 9 | 10 | {% block body %} 11 | {{ utils.header(current_user) }} 12 | 13 |
    14 |
    15 |

    16 | 公开模板 17 |

    18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {% for tpl in tpls %} 30 | 31 | 34 | 37 | 40 | 47 | 54 | 55 | {% endfor %} 56 | 57 |
    网站创建时间最近修改时间最近成功时间操作
    32 | {{ utils.tplname(tpl) }} {% if tpl.siteurl %}- {{ tpl.siteurl }}{% endif %} 33 | 35 | {{ format_date(tpl.ctime) }} 36 | 38 | {{ format_date(tpl.mtime) }} 39 | 41 | {% if tpl.last_success %} 42 | {{ format_date(tpl.last_success) }} 43 | {% else %} 44 | 从未 45 | {% endif %} 46 | 48 | {% if current_user.isadmin %} 49 | 删除 50 | {% endif %} 51 | 查看 52 | 新建任务 53 |
    58 |
    59 |
    60 | 61 | 68 | 69 | {{ utils.modal_load() }} 70 | {{ utils.apost() }} 71 | {% endblock %} 72 | -------------------------------------------------------------------------------- /web/tpl/user_setnewpwd.html: -------------------------------------------------------------------------------- 1 | {% import "utils.html" as utils %} 2 |
    3 |

    用户列表

    4 | 5 |
    6 |
    7 | 8 |
    9 |
    10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 |
    21 | 22 |
    23 | 24 | 52 | 53 | {{ utils.submit_loading() }} 54 |
    55 | 56 | -------------------------------------------------------------------------------- /web/tpl/utils_run_result.html: -------------------------------------------------------------------------------- 1 |

    {{ title }}

    2 | {% if log %} 3 |
    {% for _log in log.split('\\r\\n') %}{{ _log  }}
    {% endfor %}
    4 | {% endif %} 5 | --------------------------------------------------------------------------------