Compare commits

..

273 Commits

Author SHA1 Message Date
kuaifan
29a0d22938 build 2025-03-13 01:40:11 +08:00
kuaifan
635cc04c50 perf: 优化消息定位 2025-03-13 01:38:35 +08:00
kuaifan
bc5343652b perf: 优化消息性能 2025-03-12 22:41:41 +08:00
kuaifan
03f140fe3b fix: 看不到未读消息定位提醒 2025-03-12 14:19:07 +08:00
kuaifan
3e4a119f61 build 2025-03-12 00:18:12 +08:00
kuaifan
3b7bcbc14a no message 2025-03-11 17:41:12 +08:00
kuaifan
3c49e96e02 no message 2025-03-11 16:24:31 +08:00
kuaifan
5be209ab59 no message 2025-03-11 15:53:53 +08:00
kuaifan
56ea048ab3 no message 2025-03-11 10:21:44 +08:00
kuaifan
9fc0bd0439 no message 2025-03-10 23:12:28 +08:00
kuaifan
1c2798cbf4 perf: 优化消息定位 2025-03-10 23:06:30 +08:00
kuaifan
9d8af2eaab perf: 优化MD消息 2025-03-10 22:38:42 +08:00
kuaifan
bba1e0d12f fix: 会话内消息搜索布局错位 2025-03-10 20:52:52 +08:00
kuaifan
c060e60e4a fix: 流程设置翻译不统一 2025-03-10 20:51:58 +08:00
kuaifan
1c504bd899 no message 2025-03-10 00:12:05 +08:00
kuaifan
b617648bd8 no message 2025-03-09 23:46:01 +08:00
kuaifan
e849c7a34f build 2025-03-09 23:11:14 +08:00
kuaifan
f6dd1ce98e no message 2025-03-09 23:02:35 +08:00
kuaifan
9c78db8d45 no message 2025-03-09 22:52:38 +08:00
kuaifan
5154348cf9 perf: 优化发送语音效果 2025-03-09 22:41:15 +08:00
kuaifan
4521cea3b4 perf: 优化发送语音效果 2025-03-09 19:54:09 +08:00
kuaifan
0ff1ac7743 no message 2025-03-09 18:54:33 +08:00
kuaifan
277a751ed4 no message 2025-03-09 18:40:03 +08:00
kuaifan
96be2a86ca no message 2025-03-09 18:17:18 +08:00
kuaifan
f28bff569a no message 2025-03-09 15:49:56 +08:00
kuaifan
e34aa77a54 perf: 录音转文字支持自定义语言 2025-03-09 15:32:38 +08:00
kuaifan
e53b65496f perf: 录音转文字支持自定义语言 2025-03-09 11:33:37 +08:00
kuaifan
f6ee630615 no message 2025-03-08 19:11:17 +08:00
kuaifan
ec2e1e3152 no message 2025-03-08 17:56:09 +08:00
kuaifan
6cffe9baed perf: 优化ES模块 2025-03-08 16:39:35 +08:00
kuaifan
b63df27409 perf: 优化emoji表情 2025-03-08 15:50:35 +08:00
kuaifan
617c466ac0 perf: 按住Ctrl/Command键可连续选择表情 2025-03-08 12:13:25 +08:00
kuaifan
ed8e443f3a perf: 优化ES模块 2025-03-08 12:12:48 +08:00
kuaifan
58cb49b125 perf: 优化ES模块 2025-03-08 10:15:58 +08:00
kuaifan
7dd5baa9ec fix: 定位签到失败的问题 2025-03-07 23:43:39 +08:00
kuaifan
bbf9107560 perf: md消息支持html代码 2025-03-07 23:23:51 +08:00
kuaifan
be527355ee no message 2025-03-07 22:15:27 +08:00
kuaifan
c866500120 perf: 优化脚本 2025-03-07 16:18:06 +08:00
kuaifan
3e2a40aaa0 perf: 优化安装命令 2025-03-07 15:13:19 +08:00
kuaifan
eef9fa56c6 perf: 优化ES索引名称 2025-03-07 12:57:37 +08:00
zzzzzhy
945d84dbc4 添加es证书配置 2025-03-07 12:57:37 +08:00
kuaifan
d353d33107 no message 2025-03-07 12:57:37 +08:00
kuaifan
f54bad5d79 no message 2025-03-06 16:16:43 +08:00
kuaifan
b605c70e91 no message 2025-03-06 14:52:40 +08:00
kuaifan
1752e88c42 no message 2025-03-05 15:19:38 +08:00
kuaifan
e2718a39a0 no message 2025-03-05 10:19:28 +08:00
kuaifan
25298ac69e build 2025-03-05 08:33:52 +08:00
kuaifan
cf9f389f75 no message 2025-03-05 08:23:08 +08:00
kuaifan
567c75830a perf: 新增录音转文字 2025-03-05 01:52:37 +08:00
kuaifan
7b1d352c95 perf: 新增录音转文字 2025-03-05 01:22:12 +08:00
kuaifan
4fa54381a6 no message 2025-03-04 20:12:32 +08:00
kuaifan
9c91f7cf83 no message 2025-03-04 19:56:13 +08:00
kuaifan
edd5cd1ca1 perf: 优化数据排序 2025-03-04 19:24:13 +08:00
kuaifan
f2ec6ad05e no message 2025-03-04 18:30:23 +08:00
kuaifan
a04ef4ac38 no message 2025-03-04 16:58:17 +08:00
kuaifan
43b3d1d379 no message 2025-03-04 16:46:45 +08:00
kuaifan
b65fdeacc2 no message 2025-03-04 09:33:26 +08:00
kuaifan
622fe1e5d9 no message 2025-03-04 08:44:11 +08:00
kuaifan
a6c7c0c7ad fix: 全屏预览图片关闭窗口 2025-03-04 07:38:06 +08:00
kuaifan
e5c8748b75 no message 2025-03-04 06:35:32 +08:00
kuaifan
f096d71cc1 no message 2025-03-03 23:45:42 +08:00
kuaifan
d73a152a36 no message 2025-03-03 22:48:09 +08:00
kuaifan
f4e6fd060e no message 2025-03-03 21:00:04 +08:00
kuaifan
c78ca1de5d no message 2025-03-03 18:11:32 +08:00
kuaifan
2b219c7256 no message 2025-03-03 14:53:45 +08:00
kuaifan
6ffa651742 no message 2025-03-03 13:16:03 +08:00
kuaifan
cb3b22a4bf fix: 点击排序导致任务不显示的情况 2025-03-03 12:51:33 +08:00
kuaifan
145bfdb0e9 no message 2025-03-03 12:49:04 +08:00
kuaifan
8c7b0c502d no message 2025-03-03 11:59:02 +08:00
kuaifan
684bf12a5c no message 2025-03-03 10:27:56 +08:00
kuaifan
aaa75aff14 build 2025-03-03 08:32:50 +08:00
kuaifan
f03600bd65 perf: 添加全局搜索功能 2025-03-03 08:20:17 +08:00
kuaifan
1c4c4fe3fb perf: 添加全局搜索功能 2025-03-03 07:01:49 +08:00
kuaifan
5e46b2cd1a perf: 添加全局搜索功能 2025-03-02 23:43:18 +08:00
kuaifan
027db7c0ec perf: 添加全局搜索功能 2025-03-02 19:00:09 +08:00
kuaifan
5bb17ddc6b no message 2025-03-02 18:59:54 +08:00
kuaifan
e8edd74bc3 no message 2025-03-01 23:59:51 +08:00
kuaifan
ed064a825a perf: 优化消息搜索 2025-03-01 23:59:14 +08:00
kuaifan
32c232a0b5 perf: 优化消息搜索 2025-03-01 20:54:02 +08:00
kuaifan
c2fd747c45 no message 2025-03-01 20:39:13 +08:00
kuaifan
9148853f2c perf: 团队管理支持调整部门区域尺寸 2025-03-01 14:51:07 +08:00
kuaifan
23d0f50a3d perf: 任务详情支持调整聊天区域尺寸 2025-03-01 14:50:44 +08:00
kuaifan
36cdf87bfe perf: 优化团队部门支持3级部门 2025-03-01 13:41:10 +08:00
kuaifan
cfd2e1fd7b perf: 可见群组ID 2025-03-01 12:06:03 +08:00
kuaifan
3cafac99ff perf: 支持在团队管理打开群聊 2025-03-01 11:54:20 +08:00
kuaifan
1dd4e8da71 perf: 优化回复消息自动@逻辑 2025-03-01 11:19:50 +08:00
kuaifan
543015a36e perf: 转发预览隐藏表情回应部分 2025-03-01 10:14:51 +08:00
kuaifan
2efdfc4b1f no message 2025-03-01 00:34:43 +08:00
kuaifan
7234d9307e no message 2025-03-01 00:21:21 +08:00
kuaifan
769ce1ce7c no message 2025-02-28 22:49:45 +08:00
kuaifan
62c1d5783e perf: 优化任务日志 2025-02-28 22:38:11 +08:00
kuaifan
a6bd4a2ffe no message 2025-02-28 21:24:09 +08:00
kuaifan
f1a9077b7e no message 2025-02-28 21:20:36 +08:00
kuaifan
2c3e80bd8f perf: 已删除任务支持按标签搜索 2025-02-28 21:07:54 +08:00
kuaifan
e52d066fb0 perf: 归档任务支持按标签搜索 2025-02-28 21:07:32 +08:00
kuaifan
5279d57018 perf: 项目面板添加按标签筛选 2025-02-28 20:54:25 +08:00
kuaifan
25e5eb4427 perf: 优化 AI 提示词 2025-02-26 20:50:17 +08:00
kuaifan
b01d5ce8c4 perf: 优化 AI 设置 2025-02-26 20:12:23 +08:00
kuaifan
ff41f5c041 no message 2025-02-26 11:58:53 +08:00
kuaifan
dd0770a93f no message 2025-02-25 21:36:55 +08:00
kuaifan
9a3e76fff3 no message 2025-02-25 21:08:54 +08:00
kuaifan
7c867578ee build 2025-02-25 21:08:40 +08:00
kuaifan
d543c27000 perf: 工作报告支持查看仅未读 2025-02-25 20:39:22 +08:00
kuaifan
a8be330baa perf: AI 支持引用文件 2025-02-25 20:31:13 +08:00
kuaifan
c128c58110 perf: 优化图文消息 2025-02-25 19:51:07 +08:00
kuaifan
e32a3887cd perf: 优化文本信息复制 2025-02-25 17:47:46 +08:00
kuaifan
94932c7486 perf: 优化图文消息 2025-02-25 17:38:12 +08:00
kuaifan
a1920745fb perf: 优化样式 2025-02-25 11:43:48 +08:00
kuaifan
51e8f9555e perf: 无法再AI机器人页面看到模型的问题 2025-02-25 11:37:51 +08:00
kuaifan
213ab8418b fix: 首次跟ai聊天没有记录的问题 2025-02-25 11:14:41 +08:00
kuaifan
707f1dd6cb no message 2025-02-24 10:41:35 +08:00
kuaifan
125ce036cd perf: 优化MD消息过长处理 2025-02-24 09:12:12 +08:00
kuaifan
172c562a71 build 2025-02-24 00:08:50 +08:00
kuaifan
80bbe6711c no message 2025-02-24 00:07:31 +08:00
kuaifan
3f56c64086 perf: 优化AI支持分析指定文件 2025-02-23 23:55:02 +08:00
kuaifan
e6167119e0 no message 2025-02-22 20:27:27 +08:00
kuaifan
368fae5f32 perf: 支持在AI对话中直接引用任务提问 2025-02-22 20:27:10 +08:00
kuaifan
6ae46cf7bb perf: 支持在AI对话中直接引用任务提问 2025-02-22 17:49:11 +08:00
kuaifan
e97806c85b no message 2025-02-22 17:49:04 +08:00
kuaifan
f31e88bed1 no message 2025-02-22 12:14:03 +08:00
kuaifan
6bd20038f9 no message 2025-02-22 11:29:46 +08:00
kuaifan
30cfb1200d no message 2025-02-22 11:26:16 +08:00
kuaifan
154e0039d1 perf: 优化 AI 参数 2025-02-22 11:13:16 +08:00
kuaifan
a8f3b02ee7 perf: 优化 Ollama AI 2025-02-22 01:29:54 +08:00
kuaifan
b3e83e13bc perf: 优化设置 2025-02-22 00:59:52 +08:00
kuaifan
d0a0e77c44 perf: 优化设置 2025-02-22 00:45:20 +08:00
kuaifan
a14896307f no message 2025-02-21 23:26:54 +08:00
kuaifan
976b300277 perf: 优化AI设置 2025-02-21 23:26:14 +08:00
kuaifan
ccbd873204 perf: 优化AI设置 2025-02-21 22:15:59 +08:00
kuaifan
9c1482f9e9 feat: 添加 Grok AI、Ollama AI 2025-02-21 17:04:59 +08:00
kuaifan
5a7f4efa91 feat: 添加 Grok AI、Ollama AI 2025-02-21 12:08:54 +08:00
kuaifan
f78c4a1fb0 perf: 优化AI设置 2025-02-21 11:37:39 +08:00
kuaifan
db6500369f perf: 优化AI消息 2025-02-20 01:12:48 +08:00
kuaifan
9e4beaa317 perf: 表情回复时更新对话列表 2025-02-14 21:14:15 +08:00
kuaifan
afd021737a build 2025-02-14 20:41:04 +08:00
kuaifan
3982ed56f7 no message 2025-02-14 15:15:36 +08:00
kuaifan
df4a01a7f9 perf: onlyoffice 支持打开超过100m的文件 2025-02-14 15:07:55 +08:00
kuaifan
a6fac96ec1 perf: 优化点击上传列表效果 2025-02-14 15:06:17 +08:00
kuaifan
8ed9186ff4 perf: AI支持自定义模型列表 2025-02-14 01:07:32 +08:00
kuaifan
821df75d4b fix: 撤回消息是消息列表不更新的情况 2025-02-13 21:21:32 +08:00
kuaifan
0c09a2445c build 2025-02-12 21:40:21 +08:00
kuaifan
e6983e858d no message 2025-02-12 21:33:10 +08:00
kuaifan
f8b69df955 fix: 修复偶现的是子窗口出现身份丢失的情况 2025-02-12 21:09:54 +08:00
kuaifan
15370a93c7 perf: 优化查看长消息内容 2025-02-12 20:46:18 +08:00
kuaifan
bc18aeeadc no message 2025-02-11 15:11:23 +08:00
kuaifan
a1f143b0aa build 2025-02-11 07:53:53 +09:00
kuaifan
c13fe9d590 perf: 优化审批功能 2025-02-11 03:33:58 +09:00
kuaifan
50203fbcb3 perf: AI机器人支持多会话 2025-02-11 03:12:25 +09:00
kuaifan
ffe7ebf711 perf: AI机器人支持多会话 2025-02-11 02:41:01 +09:00
kuaifan
f0b5e0c3b9 perf: AI机器人支持多会话 2025-02-10 18:45:35 +09:00
kuaifan
501235ef12 perf: AI机器人支持多会话 2025-02-10 16:48:08 +09:00
kuaifan
da0fa31181 perf: AI机器人支持多会话 2025-02-10 15:53:14 +09:00
kuaifan
0272933f70 perf: AI机器人支持多会话 2025-02-10 15:43:02 +09:00
kuaifan
30d88761b4 perf: AI机器人支持多会话 2025-02-10 12:39:36 +09:00
kuaifan
fb286cea3c perf: AI机器人支持自定义模型 2025-02-08 12:55:11 +09:00
kuaifan
6bcc7b6c49 perf: AI机器人支持多会话 2025-02-07 16:44:02 +09:00
kuaifan
6338a44cc1 no message 2025-02-07 16:41:52 +09:00
kuaifan
ae4680f20c build 2025-02-07 05:07:20 +09:00
kuaifan
2841874417 no message 2025-02-07 05:07:10 +09:00
kuaifan
b6a4e6b4de perf: 支持下载聊天引用的文件 2025-02-07 04:55:58 +09:00
kuaifan
34cfd1e344 perf: 优化翻译消息 2025-02-07 04:38:23 +09:00
kuaifan
b467dc55e5 perf: 支持显示思考过程 2025-02-05 15:38:07 +09:00
kuaifan
9fd8d44a6e build 2025-02-05 01:37:41 +09:00
kuaifan
64262134c4 perf: 支持自定义仪表盘欢迎词 2025-02-05 01:33:32 +09:00
kuaifan
0019c9ef41 build 2025-02-04 13:14:43 +09:00
kuaifan
2676ebd047 no message 2025-02-04 13:10:46 +09:00
kuaifan
97cdd56110 fix: 跨地区发消息出现消息过期的情况 2025-02-04 13:10:46 +09:00
kuaifan
d973451bdc feat: 添加 DeepSeek AI 2025-02-04 13:10:46 +09:00
kuaifan
80313f613e perf: ChatGPT 支持自定义 Base URL 2025-02-04 13:10:46 +09:00
kuaifan
5c564524a3 Merge pull request #247 from zzzzzhy/patch-2
定时任务判断fix
2025-01-17 20:24:22 +08:00
zzzzzhy
e081fbd92b 定时任务判断fix
修复定时更新https证书任务判断逻辑
2025-01-17 14:15:24 +08:00
kuaifan
0ecc20472a Merge pull request #246 from zzzzzhy/patch-1
安装软件依赖
2025-01-16 22:35:38 +08:00
zzzzzhy
b51052f0c6 安装软件依赖 2025-01-16 20:14:08 +08:00
kuaifan
cb106e42ee Merge pull request #245 from zzzzzhy/renew_cert
feat:添加https证书自动更新
2025-01-16 19:54:17 +08:00
zzzzzhy
52f9495ff8 feat:添加https证书自动更新 2025-01-16 06:44:26 +00:00
kuaifan
440b633bad perf: 优化仪表盘任务更新规则 2025-01-15 15:41:38 +08:00
kuaifan
a07913181a fix: 多线程下载文件损坏的问题 2025-01-15 15:27:39 +08:00
kuaifan
34ffd96c86 no message 2025-01-13 10:56:30 +08:00
kuaifan
46a623b430 perf: 优化仪表盘任务更新规则 2025-01-13 10:56:29 +08:00
kuaifan
c16e37023c fix: 多线程下载文件损坏的问题 2025-01-13 10:56:29 +08:00
kuaifan
1cb0cdf540 Merge pull request #242 from nightcp/fix-report-sign-repeat 2025-01-04 20:48:09 +08:00
nightcp
073d03a882 fix: 修复新建周报或日报唯一标识重复 2025-01-04 19:58:37 +08:00
kuaifan
30b9276ab4 no message 2025-01-03 08:19:55 +08:00
kuaifan
76c8b4a4c6 no message 2025-01-03 04:48:38 +08:00
kuaifan
9ea4781d93 perf: 更新小海豚表情包 2025-01-03 03:56:22 +08:00
kuaifan
07d583f73f perf: 优化任务时间冲突提示 2025-01-02 20:25:47 +08:00
kuaifan
12c74aef7a no message 2025-01-02 14:56:30 +08:00
kuaifan
64b10e3060 perf: 优化消息 2025-01-02 14:54:22 +08:00
kuaifan
ab2b29f267 perf: 群聊总人数排除机器人 2025-01-02 14:23:23 +08:00
kuaifan
be9a968ad9 perf: 群聊总人数排除机器人 2025-01-02 13:41:09 +08:00
kuaifan
5f87067a75 fix: 部分电脑无法复制的问题 2025-01-02 12:48:35 +08:00
weifs
ef273bd9dd fix: 修复任务可见性 - 任务重覆获取, 子任务负责人看不到任务问题 2024-12-24 00:11:23 +08:00
weifs
0737a9fae7 fix: 修复任务可见性 - 任务重覆获取, 子任务负责人看不到任务问题 2024-12-23 23:19:22 +08:00
kuaifan
727d7e1d81 build 2024-12-23 11:53:40 +08:00
kuaifan
87e8589aea no message 2024-12-23 11:47:39 +08:00
kuaifan
b13758d3e9 perf: 优化任务面板 2024-12-22 17:45:58 +08:00
kuaifan
14775e2861 perf: 优化子任务的可见性 2024-12-21 22:20:39 +08:00
kuaifan
94af3822d8 perf: 优化客户端 2024-12-21 15:33:01 +08:00
kuaifan
07254c9f27 perf: 优化会议 2024-12-21 11:57:48 +08:00
kuaifan
a99c2f6944 perf: 优化客户端 2024-12-21 11:47:49 +08:00
kuaifan
f9540b08cd perf: 优化会议 2024-12-20 21:24:17 +08:00
kuaifan
34af77eb6d no message 2024-12-20 20:01:42 +08:00
kuaifan
cf3f22776c perf: 优化会员搜索 2024-12-20 19:59:18 +08:00
kuaifan
5bebc8b5ee perf: 优化打开会话 2024-12-20 19:41:00 +08:00
kuaifan
8a4b0c57f9 perf: 优化会议 2024-12-20 19:25:05 +08:00
kuaifan
1acfd7ee34 no message 2024-12-20 09:22:14 +08:00
kuaifan
a29661c54d perf: 优化会议 2024-12-20 09:01:17 +08:00
kuaifan
90558d5ece no message 2024-12-18 20:37:16 +08:00
kuaifan
e6c7007be5 perf: 优化项目面板任务加载 2024-12-18 20:14:35 +08:00
kuaifan
16d0d1687f perf: 优化客户端加载 2024-12-18 19:56:36 +08:00
kuaifan
95ab44d118 perf: 优化客户端加载 2024-12-18 15:23:25 +08:00
kuaifan
e541757b76 no message 2024-12-18 15:22:13 +08:00
kuaifan
f422aea330 fix: 移交账号后工作流的负责人没有更新 2024-12-18 00:25:04 +08:00
kuaifan
d5eb3716aa no message 2024-12-18 00:25:04 +08:00
yijixx
7fb854fb48 feat: 替换网页的资源为本地资源 2024-12-17 17:01:17 +08:00
kuaifan
60b5ecdcd7 fix: 全屏预览时深色皮肤反色的情况 2024-12-17 09:11:12 +08:00
kuaifan
6cce7d31ff build 2024-12-17 08:49:40 +08:00
kuaifan
46f5dd99a6 perf: 优化对话阅读状况 2024-12-17 08:47:05 +08:00
kuaifan
9753dec996 perf: 优化表情回复 2024-12-17 08:41:46 +08:00
kuaifan
53f2e07178 build 2024-12-17 00:14:13 +08:00
kuaifan
3aa2c604d8 perf: 优化桌面端数据处理 2024-12-17 00:07:50 +08:00
kuaifan
d8fbf36e00 perf: 优化资源 2024-12-16 23:29:05 +08:00
kuaifan
008653e3d9 fix: 桌面端查看表情图片缩略图显示错误 2024-12-16 21:23:18 +08:00
kuaifan
23188777fe fix: 项目面板任务不显示的情况 2024-12-16 21:15:30 +08:00
kuaifan
8eb0a49ee6 perf: 优化数据流 2024-12-16 17:04:45 +08:00
kuaifan
207f09a4af fix: 修复移动任务子任务不跟随的情况 2024-12-16 16:07:39 +08:00
kuaifan
69120c5045 build 2024-12-15 23:39:27 +08:00
kuaifan
b8143d1a9b no message 2024-12-15 23:24:38 +08:00
kuaifan
f7eab5893a perf: AI创建任务确认 2024-12-15 22:53:26 +08:00
kuaifan
5fc598a220 no message 2024-12-15 22:45:27 +08:00
kuaifan
783c21ad18 perf: 优化项目面板 2024-12-15 22:27:05 +08:00
kuaifan
a1ce6e6928 perf: 优化项目面板 2024-12-15 09:39:18 +08:00
kuaifan
8cbae629a5 perf: 优化项目面板 2024-12-14 18:51:46 +08:00
kuaifan
da7e832f21 fix: 复制文件权限判断 2024-12-14 18:41:30 +08:00
kuaifan
a572ba0523 perf: 优化项目面板 2024-12-14 18:38:47 +08:00
kuaifan
85a20168dc build 2024-12-13 23:28:46 +08:00
kuaifan
25be9c0fef perf: 优化子任务上下文 2024-12-13 23:19:20 +08:00
kuaifan
a8c890ba51 perf: 优化子任务时间调整 2024-12-13 19:28:05 +08:00
kuaifan
11628b98ca no message 2024-12-13 16:15:47 +08:00
kuaifan
4ae6ca945b no message 2024-12-13 16:00:21 +08:00
kuaifan
49aa1434aa perf: 优化超长文本信息 2024-12-13 15:49:31 +08:00
kuaifan
9e92c61fbf no message 2024-12-13 15:49:31 +08:00
kuaifan
c84111b6b9 perf: 记录版本信息 2024-12-13 15:49:31 +08:00
kuaifan
3a2fcdd18a no message 2024-12-13 15:49:30 +08:00
kuaifan
84a800f69b fix: @在线状态不正确 2024-12-13 15:49:30 +08:00
kuaifan
77e08aa048 no message 2024-12-13 15:49:30 +08:00
kuaifan
0d6fd903f1 perf: 支持更多办公文件格式 2024-12-13 09:47:52 +08:00
kuaifan
bcc74dd927 perf: 请假或外出时取消打卡提醒 2024-12-13 01:00:31 +08:00
kuaifan
dd0720afa7 no message 2024-12-13 00:27:41 +08:00
kuaifan
a06a4095b6 no message 2024-12-12 23:10:33 +08:00
kuaifan
29bc009c07 perf: 图片容错处理 2024-12-12 22:36:25 +08:00
kuaifan
520d2a0e20 no message 2024-12-12 22:01:01 +08:00
kuaifan
dbeb9dd561 perf: 优化全局监听事件 2024-12-12 14:14:51 +08:00
kuaifan
5b02d8008f perf: 优化数据流消息 2024-12-12 13:45:48 +08:00
kuaifan
a032c6114f no message 2024-12-12 12:55:29 +08:00
kuaifan
69ec4966d5 build 2024-12-12 00:44:59 +08:00
kuaifan
87fab80ea3 no message 2024-12-12 00:43:08 +08:00
kuaifan
bd2dabe851 no message 2024-12-12 00:30:25 +08:00
kuaifan
4a45d69e5b no message 2024-12-11 23:26:22 +08:00
kuaifan
e15bea9342 fix: 修复使用AI创建任务顺序错误的问题 2024-12-11 22:36:57 +08:00
kuaifan
7132413837 no message 2024-12-11 22:17:51 +08:00
kuaifan
c51116acaa build 2024-12-11 00:26:15 +08:00
kuaifan
002776f15e no message 2024-12-10 23:57:58 +08:00
kuaifan
c7f5c62e71 no message 2024-12-10 23:41:31 +08:00
kuaifan
3c57cf8d81 no message 2024-12-10 23:27:27 +08:00
kuaifan
f29bf3640a no message 2024-12-10 20:43:32 +08:00
kuaifan
07663dea6c no message 2024-12-10 15:46:41 +08:00
kuaifan
0ddb696e90 perf: 优化媒体播放 2024-12-10 15:20:39 +08:00
kuaifan
cc0a6d4706 no message 2024-12-10 13:08:28 +08:00
kuaifan
4c0ecc8f07 no message 2024-12-10 09:09:19 +08:00
kuaifan
d50c8ce691 perf: 优化临时会话的消息推送 2024-12-10 07:33:58 +08:00
kuaifan
8aa66661ac no message 2024-12-10 07:32:57 +08:00
kuaifan
00a9b3b57b perf: 优化任务时间显示 2024-12-10 07:32:47 +08:00
427 changed files with 17739 additions and 3956 deletions

