This commit is contained in:
caoqianming 2026-05-06 13:51:58 +08:00
commit 6d2f2a452c
14 changed files with 3214 additions and 246 deletions

View File

@ -0,0 +1,20 @@
{
"permissions": {
"allow": [
"Bash(venv/Scripts/python.exe -c \":*)",
"Bash(rm -f \"media/Structural Concrete - 2022 - Faleschini - Reliabilitybased design of transmission and anchorage lengths in prestressed.pdf\")",
"Bash(rm -f media/test_hindawi.pdf)",
"Bash(python:*)",
"Bash(source venv/Scripts/activate)",
"Bash(echo \"http_proxy=$http_proxy\")",
"Bash(echo \"https_proxy=$https_proxy\")",
"Bash(echo \"HTTP_PROXY=$HTTP_PROXY\")",
"Bash(echo \"HTTPS_PROXY=$HTTPS_PROXY\")",
"Bash(echo \"all_proxy=$all_proxy\")",
"Read(//c/code/paper_server/venv/Scripts/**)",
"Read(//c/code/paper_server/.venv/Scripts/**)",
"Bash(source C:/code/paper_server/paper_server/venv/Scripts/activate)",
"Bash(/c/software/python3_10/python -c \"import django; print\\(django.__version__\\)\")"
]
}
}

233
CLAUDE.md Normal file
View File

@ -0,0 +1,233 @@
# paper_server
## What This Repo Is
`paper_server` is a Django 4.2 backend that mixes a general admin platform with a paper/resource acquisition pipeline.
Main stack:
- Django + DRF
- PostgreSQL
- Redis cache
- Celery + django-celery-beat + django-celery-results
- Channels + Daphne for WebSocket push
The repo is not just a "paper service". It contains four major business areas:
- `apps.system`: users, departments, roles, permissions, files, schedules, config
- `apps.auth1`: login/auth flows based on JWT, session, SMS, WeChat, face login
- `apps.wf`: a configurable workflow/ticket engine
- `apps.ops`: ops endpoints for logs, backups, server metrics, cache, Celery, Redis
- `apps.resm`: paper metadata, abstract/fulltext fetch, PDF download pipeline
- `apps.utils`: shared base models, viewsets, permissions, middleware, pagination, helpers
- `apps.ws`: websocket consumers and routing
## Runtime Entry Points
- `manage.py` starts Django with `server.settings`
- `server/settings.py` is the central settings file and imports environment values from `config/conf.py`
- `server/urls.py` mounts all REST APIs, Swagger, Django admin, and the SPA entry (`dist/index.html`)
- `server/asgi.py` serves HTTP plus WebSocket traffic
- `server/celery.py` creates the Celery app using `config.conf.BASE_PROJECT_CODE`
## Environment And Config
This project expects local runtime config files under `config/`:
- `config/conf.py`: Django secret/config, database, cache, Celery broker, backup shell paths
- `config/conf.json`: runtime system config loaded through `server.settings.get_sysconfig()`
Important implication:
- the repo can start only when `config/conf.py` is valid for the target environment
- many ops tasks assume Linux paths from `BACKUP_PATH` and `SH_PATH`
- Redis is used by cache, Celery broker, and Channels
## URL Map
Primary REST prefixes:
- `api/auth/`
- `api/system/`
- `api/wf/`
- `api/ops/`
- `api/resm/`
- `api/utils/`
Other routes:
- `api/swagger/` and `api/redoc/`
- `django/admin/`
- `ws/my/`
- `ws/<room_name>/`
## Core Architectural Patterns
### Shared Base Models
`apps.utils.models` defines the core model layer:
- `BaseModel`: string primary key generated by Snowflake-style `idWorker`
- `SoftModel`: soft delete support
- `CommonAModel` / `CommonBModel`: standard audit fields
- `ParentModel`: tree-like parent linkage with a stored `parent_link`
Many business models inherit from these classes, so ID generation, soft deletion, and audit fields are cross-cutting behavior.
### Shared ViewSet Base
`apps.utils.viewsets.CustomGenericViewSet` is the main DRF base class. It adds:
- permission code registration through `perms_map`
- per-user/request cache protection for duplicate requests
- data-scope filtering based on RBAC and department range
- serializer switching per action
- `select_related` / `prefetch_related` hooks
- row locking behavior for mutable operations inside transactions
When adding endpoints, this class is usually the first place to check for inherited behavior.
### Auth And Permissions
- default auth uses JWT plus DRF basic/session fallbacks
- global default permission is authenticated + `apps.utils.permission.RbacPermission`
- custom user model is `apps.system.models.User`
- websocket auth is handled in `apps.utils.middlewares.TokenAuthMiddleware` via `token` query param
## App Notes
### `apps.system`
This is the platform foundation layer.
Key models:
- `User`
- `Dept`
- `Role`
- `Permission`
- `Post` / `UserPost` / `PostRole`
- `Dictionary` / `DictType`
- `File`
- `MySchedule`
This app owns the RBAC structure used by the rest of the project.
### `apps.wf`
This app is a full workflow engine, not just a simple approval table.
Key models:
- `Workflow`
- `State`
- `Transition`
- `CustomField`
- `Ticket`
- `TicketFlow`
Important logic lives in `apps.wf.services.WfService`:
- initialize a workflow from its start state
- generate ticket serial numbers
- resolve next state from transition conditions
- resolve participants from person/role/dept/post/field/code/robot
- enforce handle permissions
- create transition logs
- send SMS notifications
- trigger robot tasks and on-reach hooks
When working on ticket behavior, read `apps/wf/services.py` before touching serializers or views.
### `apps.ops`
This app exposes runtime/maintenance APIs:
- git reload tasks
- database/media backup
- log browsing
- CPU/memory/disk inspection
- Celery info
- Redis info
- cache get/set
- DRF request log and third-party request log listing
Some behaviors depend on shell scripts and Linux-only paths from config.
### `apps.resm`
This is the paper pipeline.
Key model:
- `Paper`: stores DOI/OpenAlex metadata, OA flags, abstract/fulltext state, fetch status, failure reason, and local file save helpers
- `PaperAbstract`: separate abstract storage
The paper fetch pipeline in `apps/resm/tasks.py` currently includes:
- metadata ingestion from OpenAlex
- abstract/fulltext XML fetch from Elsevier
- PDF fetch from OA URL
- PDF fetch from OpenAlex content API
- PDF fetch from Elsevier
- Sci-Hub fallback
- task fan-out and stuck-download release
Download behavior is stateful:
- `fetch_status="downloading"` is used as a coarse lock
- `fail_reason` accumulates fetch failures
- files are stored under `media/papers/<year>/<month>/<day>/`
This app has recent local edits in the working tree, so read carefully before changing it.
### `apps.ws`
Two websocket patterns exist:
- `MyConsumer`: per-user channel (`user_<id>`) plus optional `event` group
- `RoomConsumer`: shared room chat group
The websocket layer depends on Redis-backed Channels and JWT token parsing in the query string.
## Startup Expectations
Typical local boot sequence:
1. Ensure `config/conf.py` and `config/conf.json` are present and valid.
2. Start PostgreSQL and Redis.
3. Install dependencies from `requirements.txt`.
4. Run `python manage.py migrate`.
5. Optionally run `python manage.py loaddata db.json`.
6. Start Django/Daphne.
7. Start Celery worker/beat separately if async tasks are needed.
## Important Caveats
- The repo currently has uncommitted user changes, especially under `apps/resm/`; do not revert them casually.
- `config/conf.py` contains environment-specific secrets and infrastructure paths; treat edits there as deployment-sensitive.
- Some source files display mojibake in this terminal because the project contains non-UTF8/legacy encoded Chinese comments, but the Python logic is still readable.
- `TokenAuthMiddleware` only proceeds when a token is present; websocket behavior without token is intentionally limited.
- `apps/resm/tasks.py` currently contains hard-coded third-party API credentials and source-specific logic; changing it needs extra caution.
## Good First Files To Read
- `server/settings.py`
- `server/urls.py`
- `apps/utils/models.py`
- `apps/utils/viewsets.py`
- `apps/system/models.py`
- `apps/wf/models.py`
- `apps/wf/services.py`
- `apps/resm/models.py`
- `apps/resm/tasks.py`
## Updating This File
Update `CLAUDE.md` when any of these change:
- startup/config entry points
- app/module boundaries
- workflow engine behavior
- paper download pipeline behavior
- shared base classes or permission patterns

363
after_click_no.html Normal file
View File

