Spaces:
Running
Running
A5-R3-R4C dormant ModelPolicy v2 execution seam
#112
by GuXiaopeng - opened
This view is limited to 50 files because it contains too many changes. See the raw diff here.
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$I00E74J.jpg +0 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$I04VJDB.txt +0 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$I2MQAX1.pdf +0 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$I529DC7.png +0 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$I7D7KKM.jpg +0 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$IA7J9YA.docx +0 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$IALBAPU.txt +0 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$IB6I2XK.png +0 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$ID7299H +0 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$IEBS53S.txt +0 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$IFYIHXZ.jpg +0 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$IHCDLMF.js +0 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$IINFFGJ +0 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$IJDLI7J.docx +0 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$IKDVQ6V.jpg +0 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$ILK7V2E.txt +0 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$INQ4QNI.jpg +0 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$IO1UTXL.txt +0 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$ITJV97I.txt +0 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$IV23KEN.dotm +0 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$IVC0V5I.lnk +0 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$IXIUXTC.py +0 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$R00E74J.jpg +3 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$R04VJDB.txt +1 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$R2MQAX1.pdf +3 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$R529DC7.png +3 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$R7D7KKM.jpg +3 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RA7J9YA.docx +3 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RALBAPU.txt +2140 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RB6I2XK.png +0 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/.gitattributes +35 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/.streamlit/config.toml +8 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/.streamlit/desktop.ini +2 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/HF_COMMIT_SHA.txt +1 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/Plaintext +11 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/README.md +14 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/app.py +1378 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/config.py +404 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/core/desktop.ini +2 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/core/meta_extractor.py +52 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/core/pipeline_agent.py +0 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/core/production_guard.py +182 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/core/qualitative_engine.py +248 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/core/quantitative_engine.py +230 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/core/rag_engine.py +62 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/desktop.ini +2 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/i18n.py +856 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/managed_auth.py +645 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/requirements.txt +14 -0
- $RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/smartpaper_restore_bridge/__init__.py +81 -0
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$I00E74J.jpg
ADDED
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$I04VJDB.txt
ADDED
|
Binary file (98 Bytes). View file
|
|
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$I2MQAX1.pdf
ADDED
|
Binary file (322 Bytes). View file
|
|
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$I529DC7.png
ADDED
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$I7D7KKM.jpg
ADDED
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$IA7J9YA.docx
ADDED
|
Binary file (214 Bytes). View file
|
|
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$IALBAPU.txt
ADDED
|
Binary file (96 Bytes). View file
|
|
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$IB6I2XK.png
ADDED
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$ID7299H
ADDED
|
Binary file (98 Bytes). View file
|
|
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$IEBS53S.txt
ADDED
|
Binary file (88 Bytes). View file
|
|
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$IFYIHXZ.jpg
ADDED
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$IHCDLMF.js
ADDED
|
Binary file (94 Bytes). View file
|
|
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$IINFFGJ
ADDED
|
Binary file (144 Bytes). View file
|
|
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$IJDLI7J.docx
ADDED
|
Binary file (98 Bytes). View file
|
|
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$IKDVQ6V.jpg
ADDED
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$ILK7V2E.txt
ADDED
|
Binary file (96 Bytes). View file
|
|
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$INQ4QNI.jpg
ADDED
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$IO1UTXL.txt
ADDED
|
Binary file (158 Bytes). View file
|
|
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$ITJV97I.txt
ADDED
|
Binary file (98 Bytes). View file
|
|
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$IV23KEN.dotm
ADDED
|
Binary file (154 Bytes). View file
|
|
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$IVC0V5I.lnk
ADDED
|
Binary file (108 Bytes). View file
|
|
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$IXIUXTC.py
ADDED
|
Binary file (88 Bytes). View file
|
|
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$R00E74J.jpg
ADDED
|
Git LFS Details
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$R04VJDB.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
restore 0000006d-00000000-000050bf-708b24b133fed5049ff96a4fcb126551
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$R2MQAX1.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:40017ada62558766946a1de340fdfc3f332d42322cc3b1819ae4e9a4396bff98
|
| 3 |
+
size 1211510
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$R529DC7.png
ADDED
|
Git LFS Details
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$R7D7KKM.jpg
ADDED
|
Git LFS Details
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RA7J9YA.docx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:be51e4935cc545417d84b1adb3f4ff634349da099c1634e9f120fdbb91cc7f53
|
| 3 |
+
size 1046928
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RALBAPU.txt
ADDED
|
@@ -0,0 +1,2140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ============================================================
|
| 2 |
+
* SmartPaper Authentication Worker
|
| 3 |
+
*
|
| 4 |
+
* 已实现:
|
| 5 |
+
* 1. 支付宝扫码授权
|
| 6 |
+
* 2. RSA2调用 alipay.system.oauth.token
|
| 7 |
+
* 3. 创建或匹配SmartPaper用户
|
| 8 |
+
* 4. Hugging Face创建登录请求
|
| 9 |
+
* 5. Hugging Face轮询扫码状态
|
| 10 |
+
* 6. 签发8小时短期登录会话
|
| 11 |
+
* 7. 验证会话
|
| 12 |
+
* 8. 注销会话
|
| 13 |
+
*
|
| 14 |
+
* 固定业务规则:
|
| 15 |
+
* - 每位用户每天免费3篇论文
|
| 16 |
+
* - 9.9元增加3篇长期有效论文
|
| 17 |
+
*
|
| 18 |
+
* 尚未实现:
|
| 19 |
+
* - 每日免费篇数的实际扣减
|
| 20 |
+
* - 论文生成任务与自动排队
|
| 21 |
+
* - 支付订单和自动增加论文
|
| 22 |
+
* ============================================================ */
|
| 23 |
+
|
| 24 |
+
const LOGIN_REQUEST_TTL_MS = 10 * 60 * 1000;
|
| 25 |
+
const SESSION_TTL_MS = 8 * 60 * 60 * 1000;
|
| 26 |
+
|
| 27 |
+
const DAILY_FREE_PAPERS = 3;
|
| 28 |
+
const PAYMENT_AMOUNT_FEN = 990;
|
| 29 |
+
const PAYMENT_BONUS_PAPERS = 3;
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
/* ============================================================
|
| 33 |
+
* 响应工具
|
| 34 |
+
* ============================================================ */
|
| 35 |
+
|
| 36 |
+
function commonHeaders(contentType) {
|
| 37 |
+
return {
|
| 38 |
+
"Content-Type": contentType,
|
| 39 |
+
"Cache-Control": "no-store",
|
| 40 |
+
"X-Content-Type-Options": "nosniff",
|
| 41 |
+
"Referrer-Policy": "no-referrer",
|
| 42 |
+
"X-Frame-Options": "DENY",
|
| 43 |
+
};
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
function jsonResponse(data, status = 200) {
|
| 48 |
+
return new Response(
|
| 49 |
+
JSON.stringify(data, null, 2),
|
| 50 |
+
{
|
| 51 |
+
status,
|
| 52 |
+
headers: commonHeaders(
|
| 53 |
+
"application/json; charset=utf-8"
|
| 54 |
+
),
|
| 55 |
+
}
|
| 56 |
+
);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
function htmlResponse(title, body, status = 200) {
|
| 61 |
+
return new Response(
|
| 62 |
+
`<!doctype html>
|
| 63 |
+
<html lang="zh-CN">
|
| 64 |
+
<head>
|
| 65 |
+
<meta charset="utf-8">
|
| 66 |
+
|
| 67 |
+
<meta
|
| 68 |
+
name="viewport"
|
| 69 |
+
content="width=device-width, initial-scale=1"
|
| 70 |
+
>
|
| 71 |
+
|
| 72 |
+
<title>${escapeHtml(title)}</title>
|
| 73 |
+
|
| 74 |
+
<style>
|
| 75 |
+
:root {
|
| 76 |
+
color-scheme: light dark;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
body {
|
| 80 |
+
max-width: 760px;
|
| 81 |
+
margin: 50px auto;
|
| 82 |
+
padding: 0 20px;
|
| 83 |
+
font-family:
|
| 84 |
+
Arial,
|
| 85 |
+
"Microsoft YaHei",
|
| 86 |
+
"PingFang SC",
|
| 87 |
+
sans-serif;
|
| 88 |
+
line-height: 1.8;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
h1 {
|
| 92 |
+
margin-top: 0;
|
| 93 |
+
font-size: 28px;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.card {
|
| 97 |
+
padding: 28px;
|
| 98 |
+
border: 1px solid #8885;
|
| 99 |
+
border-radius: 14px;
|
| 100 |
+
background: #8881;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.success {
|
| 104 |
+
color: #21a33b;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.error {
|
| 108 |
+
color: #d83b3b;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.button {
|
| 112 |
+
display: inline-block;
|
| 113 |
+
margin-top: 16px;
|
| 114 |
+
padding: 11px 20px;
|
| 115 |
+
border-radius: 8px;
|
| 116 |
+
background: #1677ff;
|
| 117 |
+
color: #ffffff;
|
| 118 |
+
text-decoration: none;
|
| 119 |
+
font-weight: 600;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.button:hover {
|
| 123 |
+
background: #0d63d7;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
code {
|
| 127 |
+
word-break: break-all;
|
| 128 |
+
padding: 2px 5px;
|
| 129 |
+
border-radius: 4px;
|
| 130 |
+
background: #8882;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.muted {
|
| 134 |
+
opacity: 0.72;
|
| 135 |
+
font-size: 14px;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.links {
|
| 139 |
+
margin-top: 26px;
|
| 140 |
+
}
|
| 141 |
+
</style>
|
| 142 |
+
</head>
|
| 143 |
+
|
| 144 |
+
<body>
|
| 145 |
+
${body}
|
| 146 |
+
</body>
|
| 147 |
+
</html>`,
|
| 148 |
+
{
|
| 149 |
+
status,
|
| 150 |
+
headers: commonHeaders(
|
| 151 |
+
"text/html; charset=utf-8"
|
| 152 |
+
),
|
| 153 |
+
}
|
| 154 |
+
);
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
function escapeHtml(value) {
|
| 159 |
+
return String(value ?? "")
|
| 160 |
+
.replaceAll("&", "&")
|
| 161 |
+
.replaceAll("<", "<")
|
| 162 |
+
.replaceAll(">", ">")
|
| 163 |
+
.replaceAll('"', """)
|
| 164 |
+
.replaceAll("'", "'");
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
/* ============================================================
|
| 169 |
+
* 请求工具
|
| 170 |
+
* ============================================================ */
|
| 171 |
+
|
| 172 |
+
async function readJsonBody(request) {
|
| 173 |
+
try {
|
| 174 |
+
return await request.json();
|
| 175 |
+
} catch {
|
| 176 |
+
return null;
|
| 177 |
+
}
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
function extractServiceKey(request) {
|
| 182 |
+
const authorization =
|
| 183 |
+
request.headers.get("Authorization") || "";
|
| 184 |
+
|
| 185 |
+
if (authorization.startsWith("Bearer ")) {
|
| 186 |
+
return authorization.slice(7).trim();
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
return (
|
| 190 |
+
request.headers.get(
|
| 191 |
+
"X-SmartPaper-Service-Key"
|
| 192 |
+
) || ""
|
| 193 |
+
).trim();
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
/* ============================================================
|
| 198 |
+
* 随机数、摘要和HMAC
|
| 199 |
+
* ============================================================ */
|
| 200 |
+
|
| 201 |
+
function randomToken(byteLength = 32) {
|
| 202 |
+
const bytes = new Uint8Array(byteLength);
|
| 203 |
+
crypto.getRandomValues(bytes);
|
| 204 |
+
|
| 205 |
+
let binary = "";
|
| 206 |
+
|
| 207 |
+
for (const byte of bytes) {
|
| 208 |
+
binary += String.fromCharCode(byte);
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
return btoa(binary)
|
| 212 |
+
.replaceAll("+", "-")
|
| 213 |
+
.replaceAll("/", "_")
|
| 214 |
+
.replaceAll("=", "");
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
async function sha256Bytes(value) {
|
| 219 |
+
const encoded =
|
| 220 |
+
new TextEncoder().encode(String(value));
|
| 221 |
+
|
| 222 |
+
return new Uint8Array(
|
| 223 |
+
await crypto.subtle.digest(
|
| 224 |
+
"SHA-256",
|
| 225 |
+
encoded
|
| 226 |
+
)
|
| 227 |
+
);
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
async function sha256Hex(value) {
|
| 232 |
+
const bytes = await sha256Bytes(value);
|
| 233 |
+
|
| 234 |
+
return [...bytes]
|
| 235 |
+
.map((byte) =>
|
| 236 |
+
byte.toString(16).padStart(2, "0")
|
| 237 |
+
)
|
| 238 |
+
.join("");
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
async function importHmacKey(secret) {
|
| 243 |
+
return crypto.subtle.importKey(
|
| 244 |
+
"raw",
|
| 245 |
+
new TextEncoder().encode(secret),
|
| 246 |
+
{
|
| 247 |
+
name: "HMAC",
|
| 248 |
+
hash: "SHA-256",
|
| 249 |
+
},
|
| 250 |
+
false,
|
| 251 |
+
["sign"]
|
| 252 |
+
);
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
async function hmacHex(value, secret) {
|
| 257 |
+
if (!secret) {
|
| 258 |
+
throw new Error(
|
| 259 |
+
"SESSION_SIGNING_SECRET未配置。"
|
| 260 |
+
);
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
const key = await importHmacKey(secret);
|
| 264 |
+
|
| 265 |
+
const signature =
|
| 266 |
+
await crypto.subtle.sign(
|
| 267 |
+
"HMAC",
|
| 268 |
+
key,
|
| 269 |
+
new TextEncoder().encode(value)
|
| 270 |
+
);
|
| 271 |
+
|
| 272 |
+
return [...new Uint8Array(signature)]
|
| 273 |
+
.map((byte) =>
|
| 274 |
+
byte.toString(16).padStart(2, "0")
|
| 275 |
+
)
|
| 276 |
+
.join("");
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
async function constantTimeTextEqual(
|
| 281 |
+
valueA,
|
| 282 |
+
valueB
|
| 283 |
+
) {
|
| 284 |
+
const bytesA = await sha256Bytes(valueA);
|
| 285 |
+
const bytesB = await sha256Bytes(valueB);
|
| 286 |
+
|
| 287 |
+
if (bytesA.length !== bytesB.length) {
|
| 288 |
+
return false;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
let difference = 0;
|
| 292 |
+
|
| 293 |
+
for (let i = 0; i < bytesA.length; i += 1) {
|
| 294 |
+
difference |= bytesA[i] ^ bytesB[i];
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
return difference === 0;
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
async function isAuthorizedServiceRequest(
|
| 302 |
+
request,
|
| 303 |
+
env
|
| 304 |
+
) {
|
| 305 |
+
if (!env.SMARTPAPER_SERVICE_KEY) {
|
| 306 |
+
return false;
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
const suppliedKey =
|
| 310 |
+
extractServiceKey(request);
|
| 311 |
+
|
| 312 |
+
if (!suppliedKey) {
|
| 313 |
+
return false;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
return constantTimeTextEqual(
|
| 317 |
+
suppliedKey,
|
| 318 |
+
env.SMARTPAPER_SERVICE_KEY
|
| 319 |
+
);
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
async function requireServiceRequest(
|
| 324 |
+
request,
|
| 325 |
+
env
|
| 326 |
+
) {
|
| 327 |
+
const authorized =
|
| 328 |
+
await isAuthorizedServiceRequest(
|
| 329 |
+
request,
|
| 330 |
+
env
|
| 331 |
+
);
|
| 332 |
+
|
| 333 |
+
if (!authorized) {
|
| 334 |
+
return jsonResponse(
|
| 335 |
+
{
|
| 336 |
+
ok: false,
|
| 337 |
+
error: "unauthorized",
|
| 338 |
+
},
|
| 339 |
+
401
|
| 340 |
+
);
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
return null;
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
/* ============================================================
|
| 348 |
+
* 时间工具
|
| 349 |
+
* ============================================================ */
|
| 350 |
+
|
| 351 |
+
function getBeijingTimestamp() {
|
| 352 |
+
const parts =
|
| 353 |
+
new Intl.DateTimeFormat(
|
| 354 |
+
"zh-CN",
|
| 355 |
+
{
|
| 356 |
+
timeZone: "Asia/Shanghai",
|
| 357 |
+
year: "numeric",
|
| 358 |
+
month: "2-digit",
|
| 359 |
+
day: "2-digit",
|
| 360 |
+
hour: "2-digit",
|
| 361 |
+
minute: "2-digit",
|
| 362 |
+
second: "2-digit",
|
| 363 |
+
hourCycle: "h23",
|
| 364 |
+
}
|
| 365 |
+
).formatToParts(new Date());
|
| 366 |
+
|
| 367 |
+
const values = Object.fromEntries(
|
| 368 |
+
parts
|
| 369 |
+
.filter((part) =>
|
| 370 |
+
part.type !== "literal"
|
| 371 |
+
)
|
| 372 |
+
.map((part) => [
|
| 373 |
+
part.type,
|
| 374 |
+
part.value,
|
| 375 |
+
])
|
| 376 |
+
);
|
| 377 |
+
|
| 378 |
+
return (
|
| 379 |
+
`${values.year}-${values.month}-${values.day} ` +
|
| 380 |
+
`${values.hour}:${values.minute}:${values.second}`
|
| 381 |
+
);
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
function isExpired(isoDate) {
|
| 386 |
+
if (!isoDate) {
|
| 387 |
+
return true;
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
const timestamp = Date.parse(isoDate);
|
| 391 |
+
|
| 392 |
+
return (
|
| 393 |
+
Number.isNaN(timestamp) ||
|
| 394 |
+
timestamp <= Date.now()
|
| 395 |
+
);
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
/* ============================================================
|
| 400 |
+
* RSA2应用私钥和支付宝签名
|
| 401 |
+
* ============================================================ */
|
| 402 |
+
|
| 403 |
+
function normalizePrivateKey(
|
| 404 |
+
privateKeyText
|
| 405 |
+
) {
|
| 406 |
+
return String(privateKeyText ?? "")
|
| 407 |
+
.replaceAll("\\n", "\n")
|
| 408 |
+
.replace(
|
| 409 |
+
/-----BEGIN PRIVATE KEY-----/g,
|
| 410 |
+
""
|
| 411 |
+
)
|
| 412 |
+
.replace(
|
| 413 |
+
/-----END PRIVATE KEY-----/g,
|
| 414 |
+
""
|
| 415 |
+
)
|
| 416 |
+
.replace(
|
| 417 |
+
/-----BEGIN RSA PRIVATE KEY-----/g,
|
| 418 |
+
""
|
| 419 |
+
)
|
| 420 |
+
.replace(
|
| 421 |
+
/-----END RSA PRIVATE KEY-----/g,
|
| 422 |
+
""
|
| 423 |
+
)
|
| 424 |
+
.replace(/\s+/g, "");
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
function base64ToBytes(base64Text) {
|
| 429 |
+
const normalized =
|
| 430 |
+
String(base64Text ?? "")
|
| 431 |
+
.replace(/\s+/g, "");
|
| 432 |
+
|
| 433 |
+
const binary = atob(normalized);
|
| 434 |
+
|
| 435 |
+
const bytes =
|
| 436 |
+
new Uint8Array(binary.length);
|
| 437 |
+
|
| 438 |
+
for (
|
| 439 |
+
let index = 0;
|
| 440 |
+
index < binary.length;
|
| 441 |
+
index += 1
|
| 442 |
+
) {
|
| 443 |
+
bytes[index] =
|
| 444 |
+
binary.charCodeAt(index);
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
return bytes;
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
|
| 451 |
+
function arrayBufferToBase64(buffer) {
|
| 452 |
+
const bytes = new Uint8Array(buffer);
|
| 453 |
+
|
| 454 |
+
let binary = "";
|
| 455 |
+
|
| 456 |
+
for (
|
| 457 |
+
let index = 0;
|
| 458 |
+
index < bytes.length;
|
| 459 |
+
index += 1
|
| 460 |
+
) {
|
| 461 |
+
binary += String.fromCharCode(
|
| 462 |
+
bytes[index]
|
| 463 |
+
);
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
return btoa(binary);
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
|
| 470 |
+
async function importAlipayPrivateKey(
|
| 471 |
+
privateKeyText
|
| 472 |
+
) {
|
| 473 |
+
const normalizedKey =
|
| 474 |
+
normalizePrivateKey(privateKeyText);
|
| 475 |
+
|
| 476 |
+
if (!normalizedKey) {
|
| 477 |
+
throw new Error(
|
| 478 |
+
"Cloudflare中没有配置支付宝应用私钥。"
|
| 479 |
+
);
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
let keyBytes;
|
| 483 |
+
|
| 484 |
+
try {
|
| 485 |
+
keyBytes =
|
| 486 |
+
base64ToBytes(normalizedKey);
|
| 487 |
+
} catch {
|
| 488 |
+
throw new Error(
|
| 489 |
+
"支付宝应��私钥不是有效的Base64格式。"
|
| 490 |
+
);
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
try {
|
| 494 |
+
return await crypto.subtle.importKey(
|
| 495 |
+
"pkcs8",
|
| 496 |
+
keyBytes,
|
| 497 |
+
{
|
| 498 |
+
name: "RSASSA-PKCS1-v1_5",
|
| 499 |
+
hash: "SHA-256",
|
| 500 |
+
},
|
| 501 |
+
false,
|
| 502 |
+
["sign"]
|
| 503 |
+
);
|
| 504 |
+
} catch (error) {
|
| 505 |
+
throw new Error(
|
| 506 |
+
"无法导入支付宝应用私钥。" +
|
| 507 |
+
"请确认该值是与支付宝应用公钥配对的" +
|
| 508 |
+
"RSA2 PKCS8应用私钥。" +
|
| 509 |
+
` 原始错误:${String(error)}`
|
| 510 |
+
);
|
| 511 |
+
}
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
|
| 515 |
+
function buildAlipaySignContent(
|
| 516 |
+
parameters
|
| 517 |
+
) {
|
| 518 |
+
return Object.entries(parameters)
|
| 519 |
+
.filter(([key, value]) => (
|
| 520 |
+
key !== "sign" &&
|
| 521 |
+
value !== undefined &&
|
| 522 |
+
value !== null &&
|
| 523 |
+
value !== ""
|
| 524 |
+
))
|
| 525 |
+
.sort(([keyA], [keyB]) => {
|
| 526 |
+
if (keyA < keyB) return -1;
|
| 527 |
+
if (keyA > keyB) return 1;
|
| 528 |
+
return 0;
|
| 529 |
+
})
|
| 530 |
+
.map(([key, value]) =>
|
| 531 |
+
`${key}=${String(value)}`
|
| 532 |
+
)
|
| 533 |
+
.join("&");
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
|
| 537 |
+
async function createAlipaySignature(
|
| 538 |
+
parameters,
|
| 539 |
+
privateKeyText
|
| 540 |
+
) {
|
| 541 |
+
const contentToSign =
|
| 542 |
+
buildAlipaySignContent(parameters);
|
| 543 |
+
|
| 544 |
+
const privateKey =
|
| 545 |
+
await importAlipayPrivateKey(
|
| 546 |
+
privateKeyText
|
| 547 |
+
);
|
| 548 |
+
|
| 549 |
+
const signature =
|
| 550 |
+
await crypto.subtle.sign(
|
| 551 |
+
{
|
| 552 |
+
name: "RSASSA-PKCS1-v1_5",
|
| 553 |
+
},
|
| 554 |
+
privateKey,
|
| 555 |
+
new TextEncoder().encode(
|
| 556 |
+
contentToSign
|
| 557 |
+
)
|
| 558 |
+
);
|
| 559 |
+
|
| 560 |
+
return arrayBufferToBase64(
|
| 561 |
+
signature
|
| 562 |
+
);
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
|
| 566 |
+
/* ============================================================
|
| 567 |
+
* 配置检查
|
| 568 |
+
* ============================================================ */
|
| 569 |
+
|
| 570 |
+
function getMissingConfiguration(env) {
|
| 571 |
+
const requiredVariables = [
|
| 572 |
+
"ALIPAY_APP_ID",
|
| 573 |
+
"ALIPAY_APP_PRIVATE_KEY",
|
| 574 |
+
"ALIPAY_GATEWAY",
|
| 575 |
+
"ALIPAY_REDIRECT_URI",
|
| 576 |
+
"SESSION_SIGNING_SECRET",
|
| 577 |
+
"SMARTPAPER_SERVICE_KEY",
|
| 578 |
+
];
|
| 579 |
+
|
| 580 |
+
const missing =
|
| 581 |
+
requiredVariables.filter(
|
| 582 |
+
(name) => !env[name]
|
| 583 |
+
);
|
| 584 |
+
|
| 585 |
+
if (!env.DB) {
|
| 586 |
+
missing.push("DB binding");
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
return missing;
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
|
| 593 |
+
/* ============================================================
|
| 594 |
+
* 支付宝登录请求
|
| 595 |
+
* ============================================================ */
|
| 596 |
+
|
| 597 |
+
function buildAlipayAuthorizationUrl(
|
| 598 |
+
env,
|
| 599 |
+
state
|
| 600 |
+
) {
|
| 601 |
+
const authorizationUrl = new URL(
|
| 602 |
+
"https://openauth.alipay.com/oauth2/publicAppAuthorize.htm"
|
| 603 |
+
);
|
| 604 |
+
|
| 605 |
+
authorizationUrl.searchParams.set(
|
| 606 |
+
"app_id",
|
| 607 |
+
env.ALIPAY_APP_ID
|
| 608 |
+
);
|
| 609 |
+
|
| 610 |
+
authorizationUrl.searchParams.set(
|
| 611 |
+
"scope",
|
| 612 |
+
"auth_user"
|
| 613 |
+
);
|
| 614 |
+
|
| 615 |
+
authorizationUrl.searchParams.set(
|
| 616 |
+
"redirect_uri",
|
| 617 |
+
env.ALIPAY_REDIRECT_URI
|
| 618 |
+
);
|
| 619 |
+
|
| 620 |
+
authorizationUrl.searchParams.set(
|
| 621 |
+
"state",
|
| 622 |
+
state
|
| 623 |
+
);
|
| 624 |
+
|
| 625 |
+
return authorizationUrl.toString();
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
|
| 629 |
+
async function createLoginRequestRecord(env) {
|
| 630 |
+
const state = randomToken(32);
|
| 631 |
+
const pollToken = randomToken(32);
|
| 632 |
+
|
| 633 |
+
const stateHash =
|
| 634 |
+
await sha256Hex(state);
|
| 635 |
+
|
| 636 |
+
const pollTokenHash =
|
| 637 |
+
await hmacHex(
|
| 638 |
+
pollToken,
|
| 639 |
+
env.SESSION_SIGNING_SECRET
|
| 640 |
+
);
|
| 641 |
+
|
| 642 |
+
const loginRequestId =
|
| 643 |
+
crypto.randomUUID();
|
| 644 |
+
|
| 645 |
+
const expiresAt = new Date(
|
| 646 |
+
Date.now() + LOGIN_REQUEST_TTL_MS
|
| 647 |
+
).toISOString();
|
| 648 |
+
|
| 649 |
+
await env.DB.prepare(
|
| 650 |
+
`INSERT INTO oauth_states
|
| 651 |
+
(
|
| 652 |
+
state_hash,
|
| 653 |
+
login_request_id,
|
| 654 |
+
status,
|
| 655 |
+
created_at,
|
| 656 |
+
expires_at,
|
| 657 |
+
poll_token_hash
|
| 658 |
+
)
|
| 659 |
+
VALUES
|
| 660 |
+
(
|
| 661 |
+
?,
|
| 662 |
+
?,
|
| 663 |
+
'waiting',
|
| 664 |
+
CURRENT_TIMESTAMP,
|
| 665 |
+
?,
|
| 666 |
+
?
|
| 667 |
+
)`
|
| 668 |
+
)
|
| 669 |
+
.bind(
|
| 670 |
+
stateHash,
|
| 671 |
+
loginRequestId,
|
| 672 |
+
expiresAt,
|
| 673 |
+
pollTokenHash
|
| 674 |
+
)
|
| 675 |
+
.run();
|
| 676 |
+
|
| 677 |
+
return {
|
| 678 |
+
state,
|
| 679 |
+
pollToken,
|
| 680 |
+
loginRequestId,
|
| 681 |
+
expiresAt,
|
| 682 |
+
authorizationUrl:
|
| 683 |
+
buildAlipayAuthorizationUrl(
|
| 684 |
+
env,
|
| 685 |
+
state
|
| 686 |
+
),
|
| 687 |
+
};
|
| 688 |
+
}
|
| 689 |
+
|
| 690 |
+
|
| 691 |
+
async function handleManualAlipayLogin(env) {
|
| 692 |
+
const loginRequest =
|
| 693 |
+
await createLoginRequestRecord(env);
|
| 694 |
+
|
| 695 |
+
return Response.redirect(
|
| 696 |
+
loginRequest.authorizationUrl,
|
| 697 |
+
302
|
| 698 |
+
);
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
|
| 702 |
+
/* ============================================================
|
| 703 |
+
* auth_code换取支付宝用户标识
|
| 704 |
+
* ============================================================ */
|
| 705 |
+
|
| 706 |
+
async function exchangeAlipayAuthCode(
|
| 707 |
+
authCode,
|
| 708 |
+
env
|
| 709 |
+
) {
|
| 710 |
+
const parameters = {
|
| 711 |
+
app_id: env.ALIPAY_APP_ID,
|
| 712 |
+
method:
|
| 713 |
+
"alipay.system.oauth.token",
|
| 714 |
+
format: "JSON",
|
| 715 |
+
charset: "utf-8",
|
| 716 |
+
sign_type: "RSA2",
|
| 717 |
+
timestamp:
|
| 718 |
+
getBeijingTimestamp(),
|
| 719 |
+
version: "1.0",
|
| 720 |
+
grant_type:
|
| 721 |
+
"authorization_code",
|
| 722 |
+
code: authCode,
|
| 723 |
+
};
|
| 724 |
+
|
| 725 |
+
parameters.sign =
|
| 726 |
+
await createAlipaySignature(
|
| 727 |
+
parameters,
|
| 728 |
+
env.ALIPAY_APP_PRIVATE_KEY
|
| 729 |
+
);
|
| 730 |
+
|
| 731 |
+
const requestBody =
|
| 732 |
+
new URLSearchParams();
|
| 733 |
+
|
| 734 |
+
for (
|
| 735 |
+
const [key, value]
|
| 736 |
+
of Object.entries(parameters)
|
| 737 |
+
) {
|
| 738 |
+
requestBody.set(
|
| 739 |
+
key,
|
| 740 |
+
String(value)
|
| 741 |
+
);
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
const response = await fetch(
|
| 745 |
+
env.ALIPAY_GATEWAY,
|
| 746 |
+
{
|
| 747 |
+
method: "POST",
|
| 748 |
+
headers: {
|
| 749 |
+
"Content-Type":
|
| 750 |
+
"application/x-www-form-urlencoded;charset=utf-8",
|
| 751 |
+
},
|
| 752 |
+
body: requestBody.toString(),
|
| 753 |
+
}
|
| 754 |
+
);
|
| 755 |
+
|
| 756 |
+
const rawText =
|
| 757 |
+
await response.text();
|
| 758 |
+
|
| 759 |
+
let payload;
|
| 760 |
+
|
| 761 |
+
try {
|
| 762 |
+
payload = JSON.parse(rawText);
|
| 763 |
+
} catch {
|
| 764 |
+
throw new Error(
|
| 765 |
+
"支付宝返回了无法解析的内容。" +
|
| 766 |
+
`HTTP状态码:${response.status}`
|
| 767 |
+
);
|
| 768 |
+
}
|
| 769 |
+
|
| 770 |
+
const tokenData =
|
| 771 |
+
payload
|
| 772 |
+
.alipay_system_oauth_token_response;
|
| 773 |
+
|
| 774 |
+
const errorData =
|
| 775 |
+
payload.error_response;
|
| 776 |
+
|
| 777 |
+
if (errorData) {
|
| 778 |
+
throw new Error(
|
| 779 |
+
`支付宝接口错误:` +
|
| 780 |
+
`${errorData.sub_code ||
|
| 781 |
+
errorData.code ||
|
| 782 |
+
"unknown"};` +
|
| 783 |
+
`${errorData.sub_msg ||
|
| 784 |
+
errorData.msg ||
|
| 785 |
+
"未知错误"}`
|
| 786 |
+
);
|
| 787 |
+
}
|
| 788 |
+
|
| 789 |
+
if (!response.ok || !tokenData) {
|
| 790 |
+
throw new Error(
|
| 791 |
+
"支付宝令牌接口没有返回预期结果。"
|
| 792 |
+
);
|
| 793 |
+
}
|
| 794 |
+
|
| 795 |
+
if (
|
| 796 |
+
tokenData.code &&
|
| 797 |
+
String(tokenData.code) !== "10000"
|
| 798 |
+
) {
|
| 799 |
+
throw new Error(
|
| 800 |
+
`支付宝接口错误:` +
|
| 801 |
+
`${tokenData.sub_code ||
|
| 802 |
+
tokenData.code};` +
|
| 803 |
+
`${tokenData.sub_msg ||
|
| 804 |
+
tokenData.msg ||
|
| 805 |
+
"未知错误"}`
|
| 806 |
+
);
|
| 807 |
+
}
|
| 808 |
+
|
| 809 |
+
const openId =
|
| 810 |
+
tokenData.open_id || null;
|
| 811 |
+
|
| 812 |
+
const userId =
|
| 813 |
+
tokenData.user_id ||
|
| 814 |
+
tokenData.alipay_user_id ||
|
| 815 |
+
null;
|
| 816 |
+
|
| 817 |
+
if (!openId && !userId) {
|
| 818 |
+
throw new Error(
|
| 819 |
+
"支付宝授权成功," +
|
| 820 |
+
"但没有返回open_id或user_id。"
|
| 821 |
+
);
|
| 822 |
+
}
|
| 823 |
+
|
| 824 |
+
return {
|
| 825 |
+
openId,
|
| 826 |
+
userId,
|
| 827 |
+
};
|
| 828 |
+
}
|
| 829 |
+
|
| 830 |
+
|
| 831 |
+
/* ============================================================
|
| 832 |
+
* 用户账户
|
| 833 |
+
* ============================================================ */
|
| 834 |
+
|
| 835 |
+
async function findExistingUser(
|
| 836 |
+
env,
|
| 837 |
+
openId,
|
| 838 |
+
alipayUserId
|
| 839 |
+
) {
|
| 840 |
+
if (openId) {
|
| 841 |
+
const userByOpenId =
|
| 842 |
+
await env.DB.prepare(
|
| 843 |
+
`SELECT
|
| 844 |
+
id,
|
| 845 |
+
role,
|
| 846 |
+
status,
|
| 847 |
+
bonus_papers
|
| 848 |
+
FROM users
|
| 849 |
+
WHERE alipay_open_id = ?
|
| 850 |
+
LIMIT 1`
|
| 851 |
+
)
|
| 852 |
+
.bind(openId)
|
| 853 |
+
.first();
|
| 854 |
+
|
| 855 |
+
if (userByOpenId) {
|
| 856 |
+
return userByOpenId;
|
| 857 |
+
}
|
| 858 |
+
}
|
| 859 |
+
|
| 860 |
+
if (alipayUserId) {
|
| 861 |
+
const userByUserId =
|
| 862 |
+
await env.DB.prepare(
|
| 863 |
+
`SELECT
|
| 864 |
+
id,
|
| 865 |
+
role,
|
| 866 |
+
status,
|
| 867 |
+
bonus_papers
|
| 868 |
+
FROM users
|
| 869 |
+
WHERE alipay_user_id = ?
|
| 870 |
+
LIMIT 1`
|
| 871 |
+
)
|
| 872 |
+
.bind(alipayUserId)
|
| 873 |
+
.first();
|
| 874 |
+
|
| 875 |
+
if (userByUserId) {
|
| 876 |
+
return userByUserId;
|
| 877 |
+
}
|
| 878 |
+
}
|
| 879 |
+
|
| 880 |
+
return null;
|
| 881 |
+
}
|
| 882 |
+
|
| 883 |
+
|
| 884 |
+
async function findOrCreateSmartPaperUser(
|
| 885 |
+
env,
|
| 886 |
+
openId,
|
| 887 |
+
alipayUserId
|
| 888 |
+
) {
|
| 889 |
+
const existingUser =
|
| 890 |
+
await findExistingUser(
|
| 891 |
+
env,
|
| 892 |
+
openId,
|
| 893 |
+
alipayUserId
|
| 894 |
+
);
|
| 895 |
+
|
| 896 |
+
if (existingUser) {
|
| 897 |
+
await env.DB.prepare(
|
| 898 |
+
`UPDATE users
|
| 899 |
+
SET
|
| 900 |
+
alipay_open_id =
|
| 901 |
+
COALESCE(?, alipay_open_id),
|
| 902 |
+
|
| 903 |
+
alipay_user_id =
|
| 904 |
+
COALESCE(?, alipay_user_id),
|
| 905 |
+
|
| 906 |
+
updated_at =
|
| 907 |
+
CURRENT_TIMESTAMP,
|
| 908 |
+
|
| 909 |
+
last_login_at =
|
| 910 |
+
CURRENT_TIMESTAMP
|
| 911 |
+
|
| 912 |
+
WHERE id = ?`
|
| 913 |
+
)
|
| 914 |
+
.bind(
|
| 915 |
+
openId,
|
| 916 |
+
alipayUserId,
|
| 917 |
+
existingUser.id
|
| 918 |
+
)
|
| 919 |
+
.run();
|
| 920 |
+
|
| 921 |
+
return {
|
| 922 |
+
id: existingUser.id,
|
| 923 |
+
role: existingUser.role,
|
| 924 |
+
status: existingUser.status,
|
| 925 |
+
bonusPapers:
|
| 926 |
+
existingUser.bonus_papers ?? 0,
|
| 927 |
+
isNew: false,
|
| 928 |
+
};
|
| 929 |
+
}
|
| 930 |
+
|
| 931 |
+
try {
|
| 932 |
+
const insertResult =
|
| 933 |
+
await env.DB.prepare(
|
| 934 |
+
`INSERT INTO users
|
| 935 |
+
(
|
| 936 |
+
alipay_open_id,
|
| 937 |
+
alipay_user_id,
|
| 938 |
+
role,
|
| 939 |
+
status,
|
| 940 |
+
bonus_papers,
|
| 941 |
+
created_at,
|
| 942 |
+
updated_at,
|
| 943 |
+
last_login_at
|
| 944 |
+
)
|
| 945 |
+
VALUES
|
| 946 |
+
(
|
| 947 |
+
?,
|
| 948 |
+
?,
|
| 949 |
+
'user',
|
| 950 |
+
'active',
|
| 951 |
+
0,
|
| 952 |
+
CURRENT_TIMESTAMP,
|
| 953 |
+
CURRENT_TIMESTAMP,
|
| 954 |
+
CURRENT_TIMESTAMP
|
| 955 |
+
)`
|
| 956 |
+
)
|
| 957 |
+
.bind(
|
| 958 |
+
openId,
|
| 959 |
+
alipayUserId
|
| 960 |
+
)
|
| 961 |
+
.run();
|
| 962 |
+
|
| 963 |
+
return {
|
| 964 |
+
id:
|
| 965 |
+
insertResult.meta.last_row_id,
|
| 966 |
+
role: "user",
|
| 967 |
+
status: "active",
|
| 968 |
+
bonusPapers: 0,
|
| 969 |
+
isNew: true,
|
| 970 |
+
};
|
| 971 |
+
} catch (error) {
|
| 972 |
+
const userAfterConflict =
|
| 973 |
+
await findExistingUser(
|
| 974 |
+
env,
|
| 975 |
+
openId,
|
| 976 |
+
alipayUserId
|
| 977 |
+
);
|
| 978 |
+
|
| 979 |
+
if (userAfterConflict) {
|
| 980 |
+
return {
|
| 981 |
+
id: userAfterConflict.id,
|
| 982 |
+
role: userAfterConflict.role,
|
| 983 |
+
status:
|
| 984 |
+
userAfterConflict.status,
|
| 985 |
+
bonusPapers:
|
| 986 |
+
userAfterConflict
|
| 987 |
+
.bonus_papers ?? 0,
|
| 988 |
+
isNew: false,
|
| 989 |
+
};
|
| 990 |
+
}
|
| 991 |
+
|
| 992 |
+
throw error;
|
| 993 |
+
}
|
| 994 |
+
}
|
| 995 |
+
|
| 996 |
+
|
| 997 |
+
function formatUser(user) {
|
| 998 |
+
return {
|
| 999 |
+
id:
|
| 1000 |
+
`U${String(user.id)
|
| 1001 |
+
.padStart(6, "0")}`,
|
| 1002 |
+
numeric_id: user.id,
|
| 1003 |
+
role: user.role,
|
| 1004 |
+
status: user.status,
|
| 1005 |
+
bonus_papers:
|
| 1006 |
+
user.bonus_papers ??
|
| 1007 |
+
user.bonusPapers ??
|
| 1008 |
+
0,
|
| 1009 |
+
daily_free_limit:
|
| 1010 |
+
DAILY_FREE_PAPERS,
|
| 1011 |
+
};
|
| 1012 |
+
}
|
| 1013 |
+
|
| 1014 |
+
|
| 1015 |
+
/* ============================================================
|
| 1016 |
+
* 支付宝回调
|
| 1017 |
+
* ============================================================ */
|
| 1018 |
+
|
| 1019 |
+
async function handleAlipayCallback(
|
| 1020 |
+
request,
|
| 1021 |
+
env
|
| 1022 |
+
) {
|
| 1023 |
+
const url = new URL(request.url);
|
| 1024 |
+
|
| 1025 |
+
const authCode =
|
| 1026 |
+
url.searchParams.get("auth_code");
|
| 1027 |
+
|
| 1028 |
+
const state =
|
| 1029 |
+
url.searchParams.get("state");
|
| 1030 |
+
|
| 1031 |
+
const returnedAppId =
|
| 1032 |
+
url.searchParams.get("app_id");
|
| 1033 |
+
|
| 1034 |
+
if (!authCode) {
|
| 1035 |
+
return htmlResponse(
|
| 1036 |
+
"支付宝授权失败",
|
| 1037 |
+
`
|
| 1038 |
+
<div class="card">
|
| 1039 |
+
<h1 class="error">
|
| 1040 |
+
未收到授权码
|
| 1041 |
+
</h1>
|
| 1042 |
+
|
| 1043 |
+
<p>
|
| 1044 |
+
用户可能取消了授权,
|
| 1045 |
+
或支付宝应用权限尚未生效。
|
| 1046 |
+
</p>
|
| 1047 |
+
|
| 1048 |
+
<a
|
| 1049 |
+
class="button"
|
| 1050 |
+
href="/alipay/login"
|
| 1051 |
+
>
|
| 1052 |
+
重新登录
|
| 1053 |
+
</a>
|
| 1054 |
+
</div>
|
| 1055 |
+
`,
|
| 1056 |
+
400
|
| 1057 |
+
);
|
| 1058 |
+
}
|
| 1059 |
+
|
| 1060 |
+
if (!state) {
|
| 1061 |
+
return htmlResponse(
|
| 1062 |
+
"支付宝授权失败",
|
| 1063 |
+
`
|
| 1064 |
+
<div class="card">
|
| 1065 |
+
<h1 class="error">
|
| 1066 |
+
缺少state参数
|
| 1067 |
+
</h1>
|
| 1068 |
+
|
| 1069 |
+
<p>
|
| 1070 |
+
为保护登录安全,
|
| 1071 |
+
本次请求已被拒绝。
|
| 1072 |
+
</p>
|
| 1073 |
+
</div>
|
| 1074 |
+
`,
|
| 1075 |
+
400
|
| 1076 |
+
);
|
| 1077 |
+
}
|
| 1078 |
+
|
| 1079 |
+
const stateHash =
|
| 1080 |
+
await sha256Hex(state);
|
| 1081 |
+
|
| 1082 |
+
const stateRecord =
|
| 1083 |
+
await env.DB.prepare(
|
| 1084 |
+
`SELECT
|
| 1085 |
+
state_hash,
|
| 1086 |
+
login_request_id,
|
| 1087 |
+
status,
|
| 1088 |
+
expires_at
|
| 1089 |
+
FROM oauth_states
|
| 1090 |
+
WHERE state_hash = ?
|
| 1091 |
+
LIMIT 1`
|
| 1092 |
+
)
|
| 1093 |
+
.bind(stateHash)
|
| 1094 |
+
.first();
|
| 1095 |
+
|
| 1096 |
+
if (!stateRecord) {
|
| 1097 |
+
return htmlResponse(
|
| 1098 |
+
"支付宝授权失败",
|
| 1099 |
+
`
|
| 1100 |
+
<div class="card">
|
| 1101 |
+
<h1 class="error">
|
| 1102 |
+
登录请求不存在
|
| 1103 |
+
</h1>
|
| 1104 |
+
</div>
|
| 1105 |
+
`,
|
| 1106 |
+
400
|
| 1107 |
+
);
|
| 1108 |
+
}
|
| 1109 |
+
|
| 1110 |
+
if (
|
| 1111 |
+
stateRecord.status !== "waiting"
|
| 1112 |
+
) {
|
| 1113 |
+
return htmlResponse(
|
| 1114 |
+
"支付宝授权失败",
|
| 1115 |
+
`
|
| 1116 |
+
<div class="card">
|
| 1117 |
+
<h1 class="error">
|
| 1118 |
+
登录请求已经使用
|
| 1119 |
+
</h1>
|
| 1120 |
+
|
| 1121 |
+
<a
|
| 1122 |
+
class="button"
|
| 1123 |
+
href="/alipay/login"
|
| 1124 |
+
>
|
| 1125 |
+
创建新请求
|
| 1126 |
+
</a>
|
| 1127 |
+
</div>
|
| 1128 |
+
`,
|
| 1129 |
+
409
|
| 1130 |
+
);
|
| 1131 |
+
}
|
| 1132 |
+
|
| 1133 |
+
if (isExpired(stateRecord.expires_at)) {
|
| 1134 |
+
await env.DB.prepare(
|
| 1135 |
+
`UPDATE oauth_states
|
| 1136 |
+
SET status = 'expired'
|
| 1137 |
+
WHERE state_hash = ?`
|
| 1138 |
+
)
|
| 1139 |
+
.bind(stateHash)
|
| 1140 |
+
.run();
|
| 1141 |
+
|
| 1142 |
+
return htmlResponse(
|
| 1143 |
+
"支付宝授权失败",
|
| 1144 |
+
`
|
| 1145 |
+
<div class="card">
|
| 1146 |
+
<h1 class="error">
|
| 1147 |
+
登录请求已经过期
|
| 1148 |
+
</h1>
|
| 1149 |
+
|
| 1150 |
+
<a
|
| 1151 |
+
class="button"
|
| 1152 |
+
href="/alipay/login"
|
| 1153 |
+
>
|
| 1154 |
+
重新登录
|
| 1155 |
+
</a>
|
| 1156 |
+
</div>
|
| 1157 |
+
`,
|
| 1158 |
+
410
|
| 1159 |
+
);
|
| 1160 |
+
}
|
| 1161 |
+
|
| 1162 |
+
if (
|
| 1163 |
+
returnedAppId &&
|
| 1164 |
+
returnedAppId !==
|
| 1165 |
+
env.ALIPAY_APP_ID
|
| 1166 |
+
) {
|
| 1167 |
+
return htmlResponse(
|
| 1168 |
+
"支付宝授权失败",
|
| 1169 |
+
`
|
| 1170 |
+
<div class="card">
|
| 1171 |
+
<h1 class="error">
|
| 1172 |
+
APPID不匹配
|
| 1173 |
+
</h1>
|
| 1174 |
+
</div>
|
| 1175 |
+
`,
|
| 1176 |
+
400
|
| 1177 |
+
);
|
| 1178 |
+
}
|
| 1179 |
+
|
| 1180 |
+
const claimResult =
|
| 1181 |
+
await env.DB.prepare(
|
| 1182 |
+
`UPDATE oauth_states
|
| 1183 |
+
SET
|
| 1184 |
+
status = 'exchanging',
|
| 1185 |
+
used_at = CURRENT_TIMESTAMP
|
| 1186 |
+
WHERE state_hash = ?
|
| 1187 |
+
AND status = 'waiting'`
|
| 1188 |
+
)
|
| 1189 |
+
.bind(stateHash)
|
| 1190 |
+
.run();
|
| 1191 |
+
|
| 1192 |
+
if (
|
| 1193 |
+
!claimResult.success ||
|
| 1194 |
+
claimResult.meta.changes !== 1
|
| 1195 |
+
) {
|
| 1196 |
+
return htmlResponse(
|
| 1197 |
+
"支付宝授权失败",
|
| 1198 |
+
`
|
| 1199 |
+
<div class="card">
|
| 1200 |
+
<h1 class="error">
|
| 1201 |
+
登录请求已经被处理
|
| 1202 |
+
</h1>
|
| 1203 |
+
</div>
|
| 1204 |
+
`,
|
| 1205 |
+
409
|
| 1206 |
+
);
|
| 1207 |
+
}
|
| 1208 |
+
|
| 1209 |
+
let alipayIdentity;
|
| 1210 |
+
|
| 1211 |
+
try {
|
| 1212 |
+
alipayIdentity =
|
| 1213 |
+
await exchangeAlipayAuthCode(
|
| 1214 |
+
authCode,
|
| 1215 |
+
env
|
| 1216 |
+
);
|
| 1217 |
+
} catch (error) {
|
| 1218 |
+
await env.DB.prepare(
|
| 1219 |
+
`UPDATE oauth_states
|
| 1220 |
+
SET status =
|
| 1221 |
+
'token_exchange_failed'
|
| 1222 |
+
WHERE state_hash = ?`
|
| 1223 |
+
)
|
| 1224 |
+
.bind(stateHash)
|
| 1225 |
+
.run();
|
| 1226 |
+
|
| 1227 |
+
return htmlResponse(
|
| 1228 |
+
"支付宝身份交换失败",
|
| 1229 |
+
`
|
| 1230 |
+
<div class="card">
|
| 1231 |
+
<h1 class="error">
|
| 1232 |
+
未能取得支付宝用户标识
|
| 1233 |
+
</h1>
|
| 1234 |
+
|
| 1235 |
+
<p>
|
| 1236 |
+
<code>
|
| 1237 |
+
${escapeHtml(String(error))}
|
| 1238 |
+
</code>
|
| 1239 |
+
</p>
|
| 1240 |
+
|
| 1241 |
+
<a
|
| 1242 |
+
class="button"
|
| 1243 |
+
href="/alipay/login"
|
| 1244 |
+
>
|
| 1245 |
+
创建新请求
|
| 1246 |
+
</a>
|
| 1247 |
+
</div>
|
| 1248 |
+
`,
|
| 1249 |
+
500
|
| 1250 |
+
);
|
| 1251 |
+
}
|
| 1252 |
+
|
| 1253 |
+
let smartPaperUser;
|
| 1254 |
+
|
| 1255 |
+
try {
|
| 1256 |
+
smartPaperUser =
|
| 1257 |
+
await findOrCreateSmartPaperUser(
|
| 1258 |
+
env,
|
| 1259 |
+
alipayIdentity.openId,
|
| 1260 |
+
alipayIdentity.userId
|
| 1261 |
+
);
|
| 1262 |
+
} catch (error) {
|
| 1263 |
+
await env.DB.prepare(
|
| 1264 |
+
`UPDATE oauth_states
|
| 1265 |
+
SET status =
|
| 1266 |
+
'user_creation_failed'
|
| 1267 |
+
WHERE state_hash = ?`
|
| 1268 |
+
)
|
| 1269 |
+
.bind(stateHash)
|
| 1270 |
+
.run();
|
| 1271 |
+
|
| 1272 |
+
return htmlResponse(
|
| 1273 |
+
"SmartPaper账户创建失败",
|
| 1274 |
+
`
|
| 1275 |
+
<div class="card">
|
| 1276 |
+
<h1 class="error">
|
| 1277 |
+
无法建立用户账户
|
| 1278 |
+
</h1>
|
| 1279 |
+
|
| 1280 |
+
<p>
|
| 1281 |
+
<code>
|
| 1282 |
+
${escapeHtml(String(error))}
|
| 1283 |
+
</code>
|
| 1284 |
+
</p>
|
| 1285 |
+
</div>
|
| 1286 |
+
`,
|
| 1287 |
+
500
|
| 1288 |
+
);
|
| 1289 |
+
}
|
| 1290 |
+
|
| 1291 |
+
if (
|
| 1292 |
+
smartPaperUser.status !== "active"
|
| 1293 |
+
) {
|
| 1294 |
+
await env.DB.prepare(
|
| 1295 |
+
`UPDATE oauth_states
|
| 1296 |
+
SET
|
| 1297 |
+
status = 'user_blocked',
|
| 1298 |
+
user_id = ?
|
| 1299 |
+
WHERE state_hash = ?`
|
| 1300 |
+
)
|
| 1301 |
+
.bind(
|
| 1302 |
+
smartPaperUser.id,
|
| 1303 |
+
stateHash
|
| 1304 |
+
)
|
| 1305 |
+
.run();
|
| 1306 |
+
|
| 1307 |
+
return htmlResponse(
|
| 1308 |
+
"SmartPaper账户不可用",
|
| 1309 |
+
`
|
| 1310 |
+
<div class="card">
|
| 1311 |
+
<h1 class="error">
|
| 1312 |
+
账户已被停用
|
| 1313 |
+
</h1>
|
| 1314 |
+
</div>
|
| 1315 |
+
`,
|
| 1316 |
+
403
|
| 1317 |
+
);
|
| 1318 |
+
}
|
| 1319 |
+
|
| 1320 |
+
await env.DB.prepare(
|
| 1321 |
+
`UPDATE oauth_states
|
| 1322 |
+
SET
|
| 1323 |
+
status = 'authenticated',
|
| 1324 |
+
user_id = ?
|
| 1325 |
+
WHERE state_hash = ?
|
| 1326 |
+
AND status = 'exchanging'`
|
| 1327 |
+
)
|
| 1328 |
+
.bind(
|
| 1329 |
+
smartPaperUser.id,
|
| 1330 |
+
stateHash
|
| 1331 |
+
)
|
| 1332 |
+
.run();
|
| 1333 |
+
|
| 1334 |
+
return htmlResponse(
|
| 1335 |
+
"支付宝身份识别成功",
|
| 1336 |
+
`
|
| 1337 |
+
<div class="card">
|
| 1338 |
+
<h1 class="success">
|
| 1339 |
+
支付宝身份识别成功
|
| 1340 |
+
</h1>
|
| 1341 |
+
|
| 1342 |
+
<p>
|
| 1343 |
+
SmartPaper用户:
|
| 1344 |
+
<strong>
|
| 1345 |
+
${escapeHtml(
|
| 1346 |
+
formatUser(
|
| 1347 |
+
smartPaperUser
|
| 1348 |
+
).id
|
| 1349 |
+
)}
|
| 1350 |
+
</strong>
|
| 1351 |
+
</p>
|
| 1352 |
+
|
| 1353 |
+
<p>
|
| 1354 |
+
请返回电脑上的SmartPaper页面。
|
| 1355 |
+
系统将自动完成登录。
|
| 1356 |
+
</p>
|
| 1357 |
+
|
| 1358 |
+
<p class="muted">
|
| 1359 |
+
本页面不会显示支付宝用户标识、
|
| 1360 |
+
访问令牌或SmartPaper会话令牌。
|
| 1361 |
+
</p>
|
| 1362 |
+
</div>
|
| 1363 |
+
`
|
| 1364 |
+
);
|
| 1365 |
+
}
|
| 1366 |
+
|
| 1367 |
+
|
| 1368 |
+
/* ============================================================
|
| 1369 |
+
* Hugging Face创建登录请求
|
| 1370 |
+
* POST /login/create
|
| 1371 |
+
* ============================================================ */
|
| 1372 |
+
|
| 1373 |
+
async function handleLoginCreate(
|
| 1374 |
+
request,
|
| 1375 |
+
env
|
| 1376 |
+
) {
|
| 1377 |
+
const unauthorized =
|
| 1378 |
+
await requireServiceRequest(
|
| 1379 |
+
request,
|
| 1380 |
+
env
|
| 1381 |
+
);
|
| 1382 |
+
|
| 1383 |
+
if (unauthorized) {
|
| 1384 |
+
return unauthorized;
|
| 1385 |
+
}
|
| 1386 |
+
|
| 1387 |
+
const loginRequest =
|
| 1388 |
+
await createLoginRequestRecord(env);
|
| 1389 |
+
|
| 1390 |
+
return jsonResponse({
|
| 1391 |
+
ok: true,
|
| 1392 |
+
status: "waiting",
|
| 1393 |
+
login_request_id:
|
| 1394 |
+
loginRequest.loginRequestId,
|
| 1395 |
+
poll_token:
|
| 1396 |
+
loginRequest.pollToken,
|
| 1397 |
+
authorization_url:
|
| 1398 |
+
loginRequest.authorizationUrl,
|
| 1399 |
+
qr_content:
|
| 1400 |
+
loginRequest.authorizationUrl,
|
| 1401 |
+
expires_at:
|
| 1402 |
+
loginRequest.expiresAt,
|
| 1403 |
+
poll_interval_seconds: 2,
|
| 1404 |
+
});
|
| 1405 |
+
}
|
| 1406 |
+
|
| 1407 |
+
|
| 1408 |
+
/* ============================================================
|
| 1409 |
+
* Hugging Face查询登录状态
|
| 1410 |
+
* POST /login/status
|
| 1411 |
+
* ============================================================ */
|
| 1412 |
+
|
| 1413 |
+
async function handleLoginStatus(
|
| 1414 |
+
request,
|
| 1415 |
+
env
|
| 1416 |
+
) {
|
| 1417 |
+
const unauthorized =
|
| 1418 |
+
await requireServiceRequest(
|
| 1419 |
+
request,
|
| 1420 |
+
env
|
| 1421 |
+
);
|
| 1422 |
+
|
| 1423 |
+
if (unauthorized) {
|
| 1424 |
+
return unauthorized;
|
| 1425 |
+
}
|
| 1426 |
+
|
| 1427 |
+
const body =
|
| 1428 |
+
await readJsonBody(request);
|
| 1429 |
+
|
| 1430 |
+
const loginRequestId =
|
| 1431 |
+
String(
|
| 1432 |
+
body?.login_request_id || ""
|
| 1433 |
+
).trim();
|
| 1434 |
+
|
| 1435 |
+
const pollToken =
|
| 1436 |
+
String(
|
| 1437 |
+
body?.poll_token || ""
|
| 1438 |
+
).trim();
|
| 1439 |
+
|
| 1440 |
+
if (
|
| 1441 |
+
!loginRequestId ||
|
| 1442 |
+
!pollToken
|
| 1443 |
+
) {
|
| 1444 |
+
return jsonResponse(
|
| 1445 |
+
{
|
| 1446 |
+
ok: false,
|
| 1447 |
+
error: "invalid_request",
|
| 1448 |
+
required: [
|
| 1449 |
+
"login_request_id",
|
| 1450 |
+
"poll_token",
|
| 1451 |
+
],
|
| 1452 |
+
},
|
| 1453 |
+
400
|
| 1454 |
+
);
|
| 1455 |
+
}
|
| 1456 |
+
|
| 1457 |
+
const pollTokenHash =
|
| 1458 |
+
await hmacHex(
|
| 1459 |
+
pollToken,
|
| 1460 |
+
env.SESSION_SIGNING_SECRET
|
| 1461 |
+
);
|
| 1462 |
+
|
| 1463 |
+
let record =
|
| 1464 |
+
await env.DB.prepare(
|
| 1465 |
+
`SELECT
|
| 1466 |
+
login_request_id,
|
| 1467 |
+
status,
|
| 1468 |
+
user_id,
|
| 1469 |
+
expires_at,
|
| 1470 |
+
session_issued_at
|
| 1471 |
+
FROM oauth_states
|
| 1472 |
+
WHERE login_request_id = ?
|
| 1473 |
+
AND poll_token_hash = ?
|
| 1474 |
+
LIMIT 1`
|
| 1475 |
+
)
|
| 1476 |
+
.bind(
|
| 1477 |
+
loginRequestId,
|
| 1478 |
+
pollTokenHash
|
| 1479 |
+
)
|
| 1480 |
+
.first();
|
| 1481 |
+
|
| 1482 |
+
if (!record) {
|
| 1483 |
+
return jsonResponse(
|
| 1484 |
+
{
|
| 1485 |
+
ok: false,
|
| 1486 |
+
error:
|
| 1487 |
+
"login_request_not_found",
|
| 1488 |
+
},
|
| 1489 |
+
404
|
| 1490 |
+
);
|
| 1491 |
+
}
|
| 1492 |
+
|
| 1493 |
+
if (
|
| 1494 |
+
isExpired(record.expires_at) &&
|
| 1495 |
+
![
|
| 1496 |
+
"completed",
|
| 1497 |
+
"authenticated",
|
| 1498 |
+
"issuing",
|
| 1499 |
+
].includes(record.status)
|
| 1500 |
+
) {
|
| 1501 |
+
await env.DB.prepare(
|
| 1502 |
+
`UPDATE oauth_states
|
| 1503 |
+
SET status = 'expired'
|
| 1504 |
+
WHERE login_request_id = ?
|
| 1505 |
+
AND poll_token_hash = ?`
|
| 1506 |
+
)
|
| 1507 |
+
.bind(
|
| 1508 |
+
loginRequestId,
|
| 1509 |
+
pollTokenHash
|
| 1510 |
+
)
|
| 1511 |
+
.run();
|
| 1512 |
+
|
| 1513 |
+
return jsonResponse(
|
| 1514 |
+
{
|
| 1515 |
+
ok: false,
|
| 1516 |
+
status: "expired",
|
| 1517 |
+
},
|
| 1518 |
+
410
|
| 1519 |
+
);
|
| 1520 |
+
}
|
| 1521 |
+
|
| 1522 |
+
if (
|
| 1523 |
+
record.status === "waiting" ||
|
| 1524 |
+
record.status === "exchanging" ||
|
| 1525 |
+
record.status === "issuing"
|
| 1526 |
+
) {
|
| 1527 |
+
return jsonResponse({
|
| 1528 |
+
ok: true,
|
| 1529 |
+
status: "pending",
|
| 1530 |
+
});
|
| 1531 |
+
}
|
| 1532 |
+
|
| 1533 |
+
if (
|
| 1534 |
+
[
|
| 1535 |
+
"expired",
|
| 1536 |
+
"token_exchange_failed",
|
| 1537 |
+
"user_creation_failed",
|
| 1538 |
+
"user_blocked",
|
| 1539 |
+
].includes(record.status)
|
| 1540 |
+
) {
|
| 1541 |
+
return jsonResponse(
|
| 1542 |
+
{
|
| 1543 |
+
ok: false,
|
| 1544 |
+
status: "failed",
|
| 1545 |
+
reason: record.status,
|
| 1546 |
+
},
|
| 1547 |
+
400
|
| 1548 |
+
);
|
| 1549 |
+
}
|
| 1550 |
+
|
| 1551 |
+
if (record.status === "completed") {
|
| 1552 |
+
return jsonResponse(
|
| 1553 |
+
{
|
| 1554 |
+
ok: false,
|
| 1555 |
+
status: "already_delivered",
|
| 1556 |
+
message:
|
| 1557 |
+
"该登录结果已经领取,请重新扫码登录。",
|
| 1558 |
+
},
|
| 1559 |
+
409
|
| 1560 |
+
);
|
| 1561 |
+
}
|
| 1562 |
+
|
| 1563 |
+
if (
|
| 1564 |
+
record.status !== "authenticated" ||
|
| 1565 |
+
!record.user_id
|
| 1566 |
+
) {
|
| 1567 |
+
return jsonResponse(
|
| 1568 |
+
{
|
| 1569 |
+
ok: false,
|
| 1570 |
+
status: "invalid_state",
|
| 1571 |
+
current_status:
|
| 1572 |
+
record.status,
|
| 1573 |
+
},
|
| 1574 |
+
409
|
| 1575 |
+
);
|
| 1576 |
+
}
|
| 1577 |
+
|
| 1578 |
+
const user =
|
| 1579 |
+
await env.DB.prepare(
|
| 1580 |
+
`SELECT
|
| 1581 |
+
id,
|
| 1582 |
+
role,
|
| 1583 |
+
status,
|
| 1584 |
+
bonus_papers
|
| 1585 |
+
FROM users
|
| 1586 |
+
WHERE id = ?
|
| 1587 |
+
LIMIT 1`
|
| 1588 |
+
)
|
| 1589 |
+
.bind(record.user_id)
|
| 1590 |
+
.first();
|
| 1591 |
+
|
| 1592 |
+
if (!user) {
|
| 1593 |
+
return jsonResponse(
|
| 1594 |
+
{
|
| 1595 |
+
ok: false,
|
| 1596 |
+
error: "user_not_found",
|
| 1597 |
+
},
|
| 1598 |
+
404
|
| 1599 |
+
);
|
| 1600 |
+
}
|
| 1601 |
+
|
| 1602 |
+
if (user.status !== "active") {
|
| 1603 |
+
await env.DB.prepare(
|
| 1604 |
+
`UPDATE oauth_states
|
| 1605 |
+
SET status = 'user_blocked'
|
| 1606 |
+
WHERE login_request_id = ?`
|
| 1607 |
+
)
|
| 1608 |
+
.bind(loginRequestId)
|
| 1609 |
+
.run();
|
| 1610 |
+
|
| 1611 |
+
return jsonResponse(
|
| 1612 |
+
{
|
| 1613 |
+
ok: false,
|
| 1614 |
+
error: "user_blocked",
|
| 1615 |
+
},
|
| 1616 |
+
403
|
| 1617 |
+
);
|
| 1618 |
+
}
|
| 1619 |
+
|
| 1620 |
+
const claimResult =
|
| 1621 |
+
await env.DB.prepare(
|
| 1622 |
+
`UPDATE oauth_states
|
| 1623 |
+
SET status = 'issuing'
|
| 1624 |
+
WHERE login_request_id = ?
|
| 1625 |
+
AND poll_token_hash = ?
|
| 1626 |
+
AND status = 'authenticated'
|
| 1627 |
+
AND session_issued_at IS NULL`
|
| 1628 |
+
)
|
| 1629 |
+
.bind(
|
| 1630 |
+
loginRequestId,
|
| 1631 |
+
pollTokenHash
|
| 1632 |
+
)
|
| 1633 |
+
.run();
|
| 1634 |
+
|
| 1635 |
+
if (
|
| 1636 |
+
!claimResult.success ||
|
| 1637 |
+
claimResult.meta.changes !== 1
|
| 1638 |
+
) {
|
| 1639 |
+
return jsonResponse({
|
| 1640 |
+
ok: true,
|
| 1641 |
+
status: "pending",
|
| 1642 |
+
});
|
| 1643 |
+
}
|
| 1644 |
+
|
| 1645 |
+
const sessionToken =
|
| 1646 |
+
randomToken(48);
|
| 1647 |
+
|
| 1648 |
+
const sessionHash =
|
| 1649 |
+
await hmacHex(
|
| 1650 |
+
sessionToken,
|
| 1651 |
+
env.SESSION_SIGNING_SECRET
|
| 1652 |
+
);
|
| 1653 |
+
|
| 1654 |
+
const sessionExpiresAt =
|
| 1655 |
+
new Date(
|
| 1656 |
+
Date.now() + SESSION_TTL_MS
|
| 1657 |
+
).toISOString();
|
| 1658 |
+
|
| 1659 |
+
try {
|
| 1660 |
+
await env.DB.batch([
|
| 1661 |
+
env.DB.prepare(
|
| 1662 |
+
`INSERT INTO login_sessions
|
| 1663 |
+
(
|
| 1664 |
+
session_hash,
|
| 1665 |
+
user_id,
|
| 1666 |
+
created_at,
|
| 1667 |
+
expires_at,
|
| 1668 |
+
revoked_at
|
| 1669 |
+
)
|
| 1670 |
+
VALUES
|
| 1671 |
+
(
|
| 1672 |
+
?,
|
| 1673 |
+
?,
|
| 1674 |
+
CURRENT_TIMESTAMP,
|
| 1675 |
+
?,
|
| 1676 |
+
NULL
|
| 1677 |
+
)`
|
| 1678 |
+
).bind(
|
| 1679 |
+
sessionHash,
|
| 1680 |
+
user.id,
|
| 1681 |
+
sessionExpiresAt
|
| 1682 |
+
),
|
| 1683 |
+
|
| 1684 |
+
env.DB.prepare(
|
| 1685 |
+
`UPDATE oauth_states
|
| 1686 |
+
SET
|
| 1687 |
+
status = 'completed',
|
| 1688 |
+
session_issued_at =
|
| 1689 |
+
CURRENT_TIMESTAMP
|
| 1690 |
+
WHERE login_request_id = ?
|
| 1691 |
+
AND status = 'issuing'`
|
| 1692 |
+
).bind(loginRequestId),
|
| 1693 |
+
]);
|
| 1694 |
+
} catch (error) {
|
| 1695 |
+
await env.DB.prepare(
|
| 1696 |
+
`UPDATE oauth_states
|
| 1697 |
+
SET status = 'authenticated'
|
| 1698 |
+
WHERE login_request_id = ?
|
| 1699 |
+
AND status = 'issuing'`
|
| 1700 |
+
)
|
| 1701 |
+
.bind(loginRequestId)
|
| 1702 |
+
.run();
|
| 1703 |
+
|
| 1704 |
+
throw error;
|
| 1705 |
+
}
|
| 1706 |
+
|
| 1707 |
+
return jsonResponse({
|
| 1708 |
+
ok: true,
|
| 1709 |
+
status: "authenticated",
|
| 1710 |
+
session_token: sessionToken,
|
| 1711 |
+
session_expires_at:
|
| 1712 |
+
sessionExpiresAt,
|
| 1713 |
+
user: formatUser(user),
|
| 1714 |
+
});
|
| 1715 |
+
}
|
| 1716 |
+
|
| 1717 |
+
|
| 1718 |
+
/* ============================================================
|
| 1719 |
+
* 验证SmartPaper会话
|
| 1720 |
+
* POST /session/verify
|
| 1721 |
+
* ============================================================ */
|
| 1722 |
+
|
| 1723 |
+
async function handleSessionVerify(
|
| 1724 |
+
request,
|
| 1725 |
+
env
|
| 1726 |
+
) {
|
| 1727 |
+
const unauthorized =
|
| 1728 |
+
await requireServiceRequest(
|
| 1729 |
+
request,
|
| 1730 |
+
env
|
| 1731 |
+
);
|
| 1732 |
+
|
| 1733 |
+
if (unauthorized) {
|
| 1734 |
+
return unauthorized;
|
| 1735 |
+
}
|
| 1736 |
+
|
| 1737 |
+
const body =
|
| 1738 |
+
await readJsonBody(request);
|
| 1739 |
+
|
| 1740 |
+
const sessionToken =
|
| 1741 |
+
String(
|
| 1742 |
+
body?.session_token || ""
|
| 1743 |
+
).trim();
|
| 1744 |
+
|
| 1745 |
+
if (!sessionToken) {
|
| 1746 |
+
return jsonResponse(
|
| 1747 |
+
{
|
| 1748 |
+
ok: false,
|
| 1749 |
+
error:
|
| 1750 |
+
"session_token_required",
|
| 1751 |
+
},
|
| 1752 |
+
400
|
| 1753 |
+
);
|
| 1754 |
+
}
|
| 1755 |
+
|
| 1756 |
+
const sessionHash =
|
| 1757 |
+
await hmacHex(
|
| 1758 |
+
sessionToken,
|
| 1759 |
+
env.SESSION_SIGNING_SECRET
|
| 1760 |
+
);
|
| 1761 |
+
|
| 1762 |
+
const session =
|
| 1763 |
+
await env.DB.prepare(
|
| 1764 |
+
`SELECT
|
| 1765 |
+
s.session_hash,
|
| 1766 |
+
s.expires_at,
|
| 1767 |
+
s.revoked_at,
|
| 1768 |
+
|
| 1769 |
+
u.id,
|
| 1770 |
+
u.role,
|
| 1771 |
+
u.status,
|
| 1772 |
+
u.bonus_papers
|
| 1773 |
+
|
| 1774 |
+
FROM login_sessions AS s
|
| 1775 |
+
|
| 1776 |
+
INNER JOIN users AS u
|
| 1777 |
+
ON u.id = s.user_id
|
| 1778 |
+
|
| 1779 |
+
WHERE s.session_hash = ?
|
| 1780 |
+
LIMIT 1`
|
| 1781 |
+
)
|
| 1782 |
+
.bind(sessionHash)
|
| 1783 |
+
.first();
|
| 1784 |
+
|
| 1785 |
+
if (!session) {
|
| 1786 |
+
return jsonResponse(
|
| 1787 |
+
{
|
| 1788 |
+
ok: false,
|
| 1789 |
+
valid: false,
|
| 1790 |
+
error: "invalid_session",
|
| 1791 |
+
},
|
| 1792 |
+
401
|
| 1793 |
+
);
|
| 1794 |
+
}
|
| 1795 |
+
|
| 1796 |
+
if (session.revoked_at) {
|
| 1797 |
+
return jsonResponse(
|
| 1798 |
+
{
|
| 1799 |
+
ok: false,
|
| 1800 |
+
valid: false,
|
| 1801 |
+
error: "session_revoked",
|
| 1802 |
+
},
|
| 1803 |
+
401
|
| 1804 |
+
);
|
| 1805 |
+
}
|
| 1806 |
+
|
| 1807 |
+
if (isExpired(session.expires_at)) {
|
| 1808 |
+
return jsonResponse(
|
| 1809 |
+
{
|
| 1810 |
+
ok: false,
|
| 1811 |
+
valid: false,
|
| 1812 |
+
error: "session_expired",
|
| 1813 |
+
},
|
| 1814 |
+
401
|
| 1815 |
+
);
|
| 1816 |
+
}
|
| 1817 |
+
|
| 1818 |
+
if (session.status !== "active") {
|
| 1819 |
+
return jsonResponse(
|
| 1820 |
+
{
|
| 1821 |
+
ok: false,
|
| 1822 |
+
valid: false,
|
| 1823 |
+
error: "user_blocked",
|
| 1824 |
+
},
|
| 1825 |
+
403
|
| 1826 |
+
);
|
| 1827 |
+
}
|
| 1828 |
+
|
| 1829 |
+
return jsonResponse({
|
| 1830 |
+
ok: true,
|
| 1831 |
+
valid: true,
|
| 1832 |
+
session_expires_at:
|
| 1833 |
+
session.expires_at,
|
| 1834 |
+
user: formatUser(session),
|
| 1835 |
+
});
|
| 1836 |
+
}
|
| 1837 |
+
|
| 1838 |
+
|
| 1839 |
+
/* ============================================================
|
| 1840 |
+
* 注销SmartPaper会话
|
| 1841 |
+
* POST /logout
|
| 1842 |
+
* ============================================================ */
|
| 1843 |
+
|
| 1844 |
+
async function handleLogout(
|
| 1845 |
+
request,
|
| 1846 |
+
env
|
| 1847 |
+
) {
|
| 1848 |
+
const unauthorized =
|
| 1849 |
+
await requireServiceRequest(
|
| 1850 |
+
request,
|
| 1851 |
+
env
|
| 1852 |
+
);
|
| 1853 |
+
|
| 1854 |
+
if (unauthorized) {
|
| 1855 |
+
return unauthorized;
|
| 1856 |
+
}
|
| 1857 |
+
|
| 1858 |
+
const body =
|
| 1859 |
+
await readJsonBody(request);
|
| 1860 |
+
|
| 1861 |
+
const sessionToken =
|
| 1862 |
+
String(
|
| 1863 |
+
body?.session_token || ""
|
| 1864 |
+
).trim();
|
| 1865 |
+
|
| 1866 |
+
if (!sessionToken) {
|
| 1867 |
+
return jsonResponse(
|
| 1868 |
+
{
|
| 1869 |
+
ok: false,
|
| 1870 |
+
error:
|
| 1871 |
+
"session_token_required",
|
| 1872 |
+
},
|
| 1873 |
+
400
|
| 1874 |
+
);
|
| 1875 |
+
}
|
| 1876 |
+
|
| 1877 |
+
const sessionHash =
|
| 1878 |
+
await hmacHex(
|
| 1879 |
+
sessionToken,
|
| 1880 |
+
env.SESSION_SIGNING_SECRET
|
| 1881 |
+
);
|
| 1882 |
+
|
| 1883 |
+
await env.DB.prepare(
|
| 1884 |
+
`UPDATE login_sessions
|
| 1885 |
+
SET revoked_at =
|
| 1886 |
+
COALESCE(
|
| 1887 |
+
revoked_at,
|
| 1888 |
+
CURRENT_TIMESTAMP
|
| 1889 |
+
)
|
| 1890 |
+
WHERE session_hash = ?`
|
| 1891 |
+
)
|
| 1892 |
+
.bind(sessionHash)
|
| 1893 |
+
.run();
|
| 1894 |
+
|
| 1895 |
+
return jsonResponse({
|
| 1896 |
+
ok: true,
|
| 1897 |
+
logged_out: true,
|
| 1898 |
+
});
|
| 1899 |
+
}
|
| 1900 |
+
|
| 1901 |
+
|
| 1902 |
+
/* ============================================================
|
| 1903 |
+
* Worker主入口
|
| 1904 |
+
* ============================================================ */
|
| 1905 |
+
|
| 1906 |
+
export default {
|
| 1907 |
+
async fetch(request, env) {
|
| 1908 |
+
const url = new URL(request.url);
|
| 1909 |
+
|
| 1910 |
+
try {
|
| 1911 |
+
if (request.method === "OPTIONS") {
|
| 1912 |
+
return new Response(null, {
|
| 1913 |
+
status: 204,
|
| 1914 |
+
});
|
| 1915 |
+
}
|
| 1916 |
+
|
| 1917 |
+
if (url.pathname === "/health") {
|
| 1918 |
+
const missing =
|
| 1919 |
+
getMissingConfiguration(env);
|
| 1920 |
+
|
| 1921 |
+
return jsonResponse({
|
| 1922 |
+
ok: missing.length === 0,
|
| 1923 |
+
service:
|
| 1924 |
+
"smartpaper-auth",
|
| 1925 |
+
stage:
|
| 1926 |
+
"smartpaper-session-backend",
|
| 1927 |
+
configuration_ready:
|
| 1928 |
+
missing.length === 0,
|
| 1929 |
+
missing,
|
| 1930 |
+
rules: {
|
| 1931 |
+
daily_free_papers:
|
| 1932 |
+
DAILY_FREE_PAPERS,
|
| 1933 |
+
payment_amount_fen:
|
| 1934 |
+
PAYMENT_AMOUNT_FEN,
|
| 1935 |
+
payment_bonus_papers:
|
| 1936 |
+
PAYMENT_BONUS_PAPERS,
|
| 1937 |
+
session_hours: 8,
|
| 1938 |
+
},
|
| 1939 |
+
time:
|
| 1940 |
+
new Date().toISOString(),
|
| 1941 |
+
});
|
| 1942 |
+
}
|
| 1943 |
+
|
| 1944 |
+
if (url.pathname === "/db-test") {
|
| 1945 |
+
if (!env.DB) {
|
| 1946 |
+
return jsonResponse(
|
| 1947 |
+
{
|
| 1948 |
+
ok: false,
|
| 1949 |
+
error:
|
| 1950 |
+
"DB binding is missing",
|
| 1951 |
+
},
|
| 1952 |
+
500
|
| 1953 |
+
);
|
| 1954 |
+
}
|
| 1955 |
+
|
| 1956 |
+
const result =
|
| 1957 |
+
await env.DB.prepare(
|
| 1958 |
+
"SELECT 1 AS connected"
|
| 1959 |
+
).first();
|
| 1960 |
+
|
| 1961 |
+
return jsonResponse({
|
| 1962 |
+
ok: true,
|
| 1963 |
+
database:
|
| 1964 |
+
"smartpaper-db",
|
| 1965 |
+
connected:
|
| 1966 |
+
result?.connected === 1,
|
| 1967 |
+
});
|
| 1968 |
+
}
|
| 1969 |
+
|
| 1970 |
+
if (url.pathname === "/privacy") {
|
| 1971 |
+
return htmlResponse(
|
| 1972 |
+
"SmartPaper隐私说明",
|
| 1973 |
+
`
|
| 1974 |
+
<div class="card">
|
| 1975 |
+
<h1>
|
| 1976 |
+
SmartPaper隐私说明
|
| 1977 |
+
</h1>
|
| 1978 |
+
|
| 1979 |
+
<p>
|
| 1980 |
+
SmartPaper使用支付宝授权返回的
|
| 1981 |
+
用户唯一标识建立用户账户。
|
| 1982 |
+
</p>
|
| 1983 |
+
|
| 1984 |
+
<p>
|
| 1985 |
+
该标识用于用户登录、
|
| 1986 |
+
每日免费3篇论文的额度管理、
|
| 1987 |
+
长期有效论文管理、任务排队、
|
| 1988 |
+
失败退还和异常滥用防范。
|
| 1989 |
+
</p>
|
| 1990 |
+
|
| 1991 |
+
<p>
|
| 1992 |
+
SmartPaper原则上不收集身份证号码、
|
| 1993 |
+
真实姓名、手机号、通讯录或其他
|
| 1994 |
+
与上述目的无关的信息。
|
| 1995 |
+
</p>
|
| 1996 |
+
|
| 1997 |
+
<p>
|
| 1998 |
+
当前页面为开发测试阶段隐私说明。
|
| 1999 |
+
</p>
|
| 2000 |
+
</div>
|
| 2001 |
+
`
|
| 2002 |
+
);
|
| 2003 |
+
}
|
| 2004 |
+
|
| 2005 |
+
if (
|
| 2006 |
+
url.pathname === "/alipay/login" &&
|
| 2007 |
+
request.method === "GET"
|
| 2008 |
+
) {
|
| 2009 |
+
return await handleManualAlipayLogin(
|
| 2010 |
+
env
|
| 2011 |
+
);
|
| 2012 |
+
}
|
| 2013 |
+
|
| 2014 |
+
if (
|
| 2015 |
+
url.pathname ===
|
| 2016 |
+
"/alipay/callback" &&
|
| 2017 |
+
request.method === "GET"
|
| 2018 |
+
) {
|
| 2019 |
+
return await handleAlipayCallback(
|
| 2020 |
+
request,
|
| 2021 |
+
env
|
| 2022 |
+
);
|
| 2023 |
+
}
|
| 2024 |
+
|
| 2025 |
+
if (
|
| 2026 |
+
url.pathname ===
|
| 2027 |
+
"/login/create" &&
|
| 2028 |
+
request.method === "POST"
|
| 2029 |
+
) {
|
| 2030 |
+
return await handleLoginCreate(
|
| 2031 |
+
request,
|
| 2032 |
+
env
|
| 2033 |
+
);
|
| 2034 |
+
}
|
| 2035 |
+
|
| 2036 |
+
if (
|
| 2037 |
+
url.pathname ===
|
| 2038 |
+
"/login/status" &&
|
| 2039 |
+
request.method === "POST"
|
| 2040 |
+
) {
|
| 2041 |
+
return await handleLoginStatus(
|
| 2042 |
+
request,
|
| 2043 |
+
env
|
| 2044 |
+
);
|
| 2045 |
+
}
|
| 2046 |
+
|
| 2047 |
+
if (
|
| 2048 |
+
url.pathname ===
|
| 2049 |
+
"/session/verify" &&
|
| 2050 |
+
request.method === "POST"
|
| 2051 |
+
) {
|
| 2052 |
+
return await handleSessionVerify(
|
| 2053 |
+
request,
|
| 2054 |
+
env
|
| 2055 |
+
);
|
| 2056 |
+
}
|
| 2057 |
+
|
| 2058 |
+
if (
|
| 2059 |
+
url.pathname === "/logout" &&
|
| 2060 |
+
request.method === "POST"
|
| 2061 |
+
) {
|
| 2062 |
+
return await handleLogout(
|
| 2063 |
+
request,
|
| 2064 |
+
env
|
| 2065 |
+
);
|
| 2066 |
+
}
|
| 2067 |
+
|
| 2068 |
+
if (
|
| 2069 |
+
url.pathname === "/" &&
|
| 2070 |
+
request.method === "GET"
|
| 2071 |
+
) {
|
| 2072 |
+
return htmlResponse(
|
| 2073 |
+
"SmartPaper支付宝登录测试",
|
| 2074 |
+
`
|
| 2075 |
+
<div class="card">
|
| 2076 |
+
<h1>
|
| 2077 |
+
SmartPaper支付宝登录测试
|
| 2078 |
+
</h1>
|
| 2079 |
+
|
| 2080 |
+
<p>
|
| 2081 |
+
支付宝用户识别和SmartPaper
|
| 2082 |
+
8小时会话后端已经启用。
|
| 2083 |
+
</p>
|
| 2084 |
+
|
| 2085 |
+
<p>
|
| 2086 |
+
点击按钮后,
|
| 2087 |
+
请使用手机支付宝扫码并确认授权。
|
| 2088 |
+
</p>
|
| 2089 |
+
|
| 2090 |
+
<a
|
| 2091 |
+
class="button"
|
| 2092 |
+
href="/alipay/login"
|
| 2093 |
+
>
|
| 2094 |
+
开始支付宝扫码登录测试
|
| 2095 |
+
</a>
|
| 2096 |
+
|
| 2097 |
+
<p class="links">
|
| 2098 |
+
<a href="/health">
|
| 2099 |
+
检查Worker
|
| 2100 |
+
</a>
|
| 2101 |
+
|
| 2102 |
+
|
|
| 2103 |
+
|
| 2104 |
+
<a href="/db-test">
|
| 2105 |
+
检查数据库
|
| 2106 |
+
</a>
|
| 2107 |
+
|
| 2108 |
+
|
|
| 2109 |
+
|
| 2110 |
+
<a href="/privacy">
|
| 2111 |
+
查看隐私说明
|
| 2112 |
+
</a>
|
| 2113 |
+
</p>
|
| 2114 |
+
</div>
|
| 2115 |
+
`
|
| 2116 |
+
);
|
| 2117 |
+
}
|
| 2118 |
+
|
| 2119 |
+
return jsonResponse(
|
| 2120 |
+
{
|
| 2121 |
+
ok: false,
|
| 2122 |
+
error: "not_found",
|
| 2123 |
+
path: url.pathname,
|
| 2124 |
+
},
|
| 2125 |
+
404
|
| 2126 |
+
);
|
| 2127 |
+
} catch (error) {
|
| 2128 |
+
console.error(error);
|
| 2129 |
+
|
| 2130 |
+
return jsonResponse(
|
| 2131 |
+
{
|
| 2132 |
+
ok: false,
|
| 2133 |
+
error: "internal_error",
|
| 2134 |
+
detail: String(error),
|
| 2135 |
+
},
|
| 2136 |
+
500
|
| 2137 |
+
);
|
| 2138 |
+
}
|
| 2139 |
+
},
|
| 2140 |
+
};
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RB6I2XK.png
ADDED
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/.gitattributes
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/.streamlit/config.toml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[server]
|
| 2 |
+
enableCORS = false
|
| 3 |
+
enableXsrfProtection = false
|
| 4 |
+
enableWebsocketCompression = false
|
| 5 |
+
maxUploadSize = 1024
|
| 6 |
+
|
| 7 |
+
[browser]
|
| 8 |
+
gatherUsageStats = false
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/.streamlit/desktop.ini
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[.ShellClassInfo]
|
| 2 |
+
IconResource=C:\Program Files\Google\Drive File Stream\129.0.1.0\GoogleDriveFS.exe,27
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/HF_COMMIT_SHA.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
af60fe1bdcbab60f47f06586cca03210f7f61026
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/Plaintext
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
project_root/
|
| 2 |
+
├── app.py (主应用入口:UI界面与流程调度)
|
| 3 |
+
├── config.py (静态配置:提示词字典与基础参数)
|
| 4 |
+
├── core/
|
| 5 |
+
│ └── pipeline_agent.py (核心大脑:SSciPipelineAgent 类)
|
| 6 |
+
│ ├── rag_engine.py 🌟 [新建] 专门负责处理 20+ 篇大体量文献的切块与按需检索
|
| 7 |
+
│ ├── meta_extractor.py 🌟 [新建] 专门负责从文献中榨取 JSON 格式的黄金价值数据
|
| 8 |
+
│ └── qualitative_engine.py 🌟 [新建] 专门定性研究
|
| 9 |
+
└── utils/
|
| 10 |
+
├── file_parser.py (工具:PDF/Word/Excel解析)
|
| 11 |
+
└── docx_builder.py (工具:Markdown转Word渲染器)
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/README.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Agent Article
|
| 3 |
+
emoji: 📊
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: streamlit
|
| 7 |
+
app_file: app.py
|
| 8 |
+
pinned: false
|
| 9 |
+
short_description: Agent Article
|
| 10 |
+
sdk_version: 1.38.0
|
| 11 |
+
python_version: 3.11
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/app.py
ADDED
|
@@ -0,0 +1,1378 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""Smart Draft - Dual Track (AMJ Empirical vs ASQ Qualitative)"""
|
| 3 |
+
from core.meta_extractor import extract_golden_metadata
|
| 4 |
+
from core.rag_engine import initialize_rag
|
| 5 |
+
from core.qualitative_engine import get_qualitative_engine
|
| 6 |
+
from core.quantitative_engine import get_quantitative_engine
|
| 7 |
+
from core.production_guard import build_validated_package, replace_abstract
|
| 8 |
+
import streamlit as st
|
| 9 |
+
|
| 10 |
+
st.set_page_config(
|
| 11 |
+
page_title="Article Agent",
|
| 12 |
+
page_icon="📊",
|
| 13 |
+
layout="wide",
|
| 14 |
+
initial_sidebar_state="collapsed",
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
import warnings
|
| 18 |
+
warnings.filterwarnings("ignore", message="coroutine 'expire_cache' was never awaited")
|
| 19 |
+
|
| 20 |
+
import os
|
| 21 |
+
import streamlit.components.v1 as components
|
| 22 |
+
import time
|
| 23 |
+
import re
|
| 24 |
+
import math
|
| 25 |
+
import json
|
| 26 |
+
import secrets
|
| 27 |
+
from datetime import datetime, timezone, timedelta
|
| 28 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 29 |
+
|
| 30 |
+
from config import APP_CONFIG, AMJ_PROMPTS, ASQ_PROMPTS
|
| 31 |
+
from utils.file_parser import extract_text_from_files
|
| 32 |
+
from core.pipeline_agent import SSciPipelineAgent
|
| 33 |
+
from managed_auth import (
|
| 34 |
+
require_alipay_login,
|
| 35 |
+
render_logged_in_account,
|
| 36 |
+
start_generation_task,
|
| 37 |
+
heartbeat_generation_task,
|
| 38 |
+
finish_generation_task,
|
| 39 |
+
render_payment_purchase,
|
| 40 |
+
)
|
| 41 |
+
from i18n import LANGUAGE_OPTIONS, get_amj_csv_template, get_asq_txt_template, get_output_suffix, get_target_flag, get_target_language, tr
|
| 42 |
+
|
| 43 |
+
os.environ["STREAMLIT_SERVER_ENABLE_CORS"] = "false"
|
| 44 |
+
os.environ["STREAMLIT_SERVER_ENABLE_XSRF_PROTECTION"] = "false"
|
| 45 |
+
|
| 46 |
+
WORKSPACE_DIR = APP_CONFIG.get("workspace_dir", ".workspace")
|
| 47 |
+
os.makedirs(WORKSPACE_DIR, exist_ok=True)
|
| 48 |
+
LEGACY_CHECKPOINT_FILE = os.path.join(WORKSPACE_DIR, "snapshot.json")
|
| 49 |
+
CHECKPOINT_FILE = LEGACY_CHECKPOINT_FILE
|
| 50 |
+
|
| 51 |
+
META_SECTIONS_BLACKLIST = set(APP_CONFIG["blacklists"]["meta_sections"])
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def update_heartbeat():
|
| 55 |
+
"""Refresh the authoritative Cloudflare/D1 task lease only."""
|
| 56 |
+
now = time.time()
|
| 57 |
+
task = st.session_state.get("sp_generation_task") or {}
|
| 58 |
+
task_id = str(task.get("task_id") or "").strip()
|
| 59 |
+
interval = max(3, int(APP_CONFIG.get("task_heartbeat_interval_seconds", 5)))
|
| 60 |
+
last_remote = float(st.session_state.get("sp_last_task_heartbeat") or 0.0)
|
| 61 |
+
if not task_id or now - last_remote < interval:
|
| 62 |
+
return
|
| 63 |
+
try:
|
| 64 |
+
heartbeat_generation_task(
|
| 65 |
+
task_id,
|
| 66 |
+
current_node=str(st.session_state.get("sp_current_node") or "")[:240],
|
| 67 |
+
)
|
| 68 |
+
st.session_state["sp_last_task_heartbeat"] = now
|
| 69 |
+
except Exception:
|
| 70 |
+
# The hidden browser bridge continues heartbeats independently. A single
|
| 71 |
+
# transient UI heartbeat must not interrupt model generation.
|
| 72 |
+
pass
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def is_system_busy():
|
| 76 |
+
"""D1 is the sole concurrency authority; no duplicate Hugging Face file lock."""
|
| 77 |
+
return False
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def clear_heartbeat():
|
| 81 |
+
"""Backward-compatible no-op after removal of the duplicate local lock."""
|
| 82 |
+
return None
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def stream_with_heartbeat(generator):
|
| 86 |
+
"""Refresh the D1 lease while streaming model output."""
|
| 87 |
+
last_beat = time.time()
|
| 88 |
+
for chunk in generator:
|
| 89 |
+
now = time.time()
|
| 90 |
+
if now - last_beat > 5:
|
| 91 |
+
update_heartbeat()
|
| 92 |
+
last_beat = now
|
| 93 |
+
yield chunk
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def clean_trailing_references(text, keywords=("reference", "references", "reference list", "参考文献")):
|
| 97 |
+
lines = text.split('\n')
|
| 98 |
+
for i in range(len(lines)-1, max(-1, len(lines)-100), -1):
|
| 99 |
+
if any(re.match(rf'^##?\s*{kw}', lines[i], re.IGNORECASE) for kw in keywords):
|
| 100 |
+
return '\n'.join(lines[:i]).strip()
|
| 101 |
+
return text.strip()
|
| 102 |
+
|
| 103 |
+
def clean_markdown_headings(text):
|
| 104 |
+
return re.sub(r'(?m)^#+\s+(.*)$', r'**\1**', text)
|
| 105 |
+
|
| 106 |
+
def _read_runtime_secret(name):
|
| 107 |
+
# Read a Hugging Face/Streamlit secret and remove copied whitespace.
|
| 108 |
+
value = os.environ.get(name, "")
|
| 109 |
+
if not value:
|
| 110 |
+
try:
|
| 111 |
+
value = st.secrets.get(name, "")
|
| 112 |
+
except Exception:
|
| 113 |
+
value = ""
|
| 114 |
+
return str(value or "").strip()
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
global_ds_key = _read_runtime_secret("DEEPSEEK_API_KEY")
|
| 118 |
+
global_s2_key = _read_runtime_secret("S2_API_KEY")
|
| 119 |
+
|
| 120 |
+
WECHAT_BLOCKER_JS = """<script>try{var ua=navigator.userAgent.toLowerCase();if(ua.indexOf('micromessenger')!==-1){var parentDoc=window.parent.document;if(!parentDoc.getElementById('wx-blocker')){var overlay=parentDoc.createElement('div');overlay.id='wx-blocker';overlay.style.cssText='position:fixed;top:0;left:0;width:100vw;height:100vh;background:#1a365d;z-index:9999999;color:white;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;font-family:sans-serif;padding:30px;box-sizing:border-box;';overlay.innerHTML='<h2 style="font-size:26px;margin-bottom:15px;font-weight:bold;">环境受限 / Restricted</h2><p style="font-size:18px;line-height:1.6;color:#e2e8f0;max-width:100%;">为了保障最终双语 ZIP 压缩包正常下载,<b>本系统禁止在微信内直接运行</b>。<br><br>👉 <b>请点击右上角 [···] 菜单,选择“在浏览器打开” (Open in Browser)</b></p>';parentDoc.body.appendChild(overlay);}}}catch(e){}</script>"""
|
| 121 |
+
components.html(WECHAT_BLOCKER_JS, height=0, width=0)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def inject_file_uploader_i18n(language: str) -> None:
|
| 125 |
+
"""Localize Streamlit uploaders without modifying their React structure."""
|
| 126 |
+
payload = {
|
| 127 |
+
"browse": tr(language, "browse_files"),
|
| 128 |
+
"uploaders": [
|
| 129 |
+
{
|
| 130 |
+
"title": tr(language, "drop_docs"),
|
| 131 |
+
"limit": tr(language, "limit_docs"),
|
| 132 |
+
},
|
| 133 |
+
{
|
| 134 |
+
"title": tr(language, "drop_data"),
|
| 135 |
+
"limit": tr(language, "limit_data"),
|
| 136 |
+
},
|
| 137 |
+
{
|
| 138 |
+
"title": tr(language, "drop_docs"),
|
| 139 |
+
"limit": tr(language, "limit_docs"),
|
| 140 |
+
},
|
| 141 |
+
{
|
| 142 |
+
"title": tr(language, "drop_interviews"),
|
| 143 |
+
"limit": tr(language, "limit_interviews"),
|
| 144 |
+
},
|
| 145 |
+
],
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
script = f"""
|
| 149 |
+
<script>
|
| 150 |
+
(() => {{
|
| 151 |
+
const payload = {json.dumps(payload, ensure_ascii=False)};
|
| 152 |
+
const parentDocument = window.parent.document;
|
| 153 |
+
|
| 154 |
+
function leafTextElements(root) {{
|
| 155 |
+
return Array.from(
|
| 156 |
+
root.querySelectorAll("p, span, small, div")
|
| 157 |
+
).filter((element) => (
|
| 158 |
+
element.children.length === 0 &&
|
| 159 |
+
element.textContent.trim().length > 0
|
| 160 |
+
));
|
| 161 |
+
}}
|
| 162 |
+
|
| 163 |
+
function replaceTextSafely(element, value) {{
|
| 164 |
+
if (element && element.textContent.trim() !== value) {{
|
| 165 |
+
element.textContent = value;
|
| 166 |
+
}}
|
| 167 |
+
}}
|
| 168 |
+
|
| 169 |
+
function applyUploaderTranslations() {{
|
| 170 |
+
const zones = parentDocument.querySelectorAll(
|
| 171 |
+
'[data-testid="stFileUploaderDropzone"]'
|
| 172 |
+
);
|
| 173 |
+
|
| 174 |
+
zones.forEach((zone, index) => {{
|
| 175 |
+
const config = payload.uploaders[index];
|
| 176 |
+
if (!config) return;
|
| 177 |
+
|
| 178 |
+
const instructions = zone.querySelector(
|
| 179 |
+
'[data-testid="stFileUploaderDropzoneInstructions"]'
|
| 180 |
+
);
|
| 181 |
+
|
| 182 |
+
if (instructions) {{
|
| 183 |
+
const leaves = leafTextElements(instructions);
|
| 184 |
+
replaceTextSafely(leaves[0], config.title);
|
| 185 |
+
replaceTextSafely(leaves[1], config.limit);
|
| 186 |
+
}}
|
| 187 |
+
|
| 188 |
+
const button = zone.querySelector("button");
|
| 189 |
+
if (button) {{
|
| 190 |
+
const buttonLeaves = leafTextElements(button);
|
| 191 |
+
const textTarget = buttonLeaves[buttonLeaves.length - 1];
|
| 192 |
+
|
| 193 |
+
replaceTextSafely(textTarget, payload.browse);
|
| 194 |
+
button.setAttribute("aria-label", payload.browse);
|
| 195 |
+
button.setAttribute("title", payload.browse);
|
| 196 |
+
}}
|
| 197 |
+
}});
|
| 198 |
+
}}
|
| 199 |
+
|
| 200 |
+
[0, 100, 300, 700, 1400, 2500].forEach((delay) => {{
|
| 201 |
+
window.setTimeout(applyUploaderTranslations, delay);
|
| 202 |
+
}});
|
| 203 |
+
}})();
|
| 204 |
+
</script>
|
| 205 |
+
"""
|
| 206 |
+
|
| 207 |
+
components.html(script, height=0, width=0)
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
def inject_wechat_browser_notice() -> None:
|
| 212 |
+
"""Warn immediately when SmartPaper is opened inside WeChat or WeCom."""
|
| 213 |
+
script = """
|
| 214 |
+
<script>
|
| 215 |
+
(() => {
|
| 216 |
+
const parentWindow = window.parent;
|
| 217 |
+
const parentDocument = parentWindow.document;
|
| 218 |
+
const userAgent = String(
|
| 219 |
+
parentWindow.navigator.userAgent || navigator.userAgent || ""
|
| 220 |
+
);
|
| 221 |
+
|
| 222 |
+
const isWechat =
|
| 223 |
+
/MicroMessenger|WeChat|wxwork/i.test(userAgent);
|
| 224 |
+
|
| 225 |
+
const noticeId = "smartpaper-wechat-browser-notice";
|
| 226 |
+
const existingNotice = parentDocument.getElementById(noticeId);
|
| 227 |
+
|
| 228 |
+
if (!isWechat) {
|
| 229 |
+
if (existingNotice) existingNotice.remove();
|
| 230 |
+
return;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
if (existingNotice) return;
|
| 234 |
+
|
| 235 |
+
const overlay = parentDocument.createElement("div");
|
| 236 |
+
overlay.id = noticeId;
|
| 237 |
+
overlay.style.cssText = [
|
| 238 |
+
"position:fixed",
|
| 239 |
+
"inset:0",
|
| 240 |
+
"z-index:2147483647",
|
| 241 |
+
"display:flex",
|
| 242 |
+
"align-items:center",
|
| 243 |
+
"justify-content:center",
|
| 244 |
+
"padding:24px",
|
| 245 |
+
"background:rgba(8,12,20,.96)",
|
| 246 |
+
"box-sizing:border-box"
|
| 247 |
+
].join(";");
|
| 248 |
+
|
| 249 |
+
const card = parentDocument.createElement("div");
|
| 250 |
+
card.style.cssText = [
|
| 251 |
+
"width:min(620px,100%)",
|
| 252 |
+
"padding:30px",
|
| 253 |
+
"border:1px solid rgba(148,163,184,.35)",
|
| 254 |
+
"border-radius:18px",
|
| 255 |
+
"background:#111827",
|
| 256 |
+
"color:#f8fafc",
|
| 257 |
+
"font-family:Arial,'Microsoft YaHei','PingFang SC',sans-serif",
|
| 258 |
+
"line-height:1.7",
|
| 259 |
+
"box-shadow:0 20px 50px rgba(0,0,0,.38)"
|
| 260 |
+
].join(";");
|
| 261 |
+
|
| 262 |
+
const title = parentDocument.createElement("h2");
|
| 263 |
+
title.textContent = "请在浏览器中打开 / Open in Browser";
|
| 264 |
+
title.style.cssText =
|
| 265 |
+
"margin:0 0 14px;font-size:24px;color:#ffffff;";
|
| 266 |
+
|
| 267 |
+
const chinese = parentDocument.createElement("p");
|
| 268 |
+
chinese.textContent =
|
| 269 |
+
"微信内置页面可能无法正常完成登录和文件操作。请点击右上角“…”并选择“在浏览器打开”。";
|
| 270 |
+
chinese.style.cssText = "margin:0 0 12px;font-size:16px;";
|
| 271 |
+
|
| 272 |
+
const english = parentDocument.createElement("p");
|
| 273 |
+
english.textContent =
|
| 274 |
+
"The WeChat in-app page may not complete sign-in or file operations correctly. Tap “…” in the top-right corner and choose “Open in Browser.”";
|
| 275 |
+
english.style.cssText =
|
| 276 |
+
"margin:0 0 20px;font-size:15px;color:#cbd5e1;";
|
| 277 |
+
|
| 278 |
+
const copyButton = parentDocument.createElement("button");
|
| 279 |
+
copyButton.type = "button";
|
| 280 |
+
copyButton.textContent = "复制网址 / Copy link";
|
| 281 |
+
copyButton.style.cssText = [
|
| 282 |
+
"width:100%",
|
| 283 |
+
"padding:12px 16px",
|
| 284 |
+
"border:0",
|
| 285 |
+
"border-radius:10px",
|
| 286 |
+
"background:#1677ff",
|
| 287 |
+
"color:#ffffff",
|
| 288 |
+
"font-size:16px",
|
| 289 |
+
"font-weight:700",
|
| 290 |
+
"cursor:pointer"
|
| 291 |
+
].join(";");
|
| 292 |
+
|
| 293 |
+
copyButton.addEventListener("click", async () => {
|
| 294 |
+
const url = parentWindow.location.href;
|
| 295 |
+
|
| 296 |
+
try {
|
| 297 |
+
await parentWindow.navigator.clipboard.writeText(url);
|
| 298 |
+
copyButton.textContent = "已复制 / Copied";
|
| 299 |
+
} catch (_) {
|
| 300 |
+
const textarea = parentDocument.createElement("textarea");
|
| 301 |
+
textarea.value = url;
|
| 302 |
+
textarea.style.position = "fixed";
|
| 303 |
+
textarea.style.opacity = "0";
|
| 304 |
+
parentDocument.body.appendChild(textarea);
|
| 305 |
+
textarea.focus();
|
| 306 |
+
textarea.select();
|
| 307 |
+
parentDocument.execCommand("copy");
|
| 308 |
+
textarea.remove();
|
| 309 |
+
copyButton.textContent = "已复制 / Copied";
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
parentWindow.setTimeout(() => {
|
| 313 |
+
copyButton.textContent = "复制网址 / Copy link";
|
| 314 |
+
}, 1800);
|
| 315 |
+
});
|
| 316 |
+
|
| 317 |
+
card.appendChild(title);
|
| 318 |
+
card.appendChild(chinese);
|
| 319 |
+
card.appendChild(english);
|
| 320 |
+
card.appendChild(copyButton);
|
| 321 |
+
overlay.appendChild(card);
|
| 322 |
+
parentDocument.body.appendChild(overlay);
|
| 323 |
+
})();
|
| 324 |
+
</script>
|
| 325 |
+
"""
|
| 326 |
+
|
| 327 |
+
components.html(script, height=0, width=0)
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
if "is_running" not in st.session_state: st.session_state.is_running = False
|
| 331 |
+
if "start_pipeline" not in st.session_state: st.session_state.start_pipeline = False
|
| 332 |
+
if "resume_pipeline" not in st.session_state: st.session_state.resume_pipeline = False
|
| 333 |
+
if "zip_buffer" not in st.session_state: st.session_state.zip_buffer = None
|
| 334 |
+
if "run_mode" not in st.session_state: st.session_state.run_mode = "amj"
|
| 335 |
+
if "sp_generation_task" not in st.session_state: st.session_state.sp_generation_task = None
|
| 336 |
+
if "sp_generation_idempotency_key" not in st.session_state: st.session_state.sp_generation_idempotency_key = ""
|
| 337 |
+
if "sp_last_task_heartbeat" not in st.session_state: st.session_state.sp_last_task_heartbeat = 0.0
|
| 338 |
+
if "sp_current_node" not in st.session_state: st.session_state.sp_current_node = ""
|
| 339 |
+
if "sp_pending_task_finalization" not in st.session_state: st.session_state.sp_pending_task_finalization = None
|
| 340 |
+
if "sp_pending_package" not in st.session_state: st.session_state.sp_pending_package = None
|
| 341 |
+
if "sp_terminal_notice" not in st.session_state: st.session_state.sp_terminal_notice = None
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
def _safe_user_workspace_key(value):
|
| 345 |
+
cleaned = re.sub(r"[^A-Za-z0-9_.-]", "_", str(value or "").strip())
|
| 346 |
+
return cleaned[:80] or "anonymous"
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
def _load_recoverable_checkpoint():
|
| 350 |
+
"""Return a validated checkpoint for the current authenticated user only."""
|
| 351 |
+
if not os.path.isfile(CHECKPOINT_FILE):
|
| 352 |
+
return None
|
| 353 |
+
try:
|
| 354 |
+
with open(CHECKPOINT_FILE, "r", encoding="utf-8") as handle:
|
| 355 |
+
state = json.load(handle)
|
| 356 |
+
except Exception:
|
| 357 |
+
return None
|
| 358 |
+
required = {
|
| 359 |
+
"topic", "keywords", "target_lang", "words",
|
| 360 |
+
"agent_state", "adaptive_structure", "completed_sections",
|
| 361 |
+
}
|
| 362 |
+
if not isinstance(state, dict) or not required.issubset(state):
|
| 363 |
+
return None
|
| 364 |
+
if not isinstance(state.get("adaptive_structure"), list):
|
| 365 |
+
return None
|
| 366 |
+
if not isinstance(state.get("completed_sections"), list):
|
| 367 |
+
return None
|
| 368 |
+
return state
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
def _new_generation_idempotency_key() -> str:
|
| 372 |
+
return f"gen_{int(time.time() * 1000)}_{secrets.token_urlsafe(18)}"
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
def _prepare_new_generation_request() -> None:
|
| 376 |
+
st.session_state["sp_generation_idempotency_key"] = _new_generation_idempotency_key()
|
| 377 |
+
st.session_state["sp_generation_task"] = None
|
| 378 |
+
st.session_state["sp_last_task_heartbeat"] = 0.0
|
| 379 |
+
st.session_state["sp_current_node"] = "task_start"
|
| 380 |
+
st.session_state["zip_buffer"] = None
|
| 381 |
+
st.session_state["zip_filename"] = ""
|
| 382 |
+
st.session_state["sp_pending_task_finalization"] = None
|
| 383 |
+
st.session_state["sp_pending_package"] = None
|
| 384 |
+
st.session_state["sp_terminal_notice"] = None
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
def _begin_managed_generation_task(*, is_resume: bool, loaded_state: dict | None, mode: str, language: str):
|
| 388 |
+
checkpoint_key = ""
|
| 389 |
+
if is_resume and isinstance(loaded_state, dict):
|
| 390 |
+
checkpoint_key = str(loaded_state.get("managed_task_idempotency_key") or "").strip()
|
| 391 |
+
|
| 392 |
+
idempotency_key = checkpoint_key or str(
|
| 393 |
+
st.session_state.get("sp_generation_idempotency_key") or ""
|
| 394 |
+
).strip()
|
| 395 |
+
if not idempotency_key:
|
| 396 |
+
idempotency_key = _new_generation_idempotency_key()
|
| 397 |
+
|
| 398 |
+
response = start_generation_task(
|
| 399 |
+
idempotency_key=idempotency_key,
|
| 400 |
+
mode=mode,
|
| 401 |
+
interface_language=language,
|
| 402 |
+
)
|
| 403 |
+
task = dict(response.get("task") or {})
|
| 404 |
+
if not task.get("task_id"):
|
| 405 |
+
raise RuntimeError("task_start_missing_task_id")
|
| 406 |
+
|
| 407 |
+
st.session_state["sp_generation_idempotency_key"] = idempotency_key
|
| 408 |
+
st.session_state["sp_generation_task"] = task
|
| 409 |
+
st.session_state["sp_last_task_heartbeat"] = time.time()
|
| 410 |
+
return task, idempotency_key
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
def _task_output_dir(task_id: str) -> str:
|
| 414 |
+
root = globals().get("_user_workspace") or os.path.join(WORKSPACE_DIR, "users", "anonymous")
|
| 415 |
+
path = os.path.join(root, "tasks", _safe_user_workspace_key(task_id), "outputs")
|
| 416 |
+
os.makedirs(path, exist_ok=True)
|
| 417 |
+
return path
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
def _checkpoint_state_for_update() -> dict:
|
| 421 |
+
if not os.path.isfile(CHECKPOINT_FILE):
|
| 422 |
+
return {}
|
| 423 |
+
try:
|
| 424 |
+
with open(CHECKPOINT_FILE, "r", encoding="utf-8") as handle:
|
| 425 |
+
value = json.load(handle)
|
| 426 |
+
return value if isinstance(value, dict) else {}
|
| 427 |
+
except Exception:
|
| 428 |
+
return {}
|
| 429 |
+
|
| 430 |
+
|
| 431 |
+
def _write_checkpoint_state(value: dict) -> None:
|
| 432 |
+
if not isinstance(value, dict) or not value:
|
| 433 |
+
return
|
| 434 |
+
os.makedirs(os.path.dirname(CHECKPOINT_FILE), exist_ok=True)
|
| 435 |
+
temporary = CHECKPOINT_FILE + ".tmp"
|
| 436 |
+
with open(temporary, "w", encoding="utf-8") as handle:
|
| 437 |
+
json.dump(value, handle, ensure_ascii=False)
|
| 438 |
+
os.replace(temporary, CHECKPOINT_FILE)
|
| 439 |
+
|
| 440 |
+
|
| 441 |
+
def _persist_pending_finalization() -> None:
|
| 442 |
+
pending = st.session_state.get("sp_pending_task_finalization")
|
| 443 |
+
if not isinstance(pending, dict):
|
| 444 |
+
return
|
| 445 |
+
state = _checkpoint_state_for_update()
|
| 446 |
+
if not state:
|
| 447 |
+
return
|
| 448 |
+
state["pending_task_finalization"] = pending
|
| 449 |
+
package = st.session_state.get("sp_pending_package")
|
| 450 |
+
if isinstance(package, dict):
|
| 451 |
+
state["pending_package"] = package
|
| 452 |
+
_write_checkpoint_state(state)
|
| 453 |
+
|
| 454 |
+
|
| 455 |
+
def _restore_pending_finalization() -> None:
|
| 456 |
+
if isinstance(st.session_state.get("sp_pending_task_finalization"), dict):
|
| 457 |
+
return
|
| 458 |
+
state = _checkpoint_state_for_update()
|
| 459 |
+
pending = state.get("pending_task_finalization") if isinstance(state, dict) else None
|
| 460 |
+
if not isinstance(pending, dict):
|
| 461 |
+
return
|
| 462 |
+
st.session_state["sp_pending_task_finalization"] = pending
|
| 463 |
+
package = state.get("pending_package")
|
| 464 |
+
if isinstance(package, dict):
|
| 465 |
+
st.session_state["sp_pending_package"] = package
|
| 466 |
+
task_id = str(pending.get("task_id") or "").strip()
|
| 467 |
+
if task_id and not st.session_state.get("sp_generation_task"):
|
| 468 |
+
st.session_state["sp_generation_task"] = {"task_id": task_id}
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
def _remove_pending_checkpoint_metadata(*, delete_checkpoint=False, detach_task=False) -> None:
|
| 472 |
+
if delete_checkpoint:
|
| 473 |
+
try:
|
| 474 |
+
os.remove(CHECKPOINT_FILE)
|
| 475 |
+
except OSError:
|
| 476 |
+
pass
|
| 477 |
+
return
|
| 478 |
+
state = _checkpoint_state_for_update()
|
| 479 |
+
if not state:
|
| 480 |
+
return
|
| 481 |
+
state.pop("pending_task_finalization", None)
|
| 482 |
+
state.pop("pending_package", None)
|
| 483 |
+
if detach_task:
|
| 484 |
+
state["managed_task_idempotency_key"] = ""
|
| 485 |
+
state["managed_task_id"] = ""
|
| 486 |
+
_write_checkpoint_state(state)
|
| 487 |
+
|
| 488 |
+
|
| 489 |
+
def _prune_user_output_packages(max_files: int = 5) -> None:
|
| 490 |
+
root = globals().get("_user_workspace")
|
| 491 |
+
if not root or not os.path.isdir(root):
|
| 492 |
+
return
|
| 493 |
+
packages = []
|
| 494 |
+
for current_root, _, names in os.walk(os.path.join(root, "tasks")):
|
| 495 |
+
for name in names:
|
| 496 |
+
if name.endswith(".zip") and not name.startswith(".pending_"):
|
| 497 |
+
path = os.path.join(current_root, name)
|
| 498 |
+
try:
|
| 499 |
+
packages.append((os.path.getmtime(path), path))
|
| 500 |
+
except OSError:
|
| 501 |
+
pass
|
| 502 |
+
packages.sort()
|
| 503 |
+
while len(packages) > max_files:
|
| 504 |
+
_, old_path = packages.pop(0)
|
| 505 |
+
try:
|
| 506 |
+
os.remove(old_path)
|
| 507 |
+
except OSError:
|
| 508 |
+
pass
|
| 509 |
+
|
| 510 |
+
|
| 511 |
+
def _process_pending_task_finalization():
|
| 512 |
+
pending = st.session_state.get("sp_pending_task_finalization")
|
| 513 |
+
if not isinstance(pending, dict):
|
| 514 |
+
return None
|
| 515 |
+
task_id = str(pending.get("task_id") or "").strip()
|
| 516 |
+
status = str(pending.get("status") or "").strip().lower()
|
| 517 |
+
if not task_id or status not in {"completed", "failed", "cancelled", "resumable"}:
|
| 518 |
+
raise RuntimeError("invalid_pending_task_finalization")
|
| 519 |
+
|
| 520 |
+
package_bytes = None
|
| 521 |
+
package = st.session_state.get("sp_pending_package")
|
| 522 |
+
if status == "completed":
|
| 523 |
+
if not isinstance(package, dict):
|
| 524 |
+
raise RuntimeError("validated_package_metadata_missing")
|
| 525 |
+
pending_path = str(package.get("pending_path") or "").strip()
|
| 526 |
+
if not pending_path or not os.path.isfile(pending_path):
|
| 527 |
+
raise RuntimeError("validated_package_file_missing")
|
| 528 |
+
with open(pending_path, "rb") as handle:
|
| 529 |
+
package_bytes = handle.read()
|
| 530 |
+
if not package_bytes:
|
| 531 |
+
raise RuntimeError("validated_package_file_empty")
|
| 532 |
+
|
| 533 |
+
response = finish_generation_task(
|
| 534 |
+
task_id,
|
| 535 |
+
status=status,
|
| 536 |
+
safe_error_code=str(pending.get("safe_error_code") or "")[:80],
|
| 537 |
+
safe_error_detail=str(pending.get("safe_error_detail") or "")[:800],
|
| 538 |
+
current_node=str(pending.get("current_node") or "")[:240],
|
| 539 |
+
)
|
| 540 |
+
|
| 541 |
+
if status == "completed":
|
| 542 |
+
filename = str(package.get("filename") or "SmartPaper.zip")
|
| 543 |
+
pending_path = str(package.get("pending_path") or "")
|
| 544 |
+
final_path = os.path.join(_task_output_dir(task_id), os.path.basename(filename))
|
| 545 |
+
try:
|
| 546 |
+
os.replace(pending_path, final_path)
|
| 547 |
+
except OSError:
|
| 548 |
+
# The bytes were read and validated before quota commit, so the user
|
| 549 |
+
# can still download even if the local archival rename fails.
|
| 550 |
+
final_path = pending_path
|
| 551 |
+
st.session_state["zip_buffer"] = package_bytes
|
| 552 |
+
st.session_state["zip_filename"] = os.path.basename(filename)
|
| 553 |
+
st.session_state["sp_terminal_notice"] = {"kind": "success"}
|
| 554 |
+
_prune_user_output_packages()
|
| 555 |
+
elif status in {"failed", "cancelled"}:
|
| 556 |
+
st.session_state["sp_terminal_notice"] = {
|
| 557 |
+
"kind": "error" if status == "failed" else "warning",
|
| 558 |
+
"message": str(pending.get("safe_error_detail") or "")[:800],
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
_remove_pending_checkpoint_metadata(
|
| 562 |
+
delete_checkpoint=bool(pending.get("delete_checkpoint")),
|
| 563 |
+
detach_task=bool(pending.get("detach_checkpoint")),
|
| 564 |
+
)
|
| 565 |
+
if status == "resumable":
|
| 566 |
+
st.session_state["sp_generation_task"] = dict(response.get("task") or {"task_id": task_id})
|
| 567 |
+
else:
|
| 568 |
+
st.session_state["sp_generation_task"] = None
|
| 569 |
+
st.session_state["sp_generation_idempotency_key"] = ""
|
| 570 |
+
st.session_state["sp_last_task_heartbeat"] = 0.0
|
| 571 |
+
st.session_state["sp_pending_task_finalization"] = None
|
| 572 |
+
st.session_state["sp_pending_package"] = None
|
| 573 |
+
return response
|
| 574 |
+
|
| 575 |
+
|
| 576 |
+
def _finish_managed_generation_task(
|
| 577 |
+
status: str,
|
| 578 |
+
*,
|
| 579 |
+
safe_error_code: str = "",
|
| 580 |
+
safe_error_detail: str = "",
|
| 581 |
+
current_node: str = "",
|
| 582 |
+
delete_checkpoint: bool = False,
|
| 583 |
+
detach_checkpoint: bool = False,
|
| 584 |
+
):
|
| 585 |
+
task = st.session_state.get("sp_generation_task") or {}
|
| 586 |
+
task_id = str(task.get("task_id") or "").strip()
|
| 587 |
+
if not task_id:
|
| 588 |
+
return None
|
| 589 |
+
pending = {
|
| 590 |
+
"task_id": task_id,
|
| 591 |
+
"status": str(status or "").strip().lower(),
|
| 592 |
+
"safe_error_code": str(safe_error_code or "")[:80],
|
| 593 |
+
"safe_error_detail": str(safe_error_detail or "")[:800],
|
| 594 |
+
"current_node": str(current_node or "")[:240],
|
| 595 |
+
"delete_checkpoint": bool(delete_checkpoint),
|
| 596 |
+
"detach_checkpoint": bool(detach_checkpoint),
|
| 597 |
+
}
|
| 598 |
+
st.session_state["sp_pending_task_finalization"] = pending
|
| 599 |
+
_persist_pending_finalization()
|
| 600 |
+
return _process_pending_task_finalization()
|
| 601 |
+
|
| 602 |
+
|
| 603 |
+
def _quota_start_error_message(language: str, error: Exception) -> str:
|
| 604 |
+
raw = str(error or "")
|
| 605 |
+
return tr(language, "quota_exhausted") if "payment_required" in raw else raw
|
| 606 |
+
|
| 607 |
+
|
| 608 |
+
inject_wechat_browser_notice()
|
| 609 |
+
|
| 610 |
+
DEFAULT_TOPIC = "Generative Artificial Intelligence in Ubiquitous Learning: Evaluating a Chatbot-based Recommendation Engine for Personalized and Context-aware Education"
|
| 611 |
+
DEFAULT_KEYWORDS = "Ubiquitous computing; Context awareness; Deep learning; Personalized educational; Educational technologies; HCI; Recommendation system; LLMs; Generative AI; ChatGPT; Prompting engineering; Few-shot prompting"
|
| 612 |
+
|
| 613 |
+
_fragment = getattr(st, "fragment", None) or getattr(st, "experimental_fragment", None)
|
| 614 |
+
if _fragment is None:
|
| 615 |
+
def _fragment(function):
|
| 616 |
+
return function
|
| 617 |
+
|
| 618 |
+
|
| 619 |
+
def _current_language_code() -> str:
|
| 620 |
+
choice = st.session_state.get("interface_language_choice")
|
| 621 |
+
return LANGUAGE_OPTIONS.get(choice, "zh")
|
| 622 |
+
|
| 623 |
+
|
| 624 |
+
# Public access uses Alipay only. Password, legacy, and pilot login paths are not exposed.
|
| 625 |
+
authenticated_user = require_alipay_login(_current_language_code())
|
| 626 |
+
|
| 627 |
+
# Checkpoints are user-scoped. The former global snapshot.json could cause one
|
| 628 |
+
# user or an old run to trigger a false recovery notice for another session.
|
| 629 |
+
_user_workspace = os.path.join(
|
| 630 |
+
WORKSPACE_DIR,
|
| 631 |
+
"users",
|
| 632 |
+
_safe_user_workspace_key(authenticated_user.get("id")),
|
| 633 |
+
)
|
| 634 |
+
os.makedirs(_user_workspace, exist_ok=True)
|
| 635 |
+
CHECKPOINT_FILE = os.path.join(_user_workspace, "snapshot.json")
|
| 636 |
+
_restore_pending_finalization()
|
| 637 |
+
_process_pending_task_finalization()
|
| 638 |
+
|
| 639 |
+
|
| 640 |
+
@_fragment
|
| 641 |
+
def render_workspace_controls(user):
|
| 642 |
+
"""Render controls in an isolated rerun scope for fast language switching."""
|
| 643 |
+
top_toolbar = st.container()
|
| 644 |
+
|
| 645 |
+
with top_toolbar:
|
| 646 |
+
language_col, account_col = st.columns([5.4, 4.6])
|
| 647 |
+
|
| 648 |
+
with language_col:
|
| 649 |
+
lang_choice = st.radio(
|
| 650 |
+
"Language",
|
| 651 |
+
list(LANGUAGE_OPTIONS.keys()),
|
| 652 |
+
index=0,
|
| 653 |
+
horizontal=True,
|
| 654 |
+
label_visibility="collapsed",
|
| 655 |
+
key="interface_language_choice",
|
| 656 |
+
)
|
| 657 |
+
|
| 658 |
+
lang_code = LANGUAGE_OPTIONS[lang_choice]
|
| 659 |
+
st.session_state["sp_lang_code"] = lang_code
|
| 660 |
+
st.session_state["sp_target_lang"] = get_target_language(lang_code)
|
| 661 |
+
st.session_state["sp_output_suffix"] = get_output_suffix(lang_code)
|
| 662 |
+
st.session_state["sp_target_flag"] = get_target_flag(lang_code)
|
| 663 |
+
|
| 664 |
+
with account_col:
|
| 665 |
+
render_logged_in_account(lang_code, user)
|
| 666 |
+
|
| 667 |
+
# Cross-device global busy check remains unchanged in function, but all visible text is localized.
|
| 668 |
+
is_my_session = st.session_state.get("is_running", False) or \
|
| 669 |
+
st.session_state.get("start_pipeline", False) or \
|
| 670 |
+
st.session_state.get("resume_pipeline", False)
|
| 671 |
+
ui_blocked = (not is_my_session and is_system_busy())
|
| 672 |
+
st.session_state["sp_ui_blocked"] = ui_blocked
|
| 673 |
+
if ui_blocked:
|
| 674 |
+
st.markdown(
|
| 675 |
+
f"<h3 style='text-align:center;color:#e6a23c;'>{tr(lang_code, 'system_busy_title')}</h3>",
|
| 676 |
+
unsafe_allow_html=True,
|
| 677 |
+
)
|
| 678 |
+
st.warning(tr(lang_code, "system_busy_warning"))
|
| 679 |
+
st.info(tr(lang_code, "system_busy_info"))
|
| 680 |
+
return
|
| 681 |
+
|
| 682 |
+
st.markdown(f"<div class='main-title'><h2>{tr(lang_code, 'title')}</h2></div>", unsafe_allow_html=True)
|
| 683 |
+
|
| 684 |
+
if _load_recoverable_checkpoint() is not None and not (st.session_state.is_running or st.session_state.resume_pipeline or st.session_state.start_pipeline):
|
| 685 |
+
st.warning(tr(lang_code, "checkpoint_warning"))
|
| 686 |
+
colA, colB = st.columns([1, 1])
|
| 687 |
+
if colA.button(tr(lang_code, "resume"), key="resume_task"):
|
| 688 |
+
if is_system_busy():
|
| 689 |
+
st.warning(tr(lang_code, "busy_short"))
|
| 690 |
+
else:
|
| 691 |
+
checkpoint_state = _load_recoverable_checkpoint() or {}
|
| 692 |
+
checkpoint_key = str(checkpoint_state.get("managed_task_idempotency_key") or "").strip()
|
| 693 |
+
st.session_state["sp_generation_idempotency_key"] = checkpoint_key or _new_generation_idempotency_key()
|
| 694 |
+
st.session_state["sp_generation_task"] = None
|
| 695 |
+
st.session_state["sp_last_task_heartbeat"] = 0.0
|
| 696 |
+
update_heartbeat()
|
| 697 |
+
st.session_state.resume_pipeline = True
|
| 698 |
+
st.session_state.start_pipeline = False
|
| 699 |
+
st.rerun()
|
| 700 |
+
|
| 701 |
+
if colB.button(tr(lang_code, "clear"), key="clear_task"):
|
| 702 |
+
if is_system_busy():
|
| 703 |
+
st.warning(tr(lang_code, "busy_short"))
|
| 704 |
+
else:
|
| 705 |
+
checkpoint_state = _load_recoverable_checkpoint() or {}
|
| 706 |
+
checkpoint_task_id = str(checkpoint_state.get("managed_task_id") or "").strip()
|
| 707 |
+
if checkpoint_task_id:
|
| 708 |
+
st.session_state["sp_generation_task"] = {"task_id": checkpoint_task_id}
|
| 709 |
+
try:
|
| 710 |
+
_finish_managed_generation_task(
|
| 711 |
+
"cancelled",
|
| 712 |
+
safe_error_code="E_USER_CLEARED_CHECKPOINT",
|
| 713 |
+
safe_error_detail="User cleared the recoverable checkpoint.",
|
| 714 |
+
current_node="checkpoint_clear",
|
| 715 |
+
delete_checkpoint=True,
|
| 716 |
+
)
|
| 717 |
+
except Exception as exc:
|
| 718 |
+
st.error(str(exc))
|
| 719 |
+
return
|
| 720 |
+
else:
|
| 721 |
+
try:
|
| 722 |
+
os.remove(CHECKPOINT_FILE)
|
| 723 |
+
except OSError:
|
| 724 |
+
pass
|
| 725 |
+
clear_heartbeat()
|
| 726 |
+
st.rerun()
|
| 727 |
+
st.markdown("---")
|
| 728 |
+
|
| 729 |
+
st.text_input(
|
| 730 |
+
tr(lang_code, "topic_label"),
|
| 731 |
+
value=DEFAULT_TOPIC,
|
| 732 |
+
disabled=st.session_state.is_running,
|
| 733 |
+
key="sp_topic",
|
| 734 |
+
)
|
| 735 |
+
st.text_input(
|
| 736 |
+
tr(lang_code, "keywords_label"),
|
| 737 |
+
value=DEFAULT_KEYWORDS,
|
| 738 |
+
disabled=st.session_state.is_running,
|
| 739 |
+
key="sp_keywords",
|
| 740 |
+
)
|
| 741 |
+
st.number_input(
|
| 742 |
+
tr(lang_code, "words_label"),
|
| 743 |
+
min_value=12000,
|
| 744 |
+
max_value=25000,
|
| 745 |
+
value=20000,
|
| 746 |
+
step=1000,
|
| 747 |
+
disabled=st.session_state.is_running,
|
| 748 |
+
key="sp_words",
|
| 749 |
+
)
|
| 750 |
+
|
| 751 |
+
tab_amj, tab_asq = st.tabs([tr(lang_code, "tab_amj"), tr(lang_code, "tab_asq")])
|
| 752 |
+
|
| 753 |
+
with tab_amj:
|
| 754 |
+
st.markdown(tr(lang_code, "amj_config"))
|
| 755 |
+
st.info(tr(lang_code, "amj_info"))
|
| 756 |
+
st.file_uploader(
|
| 757 |
+
tr(lang_code, "upload_reference"),
|
| 758 |
+
type=["pdf", "doc", "docx"],
|
| 759 |
+
accept_multiple_files=True,
|
| 760 |
+
key="up_amj_ref",
|
| 761 |
+
disabled=st.session_state.is_running,
|
| 762 |
+
)
|
| 763 |
+
st.file_uploader(
|
| 764 |
+
tr(lang_code, "upload_stats"),
|
| 765 |
+
type=["csv", "xlsx", "xls"],
|
| 766 |
+
key="up_amj_data",
|
| 767 |
+
disabled=st.session_state.is_running,
|
| 768 |
+
)
|
| 769 |
+
|
| 770 |
+
with st.expander(tr(lang_code, "amj_expander")):
|
| 771 |
+
st.markdown(tr(lang_code, "amj_hint"))
|
| 772 |
+
amj_csv_template = get_amj_csv_template(lang_code)
|
| 773 |
+
st.code(amj_csv_template, language="csv")
|
| 774 |
+
st.download_button(
|
| 775 |
+
label=tr(lang_code, "download_amj_template"),
|
| 776 |
+
data=amj_csv_template,
|
| 777 |
+
file_name="AMJ_Data_Template.csv",
|
| 778 |
+
mime="text/csv",
|
| 779 |
+
use_container_width=True,
|
| 780 |
+
)
|
| 781 |
+
|
| 782 |
+
if st.button(tr(lang_code, "start_amj"), disabled=st.session_state.is_running, key="btn_amj"):
|
| 783 |
+
if is_system_busy():
|
| 784 |
+
st.warning(tr(lang_code, "busy_short"))
|
| 785 |
+
else:
|
| 786 |
+
_prepare_new_generation_request()
|
| 787 |
+
update_heartbeat()
|
| 788 |
+
st.session_state.run_mode = "amj"
|
| 789 |
+
st.session_state.start_pipeline = True
|
| 790 |
+
st.rerun()
|
| 791 |
+
|
| 792 |
+
with tab_asq:
|
| 793 |
+
st.markdown(tr(lang_code, "asq_config"))
|
| 794 |
+
st.info(tr(lang_code, "asq_info"))
|
| 795 |
+
st.file_uploader(
|
| 796 |
+
tr(lang_code, "upload_reference"),
|
| 797 |
+
type=["pdf", "doc", "docx"],
|
| 798 |
+
accept_multiple_files=True,
|
| 799 |
+
key="up_asq_ref",
|
| 800 |
+
disabled=st.session_state.is_running,
|
| 801 |
+
)
|
| 802 |
+
st.file_uploader(
|
| 803 |
+
tr(lang_code, "upload_interviews"),
|
| 804 |
+
type=["txt", "pdf", "doc", "docx"],
|
| 805 |
+
key="up_asq_data",
|
| 806 |
+
disabled=st.session_state.is_running,
|
| 807 |
+
)
|
| 808 |
+
|
| 809 |
+
with st.expander(tr(lang_code, "asq_expander")):
|
| 810 |
+
st.markdown(tr(lang_code, "asq_hint"))
|
| 811 |
+
asq_txt_template = get_asq_txt_template(lang_code)
|
| 812 |
+
st.code(asq_txt_template, language="text")
|
| 813 |
+
st.download_button(
|
| 814 |
+
label=tr(lang_code, "download_asq_template"),
|
| 815 |
+
data=asq_txt_template,
|
| 816 |
+
file_name="ASQ_Interview_Template.txt",
|
| 817 |
+
mime="text/plain",
|
| 818 |
+
use_container_width=True,
|
| 819 |
+
)
|
| 820 |
+
|
| 821 |
+
if st.button(tr(lang_code, "start_asq"), disabled=st.session_state.is_running, key="btn_asq"):
|
| 822 |
+
if is_system_busy():
|
| 823 |
+
st.warning(tr(lang_code, "busy_short"))
|
| 824 |
+
else:
|
| 825 |
+
_prepare_new_generation_request()
|
| 826 |
+
update_heartbeat()
|
| 827 |
+
st.session_state.run_mode = "asq"
|
| 828 |
+
st.session_state.start_pipeline = True
|
| 829 |
+
st.rerun()
|
| 830 |
+
|
| 831 |
+
# Localize Streamlit built-in uploader text after the uploaders are rendered.
|
| 832 |
+
inject_file_uploader_i18n(lang_code)
|
| 833 |
+
|
| 834 |
+
|
| 835 |
+
render_workspace_controls(authenticated_user)
|
| 836 |
+
|
| 837 |
+
lang_code = str(st.session_state.get("sp_lang_code") or _current_language_code())
|
| 838 |
+
target_lang = str(st.session_state.get("sp_target_lang") or get_target_language(lang_code))
|
| 839 |
+
output_suffix = str(st.session_state.get("sp_output_suffix") or get_output_suffix(lang_code))
|
| 840 |
+
target_flag = str(st.session_state.get("sp_target_flag") or get_target_flag(lang_code))
|
| 841 |
+
|
| 842 |
+
topic = str(st.session_state.get("sp_topic") or DEFAULT_TOPIC)
|
| 843 |
+
keywords = str(st.session_state.get("sp_keywords") or DEFAULT_KEYWORDS)
|
| 844 |
+
words = int(st.session_state.get("sp_words") or 20000)
|
| 845 |
+
uploaded_papers_amj = st.session_state.get("up_amj_ref") or []
|
| 846 |
+
uploaded_data_amj = st.session_state.get("up_amj_data")
|
| 847 |
+
uploaded_papers_asq = st.session_state.get("up_asq_ref") or []
|
| 848 |
+
uploaded_interviews = st.session_state.get("up_asq_data")
|
| 849 |
+
|
| 850 |
+
if st.session_state.get("sp_ui_blocked"):
|
| 851 |
+
st.stop()
|
| 852 |
+
|
| 853 |
+
if st.session_state.get("sp_payment_required") and not (st.session_state.start_pipeline or st.session_state.resume_pipeline):
|
| 854 |
+
render_payment_purchase(lang_code)
|
| 855 |
+
|
| 856 |
+
_terminal_notice = st.session_state.pop("sp_terminal_notice", None)
|
| 857 |
+
if isinstance(_terminal_notice, dict) and _terminal_notice.get("kind") in {"error", "warning"}:
|
| 858 |
+
_message = str(_terminal_notice.get("message") or "").strip()
|
| 859 |
+
if _message:
|
| 860 |
+
(st.error if _terminal_notice.get("kind") == "error" else st.warning)(_message)
|
| 861 |
+
|
| 862 |
+
progress_bar = st.empty()
|
| 863 |
+
monitor_ui = st.empty()
|
| 864 |
+
|
| 865 |
+
def render_monitor(agent, phase, current_sec="", details=""):
|
| 866 |
+
st.session_state["sp_current_node"] = str(current_sec or phase or "")[:240]
|
| 867 |
+
monitor_ui.markdown(f"""
|
| 868 |
+
<div class='log-box'>
|
| 869 |
+
<div><b>{tr(lang_code, 'monitor_pipeline')}:</b> {phase}</div><div><b>{tr(lang_code, 'monitor_node')}:</b> {current_sec}</div><div><b>{tr(lang_code, 'monitor_log')}:</b> {details}</div><hr>
|
| 870 |
+
<div><b>{tr(lang_code, 'monitor_compute')}:</b></div>
|
| 871 |
+
<div style='display: flex; gap: 8px;'>
|
| 872 |
+
<span style='background: #fef3c7; padding: 4px 8px; border-radius: 4px; color: black;'>🧠 DeepSeek: <b>{agent.ds_calls}</b></span>
|
| 873 |
+
<span style='background: #dbeafe; padding: 4px 8px; border-radius: 4px; color: black;'>📚 S2: <b>{agent.s2_calls}</b></span>
|
| 874 |
+
<span style='background: #d1fae5; padding: 4px 8px; border-radius: 4px; color: black;'>🌐 OpenAlex: <b>{agent.oa_calls}</b></span>
|
| 875 |
+
</div>
|
| 876 |
+
</div>
|
| 877 |
+
""", unsafe_allow_html=True)
|
| 878 |
+
|
| 879 |
+
FLAG_MAP = {"Simplified Chinese (简体中文)": "🇨🇳", "Russian (Русский)": "🇷🇺", "Kazakh (Қазақша)": "🇰🇿"}
|
| 880 |
+
|
| 881 |
+
if st.session_state.start_pipeline or st.session_state.resume_pipeline:
|
| 882 |
+
if not global_ds_key:
|
| 883 |
+
st.error(tr(lang_code, "api_key_missing"))
|
| 884 |
+
st.stop()
|
| 885 |
+
|
| 886 |
+
if st.session_state.get("is_running", False):
|
| 887 |
+
recoverable_state = _load_recoverable_checkpoint()
|
| 888 |
+
st.session_state.is_running = False
|
| 889 |
+
st.session_state.start_pipeline = False
|
| 890 |
+
st.session_state.resume_pipeline = bool(recoverable_state)
|
| 891 |
+
if recoverable_state:
|
| 892 |
+
st.toast(tr(lang_code, "resume_toast"), icon="⚡")
|
| 893 |
+
update_heartbeat()
|
| 894 |
+
st.rerun()
|
| 895 |
+
# A normal page rerun without a valid checkpoint is not a recovery event.
|
| 896 |
+
# Keep the already-rendered writing interface and show no recovery toast.
|
| 897 |
+
clear_heartbeat()
|
| 898 |
+
st.stop()
|
| 899 |
+
|
| 900 |
+
is_resume = st.session_state.resume_pipeline
|
| 901 |
+
loaded_state = _load_recoverable_checkpoint() if is_resume else None
|
| 902 |
+
if is_resume and loaded_state is None:
|
| 903 |
+
is_resume = False
|
| 904 |
+
|
| 905 |
+
try:
|
| 906 |
+
managed_task, managed_task_key = _begin_managed_generation_task(
|
| 907 |
+
is_resume=is_resume,
|
| 908 |
+
loaded_state=loaded_state,
|
| 909 |
+
mode=st.session_state.run_mode,
|
| 910 |
+
language=lang_code,
|
| 911 |
+
)
|
| 912 |
+
except Exception as exc:
|
| 913 |
+
clear_heartbeat()
|
| 914 |
+
st.session_state.is_running = False
|
| 915 |
+
st.session_state.start_pipeline = False
|
| 916 |
+
st.session_state.resume_pipeline = False
|
| 917 |
+
st.error(_quota_start_error_message(lang_code, exc))
|
| 918 |
+
if "payment_required" in str(exc):
|
| 919 |
+
st.session_state["sp_payment_required"] = True
|
| 920 |
+
render_payment_purchase(lang_code)
|
| 921 |
+
st.stop()
|
| 922 |
+
|
| 923 |
+
st.session_state.is_running = True
|
| 924 |
+
agent = SSciPipelineAgent(global_ds_key, global_s2_key)
|
| 925 |
+
SAFE_CHUNK_WORDS = agent.chunk_words_default
|
| 926 |
+
qual_engine = get_qualitative_engine(agent)
|
| 927 |
+
quant_engine = get_quantitative_engine()
|
| 928 |
+
completion_finalization_attempted = False
|
| 929 |
+
|
| 930 |
+
try:
|
| 931 |
+
update_heartbeat()
|
| 932 |
+
|
| 933 |
+
if is_resume and loaded_state:
|
| 934 |
+
pb = progress_bar.progress(50, text=tr(lang_code, "resume_loading"))
|
| 935 |
+
state = loaded_state
|
| 936 |
+
state["managed_task_idempotency_key"] = managed_task_key
|
| 937 |
+
state["managed_task_id"] = managed_task.get("task_id", "")
|
| 938 |
+
topic = state["topic"]
|
| 939 |
+
keywords = state["keywords"]
|
| 940 |
+
target_lang = state["target_lang"]
|
| 941 |
+
words = state["words"]
|
| 942 |
+
user_data_md = state["user_data_md"]
|
| 943 |
+
data_metadata = state.get("data_metadata", {})
|
| 944 |
+
user_lit_context = state["user_lit_context"]
|
| 945 |
+
|
| 946 |
+
agent.load_state(state["agent_state"])
|
| 947 |
+
adaptive_structure = state["adaptive_structure"]
|
| 948 |
+
completed_sections = state["completed_sections"]
|
| 949 |
+
full_draft_en = state["full_draft_en"]
|
| 950 |
+
full_draft_translated = state["full_draft_translated"]
|
| 951 |
+
synthesis_result = state["synthesis_result"]
|
| 952 |
+
refs_text = state["refs_text"]
|
| 953 |
+
context_tail = state["context_tail"]
|
| 954 |
+
|
| 955 |
+
st.session_state.run_mode = state.get("run_mode", "amj")
|
| 956 |
+
if st.session_state.run_mode == "amj":
|
| 957 |
+
APP_CONFIG.update(AMJ_PROMPTS)
|
| 958 |
+
else:
|
| 959 |
+
APP_CONFIG.update(ASQ_PROMPTS)
|
| 960 |
+
|
| 961 |
+
st.success(tr(lang_code, "resume_success", done=len(completed_sections), total=len(adaptive_structure)))
|
| 962 |
+
render_monitor(agent, tr(lang_code, "resume_phase"), tr(lang_code, "state_check"), tr(lang_code, "history_restored"))
|
| 963 |
+
|
| 964 |
+
else:
|
| 965 |
+
pb = progress_bar.progress(1, text=tr(lang_code, "pipeline_start"))
|
| 966 |
+
user_lit_context = ""
|
| 967 |
+
user_data_md = ""
|
| 968 |
+
data_metadata = {}
|
| 969 |
+
|
| 970 |
+
if st.session_state.run_mode == "amj":
|
| 971 |
+
if uploaded_papers_amj:
|
| 972 |
+
user_lit_context = extract_text_from_files(uploaded_papers_amj[:20])
|
| 973 |
+
with st.status(tr(lang_code, "literature_extract"), expanded=True) as status:
|
| 974 |
+
ph = st.empty()
|
| 975 |
+
all_paper_texts = [extract_text_from_files([f]) for f in uploaded_papers_amj[:20]]
|
| 976 |
+
|
| 977 |
+
future = ThreadPoolExecutor(max_workers=1).submit(extract_golden_metadata, agent, all_paper_texts)
|
| 978 |
+
dot_count = 0
|
| 979 |
+
while not future.done():
|
| 980 |
+
update_heartbeat()
|
| 981 |
+
ph.markdown(tr(lang_code, "literature_deep") + " ⏳" * (dot_count % 4))
|
| 982 |
+
time.sleep(1)
|
| 983 |
+
dot_count += 1
|
| 984 |
+
|
| 985 |
+
agent.global_memory["core_golden_metadata"] = future.result()
|
| 986 |
+
st.session_state.rag_engine = initialize_rag(all_paper_texts)
|
| 987 |
+
status.update(label=tr(lang_code, "literature_done"), state="complete", expanded=False)
|
| 988 |
+
|
| 989 |
+
with st.spinner(tr(lang_code, "extract_quant")):
|
| 990 |
+
update_heartbeat()
|
| 991 |
+
_, user_data_md, msg, data_metadata = quant_engine.resolve_empirical_data(uploaded_data_amj, lang_code)
|
| 992 |
+
st.info(msg)
|
| 993 |
+
|
| 994 |
+
elif st.session_state.run_mode == "asq":
|
| 995 |
+
if uploaded_papers_asq:
|
| 996 |
+
user_lit_context = extract_text_from_files(uploaded_papers_asq[:20])
|
| 997 |
+
with st.status(tr(lang_code, "literature_extract"), expanded=True) as status:
|
| 998 |
+
ph = st.empty()
|
| 999 |
+
all_paper_texts = [extract_text_from_files([f]) for f in uploaded_papers_asq[:20]]
|
| 1000 |
+
|
| 1001 |
+
future = ThreadPoolExecutor(max_workers=1).submit(extract_golden_metadata, agent, all_paper_texts)
|
| 1002 |
+
dot_count = 0
|
| 1003 |
+
while not future.done():
|
| 1004 |
+
update_heartbeat()
|
| 1005 |
+
ph.markdown(tr(lang_code, "literature_deep") + " ⏳" * (dot_count % 4))
|
| 1006 |
+
time.sleep(1)
|
| 1007 |
+
dot_count += 1
|
| 1008 |
+
|
| 1009 |
+
agent.global_memory["core_golden_metadata"] = future.result()
|
| 1010 |
+
st.session_state.rag_engine = initialize_rag(all_paper_texts)
|
| 1011 |
+
status.update(label=tr(lang_code, "literature_done"), state="complete", expanded=False)
|
| 1012 |
+
|
| 1013 |
+
update_heartbeat()
|
| 1014 |
+
raw_interviews = extract_text_from_files([uploaded_interviews]) if uploaded_interviews else ""
|
| 1015 |
+
user_data_md, msg, data_metadata = qual_engine.resolve_qualitative_data(topic, keywords, raw_interviews)
|
| 1016 |
+
st.info(msg or tr(lang_code, "qualitative_ready"))
|
| 1017 |
+
|
| 1018 |
+
state = {
|
| 1019 |
+
"topic": topic, "keywords": keywords, "target_lang": target_lang, "words": words,
|
| 1020 |
+
"user_data_md": user_data_md, "data_metadata": data_metadata, "user_lit_context": user_lit_context,
|
| 1021 |
+
"completed_sections": [], "full_draft_en": "", "full_draft_translated": "",
|
| 1022 |
+
"context_tail": "", "run_mode": st.session_state.run_mode, "interface_language": lang_code,
|
| 1023 |
+
"managed_task_idempotency_key": managed_task_key,
|
| 1024 |
+
"managed_task_id": managed_task.get("task_id", ""),
|
| 1025 |
+
}
|
| 1026 |
+
|
| 1027 |
+
pb.progress(5, text=tr(lang_code, "config_route"))
|
| 1028 |
+
|
| 1029 |
+
if st.session_state.run_mode == "amj":
|
| 1030 |
+
APP_CONFIG.update(AMJ_PROMPTS)
|
| 1031 |
+
state["prompt_config"] = AMJ_PROMPTS
|
| 1032 |
+
else:
|
| 1033 |
+
APP_CONFIG.update(ASQ_PROMPTS)
|
| 1034 |
+
state["prompt_config"] = ASQ_PROMPTS
|
| 1035 |
+
|
| 1036 |
+
pb.progress(10, text=tr(lang_code, "literature_search"))
|
| 1037 |
+
def sync_monitor_ui():
|
| 1038 |
+
update_heartbeat()
|
| 1039 |
+
render_monitor(agent, tr(lang_code, "search_phase"), "S2 + OpenAlex", tr(lang_code, "search_detail"))
|
| 1040 |
+
|
| 1041 |
+
target_refs_count = 50
|
| 1042 |
+
refs_text = agent.fetch_references_parallel(topic, keywords, min_required=target_refs_count, progress_callback=sync_monitor_ui)
|
| 1043 |
+
|
| 1044 |
+
if len(agent.citation_map) < target_refs_count:
|
| 1045 |
+
raise RuntimeError(
|
| 1046 |
+
tr(lang_code, "refs_insufficient", count=len(agent.citation_map), required=target_refs_count)
|
| 1047 |
+
)
|
| 1048 |
+
else:
|
| 1049 |
+
st.success(tr(lang_code, "refs_success", count=len(agent.citation_map)))
|
| 1050 |
+
|
| 1051 |
+
with st.spinner(tr(lang_code, "rag_sync")):
|
| 1052 |
+
update_heartbeat()
|
| 1053 |
+
if hasattr(st.session_state, "rag_engine") and st.session_state.rag_engine is not None:
|
| 1054 |
+
st.session_state.rag_engine._initialize_chunks([refs_text])
|
| 1055 |
+
else:
|
| 1056 |
+
st.session_state.rag_engine = initialize_rag([refs_text])
|
| 1057 |
+
st.success(tr(lang_code, "rag_done"))
|
| 1058 |
+
|
| 1059 |
+
if user_lit_context:
|
| 1060 |
+
pb.progress(11, text=tr(lang_code, "core_literature_parsing"))
|
| 1061 |
+
update_heartbeat()
|
| 1062 |
+
core_refs = agent.parse_user_core_literature(user_lit_context)
|
| 1063 |
+
if core_refs:
|
| 1064 |
+
refs_text = "【USER PROVIDED CORE LITERATURE】\n" + core_refs + "\n\n【RETRIEVED LITERATURE】\n" + refs_text
|
| 1065 |
+
st.success(tr(lang_code, "core_refs_done"))
|
| 1066 |
+
|
| 1067 |
+
state["refs_text"] = refs_text
|
| 1068 |
+
|
| 1069 |
+
pb.progress(12, text=tr(lang_code, "synthesis_progress"))
|
| 1070 |
+
with st.status(tr(lang_code, "synthesis_status"), expanded=True) as status:
|
| 1071 |
+
ph = st.empty()
|
| 1072 |
+
synthesis_result = ph.write_stream(stream_with_heartbeat(agent.stream_synthesize_literature(topic, refs_text, user_core_literature=user_lit_context)))
|
| 1073 |
+
synthesis_result = re.sub(r'<think>.*?</think>', '', synthesis_result, flags=re.DOTALL).strip()
|
| 1074 |
+
status.update(label=tr(lang_code, "synthesis_done"), state="complete", expanded=False)
|
| 1075 |
+
state["synthesis_result"] = synthesis_result
|
| 1076 |
+
|
| 1077 |
+
pb.progress(14, text=tr(lang_code, "anchor_progress"))
|
| 1078 |
+
with st.status(tr(lang_code, "anchor_status"), expanded=True) as status:
|
| 1079 |
+
ph = st.empty()
|
| 1080 |
+
anchor_response = ph.write_stream(stream_with_heartbeat(agent.stream_generate_anchors(topic, keywords, synthesis_result, refs_text, data_evidence=user_data_md, mode=st.session_state.run_mode)))
|
| 1081 |
+
status.update(label=tr(lang_code, "anchor_done"), state="complete", expanded=False)
|
| 1082 |
+
|
| 1083 |
+
# ==========================================================
|
| 1084 |
+
# [核心修复] 必须先剔除 <think> 标签!防止正则匹配到模型在思考时引用提示词的空占位符
|
| 1085 |
+
# ==========================================================
|
| 1086 |
+
anchor_response_clean = re.sub(r'<think>.*?</think>', '', anchor_response, flags=re.DOTALL).strip()
|
| 1087 |
+
|
| 1088 |
+
abstract_match = re.search(r'-+\s*ABSTRACT\s*-+\s*(.*?)(?=\s*-+\s*CONCLUSION\s*-+|$)', anchor_response_clean, re.DOTALL | re.IGNORECASE)
|
| 1089 |
+
conclusion_match = re.search(r'-+\s*CONCLUSION\s*-+\s*(.*?)$', anchor_response_clean, re.DOTALL | re.IGNORECASE)
|
| 1090 |
+
abstract_anchor = abstract_match.group(1).strip() if abstract_match else anchor_response_clean.strip()
|
| 1091 |
+
conclusion_anchor = conclusion_match.group(1).strip() if conclusion_match else ""
|
| 1092 |
+
agent.global_memory["abstract_anchor"] = abstract_anchor
|
| 1093 |
+
agent.global_memory["conclusion_anchor"] = conclusion_anchor
|
| 1094 |
+
# ==========================================================
|
| 1095 |
+
|
| 1096 |
+
pb.progress(15, text=tr(lang_code, "outline_progress"))
|
| 1097 |
+
update_heartbeat()
|
| 1098 |
+
plan = agent.generate_outline(topic, keywords, mode=st.session_state.run_mode, data_metadata=data_metadata)
|
| 1099 |
+
en_topic = str(plan.get("en_topic", topic))
|
| 1100 |
+
structure = plan.get("structure", [])
|
| 1101 |
+
|
| 1102 |
+
en_abstract_clean = clean_markdown_headings(re.sub(r'(?i)^abstract[:\s]*', '', str(abstract_anchor)).strip())
|
| 1103 |
+
trans_abstract_clean = ""
|
| 1104 |
+
|
| 1105 |
+
loc_abs = {"Simplified Chinese (简体中文)": "摘要", "Russian (Русский)": "Аннотация", "Kazakh (Қазақша)": "Түйіндеме"}.get(target_lang, "Abstract")
|
| 1106 |
+
loc_kw = {"Simplified Chinese (简体中文)": "关键词", "Russian (Русский)": "Ключевые слова", "Kazakh (Қазақша)": "Кілт сөздер"}.get(target_lang, "Keywords")
|
| 1107 |
+
|
| 1108 |
+
full_draft_en = f"# {en_topic}\n\n## Abstract\n{en_abstract_clean}\n\n## Keywords\n{keywords}\n\n"
|
| 1109 |
+
full_draft_translated = f"# {topic}\n\n## {loc_abs}\n{trans_abstract_clean}\n\n## {loc_kw}\n{keywords}\n\n"
|
| 1110 |
+
|
| 1111 |
+
adaptive_structure = agent.build_generation_tasks(structure, int(words), SAFE_CHUNK_WORDS)
|
| 1112 |
+
for task in adaptive_structure:
|
| 1113 |
+
task["is_first"] = bool(task.get("section_first"))
|
| 1114 |
+
|
| 1115 |
+
with st.spinner(tr(lang_code, "jit_status")):
|
| 1116 |
+
update_heartbeat()
|
| 1117 |
+
jit_texts = agent.prefetch_jit_literature(topic, keywords, adaptive_structure)
|
| 1118 |
+
if jit_texts:
|
| 1119 |
+
if hasattr(st.session_state, "rag_engine") and st.session_state.rag_engine is not None:
|
| 1120 |
+
st.session_state.rag_engine._initialize_chunks([jit_texts])
|
| 1121 |
+
else:
|
| 1122 |
+
st.session_state.rag_engine = initialize_rag([jit_texts])
|
| 1123 |
+
st.success(tr(lang_code, "jit_done"))
|
| 1124 |
+
|
| 1125 |
+
state["adaptive_structure"] = adaptive_structure
|
| 1126 |
+
state["agent_state"] = agent.export_state()
|
| 1127 |
+
state["full_draft_en"] = full_draft_en
|
| 1128 |
+
state["full_draft_translated"] = full_draft_translated
|
| 1129 |
+
completed_sections = []
|
| 1130 |
+
context_tail = ""
|
| 1131 |
+
|
| 1132 |
+
os.makedirs(WORKSPACE_DIR, exist_ok=True)
|
| 1133 |
+
with open(CHECKPOINT_FILE, "w", encoding="utf-8") as f:
|
| 1134 |
+
json.dump(state, f, ensure_ascii=False)
|
| 1135 |
+
|
| 1136 |
+
total_steps = len(adaptive_structure)
|
| 1137 |
+
current_step = len(completed_sections)
|
| 1138 |
+
st.markdown("---")
|
| 1139 |
+
ui_blocks = []
|
| 1140 |
+
target_flag = FLAG_MAP.get(target_lang, "🌐")
|
| 1141 |
+
|
| 1142 |
+
for sec in adaptive_structure:
|
| 1143 |
+
display_name = sec["display_name"]
|
| 1144 |
+
st.markdown(f"### ⚙️ {display_name}")
|
| 1145 |
+
col_en, col_trans = st.columns(2)
|
| 1146 |
+
with col_en:
|
| 1147 |
+
st.markdown(tr(lang_code, "english_draft"))
|
| 1148 |
+
en_ph = st.empty()
|
| 1149 |
+
with col_trans:
|
| 1150 |
+
st.markdown(tr(lang_code, "target_draft", flag=target_flag))
|
| 1151 |
+
trans_ph = st.empty()
|
| 1152 |
+
if display_name in completed_sections:
|
| 1153 |
+
en_ph.success(tr(lang_code, "restored"))
|
| 1154 |
+
trans_ph.info(tr(lang_code, "translation_waiting"))
|
| 1155 |
+
else:
|
| 1156 |
+
en_ph.info(tr(lang_code, "waiting"))
|
| 1157 |
+
trans_ph.info(tr(lang_code, "translation_waiting"))
|
| 1158 |
+
ui_blocks.append({"sec": sec, "en_ph": en_ph, "trans_ph": trans_ph})
|
| 1159 |
+
|
| 1160 |
+
generation_aborted = False
|
| 1161 |
+
generation_failure_code = ""
|
| 1162 |
+
generation_failure_detail = ""
|
| 1163 |
+
accumulated_deviation = 0
|
| 1164 |
+
remaining_sections = sum(1 for item in adaptive_structure if item["display_name"] not in completed_sections)
|
| 1165 |
+
all_cite_keys = list(agent.citation_map.keys())
|
| 1166 |
+
valid_chunks = [item for item in adaptive_structure if not any(word in item["section"].lower() for word in ["abstract","keyword","appendix"])]
|
| 1167 |
+
allocation_map = {}
|
| 1168 |
+
current_cite_idx = 0
|
| 1169 |
+
for idx, item in enumerate(valid_chunks):
|
| 1170 |
+
count = max(1, math.ceil(len(all_cite_keys) / max(1, len(valid_chunks))))
|
| 1171 |
+
allocation_map[item["display_name"]] = all_cite_keys[current_cite_idx:] if idx == len(valid_chunks)-1 else all_cite_keys[current_cite_idx:current_cite_idx+count]
|
| 1172 |
+
current_cite_idx += count
|
| 1173 |
+
|
| 1174 |
+
for block in ui_blocks:
|
| 1175 |
+
if generation_aborted:
|
| 1176 |
+
break
|
| 1177 |
+
sec = block["sec"]
|
| 1178 |
+
en_ph = block["en_ph"]
|
| 1179 |
+
trans_ph = block["trans_ph"]
|
| 1180 |
+
sec_title = sec["section"]
|
| 1181 |
+
display_name = sec["display_name"]
|
| 1182 |
+
if display_name in completed_sections:
|
| 1183 |
+
continue
|
| 1184 |
+
remaining_sections = max(0, remaining_sections - 1)
|
| 1185 |
+
adjustment = -accumulated_deviation / remaining_sections if remaining_sections else 0
|
| 1186 |
+
adjusted_target = min(int(SAFE_CHUNK_WORDS * 1.5), max(80, int(sec["target_words"] + adjustment)))
|
| 1187 |
+
is_quarantined = any(word in sec_title.lower() for word in ["abstract","keyword","conclusion","references","appendix"])
|
| 1188 |
+
assigned_tags = ", ".join(allocation_map.get(display_name, [])) or "the single most relevant source"
|
| 1189 |
+
citation_rule = "NO IN-TEXT CITATIONS ALLOWED IN THIS SECTION." if is_quarantined else f"Use claim-specific citation. Each sentence may contain at most one source tag. Candidate tags: {assigned_tags}. <AVAILABLE_CITATIONS>{refs_text}</AVAILABLE_CITATIONS>"
|
| 1190 |
+
memory_context = agent._build_memory_context(sec_title, 2500)
|
| 1191 |
+
memory_block = f"\n<GLOBAL_MEMORY>\n{memory_context}\n</GLOBAL_MEMORY>\n" if memory_context else ""
|
| 1192 |
+
if getattr(st.session_state, "rag_engine", None) is not None:
|
| 1193 |
+
snippets = st.session_state.rag_engine.get_relevant_context(display_name)
|
| 1194 |
+
if snippets:
|
| 1195 |
+
memory_block += f"\n<TARGETED_RAG>\n{snippets}\n</TARGETED_RAG>\n"
|
| 1196 |
+
subsection = sec.get("subsection")
|
| 1197 |
+
heading_directive = "Do not output the section heading; it will be inserted by the system."
|
| 1198 |
+
if subsection:
|
| 1199 |
+
heading_directive += f" Do not output the subsection heading '{subsection}'; it will also be inserted by the system. Use level-3 headings only when analytically necessary."
|
| 1200 |
+
elif sec_title.lower() in {"introduction", "conclusion"}:
|
| 1201 |
+
heading_directive += " Do not use additional headings."
|
| 1202 |
+
else:
|
| 1203 |
+
heading_directive += " Use level-2 or level-3 headings only when they clarify the argument; never use level-4 headings or numbered headings."
|
| 1204 |
+
table_rule = "Present statistical evidence in standard Markdown tables with plain-text captions." if sec["is_method_or_result"] and st.session_state.run_mode == "amj" else ""
|
| 1205 |
+
required_content = str(sec.get("required_content") or "")
|
| 1206 |
+
concepts = ", ".join(sec.get("concepts") or [])
|
| 1207 |
+
qualitative_rule = f"Assigned qualitative concepts: {concepts}. Use only their assigned representative quotations and do not repeat a quotation already used elsewhere." if concepts else ""
|
| 1208 |
+
data_injection = f"\n<RESEARCH_EVIDENCE>\n{user_data_md}\n</RESEARCH_EVIDENCE>\n" if user_data_md and sec["is_method_or_result"] else ""
|
| 1209 |
+
completed_label = ", ".join(completed_sections)
|
| 1210 |
+
anti_repeat = f"Already completed: {completed_label}. Do not restate their background, gap, or contribution." if completed_label else ""
|
| 1211 |
+
sys_prompt = APP_CONFIG.get("sys_academic_writer", "") + "\n" + APP_CONFIG.get("deep_refinement_directive", "")
|
| 1212 |
+
user_prompt = f"""Write the assigned English manuscript unit.
|
| 1213 |
+
Section: {sec_title}
|
| 1214 |
+
Subsection: {subsection or 'None'}
|
| 1215 |
+
Target length: {int(adjusted_target*0.9)}-{int(adjusted_target*1.1)} words.
|
| 1216 |
+
{heading_directive}
|
| 1217 |
+
{citation_rule}
|
| 1218 |
+
{table_rule}
|
| 1219 |
+
{required_content}
|
| 1220 |
+
{qualitative_rule}
|
| 1221 |
+
{anti_repeat}
|
| 1222 |
+
{memory_block}
|
| 1223 |
+
Literature synthesis: {synthesis_result}
|
| 1224 |
+
{data_injection}
|
| 1225 |
+
Previous tail: {context_tail[-2500:]}
|
| 1226 |
+
Output only final academic prose. Do not report the word count."""
|
| 1227 |
+
if agent._count_tokens(sys_prompt) + agent._count_tokens(user_prompt) > 60000:
|
| 1228 |
+
user_prompt = agent._truncate_context(user_prompt, 60000 - agent._count_tokens(sys_prompt))
|
| 1229 |
+
render_monitor(agent, tr(lang_code, "drafting_phase", current=current_step+1, total=total_steps), display_name, tr(lang_code, "target_words", words=adjusted_target, engine=tr(lang_code, "engine_amj") if st.session_state.run_mode=="amj" else tr(lang_code, "engine_asq")))
|
| 1230 |
+
try:
|
| 1231 |
+
with st.status(tr(lang_code, "draft_status", name=display_name), expanded=True) as status:
|
| 1232 |
+
update_heartbeat()
|
| 1233 |
+
en_raw = en_ph.write_stream(stream_with_heartbeat(agent.stream_deepseek(sys_prompt, user_prompt, max_tokens=7000, model_role="writing", thinking=False, allow_fallback=False)))
|
| 1234 |
+
status.update(label=tr(lang_code, "draft_done", name=display_name), state="complete", expanded=False)
|
| 1235 |
+
en_raw = re.sub(r'<think>.*?</think>', '', str(en_raw), flags=re.S).strip()
|
| 1236 |
+
en_raw = re.sub(rf'(?im)^#\s*{re.escape(sec_title)}\s*\n+', '', en_raw).strip()
|
| 1237 |
+
if subsection:
|
| 1238 |
+
en_raw = re.sub(rf'(?im)^##\s*{re.escape(subsection)}\s*\n+', '', en_raw).strip()
|
| 1239 |
+
en_raw = re.sub(r'(?m)^(#{2,3})\s+[\d\.\s一二三四五六七八九十、]+\s*(.*)', r'\1 \2', en_raw)
|
| 1240 |
+
en_raw = re.sub(r'(?m)^#{4,}\s+(.*)', r'**\1**', en_raw)
|
| 1241 |
+
en_raw = re.sub(r'(?m)^#\s+(.+)$', r'## \1', en_raw)
|
| 1242 |
+
if st.session_state.run_mode == "asq" and user_data_md and "appendix" not in sec_title.lower():
|
| 1243 |
+
quote_dict = {tag: quote for quote, tag in re.findall(r'"([^"]+)"\s*\[(QUOTE-\d+)\]', user_data_md)}
|
| 1244 |
+
for tag, quote in quote_dict.items():
|
| 1245 |
+
en_raw = en_raw.replace(tag, f'"{quote}"')
|
| 1246 |
+
en_raw = agent.polish_academic_section(en_raw, display_name, abstract=False)
|
| 1247 |
+
prefix = ""
|
| 1248 |
+
if sec.get("section_first"):
|
| 1249 |
+
prefix += f"# {sec_title}\n\n"
|
| 1250 |
+
if sec.get("subsection_first") and subsection:
|
| 1251 |
+
prefix += f"## {subsection}\n\n"
|
| 1252 |
+
en_with_headings = prefix + en_raw
|
| 1253 |
+
en_resolved = agent.audit_and_resolve_citations(en_with_headings, section_name=display_name, quarantine=is_quarantined)
|
| 1254 |
+
en_ph.markdown(en_resolved)
|
| 1255 |
+
trans_ph.info(tr(lang_code, "translation_waiting"))
|
| 1256 |
+
full_draft_en += en_resolved + "\n\n"
|
| 1257 |
+
agent._extract_memory_from_text(en_raw, sec_title)
|
| 1258 |
+
words_tail = en_raw.split()
|
| 1259 |
+
context_tail = " ".join(words_tail[-200:])
|
| 1260 |
+
actual_words = len(en_raw.split())
|
| 1261 |
+
accumulated_deviation += actual_words - adjusted_target
|
| 1262 |
+
current_step += 1
|
| 1263 |
+
completed_sections.append(display_name)
|
| 1264 |
+
state.update({
|
| 1265 |
+
"completed_sections": completed_sections,
|
| 1266 |
+
"full_draft_en": full_draft_en,
|
| 1267 |
+
"full_draft_translated": "",
|
| 1268 |
+
"context_tail": context_tail,
|
| 1269 |
+
"agent_state": agent.export_state(),
|
| 1270 |
+
"data_metadata": data_metadata,
|
| 1271 |
+
})
|
| 1272 |
+
with open(CHECKPOINT_FILE, "w", encoding="utf-8") as checkpoint:
|
| 1273 |
+
json.dump(state, checkpoint, ensure_ascii=False)
|
| 1274 |
+
st.caption(tr(lang_code, "word_count", actual=actual_words, target=adjusted_target, deviation=accumulated_deviation))
|
| 1275 |
+
pb.progress(min(15 + int((current_step/max(1,total_steps))*75), 90), text=tr(lang_code, "section_complete_progress", name=display_name))
|
| 1276 |
+
except Exception as error:
|
| 1277 |
+
en_ph.error(tr(lang_code, "node_error"))
|
| 1278 |
+
st.error(tr(lang_code, "node_error_detail", error=error))
|
| 1279 |
+
generation_aborted = True
|
| 1280 |
+
generation_failure_code = "E_GENERATION_NODE_FAILED"
|
| 1281 |
+
generation_failure_detail = str(error)[:800]
|
| 1282 |
+
|
| 1283 |
+
if generation_aborted:
|
| 1284 |
+
_finish_managed_generation_task(
|
| 1285 |
+
"failed",
|
| 1286 |
+
safe_error_code=generation_failure_code or "E_GENERATION_INTERRUPTED",
|
| 1287 |
+
safe_error_detail=generation_failure_detail or "Generation stopped; quota was released and the checkpoint was retained for a fresh retry.",
|
| 1288 |
+
current_node=str(st.session_state.get("sp_current_node") or "generation"),
|
| 1289 |
+
detach_checkpoint=True,
|
| 1290 |
+
)
|
| 1291 |
+
|
| 1292 |
+
if not generation_aborted and full_draft_en.strip():
|
| 1293 |
+
pb.progress(91, text=tr(lang_code, "pack_docx"))
|
| 1294 |
+
update_heartbeat()
|
| 1295 |
+
full_draft_en = clean_trailing_references(full_draft_en, ["Reference","References","Reference List"])
|
| 1296 |
+
# The final abstract is derived from the completed English manuscript and frozen evidence.
|
| 1297 |
+
final_abstract = agent.generate_final_abstract(topic, keywords, full_draft_en, data_evidence=user_data_md, mode=st.session_state.run_mode)
|
| 1298 |
+
full_draft_en = replace_abstract(full_draft_en, final_abstract)
|
| 1299 |
+
agent.validate_manuscript_structure(full_draft_en, adaptive_structure, translated=False)
|
| 1300 |
+
if st.session_state.run_mode == "asq":
|
| 1301 |
+
repeated_quotes = agent.find_repeated_qualitative_quotes(full_draft_en, data_metadata)
|
| 1302 |
+
if repeated_quotes:
|
| 1303 |
+
raise RuntimeError(f"Repeated qualitative quotations: {repeated_quotes[:5]}")
|
| 1304 |
+
# Only sources actually cited in the manuscript enter the reference list.
|
| 1305 |
+
references = []
|
| 1306 |
+
seen_refs = set()
|
| 1307 |
+
for tag in sorted(agent.used_citations, key=agent._citation_tag_number):
|
| 1308 |
+
ref = str((agent.citation_map.get(tag) or {}).get("ref") or "").strip()
|
| 1309 |
+
if ref and ref not in seen_refs:
|
| 1310 |
+
references.append(f"- {ref}")
|
| 1311 |
+
seen_refs.add(ref)
|
| 1312 |
+
if not references:
|
| 1313 |
+
raise RuntimeError("No cited references remained after citation validation.")
|
| 1314 |
+
full_draft_en = full_draft_en.rstrip() + "\n\n# References\n" + "\n\n".join(references)
|
| 1315 |
+
|
| 1316 |
+
pb.progress(94, text=tr(lang_code, "translation_running"))
|
| 1317 |
+
update_heartbeat()
|
| 1318 |
+
full_draft_translated = agent.translate_locked_manuscript(full_draft_en, target_lang)
|
| 1319 |
+
agent.validate_translation_alignment(full_draft_en, full_draft_translated)
|
| 1320 |
+
|
| 1321 |
+
bj_time = datetime.now(timezone(timedelta(hours=8))).strftime("%Y%m%d_%H%M")
|
| 1322 |
+
package = build_validated_package(
|
| 1323 |
+
english_markdown=full_draft_en,
|
| 1324 |
+
translated_markdown=full_draft_translated,
|
| 1325 |
+
english_filename=f"Research_EN_{bj_time}.docx",
|
| 1326 |
+
translated_filename=f"Research_{output_suffix}_{bj_time}.docx",
|
| 1327 |
+
data_metadata=data_metadata,
|
| 1328 |
+
mode=st.session_state.run_mode,
|
| 1329 |
+
citation_audit=agent.citation_audit_log,
|
| 1330 |
+
literature_metrics=agent.get_source_metrics(),
|
| 1331 |
+
)
|
| 1332 |
+
generated_zip_bytes = package.zip_bytes
|
| 1333 |
+
generated_zip_filename = f"SmartPaper_{bj_time}.zip"
|
| 1334 |
+
|
| 1335 |
+
# Persist the already validated package in the current user's task
|
| 1336 |
+
# directory before asking Worker/D1 to commit quota. The download is
|
| 1337 |
+
# exposed only after the browser receives completed+committed.
|
| 1338 |
+
task_id = str((st.session_state.get("sp_generation_task") or {}).get("task_id") or "").strip()
|
| 1339 |
+
if not task_id:
|
| 1340 |
+
raise RuntimeError("package_task_id_missing")
|
| 1341 |
+
pending_path = os.path.join(_task_output_dir(task_id), ".pending_" + generated_zip_filename)
|
| 1342 |
+
with open(pending_path, "wb") as output_file:
|
| 1343 |
+
output_file.write(generated_zip_bytes)
|
| 1344 |
+
st.session_state["sp_pending_package"] = {
|
| 1345 |
+
"pending_path": pending_path,
|
| 1346 |
+
"filename": generated_zip_filename,
|
| 1347 |
+
}
|
| 1348 |
+
completion_finalization_attempted = True
|
| 1349 |
+
_finish_managed_generation_task(
|
| 1350 |
+
"completed",
|
| 1351 |
+
current_node="package_validated",
|
| 1352 |
+
delete_checkpoint=True,
|
| 1353 |
+
)
|
| 1354 |
+
pb.progress(100, text=tr(lang_code, "complete"))
|
| 1355 |
+
render_monitor(agent, tr(lang_code, "complete"), "✅", tr(lang_code, "document_generated"))
|
| 1356 |
+
|
| 1357 |
+
except Exception as e:
|
| 1358 |
+
if not completion_finalization_attempted:
|
| 1359 |
+
try:
|
| 1360 |
+
_finish_managed_generation_task(
|
| 1361 |
+
"failed",
|
| 1362 |
+
safe_error_code="E_GENERATION_FAILED",
|
| 1363 |
+
safe_error_detail=str(e)[:800],
|
| 1364 |
+
current_node=str(st.session_state.get("sp_current_node") or "generation"),
|
| 1365 |
+
detach_checkpoint=True,
|
| 1366 |
+
)
|
| 1367 |
+
except Exception:
|
| 1368 |
+
pass
|
| 1369 |
+
st.error(tr(lang_code, "startup_error", error=e))
|
| 1370 |
+
finally:
|
| 1371 |
+
clear_heartbeat()
|
| 1372 |
+
st.session_state.is_running = False
|
| 1373 |
+
st.session_state.start_pipeline = False
|
| 1374 |
+
st.session_state.resume_pipeline = False
|
| 1375 |
+
|
| 1376 |
+
if st.session_state.zip_buffer:
|
| 1377 |
+
st.success(tr(lang_code, "success"))
|
| 1378 |
+
st.download_button(label=tr(lang_code, "download"), data=st.session_state.zip_buffer, file_name=st.session_state.zip_filename, mime="application/zip")
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/config.py
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
def load_config():
|
| 5 |
+
base_config = {
|
| 6 |
+
# --- 基础运行参数 ---
|
| 7 |
+
"lit_target_papers": int(os.environ.get("LIT_TARGET_PAPERS", "80")),
|
| 8 |
+
"lit_per_page": int(os.environ.get("LIT_PER_PAGE", "100")),
|
| 9 |
+
"lit_max_workers": int(os.environ.get("LIT_MAX_WORKERS", "5")),
|
| 10 |
+
"chunk_words_default": int(os.environ.get("CHUNK_WORDS_DEFAULT", "1500")),
|
| 11 |
+
"search_max_retries": max(1, int(os.environ.get("SEARCH_MAX_RETRIES", "4"))),
|
| 12 |
+
"translation_batch_tokens": max(800, int(os.environ.get("TRANSLATION_BATCH_TOKENS", "2500"))),
|
| 13 |
+
"translation_glossary_term_limit": max(10, int(os.environ.get("TRANSLATION_GLOSSARY_TERM_LIMIT", "40"))),
|
| 14 |
+
"translation_min_coverage_ratio": float(os.environ.get("TRANSLATION_MIN_COVERAGE_RATIO", "0.25")),
|
| 15 |
+
"translation_min_block_ratio_zh": float(os.environ.get("TRANSLATION_MIN_BLOCK_RATIO_ZH", "0.28")),
|
| 16 |
+
"translation_min_block_ratio_cyrillic": float(os.environ.get("TRANSLATION_MIN_BLOCK_RATIO_CYRILLIC", "0.65")),
|
| 17 |
+
"translation_max_block_ratio": float(os.environ.get("TRANSLATION_MAX_BLOCK_RATIO", "2.20")),
|
| 18 |
+
"openalex_email": os.environ.get("OPENALEX_EMAIL", "").strip(),
|
| 19 |
+
|
| 20 |
+
# --- RAG 与解析配置 ---
|
| 21 |
+
"rag_chunk_size": int(os.environ.get("RAG_CHUNK_SIZE", "3000")),
|
| 22 |
+
"rag_top_k": int(os.environ.get("RAG_TOP_K", "3")),
|
| 23 |
+
"max_extract_chars": int(os.environ.get("MAX_EXTRACT_CHARS", "150000")),
|
| 24 |
+
"api_timeouts": {
|
| 25 |
+
"standard": (15, 180),
|
| 26 |
+
"stream": (15, 300),
|
| 27 |
+
"search": 15
|
| 28 |
+
},
|
| 29 |
+
|
| 30 |
+
# --- API 节点配置 ---
|
| 31 |
+
"deepseek_api_url": os.environ.get(
|
| 32 |
+
"DEEPSEEK_API_URL",
|
| 33 |
+
"https://api.deepseek.com/chat/completions",
|
| 34 |
+
),
|
| 35 |
+
# Model names are centralized here and can be upgraded in HF Variables.
|
| 36 |
+
# Application and engine code select a task role, never a concrete model.
|
| 37 |
+
"deepseek_models": {
|
| 38 |
+
"reasoning": os.environ.get(
|
| 39 |
+
"DEEPSEEK_MODEL_REASONING",
|
| 40 |
+
"deepseek-v4-pro",
|
| 41 |
+
),
|
| 42 |
+
"writing": os.environ.get(
|
| 43 |
+
"DEEPSEEK_MODEL_WRITING",
|
| 44 |
+
"deepseek-v4-pro",
|
| 45 |
+
),
|
| 46 |
+
"translation": os.environ.get(
|
| 47 |
+
"DEEPSEEK_MODEL_TRANSLATION",
|
| 48 |
+
"deepseek-v4-pro",
|
| 49 |
+
),
|
| 50 |
+
"fast": os.environ.get(
|
| 51 |
+
"DEEPSEEK_MODEL_FAST",
|
| 52 |
+
"deepseek-v4-flash",
|
| 53 |
+
),
|
| 54 |
+
"fallback": os.environ.get(
|
| 55 |
+
"DEEPSEEK_MODEL_FALLBACK",
|
| 56 |
+
"deepseek-v4-flash",
|
| 57 |
+
),
|
| 58 |
+
},
|
| 59 |
+
"deepseek_task_settings": {
|
| 60 |
+
"reasoning": {
|
| 61 |
+
"thinking": True,
|
| 62 |
+
"reasoning_effort": os.environ.get(
|
| 63 |
+
"DEEPSEEK_REASONING_EFFORT",
|
| 64 |
+
"high",
|
| 65 |
+
),
|
| 66 |
+
"allow_fallback": False,
|
| 67 |
+
},
|
| 68 |
+
"writing": {
|
| 69 |
+
"thinking": False,
|
| 70 |
+
"reasoning_effort": "high",
|
| 71 |
+
"allow_fallback": False,
|
| 72 |
+
},
|
| 73 |
+
"translation": {
|
| 74 |
+
"thinking": False,
|
| 75 |
+
"reasoning_effort": "high",
|
| 76 |
+
"allow_fallback": False,
|
| 77 |
+
},
|
| 78 |
+
"fast": {
|
| 79 |
+
"thinking": False,
|
| 80 |
+
"reasoning_effort": "low",
|
| 81 |
+
"allow_fallback": True,
|
| 82 |
+
},
|
| 83 |
+
},
|
| 84 |
+
"deepseek_max_retries": int(
|
| 85 |
+
os.environ.get("DEEPSEEK_MAX_RETRIES", "3")
|
| 86 |
+
),
|
| 87 |
+
"deepseek_show_reasoning": os.environ.get(
|
| 88 |
+
"DEEPSEEK_SHOW_REASONING",
|
| 89 |
+
"false",
|
| 90 |
+
).strip().lower() in {"1", "true", "yes", "on"},
|
| 91 |
+
# Citation-semantic audit repairs only affected sentences. Technical
|
| 92 |
+
# failures fall back to deterministic single-source recovery and are
|
| 93 |
+
# recorded in the audit log instead of terminating the whole paper.
|
| 94 |
+
"citation_semantic_audit": {
|
| 95 |
+
"enabled": os.environ.get(
|
| 96 |
+
"CITATION_SEMANTIC_AUDIT_ENABLED",
|
| 97 |
+
"true",
|
| 98 |
+
).strip().lower() in {"1", "true", "yes", "on"},
|
| 99 |
+
"batch_size": max(
|
| 100 |
+
1,
|
| 101 |
+
int(os.environ.get("CITATION_SEMANTIC_BATCH_SIZE", "5")),
|
| 102 |
+
),
|
| 103 |
+
"candidate_limit": max(
|
| 104 |
+
3,
|
| 105 |
+
int(os.environ.get("CITATION_SEMANTIC_CANDIDATE_LIMIT", "10")),
|
| 106 |
+
),
|
| 107 |
+
"fail_closed": os.environ.get(
|
| 108 |
+
"CITATION_SEMANTIC_FAIL_CLOSED",
|
| 109 |
+
"false",
|
| 110 |
+
).strip().lower() in {"1", "true", "yes", "on"},
|
| 111 |
+
"technical_retries": max(
|
| 112 |
+
1,
|
| 113 |
+
int(os.environ.get("CITATION_SEMANTIC_TECHNICAL_RETRIES", "2")),
|
| 114 |
+
),
|
| 115 |
+
"json_repair_retries": max(
|
| 116 |
+
0,
|
| 117 |
+
int(os.environ.get("CITATION_SEMANTIC_JSON_REPAIR_RETRIES", "1")),
|
| 118 |
+
),
|
| 119 |
+
},
|
| 120 |
+
# Task/lease settings are consumed by the HF client. The Worker remains
|
| 121 |
+
# the authoritative source for task ownership and lock expiry.
|
| 122 |
+
"task_heartbeat_interval_seconds": max(
|
| 123 |
+
3,
|
| 124 |
+
int(os.environ.get("SMARTPAPER_TASK_HEARTBEAT_SECONDS", "5")),
|
| 125 |
+
),
|
| 126 |
+
"manuscript_quality": {
|
| 127 |
+
"abstract_max_words": int(os.environ.get("ABSTRACT_MAX_WORDS", "200")),
|
| 128 |
+
"abstract_max_numeric_mentions": int(os.environ.get("ABSTRACT_MAX_NUMERIC_MENTIONS", "6")),
|
| 129 |
+
"forbidden_phrases": [
|
| 130 |
+
"delve into", "intricate", "paramount", "overarching",
|
| 131 |
+
"pivotal", "foster", "seamlessly", "multifaceted",
|
| 132 |
+
"beacon", "landscape", "underscore", "robust",
|
| 133 |
+
"firstly", "secondly", "moreover", "furthermore",
|
| 134 |
+
"in conclusion", "to summarize", "as an ai",
|
| 135 |
+
"ai-generated", "this response",
|
| 136 |
+
],
|
| 137 |
+
},
|
| 138 |
+
"openalex_api_url": os.environ.get("OPENALEX_API_URL", "https://api.openalex.org/works"),
|
| 139 |
+
"s2_api_url": os.environ.get("S2_API_URL", "https://api.semanticscholar.org/graph/v1"),
|
| 140 |
+
|
| 141 |
+
# --- UI 多语言标签 ---
|
| 142 |
+
"ui_labels": {
|
| 143 |
+
"中文": {"title": "📊 工商管理论文智能体", "btn_generate": "🚀 生成初稿", "btn_download": "📥 下载文件", "topic_label": "📝 论文名称", "keywords_label": "🔍 关键字", "words_label": "📏 目标总单词数 (12000-25000单词)", "success": "🎉 流水线推演完毕!双语文档已就绪。"},
|
| 144 |
+
"English": {"title": "📊 Business Administration Article Agent", "btn_generate": "🚀 Generate Draft", "btn_download": "📥 Download", "topic_label": "📝 Article Title", "keywords_label": "🔍 Keywords", "words_label": "📏 Target Word Count", "success": "🎉 Task complete!"},
|
| 145 |
+
"Русский": {"title": "📊 Агент статей по бизнес-администрированию", "btn_generate": "🚀 Черновик", "btn_download": "📥 Скачать", "topic_label": "📝 Тема", "keywords_label": "🔍 Ключевые слова", "words_label": "📏 Количество слов", "success": "🎉 Задача выполнена!"},
|
| 146 |
+
"Қазақша": {"title": "📊 Бизнес-басқару жөніндегі ғылыми мақала агенті", "btn_generate": "🚀 Жоба дайындау", "btn_download": "📥 Жүктеу", "topic_label": "📝 Мақала атауы", "keywords_label": "🔍 Түйін сөздер", "words_label": "📏 Мақсатты сөз саны", "success": "🎉 Тапсырма аяқталды!"}
|
| 147 |
+
},
|
| 148 |
+
"target_lang_map": {
|
| 149 |
+
"中文": "Simplified Chinese (简体中文)",
|
| 150 |
+
"English": "Simplified Chinese (简体中文)",
|
| 151 |
+
"Русский": "Russian (Русский)",
|
| 152 |
+
"Қазақша": "Kazakh (Қазақша)"
|
| 153 |
+
},
|
| 154 |
+
|
| 155 |
+
"workspace_dir": ".workspace",
|
| 156 |
+
|
| 157 |
+
"docx_styles": {
|
| 158 |
+
"font_name": "Times New Roman",
|
| 159 |
+
"font_size_pt": 12,
|
| 160 |
+
"code_font": "Courier New",
|
| 161 |
+
"code_size_pt": 11
|
| 162 |
+
},
|
| 163 |
+
|
| 164 |
+
"blacklists": {
|
| 165 |
+
"meta_sections": [
|
| 166 |
+
"abstract", "keyword", "keywords", "摘要", "关键词", "关键字",
|
| 167 |
+
"reference", "references", "reference list", "参考文献",
|
| 168 |
+
"acknowledgment", "acknowledgements", "致谢",
|
| 169 |
+
"аннотация", "ключевые слова", "түйіндеме", "кілт сөздер"
|
| 170 |
+
]
|
| 171 |
+
},
|
| 172 |
+
|
| 173 |
+
# =====================================================================
|
| 174 |
+
# Canonical manuscript structures. Top-level titles are deterministic:
|
| 175 |
+
# the model may write content, but it may not rename, remove, or reorder
|
| 176 |
+
# required sections.
|
| 177 |
+
# =====================================================================
|
| 178 |
+
"amj_schema": [
|
| 179 |
+
{
|
| 180 |
+
"section_id": "introduction",
|
| 181 |
+
"section": "Introduction",
|
| 182 |
+
"weight": 0.10,
|
| 183 |
+
"subsections": [],
|
| 184 |
+
},
|
| 185 |
+
{
|
| 186 |
+
"section_id": "theory",
|
| 187 |
+
"section": "Theoretical Background and Hypotheses",
|
| 188 |
+
"weight": 0.27,
|
| 189 |
+
"subsections": [
|
| 190 |
+
{"subsection_id": "conceptual_background", "title": "Conceptual Background and Research Gap", "share": 0.23},
|
| 191 |
+
{"subsection_id": "theoretical_mechanisms", "title": "Theoretical Mechanisms", "share": 0.27},
|
| 192 |
+
{"subsection_id": "hypothesis_development", "title": "Hypothesis Development", "share": 0.35},
|
| 193 |
+
{"subsection_id": "integrated_model", "title": "Integrated Research Model", "share": 0.15},
|
| 194 |
+
],
|
| 195 |
+
},
|
| 196 |
+
{
|
| 197 |
+
"section_id": "methodology",
|
| 198 |
+
"section": "Methodology",
|
| 199 |
+
"weight": 0.20,
|
| 200 |
+
"subsections": [
|
| 201 |
+
{"subsection_id": "research_design", "title": "Research Design and Context", "share": 0.18},
|
| 202 |
+
{"subsection_id": "sample_data", "title": "Sample and Data Collection", "share": 0.20},
|
| 203 |
+
{"subsection_id": "measures", "title": "Measures and Construct Operationalization", "share": 0.25},
|
| 204 |
+
{"subsection_id": "analysis_strategy", "title": "Analytical Strategy", "share": 0.22},
|
| 205 |
+
{"subsection_id": "validity_bias", "title": "Validity, Bias, and Robustness Procedures", "share": 0.15},
|
| 206 |
+
],
|
| 207 |
+
},
|
| 208 |
+
{
|
| 209 |
+
"section_id": "results",
|
| 210 |
+
"section": "Results",
|
| 211 |
+
"weight": 0.23,
|
| 212 |
+
"subsections": [
|
| 213 |
+
{"subsection_id": "measurement_results", "title": "Descriptive Statistics and Measurement Assessment", "share": 0.34},
|
| 214 |
+
{"subsection_id": "hypothesis_results", "title": "Hypothesis and Association Tests", "share": 0.46},
|
| 215 |
+
{"subsection_id": "additional_results", "title": "Additional Analyses and Robustness Checks", "share": 0.20},
|
| 216 |
+
],
|
| 217 |
+
},
|
| 218 |
+
{
|
| 219 |
+
"section_id": "discussion",
|
| 220 |
+
"section": "Discussion",
|
| 221 |
+
"weight": 0.15,
|
| 222 |
+
"subsections": [
|
| 223 |
+
{"subsection_id": "interpretation", "title": "Interpretation of the Findings", "share": 0.30},
|
| 224 |
+
{"subsection_id": "theoretical_contributions", "title": "Theoretical Contributions", "share": 0.30},
|
| 225 |
+
{"subsection_id": "practical_implications", "title": "Practical Implications", "share": 0.20},
|
| 226 |
+
{"subsection_id": "limitations", "title": "Limitations and Future Research", "share": 0.20},
|
| 227 |
+
],
|
| 228 |
+
},
|
| 229 |
+
{
|
| 230 |
+
"section_id": "conclusion",
|
| 231 |
+
"section": "Conclusion",
|
| 232 |
+
"weight": 0.05,
|
| 233 |
+
"subsections": [],
|
| 234 |
+
},
|
| 235 |
+
],
|
| 236 |
+
"asq_schema": [
|
| 237 |
+
{
|
| 238 |
+
"section_id": "introduction",
|
| 239 |
+
"section": "Introduction",
|
| 240 |
+
"weight": 0.10,
|
| 241 |
+
"subsections": [],
|
| 242 |
+
},
|
| 243 |
+
{
|
| 244 |
+
"section_id": "theoretical_framing",
|
| 245 |
+
"section": "Theoretical Framing",
|
| 246 |
+
"weight": 0.18,
|
| 247 |
+
"subsections": [
|
| 248 |
+
{"subsection_id": "phenomenon_puzzle", "title": "Phenomenon and Theoretical Puzzle", "share": 0.35},
|
| 249 |
+
{"subsection_id": "conceptual_tension", "title": "Relevant Literature and Conceptual Tension", "share": 0.40},
|
| 250 |
+
{"subsection_id": "research_question", "title": "Research Question and Analytical Focus", "share": 0.25},
|
| 251 |
+
],
|
| 252 |
+
},
|
| 253 |
+
{
|
| 254 |
+
"section_id": "methods",
|
| 255 |
+
"section": "Research Setting and Methods",
|
| 256 |
+
"weight": 0.17,
|
| 257 |
+
"subsections": [
|
| 258 |
+
{"subsection_id": "research_setting", "title": "Research Setting", "share": 0.24},
|
| 259 |
+
{"subsection_id": "data_collection", "title": "Data Sources and Collection", "share": 0.28},
|
| 260 |
+
{"subsection_id": "analytic_procedure", "title": "Analytical Procedure", "share": 0.30},
|
| 261 |
+
{"subsection_id": "trustworthiness", "title": "Trustworthiness, Ethics, and Reflexivity", "share": 0.18},
|
| 262 |
+
],
|
| 263 |
+
},
|
| 264 |
+
{
|
| 265 |
+
"section_id": "findings",
|
| 266 |
+
"section": "Findings",
|
| 267 |
+
"weight": 0.30,
|
| 268 |
+
"dynamic_subsections": "qualitative_themes",
|
| 269 |
+
"subsections": [
|
| 270 |
+
{"subsection_id": "emergent_model", "title": "Emergent Process Model", "share": 0.18},
|
| 271 |
+
],
|
| 272 |
+
},
|
| 273 |
+
{
|
| 274 |
+
"section_id": "discussion",
|
| 275 |
+
"section": "Discussion",
|
| 276 |
+
"weight": 0.15,
|
| 277 |
+
"subsections": [
|
| 278 |
+
{"subsection_id": "interpretation", "title": "Interpretation of the Emergent Findings", "share": 0.30},
|
| 279 |
+
{"subsection_id": "theoretical_contributions", "title": "Theoretical Contributions", "share": 0.30},
|
| 280 |
+
{"subsection_id": "boundaries", "title": "Boundary Conditions and Transferability", "share": 0.20},
|
| 281 |
+
{"subsection_id": "practical_implications", "title": "Practical Implications", "share": 0.20},
|
| 282 |
+
],
|
| 283 |
+
},
|
| 284 |
+
{
|
| 285 |
+
"section_id": "conclusion",
|
| 286 |
+
"section": "Conclusion",
|
| 287 |
+
"weight": 0.05,
|
| 288 |
+
"subsections": [],
|
| 289 |
+
},
|
| 290 |
+
{
|
| 291 |
+
"section_id": "appendix",
|
| 292 |
+
"section": "Appendix: Data Structure and Evidence",
|
| 293 |
+
"weight": 0.05,
|
| 294 |
+
"subsections": [],
|
| 295 |
+
},
|
| 296 |
+
],
|
| 297 |
+
"manuscript_validation": {
|
| 298 |
+
"minimum_total_ratio": 0.85,
|
| 299 |
+
"maximum_total_ratio": 1.15,
|
| 300 |
+
"minimum_section_words": 120,
|
| 301 |
+
"minimum_conclusion_words": 120,
|
| 302 |
+
"minimum_qualitative_themes": 3,
|
| 303 |
+
"minimum_qualitative_quotes": 8,
|
| 304 |
+
},
|
| 305 |
+
|
| 306 |
+
"prompt_templates": {
|
| 307 |
+
"meta_extract": """You are an expert academic data extractor. Read the provided text and extract the core metadata.
|
| 308 |
+
CRITICAL: You MUST output ONLY a valid JSON object. Do not include markdown formatting or explanations.
|
| 309 |
+
|
| 310 |
+
{
|
| 311 |
+
"paper_title": "<String: Exact title>",
|
| 312 |
+
"core_theory": "<String: Max 5 words, e.g., 'Social Exchange Theory'>",
|
| 313 |
+
"variables": {
|
| 314 |
+
"IV": "<String: Independent Variables>",
|
| 315 |
+
"DV": "<String: Dependent Variables>",
|
| 316 |
+
"Mediators": "<String or 'N/A'>",
|
| 317 |
+
"Moderators": "<String or 'N/A'>"
|
| 318 |
+
},
|
| 319 |
+
"methodology": {
|
| 320 |
+
"sample_size": "<Integer or 'N/A'>",
|
| 321 |
+
"context": "<String: Max 10 words, e.g., 'US Tech Industry'>",
|
| 322 |
+
"analysis_tool": "<String: e.g., 'SEM, PLS, OLS'>"
|
| 323 |
+
},
|
| 324 |
+
"key_findings": "<String: Strict ONE-SENTENCE summary emphasizing causality>",
|
| 325 |
+
"future_gaps": "<String: Explicit limitations or future directions>"
|
| 326 |
+
}""",
|
| 327 |
+
"synthesis_system": "You are a top-tier academic researcher in Business Administration.",
|
| 328 |
+
"synthesis_user": """Topic: {topic}{user_injection}
|
| 329 |
+
Literature Metadata:
|
| 330 |
+
<LIT_DATA>
|
| 331 |
+
{refs_text}
|
| 332 |
+
</LIT_DATA>
|
| 333 |
+
|
| 334 |
+
[SYNTHESIS MANDATE: THEMATIC INTEGRATION]
|
| 335 |
+
1. You MUST NOT summarize papers sequentially (e.g., "A found X. B found Y.").
|
| 336 |
+
2. Synthesize by THEME or THEORETICAL TENSION rather than paper-by-paper summaries.
|
| 337 |
+
3. Apply claim-specific citation: each sentence may contain at most one [CITE-XX] tag. Never group multiple sources in one parenthesis or sentence. If sources support different claims, use separate sentences; if they support the same claim, select the single most direct source.
|
| 338 |
+
4. Explicitly highlight theoretical conflicts or consensus.
|
| 339 |
+
5. Conclude by revealing a specific blind spot that necessitates our current research.
|
| 340 |
+
{directive}"""
|
| 341 |
+
}
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
# =====================================================================
|
| 345 |
+
# 定量引擎 (AMJ 风格) - 保持原样,极致严谨与假设推演
|
| 346 |
+
# =====================================================================
|
| 347 |
+
amj_prompts = {
|
| 348 |
+
"sys_academic_writer": """You are a distinguished, highly cited scholar writing a manuscript for the Academy of Management Journal (AMJ). Your writing must exhibit unparalleled theoretical rigor, causal depth, and academic authority.
|
| 349 |
+
[CRITICAL LANGUAGE MANDATE]: You MUST generate all responses strictly in ACADEMIC ENGLISH. Even if the user provides topic, data, or context in Chinese or another language, your final output MUST be in English.
|
| 350 |
+
[ABSOLUTE ANTI-AI-TONE MANDATES]:
|
| 351 |
+
1. CRITICAL: NEVER use these cliché AI words: delve into, intricate, paramount, overarching, pivotal, foster, seamlessly, multifaceted, beacon, landscape, underscore, robust.
|
| 352 |
+
2. CRITICAL: NEVER use robotic transition phrases (e.g., Firstly, Moreover, Furthermore, In conclusion, To summarize). Use conceptual bridges instead.
|
| 353 |
+
3. NATURAL SYNTAX: Vary sentence structure only where the argument requires it. Avoid artificial short-long sentence alternation and repetitive templates.
|
| 354 |
+
4. ACADEMIC HEDGING: Use strategic hedging (e.g., 'This suggests a tendency toward X, bounded by Y').
|
| 355 |
+
5. NO SWEEPING GENERALIZATIONS: Every sentence must carry specific theoretical weight or empirical fact.
|
| 356 |
+
6. FOCUS ON CAUSALITY: You MUST articulate the exact theoretical mechanism driving the relationship.
|
| 357 |
+
7. ACADEMIC REGISTER: Use concise, accurate, and rigorous language. Concepts must be explicit, hierarchy clear, and reasoning logically traceable.
|
| 358 |
+
8. PROHIBITED RHETORIC: Never use metaphors, analogies, rhetorical questions, literary embellishment, or promotional claims.
|
| 359 |
+
9. ABSTRACT DISCIPLINE: In abstracts, report only the minimum numerical evidence needed to convey the main finding; do not crowd the abstract with coefficients or test statistics.
|
| 360 |
+
10. STRUCTURAL DISCIPLINE: Cover only the assigned section and subsection. Do not recreate earlier sections, invent alternative headings, or omit required analytical elements.
|
| 361 |
+
11. CLAIM-SPECIFIC CITATION: Each sentence may contain at most one [CITE-XX] tag. Never group multiple references in one parenthesis or sentence. Separate distinct source-supported claims into separate sentences; when sources support the same claim, retain only the single most direct source. Every cited sentence must express a claim directly supported by the cited source evidence.
|
| 362 |
+
""",
|
| 363 |
+
"deep_refinement_directive": "Produce a final, polished section ready for top-tier submission. Ensure flawless logical flow without meta-commentary. Apply claim-specific citation: use at most one [CITE-XX] tag per sentence, never combine sources in one parenthesis, and cite only the single most direct source when multiple sources support the same claim.\n[SELF-REFLECTION]: Before drafting, critically analyze common AI clichés related to this topic and actively suppress them in your output.",
|
| 364 |
+
"deep_synthesis_directive": "Structure this synthesis to expose a critical theoretical tension: Consensus vs Paradox, Competing Perspectives, Theoretical Blind Spot.",
|
| 365 |
+
"hypothesis_development_directive": "Construct a rigorous causal chain: 1. Base Theory. 2. Mechanism (Mediator logic). 3. Boundary Condition (Moderator logic). 4. Hypothesis Phrasing (italicized).",
|
| 366 |
+
"methodological_defense_directive": "Write with extreme defensive precision: Justify Contextual Relevance, Detail Endogeneity & Bias Mitigation, Measurement Validity, and Analytical Strategy.",
|
| 367 |
+
"discussion_depth_directive": "Elevate findings back to grand theory. Explicitly state how the study extends, refines, or challenges named theories. Provide counter-intuitive implications.",
|
| 368 |
+
"conclusion_anchor_directive": "1. Conceptual Pivot. 2. Mechanism Summary. 3. Call to Action for the next decade.",
|
| 369 |
+
"anchor_generation_directive": "Generate a STRUCTURED ABSTRACT (max 200 words) mimicking an AMJ abstract, and a CONCLUSION ANCHOR (approx. 150 words). Output format:\n---ABSTRACT---\n...\n---CONCLUSION---\n...",
|
| 370 |
+
"empirical_data_logic": "Maintain strict adherence to APA 7th edition reporting standards. Include plausible degrees of freedom, exact p-values, confidence intervals, and effect sizes.",
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
# =====================================================================
|
| 374 |
+
# 定性引擎 (ASQ 风格) - 聚焦现象描述、意义建构与语录引用
|
| 375 |
+
# =====================================================================
|
| 376 |
+
asq_prompts = {
|
| 377 |
+
"sys_academic_writer": """You are a senior editor and distinguished qualitative researcher writing for Administrative Science Quarterly (ASQ).
|
| 378 |
+
Your writing style is phenomenon-driven, deeply narrative, and theoretically rich.
|
| 379 |
+
[CRITICAL LANGUAGE MANDATE]: You MUST generate all responses strictly in ACADEMIC ENGLISH.
|
| 380 |
+
[ABSOLUTE ASQ MANDATES]:
|
| 381 |
+
1. FOCUS ON THE PUZZLE: Embrace counter-intuitive empirical phenomena and paradoxes.
|
| 382 |
+
2. THICK DESCRIPTION: Use descriptive, evocative language to ground your qualitative findings in reality.
|
| 383 |
+
3. SENSEMAKING: Focus on how organizational actors interpret, negotiate, and construct their reality.
|
| 384 |
+
4. NO FORMAL HYPOTHESES: DO NOT use 'Hypothesis 1' phrasing. Use 'Propositions' or conceptual emergent models instead.
|
| 385 |
+
5. NO AI CLICHES: Strictly avoid 'delve', 'tapestry', 'multifaceted', 'beacon', 'landscape'.
|
| 386 |
+
6. ACADEMIC REGISTER: Use concise, accurate, and rigorous language with explicit concepts and logically ordered claims.
|
| 387 |
+
7. PROHIBITED RHETORIC: Never use metaphors, analogies, rhetorical questions, literary embellishment, or promotional claims. Use concrete evidence-based description rather than decorative prose.
|
| 388 |
+
8. ABSTRACT DISCIPLINE: Keep numerical details to the minimum needed to communicate the empirical puzzle, main finding, and contribution.
|
| 389 |
+
9. CLAIM-SPECIFIC CITATION: Each sentence may contain at most one [CITE-XX] tag. Never group multiple references in one parenthesis or sentence. Separate distinct source-supported claims into separate sentences; when sources support the same claim, retain only the single most direct source. Every cited sentence must express a claim directly supported by the cited source evidence.""",
|
| 390 |
+
"deep_refinement_directive": "Ensure logical flow and focus on the emergent theoretical model. Write in continuous, engaging paragraphs. Apply claim-specific citation: use at most one [CITE-XX] tag per sentence, never combine sources in one parenthesis, and cite only the single most direct source when several sources support the same claim.",
|
| 391 |
+
"deep_synthesis_directive": "Structure this synthesis to highlight a critical empirical puzzle or theoretical paradox that qualitative investigation can unravel.",
|
| 392 |
+
"hypothesis_development_directive": "Develop the theoretical framing. DO NOT use formal hypotheses. Detail the conceptual gaps and frame the overarching research questions or theoretical propositions.",
|
| 393 |
+
"methodological_defense_directive": "Detail the research setting vividly. Justify data sources (interviews, ethnographic observations, archives), and rigorously explain the step-by-step coding process (e.g., Gioia methodology).",
|
| 394 |
+
"discussion_depth_directive": "Elevate the emergent qualitative findings back to grand theory. Contrast your inductive process model with existing linear or variance-based theories.",
|
| 395 |
+
"conclusion_anchor_directive": "Summarize the theoretical contribution, the resolution of the empirical puzzle, and the sensemaking implications.",
|
| 396 |
+
"anchor_generation_directive": "Generate a NARRATIVE ABSTRACT (max 200 words) mimicking an ASQ abstract (focusing on the puzzle, setting, and emergent model), and a CONCLUSION ANCHOR (approx. 150 words). Output format:\n---ABSTRACT---\n...\n---CONCLUSION---\n...",
|
| 397 |
+
"empirical_data_logic": "Present findings as a compelling narrative. You MUST integrate representative quotes seamlessly into the text to provide raw evidence for each 1st-order concept and 2nd-order theme. Ensure the voice of the informants is central.",
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
generic_prompts = amj_prompts.copy()
|
| 401 |
+
return base_config, amj_prompts, asq_prompts, generic_prompts
|
| 402 |
+
|
| 403 |
+
DEFAULT_CONFIG, AMJ_PROMPTS, ASQ_PROMPTS, GENERIC_PROMPTS = load_config()
|
| 404 |
+
APP_CONFIG = DEFAULT_CONFIG.copy()
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/core/desktop.ini
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[.ShellClassInfo]
|
| 2 |
+
IconResource=C:\Program Files\Google\Drive File Stream\129.0.1.0\GoogleDriveFS.exe,27
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/core/meta_extractor.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
import json
|
| 3 |
+
import concurrent.futures
|
| 4 |
+
from typing import List, Dict
|
| 5 |
+
import re
|
| 6 |
+
import logging
|
| 7 |
+
from config import APP_CONFIG
|
| 8 |
+
|
| 9 |
+
class MetaExtractor:
|
| 10 |
+
def __init__(self, agent):
|
| 11 |
+
self.agent = agent
|
| 12 |
+
self.max_workers = APP_CONFIG["lit_max_workers"]
|
| 13 |
+
|
| 14 |
+
def extract_single_paper_meta(self, text: str) -> Dict:
|
| 15 |
+
snippet = self.agent._truncate_context(text, max_tokens=3500)
|
| 16 |
+
prompt = APP_CONFIG["prompt_templates"]["meta_extract"]
|
| 17 |
+
|
| 18 |
+
try:
|
| 19 |
+
# 修改点:显式将模型降级为 deepseek-chat (V3),以获得更快的并行提取速度
|
| 20 |
+
response = self.agent.call_deepseek(
|
| 21 |
+
prompt,
|
| 22 |
+
snippet,
|
| 23 |
+
max_tokens=1000,
|
| 24 |
+
model_name="deepseek-chat"
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
# 强制提取 JSON 部分,过滤模型可能的解释性前缀或后缀
|
| 28 |
+
json_match = re.search(r'\{.*\}', response, re.DOTALL)
|
| 29 |
+
if json_match:
|
| 30 |
+
return json.loads(json_match.group())
|
| 31 |
+
return {}
|
| 32 |
+
except Exception as e:
|
| 33 |
+
logging.error(f"Meta Extraction error: {e}")
|
| 34 |
+
return {}
|
| 35 |
+
|
| 36 |
+
def run_batch_extraction(self, paper_texts: List[str]) -> List[Dict]:
|
| 37 |
+
golden_db = []
|
| 38 |
+
# 使用配置中的 lit_max_workers 进行多线程并行提取
|
| 39 |
+
with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
| 40 |
+
future_to_paper = {executor.submit(self.extract_single_paper_meta, text): text for text in paper_texts}
|
| 41 |
+
for future in concurrent.futures.as_completed(future_to_paper):
|
| 42 |
+
try:
|
| 43 |
+
data = future.result()
|
| 44 |
+
if data:
|
| 45 |
+
golden_db.append(data)
|
| 46 |
+
except Exception as e:
|
| 47 |
+
logging.error(f"Batch Future error: {e}")
|
| 48 |
+
return golden_db
|
| 49 |
+
|
| 50 |
+
def extract_golden_metadata(agent, paper_texts: List[str]):
|
| 51 |
+
extractor = MetaExtractor(agent)
|
| 52 |
+
return extractor.run_batch_extraction(paper_texts)
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/core/pipeline_agent.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/core/production_guard.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""Final-output validation and packaging for SmartPaper."""
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import hashlib
|
| 6 |
+
import io
|
| 7 |
+
import json
|
| 8 |
+
import re
|
| 9 |
+
import zipfile
|
| 10 |
+
from dataclasses import dataclass
|
| 11 |
+
from typing import Any
|
| 12 |
+
|
| 13 |
+
from docx import Document
|
| 14 |
+
|
| 15 |
+
from utils.docx_builder import DocxBuildError, text_to_docx
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class OutputValidationError(RuntimeError):
|
| 19 |
+
pass
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@dataclass(frozen=True)
|
| 23 |
+
class PackageResult:
|
| 24 |
+
zip_bytes: bytes
|
| 25 |
+
file_names: tuple[str, ...]
|
| 26 |
+
manifest: str
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def replace_abstract(manuscript: str, abstract: str) -> str:
|
| 30 |
+
value = str(manuscript or "")
|
| 31 |
+
replacement = f"## Abstract\n{str(abstract or '').strip()}\n\n"
|
| 32 |
+
updated, count = re.subn(
|
| 33 |
+
r"(?ms)^##\s+Abstract\s*$\n.*?(?=^##\s+Keywords\s*$)",
|
| 34 |
+
replacement,
|
| 35 |
+
value,
|
| 36 |
+
count=1,
|
| 37 |
+
)
|
| 38 |
+
if count != 1:
|
| 39 |
+
raise OutputValidationError("English abstract block was not found exactly once.")
|
| 40 |
+
return updated
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def validate_docx_bytes(data: bytes, *, minimum_paragraphs: int = 8) -> None:
|
| 44 |
+
if not data or len(data) < 1500:
|
| 45 |
+
raise OutputValidationError("DOCX output is empty or unexpectedly small.")
|
| 46 |
+
try:
|
| 47 |
+
document = Document(io.BytesIO(data))
|
| 48 |
+
except Exception as exc:
|
| 49 |
+
raise OutputValidationError(f"DOCX cannot be reopened: {exc}") from exc
|
| 50 |
+
nonempty = [paragraph.text.strip() for paragraph in document.paragraphs if paragraph.text.strip()]
|
| 51 |
+
if len(nonempty) < minimum_paragraphs:
|
| 52 |
+
raise OutputValidationError(
|
| 53 |
+
f"DOCX contains only {len(nonempty)} nonempty paragraphs; expected at least {minimum_paragraphs}."
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def validate_markdown_manuscript(text: str, *, language_name: str) -> None:
|
| 58 |
+
value = str(text or "").strip()
|
| 59 |
+
if len(value) < 1000:
|
| 60 |
+
raise OutputValidationError(f"{language_name} manuscript is incomplete.")
|
| 61 |
+
if re.search(r"\[?CITE-\d+\]?", value, flags=re.IGNORECASE):
|
| 62 |
+
raise OutputValidationError(f"{language_name} manuscript still contains unresolved citation tags.")
|
| 63 |
+
if re.search(r"(?i)\bas an ai(?: language model)?\b|\bai[- ]generated\b", value):
|
| 64 |
+
raise OutputValidationError(f"{language_name} manuscript contains AI-writing meta-commentary.")
|
| 65 |
+
if not re.search(r"(?m)^#\s+", value):
|
| 66 |
+
raise OutputValidationError(f"{language_name} manuscript has no title/section headings.")
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def build_generation_notes(data_metadata: dict[str, Any], mode: str) -> str:
|
| 70 |
+
metadata = dict(data_metadata or {})
|
| 71 |
+
source_type = metadata.get("source_type", "unknown")
|
| 72 |
+
lines = [
|
| 73 |
+
"SmartPaper Generation Notes",
|
| 74 |
+
"",
|
| 75 |
+
f"Research mode: {mode}",
|
| 76 |
+
f"Evidence source type: {source_type}",
|
| 77 |
+
"",
|
| 78 |
+
"This file is separate from the manuscript. It documents how research evidence was supplied or generated, the applicable boundaries, and the validation steps used before packaging.",
|
| 79 |
+
]
|
| 80 |
+
if metadata.get("disclosure_required"):
|
| 81 |
+
lines.extend(
|
| 82 |
+
[
|
| 83 |
+
"",
|
| 84 |
+
"Evidence-generation notice:",
|
| 85 |
+
"The statistical results or qualitative materials used by this run were generated by the system because verified user-provided evidence was unavailable. They were created for drafting and methodological demonstration. They must not be represented as independently collected empirical observations.",
|
| 86 |
+
"",
|
| 87 |
+
f"Generation method: {metadata.get('method') or 'Structured evidence generation under the selected quantitative or qualitative engine.'}",
|
| 88 |
+
]
|
| 89 |
+
)
|
| 90 |
+
study_facts = metadata.get("study_facts")
|
| 91 |
+
if isinstance(study_facts, dict):
|
| 92 |
+
lines.extend(["", "Frozen qualitative study facts:", json.dumps(study_facts, ensure_ascii=False, indent=2)])
|
| 93 |
+
if metadata.get("sample_size") is not None:
|
| 94 |
+
lines.append(f"Generated sample size: {metadata.get('sample_size')}")
|
| 95 |
+
if metadata.get("evidence_count") is not None:
|
| 96 |
+
lines.append(f"Generated/retained qualitative evidence items: {metadata.get('evidence_count')}")
|
| 97 |
+
themes = metadata.get("themes")
|
| 98 |
+
if isinstance(themes, list):
|
| 99 |
+
lines.append(f"Generated/retained qualitative themes: {len(themes)}")
|
| 100 |
+
parameters = metadata.get("generation_parameters")
|
| 101 |
+
if isinstance(parameters, dict) and parameters:
|
| 102 |
+
lines.extend(
|
| 103 |
+
[
|
| 104 |
+
"",
|
| 105 |
+
"Generation parameters and process:",
|
| 106 |
+
json.dumps(parameters, ensure_ascii=False, indent=2),
|
| 107 |
+
]
|
| 108 |
+
)
|
| 109 |
+
lines.extend(
|
| 110 |
+
[
|
| 111 |
+
"",
|
| 112 |
+
"Validation boundary:",
|
| 113 |
+
"The package confirms internal consistency, file integrity, citation resolution, and translation completeness. It does not independently verify external data collection, factual truth beyond the supplied sources, journal acceptance, or the result of any third-party AI-writing detector.",
|
| 114 |
+
]
|
| 115 |
+
)
|
| 116 |
+
return "\n".join(lines).strip() + "\n"
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def _manifest(entries: dict[str, bytes]) -> str:
|
| 120 |
+
rows = []
|
| 121 |
+
for name in sorted(entries):
|
| 122 |
+
rows.append(f"{hashlib.sha256(entries[name]).hexdigest()} {name}")
|
| 123 |
+
return "\n".join(rows) + "\n"
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def build_validated_package(
|
| 127 |
+
*,
|
| 128 |
+
english_markdown: str,
|
| 129 |
+
translated_markdown: str,
|
| 130 |
+
english_filename: str,
|
| 131 |
+
translated_filename: str,
|
| 132 |
+
data_metadata: dict[str, Any],
|
| 133 |
+
mode: str,
|
| 134 |
+
citation_audit: list[dict[str, Any]],
|
| 135 |
+
literature_metrics: dict[str, Any],
|
| 136 |
+
) -> PackageResult:
|
| 137 |
+
validate_markdown_manuscript(english_markdown, language_name="English")
|
| 138 |
+
validate_markdown_manuscript(translated_markdown, language_name="Target-language")
|
| 139 |
+
|
| 140 |
+
try:
|
| 141 |
+
english_docx = text_to_docx(english_markdown).getvalue()
|
| 142 |
+
translated_docx = text_to_docx(translated_markdown).getvalue()
|
| 143 |
+
except DocxBuildError as exc:
|
| 144 |
+
raise OutputValidationError(str(exc)) from exc
|
| 145 |
+
validate_docx_bytes(english_docx)
|
| 146 |
+
validate_docx_bytes(translated_docx)
|
| 147 |
+
|
| 148 |
+
entries: dict[str, bytes] = {
|
| 149 |
+
english_filename: english_docx,
|
| 150 |
+
translated_filename: translated_docx,
|
| 151 |
+
"GENERATION_NOTES.txt": build_generation_notes(data_metadata, mode).encode("utf-8"),
|
| 152 |
+
"CITATION_AUDIT.json": json.dumps(citation_audit or [], ensure_ascii=False, indent=2).encode("utf-8"),
|
| 153 |
+
"LITERATURE_METRICS.json": json.dumps(literature_metrics or {}, ensure_ascii=False, indent=2).encode("utf-8"),
|
| 154 |
+
}
|
| 155 |
+
manifest = _manifest(entries)
|
| 156 |
+
entries["FILE_MANIFEST_SHA256.txt"] = manifest.encode("utf-8")
|
| 157 |
+
|
| 158 |
+
buffer = io.BytesIO()
|
| 159 |
+
with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as archive:
|
| 160 |
+
for name, content in entries.items():
|
| 161 |
+
archive.writestr(name, content)
|
| 162 |
+
zip_bytes = buffer.getvalue()
|
| 163 |
+
|
| 164 |
+
try:
|
| 165 |
+
with zipfile.ZipFile(io.BytesIO(zip_bytes), "r") as archive:
|
| 166 |
+
bad = archive.testzip()
|
| 167 |
+
if bad:
|
| 168 |
+
raise OutputValidationError(f"ZIP integrity check failed at {bad}.")
|
| 169 |
+
names = set(archive.namelist())
|
| 170 |
+
required = set(entries)
|
| 171 |
+
if names != required:
|
| 172 |
+
raise OutputValidationError(
|
| 173 |
+
f"ZIP file list mismatch: actual={sorted(names)}, expected={sorted(required)}"
|
| 174 |
+
)
|
| 175 |
+
for name in (english_filename, translated_filename):
|
| 176 |
+
validate_docx_bytes(archive.read(name))
|
| 177 |
+
except OutputValidationError:
|
| 178 |
+
raise
|
| 179 |
+
except Exception as exc:
|
| 180 |
+
raise OutputValidationError(f"ZIP cannot be reopened: {exc}") from exc
|
| 181 |
+
|
| 182 |
+
return PackageResult(zip_bytes=zip_bytes, file_names=tuple(sorted(entries)), manifest=manifest)
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/core/qualitative_engine.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""Qualitative evidence preparation for SmartPaper's ASQ track."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import logging
|
| 7 |
+
import re
|
| 8 |
+
|
| 9 |
+
import streamlit as st
|
| 10 |
+
|
| 11 |
+
from config import APP_CONFIG
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class QualitativeEngine:
|
| 15 |
+
def __init__(self, agent):
|
| 16 |
+
self.agent = agent
|
| 17 |
+
|
| 18 |
+
@staticmethod
|
| 19 |
+
def _clean_cell(value, default="Not specified"):
|
| 20 |
+
cleaned = str(value or default).replace("\n", " ").strip()
|
| 21 |
+
return cleaned or default
|
| 22 |
+
|
| 23 |
+
def _build_request(self, topic, keywords, raw_interviews):
|
| 24 |
+
schema = """Return ONLY one valid JSON object with this schema:
|
| 25 |
+
{
|
| 26 |
+
"study_facts": {
|
| 27 |
+
"research_setting": "string",
|
| 28 |
+
"data_sources": ["string"],
|
| 29 |
+
"participant_count": integer or null,
|
| 30 |
+
"interview_count": integer or null,
|
| 31 |
+
"analysis_method": "string"
|
| 32 |
+
},
|
| 33 |
+
"evidence": [
|
| 34 |
+
{"theme": "2nd-order theme", "concept": "1st-order concept", "quote": "verbatim quote"}
|
| 35 |
+
]
|
| 36 |
+
}
|
| 37 |
+
"""
|
| 38 |
+
if raw_interviews and len(raw_interviews) > 100:
|
| 39 |
+
return f"""Topic: {topic}
|
| 40 |
+
Keywords: {keywords}
|
| 41 |
+
Analyze the supplied interview or field material.
|
| 42 |
+
|
| 43 |
+
Rules:
|
| 44 |
+
1. Use only setting, sample, source, and count information supported by the supplied material. Use null or "Not specified" when absent.
|
| 45 |
+
2. Extract up to 15 representative quotations verbatim; do not rewrite quotations.
|
| 46 |
+
3. Build a coherent Gioia-style coding structure.
|
| 47 |
+
4. The study_facts object will become the single source of truth for the entire manuscript.
|
| 48 |
+
|
| 49 |
+
{schema}
|
| 50 |
+
<QUALITATIVE_MATERIAL>
|
| 51 |
+
{raw_interviews[:30000]}
|
| 52 |
+
</QUALITATIVE_MATERIAL>
|
| 53 |
+
"""
|
| 54 |
+
|
| 55 |
+
return f"""Topic: {topic}
|
| 56 |
+
Keywords: {keywords}
|
| 57 |
+
Construct one internally coherent qualitative study design and evidence packet for a complete ASQ-style manuscript when no interview material is supplied.
|
| 58 |
+
|
| 59 |
+
Rules:
|
| 60 |
+
1. Choose one research setting and keep it fixed.
|
| 61 |
+
2. Choose one participant count, interview count, data-source set, and analysis method; do not create conflicting counts.
|
| 62 |
+
3. Produce exactly 12 natural representative quotations organized into a Gioia-style coding structure.
|
| 63 |
+
4. The study_facts object will become the single source of truth for the entire manuscript.
|
| 64 |
+
5. Avoid decorative language, generic AI phrasing, and repetitive quotation patterns.
|
| 65 |
+
|
| 66 |
+
{schema}
|
| 67 |
+
"""
|
| 68 |
+
|
| 69 |
+
def resolve_qualitative_data(self, topic, keywords, raw_interviews=""):
|
| 70 |
+
has_user_data = bool(raw_interviews and len(raw_interviews) > 100)
|
| 71 |
+
prompt = self._build_request(topic, keywords, raw_interviews)
|
| 72 |
+
model_label = self.agent.model_for("reasoning")
|
| 73 |
+
|
| 74 |
+
if has_user_data:
|
| 75 |
+
status_label = (
|
| 76 |
+
f"🧠 正在使用 {model_label} 分析访谈材料并冻结研究事实…"
|
| 77 |
+
)
|
| 78 |
+
complete_label = "✅ 访谈材料、研究事实与编码结构已完成"
|
| 79 |
+
else:
|
| 80 |
+
status_label = (
|
| 81 |
+
f"🧠 正在使用 {model_label} 构建一致的定性研究证据包…"
|
| 82 |
+
)
|
| 83 |
+
complete_label = "✅ 定性研究事实与编码结构已完成"
|
| 84 |
+
|
| 85 |
+
with st.status(status_label, expanded=True) as status:
|
| 86 |
+
placeholder = st.empty()
|
| 87 |
+
response = placeholder.write_stream(
|
| 88 |
+
self.agent.stream_deepseek(
|
| 89 |
+
"You are a rigorous qualitative research analyst.",
|
| 90 |
+
prompt,
|
| 91 |
+
model_role="reasoning",
|
| 92 |
+
thinking=True,
|
| 93 |
+
allow_fallback=False,
|
| 94 |
+
)
|
| 95 |
+
)
|
| 96 |
+
status.update(
|
| 97 |
+
label=complete_label,
|
| 98 |
+
state="complete",
|
| 99 |
+
expanded=False,
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
response = re.sub(
|
| 103 |
+
r"<think>.*?</think>",
|
| 104 |
+
"",
|
| 105 |
+
response,
|
| 106 |
+
flags=re.DOTALL,
|
| 107 |
+
).strip()
|
| 108 |
+
data = self.agent._extract_json_from_text(response)
|
| 109 |
+
|
| 110 |
+
if not isinstance(data, dict):
|
| 111 |
+
logging.error("Qualitative JSON parsing failed; using safe fallback.")
|
| 112 |
+
data = {
|
| 113 |
+
"study_facts": {
|
| 114 |
+
"research_setting": "Not specified",
|
| 115 |
+
"data_sources": ["Interview or field material"],
|
| 116 |
+
"participant_count": None,
|
| 117 |
+
"interview_count": None,
|
| 118 |
+
"analysis_method": "Gioia-style inductive coding",
|
| 119 |
+
},
|
| 120 |
+
"evidence": [
|
| 121 |
+
{
|
| 122 |
+
"theme": "Sensemaking",
|
| 123 |
+
"concept": "Initial uncertainty",
|
| 124 |
+
"quote": (
|
| 125 |
+
"We did not yet have a shared understanding "
|
| 126 |
+
"of how to respond."
|
| 127 |
+
),
|
| 128 |
+
}
|
| 129 |
+
],
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
facts = data.get("study_facts") or {}
|
| 133 |
+
evidence = data.get("evidence") or data.get("quotes") or []
|
| 134 |
+
if not isinstance(evidence, list):
|
| 135 |
+
evidence = []
|
| 136 |
+
|
| 137 |
+
data_sources = facts.get("data_sources") or ["Not specified"]
|
| 138 |
+
if not isinstance(data_sources, list):
|
| 139 |
+
data_sources = [data_sources]
|
| 140 |
+
|
| 141 |
+
setting = self._clean_cell(facts.get("research_setting"))
|
| 142 |
+
sources = ", ".join(self._clean_cell(item) for item in data_sources)
|
| 143 |
+
participants = facts.get("participant_count")
|
| 144 |
+
interviews = facts.get("interview_count")
|
| 145 |
+
method = self._clean_cell(
|
| 146 |
+
facts.get("analysis_method"),
|
| 147 |
+
"Gioia-style inductive coding",
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
md = (
|
| 151 |
+
"### Qualitative Study Design Summary\n"
|
| 152 |
+
"| Field | Frozen value |\n"
|
| 153 |
+
"| :--- | :--- |\n"
|
| 154 |
+
f"| Research setting | {setting} |\n"
|
| 155 |
+
f"| Data sources | {sources} |\n"
|
| 156 |
+
f"| Participants | {participants if participants is not None else 'Not specified'} |\n"
|
| 157 |
+
f"| Interviews | {interviews if interviews is not None else 'Not specified'} |\n"
|
| 158 |
+
f"| Analysis method | {method} |\n\n"
|
| 159 |
+
"[CONSISTENCY MANDATE]\n"
|
| 160 |
+
"Use the study-design summary above as the single source of truth. "
|
| 161 |
+
"Do not change the setting, sample, interview count, data sources, "
|
| 162 |
+
"or analysis method in any section.\n\n"
|
| 163 |
+
"### Table: Qualitative Coding Structure (Data Evidence)\n"
|
| 164 |
+
"| 2nd-order Theme | 1st-order Concept | Representative Quote |\n"
|
| 165 |
+
"| :--- | :--- | :--- |\n"
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
clean_evidence = []
|
| 169 |
+
for index, item in enumerate(evidence[:15]):
|
| 170 |
+
if not isinstance(item, dict):
|
| 171 |
+
continue
|
| 172 |
+
quote = self._clean_cell(item.get("quote"), "").replace('"', "'")
|
| 173 |
+
concept = self._clean_cell(item.get("concept"), "Analysis")
|
| 174 |
+
theme = self._clean_cell(item.get("theme"), "Observation")
|
| 175 |
+
if not quote:
|
| 176 |
+
continue
|
| 177 |
+
tag = f"QUOTE-{index + 1:02d}"
|
| 178 |
+
md += f'| {theme} | {concept} | "{quote}" [{tag}] |\n'
|
| 179 |
+
clean_evidence.append(
|
| 180 |
+
{
|
| 181 |
+
"theme": theme,
|
| 182 |
+
"concept": concept,
|
| 183 |
+
"quote": quote,
|
| 184 |
+
"tag": tag,
|
| 185 |
+
}
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
theme_map = {}
|
| 189 |
+
for item in clean_evidence:
|
| 190 |
+
theme_entry = theme_map.setdefault(
|
| 191 |
+
item["theme"],
|
| 192 |
+
{"title": item["theme"], "concepts": [], "evidence": []},
|
| 193 |
+
)
|
| 194 |
+
if item["concept"] not in theme_entry["concepts"]:
|
| 195 |
+
theme_entry["concepts"].append(item["concept"])
|
| 196 |
+
theme_entry["evidence"].append(
|
| 197 |
+
{"tag": item["tag"], "quote": item["quote"]}
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
themes = list(theme_map.values())
|
| 201 |
+
validation = APP_CONFIG.get("manuscript_validation", {})
|
| 202 |
+
minimum_themes = int(validation.get("minimum_qualitative_themes", 3))
|
| 203 |
+
minimum_quotes = int(validation.get("minimum_qualitative_quotes", 8))
|
| 204 |
+
if len(themes) < minimum_themes or len(clean_evidence) < minimum_quotes:
|
| 205 |
+
raise RuntimeError(
|
| 206 |
+
"Qualitative evidence packet is incomplete: "
|
| 207 |
+
f"themes={len(themes)}, quotes={len(clean_evidence)}; "
|
| 208 |
+
f"required themes>={minimum_themes}, quotes>={minimum_quotes}."
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
metadata = {
|
| 212 |
+
"mode": "qualitative",
|
| 213 |
+
"source_type": (
|
| 214 |
+
"user_provided" if has_user_data else "system_generated"
|
| 215 |
+
),
|
| 216 |
+
"disclosure_required": not has_user_data,
|
| 217 |
+
"study_facts": {
|
| 218 |
+
"research_setting": setting,
|
| 219 |
+
"data_sources": data_sources,
|
| 220 |
+
"participant_count": participants,
|
| 221 |
+
"interview_count": interviews,
|
| 222 |
+
"analysis_method": method,
|
| 223 |
+
},
|
| 224 |
+
"evidence_count": len(clean_evidence),
|
| 225 |
+
"generation_parameters": {
|
| 226 |
+
"coding_approach": "Gioia-style inductive coding",
|
| 227 |
+
"requested_quote_count_when_generated": 12,
|
| 228 |
+
"maximum_retained_quotes": 15,
|
| 229 |
+
"minimum_themes": minimum_themes,
|
| 230 |
+
"minimum_quotes": minimum_quotes,
|
| 231 |
+
"consistency_constraints": [
|
| 232 |
+
"one fixed research setting",
|
| 233 |
+
"one fixed participant count",
|
| 234 |
+
"one fixed interview count",
|
| 235 |
+
"one fixed data-source set",
|
| 236 |
+
"one fixed analysis method",
|
| 237 |
+
"quotation tags must remain unique",
|
| 238 |
+
],
|
| 239 |
+
},
|
| 240 |
+
"evidence": clean_evidence,
|
| 241 |
+
"themes": themes,
|
| 242 |
+
}
|
| 243 |
+
status_message = "✅ 定性研究事实、摘要锚点输入和Gioia编码证据已准备完成。"
|
| 244 |
+
return md, status_message, metadata
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def get_qualitative_engine(agent):
|
| 248 |
+
return QualitativeEngine(agent)
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/core/quantitative_engine.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""Quantitative data preparation for SmartPaper's AMJ track."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import re
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
import pandas as pd
|
| 10 |
+
|
| 11 |
+
from i18n import tr
|
| 12 |
+
from utils.file_parser import extract_data_from_files
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class QuantitativeEngine:
|
| 16 |
+
"""Keep quantitative preparation isolated from the Streamlit UI."""
|
| 17 |
+
|
| 18 |
+
@staticmethod
|
| 19 |
+
def generate_psd_corr_matrix(n_constructs, max_corr=0.5):
|
| 20 |
+
if n_constructs == 1:
|
| 21 |
+
return np.array([[1.0]])
|
| 22 |
+
base_corr = np.random.uniform(0.2, max_corr - 0.1)
|
| 23 |
+
matrix = np.random.uniform(
|
| 24 |
+
base_corr - 0.1,
|
| 25 |
+
base_corr + 0.1,
|
| 26 |
+
(n_constructs, n_constructs),
|
| 27 |
+
)
|
| 28 |
+
matrix = (matrix + matrix.T) / 2
|
| 29 |
+
np.fill_diagonal(matrix, 1.0)
|
| 30 |
+
min_eig = np.min(np.linalg.eigvals(matrix))
|
| 31 |
+
if min_eig < 0:
|
| 32 |
+
matrix -= (min_eig - 0.01) * np.eye(n_constructs)
|
| 33 |
+
diagonal = np.sqrt(np.diag(matrix))
|
| 34 |
+
return matrix / np.outer(diagonal, diagonal)
|
| 35 |
+
|
| 36 |
+
@staticmethod
|
| 37 |
+
def calculate_validity_metrics(df, constructs_info, true_loadings):
|
| 38 |
+
metrics = []
|
| 39 |
+
for construct in constructs_info:
|
| 40 |
+
construct_name = construct["name"]
|
| 41 |
+
items = construct["items"]
|
| 42 |
+
sub_df = df[items]
|
| 43 |
+
var_total = sub_df.sum(axis=1).var(ddof=1)
|
| 44 |
+
sum_var = sub_df.var(ddof=1).sum()
|
| 45 |
+
n_items = len(items)
|
| 46 |
+
alpha = (
|
| 47 |
+
0.0
|
| 48 |
+
if var_total <= 0
|
| 49 |
+
else (n_items / (n_items - 1))
|
| 50 |
+
* (1 - sum_var / var_total)
|
| 51 |
+
)
|
| 52 |
+
loadings = np.array(true_loadings[construct_name])
|
| 53 |
+
sum_loadings_sq = np.sum(loadings) ** 2
|
| 54 |
+
sum_sq_loadings = np.sum(loadings**2)
|
| 55 |
+
sum_error = np.sum(1 - loadings**2)
|
| 56 |
+
cr = sum_loadings_sq / (sum_loadings_sq + sum_error)
|
| 57 |
+
ave = sum_sq_loadings / len(loadings)
|
| 58 |
+
metrics.append(
|
| 59 |
+
{
|
| 60 |
+
"Construct": construct_name,
|
| 61 |
+
"Items": n_items,
|
| 62 |
+
"Cronbach's α": round(alpha, 3),
|
| 63 |
+
"CR": round(cr, 3),
|
| 64 |
+
"AVE": round(ave, 3),
|
| 65 |
+
}
|
| 66 |
+
)
|
| 67 |
+
return pd.DataFrame(metrics)
|
| 68 |
+
|
| 69 |
+
def generate_empirical_data_robust(
|
| 70 |
+
self,
|
| 71 |
+
construct_names=None,
|
| 72 |
+
n=300,
|
| 73 |
+
scale_points=7,
|
| 74 |
+
):
|
| 75 |
+
if not construct_names or len(construct_names) < 2:
|
| 76 |
+
construct_names = [
|
| 77 |
+
"Independent_Var",
|
| 78 |
+
"Mediator_Var",
|
| 79 |
+
"Dependent_Var",
|
| 80 |
+
]
|
| 81 |
+
|
| 82 |
+
n_constructs = len(construct_names)
|
| 83 |
+
corr = self.generate_psd_corr_matrix(n_constructs, max_corr=0.55)
|
| 84 |
+
latent_scores = np.random.multivariate_normal(
|
| 85 |
+
mean=[0] * n_constructs,
|
| 86 |
+
cov=corr,
|
| 87 |
+
size=n,
|
| 88 |
+
)
|
| 89 |
+
data = {}
|
| 90 |
+
true_loadings = {}
|
| 91 |
+
constructs_info = []
|
| 92 |
+
target_mean = scale_points / 2.0 + 0.5
|
| 93 |
+
target_std = scale_points / 5.0
|
| 94 |
+
|
| 95 |
+
for index, construct_name in enumerate(construct_names):
|
| 96 |
+
true_score = latent_scores[:, index]
|
| 97 |
+
n_items = np.random.randint(3, 6)
|
| 98 |
+
loadings = np.random.uniform(0.72, 0.88, n_items)
|
| 99 |
+
true_loadings[construct_name] = loadings
|
| 100 |
+
item_names = []
|
| 101 |
+
for item_index, loading in enumerate(loadings):
|
| 102 |
+
error = np.random.normal(
|
| 103 |
+
0,
|
| 104 |
+
np.sqrt(1 - loading**2),
|
| 105 |
+
n,
|
| 106 |
+
)
|
| 107 |
+
item_score = loading * true_score + error
|
| 108 |
+
raw_scaled = item_score * target_std + target_mean
|
| 109 |
+
item_int = np.clip(
|
| 110 |
+
np.round(raw_scaled),
|
| 111 |
+
1,
|
| 112 |
+
scale_points,
|
| 113 |
+
).astype(int)
|
| 114 |
+
column_name = f"{construct_name}_{item_index + 1}"
|
| 115 |
+
data[column_name] = item_int
|
| 116 |
+
item_names.append(column_name)
|
| 117 |
+
constructs_info.append(
|
| 118 |
+
{"name": construct_name, "items": item_names}
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
df = pd.DataFrame(data)
|
| 122 |
+
validity_df = self.calculate_validity_metrics(
|
| 123 |
+
df,
|
| 124 |
+
constructs_info,
|
| 125 |
+
true_loadings,
|
| 126 |
+
)
|
| 127 |
+
mean_scores = pd.DataFrame(
|
| 128 |
+
{
|
| 129 |
+
construct["name"]: df[construct["items"]].mean(axis=1)
|
| 130 |
+
for construct in constructs_info
|
| 131 |
+
}
|
| 132 |
+
)
|
| 133 |
+
fl_matrix = mean_scores.corr().astype(object)
|
| 134 |
+
|
| 135 |
+
for _, row in validity_df.iterrows():
|
| 136 |
+
construct_name = row["Construct"]
|
| 137 |
+
sqrt_ave = np.sqrt(row["AVE"])
|
| 138 |
+
fl_matrix.loc[construct_name, construct_name] = (
|
| 139 |
+
f"({sqrt_ave:.3f})"
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
for row_index in range(len(fl_matrix.columns)):
|
| 143 |
+
for column_index in range(row_index + 1, len(fl_matrix.columns)):
|
| 144 |
+
fl_matrix.iloc[row_index, column_index] = ""
|
| 145 |
+
|
| 146 |
+
desc_stats = mean_scores.mean().to_frame("Mean").join(
|
| 147 |
+
mean_scores.std().to_frame("SD")
|
| 148 |
+
)
|
| 149 |
+
return df, validity_df, desc_stats.join(fl_matrix)
|
| 150 |
+
|
| 151 |
+
def resolve_empirical_data(self, uploaded_files=None, language="zh"):
|
| 152 |
+
if uploaded_files:
|
| 153 |
+
user_md = extract_data_from_files(uploaded_files)
|
| 154 |
+
if user_md:
|
| 155 |
+
evidence_sections = [
|
| 156 |
+
heading.strip()
|
| 157 |
+
for heading in re.findall(
|
| 158 |
+
r"(?m)^#{1,3}\s+(.+?)\s*$",
|
| 159 |
+
user_md,
|
| 160 |
+
)
|
| 161 |
+
if heading.strip()
|
| 162 |
+
]
|
| 163 |
+
metadata = {
|
| 164 |
+
"mode": "quantitative",
|
| 165 |
+
"source_type": "user_provided",
|
| 166 |
+
"disclosure_required": False,
|
| 167 |
+
"evidence_sections": evidence_sections[:20]
|
| 168 |
+
or ["User-provided quantitative evidence"],
|
| 169 |
+
}
|
| 170 |
+
return None, user_md, tr(language, "actual_stats_parsed"), metadata
|
| 171 |
+
|
| 172 |
+
df, validity_df, fl_matrix_df = self.generate_empirical_data_robust()
|
| 173 |
+
generated_prompt = f"""### Table 1: Reliability and Convergent Validity
|
| 174 |
+
{validity_df.to_markdown(index=False)}
|
| 175 |
+
### Table 2: Descriptive Statistics and Discriminant Validity (Fornell-Larcker Criterion)
|
| 176 |
+
*Note: Diagonal elements in parentheses are the square roots of AVE.*
|
| 177 |
+
{fl_matrix_df.to_markdown()}
|
| 178 |
+
[STRICT EMPIRICAL INSTRUCTION]
|
| 179 |
+
1. ZERO HALLUCINATION: Report the exact metrics provided above.
|
| 180 |
+
2. VALIDITY DEFENSE: State convergent validity only when the displayed AVE and CR values support it.
|
| 181 |
+
3. DISCRIMINANT DEFENSE: State discriminant validity only when the displayed diagonal and correlation values support it.
|
| 182 |
+
4. CONSISTENCY: Use these tables as the single source of numerical truth throughout the paper.
|
| 183 |
+
"""
|
| 184 |
+
metadata = {
|
| 185 |
+
"mode": "quantitative",
|
| 186 |
+
"source_type": "system_generated",
|
| 187 |
+
"disclosure_required": True,
|
| 188 |
+
"sample_size": int(len(df)),
|
| 189 |
+
"scale_points": 7,
|
| 190 |
+
"method": (
|
| 191 |
+
"Positive-semidefinite latent correlation generation, "
|
| 192 |
+
"indicator loadings, discretization, and derived "
|
| 193 |
+
"reliability/validity summaries."
|
| 194 |
+
),
|
| 195 |
+
"generation_parameters": {
|
| 196 |
+
"constructs": [
|
| 197 |
+
"Independent_Var",
|
| 198 |
+
"Mediator_Var",
|
| 199 |
+
"Dependent_Var",
|
| 200 |
+
],
|
| 201 |
+
"sample_size": int(len(df)),
|
| 202 |
+
"likert_scale_points": 7,
|
| 203 |
+
"items_per_construct": "random integer from 3 to 5",
|
| 204 |
+
"indicator_loading_range": [0.72, 0.88],
|
| 205 |
+
"latent_correlation_control": "positive-semidefinite matrix with maximum target correlation 0.55",
|
| 206 |
+
"item_generation": "loading × latent score + normally distributed measurement error",
|
| 207 |
+
"discretization": "round to nearest integer and clamp to 1–7",
|
| 208 |
+
"reported_summaries": [
|
| 209 |
+
"Cronbach alpha",
|
| 210 |
+
"composite reliability",
|
| 211 |
+
"average variance extracted",
|
| 212 |
+
"descriptive statistics",
|
| 213 |
+
"Fornell-Larcker matrix",
|
| 214 |
+
],
|
| 215 |
+
},
|
| 216 |
+
"evidence_sections": [
|
| 217 |
+
"Reliability and convergent validity",
|
| 218 |
+
"Descriptive statistics and discriminant validity",
|
| 219 |
+
],
|
| 220 |
+
}
|
| 221 |
+
return (
|
| 222 |
+
df,
|
| 223 |
+
generated_prompt,
|
| 224 |
+
tr(language, "synthetic_stats_generated"),
|
| 225 |
+
metadata,
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def get_quantitative_engine():
|
| 230 |
+
return QuantitativeEngine()
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/core/rag_engine.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
from typing import List
|
| 3 |
+
import numpy as np
|
| 4 |
+
from config import APP_CONFIG
|
| 5 |
+
|
| 6 |
+
try:
|
| 7 |
+
from sklearn.feature_extraction.text import TfidfVectorizer
|
| 8 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 9 |
+
SKLEARN_AVAILABLE = True
|
| 10 |
+
except ImportError:
|
| 11 |
+
SKLEARN_AVAILABLE = False
|
| 12 |
+
|
| 13 |
+
class ResearchRAGEngine:
|
| 14 |
+
def __init__(self, raw_texts: List[str]):
|
| 15 |
+
self.chunks = []
|
| 16 |
+
self.vectorizer = None
|
| 17 |
+
self.tfidf_matrix = None
|
| 18 |
+
self.chunk_size = APP_CONFIG["rag_chunk_size"]
|
| 19 |
+
self._initialize_chunks(raw_texts)
|
| 20 |
+
|
| 21 |
+
def _initialize_chunks(self, raw_texts: List[str]):
|
| 22 |
+
for text in raw_texts:
|
| 23 |
+
for i in range(0, len(text), self.chunk_size):
|
| 24 |
+
chunk = text[i:i + self.chunk_size].strip()
|
| 25 |
+
if len(chunk) > 50:
|
| 26 |
+
self.chunks.append(chunk)
|
| 27 |
+
|
| 28 |
+
if SKLEARN_AVAILABLE and self.chunks:
|
| 29 |
+
self.vectorizer = TfidfVectorizer(stop_words='english', max_df=0.85)
|
| 30 |
+
try:
|
| 31 |
+
self.tfidf_matrix = self.vectorizer.fit_transform(self.chunks)
|
| 32 |
+
except ValueError:
|
| 33 |
+
self.vectorizer = None
|
| 34 |
+
|
| 35 |
+
def get_relevant_context(self, query: str, top_k: int = None) -> str:
|
| 36 |
+
if not self.chunks:
|
| 37 |
+
return ""
|
| 38 |
+
if top_k is None:
|
| 39 |
+
top_k = APP_CONFIG["rag_top_k"]
|
| 40 |
+
|
| 41 |
+
relevant_chunks = []
|
| 42 |
+
if self.vectorizer and self.tfidf_matrix is not None:
|
| 43 |
+
query_vec = self.vectorizer.transform([query])
|
| 44 |
+
similarities = cosine_similarity(query_vec, self.tfidf_matrix).flatten()
|
| 45 |
+
top_indices = similarities.argsort()[-top_k:][::-1]
|
| 46 |
+
relevant_chunks = [self.chunks[i] for i in top_indices if similarities[i] > 0.05]
|
| 47 |
+
else:
|
| 48 |
+
scores = []
|
| 49 |
+
query_words = set(query.lower().split())
|
| 50 |
+
for chunk in self.chunks:
|
| 51 |
+
chunk_lower = chunk.lower()
|
| 52 |
+
score = sum(1 for word in query_words if word in chunk_lower)
|
| 53 |
+
scores.append(score)
|
| 54 |
+
top_indices = np.argsort(scores)[-top_k:]
|
| 55 |
+
relevant_chunks = [self.chunks[i] for i in top_indices if scores[i] > 0]
|
| 56 |
+
|
| 57 |
+
if not relevant_chunks:
|
| 58 |
+
return ""
|
| 59 |
+
return "\n--- Target Context Snippet ---\n" + "\n\n".join(relevant_chunks)
|
| 60 |
+
|
| 61 |
+
def initialize_rag(texts: List[str]):
|
| 62 |
+
return ResearchRAGEngine(texts)
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/desktop.ini
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[.ShellClassInfo]
|
| 2 |
+
IconResource=C:\Program Files\Google\Drive File Stream\129.0.1.0\GoogleDriveFS.exe,27
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/i18n.py
ADDED
|
@@ -0,0 +1,856 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""SmartPaper user-interface translations.
|
| 3 |
+
|
| 4 |
+
The Chinese interface is the visual baseline. Other languages replace text only;
|
| 5 |
+
Streamlit component order, icons, layout, and interaction remain unchanged.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from typing import Any
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
LANGUAGE_OPTIONS = {
|
| 14 |
+
"中文": "zh",
|
| 15 |
+
"English": "en",
|
| 16 |
+
"Русский": "ru",
|
| 17 |
+
"Қазақша": "kk",
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
TARGET_LANGUAGE = {
|
| 21 |
+
"zh": "Simplified Chinese (简体中文)",
|
| 22 |
+
# Approved product rule: English UI still delivers English evidence draft + Chinese paper.
|
| 23 |
+
"en": "Simplified Chinese (简体中文)",
|
| 24 |
+
"ru": "Russian (Русский)",
|
| 25 |
+
"kk": "Kazakh (Қазақша)",
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
OUTPUT_SUFFIX = {
|
| 29 |
+
"zh": "CN",
|
| 30 |
+
"en": "CN",
|
| 31 |
+
"ru": "RU",
|
| 32 |
+
"kk": "KK",
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
TARGET_FLAG = {
|
| 36 |
+
"zh": "🇨🇳",
|
| 37 |
+
"en": "🇨🇳",
|
| 38 |
+
"ru": "🇷🇺",
|
| 39 |
+
"kk": "🇰🇿",
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
AMJ_CSV_TEMPLATES = {
|
| 45 |
+
"zh": """--- 表1:描述性统计与相关系数 ---
|
| 46 |
+
Variables,Mean,SD,1,2,3
|
| 47 |
+
Team Cohesion,4.21,0.85,(0.88),,
|
| 48 |
+
AI Integration,3.95,0.91,0.45**,(0.82),
|
| 49 |
+
Task Performance,4.10,0.78,0.52**,0.38*,(0.85)
|
| 50 |
+
|
| 51 |
+
--- 表2:信效度检验 ---
|
| 52 |
+
Construct,Items,Cronbach's Alpha,CR,AVE
|
| 53 |
+
Team Cohesion,4,0.89,0.91,0.71
|
| 54 |
+
AI Integration,3,0.85,0.88,0.68
|
| 55 |
+
|
| 56 |
+
--- 表3:假设检验 ---
|
| 57 |
+
Hypothesis,Path,Beta,T-value,P-value,Supported?
|
| 58 |
+
H1,AI Integration -> Team Cohesion,0.35,4.21,0.000,Yes""",
|
| 59 |
+
"en": """--- Table 1: Descriptive Statistics and Correlations ---
|
| 60 |
+
Variables,Mean,SD,1,2,3
|
| 61 |
+
Team Cohesion,4.21,0.85,(0.88),,
|
| 62 |
+
AI Integration,3.95,0.91,0.45**,(0.82),
|
| 63 |
+
Task Performance,4.10,0.78,0.52**,0.38*,(0.85)
|
| 64 |
+
|
| 65 |
+
--- Table 2: Reliability and Validity ---
|
| 66 |
+
Construct,Items,Cronbach's Alpha,CR,AVE
|
| 67 |
+
Team Cohesion,4,0.89,0.91,0.71
|
| 68 |
+
AI Integration,3,0.85,0.88,0.68
|
| 69 |
+
|
| 70 |
+
--- Table 3: Hypothesis Tests ---
|
| 71 |
+
Hypothesis,Path,Beta,T-value,P-value,Supported?
|
| 72 |
+
H1,AI Integration -> Team Cohesion,0.35,4.21,0.000,Yes""",
|
| 73 |
+
"ru": """--- Таблица 1: Описательная статистика и корреляции ---
|
| 74 |
+
Переменные,Среднее,SD,1,2,3
|
| 75 |
+
Сплочённость команды,4.21,0.85,(0.88),,
|
| 76 |
+
Интеграция ИИ,3.95,0.91,0.45**,(0.82),
|
| 77 |
+
Результативность задачи,4.10,0.78,0.52**,0.38*,(0.85)
|
| 78 |
+
|
| 79 |
+
--- Таблица 2: Надёжность и валидность ---
|
| 80 |
+
Конструкт,Пункты,Cronbach's Alpha,CR,AVE
|
| 81 |
+
Сплочённость команды,4,0.89,0.91,0.71
|
| 82 |
+
Интеграция ИИ,3,0.85,0.88,0.68
|
| 83 |
+
|
| 84 |
+
--- Таблица 3: Проверка гипотез ---
|
| 85 |
+
Гипотеза,Связь,Beta,T-значение,P-значение,Поддержана?
|
| 86 |
+
H1,Интеграция ИИ -> Сплочённость команды,0.35,4.21,0.000,Да""",
|
| 87 |
+
"kk": """--- 1-кесте: Сипаттамалық статистика және корреляциялар ---
|
| 88 |
+
Айнымалылар,Орташа,SD,1,2,3
|
| 89 |
+
Команда бірлігі,4.21,0.85,(0.88),,
|
| 90 |
+
ЖИ интеграциясы,3.95,0.91,0.45**,(0.82),
|
| 91 |
+
Тапсырма нәтижелілігі,4.10,0.78,0.52**,0.38*,(0.85)
|
| 92 |
+
|
| 93 |
+
--- 2-кесте: Сенімділік және валидтілік ---
|
| 94 |
+
Конструкт,Тармақтар,Cronbach's Alpha,CR,AVE
|
| 95 |
+
Команда бірлігі,4,0.89,0.91,0.71
|
| 96 |
+
ЖИ интеграциясы,3,0.85,0.88,0.68
|
| 97 |
+
|
| 98 |
+
--- 3-кесте: Гипотезаларды тексеру ---
|
| 99 |
+
Гипотеза,Байланыс,Beta,T-мәні,P-мәні,Қолдау бар ма?
|
| 100 |
+
H1,ЖИ интеграциясы -> Команда бірлігі,0.35,4.21,0.000,Иә""",
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
ASQ_TXT_TEMPLATES = {
|
| 104 |
+
"zh": """[访谈编号]:INT-01
|
| 105 |
+
[受访者岗位]:高级软件工程师
|
| 106 |
+
[访谈日期]:2026-03-12
|
| 107 |
+
[访谈时长]:45分钟
|
| 108 |
+
|
| 109 |
+
访谈者:生成式AI助手的引入如何改变了团队的日常敏捷站会?
|
| 110 |
+
|
| 111 |
+
受访者(INT-01):[在这里输入受访者的完整回答]
|
| 112 |
+
|
| 113 |
+
访谈者:[在这里输入后续问题]
|
| 114 |
+
|
| 115 |
+
受访者(INT-01):[在这里输入后续回答]
|
| 116 |
+
|
| 117 |
+
--------------------------------------------------
|
| 118 |
+
|
| 119 |
+
[访谈编号]:INT-02
|
| 120 |
+
[受访者岗位]:敏捷教练
|
| 121 |
+
[访谈日期]:2026-03-15
|
| 122 |
+
[访谈时长]:60分钟
|
| 123 |
+
|
| 124 |
+
访谈者:[在这里输入问题]
|
| 125 |
+
|
| 126 |
+
受访者(INT-02):[在这里输入回答]""",
|
| 127 |
+
"en": """[Interview ID]: INT-01
|
| 128 |
+
[Informant Role]: Senior Software Engineer
|
| 129 |
+
[Interview Date]: 2026-03-12
|
| 130 |
+
[Interview Duration]: 45 minutes
|
| 131 |
+
|
| 132 |
+
Interviewer: How did the introduction of a generative-AI assistant change the team's daily stand-up meetings?
|
| 133 |
+
|
| 134 |
+
Informant (INT-01): [Enter the informant's complete response here]
|
| 135 |
+
|
| 136 |
+
Interviewer: [Enter the follow-up question here]
|
| 137 |
+
|
| 138 |
+
Informant (INT-01): [Enter the follow-up response here]
|
| 139 |
+
|
| 140 |
+
--------------------------------------------------
|
| 141 |
+
|
| 142 |
+
[Interview ID]: INT-02
|
| 143 |
+
[Informant Role]: Scrum Master
|
| 144 |
+
[Interview Date]: 2026-03-15
|
| 145 |
+
[Interview Duration]: 60 minutes
|
| 146 |
+
|
| 147 |
+
Interviewer: [Enter the question here]
|
| 148 |
+
|
| 149 |
+
Informant (INT-02): [Enter the response here]""",
|
| 150 |
+
"ru": """[Номер интервью]: INT-01
|
| 151 |
+
[Роль информанта]: Старший инженер-программист
|
| 152 |
+
[Дата интервью]: 2026-03-12
|
| 153 |
+
[Продолжительность]: 45 минут
|
| 154 |
+
|
| 155 |
+
Интервьюер: Как внедрение помощника на основе генеративного ИИ изменило ежедневные стендап-встречи команды?
|
| 156 |
+
|
| 157 |
+
Информант (INT-01): [Введите полный ответ информанта]
|
| 158 |
+
|
| 159 |
+
Интервьюер: [Введите уточняющий вопрос]
|
| 160 |
+
|
| 161 |
+
Информант (INT-01): [Введите последующий ответ]
|
| 162 |
+
|
| 163 |
+
--------------------------------------------------
|
| 164 |
+
|
| 165 |
+
[Номер интервью]: INT-02
|
| 166 |
+
[Роль информанта]: Скрам-мастер
|
| 167 |
+
[Дата интервью]: 2026-03-15
|
| 168 |
+
[Продолжительность]: 60 минут
|
| 169 |
+
|
| 170 |
+
Интервьюер: [Введите вопрос]
|
| 171 |
+
|
| 172 |
+
Информант (INT-02): [Введите ответ]""",
|
| 173 |
+
"kk": """[Сұхбат нөмірі]: INT-01
|
| 174 |
+
[Ақпарат берушінің рөлі]: Аға бағдарламалық жасақтама инженері
|
| 175 |
+
[Сұхбат күні]: 2026-03-12
|
| 176 |
+
[Ұзақтығы]: 45 минут
|
| 177 |
+
|
| 178 |
+
Сұхбат алушы: Генеративті ЖИ көмекшісін енгізу команданың күнделікті стендап жиналыстарына қалай әсер етті?
|
| 179 |
+
|
| 180 |
+
Ақпарат беруші (INT-01): [Ақпарат берушінің толық жауабын осында енгізіңіз]
|
| 181 |
+
|
| 182 |
+
Сұхбат алушы: [Қосымша сұрақты осында енгізіңіз]
|
| 183 |
+
|
| 184 |
+
Ақпарат беруші (INT-01): [Қосымша жауапты осында енгізіңіз]
|
| 185 |
+
|
| 186 |
+
--------------------------------------------------
|
| 187 |
+
|
| 188 |
+
[Сұхбат нөмірі]: INT-02
|
| 189 |
+
[Ақпарат берушінің рөлі]: Scrum-мастер
|
| 190 |
+
[Сұхбат күні]: 2026-03-15
|
| 191 |
+
[Ұзақтығы]: 60 минут
|
| 192 |
+
|
| 193 |
+
Сұхбат алушы: [Сұрақты осында енгізіңіз]
|
| 194 |
+
|
| 195 |
+
Ақпарат беруші (INT-02): [Жауапты осында енгізіңіз]""",
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
REFERENCE_HEADINGS = {
|
| 199 |
+
"zh": "参考文献",
|
| 200 |
+
"en": "参考文献",
|
| 201 |
+
"ru": "Список литературы",
|
| 202 |
+
"kk": "Пайдаланылған әдебиеттер",
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
TEXTS: dict[str, dict[str, str]] = {
|
| 207 |
+
"zh": {
|
| 208 |
+
"title": "📊 工商管理论文智能体",
|
| 209 |
+
"topic_label": "📝 论文名称",
|
| 210 |
+
"keywords_label": "🔍 关键字",
|
| 211 |
+
"words_label": "📏 目标总单词数(12000–25000单词)",
|
| 212 |
+
"success": "🎉 流水线推演完毕!双语文档已就绪。",
|
| 213 |
+
"download": "📥 下载文件",
|
| 214 |
+
"system_busy_title": "⏳ 系统忙碌",
|
| 215 |
+
"system_busy_warning": "🚨 有用户正在生成论文",
|
| 216 |
+
"system_busy_info": "💡 预计25–35分钟后可正常使用",
|
| 217 |
+
"checkpoint_warning": "⚠️ 系统检测到上次因网络或意外中断的未完成论文任务。",
|
| 218 |
+
"resume": "🔄 恢复断点续传",
|
| 219 |
+
"clear": "🗑️ 清除临时数据",
|
| 220 |
+
"busy_short": "⏳ 有用户正在生成论文,预计25–35分钟后可用。",
|
| 221 |
+
"tab_amj": "✨ 定量研究(AMJ 引擎)",
|
| 222 |
+
"tab_asq": "📝 定性研究(ASQ 引擎)",
|
| 223 |
+
"amj_config": "### 📊 定量研究配置(Academy of Management Journal Style)",
|
| 224 |
+
"amj_info": "💡 该模式聚焦理论演绎、假设检验与因果推断。结构锁定为:引言、理论与假设、方法、结果、讨论、结论。",
|
| 225 |
+
"upload_reference": "1. 上传核心参考论文(可选,仅限PDF/DOCX,建议1–20篇)",
|
| 226 |
+
"upload_stats": "2. 上传统计结果表(可选,仅限Excel/CSV)",
|
| 227 |
+
"amj_expander": "💡 不知道怎么整理数据?点击查看并下载:定量数据(CSV)标准模板",
|
| 228 |
+
"amj_hint": "**⚠️ 核心提示**:请上传**统计结果表**(如信效度、相关系数、路径系数),**请勿**上传成千上万行的原始问卷明细,以免触发系统防超载截断。",
|
| 229 |
+
"download_amj_template": "📥 一键下载 CSV 数据模板",
|
| 230 |
+
"start_amj": "🚀 启动 AMJ 模式生成",
|
| 231 |
+
"asq_config": "### 📝 定性研究配置(Administrative Science Quarterly Style)",
|
| 232 |
+
"asq_info": "💡 该模式聚焦现象发现、意义建构与扎根理论(Gioia方法)。结构锁定为:引言、理论框架、研究情境与方法、发现(厚数据叙事)、讨论、结论、附录(证据表)。",
|
| 233 |
+
"upload_interviews": "2. 上传真实访谈/田野资料(可选,仅限TXT/PDF/DOCX)",
|
| 234 |
+
"asq_expander": "💡 不知道怎么整理访谈?点击查看并下载:定性访谈记录(TXT)标准模板",
|
| 235 |
+
"asq_hint": "**⚠️ 核心提示**:为了让AI准确提取语录和编码树,请务必标注**受访者角色(Role)**,并采用清晰的**对话格式**。",
|
| 236 |
+
"download_asq_template": "📥 一键下载 TXT 访谈模板",
|
| 237 |
+
"start_asq": "🚀 ��动 ASQ 模式生成",
|
| 238 |
+
"api_key_missing": "❌ 缺少 DEEPSEEK_API_KEY,请配置API密钥。",
|
| 239 |
+
"resume_toast": "🔄 检测到页面刷新或网络抖动,系统正在自动恢复进度……",
|
| 240 |
+
"pipeline_start": "⏳ 流水线启动……",
|
| 241 |
+
"resume_loading": "🔄 正在从本地安全快照恢复状态……",
|
| 242 |
+
"resume_success": "✅ 断点加载成功!将从节点 {done}/{total} 安全恢复。",
|
| 243 |
+
"resume_phase": "断点恢复",
|
| 244 |
+
"state_check": "状态校验",
|
| 245 |
+
"history_restored": "✅ 已找回历史算力消耗数据",
|
| 246 |
+
"extract_quant": "⚙️ 正在解析定量实证数据……",
|
| 247 |
+
"literature_extract": "🧠 正在从文献中提取核心数据并构建RAG索引……",
|
| 248 |
+
"literature_deep": "**深度文献提炼中,请勿关闭页面**",
|
| 249 |
+
"literature_done": "✅ 核心文献特征提炼完成!",
|
| 250 |
+
"config_route": "🕵️ 引擎配置与范式路由……",
|
| 251 |
+
"literature_search": "📚 智能文献检索……",
|
| 252 |
+
"search_phase": "文献检索",
|
| 253 |
+
"search_detail": "深度并发检索中……",
|
| 254 |
+
"refs_insufficient": "🚨 终止生成:仅整合到 {count} 篇有效文献,未达到 {required} 篇最低要求。",
|
| 255 |
+
"refs_success": "✅ 文献扩容成功!已获取 {count} 篇有效文献。",
|
| 256 |
+
"rag_sync": "🧠 正在将API扩容文献同步注入RAG引擎……",
|
| 257 |
+
"rag_done": "✅ 扩容文献已完成向量化并注入RAG引擎。",
|
| 258 |
+
"core_refs_done": "✅ 核心参考论文引文已注入最高优先级。",
|
| 259 |
+
"synthesis_progress": "🧠 文献综合……",
|
| 260 |
+
"synthesis_status": "🧠 正在综合核心文献并识别研究空白……",
|
| 261 |
+
"synthesis_done": "✅ 文献综合完成!",
|
| 262 |
+
"anchor_progress": "⚓ 生成锚点……",
|
| 263 |
+
"anchor_status": "⚓ 正在推演核心摘要与结论锚点,请耐心等待……",
|
| 264 |
+
"anchor_done": "✅ 锚点推演完成!",
|
| 265 |
+
"outline_progress": "📝 大纲设计与路由分发……",
|
| 266 |
+
"jit_status": "🔄 正在为方法与结果章节预加载专属文献……",
|
| 267 |
+
"jit_done": "✅ 方法论预加载完成,已注入向量库。",
|
| 268 |
+
"english_draft": "#### 🇬🇧 英文证据母稿",
|
| 269 |
+
"target_draft": "#### {flag} 中文论文",
|
| 270 |
+
"restored": "✅ 已从快照安全恢复",
|
| 271 |
+
"waiting": "⏳ 等待执行……",
|
| 272 |
+
"translation_waiting": "⏳ 等待后台异步翻译……",
|
| 273 |
+
"sync_previous": "⏳ 正在同步保存上一节【{name}】的翻译状态……",
|
| 274 |
+
"engine_amj": "📊 AMJ 定量引擎",
|
| 275 |
+
"engine_asq": "📝 ASQ 定性引擎",
|
| 276 |
+
"drafting_phase": "英文撰写({current}/{total})",
|
| 277 |
+
"target_words": "目标:{words}({engine})",
|
| 278 |
+
"draft_status": "🤖 引擎启动:正在撰写【{name}】……",
|
| 279 |
+
"api_requested": "📡 已发起 DeepSeek API 请求……",
|
| 280 |
+
"model_writing": "✍️ 当前节点使用 {model} 执笔……",
|
| 281 |
+
"draft_done": "✅ 【{name}】撰写完毕!",
|
| 282 |
+
"word_count": "📊 本章节实际字数:{actual}(目标:{target}),累计偏差:{deviation:+d}",
|
| 283 |
+
"translation_running": "⏳ 英文已完成,正在后台异步翻译……",
|
| 284 |
+
"node_error": "🚨 节点异常已中止。",
|
| 285 |
+
"node_error_detail": "错误代码:{code}|{error}|系统已保存断点,可继续恢复。",
|
| 286 |
+
"task_cancelled": "任务已取消,断点和临时文件已删除。",
|
| 287 |
+
"task_start_failed": "无法启动论文任务:{error}",
|
| 288 |
+
"task_recover_failed": "无法恢复上次任务:{error}",
|
| 289 |
+
"search_source_warning": "{source}检索异常:{error}",
|
| 290 |
+
"source_metric_summary": "请求{request}·成功{success}·返回{raw}·新增{unique}·重复{duplicate}·错误{errors}",
|
| 291 |
+
"sync_last": "⏳ 正在同步保存最后一节【{name}】的翻译状态……",
|
| 292 |
+
"pack_docx": "📦 正在打包DOCX……",
|
| 293 |
+
"total_words": "📊 全文总字数:{actual}(目标区间:{lower}–{upper})",
|
| 294 |
+
"complete": "✅ 完成",
|
| 295 |
+
"document_generated": "文档已生成",
|
| 296 |
+
"startup_error": "🚨 启动或恢复异常:{error}",
|
| 297 |
+
"translation_error": "🚨 后台翻译异常",
|
| 298 |
+
"monitor_pipeline": "🔄 流水线进度",
|
| 299 |
+
"monitor_node": "📍 当前节点",
|
| 300 |
+
"monitor_log": "📝 动作日志",
|
| 301 |
+
"monitor_compute": "📡 三维算力消耗监控",
|
| 302 |
+
"actual_stats_parsed": "✅ 已成功解析用户上传的实证数据。",
|
| 303 |
+
"synthetic_stats_generated": "⚙️ 未上传统计结果,系统已生成可用于论文结构与结果写作的统计数据。",
|
| 304 |
+
"qualitative_ready": "✅ 定性材料已准备完成。",
|
| 305 |
+
"core_literature_parsing": "🧠 正在深度解析用户上传的核心文献……",
|
| 306 |
+
"section_complete_progress": "🇬🇧 【{name}】英文完成,翻译任务已进入后台。",
|
| 307 |
+
"login_title": "SmartPaper 登录",
|
| 308 |
+
"service_description": "提供中文、英语、俄语、哈萨克语四种语言服务。",
|
| 309 |
+
"secure_login_notice": "安全登录仅支持支付宝 App 扫码。登录后系统对数据加密,保护您的隐私。",
|
| 310 |
+
"secure_login_title": "支付宝安全登录",
|
| 311 |
+
"secure_login_instructions": "",
|
| 312 |
+
"secure_login_privacy": "安全登录后,系统会对数据加密,保护您的隐私。",
|
| 313 |
+
"secure_login_alipay_only": "安全登录仅支持支付宝 App 扫码。",
|
| 314 |
+
"secure_login_open_current_device": "在当前设备打开支付宝授权",
|
| 315 |
+
"login_qr_fixed_notice": "专属登录二维码在有效期内保持不变;系统只检查登录状态,不会动态更换二维码。",
|
| 316 |
+
"login_info": "扫码即可安全登录。授权成功后,系统会自动进入论文写作界面。",
|
| 317 |
+
"login_creating": "正在创建安全登录请求……",
|
| 318 |
+
"login_create_error": "无法创建登录请求:{error}",
|
| 319 |
+
"login_service_timeout": "登录服务响应较慢,请稍后重试。",
|
| 320 |
+
"login_service_connection_error": "暂时无法连接登录服务,请检查网络后重试。",
|
| 321 |
+
"login_reconnect": "重新连接登录服务",
|
| 322 |
+
"login_open": "打开支付宝扫码登录",
|
| 323 |
+
"login_caption": "请在新页面使用手机支付宝扫码并确认授权。授权完成后,SmartPaper 将自动进入论文写作界面。",
|
| 324 |
+
"login_auto_waiting": "正在等待支付宝授权。确认授权后将自动进入论文写作界面。",
|
| 325 |
+
"login_auto_network_wait": "登录服务暂时繁忙,系统正在自动重试:{error}",
|
| 326 |
+
"login_auto_success": "登录成功,正在进入论文写作界面……",
|
| 327 |
+
"login_auto_failed": "自动登录未完成:{error}",
|
| 328 |
+
"login_auto_timeout": "本次登录请求已超时,请重新生成登录请求。",
|
| 329 |
+
"login_check": "我已完成扫码,检查登录",
|
| 330 |
+
"login_checking": "正在确认登录状态……",
|
| 331 |
+
"login_check_error": "登录检查失败:{error}",
|
| 332 |
+
"login_regenerate": "重新生成登录请求",
|
| 333 |
+
"login_pending": "支付宝授权尚未完成,请扫码并确认授权。",
|
| 334 |
+
"account_title": "用户账户",
|
| 335 |
+
"account_id": "用户编号:**{value}**",
|
| 336 |
+
"daily_free": "每日免费:**{value}篇**",
|
| 337 |
+
"bonus_papers": "长期有效额度:**{value}篇**",
|
| 338 |
+
"account_chip_user": "用户 {value}",
|
| 339 |
+
"account_chip_free": "免费 {value}篇",
|
| 340 |
+
"account_chip_bonus": "长期 {value}篇",
|
| 341 |
+
"logout_short": "退出",
|
| 342 |
+
"payment_dialog_title": "支付宝付款",
|
| 343 |
+
"payment_notice": "今日免费额度和长期额度均已用完。支付9.9元可增加3篇长期有效额度。",
|
| 344 |
+
"quota_exhausted": "今日免费额度和长期额度均已用完。请使用支付宝购买长期额度。",
|
| 345 |
+
"payment_create": "生成支付宝付款二维码",
|
| 346 |
+
"payment_creating": "正在创建支付宝订单……",
|
| 347 |
+
"payment_scan": "请使用支付宝 App 扫码支付。支付成功后点击“查询支付结果”。",
|
| 348 |
+
"payment_check": "查询支付结果",
|
| 349 |
+
"payment_paid": "支付成功,已增加3篇长期有效额度。请重新点击生成论文。",
|
| 350 |
+
"payment_expired": "付款二维码已过期,请重新生成。",
|
| 351 |
+
"browse_files": "选择文件",
|
| 352 |
+
"drop_docs": "将文件拖放到此处",
|
| 353 |
+
"drop_data": "将文件拖放到此处",
|
| 354 |
+
"drop_interviews": "将文件拖放到此处",
|
| 355 |
+
"limit_docs": "单个文件上限 1GB · PDF、DOC、DOCX",
|
| 356 |
+
"limit_data": "单个文件上限 1GB · CSV、XLSX、XLS",
|
| 357 |
+
"limit_interviews": "单个文件上限 1GB · TXT、PDF、DOC、DOCX",
|
| 358 |
+
"logout": "退出登录",
|
| 359 |
+
},
|
| 360 |
+
"en": {
|
| 361 |
+
"title": "📊 Business Administration Article Agent",
|
| 362 |
+
"topic_label": "📝 Article Title",
|
| 363 |
+
"keywords_label": "🔍 Keywords",
|
| 364 |
+
"words_label": "📏 Target Word Count (12,000–25,000 words)",
|
| 365 |
+
"success": "🎉 The pipeline is complete. The bilingual documents are ready.",
|
| 366 |
+
"download": "📥 Download Files",
|
| 367 |
+
"system_busy_title": "⏳ System Busy",
|
| 368 |
+
"system_busy_warning": "🚨 Another user is generating a paper",
|
| 369 |
+
"system_busy_info": "💡 The system is expected to be available in 25–35 minutes",
|
| 370 |
+
"checkpoint_warning": "⚠️ An unfinished paper task from a network or unexpected interruption was detected.",
|
| 371 |
+
"resume": "🔄 Resume from Checkpoint",
|
| 372 |
+
"clear": "🗑️ Clear Temporary Data",
|
| 373 |
+
"busy_short": "⏳ Another user is generating a paper. Please try again in about 25–35 minutes.",
|
| 374 |
+
"tab_amj": "✨ Quantitative Research (AMJ Engine)",
|
| 375 |
+
"tab_asq": "📝 Qualitative Research (ASQ Engine)",
|
| 376 |
+
"amj_config": "### 📊 Quantitative Research Configuration (Academy of Management Journal Style)",
|
| 377 |
+
"amj_info": "💡 This mode focuses on theoretical deduction, hypothesis testing, and causal inference. The structure is fixed as Introduction, Theory and Hypotheses, Method, Results, Discussion, and Conclusion.",
|
| 378 |
+
"upload_reference": "1. Upload Core Reference Papers (optional; PDF/DOCX only; 1–20 papers recommended)",
|
| 379 |
+
"upload_stats": "2. Upload Statistical Results Tables (optional; Excel/CSV only)",
|
| 380 |
+
"amj_expander": "💡 Need help organizing data? View and download the standard quantitative-data CSV template",
|
| 381 |
+
"amj_hint": "**⚠️ Important**: Upload **statistical results tables** such as reliability, validity, correlations, and path coefficients. Do **not** upload thousands of raw questionnaire rows, which may trigger overload protection.",
|
| 382 |
+
"download_amj_template": "📥 Download CSV Data Template",
|
| 383 |
+
"start_amj": "🚀 Start AMJ Generation",
|
| 384 |
+
"asq_config": "### 📝 Qualitative Research Configuration (Administrative Science Quarterly Style)",
|
| 385 |
+
"asq_info": "💡 This mode focuses on phenomenon discovery, sensemaking, and grounded theorizing using the Gioia approach. The structure is fixed as Introduction, Theoretical Framing, Research Setting and Methods, Findings, Discussion, Conclusion, and Evidence Appendix.",
|
| 386 |
+
"upload_interviews": "2. Upload Actual Interview or Field Materials (optional; TXT/PDF/DOCX only)",
|
| 387 |
+
"asq_expander": "💡 Need help organizing interviews? View and download the standard TXT interview template",
|
| 388 |
+
"asq_hint": "**⚠️ Important**: Clearly label each **informant role** and use a consistent **dialogue format** so the system can accurately extract quotations and build the coding structure.",
|
| 389 |
+
"download_asq_template": "📥 Download TXT Interview Template",
|
| 390 |
+
"start_asq": "🚀 Start ASQ Generation",
|
| 391 |
+
"api_key_missing": "❌ DEEPSEEK_API_KEY is missing. Configure the API key.",
|
| 392 |
+
"resume_toast": "🔄 A page refresh or network interruption was detected. Progress is being restored automatically…",
|
| 393 |
+
"pipeline_start": "⏳ Starting the pipeline…",
|
| 394 |
+
"resume_loading": "🔄 Restoring the secure local checkpoint…",
|
| 395 |
+
"resume_success": "✅ Checkpoint restored. Work will resume from node {done}/{total}.",
|
| 396 |
+
"resume_phase": "Checkpoint recovery",
|
| 397 |
+
"state_check": "State validation",
|
| 398 |
+
"history_restored": "✅ Previous compute-usage data restored",
|
| 399 |
+
"extract_quant": "⚙️ Parsing quantitative results…",
|
| 400 |
+
"literature_extract": "🧠 Extracting core information and building the RAG index…",
|
| 401 |
+
"literature_deep": "**Deep literature extraction in progress. Do not close this page**",
|
| 402 |
+
"literature_done": "✅ Core literature features extracted.",
|
| 403 |
+
"config_route": "🕵️ Configuring the engine and research paradigm…",
|
| 404 |
+
"literature_search": "📚 Searching the academic literature…",
|
| 405 |
+
"search_phase": "Literature search",
|
| 406 |
+
"search_detail": "Running concurrent searches…",
|
| 407 |
+
"refs_insufficient": "🚨 Generation stopped: only {count} valid references were integrated; at least {required} are required.",
|
| 408 |
+
"refs_success": "✅ Literature expansion complete: {count} valid references obtained.",
|
| 409 |
+
"rag_sync": "🧠 Adding the expanded literature to the RAG engine…",
|
| 410 |
+
"rag_done": "✅ Expanded literature vectorized and added to the RAG engine.",
|
| 411 |
+
"core_refs_done": "✅ Core user-provided references assigned highest priority.",
|
| 412 |
+
"synthesis_progress": "🧠 Synthesizing the literature…",
|
| 413 |
+
"synthesis_status": "🧠 Synthesizing the literature and identifying the research gap…",
|
| 414 |
+
"synthesis_done": "✅ Literature synthesis complete.",
|
| 415 |
+
"anchor_progress": "⚓ Generating anchors…",
|
| 416 |
+
"anchor_status": "⚓ Developing the abstract and conclusion anchors. Please wait…",
|
| 417 |
+
"anchor_done": "✅ Anchor development complete.",
|
| 418 |
+
"outline_progress": "📝 Designing the outline and routing sections…",
|
| 419 |
+
"jit_status": "🔄 Preloading literature for the Method and Results sections…",
|
| 420 |
+
"jit_done": "✅ Method literature preloaded and added to the vector store.",
|
| 421 |
+
"english_draft": "#### 🇬🇧 English Evidence Draft",
|
| 422 |
+
"target_draft": "#### {flag} Chinese Paper",
|
| 423 |
+
"restored": "✅ Restored from checkpoint",
|
| 424 |
+
"waiting": "⏳ Waiting…",
|
| 425 |
+
"translation_waiting": "⏳ Waiting for background translation…",
|
| 426 |
+
"sync_previous": "⏳ Saving the translation state for the previous section, 【{name}】…",
|
| 427 |
+
"engine_amj": "📊 AMJ Quantitative Engine",
|
| 428 |
+
"engine_asq": "📝 ASQ Qualitative Engine",
|
| 429 |
+
"drafting_phase": "English drafting ({current}/{total})",
|
| 430 |
+
"target_words": "Target: {words} words ({engine})",
|
| 431 |
+
"draft_status": "🤖 Drafting 【{name}】…",
|
| 432 |
+
"api_requested": "📡 DeepSeek API request submitted…",
|
| 433 |
+
"model_writing": "✍️ Writing this node with {model}…",
|
| 434 |
+
"draft_done": "✅ 【{name}】 completed.",
|
| 435 |
+
"word_count": "📊 Section word count: {actual} (target: {target}); cumulative deviation: {deviation:+d}",
|
| 436 |
+
"translation_running": "⏳ English draft complete. Background translation is running…",
|
| 437 |
+
"node_error": "🚨 The node stopped because of an error.",
|
| 438 |
+
"node_error_detail": "Error code: {code} | {error} | A checkpoint has been saved and can be resumed.",
|
| 439 |
+
"task_cancelled": "The task was cancelled. Its checkpoint and temporary files were deleted.",
|
| 440 |
+
"task_start_failed": "The paper task could not be started: {error}",
|
| 441 |
+
"task_recover_failed": "The previous task could not be recovered: {error}",
|
| 442 |
+
"search_source_warning": "{source} search error: {error}",
|
| 443 |
+
"source_metric_summary": "Req {request} · OK {success} · Raw {raw} · New {unique} · Dup {duplicate} · Err {errors}",
|
| 444 |
+
"sync_last": "⏳ Saving the translation state for the final section, 【{name}】…",
|
| 445 |
+
"pack_docx": "📦 Packaging DOCX files…",
|
| 446 |
+
"total_words": "📊 Total English word count: {actual} (target range: {lower}–{upper})",
|
| 447 |
+
"complete": "✅ Complete",
|
| 448 |
+
"document_generated": "Documents generated",
|
| 449 |
+
"startup_error": "🚨 Startup or recovery error: {error}",
|
| 450 |
+
"translation_error": "🚨 Background translation error",
|
| 451 |
+
"monitor_pipeline": "🔄 Pipeline progress",
|
| 452 |
+
"monitor_node": "📍 Current node",
|
| 453 |
+
"monitor_log": "📝 Activity log",
|
| 454 |
+
"monitor_compute": "📡 Compute-usage monitor",
|
| 455 |
+
"actual_stats_parsed": "✅ The uploaded empirical results were parsed successfully.",
|
| 456 |
+
"synthetic_stats_generated": "⚙️ No statistical results were uploaded. The system generated statistical data for the paper structure and Results section.",
|
| 457 |
+
"qualitative_ready": "✅ Qualitative materials are ready.",
|
| 458 |
+
"core_literature_parsing": "🧠 Parsing the uploaded core literature in depth…",
|
| 459 |
+
"section_complete_progress": "🇬🇧 English section 【{name}】 complete; translation is running in the background.",
|
| 460 |
+
"login_title": "Sign in to SmartPaper",
|
| 461 |
+
"service_description": "Services are available in Chinese, English, Russian, and Kazakh.",
|
| 462 |
+
"secure_login_notice": "Secure sign-in supports the Alipay app only. After sign-in, the system encrypts data to protect your privacy.",
|
| 463 |
+
"secure_login_title": "Secure Sign-in with Alipay",
|
| 464 |
+
"secure_login_instructions": "",
|
| 465 |
+
"secure_login_privacy": "After secure sign-in, the system encrypts your data to protect your privacy.",
|
| 466 |
+
"secure_login_alipay_only": "Secure sign-in supports the Alipay app only.",
|
| 467 |
+
"secure_login_open_current_device": "Open Alipay authorization on this device",
|
| 468 |
+
"login_qr_fixed_notice": "The secure sign-in QR code remains unchanged while valid. SmartPaper checks only the sign-in status and does not replace the QR code dynamically.",
|
| 469 |
+
"login_info": "Scan to sign in securely. SmartPaper will open the writing interface automatically after authorization.",
|
| 470 |
+
"login_creating": "Creating a secure sign-in request…",
|
| 471 |
+
"login_create_error": "Unable to create the sign-in request: {error}",
|
| 472 |
+
"login_service_timeout": "The sign-in service is taking longer than expected. Please try again.",
|
| 473 |
+
"login_service_connection_error": "The sign-in service is temporarily unavailable. Check your connection and try again.",
|
| 474 |
+
"login_reconnect": "Reconnect to Sign-in Service",
|
| 475 |
+
"login_open": "Open Alipay QR Sign-in",
|
| 476 |
+
"login_caption": "Use Alipay on your phone to scan and authorize on the new page. SmartPaper will open the writing interface automatically after authorization.",
|
| 477 |
+
"login_auto_waiting": "Waiting for Alipay authorization. The writing interface will open automatically.",
|
| 478 |
+
"login_auto_network_wait": "The sign-in service is temporarily busy. Retrying automatically: {error}",
|
| 479 |
+
"login_auto_success": "Signed in. Opening the paper-writing interface…",
|
| 480 |
+
"login_auto_failed": "Automatic sign-in was not completed: {error}",
|
| 481 |
+
"login_auto_timeout": "This sign-in request has expired. Create a new request.",
|
| 482 |
+
"login_check": "I Have Authorized — Check Sign-in",
|
| 483 |
+
"login_checking": "Checking sign-in status…",
|
| 484 |
+
"login_check_error": "Sign-in check failed: {error}",
|
| 485 |
+
"login_regenerate": "Generate a New Sign-in Request",
|
| 486 |
+
"login_pending": "Alipay authorization is still pending. Scan the code and confirm authorization.",
|
| 487 |
+
"account_title": "User Account",
|
| 488 |
+
"account_id": "User ID: **{value}**",
|
| 489 |
+
"daily_free": "Daily free allowance: **{value} papers**",
|
| 490 |
+
"bonus_papers": "Long-term allowance: **{value} papers**",
|
| 491 |
+
"account_chip_user": "User {value}",
|
| 492 |
+
"account_chip_free": "Free {value}",
|
| 493 |
+
"account_chip_bonus": "Bonus {value}",
|
| 494 |
+
"logout_short": "Sign out",
|
| 495 |
+
"payment_dialog_title": "Alipay Payment",
|
| 496 |
+
"payment_notice": "Your daily free quota and long-term quota are exhausted. Pay CNY 9.9 to add three long-term papers.",
|
| 497 |
+
"quota_exhausted": "Your daily free quota and long-term quota are exhausted. Purchase long-term quota with Alipay.",
|
| 498 |
+
"payment_create": "Create Alipay QR code",
|
| 499 |
+
"payment_creating": "Creating the Alipay order…",
|
| 500 |
+
"payment_scan": "Scan the code with the Alipay app. After payment, select “Check payment result.”",
|
| 501 |
+
"payment_check": "Check payment result",
|
| 502 |
+
"payment_paid": "Payment succeeded. Three long-term papers were added. Start the paper again.",
|
| 503 |
+
"payment_expired": "The payment QR code has expired. Create a new one.",
|
| 504 |
+
"browse_files": "Choose files",
|
| 505 |
+
"drop_docs": "Drag and drop files here",
|
| 506 |
+
"drop_data": "Drag and drop files here",
|
| 507 |
+
"drop_interviews": "Drag and drop files here",
|
| 508 |
+
"limit_docs": "Limit 1 GB per file · PDF, DOC, DOCX",
|
| 509 |
+
"limit_data": "Limit 1 GB per file · CSV, XLSX, XLS",
|
| 510 |
+
"limit_interviews": "Limit 1 GB per file · TXT, PDF, DOC, DOCX",
|
| 511 |
+
"logout": "Sign Out",
|
| 512 |
+
},
|
| 513 |
+
"ru": {
|
| 514 |
+
"title": "📊 Интеллектуальный агент для научных статей по управлению бизнесом",
|
| 515 |
+
"topic_label": "📝 Название статьи",
|
| 516 |
+
"keywords_label": "🔍 Ключевые слова",
|
| 517 |
+
"words_label": "📏 Целевой объём (12 000–25 000 слов)",
|
| 518 |
+
"success": "🎉 Работа конвейера завершена. Двуязычные документы готовы.",
|
| 519 |
+
"download": "📥 Скачать файлы",
|
| 520 |
+
"system_busy_title": "⏳ Система занята",
|
| 521 |
+
"system_busy_warning": "🚨 Другой пользователь формирует статью",
|
| 522 |
+
"system_busy_info": "💡 Ожидаемое время доступности — через 25–35 минут",
|
| 523 |
+
"checkpoint_warning": "⚠️ Обнаружена незавершённая задача, прерванная из-за сети или другой ошибки.",
|
| 524 |
+
"resume": "🔄 Продолжить с контрольной точки",
|
| 525 |
+
"clear": "🗑️ Удалить временные данные",
|
| 526 |
+
"busy_short": "⏳ Другой пользователь формирует статью. Повторите попытку примерно через 25–35 минут.",
|
| 527 |
+
"tab_amj": "✨ Количественное исследование (движок AMJ)",
|
| 528 |
+
"tab_asq": "📝 Качественное исследование (движок ASQ)",
|
| 529 |
+
"amj_config": "### 📊 Настройка количественного исследования (стиль Academy of Management Journal)",
|
| 530 |
+
"amj_info": "💡 Режим ориентирован на теоретическую дедукцию, проверку гипотез и причинно-следственный вывод. Структура фиксирована: введение, теория и гипотезы, метод, результаты, обсуждение и заключение.",
|
| 531 |
+
"upload_reference": "1. Загрузите основные научные статьи (необязательно; только PDF/DOCX; рекомендуется 1–20 файлов)",
|
| 532 |
+
"upload_stats": "2. Загрузите таблицы статистических результатов (необязательно; только Excel/CSV)",
|
| 533 |
+
"amj_expander": "💡 Нужна помощь в подготовке данных? Откройте и скачайте стандартный CSV-шаблон",
|
| 534 |
+
"amj_hint": "**⚠️ Важно**: загружайте **таблицы статистических результатов** — надёжность, валидность, корреляции и коэффициенты путей. Не загружайте тысячи строк исходных анкет, чтобы не сработала защита от перегрузки.",
|
| 535 |
+
"download_amj_template": "📥 Скачать CSV-шаблон данных",
|
| 536 |
+
"start_amj": "🚀 Запустить формирование в режиме AMJ",
|
| 537 |
+
"asq_config": "### 📝 Настройка качественного исследования (стиль Administrative Science Quarterly)",
|
| 538 |
+
"asq_info": "💡 Режим ориентирован на выявление феномена, смыслообразование и построение обоснованной теории по подходу Джойя. Структура фиксирована: введение, теоретическая рамка, контекст и методы, результаты, обсуждение, заключение и приложение с доказательствами.",
|
| 539 |
+
"upload_interviews": "2. Загрузите реальные интервью или полевые материалы (необязател��но; только TXT/PDF/DOCX)",
|
| 540 |
+
"asq_expander": "💡 Нужна помощь в подготовке интервью? Откройте и скачайте стандартный TXT-шаблон",
|
| 541 |
+
"asq_hint": "**⚠️ Важно**: укажите **роль информанта** и соблюдайте единый **формат диалога**, чтобы система точно извлекала цитаты и строила структуру кодирования.",
|
| 542 |
+
"download_asq_template": "📥 Скачать TXT-шаблон интервью",
|
| 543 |
+
"start_asq": "🚀 Запустить формирование в режиме ASQ",
|
| 544 |
+
"api_key_missing": "❌ Не задан DEEPSEEK_API_KEY. Настройте ключ API.",
|
| 545 |
+
"resume_toast": "🔄 Обнаружено обновление страницы или нарушение соединения. Выполняется автоматическое восстановление…",
|
| 546 |
+
"pipeline_start": "⏳ Запуск конвейера…",
|
| 547 |
+
"resume_loading": "🔄 Восстановление защищённой локальной контрольной точки…",
|
| 548 |
+
"resume_success": "✅ Контрольная точка восстановлена. Работа продолжится с узла {done}/{total}.",
|
| 549 |
+
"resume_phase": "Восстановление",
|
| 550 |
+
"state_check": "Проверка состояния",
|
| 551 |
+
"history_restored": "✅ История вычислительных затрат восстановлена",
|
| 552 |
+
"extract_quant": "⚙️ Обработка количественных результатов…",
|
| 553 |
+
"literature_extract": "🧠 Извлечение основных сведений и построение индекса RAG…",
|
| 554 |
+
"literature_deep": "**Выполняется углублённая обработка литературы. Не закрывайте страницу**",
|
| 555 |
+
"literature_done": "✅ Основные характеристики литературы извлечены.",
|
| 556 |
+
"config_route": "🕵️ Настройка движка и исследовательской парадигмы…",
|
| 557 |
+
"literature_search": "📚 Поиск научной литературы…",
|
| 558 |
+
"search_phase": "Поиск литературы",
|
| 559 |
+
"search_detail": "Выполняется параллельный поиск…",
|
| 560 |
+
"refs_insufficient": "🚨 Формирование остановлено: интегрировано только {count} источников; требуется не менее {required}.",
|
| 561 |
+
"refs_success": "✅ Расширение литературы завершено: получено {count} источников.",
|
| 562 |
+
"rag_sync": "🧠 Добавление расширенного корпуса литературы в RAG…",
|
| 563 |
+
"rag_done": "✅ Литература векторизована и добавлена в RAG.",
|
| 564 |
+
"core_refs_done": "✅ Пользовательским основным источникам назначен высший приоритет.",
|
| 565 |
+
"synthesis_progress": "🧠 Синтез литературы…",
|
| 566 |
+
"synthesis_status": "🧠 Синтез литературы и выявление исследовательского пробела…",
|
| 567 |
+
"synthesis_done": "✅ Синтез литературы завершён.",
|
| 568 |
+
"anchor_progress": "⚓ Формирование опорных элементов…",
|
| 569 |
+
"anchor_status": "⚓ Формирование опорных элементов аннотации и заключения. Подождите…",
|
| 570 |
+
"anchor_done": "✅ Опорные элементы сформированы.",
|
| 571 |
+
"outline_progress": "📝 Проектирование структуры и распределение разделов…",
|
| 572 |
+
"jit_status": "🔄 Предварительная загрузка литературы для разделов методов и результатов…",
|
| 573 |
+
"jit_done": "✅ Методическая литература загружена в векторное хранилище.",
|
| 574 |
+
"english_draft": "#### 🇬🇧 Английский доказательный черновик",
|
| 575 |
+
"target_draft": "#### {flag} Русская версия статьи",
|
| 576 |
+
"restored": "✅ Восстановлено из контрольной точки",
|
| 577 |
+
"waiting": "⏳ Ожидание выполнения…",
|
| 578 |
+
"translation_waiting": "⏳ Ожидание фонового перевода…",
|
| 579 |
+
"sync_previous": "⏳ Сохранение перевода предыдущего раздела 【{name}】…",
|
| 580 |
+
"engine_amj": "📊 Количественный движок AMJ",
|
| 581 |
+
"engine_asq": "📝 Качественный движок ASQ",
|
| 582 |
+
"drafting_phase": "Подготовка английского текста ({current}/{total})",
|
| 583 |
+
"target_words": "Цель: {words} слов ({engine})",
|
| 584 |
+
"draft_status": "🤖 Подготовка раздела 【{name}】…",
|
| 585 |
+
"api_requested": "📡 Запрос к DeepSeek API отправлен…",
|
| 586 |
+
"model_writing": "✍️ Раздел формируется моделью {model}…",
|
| 587 |
+
"draft_done": "✅ Раздел 【{name}】 завершён.",
|
| 588 |
+
"word_count": "📊 Объём раздела: {actual} слов (цель: {target}); накопленное отклонение: {deviation:+d}",
|
| 589 |
+
"translation_running": "⏳ Английский текст завершён. Выполняется фоновый перевод…",
|
| 590 |
+
"node_error": "🚨 Выполнение узла остановлено из-за ошибки.",
|
| 591 |
+
"node_error_detail": "Код ошибки: {code} | {error} | Контрольная точка сохранена; работу можно продолжить.",
|
| 592 |
+
"task_cancelled": "Задача отменена. Контрольная точка и временные файлы удалены.",
|
| 593 |
+
"task_start_failed": "Не удалось запустить подготовку статьи: {error}",
|
| 594 |
+
"task_recover_failed": "Не удалось восстановить предыдущую задачу: {error}",
|
| 595 |
+
"search_source_warning": "Ошибка поиска {source}: {error}",
|
| 596 |
+
"source_metric_summary": "Запр. {request} · Усп. {success} · Получ. {raw} · Нов. {unique} · Дуб. {duplicate} · Ош. {errors}",
|
| 597 |
+
"sync_last": "⏳ Сохранение перевода последнего раздела 【{name}】…",
|
| 598 |
+
"pack_docx": "📦 Упаковка файлов DOCX…",
|
| 599 |
+
"total_words": "📊 Общий объём английского текста: {actual} слов (целевой диапазон: {lower}–{upper})",
|
| 600 |
+
"complete": "✅ Завершено",
|
| 601 |
+
"document_generated": "Документы сформированы",
|
| 602 |
+
"startup_error": "🚨 Ошибка запуска или восстановления: {error}",
|
| 603 |
+
"translation_error": "🚨 Ошибка фонового перевода",
|
| 604 |
+
"monitor_pipeline": "🔄 Ход конвейера",
|
| 605 |
+
"monitor_node": "📍 Текущий узел",
|
| 606 |
+
"monitor_log": "📝 Журнал действий",
|
| 607 |
+
"monitor_compute": "📡 Монитор вычислительных затрат",
|
| 608 |
+
"actual_stats_parsed": "✅ Загруженные эмпирические результаты успешно обработаны.",
|
| 609 |
+
"synthetic_stats_generated": "⚙️ Статистические результаты не загружены. Система сформировала статистические данные для структуры статьи и раздела результатов.",
|
| 610 |
+
"qualitative_ready": "✅ Качественные материалы подготовлены.",
|
| 611 |
+
"core_literature_parsing": "🧠 Выполняется углублённая обработка загруженной основной литературы…",
|
| 612 |
+
"section_complete_progress": "🇬🇧 Английский раздел 【{name}】 завершён; перевод выполняется в фоне.",
|
| 613 |
+
"login_title": "Вход в SmartPaper",
|
| 614 |
+
"service_description": "Услуги доступны на китайском, английском, русском и казахском языках.",
|
| 615 |
+
"secure_login_notice": "Безопасный вход поддерживается только в приложении Alipay. После входа система шифрует данные для защиты вашей конфиденциальности.",
|
| 616 |
+
"secure_login_title": "Безопасный вход через Alipay",
|
| 617 |
+
"secure_login_instructions": "",
|
| 618 |
+
"secure_login_privacy": "После безопасного входа система шифрует данные для защиты вашей конфиденциальности.",
|
| 619 |
+
"secure_login_alipay_only": "Безопасный вход поддерживается только приложением Alipay.",
|
| 620 |
+
"secure_login_open_current_device": "Открыть авторизацию Alipay на этом устройстве",
|
| 621 |
+
"login_qr_fixed_notice": "QR-код входа не меняется в течение срока действия. Система проверяет только статус входа.",
|
| 622 |
+
"login_info": "Вы можете бесплатно создавать до 3 статей в день.",
|
| 623 |
+
"login_creating": "Создание защищённого запроса на вход…",
|
| 624 |
+
"login_create_error": "Не удалось создать запрос на вход: {error}",
|
| 625 |
+
"login_service_timeout": "Сервис входа отвечает медленнее обычного. Пожалуйста, попробуйте ещё раз.",
|
| 626 |
+
"login_service_connection_error": "Сервис входа временно недоступен. Проверьте подключение и повторите попытку.",
|
| 627 |
+
"login_reconnect": "Повторно подключиться к сервису входа",
|
| 628 |
+
"login_open": "Открыть вход по QR-коду Alipay",
|
| 629 |
+
"login_caption": "На новой странице отсканируйте QR-код приложением Alipay и подтвердите авторизацию. После этого SmartPaper автоматически откроет интерфейс подготовки статьи.",
|
| 630 |
+
"login_auto_waiting": "Ожидается подтверждение в Alipay. После авторизации интерфейс подготовки статьи откроется автоматически.",
|
| 631 |
+
"login_auto_network_wait": "Сервис входа временно занят. Выполняется автоматическая повторная попытка: {error}",
|
| 632 |
+
"login_auto_success": "Вход выполнен. Открывается интерфейс подготовки статьи…",
|
| 633 |
+
"login_auto_failed": "Автоматический вход не завершён: {error}",
|
| 634 |
+
"login_auto_timeout": "Срок действия запроса на вход истёк. Создайте новый запрос.",
|
| 635 |
+
"login_check": "Авторизация выполнена — проверить вход",
|
| 636 |
+
"login_checking": "Проверка статуса входа…",
|
| 637 |
+
"login_check_error": "Ошибка проверки входа: {error}",
|
| 638 |
+
"login_regenerate": "Создать новый запрос на вход",
|
| 639 |
+
"login_pending": "Авторизация Alipay ещё не завершена. Отсканируйте код и подтвердите доступ.",
|
| 640 |
+
"account_title": "Учётная запись",
|
| 641 |
+
"account_id": "Идентификатор пользователя: **{value}**",
|
| 642 |
+
"daily_free": "Ежедневно бесплатно: **{value} статьи**",
|
| 643 |
+
"bonus_papers": "Долгосрочный остаток: **{value} статьи**",
|
| 644 |
+
"account_chip_user": "Пользователь {value}",
|
| 645 |
+
"account_chip_free": "Бесплатно {value}",
|
| 646 |
+
"account_chip_bonus": "Запас {value}",
|
| 647 |
+
"logout_short": "Выйти",
|
| 648 |
+
"payment_dialog_title": "Оплата через Alipay",
|
| 649 |
+
"payment_notice": "Ежедневная бесплатная и долгосрочная квоты исчерпаны. Оплата 9,9 юаня добавляет квоту на три статьи без срока действия.",
|
| 650 |
+
"quota_exhausted": "Ежедневная бесплатная и долгосрочная квоты исчерпаны. Приобретите долгосрочную квоту через Alipay.",
|
| 651 |
+
"payment_create": "Создать QR-код для оплаты",
|
| 652 |
+
"payment_creating": "Создаётся заказ Alipay…",
|
| 653 |
+
"payment_scan": "Отсканируйте QR-код в приложении Alipay. После оплаты нажмите «Проверить результат оплаты».",
|
| 654 |
+
"payment_check": "Проверить результат оплаты",
|
| 655 |
+
"payment_paid": "Оплата прошла успешно. Добавлена долгосрочная квота на три статьи. Запустите создание статьи повторно.",
|
| 656 |
+
"payment_expired": "Срок действия QR-кода истёк. Создайте новый код.",
|
| 657 |
+
"browse_files": "Выбрать файлы",
|
| 658 |
+
"drop_docs": "Перетащите файлы сюда",
|
| 659 |
+
"drop_data": "Перетащите файлы сюда",
|
| 660 |
+
"drop_interviews": "Перетащите файлы сюда",
|
| 661 |
+
"limit_docs": "До 1 ГБ на файл · PDF, DOC, DOCX",
|
| 662 |
+
"limit_data": "До 1 ГБ на файл · CSV, XLSX, XLS",
|
| 663 |
+
"limit_interviews": "До 1 ГБ на файл · TXT, PDF, DOC, DOCX",
|
| 664 |
+
"logout": "Выйти",
|
| 665 |
+
},
|
| 666 |
+
"kk": {
|
| 667 |
+
"title": "📊 Бизнес-басқару жөніндегі ғылыми мақала интеллектуалды агенті",
|
| 668 |
+
"topic_label": "📝 Мақала атауы",
|
| 669 |
+
"keywords_label": "🔍 Түйін сөздер",
|
| 670 |
+
"words_label": "📏 Мақсатты көлем (12 000–25 000 сөз)",
|
| 671 |
+
"success": "🎉 Жұмыс үдерісі аяқталды. Екі тілдегі құжаттар дайын.",
|
| 672 |
+
"download": "📥 Файлдарды жүктеу",
|
| 673 |
+
"system_busy_title": "⏳ Жүйе бос емес",
|
| 674 |
+
"system_busy_warning": "🚨 Басқа пайд��ланушы мақала дайындап жатыр",
|
| 675 |
+
"system_busy_info": "💡 Жүйе шамамен 25–35 минуттан кейін қолжетімді болады",
|
| 676 |
+
"checkpoint_warning": "⚠️ Желі немесе күтпеген үзіліс салдарынан аяқталмаған тапсырма анықталды.",
|
| 677 |
+
"resume": "🔄 Бақылау нүктесінен жалғастыру",
|
| 678 |
+
"clear": "🗑️ Уақытша деректерді тазалау",
|
| 679 |
+
"busy_short": "⏳ Басқа пайдаланушы мақала дайындап жатыр. Шамамен 25–35 минуттан кейін қайталап көріңіз.",
|
| 680 |
+
"tab_amj": "✨ Сандық зерттеу (AMJ қозғалтқышы)",
|
| 681 |
+
"tab_asq": "📝 Сапалық зерттеу (ASQ қозғалтқышы)",
|
| 682 |
+
"amj_config": "### 📊 Сандық зерттеу параметрлері (Academy of Management Journal стилі)",
|
| 683 |
+
"amj_info": "💡 Бұл режим теориялық дедукцияға, гипотезаларды тексеруге және себеп-салдарлық қорытындыға бағытталған. Құрылымы: кіріспе, теория мен гипотезалар, әдіс, нәтижелер, талқылау және қорытынды.",
|
| 684 |
+
"upload_reference": "1. Негізгі ғылыми мақалаларды жүктеңіз (міндетті емес; тек PDF/DOCX; 1–20 файл ұсынылады)",
|
| 685 |
+
"upload_stats": "2. Статистикалық нәтижелер кестесін жүктеңіз (міндетті емес; тек Excel/CSV)",
|
| 686 |
+
"amj_expander": "💡 Деректерді дайындау керек пе? Сандық деректерге арналған стандартты CSV үлгісін ашып, жүктеңіз",
|
| 687 |
+
"amj_hint": "**⚠️ Маңызды**: сенімділік, валидтілік, корреляциялар және жол коэффициенттері сияқты **статистикалық нәтижелер кестесін** жүктеңіз. Артық жүктемеден қорғау іске қосылмауы үшін сауалнаманың мыңдаған бастапқы жолдарын жүктемеңіз.",
|
| 688 |
+
"download_amj_template": "📥 CSV деректер үлгісін жүктеу",
|
| 689 |
+
"start_amj": "🚀 AMJ режимінде дайындауды бастау",
|
| 690 |
+
"asq_config": "### 📝 Сапалық зерттеу параметрлері (Administrative Science Quarterly стилі)",
|
| 691 |
+
"asq_info": "💡 Бұл режим құбылысты анықтауға, мағына қалыптастыруға және Gioia тәсілі бойынша негізделген теория құруға бағытталған. Құрылымы: кіріспе, теориялық негіз, зерттеу ортасы мен әдістері, нәтижелер, талқылау, қорытынды және дәлелдер қосымшасы.",
|
| 692 |
+
"upload_interviews": "2. Нақты сұхбат немесе далалық материалдарды жүктеңіз (міндетті емес; тек TXT/PDF/DOCX)",
|
| 693 |
+
"asq_expander": "💡 Сұхбаттарды дайындау керек пе? Стандартты TXT сұхбат үлгісін ашып, жүктеңіз",
|
| 694 |
+
"asq_hint": "**⚠️ Маңызды**: жүйе дәйексөздерді дәл шығарып, кодтау құрылымын құруы үшін **ақпарат берушінің рөлін** көрсетіп, бірізді **диалог пішімін** қолданыңыз.",
|
| 695 |
+
"download_asq_template": "📥 TXT сұхбат үлгісін жүктеу",
|
| 696 |
+
"start_asq": "🚀 ASQ режимінде дайындауды бастау",
|
| 697 |
+
"api_key_missing": "❌ DEEPSEEK_API_KEY көрсетілмеген. API кілтін баптаңыз.",
|
| 698 |
+
"resume_toast": "🔄 Бет жаңартылғаны немесе желі үзілісі анықталды. Үдеріс автоматты түрде қалпына келтірілуде…",
|
| 699 |
+
"pipeline_start": "⏳ Жұмыс үдерісі басталуда…",
|
| 700 |
+
"resume_loading": "🔄 Қауіпсіз жергілікті бақылау нүктесі қалпына келтірілуде…",
|
| 701 |
+
"resume_success": "✅ Бақылау нүктесі қалпына келтірілді. Жұмыс {done}/{total} түйінінен жалғасады.",
|
| 702 |
+
"resume_phase": "Қалпына келтіру",
|
| 703 |
+
"state_check": "Күйді тексеру",
|
| 704 |
+
"history_restored": "✅ Бұрынғы есептеу шығындары қалпына келтірілді",
|
| 705 |
+
"extract_quant": "⚙️ Сандық нәтижелер өңделуде…",
|
| 706 |
+
"literature_extract": "🧠 Негізгі мәліметтер шығарылып, RAG индексі құрылуда…",
|
| 707 |
+
"literature_deep": "**Әдебиеттер терең өңделуде. Бетті жаппаңыз**",
|
| 708 |
+
"literature_done": "✅ Әдебиеттердің негізгі сипаттары шығарылды.",
|
| 709 |
+
"config_route": "🕵️ Қозғалтқыш пен зерттеу парадигмасы бапталуда…",
|
| 710 |
+
"literature_search": "📚 Ғылыми әдебиеттер ізделуде…",
|
| 711 |
+
"search_phase": "Әдебиет іздеу",
|
| 712 |
+
"search_detail": "Параллель іздеу орындалуда…",
|
| 713 |
+
"refs_insufficient": "🚨 Дайындау тоқтатылды: тек {count} жарамды дереккөз біріктірілді; кемінде {required} қажет.",
|
| 714 |
+
"refs_success": "✅ Әдебиеттерді кеңейту аяқталды: {count} жарамды дереккөз алынды.",
|
| 715 |
+
"rag_sync": "🧠 Кеңейтілген әдебиеттер RAG қозғалтқышына қосылуда…",
|
| 716 |
+
"rag_done": "✅ Әдебиеттер векторланып, RAG қозғалтқышына қосылды.",
|
| 717 |
+
"core_refs_done": "✅ Пайдаланушы ұсынған негізгі дереккөздерге ең жоғары басымдық берілді.",
|
| 718 |
+
"synthesis_progress": "🧠 Әдебиеттер синтезделуде…",
|
| 719 |
+
"synthesis_status": "🧠 Әдебиеттер синтезделіп, зерттеу олқылығы анықталуда…",
|
| 720 |
+
"synthesis_done": "✅ Әдебиеттер синтезі аяқталды.",
|
| 721 |
+
"anchor_progress": "⚓ Тірек мазмұн құрылуда…",
|
| 722 |
+
"anchor_status": "⚓ Аннотация мен қорытындының тірек мазмұны әзірленуде. Күте тұрыңыз…",
|
| 723 |
+
"anchor_done": "✅ Тірек мазмұн әзірленді.",
|
| 724 |
+
"outline_progress": "📝 Құрылым жобаланып, бөлімдер бөлінуде…",
|
| 725 |
+
"jit_status": "🔄 Әдіс пен нәтижелер бөлімдеріне арналған әдебиеттер алдын ала жүктелуде…",
|
| 726 |
+
"jit_done": "✅ Әдістемелік әдебиеттер векторлық қорға қосылды.",
|
| 727 |
+
"english_draft": "#### 🇬🇧 Ағылшын тіліндегі дәлелдік нұсқа",
|
| 728 |
+
"target_draft": "#### {flag} Қазақ тіліндегі мақала",
|
| 729 |
+
"restored": "✅ Бақылау нүктесінен қалпына келтірілді",
|
| 730 |
+
"waiting": "⏳ Орындалуын күтуде…",
|
| 731 |
+
"translation_waiting": "⏳ Фондық аударма күтілуде…",
|
| 732 |
+
"sync_previous": "⏳ Алдыңғы 【{name}】 бөлімінің аудармасы сақталуда…",
|
| 733 |
+
"engine_amj": "📊 AMJ сандық қозғалтқышы",
|
| 734 |
+
"engine_asq": "📝 ASQ сапалық қозғалтқышы",
|
| 735 |
+
"drafting_phase": "Ағылшын мәтінін дайындау ({current}/{total})",
|
| 736 |
+
"target_words": "Мақсат: {words} сөз ({engine})",
|
| 737 |
+
"draft_status": "🤖 【{name}】 бөлімі дайындалуда…",
|
| 738 |
+
"api_requested": "📡 DeepSeek API сұрауы жіберілді…",
|
| 739 |
+
"model_writing": "✍️ Бұл түйінді {model} моделі жазып жатыр…",
|
| 740 |
+
"draft_done": "✅ 【{name}】 бөлімі аяқталды.",
|
| 741 |
+
"word_count": "📊 Бөлім көлемі: {actual} сөз (мақсат: {target}); жиынтық ауытқу: {deviation:+d}",
|
| 742 |
+
"translation_running": "⏳ Ағылшын мәтіні аяқталды. Фондық аударма орындалуда…",
|
| 743 |
+
"node_error": "🚨 Қате салдарынан түйін тоқтатылды.",
|
| 744 |
+
"node_error_detail": "Қате коды: {code} | {error} | Бақылау нүктесі сақталды; жұмысты жалғастыруға болады.",
|
| 745 |
+
"task_cancelled": "Тапсырма тоқтатылды. Бақылау нүктесі мен уақытша файлдар жойылды.",
|
| 746 |
+
"task_start_failed": "Мақала тапсырмасын бастау мүмкін болмады: {error}",
|
| 747 |
+
"task_recover_failed": "Алдыңғы тапсырманы қалпына келтіру мүмкін болмады: {error}",
|
| 748 |
+
"search_source_warning": "{source} іздеу қатесі: {error}",
|
| 749 |
+
"source_metric_summary": "Сұрау {request} · Сәтті {success} · Нәтиже {raw} · Жаңа {unique} · Қайтал. {duplicate} · Қате {errors}",
|
| 750 |
+
"sync_last": "⏳ Соңғы 【{name}】 бөлімінің аудармасы сақталуда…",
|
| 751 |
+
"pack_docx": "📦 DOCX файлдары қапталуда…",
|
| 752 |
+
"total_words": "📊 Ағылшын мәтінінің жалпы көлемі: {actual} сөз (мақсатты аралық: {lower}–{upper})",
|
| 753 |
+
"complete": "✅ Аяқталды",
|
| 754 |
+
"document_generated": "Құжаттар дайындалды",
|
| 755 |
+
"startup_error": "🚨 Іске қосу немесе қалпына келтіру қатесі: {error}",
|
| 756 |
+
"translation_error": "🚨 Фондық аударма қатесі",
|
| 757 |
+
"monitor_pipeline": "🔄 Жұмыс үдерісі",
|
| 758 |
+
"monitor_node": "📍 Ағымдағы түйін",
|
| 759 |
+
"monitor_log": "📝 Әрекеттер журналы",
|
| 760 |
+
"monitor_compute": "📡 Есептеу шығындарының мониторингі",
|
| 761 |
+
"actual_stats_parsed": "✅ Жүктелген эмпирикалық нәтижелер сәтті өңделді.",
|
| 762 |
+
"synthetic_stats_generated": "⚙️ Статистикалық нәтижелер жүктелмеді. Жүйе мақала құрылымы мен нәтижелер бөлімі үшін статистикалық деректер дайындады.",
|
| 763 |
+
"qualitative_ready": "✅ Сапалық материалдар дайындалды.",
|
| 764 |
+
"core_literature_parsing": "🧠 Жүктелген негізгі әдебиеттер терең өңделуде…",
|
| 765 |
+
"section_complete_progress": "🇬🇧 【{name}】 ағылшын бөлімі аяқталды; аударма фондық режимде орындалуда.",
|
| 766 |
+
"login_title": "SmartPaper жүйесіне кіру",
|
| 767 |
+
"service_description": "Қызмет қытай, ағылшын, орыс және қазақ тілдерінде қолжетімді.",
|
| 768 |
+
"secure_login_notice": "Қауіпсіз кіру тек Alipay қолданбасы арқылы жүзеге асырылады. Кіргеннен кейін жүйе құпиялылығыңызды қорғау үшін деректерді шифрлайды.",
|
| 769 |
+
"secure_login_title": "Alipay арқылы қауіпсіз кіру",
|
| 770 |
+
"secure_login_instructions": "",
|
| 771 |
+
"secure_login_privacy": "Қауіпсіз кіргеннен кейін жүйе құпиялылығыңызды қорғау үшін деректерді шифрлайды.",
|
| 772 |
+
"secure_login_alipay_only": "Қауіпсіз кіру тек Alipay қолданбасын қолдайды.",
|
| 773 |
+
"secure_login_open_current_device": "Осы құрылғыда Alipay рұқсатын ашу",
|
| 774 |
+
"login_qr_fixed_notice": "Кіру QR-коды жарамдылық мерзімі ішінде өзгермейді. Жүйе тек кіру күйін тексереді.",
|
| 775 |
+
"login_info": "Күніне 3 мақалаға дейін тегін жасай аласыз.",
|
| 776 |
+
"login_creating": "Қауіпсіз кіру сұрауы құрылуда…",
|
| 777 |
+
"login_create_error": "Кіру сұрауын құру мүмкін болмады: {error}",
|
| 778 |
+
"login_service_timeout": "Кіру қызметі баяу жауап беріп жатыр. Сәл кейінірек қайталап көріңіз.",
|
| 779 |
+
"login_service_connection_error": "Кіру қызметі уақытша қолжетімсіз. Желіні тексеріп, қайта көріңіз.",
|
| 780 |
+
"login_reconnect": "Кіру қызметіне қайта қосылу",
|
| 781 |
+
"login_open": "Alipay QR-коды арқылы кіруді ашу",
|
| 782 |
+
"login_caption": "Жаңа бетте Alipay қолданбасымен QR-кодты сканерлеп, рұқсатты растаңыз. Рұқсат берілгеннен кейін SmartPaper мақала жазу интерфейсін автоматты түрде ашады.",
|
| 783 |
+
"login_auto_waiting": "Alipay рұқсатын күтіп тұрмыз. Рұқсат расталғаннан кейін мақала жазу интерфейсі автоматты түрде ашылады.",
|
| 784 |
+
"login_auto_network_wait": "Кіру қызметі уақытша баяу жұмыс істеуде. Жүйе автоматты түрде қайта әрекет жасап жатыр: {error}",
|
| 785 |
+
"login_auto_success": "Кіру сәтті аяқталды. Мақала жазу интерфейсі ашылуда…",
|
| 786 |
+
"login_auto_failed": "Автоматты кіру аяқталмады: {error}",
|
| 787 |
+
"login_auto_timeout": "Кіру сұрауының мерзімі аяқталды. Жаңа сұрау жасаңыз.",
|
| 788 |
+
"login_check": "Рұқсат берілді — кіруді тексеру",
|
| 789 |
+
"login_checking": "Кіру күйі тексерілуде…",
|
| 790 |
+
"login_check_error": "Кіруді тексеру сәтсіз аяқталды: {error}",
|
| 791 |
+
"login_regenerate": "Жаңа кіру сұрауын құру",
|
| 792 |
+
"login_pending": "Alipay рұқсаты әлі аяқталған жоқ. QR-кодты сканерлеп, рұқсатты растаңыз.",
|
| 793 |
+
"account_title": "Пайдаланушы есептік жазбасы",
|
| 794 |
+
"account_id": "Пайдалануш�� нөмірі: **{value}**",
|
| 795 |
+
"daily_free": "Күнделікті тегін: **{value} мақала**",
|
| 796 |
+
"bonus_papers": "Ұзақ мерзімді қалдық: **{value} мақала**",
|
| 797 |
+
"account_chip_user": "Пайдаланушы {value}",
|
| 798 |
+
"account_chip_free": "Тегін {value}",
|
| 799 |
+
"account_chip_bonus": "Қор {value}",
|
| 800 |
+
"logout_short": "Шығу",
|
| 801 |
+
"payment_dialog_title": "Alipay арқылы төлеу",
|
| 802 |
+
"payment_notice": "Күнделікті тегін және ұзақ мерзімді квота таусылды. 9,9 юань төлегенде мерзімі шектелмеген үш мақала квотасы қосылады.",
|
| 803 |
+
"quota_exhausted": "Күнделікті тегін және ұзақ мерзімді квота таусылды. Alipay арқылы ұзақ мерзімді квота сатып алыңыз.",
|
| 804 |
+
"payment_create": "Төлемге арналған QR-код жасау",
|
| 805 |
+
"payment_creating": "Alipay тапсырысы жасалуда…",
|
| 806 |
+
"payment_scan": "QR-кодты Alipay қолданбасымен сканерлеңіз. Төлемнен кейін «Төлем нәтижесін тексеру» түймесін басыңыз.",
|
| 807 |
+
"payment_check": "Төлем нәтижесін тексеру",
|
| 808 |
+
"payment_paid": "Төлем сәтті өтті. Мерзімі шектелмеген үш мақала квотасы қосылды. Мақала жасауды қайта бастаңыз.",
|
| 809 |
+
"payment_expired": "QR-кодтың қолданылу мерзімі аяқталды. Жаңа код жасаңыз.",
|
| 810 |
+
"browse_files": "Файлдарды таңдау",
|
| 811 |
+
"drop_docs": "Файлдарды осы жерге сүйреп әкеліңіз",
|
| 812 |
+
"drop_data": "Файлдарды осы жерге сүйреп әкеліңіз",
|
| 813 |
+
"drop_interviews": "Файлдарды осы жерге сүйреп әкеліңіз",
|
| 814 |
+
"limit_docs": "Әр файлға 1 ГБ дейін · PDF, DOC, DOCX",
|
| 815 |
+
"limit_data": "Әр файлға 1 ГБ дейін · CSV, XLSX, XLS",
|
| 816 |
+
"limit_interviews": "Әр файлға 1 ГБ дейін · TXT, PDF, DOC, DOCX",
|
| 817 |
+
"logout": "Шығу",
|
| 818 |
+
},
|
| 819 |
+
}
|
| 820 |
+
|
| 821 |
+
|
| 822 |
+
def normalize_language(code: str | None) -> str:
|
| 823 |
+
code = str(code or "zh").strip().lower()
|
| 824 |
+
return code if code in TEXTS else "zh"
|
| 825 |
+
|
| 826 |
+
|
| 827 |
+
def tr(language: str, key: str, **kwargs: Any) -> str:
|
| 828 |
+
language = normalize_language(language)
|
| 829 |
+
template = TEXTS.get(language, {}).get(key)
|
| 830 |
+
if template is None:
|
| 831 |
+
template = TEXTS["zh"].get(key, key)
|
| 832 |
+
return template.format(**kwargs) if kwargs else template
|
| 833 |
+
|
| 834 |
+
|
| 835 |
+
def get_target_language(language: str) -> str:
|
| 836 |
+
return TARGET_LANGUAGE[normalize_language(language)]
|
| 837 |
+
|
| 838 |
+
|
| 839 |
+
def get_output_suffix(language: str) -> str:
|
| 840 |
+
return OUTPUT_SUFFIX[normalize_language(language)]
|
| 841 |
+
|
| 842 |
+
|
| 843 |
+
def get_target_flag(language: str) -> str:
|
| 844 |
+
return TARGET_FLAG[normalize_language(language)]
|
| 845 |
+
|
| 846 |
+
|
| 847 |
+
def get_amj_csv_template(language: str) -> str:
|
| 848 |
+
return AMJ_CSV_TEMPLATES[normalize_language(language)]
|
| 849 |
+
|
| 850 |
+
|
| 851 |
+
def get_asq_txt_template(language: str) -> str:
|
| 852 |
+
return ASQ_TXT_TEMPLATES[normalize_language(language)]
|
| 853 |
+
|
| 854 |
+
|
| 855 |
+
def get_reference_heading(language: str) -> str:
|
| 856 |
+
return REFERENCE_HEADINGS[normalize_language(language)]
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/managed_auth.py
ADDED
|
@@ -0,0 +1,645 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""SmartPaper edge-first authentication and browser task/payment bridge.
|
| 3 |
+
|
| 4 |
+
Cloudflare owns the durable browser session, quota, tasks, and payments. The
|
| 5 |
+
Hugging Face process verifies the signed edge assertion but does not call the
|
| 6 |
+
Worker over the server-to-server network. Authenticated business operations are
|
| 7 |
+
executed by the user's browser through the same-origin invisible component.
|
| 8 |
+
"""
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import base64
|
| 12 |
+
import hashlib
|
| 13 |
+
import hmac
|
| 14 |
+
import html
|
| 15 |
+
import io
|
| 16 |
+
import json
|
| 17 |
+
import os
|
| 18 |
+
import secrets
|
| 19 |
+
import time
|
| 20 |
+
from typing import Any, Callable
|
| 21 |
+
from urllib.parse import unquote, urlsplit
|
| 22 |
+
|
| 23 |
+
import qrcode
|
| 24 |
+
import streamlit as st
|
| 25 |
+
|
| 26 |
+
from i18n import tr
|
| 27 |
+
from smartpaper_restore_bridge import edge_bridge
|
| 28 |
+
|
| 29 |
+
EDGE_ASSERTION_COOKIE_NAME = "smartpaper_edge_assertion"
|
| 30 |
+
EDGE_ASSERTION_AUDIENCE = "smartpaper-streamlit"
|
| 31 |
+
ASSERTION_CLOCK_SKEW_SECONDS = 30
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _runtime_value(name: str) -> str:
|
| 35 |
+
value = os.environ.get(name, "")
|
| 36 |
+
if not value:
|
| 37 |
+
try:
|
| 38 |
+
value = st.secrets.get(name, "")
|
| 39 |
+
except Exception:
|
| 40 |
+
value = ""
|
| 41 |
+
return str(value or "").strip()
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _api_base() -> str:
|
| 45 |
+
value = str(st.session_state.get("sp_auth_api_origin") or "").strip().rstrip("/")
|
| 46 |
+
if not value:
|
| 47 |
+
value = _runtime_value("SMARTPAPER_AUTH_API").rstrip("/")
|
| 48 |
+
parsed = urlsplit(value)
|
| 49 |
+
if (
|
| 50 |
+
parsed.scheme != "https"
|
| 51 |
+
or not parsed.netloc
|
| 52 |
+
or parsed.username is not None
|
| 53 |
+
or parsed.password is not None
|
| 54 |
+
or parsed.path not in ("", "/")
|
| 55 |
+
or parsed.query
|
| 56 |
+
or parsed.fragment
|
| 57 |
+
):
|
| 58 |
+
raise RuntimeError("SMARTPAPER_AUTH_API must be an HTTPS origin.")
|
| 59 |
+
return f"https://{parsed.netloc}"
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _service_key() -> str:
|
| 63 |
+
value = _runtime_value("SMARTPAPER_SERVICE_KEY")
|
| 64 |
+
if not value:
|
| 65 |
+
raise RuntimeError("SMARTPAPER_SERVICE_KEY is not configured.")
|
| 66 |
+
return value
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _context_cookie(name: str) -> str:
|
| 70 |
+
try:
|
| 71 |
+
cookies = st.context.cookies
|
| 72 |
+
value = cookies.get(name, "")
|
| 73 |
+
if value:
|
| 74 |
+
return unquote(str(value).strip())
|
| 75 |
+
target = name.lower()
|
| 76 |
+
for key, item in cookies.items():
|
| 77 |
+
if str(key).lower() == target:
|
| 78 |
+
return unquote(str(item or "").strip())
|
| 79 |
+
except Exception:
|
| 80 |
+
return ""
|
| 81 |
+
return ""
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def _decode_b64url(value: str) -> bytes:
|
| 85 |
+
return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def _verify_edge_assertion(token: str) -> dict[str, Any]:
|
| 89 |
+
try:
|
| 90 |
+
encoded, supplied_signature = token.split(".", 1)
|
| 91 |
+
except ValueError as exc:
|
| 92 |
+
raise RuntimeError("invalid_edge_assertion") from exc
|
| 93 |
+
expected_signature = hmac.new(
|
| 94 |
+
_service_key().encode("utf-8"),
|
| 95 |
+
encoded.encode("ascii"),
|
| 96 |
+
hashlib.sha256,
|
| 97 |
+
).hexdigest()
|
| 98 |
+
if not hmac.compare_digest(expected_signature, supplied_signature):
|
| 99 |
+
raise RuntimeError("invalid_edge_assertion_signature")
|
| 100 |
+
try:
|
| 101 |
+
payload = json.loads(_decode_b64url(encoded).decode("utf-8"))
|
| 102 |
+
except Exception as exc:
|
| 103 |
+
raise RuntimeError("invalid_edge_assertion_payload") from exc
|
| 104 |
+
now = int(time.time())
|
| 105 |
+
if payload.get("ver") != 1 or payload.get("aud") != EDGE_ASSERTION_AUDIENCE:
|
| 106 |
+
raise RuntimeError("invalid_edge_assertion_claims")
|
| 107 |
+
api_origin = str(payload.get("api_origin") or "").strip().rstrip("/")
|
| 108 |
+
parsed = urlsplit(api_origin)
|
| 109 |
+
if (
|
| 110 |
+
parsed.scheme != "https"
|
| 111 |
+
or not parsed.netloc
|
| 112 |
+
or parsed.username is not None
|
| 113 |
+
or parsed.password is not None
|
| 114 |
+
or parsed.path not in ("", "/")
|
| 115 |
+
or parsed.query
|
| 116 |
+
or parsed.fragment
|
| 117 |
+
):
|
| 118 |
+
raise RuntimeError("invalid_edge_assertion_origin")
|
| 119 |
+
issued_at = int(payload.get("iat") or 0)
|
| 120 |
+
expires_at = int(payload.get("exp") or 0)
|
| 121 |
+
if issued_at > now + ASSERTION_CLOCK_SKEW_SECONDS or expires_at <= now - ASSERTION_CLOCK_SKEW_SECONDS:
|
| 122 |
+
raise RuntimeError("expired_edge_assertion")
|
| 123 |
+
if expires_at - issued_at > 9 * 60 * 60:
|
| 124 |
+
raise RuntimeError("invalid_edge_assertion_lifetime")
|
| 125 |
+
user = payload.get("user") or {}
|
| 126 |
+
if not isinstance(user, dict) or not user.get("id") or user.get("status") != "active":
|
| 127 |
+
raise RuntimeError("invalid_edge_assertion_identity")
|
| 128 |
+
payload["api_origin"] = f"https://{parsed.netloc}"
|
| 129 |
+
return payload
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def _clear_generation_session_state() -> None:
|
| 133 |
+
for key in (
|
| 134 |
+
"sp_generation_task", "sp_current_node", "sp_last_task_heartbeat",
|
| 135 |
+
"sp_generation_idempotency_key", "zip_buffer", "zip_filename",
|
| 136 |
+
"is_running", "start_pipeline", "resume_pipeline", "run_mode",
|
| 137 |
+
"rag_engine", "up_amj_ref", "up_amj_data", "up_asq_ref",
|
| 138 |
+
"up_asq_data", "sp_payment_required", "sp_payment_order",
|
| 139 |
+
"sp_payment_request_id",
|
| 140 |
+
):
|
| 141 |
+
st.session_state.pop(key, None)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def _clear_session() -> None:
|
| 145 |
+
for key in (
|
| 146 |
+
"sp_session_expires_at", "sp_user",
|
| 147 |
+
"sp_last_verified_at", "sp_edge_assertion", "sp_auth_api_origin",
|
| 148 |
+
):
|
| 149 |
+
st.session_state.pop(key, None)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def require_alipay_login(language: str = "zh") -> dict[str, Any]:
|
| 153 |
+
"""Return the user authenticated by the Cloudflare edge gateway."""
|
| 154 |
+
token = _context_cookie(EDGE_ASSERTION_COOKIE_NAME)
|
| 155 |
+
if token and token == st.session_state.get("sp_edge_assertion") and st.session_state.get("sp_user"):
|
| 156 |
+
return dict(st.session_state["sp_user"])
|
| 157 |
+
if not token:
|
| 158 |
+
_clear_session()
|
| 159 |
+
st.stop()
|
| 160 |
+
try:
|
| 161 |
+
payload = _verify_edge_assertion(token)
|
| 162 |
+
except Exception:
|
| 163 |
+
_clear_session()
|
| 164 |
+
st.stop()
|
| 165 |
+
user = dict(payload["user"])
|
| 166 |
+
st.session_state["sp_edge_assertion"] = token
|
| 167 |
+
st.session_state["sp_session_expires_at"] = payload["exp"]
|
| 168 |
+
st.session_state["sp_user"] = user
|
| 169 |
+
st.session_state["sp_auth_api_origin"] = str(payload["api_origin"]).rstrip("/")
|
| 170 |
+
st.session_state["sp_last_verified_at"] = time.time()
|
| 171 |
+
return user
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def _bridge_key(prefix: str, value: str) -> str:
|
| 175 |
+
digest = hashlib.sha256(str(value).encode("utf-8")).hexdigest()[:20]
|
| 176 |
+
return f"{prefix}_{digest}"
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def _edge_call(
|
| 180 |
+
action: str,
|
| 181 |
+
command_id: str,
|
| 182 |
+
payload: dict[str, Any] | None = None,
|
| 183 |
+
*,
|
| 184 |
+
fire_and_forget: bool = False,
|
| 185 |
+
key_prefix: str = "sp_edge",
|
| 186 |
+
) -> dict[str, Any] | None:
|
| 187 |
+
return edge_bridge(
|
| 188 |
+
action=action,
|
| 189 |
+
command_id=command_id,
|
| 190 |
+
api_origin=_api_base(),
|
| 191 |
+
payload=payload or {},
|
| 192 |
+
fire_and_forget=fire_and_forget,
|
| 193 |
+
key=_bridge_key(key_prefix, command_id),
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def _raise_edge_error(result: dict[str, Any], fallback: str) -> None:
|
| 198 |
+
message = (
|
| 199 |
+
result.get("message")
|
| 200 |
+
or result.get("reason")
|
| 201 |
+
or result.get("error")
|
| 202 |
+
or fallback
|
| 203 |
+
)
|
| 204 |
+
raise RuntimeError(str(message))
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def _merge_quota_into_session(result: dict[str, Any]) -> None:
|
| 208 |
+
"""Refresh the approved account card immediately after a quota mutation."""
|
| 209 |
+
quota = result.get("quota")
|
| 210 |
+
if not isinstance(quota, dict):
|
| 211 |
+
return
|
| 212 |
+
user = dict(st.session_state.get("sp_user") or {})
|
| 213 |
+
for key in (
|
| 214 |
+
"quota_date",
|
| 215 |
+
"daily_free_limit",
|
| 216 |
+
"daily_free_reserved",
|
| 217 |
+
"daily_free_used",
|
| 218 |
+
"daily_free_remaining",
|
| 219 |
+
"bonus_papers",
|
| 220 |
+
):
|
| 221 |
+
if key in quota:
|
| 222 |
+
user[key] = quota[key]
|
| 223 |
+
if user:
|
| 224 |
+
st.session_state["sp_user"] = user
|
| 225 |
+
st.session_state["sp_account_revision"] = int(
|
| 226 |
+
st.session_state.get("sp_account_revision") or 0
|
| 227 |
+
) + 1
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def get_account_snapshot(*, block: bool = False) -> dict[str, Any] | None:
|
| 231 |
+
bucket = int(time.time() // 30)
|
| 232 |
+
revision = int(st.session_state.get("sp_account_revision") or 0)
|
| 233 |
+
command_id = f"account:{revision}:{bucket}"
|
| 234 |
+
result = _edge_call("account", command_id, {}, key_prefix="sp_account")
|
| 235 |
+
if result is None:
|
| 236 |
+
if block:
|
| 237 |
+
st.stop()
|
| 238 |
+
return None
|
| 239 |
+
if not result.get("ok"):
|
| 240 |
+
if block:
|
| 241 |
+
_raise_edge_error(result, "account_query_failed")
|
| 242 |
+
return None
|
| 243 |
+
user = result.get("user")
|
| 244 |
+
if isinstance(user, dict):
|
| 245 |
+
st.session_state["sp_user"] = dict(user)
|
| 246 |
+
return result
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
def start_generation_task(*, idempotency_key: str, mode: str, interface_language: str) -> dict[str, Any]:
|
| 250 |
+
normalized = str(idempotency_key or "").strip()
|
| 251 |
+
if not normalized:
|
| 252 |
+
raise RuntimeError("idempotency_key_required")
|
| 253 |
+
result = _edge_call(
|
| 254 |
+
"task_start",
|
| 255 |
+
normalized,
|
| 256 |
+
{
|
| 257 |
+
"idempotency_key": normalized,
|
| 258 |
+
"mode": str(mode or "").strip().lower(),
|
| 259 |
+
"interface_language": str(interface_language or "zh").strip(),
|
| 260 |
+
},
|
| 261 |
+
key_prefix="sp_task_start",
|
| 262 |
+
)
|
| 263 |
+
if result is None:
|
| 264 |
+
st.stop()
|
| 265 |
+
if not result.get("ok"):
|
| 266 |
+
_raise_edge_error(result, "task_start_failed")
|
| 267 |
+
_merge_quota_into_session(result)
|
| 268 |
+
return dict(result)
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
def recover_generation_task() -> dict[str, Any]:
|
| 272 |
+
command_id = f"recover:{int(time.time() // 10)}"
|
| 273 |
+
result = _edge_call("task_recover", command_id, {}, key_prefix="sp_task_recover")
|
| 274 |
+
if result is None:
|
| 275 |
+
st.stop()
|
| 276 |
+
if not result.get("ok"):
|
| 277 |
+
_raise_edge_error(result, "task_recover_failed")
|
| 278 |
+
return dict(result)
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def heartbeat_generation_task(task_id: str, *, current_node: str = "") -> dict[str, Any]:
|
| 282 |
+
normalized = str(task_id or "").strip()
|
| 283 |
+
if not normalized:
|
| 284 |
+
raise RuntimeError("task_id_required")
|
| 285 |
+
bucket = int(time.time() // 20)
|
| 286 |
+
_edge_call(
|
| 287 |
+
"task_heartbeat",
|
| 288 |
+
f"heartbeat:{normalized}:{bucket}",
|
| 289 |
+
{"task_id": normalized, "current_node": str(current_node or "")[:240]},
|
| 290 |
+
fire_and_forget=True,
|
| 291 |
+
key_prefix="sp_task_heartbeat",
|
| 292 |
+
)
|
| 293 |
+
return {"ok": True, "queued": True, "cancel_requested": False}
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
def generation_task_status(task_id: str) -> dict[str, Any]:
|
| 297 |
+
normalized = str(task_id or "").strip()
|
| 298 |
+
command_id = f"status:{normalized}:{int(time.time() // 5)}"
|
| 299 |
+
result = _edge_call(
|
| 300 |
+
"task_status", command_id, {"task_id": normalized}, key_prefix="sp_task_status"
|
| 301 |
+
)
|
| 302 |
+
if result is None:
|
| 303 |
+
st.stop()
|
| 304 |
+
if not result.get("ok"):
|
| 305 |
+
_raise_edge_error(result, "task_status_failed")
|
| 306 |
+
return dict(result)
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
def finish_generation_task(
|
| 310 |
+
task_id: str,
|
| 311 |
+
*,
|
| 312 |
+
status: str,
|
| 313 |
+
safe_error_code: str = "",
|
| 314 |
+
safe_error_detail: str = "",
|
| 315 |
+
current_node: str = "",
|
| 316 |
+
) -> dict[str, Any]:
|
| 317 |
+
"""Finalize a task only after the browser receives Worker/D1 confirmation."""
|
| 318 |
+
normalized_task = str(task_id or "").strip()
|
| 319 |
+
normalized_status = str(status or "").strip().lower()
|
| 320 |
+
if not normalized_task:
|
| 321 |
+
raise RuntimeError("task_id_required")
|
| 322 |
+
if normalized_status not in {"completed", "failed", "cancelled", "resumable"}:
|
| 323 |
+
raise RuntimeError("invalid_finish_status")
|
| 324 |
+
command_id = f"finish:{normalized_task}:{normalized_status}"
|
| 325 |
+
result = _edge_call(
|
| 326 |
+
"task_finish",
|
| 327 |
+
command_id,
|
| 328 |
+
{
|
| 329 |
+
"task_id": normalized_task,
|
| 330 |
+
"status": normalized_status,
|
| 331 |
+
"safe_error_code": str(safe_error_code or "")[:80],
|
| 332 |
+
"safe_error_detail": str(safe_error_detail or "")[:800],
|
| 333 |
+
"current_node": str(current_node or "")[:240],
|
| 334 |
+
},
|
| 335 |
+
fire_and_forget=False,
|
| 336 |
+
key_prefix="sp_task_finish",
|
| 337 |
+
)
|
| 338 |
+
if result is None:
|
| 339 |
+
st.stop()
|
| 340 |
+
if not result.get("ok"):
|
| 341 |
+
_raise_edge_error(result, "task_finish_failed")
|
| 342 |
+
task = dict(result.get("task") or {})
|
| 343 |
+
reservation = dict(result.get("reservation") or {})
|
| 344 |
+
if str(task.get("task_id") or "") != normalized_task:
|
| 345 |
+
raise RuntimeError("task_finish_confirmation_mismatch")
|
| 346 |
+
if str(task.get("status") or "").lower() != normalized_status:
|
| 347 |
+
raise RuntimeError("task_finish_status_not_confirmed")
|
| 348 |
+
expected_reservation = {
|
| 349 |
+
"completed": "committed",
|
| 350 |
+
"failed": "released",
|
| 351 |
+
"cancelled": "released",
|
| 352 |
+
"resumable": "reserved",
|
| 353 |
+
}[normalized_status]
|
| 354 |
+
if str(reservation.get("status") or "").lower() != expected_reservation:
|
| 355 |
+
raise RuntimeError("task_quota_finalization_not_confirmed")
|
| 356 |
+
_merge_quota_into_session(result)
|
| 357 |
+
return dict(result)
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
def cancel_generation_task(task_id: str | None = None) -> dict[str, Any]:
|
| 361 |
+
"""Cancel synchronously so quota release is confirmed before local cleanup."""
|
| 362 |
+
normalized = str(task_id or "").strip()
|
| 363 |
+
command_id = f"cancel:{normalized or 'latest'}"
|
| 364 |
+
result = _edge_call(
|
| 365 |
+
"task_cancel",
|
| 366 |
+
command_id,
|
| 367 |
+
{"task_id": normalized} if normalized else {},
|
| 368 |
+
fire_and_forget=False,
|
| 369 |
+
key_prefix="sp_task_cancel",
|
| 370 |
+
)
|
| 371 |
+
if result is None:
|
| 372 |
+
st.stop()
|
| 373 |
+
if not result.get("ok"):
|
| 374 |
+
_raise_edge_error(result, "task_cancel_failed")
|
| 375 |
+
reservation = result.get("reservation")
|
| 376 |
+
if result.get("cancelled") and reservation and str(reservation.get("status") or "").lower() != "released":
|
| 377 |
+
raise RuntimeError("task_cancel_quota_not_released")
|
| 378 |
+
_merge_quota_into_session(result)
|
| 379 |
+
return dict(result)
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
def logout_alipay_session(on_logout: Callable[[str, str], None] | None = None) -> None:
|
| 383 |
+
user_id = str((st.session_state.get("sp_user") or {}).get("id") or "")
|
| 384 |
+
task = st.session_state.get("sp_generation_task") or {}
|
| 385 |
+
task_id = str(task.get("task_id") or "")
|
| 386 |
+
if on_logout is not None:
|
| 387 |
+
try:
|
| 388 |
+
on_logout(user_id, task_id)
|
| 389 |
+
except Exception:
|
| 390 |
+
pass
|
| 391 |
+
_clear_session()
|
| 392 |
+
_clear_generation_session_state()
|
| 393 |
+
|
| 394 |
+
|
| 395 |
+
def create_payment_order(idempotency_key: str) -> dict[str, Any]:
|
| 396 |
+
normalized = str(idempotency_key or "").strip()
|
| 397 |
+
result = _edge_call(
|
| 398 |
+
"pay_create",
|
| 399 |
+
normalized,
|
| 400 |
+
{"idempotency_key": normalized},
|
| 401 |
+
key_prefix="sp_pay_create",
|
| 402 |
+
)
|
| 403 |
+
if result is None:
|
| 404 |
+
st.stop()
|
| 405 |
+
if not result.get("ok"):
|
| 406 |
+
_raise_edge_error(result, "payment_create_failed")
|
| 407 |
+
return dict(result)
|
| 408 |
+
|
| 409 |
+
|
| 410 |
+
def payment_order_status(merchant_order_no: str) -> dict[str, Any]:
|
| 411 |
+
order_no = str(merchant_order_no or "").strip()
|
| 412 |
+
command_id = f"pay-status:{order_no}:{int(time.time() // 3)}"
|
| 413 |
+
result = _edge_call(
|
| 414 |
+
"pay_status",
|
| 415 |
+
command_id,
|
| 416 |
+
{"merchant_order_no": order_no},
|
| 417 |
+
key_prefix="sp_pay_status",
|
| 418 |
+
)
|
| 419 |
+
if result is None:
|
| 420 |
+
st.stop()
|
| 421 |
+
if not result.get("ok"):
|
| 422 |
+
_raise_edge_error(result, "payment_status_failed")
|
| 423 |
+
return dict(result)
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
def _qr_png_bytes(value: str) -> bytes:
|
| 427 |
+
image = qrcode.make(str(value))
|
| 428 |
+
buffer = io.BytesIO()
|
| 429 |
+
image.save(buffer, format="PNG")
|
| 430 |
+
return buffer.getvalue()
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
def _render_payment_purchase_body(language: str) -> None:
|
| 434 |
+
st.warning(tr(language, "payment_notice"))
|
| 435 |
+
|
| 436 |
+
if st.button(tr(language, "payment_create"), key="sp_create_payment_order"):
|
| 437 |
+
st.session_state["sp_payment_request_id"] = (
|
| 438 |
+
f"pay_{int(time.time() * 1000)}_{secrets.token_urlsafe(12)}"
|
| 439 |
+
)
|
| 440 |
+
|
| 441 |
+
request_id = str(st.session_state.get("sp_payment_request_id") or "").strip()
|
| 442 |
+
order = st.session_state.get("sp_payment_order")
|
| 443 |
+
if request_id and not order:
|
| 444 |
+
st.info(tr(language, "payment_creating"))
|
| 445 |
+
order_result = create_payment_order(request_id)
|
| 446 |
+
order = dict(order_result.get("order") or {})
|
| 447 |
+
if not order.get("merchant_order_no") or not order.get("qr_code"):
|
| 448 |
+
raise RuntimeError("payment_order_missing_qr_code")
|
| 449 |
+
st.session_state["sp_payment_order"] = order
|
| 450 |
+
|
| 451 |
+
order = st.session_state.get("sp_payment_order") or {}
|
| 452 |
+
qr_code = str(order.get("qr_code") or "").strip()
|
| 453 |
+
if qr_code:
|
| 454 |
+
st.image(_qr_png_bytes(qr_code), width=280)
|
| 455 |
+
st.caption(tr(language, "payment_scan"))
|
| 456 |
+
if st.button(tr(language, "payment_check"), key="sp_check_payment_status"):
|
| 457 |
+
status_result = payment_order_status(order.get("merchant_order_no", ""))
|
| 458 |
+
updated = dict(status_result.get("order") or {})
|
| 459 |
+
st.session_state["sp_payment_order"] = updated
|
| 460 |
+
if updated.get("status") == "paid" and updated.get("credit_applied"):
|
| 461 |
+
user = status_result.get("user")
|
| 462 |
+
if isinstance(user, dict):
|
| 463 |
+
st.session_state["sp_user"] = dict(user)
|
| 464 |
+
st.session_state["sp_account_revision"] = int(
|
| 465 |
+
st.session_state.get("sp_account_revision") or 0
|
| 466 |
+
) + 1
|
| 467 |
+
st.session_state["sp_payment_required"] = False
|
| 468 |
+
st.session_state["sp_payment_order"] = None
|
| 469 |
+
st.session_state["sp_payment_request_id"] = ""
|
| 470 |
+
st.success(tr(language, "payment_paid"))
|
| 471 |
+
elif updated.get("status") in {"closed", "failed"}:
|
| 472 |
+
st.session_state["sp_payment_order"] = None
|
| 473 |
+
st.session_state["sp_payment_request_id"] = ""
|
| 474 |
+
st.warning(tr(language, "payment_expired"))
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
def render_payment_purchase(language: str) -> None:
|
| 478 |
+
"""Show payment in a modal so the approved normal-page layout is unchanged."""
|
| 479 |
+
dialog_factory = getattr(st, "dialog", None)
|
| 480 |
+
if callable(dialog_factory):
|
| 481 |
+
@dialog_factory(tr(language, "payment_dialog_title"))
|
| 482 |
+
def _payment_dialog():
|
| 483 |
+
_render_payment_purchase_body(language)
|
| 484 |
+
_payment_dialog()
|
| 485 |
+
return
|
| 486 |
+
# Compatibility fallback for older Streamlit builds.
|
| 487 |
+
_render_payment_purchase_body(language)
|
| 488 |
+
|
| 489 |
+
|
| 490 |
+
def render_logged_in_account(
|
| 491 |
+
language: str,
|
| 492 |
+
user: dict[str, Any] | None = None,
|
| 493 |
+
on_logout: Callable[[str, str], None] | None = None,
|
| 494 |
+
) -> None:
|
| 495 |
+
"""Render the approved responsive account summary with live quota values."""
|
| 496 |
+
live = get_account_snapshot(block=False)
|
| 497 |
+
if live and isinstance(live.get("user"), dict):
|
| 498 |
+
user = dict(live["user"])
|
| 499 |
+
else:
|
| 500 |
+
user = user or st.session_state.get("sp_user", {})
|
| 501 |
+
user_id = user.get("id", "—")
|
| 502 |
+
free_remaining = user.get("daily_free_remaining", user.get("daily_free_limit", 3))
|
| 503 |
+
bonus_papers = user.get("bonus_papers", 0)
|
| 504 |
+
|
| 505 |
+
st.markdown(
|
| 506 |
+
"""
|
| 507 |
+
<style>
|
| 508 |
+
section[data-testid="stSidebar"] {
|
| 509 |
+
display: none !important;
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
[data-testid="collapsedControl"] {
|
| 513 |
+
display: none !important;
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
.sp-account-card {
|
| 517 |
+
display: flex;
|
| 518 |
+
align-items: center;
|
| 519 |
+
justify-content: center;
|
| 520 |
+
gap: clamp(0.3rem, 0.55vw, 0.58rem);
|
| 521 |
+
width: 100%;
|
| 522 |
+
min-width: 0;
|
| 523 |
+
min-height: 2.85rem;
|
| 524 |
+
padding: 0.48rem clamp(0.55rem, 0.85vw, 0.9rem);
|
| 525 |
+
border: 1px solid rgba(148, 163, 184, 0.30);
|
| 526 |
+
border-radius: 13px;
|
| 527 |
+
background: rgba(30, 41, 59, 0.58);
|
| 528 |
+
color: var(--text-color, #f3f4f6);
|
| 529 |
+
font-size: clamp(0.76rem, 0.84vw, 0.92rem);
|
| 530 |
+
font-weight: 500;
|
| 531 |
+
line-height: 1.25;
|
| 532 |
+
white-space: nowrap;
|
| 533 |
+
overflow: visible;
|
| 534 |
+
box-sizing: border-box;
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
.sp-account-card span {
|
| 538 |
+
display: inline-block;
|
| 539 |
+
white-space: nowrap;
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
.sp-account-separator {
|
| 543 |
+
flex: 0 0 auto;
|
| 544 |
+
opacity: 0.48;
|
| 545 |
+
}
|
| 546 |
+
|
| 547 |
+
div[data-testid="stMarkdownContainer"]:has(.sp-logout-link) {
|
| 548 |
+
width: 100%;
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
div[data-testid="stMarkdownContainer"]:has(.sp-logout-link) p {
|
| 552 |
+
width: 100%;
|
| 553 |
+
margin: 0;
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
.sp-logout-link {
|
| 557 |
+
display: flex;
|
| 558 |
+
align-items: center;
|
| 559 |
+
justify-content: center;
|
| 560 |
+
width: 100%;
|
| 561 |
+
min-width: 6.8rem;
|
| 562 |
+
min-height: 2.85rem;
|
| 563 |
+
padding: 0.45rem 0.72rem;
|
| 564 |
+
border: 1px solid rgba(148, 163, 184, 0.30);
|
| 565 |
+
border-radius: 0.5rem;
|
| 566 |
+
background: transparent;
|
| 567 |
+
color: var(--text-color, #f3f4f6) !important;
|
| 568 |
+
text-decoration: none !important;
|
| 569 |
+
white-space: nowrap !important;
|
| 570 |
+
overflow-wrap: normal !important;
|
| 571 |
+
word-break: keep-all !important;
|
| 572 |
+
line-height: 1.15;
|
| 573 |
+
font-size: clamp(0.78rem, 0.82vw, 0.92rem);
|
| 574 |
+
font-weight: 400;
|
| 575 |
+
box-sizing: border-box;
|
| 576 |
+
cursor: pointer;
|
| 577 |
+
transition: border-color 120ms ease, background-color 120ms ease;
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
.sp-logout-link:hover {
|
| 581 |
+
border-color: rgba(255, 75, 75, 0.78);
|
| 582 |
+
background: rgba(255, 255, 255, 0.035);
|
| 583 |
+
color: var(--text-color, #f3f4f6) !important;
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
.sp-logout-link:focus-visible {
|
| 587 |
+
outline: 2px solid rgba(255, 75, 75, 0.85);
|
| 588 |
+
outline-offset: 2px;
|
| 589 |
+
}
|
| 590 |
+
|
| 591 |
+
@media (max-width: 1150px) {
|
| 592 |
+
.sp-account-card {
|
| 593 |
+
font-size: 0.75rem;
|
| 594 |
+
gap: 0.3rem;
|
| 595 |
+
padding-left: 0.48rem;
|
| 596 |
+
padding-right: 0.48rem;
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
.sp-logout-link {
|
| 600 |
+
min-width: 5.8rem;
|
| 601 |
+
font-size: 0.78rem;
|
| 602 |
+
padding-left: 0.46rem;
|
| 603 |
+
padding-right: 0.46rem;
|
| 604 |
+
}
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
@media (max-width: 760px) {
|
| 608 |
+
.sp-account-card {
|
| 609 |
+
justify-content: flex-start;
|
| 610 |
+
overflow-x: auto;
|
| 611 |
+
scrollbar-width: none;
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
.sp-account-card::-webkit-scrollbar {
|
| 615 |
+
display: none;
|
| 616 |
+
}
|
| 617 |
+
}
|
| 618 |
+
</style>
|
| 619 |
+
""",
|
| 620 |
+
unsafe_allow_html=True,
|
| 621 |
+
)
|
| 622 |
+
|
| 623 |
+
info_col, logout_col = st.columns([5.9, 1.65])
|
| 624 |
+
with info_col:
|
| 625 |
+
st.markdown(
|
| 626 |
+
f"""
|
| 627 |
+
<div class="sp-account-card">
|
| 628 |
+
<span>{html.escape(tr(language, 'account_chip_user', value=user_id))}</span>
|
| 629 |
+
<span class="sp-account-separator">•</span>
|
| 630 |
+
<span>{html.escape(tr(language, 'account_chip_free', value=free_remaining))}</span>
|
| 631 |
+
<span class="sp-account-separator">•</span>
|
| 632 |
+
<span>{html.escape(tr(language, 'account_chip_bonus', value=bonus_papers))}</span>
|
| 633 |
+
</div>
|
| 634 |
+
""",
|
| 635 |
+
unsafe_allow_html=True,
|
| 636 |
+
)
|
| 637 |
+
with logout_col:
|
| 638 |
+
logout_label = html.escape(tr(language, "logout_short"))
|
| 639 |
+
logout_help = html.escape(tr(language, "logout"), quote=True)
|
| 640 |
+
st.markdown(
|
| 641 |
+
f'<a class="sp-logout-link" href="/edge/logout" target="_self" '
|
| 642 |
+
f'rel="nofollow" role="button" aria-label="{logout_help}" '
|
| 643 |
+
f'title="{logout_help}">{logout_label}</a>',
|
| 644 |
+
unsafe_allow_html=True,
|
| 645 |
+
)
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/requirements.txt
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit==1.38.0
|
| 2 |
+
requests>=2.31,<3
|
| 3 |
+
python-docx>=1.1,<2
|
| 4 |
+
pandas>=2.1,<3
|
| 5 |
+
numpy>=1.26,<3
|
| 6 |
+
scikit-learn>=1.4,<2
|
| 7 |
+
tiktoken>=0.7,<1
|
| 8 |
+
langdetect==1.0.9
|
| 9 |
+
mistune>=3,<4
|
| 10 |
+
beautifulsoup4>=4.12,<5
|
| 11 |
+
PyMuPDF>=1.24,<2
|
| 12 |
+
openpyxl>=3.1,<4
|
| 13 |
+
tabulate>=0.9,<1
|
| 14 |
+
qrcode[pil]>=8,<9
|
$RECYCLE.BIN/S-1-5-21-2308098342-1360407909-2433281377-1001/$RD7299H/smartpaper_restore_bridge/__init__.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""Invisible browser bridge for SmartPaper recovery and edge API calls.
|
| 3 |
+
|
| 4 |
+
The component is served by Streamlit through the public Cloudflare origin. It
|
| 5 |
+
therefore allows the authenticated browser to call same-origin Worker endpoints
|
| 6 |
+
with the existing HttpOnly browser cookie. No service key or cookie value is
|
| 7 |
+
passed to Python or JavaScript arguments.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
import streamlit.components.v1 as components
|
| 16 |
+
|
| 17 |
+
_FRONTEND_DIR = Path(__file__).resolve().parent / "frontend"
|
| 18 |
+
_FRONTEND_ENTRY = _FRONTEND_DIR / "index.html"
|
| 19 |
+
if not _FRONTEND_ENTRY.is_file():
|
| 20 |
+
raise RuntimeError("SmartPaper restore bridge frontend is missing.")
|
| 21 |
+
|
| 22 |
+
_COMPONENT = components.declare_component(
|
| 23 |
+
"smartpaper_restore_bridge",
|
| 24 |
+
path=str(_FRONTEND_DIR),
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def restore_bridge(
|
| 29 |
+
*,
|
| 30 |
+
action: str,
|
| 31 |
+
command_id: str,
|
| 32 |
+
token: str = "",
|
| 33 |
+
expires_at: str = "",
|
| 34 |
+
key: str = "sp_restore_bridge",
|
| 35 |
+
) -> dict[str, Any] | None:
|
| 36 |
+
"""Backward-compatible recovery-token storage operation."""
|
| 37 |
+
value = _COMPONENT(
|
| 38 |
+
action=str(action or "read"),
|
| 39 |
+
command_id=str(command_id or ""),
|
| 40 |
+
token=str(token or ""),
|
| 41 |
+
expires_at=str(expires_at or ""),
|
| 42 |
+
api_origin="",
|
| 43 |
+
payload={},
|
| 44 |
+
fire_and_forget=False,
|
| 45 |
+
height=0,
|
| 46 |
+
default=None,
|
| 47 |
+
key=key,
|
| 48 |
+
)
|
| 49 |
+
return value if isinstance(value, dict) else None
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def edge_bridge(
|
| 53 |
+
*,
|
| 54 |
+
action: str,
|
| 55 |
+
command_id: str,
|
| 56 |
+
api_origin: str,
|
| 57 |
+
payload: dict[str, Any] | None = None,
|
| 58 |
+
fire_and_forget: bool = False,
|
| 59 |
+
key: str = "sp_edge_bridge",
|
| 60 |
+
) -> dict[str, Any] | None:
|
| 61 |
+
"""Execute one authenticated same-origin Worker command in the browser.
|
| 62 |
+
|
| 63 |
+
The first synchronous invocation usually returns ``None``. Streamlit reruns
|
| 64 |
+
after the browser completes the request and then returns the result. Only
|
| 65 |
+
non-terminal heartbeat calls may use ``fire_and_forget=True``. Task
|
| 66 |
+
completion, failure, cancellation, payment, and account mutations remain
|
| 67 |
+
synchronous so Python receives the Worker/D1 postcondition.
|
| 68 |
+
"""
|
| 69 |
+
value = _COMPONENT(
|
| 70 |
+
action=str(action or ""),
|
| 71 |
+
command_id=str(command_id or ""),
|
| 72 |
+
token="",
|
| 73 |
+
expires_at="",
|
| 74 |
+
api_origin=str(api_origin or ""),
|
| 75 |
+
payload=dict(payload or {}),
|
| 76 |
+
fire_and_forget=bool(fire_and_forget),
|
| 77 |
+
height=0,
|
| 78 |
+
default=None,
|
| 79 |
+
key=key,
|
| 80 |
+
)
|
| 81 |
+
return value if isinstance(value, dict) else None
|