View File

@@ -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
View File

@@ -15,6 +15,7 @@
.idea
.vscode
.vagrant
.windsurfrules
.phpunit.result.cache
Homestead.json
Homestead.yaml

116
.prefetch
View File

@@ -1,55 +1,115 @@
office/web-apps/apps/api/documents/api.js?hash={version}
office/{path}/fonts/000
office/{path}/fonts/020
office/{path}/fonts/020
office/{path}/fonts/001
office/{path}/fonts/002
office/{path}/fonts/020
office/{path}/fonts/022
office/{path}/fonts/022
office/{path}/fonts/022
office/{path}/fonts/023
office/{path}/fonts/023
office/{path}/fonts/023
office/{path}/fonts/024
office/{path}/fonts/024
office/{path}/fonts/024
office/{path}/fonts/027
office/{path}/fonts/027
office/{path}/fonts/028
office/{path}/fonts/028
office/{path}/fonts/029
office/{path}/fonts/029
office/{path}/fonts/030
office/{path}/fonts/030
office/{path}/fonts/036
office/{path}/fonts/036
office/{path}/fonts/037
office/{path}/fonts/037
office/{path}/fonts/038
office/{path}/fonts/038
office/{path}/fonts/039
office/{path}/fonts/039
office/{path}/fonts/058
office/{path}/fonts/058
office/{path}/fonts/050
office/{path}/fonts/051
office/{path}/fonts/052
office/{path}/fonts/053
office/{path}/fonts/058
office/{path}/fonts/059
office/{path}/fonts/059
office/{path}/fonts/059
office/{path}/fonts/060
office/{path}/fonts/060
office/{path}/fonts/060
office/{path}/fonts/061
office/{path}/fonts/061
office/{path}/fonts/061
office/{path}/fonts/062
office/{path}/fonts/063
office/{path}/fonts/064
office/{path}/fonts/065
office/{path}/fonts/066
office/{path}/fonts/067
office/{path}/fonts/068
office/{path}/fonts/069
office/{path}/fonts/070
office/{path}/fonts/071
office/{path}/fonts/072
office/{path}/fonts/073
office/{path}/fonts/074
office/{path}/fonts/075
office/{path}/fonts/076
office/{path}/fonts/077
office/{path}/fonts/078
office/{path}/fonts/079
office/{path}/fonts/080
office/{path}/fonts/081
office/{path}/fonts/081
office/{path}/fonts/081
office/{path}/fonts/086
office/{path}/fonts/091
office/{path}/fonts/092
office/{path}/fonts/093
office/{path}/fonts/094
office/{path}/fonts/095
office/{path}/fonts/096
office/{path}/fonts/097
office/{path}/fonts/098
office/{path}/fonts/099
office/{path}/fonts/100
office/{path}/fonts/101
office/{path}/fonts/102
office/{path}/fonts/103
office/{path}/fonts/131
office/{path}/fonts/132
office/{path}/fonts/133
office/{path}/fonts/134
office/{path}/fonts/135
office/{path}/fonts/136
office/{path}/fonts/137
office/{path}/fonts/138
office/{path}/fonts/139
office/{path}/fonts/140
office/{path}/fonts/141
office/{path}/fonts/142
office/{path}/fonts/143
office/{path}/fonts/145
office/{path}/fonts/147
office/{path}/fonts/152
office/{path}/fonts/154
office/{path}/fonts/177
office/{path}/fonts/178
office/{path}/fonts/179
office/{path}/fonts/180
office/{path}/fonts/181
office/{path}/fonts/182
office/{path}/fonts/183
office/{path}/fonts/184
office/{path}/fonts/184
office/{path}/fonts/185
office/{path}/fonts/186
office/{path}/fonts/187
office/{path}/fonts/188
office/{path}/fonts/189
office/{path}/fonts/190
office/{path}/fonts/191
office/{path}/fonts/192
office/{path}/fonts/193
office/{path}/fonts/198
office/{path}/fonts/199
office/{path}/fonts/200
office/{path}/fonts/201
office/{path}/fonts/202
office/{path}/fonts/203
office/{path}/fonts/204
office/{path}/fonts/205
office/{path}/fonts/206
office/{path}/fonts/207
office/{path}/fonts/208
office/{path}/fonts/209
office/{path}/fonts/210
office/{path}/fonts/211
office/{path}/fonts/212
office/{path}/fonts/214
office/{path}/fonts/215
office/{path}/fonts/216
office/{path}/fonts/217
office/{path}/sdkjs/cell/sdk-all-min.js
office/{path}/sdkjs/cell/sdk-all.js
office/{path}/sdkjs/common/AllFonts.js
@@ -107,4 +167,4 @@ drawio/webapp/styles/grapheditor.css
minder/css/chunk-vendors.fe9c56c6.css
minder/js/app.aa385de3.js
minder/js/chunk-vendors.cc7455b8.js
minder/js/chunk-vendors.cc7455b8.js

View File

@@ -2,6 +2,269 @@
All notable changes to this project will be documented in this file.
## [0.44.19]
### Bug Fixes
- 看不到未读消息定位提醒
### Performance
- 优化消息定位
- 优化消息性能
## [0.44.15]
### Bug Fixes
- 会话内消息搜索布局错位
- 流程设置翻译不统一
### Performance
- 优化消息定位
- 优化MD消息
## [0.44.3]
### Bug Fixes
- 定位签到失败的问题
### Performance
- 优化发送语音效果
- 录音转文字支持自定义语言
- 优化ES模块
- 优化emoji表情
- 按住Ctrl/Command键可连续选择表情
- Md消息支持html代码
- 优化脚本
- 优化安装命令
- 优化ES索引名称
## [0.43.73]
### Bug Fixes
- 全屏预览图片关闭窗口
- 点击排序导致任务不显示的情况
### Performance
- 新增录音转文字
- 优化数据排序
## [0.43.49]
### Performance
- 添加全局搜索功能
- 优化消息搜索
- 团队管理支持调整部门区域尺寸
- 任务详情支持调整聊天区域尺寸
- 优化团队部门支持3级部门
- 可见群组ID
- 支持在团队管理打开群聊
- 优化回复消息自动@逻辑
- 转发预览隐藏表情回应部分
- 优化任务日志
- 已删除任务支持按标签搜索
- 归档任务支持按标签搜索
- 项目面板添加按标签筛选
- 优化 AI 提示词
- 优化 AI 设置
## [0.43.18]
### Bug Fixes
- 首次跟ai聊天没有记录的问题
### Performance
- 工作报告支持查看仅未读
- AI 支持引用文件
- 优化图文消息
- 优化文本信息复制
- 优化样式
- 无法再AI机器人页面看到模型的问题
## [0.43.7]
### Features
- 添加 Grok AI、Ollama AI
### Performance
- 优化MD消息过长处理
- 优化AI支持分析指定文件
- 支持在AI对话中直接引用任务提问
- 优化 AI 参数
- 优化 Ollama AI
- 优化设置
- 优化AI设置
- 优化AI消息
## [0.42.85]
### Bug Fixes
- 撤回消息是消息列表不更新的情况
### Performance
- 表情回复时更新对话列表
- Onlyoffice 支持打开超过100m的文件
- 优化点击上传列表效果
- AI支持自定义模型列表
## [0.42.79]
### Bug Fixes
- 修复偶现的是子窗口出现身份丢失的情况
### Performance
- 优化查看长消息内容
## [0.42.74]
### Performance
- 优化审批功能
- AI机器人支持多会话
- AI机器人支持自定义模型
## [0.42.61]
### Performance
- 支持下载聊天引用的文件
- 优化翻译消息
- 支持显示思考过程
## [0.42.57]
### Bug Fixes
- 跨地区发消息出现消息过期的情况
- 多线程下载文件损坏的问题
- 修复新建周报或日报唯一标识重复
### Features
- 添加 DeepSeek AI
- 添加https证书自动更新
### Performance
- 支持自定义仪表盘欢迎词
- ChatGPT 支持自定义 Base URL
- 优化仪表盘任务更新规则
## [0.42.37]
### Bug Fixes
- 部分电脑无法复制的问题
- 修复任务可见性 - 任务重覆获取, 子任务负责人看不到任务问题
### Performance
- 更新小海豚表情包
- 优化任务时间冲突提示
- 优化消息
- 群聊总人数排除机器人
## [0.42.26]
### Bug Fixes
- 移交账号后工作流的负责人没有更新
- 全屏预览时深色皮肤反色的情况
### Features
- 替换网页的资源为本地资源
### Performance
- 优化任务面板
- 优化子任务的可见性
- 优化客户端
- 优化会议
- 优化会员搜索
- 优化打开会话
- 优化项目面板任务加载
- 优化客户端加载
## [0.42.3]
### Performance
- 优化对话阅读状况
- 优化表情回复
## [0.42.0]
### Bug Fixes
- 桌面端查看表情图片缩略图显示错误
- 项目面板任务不显示的情况
- 修复移动任务子任务不跟随的情况
### Performance
- 优化桌面端数据处理
- 优化资源
- 优化数据流
## [0.41.93]
### Bug Fixes
- 复制文件权限判断
### Performance
- AI创建任务确认
- 优化项目面板
## [0.41.84]
### Bug Fixes
- @在线状态不正确
### Performance
- 优化子任务上下文
- 优化子任务时间调整
- 优化超长文本信息
- 记录版本信息
- 支持更多办公文件格式
- 请假或外出时取消打卡提醒
- 图片容错处理
- 优化全局监听事件
- 优化数据流消息
## [0.41.64]
### Bug Fixes
- 修复使用AI创建任务顺序错误的问题
## [0.41.55]
### Performance
- 优化媒体播放
- 优化临时会话的消息推送
- 优化任务时间显示
## [0.41.45]
### Bug Fixes

View File

@@ -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

View File

@@ -47,16 +47,6 @@ cd dootask
./cmd port 80
```
### 更换URL
```bash
# 此地址仅影响邮件回复功能
./cmd url {域名地址}
# 例如:
./cmd url https://domain.com
```
### 停止服务
```bash

