auto-playwright-web 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. auto_playwright_web-0.1.0/PKG-INFO +68 -0
  2. auto_playwright_web-0.1.0/README.md +50 -0
  3. auto_playwright_web-0.1.0/app/__init__.py +1 -0
  4. auto_playwright_web-0.1.0/app/cli.py +161 -0
  5. auto_playwright_web-0.1.0/app/configs/__init__.py +1 -0
  6. auto_playwright_web-0.1.0/app/configs/base.py +19 -0
  7. auto_playwright_web-0.1.0/app/configs/config.py +3 -0
  8. auto_playwright_web-0.1.0/app/dependencies.py +123 -0
  9. auto_playwright_web-0.1.0/app/domain.py +65 -0
  10. auto_playwright_web-0.1.0/app/main.py +110 -0
  11. auto_playwright_web-0.1.0/app/repositories/__init__.py +1 -0
  12. auto_playwright_web-0.1.0/app/repositories/base.py +38 -0
  13. auto_playwright_web-0.1.0/app/repositories/memory.py +78 -0
  14. auto_playwright_web-0.1.0/app/repositories/sqlite.py +321 -0
  15. auto_playwright_web-0.1.0/app/routers/__init__.py +1 -0
  16. auto_playwright_web-0.1.0/app/routers/auth.py +45 -0
  17. auto_playwright_web-0.1.0/app/routers/common_library.py +37 -0
  18. auto_playwright_web-0.1.0/app/routers/executions.py +64 -0
  19. auto_playwright_web-0.1.0/app/routers/open_api.py +84 -0
  20. auto_playwright_web-0.1.0/app/routers/schedules.py +171 -0
  21. auto_playwright_web-0.1.0/app/routers/scripts.py +76 -0
  22. auto_playwright_web-0.1.0/app/routers/user_env_vars.py +39 -0
  23. auto_playwright_web-0.1.0/app/routers/webhooks.py +22 -0
  24. auto_playwright_web-0.1.0/app/scheduler.py +43 -0
  25. auto_playwright_web-0.1.0/app/schemas/__init__.py +1 -0
  26. auto_playwright_web-0.1.0/app/schemas/auth.py +32 -0
  27. auto_playwright_web-0.1.0/app/schemas/common_library.py +24 -0
  28. auto_playwright_web-0.1.0/app/schemas/execution.py +33 -0
  29. auto_playwright_web-0.1.0/app/schemas/schedule.py +42 -0
  30. auto_playwright_web-0.1.0/app/schemas/script.py +111 -0
  31. auto_playwright_web-0.1.0/app/schemas/token.py +30 -0
  32. auto_playwright_web-0.1.0/app/schemas/user.py +10 -0
  33. auto_playwright_web-0.1.0/app/schemas/user_env_var.py +31 -0
  34. auto_playwright_web-0.1.0/app/schemas/webhook.py +22 -0
  35. auto_playwright_web-0.1.0/app/security.py +90 -0
  36. auto_playwright_web-0.1.0/app/service/__init__.py +1 -0
  37. auto_playwright_web-0.1.0/app/service/auth_service.py +69 -0
  38. auto_playwright_web-0.1.0/app/service/common_library_service.py +34 -0
  39. auto_playwright_web-0.1.0/app/service/execution_service.py +229 -0
  40. auto_playwright_web-0.1.0/app/service/notification_service.py +44 -0
  41. auto_playwright_web-0.1.0/app/service/runner.py +146 -0
  42. auto_playwright_web-0.1.0/app/service/schedule_service.py +104 -0
  43. auto_playwright_web-0.1.0/app/service/script_loader.py +131 -0
  44. auto_playwright_web-0.1.0/app/service/script_service.py +205 -0
  45. auto_playwright_web-0.1.0/app/service/token_service.py +75 -0
  46. auto_playwright_web-0.1.0/app/service/user_env_var_service.py +97 -0
  47. auto_playwright_web-0.1.0/app/service/webhook_service.py +41 -0
  48. auto_playwright_web-0.1.0/app/static/assets/EnvVarsPage-BQXP7S1T.css +1 -0
  49. auto_playwright_web-0.1.0/app/static/assets/EnvVarsPage-C2RsrYZw.js +4 -0
  50. auto_playwright_web-0.1.0/app/static/assets/ExecutionsPage-2b2aSqsU.js +3 -0
  51. auto_playwright_web-0.1.0/app/static/assets/ExecutionsPage-DpccwQ-n.css +1 -0
  52. auto_playwright_web-0.1.0/app/static/assets/SchedulesPage-CJt0O7JF.css +1 -0
  53. auto_playwright_web-0.1.0/app/static/assets/SchedulesPage-CzIaVV1C.js +1 -0
  54. auto_playwright_web-0.1.0/app/static/assets/ScriptsPage-3yx9JHib.js +9 -0
  55. auto_playwright_web-0.1.0/app/static/assets/ScriptsPage-w9DcygkJ.css +1 -0
  56. auto_playwright_web-0.1.0/app/static/assets/abap-BrgZPUOV.js +6 -0
  57. auto_playwright_web-0.1.0/app/static/assets/apex-DyP6w7ZV.js +6 -0
  58. auto_playwright_web-0.1.0/app/static/assets/api-DM5brzz4.js +9 -0
  59. auto_playwright_web-0.1.0/app/static/assets/azcli-BaLxmfj-.js +6 -0
  60. auto_playwright_web-0.1.0/app/static/assets/bat-CFOPXBzS.js +6 -0
  61. auto_playwright_web-0.1.0/app/static/assets/bicep-BfEKNvv3.js +7 -0
  62. auto_playwright_web-0.1.0/app/static/assets/cameligo-BFG1Mk7z.js +6 -0
  63. auto_playwright_web-0.1.0/app/static/assets/clojure-DTECt2xU.js +6 -0
  64. auto_playwright_web-0.1.0/app/static/assets/codicon-DCmgc-ay.ttf +0 -0
  65. auto_playwright_web-0.1.0/app/static/assets/coffee-CDGzqUPQ.js +6 -0
  66. auto_playwright_web-0.1.0/app/static/assets/cpp-CLLBncYj.js +6 -0
  67. auto_playwright_web-0.1.0/app/static/assets/csharp-dUCx_-0o.js +6 -0
  68. auto_playwright_web-0.1.0/app/static/assets/csp-5Rap-vPy.js +6 -0
  69. auto_playwright_web-0.1.0/app/static/assets/css-D3h14YRZ.js +8 -0
  70. auto_playwright_web-0.1.0/app/static/assets/cssMode-Dd-OIW4z.js +9 -0
  71. auto_playwright_web-0.1.0/app/static/assets/cypher-DrQuvNYM.js +6 -0
  72. auto_playwright_web-0.1.0/app/static/assets/dart-CFKIUWau.js +6 -0
  73. auto_playwright_web-0.1.0/app/static/assets/dockerfile-Zznr-cwX.js +6 -0
  74. auto_playwright_web-0.1.0/app/static/assets/ecl-Ce3n6wWz.js +6 -0
  75. auto_playwright_web-0.1.0/app/static/assets/editor.worker-DX6ApQqM.js +12 -0
  76. auto_playwright_web-0.1.0/app/static/assets/elixir-deUWdS0T.js +6 -0
  77. auto_playwright_web-0.1.0/app/static/assets/flow9-i9-g7ZhI.js +6 -0
  78. auto_playwright_web-0.1.0/app/static/assets/freemarker2-DorxjobG.js +8 -0
  79. auto_playwright_web-0.1.0/app/static/assets/fsharp-CzKuDChf.js +6 -0
  80. auto_playwright_web-0.1.0/app/static/assets/go-Cphgjts3.js +6 -0
  81. auto_playwright_web-0.1.0/app/static/assets/graphql-Cg7bfA9N.js +6 -0
  82. auto_playwright_web-0.1.0/app/static/assets/handlebars-CZINcTMr.js +6 -0
  83. auto_playwright_web-0.1.0/app/static/assets/hcl-0cvrggvQ.js +6 -0
  84. auto_playwright_web-0.1.0/app/static/assets/html-Cwf08_sN.js +6 -0
  85. auto_playwright_web-0.1.0/app/static/assets/htmlMode-CsM53ifH.js +9 -0
  86. auto_playwright_web-0.1.0/app/static/assets/index-BivV9deF.css +1 -0
  87. auto_playwright_web-0.1.0/app/static/assets/index-w3kBh3h9.js +847 -0
  88. auto_playwright_web-0.1.0/app/static/assets/ini-Drc7WvVn.js +6 -0
  89. auto_playwright_web-0.1.0/app/static/assets/java-B_fMsGYe.js +6 -0
  90. auto_playwright_web-0.1.0/app/static/assets/javascript-CC0vVKQH.js +6 -0
  91. auto_playwright_web-0.1.0/app/static/assets/json.worker-CIZWV9vN.js +49 -0
  92. auto_playwright_web-0.1.0/app/static/assets/jsonMode-mEWbQgVL.js +15 -0
  93. auto_playwright_web-0.1.0/app/static/assets/julia-Bqgm2twL.js +6 -0
  94. auto_playwright_web-0.1.0/app/static/assets/kotlin-BSkB5QuD.js +6 -0
  95. auto_playwright_web-0.1.0/app/static/assets/less-BsTHnhdd.js +7 -0
  96. auto_playwright_web-0.1.0/app/static/assets/lexon-YWi4-JPR.js +6 -0
  97. auto_playwright_web-0.1.0/app/static/assets/liquid-b8gV0sP_.js +6 -0
  98. auto_playwright_web-0.1.0/app/static/assets/lua-nf6ki56Z.js +6 -0
  99. auto_playwright_web-0.1.0/app/static/assets/m3-Cpb6xl2v.js +6 -0
  100. auto_playwright_web-0.1.0/app/static/assets/markdown-DSZPf7rp.js +6 -0
  101. auto_playwright_web-0.1.0/app/static/assets/mdx-Bhwfss4Q.js +6 -0
  102. auto_playwright_web-0.1.0/app/static/assets/mips-B_c3zf-v.js +6 -0
  103. auto_playwright_web-0.1.0/app/static/assets/msdax-rUNN04Wq.js +6 -0
  104. auto_playwright_web-0.1.0/app/static/assets/mysql-DDwshQtU.js +6 -0
  105. auto_playwright_web-0.1.0/app/static/assets/objective-c-B5zXfXm9.js +6 -0
  106. auto_playwright_web-0.1.0/app/static/assets/pascal-CXOwvkN_.js +6 -0
  107. auto_playwright_web-0.1.0/app/static/assets/pascaligo-Bc-ZgV77.js +6 -0
  108. auto_playwright_web-0.1.0/app/static/assets/perl-CwNk8-XU.js +6 -0
  109. auto_playwright_web-0.1.0/app/static/assets/pgsql-tGk8EFnU.js +6 -0
  110. auto_playwright_web-0.1.0/app/static/assets/php-CpIb_Oan.js +6 -0
  111. auto_playwright_web-0.1.0/app/static/assets/pla-B03wrqEc.js +6 -0
  112. auto_playwright_web-0.1.0/app/static/assets/postiats-BKlk5iyT.js +6 -0
  113. auto_playwright_web-0.1.0/app/static/assets/powerquery-Bhzvs7bI.js +6 -0
  114. auto_playwright_web-0.1.0/app/static/assets/powershell-Dd3NCNK9.js +6 -0
  115. auto_playwright_web-0.1.0/app/static/assets/protobuf-COyEY5Pt.js +7 -0
  116. auto_playwright_web-0.1.0/app/static/assets/pug-BaJupSGV.js +6 -0
  117. auto_playwright_web-0.1.0/app/static/assets/python-CYyzhGkK.js +6 -0
  118. auto_playwright_web-0.1.0/app/static/assets/qsharp-DXyYeYxl.js +6 -0
  119. auto_playwright_web-0.1.0/app/static/assets/r-CdQndTaG.js +6 -0
  120. auto_playwright_web-0.1.0/app/static/assets/razor-BrDHW0IA.js +6 -0
  121. auto_playwright_web-0.1.0/app/static/assets/redis-CVwtpugi.js +6 -0
  122. auto_playwright_web-0.1.0/app/static/assets/redshift-25W9uPmb.js +6 -0
  123. auto_playwright_web-0.1.0/app/static/assets/restructuredtext-DfzH4Xui.js +6 -0
  124. auto_playwright_web-0.1.0/app/static/assets/ruby-Cp1zYvxS.js +6 -0
  125. auto_playwright_web-0.1.0/app/static/assets/rust-D5C2fndG.js +6 -0
  126. auto_playwright_web-0.1.0/app/static/assets/sb-CDntyWJ8.js +6 -0
  127. auto_playwright_web-0.1.0/app/static/assets/scala-BoFRg7Ot.js +6 -0
  128. auto_playwright_web-0.1.0/app/static/assets/scheme-Bio4gycK.js +6 -0
  129. auto_playwright_web-0.1.0/app/static/assets/scss-4Ik7cdeQ.js +8 -0
  130. auto_playwright_web-0.1.0/app/static/assets/shell-CX-rkNHf.js +6 -0
  131. auto_playwright_web-0.1.0/app/static/assets/solidity-Tw7wswEv.js +6 -0
  132. auto_playwright_web-0.1.0/app/static/assets/sophia-C5WLch3f.js +6 -0
  133. auto_playwright_web-0.1.0/app/static/assets/sparql-DHaeiCBh.js +6 -0
  134. auto_playwright_web-0.1.0/app/static/assets/sql-CCSDG5nI.js +6 -0
  135. auto_playwright_web-0.1.0/app/static/assets/st-pnP8ivHi.js +6 -0
  136. auto_playwright_web-0.1.0/app/static/assets/swift-DwJ7jVG9.js +8 -0
  137. auto_playwright_web-0.1.0/app/static/assets/systemverilog-B9Xyijhd.js +6 -0
  138. auto_playwright_web-0.1.0/app/static/assets/tcl-DnHyzjbg.js +6 -0
  139. auto_playwright_web-0.1.0/app/static/assets/tsMode-CNwb3tei.js +16 -0
  140. auto_playwright_web-0.1.0/app/static/assets/twig-CPajHgWi.js +6 -0
  141. auto_playwright_web-0.1.0/app/static/assets/typescript-CE4jKLq-.js +6 -0
  142. auto_playwright_web-0.1.0/app/static/assets/typespec-D-MeaMDU.js +6 -0
  143. auto_playwright_web-0.1.0/app/static/assets/vb-DgyLZaXg.js +6 -0
  144. auto_playwright_web-0.1.0/app/static/assets/wgsl-BIv9DU6q.js +303 -0
  145. auto_playwright_web-0.1.0/app/static/assets/xml-BRP0X2sF.js +6 -0
  146. auto_playwright_web-0.1.0/app/static/assets/yaml-O7uTeIlb.js +6 -0
  147. auto_playwright_web-0.1.0/app/static/favicon.svg +5 -0
  148. auto_playwright_web-0.1.0/app/static/index.html +14 -0
  149. auto_playwright_web-0.1.0/auto_playwright_web.egg-info/PKG-INFO +68 -0
  150. auto_playwright_web-0.1.0/auto_playwright_web.egg-info/SOURCES.txt +161 -0
  151. auto_playwright_web-0.1.0/auto_playwright_web.egg-info/dependency_links.txt +1 -0
  152. auto_playwright_web-0.1.0/auto_playwright_web.egg-info/entry_points.txt +2 -0
  153. auto_playwright_web-0.1.0/auto_playwright_web.egg-info/requires.txt +11 -0
  154. auto_playwright_web-0.1.0/auto_playwright_web.egg-info/top_level.txt +1 -0
  155. auto_playwright_web-0.1.0/pyproject.toml +32 -0
  156. auto_playwright_web-0.1.0/setup.cfg +4 -0
  157. auto_playwright_web-0.1.0/tests/test_cli.py +24 -0
  158. auto_playwright_web-0.1.0/tests/test_e2e.py +75 -0
  159. auto_playwright_web-0.1.0/tests/test_execution_file_first.py +74 -0
  160. auto_playwright_web-0.1.0/tests/test_script_loader.py +73 -0
  161. auto_playwright_web-0.1.0/tests/test_sqlite_repository.py +93 -0
  162. auto_playwright_web-0.1.0/tests/test_static_spa.py +23 -0
  163. auto_playwright_web-0.1.0/tests/test_streamlined_api.py +80 -0
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: auto-playwright-web
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Requires-Python: <3.13,>=3.12
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: aiosqlite>=0.20.0
8
+ Requires-Dist: apscheduler>=3.10.4
9
+ Requires-Dist: fastapi>=0.115.0
10
+ Requires-Dist: httpx>=0.27.0
11
+ Requires-Dist: loguru>=0.7.2
12
+ Requires-Dist: playwright>=1.45.0
13
+ Requires-Dist: pytest>=8.2.0
14
+ Requires-Dist: pytest-asyncio>=0.24.0
15
+ Requires-Dist: python-dotenv>=1.0.1
16
+ Requires-Dist: pyyaml>=6.0.1
17
+ Requires-Dist: uvicorn>=0.30.0
18
+
19
+ # Auto Playwright Web
20
+
21
+ 多用户 Playwright 脚本管理平台,后端使用 FastAPI,前端使用 Vite + Vue 3 + Element Plus + Monaco Editor。
22
+
23
+ ## 默认账号
24
+
25
+ - 管理员:`admin / admin123`
26
+ - 测试内存仓储还包含普通用户:`demo / demo123`
27
+
28
+ 生产 MySQL 首次初始化请先创建业务库,再执行:
29
+
30
+ ```bash
31
+ mysql -uroot -p123456 auto_playwright_web < app/scripts/init_db.sql
32
+ ```
33
+
34
+ ## 本地开发
35
+
36
+ ```bash
37
+ uv sync
38
+ uv run uvicorn app.main:app --reload
39
+ cd frontend
40
+ npm install
41
+ npm run dev
42
+ ```
43
+
44
+ 默认 API 前缀为 `/api`,前端开发服务器会代理到 `http://127.0.0.1:8000`。
45
+
46
+ ## AI 生成脚本
47
+
48
+ 管理员可以在脚本编辑抽屉里点击“AI 生成”,根据需求自动生成完整脚本草稿,包括脚本编码、名称、参数定义、超时时间和 Python Playwright 源码。后端不使用 OpenAI SDK,而是通过 HTTP 调用 Chat Completions 接口,并以流式响应填充前端编辑器。启用前需要配置:
49
+
50
+ ```bash
51
+ OPENAI_API_KEY=你的 OpenAI API Key
52
+ OPENAI_MODEL=gpt-5.4-mini
53
+ OPENAI_BASE_URL=https://api.openai.com/v1
54
+ ```
55
+
56
+ `OPENAI_MODEL` 和 `OPENAI_BASE_URL` 都可以按部署环境覆盖。
57
+
58
+ ## 脚本约定
59
+
60
+ 脚本由管理员维护,所有用户共享。脚本入口必须为:
61
+
62
+ ```python
63
+ async def main(params: dict) -> dict:
64
+ print("print 输出会进入 stdout_text")
65
+ return {"ok": True}
66
+ ```
67
+
68
+ 建议脚本使用 `logging` 输出排查日志,平台同时兼容 `print`。`print` 会进入 stdout,`logging` 默认进入 stderr。
@@ -0,0 +1,50 @@
1
+ # Auto Playwright Web
2
+
3
+ 多用户 Playwright 脚本管理平台,后端使用 FastAPI,前端使用 Vite + Vue 3 + Element Plus + Monaco Editor。
4
+
5
+ ## 默认账号
6
+
7
+ - 管理员:`admin / admin123`
8
+ - 测试内存仓储还包含普通用户:`demo / demo123`
9
+
10
+ 生产 MySQL 首次初始化请先创建业务库,再执行:
11
+
12
+ ```bash
13
+ mysql -uroot -p123456 auto_playwright_web < app/scripts/init_db.sql
14
+ ```
15
+
16
+ ## 本地开发
17
+
18
+ ```bash
19
+ uv sync
20
+ uv run uvicorn app.main:app --reload
21
+ cd frontend
22
+ npm install
23
+ npm run dev
24
+ ```
25
+
26
+ 默认 API 前缀为 `/api`,前端开发服务器会代理到 `http://127.0.0.1:8000`。
27
+
28
+ ## AI 生成脚本
29
+
30
+ 管理员可以在脚本编辑抽屉里点击“AI 生成”,根据需求自动生成完整脚本草稿,包括脚本编码、名称、参数定义、超时时间和 Python Playwright 源码。后端不使用 OpenAI SDK,而是通过 HTTP 调用 Chat Completions 接口,并以流式响应填充前端编辑器。启用前需要配置:
31
+
32
+ ```bash
33
+ OPENAI_API_KEY=你的 OpenAI API Key
34
+ OPENAI_MODEL=gpt-5.4-mini
35
+ OPENAI_BASE_URL=https://api.openai.com/v1
36
+ ```
37
+
38
+ `OPENAI_MODEL` 和 `OPENAI_BASE_URL` 都可以按部署环境覆盖。
39
+
40
+ ## 脚本约定
41
+
42
+ 脚本由管理员维护,所有用户共享。脚本入口必须为:
43
+
44
+ ```python
45
+ async def main(params: dict) -> dict:
46
+ print("print 输出会进入 stdout_text")
47
+ return {"ok": True}
48
+ ```
49
+
50
+ 建议脚本使用 `logging` 输出排查日志,平台同时兼容 `print`。`print` 会进入 stdout,`logging` 默认进入 stderr。
@@ -0,0 +1 @@
1
+ """Playwright 脚本管理平台后端包。"""
@@ -0,0 +1,161 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import asyncio
5
+ import json
6
+ import os
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ __version__ = "0.1.0"
11
+
12
+
13
+ def init_workspace(target_dir: Path | str = ".") -> None:
14
+ """初始化标准工作区目录与示例脚本。"""
15
+ ws = Path(target_dir).resolve()
16
+ scripts_dir = ws / "scripts"
17
+ common_dir = ws / "common"
18
+ storage_dir = ws / ".storage"
19
+
20
+ scripts_dir.mkdir(parents=True, exist_ok=True)
21
+ common_dir.mkdir(parents=True, exist_ok=True)
22
+ storage_dir.mkdir(parents=True, exist_ok=True)
23
+
24
+ # 写入 common/__init__.py
25
+ init_py = common_dir / "__init__.py"
26
+ if not init_py.exists():
27
+ init_py.write_text("# 公共工具包\n", encoding="utf-8")
28
+
29
+ # 写入 .env.example
30
+ env_example = ws / ".env.example"
31
+ if not env_example.exists():
32
+ env_example.write_text(
33
+ "# 工作区环境变量模板\n# APW_API_KEY=your_secret_key\n# BILIBILI_USERNAME=demo\n# BILIBILI_PASSWORD=secret\n",
34
+ encoding="utf-8",
35
+ )
36
+
37
+ # 写入示例脚本 scripts/example.py
38
+ example_script = scripts_dir / "example.py"
39
+ if not example_script.exists():
40
+ example_script.write_text(
41
+ '''"""
42
+ ---
43
+ name: 示例演示任务
44
+ timeout: 30
45
+ params:
46
+ keyword:
47
+ type: string
48
+ default: "Playwright"
49
+ description: 搜索关键字
50
+ ---
51
+ """
52
+ async def main(params: dict) -> dict:
53
+ keyword = params.get("keyword", "")
54
+ print(f"[example] 正在执行任务,关键字: {keyword}")
55
+ return {"ok": True, "keyword": keyword}
56
+
57
+ if __name__ == "__main__":
58
+ import asyncio
59
+ asyncio.run(main({"keyword": "LocalTest"}))
60
+ ''',
61
+ encoding="utf-8",
62
+ )
63
+ print(f"✅ 工作区初始化完成: {ws}")
64
+
65
+
66
+ def start_server(host: str, port: int, workdir: str) -> None:
67
+ """启动 Web 界面与 API 守护进程。"""
68
+ ws = Path(workdir).resolve()
69
+ os.environ["SCRIPTS_DIR"] = str(ws / "scripts")
70
+ os.environ["DB_PATH"] = str(ws / "data.db")
71
+ os.environ["RUNNER_WORKSPACE"] = str(ws / ".runner")
72
+
73
+ import uvicorn
74
+ from app.main import create_app
75
+
76
+ app = create_app(
77
+ db_path=ws / "data.db",
78
+ scripts_dir=ws / "scripts",
79
+ runner_workspace=ws / ".runner",
80
+ )
81
+ print(f"🚀 auto-playwright-web 启动中: http://{host}:{port}")
82
+ uvicorn.run(app, host=host, port=port)
83
+
84
+
85
+ def run_script_cli(script_code: str, params_str: str, workdir: str) -> None:
86
+ """在终端直接触发指定脚本执行。"""
87
+ ws = Path(workdir).resolve()
88
+ from app.repositories.sqlite import SQLiteRepository
89
+ from app.service.execution_service import ExecutionService
90
+ from app.service.script_loader import ScriptLoader
91
+
92
+ repo = SQLiteRepository(ws / "data.db")
93
+ loader = ScriptLoader(ws / "scripts")
94
+ service = ExecutionService(
95
+ repository=repo,
96
+ script_loader=loader,
97
+ runner_workspace=ws / ".runner",
98
+ workspace_dir=ws,
99
+ )
100
+
101
+ try:
102
+ params = json.loads(params_str) if params_str else {}
103
+ except Exception as e:
104
+ print(f"❌ 参数解析失败,请提供合法的 JSON 字符串: {e}", file=sys.stderr)
105
+ sys.exit(1)
106
+
107
+ async def _execute():
108
+ await repo.init_db()
109
+ execution = await service.execute_sync(script_code, params, source="cli")
110
+ print("---------- 执行结果 ----------")
111
+ print(f"状态: {execution.status}")
112
+ print(f"耗时: {execution.duration_ms}ms")
113
+ if execution.stdout_text:
114
+ print(f"输出:\n{execution.stdout_text}")
115
+ if execution.result_json:
116
+ print(f"返回值:\n{json.dumps(execution.result_json, ensure_ascii=False, indent=2)}")
117
+ if execution.error_stack:
118
+ print(f"异常堆栈:\n{execution.error_stack}", file=sys.stderr)
119
+
120
+ asyncio.run(_execute())
121
+
122
+
123
+ def main() -> None:
124
+ parser = argparse.ArgumentParser(prog="apw", description="Auto-Playwright-Web 命令行工具")
125
+ subparsers = parser.add_subparsers(dest="subcommand", help="子命令")
126
+
127
+ # start
128
+ p_start = subparsers.add_parser("start", help="启动 Web 界面与 API 服务")
129
+ p_start.add_argument("--host", default="0.0.0.0", help="监听地址 (默认 0.0.0.0)")
130
+ p_start.add_argument("--port", type=int, default=8000, help="端口 (默认 8000)")
131
+ p_start.add_argument("--workdir", default=".", help="工作区目录 (默认当前目录)")
132
+
133
+ # init
134
+ p_init = subparsers.add_parser("init", help="初始化标准工作区")
135
+ p_init.add_argument("--workdir", default=".", help="工作区目录 (默认当前目录)")
136
+
137
+ # run
138
+ p_run = subparsers.add_parser("run", help="命令行直接执行脚本")
139
+ p_run.add_argument("script_code", help="脚本编码")
140
+ p_run.add_argument("--params", default="{}", help="入参 JSON 字符串")
141
+ p_run.add_argument("--workdir", default=".", help="工作区目录 (默认当前目录)")
142
+
143
+ # version
144
+ subparsers.add_parser("version", help="查看当前版本")
145
+
146
+ args = parser.parse_args()
147
+
148
+ if args.subcommand == "start":
149
+ start_server(args.host, args.port, args.workdir)
150
+ elif args.subcommand == "init":
151
+ init_workspace(args.workdir)
152
+ elif args.subcommand == "run":
153
+ run_script_cli(args.script_code, args.params, args.workdir)
154
+ elif args.subcommand == "version":
155
+ print(f"auto-playwright-web v{__version__}")
156
+ else:
157
+ parser.print_help()
158
+
159
+
160
+ if __name__ == "__main__":
161
+ main()
@@ -0,0 +1 @@
1
+ """配置模块:按运行环境组织应用配置。"""
@@ -0,0 +1,19 @@
1
+ import os
2
+ from dataclasses import dataclass
3
+
4
+
5
+ @dataclass(slots=True)
6
+ class Settings:
7
+ """应用配置。"""
8
+
9
+ app_name: str = os.getenv("APP_NAME", "auto-playwright-web")
10
+ app_env: str = os.getenv("APP_ENV", "prod")
11
+ enable_docs: bool = os.getenv("APP_ENABLE_DOCS", "true").lower() == "true"
12
+ api_key: str = os.getenv("APW_API_KEY", "")
13
+ runner_workspace: str = os.getenv("RUNNER_WORKSPACE", ".runner")
14
+ scripts_dir: str = os.getenv("SCRIPTS_DIR", "scripts")
15
+ db_path: str = os.getenv("DB_PATH", "data.db")
16
+ max_concurrent_executions: int = int(os.getenv("MAX_CONCURRENT_EXECUTIONS", "3"))
17
+
18
+
19
+ settings = Settings()
@@ -0,0 +1,3 @@
1
+ from app.configs.base import Settings, settings
2
+
3
+ __all__ = ["Settings", "settings"]
@@ -0,0 +1,123 @@
1
+ from __future__ import annotations
2
+
3
+ from fastapi import Depends, Header, HTTPException, Request, status
4
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
5
+
6
+ from app.domain import User
7
+ from app.repositories.base import Repository
8
+ from app.security import decode_access_token
9
+ from app.service.auth_service import AuthService
10
+ from app.service.ai_service import AIScriptService
11
+ from app.service.common_library_service import CommonLibraryService
12
+ from app.service.execution_service import ExecutionService
13
+ from app.service.schedule_service import ScheduleService
14
+ from app.service.script_service import ScriptService
15
+ from app.service.token_service import TokenService
16
+ from app.service.user_env_var_service import UserEnvVarService
17
+ from app.service.webhook_service import WebhookService
18
+
19
+ bearer_scheme = HTTPBearer(auto_error=False)
20
+
21
+
22
+ def get_repository(request: Request) -> Repository:
23
+ """从应用状态获取仓储。"""
24
+
25
+ return request.app.state.repository
26
+
27
+
28
+ def get_auth_service(request: Request) -> AuthService:
29
+ """从应用状态获取认证服务。"""
30
+
31
+ return request.app.state.auth_service
32
+
33
+
34
+ def get_ai_script_service(request: Request) -> AIScriptService:
35
+ """从应用状态获取 AI 脚本生成服务。"""
36
+
37
+ return request.app.state.ai_script_service
38
+
39
+
40
+ def get_script_service(request: Request) -> ScriptService:
41
+ """从应用状态获取脚本服务。"""
42
+
43
+ return request.app.state.script_service
44
+
45
+
46
+ def get_common_library_service(request: Request) -> CommonLibraryService:
47
+ """从应用状态获取公共函数库服务。"""
48
+
49
+ return request.app.state.common_library_service
50
+
51
+
52
+ def get_execution_service(request: Request) -> ExecutionService:
53
+ """从应用状态获取执行服务。"""
54
+
55
+ return request.app.state.execution_service
56
+
57
+
58
+ def get_token_service(request: Request) -> TokenService:
59
+ """从应用状态获取 Token 服务。"""
60
+
61
+ return request.app.state.token_service
62
+
63
+
64
+ def get_schedule_service(request: Request) -> ScheduleService:
65
+ """从应用状态获取定时任务服务。"""
66
+
67
+ return request.app.state.schedule_service
68
+
69
+
70
+ def get_webhook_service(request: Request) -> WebhookService:
71
+ """从应用状态获取 Webhook 服务。"""
72
+
73
+ return request.app.state.webhook_service
74
+
75
+
76
+ def get_user_env_var_service(request: Request) -> UserEnvVarService:
77
+ """从应用状态获取用户环境变量服务。"""
78
+
79
+ return request.app.state.user_env_var_service
80
+
81
+
82
+ async def get_current_user(
83
+ credentials: HTTPAuthorizationCredentials | None = Depends(bearer_scheme),
84
+ auth_service: AuthService = Depends(get_auth_service),
85
+ ) -> User:
86
+ """解析 Bearer JWT 并返回当前用户。"""
87
+
88
+ if not credentials:
89
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="请先登录")
90
+ payload = decode_access_token(credentials.credentials)
91
+ return await auth_service.get_user(int(payload["sub"]))
92
+
93
+
94
+ async def get_open_api_token(x_api_token: str | None = Header(default=None)) -> str:
95
+ """读取开放 API Token 请求头。"""
96
+
97
+ if not x_api_token:
98
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="缺少 X-API-Token")
99
+ return x_api_token
100
+
101
+
102
+ async def get_current_user_or_token(
103
+ request: Request,
104
+ credentials: HTTPAuthorizationCredentials | None = Depends(bearer_scheme),
105
+ x_api_token: str | None = Header(default=None),
106
+ auth_service: AuthService = Depends(get_auth_service),
107
+ token_service: TokenService = Depends(get_token_service),
108
+ ) -> User:
109
+ """统一认证:优先 Bearer JWT,其次 X-API-Token,并校验接口授权范围。"""
110
+
111
+ if credentials:
112
+ payload = decode_access_token(credentials.credentials)
113
+ return await auth_service.get_user(int(payload["sub"]))
114
+
115
+ if x_api_token:
116
+ user, scopes = await token_service.authenticate_token_scopes(x_api_token)
117
+ if scopes:
118
+ route_key = f"{request.method}:{request.scope['route'].path}"
119
+ if route_key not in scopes:
120
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="API Token 无权访问此接口")
121
+ return user
122
+
123
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="请先登录或提供 API Token")
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from typing import Any
6
+
7
+
8
+ def now() -> datetime:
9
+ """返回当前时间。"""
10
+ return datetime.now()
11
+
12
+
13
+ @dataclass(slots=True)
14
+ class Execution:
15
+ """脚本执行记录。"""
16
+
17
+ id: int
18
+ execution_id: str
19
+ script_id: int
20
+ script_code: str
21
+ user_id: int = 1
22
+ source: str = "manual"
23
+ params_json: dict[str, Any] = field(default_factory=dict)
24
+ status: str = "pending"
25
+ result_json: dict[str, Any] | None = None
26
+ stdout_text: str = ""
27
+ stderr_text: str = ""
28
+ error_stack: str = ""
29
+ duration_ms: int = 0
30
+ started_time: datetime | None = None
31
+ finished_time: datetime | None = None
32
+ create_time: datetime = field(default_factory=now)
33
+ update_time: datetime = field(default_factory=now)
34
+ create_by: str = ""
35
+ update_by: str = ""
36
+
37
+
38
+ @dataclass(slots=True)
39
+ class Schedule:
40
+ """定时任务。"""
41
+
42
+ id: int
43
+ user_id: int
44
+ script_id: int
45
+ script_code: str
46
+ name: str
47
+ cron_expr: str
48
+ params_json: dict[str, Any]
49
+ enabled: bool = True
50
+ last_run_time: datetime | None = None
51
+ next_run_time: datetime | None = None
52
+ create_time: datetime = field(default_factory=now)
53
+ update_time: datetime = field(default_factory=now)
54
+ create_by: str = ""
55
+ update_by: str = ""
56
+
57
+
58
+ @dataclass(slots=True)
59
+ class User:
60
+ """单用户身份兼容。"""
61
+
62
+ id: int = 1
63
+ username: str = "admin"
64
+ role: str = "admin"
65
+ enabled: bool = True
@@ -0,0 +1,110 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from contextlib import asynccontextmanager
5
+ from pathlib import Path
6
+
7
+ from dotenv import load_dotenv
8
+ from fastapi import FastAPI, HTTPException, Request, status
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+ from fastapi.responses import FileResponse, JSONResponse
11
+ from fastapi.staticfiles import StaticFiles
12
+
13
+ from app.configs.config import settings
14
+ from app.repositories.sqlite import SQLiteRepository
15
+ from app.routers import executions, open_api, schedules, scripts, user_env_vars
16
+ from app.scheduler import PlatformScheduler
17
+ from app.service.execution_service import ExecutionService
18
+ from app.service.script_loader import ScriptLoader
19
+
20
+
21
+ def create_app(
22
+ db_path: Path | str | None = None,
23
+ scripts_dir: Path | str | None = None,
24
+ runner_workspace: Path | str | None = None,
25
+ start_scheduler: bool = True,
26
+ ) -> FastAPI:
27
+ """创建并装配轻量级 FastAPI 应用。"""
28
+
29
+ # 加载本地 .env
30
+ load_dotenv()
31
+
32
+ resolved_db_path = Path(db_path or settings.db_path)
33
+ resolved_scripts_dir = Path(scripts_dir or settings.scripts_dir)
34
+ resolved_workspace = Path(runner_workspace or settings.runner_workspace)
35
+
36
+ repository = SQLiteRepository(resolved_db_path)
37
+ script_loader = ScriptLoader(resolved_scripts_dir)
38
+ execution_service = ExecutionService(
39
+ repository=repository,
40
+ script_loader=script_loader,
41
+ runner_workspace=resolved_workspace,
42
+ max_concurrent_executions=settings.max_concurrent_executions,
43
+ )
44
+ platform_scheduler = PlatformScheduler(repository, execution_service)
45
+
46
+ @asynccontextmanager
47
+ async def lifespan(app: FastAPI):
48
+ await repository.init_db()
49
+ if start_scheduler:
50
+ await platform_scheduler.start()
51
+ try:
52
+ yield
53
+ finally:
54
+ await platform_scheduler.stop()
55
+
56
+ app = FastAPI(
57
+ title=settings.app_name,
58
+ docs_url="/docs" if settings.enable_docs else None,
59
+ redoc_url="/redoc" if settings.enable_docs else None,
60
+ openapi_url="/openapi.json" if settings.enable_docs else None,
61
+ lifespan=lifespan,
62
+ )
63
+
64
+ # 全局 API Key 拦截(如果配置了 APW_API_KEY)
65
+ @app.middleware("http")
66
+ async def api_key_auth_middleware(request: Request, call_next):
67
+ api_key = settings.api_key or os.getenv("APW_API_KEY", "")
68
+ if api_key and request.url.path.startswith("/api/"):
69
+ client_key = request.headers.get("X-API-Key") or request.query_params.get("api_key")
70
+ if client_key != api_key:
71
+ return JSONResponse(status_code=401, content={"detail": "无效的 API Key"})
72
+ return await call_next(request)
73
+
74
+ app.add_middleware(
75
+ CORSMiddleware,
76
+ allow_origins=["*"],
77
+ allow_credentials=True,
78
+ allow_methods=["*"],
79
+ allow_headers=["*"],
80
+ )
81
+
82
+ app.state.repository = repository
83
+ app.state.script_loader = script_loader
84
+ app.state.execution_service = execution_service
85
+ app.state.platform_scheduler = platform_scheduler
86
+
87
+ app.include_router(scripts.router)
88
+ app.include_router(open_api.router)
89
+ app.include_router(executions.router)
90
+ app.include_router(schedules.router)
91
+ app.include_router(user_env_vars.router)
92
+
93
+ # 挂载内置静态前端(若存在)
94
+ static_dir = Path(__file__).parent / "static"
95
+ if static_dir.exists():
96
+ app.mount("/assets", StaticFiles(directory=static_dir / "assets"), name="assets")
97
+
98
+ @app.get("/{full_path:path}", include_in_schema=False)
99
+ async def serve_spa(full_path: str):
100
+ if full_path.startswith("api/") or full_path.startswith("docs") or full_path.startswith("openapi"):
101
+ raise HTTPException(status_code=404, detail="Not Found")
102
+ target = static_dir / full_path
103
+ if target.is_file():
104
+ return FileResponse(target)
105
+ return FileResponse(static_dir / "index.html")
106
+
107
+ return app
108
+
109
+
110
+ app = create_app()
@@ -0,0 +1 @@
1
+ """仓储模块:隔离业务逻辑与持久化实现。"""
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import Protocol
5
+
6
+ from app.domain import Execution, Schedule
7
+
8
+
9
+ class Repository(Protocol):
10
+ """仓储接口协议:SQLiteRepository 实现此协议。"""
11
+
12
+ async def init_db(self) -> None: ...
13
+
14
+ async def create_execution(self, execution: Execution) -> Execution: ...
15
+
16
+ async def update_execution(self, execution: Execution) -> Execution: ...
17
+
18
+ async def get_execution(self, execution_id: str | int) -> Execution | None: ...
19
+
20
+ async def list_executions(
21
+ self,
22
+ script_code: str | None = None,
23
+ start_time: datetime | None = None,
24
+ end_time: datetime | None = None,
25
+ limit: int = 100,
26
+ ) -> list[Execution]: ...
27
+
28
+ async def create_schedule(self, schedule: Schedule) -> Schedule: ...
29
+
30
+ async def update_schedule(self, schedule_id: int, schedule: Schedule) -> Schedule: ...
31
+
32
+ async def delete_schedule(self, schedule_id: int) -> None: ...
33
+
34
+ async def get_schedule(self, schedule_id: int) -> Schedule | None: ...
35
+
36
+ async def list_schedules(self) -> list[Schedule]: ...
37
+
38
+ async def list_enabled_schedules(self) -> list[Schedule]: ...