Compare commits

...

419 Commits

Author SHA1 Message Date
kuaifan
29ed0ad05a build 2025-06-02 19:34:59 +08:00
kuaifan
0aafe79c65 no message 2025-06-02 18:36:33 +08:00
kuaifan
802afd592c fix: 修复客户端无法打开工作报告 2025-06-02 18:25:19 +08:00
kuaifan
ac1644e32d fix: 修复部分机子无法打开OKR的情况 2025-06-02 18:14:12 +08:00
kuaifan
5a1f130bec fix: 修复客户端无法打开工作报告 2025-06-02 18:13:51 +08:00
kuaifan
678868153a fix: 修复客户端无法打开工作报告 2025-06-02 10:06:22 +08:00
kuaifan
991f050dbb build 2025-05-31 22:53:46 +08:00
kuaifan
be04355685 no message 2025-05-31 10:30:47 +08:00
kuaifan
762b6b3f3b no message 2025-05-30 19:11:19 +08:00
kuaifan
71d9cbdce6 no message 2025-05-30 15:10:13 +08:00
kuaifan
d995ef19b5 no message 2025-05-30 09:25:28 +08:00
Pang
bf80e4b02b feat: 桌面端使用web服务启动 2025-05-30 07:33:14 +08:00
Pang
64a047cd7c feat: 桌面端使用web服务启动 2025-05-30 07:13:19 +08:00
kuaifan
566421003c build 2025-05-29 08:52:24 +08:00
kuaifan
198da7608d no message 2025-05-29 08:49:37 +08:00
kuaifan
0a7edb219e no message 2025-05-29 02:02:21 +08:00
kuaifan
65c20f2211 fix: 修复移动端审批列表无法滚动到底部的情况 2025-05-29 01:51:24 +08:00
kuaifan
144767152a no message 2025-05-29 01:44:06 +08:00
kuaifan
82be52be52 no message 2025-05-29 01:35:28 +08:00
kuaifan
46c8caa627 no message 2025-05-29 01:35:28 +08:00
kuaifan
0027e838a0 no message 2025-05-29 01:35:28 +08:00
kuaifan
7e846e2a58 no message 2025-05-29 01:35:28 +08:00
kuaifan
de7f55bb97 no message 2025-05-29 01:35:28 +08:00
kuaifan
2ac9a59469 no message 2025-05-29 01:35:28 +08:00
kuaifan
bdb4014b94 no message 2025-05-29 01:35:28 +08:00
weifashi
9509bd1510 fix: 修复重复周期 子任务没有复制过去 2025-05-28 22:11:56 +08:00
kuaifan
3f11770baa no message 2025-05-28 15:23:59 +08:00
kuaifan
259a040b7e no message 2025-05-28 11:42:42 +08:00
kuaifan
ccb14630f7 no message 2025-05-28 10:13:05 +08:00
kuaifan
4aa865a60f no message 2025-05-28 07:23:20 +08:00
kuaifan
c7ea7b057c build 2025-05-28 07:05:27 +08:00
kuaifan
91dbbd46c0 no message 2025-05-27 16:34:02 +08:00
kuaifan
e5146077eb no message 2025-05-27 09:18:34 +08:00
kuaifan
76918bf973 no message 2025-05-27 07:32:48 +08:00
kuaifan
b7d3e69f87 no message 2025-05-26 23:44:16 +08:00
kuaifan
d7bccfd267 no message 2025-05-26 23:15:26 +08:00
kuaifan
2a25917e41 no message 2025-05-26 23:04:00 +08:00
kuaifan
d73239b274 build 2025-05-26 22:45:59 +08:00
kuaifan
9f6fffbe6b feat: 新增应用商店 2025-05-26 21:19:51 +08:00
kuaifan
72f7ff3df5 no message 2025-05-26 21:02:06 +08:00
kuaifan
2af1dba8dc no message 2025-05-26 20:54:06 +08:00
kuaifan
ca353d747b no message 2025-05-26 20:26:40 +08:00
kuaifan
1589d4df1c no message 2025-05-26 20:12:51 +08:00
kuaifan
b3abe8af9c no message 2025-05-26 17:45:16 +08:00
kuaifan
c178e36f9b no message 2025-05-26 15:55:42 +08:00
kuaifan
d93092de99 no message 2025-05-26 15:45:37 +08:00
kuaifan
790b05880a no message 2025-05-25 18:26:32 +08:00
kuaifan
f34766ade0 no message 2025-05-25 08:46:25 +08:00
kuaifan
0e1d5e802c no message 2025-05-25 08:09:57 +08:00
kuaifan
db526dfcc8 no message 2025-05-25 00:09:14 +08:00
kuaifan
c18db60e80 no message 2025-05-24 23:48:21 +08:00
kuaifan
b579a6ade2 no message 2025-05-24 21:04:13 +08:00
kuaifan
9d1d642734 no message 2025-05-24 20:28:36 +08:00
kuaifan
261c051052 no message 2025-05-24 19:40:36 +08:00
kuaifan
e499e2d0dc no message 2025-05-24 19:21:26 +08:00
kuaifan
b860b6f389 no message 2025-05-24 19:09:40 +08:00
kuaifan
05d5d5a967 no message 2025-05-24 19:06:14 +08:00
kuaifan
74ba1cc723 no message 2025-05-24 18:26:48 +08:00
kuaifan
f2042efdc2 no message 2025-05-24 18:10:34 +08:00
kuaifan
6b7e7fa1e4 no message 2025-05-24 17:32:40 +08:00
kuaifan
6677e6e74f no message 2025-05-24 16:53:48 +08:00
kuaifan
c3994ddbea no message 2025-05-24 16:05:25 +08:00
kuaifan
a981ff2f6c no message 2025-05-24 15:57:31 +08:00
kuaifan
3e0ba398d4 no message 2025-05-24 15:42:37 +08:00
kuaifan
aa4f7c8536 no message 2025-05-24 14:39:54 +08:00
kuaifan
959f9454d8 no message 2025-05-24 13:30:01 +08:00
kuaifan
6b72a309f5 no message 2025-05-24 07:49:15 +08:00
kuaifan
c388fe373d no message 2025-05-24 07:37:08 +08:00
kuaifan
270ddc6487 feat: 检查应用是否已安装 2025-05-23 13:39:57 +08:00
kuaifan
5ccaa8f106 perf: 更新AI默认模型列表 2025-05-23 13:03:52 +08:00
王昱
1d92c2668d feat: 检查应用是否已安装 2025-05-23 12:40:33 +08:00
kuaifan
6e03a05e6d no message 2025-05-23 11:30:31 +08:00
kuaifan
2905059947 no message 2025-05-23 11:17:05 +08:00
kuaifan
1df927f771 no message 2025-05-22 23:01:07 +08:00
kuaifan
bd8b6d0319 no message 2025-05-21 16:23:11 +08:00
kuaifan
fef39b2720 no message 2025-05-20 20:07:16 +08:00
kuaifan
7de433c5fc no message 2025-05-20 17:53:49 +08:00
kuaifan
0a711f2656 no message 2025-05-18 17:06:11 +08:00
kuaifan
04b6b8aa8a no message 2025-05-18 13:25:13 +08:00
kuaifan
58f286efe4 no message 2025-05-18 11:30:42 +08:00
kuaifan
a01b292eb3 no message 2025-05-18 11:00:06 +08:00
kuaifan
18c608ad7e no message 2025-05-18 00:20:23 +08:00
kuaifan
8d144f4e12 no message 2025-05-17 23:54:24 +08:00
kuaifan
7f895bfbec no message 2025-05-17 23:25:09 +08:00
kuaifan
b0c356fa9b no message 2025-05-17 23:12:47 +08:00
kuaifan
79defdc3f3 no message 2025-05-17 22:46:10 +08:00
kuaifan
b2d9568deb no message 2025-05-17 20:56:54 +08:00
kuaifan
a130c049bf no message 2025-05-17 13:53:07 +08:00
kuaifan
8904039515 no message 2025-05-17 09:52:52 +08:00
kuaifan
7d28181b16 no message 2025-05-17 09:09:59 +08:00
kuaifan
98e4c81b9b no message 2025-05-17 07:09:47 +08:00
kuaifan
10f5af5f09 no message 2025-05-17 02:03:49 +08:00
kuaifan
18ffad5de5 no message 2025-05-16 20:24:43 +08:00
kuaifan
428db42140 no message 2025-05-16 08:23:58 +08:00
kuaifan
6e5426764e no message 2025-05-15 23:16:10 +08:00
kuaifan
f3cfcc650c no message 2025-05-15 23:04:09 +08:00
kuaifan
fc88573c9d no message 2025-05-15 22:53:48 +08:00
kuaifan
5cf1c3d14f no message 2025-05-15 22:44:41 +08:00
kuaifan
79f15cc34d no message 2025-05-15 21:37:18 +08:00
kuaifan
775cab1080 no message 2025-05-15 21:26:14 +08:00
kuaifan
3e20e7d0ce no message 2025-05-15 21:02:42 +08:00
kuaifan
54407e0a60 no message 2025-05-15 20:00:17 +08:00
kuaifan
ef696391d8 no message 2025-05-15 19:42:16 +08:00
kuaifan
0c34df290e no message 2025-05-15 17:43:04 +08:00
kuaifan
04d31bd814 no message 2025-05-15 16:56:08 +08:00
kuaifan
9888d9f59e no message 2025-05-15 16:44:58 +08:00
kuaifan
3bb1bf0967 no message 2025-05-15 16:01:45 +08:00
kuaifan
dfbcb1f45c no message 2025-05-15 15:54:58 +08:00
kuaifan
12ecf4de40 no message 2025-05-15 15:39:03 +08:00
kuaifan
7be1171004 no message 2025-05-15 15:32:52 +08:00
kuaifan
2bb646d150 no message 2025-05-15 15:03:49 +08:00
kuaifan
e7749b2dff no message 2025-05-15 09:22:18 +08:00
kuaifan
434d8eabc8 no message 2025-05-15 08:09:56 +08:00
kuaifan
0a14219112 no message 2025-05-15 00:39:25 +08:00
kuaifan
5b811df8ee no message 2025-05-15 00:20:41 +08:00
kuaifan
bc264109f3 no message 2025-05-14 23:55:00 +08:00
kuaifan
9c29c1ca9b no message 2025-05-13 12:55:08 +08:00
kuaifan
fe4f62ff8d no message 2025-05-13 09:51:21 +08:00
kuaifan
35dfb9d1ff no message 2025-05-13 01:25:24 +08:00
kuaifan
3809aca09d no message 2025-05-12 14:15:42 +08:00
kuaifan
bcd5bb5009 no message 2025-05-12 14:08:53 +08:00
kuaifan
0d7cc6a386 no message 2025-05-12 13:47:05 +08:00
kuaifan
12265699b3 no message 2025-05-12 13:32:20 +08:00
kuaifan
783c0356c7 no message 2025-05-12 13:13:23 +08:00
kuaifan
7ef31bc0b5 no message 2025-05-12 12:14:30 +08:00
kuaifan
467f2368dd no message 2025-05-12 09:49:31 +08:00
kuaifan
2cfcb081a2 no message 2025-05-12 08:33:51 +08:00
kuaifan
1829ac851d no message 2025-05-11 10:07:38 +08:00
kuaifan
2e715004ae no message 2025-05-11 00:39:15 +08:00
kuaifan
45ee092593 no message 2025-05-10 21:31:48 +08:00
kuaifan
20e543f721 no message 2025-05-10 12:32:07 +08:00
kuaifan
b2148eb656 no message 2025-05-09 18:13:45 +08:00
kuaifan
efab4fb41b no message 2025-05-09 10:43:36 +08:00
kuaifan
9ac3fd3615 no message 2025-05-09 10:16:59 +08:00
kuaifan
14bc7a0f76 no message 2025-05-09 09:53:46 +08:00
kuaifan
26727fea17 no message 2025-05-09 07:33:24 +08:00
kuaifan
34f8d4c2a6 no message 2025-05-09 00:54:10 +08:00
kuaifan
02708807bd no message 2025-05-09 00:39:21 +08:00
kuaifan
176e5de531 no message 2025-05-08 23:38:02 +08:00
kuaifan
b9180a4426 no message 2025-05-08 23:02:31 +08:00
kuaifan
959b30c788 no message 2025-05-08 20:36:26 +08:00
kuaifan
df79ef59ea no message 2025-05-08 20:05:46 +08:00
kuaifan
4dc1f01cf0 no message 2025-05-08 20:01:16 +08:00
kuaifan
cb17110562 no message 2025-05-08 19:46:10 +08:00
kuaifan
4424e4f9be no message 2025-05-08 16:46:39 +08:00
kuaifan
fb641ac960 no message 2025-05-08 15:53:30 +08:00
kuaifan
a5faa378b0 no message 2025-05-08 13:07:59 +08:00
kuaifan
66ea277a59 no message 2025-05-08 10:34:47 +08:00
kuaifan
006bc6ceda no message 2025-05-08 07:11:58 +08:00
kuaifan
aef3e869dc no message 2025-05-07 20:28:52 +08:00
kuaifan
9c46d28871 no message 2025-05-07 17:53:21 +08:00
kuaifan
1fc141050f no message 2025-05-07 13:53:12 +08:00
kuaifan
1e45d199e2 no message 2025-05-07 13:32:50 +08:00
kuaifan
3018f3653c no message 2025-05-07 12:21:09 +08:00
kuaifan
1c5b856800 no message 2025-05-07 09:41:25 +08:00
kuaifan
f53a5ea6c1 no message 2025-05-07 08:27:22 +08:00
kuaifan
a608734be9 no message 2025-05-06 23:33:00 +08:00
kuaifan
e52523f903 no message 2025-05-06 22:41:43 +08:00
kuaifan
82778014b8 no message 2025-05-06 22:12:38 +08:00
kuaifan
cb4b9a361f no message 2025-05-06 16:13:37 +08:00
kuaifan
3a337940d1 no message 2025-05-06 16:13:09 +08:00
kuaifan
9abdafb905 no message 2025-05-06 09:55:42 +08:00
kuaifan
cd494b52a4 no message 2025-05-06 09:19:21 +08:00
kuaifan
5581d1431b no message 2025-05-06 04:36:11 +08:00
kuaifan
6539b14ecf no message 2025-05-06 03:37:28 +08:00
kuaifan
45b30e4a33 no message 2025-05-05 12:08:20 +08:00
kuaifan
ff48d543e7 no message 2025-05-05 11:19:47 +08:00
kuaifan
6562b74130 no message 2025-05-05 09:38:23 +08:00
kuaifan
23a68370b4 no message 2025-05-05 08:25:21 +08:00
kuaifan
cc9f346d49 no message 2025-05-05 08:04:09 +08:00
kuaifan
8c18865138 no message 2025-05-05 06:31:01 +08:00
kuaifan
db3a17a2c8 no message 2025-05-05 00:17:45 +08:00
kuaifan
5a0d1ac0c0 no message 2025-05-04 22:45:07 +08:00
kuaifan
5414accc6c no message 2025-05-04 12:57:04 +08:00
kuaifan
c415ace453 no message 2025-05-01 12:30:20 +08:00
kuaifan
bf34beec20 no message 2025-05-01 11:55:15 +08:00
kuaifan
f4d459af7f fix: 修复录音文件转文字后无法切换翻译的问题 2025-05-01 11:49:22 +08:00
kuaifan
508cc2bd91 no message 2025-04-24 22:40:33 +08:00
kuaifan
35b7e3a289 no message 2025-04-24 21:22:04 +08:00
kuaifan
fc907d23a7 no message 2025-04-24 20:40:52 +08:00
kuaifan
45e663fcf8 no message 2025-04-24 20:15:10 +08:00
kuaifan
b00c6a9268 no message 2025-04-24 09:16:18 +08:00
kuaifan
ad7b0cd834 no message 2025-04-24 09:11:41 +08:00
kuaifan
eccb3e2825 no message 2025-04-24 08:57:43 +08:00
kuaifan
f5a343f358 no message 2025-04-24 07:00:09 +08:00
kuaifan
f8b65a5546 no message 2025-04-23 22:46:52 +08:00
kuaifan
0f0b9c5551 no message 2025-04-23 14:50:28 +08:00
kuaifan
41ab11e7b4 build 2025-04-22 23:02:38 +08:00
kuaifan
9949e7c8d4 no message 2025-04-22 22:11:36 +08:00
kuaifan
9e36d84f19 no message 2025-04-22 22:07:39 +08:00
kuaifan
ada526fa63 no message 2025-04-22 21:37:13 +08:00
kuaifan
ca65eb907d no message 2025-04-22 21:04:17 +08:00
kuaifan
7f2a0dd3e8 no message 2025-04-22 20:39:38 +08:00
kuaifan
6e34409225 no message 2025-04-22 18:12:04 +08:00
kuaifan
4af354e918 no message 2025-04-22 16:32:06 +08:00
kuaifan
207d0caf2a no message 2025-04-22 15:56:35 +08:00
kuaifan
f7487d22d5 no message 2025-04-22 15:52:47 +08:00
kuaifan
3c10976aff no message 2025-04-22 15:00:29 +08:00
kuaifan
ec8e144655 no message 2025-04-22 12:35:41 +08:00
kuaifan
ae5ccfd775 no message 2025-04-22 11:48:54 +08:00
kuaifan
9d9e22451d no message 2025-04-22 09:37:48 +08:00
kuaifan
e68daf870f no message 2025-04-22 09:06:29 +08:00
kuaifan
534e95f86c no message 2025-04-22 00:19:36 +08:00
kuaifan
077713003f feat: 添加删除附件日志记录 2025-04-21 22:56:51 +08:00
kuaifan
35fd8e62ac no message 2025-04-21 22:48:26 +08:00
kuaifan
b7c2ddd59d perf: 优化从任务页面发送消息 2025-04-21 21:29:15 +08:00
kuaifan
f931567f56 perf: 优化已归档/已删除任务列表支持按状态检索 2025-04-21 20:14:31 +08:00
kuaifan
8545e0692c fix: 修复任务详情查看历史空白的情况 2025-04-21 19:53:17 +08:00
kuaifan
19eb05269b fix: 修复我的机器人不回复的情况 2025-04-21 18:28:56 +08:00
kuaifan
92d757662a perf: 优化长按消息菜单位置 2025-04-21 14:50:18 +08:00
kuaifan
1b1406a4d9 perf: 优化登录设备名称 2025-04-21 14:49:36 +08:00
kuaifan
03fc19f070 perf: 优化登录设备名称 2025-04-21 14:26:31 +08:00
kuaifan
ffa09f1b29 fix: 修复设待办后数据不立即显示的问题 2025-04-21 13:46:42 +08:00
kuaifan
ff9a1523fd no message 2025-04-21 12:40:49 +08:00
kuaifan
8a7e5c0830 no message 2025-04-21 12:19:20 +08:00
kuaifan
8e90ad69b1 no message 2025-04-21 12:06:47 +08:00
kuaifan
52c389edd8 no message 2025-04-21 12:06:47 +08:00
kuaifan
f7206c1603 no message 2025-04-21 12:06:35 +08:00
kuaifan
86d9baa503 no message 2025-04-21 08:36:49 +08:00
kuaifan
92c4565590 no message 2025-04-21 01:54:50 +08:00
kuaifan
c51870ff79 build 2025-04-21 00:56:08 +08:00
kuaifan
182f061354 no message 2025-04-20 23:10:31 +08:00
kuaifan
80507cab27 no message 2025-04-20 19:37:12 +08:00
kuaifan
f801ae9b63 no message 2025-04-20 17:53:32 +08:00
kuaifan
977173d987 no message 2025-04-20 09:40:46 +08:00
kuaifan
cd0fcb903f no message 2025-04-20 09:19:46 +08:00
kuaifan
7bae1d9537 feat: 新增系统分享搜索功能 2025-04-20 00:24:33 +08:00
kuaifan
b43cbb7afe no message 2025-04-19 22:06:07 +08:00
kuaifan
72982387cc no message 2025-04-19 21:39:02 +08:00
kuaifan
ff0245840a no message 2025-04-19 21:33:10 +08:00
kuaifan
c55f64e209 no message 2025-04-19 21:21:30 +08:00
kuaifan
a4cb5d1b14 no message 2025-04-19 19:48:24 +08:00
kuaifan
13e1415355 no message 2025-04-19 19:11:05 +08:00
kuaifan
7b49d66a8e no message 2025-04-19 19:06:34 +08:00
kuaifan
63c6e12aca no message 2025-04-19 16:57:56 +08:00
kuaifan
b64d4fd96f no message 2025-04-19 07:57:43 +08:00
kuaifan
dda603c7d8 perf: 优化通用菜单 2025-04-19 01:09:29 +08:00
kuaifan
e22de5cba1 no message 2025-04-18 22:46:19 +08:00
kuaifan
bdabfdcb3d perf: 优化视频压缩 2025-04-18 22:28:24 +08:00
kuaifan
00a8514245 no message 2025-04-18 21:44:33 +08:00
kuaifan
94fd3197b3 no message 2025-04-18 20:26:35 +08:00
kuaifan
7957353c3f no message 2025-04-18 19:25:13 +08:00
kuaifan
b3b7589db3 no message 2025-04-18 14:49:05 +08:00
kuaifan
5aed9ce29e perf: 优化全文搜索 2025-04-18 13:56:11 +08:00
kuaifan
924f0a9f7c perf: 优化全文搜索 2025-04-18 12:40:32 +08:00
kuaifan
7a7cd72db9 perf: 优化全文搜索 2025-04-18 11:59:28 +08:00
kuaifan
e9e9bab479 perf: 优化全文搜索 2025-04-18 01:45:03 +08:00
kuaifan
f258dcfca2 perf: 优化全文搜索 2025-04-18 00:46:59 +08:00
kuaifan
fe84f812e7 perf: 优化全文搜索 2025-04-17 22:14:38 +08:00
kuaifan
9eba376976 perf: 优化全文搜索 2025-04-17 21:55:14 +08:00
kuaifan
462705c4ed perf: 优化全文搜索 2025-04-17 16:45:13 +08:00
kuaifan
a2533ce7f9 perf: 优化全文搜索 2025-04-17 13:04:45 +08:00
kuaifan
dbf42c51a4 perf: 优化全文搜索 2025-04-17 12:45:47 +08:00
kuaifan
f61e7caf2b perf: 优化全文搜索 2025-04-17 12:27:21 +08:00
kuaifan
679c2070c1 perf: 优化全文搜索 2025-04-17 11:14:11 +08:00
kuaifan
92d46e1da3 no message 2025-04-17 10:34:32 +08:00
kuaifan
7ab94205e4 no message 2025-04-17 10:09:28 +08:00
kuaifan
ab616c5d32 perf: 优化长按菜单 2025-04-17 09:46:57 +08:00
kuaifan
8f2f68dffc no message 2025-04-17 09:45:48 +08:00
kuaifan
18b7e17e95 no message 2025-04-16 21:34:33 +08:00
kuaifan
cca2298d3a no message 2025-04-16 19:47:39 +08:00
kuaifan
f3683bcc84 no message 2025-04-16 13:00:36 +08:00
kuaifan
fa2959515e no message 2025-04-16 08:49:33 +08:00
kuaifan
7ab5ddc408 no message 2025-04-15 00:24:54 +08:00
kuaifan
f273858248 build 2025-04-15 00:06:25 +08:00
kuaifan
ca8f7374da no message 2025-04-14 23:59:23 +08:00
kuaifan
ff1dce833a no message 2025-04-14 22:08:59 +08:00
kuaifan
d3d5a7bade no message 2025-04-14 19:48:21 +08:00
kuaifan
f5d6702472 no message 2025-04-14 18:23:04 +08:00
kuaifan
3db687ad40 no message 2025-04-14 17:30:59 +08:00
kuaifan
a5cb958398 perf: 优化移动任务 2025-04-14 15:50:20 +08:00
kuaifan
9e522091c6 no message 2025-04-14 15:29:53 +08:00
kuaifan
79f256976e no message 2025-04-14 14:24:00 +08:00
kuaifan
b560c0bafd feat: 新增任务发送功能 2025-04-14 13:40:46 +08:00
kuaifan
bd157d305e fix: 修复调整任务排序后出现空白的情况 2025-04-14 13:22:11 +08:00
kuaifan
923016197a perf: 优化自己的对话不限修改撤回时间 2025-04-14 13:18:38 +08:00
kuaifan
dcf96e2bf5 perf: 优化访问链接 2025-04-14 13:02:04 +08:00
kuaifan
d4697cb203 perf: 优化访问链接 2025-04-14 12:00:54 +08:00
kuaifan
6e6a50b46e no message 2025-04-14 09:39:47 +08:00
kuaifan
b9830bc64a no message 2025-04-14 08:00:02 +08:00
kuaifan
7c501cec45 no message 2025-04-13 14:59:37 +08:00
kuaifan
add23934ca no message 2025-04-13 13:04:03 +08:00
kuaifan
a8b798b00c no message 2025-04-13 11:33:43 +08:00
kuaifan
b522b1de05 no message 2025-04-13 11:19:37 +08:00
kuaifan
3660cbd450 no message 2025-04-13 10:50:47 +08:00
kuaifan
50f8bb8721 feat: 新增会员详情窗口 2025-04-13 10:50:40 +08:00
kuaifan
e1a2d90382 no message 2025-04-13 09:16:19 +08:00
kuaifan
d8872f215b no message 2025-04-13 00:18:39 +08:00
kuaifan
484bc6ea39 no message 2025-04-12 19:45:14 +08:00
kuaifan
7d1979f067 perf: 优化日历 2025-04-12 19:45:06 +08:00
kuaifan
6927c0b30b no message 2025-04-12 18:54:39 +08:00
kuaifan
aa74c5ccaf perf: 优化长按事件 2025-04-12 18:42:27 +08:00
kuaifan
e3d0f571d2 no message 2025-04-12 17:47:43 +08:00
kuaifan
d03dabdfdf perf: 优化日历 2025-04-12 17:40:05 +08:00
kuaifan
fc339ae55f no message 2025-04-12 17:39:23 +08:00
kuaifan
a0aa04fd8c no message 2025-04-12 15:08:33 +08:00
kuaifan
6dc5ae1ae4 perf: 优化移动端任务窗口布局 2025-04-12 13:20:17 +08:00
kuaifan
df02a6b50f no message 2025-04-12 11:55:43 +08:00
kuaifan
9e4f733c28 no message 2025-04-12 09:11:58 +08:00
kuaifan
1175b330f5 no message 2025-04-11 19:17:26 +08:00
kuaifan
3cb9fff07f perf: 优化长按操作 2025-04-11 13:47:53 +08:00
kuaifan
bfdb72dd0a no message 2025-04-11 09:55:18 +08:00
kuaifan
5489462f90 no message 2025-04-11 09:02:16 +08:00
kuaifan
94ac3c3922 no message 2025-04-10 17:24:51 +08:00
kuaifan
bf75946e14 no message 2025-04-10 17:06:12 +08:00
kuaifan
b2a70e0cce perf: 优化转发确认选项保持上一次选择 2025-04-10 17:05:42 +08:00
kuaifan
83780f9bcd feat: 添加从团队管理打开会话窗口 2025-04-10 17:05:03 +08:00
kuaifan
bfb9795913 no message 2025-04-10 16:50:11 +08:00
kuaifan
208598a6df no message 2025-04-10 16:02:01 +08:00
kuaifan
6c79753051 no message 2025-04-10 11:57:56 +08:00
kuaifan
095a238fff perf: 优化移动端布局 2025-04-10 11:29:31 +08:00
kuaifan
ebbde8afd3 perf: 优化移动端布局 2025-04-10 11:13:19 +08:00
kuaifan
bba5bb7411 fix: 修复移动任务时负责人和协助人可以同时选择的情况 2025-04-10 11:06:29 +08:00
kuaifan
9c155c6cf5 perf: 优化禁止选择会员效果 2025-04-10 11:05:50 +08:00
kuaifan
19da7a74df perf: 优化长按菜单位置 2025-04-10 10:47:12 +08:00
kuaifan
f5d76fd5ff perf: 优化移动端打开会话等待效果 2025-04-10 10:40:40 +08:00
kuaifan
77940c9430 perf: 优化长按菜单位置 2025-04-10 07:46:32 +08:00
kuaifan
54a42a14b6 perf: 优化会议弹窗 2025-04-10 07:45:51 +08:00
kuaifan
52faf7884b perf: 任务详情点任务聊天时不要发送消息 2025-04-10 07:27:10 +08:00
kuaifan
841ed4e682 perf: 优化移动端布局 2025-04-09 23:25:48 +08:00
kuaifan
bc417b9eea perf: 优化移动端布局 2025-04-09 19:12:12 +08:00
kuaifan
da7dc477c8 no message 2025-04-09 13:47:26 +08:00
kuaifan
6c519ebd61 no message 2025-04-08 21:43:41 +08:00
kuaifan
88e859817b no message 2025-04-08 15:34:05 +08:00
kuaifan
f5dd36260f perf: 优化国际化 2025-04-08 15:33:08 +08:00
kuaifan
a5325b84ae no message 2025-04-08 15:04:00 +08:00
kuaifan
7095c9e71e no message 2025-04-08 14:43:56 +08:00
kuaifan
fb4373c83a fix: 修复无法从任务页面打开聊天的情况 2025-04-08 14:31:01 +08:00
kuaifan
dd59a1aebb no message 2025-04-08 12:38:30 +08:00
kuaifan
6f7edd0b40 fix: 修复移动端焦点抖动的问题 2025-04-08 08:49:30 +08:00
kuaifan
397421010e build 2025-04-07 23:11:32 +08:00
kuaifan
8872b0519d no message 2025-04-07 23:09:24 +08:00
kuaifan
83d3b3ffbf fix: 修复部分页面出现空白的情况 2025-04-07 20:09:45 +08:00
kuaifan
d1702bd62c no message 2025-04-07 18:26:02 +08:00
kuaifan
928235eac8 fix: 修复输入框无法点击添加链接的情况 2025-04-07 16:22:36 +08:00
kuaifan
3bdfaab158 perf: 优化数据结构 2025-04-07 14:32:24 +08:00
kuaifan
d7902b4d08 perf: 优化数据结构 2025-04-07 14:06:27 +08:00
kuaifan
3334abfb8f no message 2025-04-07 12:16:33 +08:00
kuaifan
3e44e584c0 perf: 优化数据结构 2025-04-07 11:41:01 +08:00
kuaifan
195a305fc3 perf: 优化数据结构 2025-04-07 11:35:19 +08:00
kuaifan
cedffd17b3 perf: 优化图片存储名 2025-04-07 11:10:17 +08:00
kuaifan
59b29014d9 fix: 修复AI机器人不存在的情况 2025-04-07 09:05:17 +08:00
kuaifan
06db036e4a build 2025-04-07 08:49:39 +08:00
kuaifan
617e0837c9 no message 2025-04-07 08:46:09 +08:00
kuaifan
7a275bd802 perf: 优化数据结构 2025-04-07 08:30:06 +08:00
kuaifan
83f58eae68 perf: 优化数据结构 2025-04-07 06:55:35 +08:00
kuaifan
19815fe27d no message 2025-04-07 06:47:59 +08:00
kuaifan
0f75556bed perf: 优化数据结构 2025-04-07 06:15:17 +08:00
kuaifan
dc0f925d24 no message 2025-04-07 01:21:50 +08:00
kuaifan
c5948c4171 feat: 新增转发至AI开启新会话 2025-04-07 01:20:58 +08:00
kuaifan
d144b06c1f perf: 优化数据结构 2025-04-06 23:43:18 +08:00
kuaifan
92dfea677b perf: 优化数据结构 2025-04-06 23:14:20 +08:00
kuaifan
7b5867e2c0 perf: 优化数据结构 2025-04-04 09:18:51 +08:00
kuaifan
b643fe56d5 perf: 优化数据结构 2025-04-04 08:22:18 +08:00
kuaifan
38fa72e9da perf: 优化数据结构 2025-04-03 22:35:46 +08:00
kuaifan
82fddefc94 perf: 优化消息窗口显示 2025-04-03 22:12:29 +08:00
kuaifan
0c9c9cb90a perf: 优化消息窗口显示 2025-04-03 10:01:02 +08:00
kuaifan
38b50a8a84 perf: 优化消息窗口显示 2025-04-02 20:35:03 +08:00
kuaifan
0f250dbafd perf: 优化目录结构 2025-04-02 19:17:03 +08:00
kuaifan
168650649f perf: 优化日历 2025-04-02 18:48:25 +08:00
kuaifan
52babf82ae perf: 优化任务时间范围选择 2025-03-31 23:49:11 +08:00
kuaifan
0c8517667f build 2025-03-30 11:13:39 +08:00
kuaifan
77d105cb9f no message 2025-03-30 10:45:24 +08:00
kuaifan
8af33ea66a perf: 优化消息窗口 2025-03-30 10:13:43 +08:00
kuaifan
a57740e14e perf: 优化消息窗口 2025-03-30 08:56:22 +08:00
kuaifan
82230d70a5 perf: 优化消息窗口 2025-03-30 08:50:50 +08:00
kuaifan
15f3f9c0e5 perf: 优化消息窗口 2025-03-30 07:51:13 +08:00
kuaifan
7fc328492b no message 2025-03-29 22:09:07 +08:00
kuaifan
81cedca590 no message 2025-03-29 14:11:54 +08:00
kuaifan
df4e00e23f perf: 优化消息长按菜单 2025-03-29 14:11:47 +08:00
kuaifan
ad70f23a05 perf: 优化内置浏览器 2025-03-29 13:55:16 +08:00
kuaifan
c93beb27fd no message 2025-03-29 12:44:20 +08:00
kuaifan
41da2231ed no message 2025-03-29 12:33:12 +08:00
kuaifan
9d9213fbdb feat: 添加移动端提示可能要发送的图片 2025-03-29 00:23:49 +08:00
kuaifan
50106d19e8 fix: 修复未读数错误暴增的情况 2025-03-28 19:44:43 +08:00
kuaifan
62d1e676bd perf: 优化App隐私政策提示 2025-03-28 19:35:12 +08:00
kuaifan
0b7d49785c no message 2025-03-28 18:45:09 +08:00
kuaifan
40736c4a05 no message 2025-03-28 18:30:53 +08:00
kuaifan
1f0ab02702 feat: 添加移动端提示可能要发送的图片 2025-03-28 17:01:07 +08:00
kuaifan
21aa4f7b2b fix: 修复地址可能存在localhost的情况 2025-03-28 15:45:13 +08:00
kuaifan
43d0a85061 feat: 添加移动端提示可能要发送的图片 2025-03-28 14:07:33 +08:00
kuaifan
8bdd31ae67 no message 2025-03-28 14:07:26 +08:00
kuaifan
b78f93979d no message 2025-03-27 20:46:12 +08:00
kuaifan
8d6b4a1d2e feat: 添加移动端提示可能要发送的图片 2025-03-27 20:46:03 +08:00
kuaifan
7630c83ae0 perf: 优化对话独立窗口显示 2025-03-27 20:25:51 +08:00
kuaifan
c7c47aff5a fix: 修复消息编辑和发布时序号对不上 2025-03-27 18:09:07 +08:00
kuaifan
72e475cb84 fix: 修复草稿出现上一次内容的情况 2025-03-27 17:03:26 +08:00
kuaifan
f750a6aec2 no message 2025-03-26 23:50:41 +08:00
kuaifan
0dde37e1f1 no message 2025-03-26 18:57:19 +08:00
kuaifan
a2066fc137 no message 2025-03-26 15:26:07 +08:00
kuaifan
f652f35c3a perf: 优化移动端选择交互 2025-03-26 14:17:15 +08:00
kuaifan
562697da27 fix: 修复本地群消息通知没有会员昵称的问题 2025-03-25 18:31:44 +08:00
kuaifan
d23d77ff90 perf: 优化移动端选中消息文本 2025-03-25 18:14:40 +08:00
kuaifan
119443cc88 no message 2025-03-25 17:36:21 +08:00
kuaifan
27ae831799 perf: 优化移动端选中消息文本 2025-03-25 17:35:47 +08:00
kuaifan
b482947207 perf: 优化撤回消息逻辑 2025-03-25 16:39:49 +08:00
kuaifan
6ebc89695a perf: 优化移动端选中消息文本 2025-03-25 16:38:19 +08:00
kuaifan
a65dfec7a8 perf: 优化提及搜索 2025-03-24 21:53:04 +08:00
kuaifan
a0cd79e587 fix: 修复了拉人进群无法踢出去的问题 2025-03-24 21:33:04 +08:00
kuaifan
8fe1e2fee4 no message 2025-03-24 21:13:17 +08:00
kuaifan
3cc9f7bc40 fix: 提及出现白色字的情况 2025-03-24 21:05:25 +08:00
kuaifan
8d3d5025ed no message 2025-03-24 20:39:53 +08:00
kuaifan
a49c0aea47 perf: 优化机器人Webhook消息 2025-03-24 20:34:28 +08:00
kuaifan
d366cf9885 build 2025-03-23 23:24:48 +08:00
kuaifan
be53afe6b4 no message 2025-03-22 19:24:28 +08:00
kuaifan
cdd980112d no message 2025-03-22 19:02:00 +08:00
kuaifan
bca284969d no message 2025-03-22 18:44:09 +08:00
kuaifan
dd899a3e13 feat: 添加我的机器人管理 2025-03-22 18:19:39 +08:00
kuaifan
d6ca66aa2f no message 2025-03-21 22:16:47 +08:00
kuaifan
20ba671cd3 no message 2025-03-21 13:37:27 +08:00
kuaifan
672795ac49 perf: 优化初始化逻辑 2025-03-21 13:37:21 +08:00
kuaifan
9716d7fe43 perf: 优化docker配置 2025-03-21 11:34:09 +08:00
kuaifan
193ad8d902 build 2025-03-21 09:06:41 +08:00
kuaifan
a87f903c50 no message 2025-03-21 09:04:58 +08:00
kuaifan
82f154a229 no message 2025-03-21 00:18:09 +08:00
kuaifan
bee36801ab feat: 新增独立窗口打开会话 2025-03-21 00:09:16 +08:00
kuaifan
37f379c890 no message 2025-03-20 23:18:04 +08:00
kuaifan
88b995ca9c perf: 优化AI支持文件类型 2025-03-20 15:34:31 +08:00
kuaifan
919289c5ca fix: 修复搜索结果显示即将到期 2025-03-20 14:51:13 +08:00
903 changed files with 15980 additions and 59294 deletions