View 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}");
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
/**

View File

@@ -13,7 +13,10 @@ use App\Models\User;
use App\Module\Base;
use App\Module\Timer;
use App\Module\Extranet;
use App\Module\ElasticSearch\ElasticSearchUserMsg;
use App\Module\TimeRange;
use App\Module\MsgTool;
use App\Module\Table\OnlineData;
use App\Models\FileContent;
use App\Models\AbstractModel;
use App\Models\WebSocketDialog;
@@ -23,6 +26,7 @@ use App\Models\WebSocketDialogConfig;
use App\Models\WebSocketDialogMsgRead;
use App\Models\WebSocketDialogMsgTodo;
use App\Models\WebSocketDialogMsgTranslate;
use App\Models\WebSocketDialogSession;
use Hhxsv5\LaravelS\Swoole\Task\Task;
/**
@@ -168,27 +172,15 @@ class DialogController extends AbstractController
}
// 搜索消息会话
if (count($list) < 20) {
$prefix = DB::getTablePrefix();
if (preg_match('/[+\-><()~*"@]/', $key)) {
$against = "\"{$key}\"";
} else {
$against = "*{$key}*";
$searchResults = ElasticSearchUserMsg::searchByKeyword($user->userid, $key, 20 - count($list));
if ($searchResults) {
foreach ($searchResults as $item) {
if ($dialog = WebSocketDialog::find($item['id'])) {
$dialog = array_merge($dialog->toArray(), $item);
$list[] = WebSocketDialog::synthesizeData($dialog, $user->userid);
}
}
}
$msgs = DB::table('web_socket_dialog_users as u')
->select(['d.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at', 'm.id as search_msg_id'])
->join('web_socket_dialogs as d', 'u.dialog_id', '=', 'd.id')
->join('web_socket_dialog_msgs as m', 'm.dialog_id', '=', 'd.id')
->where('u.userid', $user->userid)
->whereNull('d.deleted_at')
->whereRaw("MATCH({$prefix}m.key) AGAINST('{$against}' IN BOOLEAN MODE)")
->orderByDesc('m.id')
->take(20 - count($list))
->get()
->map(function($item) use ($user) {
return WebSocketDialog::synthesizeData($item, $user->userid);
})
->all();
$list = array_merge($list, $msgs);
}
//
return Base::retSuccess('success', $list);
@@ -255,6 +247,9 @@ class DialogController extends AbstractController
->where('d.id', $dialog_id)
->whereNull('d.deleted_at')
->first();
if (empty($item)) {
return Base::retError('不在成员列表内');
}
return Base::retSuccess('success', WebSocketDialog::synthesizeData($item, $user->userid));
}
@@ -287,6 +282,9 @@ class DialogController extends AbstractController
$array = array_filter($data->toArray(), function ($item) {
return $item['userid'] > 0;
});
foreach ($array as &$item) {
$item['online'] = $item['bot'] || OnlineData::live($item['userid']) > 0;
}
} else {
$data = WebSocketDialogUser::select(['web_socket_dialog_users.*', 'users.bot'])
->join('users', 'web_socket_dialog_users.userid', '=', 'users.userid')
@@ -530,6 +528,9 @@ class DialogController extends AbstractController
->on('read.msg_id', '=', 'web_socket_dialog_msgs.id');
})->where('web_socket_dialog_msgs.dialog_id', $dialog_id);
//
if ($dialog->session_id > 0) {
$builder->whereSessionId($dialog->session_id);
}
if ($msg_type) {
if ($msg_type === 'tag') {
$builder->where('tag', '>', 0);
@@ -711,7 +712,44 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/one 15. 获取单条消息
* @api {get} api/dialog/msg/esearch 15. 搜索消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__esearch
*
* @apiParam {String} key 搜索关键词
* @apiParam {Number} [pagesize] 每页显示数量,默认:20最大:50
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__esearch()
{
$user = User::auth();
//
$key = trim(Request::input('key'));
$list = [];
//
$searchResults = ElasticSearchUserMsg::searchByKeyword($user->userid, $key, Base::getPaginate(50, 20));
if ($searchResults) {
foreach ($searchResults as $item) {
if ($dialog = WebSocketDialog::find($item['id'])) {
$dialog = array_merge($dialog->toArray(), $item);
$list[] = WebSocketDialog::synthesizeData($dialog, $user->userid);
}
}
}
//
return Base::retSuccess('success', [
'data' => $list,
]);
}
/**
* @api {get} api/dialog/msg/one 16. 获取单条消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -740,7 +778,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/dot 16. 聊天消息去除点
* @api {get} api/dialog/msg/dot 17. 聊天消息去除点
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -773,7 +811,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/read 17. 已读聊天消息
* @api {get} api/dialog/msg/read 18. 已读聊天消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -844,7 +882,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/unread 18. 获取未读消息数据
* @api {get} api/dialog/msg/unread 19. 获取未读消息数据
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -887,7 +925,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/checked 19. 设置消息checked
* @api {get} api/dialog/msg/checked 20. 设置消息checked
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -953,7 +991,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/stream 20. 通知成员监听消息
* @api {post} api/dialog/msg/stream 21. 通知成员监听消息
*
* @apiDescription 通知指定会员EventSource监听流动消息
* @apiVersion 1.0.0
@@ -997,7 +1035,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/sendtext 21. 发送消息
* @api {post} api/dialog/msg/sendtext 22. 发送消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1018,10 +1056,11 @@ class DialogController extends AbstractController
* @apiParam {Number} [reply_id] 回复ID
* @apiParam {String} [reply_check] 配合 reply_id 使用判断是否需要验证回复ID的有效性
* - no: 不进行判断,直接使用提供的 reply_id默认
* - yes: 进行判断,如果上一条消息的 ID 为 reply_id则认为 reply_id 无效
* - yes: 进行判断,如果 reply_id 到最新消息都没有会员发的消息,则 reply_id 无效
* @apiParam {String} [silence] 是否静默发送
* - no: 正常发送(默认)
* - yes: 静默发送
* @apiParam {String} [model_name] 模型名称仅AI机器人支持
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -1042,6 +1081,7 @@ class DialogController extends AbstractController
$key = trim(Request::input('key'));
$text_type = strtolower(trim(Request::input('text_type')));
$silence = in_array(strtolower(trim(Request::input('silence'))), ['yes', 'true', '1']);
$model_name = trim(Request::input('model_name'));
$markdown = in_array($text_type, ['md', 'markdown']);
//
$result = [];
@@ -1055,8 +1095,12 @@ class DialogController extends AbstractController
} elseif ($reply_id > 0) {
$action = "reply-$reply_id";
if ($reply_check === 'yes') {
$lastMsgId = WebSocketDialogMsg::whereDialogId($dialog_id)->orderByDesc('id')->value('id');
if ($lastMsgId == $reply_id) {
$exisUserMsg = WebSocketDialogMsg::whereDialogId($dialog_id)
->where('id', '>', $reply_id)
->whereBot(0)
->whereNotIn('type', ['notice', 'template'])
->exists();
if (!$exisUserMsg) {
$action = "";
}
}
@@ -1086,27 +1130,46 @@ class DialogController extends AbstractController
if (empty($size)) {
return Base::retError('消息发送保存失败');
}
$ext = $markdown ? 'md' : 'htm';
$fileData = [
'name' => "LongText-{$strlen}.{$ext}",
'size' => $size,
'file' => $file,
'path' => $path,
'url' => Base::fillUrl($path),
'thumb' => '',
'width' => -1,
'height' => -1,
'ext' => $ext,
$type = $markdown ? 'md' : 'htm';
$desc = $text;
if ($markdown) {
$desc = preg_replace("/:::\s*reasoning[\s\S]*?:::/", "", $desc);
$desc = Base::markdown2html($desc);
}
$desc = strip_tags($desc);
$desc = mb_substr(WebSocketDialogMsg::filterEscape($desc), 0, 200);
$text = MsgTool::truncateText($text, 500, $type);
$msgData = [
'type' => $type, // 内容类型
'desc' => $desc, // 描述内容
'text' => $text, // 简要内容
'file' => [
'name' => "LongText-{$strlen}.{$type}",
'size' => $size,
'file' => $file,
'path' => $path,
'url' => Base::fillUrl($path),
'thumb' => '',
'width' => -1,
'height' => -1,
'ext' => $type,
],
];
if (empty($key)) {
$key = mb_substr(strip_tags($text), 0, 200);
$key = $desc;
}
$result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'file', $fileData, $user->userid, false, false, $silence, $key);
if ($model_name) {
$msgData['model_name'] = $model_name;
}
$result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'longtext', $msgData, $user->userid, false, false, $silence, $key);
} else {
$msgData = ['text' => $text];
if ($markdown) {
$msgData['type'] = 'md';
}
if ($model_name) {
$msgData['model_name'] = $model_name;
}
$result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'text', $msgData, $user->userid, false, false, $silence, $key);
}
}
@@ -1114,7 +1177,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/sendnotice 22. 发送通知
* @api {post} api/dialog/msg/sendnotice 23. 发送通知
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1167,7 +1230,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/sendtemplate 23. 发送模板消息
* @api {post} api/dialog/msg/sendtemplate 24. 发送模板消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1236,7 +1299,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/sendrecord 24. 发送语音
* @api {post} api/dialog/msg/sendrecord 25. 发送语音
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1284,7 +1347,79 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/sendfile 25. 文件上传
* @api {post} api/dialog/msg/convertrecord 26. 录音转文字
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__convertrecord
*
* @apiParam {String} base64 语音base64
* @apiParam {Number} duration 语音时长(毫秒)
* @apiParam {String} [language] 识别语言
* - 比如zh
* - 默认:自动识别
* - 格式:符合 ISO_639 标准
* - 此参数不一定起效果AI会根据语音和language参考翻译识别结果
* @apiParam {String} [translate] 翻译识别结果
* - 比如zh
* - 默认:不翻译结果
* - 格式:符合 ISO_639 标准
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__convertrecord()
{
$user = User::auth();
$user->checkChatInformation();
//
$path = "uploads/tmp/chat/" . date("Ym") . "/" . $user->userid . "/";
$base64 = Request::input('base64');
$language = Request::input('language');
$translate = Request::input('translate');
$duration = intval(Request::input('duration'));
if ($duration < 600) {
return Base::retError('说话时间太短');
}
// 保存录音
$data = Base::record64save([
"base64" => $base64,
"path" => $path,
]);
if (Base::isError($data)) {
return Base::retError($data['msg']);
}
$recordData = $data['data'];
// 转文字
$extParams = [];
if ($language) {
$extParams = [
'language' => $language === 'zh-CHT' ? 'zh' : $language,
'prompt' => "将此语音识别为“" . Doo::getLanguages($language) . "”。",
];
}
$result = Extranet::openAItranscriptions($recordData['file'], $extParams);
if (Base::isError($result)) {
return $result;
}
if (strlen($result['data']) < 1) {
return Base::retError('转文字失败');
}
// 翻译
if ($translate) {
$result = Extranet::openAItranslations($result['data'], Doo::getLanguages($translate));
if (Base::isError($result)) {
return $result;
}
}
// 返回
return $result;
}
/**
* @api {post} api/dialog/msg/sendfile 27. 文件上传
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1316,7 +1451,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/sendfiles 26. 群发文件上传
* @api {post} api/dialog/msg/sendfiles 28. 群发文件上传
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1372,7 +1507,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/sendfileid 27. 通过文件ID发送文件
* @api {get} api/dialog/msg/sendfileid 29. 通过文件ID发送文件
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1442,7 +1577,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/sendanon 28. 发送匿名消息
* @api {post} api/dialog/msg/sendanon 30. 发送匿名消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1498,7 +1633,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/sendlocation 29. 发送位置消息
* @api {post} api/dialog/msg/sendlocation 31. 发送位置消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1558,7 +1693,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/readlist 30. 获取消息阅读情况
* @api {get} api/dialog/msg/readlist 32. 获取消息阅读情况
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1587,7 +1722,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/detail 31. 消息详情
* @api {get} api/dialog/msg/detail 33. 消息详情
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1629,13 +1764,25 @@ class DialogController extends AbstractController
$msg = File::formatFileData($msg);
$data['content'] = $msg['content'];
$data['file_mode'] = $msg['file_mode'];
} elseif ($data['type'] == 'longtext') {
$data['content'] = [
'type' => 'htm',
'content' => Doo::translate("内容不存在")
];
if (isset($data['msg']['file']['path'])) {
$filePath = public_path($data['msg']['file']['path']);
if (file_exists($filePath)) {
$data['content']['type'] = $data['msg']['type'];
$data['content']['content'] = file_get_contents($filePath);
}
}
}
//
return Base::retSuccess('success', $data);
}
/**
* @api {get} api/dialog/msg/download 32. 文件下载
* @api {get} api/dialog/msg/download 34. 文件下载
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1672,11 +1819,11 @@ class DialogController extends AbstractController
}
//
$filePath = public_path($array['path']);
return Base::BinaryFileResponse($filePath, $array['name']);
return Base::DownloadFileResponse($filePath, $array['name']);
}
/**
* @api {get} api/dialog/msg/withdraw 33. 聊天消息撤回
* @api {get} api/dialog/msg/withdraw 35. 聊天消息撤回
*
* @apiDescription 消息撤回限制24小时内需要token身份
* @apiVersion 1.0.0
@@ -1702,7 +1849,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/voice2text 34. 语音消息转文字
* @api {get} api/dialog/msg/voice2text 36. 语音消息转文字
*
* @apiDescription 将语音消息转文字需要token身份
* @apiVersion 1.0.0
@@ -1754,7 +1901,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/translation 35. 翻译消息
* @api {get} api/dialog/msg/translation 37. 翻译消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1762,6 +1909,8 @@ class DialogController extends AbstractController
* @apiName msg__translation
*
* @apiParam {Number} msg_id 消息ID
* @apiParam {Number} [force] 强制翻译1是、0否
* - 默认不强制翻译,已翻译过的消息不再翻译
* @apiParam {String} [language] 目标语言,默认当前语言
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
@@ -1773,6 +1922,7 @@ class DialogController extends AbstractController
User::auth();
//
$msg_id = intval(Request::input("msg_id"));
$force = intval(Request::input("force"));
$language = Base::inputOrHeader('language');
$targetLanguage = Doo::getLanguages($language);
//
@@ -1790,13 +1940,20 @@ class DialogController extends AbstractController
//
$row = WebSocketDialogMsgTranslate::whereMsgId($msg_id)->whereLanguage($language)->first();
if ($row) {
return Base::retSuccess("success", $row->only(['msg_id', 'language', 'content']));
if ($force) {
$row->delete();
} else {
return Base::retSuccess("success", $row->only(['msg_id', 'language', 'content']));
}
}
//
$msgData = Base::json2array($msg->getRawOriginal('msg'));
if (empty($msgData['text'])) {
return Base::retError("消息内容为空");
}
if ($msg->type === 'text' && $msgData['type'] === 'md') {
$msgData['text'] = preg_replace('/:::\s*reasoning.*?:::/s', '', $msgData['text']);
}
$res = Extranet::openAItranslations($msgData['text'], $targetLanguage);
if (Base::isError($res)) {
return $res;
@@ -1813,7 +1970,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/mark 36. 消息标记操作
* @api {get} api/dialog/msg/mark 38. 消息标记操作
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1877,7 +2034,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/silence 37. 消息免打扰
* @api {get} api/dialog/msg/silence 39. 消息免打扰
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1940,7 +2097,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/forward 38. 转发消息给
* @api {get} api/dialog/msg/forward 40. 转发消息给
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1981,7 +2138,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/emoji 39. emoji回复
* @api {get} api/dialog/msg/emoji 41. emoji回复
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2016,7 +2173,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/tag 40. 标注/取消标注
* @api {get} api/dialog/msg/tag 42. 标注/取消标注
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2045,7 +2202,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/todo 41. 设待办/取消待办
* @api {get} api/dialog/msg/todo 43. 设待办/取消待办
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2088,7 +2245,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/todolist 42. 获取消息待办情况
* @api {get} api/dialog/msg/todolist 44. 获取消息待办情况
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2118,7 +2275,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/done 43. 完成待办
* @api {get} api/dialog/msg/done 45. 完成待办
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2171,7 +2328,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/color 44. 设置颜色
* @api {get} api/dialog/msg/color 46. 设置颜色
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2212,7 +2369,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/group/add 45. 新增群组
* @api {get} api/dialog/group/add 47. 新增群组
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2274,7 +2431,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/group/edit 46. 修改群组
* @api {get} api/dialog/group/edit 48. 修改群组
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2336,7 +2493,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/group/adduser 47. 添加群成员
* @api {get} api/dialog/group/adduser 49. 添加群成员
*
* @apiDescription 需要token身份
* - 有群主时:只有群主可以邀请
@@ -2372,7 +2529,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/group/deluser 48. 移出(退出)群成员
* @api {get} api/dialog/group/deluser 50. 移出(退出)群成员
*
* @apiDescription 需要token身份
* - 只有群主、邀请人可以踢人
@@ -2416,7 +2573,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/group/transfer 49. 转让群组
* @api {get} api/dialog/group/transfer 51. 转让群组
*
* @apiDescription 需要token身份
* - 只有群主且是个人类型群可以解散
@@ -2465,7 +2622,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/group/disband 50. 解散群组
* @api {get} api/dialog/group/disband 52. 解散群组
*
* @apiDescription 需要token身份
* - 只有群主且是个人类型群可以解散
@@ -2493,7 +2650,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/group/searchuser 51. 搜索个人群(仅限管理员)
* @api {get} api/dialog/group/searchuser 53. 搜索个人群(仅限管理员)
*
* @apiDescription 需要token身份用于创建部门搜索个人群组
* @apiVersion 1.0.0
@@ -2522,7 +2679,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/okr/add 52. 创建OKR评论会话
* @api {post} api/dialog/okr/add 54. 创建OKR评论会话
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2561,7 +2718,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/okr/push 53. 推送OKR相关信息
* @api {post} api/dialog/okr/push 55. 推送OKR相关信息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2597,7 +2754,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/wordchain 54. 发送接龙消息
* @api {post} api/dialog/msg/wordchain 56. 发送接龙消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2683,7 +2840,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/vote 55. 发起投票
* @api {post} api/dialog/msg/vote 57. 发起投票
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2799,7 +2956,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/top 56. 置顶/取消置顶
* @api {get} api/dialog/msg/top 58. 置顶/取消置顶
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2859,7 +3016,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/topinfo 57. 获取置顶消息
* @api {get} api/dialog/msg/topinfo 59. 获取置顶消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2886,7 +3043,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/applied 58. 标记消息已应用
* @api {get} api/dialog/msg/applied 60. 标记消息已应用
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2935,7 +3092,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/sticker/search 59. 搜索在线表情
* @api {get} api/dialog/sticker/search 61. 搜索在线表情
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2959,7 +3116,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/config 60. 获取会话配置
* @api {get} api/dialog/config 62. 获取会话配置
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2995,7 +3152,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/config/save 61. 保存会话配置
* @api {post} api/dialog/config/save 63. 保存会话配置
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -3033,10 +3190,138 @@ class DialogController extends AbstractController
]
)) {
WebSocketDialogMsg::sendMsg(null, $dialog_id, 'notice', [
'notice' => $value ? ("修改提示词:" . $value) : "取消提示词",
'notice' => $value ? ("修改提示词:" . Base::cutStr($value, 100)) : "取消提示词",
], User::userid(), true, true);
}
return Base::retSuccess('保存成功');
}
/**
* @api {get} api/dialog/session/create 64. AI-开启新会话
*
* @apiDescription 需要token身份仅限与AI用户会话
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName session_create
*
* @apiParam {Number} dialog_id 对话ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function session__create()
{
User::auth();
//
$dialog_id = intval(Request::input('dialog_id'));
//
$dialog = WebSocketDialog::checkDialog($dialog_id);
//
if ($dialog->type != 'user') {
return Base::retError('当前对话不支持');
}
//
$hasAiUser = WebSocketDialogUser::join('users as u', 'web_socket_dialog_users.userid', '=', 'u.userid')
->where('dialog_id', $dialog->id)
->where('u.email', 'like', 'ai-%@bot.system')
->exists();
if (!$hasAiUser) {
return Base::retError('当前对话不支持');
}
//
$session = WebSocketDialogSession::whereDialogId($dialog->id)
->whereTitle('')
->first();
if ($session) {
$dialog->session_id = $session->id;
$dialog->save();
return Base::retSuccess('success', $session);
}
//
$session = WebSocketDialogSession::create([
'dialog_id' => $dialog->id,
'status' => 1,
'title' => '',
]);
$session->save();
$dialog->session_id = $session->id;
$dialog->save();
//
return Base::retSuccess('success', $session);
}
/**
* @api {get} api/dialog/session/list 65. AI-获取会话列表
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName session_list
*
* @apiParam {Number} dialog_id 对话ID
*
* @apiParam {Number} [page] 当前页,默认:1
* @apiParam {Number} [pagesize] 每页显示数量,默认:20最大:50
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function session__list()
{
User::auth();
//
$dialog_id = intval(Request::input('dialog_id'));
//
$dialog = WebSocketDialog::checkDialog($dialog_id);
//
$sessions = WebSocketDialogSession::whereDialogId($dialog->id)
->orderByDesc('id')
->paginate(Base::getPaginate(100, 10));
$sessions->transform(function ($item) use ($dialog) {
if ($item->id === $dialog->session_id) {
$item->is_open = 1;
} else {
$item->is_open = 0;
}
return $item;
});
//
return Base::retSuccess('success', $sessions);
}
/**
* @api {get} api/dialog/session/open 66. AI-打开会话
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName session_open
*
* @apiParam {Number} session_id 会话ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function session__open()
{
User::auth();
//
$session_id = intval(Request::input('session_id'));
//
$session = WebSocketDialogSession::whereId($session_id)->first();
if (empty($session)) {
return Base::retError('会话不存在或已被删除');
}
//
$dialog = WebSocketDialog::checkDialog($session->dialog_id);
//
$dialog->session_id = $session->id;
$dialog->save();
//
return Base::retSuccess('success', $session);
}
}

View File

@@ -106,6 +106,7 @@ class FileController extends AbstractController
*
* @apiParam {String} [link] 通过分享地址搜索https://t.hitosea.com/single/file/ODcwOCwzOSxpa0JBS2lmVQ==
* @apiParam {String} [key] 关键词
* @apiParam {Number} [take] 获取数量默认50最大100
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -118,7 +119,7 @@ class FileController extends AbstractController
$link = trim(Request::input('link'));
$key = trim(Request::input('key'));
$id = 0;
$take = 50;
$take = Base::getPaginate(100, 50, 'take');
if (preg_match("/\/single\/file\/(.*?)$/i", $link, $match)) {
$id = intval(FileLink::whereCode($match[1])->value('file_id'));
$take = 1;
@@ -301,6 +302,7 @@ class FileController extends AbstractController
//
$userid = $user->userid;
if ($row->pid > 0) {
File::permissionFind($row->pid, $user, 1);
$userid = intval(File::whereId($row->pid)->value('userid'));
}
//

View File

@@ -941,7 +941,8 @@ class ProjectController extends AbstractController
* @apiName task__lists
*
* @apiParam {Object} [keys] 搜索条件
* - keys.name: ID、任务名称
* - keys.name: ID、任务名称、任务描述
* - keys.tag: 标签名称
*
* @apiParam {Number} [project_id] 项目ID
* @apiParam {Number} [parent_id] 主任务IDproject_id && parent_id ≤ 0 时 仅查询自己参与的任务)
@@ -994,9 +995,17 @@ class ProjectController extends AbstractController
if (Base::isNumber($keys['name'])) {
$builder->where("project_tasks.id", intval($keys['name']));
} else {
$builder->where("project_tasks.name", "like", "%{$keys['name']}%");
$builder->where(function ($query) use ($keys) {
$query->where("project_tasks.name", "like", "%{$keys['name']}%");
$query->orWhere("project_tasks.desc", "like", "%{$keys['name']}%");
});
}
}
if ($keys['tag']) {
$builder->whereHas('taskTag', function ($query) use ($keys) {
$query->where('project_task_tags.name', $keys['tag']);
});
}
//
$scopeAll = false;
if ($parent_id > 0) {
@@ -1058,20 +1067,20 @@ class ProjectController extends AbstractController
$query->where('project_users.owner', 1);
$query->where('project_users.userid', $userid);
});
$builder->leftJoin('project_task_users as project_sub_task_users', function ($query) use($userid) {
$query->on('project_sub_task_users.task_pid', '=', 'project_tasks.parent_id');
$query->where('project_sub_task_users.userid', $userid);
});
$builder->leftJoin('project_task_visibility_users', function ($query) use($userid) {
$query->on('project_task_visibility_users.task_id', '=', 'project_tasks.id');
$query->where('project_task_visibility_users.userid', $userid);
});
$builder->leftJoin('project_task_visibility_users as project_sub_task_visibility_users', function ($query) use($userid) {
$query->on('project_sub_task_visibility_users.task_id', '=', 'project_tasks.parent_id');
$query->where('project_sub_task_visibility_users.userid', $userid);
});
$builder->where(function ($query) use ($userid) {
$query->where("project_tasks.visibility", 1);
$query->orWhere("project_users.userid", $userid);
$query->orWhere("project_task_users.userid", $userid);
$query->orWhere("project_task_visibility_users.userid", $userid);
$query->orWhere("project_sub_task_users.userid", $userid);
$query->orWhere("project_sub_task_visibility_users.userid", $userid);
});
// 优化子查询汇总
$builder->leftJoinSub(function ($query) {
@@ -1159,6 +1168,7 @@ class ProjectController extends AbstractController
$list = ProjectTask::with(['taskUser'])
->select([
'projects.name as project_name',
'project_tasks.project_id',
'project_tasks.id',
'project_tasks.name',
'project_tasks.start_at',
@@ -1852,7 +1862,7 @@ class ProjectController extends AbstractController
}
//
$filePath = public_path($file->getRawOriginal('path'));
return Base::BinaryFileResponse($filePath, $file->name);
return Base::DownloadFileResponse($filePath, $file->name);
}
/**
@@ -2399,6 +2409,7 @@ class ProjectController extends AbstractController
*/
public function task__move()
{
Base::checkClientVersion('0.42.0');
User::auth();
//
$task_id = intval(Request::input('task_id'));
@@ -2436,9 +2447,26 @@ class ProjectController extends AbstractController
//
$task->moveTask($project_id, $column_id, $flow_item_id, $owner, $assist, $completeAt);
//
$task = ProjectTask::userTask($task_id);
$data = [];
$mainTask = ProjectTask::userTask($task_id)?->toArray();
if ($mainTask) {
$mainTask['column_name'] = ProjectColumn::whereId($mainTask['column_id'])->value('name');
$mainTask['project_name'] = Project::whereId($mainTask['project_id'])->value('name');
$data[] = $mainTask;
//
$subTasks = ProjectTask::whereParentId($task_id)->get();
foreach ($subTasks as $subTask) {
$data[] = [
'id' => $subTask->id,
'project_id' => $subTask->project_id,
'column_id' => $subTask->column_id,
'column_name' => $mainTask['column_name'],
'project_name' => $mainTask['project_name'],
];
}
}
//
return Base::retSuccess('移动成功', $task);
return Base::retSuccess('移动成功', $data);
}
/**
@@ -2567,7 +2595,7 @@ class ProjectController extends AbstractController
$builder->with(['projectTask:id,parent_id,name'])->whereProjectId($project->id)->whereTaskOnly(0);
}
//
$list = $builder->orderByDesc('created_at')->paginate(Base::getPaginate(100, 20));
$list = $builder->orderByDesc('created_at')->orderByDesc('id')->paginate(Base::getPaginate(100, 20));
$list->transform(function (ProjectLog $log) use ($task_id) {
$timestamp = Carbon::parse($log->created_at)->timestamp;
if ($task_id === 0) {

View File

@@ -71,6 +71,7 @@ class ReportController extends AbstractController
* @apiParam {Object} [keys] 搜索条件
* - keys.key: 关键词
* - keys.type: 汇报类型weekly:周报daily:日报
* - keys.status: 状态unread:未读read:已读
* - keys.created_at: 汇报时间
* @apiParam {Number} [page] 当前页,默认:1
* @apiParam {Number} [pagesize] 每页显示数量,默认:20最大:50
@@ -98,6 +99,11 @@ class ReportController extends AbstractController
if (in_array($keys['type'], [Report::WEEKLY, Report::DAILY])) {
$builder->whereType($keys['type']);
}
if (in_array($keys['status'], ['unread', 'read'])) {
$builder->whereHas("receivesUser", function ($query) use ($user, $keys) {
$query->where("report_receives.userid", $user->userid)->where("report_receives.read", $keys['status'] === 'unread' ? 0 : 1);
});
}
if (is_array($keys['created_at'])) {
if ($keys['created_at'][0] > 0) $builder->where('created_at', '>=', Base::newCarbon($keys['created_at'][0])->startOfDay());
if ($keys['created_at'][1] > 0) $builder->where('created_at', '<=', Base::newCarbon($keys['created_at'][1])->endOfDay());

View File

@@ -41,7 +41,7 @@ class SystemController extends AbstractController
* @apiParam {String} type
* - get: 获取(默认)
* - all: 获取所有(需要管理员权限)
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'temp_account_alias', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'voice2text', 'translation', 'e2e_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'all_group_mute', 'all_group_autoin', 'user_private_chat_mute', 'user_group_chat_mute', 'system_alias', 'image_compress', 'image_quality', 'image_save_local', 'start_home']
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'temp_account_alias', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'voice2text', 'translation', 'e2e_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'all_group_mute', 'all_group_autoin', 'user_private_chat_mute', 'user_group_chat_mute', 'system_alias', 'system_welcome', 'image_compress', 'image_quality', 'image_save_local', 'start_home']
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -80,6 +80,7 @@ class SystemController extends AbstractController
'user_private_chat_mute',
'user_group_chat_mute',
'system_alias',
'system_welcome',
'image_compress',
'image_quality',
'image_save_local',
@@ -108,6 +109,9 @@ class SystemController extends AbstractController
if ($all['system_alias'] == env('APP_NAME')) {
$all['system_alias'] = '';
}
if ($all['system_welcome'] == '欢迎您,{username}') {
$all['system_welcome'] = '';
}
$setting = Base::setting('system', Base::newTrim($all));
} else {
$setting = Base::setting('system');
@@ -283,6 +287,8 @@ class SystemController extends AbstractController
* @apiParam {String} type
* - get: 获取(默认)
* - save: 保存设置(参数:[...]
* @apiParam {String} filter 过滤字段(可选)
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
@@ -292,6 +298,7 @@ class SystemController extends AbstractController
User::auth('admin');
//
$type = trim(Request::input('type'));
$filter = trim(Request::input('filter'));
$setting = Base::setting('aibotSetting');
if ($type == 'save') {
if (env("SYSTEM_SETTING") == 'disabled') {
@@ -306,10 +313,18 @@ class SystemController extends AbstractController
}
$setting = Base::setting('aibotSetting', Base::newTrim($setting));
}
if ($filter) {
$setting = array_filter($setting, function($value, $key) use ($filter) {
return str_starts_with($key, $filter);
}, ARRAY_FILTER_USE_BOTH);
}
//
if (env("SYSTEM_SETTING") == 'disabled') {
foreach ($setting as $key => $item) {
if (str_contains($key, '_key')) {
if (empty($item)) {
continue;
}
if (str_ends_with($key, '_key') || str_ends_with($key, '_secret')) {
$setting[$key] = substr($item, 0, 4) . str_repeat('*', strlen($item) - 8) . substr($item, -4);
}
}
@@ -319,7 +334,66 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/setting/checkin 05. 获取签到设置、保存签到设置(限管理员)
* @api {get} api/system/setting/aibot_models 05. 获取AI模型
*
* @apiDescription 获取所有AI机器人模型设置
* @apiVersion 1.0.0
* @apiGroup system
* @apiName aibot_models
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function setting__aibot_models()
{
$setting = Base::setting('aibotSetting');
$setting = array_filter($setting, function($value, $key) {
return str_ends_with($key, '_models') || str_ends_with($key, '_model');
}, ARRAY_FILTER_USE_BOTH);
return Base::retSuccess('success', $setting ?: json_decode('{}'));
}
/**
* @api {get} api/system/setting/aibot_defmodels 06. 获取AI默认模型
*
* @apiDescription 获取AI机器人默认模型
* @apiVersion 1.0.0
* @apiGroup system
* @apiName setting__aibot_defmodels
*
* @apiParam {String} type AI类型
* @apiParam {String} [base_url] 基础URL仅 type=ollama 时有效)
* @apiParam {String} [key] Key仅 type=ollama 时有效)
* @apiParam {String} [agency] 使用代理(仅 type=ollama 时有效)
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function setting__aibot_defmodels()
{
$type = trim(Request::input('type'));
if ($type == 'ollama') {
$baseUrl = trim(Request::input('base_url'));
$key = trim(Request::input('key'));
$agency = trim(Request::input('agency'));
if (empty($baseUrl)) {
return Base::retError('请先填写 Base URL');
}
return Extranet::ollamaModels($baseUrl, $key, $agency);
}
$models = Setting::AIDefaultModels($type);
if (empty($models)) {
return Base::retError('未找到默认模型');
}
return Base::retSuccess('success', [
'models' => $models
]);
}
/**
* @api {get} api/system/setting/checkin 07. 获取签到设置、保存签到设置(限管理员)
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -425,7 +499,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/setting/apppush 06. 获取APP推送设置、保存APP推送设置限管理员
* @api {get} api/system/setting/apppush 08. 获取APP推送设置、保存APP推送设置限管理员
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -470,7 +544,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/setting/thirdaccess 07. 第三方帐号(限管理员)
* @api {get} api/system/setting/thirdaccess 09. 第三方帐号(限管理员)
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -540,7 +614,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/setting/file 08. 文件设置(限管理员)
* @api {get} api/system/setting/file 10. 文件设置(限管理员)
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -580,7 +654,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/demo 09. 获取演示帐号
* @api {get} api/system/demo 11. 获取演示帐号
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -604,7 +678,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/priority 10. 任务优先级
* @api {post} api/system/priority 12. 任务优先级
*
* @apiDescription 获取任务优先级、保存任务优先级
* @apiVersion 1.0.0
@@ -653,7 +727,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/column/template 11. 创建项目模板
* @api {post} api/system/column/template 13. 创建项目模板
*
* @apiDescription 获取创建项目模板、保存创建项目模板
* @apiVersion 1.0.0
@@ -700,7 +774,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/license 12. License
* @api {post} api/system/license 14. License
*
* @apiDescription 获取License信息、保存License限管理员
* @apiVersion 1.0.0
@@ -769,7 +843,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/info 13. 获取终端详细信息
* @api {get} api/system/get/info 15. 获取终端详细信息
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -798,7 +872,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/ip 14. 获取IP地址
* @api {get} api/system/get/ip 16. 获取IP地址
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -813,7 +887,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/cnip 15. 是否中国IP地址
* @api {get} api/system/get/cnip 17. 是否中国IP地址
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -830,7 +904,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/ipgcj02 16. 获取IP地址经纬度
* @api {get} api/system/get/ipgcj02 18. 获取IP地址经纬度
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -847,7 +921,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/ipinfo 17. 获取IP地址详细信息
* @api {get} api/system/get/ipinfo 19. 获取IP地址详细信息
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -864,7 +938,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/imgupload 18. 上传图片
* @api {post} api/system/imgupload 20. 上传图片
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -930,7 +1004,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/imgview 19. 浏览图片空间
* @api {get} api/system/get/imgview 21. 浏览图片空间
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1027,7 +1101,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/fileupload 20. 上传文件
* @api {post} api/system/fileupload 22. 上传文件
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1071,7 +1145,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/updatelog 21. 获取更新日志
* @api {get} api/system/get/updatelog 23. 获取更新日志
*
* @apiDescription 获取更新日志
* @apiVersion 1.0.0
@@ -1114,7 +1188,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/email/check 22. 邮件发送测试(限管理员)
* @api {get} api/system/email/check 24. 邮件发送测试(限管理员)
*
* @apiDescription 测试配置邮箱是否能发送邮件
* @apiVersion 1.0.0
@@ -1160,7 +1234,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/checkin/export 23. 导出签到数据(限管理员)
* @api {get} api/system/checkin/export 25. 导出签到数据(限管理员)
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -1329,7 +1403,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/checkin/down 24. 下载导出的签到数据
* @api {get} api/system/checkin/down 26. 下载导出的签到数据
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -1355,7 +1429,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/version 25. 获取版本号
* @api {get} api/system/version 27. 获取版本号
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -1392,7 +1466,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/prefetch 26. 预加载的资源
* @api {get} api/system/prefetch 28. 预加载的资源
*
* @apiVersion 1.0.0
* @apiGroup system

View File

@@ -611,7 +611,38 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/basic 11. 获取指定会员基础信息
* @api {get} api/users/search/ai 11. 获取AI机器人
*
* @apiDescription 搜索会员列表
* @apiVersion 1.0.0
* @apiGroup users
* @apiName search__ai
*
* @apiParam {String} type AI 类型比如openai
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function search__ai()
{
User::auth();
//
$type = trim(Request::input('type'));
$botName = "ai-{$type}";
if (!UserBot::isAiBot("{$botName}@bot.system")) {
return Base::retError('AI机器人不存在');
}
//
$botUser = User::botGetOrCreate($botName);
if (empty($botUser)) {
return Base::retError('AI机器人不存在');
}
return Base::retSuccess('success', $botUser);
}
/**
* @api {get} api/users/basic 12. 获取指定会员基础信息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -654,7 +685,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/lists 12. 会员列表(限管理员)
* @api {get} api/users/lists 13. 会员列表(限管理员)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -803,7 +834,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/operation 13. 操作会员(限管理员)
* @api {get} api/users/operation 14. 操作会员(限管理员)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1061,7 +1092,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/email/verification 14. 邮箱验证
* @api {get} api/users/email/verification 15. 邮箱验证
*
* @apiDescription 不需要token身份
* @apiVersion 1.0.0
@@ -1109,7 +1140,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/umeng/alias 15. 设置友盟别名
* @api {get} api/users/umeng/alias 16. 设置友盟别名
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1120,6 +1151,9 @@ class UsersController extends AbstractController
* - update: 更新(默认)
* - remove: 删除
* @apiParam {String} alias 别名
* @apiParam {String} [userAgent] 浏览器信息
* @apiParam {String} [deviceModel] 设备型号
* @apiParam {String} [isNotified] 是否有通知权限0不通知、1通知
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -1151,11 +1185,15 @@ class UsersController extends AbstractController
'alias' => $data['alias'],
'platform' => Base::platform(),
];
$version = $data['appVersion'] ? ($data['appVersionName'] . " ({$data['appVersion']})") : '';
$isNotified = trim($data['isNotified']) === 'true' || $data['isNotified'] === true ? 1 : intval($data['isNotified']);
$row = UmengAlias::where($inArray);
if ($row->exists()) {
$row->update([
'ua' => $data['userAgent'],
'device' => $data['deviceModel'],
'version' => $version,
'is_notified' => $isNotified,
'updated_at' => Carbon::now()
]);
return Base::retSuccess('别名已存在');
@@ -1163,6 +1201,8 @@ class UsersController extends AbstractController
$row = UmengAlias::createInstance(array_merge($inArray, [
'ua' => $data['userAgent'],
'device' => $data['deviceModel'],
'version' => $version,
'is_notified' => $isNotified,
]));
if ($row->save()) {
return Base::retSuccess('添加成功');
@@ -1172,7 +1212,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/meeting/open 16. 【会议】创建会议、加入会议
* @api {get} api/users/meeting/open 17. 【会议】创建会议、加入会议
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1290,7 +1330,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/meeting/link 17. 【会议】获取分享链接
* @api {get} api/users/meeting/link 18. 【会议】获取分享链接
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1319,7 +1359,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/meeting/tourist 18. 【会议】游客信息
* @api {get} api/users/meeting/tourist 19. 【会议】游客信息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1342,7 +1382,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/meeting/invitation 19. 【会议】发送邀请
* @api {get} api/users/meeting/invitation 20. 【会议】发送邀请
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1389,7 +1429,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/email/send 20. 发送邮箱验证码
* @api {get} api/users/email/send 21. 发送邮箱验证码
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1429,7 +1469,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/email/edit 21. 修改邮箱
* @api {get} api/users/email/edit 22. 修改邮箱
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1474,7 +1514,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/delete/account 22. 删除帐号
* @api {get} api/users/delete/account 23. 删除帐号
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1536,7 +1576,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/department/list 23. 部门列表(限管理员)
* @api {get} api/users/department/list 24. 部门列表(限管理员)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1555,7 +1595,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/department/add 24. 新建、修改部门(限管理员)
* @api {get} api/users/department/add 25. 新建、修改部门(限管理员)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1613,19 +1653,22 @@ class UsersController extends AbstractController
if (empty($parentDepartment)) {
return Base::retError('上级部门不存在或已被删除');
}
if ($parentDepartment->parent_id > 0) {
return Base::retError('上级部门层级错误');
if (count($parentDepartment->parents()) > 2) {
return Base::retError('部门层级最多只能创建3级');
}
if (UserDepartment::whereParentId($parent_id)->count() > 20) {
if ($id > 0 && UserDepartment::whereParentId($id)->whereId($parent_id)->exists()) {
return Base::retError('不能选择自己的子部门作为上级部门');
}
if (UserDepartment::whereParentId($parent_id)->count() >= 20) {
return Base::retError('每个部门最多只能创建20个子部门');
}
if ($id > 0 && UserDepartment::whereParentId($id)->exists()) {
return Base::retError('含有子部门无法修改上级部门');
}
}
if (empty($owner_userid) || !User::whereUserid($owner_userid)->exists()) {
return Base::retError('请选择正确的部门负责人');
}
if (UserDepartment::whereOwnerUserid($owner_userid)->count() >= 10) {
return Base::retError('每个用户最多只能负责10个部门');
}
//
$userDepartment->saveDepartment([
'name' => $name,
@@ -1634,11 +1677,11 @@ class UsersController extends AbstractController
], $dialog_useid);
Cache::forever("UserDepartment::rand", Base::generatePassword());
//
return Base::retSuccess($parent_id > 0 ? '保存成功' : '新建成功');
return Base::retSuccess($id > 0 ? '保存成功' : '新建成功');
}
/**
* @api {get} api/users/department/del 25. 删除部门(限管理员)
* @api {get} api/users/department/del 26. 删除部门(限管理员)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1661,6 +1704,9 @@ class UsersController extends AbstractController
if (empty($userDepartment)) {
return Base::retError('部门不存在或已被删除');
}
if (UserDepartment::whereParentId($id)->exists()) {
return Base::retError('含有子部门无法删除');
}
$userDepartment->deleteDepartment();
Cache::forever("UserDepartment::rand", Base::generatePassword());
//
@@ -1668,7 +1714,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/checkin/get 26. 获取签到设置
* @api {get} api/users/checkin/get 27. 获取签到设置
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1695,7 +1741,7 @@ class UsersController extends AbstractController
}
/**
* @api {post} api/users/checkin/save 27. 保存签到设置
* @api {post} api/users/checkin/save 28. 保存签到设置
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1770,7 +1816,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/checkin/list 28. 获取签到数据
* @api {get} api/users/checkin/list 29. 获取签到数据
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1817,7 +1863,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/socket/status 29. 获取socket状态
* @api {get} api/users/socket/status 30. 获取socket状态
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1840,7 +1886,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/key/client 30. 客户端KEY
* @api {get} api/users/key/client 31. 客户端KEY
*
* @apiDescription 获取客户端KEY用于加密数据发送给服务端
* @apiVersion 1.0.0
@@ -1882,7 +1928,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/bot/info 31. 机器人信息
* @api {get} api/users/bot/info 32. 机器人信息
*
* @apiDescription 需要token身份获取我的机器人信息
* @apiVersion 1.0.0
@@ -1933,7 +1979,7 @@ class UsersController extends AbstractController
}
/**
* @api {post} api/users/bot/edit 32. 编辑机器人
* @api {post} api/users/bot/edit 33. 编辑机器人
*
* @apiDescription 需要token身份编辑 我的机器人 或 管理员修改系统机器人 信息
* @apiVersion 1.0.0
@@ -2019,7 +2065,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/share/list 33. 获取分享列表
* @api {get} api/users/share/list 34. 获取分享列表
*
* @apiVersion 1.0.0
* @apiGroup users
@@ -2104,7 +2150,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/annual/report 34. 年度报告
* @api {get} api/users/annual/report 35. 年度报告
*
* @apiVersion 1.0.0
* @apiGroup users

View File

@@ -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";
}

View File

@@ -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;
}

View 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);
}
}

View File

@@ -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('文件不存在或已被删除');
}

View File

@@ -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;
}
}

View File

@@ -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();
});

View File

@@ -12,6 +12,7 @@ use App\Tasks\PushTask;
use App\Exceptions\ApiException;
use App\Observers\ProjectTaskObserver;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use League\HTMLToMarkdown\HtmlConverter;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
@@ -770,6 +771,7 @@ class ProjectTask extends AbstractModel
if (Arr::exists($data, 'times')) {
$oldAt = [Carbon::parse($this->start_at), Carbon::parse($this->end_at)];
$oldStringAt = $this->start_at ? ($oldAt[0]->toDateTimeString() . '~' . $oldAt[1]->toDateTimeString()) : '';
$isOverdue = $this->overdue;
$clearSubTaskTime = false;
$this->start_at = null;
$this->end_at = null;
@@ -835,7 +837,19 @@ class ProjectTask extends AbstractModel
}
});
}
$newStringAt = $this->start_at && !$clearSubTaskTime ? ($this->start_at->toDateTimeString() . '~' . $this->end_at->toDateTimeString()) : '';
$existAt = $this->start_at && !$clearSubTaskTime;
$newStringAt = $existAt ? ($this->start_at->toDateTimeString() . '~' . $this->end_at->toDateTimeString()) : '';
if ($isOverdue) {
$this->addLog("{任务}超期未完成", [
'cache' => [
'task_at' => $oldStringAt,
'change_at' => $newStringAt,
'over_sec' => ($existAt ? $this->end_at : Carbon::now())->diffInSeconds($oldAt[1]),
'owners' => $this->taskUser->where('owner', 1)->pluck('userid')->toArray(),
'assists' => $this->taskUser->where('owner', 0)->pluck('userid')->toArray(),
]
]);
}
$newDesc = $desc ? "(备注:{$desc}" : "";
$this->addLog("修改{任务}时间" . $newDesc, [
'change' => [$oldStringAt, $newStringAt]
@@ -1180,7 +1194,11 @@ class ProjectTask extends AbstractModel
'userid' => $userid,
], [
'important' => 1
]);
], function () use ($userid) {
return [
'bot' => User::isBot($userid) ? 1 : 0,
];
});
}
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->whereImportant(1)->remove();
});
@@ -1343,6 +1361,16 @@ class ProjectTask extends AbstractModel
if (!$this->hasOwner()) {
throw new ApiException('请先领取任务');
}
if ($this->overdue) {
$this->addLog("{任务}超期未完成", [
'cache' => [
'task_at' => $this->start_at . '~' . $this->end_at,
'over_sec' => Carbon::now()->diffInSeconds($this->end_at),
'owners' => $this->taskUser->where('owner', 1)->pluck('userid')->toArray(),
'assists' => $this->taskUser->where('owner', 0)->pluck('userid')->toArray(),
]
]);
}
if (empty($complete_name)) {
$complete_name = '已完成';
}
@@ -1833,6 +1861,11 @@ class ProjectTask extends AbstractModel
$taskUser->save();
}
}
// 子任务
ProjectTask::whereParentId($this->id)->change([
'project_id' => $projectId,
'column_id' => $columnId,
]);
//
if ($flowItemId) {
$flowItem = projectFlowItem::whereProjectId($projectId)->whereId($flowItemId)->first();
@@ -1860,6 +1893,67 @@ class ProjectTask extends AbstractModel
return true;
}
/**
* 生成AI上下文
* @return array
*/
public function AIContext()
{
$contexts = [];
if ($this->archived_at) {
$contexts[] = "任务状态:已归档";
$contexts[] = "归档时间:" . $this->archived_at;
} elseif ($this->complete_at) {
$contexts[] = "任务状态:已完成";
$contexts[] = "完成时间:" . $this->complete_at;
} elseif ($this->end_at && Carbon::parse($this->end_at)->lt(Carbon::now())) {
$contexts[] = "任务状态:已过期";
$contexts[] = "任务截止时间:" . $this->end_at;
} else {
$contexts[] = "任务状态:进行中";
if ($this->start_at) {
$contexts[] = "任务开始时间:" . $this->start_at;
}
if ($this->end_at) {
$contexts[] = "任务截止时间:" . $this->end_at;
}
}
$contexts[] = "当前系统时间:" . Carbon::now()->toDateTimeString();
if ($this->content) {
$taskDesc = $this->content?->getContentInfo();
if ($taskDesc) {
$converter = new HtmlConverter(['strip_tags' => true]);
$descContent = Base::cutStr($converter->convert($taskDesc['content']), 2000);
$contexts[] = <<<EOF
任务描述:
```md
{$descContent}
```
EOF;
}
}
$subTask = ProjectTask::select(['id', 'name', 'complete_at', 'end_at'])->whereParentId($this->id)->get();
if ($subTask->isNotEmpty()) {
$subTaskContent = $subTask->map(function($item) {
if ($item->complete_at) {
$status = " (已完成)";
} elseif ($item->end_at && Carbon::parse($item->end_at)->lt(Carbon::now())) {
$status = " (已过期)";
} else {
$status = " (进行中)";
}
return " - {$item->name} {$status}";
})->join("\n");
if ($subTaskContent) {
$contexts[] = <<<EOF
子任务列表:
{$subTaskContent}
EOF;
}
}
return $contexts;
}
/**
* 获取任务
* @param $task_id

View File

@@ -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)

View File

@@ -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();
}
}
});
}
}
/**

View File

@@ -139,12 +139,12 @@ class Report extends AbstractModel
// 如果设置了周期偏移量
empty( $offset ) || $now_dt->subWeeks( abs( $offset ) );
$now_dt->startOfWeek(); // 设置为当周第一天
return $now_dt->year . $now_dt->weekOfYear;
return now()->year . $now_dt->weekOfYear;
},
Report::DAILY => function() use ($now_dt, $offset) {
// 如果设置了周期偏移量
empty( $offset ) || $now_dt->subDays( abs( $offset ) );
return $now_dt->format("Ymd");
return now()->format("Ymd");
},
default => "",
};

View File

@@ -65,20 +65,34 @@ class Setting extends AbstractModel
$value['claude_key'] = $value['claude_token'];
}
$array = [];
$aiList = ['openai', 'claude', 'gemini', 'zhipu', 'qianwen', 'wenxin'];
$fieldList = ['key', 'model', 'agency', 'system', 'secret'];
$aiList = ['openai', 'claude', 'deepseek', 'gemini', 'grok', 'ollama', 'zhipu', 'qianwen', 'wenxin'];
$fieldList = ['key', 'secret', 'models', 'model', 'base_url', 'agency', 'temperature', 'system'];
foreach ($aiList as $aiName) {
foreach ($fieldList as $fieldName) {
$key = $aiName . '_' . $fieldName;
$array[$key] = $value[$key] ?: match ($key) {
'openai_model' => 'gpt-4o-mini',
'claude_model' => 'claude-3-5-sonnet-latest',
'gemini_model' => 'gemini-1.5-flash',
'zhipu_model' => 'glm-4',
'qianwen_model' => 'qwen-turbo',
'wenxin_model' => 'ernie-4.0-8k',
default => '',
};
$content = $value[$key] ? trim($value[$key]) : '';
switch ($fieldName) {
case 'models':
if ($content) {
$content = explode("\n", $content);
$content = array_filter($content);
}
if (empty($content)) {
$content = self::AIDefaultModels($aiName);
}
$content = implode("\n", $content);
break;
case 'model':
$models = Setting::AIModels2Array($array[$key . 's'], true);
$content = in_array($content, $models) ? $content : ($models[0] ?? '');
break;
case 'temperature':
if ($content) {
$content = floatval(min(1, max(0, floatval($content) ?: 0.7)));
}
break;
}
$array[$key] = $content;
}
}
$value = $array;
@@ -98,6 +112,121 @@ class Setting extends AbstractModel
return !!$array[$ai . '_key'];
}
/**
* AI默认模型
* @param string $ai
* @return array
*/
public static function AIDefaultModels($ai = 'openai')
{
return match ($ai) {
'openai' => [
'gpt-4 | GPT-4',
'gpt-4-turbo | GPT-4 Turbo',
'gpt-4o | GPT-4o',
'gpt-4o-mini | GPT-4o Mini',
'o1 | GPT-o1',
'o1-mini | GPT-o1 Mini',
'o3-mini | GPT-o3 Mini',
'gpt-3.5-turbo | GPT-3.5 Turbo',
'gpt-3.5-turbo-16k | GPT-3.5 Turbo 16K',
'gpt-3.5-turbo-0125 | GPT-3.5 Turbo 0125',
'gpt-3.5-turbo-1106 | GPT-3.5 Turbo 1106'
],
'claude' => [
'claude-3-5-sonnet-latest | Claude 3.5 Sonnet',
'claude-3-5-sonnet-20241022 | Claude 3.5 Sonnet 20241022',
'claude-3-5-haiku-latest | Claude 3.5 Haiku',
'claude-3-5-haiku-20241022 | Claude 3.5 Haiku 20241022',
'claude-3-opus-latest | Claude 3 Opus',
'claude-3-opus-20240229 | Claude 3 Opus 20240229',
'claude-3-haiku-20240307 | Claude 3 Haiku 20240307',
'claude-2.1 | Claude 2.1',
'claude-2.0 | Claude 2.0'
],
'deepseek' => [
'deepseek-chat | DeepSeek V3',
'deepseek-reasoner | DeepSeek R1'
],
'gemini' => [
'gemini-2.0-flash | Gemini 2.0 Flash',
'gemini-2.0-flash-lite-preview-02-05 | Gemini 2.0 Flash-Lite Preview',
'gemini-1.5-flash | Gemini 1.5 Flash',
'gemini-1.5-flash-8b | Gemini 1.5 Flash 8B',
'gemini-1.5-pro | Gemini 1.5 Pro',
'gemini-1.0-pro | Gemini 1.0 Pro'
],
'grok' => [
'grok-2-vision-1212 | Grok 2 Vision 1212',
'grok-2-vision | Grok 2 Vision',
'grok-2-vision-latest | Grok 2 Vision Latest',
'grok-2-1212 | Grok 2 1212',
'grok-2 | Grok 2',
'grok-2-latest | Grok 2 Latest',
'grok-vision-beta | Grok Vision Beta',
'grok-beta | Grok Beta',
],
'zhipu' => [
'glm-4 | GLM-4',
'glm-4-plus | GLM-4 Plus',
'glm-4-air | GLM-4 Air',
'glm-4-airx | GLM-4 AirX',
'glm-4-long | GLM-4 Long',
'glm-4-flash | GLM-4 Flash',
'glm-4v | GLM-4V',
'glm-4v-plus | GLM-4V Plus',
'glm-3-turbo | GLM-3 Turbo'
],
'qianwen' => [
'qwen-max | QWEN Max',
'qwen-max-latest | QWEN Max Latest',
'qwen-turbo | QWEN Turbo',
'qwen-turbo-latest | QWEN Turbo Latest',
'qwen-plus | QWEN Plus',
'qwen-plus-latest | QWEN Plus Latest',
'qwen-long | QWEN Long'
],
'wenxin' => [
'ernie-4.0-8k | Ernie 4.0 8K',
'ernie-4.0-8k-latest | Ernie 4.0 8K Latest',
'ernie-4.0-turbo-128k | Ernie 4.0 Turbo 128K',
'ernie-4.0-turbo-8k | Ernie 4.0 Turbo 8K',
'ernie-3.5-128k | Ernie 3.5 128K',
'ernie-3.5-8k | Ernie 3.5 8K',
'ernie-speed-128k | Ernie Speed 128K',
'ernie-speed-8k | Ernie Speed 8K',
'ernie-lite-8k | Ernie Lite 8K',
'ernie-tiny-8k | Ernie Tiny 8K'
],
default => [],
};
}
/**
* AI模型转数组
* @param $models
* @param bool $retValue
* @return array
*/
public static function AIModels2Array($models, $retValue = false)
{
$list = is_array($models) ? $models : explode("\n", $models);
$array = [];
foreach ($list as $item) {
$arr = Base::newTrim(explode('|', $item . '|'));
if ($arr[0]) {
$array[] = [
'value' => $arr[0],
'label' => $arr[1] ?: $arr[0]
];
}
}
if ($retValue) {
return array_column($array, 'value');
}
return $array;
}
/**
* 验证邮箱地址(过滤忽略地址)
* @param $array

View File

@@ -15,7 +15,9 @@ use Hedeqiang\UMeng\IOS;
* @property string|null $alias 别名
* @property string|null $platform 平台类型
* @property string|null $device 设备类型
* @property string|null $version 应用版本号
* @property string|null $ua userAgent
* @property int|null $is_notified 通知权限
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
@@ -31,10 +33,12 @@ use Hedeqiang\UMeng\IOS;
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereDevice($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereIsNotified($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias wherePlatform($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereUa($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereVersion($value)
* @mixin \Eloquent
*/
class UmengAlias extends AbstractModel
@@ -189,7 +193,11 @@ class UmengAlias extends AbstractModel
$lists = $rows->take(5)->groupBy('platform'); // 每个会员最多推送5个别名
foreach ($lists as $platform => $list) {
$alias = $list->pluck('alias')->implode(',');
self::pushMsgToAlias($alias, $platform, $array);
try {
self::pushMsgToAlias($alias, $platform, $array);
} catch (\Exception $e) {
info("[PushMsg] fail: " . $e->getMessage());
}
}
}
});

