Compare commits
273 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
53f2e07178 | ||
|
|
3aa2c604d8 | ||
|
|
d8fbf36e00 | ||
|
|
008653e3d9 | ||
|
|
23188777fe | ||
|
|
8eb0a49ee6 | ||
|
|
207f09a4af | ||
|
|
69120c5045 | ||
|
|
b8143d1a9b | ||
|
|
f7eab5893a | ||
|
|
5fc598a220 | ||
|
|
783c21ad18 | ||
|
|
a1ce6e6928 | ||
|
|
8cbae629a5 | ||
|
|
da7e832f21 | ||
|
|
a572ba0523 | ||
|
|
85a20168dc | ||
|
|
25be9c0fef | ||
|
|
a8c890ba51 | ||
|
|
11628b98ca | ||
|
|
4ae6ca945b | ||
|
|
49aa1434aa | ||
|
|
9e92c61fbf | ||
|
|
c84111b6b9 | ||
|
|
3a2fcdd18a | ||
|
|
84a800f69b | ||
|
|
77e08aa048 | ||
|
|
0d6fd903f1 | ||
|
|
bcc74dd927 | ||
|
|
dd0720afa7 | ||
|
|
a06a4095b6 | ||
|
|
29bc009c07 | ||
|
|
520d2a0e20 | ||
|
|
dbeb9dd561 | ||
|
|
5b02d8008f | ||
|
|
a032c6114f | ||
|
|
69ec4966d5 | ||
|
|
87fab80ea3 | ||
|
|
bd2dabe851 | ||
|
|
4a45d69e5b | ||
|
|
e15bea9342 | ||
|
|
7132413837 | ||
|
|
c51116acaa | ||
|
|
002776f15e | ||
|
|
c7f5c62e71 | ||
|
|
3c57cf8d81 | ||
|
|
f29bf3640a | ||
|
|
07663dea6c | ||
|
|
0ddb696e90 | ||
|
|
cc0a6d4706 | ||
|
|
4c0ecc8f07 | ||
|
|
d50c8ce691 | ||
|
|
8aa66661ac | ||
|
|
00a9b3b57b |
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
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -15,6 +15,7 @@
|
||||
.idea
|
||||
.vscode
|
||||
.vagrant
|
||||
.windsurfrules
|
||||
.phpunit.result.cache
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
|
||||
116
.prefetch
116
.prefetch
@@ -1,55 +1,115 @@
|
||||
office/web-apps/apps/api/documents/api.js?hash={version}
|
||||
|
||||
office/{path}/fonts/000
|
||||
office/{path}/fonts/020
|
||||
office/{path}/fonts/020
|
||||
office/{path}/fonts/001
|
||||
office/{path}/fonts/002
|
||||
office/{path}/fonts/020
|
||||
office/{path}/fonts/022
|
||||
office/{path}/fonts/022
|
||||
office/{path}/fonts/022
|
||||
office/{path}/fonts/023
|
||||
office/{path}/fonts/023
|
||||
office/{path}/fonts/023
|
||||
office/{path}/fonts/024
|
||||
office/{path}/fonts/024
|
||||
office/{path}/fonts/024
|
||||
office/{path}/fonts/027
|
||||
office/{path}/fonts/027
|
||||
office/{path}/fonts/028
|
||||
office/{path}/fonts/028
|
||||
office/{path}/fonts/029
|
||||
office/{path}/fonts/029
|
||||
office/{path}/fonts/030
|
||||
office/{path}/fonts/030
|
||||
office/{path}/fonts/036
|
||||
office/{path}/fonts/036
|
||||
office/{path}/fonts/037
|
||||
office/{path}/fonts/037
|
||||
office/{path}/fonts/038
|
||||
office/{path}/fonts/038
|
||||
office/{path}/fonts/039
|
||||
office/{path}/fonts/039
|
||||
office/{path}/fonts/058
|
||||
office/{path}/fonts/058
|
||||
office/{path}/fonts/050
|
||||
office/{path}/fonts/051
|
||||
office/{path}/fonts/052
|
||||
office/{path}/fonts/053
|
||||
office/{path}/fonts/058
|
||||
office/{path}/fonts/059
|
||||
office/{path}/fonts/059
|
||||
office/{path}/fonts/059
|
||||
office/{path}/fonts/060
|
||||
office/{path}/fonts/060
|
||||
office/{path}/fonts/060
|
||||
office/{path}/fonts/061
|
||||
office/{path}/fonts/061
|
||||
office/{path}/fonts/061
|
||||
office/{path}/fonts/062
|
||||
office/{path}/fonts/063
|
||||
office/{path}/fonts/064
|
||||
office/{path}/fonts/065
|
||||
office/{path}/fonts/066
|
||||
office/{path}/fonts/067
|
||||
office/{path}/fonts/068
|
||||
office/{path}/fonts/069
|
||||
office/{path}/fonts/070
|
||||
office/{path}/fonts/071
|
||||
office/{path}/fonts/072
|
||||
office/{path}/fonts/073
|
||||
office/{path}/fonts/074
|
||||
office/{path}/fonts/075
|
||||
office/{path}/fonts/076
|
||||
office/{path}/fonts/077
|
||||
office/{path}/fonts/078
|
||||
office/{path}/fonts/079
|
||||
office/{path}/fonts/080
|
||||
office/{path}/fonts/081
|
||||
office/{path}/fonts/081
|
||||
office/{path}/fonts/081
|
||||
office/{path}/fonts/086
|
||||
office/{path}/fonts/091
|
||||
office/{path}/fonts/092
|
||||
office/{path}/fonts/093
|
||||
office/{path}/fonts/094
|
||||
office/{path}/fonts/095
|
||||
office/{path}/fonts/096
|
||||
office/{path}/fonts/097
|
||||
office/{path}/fonts/098
|
||||
office/{path}/fonts/099
|
||||
office/{path}/fonts/100
|
||||
office/{path}/fonts/101
|
||||
office/{path}/fonts/102
|
||||
office/{path}/fonts/103
|
||||
office/{path}/fonts/131
|
||||
office/{path}/fonts/132
|
||||
office/{path}/fonts/133
|
||||
office/{path}/fonts/134
|
||||
office/{path}/fonts/135
|
||||
office/{path}/fonts/136
|
||||
office/{path}/fonts/137
|
||||
office/{path}/fonts/138
|
||||
office/{path}/fonts/139
|
||||
office/{path}/fonts/140
|
||||
office/{path}/fonts/141
|
||||
office/{path}/fonts/142
|
||||
office/{path}/fonts/143
|
||||
office/{path}/fonts/145
|
||||
office/{path}/fonts/147
|
||||
office/{path}/fonts/152
|
||||
office/{path}/fonts/154
|
||||
office/{path}/fonts/177
|
||||
office/{path}/fonts/178
|
||||
office/{path}/fonts/179
|
||||
office/{path}/fonts/180
|
||||
office/{path}/fonts/181
|
||||
office/{path}/fonts/182
|
||||
office/{path}/fonts/183
|
||||
office/{path}/fonts/184
|
||||
office/{path}/fonts/184
|
||||
office/{path}/fonts/185
|
||||
office/{path}/fonts/186
|
||||
office/{path}/fonts/187
|
||||
office/{path}/fonts/188
|
||||
office/{path}/fonts/189
|
||||
office/{path}/fonts/190
|
||||
office/{path}/fonts/191
|
||||
office/{path}/fonts/192
|
||||
office/{path}/fonts/193
|
||||
office/{path}/fonts/198
|
||||
office/{path}/fonts/199
|
||||
office/{path}/fonts/200
|
||||
office/{path}/fonts/201
|
||||
office/{path}/fonts/202
|
||||
office/{path}/fonts/203
|
||||
office/{path}/fonts/204
|
||||
office/{path}/fonts/205
|
||||
office/{path}/fonts/206
|
||||
office/{path}/fonts/207
|
||||
office/{path}/fonts/208
|
||||
office/{path}/fonts/209
|
||||
office/{path}/fonts/210
|
||||
office/{path}/fonts/211
|
||||
office/{path}/fonts/212
|
||||
office/{path}/fonts/214
|
||||
office/{path}/fonts/215
|
||||
office/{path}/fonts/216
|
||||
office/{path}/fonts/217
|
||||
office/{path}/sdkjs/cell/sdk-all-min.js
|
||||
office/{path}/sdkjs/cell/sdk-all.js
|
||||
office/{path}/sdkjs/common/AllFonts.js
|
||||
@@ -107,4 +167,4 @@ drawio/webapp/styles/grapheditor.css
|
||||
|
||||
minder/css/chunk-vendors.fe9c56c6.css
|
||||
minder/js/app.aa385de3.js
|
||||
minder/js/chunk-vendors.cc7455b8.js
|
||||
minder/js/chunk-vendors.cc7455b8.js
|
||||
|
||||
263
CHANGELOG.md
263
CHANGELOG.md
@@ -2,6 +2,269 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [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
|
||||
|
||||
- 桌面端查看表情图片缩略图显示错误
|
||||
- 项目面板任务不显示的情况
|
||||
- 修复移动任务子任务不跟随的情况
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化桌面端数据处理
|
||||
- 优化资源
|
||||
- 优化数据流
|
||||
|
||||
## [0.41.93]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 复制文件权限判断
|
||||
|
||||
### Performance
|
||||
|
||||
- AI创建任务确认
|
||||
- 优化项目面板
|
||||
|
||||
## [0.41.84]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- @在线状态不正确
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化子任务上下文
|
||||
- 优化子任务时间调整
|
||||
- 优化超长文本信息
|
||||
- 记录版本信息
|
||||
- 支持更多办公文件格式
|
||||
- 请假或外出时取消打卡提醒
|
||||
- 图片容错处理
|
||||
- 优化全局监听事件
|
||||
- 优化数据流消息
|
||||
|
||||
## [0.41.64]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复使用AI创建任务顺序错误的问题
|
||||
|
||||
## [0.41.55]
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化媒体播放
|
||||
- 优化临时会话的消息推送
|
||||
- 优化任务时间显示
|
||||
|
||||
## [0.41.45]
|
||||
|
||||
### 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
|
||||
|
||||
254
app/Console/Commands/SyncDialogUserMsgToElasticsearch.php
Normal file
254
app/Console/Commands/SyncDialogUserMsgToElasticsearch.php
Normal file
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Models\WebSocketDialogUser;
|
||||
use App\Module\ElasticSearch\ElasticSearchKeyValue;
|
||||
use App\Module\ElasticSearch\ElasticSearchUserMsg;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SyncDialogUserMsgToElasticsearch extends Command
|
||||
{
|
||||
/**
|
||||
* 更新数据
|
||||
* --f: 全量更新 (默认)
|
||||
* --i: 增量更新(从上次更新的最后一个ID接上)
|
||||
*
|
||||
* 清理数据
|
||||
* --c: 清除索引
|
||||
*/
|
||||
|
||||
protected $signature = 'elasticsearch:sync-dialog-user-msg {--f} {--i} {--c} {--batch=500}';
|
||||
protected $description = '同步聊天会话用户和消息到Elasticsearch';
|
||||
protected $es;
|
||||
|
||||
/**
|
||||
* SyncDialogUserMsgToElasticsearch constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
try {
|
||||
$this->es = new ElasticSearchUserMsg();
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Elasticsearch连接失败: ' . $e->getMessage());
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('开始同步聊天数据...');
|
||||
|
||||
// 清除索引
|
||||
if ($this->option('c')) {
|
||||
$this->info('清除索引...');
|
||||
if (!$this->es->indexExists()) {
|
||||
$this->saveLastId(true);
|
||||
$this->info('索引不存在');
|
||||
return 0;
|
||||
}
|
||||
$result = $this->es->deleteIndex();
|
||||
if (isset($result['error'])) {
|
||||
$this->error('删除索引失败: ' . $result['error']);
|
||||
return 1;
|
||||
}
|
||||
$this->saveLastId(true);
|
||||
$this->info('索引删除成功');
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 判断创建索引
|
||||
if (!$this->es->indexExists()) {
|
||||
$this->info('创建索引...');
|
||||
$result = ElasticSearchUserMsg::generateIndex();
|
||||
if (isset($result['error'])) {
|
||||
$this->error('创建索引失败: ' . $result['error']);
|
||||
return 1;
|
||||
}
|
||||
$this->saveLastId(true);
|
||||
$this->info('索引创建成功');
|
||||
}
|
||||
|
||||
// 同步用户-会话数据
|
||||
$this->syncDialogUsers($this->option('batch'));
|
||||
|
||||
// 同步消息数据
|
||||
$this->syncDialogMsgs($this->option('batch'));
|
||||
|
||||
// 完成
|
||||
$this->info("\n同步完成");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存最后一个ID
|
||||
* @param string|true $type
|
||||
* @param integer $lastId
|
||||
*/
|
||||
private function saveLastId($type, $lastId = 0)
|
||||
{
|
||||
if ($type === true) {
|
||||
$setting = [];
|
||||
} else {
|
||||
$setting = ElasticSearchKeyValue::getArray('elasticSearch:sync');
|
||||
$setting[$type] = $lastId;
|
||||
}
|
||||
ElasticSearchKeyValue::save('elasticSearch:sync', $setting);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最后一个ID
|
||||
* @param $type
|
||||
* @return int
|
||||
*/
|
||||
private function getLastId($type)
|
||||
{
|
||||
if ($this->option('i')) {
|
||||
$setting = ElasticSearchKeyValue::getArray('elasticSearch:sync');
|
||||
return intval($setting[$type] ?? 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步用户-会话数据(父文档)
|
||||
* @param $batchSize
|
||||
* @return void
|
||||
*/
|
||||
private function syncDialogUsers($batchSize)
|
||||
{
|
||||
$this->info("\n同步用户数据...");
|
||||
$lastId = $this->getLastId('dialog_user');
|
||||
|
||||
$num = 0;
|
||||
$count = WebSocketDialogUser::where('id', '>', $lastId)->count();
|
||||
|
||||
do {
|
||||
// 获取一批用户-会话关系
|
||||
$dialogUsers = WebSocketDialogUser::where('id', '>', $lastId)
|
||||
->orderBy('id')
|
||||
->limit($batchSize)
|
||||
->get();
|
||||
|
||||
if ($dialogUsers->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
$num += count($dialogUsers);
|
||||
$progress = round($num / $count * 100, 2);
|
||||
$this->info("{$num}/{$count} ({$progress}%) 正在同步用户ID {$lastId} ~ {$dialogUsers->last()->id}");
|
||||
|
||||
// 批量索引数据
|
||||
$params = ['body' => []];
|
||||
foreach ($dialogUsers as $dialogUser) {
|
||||
$params['body'][] = [
|
||||
'index' => [
|
||||
'_index' => ElasticSearchUserMsg::indexName(),
|
||||
'_id' => ElasticSearchUserMsg::generateUserDicId($dialogUser),
|
||||
]
|
||||
];
|
||||
$params['body'][] = ElasticSearchUserMsg::generateUserFormat($dialogUser);
|
||||
}
|
||||
|
||||
if ($params['body']) {
|
||||
$result = $this->es->bulk($params);
|
||||
if (isset($result['errors']) && $result['errors']) {
|
||||
$this->error('批量索引用户数据部分失败');
|
||||
Log::error('Elasticsearch批量索引失败: ' . json_encode($result['items']));
|
||||
}
|
||||
}
|
||||
|
||||
$lastId = $dialogUsers->last()->id;
|
||||
$this->saveLastId('dialog_user', $lastId);
|
||||
} while (count($dialogUsers) == $batchSize);
|
||||
|
||||
$this->info("同步用户数据结束 - 最后ID {$lastId}");
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步消息数据(子文档)
|
||||
*/
|
||||
private function syncDialogMsgs($batchSize)
|
||||
{
|
||||
$this->info("\n同步消息数据...");
|
||||
$lastId = $this->getLastId('dialog_msg');
|
||||
|
||||
$num = 0;
|
||||
$count = WebSocketDialogMsg::where('id', '>', $lastId)->count();
|
||||
|
||||
do {
|
||||
// 获取一批消息
|
||||
$dialogMsgs = WebSocketDialogMsg::where('id', '>', $lastId)
|
||||
->orderBy('id')
|
||||
->limit($batchSize)
|
||||
->get();
|
||||
|
||||
if ($dialogMsgs->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
$num += count($dialogMsgs);
|
||||
$progress = round($num / $count * 100, 2);
|
||||
$this->info("{$num}/{$count} ({$progress}%) 正在同步消息ID {$lastId} ~ {$dialogMsgs->last()->id}");
|
||||
|
||||
// 获取这些消息所属的会话对应的所有用户
|
||||
$dialogIds = $dialogMsgs->pluck('dialog_id')->unique()->toArray();
|
||||
$userDialogMap = [];
|
||||
|
||||
if (!empty($dialogIds)) {
|
||||
$dialogUsers = WebSocketDialogUser::whereIn('dialog_id', $dialogIds)->get();
|
||||
|
||||
foreach ($dialogUsers as $dialogUser) {
|
||||
$userDialogMap[$dialogUser->dialog_id][] = $dialogUser->userid;
|
||||
}
|
||||
}
|
||||
|
||||
// 批量索引消息数据
|
||||
$params = ['body' => []];
|
||||
foreach ($dialogMsgs as $dialogMsg) {
|
||||
// 如果该会话没有用户,跳过
|
||||
if (empty($userDialogMap[$dialogMsg->dialog_id])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 为每个用户-会话关系创建子文档
|
||||
foreach ($userDialogMap[$dialogMsg->dialog_id] as $userid) {
|
||||
$params['body'][] = [
|
||||
'index' => [
|
||||
'_index' => ElasticSearchUserMsg::indexName(),
|
||||
'_id' => ElasticSearchUserMsg::generateMsgDicId($dialogMsg, $userid),
|
||||
'routing' => ElasticSearchUserMsg::generateMsgParentId($dialogMsg, $userid) // 路由到父文档
|
||||
]
|
||||
];
|
||||
|
||||
$params['body'][] = ElasticSearchUserMsg::generateMsgFormat($dialogMsg, $userid);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($params['body'])) {
|
||||
// 分批处理
|
||||
$chunks = array_chunk($params['body'], 1000);
|
||||
foreach ($chunks as $chunk) {
|
||||
$chunkParams = ['body' => $chunk];
|
||||
$result = $this->es->bulk($chunkParams);
|
||||
if (isset($result['errors']) && $result['errors']) {
|
||||
$this->error('批量索引消息数据部分失败');
|
||||
Log::error('Elasticsearch批量索引失败: ' . json_encode($result['items']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$lastId = $dialogMsgs->last()->id;
|
||||
$this->saveLastId('dialog_msg', $lastId);
|
||||
} while (count($dialogMsgs) == $batchSize);
|
||||
|
||||
$this->info("同步消息结束 - 最后ID {$lastId}");
|
||||
}
|
||||
}
|
||||
@@ -223,6 +223,19 @@ class Handler extends ExceptionHandler
|
||||
} catch (\ImagickException) { }
|
||||
}
|
||||
|
||||
// 容错处理
|
||||
$patternFault = '/^(images\/.*\.(png|jpg|jpeg))\/crop\/([^\/]+)$/';
|
||||
$matchesFault = null;
|
||||
if (preg_match($patternFault, $path, $matchesFault)) {
|
||||
$file = public_path($matchesFault[1]);
|
||||
if (!file_exists($file)) {
|
||||
$file = public_path('images/other/imgerr.jpg');
|
||||
}
|
||||
if (file_exists($file)) {
|
||||
return response()->file($file);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ use App\Tasks\PushTask;
|
||||
use App\Module\BillExport;
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Models\ApproveProcMsg;
|
||||
use App\Models\ApproveProcInstHistory;
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Models\UserDepartment;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
@@ -1146,13 +1147,9 @@ class ApproveController extends AbstractController
|
||||
*/
|
||||
public function user__status()
|
||||
{
|
||||
$data['userid'] = intval(Request::input('userid'));
|
||||
$ret = Ihttp::ihttp_get($this->flow_url . '/api/v1/workflow/process/getUserApprovalStatus?' . http_build_query($data));
|
||||
$procdef = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||
if (isset($procdef['status']) && $procdef['status'] == 200) {
|
||||
return Base::retSuccess('success', $procdef['data']["proc_def_name"] ?? '');
|
||||
}
|
||||
return Base::retSuccess('success', '');
|
||||
$userid = intval(Request::input('userid'));
|
||||
$status = ApproveProcInstHistory::getUserApprovalStatus($userid);
|
||||
return Base::retSuccess('success', $status);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,7 +13,10 @@ use App\Models\User;
|
||||
use App\Module\Base;
|
||||
use App\Module\Timer;
|
||||
use App\Module\Extranet;
|
||||
use App\Module\ElasticSearch\ElasticSearchUserMsg;
|
||||
use App\Module\TimeRange;
|
||||
use App\Module\MsgTool;
|
||||
use App\Module\Table\OnlineData;
|
||||
use App\Models\FileContent;
|
||||
use App\Models\AbstractModel;
|
||||
use App\Models\WebSocketDialog;
|
||||
@@ -23,6 +26,7 @@ use App\Models\WebSocketDialogConfig;
|
||||
use App\Models\WebSocketDialogMsgRead;
|
||||
use App\Models\WebSocketDialogMsgTodo;
|
||||
use App\Models\WebSocketDialogMsgTranslate;
|
||||
use App\Models\WebSocketDialogSession;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
|
||||
/**
|
||||
@@ -168,27 +172,15 @@ class DialogController extends AbstractController
|
||||
}
|
||||
// 搜索消息会话
|
||||
if (count($list) < 20) {
|
||||
$prefix = DB::getTablePrefix();
|
||||
if (preg_match('/[+\-><()~*"@]/', $key)) {
|
||||
$against = "\"{$key}\"";
|
||||
} else {
|
||||
$against = "*{$key}*";
|
||||
$searchResults = ElasticSearchUserMsg::searchByKeyword($user->userid, $key, 20 - 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)
|
||||
->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);
|
||||
@@ -255,6 +247,9 @@ class DialogController extends AbstractController
|
||||
->where('d.id', $dialog_id)
|
||||
->whereNull('d.deleted_at')
|
||||
->first();
|
||||
if (empty($item)) {
|
||||
return Base::retError('不在成员列表内');
|
||||
}
|
||||
return Base::retSuccess('success', WebSocketDialog::synthesizeData($item, $user->userid));
|
||||
}
|
||||
|
||||
@@ -287,6 +282,9 @@ class DialogController extends AbstractController
|
||||
$array = array_filter($data->toArray(), function ($item) {
|
||||
return $item['userid'] > 0;
|
||||
});
|
||||
foreach ($array as &$item) {
|
||||
$item['online'] = $item['bot'] || OnlineData::live($item['userid']) > 0;
|
||||
}
|
||||
} else {
|
||||
$data = WebSocketDialogUser::select(['web_socket_dialog_users.*', 'users.bot'])
|
||||
->join('users', 'web_socket_dialog_users.userid', '=', 'users.userid')
|
||||
@@ -530,6 +528,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);
|
||||
@@ -711,7 +712,44 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/one 15. 获取单条消息
|
||||
* @api {get} api/dialog/msg/esearch 15. 搜索消息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup dialog
|
||||
* @apiName msg__esearch
|
||||
*
|
||||
* @apiParam {String} key 搜索关键词
|
||||
* @apiParam {Number} [pagesize] 每页显示数量,默认:20,最大:50
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function msg__esearch()
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$key = trim(Request::input('key'));
|
||||
$list = [];
|
||||
//
|
||||
$searchResults = ElasticSearchUserMsg::searchByKeyword($user->userid, $key, Base::getPaginate(50, 20));
|
||||
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,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/one 16. 获取单条消息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -740,7 +778,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/dot 16. 聊天消息去除点
|
||||
* @api {get} api/dialog/msg/dot 17. 聊天消息去除点
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -773,7 +811,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/read 17. 已读聊天消息
|
||||
* @api {get} api/dialog/msg/read 18. 已读聊天消息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -844,7 +882,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/unread 18. 获取未读消息数据
|
||||
* @api {get} api/dialog/msg/unread 19. 获取未读消息数据
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -887,7 +925,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/checked 19. 设置消息checked
|
||||
* @api {get} api/dialog/msg/checked 20. 设置消息checked
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -953,7 +991,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/dialog/msg/stream 20. 通知成员监听消息
|
||||
* @api {post} api/dialog/msg/stream 21. 通知成员监听消息
|
||||
*
|
||||
* @apiDescription 通知指定会员EventSource监听流动消息
|
||||
* @apiVersion 1.0.0
|
||||
@@ -997,7 +1035,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/dialog/msg/sendtext 21. 发送消息
|
||||
* @api {post} api/dialog/msg/sendtext 22. 发送消息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1018,10 +1056,11 @@ class DialogController extends AbstractController
|
||||
* @apiParam {Number} [reply_id] 回复ID
|
||||
* @apiParam {String} [reply_check] 配合 reply_id 使用,判断是否需要验证回复ID的有效性
|
||||
* - no: 不进行判断,直接使用提供的 reply_id(默认)
|
||||
* - yes: 进行判断,如果上一条消息的 ID 为 reply_id,则认为 reply_id 无效
|
||||
* - yes: 进行判断,如果 reply_id 到最新消息都没有会员发的消息,则 reply_id 无效
|
||||
* @apiParam {String} [silence] 是否静默发送
|
||||
* - no: 正常发送(默认)
|
||||
* - yes: 静默发送
|
||||
* @apiParam {String} [model_name] 模型名称(仅AI机器人支持)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -1042,6 +1081,7 @@ 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 = [];
|
||||
@@ -1055,8 +1095,12 @@ class DialogController extends AbstractController
|
||||
} elseif ($reply_id > 0) {
|
||||
$action = "reply-$reply_id";
|
||||
if ($reply_check === 'yes') {
|
||||
$lastMsgId = WebSocketDialogMsg::whereDialogId($dialog_id)->orderByDesc('id')->value('id');
|
||||
if ($lastMsgId == $reply_id) {
|
||||
$exisUserMsg = WebSocketDialogMsg::whereDialogId($dialog_id)
|
||||
->where('id', '>', $reply_id)
|
||||
->whereBot(0)
|
||||
->whereNotIn('type', ['notice', 'template'])
|
||||
->exists();
|
||||
if (!$exisUserMsg) {
|
||||
$action = "";
|
||||
}
|
||||
}
|
||||
@@ -1086,27 +1130,46 @@ class DialogController extends AbstractController
|
||||
if (empty($size)) {
|
||||
return Base::retError('消息发送保存失败');
|
||||
}
|
||||
$ext = $markdown ? 'md' : 'htm';
|
||||
$fileData = [
|
||||
'name' => "LongText-{$strlen}.{$ext}",
|
||||
'size' => $size,
|
||||
'file' => $file,
|
||||
'path' => $path,
|
||||
'url' => Base::fillUrl($path),
|
||||
'thumb' => '',
|
||||
'width' => -1,
|
||||
'height' => -1,
|
||||
'ext' => $ext,
|
||||
$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, // 简要内容
|
||||
'file' => [
|
||||
'name' => "LongText-{$strlen}.{$type}",
|
||||
'size' => $size,
|
||||
'file' => $file,
|
||||
'path' => $path,
|
||||
'url' => Base::fillUrl($path),
|
||||
'thumb' => '',
|
||||
'width' => -1,
|
||||
'height' => -1,
|
||||
'ext' => $type,
|
||||
],
|
||||
];
|
||||
if (empty($key)) {
|
||||
$key = mb_substr(strip_tags($text), 0, 200);
|
||||
$key = $desc;
|
||||
}
|
||||
$result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'file', $fileData, $user->userid, false, false, $silence, $key);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1114,7 +1177,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/dialog/msg/sendnotice 22. 发送通知
|
||||
* @api {post} api/dialog/msg/sendnotice 23. 发送通知
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1167,7 +1230,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/dialog/msg/sendtemplate 23. 发送模板消息
|
||||
* @api {post} api/dialog/msg/sendtemplate 24. 发送模板消息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1236,7 +1299,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/dialog/msg/sendrecord 24. 发送语音
|
||||
* @api {post} api/dialog/msg/sendrecord 25. 发送语音
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1284,7 +1347,79 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/dialog/msg/sendfile 25. 文件上传
|
||||
* @api {post} api/dialog/msg/convertrecord 26. 录音转文字
|
||||
*
|
||||
* @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 27. 文件上传
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1316,7 +1451,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/dialog/msg/sendfiles 26. 群发文件上传
|
||||
* @api {post} api/dialog/msg/sendfiles 28. 群发文件上传
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1372,7 +1507,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/sendfileid 27. 通过文件ID发送文件
|
||||
* @api {get} api/dialog/msg/sendfileid 29. 通过文件ID发送文件
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1442,7 +1577,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/dialog/msg/sendanon 28. 发送匿名消息
|
||||
* @api {post} api/dialog/msg/sendanon 30. 发送匿名消息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1498,7 +1633,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
|
||||
@@ -1558,7 +1693,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
|
||||
@@ -1587,7 +1722,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
|
||||
@@ -1629,13 +1764,25 @@ class DialogController extends AbstractController
|
||||
$msg = File::formatFileData($msg);
|
||||
$data['content'] = $msg['content'];
|
||||
$data['file_mode'] = $msg['file_mode'];
|
||||
} elseif ($data['type'] == 'longtext') {
|
||||
$data['content'] = [
|
||||
'type' => 'htm',
|
||||
'content' => Doo::translate("内容不存在")
|
||||
];
|
||||
if (isset($data['msg']['file']['path'])) {
|
||||
$filePath = public_path($data['msg']['file']['path']);
|
||||
if (file_exists($filePath)) {
|
||||
$data['content']['type'] = $data['msg']['type'];
|
||||
$data['content']['content'] = file_get_contents($filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
return Base::retSuccess('success', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/download 32. 文件下载
|
||||
* @api {get} api/dialog/msg/download 34. 文件下载
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1672,11 +1819,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
|
||||
@@ -1702,7 +1849,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/voice2text 34. 语音消息转文字
|
||||
* @api {get} api/dialog/msg/voice2text 36. 语音消息转文字
|
||||
*
|
||||
* @apiDescription 将语音消息转文字,需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1754,7 +1901,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
|
||||
@@ -1762,6 +1909,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错误)
|
||||
@@ -1773,6 +1922,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);
|
||||
//
|
||||
@@ -1790,13 +1940,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;
|
||||
@@ -1813,7 +1970,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
|
||||
@@ -1877,7 +2034,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
|
||||
@@ -1940,7 +2097,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 +2138,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
|
||||
@@ -2016,7 +2173,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
|
||||
@@ -2045,7 +2202,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
|
||||
@@ -2088,7 +2245,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
|
||||
@@ -2118,7 +2275,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
|
||||
@@ -2171,7 +2328,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
|
||||
@@ -2212,7 +2369,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
|
||||
@@ -2274,7 +2431,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
|
||||
@@ -2336,7 +2493,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/group/adduser 47. 添加群成员
|
||||
* @api {get} api/dialog/group/adduser 49. 添加群成员
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* - 有群主时:只有群主可以邀请
|
||||
@@ -2372,7 +2529,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/group/deluser 48. 移出(退出)群成员
|
||||
* @api {get} api/dialog/group/deluser 50. 移出(退出)群成员
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* - 只有群主、邀请人可以踢人
|
||||
@@ -2416,7 +2573,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/group/transfer 49. 转让群组
|
||||
* @api {get} api/dialog/group/transfer 51. 转让群组
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* - 只有群主且是个人类型群可以解散
|
||||
@@ -2465,7 +2622,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/group/disband 50. 解散群组
|
||||
* @api {get} api/dialog/group/disband 52. 解散群组
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* - 只有群主且是个人类型群可以解散
|
||||
@@ -2493,7 +2650,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
|
||||
@@ -2522,7 +2679,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
|
||||
@@ -2561,7 +2718,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
|
||||
@@ -2597,7 +2754,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
|
||||
@@ -2683,7 +2840,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
|
||||
@@ -2799,7 +2956,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
|
||||
@@ -2859,7 +3016,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
|
||||
@@ -2886,7 +3043,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
|
||||
@@ -2935,7 +3092,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
|
||||
@@ -2959,7 +3116,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/config 60. 获取会话配置
|
||||
* @api {get} api/dialog/config 62. 获取会话配置
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -2995,7 +3152,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
|
||||
@@ -3033,10 +3190,138 @@ 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
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function session__create()
|
||||
{
|
||||
User::auth();
|
||||
//
|
||||
$dialog_id = intval(Request::input('dialog_id'));
|
||||
//
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +106,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 +119,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;
|
||||
@@ -301,6 +302,7 @@ class FileController extends AbstractController
|
||||
//
|
||||
$userid = $user->userid;
|
||||
if ($row->pid > 0) {
|
||||
File::permissionFind($row->pid, $user, 1);
|
||||
$userid = intval(File::whereId($row->pid)->value('userid'));
|
||||
}
|
||||
//
|
||||
|
||||
@@ -941,7 +941,8 @@ class ProjectController extends AbstractController
|
||||
* @apiName task__lists
|
||||
*
|
||||
* @apiParam {Object} [keys] 搜索条件
|
||||
* - keys.name: ID、任务名称
|
||||
* - keys.name: ID、任务名称、任务描述
|
||||
* - keys.tag: 标签名称
|
||||
*
|
||||
* @apiParam {Number} [project_id] 项目ID
|
||||
* @apiParam {Number} [parent_id] 主任务ID(project_id && parent_id ≤ 0 时 仅查询自己参与的任务)
|
||||
@@ -994,9 +995,17 @@ 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']);
|
||||
});
|
||||
}
|
||||
//
|
||||
$scopeAll = false;
|
||||
if ($parent_id > 0) {
|
||||
@@ -1058,20 +1067,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 +1168,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',
|
||||
@@ -1852,7 +1862,7 @@ class ProjectController extends AbstractController
|
||||
}
|
||||
//
|
||||
$filePath = public_path($file->getRawOriginal('path'));
|
||||
return Base::BinaryFileResponse($filePath, $file->name);
|
||||
return Base::DownloadFileResponse($filePath, $file->name);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2399,6 +2409,7 @@ class ProjectController extends AbstractController
|
||||
*/
|
||||
public function task__move()
|
||||
{
|
||||
Base::checkClientVersion('0.42.0');
|
||||
User::auth();
|
||||
//
|
||||
$task_id = intval(Request::input('task_id'));
|
||||
@@ -2436,9 +2447,26 @@ class ProjectController extends AbstractController
|
||||
//
|
||||
$task->moveTask($project_id, $column_id, $flow_item_id, $owner, $assist, $completeAt);
|
||||
//
|
||||
$task = ProjectTask::userTask($task_id);
|
||||
$data = [];
|
||||
$mainTask = ProjectTask::userTask($task_id)?->toArray();
|
||||
if ($mainTask) {
|
||||
$mainTask['column_name'] = ProjectColumn::whereId($mainTask['column_id'])->value('name');
|
||||
$mainTask['project_name'] = Project::whereId($mainTask['project_id'])->value('name');
|
||||
$data[] = $mainTask;
|
||||
//
|
||||
$subTasks = ProjectTask::whereParentId($task_id)->get();
|
||||
foreach ($subTasks as $subTask) {
|
||||
$data[] = [
|
||||
'id' => $subTask->id,
|
||||
'project_id' => $subTask->project_id,
|
||||
'column_id' => $subTask->column_id,
|
||||
'column_name' => $mainTask['column_name'],
|
||||
'project_name' => $mainTask['project_name'],
|
||||
];
|
||||
}
|
||||
}
|
||||
//
|
||||
return Base::retSuccess('移动成功', $task);
|
||||
return Base::retSuccess('移动成功', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2567,7 +2595,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) {
|
||||
|
||||
@@ -71,6 +71,7 @@ class ReportController extends AbstractController
|
||||
* @apiParam {Object} [keys] 搜索条件
|
||||
* - keys.key: 关键词
|
||||
* - keys.type: 汇报类型,weekly:周报,daily:日报
|
||||
* - keys.status: 状态,unread:未读,read:已读
|
||||
* - keys.created_at: 汇报时间
|
||||
* @apiParam {Number} [page] 当前页,默认:1
|
||||
* @apiParam {Number} [pagesize] 每页显示数量,默认:20,最大:50
|
||||
@@ -98,6 +99,11 @@ class ReportController extends AbstractController
|
||||
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());
|
||||
|
||||
@@ -41,7 +41,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', '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 返回信息(错误描述)
|
||||
@@ -80,6 +80,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 +109,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');
|
||||
@@ -283,6 +287,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 +298,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 +313,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 +334,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 +499,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 +544,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 +614,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 +654,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 +678,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/system/priority 10. 任务优先级
|
||||
* @api {post} api/system/priority 12. 任务优先级
|
||||
*
|
||||
* @apiDescription 获取任务优先级、保存任务优先级
|
||||
* @apiVersion 1.0.0
|
||||
@@ -653,7 +727,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 +774,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 +843,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 +872,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 +887,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 +904,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 +921,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 +938,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/system/imgupload 18. 上传图片
|
||||
* @api {post} api/system/imgupload 20. 上传图片
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -930,7 +1004,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,7 +1101,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/system/fileupload 20. 上传文件
|
||||
* @api {post} api/system/fileupload 22. 上传文件
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1071,7 +1145,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/updatelog 21. 获取更新日志
|
||||
* @api {get} api/system/get/updatelog 23. 获取更新日志
|
||||
*
|
||||
* @apiDescription 获取更新日志
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1114,7 +1188,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 +1234,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 +1403,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 +1429,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/version 25. 获取版本号
|
||||
* @api {get} api/system/version 27. 获取版本号
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -1392,7 +1466,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/prefetch 26. 预加载的资源
|
||||
* @api {get} api/system/prefetch 28. 预加载的资源
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
|
||||
@@ -611,7 +611,38 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/basic 11. 获取指定会员基础信息
|
||||
* @api {get} api/users/search/ai 11. 获取AI机器人
|
||||
*
|
||||
* @apiDescription 搜索会员列表
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName search__ai
|
||||
*
|
||||
* @apiParam {String} type AI 类型(比如:openai)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function search__ai()
|
||||
{
|
||||
User::auth();
|
||||
//
|
||||
$type = trim(Request::input('type'));
|
||||
$botName = "ai-{$type}";
|
||||
if (!UserBot::isAiBot("{$botName}@bot.system")) {
|
||||
return Base::retError('AI机器人不存在');
|
||||
}
|
||||
//
|
||||
$botUser = User::botGetOrCreate($botName);
|
||||
if (empty($botUser)) {
|
||||
return Base::retError('AI机器人不存在');
|
||||
}
|
||||
return Base::retSuccess('success', $botUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/basic 12. 获取指定会员基础信息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -654,7 +685,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/lists 12. 会员列表(限管理员)
|
||||
* @api {get} api/users/lists 13. 会员列表(限管理员)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -803,7 +834,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/operation 13. 操作会员(限管理员)
|
||||
* @api {get} api/users/operation 14. 操作会员(限管理员)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1061,7 +1092,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/email/verification 14. 邮箱验证
|
||||
* @api {get} api/users/email/verification 15. 邮箱验证
|
||||
*
|
||||
* @apiDescription 不需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1109,7 +1140,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/umeng/alias 15. 设置友盟别名
|
||||
* @api {get} api/users/umeng/alias 16. 设置友盟别名
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1120,6 +1151,9 @@ class UsersController extends AbstractController
|
||||
* - update: 更新(默认)
|
||||
* - remove: 删除
|
||||
* @apiParam {String} alias 别名
|
||||
* @apiParam {String} [userAgent] 浏览器信息
|
||||
* @apiParam {String} [deviceModel] 设备型号
|
||||
* @apiParam {String} [isNotified] 是否有通知权限(0不通知、1通知)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -1151,11 +1185,15 @@ class UsersController extends AbstractController
|
||||
'alias' => $data['alias'],
|
||||
'platform' => Base::platform(),
|
||||
];
|
||||
$version = $data['appVersion'] ? ($data['appVersionName'] . " ({$data['appVersion']})") : '';
|
||||
$isNotified = trim($data['isNotified']) === 'true' || $data['isNotified'] === true ? 1 : intval($data['isNotified']);
|
||||
$row = UmengAlias::where($inArray);
|
||||
if ($row->exists()) {
|
||||
$row->update([
|
||||
'ua' => $data['userAgent'],
|
||||
'device' => $data['deviceModel'],
|
||||
'version' => $version,
|
||||
'is_notified' => $isNotified,
|
||||
'updated_at' => Carbon::now()
|
||||
]);
|
||||
return Base::retSuccess('别名已存在');
|
||||
@@ -1163,6 +1201,8 @@ class UsersController extends AbstractController
|
||||
$row = UmengAlias::createInstance(array_merge($inArray, [
|
||||
'ua' => $data['userAgent'],
|
||||
'device' => $data['deviceModel'],
|
||||
'version' => $version,
|
||||
'is_notified' => $isNotified,
|
||||
]));
|
||||
if ($row->save()) {
|
||||
return Base::retSuccess('添加成功');
|
||||
@@ -1172,7 +1212,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/meeting/open 16. 【会议】创建会议、加入会议
|
||||
* @api {get} api/users/meeting/open 17. 【会议】创建会议、加入会议
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1290,7 +1330,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/meeting/link 17. 【会议】获取分享链接
|
||||
* @api {get} api/users/meeting/link 18. 【会议】获取分享链接
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1319,7 +1359,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/meeting/tourist 18. 【会议】游客信息
|
||||
* @api {get} api/users/meeting/tourist 19. 【会议】游客信息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1342,7 +1382,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/meeting/invitation 19. 【会议】发送邀请
|
||||
* @api {get} api/users/meeting/invitation 20. 【会议】发送邀请
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1389,7 +1429,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/email/send 20. 发送邮箱验证码
|
||||
* @api {get} api/users/email/send 21. 发送邮箱验证码
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1429,7 +1469,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/email/edit 21. 修改邮箱
|
||||
* @api {get} api/users/email/edit 22. 修改邮箱
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1474,7 +1514,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/delete/account 22. 删除帐号
|
||||
* @api {get} api/users/delete/account 23. 删除帐号
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1536,7 +1576,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/department/list 23. 部门列表(限管理员)
|
||||
* @api {get} api/users/department/list 24. 部门列表(限管理员)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1555,7 +1595,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/department/add 24. 新建、修改部门(限管理员)
|
||||
* @api {get} api/users/department/add 25. 新建、修改部门(限管理员)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1613,19 +1653,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,
|
||||
@@ -1634,11 +1677,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 25. 删除部门(限管理员)
|
||||
* @api {get} api/users/department/del 26. 删除部门(限管理员)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1661,6 +1704,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());
|
||||
//
|
||||
@@ -1668,7 +1714,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/checkin/get 26. 获取签到设置
|
||||
* @api {get} api/users/checkin/get 27. 获取签到设置
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1695,7 +1741,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/users/checkin/save 27. 保存签到设置
|
||||
* @api {post} api/users/checkin/save 28. 保存签到设置
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1770,7 +1816,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/checkin/list 28. 获取签到数据
|
||||
* @api {get} api/users/checkin/list 29. 获取签到数据
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1817,7 +1863,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/socket/status 29. 获取socket状态
|
||||
* @api {get} api/users/socket/status 30. 获取socket状态
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1840,7 +1886,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/key/client 30. 客户端KEY
|
||||
* @api {get} api/users/key/client 31. 客户端KEY
|
||||
*
|
||||
* @apiDescription 获取客户端KEY,用于加密数据发送给服务端
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1882,7 +1928,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/bot/info 31. 机器人信息
|
||||
* @api {get} api/users/bot/info 32. 机器人信息
|
||||
*
|
||||
* @apiDescription 需要token身份,获取我的机器人信息
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1933,7 +1979,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/users/bot/edit 32. 编辑机器人
|
||||
* @api {post} api/users/bot/edit 33. 编辑机器人
|
||||
*
|
||||
* @apiDescription 需要token身份,编辑 我的机器人 或 管理员修改系统机器人 信息
|
||||
* @apiVersion 1.0.0
|
||||
@@ -2019,7 +2065,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/share/list 33. 获取分享列表
|
||||
* @api {get} api/users/share/list 34. 获取分享列表
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
@@ -2104,7 +2150,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/annual/report 34. 年度报告
|
||||
* @api {get} api/users/annual/report 35. 年度报告
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
|
||||
@@ -23,6 +23,7 @@ use App\Tasks\AutoArchivedTask;
|
||||
use App\Tasks\DeleteBotMsgTask;
|
||||
use App\Tasks\CheckinRemindTask;
|
||||
use App\Tasks\CloseMeetingRoomTask;
|
||||
use App\Tasks\ElasticSearchSyncTask;
|
||||
use App\Tasks\UnclaimedTaskRemindTask;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
use Laravolt\Avatar\Avatar;
|
||||
@@ -258,6 +259,8 @@ class IndexController extends InvokeController
|
||||
Task::deliver(new UnclaimedTaskRemindTask());
|
||||
// 关闭会议室
|
||||
Task::deliver(new CloseMeetingRoomTask());
|
||||
// ElasticSearch 同步
|
||||
Task::deliver(new ElasticSearchSyncTask());
|
||||
|
||||
return "success";
|
||||
}
|
||||
|
||||
@@ -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,6 +220,12 @@ class AbstractModel extends Model
|
||||
$row = static::where($where)->first();
|
||||
if (empty($row)) {
|
||||
$row = new static;
|
||||
if ($update instanceof \Closure) {
|
||||
$update = $update();
|
||||
}
|
||||
if ($insert instanceof \Closure) {
|
||||
$insert = $insert();
|
||||
}
|
||||
$array = array_merge($where, $insert ?: $update);
|
||||
if (isset($array[$row->primaryKey])) {
|
||||
unset($array[$row->primaryKey]);
|
||||
@@ -227,6 +233,9 @@ class AbstractModel extends Model
|
||||
$row->updateInstance($array);
|
||||
$isInsert = true;
|
||||
} elseif ($update) {
|
||||
if ($update instanceof \Closure) {
|
||||
$update = $update();
|
||||
}
|
||||
$row->updateInstance($update);
|
||||
$isInsert = false;
|
||||
}
|
||||
|
||||
99
app/Models/ApproveProcInstHistory.php
Normal file
99
app/Models/ApproveProcInstHistory.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
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';
|
||||
|
||||
/**
|
||||
* 获取用户审批状态(请假、外出)
|
||||
* @param $userid
|
||||
* @return mixed|null
|
||||
*/
|
||||
public static function getUserApprovalStatus($userid)
|
||||
{
|
||||
if (empty($userid)) {
|
||||
return null;
|
||||
}
|
||||
return Cache::remember('user_is_leave_' . $userid, Carbon::now()->addMinute(), function () use ($userid) {
|
||||
return self::where([
|
||||
['start_user_id', '=', $userid],
|
||||
[DB::raw("JSON_UNQUOTE(JSON_EXTRACT(var, '$.startTime'))"), '<=', Carbon::now()->toDateTimeString()],
|
||||
[DB::raw("JSON_UNQUOTE(JSON_EXTRACT(var, '$.endTime'))"), '>=', Carbon::now()->toDateTimeString()],
|
||||
['state', '=', 2]
|
||||
])->where(function ($query) {
|
||||
$query->where('proc_def_name', 'like', '%请假%')
|
||||
->orWhere('proc_def_name', 'like', '%外出%');
|
||||
})->orderByDesc('id')->value('proc_def_name');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断用户是否请假(包含:请假、外出)
|
||||
* @param $userid
|
||||
* @return bool
|
||||
*/
|
||||
public static function userIsLeave($userid)
|
||||
{
|
||||
return (bool)self::getUserApprovalStatus($userid);
|
||||
}
|
||||
}
|
||||
@@ -79,9 +79,28 @@ class File extends AbstractModel
|
||||
* office文件
|
||||
*/
|
||||
const officeExt = [
|
||||
'doc', 'docx',
|
||||
'xls', 'xlsx',
|
||||
'ppt', 'pptx',
|
||||
// 文本文件
|
||||
'doc', 'docx', // Microsoft Word 文档
|
||||
'dot', 'dotx', // Word 模板
|
||||
'odt', // OpenDocument 文本格式
|
||||
'ott', // OpenDocument 文本模板
|
||||
'rtf', // 富文本格式
|
||||
|
||||
// 电子表格
|
||||
'xls', 'xlsx', // Microsoft Excel 电子表格
|
||||
'xlsm', // Excel 含宏的工作簿
|
||||
'xlt', 'xltx', // Excel 模板
|
||||
'ods', // OpenDocument 电子表格格式
|
||||
'ots', // OpenDocument 电子表格模板
|
||||
'csv', // 逗号分隔值
|
||||
'tsv', // 制表符分隔值
|
||||
|
||||
// 演示文稿
|
||||
'ppt', 'pptx', // Microsoft PowerPoint 演示文稿
|
||||
'pps', 'ppsx', // PowerPoint 幻灯片放映
|
||||
'pot', 'potx', // PowerPoint 模板
|
||||
'odp', // OpenDocument 演示文稿格式
|
||||
'otp', // OpenDocument 演示文稿模板
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -264,9 +283,9 @@ class File extends AbstractModel
|
||||
'text', 'md', 'markdown' => 'document',
|
||||
'drawio' => 'drawio',
|
||||
'mind' => 'mind',
|
||||
'doc', 'docx' => "word",
|
||||
'xls', 'xlsx' => "excel",
|
||||
'ppt', 'pptx' => "ppt",
|
||||
'doc', 'docx', 'dot', 'dotx', 'odt', 'ott', 'rtf' => "word",
|
||||
'xls', 'xlsx', 'xlsm', 'xlt', 'xltx', 'ods', 'ots', 'csv', 'tsv' => "excel",
|
||||
'ppt', 'pptx', 'pps', 'ppsx', 'pot', 'potx', 'odp', 'otp' => "ppt",
|
||||
'wps' => "wps",
|
||||
'jpg', 'jpeg', 'webp', 'png', 'gif', 'bmp', 'ico', 'raw', 'svg' => "picture",
|
||||
'rar', 'zip', 'jar', '7-zip', 'tar', 'gzip', '7z', 'gz', 'apk', 'dmg' => "archive",
|
||||
@@ -706,9 +725,9 @@ class File extends AbstractModel
|
||||
* @param int $permission
|
||||
* @return File
|
||||
*/
|
||||
public static function permissionFind(int $id, $user, int $limit = 0, int &$permission = -1)
|
||||
public static function permissionFind($id, $user, int $limit = 0, int &$permission = -1)
|
||||
{
|
||||
$file = File::find($id);
|
||||
$file = File::find(intval($id));
|
||||
if (empty($file)) {
|
||||
throw new ApiException('文件不存在或已被删除');
|
||||
}
|
||||
|
||||
@@ -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,11 @@ class Project extends AbstractModel
|
||||
'userid' => $userid,
|
||||
], [
|
||||
'important' => 1
|
||||
]);
|
||||
], function () use ($userid) {
|
||||
return [
|
||||
'bot' => User::isBot($userid) ? 1 : 0,
|
||||
];
|
||||
});
|
||||
}
|
||||
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->whereImportant(1)->remove();
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
@@ -770,6 +771,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 +837,19 @@ 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) {
|
||||
$this->addLog("{任务}超期未完成", [
|
||||
'cache' => [
|
||||
'task_at' => $oldStringAt,
|
||||
'change_at' => $newStringAt,
|
||||
'over_sec' => ($existAt ? $this->end_at : Carbon::now())->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]
|
||||
@@ -1180,7 +1194,11 @@ class ProjectTask extends AbstractModel
|
||||
'userid' => $userid,
|
||||
], [
|
||||
'important' => 1
|
||||
]);
|
||||
], function () use ($userid) {
|
||||
return [
|
||||
'bot' => User::isBot($userid) ? 1 : 0,
|
||||
];
|
||||
});
|
||||
}
|
||||
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->whereImportant(1)->remove();
|
||||
});
|
||||
@@ -1343,6 +1361,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 = '已完成';
|
||||
}
|
||||
@@ -1833,6 +1861,11 @@ class ProjectTask extends AbstractModel
|
||||
$taskUser->save();
|
||||
}
|
||||
}
|
||||
// 子任务
|
||||
ProjectTask::whereParentId($this->id)->change([
|
||||
'project_id' => $projectId,
|
||||
'column_id' => $columnId,
|
||||
]);
|
||||
//
|
||||
if ($flowItemId) {
|
||||
$flowItem = projectFlowItem::whereProjectId($projectId)->whereId($flowItemId)->first();
|
||||
@@ -1860,6 +1893,67 @@ class ProjectTask extends AbstractModel
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -139,12 +139,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 => "",
|
||||
};
|
||||
|
||||
@@ -65,20 +65,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 +112,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
|
||||
|
||||
@@ -15,7 +15,9 @@ use Hedeqiang\UMeng\IOS;
|
||||
* @property string|null $alias 别名
|
||||
* @property string|null $platform 平台类型
|
||||
* @property string|null $device 设备类型
|
||||
* @property string|null $version 应用版本号
|
||||
* @property string|null $ua userAgent
|
||||
* @property int|null $is_notified 通知权限
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
@@ -31,10 +33,12 @@ use Hedeqiang\UMeng\IOS;
|
||||
* @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 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
|
||||
@@ -189,7 +193,11 @@ class UmengAlias extends AbstractModel
|
||||
$lists = $rows->take(5)->groupBy('platform'); // 每个会员最多推送5个别名
|
||||
foreach ($lists as $platform => $list) {
|
||||
$alias = $list->pluck('alias')->implode(',');
|
||||
self::pushMsgToAlias($alias, $platform, $array);
|
||||
try {
|
||||
self::pushMsgToAlias($alias, $platform, $array);
|
||||
} catch (\Exception $e) {
|
||||
info("[PushMsg] fail: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -242,6 +242,19 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否管理员
|
||||
*/
|
||||
@@ -593,8 +606,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':
|
||||
@@ -683,4 +702,21 @@ class User extends AbstractModel
|
||||
}
|
||||
return $botUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否机器人
|
||||
* @param $userid
|
||||
* @return bool|mixed
|
||||
*/
|
||||
public static function isBot($userid)
|
||||
{
|
||||
if (empty($userid)) {
|
||||
return false;
|
||||
}
|
||||
$userid = intval($userid);
|
||||
if (RequestContext::has("isBot_" . $userid)) {
|
||||
return RequestContext::get("isBot_" . $userid);
|
||||
}
|
||||
return (bool)User::find($userid)?->bot;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,16 @@ 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 邮箱 或 邮箱前缀
|
||||
@@ -73,10 +83,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提醒',
|
||||
@@ -177,11 +190,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('历史会话'),
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -265,7 +267,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();
|
||||
// 最后消息
|
||||
@@ -330,6 +334,9 @@ class WebSocketDialog extends AbstractModel
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (empty($data['pinyin'])) {
|
||||
$data['pinyin'] = Base::cn2pinyin($data['name']);
|
||||
}
|
||||
|
||||
// 已存在的消息类型
|
||||
if ($hasData === true) {
|
||||
@@ -398,6 +405,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 +447,11 @@ class WebSocketDialog extends AbstractModel
|
||||
WebSocketDialogUser::updateInsert([
|
||||
'dialog_id' => $this->id,
|
||||
'userid' => $value,
|
||||
], $updateData, [], $isInsert);
|
||||
], $updateData, function() use ($value) {
|
||||
return [
|
||||
'bot' => User::isBot($value) ? 1 : 0,
|
||||
];
|
||||
}, $isInsert);
|
||||
if ($isInsert) {
|
||||
WebSocketDialogMsg::sendMsg(null, $this->id, 'notice', [
|
||||
'notice' => User::userid2nickname($value) . " 已加入群组"
|
||||
@@ -429,10 +460,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 +519,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -741,6 +770,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;
|
||||
});
|
||||
}
|
||||
@@ -755,6 +795,9 @@ class WebSocketDialog extends AbstractModel
|
||||
*/
|
||||
public static function getUserDialog($userid1, $userid2, $ttl, &$cacheKey = null)
|
||||
{
|
||||
if ($userid1 == $userid2) {
|
||||
$userid2 = 0;
|
||||
}
|
||||
$userids = [$userid1, $userid2];
|
||||
sort($userids);
|
||||
$cacheKey = "Dialog::user:" . implode('-', $userids);
|
||||
|
||||
@@ -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 消息类型(用于搜索)
|
||||
@@ -30,6 +31,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @property int|null $todo 设为待办会员ID
|
||||
* @property int|null $link 是否存在链接
|
||||
* @property int|null $modify 是否编辑
|
||||
* @property int|null $bot 是否机器人的消息
|
||||
* @property int|null $reply_num 有多少条回复
|
||||
* @property int|null $reply_id 回复ID
|
||||
* @property int|null $forward_id 转发ID
|
||||
@@ -50,6 +52,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereBot($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDeletedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDialogId($value)
|
||||
@@ -67,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)
|
||||
@@ -301,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);
|
||||
}
|
||||
@@ -590,6 +599,9 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
case 'text':
|
||||
return self::previewTextMsg($data['msg'], $preserveHtml);
|
||||
|
||||
case 'longtext':
|
||||
return $data['msg']['desc'] ? Base::cutStr($data['msg']['desc'], 50) : ("[" . Doo::translate("长文本") . "]");
|
||||
|
||||
case 'vote':
|
||||
$action = Doo::translate("投票");
|
||||
return "[{$action}] " . self::previewTextMsg($data['msg'], $preserveHtml);
|
||||
@@ -652,6 +664,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);
|
||||
}
|
||||
@@ -751,9 +767,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':
|
||||
@@ -772,14 +794,24 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
break;
|
||||
}
|
||||
}
|
||||
$key = str_replace([""", "&", "<", ">"], "", $key);
|
||||
$key = str_replace(["\r", "\n", "\t", " "], " ", $key);
|
||||
$key = preg_replace("/^\/[A-Za-z]+/", " ", $key);
|
||||
$key = preg_replace("/\s+/", " ", $key);
|
||||
$this->key = trim($key);
|
||||
$this->key = self::filterEscape($key);
|
||||
$this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤转义
|
||||
* @param $content
|
||||
* @return string
|
||||
*/
|
||||
public static function filterEscape($content)
|
||||
{
|
||||
$content = str_replace([""", "&", "<", ">"], "", $content);
|
||||
$content = str_replace(["\r", "\n", "\t", " "], " ", $content);
|
||||
$content = preg_replace("/^\/[A-Za-z]+/", " ", $content);
|
||||
$content = preg_replace("/\s+/", " ", $content);
|
||||
return trim($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回引用消息(如果是文本只取预览)
|
||||
* @return array|mixed
|
||||
@@ -867,8 +899,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"];
|
||||
}
|
||||
@@ -1121,6 +1161,7 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
}
|
||||
//
|
||||
$updateData = [
|
||||
'type' => $type,
|
||||
'mtype' => $mtype,
|
||||
'link' => $link,
|
||||
'msg' => array_merge($oldMsg, $msg),
|
||||
@@ -1163,12 +1204,14 @@ 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,
|
||||
'type' => $type,
|
||||
'mtype' => $mtype,
|
||||
'link' => $link,
|
||||
'bot' => User::isBot($sender) ? 1 : 0,
|
||||
'msg' => $msg,
|
||||
'read' => 0,
|
||||
]);
|
||||
@@ -1176,6 +1219,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'],
|
||||
|
||||
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)
|
||||
|
||||
@@ -14,6 +14,7 @@ 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;
|
||||
@@ -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)
|
||||
@@ -2017,7 +2018,7 @@ class Base
|
||||
$type = ['mp3', 'wma', 'wav', 'amr'];
|
||||
break;
|
||||
case 'excel':
|
||||
$type = ['xls', 'xlsx'];
|
||||
$type = ['xls', 'xlsx', 'xlsm', 'xlt', 'xltx', 'ods', 'ots', 'csv', 'tsv'];
|
||||
break;
|
||||
case 'app':
|
||||
$type = ['apk'];
|
||||
@@ -2758,12 +2759,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 +2781,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 +2798,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');
|
||||
}
|
||||
|
||||
308
app/Module/ElasticSearch/ElasticSearchBase.php
Normal file
308
app/Module/ElasticSearch/ElasticSearchBase.php
Normal file
@@ -0,0 +1,308 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module\ElasticSearch;
|
||||
|
||||
use Elastic\Elasticsearch\ClientBuilder;
|
||||
use Elastic\Elasticsearch\Exception\MissingParameterException;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Elasticsearch基础类
|
||||
*
|
||||
* Class ElasticSearchBase
|
||||
* @package App\Module\ElasticSearch
|
||||
*/
|
||||
class ElasticSearchBase
|
||||
{
|
||||
/**
|
||||
* Elasticsearch客户端实例
|
||||
*
|
||||
* @var \Elastic\Elasticsearch\Client
|
||||
*/
|
||||
protected $client;
|
||||
|
||||
/**
|
||||
* 当前操作的索引名称
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $index;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param null $index 默认索引名称
|
||||
* @throws \Elastic\Elasticsearch\Exception\ConfigException
|
||||
*/
|
||||
public function __construct($index = null)
|
||||
{
|
||||
$host = env('ELASTICSEARCH_HOST', env('APP_IPPR') . '.15');
|
||||
$port = env('ELASTICSEARCH_PORT', '9200');
|
||||
$scheme = env('ELASTICSEARCH_SCHEME', 'http');
|
||||
$user = env('ELASTICSEARCH_USER', '');
|
||||
$pass = env('ELASTICSEARCH_PASS', '');
|
||||
$verifi = env('ELASTICSEARCH_VERIFI', false);
|
||||
$ca = env('ELASTICSEARCH_CA', '');
|
||||
$key = env('ELASTICSEARCH_KEY', '');
|
||||
$cert = env('ELASTICSEARCH_CERT', '');
|
||||
// 为8.x版本客户端配置连接
|
||||
$config = [
|
||||
'hosts' => ["{$scheme}://{$host}:{$port}"]
|
||||
];
|
||||
|
||||
// 如果设置了用户名和密码
|
||||
if (!empty($user)) {
|
||||
$config['basicAuthentication'] = [$user, $pass];
|
||||
}
|
||||
|
||||
$config['SSLVerification'] = $verifi;
|
||||
if ($verifi) {
|
||||
$config['SSLCert'] = $cert;
|
||||
$config['CABundle'] = $ca;
|
||||
$config['SSLKey'] = $key;
|
||||
}
|
||||
// 8.x版本使用ClientBuilder::fromConfig创建客户端
|
||||
$this->client = ClientBuilder::fromConfig($config);
|
||||
|
||||
if ($index) {
|
||||
$this->index = $index;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置索引名称
|
||||
*
|
||||
* @param string $index
|
||||
* @return $this
|
||||
*/
|
||||
public function setIndex($index)
|
||||
{
|
||||
$this->index = $index;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查索引是否存在
|
||||
*
|
||||
* @return bool
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function indexExists()
|
||||
{
|
||||
$params = ['index' => $this->index];
|
||||
return $this->client->indices()->exists($params)->asBool();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建索引
|
||||
*
|
||||
* @param array $settings 索引设置
|
||||
* @param array $mappings 字段映射
|
||||
* @return array
|
||||
*/
|
||||
public function createIndex($settings = [], $mappings = [])
|
||||
{
|
||||
$params = [
|
||||
'index' => $this->index
|
||||
];
|
||||
|
||||
$body = [];
|
||||
if (!empty($settings)) {
|
||||
$body['settings'] = $settings;
|
||||
}
|
||||
|
||||
if (!empty($mappings)) {
|
||||
$body['mappings'] = $mappings;
|
||||
}
|
||||
|
||||
if (!empty($body)) {
|
||||
$params['body'] = $body;
|
||||
}
|
||||
|
||||
try {
|
||||
// 在8.x中,索引操作位于indices()命名空间
|
||||
return $this->client->indices()->create($params)->asArray();
|
||||
} catch (\Exception $e) {
|
||||
Log::error('创建Elasticsearch索引失败: ' . $e->getMessage());
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除索引
|
||||
* @return array
|
||||
*/
|
||||
public function deleteIndex()
|
||||
{
|
||||
try {
|
||||
$params = ['index' => $this->index];
|
||||
return $this->client->indices()->delete($params)->asArray();
|
||||
} catch (\Exception $e) {
|
||||
Log::error('删除Elasticsearch索引失败: ' . $e->getMessage());
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作(批量添加/更新/删除文档)
|
||||
*
|
||||
* @param array $operations 批量操作的数据
|
||||
* @return array
|
||||
*/
|
||||
public function bulk($operations)
|
||||
{
|
||||
try {
|
||||
// 在8.x中,批量操作API签名相同,但内部实现有所变化
|
||||
return $this->client->bulk($operations)->asArray();
|
||||
} catch (\Exception $e) {
|
||||
Log::error('批量操作失败: ' . $e->getMessage());
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 索引单个文档
|
||||
*
|
||||
* @param array $document 文档数据
|
||||
* @param string $id 文档ID
|
||||
* @param string|null $routing 路由值,用于父子文档
|
||||
* @return array
|
||||
*/
|
||||
public function indexDocument($document, $id, $routing = null)
|
||||
{
|
||||
$params = [
|
||||
'index' => $this->index,
|
||||
'id' => $id,
|
||||
'body' => $document
|
||||
];
|
||||
|
||||
if ($routing) {
|
||||
$params['routing'] = $routing;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->client->index($params)->asArray();
|
||||
} catch (\Exception $e) {
|
||||
Log::error('索引文档失败: ' . $e->getMessage());
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文档
|
||||
*
|
||||
* @param string $id 文档ID
|
||||
* @param string|null $routing 路由值,用于父子文档
|
||||
* @return array
|
||||
*/
|
||||
public function deleteDocument($id, $routing = null)
|
||||
{
|
||||
$params = [
|
||||
'index' => $this->index,
|
||||
'id' => $id
|
||||
];
|
||||
|
||||
if ($routing) {
|
||||
$params['routing'] = $routing;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->client->delete($params)->asArray();
|
||||
} catch (MissingParameterException $e) {
|
||||
// 文档不存在时返回成功
|
||||
return ['result' => 'not_found', 'error' => $e->getMessage()];
|
||||
} catch (\Exception $e) {
|
||||
Log::error('删除文档失败: ' . $e->getMessage());
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新索引
|
||||
* @return array
|
||||
*/
|
||||
public function refreshIndex()
|
||||
{
|
||||
$params = [
|
||||
'index' => $this->index
|
||||
];
|
||||
|
||||
try {
|
||||
return $this->client->indices()->refresh($params)->asArray();
|
||||
} catch (\Exception $e) {
|
||||
Log::error('刷新索引失败: ' . $e->getMessage());
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查索引映射
|
||||
* @return array
|
||||
*/
|
||||
public function checkIndexMapping()
|
||||
{
|
||||
try {
|
||||
return $this->client->indices()->getMapping(['index' => $this->index])->asArray();
|
||||
} catch (\Exception $e) {
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用搜索方法
|
||||
*
|
||||
* @param array $query 搜索查询
|
||||
* @param int $from 起始位置
|
||||
* @param int $size 返回结果数量
|
||||
* @param array $sort 排序规则
|
||||
* @return array
|
||||
*/
|
||||
public function search($query, $from = 0, $size = 10, $sort = [])
|
||||
{
|
||||
$params = [
|
||||
'index' => $this->index,
|
||||
'body' => [
|
||||
'query' => $query,
|
||||
'from' => $from,
|
||||
'size' => $size
|
||||
]
|
||||
];
|
||||
|
||||
if (!empty($sort)) {
|
||||
$params['body']['sort'] = $sort;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->client->search($params)->asArray();
|
||||
} catch (\Exception $e) {
|
||||
Log::error('搜索失败: ' . $e->getMessage());
|
||||
return ['error' => $e->getMessage(), 'hits' => ['total' => ['value' => 0], 'hits' => []]];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 索引名称
|
||||
*/
|
||||
const indexName = 'default';
|
||||
|
||||
/**
|
||||
* 获取索引名称
|
||||
* @param string $index 索引名称
|
||||
* @param string|null $prefix 索引前缀
|
||||
* @param string|null $subfix 索引后缀
|
||||
* @return string
|
||||
*/
|
||||
public static function indexName($index = '', $prefix = '', $subfix = '')
|
||||
{
|
||||
$index = $index ?: static::indexName;
|
||||
$prefix = $prefix ?: env('ES_INDEX_PREFIX', '');
|
||||
$subfix = $subfix ?: env('ES_INDEX_SUFFIX', '');
|
||||
if ($prefix) {
|
||||
$index = rtrim($prefix, '_') . '_' . $index;
|
||||
}
|
||||
if ($subfix) {
|
||||
$index = $index . '_' . ltrim($subfix, '_');
|
||||
}
|
||||
return $index;
|
||||
}
|
||||
}
|
||||
204
app/Module/ElasticSearch/ElasticSearchKeyValue.php
Normal file
204
app/Module/ElasticSearch/ElasticSearchKeyValue.php
Normal file
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module\ElasticSearch;
|
||||
|
||||
use App\Module\Base;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Elasticsearch键值存储
|
||||
*
|
||||
* Class ElasticSearchKeyValue
|
||||
* @package App\Module\ElasticSearch
|
||||
*/
|
||||
class ElasticSearchKeyValue extends ElasticSearchBase
|
||||
{
|
||||
const indexName = 'key_value_store';
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @return ElasticSearchBase
|
||||
* @throws \Elastic\Elasticsearch\Exception\ConfigException
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
return parent::__construct(self::indexName());
|
||||
}
|
||||
|
||||
/** ******************************************************************************************************** */
|
||||
/** *********************************** 键值存储方法 ******************************************************** */
|
||||
/** ******************************************************************************************************** */
|
||||
|
||||
/**
|
||||
* 创建键值存储索引
|
||||
* @return array
|
||||
*/
|
||||
public static function generateIndex()
|
||||
{
|
||||
try {
|
||||
$es = new self();
|
||||
|
||||
// 如果索引已存在,则直接返回
|
||||
if ($es->indexExists()) {
|
||||
return ['acknowledged' => true, 'message' => '索引已存在'];
|
||||
}
|
||||
|
||||
// 定义映射
|
||||
$mappings = [
|
||||
'properties' => [
|
||||
'key' => ['type' => 'keyword'],
|
||||
'value' => ['type' => 'text', 'fields' => ['keyword' => ['type' => 'keyword']]],
|
||||
'created_at' => ['type' => 'integer'],
|
||||
'updated_at' => ['type' => 'integer']
|
||||
]
|
||||
];
|
||||
|
||||
// 索引设置
|
||||
$settings = [
|
||||
'number_of_shards' => 1,
|
||||
'number_of_replicas' => 1,
|
||||
'refresh_interval' => '1s'
|
||||
];
|
||||
|
||||
return $es->createIndex($settings, $mappings);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('创建键值存储索引失败: ' . $e->getMessage());
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存键值对
|
||||
* @param string $key 键名
|
||||
* @param mixed $value 键值
|
||||
* @param string $namespace 命名空间,用于区分不同的键值存储场景
|
||||
* @return array
|
||||
*/
|
||||
public static function save($key, $value, $namespace = 'default')
|
||||
{
|
||||
try {
|
||||
// 确保索引存在
|
||||
self::generateIndex();
|
||||
|
||||
$es = new self();
|
||||
|
||||
// 生成文档ID
|
||||
$docId = "{$namespace}:{$key}";
|
||||
|
||||
// 准备文档数据
|
||||
$document = [
|
||||
'key' => $key,
|
||||
'value' => is_array($value) ? json_encode($value, JSON_UNESCAPED_UNICODE) : $value,
|
||||
'namespace' => $namespace,
|
||||
'created_at' => time(),
|
||||
'updated_at' => time()
|
||||
];
|
||||
|
||||
// 索引文档
|
||||
$result = $es->indexDocument($document, $docId);
|
||||
|
||||
// 刷新索引以确保立即可见
|
||||
$es->refreshIndex();
|
||||
|
||||
return $result;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('保存键值对失败: ' . $e->getMessage());
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取键值
|
||||
* @param string $key 键名
|
||||
* @param mixed $default 默认值,当键不存在时返回
|
||||
* @param string $namespace 命名空间,用于区分不同的键值存储场景
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get($key, $default = null, $namespace = 'default')
|
||||
{
|
||||
try {
|
||||
$es = new self();
|
||||
|
||||
// 如果索引不存在,直接返回默认值
|
||||
if (!$es->indexExists()) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
// 生成文档ID
|
||||
$docId = "{$namespace}:{$key}";
|
||||
|
||||
// 查询参数
|
||||
$params = [
|
||||
'index' => self::indexName(),
|
||||
'id' => $docId
|
||||
];
|
||||
|
||||
try {
|
||||
// 获取文档
|
||||
$response = $es->client->get($params)->asArray();
|
||||
|
||||
// 获取值
|
||||
$value = $response['_source']['value'] ?? $default;
|
||||
|
||||
// 如果值是JSON字符串,尝试解码
|
||||
if (is_string($value) && $decoded = json_decode($value, true)) {
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
} catch (\Exception $e) {
|
||||
// 文档不存在或其他错误,返回默认值
|
||||
return $default;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('获取键值对失败: ' . $e->getMessage());
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取键值,返回数组
|
||||
* @param string $key 键名
|
||||
* @param array $default 默认值,当键不存在时返回
|
||||
* @param string $namespace 命名空间,用于区分不同的键值存储场景
|
||||
* @return array
|
||||
*/
|
||||
public static function getArray($key, $default = [], $namespace = 'default')
|
||||
{
|
||||
return Base::string2array(self::get($key, $default, $namespace));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除键值对
|
||||
* @param string $key 键名
|
||||
* @param string $namespace 命名空间
|
||||
* @return array
|
||||
*/
|
||||
public static function delete($key, $namespace = 'default')
|
||||
{
|
||||
try {
|
||||
$es = new self();
|
||||
|
||||
// 如果索引不存在,直接返回成功
|
||||
if (!$es->indexExists()) {
|
||||
return ['result' => 'not_found'];
|
||||
}
|
||||
|
||||
// 生成文档ID
|
||||
$docId = "{$namespace}:{$key}";
|
||||
|
||||
// 删除文档
|
||||
$result = $es->deleteDocument($docId);
|
||||
|
||||
// 刷新索引以确保立即生效
|
||||
$es->refreshIndex();
|
||||
|
||||
return $result;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('删除键值对失败: ' . $e->getMessage());
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
}
|
||||
375
app/Module/ElasticSearch/ElasticSearchUserMsg.php
Normal file
375
app/Module/ElasticSearch/ElasticSearchUserMsg.php
Normal file
@@ -0,0 +1,375 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module\ElasticSearch;
|
||||
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Models\WebSocketDialogUser;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* 对话系统消息索引
|
||||
*
|
||||
* Class ElasticSearchUserMsg
|
||||
* @package App\Module\ElasticSearch
|
||||
*/
|
||||
class ElasticSearchUserMsg extends ElasticSearchBase
|
||||
{
|
||||
const indexName = 'dialog_user_msg';
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @return ElasticSearchBase
|
||||
* @throws \Elastic\Elasticsearch\Exception\ConfigException
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
return parent::__construct(self::indexName());
|
||||
}
|
||||
|
||||
/** ******************************************************************************************************** */
|
||||
/** *********************************************** 基础 ************************************************** */
|
||||
/** ******************************************************************************************************** */
|
||||
|
||||
/**
|
||||
* 创建聊天系统索引 - 使用父子关系
|
||||
* @return array
|
||||
*/
|
||||
public static function generateIndex()
|
||||
{
|
||||
// 定义映射
|
||||
$mappings = [
|
||||
'properties' => [
|
||||
// 共用字段
|
||||
'dialog_id' => ['type' => 'keyword'],
|
||||
'created_at' => ['type' => 'date'],
|
||||
'updated_at' => ['type' => 'date'],
|
||||
|
||||
// dialog_users 字段
|
||||
'userid' => ['type' => 'keyword'],
|
||||
'top_at' => ['type' => 'date'],
|
||||
'last_at' => ['type' => 'date'],
|
||||
'mark_unread' => ['type' => 'integer'],
|
||||
'silence' => ['type' => 'integer'],
|
||||
'hide' => ['type' => 'integer'],
|
||||
'color' => ['type' => 'keyword'],
|
||||
|
||||
// dialog_msgs 字段
|
||||
'msg_id' => ['type' => 'keyword'],
|
||||
'sender_userid' => ['type' => 'keyword'],
|
||||
'msg_type' => ['type' => 'keyword'],
|
||||
'key' => ['type' => 'text'],
|
||||
'bot' => ['type' => 'integer'],
|
||||
|
||||
// Join字段定义父子关系
|
||||
'relationship' => [
|
||||
'type' => 'join',
|
||||
'relations' => [
|
||||
'dialog_user' => 'dialog_msg' // dialog_user是父文档,dialog_msg是子文档
|
||||
]
|
||||
],
|
||||
]
|
||||
];
|
||||
|
||||
// 索引设置
|
||||
$settings = [
|
||||
'number_of_shards' => 5,
|
||||
'number_of_replicas' => 1,
|
||||
'refresh_interval' => '5s'
|
||||
];
|
||||
|
||||
try {
|
||||
$es = new self();
|
||||
return $es->createIndex($settings, $mappings);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('创建聊天系统索引失败: ' . $e->getMessage());
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建对话系统特定的搜索 - 根据用户ID和消息关键词搜索会话
|
||||
* @param string $userid 用户ID
|
||||
* @param string $keyword 消息关键词
|
||||
* @param int $size 返回结果数量
|
||||
* @return array
|
||||
*/
|
||||
public static function searchByKeyword($userid, $keyword, $size = 20)
|
||||
{
|
||||
// 注意这里的类型名称要与创建索引时的一致
|
||||
$query = [
|
||||
'bool' => [
|
||||
'must' => [
|
||||
[
|
||||
'term' => [
|
||||
'userid' => $userid
|
||||
]
|
||||
],
|
||||
[
|
||||
'has_child' => [
|
||||
'type' => 'dialog_msg',
|
||||
'query' => [
|
||||
'bool' => [
|
||||
'must' => [
|
||||
[
|
||||
'match_phrase' => [
|
||||
'key' => $keyword
|
||||
]
|
||||
],
|
||||
[
|
||||
'term' => [
|
||||
'bot' => 0
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
'inner_hits' => [
|
||||
'size' => 1,
|
||||
'sort' => [
|
||||
'msg_id' => 'desc'
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
// 结果集合
|
||||
$searchMap = [];
|
||||
|
||||
try {
|
||||
// 开始搜索
|
||||
$es = new self();
|
||||
$results = $es->search($query, 0, $size, ['last_at' => 'desc']);
|
||||
|
||||
// 处理搜索结果
|
||||
$hits = $results['hits']['hits'] ?? [];
|
||||
|
||||
foreach ($hits as $hit) {
|
||||
if (isset($hit['inner_hits']['dialog_msg']['hits']['hits'][0])) {
|
||||
$msgHit = $hit['inner_hits']['dialog_msg']['hits']['hits'][0];
|
||||
$source = $hit['_source'];
|
||||
$msgSource = $msgHit['_source'];
|
||||
|
||||
$searchMap[] = [
|
||||
'id' => $source['dialog_id'],
|
||||
'top_at' => $source['top_at'],
|
||||
'last_at' => $source['last_at'],
|
||||
'mark_unread' => $source['mark_unread'],
|
||||
'silence' => $source['silence'],
|
||||
'hide' => $source['hide'],
|
||||
'color' => $source['color'],
|
||||
'user_at' => $source['updated_at'],
|
||||
'search_msg_id' => $msgSource['msg_id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('searchByKeyword: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// 返回搜索结果
|
||||
return $searchMap;
|
||||
}
|
||||
|
||||
/** ******************************************************************************************************** */
|
||||
/** *********************************************** 用户 ************************************************** */
|
||||
/** ******************************************************************************************************** */
|
||||
|
||||
/**
|
||||
* 会话用户 - 生成文档ID
|
||||
* @param WebSocketDialogUser $dialogUser
|
||||
* @return string
|
||||
*/
|
||||
public static function generateUserDicId(WebSocketDialogUser $dialogUser)
|
||||
{
|
||||
return "user_{$dialogUser->userid}_dialog_{$dialogUser->dialog_id}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话用户 - 生成文档格式
|
||||
* @param WebSocketDialogUser $dialogUser
|
||||
* @return array
|
||||
*/
|
||||
public static function generateUserFormat(WebSocketDialogUser $dialogUser)
|
||||
{
|
||||
return [
|
||||
'dialog_id' => $dialogUser->dialog_id,
|
||||
'created_at' => $dialogUser->created_at,
|
||||
'updated_at' => $dialogUser->updated_at,
|
||||
|
||||
'userid' => $dialogUser->userid,
|
||||
'top_at' => $dialogUser->top_at,
|
||||
'last_at' => $dialogUser->last_at,
|
||||
'mark_unread' => $dialogUser->mark_unread ? 1 : 0,
|
||||
'silence' => $dialogUser->silence ? 1 : 0,
|
||||
'hide' => $dialogUser->hide ? 1 : 0,
|
||||
'color' => $dialogUser->color,
|
||||
|
||||
'relationship' => [
|
||||
'name' => 'dialog_user'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话用户 - 同步到Elasticsearch
|
||||
* @param WebSocketDialogUser $dialogUser
|
||||
* @return void
|
||||
*/
|
||||
public static function syncUser(WebSocketDialogUser $dialogUser)
|
||||
{
|
||||
try {
|
||||
$es = new self();
|
||||
$es->indexDocument(self::generateUserFormat($dialogUser), self::generateUserDicId($dialogUser));
|
||||
} catch (\Exception $e) {
|
||||
Log::error('syncUser: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话用户 - 从Elasticsearch删除
|
||||
*/
|
||||
public static function deleteUser(WebSocketDialogUser $dialogUser)
|
||||
{
|
||||
try {
|
||||
$es = new self();
|
||||
|
||||
$docId = "user_{$dialogUser->userid}_dialog_{$dialogUser->dialog_id}";
|
||||
|
||||
// 删除用户-会话文档
|
||||
$es->deleteDocument($docId);
|
||||
|
||||
// 注意:这里可能还需要删除所有关联的消息文档
|
||||
// 但由于父子关系,可以通过查询找到所有子文档并删除
|
||||
// 这里为简化,可以选择在后台任务中处理,或者直接依赖ES的级联删除功能
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('deleteUser: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** ******************************************************************************************************** */
|
||||
/** *********************************************** 消息 ************************************************** */
|
||||
/** ******************************************************************************************************** */
|
||||
|
||||
/**
|
||||
* 会话消息 - 生成父文档ID
|
||||
* @param WebSocketDialogMsg $dialogMsg
|
||||
* @param $userid
|
||||
* @return string
|
||||
*/
|
||||
public static function generateMsgParentId(WebSocketDialogMsg $dialogMsg, $userid)
|
||||
{
|
||||
return "user_{$userid}_dialog_{$dialogMsg->dialog_id}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话消息 - 生成文档ID
|
||||
* @param WebSocketDialogMsg $dialogMsg
|
||||
* @param $userid
|
||||
* @return string
|
||||
*/
|
||||
public static function generateMsgDicId(WebSocketDialogMsg $dialogMsg, $userid)
|
||||
{
|
||||
return "msg_{$dialogMsg->id}_user_{$userid}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话消息 - 生成文档格式
|
||||
* @param WebSocketDialogMsg $dialogMsg
|
||||
* @param $userid
|
||||
* @return array
|
||||
*/
|
||||
public static function generateMsgFormat(WebSocketDialogMsg $dialogMsg, $userid)
|
||||
{
|
||||
return [
|
||||
'dialog_id' => $dialogMsg->dialog_id,
|
||||
'created_at' => $dialogMsg->created_at,
|
||||
'updated_at' => $dialogMsg->updated_at,
|
||||
|
||||
'msg_id' => $dialogMsg->id,
|
||||
'sender_userid' => $dialogMsg->userid,
|
||||
'msg_type' => $dialogMsg->type,
|
||||
'key' => $dialogMsg->key,
|
||||
'bot' => $dialogMsg->bot ? 1 : 0,
|
||||
|
||||
'relationship' => [
|
||||
'name' => 'dialog_msg',
|
||||
'parent' => self::generateMsgParentId($dialogMsg, $userid)
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话消息 - 同步到Elasticsearch
|
||||
*/
|
||||
public static function syncMsg(WebSocketDialogMsg $dialogMsg)
|
||||
{
|
||||
try {
|
||||
$es = new self();
|
||||
|
||||
// 获取此会话的所有用户
|
||||
$dialogUsers = WebSocketDialogUser::whereDialogId($dialogMsg->dialog_id)->get();
|
||||
|
||||
if ($dialogUsers->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = ['body' => []];
|
||||
|
||||
foreach ($dialogUsers as $dialogUser) {
|
||||
$params['body'][] = [
|
||||
'index' => [
|
||||
'_index' => self::indexName(),
|
||||
'_id' => self::generateMsgDicId($dialogMsg, $dialogUser->userid),
|
||||
'routing' => self::generateMsgParentId($dialogMsg, $dialogUser->userid)
|
||||
]
|
||||
];
|
||||
$params['body'][] = self::generateMsgFormat($dialogMsg, $dialogUser->userid);
|
||||
}
|
||||
|
||||
if (!empty($params['body'])) {
|
||||
$es->bulk($params);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('syncMsg: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话消息 - 从Elasticsearch删除
|
||||
*/
|
||||
public static function deleteMsg(WebSocketDialogMsg $dialogMsg)
|
||||
{
|
||||
try {
|
||||
$es = new self();
|
||||
|
||||
// 获取此会话的所有用户
|
||||
$dialogUsers = WebSocketDialogUser::whereDialogId($dialogMsg->dialog_id)->get();
|
||||
|
||||
if ($dialogUsers->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = ['body' => []];
|
||||
|
||||
foreach ($dialogUsers as $dialogUser) {
|
||||
$params['body'][] = [
|
||||
'delete' => [
|
||||
'_index' => self::indexName(),
|
||||
'_id' => self::generateMsgDicId($dialogMsg, $dialogUser->userid),
|
||||
'routing' => self::generateMsgParentId($dialogMsg, $dialogUser->userid)
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($params['body'])) {
|
||||
$es->bulk($params);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('deleteMsg: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
136
app/Module/MsgTool.php
Normal file
136
app/Module/MsgTool.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module;
|
||||
|
||||
|
||||
use DOMDocument;
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
use League\CommonMark\Exception\CommonMarkException;
|
||||
use League\HTMLToMarkdown\HtmlConverter;
|
||||
|
||||
class MsgTool
|
||||
{
|
||||
/**
|
||||
* 截取文本并保持标签完整性
|
||||
*
|
||||
* @param string $text 要截取的文本
|
||||
* @param int $length 截取长度
|
||||
* @param string $type 文本类型 (htm 或 md)
|
||||
* @return string 处理后的文本
|
||||
*/
|
||||
public static function truncateText($text, $length, $type = 'htm')
|
||||
{
|
||||
if (empty($text) || mb_strlen($text) <= $length) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
$isMd = strtolower($type) === 'md';
|
||||
$placeholders = [];
|
||||
|
||||
// 如果是Markdown,先处理特殊标记及转换为HTML
|
||||
if ($isMd) {
|
||||
// 处理特殊标记
|
||||
$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 "";
|
||||
}
|
||||
}
|
||||
|
||||
// 创建DOM文档
|
||||
$dom = new DOMDocument('1.0', 'UTF-8');
|
||||
libxml_use_internal_errors(true);
|
||||
$dom->loadHTML(mb_convert_encoding($text, 'HTML-ENTITIES', 'UTF-8'));
|
||||
libxml_clear_errors();
|
||||
|
||||
// 获取body元素
|
||||
$body = $dom->getElementsByTagName('body')->item(0);
|
||||
$truncatedHtml = '';
|
||||
$currentLength = 0;
|
||||
|
||||
// 递归函数来遍历节点并截取内容
|
||||
self::traverseNodes($body, $currentLength, $length, $truncatedHtml);
|
||||
|
||||
// 如果是Markdown,转换回Markdown及还原特殊标记
|
||||
if ($isMd) {
|
||||
// 转换回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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归遍历节点
|
||||
* @param $node
|
||||
* @param $currentLength
|
||||
* @param $length
|
||||
* @param $truncatedHtml
|
||||
* @return void
|
||||
*/
|
||||
private static function traverseNodes($node, &$currentLength, $length, &$truncatedHtml)
|
||||
{
|
||||
foreach ($node->childNodes as $child) {
|
||||
if ($currentLength >= $length) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($child->nodeType === XML_TEXT_NODE) {
|
||||
$textContent = $child->textContent;
|
||||
$remainingLength = $length - $currentLength;
|
||||
|
||||
if (mb_strlen($textContent) > $remainingLength) {
|
||||
$truncatedHtml .= htmlspecialchars(mb_substr($textContent, 0, $remainingLength) . '...');
|
||||
$currentLength += $remainingLength;
|
||||
} else {
|
||||
$truncatedHtml .= htmlspecialchars($textContent);
|
||||
$currentLength += mb_strlen($textContent);
|
||||
}
|
||||
} elseif ($child->nodeType === XML_ELEMENT_NODE) {
|
||||
$truncatedHtml .= '<' . $child->nodeName;
|
||||
|
||||
// 添加属性
|
||||
if ($child->hasAttributes()) {
|
||||
foreach ($child->attributes as $attr) {
|
||||
$truncatedHtml .= ' ' . $attr->nodeName . '="' . htmlspecialchars($attr->nodeValue) . '"';
|
||||
}
|
||||
}
|
||||
|
||||
$truncatedHtml .= '>';
|
||||
|
||||
self::traverseNodes($child, $currentLength, $length, $truncatedHtml);
|
||||
|
||||
if ($currentLength < $length || $child->firstChild) {
|
||||
$truncatedHtml .= '</' . $child->nodeName . '>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
182
app/Module/TextExtractor.php
Normal file
182
app/Module/TextExtractor.php
Normal file
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use PhpOffice\PhpWord\IOFactory;
|
||||
use Smalot\PdfParser\Parser;
|
||||
|
||||
class TextExtractor
|
||||
{
|
||||
/**
|
||||
* 从文件中提取文本
|
||||
*
|
||||
* @param string $filePath 文件路径
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
public function extractText(string $filePath): string
|
||||
{
|
||||
if (!file_exists($filePath)) {
|
||||
throw new Exception("文件不存在: {$filePath}");
|
||||
}
|
||||
|
||||
$fileExtension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
|
||||
|
||||
try {
|
||||
return match($fileExtension) {
|
||||
'pdf' => $this->extractFromPDF($filePath),
|
||||
'docx' => $this->extractFromDOCX($filePath),
|
||||
'ipynb' => $this->extractFromIPYNB($filePath),
|
||||
default => $this->extractFromOtherFile($filePath),
|
||||
};
|
||||
} catch (Exception $e) {
|
||||
Log::error('文本提取失败', [
|
||||
'file' => $filePath,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从PDF文件中提取文本
|
||||
*
|
||||
* @param string $filePath
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function extractFromPDF(string $filePath): string
|
||||
{
|
||||
try {
|
||||
$parser = new Parser();
|
||||
$pdf = $parser->parseFile($filePath);
|
||||
|
||||
return $pdf->getText();
|
||||
} catch (Exception $e) {
|
||||
Log::error('PDF解析失败', [
|
||||
'file' => $filePath,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
throw new Exception("PDF文本提取失败: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从DOCX文件中提取文本
|
||||
*
|
||||
* @param string $filePath
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function extractFromDOCX(string $filePath): string
|
||||
{
|
||||
$phpWord = IOFactory::load($filePath);
|
||||
$text = '';
|
||||
|
||||
foreach ($phpWord->getSections() as $section) {
|
||||
foreach ($section->getElements() as $element) {
|
||||
if (method_exists($element, 'getText')) {
|
||||
$text .= $element->getText() . "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Jupyter Notebook文件中提取文本
|
||||
*
|
||||
* @param string $filePath
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function extractFromIPYNB(string $filePath): string
|
||||
{
|
||||
$content = file_get_contents($filePath);
|
||||
$notebook = json_decode($content, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new Exception("IPYNB文件解析失败: " . json_last_error_msg());
|
||||
}
|
||||
|
||||
$extractedText = '';
|
||||
|
||||
foreach ($notebook['cells'] ?? [] as $cell) {
|
||||
if (in_array($cell['cell_type'] ?? '', ['markdown', 'code']) && isset($cell['source'])) {
|
||||
$source = $cell['source'];
|
||||
$extractedText .= is_array($source)
|
||||
? implode("\n", $source)
|
||||
: $source;
|
||||
$extractedText .= "\n";
|
||||
}
|
||||
}
|
||||
|
||||
return $extractedText;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从其他类型文件中提取文本
|
||||
*
|
||||
* @param string $filePath
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function extractFromOtherFile(string $filePath): string
|
||||
{
|
||||
if ($this->isBinaryFile($filePath)) {
|
||||
throw new Exception("无法读取该类型文件的文本内容");
|
||||
}
|
||||
|
||||
return file_get_contents($filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件是否为二进制文件
|
||||
*
|
||||
* @param string $filePath
|
||||
* @return bool
|
||||
*/
|
||||
protected function isBinaryFile(string $filePath): bool
|
||||
{
|
||||
$finfo = finfo_open(FILEINFO_MIME);
|
||||
$mimeType = finfo_file($finfo, $filePath);
|
||||
finfo_close($finfo);
|
||||
|
||||
return !str_contains($mimeType, 'text/')
|
||||
&& !str_contains($mimeType, 'application/json')
|
||||
&& !str_contains($mimeType, 'application/xml');
|
||||
}
|
||||
|
||||
/** ********************************************************************* */
|
||||
/** ********************************************************************* */
|
||||
/** ********************************************************************* */
|
||||
|
||||
/**
|
||||
* 获取文件内容
|
||||
* @param $filePath
|
||||
* @param float|int $maxSize 最大文件大小,单位字节,默认300KB
|
||||
* @return string
|
||||
*/
|
||||
public static function getFileContent($filePath, float|int $maxSize = 300 * 1024)
|
||||
{
|
||||
if (!file_exists($filePath) || !is_file($filePath)) {
|
||||
return "(Failed to read contents of {$filePath})";
|
||||
}
|
||||
if (filesize($filePath) > $maxSize) {
|
||||
return "(File size exceeds " . Base::readableBytes($maxSize) . ", unable to display content)";
|
||||
}
|
||||
$te = new self();
|
||||
try {
|
||||
$isBinary = $te->isBinaryFile($filePath);
|
||||
if ($isBinary) {
|
||||
return "(Binary file, unable to display content)";
|
||||
}
|
||||
return $te->extractText($filePath);
|
||||
} catch (Exception $e) {
|
||||
return "(Failed to read contents of {$filePath}: {$e->getMessage()})";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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\ElasticSearch\ElasticSearchUserMsg;
|
||||
|
||||
class WebSocketDialogMsgObserver
|
||||
{
|
||||
/**
|
||||
* Handle the WebSocketDialogMsg "created" event.
|
||||
*
|
||||
* @param \App\Models\WebSocketDialogMsg $webSocketDialogMsg
|
||||
* @return void
|
||||
*/
|
||||
public function created(WebSocketDialogMsg $webSocketDialogMsg)
|
||||
{
|
||||
ElasticSearchUserMsg::syncMsg($webSocketDialogMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the WebSocketDialogMsg "updated" event.
|
||||
*
|
||||
* @param \App\Models\WebSocketDialogMsg $webSocketDialogMsg
|
||||
* @return void
|
||||
*/
|
||||
public function updated(WebSocketDialogMsg $webSocketDialogMsg)
|
||||
{
|
||||
ElasticSearchUserMsg::syncMsg($webSocketDialogMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the WebSocketDialogMsg "deleted" event.
|
||||
*
|
||||
* @param \App\Models\WebSocketDialogMsg $webSocketDialogMsg
|
||||
* @return void
|
||||
*/
|
||||
public function deleted(WebSocketDialogMsg $webSocketDialogMsg)
|
||||
{
|
||||
ElasticSearchUserMsg::deleteMsg($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\ElasticSearch\ElasticSearchUserMsg;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class WebSocketDialogUserObserver
|
||||
@@ -29,6 +30,7 @@ class WebSocketDialogUserObserver
|
||||
}
|
||||
}
|
||||
Deleted::forget('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
|
||||
ElasticSearchUserMsg::syncUser($webSocketDialogUser);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,7 +41,7 @@ class WebSocketDialogUserObserver
|
||||
*/
|
||||
public function updated(WebSocketDialogUser $webSocketDialogUser)
|
||||
{
|
||||
//
|
||||
ElasticSearchUserMsg::syncUser($webSocketDialogUser);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,6 +53,7 @@ class WebSocketDialogUserObserver
|
||||
public function deleted(WebSocketDialogUser $webSocketDialogUser)
|
||||
{
|
||||
Deleted::record('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
|
||||
ElasticSearchUserMsg::deleteUser($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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,10 @@ class WebSocketService implements WebSocketHandlerInterface
|
||||
case 'receipt':
|
||||
return;
|
||||
|
||||
// 握手信息
|
||||
case 'handshake':
|
||||
break;
|
||||
|
||||
// 访问状态
|
||||
case 'path':
|
||||
$row = WebSocket::whereFd($frame->fd)->first();
|
||||
|
||||
@@ -2,19 +2,21 @@
|
||||
|
||||
namespace App\Tasks;
|
||||
|
||||
use App\Models\FileContent;
|
||||
use App\Models\Project;
|
||||
use App\Models\ProjectTask;
|
||||
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 League\HTMLToMarkdown\HtmlConverter;
|
||||
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
@@ -86,7 +88,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
}
|
||||
|
||||
// 提取指令
|
||||
$command = $this->extractCommand($msg, $this->mention);
|
||||
$command = $this->extractCommand($msg, $botUser->isAiBot(), $this->mention);
|
||||
if (empty($command)) {
|
||||
return;
|
||||
}
|
||||
@@ -415,10 +417,9 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
$userBot = null;
|
||||
$extras = [];
|
||||
$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,30 +428,67 @@ 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 ($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 = '';
|
||||
$replyCommand = null;
|
||||
if ($replyMsg) {
|
||||
$replyCommand = $this->extractCommand($replyMsg);
|
||||
if ($replyCommand) {
|
||||
$replyCommand = Base::cutStr($replyCommand, 200) . "\n\n ------------------ Reference above ------------------ \n\n";
|
||||
switch ($replyMsg->type) {
|
||||
case 'text':
|
||||
$replyCommand = $this->extractCommand($replyMsg, true);
|
||||
if ($replyCommand) {
|
||||
$replyCommand = Base::cutStr($replyCommand, 20000);
|
||||
}
|
||||
break;
|
||||
case 'file':
|
||||
$msgData = Base::json2array($replyMsg->getRawOriginal('msg'));
|
||||
$replyCommand = TextExtractor::getFileContent(public_path($msgData['path']));
|
||||
break;
|
||||
}
|
||||
}
|
||||
$command = $replyCommand . $command;
|
||||
if ($replyCommand) {
|
||||
$command = <<<EOF
|
||||
<quoted_content>
|
||||
{$replyCommand}
|
||||
</quoted_content>
|
||||
|
||||
The content within the above quoted_content tags is a citation.
|
||||
|
||||
{$command}
|
||||
EOF;
|
||||
}
|
||||
}
|
||||
$this->AIGenerateSystemMessageOrBeforeText($msg->userid, $dialog, $extras);
|
||||
$this->AIGenerateSystemMessage($msg->userid, $dialog, $extras);
|
||||
$webhookUrl = "{$serverUrl}/ai/chat";
|
||||
} else {
|
||||
// 用户机器人
|
||||
@@ -532,10 +570,11 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
/**
|
||||
* 提取消息指令(提取消息内容)
|
||||
* @param WebSocketDialogMsg $msg
|
||||
* @param bool $isAiBot
|
||||
* @param bool $mention
|
||||
* @return string
|
||||
*/
|
||||
private function extractCommand(WebSocketDialogMsg $msg, bool $mention = false)
|
||||
private function extractCommand(WebSocketDialogMsg $msg, bool $isAiBot = false, bool $mention = false)
|
||||
{
|
||||
if ($msg->type !== 'text') {
|
||||
return '';
|
||||
@@ -549,26 +588,61 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
if (str_starts_with($command, '%3A.')) {
|
||||
$command = ":" . substr($command, 4);
|
||||
}
|
||||
} else {
|
||||
$command = trim(strip_tags($original));
|
||||
return $command;
|
||||
}
|
||||
if (empty($command)) {
|
||||
return '';
|
||||
$aiContents = [];
|
||||
if ($isAiBot) {
|
||||
if (preg_match_all("/<span class=\"mention task\" data-id=\"(\d+)\">(.*?)<\/span>/", $original, $match)) {
|
||||
$taskIds = Base::newIntval($match[1]);
|
||||
foreach ($taskIds as $index => $taskId) {
|
||||
$taskName = addslashes($match[2][$index]) . " (ID:{$taskId})";
|
||||
$taskContext = "任务状态:不存在或已删除";
|
||||
$taskInfo = ProjectTask::with(['content'])->whereId($taskId)->first();
|
||||
if ($taskInfo) {
|
||||
$taskName = addslashes($taskInfo->name) . " (ID:{$taskId})";
|
||||
$taskContext = implode("\n", $taskInfo->AIContext());
|
||||
}
|
||||
$aiContents[] = "<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 file\" href=\"([^\"']+?)\"[^>]*?>(.*?)<\/a>/", $original, $match)) {
|
||||
$filePaths = $match[1];
|
||||
foreach ($filePaths as $index => $filePath) {
|
||||
if (preg_match("/single\/file\/(.*?)$/", $filePath, $fileMatch)) {
|
||||
$fileName = addslashes($match[2][$index]);
|
||||
$fileContent = "文件状态:不存在或已删除";
|
||||
$fileInfo = FileContent::idOrCodeToContent($fileMatch[1]);
|
||||
if ($fileInfo && isset($fileInfo->content['url'])) {
|
||||
$filePath = public_path($fileInfo->content['url']);
|
||||
if (file_exists($filePath)) {
|
||||
$fileName .= " (ID:{$fileInfo->id})";
|
||||
$fileContent = TextExtractor::getFileContent($filePath);
|
||||
}
|
||||
}
|
||||
$aiContents[] = "<file_content path=\"{$fileName}\">\n{$fileContent}\n</file_content>";
|
||||
$original = str_replace($match[0][$index], "'{$fileName}' (see below for file_content tag)", $original);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $command;
|
||||
$command = trim(strip_tags($original));
|
||||
if ($aiContents) {
|
||||
$command .= "\n\n" . implode("\n\n", $aiContents);
|
||||
}
|
||||
return $command ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成AI系统提示词或前置消息
|
||||
* 生成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 +651,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
'type' => 'ai_prompt',
|
||||
])->value('value');
|
||||
if ($aiPrompt) {
|
||||
$system_message = $aiPrompt;
|
||||
$extras['system_message'] = $aiPrompt;
|
||||
}
|
||||
break;
|
||||
case "group":
|
||||
@@ -585,14 +659,17 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
case 'user':
|
||||
break;
|
||||
case 'project':
|
||||
$projectInfo = Project::select(['id', 'name', 'archived_at', 'deleted_at'])->whereDialogId($dialog->id)->first();
|
||||
$projectInfo = Project::whereDialogId($dialog->id)->first();
|
||||
if ($projectInfo) {
|
||||
$before_text[] = "当前我在项目【{$projectInfo->name}】中";
|
||||
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
|
||||
desc: 任务描述1
|
||||
@@ -604,24 +681,17 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
}
|
||||
break;
|
||||
case 'task':
|
||||
$taskInfo = ProjectTask::with(['content'])->select(['id', 'name', 'complete_at', 'archived_at', 'deleted_at'])->whereDialogId($dialog->id)->first();
|
||||
$taskInfo = ProjectTask::with(['content'])->whereDialogId($dialog->id)->first();
|
||||
if ($taskInfo) {
|
||||
$before_text[] = "当前我在任务【{$taskInfo->name}】中";
|
||||
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;
|
||||
}
|
||||
}
|
||||
$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
|
||||
title: 子任务标题2
|
||||
@@ -629,17 +699,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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
namespace App\Tasks;
|
||||
|
||||
use App\Models\ApproveProcInstHistory;
|
||||
use App\Models\User;
|
||||
use App\Models\UserCheckinRecord;
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
use App\Module\Extranet;
|
||||
use App\Module\Timer;
|
||||
use Cache;
|
||||
@@ -82,6 +82,9 @@ class CheckinRemindTask extends AbstractTask
|
||||
if (!UserCheckinRecord::whereUserid($user->userid)->where('created_at', '>', Carbon::now()->subDays(3))->exists()) {
|
||||
continue; // 3天内没有打卡
|
||||
}
|
||||
if (ApproveProcInstHistory::userIsLeave($user->userid)) {
|
||||
continue; // 请假、外出
|
||||
}
|
||||
$dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid);
|
||||
if ($dialog) {
|
||||
if ($type === 'exceed') {
|
||||
|
||||
39
app/Tasks/ElasticSearchSyncTask.php
Normal file
39
app/Tasks/ElasticSearchSyncTask.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tasks;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/**
|
||||
* 同步聊天数据到Elasticsearch
|
||||
*/
|
||||
class ElasticSearchSyncTask extends AbstractTask
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function start()
|
||||
{
|
||||
// 120分钟执行一次
|
||||
$time = intval(Cache::get("ElasticSearchSyncTask:Time"));
|
||||
if (time() - $time < 120 * 60) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 执行开始,120分钟后缓存标记失效
|
||||
Cache::put("ElasticSearchSyncTask:Time", time(), Carbon::now()->addMinutes(120));
|
||||
|
||||
// 开始执行同步
|
||||
@shell_exec("php /var/www/artisan elasticsearch:sync-dialog-user-msg --i");
|
||||
|
||||
// 执行完成,5分钟后缓存标记失效(5分钟任务可重复执行)
|
||||
Cache::put("ElasticSearchSyncTask:Time", time(), Carbon::now()->addMinutes(5));
|
||||
}
|
||||
|
||||
public function end()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tasks;
|
||||
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
|
||||
@@ -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,
|
||||
|
||||
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
|
||||
|
||||
|
||||
83
cmd
83
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,10 +417,49 @@ 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=(
|
||||
"docker/log/supervisor"
|
||||
"docker/mysql/data"
|
||||
"docker/office/logs"
|
||||
"docker/office/data"
|
||||
"docker/es/data"
|
||||
)
|
||||
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
|
||||
# 设置ES索引后缀
|
||||
env_set ES_INDEX_SUFFIX "$(rand_string 6)"
|
||||
# 启动容器
|
||||
[[ "$(arg_get port)" -gt 0 ]] && env_set APP_PORT "$(arg_get port)"
|
||||
$COMPOSE up php -d
|
||||
@@ -426,17 +477,17 @@ if [ $# -gt 0 ]; then
|
||||
[[ -z "$(env_get APP_KEY)" ]] && run_exec php "php artisan key:generate"
|
||||
switch_debug "false"
|
||||
# 检查数据库
|
||||
remaining=20
|
||||
remaining=10
|
||||
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"
|
||||
sleep 3
|
||||
done
|
||||
# 数据库迁移
|
||||
remaining=20
|
||||
remaining=10
|
||||
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
|
||||
@@ -447,12 +498,10 @@ if [ $# -gt 0 ]; then
|
||||
run_exec php "php artisan migrate --seed"
|
||||
done
|
||||
# 设置初始化密码
|
||||
res=`run_exec mariadb "sh /etc/mysql/repassword.sh"`
|
||||
$COMPOSE up -d
|
||||
restart_php
|
||||
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
|
||||
@@ -513,7 +562,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
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
"require": {
|
||||
"php": "^8.0",
|
||||
"ext-curl": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-fileinfo": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-imagick": "*",
|
||||
"ext-json": "*",
|
||||
@@ -18,6 +20,7 @@
|
||||
"ext-simplexml": "*",
|
||||
"ext-zip": "*",
|
||||
"directorytree/ldaprecord-laravel": "^2.7",
|
||||
"elasticsearch/elasticsearch": "^8.17",
|
||||
"fideloper/proxy": "^4.4.1",
|
||||
"firebase/php-jwt": "^6.9",
|
||||
"fruitcake/laravel-cors": "^2.0.4",
|
||||
@@ -34,7 +37,9 @@
|
||||
"mews/captcha": "^3.2.6",
|
||||
"orangehill/iseed": "^3.0.1",
|
||||
"overtrue/pinyin": "^4.0",
|
||||
"phpoffice/phpword": "^1.3",
|
||||
"predis/predis": "^1.1.7",
|
||||
"smalot/pdfparser": "^2.11",
|
||||
"symfony/mailer": "^6.0"
|
||||
},
|
||||
"require-dev": {
|
||||
@@ -83,7 +88,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,
|
||||
|
||||
1277
composer.lock
generated
1277
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AddFulltextIndexToWebSocketDialogMsgsTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$tableName = 'web_socket_dialog_msgs';
|
||||
$column = 'key'; // 需要添加 FULLTEXT 索引的字段
|
||||
|
||||
// 检查 FULLTEXT 索引是否已经存在
|
||||
if (!$this->fullTextIndexExists($tableName, $column)) {
|
||||
Schema::table($tableName, function (Blueprint $table) use ($column) {
|
||||
$table->fullText($column);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
// 删除 FULLTEXT 索引
|
||||
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
|
||||
$table->dropFullText(['key']);
|
||||
});
|
||||
}
|
||||
|
||||
private function fullTextIndexExists($tableName, $column)
|
||||
{
|
||||
// 获取当前数据库名称
|
||||
$databaseName = env('DB_DATABASE');
|
||||
|
||||
// 查询 information_schema.statistics 表
|
||||
$prefix = DB::getTablePrefix();
|
||||
$indexExists = DB::table(DB::raw('information_schema.statistics'))
|
||||
->where('table_schema', $databaseName)
|
||||
->where('table_name', $prefix . $tableName)
|
||||
->where('index_type', 'FULLTEXT')
|
||||
->get();
|
||||
|
||||
// 检查返回的索引是否包含指定的列
|
||||
foreach ($indexExists as $index) {
|
||||
$indexColumns = explode(',', $index->COLUMN_NAME ?? $index->column_name ?? '');
|
||||
// 如果索引包含指定的列,则返回 true
|
||||
if (in_array($column, $indexColumns)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddBotToWebSocketDialogMsgs 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', 'bot')) {
|
||||
$table->tinyInteger('bot')->nullable()->default(0)->after('modify')->comment('是否机器人的消息');
|
||||
$table->index('bot');
|
||||
}
|
||||
});
|
||||
|
||||
// 获取表前缀
|
||||
$prefix = DB::getTablePrefix();
|
||||
|
||||
// 使用原生SQL更新数据
|
||||
/** @noinspection SqlNoDataSourceInspection */
|
||||
DB::statement("
|
||||
UPDATE {$prefix}web_socket_dialog_msgs m
|
||||
INNER JOIN {$prefix}users u ON u.userid = m.userid
|
||||
SET m.bot = 1
|
||||
WHERE u.bot = 1
|
||||
");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('web_socket_dialog_msgs', 'bot')) {
|
||||
$table->dropIndex('bot');
|
||||
$table->dropColumn('bot');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddIsNotifiedToUmengAlias extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('umeng_alias', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('umeng_alias', 'is_notified')) {
|
||||
$table->tinyInteger('is_notified')->nullable()->default(0)->after('ua')->comment('通知权限');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('umeng_alias', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('umeng_alias', 'is_notified')) {
|
||||
$table->dropColumn('is_notified');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddVersionToUmengAlias extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('umeng_alias', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('umeng_alias', 'version')) {
|
||||
$table->string('version', 50)->nullable()->default('')->after('device')->comment('应用版本号');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('umeng_alias', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('umeng_alias', 'version')) {
|
||||
$table->dropColumn('version');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UpdateProjectTasksSubtaskProjectIdAndColumnId 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.project_id = parent.project_id,
|
||||
subtask.column_id = parent.column_id,
|
||||
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,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()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ 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
|
||||
@@ -25,8 +25,9 @@ services:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.2"
|
||||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
- redis
|
||||
- es
|
||||
restart: unless-stopped
|
||||
|
||||
nginx:
|
||||
@@ -84,7 +85,9 @@ 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
|
||||
- ./docker/office/resources/documenteditor/mobile/css/526.caf35c11a8d72ca5ac85.css:/var/www/onlyoffice/documentserver/web-apps/apps/documenteditor/mobile/css/526.caf35c11a8d72ca5ac85.css
|
||||
- ./docker/office/resources/presentationeditor/main/resources/css/app.css:/var/www/onlyoffice/documentserver/web-apps/apps/presentationeditor/main/resources/css/app.css
|
||||
@@ -167,10 +170,11 @@ services:
|
||||
|
||||
ai:
|
||||
container_name: "dootask-ai-${APP_ID}"
|
||||
image: "kuaifan/dootask-ai:0.1.4"
|
||||
image: "kuaifan/dootask-ai:0.3.4"
|
||||
environment:
|
||||
REDIS_HOST: "${REDIS_HOST}"
|
||||
REDIS_PORT: "${REDIS_PORT}"
|
||||
TIMEOUT: 600
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.12"
|
||||
@@ -221,6 +225,21 @@ services:
|
||||
ipv4_address: "${APP_IPPR}.14"
|
||||
restart: unless-stopped
|
||||
|
||||
es:
|
||||
container_name: "dootask-es-${APP_ID}"
|
||||
image: "elasticsearch:8.17.2"
|
||||
volumes:
|
||||
- ./docker/es/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
|
||||
- ./docker/es/data:/usr/share/elasticsearch/data
|
||||
environment:
|
||||
discovery.type: single-node
|
||||
xpack.security.enabled: false
|
||||
ES_JAVA_OPTS: "-Xms1g -Xmx1g"
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.15"
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
extnetwork:
|
||||
name: "dootask-networks-${APP_ID}"
|
||||
|
||||
2
docker/es/config/elasticsearch.yml
Normal file
2
docker/es/config/elasticsearch.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
cluster.name: "docker-cluster"
|
||||
network.host: 0.0.0.0
|
||||
2
docker/es/data/.gitignore
vendored
Executable file
2
docker/es/data/.gitignore
vendored
Executable file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
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,125 +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_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
docker/office/resources/common/main/resources/img/header/.gitignore
vendored
Normal file
1
docker/office/resources/common/main/resources/img/header/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.gz
|
||||
@@ -0,0 +1,2 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="85" height="20" fill="none">
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 82 B |
@@ -0,0 +1,2 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="85" height="20" fill="none">
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 83 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="28" height="60"><symbol id="svg-icon-crypted" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 3C7 5 4 5 4 5v3.5c0 2 .563 7.477 6 8.5 5.436-1.023 6-6.5 6-8.5V5s-3 0-6-2m4.023 4.965-1.046-.93-3.55 3.993-2.479-2.066-.896 1.076 3.52 2.934z" clip-rule="evenodd"/></symbol><symbol id="svg-icon-users" viewBox="0 0 28 20"><path fill-rule="evenodd" d="M27 10a9 9 0 1 1-18 0 9 9 0 0 1 18 0m1 0c0 5.523-4.477 10-10 10a9.96 9.96 0 0 1-6.798-2.666 8 8 0 1 1 0-14.667A9.96 9.96 0 0 1 18 0c5.523 0 10 4.477 10 10M10.451 3.441A7 7 0 0 0 8 3a7 7 0 0 0-5.69 11.079 11 11 0 0 1 1.124-.76C4.544 12.666 6.132 12 8 12q.1 0 .2.003A10 10 0 0 1 8.05 11H8a3 3 0 1 1 1.198-5.751 10 10 0 0 1 1.253-1.808M8.767 6.152A2 2 0 0 0 8 6a2 2 0 1 0 0 4 10 10 0 0 1 .767-3.848m-.304 6.864A7 7 0 0 0 8 13c-1.632 0-3.043.584-4.059 1.181a10 10 0 0 0-.99.667A6.98 6.98 0 0 0 8 17a7 7 0 0 0 2.451-.441 10 10 0 0 1-1.988-3.543" clip-rule="evenodd"/></symbol></svg>
|
||||
|
After Width: | Height: | Size: 994 B |
1
electron/.gitignore
vendored
1
electron/.gitignore
vendored
@@ -10,3 +10,4 @@ cache/*
|
||||
|
||||
.devload
|
||||
.native
|
||||
.build
|
||||
159
electron/build.js
vendored
159
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,20 @@ 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 +718,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 +736,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 +766,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(_ => { });
|
||||
}
|
||||
|
||||
153
electron/electron.js
vendored
153
electron/electron.js
vendored
@@ -54,6 +54,8 @@ let screenshotObj = null,
|
||||
|
||||
let childWindow = [],
|
||||
preloadWindow = null,
|
||||
mediaWindow = null,
|
||||
mediaType = null,
|
||||
webTabWindow = null,
|
||||
webTabView = [],
|
||||
webTabHeight = 38;
|
||||
@@ -161,7 +163,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();
|
||||
}
|
||||
@@ -432,7 +441,7 @@ function createChildWindow(args) {
|
||||
if (/^https?:/i.test(hash)) {
|
||||
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(() => {
|
||||
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 {
|
||||
@@ -475,36 +484,70 @@ function updateChildWindow(browser, args) {
|
||||
* @param type
|
||||
*/
|
||||
function createMediaWindow(args, type = 'image') {
|
||||
const imageWindow = new BrowserWindow({
|
||||
width: args.width || 970,
|
||||
height: args.height || 700,
|
||||
minWidth: 360,
|
||||
minHeight: 360,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
webSecurity: false,
|
||||
plugins: true
|
||||
},
|
||||
show: false
|
||||
});
|
||||
if (mediaWindow === null) {
|
||||
mediaWindow = new BrowserWindow({
|
||||
width: args.width || 970,
|
||||
height: args.height || 700,
|
||||
minWidth: 360,
|
||||
minHeight: 360,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
webSecurity: false,
|
||||
plugins: true
|
||||
},
|
||||
show: false
|
||||
});
|
||||
|
||||
// 监听关闭事件
|
||||
mediaWindow.addListener('close', event => {
|
||||
if (!willQuitApp) {
|
||||
event.preventDefault()
|
||||
if (mediaWindow.isFullScreen()) {
|
||||
mediaWindow.once('leave-full-screen', () => {
|
||||
mediaWindow.hide();
|
||||
})
|
||||
mediaWindow.setFullScreen(false)
|
||||
} else {
|
||||
mediaWindow.webContents.send('on-close');
|
||||
mediaWindow.hide();
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 监听关闭事件
|
||||
mediaWindow.addListener('closed', () => {
|
||||
mediaWindow = null;
|
||||
mediaType = null;
|
||||
})
|
||||
|
||||
// 设置右键菜单
|
||||
electronMenu.webContentsMenu(mediaWindow.webContents)
|
||||
} else {
|
||||
// 直接显示
|
||||
mediaWindow.show();
|
||||
}
|
||||
|
||||
// 加载图片浏览器的HTML
|
||||
let filePath = './render/viewer/index.html';
|
||||
if (type === 'video') {
|
||||
filePath = './render/video/index.html';
|
||||
if (mediaType === type) {
|
||||
// 更新窗口
|
||||
mediaWindow.webContents.send('load-media', args);
|
||||
} else {
|
||||
// 重置窗口
|
||||
mediaType = type;
|
||||
let filePath = './render/viewer/index.html';
|
||||
if (type === 'video') {
|
||||
filePath = './render/video/index.html';
|
||||
}
|
||||
mediaWindow.loadFile(filePath, {}).then(_ => { }).catch(_ => { })
|
||||
}
|
||||
imageWindow.loadFile(filePath, {}).then(_ => { }).catch(_ => { })
|
||||
|
||||
// 设置右键菜单
|
||||
electronMenu.webContentsMenu(imageWindow.webContents)
|
||||
|
||||
// 窗口准备好后显示
|
||||
imageWindow.on('ready-to-show', () => {
|
||||
imageWindow.show();
|
||||
// 发送图片数据到渲染进程
|
||||
imageWindow.webContents.send('load-media', args);
|
||||
// 窗口准备好后事件
|
||||
mediaWindow.removeAllListeners("ready-to-show");
|
||||
mediaWindow.addListener('ready-to-show', () => {
|
||||
mediaWindow.show();
|
||||
mediaWindow.webContents.send('load-media', args);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -527,8 +570,6 @@ function createWebTabWindow(args) {
|
||||
|
||||
// 创建父级窗口
|
||||
if (!webTabWindow) {
|
||||
let config = Object.assign(args.config || {}, userConf.get('webTabWindow', {}));
|
||||
let webPreferences = args.webPreferences || {};
|
||||
const titleBarOverlay = {
|
||||
height: webTabHeight
|
||||
}
|
||||
@@ -549,14 +590,14 @@ function createWebTabWindow(args) {
|
||||
titleBarStyle: 'hidden',
|
||||
titleBarOverlay,
|
||||
backgroundColor: nativeTheme.shouldUseDarkColors ? '#3B3B3D' : '#EFF0F4',
|
||||
webPreferences: Object.assign({
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'electron-preload.js'),
|
||||
webSecurity: true,
|
||||
nodeIntegration: true,
|
||||
contextIsolation: true,
|
||||
nativeWindowOpen: true
|
||||
}, webPreferences),
|
||||
}, config))
|
||||
},
|
||||
}, userConf.get('webTabWindow', {})))
|
||||
|
||||
webTabWindow.on('resize', () => {
|
||||
resizeWebTab(0)
|
||||
@@ -621,15 +662,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')
|
||||
@@ -840,6 +886,7 @@ function monitorThemeChanges() {
|
||||
const backgroundColor = utils.getDefaultBackgroundColor()
|
||||
mainWindow?.setBackgroundColor(backgroundColor);
|
||||
preloadWindow?.setBackgroundColor(backgroundColor);
|
||||
mediaWindow?.setBackgroundColor(backgroundColor);
|
||||
childWindow.some(({browser}) => browser.setBackgroundColor(backgroundColor))
|
||||
webTabWindow?.setBackgroundColor(nativeTheme.shouldUseDarkColors ? '#3B3B3D' : '#EFF0F4')
|
||||
// 通知所有窗口
|
||||
@@ -1110,6 +1157,7 @@ ipcMain.on('childWindowCloseAll', (event) => {
|
||||
browser && browser.close()
|
||||
})
|
||||
preloadWindow?.close()
|
||||
mediaWindow?.close()
|
||||
event.returnValue = "ok"
|
||||
})
|
||||
|
||||
@@ -1121,6 +1169,7 @@ ipcMain.on('childWindowDestroyAll', (event) => {
|
||||
browser && browser.destroy()
|
||||
})
|
||||
preloadWindow?.destroy()
|
||||
mediaWindow?.destroy()
|
||||
event.returnValue = "ok"
|
||||
})
|
||||
|
||||
@@ -1362,15 +1411,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)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查更新
|
||||
@@ -1380,6 +1431,9 @@ ipcMain.on('updateCheckAndDownload', (event, args) => {
|
||||
if (autoUpdating + 3600 > utils.dayjs().unix()) {
|
||||
return // 限制1小时仅执行一次
|
||||
}
|
||||
if (!autoUpdater) {
|
||||
return
|
||||
}
|
||||
if (args.provider) {
|
||||
autoUpdater.setFeedURL(args)
|
||||
}
|
||||
@@ -1437,6 +1491,7 @@ ipcMain.on('updateQuitAndInstall', (event, args) => {
|
||||
browser && browser.destroy()
|
||||
})
|
||||
preloadWindow?.destroy()
|
||||
mediaWindow?.destroy()
|
||||
|
||||
// 启动更新子窗口
|
||||
createUpdaterWindow(args.updateTitle)
|
||||
@@ -1444,7 +1499,7 @@ ipcMain.on('updateQuitAndInstall', (event, args) => {
|
||||
// 退出并安装更新
|
||||
setTimeout(_ => {
|
||||
mainWindow.hide()
|
||||
autoUpdater.quitAndInstall(true, true)
|
||||
autoUpdater?.quitAndInstall(true, true)
|
||||
}, 600)
|
||||
})
|
||||
|
||||
|
||||
@@ -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-->
|
||||
|
||||
@@ -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.0",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -235,7 +235,7 @@
|
||||
disableCaptions: 'Désactiver les sous-titres',
|
||||
enterFullscreen: 'Plein écran',
|
||||
exitFullscreen: 'Quitter le plein écran',
|
||||
frameTitle: 'Lecteur vidéo',
|
||||
frameTitle: 'Lecteur Vidéo',
|
||||
captions: 'Sous-titres',
|
||||
settings: 'Paramètres',
|
||||
menuBack: 'Retour au menu précédent',
|
||||
@@ -310,46 +310,79 @@
|
||||
ru: 'Видеоплеер'
|
||||
};
|
||||
|
||||
// 获取视频MIME类型
|
||||
const getVideoMimeType = (filename) => {
|
||||
const ext = filename.split('.').pop().toLowerCase();
|
||||
const mimeTypes = {
|
||||
'mp4': 'video/mp4',
|
||||
'webm': 'video/webm',
|
||||
'ogg': 'video/ogg',
|
||||
'mov': 'video/quicktime',
|
||||
'm4v': 'video/mp4',
|
||||
'avi': 'video/x-msvideo',
|
||||
'wmv': 'video/x-ms-wmv',
|
||||
'flv': 'video/x-flv',
|
||||
'mkv': 'video/x-matroska',
|
||||
'3gp': 'video/3gpp'
|
||||
};
|
||||
return mimeTypes[ext] || 'video/mp4';
|
||||
};
|
||||
|
||||
let player = null;
|
||||
|
||||
// 接收主进程传来的关闭事件
|
||||
ipcRenderer.on('on-close', () => {
|
||||
if (player) {
|
||||
player.stop()
|
||||
}
|
||||
});
|
||||
|
||||
// 接收主进程传来的视频路径
|
||||
ipcRenderer.on('load-media', (event, args) => {
|
||||
const videoElement = document.querySelector('video');
|
||||
videoElement.src = args.video;
|
||||
|
||||
// 销毁旧的播放器实例
|
||||
if (player) {
|
||||
player.destroy();
|
||||
// 检查视频源是否有效
|
||||
if (!args || !args.video) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置语言
|
||||
const currentLang = args.lang || 'en';
|
||||
|
||||
// 创建新的播放器实例
|
||||
player = new Plyr('#player', {
|
||||
controls: [
|
||||
'play-large', // 大播放按钮
|
||||
'play', // 播放/暂停
|
||||
'progress', // 进度条
|
||||
'current-time', // 当前时间
|
||||
'duration', // 总时长
|
||||
'mute', // 静音
|
||||
'volume', // 音量
|
||||
'settings', // 设置
|
||||
'fullscreen' // 全屏
|
||||
],
|
||||
settings: ['captions', 'quality', 'speed', 'loop'], // 设置菜单选项
|
||||
speed: {selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 2]}, // 播放速度选项
|
||||
keyboard: {focused: true, global: true}, // 启用键盘快捷键
|
||||
tooltips: {controls: true, seek: true}, // 显示工具提示
|
||||
hideControls: true, // 自动隐藏控制栏
|
||||
i18n: translations[currentLang] || translations['en'] // 设置语言
|
||||
});
|
||||
// 如果播放器不存在,创建新的实例
|
||||
if (!player) {
|
||||
player = new Plyr('#player', {
|
||||
controls: [
|
||||
'play-large', // 大播放按钮
|
||||
'play', // 播放/暂停
|
||||
'progress', // 进度条
|
||||
'current-time', // 当前时间
|
||||
'duration', // 总时长
|
||||
'mute', // 静音
|
||||
'volume', // 音量
|
||||
'settings', // 设置
|
||||
'fullscreen' // 全屏
|
||||
],
|
||||
settings: ['captions', 'quality', 'speed', 'loop'], // 设置菜单选项
|
||||
speed: {selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 2]}, // 播放速度选项
|
||||
keyboard: {focused: true, global: true}, // 启用键盘快捷键
|
||||
tooltips: {controls: true, seek: true}, // 显示工具提示
|
||||
hideControls: true, // 自动隐藏控制栏
|
||||
i18n: translations[currentLang] || translations['en'] // 设置语言
|
||||
});
|
||||
|
||||
// 错误处理
|
||||
player.on('error', (event) => {
|
||||
console.error('Player error:', event);
|
||||
});
|
||||
// 错误处理
|
||||
player.on('error', (event) => {
|
||||
console.error('Player error:', event);
|
||||
});
|
||||
}
|
||||
|
||||
// 直接更新播放器源
|
||||
player.source = {
|
||||
type: 'video',
|
||||
sources: [{
|
||||
src: args.video,
|
||||
type: getVideoMimeType(args.video)
|
||||
}]
|
||||
};
|
||||
|
||||
// 更新网页标题
|
||||
document.title = args.title || titleTranslations[currentLang] || titleTranslations['en'];
|
||||
|
||||
@@ -35,16 +35,18 @@
|
||||
<script src="./viewer.min.js"></script>
|
||||
<script>
|
||||
const {ipcRenderer} = require('electron');
|
||||
let viewer = null;
|
||||
|
||||
const thumbnailUrl = (url) => {
|
||||
url = `${url}`
|
||||
.replace(/_thumb\.(png|jpg|jpeg)$/, '')
|
||||
.replace(/\/crop\/([^\/]+)$/, '')
|
||||
if (!/^https?:\/\/[^\/]+\/uploads\//.test(url)) {
|
||||
return url
|
||||
}
|
||||
const crops = {
|
||||
ratio: 3,
|
||||
percentage: '320x0'
|
||||
}
|
||||
url = `${url}`
|
||||
.replace(/_thumb\.(png|jpg|jpeg)$/, '')
|
||||
.replace(/\/crop\/([^\/]+)$/, '')
|
||||
return url + "/crop/" + Object.keys(crops).map(key => {
|
||||
return `${key}:${crops[key]}`
|
||||
}).join(",")
|
||||
@@ -63,8 +65,22 @@
|
||||
ru: 'Просмотр изображений'
|
||||
};
|
||||
|
||||
let viewer = null;
|
||||
|
||||
// 接收主进程传来的关闭事件
|
||||
ipcRenderer.on('on-close', () => {
|
||||
if (viewer) {
|
||||
viewer.destroy()
|
||||
}
|
||||
});
|
||||
|
||||
// 接收主进程发送的图片数据
|
||||
ipcRenderer.on('load-media', (event, args) => {
|
||||
// 检查数据源是否有效
|
||||
if (!args || !args.images) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.getElementById('image-container');
|
||||
container.innerHTML = ''; // 清空容器
|
||||
|
||||
|
||||
@@ -171,9 +171,9 @@ LDAP 用户禁止修改邮箱
|
||||
部门不存在或已被删除
|
||||
最多只能创建(*)个部门
|
||||
上级部门不存在或已被删除
|
||||
上级部门层级错误
|
||||
部门层级最多只能创建(*)级
|
||||
每个部门最多只能创建(*)个子部门
|
||||
含有子部门无法修改上级部门
|
||||
含有子部门无法删除
|
||||
请选择正确的部门负责人
|
||||
新建成功
|
||||
此功能未开启,请联系管理员开启
|
||||
@@ -801,4 +801,25 @@ webhook地址最长仅支持255个字符。
|
||||
新增子任务标签
|
||||
更新子任务标签
|
||||
|
||||
AI机器人不存在
|
||||
|
||||
内容不存在
|
||||
长文本
|
||||
|
||||
选择模型
|
||||
当前对话不支持
|
||||
会话不存在或已被删除
|
||||
开启新会话
|
||||
历史会话
|
||||
|
||||
未找到默认模型
|
||||
思考中...
|
||||
|
||||
请先填写 Base URL
|
||||
获取失败
|
||||
|
||||
任务超期未完成
|
||||
每个用户最多只能负责(*)个部门
|
||||
不能选择自己的子部门作为上级部门
|
||||
|
||||
转文字失败
|
||||
|
||||
@@ -1025,7 +1025,6 @@ Pro版
|
||||
隐藏共享文件
|
||||
单聊
|
||||
显示文件
|
||||
ID、任务名...
|
||||
仅显示我的
|
||||
语音
|
||||
群头像
|
||||
@@ -1221,7 +1220,6 @@ OKR 结果分析
|
||||
AI 机器人
|
||||
任务相关
|
||||
请填写名称!
|
||||
访问OpenAI网站查看
|
||||
使用代理
|
||||
支持 http 或 socks 代理
|
||||
例如:http://proxy.com 或 socks5://proxy.com
|
||||
@@ -1705,7 +1703,6 @@ WiFi签到延迟时长为±1分钟。
|
||||
选择群组
|
||||
输入关键词搜索群
|
||||
仅支持选择个人群转为部门群
|
||||
含有子部门无法修改上级部门
|
||||
删除部门
|
||||
你确定要删除【(*)】部门吗?
|
||||
注意:此操作不可恢复,部门下的成员将移至默认部门。
|
||||
@@ -1903,3 +1900,85 @@ WiFi签到延迟时长为±1分钟。
|
||||
请选择示例标签
|
||||
全部保存成功
|
||||
|
||||
消息详情
|
||||
长文本
|
||||
你确定要创建任务吗?
|
||||
你确定要创建子任务吗?
|
||||
|
||||
正在处理,请稍后再试...
|
||||
|
||||
开启麦克风失败!
|
||||
开启摄像头失败!
|
||||
|
||||
群机器人
|
||||
群成员 ((*)人)
|
||||
此消息已经过期
|
||||
|
||||
DeepSeek大语言模型算法是北京深度求索人工智能基础技术研究有限公司推出的深度合成服务算法。
|
||||
API请求的基础URL路径,如果没有请留空
|
||||
|
||||
欢迎词
|
||||
仪表盘欢迎词,(*)代表用户昵称
|
||||
思考过程
|
||||
隐藏翻译
|
||||
重新翻译
|
||||
|
||||
当前客户端不支持该指令
|
||||
默认模型
|
||||
|
||||
与(*)会话历史
|
||||
当前
|
||||
新会话
|
||||
|
||||
模型温度,低则保守,高则多样
|
||||
例如:0.7,范围:0-1,默认:0.7
|
||||
|
||||
模型列表
|
||||
一行一个模型名称
|
||||
请选择默认模型
|
||||
可选数据来自模型列表
|
||||
|
||||
使用默认模型列表
|
||||
未知操作
|
||||
获取失败
|
||||
获取成功
|
||||
|
||||
Grok是由xAI开发的生成式人工智能聊天机器人,旨在通过实时回答用户问题来提供帮助。
|
||||
Ollama 是一个轻量级、可扩展的框架,旨在让用户能够在本地机器上构建和运行大型语言模型。
|
||||
|
||||
AI 列表
|
||||
AI 设置
|
||||
思考中...
|
||||
|
||||
请先填写 Base URL
|
||||
|
||||
例如:http://proxy.com
|
||||
API请求的URL路径
|
||||
如果没有请留空
|
||||
获取本地模型列表
|
||||
|
||||
任务超期未完成
|
||||
打开部门群
|
||||
群组 ID
|
||||
最多选择(*)个部门
|
||||
已共享
|
||||
联系人
|
||||
未搜到跟「(*)」相关的结果
|
||||
暂无相关结果
|
||||
请输入关键字
|
||||
请输入关键字搜索
|
||||
正在拼命搜索...
|
||||
|
||||
加载失败,请重启软件
|
||||
发送原语音
|
||||
转文字失败
|
||||
|
||||
请稍后再试...
|
||||
选择识别语言
|
||||
自动识别
|
||||
选择翻译结果
|
||||
不翻译结果
|
||||
即将到期
|
||||
|
||||
创建任务
|
||||
在消息中打开
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user