Compare commits
327 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d366cf9885 | ||
|
|
be53afe6b4 | ||
|
|
cdd980112d | ||
|
|
bca284969d | ||
|
|
dd899a3e13 | ||
|
|
d6ca66aa2f | ||
|
|
20ba671cd3 | ||
|
|
672795ac49 | ||
|
|
9716d7fe43 | ||
|
|
193ad8d902 | ||
|
|
a87f903c50 | ||
|
|
82f154a229 | ||
|
|
bee36801ab | ||
|
|
37f379c890 | ||
|
|
88b995ca9c | ||
|
|
919289c5ca | ||
|
|
0535b56766 | ||
|
|
6afd413b87 | ||
|
|
4818409329 | ||
|
|
919b652a06 | ||
|
|
15d3ec9d81 | ||
|
|
e0be6e429e | ||
|
|
8d24be914d | ||
|
|
8bbe9c97e9 | ||
|
|
ccbd904a3f | ||
|
|
4ed3db7e41 | ||
|
|
65ced28004 | ||
|
|
4c282962b3 | ||
|
|
c64c436b9f | ||
|
|
378e270f41 | ||
|
|
7217bd7d1a | ||
|
|
ff2461d89d | ||
|
|
0eb3430c14 | ||
|
|
da7c1e40e3 | ||
|
|
8c9e928ddc | ||
|
|
477aef7db6 | ||
|
|
cc97d9f1ea | ||
|
|
ee6cf05a92 | ||
|
|
575db58476 | ||
|
|
986a2f8cbb | ||
|
|
0b1da914cd | ||
|
|
04acd7c56d | ||
|
|
4430d85242 | ||
|
|
55ade32589 | ||
|
|
0ffbaaaeaa | ||
|
|
62b40ddb84 | ||
|
|
2d5ce87605 | ||
|
|
7ca0bc5960 | ||
|
|
021c09e426 | ||
|
|
75db81f2f9 | ||
|
|
f162617765 | ||
|
|
b7d10a4c58 | ||
|
|
79ca1aea02 | ||
|
|
957201804c | ||
|
|
cf5e126eaa | ||
|
|
69fc0a118b | ||
|
|
4dacc26567 | ||
|
|
7de1ed7d45 | ||
|
|
ab47f01625 | ||
|
|
13c4fa4f1f | ||
|
|
173631f115 | ||
|
|
8462e9c097 | ||
|
|
3c9447e1b6 | ||
|
|
38eaf2eb02 | ||
|
|
c8364ed17b | ||
|
|
ba52738904 | ||
|
|
4061ae4275 | ||
|
|
82afb5b150 | ||
|
|
e1203f0c8d | ||
|
|
e6f6b3fee2 | ||
|
|
e5efcd3d26 | ||
|
|
bf45587c80 | ||
|
|
29a0d22938 | ||
|
|
635cc04c50 | ||
|
|
bc5343652b | ||
|
|
03f140fe3b | ||
|
|
3e4a119f61 | ||
|
|
3b7bcbc14a | ||
|
|
3c49e96e02 | ||
|
|
5be209ab59 | ||
|
|
56ea048ab3 | ||
|
|
9fc0bd0439 | ||
|
|
1c2798cbf4 | ||
|
|
9d8af2eaab | ||
|
|
bba1e0d12f | ||
|
|
c060e60e4a | ||
|
|
1c504bd899 | ||
|
|
b617648bd8 | ||
|
|
e849c7a34f | ||
|
|
f6dd1ce98e | ||
|
|
9c78db8d45 | ||
|
|
5154348cf9 | ||
|
|
4521cea3b4 | ||
|
|
0ff1ac7743 | ||
|
|
277a751ed4 | ||
|
|
96be2a86ca | ||
|
|
f28bff569a | ||
|
|
e34aa77a54 | ||
|
|
e53b65496f | ||
|
|
f6ee630615 | ||
|
|
ec2e1e3152 | ||
|
|
6cffe9baed | ||
|
|
b63df27409 | ||
|
|
617c466ac0 | ||
|
|
ed8e443f3a | ||
|
|
58cb49b125 | ||
|
|
7dd5baa9ec | ||
|
|
bbf9107560 | ||
|
|
be527355ee | ||
|
|
c866500120 | ||
|
|
3e2a40aaa0 | ||
|
|
eef9fa56c6 | ||
|
|
945d84dbc4 | ||
|
|
d353d33107 | ||
|
|
f54bad5d79 | ||
|
|
b605c70e91 | ||
|
|
1752e88c42 | ||
|
|
e2718a39a0 | ||
|
|
25298ac69e | ||
|
|
cf9f389f75 | ||
|
|
567c75830a | ||
|
|
7b1d352c95 | ||
|
|
4fa54381a6 | ||
|
|
9c91f7cf83 | ||
|
|
edd5cd1ca1 | ||
|
|
f2ec6ad05e | ||
|
|
a04ef4ac38 | ||
|
|
43b3d1d379 | ||
|
|
b65fdeacc2 | ||
|
|
622fe1e5d9 | ||
|
|
a6c7c0c7ad | ||
|
|
e5c8748b75 | ||
|
|
f096d71cc1 | ||
|
|
d73a152a36 | ||
|
|
f4e6fd060e | ||
|
|
c78ca1de5d | ||
|
|
2b219c7256 | ||
|
|
6ffa651742 | ||
|
|
cb3b22a4bf | ||
|
|
145bfdb0e9 | ||
|
|
8c7b0c502d | ||
|
|
684bf12a5c | ||
|
|
aaa75aff14 | ||
|
|
f03600bd65 | ||
|
|
1c4c4fe3fb | ||
|
|
5e46b2cd1a | ||
|
|
027db7c0ec | ||
|
|
5bb17ddc6b | ||
|
|
e8edd74bc3 | ||
|
|
ed064a825a | ||
|
|
32c232a0b5 | ||
|
|
c2fd747c45 | ||
|
|
9148853f2c | ||
|
|
23d0f50a3d | ||
|
|
36cdf87bfe | ||
|
|
cfd2e1fd7b | ||
|
|
3cafac99ff | ||
|
|
1dd4e8da71 | ||
|
|
543015a36e | ||
|
|
2efdfc4b1f | ||
|
|
7234d9307e | ||
|
|
769ce1ce7c | ||
|
|
62c1d5783e | ||
|
|
a6bd4a2ffe | ||
|
|
f1a9077b7e | ||
|
|
2c3e80bd8f | ||
|
|
e52d066fb0 | ||
|
|
5279d57018 | ||
|
|
25e5eb4427 | ||
|
|
b01d5ce8c4 | ||
|
|
ff41f5c041 | ||
|
|
dd0770a93f | ||
|
|
9a3e76fff3 | ||
|
|
7c867578ee | ||
|
|
d543c27000 | ||
|
|
a8be330baa | ||
|
|
c128c58110 | ||
|
|
e32a3887cd | ||
|
|
94932c7486 | ||
|
|
a1920745fb | ||
|
|
51e8f9555e | ||
|
|
213ab8418b | ||
|
|
707f1dd6cb | ||
|
|
125ce036cd | ||
|
|
172c562a71 | ||
|
|
80bbe6711c | ||
|
|
3f56c64086 | ||
|
|
e6167119e0 | ||
|
|
368fae5f32 | ||
|
|
6ae46cf7bb | ||
|
|
e97806c85b | ||
|
|
f31e88bed1 | ||
|
|
6bd20038f9 | ||
|
|
30cfb1200d | ||
|
|
154e0039d1 | ||
|
|
a8f3b02ee7 | ||
|
|
b3e83e13bc | ||
|
|
d0a0e77c44 | ||
|
|
a14896307f | ||
|
|
976b300277 | ||
|
|
ccbd873204 | ||
|
|
9c1482f9e9 | ||
|
|
5a7f4efa91 | ||
|
|
f78c4a1fb0 | ||
|
|
db6500369f | ||
|
|
9e4beaa317 | ||
|
|
afd021737a | ||
|
|
3982ed56f7 | ||
|
|
df4a01a7f9 | ||
|
|
a6fac96ec1 | ||
|
|
8ed9186ff4 | ||
|
|
821df75d4b | ||
|
|
0c09a2445c | ||
|
|
e6983e858d | ||
|
|
f8b69df955 | ||
|
|
15370a93c7 | ||
|
|
bc18aeeadc | ||
|
|
a1f143b0aa | ||
|
|
c13fe9d590 | ||
|
|
50203fbcb3 | ||
|
|
ffe7ebf711 | ||
|
|
f0b5e0c3b9 | ||
|
|
501235ef12 | ||
|
|
da0fa31181 | ||
|
|
0272933f70 | ||
|
|
30d88761b4 | ||
|
|
fb286cea3c | ||
|
|
6bcc7b6c49 | ||
|
|
6338a44cc1 | ||
|
|
ae4680f20c | ||
|
|
2841874417 | ||
|
|
b6a4e6b4de | ||
|
|
34cfd1e344 | ||
|
|
b467dc55e5 | ||
|
|
9fd8d44a6e | ||
|
|
64262134c4 | ||
|
|
0019c9ef41 | ||
|
|
2676ebd047 | ||
|
|
97cdd56110 | ||
|
|
d973451bdc | ||
|
|
80313f613e | ||
|
|
5c564524a3 | ||
|
|
e081fbd92b | ||
|
|
0ecc20472a | ||
|
|
b51052f0c6 | ||
|
|
cb106e42ee | ||
|
|
52f9495ff8 | ||
|
|
440b633bad | ||
|
|
a07913181a | ||
|
|
34ffd96c86 | ||
|
|
46a623b430 | ||
|
|
c16e37023c | ||
|
|
1cb0cdf540 | ||
|
|
073d03a882 | ||
|
|
30b9276ab4 | ||
|
|
76c8b4a4c6 | ||
|
|
9ea4781d93 | ||
|
|
07d583f73f | ||
|
|
12c74aef7a | ||
|
|
64b10e3060 | ||
|
|
ab2b29f267 | ||
|
|
be9a968ad9 | ||
|
|
5f87067a75 | ||
|
|
ef273bd9dd | ||
|
|
0737a9fae7 | ||
|
|
727d7e1d81 | ||
|
|
87e8589aea | ||
|
|
b13758d3e9 | ||
|
|
14775e2861 | ||
|
|
94af3822d8 | ||
|
|
07254c9f27 | ||
|
|
a99c2f6944 | ||
|
|
f9540b08cd | ||
|
|
34af77eb6d | ||
|
|
cf3f22776c | ||
|
|
5bebc8b5ee | ||
|
|
8a4b0c57f9 | ||
|
|
1acfd7ee34 | ||
|
|
a29661c54d | ||
|
|
90558d5ece | ||
|
|
e6c7007be5 | ||
|
|
16d0d1687f | ||
|
|
95ab44d118 | ||
|
|
e541757b76 | ||
|
|
f422aea330 | ||
|
|
d5eb3716aa | ||
|
|
7fb854fb48 | ||
|
|
60b5ecdcd7 | ||
|
|
6cce7d31ff | ||
|
|
46f5dd99a6 | ||
|
|
9753dec996 | ||
|
|
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 |
@@ -17,7 +17,7 @@ LOG_CHANNEL=stack
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST="${APP_IPPR}.5"
|
||||
DB_HOST=mariadb
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=dootask
|
||||
DB_USERNAME=dootask
|
||||
@@ -34,7 +34,7 @@ SESSION_LIFETIME=120
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_HOST="${APP_IPPR}.4"
|
||||
REDIS_HOST=redis
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
|
||||
49
.github/workflows/publish.yml
vendored
49
.github/workflows/publish.yml
vendored
@@ -115,9 +115,50 @@ jobs:
|
||||
})
|
||||
return data.id
|
||||
|
||||
build-client:
|
||||
pack-vendor:
|
||||
needs: [ check-version, create-release ]
|
||||
if: needs.check-version.outputs.should_release == 'true'
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.0'
|
||||
extensions: mbstring, intl, gd, xml, zip, swoole
|
||||
tools: composer:v2
|
||||
|
||||
- name: Install Dependencies
|
||||
run: composer install
|
||||
|
||||
- name: Create Vendor Archive
|
||||
run: tar -czf vendor.tar.gz vendor/
|
||||
|
||||
- name: Upload Vendor Archive
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
const data = await fs.promises.readFile('vendor.tar.gz');
|
||||
|
||||
await github.rest.repos.uploadReleaseAsset({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: process.env.RELEASE_ID,
|
||||
name: 'vendor.tar.gz',
|
||||
data: data
|
||||
});
|
||||
|
||||
build-client:
|
||||
needs: [ check-version, create-release, pack-vendor ]
|
||||
if: needs.check-version.outputs.should_release == 'true'
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
@@ -231,7 +272,7 @@ jobs:
|
||||
./cmd electron win
|
||||
|
||||
publish-release:
|
||||
needs: [ check-version, create-release, build-client ]
|
||||
needs: [ check-version, create-release, pack-vendor, build-client ]
|
||||
if: needs.check-version.outputs.should_release == 'true' && github.ref == 'refs/heads/pro'
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -258,7 +299,7 @@ jobs:
|
||||
env:
|
||||
PUBLISH_KEY: ${{ secrets.PUBLISH_KEY }}
|
||||
run: |
|
||||
pushd electron
|
||||
pushd electron || exit
|
||||
npm install
|
||||
popd
|
||||
popd || exit
|
||||
node ./electron/build.js published
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -15,6 +15,7 @@
|
||||
.idea
|
||||
.vscode
|
||||
.vagrant
|
||||
.windsurfrules
|
||||
.phpunit.result.cache
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
|
||||
321
CHANGELOG.md
321
CHANGELOG.md
@@ -2,6 +2,327 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.44.91]
|
||||
|
||||
### Features
|
||||
|
||||
- 添加我的机器人管理
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化初始化逻辑
|
||||
- 优化docker配置
|
||||
|
||||
## [0.44.82]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复搜索结果显示即将到期
|
||||
|
||||
### Features
|
||||
|
||||
- 新增独立窗口打开会话
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化AI支持文件类型
|
||||
|
||||
## [0.44.74]
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化AI解析文件
|
||||
- 优化 WebSocket 消息
|
||||
- 优化数据
|
||||
|
||||
## [0.44.67]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复查看待办图片不符的情况
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化数据
|
||||
- 优化未读消息数
|
||||
- 优化搜索组件
|
||||
- 已归档/已删除任务列表支持按状态检索
|
||||
- 优化消息流效果
|
||||
- 优化AI上下文
|
||||
- 优化工作流获取
|
||||
- 优化转发功能
|
||||
|
||||
## [0.44.53]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 工作流存在已离职人员
|
||||
|
||||
### Features
|
||||
|
||||
- 可点击标注图标查看标注人员
|
||||
- 支持分享工作报告到消息
|
||||
- 支持AI分析工作报告
|
||||
- 支持使用%发送工作报告
|
||||
- 新增自定义撤回及修改消息时限
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化转发消息
|
||||
- 优化工作报告列表
|
||||
- 优化引用消息
|
||||
- 优化全局提示
|
||||
- 优化草稿消息
|
||||
|
||||
## [0.44.19]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 看不到未读消息定位提醒
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化消息定位
|
||||
- 优化消息性能
|
||||
|
||||
## [0.44.15]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 会话内消息搜索布局错位
|
||||
- 流程设置翻译不统一
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化消息定位
|
||||
- 优化MD消息
|
||||
|
||||
## [0.44.3]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 定位签到失败的问题
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化发送语音效果
|
||||
- 录音转文字支持自定义语言
|
||||
- 优化ES模块
|
||||
- 优化emoji表情
|
||||
- 按住Ctrl/Command键可连续选择表情
|
||||
- Md消息支持html代码
|
||||
- 优化脚本
|
||||
- 优化安装命令
|
||||
- 优化ES索引名称
|
||||
|
||||
## [0.43.73]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 全屏预览图片关闭窗口
|
||||
- 点击排序导致任务不显示的情况
|
||||
|
||||
### Performance
|
||||
|
||||
- 新增录音转文字
|
||||
- 优化数据排序
|
||||
|
||||
## [0.43.49]
|
||||
|
||||
### Performance
|
||||
|
||||
- 添加全局搜索功能
|
||||
- 优化消息搜索
|
||||
- 团队管理支持调整部门区域尺寸
|
||||
- 任务详情支持调整聊天区域尺寸
|
||||
- 优化团队部门支持3级部门
|
||||
- 可见群组ID
|
||||
- 支持在团队管理打开群聊
|
||||
- 优化回复消息自动@逻辑
|
||||
- 转发预览隐藏表情回应部分
|
||||
- 优化任务日志
|
||||
- 已删除任务支持按标签搜索
|
||||
- 归档任务支持按标签搜索
|
||||
- 项目面板添加按标签筛选
|
||||
- 优化 AI 提示词
|
||||
- 优化 AI 设置
|
||||
|
||||
## [0.43.18]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 首次跟ai聊天没有记录的问题
|
||||
|
||||
### Performance
|
||||
|
||||
- 工作报告支持查看仅未读
|
||||
- AI 支持引用文件
|
||||
- 优化图文消息
|
||||
- 优化文本信息复制
|
||||
- 优化样式
|
||||
- 无法再AI机器人页面看到模型的问题
|
||||
|
||||
## [0.43.7]
|
||||
|
||||
### Features
|
||||
|
||||
- 添加 Grok AI、Ollama AI
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化MD消息过长处理
|
||||
- 优化AI支持分析指定文件
|
||||
- 支持在AI对话中直接引用任务提问
|
||||
- 优化 AI 参数
|
||||
- 优化 Ollama AI
|
||||
- 优化设置
|
||||
- 优化AI设置
|
||||
- 优化AI消息
|
||||
|
||||
## [0.42.85]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 撤回消息是消息列表不更新的情况
|
||||
|
||||
### Performance
|
||||
|
||||
- 表情回复时更新对话列表
|
||||
- Onlyoffice 支持打开超过100m的文件
|
||||
- 优化点击上传列表效果
|
||||
- AI支持自定义模型列表
|
||||
|
||||
## [0.42.79]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复偶现的是子窗口出现身份丢失的情况
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化查看长消息内容
|
||||
|
||||
## [0.42.74]
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化审批功能
|
||||
- AI机器人支持多会话
|
||||
- AI机器人支持自定义模型
|
||||
|
||||
## [0.42.61]
|
||||
|
||||
### Performance
|
||||
|
||||
- 支持下载聊天引用的文件
|
||||
- 优化翻译消息
|
||||
- 支持显示思考过程
|
||||
|
||||
## [0.42.57]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 跨地区发消息出现消息过期的情况
|
||||
- 多线程下载文件损坏的问题
|
||||
- 修复新建周报或日报唯一标识重复
|
||||
|
||||
### Features
|
||||
|
||||
- 添加 DeepSeek AI
|
||||
- 添加https证书自动更新
|
||||
|
||||
### Performance
|
||||
|
||||
- 支持自定义仪表盘欢迎词
|
||||
- ChatGPT 支持自定义 Base URL
|
||||
- 优化仪表盘任务更新规则
|
||||
|
||||
## [0.42.37]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 部分电脑无法复制的问题
|
||||
- 修复任务可见性 - 任务重覆获取, 子任务负责人看不到任务问题
|
||||
|
||||
### Performance
|
||||
|
||||
- 更新小海豚表情包
|
||||
- 优化任务时间冲突提示
|
||||
- 优化消息
|
||||
- 群聊总人数排除机器人
|
||||
|
||||
## [0.42.26]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 移交账号后工作流的负责人没有更新
|
||||
- 全屏预览时深色皮肤反色的情况
|
||||
|
||||
### Features
|
||||
|
||||
- 替换网页的资源为本地资源
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化任务面板
|
||||
- 优化子任务的可见性
|
||||
- 优化客户端
|
||||
- 优化会议
|
||||
- 优化会员搜索
|
||||
- 优化打开会话
|
||||
- 优化项目面板任务加载
|
||||
- 优化客户端加载
|
||||
|
||||
## [0.42.3]
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化对话阅读状况
|
||||
- 优化表情回复
|
||||
|
||||
## [0.42.0]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 桌面端查看表情图片缩略图显示错误
|
||||
- 项目面板任务不显示的情况
|
||||
- 修复移动任务子任务不跟随的情况
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化桌面端数据处理
|
||||
- 优化资源
|
||||
- 优化数据流
|
||||
|
||||
## [0.41.93]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 复制文件权限判断
|
||||
|
||||
### Performance
|
||||
|
||||
- AI创建任务确认
|
||||
- 优化项目面板
|
||||
|
||||
## [0.41.84]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- @在线状态不正确
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化子任务上下文
|
||||
- 优化子任务时间调整
|
||||
- 优化超长文本信息
|
||||
- 记录版本信息
|
||||
- 支持更多办公文件格式
|
||||
- 请假或外出时取消打卡提醒
|
||||
- 图片容错处理
|
||||
- 优化全局监听事件
|
||||
- 优化数据流消息
|
||||
|
||||
## [0.41.64]
|
||||
|
||||
### 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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,8 +12,12 @@ use App\Models\File;
|
||||
use App\Models\User;
|
||||
use App\Module\Base;
|
||||
use App\Module\Timer;
|
||||
use App\Models\Setting;
|
||||
use App\Module\Extranet;
|
||||
use App\Module\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 +27,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,28 +173,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)
|
||||
->where('m.bot', 0)
|
||||
->whereNull('d.deleted_at')
|
||||
->whereRaw("MATCH({$prefix}m.key) AGAINST('{$against}' IN BOOLEAN MODE)")
|
||||
->orderByDesc('m.id')
|
||||
->take(20 - count($list))
|
||||
->get()
|
||||
->map(function($item) use ($user) {
|
||||
return WebSocketDialog::synthesizeData($item, $user->userid);
|
||||
})
|
||||
->all();
|
||||
$list = array_merge($list, $msgs);
|
||||
}
|
||||
//
|
||||
return Base::retSuccess('success', $list);
|
||||
@@ -256,6 +248,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));
|
||||
}
|
||||
|
||||
@@ -288,6 +283,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')
|
||||
@@ -531,6 +529,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);
|
||||
@@ -712,7 +713,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
|
||||
@@ -741,7 +779,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
|
||||
@@ -774,7 +812,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
|
||||
@@ -845,7 +883,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
|
||||
@@ -888,7 +926,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
|
||||
@@ -954,7 +992,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
|
||||
@@ -998,7 +1036,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
|
||||
@@ -1023,6 +1061,7 @@ class DialogController extends AbstractController
|
||||
* @apiParam {String} [silence] 是否静默发送
|
||||
* - no: 正常发送(默认)
|
||||
* - yes: 静默发送
|
||||
* @apiParam {String} [model_name] 模型名称(仅AI机器人支持)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -1043,6 +1082,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 = [];
|
||||
@@ -1053,6 +1093,9 @@ class DialogController extends AbstractController
|
||||
//
|
||||
if ($update_id > 0) {
|
||||
$action = $update_mark ? "update-$update_id" : "change-$update_id";
|
||||
if (!($user->bot || $user->isAdmin())) {
|
||||
Setting::validateMsgLimit('edit', $update_id);
|
||||
}
|
||||
} elseif ($reply_id > 0) {
|
||||
$action = "reply-$reply_id";
|
||||
if ($reply_check === 'yes') {
|
||||
@@ -1091,27 +1134,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);
|
||||
}
|
||||
}
|
||||
@@ -1119,7 +1181,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
|
||||
@@ -1172,7 +1234,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
|
||||
@@ -1241,7 +1303,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
|
||||
@@ -1289,7 +1351,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
|
||||
@@ -1321,7 +1455,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
|
||||
@@ -1377,7 +1511,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
|
||||
@@ -1387,6 +1521,7 @@ class DialogController extends AbstractController
|
||||
* @apiParam {Number} file_id 消息ID
|
||||
* @apiParam {Array} dialogids 转发给的对话ID
|
||||
* @apiParam {Array} userids 转发给的成员ID
|
||||
* @apiParam {String} leave_message 转发留言
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -1399,55 +1534,24 @@ class DialogController extends AbstractController
|
||||
$file_id = intval(Request::input("file_id"));
|
||||
$dialogids = Request::input('dialogids');
|
||||
$userids = Request::input('userids');
|
||||
$leave_message = Request::input('leave_message');
|
||||
//
|
||||
if (empty($dialogids) && empty($userids)) {
|
||||
return Base::retError("请选择转发对话或成员");
|
||||
return Base::retError("请选择对话或成员");
|
||||
}
|
||||
//
|
||||
$file = File::permissionFind($file_id, $user);
|
||||
$fileLink = $file->getShareLink($user->userid);
|
||||
$fileMsg = "<a class=\"mention file\" href=\"{{RemoteURL}}single/file/{$fileLink['code']}\" target=\"_blank\">~{$file->getNameAndExt()}</a>";
|
||||
$fileMsg = "<p><a class=\"mention file\" href=\"{{RemoteURL}}single/file/{$fileLink['code']}\" target=\"_blank\">~{$file->getNameAndExt()}</a></p>";
|
||||
if ($leave_message) {
|
||||
$fileMsg .= "<p>{$leave_message}</p>";
|
||||
}
|
||||
//
|
||||
return AbstractModel::transaction(function() use ($user, $fileMsg, $userids, $dialogids) {
|
||||
$msgs = [];
|
||||
$already = [];
|
||||
if ($dialogids) {
|
||||
if (!is_array($dialogids)) {
|
||||
$dialogids = [$dialogids];
|
||||
}
|
||||
foreach ($dialogids as $dialogid) {
|
||||
$res = WebSocketDialogMsg::sendMsg(null, $dialogid, 'text', ['text' => $fileMsg], $user->userid);
|
||||
if (Base::isSuccess($res)) {
|
||||
$msgs[] = $res['data'];
|
||||
$already[] = $dialogid;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($userids) {
|
||||
if (!is_array($userids)) {
|
||||
$userids = [$userids];
|
||||
}
|
||||
foreach ($userids as $userid) {
|
||||
if (!User::whereUserid($userid)->exists()) {
|
||||
continue;
|
||||
}
|
||||
$dialog = WebSocketDialog::checkUserDialog($user, $userid);
|
||||
if ($dialog && !in_array($dialog->id, $already)) {
|
||||
$res = WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $fileMsg], $user->userid);
|
||||
if (Base::isSuccess($res)) {
|
||||
$msgs[] = $res['data'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Base::retSuccess('发送成功', [
|
||||
'msgs' => $msgs
|
||||
]);
|
||||
});
|
||||
return WebSocketDialogMsg::sendMsgBatch($user, $userids, $dialogids, $fileMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/dialog/msg/sendanon 28. 发送匿名消息
|
||||
* @api {post} api/dialog/msg/sendanon 30. 发送匿名消息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1503,7 +1607,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
|
||||
@@ -1563,7 +1667,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
|
||||
@@ -1592,7 +1696,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
|
||||
@@ -1634,13 +1738,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
|
||||
@@ -1677,11 +1793,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,12 +1818,15 @@ class DialogController extends AbstractController
|
||||
if (empty($msg)) {
|
||||
return Base::retError("消息不存在或已被删除");
|
||||
}
|
||||
if (!($user->bot || $user->isAdmin())) {
|
||||
Setting::validateMsgLimit('rev', $msg);
|
||||
}
|
||||
$msg->withdrawMsg();
|
||||
return Base::retSuccess("success");
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/voice2text 34. 语音消息转文字
|
||||
* @api {get} api/dialog/msg/voice2text 36. 语音消息转文字
|
||||
*
|
||||
* @apiDescription 将语音消息转文字,需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1759,7 +1878,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
|
||||
@@ -1767,6 +1886,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错误)
|
||||
@@ -1778,6 +1899,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);
|
||||
//
|
||||
@@ -1795,13 +1917,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;
|
||||
@@ -1818,7 +1947,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
|
||||
@@ -1882,7 +2011,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
|
||||
@@ -1945,7 +2074,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
|
||||
@@ -1956,7 +2085,7 @@ class DialogController extends AbstractController
|
||||
* @apiParam {Array} dialogids 转发给的对话ID
|
||||
* @apiParam {Array} userids 转发给的成员ID
|
||||
* @apiParam {Number} show_source 是否显示原发送者信息
|
||||
* @apiParam {Array} leave_message 转发留言
|
||||
* @apiParam {String} leave_message 转发留言
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -1973,7 +2102,7 @@ class DialogController extends AbstractController
|
||||
$leave_message = Request::input('leave_message');
|
||||
//
|
||||
if (empty($dialogids) && empty($userids)) {
|
||||
return Base::retError("请选择转发对话或成员");
|
||||
return Base::retError("请选择对话或成员");
|
||||
}
|
||||
//
|
||||
$msg = WebSocketDialogMsg::whereId($msg_id)->first();
|
||||
@@ -1986,7 +2115,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
|
||||
@@ -2021,7 +2150,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
|
||||
@@ -2050,7 +2179,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
|
||||
@@ -2093,7 +2222,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
|
||||
@@ -2123,7 +2252,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
|
||||
@@ -2176,7 +2305,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
|
||||
@@ -2217,7 +2346,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
|
||||
@@ -2279,7 +2408,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
|
||||
@@ -2341,7 +2470,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/group/adduser 47. 添加群成员
|
||||
* @api {get} api/dialog/group/adduser 49. 添加群成员
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* - 有群主时:只有群主可以邀请
|
||||
@@ -2377,7 +2506,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/group/deluser 48. 移出(退出)群成员
|
||||
* @api {get} api/dialog/group/deluser 50. 移出(退出)群成员
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* - 只有群主、邀请人可以踢人
|
||||
@@ -2421,7 +2550,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/group/transfer 49. 转让群组
|
||||
* @api {get} api/dialog/group/transfer 51. 转让群组
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* - 只有群主且是个人类型群可以解散
|
||||
@@ -2470,7 +2599,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/group/disband 50. 解散群组
|
||||
* @api {get} api/dialog/group/disband 52. 解散群组
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* - 只有群主且是个人类型群可以解散
|
||||
@@ -2498,7 +2627,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
|
||||
@@ -2527,7 +2656,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
|
||||
@@ -2566,7 +2695,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
|
||||
@@ -2602,7 +2731,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
|
||||
@@ -2688,7 +2817,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
|
||||
@@ -2804,7 +2933,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
|
||||
@@ -2864,7 +2993,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
|
||||
@@ -2891,7 +3020,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
|
||||
@@ -2940,7 +3069,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
|
||||
@@ -2964,7 +3093,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/config 60. 获取会话配置
|
||||
* @api {get} api/dialog/config 62. 获取会话配置
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -3000,7 +3129,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
|
||||
@@ -3038,10 +3167,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ class FileController extends AbstractController
|
||||
}
|
||||
return Base::retError($msg, $data);
|
||||
}
|
||||
$fileLink->increment("num");
|
||||
} else {
|
||||
return Base::retError('参数错误');
|
||||
}
|
||||
@@ -106,6 +107,7 @@ class FileController extends AbstractController
|
||||
*
|
||||
* @apiParam {String} [link] 通过分享地址搜索(如:https://t.hitosea.com/single/file/ODcwOCwzOSxpa0JBS2lmVQ==)
|
||||
* @apiParam {String} [key] 关键词
|
||||
* @apiParam {Number} [take] 获取数量(默认:50,最大:100)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -118,7 +120,7 @@ class FileController extends AbstractController
|
||||
$link = trim(Request::input('link'));
|
||||
$key = trim(Request::input('key'));
|
||||
$id = 0;
|
||||
$take = 50;
|
||||
$take = Base::getPaginate(100, 50, 'take');
|
||||
if (preg_match("/\/single\/file\/(.*?)$/i", $link, $match)) {
|
||||
$id = intval(FileLink::whereCode($match[1])->value('file_id'));
|
||||
$take = 1;
|
||||
@@ -301,6 +303,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'));
|
||||
}
|
||||
//
|
||||
@@ -663,7 +666,7 @@ class FileController extends AbstractController
|
||||
//
|
||||
if ($status === 2) {
|
||||
$parse = parse_url($url);
|
||||
$from = 'http://' . env('APP_IPPR') . '.3' . $parse['path'] . '?' . $parse['query'];
|
||||
$from = 'http://nginx' . $parse['path'] . '?' . $parse['query'];
|
||||
$path = 'uploads/file/' . $file->type . '/' . date("Ym") . '/' . $file->id . '/' . $key;
|
||||
$save = public_path($path);
|
||||
Base::makeDir(dirname($save));
|
||||
|
||||
@@ -941,7 +941,9 @@ class ProjectController extends AbstractController
|
||||
* @apiName task__lists
|
||||
*
|
||||
* @apiParam {Object} [keys] 搜索条件
|
||||
* - keys.name: ID、任务名称
|
||||
* - keys.name: ID、任务名称、任务描述
|
||||
* - keys.tag: 标签名称
|
||||
* - keys.status: 任务状态 (completed: 已完成、uncompleted: 未完成、flow-xx: 流程状态ID)
|
||||
*
|
||||
* @apiParam {Number} [project_id] 项目ID
|
||||
* @apiParam {Number} [parent_id] 主任务ID(project_id && parent_id ≤ 0 时 仅查询自己参与的任务)
|
||||
@@ -994,7 +996,29 @@ class ProjectController extends AbstractController
|
||||
if (Base::isNumber($keys['name'])) {
|
||||
$builder->where("project_tasks.id", intval($keys['name']));
|
||||
} else {
|
||||
$builder->where("project_tasks.name", "like", "%{$keys['name']}%");
|
||||
$builder->where(function ($query) use ($keys) {
|
||||
$query->where("project_tasks.name", "like", "%{$keys['name']}%");
|
||||
$query->orWhere("project_tasks.desc", "like", "%{$keys['name']}%");
|
||||
});
|
||||
}
|
||||
}
|
||||
if ($keys['tag']) {
|
||||
$builder->whereHas('taskTag', function ($query) use ($keys) {
|
||||
$query->where('project_task_tags.name', $keys['tag']);
|
||||
});
|
||||
}
|
||||
if ($keys['status']) {
|
||||
if ($keys['status'] == 'completed') {
|
||||
$builder->whereNotNull('project_tasks.complete_at');
|
||||
} elseif ($keys['status'] == 'uncompleted') {
|
||||
$builder->whereNull('project_tasks.complete_at');
|
||||
} elseif (str_starts_with($keys['status'], 'flow-')) {
|
||||
$flow = str_replace('flow-', '', $keys['status']);
|
||||
if (Base::isNumber($flow)) {
|
||||
$builder->where('project_tasks.flow_item_id', intval($flow));
|
||||
} elseif ($flow) {
|
||||
$builder->where('project_tasks.flow_item_name', 'like', "%{$flow}%");
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
@@ -1058,20 +1082,20 @@ class ProjectController extends AbstractController
|
||||
$query->where('project_users.owner', 1);
|
||||
$query->where('project_users.userid', $userid);
|
||||
});
|
||||
$builder->leftJoin('project_task_users as project_sub_task_users', function ($query) use($userid) {
|
||||
$query->on('project_sub_task_users.task_pid', '=', 'project_tasks.parent_id');
|
||||
$query->where('project_sub_task_users.userid', $userid);
|
||||
});
|
||||
$builder->leftJoin('project_task_visibility_users', function ($query) use($userid) {
|
||||
$query->on('project_task_visibility_users.task_id', '=', 'project_tasks.id');
|
||||
$query->where('project_task_visibility_users.userid', $userid);
|
||||
});
|
||||
$builder->leftJoin('project_task_visibility_users as project_sub_task_visibility_users', function ($query) use($userid) {
|
||||
$query->on('project_sub_task_visibility_users.task_id', '=', 'project_tasks.parent_id');
|
||||
$query->where('project_sub_task_visibility_users.userid', $userid);
|
||||
});
|
||||
$builder->where(function ($query) use ($userid) {
|
||||
$query->where("project_tasks.visibility", 1);
|
||||
$query->orWhere("project_users.userid", $userid);
|
||||
$query->orWhere("project_task_users.userid", $userid);
|
||||
$query->orWhere("project_task_visibility_users.userid", $userid);
|
||||
$query->orWhere("project_sub_task_users.userid", $userid);
|
||||
$query->orWhere("project_sub_task_visibility_users.userid", $userid);
|
||||
});
|
||||
// 优化子查询汇总
|
||||
$builder->leftJoinSub(function ($query) {
|
||||
@@ -1159,6 +1183,7 @@ class ProjectController extends AbstractController
|
||||
$list = ProjectTask::with(['taskUser'])
|
||||
->select([
|
||||
'projects.name as project_name',
|
||||
'project_tasks.project_id',
|
||||
'project_tasks.id',
|
||||
'project_tasks.name',
|
||||
'project_tasks.start_at',
|
||||
@@ -1852,7 +1877,7 @@ class ProjectController extends AbstractController
|
||||
}
|
||||
//
|
||||
$filePath = public_path($file->getRawOriginal('path'));
|
||||
return Base::BinaryFileResponse($filePath, $file->name);
|
||||
return Base::DownloadFileResponse($filePath, $file->name);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2297,8 +2322,8 @@ class ProjectController extends AbstractController
|
||||
* @apiGroup project
|
||||
* @apiName task__flow
|
||||
*
|
||||
* @apiParam {Number} task_id 任务ID
|
||||
* @apiParam {Number} project_id 项目ID - 存在时只返回这个项目的
|
||||
* @apiParam {Number} [task_id] 任务ID
|
||||
* @apiParam {Number} [project_id] 项目ID(存在时只返回这个项目的工作流,主要用于任务移动到其他项目时)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -2399,6 +2424,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 +2462,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 +2610,7 @@ class ProjectController extends AbstractController
|
||||
$builder->with(['projectTask:id,parent_id,name'])->whereProjectId($project->id)->whereTaskOnly(0);
|
||||
}
|
||||
//
|
||||
$list = $builder->orderByDesc('created_at')->paginate(Base::getPaginate(100, 20));
|
||||
$list = $builder->orderByDesc('created_at')->orderByDesc('id')->paginate(Base::getPaginate(100, 20));
|
||||
$list->transform(function (ProjectLog $log) use ($task_id) {
|
||||
$timestamp = Carbon::parse($log->created_at)->timestamp;
|
||||
if ($task_id === 0) {
|
||||
|
||||
@@ -6,8 +6,10 @@ use App\Exceptions\ApiException;
|
||||
use App\Models\AbstractModel;
|
||||
use App\Models\ProjectTask;
|
||||
use App\Models\Report;
|
||||
use App\Models\ReportLink;
|
||||
use App\Models\ReportReceive;
|
||||
use App\Models\User;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
use App\Tasks\PushTask;
|
||||
@@ -28,11 +30,13 @@ class ReportController extends AbstractController
|
||||
/**
|
||||
* @api {get} api/report/my 01. 我发送的汇报
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName my
|
||||
*
|
||||
* @apiParam {Object} [keys] 搜索条件
|
||||
* - keys.key: 关键词
|
||||
* - keys.type: 汇报类型,weekly:周报,daily:日报
|
||||
* - keys.created_at: 汇报时间
|
||||
* @apiParam {Number} [page] 当前页,默认:1
|
||||
@@ -49,6 +53,15 @@ class ReportController extends AbstractController
|
||||
$builder = Report::with(['receivesUser'])->whereUserid($user->userid);
|
||||
$keys = Request::input('keys');
|
||||
if (is_array($keys)) {
|
||||
if ($keys['key']) {
|
||||
if (str_contains($keys['key'], '@')) {
|
||||
$builder->whereHas('sendUser', function ($q2) use ($keys) {
|
||||
$q2->where("users.email", "LIKE", "%{$keys['key']}%");
|
||||
});
|
||||
} else {
|
||||
$builder->where("title", "LIKE", "%{$keys['key']}%");
|
||||
}
|
||||
}
|
||||
if (in_array($keys['type'], [Report::WEEKLY, Report::DAILY])) {
|
||||
$builder->whereType($keys['type']);
|
||||
}
|
||||
@@ -64,13 +77,16 @@ class ReportController extends AbstractController
|
||||
/**
|
||||
* @api {get} api/report/receive 02. 我接收的汇报
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName receive
|
||||
*
|
||||
* @apiParam {Object} [keys] 搜索条件
|
||||
* - keys.key: 关键词
|
||||
* - keys.department_id: 部门ID
|
||||
* - keys.type: 汇报类型,weekly:周报,daily:日报
|
||||
* - keys.status: 状态,unread:未读,read:已读
|
||||
* - keys.created_at: 汇报时间
|
||||
* @apiParam {Number} [page] 当前页,默认:1
|
||||
* @apiParam {Number} [pagesize] 每页显示数量,默认:20,最大:50
|
||||
@@ -89,15 +105,29 @@ class ReportController extends AbstractController
|
||||
$keys = Request::input('keys');
|
||||
if (is_array($keys)) {
|
||||
if ($keys['key']) {
|
||||
$builder->where(function($query) use ($keys) {
|
||||
$query->whereHas('sendUser', function ($q2) use ($keys) {
|
||||
if (str_contains($keys['key'], '@')) {
|
||||
$builder->whereHas('sendUser', function ($q2) use ($keys) {
|
||||
$q2->where("users.email", "LIKE", "%{$keys['key']}%");
|
||||
})->orWhere("title", "LIKE", "%{$keys['key']}%");
|
||||
});
|
||||
} elseif (Base::isNumber($keys['key'])) {
|
||||
$builder->where("userid", intval($keys['key']));
|
||||
} else {
|
||||
$builder->where("title", "LIKE", "%{$keys['key']}%");
|
||||
}
|
||||
}
|
||||
if ($keys['department_id']) {
|
||||
$builder->whereHas('sendUser', function ($query) use ($keys) {
|
||||
$query->where("users.department", "LIKE", "%,{$keys['department_id']},%");
|
||||
});
|
||||
}
|
||||
if (in_array($keys['type'], [Report::WEEKLY, Report::DAILY])) {
|
||||
$builder->whereType($keys['type']);
|
||||
}
|
||||
if (in_array($keys['status'], ['unread', 'read'])) {
|
||||
$builder->whereHas("receivesUser", function ($query) use ($user, $keys) {
|
||||
$query->where("report_receives.userid", $user->userid)->where("report_receives.read", $keys['status'] === 'unread' ? 0 : 1);
|
||||
});
|
||||
}
|
||||
if (is_array($keys['created_at'])) {
|
||||
if ($keys['created_at'][0] > 0) $builder->where('created_at', '>=', Base::newCarbon($keys['created_at'][0])->startOfDay());
|
||||
if ($keys['created_at'][1] > 0) $builder->where('created_at', '<=', Base::newCarbon($keys['created_at'][1])->endOfDay());
|
||||
@@ -115,6 +145,7 @@ class ReportController extends AbstractController
|
||||
/**
|
||||
* @api {get} api/report/store 03. 保存并发送工作汇报
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName store
|
||||
@@ -240,6 +271,7 @@ class ReportController extends AbstractController
|
||||
/**
|
||||
* @api {get} api/report/template 04. 生成汇报模板
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName template
|
||||
@@ -411,11 +443,13 @@ class ReportController extends AbstractController
|
||||
/**
|
||||
* @api {get} api/report/detail 05. 报告详情
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName detail
|
||||
*
|
||||
* @apiParam {Number} [id] 报告id
|
||||
* @apiParam {Number} [id] 报告ID
|
||||
* @apiParam {String} [code] 报告分享代码,与ID二选一,优先ID
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -424,30 +458,43 @@ class ReportController extends AbstractController
|
||||
public function detail(): array
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$id = intval(trim(Request::input("id")));
|
||||
if (empty($id))
|
||||
$code = trim(Request::input("code"));
|
||||
//
|
||||
if (empty($id) && empty($code)) {
|
||||
return Base::retError("缺少ID参数");
|
||||
|
||||
$one = Report::getOne($id);
|
||||
$one->type_val = $one->getRawOriginal("type");
|
||||
|
||||
// 标记为已读
|
||||
if (!empty($one->receivesUser)) {
|
||||
foreach ($one->receivesUser as $item) {
|
||||
if ($item->userid === $user->userid && $item->pivot->read === 0) {
|
||||
$one->receivesUser()->updateExistingPivot($user->userid, [
|
||||
"read" => 1,
|
||||
]);
|
||||
}
|
||||
//
|
||||
if (!empty($id)) {
|
||||
$one = Report::getOne($id);
|
||||
$one->type_val = $one->getRawOriginal("type");
|
||||
// 标记为已读
|
||||
if (!empty($one->receivesUser)) {
|
||||
foreach ($one->receivesUser as $item) {
|
||||
if ($item->userid === $user->userid && $item->pivot->read === 0) {
|
||||
$one->receivesUser()->updateExistingPivot($user->userid, [
|
||||
"read" => 1,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$link = ReportLink::whereCode($code)->first();
|
||||
if (empty($link)) {
|
||||
return Base::retError("报告不存在或已被删除");
|
||||
}
|
||||
$one = Report::getOne($link->rid);
|
||||
$one->report_link = $link;
|
||||
$link->increment("num");
|
||||
}
|
||||
|
||||
return Base::retSuccess("success", $one);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/mark 06. 标记已读/未读
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName mark
|
||||
@@ -488,8 +535,70 @@ class ReportController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/last_submitter 07. 获取最后一次提交的接收人
|
||||
* @api {get} api/report/share 07. 分享报告到消息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName share
|
||||
*
|
||||
* @apiParam {Number} id 报告id(组)
|
||||
* @apiParam {Array} dialogids 转发给的对话ID
|
||||
* @apiParam {Array} userids 转发给的成员ID
|
||||
* @apiParam {String} leave_message 转发留言
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function share()
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$id = Request::input('id');
|
||||
$dialogids = Request::input('dialogids');
|
||||
$userids = Request::input('userids');
|
||||
$leave_message = Request::input('leave_message');
|
||||
//
|
||||
if (is_array($id)) {
|
||||
if (count(Base::arrayRetainInt($id)) > 20) {
|
||||
return Base::retError("最多只能操作20条数据");
|
||||
}
|
||||
$builder = Report::whereIn("id", Base::arrayRetainInt($id));
|
||||
} else {
|
||||
$builder = Report::whereId(intval($id));
|
||||
}
|
||||
$reportMsgs = [];
|
||||
$builder ->chunkById(100, function ($list) use (&$reportMsgs, $user) {
|
||||
/** @var Report $item */
|
||||
foreach ($list as $item) {
|
||||
$reportLink = ReportLink::generateLink($item->id, $user->userid);
|
||||
$reportMsgs[] = "<a class=\"mention report\" href=\"{{RemoteURL}}single/report/detail/{$reportLink['code']}\" target=\"_blank\">%{$item->title}</a>";
|
||||
}
|
||||
});
|
||||
if (empty($reportMsgs)) {
|
||||
return Base::retError("报告不存在或已被删除");
|
||||
}
|
||||
$reportTag = count($reportMsgs) > 1 ? 'li' : 'p';
|
||||
$reportMsgs = array_map(function ($item) use ($reportTag) {
|
||||
return "<{$reportTag}>{$item}</{$reportTag}>";
|
||||
}, $reportMsgs);
|
||||
if ($reportTag === 'li') {
|
||||
array_unshift($reportMsgs, "<ol>");
|
||||
$reportMsgs[] = "</ol>";
|
||||
}
|
||||
if ($leave_message) {
|
||||
$reportMsgs[] = "<p>{$leave_message}</p>";
|
||||
}
|
||||
$msgText = implode("", $reportMsgs);
|
||||
//
|
||||
return WebSocketDialogMsg::sendMsgBatch($user, $userids, $dialogids, $msgText);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/last_submitter 08. 获取最后一次提交的接收人
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName last_submitter
|
||||
@@ -505,8 +614,9 @@ class ReportController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/unread 08. 获取未读
|
||||
* @api {get} api/report/unread 09. 获取未读
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName unread
|
||||
@@ -529,8 +639,9 @@ class ReportController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/read 09. 标记汇报已读,可批量
|
||||
* @api {get} api/report/read 10. 标记汇报已读,可批量
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName read
|
||||
|
||||
@@ -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 返回信息(错误描述)
|
||||
@@ -71,6 +71,8 @@ class SystemController extends AbstractController
|
||||
'voice2text',
|
||||
'translation',
|
||||
'e2e_message',
|
||||
'msg_rev_limit',
|
||||
'msg_edit_limit',
|
||||
'auto_archived',
|
||||
'archived_day',
|
||||
'task_visible',
|
||||
@@ -80,6 +82,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 +111,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');
|
||||
@@ -131,6 +137,8 @@ class SystemController extends AbstractController
|
||||
$setting['voice2text'] = $setting['voice2text'] ?: 'close';
|
||||
$setting['translation'] = $setting['translation'] ?: 'close';
|
||||
$setting['e2e_message'] = $setting['e2e_message'] ?: 'close';
|
||||
$setting['msg_rev_limit'] = $setting['msg_rev_limit'] ?: '';
|
||||
$setting['msg_edit_limit'] = $setting['msg_edit_limit'] ?: '';
|
||||
$setting['auto_archived'] = $setting['auto_archived'] ?: 'close';
|
||||
$setting['archived_day'] = floatval($setting['archived_day']) ?: 7;
|
||||
$setting['task_visible'] = $setting['task_visible'] ?: 'close';
|
||||
@@ -283,6 +291,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 +302,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 +317,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 +338,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 +503,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 +548,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 +618,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 +658,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 +682,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/system/priority 10. 任务优先级
|
||||
* @api {post} api/system/priority 12. 任务优先级
|
||||
*
|
||||
* @apiDescription 获取任务优先级、保存任务优先级
|
||||
* @apiVersion 1.0.0
|
||||
@@ -653,7 +731,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 +778,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 +847,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 +876,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 +891,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 +908,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 +925,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 +942,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 +1008,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 +1105,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 +1149,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/updatelog 21. 获取更新日志
|
||||
* @api {get} api/system/get/updatelog 23. 获取更新日志
|
||||
*
|
||||
* @apiDescription 获取更新日志
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1104,7 +1182,7 @@ class SystemController extends AbstractController
|
||||
if ($logResults) {
|
||||
$logVersion = $logResults[0]['title'];
|
||||
$logContent = implode("\n", array_map(function($item) {
|
||||
return "## [{$item['title']}]" . $item['content'];
|
||||
return "## {$item['title']}" . $item['content'];
|
||||
}, $logResults));
|
||||
}
|
||||
return Base::retSuccess('success', [
|
||||
@@ -1114,7 +1192,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 +1238,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 +1407,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 +1433,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 +1470,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/prefetch 26. 预加载的资源
|
||||
* @api {get} api/system/prefetch 28. 预加载的资源
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -1432,7 +1510,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
// 添加office资源
|
||||
$officePath = '';
|
||||
$officeApi = 'http://' . env('APP_IPPR') . '.6/web-apps/apps/api/documents/api.js';
|
||||
$officeApi = 'http://office/web-apps/apps/api/documents/api.js';
|
||||
$content = @file_get_contents($officeApi);
|
||||
if ($content) {
|
||||
if (preg_match("/const\s+ver\s*=\s*'\/*([^']+)'/", $content, $matches)) {
|
||||
|
||||
@@ -1185,12 +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'],
|
||||
'is_notified' => intval($data['isNotified']),
|
||||
'version' => $version,
|
||||
'is_notified' => $isNotified,
|
||||
'updated_at' => Carbon::now()
|
||||
]);
|
||||
return Base::retSuccess('别名已存在');
|
||||
@@ -1198,7 +1201,8 @@ class UsersController extends AbstractController
|
||||
$row = UmengAlias::createInstance(array_merge($inArray, [
|
||||
'ua' => $data['userAgent'],
|
||||
'device' => $data['deviceModel'],
|
||||
'is_notified' => intval($data['isNotified']),
|
||||
'version' => $version,
|
||||
'is_notified' => $isNotified,
|
||||
]));
|
||||
if ($row->save()) {
|
||||
return Base::retSuccess('添加成功');
|
||||
@@ -1649,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,
|
||||
@@ -1670,7 +1677,7 @@ class UsersController extends AbstractController
|
||||
], $dialog_useid);
|
||||
Cache::forever("UserDepartment::rand", Base::generatePassword());
|
||||
//
|
||||
return Base::retSuccess($parent_id > 0 ? '保存成功' : '新建成功');
|
||||
return Base::retSuccess($id > 0 ? '保存成功' : '新建成功');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1697,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());
|
||||
//
|
||||
@@ -1918,7 +1928,51 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/bot/info 32. 机器人信息
|
||||
* @api {get} api/users/bot/list 32. 机器人列表
|
||||
*
|
||||
* @apiDescription 需要token身份,获取我的机器人列表
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName bot__list
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function bot__list()
|
||||
{
|
||||
// 获取当前认证用户
|
||||
$user = User::auth();
|
||||
|
||||
// 使用连表查询一次性获取所有机器人数据
|
||||
$bots = User::join('user_bots', 'user_bots.bot_id', '=', 'users.userid')
|
||||
->where('user_bots.userid', $user->userid)
|
||||
->select([
|
||||
'users.userid',
|
||||
'users.nickname',
|
||||
'users.userimg',
|
||||
'user_bots.clear_day',
|
||||
'user_bots.webhook_url'
|
||||
])
|
||||
->orderByDesc('id')
|
||||
->get()
|
||||
->toArray();
|
||||
foreach ($bots as &$bot) {
|
||||
$bot['id'] = $bot['userid'];
|
||||
$bot['name'] = $bot['nickname'];
|
||||
$bot['avatar'] = $bot['userimg'];
|
||||
$bot['system_name'] = UserBot::systemBotName($bot['name']);
|
||||
unset($bot['userid'], $bot['nickname'], $bot['userimg']);
|
||||
}
|
||||
|
||||
// 返回成功响应,将机器人列表包装在list字段中
|
||||
return Base::retSuccess('success', [
|
||||
'list' => $bots
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/bot/info 33. 机器人信息
|
||||
*
|
||||
* @apiDescription 需要token身份,获取我的机器人信息
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1969,14 +2023,14 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/users/bot/edit 33. 编辑机器人
|
||||
* @api {post} api/users/bot/edit 34. 添加、编辑机器人
|
||||
*
|
||||
* @apiDescription 需要token身份,编辑 我的机器人 或 管理员修改系统机器人 信息
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName bot__edit
|
||||
*
|
||||
* @apiParam {Number} id 机器人ID
|
||||
* @apiParam {Number} [id] 机器人ID(编辑时必填,留空为添加)
|
||||
* @apiParam {String} [name] 机器人名称
|
||||
* @apiParam {String} [avatar] 机器人头像
|
||||
* @apiParam {Number} [clear_day] 清理天数(仅 我的机器人)
|
||||
@@ -1991,10 +2045,19 @@ class UsersController extends AbstractController
|
||||
$user = User::auth();
|
||||
//
|
||||
$botId = intval(Request::input('id'));
|
||||
$botUser = User::whereUserid($botId)->whereBot(1)->first();
|
||||
if (empty($botUser)) {
|
||||
return Base::retError('机器人不存在');
|
||||
if (empty($botId)) {
|
||||
$res = UserBot::newbot($user->userid, trim(Request::input('name')));
|
||||
if (Base::isError($res)) {
|
||||
return $res;
|
||||
}
|
||||
$botUser = $res['data'];
|
||||
} else {
|
||||
$botUser = User::whereUserid($botId)->whereBot(1)->first();
|
||||
if (empty($botUser)) {
|
||||
return Base::retError('机器人不存在');
|
||||
}
|
||||
}
|
||||
//
|
||||
$userBot = UserBot::whereBotId($botUser->userid)->whereUserid($user->userid)->first();
|
||||
if (empty($userBot)) {
|
||||
if (UserBot::systemBotName($botUser->email)) {
|
||||
@@ -2051,11 +2114,61 @@ class UsersController extends AbstractController
|
||||
$data['clear_day'] = $userBot->clear_day;
|
||||
$data['webhook_url'] = $userBot->webhook_url;
|
||||
}
|
||||
return Base::retSuccess('修改成功', $data);
|
||||
return Base::retSuccess($botId ? '修改成功' : '添加成功', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/share/list 34. 获取分享列表
|
||||
* @api {get} api/users/bot/delete 35. 删除机器人
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName bot__delete
|
||||
*
|
||||
* @apiParam {Number} id 机器人ID
|
||||
* @apiParam {String} remark 删除备注
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function bot__delete()
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$botId = intval(Request::input('id'));
|
||||
$remark = trim(Request::input('remark'));
|
||||
//
|
||||
if (empty($remark)) {
|
||||
return Base::retError('请输入删除备注');
|
||||
}
|
||||
if (mb_strlen($remark) > 255) {
|
||||
return Base::retError('删除备注长度限制255个字');
|
||||
}
|
||||
//
|
||||
$botUser = User::whereUserid($botId)->whereBot(1)->first();
|
||||
if (empty($botUser)) {
|
||||
return Base::retError('机器人不存在');
|
||||
}
|
||||
$userBot = UserBot::whereBotId($botUser->userid)->whereUserid($user->userid)->first();
|
||||
if (empty($userBot)) {
|
||||
if (UserBot::systemBotName($botUser->email)) {
|
||||
// 系统机器人(仅限管理员)
|
||||
return Base::retError('系统机器人不能删除');
|
||||
} else {
|
||||
// 其他用户的机器人(仅限主人)
|
||||
return Base::retError('不是你的机器人');
|
||||
}
|
||||
}
|
||||
//
|
||||
if (!$botUser->deleteUser($remark)) {
|
||||
return Base::retError('删除失败');
|
||||
}
|
||||
return Base::retSuccess('删除成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/share/list 36. 获取分享列表
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
@@ -2140,7 +2253,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/annual/report 35. 年度报告
|
||||
* @api {get} api/users/annual/report 37. 年度报告
|
||||
*
|
||||
* @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";
|
||||
}
|
||||
@@ -490,7 +493,7 @@ class IndexController extends InvokeController
|
||||
if (in_array($ext, File::localExt)) {
|
||||
$url = Base::fillUrl($path);
|
||||
} else {
|
||||
$url = 'http://' . env('APP_IPPR') . '.3/' . $path;
|
||||
$url = 'http://nginx/' . $path;
|
||||
}
|
||||
$url = Base::urlAddparameter($url, [
|
||||
'fullfilename' => Base::rightDelete($name, '.' . $ext) . '_' . filemtime($file) . '.' . $ext
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -415,6 +419,7 @@ class Project extends AbstractModel
|
||||
$hasStart = false;
|
||||
$hasEnd = false;
|
||||
$upTaskList = [];
|
||||
$projectUserids = $this->relationUserids();
|
||||
foreach ($flows as $item) {
|
||||
$id = intval($item['id']);
|
||||
$turns = Base::arrayRetainInt($item['turns'] ?: [], true);
|
||||
@@ -431,6 +436,12 @@ class Project extends AbstractModel
|
||||
if ($userlimit && empty($userids)) {
|
||||
throw new ApiException("状态[{$item['name']}]设置错误,设置限制负责人时必须填写状态负责人");
|
||||
}
|
||||
foreach ($userids as $userid) {
|
||||
if (!in_array($userid, $projectUserids)) {
|
||||
$nickname = User::userid2nickname($userid);
|
||||
throw new ApiException("状态[{$item['name']}]设置错误,状态负责人[{$nickname}]不在项目成员内");
|
||||
}
|
||||
}
|
||||
$flow = ProjectFlowItem::updateInsert([
|
||||
'id' => $id,
|
||||
'project_id' => $this->id,
|
||||
|
||||
@@ -12,6 +12,7 @@ use App\Tasks\PushTask;
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Observers\ProjectTaskObserver;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
use League\HTMLToMarkdown\HtmlConverter;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
@@ -617,7 +618,12 @@ class ProjectTask extends AbstractModel
|
||||
$data['complete_at'] = false;
|
||||
}
|
||||
}
|
||||
if ($newFlowItem->userids) {
|
||||
$flowUserids = $newFlowItem->userids;
|
||||
if ($flowUserids) {
|
||||
// 确认负责人在任务中
|
||||
$flowUserids = ProjectUser::whereProjectId($this->project_id)->whereIn('userid', $flowUserids)->pluck('userid')->toArray();
|
||||
}
|
||||
if ($flowUserids) {
|
||||
// 判断自动添加负责人
|
||||
$flowData['owner'] = $data['owner'] = $this->taskUser->where('owner', 1)->pluck('userid')->toArray();
|
||||
if (in_array($newFlowItem->usertype, ["replace", "merge"])) {
|
||||
@@ -626,14 +632,14 @@ class ProjectTask extends AbstractModel
|
||||
$flowData['assist'] = $data['assist'] = $this->taskUser->where('owner', 0)->pluck('userid')->toArray();
|
||||
$data['assist'] = array_merge($data['assist'], $data['owner']);
|
||||
}
|
||||
$data['owner'] = $newFlowItem->userids;
|
||||
$data['owner'] = $flowUserids;
|
||||
// 判断剔除模式:保留操作状态的人员
|
||||
if ($newFlowItem->usertype == "merge") {
|
||||
$data['owner'][] = User::userid();
|
||||
}
|
||||
} else {
|
||||
// 添加模式
|
||||
$data['owner'] = array_merge($data['owner'], $newFlowItem->userids);
|
||||
$data['owner'] = array_merge($data['owner'], $flowUserids);
|
||||
}
|
||||
$data['owner'] = array_values(array_unique($data['owner']));
|
||||
if (isset($data['assist'])) {
|
||||
@@ -770,6 +776,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 +842,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 +1199,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 +1366,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 +1866,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 +1898,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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Module\Base;
|
||||
use Carbon\Carbon;
|
||||
use Carbon\Traits\Creator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
@@ -95,6 +96,24 @@ class Report extends AbstractModel
|
||||
return $this->appendattrs['receives'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取汇报内容
|
||||
* @param $id
|
||||
* @return self|null
|
||||
*/
|
||||
public static function idOrCodeToContent($id)
|
||||
{
|
||||
if (Base::isNumber($id)) {
|
||||
return self::find($id);
|
||||
} elseif ($id) {
|
||||
$reportLink = ReportLink::whereCode($id)->first();
|
||||
if ($reportLink) {
|
||||
return self::find($reportLink->rid);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单条记录
|
||||
* @param $id
|
||||
@@ -139,12 +158,12 @@ class Report extends AbstractModel
|
||||
// 如果设置了周期偏移量
|
||||
empty( $offset ) || $now_dt->subWeeks( abs( $offset ) );
|
||||
$now_dt->startOfWeek(); // 设置为当周第一天
|
||||
return $now_dt->year . $now_dt->weekOfYear;
|
||||
return now()->year . $now_dt->weekOfYear;
|
||||
},
|
||||
Report::DAILY => function() use ($now_dt, $offset) {
|
||||
// 如果设置了周期偏移量
|
||||
empty( $offset ) || $now_dt->subDays( abs( $offset ) );
|
||||
return $now_dt->format("Ymd");
|
||||
return now()->format("Ymd");
|
||||
},
|
||||
default => "",
|
||||
};
|
||||
|
||||
86
app/Models/ReportLink.php
Normal file
86
app/Models/ReportLink.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Module\Base;
|
||||
|
||||
/**
|
||||
* App\Models\ReportLink
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $rid 报告ID
|
||||
* @property int|null $num 累计访问
|
||||
* @property string|null $code 链接码
|
||||
* @property int|null $userid 会员ID
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\Report|null $report
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereCode($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereNum($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereRid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ReportLink extends AbstractModel
|
||||
{
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
*/
|
||||
public function report(): \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
{
|
||||
return $this->hasOne(Report::class, 'id', 'report_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成链接
|
||||
* @param $rid
|
||||
* @param $userid
|
||||
* @param $refresh
|
||||
* @return array
|
||||
*/
|
||||
public static function generateLink($rid, $userid, $refresh = false)
|
||||
{
|
||||
$report = Report::find($rid);
|
||||
if (empty($report)) {
|
||||
throw new ApiException('报告不存在或已被删除');
|
||||
}
|
||||
if ($report->userid != $userid) {
|
||||
if (!ReportReceive::whereRid($rid)->whereUserid($userid)->exists()) {
|
||||
throw new ApiException('您没有权限查看该报告');
|
||||
}
|
||||
}
|
||||
$reportLink = ReportLink::whereRid($rid)->whereUserid($userid)->first();
|
||||
if (empty($reportLink)) {
|
||||
$reportLink = ReportLink::createInstance([
|
||||
'rid' => $rid,
|
||||
'userid' => $userid,
|
||||
'code' => base64_encode("{$rid},{$userid}," . Base::generatePassword()),
|
||||
]);
|
||||
$reportLink->save();
|
||||
} else {
|
||||
if ($refresh == 'yes') {
|
||||
$reportLink->code = base64_encode("{$rid},{$userid}," . Base::generatePassword());
|
||||
$reportLink->save();
|
||||
}
|
||||
}
|
||||
return [
|
||||
'id' => $rid,
|
||||
'url' => Base::fillUrl('single/report/detail/' . $reportLink->code),
|
||||
'code' => $reportLink->code,
|
||||
'num' => $reportLink->num
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
use App\Module\Timer;
|
||||
use Carbon\Carbon;
|
||||
|
||||
/**
|
||||
* App\Models\Setting
|
||||
@@ -65,20 +68,34 @@ class Setting extends AbstractModel
|
||||
$value['claude_key'] = $value['claude_token'];
|
||||
}
|
||||
$array = [];
|
||||
$aiList = ['openai', 'claude', 'gemini', 'zhipu', 'qianwen', 'wenxin'];
|
||||
$fieldList = ['key', 'model', 'agency', 'system', 'secret'];
|
||||
$aiList = ['openai', 'claude', 'deepseek', 'gemini', 'grok', 'ollama', 'zhipu', 'qianwen', 'wenxin'];
|
||||
$fieldList = ['key', 'secret', 'models', 'model', 'base_url', 'agency', 'temperature', 'system'];
|
||||
foreach ($aiList as $aiName) {
|
||||
foreach ($fieldList as $fieldName) {
|
||||
$key = $aiName . '_' . $fieldName;
|
||||
$array[$key] = $value[$key] ?: match ($key) {
|
||||
'openai_model' => 'gpt-4o-mini',
|
||||
'claude_model' => 'claude-3-5-sonnet-latest',
|
||||
'gemini_model' => 'gemini-1.5-flash',
|
||||
'zhipu_model' => 'glm-4',
|
||||
'qianwen_model' => 'qwen-turbo',
|
||||
'wenxin_model' => 'ernie-4.0-8k',
|
||||
default => '',
|
||||
};
|
||||
$content = $value[$key] ? trim($value[$key]) : '';
|
||||
switch ($fieldName) {
|
||||
case 'models':
|
||||
if ($content) {
|
||||
$content = explode("\n", $content);
|
||||
$content = array_filter($content);
|
||||
}
|
||||
if (empty($content)) {
|
||||
$content = self::AIDefaultModels($aiName);
|
||||
}
|
||||
$content = implode("\n", $content);
|
||||
break;
|
||||
case 'model':
|
||||
$models = Setting::AIModels2Array($array[$key . 's'], true);
|
||||
$content = in_array($content, $models) ? $content : ($models[0] ?? '');
|
||||
break;
|
||||
case 'temperature':
|
||||
if ($content) {
|
||||
$content = floatval(min(1, max(0, floatval($content) ?: 0.7)));
|
||||
}
|
||||
break;
|
||||
}
|
||||
$array[$key] = $content;
|
||||
}
|
||||
}
|
||||
$value = $array;
|
||||
@@ -98,6 +115,121 @@ class Setting extends AbstractModel
|
||||
return !!$array[$ai . '_key'];
|
||||
}
|
||||
|
||||
/**
|
||||
* AI默认模型
|
||||
* @param string $ai
|
||||
* @return array
|
||||
*/
|
||||
public static function AIDefaultModels($ai = 'openai')
|
||||
{
|
||||
return match ($ai) {
|
||||
'openai' => [
|
||||
'gpt-4 | GPT-4',
|
||||
'gpt-4-turbo | GPT-4 Turbo',
|
||||
'gpt-4o | GPT-4o',
|
||||
'gpt-4o-mini | GPT-4o Mini',
|
||||
'o1 | GPT-o1',
|
||||
'o1-mini | GPT-o1 Mini',
|
||||
'o3-mini | GPT-o3 Mini',
|
||||
'gpt-3.5-turbo | GPT-3.5 Turbo',
|
||||
'gpt-3.5-turbo-16k | GPT-3.5 Turbo 16K',
|
||||
'gpt-3.5-turbo-0125 | GPT-3.5 Turbo 0125',
|
||||
'gpt-3.5-turbo-1106 | GPT-3.5 Turbo 1106'
|
||||
],
|
||||
'claude' => [
|
||||
'claude-3-5-sonnet-latest | Claude 3.5 Sonnet',
|
||||
'claude-3-5-sonnet-20241022 | Claude 3.5 Sonnet 20241022',
|
||||
'claude-3-5-haiku-latest | Claude 3.5 Haiku',
|
||||
'claude-3-5-haiku-20241022 | Claude 3.5 Haiku 20241022',
|
||||
'claude-3-opus-latest | Claude 3 Opus',
|
||||
'claude-3-opus-20240229 | Claude 3 Opus 20240229',
|
||||
'claude-3-haiku-20240307 | Claude 3 Haiku 20240307',
|
||||
'claude-2.1 | Claude 2.1',
|
||||
'claude-2.0 | Claude 2.0'
|
||||
],
|
||||
'deepseek' => [
|
||||
'deepseek-chat | DeepSeek V3',
|
||||
'deepseek-reasoner | DeepSeek R1'
|
||||
],
|
||||
'gemini' => [
|
||||
'gemini-2.0-flash | Gemini 2.0 Flash',
|
||||
'gemini-2.0-flash-lite-preview-02-05 | Gemini 2.0 Flash-Lite Preview',
|
||||
'gemini-1.5-flash | Gemini 1.5 Flash',
|
||||
'gemini-1.5-flash-8b | Gemini 1.5 Flash 8B',
|
||||
'gemini-1.5-pro | Gemini 1.5 Pro',
|
||||
'gemini-1.0-pro | Gemini 1.0 Pro'
|
||||
],
|
||||
'grok' => [
|
||||
'grok-2-vision-1212 | Grok 2 Vision 1212',
|
||||
'grok-2-vision | Grok 2 Vision',
|
||||
'grok-2-vision-latest | Grok 2 Vision Latest',
|
||||
'grok-2-1212 | Grok 2 1212',
|
||||
'grok-2 | Grok 2',
|
||||
'grok-2-latest | Grok 2 Latest',
|
||||
'grok-vision-beta | Grok Vision Beta',
|
||||
'grok-beta | Grok Beta',
|
||||
],
|
||||
'zhipu' => [
|
||||
'glm-4 | GLM-4',
|
||||
'glm-4-plus | GLM-4 Plus',
|
||||
'glm-4-air | GLM-4 Air',
|
||||
'glm-4-airx | GLM-4 AirX',
|
||||
'glm-4-long | GLM-4 Long',
|
||||
'glm-4-flash | GLM-4 Flash',
|
||||
'glm-4v | GLM-4V',
|
||||
'glm-4v-plus | GLM-4V Plus',
|
||||
'glm-3-turbo | GLM-3 Turbo'
|
||||
],
|
||||
'qianwen' => [
|
||||
'qwen-max | QWEN Max',
|
||||
'qwen-max-latest | QWEN Max Latest',
|
||||
'qwen-turbo | QWEN Turbo',
|
||||
'qwen-turbo-latest | QWEN Turbo Latest',
|
||||
'qwen-plus | QWEN Plus',
|
||||
'qwen-plus-latest | QWEN Plus Latest',
|
||||
'qwen-long | QWEN Long'
|
||||
],
|
||||
'wenxin' => [
|
||||
'ernie-4.0-8k | Ernie 4.0 8K',
|
||||
'ernie-4.0-8k-latest | Ernie 4.0 8K Latest',
|
||||
'ernie-4.0-turbo-128k | Ernie 4.0 Turbo 128K',
|
||||
'ernie-4.0-turbo-8k | Ernie 4.0 Turbo 8K',
|
||||
'ernie-3.5-128k | Ernie 3.5 128K',
|
||||
'ernie-3.5-8k | Ernie 3.5 8K',
|
||||
'ernie-speed-128k | Ernie Speed 128K',
|
||||
'ernie-speed-8k | Ernie Speed 8K',
|
||||
'ernie-lite-8k | Ernie Lite 8K',
|
||||
'ernie-tiny-8k | Ernie Tiny 8K'
|
||||
],
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* AI模型转数组
|
||||
* @param $models
|
||||
* @param bool $retValue
|
||||
* @return array
|
||||
*/
|
||||
public static function AIModels2Array($models, $retValue = false)
|
||||
{
|
||||
$list = is_array($models) ? $models : explode("\n", $models);
|
||||
$array = [];
|
||||
foreach ($list as $item) {
|
||||
$arr = Base::newTrim(explode('|', $item . '|'));
|
||||
if ($arr[0]) {
|
||||
$array[] = [
|
||||
'value' => $arr[0],
|
||||
'label' => $arr[1] ?: $arr[0]
|
||||
];
|
||||
}
|
||||
}
|
||||
if ($retValue) {
|
||||
return array_column($array, 'value');
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮箱地址(过滤忽略地址)
|
||||
* @param $array
|
||||
@@ -134,4 +266,36 @@ class Setting extends AbstractModel
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证消息限制
|
||||
* @param $type
|
||||
* @param $msg
|
||||
* @return void
|
||||
*/
|
||||
public static function validateMsgLimit($type, $msg)
|
||||
{
|
||||
$keyName = 'msg_edit_limit';
|
||||
$error = '此消息不可修改';
|
||||
if ($type == 'rev') {
|
||||
$keyName = 'msg_rev_limit';
|
||||
$error = '此消息不可撤回';
|
||||
}
|
||||
$limitNum = intval(Base::settingFind('system', $keyName, 0));
|
||||
if ($limitNum <= 0) {
|
||||
return;
|
||||
}
|
||||
if ($msg instanceof WebSocketDialogMsg) {
|
||||
$dialogMsg = $msg;
|
||||
} else {
|
||||
$dialogMsg = WebSocketDialogMsg::find($msg);
|
||||
}
|
||||
if (!$dialogMsg) {
|
||||
return;
|
||||
}
|
||||
$limitTime = Carbon::parse($dialogMsg->created_at)->addMinutes($limitNum);
|
||||
if ($limitTime->lt(Carbon::now())) {
|
||||
throw new ApiException('已超过' . Doo::translate(Base::forumMinuteDay($limitNum)) . ',' . $error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use Hedeqiang\UMeng\IOS;
|
||||
* @property string|null $alias 别名
|
||||
* @property string|null $platform 平台类型
|
||||
* @property string|null $device 设备类型
|
||||
* @property string|null $version 应用版本号
|
||||
* @property string|null $ua userAgent
|
||||
* @property int|null $is_notified 通知权限
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
@@ -37,6 +38,7 @@ use Hedeqiang\UMeng\IOS;
|
||||
* @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
|
||||
@@ -191,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':
|
||||
|
||||
@@ -83,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提醒',
|
||||
@@ -187,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('历史会话'),
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -425,4 +452,39 @@ class UserBot extends AbstractModel
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建我的机器人
|
||||
* @param $userid
|
||||
* @param $botName
|
||||
* @return array
|
||||
*/
|
||||
public static function newbot($userid, $botName)
|
||||
{
|
||||
if (User::select(['users.*'])
|
||||
->join('user_bots', 'users.userid', '=', 'user_bots.bot_id')
|
||||
->where('users.bot', 1)
|
||||
->where('user_bots.userid', $userid)
|
||||
->count() >= 50) {
|
||||
return Base::retError("超过最大创建数量。");
|
||||
}
|
||||
if (strlen($botName) < 2 || strlen($botName) > 20) {
|
||||
return Base::retError("机器人名称由2-20个字符组成。");
|
||||
}
|
||||
$data = User::botGetOrCreate("user-" . Base::generatePassword(), [
|
||||
'nickname' => $botName
|
||||
], $userid);
|
||||
if (empty($data)) {
|
||||
return Base::retError("创建失败。");
|
||||
}
|
||||
$dialog = WebSocketDialog::checkUserDialog($data, $userid);
|
||||
if ($dialog) {
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||
'type' => '/hello',
|
||||
'title' => '创建成功。',
|
||||
'data' => $data,
|
||||
], $data->userid);
|
||||
}
|
||||
return Base::retSuccess("创建成功。", $data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ class UserCheckinFace extends AbstractModel
|
||||
$record = base64_encode(file_get_contents($faceFile));
|
||||
}
|
||||
|
||||
$url = 'http://' . env('APP_IPPR') . '.14' . ":7788/user";
|
||||
$url = "http://face:7788/user";
|
||||
$data = [
|
||||
'name' => $nickname,
|
||||
'enrollid' => $userid,
|
||||
@@ -92,7 +92,7 @@ class UserCheckinFace extends AbstractModel
|
||||
}
|
||||
|
||||
public static function deleteDeviceUser($userid) {
|
||||
$url = 'http://' . env('APP_IPPR') . '.14' . ":7788/user/delete";
|
||||
$url = "http://face:7788/user/delete";
|
||||
$data = [
|
||||
'enrollid' => $userid,
|
||||
'backupnum' => 50, // 13 删除整个用户 50 删除图片
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace App\Models;
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\WebSocketDialog|null $dialog
|
||||
* @property-read \App\Models\User|null $user
|
||||
* @property-read \App\Models\User $user
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
|
||||
@@ -18,6 +18,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @property int $id
|
||||
* @property int|null $dialog_id 对话ID
|
||||
* @property string|null $dialog_type 对话类型
|
||||
* @property int|null $session_id 会话ID
|
||||
* @property int|null $userid 发送会员ID
|
||||
* @property string|null $type 消息类型
|
||||
* @property string|null $mtype 消息类型(用于搜索)
|
||||
@@ -69,6 +70,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereReplyId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereReplyNum($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereSend($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereSessionId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereTag($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereTodo($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereType($value)
|
||||
@@ -303,7 +305,12 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
];
|
||||
//
|
||||
$dialog = WebSocketDialog::find($this->dialog_id);
|
||||
$dialog?->pushMsg('update', $resData);
|
||||
if ($dialog) {
|
||||
$dialog->pushMsg('update', $resData);
|
||||
WebSocketDialogUser::whereDialogId($dialog->id)->change([
|
||||
'updated_at' => Carbon::now()->toDateTimeString('millisecond'),
|
||||
]);
|
||||
}
|
||||
//
|
||||
return Base::retSuccess('success', $resData);
|
||||
}
|
||||
@@ -532,10 +539,6 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
*/
|
||||
public function withdrawMsg()
|
||||
{
|
||||
$send_dt = Carbon::parse($this->created_at)->addDay();
|
||||
if ($send_dt->lt(Carbon::now())) {
|
||||
throw new ApiException('已超过24小时,此消息不能撤回');
|
||||
}
|
||||
AbstractModel::transaction(function() {
|
||||
$deleteRead = WebSocketDialogMsgRead::whereMsgId($this->id)->whereNull('read_at')->delete(); // 未阅读记录不需要软删除,直接删除即可
|
||||
$this->delete();
|
||||
@@ -592,6 +595,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);
|
||||
@@ -654,6 +660,10 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
$text = $msgData['text'] ?? '';
|
||||
if (!$text) return '';
|
||||
if ($msgData['type'] === 'md') {
|
||||
$text = preg_replace("/:::\s*reasoning[\s\S]*?:::/", "", $text);
|
||||
if (preg_match('/:::\s*reasoning\s+/', $text)) {
|
||||
return Doo::translate('思考中...');
|
||||
}
|
||||
$text = Base::markdown2html($text);
|
||||
$text = self::previewConvertTaskList($text);
|
||||
}
|
||||
@@ -753,9 +763,15 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
$key = '';
|
||||
switch ($this->type) {
|
||||
case 'text':
|
||||
if (!preg_match("/<span[^>]*?data-quick-key=([\"'])([^\"']+?)\\1[^>]*?>/is", $this->msg['text'])) {
|
||||
$key = strip_tags($this->msg['text']);
|
||||
if (preg_match("/<span[^>]*?data-quick-key=([\"'])([^\"']+?)\\1[^>]*?>/i", $this->msg['text'])) {
|
||||
break;
|
||||
}
|
||||
$key = $this->msg['text'];
|
||||
if ($this->msg['type'] === 'md') {
|
||||
$key = preg_replace("/:::\s*reasoning[\s\S]*?:::/", "", $key);
|
||||
$key = Base::markdown2html($key);
|
||||
}
|
||||
$key = strip_tags($key);
|
||||
break;
|
||||
|
||||
case 'vote':
|
||||
@@ -774,14 +790,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
|
||||
@@ -869,8 +895,16 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
$imageSaveLocal = Base::settingFind("system", "image_save_local");
|
||||
preg_match_all("/<img[^>]*?src=([\"'])(.*?(png|jpg|jpeg|webp|gif).*?)\\1[^>]*?>/is", $text, $matchs);
|
||||
foreach ($matchs[2] as $key => $str) {
|
||||
$parsed = parse_url($str);
|
||||
if (str_starts_with($parsed['path'], "/uploads/")) {
|
||||
$relativePath = ltrim($parsed['path'], "/");
|
||||
$relativePath = Base::thumbRestore($relativePath);
|
||||
if (file_exists(public_path($relativePath))) {
|
||||
$str = "{{RemoteURL}}{$relativePath}";
|
||||
}
|
||||
}
|
||||
if ($imageSaveLocal === 'close') {
|
||||
$imageSize = getimagesize($str);
|
||||
$imageSize = @getimagesize($str);
|
||||
if ($imageSize === false) {
|
||||
$imageSize = ["auto", "auto"];
|
||||
}
|
||||
@@ -905,7 +939,7 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
}
|
||||
}
|
||||
}
|
||||
// @成员、#任务、~文件
|
||||
// @成员、#任务、~文件、%报告
|
||||
preg_match_all("/<span\s+class=\"mention\"(.*?)>.*?<\/span>.*?<\/span>.*?<\/span>/s", $text, $matchs);
|
||||
foreach ($matchs[1] as $key => $str) {
|
||||
preg_match("/data-denotation-char=\"(.*?)\"/", $str, $matchChar);
|
||||
@@ -913,6 +947,7 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
preg_match("/data-value=\"(.*?)\"/s", $str, $matchValye);
|
||||
$keyId = $matchId[1];
|
||||
if ($matchChar[1] === "~") {
|
||||
// 文件特殊处理
|
||||
if (Base::isNumber($keyId)) {
|
||||
$file = File::permissionFind($keyId, User::auth());
|
||||
if ($file->type == 'folder') {
|
||||
@@ -928,6 +963,19 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
throw new ApiException('文件分享错误');
|
||||
}
|
||||
}
|
||||
} elseif ($matchChar[1] === "%") {
|
||||
// 报告特殊处理
|
||||
if (Base::isNumber($keyId)) {
|
||||
$reportLink = ReportLink::generateLink($keyId, User::userid());
|
||||
$keyId = $reportLink['code'];
|
||||
} else {
|
||||
preg_match("/\/single\/report\/detail\/(.*?)$/i", $keyId, $match);
|
||||
if ($match && strlen($match[1]) >= 8) {
|
||||
$keyId = $match[1];
|
||||
} else {
|
||||
throw new ApiException('报告分享错误');
|
||||
}
|
||||
}
|
||||
}
|
||||
$text = str_replace($matchs[0][$key], "[:{$matchChar[1]}:{$keyId}:{$matchValye[1]}:]", $text);
|
||||
}
|
||||
@@ -956,31 +1004,18 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
foreach ($matchs[0] as $key => $str) {
|
||||
$herf = $matchs[2][$key];
|
||||
$title = $matchs[3][$key] ?: $herf;
|
||||
preg_match("/\/single\/file\/(.*?)$/i", strip_tags($title), $match);
|
||||
if ($match && strlen($match[1]) >= 8) {
|
||||
$file = File::select(['files.id', 'files.name', 'files.ext'])->join('file_links as L', 'files.id', '=', 'L.file_id')->where('L.code', $match[1])->first();
|
||||
if ($file && $file->name) {
|
||||
$name = $file->ext ? "{$file->name}.{$file->ext}" : $file->name;
|
||||
$text = str_replace($str, "[:~:{$match[1]}:{$name}:]", $text);
|
||||
continue;
|
||||
}
|
||||
if (self::formatLink($str, strip_tags($title), $text)) {
|
||||
continue;
|
||||
}
|
||||
$herf = base64_encode($herf);
|
||||
$title = base64_encode($title);
|
||||
$text = str_replace($str, "[:LINK:{$herf}:{$title}:]", $text);
|
||||
}
|
||||
// 文件分享链接
|
||||
// 分享链接
|
||||
preg_match_all("/(https?:\/\/)((\w|=|\?|\.|\/|&|-|:|\+|%|;|#|@|,|!)+)/i", $text, $matchs);
|
||||
if ($matchs) {
|
||||
foreach ($matchs[0] as $str) {
|
||||
preg_match("/\/single\/file\/(.*?)$/i", $str, $match);
|
||||
if ($match && strlen($match[1]) >= 8) {
|
||||
$file = File::select(['files.id', 'files.name', 'files.ext'])->join('file_links as L', 'files.id', '=', 'L.file_id')->where('L.code', $match[1])->first();
|
||||
if ($file && $file->name) {
|
||||
$name = $file->ext ? "{$file->name}.{$file->ext}" : $file->name;
|
||||
$text = str_replace($str, "[:~:{$match[1]}:{$name}:]", $text);
|
||||
}
|
||||
}
|
||||
self::formatLink($str, $str, $text);
|
||||
}
|
||||
}
|
||||
// 过滤标签
|
||||
@@ -1010,10 +1045,41 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
$text = preg_replace("/\[:@:(.*?):(.*?):\]/i", "<span class=\"mention user\" data-id=\"$1\">@$2</span>", $text);
|
||||
$text = preg_replace("/\[:#:(.*?):(.*?):\]/is", "<span class=\"mention task\" data-id=\"$1\">#$2</span>", $text);
|
||||
$text = preg_replace("/\[:~:(.*?):(.*?):\]/i", "<a class=\"mention file\" href=\"{{RemoteURL}}single/file/$1\" target=\"_blank\">~$2</a>", $text);
|
||||
$text = preg_replace("/\[:%:(.*?):(.*?):\]/i", "<a class=\"mention report\" href=\"{{RemoteURL}}single/report/detail/$1\" target=\"_blank\">%$2</a>", $text);
|
||||
$text = preg_replace("/\[:QUICK:(.*?):(.*?):\]/i", "<span data-quick-key=\"$1\">$2</span>", $text);
|
||||
return preg_replace("/^(<p><\/p>)+|(<p><\/p>)+$/i", "", $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 链接转换处理
|
||||
* @param $search
|
||||
* @param $subject
|
||||
* @param $content
|
||||
* @return bool
|
||||
*/
|
||||
public static function formatLink($search, $subject, &$content)
|
||||
{
|
||||
$ret = false;
|
||||
preg_match("/\/single\/file\/(.*?)$/i", $subject, $match);
|
||||
if ($match && strlen($match[1]) >= 8) {
|
||||
$file = File::select(['files.id', 'files.name', 'files.ext'])->join('file_links as L', 'files.id', '=', 'L.file_id')->where('L.code', $match[1])->first();
|
||||
if ($file && $file->name) {
|
||||
$name = $file->ext ? "{$file->name}.{$file->ext}" : $file->name;
|
||||
$content = str_replace($search, "[:~:{$match[1]}:{$name}:]", $content);
|
||||
$ret = true;
|
||||
}
|
||||
}
|
||||
preg_match("/\/single\/report\/detail\/(.*?)$/i", $subject, $match);
|
||||
if ($match && strlen($match[1]) >= 8) {
|
||||
$report = Report::select(['reports.id', 'reports.title'])->join('report_links as L', 'reports.id', '=', 'L.rid')->where('L.code', $match[1])->first();
|
||||
if ($report && $report->title) {
|
||||
$content = str_replace($search, "[:%:{$match[1]}:{$report->title}:]", $content);
|
||||
$ret = true;
|
||||
}
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息、修改消息
|
||||
* @param string $action 动作
|
||||
@@ -1123,6 +1189,7 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
}
|
||||
//
|
||||
$updateData = [
|
||||
'type' => $type,
|
||||
'mtype' => $mtype,
|
||||
'link' => $link,
|
||||
'msg' => array_merge($oldMsg, $msg),
|
||||
@@ -1165,6 +1232,7 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
$dialogMsg = self::createInstance([
|
||||
'dialog_id' => $dialog_id,
|
||||
'dialog_type' => $dialog->type,
|
||||
'session_id' => $dialog->session_id,
|
||||
'reply_id' => $reply_id,
|
||||
'forward_id' => $forward_id,
|
||||
'userid' => $sender,
|
||||
@@ -1179,6 +1247,8 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
$dialogMsg->send = 1;
|
||||
$dialogMsg->generateKeyAndSave($search_key);
|
||||
//
|
||||
WebSocketDialogSession::updateTitle($dialogMsg->session_id, $dialogMsg);
|
||||
//
|
||||
if ($dialogMsg->type === 'meeting') {
|
||||
MeetingMsg::createInstance([
|
||||
'meetingid' => $dialogMsg->msg['meetingid'],
|
||||
@@ -1210,6 +1280,55 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量发送消息
|
||||
* @param User $user 发送的会员
|
||||
* @param array $userids 接收的会员ID
|
||||
* @param array $dialogids 接收的会话ID
|
||||
* @param string $msgText 发送的消息
|
||||
* @return array
|
||||
*/
|
||||
public static function sendMsgBatch($user, $userids, $dialogids, $msgText)
|
||||
{
|
||||
return AbstractModel::transaction(function() use ($user, $userids, $dialogids, $msgText) {
|
||||
$msgs = [];
|
||||
$already = [];
|
||||
if ($dialogids) {
|
||||
if (!is_array($dialogids)) {
|
||||
$dialogids = [$dialogids];
|
||||
}
|
||||
foreach ($dialogids as $dialogid) {
|
||||
$res = WebSocketDialogMsg::sendMsg(null, $dialogid, 'text', ['text' => $msgText], $user->userid);
|
||||
if (Base::isSuccess($res)) {
|
||||
$msgs[] = $res['data'];
|
||||
$already[] = $dialogid;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($userids) {
|
||||
if (!is_array($userids)) {
|
||||
$userids = [$userids];
|
||||
}
|
||||
foreach ($userids as $userid) {
|
||||
if (!User::whereUserid($userid)->exists()) {
|
||||
continue;
|
||||
}
|
||||
$dialog = WebSocketDialog::checkUserDialog($user, $userid);
|
||||
if ($dialog && !in_array($dialog->id, $already)) {
|
||||
$res = WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $msgText], $user->userid);
|
||||
if (Base::isSuccess($res)) {
|
||||
$msgs[] = $res['data'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Base::retSuccess('发送成功', [
|
||||
'msgs' => $msgs
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 将被@的人加入群
|
||||
* @param WebSocketDialog $dialog 对话
|
||||
|
||||
99
app/Models/WebSocketDialogSession.php
Normal file
99
app/Models/WebSocketDialogSession.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Module\Base;
|
||||
use App\Module\Extranet;
|
||||
use Swoole\Coroutine;
|
||||
use Cache;
|
||||
|
||||
/**
|
||||
* App\Models\WebSocketDialogSession
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $dialog_id 对话ID
|
||||
* @property string $title 会话标题
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\WebSocketDialog|null $dialog
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession whereDialogId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession whereTitle($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class WebSocketDialogSession extends AbstractModel
|
||||
{
|
||||
/**
|
||||
* 可以批量赋值的属性
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'dialog_id',
|
||||
'userid',
|
||||
'title',
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取关联的对话
|
||||
*/
|
||||
public function dialog()
|
||||
{
|
||||
return $this->belongsTo(WebSocketDialog::class, 'dialog_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $sessionId
|
||||
* @param WebSocketDialogMsg $dialogMsg
|
||||
* @return void
|
||||
*/
|
||||
public static function updateTitle($sessionId, $dialogMsg)
|
||||
{
|
||||
if (!$sessionId) {
|
||||
return;
|
||||
}
|
||||
if ($dialogMsg->type != 'text') {
|
||||
return;
|
||||
}
|
||||
$cacheKey = 'dialog_session_title_' . $sessionId;
|
||||
if (Cache::has($cacheKey)) {
|
||||
return;
|
||||
}
|
||||
$originalTitle = $dialogMsg->key ?: $dialogMsg->msg['text'] ?: 'Untitled';
|
||||
$title = Base::cutStr($originalTitle, 100);
|
||||
if ($title == '...') {
|
||||
return;
|
||||
}
|
||||
$session = self::whereId($sessionId)->first();
|
||||
if (!$session) {
|
||||
return;
|
||||
}
|
||||
$session->title = $title;
|
||||
$session->save();
|
||||
Cache::forever($cacheKey, true);
|
||||
// 通过AI接口更新对话标题
|
||||
go(function () use ($session, $title, $originalTitle) {
|
||||
Coroutine::sleep(0.1);
|
||||
$res = Extranet::openAIGenerateTitle($originalTitle);
|
||||
if (Base::isError($res)) {
|
||||
return;
|
||||
}
|
||||
$newTitle = $res['data'];
|
||||
if ($newTitle && $newTitle != $title) {
|
||||
$session->title = Base::cutStr($newTitle, 100);
|
||||
$session->save();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,13 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
||||
/**
|
||||
* App\Models\WebSocketDialogUser
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $dialog_id 对话ID
|
||||
* @property int|null $userid 会员ID
|
||||
* @property int|null $bot 是否机器人
|
||||
* @property \Illuminate\Support\Carbon|null $top_at 置顶时间
|
||||
* @property \Illuminate\Support\Carbon|null $last_at 最后消息时间
|
||||
* @property int|null $mark_unread 是否标记为未读:0否,1是
|
||||
@@ -30,6 +29,7 @@ use Carbon\Carbon;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereBot($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereColor($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereDialogId($value)
|
||||
|
||||
@@ -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)
|
||||
@@ -1475,14 +1476,36 @@ class Base
|
||||
public static function forumHourDay($hour)
|
||||
{
|
||||
$hour = intval($hour);
|
||||
if ($hour > 24) {
|
||||
if ($hour >= 24) {
|
||||
$day = floor($hour / 24);
|
||||
$hour -= $day * 24;
|
||||
return $day . '天' . $hour . '小时';
|
||||
if ($hour > 0) {
|
||||
return $day . '天' . $hour . '小时';
|
||||
}
|
||||
return $day . '天';
|
||||
}
|
||||
return $hour . '小时';
|
||||
}
|
||||
|
||||
/**
|
||||
* 分钟转天/小时/分钟
|
||||
* @param $minute
|
||||
* @return string
|
||||
*/
|
||||
public static function forumMinuteDay($minute)
|
||||
{
|
||||
$minute = intval($minute);
|
||||
if ($minute >= 60) {
|
||||
$hour = floor($minute / 60);
|
||||
$minute -= $hour * 60;
|
||||
if ($minute > 0) {
|
||||
return Base::forumHourDay($hour) . $minute . '分钟';
|
||||
}
|
||||
return Base::forumHourDay($hour);
|
||||
}
|
||||
return $minute . '分钟';
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Carbon对象
|
||||
* @param $var
|
||||
@@ -2017,7 +2040,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 +2781,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 +2803,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 +2820,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', 'es');
|
||||
$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 . '>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
259
app/Module/TextExtractor.php
Normal file
259
app/Module/TextExtractor.php
Normal file
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module;
|
||||
|
||||
use Exception;
|
||||
use PhpOffice\PhpWord\IOFactory as WordIOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory as SpreadsheetIOFactory;
|
||||
use PhpOffice\PhpPresentation\IOFactory as PresentationIOFactory;
|
||||
use Illuminate\Support\Facades\File as FileFacade;
|
||||
|
||||
|
||||
class TextExtractor
|
||||
{
|
||||
private string $filePath;
|
||||
private string $fileMimeType;
|
||||
private string $fileExtension;
|
||||
|
||||
/**
|
||||
* @param string $filePath
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __construct(string $filePath)
|
||||
{
|
||||
if (!file_exists($filePath)) {
|
||||
throw new Exception("File does not exist: {$filePath}");
|
||||
}
|
||||
$this->filePath = $filePath;
|
||||
$this->fileMimeType = FileFacade::mimeType($filePath);
|
||||
$this->fileExtension = $this->detectFileType();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件中提取文本
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
public function extractContent(): string
|
||||
{
|
||||
return match ($this->fileExtension) {
|
||||
// Word文档
|
||||
'docx' => $this->parseWordDocument(),
|
||||
|
||||
// Excel文档
|
||||
'xlsx', 'xls', 'csv' => $this->parseSpreadsheet(),
|
||||
|
||||
// PowerPoint文档
|
||||
'ppt', 'pptx' => $this->parsePresentation(),
|
||||
|
||||
// PDF文档
|
||||
'pdf' => $this->parsePdf(),
|
||||
|
||||
// RTF文档
|
||||
'rtf' => $this->parseRtf(),
|
||||
|
||||
// 其他文本文件
|
||||
default => $this->parseOther(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件类型
|
||||
* @return string
|
||||
*/
|
||||
private function detectFileType(): string
|
||||
{
|
||||
return match ($this->fileMimeType) {
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
|
||||
'application/vnd.ms-excel' => 'xls',
|
||||
'text/csv', 'application/csv' => 'csv',
|
||||
'application/vnd.ms-powerpoint' => 'ppt',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx',
|
||||
'application/pdf' => 'pdf',
|
||||
'application/rtf', 'text/rtf' => 'rtf',
|
||||
default => strtolower(pathinfo($this->filePath, PATHINFO_EXTENSION)),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Word documents (.doc, .docx)
|
||||
* @return string
|
||||
*/
|
||||
private function parseWordDocument(): string
|
||||
{
|
||||
$phpWord = WordIOFactory::load($this->filePath);
|
||||
$text = '';
|
||||
|
||||
// Extract text from each section
|
||||
foreach ($phpWord->getSections() as $section) {
|
||||
foreach ($section->getElements() as $element) {
|
||||
if (method_exists($element, 'getText')) {
|
||||
$text .= $element->getText() . "\n";
|
||||
} elseif (method_exists($element, 'getElements')) {
|
||||
foreach ($element->getElements() as $childElement) {
|
||||
if (method_exists($childElement, 'getText')) {
|
||||
$text .= $childElement->getText() . "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse spreadsheet files (.xlsx, .xls, .csv)
|
||||
* @return string
|
||||
*/
|
||||
private function parseSpreadsheet(): string
|
||||
{
|
||||
$spreadsheet = SpreadsheetIOFactory::load($this->filePath);
|
||||
$text = '';
|
||||
|
||||
// Extract text from all worksheets
|
||||
foreach ($spreadsheet->getWorksheetIterator() as $worksheet) {
|
||||
$text .= 'Worksheet: ' . $worksheet->getTitle() . "\n";
|
||||
|
||||
foreach ($worksheet->getRowIterator() as $row) {
|
||||
$cellIterator = $row->getCellIterator();
|
||||
$cellIterator->setIterateOnlyExistingCells(false);
|
||||
$rowText = '';
|
||||
|
||||
foreach ($cellIterator as $cell) {
|
||||
$value = $cell->getValue();
|
||||
if (!empty($value)) {
|
||||
$rowText .= $value . "\t";
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty(trim($rowText))) {
|
||||
$text .= trim($rowText) . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
$text .= "\n";
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse presentation files (.ppt, .pptx)
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
private function parsePresentation(): string
|
||||
{
|
||||
$presentation = PresentationIOFactory::load($this->filePath);
|
||||
$text = '';
|
||||
|
||||
// Extract text from all slides
|
||||
foreach ($presentation->getAllSlides() as $slide) {
|
||||
foreach ($slide->getShapeCollection() as $shape) {
|
||||
if ($shape instanceof \PhpOffice\PhpPresentation\Shape\RichText) {
|
||||
foreach ($shape->getParagraphs() as $paragraph) {
|
||||
foreach ($paragraph->getRichTextElements() as $element) {
|
||||
$text .= $element->getText();
|
||||
}
|
||||
$text .= "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
$text .= "\n";
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse PDF files (requires additional library like Smalot\PdfParser)
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
private function parsePdf(): string
|
||||
{
|
||||
// You'll need to install the Smalot PDF Parser: composer require smalot/pdfparser
|
||||
if (!class_exists('\Smalot\PdfParser\Parser')) {
|
||||
throw new \Exception("PDF Parser not available. Install with: composer require smalot/pdfparser");
|
||||
}
|
||||
|
||||
$parser = new \Smalot\PdfParser\Parser();
|
||||
$pdf = $parser->parseFile($this->filePath);
|
||||
return $pdf->getText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse RTF files
|
||||
* @return string
|
||||
*/
|
||||
private function parseRtf(): string
|
||||
{
|
||||
// Simple RTF to text conversion
|
||||
$content = file_get_contents($this->filePath);
|
||||
|
||||
// Remove RTF control words and groups
|
||||
$content = preg_replace('/\\\\([a-z]{1,32})(-?[0-9]{1,10})?[ ]?/i', '', $content);
|
||||
$content = preg_replace('/\\\\([^a-z]|[a-z]{33,})/i', '', $content);
|
||||
$content = preg_replace('/\{\*?\\\\[^{}]*\}/', '', $content);
|
||||
$content = preg_replace('/\{[\r\n]*\}/', '', $content);
|
||||
|
||||
// Convert special characters
|
||||
$content = preg_replace('/\\\\\'([0-9a-f]{2})/i', '', $content);
|
||||
|
||||
// Remove remaining curly braces
|
||||
$content = str_replace(['{', '}'], '', $content);
|
||||
|
||||
return $content ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Other(text) files
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
private function parseOther(): string
|
||||
{
|
||||
$isBinary = !str_contains($this->fileMimeType, 'text/')
|
||||
&& !str_contains($this->fileMimeType, 'application/json')
|
||||
&& !str_contains($this->fileMimeType, 'application/xml');
|
||||
|
||||
if ($isBinary) {
|
||||
throw new Exception("Unable to read the text content of this type of file");
|
||||
}
|
||||
|
||||
return file_get_contents($this->filePath);
|
||||
}
|
||||
|
||||
/** ********************************************************************* */
|
||||
/** ********************************************************************* */
|
||||
/** ********************************************************************* */
|
||||
|
||||
/**
|
||||
* 获取文件内容
|
||||
* @param $filePath
|
||||
* @param int $fileMaxSize 最大文件大小,单位字节,默认1024KB
|
||||
* @param int $contentMaxSize 最大内容大小,单位字节,默认300KB
|
||||
* @return array
|
||||
*/
|
||||
public static function extractFile($filePath, int $fileMaxSize = 1024, int $contentMaxSize = 300): array
|
||||
{
|
||||
if (!file_exists($filePath) || !is_file($filePath)) {
|
||||
return Base::retError("Failed to read contents of {$filePath}");
|
||||
}
|
||||
if (filesize($filePath) > $fileMaxSize * 1024) {
|
||||
return Base::retError("File size exceeds " . Base::readableBytes($fileMaxSize * 1024) . ", unable to display content");
|
||||
}
|
||||
try {
|
||||
$extractor = new self($filePath);
|
||||
$content = $extractor->extractContent();
|
||||
if (strlen($content) > $contentMaxSize * 1024) {
|
||||
return Base::retError("Content size exceeds " . Base::readableBytes($contentMaxSize * 1024) . ", unable to display content");
|
||||
}
|
||||
return Base::retSuccess("success", $content);
|
||||
} catch (Exception $e) {
|
||||
return Base::retError($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,24 @@
|
||||
|
||||
namespace App\Tasks;
|
||||
|
||||
use App\Models\FileContent;
|
||||
use App\Models\Project;
|
||||
use App\Models\ProjectTask;
|
||||
use App\Models\Report;
|
||||
use App\Models\User;
|
||||
use App\Models\UserBot;
|
||||
use App\Models\UserDepartment;
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Models\WebSocketDialogConfig;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
use App\Module\Ihttp;
|
||||
use App\Module\TextExtractor;
|
||||
use Carbon\Carbon;
|
||||
use DB;
|
||||
use Exception;
|
||||
use League\HTMLToMarkdown\HtmlConverter;
|
||||
use DB;
|
||||
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
@@ -86,8 +91,16 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
}
|
||||
|
||||
// 提取指令
|
||||
$command = $this->extractCommand($msg, $this->mention);
|
||||
if (empty($command)) {
|
||||
try {
|
||||
$command = $this->extractCommand($msg, $botUser->isAiBot(), $this->mention);
|
||||
if (empty($command)) {
|
||||
return;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'template', [
|
||||
'type' => 'content',
|
||||
'content' => $e->getMessage() ?: "指令解析失败。",
|
||||
], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -195,32 +208,11 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
* 创建
|
||||
*/
|
||||
case '/newbot':
|
||||
if (User::select(['users.*'])
|
||||
->join('user_bots', 'users.userid', '=', 'user_bots.bot_id')
|
||||
->where('users.bot', 1)
|
||||
->where('user_bots.userid', $msg->userid)
|
||||
->count() >= 50) {
|
||||
$content = "超过最大创建数量。";
|
||||
break;
|
||||
}
|
||||
if (strlen($array[1]) < 2 || strlen($array[1]) > 20) {
|
||||
$content = "机器人名称由2-20个字符组成。";
|
||||
break;
|
||||
}
|
||||
$data = User::botGetOrCreate("user-" . Base::generatePassword(), [
|
||||
'nickname' => $array[1]
|
||||
], $msg->userid);
|
||||
if (empty($data)) {
|
||||
$content = "创建失败。";
|
||||
break;
|
||||
}
|
||||
$dialog = WebSocketDialog::checkUserDialog($data, $msg->userid);
|
||||
if ($dialog) {
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||
'type' => '/hello',
|
||||
'title' => '创建成功。',
|
||||
'data' => $data,
|
||||
], $data->userid); // todo 未能在任务end事件来发送任务
|
||||
$res = UserBot::newbot($msg->userid, $array[1]);
|
||||
if (Base::isError($res)) {
|
||||
$content = $res['msg'];
|
||||
} else {
|
||||
$data = $res['data'];
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -411,14 +403,13 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
*/
|
||||
private function botWebhookBusiness(string $command, WebSocketDialogMsg $msg, User $botUser, WebSocketDialog $dialog)
|
||||
{
|
||||
$serverUrl = 'http://' . env('APP_IPPR') . '.3';
|
||||
$serverUrl = 'http://nginx';
|
||||
$userBot = null;
|
||||
$extras = [];
|
||||
$errorContent = null;
|
||||
if (preg_match('/^ai-(.*?)@bot\.system$/', $botUser->email, $matches)) {
|
||||
if ($botUser->isAiBot($type)) {
|
||||
// AI机器人
|
||||
$setting = Base::setting('aibotSetting');
|
||||
$type = $matches[1];
|
||||
$extras = [
|
||||
'model_type' => match ($type) {
|
||||
'qianwen' => 'qwen',
|
||||
@@ -427,33 +418,85 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
'model_name' => $setting[$type . '_model'],
|
||||
'system_message' => $setting[$type . '_system'],
|
||||
'api_key' => $setting[$type . '_key'],
|
||||
'base_url' => $setting[$type . '_base_url'],
|
||||
'agency' => $setting[$type . '_agency'],
|
||||
'server_url' => $serverUrl,
|
||||
];
|
||||
if ($setting[$type . '_temperature']) {
|
||||
$extras['temperature'] = floatval($setting[$type . '_temperature']);
|
||||
}
|
||||
if ($msg->msg['model_name']) {
|
||||
$extras['model_name'] = $msg->msg['model_name'];
|
||||
}
|
||||
if (preg_match("/(.*?)(\s+|\s*[_-]\s*)(think|thinking|reasoning)\s*$/", $extras['model_name'], $match)) {
|
||||
$extras['model_name'] = $match[1];
|
||||
$extras['max_tokens'] = 20000;
|
||||
$extras['thinking'] = 4096;
|
||||
$extras['temperature'] = 1.0;
|
||||
}
|
||||
if ($dialog->session_id) {
|
||||
$extras['context_key'] = 'session_' . $dialog->session_id;
|
||||
}
|
||||
if ($type === 'wenxin') {
|
||||
$extras['api_key'] .= ':' . $setting['wenxin_secret'];
|
||||
}
|
||||
if ($type === 'ollama') {
|
||||
if (empty($extras['base_url'])) {
|
||||
$errorContent = '机器人未启用。';
|
||||
}
|
||||
if (empty($extras['api_key'])) {
|
||||
$extras['api_key'] = Base::strRandom(6);
|
||||
}
|
||||
}
|
||||
if (empty($extras['api_key'])) {
|
||||
$errorContent = '机器人未启用。';
|
||||
}
|
||||
if (in_array($this->client['platform'], ['win', 'mac', 'web']) && !Base::judgeClientVersion("0.41.11", $this->client['version'])) {
|
||||
$errorContent = '当前客户端版本低(所需版本≥v0.41.11)。';
|
||||
}
|
||||
|
||||
if ($msg->reply_id > 0) {
|
||||
$replyMsg = WebSocketDialogMsg::find($msg->reply_id);
|
||||
$replyCommand = '';
|
||||
$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':
|
||||
try {
|
||||
$replyCommand = $this->extractCommand($replyMsg, true);
|
||||
} catch (Exception) {
|
||||
$errorContent = "引用消息解析失败。";
|
||||
}
|
||||
break;
|
||||
case 'file':
|
||||
$msgData = Base::json2array($replyMsg->getRawOriginal('msg'));
|
||||
$fileResult = TextExtractor::extractFile(public_path($msgData['path']));
|
||||
if (Base::isError($fileResult)) {
|
||||
$errorContent = $fileResult['msg'];
|
||||
} else {
|
||||
$replyCommand = $fileResult['data'];
|
||||
}
|
||||
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 {
|
||||
// 用户机器人
|
||||
if (str_starts_with($command, '/')) {
|
||||
return;
|
||||
}
|
||||
$userBot = UserBot::whereBotId($botUser->userid)->first();
|
||||
$webhookUrl = $userBot?->webhook_url;
|
||||
}
|
||||
@@ -532,15 +575,18 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
/**
|
||||
* 提取消息指令(提取消息内容)
|
||||
* @param WebSocketDialogMsg $msg
|
||||
* @param bool $isAiBot
|
||||
* @param bool $mention
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
private function extractCommand(WebSocketDialogMsg $msg, bool $mention = false)
|
||||
private function extractCommand(WebSocketDialogMsg $msg, bool $isAiBot = false, bool $mention = false)
|
||||
{
|
||||
if ($msg->type !== 'text') {
|
||||
return '';
|
||||
}
|
||||
$original = $msg->msg['text'];
|
||||
|
||||
$original = $msg->msg['text'] ?: '';
|
||||
if ($mention) {
|
||||
$original = preg_replace("/<span class=\"mention user\" data-id=\"(\d+)\">(.*?)<\/span>/", "", $original);
|
||||
}
|
||||
@@ -549,26 +595,94 @@ 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 '';
|
||||
if (!$isAiBot) {
|
||||
return trim(strip_tags($original));
|
||||
}
|
||||
return $command;
|
||||
|
||||
$contents = [];
|
||||
// 任务
|
||||
if (preg_match_all("/<span class=\"mention task\" data-id=\"(\d+)\">(.*?)<\/span>/", $original, $match)) {
|
||||
$taskIds = Base::newIntval($match[1]);
|
||||
foreach ($taskIds as $index => $taskId) {
|
||||
$taskInfo = ProjectTask::with(['content'])->whereId($taskId)->first();
|
||||
if (!$taskInfo) {
|
||||
throw new Exception("任务不存在或已被删除");
|
||||
}
|
||||
$taskName = addslashes($taskInfo->name) . " (ID:{$taskId})";
|
||||
$taskContext = implode("\n", $taskInfo->AIContext());
|
||||
$contents[] = "<task_content path=\"{$taskName}\">\n{$taskContext}\n</task_content>";
|
||||
$original = str_replace($match[0][$index], "'{$taskName}' (see below for task_content tag)", $original);
|
||||
}
|
||||
}
|
||||
// 文件、报告
|
||||
if (preg_match_all("/<a class=\"mention ([^'\"]*)\" href=\"([^\"']+?)\"[^>]*?>[~%]([^>]*)<\/a>/", $original, $match)) {
|
||||
$urlPaths = $match[2];
|
||||
foreach ($urlPaths as $index => $urlPath) {
|
||||
$pathTag = null;
|
||||
$pathName = null;
|
||||
$pathContent = null;
|
||||
// 文件
|
||||
if (preg_match("/single\/file\/(.*?)$/", $urlPath, $fileMatch)) {
|
||||
$fileInfo = FileContent::idOrCodeToContent($fileMatch[1]);
|
||||
if (!$fileInfo || !isset($fileInfo->content['url'])) {
|
||||
throw new Exception("文件不存在或已被删除");
|
||||
}
|
||||
$urlPath = public_path($fileInfo->content['url']);
|
||||
if (!file_exists($urlPath)) {
|
||||
throw new Exception("文件不存在或已被删除");
|
||||
}
|
||||
$fileResult = TextExtractor::extractFile($urlPath);
|
||||
if (Base::isError($fileResult)) {
|
||||
throw new Exception("文件读取失败:" . $fileResult['msg']);
|
||||
}
|
||||
$pathTag = "file_content";
|
||||
$pathName = addslashes($match[3][$index]) . " (ID:{$fileInfo->id})";
|
||||
$pathContent = $fileResult['data'];
|
||||
}
|
||||
// 报告
|
||||
elseif (preg_match("/single\/report\/detail\/(.*?)$/", $urlPath, $reportMatch)) {
|
||||
$reportInfo = Report::idOrCodeToContent($reportMatch[1]);
|
||||
if (!$reportInfo) {
|
||||
throw new Exception("报告不存在或已被删除");
|
||||
}
|
||||
$pathTag = "report_content";
|
||||
$pathName = addslashes($match[3][$index]) . " (ID:{$reportInfo->id})";
|
||||
$pathContent = $reportInfo->content;
|
||||
}
|
||||
if ($pathTag) {
|
||||
$contents[] = "<{$pathTag} path=\"{$pathName}\">\n{$pathContent}\n</{$pathTag}>";
|
||||
$original = str_replace($match[0][$index], "'{$pathName}' (see below for {$pathTag} tag)", $original);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($msg->msg['type'] !== 'md') {
|
||||
// 转换为Markdown
|
||||
try {
|
||||
$converter = new HtmlConverter();
|
||||
$original = $converter->convert($original);
|
||||
} catch (\Exception) {
|
||||
throw new Exception("Failed to convert HTML to Markdown");
|
||||
}
|
||||
}
|
||||
if ($contents) {
|
||||
// 添加tag内容
|
||||
$original .= "\n\n" . implode("\n\n", $contents);
|
||||
}
|
||||
return $original ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成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 +691,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
'type' => 'ai_prompt',
|
||||
])->value('value');
|
||||
if ($aiPrompt) {
|
||||
$system_message = $aiPrompt;
|
||||
$extras['system_message'] = $aiPrompt;
|
||||
}
|
||||
break;
|
||||
case "group":
|
||||
@@ -585,14 +699,16 @@ 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
|
||||
@@ -605,24 +721,16 @@ 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
|
||||
@@ -631,17 +739,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()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
4
bin/version.js
vendored
4
bin/version.js
vendored
File diff suppressed because one or more lines are too long
122
cmd
122
cmd
@@ -156,9 +156,9 @@ run_electron() {
|
||||
npm install
|
||||
fi
|
||||
if [ ! -d "./electron/node_modules" ]; then
|
||||
pushd electron
|
||||
pushd electron || exit
|
||||
npm install
|
||||
popd
|
||||
popd || exit
|
||||
fi
|
||||
#
|
||||
if [ -d "./electron/dist" ]; then
|
||||
@@ -178,8 +178,9 @@ run_electron() {
|
||||
|
||||
run_exec() {
|
||||
local container=$1
|
||||
local cmd=$2
|
||||
local name=`docker_name $container`
|
||||
shift 1
|
||||
local cmd=$@
|
||||
local name=$(docker_name "$container")
|
||||
if [ -z "$name" ]; then
|
||||
error "没有找到 $container 容器!"
|
||||
exit 1
|
||||
@@ -322,15 +323,26 @@ https_auto() {
|
||||
if [[ "$restart_nginx" == "y" ]]; then
|
||||
$COMPOSE up -d
|
||||
fi
|
||||
docker run -it --rm -v $(pwd):/work nginx:alpine sh "/work/bin/https"
|
||||
docker run -it --rm -v $(pwd):/work nginx:alpine sh /work/bin/https install
|
||||
if [[ 0 -eq $? ]]; then
|
||||
run_exec nginx "nginx -s reload"
|
||||
fi
|
||||
new_job="* 6 * * * docker run -it --rm -v $(pwd):/work nginx:alpine sh /work/bin/https renew"
|
||||
current_crontab=$(crontab -l 2>/dev/null)
|
||||
if ! echo "$current_crontab" | grep -v "https renew"; then
|
||||
echo "任务已存在,无需添加。"
|
||||
else
|
||||
crontab -l |{
|
||||
cat
|
||||
echo "$new_job"
|
||||
} | crontab -
|
||||
echo "任务已添加。"
|
||||
fi
|
||||
}
|
||||
|
||||
env_get() {
|
||||
local key=$1
|
||||
local value=`cat ${cur_path}/.env | grep "^$key=" | awk -F '=' '{print $2}'`
|
||||
local value=`cat ${cur_path}/.env | grep "^$key=" | awk -F '=' '{print $2}' | tr -d '\r\n'`
|
||||
echo "$value"
|
||||
}
|
||||
|
||||
@@ -405,14 +417,53 @@ 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
|
||||
# 安装composer依赖
|
||||
# 安装PHP依赖
|
||||
run_exec php "composer install"
|
||||
if [ ! -f "${cur_path}/vendor/autoload.php" ]; then
|
||||
run_exec php "composer config repo.packagist composer https://packagist.phpcomposer.com"
|
||||
@@ -424,45 +475,32 @@ if [ $# -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
[[ -z "$(env_get APP_KEY)" ]] && run_exec php "php artisan key:generate"
|
||||
# 设置生产模式
|
||||
switch_debug "false"
|
||||
# 检查数据库
|
||||
remaining=20
|
||||
while [ ! -f "${cur_path}/docker/mysql/data/$(env_get DB_DATABASE)/db.opt" ]; do
|
||||
((remaining=$remaining-1))
|
||||
if [ $remaining -lt 0 ]; then
|
||||
error "数据库初始化失败!"
|
||||
exit 1
|
||||
fi
|
||||
chmod -R 775 "${cur_path}/docker/mysql/data"
|
||||
done
|
||||
# 数据库迁移
|
||||
remaining=20
|
||||
while [ ! -f "${cur_path}/docker/mysql/data/$(env_get DB_DATABASE)/$(env_get DB_PREFIX)migrations.ibd" ]; do
|
||||
((remaining=$remaining-1))
|
||||
if [ $remaining -lt 0 ]; then
|
||||
error "数据库安装失败!"
|
||||
exit 1
|
||||
fi
|
||||
sleep 3
|
||||
run_exec php "php artisan migrate --seed"
|
||||
done
|
||||
# 设置初始化密码
|
||||
res=`run_exec mariadb "sh /etc/mysql/repassword.sh"`
|
||||
run_exec php "php artisan migrate --seed"
|
||||
# 启动其他容器
|
||||
$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
|
||||
run_mysql backup
|
||||
fi
|
||||
if [[ -z "$(arg_get local)" ]]; then
|
||||
git fetch --all
|
||||
git reset --hard origin/$(git branch | sed -n -e 's/^\* \(.*\)/\1/p')
|
||||
current_branch=$(git branch | sed -n -e 's/^\* \(.*\)/\1/p')
|
||||
db_changes=$(git diff --name-only HEAD..origin/$current_branch | grep -E "^database/")
|
||||
if [[ -n "$db_changes" ]]; then
|
||||
info "数据库有迁移变动,执行数据库备份..."
|
||||
run_mysql backup
|
||||
fi
|
||||
git reset --hard origin/$current_branch
|
||||
git pull
|
||||
run_exec php "composer update"
|
||||
else
|
||||
info "执行数据库备份..."
|
||||
run_mysql backup
|
||||
fi
|
||||
run_exec php "php artisan migrate"
|
||||
run_exec nginx "nginx -s reload"
|
||||
@@ -513,7 +551,7 @@ if [ $# -gt 0 ]; then
|
||||
success "修改成功"
|
||||
elif [[ "$1" == "repassword" ]]; then
|
||||
shift 1
|
||||
run_exec mariadb "sh /etc/mysql/repassword.sh \"$@\""
|
||||
run_exec mariadb "sh /etc/mysql/repassword.sh $@"
|
||||
elif [[ "$1" == "serve" ]] || [[ "$1" == "dev" ]] || [[ "$1" == "development" ]]; then
|
||||
shift 1
|
||||
run_compile dev
|
||||
@@ -539,9 +577,9 @@ if [ $# -gt 0 ]; then
|
||||
elif [[ "$1" == "npm" ]]; then
|
||||
shift 1
|
||||
npm $@
|
||||
cd electron
|
||||
pushd electron || exit
|
||||
npm $@
|
||||
cd ..
|
||||
popd || exit
|
||||
docker run --rm -it -v ${cur_path}/resources/mobile:/work -w /work --entrypoint=/bin/bash node:16 -c "npm $@"
|
||||
elif [[ "$1" == "doc" ]]; then
|
||||
shift 1
|
||||
|
||||
@@ -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,10 @@
|
||||
"mews/captcha": "^3.2.6",
|
||||
"orangehill/iseed": "^3.0.1",
|
||||
"overtrue/pinyin": "^4.0",
|
||||
"phpoffice/phppresentation": "^1.1",
|
||||
"phpoffice/phpword": "^1.3",
|
||||
"predis/predis": "^1.1.7",
|
||||
"smalot/pdfparser": "^2.11",
|
||||
"symfony/mailer": "^6.0"
|
||||
},
|
||||
"require-dev": {
|
||||
@@ -83,7 +89,10 @@
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
|
||||
1451
composer.lock
generated
1451
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateReportLinksTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('report_links', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('rid')->nullable()->default(0)->index()->comment('报告ID');
|
||||
$table->integer('num')->nullable()->default(0)->comment('累计访问');
|
||||
$table->string('code')->nullable()->default('')->comment('链接码');
|
||||
$table->bigInteger('userid')->nullable()->default(0)->index()->comment('会员ID');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('report_links');
|
||||
}
|
||||
}
|
||||
@@ -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,10 @@ services:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.2"
|
||||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
mariadb:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
nginx:
|
||||
@@ -41,20 +43,16 @@ services:
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.3"
|
||||
links:
|
||||
- php
|
||||
- office
|
||||
- fileview
|
||||
- drawio-webapp
|
||||
- drawio-export
|
||||
- minder
|
||||
- okr
|
||||
- ai
|
||||
restart: unless-stopped
|
||||
|
||||
redis:
|
||||
container_name: "dootask-redis-${APP_ID}"
|
||||
image: "redis:alpine"
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.4"
|
||||
@@ -73,6 +71,11 @@ services:
|
||||
MYSQL_DATABASE: "${DB_DATABASE}"
|
||||
MYSQL_USER: "${DB_USERNAME}"
|
||||
MYSQL_PASSWORD: "${DB_PASSWORD}"
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "${DB_USERNAME}", "-p${DB_PASSWORD}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.5"
|
||||
@@ -84,7 +87,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
|
||||
@@ -100,7 +105,7 @@ services:
|
||||
|
||||
fileview:
|
||||
container_name: "dootask-fileview-${APP_ID}"
|
||||
image: "kuaifan/fileview:4.2.0-SNAPSHOT-RC25"
|
||||
image: "kuaifan/fileview:4.4.0-3"
|
||||
environment:
|
||||
KK_CONTEXT_PATH: "/fileview"
|
||||
KK_OFFICE_PREVIEW_SWITCH_DISABLED: true
|
||||
@@ -123,8 +128,6 @@ services:
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.8"
|
||||
depends_on:
|
||||
- drawio-export
|
||||
restart: unless-stopped
|
||||
|
||||
drawio-export:
|
||||
@@ -161,21 +164,18 @@ services:
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.11"
|
||||
depends_on:
|
||||
- mariadb
|
||||
restart: unless-stopped
|
||||
|
||||
ai:
|
||||
container_name: "dootask-ai-${APP_ID}"
|
||||
image: "kuaifan/dootask-ai:0.2.0"
|
||||
image: "kuaifan/dootask-ai:0.3.5"
|
||||
environment:
|
||||
REDIS_HOST: "${REDIS_HOST}"
|
||||
REDIS_PORT: "${REDIS_PORT}"
|
||||
TIMEOUT: 600
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.12"
|
||||
depends_on:
|
||||
- redis
|
||||
restart: unless-stopped
|
||||
|
||||
okr:
|
||||
@@ -183,7 +183,7 @@ services:
|
||||
image: "kuaifan/doookr:0.4.5"
|
||||
environment:
|
||||
TZ: "${TIMEZONE:-PRC}"
|
||||
DOO_TASK_URL: "http://${APP_IPPR}.3"
|
||||
DOO_TASK_URL: "http://nginx"
|
||||
MYSQL_HOST: "${DB_HOST}"
|
||||
MYSQL_PORT: "${DB_PORT}"
|
||||
MYSQL_DBNAME: "${DB_DATABASE}"
|
||||
@@ -195,8 +195,6 @@ services:
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.13"
|
||||
depends_on:
|
||||
- mariadb
|
||||
restart: unless-stopped
|
||||
|
||||
face:
|
||||
@@ -213,14 +211,27 @@ services:
|
||||
MYSQL_PASSWORD: "${DB_PASSWORD}"
|
||||
MYSQL_DB_NAME: "${DB_DATABASE}"
|
||||
DB_PREFIX: "${DB_PREFIX}"
|
||||
REPORT_API: "http://${APP_IPPR}.3:80/api/public/checkin/report"
|
||||
depends_on:
|
||||
- mariadb
|
||||
REPORT_API: "http://nginx/api/public/checkin/report"
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.14"
|
||||
restart: unless-stopped
|
||||
|
||||
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(_ => { });
|
||||
}
|
||||
|
||||
16
electron/electron-preload.js
vendored
16
electron/electron-preload.js
vendored
@@ -33,27 +33,25 @@ contextBridge.exposeInMainWorld(
|
||||
request: (msg, callback, error) => {
|
||||
msg.reqId = reqId++;
|
||||
reqInfo[msg.reqId] = {callback: callback, error: error};
|
||||
|
||||
//TODO Maybe a special function for this better than this hack?
|
||||
//File watch special case where the callback is called multiple times
|
||||
if (msg.action == 'watchFile') {
|
||||
fileChangedListeners[msg.path] = msg.listener;
|
||||
delete msg.listener;
|
||||
}
|
||||
|
||||
ipcRenderer.send('rendererReq', msg);
|
||||
},
|
||||
registerMsgListener: function (action, callback) {
|
||||
ipcRenderer.on(action, function (event, args) {
|
||||
callback(args);
|
||||
});
|
||||
},
|
||||
|
||||
sendMessage: function (action, args) {
|
||||
ipcRenderer.send(action, args);
|
||||
},
|
||||
sendAsync: function (action, args) {
|
||||
return ipcRenderer.invoke(action, args)
|
||||
},
|
||||
|
||||
listener: function (action, callback) {
|
||||
ipcRenderer.on(action, function (event, args) {
|
||||
callback(args);
|
||||
});
|
||||
},
|
||||
listenOnce: function (action, callback) {
|
||||
ipcRenderer.once(action, function (event, args) {
|
||||
callback(args);
|
||||
|
||||
92
electron/electron.js
vendored
92
electron/electron.js
vendored
@@ -163,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();
|
||||
}
|
||||
@@ -434,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 {
|
||||
@@ -497,8 +504,15 @@ function createMediaWindow(args, type = 'image') {
|
||||
mediaWindow.addListener('close', event => {
|
||||
if (!willQuitApp) {
|
||||
event.preventDefault()
|
||||
mediaWindow.webContents.send('on-close');
|
||||
mediaWindow.hide();
|
||||
if (mediaWindow.isFullScreen()) {
|
||||
mediaWindow.once('leave-full-screen', () => {
|
||||
mediaWindow.hide();
|
||||
})
|
||||
mediaWindow.setFullScreen(false)
|
||||
} else {
|
||||
mediaWindow.webContents.send('on-close');
|
||||
mediaWindow.hide();
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -556,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
|
||||
}
|
||||
@@ -577,15 +589,15 @@ function createWebTabWindow(args) {
|
||||
autoHideMenuBar: true,
|
||||
titleBarStyle: 'hidden',
|
||||
titleBarOverlay,
|
||||
backgroundColor: nativeTheme.shouldUseDarkColors ? '#3B3B3D' : '#EFF0F4',
|
||||
webPreferences: Object.assign({
|
||||
backgroundColor: nativeTheme.shouldUseDarkColors ? '#575757' : '#FFFFFF',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'electron-preload.js'),
|
||||
webSecurity: true,
|
||||
nodeIntegration: true,
|
||||
contextIsolation: true,
|
||||
nativeWindowOpen: true
|
||||
}, webPreferences),
|
||||
}, config))
|
||||
},
|
||||
}, userConf.get('webTabWindow', {})))
|
||||
|
||||
webTabWindow.on('resize', () => {
|
||||
resizeWebTab(0)
|
||||
@@ -650,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')
|
||||
@@ -871,7 +888,7 @@ function monitorThemeChanges() {
|
||||
preloadWindow?.setBackgroundColor(backgroundColor);
|
||||
mediaWindow?.setBackgroundColor(backgroundColor);
|
||||
childWindow.some(({browser}) => browser.setBackgroundColor(backgroundColor))
|
||||
webTabWindow?.setBackgroundColor(nativeTheme.shouldUseDarkColors ? '#3B3B3D' : '#EFF0F4')
|
||||
webTabWindow?.setBackgroundColor(nativeTheme.shouldUseDarkColors ? '#575757' : '#FFFFFF')
|
||||
// 通知所有窗口
|
||||
BrowserWindow.getAllWindows().forEach(window => {
|
||||
window.webContents.send('systemThemeChanged', {
|
||||
@@ -1244,13 +1261,17 @@ ipcMain.on('windowMax', (event) => {
|
||||
})
|
||||
|
||||
/**
|
||||
* 给主窗口发送信息
|
||||
* @param args {channel, data}
|
||||
* 给所有窗口广播指令(除了本身)
|
||||
* @param args {type, payload}
|
||||
*/
|
||||
ipcMain.on('sendForwardMain', (event, args) => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send(args.channel, args.data)
|
||||
}
|
||||
ipcMain.on('broadcastCommand', (event, args) => {
|
||||
const channel = args.channel || args.command
|
||||
const payload = args.payload || args.data
|
||||
BrowserWindow.getAllWindows().forEach(window => {
|
||||
if (window.webContents.id !== event.sender.id) {
|
||||
window.webContents.send(channel, payload)
|
||||
}
|
||||
})
|
||||
event.returnValue = "ok"
|
||||
})
|
||||
|
||||
@@ -1394,15 +1415,17 @@ ipcMain.handle('getStore', (event, args) => {
|
||||
//================================================================
|
||||
|
||||
let autoUpdating = 0
|
||||
autoUpdater.logger = loger
|
||||
autoUpdater.autoDownload = false
|
||||
autoUpdater.autoInstallOnAppQuit = true
|
||||
autoUpdater.on('update-available', info => {
|
||||
mainWindow.webContents.send("updateAvailable", info)
|
||||
})
|
||||
autoUpdater.on('update-downloaded', info => {
|
||||
mainWindow.webContents.send("updateDownloaded", info)
|
||||
})
|
||||
if (autoUpdater) {
|
||||
autoUpdater.logger = loger
|
||||
autoUpdater.autoDownload = false
|
||||
autoUpdater.autoInstallOnAppQuit = true
|
||||
autoUpdater.on('update-available', info => {
|
||||
mainWindow.webContents.send("updateAvailable", info)
|
||||
})
|
||||
autoUpdater.on('update-downloaded', info => {
|
||||
mainWindow.webContents.send("updateDownloaded", info)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查更新
|
||||
@@ -1412,6 +1435,9 @@ ipcMain.on('updateCheckAndDownload', (event, args) => {
|
||||
if (autoUpdating + 3600 > utils.dayjs().unix()) {
|
||||
return // 限制1小时仅执行一次
|
||||
}
|
||||
if (!autoUpdater) {
|
||||
return
|
||||
}
|
||||
if (args.provider) {
|
||||
autoUpdater.setFeedURL(args)
|
||||
}
|
||||
@@ -1477,7 +1503,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-->
|
||||
|
||||
1
electron/notarize.js
vendored
1
electron/notarize.js
vendored
@@ -12,6 +12,7 @@ exports.default = async function notarizing(context) {
|
||||
}
|
||||
|
||||
return await notarize({
|
||||
tool: "notarytool",
|
||||
appBundleId: config.build.appId,
|
||||
appPath: `${appOutDir}/${appName}.app`,
|
||||
appleId: APPLEID,
|
||||
|
||||
@@ -26,17 +26,17 @@
|
||||
"url": "https://github.com/kuaifan/dootask.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^7.6.0",
|
||||
"@electron-forge/maker-deb": "^7.6.0",
|
||||
"@electron-forge/maker-rpm": "^7.6.0",
|
||||
"@electron-forge/maker-squirrel": "^7.6.0",
|
||||
"@electron-forge/maker-zip": "^7.6.0",
|
||||
"@electron-forge/cli": "^7.7.0",
|
||||
"@electron-forge/maker-deb": "^7.7.0",
|
||||
"@electron-forge/maker-rpm": "^7.7.0",
|
||||
"@electron-forge/maker-squirrel": "^7.7.0",
|
||||
"@electron-forge/maker-zip": "^7.7.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"electron": "^33.2.1",
|
||||
"electron": "^34.3.4",
|
||||
"electron-builder": "^25.1.8",
|
||||
"electron-notarize": "^1.2.2",
|
||||
"form-data": "^4.0.1",
|
||||
"inquirer": "^8.2.0",
|
||||
"inquirer": "^12.4.2",
|
||||
"ora": "^4.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -108,7 +108,14 @@
|
||||
"target": "dmg",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
"arm64",
|
||||
"universal"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "pkg",
|
||||
"arch": [
|
||||
"universal"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -37,13 +37,16 @@
|
||||
const {ipcRenderer} = require('electron');
|
||||
|
||||
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(",")
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
消息内容最大不能超过(*)字
|
||||
消息发送保存失败
|
||||
说话时间太短
|
||||
请选择转发对话或成员
|
||||
请选择对话或成员
|
||||
发送成功
|
||||
不是发送人
|
||||
文件不存在
|
||||
@@ -171,9 +171,9 @@ LDAP 用户禁止修改邮箱
|
||||
部门不存在或已被删除
|
||||
最多只能创建(*)个部门
|
||||
上级部门不存在或已被删除
|
||||
上级部门层级错误
|
||||
部门层级最多只能创建(*)级
|
||||
每个部门最多只能创建(*)个子部门
|
||||
含有子部门无法修改上级部门
|
||||
含有子部门无法删除
|
||||
请选择正确的部门负责人
|
||||
新建成功
|
||||
此功能未开启,请联系管理员开启
|
||||
@@ -255,7 +255,8 @@ LDAP 用户禁止修改邮箱
|
||||
此消息不支持设待办
|
||||
仅支持设此待办人员【(*)】取消
|
||||
转发成功
|
||||
已超过(*)小时,此消息不能撤回
|
||||
已超过(*),此消息不可撤回
|
||||
已超过(*),此消息不可修改
|
||||
文件分享错误
|
||||
获取会话失败
|
||||
消息不存在
|
||||
@@ -467,6 +468,7 @@ OKR提醒
|
||||
缺卡提醒
|
||||
打卡提醒
|
||||
任务待领取
|
||||
指令解析失败。
|
||||
非常抱歉,我不是你的机器人,无法完成你的指令。
|
||||
您没有创建机器人。
|
||||
机器人不存在。
|
||||
@@ -489,6 +491,7 @@ webhook地址最长仅支持255个字符。
|
||||
不支持的指令
|
||||
机器人未启用。
|
||||
当前客户端版本低(所需版本≥(*))。
|
||||
引用消息解析失败。
|
||||
审批结果
|
||||
审批评论通知
|
||||
审批通知
|
||||
@@ -802,3 +805,42 @@ webhook地址最长仅支持255个字符。
|
||||
更新子任务标签
|
||||
|
||||
AI机器人不存在
|
||||
|
||||
内容不存在
|
||||
长文本
|
||||
|
||||
选择模型
|
||||
当前对话不支持
|
||||
会话不存在或已被删除
|
||||
开启新会话
|
||||
历史会话
|
||||
|
||||
未找到默认模型
|
||||
思考中...
|
||||
|
||||
请先填写 Base URL
|
||||
获取失败
|
||||
|
||||
任务超期未完成
|
||||
每个用户最多只能负责(*)个部门
|
||||
不能选择自己的子部门作为上级部门
|
||||
|
||||
转文字失败
|
||||
状态[(*)]设置错误,状态负责人[(*)]不在项目成员内
|
||||
|
||||
(*)天(*)小时(*)分钟
|
||||
(*)天(*)小时
|
||||
(*)天(*)分钟
|
||||
(*)天
|
||||
(*)小时(*)分钟
|
||||
(*)小时
|
||||
(*)分钟
|
||||
|
||||
任务不存在或已被删除
|
||||
文件不存在或已被删除
|
||||
报告不存在或已被删除
|
||||
文件读取失败:(*)
|
||||
|
||||
请输入删除备注
|
||||
删除备注长度限制(*)个字
|
||||
系统机器人不能删除
|
||||
|
||||
@@ -547,7 +547,6 @@ SMTP服务器
|
||||
空白模板
|
||||
立即上传
|
||||
立即升级
|
||||
立即登录
|
||||
签到功能
|
||||
签到数据
|
||||
签到日期
|
||||
@@ -1025,7 +1024,6 @@ Pro版
|
||||
隐藏共享文件
|
||||
单聊
|
||||
显示文件
|
||||
ID、任务名...
|
||||
仅显示我的
|
||||
语音
|
||||
群头像
|
||||
@@ -1221,7 +1219,6 @@ OKR 结果分析
|
||||
AI 机器人
|
||||
任务相关
|
||||
请填写名称!
|
||||
访问OpenAI网站查看
|
||||
使用代理
|
||||
支持 http 或 socks 代理
|
||||
例如:http://proxy.com 或 socks5://proxy.com
|
||||
@@ -1359,7 +1356,6 @@ APP 推送
|
||||
搜索项目名称
|
||||
服务器版本过低,请升级服务器。
|
||||
不显示原发送者信息
|
||||
转发给
|
||||
留言
|
||||
多选
|
||||
@我的
|
||||
@@ -1520,6 +1516,7 @@ License Key
|
||||
网络异常,请重试。
|
||||
请求失败,请重试。
|
||||
任务待领取
|
||||
指令解析失败。
|
||||
非常抱歉,我不是你的机器人,无法完成你的指令。
|
||||
您没有创建机器人。
|
||||
机器人不存在。
|
||||
@@ -1545,6 +1542,7 @@ API接口文档
|
||||
不支持的指令
|
||||
机器人未启用。
|
||||
当前客户端版本低(所需版本≥(*))。
|
||||
引用消息解析失败。
|
||||
审批结果
|
||||
审批评论通知
|
||||
审批通知
|
||||
@@ -1705,7 +1703,6 @@ WiFi签到延迟时长为±1分钟。
|
||||
选择群组
|
||||
输入关键词搜索群
|
||||
仅支持选择个人群转为部门群
|
||||
含有子部门无法修改上级部门
|
||||
删除部门
|
||||
你确定要删除【(*)】部门吗?
|
||||
注意:此操作不可恢复,部门下的成员将移至默认部门。
|
||||
@@ -1903,3 +1900,129 @@ WiFi签到延迟时长为±1分钟。
|
||||
请选择示例标签
|
||||
全部保存成功
|
||||
|
||||
消息详情
|
||||
长文本
|
||||
你确定要创建任务吗?
|
||||
你确定要创建子任务吗?
|
||||
|
||||
正在处理,请稍后再试...
|
||||
|
||||
开启麦克风失败!
|
||||
开启摄像头失败!
|
||||
|
||||
群机器人
|
||||
群成员 ((*)人)
|
||||
此消息已经过期
|
||||
|
||||
DeepSeek大语言模型算法是北京深度求索人工智能基础技术研究有限公司推出的深度合成服务算法。
|
||||
API请求的基础URL路径,如果没有请留空
|
||||
|
||||
欢迎词
|
||||
仪表盘欢迎词,(*)代表用户昵称
|
||||
思考过程
|
||||
隐藏翻译
|
||||
重新翻译
|
||||
|
||||
当前客户端不支持该指令
|
||||
默认模型
|
||||
|
||||
与(*)会话历史
|
||||
当前
|
||||
新会话
|
||||
|
||||
模型温度,低则保守,高则多样
|
||||
例如:0.7,范围:0-1,默认:0.7
|
||||
|
||||
模型列表
|
||||
一行一个模型名称
|
||||
请选择默认模型
|
||||
可选数据来自模型列表
|
||||
|
||||
使用默认模型列表
|
||||
未知操作
|
||||
获取失败
|
||||
获取成功
|
||||
|
||||
Grok是由xAI开发的生成式人工智能聊天机器人,旨在通过实时回答用户问题来提供帮助。
|
||||
Ollama 是一个轻量级、可扩展的框架,旨在让用户能够在本地机器上构建和运行大型语言模型。
|
||||
|
||||
AI 列表
|
||||
AI 设置
|
||||
思考中...
|
||||
|
||||
请先填写 Base URL
|
||||
|
||||
例如:http://proxy.com
|
||||
API请求的URL路径
|
||||
如果没有请留空
|
||||
获取本地模型列表
|
||||
|
||||
任务超期未完成
|
||||
打开部门群
|
||||
群组 ID
|
||||
最多选择(*)个部门
|
||||
已共享
|
||||
联系人
|
||||
未搜到跟「(*)」相关的结果
|
||||
暂无相关结果
|
||||
请输入关键字
|
||||
请输入关键字搜索
|
||||
正在拼命搜索...
|
||||
|
||||
加载失败,请重启软件
|
||||
发送原语音
|
||||
转文字失败
|
||||
|
||||
请稍后再试...
|
||||
选择识别语言
|
||||
自动识别
|
||||
选择翻译结果
|
||||
不翻译结果
|
||||
即将到期
|
||||
|
||||
创建任务
|
||||
|
||||
项目已归档,无法查看
|
||||
默认:90
|
||||
出差
|
||||
仅未读
|
||||
仅已读
|
||||
汇报状态
|
||||
无法录音:NotFoundError: Requested device not found,无可用麦克风
|
||||
关闭窗口
|
||||
关闭提示
|
||||
网络连接失败
|
||||
重试
|
||||
检查
|
||||
|
||||
撤回消息限制
|
||||
消息发出后的可撤回时长。
|
||||
系统管理员除外
|
||||
修改消息限制
|
||||
消息发出后的可修改时长。
|
||||
汇报部门
|
||||
确认转发
|
||||
确认发送
|
||||
|
||||
分享到消息
|
||||
分享报告到消息
|
||||
确认分享
|
||||
每次最多分享(*)个
|
||||
|
||||
标注人员:(*) (ID: (*))
|
||||
标注人员不存在
|
||||
附言
|
||||
|
||||
任务不存在或已被删除
|
||||
文件不存在或已被删除
|
||||
报告不存在或已被删除
|
||||
文件读取失败:(*)
|
||||
独立窗口显示
|
||||
在消息中显示
|
||||
|
||||
添加机器人
|
||||
消息保留
|
||||
您没有创建机器人
|
||||
清理时间
|
||||
请输入备注原因
|
||||
删除机器人:(*)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user