View File

@@ -242,6 +242,19 @@ class User extends AbstractModel
return in_array('admin', $this->identity);
}
/**
* 返回是否AI机器人
* @return bool
*/
public function isAiBot(&$aiName = '')
{
if (preg_match('/^ai-(.*?)@bot\.system$/', $this->email, $matches)) {
$aiName = $matches[1];
return true;
}
return false;
}
/**
* 判断是否管理员
*/
@@ -593,8 +606,14 @@ class User extends AbstractModel
return url("images/avatar/default_openai.png");
case 'ai-claude@bot.system':
return url("images/avatar/default_claude.png");
case 'ai-deepseek@bot.system':
return url("images/avatar/default_deepseek.png");
case 'ai-gemini@bot.system':
return url("images/avatar/default_gemini.png");
case 'ai-grok@bot.system':
return url("images/avatar/default_grok.png");
case 'ai-ollama@bot.system':
return url("images/avatar/default_ollama.png");
case 'ai-zhipu@bot.system':
return url("images/avatar/default_zhipu.png");
case 'bot-manager@bot.system':
@@ -683,4 +702,21 @@ class User extends AbstractModel
}
return $botUser;
}
/**
* 是否机器人
* @param $userid
* @return bool|mixed
*/
public static function isBot($userid)
{
if (empty($userid)) {
return false;
}
$userid = intval($userid);
if (RequestContext::has("isBot_" . $userid)) {
return RequestContext::get("isBot_" . $userid);
}
return (bool)User::find($userid)?->bot;
}
}