View File

@@ -17,7 +17,7 @@ LOG_CHANNEL=stack
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST="${APP_IPPR}.5"
DB_HOST=mariadb
DB_PORT=3306
DB_DATABASE=dootask
DB_USERNAME=dootask
@@ -34,7 +34,7 @@ SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
REDIS_HOST="${APP_IPPR}.4"
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379

1
.gitignore vendored
View File

@@ -29,3 +29,4 @@ vars.yaml
laravels.conf
laravels.pid
README_LOCAL.md
dootask.lock

View File

@@ -2,6 +2,179 @@
All notable changes to this project will be documented in this file.
## [1.0.37]
### Bug Fixes
- 修复客户端无法打开工作报告
- 修复部分机子无法打开OKR的情况
## [1.0.31]
### Bug Fixes
- 修复移动端审批列表无法滚动到底部的情况
- 修复重复周期 子任务没有复制过去
### Features
- 桌面端使用web服务启动
## [1.0.0]
### Bug Fixes
- 修复录音文件转文字后无法切换翻译的问题
### Features
- 新增应用商店
- 检查应用是否已安装
### Performance
- 更新AI默认模型列表
## [0.47.7]
### Bug Fixes
- 修复任务详情查看历史空白的情况
- 修复我的机器人不回复的情况
- 修复设待办后数据不立即显示的问题
### Features
- 添加删除附件日志记录
### Performance
- 优化从任务页面发送消息
- 优化已归档/已删除任务列表支持按状态检索
- 优化长按消息菜单位置
- 优化登录设备名称
## [0.46.74]
### Features
- 新增系统分享搜索功能
### Performance
- 优化通用菜单
- 优化视频压缩
- 优化全文搜索
- 优化长按菜单
## [0.46.16]
### Bug Fixes
- 修复调整任务排序后出现空白的情况
- 修复移动任务时负责人和协助人可以同时选择的情况
- 修复无法从任务页面打开聊天的情况
- 修复移动端焦点抖动的问题
### Features
- 新增任务发送功能
- 新增会员详情窗口
- 添加从团队管理打开会话窗口
### Performance
- 优化移动任务
- 优化自己的对话不限修改撤回时间
- 优化访问链接
- 优化日历
- 优化长按事件
- 优化移动端任务窗口布局
- 优化长按操作
- 优化转发确认选项保持上一次选择
- 优化移动端布局
- 优化禁止选择会员效果
- 优化长按菜单位置
- 优化移动端打开会话等待效果
- 优化会议弹窗
- 任务详情点任务聊天时不要发送消息
- 优化国际化
## [0.45.64]
### Bug Fixes
- 修复部分页面出现空白的情况
- 修复输入框无法点击添加链接的情况
- 修复AI机器人不存在的情况
### Features
- 新增转发至AI开启新会话
### Performance
- 优化数据结构
- 优化图片存储名
- 优化消息窗口显示
- 优化目录结构
- 优化日历
- 优化任务时间范围选择
## [0.45.33]
### Bug Fixes
- 修复未读数错误暴增的情况
- 修复地址可能存在localhost的情况
- 修复消息编辑和发布时序号对不上
- 修复草稿出现上一次内容的情况
- 修复本地群消息通知没有会员昵称的问题
- 修复了拉人进群无法踢出去的问题
- 提及出现白色字的情况
### Features
- 添加移动端提示可能要发送的图片
### Performance
- 优化消息窗口
- 优化消息长按菜单
- 优化内置浏览器
- 优化App隐私政策提示
- 优化对话独立窗口显示
- 优化移动端选择交互
- 优化移动端选中消息文本
- 优化撤回消息逻辑
- 优化提及搜索
- 优化机器人Webhook消息
## [0.44.91]
### Features
- 添加我的机器人管理
### Performance
- 优化初始化逻辑
- 优化docker配置
## [0.44.82]
### Bug Fixes
- 修复搜索结果显示即将到期
### Features
- 新增独立窗口打开会话
### Performance
- 优化AI支持文件类型
## [0.44.74]
### Performance

139
README.md
View File

