Compare commits
507 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41ab11e7b4 | ||
|
|
9949e7c8d4 | ||
|
|
9e36d84f19 | ||
|
|
ada526fa63 | ||
|
|
ca65eb907d | ||
|
|
7f2a0dd3e8 | ||
|
|
6e34409225 | ||
|
|
4af354e918 | ||
|
|
207d0caf2a | ||
|
|
f7487d22d5 | ||
|
|
3c10976aff | ||
|
|
ec8e144655 | ||
|
|
ae5ccfd775 | ||
|
|
9d9e22451d | ||
|
|
e68daf870f | ||
|
|
534e95f86c | ||
|
|
077713003f | ||
|
|
35fd8e62ac | ||
|
|
b7c2ddd59d | ||
|
|
f931567f56 | ||
|
|
8545e0692c | ||
|
|
19eb05269b | ||
|
|
92d757662a | ||
|
|
1b1406a4d9 | ||
|
|
03fc19f070 | ||
|
|
ffa09f1b29 | ||
|
|
ff9a1523fd | ||
|
|
8a7e5c0830 | ||
|
|
8e90ad69b1 | ||
|
|
52c389edd8 | ||
|
|
f7206c1603 | ||
|
|
86d9baa503 | ||
|
|
92c4565590 | ||
|
|
c51870ff79 | ||
|
|
182f061354 | ||
|
|
80507cab27 | ||
|
|
f801ae9b63 | ||
|
|
977173d987 | ||
|
|
cd0fcb903f | ||
|
|
7bae1d9537 | ||
|
|
b43cbb7afe | ||
|
|
72982387cc | ||
|
|
ff0245840a | ||
|
|
c55f64e209 | ||
|
|
a4cb5d1b14 | ||
|
|
13e1415355 | ||
|
|
7b49d66a8e | ||
|
|
63c6e12aca | ||
|
|
b64d4fd96f | ||
|
|
dda603c7d8 | ||
|
|
e22de5cba1 | ||
|
|
bdabfdcb3d | ||
|
|
00a8514245 | ||
|
|
94fd3197b3 | ||
|
|
7957353c3f | ||
|
|
b3b7589db3 | ||
|
|
5aed9ce29e | ||
|
|
924f0a9f7c | ||
|
|
7a7cd72db9 | ||
|
|
e9e9bab479 | ||
|
|
f258dcfca2 | ||
|
|
fe84f812e7 | ||
|
|
9eba376976 | ||
|
|
462705c4ed | ||
|
|
a2533ce7f9 | ||
|
|
dbf42c51a4 | ||
|
|
f61e7caf2b | ||
|
|
679c2070c1 | ||
|
|
92d46e1da3 | ||
|
|
7ab94205e4 | ||
|
|
ab616c5d32 | ||
|
|
8f2f68dffc | ||
|
|
18b7e17e95 | ||
|
|
cca2298d3a | ||
|
|
f3683bcc84 | ||
|
|
fa2959515e | ||
|
|
7ab5ddc408 | ||
|
|
f273858248 | ||
|
|
ca8f7374da | ||
|
|
ff1dce833a | ||
|
|
d3d5a7bade | ||
|
|
f5d6702472 | ||
|
|
3db687ad40 | ||
|
|
a5cb958398 | ||
|
|
9e522091c6 | ||
|
|
79f256976e | ||
|
|
b560c0bafd | ||
|
|
bd157d305e | ||
|
|
923016197a | ||
|
|
dcf96e2bf5 | ||
|
|
d4697cb203 | ||
|
|
6e6a50b46e | ||
|
|
b9830bc64a | ||
|
|
7c501cec45 | ||
|
|
add23934ca | ||
|
|
a8b798b00c | ||
|
|
b522b1de05 | ||
|
|
3660cbd450 | ||
|
|
50f8bb8721 | ||
|
|
e1a2d90382 | ||
|
|
d8872f215b | ||
|
|
484bc6ea39 | ||
|
|
7d1979f067 | ||
|
|
6927c0b30b | ||
|
|
aa74c5ccaf | ||
|
|
e3d0f571d2 | ||
|
|
d03dabdfdf | ||
|
|
fc339ae55f | ||
|
|
a0aa04fd8c | ||
|
|
6dc5ae1ae4 | ||
|
|
df02a6b50f | ||
|
|
9e4f733c28 | ||
|
|
1175b330f5 | ||
|
|
3cb9fff07f | ||
|
|
bfdb72dd0a | ||
|
|
5489462f90 | ||
|
|
94ac3c3922 | ||
|
|
bf75946e14 | ||
|
|
b2a70e0cce | ||
|
|
83780f9bcd | ||
|
|
bfb9795913 | ||
|
|
208598a6df | ||
|
|
6c79753051 | ||
|
|
095a238fff | ||
|
|
ebbde8afd3 | ||
|
|
bba5bb7411 | ||
|
|
9c155c6cf5 | ||
|
|
19da7a74df | ||
|
|
f5d76fd5ff | ||
|
|
77940c9430 | ||
|
|
54a42a14b6 | ||
|
|
52faf7884b | ||
|
|
841ed4e682 | ||
|
|
bc417b9eea | ||
|
|
da7dc477c8 | ||
|
|
6c519ebd61 | ||
|
|
88e859817b | ||
|
|
f5dd36260f | ||
|
|
a5325b84ae | ||
|
|
7095c9e71e | ||
|
|
fb4373c83a | ||
|
|
dd59a1aebb | ||
|
|
6f7edd0b40 | ||
|
|
397421010e | ||
|
|
8872b0519d | ||
|
|
83d3b3ffbf | ||
|
|
d1702bd62c | ||
|
|
928235eac8 | ||
|
|
3bdfaab158 | ||
|
|
d7902b4d08 | ||
|
|
3334abfb8f | ||
|
|
3e44e584c0 | ||
|
|
195a305fc3 | ||
|
|
cedffd17b3 | ||
|
|
59b29014d9 | ||
|
|
06db036e4a | ||
|
|
617e0837c9 | ||
|
|
7a275bd802 | ||
|
|
83f58eae68 | ||
|
|
19815fe27d | ||
|
|
0f75556bed | ||
|
|
dc0f925d24 | ||
|
|
c5948c4171 | ||
|
|
d144b06c1f | ||
|
|
92dfea677b | ||
|
|
7b5867e2c0 | ||
|
|
b643fe56d5 | ||
|
|
38fa72e9da | ||
|
|
82fddefc94 | ||
|
|
0c9c9cb90a | ||
|
|
38b50a8a84 | ||
|
|
0f250dbafd | ||
|
|
168650649f | ||
|
|
52babf82ae | ||
|
|
0c8517667f | ||
|
|
77d105cb9f | ||
|
|
8af33ea66a | ||
|
|
a57740e14e | ||
|
|
82230d70a5 | ||
|
|
15f3f9c0e5 | ||
|
|
7fc328492b | ||
|
|
81cedca590 | ||
|
|
df4e00e23f | ||
|
|
ad70f23a05 | ||
|
|
c93beb27fd | ||
|
|
41da2231ed | ||
|
|
9d9213fbdb | ||
|
|
50106d19e8 | ||
|
|
62d1e676bd | ||
|
|
0b7d49785c | ||
|
|
40736c4a05 | ||
|
|
1f0ab02702 | ||
|
|
21aa4f7b2b | ||
|
|
43d0a85061 | ||
|
|
8bdd31ae67 | ||
|
|
b78f93979d | ||
|
|
8d6b4a1d2e | ||
|
|
7630c83ae0 | ||
|
|
c7c47aff5a | ||
|
|
72e475cb84 | ||
|
|
f750a6aec2 | ||
|
|
0dde37e1f1 | ||
|
|
a2066fc137 | ||
|
|
f652f35c3a | ||
|
|
562697da27 | ||
|
|
d23d77ff90 | ||
|
|
119443cc88 | ||
|
|
27ae831799 | ||
|
|
b482947207 | ||
|
|
6ebc89695a | ||
|
|
a65dfec7a8 | ||
|
|
a0cd79e587 | ||
|
|
8fe1e2fee4 | ||
|
|
3cc9f7bc40 | ||
|
|
8d3d5025ed | ||
|
|
a49c0aea47 | ||
|
|
d366cf9885 | ||
|
|
be53afe6b4 | ||
|
|
cdd980112d | ||
|
|
bca284969d | ||
|
|
dd899a3e13 | ||
|
|
d6ca66aa2f | ||
|
|
20ba671cd3 | ||
|
|
672795ac49 | ||
|
|
9716d7fe43 | ||
|
|
193ad8d902 | ||
|
|
a87f903c50 | ||
|
|
82f154a229 | ||
|
|
bee36801ab | ||
|
|
37f379c890 | ||
|
|
88b995ca9c | ||
|
|
919289c5ca | ||
|
|
0535b56766 | ||
|
|
6afd413b87 | ||
|
|
4818409329 | ||
|
|
919b652a06 | ||
|
|
15d3ec9d81 | ||
|
|
e0be6e429e | ||
|
|
8d24be914d | ||
|
|
8bbe9c97e9 | ||
|
|
ccbd904a3f | ||
|
|
4ed3db7e41 | ||
|
|
65ced28004 | ||
|
|
4c282962b3 | ||
|
|
c64c436b9f | ||
|
|
378e270f41 | ||
|
|
7217bd7d1a | ||
|
|
ff2461d89d | ||
|
|
0eb3430c14 | ||
|
|
da7c1e40e3 | ||
|
|
8c9e928ddc | ||
|
|
477aef7db6 | ||
|
|
cc97d9f1ea | ||
|
|
ee6cf05a92 | ||
|
|
575db58476 | ||
|
|
986a2f8cbb | ||
|
|
0b1da914cd | ||
|
|
04acd7c56d | ||
|
|
4430d85242 | ||
|
|
55ade32589 | ||
|
|
0ffbaaaeaa | ||
|
|
62b40ddb84 | ||
|
|
2d5ce87605 | ||
|
|
7ca0bc5960 | ||
|
|
021c09e426 | ||
|
|
75db81f2f9 | ||
|
|
f162617765 | ||
|
|
b7d10a4c58 | ||
|
|
79ca1aea02 | ||
|
|
957201804c | ||
|
|
cf5e126eaa | ||
|
|
69fc0a118b | ||
|
|
4dacc26567 | ||
|
|
7de1ed7d45 | ||
|
|
ab47f01625 | ||
|
|
13c4fa4f1f | ||
|
|
173631f115 | ||
|
|
8462e9c097 | ||
|
|
3c9447e1b6 | ||
|
|
38eaf2eb02 | ||
|
|
c8364ed17b | ||
|
|
ba52738904 | ||
|
|
4061ae4275 | ||
|
|
82afb5b150 | ||
|
|
e1203f0c8d | ||
|
|
e6f6b3fee2 | ||
|
|
e5efcd3d26 | ||
|
|
bf45587c80 | ||
|
|
29a0d22938 | ||
|
|
635cc04c50 | ||
|
|
bc5343652b | ||
|
|
03f140fe3b | ||
|
|
3e4a119f61 | ||
|
|
3b7bcbc14a | ||
|
|
3c49e96e02 | ||
|
|
5be209ab59 | ||
|
|
56ea048ab3 | ||
|
|
9fc0bd0439 | ||
|
|
1c2798cbf4 | ||
|
|
9d8af2eaab | ||
|
|
bba1e0d12f | ||
|
|
c060e60e4a | ||
|
|
1c504bd899 | ||
|
|
b617648bd8 | ||
|
|
e849c7a34f | ||
|
|
f6dd1ce98e | ||
|
|
9c78db8d45 | ||
|
|
5154348cf9 | ||
|
|
4521cea3b4 | ||
|
|
0ff1ac7743 | ||
|
|
277a751ed4 | ||
|
|
96be2a86ca | ||
|
|
f28bff569a | ||
|
|
e34aa77a54 | ||
|
|
e53b65496f | ||
|
|
f6ee630615 | ||
|
|
ec2e1e3152 | ||
|
|
6cffe9baed | ||
|
|
b63df27409 | ||
|
|
617c466ac0 | ||
|
|
ed8e443f3a | ||
|
|
58cb49b125 | ||
|
|
7dd5baa9ec | ||
|
|
bbf9107560 | ||
|
|
be527355ee | ||
|
|
c866500120 | ||
|
|
3e2a40aaa0 | ||
|
|
eef9fa56c6 | ||
|
|
945d84dbc4 | ||
|
|
d353d33107 | ||
|
|
f54bad5d79 | ||
|
|
b605c70e91 | ||
|
|
1752e88c42 | ||
|
|
e2718a39a0 | ||
|
|
25298ac69e | ||
|
|
cf9f389f75 | ||
|
|
567c75830a | ||
|
|
7b1d352c95 | ||
|
|
4fa54381a6 | ||
|
|
9c91f7cf83 | ||
|
|
edd5cd1ca1 | ||
|
|
f2ec6ad05e | ||
|
|
a04ef4ac38 | ||
|
|
43b3d1d379 | ||
|
|
b65fdeacc2 | ||
|
|
622fe1e5d9 | ||
|
|
a6c7c0c7ad | ||
|
|
e5c8748b75 | ||
|
|
f096d71cc1 | ||
|
|
d73a152a36 | ||
|
|
f4e6fd060e | ||
|
|
c78ca1de5d | ||
|
|
2b219c7256 | ||
|
|
6ffa651742 | ||
|
|
cb3b22a4bf | ||
|
|
145bfdb0e9 | ||
|
|
8c7b0c502d | ||
|
|
684bf12a5c | ||
|
|
aaa75aff14 | ||
|
|
f03600bd65 | ||
|
|
1c4c4fe3fb | ||
|
|
5e46b2cd1a | ||
|
|
027db7c0ec | ||
|
|
5bb17ddc6b | ||
|
|
e8edd74bc3 | ||
|
|
ed064a825a | ||
|
|
32c232a0b5 | ||
|
|
c2fd747c45 | ||
|
|
9148853f2c | ||
|
|
23d0f50a3d | ||
|
|
36cdf87bfe | ||
|
|
cfd2e1fd7b | ||
|
|
3cafac99ff | ||
|
|
1dd4e8da71 | ||
|
|
543015a36e | ||
|
|
2efdfc4b1f | ||
|
|
7234d9307e | ||
|
|
769ce1ce7c | ||
|
|
62c1d5783e | ||
|
|
a6bd4a2ffe | ||
|
|
f1a9077b7e | ||
|
|
2c3e80bd8f | ||
|
|
e52d066fb0 | ||
|
|
5279d57018 | ||
|
|
25e5eb4427 | ||
|
|
b01d5ce8c4 | ||
|
|
ff41f5c041 | ||
|
|
dd0770a93f | ||
|
|
9a3e76fff3 | ||
|
|
7c867578ee | ||
|
|
d543c27000 | ||
|
|
a8be330baa | ||
|
|
c128c58110 | ||
|
|
e32a3887cd | ||
|
|
94932c7486 | ||
|
|
a1920745fb | ||
|
|
51e8f9555e | ||
|
|
213ab8418b | ||
|
|
707f1dd6cb | ||
|
|
125ce036cd | ||
|
|
172c562a71 | ||
|
|
80bbe6711c | ||
|
|
3f56c64086 | ||
|
|
e6167119e0 | ||
|
|
368fae5f32 | ||
|
|
6ae46cf7bb | ||
|
|
e97806c85b | ||
|
|
f31e88bed1 | ||
|
|
6bd20038f9 | ||
|
|
30cfb1200d | ||
|
|
154e0039d1 | ||
|
|
a8f3b02ee7 | ||
|
|
b3e83e13bc | ||
|
|
d0a0e77c44 | ||
|
|
a14896307f | ||
|
|
976b300277 | ||
|
|
ccbd873204 | ||
|
|
9c1482f9e9 | ||
|
|
5a7f4efa91 | ||
|
|
f78c4a1fb0 | ||
|
|
db6500369f | ||
|
|
9e4beaa317 | ||
|
|
afd021737a | ||
|
|
3982ed56f7 | ||
|
|
df4a01a7f9 | ||
|
|
a6fac96ec1 | ||
|
|
8ed9186ff4 | ||
|
|
821df75d4b | ||
|
|
0c09a2445c | ||
|
|
e6983e858d | ||
|
|
f8b69df955 | ||
|
|
15370a93c7 | ||
|
|
bc18aeeadc | ||
|
|
a1f143b0aa | ||
|
|
c13fe9d590 | ||
|
|
50203fbcb3 | ||
|
|
ffe7ebf711 | ||
|
|
f0b5e0c3b9 | ||
|
|
501235ef12 | ||
|
|
da0fa31181 | ||
|
|
0272933f70 | ||
|
|
30d88761b4 | ||
|
|
fb286cea3c | ||
|
|
6bcc7b6c49 | ||
|
|
6338a44cc1 | ||
|
|
ae4680f20c | ||
|
|
2841874417 | ||
|
|
b6a4e6b4de | ||
|
|
34cfd1e344 | ||
|
|
b467dc55e5 | ||
|
|
9fd8d44a6e | ||
|
|
64262134c4 | ||
|
|
0019c9ef41 | ||
|
|
2676ebd047 | ||
|
|
97cdd56110 | ||
|
|
d973451bdc | ||
|
|
80313f613e | ||
|
|
5c564524a3 | ||
|
|
e081fbd92b | ||
|
|
0ecc20472a | ||
|
|
b51052f0c6 | ||
|
|
cb106e42ee | ||
|
|
52f9495ff8 | ||
|
|
440b633bad | ||
|
|
a07913181a | ||
|
|
34ffd96c86 | ||
|
|
46a623b430 | ||
|
|
c16e37023c | ||
|
|
1cb0cdf540 | ||
|
|
073d03a882 | ||
|
|
30b9276ab4 | ||
|
|
76c8b4a4c6 | ||
|
|
9ea4781d93 | ||
|
|
07d583f73f | ||
|
|
12c74aef7a | ||
|
|
64b10e3060 | ||
|
|
ab2b29f267 | ||
|
|
be9a968ad9 | ||
|
|
5f87067a75 | ||
|
|
ef273bd9dd | ||
|
|
0737a9fae7 | ||
|
|
727d7e1d81 | ||
|
|
87e8589aea | ||
|
|
b13758d3e9 | ||
|
|
14775e2861 | ||
|
|
94af3822d8 | ||
|
|
07254c9f27 | ||
|
|
a99c2f6944 | ||
|
|
f9540b08cd | ||
|
|
34af77eb6d | ||
|
|
cf3f22776c | ||
|
|
5bebc8b5ee | ||
|
|
8a4b0c57f9 | ||
|
|
1acfd7ee34 | ||
|
|
a29661c54d | ||
|
|
90558d5ece | ||
|
|
e6c7007be5 | ||
|
|
16d0d1687f | ||
|
|
95ab44d118 | ||
|
|
e541757b76 | ||
|
|
f422aea330 | ||
|
|
d5eb3716aa | ||
|
|
7fb854fb48 | ||
|
|
60b5ecdcd7 | ||
|
|
6cce7d31ff | ||
|
|
46f5dd99a6 | ||
|
|
9753dec996 |
@@ -17,7 +17,7 @@ LOG_CHANNEL=stack
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST="${APP_IPPR}.5"
|
||||
DB_HOST=mariadb
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=dootask
|
||||
DB_USERNAME=dootask
|
||||
@@ -34,7 +34,7 @@ SESSION_LIFETIME=120
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_HOST="${APP_IPPR}.4"
|
||||
REDIS_HOST=redis
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
|
||||
49
.github/workflows/publish.yml
vendored
49
.github/workflows/publish.yml
vendored
@@ -115,9 +115,50 @@ jobs:
|
||||
})
|
||||
return data.id
|
||||
|
||||
build-client:
|
||||
pack-vendor:
|
||||
needs: [ check-version, create-release ]
|
||||
if: needs.check-version.outputs.should_release == 'true'
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.0'
|
||||
extensions: mbstring, intl, gd, xml, zip, swoole
|
||||
tools: composer:v2
|
||||
|
||||
- name: Install Dependencies
|
||||
run: composer install
|
||||
|
||||
- name: Create Vendor Archive
|
||||
run: tar -czf vendor.tar.gz vendor/
|
||||
|
||||
- name: Upload Vendor Archive
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
const data = await fs.promises.readFile('vendor.tar.gz');
|
||||
|
||||
await github.rest.repos.uploadReleaseAsset({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: process.env.RELEASE_ID,
|
||||
name: 'vendor.tar.gz',
|
||||
data: data
|
||||
});
|
||||
|
||||
build-client:
|
||||
needs: [ check-version, create-release, pack-vendor ]
|
||||
if: needs.check-version.outputs.should_release == 'true'
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
@@ -231,7 +272,7 @@ jobs:
|
||||
./cmd electron win
|
||||
|
||||
publish-release:
|
||||
needs: [ check-version, create-release, build-client ]
|
||||
needs: [ check-version, create-release, pack-vendor, build-client ]
|
||||
if: needs.check-version.outputs.should_release == 'true' && github.ref == 'refs/heads/pro'
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -258,7 +299,7 @@ jobs:
|
||||
env:
|
||||
PUBLISH_KEY: ${{ secrets.PUBLISH_KEY }}
|
||||
run: |
|
||||
pushd electron
|
||||
pushd electron || exit
|
||||
npm install
|
||||
popd
|
||||
popd || exit
|
||||
node ./electron/build.js published
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -15,6 +15,7 @@
|
||||
.idea
|
||||
.vscode
|
||||
.vagrant
|
||||
.windsurfrules
|
||||
.phpunit.result.cache
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
@@ -28,3 +29,4 @@ vars.yaml
|
||||
laravels.conf
|
||||
laravels.pid
|
||||
README_LOCAL.md
|
||||
dootask.lock
|
||||
|
||||
393
CHANGELOG.md
393
CHANGELOG.md
@@ -2,6 +2,399 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.47.7]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复任务详情查看历史空白的情况
|
||||
- 修复我的机器人不回复的情况
|
||||
- 修复设待办后数据不立即显示的问题
|
||||
|
||||
### Features
|
||||
|
||||
- 添加删除附件日志记录
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化从任务页面发送消息
|
||||
- 优化已归档/已删除任务列表支持按状态检索
|
||||
- 优化长按消息菜单位置
|
||||
- 优化登录设备名称
|
||||
|
||||
## [0.46.74]
|
||||
|
||||
### Features
|
||||
|
||||
- 新增系统分享搜索功能
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化通用菜单
|
||||
- 优化视频压缩
|
||||
- 优化全文搜索
|
||||
- 优化长按菜单
|
||||
|
||||
## [0.46.16]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复调整任务排序后出现空白的情况
|
||||
- 修复移动任务时负责人和协助人可以同时选择的情况
|
||||
- 修复无法从任务页面打开聊天的情况
|
||||
- 修复移动端焦点抖动的问题
|
||||
|
||||
### Features
|
||||
|
||||
- 新增任务发送功能
|
||||
- 新增会员详情窗口
|
||||
- 添加从团队管理打开会话窗口
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化移动任务
|
||||
- 优化自己的对话不限修改撤回时间
|
||||
- 优化访问链接
|
||||
- 优化日历
|
||||
- 优化长按事件
|
||||
- 优化移动端任务窗口布局
|
||||
- 优化长按操作
|
||||
- 优化转发确认选项保持上一次选择
|
||||
- 优化移动端布局
|
||||
- 优化禁止选择会员效果
|
||||
- 优化长按菜单位置
|
||||
- 优化移动端打开会话等待效果
|
||||
- 优化会议弹窗
|
||||
- 任务详情点任务聊天时不要发送消息
|
||||
- 优化国际化
|
||||
|
||||
## [0.45.64]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复部分页面出现空白的情况
|
||||
- 修复输入框无法点击添加链接的情况
|
||||
- 修复AI机器人不存在的情况
|
||||
|
||||
### Features
|
||||
|
||||
- 新增转发至AI开启新会话
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化数据结构
|
||||
- 优化图片存储名
|
||||
- 优化消息窗口显示
|
||||
- 优化目录结构
|
||||
- 优化日历
|
||||
- 优化任务时间范围选择
|
||||
|
||||
## [0.45.33]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复未读数错误暴增的情况
|
||||
- 修复地址可能存在localhost的情况
|
||||
- 修复消息编辑和发布时序号对不上
|
||||
- 修复草稿出现上一次内容的情况
|
||||
- 修复本地群消息通知没有会员昵称的问题
|
||||
- 修复了拉人进群无法踢出去的问题
|
||||
- 提及出现白色字的情况
|
||||
|
||||
### Features
|
||||
|
||||
- 添加移动端提示可能要发送的图片
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化消息窗口
|
||||
- 优化消息长按菜单
|
||||
- 优化内置浏览器
|
||||
- 优化App隐私政策提示
|
||||
- 优化对话独立窗口显示
|
||||
- 优化移动端选择交互
|
||||
- 优化移动端选中消息文本
|
||||
- 优化撤回消息逻辑
|
||||
- 优化提及搜索
|
||||
- 优化机器人Webhook消息
|
||||
|
||||
## [0.44.91]
|
||||
|
||||
### Features
|
||||
|
||||
- 添加我的机器人管理
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化初始化逻辑
|
||||
- 优化docker配置
|
||||
|
||||
## [0.44.82]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复搜索结果显示即将到期
|
||||
|
||||
### Features
|
||||
|
||||
- 新增独立窗口打开会话
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化AI支持文件类型
|
||||
|
||||
## [0.44.74]
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化AI解析文件
|
||||
- 优化 WebSocket 消息
|
||||
- 优化数据
|
||||
|
||||
## [0.44.67]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复查看待办图片不符的情况
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化数据
|
||||
- 优化未读消息数
|
||||
- 优化搜索组件
|
||||
- 已归档/已删除任务列表支持按状态检索
|
||||
- 优化消息流效果
|
||||
- 优化AI上下文
|
||||
- 优化工作流获取
|
||||
- 优化转发功能
|
||||
|
||||
## [0.44.53]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 工作流存在已离职人员
|
||||
|
||||
### Features
|
||||
|
||||
- 可点击标注图标查看标注人员
|
||||
- 支持分享工作报告到消息
|
||||
- 支持AI分析工作报告
|
||||
- 支持使用%发送工作报告
|
||||
- 新增自定义撤回及修改消息时限
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化转发消息
|
||||
- 优化工作报告列表
|
||||
- 优化引用消息
|
||||
- 优化全局提示
|
||||
- 优化草稿消息
|
||||
|
||||
## [0.44.19]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 看不到未读消息定位提醒
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化消息定位
|
||||
- 优化消息性能
|
||||
|
||||
## [0.44.15]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 会话内消息搜索布局错位
|
||||
- 流程设置翻译不统一
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化消息定位
|
||||
- 优化MD消息
|
||||
|
||||
## [0.44.3]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 定位签到失败的问题
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化发送语音效果
|
||||
- 录音转文字支持自定义语言
|
||||
- 优化ES模块
|
||||
- 优化emoji表情
|
||||
- 按住Ctrl/Command键可连续选择表情
|
||||
- Md消息支持html代码
|
||||
- 优化脚本
|
||||
- 优化安装命令
|
||||
- 优化ES索引名称
|
||||
|
||||
## [0.43.73]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 全屏预览图片关闭窗口
|
||||
- 点击排序导致任务不显示的情况
|
||||
|
||||
### Performance
|
||||
|
||||
- 新增录音转文字
|
||||
- 优化数据排序
|
||||
|
||||
## [0.43.49]
|
||||
|
||||
### Performance
|
||||
|
||||
- 添加全局搜索功能
|
||||
- 优化消息搜索
|
||||
- 团队管理支持调整部门区域尺寸
|
||||
- 任务详情支持调整聊天区域尺寸
|
||||
- 优化团队部门支持3级部门
|
||||
- 可见群组ID
|
||||
- 支持在团队管理打开群聊
|
||||
- 优化回复消息自动@逻辑
|
||||
- 转发预览隐藏表情回应部分
|
||||
- 优化任务日志
|
||||
- 已删除任务支持按标签搜索
|
||||
- 归档任务支持按标签搜索
|
||||
- 项目面板添加按标签筛选
|
||||
- 优化 AI 提示词
|
||||
- 优化 AI 设置
|
||||
|
||||
## [0.43.18]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 首次跟ai聊天没有记录的问题
|
||||
|
||||
### Performance
|
||||
|
||||
- 工作报告支持查看仅未读
|
||||
- AI 支持引用文件
|
||||
- 优化图文消息
|
||||
- 优化文本信息复制
|
||||
- 优化样式
|
||||
- 无法再AI机器人页面看到模型的问题
|
||||
|
||||
## [0.43.7]
|
||||
|
||||
### Features
|
||||
|
||||
- 添加 Grok AI、Ollama AI
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化MD消息过长处理
|
||||
- 优化AI支持分析指定文件
|
||||
- 支持在AI对话中直接引用任务提问
|
||||
- 优化 AI 参数
|
||||
- 优化 Ollama AI
|
||||
- 优化设置
|
||||
- 优化AI设置
|
||||
- 优化AI消息
|
||||
|
||||
## [0.42.85]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 撤回消息是消息列表不更新的情况
|
||||
|
||||
### Performance
|
||||
|
||||
- 表情回复时更新对话列表
|
||||
- Onlyoffice 支持打开超过100m的文件
|
||||
- 优化点击上传列表效果
|
||||
- AI支持自定义模型列表
|
||||
|
||||
## [0.42.79]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复偶现的是子窗口出现身份丢失的情况
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化查看长消息内容
|
||||
|
||||
## [0.42.74]
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化审批功能
|
||||
- AI机器人支持多会话
|
||||
- AI机器人支持自定义模型
|
||||
|
||||
## [0.42.61]
|
||||
|
||||
### Performance
|
||||
|
||||
- 支持下载聊天引用的文件
|
||||
- 优化翻译消息
|
||||
- 支持显示思考过程
|
||||
|
||||
## [0.42.57]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 跨地区发消息出现消息过期的情况
|
||||
- 多线程下载文件损坏的问题
|
||||
- 修复新建周报或日报唯一标识重复
|
||||
|
||||
### Features
|
||||
|
||||
- 添加 DeepSeek AI
|
||||
- 添加https证书自动更新
|
||||
|
||||
### Performance
|
||||
|
||||
- 支持自定义仪表盘欢迎词
|
||||
- ChatGPT 支持自定义 Base URL
|
||||
- 优化仪表盘任务更新规则
|
||||
|
||||
## [0.42.37]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 部分电脑无法复制的问题
|
||||
- 修复任务可见性 - 任务重覆获取, 子任务负责人看不到任务问题
|
||||
|
||||
### Performance
|
||||
|
||||
- 更新小海豚表情包
|
||||
- 优化任务时间冲突提示
|
||||
- 优化消息
|
||||
- 群聊总人数排除机器人
|
||||
|
||||
## [0.42.26]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 移交账号后工作流的负责人没有更新
|
||||
- 全屏预览时深色皮肤反色的情况
|
||||
|
||||
### Features
|
||||
|
||||
- 替换网页的资源为本地资源
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化任务面板
|
||||
- 优化子任务的可见性
|
||||
- 优化客户端
|
||||
- 优化会议
|
||||
- 优化会员搜索
|
||||
- 优化打开会话
|
||||
- 优化项目面板任务加载
|
||||
- 优化客户端加载
|
||||
|
||||
## [0.42.3]
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化对话阅读状况
|
||||
- 优化表情回复
|
||||
|
||||
## [0.42.0]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
10
README.md
10
README.md
@@ -47,16 +47,6 @@ cd dootask
|
||||
./cmd port 80
|
||||
```
|
||||
|
||||
### Change App Url
|
||||
|
||||
```bash
|
||||
# This URL only affects the email reply.
|
||||
./cmd url {Your domain url}
|
||||
|
||||
# example:
|
||||
./cmd url https://domain.com
|
||||
```
|
||||
|
||||
### Stop server
|
||||
|
||||
```bash
|
||||
|
||||
10
README_CN.md
10
README_CN.md
@@ -47,16 +47,6 @@ cd dootask
|
||||
./cmd port 80
|
||||
```
|
||||
|
||||
### 更换URL
|
||||
|
||||
```bash
|
||||
# 此地址仅影响邮件回复功能
|
||||
./cmd url {域名地址}
|
||||
|
||||
# 例如:
|
||||
./cmd url https://domain.com
|
||||
```
|
||||
|
||||
### 停止服务
|
||||
|
||||
```bash
|
||||
|
||||
169
app/Console/Commands/SyncUserMsgToZincSearch.php
Normal file
169
app/Console/Commands/SyncUserMsgToZincSearch.php
Normal file
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Module\ZincSearch\ZincSearchKeyValue;
|
||||
use App\Module\ZincSearch\ZincSearchDialogMsg;
|
||||
use Cache;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class SyncUserMsgToZincSearch extends Command
|
||||
{
|
||||
/**
|
||||
* 更新数据
|
||||
* --f: 全量更新 (默认)
|
||||
* --i: 增量更新(从上次更新的最后一个ID接上)
|
||||
*
|
||||
* 清理数据
|
||||
* --c: 清除索引
|
||||
*/
|
||||
|
||||
protected $signature = 'zinc:sync-user-msg {--f} {--i} {--c} {--batch=1000}';
|
||||
protected $description = '同步聊天会话用户和消息到 ZincSearch';
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
// 注册信号处理器(仅在支持pcntl扩展的环境下)
|
||||
if (extension_loaded('pcntl')) {
|
||||
pcntl_async_signals(true); // 启用异步信号处理
|
||||
pcntl_signal(SIGINT, [$this, 'handleSignal']); // Ctrl+C
|
||||
pcntl_signal(SIGTERM, [$this, 'handleSignal']); // kill
|
||||
}
|
||||
|
||||
// 检查锁,如果已被占用则退出
|
||||
$lockInfo = $this->getLock();
|
||||
if ($lockInfo) {
|
||||
$this->error("命令已在运行中,开始时间: {$lockInfo['started_at']}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 设置锁
|
||||
$this->setLock();
|
||||
|
||||
// 清除索引
|
||||
if ($this->option('c')) {
|
||||
$this->info('清除索引...');
|
||||
ZincSearchKeyValue::clear();
|
||||
ZincSearchDialogMsg::clear();
|
||||
$this->info("索引删除成功");
|
||||
$this->releaseLock();
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->info('开始同步聊天数据...');
|
||||
|
||||
// 同步消息数据
|
||||
$this->syncDialogMsgs();
|
||||
|
||||
// 完成
|
||||
$this->info("\n同步完成");
|
||||
$this->releaseLock();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取锁信息
|
||||
*
|
||||
* @return array|null 如果锁存在返回锁信息,否则返回null
|
||||
*/
|
||||
private function getLock(): ?array
|
||||
{
|
||||
$lockKey = md5($this->signature);
|
||||
return Cache::has($lockKey) ? Cache::get($lockKey) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置锁
|
||||
*/
|
||||
private function setLock(): void
|
||||
{
|
||||
$lockKey = md5($this->signature);
|
||||
$lockInfo = [
|
||||
'started_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
Cache::put($lockKey, $lockInfo, 300); // 5分钟
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放锁
|
||||
*/
|
||||
private function releaseLock(): void
|
||||
{
|
||||
$lockKey = md5($this->signature);
|
||||
Cache::forget($lockKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理终端信号
|
||||
*
|
||||
* @param int $signal
|
||||
* @return void
|
||||
*/
|
||||
public function handleSignal(int $signal): void
|
||||
{
|
||||
// 释放锁
|
||||
$this->releaseLock();
|
||||
exit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步消息数据
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function syncDialogMsgs(): void
|
||||
{
|
||||
// 获取上次同步的最后ID
|
||||
$lastKey = "sync:dialogUserMsgLastId";
|
||||
$lastId = $this->option('i') ? intval(ZincSearchKeyValue::get($lastKey, 0)) : 0;
|
||||
|
||||
if ($lastId > 0) {
|
||||
$this->info("\n同步消息数据({$lastId})...");
|
||||
} else {
|
||||
$this->info("\n同步消息数据...");
|
||||
}
|
||||
|
||||
$num = 0;
|
||||
$count = WebSocketDialogMsg::where('id', '>', $lastId)->count();
|
||||
$batchSize = $this->option('batch');
|
||||
|
||||
$total = 0;
|
||||
$lastNum = 0;
|
||||
|
||||
do {
|
||||
// 获取一批
|
||||
$dialogMsgs = WebSocketDialogMsg::where('id', '>', $lastId)
|
||||
->orderBy('id')
|
||||
->limit($batchSize)
|
||||
->get();
|
||||
|
||||
if ($dialogMsgs->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
$num += count($dialogMsgs);
|
||||
$progress = round($num / $count * 100, 2);
|
||||
if ($progress < 100) {
|
||||
$progress = number_format($progress, 2);
|
||||
}
|
||||
$this->info("{$num}/{$count} ({$progress}%) 正在同步消息ID {$dialogMsgs->first()->id} ~ {$dialogMsgs->last()->id} ({$total}|{$lastNum})");
|
||||
|
||||
// 刷新锁
|
||||
$this->setLock();
|
||||
|
||||
// 同步数据
|
||||
$lastNum = ZincSearchDialogMsg::batchSync($dialogMsgs);
|
||||
$total += $lastNum;
|
||||
|
||||
// 更新最后ID
|
||||
$lastId = $dialogMsgs->last()->id;
|
||||
ZincSearchKeyValue::set($lastKey, $lastId);
|
||||
} while (count($dialogMsgs) == $batchSize);
|
||||
|
||||
$this->info("同步消息结束 - 最后ID {$lastId}");
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\WebSocket;
|
||||
use App\Services\RequestContext;
|
||||
use Hhxsv5\LaravelS\Swoole\Events\WorkerStartInterface;
|
||||
use Swoole\Http\Server;
|
||||
|
||||
@@ -25,5 +26,6 @@ class WorkerStartEvent implements WorkerStartInterface
|
||||
private function handleFirstWorkerTasks()
|
||||
{
|
||||
WebSocket::query()->delete();
|
||||
RequestContext::clearBaseUrlCache();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,12 @@ use App\Models\File;
|
||||
use App\Models\User;
|
||||
use App\Module\Base;
|
||||
use App\Module\Timer;
|
||||
use App\Models\Setting;
|
||||
use App\Module\Extranet;
|
||||
use App\Module\TimeRange;
|
||||
use App\Module\MsgTool;
|
||||
use App\Module\Table\OnlineData;
|
||||
use App\Models\FileContent;
|
||||
use App\Models\ProjectTask;
|
||||
use App\Models\AbstractModel;
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
@@ -25,6 +26,9 @@ use App\Models\WebSocketDialogConfig;
|
||||
use App\Models\WebSocketDialogMsgRead;
|
||||
use App\Models\WebSocketDialogMsgTodo;
|
||||
use App\Models\WebSocketDialogMsgTranslate;
|
||||
use App\Models\WebSocketDialogSession;
|
||||
use App\Module\Table\OnlineData;
|
||||
use App\Module\ZincSearch\ZincSearchDialogMsg;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
|
||||
/**
|
||||
@@ -116,34 +120,11 @@ class DialogController extends AbstractController
|
||||
return Base::retError('请输入搜索关键词');
|
||||
}
|
||||
// 搜索会话
|
||||
$list = DB::table('web_socket_dialog_users as u')
|
||||
->select(['d.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
|
||||
->join('web_socket_dialogs as d', 'u.dialog_id', '=', 'd.id')
|
||||
->where('u.userid', $user->userid)
|
||||
->where('d.name', 'LIKE', "%{$key}%")
|
||||
->whereNull('d.deleted_at')
|
||||
->orderByDesc('u.top_at')
|
||||
->orderByDesc('u.last_at')
|
||||
->take(20)
|
||||
->get()
|
||||
->map(function($item) use ($user) {
|
||||
return WebSocketDialog::synthesizeData($item, $user->userid);
|
||||
})
|
||||
->all();
|
||||
$take = 20;
|
||||
$list = WebSocketDialog::searchDialog($user->userid, $key, $take);
|
||||
// 搜索联系人
|
||||
if (count($list) < 20 && Base::judgeClientVersion("0.21.60")) {
|
||||
$users = User::select(User::$basicField)
|
||||
->where(function ($query) use ($key) {
|
||||
if (str_contains($key, "@")) {
|
||||
$query->where("email", "like", "%{$key}%");
|
||||
} else {
|
||||
$query->where("nickname", "like", "%{$key}%")
|
||||
->orWhere("pinyin", "like", "%{$key}%")
|
||||
->orWhere("profession", "like", "%{$key}%");
|
||||
}
|
||||
})->orderBy('userid')
|
||||
->take(20 - count($list))
|
||||
->get();
|
||||
if (count($list) < $take && Base::judgeClientVersion("0.21.60")) {
|
||||
$users = User::searchUser($key, $take - count($list));
|
||||
$users->transform(function (User $item) use ($user) {
|
||||
$id = 'u:' . $item->userid;
|
||||
$lastAt = null;
|
||||
@@ -169,29 +150,16 @@ class DialogController extends AbstractController
|
||||
$list = array_merge($list, $users->toArray());
|
||||
}
|
||||
// 搜索消息会话
|
||||
if (count($list) < 20) {
|
||||
$prefix = DB::getTablePrefix();
|
||||
if (preg_match('/[+\-><()~*"@]/', $key)) {
|
||||
$against = "\"{$key}\"";
|
||||
} else {
|
||||
$against = "*{$key}*";
|
||||
if (count($list) < $take) {
|
||||
$searchResults = ZincSearchDialogMsg::search($user->userid, $key, 0, $take - count($list));
|
||||
if ($searchResults) {
|
||||
foreach ($searchResults as $item) {
|
||||
if ($dialog = WebSocketDialog::find($item['id'])) {
|
||||
$dialog = array_merge($dialog->toArray(), $item);
|
||||
$list[] = WebSocketDialog::synthesizeData($dialog, $user->userid);
|
||||
}
|
||||
}
|
||||
}
|
||||
$msgs = DB::table('web_socket_dialog_users as u')
|
||||
->select(['d.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at', 'm.id as search_msg_id'])
|
||||
->join('web_socket_dialogs as d', 'u.dialog_id', '=', 'd.id')
|
||||
->join('web_socket_dialog_msgs as m', 'm.dialog_id', '=', 'd.id')
|
||||
->where('u.userid', $user->userid)
|
||||
->where('m.bot', 0)
|
||||
->whereNull('d.deleted_at')
|
||||
->whereRaw("MATCH({$prefix}m.key) AGAINST('{$against}' IN BOOLEAN MODE)")
|
||||
->orderByDesc('m.id')
|
||||
->take(20 - count($list))
|
||||
->get()
|
||||
->map(function($item) use ($user) {
|
||||
return WebSocketDialog::synthesizeData($item, $user->userid);
|
||||
})
|
||||
->all();
|
||||
$list = array_merge($list, $msgs);
|
||||
}
|
||||
//
|
||||
return Base::retSuccess('success', $list);
|
||||
@@ -251,14 +219,10 @@ class DialogController extends AbstractController
|
||||
//
|
||||
$dialog_id = intval(Request::input('dialog_id'));
|
||||
//
|
||||
$item = DB::table('web_socket_dialog_users as u')
|
||||
->select(['d.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
|
||||
->join('web_socket_dialogs as d', 'u.dialog_id', '=', 'd.id')
|
||||
->where('u.userid', $user->userid)
|
||||
->where('d.id', $dialog_id)
|
||||
->whereNull('d.deleted_at')
|
||||
->first();
|
||||
return Base::retSuccess('success', WebSocketDialog::synthesizeData($item, $user->userid));
|
||||
$dialog = WebSocketDialog::checkDialog($dialog_id);
|
||||
$data = WebSocketDialog::synthesizeData($dialog, $user->userid);
|
||||
//
|
||||
return Base::retSuccess('success', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -536,6 +500,9 @@ class DialogController extends AbstractController
|
||||
->on('read.msg_id', '=', 'web_socket_dialog_msgs.id');
|
||||
})->where('web_socket_dialog_msgs.dialog_id', $dialog_id);
|
||||
//
|
||||
if ($dialog->session_id > 0) {
|
||||
$builder->whereSessionId($dialog->session_id);
|
||||
}
|
||||
if ($msg_type) {
|
||||
if ($msg_type === 'tag') {
|
||||
$builder->where('tag', '>', 0);
|
||||
@@ -682,15 +649,18 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/search 14. 搜索消息位置
|
||||
* @api {get} api/dialog/msg/search 14. 搜索消息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup dialog
|
||||
* @apiName msg__search
|
||||
*
|
||||
* @apiParam {Number} dialog_id 对话ID
|
||||
* @apiParam {String} key 搜索关键词
|
||||
* @apiParam {Number} [dialog_id] 对话ID(存在则搜索消息在对话的位置)
|
||||
* @apiParam {Number} [take] 搜索数量
|
||||
* - dialog_id > 0, 默认:200,最大:200
|
||||
* - dialog_id <= 0, 默认:20,最大:50
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -698,22 +668,38 @@ class DialogController extends AbstractController
|
||||
*/
|
||||
public function msg__search()
|
||||
{
|
||||
User::auth();
|
||||
$user = User::auth();
|
||||
//
|
||||
$dialog_id = intval(Request::input('dialog_id'));
|
||||
$key = trim(Request::input('key'));
|
||||
$dialogId = intval(Request::input('dialog_id'));
|
||||
//
|
||||
if (empty($key)) {
|
||||
return Base::retError('关键词不能为空');
|
||||
}
|
||||
//
|
||||
WebSocketDialog::checkDialog($dialog_id);
|
||||
//
|
||||
$data = WebSocketDialogMsg::whereDialogId($dialog_id)
|
||||
->where('key', 'LIKE', "%{$key}%")
|
||||
->take(200)
|
||||
->pluck('id');
|
||||
return Base::retSuccess('success', compact('data'));
|
||||
if ($dialogId > 0) {
|
||||
// 搜索位置
|
||||
WebSocketDialog::checkDialog($dialogId);
|
||||
//
|
||||
$data = WebSocketDialogMsg::whereDialogId($dialogId)
|
||||
->where('key', 'LIKE', "%{$key}%")
|
||||
->take(Base::getPaginate(200, 200, 'take'))
|
||||
->pluck('id');
|
||||
return Base::retSuccess('success', compact('data'));
|
||||
} else {
|
||||
// 搜索消息
|
||||
$list = [];
|
||||
$searchResults = ZincSearchDialogMsg::search($user->userid, $key, 0, Base::getPaginate(50, 20, 'take'));
|
||||
if ($searchResults) {
|
||||
foreach ($searchResults as $item) {
|
||||
if ($dialog = WebSocketDialog::find($item['id'])) {
|
||||
$dialog = array_merge($dialog->toArray(), $item);
|
||||
$list[] = WebSocketDialog::synthesizeData($dialog, $user->userid);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Base::retSuccess('success', ['data' => $list]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1028,6 +1014,7 @@ class DialogController extends AbstractController
|
||||
* @apiParam {String} [silence] 是否静默发送
|
||||
* - no: 正常发送(默认)
|
||||
* - yes: 静默发送
|
||||
* @apiParam {String} [model_name] 模型名称(仅AI机器人支持)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -1048,16 +1035,20 @@ class DialogController extends AbstractController
|
||||
$key = trim(Request::input('key'));
|
||||
$text_type = strtolower(trim(Request::input('text_type')));
|
||||
$silence = in_array(strtolower(trim(Request::input('silence'))), ['yes', 'true', '1']);
|
||||
$model_name = trim(Request::input('model_name'));
|
||||
$markdown = in_array($text_type, ['md', 'markdown']);
|
||||
//
|
||||
$result = [];
|
||||
$dialogIds = $dialog_ids ? explode(',', $dialog_ids) : [$dialog_id ?: 0];
|
||||
foreach ($dialogIds as $dialog_id) {
|
||||
//
|
||||
WebSocketDialog::checkDialog($dialog_id);
|
||||
$dialog = WebSocketDialog::checkDialog($dialog_id);
|
||||
//
|
||||
if ($update_id > 0) {
|
||||
$action = $update_mark ? "update-$update_id" : "change-$update_id";
|
||||
if (!$user->bot && !$dialog->isSelfDialog()) {
|
||||
Setting::validateMsgLimit('edit', $update_id);
|
||||
}
|
||||
} elseif ($reply_id > 0) {
|
||||
$action = "reply-$reply_id";
|
||||
if ($reply_check === 'yes') {
|
||||
@@ -1096,16 +1087,21 @@ class DialogController extends AbstractController
|
||||
if (empty($size)) {
|
||||
return Base::retError('消息发送保存失败');
|
||||
}
|
||||
$ext = $markdown ? 'md' : 'htm';
|
||||
$text = MsgTool::truncateText($text, 500, $ext);
|
||||
$desc = strip_tags($markdown ? Base::markdown2html($text) : $text);
|
||||
$type = $markdown ? 'md' : 'htm';
|
||||
$desc = $text;
|
||||
if ($markdown) {
|
||||
$desc = preg_replace("/:::\s*reasoning[\s\S]*?:::/", "", $desc);
|
||||
$desc = Base::markdown2html($desc);
|
||||
}
|
||||
$desc = strip_tags($desc);
|
||||
$desc = mb_substr(WebSocketDialogMsg::filterEscape($desc), 0, 200);
|
||||
$text = MsgTool::truncateText($text, 500, $type);
|
||||
$msgData = [
|
||||
'type' => $type, // 内容类型
|
||||
'desc' => $desc, // 描述内容
|
||||
'text' => $text, // 简要内容
|
||||
'type' => $ext, // 内容类型
|
||||
'file' => [
|
||||
'name' => "LongText-{$strlen}.{$ext}",
|
||||
'name' => "LongText-{$strlen}.{$type}",
|
||||
'size' => $size,
|
||||
'file' => $file,
|
||||
'path' => $path,
|
||||
@@ -1113,18 +1109,24 @@ class DialogController extends AbstractController
|
||||
'thumb' => '',
|
||||
'width' => -1,
|
||||
'height' => -1,
|
||||
'ext' => $ext,
|
||||
'ext' => $type,
|
||||
],
|
||||
];
|
||||
if (empty($key)) {
|
||||
$key = $desc;
|
||||
}
|
||||
if ($model_name) {
|
||||
$msgData['model_name'] = $model_name;
|
||||
}
|
||||
$result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'longtext', $msgData, $user->userid, false, false, $silence, $key);
|
||||
} else {
|
||||
$msgData = ['text' => $text];
|
||||
if ($markdown) {
|
||||
$msgData['type'] = 'md';
|
||||
}
|
||||
if ($model_name) {
|
||||
$msgData['model_name'] = $model_name;
|
||||
}
|
||||
$result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'text', $msgData, $user->userid, false, false, $silence, $key);
|
||||
}
|
||||
}
|
||||
@@ -1302,7 +1304,79 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/dialog/msg/sendfile 25. 文件上传
|
||||
* @api {post} api/dialog/msg/convertrecord 25. 录音转文字
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup dialog
|
||||
* @apiName msg__convertrecord
|
||||
*
|
||||
* @apiParam {String} base64 语音base64
|
||||
* @apiParam {Number} duration 语音时长(毫秒)
|
||||
* @apiParam {String} [language] 识别语言
|
||||
* - 比如:zh
|
||||
* - 默认:自动识别
|
||||
* - 格式:符合 ISO_639 标准
|
||||
* - 此参数不一定起效果,AI会根据语音和language参考翻译识别结果
|
||||
* @apiParam {String} [translate] 翻译识别结果
|
||||
* - 比如:zh
|
||||
* - 默认:不翻译结果
|
||||
* - 格式:符合 ISO_639 标准
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function msg__convertrecord()
|
||||
{
|
||||
$user = User::auth();
|
||||
$user->checkChatInformation();
|
||||
//
|
||||
$path = "uploads/tmp/chat/" . date("Ym") . "/" . $user->userid . "/";
|
||||
$base64 = Request::input('base64');
|
||||
$language = Request::input('language');
|
||||
$translate = Request::input('translate');
|
||||
$duration = intval(Request::input('duration'));
|
||||
if ($duration < 600) {
|
||||
return Base::retError('说话时间太短');
|
||||
}
|
||||
// 保存录音
|
||||
$data = Base::record64save([
|
||||
"base64" => $base64,
|
||||
"path" => $path,
|
||||
]);
|
||||
if (Base::isError($data)) {
|
||||
return Base::retError($data['msg']);
|
||||
}
|
||||
$recordData = $data['data'];
|
||||
// 转文字
|
||||
$extParams = [];
|
||||
if ($language) {
|
||||
$extParams = [
|
||||
'language' => $language === 'zh-CHT' ? 'zh' : $language,
|
||||
'prompt' => "将此语音识别为“" . Doo::getLanguages($language) . "”。",
|
||||
];
|
||||
}
|
||||
$result = Extranet::openAItranscriptions($recordData['file'], $extParams);
|
||||
if (Base::isError($result)) {
|
||||
return $result;
|
||||
}
|
||||
if (strlen($result['data']) < 1) {
|
||||
return Base::retError('转文字失败');
|
||||
}
|
||||
// 翻译
|
||||
if ($translate) {
|
||||
$result = Extranet::openAItranslations($result['data'], Doo::getLanguages($translate));
|
||||
if (Base::isError($result)) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
// 返回
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/dialog/msg/sendfile 26. 文件上传
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1334,7 +1408,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/dialog/msg/sendfiles 26. 群发文件上传
|
||||
* @api {post} api/dialog/msg/sendfiles 27. 群发文件上传
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1390,7 +1464,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/sendfileid 27. 通过文件ID发送文件
|
||||
* @api {get} api/dialog/msg/sendfileid 28. 通过文件ID发送文件
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1400,6 +1474,7 @@ class DialogController extends AbstractController
|
||||
* @apiParam {Number} file_id 消息ID
|
||||
* @apiParam {Array} dialogids 转发给的对话ID
|
||||
* @apiParam {Array} userids 转发给的成员ID
|
||||
* @apiParam {String} leave_message 转发留言
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -1412,55 +1487,63 @@ class DialogController extends AbstractController
|
||||
$file_id = intval(Request::input("file_id"));
|
||||
$dialogids = Request::input('dialogids');
|
||||
$userids = Request::input('userids');
|
||||
$leave_message = Request::input('leave_message');
|
||||
//
|
||||
if (empty($dialogids) && empty($userids)) {
|
||||
return Base::retError("请选择转发对话或成员");
|
||||
return Base::retError("请选择对话或成员");
|
||||
}
|
||||
//
|
||||
$file = File::permissionFind($file_id, $user);
|
||||
$fileLink = $file->getShareLink($user->userid);
|
||||
$fileMsg = "<a class=\"mention file\" href=\"{{RemoteURL}}single/file/{$fileLink['code']}\" target=\"_blank\">~{$file->getNameAndExt()}</a>";
|
||||
$fileMsg = "<p><a class=\"mention file\" href=\"{{RemoteURL}}single/file/{$fileLink['code']}\" target=\"_blank\">~{$file->getNameAndExt()}</a></p>";
|
||||
if ($leave_message) {
|
||||
$fileMsg .= "<p>{$leave_message}</p>";
|
||||
}
|
||||
//
|
||||
return AbstractModel::transaction(function() use ($user, $fileMsg, $userids, $dialogids) {
|
||||
$msgs = [];
|
||||
$already = [];
|
||||
if ($dialogids) {
|
||||
if (!is_array($dialogids)) {
|
||||
$dialogids = [$dialogids];
|
||||
}
|
||||
foreach ($dialogids as $dialogid) {
|
||||
$res = WebSocketDialogMsg::sendMsg(null, $dialogid, 'text', ['text' => $fileMsg], $user->userid);
|
||||
if (Base::isSuccess($res)) {
|
||||
$msgs[] = $res['data'];
|
||||
$already[] = $dialogid;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($userids) {
|
||||
if (!is_array($userids)) {
|
||||
$userids = [$userids];
|
||||
}
|
||||
foreach ($userids as $userid) {
|
||||
if (!User::whereUserid($userid)->exists()) {
|
||||
continue;
|
||||
}
|
||||
$dialog = WebSocketDialog::checkUserDialog($user, $userid);
|
||||
if ($dialog && !in_array($dialog->id, $already)) {
|
||||
$res = WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $fileMsg], $user->userid);
|
||||
if (Base::isSuccess($res)) {
|
||||
$msgs[] = $res['data'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Base::retSuccess('发送成功', [
|
||||
'msgs' => $msgs
|
||||
]);
|
||||
});
|
||||
return WebSocketDialogMsg::sendMsgBatch($user, $userids, $dialogids, $fileMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/dialog/msg/sendanon 28. 发送匿名消息
|
||||
* @api {get} api/dialog/msg/sendtaskid 29. 通过任务ID发送任务
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup dialog
|
||||
* @apiName msg__sendtaskid
|
||||
*
|
||||
* @apiParam {Number} task_id 消息ID
|
||||
* @apiParam {Array} dialogids 转发给的对话ID
|
||||
* @apiParam {Array} userids 转发给的成员ID
|
||||
* @apiParam {String} leave_message 转发留言
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function msg__sendtaskid()
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$task_id = intval(Request::input("task_id"));
|
||||
$dialogids = Request::input('dialogids');
|
||||
$userids = Request::input('userids');
|
||||
$leave_message = Request::input('leave_message');
|
||||
//
|
||||
if (empty($dialogids) && empty($userids)) {
|
||||
return Base::retError("请选择对话或成员");
|
||||
}
|
||||
//
|
||||
$task = ProjectTask::userTask($task_id, null);
|
||||
$taskMsg = "<p><span class=\"mention task\" data-id=\"{$task_id}\">#{$task->name}</span></p>";
|
||||
if ($leave_message) {
|
||||
$taskMsg .= "<p>{$leave_message}</p>";
|
||||
}
|
||||
//
|
||||
return WebSocketDialogMsg::sendMsgBatch($user, $userids, $dialogids, $taskMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/dialog/msg/sendanon 30. 发送匿名消息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1516,7 +1599,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/dialog/msg/sendlocation 29. 发送位置消息
|
||||
* @api {post} api/dialog/msg/sendlocation 31. 发送位置消息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1576,7 +1659,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/readlist 30. 获取消息阅读情况
|
||||
* @api {get} api/dialog/msg/readlist 32. 获取消息阅读情况
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1605,7 +1688,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/detail 31. 消息详情
|
||||
* @api {get} api/dialog/msg/detail 33. 消息详情
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1665,7 +1748,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/download 32. 文件下载
|
||||
* @api {get} api/dialog/msg/download 34. 文件下载
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1702,11 +1785,11 @@ class DialogController extends AbstractController
|
||||
}
|
||||
//
|
||||
$filePath = public_path($array['path']);
|
||||
return Base::BinaryFileResponse($filePath, $array['name']);
|
||||
return Base::DownloadFileResponse($filePath, $array['name']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/withdraw 33. 聊天消息撤回
|
||||
* @api {get} api/dialog/msg/withdraw 35. 聊天消息撤回
|
||||
*
|
||||
* @apiDescription 消息撤回限制24小时内,需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1727,12 +1810,17 @@ class DialogController extends AbstractController
|
||||
if (empty($msg)) {
|
||||
return Base::retError("消息不存在或已被删除");
|
||||
}
|
||||
$dialog = WebSocketDialog::checkDialog($msg->dialog_id);
|
||||
//
|
||||
if (!$user->bot && !$dialog->isSelfDialog()) {
|
||||
Setting::validateMsgLimit('rev', $msg);
|
||||
}
|
||||
$msg->withdrawMsg();
|
||||
return Base::retSuccess("success");
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/voice2text 34. 语音消息转文字
|
||||
* @api {get} api/dialog/msg/voice2text 36. 语音消息转文字
|
||||
*
|
||||
* @apiDescription 将语音消息转文字,需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1784,7 +1872,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/translation 35. 翻译消息
|
||||
* @api {get} api/dialog/msg/translation 37. 翻译消息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1792,6 +1880,8 @@ class DialogController extends AbstractController
|
||||
* @apiName msg__translation
|
||||
*
|
||||
* @apiParam {Number} msg_id 消息ID
|
||||
* @apiParam {Number} [force] 强制翻译(1是、0否)
|
||||
* - 默认不强制翻译,已翻译过的消息不再翻译
|
||||
* @apiParam {String} [language] 目标语言,默认当前语言
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
@@ -1803,6 +1893,7 @@ class DialogController extends AbstractController
|
||||
User::auth();
|
||||
//
|
||||
$msg_id = intval(Request::input("msg_id"));
|
||||
$force = intval(Request::input("force"));
|
||||
$language = Base::inputOrHeader('language');
|
||||
$targetLanguage = Doo::getLanguages($language);
|
||||
//
|
||||
@@ -1820,13 +1911,20 @@ class DialogController extends AbstractController
|
||||
//
|
||||
$row = WebSocketDialogMsgTranslate::whereMsgId($msg_id)->whereLanguage($language)->first();
|
||||
if ($row) {
|
||||
return Base::retSuccess("success", $row->only(['msg_id', 'language', 'content']));
|
||||
if ($force) {
|
||||
$row->delete();
|
||||
} else {
|
||||
return Base::retSuccess("success", $row->only(['msg_id', 'language', 'content']));
|
||||
}
|
||||
}
|
||||
//
|
||||
$msgData = Base::json2array($msg->getRawOriginal('msg'));
|
||||
if (empty($msgData['text'])) {
|
||||
return Base::retError("消息内容为空");
|
||||
}
|
||||
if ($msg->type === 'text' && $msgData['type'] === 'md') {
|
||||
$msgData['text'] = preg_replace('/:::\s*reasoning.*?:::/s', '', $msgData['text']);
|
||||
}
|
||||
$res = Extranet::openAItranslations($msgData['text'], $targetLanguage);
|
||||
if (Base::isError($res)) {
|
||||
return $res;
|
||||
@@ -1843,7 +1941,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/mark 36. 消息标记操作
|
||||
* @api {get} api/dialog/msg/mark 38. 消息标记操作
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1907,7 +2005,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/silence 37. 消息免打扰
|
||||
* @api {get} api/dialog/msg/silence 39. 消息免打扰
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1970,7 +2068,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/forward 38. 转发消息给
|
||||
* @api {get} api/dialog/msg/forward 40. 转发消息给
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1981,7 +2079,7 @@ class DialogController extends AbstractController
|
||||
* @apiParam {Array} dialogids 转发给的对话ID
|
||||
* @apiParam {Array} userids 转发给的成员ID
|
||||
* @apiParam {Number} show_source 是否显示原发送者信息
|
||||
* @apiParam {Array} leave_message 转发留言
|
||||
* @apiParam {String} leave_message 转发留言
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -1998,7 +2096,7 @@ class DialogController extends AbstractController
|
||||
$leave_message = Request::input('leave_message');
|
||||
//
|
||||
if (empty($dialogids) && empty($userids)) {
|
||||
return Base::retError("请选择转发对话或成员");
|
||||
return Base::retError("请选择对话或成员");
|
||||
}
|
||||
//
|
||||
$msg = WebSocketDialogMsg::whereId($msg_id)->first();
|
||||
@@ -2011,7 +2109,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/emoji 39. emoji回复
|
||||
* @api {get} api/dialog/msg/emoji 41. emoji回复
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -2046,7 +2144,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/tag 40. 标注/取消标注
|
||||
* @api {get} api/dialog/msg/tag 42. 标注/取消标注
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -2075,7 +2173,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/todo 41. 设待办/取消待办
|
||||
* @api {get} api/dialog/msg/todo 43. 设待办/取消待办
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -2118,7 +2216,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/todolist 42. 获取消息待办情况
|
||||
* @api {get} api/dialog/msg/todolist 44. 获取消息待办情况
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -2148,7 +2246,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/done 43. 完成待办
|
||||
* @api {get} api/dialog/msg/done 45. 完成待办
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -2201,7 +2299,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/color 44. 设置颜色
|
||||
* @api {get} api/dialog/msg/color 46. 设置颜色
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -2242,7 +2340,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/group/add 45. 新增群组
|
||||
* @api {get} api/dialog/group/add 47. 新增群组
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -2304,7 +2402,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/group/edit 46. 修改群组
|
||||
* @api {get} api/dialog/group/edit 48. 修改群组
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -2366,7 +2464,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/group/adduser 47. 添加群成员
|
||||
* @api {get} api/dialog/group/adduser 49. 添加群成员
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* - 有群主时:只有群主可以邀请
|
||||
@@ -2402,7 +2500,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/group/deluser 48. 移出(退出)群成员
|
||||
* @api {get} api/dialog/group/deluser 50. 移出(退出)群成员
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* - 只有群主、邀请人可以踢人
|
||||
@@ -2446,7 +2544,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/group/transfer 49. 转让群组
|
||||
* @api {get} api/dialog/group/transfer 51. 转让群组
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* - 只有群主且是个人类型群可以解散
|
||||
@@ -2495,7 +2593,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/group/disband 50. 解散群组
|
||||
* @api {get} api/dialog/group/disband 52. 解散群组
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* - 只有群主且是个人类型群可以解散
|
||||
@@ -2523,7 +2621,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/group/searchuser 51. 搜索个人群(仅限管理员)
|
||||
* @api {get} api/dialog/group/searchuser 53. 搜索个人群(仅限管理员)
|
||||
*
|
||||
* @apiDescription 需要token身份,用于创建部门搜索个人群组
|
||||
* @apiVersion 1.0.0
|
||||
@@ -2552,7 +2650,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/dialog/okr/add 52. 创建OKR评论会话
|
||||
* @api {post} api/dialog/okr/add 54. 创建OKR评论会话
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -2591,7 +2689,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/dialog/okr/push 53. 推送OKR相关信息
|
||||
* @api {post} api/dialog/okr/push 55. 推送OKR相关信息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -2627,7 +2725,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/dialog/msg/wordchain 54. 发送接龙消息
|
||||
* @api {post} api/dialog/msg/wordchain 56. 发送接龙消息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -2713,7 +2811,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/dialog/msg/vote 55. 发起投票
|
||||
* @api {post} api/dialog/msg/vote 57. 发起投票
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -2829,7 +2927,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/top 56. 置顶/取消置顶
|
||||
* @api {get} api/dialog/msg/top 58. 置顶/取消置顶
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -2889,7 +2987,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/topinfo 57. 获取置顶消息
|
||||
* @api {get} api/dialog/msg/topinfo 59. 获取置顶消息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -2916,7 +3014,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/applied 58. 标记消息已应用
|
||||
* @api {get} api/dialog/msg/applied 60. 标记消息已应用
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -2965,7 +3063,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/sticker/search 59. 搜索在线表情
|
||||
* @api {get} api/dialog/sticker/search 61. 搜索在线表情
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -2989,7 +3087,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/config 60. 获取会话配置
|
||||
* @api {get} api/dialog/config 62. 获取会话配置
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -3025,7 +3123,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/dialog/config/save 61. 保存会话配置
|
||||
* @api {post} api/dialog/config/save 63. 保存会话配置
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -3063,10 +3161,142 @@ class DialogController extends AbstractController
|
||||
]
|
||||
)) {
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog_id, 'notice', [
|
||||
'notice' => $value ? ("修改提示词:" . $value) : "取消提示词",
|
||||
'notice' => $value ? ("修改提示词:" . Base::cutStr($value, 100)) : "取消提示词",
|
||||
], User::userid(), true, true);
|
||||
}
|
||||
|
||||
return Base::retSuccess('保存成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/session/create 64. AI-开启新会话
|
||||
*
|
||||
* @apiDescription 需要token身份,仅限与AI用户会话
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup dialog
|
||||
* @apiName session_create
|
||||
*
|
||||
* @apiParam {Number} dialog_id 对话ID
|
||||
* @apiParam {Number} [userid] 用户ID(与 dialog_id 二选一,userid 优先)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function session__create()
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$dialog_id = intval(Request::input('dialog_id'));
|
||||
$userid = intval(Request::input('userid'));
|
||||
//
|
||||
if ($userid) {
|
||||
$dialog = WebSocketDialog::checkUserDialog($user, $userid);
|
||||
} else {
|
||||
$dialog = WebSocketDialog::checkDialog($dialog_id);
|
||||
}
|
||||
//
|
||||
if ($dialog->type != 'user') {
|
||||
return Base::retError('当前对话不支持');
|
||||
}
|
||||
//
|
||||
$hasAiUser = WebSocketDialogUser::join('users as u', 'web_socket_dialog_users.userid', '=', 'u.userid')
|
||||
->where('dialog_id', $dialog->id)
|
||||
->where('u.email', 'like', 'ai-%@bot.system')
|
||||
->exists();
|
||||
if (!$hasAiUser) {
|
||||
return Base::retError('当前对话不支持');
|
||||
}
|
||||
//
|
||||
$session = WebSocketDialogSession::whereDialogId($dialog->id)->whereTitle('')->first();
|
||||
if ($session) {
|
||||
$dialog->session_id = $session->id;
|
||||
$dialog->save();
|
||||
return Base::retSuccess('success', $session);
|
||||
}
|
||||
//
|
||||
$session = WebSocketDialogSession::create([
|
||||
'dialog_id' => $dialog->id,
|
||||
'status' => 1,
|
||||
'title' => '',
|
||||
]);
|
||||
$session->save();
|
||||
$dialog->session_id = $session->id;
|
||||
$dialog->save();
|
||||
//
|
||||
return Base::retSuccess('success', $session);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/session/list 65. AI-获取会话列表
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup dialog
|
||||
* @apiName session_list
|
||||
*
|
||||
* @apiParam {Number} dialog_id 对话ID
|
||||
*
|
||||
* @apiParam {Number} [page] 当前页,默认:1
|
||||
* @apiParam {Number} [pagesize] 每页显示数量,默认:20,最大:50
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function session__list()
|
||||
{
|
||||
User::auth();
|
||||
//
|
||||
$dialog_id = intval(Request::input('dialog_id'));
|
||||
//
|
||||
$dialog = WebSocketDialog::checkDialog($dialog_id);
|
||||
//
|
||||
$sessions = WebSocketDialogSession::whereDialogId($dialog->id)
|
||||
->orderByDesc('id')
|
||||
->paginate(Base::getPaginate(100, 10));
|
||||
$sessions->transform(function ($item) use ($dialog) {
|
||||
if ($item->id === $dialog->session_id) {
|
||||
$item->is_open = 1;
|
||||
} else {
|
||||
$item->is_open = 0;
|
||||
}
|
||||
return $item;
|
||||
});
|
||||
//
|
||||
return Base::retSuccess('success', $sessions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/session/open 66. AI-打开会话
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup dialog
|
||||
* @apiName session_open
|
||||
*
|
||||
* @apiParam {Number} session_id 会话ID
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function session__open()
|
||||
{
|
||||
User::auth();
|
||||
//
|
||||
$session_id = intval(Request::input('session_id'));
|
||||
//
|
||||
$session = WebSocketDialogSession::whereId($session_id)->first();
|
||||
if (empty($session)) {
|
||||
return Base::retError('会话不存在或已被删除');
|
||||
}
|
||||
//
|
||||
$dialog = WebSocketDialog::checkDialog($session->dialog_id);
|
||||
//
|
||||
$dialog->session_id = $session->id;
|
||||
$dialog->save();
|
||||
//
|
||||
return Base::retSuccess('success', $session);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ class FileController extends AbstractController
|
||||
}
|
||||
return Base::retError($msg, $data);
|
||||
}
|
||||
$fileLink->increment("num");
|
||||
} else {
|
||||
return Base::retError('参数错误');
|
||||
}
|
||||
@@ -106,6 +107,7 @@ class FileController extends AbstractController
|
||||
*
|
||||
* @apiParam {String} [link] 通过分享地址搜索(如:https://t.hitosea.com/single/file/ODcwOCwzOSxpa0JBS2lmVQ==)
|
||||
* @apiParam {String} [key] 关键词
|
||||
* @apiParam {Number} [take] 获取数量(默认:50,最大:100)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -118,7 +120,7 @@ class FileController extends AbstractController
|
||||
$link = trim(Request::input('link'));
|
||||
$key = trim(Request::input('key'));
|
||||
$id = 0;
|
||||
$take = 50;
|
||||
$take = Base::getPaginate(100, 50, 'take');
|
||||
if (preg_match("/\/single\/file\/(.*?)$/i", $link, $match)) {
|
||||
$id = intval(FileLink::whereCode($match[1])->value('file_id'));
|
||||
$take = 1;
|
||||
@@ -664,7 +666,7 @@ class FileController extends AbstractController
|
||||
//
|
||||
if ($status === 2) {
|
||||
$parse = parse_url($url);
|
||||
$from = 'http://' . env('APP_IPPR') . '.3' . $parse['path'] . '?' . $parse['query'];
|
||||
$from = 'http://nginx' . $parse['path'] . '?' . $parse['query'];
|
||||
$path = 'uploads/file/' . $file->type . '/' . date("Ym") . '/' . $file->id . '/' . $key;
|
||||
$save = public_path($path);
|
||||
Base::makeDir(dirname($save));
|
||||
|
||||
@@ -941,7 +941,9 @@ class ProjectController extends AbstractController
|
||||
* @apiName task__lists
|
||||
*
|
||||
* @apiParam {Object} [keys] 搜索条件
|
||||
* - keys.name: ID、任务名称
|
||||
* - keys.name: ID、任务名称、任务描述
|
||||
* - keys.tag: 标签名称
|
||||
* - keys.status: 任务状态 (completed: 已完成、uncompleted: 未完成、flow-xx: 流程状态ID)
|
||||
*
|
||||
* @apiParam {Number} [project_id] 项目ID
|
||||
* @apiParam {Number} [parent_id] 主任务ID(project_id && parent_id ≤ 0 时 仅查询自己参与的任务)
|
||||
@@ -994,7 +996,29 @@ class ProjectController extends AbstractController
|
||||
if (Base::isNumber($keys['name'])) {
|
||||
$builder->where("project_tasks.id", intval($keys['name']));
|
||||
} else {
|
||||
$builder->where("project_tasks.name", "like", "%{$keys['name']}%");
|
||||
$builder->where(function ($query) use ($keys) {
|
||||
$query->where("project_tasks.name", "like", "%{$keys['name']}%");
|
||||
$query->orWhere("project_tasks.desc", "like", "%{$keys['name']}%");
|
||||
});
|
||||
}
|
||||
}
|
||||
if ($keys['tag']) {
|
||||
$builder->whereHas('taskTag', function ($query) use ($keys) {
|
||||
$query->where('project_task_tags.name', $keys['tag']);
|
||||
});
|
||||
}
|
||||
if ($keys['status']) {
|
||||
if ($keys['status'] == 'completed') {
|
||||
$builder->whereNotNull('project_tasks.complete_at');
|
||||
} elseif ($keys['status'] == 'uncompleted') {
|
||||
$builder->whereNull('project_tasks.complete_at');
|
||||
} elseif (str_starts_with($keys['status'], 'flow-')) {
|
||||
$flow = str_replace('flow-', '', $keys['status']);
|
||||
if (Base::isNumber($flow)) {
|
||||
$builder->where('project_tasks.flow_item_id', intval($flow));
|
||||
} elseif ($flow) {
|
||||
$builder->where('project_tasks.flow_item_name', 'like', "%{$flow}%");
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
@@ -1058,20 +1082,20 @@ class ProjectController extends AbstractController
|
||||
$query->where('project_users.owner', 1);
|
||||
$query->where('project_users.userid', $userid);
|
||||
});
|
||||
$builder->leftJoin('project_task_users as project_sub_task_users', function ($query) use($userid) {
|
||||
$query->on('project_sub_task_users.task_pid', '=', 'project_tasks.parent_id');
|
||||
$query->where('project_sub_task_users.userid', $userid);
|
||||
});
|
||||
$builder->leftJoin('project_task_visibility_users', function ($query) use($userid) {
|
||||
$query->on('project_task_visibility_users.task_id', '=', 'project_tasks.id');
|
||||
$query->where('project_task_visibility_users.userid', $userid);
|
||||
});
|
||||
$builder->leftJoin('project_task_visibility_users as project_sub_task_visibility_users', function ($query) use($userid) {
|
||||
$query->on('project_sub_task_visibility_users.task_id', '=', 'project_tasks.parent_id');
|
||||
$query->where('project_sub_task_visibility_users.userid', $userid);
|
||||
});
|
||||
$builder->where(function ($query) use ($userid) {
|
||||
$query->where("project_tasks.visibility", 1);
|
||||
$query->orWhere("project_users.userid", $userid);
|
||||
$query->orWhere("project_task_users.userid", $userid);
|
||||
$query->orWhere("project_task_visibility_users.userid", $userid);
|
||||
$query->orWhere("project_sub_task_users.userid", $userid);
|
||||
$query->orWhere("project_sub_task_visibility_users.userid", $userid);
|
||||
});
|
||||
// 优化子查询汇总
|
||||
$builder->leftJoinSub(function ($query) {
|
||||
@@ -1159,6 +1183,7 @@ class ProjectController extends AbstractController
|
||||
$list = ProjectTask::with(['taskUser'])
|
||||
->select([
|
||||
'projects.name as project_name',
|
||||
'project_tasks.project_id',
|
||||
'project_tasks.id',
|
||||
'project_tasks.name',
|
||||
'project_tasks.start_at',
|
||||
@@ -1758,6 +1783,13 @@ class ProjectController extends AbstractController
|
||||
//
|
||||
ProjectPermission::userTaskPermission(Project::userProject($task->project_id), ProjectPermission::TASK_REMOVE, $task);
|
||||
//
|
||||
$task->addLog('删除附件:' . $file->name, [
|
||||
'file_id' => $file->id,
|
||||
'name' => $file->name,
|
||||
'size' => $file->size,
|
||||
'path' => $file->getRawOriginal('path'),
|
||||
'thumb' => $file->getRawOriginal('thumb'),
|
||||
]);
|
||||
$task->pushMsg('filedelete', $file);
|
||||
$file->delete();
|
||||
//
|
||||
@@ -1852,7 +1884,7 @@ class ProjectController extends AbstractController
|
||||
}
|
||||
//
|
||||
$filePath = public_path($file->getRawOriginal('path'));
|
||||
return Base::BinaryFileResponse($filePath, $file->name);
|
||||
return Base::DownloadFileResponse($filePath, $file->name);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2297,8 +2329,8 @@ class ProjectController extends AbstractController
|
||||
* @apiGroup project
|
||||
* @apiName task__flow
|
||||
*
|
||||
* @apiParam {Number} task_id 任务ID
|
||||
* @apiParam {Number} project_id 项目ID - 存在时只返回这个项目的
|
||||
* @apiParam {Number} [task_id] 任务ID
|
||||
* @apiParam {Number} [project_id] 项目ID(存在时只返回这个项目的工作流,主要用于任务移动到其他项目时)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -2392,6 +2424,11 @@ class ProjectController extends AbstractController
|
||||
* @apiParam {Number} flow_item_id 工作流id
|
||||
* @apiParam {Array} owner 负责人
|
||||
* @apiParam {Array} assist 协助人
|
||||
* @apiParam {String} [completed] 是否已完成
|
||||
* - 没有 工作流id 时此参数才生效
|
||||
* - 有值表示已完成
|
||||
* - 空值表示未完成
|
||||
* - 不存在不改变状态
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -2408,7 +2445,7 @@ class ProjectController extends AbstractController
|
||||
$flow_item_id = intval(Request::input('flow_item_id'));
|
||||
$owner = Request::input('owner', []);
|
||||
$assist = Request::input('assist', []);
|
||||
$completeAt = trim(Request::input('complete_at', ''));
|
||||
$completed = Request::exists('completed') ? (bool)Request::input('completed') : null;
|
||||
//
|
||||
$task = ProjectTask::userTask($task_id);
|
||||
//
|
||||
@@ -2429,13 +2466,13 @@ class ProjectController extends AbstractController
|
||||
if (empty($flowItem)) {
|
||||
return Base::retError('任务状态不存在');
|
||||
}
|
||||
} else if (!$flow_item_id && !$completeAt) {
|
||||
} else {
|
||||
if (projectFlowItem::whereProjectId($project->id)->count() > 0) {
|
||||
return Base::retError('请选择移动后状态', [], 102);
|
||||
}
|
||||
}
|
||||
//
|
||||
$task->moveTask($project_id, $column_id, $flow_item_id, $owner, $assist, $completeAt);
|
||||
$task->moveTask($project_id, $column_id, $flow_item_id, $owner, $assist, $completed);
|
||||
//
|
||||
$data = [];
|
||||
$mainTask = ProjectTask::userTask($task_id)?->toArray();
|
||||
@@ -2585,7 +2622,7 @@ class ProjectController extends AbstractController
|
||||
$builder->with(['projectTask:id,parent_id,name'])->whereProjectId($project->id)->whereTaskOnly(0);
|
||||
}
|
||||
//
|
||||
$list = $builder->orderByDesc('created_at')->paginate(Base::getPaginate(100, 20));
|
||||
$list = $builder->orderByDesc('created_at')->orderByDesc('id')->paginate(Base::getPaginate(100, 20));
|
||||
$list->transform(function (ProjectLog $log) use ($task_id) {
|
||||
$timestamp = Carbon::parse($log->created_at)->timestamp;
|
||||
if ($task_id === 0) {
|
||||
|
||||
@@ -6,8 +6,10 @@ use App\Exceptions\ApiException;
|
||||
use App\Models\AbstractModel;
|
||||
use App\Models\ProjectTask;
|
||||
use App\Models\Report;
|
||||
use App\Models\ReportLink;
|
||||
use App\Models\ReportReceive;
|
||||
use App\Models\User;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
use App\Tasks\PushTask;
|
||||
@@ -28,11 +30,13 @@ class ReportController extends AbstractController
|
||||
/**
|
||||
* @api {get} api/report/my 01. 我发送的汇报
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName my
|
||||
*
|
||||
* @apiParam {Object} [keys] 搜索条件
|
||||
* - keys.key: 关键词
|
||||
* - keys.type: 汇报类型,weekly:周报,daily:日报
|
||||
* - keys.created_at: 汇报时间
|
||||
* @apiParam {Number} [page] 当前页,默认:1
|
||||
@@ -49,6 +53,15 @@ class ReportController extends AbstractController
|
||||
$builder = Report::with(['receivesUser'])->whereUserid($user->userid);
|
||||
$keys = Request::input('keys');
|
||||
if (is_array($keys)) {
|
||||
if ($keys['key']) {
|
||||
if (str_contains($keys['key'], '@')) {
|
||||
$builder->whereHas('sendUser', function ($q2) use ($keys) {
|
||||
$q2->where("users.email", "LIKE", "%{$keys['key']}%");
|
||||
});
|
||||
} else {
|
||||
$builder->where("title", "LIKE", "%{$keys['key']}%");
|
||||
}
|
||||
}
|
||||
if (in_array($keys['type'], [Report::WEEKLY, Report::DAILY])) {
|
||||
$builder->whereType($keys['type']);
|
||||
}
|
||||
@@ -64,13 +77,16 @@ class ReportController extends AbstractController
|
||||
/**
|
||||
* @api {get} api/report/receive 02. 我接收的汇报
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName receive
|
||||
*
|
||||
* @apiParam {Object} [keys] 搜索条件
|
||||
* - keys.key: 关键词
|
||||
* - keys.department_id: 部门ID
|
||||
* - keys.type: 汇报类型,weekly:周报,daily:日报
|
||||
* - keys.status: 状态,unread:未读,read:已读
|
||||
* - keys.created_at: 汇报时间
|
||||
* @apiParam {Number} [page] 当前页,默认:1
|
||||
* @apiParam {Number} [pagesize] 每页显示数量,默认:20,最大:50
|
||||
@@ -89,15 +105,29 @@ class ReportController extends AbstractController
|
||||
$keys = Request::input('keys');
|
||||
if (is_array($keys)) {
|
||||
if ($keys['key']) {
|
||||
$builder->where(function($query) use ($keys) {
|
||||
$query->whereHas('sendUser', function ($q2) use ($keys) {
|
||||
if (str_contains($keys['key'], '@')) {
|
||||
$builder->whereHas('sendUser', function ($q2) use ($keys) {
|
||||
$q2->where("users.email", "LIKE", "%{$keys['key']}%");
|
||||
})->orWhere("title", "LIKE", "%{$keys['key']}%");
|
||||
});
|
||||
} elseif (Base::isNumber($keys['key'])) {
|
||||
$builder->where("userid", intval($keys['key']));
|
||||
} else {
|
||||
$builder->where("title", "LIKE", "%{$keys['key']}%");
|
||||
}
|
||||
}
|
||||
if ($keys['department_id']) {
|
||||
$builder->whereHas('sendUser', function ($query) use ($keys) {
|
||||
$query->where("users.department", "LIKE", "%,{$keys['department_id']},%");
|
||||
});
|
||||
}
|
||||
if (in_array($keys['type'], [Report::WEEKLY, Report::DAILY])) {
|
||||
$builder->whereType($keys['type']);
|
||||
}
|
||||
if (in_array($keys['status'], ['unread', 'read'])) {
|
||||
$builder->whereHas("receivesUser", function ($query) use ($user, $keys) {
|
||||
$query->where("report_receives.userid", $user->userid)->where("report_receives.read", $keys['status'] === 'unread' ? 0 : 1);
|
||||
});
|
||||
}
|
||||
if (is_array($keys['created_at'])) {
|
||||
if ($keys['created_at'][0] > 0) $builder->where('created_at', '>=', Base::newCarbon($keys['created_at'][0])->startOfDay());
|
||||
if ($keys['created_at'][1] > 0) $builder->where('created_at', '<=', Base::newCarbon($keys['created_at'][1])->endOfDay());
|
||||
@@ -115,6 +145,7 @@ class ReportController extends AbstractController
|
||||
/**
|
||||
* @api {get} api/report/store 03. 保存并发送工作汇报
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName store
|
||||
@@ -190,7 +221,6 @@ class ReportController extends AbstractController
|
||||
$report->updateInstance([
|
||||
"title" => $input["title"],
|
||||
"type" => $input["type"],
|
||||
"content" => htmlspecialchars($input["content"]),
|
||||
]);
|
||||
} else {
|
||||
// 生成唯一标识
|
||||
@@ -204,11 +234,25 @@ class ReportController extends AbstractController
|
||||
"title" => $input["title"],
|
||||
"type" => $input["type"],
|
||||
"userid" => $user->userid,
|
||||
"content" => htmlspecialchars($input["content"]),
|
||||
]);
|
||||
}
|
||||
$report->save();
|
||||
|
||||
// 保存内容
|
||||
$content = $input["content"];
|
||||
preg_match_all("/<img\s+src=\"data:image\/(png|jpg|jpeg|webp);base64,(.*?)\"/s", $content, $matchs);
|
||||
foreach ($matchs[2] as $key => $text) {
|
||||
$tmpPath = "uploads/report/" . Carbon::parse($report->created_at)->format("Ym") . "/" . $report->id . "/attached/";
|
||||
Base::makeDir(public_path($tmpPath));
|
||||
$tmpPath .= md5($text) . "." . $matchs[1][$key];
|
||||
if (Base::saveContentImage(public_path($tmpPath), base64_decode($text))) {
|
||||
$paramet = getimagesize(public_path($tmpPath));
|
||||
$content = str_replace($matchs[0][$key], '<img src="' . Base::fillUrl($tmpPath) . '" original-width="' . $paramet[0] . '" original-height="' . $paramet[1] . '"', $content);
|
||||
}
|
||||
}
|
||||
$report->content = htmlspecialchars($content);
|
||||
$report->save();
|
||||
|
||||
// 删除关联
|
||||
$report->Receives()->delete();
|
||||
if ($input["receive_content"]) {
|
||||
@@ -240,6 +284,7 @@ class ReportController extends AbstractController
|
||||
/**
|
||||
* @api {get} api/report/template 04. 生成汇报模板
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName template
|
||||
@@ -411,11 +456,13 @@ class ReportController extends AbstractController
|
||||
/**
|
||||
* @api {get} api/report/detail 05. 报告详情
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName detail
|
||||
*
|
||||
* @apiParam {Number} [id] 报告id
|
||||
* @apiParam {Number} [id] 报告ID
|
||||
* @apiParam {String} [code] 报告分享代码,与ID二选一,优先ID
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -424,30 +471,43 @@ class ReportController extends AbstractController
|
||||
public function detail(): array
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$id = intval(trim(Request::input("id")));
|
||||
if (empty($id))
|
||||
$code = trim(Request::input("code"));
|
||||
//
|
||||
if (empty($id) && empty($code)) {
|
||||
return Base::retError("缺少ID参数");
|
||||
|
||||
$one = Report::getOne($id);
|
||||
$one->type_val = $one->getRawOriginal("type");
|
||||
|
||||
// 标记为已读
|
||||
if (!empty($one->receivesUser)) {
|
||||
foreach ($one->receivesUser as $item) {
|
||||
if ($item->userid === $user->userid && $item->pivot->read === 0) {
|
||||
$one->receivesUser()->updateExistingPivot($user->userid, [
|
||||
"read" => 1,
|
||||
]);
|
||||
}
|
||||
//
|
||||
if (!empty($id)) {
|
||||
$one = Report::getOne($id);
|
||||
$one->type_val = $one->getRawOriginal("type");
|
||||
// 标记为已读
|
||||
if (!empty($one->receivesUser)) {
|
||||
foreach ($one->receivesUser as $item) {
|
||||
if ($item->userid === $user->userid && $item->pivot->read === 0) {
|
||||
$one->receivesUser()->updateExistingPivot($user->userid, [
|
||||
"read" => 1,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$link = ReportLink::whereCode($code)->first();
|
||||
if (empty($link)) {
|
||||
return Base::retError("报告不存在或已被删除");
|
||||
}
|
||||
$one = Report::getOne($link->rid);
|
||||
$one->report_link = $link;
|
||||
$link->increment("num");
|
||||
}
|
||||
|
||||
return Base::retSuccess("success", $one);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/mark 06. 标记已读/未读
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName mark
|
||||
@@ -488,8 +548,71 @@ class ReportController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/last_submitter 07. 获取最后一次提交的接收人
|
||||
* @api {get} api/report/share 07. 分享报告到消息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName share
|
||||
*
|
||||
* @apiParam {Number} id 报告id(组)
|
||||
* @apiParam {Array} dialogids 转发给的对话ID
|
||||
* @apiParam {Array} userids 转发给的成员ID
|
||||
* @apiParam {String} leave_message 转发留言
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function share()
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$id = Request::input('id');
|
||||
$dialogids = Request::input('dialogids');
|
||||
$userids = Request::input('userids');
|
||||
$leave_message = Request::input('leave_message');
|
||||
//
|
||||
if (is_array($id)) {
|
||||
if (count(Base::arrayRetainInt($id)) > 20) {
|
||||
return Base::retError("最多只能操作20条数据");
|
||||
}
|
||||
$builder = Report::whereIn("id", Base::arrayRetainInt($id));
|
||||
} else {
|
||||
$builder = Report::whereId(intval($id));
|
||||
}
|
||||
$reportMsgs = [];
|
||||
$builder ->chunkById(100, function ($list) use (&$reportMsgs, $user) {
|
||||
/** @var Report $item */
|
||||
foreach ($list as $item) {
|
||||
$reportLink = ReportLink::generateLink($item->id, $user->userid);
|
||||
$reportMsgs[] = "<a class=\"mention report\" href=\"{{RemoteURL}}single/report/detail/{$reportLink['code']}\" target=\"_blank\">%{$item->title}</a>";
|
||||
}
|
||||
});
|
||||
if (empty($reportMsgs)) {
|
||||
return Base::retError("报告不存在或已被删除");
|
||||
}
|
||||
$reportTag = count($reportMsgs) > 1 ? 'li' : 'p';
|
||||
$reportAttr = $reportTag === 'li' ? ' data-list="ordered"' : '';
|
||||
$reportMsgs = array_map(function ($item) use ($reportAttr, $reportTag) {
|
||||
return "<{$reportTag}{$reportAttr}>{$item}</{$reportTag}>";
|
||||
}, $reportMsgs);
|
||||
if ($reportTag === 'li') {
|
||||
array_unshift($reportMsgs, "<ol>");
|
||||
$reportMsgs[] = "</ol>";
|
||||
}
|
||||
if ($leave_message) {
|
||||
$reportMsgs[] = "<p>{$leave_message}</p>";
|
||||
}
|
||||
$msgText = implode("", $reportMsgs);
|
||||
//
|
||||
return WebSocketDialogMsg::sendMsgBatch($user, $userids, $dialogids, $msgText);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/last_submitter 08. 获取最后一次提交的接收人
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName last_submitter
|
||||
@@ -505,8 +628,9 @@ class ReportController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/unread 08. 获取未读
|
||||
* @api {get} api/report/unread 09. 获取未读
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName unread
|
||||
@@ -529,8 +653,9 @@ class ReportController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/read 09. 标记汇报已读,可批量
|
||||
* @api {get} api/report/read 10. 标记汇报已读,可批量
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName read
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Models\UserDevice;
|
||||
use Request;
|
||||
use Session;
|
||||
use Response;
|
||||
@@ -41,7 +40,7 @@ class SystemController extends AbstractController
|
||||
* @apiParam {String} type
|
||||
* - get: 获取(默认)
|
||||
* - all: 获取所有(需要管理员权限)
|
||||
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'temp_account_alias', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'voice2text', 'translation', 'e2e_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'all_group_mute', 'all_group_autoin', 'user_private_chat_mute', 'user_group_chat_mute', 'system_alias', 'image_compress', 'image_quality', 'image_save_local', 'start_home'])
|
||||
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'temp_account_alias', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'voice2text', 'translation', 'convert_video', 'compress_video', 'e2e_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'all_group_mute', 'all_group_autoin', 'user_private_chat_mute', 'user_group_chat_mute', 'system_alias', 'system_welcome', 'image_compress', 'image_quality', 'image_save_local', 'start_home'])
|
||||
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -70,7 +69,11 @@ class SystemController extends AbstractController
|
||||
'anon_message',
|
||||
'voice2text',
|
||||
'translation',
|
||||
'convert_video',
|
||||
'compress_video',
|
||||
'e2e_message',
|
||||
'msg_rev_limit',
|
||||
'msg_edit_limit',
|
||||
'auto_archived',
|
||||
'archived_day',
|
||||
'task_visible',
|
||||
@@ -80,6 +83,7 @@ class SystemController extends AbstractController
|
||||
'user_private_chat_mute',
|
||||
'user_group_chat_mute',
|
||||
'system_alias',
|
||||
'system_welcome',
|
||||
'image_compress',
|
||||
'image_quality',
|
||||
'image_save_local',
|
||||
@@ -108,6 +112,9 @@ class SystemController extends AbstractController
|
||||
if ($all['system_alias'] == env('APP_NAME')) {
|
||||
$all['system_alias'] = '';
|
||||
}
|
||||
if ($all['system_welcome'] == '欢迎您,{username}') {
|
||||
$all['system_welcome'] = '';
|
||||
}
|
||||
$setting = Base::setting('system', Base::newTrim($all));
|
||||
} else {
|
||||
$setting = Base::setting('system');
|
||||
@@ -130,7 +137,11 @@ class SystemController extends AbstractController
|
||||
$setting['anon_message'] = $setting['anon_message'] ?: 'open';
|
||||
$setting['voice2text'] = $setting['voice2text'] ?: 'close';
|
||||
$setting['translation'] = $setting['translation'] ?: 'close';
|
||||
$setting['convert_video'] = $setting['convert_video'] ?: 'close';
|
||||
$setting['compress_video'] = $setting['compress_video'] ?: 'close';
|
||||
$setting['e2e_message'] = $setting['e2e_message'] ?: 'close';
|
||||
$setting['msg_rev_limit'] = $setting['msg_rev_limit'] ?: '';
|
||||
$setting['msg_edit_limit'] = $setting['msg_edit_limit'] ?: '';
|
||||
$setting['auto_archived'] = $setting['auto_archived'] ?: 'close';
|
||||
$setting['archived_day'] = floatval($setting['archived_day']) ?: 7;
|
||||
$setting['task_visible'] = $setting['task_visible'] ?: 'close';
|
||||
@@ -283,6 +294,8 @@ class SystemController extends AbstractController
|
||||
* @apiParam {String} type
|
||||
* - get: 获取(默认)
|
||||
* - save: 保存设置(参数:[...])
|
||||
* @apiParam {String} filter 过滤字段(可选)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
@@ -292,6 +305,7 @@ class SystemController extends AbstractController
|
||||
User::auth('admin');
|
||||
//
|
||||
$type = trim(Request::input('type'));
|
||||
$filter = trim(Request::input('filter'));
|
||||
$setting = Base::setting('aibotSetting');
|
||||
if ($type == 'save') {
|
||||
if (env("SYSTEM_SETTING") == 'disabled') {
|
||||
@@ -306,10 +320,18 @@ class SystemController extends AbstractController
|
||||
}
|
||||
$setting = Base::setting('aibotSetting', Base::newTrim($setting));
|
||||
}
|
||||
if ($filter) {
|
||||
$setting = array_filter($setting, function($value, $key) use ($filter) {
|
||||
return str_starts_with($key, $filter);
|
||||
}, ARRAY_FILTER_USE_BOTH);
|
||||
}
|
||||
//
|
||||
if (env("SYSTEM_SETTING") == 'disabled') {
|
||||
foreach ($setting as $key => $item) {
|
||||
if (str_contains($key, '_key')) {
|
||||
if (empty($item)) {
|
||||
continue;
|
||||
}
|
||||
if (str_ends_with($key, '_key') || str_ends_with($key, '_secret')) {
|
||||
$setting[$key] = substr($item, 0, 4) . str_repeat('*', strlen($item) - 8) . substr($item, -4);
|
||||
}
|
||||
}
|
||||
@@ -319,7 +341,66 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/checkin 05. 获取签到设置、保存签到设置(限管理员)
|
||||
* @api {get} api/system/setting/aibot_models 05. 获取AI模型
|
||||
*
|
||||
* @apiDescription 获取所有AI机器人模型设置
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
* @apiName aibot_models
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function setting__aibot_models()
|
||||
{
|
||||
$setting = Base::setting('aibotSetting');
|
||||
$setting = array_filter($setting, function($value, $key) {
|
||||
return str_ends_with($key, '_models') || str_ends_with($key, '_model');
|
||||
}, ARRAY_FILTER_USE_BOTH);
|
||||
return Base::retSuccess('success', $setting ?: json_decode('{}'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/aibot_defmodels 06. 获取AI默认模型
|
||||
*
|
||||
* @apiDescription 获取AI机器人默认模型
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
* @apiName setting__aibot_defmodels
|
||||
*
|
||||
* @apiParam {String} type AI类型
|
||||
* @apiParam {String} [base_url] 基础URL(仅 type=ollama 时有效)
|
||||
* @apiParam {String} [key] Key(仅 type=ollama 时有效)
|
||||
* @apiParam {String} [agency] 使用代理(仅 type=ollama 时有效)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function setting__aibot_defmodels()
|
||||
{
|
||||
$type = trim(Request::input('type'));
|
||||
if ($type == 'ollama') {
|
||||
$baseUrl = trim(Request::input('base_url'));
|
||||
$key = trim(Request::input('key'));
|
||||
$agency = trim(Request::input('agency'));
|
||||
if (empty($baseUrl)) {
|
||||
return Base::retError('请先填写 Base URL');
|
||||
}
|
||||
return Extranet::ollamaModels($baseUrl, $key, $agency);
|
||||
}
|
||||
$models = Setting::AIDefaultModels($type);
|
||||
if (empty($models)) {
|
||||
return Base::retError('未找到默认模型');
|
||||
}
|
||||
return Base::retSuccess('success', [
|
||||
'models' => $models
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/checkin 07. 获取签到设置、保存签到设置(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -425,7 +506,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/apppush 06. 获取APP推送设置、保存APP推送设置(限管理员)
|
||||
* @api {get} api/system/setting/apppush 08. 获取APP推送设置、保存APP推送设置(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -470,7 +551,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/thirdaccess 07. 第三方帐号(限管理员)
|
||||
* @api {get} api/system/setting/thirdaccess 09. 第三方帐号(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -540,7 +621,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/file 08. 文件设置(限管理员)
|
||||
* @api {get} api/system/setting/file 10. 文件设置(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -580,7 +661,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/demo 09. 获取演示帐号
|
||||
* @api {get} api/system/demo 11. 获取演示帐号
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -604,7 +685,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/system/priority 10. 任务优先级
|
||||
* @api {post} api/system/priority 12. 任务优先级
|
||||
*
|
||||
* @apiDescription 获取任务优先级、保存任务优先级
|
||||
* @apiVersion 1.0.0
|
||||
@@ -653,7 +734,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/system/column/template 11. 创建项目模板
|
||||
* @api {post} api/system/column/template 13. 创建项目模板
|
||||
*
|
||||
* @apiDescription 获取创建项目模板、保存创建项目模板
|
||||
* @apiVersion 1.0.0
|
||||
@@ -700,7 +781,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/system/license 12. License
|
||||
* @api {post} api/system/license 14. License
|
||||
*
|
||||
* @apiDescription 获取License信息、保存License(限管理员)
|
||||
* @apiVersion 1.0.0
|
||||
@@ -769,7 +850,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/info 13. 获取终端详细信息
|
||||
* @api {get} api/system/get/info 15. 获取终端详细信息
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -798,7 +879,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/ip 14. 获取IP地址
|
||||
* @api {get} api/system/get/ip 16. 获取IP地址
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -813,7 +894,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/cnip 15. 是否中国IP地址
|
||||
* @api {get} api/system/get/cnip 17. 是否中国IP地址
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -830,7 +911,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/ipgcj02 16. 获取IP地址经纬度
|
||||
* @api {get} api/system/get/ipgcj02 18. 获取IP地址经纬度
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -847,7 +928,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/ipinfo 17. 获取IP地址详细信息
|
||||
* @api {get} api/system/get/ipinfo 19. 获取IP地址详细信息
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -864,7 +945,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/system/imgupload 18. 上传图片
|
||||
* @api {post} api/system/imgupload 20. 上传图片
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -873,7 +954,7 @@ class SystemController extends AbstractController
|
||||
*
|
||||
* @apiParam {File} image post-图片对象
|
||||
* @apiParam {String} [image64] post-图片base64(与'image'二选一)
|
||||
* @apiParam {String} filename post-文件名
|
||||
* @apiParam {String} [filename] post-文件名
|
||||
* @apiParam {Number} [width] 压缩图片宽(默认0)
|
||||
* @apiParam {Number} [height] 压缩图片高(默认0)
|
||||
* @apiParam {String} [whcut] 压缩方式(等比缩放)
|
||||
@@ -930,7 +1011,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/imgview 19. 浏览图片空间
|
||||
* @api {get} api/system/get/imgview 21. 浏览图片空间
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1027,16 +1108,16 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/system/fileupload 20. 上传文件
|
||||
* @api {post} api/system/fileupload 22. 上传文件
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
* @apiName fileupload
|
||||
*
|
||||
* @apiParam {String} [image64] 图片base64
|
||||
* @apiParam {String} filename 文件名
|
||||
* @apiParam {String} [files] 文件名
|
||||
* @apiParam {File} files 文件名
|
||||
* @apiParam {String} [image64] 图片base64(与'files'二选一)
|
||||
* @apiParam {String} [filename] 文件名
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -1071,7 +1152,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/updatelog 21. 获取更新日志
|
||||
* @api {get} api/system/get/updatelog 23. 获取更新日志
|
||||
*
|
||||
* @apiDescription 获取更新日志
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1104,7 +1185,7 @@ class SystemController extends AbstractController
|
||||
if ($logResults) {
|
||||
$logVersion = $logResults[0]['title'];
|
||||
$logContent = implode("\n", array_map(function($item) {
|
||||
return "## [{$item['title']}]" . $item['content'];
|
||||
return "## {$item['title']}" . $item['content'];
|
||||
}, $logResults));
|
||||
}
|
||||
return Base::retSuccess('success', [
|
||||
@@ -1114,7 +1195,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/email/check 22. 邮件发送测试(限管理员)
|
||||
* @api {get} api/system/email/check 24. 邮件发送测试(限管理员)
|
||||
*
|
||||
* @apiDescription 测试配置邮箱是否能发送邮件
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1160,7 +1241,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/checkin/export 23. 导出签到数据(限管理员)
|
||||
* @api {get} api/system/checkin/export 25. 导出签到数据(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -1329,7 +1410,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/checkin/down 24. 下载导出的签到数据
|
||||
* @api {get} api/system/checkin/down 26. 下载导出的签到数据
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -1355,7 +1436,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/version 25. 获取版本号
|
||||
* @api {get} api/system/version 27. 获取版本号
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -1363,23 +1444,29 @@ class SystemController extends AbstractController
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"device_count": 3, // 设备数量
|
||||
"version": "0.0.1", // 服务端版本号
|
||||
"publish": {
|
||||
"provider": "generic",
|
||||
"url": ""
|
||||
}
|
||||
}
|
||||
// 如果header请求中存在version字段,则返回数据包裹在 {ret:1,data:{},msg:"success"} 中
|
||||
*/
|
||||
public function version()
|
||||
{
|
||||
$url = url('');
|
||||
$package = Base::getPackage();
|
||||
$array = [
|
||||
'device_count' => 0,
|
||||
'version' => Base::getVersion(),
|
||||
'publish' => [],
|
||||
];
|
||||
if (Doo::userId()) {
|
||||
$array['device_count'] = UserDevice::whereUserid(Doo::userId())->count();
|
||||
}
|
||||
if (is_array($package['app'])) {
|
||||
$i = 0;
|
||||
$url = url('');
|
||||
foreach ($package['app'] as $item) {
|
||||
$urls = $item['urls'] && is_array($item['urls']) ? $item['urls'] : $item['url'];
|
||||
if (is_array($item['publish']) && ($i === 0 || Base::hostContrast($url, $urls))) {
|
||||
@@ -1388,11 +1475,14 @@ class SystemController extends AbstractController
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
if (Request::hasHeader('version')) {
|
||||
return Base::retSuccess('success', $array);
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/prefetch 26. 预加载的资源
|
||||
* @api {get} api/system/prefetch 28. 预加载的资源
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -1432,7 +1522,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
// 添加office资源
|
||||
$officePath = '';
|
||||
$officeApi = 'http://' . env('APP_IPPR') . '.6/web-apps/apps/api/documents/api.js';
|
||||
$officeApi = 'http://office/web-apps/apps/api/documents/api.js';
|
||||
$content = @file_get_contents($officeApi);
|
||||
if ($content) {
|
||||
if (preg_match("/const\s+ver\s*=\s*'\/*([^']+)'/", $content, $matches)) {
|
||||
@@ -1451,6 +1541,16 @@ class SystemController extends AbstractController
|
||||
return !str_starts_with($item, 'office/{path}/');
|
||||
});
|
||||
}
|
||||
// 添加OKR资源
|
||||
$okrContent = @file_get_contents("http://nginx/apps/okr/");
|
||||
preg_match_all('/<script[^>]*src=["\']([^"\']+)["\'][^>]*>/i', $okrContent, $scriptMatches);
|
||||
foreach ($scriptMatches[1] as $src) {
|
||||
$array[] = $src;
|
||||
}
|
||||
preg_match_all('/<link[^>]*rel=["\']stylesheet["\'][^>]*href=["\']([^"\']+)["\'][^>]*>/i', $okrContent, $linkMatches);
|
||||
foreach ($linkMatches[1] as $href) {
|
||||
$array[] = $href;
|
||||
}
|
||||
}
|
||||
|
||||
return array_map(function($item) use ($version) {
|
||||
|
||||
@@ -19,6 +19,7 @@ use App\Models\UserBot;
|
||||
use App\Models\WebSocket;
|
||||
use App\Models\UmengAlias;
|
||||
use App\Models\UserDelete;
|
||||
use App\Models\UserDevice;
|
||||
use App\Models\UserTransfer;
|
||||
use App\Models\AbstractModel;
|
||||
use App\Models\UserCheckinFace;
|
||||
@@ -267,7 +268,23 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/reg/needinvite 06. 是否需要邀请码
|
||||
* @api {get} api/users/logout 06. 退出登录
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName logout
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
*/
|
||||
public function logout()
|
||||
{
|
||||
UserDevice::forget();
|
||||
return Base::retSuccess('退出成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/reg/needinvite 07. 是否需要邀请码
|
||||
*
|
||||
* @apiDescription 用于判断注册是否需要邀请码
|
||||
* @apiVersion 1.0.0
|
||||
@@ -286,7 +303,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/info 07. 获取我的信息
|
||||
* @api {get} api/users/info 08. 获取我的信息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -336,7 +353,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/editdata 08. 修改自己的资料
|
||||
* @api {get} api/users/editdata 09. 修改自己的资料
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -428,7 +445,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/editpass 09. 修改自己的密码
|
||||
* @api {get} api/users/editpass 10. 修改自己的密码
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -469,7 +486,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/search 10. 搜索会员列表
|
||||
* @api {get} api/users/search 11. 搜索会员列表
|
||||
*
|
||||
* @apiDescription 搜索会员列表
|
||||
* @apiVersion 1.0.0
|
||||
@@ -611,7 +628,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/search/ai 11. 获取AI机器人
|
||||
* @api {get} api/users/search/ai 12. 获取AI机器人
|
||||
*
|
||||
* @apiDescription 搜索会员列表
|
||||
* @apiVersion 1.0.0
|
||||
@@ -630,7 +647,7 @@ class UsersController extends AbstractController
|
||||
//
|
||||
$type = trim(Request::input('type'));
|
||||
$botName = "ai-{$type}";
|
||||
if (!UserBot::isAiBot("{$botName}@bot.system")) {
|
||||
if (!UserBot::systemBotName($botName)) {
|
||||
return Base::retError('AI机器人不存在');
|
||||
}
|
||||
//
|
||||
@@ -642,7 +659,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/basic 12. 获取指定会员基础信息
|
||||
* @api {get} api/users/basic 13. 获取指定会员基础信息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -685,7 +702,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/lists 13. 会员列表(限管理员)
|
||||
* @api {get} api/users/lists 14. 会员列表(限管理员)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -834,7 +851,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/operation 14. 操作会员(限管理员)
|
||||
* @api {get} api/users/operation 15. 操作会员(限管理员)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1092,7 +1109,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/email/verification 15. 邮箱验证
|
||||
* @api {get} api/users/email/verification 16. 邮箱验证
|
||||
*
|
||||
* @apiDescription 不需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1140,7 +1157,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/umeng/alias 16. 设置友盟别名
|
||||
* @api {get} api/users/umeng/alias 17. 设置友盟别名
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1154,6 +1171,7 @@ class UsersController extends AbstractController
|
||||
* @apiParam {String} [userAgent] 浏览器信息
|
||||
* @apiParam {String} [deviceModel] 设备型号
|
||||
* @apiParam {String} [isNotified] 是否有通知权限(0不通知、1通知)
|
||||
* @apiParam {Number} [isDebug] 是否调试(0不调试、1调试)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -1162,6 +1180,10 @@ class UsersController extends AbstractController
|
||||
public function umeng__alias()
|
||||
{
|
||||
$data = Request::input();
|
||||
// 判断是否调试
|
||||
if (intval($data['isDebug'])) {
|
||||
return Base::retError('调试模式下不允许使用');
|
||||
}
|
||||
// 表单验证
|
||||
Base::validator($data, [
|
||||
'alias.required' => '别名不能为空',
|
||||
@@ -1192,6 +1214,7 @@ class UsersController extends AbstractController
|
||||
$row->update([
|
||||
'ua' => $data['userAgent'],
|
||||
'device' => $data['deviceModel'],
|
||||
'device_hash' => UserDevice::check(),
|
||||
'version' => $version,
|
||||
'is_notified' => $isNotified,
|
||||
'updated_at' => Carbon::now()
|
||||
@@ -1201,6 +1224,7 @@ class UsersController extends AbstractController
|
||||
$row = UmengAlias::createInstance(array_merge($inArray, [
|
||||
'ua' => $data['userAgent'],
|
||||
'device' => $data['deviceModel'],
|
||||
'device_hash' => UserDevice::check(),
|
||||
'version' => $version,
|
||||
'is_notified' => $isNotified,
|
||||
]));
|
||||
@@ -1212,7 +1236,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/meeting/open 17. 【会议】创建会议、加入会议
|
||||
* @api {get} api/users/meeting/open 18. 【会议】创建会议、加入会议
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1330,7 +1354,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/meeting/link 18. 【会议】获取分享链接
|
||||
* @api {get} api/users/meeting/link 19. 【会议】获取分享链接
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1359,7 +1383,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/meeting/tourist 19. 【会议】游客信息
|
||||
* @api {get} api/users/meeting/tourist 20. 【会议】游客信息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1382,7 +1406,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/meeting/invitation 20. 【会议】发送邀请
|
||||
* @api {get} api/users/meeting/invitation 21. 【会议】发送邀请
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1429,7 +1453,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/email/send 21. 发送邮箱验证码
|
||||
* @api {get} api/users/email/send 22. 发送邮箱验证码
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1469,7 +1493,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/email/edit 22. 修改邮箱
|
||||
* @api {get} api/users/email/edit 23. 修改邮箱
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1514,7 +1538,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/delete/account 23. 删除帐号
|
||||
* @api {get} api/users/delete/account 24. 删除帐号
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1576,7 +1600,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/department/list 24. 部门列表(限管理员)
|
||||
* @api {get} api/users/department/list 25. 部门列表(限管理员)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1595,7 +1619,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/department/add 25. 新建、修改部门(限管理员)
|
||||
* @api {get} api/users/department/add 26. 新建、修改部门(限管理员)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1653,19 +1677,22 @@ class UsersController extends AbstractController
|
||||
if (empty($parentDepartment)) {
|
||||
return Base::retError('上级部门不存在或已被删除');
|
||||
}
|
||||
if ($parentDepartment->parent_id > 0) {
|
||||
return Base::retError('上级部门层级错误');
|
||||
if (count($parentDepartment->parents()) > 2) {
|
||||
return Base::retError('部门层级最多只能创建3级');
|
||||
}
|
||||
if (UserDepartment::whereParentId($parent_id)->count() > 20) {
|
||||
if ($id > 0 && UserDepartment::whereParentId($id)->whereId($parent_id)->exists()) {
|
||||
return Base::retError('不能选择自己的子部门作为上级部门');
|
||||
}
|
||||
if (UserDepartment::whereParentId($parent_id)->count() >= 20) {
|
||||
return Base::retError('每个部门最多只能创建20个子部门');
|
||||
}
|
||||
if ($id > 0 && UserDepartment::whereParentId($id)->exists()) {
|
||||
return Base::retError('含有子部门无法修改上级部门');
|
||||
}
|
||||
}
|
||||
if (empty($owner_userid) || !User::whereUserid($owner_userid)->exists()) {
|
||||
return Base::retError('请选择正确的部门负责人');
|
||||
}
|
||||
if (UserDepartment::whereOwnerUserid($owner_userid)->count() >= 10) {
|
||||
return Base::retError('每个用户最多只能负责10个部门');
|
||||
}
|
||||
//
|
||||
$userDepartment->saveDepartment([
|
||||
'name' => $name,
|
||||
@@ -1674,11 +1701,11 @@ class UsersController extends AbstractController
|
||||
], $dialog_useid);
|
||||
Cache::forever("UserDepartment::rand", Base::generatePassword());
|
||||
//
|
||||
return Base::retSuccess($parent_id > 0 ? '保存成功' : '新建成功');
|
||||
return Base::retSuccess($id > 0 ? '保存成功' : '新建成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/department/del 26. 删除部门(限管理员)
|
||||
* @api {get} api/users/department/del 27. 删除部门(限管理员)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1701,6 +1728,9 @@ class UsersController extends AbstractController
|
||||
if (empty($userDepartment)) {
|
||||
return Base::retError('部门不存在或已被删除');
|
||||
}
|
||||
if (UserDepartment::whereParentId($id)->exists()) {
|
||||
return Base::retError('含有子部门无法删除');
|
||||
}
|
||||
$userDepartment->deleteDepartment();
|
||||
Cache::forever("UserDepartment::rand", Base::generatePassword());
|
||||
//
|
||||
@@ -1708,7 +1738,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/checkin/get 27. 获取签到设置
|
||||
* @api {get} api/users/checkin/get 28. 获取签到设置
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1735,7 +1765,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/users/checkin/save 28. 保存签到设置
|
||||
* @api {post} api/users/checkin/save 29. 保存签到设置
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1810,7 +1840,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/checkin/list 29. 获取签到数据
|
||||
* @api {get} api/users/checkin/list 30. 获取签到数据
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1857,7 +1887,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/socket/status 30. 获取socket状态
|
||||
* @api {get} api/users/socket/status 31. 获取socket状态
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1880,7 +1910,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/key/client 31. 客户端KEY
|
||||
* @api {get} api/users/key/client 32. 客户端KEY
|
||||
*
|
||||
* @apiDescription 获取客户端KEY,用于加密数据发送给服务端
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1922,7 +1952,51 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/bot/info 32. 机器人信息
|
||||
* @api {get} api/users/bot/list 33. 机器人列表
|
||||
*
|
||||
* @apiDescription 需要token身份,获取我的机器人列表
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName bot__list
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function bot__list()
|
||||
{
|
||||
// 获取当前认证用户
|
||||
$user = User::auth();
|
||||
|
||||
// 使用连表查询一次性获取所有机器人数据
|
||||
$bots = User::join('user_bots', 'user_bots.bot_id', '=', 'users.userid')
|
||||
->where('user_bots.userid', $user->userid)
|
||||
->select([
|
||||
'users.userid',
|
||||
'users.nickname',
|
||||
'users.userimg',
|
||||
'user_bots.clear_day',
|
||||
'user_bots.webhook_url'
|
||||
])
|
||||
->orderByDesc('id')
|
||||
->get()
|
||||
->toArray();
|
||||
foreach ($bots as &$bot) {
|
||||
$bot['id'] = $bot['userid'];
|
||||
$bot['name'] = $bot['nickname'];
|
||||
$bot['avatar'] = $bot['userimg'];
|
||||
$bot['system_name'] = UserBot::systemBotName($bot['name']);
|
||||
unset($bot['userid'], $bot['nickname'], $bot['userimg']);
|
||||
}
|
||||
|
||||
// 返回成功响应,将机器人列表包装在list字段中
|
||||
return Base::retSuccess('success', [
|
||||
'list' => $bots
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/bot/info 34. 机器人信息
|
||||
*
|
||||
* @apiDescription 需要token身份,获取我的机器人信息
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1973,14 +2047,14 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/users/bot/edit 33. 编辑机器人
|
||||
* @api {post} api/users/bot/edit 35. 添加、编辑机器人
|
||||
*
|
||||
* @apiDescription 需要token身份,编辑 我的机器人 或 管理员修改系统机器人 信息
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName bot__edit
|
||||
*
|
||||
* @apiParam {Number} id 机器人ID
|
||||
* @apiParam {Number} [id] 机器人ID(编辑时必填,留空为添加)
|
||||
* @apiParam {String} [name] 机器人名称
|
||||
* @apiParam {String} [avatar] 机器人头像
|
||||
* @apiParam {Number} [clear_day] 清理天数(仅 我的机器人)
|
||||
@@ -1995,10 +2069,19 @@ class UsersController extends AbstractController
|
||||
$user = User::auth();
|
||||
//
|
||||
$botId = intval(Request::input('id'));
|
||||
$botUser = User::whereUserid($botId)->whereBot(1)->first();
|
||||
if (empty($botUser)) {
|
||||
return Base::retError('机器人不存在');
|
||||
if (empty($botId)) {
|
||||
$res = UserBot::newbot($user->userid, trim(Request::input('name')));
|
||||
if (Base::isError($res)) {
|
||||
return $res;
|
||||
}
|
||||
$botUser = $res['data'];
|
||||
} else {
|
||||
$botUser = User::whereUserid($botId)->whereBot(1)->first();
|
||||
if (empty($botUser)) {
|
||||
return Base::retError('机器人不存在');
|
||||
}
|
||||
}
|
||||
//
|
||||
$userBot = UserBot::whereBotId($botUser->userid)->whereUserid($user->userid)->first();
|
||||
if (empty($userBot)) {
|
||||
if (UserBot::systemBotName($botUser->email)) {
|
||||
@@ -2055,17 +2138,68 @@ class UsersController extends AbstractController
|
||||
$data['clear_day'] = $userBot->clear_day;
|
||||
$data['webhook_url'] = $userBot->webhook_url;
|
||||
}
|
||||
return Base::retSuccess('修改成功', $data);
|
||||
return Base::retSuccess($botId ? '修改成功' : '添加成功', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/share/list 34. 获取分享列表
|
||||
* @api {get} api/users/bot/delete 36. 删除机器人
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName bot__delete
|
||||
*
|
||||
* @apiParam {Number} id 机器人ID
|
||||
* @apiParam {String} remark 删除备注
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function bot__delete()
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$botId = intval(Request::input('id'));
|
||||
$remark = trim(Request::input('remark'));
|
||||
//
|
||||
if (empty($remark)) {
|
||||
return Base::retError('请输入删除备注');
|
||||
}
|
||||
if (mb_strlen($remark) > 255) {
|
||||
return Base::retError('删除备注长度限制255个字');
|
||||
}
|
||||
//
|
||||
$botUser = User::whereUserid($botId)->whereBot(1)->first();
|
||||
if (empty($botUser)) {
|
||||
return Base::retError('机器人不存在');
|
||||
}
|
||||
$userBot = UserBot::whereBotId($botUser->userid)->whereUserid($user->userid)->first();
|
||||
if (empty($userBot)) {
|
||||
if (UserBot::systemBotName($botUser->email)) {
|
||||
// 系统机器人(仅限管理员)
|
||||
return Base::retError('系统机器人不能删除');
|
||||
} else {
|
||||
// 其他用户的机器人(仅限主人)
|
||||
return Base::retError('不是你的机器人');
|
||||
}
|
||||
}
|
||||
//
|
||||
if (!$botUser->deleteUser($remark)) {
|
||||
return Base::retError('删除失败');
|
||||
}
|
||||
return Base::retSuccess('删除成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/share/list 37. 获取分享列表
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName share__list
|
||||
*
|
||||
* @apiParam {String} [type] 分享类型:file-文件,text-列表 默认file
|
||||
* @apiParam {String} [key] 搜索关键词(用于搜索会话)
|
||||
* @apiParam {Number} [pid] 父级文件id,用于获取子目录和上传到指定目录的id
|
||||
* @apiParam {Number} [upload_file_id] 上传文件id
|
||||
*
|
||||
@@ -2077,6 +2211,7 @@ class UsersController extends AbstractController
|
||||
{
|
||||
$user = User::auth();
|
||||
$type = Request::input('type', 'file');
|
||||
$key = Request::input('key');
|
||||
$pid = intval(Request::input('pid', -1));
|
||||
$uploadFileId = intval(Request::input('upload_file_id', -1));
|
||||
// 上传文件
|
||||
@@ -2109,10 +2244,14 @@ class UsersController extends AbstractController
|
||||
'icon' => url("images/file/light/folder.png"),
|
||||
'extend' => ['upload_file_id' => 0],
|
||||
'name' => Doo::translate('文件'),
|
||||
'sort' => Carbon::parse("9999")->timestamp,
|
||||
];
|
||||
}
|
||||
$dialogList = WebSocketDialog::getDialogList($user->userid);
|
||||
foreach ($dialogList['data'] as $dialog) {
|
||||
$dialogTake = 50;
|
||||
$dialogList = WebSocketDialog::searchDialog($user->userid, $key, $dialogTake);
|
||||
$dialogIds = [];
|
||||
$itemUrl = $type == "file" ? Base::fillUrl("api/dialog/msg/sendfiles") : Base::fillUrl("api/dialog/msg/sendtext");
|
||||
foreach ($dialogList as $dialog) {
|
||||
if ($dialog['avatar']) {
|
||||
$avatar = url($dialog['avatar']);
|
||||
} else if ($dialog['type'] == 'user') {
|
||||
@@ -2129,7 +2268,8 @@ class UsersController extends AbstractController
|
||||
'type' => 'item',
|
||||
'name' => $dialog['name'],
|
||||
'icon' => $avatar,
|
||||
'url' => $type == "file" ? Base::fillUrl("api/dialog/msg/sendfiles") : Base::fillUrl("api/dialog/msg/sendtext"),
|
||||
'url' => $itemUrl,
|
||||
'sort' => Carbon::parse($dialog['last_at'])->timestamp,
|
||||
'extend' => [
|
||||
'dialog_ids' => $dialog['id'],
|
||||
'text_type' => 'text',
|
||||
@@ -2137,6 +2277,33 @@ class UsersController extends AbstractController
|
||||
'silence' => 'no'
|
||||
]
|
||||
];
|
||||
$dialogIds[] = $dialog['id'];
|
||||
}
|
||||
if ($key && count($dialogList) < $dialogTake) {
|
||||
$dialogUsers = User::searchUser($key, $dialogTake - count($dialogList));
|
||||
foreach ($dialogUsers as $item) {
|
||||
$dialog = WebSocketDialog::getUserDialog($user->userid, $item->userid, now()->addDay());
|
||||
if ($dialog && !in_array($dialog->id, $dialogIds)) {
|
||||
$lists[] = [
|
||||
'type' => 'item',
|
||||
'name' => $item->nickname,
|
||||
'icon' => $item->userimg,
|
||||
'url' => $itemUrl,
|
||||
'sort' => Carbon::parse($item->line_at)->timestamp,
|
||||
'extend' => [
|
||||
'dialog_ids' => $dialog->id,
|
||||
'text_type' => 'text',
|
||||
'reply_id' => 0,
|
||||
'silence' => 'no'
|
||||
]
|
||||
];
|
||||
$dialogIds[] = $dialog->id;
|
||||
}
|
||||
}
|
||||
// 根据 $lists sort 从大到小排序
|
||||
usort($lists, function ($a, $b) {
|
||||
return $b['sort'] <=> $a['sort'];
|
||||
});
|
||||
}
|
||||
}
|
||||
// 返回
|
||||
@@ -2144,7 +2311,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/annual/report 35. 年度报告
|
||||
* @api {get} api/users/annual/report 38. 年度报告
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
@@ -2311,4 +2478,97 @@ class UsersController extends AbstractController
|
||||
//
|
||||
return Base::retSuccess('success', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/device/list 39. 获取设备列表
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName device__list
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function device__list()
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$list = UserDevice::whereUserid($user->userid)->orderByDesc('id')->take(UserDevice::$deviceLimit)->get();
|
||||
//
|
||||
return Base::retSuccess('success', [
|
||||
'list' => $list
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/device/logout 40. 登出设备(删除设备)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName device__logout
|
||||
*
|
||||
* @apiParam {Number} id 设备id
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function device__logout()
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$id = intval(Request::input('id'));
|
||||
if (empty($id)) {
|
||||
return Base::retError('参数错误');
|
||||
}
|
||||
$userDevice = UserDevice::whereUserid($user->userid)->whereId($id)->first();
|
||||
if (empty($userDevice)) {
|
||||
return Base::retError('设备不存在或已被删除');
|
||||
}
|
||||
UserDevice::forget($userDevice->id);
|
||||
//
|
||||
return Base::retSuccess('操作成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/device/edit 41. 编辑设备
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName device__edit
|
||||
*
|
||||
* @apiParam {Object} detail 设备信息
|
||||
* @apiParam {String} detail.device_name 设备名称
|
||||
* @apiParam {String} detail.app_brand 设备品牌
|
||||
* @apiParam {String} detail.app_model 设备型号
|
||||
* @apiParam {String} detail.app_os 设备操作系统
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function device__edit()
|
||||
{
|
||||
User::auth();
|
||||
//
|
||||
$detail = Request::input();
|
||||
$detail = array_intersect_key($detail, array_flip([ 'device_name', 'app_brand', 'app_model','app_os']));
|
||||
if (empty($detail)) {
|
||||
return Base::retError('参数错误');
|
||||
}
|
||||
//
|
||||
$row = UserDevice::record();
|
||||
if (empty($row)) {
|
||||
return Base::retError('设备不存在或已被删除');
|
||||
}
|
||||
$deviceInfo = array_merge(Base::json2array($row->detail), $detail);
|
||||
$row->detail = Base::array2json($deviceInfo);
|
||||
$row->save();
|
||||
//
|
||||
return Base::retSuccess('保存成功');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ use App\Tasks\AutoArchivedTask;
|
||||
use App\Tasks\DeleteBotMsgTask;
|
||||
use App\Tasks\CheckinRemindTask;
|
||||
use App\Tasks\CloseMeetingRoomTask;
|
||||
use App\Tasks\ZincSearchSyncTask;
|
||||
use App\Tasks\UnclaimedTaskRemindTask;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
use Laravolt\Avatar\Avatar;
|
||||
@@ -241,11 +242,12 @@ class IndexController extends InvokeController
|
||||
// App推送
|
||||
Task::deliver(new AppPushTask());
|
||||
// 删除过期的临时表数据
|
||||
Task::deliver(new DeleteTmpTask('wg_tmp_msgs', 1));
|
||||
Task::deliver(new DeleteTmpTask('task_worker', 12));
|
||||
Task::deliver(new DeleteTmpTask('tmp_msgs', 1));
|
||||
Task::deliver(new DeleteTmpTask('tmp'));
|
||||
Task::deliver(new DeleteTmpTask('task_worker', 12));
|
||||
Task::deliver(new DeleteTmpTask('file'));
|
||||
Task::deliver(new DeleteTmpTask('tmp_file', 24));
|
||||
Task::deliver(new DeleteTmpTask('user_device', 24));
|
||||
// 删除机器人消息
|
||||
Task::deliver(new DeleteBotMsgTask());
|
||||
// 周期任务
|
||||
@@ -258,6 +260,8 @@ class IndexController extends InvokeController
|
||||
Task::deliver(new UnclaimedTaskRemindTask());
|
||||
// 关闭会议室
|
||||
Task::deliver(new CloseMeetingRoomTask());
|
||||
// ZincSearch 同步
|
||||
Task::deliver(new ZincSearchSyncTask());
|
||||
|
||||
return "success";
|
||||
}
|
||||
@@ -321,7 +325,7 @@ class IndexController extends InvokeController
|
||||
"file" => Request::file('file'),
|
||||
"type" => 'publish',
|
||||
"path" => $draftPath,
|
||||
"fileName" => true,
|
||||
"saveName" => true,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -468,7 +472,7 @@ class IndexController extends InvokeController
|
||||
action: "eeuiAppSendMessage",
|
||||
data: [
|
||||
{
|
||||
action: 'setPageData',
|
||||
action: 'setPageData', // 设置页面数据
|
||||
data: {
|
||||
showProgress: true,
|
||||
titleFixed: true,
|
||||
@@ -476,7 +480,7 @@ class IndexController extends InvokeController
|
||||
}
|
||||
},
|
||||
{
|
||||
action: 'createTarget',
|
||||
action: 'createTarget', // 创建目标(访问新地址)
|
||||
url: "{$redirectUrl}",
|
||||
}
|
||||
]
|
||||
@@ -490,7 +494,7 @@ class IndexController extends InvokeController
|
||||
if (in_array($ext, File::localExt)) {
|
||||
$url = Base::fillUrl($path);
|
||||
} else {
|
||||
$url = 'http://' . env('APP_IPPR') . '.3/' . $path;
|
||||
$url = 'http://nginx/' . $path;
|
||||
}
|
||||
$url = Base::urlAddparameter($url, [
|
||||
'fullfilename' => Base::rightDelete($name, '.' . $ext) . '_' . filemtime($file) . '.' . $ext
|
||||
|
||||
@@ -24,6 +24,9 @@ class WebApi
|
||||
RequestContext::set('start_time', microtime(true));
|
||||
RequestContext::set('header_language', $request->header('language'));
|
||||
|
||||
// 更新请求的基本URL
|
||||
RequestContext::updateBaseUrl($request);
|
||||
|
||||
// 加载Doo类
|
||||
Doo::load();
|
||||
|
||||
|
||||
@@ -210,8 +210,8 @@ class AbstractModel extends Model
|
||||
/**
|
||||
* 数据库更新或插入
|
||||
* @param $where
|
||||
* @param array $update 存在时更新的内容
|
||||
* @param array $insert 不存在时插入的内容,如果没有则插入更新内容
|
||||
* @param array|\Closure $update 存在时更新的内容
|
||||
* @param array|\Closure $insert 不存在时插入的内容,如果没有则插入更新内容
|
||||
* @param bool $isInsert 是否是插入数据
|
||||
* @return AbstractModel|\Illuminate\Database\Eloquent\Builder|Model|object|static|null
|
||||
*/
|
||||
@@ -220,13 +220,25 @@ class AbstractModel extends Model
|
||||
$row = static::where($where)->first();
|
||||
if (empty($row)) {
|
||||
$row = new static;
|
||||
$array = array_merge($where, $insert ?: $update);
|
||||
if ($insert instanceof \Closure) {
|
||||
$insert = $insert();
|
||||
}
|
||||
if (empty($insert)) {
|
||||
if ($update instanceof \Closure) {
|
||||
$update = $update();
|
||||
}
|
||||
$insert = $update;
|
||||
}
|
||||
$array = array_merge($where, $insert);
|
||||
if (isset($array[$row->primaryKey])) {
|
||||
unset($array[$row->primaryKey]);
|
||||
}
|
||||
$row->updateInstance($array);
|
||||
$isInsert = true;
|
||||
} elseif ($update) {
|
||||
if ($update instanceof \Closure) {
|
||||
$update = $update();
|
||||
}
|
||||
$row->updateInstance($update);
|
||||
$isInsert = false;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,60 @@ use Cache;
|
||||
use Carbon\Carbon;
|
||||
use DB;
|
||||
|
||||
/**
|
||||
* App\Models\ApproveProcInstHistory
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $proc_def_id 流程定义ID
|
||||
* @property string|null $proc_def_name 流程定义名
|
||||
* @property string|null $title 标题
|
||||
* @property int|null $department_id 用户部门ID
|
||||
* @property string|null $department 用户部门
|
||||
* @property string|null $company 用户公司
|
||||
* @property string|null $node_id 当前节点
|
||||
* @property string|null $candidate 审批人
|
||||
* @property int|null $task_id 当前任务
|
||||
* @property string|null $start_time 开始时间
|
||||
* @property string|null $end_time 结束时间
|
||||
* @property int|null $duration 持续时间
|
||||
* @property string|null $start_user_id 开始用户ID
|
||||
* @property string|null $start_user_name 开始用户名
|
||||
* @property int|null $is_finished 是否完成
|
||||
* @property string|null $var
|
||||
* @property int $state 当前状态: 0待审批,1审批中,2通过,3拒绝,4撤回
|
||||
* @property string|null $latest_comment
|
||||
* @property string|null $global_comment
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereCandidate($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereCompany($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereDepartment($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereDepartmentId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereDuration($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereEndTime($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereGlobalComment($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereIsFinished($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereLatestComment($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereNodeId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereProcDefId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereProcDefName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereStartTime($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereStartUserId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereStartUserName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereState($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereTaskId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereTitle($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereVar($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ApproveProcInstHistory extends AbstractModel
|
||||
{
|
||||
protected $table = 'approve_proc_inst_history';
|
||||
|
||||
@@ -117,7 +117,7 @@ class File extends AbstractModel
|
||||
'jpg', 'jpeg', 'webp', 'png', 'gif', 'bmp', 'ico', 'raw',
|
||||
'tif', 'tiff',
|
||||
'mp3', 'wav', 'mp4', 'flv',
|
||||
'avi', 'mov', 'wmv', 'mkv', '3gp', 'rm',
|
||||
// 'avi', 'mov', 'wmv', 'mkv', '3gp', 'rm', // 这一排是要转换的,无法使用本地播放
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
|
||||
use App\Module\Base;
|
||||
use App\Module\Timer;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
/**
|
||||
* App\Models\FileContent
|
||||
@@ -104,10 +104,10 @@ class FileContent extends AbstractModel
|
||||
|
||||
/**
|
||||
* 获取格式内容(或下载)
|
||||
* @param File $file
|
||||
* @param $file
|
||||
* @param $content
|
||||
* @param $download
|
||||
* @return array|\Symfony\Component\HttpFoundation\BinaryFileResponse
|
||||
* @return array|StreamedResponse
|
||||
*/
|
||||
public static function formatContent($file, $content, $download = false)
|
||||
{
|
||||
@@ -119,7 +119,7 @@ class FileContent extends AbstractModel
|
||||
} else {
|
||||
$filePath = public_path($content['url']);
|
||||
}
|
||||
return Base::BinaryFileResponse($filePath, $name);
|
||||
return Base::DownloadFileResponse($filePath, $name);
|
||||
}
|
||||
if (empty($content)) {
|
||||
$content = match ($file->type) {
|
||||
@@ -148,7 +148,7 @@ class FileContent extends AbstractModel
|
||||
if ($download) {
|
||||
$filePath = public_path($path);
|
||||
if (isset($filePath)) {
|
||||
return Base::BinaryFileResponse($filePath, $name);
|
||||
return Base::DownloadFileResponse($filePath, $name);
|
||||
} else {
|
||||
abort(403, "This file not support download.");
|
||||
}
|
||||
@@ -156,4 +156,28 @@ class FileContent extends AbstractModel
|
||||
}
|
||||
return Base::retSuccess('success', [ 'content' => $content ]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件内容
|
||||
* @param $id
|
||||
* @return self|null
|
||||
*/
|
||||
public static function idOrCodeToContent($id)
|
||||
{
|
||||
$builder = null;
|
||||
if (Base::isNumber($id)) {
|
||||
$builder = FileContent::whereFid($id);
|
||||
} elseif ($id) {
|
||||
$fileLink = FileLink::whereCode($id)->first();
|
||||
if ($fileLink) {
|
||||
$builder = FileContent::whereFid($fileLink->file_id);
|
||||
}
|
||||
}
|
||||
/** @var self $fileContent */
|
||||
$fileContent = $builder?->orderByDesc('id')->first();
|
||||
if ($fileContent) {
|
||||
$fileContent->content = Base::json2array($fileContent->content ?: []);
|
||||
}
|
||||
return $fileContent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +219,12 @@ class Project extends AbstractModel
|
||||
'userid' => $userid,
|
||||
], [
|
||||
'important' => 1
|
||||
]);
|
||||
], function () use ($userid) {
|
||||
return [
|
||||
'important' => 1,
|
||||
'bot' => User::isBot($userid) ? 1 : 0,
|
||||
];
|
||||
});
|
||||
}
|
||||
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->whereImportant(1)->remove();
|
||||
});
|
||||
@@ -415,6 +420,7 @@ class Project extends AbstractModel
|
||||
$hasStart = false;
|
||||
$hasEnd = false;
|
||||
$upTaskList = [];
|
||||
$projectUserids = $this->relationUserids();
|
||||
foreach ($flows as $item) {
|
||||
$id = intval($item['id']);
|
||||
$turns = Base::arrayRetainInt($item['turns'] ?: [], true);
|
||||
@@ -431,6 +437,12 @@ class Project extends AbstractModel
|
||||
if ($userlimit && empty($userids)) {
|
||||
throw new ApiException("状态[{$item['name']}]设置错误,设置限制负责人时必须填写状态负责人");
|
||||
}
|
||||
foreach ($userids as $userid) {
|
||||
if (!in_array($userid, $projectUserids)) {
|
||||
$nickname = User::userid2nickname($userid);
|
||||
throw new ApiException("状态[{$item['name']}]设置错误,状态负责人[{$nickname}]不在项目成员内");
|
||||
}
|
||||
}
|
||||
$flow = ProjectFlowItem::updateInsert([
|
||||
'id' => $id,
|
||||
'project_id' => $this->id,
|
||||
|
||||
@@ -12,6 +12,7 @@ use App\Tasks\PushTask;
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Observers\ProjectTaskObserver;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
use League\HTMLToMarkdown\HtmlConverter;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
@@ -617,7 +618,12 @@ class ProjectTask extends AbstractModel
|
||||
$data['complete_at'] = false;
|
||||
}
|
||||
}
|
||||
if ($newFlowItem->userids) {
|
||||
$flowUserids = $newFlowItem->userids;
|
||||
if ($flowUserids) {
|
||||
// 确认负责人在任务中
|
||||
$flowUserids = ProjectUser::whereProjectId($this->project_id)->whereIn('userid', $flowUserids)->pluck('userid')->toArray();
|
||||
}
|
||||
if ($flowUserids) {
|
||||
// 判断自动添加负责人
|
||||
$flowData['owner'] = $data['owner'] = $this->taskUser->where('owner', 1)->pluck('userid')->toArray();
|
||||
if (in_array($newFlowItem->usertype, ["replace", "merge"])) {
|
||||
@@ -626,14 +632,14 @@ class ProjectTask extends AbstractModel
|
||||
$flowData['assist'] = $data['assist'] = $this->taskUser->where('owner', 0)->pluck('userid')->toArray();
|
||||
$data['assist'] = array_merge($data['assist'], $data['owner']);
|
||||
}
|
||||
$data['owner'] = $newFlowItem->userids;
|
||||
$data['owner'] = $flowUserids;
|
||||
// 判断剔除模式:保留操作状态的人员
|
||||
if ($newFlowItem->usertype == "merge") {
|
||||
$data['owner'][] = User::userid();
|
||||
}
|
||||
} else {
|
||||
// 添加模式
|
||||
$data['owner'] = array_merge($data['owner'], $newFlowItem->userids);
|
||||
$data['owner'] = array_merge($data['owner'], $flowUserids);
|
||||
}
|
||||
$data['owner'] = array_values(array_unique($data['owner']));
|
||||
if (isset($data['assist'])) {
|
||||
@@ -729,7 +735,9 @@ class ProjectTask extends AbstractModel
|
||||
if (count($older) == 0 && count($array) == 1 && $array[0] == User::userid()) {
|
||||
$this->addLog("认领{任务}");
|
||||
} else {
|
||||
$this->addLog("修改{任务}负责人", ['userid' => $array]);
|
||||
if (array_merge(array_diff($array, $older), array_diff($older, $array))) {
|
||||
$this->addLog("修改{任务}负责人", ['userid' => $array]);
|
||||
}
|
||||
}
|
||||
$this->taskPush(array_values(array_diff($array, $older)), 0);
|
||||
}
|
||||
@@ -770,6 +778,7 @@ class ProjectTask extends AbstractModel
|
||||
if (Arr::exists($data, 'times')) {
|
||||
$oldAt = [Carbon::parse($this->start_at), Carbon::parse($this->end_at)];
|
||||
$oldStringAt = $this->start_at ? ($oldAt[0]->toDateTimeString() . '~' . $oldAt[1]->toDateTimeString()) : '';
|
||||
$isOverdue = $this->overdue;
|
||||
$clearSubTaskTime = false;
|
||||
$this->start_at = null;
|
||||
$this->end_at = null;
|
||||
@@ -835,7 +844,20 @@ class ProjectTask extends AbstractModel
|
||||
}
|
||||
});
|
||||
}
|
||||
$newStringAt = $this->start_at && !$clearSubTaskTime ? ($this->start_at->toDateTimeString() . '~' . $this->end_at->toDateTimeString()) : '';
|
||||
$existAt = $this->start_at && !$clearSubTaskTime;
|
||||
$newStringAt = $existAt ? ($this->start_at->toDateTimeString() . '~' . $this->end_at->toDateTimeString()) : '';
|
||||
if ($isOverdue) {
|
||||
$effectiveEndTime = $existAt ? Carbon::parse($this->end_at)->min(Carbon::now()) : Carbon::now();
|
||||
$this->addLog("{任务}超期未完成", [
|
||||
'cache' => [
|
||||
'task_at' => $oldStringAt,
|
||||
'change_at' => $newStringAt,
|
||||
'over_sec' => $effectiveEndTime->diffInSeconds($oldAt[1]),
|
||||
'owners' => $this->taskUser->where('owner', 1)->pluck('userid')->toArray(),
|
||||
'assists' => $this->taskUser->where('owner', 0)->pluck('userid')->toArray(),
|
||||
]
|
||||
]);
|
||||
}
|
||||
$newDesc = $desc ? "(备注:{$desc})" : "";
|
||||
$this->addLog("修改{任务}时间" . $newDesc, [
|
||||
'change' => [$oldStringAt, $newStringAt]
|
||||
@@ -870,6 +892,7 @@ class ProjectTask extends AbstractModel
|
||||
}
|
||||
// 协助人员
|
||||
if (Arr::exists($data, 'assist')) {
|
||||
$older = $this->taskUser->where('owner', 0)->pluck('userid')->toArray();
|
||||
$array = [];
|
||||
$assist = is_array($data['assist']) ? $data['assist'] : [$data['assist']];
|
||||
if (count($assist) > 10) {
|
||||
@@ -890,7 +913,9 @@ class ProjectTask extends AbstractModel
|
||||
$array[] = $uid;
|
||||
}
|
||||
if ($array) {
|
||||
$this->addLog("修改{任务}协助人员", ['userid' => $array]);
|
||||
if (array_merge(array_diff($array, $older), array_diff($older, $array))) {
|
||||
$this->addLog("修改{任务}协助人员", ['userid' => $array]);
|
||||
}
|
||||
}
|
||||
$rows = ProjectTaskUser::whereTaskId($this->id)->whereOwner(0)->whereNotIn('userid', $array)->get();
|
||||
if ($rows->isNotEmpty()) {
|
||||
@@ -1180,7 +1205,12 @@ class ProjectTask extends AbstractModel
|
||||
'userid' => $userid,
|
||||
], [
|
||||
'important' => 1
|
||||
]);
|
||||
], function () use ($userid) {
|
||||
return [
|
||||
'important' => 1,
|
||||
'bot' => User::isBot($userid) ? 1 : 0,
|
||||
];
|
||||
});
|
||||
}
|
||||
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->whereImportant(1)->remove();
|
||||
});
|
||||
@@ -1324,6 +1354,9 @@ class ProjectTask extends AbstractModel
|
||||
$addMsg = $this->parent_id == 0 && $this->dialog_id > 0;
|
||||
if ($complete_at === null) {
|
||||
// 标记未完成
|
||||
if (!$this->complete_at) {
|
||||
return; // 本来就未完成
|
||||
}
|
||||
$this->complete_at = null;
|
||||
$this->addLog("标记{任务}未完成");
|
||||
if ($addMsg) {
|
||||
@@ -1333,6 +1366,9 @@ class ProjectTask extends AbstractModel
|
||||
}
|
||||
} else {
|
||||
// 标记已完成
|
||||
if ($this->complete_at) {
|
||||
return; // 本来就已完成
|
||||
}
|
||||
if ($this->parent_id == 0) {
|
||||
if (self::whereParentId($this->id)->whereCompleteAt(null)->exists()) {
|
||||
throw new ApiException('子任务未完成', [
|
||||
@@ -1343,6 +1379,16 @@ class ProjectTask extends AbstractModel
|
||||
if (!$this->hasOwner()) {
|
||||
throw new ApiException('请先领取任务');
|
||||
}
|
||||
if ($this->overdue) {
|
||||
$this->addLog("{任务}超期未完成", [
|
||||
'cache' => [
|
||||
'task_at' => $this->start_at . '~' . $this->end_at,
|
||||
'over_sec' => Carbon::now()->diffInSeconds($this->end_at),
|
||||
'owners' => $this->taskUser->where('owner', 1)->pluck('userid')->toArray(),
|
||||
'assists' => $this->taskUser->where('owner', 0)->pluck('userid')->toArray(),
|
||||
]
|
||||
]);
|
||||
}
|
||||
if (empty($complete_name)) {
|
||||
$complete_name = '已完成';
|
||||
}
|
||||
@@ -1389,11 +1435,12 @@ class ProjectTask extends AbstractModel
|
||||
$this->archived_at = null;
|
||||
$this->archived_userid = User::userid();
|
||||
$this->archived_follow = 0;
|
||||
$this->addLog("任务取消归档");
|
||||
$logText = "任务取消归档";
|
||||
$userid = 0;
|
||||
} else {
|
||||
// 归档任务
|
||||
if ($isAuto === true) {
|
||||
$logText = "自动任务归档";
|
||||
$logText = "任务自动归档";
|
||||
$userid = 0;
|
||||
} else {
|
||||
$logText = "任务归档";
|
||||
@@ -1402,13 +1449,20 @@ class ProjectTask extends AbstractModel
|
||||
$this->archived_at = $archived_at;
|
||||
$this->archived_userid = $userid;
|
||||
$this->archived_follow = 0;
|
||||
$this->addLog($logText, [], $userid);
|
||||
}
|
||||
// 添加日志
|
||||
$this->addLog($logText, [], $userid);
|
||||
// 推送状态
|
||||
$this->pushMsg($archived_at === null ? 'recovery' : 'archived', [
|
||||
'id' => $this->id,
|
||||
'archived_at' => $this->archived_at,
|
||||
'archived_userid' => $this->archived_userid,
|
||||
]);
|
||||
// 更新对话时间
|
||||
if ($this->dialog_id > 0) {
|
||||
WebSocketDialogUser::whereDialogId($this->dialog_id)->update(['updated_at' => Carbon::now()]); // 因为是若提醒,可以直接使用 update 更新
|
||||
}
|
||||
// 更新保存
|
||||
self::whereParentId($this->id)->change([
|
||||
'archived_at' => $this->archived_at,
|
||||
'archived_userid' => $this->archived_userid,
|
||||
@@ -1797,16 +1851,30 @@ class ProjectTask extends AbstractModel
|
||||
* @param int $flowItemId
|
||||
* @param array $owner
|
||||
* @param array $assist
|
||||
* @param string $completeAt
|
||||
* @param string|null $completed
|
||||
* @return bool
|
||||
*/
|
||||
public function moveTask(int $projectId, int $columnId,int $flowItemId = 0,array $owner = [], array $assist = [], string $completeAt='')
|
||||
public function moveTask(int $projectId, int $columnId, int $flowItemId = 0, array $owner = [], array $assist = [], ?string $completed = null)
|
||||
{
|
||||
AbstractModel::transaction(function () use ($projectId, $columnId, $flowItemId, $owner, $assist, $completeAt) {
|
||||
AbstractModel::transaction(function () use ($projectId, $columnId, $flowItemId, $owner, $assist, $completed) {
|
||||
$newTaskUser = array_merge($owner, $assist);
|
||||
//
|
||||
$oldProject = Project::find($this->project_id);
|
||||
$newProject = $this->project_id != $projectId ? Project::find($projectId) : $oldProject;
|
||||
if (!$oldProject || !$newProject) {
|
||||
throw new ApiException('项目不存在');
|
||||
}
|
||||
//
|
||||
$this->project_id = $projectId;
|
||||
$this->column_id = $columnId;
|
||||
// 日志
|
||||
$log = $this->addLog("移动{任务}", [
|
||||
'change' => [$oldProject->name, $newProject->name]
|
||||
]);
|
||||
if ($this->dialog_id) {
|
||||
$notice = $oldProject->id != $newProject->id ? "「{$oldProject->name}」移动至「{$newProject->name}」" : $log->detail;
|
||||
WebSocketDialogMsg::sendMsg(null, $this->dialog_id, 'notice', ['notice' => $notice], User::userid(), true, true);
|
||||
}
|
||||
// 任务内容
|
||||
if ($this->content) {
|
||||
$this->content->project_id = $projectId;
|
||||
@@ -1840,6 +1908,7 @@ class ProjectTask extends AbstractModel
|
||||
]);
|
||||
//
|
||||
if ($flowItemId) {
|
||||
// 更新任务流程
|
||||
$flowItem = projectFlowItem::whereProjectId($projectId)->whereId($flowItemId)->first();
|
||||
$this->flow_item_id = $flowItemId;
|
||||
$this->flow_item_name = $flowItem->status . "|" . $flowItem->name;
|
||||
@@ -1849,22 +1918,81 @@ class ProjectTask extends AbstractModel
|
||||
$this->completeTask(null);
|
||||
}
|
||||
} else {
|
||||
// 没有流程只更新状态
|
||||
$this->flow_item_id = 0;
|
||||
$this->flow_item_name = '';
|
||||
}
|
||||
//
|
||||
if ($completeAt) {
|
||||
$this->complete_at = $completeAt;
|
||||
if ($completed !== null) {
|
||||
$this->completeTask($completed ? Carbon::now(): null);
|
||||
}
|
||||
}
|
||||
//
|
||||
$this->save();
|
||||
//
|
||||
$this->addLog("移动{任务}");
|
||||
});
|
||||
$this->pushMsg('update');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成AI上下文
|
||||
* @return array
|
||||
*/
|
||||
public function AIContext()
|
||||
{
|
||||
$contexts = [];
|
||||
if ($this->archived_at) {
|
||||
$contexts[] = "任务状态:已归档";
|
||||
$contexts[] = "归档时间:" . $this->archived_at;
|
||||
} elseif ($this->complete_at) {
|
||||
$contexts[] = "任务状态:已完成";
|
||||
$contexts[] = "完成时间:" . $this->complete_at;
|
||||
} elseif ($this->end_at && Carbon::parse($this->end_at)->lt(Carbon::now())) {
|
||||
$contexts[] = "任务状态:已过期";
|
||||
$contexts[] = "任务截止时间:" . $this->end_at;
|
||||
} else {
|
||||
$contexts[] = "任务状态:进行中";
|
||||
if ($this->start_at) {
|
||||
$contexts[] = "任务开始时间:" . $this->start_at;
|
||||
}
|
||||
if ($this->end_at) {
|
||||
$contexts[] = "任务截止时间:" . $this->end_at;
|
||||
}
|
||||
}
|
||||
$contexts[] = "当前系统时间:" . Carbon::now()->toDateTimeString();
|
||||
if ($this->content) {
|
||||
$taskDesc = $this->content?->getContentInfo();
|
||||
if ($taskDesc) {
|
||||
$converter = new HtmlConverter(['strip_tags' => true]);
|
||||
$descContent = Base::cutStr($converter->convert($taskDesc['content']), 2000);
|
||||
$contexts[] = <<<EOF
|
||||
任务描述:
|
||||
```md
|
||||
{$descContent}
|
||||
```
|
||||
EOF;
|
||||
}
|
||||
}
|
||||
$subTask = ProjectTask::select(['id', 'name', 'complete_at', 'end_at'])->whereParentId($this->id)->get();
|
||||
if ($subTask->isNotEmpty()) {
|
||||
$subTaskContent = $subTask->map(function($item) {
|
||||
if ($item->complete_at) {
|
||||
$status = " (已完成)";
|
||||
} elseif ($item->end_at && Carbon::parse($item->end_at)->lt(Carbon::now())) {
|
||||
$status = " (已过期)";
|
||||
} else {
|
||||
$status = " (进行中)";
|
||||
}
|
||||
return " - {$item->name} {$status}";
|
||||
})->join("\n");
|
||||
if ($subTaskContent) {
|
||||
$contexts[] = <<<EOF
|
||||
子任务列表:
|
||||
{$subTaskContent}
|
||||
EOF;
|
||||
}
|
||||
}
|
||||
return $contexts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务
|
||||
* @param $task_id
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace App\Models;
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\Project $project
|
||||
* @property-read \App\Models\User|null $user
|
||||
* @property-read \App\Models\User $user
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Module\Base;
|
||||
|
||||
/**
|
||||
* App\Models\ProjectUser
|
||||
*
|
||||
@@ -50,7 +52,9 @@ class ProjectUser extends AbstractModel
|
||||
*/
|
||||
public static function transfer($originalUserid, $newUserid)
|
||||
{
|
||||
self::whereUserid($originalUserid)->chunkById(100, function ($list) use ($originalUserid, $newUserid) {
|
||||
$projectIds = [];
|
||||
// 移交项目身份
|
||||
self::whereUserid($originalUserid)->chunkById(100, function ($list) use ($originalUserid, $newUserid, &$projectIds) {
|
||||
/** @var self $item */
|
||||
foreach ($list as $item) {
|
||||
$row = self::whereProjectId($item->project_id)->whereUserid($newUserid)->first();
|
||||
@@ -72,9 +76,23 @@ class ProjectUser extends AbstractModel
|
||||
}
|
||||
$item->project->addLog("移交项目身份", ['userid' => [$originalUserid, ' => ', $newUserid]]);
|
||||
$item->project->syncDialogUser();
|
||||
$projectIds[] = $item->project_id;
|
||||
}
|
||||
}
|
||||
});
|
||||
// 移交工作流状态负责人
|
||||
if ($projectIds) {
|
||||
ProjectFlowItem::whereIn('project_id', $projectIds)->chunkById(100, function ($list) use ($originalUserid, $newUserid) {
|
||||
/** @var ProjectFlowItem $item */
|
||||
foreach ($list as $item) {
|
||||
if (in_array($originalUserid, $item->userids)) {
|
||||
$userids = array_values(array_diff($item->userids, [$originalUserid]));
|
||||
$item->userids = Base::array2json(array_merge($userids, [$newUserid]));
|
||||
$item->save();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Module\Base;
|
||||
use Carbon\Carbon;
|
||||
use Carbon\Traits\Creator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
@@ -95,6 +96,24 @@ class Report extends AbstractModel
|
||||
return $this->appendattrs['receives'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取汇报内容
|
||||
* @param $id
|
||||
* @return self|null
|
||||
*/
|
||||
public static function idOrCodeToContent($id)
|
||||
{
|
||||
if (Base::isNumber($id)) {
|
||||
return self::find($id);
|
||||
} elseif ($id) {
|
||||
$reportLink = ReportLink::whereCode($id)->first();
|
||||
if ($reportLink) {
|
||||
return self::find($reportLink->rid);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单条记录
|
||||
* @param $id
|
||||
@@ -139,12 +158,12 @@ class Report extends AbstractModel
|
||||
// 如果设置了周期偏移量
|
||||
empty( $offset ) || $now_dt->subWeeks( abs( $offset ) );
|
||||
$now_dt->startOfWeek(); // 设置为当周第一天
|
||||
return $now_dt->year . $now_dt->weekOfYear;
|
||||
return now()->year . $now_dt->weekOfYear;
|
||||
},
|
||||
Report::DAILY => function() use ($now_dt, $offset) {
|
||||
// 如果设置了周期偏移量
|
||||
empty( $offset ) || $now_dt->subDays( abs( $offset ) );
|
||||
return $now_dt->format("Ymd");
|
||||
return now()->format("Ymd");
|
||||
},
|
||||
default => "",
|
||||
};
|
||||
|
||||
86
app/Models/ReportLink.php
Normal file
86
app/Models/ReportLink.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Module\Base;
|
||||
|
||||
/**
|
||||
* App\Models\ReportLink
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $rid 报告ID
|
||||
* @property int|null $num 累计访问
|
||||
* @property string|null $code 链接码
|
||||
* @property int|null $userid 会员ID
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\Report|null $report
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereCode($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereNum($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereRid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ReportLink extends AbstractModel
|
||||
{
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
*/
|
||||
public function report(): \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
{
|
||||
return $this->hasOne(Report::class, 'id', 'report_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成链接
|
||||
* @param $rid
|
||||
* @param $userid
|
||||
* @param $refresh
|
||||
* @return array
|
||||
*/
|
||||
public static function generateLink($rid, $userid, $refresh = false)
|
||||
{
|
||||
$report = Report::find($rid);
|
||||
if (empty($report)) {
|
||||
throw new ApiException('报告不存在或已被删除');
|
||||
}
|
||||
if ($report->userid != $userid) {
|
||||
if (!ReportReceive::whereRid($rid)->whereUserid($userid)->exists()) {
|
||||
throw new ApiException('您没有权限查看该报告');
|
||||
}
|
||||
}
|
||||
$reportLink = ReportLink::whereRid($rid)->whereUserid($userid)->first();
|
||||
if (empty($reportLink)) {
|
||||
$reportLink = ReportLink::createInstance([
|
||||
'rid' => $rid,
|
||||
'userid' => $userid,
|
||||
'code' => base64_encode("{$rid},{$userid}," . Base::generatePassword()),
|
||||
]);
|
||||
$reportLink->save();
|
||||
} else {
|
||||
if ($refresh == 'yes') {
|
||||
$reportLink->code = base64_encode("{$rid},{$userid}," . Base::generatePassword());
|
||||
$reportLink->save();
|
||||
}
|
||||
}
|
||||
return [
|
||||
'id' => $rid,
|
||||
'url' => Base::fillUrl('single/report/detail/' . $reportLink->code),
|
||||
'code' => $reportLink->code,
|
||||
'num' => $reportLink->num
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
use App\Module\Timer;
|
||||
use Carbon\Carbon;
|
||||
|
||||
/**
|
||||
* App\Models\Setting
|
||||
@@ -65,20 +68,34 @@ class Setting extends AbstractModel
|
||||
$value['claude_key'] = $value['claude_token'];
|
||||
}
|
||||
$array = [];
|
||||
$aiList = ['openai', 'claude', 'gemini', 'zhipu', 'qianwen', 'wenxin'];
|
||||
$fieldList = ['key', 'model', 'agency', 'system', 'secret'];
|
||||
$aiList = ['openai', 'claude', 'deepseek', 'gemini', 'grok', 'ollama', 'zhipu', 'qianwen', 'wenxin'];
|
||||
$fieldList = ['key', 'secret', 'models', 'model', 'base_url', 'agency', 'temperature', 'system'];
|
||||
foreach ($aiList as $aiName) {
|
||||
foreach ($fieldList as $fieldName) {
|
||||
$key = $aiName . '_' . $fieldName;
|
||||
$array[$key] = $value[$key] ?: match ($key) {
|
||||
'openai_model' => 'gpt-4o-mini',
|
||||
'claude_model' => 'claude-3-5-sonnet-latest',
|
||||
'gemini_model' => 'gemini-1.5-flash',
|
||||
'zhipu_model' => 'glm-4',
|
||||
'qianwen_model' => 'qwen-turbo',
|
||||
'wenxin_model' => 'ernie-4.0-8k',
|
||||
default => '',
|
||||
};
|
||||
$content = $value[$key] ? trim($value[$key]) : '';
|
||||
switch ($fieldName) {
|
||||
case 'models':
|
||||
if ($content) {
|
||||
$content = explode("\n", $content);
|
||||
$content = array_filter($content);
|
||||
}
|
||||
if (empty($content)) {
|
||||
$content = self::AIDefaultModels($aiName);
|
||||
}
|
||||
$content = implode("\n", $content);
|
||||
break;
|
||||
case 'model':
|
||||
$models = Setting::AIModels2Array($array[$key . 's'], true);
|
||||
$content = in_array($content, $models) ? $content : ($models[0] ?? '');
|
||||
break;
|
||||
case 'temperature':
|
||||
if ($content) {
|
||||
$content = floatval(min(1, max(0, floatval($content) ?: 0.7)));
|
||||
}
|
||||
break;
|
||||
}
|
||||
$array[$key] = $content;
|
||||
}
|
||||
}
|
||||
$value = $array;
|
||||
@@ -98,6 +115,121 @@ class Setting extends AbstractModel
|
||||
return !!$array[$ai . '_key'];
|
||||
}
|
||||
|
||||
/**
|
||||
* AI默认模型
|
||||
* @param string $ai
|
||||
* @return array
|
||||
*/
|
||||
public static function AIDefaultModels($ai = 'openai')
|
||||
{
|
||||
return match ($ai) {
|
||||
'openai' => [
|
||||
'gpt-4 | GPT-4',
|
||||
'gpt-4-turbo | GPT-4 Turbo',
|
||||
'gpt-4o | GPT-4o',
|
||||
'gpt-4o-mini | GPT-4o Mini',
|
||||
'o1 | GPT-o1',
|
||||
'o1-mini | GPT-o1 Mini',
|
||||
'o3-mini | GPT-o3 Mini',
|
||||
'gpt-3.5-turbo | GPT-3.5 Turbo',
|
||||
'gpt-3.5-turbo-16k | GPT-3.5 Turbo 16K',
|
||||
'gpt-3.5-turbo-0125 | GPT-3.5 Turbo 0125',
|
||||
'gpt-3.5-turbo-1106 | GPT-3.5 Turbo 1106'
|
||||
],
|
||||
'claude' => [
|
||||
'claude-3-5-sonnet-latest | Claude 3.5 Sonnet',
|
||||
'claude-3-5-sonnet-20241022 | Claude 3.5 Sonnet 20241022',
|
||||
'claude-3-5-haiku-latest | Claude 3.5 Haiku',
|
||||
'claude-3-5-haiku-20241022 | Claude 3.5 Haiku 20241022',
|
||||
'claude-3-opus-latest | Claude 3 Opus',
|
||||
'claude-3-opus-20240229 | Claude 3 Opus 20240229',
|
||||
'claude-3-haiku-20240307 | Claude 3 Haiku 20240307',
|
||||
'claude-2.1 | Claude 2.1',
|
||||
'claude-2.0 | Claude 2.0'
|
||||
],
|
||||
'deepseek' => [
|
||||
'deepseek-chat | DeepSeek V3',
|
||||
'deepseek-reasoner | DeepSeek R1'
|
||||
],
|
||||
'gemini' => [
|
||||
'gemini-2.0-flash | Gemini 2.0 Flash',
|
||||
'gemini-2.0-flash-lite-preview-02-05 | Gemini 2.0 Flash-Lite Preview',
|
||||
'gemini-1.5-flash | Gemini 1.5 Flash',
|
||||
'gemini-1.5-flash-8b | Gemini 1.5 Flash 8B',
|
||||
'gemini-1.5-pro | Gemini 1.5 Pro',
|
||||
'gemini-1.0-pro | Gemini 1.0 Pro'
|
||||
],
|
||||
'grok' => [
|
||||
'grok-2-vision-1212 | Grok 2 Vision 1212',
|
||||
'grok-2-vision | Grok 2 Vision',
|
||||
'grok-2-vision-latest | Grok 2 Vision Latest',
|
||||
'grok-2-1212 | Grok 2 1212',
|
||||
'grok-2 | Grok 2',
|
||||
'grok-2-latest | Grok 2 Latest',
|
||||
'grok-vision-beta | Grok Vision Beta',
|
||||
'grok-beta | Grok Beta',
|
||||
],
|
||||
'zhipu' => [
|
||||
'glm-4 | GLM-4',
|
||||
'glm-4-plus | GLM-4 Plus',
|
||||
'glm-4-air | GLM-4 Air',
|
||||
'glm-4-airx | GLM-4 AirX',
|
||||
'glm-4-long | GLM-4 Long',
|
||||
'glm-4-flash | GLM-4 Flash',
|
||||
'glm-4v | GLM-4V',
|
||||
'glm-4v-plus | GLM-4V Plus',
|
||||
'glm-3-turbo | GLM-3 Turbo'
|
||||
],
|
||||
'qianwen' => [
|
||||
'qwen-max | QWEN Max',
|
||||
'qwen-max-latest | QWEN Max Latest',
|
||||
'qwen-turbo | QWEN Turbo',
|
||||
'qwen-turbo-latest | QWEN Turbo Latest',
|
||||
'qwen-plus | QWEN Plus',
|
||||
'qwen-plus-latest | QWEN Plus Latest',
|
||||
'qwen-long | QWEN Long'
|
||||
],
|
||||
'wenxin' => [
|
||||
'ernie-4.0-8k | Ernie 4.0 8K',
|
||||
'ernie-4.0-8k-latest | Ernie 4.0 8K Latest',
|
||||
'ernie-4.0-turbo-128k | Ernie 4.0 Turbo 128K',
|
||||
'ernie-4.0-turbo-8k | Ernie 4.0 Turbo 8K',
|
||||
'ernie-3.5-128k | Ernie 3.5 128K',
|
||||
'ernie-3.5-8k | Ernie 3.5 8K',
|
||||
'ernie-speed-128k | Ernie Speed 128K',
|
||||
'ernie-speed-8k | Ernie Speed 8K',
|
||||
'ernie-lite-8k | Ernie Lite 8K',
|
||||
'ernie-tiny-8k | Ernie Tiny 8K'
|
||||
],
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* AI模型转数组
|
||||
* @param $models
|
||||
* @param bool $retValue
|
||||
* @return array
|
||||
*/
|
||||
public static function AIModels2Array($models, $retValue = false)
|
||||
{
|
||||
$list = is_array($models) ? $models : explode("\n", $models);
|
||||
$array = [];
|
||||
foreach ($list as $item) {
|
||||
$arr = Base::newTrim(explode('|', $item . '|'));
|
||||
if ($arr[0]) {
|
||||
$array[] = [
|
||||
'value' => $arr[0],
|
||||
'label' => $arr[1] ?: $arr[0]
|
||||
];
|
||||
}
|
||||
}
|
||||
if ($retValue) {
|
||||
return array_column($array, 'value');
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮箱地址(过滤忽略地址)
|
||||
* @param $array
|
||||
@@ -134,4 +266,36 @@ class Setting extends AbstractModel
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证消息限制
|
||||
* @param $type
|
||||
* @param $msg
|
||||
* @return void
|
||||
*/
|
||||
public static function validateMsgLimit($type, $msg)
|
||||
{
|
||||
$keyName = 'msg_edit_limit';
|
||||
$error = '此消息不可修改';
|
||||
if ($type == 'rev') {
|
||||
$keyName = 'msg_rev_limit';
|
||||
$error = '此消息不可撤回';
|
||||
}
|
||||
$limitNum = intval(Base::settingFind('system', $keyName, 0));
|
||||
if ($limitNum <= 0) {
|
||||
return;
|
||||
}
|
||||
if ($msg instanceof WebSocketDialogMsg) {
|
||||
$dialogMsg = $msg;
|
||||
} else {
|
||||
$dialogMsg = WebSocketDialogMsg::find($msg);
|
||||
}
|
||||
if (!$dialogMsg) {
|
||||
return;
|
||||
}
|
||||
$limitTime = Carbon::parse($dialogMsg->created_at)->addMinutes($limitNum);
|
||||
if ($limitTime->lt(Carbon::now())) {
|
||||
throw new ApiException('已超过' . Doo::translate(Base::forumMinuteDay($limitNum)) . ',' . $error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use Hedeqiang\UMeng\IOS;
|
||||
* @property string|null $alias 别名
|
||||
* @property string|null $platform 平台类型
|
||||
* @property string|null $device 设备类型
|
||||
* @property string|null $device_hash 设备哈希值,用于关联UserDevice表
|
||||
* @property string|null $version 应用版本号
|
||||
* @property string|null $ua userAgent
|
||||
* @property int|null $is_notified 通知权限
|
||||
@@ -32,13 +33,14 @@ use Hedeqiang\UMeng\IOS;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereAlias($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereDevice($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereVersion($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereDeviceHash($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereIsNotified($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias wherePlatform($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereUa($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereUserid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereVersion($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class UmengAlias extends AbstractModel
|
||||
|
||||
@@ -242,6 +242,31 @@ class User extends AbstractModel
|
||||
return in_array('admin', $this->identity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回是否AI机器人
|
||||
* @return bool
|
||||
*/
|
||||
public function isAiBot(&$aiName = '')
|
||||
{
|
||||
if (preg_match('/^ai-(.*?)@bot\.system$/', $this->email, $matches)) {
|
||||
$aiName = $matches[1];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回是否用户机器人
|
||||
* @return bool
|
||||
*/
|
||||
public function isUserBot()
|
||||
{
|
||||
if (preg_match('/^user-(.*?)@bot\.system$/', $this->email)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否管理员
|
||||
*/
|
||||
@@ -419,7 +444,9 @@ class User extends AbstractModel
|
||||
{
|
||||
$user = self::authInfo();
|
||||
if (!$user) {
|
||||
if (Base::token()) {
|
||||
$token = Base::token();
|
||||
if ($token) {
|
||||
UserDevice::forget($token);
|
||||
throw new ApiException('身份已失效,请重新登录', [], -1);
|
||||
} else {
|
||||
throw new ApiException('请登录后继续...', [], -1);
|
||||
@@ -441,31 +468,46 @@ class User extends AbstractModel
|
||||
private static function authInfo()
|
||||
{
|
||||
if (RequestContext::has('auth')) {
|
||||
// 缓存
|
||||
return RequestContext::get('auth');
|
||||
}
|
||||
if (Doo::userId() > 0
|
||||
&& !Doo::userExpired()
|
||||
&& $user = self::whereUserid(Doo::userId())->whereEmail(Doo::userEmail())->whereEncrypt(Doo::userEncrypt())->first()) {
|
||||
$upArray = [];
|
||||
if (Base::getIp() && $user->line_ip != Base::getIp()) {
|
||||
$upArray['line_ip'] = Base::getIp();
|
||||
}
|
||||
if (Carbon::parse($user->line_at)->addSeconds(30)->lt(Carbon::now())) {
|
||||
$upArray['line_at'] = Carbon::now();
|
||||
}
|
||||
$headerLanguage = RequestContext::get('header_language');
|
||||
if (empty($user->lang) || $headerLanguage) {
|
||||
if (Doo::checkLanguage($headerLanguage) && $user->lang != $headerLanguage) {
|
||||
$upArray['lang'] = $headerLanguage;
|
||||
}
|
||||
}
|
||||
if ($upArray) {
|
||||
$user->updateInstance($upArray);
|
||||
$user->save();
|
||||
}
|
||||
return RequestContext::save('auth', $user);
|
||||
if (Doo::userId() <= 0) {
|
||||
// 没有登录
|
||||
return RequestContext::save('auth', false);
|
||||
}
|
||||
return RequestContext::save('auth', false);
|
||||
if (Doo::userExpired()) {
|
||||
// 登录过期
|
||||
return RequestContext::save('auth', false);
|
||||
}
|
||||
if (!UserDevice::check()) {
|
||||
// token 不存在
|
||||
return RequestContext::save('auth', false);
|
||||
}
|
||||
$user = self::whereUserid(Doo::userId())->whereEmail(Doo::userEmail())->whereEncrypt(Doo::userEncrypt())->first();
|
||||
if (!$user) {
|
||||
// 登录信息不匹配
|
||||
return RequestContext::save('auth', false);
|
||||
}
|
||||
|
||||
// 更新登录信息
|
||||
$upArray = [];
|
||||
if (Base::getIp() && $user->line_ip != Base::getIp()) {
|
||||
$upArray['line_ip'] = Base::getIp();
|
||||
}
|
||||
if (Carbon::parse($user->line_at)->addSeconds(30)->lt(Carbon::now())) {
|
||||
$upArray['line_at'] = Carbon::now();
|
||||
}
|
||||
$headerLanguage = RequestContext::get('header_language');
|
||||
if (empty($user->lang) || $headerLanguage) {
|
||||
if (Doo::checkLanguage($headerLanguage) && $user->lang != $headerLanguage) {
|
||||
$upArray['lang'] = $headerLanguage;
|
||||
}
|
||||
}
|
||||
if ($upArray) {
|
||||
$user->updateInstance($upArray);
|
||||
$user->save();
|
||||
}
|
||||
return RequestContext::save('auth', $user);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -489,6 +531,7 @@ class User extends AbstractModel
|
||||
} else {
|
||||
$token = Doo::userToken();
|
||||
}
|
||||
UserDevice::record($token);
|
||||
unset($userinfo->encrypt);
|
||||
unset($userinfo->password);
|
||||
return $userinfo->token = $token;
|
||||
@@ -593,8 +636,14 @@ class User extends AbstractModel
|
||||
return url("images/avatar/default_openai.png");
|
||||
case 'ai-claude@bot.system':
|
||||
return url("images/avatar/default_claude.png");
|
||||
case 'ai-deepseek@bot.system':
|
||||
return url("images/avatar/default_deepseek.png");
|
||||
case 'ai-gemini@bot.system':
|
||||
return url("images/avatar/default_gemini.png");
|
||||
case 'ai-grok@bot.system':
|
||||
return url("images/avatar/default_grok.png");
|
||||
case 'ai-ollama@bot.system':
|
||||
return url("images/avatar/default_ollama.png");
|
||||
case 'ai-zhipu@bot.system':
|
||||
return url("images/avatar/default_zhipu.png");
|
||||
case 'bot-manager@bot.system':
|
||||
@@ -700,4 +749,26 @@ class User extends AbstractModel
|
||||
}
|
||||
return (bool)User::find($userid)?->bot;
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索用户
|
||||
* @param $key
|
||||
* @param $take
|
||||
* @return User[]|\Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Query\Builder[]|\Illuminate\Support\Collection
|
||||
*/
|
||||
public static function searchUser($key, $take = 20)
|
||||
{
|
||||
return User::select(User::$basicField)
|
||||
->where(function ($query) use ($key) {
|
||||
if (str_contains($key, "@")) {
|
||||
$query->where("email", "like", "%{$key}%");
|
||||
} else {
|
||||
$query->where("nickname", "like", "%{$key}%")
|
||||
->orWhere("pinyin", "like", "%{$key}%")
|
||||
->orWhere("profession", "like", "%{$key}%");
|
||||
}
|
||||
})->orderBy('userid')
|
||||
->take($take)
|
||||
->get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,16 +55,6 @@ class UserBot extends AbstractModel
|
||||
return str_ends_with($email, '@bot.system') && self::systemBotName($email);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否系统AI机器人
|
||||
* @param $email
|
||||
* @return bool
|
||||
*/
|
||||
public static function isAiBot($email)
|
||||
{
|
||||
return str_starts_with($email, 'ai-') && self::isSystemBot($email);
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统机器人名称
|
||||
* @param $name string 邮箱 或 邮箱前缀
|
||||
@@ -83,10 +73,13 @@ class UserBot extends AbstractModel
|
||||
'approval-alert' => '审批',
|
||||
'ai-openai' => 'ChatGPT',
|
||||
'ai-claude' => 'Claude',
|
||||
'ai-wenxin' => '文心一言',
|
||||
'ai-qianwen' => '通义千问',
|
||||
'ai-deepseek' => 'DeepSeek',
|
||||
'ai-gemini' => 'Gemini',
|
||||
'ai-grok' => 'Grok',
|
||||
'ai-ollama' => 'Ollama',
|
||||
'ai-zhipu' => '智谱清言',
|
||||
'ai-qianwen' => '通义千问',
|
||||
'ai-wenxin' => '文心一言',
|
||||
'bot-manager' => '机器人管理',
|
||||
'meeting-alert' => '会议通知',
|
||||
'okr-alert' => 'OKR提醒',
|
||||
@@ -187,11 +180,35 @@ class UserBot extends AbstractModel
|
||||
];
|
||||
|
||||
default:
|
||||
if (preg_match('/^ai-(.*?)@bot\.system$/', $email)) {
|
||||
return [
|
||||
[
|
||||
if (preg_match('/^ai-(.*?)@bot\.system$/', $email, $match)) {
|
||||
if (!Base::judgeClientVersion('0.42.62')) {
|
||||
return [
|
||||
'key' => '%3A.clear',
|
||||
'label' => Doo::translate('清空上下文')
|
||||
];
|
||||
}
|
||||
$aibotSetting = Base::setting('aibotSetting');
|
||||
$aibotModel = $aibotSetting[$match[1] . '_model'];
|
||||
$aibotModels = Setting::AIModels2Array($aibotSetting[$match[1] . '_models']);
|
||||
if (empty($aibotModels)) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
[
|
||||
'key' => '~ai-model-select',
|
||||
'label' => Doo::translate('选择模型'),
|
||||
'config' => [
|
||||
'model' => $aibotModel,
|
||||
'models' => $aibotModels
|
||||
]
|
||||
],
|
||||
[
|
||||
'key' => '~ai-session-create',
|
||||
'label' => Doo::translate('开启新会话'),
|
||||
],
|
||||
[
|
||||
'key' => '~ai-session-history',
|
||||
'label' => Doo::translate('历史会话'),
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -425,4 +442,39 @@ class UserBot extends AbstractModel
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建我的机器人
|
||||
* @param $userid
|
||||
* @param $botName
|
||||
* @return array
|
||||
*/
|
||||
public static function newbot($userid, $botName)
|
||||
{
|
||||
if (User::select(['users.*'])
|
||||
->join('user_bots', 'users.userid', '=', 'user_bots.bot_id')
|
||||
->where('users.bot', 1)
|
||||
->where('user_bots.userid', $userid)
|
||||
->count() >= 50) {
|
||||
return Base::retError("超过最大创建数量。");
|
||||
}
|
||||
if (strlen($botName) < 2 || strlen($botName) > 20) {
|
||||
return Base::retError("机器人名称由2-20个字符组成。");
|
||||
}
|
||||
$data = User::botGetOrCreate("user-" . Base::generatePassword(), [
|
||||
'nickname' => $botName
|
||||
], $userid);
|
||||
if (empty($data)) {
|
||||
return Base::retError("创建失败。");
|
||||
}
|
||||
$dialog = WebSocketDialog::checkUserDialog($data, $userid);
|
||||
if ($dialog) {
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||
'type' => '/hello',
|
||||
'title' => '创建成功。',
|
||||
'data' => $data,
|
||||
], $data->userid);
|
||||
}
|
||||
return Base::retSuccess("创建成功。", $data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ class UserCheckinFace extends AbstractModel
|
||||
$record = base64_encode(file_get_contents($faceFile));
|
||||
}
|
||||
|
||||
$url = 'http://' . env('APP_IPPR') . '.14' . ":7788/user";
|
||||
$url = "http://face:7788/user";
|
||||
$data = [
|
||||
'name' => $nickname,
|
||||
'enrollid' => $userid,
|
||||
@@ -92,7 +92,7 @@ class UserCheckinFace extends AbstractModel
|
||||
}
|
||||
|
||||
public static function deleteDeviceUser($userid) {
|
||||
$url = 'http://' . env('APP_IPPR') . '.14' . ":7788/user/delete";
|
||||
$url = "http://face:7788/user/delete";
|
||||
$data = [
|
||||
'enrollid' => $userid,
|
||||
'backupnum' => 50, // 13 删除整个用户 50 删除图片
|
||||
|
||||
@@ -37,11 +37,6 @@ use App\Module\Base;
|
||||
*/
|
||||
class UserDelete extends AbstractModel
|
||||
{
|
||||
/**
|
||||
* 昵称
|
||||
* @param $value
|
||||
* @return string
|
||||
*/
|
||||
public function getCacheAttribute($value)
|
||||
{
|
||||
if (!is_array($value)) {
|
||||
@@ -65,13 +60,25 @@ class UserDelete extends AbstractModel
|
||||
*/
|
||||
public static function userid2basic($userid)
|
||||
{
|
||||
$row = self::whereUserid($userid)->first();
|
||||
if (empty($row) || empty($row->cache)) {
|
||||
return null;
|
||||
}
|
||||
$cache = $row->cache;
|
||||
$cache = array_intersect_key($cache, array_flip(array_merge(User::$basicField, ['department_name'])));
|
||||
$cache['delete_at'] = $row->created_at->toDateTimeString();
|
||||
return $cache;
|
||||
return \Cache::remember("UserDelete:{$userid}", now()->addDays(3), function () use ($userid) {
|
||||
$row = self::whereUserid($userid)->first();
|
||||
if (empty($row) || empty($row->cache)) {
|
||||
return null;
|
||||
}
|
||||
$cache = $row->cache;
|
||||
$cache = array_intersect_key($cache, array_flip(array_merge(User::$basicField, ['department_name'])));
|
||||
$cache['delete_at'] = $row->created_at->toDateTimeString();
|
||||
return $cache;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* userid 获取 昵称
|
||||
* @param $userid
|
||||
* @return string
|
||||
*/
|
||||
public static function userid2nickname($userid)
|
||||
{
|
||||
return self::userid2basic($userid)['nickname'] ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,21 @@ use App\Exceptions\ApiException;
|
||||
*/
|
||||
class UserDepartment extends AbstractModel
|
||||
{
|
||||
/**
|
||||
* 获取所有父级部门
|
||||
* @return array
|
||||
*/
|
||||
public function parents()
|
||||
{
|
||||
$parents = [];
|
||||
$parent = $this;
|
||||
while ($parent) {
|
||||
$parents[] = $parent;
|
||||
$parent = $parent->parent_id ? self::find($parent->parent_id) : null;
|
||||
}
|
||||
return $parents;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存部门
|
||||
* @param $data
|
||||
@@ -131,9 +146,7 @@ class UserDepartment extends AbstractModel
|
||||
});
|
||||
// 解散群组
|
||||
$dialog = WebSocketDialog::find($this->dialog_id);
|
||||
if ($dialog) {
|
||||
$dialog->deleteDialog();
|
||||
}
|
||||
$dialog?->deleteDialog();
|
||||
//
|
||||
$this->delete();
|
||||
}
|
||||
|
||||
314
app/Models/UserDevice.php
Normal file
314
app/Models/UserDevice.php
Normal file
@@ -0,0 +1,314 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
use DeviceDetector\DeviceDetector;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Request;
|
||||
|
||||
/**
|
||||
* App\Models\UserDevice
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $userid 会员ID
|
||||
* @property string|null $hash TOKEN MD5
|
||||
* @property string|null $detail 详细信息
|
||||
* @property string|null $expired_at 过期时间
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||
* @property-read int $is_current
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice onlyTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereDeletedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereDetail($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereExpiredAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereHash($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereUserid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice withTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice withoutTrashed()
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class UserDevice extends AbstractModel
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'user_devices';
|
||||
|
||||
public static int $deviceLimit = 200; // 每个用户设备限制数量
|
||||
|
||||
protected $appends = [
|
||||
'is_current',
|
||||
];
|
||||
|
||||
public function getDetailAttribute($value)
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
return Base::json2array($value);
|
||||
}
|
||||
|
||||
public function getIsCurrentAttribute(): int
|
||||
{
|
||||
return $this->hash === md5(Doo::userToken()) ? 1 : 0;
|
||||
}
|
||||
|
||||
/** ****************************************************************************** */
|
||||
/** ****************************************************************************** */
|
||||
/** ****************************************************************************** */
|
||||
|
||||
/**
|
||||
* 缓存key
|
||||
* @param string $hash
|
||||
* @return string
|
||||
*/
|
||||
private static function ck(string $hash): string
|
||||
{
|
||||
return "user_devices:{$hash}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 UA 获取设备信息
|
||||
* @param string $ua
|
||||
* @return array
|
||||
*/
|
||||
private static function getDeviceInfo(string $ua): array
|
||||
{
|
||||
$result = [
|
||||
'ip' => Base::getIp(),
|
||||
'type' => '电脑',
|
||||
'os' => 'Unknown',
|
||||
'browser' => 'Unknown',
|
||||
'version' => '',
|
||||
|
||||
'app_name' => '', // 客户端名称
|
||||
'app_type' => '', // 客户端类型
|
||||
'app_version' => '', // 客户端版本
|
||||
];
|
||||
|
||||
if (empty($ua)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// 使用 Device-Detector 解析 UA
|
||||
$dd = new DeviceDetector($ua);
|
||||
|
||||
// 解析 UA 字符串
|
||||
$dd->parse();
|
||||
|
||||
// 获取客户端信息(浏览器)
|
||||
$clientInfo = $dd->getClient();
|
||||
if (!empty($clientInfo)) {
|
||||
$result['browser'] = $clientInfo['name'] ?? 'Unknown';
|
||||
$result['version'] = $clientInfo['version'] ?? '';
|
||||
}
|
||||
|
||||
// 获取操作系统信息
|
||||
$osInfo = $dd->getOs();
|
||||
if (!empty($osInfo)) {
|
||||
$result['os'] = trim(($osInfo['name'] ?? '') . ' ' . ($osInfo['version'] ?? ''));
|
||||
if (empty($result['os'])) {
|
||||
$result['os'] = 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match("/android_kuaifan_eeui/i", $ua)) {
|
||||
// Android 客户端
|
||||
$result['app_type'] = 'Android';
|
||||
if ($dd->getBrandName() && $dd->getModel()) {
|
||||
// 厂商+型号
|
||||
$result['app_name'] = $dd->getBrandName() . ' ' . $dd->getModel();
|
||||
} elseif ($dd->getBrandName()) {
|
||||
// 仅厂商
|
||||
$result['app_name'] = $dd->getBrandName();
|
||||
} elseif ($dd->isTablet()) {
|
||||
// 平板
|
||||
$result['app_name'] = 'Tablet';
|
||||
} elseif ($dd->isPhablet()) {
|
||||
// 平板
|
||||
$result['app_name'] = 'Phablet';
|
||||
}
|
||||
$result['app_version'] = self::getAfterVersion($ua, 'kuaifan_eeui/');
|
||||
} elseif (preg_match("/ios_kuaifan_eeui/i", $ua)) {
|
||||
// iOS 客户端
|
||||
$result['app_type'] = 'iOS';
|
||||
if (preg_match("/(macintosh|ipad)/i", $ua)) {
|
||||
// iPad
|
||||
$result['app_name'] = 'iPad';
|
||||
} elseif (preg_match("/iphone/i", $ua)) {
|
||||
// iPhone
|
||||
$result['app_name'] = 'iPhone';
|
||||
}
|
||||
$result['app_version'] = self::getAfterVersion($ua, 'kuaifan_eeui/');
|
||||
} elseif (preg_match("/dootask/i", $ua)) {
|
||||
// DooTask 客户端
|
||||
$result['app_type'] = $osInfo['name'];
|
||||
$result['app_version'] = self::getAfterVersion($ua, 'dootask/');
|
||||
} else {
|
||||
// 其他客户端
|
||||
$result['app_type'] = 'Web';
|
||||
$result['app_version'] = Base::getClientVersion();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 ua 的 find 之后的内容获取版本号
|
||||
* @param string $ua
|
||||
* @param string $find
|
||||
* @return string
|
||||
*/
|
||||
private static function getAfterVersion(string $ua, string $find): string
|
||||
{
|
||||
$findPattern = preg_quote($find, '/');
|
||||
if (preg_match("/{$findPattern}(.*?)(?:\s|$)/i", $ua, $matches)) {
|
||||
$appInfo = $matches[1];
|
||||
|
||||
// 从内容中提取版本号(寻找符合x.x.x格式的部分)
|
||||
if (preg_match("/(\d+\.\d+(?:\.\d+)*)/", $appInfo, $versionMatches)) {
|
||||
return $versionMatches[1];
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/** ****************************************************************************** */
|
||||
/** ****************************************************************************** */
|
||||
/** ****************************************************************************** */
|
||||
|
||||
/**
|
||||
* 检查用户是否存在
|
||||
* @return string|null
|
||||
*/
|
||||
public static function check(): ?string
|
||||
{
|
||||
$token = Doo::userToken();
|
||||
$userid = Doo::userId();
|
||||
|
||||
$hash = md5($token);
|
||||
if (Cache::has(self::ck($hash))) {
|
||||
return $hash;
|
||||
}
|
||||
|
||||
$row = self::whereHash($hash)->first();
|
||||
if ($row) {
|
||||
// 判断是否过期
|
||||
if ($row->expired_at && Carbon::parse($row->expired_at)->isPast()) {
|
||||
self::forget($row);
|
||||
return null;
|
||||
}
|
||||
// 更新缓存
|
||||
self::record();
|
||||
return $hash;
|
||||
}
|
||||
// 没有记录,尝试创建一个(防止升级后所有登录都失效,保证留一个可以保持登录) // todo 后期删除
|
||||
return AbstractModel::transaction(function () use ($hash, $userid) {
|
||||
if (self::whereUserid($userid)->withoutTrashed()->lockForUpdate()->exists()) {
|
||||
return null;
|
||||
}
|
||||
return self::record() ? $hash : null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录设备(添加、更新)
|
||||
* @param string|null $token
|
||||
* @return self|null
|
||||
*/
|
||||
public static function record(string $token = null): ?self
|
||||
{
|
||||
return AbstractModel::transaction(function () use ($token) {
|
||||
if (empty($token)) {
|
||||
$token = Doo::userToken();
|
||||
$userid = Doo::userId();
|
||||
$expiredAt = Doo::userExpiredAt();
|
||||
} else {
|
||||
$info = Doo::tokenDecode($token);
|
||||
$userid = $info['userid'] ?? 0;
|
||||
$expiredAt = $info['expired_at'];
|
||||
}
|
||||
|
||||
$hash = md5($token);
|
||||
$row = self::whereHash($hash)->lockForUpdate()->first();
|
||||
if (empty($row)) {
|
||||
// 生成一个新的设备记录
|
||||
$row = self::createInstance([
|
||||
'userid' => $userid,
|
||||
'hash' => $hash,
|
||||
]);
|
||||
if (!$row->save()) {
|
||||
return null;
|
||||
}
|
||||
// 删除多余的设备记录
|
||||
$currentDeviceCount = self::whereUserid($userid)->count();
|
||||
if ($currentDeviceCount > self::$deviceLimit) {
|
||||
$rows = self::whereUserid($userid)->orderBy('id')->take($currentDeviceCount - self::$deviceLimit)->get();
|
||||
foreach ($rows as $row) {
|
||||
UserDevice::forget($row);
|
||||
}
|
||||
}
|
||||
}
|
||||
$row->expired_at = $expiredAt;
|
||||
if (Request::hasHeader('version')) {
|
||||
$deviceInfo = array_merge(Base::json2array($row->detail), self::getDeviceInfo($_SERVER['HTTP_USER_AGENT'] ?? ''));
|
||||
$row->detail = Base::array2json($deviceInfo);
|
||||
}
|
||||
$row->save();
|
||||
|
||||
Cache::put(self::ck($hash), $row->userid, now()->addHour());
|
||||
return $row;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 忘记设备(删除)
|
||||
* @param UserDevice|string|int|null $token
|
||||
* - UserDevice 表示指定的设备对象
|
||||
* - string 表示指定的 token
|
||||
* - int 表示指定的数据ID
|
||||
* - null 表示当前登录的设备
|
||||
* @return void
|
||||
*/
|
||||
public static function forget(UserDevice|string|int $token = null): void
|
||||
{
|
||||
if ($token instanceof UserDevice) {
|
||||
$hash = $token->hash;
|
||||
$token->delete();
|
||||
} elseif (Base::isNumber($token)) {
|
||||
$row = self::find(intval($token));
|
||||
if ($row) {
|
||||
$hash = $row->hash;
|
||||
$row->delete();
|
||||
}
|
||||
} else {
|
||||
if ($token === null) {
|
||||
$token = Doo::userToken();
|
||||
}
|
||||
if ($token) {
|
||||
$hash = md5($token);
|
||||
self::whereHash($hash)->delete();
|
||||
}
|
||||
}
|
||||
if (isset($hash)) {
|
||||
Cache::forget(self::ck($hash));
|
||||
UmengAlias::whereDeviceHash($hash)->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ use Illuminate\Support\Facades\DB;
|
||||
* @property int $id
|
||||
* @property string|null $type 对话类型
|
||||
* @property string|null $group_type 聊天室类型
|
||||
* @property int|null $session_id 会话ID
|
||||
* @property string|null $name 对话名称
|
||||
* @property string $avatar 头像(群)
|
||||
* @property int|null $owner_id 群主用户ID
|
||||
@@ -48,6 +49,7 @@ use Illuminate\Support\Facades\DB;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereLinkId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereOwnerId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereSessionId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereTopMsgId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereTopUserid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereType($value)
|
||||
@@ -95,6 +97,32 @@ class WebSocketDialog extends AbstractModel
|
||||
->whereNull('users.disable_at');
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索对话
|
||||
* @param $userid
|
||||
* @param $key
|
||||
* @param $take
|
||||
* @return array
|
||||
*/
|
||||
public static function searchDialog($userid, $key, $take = 20)
|
||||
{
|
||||
return DB::table('web_socket_dialog_users as u')
|
||||
->select(['d.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
|
||||
->join('web_socket_dialogs as d', 'u.dialog_id', '=', 'd.id')
|
||||
->where('u.userid', $userid)
|
||||
->where(function ($query) use ($key) {
|
||||
$query->where('d.name', 'like', '%' . $key . '%');
|
||||
})
|
||||
->whereNull('d.deleted_at')
|
||||
->orderByDesc('u.top_at')
|
||||
->orderByDesc('u.last_at')
|
||||
->take($take)
|
||||
->get()
|
||||
->map(function($item) use ($userid) {
|
||||
return WebSocketDialog::synthesizeData($item, $userid);
|
||||
})
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对话列表
|
||||
@@ -265,7 +293,9 @@ class WebSocketDialog extends AbstractModel
|
||||
// 未读消息
|
||||
$data = array_merge($data, self::generateUnread($data['id'], $userid));
|
||||
// 对话人数
|
||||
$data['people'] = $data['people'] ?? WebSocketDialogUser::whereDialogId($data['id'])->count();
|
||||
if (!isset($data['people'])) {
|
||||
$data = array_merge($data, self::generatePeople($data['id']));
|
||||
}
|
||||
// 有待办
|
||||
$data['todo_num'] = $data['todo_num'] ?? WebSocketDialogMsgTodo::whereDialogId($data['id'])->whereUserid($userid)->whereDoneAt(null)->count();
|
||||
// 最后消息
|
||||
@@ -294,7 +324,8 @@ class WebSocketDialog extends AbstractModel
|
||||
$data['is_disable'] = $basic->isDisable(true);
|
||||
$data['quick_msgs'] = UserBot::quickMsgs($basic->email);
|
||||
} else {
|
||||
$data['name'] = 'non-existent';
|
||||
$data['name'] = UserDelete::userid2nickname($dialog_user->userid) ?: '[Delete]';
|
||||
$data['is_disable'] = 1;
|
||||
$data['dialog_delete'] = 1;
|
||||
}
|
||||
$data['dialog_user'] = $dialog_user;
|
||||
@@ -330,6 +361,9 @@ class WebSocketDialog extends AbstractModel
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (empty($data['pinyin'])) {
|
||||
$data['pinyin'] = Base::cn2pinyin($data['name']);
|
||||
}
|
||||
|
||||
// 已存在的消息类型
|
||||
if ($hasData === true) {
|
||||
@@ -398,6 +432,26 @@ class WebSocketDialog extends AbstractModel
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对话人数
|
||||
* @param $dialogId
|
||||
* @return array
|
||||
*/
|
||||
public static function generatePeople($dialogId)
|
||||
{
|
||||
$counts = WebSocketDialogUser::whereDialogId($dialogId)
|
||||
->groupBy('bot')
|
||||
->selectRaw('bot, COUNT(*) as count')
|
||||
->pluck('count', 'bot');
|
||||
$userCount = $counts->get(0, 0); // 非机器人数量
|
||||
$botCount = $counts->get(1, 0); // 机器人数量
|
||||
return [
|
||||
'people' => $userCount + $botCount,
|
||||
'people_user' => $userCount,
|
||||
'people_bot' => $botCount,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 加入聊天室
|
||||
* @param int|array $userid 加入的会员ID或会员ID组
|
||||
@@ -420,7 +474,11 @@ class WebSocketDialog extends AbstractModel
|
||||
WebSocketDialogUser::updateInsert([
|
||||
'dialog_id' => $this->id,
|
||||
'userid' => $value,
|
||||
], $updateData, [], $isInsert);
|
||||
], $updateData, function() use ($value, $updateData) {
|
||||
return array_merge($updateData, [
|
||||
'bot' => User::isBot($value) ? 1 : 0
|
||||
]);
|
||||
}, $isInsert);
|
||||
if ($isInsert) {
|
||||
WebSocketDialogMsg::sendMsg(null, $this->id, 'notice', [
|
||||
'notice' => User::userid2nickname($value) . " 已加入群组"
|
||||
@@ -429,10 +487,9 @@ class WebSocketDialog extends AbstractModel
|
||||
}
|
||||
}
|
||||
});
|
||||
$this->pushMsg("groupUpdate", [
|
||||
'id' => $this->id,
|
||||
'people' => WebSocketDialogUser::whereDialogId($this->id)->count()
|
||||
]);
|
||||
$data = WebSocketDialog::generatePeople($this->id);
|
||||
$data['id'] = $this->id;
|
||||
$this->pushMsg("groupUpdate", $data);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -489,10 +546,9 @@ class WebSocketDialog extends AbstractModel
|
||||
});
|
||||
});
|
||||
//
|
||||
$this->pushMsg("groupUpdate", [
|
||||
'id' => $this->id,
|
||||
'people' => WebSocketDialogUser::whereDialogId($this->id)->count()
|
||||
]);
|
||||
$data = WebSocketDialog::generatePeople($this->id);
|
||||
$data['id'] = $this->id;
|
||||
$this->pushMsg("groupUpdate", $data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -637,6 +693,18 @@ class WebSocketDialog extends AbstractModel
|
||||
Task::deliver($task);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否是单人对话
|
||||
* @return bool
|
||||
*/
|
||||
public function isSelfDialog()
|
||||
{
|
||||
if ($this->type !== 'user') {
|
||||
return false;
|
||||
}
|
||||
return WebSocketDialogUser::whereDialogId($this->id)->where('userid', '>', 0)->count() === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对话(同时检验对话身份)
|
||||
* @param $dialog_id
|
||||
@@ -645,6 +713,9 @@ class WebSocketDialog extends AbstractModel
|
||||
*/
|
||||
public static function checkDialog($dialog_id, $checkOwner = false)
|
||||
{
|
||||
if ($dialog_id <= 0) {
|
||||
throw new ApiException('参数错误');
|
||||
}
|
||||
$dialog = WebSocketDialog::find($dialog_id);
|
||||
if (empty($dialog)) {
|
||||
throw new ApiException('对话不存在或已被删除', ['dialog_id' => $dialog_id], -4003);
|
||||
@@ -658,18 +729,25 @@ class WebSocketDialog extends AbstractModel
|
||||
throw new ApiException('仅限群主操作');
|
||||
}
|
||||
//
|
||||
if ($dialog->group_type === 'task') {
|
||||
// 任务群对话校验是否在项目内
|
||||
$project_id = intval(ProjectTask::whereDialogId($dialog->id)->value('project_id'));
|
||||
if ($project_id > 0) {
|
||||
if (ProjectUser::whereProjectId($project_id)->whereUserid($userid)->exists()) {
|
||||
switch ($dialog->group_type) {
|
||||
case 'project':
|
||||
case 'task':
|
||||
// 项目群、任务群对话校验是否在项目内
|
||||
if ($dialog->group_type === 'project') {
|
||||
$projectId = intval(Project::whereDialogId($dialog->id)->value('id'));
|
||||
} else {
|
||||
$projectId = intval(ProjectTask::whereDialogId($dialog->id)->value('project_id'));
|
||||
}
|
||||
if ($projectId > 0 && ProjectUser::whereProjectId($projectId)->whereUserid($userid)->exists()) {
|
||||
return $dialog;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($dialog->group_type == 'okr') {
|
||||
return $dialog;
|
||||
break;
|
||||
|
||||
case 'okr':
|
||||
// OKR群对话不用校验
|
||||
return $dialog;
|
||||
}
|
||||
//
|
||||
if (!WebSocketDialogUser::whereDialogId($dialog->id)->whereUserid($userid)->exists()) {
|
||||
WebSocketDialogMsgRead::forceRead($dialog_id, $userid);
|
||||
throw new ApiException('不在成员列表内', ['dialog_id' => $dialog_id], -4003);
|
||||
@@ -741,6 +819,17 @@ class WebSocketDialog extends AbstractModel
|
||||
'dialog_id' => $dialog->id,
|
||||
'userid' => $receiver,
|
||||
])->save();
|
||||
//
|
||||
if ($user->isAiBot() || User::find($receiver)?->isAiBot()) {
|
||||
$session = WebSocketDialogSession::create([
|
||||
'dialog_id' => $dialog->id,
|
||||
'status' => 1,
|
||||
'title' => '',
|
||||
]);
|
||||
$session->save();
|
||||
$dialog->session_id = $session->id;
|
||||
$dialog->save();
|
||||
}
|
||||
return $dialog;
|
||||
});
|
||||
}
|
||||
@@ -816,13 +905,15 @@ class WebSocketDialog extends AbstractModel
|
||||
Base::makeDir(public_path($path));
|
||||
copy($filePath, public_path($path) . basename($filePath));
|
||||
} else {
|
||||
$setting = Base::setting("system");
|
||||
$data = Base::upload([
|
||||
"file" => $files,
|
||||
"type" => 'more',
|
||||
"path" => $path,
|
||||
"fileName" => $fileName,
|
||||
"quality" => true,
|
||||
"convertVideo" => true
|
||||
"convertVideo" => $setting['convert_video'] === 'open',
|
||||
"compressVideo" => $setting['compress_video'] === 'open',
|
||||
]);
|
||||
}
|
||||
//
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace App\Models;
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\WebSocketDialog|null $dialog
|
||||
* @property-read \App\Models\User|null $user
|
||||
* @property-read \App\Models\User $user
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
|
||||
@@ -18,6 +18,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @property int $id
|
||||
* @property int|null $dialog_id 对话ID
|
||||
* @property string|null $dialog_type 对话类型
|
||||
* @property int|null $session_id 会话ID
|
||||
* @property int|null $userid 发送会员ID
|
||||
* @property string|null $type 消息类型
|
||||
* @property string|null $mtype 消息类型(用于搜索)
|
||||
@@ -69,6 +70,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereReplyId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereReplyNum($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereSend($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereSessionId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereTag($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereTodo($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereType($value)
|
||||
@@ -303,7 +305,12 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
];
|
||||
//
|
||||
$dialog = WebSocketDialog::find($this->dialog_id);
|
||||
$dialog?->pushMsg('update', $resData);
|
||||
if ($dialog) {
|
||||
$dialog->pushMsg('update', $resData);
|
||||
WebSocketDialogUser::whereDialogId($dialog->id)->change([
|
||||
'updated_at' => Carbon::now()->toDateTimeString('millisecond'),
|
||||
]);
|
||||
}
|
||||
//
|
||||
return Base::retSuccess('success', $resData);
|
||||
}
|
||||
@@ -372,7 +379,6 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
'dialog_id' => $this->dialog_id,
|
||||
];
|
||||
$dialog = WebSocketDialog::find($this->dialog_id);
|
||||
$dialog->pushMsg('update', $upData);
|
||||
//
|
||||
$retData = [
|
||||
'add' => [],
|
||||
@@ -419,6 +425,7 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
}
|
||||
}
|
||||
//
|
||||
$dialog->pushMsg('update', $upData);
|
||||
return Base::retSuccess($this->todo ? '设置成功' : '取消成功', $retData);
|
||||
}
|
||||
|
||||
@@ -532,10 +539,6 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
*/
|
||||
public function withdrawMsg()
|
||||
{
|
||||
$send_dt = Carbon::parse($this->created_at)->addDay();
|
||||
if ($send_dt->lt(Carbon::now())) {
|
||||
throw new ApiException('已超过24小时,此消息不能撤回');
|
||||
}
|
||||
AbstractModel::transaction(function() {
|
||||
$deleteRead = WebSocketDialogMsgRead::whereMsgId($this->id)->whereNull('read_at')->delete(); // 未阅读记录不需要软删除,直接删除即可
|
||||
$this->delete();
|
||||
@@ -657,6 +660,10 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
$text = $msgData['text'] ?? '';
|
||||
if (!$text) return '';
|
||||
if ($msgData['type'] === 'md') {
|
||||
$text = preg_replace("/:::\s*reasoning[\s\S]*?:::/", "", $text);
|
||||
if (preg_match('/:::\s*reasoning\s+/', $text)) {
|
||||
return Doo::translate('思考中...');
|
||||
}
|
||||
$text = Base::markdown2html($text);
|
||||
$text = self::previewConvertTaskList($text);
|
||||
}
|
||||
@@ -756,9 +763,15 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
$key = '';
|
||||
switch ($this->type) {
|
||||
case 'text':
|
||||
if (!preg_match("/<span[^>]*?data-quick-key=([\"'])([^\"']+?)\\1[^>]*?>/is", $this->msg['text'])) {
|
||||
$key = strip_tags($this->msg['text']);
|
||||
if (preg_match("/<span[^>]*?data-quick-key=([\"'])([^\"']+?)\\1[^>]*?>/i", $this->msg['text'])) {
|
||||
break;
|
||||
}
|
||||
$key = $this->msg['text'];
|
||||
if ($this->msg['type'] === 'md') {
|
||||
$key = preg_replace("/:::\s*reasoning[\s\S]*?:::/", "", $key);
|
||||
$key = Base::markdown2html($key);
|
||||
}
|
||||
$key = strip_tags($key);
|
||||
break;
|
||||
|
||||
case 'vote':
|
||||
@@ -882,8 +895,16 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
$imageSaveLocal = Base::settingFind("system", "image_save_local");
|
||||
preg_match_all("/<img[^>]*?src=([\"'])(.*?(png|jpg|jpeg|webp|gif).*?)\\1[^>]*?>/is", $text, $matchs);
|
||||
foreach ($matchs[2] as $key => $str) {
|
||||
$parsed = parse_url($str);
|
||||
if (str_starts_with($parsed['path'], "/uploads/")) {
|
||||
$relativePath = ltrim($parsed['path'], "/");
|
||||
$relativePath = Base::thumbRestore($relativePath);
|
||||
if (file_exists(public_path($relativePath))) {
|
||||
$str = "{{RemoteURL}}{$relativePath}";
|
||||
}
|
||||
}
|
||||
if ($imageSaveLocal === 'close') {
|
||||
$imageSize = getimagesize($str);
|
||||
$imageSize = @getimagesize($str);
|
||||
if ($imageSize === false) {
|
||||
$imageSize = ["auto", "auto"];
|
||||
}
|
||||
@@ -918,7 +939,7 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
}
|
||||
}
|
||||
}
|
||||
// @成员、#任务、~文件
|
||||
// @成员、#任务、~文件、%报告
|
||||
preg_match_all("/<span\s+class=\"mention\"(.*?)>.*?<\/span>.*?<\/span>.*?<\/span>/s", $text, $matchs);
|
||||
foreach ($matchs[1] as $key => $str) {
|
||||
preg_match("/data-denotation-char=\"(.*?)\"/", $str, $matchChar);
|
||||
@@ -926,6 +947,7 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
preg_match("/data-value=\"(.*?)\"/s", $str, $matchValye);
|
||||
$keyId = $matchId[1];
|
||||
if ($matchChar[1] === "~") {
|
||||
// 文件特殊处理
|
||||
if (Base::isNumber($keyId)) {
|
||||
$file = File::permissionFind($keyId, User::auth());
|
||||
if ($file->type == 'folder') {
|
||||
@@ -941,6 +963,19 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
throw new ApiException('文件分享错误');
|
||||
}
|
||||
}
|
||||
} elseif ($matchChar[1] === "%") {
|
||||
// 报告特殊处理
|
||||
if (Base::isNumber($keyId)) {
|
||||
$reportLink = ReportLink::generateLink($keyId, User::userid());
|
||||
$keyId = $reportLink['code'];
|
||||
} else {
|
||||
preg_match("/\/single\/report\/detail\/(.*?)$/i", $keyId, $match);
|
||||
if ($match && strlen($match[1]) >= 8) {
|
||||
$keyId = $match[1];
|
||||
} else {
|
||||
throw new ApiException('报告分享错误');
|
||||
}
|
||||
}
|
||||
}
|
||||
$text = str_replace($matchs[0][$key], "[:{$matchChar[1]}:{$keyId}:{$matchValye[1]}:]", $text);
|
||||
}
|
||||
@@ -969,31 +1004,18 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
foreach ($matchs[0] as $key => $str) {
|
||||
$herf = $matchs[2][$key];
|
||||
$title = $matchs[3][$key] ?: $herf;
|
||||
preg_match("/\/single\/file\/(.*?)$/i", strip_tags($title), $match);
|
||||
if ($match && strlen($match[1]) >= 8) {
|
||||
$file = File::select(['files.id', 'files.name', 'files.ext'])->join('file_links as L', 'files.id', '=', 'L.file_id')->where('L.code', $match[1])->first();
|
||||
if ($file && $file->name) {
|
||||
$name = $file->ext ? "{$file->name}.{$file->ext}" : $file->name;
|
||||
$text = str_replace($str, "[:~:{$match[1]}:{$name}:]", $text);
|
||||
continue;
|
||||
}
|
||||
if (self::formatLink($str, strip_tags($title), $text)) {
|
||||
continue;
|
||||
}
|
||||
$herf = base64_encode($herf);
|
||||
$title = base64_encode($title);
|
||||
$text = str_replace($str, "[:LINK:{$herf}:{$title}:]", $text);
|
||||
}
|
||||
// 文件分享链接
|
||||
// 分享链接
|
||||
preg_match_all("/(https?:\/\/)((\w|=|\?|\.|\/|&|-|:|\+|%|;|#|@|,|!)+)/i", $text, $matchs);
|
||||
if ($matchs) {
|
||||
foreach ($matchs[0] as $str) {
|
||||
preg_match("/\/single\/file\/(.*?)$/i", $str, $match);
|
||||
if ($match && strlen($match[1]) >= 8) {
|
||||
$file = File::select(['files.id', 'files.name', 'files.ext'])->join('file_links as L', 'files.id', '=', 'L.file_id')->where('L.code', $match[1])->first();
|
||||
if ($file && $file->name) {
|
||||
$name = $file->ext ? "{$file->name}.{$file->ext}" : $file->name;
|
||||
$text = str_replace($str, "[:~:{$match[1]}:{$name}:]", $text);
|
||||
}
|
||||
}
|
||||
self::formatLink($str, $str, $text);
|
||||
}
|
||||
}
|
||||
// 过滤标签
|
||||
@@ -1023,10 +1045,41 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
$text = preg_replace("/\[:@:(.*?):(.*?):\]/i", "<span class=\"mention user\" data-id=\"$1\">@$2</span>", $text);
|
||||
$text = preg_replace("/\[:#:(.*?):(.*?):\]/is", "<span class=\"mention task\" data-id=\"$1\">#$2</span>", $text);
|
||||
$text = preg_replace("/\[:~:(.*?):(.*?):\]/i", "<a class=\"mention file\" href=\"{{RemoteURL}}single/file/$1\" target=\"_blank\">~$2</a>", $text);
|
||||
$text = preg_replace("/\[:%:(.*?):(.*?):\]/i", "<a class=\"mention report\" href=\"{{RemoteURL}}single/report/detail/$1\" target=\"_blank\">%$2</a>", $text);
|
||||
$text = preg_replace("/\[:QUICK:(.*?):(.*?):\]/i", "<span data-quick-key=\"$1\">$2</span>", $text);
|
||||
return preg_replace("/^(<p><\/p>)+|(<p><\/p>)+$/i", "", $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 链接转换处理
|
||||
* @param $search
|
||||
* @param $subject
|
||||
* @param $content
|
||||
* @return bool
|
||||
*/
|
||||
public static function formatLink($search, $subject, &$content)
|
||||
{
|
||||
$ret = false;
|
||||
preg_match("/\/single\/file\/(.*?)$/i", $subject, $match);
|
||||
if ($match && strlen($match[1]) >= 8) {
|
||||
$file = File::select(['files.id', 'files.name', 'files.ext'])->join('file_links as L', 'files.id', '=', 'L.file_id')->where('L.code', $match[1])->first();
|
||||
if ($file && $file->name) {
|
||||
$name = $file->ext ? "{$file->name}.{$file->ext}" : $file->name;
|
||||
$content = str_replace($search, "[:~:{$match[1]}:{$name}:]", $content);
|
||||
$ret = true;
|
||||
}
|
||||
}
|
||||
preg_match("/\/single\/report\/detail\/(.*?)$/i", $subject, $match);
|
||||
if ($match && strlen($match[1]) >= 8) {
|
||||
$report = Report::select(['reports.id', 'reports.title'])->join('report_links as L', 'reports.id', '=', 'L.rid')->where('L.code', $match[1])->first();
|
||||
if ($report && $report->title) {
|
||||
$content = str_replace($search, "[:%:{$match[1]}:{$report->title}:]", $content);
|
||||
$ret = true;
|
||||
}
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息、修改消息
|
||||
* @param string $action 动作
|
||||
@@ -1136,6 +1189,7 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
}
|
||||
//
|
||||
$updateData = [
|
||||
'type' => $type,
|
||||
'mtype' => $mtype,
|
||||
'link' => $link,
|
||||
'msg' => array_merge($oldMsg, $msg),
|
||||
@@ -1178,6 +1232,7 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
$dialogMsg = self::createInstance([
|
||||
'dialog_id' => $dialog_id,
|
||||
'dialog_type' => $dialog->type,
|
||||
'session_id' => $dialog->session_id,
|
||||
'reply_id' => $reply_id,
|
||||
'forward_id' => $forward_id,
|
||||
'userid' => $sender,
|
||||
@@ -1192,6 +1247,8 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
$dialogMsg->send = 1;
|
||||
$dialogMsg->generateKeyAndSave($search_key);
|
||||
//
|
||||
WebSocketDialogSession::updateTitle($dialogMsg->session_id, $dialogMsg);
|
||||
//
|
||||
if ($dialogMsg->type === 'meeting') {
|
||||
MeetingMsg::createInstance([
|
||||
'meetingid' => $dialogMsg->msg['meetingid'],
|
||||
@@ -1223,6 +1280,55 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量发送消息
|
||||
* @param User $user 发送的会员
|
||||
* @param array $userids 接收的会员ID
|
||||
* @param array $dialogids 接收的会话ID
|
||||
* @param string $msgText 发送的消息
|
||||
* @return array
|
||||
*/
|
||||
public static function sendMsgBatch($user, $userids, $dialogids, $msgText)
|
||||
{
|
||||
return AbstractModel::transaction(function() use ($user, $userids, $dialogids, $msgText) {
|
||||
$msgs = [];
|
||||
$already = [];
|
||||
if ($dialogids) {
|
||||
if (!is_array($dialogids)) {
|
||||
$dialogids = [$dialogids];
|
||||
}
|
||||
foreach ($dialogids as $dialogid) {
|
||||
$res = WebSocketDialogMsg::sendMsg(null, $dialogid, 'text', ['text' => $msgText], $user->userid);
|
||||
if (Base::isSuccess($res)) {
|
||||
$msgs[] = $res['data'];
|
||||
$already[] = $dialogid;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($userids) {
|
||||
if (!is_array($userids)) {
|
||||
$userids = [$userids];
|
||||
}
|
||||
foreach ($userids as $userid) {
|
||||
if (!User::whereUserid($userid)->exists()) {
|
||||
continue;
|
||||
}
|
||||
$dialog = WebSocketDialog::checkUserDialog($user, $userid);
|
||||
if ($dialog && !in_array($dialog->id, $already)) {
|
||||
$res = WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $msgText], $user->userid);
|
||||
if (Base::isSuccess($res)) {
|
||||
$msgs[] = $res['data'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Base::retSuccess('发送成功', [
|
||||
'msgs' => $msgs
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 将被@的人加入群
|
||||
* @param WebSocketDialog $dialog 对话
|
||||
|
||||
99
app/Models/WebSocketDialogSession.php
Normal file
99
app/Models/WebSocketDialogSession.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Module\Base;
|
||||
use App\Module\Extranet;
|
||||
use Swoole\Coroutine;
|
||||
use Cache;
|
||||
|
||||
/**
|
||||
* App\Models\WebSocketDialogSession
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $dialog_id 对话ID
|
||||
* @property string $title 会话标题
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\WebSocketDialog|null $dialog
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession whereDialogId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession whereTitle($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class WebSocketDialogSession extends AbstractModel
|
||||
{
|
||||
/**
|
||||
* 可以批量赋值的属性
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'dialog_id',
|
||||
'userid',
|
||||
'title',
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取关联的对话
|
||||
*/
|
||||
public function dialog()
|
||||
{
|
||||
return $this->belongsTo(WebSocketDialog::class, 'dialog_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $sessionId
|
||||
* @param WebSocketDialogMsg $dialogMsg
|
||||
* @return void
|
||||
*/
|
||||
public static function updateTitle($sessionId, $dialogMsg)
|
||||
{
|
||||
if (!$sessionId) {
|
||||
return;
|
||||
}
|
||||
if ($dialogMsg->type != 'text') {
|
||||
return;
|
||||
}
|
||||
$cacheKey = 'dialog_session_title_' . $sessionId;
|
||||
if (Cache::has($cacheKey)) {
|
||||
return;
|
||||
}
|
||||
$originalTitle = $dialogMsg->key ?: $dialogMsg->msg['text'] ?: 'Untitled';
|
||||
$title = Base::cutStr($originalTitle, 100);
|
||||
if ($title == '...') {
|
||||
return;
|
||||
}
|
||||
$session = self::whereId($sessionId)->first();
|
||||
if (!$session) {
|
||||
return;
|
||||
}
|
||||
$session->title = $title;
|
||||
$session->save();
|
||||
Cache::forever($cacheKey, true);
|
||||
// 通过AI接口更新对话标题
|
||||
go(function () use ($session, $title, $originalTitle) {
|
||||
Coroutine::sleep(0.1);
|
||||
$res = Extranet::openAIGenerateTitle($originalTitle);
|
||||
if (Base::isError($res)) {
|
||||
return;
|
||||
}
|
||||
$newTitle = $res['data'];
|
||||
if ($newTitle && $newTitle != $title) {
|
||||
$session->title = Base::cutStr($newTitle, 100);
|
||||
$session->save();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,13 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
||||
/**
|
||||
* App\Models\WebSocketDialogUser
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $dialog_id 对话ID
|
||||
* @property int|null $userid 会员ID
|
||||
* @property int|null $bot 是否机器人
|
||||
* @property \Illuminate\Support\Carbon|null $top_at 置顶时间
|
||||
* @property \Illuminate\Support\Carbon|null $last_at 最后消息时间
|
||||
* @property int|null $mark_unread 是否标记为未读:0否,1是
|
||||
@@ -30,6 +29,7 @@ use Carbon\Carbon;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereBot($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereColor($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereDialogId($value)
|
||||
|
||||
@@ -9,11 +9,12 @@ use App\Services\RequestContext;
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
use League\CommonMark\Exception\CommonMarkException;
|
||||
use League\HTMLToMarkdown\HtmlConverter;
|
||||
use Overtrue\Pinyin\Pinyin;
|
||||
use Redirect;
|
||||
use Request;
|
||||
use Storage;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Symfony\Component\HttpFoundation\File\Exception\FileException;
|
||||
use Symfony\Component\HttpFoundation\File\File;
|
||||
use Validator;
|
||||
@@ -801,16 +802,16 @@ class Base
|
||||
str_starts_with(str_replace(' ', '', $str), "data:image/")
|
||||
) {
|
||||
return $str;
|
||||
} else {
|
||||
if (RequestContext::has('fill_url_remote_url')) {
|
||||
return "{{RemoteURL}}" . $str;
|
||||
}
|
||||
try {
|
||||
return url($str);
|
||||
} catch (\Throwable) {
|
||||
return self::getSchemeAndHost() . "/" . $str;
|
||||
}
|
||||
}
|
||||
if (RequestContext::has('fill_url_remote_url')) {
|
||||
return "{{RemoteURL}}" . $str;
|
||||
}
|
||||
try {
|
||||
$fillUrl = url($str);
|
||||
} catch (\Throwable) {
|
||||
$fillUrl = self::getSchemeAndHost() . "/" . $str;
|
||||
}
|
||||
return RequestContext::replaceBaseUrl($fillUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1294,7 +1295,7 @@ class Base
|
||||
* 获取或设置
|
||||
* @param $setname // 配置名称
|
||||
* @param bool $array // 保存内容
|
||||
* @param false $isUpdate // 保存内容为更新模式,默认否
|
||||
* @param bool $isUpdate // 保存内容为更新模式,默认否
|
||||
* @return array
|
||||
*/
|
||||
public static function setting($setname, $array = false, $isUpdate = false)
|
||||
@@ -1475,14 +1476,36 @@ class Base
|
||||
public static function forumHourDay($hour)
|
||||
{
|
||||
$hour = intval($hour);
|
||||
if ($hour > 24) {
|
||||
if ($hour >= 24) {
|
||||
$day = floor($hour / 24);
|
||||
$hour -= $day * 24;
|
||||
return $day . '天' . $hour . '小时';
|
||||
if ($hour > 0) {
|
||||
return $day . '天' . $hour . '小时';
|
||||
}
|
||||
return $day . '天';
|
||||
}
|
||||
return $hour . '小时';
|
||||
}
|
||||
|
||||
/**
|
||||
* 分钟转天/小时/分钟
|
||||
* @param $minute
|
||||
* @return string
|
||||
*/
|
||||
public static function forumMinuteDay($minute)
|
||||
{
|
||||
$minute = intval($minute);
|
||||
if ($minute >= 60) {
|
||||
$hour = floor($minute / 60);
|
||||
$minute -= $hour * 60;
|
||||
if ($minute > 0) {
|
||||
return Base::forumHourDay($hour) . $minute . '分钟';
|
||||
}
|
||||
return Base::forumHourDay($hour);
|
||||
}
|
||||
return $minute . '分钟';
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Carbon对象
|
||||
* @param $var
|
||||
@@ -1849,18 +1872,18 @@ class Base
|
||||
if (!in_array($extension, ['mp3', 'wav'])) {
|
||||
return Base::retError('语音格式错误');
|
||||
}
|
||||
$fileName = 'record_' . md5($base64) . '.' . $extension;
|
||||
$saveName = 'record_' . md5($base64) . '.' . $extension;
|
||||
$fileDir = $param['path'];
|
||||
$filePath = public_path($fileDir);
|
||||
Base::makeDir($filePath);
|
||||
if (file_put_contents($filePath . $fileName, base64_decode(str_replace($res[1], '', $base64)))) {
|
||||
$fileSize = filesize($filePath . $fileName);
|
||||
if (file_put_contents($filePath . $saveName, base64_decode(str_replace($res[1], '', $base64)))) {
|
||||
$fileSize = filesize($filePath . $saveName);
|
||||
$array = [
|
||||
"name" => $fileName, //原文件名
|
||||
"name" => $saveName, //原文件名
|
||||
"size" => Base::twoFloat($fileSize / 1024, true), //大小KB
|
||||
"file" => $filePath . $fileName, //文件的完整路径 "D:\www....KzZ.jpg"
|
||||
"path" => $fileDir . $fileName, //相对路径 "uploads/pic....KzZ.jpg"
|
||||
"url" => Base::fillUrl($fileDir . $fileName), //完整的URL "https://.....hhsKzZ.jpg"
|
||||
"file" => $filePath . $saveName, //文件的完整路径 "D:\www....KzZ.jpg"
|
||||
"path" => $fileDir . $saveName, //相对路径 "uploads/pic....KzZ.jpg"
|
||||
"url" => Base::fillUrl($fileDir . $saveName), //完整的URL "https://.....hhsKzZ.jpg"
|
||||
"ext" => $extension, //文件后缀名
|
||||
];
|
||||
return Base::retSuccess('success', $array);
|
||||
@@ -1871,8 +1894,23 @@ class Base
|
||||
|
||||
/**
|
||||
* image64图片保存
|
||||
* @param array $param [ image64=带前缀的base64, path=>文件路径, fileName=>文件名称, scale=>[压缩原图宽,高, 压缩方式], autoThumb=>false不要自动生成缩略图, 'quality'=>压缩图片质量(默认:0不压缩) ]
|
||||
* @return array [name=>文件名, size=>文件大小(单位KB),file=>绝对地址, path=>相对地址, url=>全路径地址, ext=>文件后缀名]
|
||||
* @param array $param [
|
||||
image64=带前缀的base64,
|
||||
path=>文件路径,
|
||||
fileName=>文件名称,
|
||||
saveName=>保存文件名称,
|
||||
scale=>[压缩原图宽,高, 压缩方式],
|
||||
autoThumb=>false不要自动生成缩略图,
|
||||
quality=>压缩图片质量(默认:0不压缩)
|
||||
]
|
||||
* @return array [
|
||||
name=>文件名,
|
||||
size=>文件大小(单位KB),
|
||||
file=>绝对地址,
|
||||
path=>相对地址,
|
||||
url=>全路径地址,
|
||||
ext=>文件后缀名
|
||||
]
|
||||
*/
|
||||
public static function image64save($param)
|
||||
{
|
||||
@@ -1883,8 +1921,8 @@ class Base
|
||||
return Base::retError('图片格式错误');
|
||||
}
|
||||
$scaleName = "";
|
||||
if ($param['fileName']) {
|
||||
$fileName = basename($param['fileName']);
|
||||
if ($param['saveName']) {
|
||||
$saveName = basename($param['saveName']);
|
||||
} else {
|
||||
if ($param['scale'] && is_array($param['scale'])) {
|
||||
list($width, $height) = $param['scale'];
|
||||
@@ -1895,21 +1933,21 @@ class Base
|
||||
}
|
||||
}
|
||||
}
|
||||
$fileName = 'paste_' . md5($imgBase64) . '.' . $extension;
|
||||
$saveName = 'paste_' . md5($imgBase64) . '.' . $extension;
|
||||
$scaleName = md5_file($imgBase64) . $scaleName . '.' . $extension;
|
||||
}
|
||||
$fileDir = $param['path'];
|
||||
$filePath = public_path($fileDir);
|
||||
$fileFullPath = $filePath . $fileName;
|
||||
$fileFullPath = $filePath . $saveName;
|
||||
Base::makeDir($filePath);
|
||||
if (file_put_contents($fileFullPath, base64_decode(str_replace($res[1], '', $imgBase64)))) {
|
||||
$fileSize = filesize($fileFullPath);
|
||||
$array = [
|
||||
"name" => $fileName, //原文件名
|
||||
"name" => $param['fileName'] ?: $saveName, //原文件名
|
||||
"size" => Base::twoFloat($fileSize / 1024, true), //大小KB
|
||||
"file" => $fileFullPath, //文件的完整路径 "D:\www....KzZ.jpg"
|
||||
"path" => $fileDir . $fileName, //相对路径 "uploads/pic....KzZ.jpg"
|
||||
"url" => Base::fillUrl($fileDir . $fileName), //完整的URL "https://.....hhsKzZ.jpg"
|
||||
"path" => $fileDir . $saveName, //相对路径 "uploads/pic....KzZ.jpg"
|
||||
"url" => Base::fillUrl($fileDir . $saveName), //完整的URL "https://.....hhsKzZ.jpg"
|
||||
"thumb" => '', //缩略图(预览图) "https://.....hhsKzZ.jpg_thumb.jpg"
|
||||
"width" => -1, //图片宽度
|
||||
"height" => -1, //图片高度
|
||||
@@ -1940,10 +1978,10 @@ class Base
|
||||
// 重命名
|
||||
if ($scaleName) {
|
||||
$scaleName = str_replace(['{WIDTH}', '{HEIGHT}'], [$array['width'], $array['height']], $scaleName);
|
||||
if (rename($array['file'], Base::rightDelete($array['file'], $fileName) . $scaleName)) {
|
||||
$array['file'] = Base::rightDelete($array['file'], $fileName) . $scaleName;
|
||||
$array['path'] = Base::rightDelete($array['path'], $fileName) . $scaleName;
|
||||
$array['url'] = Base::rightDelete($array['url'], $fileName) . $scaleName;
|
||||
if (rename($array['file'], Base::rightDelete($array['file'], $saveName) . $scaleName)) {
|
||||
$array['file'] = Base::rightDelete($array['file'], $saveName) . $scaleName;
|
||||
$array['path'] = Base::rightDelete($array['path'], $saveName) . $scaleName;
|
||||
$array['url'] = Base::rightDelete($array['url'], $saveName) . $scaleName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1977,12 +2015,14 @@ class Base
|
||||
file=>Request::file,
|
||||
path=>文件路径,
|
||||
fileName=>文件名称,
|
||||
saveName=>保存文件名称,
|
||||
scale=>[压缩原图宽,高, 压缩方式],
|
||||
size=>限制大小KB,
|
||||
autoThumb=>false不要自动生成缩略图,
|
||||
chmod=>权限(默认0644),
|
||||
quality=>压缩图片质量(默认:0不压缩),
|
||||
convertVideo=>转换视频格式(默认false) ,
|
||||
compressVideo=>压缩视频(默认false,如果转换就不压缩) ,
|
||||
]
|
||||
* @return array [
|
||||
name=>原文件名,
|
||||
@@ -2068,10 +2108,10 @@ class Base
|
||||
}
|
||||
}
|
||||
$scaleName = "";
|
||||
if ($param['fileName'] === true) {
|
||||
$fileName = $file->getClientOriginalName();
|
||||
} elseif ($param['fileName']) {
|
||||
$fileName = basename($param['fileName']);
|
||||
if ($param['saveName'] === true) {
|
||||
$saveName = $file->getClientOriginalName();
|
||||
} elseif ($param['saveName']) {
|
||||
$saveName = basename($param['saveName']);
|
||||
} else {
|
||||
if ($param['scale'] && is_array($param['scale'])) {
|
||||
list($width, $height) = $param['scale'];
|
||||
@@ -2082,19 +2122,19 @@ class Base
|
||||
}
|
||||
}
|
||||
}
|
||||
$fileName = md5_file($file);
|
||||
$saveName = md5_file($file);
|
||||
$scaleName = md5_file($file) . $scaleName;
|
||||
if ($extension) {
|
||||
$fileName = $fileName . '.' . $extension;
|
||||
$saveName = $saveName . '.' . $extension;
|
||||
$scaleName = $scaleName . '.' . $extension;
|
||||
}
|
||||
}
|
||||
//
|
||||
$file->move(public_path($param['path']), $fileName);
|
||||
$file->move(public_path($param['path']), $saveName);
|
||||
//
|
||||
$path = $param['path'] . $fileName;
|
||||
$path = $param['path'] . $saveName;
|
||||
$array = [
|
||||
"name" => $file->getClientOriginalName(), //原文件名
|
||||
"name" => $param['fileName'] ?: $file->getClientOriginalName(), //原文件名
|
||||
"size" => Base::twoFloat($fileSize / 1024, true), //大小KB
|
||||
"file" => public_path($path), //文件的完整路径 "D:\www....KzZ.jpg"
|
||||
"path" => $path, //相对路径 "uploads/pic....KzZ.jpg"
|
||||
@@ -2140,6 +2180,7 @@ class Base
|
||||
}
|
||||
@shell_exec($command);
|
||||
if (file_exists($output) && filesize($output) > 0) {
|
||||
// 压缩后的文件正常
|
||||
@unlink($array['file']);
|
||||
$array = array_merge($array, [
|
||||
"name" => Base::rightReplace($array['name'], ".{$array['ext']}", '.mp4'),
|
||||
@@ -2150,6 +2191,27 @@ class Base
|
||||
"ext" => 'mp4',
|
||||
]);
|
||||
}
|
||||
$param['compressVideo'] = false; // 如果转换就不压缩
|
||||
}
|
||||
if ($param['compressVideo'] && $array['ext'] == 'mp4') {
|
||||
// 压缩视频
|
||||
$output = $array['file'] . '_compress';
|
||||
$command = sprintf("ffmpeg -y -i %s -c:v libx264 -crf 28 -preset medium -c:a aac -b:a 96k %s 2>&1", escapeshellarg($array['file']), escapeshellarg($output));
|
||||
@shell_exec($command);
|
||||
if (file_exists($output) && filesize($output) > 0) {
|
||||
// 压缩后的文件正常
|
||||
if (filesize($output) < filesize($array['file'])) {
|
||||
// 小于原文件
|
||||
@unlink($array['file']);
|
||||
$array = array_merge($array, [
|
||||
"size" => Base::twoFloat(filesize($output) / 1024, true),
|
||||
"file" => $output,
|
||||
]);
|
||||
} else {
|
||||
// 大于原文件
|
||||
@unlink($output);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (in_array($array['ext'], ['mov', 'webm', 'mp4'])) {
|
||||
// 视频尺寸
|
||||
@@ -2184,10 +2246,10 @@ class Base
|
||||
// 重命名
|
||||
if ($scaleName) {
|
||||
$scaleName = str_replace(['{WIDTH}', '{HEIGHT}'], [$array['width'], $array['height']], $scaleName);
|
||||
if (rename($array['file'], Base::rightDelete($array['file'], $fileName) . $scaleName)) {
|
||||
$array['file'] = Base::rightDelete($array['file'], $fileName) . $scaleName;
|
||||
$array['path'] = Base::rightDelete($array['path'], $fileName) . $scaleName;
|
||||
$array['url'] = Base::rightDelete($array['url'], $fileName) . $scaleName;
|
||||
if (rename($array['file'], Base::rightDelete($array['file'], $saveName) . $scaleName)) {
|
||||
$array['file'] = Base::rightDelete($array['file'], $saveName) . $scaleName;
|
||||
$array['path'] = Base::rightDelete($array['path'], $saveName) . $scaleName;
|
||||
$array['url'] = Base::rightDelete($array['url'], $saveName) . $scaleName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2758,12 +2820,12 @@ class Base
|
||||
}
|
||||
|
||||
/**
|
||||
* BinaryFileResponse 下载文件
|
||||
* DownloadFileResponse 下载文件
|
||||
* @param File|\SplFileInfo|string $file 文件对象或路径
|
||||
* @param string|null $name 下载文件名
|
||||
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
|
||||
* @return StreamedResponse
|
||||
*/
|
||||
public static function BinaryFileResponse($file, $name = null)
|
||||
public static function DownloadFileResponse($file, $name = null)
|
||||
{
|
||||
try {
|
||||
// 处理文件对象
|
||||
@@ -2780,6 +2842,12 @@ class Base
|
||||
throw new FileException('File must be readable and exist.');
|
||||
}
|
||||
|
||||
// 获取文件信息
|
||||
$size = $file->getSize();
|
||||
if ($size === false || $size < 0) {
|
||||
throw new FileException('Unable to determine file size.');
|
||||
}
|
||||
|
||||
// 处理文件名
|
||||
if (empty($name)) {
|
||||
$name = basename($file->getPathname());
|
||||
@@ -2791,34 +2859,98 @@ class Base
|
||||
$name = Base::cutStr($name, 180);
|
||||
$name = str_replace(['"', '<', '>', '|', '/', '\\', '?', ':'], '', $name);
|
||||
|
||||
// IE 浏览器特殊处理
|
||||
$encodedName = $name;
|
||||
if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/MSIE|Internet Explorer|Trident/i", $_SERVER['HTTP_USER_AGENT'])) {
|
||||
$encodedName = rawurlencode($name);
|
||||
// 获取MIME类型
|
||||
$mimeType = $file->getMimeType();
|
||||
if (empty($mimeType)) {
|
||||
$mimeType = 'application/octet-stream';
|
||||
}
|
||||
|
||||
// 创建响应对象
|
||||
return new \Symfony\Component\HttpFoundation\BinaryFileResponse($file->getPathname(), 200, [
|
||||
'Content-Type' => $file->getMimeType() ?: 'application/octet-stream',
|
||||
// 处理 Range 请求
|
||||
$start = 0;
|
||||
$end = $size - 1;
|
||||
$length = $size;
|
||||
$isRangeRequest = false;
|
||||
|
||||
if (isset($_SERVER['HTTP_RANGE'])) {
|
||||
$range = str_replace('bytes=', '', $_SERVER['HTTP_RANGE']);
|
||||
if (preg_match('/^(\d+)-(\d*)$/', $range, $matches)) {
|
||||
$start = intval($matches[1]);
|
||||
$end = !empty($matches[2]) ? intval($matches[2]) : $size - 1;
|
||||
|
||||
// 验证范围的有效性
|
||||
if ($start >= 0 && $end < $size && $start <= $end) {
|
||||
$length = $end - $start + 1;
|
||||
$isRangeRequest = true;
|
||||
} else {
|
||||
$start = 0;
|
||||
$end = $size - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置基本响应头
|
||||
$headers = [
|
||||
'Content-Type' => $mimeType,
|
||||
'Content-Disposition' => sprintf(
|
||||
'attachment; filename="%s"; filename*=UTF-8\'\'%s',
|
||||
$encodedName,
|
||||
$name,
|
||||
rawurlencode($name)
|
||||
),
|
||||
// 添加缓存控制和安全相关的头
|
||||
'Cache-Control' => 'private, no-transform, no-store, must-revalidate',
|
||||
'Pragma' => 'public',
|
||||
'Expires' => '0',
|
||||
'Accept-Ranges' => 'bytes', // 支持断点续传
|
||||
'X-Content-Type-Options' => 'nosniff', // 安全相关
|
||||
]);
|
||||
'Accept-Ranges' => 'bytes',
|
||||
'Cache-Control' => 'private, no-transform, no-store, must-revalidate, max-age=0',
|
||||
'Content-Length' => $length,
|
||||
'Last-Modified' => gmdate('D, d M Y H:i:s', $file->getMTime()) . ' GMT',
|
||||
'ETag' => sprintf('"%s"', md5_file($file->getPathname()))
|
||||
];
|
||||
|
||||
if ($isRangeRequest) {
|
||||
$headers['Content-Range'] = "bytes {$start}-{$end}/{$size}";
|
||||
$statusCode = 206;
|
||||
} else {
|
||||
$statusCode = 200;
|
||||
}
|
||||
|
||||
// 创建流式响应
|
||||
return new StreamedResponse(
|
||||
function () use ($file, $start, $length) {
|
||||
$handle = fopen($file->getPathname(), 'rb');
|
||||
if ($handle === false) {
|
||||
throw new FileException('Cannot open file for reading');
|
||||
}
|
||||
|
||||
if (fseek($handle, $start) === -1) {
|
||||
fclose($handle);
|
||||
throw new FileException('Cannot seek to position ' . $start);
|
||||
}
|
||||
|
||||
$remaining = $length;
|
||||
$bufferSize = 8192; // 8KB chunks
|
||||
|
||||
while ($remaining > 0 && !feof($handle)) {
|
||||
$readSize = min($bufferSize, $remaining);
|
||||
$buffer = fread($handle, $readSize);
|
||||
if ($buffer === false) {
|
||||
break;
|
||||
}
|
||||
echo $buffer;
|
||||
flush();
|
||||
$remaining -= strlen($buffer);
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
},
|
||||
$statusCode,
|
||||
$headers
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('File download failed', [
|
||||
'error' => $e->getMessage(),
|
||||
'file' => $file->getPathname() ?? null,
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'file' => $file ?? null,
|
||||
'name' => $name ?? null,
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null, // 添加更多调试信息
|
||||
'ip' => request()->ip()
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null,
|
||||
'ip' => request()->ip(),
|
||||
'range' => $_SERVER['HTTP_RANGE'] ?? null
|
||||
]);
|
||||
abort(403, 'File download failed');
|
||||
}
|
||||
@@ -2884,11 +3016,26 @@ class Base
|
||||
*/
|
||||
public static function markdown2html($markdown)
|
||||
{
|
||||
$converter = new CommonMarkConverter();
|
||||
try {
|
||||
$converter = new CommonMarkConverter();
|
||||
return $converter->convert($markdown);
|
||||
} catch (CommonMarkException $e) {
|
||||
} catch (\League\CommonMark\Exception\CommonMarkException $e) {
|
||||
return $markdown;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* html 转 MD(markdown)
|
||||
* @param $html
|
||||
* @return mixed|string
|
||||
*/
|
||||
public static function html2markdown($html)
|
||||
{
|
||||
try {
|
||||
$converter = new HtmlConverter();
|
||||
return $converter->convert($html);
|
||||
} catch (\Exception) {
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,12 +181,12 @@ class Doo
|
||||
|
||||
/**
|
||||
* token过期时间(来自请求的token)
|
||||
* @return string
|
||||
* @return string|null
|
||||
*/
|
||||
public static function userExpiredAt(): string
|
||||
public static function userExpiredAt(): ?string
|
||||
{
|
||||
$expiredAt = self::string(self::doo()->userExpiredAt());
|
||||
return $expiredAt === 'forever' ? '' : $expiredAt;
|
||||
return $expiredAt === 'forever' ? null : $expiredAt;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -263,7 +263,9 @@ class Doo
|
||||
*/
|
||||
public static function tokenDecode($token): array
|
||||
{
|
||||
return Base::json2array(self::string(self::doo()->tokenDecode($token)));
|
||||
$array = Base::json2array(self::string(self::doo()->tokenDecode($token)));
|
||||
$array['expired_at'] = $array['expired_at'] === 'forever' ? null : $array['expired_at'];
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,9 +15,10 @@ class Extranet
|
||||
/**
|
||||
* 通过 openAI 语音转文字
|
||||
* @param string $filePath
|
||||
* @param array $extParams
|
||||
* @return array
|
||||
*/
|
||||
public static function openAItranscriptions($filePath)
|
||||
public static function openAItranscriptions($filePath, $extParams = [])
|
||||
{
|
||||
if (!file_exists($filePath)) {
|
||||
return Base::retError("语音文件不存在");
|
||||
@@ -27,32 +28,34 @@ class Extranet
|
||||
if ($systemSetting['voice2text'] !== 'open' || empty($aibotSetting['openai_key'])) {
|
||||
return Base::retError("语音转文字功能未开启");
|
||||
}
|
||||
//
|
||||
$extra = [
|
||||
'Content-Type' => 'multipart/form-data',
|
||||
'Authorization' => 'Bearer ' . $aibotSetting['openai_key'],
|
||||
];
|
||||
if ($aibotSetting['openai_agency']) {
|
||||
$extra['CURLOPT_PROXY'] = $aibotSetting['openai_agency'];
|
||||
if (str_contains($aibotSetting['openai_agency'], 'socks')) {
|
||||
$extra['CURLOPT_PROXYTYPE'] = CURLPROXY_SOCKS5;
|
||||
} else {
|
||||
$extra['CURLOPT_PROXYTYPE'] = CURLPROXY_HTTP;
|
||||
}
|
||||
$extra['CURLOPT_PROXYTYPE'] = str_contains($aibotSetting['openai_agency'], 'socks') ? CURLPROXY_SOCKS5 : CURLPROXY_HTTP;
|
||||
}
|
||||
$res = Ihttp::ihttp_request('https://api.openai.com/v1/audio/transcriptions', [
|
||||
$post = array_merge($extParams, [
|
||||
'file' => new \CURLFile($filePath),
|
||||
'model' => 'whisper-1'
|
||||
], $extra, 15);
|
||||
if (Base::isError($res)) {
|
||||
return Base::retError("语音转文字失败", $res);
|
||||
'model' => 'whisper-1',
|
||||
]);
|
||||
$cacheKey = "openAItranscriptions::" . md5($filePath . '_' . Base::array2json($extra) . '_' . Base::array2json($extParams));
|
||||
$result = Cache::remember($cacheKey, Carbon::now()->addDays(), function() use ($extra, $post) {
|
||||
$res = Ihttp::ihttp_request('https://api.openai.com/v1/audio/transcriptions', $post, $extra, 15);
|
||||
if (Base::isError($res)) {
|
||||
return Base::retError("语音转文字失败", $res);
|
||||
}
|
||||
$resData = Base::json2array($res['data']);
|
||||
if (empty($resData['text'])) {
|
||||
return Base::retError("语音转文字失败", $resData);
|
||||
}
|
||||
return Base::retSuccess("success", $resData['text']);
|
||||
});
|
||||
if (Base::isError($result)) {
|
||||
Cache::forget($cacheKey);
|
||||
}
|
||||
$resData = Base::json2array($res['data']);
|
||||
if (empty($resData['text'])) {
|
||||
return Base::retError("语音转文字失败", $resData);
|
||||
}
|
||||
//
|
||||
return Base::retSuccess("success", $resData['text']);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,18 +77,78 @@ class Extranet
|
||||
];
|
||||
if ($aibotSetting['openai_agency']) {
|
||||
$extra['CURLOPT_PROXY'] = $aibotSetting['openai_agency'];
|
||||
if (str_contains($aibotSetting['openai_agency'], 'socks')) {
|
||||
$extra['CURLOPT_PROXYTYPE'] = CURLPROXY_SOCKS5;
|
||||
} else {
|
||||
$extra['CURLOPT_PROXYTYPE'] = CURLPROXY_HTTP;
|
||||
}
|
||||
$extra['CURLOPT_PROXYTYPE'] = str_contains($aibotSetting['openai_agency'], 'socks') ? CURLPROXY_SOCKS5 : CURLPROXY_HTTP;
|
||||
}
|
||||
$res = Ihttp::ihttp_request('https://api.openai.com/v1/chat/completions', json_encode([
|
||||
"model" => "gpt-3.5-turbo",
|
||||
$post = json_encode([
|
||||
"model" => "gpt-4o-mini",
|
||||
"messages" => [
|
||||
[
|
||||
"role" => "system",
|
||||
"content" => "你是一个专业的翻译器,翻译的结果尽量符合“项目任务管理系统”的使用,并且翻译的结果不用额外添加换行尽量保持原格式,将提供的文本翻译成“{$targetLanguage}”语言。"
|
||||
"content" => <<<EOF
|
||||
你是一名专业翻译人员,请将 <translation_original_text> 标签内的内容翻译为{$targetLanguage}。
|
||||
|
||||
翻译要求:
|
||||
- 翻译结果需符合“项目任务管理系统”的专业术语和使用场景。
|
||||
- 保持原文格式、结构和排版不变。
|
||||
- 语言表达准确、简洁,符合项目管理领域的行业规范。
|
||||
- 注意专业术语的一致性和连贯性。
|
||||
EOF
|
||||
],
|
||||
[
|
||||
"role" => "user",
|
||||
"content" => "<translation_original_text>{$text}</translation_original_text>"
|
||||
]
|
||||
]
|
||||
]);
|
||||
$cacheKey = "openAItranslations::" . md5(Base::array2json($extra) . '_' . Base::array2json($post));
|
||||
$result = Cache::remember($cacheKey, Carbon::now()->addDays(), function() use ($extra, $post) {
|
||||
$res = Ihttp::ihttp_request('https://api.openai.com/v1/chat/completions', $post, $extra, 15);
|
||||
if (Base::isError($res)) {
|
||||
return Base::retError("翻译失败", $res);
|
||||
}
|
||||
$resData = Base::json2array($res['data']);
|
||||
if (empty($resData['choices'])) {
|
||||
return Base::retError("翻译失败", $resData);
|
||||
}
|
||||
$result = $resData['choices'][0]['message']['content'];
|
||||
$result = preg_replace('/^\"|\"$/', '', trim($result));
|
||||
$result = preg_replace('/<\/*translation_original_text>/', '', trim($result));
|
||||
if (empty($result)) {
|
||||
return Base::retError("翻译失败", $result);
|
||||
}
|
||||
return Base::retSuccess("success", $result);
|
||||
});
|
||||
if (Base::isError($result)) {
|
||||
Cache::forget($cacheKey);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 openAI 生成标题
|
||||
* @param $text
|
||||
* @return array
|
||||
*/
|
||||
public static function openAIGenerateTitle($text)
|
||||
{
|
||||
$aibotSetting = Base::setting('aibotSetting');
|
||||
if (empty($aibotSetting['openai_key'])) {
|
||||
return Base::retError("AI接口未配置");
|
||||
}
|
||||
$extra = [
|
||||
'Content-Type' => 'application/json',
|
||||
'Authorization' => 'Bearer ' . $aibotSetting['openai_key'],
|
||||
];
|
||||
if ($aibotSetting['openai_agency']) {
|
||||
$extra['CURLOPT_PROXY'] = $aibotSetting['openai_agency'];
|
||||
$extra['CURLOPT_PROXYTYPE'] = str_contains($aibotSetting['openai_agency'], 'socks') ? CURLPROXY_SOCKS5 : CURLPROXY_HTTP;
|
||||
}
|
||||
$res = Ihttp::ihttp_request('https://api.openai.com/v1/chat/completions', json_encode([
|
||||
"model" => "gpt-4o-mini",
|
||||
"messages" => [
|
||||
[
|
||||
"role" => "system",
|
||||
"content" => "你是一个专业的标题生成器,擅长为对话生成简洁的标题,请将提供的文本生成一个标题。"
|
||||
],
|
||||
[
|
||||
"role" => "user",
|
||||
@@ -94,20 +157,61 @@ class Extranet
|
||||
]
|
||||
]), $extra, 15);
|
||||
if (Base::isError($res)) {
|
||||
return Base::retError("翻译失败", $res);
|
||||
return Base::retError("生成失败", $res);
|
||||
}
|
||||
$resData = Base::json2array($res['data']);
|
||||
if (empty($resData['choices'])) {
|
||||
return Base::retError("翻译失败", $resData);
|
||||
return Base::retError("生成失败", $resData);
|
||||
}
|
||||
$result = $resData['choices'][0]['message']['content'];
|
||||
$result = preg_replace('/^\"|\"$/', '', $result);
|
||||
if (empty($result)) {
|
||||
return Base::retError("翻译失败", $result);
|
||||
return Base::retError("生成失败", $result);
|
||||
}
|
||||
return Base::retSuccess("success", $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 ollama 模型
|
||||
* @param $baseUrl
|
||||
* @param $key
|
||||
* @param $agency
|
||||
* @return array
|
||||
*/
|
||||
public static function ollamaModels($baseUrl, $key = null, $agency = null)
|
||||
{
|
||||
$extra = [
|
||||
'Content-Type' => 'application/json',
|
||||
];
|
||||
if ($key) {
|
||||
$extra['Authorization'] = 'Bearer ' . $key;
|
||||
}
|
||||
if ($agency) {
|
||||
$extra['CURLOPT_PROXY'] = $agency;
|
||||
$extra['CURLOPT_PROXYTYPE'] = str_contains($agency, 'socks') ? CURLPROXY_SOCKS5 : CURLPROXY_HTTP;
|
||||
}
|
||||
$res = Ihttp::ihttp_request(rtrim($baseUrl, '/') . '/api/tags', [], $extra, 15);
|
||||
if (Base::isError($res)) {
|
||||
return Base::retError("获取失败", $res);
|
||||
}
|
||||
$resData = Base::json2array($res['data']);
|
||||
if (empty($resData['models'])) {
|
||||
return Base::retError("获取失败", $resData);
|
||||
}
|
||||
$models = [];
|
||||
foreach ($resData['models'] as $model) {
|
||||
if ($model['name'] !== $model['model']) {
|
||||
$models[] = "{$model['model']} | {$model['name']}";
|
||||
} else {
|
||||
$models[] = $model['model'];
|
||||
}
|
||||
}
|
||||
return Base::retSuccess("success", [
|
||||
'models' => $models,
|
||||
'original' => $resData['models']
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取IP地址经纬度
|
||||
* @param string $ip
|
||||
|
||||
@@ -25,11 +25,23 @@ class MsgTool
|
||||
}
|
||||
|
||||
$isMd = strtolower($type) === 'md';
|
||||
$placeholders = [];
|
||||
|
||||
// 如果是Markdown,转换为HTML
|
||||
// 如果是Markdown,先处理特殊标记及转换为HTML
|
||||
if ($isMd) {
|
||||
$converter = new CommonMarkConverter();
|
||||
// 处理特殊标记
|
||||
$pattern = '/:::\s*reasoning\s+(.*?)\s*:::/s';
|
||||
$counter = 0;
|
||||
$text = preg_replace_callback($pattern, function($matches) use ($type, $length, &$placeholders, &$counter) {
|
||||
// 使用更简短的占位符,避免被markdown解析
|
||||
$placeholder = "@PH::{$counter}::PH@";
|
||||
$placeholders[$placeholder] = "::: reasoning\n" . self::truncateText($matches[1], $length, $type) . "\n:::";
|
||||
$counter++;
|
||||
return $placeholder;
|
||||
}, $text);
|
||||
// 转换为HTML
|
||||
try {
|
||||
$converter = new CommonMarkConverter();
|
||||
$text = $converter->convert($text);
|
||||
} catch (CommonMarkException) {
|
||||
return "";
|
||||
@@ -50,10 +62,26 @@ class MsgTool
|
||||
// 递归函数来遍历节点并截取内容
|
||||
self::traverseNodes($body, $currentLength, $length, $truncatedHtml);
|
||||
|
||||
// 如果是Markdown,转换回Markdown
|
||||
// 如果是Markdown,转换回Markdown及还原特殊标记
|
||||
if ($isMd) {
|
||||
$converter = new HtmlConverter();
|
||||
$truncatedHtml = $converter->convert($truncatedHtml);
|
||||
// 转换回Markdown
|
||||
try {
|
||||
$converter = new HtmlConverter();
|
||||
$truncatedHtml = $converter->convert($truncatedHtml);
|
||||
} catch (\Exception) {
|
||||
return "";
|
||||
}
|
||||
// 还原特殊标记
|
||||
if (!empty($placeholders)) {
|
||||
$truncatedHtml = preg_replace('/@P?H?:*\s*$/', '', $truncatedHtml);
|
||||
$preCount = substr_count($truncatedHtml, '@PH::');
|
||||
$sufCount = substr_count($truncatedHtml, '::PH@');
|
||||
$diffCount = $preCount - $sufCount;
|
||||
if ($diffCount > 0) {
|
||||
$truncatedHtml .= str_repeat('::PH@', $diffCount);
|
||||
}
|
||||
$truncatedHtml = strtr($truncatedHtml, $placeholders);
|
||||
}
|
||||
}
|
||||
|
||||
return $truncatedHtml;
|
||||
|
||||
259
app/Module/TextExtractor.php
Normal file
259
app/Module/TextExtractor.php
Normal file
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module;
|
||||
|
||||
use Exception;
|
||||
use PhpOffice\PhpWord\IOFactory as WordIOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory as SpreadsheetIOFactory;
|
||||
use PhpOffice\PhpPresentation\IOFactory as PresentationIOFactory;
|
||||
use Illuminate\Support\Facades\File as FileFacade;
|
||||
|
||||
|
||||
class TextExtractor
|
||||
{
|
||||
private string $filePath;
|
||||
private string $fileMimeType;
|
||||
private string $fileExtension;
|
||||
|
||||
/**
|
||||
* @param string $filePath
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __construct(string $filePath)
|
||||
{
|
||||
if (!file_exists($filePath)) {
|
||||
throw new Exception("File does not exist: {$filePath}");
|
||||
}
|
||||
$this->filePath = $filePath;
|
||||
$this->fileMimeType = FileFacade::mimeType($filePath);
|
||||
$this->fileExtension = $this->detectFileType();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件中提取文本
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
public function extractContent(): string
|
||||
{
|
||||
return match ($this->fileExtension) {
|
||||
// Word文档
|
||||
'docx' => $this->parseWordDocument(),
|
||||
|
||||
// Excel文档
|
||||
'xlsx', 'xls', 'csv' => $this->parseSpreadsheet(),
|
||||
|
||||
// PowerPoint文档
|
||||
'ppt', 'pptx' => $this->parsePresentation(),
|
||||
|
||||
// PDF文档
|
||||
'pdf' => $this->parsePdf(),
|
||||
|
||||
// RTF文档
|
||||
'rtf' => $this->parseRtf(),
|
||||
|
||||
// 其他文本文件
|
||||
default => $this->parseOther(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件类型
|
||||
* @return string
|
||||
*/
|
||||
private function detectFileType(): string
|
||||
{
|
||||
return match ($this->fileMimeType) {
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
|
||||
'application/vnd.ms-excel' => 'xls',
|
||||
'text/csv', 'application/csv' => 'csv',
|
||||
'application/vnd.ms-powerpoint' => 'ppt',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx',
|
||||
'application/pdf' => 'pdf',
|
||||
'application/rtf', 'text/rtf' => 'rtf',
|
||||
default => strtolower(pathinfo($this->filePath, PATHINFO_EXTENSION)),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Word documents (.doc, .docx)
|
||||
* @return string
|
||||
*/
|
||||
private function parseWordDocument(): string
|
||||
{
|
||||
$phpWord = WordIOFactory::load($this->filePath);
|
||||
$text = '';
|
||||
|
||||
// Extract text from each section
|
||||
foreach ($phpWord->getSections() as $section) {
|
||||
foreach ($section->getElements() as $element) {
|
||||
if (method_exists($element, 'getText')) {
|
||||
$text .= $element->getText() . "\n";
|
||||
} elseif (method_exists($element, 'getElements')) {
|
||||
foreach ($element->getElements() as $childElement) {
|
||||
if (method_exists($childElement, 'getText')) {
|
||||
$text .= $childElement->getText() . "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse spreadsheet files (.xlsx, .xls, .csv)
|
||||
* @return string
|
||||
*/
|
||||
private function parseSpreadsheet(): string
|
||||
{
|
||||
$spreadsheet = SpreadsheetIOFactory::load($this->filePath);
|
||||
$text = '';
|
||||
|
||||
// Extract text from all worksheets
|
||||
foreach ($spreadsheet->getWorksheetIterator() as $worksheet) {
|
||||
$text .= 'Worksheet: ' . $worksheet->getTitle() . "\n";
|
||||
|
||||
foreach ($worksheet->getRowIterator() as $row) {
|
||||
$cellIterator = $row->getCellIterator();
|
||||
$cellIterator->setIterateOnlyExistingCells(false);
|
||||
$rowText = '';
|
||||
|
||||
foreach ($cellIterator as $cell) {
|
||||
$value = $cell->getValue();
|
||||
if (!empty($value)) {
|
||||
$rowText .= $value . "\t";
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty(trim($rowText))) {
|
||||
$text .= trim($rowText) . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
$text .= "\n";
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse presentation files (.ppt, .pptx)
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
private function parsePresentation(): string
|
||||
{
|
||||
$presentation = PresentationIOFactory::load($this->filePath);
|
||||
$text = '';
|
||||
|
||||
// Extract text from all slides
|
||||
foreach ($presentation->getAllSlides() as $slide) {
|
||||
foreach ($slide->getShapeCollection() as $shape) {
|
||||
if ($shape instanceof \PhpOffice\PhpPresentation\Shape\RichText) {
|
||||
foreach ($shape->getParagraphs() as $paragraph) {
|
||||
foreach ($paragraph->getRichTextElements() as $element) {
|
||||
$text .= $element->getText();
|
||||
}
|
||||
$text .= "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
$text .= "\n";
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse PDF files (requires additional library like Smalot\PdfParser)
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
private function parsePdf(): string
|
||||
{
|
||||
// You'll need to install the Smalot PDF Parser: composer require smalot/pdfparser
|
||||
if (!class_exists('\Smalot\PdfParser\Parser')) {
|
||||
throw new \Exception("PDF Parser not available. Install with: composer require smalot/pdfparser");
|
||||
}
|
||||
|
||||
$parser = new \Smalot\PdfParser\Parser();
|
||||
$pdf = $parser->parseFile($this->filePath);
|
||||
return $pdf->getText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse RTF files
|
||||
* @return string
|
||||
*/
|
||||
private function parseRtf(): string
|
||||
{
|
||||
// Simple RTF to text conversion
|
||||
$content = file_get_contents($this->filePath);
|
||||
|
||||
// Remove RTF control words and groups
|
||||
$content = preg_replace('/\\\\([a-z]{1,32})(-?[0-9]{1,10})?[ ]?/i', '', $content);
|
||||
$content = preg_replace('/\\\\([^a-z]|[a-z]{33,})/i', '', $content);
|
||||
$content = preg_replace('/\{\*?\\\\[^{}]*\}/', '', $content);
|
||||
$content = preg_replace('/\{[\r\n]*\}/', '', $content);
|
||||
|
||||
// Convert special characters
|
||||
$content = preg_replace('/\\\\\'([0-9a-f]{2})/i', '', $content);
|
||||
|
||||
// Remove remaining curly braces
|
||||
$content = str_replace(['{', '}'], '', $content);
|
||||
|
||||
return $content ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Other(text) files
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
private function parseOther(): string
|
||||
{
|
||||
$isBinary = !str_contains($this->fileMimeType, 'text/')
|
||||
&& !str_contains($this->fileMimeType, 'application/json')
|
||||
&& !str_contains($this->fileMimeType, 'application/xml');
|
||||
|
||||
if ($isBinary) {
|
||||
throw new Exception("Unable to read the text content of this type of file");
|
||||
}
|
||||
|
||||
return file_get_contents($this->filePath);
|
||||
}
|
||||
|
||||
/** ********************************************************************* */
|
||||
/** ********************************************************************* */
|
||||
/** ********************************************************************* */
|
||||
|
||||
/**
|
||||
* 获取文件内容
|
||||
* @param $filePath
|
||||
* @param int $fileMaxSize 最大文件大小,单位字节,默认1024KB
|
||||
* @param int $contentMaxSize 最大内容大小,单位字节,默认300KB
|
||||
* @return array
|
||||
*/
|
||||
public static function extractFile($filePath, int $fileMaxSize = 1024, int $contentMaxSize = 300): array
|
||||
{
|
||||
if (!file_exists($filePath) || !is_file($filePath)) {
|
||||
return Base::retError("Failed to read contents of {$filePath}");
|
||||
}
|
||||
if (filesize($filePath) > $fileMaxSize * 1024) {
|
||||
return Base::retError("File size exceeds " . Base::readableBytes($fileMaxSize * 1024) . ", unable to display content");
|
||||
}
|
||||
try {
|
||||
$extractor = new self($filePath);
|
||||
$content = $extractor->extractContent();
|
||||
if (strlen($content) > $contentMaxSize * 1024) {
|
||||
return Base::retError("Content size exceeds " . Base::readableBytes($contentMaxSize * 1024) . ", unable to display content");
|
||||
}
|
||||
return Base::retSuccess("success", $content);
|
||||
} catch (Exception $e) {
|
||||
return Base::retError($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
258
app/Module/ZincSearch/ZincSearchBase.php
Normal file
258
app/Module/ZincSearch/ZincSearchBase.php
Normal file
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module\ZincSearch;
|
||||
|
||||
/**
|
||||
* ZincSearch 公共类
|
||||
*/
|
||||
class ZincSearchBase
|
||||
{
|
||||
private mixed $host;
|
||||
private mixed $port;
|
||||
private mixed $user;
|
||||
private mixed $pass;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->host = env('ZINCSEARCH_HOST', 'search');
|
||||
$this->port = env('ZINCSEARCH_PORT', '4080');
|
||||
$this->user = env('DB_USERNAME', '');
|
||||
$this->pass = env('DB_PASSWORD', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用请求方法
|
||||
*/
|
||||
private function request($path, $body = null, $method = 'POST')
|
||||
{
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, "http://{$this->host}:{$this->port}{$path}");
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_USERPWD, $this->user . ':' . $this->pass);
|
||||
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
|
||||
|
||||
$headers = ['Content-Type: application/json'];
|
||||
if ($method === 'BULK') {
|
||||
$headers = ['Content-Type: text/plain'];
|
||||
$method = 'POST';
|
||||
}
|
||||
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
if ($body !== null) {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||
}
|
||||
$result = curl_exec($ch);
|
||||
$error = curl_error($ch);
|
||||
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if ($error) {
|
||||
return ['success' => false, 'error' => $error];
|
||||
}
|
||||
$data = json_decode($result, true);
|
||||
return [
|
||||
'success' => $status >= 200 && $status < 300,
|
||||
'status' => $status,
|
||||
'data' => $data
|
||||
];
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 索引管理相关方法
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 创建索引
|
||||
*/
|
||||
public static function createIndex($index, $mappings = []): array
|
||||
{
|
||||
$body = json_encode([
|
||||
'name' => $index,
|
||||
'mappings' => $mappings
|
||||
]);
|
||||
return (new self())->request("/api/index", $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取索引信息
|
||||
*/
|
||||
public static function getIndex($index): array
|
||||
{
|
||||
return (new self())->request("/api/index/{$index}", null, 'GET');
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断索引是否存在
|
||||
*/
|
||||
public static function indexExists($index): bool
|
||||
{
|
||||
$result = self::getIndex($index);
|
||||
return $result['success'] && isset($result['data']['name']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有索引
|
||||
*/
|
||||
public static function listIndices(): array
|
||||
{
|
||||
return (new self())->request("/api/index", null, 'GET');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除索引
|
||||
*/
|
||||
public static function deleteIndex($index): array
|
||||
{
|
||||
return (new self())->request("/api/index/{$index}", null, 'DELETE');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除所有索引
|
||||
*/
|
||||
public static function deleteAllIndices(): array
|
||||
{
|
||||
$instance = new self();
|
||||
$result = $instance->request("/api/index", null, 'GET');
|
||||
|
||||
if (!$result['success']) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$indices = $result['data'] ?? [];
|
||||
$deleteResults = [];
|
||||
$success = true;
|
||||
|
||||
foreach ($indices as $index) {
|
||||
$indexName = $index['name'] ?? '';
|
||||
if (!empty($indexName)) {
|
||||
$deleteResult = $instance->request("/api/index/{$indexName}", null, 'DELETE');
|
||||
$deleteResults[$indexName] = $deleteResult;
|
||||
|
||||
if (!$deleteResult['success']) {
|
||||
$success = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => $success,
|
||||
'message' => $success ? '所有索引删除成功' : '部分索引删除失败',
|
||||
'details' => $deleteResults
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析文本
|
||||
*/
|
||||
public static function analyze($analyzer, $text): array
|
||||
{
|
||||
$body = json_encode([
|
||||
'analyzer' => $analyzer,
|
||||
'text' => $text
|
||||
]);
|
||||
return (new self())->request("/api/_analyze", $body);
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 文档管理相关方法
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 写入单条文档
|
||||
*/
|
||||
public static function addDoc($index, $doc): array
|
||||
{
|
||||
$body = json_encode($doc);
|
||||
return (new self())->request("/api/{$index}/_doc", $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新文档
|
||||
*/
|
||||
public static function updateDoc($index, $id, $doc): array
|
||||
{
|
||||
$body = json_encode($doc);
|
||||
return (new self())->request("/api/{$index}/_update/{$id}", $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文档
|
||||
*/
|
||||
public static function deleteDoc($index, $id): array
|
||||
{
|
||||
return (new self())->request("/api/{$index}/_doc/{$id}", null, 'DELETE');
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量写入文档
|
||||
*/
|
||||
public static function addDocs($index, $docs): array
|
||||
{
|
||||
$body = json_encode([
|
||||
'index' => $index,
|
||||
'records' => $docs
|
||||
]);
|
||||
return (new self())->request("/api/_bulkv2", $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用原始BULK API批量写入文档
|
||||
* 请求格式为Elasticsearch兼容格式
|
||||
*/
|
||||
public static function bulkDocs($data): array
|
||||
{
|
||||
return (new self())->request("/api/_bulk", $data, 'BULK');
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 搜索相关方法
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 查询文档
|
||||
*/
|
||||
public static function search($index, $query, $from = 0, $size = 10): array
|
||||
{
|
||||
$searchParams = [
|
||||
'search_type' => 'match',
|
||||
'query' => [
|
||||
'term' => $query
|
||||
],
|
||||
'from' => $from,
|
||||
'max_results' => $size
|
||||
];
|
||||
|
||||
$body = json_encode($searchParams);
|
||||
return (new self())->request("/api/{$index}/_search", $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 高级查询文档
|
||||
*/
|
||||
public static function advancedSearch($index, $searchParams): array
|
||||
{
|
||||
$body = json_encode($searchParams);
|
||||
return (new self())->request("/api/{$index}/_search", $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容ES查询文档
|
||||
*/
|
||||
public static function elasticSearch($index, $searchParams): array
|
||||
{
|
||||
$body = json_encode($searchParams);
|
||||
return (new self())->request("/es/{$index}/_search", $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 多索引查询
|
||||
*/
|
||||
public static function multiSearch($queries): array
|
||||
{
|
||||
$body = json_encode($queries);
|
||||
return (new self())->request("/api/_msearch", $body);
|
||||
}
|
||||
}
|
||||
565
app/Module/ZincSearch/ZincSearchDialogMsg.php
Normal file
565
app/Module/ZincSearch/ZincSearchDialogMsg.php
Normal file
@@ -0,0 +1,565 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module\ZincSearch;
|
||||
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Models\WebSocketDialogUser;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Swoole\Coroutine;
|
||||
|
||||
/**
|
||||
* ZincSearch 会话消息类
|
||||
*
|
||||
* 使用方法:
|
||||
*
|
||||
* 1. 基础方法
|
||||
* - 清空所有数据: clear();
|
||||
*
|
||||
* 2. 搜索方法
|
||||
* - 关键词搜索: search('用户ID', '关键词');
|
||||
*
|
||||
* 3. 基本方法
|
||||
* - 单个同步: sync(WebSocketDialogMsg $dialogMsg);
|
||||
* - 批量同步: batchSync(WebSocketDialogMsg[] $dialogMsgs);
|
||||
* - 用户同步: userSync(WebSocketDialogUser $dialogUser);
|
||||
* - 删除消息: delete(WebSocketDialogMsg|WebSocketDialogUser|int $data);
|
||||
*/
|
||||
class ZincSearchDialogMsg
|
||||
{
|
||||
/**
|
||||
* 索引名称
|
||||
*/
|
||||
protected static string $indexNameMsg = 'dialogMsg';
|
||||
protected static string $indexNameUser = 'dialogUser';
|
||||
|
||||
// ==============================
|
||||
// 基础方法
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 确保索引存在
|
||||
*/
|
||||
private static function ensureIndex(): bool
|
||||
{
|
||||
if (!ZincSearchBase::indexExists(self::$indexNameMsg)) {
|
||||
$mappings = [
|
||||
'properties' => [
|
||||
// 拓展数据
|
||||
'dialog_userid' => ['type' => 'keyword', 'index' => true], // 对话ID+用户ID
|
||||
'to_userid' => ['type' => 'numeric', 'index' => true], // 此消息发给的用户ID
|
||||
|
||||
// 消息数据
|
||||
'id' => ['type' => 'numeric', 'index' => true],
|
||||
'dialog_id' => ['type' => 'numeric', 'index' => true],
|
||||
'dialog_type' => ['type' => 'keyword', 'index' => true],
|
||||
'session_id' => ['type' => 'numeric', 'index' => true],
|
||||
'userid' => ['type' => 'numeric', 'index' => true],
|
||||
'type' => ['type' => 'keyword', 'index' => true],
|
||||
'key' => ['type' => 'text', 'index' => true],
|
||||
'created_at' => ['type' => 'date', 'index' => true],
|
||||
'updated_at' => ['type' => 'date', 'index' => true],
|
||||
]
|
||||
];
|
||||
$result = ZincSearchBase::createIndex(self::$indexNameMsg, $mappings);
|
||||
return $result['success'] ?? false;
|
||||
}
|
||||
if (!ZincSearchBase::indexExists(self::$indexNameUser)) {
|
||||
$mappings = [
|
||||
'properties' => [
|
||||
// 拓展数据
|
||||
'dialog_userid' => ['type' => 'keyword', 'index' => true], // 对话ID+用户ID
|
||||
|
||||
// 用户数据
|
||||
'id' => ['type' => 'numeric', 'index' => true],
|
||||
'dialog_id' => ['type' => 'numeric', 'index' => true],
|
||||
'userid' => ['type' => 'numeric', 'index' => true],
|
||||
'top_at' => ['type' => 'date', 'index' => true],
|
||||
'last_at' => ['type' => 'date', 'index' => true],
|
||||
'mark_unread' => ['type' => 'numeric', 'index' => true],
|
||||
'silence' => ['type' => 'numeric', 'index' => true],
|
||||
'hide' => ['type' => 'numeric', 'index' => true],
|
||||
'color' => ['type' => 'keyword', 'index' => true],
|
||||
'created_at' => ['type' => 'date', 'index' => true],
|
||||
'updated_at' => ['type' => 'date', 'index' => true],
|
||||
]
|
||||
];
|
||||
$result = ZincSearchBase::createIndex(self::$indexNameUser, $mappings);
|
||||
return $result['success'] ?? false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有键值
|
||||
*
|
||||
* @return bool 是否成功
|
||||
*/
|
||||
public static function clear(): bool
|
||||
{
|
||||
// 检查索引是否存在然后删除
|
||||
if (ZincSearchBase::indexExists(self::$indexNameMsg)) {
|
||||
$deleteResult = ZincSearchBase::deleteIndex(self::$indexNameMsg);
|
||||
if (!($deleteResult['success'] ?? false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (ZincSearchBase::indexExists(self::$indexNameUser)) {
|
||||
$deleteResult = ZincSearchBase::deleteIndex(self::$indexNameUser);
|
||||
if (!($deleteResult['success'] ?? false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return self::ensureIndex();
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 搜索方法
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 根据用户ID和消息关键词搜索会话
|
||||
*
|
||||
* @param string $userid 用户ID
|
||||
* @param string $keyword 消息关键词
|
||||
* @param int $from 起始位置
|
||||
* @param int $size 返回结果数量
|
||||
* @return array
|
||||
*/
|
||||
public static function search(string $userid, string $keyword, int $from = 0, int $size = 20): array
|
||||
{
|
||||
$searchParams = [
|
||||
'query' => [
|
||||
'bool' => [
|
||||
'must' => [
|
||||
['term' => ['to_userid' => $userid]],
|
||||
['match_phrase' => ['key' => $keyword]]
|
||||
]
|
||||
]
|
||||
],
|
||||
'from' => $from,
|
||||
'size' => $size,
|
||||
'sort' => [
|
||||
['updated_at' => 'desc']
|
||||
]
|
||||
];
|
||||
|
||||
try {
|
||||
$result = ZincSearchBase::elasticSearch(self::$indexNameMsg, $searchParams);
|
||||
$hits = $result['data']['hits']['hits'] ?? [];
|
||||
|
||||
// 收集所有的用户信息
|
||||
$dialogUserids = [];
|
||||
foreach ($hits as $hit) {
|
||||
$source = $hit['_source'];
|
||||
$dialogUserids[] = $source['dialog_userid'];
|
||||
}
|
||||
$userInfos = self::searchUser(array_unique($dialogUserids));
|
||||
|
||||
// 组合返回结果,将用户信息合并到消息中
|
||||
$msgs = [];
|
||||
foreach ($hits as $hit) {
|
||||
$msgInfo = $hit['_source'];
|
||||
$userInfo = $userInfos[$msgInfo['dialog_userid']] ?? [];
|
||||
if ($userInfo) {
|
||||
$msgs[] = [
|
||||
'id' => $msgInfo['dialog_id'],
|
||||
'search_msg_id' => $msgInfo['id'],
|
||||
'user_at' => Carbon::parse($msgInfo['updated_at'])->format('Y-m-d H:i:s'),
|
||||
|
||||
'mark_unread' => $userInfo['mark_unread'],
|
||||
'silence' => $userInfo['silence'],
|
||||
'hide' => $userInfo['hide'],
|
||||
'color' => $userInfo['color'],
|
||||
'top_at' => Carbon::parse($userInfo['top_at'])->format('Y-m-d H:i:s'),
|
||||
'last_at' => Carbon::parse($userInfo['last_at'])->format('Y-m-d H:i:s'),
|
||||
];
|
||||
}
|
||||
}
|
||||
return $msgs;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('search: ' . $e->getMessage());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据对话用户ID搜索用户信息
|
||||
* @param array $dialogUserids
|
||||
* @return array
|
||||
*/
|
||||
private static function searchUser(array $dialogUserids): array
|
||||
{
|
||||
if (empty($dialogUserids)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$userInfos = [];
|
||||
|
||||
// 构建用户查询条件
|
||||
$userSearchParams = [
|
||||
'query' => [
|
||||
'bool' => [
|
||||
'should' => []
|
||||
]
|
||||
],
|
||||
'size' => count($dialogUserids) // 确保取到所有符合条件的记录
|
||||
];
|
||||
|
||||
// 添加所有 dialog_userid 到查询条件
|
||||
foreach ($dialogUserids as $dialogUserid) {
|
||||
$userSearchParams['query']['bool']['should'][] = [
|
||||
'term' => ['dialog_userid' => $dialogUserid]
|
||||
];
|
||||
}
|
||||
|
||||
// 查询用户信息
|
||||
$userResult = ZincSearchBase::elasticSearch(self::$indexNameUser, $userSearchParams);
|
||||
$userHits = $userResult['data']['hits']['hits'] ?? [];
|
||||
|
||||
// 以 dialog_userid 为键保存用户信息
|
||||
foreach ($userHits as $userHit) {
|
||||
$userSource = $userHit['_source'];
|
||||
$userInfos[$userSource['dialog_userid']] = $userSource;
|
||||
}
|
||||
|
||||
return $userInfos;
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 基本方法
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 生成 dialog_userid
|
||||
*
|
||||
* @param WebSocketDialogUser $dialogUser
|
||||
* @return string
|
||||
*/
|
||||
private static function generateDialogUserid(WebSocketDialogUser $dialogUser): string
|
||||
{
|
||||
return "{$dialogUser->dialog_id}_{$dialogUser->userid}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成文档内容
|
||||
*
|
||||
* @param WebSocketDialogMsg $dialogMsg
|
||||
* @param WebSocketDialogUser $dialogUser
|
||||
* @return array
|
||||
*/
|
||||
private static function generateMsgData(WebSocketDialogMsg $dialogMsg, WebSocketDialogUser $dialogUser): array
|
||||
{
|
||||
return [
|
||||
'_id' => self::$indexNameMsg . "_" . $dialogMsg->id . "_" . $dialogUser->userid,
|
||||
'dialog_userid' => self::generateDialogUserid($dialogUser),
|
||||
'to_userid' => $dialogUser->userid,
|
||||
|
||||
'id' => $dialogMsg->id,
|
||||
'dialog_id' => $dialogMsg->dialog_id,
|
||||
'dialog_type' => $dialogMsg->dialog_type,
|
||||
'session_id' => $dialogMsg->session_id,
|
||||
'userid' => $dialogMsg->userid,
|
||||
'type' => $dialogMsg->type,
|
||||
'key' => $dialogMsg->key,
|
||||
'created_at' => $dialogMsg->created_at,
|
||||
'updated_at' => $dialogMsg->updated_at,
|
||||
];
|
||||
}
|
||||
private static function generateUserData(WebSocketDialogUser $dialogUser): array
|
||||
{
|
||||
return [
|
||||
'_id' => self::$indexNameUser . "_" . $dialogUser->id,
|
||||
'dialog_userid' => self::generateDialogUserid($dialogUser),
|
||||
|
||||
'id' => $dialogUser->id,
|
||||
'dialog_id' => $dialogUser->dialog_id,
|
||||
'userid' => $dialogUser->userid,
|
||||
'top_at' => $dialogUser->top_at,
|
||||
'last_at' => $dialogUser->last_at,
|
||||
'mark_unread' => $dialogUser->mark_unread,
|
||||
'silence' => $dialogUser->silence,
|
||||
'hide' => $dialogUser->hide,
|
||||
'color' => $dialogUser->color,
|
||||
'created_at' => $dialogUser->created_at,
|
||||
'updated_at' => $dialogUser->updated_at,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步消息
|
||||
*
|
||||
* @param WebSocketDialogMsg $dialogMsg
|
||||
* @return bool
|
||||
*/
|
||||
public static function sync(WebSocketDialogMsg $dialogMsg): bool
|
||||
{
|
||||
if (!self::ensureIndex()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($dialogMsg->bot) {
|
||||
// 如果是机器人消息,跳过
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取此会话的所有用户
|
||||
$dialogUsers = WebSocketDialogUser::whereDialogId($dialogMsg->dialog_id)->get();
|
||||
|
||||
if ($dialogUsers->isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$msgs = [];
|
||||
$users = [];
|
||||
foreach ($dialogUsers as $dialogUser) {
|
||||
if (empty($dialogMsg->key)) {
|
||||
// 如果消息没有关键词,跳过
|
||||
continue;
|
||||
}
|
||||
if ($dialogUser->userid == 0) {
|
||||
// 跳过系统用户
|
||||
continue;
|
||||
}
|
||||
$msgs[] = self::generateMsgData($dialogMsg, $dialogUser);
|
||||
$users[$dialogUser->id] = self::generateUserData($dialogUser);
|
||||
}
|
||||
|
||||
if ($msgs) {
|
||||
// 批量写入消息
|
||||
ZincSearchBase::addDocs(self::$indexNameMsg, $msgs);
|
||||
}
|
||||
|
||||
if ($users) {
|
||||
// 批量写入用户
|
||||
ZincSearchBase::addDocs(self::$indexNameUser, array_values($users));
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('sync: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量同步消息
|
||||
*
|
||||
* @param WebSocketDialogMsg[] $dialogMsgs
|
||||
* @return int 成功同步的消息数
|
||||
*/
|
||||
public static function batchSync($dialogMsgs): int
|
||||
{
|
||||
if (!self::ensureIndex()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
try {
|
||||
$msgs = [];
|
||||
$users = [];
|
||||
$userDialogs = [];
|
||||
|
||||
// 预处理:收集所有涉及的对话ID
|
||||
$dialogIds = [];
|
||||
foreach ($dialogMsgs as $dialogMsg) {
|
||||
$dialogIds[] = $dialogMsg->dialog_id;
|
||||
}
|
||||
$dialogIds = array_unique($dialogIds);
|
||||
|
||||
// 获取所有相关的用户-对话关系
|
||||
if (!empty($dialogIds)) {
|
||||
$dialogUsers = WebSocketDialogUser::whereIn('dialog_id', $dialogIds)->get();
|
||||
|
||||
// 按对话ID组织用户
|
||||
foreach ($dialogUsers as $dialogUser) {
|
||||
$userDialogs[$dialogUser->dialog_id][] = $dialogUser;
|
||||
}
|
||||
}
|
||||
|
||||
// 为每条消息准备所有相关用户的文档
|
||||
foreach ($dialogMsgs as $dialogMsg) {
|
||||
if (!isset($userDialogs[$dialogMsg->dialog_id])) {
|
||||
// 如果该会话没有用户,跳过
|
||||
continue;
|
||||
}
|
||||
if ($dialogMsg->bot) {
|
||||
// 如果是机器人消息,跳过
|
||||
continue;
|
||||
}
|
||||
/** @var WebSocketDialogUser $dialogUser */
|
||||
foreach ($userDialogs[$dialogMsg->dialog_id] as $dialogUser) {
|
||||
if (empty($dialogMsg->key)) {
|
||||
// 如果消息没有关键词,跳过
|
||||
continue;
|
||||
}
|
||||
if ($dialogUser->userid == 0) {
|
||||
// 跳过系统用户
|
||||
continue;
|
||||
}
|
||||
$msgs[] = self::generateMsgData($dialogMsg, $dialogUser);
|
||||
$users[$dialogUser->id] = self::generateUserData($dialogUser);
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($msgs) {
|
||||
// 批量写入消息
|
||||
ZincSearchBase::addDocs(self::$indexNameMsg, $msgs);
|
||||
}
|
||||
|
||||
if ($users) {
|
||||
// 批量写入用户
|
||||
ZincSearchBase::addDocs(self::$indexNameUser, array_values($users));
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('batchSync: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步用户
|
||||
* @param WebSocketDialogUser $dialogUser
|
||||
* @return bool
|
||||
*/
|
||||
public static function userSync(WebSocketDialogUser $dialogUser): bool
|
||||
{
|
||||
if (!self::ensureIndex()) {
|
||||
return false;
|
||||
}
|
||||
$data = self::generateUserData($dialogUser);
|
||||
|
||||
// 生成查询用户条件
|
||||
$searchParams = [
|
||||
'query' => [
|
||||
'bool' => [
|
||||
'must' => [
|
||||
['term' => ['dialog_userid' => $data['dialog_userid']]]
|
||||
]
|
||||
]
|
||||
],
|
||||
'size' => 1
|
||||
];
|
||||
|
||||
try {
|
||||
// 查询用户是否存在
|
||||
$result = ZincSearchBase::elasticSearch(self::$indexNameUser, $searchParams);
|
||||
$hits = $result['data']['hits']['hits'] ?? [];
|
||||
|
||||
// 同步用户(存在更新、不存在添加)
|
||||
$result = ZincSearchBase::addDoc(self::$indexNameUser, $data);
|
||||
if (!isset($result['success'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 用户不存在,同步消息
|
||||
if (empty($hits)) {
|
||||
go(function () use ($dialogUser) {
|
||||
Coroutine::sleep(0.1);
|
||||
|
||||
$lastId = 0; // 上次同步的最后ID
|
||||
$batchSize = 500; // 每批处理的消息数量
|
||||
|
||||
// 分批同步消息
|
||||
do {
|
||||
// 获取一批
|
||||
$dialogMsgs = WebSocketDialogMsg::whereDialogId($dialogUser->dialog_id)
|
||||
->where('id', '>', $lastId)
|
||||
->orderBy('id')
|
||||
->limit($batchSize)
|
||||
->get();
|
||||
|
||||
if ($dialogMsgs->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 同步数据
|
||||
ZincSearchDialogMsg::batchSync($dialogMsgs);
|
||||
|
||||
// 更新最后ID
|
||||
$lastId = $dialogMsgs->last()->id;
|
||||
} while (count($dialogMsgs) == $batchSize);
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('userSync: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*
|
||||
* @param WebSocketDialogMsg|WebSocketDialogUser|int $data
|
||||
* @return int
|
||||
*/
|
||||
public static function delete(mixed $data): int
|
||||
{
|
||||
$batchSize = 500; // 每批处理的文档数量
|
||||
$totalDeleted = 0; // 总共删除的文档数量
|
||||
$from = 0;
|
||||
|
||||
// 根据数据类型生成查询条件
|
||||
if ($data instanceof WebSocketDialogMsg) {
|
||||
$query = [
|
||||
'field' => 'id',
|
||||
'term' => (string) $data->id
|
||||
];
|
||||
} elseif ($data instanceof WebSocketDialogUser) {
|
||||
$query = [
|
||||
'field' => 'dialog_userid',
|
||||
'term' => self::generateDialogUserid($data),
|
||||
];
|
||||
} else {
|
||||
$query = [
|
||||
'field' => 'id',
|
||||
'term' => (string) $data
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
// 根据消息ID查找相关文档
|
||||
$result = ZincSearchBase::advancedSearch(self::$indexNameMsg, [
|
||||
'search_type' => 'term',
|
||||
'query' => $query,
|
||||
'from' => $from,
|
||||
'max_results' => $batchSize
|
||||
]);
|
||||
$hits = $result['data']['hits']['hits'] ?? [];
|
||||
|
||||
// 如果没有更多文档,退出循环
|
||||
if (empty($hits)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 删除本批次找到的所有文档
|
||||
foreach ($hits as $hit) {
|
||||
if (isset($hit['_id'])) {
|
||||
ZincSearchBase::deleteDoc(self::$indexNameMsg, $hit['_id']);
|
||||
$totalDeleted++;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果返回的文档数少于批次大小,说明已经没有更多文档了
|
||||
if (count($hits) < $batchSize) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 移动到下一批
|
||||
$from += $batchSize;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('delete: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return $totalDeleted;
|
||||
}
|
||||
}
|
||||
276
app/Module/ZincSearch/ZincSearchKeyValue.php
Normal file
276
app/Module/ZincSearch/ZincSearchKeyValue.php
Normal file
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module\ZincSearch;
|
||||
|
||||
/**
|
||||
* ZincSearch 键值存储类
|
||||
*
|
||||
* 使用方法:
|
||||
*
|
||||
* 1. 基础方法
|
||||
* - 确保索引存在: ensureIndex();
|
||||
* - 清空所有数据: clear();
|
||||
*
|
||||
* 2. 基本操作
|
||||
* - 设置键值: set('site_name', '我的网站');
|
||||
* - 设置复杂数据: set('site_config', ['logo' => 'logo.png', 'theme' => 'dark']);
|
||||
* - 合并现有数据: set('site_config', ['footer' => '版权所有'], true);
|
||||
* - 获取键值: $siteName = get('site_name');
|
||||
* - 获取键值带默认值: $theme = get('theme', 'light');
|
||||
* - 删除键值: delete('temporary_data');
|
||||
*
|
||||
* 3. 批量操作
|
||||
* - 批量设置: batchSet(['user_count' => 100, 'active_users' => 50]);
|
||||
* - 批量获取: $stats = batchGet(['user_count', 'active_users']);
|
||||
*/
|
||||
class ZincSearchKeyValue
|
||||
{
|
||||
/**
|
||||
* 索引名称
|
||||
*/
|
||||
protected static string $indexName = 'keyValue';
|
||||
|
||||
// ==============================
|
||||
// 基础方法
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 确保索引存在
|
||||
*/
|
||||
public static function ensureIndex(): bool
|
||||
{
|
||||
if (!ZincSearchBase::indexExists(self::$indexName)) {
|
||||
$mappings = [
|
||||
'properties' => [
|
||||
'key' => ['type' => 'keyword', 'index' => true],
|
||||
'value' => ['type' => 'text', 'index' => true],
|
||||
'created_at' => ['type' => 'date', 'index' => true],
|
||||
'updated_at' => ['type' => 'date', 'index' => true]
|
||||
]
|
||||
];
|
||||
$result = ZincSearchBase::createIndex(self::$indexName, $mappings);
|
||||
return $result['success'] ?? false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有键值
|
||||
*
|
||||
* @return bool 是否成功
|
||||
*/
|
||||
public static function clear(): bool
|
||||
{
|
||||
// 检查索引是否存在
|
||||
if (!ZincSearchBase::indexExists(self::$indexName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 删除再重建索引
|
||||
$deleteResult = ZincSearchBase::deleteIndex(self::$indexName);
|
||||
if (!($deleteResult['success'] ?? false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::ensureIndex();
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 基本操作
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 设置键值
|
||||
*
|
||||
* @param string $key 键名
|
||||
* @param mixed $value 值
|
||||
* @param bool $merge 是否合并现有数据(如果值是数组)
|
||||
* @return bool 是否成功
|
||||
*/
|
||||
public static function set(string $key, mixed $value, bool $merge = false): bool
|
||||
{
|
||||
if (!self::ensureIndex()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查键是否已存在
|
||||
if ($merge && is_array($value)) {
|
||||
$existingData = self::get($key);
|
||||
if (is_array($existingData)) {
|
||||
$value = array_merge($existingData, $value);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否存在相同键的文档 - 使用精确查询而不是普通搜索
|
||||
$searchParams = [
|
||||
'search_type' => 'term',
|
||||
'query' => [
|
||||
'field' => 'key',
|
||||
'term' => $key
|
||||
],
|
||||
'from' => 0,
|
||||
'max_results' => 1
|
||||
];
|
||||
|
||||
$result = ZincSearchBase::advancedSearch(self::$indexName, $searchParams);
|
||||
$docs = $result['data']['hits']['hits'] ?? [];
|
||||
$now = date('c');
|
||||
|
||||
if (!empty($docs)) {
|
||||
$docId = $docs[0]['_id'] ?? null;
|
||||
if ($docId) {
|
||||
// 更新现有文档
|
||||
$docData = [
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
'updated_at' => $now
|
||||
];
|
||||
$updateResult = ZincSearchBase::updateDoc(self::$indexName, $docId, $docData);
|
||||
return $updateResult['success'] ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新文档
|
||||
$docData = [
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now
|
||||
];
|
||||
$addResult = ZincSearchBase::addDoc(self::$indexName, $docData);
|
||||
return $addResult['success'] ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取键值
|
||||
*
|
||||
* @param string $key 键名
|
||||
* @param mixed $default 默认值
|
||||
* @return mixed 值或默认值
|
||||
*/
|
||||
public static function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
if (!self::ensureIndex() || empty($key)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
// 精确匹配键名
|
||||
$searchParams = [
|
||||
'search_type' => 'term',
|
||||
'query' => [
|
||||
'field' => 'key',
|
||||
'term' => $key
|
||||
],
|
||||
'from' => 0,
|
||||
'max_results' => 1
|
||||
];
|
||||
|
||||
$result = ZincSearchBase::advancedSearch(self::$indexName, $searchParams);
|
||||
if (!($result['success'] ?? false)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$hits = $result['data']['hits']['hits'] ?? [];
|
||||
if (empty($hits)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return $hits[0]['_source']['value'] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除键值
|
||||
*
|
||||
* @param string $key 键名
|
||||
* @return bool 是否成功
|
||||
*/
|
||||
public static function delete(string $key): bool
|
||||
{
|
||||
if (!self::ensureIndex() || empty($key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 查找文档ID
|
||||
$searchParams = [
|
||||
'search_type' => 'term',
|
||||
'query' => [
|
||||
'field' => 'key',
|
||||
'term' => $key
|
||||
],
|
||||
'from' => 0,
|
||||
'max_results' => 1
|
||||
];
|
||||
|
||||
$result = ZincSearchBase::advancedSearch(self::$indexName, $searchParams);
|
||||
if (!($result['success'] ?? false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$hits = $result['data']['hits']['hits'] ?? [];
|
||||
if (empty($hits)) {
|
||||
return true; // 不存在视为删除成功
|
||||
}
|
||||
|
||||
$docId = $hits[0]['_id'] ?? null;
|
||||
if (empty($docId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$deleteResult = ZincSearchBase::deleteDoc(self::$indexName, $docId);
|
||||
return $deleteResult['success'] ?? false;
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 批量操作
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 批量设置键值对
|
||||
*
|
||||
* @param array $keyValues 键值对数组
|
||||
* @return bool 是否全部成功
|
||||
*/
|
||||
public static function batchSet(array $keyValues): bool
|
||||
{
|
||||
if (!self::ensureIndex() || empty($keyValues)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$docs = [];
|
||||
$now = date('c');
|
||||
|
||||
foreach ($keyValues as $key => $value) {
|
||||
$docs[] = [
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now
|
||||
];
|
||||
}
|
||||
|
||||
$result = ZincSearchBase::addDocs(self::$indexName, $docs);
|
||||
return $result['success'] ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取键值
|
||||
*
|
||||
* @param array $keys 键名数组
|
||||
* @return array 键值对数组
|
||||
*/
|
||||
public static function batchGet(array $keys): array
|
||||
{
|
||||
if (!self::ensureIndex() || empty($keys)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$results = [];
|
||||
|
||||
// 遍历查询每个键
|
||||
foreach ($keys as $key) {
|
||||
$results[$key] = self::get($key);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@ class ProjectTaskObserver
|
||||
}
|
||||
$array = [];
|
||||
if (in_array('task', $dataType)) {
|
||||
$array = array_merge($array, ProjectTaskUser::whereTaskId($projectTask->id)->pluck('userid')->toArray());
|
||||
$array = array_merge($array, ProjectTaskUser::whereTaskId($projectTask->id)->orWhere('task_pid' ,$projectTask->id)->pluck('userid')->toArray());
|
||||
}
|
||||
if (in_array('visibility', $dataType)) {
|
||||
$array = array_merge($array, ProjectTaskVisibilityUser::whereTaskId($projectTask->id)->pluck('userid')->toArray());
|
||||
@@ -121,5 +121,6 @@ class ProjectTaskObserver
|
||||
Deleted::forget('projectTask', $projectTask->id, $forgetUserids);
|
||||
break;
|
||||
}
|
||||
ProjectTask::whereParentId($projectTask->id)->change(['visibility' => $projectTask->visibility]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ class ProjectTaskUserObserver
|
||||
public function created(ProjectTaskUser $projectTaskUser)
|
||||
{
|
||||
Deleted::forget('projectTask', $projectTaskUser->task_id, $projectTaskUser->userid);
|
||||
if ($projectTaskUser->task_pid) {
|
||||
Deleted::forget('projectTask', $projectTaskUser->task_pid, $projectTaskUser->userid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
64
app/Observers/WebSocketDialogMsgObserver.php
Normal file
64
app/Observers/WebSocketDialogMsgObserver.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Module\ZincSearch\ZincSearchDialogMsg;
|
||||
|
||||
class WebSocketDialogMsgObserver
|
||||
{
|
||||
/**
|
||||
* Handle the WebSocketDialogMsg "created" event.
|
||||
*
|
||||
* @param \App\Models\WebSocketDialogMsg $webSocketDialogMsg
|
||||
* @return void
|
||||
*/
|
||||
public function created(WebSocketDialogMsg $webSocketDialogMsg)
|
||||
{
|
||||
ZincSearchDialogMsg::sync($webSocketDialogMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the WebSocketDialogMsg "updated" event.
|
||||
*
|
||||
* @param \App\Models\WebSocketDialogMsg $webSocketDialogMsg
|
||||
* @return void
|
||||
*/
|
||||
public function updated(WebSocketDialogMsg $webSocketDialogMsg)
|
||||
{
|
||||
ZincSearchDialogMsg::sync($webSocketDialogMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the WebSocketDialogMsg "deleted" event.
|
||||
*
|
||||
* @param \App\Models\WebSocketDialogMsg $webSocketDialogMsg
|
||||
* @return void
|
||||
*/
|
||||
public function deleted(WebSocketDialogMsg $webSocketDialogMsg)
|
||||
{
|
||||
ZincSearchDialogMsg::delete($webSocketDialogMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the WebSocketDialogMsg "restored" event.
|
||||
*
|
||||
* @param \App\Models\WebSocketDialogMsg $webSocketDialogMsg
|
||||
* @return void
|
||||
*/
|
||||
public function restored(WebSocketDialogMsg $webSocketDialogMsg)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the WebSocketDialogMsg "force deleted" event.
|
||||
*
|
||||
* @param \App\Models\WebSocketDialogMsg $webSocketDialogMsg
|
||||
* @return void
|
||||
*/
|
||||
public function forceDeleted(WebSocketDialogMsg $webSocketDialogMsg)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Observers;
|
||||
|
||||
use App\Models\Deleted;
|
||||
use App\Models\WebSocketDialogUser;
|
||||
use App\Module\ZincSearch\ZincSearchDialogMsg;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class WebSocketDialogUserObserver
|
||||
@@ -29,6 +30,7 @@ class WebSocketDialogUserObserver
|
||||
}
|
||||
}
|
||||
Deleted::forget('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
|
||||
ZincSearchDialogMsg::userSync($webSocketDialogUser);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,7 +41,7 @@ class WebSocketDialogUserObserver
|
||||
*/
|
||||
public function updated(WebSocketDialogUser $webSocketDialogUser)
|
||||
{
|
||||
//
|
||||
ZincSearchDialogMsg::userSync($webSocketDialogUser);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,6 +53,7 @@ class WebSocketDialogUserObserver
|
||||
public function deleted(WebSocketDialogUser $webSocketDialogUser)
|
||||
{
|
||||
Deleted::record('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
|
||||
ZincSearchDialogMsg::delete($webSocketDialogUser);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,11 +7,13 @@ use App\Models\ProjectTask;
|
||||
use App\Models\ProjectTaskUser;
|
||||
use App\Models\ProjectUser;
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Models\WebSocketDialogUser;
|
||||
use App\Observers\ProjectObserver;
|
||||
use App\Observers\ProjectTaskObserver;
|
||||
use App\Observers\ProjectTaskUserObserver;
|
||||
use App\Observers\ProjectUserObserver;
|
||||
use App\Observers\WebSocketDialogMsgObserver;
|
||||
use App\Observers\WebSocketDialogObserver;
|
||||
use App\Observers\WebSocketDialogUserObserver;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
@@ -43,6 +45,7 @@ class EventServiceProvider extends ServiceProvider
|
||||
ProjectTaskUser::observe(ProjectTaskUserObserver::class);
|
||||
ProjectUser::observe(ProjectUserObserver::class);
|
||||
WebSocketDialog::observe(WebSocketDialogObserver::class);
|
||||
WebSocketDialogMsg::observe(WebSocketDialogMsgObserver::class);
|
||||
WebSocketDialogUser::observe(WebSocketDialogUserObserver::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,4 +139,64 @@ class RequestContext
|
||||
self::$context[$requestId] ??= [];
|
||||
self::$context[$requestId] = array_merge(self::$context[$requestId], $data);
|
||||
}
|
||||
|
||||
/** ***************************************************************************************** */
|
||||
/** ***************************************************************************************** */
|
||||
/** ***************************************************************************************** */
|
||||
|
||||
/**
|
||||
* 更新请求的基本URL
|
||||
*
|
||||
* @param Request $request
|
||||
* @return void
|
||||
*/
|
||||
public static function updateBaseUrl($request)
|
||||
{
|
||||
if ($request->path() !== 'api/system/setting') {
|
||||
return;
|
||||
}
|
||||
$schemeAndHttpHost = $request->getSchemeAndHttpHost();
|
||||
if (str_contains($schemeAndHttpHost, '127.0.0.1') || str_contains($schemeAndHttpHost, 'localhost')) {
|
||||
return;
|
||||
}
|
||||
\Cache::forever('RequestContext::base_url', $schemeAndHttpHost);
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换请求的基本URL
|
||||
*
|
||||
* @param string $url
|
||||
* @return string
|
||||
*/
|
||||
public static function replaceBaseUrl(string $url): string
|
||||
{
|
||||
// 先提取主机部分
|
||||
$pattern = '/^(https?:\/\/[^\/?#:]+(:\d+)?)/i';
|
||||
if (!preg_match($pattern, $url, $matches)) {
|
||||
return $url; // 如果不是有效URL直接返回
|
||||
}
|
||||
|
||||
$schemeAndHttpHost = $matches[1] ?? '';
|
||||
if (!$schemeAndHttpHost) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
// 只检查主机部分是否为本地主机
|
||||
if (str_contains($schemeAndHttpHost, '127.0.0.1') || str_contains($schemeAndHttpHost, 'localhost')) {
|
||||
$baseUrl = \Cache::get('RequestContext::base_url');
|
||||
if ($baseUrl) {
|
||||
return $baseUrl . substr($url, strlen($schemeAndHttpHost));
|
||||
}
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除基本URL缓存
|
||||
*/
|
||||
public static function clearBaseUrlCache(): void
|
||||
{
|
||||
\Cache::forget('RequestContext::base_url');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,19 +2,24 @@
|
||||
|
||||
namespace App\Tasks;
|
||||
|
||||
use App\Models\FileContent;
|
||||
use App\Models\Project;
|
||||
use App\Models\ProjectTask;
|
||||
use App\Models\Report;
|
||||
use App\Models\User;
|
||||
use App\Models\UserBot;
|
||||
use App\Models\UserDepartment;
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Models\WebSocketDialogConfig;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
use App\Module\Ihttp;
|
||||
use App\Module\TextExtractor;
|
||||
use Carbon\Carbon;
|
||||
use DB;
|
||||
use Exception;
|
||||
use League\HTMLToMarkdown\HtmlConverter;
|
||||
use DB;
|
||||
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
@@ -86,8 +91,16 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
}
|
||||
|
||||
// 提取指令
|
||||
$command = $this->extractCommand($msg, $this->mention);
|
||||
if (empty($command)) {
|
||||
try {
|
||||
$command = $this->extractCommand($msg, $botUser, $this->mention);
|
||||
if (empty($command)) {
|
||||
return;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'template', [
|
||||
'type' => 'content',
|
||||
'content' => $e->getMessage() ?: "指令解析失败。",
|
||||
], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -195,32 +208,11 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
* 创建
|
||||
*/
|
||||
case '/newbot':
|
||||
if (User::select(['users.*'])
|
||||
->join('user_bots', 'users.userid', '=', 'user_bots.bot_id')
|
||||
->where('users.bot', 1)
|
||||
->where('user_bots.userid', $msg->userid)
|
||||
->count() >= 50) {
|
||||
$content = "超过最大创建数量。";
|
||||
break;
|
||||
}
|
||||
if (strlen($array[1]) < 2 || strlen($array[1]) > 20) {
|
||||
$content = "机器人名称由2-20个字符组成。";
|
||||
break;
|
||||
}
|
||||
$data = User::botGetOrCreate("user-" . Base::generatePassword(), [
|
||||
'nickname' => $array[1]
|
||||
], $msg->userid);
|
||||
if (empty($data)) {
|
||||
$content = "创建失败。";
|
||||
break;
|
||||
}
|
||||
$dialog = WebSocketDialog::checkUserDialog($data, $msg->userid);
|
||||
if ($dialog) {
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||
'type' => '/hello',
|
||||
'title' => '创建成功。',
|
||||
'data' => $data,
|
||||
], $data->userid); // todo 未能在任务end事件来发送任务
|
||||
$res = UserBot::newbot($msg->userid, $array[1]);
|
||||
if (Base::isError($res)) {
|
||||
$content = $res['msg'];
|
||||
} else {
|
||||
$data = $res['data'];
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -411,14 +403,14 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
*/
|
||||
private function botWebhookBusiness(string $command, WebSocketDialogMsg $msg, User $botUser, WebSocketDialog $dialog)
|
||||
{
|
||||
$serverUrl = 'http://' . env('APP_IPPR') . '.3';
|
||||
$serverUrl = 'http://nginx';
|
||||
$userBot = null;
|
||||
$extras = [];
|
||||
$replyText = null;
|
||||
$errorContent = null;
|
||||
if (preg_match('/^ai-(.*?)@bot\.system$/', $botUser->email, $matches)) {
|
||||
if ($botUser->isAiBot($type)) {
|
||||
// AI机器人
|
||||
$setting = Base::setting('aibotSetting');
|
||||
$type = $matches[1];
|
||||
$extras = [
|
||||
'model_type' => match ($type) {
|
||||
'qianwen' => 'qwen',
|
||||
@@ -427,33 +419,74 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
'model_name' => $setting[$type . '_model'],
|
||||
'system_message' => $setting[$type . '_system'],
|
||||
'api_key' => $setting[$type . '_key'],
|
||||
'base_url' => $setting[$type . '_base_url'],
|
||||
'agency' => $setting[$type . '_agency'],
|
||||
'server_url' => $serverUrl,
|
||||
];
|
||||
if ($setting[$type . '_temperature']) {
|
||||
$extras['temperature'] = floatval($setting[$type . '_temperature']);
|
||||
}
|
||||
if ($msg->msg['model_name']) {
|
||||
$extras['model_name'] = $msg->msg['model_name'];
|
||||
}
|
||||
if (preg_match("/(.*?)(\s+|\s*[_-]\s*)(think|thinking|reasoning)\s*$/", $extras['model_name'], $match)) {
|
||||
$extras['model_name'] = $match[1];
|
||||
$extras['max_tokens'] = 20000;
|
||||
$extras['thinking'] = 4096;
|
||||
$extras['temperature'] = 1.0;
|
||||
}
|
||||
if ($dialog->session_id) {
|
||||
$extras['context_key'] = 'session_' . $dialog->session_id;
|
||||
}
|
||||
if ($type === 'wenxin') {
|
||||
$extras['api_key'] .= ':' . $setting['wenxin_secret'];
|
||||
}
|
||||
if ($type === 'ollama') {
|
||||
if (empty($extras['base_url'])) {
|
||||
$errorContent = '机器人未启用。';
|
||||
}
|
||||
if (empty($extras['api_key'])) {
|
||||
$extras['api_key'] = Base::strRandom(6);
|
||||
}
|
||||
}
|
||||
if (empty($extras['api_key'])) {
|
||||
$errorContent = '机器人未启用。';
|
||||
}
|
||||
if (in_array($this->client['platform'], ['win', 'mac', 'web']) && !Base::judgeClientVersion("0.41.11", $this->client['version'])) {
|
||||
$errorContent = '当前客户端版本低(所需版本≥v0.41.11)。';
|
||||
}
|
||||
|
||||
if ($msg->reply_id > 0) {
|
||||
$replyMsg = WebSocketDialogMsg::find($msg->reply_id);
|
||||
$replyCommand = '';
|
||||
if ($replyMsg) {
|
||||
$replyCommand = $this->extractCommand($replyMsg);
|
||||
if ($replyCommand) {
|
||||
$replyCommand = "<quoted>" . Base::cutStr($replyCommand, 2000) . "</quoted>\n\nThe content within the above <quoted> tags is a citation.\n\n";
|
||||
}
|
||||
$replyCommand = $this->extractReplyCommand($msg->reply_id, $botUser);
|
||||
if (Base::isError($replyCommand)) {
|
||||
$errorContent = $replyCommand['msg'];
|
||||
} else {
|
||||
$command = <<<EOF
|
||||
<quoted_content>
|
||||
{$replyCommand['data']}
|
||||
</quoted_content>
|
||||
|
||||
The content within the above quoted_content tags is a citation.
|
||||
|
||||
{$command}
|
||||
EOF;
|
||||
}
|
||||
$command = $replyCommand . $command;
|
||||
}
|
||||
$this->AIGenerateSystemMessageOrBeforeText($msg->userid, $dialog, $extras);
|
||||
$this->AIGenerateSystemMessage($msg->userid, $dialog, $extras);
|
||||
$webhookUrl = "{$serverUrl}/ai/chat";
|
||||
} else {
|
||||
// 用户机器人
|
||||
if ($botUser->isUserBot() && str_starts_with($command, '/')) {
|
||||
// 用户机器人不处理指令类型命令
|
||||
return;
|
||||
}
|
||||
|
||||
if ($msg->reply_id > 0) {
|
||||
$replyCommand = $this->extractReplyCommand($msg->reply_id, $botUser);
|
||||
if (Base::isSuccess($replyCommand)) {
|
||||
$replyText = $replyCommand['data'] ?: '';
|
||||
}
|
||||
}
|
||||
$userBot = UserBot::whereBotId($botUser->userid)->first();
|
||||
$webhookUrl = $userBot?->webhook_url;
|
||||
}
|
||||
@@ -471,6 +504,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
try {
|
||||
$data = [
|
||||
'text' => $command,
|
||||
'reply_text' => $replyText,
|
||||
'token' => User::generateToken($botUser),
|
||||
'dialog_id' => $dialog->id,
|
||||
'dialog_type' => $dialog->type,
|
||||
@@ -532,15 +566,18 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
/**
|
||||
* 提取消息指令(提取消息内容)
|
||||
* @param WebSocketDialogMsg $msg
|
||||
* @param User $botUser
|
||||
* @param bool $mention
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
private function extractCommand(WebSocketDialogMsg $msg, bool $mention = false)
|
||||
private function extractCommand(WebSocketDialogMsg $msg, User $botUser, bool $mention = false)
|
||||
{
|
||||
if ($msg->type !== 'text') {
|
||||
return '';
|
||||
}
|
||||
$original = $msg->msg['text'];
|
||||
|
||||
$original = $msg->msg['text'] ?: '';
|
||||
if ($mention) {
|
||||
$original = preg_replace("/<span class=\"mention user\" data-id=\"(\d+)\">(.*?)<\/span>/", "", $original);
|
||||
}
|
||||
@@ -549,26 +586,127 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
if (str_starts_with($command, '%3A.')) {
|
||||
$command = ":" . substr($command, 4);
|
||||
}
|
||||
return $command;
|
||||
}
|
||||
|
||||
if ($botUser->isAiBot()) {
|
||||
// AI 机器人
|
||||
$contents = [];
|
||||
if (preg_match_all("/<span class=\"mention task\" data-id=\"(\d+)\">(.*?)<\/span>/", $original, $match)) {
|
||||
// 任务
|
||||
$taskIds = Base::newIntval($match[1]);
|
||||
foreach ($taskIds as $index => $taskId) {
|
||||
$taskInfo = ProjectTask::with(['content'])->whereId($taskId)->first();
|
||||
if (!$taskInfo) {
|
||||
throw new Exception("任务不存在或已被删除");
|
||||
}
|
||||
$taskName = addslashes($taskInfo->name) . " (ID:{$taskId})";
|
||||
$taskContext = implode("\n", $taskInfo->AIContext());
|
||||
$contents[] = "<task_content path=\"{$taskName}\">\n{$taskContext}\n</task_content>";
|
||||
$original = str_replace($match[0][$index], "'{$taskName}' (see below for task_content tag)", $original);
|
||||
}
|
||||
}
|
||||
if (preg_match_all("/<a class=\"mention ([^'\"]*)\" href=\"([^\"']+?)\"[^>]*?>[~%]([^>]*)<\/a>/", $original, $match)) {
|
||||
// 文件、报告
|
||||
$urlPaths = $match[2];
|
||||
foreach ($urlPaths as $index => $urlPath) {
|
||||
$pathTag = null;
|
||||
$pathName = null;
|
||||
$pathContent = null;
|
||||
// 文件
|
||||
if (preg_match("/single\/file\/(.*?)$/", $urlPath, $fileMatch)) {
|
||||
$fileInfo = FileContent::idOrCodeToContent($fileMatch[1]);
|
||||
if (!$fileInfo || !isset($fileInfo->content['url'])) {
|
||||
throw new Exception("文件不存在或已被删除");
|
||||
}
|
||||
$urlPath = public_path($fileInfo->content['url']);
|
||||
if (!file_exists($urlPath)) {
|
||||
throw new Exception("文件不存在或已被删除");
|
||||
}
|
||||
$fileResult = TextExtractor::extractFile($urlPath);
|
||||
if (Base::isError($fileResult)) {
|
||||
throw new Exception("文件读取失败:" . $fileResult['msg']);
|
||||
}
|
||||
$pathTag = "file_content";
|
||||
$pathName = addslashes($match[3][$index]) . " (ID:{$fileInfo->id})";
|
||||
$pathContent = $fileResult['data'];
|
||||
}
|
||||
// 报告
|
||||
elseif (preg_match("/single\/report\/detail\/(.*?)$/", $urlPath, $reportMatch)) {
|
||||
$reportInfo = Report::idOrCodeToContent($reportMatch[1]);
|
||||
if (!$reportInfo) {
|
||||
throw new Exception("报告不存在或已被删除");
|
||||
}
|
||||
$pathTag = "report_content";
|
||||
$pathName = addslashes($match[3][$index]) . " (ID:{$reportInfo->id})";
|
||||
$pathContent = $reportInfo->content;
|
||||
}
|
||||
if ($pathTag) {
|
||||
$contents[] = "<{$pathTag} path=\"{$pathName}\">\n{$pathContent}\n</{$pathTag}>";
|
||||
$original = str_replace($match[0][$index], "'{$pathName}' (see below for {$pathTag} tag)", $original);
|
||||
}
|
||||
}
|
||||
}
|
||||
$original = Base::html2markdown($original);
|
||||
if ($contents) {
|
||||
// 添加tag内容
|
||||
$original .= "\n\n" . implode("\n\n", $contents);
|
||||
}
|
||||
return $original;
|
||||
} elseif ($botUser->isUserBot()) {
|
||||
// 用户机器人
|
||||
return Base::html2markdown($original);
|
||||
} else {
|
||||
$command = trim(strip_tags($original));
|
||||
// 其他机器人(系统)
|
||||
return trim(strip_tags($original));
|
||||
}
|
||||
if (empty($command)) {
|
||||
return '';
|
||||
}
|
||||
return $command;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成AI系统提示词或前置消息
|
||||
* 提取回复消息指令
|
||||
* @param $id
|
||||
* @param User $botUser
|
||||
* @return array
|
||||
*/
|
||||
private function extractReplyCommand($id, User $botUser)
|
||||
{
|
||||
$replyMsg = WebSocketDialogMsg::find($id);
|
||||
$replyCommand = null;
|
||||
if ($replyMsg) {
|
||||
switch ($replyMsg->type) {
|
||||
case 'text':
|
||||
try {
|
||||
$replyCommand = $this->extractCommand($replyMsg, $botUser);
|
||||
} catch (Exception) {
|
||||
return Base::retError('error', "引用消息解析失败。");
|
||||
}
|
||||
break;
|
||||
case 'file':
|
||||
if ($botUser->isAiBot()) {
|
||||
$msgData = Base::json2array($replyMsg->getRawOriginal('msg'));
|
||||
$fileResult = TextExtractor::extractFile(public_path($msgData['path']));
|
||||
if (Base::isError($fileResult)) {
|
||||
return Base::retError('error', $fileResult['msg']);
|
||||
} else {
|
||||
$replyCommand = $fileResult['data'];
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Base::retSuccess('success', $replyCommand);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成AI系统提示词
|
||||
* @param int|null $userid
|
||||
* @param WebSocketDialog $dialog
|
||||
* @param array $extras
|
||||
* @return void
|
||||
*/
|
||||
private function AIGenerateSystemMessageOrBeforeText(int|null $userid, WebSocketDialog $dialog, array &$extras)
|
||||
private function AIGenerateSystemMessage(int|null $userid, WebSocketDialog $dialog, array &$extras)
|
||||
{
|
||||
$system_message = null;
|
||||
$before_text = [];
|
||||
$system_messages = [];
|
||||
switch ($dialog->type) {
|
||||
case "user":
|
||||
$aiPrompt = WebSocketDialogConfig::where([
|
||||
@@ -577,7 +715,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
'type' => 'ai_prompt',
|
||||
])->value('value');
|
||||
if ($aiPrompt) {
|
||||
$system_message = $aiPrompt;
|
||||
$extras['system_message'] = $aiPrompt;
|
||||
}
|
||||
break;
|
||||
case "group":
|
||||
@@ -587,16 +725,14 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
case 'project':
|
||||
$projectInfo = Project::whereDialogId($dialog->id)->first();
|
||||
if ($projectInfo) {
|
||||
$projectText = "当前我在项目【{$projectInfo->name}】中";
|
||||
if ($projectInfo->archived_at) {
|
||||
$projectText .= ",此项目已经归档";
|
||||
}
|
||||
$before_text[] = $projectText;
|
||||
if ($projectInfo->desc) {
|
||||
$before_text[] = "项目描述:{$projectInfo->desc}";
|
||||
}
|
||||
$before_text[] = <<<EOF
|
||||
如果你判断我想要添加任务,请按照以下格式回复:
|
||||
$projectDesc = $projectInfo->desc ?: "-";
|
||||
$projectStatus = $projectInfo->archived_at ? '已归档' : '正在进行中';
|
||||
$system_messages[] = <<<EOF
|
||||
当前我在项目【{$projectInfo->name}】中
|
||||
项目描述:{$projectDesc}
|
||||
项目状态:{$projectStatus}
|
||||
|
||||
如果你判断我想要或需要添加任务,请按照以下格式回复:
|
||||
|
||||
::: create-task-list
|
||||
title: 任务标题1
|
||||
@@ -611,48 +747,14 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
case 'task':
|
||||
$taskInfo = ProjectTask::with(['content'])->whereDialogId($dialog->id)->first();
|
||||
if ($taskInfo) {
|
||||
$taskText = "当前我在任务【{$taskInfo->name}】中";
|
||||
if ($taskInfo->archived_at) {
|
||||
$taskText .= ",此任务已经归档";
|
||||
} elseif ($taskInfo->complete_at) {
|
||||
$taskText .= ",此任务已经完成";
|
||||
} elseif ($taskInfo->end_at && Carbon::parse($taskInfo->end_at)->lt(Carbon::now())) {
|
||||
$taskText .= ",此任务已经过期";
|
||||
}
|
||||
$before_text[] = $taskText;
|
||||
if ($taskInfo->content) {
|
||||
$taskDesc = $taskInfo->content?->getContentInfo();
|
||||
if ($taskDesc) {
|
||||
$converter = new HtmlConverter(['strip_tags' => true]);
|
||||
$descContent = Base::cutStr($converter->convert($taskDesc['content']), 2000);
|
||||
$before_text[] = <<<EOF
|
||||
任务描述:
|
||||
```md
|
||||
{$descContent}
|
||||
```
|
||||
EOF;
|
||||
}
|
||||
}
|
||||
$subTask = ProjectTask::select(['id', 'name', 'complete_at', 'end_at'])->whereParentId($taskInfo->id)->get();
|
||||
if ($subTask->isNotEmpty()) {
|
||||
$subTaskContent = $subTask->map(function($item) {
|
||||
$status = "";
|
||||
if ($item->complete_at) {
|
||||
$status = " (已完成)";
|
||||
} elseif ($item->end_at && Carbon::parse($item->end_at)->lt(Carbon::now())) {
|
||||
$status = " (已过期)";
|
||||
}
|
||||
return " - {$item->name} {$status}";
|
||||
})->join("\n");
|
||||
if ($subTaskContent) {
|
||||
$before_text[] = <<<EOF
|
||||
子任务列表:
|
||||
{$subTaskContent}
|
||||
EOF;
|
||||
}
|
||||
}
|
||||
$before_text[] = <<<EOF
|
||||
如果你判断我想要添加子任务,请按照以下格式回复:
|
||||
$taskContext = implode("\n", $taskInfo->AIContext());
|
||||
$system_messages[] = <<<EOF
|
||||
当前我在任务【{$taskInfo->name}】中
|
||||
当前时间:{$taskInfo->updated_at}
|
||||
任务ID:{$taskInfo->id}
|
||||
{$taskContext}
|
||||
|
||||
如果你判断我想要或需要添加子任务,请按照以下格式回复:
|
||||
|
||||
::: create-subtask-list
|
||||
title: 子任务标题1
|
||||
@@ -661,17 +763,23 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
EOF;
|
||||
}
|
||||
break;
|
||||
case 'department':
|
||||
$userDepartment = UserDepartment::whereDialogId($dialog->id)->first();
|
||||
if ($userDepartment) {
|
||||
$system_messages[] = "当前我在【{$userDepartment->name}】的部门群聊中";
|
||||
}
|
||||
break;
|
||||
case 'all':
|
||||
$before_text[] = "当前我团队【全体成员】的群聊中";
|
||||
$system_messages[] = "当前我在【全体成员】的群聊中";
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if ($system_message) {
|
||||
$extras['system_message'] = $system_message;
|
||||
if ($extras['system_message']) {
|
||||
array_unshift($system_messages, $extras['system_message']);
|
||||
}
|
||||
if ($before_text) {
|
||||
$extras['before_text'] = Base::newTrim($before_text);
|
||||
if ($system_messages) {
|
||||
$extras['system_message'] = implode("\n\n====\n\n", Base::newTrim($system_messages));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Tasks;
|
||||
use App\Models\File;
|
||||
use App\Models\TaskWorker;
|
||||
use App\Models\Tmp;
|
||||
use App\Models\UserDevice;
|
||||
use App\Models\WebSocketTmpMsg;
|
||||
use App\Module\Base;
|
||||
use Carbon\Carbon;
|
||||
@@ -33,89 +34,75 @@ class DeleteTmpTask extends AbstractTask
|
||||
public function start()
|
||||
{
|
||||
switch ($this->data) {
|
||||
/**
|
||||
* 表pre_tmp_msgs
|
||||
*/
|
||||
case 'wg_tmp_msgs':
|
||||
{
|
||||
WebSocketTmpMsg::where('created_at', '<', Carbon::now()->subHours($this->hours))
|
||||
->orderBy('id')
|
||||
->chunk(500, function ($msgs) {
|
||||
/** @var WebSocketTmpMsg $msg */
|
||||
foreach ($msgs as $msg) {
|
||||
$msg->delete();
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
/**
|
||||
* 表pre_tmp
|
||||
*/
|
||||
case 'tmp':
|
||||
{
|
||||
Tmp::where('created_at', '<', Carbon::now()->subHours($this->hours))
|
||||
->orderBy('id')
|
||||
->chunk(500, function ($tmps) {
|
||||
/** @var Tmp $tmp */
|
||||
foreach ($tmps as $tmp) {
|
||||
$tmp->delete();
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
/**
|
||||
* 表pre_task_worker
|
||||
*/
|
||||
case 'task_worker':
|
||||
{
|
||||
TaskWorker::onlyTrashed()
|
||||
->where('deleted_at', '<', Carbon::now()->subHours($this->hours))
|
||||
->orderBy('id')
|
||||
->forceDelete();
|
||||
}
|
||||
break;
|
||||
|
||||
/**
|
||||
* 表pre_file
|
||||
*/
|
||||
case 'file':
|
||||
{
|
||||
$day = intval(env("AUTO_EMPTY_FILE_RECYCLE", 365));
|
||||
if ($day <= 0) {
|
||||
return;
|
||||
}
|
||||
File::onlyTrashed()
|
||||
->where('deleted_at', '<', Carbon::now()->subHours($day))
|
||||
->orderBy('id')
|
||||
->chunk(500, function ($files) {
|
||||
/** @var File $file */
|
||||
foreach ($files as $file) {
|
||||
$file->forceDeleteFile();
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
/**
|
||||
* tmp_file 删除临时文件
|
||||
*/
|
||||
case 'tmp_file':
|
||||
{
|
||||
$day = intval(env("AUTO_EMPTY_TEMP_FILE", 30));
|
||||
if ($day <= 0) {
|
||||
return;
|
||||
}
|
||||
$files = Base::recursiveFiles(public_path('uploads/tmp'));
|
||||
foreach ($files as $file) {
|
||||
$time = @filemtime($file);
|
||||
if ($time && $time < time() - 3600 * 24 * $day) {
|
||||
unlink($file);
|
||||
case 'tmp_msgs':
|
||||
WebSocketTmpMsg::where('created_at', '<', Carbon::now()->subHours($this->hours))
|
||||
->orderBy('id')
|
||||
->chunk(500, function ($msgs) {
|
||||
/** @var WebSocketTmpMsg $msg */
|
||||
foreach ($msgs as $msg) {
|
||||
$msg->delete();
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'tmp':
|
||||
Tmp::where('created_at', '<', Carbon::now()->subHours($this->hours))
|
||||
->orderBy('id')
|
||||
->chunk(500, function ($tmps) {
|
||||
/** @var Tmp $tmp */
|
||||
foreach ($tmps as $tmp) {
|
||||
$tmp->delete();
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'task_worker':
|
||||
TaskWorker::onlyTrashed()
|
||||
->where('deleted_at', '<', Carbon::now()->subHours($this->hours))
|
||||
->orderBy('id')
|
||||
->forceDelete();
|
||||
break;
|
||||
|
||||
case 'file':
|
||||
$day = intval(env("AUTO_EMPTY_FILE_RECYCLE", 365));
|
||||
if ($day <= 0) {
|
||||
return;
|
||||
}
|
||||
File::onlyTrashed()
|
||||
->where('deleted_at', '<', Carbon::now()->subHours($day))
|
||||
->orderBy('id')
|
||||
->chunk(500, function ($files) {
|
||||
/** @var File $file */
|
||||
foreach ($files as $file) {
|
||||
$file->forceDeleteFile();
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'tmp_file':
|
||||
$day = intval(env("AUTO_EMPTY_TEMP_FILE", 30));
|
||||
if ($day <= 0) {
|
||||
return;
|
||||
}
|
||||
$files = Base::recursiveFiles(public_path('uploads/tmp'));
|
||||
foreach ($files as $file) {
|
||||
$time = @filemtime($file);
|
||||
if ($time && $time < time() - 3600 * 24 * $day) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'user_device':
|
||||
UserDevice::where('expired_at', '<', Carbon::now()->subHours($this->hours))
|
||||
->orderBy('id')
|
||||
->chunk(500, function ($devices) {
|
||||
/** @var UserDevice $device */
|
||||
foreach ($devices as $device) {
|
||||
UserDevice::forget($device);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -119,14 +119,20 @@ class WebSocketDialogMsgTask extends AbstractTask
|
||||
$mention = array_intersect([0, $userid], $mentions) ? 1 : 0;
|
||||
$silence = $mention ? false : $silence;
|
||||
$dot = $msg->type === 'record' ? 1 : 0;
|
||||
WebSocketDialogMsgRead::createInstance([
|
||||
$msgRead = WebSocketDialogMsgRead::createInstance([
|
||||
'dialog_id' => $msg->dialog_id,
|
||||
'msg_id' => $msg->id,
|
||||
'userid' => $userid,
|
||||
'mention' => $mention,
|
||||
'silence' => $silence,
|
||||
'dot' => $dot,
|
||||
])->saveOrIgnore();
|
||||
]);
|
||||
if ($msgRead->saveOrIgnore()) {
|
||||
if ($dialog->session_id && $dialog->session_id != $msg->session_id) {
|
||||
$msgRead->read_at = Carbon::now();
|
||||
$msgRead->save();
|
||||
}
|
||||
}
|
||||
$array[$userid] = [
|
||||
'userid' => $userid,
|
||||
'mention' => $mention,
|
||||
|
||||
39
app/Tasks/ZincSearchSyncTask.php
Normal file
39
app/Tasks/ZincSearchSyncTask.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tasks;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/**
|
||||
* 同步聊天数据到ZincSearch
|
||||
*/
|
||||
class ZincSearchSyncTask extends AbstractTask
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function start()
|
||||
{
|
||||
// 120分钟执行一次
|
||||
$time = intval(Cache::get("ZincSearchSyncTask:Time"));
|
||||
if (time() - $time < 120 * 60) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 执行开始,120分钟后缓存标记失效
|
||||
Cache::put("ZincSearchSyncTask:Time", time(), Carbon::now()->addMinutes(120));
|
||||
|
||||
// 开始执行同步
|
||||
@shell_exec("php /var/www/artisan zinc:sync-user-msg --i");
|
||||
|
||||
// 执行完成,5分钟后缓存标记失效(5分钟任务可重复执行)
|
||||
Cache::put("ZincSearchSyncTask:Time", time(), Carbon::now()->addMinutes(5));
|
||||
}
|
||||
|
||||
public function end()
|
||||
{
|
||||
}
|
||||
}
|
||||
54
bin/https
54
bin/https
@@ -142,6 +142,7 @@ install() {
|
||||
if /root/.acme.sh/acme.sh --installcert -d "${domain}" --fullchainpath "${sslPath}/${domain}.crt" --keypath "${sslPath}/${domain}.key" --ecc --force; then
|
||||
success "SSL 证书配置成功"
|
||||
sleep 2
|
||||
cp -r /root/.acme.sh/${domain}_ecc/*.conf ${sslPath}/
|
||||
fi
|
||||
else
|
||||
error "SSL 证书生成失败"
|
||||
@@ -165,5 +166,54 @@ error_page 497 https://\$host\$request_uri;
|
||||
EOF
|
||||
}
|
||||
|
||||
check
|
||||
install
|
||||
UPDATE_LOG="$(dirname "$PWD")/docker/nginx/site/ssl/update.log"
|
||||
SSL_PATH="$(dirname "$PWD")/docker/nginx/site/ssl"
|
||||
upgrade_cert(){
|
||||
curl https://get.acme.sh | sh
|
||||
if [[ 0 -ne $? ]]; then
|
||||
echo "安装证书更新脚本失败"
|
||||
echo $(date)": 安装证书更新脚本失败" >> ${UPDATE_LOG}
|
||||
exit 1
|
||||
fi
|
||||
file=$1
|
||||
domain=$(basename "$file" .key)
|
||||
old_crt_md5=$(md5sum ${SSL_PATH}/${domain}.crt| awk '{print $1}')
|
||||
/root/.acme.sh/acme.sh --renew --standalone -d ${domain} --fullchainpath "${SSL_PATH}/${domain}.crt" --keypath "${SSL_PATH}/${domain}.key" --ecc --force
|
||||
new_crt_md5=$(md5sum ${SSL_PATH}/${domain}.crt| awk '{print $1}')
|
||||
if [ "${old_key_md5}" == "${new_key_md5}" ]; then
|
||||
echo "${domain} 证书更新脚本失败"
|
||||
echo $(date)": ${domain} 证书更新失败" >> ${UPDATE_LOG}
|
||||
echo $(date)": ${old_crt_md5} == ${new_crt_md5}" >> ${UPDATE_LOG}
|
||||
else
|
||||
echo "${domain} 证书更新脚本成功"
|
||||
echo $(date)": ${domain} 证书更新成功" >> ${UPDATE_LOG}
|
||||
fi
|
||||
}
|
||||
|
||||
check_expire(){
|
||||
apk add --no-cache openssl socat
|
||||
find ${SSL_PATH} -type f -name "*.key" | while read -r file; do
|
||||
CERT_PATH=$file
|
||||
expiry_date=$(openssl x509 -enddate -noout -in "$CERT_PATH" | cut -d= -f2)
|
||||
expiry_timestamp=$(date -d "$expiry_date" +%s)
|
||||
current_timestamp=$(date +%s)
|
||||
days_remaining=$(( (expiry_timestamp - current_timestamp) / 86400 ))
|
||||
echo "剩余时间${days_remaining}天" >> ${UPDATE_LOG}
|
||||
if [ "$days_remaining" -lt 30 ]; then
|
||||
upgrade_cert $file
|
||||
fi
|
||||
done
|
||||
}
|
||||
case "${1}" in
|
||||
"install")
|
||||
check
|
||||
install
|
||||
;;
|
||||
"renew")
|
||||
check_expire
|
||||
;;
|
||||
*)
|
||||
echo "test"
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
4
bin/version.js
vendored
4
bin/version.js
vendored
File diff suppressed because one or more lines are too long
123
cmd
123
cmd
@@ -156,9 +156,9 @@ run_electron() {
|
||||
npm install
|
||||
fi
|
||||
if [ ! -d "./electron/node_modules" ]; then
|
||||
pushd electron
|
||||
pushd electron || exit
|
||||
npm install
|
||||
popd
|
||||
popd || exit
|
||||
fi
|
||||
#
|
||||
if [ -d "./electron/dist" ]; then
|
||||
@@ -178,8 +178,9 @@ run_electron() {
|
||||
|
||||
run_exec() {
|
||||
local container=$1
|
||||
local cmd=$2
|
||||
local name=`docker_name $container`
|
||||
shift 1
|
||||
local cmd=$@
|
||||
local name=$(docker_name "$container")
|
||||
if [ -z "$name" ]; then
|
||||
error "没有找到 $container 容器!"
|
||||
exit 1
|
||||
@@ -322,15 +323,26 @@ https_auto() {
|
||||
if [[ "$restart_nginx" == "y" ]]; then
|
||||
$COMPOSE up -d
|
||||
fi
|
||||
docker run -it --rm -v $(pwd):/work nginx:alpine sh "/work/bin/https"
|
||||
docker run -it --rm -v $(pwd):/work nginx:alpine sh /work/bin/https install
|
||||
if [[ 0 -eq $? ]]; then
|
||||
run_exec nginx "nginx -s reload"
|
||||
fi
|
||||
new_job="* 6 * * * docker run -it --rm -v $(pwd):/work nginx:alpine sh /work/bin/https renew"
|
||||
current_crontab=$(crontab -l 2>/dev/null)
|
||||
if ! echo "$current_crontab" | grep -v "https renew"; then
|
||||
echo "任务已存在,无需添加。"
|
||||
else
|
||||
crontab -l |{
|
||||
cat
|
||||
echo "$new_job"
|
||||
} | crontab -
|
||||
echo "任务已添加。"
|
||||
fi
|
||||
}
|
||||
|
||||
env_get() {
|
||||
local key=$1
|
||||
local value=`cat ${cur_path}/.env | grep "^$key=" | awk -F '=' '{print $2}'`
|
||||
local value=`cat ${cur_path}/.env | grep "^$key=" | awk -F '=' '{print $2}' | tr -d '\r\n'`
|
||||
echo "$value"
|
||||
}
|
||||
|
||||
@@ -405,14 +417,50 @@ if [ $# -gt 0 ]; then
|
||||
rm -rf vendor
|
||||
rm -rf composer.lock
|
||||
fi
|
||||
mkdir -p "${cur_path}/docker/log/supervisor"
|
||||
mkdir -p "${cur_path}/docker/mysql/data"
|
||||
chmod -R 775 "${cur_path}/docker/log/supervisor"
|
||||
chmod -R 775 "${cur_path}/docker/mysql/data"
|
||||
# 目录权限
|
||||
volumes=(
|
||||
"bootstrap/cache"
|
||||
"docker"
|
||||
"public"
|
||||
"storage"
|
||||
)
|
||||
cmda=""
|
||||
cmdb=""
|
||||
for vol in "${volumes[@]}"; do
|
||||
tmp_path="${cur_path}/${vol}"
|
||||
mkdir -p "${tmp_path}"
|
||||
chmod -R 775 "${tmp_path}"
|
||||
rm -f "${tmp_path}/dootask.lock"
|
||||
cmda="${cmda} -v ${tmp_path}:/usr/share/${vol}"
|
||||
cmdb="${cmdb} touch /usr/share/${vol}/dootask.lock &&"
|
||||
done
|
||||
# 目录权限检测
|
||||
remaining=10
|
||||
while true; do
|
||||
((remaining=$remaining-1))
|
||||
writable="yes"
|
||||
docker run --rm ${cmda} nginx:alpine sh -c "${cmdb} touch /usr/share/docker/dootask.lock" &> /dev/null
|
||||
for vol in "${volumes[@]}"; do
|
||||
if [ ! -f "${vol}/dootask.lock" ]; then
|
||||
if [ $remaining -lt 0 ]; then
|
||||
error "目录【${vol}】权限不足!"
|
||||
exit 1
|
||||
else
|
||||
writable="no"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [ "$writable" == "yes" ]; then
|
||||
break
|
||||
else
|
||||
sleep 3
|
||||
fi
|
||||
done
|
||||
# 启动容器
|
||||
[[ "$(arg_get port)" -gt 0 ]] && env_set APP_PORT "$(arg_get port)"
|
||||
$COMPOSE up php -d
|
||||
# 安装composer依赖
|
||||
# 安装PHP依赖
|
||||
run_exec php "composer install"
|
||||
if [ ! -f "${cur_path}/vendor/autoload.php" ]; then
|
||||
run_exec php "composer config repo.packagist composer https://packagist.phpcomposer.com"
|
||||
@@ -424,50 +472,37 @@ if [ $# -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
[[ -z "$(env_get APP_KEY)" ]] && run_exec php "php artisan key:generate"
|
||||
# 设置生产模式
|
||||
switch_debug "false"
|
||||
# 检查数据库
|
||||
remaining=20
|
||||
while [ ! -f "${cur_path}/docker/mysql/data/$(env_get DB_DATABASE)/db.opt" ]; do
|
||||
((remaining=$remaining-1))
|
||||
if [ $remaining -lt 0 ]; then
|
||||
error "数据库初始化失败!"
|
||||
exit 1
|
||||
fi
|
||||
chmod -R 775 "${cur_path}/docker/mysql/data"
|
||||
done
|
||||
# 数据库迁移
|
||||
remaining=20
|
||||
while [ ! -f "${cur_path}/docker/mysql/data/$(env_get DB_DATABASE)/$(env_get DB_PREFIX)migrations.ibd" ]; do
|
||||
((remaining=$remaining-1))
|
||||
if [ $remaining -lt 0 ]; then
|
||||
error "数据库安装失败!"
|
||||
exit 1
|
||||
fi
|
||||
sleep 3
|
||||
run_exec php "php artisan migrate --seed"
|
||||
done
|
||||
# 设置初始化密码
|
||||
res=`run_exec mariadb "sh /etc/mysql/repassword.sh"`
|
||||
$COMPOSE up -d
|
||||
restart_php
|
||||
run_exec php "php artisan migrate --seed"
|
||||
# 启动其他容器
|
||||
$COMPOSE up -d --remove-orphans
|
||||
success "安装完成"
|
||||
info "地址: http://${GreenBG}127.0.0.1:$(env_get APP_PORT)${Font}"
|
||||
info "$res"
|
||||
# 设置初始化密码
|
||||
run_exec mariadb "sh /etc/mysql/repassword.sh"
|
||||
elif [[ "$1" == "update" ]]; then
|
||||
shift 1
|
||||
if [[ "$@" != "nobackup" ]]; then
|
||||
run_mysql backup
|
||||
fi
|
||||
if [[ -z "$(arg_get local)" ]]; then
|
||||
git fetch --all
|
||||
git reset --hard origin/$(git branch | sed -n -e 's/^\* \(.*\)/\1/p')
|
||||
current_branch=$(git branch | sed -n -e 's/^\* \(.*\)/\1/p')
|
||||
db_changes=$(git diff --name-only HEAD..origin/$current_branch | grep -E "^database/")
|
||||
if [[ -n "$db_changes" ]]; then
|
||||
info "数据库有迁移变动,执行数据库备份..."
|
||||
run_mysql backup
|
||||
fi
|
||||
git reset --hard origin/$current_branch
|
||||
git pull
|
||||
run_exec php "composer update"
|
||||
else
|
||||
info "执行数据库备份..."
|
||||
run_mysql backup
|
||||
fi
|
||||
run_exec php "php artisan migrate"
|
||||
run_exec nginx "nginx -s reload"
|
||||
restart_php
|
||||
$COMPOSE up -d
|
||||
$COMPOSE up -d --remove-orphans
|
||||
elif [[ "$1" == "uninstall" ]]; then
|
||||
shift 1
|
||||
read -rp "确定要卸载(含:删除容器、数据库、日志)吗?(Y/n): " uninstall
|
||||
@@ -513,7 +548,7 @@ if [ $# -gt 0 ]; then
|
||||
success "修改成功"
|
||||
elif [[ "$1" == "repassword" ]]; then
|
||||
shift 1
|
||||
run_exec mariadb "sh /etc/mysql/repassword.sh \"$@\""
|
||||
run_exec mariadb "sh /etc/mysql/repassword.sh $@"
|
||||
elif [[ "$1" == "serve" ]] || [[ "$1" == "dev" ]] || [[ "$1" == "development" ]]; then
|
||||
shift 1
|
||||
run_compile dev
|
||||
@@ -539,9 +574,9 @@ if [ $# -gt 0 ]; then
|
||||
elif [[ "$1" == "npm" ]]; then
|
||||
shift 1
|
||||
npm $@
|
||||
cd electron
|
||||
pushd electron || exit
|
||||
npm $@
|
||||
cd ..
|
||||
popd || exit
|
||||
docker run --rm -it -v ${cur_path}/resources/mobile:/work -w /work --entrypoint=/bin/bash node:16 -c "npm $@"
|
||||
elif [[ "$1" == "doc" ]]; then
|
||||
shift 1
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"php": "^8.0",
|
||||
"ext-curl": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-fileinfo": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-imagick": "*",
|
||||
"ext-json": "*",
|
||||
@@ -32,10 +33,14 @@
|
||||
"league/html-to-markdown": "^5.1",
|
||||
"maatwebsite/excel": "^3.1.31",
|
||||
"madnest/madzipper": "^v1.1.0",
|
||||
"matomo/device-detector": "^6.4",
|
||||
"mews/captcha": "^3.2.6",
|
||||
"orangehill/iseed": "^3.0.1",
|
||||
"overtrue/pinyin": "^4.0",
|
||||
"phpoffice/phppresentation": "^1.1",
|
||||
"phpoffice/phpword": "^1.3",
|
||||
"predis/predis": "^1.1.7",
|
||||
"smalot/pdfparser": "^2.11",
|
||||
"symfony/mailer": "^6.0"
|
||||
},
|
||||
"require-dev": {
|
||||
@@ -84,7 +89,10 @@
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
|
||||
1152
composer.lock
generated
1152
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UpdateProjectTasksSubtaskVisibility extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
$prefix = DB::getTablePrefix();
|
||||
$now = Carbon::now();
|
||||
DB::statement("
|
||||
UPDATE {$prefix}project_tasks AS subtask
|
||||
INNER JOIN {$prefix}project_tasks AS parent ON subtask.parent_id = parent.id
|
||||
SET
|
||||
subtask.visibility = parent.visibility,
|
||||
subtask.updated_at = '{$now}'
|
||||
WHERE subtask.parent_id > 0
|
||||
");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
// No need for down operation as this is a data correction
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddBotToWebSocketDialogUsersTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('web_socket_dialog_users', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('web_socket_dialog_users', 'bot')) {
|
||||
$table->tinyInteger('bot')->nullable()->default(0)->after('userid')->comment('是否机器人');
|
||||
$table->index('bot');
|
||||
}
|
||||
});
|
||||
|
||||
// 获取表前缀
|
||||
$prefix = DB::getTablePrefix();
|
||||
|
||||
// 使用原生SQL更新数据
|
||||
/** @noinspection SqlNoDataSourceInspection */
|
||||
DB::statement("
|
||||
UPDATE {$prefix}web_socket_dialog_users du
|
||||
INNER JOIN {$prefix}users u ON u.userid = du.userid
|
||||
SET du.bot = 1
|
||||
WHERE u.bot = 1
|
||||
");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('web_socket_dialog_users', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('web_socket_dialog_users', 'bot')) {
|
||||
$table->dropIndex('bot');
|
||||
$table->dropColumn('bot');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class WebSocketDialogMsgsAddSessionId extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('web_socket_dialog_msgs', 'session_id')) {
|
||||
$table->bigInteger('session_id')->index()->nullable()->default(0)->after('dialog_type')->comment('会话ID');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
|
||||
$table->dropColumn('session_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class WebSocketDialogsAddSessionId extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('web_socket_dialogs', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('web_socket_dialogs', 'session_id')) {
|
||||
$table->bigInteger('session_id')->index()->nullable()->default(0)->after('group_type')->comment('会话ID');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('web_socket_dialogs', function (Blueprint $table) {
|
||||
$table->dropColumn('session_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Models\WebSocketDialogSession;
|
||||
use App\Module\Base;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateWebSocketDialogSessionsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
if (Schema::hasTable('web_socket_dialog_sessions')) {
|
||||
return;
|
||||
}
|
||||
Schema::create('web_socket_dialog_sessions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->bigInteger('dialog_id')->unsigned()->index()->comment('对话ID');
|
||||
$table->string('title', 255)->default('')->comment('会话标题');
|
||||
$table->timestamps();
|
||||
});
|
||||
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.email'])
|
||||
->join('web_socket_dialog_users as du', 'web_socket_dialogs.id', '=', 'du.dialog_id')
|
||||
->join('users as u', 'du.userid', '=', 'u.userid')
|
||||
->where('u.email', 'like', 'ai-%@bot.system')
|
||||
->where('web_socket_dialogs.type', 'user')
|
||||
->get();
|
||||
foreach ($list as $item) {
|
||||
$title = WebSocketDialogMsg::whereDialogId($item->id)->where('key', '!=', '')->orderBy('id')->value('key');
|
||||
$session = WebSocketDialogSession::createInstance([
|
||||
'dialog_id' => $item->id,
|
||||
'title' => $title ? Base::cutStr($title, 100) : 'Unknown',
|
||||
'created_at' => $item->created_at,
|
||||
]);
|
||||
$session->save();
|
||||
$item->session_id = $session->id;
|
||||
$item->save();
|
||||
WebSocketDialogMsg::whereDialogId($item->id)->update(['session_id' => $session->id]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('web_socket_dialog_sessions');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Module\Base;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class UpdateAiModelsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
$row = Setting::whereName('aibotSetting')->first();
|
||||
if (empty($row)) {
|
||||
return;
|
||||
}
|
||||
$value = Base::json2array($row->getRawOriginal('setting'));
|
||||
foreach ($value as $key => $item) {
|
||||
if (str_ends_with($key, '_models')) {
|
||||
$value[$key] = preg_replace('/\s*:\s*/', ' | ', $item);
|
||||
}
|
||||
}
|
||||
$row->setting = Base::array2json($value);
|
||||
$row->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
$row = Setting::whereName('aibotSetting')->first();
|
||||
if (empty($row)) {
|
||||
return;
|
||||
}
|
||||
$value = Base::json2array($row->getRawOriginal('setting'));
|
||||
foreach ($value as $key => $item) {
|
||||
if (str_ends_with($key, '_models')) {
|
||||
$value[$key] = preg_replace('/\s*\|\s*/', ': ', $item);
|
||||
}
|
||||
}
|
||||
$row->setting = Base::array2json($value);
|
||||
$row->save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Models\WebSocketDialogSession;
|
||||
use App\Module\Base;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class UpdateWebSocketDialogMsgsSessionId extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.email'])
|
||||
->join('web_socket_dialog_users as du', 'web_socket_dialogs.id', '=', 'du.dialog_id')
|
||||
->join('users as u', 'du.userid', '=', 'u.userid')
|
||||
->where('u.email', 'like', 'ai-%@bot.system')
|
||||
->where('web_socket_dialogs.type', 'user')
|
||||
->get();
|
||||
foreach ($list as $item) {
|
||||
$msg = WebSocketDialogMsg::whereDialogId($item->id)->whereSessionId(0)->orderBy('id')->first();
|
||||
if ($msg || empty($item->session_id)) {
|
||||
$title = $msg?->key;
|
||||
$session = WebSocketDialogSession::createInstance([
|
||||
'dialog_id' => $item->id,
|
||||
'title' => $title ? Base::cutStr($title, 100) : 'Unknown',
|
||||
'created_at' => $item->created_at,
|
||||
]);
|
||||
$session->save();
|
||||
if (empty($item->session_id)) {
|
||||
$item->session_id = $session->id;
|
||||
$item->save();
|
||||
}
|
||||
if ($msg) {
|
||||
WebSocketDialogMsg::whereDialogId($item->id)->whereSessionId(0)->update(['session_id' => $session->id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateReportLinksTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('report_links', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('rid')->nullable()->default(0)->index()->comment('报告ID');
|
||||
$table->integer('num')->nullable()->default(0)->comment('累计访问');
|
||||
$table->string('code')->nullable()->default('')->comment('链接码');
|
||||
$table->bigInteger('userid')->nullable()->default(0)->index()->comment('会员ID');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('report_links');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateUserDevicesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
if (Schema::hasTable('user_devices'))
|
||||
return;
|
||||
|
||||
Schema::create('user_devices', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('userid')->index()->nullable()->default(0)->comment('会员ID');
|
||||
$table->string('hash')->index()->nullable()->default('')->comment('TOKEN MD5');
|
||||
$table->longText('detail')->nullable()->comment('详细信息');
|
||||
$table->timestamp('expired_at')->nullable()->comment('过期时间');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('user_devices');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddDeviceHashToUmengAliasTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('umeng_alias', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('umeng_alias', 'device_hash')) {
|
||||
$table->string('device_hash')->index()->nullable()->after('device')->comment('设备哈希值,用于关联UserDevice表');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('umeng_alias', function (Blueprint $table) {
|
||||
$table->dropColumn('device_hash');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,13 @@ services:
|
||||
php:
|
||||
container_name: "dootask-php-${APP_ID}"
|
||||
image: "kuaifan/php:swoole-8.0.rc18"
|
||||
shm_size: "2gb"
|
||||
shm_size: 2G
|
||||
ulimits:
|
||||
core:
|
||||
soft: 0
|
||||
hard: 0
|
||||
volumes:
|
||||
- shared_data:/usr/share/dootask
|
||||
- ./docker/crontab/crontab.conf:/etc/supervisor/conf.d/crontab.conf
|
||||
- ./docker/php/php.conf:/etc/supervisor/conf.d/php.conf
|
||||
- ./docker/php/php.ini:/usr/local/etc/php/php.ini
|
||||
@@ -25,8 +26,10 @@ services:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.2"
|
||||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
mariadb:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
nginx:
|
||||
@@ -41,20 +44,18 @@ services:
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.3"
|
||||
links:
|
||||
- php
|
||||
- office
|
||||
- fileview
|
||||
- drawio-webapp
|
||||
- drawio-export
|
||||
- minder
|
||||
- okr
|
||||
- ai
|
||||
restart: unless-stopped
|
||||
|
||||
redis:
|
||||
container_name: "dootask-redis-${APP_ID}"
|
||||
image: "redis:alpine"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.4"
|
||||
@@ -73,6 +74,11 @@ services:
|
||||
MYSQL_DATABASE: "${DB_DATABASE}"
|
||||
MYSQL_USER: "${DB_USERNAME}"
|
||||
MYSQL_PASSWORD: "${DB_PASSWORD}"
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "${DB_USERNAME}", "-p${DB_PASSWORD}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.5"
|
||||
@@ -84,6 +90,7 @@ services:
|
||||
volumes:
|
||||
- ./docker/office/logs:/var/log/onlyoffice
|
||||
- ./docker/office/data:/var/www/onlyoffice/Data
|
||||
- ./docker/office/etc/documentserver/default.json:/etc/onlyoffice/documentserver/default.json
|
||||
- ./docker/office/resources/require.js:/var/www/onlyoffice/documentserver/web-apps/vendor/requirejs/require.js
|
||||
- ./docker/office/resources/common/main/resources/img/header:/var/www/onlyoffice/documentserver/web-apps/apps/common/main/resources/img/header
|
||||
- ./docker/office/resources/documenteditor/main/resources/css/app.css:/var/www/onlyoffice/documentserver/web-apps/apps/documenteditor/main/resources/css/app.css
|
||||
@@ -101,12 +108,11 @@ services:
|
||||
|
||||
fileview:
|
||||
container_name: "dootask-fileview-${APP_ID}"
|
||||
image: "kuaifan/fileview:4.2.0-SNAPSHOT-RC25"
|
||||
image: "kuaifan/fileview:4.4.0-4"
|
||||
environment:
|
||||
KK_CONTEXT_PATH: "/fileview"
|
||||
KK_OFFICE_PREVIEW_SWITCH_DISABLED: true
|
||||
KK_FILE_UPLOAD_ENABLED: true
|
||||
KK_MEDIA: "mp3,wav,mp4,mov,avi,wmv"
|
||||
KK_MEDIA_CONVERT_DISABLE: true
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.7"
|
||||
@@ -124,8 +130,6 @@ services:
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.8"
|
||||
depends_on:
|
||||
- drawio-export
|
||||
restart: unless-stopped
|
||||
|
||||
drawio-export:
|
||||
@@ -162,21 +166,18 @@ services:
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.11"
|
||||
depends_on:
|
||||
- mariadb
|
||||
restart: unless-stopped
|
||||
|
||||
ai:
|
||||
container_name: "dootask-ai-${APP_ID}"
|
||||
image: "kuaifan/dootask-ai:0.2.6"
|
||||
image: "kuaifan/dootask-ai:0.3.5"
|
||||
environment:
|
||||
REDIS_HOST: "${REDIS_HOST}"
|
||||
REDIS_PORT: "${REDIS_PORT}"
|
||||
TIMEOUT: 600
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.12"
|
||||
depends_on:
|
||||
- redis
|
||||
restart: unless-stopped
|
||||
|
||||
okr:
|
||||
@@ -184,7 +185,7 @@ services:
|
||||
image: "kuaifan/doookr:0.4.5"
|
||||
environment:
|
||||
TZ: "${TIMEZONE:-PRC}"
|
||||
DOO_TASK_URL: "http://${APP_IPPR}.3"
|
||||
DOO_TASK_URL: "http://nginx"
|
||||
MYSQL_HOST: "${DB_HOST}"
|
||||
MYSQL_PORT: "${DB_PORT}"
|
||||
MYSQL_DBNAME: "${DB_DATABASE}"
|
||||
@@ -196,8 +197,6 @@ services:
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.13"
|
||||
depends_on:
|
||||
- mariadb
|
||||
restart: unless-stopped
|
||||
|
||||
face:
|
||||
@@ -214,14 +213,30 @@ services:
|
||||
MYSQL_PASSWORD: "${DB_PASSWORD}"
|
||||
MYSQL_DB_NAME: "${DB_DATABASE}"
|
||||
DB_PREFIX: "${DB_PREFIX}"
|
||||
REPORT_API: "http://${APP_IPPR}.3:80/api/public/checkin/report"
|
||||
depends_on:
|
||||
- mariadb
|
||||
REPORT_API: "http://nginx/api/public/checkin/report"
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.14"
|
||||
restart: unless-stopped
|
||||
|
||||
search:
|
||||
container_name: "dootask-search-${APP_ID}"
|
||||
image: "public.ecr.aws/zinclabs/zincsearch:0.4.10"
|
||||
volumes:
|
||||
- search_data:/data
|
||||
environment:
|
||||
ZINC_DATA_PATH: "/data"
|
||||
ZINC_FIRST_ADMIN_USER: "${DB_USERNAME}"
|
||||
ZINC_FIRST_ADMIN_PASSWORD: "${DB_PASSWORD}"
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '2'
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.15"
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
extnetwork:
|
||||
name: "dootask-networks-${APP_ID}"
|
||||
@@ -229,3 +244,11 @@ networks:
|
||||
config:
|
||||
- subnet: "${APP_IPPR}.0/24"
|
||||
gateway: "${APP_IPPR}.1"
|
||||
|
||||
volumes:
|
||||
shared_data:
|
||||
name: "dootask-shared-data-${APP_ID}"
|
||||
redis_data:
|
||||
name: "dootask-redis-data-${APP_ID}"
|
||||
search_data:
|
||||
name: "dootask-search-data-${APP_ID}"
|
||||
|
||||
3
docker/log/.gitignore
vendored
3
docker/log/.gitignore
vendored
@@ -1 +1,2 @@
|
||||
*/
|
||||
*
|
||||
!.gitignore
|
||||
|
||||
@@ -1,27 +1,99 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# 重置用户密码脚本
|
||||
#
|
||||
# 使用方法:
|
||||
# ./repassword.sh [账号标识符] [自定义密码]
|
||||
#
|
||||
# 参数说明:
|
||||
# [账号标识符]: 可选,可以是用户ID(纯数字)或邮箱地址。不提供时默认为第一个管理员用户
|
||||
# [自定义密码]: 可选,指定要设置的新密码。不提供时会自动生成随机密码
|
||||
#
|
||||
# 使用示例:
|
||||
# ./repassword.sh # 重置第一个管理员用户密码(随机生成)
|
||||
# ./repassword.sh 123 # 重置ID=123的用户密码(随机生成)
|
||||
# ./repassword.sh user@example.com # 重置邮箱为user@example.com的用户密码(随机生成)
|
||||
# ./repassword.sh 123 newpass # 重置ID=123的用户密码为"newpass"
|
||||
# ./repassword.sh user@example.com newpass # 重置邮箱为user@example.com的用户密码为"newpass"
|
||||
#
|
||||
|
||||
new_password=$1
|
||||
account_identifier=$1
|
||||
custom_password=$2
|
||||
|
||||
GreenBG="\033[42;37m"
|
||||
RedBG="\033[41;37m"
|
||||
Font="\033[0m"
|
||||
|
||||
# 生成随机密码
|
||||
new_encrypt=$(date +%s%N | md5sum | awk '{print $1}' | cut -c 1-6)
|
||||
if [ -z "$new_password" ]; then
|
||||
if [ -z "$custom_password" ]; then
|
||||
new_password=$(date +%s%N | md5sum | awk '{print $1}' | cut -c 1-16)
|
||||
else
|
||||
new_password=$custom_password
|
||||
fi
|
||||
md5_password=$(echo -n `echo -n $new_password | md5sum | awk '{print $1}'`$new_encrypt | md5sum | awk '{print $1}')
|
||||
|
||||
content=$(echo "select \`email\` from ${MYSQL_PREFIX}users where \`userid\`=1;" | mysql -u$MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE)
|
||||
account=$(echo "$content" | sed -n '2p')
|
||||
# 构建查询条件
|
||||
if [ -z "$account_identifier" ]; then
|
||||
# 默认查询第一个管理员
|
||||
admin_query=$(echo "SELECT userid FROM ${MYSQL_PREFIX}users WHERE identity LIKE '%,admin,%' ORDER BY userid LIMIT 1;" | mysql -u$MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE)
|
||||
identifier_value=$(echo "$admin_query" | sed -n '2p')
|
||||
|
||||
if [ -z "$identifier_value" ]; then
|
||||
echo "${RedBG}错误:未找到管理员用户!${Font}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
where_field="userid"
|
||||
identifier_type="管理员ID"
|
||||
else
|
||||
# 检查是否为纯数字(ID)
|
||||
# 使用更兼容的 shell 语法检查是否为纯数字
|
||||
case "$account_identifier" in
|
||||
''|*[!0-9]*)
|
||||
# 非纯数字,视为邮箱
|
||||
where_field="email"
|
||||
identifier_type="邮箱"
|
||||
identifier_value="$account_identifier"
|
||||
;;
|
||||
*)
|
||||
# 纯数字,视为ID
|
||||
where_field="userid"
|
||||
identifier_type="ID"
|
||||
identifier_value="$account_identifier"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# 构建 WHERE 条件(为邮箱添加引号)
|
||||
if [ "$where_field" = "email" ]; then
|
||||
where_condition="where $where_field='$identifier_value'"
|
||||
else
|
||||
where_condition="where $where_field=$identifier_value"
|
||||
fi
|
||||
|
||||
# 查询用户信息
|
||||
content=$(echo "select userid,email from ${MYSQL_PREFIX}users $where_condition;" | mysql -u$MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE)
|
||||
|
||||
# 提取用户ID和邮箱
|
||||
user_id=$(echo "$content" | sed -n '2p' | awk '{print $1}')
|
||||
account=$(echo "$content" | sed -n '2p' | awk '{print $2}')
|
||||
|
||||
if [ -z "$account" ]; then
|
||||
echo "错误:账号不存在!"
|
||||
echo "${RedBG}错误:${identifier_type} '${identifier_value}' 的账号不存在!${Font}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 更新密码
|
||||
mysql -u$MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE <<EOF
|
||||
update ${MYSQL_PREFIX}users set \`encrypt\`='${new_encrypt}',\`password\`='${md5_password}' where \`userid\`=1;
|
||||
update ${MYSQL_PREFIX}users set encrypt='${new_encrypt}',password='${md5_password}' $where_condition;
|
||||
EOF
|
||||
|
||||
echo "账号: ${GreenBG}${account}${Font}"
|
||||
# 只在 identifier_type="ID" 时才输出ID
|
||||
if [ "$identifier_type" = "ID" ]; then
|
||||
echo "ID: ${GreenBG}${user_id}${Font}"
|
||||
fi
|
||||
|
||||
# 输出邮箱和密码
|
||||
echo "邮箱: ${GreenBG}${account}${Font}"
|
||||
echo "密码: ${GreenBG}${new_password}${Font}"
|
||||
|
||||
1
docker/nginx/conf.d/.gitignore
vendored
1
docker/nginx/conf.d/.gitignore
vendored
@@ -1 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
|
||||
@@ -78,129 +78,7 @@ server {
|
||||
proxy_pass http://service;
|
||||
}
|
||||
|
||||
location /fileview {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Real-PORT $remote_port;
|
||||
proxy_set_header X-Forwarded-Host $the_host;
|
||||
proxy_set_header X-Forwarded-Proto $the_scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header Scheme $scheme;
|
||||
proxy_set_header Server-Protocol $server_protocol;
|
||||
proxy_set_header Server-Name $server_name;
|
||||
proxy_set_header Server-Addr $server_addr;
|
||||
proxy_set_header Server-Port $server_port;
|
||||
proxy_pass http://fileview:8012;
|
||||
}
|
||||
|
||||
location /office/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Real-PORT $remote_port;
|
||||
proxy_set_header X-Forwarded-Host $the_host/office;
|
||||
proxy_set_header X-Forwarded-Proto $the_scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header Scheme $scheme;
|
||||
proxy_set_header Server-Protocol $server_protocol;
|
||||
proxy_set_header Server-Name $server_name;
|
||||
proxy_set_header Server-Addr $server_addr;
|
||||
proxy_set_header Server-Port $server_port;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 3600s;
|
||||
proxy_pass http://office/;
|
||||
}
|
||||
|
||||
location /drawio/webapp/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Real-PORT $remote_port;
|
||||
proxy_set_header X-Forwarded-Host $the_host/drawio/webapp;
|
||||
proxy_set_header X-Forwarded-Proto $the_scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header Scheme $scheme;
|
||||
proxy_set_header Server-Protocol $server_protocol;
|
||||
proxy_set_header Server-Name $server_name;
|
||||
proxy_set_header Server-Addr $server_addr;
|
||||
proxy_set_header Server-Port $server_port;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_pass http://drawio-webapp:8080/;
|
||||
}
|
||||
|
||||
location /drawio/export/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Real-PORT $remote_port;
|
||||
proxy_set_header X-Forwarded-Host $the_host/drawio/export;
|
||||
proxy_set_header X-Forwarded-Proto $the_scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header Scheme $scheme;
|
||||
proxy_set_header Server-Protocol $server_protocol;
|
||||
proxy_set_header Server-Name $server_name;
|
||||
proxy_set_header Server-Addr $server_addr;
|
||||
proxy_set_header Server-Port $server_port;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_pass http://drawio-export:8000/;
|
||||
}
|
||||
|
||||
location /minder/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Real-PORT $remote_port;
|
||||
proxy_set_header X-Forwarded-Host $the_host;
|
||||
proxy_set_header X-Forwarded-Proto $the_scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header Scheme $scheme;
|
||||
proxy_set_header Server-Protocol $server_protocol;
|
||||
proxy_set_header Server-Name $server_name;
|
||||
proxy_set_header Server-Addr $server_addr;
|
||||
proxy_set_header Server-Port $server_port;
|
||||
proxy_pass http://minder/;
|
||||
}
|
||||
|
||||
# 审批
|
||||
location /approve/ {
|
||||
proxy_pass http://approve/;
|
||||
}
|
||||
location /approve/api/ {
|
||||
auth_request /approveAuth;
|
||||
proxy_pass http://approve/api/;
|
||||
}
|
||||
location /approveAuth {
|
||||
internal;
|
||||
proxy_set_header Content-Type "application/json";
|
||||
proxy_set_header Content-Length $request_length;
|
||||
proxy_pass http://service/api/approve/verifyToken;
|
||||
}
|
||||
|
||||
# OKR
|
||||
location /apps/okr/ {
|
||||
proxy_pass http://okr:5566/apps/okr/;
|
||||
}
|
||||
|
||||
# AI
|
||||
location /ai/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_set_header Scheme $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_pass http://ai:5001/;
|
||||
}
|
||||
include /etc/nginx/conf.d/location/*.conf;
|
||||
}
|
||||
|
||||
include /etc/nginx/conf.d/conf.d/*.conf;
|
||||
|
||||
12
docker/nginx/location/ai.conf
Normal file
12
docker/nginx/location/ai.conf
Normal file
@@ -0,0 +1,12 @@
|
||||
location /ai/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 3600s;
|
||||
proxy_set_header Scheme $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_pass http://ai:5001/;
|
||||
}
|
||||
13
docker/nginx/location/approve.conf
Normal file
13
docker/nginx/location/approve.conf
Normal file
@@ -0,0 +1,13 @@
|
||||
location /approve/ {
|
||||
proxy_pass http://approve/;
|
||||
}
|
||||
location /approve/api/ {
|
||||
auth_request /approveAuth;
|
||||
proxy_pass http://approve/api/;
|
||||
}
|
||||
location /approveAuth {
|
||||
internal;
|
||||
proxy_set_header Content-Type "application/json";
|
||||
proxy_set_header Content-Length $request_length;
|
||||
proxy_pass http://service/api/approve/verifyToken;
|
||||
}
|
||||
35
docker/nginx/location/drawio.conf
Normal file
35
docker/nginx/location/drawio.conf
Normal file
@@ -0,0 +1,35 @@
|
||||
location /drawio/webapp/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Real-PORT $remote_port;
|
||||
proxy_set_header X-Forwarded-Host $the_host/drawio/webapp;
|
||||
proxy_set_header X-Forwarded-Proto $the_scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header Scheme $scheme;
|
||||
proxy_set_header Server-Protocol $server_protocol;
|
||||
proxy_set_header Server-Name $server_name;
|
||||
proxy_set_header Server-Addr $server_addr;
|
||||
proxy_set_header Server-Port $server_port;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_pass http://drawio-webapp:8080/;
|
||||
}
|
||||
|
||||
location /drawio/export/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Real-PORT $remote_port;
|
||||
proxy_set_header X-Forwarded-Host $the_host/drawio/export;
|
||||
proxy_set_header X-Forwarded-Proto $the_scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header Scheme $scheme;
|
||||
proxy_set_header Server-Protocol $server_protocol;
|
||||
proxy_set_header Server-Name $server_name;
|
||||
proxy_set_header Server-Addr $server_addr;
|
||||
proxy_set_header Server-Port $server_port;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_pass http://drawio-export:8000/;
|
||||
}
|
||||
16
docker/nginx/location/fileview.conf
Normal file
16
docker/nginx/location/fileview.conf
Normal file
@@ -0,0 +1,16 @@
|
||||
location /fileview {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Real-PORT $remote_port;
|
||||
proxy_set_header X-Forwarded-Host $the_host;
|
||||
proxy_set_header X-Forwarded-Proto $the_scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header Scheme $scheme;
|
||||
proxy_set_header Server-Protocol $server_protocol;
|
||||
proxy_set_header Server-Name $server_name;
|
||||
proxy_set_header Server-Addr $server_addr;
|
||||
proxy_set_header Server-Port $server_port;
|
||||
proxy_pass http://fileview:8012;
|
||||
}
|
||||
16
docker/nginx/location/minder.conf
Normal file
16
docker/nginx/location/minder.conf
Normal file
@@ -0,0 +1,16 @@
|
||||
location /minder/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Real-PORT $remote_port;
|
||||
proxy_set_header X-Forwarded-Host $the_host;
|
||||
proxy_set_header X-Forwarded-Proto $the_scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header Scheme $scheme;
|
||||
proxy_set_header Server-Protocol $server_protocol;
|
||||
proxy_set_header Server-Name $server_name;
|
||||
proxy_set_header Server-Addr $server_addr;
|
||||
proxy_set_header Server-Port $server_port;
|
||||
proxy_pass http://minder/;
|
||||
}
|
||||
20
docker/nginx/location/office.conf
Normal file
20
docker/nginx/location/office.conf
Normal file
@@ -0,0 +1,20 @@
|
||||
location /office/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Real-PORT $remote_port;
|
||||
proxy_set_header X-Forwarded-Host $the_host/office;
|
||||
proxy_set_header X-Forwarded-Proto $the_scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header Scheme $scheme;
|
||||
proxy_set_header Server-Protocol $server_protocol;
|
||||
proxy_set_header Server-Name $server_name;
|
||||
proxy_set_header Server-Addr $server_addr;
|
||||
proxy_set_header Server-Port $server_port;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 3600s;
|
||||
proxy_pass http://office/;
|
||||
}
|
||||
3
docker/nginx/location/okr.conf
Normal file
3
docker/nginx/location/okr.conf
Normal file
@@ -0,0 +1,3 @@
|
||||
location /apps/okr/ {
|
||||
proxy_pass http://okr:5566/apps/okr/;
|
||||
}
|
||||
1
docker/nginx/site/.gitignore
vendored
1
docker/nginx/site/.gitignore
vendored
@@ -1 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
|
||||
508
docker/office/etc/documentserver/default.json
Normal file
508
docker/office/etc/documentserver/default.json
Normal file
@@ -0,0 +1,508 @@
|
||||
{
|
||||
"statsd": {
|
||||
"useMetrics": false,
|
||||
"host": "localhost",
|
||||
"port": "8125",
|
||||
"prefix": "ds."
|
||||
},
|
||||
"log": {
|
||||
"filePath": "",
|
||||
"options": {
|
||||
"replaceConsole": true
|
||||
}
|
||||
},
|
||||
"queue": {
|
||||
"type": "rabbitmq",
|
||||
"visibilityTimeout": 300,
|
||||
"retentionPeriod": 900
|
||||
},
|
||||
"email": {
|
||||
"smtpServerConfiguration": {
|
||||
"host": "localhost",
|
||||
"port": 587,
|
||||
"auth": {
|
||||
"user": "onlyoffice",
|
||||
"pass": "onlyoffice"
|
||||
}
|
||||
},
|
||||
"connectionConfiguration": {
|
||||
"disableFileAccess": false,
|
||||
"disableUrlAccess": false
|
||||
},
|
||||
"contactDefaults": {
|
||||
"from": "from@example.com",
|
||||
"to": "to@example.com"
|
||||
}
|
||||
},
|
||||
"notification": {
|
||||
"rules": {
|
||||
"licenseExpirationWarning": {
|
||||
"enable": false,
|
||||
"transportType": [
|
||||
"email"
|
||||
],
|
||||
"template": {
|
||||
"title": "%s Docs license expiration warning",
|
||||
"body": "Attention! Your license is about to expire on %s.\nUpon reaching this date, you will no longer be entitled to receive personal technical support and install new Docs versions released after this date."
|
||||
},
|
||||
"policies": {
|
||||
"repeatInterval": "1d"
|
||||
}
|
||||
},
|
||||
"licenseExpirationError": {
|
||||
"enable": false,
|
||||
"transportType": [
|
||||
"email"
|
||||
],
|
||||
"template": {
|
||||
"title": "%s Docs license expiration warning",
|
||||
"body": "Attention! Your license expired on %s.\nYou are no longer entitled to receive personal technical support and install new Docs versions released after this date.\nPlease contact sales@onlyoffice.com to discuss license renewal."
|
||||
},
|
||||
"policies": {
|
||||
"repeatInterval": "1d"
|
||||
}
|
||||
},
|
||||
"licenseLimitEdit": {
|
||||
"enable": false,
|
||||
"transportType": [
|
||||
"email"
|
||||
],
|
||||
"template": {
|
||||
"title": "%s Docs license connection limit warning",
|
||||
"body": "Attention! You have reached %s%% of the %s limit set by your license."
|
||||
},
|
||||
"policies": {
|
||||
"repeatInterval": "1h"
|
||||
}
|
||||
},
|
||||
"licenseLimitLiveViewer": {
|
||||
"enable": false,
|
||||
"transportType": [
|
||||
"email"
|
||||
],
|
||||
"template": {
|
||||
"title": "%s Docs license connection limit warning",
|
||||
"body": "Attention! You have reached %s%% of the live viewer %s limit set by your license."
|
||||
},
|
||||
"policies": {
|
||||
"repeatInterval": "1h"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
"name": "storage-fs",
|
||||
"fs": {
|
||||
"folderPath": "",
|
||||
"urlExpires": 900,
|
||||
"secretString": "verysecretstring"
|
||||
},
|
||||
"region": "",
|
||||
"endpoint": "http://localhost/s3",
|
||||
"bucketName": "cache",
|
||||
"storageFolderName": "files",
|
||||
"cacheFolderName": "data",
|
||||
"urlExpires": 604800,
|
||||
"accessKeyId": "AKID",
|
||||
"secretAccessKey": "SECRET",
|
||||
"sslEnabled": false,
|
||||
"s3ForcePathStyle": true,
|
||||
"externalHost": ""
|
||||
},
|
||||
"persistentStorage": {
|
||||
},
|
||||
"rabbitmq": {
|
||||
"url": "amqp://guest:guest@localhost:5672",
|
||||
"socketOptions": {},
|
||||
"exchangepubsub": "ds.pubsub",
|
||||
"queueconverttask": "ds.converttask",
|
||||
"queueconvertresponse": "ds.convertresponse",
|
||||
"exchangeconvertdead": "ds.exchangeconvertdead",
|
||||
"queueconvertdead": "ds.convertdead",
|
||||
"queuedelayed": "ds.delayed"
|
||||
},
|
||||
"activemq": {
|
||||
"connectOptions": {
|
||||
"port": 5672,
|
||||
"host": "localhost",
|
||||
"reconnect": false
|
||||
},
|
||||
"queueconverttask": "ds.converttask",
|
||||
"queueconvertresponse": "ds.convertresponse",
|
||||
"queueconvertdead": "ActiveMQ.DLQ",
|
||||
"queuedelayed": "ds.delayed",
|
||||
"topicpubsub": "ds.pubsub"
|
||||
},
|
||||
"dnscache": {
|
||||
"enable" : true,
|
||||
"ttl" : 300,
|
||||
"cachesize" : 1000
|
||||
},
|
||||
"openpgpjs": {
|
||||
"config": {
|
||||
},
|
||||
"encrypt": {
|
||||
"passwords": ["verysecretstring"]
|
||||
},
|
||||
"decrypt": {
|
||||
"passwords": ["verysecretstring"]
|
||||
}
|
||||
},
|
||||
"aesEncrypt": {
|
||||
"config": {
|
||||
"keyByteLength": 32,
|
||||
"saltByteLength": 64,
|
||||
"initializationVectorByteLength": 16,
|
||||
"iterationsByteLength": 5
|
||||
},
|
||||
"secret": "verysecretstring"
|
||||
},
|
||||
"bottleneck": {
|
||||
"getChanges": {
|
||||
}
|
||||
},
|
||||
"win-ca": {
|
||||
"inject": "+"
|
||||
},
|
||||
"wopi": {
|
||||
"enable": false,
|
||||
"host" : "",
|
||||
"htmlTemplate" : "../../web-apps/apps/api/wopi",
|
||||
"wopiZone" : "external-http",
|
||||
"favIconUrlWord" : "/web-apps/apps/documenteditor/main/resources/img/favicon.ico",
|
||||
"favIconUrlCell" : "/web-apps/apps/spreadsheeteditor/main/resources/img/favicon.ico",
|
||||
"favIconUrlSlide" : "/web-apps/apps/presentationeditor/main/resources/img/favicon.ico",
|
||||
"favIconUrlPdf" : "/web-apps/apps/pdfeditor/main/resources/img/favicon.ico",
|
||||
"fileInfoBlockList" : ["FileUrl"],
|
||||
"pdfView": ["djvu", "xps", "oxps"],
|
||||
"pdfEdit": ["pdf"],
|
||||
"forms": ["pdf"],
|
||||
"wordView": ["doc", "dotm", "dot", "fodt", "ott", "rtf", "mht", "mhtml", "html", "htm", "xml", "epub", "fb2", "sxw", "stw", "wps", "wpt", "docxf", "oform"],
|
||||
"wordEdit": ["docx", "dotx", "docm", "odt", "txt"],
|
||||
"cellView": ["xls", "xlsb", "xltm", "xlt", "fods", "ots", "sxc", "xml", "et", "ett"],
|
||||
"cellEdit": ["xlsx", "xltx", "xlsm", "ods", "csv"],
|
||||
"slideView": ["ppt", "ppsx", "ppsm", "pps", "potm", "pot", "fodp", "otp", "sxi", "dps", "dpt"],
|
||||
"slideEdit": ["pptx", "potx", "pptm", "odp"],
|
||||
"publicKey": "BgIAAACkAABSU0ExAAgAAAEAAQBpTpiJQ2hD8plpGTfEEmcq4IKyr31HikXpuVSBraMfqyodn2PGXBJ3daNSmdPOc0Nz4HO9Auljn8YYXDPBdpiABptSKvEDPF23Q+Qytg0+vCRyondyBcW91w7KLzXce3fnk8ZfJ8QtbZPL9m11wJIWZueQF+l0HKYx4lty+nccbCanytFTADkGQ3SnmExGEF3rBz6I9+OcrDDK9NKPJgEmCiuyei/d4XbPgKls3EIG0h38X5mVF2VytfWm2Yu850B6z3N4MYhj4b4vsYT62zEC4pMRUeb8dIBy4Jsmr3avtmeO00MUH6DVyPC8nirixj2YIOPKk13CdVqGDSXA3cvl",
|
||||
"modulus": "5cvdwCUNhlp1wl2TyuMgmD3G4iqevPDI1aAfFEPTjme2r3avJpvgcoB0/OZREZPiAjHb+oSxL77hY4gxeHPPekDnvIvZpvW1cmUXlZlf/B3SBkLcbKmAz3bh3S96sisKJgEmj9L0yjCsnOP3iD4H610QRkyYp3RDBjkAU9HKpyZsHHf6clviMaYcdOkXkOdmFpLAdW32y5NtLcQnX8aT53d73DUvyg7XvcUFcneiciS8Pg22MuRDt108A/EqUpsGgJh2wTNcGMafY+kCvXPgc0NzztOZUqN1dxJcxmOfHSqrH6OtgVS56UWKR32vsoLgKmcSxDcZaZnyQ2hDiZhOaQ==",
|
||||
"exponent": 65537,
|
||||
"privateKey": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDly93AJQ2GWnXC\nXZPK4yCYPcbiKp688MjVoB8UQ9OOZ7avdq8mm+BygHT85lERk+ICMdv6hLEvvuFj\niDF4c896QOe8i9mm9bVyZReVmV/8HdIGQtxsqYDPduHdL3qyKwomASaP0vTKMKyc\n4/eIPgfrXRBGTJindEMGOQBT0cqnJmwcd/pyW+Ixphx06ReQ52YWksB1bfbLk20t\nxCdfxpPnd3vcNS/KDte9xQVyd6JyJLw+DbYy5EO3XTwD8SpSmwaAmHbBM1wYxp9j\n6QK9c+BzQ3PO05lSo3V3ElzGY58dKqsfo62BVLnpRYpHfa+yguAqZxLENxlpmfJD\naEOJmE5pAgMBAAECggEALiL+RKOr0Xu8BOgQ0j1DwA03LxVrhXe6etmJI+JySTcd\ngKENjWziZVrRIi2DvUm5qMMl7WhSwslKK1eexxZJY7xASqSxcEoIwgz17T07/jxm\nfIdUBiUKDZ1Kv8PWmIr3oKW+fkXWi/m1zlIe0qXRpTmsGNEsHQLEqi0rmaiXTXOR\n/2Ldwi6kZR3sWFx97YS4Mx/pueGJTXEai6AVEZzN5Gog6xD8HXR1Rvq+hhd+MocG\nfnU4HgilKRfoJlWd9FOscgSufKG0L3ViO4fSKU46l5aullDYUk5ECMWiwuKSqSE7\nqD45jI3mbOre7S4u3S3TWdD3lzwiXL49LdwKlEC4mQKBgQD0sLr0GH4Wr+QX2xJE\nuA/Cb8QW41l8iSCBTRZZR/sJOd+o3rbcVidlzO/EbZblXG4ZPDmRjgBCGKIP5EZi\n0DsL+Wv32WOo44LpxJGhqExbm0H1iZ1zZ97l0P8fvIhHE42gmaLToOIGDhPSXGvv\nzlqOHbGbq4jsERc1jp1bej5q6wKBgQDwaueIc4pRchH98QYidcyr8Vwg9KhbnfYX\ny3W4RPlZtBdF34iJaio+ASzugo/zy1RTcVrsCskYWXyKDUQz1yu0iCng+fDCUnTm\nXGmEoEGNhk4vTJOt7hBav1/Ja/dUipGf6mXUuanwJ0e+1/Et/B0ah5X1Um5AyNZI\nM+SyRz3u+wKBgQCjvtUNXoqaghCBCmB6TjZ1prexnWkYFugCv2SSUMIk1W7gIlJ6\ntsjcrj1R1Qii6qzfBFd+GWoA0V06h0e2/qRVCg//p6GytrW33IycgvS+ZPLJ7tLI\nFR2r66WfRlpoPiSL8eRt/P7kkG0hXCn7K7ub2TEu/Ka/W1yNwad6PR8iCwKBgQC8\nXcZSrtQsxAc8w99emJVoEo9wcsCGJ9ltA0iUu9XyZpvlbyJ3J+s48YrWxQ0sop7L\nUgE+96Rfo51kPMi3JVtk81p8ntf4KMrWwokaFMXHsPcJMCJ1IBVIRLE0C5eZcYhv\nlyN57I4tT1lzOZYJxYK4Cot/zrn7oF/j6mTBGfh4iQKBgQCiJMUxRz01/czH/XSX\ngo3dVbHQ4FEOufWnE3Eb93S8r0/eq1RM118rb0TqzuiadW2xYDU4nucWQlrlmq0d\nFY/m+Hy97pqyk6jmoU5I/D+ssBIoYHWLnH9/xfvDEk2JGSJSHtzu0D4EDC/rgQ49\nMbYsO5oUrF8tPlhj5vzbf3GKLA==\n-----END PRIVATE KEY-----\n",
|
||||
"publicKeyOld": "BgIAAACkAABSU0ExAAgAAAEAAQBpTpiJQ2hD8plpGTfEEmcq4IKyr31HikXpuVSBraMfqyodn2PGXBJ3daNSmdPOc0Nz4HO9Auljn8YYXDPBdpiABptSKvEDPF23Q+Qytg0+vCRyondyBcW91w7KLzXce3fnk8ZfJ8QtbZPL9m11wJIWZueQF+l0HKYx4lty+nccbCanytFTADkGQ3SnmExGEF3rBz6I9+OcrDDK9NKPJgEmCiuyei/d4XbPgKls3EIG0h38X5mVF2VytfWm2Yu850B6z3N4MYhj4b4vsYT62zEC4pMRUeb8dIBy4Jsmr3avtmeO00MUH6DVyPC8nirixj2YIOPKk13CdVqGDSXA3cvl",
|
||||
"modulusOld": "5cvdwCUNhlp1wl2TyuMgmD3G4iqevPDI1aAfFEPTjme2r3avJpvgcoB0/OZREZPiAjHb+oSxL77hY4gxeHPPekDnvIvZpvW1cmUXlZlf/B3SBkLcbKmAz3bh3S96sisKJgEmj9L0yjCsnOP3iD4H610QRkyYp3RDBjkAU9HKpyZsHHf6clviMaYcdOkXkOdmFpLAdW32y5NtLcQnX8aT53d73DUvyg7XvcUFcneiciS8Pg22MuRDt108A/EqUpsGgJh2wTNcGMafY+kCvXPgc0NzztOZUqN1dxJcxmOfHSqrH6OtgVS56UWKR32vsoLgKmcSxDcZaZnyQ2hDiZhOaQ==",
|
||||
"exponentOld": 65537,
|
||||
"privateKeyOld": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDly93AJQ2GWnXC\nXZPK4yCYPcbiKp688MjVoB8UQ9OOZ7avdq8mm+BygHT85lERk+ICMdv6hLEvvuFj\niDF4c896QOe8i9mm9bVyZReVmV/8HdIGQtxsqYDPduHdL3qyKwomASaP0vTKMKyc\n4/eIPgfrXRBGTJindEMGOQBT0cqnJmwcd/pyW+Ixphx06ReQ52YWksB1bfbLk20t\nxCdfxpPnd3vcNS/KDte9xQVyd6JyJLw+DbYy5EO3XTwD8SpSmwaAmHbBM1wYxp9j\n6QK9c+BzQ3PO05lSo3V3ElzGY58dKqsfo62BVLnpRYpHfa+yguAqZxLENxlpmfJD\naEOJmE5pAgMBAAECggEALiL+RKOr0Xu8BOgQ0j1DwA03LxVrhXe6etmJI+JySTcd\ngKENjWziZVrRIi2DvUm5qMMl7WhSwslKK1eexxZJY7xASqSxcEoIwgz17T07/jxm\nfIdUBiUKDZ1Kv8PWmIr3oKW+fkXWi/m1zlIe0qXRpTmsGNEsHQLEqi0rmaiXTXOR\n/2Ldwi6kZR3sWFx97YS4Mx/pueGJTXEai6AVEZzN5Gog6xD8HXR1Rvq+hhd+MocG\nfnU4HgilKRfoJlWd9FOscgSufKG0L3ViO4fSKU46l5aullDYUk5ECMWiwuKSqSE7\nqD45jI3mbOre7S4u3S3TWdD3lzwiXL49LdwKlEC4mQKBgQD0sLr0GH4Wr+QX2xJE\nuA/Cb8QW41l8iSCBTRZZR/sJOd+o3rbcVidlzO/EbZblXG4ZPDmRjgBCGKIP5EZi\n0DsL+Wv32WOo44LpxJGhqExbm0H1iZ1zZ97l0P8fvIhHE42gmaLToOIGDhPSXGvv\nzlqOHbGbq4jsERc1jp1bej5q6wKBgQDwaueIc4pRchH98QYidcyr8Vwg9KhbnfYX\ny3W4RPlZtBdF34iJaio+ASzugo/zy1RTcVrsCskYWXyKDUQz1yu0iCng+fDCUnTm\nXGmEoEGNhk4vTJOt7hBav1/Ja/dUipGf6mXUuanwJ0e+1/Et/B0ah5X1Um5AyNZI\nM+SyRz3u+wKBgQCjvtUNXoqaghCBCmB6TjZ1prexnWkYFugCv2SSUMIk1W7gIlJ6\ntsjcrj1R1Qii6qzfBFd+GWoA0V06h0e2/qRVCg//p6GytrW33IycgvS+ZPLJ7tLI\nFR2r66WfRlpoPiSL8eRt/P7kkG0hXCn7K7ub2TEu/Ka/W1yNwad6PR8iCwKBgQC8\nXcZSrtQsxAc8w99emJVoEo9wcsCGJ9ltA0iUu9XyZpvlbyJ3J+s48YrWxQ0sop7L\nUgE+96Rfo51kPMi3JVtk81p8ntf4KMrWwokaFMXHsPcJMCJ1IBVIRLE0C5eZcYhv\nlyN57I4tT1lzOZYJxYK4Cot/zrn7oF/j6mTBGfh4iQKBgQCiJMUxRz01/czH/XSX\ngo3dVbHQ4FEOufWnE3Eb93S8r0/eq1RM118rb0TqzuiadW2xYDU4nucWQlrlmq0d\nFY/m+Hy97pqyk6jmoU5I/D+ssBIoYHWLnH9/xfvDEk2JGSJSHtzu0D4EDC/rgQ49\nMbYsO5oUrF8tPlhj5vzbf3GKLA==\n-----END PRIVATE KEY-----\n",
|
||||
"refreshLockInterval": "10m",
|
||||
"dummy" : {
|
||||
"enable": false,
|
||||
"sampleFilePath": ""
|
||||
}
|
||||
},
|
||||
"tenants": {
|
||||
"baseDir": "",
|
||||
"baseDomain": "",
|
||||
"filenameConfig": "config.json",
|
||||
"filenameSecret": "secret.key",
|
||||
"filenameLicense": "license.lic",
|
||||
"defaultTenant": "localhost",
|
||||
"cache" : {
|
||||
"stdTTL": 300,
|
||||
"checkperiod": 60,
|
||||
"useClones": false
|
||||
}
|
||||
},
|
||||
"externalRequest": {
|
||||
"directIfIn" : {
|
||||
"allowList": [],
|
||||
"jwtToken": true
|
||||
},
|
||||
"action": {
|
||||
"allow": true,
|
||||
"blockPrivateIP": true,
|
||||
"proxyUrl": "",
|
||||
"proxyUser": {
|
||||
"username": "",
|
||||
"password": ""
|
||||
},
|
||||
"proxyHeaders": {
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"CoAuthoring": {
|
||||
"server": {
|
||||
"port": 8000,
|
||||
"workerpercpu": 1,
|
||||
"mode": "development",
|
||||
"limits_tempfile_upload": 104857600,
|
||||
"limits_image_size": 26214400,
|
||||
"limits_image_download_timeout": {
|
||||
"connectionAndInactivity": "2m",
|
||||
"wholeCycle": "2m"
|
||||
},
|
||||
"callbackRequestTimeout": {
|
||||
"connectionAndInactivity": "10m",
|
||||
"wholeCycle": "10m"
|
||||
},
|
||||
"healthcheckfilepath": "../public/healthcheck.docx",
|
||||
"savetimeoutdelay": 5000,
|
||||
"edit_singleton": false,
|
||||
"forgottenfiles": "forgotten",
|
||||
"forgottenfilesname": "output",
|
||||
"maxRequestChanges": 20000,
|
||||
"openProtectedFile": true,
|
||||
"isAnonymousSupport": true,
|
||||
"editorDataStorage": "editorDataMemory",
|
||||
"editorStatStorage": "",
|
||||
"assemblyFormatAsOrigin": true,
|
||||
"newFileTemplate" : "../../document-templates/new",
|
||||
"downloadFileAllowExt": ["pdf", "xlsx"],
|
||||
"tokenRequiredParams": true,
|
||||
"forceSaveUsingButtonWithoutChanges": false
|
||||
},
|
||||
"requestDefaults": {
|
||||
"headers": {
|
||||
"User-Agent": "Node.js/6.13",
|
||||
"Connection": "Keep-Alive"
|
||||
},
|
||||
"gzip": true,
|
||||
"rejectUnauthorized": true
|
||||
},
|
||||
"autoAssembly": {
|
||||
"enable": false,
|
||||
"interval": "5m",
|
||||
"step": "1m"
|
||||
},
|
||||
"utils": {
|
||||
"utils_common_fontdir": "null",
|
||||
"utils_fonts_search_patterns": "*.ttf;*.ttc;*.otf",
|
||||
"limits_image_types_upload": "jpg;jpeg;jpe;png;gif;bmp;svg;tiff;tif"
|
||||
},
|
||||
"sql": {
|
||||
"type": "postgres",
|
||||
"tableChanges": "doc_changes",
|
||||
"tableResult": "task_result",
|
||||
"dbHost": "localhost",
|
||||
"dbPort": 5432,
|
||||
"dbName": "onlyoffice",
|
||||
"dbUser": "onlyoffice",
|
||||
"dbPass": "onlyoffice",
|
||||
"charset": "utf8",
|
||||
"connectionlimit": 10,
|
||||
"max_allowed_packet": 1048575,
|
||||
"pgPoolExtraOptions": {
|
||||
"idleTimeoutMillis": 30000,
|
||||
"maxLifetimeSeconds ": 60000,
|
||||
"statement_timeout ": 60000,
|
||||
"query_timeout ": 60000,
|
||||
"connectionTimeoutMillis": 60000
|
||||
},
|
||||
"damengExtraOptions": {
|
||||
"columnNameUpperCase": false,
|
||||
"columnNameCase": "lower",
|
||||
"connectTimeout": 60000,
|
||||
"loginEncrypt": false,
|
||||
"localTimezone": 0,
|
||||
"poolTimeout": 60,
|
||||
"socketTimeout": 60000,
|
||||
"queueTimeout": 60000
|
||||
},
|
||||
"oracleExtraOptions": {
|
||||
"connectTimeout": 60
|
||||
},
|
||||
"msSqlExtraOptions": {
|
||||
"options": {
|
||||
"encrypt": false,
|
||||
"trustServerCertificate": true
|
||||
},
|
||||
"pool": {
|
||||
"idleTimeoutMillis": 30000
|
||||
}
|
||||
},
|
||||
"mysqlExtraOptions": {
|
||||
"connectTimeout": 60000,
|
||||
"queryTimeout": 60000
|
||||
}
|
||||
},
|
||||
"redis": {
|
||||
"name": "redis",
|
||||
"prefix": "ds:",
|
||||
"host": "127.0.0.1",
|
||||
"port": 6379,
|
||||
"options": {},
|
||||
"optionsCluster": {},
|
||||
"iooptions": {
|
||||
"lazyConnect": true
|
||||
},
|
||||
"iooptionsClusterNodes": [
|
||||
],
|
||||
"iooptionsClusterOptions": {
|
||||
"lazyConnect": true
|
||||
}
|
||||
},
|
||||
"pubsub": {
|
||||
"maxChanges": 1000
|
||||
},
|
||||
"expire": {
|
||||
"saveLock": 60,
|
||||
"presence": 300,
|
||||
"locks": 604800,
|
||||
"changeindex": 86400,
|
||||
"lockDoc": 30,
|
||||
"message": 86400,
|
||||
"lastsave": 604800,
|
||||
"forcesave": 604800,
|
||||
"forcesaveLock": 5000,
|
||||
"saved": 3600,
|
||||
"documentsCron": "0 */2 * * * *",
|
||||
"files": 86400,
|
||||
"filesCron": "00 00 */1 * * *",
|
||||
"filesremovedatonce": 100,
|
||||
"sessionidle": "1h",
|
||||
"sessionabsolute": "30d",
|
||||
"sessionclosecommand": "2m",
|
||||
"pemStdTTL": "1h",
|
||||
"pemCheckPeriod": "10m",
|
||||
"updateVersionStatus": "5m",
|
||||
"monthUniqueUsers": "1y"
|
||||
},
|
||||
"ipfilter": {
|
||||
"rules": [{"address": "*", "allowed": true}],
|
||||
"useforrequest": false,
|
||||
"errorcode": 403
|
||||
},
|
||||
"request-filtering-agent" : {
|
||||
"allowPrivateIPAddress": false,
|
||||
"allowMetaIPAddress": false
|
||||
},
|
||||
"secret": {
|
||||
"browser": {"string": "secret", "file": ""},
|
||||
"inbox": {"string": "secret", "file": ""},
|
||||
"outbox": {"string": "secret", "file": ""},
|
||||
"session": {"string": "secret", "file": ""}
|
||||
},
|
||||
"token": {
|
||||
"enable": {
|
||||
"browser": false,
|
||||
"request": {
|
||||
"inbox": false,
|
||||
"outbox": false
|
||||
}
|
||||
},
|
||||
"browser": {
|
||||
"secretFromInbox": true
|
||||
},
|
||||
"inbox": {
|
||||
"header": "Authorization",
|
||||
"prefix": "Bearer ",
|
||||
"inBody": false
|
||||
},
|
||||
"outbox": {
|
||||
"header": "Authorization",
|
||||
"prefix": "Bearer ",
|
||||
"algorithm": "HS256",
|
||||
"expires": "5m",
|
||||
"inBody": false,
|
||||
"urlExclusionRegex": ""
|
||||
},
|
||||
"session": {
|
||||
"algorithm": "HS256",
|
||||
"expires": "30d"
|
||||
},
|
||||
"verifyOptions": {
|
||||
"clockTolerance": 60
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"uri": "/sdkjs-plugins",
|
||||
"autostart": []
|
||||
},
|
||||
"themes": {
|
||||
"uri": "/web-apps/apps/common/main/resources/themes"
|
||||
},
|
||||
"editor":{
|
||||
"spellcheckerUrl": "",
|
||||
"reconnection":{
|
||||
"attempts": 50,
|
||||
"delay": "2s"
|
||||
},
|
||||
"binaryChanges": false,
|
||||
"websocketMaxPayloadSize": "1.5MB",
|
||||
"maxChangesSize": "0mb"
|
||||
},
|
||||
"sockjs": {
|
||||
"sockjs_url": "",
|
||||
"disable_cors": true,
|
||||
"websocket": true
|
||||
},
|
||||
"socketio": {
|
||||
"connection": {
|
||||
"path": "/doc/",
|
||||
"serveClient": false,
|
||||
"pingTimeout": 20000,
|
||||
"pingInterval": 25000,
|
||||
"maxHttpBufferSize": 1e8
|
||||
}
|
||||
},
|
||||
"callbackBackoffOptions": {
|
||||
"retries": 0,
|
||||
"timeout":{
|
||||
"factor": 2,
|
||||
"minTimeout": 1000,
|
||||
"maxTimeout": 2147483647,
|
||||
"randomize": false
|
||||
},
|
||||
"httpStatus": "429,500-599"
|
||||
}
|
||||
}
|
||||
},
|
||||
"license" : {
|
||||
"license_file": "",
|
||||
"warning_limit_percents": 70,
|
||||
"packageType": 0,
|
||||
"warning_license_expiration": "30d"
|
||||
},
|
||||
"FileConverter": {
|
||||
"converter": {
|
||||
"maxDownloadBytes": 1048576000,
|
||||
"downloadTimeout": {
|
||||
"connectionAndInactivity": "2m",
|
||||
"wholeCycle": "2m"
|
||||
},
|
||||
"downloadAttemptMaxCount": 3,
|
||||
"downloadAttemptDelay": 1000,
|
||||
"maxprocesscount": 1,
|
||||
"fontDir": "null",
|
||||
"presentationThemesDir": "null",
|
||||
"x2tPath": "null",
|
||||
"docbuilderPath": "null",
|
||||
"args": "",
|
||||
"spawnOptions": {},
|
||||
"errorfiles": "",
|
||||
"streamWriterBufferSize": 8388608,
|
||||
"maxRedeliveredCount": 2,
|
||||
"inputLimits": [
|
||||
{
|
||||
"type": "docx;dotx;docm;dotm",
|
||||
"zip": {
|
||||
"uncompressed": "500MB",
|
||||
"template": "*.xml"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "xlsx;xltx;xlsm;xltm",
|
||||
"zip": {
|
||||
"uncompressed": "3000MB",
|
||||
"template": "*.xml"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "pptx;ppsx;potx;pptm;ppsm;potm",
|
||||
"zip": {
|
||||
"uncompressed": "500MB",
|
||||
"template": "*.xml"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
3
docker/office/logs/.gitignore
vendored
3
docker/office/logs/.gitignore
vendored
@@ -1 +1,2 @@
|
||||
*/
|
||||
*
|
||||
!.gitignore
|
||||
|
||||
1
electron/.gitignore
vendored
1
electron/.gitignore
vendored
@@ -10,3 +10,4 @@ cache/*
|
||||
|
||||
.devload
|
||||
.native
|
||||
.build
|
||||
158
electron/build.js
vendored
158
electron/build.js
vendored
@@ -492,12 +492,9 @@ async function startBuild(data) {
|
||||
console.log("版本:", config.version + ` (${config.codeVerson})`);
|
||||
console.log("系统:", platform.replace('build-', '').toUpperCase());
|
||||
console.log("架构:", archs.map(arch => arch.toUpperCase()).join(', '));
|
||||
console.log("发布:", publish ? '是' : '否');
|
||||
if (publish) {
|
||||
console.log("升级提示:", release ? '是' : '否');
|
||||
if (platform === 'build-mac') {
|
||||
console.log("公证:", notarize ? '是' : '否');
|
||||
}
|
||||
console.log("发布:", publish ? `是(${release ? '提示升级' : '静默升级'})` : '否');
|
||||
if (platform === 'build-mac') {
|
||||
console.log("公证:", notarize ? '是' : '否');
|
||||
}
|
||||
console.log("===============\n");
|
||||
// drawio
|
||||
@@ -512,6 +509,7 @@ async function startBuild(data) {
|
||||
fse.copySync(path.resolve(__dirname, "../public/language"), path.resolve(electronDir, "language"))
|
||||
// config.js
|
||||
fs.writeFileSync(electronDir + "/config.js", "window.systemInfo = " + JSON.stringify(systemInfo), 'utf8');
|
||||
fs.writeFileSync(electronDir + "/dark", '', 'utf8');
|
||||
fs.writeFileSync(nativeCachePath, utils.formatUrl(data.url));
|
||||
fs.writeFileSync(devloadCachePath, "", 'utf8');
|
||||
// index.html
|
||||
@@ -697,6 +695,19 @@ if (["dev"].includes(argv[2])) {
|
||||
});
|
||||
} else {
|
||||
// 手编译(默认)
|
||||
let cachedConfig = {};
|
||||
try {
|
||||
const buildConfigPath = path.join(__dirname, '.build');
|
||||
if (fs.existsSync(buildConfigPath)) {
|
||||
const configContent = fs.readFileSync(buildConfigPath, 'utf-8');
|
||||
if (configContent.trim()) {
|
||||
cachedConfig = JSON.parse(configContent);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('读取缓存配置失败:', error.message);
|
||||
}
|
||||
|
||||
const questions = [
|
||||
{
|
||||
type: 'checkbox',
|
||||
@@ -706,14 +717,14 @@ if (["dev"].includes(argv[2])) {
|
||||
{
|
||||
name: "MacOS",
|
||||
value: platforms[0],
|
||||
checked: true
|
||||
},
|
||||
{
|
||||
name: "Windows",
|
||||
value: platforms[1]
|
||||
}
|
||||
],
|
||||
validate: function(answer) {
|
||||
default: (cachedConfig && cachedConfig.platform) || [],
|
||||
validate: (answer) => {
|
||||
if (answer.length < 1) {
|
||||
return '请至少选择一个系统';
|
||||
}
|
||||
@@ -724,18 +735,27 @@ if (["dev"].includes(argv[2])) {
|
||||
type: 'checkbox',
|
||||
name: 'arch',
|
||||
message: "选择系统架构",
|
||||
choices: [
|
||||
{
|
||||
name: "arm64",
|
||||
value: architectures[0],
|
||||
checked: true
|
||||
},
|
||||
{
|
||||
name: "x64",
|
||||
value: architectures[1]
|
||||
choices: ({platform}) => {
|
||||
const array = [
|
||||
{
|
||||
name: "arm64",
|
||||
value: architectures[0],
|
||||
},
|
||||
{
|
||||
name: "x64",
|
||||
value: architectures[1]
|
||||
}
|
||||
]
|
||||
if (platform.find(item => item === 'build-mac')) {
|
||||
array.push({
|
||||
name: "通用" + (platform.length > 1 ? " (仅MacOS)" : ""),
|
||||
value: 'universal'
|
||||
})
|
||||
}
|
||||
],
|
||||
validate: function(answer) {
|
||||
return array;
|
||||
},
|
||||
default: (cachedConfig && cachedConfig.arch) || [],
|
||||
validate: (answer) => {
|
||||
if (answer.length < 1) {
|
||||
return '请至少选择一个架构';
|
||||
}
|
||||
@@ -745,82 +765,94 @@ if (["dev"].includes(argv[2])) {
|
||||
{
|
||||
type: 'list',
|
||||
name: 'publish',
|
||||
message: "选择是否要发布",
|
||||
message: "选择是否发布",
|
||||
choices: [{
|
||||
name: "否",
|
||||
value: false
|
||||
}, {
|
||||
name: "是",
|
||||
value: true
|
||||
}]
|
||||
}
|
||||
];
|
||||
|
||||
// 根据publish选项动态添加后续问题
|
||||
const publishQuestions = [
|
||||
}],
|
||||
default: (cachedConfig && cachedConfig.publish !== undefined) ?
|
||||
(cachedConfig.publish ? 1 : 0) : 0
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
name: 'release',
|
||||
message: "选择是否弹出升级提示框",
|
||||
message: "选择升级方式",
|
||||
when: ({publish}) => publish,
|
||||
choices: [{
|
||||
name: "是",
|
||||
name: "弹出提示",
|
||||
value: true
|
||||
}, {
|
||||
name: "否",
|
||||
name: "静默",
|
||||
value: false
|
||||
}]
|
||||
}],
|
||||
default: (cachedConfig && cachedConfig.release !== undefined) ?
|
||||
(cachedConfig.release ? 0 : 1) : 0
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
name: 'notarize',
|
||||
message: "选择是否需要公证MacOS应用",
|
||||
when: (answers) => answers.platform === 'build-mac', // 只在MacOS时显示
|
||||
message: ({platform}) => platform.length > 1 ? "选择是否公证(仅MacOS)" : "选择是否公证",
|
||||
when: ({platform}) => platform.find(item => item === 'build-mac'),
|
||||
choices: [{
|
||||
name: "否",
|
||||
value: false
|
||||
}, {
|
||||
name: "是",
|
||||
value: true
|
||||
}]
|
||||
}],
|
||||
default: (cachedConfig && cachedConfig.notarize !== undefined) ?
|
||||
(cachedConfig.notarize ? 1 : 0) : 0
|
||||
}
|
||||
];
|
||||
|
||||
// 先询问基本问题
|
||||
inquirer.prompt(questions).then(async answers => {
|
||||
// 如果选择发布,继续询问发布相关问题
|
||||
if (answers.publish) {
|
||||
const publishAnswers = await inquirer.prompt(publishQuestions);
|
||||
Object.assign(answers, publishAnswers);
|
||||
// 开始提问
|
||||
const prompt = inquirer.createPromptModule();
|
||||
prompt(questions)
|
||||
.then(async answers => {
|
||||
answers = Object.assign({
|
||||
release: false,
|
||||
notarize: false
|
||||
}, answers);
|
||||
|
||||
if (!PUBLISH_KEY && (!GITHUB_TOKEN || !utils.strExists(GITHUB_REPOSITORY, "/"))) {
|
||||
console.error("发布需要 PUBLISH_KEY 或 GitHub Token 和 Repository, 请检查环境变量!");
|
||||
process.exit()
|
||||
// 缓存当前配置
|
||||
try {
|
||||
fs.writeFileSync(path.join(__dirname, '.build'), JSON.stringify(answers, null, 4), 'utf-8');
|
||||
} catch (error) {
|
||||
console.warn('保存配置缓存失败:', error.message);
|
||||
}
|
||||
|
||||
if (answers.notarize === true) {
|
||||
if (!APPLEID || !APPLEIDPASS) {
|
||||
console.error("公证MacOS应用需要 Apple ID 和 Apple ID 密码, 请检查环境变量!");
|
||||
// 发布判断环境变量
|
||||
if (answers.publish) {
|
||||
if (!PUBLISH_KEY && (!GITHUB_TOKEN || !utils.strExists(GITHUB_REPOSITORY, "/"))) {
|
||||
console.error("发布需要 PUBLISH_KEY 或 GitHub Token 和 Repository, 请检查环境变量!");
|
||||
process.exit()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果不发布,设置默认值
|
||||
answers.release = false;
|
||||
answers.notarize = false;
|
||||
}
|
||||
|
||||
// 开始构建
|
||||
for (const platform of answers.platform) {
|
||||
for (const data of config.app) {
|
||||
data.configure = {
|
||||
platform,
|
||||
archs: answers.arch,
|
||||
publish: answers.publish,
|
||||
release: answers.release,
|
||||
notarize: answers.notarize
|
||||
};
|
||||
await startBuild(data);
|
||||
// 公证判断环境变量
|
||||
if (answers.notarize === true) {
|
||||
if (!APPLEID || !APPLEIDPASS) {
|
||||
console.error("公证需要 Apple ID 和 Apple ID 密码, 请检查环境变量!");
|
||||
process.exit()
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 开始构建
|
||||
for (const platform of answers.platform) {
|
||||
for (const data of config.app) {
|
||||
data.configure = {
|
||||
platform,
|
||||
archs: answers.arch,
|
||||
publish: answers.publish,
|
||||
release: answers.release,
|
||||
notarize: answers.notarize
|
||||
};
|
||||
await startBuild(data);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(_ => { });
|
||||
}
|
||||
|
||||
128
electron/electron-menu.js
vendored
128
electron/electron-menu.js
vendored
@@ -17,6 +17,11 @@ const PERMITTED_URL_SCHEMES = ["http:", "https:", MAILTO_PREFIX];
|
||||
|
||||
const electronMenu = {
|
||||
language: {
|
||||
copy: "复制",
|
||||
back: "后退",
|
||||
forward: "前进",
|
||||
reload: "重新加载",
|
||||
print: "打印",
|
||||
openInBrowser: "在浏览器中打开",
|
||||
saveImageAs: "图片存储为...",
|
||||
copyImage: "复制图片",
|
||||
@@ -116,77 +121,100 @@ const electronMenu = {
|
||||
}
|
||||
},
|
||||
|
||||
webContentsMenu(webContents) {
|
||||
webContentsMenu(webContents, isBrowser = false) {
|
||||
webContents.on("context-menu", function (e, params) {
|
||||
const popupMenu = new Menu();
|
||||
if (params.linkURL || params.srcURL) {
|
||||
const url = params.linkURL || params.srcURL;
|
||||
const popupMenu = new Menu();
|
||||
|
||||
if (!electronMenu.isBlobOrDataUrl(url) && !utils.isLocalAssetPath(url)) {
|
||||
popupMenu.append(
|
||||
new MenuItem({
|
||||
label: electronMenu.language.openInBrowser,
|
||||
accelerator: "o",
|
||||
click() {
|
||||
electronMenu.safeOpenURL(url);
|
||||
},
|
||||
}),
|
||||
);
|
||||
popupMenu.append(new MenuItem({
|
||||
label: electronMenu.language.openInBrowser,
|
||||
click: async function () {
|
||||
electronMenu.safeOpenURL(url);
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
if (params.hasImageContents) {
|
||||
if (!electronMenu.isBlob(url)) {
|
||||
popupMenu.append(
|
||||
new MenuItem({
|
||||
label: electronMenu.language.saveImageAs,
|
||||
accelerator: "s",
|
||||
click: async function () {
|
||||
await electronMenu.saveImageAs(url, params);
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
popupMenu.append(
|
||||
new MenuItem({
|
||||
label: electronMenu.language.copyImage,
|
||||
accelerator: "c",
|
||||
click() {
|
||||
webContents.copyImageAt(params.x, params.y);
|
||||
popupMenu.append(new MenuItem({
|
||||
label: electronMenu.language.saveImageAs,
|
||||
click: async function () {
|
||||
await electronMenu.saveImageAs(url, params);
|
||||
},
|
||||
}),
|
||||
);
|
||||
}));
|
||||
}
|
||||
popupMenu.append(new MenuItem({
|
||||
label: electronMenu.language.copyImage,
|
||||
click: async function () {
|
||||
webContents.copyImageAt(params.x, params.y);
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
if (!electronMenu.isBlobOrDataUrl(url)) {
|
||||
if (url.startsWith(MAILTO_PREFIX)) {
|
||||
popupMenu.append(
|
||||
new MenuItem({
|
||||
label: electronMenu.language.copyEmailAddress,
|
||||
accelerator: "a",
|
||||
click() {
|
||||
clipboard.writeText(url.substring(MAILTO_PREFIX.length));
|
||||
},
|
||||
}),
|
||||
);
|
||||
popupMenu.append(new MenuItem({
|
||||
label: electronMenu.language.copyEmailAddress,
|
||||
click: async function () {
|
||||
clipboard.writeText(url.substring(MAILTO_PREFIX.length));
|
||||
},
|
||||
}));
|
||||
} else if (!utils.isLocalAssetPath(url)) {
|
||||
popupMenu.append(
|
||||
new MenuItem({
|
||||
label: params.hasImageContents ? electronMenu.language.copyImageAddress : electronMenu.language.copyLinkAddress,
|
||||
accelerator: "a",
|
||||
click() {
|
||||
clipboard.writeText(url);
|
||||
},
|
||||
}),
|
||||
);
|
||||
popupMenu.append(new MenuItem({
|
||||
label: params.hasImageContents ? electronMenu.language.copyImageAddress : electronMenu.language.copyLinkAddress,
|
||||
click: async function () {
|
||||
clipboard.writeText(url);
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isBrowser) {
|
||||
if (popupMenu.items.length > 0) {
|
||||
popupMenu.popup({});
|
||||
e.preventDefault();
|
||||
popupMenu.insert(0, new MenuItem({type: 'separator'}))
|
||||
}
|
||||
|
||||
popupMenu.insert(0, new MenuItem({
|
||||
label: electronMenu.language.print,
|
||||
click: () => webContents.print()
|
||||
}))
|
||||
|
||||
popupMenu.insert(0, new MenuItem({
|
||||
label: electronMenu.language.reload,
|
||||
click: () => webContents.reload()
|
||||
}))
|
||||
|
||||
popupMenu.insert(0, new MenuItem({
|
||||
label: electronMenu.language.forward,
|
||||
enabled: webContents.navigationHistory.canGoForward(),
|
||||
click: () => webContents.navigationHistory.goForward()
|
||||
}))
|
||||
|
||||
popupMenu.insert(0, new MenuItem({
|
||||
label: electronMenu.language.back,
|
||||
enabled: webContents.navigationHistory.canGoBack(),
|
||||
click: () => webContents.navigationHistory.goBack()
|
||||
}))
|
||||
}
|
||||
|
||||
if (params.selectionText) {
|
||||
if (popupMenu.items.length > 0) {
|
||||
popupMenu.insert(0, new MenuItem({type: 'separator'}))
|
||||
}
|
||||
popupMenu.insert(0, new MenuItem({
|
||||
label: electronMenu.language.copy,
|
||||
role: 'copy'
|
||||
}))
|
||||
}
|
||||
|
||||
if (popupMenu.items.length > 0) {
|
||||
popupMenu.popup({});
|
||||
e.preventDefault();
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
module.exports = electronMenu;
|
||||
|
||||
16
electron/electron-preload.js
vendored
16
electron/electron-preload.js
vendored
@@ -33,27 +33,25 @@ contextBridge.exposeInMainWorld(
|
||||
request: (msg, callback, error) => {
|
||||
msg.reqId = reqId++;
|
||||
reqInfo[msg.reqId] = {callback: callback, error: error};
|
||||
|
||||
//TODO Maybe a special function for this better than this hack?
|
||||
//File watch special case where the callback is called multiple times
|
||||
if (msg.action == 'watchFile') {
|
||||
fileChangedListeners[msg.path] = msg.listener;
|
||||
delete msg.listener;
|
||||
}
|
||||
|
||||
ipcRenderer.send('rendererReq', msg);
|
||||
},
|
||||
registerMsgListener: function (action, callback) {
|
||||
ipcRenderer.on(action, function (event, args) {
|
||||
callback(args);
|
||||
});
|
||||
},
|
||||
|
||||
sendMessage: function (action, args) {
|
||||
ipcRenderer.send(action, args);
|
||||
},
|
||||
sendAsync: function (action, args) {
|
||||
return ipcRenderer.invoke(action, args)
|
||||
},
|
||||
|
||||
listener: function (action, callback) {
|
||||
ipcRenderer.on(action, function (event, args) {
|
||||
callback(args);
|
||||
});
|
||||
},
|
||||
listenOnce: function (action, callback) {
|
||||
ipcRenderer.once(action, function (event, args) {
|
||||
callback(args);
|
||||
|
||||
177
electron/electron.js
vendored
177
electron/electron.js
vendored
@@ -58,7 +58,8 @@ let childWindow = [],
|
||||
mediaType = null,
|
||||
webTabWindow = null,
|
||||
webTabView = [],
|
||||
webTabHeight = 38;
|
||||
webTabHeight = 40,
|
||||
webTabClosedByShortcut = false;
|
||||
|
||||
let showState = {},
|
||||
onShowWindow = (win) => {
|
||||
@@ -163,7 +164,14 @@ function createMainWindow() {
|
||||
if (!willQuitApp) {
|
||||
utils.onBeforeUnload(event, mainWindow).then(() => {
|
||||
if (['darwin', 'win32'].includes(process.platform)) {
|
||||
mainWindow.hide();
|
||||
if (mainWindow.isFullScreen()) {
|
||||
mainWindow.once('leave-full-screen', () => {
|
||||
mainWindow.hide();
|
||||
})
|
||||
mainWindow.setFullScreen(false)
|
||||
} else {
|
||||
mainWindow.hide();
|
||||
}
|
||||
} else {
|
||||
app.quit();
|
||||
}
|
||||
@@ -178,11 +186,12 @@ function createMainWindow() {
|
||||
// 新窗口处理
|
||||
mainWindow.webContents.setWindowOpenHandler(({url}) => {
|
||||
if (allowedCalls.test(url)) {
|
||||
return {action: 'allow'}
|
||||
}
|
||||
utils.onBeforeOpenWindow(mainWindow.webContents, url).then(() => {
|
||||
openExternal(url)
|
||||
})
|
||||
} else {
|
||||
utils.onBeforeOpenWindow(mainWindow.webContents, url).then(() => {
|
||||
openExternal(url)
|
||||
})
|
||||
}
|
||||
return {action: 'deny'}
|
||||
})
|
||||
|
||||
@@ -418,11 +427,12 @@ function createChildWindow(args) {
|
||||
// 新窗口处理
|
||||
browser.webContents.setWindowOpenHandler(({url}) => {
|
||||
if (allowedCalls.test(url)) {
|
||||
return {action: 'allow'}
|
||||
}
|
||||
utils.onBeforeOpenWindow(browser.webContents, url).then(() => {
|
||||
openExternal(url)
|
||||
})
|
||||
} else {
|
||||
utils.onBeforeOpenWindow(browser.webContents, url).then(() => {
|
||||
openExternal(url)
|
||||
})
|
||||
}
|
||||
return {action: 'deny'}
|
||||
})
|
||||
|
||||
@@ -430,13 +440,18 @@ function createChildWindow(args) {
|
||||
electronMenu.webContentsMenu(browser.webContents)
|
||||
|
||||
// 加载地址
|
||||
const hash = args.hash || args.path;
|
||||
const hash = `${args.hash || args.path}`;
|
||||
if (/^https?:/i.test(hash)) {
|
||||
browser.loadURL(hash).then(_ => { }).catch(_ => { })
|
||||
browser.loadURL(hash)
|
||||
.then(_ => { })
|
||||
.catch(_ => { })
|
||||
} else if (isPreload) {
|
||||
browser.webContents.executeJavaScript(`if(typeof $A.goForward === 'function'){$A.goForward('${hash}')}else{throw new Error('no function')}`, true).catch(() => {
|
||||
utils.loadUrlOrFile(browser, devloadUrl, hash)
|
||||
});
|
||||
browser
|
||||
.webContents
|
||||
.executeJavaScript(`if(typeof window.__initializeApp === 'function'){window.__initializeApp('${hash}')}else{throw new Error('no function')}`, true)
|
||||
.catch(() => {
|
||||
utils.loadUrlOrFile(browser, devloadUrl, hash)
|
||||
});
|
||||
} else {
|
||||
utils.loadUrlOrFile(browser, devloadUrl, hash)
|
||||
}
|
||||
@@ -497,8 +512,15 @@ function createMediaWindow(args, type = 'image') {
|
||||
mediaWindow.addListener('close', event => {
|
||||
if (!willQuitApp) {
|
||||
event.preventDefault()
|
||||
mediaWindow.webContents.send('on-close');
|
||||
mediaWindow.hide();
|
||||
if (mediaWindow.isFullScreen()) {
|
||||
mediaWindow.once('leave-full-screen', () => {
|
||||
mediaWindow.hide();
|
||||
})
|
||||
mediaWindow.setFullScreen(false)
|
||||
} else {
|
||||
mediaWindow.webContents.send('on-close');
|
||||
mediaWindow.hide();
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -550,14 +572,8 @@ function createWebTabWindow(args) {
|
||||
args = {url: args}
|
||||
}
|
||||
|
||||
if (!allowedUrls.test(args.url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建父级窗口
|
||||
if (!webTabWindow) {
|
||||
let config = Object.assign(args.config || {}, userConf.get('webTabWindow', {}));
|
||||
let webPreferences = args.webPreferences || {};
|
||||
const titleBarOverlay = {
|
||||
height: webTabHeight
|
||||
}
|
||||
@@ -577,15 +593,21 @@ function createWebTabWindow(args) {
|
||||
autoHideMenuBar: true,
|
||||
titleBarStyle: 'hidden',
|
||||
titleBarOverlay,
|
||||
backgroundColor: nativeTheme.shouldUseDarkColors ? '#3B3B3D' : '#EFF0F4',
|
||||
webPreferences: Object.assign({
|
||||
backgroundColor: nativeTheme.shouldUseDarkColors ? '#575757' : '#FFFFFF',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'electron-preload.js'),
|
||||
webSecurity: true,
|
||||
nodeIntegration: true,
|
||||
contextIsolation: true,
|
||||
nativeWindowOpen: true
|
||||
}, webPreferences),
|
||||
}, config))
|
||||
},
|
||||
}, userConf.get('webTabWindow', {})))
|
||||
|
||||
const originalClose = webTabWindow.close;
|
||||
webTabWindow.close = function() {
|
||||
webTabClosedByShortcut = true;
|
||||
return originalClose.apply(this, arguments);
|
||||
};
|
||||
|
||||
webTabWindow.on('resize', () => {
|
||||
resizeWebTab(0)
|
||||
@@ -604,12 +626,15 @@ function createWebTabWindow(args) {
|
||||
})
|
||||
|
||||
webTabWindow.on('close', event => {
|
||||
if (!willQuitApp) {
|
||||
closeWebTab(0)
|
||||
event.preventDefault()
|
||||
} else {
|
||||
userConf.set('webTabWindow', webTabWindow.getBounds())
|
||||
if (webTabClosedByShortcut) {
|
||||
webTabClosedByShortcut = false
|
||||
if (!willQuitApp) {
|
||||
closeWebTab(0)
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
}
|
||||
userConf.set('webTabWindow', webTabWindow.getBounds())
|
||||
})
|
||||
|
||||
webTabWindow.on('closed', () => {
|
||||
@@ -636,6 +661,8 @@ function createWebTabWindow(args) {
|
||||
if (utils.isMetaOrControl(input) && input.key.toLowerCase() === 'r') {
|
||||
reloadWebTab(0)
|
||||
event.preventDefault()
|
||||
} else if (utils.isMetaOrControl(input) && input.key.toLowerCase() === 'w') {
|
||||
webTabClosedByShortcut = true
|
||||
} else if (utils.isMetaOrControl(input) && input.shift && input.key.toLowerCase() === 'i') {
|
||||
devToolsWebTab(0)
|
||||
}
|
||||
@@ -650,15 +677,20 @@ function createWebTabWindow(args) {
|
||||
webTabWindow.show();
|
||||
|
||||
// 创建 tab 子窗口
|
||||
const browserView = new BrowserView({
|
||||
const viewOptions = Object.assign({
|
||||
useHTMLTitleAndIcon: true,
|
||||
useLoadingView: true,
|
||||
useErrorView: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'electron-preload.js'),
|
||||
}
|
||||
})
|
||||
if (nativeTheme.shouldUseDarkColors) {
|
||||
}, args.config || {})
|
||||
viewOptions.webPreferences = Object.assign({
|
||||
preload: path.join(__dirname, 'electron-preload.js'),
|
||||
nodeIntegration: true,
|
||||
contextIsolation: true
|
||||
}, args.webPreferences || {})
|
||||
const browserView = new BrowserView(viewOptions)
|
||||
if (args.backgroundColor) {
|
||||
browserView.setBackgroundColor(args.backgroundColor)
|
||||
} else if (nativeTheme.shouldUseDarkColors) {
|
||||
browserView.setBackgroundColor('#575757')
|
||||
} else {
|
||||
browserView.setBackgroundColor('#FFFFFF')
|
||||
@@ -674,9 +706,10 @@ function createWebTabWindow(args) {
|
||||
})
|
||||
browserView.webContents.setWindowOpenHandler(({url}) => {
|
||||
if (allowedCalls.test(url)) {
|
||||
return {action: 'allow'}
|
||||
openExternal(url)
|
||||
} else {
|
||||
createWebTabWindow({url})
|
||||
}
|
||||
createWebTabWindow({url})
|
||||
return {action: 'deny'}
|
||||
})
|
||||
browserView.webContents.on('page-title-updated', (event, title) => {
|
||||
@@ -725,10 +758,18 @@ function createWebTabWindow(args) {
|
||||
if (utils.isMetaOrControl(input) && input.key.toLowerCase() === 'r') {
|
||||
browserView.webContents.reload()
|
||||
event.preventDefault()
|
||||
} else if (utils.isMetaOrControl(input) && input.key.toLowerCase() === 'w') {
|
||||
webTabClosedByShortcut = true
|
||||
} else if (utils.isMetaOrControl(input) && input.shift && input.key.toLowerCase() === 'i') {
|
||||
browserView.webContents.toggleDevTools()
|
||||
}
|
||||
})
|
||||
|
||||
const originalUA = browserView.webContents.session.getUserAgent() || browserView.webContents.getUserAgent()
|
||||
browserView.webContents.setUserAgent(originalUA + " SubTaskWindow/" + process.platform + "/" + os.arch() + "/1.0");
|
||||
|
||||
electronMenu.webContentsMenu(browserView.webContents, true)
|
||||
|
||||
browserView.webContents.loadURL(args.url).then(_ => { }).catch(_ => { })
|
||||
|
||||
webTabWindow.addBrowserView(browserView)
|
||||
@@ -871,7 +912,7 @@ function monitorThemeChanges() {
|
||||
preloadWindow?.setBackgroundColor(backgroundColor);
|
||||
mediaWindow?.setBackgroundColor(backgroundColor);
|
||||
childWindow.some(({browser}) => browser.setBackgroundColor(backgroundColor))
|
||||
webTabWindow?.setBackgroundColor(nativeTheme.shouldUseDarkColors ? '#3B3B3D' : '#EFF0F4')
|
||||
webTabWindow?.setBackgroundColor(nativeTheme.shouldUseDarkColors ? '#575757' : '#FFFFFF')
|
||||
// 通知所有窗口
|
||||
BrowserWindow.getAllWindows().forEach(window => {
|
||||
window.webContents.send('systemThemeChanged', {
|
||||
@@ -1002,6 +1043,16 @@ ipcMain.on('openChildWindow', (event, args) => {
|
||||
event.returnValue = "ok"
|
||||
})
|
||||
|
||||
/**
|
||||
* 显示预加载窗口(用于调试)
|
||||
*/
|
||||
ipcMain.on('showPreloadWindow', (event) => {
|
||||
if (preloadWindow) {
|
||||
onShowWindow(preloadWindow)
|
||||
}
|
||||
event.returnValue = "ok"
|
||||
})
|
||||
|
||||
/**
|
||||
* 更新路由窗口
|
||||
* @param args {?name, ?path} // name: 不是要更改的窗口名,是要把窗口名改成什么, path: 地址
|
||||
@@ -1244,13 +1295,17 @@ ipcMain.on('windowMax', (event) => {
|
||||
})
|
||||
|
||||
/**
|
||||
* 给主窗口发送信息
|
||||
* @param args {channel, data}
|
||||
* 给所有窗口广播指令(除了本身)
|
||||
* @param args {type, payload}
|
||||
*/
|
||||
ipcMain.on('sendForwardMain', (event, args) => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send(args.channel, args.data)
|
||||
}
|
||||
ipcMain.on('broadcastCommand', (event, args) => {
|
||||
const channel = args.channel || args.command
|
||||
const payload = args.payload || args.data
|
||||
BrowserWindow.getAllWindows().forEach(window => {
|
||||
if (window.webContents.id !== event.sender.id) {
|
||||
window.webContents.send(channel, payload)
|
||||
}
|
||||
})
|
||||
event.returnValue = "ok"
|
||||
})
|
||||
|
||||
@@ -1394,15 +1449,17 @@ ipcMain.handle('getStore', (event, args) => {
|
||||
//================================================================
|
||||
|
||||
let autoUpdating = 0
|
||||
autoUpdater.logger = loger
|
||||
autoUpdater.autoDownload = false
|
||||
autoUpdater.autoInstallOnAppQuit = true
|
||||
autoUpdater.on('update-available', info => {
|
||||
mainWindow.webContents.send("updateAvailable", info)
|
||||
})
|
||||
autoUpdater.on('update-downloaded', info => {
|
||||
mainWindow.webContents.send("updateDownloaded", info)
|
||||
})
|
||||
if (autoUpdater) {
|
||||
autoUpdater.logger = loger
|
||||
autoUpdater.autoDownload = false
|
||||
autoUpdater.autoInstallOnAppQuit = true
|
||||
autoUpdater.on('update-available', info => {
|
||||
mainWindow.webContents.send("updateAvailable", info)
|
||||
})
|
||||
autoUpdater.on('update-downloaded', info => {
|
||||
mainWindow.webContents.send("updateDownloaded", info)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查更新
|
||||
@@ -1412,6 +1469,9 @@ ipcMain.on('updateCheckAndDownload', (event, args) => {
|
||||
if (autoUpdating + 3600 > utils.dayjs().unix()) {
|
||||
return // 限制1小时仅执行一次
|
||||
}
|
||||
if (!autoUpdater) {
|
||||
return
|
||||
}
|
||||
if (args.provider) {
|
||||
autoUpdater.setFeedURL(args)
|
||||
}
|
||||
@@ -1477,7 +1537,7 @@ ipcMain.on('updateQuitAndInstall', (event, args) => {
|
||||
// 退出并安装更新
|
||||
setTimeout(_ => {
|
||||
mainWindow.hide()
|
||||
autoUpdater.quitAndInstall(true, true)
|
||||
autoUpdater?.quitAndInstall(true, true)
|
||||
}, 600)
|
||||
})
|
||||
|
||||
@@ -2451,10 +2511,9 @@ function windowAction(method) {
|
||||
function openExternal(url) {
|
||||
//Only open http(s), mailto, tel, and callto links
|
||||
if (allowedUrls.test(url)) {
|
||||
shell.openExternal(url);
|
||||
shell.openExternal(url).catch(_ => {});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
|
||||
<meta name="renderer" content="webkit">
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<title></title>
|
||||
<!--style-->
|
||||
|
||||
1
electron/notarize.js
vendored
1
electron/notarize.js
vendored
@@ -12,6 +12,7 @@ exports.default = async function notarizing(context) {
|
||||
}
|
||||
|
||||
return await notarize({
|
||||
tool: "notarytool",
|
||||
appBundleId: config.build.appId,
|
||||
appPath: `${appOutDir}/${appName}.app`,
|
||||
appleId: APPLEID,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"start": "electron-forge start",
|
||||
"start-quiet": "sleep 3 && electron-forge start &> /dev/null",
|
||||
"start-quiet": "sleep 3 && electron-forge start",
|
||||
"build": "electron-builder",
|
||||
"build-mac": "electron-builder --mac",
|
||||
"build-win": "electron-builder --win",
|
||||
@@ -26,17 +26,17 @@
|
||||
"url": "https://github.com/kuaifan/dootask.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^7.6.0",
|
||||
"@electron-forge/maker-deb": "^7.6.0",
|
||||
"@electron-forge/maker-rpm": "^7.6.0",
|
||||
"@electron-forge/maker-squirrel": "^7.6.0",
|
||||
"@electron-forge/maker-zip": "^7.6.0",
|
||||
"@electron-forge/cli": "^7.7.0",
|
||||
"@electron-forge/maker-deb": "^7.7.0",
|
||||
"@electron-forge/maker-rpm": "^7.7.0",
|
||||
"@electron-forge/maker-squirrel": "^7.7.0",
|
||||
"@electron-forge/maker-zip": "^7.7.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"electron": "^33.2.1",
|
||||
"electron": "^34.3.4",
|
||||
"electron-builder": "^25.1.8",
|
||||
"electron-notarize": "^1.2.2",
|
||||
"form-data": "^4.0.1",
|
||||
"inquirer": "^8.2.0",
|
||||
"inquirer": "^12.4.2",
|
||||
"ora": "^4.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -108,7 +108,14 @@
|
||||
"target": "dmg",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
"arm64",
|
||||
"universal"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "pkg",
|
||||
"arch": [
|
||||
"universal"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
54
electron/render/tabs/assets/css/style.css
vendored
54
electron/render/tabs/assets/css/style.css
vendored
@@ -23,11 +23,17 @@ html, body {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.app {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav {
|
||||
font-family: var(--tab-font-family);
|
||||
font-feature-settings: 'clig', 'kern';
|
||||
flex: 1;
|
||||
width: 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
cursor: default;
|
||||
background-color: var(--tab-background);
|
||||
-webkit-app-region: drag;
|
||||
@@ -35,8 +41,8 @@ html, body {
|
||||
|
||||
.nav ul {
|
||||
display: flex;
|
||||
height: 30px;
|
||||
margin: 8px 46px 0 0;
|
||||
height: 35px;
|
||||
margin-top: 5px;
|
||||
user-select: none;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
@@ -51,7 +57,7 @@ html, body {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
height: calc(100% - 5px);
|
||||
padding: 7px 8px;
|
||||
margin: 0 8px 0 0;
|
||||
min-width: 100px;
|
||||
@@ -73,31 +79,7 @@ html, body {
|
||||
.nav ul li.active {
|
||||
color: var(--tab-active-color);
|
||||
background: var(--tab-active-background);
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
|
||||
.nav ul li.active::before {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: -6px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-image: url(../image/select_left.png);
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.nav ul li.active::after {
|
||||
position: absolute;
|
||||
right: -6px;
|
||||
bottom: 0;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-image: url(../image/select_right.png);
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
content: '';
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.nav ul li.active .tab-icon.background {
|
||||
@@ -120,14 +102,13 @@ html, body {
|
||||
|
||||
/* 浏览器打开 */
|
||||
.browser {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 38px;
|
||||
height: 40px;
|
||||
padding: 0 14px;
|
||||
cursor: pointer;
|
||||
background-color: var(--tab-background);
|
||||
-webkit-app-region: none;
|
||||
}
|
||||
.browser span {
|
||||
@@ -247,13 +228,6 @@ body.darwin.full-screen .nav ul {
|
||||
--tab-active-background: #575757;
|
||||
--tab-close-color: #E3E3E3;
|
||||
}
|
||||
.nav ul li.active::before {
|
||||
background-image: url(../image/dark/select_left.png);
|
||||
}
|
||||
|
||||
.nav ul li.active::after {
|
||||
background-image: url(../image/dark/select_right.png);
|
||||
}
|
||||
|
||||
.nav ul li.active .tab-icon.background {
|
||||
background-image: url(../image/dark/link_normal_selected_icon.png);
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 205 B |
Binary file not shown.
|
Before Width: | Height: | Size: 219 B |
Binary file not shown.
|
Before Width: | Height: | Size: 235 B |
Binary file not shown.
|
Before Width: | Height: | Size: 236 B |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user