View File

@@ -55,6 +55,16 @@ class UserBot extends AbstractModel
return str_ends_with($email, '@bot.system') && self::systemBotName($email);
}
/**
* 判断是否系统AI机器人
* @param $email
* @return bool
*/
public static function isAiBot($email)
{
return str_starts_with($email, 'ai-') && self::isSystemBot($email);
}
/**
* 系统机器人名称
* @param $name string 邮箱 或 邮箱前缀
@@ -73,10 +83,13 @@ class UserBot extends AbstractModel
'approval-alert' => '审批',
'ai-openai' => 'ChatGPT',
'ai-claude' => 'Claude',
'ai-wenxin' => '文心一言',
'ai-qianwen' => '通义千问',
'ai-deepseek' => 'DeepSeek',
'ai-gemini' => 'Gemini',
'ai-grok' => 'Grok',
'ai-ollama' => 'Ollama',
'ai-zhipu' => '智谱清言',
'ai-qianwen' => '通义千问',
'ai-wenxin' => '文心一言',
'bot-manager' => '机器人管理',
'meeting-alert' => '会议通知',
'okr-alert' => 'OKR提醒',
@@ -177,11 +190,35 @@ class UserBot extends AbstractModel
];
default:
if (preg_match('/^ai-(.*?)@bot\.system$/', $email)) {
return [
[
if (preg_match('/^ai-(.*?)@bot\.system$/', $email, $match)) {
if (!Base::judgeClientVersion('0.42.62')) {
return [
'key' => '%3A.clear',
'label' => Doo::translate('清空上下文')
];
}
$aibotSetting = Base::setting('aibotSetting');
$aibotModel = $aibotSetting[$match[1] . '_model'];
$aibotModels = Setting::AIModels2Array($aibotSetting[$match[1] . '_models']);
if (empty($aibotModels)) {
return [];
}
return [
[
'key' => '~ai-model-select',
'label' => Doo::translate('选择模型'),
'config' => [
'model' => $aibotModel,
'models' => $aibotModels
]
],
[
'key' => '~ai-session-create',
'label' => Doo::translate('开启新会话'),
],
[
'key' => '~ai-session-history',
'label' => Doo::translate('历史会话'),
]
];
}

View File

@@ -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();
}

View File

@@ -19,6 +19,7 @@ use Illuminate\Support\Facades\DB;
* @property int $id
* @property string|null $type 对话类型
* @property string|null $group_type 聊天室类型
* @property int|null $session_id 会话ID
* @property string|null $name 对话名称
* @property string $avatar 头像(群)
* @property int|null $owner_id 群主用户ID
@@ -48,6 +49,7 @@ use Illuminate\Support\Facades\DB;
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereLinkId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereOwnerId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereSessionId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereTopMsgId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereTopUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereType($value)
@@ -265,7 +267,9 @@ class WebSocketDialog extends AbstractModel
// 未读消息
$data = array_merge($data, self::generateUnread($data['id'], $userid));
// 对话人数
$data['people'] = $data['people'] ?? WebSocketDialogUser::whereDialogId($data['id'])->count();
if (!isset($data['people'])) {
$data = array_merge($data, self::generatePeople($data['id']));
}
// 有待办
$data['todo_num'] = $data['todo_num'] ?? WebSocketDialogMsgTodo::whereDialogId($data['id'])->whereUserid($userid)->whereDoneAt(null)->count();
// 最后消息
@@ -330,6 +334,9 @@ class WebSocketDialog extends AbstractModel
}
break;
}
if (empty($data['pinyin'])) {
$data['pinyin'] = Base::cn2pinyin($data['name']);
}
// 已存在的消息类型
if ($hasData === true) {
@@ -398,6 +405,26 @@ class WebSocketDialog extends AbstractModel
return $data;
}
/**
* 获取对话人数
* @param $dialogId
* @return array
*/
public static function generatePeople($dialogId)
{
$counts = WebSocketDialogUser::whereDialogId($dialogId)
->groupBy('bot')
->selectRaw('bot, COUNT(*) as count')
->pluck('count', 'bot');
$userCount = $counts->get(0, 0); // 非机器人数量
$botCount = $counts->get(1, 0); // 机器人数量
return [
'people' => $userCount + $botCount,
'people_user' => $userCount,
'people_bot' => $botCount,
];
}
/**
* 加入聊天室
* @param int|array $userid 加入的会员ID或会员ID组
@@ -420,7 +447,11 @@ class WebSocketDialog extends AbstractModel
WebSocketDialogUser::updateInsert([
'dialog_id' => $this->id,
'userid' => $value,
], $updateData, [], $isInsert);
], $updateData, function() use ($value) {
return [
'bot' => User::isBot($value) ? 1 : 0,
];
}, $isInsert);
if ($isInsert) {
WebSocketDialogMsg::sendMsg(null, $this->id, 'notice', [
'notice' => User::userid2nickname($value) . " 已加入群组"
@@ -429,10 +460,9 @@ class WebSocketDialog extends AbstractModel
}
}
});
$this->pushMsg("groupUpdate", [
'id' => $this->id,
'people' => WebSocketDialogUser::whereDialogId($this->id)->count()
]);
$data = WebSocketDialog::generatePeople($this->id);
$data['id'] = $this->id;
$this->pushMsg("groupUpdate", $data);
return true;
}
@@ -489,10 +519,9 @@ class WebSocketDialog extends AbstractModel
});
});
//
$this->pushMsg("groupUpdate", [
'id' => $this->id,
'people' => WebSocketDialogUser::whereDialogId($this->id)->count()
]);
$data = WebSocketDialog::generatePeople($this->id);
$data['id'] = $this->id;
$this->pushMsg("groupUpdate", $data);
}
/**
@@ -741,6 +770,17 @@ class WebSocketDialog extends AbstractModel
'dialog_id' => $dialog->id,
'userid' => $receiver,
])->save();
//
if ($user->isAiBot() || User::find($receiver)?->isAiBot()) {
$session = WebSocketDialogSession::create([
'dialog_id' => $dialog->id,
'status' => 1,
'title' => '',
]);
$session->save();
$dialog->session_id = $session->id;
$dialog->save();
}
return $dialog;
});
}
@@ -755,6 +795,9 @@ class WebSocketDialog extends AbstractModel
*/
public static function getUserDialog($userid1, $userid2, $ttl, &$cacheKey = null)
{
if ($userid1 == $userid2) {
$userid2 = 0;
}
$userids = [$userid1, $userid2];
sort($userids);
$cacheKey = "Dialog::user:" . implode('-', $userids);

View File

@@ -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)

View File