@@ -1,148 +1,147 @@
# Install (Docker)
# DooTask - Open Source Task Management System
English | **[中文文档](./README_CN.md)**
- [Screenshot preview](./README_PREVIEW.md)
- [Demo site](http://www.dootask.com/)
- [Screenshot Preview](./README_PREVIEW.md)
- [Demo Site](http://www.dootask.com/)
**QQ Group**
Group No.: `546574618`
- Group Number: `546574618`
## Setup
## 📍 Migration from 0.x to 1.x
- `Docker v20.10+` & `Docker Compose v2.0+` must be installed
- System: `Centos/Debian/Ubuntu/macOS/Windows`
- Hardware suggestion: 2 cores and above 4G memory
- Special note: Windows users please use `git bash` or `cmder` to run the command
- Please ensure to back up your data before upgrading!
- If the upgrade fails, try running `./cmd update` multiple times.
- If you encounter "Container xxx not found" during upgrade, run `./cmd reup` and then execute `./cmd update`.
- If you see a 502 error after upgrading, run `./cmd reup` to restart the services.
- If you encounter "Application 'xxx' not installed" after upgrading, log in with the admin account and install the relevant applications from the App Store.
### Deployment (Pro Edition)
## Installation Requirements
- Required: `Docker v20.10+` and `Docker Compose v2.0+`
- Supported Systems: `CentOS/Debian/Ubuntu/macOS` and other Linux/Unix systems
- Hardware Recommendation: 2+ cores, 4GB+ memory
- Special Note: Windows users can install Linux environment using WSL2 before installing DooTask.
### Deploy Project
```bash
# 1、Clone the repository
# 1、Clone the project to your local machine or server
# Clone projects on github
git clone -b pro --depth=1 https://github.com/kuaifan/dootask.git
# Or you can use gitee
git clone -b pro --depth=1 https://gitee.com/aipaw/dootask.git
# Clone project from GitHub
git clone --depth=1 https://github.com/kuaifan/dootask.git
# Or you can use Gitee
git clone --depth=1 https://gitee.com/aipaw/dootask.git
# 2、Enter directory
cd dootask
# 3、InstallationCustom port installation, as: ./cmd install --port 80
# 3、One-click installation (Custom port installation: ./cmd install --port 80)
./cmd install
```
### Reset password
### Reset Password
```bash
# Reset default account password
# Reset default administrator password
./cmd repassword
```
### Change port
### Change Port
```bash
# This method only replaces the HTTP port. To replace the HTTPS port, please read the SSL configuration below
# This method only changes HTTP port. For HTTPS port, please read SSL configuration below
./cmd port 80
```
### Stop server
### Stop Service
```bash
./cmd stop
# P.S: Once application is set up, whenever you want to start the server (if it is stopped) run below command
./cmd start
./cmd down
```
### Development compilation
- `NodeJs 20+` must be installed
### Start Service
```bash
# Development
./cmd up
```
### Development & Build
Please ensure you have installed `NodeJs 20+`
```bash
# Development mode
./cmd dev
# Production (This is web client. For App/PC/Mac clients, Please read README-CLIENT.md)
# Build project (This is for web client. For desktop apps, refer to ".github/workflows/publish.yml")
./cmd prod
```
### Shortcuts for running command
### SSL Configuration
```bash
# You can do this using the following command
./cmd artisan "your command" # To run a artisan command
./cmd php "your command" # To run a php command
./cmd nginx "your command" # To run a nginx command
./cmd redis "your command" # To run a redis command
./cmd composer "your command" # To run a composer command
./cmd supervisorctl "your command" # To run a supervisorctl command
./cmd mysql "your command" # To run a mysql command (backup: Backup database, recovery: Restore database, open: Open database external port access, close: Close database external port access)
```
### SSL configuration
#### Method 1: Automatic configuration
#### Method 1: Automatic Configuration
```bash
# Running commands in a project
# Run command and follow the prompts
./cmd https
```
#### Or Method 2: Nginx Agent Configuration
#### Method 2: Nginx Proxy Configuration
```bash
# 1、Nginx config add
# 1、Add Nginx proxy configuration
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 2、Running commands in a project (If you unconfigure the NGINX agent, run: ./cmd https close)
# 2、Run command (To cancel Nginx proxy configuration: ./cmd https close)
./cmd https agent
```
## Upgrade
## Upgrade & Update
**Note: Please back up your data before upgrading!**
**Note: Please backup your data before upgrading!**
```bash
# Method 1: Running commands in a project
./cmd update
# Or method 2: use this method if method 1 fails
git pull
./cmd mysql backup
./cmd uninstall
./cmd install
./cmd mysql recovery
```
* Please try again if the upgrade fails across a large version.
* If 502 after the upgrade please run `./cmd restart` restart the service.
* Please retry if upgrade fails across major versions.
* If you encounter 502 errors after upgrade, run `./cmd reup` to restart services.
## Transfer
## Project Migration
Follow these steps to complete the project migration after the new project is installed:
After installing the new project, follow these steps to complete migration:
1. Backup original database
1Backup original database
```bash
# Run command under old project
# Run command in the old project
./cmd mysql backup
```
2. Copy `database backup file` and `public/uploads` directory to the new project.
2Copy the following files and directories from old project to the same paths in new project
3. Restore database to new project
- `Database backup file`
- `docker/appstore`
- `public/uploads`
3、Restore database to new project
```bash
# Run command under new project
# Run command in the new project
./cmd mysql recovery
```
## Uninstall
## Uninstall Project
```bash
# Running commands in a project
./cmd uninstall
```
### More Commands
```bash
./cmd help
```

View File

@@ -1,4 +1,4 @@
# Install (Docker)
# DooTask - 开源任务管理系统
**[English](./README.md)** | 中文文档
@@ -9,22 +9,30 @@
- QQ群号: `546574618`
## 📍 0.x 迁移到 1.x
- 升级时请务必备份好数据!
- 如果升级失败请尝试执行 `./cmd update` 重试几次。
- 如果升级中出现 `没有找到 xxx 容器` 的提示,请运行 `./cmd reup` 后再执行 `./cmd update`
- 如果升级后出现502错误请运行 `./cmd reup` 重启服务即可。
- 如果升级后出现 `应用「xxx」未安装` 的提示,请使用管理员账号进入应用商店安装相关应用。
## 安装程序
- 必须安装:`Docker v20.10+``Docker Compose v2.0+`
- 支持环境:`Centos/Debian/Ubuntu/macOS/Windows`
- 支持环境:`Centos/Debian/Ubuntu/macOS` 等 linux/unix 系统
- 硬件建议2核4G以上
- 特别说明Windows 用户请使用 `git bash` 或者 `cmder` 运行命令
- 特别说明Windows 可以使用 WSL2 安装 Linux 环境后再安装 DooTask。
### 部署项目Pro版
### 部署项目
```bash
# 1、克隆项目到您的本地或服务器
# 通过github克隆项目
git clone -b pro --depth=1 https://github.com/kuaifan/dootask.git
git clone --depth=1 https://github.com/kuaifan/dootask.git
# 或者你也可以使用gitee
git clone -b pro --depth=1 https://gitee.com/aipaw/dootask.git
git clone --depth=1 https://gitee.com/aipaw/dootask.git
# 2、进入目录
cd dootask
@@ -50,48 +58,37 @@ cd dootask
### 停止服务
```bash
./cmd stop
./cmd down
```
# 一旦应用程序被设置,无论何时你想要启动服务器(如果它被停止)运行以下命令
./cmd start
### 启动服务
```bash
./cmd up
```
### 开发编译
- 请确保你已经安装了 `NodeJs 20+`
请确保你已经安装了 `NodeJs 20+`
```bash
# 开发模式
./cmd dev
# 编译项目(这是网页端的,App/Pc/Mac客户端请查看 README_CLIENT.md
# 编译项目(这是网页端的,客户端请参考“.github/workflows/publish.yml”文件
./cmd prod
```
### 运行命令的快捷方式
```bash
# 你可以使用以下命令来执行
./cmd artisan "your command" # 运行 artisan 命令
./cmd php "your command" # 运行 php 命令
./cmd nginx "your command" # 运行 nginx 命令
./cmd redis "your command" # 运行 redis 命令
./cmd composer "your command" # 运行 composer 命令
./cmd supervisorctl "your command" # 运行 supervisorctl 命令
./cmd mysql "your command" # 运行 mysql 命令 (backup: 备份数据库recovery: 还原数据库open: 开启数据库外部端口访问close: 关闭数据库外部端口访问)
```
### SSL 配置
#### 方法1自动配置
```bash
# 在项目下运行命令,根据提示执行即可
# 执行指令,根据提示执行即可
./cmd https
```
#### (或者)方法2Nginx 代理配置
#### 方法2Nginx 代理配置
```bash
# 1、Nginx 代理配置添加
@@ -99,7 +96,7 @@ proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 2、在项目下运行命令(如果取消 Nginx 代理配置请运行:./cmd https close
# 2、执行指令(如果取消 Nginx 代理配置请运行:./cmd https close
./cmd https agent
```
@@ -108,19 +105,11 @@ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
**注意:在升级之前请备份好你的数据!**
```bash
# 方法1在项目下运行命令
./cmd update
# 或者方法2如果方法1失败请使用此方法
git pull
./cmd mysql backup
./cmd uninstall
./cmd install
./cmd mysql recovery
```
* 跨越大版本升级失败时请重试执行一次。
* 如果升级后出现502请运行 `./cmd restart` 重启服务即可。
* 如果升级后出现502请运行 `./cmd reup` 重启服务即可。
## 迁移项目
@@ -129,21 +118,30 @@ git pull
1、备份原数据库
```bash
# 在旧的项目下运行命
# 在旧的项目下执行指
./cmd mysql backup
```
2、将`数据库备份文件`及`public/uploads`目录拷贝至新项目
2、将旧项目以下文件和目录拷贝至新项目同路径位置
- `数据库备份文件`
- `docker/appstore`
- `public/uploads`
3、还原数据库至新项目
```bash
# 在新的项目下运行命
# 在新的项目下执行指
./cmd mysql recovery
```
## 卸载项目
```bash
# 在项目下运行命令
./cmd uninstall
```
### 更多指令
```bash
./cmd help
```

View File

@@ -1,254 +0,0 @@
<?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

@@ -0,0 +1,175 @@
<?php
namespace App\Console\Commands;
use App\Models\WebSocketDialogMsg;
use App\Module\Apps;
use App\Module\ZincSearch\ZincSearchKeyValue;
use App\Module\ZincSearch\ZincSearchDialogMsg;
use Cache;
use Illuminate\Console\Command;
class SyncUserMsgToZincSearch extends Command
{
/**
* 更新数据
* --f: 全量更新 (默认)
* --i: 增量更新从上次更新的最后一个ID接上
*
* 清理数据
* --c: 清除索引
*/
protected $signature = 'zinc:sync-user-msg {--f} {--i} {--c} {--batch=1000}';
protected $description = '同步聊天会话用户和消息到 ZincSearch';
/**
* @return int
*/
public function handle(): int
{
if (!Apps::isInstalled("search")) {
$this->error("应用「ZincSearch」未安装");
return 1;
}
// 注册信号处理器仅在支持pcntl扩展的环境下
if (extension_loaded('pcntl')) {
pcntl_async_signals(true); // 启用异步信号处理
pcntl_signal(SIGINT, [$this, 'handleSignal']); // Ctrl+C
pcntl_signal(SIGTERM, [$this, 'handleSignal']); // kill
}
// 检查锁,如果已被占用则退出
$lockInfo = $this->getLock();
if ($lockInfo) {
$this->error("命令已在运行中,开始时间: {$lockInfo['started_at']}");
return 1;
}
// 设置锁
$this->setLock();
// 清除索引
if ($this->option('c')) {
$this->info('清除索引...');
ZincSearchKeyValue::clear();
ZincSearchDialogMsg::clear();
$this->info("索引删除成功");
$this->releaseLock();
return 0;
}
$this->info('开始同步聊天数据...');
// 同步消息数据
$this->syncDialogMsgs();
// 完成
$this->info("\n同步完成");
$this->releaseLock();
return 0;
}
/**
* 获取锁信息
*
* @return array|null 如果锁存在返回锁信息否则返回null
*/
private function getLock(): ?array
{
$lockKey = md5($this->signature);
return Cache::has($lockKey) ? Cache::get($lockKey) : null;
}
/**
* 设置锁
*/
private function setLock(): void
{
$lockKey = md5($this->signature);
$lockInfo = [
'started_at' => date('Y-m-d H:i:s')
];
Cache::put($lockKey, $lockInfo, 300); // 5分钟
}
/**
* 释放锁
*/
private function releaseLock(): void
{
$lockKey = md5($this->signature);
Cache::forget($lockKey);
}
/**
* 处理终端信号
*
* @param int $signal
* @return void
*/
public function handleSignal(int $signal): void
{
// 释放锁
$this->releaseLock();
exit(0);
}
/**
* 同步消息数据
*
* @return void
*/
private function syncDialogMsgs(): void
{
// 获取上次同步的最后ID
$lastKey = "sync:dialogUserMsgLastId";
$lastId = $this->option('i') ? intval(ZincSearchKeyValue::get($lastKey, 0)) : 0;
if ($lastId > 0) {
$this->info("\n同步消息数据({$lastId}...");
} else {
$this->info("\n同步消息数据...");
}
$num = 0;
$count = WebSocketDialogMsg::where('id', '>', $lastId)->count();
$batchSize = $this->option('batch');
$total = 0;
$lastNum = 0;
do {
// 获取一批
$dialogMsgs = WebSocketDialogMsg::where('id', '>', $lastId)
->orderBy('id')
->limit($batchSize)
->get();
if ($dialogMsgs->isEmpty()) {
break;
}
$num += count($dialogMsgs);
$progress = round($num / $count * 100, 2);
if ($progress < 100) {
$progress = number_format($progress, 2);
}
$this->info("{$num}/{$count} ({$progress}%) 正在同步消息ID {$dialogMsgs->first()->id} ~ {$dialogMsgs->last()->id} ({$total}|{$lastNum})");
// 刷新锁
$this->setLock();
// 同步数据
$lastNum = ZincSearchDialogMsg::batchSync($dialogMsgs);
$total += $lastNum;
// 更新最后ID
$lastId = $dialogMsgs->last()->id;
ZincSearchKeyValue::set($lastKey, $lastId);
} while (count($dialogMsgs) == $batchSize);
$this->info("同步消息结束 - 最后ID {$lastId}");
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Events;
use App\Models\WebSocket;
use App\Services\RequestContext;
use Hhxsv5\LaravelS\Swoole\Events\WorkerStartInterface;
use Swoole\Http\Server;
@@ -25,5 +26,6 @@ class WorkerStartEvent implements WorkerStartInterface
private function handleFirstWorkerTasks()
{
WebSocket::query()->delete();
RequestContext::clearBaseUrlCache();
}
}

View File

@@ -10,13 +10,18 @@ class ApiException extends RuntimeException
*/
protected $data;
/**
* @var bool
*/
protected $writeLog = true;
/**
* ApiException constructor.
* @param string $msg
* @param string|array $msg
* @param array $data
* @param int $code
*/
public function __construct($msg = '', $data = [], $code = 0)
public function __construct($msg = '', $data = [], $code = 0, $writeLog = true)
{
if (is_array($msg) && isset($msg['code'])) {
$code = $msg['code'];
@@ -24,6 +29,7 @@ class ApiException extends RuntimeException
$msg = $msg['msg'];
}
$this->data = $data;
$this->writeLog = $writeLog && $code !== -1;
parent::__construct($msg, $code);
}
@@ -34,4 +40,12 @@ class ApiException extends RuntimeException
{
return $this->data;
}
/**
* @return bool
*/
public function isWriteLog(): bool
{
return $this->writeLog;
}
}

View File

@@ -74,7 +74,7 @@ class Handler extends ExceptionHandler
public function report(Throwable $e)
{
if ($e instanceof ApiException) {
if ($e->getCode() !== -1) {
if ($e->isWriteLog()) {
Log::error($e->getMessage(), [
'code' => $e->getCode(),
'data' => $e->getData(),

View File

@@ -20,6 +20,7 @@ use App\Models\ApproveProcInstHistory;
use App\Exceptions\ApiException;
use App\Models\UserDepartment;
use App\Models\WebSocketDialogMsg;
use App\Module\Apps;
use App\Module\BillMultipleExport;
use Hhxsv5\LaravelS\Swoole\Task\Task;
@@ -34,6 +35,7 @@ class ApproveController extends AbstractController
public function __construct()
{
Apps::isInstalledThrow('approve');
$this->flow_url = env('FLOW_URL') ?: 'http://approve';
}

View File

@@ -14,11 +14,10 @@ use App\Module\Base;
use App\Module\Timer;
use App\Models\Setting;
use App\Module\Extranet;
use App\Module\ElasticSearch\ElasticSearchUserMsg;
use App\Module\TimeRange;
use App\Module\MsgTool;
use App\Module\Table\OnlineData;
use App\Models\FileContent;
use App\Models\ProjectTask;
use App\Models\AbstractModel;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
@@ -28,6 +27,8 @@ use App\Models\WebSocketDialogMsgRead;
use App\Models\WebSocketDialogMsgTodo;
use App\Models\WebSocketDialogMsgTranslate;
use App\Models\WebSocketDialogSession;
use App\Module\Table\OnlineData;
use App\Module\ZincSearch\ZincSearchDialogMsg;
use Hhxsv5\LaravelS\Swoole\Task\Task;
/**
@@ -119,34 +120,11 @@ class DialogController extends AbstractController
return Base::retError('请输入搜索关键词');
}
// 搜索会话
$list = DB::table('web_socket_dialog_users as u')
->select(['d.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
->join('web_socket_dialogs as d', 'u.dialog_id', '=', 'd.id')
->where('u.userid', $user->userid)
->where('d.name', 'LIKE', "%{$key}%")
->whereNull('d.deleted_at')
->orderByDesc('u.top_at')
->orderByDesc('u.last_at')
->take(20)
->get()
->map(function($item) use ($user) {
return WebSocketDialog::synthesizeData($item, $user->userid);
})
->all();
$take = 20;
$list = WebSocketDialog::searchDialog($user->userid, $key, $take);
// 搜索联系人
if (count($list) < 20 && Base::judgeClientVersion("0.21.60")) {
$users = User::select(User::$basicField)
->where(function ($query) use ($key) {
if (str_contains($key, "@")) {
$query->where("email", "like", "%{$key}%");
} else {
$query->where("nickname", "like", "%{$key}%")
->orWhere("pinyin", "like", "%{$key}%")
->orWhere("profession", "like", "%{$key}%");
}
})->orderBy('userid')
->take(20 - count($list))
->get();
if (count($list) < $take && Base::judgeClientVersion("0.21.60")) {
$users = User::searchUser($key, $take - count($list));
$users->transform(function (User $item) use ($user) {
$id = 'u:' . $item->userid;
$lastAt = null;
@@ -172,8 +150,8 @@ class DialogController extends AbstractController
$list = array_merge($list, $users->toArray());
}
// 搜索消息会话
if (count($list) < 20) {
$searchResults = ElasticSearchUserMsg::searchByKeyword($user->userid, $key, 20 - count($list));
if (count($list) < $take) {
$searchResults = ZincSearchDialogMsg::search($user->userid, $key, 0, $take - count($list));
if ($searchResults) {
foreach ($searchResults as $item) {
if ($dialog = WebSocketDialog::find($item['id'])) {
@@ -241,17 +219,10 @@ class DialogController extends AbstractController
//
$dialog_id = intval(Request::input('dialog_id'));
//
$item = DB::table('web_socket_dialog_users as u')
->select(['d.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
->join('web_socket_dialogs as d', 'u.dialog_id', '=', 'd.id')
->where('u.userid', $user->userid)
->where('d.id', $dialog_id)
->whereNull('d.deleted_at')
->first();
if (empty($item)) {
return Base::retError('不在成员列表内');
}
return Base::retSuccess('success', WebSocketDialog::synthesizeData($item, $user->userid));
$dialog = WebSocketDialog::checkDialog($dialog_id);
$data = WebSocketDialog::synthesizeData($dialog, $user->userid);
//
return Base::retSuccess('success', $data);
}
/**
@@ -678,15 +649,18 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/search 14. 搜索消息位置
* @api {get} api/dialog/msg/search 14. 搜索消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__search
*
* @apiParam {Number} dialog_id 对话ID
* @apiParam {String} key 搜索关键词
* @apiParam {Number} [dialog_id] 对话ID存在则搜索消息在对话的位置
* @apiParam {Number} [take] 搜索数量
* - dialog_id > 0, 默认:200最大:200
* - dialog_id <= 0, 默认:20最大:50
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -694,63 +668,42 @@ class DialogController extends AbstractController
*/
public function msg__search()
{
User::auth();
$user = User::auth();
//
$dialog_id = intval(Request::input('dialog_id'));
$key = trim(Request::input('key'));
$dialogId = intval(Request::input('dialog_id'));
//
if (empty($key)) {
return Base::retError('关键词不能为空');
}
//
WebSocketDialog::checkDialog($dialog_id);
//
$data = WebSocketDialogMsg::whereDialogId($dialog_id)
->where('key', 'LIKE', "%{$key}%")
->take(200)
->pluck('id');
return Base::retSuccess('success', compact('data'));
}
/**
* @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);
if ($dialogId > 0) {
// 搜索位置
WebSocketDialog::checkDialog($dialogId);
//
$data = WebSocketDialogMsg::whereDialogId($dialogId)
->where('key', 'LIKE', "%{$key}%")
->take(Base::getPaginate(200, 200, 'take'))
->pluck('id');
return Base::retSuccess('success', compact('data'));
} else {
// 搜索消息
$list = [];
$searchResults = ZincSearchDialogMsg::search($user->userid, $key, 0, Base::getPaginate(50, 20, 'take'));
if ($searchResults) {
foreach ($searchResults as $item) {
if ($dialog = WebSocketDialog::find($item['id'])) {
$dialog = array_merge($dialog->toArray(), $item);
$list[] = WebSocketDialog::synthesizeData($dialog, $user->userid);
}
}
}
return Base::retSuccess('success', ['data' => $list]);
}
//
return Base::retSuccess('success', [
'data' => $list,
]);
}
/**
* @api {get} api/dialog/msg/one 16. 获取单条消息
* @api {get} api/dialog/msg/one 15. 获取单条消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -779,7 +732,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/dot 17. 聊天消息去除点
* @api {get} api/dialog/msg/dot 16. 聊天消息去除点
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -812,7 +765,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/read 18. 已读聊天消息
* @api {get} api/dialog/msg/read 17. 已读聊天消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -883,7 +836,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/unread 19. 获取未读消息数据
* @api {get} api/dialog/msg/unread 18. 获取未读消息数据
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -926,7 +879,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/checked 20. 设置消息checked
* @api {get} api/dialog/msg/checked 19. 设置消息checked
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -992,7 +945,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/stream 21. 通知成员监听消息
* @api {post} api/dialog/msg/stream 20. 通知成员监听消息
*
* @apiDescription 通知指定会员EventSource监听流动消息
* @apiVersion 1.0.0
@@ -1036,7 +989,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/sendtext 22. 发送消息
* @api {post} api/dialog/msg/sendtext 21. 发送消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1089,11 +1042,11 @@ class DialogController extends AbstractController
$dialogIds = $dialog_ids ? explode(',', $dialog_ids) : [$dialog_id ?: 0];
foreach ($dialogIds as $dialog_id) {
//
WebSocketDialog::checkDialog($dialog_id);
$dialog = WebSocketDialog::checkDialog($dialog_id);
//
if ($update_id > 0) {
$action = $update_mark ? "update-$update_id" : "change-$update_id";
if (!($user->bot || $user->isAdmin())) {
if (!$user->bot && !$dialog->isSelfDialog()) {
Setting::validateMsgLimit('edit', $update_id);
}
} elseif ($reply_id > 0) {
@@ -1181,7 +1134,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/sendnotice 23. 发送通知
* @api {post} api/dialog/msg/sendnotice 22. 发送通知
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1234,7 +1187,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/sendtemplate 24. 发送模板消息
* @api {post} api/dialog/msg/sendtemplate 23. 发送模板消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1303,7 +1256,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/sendrecord 25. 发送语音
* @api {post} api/dialog/msg/sendrecord 24. 发送语音
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1351,7 +1304,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/convertrecord 26. 录音转文字
* @api {post} api/dialog/msg/convertrecord 25. 录音转文字
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1423,7 +1376,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/sendfile 27. 文件上传
* @api {post} api/dialog/msg/sendfile 26. 文件上传
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1455,7 +1408,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/sendfiles 28. 群发文件上传
* @api {post} api/dialog/msg/sendfiles 27. 群发文件上传
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1511,7 +1464,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/sendfileid 29. 通过文件ID发送文件
* @api {get} api/dialog/msg/sendfileid 28. 通过文件ID发送文件
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1550,6 +1503,45 @@ class DialogController extends AbstractController
return WebSocketDialogMsg::sendMsgBatch($user, $userids, $dialogids, $fileMsg);
}
/**
* @api {get} api/dialog/msg/sendtaskid 29. 通过任务ID发送任务
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__sendtaskid
*
* @apiParam {Number} task_id 消息ID
* @apiParam {Array} dialogids 转发给的对话ID
* @apiParam {Array} userids 转发给的成员ID
* @apiParam {String} leave_message 转发留言
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__sendtaskid()
{
$user = User::auth();
//
$task_id = intval(Request::input("task_id"));
$dialogids = Request::input('dialogids');
$userids = Request::input('userids');
$leave_message = Request::input('leave_message');
//
if (empty($dialogids) && empty($userids)) {
return Base::retError("请选择对话或成员");
}
//
$task = ProjectTask::userTask($task_id, null);
$taskMsg = "<p><span class=\"mention task\" data-id=\"{$task_id}\">#{$task->name}</span></p>";
if ($leave_message) {
$taskMsg .= "<p>{$leave_message}</p>";
}
//
return WebSocketDialogMsg::sendMsgBatch($user, $userids, $dialogids, $taskMsg);
}
/**
* @api {post} api/dialog/msg/sendanon 30. 发送匿名消息
*
@@ -1818,7 +1810,9 @@ class DialogController extends AbstractController
if (empty($msg)) {
return Base::retError("消息不存在或已被删除");
}
if (!($user->bot || $user->isAdmin())) {
$dialog = WebSocketDialog::checkDialog($msg->dialog_id);
//
if (!$user->bot && !$dialog->isSelfDialog()) {
Setting::validateMsgLimit('rev', $msg);
}
$msg->withdrawMsg();
@@ -3183,6 +3177,7 @@ class DialogController extends AbstractController
* @apiName session_create
*
* @apiParam {Number} dialog_id 对话ID
* @apiParam {Number} [userid] 用户ID与 dialog_id 二选一userid 优先)
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -3190,11 +3185,16 @@ class DialogController extends AbstractController
*/
public function session__create()
{
User::auth();
$user = User::auth();
//
$dialog_id = intval(Request::input('dialog_id'));
$userid = intval(Request::input('userid'));
//
$dialog = WebSocketDialog::checkDialog($dialog_id);
if ($userid) {
$dialog = WebSocketDialog::checkUserDialog($user, $userid);
} else {
$dialog = WebSocketDialog::checkDialog($dialog_id);
}
//
if ($dialog->type != 'user') {
return Base::retError('当前对话不支持');
@@ -3208,9 +3208,7 @@ class DialogController extends AbstractController
return Base::retError('当前对话不支持');
}
//
$session = WebSocketDialogSession::whereDialogId($dialog->id)
->whereTitle('')
->first();
$session = WebSocketDialogSession::whereDialogId($dialog->id)->whereTitle('')->first();
if ($session) {
$dialog->session_id = $session->id;
$dialog->save();

View File

@@ -503,6 +503,10 @@ class FileController extends AbstractController
return Base::retError('参数错误');
}
//
if ($down == 'no') {
File::isNeedInstallApp($file->type);
}
//
if ($only_update_at == 'yes') {
return Base::retSuccess('success', [
'id' => $file->id,
@@ -577,10 +581,12 @@ class FileController extends AbstractController
$contentArray = Base::json2array($content);
$contentString = $contentArray['xml'];
$file->ext = 'drawio';
File::isNeedInstallApp($file->type);
break;
case 'mind':
$contentString = $content;
$file->ext = 'mind';
File::isNeedInstallApp($file->type);
break;
case 'txt':
case 'code':
@@ -632,6 +638,8 @@ class FileController extends AbstractController
{
User::auth();
//
File::isNeedInstallApp('office');
//
$config = Request::input('config');
$token = \Firebase\JWT\JWT::encode($config, env('APP_KEY') ,'HS256');
return Base::retSuccess('成功', [
@@ -657,6 +665,8 @@ class FileController extends AbstractController
{
$user = User::auth();
//
File::isNeedInstallApp('office');
//
$id = intval(Request::input('id'));
$status = intval(Request::input('status'));
$key = Request::input('key');
@@ -666,7 +676,7 @@ class FileController extends AbstractController
//
if ($status === 2) {
$parse = parse_url($url);
$from = 'http://' . env('APP_IPPR') . '.3' . $parse['path'] . '?' . $parse['query'];
$from = 'http://nginx' . $parse['path'] . '?' . $parse['query'];
$path = 'uploads/file/' . $file->type . '/' . date("Ym") . '/' . $file->id . '/' . $key;
$save = public_path($path);
Base::makeDir(dirname($save));
@@ -777,6 +787,8 @@ class FileController extends AbstractController
//
$file = File::permissionFind($id, $user);
//
File::isNeedInstallApp($file->type);
//
$history = FileContent::whereFid($file->id)->whereId($history_id)->first();
if (empty($history)) {
return Base::retError('历史数据不存在或已被删除');

View File

@@ -1783,6 +1783,13 @@ class ProjectController extends AbstractController
//
ProjectPermission::userTaskPermission(Project::userProject($task->project_id), ProjectPermission::TASK_REMOVE, $task);
//
$task->addLog('删除附件:' . $file->name, [
'file_id' => $file->id,
'name' => $file->name,
'size' => $file->size,
'path' => $file->getRawOriginal('path'),
'thumb' => $file->getRawOriginal('thumb'),
]);
$task->pushMsg('filedelete', $file);
$file->delete();
//
@@ -1824,6 +1831,7 @@ class ProjectController extends AbstractController
'update_at' => Carbon::parse($file->updated_at)->toDateTimeString()
]);
}
File::isNeedInstallApp($file->ext);
//
$data = $file->toArray();
$data['path'] = $file->getRawOriginal('path');
@@ -2417,6 +2425,11 @@ class ProjectController extends AbstractController
* @apiParam {Number} flow_item_id 工作流id
* @apiParam {Array} owner 负责人
* @apiParam {Array} assist 协助人
* @apiParam {String} [completed] 是否已完成
* - 没有 工作流id 时此参数才生效
* - 有值表示已完成
* - 空值表示未完成
* - 不存在不改变状态
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -2433,7 +2446,7 @@ class ProjectController extends AbstractController
$flow_item_id = intval(Request::input('flow_item_id'));
$owner = Request::input('owner', []);
$assist = Request::input('assist', []);
$completeAt = trim(Request::input('complete_at', ''));
$completed = Request::exists('completed') ? (bool)Request::input('completed') : null;
//
$task = ProjectTask::userTask($task_id);
//
@@ -2454,13 +2467,13 @@ class ProjectController extends AbstractController
if (empty($flowItem)) {
return Base::retError('任务状态不存在');
}
} else if (!$flow_item_id && !$completeAt) {
} else {
if (projectFlowItem::whereProjectId($project->id)->count() > 0) {
return Base::retError('请选择移动后状态', [], 102);
}
}
//
$task->moveTask($project_id, $column_id, $flow_item_id, $owner, $assist, $completeAt);
$task->moveTask($project_id, $column_id, $flow_item_id, $owner, $assist, $completed);
//
$data = [];
$mainTask = ProjectTask::userTask($task_id)?->toArray();

View File

@@ -221,7 +221,6 @@ class ReportController extends AbstractController
$report->updateInstance([
"title" => $input["title"],
"type" => $input["type"],
"content" => htmlspecialchars($input["content"]),
]);
} else {
// 生成唯一标识
@@ -235,11 +234,25 @@ class ReportController extends AbstractController
"title" => $input["title"],
"type" => $input["type"],
"userid" => $user->userid,
"content" => htmlspecialchars($input["content"]),
]);
}
$report->save();
// 保存内容
$content = $input["content"];
preg_match_all("/<img\s+src=\"data:image\/(png|jpg|jpeg|webp);base64,(.*?)\"/s", $content, $matchs);
foreach ($matchs[2] as $key => $text) {
$tmpPath = "uploads/report/" . Carbon::parse($report->created_at)->format("Ym") . "/" . $report->id . "/attached/";
Base::makeDir(public_path($tmpPath));
$tmpPath .= md5($text) . "." . $matchs[1][$key];
if (Base::saveContentImage(public_path($tmpPath), base64_decode($text))) {
$paramet = getimagesize(public_path($tmpPath));
$content = str_replace($matchs[0][$key], '<img src="' . Base::fillUrl($tmpPath) . '" original-width="' . $paramet[0] . '" original-height="' . $paramet[1] . '"', $content);
}
}
$report->content = htmlspecialchars($content);
$report->save();
// 删除关联
$report->Receives()->delete();
if ($input["receive_content"]) {
@@ -580,8 +593,9 @@ class ReportController extends AbstractController
return Base::retError("报告不存在或已被删除");
}
$reportTag = count($reportMsgs) > 1 ? 'li' : 'p';
$reportMsgs = array_map(function ($item) use ($reportTag) {
return "<{$reportTag}>{$item}</{$reportTag}>";
$reportAttr = $reportTag === 'li' ? ' data-list="ordered"' : '';
$reportMsgs = array_map(function ($item) use ($reportAttr, $reportTag) {
return "<{$reportTag}{$reportAttr}>{$item}</{$reportTag}>";
}, $reportMsgs);
if ($reportTag === 'li') {
array_unshift($reportMsgs, "<ol>");

View File

@@ -2,8 +2,7 @@
namespace App\Http\Controllers\Api;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Models\UserDevice;
use Request;
use Session;
use Response;
@@ -19,6 +18,7 @@ use LdapRecord\Container;
use App\Module\BillExport;
use Guanguans\Notify\Factory;
use App\Models\UserCheckinRecord;
use App\Module\Apps;
use App\Module\BillMultipleExport;
use LdapRecord\LdapRecordException;
use Guanguans\Notify\Messages\EmailMessage;
@@ -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', 'system_welcome', 'image_compress', 'image_quality', 'image_save_local', 'start_home']
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'temp_account_alias', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'voice2text', 'translation', 'convert_video', 'compress_video', 'e2e_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'all_group_mute', 'all_group_autoin', 'user_private_chat_mute', 'user_group_chat_mute', 'system_alias', 'system_welcome', 'image_compress', 'image_quality', 'image_save_local']
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -70,6 +70,8 @@ class SystemController extends AbstractController
'anon_message',
'voice2text',
'translation',
'convert_video',
'compress_video',
'e2e_message',
'msg_rev_limit',
'msg_edit_limit',
@@ -86,7 +88,6 @@ class SystemController extends AbstractController
'image_compress',
'image_quality',
'image_save_local',
'start_home',
'file_upload_limit',
'unclaimed_task_reminder',
'unclaimed_task_reminder_time',
@@ -136,6 +137,8 @@ class SystemController extends AbstractController
$setting['anon_message'] = $setting['anon_message'] ?: 'open';
$setting['voice2text'] = $setting['voice2text'] ?: 'close';
$setting['translation'] = $setting['translation'] ?: 'close';
$setting['convert_video'] = $setting['convert_video'] ?: 'close';
$setting['compress_video'] = $setting['compress_video'] ?: 'close';
$setting['e2e_message'] = $setting['e2e_message'] ?: 'close';
$setting['msg_rev_limit'] = $setting['msg_rev_limit'] ?: '';
$setting['msg_edit_limit'] = $setting['msg_edit_limit'] ?: '';
@@ -146,11 +149,9 @@ class SystemController extends AbstractController
$setting['all_group_autoin'] = $setting['all_group_autoin'] ?: 'yes';
$setting['user_private_chat_mute'] = $setting['user_private_chat_mute'] ?: 'open';
$setting['user_group_chat_mute'] = $setting['user_group_chat_mute'] ?: 'open';
$setting['start_home'] = $setting['start_home'] ?: 'close';
$setting['file_upload_limit'] = $setting['file_upload_limit'] ?: '';
$setting['unclaimed_task_reminder'] = $setting['unclaimed_task_reminder'] ?: 'close';
$setting['unclaimed_task_reminder_time'] = $setting['unclaimed_task_reminder_time'] ?: '';
$setting['server_closeai'] = env("SERVER_CLOSEAI") ?: 'open';
$setting['server_timezone'] = config('app.timezone');
$setting['server_version'] = Base::getVersion();
//
@@ -301,6 +302,8 @@ class SystemController extends AbstractController
{
User::auth('admin');
//
Apps::isInstalledThrow('ai');
//
$type = trim(Request::input('type'));
$filter = trim(Request::input('filter'));
$setting = Base::setting('aibotSetting');
@@ -450,16 +453,22 @@ class SystemController extends AbstractController
if (!$botUser) {
return Base::retError('创建签到机器人失败');
}
if (in_array('locat', $all['modes'])) {
if (empty($all['locat_bd_lbs_key'])) {
return Base::retError('请填写百度地图AK');
if (is_array($all['modes'])) {
if (in_array('locat', $all['modes'])) {
if (empty($all['locat_bd_lbs_key'])) {
return Base::retError('请填写百度地图AK');
}
if (!is_array($all['locat_bd_lbs_point'])) {
return Base::retError('请选择允许签到位置');
}
$all['locat_bd_lbs_point']['radius'] = intval($all['locat_bd_lbs_point']['radius']);
if (empty($all['locat_bd_lbs_point']['lng']) || empty($all['locat_bd_lbs_point']['lat']) || empty($all['locat_bd_lbs_point']['radius'])) {
return Base::retError('请选择有效的签到位置');
}
}
if (!is_array($all['locat_bd_lbs_point'])) {
return Base::retError('请选择允许签到位置');
}
$all['locat_bd_lbs_point']['radius'] = intval($all['locat_bd_lbs_point']['radius']);
if (empty($all['locat_bd_lbs_point']['lng']) || empty($all['locat_bd_lbs_point']['lat']) || empty($all['locat_bd_lbs_point']['radius'])) {
return Base::retError('请选择有效的签到位置');
// 人脸识别
if (in_array('face', $all['modes'])) {
Apps::isInstalledThrow('face');
}
}
}
@@ -809,6 +818,7 @@ class SystemController extends AbstractController
'info' => Doo::license(),
'macs' => Doo::macs(),
'doo_sn' => Doo::dooSN(),
'doo_version' => Doo::dooVersion(),
'user_count' => User::whereBot(0)->whereNull('disable_at')->count(),
'error' => []
];
@@ -817,7 +827,7 @@ class SystemController extends AbstractController
if ($data['info']['sn'] != $data['doo_sn']) {
$data['error'][] = '终端SN与License不匹配';
}
if ($data['info']['mac']) {
if ($data['info']['mac'] && $data['macs']) {
$approved = false;
foreach ($data['info']['mac'] as $mac) {
if (in_array($mac, $data['macs'])) {
@@ -951,7 +961,7 @@ class SystemController extends AbstractController
*
* @apiParam {File} image post-图片对象
* @apiParam {String} [image64] post-图片base64与'image'二选一)
* @apiParam {String} filename post-文件名
* @apiParam {String} [filename] post-文件名
* @apiParam {Number} [width] 压缩图片宽默认0
* @apiParam {Number} [height] 压缩图片高默认0
* @apiParam {String} [whcut] 压缩方式(等比缩放)
@@ -1112,9 +1122,9 @@ class SystemController extends AbstractController
* @apiGroup system
* @apiName fileupload
*
* @apiParam {String} [image64] 图片base64
* @apiParam {String} filename 文件名
* @apiParam {String} [files] 文件名
* @apiParam {File} files 文件名
* @apiParam {String} [image64] 图片base64与'files'二选一)
* @apiParam {String} [filename] 文件名
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -1441,23 +1451,29 @@ class SystemController extends AbstractController
*
* @apiSuccessExample {json} Success-Response:
{
"version": "0.0.1",
"device_count": 3, // 设备数量
"version": "0.0.1", // 服务端版本号
"publish": {
"provider": "generic",
"url": ""
}
}
// 如果header请求中存在version字段则返回数据包裹在 {ret:1,data:{},msg:"success"} 中
*/
public function version()
{
$url = url('');
$package = Base::getPackage();
$array = [
'device_count' => 0,
'version' => Base::getVersion(),
'publish' => [],
];
if (Doo::userId()) {
$array['device_count'] = UserDevice::whereUserid(Doo::userId())->count();
}
if (is_array($package['app'])) {
$i = 0;
$url = url('');
foreach ($package['app'] as $item) {
$urls = $item['urls'] && is_array($item['urls']) ? $item['urls'] : $item['url'];
if (is_array($item['publish']) && ($i === 0 || Base::hostContrast($url, $urls))) {
@@ -1466,6 +1482,9 @@ class SystemController extends AbstractController
$i++;
}
}
if (Request::hasHeader('version')) {
return Base::retSuccess('success', $array);
}
return $array;
}
@@ -1510,11 +1529,13 @@ class SystemController extends AbstractController
}
// 添加office资源
$officePath = '';
$officeApi = 'http://' . env('APP_IPPR') . '.6/web-apps/apps/api/documents/api.js';
$content = @file_get_contents($officeApi);
if ($content) {
if (preg_match("/const\s+ver\s*=\s*'\/*([^']+)'/", $content, $matches)) {
$officePath = $matches[1];
if (Apps::isInstalled('office')) {
$officeApi = 'http://office/web-apps/apps/api/documents/api.js';
$content = @file_get_contents($officeApi);
if ($content) {
if (preg_match("/const\s+ver\s*=\s*'\/*([^']+)'/", $content, $matches)) {
$officePath = $matches[1];
}
}
}
if ($officePath) {
@@ -1529,6 +1550,18 @@ class SystemController extends AbstractController
return !str_starts_with($item, 'office/{path}/');
});
}
// 添加OKR资源
if (Apps::isInstalled('okr')) {
$okrContent = @file_get_contents("http://nginx/apps/okr/");
preg_match_all('/<script[^>]*src=["\']([^"\']+)["\'][^>]*>/i', $okrContent, $scriptMatches);
foreach ($scriptMatches[1] as $src) {
$array[] = $src;
}
preg_match_all('/<link[^>]*rel=["\']stylesheet["\'][^>]*href=["\']([^"\']+)["\'][^>]*>/i', $okrContent, $linkMatches);
foreach ($linkMatches[1] as $href) {
$array[] = $href;
}
}
}
return array_map(function($item) use ($version) {

View File

@@ -19,6 +19,7 @@ use App\Models\UserBot;
use App\Models\WebSocket;
use App\Models\UmengAlias;
use App\Models\UserDelete;
use App\Models\UserDevice;
use App\Models\UserTransfer;
use App\Models\AbstractModel;
use App\Models\UserCheckinFace;
@@ -154,7 +155,7 @@ class UsersController extends AbstractController
//
if (!Project::withTrashed()->whereUserid($user->userid)->wherePersonal(1)->exists()) {
Project::createProject([
'name' => Doo::translate('个人项目'),
'name' => "📝 " . Doo::translate('个人项目'),
'desc' => Doo::translate('注册时系统自动创建项目,你可以自由删除。'),
'personal' => 1,
], $user->userid);
@@ -267,7 +268,23 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/reg/needinvite 06. 是否需要邀请码
* @api {get} api/users/logout 06. 退出登录
*
* @apiVersion 1.0.0
* @apiGroup users
* @apiName logout
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
*/
public function logout()
{
UserDevice::forget();
return Base::retSuccess('退出成功');
}
/**
* @api {get} api/users/reg/needinvite 07. 是否需要邀请码
*
* @apiDescription 用于判断注册是否需要邀请码
* @apiVersion 1.0.0
@@ -286,7 +303,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/info 07. 获取我的信息
* @api {get} api/users/info 08. 获取我的信息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -336,7 +353,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/editdata 08. 修改自己的资料
* @api {get} api/users/editdata 09. 修改自己的资料
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -428,7 +445,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/editpass 09. 修改自己的密码
* @api {get} api/users/editpass 10. 修改自己的密码
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -469,7 +486,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/search 10. 搜索会员列表
* @api {get} api/users/search 11. 搜索会员列表
*
* @apiDescription 搜索会员列表
* @apiVersion 1.0.0
@@ -611,7 +628,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/search/ai 11. 获取AI机器人
* @api {get} api/users/search/ai 12. 获取AI机器人
*
* @apiDescription 搜索会员列表
* @apiVersion 1.0.0
@@ -630,7 +647,7 @@ class UsersController extends AbstractController
//
$type = trim(Request::input('type'));
$botName = "ai-{$type}";
if (!UserBot::isAiBot("{$botName}@bot.system")) {
if (!UserBot::systemBotName($botName)) {
return Base::retError('AI机器人不存在');
}
//
@@ -642,7 +659,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/basic 12. 获取指定会员基础信息
* @api {get} api/users/basic 13. 获取指定会员基础信息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -685,7 +702,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/lists 13. 会员列表(限管理员)
* @api {get} api/users/lists 14. 会员列表(限管理员)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -834,7 +851,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/operation 14. 操作会员(限管理员)
* @api {get} api/users/operation 15. 操作会员(限管理员)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -925,8 +942,7 @@ class UsersController extends AbstractController
return UserCheckinMac::saveMac($userInfo->userid, $array);
case 'checkin_face':
$faceimg = $data['checkin_face'] ? $data['checkin_face'] : '';
$faceimg = $data['checkin_face'] ?: '';
return UserCheckinFace::saveFace($userInfo->userid, $userInfo->nickname, $faceimg, "管理员上传");
case 'department':
@@ -1092,7 +1108,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/email/verification 15. 邮箱验证
* @api {get} api/users/email/verification 16. 邮箱验证
*
* @apiDescription 不需要token身份
* @apiVersion 1.0.0
@@ -1140,7 +1156,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/umeng/alias 16. 设置友盟别名
* @api {get} api/users/umeng/alias 17. 设置友盟别名
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1154,6 +1170,7 @@ class UsersController extends AbstractController
* @apiParam {String} [userAgent] 浏览器信息
* @apiParam {String} [deviceModel] 设备型号
* @apiParam {String} [isNotified] 是否有通知权限0不通知、1通知
* @apiParam {Number} [isDebug] 是否调试0不调试、1调试
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -1162,6 +1179,10 @@ class UsersController extends AbstractController
public function umeng__alias()
{
$data = Request::input();
// 判断是否调试
if (intval($data['isDebug'])) {
return Base::retError('调试模式下不允许使用');
}
// 表单验证
Base::validator($data, [
'alias.required' => '别名不能为空',
@@ -1192,6 +1213,7 @@ class UsersController extends AbstractController
$row->update([
'ua' => $data['userAgent'],
'device' => $data['deviceModel'],
'device_hash' => UserDevice::check(),
'version' => $version,
'is_notified' => $isNotified,
'updated_at' => Carbon::now()
@@ -1201,6 +1223,7 @@ class UsersController extends AbstractController
$row = UmengAlias::createInstance(array_merge($inArray, [
'ua' => $data['userAgent'],
'device' => $data['deviceModel'],
'device_hash' => UserDevice::check(),
'version' => $version,
'is_notified' => $isNotified,
]));
@@ -1212,7 +1235,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/meeting/open 17. 【会议】创建会议、加入会议
* @api {get} api/users/meeting/open 18. 【会议】创建会议、加入会议
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1330,7 +1353,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/meeting/link 18. 【会议】获取分享链接
* @api {get} api/users/meeting/link 19. 【会议】获取分享链接
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1359,7 +1382,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/meeting/tourist 19. 【会议】游客信息
* @api {get} api/users/meeting/tourist 20. 【会议】游客信息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1382,7 +1405,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/meeting/invitation 20. 【会议】发送邀请
* @api {get} api/users/meeting/invitation 21. 【会议】发送邀请
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1429,7 +1452,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/email/send 21. 发送邮箱验证码
* @api {get} api/users/email/send 22. 发送邮箱验证码
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1469,7 +1492,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/email/edit 22. 修改邮箱
* @api {get} api/users/email/edit 23. 修改邮箱
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1514,7 +1537,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/delete/account 23. 删除帐号
* @api {get} api/users/delete/account 24. 删除帐号
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1576,7 +1599,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/department/list 24. 部门列表(限管理员)
* @api {get} api/users/department/list 25. 部门列表(限管理员)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1595,7 +1618,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/department/add 25. 新建、修改部门(限管理员)
* @api {get} api/users/department/add 26. 新建、修改部门(限管理员)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1681,7 +1704,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/department/del 26. 删除部门(限管理员)
* @api {get} api/users/department/del 27. 删除部门(限管理员)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1714,7 +1737,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/checkin/get 27. 获取签到设置
* @api {get} api/users/checkin/get 28. 获取签到设置
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1741,7 +1764,7 @@ class UsersController extends AbstractController
}
/**
* @api {post} api/users/checkin/save 28. 保存签到设置
* @api {post} api/users/checkin/save 29. 保存签到设置
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1816,7 +1839,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/checkin/list 29. 获取签到数据
* @api {get} api/users/checkin/list 30. 获取签到数据
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1863,7 +1886,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/socket/status 30. 获取socket状态
* @api {get} api/users/socket/status 31. 获取socket状态
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1886,7 +1909,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/key/client 31. 客户端KEY
* @api {get} api/users/key/client 32. 客户端KEY
*
* @apiDescription 获取客户端KEY用于加密数据发送给服务端
* @apiVersion 1.0.0
@@ -1928,7 +1951,51 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/bot/info 32. 机器人信息
* @api {get} api/users/bot/list 33. 机器人列表
*
* @apiDescription 需要token身份获取我的机器人列表
* @apiVersion 1.0.0
* @apiGroup users
* @apiName bot__list
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function bot__list()
{
// 获取当前认证用户
$user = User::auth();
// 使用连表查询一次性获取所有机器人数据
$bots = User::join('user_bots', 'user_bots.bot_id', '=', 'users.userid')
->where('user_bots.userid', $user->userid)
->select([
'users.userid',
'users.nickname',
'users.userimg',
'user_bots.clear_day',
'user_bots.webhook_url'
])
->orderByDesc('id')
->get()
->toArray();
foreach ($bots as &$bot) {
$bot['id'] = $bot['userid'];
$bot['name'] = $bot['nickname'];
$bot['avatar'] = $bot['userimg'];
$bot['system_name'] = UserBot::systemBotName($bot['name']);
unset($bot['userid'], $bot['nickname'], $bot['userimg']);
}
// 返回成功响应将机器人列表包装在list字段中
return Base::retSuccess('success', [
'list' => $bots
]);
}
/**
* @api {get} api/users/bot/info 34. 机器人信息
*
* @apiDescription 需要token身份获取我的机器人信息
* @apiVersion 1.0.0
@@ -1979,14 +2046,14 @@ class UsersController extends AbstractController
}
/**
* @api {post} api/users/bot/edit 33. 编辑机器人
* @api {post} api/users/bot/edit 35. 添加、编辑机器人
*
* @apiDescription 需要token身份编辑 我的机器人 或 管理员修改系统机器人 信息
* @apiVersion 1.0.0
* @apiGroup users
* @apiName bot__edit
*
* @apiParam {Number} id 机器人ID
* @apiParam {Number} [id] 机器人ID(编辑时必填,留空为添加)
* @apiParam {String} [name] 机器人名称
* @apiParam {String} [avatar] 机器人头像
* @apiParam {Number} [clear_day] 清理天数(仅 我的机器人)
@@ -2001,10 +2068,19 @@ class UsersController extends AbstractController
$user = User::auth();
//
$botId = intval(Request::input('id'));
$botUser = User::whereUserid($botId)->whereBot(1)->first();
if (empty($botUser)) {
return Base::retError('机器人不存在');
if (empty($botId)) {
$res = UserBot::newbot($user->userid, trim(Request::input('name')));
if (Base::isError($res)) {
return $res;
}
$botUser = $res['data'];
} else {
$botUser = User::whereUserid($botId)->whereBot(1)->first();
if (empty($botUser)) {
return Base::retError('机器人不存在');
}
}
//
$userBot = UserBot::whereBotId($botUser->userid)->whereUserid($user->userid)->first();
if (empty($userBot)) {
if (UserBot::systemBotName($botUser->email)) {
@@ -2061,17 +2137,68 @@ class UsersController extends AbstractController
$data['clear_day'] = $userBot->clear_day;
$data['webhook_url'] = $userBot->webhook_url;
}
return Base::retSuccess('修改成功', $data);
return Base::retSuccess($botId ? '修改成功' : '添加成功', $data);
}
/**
* @api {get} api/users/share/list 34. 获取分享列表
* @api {get} api/users/bot/delete 36. 删除机器人
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup users
* @apiName bot__delete
*
* @apiParam {Number} id 机器人ID
* @apiParam {String} remark 删除备注
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function bot__delete()
{
$user = User::auth();
//
$botId = intval(Request::input('id'));
$remark = trim(Request::input('remark'));
//
if (empty($remark)) {
return Base::retError('请输入删除备注');
}
if (mb_strlen($remark) > 255) {
return Base::retError('删除备注长度限制255个字');
}
//
$botUser = User::whereUserid($botId)->whereBot(1)->first();
if (empty($botUser)) {
return Base::retError('机器人不存在');
}
$userBot = UserBot::whereBotId($botUser->userid)->whereUserid($user->userid)->first();
if (empty($userBot)) {
if (UserBot::systemBotName($botUser->email)) {
// 系统机器人(仅限管理员)
return Base::retError('系统机器人不能删除');
} else {
// 其他用户的机器人(仅限主人)
return Base::retError('不是你的机器人');
}
}
//
if (!$botUser->deleteUser($remark)) {
return Base::retError('删除失败');
}
return Base::retSuccess('删除成功');
}
/**
* @api {get} api/users/share/list 37. 获取分享列表
*
* @apiVersion 1.0.0
* @apiGroup users
* @apiName share__list
*
* @apiParam {String} [type] 分享类型file-文件text-列表 默认file
* @apiParam {String} [key] 搜索关键词(用于搜索会话)
* @apiParam {Number} [pid] 父级文件id用于获取子目录和上传到指定目录的id
* @apiParam {Number} [upload_file_id] 上传文件id
*
@@ -2083,6 +2210,7 @@ class UsersController extends AbstractController
{
$user = User::auth();
$type = Request::input('type', 'file');
$key = Request::input('key');
$pid = intval(Request::input('pid', -1));
$uploadFileId = intval(Request::input('upload_file_id', -1));
// 上传文件
@@ -2115,10 +2243,14 @@ class UsersController extends AbstractController
'icon' => url("images/file/light/folder.png"),
'extend' => ['upload_file_id' => 0],
'name' => Doo::translate('文件'),
'sort' => Carbon::parse("9999")->timestamp,
];
}
$dialogList = WebSocketDialog::getDialogList($user->userid);
foreach ($dialogList['data'] as $dialog) {
$dialogTake = 50;
$dialogList = WebSocketDialog::searchDialog($user->userid, $key, $dialogTake);
$dialogIds = [];
$itemUrl = $type == "file" ? Base::fillUrl("api/dialog/msg/sendfiles") : Base::fillUrl("api/dialog/msg/sendtext");
foreach ($dialogList as $dialog) {
if ($dialog['avatar']) {
$avatar = url($dialog['avatar']);
} else if ($dialog['type'] == 'user') {
@@ -2135,7 +2267,8 @@ class UsersController extends AbstractController
'type' => 'item',
'name' => $dialog['name'],
'icon' => $avatar,
'url' => $type == "file" ? Base::fillUrl("api/dialog/msg/sendfiles") : Base::fillUrl("api/dialog/msg/sendtext"),
'url' => $itemUrl,
'sort' => Carbon::parse($dialog['last_at'])->timestamp,
'extend' => [
'dialog_ids' => $dialog['id'],
'text_type' => 'text',
@@ -2143,6 +2276,33 @@ class UsersController extends AbstractController
'silence' => 'no'
]
];
$dialogIds[] = $dialog['id'];
}
if ($key && count($dialogList) < $dialogTake) {
$dialogUsers = User::searchUser($key, $dialogTake - count($dialogList));
foreach ($dialogUsers as $item) {
$dialog = WebSocketDialog::getUserDialog($user->userid, $item->userid, now()->addDay());
if ($dialog && !in_array($dialog->id, $dialogIds)) {
$lists[] = [
'type' => 'item',
'name' => $item->nickname,
'icon' => $item->userimg,
'url' => $itemUrl,
'sort' => Carbon::parse($item->line_at)->timestamp,
'extend' => [
'dialog_ids' => $dialog->id,
'text_type' => 'text',
'reply_id' => 0,
'silence' => 'no'
]
];
$dialogIds[] = $dialog->id;
}
}
// 根据 $lists sort 从大到小排序
usort($lists, function ($a, $b) {
return $b['sort'] <=> $a['sort'];
});
}
}
// 返回
@@ -2150,7 +2310,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/annual/report 35. 年度报告
* @api {get} api/users/annual/report 38. 年度报告
*
* @apiVersion 1.0.0
* @apiGroup users
@@ -2317,4 +2477,97 @@ class UsersController extends AbstractController
//
return Base::retSuccess('success', $data);
}
/**
* @api {get} api/users/device/list 39. 获取设备列表
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup users
* @apiName device__list
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function device__list()
{
$user = User::auth();
//
$list = UserDevice::whereUserid($user->userid)->orderByDesc('id')->take(UserDevice::$deviceLimit)->get();
//
return Base::retSuccess('success', [
'list' => $list
]);
}
/**
* @api {get} api/users/device/logout 40. 登出设备(删除设备)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup users
* @apiName device__logout
*
* @apiParam {Number} id 设备id
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function device__logout()
{
$user = User::auth();
//
$id = intval(Request::input('id'));
if (empty($id)) {
return Base::retError('参数错误');
}
$userDevice = UserDevice::whereUserid($user->userid)->whereId($id)->first();
if (empty($userDevice)) {
return Base::retError('设备不存在或已被删除');
}
UserDevice::forget($userDevice->id);
//
return Base::retSuccess('操作成功');
}
/**
* @api {get} api/users/device/edit 41. 编辑设备
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup users
* @apiName device__edit
*
* @apiParam {Object} detail 设备信息
* @apiParam {String} detail.device_name 设备名称
* @apiParam {String} detail.app_brand 设备品牌
* @apiParam {String} detail.app_model 设备型号
* @apiParam {String} detail.app_os 设备操作系统
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function device__edit()
{
User::auth();
//
$detail = Request::input();
$detail = array_intersect_key($detail, array_flip([ 'device_name', 'app_brand', 'app_model','app_os']));
if (empty($detail)) {
return Base::retError('参数错误');
}
//
$row = UserDevice::record();
if (empty($row)) {
return Base::retError('设备不存在或已被删除');
}
$deviceInfo = array_merge(Base::json2array($row->detail), $detail);
$row->detail = Base::array2json($deviceInfo);
$row->save();
//
return Base::retSuccess('保存成功');
}
}

View File

@@ -8,8 +8,6 @@ use Request;
use Redirect;
use Response;
use App\Models\File;
use App\Models\User;
use App\Models\UserTransfer;
use App\Module\Doo;
use App\Module\Base;
use App\Module\Extranet;
@@ -23,7 +21,7 @@ use App\Tasks\AutoArchivedTask;
use App\Tasks\DeleteBotMsgTask;
use App\Tasks\CheckinRemindTask;
use App\Tasks\CloseMeetingRoomTask;
use App\Tasks\ElasticSearchSyncTask;
use App\Tasks\ZincSearchSyncTask;
use App\Tasks\UnclaimedTaskRemindTask;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Laravolt\Avatar\Avatar;
@@ -86,6 +84,15 @@ class IndexController extends InvokeController
return Redirect::to(Base::fillUrl('api/system/version'), 301);
}
/**
* 健康检查
* @return string
*/
public function health()
{
return "ok";
}
/**
* 头像
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response|\Symfony\Component\HttpFoundation\BinaryFileResponse
@@ -242,11 +249,12 @@ class IndexController extends InvokeController
// App推送
Task::deliver(new AppPushTask());
// 删除过期的临时表数据
Task::deliver(new DeleteTmpTask('wg_tmp_msgs', 1));
Task::deliver(new DeleteTmpTask('task_worker', 12));
Task::deliver(new DeleteTmpTask('tmp_msgs', 1));
Task::deliver(new DeleteTmpTask('tmp'));
Task::deliver(new DeleteTmpTask('task_worker', 12));
Task::deliver(new DeleteTmpTask('file'));
Task::deliver(new DeleteTmpTask('tmp_file', 24));
Task::deliver(new DeleteTmpTask('user_device', 24));
// 删除机器人消息
Task::deliver(new DeleteBotMsgTask());
// 周期任务
@@ -259,8 +267,8 @@ class IndexController extends InvokeController
Task::deliver(new UnclaimedTaskRemindTask());
// 关闭会议室
Task::deliver(new CloseMeetingRoomTask());
// ElasticSearch 同步
Task::deliver(new ElasticSearchSyncTask());
// ZincSearch 同步
Task::deliver(new ZincSearchSyncTask());
return "success";
}
@@ -324,7 +332,7 @@ class IndexController extends InvokeController
"file" => Request::file('file'),
"type" => 'publish',
"path" => $draftPath,
"fileName" => true,
"saveName" => true,
]);
}
@@ -471,7 +479,7 @@ class IndexController extends InvokeController
action: "eeuiAppSendMessage",
data: [
{
action: 'setPageData',
action: 'setPageData', // 设置页面数据
data: {
showProgress: true,
titleFixed: true,
@@ -479,7 +487,7 @@ class IndexController extends InvokeController
}
},
{
action: 'createTarget',
action: 'createTarget', // 创建目标(访问新地址)
url: "{$redirectUrl}",
}
]
@@ -493,7 +501,7 @@ class IndexController extends InvokeController
if (in_array($ext, File::localExt)) {
$url = Base::fillUrl($path);
} else {
$url = 'http://' . env('APP_IPPR') . '.3/' . $path;
$url = 'http://nginx/' . $path;
}
$url = Base::urlAddparameter($url, [
'fullfilename' => Base::rightDelete($name, '.' . $ext) . '_' . filemtime($file) . '.' . $ext
@@ -501,43 +509,4 @@ class IndexController extends InvokeController
$redirectUrl = Base::fillUrl("fileview/onlinePreview?url=" . urlencode(base64_encode($url)));
return Redirect::to($redirectUrl, 301);
}
/**
* 修复操作离职后续操作(todo 临时,后期删除)
* @return array
*/
public function migration__userdialog()
{
if (Request::header('app-key') !== env('APP_KEY')) {
return Base::retError("key error");
}
go(function() {
Coroutine::sleep(3);
$handled = [];
UserTransfer::orderBy('id')->chunkById(10, function ($transfers) use ($handled) {
/** @var UserTransfer $transfer */
foreach ($transfers as $transfer) {
if (in_array($transfer->original_userid, $handled)) {
continue;
}
$handled[] = $transfer->original_userid;
//
$user = User::find($transfer->original_userid);
if ($user?->isDisable()) {
$transfer->exitDialog();
}
}
});
});
return Base::retSuccess('success');
}
/**
* 保存配置 (todo 已废弃)
* @return string
*/
public function storage__synch()
{
return '<!-- Deprecated -->';
}
}

View File

@@ -24,6 +24,9 @@ class WebApi
RequestContext::set('start_time', microtime(true));
RequestContext::set('header_language', $request->header('language'));
// 更新请求的基本URL
RequestContext::updateBaseUrl($request);
// 加载Doo类
Doo::load();

View File

@@ -151,6 +151,25 @@ class AbstractModel extends Model
return $date->format($this->dateFormat ?: 'Y-m-d H:i:s');
}
/**
* 通过模型创建实例
* @param array $param
* @param bool $force
* @return static
*/
public static function fillInstance(array $param = [], bool $force = true)
{
$instance = new static;
if ($param) {
if ($force) {
$instance->forceFill($param);
} else {
$instance->fill($param);
}
}
return $instance;
}
/**
* 创建/更新数据
* @param array $param
@@ -209,24 +228,35 @@ class AbstractModel extends Model
/**
* 数据库更新或插入
* @param $where
* @param array|\Closure $update 存在时更新的内容
* @param array|\Closure $insert 不存在时插入的内容,如果没有则插入更新内容
* @param bool $isInsert 是否是插入数据
* @param array $where 查询条件
* @param array|\Closure $update 存在时更新的内容
* @param array|\Closure $insert 不存在时插入的内容,如果没有则插入更新内容
* @param bool $isInsert 是否是插入数据
* @param bool|null $lockForUpdate 是否加锁true:加锁false:不加锁null:在事务中会自动加锁)
* @return AbstractModel|\Illuminate\Database\Eloquent\Builder|Model|object|static|null
*/
public static function updateInsert($where, $update = [], $insert = [], &$isInsert = true)
public static function updateInsert($where, $update = [], $insert = [], &$isInsert = true, $lockForUpdate = null)
{
$row = static::where($where)->first();
$query = static::where($where);
if ($lockForUpdate === null) {
$lockForUpdate = \DB::transactionLevel() > 0;
}
if ($lockForUpdate) {
$query->lockForUpdate();
}
$row = $query->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 (empty($insert)) {
if ($update instanceof \Closure) {
$update = $update();
}
$insert = $update;
}
$array = array_merge($where, $insert);
if (isset($array[$row->primaryKey])) {
unset($array[$row->primaryKey]);
}

View File

@@ -3,6 +3,7 @@
namespace App\Models;
use Request;
use App\Module\Apps;
use App\Module\Base;
use App\Tasks\PushTask;
use App\Exceptions\ApiException;
@@ -117,7 +118,7 @@ class File extends AbstractModel
'jpg', 'jpeg', 'webp', 'png', 'gif', 'bmp', 'ico', 'raw',
'tif', 'tiff',
'mp3', 'wav', 'mp4', 'flv',
'avi', 'mov', 'wmv', 'mkv', '3gp', 'rm',
// 'avi', 'mov', 'wmv', 'mkv', '3gp', 'rm', // 这一排是要转换的,无法使用本地播放
];
/**
@@ -978,4 +979,41 @@ class File extends AbstractModel
];
Task::deliver(new PushTask($params));
}
/**
* 根据文件类型判断是否需要安装应用
* @param $type
* @return void
*/
public static function isNeedInstallApp($type): void
{
// 文件类型与应用的映射配置
$fileTypeAppMapping = [
// Office 应用映射
[
'types' => ['word', 'excel', 'ppt', 'docx', 'xlsx', 'pptx'],
'app_id' => 'office',
'app_name' => 'OnlyOffice'
],
// Drawio 应用映射
[
'types' => ['drawio'],
'app_id' => 'drawio',
'app_name' => 'Drawio'
],
// Minder 应用映射
[
'types' => ['mind'],
'app_id' => 'minder',
'app_name' => 'Minder'
]
];
// 遍历配置检查是否需要安装应用
foreach ($fileTypeAppMapping as $config) {
if (in_array($type, $config['types'])) {
Apps::isInstalledThrow($config['app_id']);
}
}
}
}

View File

@@ -221,6 +221,7 @@ class Project extends AbstractModel
'important' => 1
], function () use ($userid) {
return [
'important' => 1,
'bot' => User::isBot($userid) ? 1 : 0,
];
});

View File

@@ -735,7 +735,9 @@ class ProjectTask extends AbstractModel
if (count($older) == 0 && count($array) == 1 && $array[0] == User::userid()) {
$this->addLog("认领{任务}");
} else {
$this->addLog("修改{任务}负责人", ['userid' => $array]);
if (array_merge(array_diff($array, $older), array_diff($older, $array))) {
$this->addLog("修改{任务}负责人", ['userid' => $array]);
}
}
$this->taskPush(array_values(array_diff($array, $older)), 0);
}
@@ -845,11 +847,12 @@ class ProjectTask extends AbstractModel
$existAt = $this->start_at && !$clearSubTaskTime;
$newStringAt = $existAt ? ($this->start_at->toDateTimeString() . '~' . $this->end_at->toDateTimeString()) : '';
if ($isOverdue) {
$effectiveEndTime = $existAt ? Carbon::parse($this->end_at)->min(Carbon::now()) : Carbon::now();
$this->addLog("{任务}超期未完成", [
'cache' => [
'task_at' => $oldStringAt,
'change_at' => $newStringAt,
'over_sec' => ($existAt ? $this->end_at : Carbon::now())->diffInSeconds($oldAt[1]),
'over_sec' => $effectiveEndTime->diffInSeconds($oldAt[1]),
'owners' => $this->taskUser->where('owner', 1)->pluck('userid')->toArray(),
'assists' => $this->taskUser->where('owner', 0)->pluck('userid')->toArray(),
]
@@ -889,6 +892,7 @@ class ProjectTask extends AbstractModel
}
// 协助人员
if (Arr::exists($data, 'assist')) {
$older = $this->taskUser->where('owner', 0)->pluck('userid')->toArray();
$array = [];
$assist = is_array($data['assist']) ? $data['assist'] : [$data['assist']];
if (count($assist) > 10) {
@@ -909,7 +913,9 @@ class ProjectTask extends AbstractModel
$array[] = $uid;
}
if ($array) {
$this->addLog("修改{任务}协助人员", ['userid' => $array]);
if (array_merge(array_diff($array, $older), array_diff($older, $array))) {
$this->addLog("修改{任务}协助人员", ['userid' => $array]);
}
}
$rows = ProjectTaskUser::whereTaskId($this->id)->whereOwner(0)->whereNotIn('userid', $array)->get();
if ($rows->isNotEmpty()) {
@@ -1138,9 +1144,6 @@ class ProjectTask extends AbstractModel
*/
public function copyTask()
{
if ($this->parent_id > 0) {
throw new ApiException('子任务禁止复制');
}
return AbstractModel::transaction(function() {
// 复制任务
$task = $this->replicate();
@@ -1201,6 +1204,7 @@ class ProjectTask extends AbstractModel
'important' => 1
], function () use ($userid) {
return [
'important' => 1,
'bot' => User::isBot($userid) ? 1 : 0,
];
});
@@ -1347,6 +1351,9 @@ class ProjectTask extends AbstractModel
$addMsg = $this->parent_id == 0 && $this->dialog_id > 0;
if ($complete_at === null) {
// 标记未完成
if (!$this->complete_at) {
return; // 本来就未完成
}
$this->complete_at = null;
$this->addLog("标记{任务}未完成");
if ($addMsg) {
@@ -1356,6 +1363,9 @@ class ProjectTask extends AbstractModel
}
} else {
// 标记已完成
if ($this->complete_at) {
return; // 本来就已完成
}
if ($this->parent_id == 0) {
if (self::whereParentId($this->id)->whereCompleteAt(null)->exists()) {
throw new ApiException('子任务未完成', [
@@ -1422,11 +1432,12 @@ class ProjectTask extends AbstractModel
$this->archived_at = null;
$this->archived_userid = User::userid();
$this->archived_follow = 0;
$this->addLog("任务取消归档");
$logText = "任务取消归档";
$userid = 0;
} else {
// 归档任务
if ($isAuto === true) {
$logText = "自动任务归档";
$logText = "任务自动归档";
$userid = 0;
} else {
$logText = "任务归档";
@@ -1435,13 +1446,20 @@ class ProjectTask extends AbstractModel
$this->archived_at = $archived_at;
$this->archived_userid = $userid;
$this->archived_follow = 0;
$this->addLog($logText, [], $userid);
}
// 添加日志
$this->addLog($logText, [], $userid);
// 推送状态
$this->pushMsg($archived_at === null ? 'recovery' : 'archived', [
'id' => $this->id,
'archived_at' => $this->archived_at,
'archived_userid' => $this->archived_userid,
]);
// 更新对话时间
if ($this->dialog_id > 0) {
WebSocketDialogUser::whereDialogId($this->dialog_id)->update(['updated_at' => Carbon::now()]); // 因为是若提醒,可以直接使用 update 更新
}
// 更新保存
self::whereParentId($this->id)->change([
'archived_at' => $this->archived_at,
'archived_userid' => $this->archived_userid,
@@ -1830,16 +1848,30 @@ class ProjectTask extends AbstractModel
* @param int $flowItemId
* @param array $owner
* @param array $assist
* @param string $completeAt
* @param string|null $completed
* @return bool
*/
public function moveTask(int $projectId, int $columnId,int $flowItemId = 0,array $owner = [], array $assist = [], string $completeAt='')
public function moveTask(int $projectId, int $columnId, int $flowItemId = 0, array $owner = [], array $assist = [], ?string $completed = null)
{
AbstractModel::transaction(function () use ($projectId, $columnId, $flowItemId, $owner, $assist, $completeAt) {
AbstractModel::transaction(function () use ($projectId, $columnId, $flowItemId, $owner, $assist, $completed) {
$newTaskUser = array_merge($owner, $assist);
//
$oldProject = Project::find($this->project_id);
$newProject = $this->project_id != $projectId ? Project::find($projectId) : $oldProject;
if (!$oldProject || !$newProject) {
throw new ApiException('项目不存在');
}
//
$this->project_id = $projectId;
$this->column_id = $columnId;
// 日志
$log = $this->addLog("移动{任务}", [
'change' => [$oldProject->name, $newProject->name]
]);
if ($this->dialog_id) {
$notice = $oldProject->id != $newProject->id ? "{$oldProject->name}」移动至「{$newProject->name}" : $log->detail;
WebSocketDialogMsg::sendMsg(null, $this->dialog_id, 'notice', ['notice' => $notice], User::userid(), true, true);
}
// 任务内容
if ($this->content) {
$this->content->project_id = $projectId;
@@ -1873,6 +1905,7 @@ class ProjectTask extends AbstractModel
]);
//
if ($flowItemId) {
// 更新任务流程
$flowItem = projectFlowItem::whereProjectId($projectId)->whereId($flowItemId)->first();
$this->flow_item_id = $flowItemId;
$this->flow_item_name = $flowItem->status . "|" . $flowItem->name;
@@ -1882,17 +1915,15 @@ class ProjectTask extends AbstractModel
$this->completeTask(null);
}
} else {
// 没有流程只更新状态
$this->flow_item_id = 0;
$this->flow_item_name = '';
}
//
if ($completeAt) {
$this->complete_at = $completeAt;
if ($completed !== null) {
$this->completeTask($completed ? Carbon::now(): null);
}
}
//
$this->save();
//
$this->addLog("移动{任务}");
});
$this->pushMsg('update');
return true;

View File

@@ -124,50 +124,49 @@ class Setting extends AbstractModel
{
return match ($ai) {
'openai' => [
'gpt-4 | GPT-4',
'gpt-4-turbo | GPT-4 Turbo',
'gpt-4.1 | GPT-4.1',
'gpt-4o | GPT-4o',
'gpt-4 | GPT-4',
'gpt-4o-mini | GPT-4o Mini',
'gpt-4-turbo | GPT-4 Turbo',
'o3 (thinking) | GPT-o3',
'o1 | GPT-o1',
'o1-mini | GPT-o1 Mini',
'o4-mini | GPT-o4 Mini',
'o3-mini | GPT-o3 Mini',
'o1-mini | GPT-o1 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'
'claude-opus-4-0 (thinking) | Claude Opus 4',
'claude-sonnet-4-0 (thinking) | Claude Sonnet 4',
'claude-3-7-sonnet-latest (thinking) | Claude Sonnet 3.7',
'claude-3-5-sonnet-latest | Claude Sonnet 3.5',
'claude-3-5-haiku-latest | Claude Haiku 3.5',
'claude-3-opus-latest | Claude Opus 3'
],
'deepseek' => [
'deepseek-chat | DeepSeek V3',
'deepseek-reasoner | DeepSeek R1'
],
'gemini' => [
'gemini-2.5-pro-preview-05-06 (thinking) | Gemini 2.5 Pro Preview',
'gemini-2.0-flash | Gemini 2.0 Flash',
'gemini-2.0-flash-lite-preview-02-05 | Gemini 2.0 Flash-Lite Preview',
'gemini-2.0-flash-lite | Gemini 2.0 Flash-Lite',
'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',
'grok-3-latest | Grok 3',
'grok-3-fast-latest | Grok 3 Fast',
'grok-3-mini-latest | Grok 3 Mini',
'grok-3-mini-fast-latest | Grok 3 Mini Fast',
'grok-2-vision-latest | Grok 2 Vision',
'grok-2-latest | Grok 2',
],
'zhipu' => [
'glm-4 | GLM-4',

View File

@@ -15,6 +15,7 @@ use Hedeqiang\UMeng\IOS;
* @property string|null $alias 别名
* @property string|null $platform 平台类型
* @property string|null $device 设备类型
* @property string|null $device_hash 设备哈希值用于关联UserDevice表
* @property string|null $version 应用版本号
* @property string|null $ua userAgent
* @property int|null $is_notified 通知权限
@@ -32,6 +33,7 @@ use Hedeqiang\UMeng\IOS;
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereAlias($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereDevice($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereDeviceHash($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereIsNotified($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias wherePlatform($value)
@@ -45,6 +47,54 @@ class UmengAlias extends AbstractModel
{
protected $table = 'umeng_alias';
private static $waitSend = [];
/**
* 推送消息
* @param $push
* @return void
*/
private static function sendTask($push = null)
{
if ($push) {
self::$waitSend[] = $push;
}
if (!self::$waitSend) {
return;
}
$first = array_shift(self::$waitSend);
if (empty($first)) {
return;
}
try {
switch ($first['platform']) {
case 'ios':
$instance = new IOS($first['config']);
break;
case 'android':
$instance = new Android($first['config']);
break;
default:
return;
}
$instance->send($first['data']);
} catch (\Exception $e) {
$first['retry'] = intval($first['retry'] ?? 0) + 1;
if ($first['retry'] > 3) {
info("[PushMsg] fail: " . $e->getMessage());
} else {
info("[PushMsg] retry ({$first['retry']}): " . $e->getMessage());
self::$waitSend[] = $first;
}
} finally {
self::sendTask();
}
}
/**
* 推送内容处理
* @param $string
@@ -88,13 +138,13 @@ class UmengAlias extends AbstractModel
* @param string $alias
* @param string $platform
* @param array $array [title, subtitle, body, description, extra, seconds, badge]
* @return array|false
* @return void
*/
public static function pushMsgToAlias($alias, $platform, $array)
private static function pushMsgToAlias($alias, $platform, $array)
{
$config = self::getPushConfig();
if ($config === false) {
return false;
return;
}
//
$title = self::specialCharacters($array['title'] ?: ''); // 标题
@@ -108,65 +158,71 @@ class UmengAlias extends AbstractModel
switch ($platform) {
case 'ios':
if (!isset($config['iOS'])) {
return false;
return;
}
$ios = new IOS($config);
return $ios->send([
'description' => $description,
'payload' => array_merge([
'aps' => [
'alert' => [
'title' => $title,
'subtitle' => $subtitle,
'body' => $body,
self::sendTask([
'platform' => $platform,
'config' => $config,
'data' => [
'description' => $description,
'payload' => array_merge([
'aps' => [
'alert' => [
'title' => $title,
'subtitle' => $subtitle,
'body' => $body,
],
'sound' => 'default',
'badge' => $badge,
],
'sound' => 'default',
'badge' => $badge,
], $extra),
'type' => 'customizedcast',
'alias_type' => 'userid',
'alias' => $alias,
'policy' => [
'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(),
],
], $extra),
'type' => 'customizedcast',
'alias_type' => 'userid',
'alias' => $alias,
'policy' => [
'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(),
],
]
]);
break;
case 'android':
if (!isset($config['Android'])) {
return false;
return;
}
$android = new Android($config);
return $android->send([
'description' => $description,
'payload' => array_merge([
'display_type' => 'notification',
'body' => [
'ticker' => $title,
'text' => $body,
'title' => $title,
'after_open' => 'go_app',
'play_sound' => true,
self::sendTask([
'platform' => $platform,
'config' => $config,
'data' => [
'description' => $description,
'payload' => array_merge([
'display_type' => 'notification',
'body' => [
'ticker' => $title,
'text' => $body,
'title' => $title,
'after_open' => 'go_app',
'play_sound' => true,
],
], $extra),
'type' => 'customizedcast',
'alias_type' => 'userid',
'alias' => $alias,
'mipush' => true,
'mi_activity' => 'app.eeui.umeng.activity.MfrMessageActivity',
'policy' => [
'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(),
],
], $extra),
'type' => 'customizedcast',
'alias_type' => 'userid',
'alias' => $alias,
'mipush' => true,
'mi_activity' => 'app.eeui.umeng.activity.MfrMessageActivity',
'policy' => [
'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(),
],
'channel_properties' => [
'vivo_category' => 'IM',
'huawei_channel_importance' => 'NORMAL',
'huawei_channel_category' => 'IM',
'channel_fcm' => 0,
],
'channel_properties' => [
'oppo_channel_id' => 'dootask',
'vivo_category' => 'IM',
'huawei_channel_importance' => 'NORMAL',
'huawei_channel_category' => 'IM',
'channel_fcm' => 0,
],
]
]);
default:
return false;
break;
}
}

View File

@@ -255,6 +255,18 @@ class User extends AbstractModel
return false;
}
/**
* 返回是否用户机器人
* @return bool
*/
public function isUserBot()
{
if (preg_match('/^user-(.*?)@bot\.system$/', $this->email)) {
return true;
}
return false;
}
/**
* 判断是否管理员
*/
@@ -432,7 +444,9 @@ class User extends AbstractModel
{
$user = self::authInfo();
if (!$user) {
if (Base::token()) {
$token = Base::token();
if ($token) {
UserDevice::forget($token);
throw new ApiException('身份已失效,请重新登录', [], -1);
} else {
throw new ApiException('请登录后继续...', [], -1);
@@ -454,31 +468,46 @@ class User extends AbstractModel
private static function authInfo()
{
if (RequestContext::has('auth')) {
// 缓存
return RequestContext::get('auth');
}
if (Doo::userId() > 0
&& !Doo::userExpired()
&& $user = self::whereUserid(Doo::userId())->whereEmail(Doo::userEmail())->whereEncrypt(Doo::userEncrypt())->first()) {
$upArray = [];
if (Base::getIp() && $user->line_ip != Base::getIp()) {
$upArray['line_ip'] = Base::getIp();
}
if (Carbon::parse($user->line_at)->addSeconds(30)->lt(Carbon::now())) {
$upArray['line_at'] = Carbon::now();
}
$headerLanguage = RequestContext::get('header_language');
if (empty($user->lang) || $headerLanguage) {
if (Doo::checkLanguage($headerLanguage) && $user->lang != $headerLanguage) {
$upArray['lang'] = $headerLanguage;
}
}
if ($upArray) {
$user->updateInstance($upArray);
$user->save();
}
return RequestContext::save('auth', $user);
if (Doo::userId() <= 0) {
// 没有登录
return RequestContext::save('auth', false);
}
return RequestContext::save('auth', false);
if (Doo::userExpired()) {
// 登录过期
return RequestContext::save('auth', false);
}
if (!UserDevice::check()) {
// token 不存在
return RequestContext::save('auth', false);
}
$user = self::whereUserid(Doo::userId())->whereEmail(Doo::userEmail())->whereEncrypt(Doo::userEncrypt())->first();
if (!$user) {
// 登录信息不匹配
return RequestContext::save('auth', false);
}
// 更新登录信息
$upArray = [];
if (Base::getIp() && $user->line_ip != Base::getIp()) {
$upArray['line_ip'] = Base::getIp();
}
if (Carbon::parse($user->line_at)->addSeconds(30)->lt(Carbon::now())) {
$upArray['line_at'] = Carbon::now();
}
$headerLanguage = RequestContext::get('header_language');
if (empty($user->lang) || $headerLanguage) {
if (Doo::checkLanguage($headerLanguage) && $user->lang != $headerLanguage) {
$upArray['lang'] = $headerLanguage;
}
}
if ($upArray) {
$user->updateInstance($upArray);
$user->save();
}
return RequestContext::save('auth', $user);
}
/**
@@ -502,6 +531,7 @@ class User extends AbstractModel
} else {
$token = Doo::userToken();
}
UserDevice::record($token);
unset($userinfo->encrypt);
unset($userinfo->password);
return $userinfo->token = $token;
@@ -719,4 +749,26 @@ class User extends AbstractModel
}
return (bool)User::find($userid)?->bot;
}
/**
* 搜索用户
* @param $key
* @param $take
* @return User[]|\Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Query\Builder[]|\Illuminate\Support\Collection
*/
public static function searchUser($key, $take = 20)
{
return User::select(User::$basicField)
->where(function ($query) use ($key) {
if (str_contains($key, "@")) {
$query->where("email", "like", "%{$key}%");
} else {
$query->where("nickname", "like", "%{$key}%")
->orWhere("pinyin", "like", "%{$key}%")
->orWhere("profession", "like", "%{$key}%");
}
})->orderBy('userid')
->take($take)
->get();
}
}

View File

@@ -55,16 +55,6 @@ class UserBot extends AbstractModel
return str_ends_with($email, '@bot.system') && self::systemBotName($email);
}
/**
* 判断是否系统AI机器人
* @param $email
* @return bool
*/
public static function isAiBot($email)
{
return str_starts_with($email, 'ai-') && self::isSystemBot($email);
}
/**
* 系统机器人名称
* @param $name string 邮箱 或 邮箱前缀
@@ -452,4 +442,39 @@ class UserBot extends AbstractModel
default => [],
};
}
/**
* 创建我的机器人
* @param $userid
* @param $botName
* @return array
*/
public static function newbot($userid, $botName)
{
if (User::select(['users.*'])
->join('user_bots', 'users.userid', '=', 'user_bots.bot_id')
->where('users.bot', 1)
->where('user_bots.userid', $userid)
->count() >= 50) {
return Base::retError("超过最大创建数量。");
}
if (strlen($botName) < 2 || strlen($botName) > 20) {
return Base::retError("机器人名称由2-20个字符组成。");
}
$data = User::botGetOrCreate("user-" . Base::generatePassword(), [
'nickname' => $botName
], $userid);
if (empty($data)) {
return Base::retError("创建失败。");
}
$dialog = WebSocketDialog::checkUserDialog($data, $userid);
if ($dialog) {
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => '/hello',
'title' => '创建成功。',
'data' => $data,
], $data->userid);
}
return Base::retSuccess("创建成功。", $data);
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Apps;
use App\Module\Base;
use App\Module\Ihttp;
@@ -37,8 +38,9 @@ use App\Module\Ihttp;
class UserCheckinFace extends AbstractModel
{
public static function saveFace($userid, $nickname, $faceimg, $remark='')
public static function saveFace($userid, $nickname, $faceimg, $remark = '')
{
Apps::isInstalledThrow('face');
// 取上传图片的URL
$faceimg = Base::unFillUrl($faceimg);
$record = "";
@@ -47,7 +49,7 @@ class UserCheckinFace extends AbstractModel
$record = base64_encode(file_get_contents($faceFile));
}
$url = 'http://' . env('APP_IPPR') . '.14' . ":7788/user";
$url = "http://face:7788/user";
$data = [
'name' => $nickname,
'enrollid' => $userid,
@@ -59,14 +61,14 @@ class UserCheckinFace extends AbstractModel
}
$res = Ihttp::ihttp_post($url, json_encode($data), 15);
if($res['data'] && $data = json_decode($res['data'])){
if($data->ret != 1 && $data->msg){
if ($res['data'] && $data = json_decode($res['data'])) {
if ($data->ret != 1 && $data->msg) {
throw new ApiException($data->msg);
}
}
return AbstractModel::transaction(function() use ($userid, $faceimg, $remark) {
return AbstractModel::transaction(function () use ($userid, $faceimg, $remark) {
$checkinFace = self::query()->whereUserid($userid)->first();
if ($checkinFace) {
self::updateData(['id' => $checkinFace->id], [
@@ -82,27 +84,24 @@ class UserCheckinFace extends AbstractModel
$checkinFace->save();
}
if ($faceimg == '') {
$res = UserCheckinFace::deleteDeviceUser($userid);
if ($res) {
return $res;
}
UserCheckinFace::deleteDeviceUser($userid);
}
return Base::retSuccess('设置成功');
});
}
public static function deleteDeviceUser($userid) {
$url = 'http://' . env('APP_IPPR') . '.14' . ":7788/user/delete";
private static function deleteDeviceUser($userid)
{
$url = "http://face:7788/user/delete";
$data = [
'enrollid' => $userid,
'backupnum' => 50, // 13 删除整个用户 50 删除图片
];
$res = Ihttp::ihttp_post($url, json_encode($data));
if($res['data'] && $data = json_decode($res['data'])){
if($data->ret != 1 && $data->msg){
if ($res['data'] && $data = json_decode($res['data'])) {
if ($data->ret != 1 && $data->msg) {
throw new ApiException($data->msg);
// return Base::retError($data->msg);
}
}
}

View File

@@ -37,11 +37,6 @@ use App\Module\Base;
*/
class UserDelete extends AbstractModel
{
/**
* 昵称
* @param $value
* @return string
*/
public function getCacheAttribute($value)
{
if (!is_array($value)) {
@@ -65,13 +60,25 @@ class UserDelete extends AbstractModel
*/
public static function userid2basic($userid)
{
$row = self::whereUserid($userid)->first();
if (empty($row) || empty($row->cache)) {
return null;
}
$cache = $row->cache;
$cache = array_intersect_key($cache, array_flip(array_merge(User::$basicField, ['department_name'])));
$cache['delete_at'] = $row->created_at->toDateTimeString();
return $cache;
return \Cache::remember("UserDelete:{$userid}", now()->addDays(3), function () use ($userid) {
$row = self::whereUserid($userid)->first();
if (empty($row) || empty($row->cache)) {
return null;
}
$cache = $row->cache;
$cache = array_intersect_key($cache, array_flip(array_merge(User::$basicField, ['department_name'])));
$cache['delete_at'] = $row->created_at->toDateTimeString();
return $cache;
});
}
/**
* userid 获取 昵称
* @param $userid
* @return string
*/
public static function userid2nickname($userid)
{
return self::userid2basic($userid)['nickname'] ?? '';
}
}

314
app/Models/UserDevice.php Normal file
View File

@@ -0,0 +1,314 @@
<?php
namespace App\Models;
use App\Module\Base;
use App\Module\Doo;
use Cache;
use Carbon\Carbon;
use DeviceDetector\DeviceDetector;
use Illuminate\Database\Eloquent\SoftDeletes;
use Request;
/**
* App\Models\UserDevice
*
* @property int $id
* @property int|null $userid 会员ID
* @property string|null $hash TOKEN MD5
* @property string|null $detail 详细信息
* @property string|null $expired_at 过期时间
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read int $is_current
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereDetail($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereExpiredAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereHash($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice withTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice withoutTrashed()
* @mixin \Eloquent
*/
class UserDevice extends AbstractModel
{
use SoftDeletes;
protected $table = 'user_devices';
public static int $deviceLimit = 200; // 每个用户设备限制数量
protected $appends = [
'is_current',
];
public function getDetailAttribute($value)
{
if (is_array($value)) {
return $value;
}
return Base::json2array($value);
}
public function getIsCurrentAttribute(): int
{
return $this->hash === md5(Doo::userToken()) ? 1 : 0;
}
/** ****************************************************************************** */
/** ****************************************************************************** */
/** ****************************************************************************** */
/**
* 缓存key
* @param string $hash
* @return string
*/
private static function ck(string $hash): string
{
return "user_devices:{$hash}";
}
/**
* 解析 UA 获取设备信息
* @param string $ua
* @return array
*/
private static function getDeviceInfo(string $ua): array
{
$result = [
'ip' => Base::getIp(),
'type' => '电脑',
'os' => 'Unknown',
'browser' => 'Unknown',
'version' => '',
'app_name' => '', // 客户端名称
'app_type' => '', // 客户端类型
'app_version' => '', // 客户端版本
];
if (empty($ua)) {
return $result;
}
// 使用 Device-Detector 解析 UA
$dd = new DeviceDetector($ua);
// 解析 UA 字符串
$dd->parse();
// 获取客户端信息(浏览器)
$clientInfo = $dd->getClient();
if (!empty($clientInfo)) {
$result['browser'] = $clientInfo['name'] ?? 'Unknown';
$result['version'] = $clientInfo['version'] ?? '';
}
// 获取操作系统信息
$osInfo = $dd->getOs();
if (!empty($osInfo)) {
$result['os'] = trim(($osInfo['name'] ?? '') . ' ' . ($osInfo['version'] ?? ''));
if (empty($result['os'])) {
$result['os'] = 'Unknown';
}
}
if (preg_match("/android_kuaifan_eeui/i", $ua)) {
// Android 客户端
$result['app_type'] = 'Android';
if ($dd->getBrandName() && $dd->getModel()) {
// 厂商+型号
$result['app_name'] = $dd->getBrandName() . ' ' . $dd->getModel();
} elseif ($dd->getBrandName()) {
// 仅厂商
$result['app_name'] = $dd->getBrandName();
} elseif ($dd->isTablet()) {
// 平板
$result['app_name'] = 'Tablet';
} elseif ($dd->isPhablet()) {
// 平板
$result['app_name'] = 'Phablet';
}
$result['app_version'] = self::getAfterVersion($ua, 'kuaifan_eeui/');
} elseif (preg_match("/ios_kuaifan_eeui/i", $ua)) {
// iOS 客户端
$result['app_type'] = 'iOS';
if (preg_match("/(macintosh|ipad)/i", $ua)) {
// iPad
$result['app_name'] = 'iPad';
} elseif (preg_match("/iphone/i", $ua)) {
// iPhone
$result['app_name'] = 'iPhone';
}
$result['app_version'] = self::getAfterVersion($ua, 'kuaifan_eeui/');
} elseif (preg_match("/dootask/i", $ua)) {
// DooTask 客户端
$result['app_type'] = $osInfo['name'];
$result['app_version'] = self::getAfterVersion($ua, 'dootask/');
} else {
// 其他客户端
$result['app_type'] = 'Web';
$result['app_version'] = Base::getClientVersion();
}
return $result;
}
/**
* 从 ua 的 find 之后的内容获取版本号
* @param string $ua
* @param string $find
* @return string
*/
private static function getAfterVersion(string $ua, string $find): string
{
$findPattern = preg_quote($find, '/');
if (preg_match("/{$findPattern}(.*?)(?:\s|$)/i", $ua, $matches)) {
$appInfo = $matches[1];
// 从内容中提取版本号寻找符合x.x.x格式的部分
if (preg_match("/(\d+\.\d+(?:\.\d+)*)/", $appInfo, $versionMatches)) {
return $versionMatches[1];
}
}
return '';
}
/** ****************************************************************************** */
/** ****************************************************************************** */
/** ****************************************************************************** */
/**
* 检查用户是否存在
* @return string|null
*/
public static function check(): ?string
{
$token = Doo::userToken();
$userid = Doo::userId();
$hash = md5($token);
if (Cache::has(self::ck($hash))) {
return $hash;
}
$row = self::whereHash($hash)->first();
if ($row) {
// 判断是否过期
if ($row->expired_at && Carbon::parse($row->expired_at)->isPast()) {
self::forget($row);
return null;
}
// 更新缓存
self::record();
return $hash;
}
// 没有记录,尝试创建一个(防止升级后所有登录都失效,保证留一个可以保持登录) // todo 后期删除
return AbstractModel::transaction(function () use ($hash, $userid) {
if (self::whereUserid($userid)->withoutTrashed()->lockForUpdate()->exists()) {
return null;
}
return self::record() ? $hash : null;
});
}
/**
* 记录设备(添加、更新)
* @param string|null $token
* @return self|null
*/
public static function record(string $token = null): ?self
{
return AbstractModel::transaction(function () use ($token) {
if (empty($token)) {
$token = Doo::userToken();
$userid = Doo::userId();
$expiredAt = Doo::userExpiredAt();
} else {
$info = Doo::tokenDecode($token);
$userid = $info['userid'] ?? 0;
$expiredAt = $info['expired_at'];
}
$hash = md5($token);
$row = self::whereHash($hash)->lockForUpdate()->first();
if (empty($row)) {
// 生成一个新的设备记录
$row = self::createInstance([
'userid' => $userid,
'hash' => $hash,
]);
if (!$row->save()) {
return null;
}
// 删除多余的设备记录
$currentDeviceCount = self::whereUserid($userid)->count();
if ($currentDeviceCount > self::$deviceLimit) {
$rows = self::whereUserid($userid)->orderBy('id')->take($currentDeviceCount - self::$deviceLimit)->get();
foreach ($rows as $row) {
UserDevice::forget($row);
}
}
}
$row->expired_at = $expiredAt;
if (Request::hasHeader('version')) {
$deviceInfo = array_merge(Base::json2array($row->detail), self::getDeviceInfo($_SERVER['HTTP_USER_AGENT'] ?? ''));
$row->detail = Base::array2json($deviceInfo);
}
$row->save();
Cache::put(self::ck($hash), $row->userid, now()->addHour());
return $row;
});
}
/**
* 忘记设备(删除)
* @param UserDevice|string|int|null $token
* - UserDevice 表示指定的设备对象
* - string 表示指定的 token
* - int 表示指定的数据ID
* - null 表示当前登录的设备
* @return void
*/
public static function forget(UserDevice|string|int $token = null): void
{
if ($token instanceof UserDevice) {
$hash = $token->hash;
$token->delete();
} elseif (Base::isNumber($token)) {
$row = self::find(intval($token));
if ($row) {
$hash = $row->hash;
$row->delete();
}
} else {
if ($token === null) {
$token = Doo::userToken();
}
if ($token) {
$hash = md5($token);
self::whereHash($hash)->delete();
}
}
if (isset($hash)) {
Cache::forget(self::ck($hash));
UmengAlias::whereDeviceHash($hash)->delete();
}
}
}

View File

@@ -97,6 +97,32 @@ class WebSocketDialog extends AbstractModel
->whereNull('users.disable_at');
}
/**
* 搜索对话
* @param $userid
* @param $key
* @param $take
* @return array
*/
public static function searchDialog($userid, $key, $take = 20)
{
return DB::table('web_socket_dialog_users as u')
->select(['d.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
->join('web_socket_dialogs as d', 'u.dialog_id', '=', 'd.id')
->where('u.userid', $userid)
->where(function ($query) use ($key) {
$query->where('d.name', 'like', '%' . $key . '%');
})
->whereNull('d.deleted_at')
->orderByDesc('u.top_at')
->orderByDesc('u.last_at')
->take($take)
->get()
->map(function($item) use ($userid) {
return WebSocketDialog::synthesizeData($item, $userid);
})
->all();
}
/**
* 获取对话列表
@@ -298,7 +324,8 @@ class WebSocketDialog extends AbstractModel
$data['is_disable'] = $basic->isDisable(true);
$data['quick_msgs'] = UserBot::quickMsgs($basic->email);
} else {
$data['name'] = 'non-existent';
$data['name'] = UserDelete::userid2nickname($dialog_user->userid) ?: '[Delete]';
$data['is_disable'] = 1;
$data['dialog_delete'] = 1;
}
$data['dialog_user'] = $dialog_user;
@@ -447,10 +474,10 @@ class WebSocketDialog extends AbstractModel
WebSocketDialogUser::updateInsert([
'dialog_id' => $this->id,
'userid' => $value,
], $updateData, function() use ($value) {
return [
'bot' => User::isBot($value) ? 1 : 0,
];
], $updateData, function() use ($value, $updateData) {
return array_merge($updateData, [
'bot' => User::isBot($value) ? 1 : 0
]);
}, $isInsert);
if ($isInsert) {
WebSocketDialogMsg::sendMsg(null, $this->id, 'notice', [
@@ -666,6 +693,18 @@ class WebSocketDialog extends AbstractModel
Task::deliver($task);
}
/**
* 检查是否是单人对话
* @return bool
*/
public function isSelfDialog()
{
if ($this->type !== 'user') {
return false;
}
return WebSocketDialogUser::whereDialogId($this->id)->where('userid', '>', 0)->count() === 1;
}
/**
* 获取对话(同时检验对话身份)
* @param $dialog_id
@@ -674,6 +713,9 @@ class WebSocketDialog extends AbstractModel
*/
public static function checkDialog($dialog_id, $checkOwner = false)
{
if ($dialog_id <= 0) {
throw new ApiException('参数错误');
}
$dialog = WebSocketDialog::find($dialog_id);
if (empty($dialog)) {
throw new ApiException('对话不存在或已被删除', ['dialog_id' => $dialog_id], -4003);
@@ -687,18 +729,25 @@ class WebSocketDialog extends AbstractModel
throw new ApiException('仅限群主操作');
}
//
if ($dialog->group_type === 'task') {
// 任务群对话校验是否在项目内
$project_id = intval(ProjectTask::whereDialogId($dialog->id)->value('project_id'));
if ($project_id > 0) {
if (ProjectUser::whereProjectId($project_id)->whereUserid($userid)->exists()) {
switch ($dialog->group_type) {
case 'project':
case 'task':
// 项目群、任务群对话校验是否在项目内
if ($dialog->group_type === 'project') {
$projectId = intval(Project::whereDialogId($dialog->id)->value('id'));
} else {
$projectId = intval(ProjectTask::whereDialogId($dialog->id)->value('project_id'));
}
if ($projectId > 0 && ProjectUser::whereProjectId($projectId)->whereUserid($userid)->exists()) {
return $dialog;
}
}
}
if ($dialog->group_type == 'okr') {
return $dialog;
break;
case 'okr':
// OKR群对话不用校验
return $dialog;
}
//
if (!WebSocketDialogUser::whereDialogId($dialog->id)->whereUserid($userid)->exists()) {
WebSocketDialogMsgRead::forceRead($dialog_id, $userid);
throw new ApiException('不在成员列表内', ['dialog_id' => $dialog_id], -4003);
@@ -842,7 +891,7 @@ class WebSocketDialog extends AbstractModel
$data = [];
foreach ($dialogIds as $dialog_id) {
$dialog = WebSocketDialog::checkDialog($dialog_id);
//
$action = $replyId > 0 ? "reply-$replyId" : "";
$path = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/";
if ($image64) {
@@ -856,49 +905,52 @@ class WebSocketDialog extends AbstractModel
Base::makeDir(public_path($path));
copy($filePath, public_path($path) . basename($filePath));
} else {
$setting = Base::setting("system");
$data = Base::upload([
"file" => $files,
"type" => 'more',
"path" => $path,
"fileName" => $fileName,
"quality" => true,
"convertVideo" => true
"convertVideo" => $setting['convert_video'] === 'open',
"compressVideo" => $setting['compress_video'] === 'open',
]);
}
//
if (Base::isError($data)) {
throw new ApiException($data['msg']);
} else {
$fileData = $data['data'];
$filePath = $fileData['file'];
$fileName = $fileData['name'];
$fileData['thumb'] = Base::unFillUrl($fileData['thumb']);
$fileData['size'] *= 1024;
//
if ($dialog->type === 'group' && $dialog->group_type === 'task') { // 任务群组保存文件
if ($imageAttachment || !in_array($fileData['ext'], File::imageExt)) { // 如果是图片不保存
$task = ProjectTask::whereDialogId($dialog->id)->first();
if ($task) {
$file = ProjectTaskFile::createInstance([
'project_id' => $task->project_id,
'task_id' => $task->id,
'name' => $fileData['name'],
'size' => $fileData['size'],
'ext' => $fileData['ext'],
'path' => $fileData['path'],
'thumb' => $fileData['thumb'],
'userid' => $user->userid,
]);
$file->save();
}
}
$fileData = $data['data'];
$filePath = $fileData['file'];
$fileName = $fileData['name'];
$fileData['thumb'] = Base::unFillUrl($fileData['thumb']);
$fileData['size'] *= 1024;
// 任务群组保存文件
if ($dialog->group_type === 'task') {
// 如果是图片不保存
if ($imageAttachment || !in_array($fileData['ext'], File::imageExt)) {
$task = ProjectTask::whereDialogId($dialog->id)->first();
if ($task) {
$file = ProjectTaskFile::createInstance([
'project_id' => $task->project_id,
'task_id' => $task->id,
'name' => $fileData['name'],
'size' => $fileData['size'],
'ext' => $fileData['ext'],
'path' => $fileData['path'],
'thumb' => $fileData['thumb'],
'userid' => $user->userid,
]);
$file->save();
}
}
//
$result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'file', $fileData, $user->userid);
if (Base::isSuccess($result)) {
if (isset($task)) {
$result['data']['task_id'] = $task->id;
}
}
// 发送消息
$result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'file', $fileData, $user->userid);
if (Base::isSuccess($result)) {
if (isset($task)) {
$result['data']['task_id'] = $task->id;
}
}
}

View File

@@ -379,7 +379,6 @@ class WebSocketDialogMsg extends AbstractModel
'dialog_id' => $this->dialog_id,
];
$dialog = WebSocketDialog::find($this->dialog_id);
$dialog->pushMsg('update', $upData);
//
$retData = [
'add' => [],
@@ -426,6 +425,7 @@ class WebSocketDialogMsg extends AbstractModel
}
}
//
$dialog->pushMsg('update', $upData);
return Base::retSuccess($this->todo ? '设置成功' : '取消成功', $retData);
}

View File

@@ -3,8 +3,8 @@
namespace App\Models;
use App\Module\Base;
use App\Module\Extranet;
use Swoole\Coroutine;
use App\Tasks\UpdateSessionTitleViaAiTask;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Cache;
/**
@@ -82,18 +82,6 @@ class WebSocketDialogSession extends AbstractModel
$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();
}
});
Task::deliver(new UpdateSessionTitleViaAiTask($session->id, $originalTitle));
}
}

60
app/Module/Apps.php Normal file
View File

@@ -0,0 +1,60 @@
<?php
namespace App\Module;
use App\Exceptions\ApiException;
use App\Services\RequestContext;
use Symfony\Component\Yaml\Yaml;
class Apps
{
/**
* 判断应用是否已安装
*
* @param string $appId 应用ID名称
* @return bool 如果应用已安装返回 true否则返回 false
*/
public static function isInstalled(string $appId): bool
{
if ($appId === 'appstore') {
return true;
}
$key = 'app_installed_' . $appId;
if (RequestContext::has($key)) {
return RequestContext::get($key);
}
$configFile = base_path('docker/appstore/config/' . $appId . '/config.yml');
$installed = false;
if (file_exists($configFile)) {
$configData = Yaml::parseFile($configFile);
$installed = $configData['status'] === 'installed';
}
return RequestContext::save($key, $installed);
}
/**
* 判断应用是否已安装,如果未安装则抛出异常
* @param string $appId
* @return void
*/
public static function isInstalledThrow(string $appId): void
{
if (!self::isInstalled($appId)) {
$name = match ($appId) {
'ai' => 'AI Robot',
'face' => 'Face check-in',
'appstore' => 'AppStore',
'approve' => 'Approval',
'office' => 'OnlyOffice',
'drawio' => 'Drawio',
'minder' => 'Minder',
'search' => 'ZincSearch',
default => $appId,
};
throw new ApiException("应用「{$name}」未安装", [], 0, false);
}
}
}

View File

@@ -9,7 +9,7 @@ use App\Services\RequestContext;
use Cache;
use Carbon\Carbon;
use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Exception\CommonMarkException;
use League\HTMLToMarkdown\HtmlConverter;
use Overtrue\Pinyin\Pinyin;
use Redirect;
use Request;
@@ -351,7 +351,7 @@ class Base
/**
* 删除文件夹及文件夹下所有的文件
* @param $dirName
* @param bool $undeleteDir 不删除文件夹(只删除文件)
* @param bool $undeleteDir 不删除文件夹本身(只删除文件夹里面的内容
*/
public static function deleteDirAndFile($dirName, $undeleteDir = false)
{
@@ -802,16 +802,16 @@ class Base
str_starts_with(str_replace(' ', '', $str), "data:image/")
) {
return $str;
} else {
if (RequestContext::has('fill_url_remote_url')) {
return "{{RemoteURL}}" . $str;
}
try {
return url($str);
} catch (\Throwable) {
return self::getSchemeAndHost() . "/" . $str;
}
}
if (RequestContext::has('fill_url_remote_url')) {
return "{{RemoteURL}}" . $str;
}
try {
$fillUrl = url($str);
} catch (\Throwable) {
$fillUrl = self::getSchemeAndHost() . "/" . $str;
}
return RequestContext::replaceBaseUrl($fillUrl);
}
/**
@@ -827,12 +827,19 @@ class Base
}
return $str;
}
try {
$find = url('');
} catch (\Throwable) {
$find = self::getSchemeAndHost();
if (empty($str)) {
return $str;
}
return Base::leftDelete($str, $find . '/');
$parsedUrl = parse_url($str);
if (isset($parsedUrl['scheme']) && isset($parsedUrl['host'])) {
$relativePath = $parsedUrl['path'] ?? '';
$relativePath = ltrim($relativePath, '/');
$absolutePath = public_path($relativePath);
if (file_exists($absolutePath) || file_exists(Base::thumbRestore($absolutePath))) {
return $relativePath;
}
}
return $str;
}
/**
@@ -1872,18 +1879,18 @@ class Base
if (!in_array($extension, ['mp3', 'wav'])) {
return Base::retError('语音格式错误');
}
$fileName = 'record_' . md5($base64) . '.' . $extension;
$saveName = 'record_' . md5($base64) . '.' . $extension;
$fileDir = $param['path'];
$filePath = public_path($fileDir);
Base::makeDir($filePath);
if (file_put_contents($filePath . $fileName, base64_decode(str_replace($res[1], '', $base64)))) {
$fileSize = filesize($filePath . $fileName);
if (file_put_contents($filePath . $saveName, base64_decode(str_replace($res[1], '', $base64)))) {
$fileSize = filesize($filePath . $saveName);
$array = [
"name" => $fileName, //原文件名
"name" => $saveName, //原文件名
"size" => Base::twoFloat($fileSize / 1024, true), //大小KB
"file" => $filePath . $fileName, //文件的完整路径 "D:\www....KzZ.jpg"
"path" => $fileDir . $fileName, //相对路径 "uploads/pic....KzZ.jpg"
"url" => Base::fillUrl($fileDir . $fileName), //完整的URL "https://.....hhsKzZ.jpg"
"file" => $filePath . $saveName, //文件的完整路径 "D:\www....KzZ.jpg"
"path" => $fileDir . $saveName, //相对路径 "uploads/pic....KzZ.jpg"
"url" => Base::fillUrl($fileDir . $saveName), //完整的URL "https://.....hhsKzZ.jpg"
"ext" => $extension, //文件后缀名
];
return Base::retSuccess('success', $array);
@@ -1894,8 +1901,23 @@ class Base
/**
* image64图片保存
* @param array $param [ image64=带前缀的base64, path=>文件路径, fileName=>文件名称, scale=>[压缩原图宽,高, 压缩方式], autoThumb=>false不要自动生成缩略图, 'quality'=>压缩图片质量(默认0不压缩) ]
* @return array [name=>文件名, size=>文件大小(单位KB),file=>绝对地址, path=>相对地址, url=>全路径地址, ext=>文件后缀名]
* @param array $param [
image64=带前缀的base64,
path=>文件路径,
fileName=>文件名称,
saveName=>保存文件名称,
scale=>[压缩原图宽,高, 压缩方式],
autoThumb=>false不要自动生成缩略图,
quality=>压缩图片质量(默认0不压缩)
]
* @return array [
name=>文件名,
size=>文件大小(单位KB),
file=>绝对地址,
path=>相对地址,
url=>全路径地址,
ext=>文件后缀名
]
*/
public static function image64save($param)
{
@@ -1906,8 +1928,8 @@ class Base
return Base::retError('图片格式错误');
}
$scaleName = "";
if ($param['fileName']) {
$fileName = basename($param['fileName']);
if ($param['saveName']) {
$saveName = basename($param['saveName']);
} else {
if ($param['scale'] && is_array($param['scale'])) {
list($width, $height) = $param['scale'];
@@ -1918,21 +1940,21 @@ class Base
}
}
}
$fileName = 'paste_' . md5($imgBase64) . '.' . $extension;
$saveName = 'paste_' . md5($imgBase64) . '.' . $extension;
$scaleName = md5_file($imgBase64) . $scaleName . '.' . $extension;
}
$fileDir = $param['path'];
$filePath = public_path($fileDir);
$fileFullPath = $filePath . $fileName;
$fileFullPath = $filePath . $saveName;
Base::makeDir($filePath);
if (file_put_contents($fileFullPath, base64_decode(str_replace($res[1], '', $imgBase64)))) {
$fileSize = filesize($fileFullPath);
$array = [
"name" => $fileName, //原文件名
"name" => $param['fileName'] ?: $saveName, //原文件名
"size" => Base::twoFloat($fileSize / 1024, true), //大小KB
"file" => $fileFullPath, //文件的完整路径 "D:\www....KzZ.jpg"
"path" => $fileDir . $fileName, //相对路径 "uploads/pic....KzZ.jpg"
"url" => Base::fillUrl($fileDir . $fileName), //完整的URL "https://.....hhsKzZ.jpg"
"path" => $fileDir . $saveName, //相对路径 "uploads/pic....KzZ.jpg"
"url" => Base::fillUrl($fileDir . $saveName), //完整的URL "https://.....hhsKzZ.jpg"
"thumb" => '', //缩略图(预览图) "https://.....hhsKzZ.jpg_thumb.jpg"
"width" => -1, //图片宽度
"height" => -1, //图片高度
@@ -1963,10 +1985,10 @@ class Base
// 重命名
if ($scaleName) {
$scaleName = str_replace(['{WIDTH}', '{HEIGHT}'], [$array['width'], $array['height']], $scaleName);
if (rename($array['file'], Base::rightDelete($array['file'], $fileName) . $scaleName)) {
$array['file'] = Base::rightDelete($array['file'], $fileName) . $scaleName;
$array['path'] = Base::rightDelete($array['path'], $fileName) . $scaleName;
$array['url'] = Base::rightDelete($array['url'], $fileName) . $scaleName;
if (rename($array['file'], Base::rightDelete($array['file'], $saveName) . $scaleName)) {
$array['file'] = Base::rightDelete($array['file'], $saveName) . $scaleName;
$array['path'] = Base::rightDelete($array['path'], $saveName) . $scaleName;
$array['url'] = Base::rightDelete($array['url'], $saveName) . $scaleName;
}
}
}
@@ -2000,12 +2022,14 @@ class Base
file=>Request::file,
path=>文件路径,
fileName=>文件名称,
saveName=>保存文件名称,
scale=>[压缩原图宽,高, 压缩方式],
size=>限制大小KB,
autoThumb=>false不要自动生成缩略图,
chmod=>权限(默认0644),
quality=>压缩图片质量(默认0不压缩),
convertVideo=>转换视频格式(默认false) ,
compressVideo=>压缩视频(默认false如果转换就不压缩) ,
]
* @return array [
name=>原文件名,
@@ -2091,10 +2115,10 @@ class Base
}
}
$scaleName = "";
if ($param['fileName'] === true) {
$fileName = $file->getClientOriginalName();
} elseif ($param['fileName']) {
$fileName = basename($param['fileName']);
if ($param['saveName'] === true) {
$saveName = $file->getClientOriginalName();
} elseif ($param['saveName']) {
$saveName = basename($param['saveName']);
} else {
if ($param['scale'] && is_array($param['scale'])) {
list($width, $height) = $param['scale'];
@@ -2105,19 +2129,19 @@ class Base
}
}
}
$fileName = md5_file($file);
$saveName = md5_file($file);
$scaleName = md5_file($file) . $scaleName;
if ($extension) {
$fileName = $fileName . '.' . $extension;
$saveName = $saveName . '.' . $extension;
$scaleName = $scaleName . '.' . $extension;
}
}
//
$file->move(public_path($param['path']), $fileName);
$file->move(public_path($param['path']), $saveName);
//
$path = $param['path'] . $fileName;
$path = $param['path'] . $saveName;
$array = [
"name" => $file->getClientOriginalName(), //原文件名
"name" => $param['fileName'] ?: $file->getClientOriginalName(), //原文件名
"size" => Base::twoFloat($fileSize / 1024, true), //大小KB
"file" => public_path($path), //文件的完整路径 "D:\www....KzZ.jpg"
"path" => $path, //相对路径 "uploads/pic....KzZ.jpg"
@@ -2163,6 +2187,7 @@ class Base
}
@shell_exec($command);
if (file_exists($output) && filesize($output) > 0) {
// 压缩后的文件正常
@unlink($array['file']);
$array = array_merge($array, [
"name" => Base::rightReplace($array['name'], ".{$array['ext']}", '.mp4'),
@@ -2173,6 +2198,27 @@ class Base
"ext" => 'mp4',
]);
}
$param['compressVideo'] = false; // 如果转换就不压缩
}
if ($param['compressVideo'] && $array['ext'] == 'mp4') {
// 压缩视频
$output = $array['file'] . '_compress';
$command = sprintf("ffmpeg -y -i %s -c:v libx264 -crf 28 -preset medium -c:a aac -b:a 96k %s 2>&1", escapeshellarg($array['file']), escapeshellarg($output));
@shell_exec($command);
if (file_exists($output) && filesize($output) > 0) {
// 压缩后的文件正常
if (filesize($output) < filesize($array['file'])) {
// 小于原文件
@unlink($array['file']);
$array = array_merge($array, [
"size" => Base::twoFloat(filesize($output) / 1024, true),
"file" => $output,
]);
} else {
// 大于原文件
@unlink($output);
}
}
}
if (in_array($array['ext'], ['mov', 'webm', 'mp4'])) {
// 视频尺寸
@@ -2207,10 +2253,10 @@ class Base
// 重命名
if ($scaleName) {
$scaleName = str_replace(['{WIDTH}', '{HEIGHT}'], [$array['width'], $array['height']], $scaleName);
if (rename($array['file'], Base::rightDelete($array['file'], $fileName) . $scaleName)) {
$array['file'] = Base::rightDelete($array['file'], $fileName) . $scaleName;
$array['path'] = Base::rightDelete($array['path'], $fileName) . $scaleName;
$array['url'] = Base::rightDelete($array['url'], $fileName) . $scaleName;
if (rename($array['file'], Base::rightDelete($array['file'], $saveName) . $scaleName)) {
$array['file'] = Base::rightDelete($array['file'], $saveName) . $scaleName;
$array['path'] = Base::rightDelete($array['path'], $saveName) . $scaleName;
$array['url'] = Base::rightDelete($array['url'], $saveName) . $scaleName;
}
}
}
@@ -2535,22 +2581,37 @@ class Base
/**
* 中文转拼音
* @param $str
* @param $delim
* @return string
*/
public static function cn2pinyin($str)
public static function cn2pinyin($str, $delim = '')
{
if (empty($str)) {
return '';
}
if (!preg_match("/^[a-zA-Z0-9_.]+$/", $str)) {
$str = Cache::rememberForever("cn2pinyin:" . md5($str), function() use ($str) {
$str = Cache::rememberForever("cn2pinyin:" . md5($str . '_' . $delim), function () use ($delim, $str) {
$pinyin = new Pinyin();
return $pinyin->permalink($str, '');
return $pinyin->permalink($str, $delim);
});
}
return $str;
}
/**
* 驼峰转下划线
* @param $str
* @return string
*/
public static function camel2snake($str)
{
if (empty($str)) {
return '';
}
$str = preg_replace('/([a-z])([A-Z])/', '$1_$2', $str);
return strtolower($str);
}
/**
* 缓存数据
* @param $name
@@ -2977,11 +3038,26 @@ class Base
*/
public static function markdown2html($markdown)
{
$converter = new CommonMarkConverter();
try {
$converter = new CommonMarkConverter();
return $converter->convert($markdown);
} catch (CommonMarkException $e) {
} catch (\League\CommonMark\Exception\CommonMarkException $e) {
return $markdown;
}
}
/**
* html 转 MD(markdown)
* @param $html
* @return mixed|string
*/
public static function html2markdown($html)
{
try {
$converter = new HtmlConverter();
return $converter->convert($html);
} catch (\Exception) {
return $html;
}
}
}

View File

@@ -47,6 +47,7 @@ class Doo
char* md5s(char* text, char* password);
char* macs();
char* dooSN();
char* version();
char* pgpGenerateKeyPair(char* name, char* email, char* passphrase);
char* pgpEncrypt(char* plainText, char* publicKey);
char* pgpDecrypt(char* cipherText, char* privateKey, char* passphrase);
@@ -181,12 +182,12 @@ class Doo
/**
* token过期时间来自请求的token
* @return string
* @return string|null
*/
public static function userExpiredAt(): string
public static function userExpiredAt(): ?string
{
$expiredAt = self::string(self::doo()->userExpiredAt());
return $expiredAt === 'forever' ? '' : $expiredAt;
return $expiredAt === 'forever' ? null : $expiredAt;
}
/**
@@ -263,7 +264,9 @@ class Doo
*/
public static function tokenDecode($token): array
{
return Base::json2array(self::string(self::doo()->tokenDecode($token)));
$array = Base::json2array(self::string(self::doo()->tokenDecode($token)));
$array['expired_at'] = $array['expired_at'] === 'forever' ? null : $array['expired_at'];
return $array;
}
/**
@@ -359,6 +362,15 @@ class Doo
return self::string(self::doo()->dooSN());
}
/**
* 获取当前版本
* @return string
*/
public static function dooVersion(): string
{
return self::string(self::doo()->version());
}
/**
* 生成PGP密钥对
* @param $name

View File

@@ -1,308 +0,0 @@
<?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

@@ -1,204 +0,0 @@
<?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

@@ -1,375 +0,0 @@
<?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

@@ -6,52 +6,83 @@ use Exception;
use PhpOffice\PhpWord\IOFactory as WordIOFactory;
use PhpOffice\PhpSpreadsheet\IOFactory as SpreadsheetIOFactory;
use PhpOffice\PhpPresentation\IOFactory as PresentationIOFactory;
use Illuminate\Support\Facades\File as FileFacade;
class TextExtractor
{
private string $filePath;
private string $fileMimeType;
private string $fileExtension;
/**
* 从文件中提取文本
*
* @param string $filePath 文件路径
* @return string
* @param string $filePath
* @throws Exception
*/
public function extractContent(string $filePath): string
public function __construct(string $filePath)
{
if (!file_exists($filePath)) {
throw new Exception("File does not exist: {$filePath}");
}
$this->filePath = $filePath;
$this->fileMimeType = FileFacade::mimeType($filePath);
$this->fileExtension = $this->detectFileType();
}
$fileExtension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
/**
* 从文件中提取文本
* @return string
* @throws Exception
*/
public function extractContent(): string
{
return match ($this->fileExtension) {
// Word文档
'docx' => $this->parseWordDocument(),
return match ($fileExtension) {
// Word documents
'docx' => $this->parseWordDocument($filePath),
// Excel文档
'xlsx', 'xls', 'csv' => $this->parseSpreadsheet(),
// Spreadsheet files
'xlsx', 'xls', 'csv' => $this->parseSpreadsheet($filePath),
// PowerPoint文档
'ppt', 'pptx' => $this->parsePresentation(),
// Presentation files
'ppt', 'pptx' => $this->parsePresentation($filePath),
// PDF文档
'pdf' => $this->parsePdf(),
// PDF files (requires additional library)
'pdf' => $this->parsePdf($filePath),
// RTF文档
'rtf' => $this->parseRtf(),
// RTF files
'rtf' => $this->parseRtf($filePath),
// 其他文本文件
default => $this->parseOther(),
};
}
// Default case
default => $this->parseOther($filePath),
/**
* 获取文件类型
* @return string
*/
private function detectFileType(): string
{
return match ($this->fileMimeType) {
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
'application/vnd.ms-excel' => 'xls',
'text/csv', 'application/csv' => 'csv',
'application/vnd.ms-powerpoint' => 'ppt',
'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx',
'application/pdf' => 'pdf',
'application/rtf', 'text/rtf' => 'rtf',
default => strtolower(pathinfo($this->filePath, PATHINFO_EXTENSION)),
};
}
/**
* Parse Word documents (.doc, .docx)
* @return string
*/
private function parseWordDocument(string $filePath): string
private function parseWordDocument(): string
{
$phpWord = WordIOFactory::load($filePath);
$phpWord = WordIOFactory::load($this->filePath);
$text = '';
// Extract text from each section
@@ -74,10 +105,11 @@ class TextExtractor
/**
* Parse spreadsheet files (.xlsx, .xls, .csv)
* @return string
*/
private function parseSpreadsheet(string $filePath): string
private function parseSpreadsheet(): string
{
$spreadsheet = SpreadsheetIOFactory::load($filePath);
$spreadsheet = SpreadsheetIOFactory::load($this->filePath);
$text = '';
// Extract text from all worksheets
@@ -109,11 +141,12 @@ class TextExtractor
/**
* Parse presentation files (.ppt, .pptx)
* @return string
* @throws Exception
*/
private function parsePresentation(string $filePath): string
private function parsePresentation(): string
{
$presentation = PresentationIOFactory::load($filePath);
$presentation = PresentationIOFactory::load($this->filePath);
$text = '';
// Extract text from all slides
@@ -136,9 +169,10 @@ class TextExtractor
/**
* Parse PDF files (requires additional library like Smalot\PdfParser)
* @return string
* @throws Exception
*/
private function parsePdf(string $filePath): string
private function parsePdf(): string
{
// You'll need to install the Smalot PDF Parser: composer require smalot/pdfparser
if (!class_exists('\Smalot\PdfParser\Parser')) {
@@ -146,17 +180,18 @@ class TextExtractor
}
$parser = new \Smalot\PdfParser\Parser();
$pdf = $parser->parseFile($filePath);
$pdf = $parser->parseFile($this->filePath);
return $pdf->getText();
}
/**
* Parse RTF files
* @return string
*/
private function parseRtf(string $filePath): string
private function parseRtf(): string
{
// Simple RTF to text conversion
$content = file_get_contents($filePath);
$content = file_get_contents($this->filePath);
// Remove RTF control words and groups
$content = preg_replace('/\\\\([a-z]{1,32})(-?[0-9]{1,10})?[ ]?/i', '', $content);
@@ -175,23 +210,20 @@ class TextExtractor
/**
* Parse Other(text) files
* @return string
* @throws Exception
*/
private function parseOther(string $filePath): string
private function parseOther(): string
{
$finfo = finfo_open(FILEINFO_MIME);
$mimeType = finfo_file($finfo, $filePath);
finfo_close($finfo);
$isBinary = !str_contains($mimeType, 'text/')
&& !str_contains($mimeType, 'application/json')
&& !str_contains($mimeType, 'application/xml');
$isBinary = !str_contains($this->fileMimeType, 'text/')
&& !str_contains($this->fileMimeType, 'application/json')
&& !str_contains($this->fileMimeType, 'application/xml');
if ($isBinary) {
throw new Exception("Unable to read the text content of this type of file");
}
return file_get_contents($filePath);
return file_get_contents($this->filePath);
}
/** ********************************************************************* */
@@ -201,20 +233,25 @@ class TextExtractor
/**
* 获取文件内容
* @param $filePath
* @param float|int $maxSize 最大文件大小,单位字节,默认300KB
* @param int $fileMaxSize 最大文件大小,单位字节,默认1024KB
* @param int $contentMaxSize 最大内容大小单位字节默认300KB
* @return array
*/
public static function extractFile($filePath, float|int $maxSize = 300 * 1024): array
public static function extractFile($filePath, int $fileMaxSize = 1024, int $contentMaxSize = 300): array
{
if (!file_exists($filePath) || !is_file($filePath)) {
return Base::retError("Failed to read contents of {$filePath}");
}
if (filesize($filePath) > $maxSize) {
return Base::retError("File size exceeds " . Base::readableBytes($maxSize) . ", unable to display content");
if (filesize($filePath) > $fileMaxSize * 1024) {
return Base::retError("File size exceeds " . Base::readableBytes($fileMaxSize * 1024) . ", unable to display content");
}
try {
$extractor = new self();
return Base::retSuccess("success", $extractor->extractContent($filePath));
$extractor = new self($filePath);
$content = $extractor->extractContent();
if (strlen($content) > $contentMaxSize * 1024) {
return Base::retError("Content size exceeds " . Base::readableBytes($contentMaxSize * 1024) . ", unable to display content");
}
return Base::retSuccess("success", $content);
} catch (Exception $e) {
return Base::retError($e->getMessage());
}

View File

@@ -0,0 +1,267 @@
<?php
namespace App\Module\ZincSearch;
use App\Module\Apps;
use App\Module\Doo;
/**
* ZincSearch 公共类
*/
class ZincSearchBase
{
private mixed $host;
private mixed $port;
private mixed $user;
private mixed $pass;
/**
* 构造函数
*/
public function __construct()
{
$this->host = env('ZINCSEARCH_HOST', 'search');
$this->port = env('ZINCSEARCH_PORT', '4080');
$this->user = env('DB_USERNAME', '');
$this->pass = env('DB_PASSWORD', '');
}
/**
* 通用请求方法
*/
private function request($path, $body = null, $method = 'POST')
{
if (!Apps::isInstalled("search")) {
return [
'success' => false,
'error' => Doo::translate("应用「ZincSearch」未安装")
];
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://{$this->host}:{$this->port}{$path}");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_USERPWD, $this->user . ':' . $this->pass);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
$headers = ['Content-Type: application/json'];
if ($method === 'BULK') {
$headers = ['Content-Type: text/plain'];
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
$result = curl_exec($ch);
$error = curl_error($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($error) {
return ['success' => false, 'error' => $error];
}
$data = json_decode($result, true);
return [
'success' => $status >= 200 && $status < 300,
'status' => $status,
'data' => $data
];
}
// ==============================
// 索引管理相关方法
// ==============================
/**
* 创建索引
*/
public static function createIndex($index, $mappings = []): array
{
$body = json_encode([
'name' => $index,
'mappings' => $mappings
]);
return (new self())->request("/api/index", $body);
}
/**
* 获取索引信息
*/
public static function getIndex($index): array
{
return (new self())->request("/api/index/{$index}", null, 'GET');
}
/**
* 判断索引是否存在
*/
public static function indexExists($index): bool
{
$result = self::getIndex($index);
return $result['success'] && isset($result['data']['name']);
}
/**
* 获取所有索引
*/
public static function listIndices(): array
{
return (new self())->request("/api/index", null, 'GET');
}
/**
* 删除索引
*/
public static function deleteIndex($index): array
{
return (new self())->request("/api/index/{$index}", null, 'DELETE');
}
/**
* 删除所有索引
*/
public static function deleteAllIndices(): array
{
$instance = new self();
$result = $instance->request("/api/index", null, 'GET');
if (!$result['success']) {
return $result;
}
$indices = $result['data'] ?? [];
$deleteResults = [];
$success = true;
foreach ($indices as $index) {
$indexName = $index['name'] ?? '';
if (!empty($indexName)) {
$deleteResult = $instance->request("/api/index/{$indexName}", null, 'DELETE');
$deleteResults[$indexName] = $deleteResult;
if (!$deleteResult['success']) {
$success = false;
}
}
}
return [
'success' => $success,
'message' => $success ? '所有索引删除成功' : '部分索引删除失败',
'details' => $deleteResults
];
}
/**
* 分析文本
*/
public static function analyze($analyzer, $text): array
{
$body = json_encode([
'analyzer' => $analyzer,
'text' => $text
]);
return (new self())->request("/api/_analyze", $body);
}
// ==============================
// 文档管理相关方法
// ==============================
/**
* 写入单条文档
*/
public static function addDoc($index, $doc): array
{
$body = json_encode($doc);
return (new self())->request("/api/{$index}/_doc", $body);
}
/**
* 更新文档
*/
public static function updateDoc($index, $id, $doc): array
{
$body = json_encode($doc);
return (new self())->request("/api/{$index}/_update/{$id}", $body);
}
/**
* 删除文档
*/
public static function deleteDoc($index, $id): array
{
return (new self())->request("/api/{$index}/_doc/{$id}", null, 'DELETE');
}
/**
* 批量写入文档
*/
public static function addDocs($index, $docs): array
{
$body = json_encode([
'index' => $index,
'records' => $docs
]);
return (new self())->request("/api/_bulkv2", $body);
}
/**
* 使用原始BULK API批量写入文档
* 请求格式为Elasticsearch兼容格式
*/
public static function bulkDocs($data): array
{
return (new self())->request("/api/_bulk", $data, 'BULK');
}
// ==============================
// 搜索相关方法
// ==============================
/**
* 查询文档
*/
public static function search($index, $query, $from = 0, $size = 10): array
{
$searchParams = [
'search_type' => 'match',
'query' => [
'term' => $query
],
'from' => $from,
'max_results' => $size
];
$body = json_encode($searchParams);
return (new self())->request("/api/{$index}/_search", $body);
}
/**
* 高级查询文档
*/
public static function advancedSearch($index, $searchParams): array
{
$body = json_encode($searchParams);
return (new self())->request("/api/{$index}/_search", $body);
}
/**
* 兼容ES查询文档
*/
public static function elasticSearch($index, $searchParams): array
{
$body = json_encode($searchParams);
return (new self())->request("/es/{$index}/_search", $body);
}
/**
* 多索引查询
*/
public static function multiSearch($queries): array
{
$body = json_encode($queries);
return (new self())->request("/api/_msearch", $body);
}
}

View File

@@ -0,0 +1,612 @@
<?php
namespace App\Module\ZincSearch;
use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialogUser;
use App\Module\Apps;
use Carbon\Carbon;
use DB;
use Illuminate\Support\Facades\Log;
/**
* ZincSearch 会话消息类
*
* 使用方法:
*
* 1. 基础方法
* - 清空所有数据: clear();
*
* 2. 搜索方法
* - 关键词搜索: search('用户ID', '关键词');
*
* 3. 基本方法
* - 单个同步: sync(WebSocketDialogMsg $dialogMsg);
* - 批量同步: batchSync(WebSocketDialogMsg[] $dialogMsgs);
* - 用户同步: userSync(WebSocketDialogUser $dialogUser);
* - 删除消息: delete(WebSocketDialogMsg|WebSocketDialogUser|int $data);
*/
class ZincSearchDialogMsg
{
/**
* 索引名称
*/
protected static string $indexNameMsg = 'dialogMsg';
protected static string $indexNameUser = 'dialogUser';
// ==============================
// 基础方法
// ==============================
/**
* 确保索引存在
*/
private static function ensureIndex(): bool
{
if (!ZincSearchBase::indexExists(self::$indexNameMsg)) {
$mappings = [
'properties' => [
// 拓展数据
'dialog_userid' => ['type' => 'keyword', 'index' => true], // 对话ID+用户ID
'to_userid' => ['type' => 'numeric', 'index' => true], // 此消息发给的用户ID
// 消息数据
'id' => ['type' => 'numeric', 'index' => true],
'dialog_id' => ['type' => 'numeric', 'index' => true],
'dialog_type' => ['type' => 'keyword', 'index' => true],
'session_id' => ['type' => 'numeric', 'index' => true],
'userid' => ['type' => 'numeric', 'index' => true],
'type' => ['type' => 'keyword', 'index' => true],
'key' => ['type' => 'text', 'index' => true],
'created_at' => ['type' => 'date', 'index' => true],
'updated_at' => ['type' => 'date', 'index' => true],
]
];
$result = ZincSearchBase::createIndex(self::$indexNameMsg, $mappings);
return $result['success'] ?? false;
}
if (!ZincSearchBase::indexExists(self::$indexNameUser)) {
$mappings = [
'properties' => [
// 拓展数据
'dialog_userid' => ['type' => 'keyword', 'index' => true], // 对话ID+用户ID
// 用户数据
'id' => ['type' => 'numeric', 'index' => true],
'dialog_id' => ['type' => 'numeric', 'index' => true],
'userid' => ['type' => 'numeric', 'index' => true],
'top_at' => ['type' => 'date', 'index' => true],
'last_at' => ['type' => 'date', 'index' => true],
'mark_unread' => ['type' => 'numeric', 'index' => true],
'silence' => ['type' => 'numeric', 'index' => true],
'hide' => ['type' => 'numeric', 'index' => true],
'color' => ['type' => 'keyword', 'index' => true],
'created_at' => ['type' => 'date', 'index' => true],
'updated_at' => ['type' => 'date', 'index' => true],
]
];
$result = ZincSearchBase::createIndex(self::$indexNameUser, $mappings);
return $result['success'] ?? false;
}
return true;
}
/**
* 清空所有键值
*
* @return bool 是否成功
*/
public static function clear(): bool
{
// 检查索引是否存在然后删除
if (ZincSearchBase::indexExists(self::$indexNameMsg)) {
$deleteResult = ZincSearchBase::deleteIndex(self::$indexNameMsg);
if (!($deleteResult['success'] ?? false)) {
return false;
}
}
if (ZincSearchBase::indexExists(self::$indexNameUser)) {
$deleteResult = ZincSearchBase::deleteIndex(self::$indexNameUser);
if (!($deleteResult['success'] ?? false)) {
return false;
}
}
return self::ensureIndex();
}
// ==============================
// 搜索方法
// ==============================
/**
* 根据用户ID和消息关键词搜索会话
*
* @param string $userid 用户ID
* @param string $keyword 消息关键词
* @param int $from 起始位置
* @param int $size 返回结果数量
* @return array
*/
public static function search(string $userid, string $keyword, int $from = 0, int $size = 20): array
{
if (!Apps::isInstalled("search")) {
// 如果搜索功能未安装,使用数据库查询
return self::searchByMysql($userid, $keyword, $from, $size);
}
$searchParams = [
'query' => [
'bool' => [
'must' => [
['term' => ['to_userid' => $userid]],
['match_phrase' => ['key' => $keyword]]
]
]
],
'from' => $from,
'size' => $size,
'sort' => [
['updated_at' => 'desc']
]
];
try {
$result = ZincSearchBase::elasticSearch(self::$indexNameMsg, $searchParams);
$hits = $result['data']['hits']['hits'] ?? [];
// 收集所有的用户信息
$dialogUserids = [];
foreach ($hits as $hit) {
$source = $hit['_source'];
$dialogUserids[] = $source['dialog_userid'];
}
$userInfos = self::searchUser(array_unique($dialogUserids));
// 组合返回结果,将用户信息合并到消息中
$msgs = [];
foreach ($hits as $hit) {
$msgInfo = $hit['_source'];
$userInfo = $userInfos[$msgInfo['dialog_userid']] ?? [];
if ($userInfo) {
$msgs[] = [
'id' => $msgInfo['dialog_id'],
'search_msg_id' => $msgInfo['id'],
'user_at' => Carbon::parse($msgInfo['updated_at'])->format('Y-m-d H:i:s'),
'mark_unread' => $userInfo['mark_unread'],
'silence' => $userInfo['silence'],
'hide' => $userInfo['hide'],
'color' => $userInfo['color'],
'top_at' => Carbon::parse($userInfo['top_at'])->format('Y-m-d H:i:s'),
'last_at' => Carbon::parse($userInfo['last_at'])->format('Y-m-d H:i:s'),
];
}
}
return $msgs;
} catch (\Exception $e) {
Log::error('search: ' . $e->getMessage());
return [];
}
}
/**
* 根据用户ID和消息关键词搜索会话MySQL 版本主要用于未安装ZincSearch的情况
*
* @param string $userid 用户ID
* @param string $keyword 消息关键词
* @param int $from 起始位置
* @param int $size 返回结果数量
* @return array
*/
private static function searchByMysql(string $userid, string $keyword, int $from = 0, int $size = 20): array
{
$items = 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', $userid)
->where('m.bot', 0)
->whereNull('d.deleted_at')
->where('m.key', 'like', "%{$keyword}%")
->orderByDesc('m.id')
->offset($from)
->limit($size)
->get()
->all();
$msgs = [];
foreach ($items as $item) {
$msgs[] = [
'id' => $item->id,
'search_msg_id' => $item->search_msg_id,
'user_at' => Carbon::parse($item->user_at)->format('Y-m-d H:i:s'),
'mark_unread' => $item->mark_unread,
'silence' => $item->silence,
'hide' => $item->hide,
'color' => $item->color,
'top_at' => Carbon::parse($item->top_at)->format('Y-m-d H:i:s'),
'last_at' => Carbon::parse($item->last_at)->format('Y-m-d H:i:s'),
];
}
return $msgs;
}
/**
* 根据对话用户ID搜索用户信息
* @param array $dialogUserids
* @return array
*/
private static function searchUser(array $dialogUserids): array
{
if (empty($dialogUserids)) {
return [];
}
$userInfos = [];
// 构建用户查询条件
$userSearchParams = [
'query' => [
'bool' => [
'should' => []
]
],
'size' => count($dialogUserids) // 确保取到所有符合条件的记录
];
// 添加所有 dialog_userid 到查询条件
foreach ($dialogUserids as $dialogUserid) {
$userSearchParams['query']['bool']['should'][] = [
'term' => ['dialog_userid' => $dialogUserid]
];
}
// 查询用户信息
$userResult = ZincSearchBase::elasticSearch(self::$indexNameUser, $userSearchParams);
$userHits = $userResult['data']['hits']['hits'] ?? [];
// 以 dialog_userid 为键保存用户信息
foreach ($userHits as $userHit) {
$userSource = $userHit['_source'];
$userInfos[$userSource['dialog_userid']] = $userSource;
}
return $userInfos;
}
// ==============================
// 生成内容
// ==============================
/**
* 生成 dialog_userid
*
* @param WebSocketDialogUser $dialogUser
* @return string
*/
private static function generateDialogUserid(WebSocketDialogUser $dialogUser): string
{
return "{$dialogUser->dialog_id}_{$dialogUser->userid}";
}
/**
* 生成文档内容
*
* @param WebSocketDialogMsg $dialogMsg
* @param WebSocketDialogUser $dialogUser
* @return array
*/
private static function generateMsgData(WebSocketDialogMsg $dialogMsg, WebSocketDialogUser $dialogUser): array
{
return [
'_id' => self::$indexNameMsg . "_" . $dialogMsg->id . "_" . $dialogUser->userid,
'dialog_userid' => self::generateDialogUserid($dialogUser),
'to_userid' => $dialogUser->userid,
'id' => $dialogMsg->id,
'dialog_id' => $dialogMsg->dialog_id,
'dialog_type' => $dialogMsg->dialog_type,
'session_id' => $dialogMsg->session_id,
'userid' => $dialogMsg->userid,
'type' => $dialogMsg->type,
'key' => $dialogMsg->key,
'created_at' => $dialogMsg->created_at,
'updated_at' => $dialogMsg->updated_at,
];
}
private static function generateUserData(WebSocketDialogUser $dialogUser): array
{
return [
'_id' => self::$indexNameUser . "_" . $dialogUser->id,
'dialog_userid' => self::generateDialogUserid($dialogUser),
'id' => $dialogUser->id,
'dialog_id' => $dialogUser->dialog_id,
'userid' => $dialogUser->userid,
'top_at' => $dialogUser->top_at,
'last_at' => $dialogUser->last_at,
'mark_unread' => $dialogUser->mark_unread,
'silence' => $dialogUser->silence,
'hide' => $dialogUser->hide,
'color' => $dialogUser->color,
'created_at' => $dialogUser->created_at,
'updated_at' => $dialogUser->updated_at,
];
}
// ==============================
// 基本方法
// ==============================
/**
* 同步消息(建议在异步进程中使用)
*
* @param WebSocketDialogMsg $dialogMsg
* @return bool
*/
public static function sync(WebSocketDialogMsg $dialogMsg): bool
{
if (!self::ensureIndex()) {
return false;
}
if ($dialogMsg->bot) {
// 如果是机器人消息,跳过
return true;
}
try {
// 获取此会话的所有用户
$dialogUsers = WebSocketDialogUser::whereDialogId($dialogMsg->dialog_id)->get();
if ($dialogUsers->isEmpty()) {
return true;
}
$msgs = [];
$users = [];
foreach ($dialogUsers as $dialogUser) {
if (empty($dialogMsg->key)) {
// 如果消息没有关键词,跳过
continue;
}
if ($dialogUser->userid == 0) {
// 跳过系统用户
continue;
}
$msgs[] = self::generateMsgData($dialogMsg, $dialogUser);
$users[$dialogUser->id] = self::generateUserData($dialogUser);
}
if ($msgs) {
// 批量写入消息
ZincSearchBase::addDocs(self::$indexNameMsg, $msgs);
}
if ($users) {
// 批量写入用户
ZincSearchBase::addDocs(self::$indexNameUser, array_values($users));
}
return true;
} catch (\Exception $e) {
Log::error('sync: ' . $e->getMessage());
return false;
}
}
/**
* 批量同步消息(建议在异步进程中使用)
*
* @param WebSocketDialogMsg[] $dialogMsgs
* @return int 成功同步的消息数
*/
public static function batchSync($dialogMsgs): int
{
if (!self::ensureIndex()) {
return 0;
}
$count = 0;
try {
$msgs = [];
$users = [];
$userDialogs = [];
// 预处理收集所有涉及的对话ID
$dialogIds = [];
foreach ($dialogMsgs as $dialogMsg) {
$dialogIds[] = $dialogMsg->dialog_id;
}
$dialogIds = array_unique($dialogIds);
// 获取所有相关的用户-对话关系
if (!empty($dialogIds)) {
$dialogUsers = WebSocketDialogUser::whereIn('dialog_id', $dialogIds)->get();
// 按对话ID组织用户
foreach ($dialogUsers as $dialogUser) {
$userDialogs[$dialogUser->dialog_id][] = $dialogUser;
}
}
// 为每条消息准备所有相关用户的文档
foreach ($dialogMsgs as $dialogMsg) {
if (!isset($userDialogs[$dialogMsg->dialog_id])) {
// 如果该会话没有用户,跳过
continue;
}
if ($dialogMsg->bot) {
// 如果是机器人消息,跳过
continue;
}
/** @var WebSocketDialogUser $dialogUser */
foreach ($userDialogs[$dialogMsg->dialog_id] as $dialogUser) {
if (empty($dialogMsg->key)) {
// 如果消息没有关键词,跳过
continue;
}
if ($dialogUser->userid == 0) {
// 跳过系统用户
continue;
}
$msgs[] = self::generateMsgData($dialogMsg, $dialogUser);
$users[$dialogUser->id] = self::generateUserData($dialogUser);
$count++;
}
}
if ($msgs) {
// 批量写入消息
ZincSearchBase::addDocs(self::$indexNameMsg, $msgs);
}
if ($users) {
// 批量写入用户
ZincSearchBase::addDocs(self::$indexNameUser, array_values($users));
}
} catch (\Exception $e) {
Log::error('batchSync: ' . $e->getMessage());
}
return $count;
}
/**
* 同步用户(建议在异步进程中使用)
* @param WebSocketDialogUser $dialogUser
* @return bool
*/
public static function userSync(WebSocketDialogUser $dialogUser): bool
{
if (!self::ensureIndex()) {
return false;
}
$data = self::generateUserData($dialogUser);
// 生成查询用户条件
$searchParams = [
'query' => [
'bool' => [
'must' => [
['term' => ['dialog_userid' => $data['dialog_userid']]]
]
]
],
'size' => 1
];
try {
// 查询用户是否存在
$result = ZincSearchBase::elasticSearch(self::$indexNameUser, $searchParams);
$hits = $result['data']['hits']['hits'] ?? [];
// 同步用户(存在更新、不存在添加)
$result = ZincSearchBase::addDoc(self::$indexNameUser, $data);
if (!isset($result['success'])) {
return false;
}
// 用户不存在,同步消息
if (empty($hits)) {
$lastId = 0; // 上次同步的最后ID
$batchSize = 500; // 每批处理的消息数量
// 分批同步消息
do {
// 获取一批
$dialogMsgs = WebSocketDialogMsg::whereDialogId($dialogUser->dialog_id)
->where('id', '>', $lastId)
->orderBy('id')
->limit($batchSize)
->get();
if ($dialogMsgs->isEmpty()) {
break;
}
// 同步数据
ZincSearchDialogMsg::batchSync($dialogMsgs);
// 更新最后ID
$lastId = $dialogMsgs->last()->id;
} while (count($dialogMsgs) == $batchSize);
}
return true;
} catch (\Exception $e) {
Log::error('userSync: ' . $e->getMessage());
return false;
}
}
/**
* 删除(建议在异步进程中使用)
*
* @param WebSocketDialogMsg|WebSocketDialogUser|int $data
* @return int
*/
public static function delete(mixed $data): int
{
$batchSize = 500; // 每批处理的文档数量
$totalDeleted = 0; // 总共删除的文档数量
$from = 0;
// 根据数据类型生成查询条件
if ($data instanceof WebSocketDialogMsg) {
$query = [
'field' => 'id',
'term' => (string) $data->id
];
} elseif ($data instanceof WebSocketDialogUser) {
$query = [
'field' => 'dialog_userid',
'term' => self::generateDialogUserid($data),
];
} else {
$query = [
'field' => 'id',
'term' => (string) $data
];
}
try {
while (true) {
// 根据消息ID查找相关文档
$result = ZincSearchBase::advancedSearch(self::$indexNameMsg, [
'search_type' => 'term',
'query' => $query,
'from' => $from,
'max_results' => $batchSize
]);
$hits = $result['data']['hits']['hits'] ?? [];
// 如果没有更多文档,退出循环
if (empty($hits)) {
break;
}
// 删除本批次找到的所有文档
foreach ($hits as $hit) {
if (isset($hit['_id'])) {
ZincSearchBase::deleteDoc(self::$indexNameMsg, $hit['_id']);
$totalDeleted++;
}
}
// 如果返回的文档数少于批次大小,说明已经没有更多文档了
if (count($hits) < $batchSize) {
break;
}
// 移动到下一批
$from += $batchSize;
}
} catch (\Exception $e) {
Log::error('delete: ' . $e->getMessage());
}
return $totalDeleted;
}
}

View File

@@ -0,0 +1,276 @@
<?php
namespace App\Module\ZincSearch;
/**
* ZincSearch 键值存储类
*
* 使用方法:
*
* 1. 基础方法
* - 确保索引存在: ensureIndex();
* - 清空所有数据: clear();
*
* 2. 基本操作
* - 设置键值: set('site_name', '我的网站');
* - 设置复杂数据: set('site_config', ['logo' => 'logo.png', 'theme' => 'dark']);
* - 合并现有数据: set('site_config', ['footer' => '版权所有'], true);
* - 获取键值: $siteName = get('site_name');
* - 获取键值带默认值: $theme = get('theme', 'light');
* - 删除键值: delete('temporary_data');
*
* 3. 批量操作
* - 批量设置: batchSet(['user_count' => 100, 'active_users' => 50]);
* - 批量获取: $stats = batchGet(['user_count', 'active_users']);
*/
class ZincSearchKeyValue
{
/**
* 索引名称
*/
protected static string $indexName = 'keyValue';
// ==============================
// 基础方法
// ==============================
/**
* 确保索引存在
*/
public static function ensureIndex(): bool
{
if (!ZincSearchBase::indexExists(self::$indexName)) {
$mappings = [
'properties' => [
'key' => ['type' => 'keyword', 'index' => true],
'value' => ['type' => 'text', 'index' => true],
'created_at' => ['type' => 'date', 'index' => true],
'updated_at' => ['type' => 'date', 'index' => true]
]
];
$result = ZincSearchBase::createIndex(self::$indexName, $mappings);
return $result['success'] ?? false;
}
return true;
}
/**
* 清空所有键值
*
* @return bool 是否成功
*/
public static function clear(): bool
{
// 检查索引是否存在
if (!ZincSearchBase::indexExists(self::$indexName)) {
return true;
}
// 删除再重建索引
$deleteResult = ZincSearchBase::deleteIndex(self::$indexName);
if (!($deleteResult['success'] ?? false)) {
return false;
}
return self::ensureIndex();
}
// ==============================
// 基本操作
// ==============================
/**
* 设置键值
*
* @param string $key 键名
* @param mixed $value 值
* @param bool $merge 是否合并现有数据(如果值是数组)
* @return bool 是否成功
*/
public static function set(string $key, mixed $value, bool $merge = false): bool
{
if (!self::ensureIndex()) {
return false;
}
// 检查键是否已存在
if ($merge && is_array($value)) {
$existingData = self::get($key);
if (is_array($existingData)) {
$value = array_merge($existingData, $value);
}
}
// 检查是否存在相同键的文档 - 使用精确查询而不是普通搜索
$searchParams = [
'search_type' => 'term',
'query' => [
'field' => 'key',
'term' => $key
],
'from' => 0,
'max_results' => 1
];
$result = ZincSearchBase::advancedSearch(self::$indexName, $searchParams);
$docs = $result['data']['hits']['hits'] ?? [];
$now = date('c');
if (!empty($docs)) {
$docId = $docs[0]['_id'] ?? null;
if ($docId) {
// 更新现有文档
$docData = [
'key' => $key,
'value' => $value,
'updated_at' => $now
];
$updateResult = ZincSearchBase::updateDoc(self::$indexName, $docId, $docData);
return $updateResult['success'] ?? false;
}
}
// 创建新文档
$docData = [
'key' => $key,
'value' => $value,
'created_at' => $now,
'updated_at' => $now
];
$addResult = ZincSearchBase::addDoc(self::$indexName, $docData);
return $addResult['success'] ?? false;
}
/**
* 获取键值
*
* @param string $key 键名
* @param mixed $default 默认值
* @return mixed 值或默认值
*/
public static function get(string $key, mixed $default = null): mixed
{
if (!self::ensureIndex() || empty($key)) {
return $default;
}
// 精确匹配键名
$searchParams = [
'search_type' => 'term',
'query' => [
'field' => 'key',
'term' => $key
],
'from' => 0,
'max_results' => 1
];
$result = ZincSearchBase::advancedSearch(self::$indexName, $searchParams);
if (!($result['success'] ?? false)) {
return $default;
}
$hits = $result['data']['hits']['hits'] ?? [];
if (empty($hits)) {
return $default;
}
return $hits[0]['_source']['value'] ?? $default;
}
/**
* 删除键值
*
* @param string $key 键名
* @return bool 是否成功
*/
public static function delete(string $key): bool
{
if (!self::ensureIndex() || empty($key)) {
return false;
}
// 查找文档ID
$searchParams = [
'search_type' => 'term',
'query' => [
'field' => 'key',
'term' => $key
],
'from' => 0,
'max_results' => 1
];
$result = ZincSearchBase::advancedSearch(self::$indexName, $searchParams);
if (!($result['success'] ?? false)) {
return false;
}
$hits = $result['data']['hits']['hits'] ?? [];
if (empty($hits)) {
return true; // 不存在视为删除成功
}
$docId = $hits[0]['_id'] ?? null;
if (empty($docId)) {
return false;
}
$deleteResult = ZincSearchBase::deleteDoc(self::$indexName, $docId);
return $deleteResult['success'] ?? false;
}
// ==============================
// 批量操作
// ==============================
/**
* 批量设置键值对
*
* @param array $keyValues 键值对数组
* @return bool 是否全部成功
*/
public static function batchSet(array $keyValues): bool
{
if (!self::ensureIndex() || empty($keyValues)) {
return false;
}
$docs = [];
$now = date('c');
foreach ($keyValues as $key => $value) {
$docs[] = [
'key' => $key,
'value' => $value,
'created_at' => $now,
'updated_at' => $now
];
}
$result = ZincSearchBase::addDocs(self::$indexName, $docs);
return $result['success'] ?? false;
}
/**
* 批量获取键值
*
* @param array $keys 键名数组
* @return array 键值对数组
*/
public static function batchGet(array $keys): array
{
if (!self::ensureIndex() || empty($keys)) {
return [];
}
$results = [];
// 遍历查询每个键
foreach ($keys as $key) {
$results[$key] = self::get($key);
}
return $results;
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Observers;
use Hhxsv5\LaravelS\Swoole\Task\Task;
class AbstractObserver
{
/**
* @param $task
* @return void
*/
public static function taskDeliver($task)
{
if (app()->bound('swoole')) {
Task::deliver($task);
}
}
}

View File

@@ -3,9 +3,9 @@
namespace App\Observers;
use App\Models\WebSocketDialogMsg;
use App\Module\ElasticSearch\ElasticSearchUserMsg;
use App\Tasks\ZincSearchSyncTask;
class WebSocketDialogMsgObserver
class WebSocketDialogMsgObserver extends AbstractObserver
{
/**
* Handle the WebSocketDialogMsg "created" event.
@@ -15,7 +15,7 @@ class WebSocketDialogMsgObserver
*/
public function created(WebSocketDialogMsg $webSocketDialogMsg)
{
ElasticSearchUserMsg::syncMsg($webSocketDialogMsg);
self::taskDeliver(new ZincSearchSyncTask('sync', $webSocketDialogMsg->toArray()));
}
/**
@@ -26,7 +26,7 @@ class WebSocketDialogMsgObserver
*/
public function updated(WebSocketDialogMsg $webSocketDialogMsg)
{
ElasticSearchUserMsg::syncMsg($webSocketDialogMsg);
self::taskDeliver(new ZincSearchSyncTask('sync', $webSocketDialogMsg->toArray()));
}
/**
@@ -37,7 +37,7 @@ class WebSocketDialogMsgObserver
*/
public function deleted(WebSocketDialogMsg $webSocketDialogMsg)
{
ElasticSearchUserMsg::deleteMsg($webSocketDialogMsg);
self::taskDeliver(new ZincSearchSyncTask('delete', $webSocketDialogMsg->toArray()));
}
/**

View File

@@ -4,10 +4,10 @@ namespace App\Observers;
use App\Models\Deleted;
use App\Models\WebSocketDialogUser;
use App\Module\ElasticSearch\ElasticSearchUserMsg;
use App\Tasks\ZincSearchSyncTask;
use Carbon\Carbon;
class WebSocketDialogUserObserver
class WebSocketDialogUserObserver extends AbstractObserver
{
/**
* Handle the WebSocketDialogUser "created" event.
@@ -30,7 +30,7 @@ class WebSocketDialogUserObserver
}
}
Deleted::forget('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
ElasticSearchUserMsg::syncUser($webSocketDialogUser);
self::taskDeliver(new ZincSearchSyncTask('userSync', $webSocketDialogUser->toArray()));
}
/**
@@ -41,7 +41,7 @@ class WebSocketDialogUserObserver
*/
public function updated(WebSocketDialogUser $webSocketDialogUser)
{
ElasticSearchUserMsg::syncUser($webSocketDialogUser);
self::taskDeliver(new ZincSearchSyncTask('userSync', $webSocketDialogUser->toArray()));
}
/**
@@ -53,7 +53,7 @@ class WebSocketDialogUserObserver
public function deleted(WebSocketDialogUser $webSocketDialogUser)
{
Deleted::record('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
ElasticSearchUserMsg::deleteUser($webSocketDialogUser);
self::taskDeliver(new ZincSearchSyncTask('deleteUser', $webSocketDialogUser->toArray()));
}
/**

View File

@@ -139,4 +139,64 @@ class RequestContext
self::$context[$requestId] ??= [];
self::$context[$requestId] = array_merge(self::$context[$requestId], $data);
}
/** ***************************************************************************************** */
/** ***************************************************************************************** */
/** ***************************************************************************************** */
/**
* 更新请求的基本URL
*
* @param Request $request
* @return void
*/
public static function updateBaseUrl($request)
{
if ($request->path() !== 'api/system/setting') {
return;
}
$schemeAndHttpHost = $request->getSchemeAndHttpHost();
if (str_contains($schemeAndHttpHost, '127.0.0.1') || str_contains($schemeAndHttpHost, 'localhost')) {
return;
}
\Cache::forever('RequestContext::base_url', $schemeAndHttpHost);
}
/**
* 替换请求的基本URL
*
* @param string $url
* @return string
*/
public static function replaceBaseUrl(string $url): string
{
// 先提取主机部分
$pattern = '/^(https?:\/\/[^\/?#:]+(:\d+)?)/i';
if (!preg_match($pattern, $url, $matches)) {
return $url; // 如果不是有效URL直接返回
}
$schemeAndHttpHost = $matches[1] ?? '';
if (!$schemeAndHttpHost) {
return $url;
}
// 只检查主机部分是否为本地主机
if (str_contains($schemeAndHttpHost, '127.0.0.1') || str_contains($schemeAndHttpHost, 'localhost')) {
$baseUrl = \Cache::get('RequestContext::base_url');
if ($baseUrl) {
return $baseUrl . substr($url, strlen($schemeAndHttpHost));
}
}
return $url;
}
/**
* 清除基本URL缓存
*/
public static function clearBaseUrlCache(): void
{
\Cache::forget('RequestContext::base_url');
}
}

View File

@@ -1,6 +1,8 @@
<?php
namespace App\Tasks;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Models\TaskWorker;
use App\Module\Base;
use Carbon\Carbon;

View File

@@ -6,8 +6,6 @@ use App\Models\ProjectTask;
use App\Module\Base;
use Carbon\Carbon;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
class AppPushTask extends AbstractTask
{
public function __construct()

View File

@@ -1,8 +1,6 @@
<?php
namespace App\Tasks;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Models\ProjectTask;
use App\Module\Base;
use Carbon\Carbon;

View File

@@ -12,6 +12,7 @@ use App\Models\UserDepartment;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogConfig;
use App\Models\WebSocketDialogMsg;
use App\Module\Apps;
use App\Module\Base;
use App\Module\Doo;
use App\Module\Ihttp;
@@ -21,9 +22,6 @@ use Exception;
use League\HTMLToMarkdown\HtmlConverter;
use DB;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
/**
* 推送会话消息
* Class BotReceiveMsgTask
@@ -92,7 +90,7 @@ class BotReceiveMsgTask extends AbstractTask
// 提取指令
try {
$command = $this->extractCommand($msg, $botUser->isAiBot(), $this->mention);
$command = $this->extractCommand($msg, $botUser, $this->mention);
if (empty($command)) {
return;
}
@@ -208,32 +206,11 @@ class BotReceiveMsgTask extends AbstractTask
* 创建
*/
case '/newbot':
if (User::select(['users.*'])
->join('user_bots', 'users.userid', '=', 'user_bots.bot_id')
->where('users.bot', 1)
->where('user_bots.userid', $msg->userid)
->count() >= 50) {
$content = "超过最大创建数量。";
break;
}
if (strlen($array[1]) < 2 || strlen($array[1]) > 20) {
$content = "机器人名称由2-20个字符组成。";
break;
}
$data = User::botGetOrCreate("user-" . Base::generatePassword(), [
'nickname' => $array[1]
], $msg->userid);
if (empty($data)) {
$content = "创建失败。";
break;
}
$dialog = WebSocketDialog::checkUserDialog($data, $msg->userid);
if ($dialog) {
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => '/hello',
'title' => '创建成功。',
'data' => $data,
], $data->userid); // todo 未能在任务end事件来发送任务
$res = UserBot::newbot($msg->userid, $array[1]);
if (Base::isError($res)) {
$content = $res['msg'];
} else {
$data = $res['data'];
}
break;
@@ -424,9 +401,10 @@ class BotReceiveMsgTask extends AbstractTask
*/
private function botWebhookBusiness(string $command, WebSocketDialogMsg $msg, User $botUser, WebSocketDialog $dialog)
{
$serverUrl = 'http://' . env('APP_IPPR') . '.3';
$serverUrl = 'http://nginx';
$userBot = null;
$extras = [];
$replyText = null;
$errorContent = null;
if ($botUser->isAiBot($type)) {
// AI机器人
@@ -449,15 +427,28 @@ class BotReceiveMsgTask extends AbstractTask
if ($msg->msg['model_name']) {
$extras['model_name'] = $msg->msg['model_name'];
}
if (preg_match("/(.*?)(\s+|\s*[_-]\s*)(think|thinking|reasoning)\s*$/", $extras['model_name'], $match)) {
$extras['model_name'] = $match[1];
// 提取模型“思考”参数
$thinkPatterns = [
"/^(.+?)(\s+|\s*[_-]\s*)(think|thinking|reasoning)\s*$/",
"/^(.+?)\s*\(\s*(think|thinking|reasoning)\s*\)\s*$/"
];
$thinkMatch = [];
foreach ($thinkPatterns as $pattern) {
if (preg_match($pattern, $extras['model_name'], $thinkMatch)) {
break;
}
}
if ($thinkMatch && !empty($thinkMatch[1])) {
$extras['model_name'] = $thinkMatch[1];
$extras['max_tokens'] = 20000;
$extras['thinking'] = 4096;
$extras['temperature'] = 1.0;
}
// 设定会话ID
if ($dialog->session_id) {
$extras['context_key'] = 'session_' . $dialog->session_id;
}
// 设置文心一言的API密钥
if ($type === 'wenxin') {
$extras['api_key'] .= ':' . $setting['wenxin_secret'];
}
@@ -475,34 +466,17 @@ class BotReceiveMsgTask extends AbstractTask
if (in_array($this->client['platform'], ['win', 'mac', 'web']) && !Base::judgeClientVersion("0.41.11", $this->client['version'])) {
$errorContent = '当前客户端版本低所需版本≥v0.41.11)。';
}
if (!Apps::isInstalled('ai')) {
$errorContent = '应用「AI Robot」未安装';
}
if ($msg->reply_id > 0) {
$replyMsg = WebSocketDialogMsg::find($msg->reply_id);
$replyCommand = null;
if ($replyMsg) {
switch ($replyMsg->type) {
case 'text':
try {
$replyCommand = $this->extractCommand($replyMsg, true);
} catch (Exception) {
$errorContent = "引用消息解析失败。";
}
break;
case 'file':
$msgData = Base::json2array($replyMsg->getRawOriginal('msg'));
$fileResult = TextExtractor::extractFile(public_path($msgData['path']));
if (Base::isError($fileResult)) {
$errorContent = $fileResult['msg'];
} else {
$replyCommand = $fileResult['data'];
}
break;
}
}
if ($replyCommand) {
$replyCommand = $this->extractReplyCommand($msg->reply_id, $botUser);
if (Base::isError($replyCommand)) {
$errorContent = $replyCommand['msg'];
} else {
$command = <<<EOF
<quoted_content>
{$replyCommand}
{$replyCommand['data']}
</quoted_content>
The content within the above quoted_content tags is a citation.
@@ -515,6 +489,17 @@ class BotReceiveMsgTask extends AbstractTask
$webhookUrl = "{$serverUrl}/ai/chat";
} else {
// 用户机器人
if ($botUser->isUserBot() && str_starts_with($command, '/')) {
// 用户机器人不处理指令类型命令
return;
}
if ($msg->reply_id > 0) {
$replyCommand = $this->extractReplyCommand($msg->reply_id, $botUser);
if (Base::isSuccess($replyCommand)) {
$replyText = $replyCommand['data'] ?: '';
}
}
$userBot = UserBot::whereBotId($botUser->userid)->first();
$webhookUrl = $userBot?->webhook_url;
}
@@ -532,6 +517,7 @@ class BotReceiveMsgTask extends AbstractTask
try {
$data = [
'text' => $command,
'reply_text' => $replyText,
'token' => User::generateToken($botUser),
'dialog_id' => $dialog->id,
'dialog_type' => $dialog->type,
@@ -593,12 +579,12 @@ class BotReceiveMsgTask extends AbstractTask
/**
* 提取消息指令(提取消息内容)
* @param WebSocketDialogMsg $msg
* @param bool $isAiBot
* @param User $botUser
* @param bool $mention
* @return string
* @throws Exception
*/
private function extractCommand(WebSocketDialogMsg $msg, bool $isAiBot = false, bool $mention = false)
private function extractCommand(WebSocketDialogMsg $msg, User $botUser, bool $mention = false)
{
if ($msg->type !== 'text') {
return '';
@@ -615,80 +601,113 @@ class BotReceiveMsgTask extends AbstractTask
}
return $command;
}
if (!$isAiBot) {
if ($botUser->isAiBot()) {
// AI 机器人
$contents = [];
if (preg_match_all("/<span class=\"mention task\" data-id=\"(\d+)\">(.*?)<\/span>/", $original, $match)) {
// 任务
$taskIds = Base::newIntval($match[1]);
foreach ($taskIds as $index => $taskId) {
$taskInfo = ProjectTask::with(['content'])->whereId($taskId)->first();
if (!$taskInfo) {
throw new Exception("任务不存在或已被删除");
}
$taskName = addslashes($taskInfo->name) . " (ID:{$taskId})";
$taskContext = implode("\n", $taskInfo->AIContext());
$contents[] = "<task_content path=\"{$taskName}\">\n{$taskContext}\n</task_content>";
$original = str_replace($match[0][$index], "'{$taskName}' (see below for task_content tag)", $original);
}
}
if (preg_match_all("/<a class=\"mention ([^'\"]*)\" href=\"([^\"']+?)\"[^>]*?>[~%]([^>]*)<\/a>/", $original, $match)) {
// 文件、报告
$urlPaths = $match[2];
foreach ($urlPaths as $index => $urlPath) {
$pathTag = null;
$pathName = null;
$pathContent = null;
// 文件
if (preg_match("/single\/file\/(.*?)$/", $urlPath, $fileMatch)) {
$fileInfo = FileContent::idOrCodeToContent($fileMatch[1]);
if (!$fileInfo || !isset($fileInfo->content['url'])) {
throw new Exception("文件不存在或已被删除");
}
$urlPath = public_path($fileInfo->content['url']);
if (!file_exists($urlPath)) {
throw new Exception("文件不存在或已被删除");
}
$fileResult = TextExtractor::extractFile($urlPath);
if (Base::isError($fileResult)) {
throw new Exception("文件读取失败:" . $fileResult['msg']);
}
$pathTag = "file_content";
$pathName = addslashes($match[3][$index]) . " (ID:{$fileInfo->id})";
$pathContent = $fileResult['data'];
}
// 报告
elseif (preg_match("/single\/report\/detail\/(.*?)$/", $urlPath, $reportMatch)) {
$reportInfo = Report::idOrCodeToContent($reportMatch[1]);
if (!$reportInfo) {
throw new Exception("报告不存在或已被删除");
}
$pathTag = "report_content";
$pathName = addslashes($match[3][$index]) . " (ID:{$reportInfo->id})";
$pathContent = $reportInfo->content;
}
if ($pathTag) {
$contents[] = "<{$pathTag} path=\"{$pathName}\">\n{$pathContent}\n</{$pathTag}>";
$original = str_replace($match[0][$index], "'{$pathName}' (see below for {$pathTag} tag)", $original);
}
}
}
$original = Base::html2markdown($original);
if ($contents) {
// 添加tag内容
$original .= "\n\n" . implode("\n\n", $contents);
}
return $original;
} elseif ($botUser->isUserBot()) {
// 用户机器人
return Base::html2markdown($original);
} else {
// 其他机器人(系统)
return trim(strip_tags($original));
}
}
$contents = [];
// 任务
if (preg_match_all("/<span class=\"mention task\" data-id=\"(\d+)\">(.*?)<\/span>/", $original, $match)) {
$taskIds = Base::newIntval($match[1]);
foreach ($taskIds as $index => $taskId) {
$taskInfo = ProjectTask::with(['content'])->whereId($taskId)->first();
if (!$taskInfo) {
throw new Exception("任务不存在或已被删除");
}
$taskName = addslashes($taskInfo->name) . " (ID:{$taskId})";
$taskContext = implode("\n", $taskInfo->AIContext());
$contents[] = "<task_content path=\"{$taskName}\">\n{$taskContext}\n</task_content>";
$original = str_replace($match[0][$index], "'{$taskName}' (see below for task_content tag)", $original);
/**
* 提取回复消息指令
* @param $id
* @param User $botUser
* @return array
*/
private function extractReplyCommand($id, User $botUser)
{
$replyMsg = WebSocketDialogMsg::find($id);
$replyCommand = null;
if ($replyMsg) {
switch ($replyMsg->type) {
case 'text':
try {
$replyCommand = $this->extractCommand($replyMsg, $botUser);
} catch (Exception) {
return Base::retError('error', "引用消息解析失败。");
}
break;
case 'file':
if ($botUser->isAiBot()) {
$msgData = Base::json2array($replyMsg->getRawOriginal('msg'));
$fileResult = TextExtractor::extractFile(public_path($msgData['path']));
if (Base::isError($fileResult)) {
return Base::retError('error', $fileResult['msg']);
} else {
$replyCommand = $fileResult['data'];
}
}
break;
}
}
// 文件、报告
if (preg_match_all("/<a class=\"mention ([^'\"]*)\" href=\"([^\"']+?)\"[^>]*?>[~%]([^>]*)<\/a>/", $original, $match)) {
$urlPaths = $match[2];
foreach ($urlPaths as $index => $urlPath) {
$pathTag = null;
$pathName = null;
$pathContent = null;
// 文件
if (preg_match("/single\/file\/(.*?)$/", $urlPath, $fileMatch)) {
$fileInfo = FileContent::idOrCodeToContent($fileMatch[1]);
if (!$fileInfo || !isset($fileInfo->content['url'])) {
throw new Exception("文件不存在或已被删除");
}
$urlPath = public_path($fileInfo->content['url']);
if (!file_exists($urlPath)) {
throw new Exception("文件不存在或已被删除");
}
$fileResult = TextExtractor::extractFile($urlPath);
if (Base::isError($fileResult)) {
throw new Exception("文件读取失败:" . $fileResult['msg']);
}
$pathTag = "file_content";
$pathName = addslashes($match[3][$index]) . " (ID:{$fileInfo->id})";
$pathContent = $fileResult['data'];
}
// 报告
elseif (preg_match("/single\/report\/detail\/(.*?)$/", $urlPath, $reportMatch)) {
$reportInfo = Report::idOrCodeToContent($reportMatch[1]);
if (!$reportInfo) {
throw new Exception("报告不存在或已被删除");
}
$pathTag = "report_content";
$pathName = addslashes($match[3][$index]) . " (ID:{$reportInfo->id})";
$pathContent = $reportInfo->content;
}
if ($pathTag) {
$contents[] = "<{$pathTag} path=\"{$pathName}\">\n{$pathContent}\n</{$pathTag}>";
$original = str_replace($match[0][$index], "'{$pathName}' (see below for {$pathTag} tag)", $original);
}
}
}
if ($msg->msg['type'] !== 'md') {
// 转换为Markdown
try {
$converter = new HtmlConverter();
$original = $converter->convert($original);
} catch (\Exception) {
throw new Exception("Failed to convert HTML to Markdown");
}
}
if ($contents) {
// 添加tag内容
$original .= "\n\n" . implode("\n\n", $contents);
}
return $original ?: '';
return Base::retSuccess('success', $replyCommand);
}
/**

View File

@@ -13,8 +13,6 @@ use App\Module\Timer;
use Cache;
use Carbon\Carbon;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
class CheckinRemindTask extends AbstractTask
{
public function __construct()

View File

@@ -9,8 +9,6 @@ use Carbon\Carbon;
use App\Models\WebSocketDialogMsg;
use Illuminate\Support\Facades\Cache;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
class CloseMeetingRoomTask extends AbstractTask
{
public function __construct()

View File

@@ -2,8 +2,6 @@
namespace App\Tasks;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Models\UserBot;
use App\Models\WebSocketDialogMsg;
use Carbon\Carbon;

View File

@@ -5,6 +5,7 @@ namespace App\Tasks;
use App\Models\File;
use App\Models\TaskWorker;
use App\Models\Tmp;
use App\Models\UserDevice;
use App\Models\WebSocketTmpMsg;
use App\Module\Base;
use Carbon\Carbon;
@@ -33,89 +34,75 @@ class DeleteTmpTask extends AbstractTask
public function start()
{
switch ($this->data) {
/**
* 表pre_tmp_msgs
*/
case 'wg_tmp_msgs':
{
WebSocketTmpMsg::where('created_at', '<', Carbon::now()->subHours($this->hours))
->orderBy('id')
->chunk(500, function ($msgs) {
/** @var WebSocketTmpMsg $msg */
foreach ($msgs as $msg) {
$msg->delete();
}
});
}
break;
/**
* 表pre_tmp
*/
case 'tmp':
{
Tmp::where('created_at', '<', Carbon::now()->subHours($this->hours))
->orderBy('id')
->chunk(500, function ($tmps) {
/** @var Tmp $tmp */
foreach ($tmps as $tmp) {
$tmp->delete();
}
});
}
break;
/**
* 表pre_task_worker
*/
case 'task_worker':
{
TaskWorker::onlyTrashed()
->where('deleted_at', '<', Carbon::now()->subHours($this->hours))
->orderBy('id')
->forceDelete();
}
break;
/**
* 表pre_file
*/
case 'file':
{
$day = intval(env("AUTO_EMPTY_FILE_RECYCLE", 365));
if ($day <= 0) {
return;
}
File::onlyTrashed()
->where('deleted_at', '<', Carbon::now()->subHours($day))
->orderBy('id')
->chunk(500, function ($files) {
/** @var File $file */
foreach ($files as $file) {
$file->forceDeleteFile();
}
});
}
break;
/**
* tmp_file 删除临时文件
*/
case 'tmp_file':
{
$day = intval(env("AUTO_EMPTY_TEMP_FILE", 30));
if ($day <= 0) {
return;
}
$files = Base::recursiveFiles(public_path('uploads/tmp'));
foreach ($files as $file) {
$time = @filemtime($file);
if ($time && $time < time() - 3600 * 24 * $day) {
unlink($file);
case 'tmp_msgs':
WebSocketTmpMsg::where('created_at', '<', Carbon::now()->subHours($this->hours))
->orderBy('id')
->chunk(500, function ($msgs) {
/** @var WebSocketTmpMsg $msg */
foreach ($msgs as $msg) {
$msg->delete();
}
});
break;
case 'tmp':
Tmp::where('created_at', '<', Carbon::now()->subHours($this->hours))
->orderBy('id')
->chunk(500, function ($tmps) {
/** @var Tmp $tmp */
foreach ($tmps as $tmp) {
$tmp->delete();
}
});
break;
case 'task_worker':
TaskWorker::onlyTrashed()
->where('deleted_at', '<', Carbon::now()->subHours($this->hours))
->orderBy('id')
->forceDelete();
break;
case 'file':
$day = intval(env("AUTO_EMPTY_FILE_RECYCLE", 365));
if ($day <= 0) {
return;
}
File::onlyTrashed()
->where('deleted_at', '<', Carbon::now()->subHours($day))
->orderBy('id')
->chunk(500, function ($files) {
/** @var File $file */
foreach ($files as $file) {
$file->forceDeleteFile();
}
});
break;
case 'tmp_file':
$day = intval(env("AUTO_EMPTY_TEMP_FILE", 30));
if ($day <= 0) {
return;
}
$files = Base::recursiveFiles(public_path('uploads/tmp'));
foreach ($files as $file) {
$time = @filemtime($file);
if ($time && $time < time() - 3600 * 24 * $day) {
unlink($file);
}
}
break;
case 'user_device':
UserDevice::where('expired_at', '<', Carbon::now()->subHours($this->hours))
->orderBy('id')
->chunk(500, function ($devices) {
/** @var UserDevice $device */
foreach ($devices as $device) {
UserDevice::forget($device);
}
});
break;
}
}

View File

@@ -1,39 +0,0 @@
<?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,8 +1,6 @@
<?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

@@ -7,9 +7,6 @@ use App\Module\Extranet;
use Cache;
use Carbon\Carbon;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
/**
* 获取笑话、心灵鸡汤
*

View File

@@ -4,9 +4,6 @@ namespace App\Tasks;
use App\Models\WebSocket;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
/**
* 上线、离线通知
* Class LineTask

View File

@@ -8,9 +8,6 @@ use App\Models\ProjectTask;
use App\Models\ProjectTaskUser;
use Carbon\Carbon;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
/**
* 任务重复周期
*/
@@ -29,6 +26,10 @@ class LoopTask extends AbstractTask
])->chunkById(100, function ($list) {
/** @var ProjectTask $item */
foreach ($list as $item) {
if ($item->parent_id > 0) {
// 如果是子任务则不处理
continue;
}
try {
$task = $item->copyTask();
// 工作流
@@ -63,6 +64,18 @@ class LoopTask extends AbstractTask
$task->start_at = Carbon::parse($task->loop_at);
$task->end_at = $task->start_at->clone()->addSeconds($diffSecond);
}
// 处理子任务
$subTasks = ProjectTask::whereParentId($item->id)->get();
if (!$subTasks->isEmpty()) {
foreach ($subTasks as $subTask) {
$newSubTask = $subTask->copyTask();
$newSubTask->parent_id = $task->id;
$newSubTask->start_at = $task->start_at;
$newSubTask->end_at = $task->end_at;
$newSubTask->save();
}
}
//
$task->refreshLoop(true);
$task->addLog("创建任务来自周期任务ID{$item->id}", [], $task->userid);
// 清空旧周期

View File

@@ -1,8 +1,6 @@
<?php
namespace App\Tasks;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Models\WebSocket;
use App\Models\WebSocketTmpMsg;
use App\Module\Base;

View File

@@ -1,8 +1,6 @@
<?php
namespace App\Tasks;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Models\UmengAlias;
use App\Module\Base;

View File

@@ -10,8 +10,6 @@ use Carbon\Carbon;
use App\Models\WebSocketDialogMsg;
use Illuminate\Support\Facades\Cache;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
class UnclaimedTaskRemindTask extends AbstractTask
{
public function __construct()

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Tasks;
use App\Models\WebSocketDialogSession;
use App\Module\Base;
use App\Module\Extranet;
/**
* 通过AI接口更新对话标题
*/
class UpdateSessionTitleViaAiTask extends AbstractTask
{
public function __construct($sessionId, $msgText)
{
parent::__construct();
$this->sessionId = $sessionId;
$this->msgText = $msgText;
}
public function start()
{
if (empty($this->sessionId) || empty($this->msgText)) {
return;
}
$session = WebSocketDialogSession::whereId($this->sessionId)->first();
if (!$session) {
return;
}
$res = Extranet::openAIGenerateTitle($this->msgText);
if (Base::isError($res)) {
return;
}
$newTitle = $res['data'];
if ($newTitle && $newTitle != $session->title) {
$session->title = Base::cutStr($newTitle, 100);
$session->save();
}
}
public function end()
{
}
}

View File

@@ -2,8 +2,6 @@
namespace App\Tasks;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Models\User;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;

View File

@@ -0,0 +1,88 @@
<?php
namespace App\Tasks;
use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialogUser;
use App\Module\Apps;
use App\Module\ZincSearch\ZincSearchDialogMsg;
use Carbon\Carbon;
use Illuminate\Support\Facades\Cache;
/**
* 同步聊天数据到ZincSearch
*/
class ZincSearchSyncTask extends AbstractTask
{
private $action;
private $data;
public function __construct($action = null, $data = null)
{
parent::__construct(...func_get_args());
$this->action = $action;
$this->data = $data;
}
public function start()
{
if (!Apps::isInstalled("search")) {
// 如果没有安装搜索模块,则不执行
return;
}
switch ($this->action) {
case 'sync':
// 同步消息数据
ZincSearchDialogMsg::sync(WebSocketDialogMsg::fillInstance($this->data));
break;
case 'delete':
// 删除消息数据
ZincSearchDialogMsg::delete(WebSocketDialogMsg::fillInstance($this->data));
break;
case 'userSync':
// 同步用户数据
ZincSearchDialogMsg::userSync(WebSocketDialogUser::fillInstance($this->data));
break;
case 'deleteUser':
// 删除用户数据
ZincSearchDialogMsg::delete(WebSocketDialogUser::fillInstance($this->data));
break;
default:
// 增量更新
$this->incrementalUpdate();
break;
}
}
/**
* 增量更新
* @return void
*/
private function incrementalUpdate()
{
// 120分钟执行一次
$time = intval(Cache::get("ZincSearchSyncTask:Time"));
if (time() - $time < 120 * 60) {
return;
}
// 执行开始120分钟后缓存标记失效
Cache::put("ZincSearchSyncTask:Time", time(), Carbon::now()->addMinutes(120));
// 开始执行同步
@shell_exec("php /var/www/artisan zinc:sync-user-msg --i");
// 执行完成5分钟后缓存标记失效5分钟任务可重复执行
Cache::put("ZincSearchSyncTask:Time", time(), Carbon::now()->addMinutes(5));
}
public function end()
{
}
}

View File

@@ -155,8 +155,8 @@ install() {
cat >${sitePath}/ssl.conf <<EOF
server_name ${domain};
listen 443 ssl;
ssl_certificate /etc/nginx/conf.d/site/ssl/${domain}.crt;
ssl_certificate_key /etc/nginx/conf.d/site/ssl/${domain}.key;
ssl_certificate /var/www/docker/nginx/site/ssl/${domain}.crt;
ssl_certificate_key /var/www/docker/nginx/site/ssl/${domain}.key;
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
ssl_prefer_server_ciphers on;

4
bin/version.js vendored

File diff suppressed because one or more lines are too long

790
cmd

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,7 @@
"php": "^8.0",
"ext-curl": "*",
"ext-dom": "*",
"ext-ffi": "*",
"ext-fileinfo": "*",
"ext-gd": "*",
"ext-imagick": "*",
@@ -20,7 +21,6 @@
"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,6 +34,7 @@
"league/html-to-markdown": "^5.1",
"maatwebsite/excel": "^3.1.31",
"madnest/madzipper": "^v1.1.0",
"matomo/device-detector": "^6.4",
"mews/captcha": "^3.2.6",
"orangehill/iseed": "^3.0.1",
"overtrue/pinyin": "^4.0",

797
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,7 @@ class AddProjectsPersonal extends Migration
});
if ($isAdd) {
// 更新数据
\App\Models\Project::whereName('个人项目')->chunkById(100, function ($lists) {
\App\Models\Project::where('name','like', '%个人项目%')->chunkById(100, function ($lists) {
/** @var \App\Models\Project $item */
foreach ($lists as $item) {
if ($item->desc == '注册时系统自动创建项目,你可以自由删除。') {

View File

@@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateUserDevicesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasTable('user_devices'))
return;
Schema::create('user_devices', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('userid')->index()->nullable()->default(0)->comment('会员ID');
$table->string('hash')->index()->nullable()->default('')->comment('TOKEN MD5');
$table->longText('detail')->nullable()->comment('详细信息');
$table->timestamp('expired_at')->nullable()->comment('过期时间');
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('user_devices');
}
}

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddDeviceHashToUmengAliasTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('umeng_alias', function (Blueprint $table) {
if (!Schema::hasColumn('umeng_alias', 'device_hash')) {
$table->string('device_hash')->index()->nullable()->after('device')->comment('设备哈希值用于关联UserDevice表');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('umeng_alias', function (Blueprint $table) {
$table->dropColumn('device_hash');
});
}
}

View File

@@ -1,17 +1,18 @@
services:
php:
container_name: "dootask-php-${APP_ID}"
image: "kuaifan/php:swoole-8.0.rc18"
image: "kuaifan/php:swoole-8.0.rc20"
shm_size: 2G
ulimits:
core:
soft: 0
hard: 0
volumes:
- shared_data:/usr/share/dootask
- ./docker/crontab/crontab.conf:/etc/supervisor/conf.d/crontab.conf
- ./docker/php/php.conf:/etc/supervisor/conf.d/php.conf
- ./docker/php/php.ini:/usr/local/etc/php/php.ini
- ./docker/log/supervisor:/var/log/supervisor
- ./docker/logs/supervisor:/var/log/supervisor
- ./:/var/www
environment:
LANG: "C.UTF-8"
@@ -21,13 +22,18 @@ services:
MYSQL_DB_NAME: "${DB_DATABASE}"
MYSQL_USERNAME: "${DB_USERNAME}"
MYSQL_PASSWORD: "${DB_PASSWORD}"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:${LARAVELS_LISTEN_PORT}/health"]
interval: 5s
timeout: 5s
retries: 5
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.2"
- extnetwork
depends_on:
- mariadb
- redis
- es
mariadb:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
nginx:
@@ -35,30 +41,34 @@ services:
image: "nginx:alpine"
ports:
- "${APP_PORT}:80"
- "${APP_SSL_PORT:-}:443"
- "${APP_SSL_PORT:-0}:443"
volumes:
- ./docker/nginx:/etc/nginx/conf.d
- ./public:/var/www/public
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
- ./:/var/www
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 5s
timeout: 5s
retries: 5
depends_on:
php:
condition: service_healthy
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.3"
links:
- php
- office
- fileview
- drawio-webapp
- drawio-export
- minder
- okr
- ai
- extnetwork
restart: unless-stopped
redis:
container_name: "dootask-redis-${APP_ID}"
image: "redis:alpine"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.4"
- extnetwork
restart: unless-stopped
mariadb:
@@ -74,176 +84,40 @@ services:
MYSQL_DATABASE: "${DB_DATABASE}"
MYSQL_USER: "${DB_USERNAME}"
MYSQL_PASSWORD: "${DB_PASSWORD}"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "${DB_USERNAME}", "-p${DB_PASSWORD}"]
interval: 5s
timeout: 5s
retries: 5
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.5"
- extnetwork
restart: unless-stopped
office:
container_name: "dootask-office-${APP_ID}"
image: "onlyoffice/documentserver:8.2.2.1"
appstore:
container_name: "dootask-appstore-${APP_ID}"
privileged: true
image: "dootask/appstore:0.0.9"
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
- ./docker/office/resources/presentationeditor/mobile/css/923.f9cf19de1a25c2e7bf8b.css:/var/www/onlyoffice/documentserver/web-apps/apps/presentationeditor/mobile/css/923.f9cf19de1a25c2e7bf8b.css
- ./docker/office/resources/spreadsheeteditor/main/resources/css/app.css:/var/www/onlyoffice/documentserver/web-apps/apps/spreadsheeteditor/main/resources/css/app.css
- ./docker/office/resources/spreadsheeteditor/mobile/css/611.1bef49f175e18fc085db.css:/var/www/onlyoffice/documentserver/web-apps/apps/spreadsheeteditor/mobile/css/611.1bef49f175e18fc085db.css
- shared_data:/usr/share/dootask
- /var/run/docker.sock:/var/run/docker.sock
- ./:/var/www
environment:
JWT_SECRET: ${APP_KEY}
HOST_PWD: "${PWD}"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 5s
timeout: 5s
retries: 5
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.6"
restart: unless-stopped
fileview:
container_name: "dootask-fileview-${APP_ID}"
image: "kuaifan/fileview:4.2.0-SNAPSHOT-RC25"
environment:
KK_CONTEXT_PATH: "/fileview"
KK_OFFICE_PREVIEW_SWITCH_DISABLED: true
KK_FILE_UPLOAD_ENABLED: true
KK_MEDIA: "mp3,wav,mp4,mov,avi,wmv"
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.7"
restart: unless-stopped
drawio-webapp:
container_name: "dootask-drawio-webapp-${APP_ID}"
image: "jgraph/drawio:24.7.17"
volumes:
- ./docker/drawio/webapp/index.html:/usr/local/tomcat/webapps/draw/index.html
- ./docker/drawio/webapp/stencils:/usr/local/tomcat/webapps/draw/stencils
- ./docker/drawio/webapp/js/app.min.js:/usr/local/tomcat/webapps/draw/js/app.min.js
- ./docker/drawio/webapp/js/croppie/croppie.min.css:/usr/local/tomcat/webapps/draw/js/croppie/croppie.min.css
- ./docker/drawio/webapp/js/diagramly/ElectronApp.js:/usr/local/tomcat/webapps/draw/js/diagramly/ElectronApp.js
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.8"
depends_on:
- drawio-export
restart: unless-stopped
drawio-export:
container_name: "dootask-drawio-export-${APP_ID}"
image: "kuaifan/export-server:0.0.1"
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.9"
volumes:
- ./docker/drawio/export/fonts:/usr/share/fonts/drawio
restart: unless-stopped
minder:
container_name: "dootask-minder-${APP_ID}"
image: "kuaifan/minder:0.1.3"
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.10"
restart: unless-stopped
approve:
container_name: "dootask-approve-${APP_ID}"
image: "kuaifan/dooapprove:0.1.5"
environment:
TZ: "${TIMEZONE:-PRC}"
MYSQL_HOST: "${DB_HOST}"
MYSQL_PORT: "${DB_PORT}"
MYSQL_DBNAME: "${DB_DATABASE}"
MYSQL_USERNAME: "${DB_USERNAME}"
MYSQL_PASSWORD: "${DB_PASSWORD}"
MYSQL_Prefix: "${DB_PREFIX}approve_"
DEMO_DATA: true
KEY: ${APP_KEY}
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.11"
depends_on:
- mariadb
restart: unless-stopped
ai:
container_name: "dootask-ai-${APP_ID}"
image: "kuaifan/dootask-ai:0.3.5"
environment:
REDIS_HOST: "${REDIS_HOST}"
REDIS_PORT: "${REDIS_PORT}"
TIMEOUT: 600
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.12"
depends_on:
- redis
restart: unless-stopped
okr:
container_name: "dootask-okr-${APP_ID}"
image: "kuaifan/doookr:0.4.5"
environment:
TZ: "${TIMEZONE:-PRC}"
DOO_TASK_URL: "http://${APP_IPPR}.3"
MYSQL_HOST: "${DB_HOST}"
MYSQL_PORT: "${DB_PORT}"
MYSQL_DBNAME: "${DB_DATABASE}"
MYSQL_USERNAME: "${DB_USERNAME}"
MYSQL_PASSWORD: "${DB_PASSWORD}"
MYSQL_PREFIX: "${DB_PREFIX}"
DEMO_DATA: true
KEY: ${APP_KEY}
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.13"
depends_on:
- mariadb
restart: unless-stopped
face:
container_name: "dootask-face-${APP_ID}"
image: "hitosea2020/dooface:0.0.1"
ports:
- "7788:7788"
environment:
TZ: "${TIMEZONE:-PRC}"
STORAGE: mysql
MYSQL_HOST: "${DB_HOST}"
MYSQL_PORT: "${DB_PORT}"
MYSQL_USERNAME: "${DB_USERNAME}"
MYSQL_PASSWORD: "${DB_PASSWORD}"
MYSQL_DB_NAME: "${DB_DATABASE}"
DB_PREFIX: "${DB_PREFIX}"
REPORT_API: "http://${APP_IPPR}.3:80/api/public/checkin/report"
depends_on:
- mariadb
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.14"
restart: unless-stopped
es:
container_name: "dootask-es-${APP_ID}"
image: "elasticsearch:8.17.2"
volumes:
- ./docker/es/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
- ./docker/es/data:/usr/share/elasticsearch/data
environment:
discovery.type: single-node
xpack.security.enabled: false
ES_JAVA_OPTS: "-Xms1g -Xmx1g"
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.15"
- extnetwork
restart: unless-stopped
networks:
extnetwork:
name: "dootask-networks-${APP_ID}"
ipam:
config:
- subnet: "${APP_IPPR}.0/24"
gateway: "${APP_IPPR}.1"
volumes:
shared_data:
name: "dootask-shared-data-${APP_ID}"
redis_data:
name: "dootask-redis-data-${APP_ID}"

5
docker/appstore/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
apps/*
config/*
log/*
temp/*
!.gitkeep

View File

View File

View File

View File

@@ -1 +0,0 @@

View File

@@ -1,3 +0,0 @@
# Change
diff https://github.com/jgraph/drawio/tree/acd938b1e42cff3be3b629e6239cdec9a9baddcc

View File

@@ -1,468 +0,0 @@
<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=5" ><![endif]-->
<!DOCTYPE html>
<html>
<head>
<title>Flowchart Maker &amp; Online Diagram Software</title>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="Description" content="draw.io is free online diagram software for making flowcharts, process diagrams, org charts, UML, ER and network diagrams">
<meta name="Keywords" content="drawio, diagram, online, flow chart, flowchart maker, uml, erd">
<meta itemprop="name" content="draw.io - free flowchart maker and diagrams online">
<meta itemprop="description" content="draw.io is a free online diagramming application and flowchart maker . You can use it to create UML, entity relationship,
org charts, BPMN and BPM, database schema and networks. Also possible are telecommunication network, workflow, flowcharts, maps overlays and GIS, electronic
circuit and social network diagrams.">
<meta itemprop="image" content="https://lh4.googleusercontent.com/-cLKEldMbT_E/Tx8qXDuw6eI/AAAAAAAAAAs/Ke0pnlk8Gpg/w500-h344-k/BPMN%2Bdiagram%2Brc2f.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="msapplication-config" content="images/browserconfig.xml">
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#d89000">
<script id="geBootstrap" type="text/javascript">
window.EXPORT_URL = window.location.origin + "/drawio/export/";
window.DRAWIO_LIGHTBOX_URL = window.location.origin + "/drawio/webapp";
setInterval(function() {window.ICONSEARCH_PATH = window.location.origin + "/drawio/iconsearch";}, 1000)
/**
* URL Parameters and protocol description are here:
*
* https://www.drawio.com/doc/faq/supported-url-parameters
*
* Parameters for developers:
*
* - dev=1: For developers only
* - test=1: For developers only
* - export=URL for export: For developers only
* - ignoremime=1: For developers only (see DriveClient.js). Use Cmd-S to override mime.
* - createindex=1: For developers only (see etc/build/README)
* - filesupport=0: For developers only (see Editor.js in core)
* - savesidebar=1: For developers only (see Sidebar.js)
* - pages=1: For developers only (see Pages.js)
* - lic=email: For developers only (see LicenseServlet.java)
* --
* - networkshapes=1: For testing network shapes (temporary)
*/
var urlParams = (function()
{
var result = new Object();
var params = window.location.search.slice(1).split('&');
for (var i = 0; i < params.length; i++)
{
var idx = params[i].indexOf('=');
if (idx > 0)
{
result[params[i].substring(0, idx)] = params[i].substring(idx + 1);
}
}
return result;
})();
// Forces CDN caches by passing URL parameters via URL hash
if (window.location.hash != null && window.location.hash.substring(0, 2) == '#P')
{
try
{
urlParams = JSON.parse(decodeURIComponent(window.location.hash.substring(2)));
if (urlParams.hash != null)
{
window.location.hash = urlParams.hash;
}
}
catch (e)
{
// ignore
}
}
// Global variable for desktop
var mxIsElectron = navigator.userAgent != null && navigator.userAgent.toLowerCase().indexOf(' electron/') > -1 &&
navigator.userAgent.indexOf(' draw.io/') > -1;
// Redirects page if required
if (urlParams['dev'] != '1')
{
(function()
{
var proto = window.location.protocol;
if (!mxIsElectron)
{
var host = window.location.host;
// Redirects apex, drive and rt to www
if (host === 'draw.io' || host === 'rt.draw.io' || host === 'drive.draw.io')
{
host = 'www.draw.io';
}
var href = proto + '//' + host + window.location.href.substring(
window.location.protocol.length +
window.location.host.length + 2);
// Redirects if href changes
if (href != window.location.href)
{
window.location.href = href;
}
}
})();
}
/**
* Adds meta tag to the page.
*/
function mxmeta(name, content, httpEquiv)
{
try
{
var s = document.createElement('meta');
if (name != null)
{
s.setAttribute('name', name);
}
s.setAttribute('content', content);
if (httpEquiv != null)
{
s.setAttribute('http-equiv', httpEquiv);
}
var t = document.getElementsByTagName('meta')[0];
t.parentNode.insertBefore(s, t);
}
catch (e)
{
// ignore
}
};
/**
* Synchronously adds scripts to the page.
*/
function mxscript(src, onLoad, id, dataAppKey, noWrite, onError)
{
var defer = onLoad == null && !noWrite;
if ((urlParams['dev'] != '1' && typeof document.createElement('canvas').getContext === "function") ||
onLoad != null || noWrite)
{
var s = document.createElement('script');
s.setAttribute('type', 'text/javascript');
s.setAttribute('defer', 'true');
s.setAttribute('src', src);
if (id != null)
{
s.setAttribute('id', id);
}
if (dataAppKey != null)
{
s.setAttribute('data-app-key', dataAppKey);
}
if (onLoad != null)
{
var r = false;
s.onload = s.onreadystatechange = function()
{
if (!r && (!this.readyState || this.readyState == 'complete'))
{
r = true;
onLoad();
}
};
}
if (onError != null)
{
s.onerror = function(e)
{
onError('Failed to load ' + src, e);
};
}
var t = document.getElementsByTagName('script')[0];
if (t != null)
{
t.parentNode.insertBefore(s, t);
}
}
else
{
document.write('<script src="' + src + '"' + ((id != null) ? ' id="' + id +'" ' : '') +
((dataAppKey != null) ? ' data-app-key="' + dataAppKey +'" ' : '') + '></scr' + 'ipt>');
}
};
/**
* Asynchronously adds scripts to the page.
*/
function mxinclude(src)
{
var g = document.createElement('script');
g.type = 'text/javascript';
g.async = true;
g.src = src;
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(g, s);
};
/**
* Adds meta tags with application name (depends on offline URL parameter)
*/
(function()
{
var name = 'diagrams.net';
mxmeta('apple-mobile-web-app-title', name);
mxmeta('application-name', name);
if (mxIsElectron)
{
mxmeta(null, 'default-src \'self\'; script-src \'self\' \'sha256-6g514VrT/cZFZltSaKxIVNFF46+MFaTSDTPB8WfYK+c=\'; connect-src \'self\' https://*.draw.io https://*.diagrams.net https://fonts.googleapis.com https://fonts.gstatic.com; img-src * data:; media-src *; font-src *; frame-src \'none\'; style-src \'self\' \'unsafe-inline\' https://fonts.googleapis.com; base-uri \'none\';child-src \'self\';object-src \'none\';', 'Content-Security-Policy');
}
})();
// Checks for local storage
var isLocalStorage = false;
try
{
isLocalStorage = urlParams['local'] != '1' && typeof(localStorage) != 'undefined';
}
catch (e)
{
// ignored
}
var mxScriptsLoaded = false, mxWinLoaded = false;
function checkAllLoaded()
{
if (mxScriptsLoaded && mxWinLoaded)
{
App.main();
}
};
var t0 = new Date();
// Changes paths for local development environment
if (urlParams['dev'] == '1')
{
// Used to request grapheditor/mxgraph sources in dev mode
var mxDevUrl = '';
// Used to request draw.io sources in dev mode
var drawDevUrl = '';
var geBasePath = 'js/grapheditor';
var mxBasePath = 'mxgraph/src';
if (document.location.protocol == 'file:')
{
// Forces includes for dev environment in node.js
mxForceIncludes = true;
}
mxForceIncludes = false;
mxscript(drawDevUrl + 'js/PreConfig.js');
mxscript(drawDevUrl + 'js/diagramly/Init.js');
mxscript(geBasePath + '/Init.js');
mxscript(mxBasePath + '/mxClient.js');
// Adds all JS code that depends on mxClient. This indirection via Devel.js is
// required in some browsers to make sure mxClient.js (and the files that it
// loads asynchronously) are available when the code loaded in Devel.js runs.
mxscript(drawDevUrl + 'js/diagramly/Devel.js');
// Electron
if (mxIsElectron)
{
mxscript('js/diagramly/DesktopLibrary.js');
mxscript('js/diagramly/ElectronApp.js');
}
mxscript(drawDevUrl + 'js/PostConfig.js');
}
else
{
(function()
{
var hostName = window.location.hostname;
// Supported domains are *.draw.io and the packaged version in Quip
var supportedDomain = (hostName.substring(hostName.length - 8, hostName.length) === '.draw.io') ||
(hostName.substring(hostName.length - 13, hostName.length) === '.diagrams.net');
function loadAppJS()
{
mxscript('js/app.min.js', function()
{
mxScriptsLoaded = true;
checkAllLoaded();
// Electron
if (mxIsElectron)
{
mxscript('js/diagramly/DesktopLibrary.js', function()
{
mxscript('js/diagramly/ElectronApp.js', function()
{
mxscript('js/extensions.min.js', function()
{
mxscript('js/stencils.min.js', function()
{
mxscript('js/shapes-14-6-5.min.js', function()
{
mxscript('js/PostConfig.js');
});
});
});
});
});
}
else if (!supportedDomain || navigator.onLine)
{
mxscript('js/PostConfig.js');
}
});
};
if (!supportedDomain || mxIsElectron || navigator.onLine)
{
mxscript('js/PreConfig.js', loadAppJS);
}
else
{
loadAppJS();
}
})();
}
// Adds basic error handling
window.onerror = function()
{
var status = document.getElementById('geStatus');
if (status != null)
{
status.innerHTML = 'Page could not be loaded. Please try refreshing.';
}
};
</script>
<link rel="icon" href="favicon.ico" sizes="any">
<link rel="apple-touch-icon" href="images/apple-touch-icon.png">
<link rel="stylesheet" type="text/css" href="styles/grapheditor.css">
<link rel="stylesheet" media="(forced-colors: active)" href="styles/high-contrast.css" id="high-contrast-stylesheet">
<link rel="canonical" href="https://app.diagrams.net">
<link rel="manifest" href="images/manifest.json">
<link rel="shortcut icon" href="favicon.ico">
<style type="text/css">
body { overflow:hidden; }
div.picker { z-index: 10007; }
.geSidebarContainer .geTitle input {
font-size:8pt;
color:#606060;
}
.geBlock {
display: none;
z-index:-3;
margin:100px;
margin-top:40px;
margin-bottom:30px;
padding:20px;
text-align:center;
min-width:50%;
}
.geBlock h1, .geBlock h2 {
margin-top:0px;
padding-top:0px;
}
.geEditor *:not(.geScrollable)::-webkit-scrollbar {
width:10px;
height:10px;
}
.geEditor ::-webkit-scrollbar-track {
background-clip:padding-box;
border:solid transparent;
border-width:1px;
}
.geEditor ::-webkit-scrollbar-corner {
background-color:transparent;
}
.geEditor ::-webkit-scrollbar-thumb {
background-color:rgba(0,0,0,.1);
background-clip:padding-box;
border:solid transparent;
border-radius:10px;
}
.geEditor ::-webkit-scrollbar-thumb:hover {
background-color:rgba(0,0,0,.4);
}
.geTemplate {
border:1px solid transparent;
display:inline-block;
_display:inline;
vertical-align:top;
border-radius:3px;
overflow:hidden;
font-size:14pt;
cursor:pointer;
margin:5px;
}
</style>
<!-- Workaround for binary XHR in IE 9/10, see App.loadUrl -->
<!--[if (IE 9)|(IE 10)]><!-->
<script type="text/vbscript">
Function mxUtilsBinaryToArray(Binary)
Dim i
ReDim byteArray(LenB(Binary))
For i = 1 To LenB(Binary)
byteArray(i-1) = AscB(MidB(Binary, i, 1))
Next
mxUtilsBinaryToArray = byteArray
End Function
</script>
<!--<![endif]-->
</head>
<body class="geEditor">
<div id="geInfo">
<div class="geBlock">
<h1>Flowchart Maker and Online Diagram Software</h1>
<p>
draw.io is free online diagram software. You can use it as a flowchart maker, network diagram software, to create UML online, as an ER diagram tool,
to design database schema, to build BPMN online, as a circuit diagram maker, and more. draw.io can import .vsdx, Gliffy&trade; and Lucidchart&trade; files .
</p>
<h2 id="geStatus">Loading...</h2>
<p>
Please ensure JavaScript is enabled.
</p>
</div>
</div>
<script id="geMain" type="text/javascript">
/**
* Main
*/
if (urlParams['dev'] != '1' && typeof document.createElement('canvas').getContext === "function")
{
window.addEventListener('load', function()
{
mxWinLoaded = true;
checkAllLoaded();
});
}
else
{
App.main();
}
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.croppie-container{width:100%;height:100%}.croppie-container .cr-image{z-index:-1;position:absolute;top:0;left:0;transform-origin:0 0;max-height:none;max-width:none}.croppie-container .cr-boundary{position:relative;overflow:hidden;margin:0 auto;z-index:1;width:100%;height:100%}.croppie-container .cr-resizer,.croppie-container .cr-viewport{position:absolute;border:2px solid #fff;margin:auto;top:0;bottom:0;right:0;left:0;box-shadow:0 0 2000px 2000px rgba(0,0,0,.5);z-index:0}.croppie-container .cr-resizer{z-index:2;box-shadow:none;pointer-events:none}.croppie-container .cr-resizer-horisontal,.croppie-container .cr-resizer-vertical{position:absolute;pointer-events:all}.croppie-container .cr-resizer-horisontal::after,.croppie-container .cr-resizer-vertical::after{display:block;position:absolute;box-sizing:border-box;border:1px solid #000;background:#fff;width:10px;height:10px;content:''}.croppie-container .cr-resizer-vertical{bottom:-5px;cursor:row-resize;width:100%;height:10px}.croppie-container .cr-resizer-vertical::after{left:50%;margin-left:-5px}.croppie-container .cr-resizer-horisontal{right:-5px;cursor:col-resize;width:10px;height:100%}.croppie-container .cr-resizer-horisontal::after{top:50%;margin-top:-5px}.croppie-container .cr-original-image{display:none}.croppie-container .cr-vp-circle{border-radius:50%}.croppie-container .cr-overlay{z-index:1;position:absolute;cursor:move;touch-action:none}.croppie-container .cr-slider-wrap{width:75%;margin:15px auto;text-align:center}.croppie-result{position:relative;overflow:hidden}.croppie-result img{position:absolute}.croppie-container .cr-image,.croppie-container .cr-overlay,.croppie-container .cr-viewport{-webkit-transform:translateZ(0);-moz-transform:translateZ(0);-ms-transform:translateZ(0);transform:translateZ(0)}.cr-slider{-webkit-appearance:none;width:300px;max-width:100%;padding-top:8px;padding-bottom:8px;background-color:transparent}.cr-slider::-webkit-slider-runnable-track{width:100%;height:3px;background:rgba(0,0,0,.5);border:0;border-radius:3px}.cr-slider::-webkit-slider-thumb{-webkit-appearance:none;border:none;height:16px;width:16px;border-radius:50%;background:#ddd;margin-top:-6px}.cr-slider:focus{outline:0}.cr-slider::-moz-range-track{width:100%;height:3px;background:rgba(0,0,0,.5);border:0;border-radius:3px}.cr-slider::-moz-range-thumb{border:none;height:16px;width:16px;border-radius:50%;background:#ddd;margin-top:-6px}.cr-slider:-moz-focusring{outline:1px solid #fff;outline-offset:-1px}.cr-slider::-ms-track{width:100%;height:5px;background:0 0;border-color:transparent;border-width:6px 0;color:transparent}.cr-slider::-ms-fill-lower{background:rgba(0,0,0,.5);border-radius:10px}.cr-slider::-ms-fill-upper{background:rgba(0,0,0,.5);border-radius:10px}.cr-slider::-ms-thumb{border:none;height:16px;width:16px;border-radius:50%;background:#ddd;margin-top:1px}.cr-slider:focus::-ms-fill-lower{background:rgba(0,0,0,.5)}.cr-slider:focus::-ms-fill-upper{background:rgba(0,0,0,.5)}.cr-rotate-controls{position:absolute;bottom:5px;left:5px;z-index:1}.cr-rotate-controls button{border:0;background:0 0}.cr-rotate-controls i:before{display:inline-block;font-style:normal;font-weight:900;font-size:22px}.cr-rotate-l i:before{content:'↺'}.cr-rotate-r i:before{content:'↻'}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

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