@ -0,0 +1,363 @@
<!DOCTYPE html><html><head>
<title translate="en:title">Sci-Hub: the article is not available through Sci-Hub. What can I do?</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
</head>
<body>
<style>
@import url('/styles/Atkinson.css');
@import url('/styles/Overpass.css');
html { font-size: 12px }
@media only screen and (min-width: 1400px) { html { font-size: 13px } }
@media only screen and (min-width: 1600px) { html { font-size: 14px } }
@media only screen and (min-width: 1800px) { html { font-size: 16px } }
@media only screen and (min-width: 2048px) { html { font-size: 18px } }
@media only screen and (max-height: 600px) { html { font-size: 10px } }
body {font-family: 'Atkinson Hyperlegible Mono', 'Overpass Mono', monospace; background-color: white; margin: 0; padding: 0 }
a {text-decoration: none; color: #c9211e}
fixed-width {margin: 0 auto; padding: 2.33rem; max-width: 2048px; overflow-x: hidden; display: block }
.vertical,
row-split { display: flex; flex-direction: column; align-items: center }
column-split { display: flex; align-items: flex-start; width: 100% }
column-center { flex-grow: 1 }
mobile-content { display: none }
mobile-content.wide column-split > * { margin-left: 2rem }
mobile-content.wide .picture { overflow: hidden; max-height: 30rem; }
block-rounded { border: dotted 2px #ccc; border-radius: 2rem; flex-grow: 1; margin: 0; place-items: center; display: grid; font-size: 1.33rem }
block-space { margin-top: 3rem; display: block }
header-small { font-size: 1.66rem; font-weight: bold; margin: 1rem; color: #c9211e}
block-rounded.info { display: block; padding: 2.88rem; font-size: 1.22rem }
block-rounded.info h1 { font-size: 1.66rem; color: #333; margin-bottom: 2rem; font-weight: bold; color: #333 }
block-date { display: block; padding: 1rem; margin-right: 1.88rem; max-width: fit-content; max-height: fit-content; text-align: center; color: white; border-radius: 1rem; font-size: 1.66rem; font-weight: bold; }
block-date.black { background-color: black; }
block-date.red { background-color: #c9211e; }
block-date.white { border: dotted 2px #c9211e; background-color: white; color: #c9211e }
block-date.gray { border: none; border-radius: 0; border-bottom: dotted 2px #333; background-color: white; color: #333; font-weight: normal; }
block-date .number { font-size: 2rem }
span.rotate { animation: spin 3s linear infinite; display: inline-block; font-size: 1.88rem; }
@keyframes spin { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }
.yes { display: inline-block }
.no { display: none }
@media only screen and (max-width: 1024px)
{
fixed-width { padding: 1.88rem }
column-split { justify-content: space-around; align-items: center }
mobile-content { display: block; margin: 3rem 3rem 0 0 }
}
@media only screen and (max-width: 640px)
{
fixed-width { padding: 0.88rem }
mobile-content.wide { display: none }
mobile-content { margin-right: 0; }
block-rounded { margin: 0 1rem; }
}
@media only screen and (max-width: 480px)
{
fixed-width { padding: 0 }
}
</style>
<script>
function local(id) { return document.querySelector(".messages ." + id).innerText }
</script>
<style type="text/css">
body { background-color: #eee; }
fixed-width { width: fit-content; padding: 6rem; max-width: 800px; background-color: white; border-radius: 2rem; border: dashed 4.8rem #eee }
.from img { border: dotted 2px #aaa; border-radius: 50%; width: 8rem }
.paper { margin: 3rem auto; width: fit-content; justify-items: left; padding: 2rem; border: dashed 2px #bbb; font-size: 1.22rem; background-color: #fafafa; }
.paper .title { font-size: 1.55rem; margin: 1rem 0 }
.paper .doi { margin-top: 2rem }
.paper a { color: #777 }
.paper a .arrow { display: none }
.paper a:hover { margin-left: 0.88rem; }
.paper a:hover .arrow { display: inline-block }
.message { margin-left: 3rem; font-size: 1.22rem; padding: 1rem; border: 0 }
.info { font-size: 1.22rem; border: 0; padding: 1rem 0 1rem 1rem; text-align: left; }
.info p { margin:0; padding: 0 }
.info a { display: inline-block; padding: 0.1rem 0.8rem; border-radius: 0.8rem; background-color: #c9211e; color: white; font-weight: bold }
.info a:hover { background-color: white; border: dashed 1px #c9211e; color: #c9211e }
.info b { color: #c9211e; }
.header { color: #c9211e; font-size: 3rem; font-weight: bold; margin-top: 2rem }
.openaccess { background: white; padding: 2rem; margin: 2rem 1rem 4rem 4rem; border: dashed 1px #aaa }
.openaccess a { background: none; color: #c9211e; padding: 0;; }
.openaccess a:hover { background-color: green; color: white; border-color: green; padding: 0.1rem 0.8rem; }
.similar { margin: 8rem 2rem 8rem 6rem; display: block }
.similar .header { font-size: 3.8rem; color: #c9211e; font-weight: bold; display: grid; align-items: center; text-align: center; }
.similar .explanation { font-size: 1.22rem; background-color: white; border-radius: 2rem; border: dashed 2.8rem #eee; padding: 3rem 0; margin-left: 4%; text-align: center; }
.similar .columns { display: flex; flex-wrap: wrap; justify-content: center; }
.similar .columns div { width: 41%; max-width: 1024px; min-width: 680px; }
.similar .list {display: flex; flex-wrap: wrap; max-width: 100vw; justify-content: center;}
.similar .article {display: block; margin: 4rem 4% 0 0; width: 41%; max-width: 1024px; min-width: 640px; height: fit-content; padding: 1.23rem; border: solid 2px #eee; border-radius: 1rem; box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px; color: black; text-decoration: none; background-color: white; }
.similar .article .time,
.similar .article .geo {color: #888; font-weight: bold; font-size: 1.23rem; width: fit-content}
.similar .article .title {font-size: 1.66rem; margin: 1rem 0 1rem 0}
.similar .article .journal {font-size: 1.33rem;}
.similar .article .year {font-size: 1.33rem;}
.similar .article .abstract {margin-top: 1.23rem; font-size: 1.22rem; text-align: justify;}
.similar .article:hover {border: dashed 2px #888}
@media only screen and (max-width: 1800px)
{
fixed-width { max-width: 640px; }
}
@media only screen and (max-width: 1600px)
{
.similar .header { margin-bottom: 3rem; }
}
</style>
<fixed-width>
<column-split>
<div class="from"><a href="/"><img src="/pictures/ravenround.gif"></a></div>
<block-rounded class="message" translate="en:message">Alas, the following paper is not yet available in my database:</block-rounded>
</column-split>
<block-rounded class="paper">
<div class="title">Comparative study of attenuation properties of some ternary borate glass systems</div>
<div>Experimental and Theoretical NANOTECHNOLOGY, 2017</div>
<div class="abstract"><jats:p>Some borate-based systems doped with sodium, zinc and lead are chosen to study gamma ray mass attenuation properties. Different parameters like mass attenuation, half value layer and mean free path have been analyzed at different photon energies. The values of molar volume have been estimated from density values to get idea regarding the compactness of the network structure. XCOM computer software has been employed to estimate mass attenuation coefficient at various energies. Further the values of...</jats:p></div>
<div class="doi"><a target="_blank" href="//doi.org/10.56053/1.3.123">10.56053/1.3.123 <span class="arrow"></span></a></div>
</block-rounded>
<block-rounded class="info recent" translate="en:recent">The paper is recent: it was published after 2021. Recent papers are mostly unavailable since automatic download doesn't work on them.</block-rounded>
<block-rounded class="info openaccess">
<p><span translate="en:openaccess">O! According to the OpenAlex index, the paper is open access and should be accessible for free on publisher website. You can try to download it</span> <a target="blank" href="https://etnano.com/index.php/en/article/download/18/12" translate="en:here">here</a>. <span translate="en:warning">But there is no guarantee, because this index often contains errors.</span></p>
</block-rounded>
<div class="header" translate="en:question">What can I do?</div>
<block-rounded class="info">
<p translate="en:todo">You can request such paper through <a target="blank" href="//sci-net.xyz/">Sci-Net platform</a>. Most papers, except some rare cases, are uploaded on request within a few minutes.</p>
<p style="margin-top: 2rem" translate="en:goal">...after request is solved, the paper will become available on Sci-Hub <b>for free to everyone</b>. Therefore, by using the platform you help Sci-Hub grow and increase the amount of research papers available open access.</p>
</block-rounded>
</fixed-width>
<div class="similar">
<div class="columns">
<div class="header" translate="en:similar">similar<br>articles</div>
<div class="explanation" translate="en:alternatives">There are a number of similar articles that are present in Sci-Hub database. You might find them interesting</div>
</div>
<div class="list">
<a class="article" target="blank" href="/10.1016/j.nimb.2004.05.016">
<div class="title">Comparative study of lead borate and bismuth lead borate glass systems as gamma-radiation shielding materials</div>
<div><span class="journal">Nuclear Instruments and Methods in Physics Research Section B: Beam Interactions with Materials and Atoms</span>, <span class="year">2004</span></div>
<div class="abstract">Gamma-ray mass attenuation coefficients have been measured experimentally and calculated theoretically for PbOB2O3 and Bi2O3PbOB2O3 glass systems using narrow beam transmission method. These values have been used to calculate half value layer (HVL) parameter. These parameters have also been calculated theoretically for some standard radiation shielding concretes at same energies. Effect of replacing lead by bismuth has been analyzed in terms of density, molar volume and mass attenuation...</div>
</a>
<a class="article" target="blank" href="/10.1590/1980-5373-mr-2018-0404">
<div class="title">Gamma Ray and FTIR Studies in Zinc Doped Lead Borate Glasses for Radiation Shielding Application</div>
<div><span class="journal">Materials Research</span>, <span class="year">2018</span></div>
<div class="abstract">Gamma ray shielding properties of borate glass samples containing oxides of lead and zinc are prepared by melt and quench technique and evaluated theoretically using XCOM computer software for gamma ray shielding properties. However, gamma ray shielding properties are discussed in terms of various calculated parameters such as half value layer, mean free path and mass attenuation coefficient. The calculated parameters are compared by the author with conventional shielding material concrete. FTIR studies...</div>
</a>
<a class="article" target="blank" href="/10.1016/j.jnucmat.2009.12.020">
<div class="title">Study on borate glass system containing with Bi2O3 and BaO for gamma-rays shielding materials: Comparison with PbO</div>
<div><span class="journal">Journal of Nuclear Materials</span>, <span class="year">2010</span></div>
<div class="abstract">In this work, the mass attenuation coefficients and shielding parameters of borate glass matrices containing with Bi2O3 and BaO have been investigated at 662 keV, and compare with PbO in same glass structure. The theoretical values were calculated by WinXCom software and compare with experiential data. The results found that the mass attenuation coefficients were increased with increasing of Bi2O3, BaO and PbO concentration, due to increase photoelectric absorption of all glass samples. However, Compton...</div>
</a>
<a class="article" target="blank" href="/10.1016/j.anucene.2013.08.012">
<div class="title">Investigation of lead borate glasses doped with aluminium oxide as gamma ray shielding materials</div>
<div><span class="journal">Annals of Nuclear Energy</span>, <span class="year">2014</span></div>
<div class="abstract">Gamma-ray attenuation coefficients of xPbO⋅(0.90 x)B2O3⋅0.10Al2O3 (x = 0.25, 0.30, 0.35, 0.40 and 0.45) glass system have been calculated with WinXCOM computer program developed by National Institute of Standards and Technology. Results have been further used to calculate half value layer and mean free path values. Gamma-ray shielding parameters of glass samples have been compared with standard nuclear radiation shielding concretes. The prepared glass samples have higher values of mass attenuation...</div>
</a>
<a class="article" target="blank" href="/10.1016/0168-583x(96)00196-6">
<div class="title">Gamma-ray attenuation coefficients in some heavy metal oxide borate glasses at 662 keV</div>
<div><span class="journal">Nuclear Instruments and Methods in Physics Research Section B: Beam Interactions with Materials and Atoms</span>, <span class="year">1996</span></div>
<div class="abstract">The linear attenuation coefficient (μ) and mass attenuation coefficients (μϱ) of glasses in three systems: xPbO(1 x)B2O3, 0.25PbO · xCdO(0.75 x)B2O3 and xBi2O3(1 x)B2O3 were measured at 662 keV. Appreciable variations were noted in the attenuation coefficients due to changes in the chemical composition of glasses. In addition to this, absorption cross-sections per atom were also calculated. A comparison of shielding properties of these glasses with standard shielding materials like lead,...</div>
</a>
<a class="article" target="blank" href="/10.1016/j.radmeas.2004.09.009">
<div class="title">Gamma-ray attenuation studies of glass system</div>
<div><span class="journal">Radiation Measurements</span>, <span class="year">2006</span></div>
<div class="abstract">Abstract PbO BaO B 2 O 3 glass system has been investigated in terms of molar mass, mass attenuation coefficient and half value layer parameters by using gamma-ray at 511,662 and 1274&nbsp;keV photon energies. Gamma-ray attenuation coefficients of the prepared glass samples have been compared with tabulations based upon the results of XCOM. Good agreement has been observed between experimental and theoretical tabulations. Our results have uncertainty less than 3%. Radiation shielding properties of the...</div>
</a>
<a class="article" target="blank" href="/10.1016/j.jnoncrysol.2017.06.001">
<div class="title">Investigation of structural, thermal properties and shielding parameters for multicomponent borate glasses for gamma and neutron radiation shielding applications</div>
<div><span class="journal">Journal of Non-Crystalline Solids</span>, <span class="year">2017</span></div>
<div class="abstract">Multicomponent borate glasses with the chemical composition (60 x) B2O310 Bi2O310 Al2O310 ZnO10 Li2O(x) Dy2O3 or Tb4O7 (x = 0.5 mol%), and (60 x y) B2O310 Bi2O310 Al2O310 ZnO10 Li2O(x) Dy2O3(y) Tb4O7 (x = 0.25, 0.5, 0.75, 1.0, 1.5, and 2.0 mol%, y = 0.5 mol%) have been fabricated by a conventional melt-quenching technique and were characterized by X-ray diffraction (XRD), Attenuated Total reflectance-Fourier transform Infrared (ATR-FTIR) spectroscopy, Raman...</div>
</a>
<a class="article" target="blank" href="/10.1063/1.4872741">
<div class="title">Gamma ray shielding and structural properties of Bi2O3PbOB2O3V2O5 glass system</div>
<div><span class="journal">AIP Conference Proceedings</span>, <span class="year">2014</span></div>
<div class="abstract">The present work has been undertaken to evaluate the applicability of Bi2O3PbOB2O3V2O5 glass system as gamma ray shielding material. Gamma ray mass attenuation coefficient has been determined theoretically using WinXcom computer software developed by National Institute of Standards and Technology. A meaningful comparison of their radiation shielding properties has been made in terms of their half value layer parameter with standard radiation shielding concrete 'barite'. Structural properties of...</div>
</a>
<a class="article" target="blank" href="/10.1016/j.radphyschem.2013.09.015">
<div class="title">Comparative study of gamma ray shielding and some properties of PbOSiO2Al2O3 and Bi2O3SiO2Al2O3 glass systems</div>
<div><span class="journal">Radiation Physics and Chemistry</span>, <span class="year">2014</span></div>
<div class="abstract">Gamma-ray shielding properties have been estimated in terms of mass attenuation coefficient, half value layer and mean free path values, whereas, structural studies have been performed in terms of density, optical band gap, glass transition temperature and longitudinal ultrasonic velocity parameters. X-ray diffraction, UVvisible, DSC and ultrasonic techniques have been used to explore the structural properties of PbOSiO2Al2O3 and Bi2O3SiO2Al2O3 glass systems.</div>
</a>
<a class="article" target="blank" href="/10.1016/j.ijleo.2021.166790">
<div class="title">Optical and radiation shielding features for a new series of borate glass samples</div>
<div><span class="journal">Optik</span>, <span class="year">2021</span></div>
<div class="abstract">A series of borate glass (80-y)B 2 O 3 -10ZnO-10CdO-yBaO, where 10 ≤ y ≤ 30, was synthesized by the melt quench method. The durability and optical features were explored to study the structural features. The sample S2 appears the highest durability than other samples. Several optical parameters were determined, such as bandgap, refractive index, reflection loss, metallization, and cutoff wavelength; these parameters show a good relationship with durability. Moreover, comprehensive radiation shielding...</div>
</a>
<a class="article" target="blank" href="/10.1063/1.4980454">
<div class="title">Gamma ray shielding and structural properties of PbO-P2O5-Na2WO4 glass system</div>
<div><span class="journal">AIP Conference Proceedings</span>, <span class="year">2017</span></div>
<div class="abstract">The present work has been undertaken to study the gamma ray shielding properties of PbO-P2O5-Na2WO4 glass system. The values of mass attenuation coefficient and half value layer parameter at photon energies 511, 662 and 1173 KeV have been determined using XCOM computer software developed by National Institute of Standards and Technology. The density, molar volume, XRD, UV-VIS and Raman studies have been performed to study the structural properties of the prepared glass system to check the possibility of...</div>
</a>
<a class="article" target="blank" href="/10.1590/1980-5373-mr-2016-0040">
<div class="title">Comparative Study of Radiation Shielding Parameters for Bismuth Borate Glasses</div>
<div><span class="journal">Materials Research</span>, <span class="year">2016</span></div>
<div class="abstract">Melt and quench technique was used for the preparation of glassy samples of the composition x Bi2O3-(1-x) B2O3 where x= .05 to .040. XCOM computer program is used for the evaluation of gamma-ray shielding parameters of the prepared glass samples. Further the values of mass attenuation coefficients, effective atomic number and half value layer for the glassy samples have been calculated in the energy range from 1KeV to 100GeV. Rigidity of the glass samples have been analyzed by molar volume of the prepared...</div>
</a>
<a class="article" target="blank" href="/10.1016/j.jpcs.2010.04.016">
<div class="title">Investigation of structural properties of lead strontium borate glasses for gamma-ray shielding applications</div>
<div><span class="journal">Journal of Physics and Chemistry of Solids</span>, <span class="year">2010</span></div>
<div class="abstract">Glasses of the system PbOSrOB2O3 with the value of molar ratio R (=PbO/B2O3) in the region 0.14≤R≤2.0 were prepared using the melt quenching technique. In order to evaluate gamma-ray shielding properties for glass samples, mass attenuation coefficients have been calculated with the XCOM computer program. The longitudinal velocities of ultrasonic waves were measured in these glass samples at room temperature using the pulse echo technique. The results indicate that with increase in R value,...</div>
</a>
<a class="article" target="blank" href="/10.1016/j.radphyschem.2014.04.032">
<div class="title">Gamma ray attenuation in a developed borate glassy system</div>
<div><span class="journal">Radiation Physics and Chemistry</span>, <span class="year">2014</span></div>
<div class="abstract">Measurements and calculations of gamma ray attenuation coefficients in glass barriers of xBaO5ZnO5MgO14Na2O-1Li2O(75x)B2O3, previously prepared by the melt-quenching technique [1], were performed for γ-ray of energies 121.8, 244.7, 344.14, 661.66, 778.7, 974, 1086.7, 1173.24, 1332.5, and 1407.9 keV; which emitted from 152Eu, 137Cs, and 60Co radioactive gamma ray sources. The transmitted γ-rays were detected by 3″×3″ and 5″×5″ NaI (Tl) scintillation γ-ray spectrometers, and a...</div>
</a>
<a class="article" target="blank" href="/10.1016/j.radphyschem.2021.109541">
<div class="title">Gamma rays and thermal neutron attenuation studies of special composite mixes for using in different applications</div>
<div><span class="journal">Radiation Physics and Chemistry</span>, <span class="year">2021</span></div>
<div class="abstract">Lead borate glass and unglazed ceramic composites have been investigated as shielding against gamma rays and thermal neutron. Attenuation parameters, mass attenuation coefficient and half-value layer using a wide range of gamma-rays energies 356, 511, 662, 1173, 1274 and 1332 keV, were determined. Transmitted gamma rays were measured by NaI(Tl) scintillation detector. Mass attenuation coefficients of the prepared composites samples have been measured experimentally and compared theoretically by XCOM,...</div>
</a>
<a class="article" target="blank" href="/10.1016/j.jnoncrysol.2018.12.010">
<div class="title">Physical, structural, optical and gamma radiation shielding properties of borate glasses containing heavy metals (Bi2O3/MoO3)</div>
<div><span class="journal">Journal of Non-Crystalline Solids</span>, <span class="year">2019</span></div>
<div class="abstract">In an attempt to develop a novel gamma radiation shielding glasses, we prepared borate glasses contains a high concentration of heavy metals like Bi2O3 and MoO3 with the composition of 20MoO3-(80-x)B2O3-xBi2O3, were x varied from 30 to 45 mol% using tradition melt-quenching-annealing method. A structural investigation such as XRD and FTIR were characterized to confirm the amorphous structure of the prepared glasses and prove the availability of all chemicals included in these compositions after the melting...</div>
</a>
<a class="article" target="blank" href="/10.1063/5.0052351">
<div class="title">Physical and radiation shielding properties of tantalum-zinc-sodium-borate glasses</div>
<div><span class="journal">AIP Conference Proceedings</span>, <span class="year">2021</span></div>
<div class="abstract">In this paper, the physical properties such as density, molar volume, oxygen packing density, oxygen molar volume of Zinc-Sodium-Borate glass system with different amounts of Tantalum oxide have been evaluated. In order to study the radiation shielding competence of the Tantalum-Zinc-Sodium-Borate glasses the mass attenuation coefficients and Half Value Layer (HVL) were calculated for γ-ray photon energies of 59.54, 122, 279, 356, 511, 662, 835 KeV. A comparison of the radiation shielding properties of...</div>
</a>
<a class="article" target="blank" href="/10.1016/j.saa.2018.08.038">
<div class="title">Investigation of bismuth borate glass system modified with barium for structural and gamma-ray shielding properties</div>
<div><span class="journal">Spectrochimica Acta Part A: Molecular and Biomolecular Spectroscopy</span>, <span class="year">2019</span></div>
<div class="abstract">In the present paper, transparent and non-toxic Bi2O3-B2O3 glasses doped with BaO have been prepared by the authors which may replace the standard radiation shielding concretes and lead based commercial glasses for gamma ray shielding applications. The effects of BaO on the structural and optical properties of the prepared glass system have been investigated by Raman, FTIR and UVVisible techniques. It has been observed that barium plays the role of a modifier and it is responsible for conversion of...</div>
</a>
<a class="article" target="blank" href="/10.1063/1.5032878">
<div class="title">Evaluation of the gamma radiation shielding parameters of bismuth modified quaternary glass system</div>
<div><span class="journal">AIP Conference Proceedings</span>, <span class="year">2018</span></div>
<div class="abstract">Glasses modified with heavy metal oxides (HMO) are an interesting area of research in the field of gamma-ray shielding. Bismuth modified lithium-zinc-borate glasses have been studied whereby bismuth oxide is added from 0 to 50 mol%. The gamma ray shielding properties of the glasses were evaluated at photon energy 662 keV with the help of XMuDat computer program by using the Hubbell and Seltzer database. Various gamma ray shielding parameters such as attenuation coefficient, shield thickness in terms of...</div>
</a>
<a class="article" target="blank" href="/10.1016/j.net.2020.07.035">
<div class="title">Investigation of photon, neutron and proton shielding features of H3BO3ZnONa2OBaO glass system</div>
<div><span class="journal">Nuclear Engineering and Technology</span>, <span class="year">2021</span></div>
<div class="abstract">The current study aims to explore the shielding properties of multi-component borate-based glass series. Seven glass-samples with composition of (80-y)H3BO310ZnO10Na2OyBaO where (y = 0, 5, 10, 15, 20, 25 and 30 mol.%) were synthesized by melt-quench method. Various shielding features for photons, neutrons, and protons were determined for all prepared samples. XCOM, Phy-X program, and SRIM code were performed to determine and explain several shielding properties such as equivalent atomic number,...</div>
</a>
<a class="article" target="blank" href="/10.1063/5.0053029">
<div class="title">Gamma radiation shielding characteristics for some rare earth doped lead borate glasses</div>
<div><span class="journal">AIP Conference Proceedings</span>, <span class="year">2021</span></div>
<div class="abstract">A novel rare earth doped lead borate glass system (65B2O3xGd2O3xSm2O3-(35-2x) PbO) has been prepared by the means of melt-quench technique. XRD results confirm amorphous nature of glass samples. The density of glasses has been measured using Archimedes method. The fundamental radiation shielding parameters such as mass attenuation coefficient and effective atomic number decreases as PbO is replaced by Sm2O3 and Gd2O3 in the glass series. Whereas, the thickness normalizing parameters such as mean...</div>
</a>
<a class="article" target="blank" href="/10.1515/ract-2018-2938">
<div class="title">Evaluation of gamma-ray attenuation properties of lithium borate glasses doped with barite, limonite and serpentine</div>
<div><span class="journal">Radiochimica Acta</span>, <span class="year">2018</span></div>
<div class="abstract">Abstract The values of mass attenuation coefficient, the effective atomic number and the electron density of barite-doped, limonite-doped, serpentine-doped and undoped lithium borate glasses were obtained not only from experimental study using the narrow beam transmission method for 81, 121, 244, 276, 344, 383, 444 and 778 keV gamma energies with Hp-Ge detector, but also therotical work by WinXCom software (1 keV10 5 MeV). From the obtained results, all glasses type mass attenuation coefficient values...</div>
</a>
<a class="article" target="blank" href="/10.1016/j.radphyschem.2017.03.031">
<div class="title">Evaluation of gamma-ray attenuation properties of bismuth borate glass systems using Monte Carlo method</div>
<div><span class="journal">Radiation Physics and Chemistry</span>, <span class="year">2017</span></div>
<div class="abstract">Abstract A Monte Carlo method was developed to investigate radiation shielding properties of bismuth borate glass. The mass attenuation coefficients and half-value layer parameters were determined for different fractional amounts of Bi2O3 in the glass samples for the 356, 662, 1173 and 1332&nbsp;keV photon energies. A comparison of the theoretical and experimental attenuation coefficients is presented.</div>
</a>
<a class="article" target="blank" href="/10.1016/j.nucengdes.2014.12.033">
<div class="title">Correlation of gamma ray shielding and structural properties of PbOBaOP 2 O 5 glass system</div>
<div><span class="journal">Nuclear Engineering and Design</span>, <span class="year">2015</span></div>
<div class="abstract">The presented work has been undertaken to evaluate the applicability of BaO doped PbO-P2O5 glass system as gamma ray shielding material in terms of mass attenuation coefficient and half value layer at photon energies 662, 1173 and1332 keV. A meaningful comparison of their radiation shielding properties has been made in terms of their mass attenuation coefficient and HVL parameters with standard radiation shielding concrete barite. The density, molar volume, XRD, FTIR, Raman and UVvisible...</div>
</a>
<a class="article" target="blank" href="/10.1016/j.radphyschem.2019.04.005">
<div class="title">Borate multicomponent of bismuth rich glasses for gamma radiation shielding application</div>
<div><span class="journal">Radiation Physics and Chemistry</span>, <span class="year">2019</span></div>
<div class="abstract">Abstract In this work, six borate-bismuth glasses have been synthesized using a conventional melt-quenching-aneling process with a composition of (80-x)B2O3 10ZnO 10MgO-xBi2O3 where x=10, 20, 30, 40, 50 and 60mol%. The glasses were melted at 975°C for 30min and annealed at 300°C for 5h. Different physical properties of these glasses have been measured and estimated. X-ray diffraction has been utilized to investigate the structural nature of these glasses. Optical absorption and...</div>
</a>
<a class="article" target="blank" href="/10.1016/j.ceramint.2020.04.112">
<div class="title">Direct influence of mercury oxide on structural, optical and radiation shielding properties of a new borate glass system</div>
<div><span class="journal">Ceramics International</span>, <span class="year">2020</span></div>
<div class="abstract">Abstract A series of barium sodium borate glasses of chemical composition (60-x) B2O3+20Na2O2+20BaCO3+x HgO, (where x&nbsp;=&nbsp;0, 2.5, 5, 7.5, 10, 12.5, and 15&nbsp;wt%) doped with mercury oxide was synthesized. The melt-quenching method was used to synthesize the glass series at 1100&nbsp;°C for melting and 60&nbsp;min for annealing at 300&nbsp;°C. X-ray diffraction was used to study the structure of the synthesized samples at room temperature between 10° and 70°. We used a Tensor Model 27 FTIR spectrometer to show the...</div>
</a>
<a class="article" target="blank" href="/10.1063/1.3526246">
<div class="title">Photon Interaction Parameters for Some Borate Glasses</div>
<div><span class="journal">AIP Conference Proceedings</span>, <span class="year">2010</span></div>
<div class="abstract">Some photon interaction parameters of dosimetric interest such as mass attenuation coefficients, effective atomic number, electron density and KERMA relative to air have been computed in the wide energy range from 1 keV to 100 GeV for some borate glasses viz. bariumlead borate, bismuthborate, calciumstrontium borate, lead borate and zincborate glass. It has been observed that lead borate glass and bariumlead borate glass have maximum values of mass attenuation coefficient, effective atomic...</div>
</a>
<a class="article" target="blank" href="/10.1007/s42452-020-2115-7">
<div class="title">Effect of gamma ray on some properties of bismuth borate glasses containing different transition metals</div>
<div><span class="journal">SN Applied Sciences</span>, <span class="year">2020</span></div>
<div class="abstract">This work aims to study the influence of gamma irradiation on some bismuth borate glasses doped with different transition metal oxides (CuO, CoO, ZnO and CdO). The structure of the glass matrix was analyzed by Fourier transform infrared and ultravioletvisible spectroscopy before and after gamma irradiation at doses of 1&nbsp;kGy, 20&nbsp;kGy and 50&nbsp;kGy. Additionally, the attention is made in determining the attenuation parameters against gamma rays. Mass attenuation coefficient determined experimentally and...</div>
</a>
<a class="article" target="blank" href="/10.1016/j.jnoncrysol.2014.08.003">
<div class="title">Radiation shielding competence of silicate and borate heavy metal oxide glasses: Comparative study</div>
<div><span class="journal">Journal of Non-Crystalline Solids</span>, <span class="year">2014</span></div>
<div class="abstract">Abstract Gamma-ray shielding competence of silicate and borate heavy metal oxide glasses has been investigated using linear attenuation coefficients, effective atomic numbers and exposure buildup factors (EBF). The gamma-ray EBF were computed using the Geometric Progression (G-P) fitting method for photon energies from 0.015 to 15&nbsp;MeV, and for penetration depths up to 40 mean free paths (mfps). The macroscopic effective removal cross-section for fast neutron has been calculated for energy range from 2 to...</div>
</a>
<a class="article" target="blank" href="/10.1002/pssb.202000417">
<div class="title">Investigation of GammaRadiation Shielding Properties of Cadmium Bismuth Borate Glass Experimentally and by Using XCOM Program and MCNP5 Code</div>
<div><span class="journal">physica status solidi (b)</span>, <span class="year">2020</span></div>
<div class="abstract">New glass systems of bismuth borate with various concentrations of cadmium oxide are prepared based on the melt-quenching method. The X-ray diffraction (XRD) reveals a fully amorphous structure of the prepared glasses (S1S4), and the UVvis results display good transparency (&gt;50%) in the visible and near-UV region. In addition, the radiation shielding properties (mass attenuation coefficient, half-value layer, tenth value layer, mean free path, effective atomic number, and electron density) of the new...</div>
</a>
<a class="article" target="blank" href="/10.1016/j.matchemphys.2018.04.106">
<div class="title">Comparative investigations of gamma and neutron radiation shielding parameters for different borate and tellurite glass systems using WinXCom program and MCNPX code</div>
<div><span class="journal">Materials Chemistry and Physics</span>, <span class="year">2018</span></div>
<div class="abstract">In the present article, for different chemical compositions of B2O3Bi2O3, B2O3Sb2O3, B2O3WO3La2O3, B2O3MoO3ZnO, and TeO2MO (M = Mg, Ba, and Zn) glasses, by applying WinXCom program we calculated the mass attenuation coefficient (μ/ρ) values, and from these values, the effective atomic number (Zeff), electron density (Ne), mean free path (MFP), half-value layer (HVL), and exposure buildup factor (EBF) values using Geometric progression (GP) fitting method, including macroscopic...</div>
</a>
<a class="article" target="blank" href="/10.1016/j.net.2020.06.034">
<div class="title">Investigations on borate glasses within SBC-Bx system for gamma-ray shielding applications</div>
<div><span class="journal">Nuclear Engineering and Technology</span>, <span class="year">2021</span></div>
<div class="abstract">This paper examines gamma-ray shielding properties of SBC-Bx glass system with the chemical composition of 40SiO210B2O3xBaO(45-x)CaO yZnO zMgO (where x = 0, 10, 20, 30, and 35 mol% and y = z = 6 mol%). Mass attenuation coefficient (μ/ρ) which is an essential parameter to study gamma-ray shielding properties was obtained in the photon energy range of 0.01515 MeV using PHITS Monte Carlo code for the proposed glasses. The obtained results were compared with those calculated by WinXCOM...</div>
</a>
</div>
</div>
</body></html>

BIN
after_click_no.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

@ -3,16 +3,6 @@ from typing import Optional
import asyncio
import sys
import os
import time
from PIL import Image
import io
# 尝试导入 OpenCV 用于更好的模板匹配
try:
import cv2
HAS_CV2 = True
except ImportError:
HAS_CV2 = False
# 尝试导入 playwright-stealth如果没有安装则忽略
try:
@ -20,25 +10,46 @@ try:
except ImportError:
stealth_async = None
# 隐藏自动化特征的浏览器启动参数
_STEALTH_ARGS = [
"--disable-blink-features=AutomationControlled",
"--no-first-run",
"--no-default-browser-check",
"--disable-infobars",
"--disable-extensions",
"--disable-notifications",
]
_USER_AGENT = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/131.0.0.0 Safari/537.36"
)
async def download_from_url_playwright(url: str, save_path: str) -> tuple[bool, str]:
async with async_playwright() as p:
try:
browser = await p.chromium.launch(headless=False)
context = await browser.new_context(viewport={"width": 1920, "height": 1080})
browser = await p.chromium.launch(
headless=False,
args=_STEALTH_ARGS,
)
context = await browser.new_context(
viewport={"width": 1920, "height": 1080},
user_agent=_USER_AGENT,
locale="zh-CN",
timezone_id="Asia/Shanghai",
)
page = await context.new_page()
page.set_default_timeout(60000)
# 应用 stealth 模式绕过反爬虫检测
if stealth_async:
await stealth_async(page)
else:
# 如果没有 stealth手动设置一些反爬虫对抗
await page.add_init_script("""
Object.defineProperty(navigator, 'webdriver', {
get: () => false,
});
""")
await page.add_init_script(
"Object.defineProperty(navigator,'webdriver',{get:()=>false});"
)
pdf_content: Optional[bytes] = None
@ -46,53 +57,23 @@ async def download_from_url_playwright(url: str, save_path: str) -> tuple[bool,
nonlocal pdf_content
if "application/pdf" in response.headers.get("content-type", ""):
try:
# 确保完全读取响应体
pdf_content = await response.body()
print(f"✓ 成功捕获 PDF大小: {len(pdf_content)} bytes")
except Exception as e:
print(f"⚠ 读取 PDF 响应体失败: {e}")
page.on("response", on_response)
# 先用较宽松的等待条件加载页面,避免卡在 Cloudflare
# 加载页面
try:
await page.goto(url, wait_until="domcontentloaded", timeout=5000)
except:
await page.goto(url, wait_until="domcontentloaded", timeout=30000)
except Exception:
print("⚠ 页面加载超时,但继续处理...")
await page.wait_for_timeout(5000)
# 处理 Cloudflare 校验
print("开始处理 Cloudflare 校验...")
await page.wait_for_timeout(3000)
# Cloudflare 可能需要连续点击多次最多尝试5次
max_cloudflare_attempts = 5
for attempt in range(max_cloudflare_attempts):
# 检查是否已获取到 PDF如果已获取则无需继续验证
if pdf_content:
print("✓ 已获取到 PDF 内容,停止验证框处理")
break
print(f"\nCloudflare 验证尝试 {attempt + 1}/{max_cloudflare_attempts}")
success = await handle_cloudflare_with_image(page)
if success:
print("✓ 成功处理一次验证框")
# 等待新验证框出现或页面刷新
await page.wait_for_timeout(2000)
# 检查是否还有验证框,如果没有则说明验证完成
# 这里简单地继续尝试,直到达到最大次数
if attempt < max_cloudflare_attempts - 1:
print(" 检查是否还有验证框...")
await page.wait_for_timeout(1000)
else:
print("⚠ 未找到验证框,可能已完成验证或验证框已消失")
break
print("✓ Cloudflare 验证处理完成")
# 等待 Cloudflare 挑战完成
await handle_cloudflare(page)
await page.wait_for_timeout(2000)
# 如果尚未获取 PDF继续等待响应
if not pdf_content:
print("等待 PDF 响应...")
@ -101,19 +82,17 @@ async def download_from_url_playwright(url: str, save_path: str) -> tuple[bool,
lambda response: "application/pdf" in response.headers.get("content-type", ""),
timeout=15000
)
# 确保响应体完全加载
pdf_content = await response.body()
print(f"✓ 通过 wait_for_response 获取 PDF大小: {len(pdf_content)} bytes")
except Exception as e:
print(f"⚠ 等待 PDF 响应超时: {e}")
if pdf_content:
# 验证文件大小PDF 通常大于 10KB
pdf_size = len(pdf_content)
if pdf_size < 10240:
await browser.close()
return False, f"PDF 文件过小: {pdf_size} bytes可能下载不完整"
with open(save_path, "wb") as f:
f.write(pdf_content)
print(f"✓ PDF 已保存到: {save_path},大小: {pdf_size} bytes")
@ -128,84 +107,89 @@ async def download_from_url_playwright(url: str, save_path: str) -> tuple[bool,
finally:
try:
await browser.close()
except:
except Exception:
pass
async def handle_cloudflare_with_image(page: Page) -> bool:
"""
使用图像识别方式处理 Cloudflare 验证框
支持模板匹配和颜色识别两种方式
"""
# 在尝试之前先等待2秒让验证框完全加载
await page.wait_for_timeout(2000)
max_retries = 5
for retry in range(max_retries):
print(f"图像识别方式尝试第 {retry + 1}/{max_retries}")
try:
# 方式1: 通过模板图像识别(如果有模板文件)
success = await try_template_matching()
if success:
print(" ✓ 模板匹配方式成功")
await page.wait_for_timeout(5000)
return True
print(f" 等待后重试... ({retry + 1}/{max_retries})")
await page.wait_for_timeout(2000)
except Exception as e:
print(f" ✗ 图像处理异常: {e}")
await page.wait_for_timeout(2000)
return False
def download_pdf_with_curl_cffi(url: str, save_path: str) -> tuple[bool, str]:
"""使用 curl-cffi 伪造 Chrome TLS 指纹下载 PDF可绕过 Cloudflare JS 挑战"""
try:
import curl_cffi.requests as cf
resp = cf.get(
url,
impersonate="chrome131",
headers={
"Accept": "application/pdf,application/octet-stream,*/*",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Referer": url,
},
timeout=30,
allow_redirects=True,
)
if resp.status_code != 200:
return False, f"http_{resp.status_code}"
content = resp.content
if not content.startswith(b"%PDF") or len(content) < 10240:
return False, "not_valid_pdf"
import os
os.makedirs(os.path.dirname(os.path.abspath(save_path)), exist_ok=True)
with open(save_path, "wb") as f:
f.write(content)
return True, ""
except ImportError:
return False, "curl_cffi_not_installed"
except Exception as e:
return False, str(e)
async def try_template_matching() -> bool:
_CHALLENGE_TITLES = ["just a moment", "cloudflare", "checking your browser", "ddos-guard", "ddos guard", "attention required"]
async def handle_cloudflare(page: Page, timeout: int = 45000) -> bool:
"""
通过模板匹配查找并点击验证框
使用 pyautogui.locateOnScreen 直接在屏幕上定位验证框模板
处理 bot 检测安全验证Cloudflare / DDoS-Guard
1. 通过页面标题检测是否处于挑战页面
2. 尝试点击 Turnstile 复选框iframe 如果有
3. 等待标题变化表明挑战已自动通过
"""
import pyautogui
template_paths = [
'apps/resm/cloudflare_checkbox2.png',
]
# pyautogui 定位的准确度阈值0.0-1.0,越高越严格)
ACCURACY = 0.4
for template_path in template_paths:
if not os.path.exists(template_path):
print(f" 模板文件不存在: {template_path}")
continue
try:
title = await page.title()
except Exception:
return False
title_l = title.lower()
if not any(kw in title_l for kw in _CHALLENGE_TITLES):
return True # 无挑战
print(f"检测到安全挑战页面(标题: {title!r}),等待 JS 自动通过...")
# 尝试点击 Cloudflare Turnstile 复选框(如果存在)
for frame_sel in [
'iframe[src*="challenges.cloudflare.com"]',
'iframe[title*="cloudflare"]',
'iframe[title*="challenge"]',
]:
try:
print(f" 尝试在屏幕上定位模板: {template_path} (confidence={ACCURACY})")
# 直接在屏幕上查找模板,使用 confidence 参数
loc = pyautogui.locateOnScreen(template_path, confidence=ACCURACY)
if loc:
# loc 是 (left, top, width, height) 或 (x, y, w, h)
# pyautogui.center(loc) 返回中心坐标
center_x, center_y = pyautogui.center(loc)
print(f" 找到验证框位置: ({center_x}, {center_y})")
print(f" 模板匹配区域: {loc}")
pyautogui.click(center_x, center_y, clicks=1, interval=0.1)
return True
else:
print(f" 未找到模板 (confidence={ACCURACY})")
return False
except Exception as e:
# 捕获所有异常,包括 ImageNotFoundException
error_type = type(e).__name__
if "ImageNotFoundException" in error_type:
print(f" 模板匹配异常: {error_type} - 屏幕上找不到模板,停止尝试")
else:
print(f" 模板匹配异常: {error_type} - {e}")
return False
return False
checkbox = page.frame_locator(frame_sel).locator('input[type="checkbox"]')
if await checkbox.count() > 0:
await checkbox.click(timeout=3000)
print(" ✓ 点击了验证复选框")
break
except Exception:
pass
try:
await page.wait_for_function(
"""() => {
const t = document.title.toLowerCase();
const kws = ['just a moment', 'cloudflare', 'checking your browser',
'ddos-guard', 'ddos guard', 'attention required'];
return !kws.some(kw => t.includes(kw));
}""",
timeout=timeout,
)
print(f" ✓ 安全挑战已通过")
return True
except Exception as e:
print(f"⚠ 安全挑战处理超时,继续尝试获取 PDF...")
return False

View File

@ -5,6 +5,12 @@ from pathlib import Path
from typing import Optional
from playwright.async_api import async_playwright, Page, Browser
# 尝试导入 playwright-stealth
try:
from playwright_stealth import stealth_async
except ImportError:
stealth_async = None
# 初始化日志
Path("log").mkdir(parents=True, exist_ok=True)
LOG_PATH = Path("log") / "scihub_downloader.log"
@ -15,6 +21,23 @@ logging.basicConfig(
)
logger = logging.getLogger("scihub")
# 隐藏自动化特征的浏览器启动参数
_STEALTH_ARGS = [
"--disable-blink-features=AutomationControlled",
"--no-first-run",
"--no-default-browser-check",
"--disable-infobars",
"--disable-extensions",
"--disable-notifications",
"--disable-popup-blocking",
]
_USER_AGENT = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/131.0.0.0 Safari/537.36"
)
async def _wait_for_user_to_solve_challenge(page: Page):
logger.info("请在浏览器中完成验证(如果需要),完成后按回车继续...")
@ -40,7 +63,7 @@ async def _try_click_robot_button(page: Page, headless: bool) -> bool:
await page.wait_for_timeout(1500)
# 等待可能的导航/重定向
try:
await page.wait_for_navigation(timeout=8000)
await page.wait_for_load_state("domcontentloaded", timeout=8000)
logger.info("点击验证后检测到导航完成")
except Exception:
await page.wait_for_timeout(500)
@ -57,7 +80,6 @@ async def _try_click_robot_button(page: Page, headless: bool) -> bool:
async def _click_no_button(page: Page) -> bool:
"""尝试点击 'No' 按钮(可选步骤,如果找不到则直接继续)"""
# 精确匹配 <div class="answer" onclick="check()">No</div>
selectors = ["div.answer[onclick=\"check()\"]", "div.answer:has-text('No')", "text=No"]
for sel in selectors:
try:
@ -73,13 +95,11 @@ async def _click_no_button(page: Page) -> bool:
logger.warning(f"点击 'No' 失败: {click_err}")
pass
await page.wait_for_timeout(1200)
# 点击 No 后也可能触发重定向
try:
await page.wait_for_navigation(timeout=8000)
await page.wait_for_load_state("domcontentloaded", timeout=8000)
logger.info("点击 No 后检测到导航完成")
except Exception:
pass
# 保存结果用于排查
try:
await page.screenshot(path="after_click_no.png", full_page=True)
html = await page.content()
@ -95,90 +115,153 @@ async def _click_no_button(page: Page) -> bool:
return False
# 各种 bot 检测页面的标题关键词
_CHALLENGE_TITLES = ["just a moment", "cloudflare", "checking your browser", "ddos-guard", "attention required", "ddos guard"]
async def _wait_challenge_clear(page: Page, timeout: int = 45000) -> bool:
"""等待 bot 检测Cloudflare/DDoS-Guard 等)挑战页面自动通过"""
try:
title = await page.title()
except Exception:
return False
title_l = title.lower()
if not any(kw in title_l for kw in _CHALLENGE_TITLES):
return True # 无挑战,直接通过
logger.info(f"检测到安全挑战页面(标题: {title!r}),等待 JS 自动通过...")
# 尝试点击 Cloudflare Turnstile 复选框(如果有)
for frame_sel in [
'iframe[src*="challenges.cloudflare.com"]',
'iframe[title*="cloudflare"]',
'iframe[title*="challenge"]',
]:
try:
checkbox = page.frame_locator(frame_sel).locator('input[type="checkbox"]')
if await checkbox.count() > 0:
await checkbox.click(timeout=3000)
logger.info("点击了验证复选框")
break
except Exception:
pass
try:
await page.wait_for_function(
"""() => {
const t = document.title.toLowerCase();
const keywords = ['just a moment', 'cloudflare', 'checking your browser',
'ddos-guard', 'ddos guard', 'attention required'];
return !keywords.some(kw => t.includes(kw));
}""",
timeout=timeout,
)
logger.info(f"挑战已通过,当前标题: {await page.title()!r}")
return True
except Exception as e:
logger.warning(f"等待挑战超时({timeout}ms: {e}")
return False
async def download_pdf_with_playwright(url: str, output: str = "paper.pdf", headless: bool = False) -> Optional[bytes]:
async with async_playwright() as p:
browser: Browser = await p.chromium.launch(headless=headless)
context = await browser.new_context(viewport={"width": 1920, "height": 1080})
browser: Browser = await p.chromium.launch(
headless=headless,
args=_STEALTH_ARGS,
)
context = await browser.new_context(
viewport={"width": 1920, "height": 1080},
user_agent=_USER_AGENT,
locale="zh-CN",
timezone_id="Asia/Shanghai",
)
page = await context.new_page()
# 应用 stealth 模式
if stealth_async:
await stealth_async(page)
else:
await page.add_init_script(
"Object.defineProperty(navigator,'webdriver',{get:()=>false});"
)
pdf_url: Optional[str] = None
pdf_content: Optional[bytes] = None
async def on_response(response):
nonlocal pdf_content
try:
ct = response.headers.get("content-type", "")
if "application/pdf" in ct:
logger.info(f"捕获到 PDF 响应: {response.url}")
pdf_content = await response.body()
except Exception:
logger.exception("处理响应时出错")
nonlocal pdf_url
ct = response.headers.get("content-type", "")
resp_url = response.url
# 忽略 chrome-extension 等内部 URL
if "application/pdf" in ct and resp_url.startswith("http"):
logger.info(f"检测到 PDF 响应 URL: {resp_url}")
pdf_url = resp_url
page.on("response", on_response)
try:
logger.info(f"打开: {url}")
await page.goto(url, wait_until="networkidle")
try:
await page.goto(url, wait_until="domcontentloaded", timeout=30000)
except Exception:
logger.info("页面加载超时(可能正在处理安全验证),继续等待...")
await page.wait_for_timeout(3000)
# 等待 bot 检测挑战完成DDoS-Guard / Cloudflare 等)
await _wait_challenge_clear(page)
await page.wait_for_timeout(1000)
# 尝试点击验证 & No
await _try_click_robot_button(page, headless)
await _click_no_button(page)
# 点击后充分等待以让页面加载和触发PDF响应
logger.info("等待页面加载和PDF响应...")
# 等待页面加载并检测 PDF 响应 URL
logger.info("等待 PDF 响应...")
await page.wait_for_timeout(3000)
# 尝试主动等待PDF响应点击后可能会自动加载或重定向触发PDF请求
if not pdf_content:
if not pdf_url:
try:
await page.wait_for_response(
lambda r: "application/pdf" in r.headers.get("content-type", ""),
timeout=5000,
resp = await page.wait_for_response(
lambda r: "application/pdf" in r.headers.get("content-type", "")
and r.url.startswith("http"),
timeout=8000,
)
logger.info("捕获到主动等待的 PDF 响应")
pdf_url = resp.url
logger.info(f"主动等待到 PDF URL: {pdf_url}")
except Exception:
logger.info("主动等待 PDF 响应超时,继续其他方式")
logger.info("等待 PDF 响应超时")
# 尝试通过页面下载按钮
# download_selectors = ["a[href*='.pdf']", "button:has-text('Download')", "a:has-text('PDF')"]
# for sel in download_selectors:
# try:
# if await page.locator(sel).count() > 0:
# logger.info(f"尝试点击下载元素: {sel}")
# async with page.expect_download() as di:
# await page.click(sel)
# download = await di.value
# await download.save_as(output)
# logger.info(f"已保存 PDF: {output}")
# with open(output, "rb") as f:
# pdf_content = f.read()
# break
# except Exception:
# logger.exception(f"通过选择器下载失败: {sel}")
# 直接导航到 PDF URL 下载完整内容
if pdf_url:
logger.info(f"直接请求 PDF: {pdf_url}")
try:
pdf_resp = await page.goto(pdf_url, wait_until="networkidle", timeout=30000)
if pdf_resp and pdf_resp.status == 200:
pdf_content = await pdf_resp.body()
logger.info(f"下载成功,大小: {len(pdf_content)} bytes")
except Exception as e:
logger.warning(f"直接导航下载失败: {e},尝试 fetch")
try:
pdf_content = await page.evaluate(f"""
async () => {{
const r = await fetch({pdf_url!r});
const buf = await r.arrayBuffer();
return Array.from(new Uint8Array(buf));
}}
""")
if pdf_content:
pdf_content = bytes(pdf_content)
except Exception as e2:
logger.warning(f"fetch 下载也失败: {e2}")
# 回退:查找页面内 PDF 链接并直接访问
# if not pdf_content:
# logger.info("尝试查找页面内 PDF 链接")
# try:
# links = await page.eval_on_selector_all("a[href]", "els => els.map(e=>e.href)")
# candidates = [u for u in links if ".pdf" in u]
# if candidates:
# pdf_url = candidates[0]
# logger.info(f"直接访问 PDF 链接: {pdf_url}")
# resp = await page.goto(pdf_url, wait_until="networkidle")
# if resp and resp.status == 200:
# pdf_content = await resp.body()
# with open(output, "wb") as f:
# f.write(pdf_content)
# logger.info(f"已保存 PDF: {output}")
# except Exception:
# logger.exception("直接访问 PDF 链接失败")
if pdf_content:
if pdf_content and len(pdf_content) > 10240:
logger.info(f"下载成功,大小: {len(pdf_content)} bytes")
return pdf_content
else:
logger.warning("未能获取 PDF已保存页面快照供排查")
if pdf_content:
logger.warning(f"PDF 文件过小({len(pdf_content)} bytes可能是错误页")
logger.warning("未能获取有效 PDF已保存页面快照供排查")
try:
await page.screenshot(path="scihub_screenshot.png", full_page=True)
html = await page.content()
@ -198,15 +281,24 @@ async def download_pdf_with_playwright(url: str, output: str = "paper.pdf", head
logger.exception("关闭 browser 失败")
# 按优先级排列的 sci-hub 域名(国内相对可访问)
_SCIHUB_DOMAINS = [
"sci-hub.ren",
"sci-hub.ee",
"sci-hub.st",
"sci-hub.se",
]
def download_paper_by_doi(doi: str, output: Optional[str] = None, headless: bool = True) -> tuple[bool, str]:
"""
通过 DOI 下载论文 PDF task 调用
参数
doi: DOI 字符串例如 "10.1016/j.conbuildmat.2017.10.091"
output: 输出文件路径默认基于 DOI 生成格式10.1016_j.xxx.pdf
headless: 是否无头模式默认 True
返回
(True, "文件路径") 如果成功
(False, "scihub_error_*: 错误详情") 如果失败错误码前缀包括
@ -222,33 +314,35 @@ def download_paper_by_doi(doi: str, output: Optional[str] = None, headless: bool
err = "scihub_error_empty_doi: DOI 为空"
logger.error(err)
return False, err
url = f"https://sci-hub.st/{doi}"
output_path = output or f"{doi.replace('/', '_')}.pdf"
logger.info(f"开始下载 DOI: {doi}")
logger.info(f"目标 URL: {url}")
logger.info(f"输出文件: {output_path}")
try:
pdf_content = asyncio.run(download_pdf_with_playwright(url, output=output_path, headless=headless))
except asyncio.TimeoutError as e:
err = f"scihub_error_timeout: 网页加载超时(可能网络慢或网站不可用)"
logger.error(err)
return False, err
except Exception as e:
err = f"scihub_error_load_failed: 加载页面时出错 - {str(e)}"
logger.exception(err)
return False, err
if pdf_content:
logger.info(f"✓ 成功下载: {output_path} ({len(pdf_content)} bytes)")
return True, output_path
else:
# PDF 内容为空,说明所有获取方式都失败
err = f"scihub_error_pdf_not_found: 无法从 Sci-Hub 获取 PDF可能 DOI 不存在、网站不可用、或无权限访问)"
logger.error(err)
return False, err
for domain in _SCIHUB_DOMAINS:
url = f"https://{domain}/{doi}"
logger.info(f"尝试域名: {url}")
try:
pdf_content = asyncio.run(download_pdf_with_playwright(url, output=output_path, headless=headless))
except asyncio.TimeoutError:
logger.warning(f"{domain} 超时,尝试下一个域名")
continue
except Exception as e:
logger.warning(f"{domain} 出错: {e},尝试下一个域名")
continue
if pdf_content:
# 写入文件
import os
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
with open(output_path, "wb") as f:
f.write(pdf_content)
logger.info(f"✓ 成功下载({domain}: {output_path} ({len(pdf_content)} bytes)")
return True, output_path
else:
logger.warning(f"{domain} 未获取到 PDF尝试下一个域名")
err = "scihub_error_pdf_not_found: 所有域名均无法获取 PDF"
logger.error(err)
return False, err
except Exception as e:
err = f"scihub_error_exception: 执行下载时发生异常 - {str(e)}"
logger.exception(err)
@ -274,4 +368,3 @@ if __name__ == "__main__":
else:
logger.error(f"失败: {msg}")
raise SystemExit(1)

View File

@ -414,18 +414,21 @@ def download_pdf(paper_id):
paper.fetch_end()
def save_pdf_from_oa_url(paper:Paper):
def save_pdf_from_oa_url(paper: Paper):
from .d_oaurl import download_pdf_with_curl_cffi, download_from_url_playwright
# 策略1: 直接请求
try:
headers = get_random_headers()
res = requests.get(paper.oa_url, headers=headers, timeout=(3, 15))
except requests.RequestException as e:
paper.save_fail_reason("oa_url_request_error")
return f"oa_url_request_error: {str(e)}"
if res.status_code in [200, 201, 202]:
# 检查是否是PDF文件检查魔数 %PDF 或 content-type
is_pdf = (
res.content.startswith(b'%PDF') or
res.content.startswith(b'%PDF') or
res.headers.get("content-type", "").startswith("application/pdf") or
res.headers.get("content-type", "") == "application/octet-stream"
)
@ -435,19 +438,26 @@ def save_pdf_from_oa_url(paper:Paper):
else:
paper.save_fail_reason("oa_url_not_pdf")
return "oa_url_not_pdf"
elif res.status_code == 403:
paper.save_fail_reason("oa_url_need_play")
# paper_path = paper.init_paper_path("pdf")
# is_ok, err_msg = run_async(download_from_url_playwright(paper.oa_url, paper_path))
# if is_ok:
# paper.has_fulltext = True
# paper.has_fulltext_pdf = True
# paper.save(update_fields=["has_fulltext", "has_fulltext_pdf", "update_time"])
# return "success"
# else:
# paper.save_fail_reason(f"oa_url_pdf_play_error: {err_msg}")
# return f"oa_url_pdf_play_error: {err_msg}"
return f"oa_url_pdf_oerror: {res.status_code}"
# 策略2: curl-cffi处理 Cloudflare JS 挑战)
paper_path = paper.init_paper_path("pdf")
is_ok, err_msg = download_pdf_with_curl_cffi(paper.oa_url, paper_path)
if is_ok:
paper.has_fulltext = True
paper.has_fulltext_pdf = True
paper.save(update_fields=["has_fulltext", "has_fulltext_pdf", "update_time"])
return "success"
# 策略3: Playwright最终回退
is_ok, err_msg = run_async(download_from_url_playwright(paper.oa_url, paper_path))
if is_ok:
paper.has_fulltext = True
paper.has_fulltext_pdf = True
paper.save(update_fields=["has_fulltext", "has_fulltext_pdf", "update_time"])
return "success"
paper.save_fail_reason(f"oa_url_all_methods_failed: {err_msg}")
return f"oa_url_all_methods_failed: {err_msg}"
def save_pdf_from_openalex(paper:Paper):
if cache.get("openalex_api_exceed"):

View File

@ -1,6 +1,6 @@
from django.urls import path, include
from rest_framework import routers
from .views import PaperViewSet
from .views import PaperViewSet, paper_pdf_view
API_BASE_URL = 'api/resm/'
HTML_BASE_URL = 'resm/'
@ -9,5 +9,6 @@ router = routers.DefaultRouter()
router.register('paper', PaperViewSet, basename="paper")
urlpatterns = [
path(API_BASE_URL, include(router.urls))
path(API_BASE_URL, include(router.urls)),
path('resm/paper/<str:pk>/pdf/', paper_pdf_view, name='paper-pdf'),
]

View File

@ -1,9 +1,31 @@
from django.shortcuts import render
from django.shortcuts import get_object_or_404
from django.http import FileResponse, Http404
from rest_framework.response import Response
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from .models import Paper, PaperAbstract
from .serializers import PaperListSerializer
from apps.utils.viewsets import CustomGenericViewSet, CustomListModelMixin
from rest_framework.permissions import AllowAny
import os
@api_view(['GET'])
@permission_classes([AllowAny])
def paper_pdf_view(request, pk):
paper = get_object_or_404(Paper, pk=pk)
if not paper.has_fulltext_pdf:
raise Http404("PDF not available")
pdf_path = paper.init_paper_path("pdf")
if not os.path.isfile(pdf_path):
raise Http404("PDF file not found on disk")
safe_doi = paper.doi.replace("/", "_")
response = FileResponse(
open(pdf_path, 'rb'),
content_type='application/pdf',
)
response['Content-Disposition'] = f'inline; filename="{safe_doi}.pdf"'
return response
# Create your views here.
class PaperViewSet(CustomGenericViewSet, CustomListModelMixin):

View File

@ -1,4 +1,4 @@
celery==5.6.2
celery==5.6.2
Django==4.2.27
django-celery-beat==2.8.1
django-celery-results==2.6.0
@ -24,3 +24,5 @@ playwright-stealth==2.0.1
pyautogui==0.9.54
pillow>=10.0.0
opencv-python>=4.8.0
DrissionPage>=4.1.0
curl-cffi>=0.7.0

1580
scihub_page.html Normal file

File diff suppressed because one or more lines are too long

BIN
scihub_screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
test.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

660
todo.html Normal file
View File

@ -0,0 +1,660 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chronicle — 待办</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;1,300;1,400&family=Cormorant+SC:wght@300;400;500;600&family=EB+Garamond:ital,wght@0,400;0,500;1,400&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0B0E16;
--bg2: #111520;
--bg3: #181D2B;
--gold: #C9A55E;
--gold-lt: #E0C07A;
--gold-dim: rgba(201,165,94,.28);
--gold-glow: rgba(201,165,94,.12);
--cream: #F0E9DC;
--cream-mid: rgba(240,233,220,.55);
--cream-faint: rgba(240,233,220,.10);
--silver: #7A8499;
--red: #B84E3A;
}
html { scroll-behavior: smooth; }
body {
background: var(--bg);
color: var(--cream);
font-family: 'EB Garamond', Georgia, serif;
font-size: 18px;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 64px 24px 120px;
position: relative;
overflow-x: hidden;
}
/* Radial ambient glow */
body::after {
content: '';
position: fixed;
top: -20%;
left: 50%;
transform: translateX(-50%);
width: 800px;
height: 500px;
background: radial-gradient(ellipse at center, rgba(201,165,94,.06) 0%, transparent 70%);
pointer-events: none;
z-index: 0;
}
/* Noise grain overlay */
body::before {
content: '';
position: fixed;
inset: 0;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.035'/%3E%3C/svg%3E");
pointer-events: none;
z-index: 0;
}
.container {
width: 100%;
max-width: 620px;
position: relative;
z-index: 1;
}
/* ── HEADER ─────────────────────────────────────────── */
.header {
text-align: center;
margin-bottom: 48px;
animation: fadeDown .7s ease both;
}
.eyebrow {
font-family: 'Cormorant SC', serif;
font-size: 10.5px;
letter-spacing: 7px;
color: var(--gold);
text-transform: uppercase;
opacity: .85;
margin-bottom: 14px;
}
.wordmark {
font-family: 'Cormorant SC', serif;
font-size: 62px;
font-weight: 300;
letter-spacing: 10px;
color: var(--cream);
line-height: 1;
margin-bottom: 18px;
text-shadow: 0 0 60px rgba(201,165,94,.15);
}
.dateline {
font-style: italic;
font-size: 15.5px;
color: var(--silver);
letter-spacing: .8px;
}
.ornament-row {
display: flex;
align-items: center;
gap: 14px;
margin: 26px 0 0;
}
.ornament-line {
flex: 1;
height: 1px;
background: linear-gradient(90deg, transparent, var(--gold-dim), transparent);
}
.ornament-gem { color: var(--gold); font-size: 9px; }
/* Progress ring */
.progress-wrap {
margin: 28px auto 0;
width: 72px;
height: 72px;
position: relative;
display: none;
}
.progress-wrap.visible { display: block; }
.progress-ring { transform: rotate(-90deg); display: block; }
.ring-bg { fill: none; stroke: var(--cream-faint); stroke-width: 2; }
.ring-fill {
fill: none;
stroke: var(--gold);
stroke-width: 2;
stroke-linecap: round;
stroke-dasharray: 197.92;
stroke-dashoffset: 197.92;
transition: stroke-dashoffset .7s cubic-bezier(.4,0,.2,1);
}
.ring-label {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Cormorant SC', serif;
font-size: 14px;
color: var(--gold);
}
/* ── INPUT ───────────────────────────────────────────── */
.input-area {
margin-bottom: 36px;
animation: fadeUp .7s .18s ease both;
}
.input-shell {
display: flex;
align-items: center;
background: var(--bg2);
border: 1px solid var(--gold-dim);
padding: 0 18px;
transition: border-color .3s, box-shadow .3s;
}
.input-shell:focus-within {
border-color: rgba(201,165,94,.7);
box-shadow: 0 0 0 1px var(--gold-dim), 0 12px 40px var(--gold-glow);
}
.input-bullet {
color: var(--gold);
font-size: 9px;
margin-right: 14px;
flex-shrink: 0;
opacity: .8;
}
.todo-input {
flex: 1;
background: none;
border: none;
outline: none;
color: var(--cream);
font-family: 'EB Garamond', serif;
font-size: 18px;
padding: 17px 0;
letter-spacing: .2px;
}
.todo-input::placeholder { color: var(--silver); font-style: italic; opacity: .55; }
.add-btn {
background: none;
border: none;
cursor: pointer;
color: var(--gold);
padding: 0 0 0 14px;
display: flex;
align-items: center;
opacity: .65;
transition: opacity .2s, transform .2s;
flex-shrink: 0;
}
.add-btn:hover { opacity: 1; transform: scale(1.18) rotate(90deg); }
/* ── STATS BAR ───────────────────────────────────────── */
.stats-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 2px;
margin-bottom: 20px;
animation: fadeUp .7s .28s ease both;
}
.active-count {
font-family: 'Cormorant SC', serif;
font-size: 13px;
letter-spacing: 3px;
color: var(--silver);
}
.active-count em { color: var(--gold); font-style: normal; font-size: 16px; }
.filters { display: flex; gap: 0; }
.ftab {
background: none;
border: none;
cursor: pointer;
font-family: 'Cormorant SC', serif;
font-size: 12px;
letter-spacing: 2px;
color: var(--silver);
padding: 4px 14px;
transition: color .2s;
position: relative;
}
.ftab::after {
content: '';
position: absolute;
bottom: -1px;
left: 14px; right: 14px;
height: 1px;
background: var(--gold);
transform: scaleX(0);
transition: transform .25s ease;
transform-origin: center;
}
.ftab.on { color: var(--gold-lt); }
.ftab.on::after { transform: scaleX(1); }
.ftab:hover:not(.on) { color: var(--cream-mid); }
/* ── LIST ────────────────────────────────────────────── */
.todo-list {
list-style: none;
animation: fadeUp .7s .38s ease both;
}
.todo-item {
display: flex;
align-items: flex-start;
gap: 15px;
padding: 15px 0;
border-bottom: 1px solid var(--cream-faint);
position: relative;
animation: itemIn .32s ease both;
}
.todo-item:first-child { border-top: 1px solid var(--cream-faint); }
@keyframes itemIn {
from { opacity: 0; transform: translateX(-10px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes itemOut {
to { opacity: 0; transform: translateX(16px); max-height: 0; padding: 0; margin: 0; overflow: hidden; }
}
.todo-item.bye { animation: itemOut .28s ease forwards; }
/* Checkbox */
.chk {
width: 21px; height: 21px;
border: 1px solid var(--gold-dim);
border-radius: 50%;
cursor: pointer;
flex-shrink: 0;
margin-top: 3px;
display: flex; align-items: center; justify-content: center;
transition: border-color .25s, background .25s, box-shadow .25s;
position: relative;
}
.chk:hover { border-color: var(--gold); box-shadow: 0 0 10px rgba(201,165,94,.25); }
.chk.on {
background: var(--gold);
border-color: var(--gold);
box-shadow: 0 0 12px rgba(201,165,94,.3);
}
.chk.on::after {
content: '';
display: block;
width: 5px; height: 9px;
border-right: 1.5px solid var(--bg);
border-bottom: 1.5px solid var(--bg);
transform: rotate(45deg) translate(-1px,-1px);
}
/* Content */
.todo-body { flex: 1; min-width: 0; }
.todo-text {
font-size: 18px;
line-height: 1.55;
color: var(--cream);
word-break: break-word;
outline: none;
cursor: text;
transition: color .3s;
caret-color: var(--gold);
}
.todo-text:focus { color: var(--cream); }
.todo-item.done .todo-text {
color: var(--silver);
text-decoration: line-through;
text-decoration-color: rgba(201,165,94,.35);
text-decoration-thickness: 1px;
}
.todo-time {
font-size: 13px;
font-style: italic;
color: var(--silver);
opacity: .5;
margin-top: 3px;
}
/* Actions */
.todo-acts {
display: flex;
gap: 6px;
align-items: center;
margin-top: 4px;
opacity: 0;
transition: opacity .2s;
}
.todo-item:hover .todo-acts { opacity: 1; }
.act-btn {
background: none;
border: none;
cursor: pointer;
color: var(--silver);
padding: 3px;
display: flex; align-items: center;
transition: color .2s, transform .2s;
opacity: .7;
}
.act-btn:hover { color: var(--red); transform: scale(1.2); opacity: 1; }
/* ── EMPTY STATE ─────────────────────────────────────── */
.empty {
text-align: center;
padding: 64px 20px;
animation: fadeUp .5s ease both;
}
.empty-icon {
font-family: 'Cormorant SC', serif;
font-size: 52px;
color: rgba(201,165,94,.2);
margin-bottom: 16px;
display: block;
line-height: 1;
}
.empty-msg { font-style: italic; color: var(--silver); font-size: 17px; }
/* ── FOOTER ──────────────────────────────────────────── */
.footer {
margin-top: 32px;
display: flex;
justify-content: flex-end;
animation: fadeUp .7s .48s ease both;
}
.clear-btn {
background: none;
border: none;
cursor: pointer;
font-family: 'Cormorant SC', serif;
font-size: 12px;
letter-spacing: 2.5px;
color: var(--silver);
transition: color .2s;
padding: 4px 0;
}
.clear-btn:hover:not(:disabled) { color: var(--red); }
.clear-btn:disabled { opacity: .25; cursor: default; pointer-events: none; }
/* ── KEYFRAMES ───────────────────────────────────────── */
@keyframes fadeDown {
from { opacity: 0; transform: translateY(-22px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeUp {
from { opacity: 0; transform: translateY(22px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes addPulse {
0% { box-shadow: 0 0 0 0 rgba(201,165,94,.45); }
70% { box-shadow: 0 0 0 12px rgba(201,165,94,0); }
100% { box-shadow: 0 0 0 0 rgba(201,165,94,0); }
}
.input-shell.pulse { animation: addPulse .55s ease; }
/* ── SCROLLBAR ───────────────────────────────────────── */
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--gold-dim); border-radius: 3px; }
</style>
</head>
<body>
<div class="container">
<!-- HEADER -->
<header class="header">
<div class="eyebrow">&nbsp; Chronicle &nbsp;</div>
<div class="wordmark">&nbsp;</div>
<div class="dateline" id="dateline"></div>
<div class="ornament-row">
<div class="ornament-line"></div>
<span class="ornament-gem"></span>
<div class="ornament-line"></div>
</div>
<!-- Progress ring -->
<div class="progress-wrap" id="progWrap">
<svg class="progress-ring" width="72" height="72" viewBox="0 0 72 72">
<circle class="ring-bg" cx="36" cy="36" r="31.5"/>
<circle class="ring-fill" id="ringFill" cx="36" cy="36" r="31.5"/>
</svg>
<div class="ring-label" id="ringLabel">0%</div>
</div>
</header>
<!-- INPUT -->
<div class="input-area">
<div class="input-shell" id="inputShell">
<span class="input-bullet"></span>
<input class="todo-input" id="mainInput"
placeholder="记录一件待办事项…"
maxlength="200"
autocomplete="off" />
<button class="add-btn" id="addBtn" title="添加 (Enter)">
<svg width="17" height="17" viewBox="0 0 17 17" fill="none"
stroke="currentColor" stroke-width="1.6" stroke-linecap="round">
<line x1="8.5" y1="2" x2="8.5" y2="15"/>
<line x1="2" y1="8.5" x2="15" y2="8.5"/>
</svg>
</button>
</div>
</div>
<!-- STATS BAR -->
<div class="stats-bar">
<div class="active-count"><em id="cntEl">0</em>&nbsp;项待完成</div>
<div class="filters">
<button class="ftab on" data-f="all">全部</button>
<button class="ftab" data-f="active">进行中</button>
<button class="ftab" data-f="done">已完成</button>
</div>
</div>
<!-- LIST -->
<ul class="todo-list" id="todoList"></ul>
<!-- FOOTER -->
<div class="footer">
<button class="clear-btn" id="clearBtn" disabled>清除已完成</button>
</div>
</div>
<script>
(function () {
/* ── STATE ─────────────────────────────────────── */
let todos = JSON.parse(localStorage.getItem('chronicle_v2') || '[]');
let filter = 'all';
/* ── ELEMENTS ──────────────────────────────────── */
const listEl = document.getElementById('todoList');
const mainInput = document.getElementById('mainInput');
const addBtn = document.getElementById('addBtn');
const cntEl = document.getElementById('cntEl');
const clearBtn = document.getElementById('clearBtn');
const inputShell= document.getElementById('inputShell');
const progWrap = document.getElementById('progWrap');
const ringFill = document.getElementById('ringFill');
const ringLabel = document.getElementById('ringLabel');
const CIRC = 197.92; // 2π × 31.5
/* ── DATE ──────────────────────────────────────── */
const WD = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const now = new Date();
document.getElementById('dateline').textContent =
`${now.getFullYear()} · ${pad(now.getMonth()+1)} · ${pad(now.getDate())} · ${WD[now.getDay()]}`;
function pad(n) { return String(n).padStart(2,'0'); }
function uid() { return Date.now().toString(36) + Math.random().toString(36).slice(2,5); }
function save() { localStorage.setItem('chronicle_v2', JSON.stringify(todos)); }
function ftime(ts) {
const d = new Date(ts);
return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
function esc(s) {
const d = document.createElement('div');
d.appendChild(document.createTextNode(s));
return d.innerHTML;
}
/* ── STATS ─────────────────────────────────────── */
function updateStats() {
const active = todos.filter(t => !t.done).length;
const done = todos.filter(t => t.done).length;
const total = todos.length;
cntEl.textContent = active;
clearBtn.disabled = done === 0;
if (total > 0) {
progWrap.classList.add('visible');
const pct = Math.round(done / total * 100);
const offset = CIRC * (1 - done / total);
ringFill.style.strokeDashoffset = offset;
ringLabel.textContent = pct + '%';
} else {
progWrap.classList.remove('visible');
}
}
/* ── RENDER ────────────────────────────────────── */
const EMPTY = {
all: { icon: '◇', msg: '万事俱备,只欠东风' },
active: { icon: '✦', msg: '所有任务均已完成' },
done: { icon: '○', msg: '尚无已完成的事项' },
};
function render() {
const visible = filter === 'all' ? todos
: filter === 'active' ? todos.filter(t => !t.done)
: todos.filter(t => t.done);
listEl.innerHTML = '';
if (!visible.length) {
const e = EMPTY[filter];
listEl.innerHTML = `
<div class="empty">
<span class="empty-icon">${e.icon}</span>
<p class="empty-msg">${e.msg}</p>
</div>`;
} else {
visible.forEach((t, i) => {
const li = document.createElement('li');
li.className = 'todo-item' + (t.done ? ' done' : '');
li.dataset.id = t.id;
li.style.animationDelay = `${i * .045}s`;
li.innerHTML = `
<div class="chk${t.done ? ' on' : ''}" data-action="toggle" title="切换完成"></div>
<div class="todo-body">
<div class="todo-text" contenteditable="true" data-action="edit"
spellcheck="false">${esc(t.text)}</div>
<div class="todo-time">${ftime(t.createdAt)}</div>
</div>
<div class="todo-acts">
<button class="act-btn" data-action="del" title="删除">
<svg width="13" height="13" viewBox="0 0 13 13" fill="none"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
<line x1="1.5" y1="1.5" x2="11.5" y2="11.5"/>
<line x1="11.5" y1="1.5" x2="1.5" y2="11.5"/>
</svg>
</button>
</div>`;
listEl.appendChild(li);
});
}
updateStats();
}
/* ── ADD ───────────────────────────────────────── */
function add(raw) {
const text = raw.trim();
if (!text) return;
todos.unshift({ id: uid(), text, done: false, createdAt: Date.now() });
save(); render();
inputShell.classList.remove('pulse');
void inputShell.offsetWidth;
inputShell.classList.add('pulse');
}
addBtn.addEventListener('click', () => { add(mainInput.value); mainInput.value = ''; mainInput.focus(); });
mainInput.addEventListener('keydown', e => {
if (e.key === 'Enter') { add(mainInput.value); mainInput.value = ''; }
});
/* ── LIST EVENTS ───────────────────────────────── */
listEl.addEventListener('click', e => {
const src = e.target.closest('[data-action]');
if (!src) return;
const li = src.closest('.todo-item');
if (!li) return;
const id = li.dataset.id;
if (src.dataset.action === 'toggle') {
const t = todos.find(x => x.id === id);
if (t) { t.done = !t.done; save(); render(); }
} else if (src.dataset.action === 'del') {
li.classList.add('bye');
setTimeout(() => { todos = todos.filter(x => x.id !== id); save(); render(); }, 290);
}
});
listEl.addEventListener('blur', e => {
if (e.target.dataset.action !== 'edit') return;
const li = e.target.closest('.todo-item');
if (!li) return;
const id = li.dataset.id;
const text = e.target.textContent.trim();
if (!text) {
todos = todos.filter(x => x.id !== id);
} else {
const t = todos.find(x => x.id === id);
if (t) t.text = text;
}
save(); render();
}, true);
listEl.addEventListener('keydown', e => {
if (e.target.dataset.action === 'edit' && e.key === 'Enter') {
e.preventDefault(); e.target.blur();
}
});
/* ── FILTERS ───────────────────────────────────── */
document.querySelectorAll('.ftab').forEach(btn => {
btn.addEventListener('click', () => {
filter = btn.dataset.f;
document.querySelectorAll('.ftab').forEach(b => b.classList.remove('on'));
btn.classList.add('on');
render();
});
});
/* ── CLEAR DONE ────────────────────────────────── */
clearBtn.addEventListener('click', () => {
document.querySelectorAll('.todo-item.done').forEach(li => li.classList.add('bye'));
setTimeout(() => { todos = todos.filter(t => !t.done); save(); render(); }, 300);
});
/* ── INIT ──────────────────────────────────────── */
render();
})();
</script>
</body>
</html>