@@ -18,6 +18,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property int $id
* @property int|null $dialog_id 对话ID
* @property string|null $dialog_type 对话类型
* @property int|null $session_id 会话ID
* @property int|null $userid 发送会员ID
* @property string|null $type 消息类型
* @property string|null $mtype 消息类型(用于搜索)
@@ -30,6 +31,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property int|null $todo 设为待办会员ID
* @property int|null $link 是否存在链接
* @property int|null $modify 是否编辑
* @property int|null $bot 是否机器人的消息
* @property int|null $reply_num 有多少条回复
* @property int|null $reply_id 回复ID
* @property int|null $forward_id 转发ID
@@ -50,6 +52,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereBot($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDialogId($value)
@@ -67,6 +70,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereReplyId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereReplyNum($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereSend($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereSessionId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereTag($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereTodo($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereType($value)
@@ -301,7 +305,12 @@ class WebSocketDialogMsg extends AbstractModel
];
//
$dialog = WebSocketDialog::find($this->dialog_id);
$dialog?->pushMsg('update', $resData);
if ($dialog) {
$dialog->pushMsg('update', $resData);
WebSocketDialogUser::whereDialogId($dialog->id)->change([
'updated_at' => Carbon::now()->toDateTimeString('millisecond'),
]);
}
//
return Base::retSuccess('success', $resData);
}
@@ -590,6 +599,9 @@ class WebSocketDialogMsg extends AbstractModel
case 'text':
return self::previewTextMsg($data['msg'], $preserveHtml);
case 'longtext':
return $data['msg']['desc'] ? Base::cutStr($data['msg']['desc'], 50) : ("[" . Doo::translate("长文本") . "]");
case 'vote':
$action = Doo::translate("投票");
return "[{$action}] " . self::previewTextMsg($data['msg'], $preserveHtml);
@@ -652,6 +664,10 @@ class WebSocketDialogMsg extends AbstractModel
$text = $msgData['text'] ?? '';
if (!$text) return '';
if ($msgData['type'] === 'md') {
$text = preg_replace("/:::\s*reasoning[\s\S]*?:::/", "", $text);
if (preg_match('/:::\s*reasoning\s+/', $text)) {
return Doo::translate('思考中...');
}
$text = Base::markdown2html($text);
$text = self::previewConvertTaskList($text);
}
@@ -751,9 +767,15 @@ class WebSocketDialogMsg extends AbstractModel
$key = '';
switch ($this->type) {
case 'text':
if (!preg_match("/<span[^>]*?data-quick-key=([\"'])([^\"']+?)\\1[^>]*?>/is", $this->msg['text'])) {
$key = strip_tags($this->msg['text']);
if (preg_match("/<span[^>]*?data-quick-key=([\"'])([^\"']+?)\\1[^>]*?>/i", $this->msg['text'])) {
break;
}
$key = $this->msg['text'];
if ($this->msg['type'] === 'md') {
$key = preg_replace("/:::\s*reasoning[\s\S]*?:::/", "", $key);
$key = Base::markdown2html($key);
}
$key = strip_tags($key);
break;
case 'vote':
@@ -772,14 +794,24 @@ class WebSocketDialogMsg extends AbstractModel
break;
}
}
$key = str_replace(["&quot;", "&amp;", "&lt;", "&gt;"], "", $key);
$key = str_replace(["\r", "\n", "\t", "&nbsp;"], " ", $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(["&quot;", "&amp;", "&lt;", "&gt;"], "", $content);
$content = str_replace(["\r", "\n", "\t", "&nbsp;"], " ", $content);
$content = preg_replace("/^\/[A-Za-z]+/", " ", $content);
$content = preg_replace("/\s+/", " ", $content);
return trim($content);
}
/**
* 返回引用消息(如果是文本只取预览)
* @return array|mixed
@@ -867,8 +899,16 @@ class WebSocketDialogMsg extends AbstractModel
$imageSaveLocal = Base::settingFind("system", "image_save_local");
preg_match_all("/<img[^>]*?src=([\"'])(.*?(png|jpg|jpeg|webp|gif).*?)\\1[^>]*?>/is", $text, $matchs);
foreach ($matchs[2] as $key => $str) {
$parsed = parse_url($str);
if (str_starts_with($parsed['path'], "/uploads/")) {
$relativePath = ltrim($parsed['path'], "/");
$relativePath = Base::thumbRestore($relativePath);
if (file_exists(public_path($relativePath))) {
$str = "{{RemoteURL}}{$relativePath}";
}
}
if ($imageSaveLocal === 'close') {
$imageSize = getimagesize($str);
$imageSize = @getimagesize($str);
if ($imageSize === false) {
$imageSize = ["auto", "auto"];
}
@@ -1121,6 +1161,7 @@ class WebSocketDialogMsg extends AbstractModel
}
//
$updateData = [
'type' => $type,
'mtype' => $mtype,
'link' => $link,
'msg' => array_merge($oldMsg, $msg),
@@ -1163,12 +1204,14 @@ class WebSocketDialogMsg extends AbstractModel
$dialogMsg = self::createInstance([
'dialog_id' => $dialog_id,
'dialog_type' => $dialog->type,
'session_id' => $dialog->session_id,
'reply_id' => $reply_id,
'forward_id' => $forward_id,
'userid' => $sender,
'type' => $type,
'mtype' => $mtype,
'link' => $link,
'bot' => User::isBot($sender) ? 1 : 0,
'msg' => $msg,
'read' => 0,
]);
@@ -1176,6 +1219,8 @@ class WebSocketDialogMsg extends AbstractModel
$dialogMsg->send = 1;
$dialogMsg->generateKeyAndSave($search_key);
//
WebSocketDialogSession::updateTitle($dialogMsg->session_id, $dialogMsg);
//
if ($dialogMsg->type === 'meeting') {
MeetingMsg::createInstance([
'meetingid' => $dialogMsg->msg['meetingid'],

View 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();
}
});
}
}

View File

@@ -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)

View File

@@ -14,6 +14,7 @@ use Overtrue\Pinyin\Pinyin;
use Redirect;
use Request;
use Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\File;
use Validator;
@@ -1294,7 +1295,7 @@ class Base
* 获取或设置
* @param $setname // 配置名称
* @param bool $array // 保存内容
* @param false $isUpdate // 保存内容为更新模式,默认否
* @param bool $isUpdate // 保存内容为更新模式,默认否
* @return array
*/
public static function setting($setname, $array = false, $isUpdate = false)
@@ -2017,7 +2018,7 @@ class Base
$type = ['mp3', 'wma', 'wav', 'amr'];
break;
case 'excel':
$type = ['xls', 'xlsx'];
$type = ['xls', 'xlsx', 'xlsm', 'xlt', 'xltx', 'ods', 'ots', 'csv', 'tsv'];
break;
case 'app':
$type = ['apk'];
@@ -2758,12 +2759,12 @@ class Base
}
/**
* BinaryFileResponse 下载文件
* DownloadFileResponse 下载文件
* @param File|\SplFileInfo|string $file 文件对象或路径
* @param string|null $name 下载文件名
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
* @return StreamedResponse
*/
public static function BinaryFileResponse($file, $name = null)
public static function DownloadFileResponse($file, $name = null)
{
try {
// 处理文件对象
@@ -2780,6 +2781,12 @@ class Base
throw new FileException('File must be readable and exist.');
}
// 获取文件信息
$size = $file->getSize();
if ($size === false || $size < 0) {
throw new FileException('Unable to determine file size.');
}
// 处理文件名
if (empty($name)) {
$name = basename($file->getPathname());
@@ -2791,34 +2798,98 @@ class Base
$name = Base::cutStr($name, 180);
$name = str_replace(['"', '<', '>', '|', '/', '\\', '?', ':'], '', $name);
// IE 浏览器特殊处理
$encodedName = $name;
if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/MSIE|Internet Explorer|Trident/i", $_SERVER['HTTP_USER_AGENT'])) {
$encodedName = rawurlencode($name);
// 获取MIME类型
$mimeType = $file->getMimeType();
if (empty($mimeType)) {
$mimeType = 'application/octet-stream';
}
// 创建响应对象
return new \Symfony\Component\HttpFoundation\BinaryFileResponse($file->getPathname(), 200, [
'Content-Type' => $file->getMimeType() ?: 'application/octet-stream',
// 处理 Range 请求
$start = 0;
$end = $size - 1;
$length = $size;
$isRangeRequest = false;
if (isset($_SERVER['HTTP_RANGE'])) {
$range = str_replace('bytes=', '', $_SERVER['HTTP_RANGE']);
if (preg_match('/^(\d+)-(\d*)$/', $range, $matches)) {
$start = intval($matches[1]);
$end = !empty($matches[2]) ? intval($matches[2]) : $size - 1;
// 验证范围的有效性
if ($start >= 0 && $end < $size && $start <= $end) {
$length = $end - $start + 1;
$isRangeRequest = true;
} else {
$start = 0;
$end = $size - 1;
}
}
}
// 设置基本响应头
$headers = [
'Content-Type' => $mimeType,
'Content-Disposition' => sprintf(
'attachment; filename="%s"; filename*=UTF-8\'\'%s',
$encodedName,
$name,
rawurlencode($name)
),
// 添加缓存控制和安全相关的头
'Cache-Control' => 'private, no-transform, no-store, must-revalidate',
'Pragma' => 'public',
'Expires' => '0',
'Accept-Ranges' => 'bytes', // 支持断点续传
'X-Content-Type-Options' => 'nosniff', // 安全相关
]);
'Accept-Ranges' => 'bytes',
'Cache-Control' => 'private, no-transform, no-store, must-revalidate, max-age=0',
'Content-Length' => $length,
'Last-Modified' => gmdate('D, d M Y H:i:s', $file->getMTime()) . ' GMT',
'ETag' => sprintf('"%s"', md5_file($file->getPathname()))
];
if ($isRangeRequest) {
$headers['Content-Range'] = "bytes {$start}-{$end}/{$size}";
$statusCode = 206;
} else {
$statusCode = 200;
}
// 创建流式响应
return new StreamedResponse(
function () use ($file, $start, $length) {
$handle = fopen($file->getPathname(), 'rb');
if ($handle === false) {
throw new FileException('Cannot open file for reading');
}
if (fseek($handle, $start) === -1) {
fclose($handle);
throw new FileException('Cannot seek to position ' . $start);
}
$remaining = $length;
$bufferSize = 8192; // 8KB chunks
while ($remaining > 0 && !feof($handle)) {
$readSize = min($bufferSize, $remaining);
$buffer = fread($handle, $readSize);
if ($buffer === false) {
break;
}
echo $buffer;
flush();
$remaining -= strlen($buffer);
}
fclose($handle);
},
$statusCode,
$headers
);
} catch (\Exception $e) {
\Log::error('File download failed', [
'error' => $e->getMessage(),
'file' => $file->getPathname() ?? null,
'trace' => $e->getTraceAsString(),
'file' => $file ?? null,
'name' => $name ?? null,
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null, // 添加更多调试信息
'ip' => request()->ip()
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null,
'ip' => request()->ip(),
'range' => $_SERVER['HTTP_RANGE'] ?? null
]);
abort(403, 'File download failed');
}

View File

@@ -0,0 +1,308 @@
<?php
namespace App\Module\ElasticSearch;
use Elastic\Elasticsearch\ClientBuilder;
use Elastic\Elasticsearch\Exception\MissingParameterException;
use Illuminate\Support\Facades\Log;
/**
* Elasticsearch基础类
*
* Class ElasticSearchBase
* @package App\Module\ElasticSearch
*/
class ElasticSearchBase
{
/**
* Elasticsearch客户端实例
*
* @var \Elastic\Elasticsearch\Client
*/
protected $client;
/**
* 当前操作的索引名称
*
* @var string
*/
protected $index;
/**
* 构造函数
*
* @param null $index 默认索引名称
* @throws \Elastic\Elasticsearch\Exception\ConfigException
*/
public function __construct($index = null)
{
$host = env('ELASTICSEARCH_HOST', env('APP_IPPR') . '.15');
$port = env('ELASTICSEARCH_PORT', '9200');
$scheme = env('ELASTICSEARCH_SCHEME', 'http');
$user = env('ELASTICSEARCH_USER', '');
$pass = env('ELASTICSEARCH_PASS', '');
$verifi = env('ELASTICSEARCH_VERIFI', false);
$ca = env('ELASTICSEARCH_CA', '');
$key = env('ELASTICSEARCH_KEY', '');
$cert = env('ELASTICSEARCH_CERT', '');
// 为8.x版本客户端配置连接
$config = [
'hosts' => ["{$scheme}://{$host}:{$port}"]
];
// 如果设置了用户名和密码
if (!empty($user)) {
$config['basicAuthentication'] = [$user, $pass];
}
$config['SSLVerification'] = $verifi;
if ($verifi) {
$config['SSLCert'] = $cert;
$config['CABundle'] = $ca;
$config['SSLKey'] = $key;
}
// 8.x版本使用ClientBuilder::fromConfig创建客户端
$this->client = ClientBuilder::fromConfig($config);
if ($index) {
$this->index = $index;
}
}
/**
* 设置索引名称
*
* @param string $index
* @return $this
*/
public function setIndex($index)
{
$this->index = $index;
return $this;
}
/**
* 检查索引是否存在
*
* @return bool
* @throws \Exception
*/
public function indexExists()
{
$params = ['index' => $this->index];
return $this->client->indices()->exists($params)->asBool();
}
/**
* 创建索引
*
* @param array $settings 索引设置
* @param array $mappings 字段映射
* @return array
*/
public function createIndex($settings = [], $mappings = [])
{
$params = [
'index' => $this->index
];
$body = [];
if (!empty($settings)) {
$body['settings'] = $settings;
}
if (!empty($mappings)) {
$body['mappings'] = $mappings;
}
if (!empty($body)) {
$params['body'] = $body;
}
try {
// 在8.x中索引操作位于indices()命名空间
return $this->client->indices()->create($params)->asArray();
} catch (\Exception $e) {
Log::error('创建Elasticsearch索引失败: ' . $e->getMessage());
return ['error' => $e->getMessage()];
}
}
/**
* 删除索引
* @return array
*/
public function deleteIndex()
{
try {
$params = ['index' => $this->index];
return $this->client->indices()->delete($params)->asArray();
} catch (\Exception $e) {
Log::error('删除Elasticsearch索引失败: ' . $e->getMessage());
return ['error' => $e->getMessage()];
}
}
/**
* 批量操作(批量添加/更新/删除文档)
*
* @param array $operations 批量操作的数据
* @return array
*/
public function bulk($operations)
{
try {
// 在8.x中批量操作API签名相同但内部实现有所变化
return $this->client->bulk($operations)->asArray();
} catch (\Exception $e) {
Log::error('批量操作失败: ' . $e->getMessage());
return ['error' => $e->getMessage()];
}
}
/**
* 索引单个文档
*
* @param array $document 文档数据
* @param string $id 文档ID
* @param string|null $routing 路由值,用于父子文档
* @return array
*/
public function indexDocument($document, $id, $routing = null)
{
$params = [
'index' => $this->index,
'id' => $id,
'body' => $document
];
if ($routing) {
$params['routing'] = $routing;
}
try {
return $this->client->index($params)->asArray();
} catch (\Exception $e) {
Log::error('索引文档失败: ' . $e->getMessage());
return ['error' => $e->getMessage()];
}
}
/**
* 删除文档
*
* @param string $id 文档ID
* @param string|null $routing 路由值,用于父子文档
* @return array
*/
public function deleteDocument($id, $routing = null)
{
$params = [
'index' => $this->index,
'id' => $id
];
if ($routing) {
$params['routing'] = $routing;
}
try {
return $this->client->delete($params)->asArray();
} catch (MissingParameterException $e) {
// 文档不存在时返回成功
return ['result' => 'not_found', 'error' => $e->getMessage()];
} catch (\Exception $e) {
Log::error('删除文档失败: ' . $e->getMessage());
return ['error' => $e->getMessage()];
}
}
/**
* 刷新索引
* @return array
*/
public function refreshIndex()
{
$params = [
'index' => $this->index
];
try {
return $this->client->indices()->refresh($params)->asArray();
} catch (\Exception $e) {
Log::error('刷新索引失败: ' . $e->getMessage());
return ['error' => $e->getMessage()];
}
}
/**
* 检查索引映射
* @return array
*/
public function checkIndexMapping()
{
try {
return $this->client->indices()->getMapping(['index' => $this->index])->asArray();
} catch (\Exception $e) {
return ['error' => $e->getMessage()];
}
}
/**
* 通用搜索方法
*
* @param array $query 搜索查询
* @param int $from 起始位置
* @param int $size 返回结果数量
* @param array $sort 排序规则
* @return array
*/
public function search($query, $from = 0, $size = 10, $sort = [])
{
$params = [
'index' => $this->index,
'body' => [
'query' => $query,
'from' => $from,
'size' => $size
]
];
if (!empty($sort)) {
$params['body']['sort'] = $sort;
}
try {
return $this->client->search($params)->asArray();
} catch (\Exception $e) {
Log::error('搜索失败: ' . $e->getMessage());
return ['error' => $e->getMessage(), 'hits' => ['total' => ['value' => 0], 'hits' => []]];
}
}
/**
* 索引名称
*/
const indexName = 'default';
/**
* 获取索引名称
* @param string $index 索引名称
* @param string|null $prefix 索引前缀
* @param string|null $subfix 索引后缀
* @return string
*/
public static function indexName($index = '', $prefix = '', $subfix = '')
{
$index = $index ?: static::indexName;
$prefix = $prefix ?: env('ES_INDEX_PREFIX', '');
$subfix = $subfix ?: env('ES_INDEX_SUFFIX', '');
if ($prefix) {
$index = rtrim($prefix, '_') . '_' . $index;
}
if ($subfix) {
$index = $index . '_' . ltrim($subfix, '_');
}
return $index;
}
}

View 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()];
}
}
}

View 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());
}
}
}

View File

@@ -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
View 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 . '>';
}
}
}
}
}

View File

@@ -0,0 +1,182 @@
<?php
namespace App\Module;
use Exception;
use Illuminate\Support\Facades\Log;
use PhpOffice\PhpWord\IOFactory;
use Smalot\PdfParser\Parser;
class TextExtractor
{
/**
* 从文件中提取文本
*
* @param string $filePath 文件路径
* @return string
* @throws Exception
*/
public function extractText(string $filePath): string
{
if (!file_exists($filePath)) {
throw new Exception("文件不存在: {$filePath}");
}
$fileExtension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
try {
return match($fileExtension) {
'pdf' => $this->extractFromPDF($filePath),
'docx' => $this->extractFromDOCX($filePath),
'ipynb' => $this->extractFromIPYNB($filePath),
default => $this->extractFromOtherFile($filePath),
};
} catch (Exception $e) {
Log::error('文本提取失败', [
'file' => $filePath,
'error' => $e->getMessage()
]);
throw $e;
}
}
/**
* 从PDF文件中提取文本
*
* @param string $filePath
* @return string
* @throws Exception
*/
protected function extractFromPDF(string $filePath): string
{
try {
$parser = new Parser();
$pdf = $parser->parseFile($filePath);
return $pdf->getText();
} catch (Exception $e) {
Log::error('PDF解析失败', [
'file' => $filePath,
'error' => $e->getMessage()
]);
throw new Exception("PDF文本提取失败: " . $e->getMessage());
}
}
/**
* 从DOCX文件中提取文本
*
* @param string $filePath
* @return string
* @throws Exception
*/
protected function extractFromDOCX(string $filePath): string
{
$phpWord = IOFactory::load($filePath);
$text = '';
foreach ($phpWord->getSections() as $section) {
foreach ($section->getElements() as $element) {
if (method_exists($element, 'getText')) {
$text .= $element->getText() . "\n";
}
}
}
return $text;
}
/**
* 从Jupyter Notebook文件中提取文本
*
* @param string $filePath
* @return string
* @throws Exception
*/
protected function extractFromIPYNB(string $filePath): string
{
$content = file_get_contents($filePath);
$notebook = json_decode($content, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception("IPYNB文件解析失败: " . json_last_error_msg());
}
$extractedText = '';
foreach ($notebook['cells'] ?? [] as $cell) {
if (in_array($cell['cell_type'] ?? '', ['markdown', 'code']) && isset($cell['source'])) {
$source = $cell['source'];
$extractedText .= is_array($source)
? implode("\n", $source)
: $source;
$extractedText .= "\n";
}
}
return $extractedText;
}
/**
* 从其他类型文件中提取文本
*
* @param string $filePath
* @return string
* @throws Exception
*/
protected function extractFromOtherFile(string $filePath): string
{
if ($this->isBinaryFile($filePath)) {
throw new Exception("无法读取该类型文件的文本内容");
}
return file_get_contents($filePath);
}
/**
* 检查文件是否为二进制文件
*
* @param string $filePath
* @return bool
*/
protected function isBinaryFile(string $filePath): bool
{
$finfo = finfo_open(FILEINFO_MIME);
$mimeType = finfo_file($finfo, $filePath);
finfo_close($finfo);
return !str_contains($mimeType, 'text/')
&& !str_contains($mimeType, 'application/json')
&& !str_contains($mimeType, 'application/xml');
}
/** ********************************************************************* */
/** ********************************************************************* */
/** ********************************************************************* */
/**
* 获取文件内容
* @param $filePath
* @param float|int $maxSize 最大文件大小单位字节默认300KB
* @return string
*/
public static function getFileContent($filePath, float|int $maxSize = 300 * 1024)
{
if (!file_exists($filePath) || !is_file($filePath)) {
return "(Failed to read contents of {$filePath})";
}
if (filesize($filePath) > $maxSize) {
return "(File size exceeds " . Base::readableBytes($maxSize) . ", unable to display content)";
}
$te = new self();
try {
$isBinary = $te->isBinaryFile($filePath);
if ($isBinary) {
return "(Binary file, unable to display content)";
}
return $te->extractText($filePath);
} catch (Exception $e) {
return "(Failed to read contents of {$filePath}: {$e->getMessage()})";
}
}
}

View File

@@ -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]);
}
}

View File

@@ -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);
}
}
/**

View 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)
{
//
}
}

View File

@@ -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);
}
/**

View File

@@ -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);
}
}

View File

@@ -103,6 +103,10 @@ class WebSocketService implements WebSocketHandlerInterface
case 'receipt':
return;
// 握手信息
case 'handshake':
break;
// 访问状态
case 'path':
$row = WebSocket::whereFd($frame->fd)->first();

View File

@@ -2,19 +2,21 @@
namespace App\Tasks;
use App\Models\FileContent;
use App\Models\Project;
use App\Models\ProjectTask;
use App\Models\User;
use App\Models\UserBot;
use App\Models\UserDepartment;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogConfig;
use App\Models\WebSocketDialogMsg;
use App\Module\Base;
use App\Module\Doo;
use App\Module\Ihttp;
use App\Module\TextExtractor;
use Carbon\Carbon;
use DB;
use League\HTMLToMarkdown\HtmlConverter;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
@@ -86,7 +88,7 @@ class BotReceiveMsgTask extends AbstractTask
}
// 提取指令
$command = $this->extractCommand($msg, $this->mention);
$command = $this->extractCommand($msg, $botUser->isAiBot(), $this->mention);
if (empty($command)) {
return;
}
@@ -415,10 +417,9 @@ class BotReceiveMsgTask extends AbstractTask
$userBot = null;
$extras = [];
$errorContent = null;
if (preg_match('/^ai-(.*?)@bot\.system$/', $botUser->email, $matches)) {
if ($botUser->isAiBot($type)) {
// AI机器人
$setting = Base::setting('aibotSetting');
$type = $matches[1];
$extras = [
'model_type' => match ($type) {
'qianwen' => 'qwen',
@@ -427,30 +428,67 @@ class BotReceiveMsgTask extends AbstractTask
'model_name' => $setting[$type . '_model'],
'system_message' => $setting[$type . '_system'],
'api_key' => $setting[$type . '_key'],
'base_url' => $setting[$type . '_base_url'],
'agency' => $setting[$type . '_agency'],
'server_url' => $serverUrl,
];
if ($setting[$type . '_temperature']) {
$extras['temperature'] = floatval($setting[$type . '_temperature']);
}
if ($msg->msg['model_name']) {
$extras['model_name'] = $msg->msg['model_name'];
}
if ($dialog->session_id) {
$extras['context_key'] = 'session_' . $dialog->session_id;
}
if ($type === 'wenxin') {
$extras['api_key'] .= ':' . $setting['wenxin_secret'];
}
if ($type === 'ollama') {
if (empty($extras['base_url'])) {
$errorContent = '机器人未启用。';
}
if (empty($extras['api_key'])) {
$extras['api_key'] = Base::strRandom(6);
}
}
if (empty($extras['api_key'])) {
$errorContent = '机器人未启用。';
}
if (in_array($this->client['platform'], ['win', 'mac', 'web']) && !Base::judgeClientVersion("0.41.11", $this->client['version'])) {
$errorContent = '当前客户端版本低所需版本≥v0.41.11)。';
}
if ($msg->reply_id > 0) {
$replyMsg = WebSocketDialogMsg::find($msg->reply_id);
$replyCommand = '';
$replyCommand = null;
if ($replyMsg) {
$replyCommand = $this->extractCommand($replyMsg);
if ($replyCommand) {
$replyCommand = Base::cutStr($replyCommand, 200) . "\n\n ------------------ Reference above ------------------ \n\n";
switch ($replyMsg->type) {
case 'text':
$replyCommand = $this->extractCommand($replyMsg, true);
if ($replyCommand) {
$replyCommand = Base::cutStr($replyCommand, 20000);
}
break;
case 'file':
$msgData = Base::json2array($replyMsg->getRawOriginal('msg'));
$replyCommand = TextExtractor::getFileContent(public_path($msgData['path']));
break;
}
}
$command = $replyCommand . $command;
if ($replyCommand) {
$command = <<<EOF
<quoted_content>
{$replyCommand}
</quoted_content>
The content within the above quoted_content tags is a citation.
{$command}
EOF;
}
}
$this->AIGenerateSystemMessageOrBeforeText($msg->userid, $dialog, $extras);
$this->AIGenerateSystemMessage($msg->userid, $dialog, $extras);
$webhookUrl = "{$serverUrl}/ai/chat";
} else {
// 用户机器人
@@ -532,10 +570,11 @@ class BotReceiveMsgTask extends AbstractTask
/**
* 提取消息指令(提取消息内容)
* @param WebSocketDialogMsg $msg
* @param bool $isAiBot
* @param bool $mention
* @return string
*/
private function extractCommand(WebSocketDialogMsg $msg, bool $mention = false)
private function extractCommand(WebSocketDialogMsg $msg, bool $isAiBot = false, bool $mention = false)
{
if ($msg->type !== 'text') {
return '';
@@ -549,26 +588,61 @@ class BotReceiveMsgTask extends AbstractTask
if (str_starts_with($command, '%3A.')) {
$command = ":" . substr($command, 4);
}
} else {
$command = trim(strip_tags($original));
return $command;
}
if (empty($command)) {
return '';
$aiContents = [];
if ($isAiBot) {
if (preg_match_all("/<span class=\"mention task\" data-id=\"(\d+)\">(.*?)<\/span>/", $original, $match)) {
$taskIds = Base::newIntval($match[1]);
foreach ($taskIds as $index => $taskId) {
$taskName = addslashes($match[2][$index]) . " (ID:{$taskId})";
$taskContext = "任务状态:不存在或已删除";
$taskInfo = ProjectTask::with(['content'])->whereId($taskId)->first();
if ($taskInfo) {
$taskName = addslashes($taskInfo->name) . " (ID:{$taskId})";
$taskContext = implode("\n", $taskInfo->AIContext());
}
$aiContents[] = "<task_content path=\"{$taskName}\">\n{$taskContext}\n</task_content>";
$original = str_replace($match[0][$index], "'{$taskName}' (see below for task_content tag)", $original);
}
}
if (preg_match_all("/<a class=\"mention file\" href=\"([^\"']+?)\"[^>]*?>(.*?)<\/a>/", $original, $match)) {
$filePaths = $match[1];
foreach ($filePaths as $index => $filePath) {
if (preg_match("/single\/file\/(.*?)$/", $filePath, $fileMatch)) {
$fileName = addslashes($match[2][$index]);
$fileContent = "文件状态:不存在或已删除";
$fileInfo = FileContent::idOrCodeToContent($fileMatch[1]);
if ($fileInfo && isset($fileInfo->content['url'])) {
$filePath = public_path($fileInfo->content['url']);
if (file_exists($filePath)) {
$fileName .= " (ID:{$fileInfo->id})";
$fileContent = TextExtractor::getFileContent($filePath);
}
}
$aiContents[] = "<file_content path=\"{$fileName}\">\n{$fileContent}\n</file_content>";
$original = str_replace($match[0][$index], "'{$fileName}' (see below for file_content tag)", $original);
}
}
}
}
return $command;
$command = trim(strip_tags($original));
if ($aiContents) {
$command .= "\n\n" . implode("\n\n", $aiContents);
}
return $command ?: '';
}
/**
* 生成AI系统提示词或前置消息
* 生成AI系统提示词
* @param int|null $userid
* @param WebSocketDialog $dialog
* @param array $extras
* @return void
*/
private function AIGenerateSystemMessageOrBeforeText(int|null $userid, WebSocketDialog $dialog, array &$extras)
private function AIGenerateSystemMessage(int|null $userid, WebSocketDialog $dialog, array &$extras)
{
$system_message = null;
$before_text = [];
$system_messages = [];
switch ($dialog->type) {
case "user":
$aiPrompt = WebSocketDialogConfig::where([
@@ -577,7 +651,7 @@ class BotReceiveMsgTask extends AbstractTask
'type' => 'ai_prompt',
])->value('value');
if ($aiPrompt) {
$system_message = $aiPrompt;
$extras['system_message'] = $aiPrompt;
}
break;
case "group":
@@ -585,14 +659,17 @@ class BotReceiveMsgTask extends AbstractTask
case 'user':
break;
case 'project':
$projectInfo = Project::select(['id', 'name', 'archived_at', 'deleted_at'])->whereDialogId($dialog->id)->first();
$projectInfo = Project::whereDialogId($dialog->id)->first();
if ($projectInfo) {
$before_text[] = "当前我在项目【{$projectInfo->name}】中";
if ($projectInfo->desc) {
$before_text[] = "项目描述:{$projectInfo->desc}";
}
$before_text[] = <<<EOF
如果你判断我想要添加任务,请将任务标题和描述添加到回复中,例如:
$projectDesc = $projectInfo->desc ?: "-";
$projectStatus = $projectInfo->archived_at ? '已归档' : '正在进行中';
$system_messages[] = <<<EOF
当前我在项目【{$projectInfo->name}】中
项目描述:{$projectDesc}
项目状态:{$projectStatus}
如果你判断我想要或需要添加任务,请按照以下格式回复:
::: create-task-list
title: 任务标题1
desc: 任务描述1
@@ -604,24 +681,17 @@ class BotReceiveMsgTask extends AbstractTask
}
break;
case 'task':
$taskInfo = ProjectTask::with(['content'])->select(['id', 'name', 'complete_at', 'archived_at', 'deleted_at'])->whereDialogId($dialog->id)->first();
$taskInfo = ProjectTask::with(['content'])->whereDialogId($dialog->id)->first();
if ($taskInfo) {
$before_text[] = "当前我在任务【{$taskInfo->name}】中";
if ($taskInfo->content) {
$taskDesc = $taskInfo->content?->getContentInfo();
if ($taskDesc) {
$converter = new HtmlConverter(['strip_tags' => true]);
$descContent = Base::cutStr($converter->convert($taskDesc['content']), 2000);
$before_text[] = <<<EOF
任务描述
```md
{$descContent}
```
EOF;
}
}
$before_text[] = <<<EOF
如果你判断我想要添加子任务,请将子任务标题添加到回复中,例如:
$taskContext = implode("\n", $taskInfo->AIContext());
$system_messages[] = <<<EOF
当前我在任务【{$taskInfo->name}】中
当前时间:{$taskInfo->updated_at}
任务ID{$taskInfo->id}
{$taskContext}
如果你判断我想要或需要添加子任务,请按照以下格式回复
::: create-subtask-list
title: 子任务标题1
title: 子任务标题2
@@ -629,17 +699,23 @@ class BotReceiveMsgTask extends AbstractTask
EOF;
}
break;
case 'department':
$userDepartment = UserDepartment::whereDialogId($dialog->id)->first();
if ($userDepartment) {
$system_messages[] = "当前我在【{$userDepartment->name}】的部门群聊中";
}
break;
case 'all':
$before_text[] = "当前我团队【全体成员】的群聊中";
$system_messages[] = "当前我【全体成员】的群聊中";
break;
}
break;
}
if ($system_message) {
$extras['system_message'] = $system_message;
if ($extras['system_message']) {
array_unshift($system_messages, $extras['system_message']);
}
if ($before_text) {
$extras['before_text'] = Base::newTrim($before_text);
if ($system_messages) {
$extras['system_message'] = implode("\n\n====\n\n", Base::newTrim($system_messages));
}
}
}

View File

@@ -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') {

View 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()
{
}
}

View File

@@ -1,7 +1,8 @@
<?php
namespace App\Tasks;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Models\Setting;
use App\Models\User;
use App\Models\WebSocketDialogMsg;

View File

@@ -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,

View File

@@ -142,6 +142,7 @@ install() {
if /root/.acme.sh/acme.sh --installcert -d "${domain}" --fullchainpath "${sslPath}/${domain}.crt" --keypath "${sslPath}/${domain}.key" --ecc --force; then
success "SSL 证书配置成功"
sleep 2
cp -r /root/.acme.sh/${domain}_ecc/*.conf ${sslPath}/
fi
else
error "SSL 证书生成失败"
@@ -165,5 +166,54 @@ error_page 497 https://\$host\$request_uri;
EOF
}
check
install
UPDATE_LOG="$(dirname "$PWD")/docker/nginx/site/ssl/update.log"
SSL_PATH="$(dirname "$PWD")/docker/nginx/site/ssl"
upgrade_cert(){
curl https://get.acme.sh | sh
if [[ 0 -ne $? ]]; then
echo "安装证书更新脚本失败"
echo $(date)": 安装证书更新脚本失败" >> ${UPDATE_LOG}
exit 1
fi
file=$1
domain=$(basename "$file" .key)
old_crt_md5=$(md5sum ${SSL_PATH}/${domain}.crt| awk '{print $1}')
/root/.acme.sh/acme.sh --renew --standalone -d ${domain} --fullchainpath "${SSL_PATH}/${domain}.crt" --keypath "${SSL_PATH}/${domain}.key" --ecc --force
new_crt_md5=$(md5sum ${SSL_PATH}/${domain}.crt| awk '{print $1}')
if [ "${old_key_md5}" == "${new_key_md5}" ]; then
echo "${domain} 证书更新脚本失败"
echo $(date)": ${domain} 证书更新失败" >> ${UPDATE_LOG}
echo $(date)": ${old_crt_md5} == ${new_crt_md5}" >> ${UPDATE_LOG}
else
echo "${domain} 证书更新脚本成功"
echo $(date)": ${domain} 证书更新成功" >> ${UPDATE_LOG}
fi
}
check_expire(){
apk add --no-cache openssl socat
find ${SSL_PATH} -type f -name "*.key" | while read -r file; do
CERT_PATH=$file
expiry_date=$(openssl x509 -enddate -noout -in "$CERT_PATH" | cut -d= -f2)
expiry_timestamp=$(date -d "$expiry_date" +%s)
current_timestamp=$(date +%s)
days_remaining=$(( (expiry_timestamp - current_timestamp) / 86400 ))
echo "剩余时间${days_remaining}天" >> ${UPDATE_LOG}
if [ "$days_remaining" -lt 30 ]; then
upgrade_cert $file
fi
done
}
case "${1}" in
"install")
check
install
;;
"renew")
check_expire
;;
*)
echo "test"
;;
esac

83
cmd
View File

@@ -156,9 +156,9 @@ run_electron() {
npm install
fi
if [ ! -d "./electron/node_modules" ]; then
pushd electron
pushd electron || exit
npm install
popd
popd || exit
fi
#
if [ -d "./electron/dist" ]; then
@@ -178,8 +178,9 @@ run_electron() {
run_exec() {
local container=$1
local cmd=$2
local name=`docker_name $container`
shift 1
local cmd=$@
local name=$(docker_name "$container")
if [ -z "$name" ]; then
error "没有找到 $container 容器!"
exit 1
@@ -322,15 +323,26 @@ https_auto() {
if [[ "$restart_nginx" == "y" ]]; then
$COMPOSE up -d
fi
docker run -it --rm -v $(pwd):/work nginx:alpine sh "/work/bin/https"
docker run -it --rm -v $(pwd):/work nginx:alpine sh /work/bin/https install
if [[ 0 -eq $? ]]; then
run_exec nginx "nginx -s reload"
fi
new_job="* 6 * * * docker run -it --rm -v $(pwd):/work nginx:alpine sh /work/bin/https renew"
current_crontab=$(crontab -l 2>/dev/null)
if ! echo "$current_crontab" | grep -v "https renew"; then
echo "任务已存在,无需添加。"
else
crontab -l |{
cat
echo "$new_job"
} | crontab -
echo "任务已添加。"
fi
}
env_get() {
local key=$1
local value=`cat ${cur_path}/.env | grep "^$key=" | awk -F '=' '{print $2}'`
local value=`cat ${cur_path}/.env | grep "^$key=" | awk -F '=' '{print $2}' | tr -d '\r\n'`
echo "$value"
}
@@ -405,10 +417,49 @@ if [ $# -gt 0 ]; then
rm -rf vendor
rm -rf composer.lock
fi
mkdir -p "${cur_path}/docker/log/supervisor"
mkdir -p "${cur_path}/docker/mysql/data"
chmod -R 775 "${cur_path}/docker/log/supervisor"
chmod -R 775 "${cur_path}/docker/mysql/data"
# 目录权限
volumes=(
"docker/log/supervisor"
"docker/mysql/data"
"docker/office/logs"
"docker/office/data"
"docker/es/data"
)
cmda=""
cmdb=""
for vol in "${volumes[@]}"; do
tmp_path="${cur_path}/${vol}"
mkdir -p "${tmp_path}"
chmod -R 775 "${tmp_path}"
rm -f "${tmp_path}/dootask.lock"
cmda="${cmda} -v ${tmp_path}:/usr/share/${vol}"
cmdb="${cmdb} touch /usr/share/${vol}/dootask.lock &&"
done
# 目录权限检测
remaining=10
while true; do
((remaining=$remaining-1))
writable="yes"
docker run --rm ${cmda} nginx:alpine sh -c "${cmdb} touch /usr/share/docker/dootask.lock" &> /dev/null
for vol in "${volumes[@]}"; do
if [ ! -f "${vol}/dootask.lock" ]; then
if [ $remaining -lt 0 ]; then
error "目录【${vol}】权限不足!"
exit 1
else
writable="no"
break
fi
fi
done
if [ "$writable" == "yes" ]; then
break
else
sleep 3
fi
done
# 设置ES索引后缀
env_set ES_INDEX_SUFFIX "$(rand_string 6)"
# 启动容器
[[ "$(arg_get port)" -gt 0 ]] && env_set APP_PORT "$(arg_get port)"
$COMPOSE up php -d
@@ -426,17 +477,17 @@ if [ $# -gt 0 ]; then
[[ -z "$(env_get APP_KEY)" ]] && run_exec php "php artisan key:generate"
switch_debug "false"
# 检查数据库
remaining=20
remaining=10
while [ ! -f "${cur_path}/docker/mysql/data/$(env_get DB_DATABASE)/db.opt" ]; do
((remaining=$remaining-1))
if [ $remaining -lt 0 ]; then
error "数据库初始化失败!"
exit 1
fi
chmod -R 775 "${cur_path}/docker/mysql/data"
sleep 3
done
# 数据库迁移
remaining=20
remaining=10
while [ ! -f "${cur_path}/docker/mysql/data/$(env_get DB_DATABASE)/$(env_get DB_PREFIX)migrations.ibd" ]; do
((remaining=$remaining-1))
if [ $remaining -lt 0 ]; then
@@ -447,12 +498,10 @@ if [ $# -gt 0 ]; then
run_exec php "php artisan migrate --seed"
done
# 设置初始化密码
res=`run_exec mariadb "sh /etc/mysql/repassword.sh"`
$COMPOSE up -d
restart_php
success "安装完成"
info "地址: http://${GreenBG}127.0.0.1:$(env_get APP_PORT)${Font}"
info "$res"
run_exec mariadb "sh /etc/mysql/repassword.sh"
elif [[ "$1" == "update" ]]; then
shift 1
if [[ "$@" != "nobackup" ]]; then
@@ -513,7 +562,7 @@ if [ $# -gt 0 ]; then
success "修改成功"
elif [[ "$1" == "repassword" ]]; then
shift 1
run_exec mariadb "sh /etc/mysql/repassword.sh \"$@\""
run_exec mariadb "sh /etc/mysql/repassword.sh $@"
elif [[ "$1" == "serve" ]] || [[ "$1" == "dev" ]] || [[ "$1" == "development" ]]; then
shift 1
run_compile dev

View File

@@ -10,6 +10,8 @@
"require": {
"php": "^8.0",
"ext-curl": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-gd": "*",
"ext-imagick": "*",
"ext-json": "*",
@@ -18,6 +20,7 @@
"ext-simplexml": "*",
"ext-zip": "*",
"directorytree/ldaprecord-laravel": "^2.7",
"elasticsearch/elasticsearch": "^8.17",
"fideloper/proxy": "^4.4.1",
"firebase/php-jwt": "^6.9",
"fruitcake/laravel-cors": "^2.0.4",
@@ -34,7 +37,9 @@
"mews/captcha": "^3.2.6",
"orangehill/iseed": "^3.0.1",
"overtrue/pinyin": "^4.0",
"phpoffice/phpword": "^1.3",
"predis/predis": "^1.1.7",
"smalot/pdfparser": "^2.11",
"symfony/mailer": "^6.0"
},
"require-dev": {
@@ -83,7 +88,10 @@
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true
"sort-packages": true,
"allow-plugins": {
"php-http/discovery": true
}
},
"minimum-stability": "dev",
"prefer-stable": true,

1277
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,54 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
class AddFulltextIndexToWebSocketDialogMsgsTable extends Migration
{
public function up()
{
$tableName = 'web_socket_dialog_msgs';
$column = 'key'; // 需要添加 FULLTEXT 索引的字段
// 检查 FULLTEXT 索引是否已经存在
if (!$this->fullTextIndexExists($tableName, $column)) {
Schema::table($tableName, function (Blueprint $table) use ($column) {
$table->fullText($column);
});
}
}
public function down()
{
// 删除 FULLTEXT 索引
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
$table->dropFullText(['key']);
});
}
private function fullTextIndexExists($tableName, $column)
{
// 获取当前数据库名称
$databaseName = env('DB_DATABASE');
// 查询 information_schema.statistics 表
$prefix = DB::getTablePrefix();
$indexExists = DB::table(DB::raw('information_schema.statistics'))
->where('table_schema', $databaseName)
->where('table_name', $prefix . $tableName)
->where('index_type', 'FULLTEXT')
->get();
// 检查返回的索引是否包含指定的列
foreach ($indexExists as $index) {
$indexColumns = explode(',', $index->COLUMN_NAME ?? $index->column_name ?? '');
// 如果索引包含指定的列,则返回 true
if (in_array($column, $indexColumns)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,50 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddBotToWebSocketDialogMsgs extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
if (!Schema::hasColumn('web_socket_dialog_msgs', 'bot')) {
$table->tinyInteger('bot')->nullable()->default(0)->after('modify')->comment('是否机器人的消息');
$table->index('bot');
}
});
// 获取表前缀
$prefix = DB::getTablePrefix();
// 使用原生SQL更新数据
/** @noinspection SqlNoDataSourceInspection */
DB::statement("
UPDATE {$prefix}web_socket_dialog_msgs m
INNER JOIN {$prefix}users u ON u.userid = m.userid
SET m.bot = 1
WHERE u.bot = 1
");
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
if (Schema::hasColumn('web_socket_dialog_msgs', 'bot')) {
$table->dropIndex('bot');
$table->dropColumn('bot');
}
});
}
}

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddIsNotifiedToUmengAlias extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('umeng_alias', function (Blueprint $table) {
if (!Schema::hasColumn('umeng_alias', 'is_notified')) {
$table->tinyInteger('is_notified')->nullable()->default(0)->after('ua')->comment('通知权限');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('umeng_alias', function (Blueprint $table) {
if (Schema::hasColumn('umeng_alias', 'is_notified')) {
$table->dropColumn('is_notified');
}
});
}
}

View File

@@ -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');
}
});
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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');
}
});
}
}

View File

@@ -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');
});
}
}

View File

@@ -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');
});
}
}

View File

@@ -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');
}
}

View File

@@ -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();
}
}

View File

@@ -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()
{
//
}
}

View File

@@ -2,7 +2,7 @@ services:
php:
container_name: "dootask-php-${APP_ID}"
image: "kuaifan/php:swoole-8.0.rc18"
shm_size: "2gb"
shm_size: 2G
ulimits:
core:
soft: 0
@@ -25,8 +25,9 @@ services:
extnetwork:
ipv4_address: "${APP_IPPR}.2"
depends_on:
- redis
- mariadb
- redis
- es
restart: unless-stopped
nginx:
@@ -84,7 +85,9 @@ services:
volumes:
- ./docker/office/logs:/var/log/onlyoffice
- ./docker/office/data:/var/www/onlyoffice/Data
- ./docker/office/etc/documentserver/default.json:/etc/onlyoffice/documentserver/default.json
- ./docker/office/resources/require.js:/var/www/onlyoffice/documentserver/web-apps/vendor/requirejs/require.js
- ./docker/office/resources/common/main/resources/img/header:/var/www/onlyoffice/documentserver/web-apps/apps/common/main/resources/img/header
- ./docker/office/resources/documenteditor/main/resources/css/app.css:/var/www/onlyoffice/documentserver/web-apps/apps/documenteditor/main/resources/css/app.css
- ./docker/office/resources/documenteditor/mobile/css/526.caf35c11a8d72ca5ac85.css:/var/www/onlyoffice/documentserver/web-apps/apps/documenteditor/mobile/css/526.caf35c11a8d72ca5ac85.css
- ./docker/office/resources/presentationeditor/main/resources/css/app.css:/var/www/onlyoffice/documentserver/web-apps/apps/presentationeditor/main/resources/css/app.css
@@ -167,10 +170,11 @@ services:
ai:
container_name: "dootask-ai-${APP_ID}"
image: "kuaifan/dootask-ai:0.1.4"
image: "kuaifan/dootask-ai:0.3.4"
environment:
REDIS_HOST: "${REDIS_HOST}"
REDIS_PORT: "${REDIS_PORT}"
TIMEOUT: 600
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.12"
@@ -221,6 +225,21 @@ services:
ipv4_address: "${APP_IPPR}.14"
restart: unless-stopped
es:
container_name: "dootask-es-${APP_ID}"
image: "elasticsearch:8.17.2"
volumes:
- ./docker/es/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
- ./docker/es/data:/usr/share/elasticsearch/data
environment:
discovery.type: single-node
xpack.security.enabled: false
ES_JAVA_OPTS: "-Xms1g -Xmx1g"
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.15"
restart: unless-stopped
networks:
extnetwork:
name: "dootask-networks-${APP_ID}"

View File

@@ -0,0 +1,2 @@
cluster.name: "docker-cluster"
network.host: 0.0.0.0

2
docker/es/data/.gitignore vendored Executable file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

View File

@@ -1 +1,2 @@
*/
*
!.gitignore

View File

@@ -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}"

View File

@@ -1 +1,2 @@
*
!.gitignore

View File

@@ -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;

View 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/;
}

View 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;
}

View 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/;
}

View 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;
}

View 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/;
}

View 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/;
}

View File

@@ -0,0 +1,3 @@
location /apps/okr/ {
proxy_pass http://okr:5566/apps/okr/;
}

View File

@@ -1 +1,2 @@
*
!.gitignore

View 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"
}
}
]
}
}
}

View File

@@ -1 +1,2 @@
*/
*
!.gitignore

View File

@@ -0,0 +1 @@
*.gz

View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

@@ -10,3 +10,4 @@ cache/*
.devload
.native
.build

159
electron/build.js vendored
View File

@@ -492,12 +492,9 @@ async function startBuild(data) {
console.log("版本:", config.version + ` (${config.codeVerson})`);
console.log("系统:", platform.replace('build-', '').toUpperCase());
console.log("架构:", archs.map(arch => arch.toUpperCase()).join(', '));
console.log("发布:", publish ? '是' : '否');
if (publish) {
console.log("升级提示:", release ? '是' : '否');
if (platform === 'build-mac') {
console.log("公证:", notarize ? '是' : '否');
}
console.log("发布:", publish ? `是(${release ? '提示升级' : '静默升级'}` : '否');
if (platform === 'build-mac') {
console.log("公证:", notarize ? '是' : '否');
}
console.log("===============\n");
// drawio
@@ -512,6 +509,7 @@ async function startBuild(data) {
fse.copySync(path.resolve(__dirname, "../public/language"), path.resolve(electronDir, "language"))
// config.js
fs.writeFileSync(electronDir + "/config.js", "window.systemInfo = " + JSON.stringify(systemInfo), 'utf8');
fs.writeFileSync(electronDir + "/dark", '', 'utf8');
fs.writeFileSync(nativeCachePath, utils.formatUrl(data.url));
fs.writeFileSync(devloadCachePath, "", 'utf8');
// index.html
@@ -697,6 +695,20 @@ if (["dev"].includes(argv[2])) {
});
} else {
// 手编译(默认)
let cachedConfig = {};
try {
const buildConfigPath = path.join(__dirname, '.build');
if (fs.existsSync(buildConfigPath)) {
const configContent = fs.readFileSync(buildConfigPath, 'utf-8');
if (configContent.trim()) {
cachedConfig = JSON.parse(configContent);
}
}
} catch (error) {
console.warn('读取缓存配置失败:', error.message);
}
const questions = [
{
type: 'checkbox',
@@ -706,14 +718,14 @@ if (["dev"].includes(argv[2])) {
{
name: "MacOS",
value: platforms[0],
checked: true
},
{
name: "Windows",
value: platforms[1]
}
],
validate: function(answer) {
default: (cachedConfig && cachedConfig.platform) || [],
validate: (answer) => {
if (answer.length < 1) {
return '请至少选择一个系统';
}
@@ -724,18 +736,27 @@ if (["dev"].includes(argv[2])) {
type: 'checkbox',
name: 'arch',
message: "选择系统架构",
choices: [
{
name: "arm64",
value: architectures[0],
checked: true
},
{
name: "x64",
value: architectures[1]
choices: ({ platform }) => {
const array = [
{
name: "arm64",
value: architectures[0],
},
{
name: "x64",
value: architectures[1]
}
]
if (platform.find(item => item === 'build-mac')) {
array.push({
name: "通用" + (platform.length > 1 ? " (仅MacOS)" : ""),
value: 'universal'
})
}
],
validate: function(answer) {
return array;
},
default: (cachedConfig && cachedConfig.arch) || [],
validate: (answer) => {
if (answer.length < 1) {
return '请至少选择一个架构';
}
@@ -745,82 +766,94 @@ if (["dev"].includes(argv[2])) {
{
type: 'list',
name: 'publish',
message: "选择是否发布",
message: "选择是否发布",
choices: [{
name: "否",
value: false
}, {
name: "是",
value: true
}]
}
];
// 根据publish选项动态添加后续问题
const publishQuestions = [
}],
default: (cachedConfig && cachedConfig.publish !== undefined) ?
(cachedConfig.publish ? 1 : 0) : 0
},
{
type: 'list',
name: 'release',
message: "选择是否弹出升级提示框",
message: "选择升级方式",
when: ({ publish }) => publish,
choices: [{
name: "",
name: "弹出提示",
value: true
}, {
name: "",
name: "静默",
value: false
}]
}],
default: (cachedConfig && cachedConfig.release !== undefined) ?
(cachedConfig.release ? 0 : 1) : 0
},
{
type: 'list',
name: 'notarize',
message: "选择是否需要公证MacOS应用",
when: (answers) => answers.platform === 'build-mac', // 只在MacOS时显示
message: ({ platform }) => platform.length > 1 ? "选择是否公证(仅MacOS" : "选择是否公证",
when: ({ platform }) => platform.find(item => item === 'build-mac'),
choices: [{
name: "否",
value: false
}, {
name: "是",
value: true
}]
}],
default: (cachedConfig && cachedConfig.notarize !== undefined) ?
(cachedConfig.notarize ? 1 : 0) : 0
}
];
// 先询问基本问题
inquirer.prompt(questions).then(async answers => {
// 如果选择发布,继续询问发布相关问题
if (answers.publish) {
const publishAnswers = await inquirer.prompt(publishQuestions);
Object.assign(answers, publishAnswers);
// 开始提问
const prompt = inquirer.createPromptModule();
prompt(questions)
.then(async answers => {
answers = Object.assign({
release: false,
notarize: false
}, answers);
if (!PUBLISH_KEY && (!GITHUB_TOKEN || !utils.strExists(GITHUB_REPOSITORY, "/"))) {
console.error("发布需要 PUBLISH_KEY 或 GitHub Token 和 Repository, 请检查环境变量!");
process.exit()
// 缓存当前配置
try {
fs.writeFileSync(path.join(__dirname, '.build'), JSON.stringify(answers, null, 4), 'utf-8');
} catch (error) {
console.warn('保存配置缓存失败:', error.message);
}
if (answers.notarize === true) {
if (!APPLEID || !APPLEIDPASS) {
console.error("公证MacOS应用需要 Apple ID 和 Apple ID 密码, 请检查环境变量!");
// 发布判断环境变量
if (answers.publish) {
if (!PUBLISH_KEY && (!GITHUB_TOKEN || !utils.strExists(GITHUB_REPOSITORY, "/"))) {
console.error("发布需要 PUBLISH_KEY 或 GitHub Token 和 Repository, 请检查环境变量!");
process.exit()
}
}
} else {
// 如果不发布,设置默认值
answers.release = false;
answers.notarize = false;
}
// 开始构建
for (const platform of answers.platform) {
for (const data of config.app) {
data.configure = {
platform,
archs: answers.arch,
publish: answers.publish,
release: answers.release,
notarize: answers.notarize
};
await startBuild(data);
// 公证判断环境变量
if (answers.notarize === true) {
if (!APPLEID || !APPLEIDPASS) {
console.error("公证需要 Apple ID 和 Apple ID 密码, 请检查环境变量!");
process.exit()
}
}
}
});
// 开始构建
for (const platform of answers.platform) {
for (const data of config.app) {
data.configure = {
platform,
archs: answers.arch,
publish: answers.publish,
release: answers.release,
notarize: answers.notarize
};
await startBuild(data);
}
}
})
.catch(_ => { });
}

153
electron/electron.js vendored
View File

@@ -54,6 +54,8 @@ let screenshotObj = null,
let childWindow = [],
preloadWindow = null,
mediaWindow = null,
mediaType = null,
webTabWindow = null,
webTabView = [],
webTabHeight = 38;
@@ -161,7 +163,14 @@ function createMainWindow() {
if (!willQuitApp) {
utils.onBeforeUnload(event, mainWindow).then(() => {
if (['darwin', 'win32'].includes(process.platform)) {
mainWindow.hide();
if (mainWindow.isFullScreen()) {
mainWindow.once('leave-full-screen', () => {
mainWindow.hide();
})
mainWindow.setFullScreen(false)
} else {
mainWindow.hide();
}
} else {
app.quit();
}
@@ -432,7 +441,7 @@ function createChildWindow(args) {
if (/^https?:/i.test(hash)) {
browser.loadURL(hash).then(_ => { }).catch(_ => { })
} else if (isPreload) {
browser.webContents.executeJavaScript(`if(typeof $A.goForward === 'function'){$A.goForward('${hash}')}else{throw new Error('no function')}`, true).catch(() => {
browser.webContents.executeJavaScript(`if(typeof window.__initializeApp === 'function'){window.__initializeApp('${hash}')}else{throw new Error('no function')}`, true).catch(() => {
utils.loadUrlOrFile(browser, devloadUrl, hash)
});
} else {
@@ -475,36 +484,70 @@ function updateChildWindow(browser, args) {
* @param type
*/
function createMediaWindow(args, type = 'image') {
const imageWindow = new BrowserWindow({
width: args.width || 970,
height: args.height || 700,
minWidth: 360,
minHeight: 360,
autoHideMenuBar: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
webSecurity: false,
plugins: true
},
show: false
});
if (mediaWindow === null) {
mediaWindow = new BrowserWindow({
width: args.width || 970,
height: args.height || 700,
minWidth: 360,
minHeight: 360,
autoHideMenuBar: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
webSecurity: false,
plugins: true
},
show: false
});
// 监听关闭事件
mediaWindow.addListener('close', event => {
if (!willQuitApp) {
event.preventDefault()
if (mediaWindow.isFullScreen()) {
mediaWindow.once('leave-full-screen', () => {
mediaWindow.hide();
})
mediaWindow.setFullScreen(false)
} else {
mediaWindow.webContents.send('on-close');
mediaWindow.hide();
}
}
})
// 监听关闭事件
mediaWindow.addListener('closed', () => {
mediaWindow = null;
mediaType = null;
})
// 设置右键菜单
electronMenu.webContentsMenu(mediaWindow.webContents)
} else {
// 直接显示
mediaWindow.show();
}
// 加载图片浏览器的HTML
let filePath = './render/viewer/index.html';
if (type === 'video') {
filePath = './render/video/index.html';
if (mediaType === type) {
// 更新窗口
mediaWindow.webContents.send('load-media', args);
} else {
// 重置窗口
mediaType = type;
let filePath = './render/viewer/index.html';
if (type === 'video') {
filePath = './render/video/index.html';
}
mediaWindow.loadFile(filePath, {}).then(_ => { }).catch(_ => { })
}
imageWindow.loadFile(filePath, {}).then(_ => { }).catch(_ => { })
// 设置右键菜单
electronMenu.webContentsMenu(imageWindow.webContents)
// 窗口准备好后显示
imageWindow.on('ready-to-show', () => {
imageWindow.show();
// 发送图片数据到渲染进程
imageWindow.webContents.send('load-media', args);
// 窗口准备好后事件
mediaWindow.removeAllListeners("ready-to-show");
mediaWindow.addListener('ready-to-show', () => {
mediaWindow.show();
mediaWindow.webContents.send('load-media', args);
});
}
@@ -527,8 +570,6 @@ function createWebTabWindow(args) {
// 创建父级窗口
if (!webTabWindow) {
let config = Object.assign(args.config || {}, userConf.get('webTabWindow', {}));
let webPreferences = args.webPreferences || {};
const titleBarOverlay = {
height: webTabHeight
}
@@ -549,14 +590,14 @@ function createWebTabWindow(args) {
titleBarStyle: 'hidden',
titleBarOverlay,
backgroundColor: nativeTheme.shouldUseDarkColors ? '#3B3B3D' : '#EFF0F4',
webPreferences: Object.assign({
webPreferences: {
preload: path.join(__dirname, 'electron-preload.js'),
webSecurity: true,
nodeIntegration: true,
contextIsolation: true,
nativeWindowOpen: true
}, webPreferences),
}, config))
},
}, userConf.get('webTabWindow', {})))
webTabWindow.on('resize', () => {
resizeWebTab(0)
@@ -621,15 +662,20 @@ function createWebTabWindow(args) {
webTabWindow.show();
// 创建 tab 子窗口
const browserView = new BrowserView({
const viewOptions = Object.assign({
useHTMLTitleAndIcon: true,
useLoadingView: true,
useErrorView: true,
webPreferences: {
preload: path.join(__dirname, 'electron-preload.js'),
}
})
if (nativeTheme.shouldUseDarkColors) {
}, args.config || {})
viewOptions.webPreferences = Object.assign({
preload: path.join(__dirname, 'electron-preload.js'),
nodeIntegration: true,
contextIsolation: true
}, args.webPreferences || {})
const browserView = new BrowserView(viewOptions)
if (args.backgroundColor) {
browserView.setBackgroundColor(args.backgroundColor)
} else if (nativeTheme.shouldUseDarkColors) {
browserView.setBackgroundColor('#575757')
} else {
browserView.setBackgroundColor('#FFFFFF')
@@ -840,6 +886,7 @@ function monitorThemeChanges() {
const backgroundColor = utils.getDefaultBackgroundColor()
mainWindow?.setBackgroundColor(backgroundColor);
preloadWindow?.setBackgroundColor(backgroundColor);
mediaWindow?.setBackgroundColor(backgroundColor);
childWindow.some(({browser}) => browser.setBackgroundColor(backgroundColor))
webTabWindow?.setBackgroundColor(nativeTheme.shouldUseDarkColors ? '#3B3B3D' : '#EFF0F4')
// 通知所有窗口
@@ -1110,6 +1157,7 @@ ipcMain.on('childWindowCloseAll', (event) => {
browser && browser.close()
})
preloadWindow?.close()
mediaWindow?.close()
event.returnValue = "ok"
})
@@ -1121,6 +1169,7 @@ ipcMain.on('childWindowDestroyAll', (event) => {
browser && browser.destroy()
})
preloadWindow?.destroy()
mediaWindow?.destroy()
event.returnValue = "ok"
})
@@ -1362,15 +1411,17 @@ ipcMain.handle('getStore', (event, args) => {
//================================================================
let autoUpdating = 0
autoUpdater.logger = loger
autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = true
autoUpdater.on('update-available', info => {
mainWindow.webContents.send("updateAvailable", info)
})
autoUpdater.on('update-downloaded', info => {
mainWindow.webContents.send("updateDownloaded", info)
})
if (autoUpdater) {
autoUpdater.logger = loger
autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = true
autoUpdater.on('update-available', info => {
mainWindow.webContents.send("updateAvailable", info)
})
autoUpdater.on('update-downloaded', info => {
mainWindow.webContents.send("updateDownloaded", info)
})
}
/**
* 检查更新
@@ -1380,6 +1431,9 @@ ipcMain.on('updateCheckAndDownload', (event, args) => {
if (autoUpdating + 3600 > utils.dayjs().unix()) {
return // 限制1小时仅执行一次
}
if (!autoUpdater) {
return
}
if (args.provider) {
autoUpdater.setFeedURL(args)
}
@@ -1437,6 +1491,7 @@ ipcMain.on('updateQuitAndInstall', (event, args) => {
browser && browser.destroy()
})
preloadWindow?.destroy()
mediaWindow?.destroy()
// 启动更新子窗口
createUpdaterWindow(args.updateTitle)
@@ -1444,7 +1499,7 @@ ipcMain.on('updateQuitAndInstall', (event, args) => {
// 退出并安装更新
setTimeout(_ => {
mainWindow.hide()
autoUpdater.quitAndInstall(true, true)
autoUpdater?.quitAndInstall(true, true)
}, 600)
})

View File

@@ -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-->

View File

@@ -26,17 +26,17 @@
"url": "https://github.com/kuaifan/dootask.git"
},
"devDependencies": {
"@electron-forge/cli": "^7.6.0",
"@electron-forge/maker-deb": "^7.6.0",
"@electron-forge/maker-rpm": "^7.6.0",
"@electron-forge/maker-squirrel": "^7.6.0",
"@electron-forge/maker-zip": "^7.6.0",
"@electron-forge/cli": "^7.7.0",
"@electron-forge/maker-deb": "^7.7.0",
"@electron-forge/maker-rpm": "^7.7.0",
"@electron-forge/maker-squirrel": "^7.7.0",
"@electron-forge/maker-zip": "^7.7.0",
"dotenv": "^16.4.5",
"electron": "^33.2.1",
"electron": "^34.3.0",
"electron-builder": "^25.1.8",
"electron-notarize": "^1.2.2",
"form-data": "^4.0.1",
"inquirer": "^8.2.0",
"inquirer": "^12.4.2",
"ora": "^4.1.1"
},
"dependencies": {
@@ -108,7 +108,14 @@
"target": "dmg",
"arch": [
"x64",
"arm64"
"arm64",
"universal"
]
},
{
"target": "pkg",
"arch": [
"universal"
]
}
]

View File

@@ -235,7 +235,7 @@
disableCaptions: 'Désactiver les sous-titres',
enterFullscreen: 'Plein écran',
exitFullscreen: 'Quitter le plein écran',
frameTitle: 'Lecteur vidéo',
frameTitle: 'Lecteur Vidéo',
captions: 'Sous-titres',
settings: 'Paramètres',
menuBack: 'Retour au menu précédent',
@@ -310,46 +310,79 @@
ru: 'Видеоплеер'
};
// 获取视频MIME类型
const getVideoMimeType = (filename) => {
const ext = filename.split('.').pop().toLowerCase();
const mimeTypes = {
'mp4': 'video/mp4',
'webm': 'video/webm',
'ogg': 'video/ogg',
'mov': 'video/quicktime',
'm4v': 'video/mp4',
'avi': 'video/x-msvideo',
'wmv': 'video/x-ms-wmv',
'flv': 'video/x-flv',
'mkv': 'video/x-matroska',
'3gp': 'video/3gpp'
};
return mimeTypes[ext] || 'video/mp4';
};
let player = null;
// 接收主进程传来的关闭事件
ipcRenderer.on('on-close', () => {
if (player) {
player.stop()
}
});
// 接收主进程传来的视频路径
ipcRenderer.on('load-media', (event, args) => {
const videoElement = document.querySelector('video');
videoElement.src = args.video;
// 销毁旧的播放器实例
if (player) {
player.destroy();
// 检查视频源是否有效
if (!args || !args.video) {
return;
}
// 设置语言
const currentLang = args.lang || 'en';
// 创建新的播放器实例
player = new Plyr('#player', {
controls: [
'play-large', // 大播放按钮
'play', // 播放/暂停
'progress', // 进度条
'current-time', // 当前时间
'duration', // 总时长
'mute', // 静音
'volume', // 音
'settings', // 设置
'fullscreen' // 全屏
],
settings: ['captions', 'quality', 'speed', 'loop'], // 设置菜单选项
speed: {selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 2]}, // 播放速度选项
keyboard: {focused: true, global: true}, // 启用键盘快捷键
tooltips: {controls: true, seek: true}, // 显示工具提示
hideControls: true, // 自动隐藏控制栏
i18n: translations[currentLang] || translations['en'] // 设置语言
});
// 如果播放器不存在,创建新的实例
if (!player) {
player = new Plyr('#player', {
controls: [
'play-large', // 播放按钮
'play', // 播放/暂停
'progress', // 进度条
'current-time', // 当前时间
'duration', // 总时长
'mute', //
'volume', // 音量
'settings', // 设置
'fullscreen' // 全屏
],
settings: ['captions', 'quality', 'speed', 'loop'], // 设置菜单选项
speed: {selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 2]}, // 播放速度选项
keyboard: {focused: true, global: true}, // 启用键盘快捷键
tooltips: {controls: true, seek: true}, // 显示工具提示
hideControls: true, // 自动隐藏控制栏
i18n: translations[currentLang] || translations['en'] // 设置语言
});
// 错误处理
player.on('error', (event) => {
console.error('Player error:', event);
});
// 错误处理
player.on('error', (event) => {
console.error('Player error:', event);
});
}
// 直接更新播放器源
player.source = {
type: 'video',
sources: [{
src: args.video,
type: getVideoMimeType(args.video)
}]
};
// 更新网页标题
document.title = args.title || titleTranslations[currentLang] || titleTranslations['en'];

View File

@@ -35,16 +35,18 @@
<script src="./viewer.min.js"></script>
<script>
const {ipcRenderer} = require('electron');
let viewer = null;
const thumbnailUrl = (url) => {
url = `${url}`
.replace(/_thumb\.(png|jpg|jpeg)$/, '')
.replace(/\/crop\/([^\/]+)$/, '')
if (!/^https?:\/\/[^\/]+\/uploads\//.test(url)) {
return url
}
const crops = {
ratio: 3,
percentage: '320x0'
}
url = `${url}`
.replace(/_thumb\.(png|jpg|jpeg)$/, '')
.replace(/\/crop\/([^\/]+)$/, '')
return url + "/crop/" + Object.keys(crops).map(key => {
return `${key}:${crops[key]}`
}).join(",")
@@ -63,8 +65,22 @@
ru: 'Просмотр изображений'
};
let viewer = null;
// 接收主进程传来的关闭事件
ipcRenderer.on('on-close', () => {
if (viewer) {
viewer.destroy()
}
});
// 接收主进程发送的图片数据
ipcRenderer.on('load-media', (event, args) => {
// 检查数据源是否有效
if (!args || !args.images) {
return;
}
const container = document.getElementById('image-container');
container.innerHTML = ''; // 清空容器

View File

@@ -171,9 +171,9 @@ LDAP 用户禁止修改邮箱
部门不存在或已被删除
最多只能创建(*)个部门
上级部门不存在或已被删除
上级部门层级错误
部门层级最多只能创建(*)级
每个部门最多只能创建(*)个子部门
含有子部门无法修改上级部门
含有子部门无法删除
请选择正确的部门负责人
新建成功
此功能未开启,请联系管理员开启
@@ -801,4 +801,25 @@ webhook地址最长仅支持255个字符。
新增子任务标签
更新子任务标签
AI机器人不存在
内容不存在
长文本
选择模型
当前对话不支持
会话不存在或已被删除
开启新会话
历史会话
未找到默认模型
思考中...
请先填写 Base URL
获取失败
任务超期未完成
每个用户最多只能负责(*)个部门
不能选择自己的子部门作为上级部门
转文字失败

View File

@@ -1025,7 +1025,6 @@ Pro版
隐藏共享文件
单聊
显示文件
ID、任务名...
仅显示我的
语音
群头像
@@ -1221,7 +1220,6 @@ OKR 结果分析
AI 机器人
任务相关
请填写名称!
访问OpenAI网站查看
使用代理
支持 http 或 socks 代理
例如http://proxy.com 或 socks5://proxy.com
@@ -1705,7 +1703,6 @@ WiFi签到延迟时长为±1分钟。
选择群组
输入关键词搜索群
仅支持选择个人群转为部门群
含有子部门无法修改上级部门
删除部门
你确定要删除【(*)】部门吗?
注意:此操作不可恢复,部门下的成员将移至默认部门。
@@ -1903,3 +1900,85 @@ WiFi签到延迟时长为±1分钟。
请选择示例标签
全部保存成功
消息详情
长文本
你确定要创建任务吗?
你确定要创建子任务吗?
正在处理,请稍后再试...
开启麦克风失败!
开启摄像头失败!
群机器人
群成员 ((*)人)
此消息已经过期
DeepSeek大语言模型算法是北京深度求索人工智能基础技术研究有限公司推出的深度合成服务算法。
API请求的基础URL路径如果没有请留空
欢迎词
仪表盘欢迎词,(*)代表用户昵称
思考过程
隐藏翻译
重新翻译
当前客户端不支持该指令
默认模型
与(*)会话历史
当前
新会话
模型温度,低则保守,高则多样
例如0.7范围0-1默认0.7
模型列表
一行一个模型名称
请选择默认模型
可选数据来自模型列表
使用默认模型列表
未知操作
获取失败
获取成功
Grok是由xAI开发的生成式人工智能聊天机器人旨在通过实时回答用户问题来提供帮助。
Ollama 是一个轻量级、可扩展的框架,旨在让用户能够在本地机器上构建和运行大型语言模型。
AI 列表
AI 设置
思考中...
请先填写 Base URL
例如http://proxy.com
API请求的URL路径
如果没有请留空
获取本地模型列表
任务超期未完成
打开部门群
群组 ID
最多选择(*)个部门
已共享
联系人
未搜到跟「(*)」相关的结果
暂无相关结果
请输入关键字
请输入关键字搜索
正在拼命搜索...
加载失败,请重启软件
发送原语音
转文字失败
请稍后再试...
选择识别语言
自动识别
选择翻译结果
不翻译结果
即将到期
创建任务
在消息中打开

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More