Compare commits

..

486 Commits

Author SHA1 Message Date
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
kuaifan
0535b56766 build 2025-03-19 23:37:39 +08:00
kuaifan
6afd413b87 no message 2025-03-19 23:35:02 +08:00
kuaifan
4818409329 perf: 优化AI解析文件 2025-03-19 23:33:17 +08:00
kuaifan
919b652a06 perf: 优化AI解析文件 2025-03-19 22:49:03 +08:00
kuaifan
15d3ec9d81 perf: 优化 WebSocket 消息 2025-03-19 22:01:07 +08:00
kuaifan
e0be6e429e no message 2025-03-19 21:04:05 +08:00
kuaifan
8d24be914d perf: 优化数据 2025-03-19 15:14:23 +08:00
kuaifan
8bbe9c97e9 perf: 优化数据 2025-03-19 12:53:44 +08:00
kuaifan
ccbd904a3f build 2025-03-19 08:51:43 +08:00
kuaifan
4ed3db7e41 fix: 修复查看待办图片不符的情况 2025-03-19 08:48:51 +08:00
kuaifan
65ced28004 perf: 优化数据 2025-03-18 23:43:36 +08:00
kuaifan
4c282962b3 perf: 优化未读消息数 2025-03-18 18:48:39 +08:00
kuaifan
c64c436b9f perf: 优化搜索组件 2025-03-18 18:36:20 +08:00
kuaifan
378e270f41 no message 2025-03-18 17:45:53 +08:00
kuaifan
7217bd7d1a perf: 已归档/已删除任务列表支持按状态检索 2025-03-18 17:43:21 +08:00
kuaifan
ff2461d89d no message 2025-03-18 12:01:46 +08:00
kuaifan
0eb3430c14 perf: 优化消息流效果 2025-03-18 11:38:58 +08:00
kuaifan
da7c1e40e3 no message 2025-03-18 10:29:05 +08:00
kuaifan
8c9e928ddc no message 2025-03-18 01:52:18 +08:00
kuaifan
477aef7db6 perf: 优化AI上下文 2025-03-18 01:49:24 +08:00
kuaifan
cc97d9f1ea perf: 优化工作流获取 2025-03-17 20:31:30 +08:00
kuaifan
ee6cf05a92 perf: 优化转发功能 2025-03-17 10:15:12 +08:00
kuaifan
575db58476 build 2025-03-16 23:43:51 +08:00
kuaifan
986a2f8cbb no message 2025-03-16 23:28:12 +08:00
kuaifan
0b1da914cd no message 2025-03-16 23:06:45 +08:00
kuaifan
04acd7c56d feat: 可点击标注图标查看标注人员 2025-03-16 22:20:39 +08:00
kuaifan
4430d85242 perf: 优化转发消息 2025-03-16 21:51:19 +08:00
kuaifan
55ade32589 feat: 支持分享工作报告到消息 2025-03-16 21:39:12 +08:00
kuaifan
0ffbaaaeaa perf: 优化转发消息 2025-03-16 21:08:04 +08:00
kuaifan
62b40ddb84 perf: 优化转发消息 2025-03-16 20:43:09 +08:00
kuaifan
2d5ce87605 feat: 支持AI分析工作报告 2025-03-16 00:55:45 +08:00
kuaifan
7ca0bc5960 feat: 支持使用%发送工作报告 2025-03-15 17:53:21 +08:00
kuaifan
021c09e426 feat: 支持使用%发送工作报告 2025-03-15 17:06:47 +08:00
kuaifan
75db81f2f9 feat: 支持使用%发送工作报告 2025-03-15 15:59:06 +08:00
kuaifan
f162617765 no message 2025-03-14 23:14:44 +08:00
kuaifan
b7d10a4c58 perf: 优化工作报告列表 2025-03-14 23:12:40 +08:00
kuaifan
79ca1aea02 no message 2025-03-14 22:41:26 +08:00
kuaifan
957201804c feat: 新增自定义撤回及修改消息时限 2025-03-14 21:07:44 +08:00
kuaifan
cf5e126eaa perf: 优化引用消息 2025-03-14 19:53:00 +08:00
kuaifan
69fc0a118b no message 2025-03-14 15:01:35 +08:00
kuaifan
4dacc26567 perf: 优化全局提示 2025-03-14 14:50:32 +08:00
kuaifan
7de1ed7d45 no message 2025-03-14 12:11:08 +08:00
kuaifan
ab47f01625 perf: 优化全局提示 2025-03-14 12:07:49 +08:00
kuaifan
13c4fa4f1f no message 2025-03-14 09:25:33 +08:00
kuaifan
173631f115 no message 2025-03-14 09:11:04 +08:00
kuaifan
8462e9c097 no message 2025-03-14 07:28:29 +08:00
kuaifan
3c9447e1b6 no message 2025-03-13 22:01:46 +08:00
kuaifan
38eaf2eb02 perf: 优化草稿消息 2025-03-13 21:35:05 +08:00
kuaifan
c8364ed17b perf: 优化草稿消息 2025-03-13 20:54:35 +08:00
kuaifan
ba52738904 perf: 优化草稿消息 2025-03-13 20:37:41 +08:00
kuaifan
4061ae4275 perf: 优化草稿消息 2025-03-13 20:18:34 +08:00
kuaifan
82afb5b150 no message 2025-03-13 17:27:13 +08:00
kuaifan
e1203f0c8d no message 2025-03-13 17:12:11 +08:00
kuaifan
e6f6b3fee2 fix: 工作流存在已离职人员 2025-03-13 15:02:53 +08:00
kuaifan
e5efcd3d26 no message 2025-03-13 14:36:00 +08:00
kuaifan
bf45587c80 no message 2025-03-13 13:25:16 +08:00
kuaifan
29a0d22938 build 2025-03-13 01:40:11 +08:00
kuaifan
635cc04c50 perf: 优化消息定位 2025-03-13 01:38:35 +08:00
kuaifan
bc5343652b perf: 优化消息性能 2025-03-12 22:41:41 +08:00
kuaifan
03f140fe3b fix: 看不到未读消息定位提醒 2025-03-12 14:19:07 +08:00
kuaifan
3e4a119f61 build 2025-03-12 00:18:12 +08:00
kuaifan
3b7bcbc14a no message 2025-03-11 17:41:12 +08:00
kuaifan
3c49e96e02 no message 2025-03-11 16:24:31 +08:00
kuaifan
5be209ab59 no message 2025-03-11 15:53:53 +08:00
kuaifan
56ea048ab3 no message 2025-03-11 10:21:44 +08:00
kuaifan
9fc0bd0439 no message 2025-03-10 23:12:28 +08:00
kuaifan
1c2798cbf4 perf: 优化消息定位 2025-03-10 23:06:30 +08:00
kuaifan
9d8af2eaab perf: 优化MD消息 2025-03-10 22:38:42 +08:00
kuaifan
bba1e0d12f fix: 会话内消息搜索布局错位 2025-03-10 20:52:52 +08:00
kuaifan
c060e60e4a fix: 流程设置翻译不统一 2025-03-10 20:51:58 +08:00
kuaifan
1c504bd899 no message 2025-03-10 00:12:05 +08:00
kuaifan
b617648bd8 no message 2025-03-09 23:46:01 +08:00
kuaifan
e849c7a34f build 2025-03-09 23:11:14 +08:00
kuaifan
f6dd1ce98e no message 2025-03-09 23:02:35 +08:00
kuaifan
9c78db8d45 no message 2025-03-09 22:52:38 +08:00
kuaifan
5154348cf9 perf: 优化发送语音效果 2025-03-09 22:41:15 +08:00
kuaifan
4521cea3b4 perf: 优化发送语音效果 2025-03-09 19:54:09 +08:00
kuaifan
0ff1ac7743 no message 2025-03-09 18:54:33 +08:00
kuaifan
277a751ed4 no message 2025-03-09 18:40:03 +08:00
kuaifan
96be2a86ca no message 2025-03-09 18:17:18 +08:00
kuaifan
f28bff569a no message 2025-03-09 15:49:56 +08:00
kuaifan
e34aa77a54 perf: 录音转文字支持自定义语言 2025-03-09 15:32:38 +08:00
kuaifan
e53b65496f perf: 录音转文字支持自定义语言 2025-03-09 11:33:37 +08:00
kuaifan
f6ee630615 no message 2025-03-08 19:11:17 +08:00
kuaifan
ec2e1e3152 no message 2025-03-08 17:56:09 +08:00
kuaifan
6cffe9baed perf: 优化ES模块 2025-03-08 16:39:35 +08:00
kuaifan
b63df27409 perf: 优化emoji表情 2025-03-08 15:50:35 +08:00
kuaifan
617c466ac0 perf: 按住Ctrl/Command键可连续选择表情 2025-03-08 12:13:25 +08:00
kuaifan
ed8e443f3a perf: 优化ES模块 2025-03-08 12:12:48 +08:00
kuaifan
58cb49b125 perf: 优化ES模块 2025-03-08 10:15:58 +08:00
kuaifan
7dd5baa9ec fix: 定位签到失败的问题 2025-03-07 23:43:39 +08:00
kuaifan
bbf9107560 perf: md消息支持html代码 2025-03-07 23:23:51 +08:00
kuaifan
be527355ee no message 2025-03-07 22:15:27 +08:00
kuaifan
c866500120 perf: 优化脚本 2025-03-07 16:18:06 +08:00
kuaifan
3e2a40aaa0 perf: 优化安装命令 2025-03-07 15:13:19 +08:00
kuaifan
eef9fa56c6 perf: 优化ES索引名称 2025-03-07 12:57:37 +08:00
zzzzzhy
945d84dbc4 添加es证书配置 2025-03-07 12:57:37 +08:00
kuaifan
d353d33107 no message 2025-03-07 12:57:37 +08:00
kuaifan
f54bad5d79 no message 2025-03-06 16:16:43 +08:00
kuaifan
b605c70e91 no message 2025-03-06 14:52:40 +08:00
kuaifan
1752e88c42 no message 2025-03-05 15:19:38 +08:00
kuaifan
e2718a39a0 no message 2025-03-05 10:19:28 +08:00
kuaifan
25298ac69e build 2025-03-05 08:33:52 +08:00
kuaifan
cf9f389f75 no message 2025-03-05 08:23:08 +08:00
kuaifan
567c75830a perf: 新增录音转文字 2025-03-05 01:52:37 +08:00
kuaifan
7b1d352c95 perf: 新增录音转文字 2025-03-05 01:22:12 +08:00
kuaifan
4fa54381a6 no message 2025-03-04 20:12:32 +08:00
kuaifan
9c91f7cf83 no message 2025-03-04 19:56:13 +08:00
kuaifan
edd5cd1ca1 perf: 优化数据排序 2025-03-04 19:24:13 +08:00
kuaifan
f2ec6ad05e no message 2025-03-04 18:30:23 +08:00
kuaifan
a04ef4ac38 no message 2025-03-04 16:58:17 +08:00
kuaifan
43b3d1d379 no message 2025-03-04 16:46:45 +08:00
kuaifan
b65fdeacc2 no message 2025-03-04 09:33:26 +08:00
kuaifan
622fe1e5d9 no message 2025-03-04 08:44:11 +08:00
kuaifan
a6c7c0c7ad fix: 全屏预览图片关闭窗口 2025-03-04 07:38:06 +08:00
kuaifan
e5c8748b75 no message 2025-03-04 06:35:32 +08:00
kuaifan
f096d71cc1 no message 2025-03-03 23:45:42 +08:00
kuaifan
d73a152a36 no message 2025-03-03 22:48:09 +08:00
kuaifan
f4e6fd060e no message 2025-03-03 21:00:04 +08:00
kuaifan
c78ca1de5d no message 2025-03-03 18:11:32 +08:00
kuaifan
2b219c7256 no message 2025-03-03 14:53:45 +08:00
kuaifan
6ffa651742 no message 2025-03-03 13:16:03 +08:00
kuaifan
cb3b22a4bf fix: 点击排序导致任务不显示的情况 2025-03-03 12:51:33 +08:00
kuaifan
145bfdb0e9 no message 2025-03-03 12:49:04 +08:00
kuaifan
8c7b0c502d no message 2025-03-03 11:59:02 +08:00
kuaifan
684bf12a5c no message 2025-03-03 10:27:56 +08:00
kuaifan
aaa75aff14 build 2025-03-03 08:32:50 +08:00
kuaifan
f03600bd65 perf: 添加全局搜索功能 2025-03-03 08:20:17 +08:00
kuaifan
1c4c4fe3fb perf: 添加全局搜索功能 2025-03-03 07:01:49 +08:00
kuaifan
5e46b2cd1a perf: 添加全局搜索功能 2025-03-02 23:43:18 +08:00
kuaifan
027db7c0ec perf: 添加全局搜索功能 2025-03-02 19:00:09 +08:00
kuaifan
5bb17ddc6b no message 2025-03-02 18:59:54 +08:00
kuaifan
e8edd74bc3 no message 2025-03-01 23:59:51 +08:00
kuaifan
ed064a825a perf: 优化消息搜索 2025-03-01 23:59:14 +08:00
kuaifan
32c232a0b5 perf: 优化消息搜索 2025-03-01 20:54:02 +08:00
kuaifan
c2fd747c45 no message 2025-03-01 20:39:13 +08:00
kuaifan
9148853f2c perf: 团队管理支持调整部门区域尺寸 2025-03-01 14:51:07 +08:00
kuaifan
23d0f50a3d perf: 任务详情支持调整聊天区域尺寸 2025-03-01 14:50:44 +08:00
kuaifan
36cdf87bfe perf: 优化团队部门支持3级部门 2025-03-01 13:41:10 +08:00
kuaifan
cfd2e1fd7b perf: 可见群组ID 2025-03-01 12:06:03 +08:00
kuaifan
3cafac99ff perf: 支持在团队管理打开群聊 2025-03-01 11:54:20 +08:00
kuaifan
1dd4e8da71 perf: 优化回复消息自动@逻辑 2025-03-01 11:19:50 +08:00
kuaifan
543015a36e perf: 转发预览隐藏表情回应部分 2025-03-01 10:14:51 +08:00
kuaifan
2efdfc4b1f no message 2025-03-01 00:34:43 +08:00
kuaifan
7234d9307e no message 2025-03-01 00:21:21 +08:00
kuaifan
769ce1ce7c no message 2025-02-28 22:49:45 +08:00
kuaifan
62c1d5783e perf: 优化任务日志 2025-02-28 22:38:11 +08:00
kuaifan
a6bd4a2ffe no message 2025-02-28 21:24:09 +08:00
kuaifan
f1a9077b7e no message 2025-02-28 21:20:36 +08:00
kuaifan
2c3e80bd8f perf: 已删除任务支持按标签搜索 2025-02-28 21:07:54 +08:00
kuaifan
e52d066fb0 perf: 归档任务支持按标签搜索 2025-02-28 21:07:32 +08:00
kuaifan
5279d57018 perf: 项目面板添加按标签筛选 2025-02-28 20:54:25 +08:00
kuaifan
25e5eb4427 perf: 优化 AI 提示词 2025-02-26 20:50:17 +08:00
kuaifan
b01d5ce8c4 perf: 优化 AI 设置 2025-02-26 20:12:23 +08:00
kuaifan
ff41f5c041 no message 2025-02-26 11:58:53 +08:00
kuaifan
dd0770a93f no message 2025-02-25 21:36:55 +08:00
kuaifan
9a3e76fff3 no message 2025-02-25 21:08:54 +08:00
kuaifan
7c867578ee build 2025-02-25 21:08:40 +08:00
kuaifan
d543c27000 perf: 工作报告支持查看仅未读 2025-02-25 20:39:22 +08:00
kuaifan
a8be330baa perf: AI 支持引用文件 2025-02-25 20:31:13 +08:00
kuaifan
c128c58110 perf: 优化图文消息 2025-02-25 19:51:07 +08:00
kuaifan
e32a3887cd perf: 优化文本信息复制 2025-02-25 17:47:46 +08:00
kuaifan
94932c7486 perf: 优化图文消息 2025-02-25 17:38:12 +08:00
kuaifan
a1920745fb perf: 优化样式 2025-02-25 11:43:48 +08:00
kuaifan
51e8f9555e perf: 无法再AI机器人页面看到模型的问题 2025-02-25 11:37:51 +08:00
kuaifan
213ab8418b fix: 首次跟ai聊天没有记录的问题 2025-02-25 11:14:41 +08:00
kuaifan
707f1dd6cb no message 2025-02-24 10:41:35 +08:00
kuaifan
125ce036cd perf: 优化MD消息过长处理 2025-02-24 09:12:12 +08:00
kuaifan
172c562a71 build 2025-02-24 00:08:50 +08:00
kuaifan
80bbe6711c no message 2025-02-24 00:07:31 +08:00
kuaifan
3f56c64086 perf: 优化AI支持分析指定文件 2025-02-23 23:55:02 +08:00
kuaifan
e6167119e0 no message 2025-02-22 20:27:27 +08:00
kuaifan
368fae5f32 perf: 支持在AI对话中直接引用任务提问 2025-02-22 20:27:10 +08:00
kuaifan
6ae46cf7bb perf: 支持在AI对话中直接引用任务提问 2025-02-22 17:49:11 +08:00
kuaifan
e97806c85b no message 2025-02-22 17:49:04 +08:00
kuaifan
f31e88bed1 no message 2025-02-22 12:14:03 +08:00
kuaifan
6bd20038f9 no message 2025-02-22 11:29:46 +08:00
kuaifan
30cfb1200d no message 2025-02-22 11:26:16 +08:00
kuaifan
154e0039d1 perf: 优化 AI 参数 2025-02-22 11:13:16 +08:00
kuaifan
a8f3b02ee7 perf: 优化 Ollama AI 2025-02-22 01:29:54 +08:00
kuaifan
b3e83e13bc perf: 优化设置 2025-02-22 00:59:52 +08:00
kuaifan
d0a0e77c44 perf: 优化设置 2025-02-22 00:45:20 +08:00
kuaifan
a14896307f no message 2025-02-21 23:26:54 +08:00
kuaifan
976b300277 perf: 优化AI设置 2025-02-21 23:26:14 +08:00
kuaifan
ccbd873204 perf: 优化AI设置 2025-02-21 22:15:59 +08:00
kuaifan
9c1482f9e9 feat: 添加 Grok AI、Ollama AI 2025-02-21 17:04:59 +08:00
kuaifan
5a7f4efa91 feat: 添加 Grok AI、Ollama AI 2025-02-21 12:08:54 +08:00
kuaifan
f78c4a1fb0 perf: 优化AI设置 2025-02-21 11:37:39 +08:00
kuaifan
db6500369f perf: 优化AI消息 2025-02-20 01:12:48 +08:00
kuaifan
9e4beaa317 perf: 表情回复时更新对话列表 2025-02-14 21:14:15 +08:00
kuaifan
afd021737a build 2025-02-14 20:41:04 +08:00
kuaifan
3982ed56f7 no message 2025-02-14 15:15:36 +08:00
kuaifan
df4a01a7f9 perf: onlyoffice 支持打开超过100m的文件 2025-02-14 15:07:55 +08:00
kuaifan
a6fac96ec1 perf: 优化点击上传列表效果 2025-02-14 15:06:17 +08:00
kuaifan
8ed9186ff4 perf: AI支持自定义模型列表 2025-02-14 01:07:32 +08:00
kuaifan
821df75d4b fix: 撤回消息是消息列表不更新的情况 2025-02-13 21:21:32 +08:00
kuaifan
0c09a2445c build 2025-02-12 21:40:21 +08:00
kuaifan
e6983e858d no message 2025-02-12 21:33:10 +08:00
kuaifan
f8b69df955 fix: 修复偶现的是子窗口出现身份丢失的情况 2025-02-12 21:09:54 +08:00
kuaifan
15370a93c7 perf: 优化查看长消息内容 2025-02-12 20:46:18 +08:00
kuaifan
bc18aeeadc no message 2025-02-11 15:11:23 +08:00
kuaifan
a1f143b0aa build 2025-02-11 07:53:53 +09:00
kuaifan
c13fe9d590 perf: 优化审批功能 2025-02-11 03:33:58 +09:00
kuaifan
50203fbcb3 perf: AI机器人支持多会话 2025-02-11 03:12:25 +09:00
kuaifan
ffe7ebf711 perf: AI机器人支持多会话 2025-02-11 02:41:01 +09:00
kuaifan
f0b5e0c3b9 perf: AI机器人支持多会话 2025-02-10 18:45:35 +09:00
kuaifan
501235ef12 perf: AI机器人支持多会话 2025-02-10 16:48:08 +09:00
kuaifan
da0fa31181 perf: AI机器人支持多会话 2025-02-10 15:53:14 +09:00
kuaifan
0272933f70 perf: AI机器人支持多会话 2025-02-10 15:43:02 +09:00
kuaifan
30d88761b4 perf: AI机器人支持多会话 2025-02-10 12:39:36 +09:00
kuaifan
fb286cea3c perf: AI机器人支持自定义模型 2025-02-08 12:55:11 +09:00
kuaifan
6bcc7b6c49 perf: AI机器人支持多会话 2025-02-07 16:44:02 +09:00
kuaifan
6338a44cc1 no message 2025-02-07 16:41:52 +09:00
kuaifan
ae4680f20c build 2025-02-07 05:07:20 +09:00
kuaifan
2841874417 no message 2025-02-07 05:07:10 +09:00
kuaifan
b6a4e6b4de perf: 支持下载聊天引用的文件 2025-02-07 04:55:58 +09:00
kuaifan
34cfd1e344 perf: 优化翻译消息 2025-02-07 04:38:23 +09:00
kuaifan
b467dc55e5 perf: 支持显示思考过程 2025-02-05 15:38:07 +09:00
kuaifan
9fd8d44a6e build 2025-02-05 01:37:41 +09:00
kuaifan
64262134c4 perf: 支持自定义仪表盘欢迎词 2025-02-05 01:33:32 +09:00
kuaifan
0019c9ef41 build 2025-02-04 13:14:43 +09:00
kuaifan
2676ebd047 no message 2025-02-04 13:10:46 +09:00
kuaifan
97cdd56110 fix: 跨地区发消息出现消息过期的情况 2025-02-04 13:10:46 +09:00
kuaifan
d973451bdc feat: 添加 DeepSeek AI 2025-02-04 13:10:46 +09:00
kuaifan
80313f613e perf: ChatGPT 支持自定义 Base URL 2025-02-04 13:10:46 +09:00
kuaifan
5c564524a3 Merge pull request #247 from zzzzzhy/patch-2
定时任务判断fix
2025-01-17 20:24:22 +08:00
zzzzzhy
e081fbd92b 定时任务判断fix
修复定时更新https证书任务判断逻辑
2025-01-17 14:15:24 +08:00
kuaifan
0ecc20472a Merge pull request #246 from zzzzzhy/patch-1
安装软件依赖
2025-01-16 22:35:38 +08:00
zzzzzhy
b51052f0c6 安装软件依赖 2025-01-16 20:14:08 +08:00
kuaifan
cb106e42ee Merge pull request #245 from zzzzzhy/renew_cert
feat:添加https证书自动更新
2025-01-16 19:54:17 +08:00
zzzzzhy
52f9495ff8 feat:添加https证书自动更新 2025-01-16 06:44:26 +00:00
kuaifan
440b633bad perf: 优化仪表盘任务更新规则 2025-01-15 15:41:38 +08:00
kuaifan
a07913181a fix: 多线程下载文件损坏的问题 2025-01-15 15:27:39 +08:00
kuaifan
34ffd96c86 no message 2025-01-13 10:56:30 +08:00
kuaifan
46a623b430 perf: 优化仪表盘任务更新规则 2025-01-13 10:56:29 +08:00
kuaifan
c16e37023c fix: 多线程下载文件损坏的问题 2025-01-13 10:56:29 +08:00
kuaifan
1cb0cdf540 Merge pull request #242 from nightcp/fix-report-sign-repeat 2025-01-04 20:48:09 +08:00
nightcp
073d03a882 fix: 修复新建周报或日报唯一标识重复 2025-01-04 19:58:37 +08:00
kuaifan
30b9276ab4 no message 2025-01-03 08:19:55 +08:00
kuaifan
76c8b4a4c6 no message 2025-01-03 04:48:38 +08:00
kuaifan
9ea4781d93 perf: 更新小海豚表情包 2025-01-03 03:56:22 +08:00
kuaifan
07d583f73f perf: 优化任务时间冲突提示 2025-01-02 20:25:47 +08:00
kuaifan
12c74aef7a no message 2025-01-02 14:56:30 +08:00
kuaifan
64b10e3060 perf: 优化消息 2025-01-02 14:54:22 +08:00
kuaifan
ab2b29f267 perf: 群聊总人数排除机器人 2025-01-02 14:23:23 +08:00
kuaifan
be9a968ad9 perf: 群聊总人数排除机器人 2025-01-02 13:41:09 +08:00
kuaifan
5f87067a75 fix: 部分电脑无法复制的问题 2025-01-02 12:48:35 +08:00
weifs
ef273bd9dd fix: 修复任务可见性 - 任务重覆获取, 子任务负责人看不到任务问题 2024-12-24 00:11:23 +08:00
weifs
0737a9fae7 fix: 修复任务可见性 - 任务重覆获取, 子任务负责人看不到任务问题 2024-12-23 23:19:22 +08:00
kuaifan
727d7e1d81 build 2024-12-23 11:53:40 +08:00
kuaifan
87e8589aea no message 2024-12-23 11:47:39 +08:00
kuaifan
b13758d3e9 perf: 优化任务面板 2024-12-22 17:45:58 +08:00
kuaifan
14775e2861 perf: 优化子任务的可见性 2024-12-21 22:20:39 +08:00
kuaifan
94af3822d8 perf: 优化客户端 2024-12-21 15:33:01 +08:00
kuaifan
07254c9f27 perf: 优化会议 2024-12-21 11:57:48 +08:00
kuaifan
a99c2f6944 perf: 优化客户端 2024-12-21 11:47:49 +08:00
kuaifan
f9540b08cd perf: 优化会议 2024-12-20 21:24:17 +08:00
kuaifan
34af77eb6d no message 2024-12-20 20:01:42 +08:00
kuaifan
cf3f22776c perf: 优化会员搜索 2024-12-20 19:59:18 +08:00
kuaifan
5bebc8b5ee perf: 优化打开会话 2024-12-20 19:41:00 +08:00
kuaifan
8a4b0c57f9 perf: 优化会议 2024-12-20 19:25:05 +08:00
kuaifan
1acfd7ee34 no message 2024-12-20 09:22:14 +08:00
kuaifan
a29661c54d perf: 优化会议 2024-12-20 09:01:17 +08:00
kuaifan
90558d5ece no message 2024-12-18 20:37:16 +08:00
kuaifan
e6c7007be5 perf: 优化项目面板任务加载 2024-12-18 20:14:35 +08:00
kuaifan
16d0d1687f perf: 优化客户端加载 2024-12-18 19:56:36 +08:00
kuaifan
95ab44d118 perf: 优化客户端加载 2024-12-18 15:23:25 +08:00
kuaifan
e541757b76 no message 2024-12-18 15:22:13 +08:00
kuaifan
f422aea330 fix: 移交账号后工作流的负责人没有更新 2024-12-18 00:25:04 +08:00
kuaifan
d5eb3716aa no message 2024-12-18 00:25:04 +08:00
yijixx
7fb854fb48 feat: 替换网页的资源为本地资源 2024-12-17 17:01:17 +08:00
kuaifan
60b5ecdcd7 fix: 全屏预览时深色皮肤反色的情况 2024-12-17 09:11:12 +08:00
kuaifan
6cce7d31ff build 2024-12-17 08:49:40 +08:00
kuaifan
46f5dd99a6 perf: 优化对话阅读状况 2024-12-17 08:47:05 +08:00
kuaifan
9753dec996 perf: 优化表情回复 2024-12-17 08:41:46 +08:00
kuaifan
53f2e07178 build 2024-12-17 00:14:13 +08:00
kuaifan
3aa2c604d8 perf: 优化桌面端数据处理 2024-12-17 00:07:50 +08:00
kuaifan
d8fbf36e00 perf: 优化资源 2024-12-16 23:29:05 +08:00
kuaifan
008653e3d9 fix: 桌面端查看表情图片缩略图显示错误 2024-12-16 21:23:18 +08:00
kuaifan
23188777fe fix: 项目面板任务不显示的情况 2024-12-16 21:15:30 +08:00
kuaifan
8eb0a49ee6 perf: 优化数据流 2024-12-16 17:04:45 +08:00
kuaifan
207f09a4af fix: 修复移动任务子任务不跟随的情况 2024-12-16 16:07:39 +08:00
kuaifan
69120c5045 build 2024-12-15 23:39:27 +08:00
kuaifan
b8143d1a9b no message 2024-12-15 23:24:38 +08:00
kuaifan
f7eab5893a perf: AI创建任务确认 2024-12-15 22:53:26 +08:00
kuaifan
5fc598a220 no message 2024-12-15 22:45:27 +08:00
kuaifan
783c21ad18 perf: 优化项目面板 2024-12-15 22:27:05 +08:00
kuaifan
a1ce6e6928 perf: 优化项目面板 2024-12-15 09:39:18 +08:00
kuaifan
8cbae629a5 perf: 优化项目面板 2024-12-14 18:51:46 +08:00
kuaifan
da7e832f21 fix: 复制文件权限判断 2024-12-14 18:41:30 +08:00
kuaifan
a572ba0523 perf: 优化项目面板 2024-12-14 18:38:47 +08:00
kuaifan
85a20168dc build 2024-12-13 23:28:46 +08:00
kuaifan
25be9c0fef perf: 优化子任务上下文 2024-12-13 23:19:20 +08:00
kuaifan
a8c890ba51 perf: 优化子任务时间调整 2024-12-13 19:28:05 +08:00
kuaifan
11628b98ca no message 2024-12-13 16:15:47 +08:00
kuaifan
4ae6ca945b no message 2024-12-13 16:00:21 +08:00
kuaifan
49aa1434aa perf: 优化超长文本信息 2024-12-13 15:49:31 +08:00
kuaifan
9e92c61fbf no message 2024-12-13 15:49:31 +08:00
kuaifan
c84111b6b9 perf: 记录版本信息 2024-12-13 15:49:31 +08:00
kuaifan
3a2fcdd18a no message 2024-12-13 15:49:30 +08:00
kuaifan
84a800f69b fix: @在线状态不正确 2024-12-13 15:49:30 +08:00
kuaifan
77e08aa048 no message 2024-12-13 15:49:30 +08:00
kuaifan
0d6fd903f1 perf: 支持更多办公文件格式 2024-12-13 09:47:52 +08:00
kuaifan
bcc74dd927 perf: 请假或外出时取消打卡提醒 2024-12-13 01:00:31 +08:00
kuaifan
dd0720afa7 no message 2024-12-13 00:27:41 +08:00
kuaifan
a06a4095b6 no message 2024-12-12 23:10:33 +08:00
kuaifan
29bc009c07 perf: 图片容错处理 2024-12-12 22:36:25 +08:00
kuaifan
520d2a0e20 no message 2024-12-12 22:01:01 +08:00
kuaifan
dbeb9dd561 perf: 优化全局监听事件 2024-12-12 14:14:51 +08:00
kuaifan
5b02d8008f perf: 优化数据流消息 2024-12-12 13:45:48 +08:00
kuaifan
a032c6114f no message 2024-12-12 12:55:29 +08:00
kuaifan
69ec4966d5 build 2024-12-12 00:44:59 +08:00
kuaifan
87fab80ea3 no message 2024-12-12 00:43:08 +08:00
kuaifan
bd2dabe851 no message 2024-12-12 00:30:25 +08:00
kuaifan
4a45d69e5b no message 2024-12-11 23:26:22 +08:00
kuaifan
e15bea9342 fix: 修复使用AI创建任务顺序错误的问题 2024-12-11 22:36:57 +08:00
kuaifan
7132413837 no message 2024-12-11 22:17:51 +08:00
kuaifan
c51116acaa build 2024-12-11 00:26:15 +08:00
kuaifan
002776f15e no message 2024-12-10 23:57:58 +08:00
kuaifan
c7f5c62e71 no message 2024-12-10 23:41:31 +08:00
kuaifan
3c57cf8d81 no message 2024-12-10 23:27:27 +08:00
kuaifan
f29bf3640a no message 2024-12-10 20:43:32 +08:00
kuaifan
07663dea6c no message 2024-12-10 15:46:41 +08:00
kuaifan
0ddb696e90 perf: 优化媒体播放 2024-12-10 15:20:39 +08:00
kuaifan
cc0a6d4706 no message 2024-12-10 13:08:28 +08:00
kuaifan
4c0ecc8f07 no message 2024-12-10 09:09:19 +08:00
kuaifan
d50c8ce691 perf: 优化临时会话的消息推送 2024-12-10 07:33:58 +08:00
kuaifan
8aa66661ac no message 2024-12-10 07:32:57 +08:00
kuaifan
00a9b3b57b perf: 优化任务时间显示 2024-12-10 07:32:47 +08:00
kuaifan
3896d08207 build 2024-12-09 07:39:56 +08:00
kuaifan
9b736c99f8 build 2024-12-09 07:35:10 +08:00
kuaifan
129d7e5850 no message 2024-12-09 00:21:54 +08:00
kuaifan
c2b26ffe6e no message 2024-12-09 00:16:00 +08:00
kuaifan
9b01e076f5 no message 2024-12-09 00:03:11 +08:00
kuaifan
88553872fc perf: upgrade office 2024-12-08 23:57:44 +08:00
kuaifan
2b8de4c028 Merge commit '646a5e3b28b5fcaddd8f8618685cab6fdd07eef7' into pro 2024-12-08 23:41:51 +08:00
kuaifan
24c5200a90 perf: 添加项目任务标签功能 2024-12-08 23:34:39 +08:00
kuaifan
bca0410a08 no message 2024-12-08 19:19:41 +08:00
kuaifan
42234be5cf perf: 添加项目任务标签功能 2024-12-08 18:56:01 +08:00
kuaifan
8e108e2d38 perf: 添加项目任务标签功能 2024-12-08 17:13:17 +08:00
kuaifan
248b0ce196 no message 2024-12-08 16:13:46 +08:00
kuaifan
d25ee3c234 no message 2024-12-07 19:58:43 +08:00
kuaifan
8ea1234596 no message 2024-12-07 12:53:39 +08:00
kuaifan
32530e5dc9 perf: 添加项目任务标签功能 2024-12-07 12:06:01 +08:00
kuaifan
952d060e2f no message 2024-12-07 11:55:28 +08:00
kuaifan
712f9e07b7 perf: Upgrade drawio 2024-12-07 01:31:49 +08:00
kuaifan
03cd6e79bb no message 2024-12-06 22:45:54 +08:00
kuaifan
cbd9e8a33c perf: 优化AI创建任务 2024-12-06 19:49:45 +08:00
kuaifan
13222fbe9a perf: 优化已读数据 2024-12-06 17:57:38 +08:00
kuaifan
4b89eb88bd perf: 优化AI群聊 2024-12-06 16:03:42 +08:00
spylecym
646a5e3b28 fix: 修复官网-帮助中心图片替换 2024-12-06 15:39:09 +08:00
kuaifan
08153cd99b perf: 支持AI在项目群里创建任务 2024-12-06 07:16:22 +08:00
kuaifan
61ebbac333 perf: 优化AI上下文 2024-12-05 11:15:28 +08:00
kuaifan
d63c1f156f perf: 优化AI上下文 2024-12-05 07:12:51 +08:00
kuaifan
a4548e2cba fix: 可见非共享文件夹的情况 2024-12-05 00:21:47 +08:00
kuaifan
77a3f2027e perf: 优化客户端会议打开速度 2024-12-05 00:00:59 +08:00
kuaifan
ecf0c78993 no message 2024-12-04 13:57:33 +08:00
kuaifan
1a0c1e3306 no message 2024-12-04 13:42:54 +08:00
kuaifan
506207d3ba no message 2024-12-03 14:25:26 +08:00
kuaifan
76bf46c152 perf: 支持通过接口发送通知和模板消息 2024-12-03 14:25:04 +08:00
kuaifan
96c64fbb91 no message 2024-12-03 13:12:48 +08:00
kuaifan
7fedb7d275 perf: 优化仪表盘任务避免重复统计 2024-12-03 09:14:55 +08:00
kuaifan
c16f316200 no message 2024-12-03 08:32:18 +08:00
kuaifan
4c5c071b21 no message 2024-12-03 08:30:56 +08:00
kuaifan
df917001d3 perf: 支持自定义AI个人提示词 2024-12-03 08:28:13 +08:00
kuaifan
65e75f974d perf: 优化客户端媒体浏览器 2024-12-02 20:50:10 +08:00
kuaifan
8afc1db72f perf: 优化客户端媒体浏览器 2024-12-02 18:41:35 +08:00
kuaifan
71f13a0b50 perf: 支持自定义上传图片压缩质量 2024-12-02 10:40:55 +08:00
kuaifan
4f57b195a8 perf: 优化与离职账号聊天 2024-12-02 09:11:25 +08:00
kuaifan
aa1ea41c5d perf: 优化邮件通知 2024-12-02 08:46:40 +08:00
kuaifan
b45058de72 perf: 优化未设置优先级的显示 2024-12-02 08:40:44 +08:00
kuaifan
576ab9a268 no message 2024-12-02 08:35:45 +08:00
kuaifan
e3312c97a7 perf: 添加任务模板 2024-12-02 08:34:22 +08:00
kuaifan
6bafa0a6dd perf: 添加任务模板 2024-12-02 00:55:48 +08:00
kuaifan
153d26ffcd perf: 添加任务模板 2024-12-01 23:50:01 +08:00
kuaifan
74fecdd941 perf: 添加任务模板 2024-12-01 23:09:03 +08:00
kuaifan
902844e008 perf: 添加任务模板 2024-12-01 20:54:38 +08:00
kuaifan
e78d850138 perf: 添加任务模板 2024-12-01 20:06:43 +08:00
kuaifan
94cefe52dd perf: 项目可自定义任务归档时间 2024-12-01 12:48:12 +08:00
kuaifan
a011f82912 perf: 优化快速添加任务 2024-12-01 02:46:42 +08:00
kuaifan
a160b2a471 perf: 支持通过职位名称搜索成员 2024-12-01 01:52:29 +08:00
kuaifan
396144f3fb perf: 会话页面支持查看头像 2024-12-01 01:25:59 +08:00
kuaifan
ff0fadc0c1 fix: 审批导致图片显示错误 2024-12-01 01:18:08 +08:00
kuaifan
65ec3a10bf perf: 优化文件列表 2024-12-01 01:08:21 +08:00
kuaifan
01c721c7e0 perf: 更新桌面客户端框架 2024-12-01 00:48:06 +08:00
kuaifan
d9aadb4f30 perf: 优化主题变化逻辑 2024-11-30 23:51:13 +08:00
kuaifan
964611eba4 fix: win子窗口无法激活的情况 2024-11-30 23:41:42 +08:00
kuaifan
98d2627036 no message 2024-11-30 14:18:15 +08:00
kuaifan
ba64540743 perf: 更新 AI 支持更多模型和支持提示词 2024-11-30 13:32:10 +08:00
kuaifan
62c50bb4e6 perf: 更新 AI 支持更多模型和支持提示词 2024-11-30 12:01:29 +08:00
kuaifan
0d4b005f4e perf: 更新 AI 支持更多模型和支持提示词 2024-11-30 08:26:13 +08:00
kuaifan
61b1206091 Merge commit '3f5c85b434d334f3a60fa5cabd3c933c4fafa28d' into pro
# Conflicts:
#	resources/mobile
2024-11-25 19:15:12 +08:00
kuaifan
2d37faea1d no message 2024-11-25 16:28:42 +08:00
weifs
3f5c85b434 perf: 优化表情滚动条 2024-11-22 09:08:30 +08:00
weifashi
d34bff28c5 perf: 优化表情滚动条 2024-11-21 23:56:22 +08:00
weifashi
8f622dd6a5 Merge branch 'pro' into o-pro 2024-11-21 00:52:22 +08:00
kuaifan
7fbd3bc760 perf: 优化深色主题下调整浏览器窗口显示白边的情况 2024-11-20 11:59:42 +08:00
770 changed files with 43329 additions and 18179 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

View File

@@ -115,9 +115,50 @@ jobs:
})
return data.id
build-client:
pack-vendor:
needs: [ check-version, create-release ]
if: needs.check-version.outputs.should_release == 'true'
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.0'
extensions: mbstring, intl, gd, xml, zip, swoole
tools: composer:v2
- name: Install Dependencies
run: composer install
- name: Create Vendor Archive
run: tar -czf vendor.tar.gz vendor/
- name: Upload Vendor Archive
uses: actions/github-script@v7
env:
RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
with:
script: |
const fs = require('fs');
const data = await fs.promises.readFile('vendor.tar.gz');
await github.rest.repos.uploadReleaseAsset({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: process.env.RELEASE_ID,
name: 'vendor.tar.gz',
data: data
});
build-client:
needs: [ check-version, create-release, pack-vendor ]
if: needs.check-version.outputs.should_release == 'true'
permissions:
contents: write
strategy:
@@ -231,7 +272,7 @@ jobs:
./cmd electron win
publish-release:
needs: [ check-version, create-release, build-client ]
needs: [ check-version, create-release, pack-vendor, build-client ]
if: needs.check-version.outputs.should_release == 'true' && github.ref == 'refs/heads/pro'
permissions:
contents: write
@@ -258,4 +299,7 @@ jobs:
env:
PUBLISH_KEY: ${{ secrets.PUBLISH_KEY }}
run: |
pushd electron || exit
npm install
popd || exit
node ./electron/build.js published

1
.gitignore vendored
View File

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

116
.prefetch
View File

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

View File

@@ -2,6 +2,429 @@
All notable changes to this project will be documented in this file.
## [0.45.64]
### Bug Fixes
- 修复部分页面出现空白的情况
- 修复输入框无法点击添加链接的情况
- 修复AI机器人不存在的情况
### Features
- 新增转发至AI开启新会话
### Performance
- 优化数据结构
- 优化图片存储名
- 优化消息窗口显示
- 优化目录结构
- 优化日历
- 优化任务时间范围选择
## [0.45.33]
### Bug Fixes
- 修复未读数错误暴增的情况
- 修复地址可能存在localhost的情况
- 修复消息编辑和发布时序号对不上
- 修复草稿出现上一次内容的情况
- 修复本地群消息通知没有会员昵称的问题
- 修复了拉人进群无法踢出去的问题
- 提及出现白色字的情况
### Features
- 添加移动端提示可能要发送的图片
### Performance
- 优化消息窗口
- 优化消息长按菜单
- 优化内置浏览器
- 优化App隐私政策提示
- 优化对话独立窗口显示
- 优化移动端选择交互
- 优化移动端选中消息文本
- 优化撤回消息逻辑
- 优化提及搜索
- 优化机器人Webhook消息
## [0.44.91]
### Features
- 添加我的机器人管理
### Performance
- 优化初始化逻辑
- 优化docker配置
## [0.44.82]
### Bug Fixes
- 修复搜索结果显示即将到期
### Features
- 新增独立窗口打开会话
### Performance
- 优化AI支持文件类型
## [0.44.74]
### Performance
- 优化AI解析文件
- 优化 WebSocket 消息
- 优化数据
## [0.44.67]
### Bug Fixes
- 修复查看待办图片不符的情况
### Performance
- 优化数据
- 优化未读消息数
- 优化搜索组件
- 已归档/已删除任务列表支持按状态检索
- 优化消息流效果
- 优化AI上下文
- 优化工作流获取
- 优化转发功能
## [0.44.53]
### Bug Fixes
- 工作流存在已离职人员
### Features
- 可点击标注图标查看标注人员
- 支持分享工作报告到消息
- 支持AI分析工作报告
- 支持使用%发送工作报告
- 新增自定义撤回及修改消息时限
### Performance
- 优化转发消息
- 优化工作报告列表
- 优化引用消息
- 优化全局提示
- 优化草稿消息
## [0.44.19]
### Bug Fixes
- 看不到未读消息定位提醒
### Performance
- 优化消息定位
- 优化消息性能
## [0.44.15]
### Bug Fixes
- 会话内消息搜索布局错位
- 流程设置翻译不统一
### Performance
- 优化消息定位
- 优化MD消息
## [0.44.3]
### Bug Fixes
- 定位签到失败的问题
### Performance
- 优化发送语音效果
- 录音转文字支持自定义语言
- 优化ES模块
- 优化emoji表情
- 按住Ctrl/Command键可连续选择表情
- Md消息支持html代码
- 优化脚本
- 优化安装命令
- 优化ES索引名称
## [0.43.73]
### Bug Fixes
- 全屏预览图片关闭窗口
- 点击排序导致任务不显示的情况
### Performance
- 新增录音转文字
- 优化数据排序
## [0.43.49]
### Performance
- 添加全局搜索功能
- 优化消息搜索
- 团队管理支持调整部门区域尺寸
- 任务详情支持调整聊天区域尺寸
- 优化团队部门支持3级部门
- 可见群组ID
- 支持在团队管理打开群聊
- 优化回复消息自动@逻辑
- 转发预览隐藏表情回应部分
- 优化任务日志
- 已删除任务支持按标签搜索
- 归档任务支持按标签搜索
- 项目面板添加按标签筛选
- 优化 AI 提示词
- 优化 AI 设置
## [0.43.18]
### Bug Fixes
- 首次跟ai聊天没有记录的问题
### Performance
- 工作报告支持查看仅未读
- AI 支持引用文件
- 优化图文消息
- 优化文本信息复制
- 优化样式
- 无法再AI机器人页面看到模型的问题
## [0.43.7]
### Features
- 添加 Grok AI、Ollama AI
### Performance
- 优化MD消息过长处理
- 优化AI支持分析指定文件
- 支持在AI对话中直接引用任务提问
- 优化 AI 参数
- 优化 Ollama AI
- 优化设置
- 优化AI设置
- 优化AI消息
## [0.42.85]
### Bug Fixes
- 撤回消息是消息列表不更新的情况
### Performance
- 表情回复时更新对话列表
- Onlyoffice 支持打开超过100m的文件
- 优化点击上传列表效果
- AI支持自定义模型列表
## [0.42.79]
### Bug Fixes
- 修复偶现的是子窗口出现身份丢失的情况
### Performance
- 优化查看长消息内容
## [0.42.74]
### Performance
- 优化审批功能
- AI机器人支持多会话
- AI机器人支持自定义模型
## [0.42.61]
### Performance
- 支持下载聊天引用的文件
- 优化翻译消息
- 支持显示思考过程
## [0.42.57]
### Bug Fixes
- 跨地区发消息出现消息过期的情况
- 多线程下载文件损坏的问题
- 修复新建周报或日报唯一标识重复
### Features
- 添加 DeepSeek AI
- 添加https证书自动更新
### Performance
- 支持自定义仪表盘欢迎词
- ChatGPT 支持自定义 Base URL
- 优化仪表盘任务更新规则
## [0.42.37]
### Bug Fixes
- 部分电脑无法复制的问题
- 修复任务可见性 - 任务重覆获取, 子任务负责人看不到任务问题
### Performance
- 更新小海豚表情包
- 优化任务时间冲突提示
- 优化消息
- 群聊总人数排除机器人
## [0.42.26]
### Bug Fixes
- 移交账号后工作流的负责人没有更新
- 全屏预览时深色皮肤反色的情况
### Features
- 替换网页的资源为本地资源
### Performance
- 优化任务面板
- 优化子任务的可见性
- 优化客户端
- 优化会议
- 优化会员搜索
- 优化打开会话
- 优化项目面板任务加载
- 优化客户端加载
## [0.42.3]
### Performance
- 优化对话阅读状况
- 优化表情回复
## [0.42.0]
### Bug Fixes
- 桌面端查看表情图片缩略图显示错误
- 项目面板任务不显示的情况
- 修复移动任务子任务不跟随的情况
### Performance
- 优化桌面端数据处理
- 优化资源
- 优化数据流
## [0.41.93]
### Bug Fixes
- 复制文件权限判断
### Performance
- AI创建任务确认
- 优化项目面板
## [0.41.84]
### Bug Fixes
- @在线状态不正确
### Performance
- 优化子任务上下文
- 优化子任务时间调整
- 优化超长文本信息
- 记录版本信息
- 支持更多办公文件格式
- 请假或外出时取消打卡提醒
- 图片容错处理
- 优化全局监听事件
- 优化数据流消息
## [0.41.64]
### Bug Fixes
- 修复使用AI创建任务顺序错误的问题
## [0.41.55]
### Performance
- 优化媒体播放
- 优化临时会话的消息推送
- 优化任务时间显示
## [0.41.45]
### Bug Fixes
- 修复官网-帮助中心图片替换
- 可见非共享文件夹的情况
- 审批导致图片显示错误
- Win子窗口无法激活的情况
### Performance
- Upgrade office
- 添加项目任务标签功能
- Upgrade drawio
- 优化AI创建任务
- 优化已读数据
- 优化AI群聊
- 支持AI在项目群里创建任务
- 优化AI上下文
- 优化客户端会议打开速度
- 支持通过接口发送通知和模板消息
- 优化仪表盘任务避免重复统计
- 支持自定义AI个人提示词
- 优化客户端媒体浏览器
- 支持自定义上传图片压缩质量
- 优化与离职账号聊天
- 优化邮件通知
- 优化未设置优先级的显示
- 添加任务模板
- 项目可自定义任务归档时间
- 优化快速添加任务
- 支持通过职位名称搜索成员
- 会话页面支持查看头像
- 优化文件列表
- 更新桌面客户端框架
- 优化主题变化逻辑
- 更新 AI 支持更多模型和支持提示词
## [0.40.78]
### Bug Fixes
@@ -19,6 +442,7 @@ All notable changes to this project will be documented in this file.
- 重复添加任务的情况
- 重复添加任务列表的情况
- 优化消息样式
- 优化表情滚动条
- 优化websocket消息
- 优化快捷选择
- 延期任务支持快选时间
@@ -26,6 +450,7 @@ All notable changes to this project will be documented in this file.
- 新增文件打包下载权限设置
- 升级electron框架
- 优化深色主题
- 优化深色主题下调整浏览器窗口显示白边的情况
- 优化表情包资源
- 优化客户端子窗口
- 优化项目列表

View File

@@ -47,16 +47,6 @@ cd dootask
./cmd port 80
```
### Change App Url
```bash
# This URL only affects the email reply.
./cmd url {Your domain url}
# example:
./cmd url https://domain.com
```
### Stop server
```bash

View File

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

View File

@@ -0,0 +1,254 @@
<?php
namespace App\Console\Commands;
use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialogUser;
use App\Module\ElasticSearch\ElasticSearchKeyValue;
use App\Module\ElasticSearch\ElasticSearchUserMsg;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
class SyncDialogUserMsgToElasticsearch extends Command
{
/**
* 更新数据
* --f: 全量更新 (默认)
* --i: 增量更新从上次更新的最后一个ID接上
*
* 清理数据
* --c: 清除索引
*/
protected $signature = 'elasticsearch:sync-dialog-user-msg {--f} {--i} {--c} {--batch=500}';
protected $description = '同步聊天会话用户和消息到Elasticsearch';
protected $es;
/**
* SyncDialogUserMsgToElasticsearch constructor.
*/
public function __construct()
{
parent::__construct();
try {
$this->es = new ElasticSearchUserMsg();
} catch (\Exception $e) {
$this->error('Elasticsearch连接失败: ' . $e->getMessage());
exit(1);
}
}
/**
* @return int
* @throws \Exception
*/
public function handle()
{
$this->info('开始同步聊天数据...');
// 清除索引
if ($this->option('c')) {
$this->info('清除索引...');
if (!$this->es->indexExists()) {
$this->saveLastId(true);
$this->info('索引不存在');
return 0;
}
$result = $this->es->deleteIndex();
if (isset($result['error'])) {
$this->error('删除索引失败: ' . $result['error']);
return 1;
}
$this->saveLastId(true);
$this->info('索引删除成功');
return 0;
}
// 判断创建索引
if (!$this->es->indexExists()) {
$this->info('创建索引...');
$result = ElasticSearchUserMsg::generateIndex();
if (isset($result['error'])) {
$this->error('创建索引失败: ' . $result['error']);
return 1;
}
$this->saveLastId(true);
$this->info('索引创建成功');
}
// 同步用户-会话数据
$this->syncDialogUsers($this->option('batch'));
// 同步消息数据
$this->syncDialogMsgs($this->option('batch'));
// 完成
$this->info("\n同步完成");
return 0;
}
/**
* 保存最后一个ID
* @param string|true $type
* @param integer $lastId
*/
private function saveLastId($type, $lastId = 0)
{
if ($type === true) {
$setting = [];
} else {
$setting = ElasticSearchKeyValue::getArray('elasticSearch:sync');
$setting[$type] = $lastId;
}
ElasticSearchKeyValue::save('elasticSearch:sync', $setting);
}
/**
* 获取最后一个ID
* @param $type
* @return int
*/
private function getLastId($type)
{
if ($this->option('i')) {
$setting = ElasticSearchKeyValue::getArray('elasticSearch:sync');
return intval($setting[$type] ?? 0);
}
return 0;
}
/**
* 同步用户-会话数据(父文档)
* @param $batchSize
* @return void
*/
private function syncDialogUsers($batchSize)
{
$this->info("\n同步用户数据...");
$lastId = $this->getLastId('dialog_user');
$num = 0;
$count = WebSocketDialogUser::where('id', '>', $lastId)->count();
do {
// 获取一批用户-会话关系
$dialogUsers = WebSocketDialogUser::where('id', '>', $lastId)
->orderBy('id')
->limit($batchSize)
->get();
if ($dialogUsers->isEmpty()) {
break;
}
$num += count($dialogUsers);
$progress = round($num / $count * 100, 2);
$this->info("{$num}/{$count} ({$progress}%) 正在同步用户ID {$lastId} ~ {$dialogUsers->last()->id}");
// 批量索引数据
$params = ['body' => []];
foreach ($dialogUsers as $dialogUser) {
$params['body'][] = [
'index' => [
'_index' => ElasticSearchUserMsg::indexName(),
'_id' => ElasticSearchUserMsg::generateUserDicId($dialogUser),
]
];
$params['body'][] = ElasticSearchUserMsg::generateUserFormat($dialogUser);
}
if ($params['body']) {
$result = $this->es->bulk($params);
if (isset($result['errors']) && $result['errors']) {
$this->error('批量索引用户数据部分失败');
Log::error('Elasticsearch批量索引失败: ' . json_encode($result['items']));
}
}
$lastId = $dialogUsers->last()->id;
$this->saveLastId('dialog_user', $lastId);
} while (count($dialogUsers) == $batchSize);
$this->info("同步用户数据结束 - 最后ID {$lastId}");
}
/**
* 同步消息数据(子文档)
*/
private function syncDialogMsgs($batchSize)
{
$this->info("\n同步消息数据...");
$lastId = $this->getLastId('dialog_msg');
$num = 0;
$count = WebSocketDialogMsg::where('id', '>', $lastId)->count();
do {
// 获取一批消息
$dialogMsgs = WebSocketDialogMsg::where('id', '>', $lastId)
->orderBy('id')
->limit($batchSize)
->get();
if ($dialogMsgs->isEmpty()) {
break;
}
$num += count($dialogMsgs);
$progress = round($num / $count * 100, 2);
$this->info("{$num}/{$count} ({$progress}%) 正在同步消息ID {$lastId} ~ {$dialogMsgs->last()->id}");
// 获取这些消息所属的会话对应的所有用户
$dialogIds = $dialogMsgs->pluck('dialog_id')->unique()->toArray();
$userDialogMap = [];
if (!empty($dialogIds)) {
$dialogUsers = WebSocketDialogUser::whereIn('dialog_id', $dialogIds)->get();
foreach ($dialogUsers as $dialogUser) {
$userDialogMap[$dialogUser->dialog_id][] = $dialogUser->userid;
}
}
// 批量索引消息数据
$params = ['body' => []];
foreach ($dialogMsgs as $dialogMsg) {
// 如果该会话没有用户,跳过
if (empty($userDialogMap[$dialogMsg->dialog_id])) {
continue;
}
// 为每个用户-会话关系创建子文档
foreach ($userDialogMap[$dialogMsg->dialog_id] as $userid) {
$params['body'][] = [
'index' => [
'_index' => ElasticSearchUserMsg::indexName(),
'_id' => ElasticSearchUserMsg::generateMsgDicId($dialogMsg, $userid),
'routing' => ElasticSearchUserMsg::generateMsgParentId($dialogMsg, $userid) // 路由到父文档
]
];
$params['body'][] = ElasticSearchUserMsg::generateMsgFormat($dialogMsg, $userid);
}
}
if (!empty($params['body'])) {
// 分批处理
$chunks = array_chunk($params['body'], 1000);
foreach ($chunks as $chunk) {
$chunkParams = ['body' => $chunk];
$result = $this->es->bulk($chunkParams);
if (isset($result['errors']) && $result['errors']) {
$this->error('批量索引消息数据部分失败');
Log::error('Elasticsearch批量索引失败: ' . json_encode($result['items']));
}
}
}
$lastId = $dialogMsgs->last()->id;
$this->saveLastId('dialog_msg', $lastId);
} while (count($dialogMsgs) == $batchSize);
$this->info("同步消息结束 - 最后ID {$lastId}");
}
}

View File

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

@@ -223,6 +223,19 @@ class Handler extends ExceptionHandler
} catch (\ImagickException) { }
}
// 容错处理
$patternFault = '/^(images\/.*\.(png|jpg|jpeg))\/crop\/([^\/]+)$/';
$matchesFault = null;
if (preg_match($patternFault, $path, $matchesFault)) {
$file = public_path($matchesFault[1]);
if (!file_exists($file)) {
$file = public_path('images/other/imgerr.jpg');
}
if (file_exists($file)) {
return response()->file($file);
}
}
return null;
}
}

View File

@@ -16,6 +16,7 @@ use App\Tasks\PushTask;
use App\Module\BillExport;
use App\Models\WebSocketDialog;
use App\Models\ApproveProcMsg;
use App\Models\ApproveProcInstHistory;
use App\Exceptions\ApiException;
use App\Models\UserDepartment;
use App\Models\WebSocketDialogMsg;
@@ -996,7 +997,7 @@ class ApproveController extends AbstractController
if ($thumb && file_exists(public_path($thumb))) {
$imageSize = getimagesize(public_path($thumb));
$data['thumb'] = [
'url' => $thumb,
'url' => Base::fillUrl($thumb),
'width' => $imageSize[0],
'height' => $imageSize[1]
];
@@ -1146,13 +1147,9 @@ class ApproveController extends AbstractController
*/
public function user__status()
{
$data['userid'] = intval(Request::input('userid'));
$ret = Ihttp::ihttp_get($this->flow_url . '/api/v1/workflow/process/getUserApprovalStatus?' . http_build_query($data));
$procdef = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (isset($procdef['status']) && $procdef['status'] == 200) {
return Base::retSuccess('success', $procdef['data']["proc_def_name"] ?? '');
}
return Base::retSuccess('success', '');
$userid = intval(Request::input('userid'));
$status = ApproveProcInstHistory::getUserApprovalStatus($userid);
return Base::retSuccess('success', $status);
}
/**

File diff suppressed because it is too large Load Diff

View File

@@ -47,8 +47,7 @@ class FileController extends AbstractController
{
$user = User::auth();
//
$data = Request::all();
$pid = intval($data['pid']);
$pid = intval(Request::input('pid'));
//
return Base::retSuccess('success', (new File)->getFileList($user, $pid));
}
@@ -88,6 +87,7 @@ class FileController extends AbstractController
}
return Base::retError($msg, $data);
}
$fileLink->increment("num");
} else {
return Base::retError('参数错误');
}
@@ -107,6 +107,7 @@ class FileController extends AbstractController
*
* @apiParam {String} [link] 通过分享地址搜索https://t.hitosea.com/single/file/ODcwOCwzOSxpa0JBS2lmVQ==
* @apiParam {String} [key] 关键词
* @apiParam {Number} [take] 获取数量默认50最大100
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -119,7 +120,7 @@ class FileController extends AbstractController
$link = trim(Request::input('link'));
$key = trim(Request::input('key'));
$id = 0;
$take = 50;
$take = Base::getPaginate(100, 50, 'take');
if (preg_match("/\/single\/file\/(.*?)$/i", $link, $match)) {
$id = intval(FileLink::whereCode($match[1])->value('file_id'));
$take = 1;
@@ -302,6 +303,7 @@ class FileController extends AbstractController
//
$userid = $user->userid;
if ($row->pid > 0) {
File::permissionFind($row->pid, $user, 1);
$userid = intval(File::whereId($row->pid)->value('userid'));
}
//
@@ -553,7 +555,7 @@ class FileController extends AbstractController
$tmpPath = "uploads/file/document/" . date("Ym") . "/" . $id . "/attached/";
Base::makeDir(public_path($tmpPath));
$tmpPath .= md5($text) . "." . $matchs[1][$key];
if (Base::saveContentImage(public_path($tmpPath), base64_decode($text), 90)) {
if (Base::saveContentImage(public_path($tmpPath), base64_decode($text))) {
$paramet = getimagesize(public_path($tmpPath));
$data['content'] = str_replace($matchs[0][$key], '<img src="' . Base::fillUrl($tmpPath) . '" original-width="' . $paramet[0] . '" original-height="' . $paramet[1] . '"', $data['content']);
$isRep = true;
@@ -664,7 +666,7 @@ class FileController extends AbstractController
//
if ($status === 2) {
$parse = parse_url($url);
$from = 'http://' . env('APP_IPPR') . '.3' . $parse['path'] . '?' . $parse['query'];
$from = 'http://nginx' . $parse['path'] . '?' . $parse['query'];
$path = 'uploads/file/' . $file->type . '/' . date("Ym") . '/' . $file->id . '/' . $key;
$save = public_path($path);
Base::makeDir(dirname($save));

View File

@@ -39,6 +39,8 @@ use App\Module\BillMultipleExport;
use Illuminate\Support\Facades\DB;
use App\Models\ProjectTaskFlowChange;
use App\Models\ProjectTaskVisibilityUser;
use App\Models\ProjectTaskTemplate;
use App\Models\ProjectTag;
/**
* @apiDefine project
@@ -286,6 +288,8 @@ class ProjectController extends AbstractController
* @apiParam {Number} project_id 项目ID
* @apiParam {String} name 项目名称
* @apiParam {String} [desc] 项目介绍
* @apiParam {String} [archive_method] 归档方式
* @apiParam {Number} [archive_days] 自动归档天数
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -298,6 +302,8 @@ class ProjectController extends AbstractController
$project_id = intval(Request::input('project_id'));
$name = trim(Request::input('name', ''));
$desc = trim(Request::input('desc', ''));
$archive_method = Request::input('archive_method');
$archive_days = intval(Request::input('archive_days'));
if (mb_strlen($name) < 2) {
return Base::retError('项目名称不可以少于2个字');
} elseif (mb_strlen($name) > 32) {
@@ -306,9 +312,14 @@ class ProjectController extends AbstractController
if (mb_strlen($desc) > 255) {
return Base::retError('项目介绍最多只能设置255个字');
}
if ($archive_method == 'custom') {
if ($archive_days < 1 || $archive_days > 365) {
return Base::retError('自动归档天数设置错误范围1-365');
}
}
//
$project = Project::userProject($project_id, true, true);
AbstractModel::transaction(function () use ($desc, $name, $project) {
AbstractModel::transaction(function () use ($archive_days, $archive_method, $desc, $name, $project) {
if ($project->name != $name) {
$project->addLog("修改项目名称", [
'change' => [$project->name, $name]
@@ -322,6 +333,18 @@ class ProjectController extends AbstractController
$project->desc = $desc;
$project->addLog("修改项目介绍");
}
if ($project->archive_method != $archive_method) {
$project->addLog("修改归档方式", [
'change' => [$project->archive_method, $archive_method]
]);
$project->archive_method = $archive_method;
}
if ($project->archive_method == 'custom') {
$project->addLog("修改自动归档天数", [
'change' => [$project->archive_days, $archive_days]
]);
$project->archive_days = $archive_days;
}
$project->save();
});
$project->pushMsg('update');
@@ -918,7 +941,9 @@ class ProjectController extends AbstractController
* @apiName task__lists
*
* @apiParam {Object} [keys] 搜索条件
* - keys.name: ID、任务名称
* - keys.name: ID、任务名称、任务描述
* - keys.tag: 标签名称
* - keys.status: 任务状态 (completed: 已完成、uncompleted: 未完成、flow-xx: 流程状态ID)
*
* @apiParam {Number} [project_id] 项目ID
* @apiParam {Number} [parent_id] 主任务IDproject_id && parent_id ≤ 0 时 仅查询自己参与的任务)
@@ -971,7 +996,29 @@ class ProjectController extends AbstractController
if (Base::isNumber($keys['name'])) {
$builder->where("project_tasks.id", intval($keys['name']));
} else {
$builder->where("project_tasks.name", "like", "%{$keys['name']}%");
$builder->where(function ($query) use ($keys) {
$query->where("project_tasks.name", "like", "%{$keys['name']}%");
$query->orWhere("project_tasks.desc", "like", "%{$keys['name']}%");
});
}
}
if ($keys['tag']) {
$builder->whereHas('taskTag', function ($query) use ($keys) {
$query->where('project_task_tags.name', $keys['tag']);
});
}
if ($keys['status']) {
if ($keys['status'] == 'completed') {
$builder->whereNotNull('project_tasks.complete_at');
} elseif ($keys['status'] == 'uncompleted') {
$builder->whereNull('project_tasks.complete_at');
} elseif (str_starts_with($keys['status'], 'flow-')) {
$flow = str_replace('flow-', '', $keys['status']);
if (Base::isNumber($flow)) {
$builder->where('project_tasks.flow_item_id', intval($flow));
} elseif ($flow) {
$builder->where('project_tasks.flow_item_name', 'like', "%{$flow}%");
}
}
}
//
@@ -1035,20 +1082,20 @@ class ProjectController extends AbstractController
$query->where('project_users.owner', 1);
$query->where('project_users.userid', $userid);
});
$builder->leftJoin('project_task_users as project_sub_task_users', function ($query) use($userid) {
$query->on('project_sub_task_users.task_pid', '=', 'project_tasks.parent_id');
$query->where('project_sub_task_users.userid', $userid);
});
$builder->leftJoin('project_task_visibility_users', function ($query) use($userid) {
$query->on('project_task_visibility_users.task_id', '=', 'project_tasks.id');
$query->where('project_task_visibility_users.userid', $userid);
});
$builder->leftJoin('project_task_visibility_users as project_sub_task_visibility_users', function ($query) use($userid) {
$query->on('project_sub_task_visibility_users.task_id', '=', 'project_tasks.parent_id');
$query->where('project_sub_task_visibility_users.userid', $userid);
});
$builder->where(function ($query) use ($userid) {
$query->where("project_tasks.visibility", 1);
$query->orWhere("project_users.userid", $userid);
$query->orWhere("project_task_users.userid", $userid);
$query->orWhere("project_task_visibility_users.userid", $userid);
$query->orWhere("project_sub_task_users.userid", $userid);
$query->orWhere("project_sub_task_visibility_users.userid", $userid);
});
// 优化子查询汇总
$builder->leftJoinSub(function ($query) {
@@ -1136,6 +1183,7 @@ class ProjectController extends AbstractController
$list = ProjectTask::with(['taskUser'])
->select([
'projects.name as project_name',
'project_tasks.project_id',
'project_tasks.id',
'project_tasks.name',
'project_tasks.start_at',
@@ -1216,6 +1264,7 @@ class ProjectController extends AbstractController
$headings[] = Doo::translate('父级任务ID');
$headings[] = Doo::translate('所属项目');
$headings[] = Doo::translate('任务标题');
$headings[] = Doo::translate('任务标签');
$headings[] = Doo::translate('任务开始时间');
$headings[] = Doo::translate('任务结束时间');
$headings[] = Doo::translate('完成时间');
@@ -1236,7 +1285,7 @@ class ProjectController extends AbstractController
'style' => 'font-weight: bold;padding-bottom: 4px;',
];
//
$builder = ProjectTask::select(['project_tasks.*', 'project_task_users.userid as ownerid'])
$builder = ProjectTask::with(['taskTag'])->select(['project_tasks.*', 'project_task_users.userid as ownerid'])
->join('project_task_users', 'project_tasks.id', '=', 'project_task_users.task_id')
->where('project_task_users.owner', 1)
->whereIn('project_task_users.userid', $userid)
@@ -1318,6 +1367,9 @@ class ProjectController extends AbstractController
$task->parent_id ?: '-',
Base::filterEmoji($task->project?->name) ?: '-',
Base::filterEmoji($task->name),
$task->taskTag->map(function ($tag) {
return Base::filterEmoji($tag->name);
})->join(', ') ?: '-',
$task->start_at ?: '-',
$task->end_at ?: '-',
$task->complete_at ?: '-',
@@ -1440,6 +1492,7 @@ class ProjectController extends AbstractController
$headings[] = Doo::translate('父级任务ID');
$headings[] = Doo::translate('所属项目');
$headings[] = Doo::translate('任务标题');
$headings[] = Doo::translate('任务标签');
$headings[] = Doo::translate('任务开始时间');
$headings[] = Doo::translate('任务结束时间');
$headings[] = Doo::translate('任务计划用时');
@@ -1448,7 +1501,8 @@ class ProjectController extends AbstractController
$headings[] = Doo::translate('创建人');
$data = [];
//
ProjectTask::whereNull('complete_at')
ProjectTask::with(['taskTag'])
->whereNull('complete_at')
->whereNotNull('end_at')
->where('end_at', '<=', Carbon::now())
->orderBy('end_at')
@@ -1479,11 +1533,14 @@ class ProjectController extends AbstractController
$task->parent_id ?: '-',
Base::filterEmoji($task->project?->name) ?: '-',
Base::filterEmoji($task->name),
$task->taskTag->map(function ($tag) {
return Base::filterEmoji($tag->name);
})->join(', ') ?: '-',
$task->start_at ?: '-',
$task->end_at ?: '-',
$planTime,
$overTime,
implode("", $ownerNames),
implode(', ', $ownerNames),
Base::filterEmoji(User::userid2nickname($task->userid)) . " (ID: {$task->userid})",
];
}
@@ -1820,7 +1877,7 @@ class ProjectController extends AbstractController
}
//
$filePath = public_path($file->getRawOriginal('path'));
return Base::BinaryFileResponse($filePath, $file->name);
return Base::DownloadFileResponse($filePath, $file->name);
}
/**
@@ -1972,6 +2029,7 @@ class ProjectController extends AbstractController
* @apiParam {String} [content] 任务详情(子任务不支持)
* @apiParam {String} [color] 背景色(子任务不支持)
* @apiParam {Array} [assist] 修改协助人员(子任务不支持)
* @apiParam {Array} [task_tag] 任务标签(子任务不支持)
* @apiParam {Number} [visibility] 修改可见性
* @apiParam {Array} [visibility_appointor] 修改可见性人员
*
@@ -2264,8 +2322,8 @@ class ProjectController extends AbstractController
* @apiGroup project
* @apiName task__flow
*
* @apiParam {Number} task_id 任务ID
* @apiParam {Number} project_id 项目ID - 存在时只返回这个项目的
* @apiParam {Number} [task_id] 任务ID
* @apiParam {Number} [project_id] 项目ID存在时只返回这个项目的工作流,主要用于任务移动到其他项目时)
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -2366,6 +2424,7 @@ class ProjectController extends AbstractController
*/
public function task__move()
{
Base::checkClientVersion('0.42.0');
User::auth();
//
$task_id = intval(Request::input('task_id'));
@@ -2403,9 +2462,26 @@ class ProjectController extends AbstractController
//
$task->moveTask($project_id, $column_id, $flow_item_id, $owner, $assist, $completeAt);
//
$task = ProjectTask::userTask($task_id);
$data = [];
$mainTask = ProjectTask::userTask($task_id)?->toArray();
if ($mainTask) {
$mainTask['column_name'] = ProjectColumn::whereId($mainTask['column_id'])->value('name');
$mainTask['project_name'] = Project::whereId($mainTask['project_id'])->value('name');
$data[] = $mainTask;
//
$subTasks = ProjectTask::whereParentId($task_id)->get();
foreach ($subTasks as $subTask) {
$data[] = [
'id' => $subTask->id,
'project_id' => $subTask->project_id,
'column_id' => $subTask->column_id,
'column_name' => $mainTask['column_name'],
'project_name' => $mainTask['project_name'],
];
}
}
//
return Base::retSuccess('移动成功', $task);
return Base::retSuccess('移动成功', $data);
}
/**
@@ -2534,7 +2610,7 @@ class ProjectController extends AbstractController
$builder->with(['projectTask:id,parent_id,name'])->whereProjectId($project->id)->whereTaskOnly(0);
}
//
$list = $builder->orderByDesc('created_at')->paginate(Base::getPaginate(100, 20));
$list = $builder->orderByDesc('created_at')->orderByDesc('id')->paginate(Base::getPaginate(100, 20));
$list->transform(function (ProjectLog $log) use ($task_id) {
$timestamp = Carbon::parse($log->created_at)->timestamp;
if ($task_id === 0) {
@@ -2672,4 +2748,291 @@ class ProjectController extends AbstractController
$projectPermission = ProjectPermission::updatePermissions($projectId, Base::newArrayRecursive('intval', $permissions));
return Base::retSuccess("success", $projectPermission);
}
/**
* @api {get} api/project/task/template_list 47. 任务模板列表
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup project
* @apiName task__template_list
*
* @apiParam {Number} project_id 项目ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function task__template_list()
{
User::auth();
//
$projectId = intval(Request::input('project_id'));
if (!$projectId) {
return Base::retError('参数错误');
}
$templates = ProjectTaskTemplate::where('project_id', $projectId)
->orderByDesc('id')
->get();
return Base::retSuccess('success', $templates);
}
/**
* @api {post} api/project/task/template_save 48. 保存任务模板
*
* @apiDescription 需要token身份项目负责人
* @apiVersion 1.0.0
* @apiGroup project
* @apiName task__template_save
*
* @apiParam {Number} project_id 项目ID
* @apiParam {Number} [id] 模板ID
* @apiParam {String} name 模板名称
* @apiParam {String} title 任务标题
* @apiParam {String} content 任务内容
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function task__template_save()
{
$user = User::auth();
//
$projectId = intval(Request::input('project_id'));
if (!$projectId) {
return Base::retError('参数错误');
}
Project::userProject($projectId, true, true);
//
$id = intval(Request::input('id', 0));
$name = trim(Request::input('name', ''));
$title = trim(Request::input('title', ''));
$content = trim(Request::input('content', ''));
if (empty($name)) {
return Base::retError('请输入模板名称');
}
if (empty($title) && empty($content)) {
return Base::retError('请输入任务标题或内容');
}
$data = [
'project_id' => $projectId,
'name' => $name,
'title' => $title,
'content' => $content,
'userid' => $user->userid
];
if ($id > 0) {
$template = ProjectTaskTemplate::where('id', $id)
->where('project_id', $projectId)
->first();
if (!$template) {
return Base::retError('模板不存在或已被删除');
}
$template->update($data);
} else {
$templateCount = ProjectTaskTemplate::where('project_id', $projectId)->count();
if ($templateCount >= 20) {
return Base::retError('每个项目最多添加20个模板');
}
$template = ProjectTaskTemplate::create($data);
}
return Base::retSuccess('保存成功', $template);
}
/**
* @api {get} api/project/task/template_delete 49. 删除任务模板
*
* @apiDescription 需要token身份项目负责人
* @apiVersion 1.0.0
* @apiGroup project
* @apiName task__template_delete
*
* @apiParam {Number} id 模板ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function task__template_delete()
{
User::auth();
//
$id = intval(Request::input('id'));
if (!$id) {
return Base::retError('参数错误');
}
$template = ProjectTaskTemplate::find($id);
if (!$template) {
return Base::retError('模板不存在或已被删除');
}
Project::userProject($template->project_id, true, true);
$template->delete();
return Base::retSuccess('删除成功');
}
/**
* @api {get} api/project/task/template_default 50. 设置(取消)任务模板为默认
*
* @apiDescription 需要token身份项目负责人
* @apiVersion 1.0.0
* @apiGroup project
* @apiName task__template_default
*
* @apiParam {Number} id 模板ID
* @apiParam {Number} project_id 项目ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function task__template_default()
{
User::auth();
//
$id = intval(Request::input('id'));
$projectId = intval(Request::input('project_id'));
if (!$id || !$projectId) {
return Base::retError('参数错误');
}
Project::userProject($projectId, true, true);
//
$template = ProjectTaskTemplate::where('id', $id)
->where('project_id', $projectId)
->first();
if (!$template) {
return Base::retError('模板不存在或已被删除');
}
if ($template->is_default) {
$template->update(['is_default' => false]);
return Base::retSuccess('取消成功');
}
//
ProjectTaskTemplate::where('project_id', $projectId)->update(['is_default' => false]);
$template->update(['is_default' => true]);
return Base::retSuccess('设置成功');
}
/**
* @api {post} api/project/tag/save 51. 保存标签
*
* @apiDescription 需要token身份修改项目负责人添加项目所有成员
* @apiVersion 1.0.0
* @apiGroup project
* @apiName tag__save
*
* @apiParam {Number} project_id 项目ID
* @apiParam {Number} [id] 标签ID
* @apiParam {String} name 标签名称
* @apiParam {String} desc 标签描述
* @apiParam {String} color 标签颜色
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function tag__save()
{
$user = User::auth();
//
$projectId = intval(Request::input('project_id'));
if (!$projectId) {
return Base::retError('参数错误');
}
//
$id = intval(Request::input('id', 0));
$name = trim(Request::input('name', ''));
$desc = trim(Request::input('desc', ''));
$color = trim(Request::input('color', ''));
if (empty($name)) {
return Base::retError('请输入标签名称');
}
if (empty($color)) {
return Base::retError('请选择标签颜色');
}
$data = [
'project_id' => $projectId,
'name' => $name,
'desc' => $desc,
'color' => $color,
'userid' => $user->userid
];
$project = Project::userProject($projectId, true, $id > 0 ? true : null);
if ($id > 0) {
$tag = ProjectTag::where('id', $id)
->where('project_id', $projectId)
->first();
if (!$tag) {
return Base::retError('标签不存在或已被删除');
}
$tag->update($data);
} else {
$tagCount = ProjectTag::where('project_id', $projectId)->count();
if ($tagCount >= 20) {
return Base::retError('每个项目最多添加20个标签');
}
$tag = ProjectTag::create($data);
$project->addLog("添加标签【" . $tag->name . "");
}
return Base::retSuccess('保存成功', $tag);
}
/**
* @api {get} api/project/tag/delete 52. 删除标签
*
* @apiDescription 需要token身份项目负责人
* @apiVersion 1.0.0
* @apiGroup project
* @apiName tag__delete
*
* @apiParam {Number} id 标签ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function tag__delete()
{
User::auth();
//
$id = intval(Request::input('id'));
if (!$id) {
return Base::retError('参数错误');
}
$tag = ProjectTag::find($id);
if (!$tag) {
return Base::retError('标签不存在或已被删除');
}
Project::userProject($tag->project_id, true, true);
$tag->delete();
return Base::retSuccess('删除成功');
}
/**
* @api {get} api/project/tag/list 53. 标签列表
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup project
* @apiName tag__list
*
* @apiParam {Number} project_id 项目ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function tag__list()
{
User::auth();
//
$projectId = intval(Request::input('project_id'));
if (!$projectId) {
return Base::retError('参数错误');
}
$tags = ProjectTag::where('project_id', $projectId)
->orderByDesc('id')
->get();
return Base::retSuccess('success', $tags);
}
}

View File

@@ -6,8 +6,10 @@ use App\Exceptions\ApiException;
use App\Models\AbstractModel;
use App\Models\ProjectTask;
use App\Models\Report;
use App\Models\ReportLink;
use App\Models\ReportReceive;
use App\Models\User;
use App\Models\WebSocketDialogMsg;
use App\Module\Base;
use App\Module\Doo;
use App\Tasks\PushTask;
@@ -28,11 +30,13 @@ class ReportController extends AbstractController
/**
* @api {get} api/report/my 01. 我发送的汇报
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup report
* @apiName my
*
* @apiParam {Object} [keys] 搜索条件
* - keys.key: 关键词
* - keys.type: 汇报类型weekly:周报daily:日报
* - keys.created_at: 汇报时间
* @apiParam {Number} [page] 当前页,默认:1
@@ -49,6 +53,15 @@ class ReportController extends AbstractController
$builder = Report::with(['receivesUser'])->whereUserid($user->userid);
$keys = Request::input('keys');
if (is_array($keys)) {
if ($keys['key']) {
if (str_contains($keys['key'], '@')) {
$builder->whereHas('sendUser', function ($q2) use ($keys) {
$q2->where("users.email", "LIKE", "%{$keys['key']}%");
});
} else {
$builder->where("title", "LIKE", "%{$keys['key']}%");
}
}
if (in_array($keys['type'], [Report::WEEKLY, Report::DAILY])) {
$builder->whereType($keys['type']);
}
@@ -64,13 +77,16 @@ class ReportController extends AbstractController
/**
* @api {get} api/report/receive 02. 我接收的汇报
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup report
* @apiName receive
*
* @apiParam {Object} [keys] 搜索条件
* - keys.key: 关键词
* - keys.department_id: 部门ID
* - keys.type: 汇报类型weekly:周报daily:日报
* - keys.status: 状态unread:未读read:已读
* - keys.created_at: 汇报时间
* @apiParam {Number} [page] 当前页,默认:1
* @apiParam {Number} [pagesize] 每页显示数量,默认:20最大:50
@@ -89,15 +105,29 @@ class ReportController extends AbstractController
$keys = Request::input('keys');
if (is_array($keys)) {
if ($keys['key']) {
$builder->where(function($query) use ($keys) {
$query->whereHas('sendUser', function ($q2) use ($keys) {
if (str_contains($keys['key'], '@')) {
$builder->whereHas('sendUser', function ($q2) use ($keys) {
$q2->where("users.email", "LIKE", "%{$keys['key']}%");
})->orWhere("title", "LIKE", "%{$keys['key']}%");
});
} elseif (Base::isNumber($keys['key'])) {
$builder->where("userid", intval($keys['key']));
} else {
$builder->where("title", "LIKE", "%{$keys['key']}%");
}
}
if ($keys['department_id']) {
$builder->whereHas('sendUser', function ($query) use ($keys) {
$query->where("users.department", "LIKE", "%,{$keys['department_id']},%");
});
}
if (in_array($keys['type'], [Report::WEEKLY, Report::DAILY])) {
$builder->whereType($keys['type']);
}
if (in_array($keys['status'], ['unread', 'read'])) {
$builder->whereHas("receivesUser", function ($query) use ($user, $keys) {
$query->where("report_receives.userid", $user->userid)->where("report_receives.read", $keys['status'] === 'unread' ? 0 : 1);
});
}
if (is_array($keys['created_at'])) {
if ($keys['created_at'][0] > 0) $builder->where('created_at', '>=', Base::newCarbon($keys['created_at'][0])->startOfDay());
if ($keys['created_at'][1] > 0) $builder->where('created_at', '<=', Base::newCarbon($keys['created_at'][1])->endOfDay());
@@ -115,6 +145,7 @@ class ReportController extends AbstractController
/**
* @api {get} api/report/store 03. 保存并发送工作汇报
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup report
* @apiName store
@@ -240,6 +271,7 @@ class ReportController extends AbstractController
/**
* @api {get} api/report/template 04. 生成汇报模板
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup report
* @apiName template
@@ -411,11 +443,13 @@ class ReportController extends AbstractController
/**
* @api {get} api/report/detail 05. 报告详情
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup report
* @apiName detail
*
* @apiParam {Number} [id] 报告id
* @apiParam {Number} [id] 报告ID
* @apiParam {String} [code] 报告分享代码与ID二选一优先ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -424,30 +458,43 @@ class ReportController extends AbstractController
public function detail(): array
{
$user = User::auth();
//
$id = intval(trim(Request::input("id")));
if (empty($id))
$code = trim(Request::input("code"));
//
if (empty($id) && empty($code)) {
return Base::retError("缺少ID参数");
$one = Report::getOne($id);
$one->type_val = $one->getRawOriginal("type");
// 标记为已读
if (!empty($one->receivesUser)) {
foreach ($one->receivesUser as $item) {
if ($item->userid === $user->userid && $item->pivot->read === 0) {
$one->receivesUser()->updateExistingPivot($user->userid, [
"read" => 1,
]);
}
//
if (!empty($id)) {
$one = Report::getOne($id);
$one->type_val = $one->getRawOriginal("type");
// 标记为已读
if (!empty($one->receivesUser)) {
foreach ($one->receivesUser as $item) {
if ($item->userid === $user->userid && $item->pivot->read === 0) {
$one->receivesUser()->updateExistingPivot($user->userid, [
"read" => 1,
]);
}
}
}
} else {
$link = ReportLink::whereCode($code)->first();
if (empty($link)) {
return Base::retError("报告不存在或已被删除");
}
$one = Report::getOne($link->rid);
$one->report_link = $link;
$link->increment("num");
}
return Base::retSuccess("success", $one);
}
/**
* @api {get} api/report/mark 06. 标记已读/未读
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup report
* @apiName mark
@@ -488,8 +535,71 @@ class ReportController extends AbstractController
}
/**
* @api {get} api/report/last_submitter 07. 获取最后一次提交的接收人
* @api {get} api/report/share 07. 分享报告到消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup report
* @apiName share
*
* @apiParam {Number} id 报告id
* @apiParam {Array} dialogids 转发给的对话ID
* @apiParam {Array} userids 转发给的成员ID
* @apiParam {String} leave_message 转发留言
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function share()
{
$user = User::auth();
//
$id = Request::input('id');
$dialogids = Request::input('dialogids');
$userids = Request::input('userids');
$leave_message = Request::input('leave_message');
//
if (is_array($id)) {
if (count(Base::arrayRetainInt($id)) > 20) {
return Base::retError("最多只能操作20条数据");
}
$builder = Report::whereIn("id", Base::arrayRetainInt($id));
} else {
$builder = Report::whereId(intval($id));
}
$reportMsgs = [];
$builder ->chunkById(100, function ($list) use (&$reportMsgs, $user) {
/** @var Report $item */
foreach ($list as $item) {
$reportLink = ReportLink::generateLink($item->id, $user->userid);
$reportMsgs[] = "<a class=\"mention report\" href=\"{{RemoteURL}}single/report/detail/{$reportLink['code']}\" target=\"_blank\">%{$item->title}</a>";
}
});
if (empty($reportMsgs)) {
return Base::retError("报告不存在或已被删除");
}
$reportTag = count($reportMsgs) > 1 ? 'li' : 'p';
$reportAttr = $reportTag === 'li' ? ' data-list="ordered"' : '';
$reportMsgs = array_map(function ($item) use ($reportAttr, $reportTag) {
return "<{$reportTag}{$reportAttr}>{$item}</{$reportTag}>";
}, $reportMsgs);
if ($reportTag === 'li') {
array_unshift($reportMsgs, "<ol>");
$reportMsgs[] = "</ol>";
}
if ($leave_message) {
$reportMsgs[] = "<p>{$leave_message}</p>";
}
$msgText = implode("", $reportMsgs);
//
return WebSocketDialogMsg::sendMsgBatch($user, $userids, $dialogids, $msgText);
}
/**
* @api {get} api/report/last_submitter 08. 获取最后一次提交的接收人
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup report
* @apiName last_submitter
@@ -505,8 +615,9 @@ class ReportController extends AbstractController
}
/**
* @api {get} api/report/unread 08. 获取未读
* @api {get} api/report/unread 09. 获取未读
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup report
* @apiName unread
@@ -529,8 +640,9 @@ class ReportController extends AbstractController
}
/**
* @api {get} api/report/read 09. 标记汇报已读,可批量
* @api {get} api/report/read 10. 标记汇报已读,可批量
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup report
* @apiName read

View File

@@ -41,7 +41,7 @@ class SystemController extends AbstractController
* @apiParam {String} type
* - get: 获取(默认)
* - all: 获取所有(需要管理员权限)
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'temp_account_alias', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'voice2text', 'translation', 'e2e_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'all_group_mute', 'all_group_autoin', 'user_private_chat_mute', 'user_group_chat_mute', 'system_alias', 'image_compress', 'image_save_local', 'start_home']
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'temp_account_alias', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'voice2text', 'translation', 'e2e_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'all_group_mute', 'all_group_autoin', 'user_private_chat_mute', 'user_group_chat_mute', 'system_alias', 'system_welcome', 'image_compress', 'image_quality', 'image_save_local', 'start_home']
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -54,6 +54,7 @@ class SystemController extends AbstractController
if (env("SYSTEM_SETTING") == 'disabled') {
return Base::retError('当前环境禁止修改');
}
Base::checkClientVersion('0.41.11');
User::auth('admin');
$all = Request::input();
foreach ($all AS $key => $value) {
@@ -70,6 +71,8 @@ class SystemController extends AbstractController
'voice2text',
'translation',
'e2e_message',
'msg_rev_limit',
'msg_edit_limit',
'auto_archived',
'archived_day',
'task_visible',
@@ -79,7 +82,9 @@ class SystemController extends AbstractController
'user_private_chat_mute',
'user_group_chat_mute',
'system_alias',
'system_welcome',
'image_compress',
'image_quality',
'image_save_local',
'start_home',
'file_upload_limit',
@@ -97,15 +102,18 @@ class SystemController extends AbstractController
return Base::retError('自动归档时间不可大于100天');
}
}
if ($all['voice2text'] == 'open' && empty(Base::settingFind('aibotSetting', 'openai_key'))) {
if ($all['voice2text'] == 'open' && !Setting::AIOpen()) {
return Base::retError('开启语音转文字功能需要在应用中开启 ChatGPT AI 机器人。');
}
if ($all['translation'] == 'open' && empty(Base::settingFind('aibotSetting', 'openai_key'))) {
if ($all['translation'] == 'open' && !Setting::AIOpen()) {
return Base::retError('开启翻译功能需要在应用中开启 ChatGPT AI 机器人。');
}
if ($all['system_alias'] == env('APP_NAME')) {
$all['system_alias'] = '';
}
if ($all['system_welcome'] == '欢迎您,{username}') {
$all['system_welcome'] = '';
}
$setting = Base::setting('system', Base::newTrim($all));
} else {
$setting = Base::setting('system');
@@ -129,6 +137,8 @@ class SystemController extends AbstractController
$setting['voice2text'] = $setting['voice2text'] ?: 'close';
$setting['translation'] = $setting['translation'] ?: 'close';
$setting['e2e_message'] = $setting['e2e_message'] ?: 'close';
$setting['msg_rev_limit'] = $setting['msg_rev_limit'] ?: '';
$setting['msg_edit_limit'] = $setting['msg_edit_limit'] ?: '';
$setting['auto_archived'] = $setting['auto_archived'] ?: 'close';
$setting['archived_day'] = floatval($setting['archived_day']) ?: 7;
$setting['task_visible'] = $setting['task_visible'] ?: 'close';
@@ -136,8 +146,6 @@ 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['image_compress'] = $setting['image_compress'] ?: 'open';
$setting['image_save_local'] = $setting['image_save_local'] ?: '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';
@@ -282,100 +290,46 @@ class SystemController extends AbstractController
*
* @apiParam {String} type
* - get: 获取(默认)
* - save: 保存设置(参数:['openai_key', 'openai_agency', 'claude_token', 'claude_agency']
* - save: 保存设置(参数:[...]
* @apiParam {String} filter 过滤字段(可选)
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function setting__aibot()
{
$user = User::auth('admin');
User::auth('admin');
//
$type = trim(Request::input('type'));
$filter = trim(Request::input('filter'));
$setting = Base::setting('aibotSetting');
$keys = [
'openai_key',
'openai_agency',
'openai_model',
'claude_token',
'claude_agency',
'wenxin_key',
'wenxin_secret',
'wenxin_model',
'qianwen_key',
'qianwen_model',
'gemini_key',
'gemini_model',
'gemini_agency',
'zhipu_key',
'zhipu_model',
];
if ($type == 'save') {
if (env("SYSTEM_SETTING") == 'disabled') {
return Base::retError('当前环境禁止修改');
}
Base::checkClientVersion('0.41.11');
$all = Request::input();
foreach ($all as $key => $value) {
if (!in_array($key, $keys)) {
unset($all[$key]);
}
}
$backup = $setting;
$setting = Base::setting('aibotSetting', Base::newTrim($all));
$tempMsg = [
'type' => 'content',
'content' => '设置成功'
];
//
if ($backup['openai_key'] != $setting['openai_key']) {
$botUser = User::botGetOrCreate('ai-openai');
if ($botUser && $dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid)) {
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', $tempMsg, $botUser->userid, true, false, true);
}
}
if ($backup['claude_token'] != $setting['claude_token']) {
$botUser = User::botGetOrCreate('ai-claude');
if ($botUser && $dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid)) {
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', $tempMsg, $botUser->userid, true, false, true);
}
}
if ($backup['wenxin_key'] != $setting['wenxin_key']) {
$botUser = User::botGetOrCreate('ai-wenxin');
if ($botUser && $dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid)) {
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', $tempMsg, $botUser->userid, true, false, true);
}
}
if ($backup['qianwen_key'] != $setting['qianwen_key']) {
$botUser = User::botGetOrCreate('ai-qianwen');
if ($botUser && $dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid)) {
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', $tempMsg, $botUser->userid, true, false, true);
}
}
if ($backup['gemini_key'] != $setting['gemini_key']) {
$botUser = User::botGetOrCreate('ai-gemini');
if ($botUser && $dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid)) {
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', $tempMsg, $botUser->userid, true, false, true);
}
}
if ($backup['zhipu_key'] != $setting['zhipu_key']) {
$botUser = User::botGetOrCreate('ai-zhipu');
if ($botUser && $dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid)) {
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', $tempMsg, $botUser->userid, true, false, true);
if (isset($setting[$key])) {
$setting[$key] = $value;
}
}
$setting = Base::setting('aibotSetting', Base::newTrim($setting));
}
if ($filter) {
$setting = array_filter($setting, function($value, $key) use ($filter) {
return str_starts_with($key, $filter);
}, ARRAY_FILTER_USE_BOTH);
}
//
$setting['openai_model'] = $setting['openai_model'] ?: 'gpt-3.5-turbo';
$setting['wenxin_model'] = $setting['wenxin_model'] ?: 'eb-instant';
$setting['qianwen_model'] = $setting['qianwen_model'] ?: 'qwen-v1';
$setting['gemini_model'] = $setting['gemini_model'] ?: 'gemini-1.0-pro';
$setting['zhipu_model'] = $setting['zhipu_model'] ?: 'glm-4';
if (env("SYSTEM_SETTING") == 'disabled') {
foreach ($keys as $item) {
if (strlen($setting[$item]) > 12) {
$setting[$item] = substr($setting[$item], 0, 4) . str_repeat('*', strlen($setting[$item]) - 8) . substr($setting[$item], -4);
foreach ($setting as $key => $item) {
if (empty($item)) {
continue;
}
if (str_ends_with($key, '_key') || str_ends_with($key, '_secret')) {
$setting[$key] = substr($item, 0, 4) . str_repeat('*', strlen($item) - 8) . substr($item, -4);
}
}
}
@@ -384,7 +338,66 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/setting/checkin 05. 获取签到设置、保存签到设置(限管理员)
* @api {get} api/system/setting/aibot_models 05. 获取AI模型
*
* @apiDescription 获取所有AI机器人模型设置
* @apiVersion 1.0.0
* @apiGroup system
* @apiName aibot_models
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function setting__aibot_models()
{
$setting = Base::setting('aibotSetting');
$setting = array_filter($setting, function($value, $key) {
return str_ends_with($key, '_models') || str_ends_with($key, '_model');
}, ARRAY_FILTER_USE_BOTH);
return Base::retSuccess('success', $setting ?: json_decode('{}'));
}
/**
* @api {get} api/system/setting/aibot_defmodels 06. 获取AI默认模型
*
* @apiDescription 获取AI机器人默认模型
* @apiVersion 1.0.0
* @apiGroup system
* @apiName setting__aibot_defmodels
*
* @apiParam {String} type AI类型
* @apiParam {String} [base_url] 基础URL仅 type=ollama 时有效)
* @apiParam {String} [key] Key仅 type=ollama 时有效)
* @apiParam {String} [agency] 使用代理(仅 type=ollama 时有效)
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function setting__aibot_defmodels()
{
$type = trim(Request::input('type'));
if ($type == 'ollama') {
$baseUrl = trim(Request::input('base_url'));
$key = trim(Request::input('key'));
$agency = trim(Request::input('agency'));
if (empty($baseUrl)) {
return Base::retError('请先填写 Base URL');
}
return Extranet::ollamaModels($baseUrl, $key, $agency);
}
$models = Setting::AIDefaultModels($type);
if (empty($models)) {
return Base::retError('未找到默认模型');
}
return Base::retSuccess('success', [
'models' => $models
]);
}
/**
* @api {get} api/system/setting/checkin 07. 获取签到设置、保存签到设置(限管理员)
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -490,7 +503,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/setting/apppush 06. 获取APP推送设置、保存APP推送设置限管理员
* @api {get} api/system/setting/apppush 08. 获取APP推送设置、保存APP推送设置限管理员
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -535,7 +548,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/setting/thirdaccess 07. 第三方帐号(限管理员)
* @api {get} api/system/setting/thirdaccess 09. 第三方帐号(限管理员)
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -605,7 +618,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/setting/file 08. 文件设置(限管理员)
* @api {get} api/system/setting/file 10. 文件设置(限管理员)
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -645,7 +658,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/demo 09. 获取演示帐号
* @api {get} api/system/demo 11. 获取演示帐号
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -669,7 +682,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/priority 10. 任务优先级
* @api {post} api/system/priority 12. 任务优先级
*
* @apiDescription 获取任务优先级、保存任务优先级
* @apiVersion 1.0.0
@@ -718,7 +731,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/column/template 11. 创建项目模板
* @api {post} api/system/column/template 13. 创建项目模板
*
* @apiDescription 获取创建项目模板、保存创建项目模板
* @apiVersion 1.0.0
@@ -765,7 +778,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/license 12. License
* @api {post} api/system/license 14. License
*
* @apiDescription 获取License信息、保存License限管理员
* @apiVersion 1.0.0
@@ -824,11 +837,17 @@ class SystemController extends AbstractController
$data['error'][] = '终端License已过期';
}
//
if ($type === 'error') {
$data = [
'error' => $data['error']
];
}
//
return Base::retSuccess('success', $data);
}
/**
* @api {get} api/system/get/info 13. 获取终端详细信息
* @api {get} api/system/get/info 15. 获取终端详细信息
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -857,7 +876,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/ip 14. 获取IP地址
* @api {get} api/system/get/ip 16. 获取IP地址
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -872,7 +891,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/cnip 15. 是否中国IP地址
* @api {get} api/system/get/cnip 17. 是否中国IP地址
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -889,7 +908,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/ipgcj02 16. 获取IP地址经纬度
* @api {get} api/system/get/ipgcj02 18. 获取IP地址经纬度
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -906,7 +925,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/ipinfo 17. 获取IP地址详细信息
* @api {get} api/system/get/ipinfo 19. 获取IP地址详细信息
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -923,7 +942,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/imgupload 18. 上传图片
* @api {post} api/system/imgupload 20. 上传图片
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -932,7 +951,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] 压缩方式(等比缩放)
@@ -969,7 +988,7 @@ class SystemController extends AbstractController
"path" => $path,
"fileName" => $fileName,
"scale" => $scale,
"quality" => 85
"quality" => true
]);
} else {
$data = Base::upload([
@@ -978,7 +997,7 @@ class SystemController extends AbstractController
"path" => $path,
"fileName" => $fileName,
"scale" => $scale,
"quality" => 100
"quality" => true
]);
}
if (Base::isError($data)) {
@@ -989,7 +1008,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/imgview 19. 浏览图片空间
* @api {get} api/system/get/imgview 21. 浏览图片空间
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1086,16 +1105,16 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/fileupload 20. 上传文件
* @api {post} api/system/fileupload 22. 上传文件
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup system
* @apiName fileupload
*
* @apiParam {String} [image64] 图片base64
* @apiParam {String} filename 文件名
* @apiParam {String} [files] 文件名
* @apiParam {File} files 文件名
* @apiParam {String} [image64] 图片base64与'files'二选一)
* @apiParam {String} [filename] 文件名
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -1114,7 +1133,7 @@ class SystemController extends AbstractController
"image64" => $image64,
"path" => $path,
"fileName" => $fileName,
"quality" => 85
"quality" => true
]);
} else {
$data = Base::upload([
@@ -1122,7 +1141,7 @@ class SystemController extends AbstractController
"type" => 'file',
"path" => $path,
"fileName" => $fileName,
"quality" => 100
"quality" => true
]);
}
//
@@ -1130,7 +1149,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/updatelog 21. 获取更新日志
* @api {get} api/system/get/updatelog 23. 获取更新日志
*
* @apiDescription 获取更新日志
* @apiVersion 1.0.0
@@ -1163,7 +1182,7 @@ class SystemController extends AbstractController
if ($logResults) {
$logVersion = $logResults[0]['title'];
$logContent = implode("\n", array_map(function($item) {
return "## [{$item['title']}]" . $item['content'];
return "## {$item['title']}" . $item['content'];
}, $logResults));
}
return Base::retSuccess('success', [
@@ -1173,7 +1192,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/email/check 22. 邮件发送测试(限管理员)
* @api {get} api/system/email/check 24. 邮件发送测试(限管理员)
*
* @apiDescription 测试配置邮箱是否能发送邮件
* @apiVersion 1.0.0
@@ -1219,7 +1238,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/checkin/export 23. 导出签到数据(限管理员)
* @api {get} api/system/checkin/export 25. 导出签到数据(限管理员)
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -1388,7 +1407,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/checkin/down 24. 下载导出的签到数据
* @api {get} api/system/checkin/down 26. 下载导出的签到数据
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -1414,7 +1433,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/version 25. 获取版本号
* @api {get} api/system/version 27. 获取版本号
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -1451,7 +1470,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/prefetch 26. 预加载的资源
* @api {get} api/system/prefetch 28. 预加载的资源
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -1491,7 +1510,7 @@ class SystemController extends AbstractController
}
// 添加office资源
$officePath = '';
$officeApi = 'http://' . env('APP_IPPR') . '.6/web-apps/apps/api/documents/api.js';
$officeApi = 'http://office/web-apps/apps/api/documents/api.js';
$content = @file_get_contents($officeApi);
if ($content) {
if (preg_match("/const\s+ver\s*=\s*'\/*([^']+)'/", $content, $matches)) {

View File

@@ -520,7 +520,8 @@ class UsersController extends AbstractController
} else {
$builder->where(function($query) use ($keys) {
$query->where("nickname", "like", "%{$keys['key']}%")
->orWhere("pinyin", "like", "%{$keys['key']}%");
->orWhere("pinyin", "like", "%{$keys['key']}%")
->orWhere("profession", "like", "%{$keys['key']}%");
});
}
}
@@ -610,7 +611,38 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/basic 11. 获取指定会员基础信息
* @api {get} api/users/search/ai 11. 获取AI机器人
*
* @apiDescription 搜索会员列表
* @apiVersion 1.0.0
* @apiGroup users
* @apiName search__ai
*
* @apiParam {String} type AI 类型比如openai
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function search__ai()
{
User::auth();
//
$type = trim(Request::input('type'));
$botName = "ai-{$type}";
if (!UserBot::systemBotName($botName)) {
return Base::retError('AI机器人不存在');
}
//
$botUser = User::botGetOrCreate($botName);
if (empty($botUser)) {
return Base::retError('AI机器人不存在');
}
return Base::retSuccess('success', $botUser);
}
/**
* @api {get} api/users/basic 12. 获取指定会员基础信息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -653,7 +685,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/lists 12. 会员列表(限管理员)
* @api {get} api/users/lists 13. 会员列表(限管理员)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -802,7 +834,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/operation 13. 操作会员(限管理员)
* @api {get} api/users/operation 14. 操作会员(限管理员)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1060,7 +1092,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/email/verification 14. 邮箱验证
* @api {get} api/users/email/verification 15. 邮箱验证
*
* @apiDescription 不需要token身份
* @apiVersion 1.0.0
@@ -1108,7 +1140,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/umeng/alias 15. 设置友盟别名
* @api {get} api/users/umeng/alias 16. 设置友盟别名
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1119,6 +1151,10 @@ class UsersController extends AbstractController
* - update: 更新(默认)
* - remove: 删除
* @apiParam {String} alias 别名
* @apiParam {String} [userAgent] 浏览器信息
* @apiParam {String} [deviceModel] 设备型号
* @apiParam {String} [isNotified] 是否有通知权限0不通知、1通知
* @apiParam {Number} [isDebug] 是否调试0不调试、1调试
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -1127,6 +1163,10 @@ class UsersController extends AbstractController
public function umeng__alias()
{
$data = Request::input();
// 判断是否调试
if (intval($data['isDebug'])) {
return Base::retError('调试模式下不允许使用');
}
// 表单验证
Base::validator($data, [
'alias.required' => '别名不能为空',
@@ -1150,11 +1190,15 @@ class UsersController extends AbstractController
'alias' => $data['alias'],
'platform' => Base::platform(),
];
$version = $data['appVersion'] ? ($data['appVersionName'] . " ({$data['appVersion']})") : '';
$isNotified = trim($data['isNotified']) === 'true' || $data['isNotified'] === true ? 1 : intval($data['isNotified']);
$row = UmengAlias::where($inArray);
if ($row->exists()) {
$row->update([
'ua' => $data['userAgent'],
'device' => $data['deviceModel'],
'version' => $version,
'is_notified' => $isNotified,
'updated_at' => Carbon::now()
]);
return Base::retSuccess('别名已存在');
@@ -1162,6 +1206,8 @@ class UsersController extends AbstractController
$row = UmengAlias::createInstance(array_merge($inArray, [
'ua' => $data['userAgent'],
'device' => $data['deviceModel'],
'version' => $version,
'is_notified' => $isNotified,
]));
if ($row->save()) {
return Base::retSuccess('添加成功');
@@ -1171,7 +1217,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/meeting/open 16. 【会议】创建会议、加入会议
* @api {get} api/users/meeting/open 17. 【会议】创建会议、加入会议
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1289,7 +1335,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/meeting/link 17. 【会议】获取分享链接
* @api {get} api/users/meeting/link 18. 【会议】获取分享链接
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1318,7 +1364,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/meeting/tourist 18. 【会议】游客信息
* @api {get} api/users/meeting/tourist 19. 【会议】游客信息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1341,7 +1387,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/meeting/invitation 19. 【会议】发送邀请
* @api {get} api/users/meeting/invitation 20. 【会议】发送邀请
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1388,7 +1434,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/email/send 20. 发送邮箱验证码
* @api {get} api/users/email/send 21. 发送邮箱验证码
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1428,7 +1474,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/email/edit 21. 修改邮箱
* @api {get} api/users/email/edit 22. 修改邮箱
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1473,7 +1519,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/delete/account 22. 删除帐号
* @api {get} api/users/delete/account 23. 删除帐号
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1535,7 +1581,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/department/list 23. 部门列表(限管理员)
* @api {get} api/users/department/list 24. 部门列表(限管理员)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1554,7 +1600,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/department/add 24. 新建、修改部门(限管理员)
* @api {get} api/users/department/add 25. 新建、修改部门(限管理员)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1612,19 +1658,22 @@ class UsersController extends AbstractController
if (empty($parentDepartment)) {
return Base::retError('上级部门不存在或已被删除');
}
if ($parentDepartment->parent_id > 0) {
return Base::retError('上级部门层级错误');
if (count($parentDepartment->parents()) > 2) {
return Base::retError('部门层级最多只能创建3级');
}
if (UserDepartment::whereParentId($parent_id)->count() > 20) {
if ($id > 0 && UserDepartment::whereParentId($id)->whereId($parent_id)->exists()) {
return Base::retError('不能选择自己的子部门作为上级部门');
}
if (UserDepartment::whereParentId($parent_id)->count() >= 20) {
return Base::retError('每个部门最多只能创建20个子部门');
}
if ($id > 0 && UserDepartment::whereParentId($id)->exists()) {
return Base::retError('含有子部门无法修改上级部门');
}
}
if (empty($owner_userid) || !User::whereUserid($owner_userid)->exists()) {
return Base::retError('请选择正确的部门负责人');
}
if (UserDepartment::whereOwnerUserid($owner_userid)->count() >= 10) {
return Base::retError('每个用户最多只能负责10个部门');
}
//
$userDepartment->saveDepartment([
'name' => $name,
@@ -1633,11 +1682,11 @@ class UsersController extends AbstractController
], $dialog_useid);
Cache::forever("UserDepartment::rand", Base::generatePassword());
//
return Base::retSuccess($parent_id > 0 ? '保存成功' : '新建成功');
return Base::retSuccess($id > 0 ? '保存成功' : '新建成功');
}
/**
* @api {get} api/users/department/del 25. 删除部门(限管理员)
* @api {get} api/users/department/del 26. 删除部门(限管理员)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1660,6 +1709,9 @@ class UsersController extends AbstractController
if (empty($userDepartment)) {
return Base::retError('部门不存在或已被删除');
}
if (UserDepartment::whereParentId($id)->exists()) {
return Base::retError('含有子部门无法删除');
}
$userDepartment->deleteDepartment();
Cache::forever("UserDepartment::rand", Base::generatePassword());
//
@@ -1667,7 +1719,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/checkin/get 26. 获取签到设置
* @api {get} api/users/checkin/get 27. 获取签到设置
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1694,7 +1746,7 @@ class UsersController extends AbstractController
}
/**
* @api {post} api/users/checkin/save 27. 保存签到设置
* @api {post} api/users/checkin/save 28. 保存签到设置
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1769,7 +1821,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/checkin/list 28. 获取签到数据
* @api {get} api/users/checkin/list 29. 获取签到数据
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1816,7 +1868,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/socket/status 29. 获取socket状态
* @api {get} api/users/socket/status 30. 获取socket状态
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1839,7 +1891,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/key/client 30. 客户端KEY
* @api {get} api/users/key/client 31. 客户端KEY
*
* @apiDescription 获取客户端KEY用于加密数据发送给服务端
* @apiVersion 1.0.0
@@ -1881,7 +1933,51 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/bot/info 31. 机器人信息
* @api {get} api/users/bot/list 32. 机器人列表
*
* @apiDescription 需要token身份获取我的机器人列表
* @apiVersion 1.0.0
* @apiGroup users
* @apiName bot__list
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function bot__list()
{
// 获取当前认证用户
$user = User::auth();
// 使用连表查询一次性获取所有机器人数据
$bots = User::join('user_bots', 'user_bots.bot_id', '=', 'users.userid')
->where('user_bots.userid', $user->userid)
->select([
'users.userid',
'users.nickname',
'users.userimg',
'user_bots.clear_day',
'user_bots.webhook_url'
])
->orderByDesc('id')
->get()
->toArray();
foreach ($bots as &$bot) {
$bot['id'] = $bot['userid'];
$bot['name'] = $bot['nickname'];
$bot['avatar'] = $bot['userimg'];
$bot['system_name'] = UserBot::systemBotName($bot['name']);
unset($bot['userid'], $bot['nickname'], $bot['userimg']);
}
// 返回成功响应将机器人列表包装在list字段中
return Base::retSuccess('success', [
'list' => $bots
]);
}
/**
* @api {get} api/users/bot/info 33. 机器人信息
*
* @apiDescription 需要token身份获取我的机器人信息
* @apiVersion 1.0.0
@@ -1932,14 +2028,14 @@ class UsersController extends AbstractController
}
/**
* @api {post} api/users/bot/edit 32. 编辑机器人
* @api {post} api/users/bot/edit 34. 添加、编辑机器人
*
* @apiDescription 需要token身份编辑 我的机器人 或 管理员修改系统机器人 信息
* @apiVersion 1.0.0
* @apiGroup users
* @apiName bot__edit
*
* @apiParam {Number} id 机器人ID
* @apiParam {Number} [id] 机器人ID(编辑时必填,留空为添加)
* @apiParam {String} [name] 机器人名称
* @apiParam {String} [avatar] 机器人头像
* @apiParam {Number} [clear_day] 清理天数(仅 我的机器人)
@@ -1954,10 +2050,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)) {
@@ -2014,11 +2119,61 @@ class UsersController extends AbstractController
$data['clear_day'] = $userBot->clear_day;
$data['webhook_url'] = $userBot->webhook_url;
}
return Base::retSuccess('修改成功', $data);
return Base::retSuccess($botId ? '修改成功' : '添加成功', $data);
}
/**
* @api {get} api/users/share/list 33. 获取分享列表
* @api {get} api/users/bot/delete 35. 删除机器人
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup users
* @apiName bot__delete
*
* @apiParam {Number} id 机器人ID
* @apiParam {String} remark 删除备注
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function bot__delete()
{
$user = User::auth();
//
$botId = intval(Request::input('id'));
$remark = trim(Request::input('remark'));
//
if (empty($remark)) {
return Base::retError('请输入删除备注');
}
if (mb_strlen($remark) > 255) {
return Base::retError('删除备注长度限制255个字');
}
//
$botUser = User::whereUserid($botId)->whereBot(1)->first();
if (empty($botUser)) {
return Base::retError('机器人不存在');
}
$userBot = UserBot::whereBotId($botUser->userid)->whereUserid($user->userid)->first();
if (empty($userBot)) {
if (UserBot::systemBotName($botUser->email)) {
// 系统机器人(仅限管理员)
return Base::retError('系统机器人不能删除');
} else {
// 其他用户的机器人(仅限主人)
return Base::retError('不是你的机器人');
}
}
//
if (!$botUser->deleteUser($remark)) {
return Base::retError('删除失败');
}
return Base::retSuccess('删除成功');
}
/**
* @api {get} api/users/share/list 36. 获取分享列表
*
* @apiVersion 1.0.0
* @apiGroup users
@@ -2103,7 +2258,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/annual/report 34. 年度报告
* @api {get} api/users/annual/report 37. 年度报告
*
* @apiVersion 1.0.0
* @apiGroup users

View File

@@ -23,6 +23,7 @@ use App\Tasks\AutoArchivedTask;
use App\Tasks\DeleteBotMsgTask;
use App\Tasks\CheckinRemindTask;
use App\Tasks\CloseMeetingRoomTask;
use App\Tasks\ElasticSearchSyncTask;
use App\Tasks\UnclaimedTaskRemindTask;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Laravolt\Avatar\Avatar;
@@ -258,6 +259,8 @@ class IndexController extends InvokeController
Task::deliver(new UnclaimedTaskRemindTask());
// 关闭会议室
Task::deliver(new CloseMeetingRoomTask());
// ElasticSearch 同步
Task::deliver(new ElasticSearchSyncTask());
return "success";
}
@@ -321,8 +324,7 @@ class IndexController extends InvokeController
"file" => Request::file('file'),
"type" => 'publish',
"path" => $draftPath,
"fileName" => true,
"quality" => 100
"saveName" => true,
]);
}
@@ -491,7 +493,7 @@ class IndexController extends InvokeController
if (in_array($ext, File::localExt)) {
$url = Base::fillUrl($path);
} else {
$url = 'http://' . env('APP_IPPR') . '.3/' . $path;
$url = 'http://nginx/' . $path;
}
$url = Base::urlAddparameter($url, [
'fullfilename' => Base::rightDelete($name, '.' . $ext) . '_' . filemtime($file) . '.' . $ext

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

@@ -146,7 +146,7 @@ class LdapUser extends Model
$path = "uploads/user/ldap/";
$file = "{$path}{$user->userid}.jpeg";
Base::makeDir(public_path($path));
if (Base::saveContentImage(public_path($file), $userimg, 90)) {
if (Base::saveContentImage(public_path($file), $userimg)) {
$user->userimg = $file;
}
}

View File

@@ -210,8 +210,8 @@ class AbstractModel extends Model
/**
* 数据库更新或插入
* @param $where
* @param array $update 存在时更新的内容
* @param array $insert 不存在时插入的内容,如果没有则插入更新内容
* @param array|\Closure $update 存在时更新的内容
* @param array|\Closure $insert 不存在时插入的内容,如果没有则插入更新内容
* @param bool $isInsert 是否是插入数据
* @return AbstractModel|\Illuminate\Database\Eloquent\Builder|Model|object|static|null
*/
@@ -220,6 +220,12 @@ class AbstractModel extends Model
$row = static::where($where)->first();
if (empty($row)) {
$row = new static;
if ($update instanceof \Closure) {
$update = $update();
}
if ($insert instanceof \Closure) {
$insert = $insert();
}
$array = array_merge($where, $insert ?: $update);
if (isset($array[$row->primaryKey])) {
unset($array[$row->primaryKey]);
@@ -227,6 +233,9 @@ class AbstractModel extends Model
$row->updateInstance($array);
$isInsert = true;
} elseif ($update) {
if ($update instanceof \Closure) {
$update = $update();
}
$row->updateInstance($update);
$isInsert = false;
}

View File

@@ -0,0 +1,99 @@
<?php
namespace App\Models;
use Cache;
use Carbon\Carbon;
use DB;
/**
* App\Models\ApproveProcInstHistory
*
* @property int $id
* @property int $proc_def_id 流程定义ID
* @property string|null $proc_def_name 流程定义名
* @property string|null $title 标题
* @property int|null $department_id 用户部门ID
* @property string|null $department 用户部门
* @property string|null $company 用户公司
* @property string|null $node_id 当前节点
* @property string|null $candidate 审批人
* @property int|null $task_id 当前任务
* @property string|null $start_time 开始时间
* @property string|null $end_time 结束时间
* @property int|null $duration 持续时间
* @property string|null $start_user_id 开始用户ID
* @property string|null $start_user_name 开始用户名
* @property int|null $is_finished 是否完成
* @property string|null $var
* @property int $state 当前状态: 0待审批1审批中2通过3拒绝4撤回
* @property string|null $latest_comment
* @property string|null $global_comment
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereCandidate($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereCompany($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereDepartment($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereDepartmentId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereDuration($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereEndTime($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereGlobalComment($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereIsFinished($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereLatestComment($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereNodeId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereProcDefId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereProcDefName($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereStartTime($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereStartUserId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereStartUserName($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereState($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereTaskId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereTitle($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereVar($value)
* @mixin \Eloquent
*/
class ApproveProcInstHistory extends AbstractModel
{
protected $table = 'approve_proc_inst_history';
/**
* 获取用户审批状态(请假、外出)
* @param $userid
* @return mixed|null
*/
public static function getUserApprovalStatus($userid)
{
if (empty($userid)) {
return null;
}
return Cache::remember('user_is_leave_' . $userid, Carbon::now()->addMinute(), function () use ($userid) {
return self::where([
['start_user_id', '=', $userid],
[DB::raw("JSON_UNQUOTE(JSON_EXTRACT(var, '$.startTime'))"), '<=', Carbon::now()->toDateTimeString()],
[DB::raw("JSON_UNQUOTE(JSON_EXTRACT(var, '$.endTime'))"), '>=', Carbon::now()->toDateTimeString()],
['state', '=', 2]
])->where(function ($query) {
$query->where('proc_def_name', 'like', '%请假%')
->orWhere('proc_def_name', 'like', '%外出%');
})->orderByDesc('id')->value('proc_def_name');
});
}
/**
* 判断用户是否请假(包含:请假、外出)
* @param $userid
* @return bool
*/
public static function userIsLeave($userid)
{
return (bool)self::getUserApprovalStatus($userid);
}
}

View File

@@ -79,9 +79,28 @@ class File extends AbstractModel
* office文件
*/
const officeExt = [
'doc', 'docx',
'xls', 'xlsx',
'ppt', 'pptx',
// 文本文件
'doc', 'docx', // Microsoft Word 文档
'dot', 'dotx', // Word 模板
'odt', // OpenDocument 文本格式
'ott', // OpenDocument 文本模板
'rtf', // 富文本格式
// 电子表格
'xls', 'xlsx', // Microsoft Excel 电子表格
'xlsm', // Excel 含宏的工作簿
'xlt', 'xltx', // Excel 模板
'ods', // OpenDocument 电子表格格式
'ots', // OpenDocument 电子表格模板
'csv', // 逗号分隔值
'tsv', // 制表符分隔值
// 演示文稿
'ppt', 'pptx', // Microsoft PowerPoint 演示文稿
'pps', 'ppsx', // PowerPoint 幻灯片放映
'pot', 'potx', // PowerPoint 模板
'odp', // OpenDocument 演示文稿格式
'otp', // OpenDocument 演示文稿模板
];
/**
@@ -111,6 +130,8 @@ class File extends AbstractModel
* 获取文件列表
* @param user $user
* @param int $pid
* @param string $type
* @param bool $isGetparent
* @return array
*/
public function getFileList($user, int $pid, $type = "all", $isGetparent = true)
@@ -118,7 +139,7 @@ class File extends AbstractModel
$permission = 1000;
$userids = $user->isTemp() ? [$user->userid] : [0, $user->userid];
$builder = File::wherePid($pid)
->when($type=='dir',function($q){
->when($type == 'dir', function ($q) {
$q->whereType('folder');
});
if ($pid > 0) {
@@ -134,7 +155,7 @@ class File extends AbstractModel
//
if ($pid > 0) {
// 遍历获取父级
if($isGetparent){
if ($isGetparent) {
while ($pid > 0) {
$file = File::whereId($pid)->first();
if (empty($file)) {
@@ -172,8 +193,8 @@ class File extends AbstractModel
->whereIn('file_users.userid', $userids)
->groupBy('files.id')
->take(100)
->when($type=='dir',function($q){
$q->where('files.type','folder');
->when($type == 'dir', function ($q) {
$q->where('files.type', 'folder');
})
->get();
if ($list->isNotEmpty()) {
@@ -251,7 +272,7 @@ class File extends AbstractModel
"type" => 'more',
"autoThumb" => false,
"path" => $path,
"quality" => 100
"quality" => true
]);
if (Base::isError($data)) {
throw new ApiException($data['msg']);
@@ -262,9 +283,9 @@ class File extends AbstractModel
'text', 'md', 'markdown' => 'document',
'drawio' => 'drawio',
'mind' => 'mind',
'doc', 'docx' => "word",
'xls', 'xlsx' => "excel",
'ppt', 'pptx' => "ppt",
'doc', 'docx', 'dot', 'dotx', 'odt', 'ott', 'rtf' => "word",
'xls', 'xlsx', 'xlsm', 'xlt', 'xltx', 'ods', 'ots', 'csv', 'tsv' => "excel",
'ppt', 'pptx', 'pps', 'ppsx', 'pot', 'potx', 'odp', 'otp' => "ppt",
'wps' => "wps",
'jpg', 'jpeg', 'webp', 'png', 'gif', 'bmp', 'ico', 'raw', 'svg' => "picture",
'rar', 'zip', 'jar', '7-zip', 'tar', 'gzip', '7z', 'gz', 'apk', 'dmg' => "archive",
@@ -351,7 +372,8 @@ class File extends AbstractModel
*/
public function getPermission(array $userids)
{
if (in_array($this->userid, $userids) || in_array($this->created_id, $userids)) {
$validUserIds = array_filter($userids);
if (in_array($this->userid, $validUserIds) || in_array($this->created_id, $validUserIds)) {
// ① 自己的文件夹 或 自己创建的文件夹
return 1000;
}
@@ -703,9 +725,9 @@ class File extends AbstractModel
* @param int $permission
* @return File
*/
public static function permissionFind(int $id, $user, int $limit = 0, int &$permission = -1)
public static function permissionFind($id, $user, int $limit = 0, int &$permission = -1)
{
$file = File::find($id);
$file = File::find(intval($id));
if (empty($file)) {
throw new ApiException('文件不存在或已被删除');
}

View File

@@ -2,10 +2,10 @@
namespace App\Models;
use App\Module\Base;
use App\Module\Timer;
use Illuminate\Database\Eloquent\SoftDeletes;
use Symfony\Component\HttpFoundation\StreamedResponse;
/**
* App\Models\FileContent
@@ -104,10 +104,10 @@ class FileContent extends AbstractModel
/**
* 获取格式内容(或下载)
* @param File $file
* @param $file
* @param $content
* @param $download
* @return array|\Symfony\Component\HttpFoundation\BinaryFileResponse
* @return array|StreamedResponse
*/
public static function formatContent($file, $content, $download = false)
{
@@ -119,7 +119,7 @@ class FileContent extends AbstractModel
} else {
$filePath = public_path($content['url']);
}
return Base::BinaryFileResponse($filePath, $name);
return Base::DownloadFileResponse($filePath, $name);
}
if (empty($content)) {
$content = match ($file->type) {
@@ -148,7 +148,7 @@ class FileContent extends AbstractModel
if ($download) {
$filePath = public_path($path);
if (isset($filePath)) {
return Base::BinaryFileResponse($filePath, $name);
return Base::DownloadFileResponse($filePath, $name);
} else {
abort(403, "This file not support download.");
}
@@ -156,4 +156,28 @@ class FileContent extends AbstractModel
}
return Base::retSuccess('success', [ 'content' => $content ]);
}
/**
* 获取文件内容
* @param $id
* @return self|null
*/
public static function idOrCodeToContent($id)
{
$builder = null;
if (Base::isNumber($id)) {
$builder = FileContent::whereFid($id);
} elseif ($id) {
$fileLink = FileLink::whereCode($id)->first();
if ($fileLink) {
$builder = FileContent::whereFid($fileLink->file_id);
}
}
/** @var self $fileContent */
$fileContent = $builder?->orderByDesc('id')->first();
if ($fileContent) {
$fileContent->content = Base::json2array($fileContent->content ?: []);
}
return $fileContent;
}
}

View File

@@ -20,6 +20,8 @@ use Request;
* @property string|null $desc 描述、备注
* @property int|null $userid 创建人
* @property int|null $personal 是否个人项目
* @property string|null $archive_method 自动归档方式
* @property int|null $archive_days 自动归档天数
* @property string|null $user_simple 成员总数|1,2,3
* @property int|null $dialog_id 聊天会话ID
* @property \Illuminate\Support\Carbon|null $archived_at 归档时间
@@ -46,6 +48,8 @@ use Request;
* @method static \Illuminate\Database\Eloquent\Builder|Project query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|Project whereArchiveDays($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereArchiveMethod($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereArchivedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereArchivedUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereCreatedAt($value)
@@ -215,7 +219,12 @@ class Project extends AbstractModel
'userid' => $userid,
], [
'important' => 1
]);
], function () use ($userid) {
return [
'important' => 1,
'bot' => User::isBot($userid) ? 1 : 0,
];
});
}
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->whereImportant(1)->remove();
});
@@ -411,6 +420,7 @@ class Project extends AbstractModel
$hasStart = false;
$hasEnd = false;
$upTaskList = [];
$projectUserids = $this->relationUserids();
foreach ($flows as $item) {
$id = intval($item['id']);
$turns = Base::arrayRetainInt($item['turns'] ?: [], true);
@@ -427,6 +437,12 @@ class Project extends AbstractModel
if ($userlimit && empty($userids)) {
throw new ApiException("状态[{$item['name']}]设置错误,设置限制负责人时必须填写状态负责人");
}
foreach ($userids as $userid) {
if (!in_array($userid, $projectUserids)) {
$nickname = User::userid2nickname($userid);
throw new ApiException("状态[{$item['name']}]设置错误,状态负责人[{$nickname}]不在项目成员内");
}
}
$flow = ProjectFlowItem::updateInsert([
'id' => $id,
'project_id' => $this->id,

65
app/Models/ProjectTag.php Normal file
View File

@@ -0,0 +1,65 @@
<?php
namespace App\Models;
/**
* App\Models\ProjectTag
*
* @property int $id
* @property int $project_id 项目ID
* @property string $name 标签名称
* @property string|null $desc 标签描述
* @property string|null $color 颜色
* @property int $userid 创建人
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Project $project
* @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|ProjectTag newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag whereColor($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag whereDesc($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag whereProjectId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag whereUserid($value)
* @mixin \Eloquent
*/
class ProjectTag extends AbstractModel
{
protected $hidden = [
'created_at',
'updated_at',
];
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'project_id',
'name',
'desc',
'color',
'userid'
];
/**
* 关联项目
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function project()
{
return $this->belongsTo(Project::class);
}
}

View File

@@ -12,6 +12,7 @@ use App\Tasks\PushTask;
use App\Exceptions\ApiException;
use App\Observers\ProjectTaskObserver;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use League\HTMLToMarkdown\HtmlConverter;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
@@ -617,7 +618,12 @@ class ProjectTask extends AbstractModel
$data['complete_at'] = false;
}
}
if ($newFlowItem->userids) {
$flowUserids = $newFlowItem->userids;
if ($flowUserids) {
// 确认负责人在任务中
$flowUserids = ProjectUser::whereProjectId($this->project_id)->whereIn('userid', $flowUserids)->pluck('userid')->toArray();
}
if ($flowUserids) {
// 判断自动添加负责人
$flowData['owner'] = $data['owner'] = $this->taskUser->where('owner', 1)->pluck('userid')->toArray();
if (in_array($newFlowItem->usertype, ["replace", "merge"])) {
@@ -626,14 +632,14 @@ class ProjectTask extends AbstractModel
$flowData['assist'] = $data['assist'] = $this->taskUser->where('owner', 0)->pluck('userid')->toArray();
$data['assist'] = array_merge($data['assist'], $data['owner']);
}
$data['owner'] = $newFlowItem->userids;
$data['owner'] = $flowUserids;
// 判断剔除模式:保留操作状态的人员
if ($newFlowItem->usertype == "merge") {
$data['owner'][] = User::userid();
}
} else {
// 添加模式
$data['owner'] = array_merge($data['owner'], $newFlowItem->userids);
$data['owner'] = array_merge($data['owner'], $flowUserids);
}
$data['owner'] = array_values(array_unique($data['owner']));
if (isset($data['assist'])) {
@@ -770,6 +776,7 @@ class ProjectTask extends AbstractModel
if (Arr::exists($data, 'times')) {
$oldAt = [Carbon::parse($this->start_at), Carbon::parse($this->end_at)];
$oldStringAt = $this->start_at ? ($oldAt[0]->toDateTimeString() . '~' . $oldAt[1]->toDateTimeString()) : '';
$isOverdue = $this->overdue;
$clearSubTaskTime = false;
$this->start_at = null;
$this->end_at = null;
@@ -835,7 +842,19 @@ class ProjectTask extends AbstractModel
}
});
}
$newStringAt = $this->start_at && !$clearSubTaskTime ? ($this->start_at->toDateTimeString() . '~' . $this->end_at->toDateTimeString()) : '';
$existAt = $this->start_at && !$clearSubTaskTime;
$newStringAt = $existAt ? ($this->start_at->toDateTimeString() . '~' . $this->end_at->toDateTimeString()) : '';
if ($isOverdue) {
$this->addLog("{任务}超期未完成", [
'cache' => [
'task_at' => $oldStringAt,
'change_at' => $newStringAt,
'over_sec' => ($existAt ? $this->end_at : Carbon::now())->diffInSeconds($oldAt[1]),
'owners' => $this->taskUser->where('owner', 1)->pluck('userid')->toArray(),
'assists' => $this->taskUser->where('owner', 0)->pluck('userid')->toArray(),
]
]);
}
$newDesc = $desc ? "(备注:{$desc}" : "";
$this->addLog("修改{任务}时间" . $newDesc, [
'change' => [$oldStringAt, $newStringAt]
@@ -944,6 +963,60 @@ class ProjectTask extends AbstractModel
$this->addLog("修改{任务}详细描述", $logRecord);
$updateMarking['is_update_content'] = true;
}
// 标签
if (Arr::exists($data, 'task_tag')) {
$oldTags = collect($this->taskTag);
$newTags = collect($data['task_tag']);
// 找出需要删除的标签(在旧数据中存在,但在新数据中不存在)
$deletedTags = $oldTags->filter(function ($oldTag) use ($newTags) {
return !$newTags->contains('name', $oldTag['name']);
});
if ($deletedTags->isNotEmpty()) {
$this->addLog("删除{任务}标签", [
'tags' => $deletedTags->values()->all()
]);
ProjectTaskTag::whereProjectId($this->project_id)
->whereTaskId($this->id)
->whereIn('name', $deletedTags->pluck('name'))
->delete();
}
// 找出需要新增的标签(在新数据中存在,但在旧数据中不存在)
$addedTags = $newTags->filter(function ($newTag) use ($oldTags) {
return !$oldTags->contains('name', $newTag['name']);
});
if ($addedTags->isNotEmpty()) {
$this->addLog("新增{任务}标签", [
'tags' => $addedTags->values()->all()
]);
$addedTags->each(function ($tag) {
ProjectTaskTag::createInstance([
'project_id' => $this->project_id,
'task_id' => $this->id,
'name' => $tag['name'],
'color' => $tag['color'],
])->save();
});
}
// 找出需要更新的标签(标签名相同,但其他属性可能变化)
$updatedTags = $newTags->filter(function ($newTag) use ($oldTags) {
$oldTag = $oldTags->firstWhere('name', $newTag['name']);
return $oldTag && ($oldTag['color'] !== $newTag['color']);
});
if ($updatedTags->isNotEmpty()) {
$this->addLog("更新{任务}标签", [
'tags' => $updatedTags->values()->all()
]);
$updatedTags->each(function ($tag) {
ProjectTaskTag::whereProjectId($this->project_id)
->whereTaskId($this->id)
->whereName($tag['name'])
->update(['color' => $tag['color']]);
});
}
}
// 优先级
$p = false;
$oldPName = $this->p_name;
@@ -1126,7 +1199,12 @@ class ProjectTask extends AbstractModel
'userid' => $userid,
], [
'important' => 1
]);
], function () use ($userid) {
return [
'important' => 1,
'bot' => User::isBot($userid) ? 1 : 0,
];
});
}
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->whereImportant(1)->remove();
});
@@ -1289,6 +1367,16 @@ class ProjectTask extends AbstractModel
if (!$this->hasOwner()) {
throw new ApiException('请先领取任务');
}
if ($this->overdue) {
$this->addLog("{任务}超期未完成", [
'cache' => [
'task_at' => $this->start_at . '~' . $this->end_at,
'over_sec' => Carbon::now()->diffInSeconds($this->end_at),
'owners' => $this->taskUser->where('owner', 1)->pluck('userid')->toArray(),
'assists' => $this->taskUser->where('owner', 0)->pluck('userid')->toArray(),
]
]);
}
if (empty($complete_name)) {
$complete_name = '已完成';
}
@@ -1779,6 +1867,11 @@ class ProjectTask extends AbstractModel
$taskUser->save();
}
}
// 子任务
ProjectTask::whereParentId($this->id)->change([
'project_id' => $projectId,
'column_id' => $columnId,
]);
//
if ($flowItemId) {
$flowItem = projectFlowItem::whereProjectId($projectId)->whereId($flowItemId)->first();
@@ -1806,6 +1899,67 @@ class ProjectTask extends AbstractModel
return true;
}
/**
* 生成AI上下文
* @return array
*/
public function AIContext()
{
$contexts = [];
if ($this->archived_at) {
$contexts[] = "任务状态:已归档";
$contexts[] = "归档时间:" . $this->archived_at;
} elseif ($this->complete_at) {
$contexts[] = "任务状态:已完成";
$contexts[] = "完成时间:" . $this->complete_at;
} elseif ($this->end_at && Carbon::parse($this->end_at)->lt(Carbon::now())) {
$contexts[] = "任务状态:已过期";
$contexts[] = "任务截止时间:" . $this->end_at;
} else {
$contexts[] = "任务状态:进行中";
if ($this->start_at) {
$contexts[] = "任务开始时间:" . $this->start_at;
}
if ($this->end_at) {
$contexts[] = "任务截止时间:" . $this->end_at;
}
}
$contexts[] = "当前系统时间:" . Carbon::now()->toDateTimeString();
if ($this->content) {
$taskDesc = $this->content?->getContentInfo();
if ($taskDesc) {
$converter = new HtmlConverter(['strip_tags' => true]);
$descContent = Base::cutStr($converter->convert($taskDesc['content']), 2000);
$contexts[] = <<<EOF
任务描述:
```md
{$descContent}
```
EOF;
}
}
$subTask = ProjectTask::select(['id', 'name', 'complete_at', 'end_at'])->whereParentId($this->id)->get();
if ($subTask->isNotEmpty()) {
$subTaskContent = $subTask->map(function($item) {
if ($item->complete_at) {
$status = " (已完成)";
} elseif ($item->end_at && Carbon::parse($item->end_at)->lt(Carbon::now())) {
$status = " (已过期)";
} else {
$status = " (进行中)";
}
return " - {$item->name} {$status}";
})->join("\n");
if ($subTaskContent) {
$contexts[] = <<<EOF
子任务列表:
{$subTaskContent}
EOF;
}
}
return $contexts;
}
/**
* 获取任务
* @param $task_id

View File

@@ -79,7 +79,7 @@ class ProjectTaskContent extends AbstractModel
$tmpPath = $path . 'attached/';
Base::makeDir(public_path($tmpPath));
$tmpPath .= md5($text) . "." . $matchs[1][$key];
if (Base::saveContentImage(public_path($tmpPath), base64_decode($text), 90)) {
if (Base::saveContentImage(public_path($tmpPath), base64_decode($text))) {
$paramet = getimagesize(public_path($tmpPath));
$content = str_replace($matchs[0][$key], '<img src="{{RemoteURL}}' . $tmpPath . '" original-width="' . $paramet[0] . '" original-height="' . $paramet[1] . '"', $content);
}

View File

@@ -0,0 +1,77 @@
<?php
namespace App\Models;
/**
* App\Models\ProjectTaskTemplate
*
* @property int $id
* @property int $project_id 项目ID
* @property string $name 模板名称
* @property string|null $title 任务标题
* @property string|null $content 任务内容
* @property int $sort 排序
* @property int $is_default 是否默认模板
* @property int $userid 创建人
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Project $project
* @property-read \App\Models\User $user
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereContent($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereIsDefault($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereProjectId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereSort($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereTitle($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereUserid($value)
* @mixin \Eloquent
*/
class ProjectTaskTemplate extends AbstractModel
{
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'project_id',
'name',
'title',
'content',
'sort',
'is_default',
'userid'
];
/**
* 关联项目
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function project()
{
return $this->belongsTo(Project::class);
}
/**
* 关联创建者
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class, 'userid');
}
}

View File

@@ -2,6 +2,8 @@
namespace App\Models;
use App\Module\Base;
/**
* App\Models\ProjectUser
*
@@ -50,7 +52,9 @@ class ProjectUser extends AbstractModel
*/
public static function transfer($originalUserid, $newUserid)
{
self::whereUserid($originalUserid)->chunkById(100, function ($list) use ($originalUserid, $newUserid) {
$projectIds = [];
// 移交项目身份
self::whereUserid($originalUserid)->chunkById(100, function ($list) use ($originalUserid, $newUserid, &$projectIds) {
/** @var self $item */
foreach ($list as $item) {
$row = self::whereProjectId($item->project_id)->whereUserid($newUserid)->first();
@@ -72,9 +76,23 @@ class ProjectUser extends AbstractModel
}
$item->project->addLog("移交项目身份", ['userid' => [$originalUserid, ' => ', $newUserid]]);
$item->project->syncDialogUser();
$projectIds[] = $item->project_id;
}
}
});
// 移交工作流状态负责人
if ($projectIds) {
ProjectFlowItem::whereIn('project_id', $projectIds)->chunkById(100, function ($list) use ($originalUserid, $newUserid) {
/** @var ProjectFlowItem $item */
foreach ($list as $item) {
if (in_array($originalUserid, $item->userids)) {
$userids = array_values(array_diff($item->userids, [$originalUserid]));
$item->userids = Base::array2json(array_merge($userids, [$newUserid]));
$item->save();
}
}
});
}
}
/**

View File

@@ -3,6 +3,7 @@
namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use Carbon\Carbon;
use Carbon\Traits\Creator;
use Illuminate\Database\Eloquent\Builder;
@@ -95,6 +96,24 @@ class Report extends AbstractModel
return $this->appendattrs['receives'];
}
/**
* 获取汇报内容
* @param $id
* @return self|null
*/
public static function idOrCodeToContent($id)
{
if (Base::isNumber($id)) {
return self::find($id);
} elseif ($id) {
$reportLink = ReportLink::whereCode($id)->first();
if ($reportLink) {
return self::find($reportLink->rid);
}
}
return null;
}
/**
* 获取单条记录
* @param $id
@@ -139,12 +158,12 @@ class Report extends AbstractModel
// 如果设置了周期偏移量
empty( $offset ) || $now_dt->subWeeks( abs( $offset ) );
$now_dt->startOfWeek(); // 设置为当周第一天
return $now_dt->year . $now_dt->weekOfYear;
return now()->year . $now_dt->weekOfYear;
},
Report::DAILY => function() use ($now_dt, $offset) {
// 如果设置了周期偏移量
empty( $offset ) || $now_dt->subDays( abs( $offset ) );
return $now_dt->format("Ymd");
return now()->format("Ymd");
},
default => "",
};

86
app/Models/ReportLink.php Normal file
View File

@@ -0,0 +1,86 @@
<?php
namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
/**
* App\Models\ReportLink
*
* @property int $id
* @property int|null $rid 报告ID
* @property int|null $num 累计访问
* @property string|null $code 链接码
* @property int|null $userid 会员ID
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Report|null $report
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereCode($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereNum($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereRid($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereUserid($value)
* @mixin \Eloquent
*/
class ReportLink extends AbstractModel
{
/**
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function report(): \Illuminate\Database\Eloquent\Relations\HasOne
{
return $this->hasOne(Report::class, 'id', 'report_id');
}
/**
* 生成链接
* @param $rid
* @param $userid
* @param $refresh
* @return array
*/
public static function generateLink($rid, $userid, $refresh = false)
{
$report = Report::find($rid);
if (empty($report)) {
throw new ApiException('报告不存在或已被删除');
}
if ($report->userid != $userid) {
if (!ReportReceive::whereRid($rid)->whereUserid($userid)->exists()) {
throw new ApiException('您没有权限查看该报告');
}
}
$reportLink = ReportLink::whereRid($rid)->whereUserid($userid)->first();
if (empty($reportLink)) {
$reportLink = ReportLink::createInstance([
'rid' => $rid,
'userid' => $userid,
'code' => base64_encode("{$rid},{$userid}," . Base::generatePassword()),
]);
$reportLink->save();
} else {
if ($refresh == 'yes') {
$reportLink->code = base64_encode("{$rid},{$userid}," . Base::generatePassword());
$reportLink->save();
}
}
return [
'id' => $rid,
'url' => Base::fillUrl('single/report/detail/' . $reportLink->code),
'code' => $reportLink->code,
'num' => $reportLink->num
];
}
}

View File

@@ -2,8 +2,11 @@
namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use App\Module\Doo;
use App\Module\Timer;
use Carbon\Carbon;
/**
* App\Models\Setting
@@ -11,7 +14,7 @@ use App\Module\Timer;
* @property int $id
* @property string|null $name
* @property string|null $desc 参数描述、备注
* @property string|null $setting
* @property array $setting
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
@@ -36,7 +39,7 @@ class Setting extends AbstractModel
/**
* 格式化设置参数
* @param $value
* @return array|mixed
* @return array
*/
public function getSettingAttribute($value)
{
@@ -47,18 +50,186 @@ class Setting extends AbstractModel
switch ($this->name) {
case 'system':
$value['system_alias'] = $value['system_alias'] ?: env('APP_NAME');
$value['image_compress'] = $value['image_compress'] ?: 'open';
$value['image_quality'] = min(100, max(0, intval($value['image_quality']) ?: 90));
$value['image_save_local'] = $value['image_save_local'] ?: 'open';
if (!is_array($value['task_default_time']) || count($value['task_default_time']) != 2 || !Timer::isTime($value['task_default_time'][0]) || !Timer::isTime($value['task_default_time'][1])) {
$value['task_default_time'] = ['09:00', '18:00'];
}
break;
case 'fileSetting':
$value['permission_pack_type'] = $value['permission_pack_type'] ?: 'all';
$value['permission_pack_userids'] = is_array($value['permission_pack_userids']) ? $value['permission_pack_userids'] : [];
break;
case 'aibotSetting':
if ($value['claude_token'] && empty($value['claude_key'])) {
$value['claude_key'] = $value['claude_token'];
}
$array = [];
$aiList = ['openai', 'claude', 'deepseek', 'gemini', 'grok', 'ollama', 'zhipu', 'qianwen', 'wenxin'];
$fieldList = ['key', 'secret', 'models', 'model', 'base_url', 'agency', 'temperature', 'system'];
foreach ($aiList as $aiName) {
foreach ($fieldList as $fieldName) {
$key = $aiName . '_' . $fieldName;
$content = $value[$key] ? trim($value[$key]) : '';
switch ($fieldName) {
case 'models':
if ($content) {
$content = explode("\n", $content);
$content = array_filter($content);
}
if (empty($content)) {
$content = self::AIDefaultModels($aiName);
}
$content = implode("\n", $content);
break;
case 'model':
$models = Setting::AIModels2Array($array[$key . 's'], true);
$content = in_array($content, $models) ? $content : ($models[0] ?? '');
break;
case 'temperature':
if ($content) {
$content = floatval(min(1, max(0, floatval($content) ?: 0.7)));
}
break;
}
$array[$key] = $content;
}
}
$value = $array;
break;
}
return $value;
}
/**
* 是否开启AI
* @param $ai
* @return bool
*/
public static function AIOpen($ai = 'openai')
{
$array = Base::setting('aibotSetting');
return !!$array[$ai . '_key'];
}
/**
* AI默认模型
* @param string $ai
* @return array
*/
public static function AIDefaultModels($ai = 'openai')
{
return match ($ai) {
'openai' => [
'gpt-4 | GPT-4',
'gpt-4-turbo | GPT-4 Turbo',
'gpt-4o | GPT-4o',
'gpt-4o-mini | GPT-4o Mini',
'o1 | GPT-o1',
'o1-mini | GPT-o1 Mini',
'o3-mini | GPT-o3 Mini',
'gpt-3.5-turbo | GPT-3.5 Turbo',
'gpt-3.5-turbo-16k | GPT-3.5 Turbo 16K',
'gpt-3.5-turbo-0125 | GPT-3.5 Turbo 0125',
'gpt-3.5-turbo-1106 | GPT-3.5 Turbo 1106'
],
'claude' => [
'claude-3-5-sonnet-latest | Claude 3.5 Sonnet',
'claude-3-5-sonnet-20241022 | Claude 3.5 Sonnet 20241022',
'claude-3-5-haiku-latest | Claude 3.5 Haiku',
'claude-3-5-haiku-20241022 | Claude 3.5 Haiku 20241022',
'claude-3-opus-latest | Claude 3 Opus',
'claude-3-opus-20240229 | Claude 3 Opus 20240229',
'claude-3-haiku-20240307 | Claude 3 Haiku 20240307',
'claude-2.1 | Claude 2.1',
'claude-2.0 | Claude 2.0'
],
'deepseek' => [
'deepseek-chat | DeepSeek V3',
'deepseek-reasoner | DeepSeek R1'
],
'gemini' => [
'gemini-2.0-flash | Gemini 2.0 Flash',
'gemini-2.0-flash-lite-preview-02-05 | Gemini 2.0 Flash-Lite Preview',
'gemini-1.5-flash | Gemini 1.5 Flash',
'gemini-1.5-flash-8b | Gemini 1.5 Flash 8B',
'gemini-1.5-pro | Gemini 1.5 Pro',
'gemini-1.0-pro | Gemini 1.0 Pro'
],
'grok' => [
'grok-2-vision-1212 | Grok 2 Vision 1212',
'grok-2-vision | Grok 2 Vision',
'grok-2-vision-latest | Grok 2 Vision Latest',
'grok-2-1212 | Grok 2 1212',
'grok-2 | Grok 2',
'grok-2-latest | Grok 2 Latest',
'grok-vision-beta | Grok Vision Beta',
'grok-beta | Grok Beta',
],
'zhipu' => [
'glm-4 | GLM-4',
'glm-4-plus | GLM-4 Plus',
'glm-4-air | GLM-4 Air',
'glm-4-airx | GLM-4 AirX',
'glm-4-long | GLM-4 Long',
'glm-4-flash | GLM-4 Flash',
'glm-4v | GLM-4V',
'glm-4v-plus | GLM-4V Plus',
'glm-3-turbo | GLM-3 Turbo'
],
'qianwen' => [
'qwen-max | QWEN Max',
'qwen-max-latest | QWEN Max Latest',
'qwen-turbo | QWEN Turbo',
'qwen-turbo-latest | QWEN Turbo Latest',
'qwen-plus | QWEN Plus',
'qwen-plus-latest | QWEN Plus Latest',
'qwen-long | QWEN Long'
],
'wenxin' => [
'ernie-4.0-8k | Ernie 4.0 8K',
'ernie-4.0-8k-latest | Ernie 4.0 8K Latest',
'ernie-4.0-turbo-128k | Ernie 4.0 Turbo 128K',
'ernie-4.0-turbo-8k | Ernie 4.0 Turbo 8K',
'ernie-3.5-128k | Ernie 3.5 128K',
'ernie-3.5-8k | Ernie 3.5 8K',
'ernie-speed-128k | Ernie Speed 128K',
'ernie-speed-8k | Ernie Speed 8K',
'ernie-lite-8k | Ernie Lite 8K',
'ernie-tiny-8k | Ernie Tiny 8K'
],
default => [],
};
}
/**
* AI模型转数组
* @param $models
* @param bool $retValue
* @return array
*/
public static function AIModels2Array($models, $retValue = false)
{
$list = is_array($models) ? $models : explode("\n", $models);
$array = [];
foreach ($list as $item) {
$arr = Base::newTrim(explode('|', $item . '|'));
if ($arr[0]) {
$array[] = [
'value' => $arr[0],
'label' => $arr[1] ?: $arr[0]
];
}
}
if ($retValue) {
return array_column($array, 'value');
}
return $array;
}
/**
* 验证邮箱地址(过滤忽略地址)
* @param $array
@@ -95,4 +266,36 @@ class Setting extends AbstractModel
}
return $array;
}
/**
* 验证消息限制
* @param $type
* @param $msg
* @return void
*/
public static function validateMsgLimit($type, $msg)
{
$keyName = 'msg_edit_limit';
$error = '此消息不可修改';
if ($type == 'rev') {
$keyName = 'msg_rev_limit';
$error = '此消息不可撤回';
}
$limitNum = intval(Base::settingFind('system', $keyName, 0));
if ($limitNum <= 0) {
return;
}
if ($msg instanceof WebSocketDialogMsg) {
$dialogMsg = $msg;
} else {
$dialogMsg = WebSocketDialogMsg::find($msg);
}
if (!$dialogMsg) {
return;
}
$limitTime = Carbon::parse($dialogMsg->created_at)->addMinutes($limitNum);
if ($limitTime->lt(Carbon::now())) {
throw new ApiException('已超过' . Doo::translate(Base::forumMinuteDay($limitNum)) . '' . $error);
}
}
}

View File

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

View File

@@ -222,10 +222,14 @@ class User extends AbstractModel
/**
* 返回是否禁用帐号(离职)
* @param bool $incAt 是否包含禁用时间
* @return bool
*/
public function isDisable()
public function isDisable($incAt = false)
{
if ($incAt) {
return in_array('disable', $this->identity) || $this->disable_at;
}
return in_array('disable', $this->identity);
}
@@ -238,6 +242,31 @@ class User extends AbstractModel
return in_array('admin', $this->identity);
}
/**
* 返回是否AI机器人
* @return bool
*/
public function isAiBot(&$aiName = '')
{
if (preg_match('/^ai-(.*?)@bot\.system$/', $this->email, $matches)) {
$aiName = $matches[1];
return true;
}
return false;
}
/**
* 返回是否用户机器人
* @return bool
*/
public function isUserBot()
{
if (preg_match('/^user-(.*?)@bot\.system$/', $this->email)) {
return true;
}
return false;
}
/**
* 判断是否管理员
*/
@@ -589,8 +618,14 @@ class User extends AbstractModel
return url("images/avatar/default_openai.png");
case 'ai-claude@bot.system':
return url("images/avatar/default_claude.png");
case 'ai-deepseek@bot.system':
return url("images/avatar/default_deepseek.png");
case 'ai-gemini@bot.system':
return url("images/avatar/default_gemini.png");
case 'ai-grok@bot.system':
return url("images/avatar/default_grok.png");
case 'ai-ollama@bot.system':
return url("images/avatar/default_ollama.png");
case 'ai-zhipu@bot.system':
return url("images/avatar/default_zhipu.png");
case 'bot-manager@bot.system':
@@ -679,4 +714,21 @@ class User extends AbstractModel
}
return $botUser;
}
/**
* 是否机器人
* @param $userid
* @return bool|mixed
*/
public static function isBot($userid)
{
if (empty($userid)) {
return false;
}
$userid = intval($userid);
if (RequestContext::has("isBot_" . $userid)) {
return RequestContext::get("isBot_" . $userid);
}
return (bool)User::find($userid)?->bot;
}
}

View File

@@ -73,10 +73,13 @@ class UserBot extends AbstractModel
'approval-alert' => '审批',
'ai-openai' => 'ChatGPT',
'ai-claude' => 'Claude',
'ai-wenxin' => '文心一言',
'ai-qianwen' => '通义千问',
'ai-deepseek' => 'DeepSeek',
'ai-gemini' => 'Gemini',
'ai-grok' => 'Grok',
'ai-ollama' => 'Ollama',
'ai-zhipu' => '智谱清言',
'ai-qianwen' => '通义千问',
'ai-wenxin' => '文心一言',
'bot-manager' => '机器人管理',
'meeting-alert' => '会议通知',
'okr-alert' => 'OKR提醒',
@@ -177,11 +180,35 @@ class UserBot extends AbstractModel
];
default:
if (preg_match('/^ai-(.*?)@bot.system$/', $email)) {
return [
[
if (preg_match('/^ai-(.*?)@bot\.system$/', $email, $match)) {
if (!Base::judgeClientVersion('0.42.62')) {
return [
'key' => '%3A.clear',
'label' => Doo::translate('清空上下文')
];
}
$aibotSetting = Base::setting('aibotSetting');
$aibotModel = $aibotSetting[$match[1] . '_model'];
$aibotModels = Setting::AIModels2Array($aibotSetting[$match[1] . '_models']);
if (empty($aibotModels)) {
return [];
}
return [
[
'key' => '~ai-model-select',
'label' => Doo::translate('选择模型'),
'config' => [
'model' => $aibotModel,
'models' => $aibotModels
]
],
[
'key' => '~ai-session-create',
'label' => Doo::translate('开启新会话'),
],
[
'key' => '~ai-session-history',
'label' => Doo::translate('历史会话'),
]
];
}
@@ -415,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

@@ -47,7 +47,7 @@ class UserCheckinFace extends AbstractModel
$record = base64_encode(file_get_contents($faceFile));
}
$url = 'http://' . env('APP_IPPR') . '.14' . ":7788/user";
$url = "http://face:7788/user";
$data = [
'name' => $nickname,
'enrollid' => $userid,
@@ -92,7 +92,7 @@ class UserCheckinFace extends AbstractModel
}
public static function deleteDeviceUser($userid) {
$url = 'http://' . env('APP_IPPR') . '.14' . ":7788/user/delete";
$url = "http://face:7788/user/delete";
$data = [
'enrollid' => $userid,
'backupnum' => 50, // 13 删除整个用户 50 删除图片

View File

@@ -34,6 +34,21 @@ use App\Exceptions\ApiException;
*/
class UserDepartment extends AbstractModel
{
/**
* 获取所有父级部门
* @return array
*/
public function parents()
{
$parents = [];
$parent = $this;
while ($parent) {
$parents[] = $parent;
$parent = $parent->parent_id ? self::find($parent->parent_id) : null;
}
return $parents;
}
/**
* 保存部门
* @param $data
@@ -131,9 +146,7 @@ class UserDepartment extends AbstractModel
});
// 解散群组
$dialog = WebSocketDialog::find($this->dialog_id);
if ($dialog) {
$dialog->deleteDialog();
}
$dialog?->deleteDialog();
//
$this->delete();
}

View File

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

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Models;
/**
* App\Models\WebSocketDialogConfig
*
* @property int $id
* @property int $dialog_id 对话ID
* @property int $userid 用户ID
* @property string $type 配置类型
* @property string|null $value 配置值
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\WebSocketDialog|null $dialog
* @property-read \App\Models\User $user
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig whereType($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig whereUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig whereValue($value)
* @mixin \Eloquent
*/
class WebSocketDialogConfig extends AbstractModel
{
/**
* 可以批量赋值的属性
*
* @var array
*/
protected $fillable = [
'dialog_id',
'userid',
'type',
'value',
];
/**
* 获取关联的对话
*/
public function dialog()
{
return $this->belongsTo(WebSocketDialog::class, 'dialog_id');
}
/**
* 获取关联的用户
*/
public function user()
{
return $this->belongsTo(User::class, 'userid');
}
}

View File

@@ -18,6 +18,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property int $id
* @property int|null $dialog_id 对话ID
* @property string|null $dialog_type 对话类型
* @property int|null $session_id 会话ID
* @property int|null $userid 发送会员ID
* @property string|null $type 消息类型
* @property string|null $mtype 消息类型(用于搜索)
@@ -30,6 +31,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property int|null $todo 设为待办会员ID
* @property int|null $link 是否存在链接
* @property int|null $modify 是否编辑
* @property int|null $bot 是否机器人的消息
* @property int|null $reply_num 有多少条回复
* @property int|null $reply_id 回复ID
* @property int|null $forward_id 转发ID
@@ -38,6 +40,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read int|mixed $percentage
* @property-read \App\Models\User|null $user
* @property-read \App\Models\WebSocketDialog|null $webSocketDialog
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
@@ -49,6 +52,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereBot($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDialogId($value)
@@ -66,6 +70,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereReplyId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereReplyNum($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereSend($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereSessionId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereTag($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereTodo($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereType($value)
@@ -96,6 +101,14 @@ class WebSocketDialogMsg extends AbstractModel
return $this->hasOne(WebSocketDialog::class, 'id', 'dialog_id');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function user(): \Illuminate\Database\Eloquent\Relations\HasOne
{
return $this->hasOne(User::class, 'userid', 'userid');
}
/**
* 阅读占比
* @return int|mixed
@@ -103,7 +116,11 @@ class WebSocketDialogMsg extends AbstractModel
public function getPercentageAttribute()
{
if (!isset($this->appendattrs['percentage'])) {
$this->generatePercentage();
if ($this->read > $this->send || empty($this->send)) {
$this->appendattrs['percentage'] = 100;
} else {
$this->appendattrs['percentage'] = intval($this->read / $this->send * 100);
}
}
return $this->appendattrs['percentage'];
}
@@ -180,22 +197,6 @@ class WebSocketDialogMsg extends AbstractModel
return $msg;
}
/**
* 获取占比
* @param bool|int $increment 是否新增阅读数
* @return int
*/
public function generatePercentage($increment = false) {
if ($increment) {
$this->increment('read', is_bool($increment) ? 1 : $increment);
}
if ($this->read > $this->send || empty($this->send)) {
return $this->appendattrs['percentage'] = 100;
} else {
return $this->appendattrs['percentage'] = intval($this->read / $this->send * 100);
}
}
/**
* 标记已送达 同时 告诉发送人已送达
* @param $userid
@@ -225,16 +226,17 @@ class WebSocketDialogMsg extends AbstractModel
if (!$msgRead->read_at) {
$msgRead->read_at = Carbon::now();
$msgRead->save();
$this->generatePercentage(true);
//
$row = self::incrementRead($this->id);
PushTask::push([
'userid' => $this->userid,
'userid' => $row->userid,
'msg' => [
'type' => 'dialog',
'mode' => 'readed',
'data' => [
'id' => $this->id,
'read' => $this->read,
'percentage' => $this->percentage,
'id' => $row->id,
'read' => $row->read,
'percentage' => $row->percentage,
],
]
]);
@@ -243,6 +245,24 @@ class WebSocketDialogMsg extends AbstractModel
return true;
}
/**
* 增加已读数量
* @param $msgId
* @return self
*/
private static function incrementRead($msgId)
{
return self::transaction(function () use ($msgId) {
$model = WebSocketDialogMsg::lockForUpdate()->find($msgId);
if (!$model) {
throw new \Exception('记录不存在');
}
$model->increment('read');
return WebSocketDialogMsg::find($msgId);
});
}
/**
* emoji回复
* @param $symbol
@@ -285,7 +305,12 @@ class WebSocketDialogMsg extends AbstractModel
];
//
$dialog = WebSocketDialog::find($this->dialog_id);
$dialog?->pushMsg('update', $resData);
if ($dialog) {
$dialog->pushMsg('update', $resData);
WebSocketDialogUser::whereDialogId($dialog->id)->change([
'updated_at' => Carbon::now()->toDateTimeString('millisecond'),
]);
}
//
return Base::retSuccess('success', $resData);
}
@@ -514,10 +539,6 @@ class WebSocketDialogMsg extends AbstractModel
*/
public function withdrawMsg()
{
$send_dt = Carbon::parse($this->created_at)->addDay();
if ($send_dt->lt(Carbon::now())) {
throw new ApiException('已超过24小时此消息不能撤回');
}
AbstractModel::transaction(function() {
$deleteRead = WebSocketDialogMsgRead::whereMsgId($this->id)->whereNull('read_at')->delete(); // 未阅读记录不需要软删除,直接删除即可
$this->delete();
@@ -574,6 +595,9 @@ class WebSocketDialogMsg extends AbstractModel
case 'text':
return self::previewTextMsg($data['msg'], $preserveHtml);
case 'longtext':
return $data['msg']['desc'] ? Base::cutStr($data['msg']['desc'], 50) : ("[" . Doo::translate("长文本") . "]");
case 'vote':
$action = Doo::translate("投票");
return "[{$action}] " . self::previewTextMsg($data['msg'], $preserveHtml);
@@ -610,7 +634,8 @@ class WebSocketDialogMsg extends AbstractModel
return "[{$action}] " . self::previewMsg($data['msg']['data']);
case 'notice':
return Base::cutStr(Doo::translate($data['msg']['notice']), 50);
$notice = $data['msg']['source'] === 'api' ? $data['msg']['notice'] : Doo::translate($data['msg']['notice']);
return Base::cutStr($notice, 50);
case 'template':
return self::previewTemplateMsg($data['msg']);
@@ -635,7 +660,12 @@ class WebSocketDialogMsg extends AbstractModel
$text = $msgData['text'] ?? '';
if (!$text) return '';
if ($msgData['type'] === 'md') {
$text = preg_replace("/:::\s*reasoning[\s\S]*?:::/", "", $text);
if (preg_match('/:::\s*reasoning\s+/', $text)) {
return Doo::translate('思考中...');
}
$text = Base::markdown2html($text);
$text = self::previewConvertTaskList($text);
}
$text = preg_replace("/<img\s+class=\"emoticon\"[^>]*?alt=\"(\S+)\"[^>]*?>/", "[$1]", $text);
$text = preg_replace("/<img\s+class=\"emoticon\"[^>]*?>/", "[" . Doo::translate('动画表情') . "]", $text);
@@ -650,6 +680,36 @@ class WebSocketDialogMsg extends AbstractModel
return $text;
}
/**
* 转换任务列表
* @param $text
* @return array|string|string[]|null
*/
private static function previewConvertTaskList($text) {
$pattern = '/:::\s*(create-task-list|create-subtask-list)(.*?):::/s';
$replacement = function($matches) {
$content = $matches[2];
$lines = explode("\n", trim($content));
$result = [];
$currentTitle = '';
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) continue;
if (preg_match('/^title:\s*(.+)$/', $line, $titleMatch)) {
$currentTitle = $titleMatch[1];
$result[] = $currentTitle;
} elseif (preg_match('/^desc:\s*(.+)$/', $line, $descMatch)) {
if (!empty($currentTitle)) {
$result[] = $descMatch[1];
}
}
}
return implode("\n", $result);
};
return preg_replace_callback($pattern, $replacement, $text);
}
/**
* 预览文件消息
* @param $msg
@@ -679,13 +739,15 @@ class WebSocketDialogMsg extends AbstractModel
return $msg['title_raw'];
}
if ($msg['type'] === 'task_list' && count($msg['list']) === 1) {
return Doo::translate($msg['title']) . ": " . Base::cutStr($msg['list'][0]['name'], 50);
$title = $msg['source'] === 'api' ? $msg['title'] : Doo::translate($msg['title']);
return $title . ": " . Base::cutStr($msg['list'][0]['name'], 50);
}
if (!empty($msg['title'])) {
return Doo::translate($msg['title']);
return $msg['source'] === 'api' ? $msg['title'] : Doo::translate($msg['title']);
}
if ($msg['type'] === 'content' && is_string($msg['content']) && $msg['content'] !== '') {
return Base::cutStr(Doo::translate($msg['content']), 50);
$content = $msg['source'] === 'api' ? $msg['content'] : Doo::translate($msg['content']);
return Base::cutStr($content, 50);
}
return Doo::translate('未知的消息');
}
@@ -701,9 +763,15 @@ class WebSocketDialogMsg extends AbstractModel
$key = '';
switch ($this->type) {
case 'text':
if (!preg_match("/<span[^>]*?data-quick-key=([\"'])([^\"']+?)\\1[^>]*?>/is", $this->msg['text'])) {
$key = strip_tags($this->msg['text']);
if (preg_match("/<span[^>]*?data-quick-key=([\"'])([^\"']+?)\\1[^>]*?>/i", $this->msg['text'])) {
break;
}
$key = $this->msg['text'];
if ($this->msg['type'] === 'md') {
$key = preg_replace("/:::\s*reasoning[\s\S]*?:::/", "", $key);
$key = Base::markdown2html($key);
}
$key = strip_tags($key);
break;
case 'vote':
@@ -722,14 +790,24 @@ class WebSocketDialogMsg extends AbstractModel
break;
}
}
$key = str_replace(["&quot;", "&amp;", "&lt;", "&gt;"], "", $key);
$key = str_replace(["\r", "\n", "\t", "&nbsp;"], " ", $key);
$key = preg_replace("/^\/[A-Za-z]+/", " ", $key);
$key = preg_replace("/\s+/", " ", $key);
$this->key = trim($key);
$this->key = self::filterEscape($key);
$this->save();
}
/**
* 过滤转义
* @param $content
* @return string
*/
public static function filterEscape($content)
{
$content = str_replace(["&quot;", "&amp;", "&lt;", "&gt;"], "", $content);
$content = str_replace(["\r", "\n", "\t", "&nbsp;"], " ", $content);
$content = preg_replace("/^\/[A-Za-z]+/", " ", $content);
$content = preg_replace("/\s+/", " ", $content);
return trim($content);
}
/**
* 返回引用消息(如果是文本只取预览)
* @return array|mixed
@@ -760,7 +838,7 @@ class WebSocketDialogMsg extends AbstractModel
$imagePath = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/";
Base::makeDir(public_path($imagePath));
$imagePath .= md5s($base64) . "." . $matchs[1][$key];
if (Base::saveContentImage(public_path($imagePath), base64_decode($base64), 90)) {
if (Base::saveContentImage(public_path($imagePath), base64_decode($base64))) {
$imageSize = getimagesize(public_path($imagePath));
if ($extension = Image::thumbImage(public_path($imagePath), public_path($imagePath) . "_thumb.{*}", 320, 0, 80)) {
$imagePath .= "_thumb.{$extension}";
@@ -817,8 +895,16 @@ class WebSocketDialogMsg extends AbstractModel
$imageSaveLocal = Base::settingFind("system", "image_save_local");
preg_match_all("/<img[^>]*?src=([\"'])(.*?(png|jpg|jpeg|webp|gif).*?)\\1[^>]*?>/is", $text, $matchs);
foreach ($matchs[2] as $key => $str) {
$parsed = parse_url($str);
if (str_starts_with($parsed['path'], "/uploads/")) {
$relativePath = ltrim($parsed['path'], "/");
$relativePath = Base::thumbRestore($relativePath);
if (file_exists(public_path($relativePath))) {
$str = "{{RemoteURL}}{$relativePath}";
}
}
if ($imageSaveLocal === 'close') {
$imageSize = getimagesize($str);
$imageSize = @getimagesize($str);
if ($imageSize === false) {
$imageSize = ["auto", "auto"];
}
@@ -844,7 +930,7 @@ class WebSocketDialogMsg extends AbstractModel
$image = file_get_contents($str);
if (empty($image)) {
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:90:90:images/other/imgerr.jpg::]", $text);
} else if (Base::saveContentImage(public_path($imagePath), $image, 90)) {
} else if (Base::saveContentImage(public_path($imagePath), $image)) {
$imageSize = getimagesize(public_path($imagePath));
if ($extension = Image::thumbImage(public_path($imagePath), public_path($imagePath) . "_thumb.{*}", 320, 0, 80)) {
$imagePath .= "_thumb.{$extension}";
@@ -853,7 +939,7 @@ class WebSocketDialogMsg extends AbstractModel
}
}
}
// @成员、#任务、~文件
// @成员、#任务、~文件、%报告
preg_match_all("/<span\s+class=\"mention\"(.*?)>.*?<\/span>.*?<\/span>.*?<\/span>/s", $text, $matchs);
foreach ($matchs[1] as $key => $str) {
preg_match("/data-denotation-char=\"(.*?)\"/", $str, $matchChar);
@@ -861,6 +947,7 @@ class WebSocketDialogMsg extends AbstractModel
preg_match("/data-value=\"(.*?)\"/s", $str, $matchValye);
$keyId = $matchId[1];
if ($matchChar[1] === "~") {
// 文件特殊处理
if (Base::isNumber($keyId)) {
$file = File::permissionFind($keyId, User::auth());
if ($file->type == 'folder') {
@@ -876,6 +963,19 @@ class WebSocketDialogMsg extends AbstractModel
throw new ApiException('文件分享错误');
}
}
} elseif ($matchChar[1] === "%") {
// 报告特殊处理
if (Base::isNumber($keyId)) {
$reportLink = ReportLink::generateLink($keyId, User::userid());
$keyId = $reportLink['code'];
} else {
preg_match("/\/single\/report\/detail\/(.*?)$/i", $keyId, $match);
if ($match && strlen($match[1]) >= 8) {
$keyId = $match[1];
} else {
throw new ApiException('报告分享错误');
}
}
}
$text = str_replace($matchs[0][$key], "[:{$matchChar[1]}:{$keyId}:{$matchValye[1]}:]", $text);
}
@@ -904,31 +1004,18 @@ class WebSocketDialogMsg extends AbstractModel
foreach ($matchs[0] as $key => $str) {
$herf = $matchs[2][$key];
$title = $matchs[3][$key] ?: $herf;
preg_match("/\/single\/file\/(.*?)$/i", strip_tags($title), $match);
if ($match && strlen($match[1]) >= 8) {
$file = File::select(['files.id', 'files.name', 'files.ext'])->join('file_links as L', 'files.id', '=', 'L.file_id')->where('L.code', $match[1])->first();
if ($file && $file->name) {
$name = $file->ext ? "{$file->name}.{$file->ext}" : $file->name;
$text = str_replace($str, "[:~:{$match[1]}:{$name}:]", $text);
continue;
}
if (self::formatLink($str, strip_tags($title), $text)) {
continue;
}
$herf = base64_encode($herf);
$title = base64_encode($title);
$text = str_replace($str, "[:LINK:{$herf}:{$title}:]", $text);
}
// 文件分享链接
// 分享链接
preg_match_all("/(https?:\/\/)((\w|=|\?|\.|\/|&|-|:|\+|%|;|#|@|,|!)+)/i", $text, $matchs);
if ($matchs) {
foreach ($matchs[0] as $str) {
preg_match("/\/single\/file\/(.*?)$/i", $str, $match);
if ($match && strlen($match[1]) >= 8) {
$file = File::select(['files.id', 'files.name', 'files.ext'])->join('file_links as L', 'files.id', '=', 'L.file_id')->where('L.code', $match[1])->first();
if ($file && $file->name) {
$name = $file->ext ? "{$file->name}.{$file->ext}" : $file->name;
$text = str_replace($str, "[:~:{$match[1]}:{$name}:]", $text);
}
}
self::formatLink($str, $str, $text);
}
}
// 过滤标签
@@ -958,10 +1045,41 @@ class WebSocketDialogMsg extends AbstractModel
$text = preg_replace("/\[:@:(.*?):(.*?):\]/i", "<span class=\"mention user\" data-id=\"$1\">@$2</span>", $text);
$text = preg_replace("/\[:#:(.*?):(.*?):\]/is", "<span class=\"mention task\" data-id=\"$1\">#$2</span>", $text);
$text = preg_replace("/\[:~:(.*?):(.*?):\]/i", "<a class=\"mention file\" href=\"{{RemoteURL}}single/file/$1\" target=\"_blank\">~$2</a>", $text);
$text = preg_replace("/\[:%:(.*?):(.*?):\]/i", "<a class=\"mention report\" href=\"{{RemoteURL}}single/report/detail/$1\" target=\"_blank\">%$2</a>", $text);
$text = preg_replace("/\[:QUICK:(.*?):(.*?):\]/i", "<span data-quick-key=\"$1\">$2</span>", $text);
return preg_replace("/^(<p><\/p>)+|(<p><\/p>)+$/i", "", $text);
}
/**
* 链接转换处理
* @param $search
* @param $subject
* @param $content
* @return bool
*/
public static function formatLink($search, $subject, &$content)
{
$ret = false;
preg_match("/\/single\/file\/(.*?)$/i", $subject, $match);
if ($match && strlen($match[1]) >= 8) {
$file = File::select(['files.id', 'files.name', 'files.ext'])->join('file_links as L', 'files.id', '=', 'L.file_id')->where('L.code', $match[1])->first();
if ($file && $file->name) {
$name = $file->ext ? "{$file->name}.{$file->ext}" : $file->name;
$content = str_replace($search, "[:~:{$match[1]}:{$name}:]", $content);
$ret = true;
}
}
preg_match("/\/single\/report\/detail\/(.*?)$/i", $subject, $match);
if ($match && strlen($match[1]) >= 8) {
$report = Report::select(['reports.id', 'reports.title'])->join('report_links as L', 'reports.id', '=', 'L.rid')->where('L.code', $match[1])->first();
if ($report && $report->title) {
$content = str_replace($search, "[:%:{$match[1]}:{$report->title}:]", $content);
$ret = true;
}
}
return $ret;
}
/**
* 发送消息、修改消息
* @param string $action 动作
@@ -1012,7 +1130,7 @@ class WebSocketDialogMsg extends AbstractModel
$fileUrl = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/" . md5s($msg['thumb']) . ".jpg";
$filePath = public_path($fileUrl);
Base::makeDir(dirname($filePath));
if (!Base::saveContentImage($filePath, $thumb, 90)) {
if (!Base::saveContentImage($filePath, $thumb)) {
throw new ApiException('保存地图快照失败');
}
$imageSize = getimagesize($filePath);
@@ -1071,6 +1189,7 @@ class WebSocketDialogMsg extends AbstractModel
}
//
$updateData = [
'type' => $type,
'mtype' => $mtype,
'link' => $link,
'msg' => array_merge($oldMsg, $msg),
@@ -1113,12 +1232,14 @@ class WebSocketDialogMsg extends AbstractModel
$dialogMsg = self::createInstance([
'dialog_id' => $dialog_id,
'dialog_type' => $dialog->type,
'session_id' => $dialog->session_id,
'reply_id' => $reply_id,
'forward_id' => $forward_id,
'userid' => $sender,
'type' => $type,
'mtype' => $mtype,
'link' => $link,
'bot' => User::isBot($sender) ? 1 : 0,
'msg' => $msg,
'read' => 0,
]);
@@ -1126,6 +1247,8 @@ class WebSocketDialogMsg extends AbstractModel
$dialogMsg->send = 1;
$dialogMsg->generateKeyAndSave($search_key);
//
WebSocketDialogSession::updateTitle($dialogMsg->session_id, $dialogMsg);
//
if ($dialogMsg->type === 'meeting') {
MeetingMsg::createInstance([
'meetingid' => $dialogMsg->msg['meetingid'],
@@ -1157,6 +1280,55 @@ class WebSocketDialogMsg extends AbstractModel
}
}
/**
* 批量发送消息
* @param User $user 发送的会员
* @param array $userids 接收的会员ID
* @param array $dialogids 接收的会话ID
* @param string $msgText 发送的消息
* @return array
*/
public static function sendMsgBatch($user, $userids, $dialogids, $msgText)
{
return AbstractModel::transaction(function() use ($user, $userids, $dialogids, $msgText) {
$msgs = [];
$already = [];
if ($dialogids) {
if (!is_array($dialogids)) {
$dialogids = [$dialogids];
}
foreach ($dialogids as $dialogid) {
$res = WebSocketDialogMsg::sendMsg(null, $dialogid, 'text', ['text' => $msgText], $user->userid);
if (Base::isSuccess($res)) {
$msgs[] = $res['data'];
$already[] = $dialogid;
}
}
}
if ($userids) {
if (!is_array($userids)) {
$userids = [$userids];
}
foreach ($userids as $userid) {
if (!User::whereUserid($userid)->exists()) {
continue;
}
$dialog = WebSocketDialog::checkUserDialog($user, $userid);
if ($dialog && !in_array($dialog->id, $already)) {
$res = WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $msgText], $user->userid);
if (Base::isSuccess($res)) {
$msgs[] = $res['data'];
}
}
}
}
return Base::retSuccess('发送成功', [
'msgs' => $msgs
]);
});
}
/**
* 将被@的人加入群
* @param WebSocketDialog $dialog 对话

View File

@@ -91,7 +91,9 @@ class WebSocketDialogMsgRead extends AbstractModel
}
}
foreach ($dialogMsg as $item) {
$item['dialogMsg']?->generatePercentage($item['readNum']);
if ($item['dialogMsg']) {
$item['dialogMsg']->increment('read', $item['readNum']);
}
}
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace App\Models;
use App\Module\Base;
use App\Module\Extranet;
use Swoole\Coroutine;
use Cache;
/**
* App\Models\WebSocketDialogSession
*
* @property int $id
* @property int $dialog_id 对话ID
* @property string $title 会话标题
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\WebSocketDialog|null $dialog
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession whereTitle($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession whereUpdatedAt($value)
* @mixin \Eloquent
*/
class WebSocketDialogSession extends AbstractModel
{
/**
* 可以批量赋值的属性
*
* @var array
*/
protected $fillable = [
'dialog_id',
'userid',
'title',
];
/**
* 获取关联的对话
*/
public function dialog()
{
return $this->belongsTo(WebSocketDialog::class, 'dialog_id');
}
/**
* @param $sessionId
* @param WebSocketDialogMsg $dialogMsg
* @return void
*/
public static function updateTitle($sessionId, $dialogMsg)
{
if (!$sessionId) {
return;
}
if ($dialogMsg->type != 'text') {
return;
}
$cacheKey = 'dialog_session_title_' . $sessionId;
if (Cache::has($cacheKey)) {
return;
}
$originalTitle = $dialogMsg->key ?: $dialogMsg->msg['text'] ?: 'Untitled';
$title = Base::cutStr($originalTitle, 100);
if ($title == '...') {
return;
}
$session = self::whereId($sessionId)->first();
if (!$session) {
return;
}
$session->title = $title;
$session->save();
Cache::forever($cacheKey, true);
// 通过AI接口更新对话标题
go(function () use ($session, $title, $originalTitle) {
Coroutine::sleep(0.1);
$res = Extranet::openAIGenerateTitle($originalTitle);
if (Base::isError($res)) {
return;
}
$newTitle = $res['data'];
if ($newTitle && $newTitle != $title) {
$session->title = Base::cutStr($newTitle, 100);
$session->save();
}
});
}
}

View File

@@ -2,14 +2,13 @@
namespace App\Models;
use Carbon\Carbon;
/**
* App\Models\WebSocketDialogUser
*
* @property int $id
* @property int|null $dialog_id 对话ID
* @property int|null $userid 会员ID
* @property int|null $bot 是否机器人
* @property \Illuminate\Support\Carbon|null $top_at 置顶时间
* @property \Illuminate\Support\Carbon|null $last_at 最后消息时间
* @property int|null $mark_unread 是否标记为未读0否1是
@@ -30,6 +29,7 @@ use Carbon\Carbon;
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereBot($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereColor($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereDialogId($value)

View File

@@ -9,11 +9,12 @@ use App\Services\RequestContext;
use Cache;
use Carbon\Carbon;
use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Exception\CommonMarkException;
use League\HTMLToMarkdown\HtmlConverter;
use Overtrue\Pinyin\Pinyin;
use Redirect;
use Request;
use Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\File;
use Validator;
@@ -801,16 +802,16 @@ class Base
str_starts_with(str_replace(' ', '', $str), "data:image/")
) {
return $str;
} else {
if (RequestContext::has('fill_url_remote_url')) {
return "{{RemoteURL}}" . $str;
}
try {
return url($str);
} catch (\Throwable) {
return self::getSchemeAndHost() . "/" . $str;
}
}
if (RequestContext::has('fill_url_remote_url')) {
return "{{RemoteURL}}" . $str;
}
try {
$fillUrl = url($str);
} catch (\Throwable) {
$fillUrl = self::getSchemeAndHost() . "/" . $str;
}
return RequestContext::replaceBaseUrl($fillUrl);
}
/**
@@ -1294,7 +1295,7 @@ class Base
* 获取或设置
* @param $setname // 配置名称
* @param bool $array // 保存内容
* @param false $isUpdate // 保存内容为更新模式,默认否
* @param bool $isUpdate // 保存内容为更新模式,默认否
* @return array
*/
public static function setting($setname, $array = false, $isUpdate = false)
@@ -1475,14 +1476,36 @@ class Base
public static function forumHourDay($hour)
{
$hour = intval($hour);
if ($hour > 24) {
if ($hour >= 24) {
$day = floor($hour / 24);
$hour -= $day * 24;
return $day . '天' . $hour . '小时';
if ($hour > 0) {
return $day . '天' . $hour . '小时';
}
return $day . '天';
}
return $hour . '小时';
}
/**
* 分钟转天/小时/分钟
* @param $minute
* @return string
*/
public static function forumMinuteDay($minute)
{
$minute = intval($minute);
if ($minute >= 60) {
$hour = floor($minute / 60);
$minute -= $hour * 60;
if ($minute > 0) {
return Base::forumHourDay($hour) . $minute . '分钟';
}
return Base::forumHourDay($hour);
}
return $minute . '分钟';
}
/**
* 创建Carbon对象
* @param $var
@@ -1849,18 +1872,18 @@ class Base
if (!in_array($extension, ['mp3', 'wav'])) {
return Base::retError('语音格式错误');
}
$fileName = 'record_' . md5($base64) . '.' . $extension;
$saveName = 'record_' . md5($base64) . '.' . $extension;
$fileDir = $param['path'];
$filePath = public_path($fileDir);
Base::makeDir($filePath);
if (file_put_contents($filePath . $fileName, base64_decode(str_replace($res[1], '', $base64)))) {
$fileSize = filesize($filePath . $fileName);
if (file_put_contents($filePath . $saveName, base64_decode(str_replace($res[1], '', $base64)))) {
$fileSize = filesize($filePath . $saveName);
$array = [
"name" => $fileName, //原文件名
"name" => $saveName, //原文件名
"size" => Base::twoFloat($fileSize / 1024, true), //大小KB
"file" => $filePath . $fileName, //文件的完整路径 "D:\www....KzZ.jpg"
"path" => $fileDir . $fileName, //相对路径 "uploads/pic....KzZ.jpg"
"url" => Base::fillUrl($fileDir . $fileName), //完整的URL "https://.....hhsKzZ.jpg"
"file" => $filePath . $saveName, //文件的完整路径 "D:\www....KzZ.jpg"
"path" => $fileDir . $saveName, //相对路径 "uploads/pic....KzZ.jpg"
"url" => Base::fillUrl($fileDir . $saveName), //完整的URL "https://.....hhsKzZ.jpg"
"ext" => $extension, //文件后缀名
];
return Base::retSuccess('success', $array);
@@ -1871,8 +1894,23 @@ class Base
/**
* image64图片保存
* @param array $param [ image64=带前缀的base64, path=>文件路径, fileName=>文件名称, scale=>[压缩原图宽,高, 压缩方式], autoThumb=>false不要自动生成缩略图, 'quality'=>压缩图片质量(默认0不压缩) ]
* @return array [name=>文件名, size=>文件大小(单位KB),file=>绝对地址, path=>相对地址, url=>全路径地址, ext=>文件后缀名]
* @param array $param [
image64=带前缀的base64,
path=>文件路径,
fileName=>文件名称,
saveName=>保存文件名称,
scale=>[压缩原图宽,高, 压缩方式],
autoThumb=>false不要自动生成缩略图,
quality=>压缩图片质量(默认0不压缩)
]
* @return array [
name=>文件名,
size=>文件大小(单位KB),
file=>绝对地址,
path=>相对地址,
url=>全路径地址,
ext=>文件后缀名
]
*/
public static function image64save($param)
{
@@ -1883,8 +1921,8 @@ class Base
return Base::retError('图片格式错误');
}
$scaleName = "";
if ($param['fileName']) {
$fileName = basename($param['fileName']);
if ($param['saveName']) {
$saveName = basename($param['saveName']);
} else {
if ($param['scale'] && is_array($param['scale'])) {
list($width, $height) = $param['scale'];
@@ -1895,21 +1933,21 @@ class Base
}
}
}
$fileName = 'paste_' . md5($imgBase64) . '.' . $extension;
$saveName = 'paste_' . md5($imgBase64) . '.' . $extension;
$scaleName = md5_file($imgBase64) . $scaleName . '.' . $extension;
}
$fileDir = $param['path'];
$filePath = public_path($fileDir);
$fileFullPath = $filePath . $fileName;
$fileFullPath = $filePath . $saveName;
Base::makeDir($filePath);
if (file_put_contents($fileFullPath, base64_decode(str_replace($res[1], '', $imgBase64)))) {
$fileSize = filesize($fileFullPath);
$array = [
"name" => $fileName, //原文件名
"name" => $param['fileName'] ?: $saveName, //原文件名
"size" => Base::twoFloat($fileSize / 1024, true), //大小KB
"file" => $fileFullPath, //文件的完整路径 "D:\www....KzZ.jpg"
"path" => $fileDir . $fileName, //相对路径 "uploads/pic....KzZ.jpg"
"url" => Base::fillUrl($fileDir . $fileName), //完整的URL "https://.....hhsKzZ.jpg"
"path" => $fileDir . $saveName, //相对路径 "uploads/pic....KzZ.jpg"
"url" => Base::fillUrl($fileDir . $saveName), //完整的URL "https://.....hhsKzZ.jpg"
"thumb" => '', //缩略图(预览图) "https://.....hhsKzZ.jpg_thumb.jpg"
"width" => -1, //图片宽度
"height" => -1, //图片高度
@@ -1932,7 +1970,7 @@ class Base
// 图片裁剪
$cutMode = ($width > 0 && $height > 0) ? 'cover' : 'percentage';
$cutMode = $param['scale'][2] ?? $cutMode;
Image::thumbImage($array['file'], $array['file'], $width, $height, 90, $cutMode);
Image::thumbImage($array['file'], $array['file'], $width, $height, true, $cutMode);
// 更新图片尺寸
$paramet = getimagesize($array['file']);
$array['width'] = $paramet[0];
@@ -1940,18 +1978,17 @@ 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;
}
}
}
}
// 压缩图片
$quality = intval($param['quality']);
if ($quality > 0) {
Image::compressImage($array['file'], $quality);
if ($param['quality']) {
Image::compressImage($array['file'], $param['quality']);
$array['size'] = Base::twoFloat(filesize($array['file']) / 1024, true);
}
//生成缩略图
@@ -1978,6 +2015,7 @@ class Base
file=>Request::file,
path=>文件路径,
fileName=>文件名称,
saveName=>保存文件名称,
scale=>[压缩原图宽,高, 压缩方式],
size=>限制大小KB,
autoThumb=>false不要自动生成缩略图,
@@ -2018,7 +2056,7 @@ class Base
$type = ['mp3', 'wma', 'wav', 'amr'];
break;
case 'excel':
$type = ['xls', 'xlsx'];
$type = ['xls', 'xlsx', 'xlsm', 'xlt', 'xltx', 'ods', 'ots', 'csv', 'tsv'];
break;
case 'app':
$type = ['apk'];
@@ -2069,10 +2107,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'];
@@ -2083,19 +2121,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"
@@ -2177,7 +2215,7 @@ class Base
// 图片裁剪
$cutMode = ($width > 0 && $height > 0) ? 'cover' : 'percentage';
$cutMode = $param['scale'][2] ?? $cutMode;
Image::thumbImage($array['file'], $array['file'], $width, $height, 90, $cutMode);
Image::thumbImage($array['file'], $array['file'], $width, $height, true, $cutMode);
// 更新图片尺寸
$paramet = getimagesize($array['file']);
$array['width'] = $paramet[0];
@@ -2185,18 +2223,17 @@ 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;
}
}
}
}
// 压缩图片
$quality = intval($param['quality']);
if ($quality > 0) {
Image::compressImage($array['file'], $quality);
if ($param['quality']) {
Image::compressImage($array['file'], $param['quality']);
$array['size'] = Base::twoFloat(filesize($array['file']) / 1024, true);
}
// 生成缩略图
@@ -2760,12 +2797,12 @@ class Base
}
/**
* BinaryFileResponse 下载文件
* DownloadFileResponse 下载文件
* @param File|\SplFileInfo|string $file 文件对象或路径
* @param string|null $name 下载文件名
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
* @return StreamedResponse
*/
public static function BinaryFileResponse($file, $name = null)
public static function DownloadFileResponse($file, $name = null)
{
try {
// 处理文件对象
@@ -2782,6 +2819,12 @@ class Base
throw new FileException('File must be readable and exist.');
}
// 获取文件信息
$size = $file->getSize();
if ($size === false || $size < 0) {
throw new FileException('Unable to determine file size.');
}
// 处理文件名
if (empty($name)) {
$name = basename($file->getPathname());
@@ -2793,34 +2836,98 @@ class Base
$name = Base::cutStr($name, 180);
$name = str_replace(['"', '<', '>', '|', '/', '\\', '?', ':'], '', $name);
// IE 浏览器特殊处理
$encodedName = $name;
if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/MSIE|Internet Explorer|Trident/i", $_SERVER['HTTP_USER_AGENT'])) {
$encodedName = rawurlencode($name);
// 获取MIME类型
$mimeType = $file->getMimeType();
if (empty($mimeType)) {
$mimeType = 'application/octet-stream';
}
// 创建响应对象
return new \Symfony\Component\HttpFoundation\BinaryFileResponse($file->getPathname(), 200, [
'Content-Type' => $file->getMimeType() ?: 'application/octet-stream',
// 处理 Range 请求
$start = 0;
$end = $size - 1;
$length = $size;
$isRangeRequest = false;
if (isset($_SERVER['HTTP_RANGE'])) {
$range = str_replace('bytes=', '', $_SERVER['HTTP_RANGE']);
if (preg_match('/^(\d+)-(\d*)$/', $range, $matches)) {
$start = intval($matches[1]);
$end = !empty($matches[2]) ? intval($matches[2]) : $size - 1;
// 验证范围的有效性
if ($start >= 0 && $end < $size && $start <= $end) {
$length = $end - $start + 1;
$isRangeRequest = true;
} else {
$start = 0;
$end = $size - 1;
}
}
}
// 设置基本响应头
$headers = [
'Content-Type' => $mimeType,
'Content-Disposition' => sprintf(
'attachment; filename="%s"; filename*=UTF-8\'\'%s',
$encodedName,
$name,
rawurlencode($name)
),
// 添加缓存控制和安全相关的头
'Cache-Control' => 'private, no-transform, no-store, must-revalidate',
'Pragma' => 'public',
'Expires' => '0',
'Accept-Ranges' => 'bytes', // 支持断点续传
'X-Content-Type-Options' => 'nosniff', // 安全相关
]);
'Accept-Ranges' => 'bytes',
'Cache-Control' => 'private, no-transform, no-store, must-revalidate, max-age=0',
'Content-Length' => $length,
'Last-Modified' => gmdate('D, d M Y H:i:s', $file->getMTime()) . ' GMT',
'ETag' => sprintf('"%s"', md5_file($file->getPathname()))
];
if ($isRangeRequest) {
$headers['Content-Range'] = "bytes {$start}-{$end}/{$size}";
$statusCode = 206;
} else {
$statusCode = 200;
}
// 创建流式响应
return new StreamedResponse(
function () use ($file, $start, $length) {
$handle = fopen($file->getPathname(), 'rb');
if ($handle === false) {
throw new FileException('Cannot open file for reading');
}
if (fseek($handle, $start) === -1) {
fclose($handle);
throw new FileException('Cannot seek to position ' . $start);
}
$remaining = $length;
$bufferSize = 8192; // 8KB chunks
while ($remaining > 0 && !feof($handle)) {
$readSize = min($bufferSize, $remaining);
$buffer = fread($handle, $readSize);
if ($buffer === false) {
break;
}
echo $buffer;
flush();
$remaining -= strlen($buffer);
}
fclose($handle);
},
$statusCode,
$headers
);
} catch (\Exception $e) {
\Log::error('File download failed', [
'error' => $e->getMessage(),
'file' => $file->getPathname() ?? null,
'trace' => $e->getTraceAsString(),
'file' => $file ?? null,
'name' => $name ?? null,
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null, // 添加更多调试信息
'ip' => request()->ip()
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null,
'ip' => request()->ip(),
'range' => $_SERVER['HTTP_RANGE'] ?? null
]);
abort(403, 'File download failed');
}
@@ -2830,14 +2937,11 @@ class Base
* 保存图片到文件(同时压缩)
* @param $path
* @param $content
* @param int $quality 压缩图片质量(默认0不压缩)
* @return bool
*/
public static function saveContentImage($path, $content, int $quality = 0) {
public static function saveContentImage($path, $content) {
if (file_put_contents($path, $content)) {
if ($quality > 0) {
Image::compressImage($path, $quality);
}
Image::compressImage($path);
return true;
}
return false;
@@ -2889,11 +2993,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

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

View File

@@ -0,0 +1,204 @@
<?php
namespace App\Module\ElasticSearch;
use App\Module\Base;
use Illuminate\Support\Facades\Log;
/**
* Elasticsearch键值存储
*
* Class ElasticSearchKeyValue
* @package App\Module\ElasticSearch
*/
class ElasticSearchKeyValue extends ElasticSearchBase
{
const indexName = 'key_value_store';
/**
* 构造函数
* @return ElasticSearchBase
* @throws \Elastic\Elasticsearch\Exception\ConfigException
*/
public function __construct()
{
return parent::__construct(self::indexName());
}
/** ******************************************************************************************************** */
/** *********************************** 键值存储方法 ******************************************************** */
/** ******************************************************************************************************** */
/**
* 创建键值存储索引
* @return array
*/
public static function generateIndex()
{
try {
$es = new self();
// 如果索引已存在,则直接返回
if ($es->indexExists()) {
return ['acknowledged' => true, 'message' => '索引已存在'];
}
// 定义映射
$mappings = [
'properties' => [
'key' => ['type' => 'keyword'],
'value' => ['type' => 'text', 'fields' => ['keyword' => ['type' => 'keyword']]],
'created_at' => ['type' => 'integer'],
'updated_at' => ['type' => 'integer']
]
];
// 索引设置
$settings = [
'number_of_shards' => 1,
'number_of_replicas' => 1,
'refresh_interval' => '1s'
];
return $es->createIndex($settings, $mappings);
} catch (\Exception $e) {
Log::error('创建键值存储索引失败: ' . $e->getMessage());
return ['error' => $e->getMessage()];
}
}
/**
* 保存键值对
* @param string $key 键名
* @param mixed $value 键值
* @param string $namespace 命名空间,用于区分不同的键值存储场景
* @return array
*/
public static function save($key, $value, $namespace = 'default')
{
try {
// 确保索引存在
self::generateIndex();
$es = new self();
// 生成文档ID
$docId = "{$namespace}:{$key}";
// 准备文档数据
$document = [
'key' => $key,
'value' => is_array($value) ? json_encode($value, JSON_UNESCAPED_UNICODE) : $value,
'namespace' => $namespace,
'created_at' => time(),
'updated_at' => time()
];
// 索引文档
$result = $es->indexDocument($document, $docId);
// 刷新索引以确保立即可见
$es->refreshIndex();
return $result;
} catch (\Exception $e) {
Log::error('保存键值对失败: ' . $e->getMessage());
return ['error' => $e->getMessage()];
}
}
/**
* 获取键值
* @param string $key 键名
* @param mixed $default 默认值,当键不存在时返回
* @param string $namespace 命名空间,用于区分不同的键值存储场景
* @return mixed
*/
public static function get($key, $default = null, $namespace = 'default')
{
try {
$es = new self();
// 如果索引不存在,直接返回默认值
if (!$es->indexExists()) {
return $default;
}
// 生成文档ID
$docId = "{$namespace}:{$key}";
// 查询参数
$params = [
'index' => self::indexName(),
'id' => $docId
];
try {
// 获取文档
$response = $es->client->get($params)->asArray();
// 获取值
$value = $response['_source']['value'] ?? $default;
// 如果值是JSON字符串尝试解码
if (is_string($value) && $decoded = json_decode($value, true)) {
if (json_last_error() === JSON_ERROR_NONE) {
return $decoded;
}
}
return $value;
} catch (\Exception $e) {
// 文档不存在或其他错误,返回默认值
return $default;
}
} catch (\Exception $e) {
Log::error('获取键值对失败: ' . $e->getMessage());
return $default;
}
}
/**
* 获取键值,返回数组
* @param string $key 键名
* @param array $default 默认值,当键不存在时返回
* @param string $namespace 命名空间,用于区分不同的键值存储场景
* @return array
*/
public static function getArray($key, $default = [], $namespace = 'default')
{
return Base::string2array(self::get($key, $default, $namespace));
}
/**
* 删除键值对
* @param string $key 键名
* @param string $namespace 命名空间
* @return array
*/
public static function delete($key, $namespace = 'default')
{
try {
$es = new self();
// 如果索引不存在,直接返回成功
if (!$es->indexExists()) {
return ['result' => 'not_found'];
}
// 生成文档ID
$docId = "{$namespace}:{$key}";
// 删除文档
$result = $es->deleteDocument($docId);
// 刷新索引以确保立即生效
$es->refreshIndex();
return $result;
} catch (\Exception $e) {
Log::error('删除键值对失败: ' . $e->getMessage());
return ['error' => $e->getMessage()];
}
}
}

View File

@@ -0,0 +1,375 @@
<?php
namespace App\Module\ElasticSearch;
use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialogUser;
use Illuminate\Support\Facades\Log;
/**
* 对话系统消息索引
*
* Class ElasticSearchUserMsg
* @package App\Module\ElasticSearch
*/
class ElasticSearchUserMsg extends ElasticSearchBase
{
const indexName = 'dialog_user_msg';
/**
* 构造函数
* @return ElasticSearchBase
* @throws \Elastic\Elasticsearch\Exception\ConfigException
*/
public function __construct()
{
return parent::__construct(self::indexName());
}
/** ******************************************************************************************************** */
/** *********************************************** 基础 ************************************************** */
/** ******************************************************************************************************** */
/**
* 创建聊天系统索引 - 使用父子关系
* @return array
*/
public static function generateIndex()
{
// 定义映射
$mappings = [
'properties' => [
// 共用字段
'dialog_id' => ['type' => 'keyword'],
'created_at' => ['type' => 'date'],
'updated_at' => ['type' => 'date'],
// dialog_users 字段
'userid' => ['type' => 'keyword'],
'top_at' => ['type' => 'date'],
'last_at' => ['type' => 'date'],
'mark_unread' => ['type' => 'integer'],
'silence' => ['type' => 'integer'],
'hide' => ['type' => 'integer'],
'color' => ['type' => 'keyword'],
// dialog_msgs 字段
'msg_id' => ['type' => 'keyword'],
'sender_userid' => ['type' => 'keyword'],
'msg_type' => ['type' => 'keyword'],
'key' => ['type' => 'text'],
'bot' => ['type' => 'integer'],
// Join字段定义父子关系
'relationship' => [
'type' => 'join',
'relations' => [
'dialog_user' => 'dialog_msg' // dialog_user是父文档dialog_msg是子文档
]
],
]
];
// 索引设置
$settings = [
'number_of_shards' => 5,
'number_of_replicas' => 1,
'refresh_interval' => '5s'
];
try {
$es = new self();
return $es->createIndex($settings, $mappings);
} catch (\Exception $e) {
Log::error('创建聊天系统索引失败: ' . $e->getMessage());
return ['error' => $e->getMessage()];
}
}
/**
* 构建对话系统特定的搜索 - 根据用户ID和消息关键词搜索会话
* @param string $userid 用户ID
* @param string $keyword 消息关键词
* @param int $size 返回结果数量
* @return array
*/
public static function searchByKeyword($userid, $keyword, $size = 20)
{
// 注意这里的类型名称要与创建索引时的一致
$query = [
'bool' => [
'must' => [
[
'term' => [
'userid' => $userid
]
],
[
'has_child' => [
'type' => 'dialog_msg',
'query' => [
'bool' => [
'must' => [
[
'match_phrase' => [
'key' => $keyword
]
],
[
'term' => [
'bot' => 0
]
]
]
]
],
'inner_hits' => [
'size' => 1,
'sort' => [
'msg_id' => 'desc'
]
]
]
]
]
]
];
// 结果集合
$searchMap = [];
try {
// 开始搜索
$es = new self();
$results = $es->search($query, 0, $size, ['last_at' => 'desc']);
// 处理搜索结果
$hits = $results['hits']['hits'] ?? [];
foreach ($hits as $hit) {
if (isset($hit['inner_hits']['dialog_msg']['hits']['hits'][0])) {
$msgHit = $hit['inner_hits']['dialog_msg']['hits']['hits'][0];
$source = $hit['_source'];
$msgSource = $msgHit['_source'];
$searchMap[] = [
'id' => $source['dialog_id'],
'top_at' => $source['top_at'],
'last_at' => $source['last_at'],
'mark_unread' => $source['mark_unread'],
'silence' => $source['silence'],
'hide' => $source['hide'],
'color' => $source['color'],
'user_at' => $source['updated_at'],
'search_msg_id' => $msgSource['msg_id'],
];
}
}
} catch (\Exception $e) {
Log::error('searchByKeyword: ' . $e->getMessage());
}
// 返回搜索结果
return $searchMap;
}
/** ******************************************************************************************************** */
/** *********************************************** 用户 ************************************************** */
/** ******************************************************************************************************** */
/**
* 会话用户 - 生成文档ID
* @param WebSocketDialogUser $dialogUser
* @return string
*/
public static function generateUserDicId(WebSocketDialogUser $dialogUser)
{
return "user_{$dialogUser->userid}_dialog_{$dialogUser->dialog_id}";
}
/**
* 会话用户 - 生成文档格式
* @param WebSocketDialogUser $dialogUser
* @return array
*/
public static function generateUserFormat(WebSocketDialogUser $dialogUser)
{
return [
'dialog_id' => $dialogUser->dialog_id,
'created_at' => $dialogUser->created_at,
'updated_at' => $dialogUser->updated_at,
'userid' => $dialogUser->userid,
'top_at' => $dialogUser->top_at,
'last_at' => $dialogUser->last_at,
'mark_unread' => $dialogUser->mark_unread ? 1 : 0,
'silence' => $dialogUser->silence ? 1 : 0,
'hide' => $dialogUser->hide ? 1 : 0,
'color' => $dialogUser->color,
'relationship' => [
'name' => 'dialog_user'
]
];
}
/**
* 会话用户 - 同步到Elasticsearch
* @param WebSocketDialogUser $dialogUser
* @return void
*/
public static function syncUser(WebSocketDialogUser $dialogUser)
{
try {
$es = new self();
$es->indexDocument(self::generateUserFormat($dialogUser), self::generateUserDicId($dialogUser));
} catch (\Exception $e) {
Log::error('syncUser: ' . $e->getMessage());
}
}
/**
* 会话用户 - 从Elasticsearch删除
*/
public static function deleteUser(WebSocketDialogUser $dialogUser)
{
try {
$es = new self();
$docId = "user_{$dialogUser->userid}_dialog_{$dialogUser->dialog_id}";
// 删除用户-会话文档
$es->deleteDocument($docId);
// 注意:这里可能还需要删除所有关联的消息文档
// 但由于父子关系,可以通过查询找到所有子文档并删除
// 这里为简化可以选择在后台任务中处理或者直接依赖ES的级联删除功能
} catch (\Exception $e) {
Log::error('deleteUser: ' . $e->getMessage());
}
}
/** ******************************************************************************************************** */
/** *********************************************** 消息 ************************************************** */
/** ******************************************************************************************************** */
/**
* 会话消息 - 生成父文档ID
* @param WebSocketDialogMsg $dialogMsg
* @param $userid
* @return string
*/
public static function generateMsgParentId(WebSocketDialogMsg $dialogMsg, $userid)
{
return "user_{$userid}_dialog_{$dialogMsg->dialog_id}";
}
/**
* 会话消息 - 生成文档ID
* @param WebSocketDialogMsg $dialogMsg
* @param $userid
* @return string
*/
public static function generateMsgDicId(WebSocketDialogMsg $dialogMsg, $userid)
{
return "msg_{$dialogMsg->id}_user_{$userid}";
}
/**
* 会话消息 - 生成文档格式
* @param WebSocketDialogMsg $dialogMsg
* @param $userid
* @return array
*/
public static function generateMsgFormat(WebSocketDialogMsg $dialogMsg, $userid)
{
return [
'dialog_id' => $dialogMsg->dialog_id,
'created_at' => $dialogMsg->created_at,
'updated_at' => $dialogMsg->updated_at,
'msg_id' => $dialogMsg->id,
'sender_userid' => $dialogMsg->userid,
'msg_type' => $dialogMsg->type,
'key' => $dialogMsg->key,
'bot' => $dialogMsg->bot ? 1 : 0,
'relationship' => [
'name' => 'dialog_msg',
'parent' => self::generateMsgParentId($dialogMsg, $userid)
]
];
}
/**
* 会话消息 - 同步到Elasticsearch
*/
public static function syncMsg(WebSocketDialogMsg $dialogMsg)
{
try {
$es = new self();
// 获取此会话的所有用户
$dialogUsers = WebSocketDialogUser::whereDialogId($dialogMsg->dialog_id)->get();
if ($dialogUsers->isEmpty()) {
return;
}
$params = ['body' => []];
foreach ($dialogUsers as $dialogUser) {
$params['body'][] = [
'index' => [
'_index' => self::indexName(),
'_id' => self::generateMsgDicId($dialogMsg, $dialogUser->userid),
'routing' => self::generateMsgParentId($dialogMsg, $dialogUser->userid)
]
];
$params['body'][] = self::generateMsgFormat($dialogMsg, $dialogUser->userid);
}
if (!empty($params['body'])) {
$es->bulk($params);
}
} catch (\Exception $e) {
Log::error('syncMsg: ' . $e->getMessage());
}
}
/**
* 会话消息 - 从Elasticsearch删除
*/
public static function deleteMsg(WebSocketDialogMsg $dialogMsg)
{
try {
$es = new self();
// 获取此会话的所有用户
$dialogUsers = WebSocketDialogUser::whereDialogId($dialogMsg->dialog_id)->get();
if ($dialogUsers->isEmpty()) {
return;
}
$params = ['body' => []];
foreach ($dialogUsers as $dialogUser) {
$params['body'][] = [
'delete' => [
'_index' => self::indexName(),
'_id' => self::generateMsgDicId($dialogMsg, $dialogUser->userid),
'routing' => self::generateMsgParentId($dialogMsg, $dialogUser->userid)
]
];
}
if (!empty($params['body'])) {
$es->bulk($params);
}
} catch (\Exception $e) {
Log::error('deleteMsg: ' . $e->getMessage());
}
}
}

View File

@@ -15,9 +15,10 @@ class Extranet
/**
* 通过 openAI 语音转文字
* @param string $filePath
* @param array $extParams
* @return array
*/
public static function openAItranscriptions($filePath)
public static function openAItranscriptions($filePath, $extParams = [])
{
if (!file_exists($filePath)) {
return Base::retError("语音文件不存在");
@@ -27,32 +28,34 @@ class Extranet
if ($systemSetting['voice2text'] !== 'open' || empty($aibotSetting['openai_key'])) {
return Base::retError("语音转文字功能未开启");
}
//
$extra = [
'Content-Type' => 'multipart/form-data',
'Authorization' => 'Bearer ' . $aibotSetting['openai_key'],
];
if ($aibotSetting['openai_agency']) {
$extra['CURLOPT_PROXY'] = $aibotSetting['openai_agency'];
if (str_contains($aibotSetting['openai_agency'], 'socks')) {
$extra['CURLOPT_PROXYTYPE'] = CURLPROXY_SOCKS5;
} else {
$extra['CURLOPT_PROXYTYPE'] = CURLPROXY_HTTP;
}
$extra['CURLOPT_PROXYTYPE'] = str_contains($aibotSetting['openai_agency'], 'socks') ? CURLPROXY_SOCKS5 : CURLPROXY_HTTP;
}
$res = Ihttp::ihttp_request('https://api.openai.com/v1/audio/transcriptions', [
$post = array_merge($extParams, [
'file' => new \CURLFile($filePath),
'model' => 'whisper-1'
], $extra, 15);
if (Base::isError($res)) {
return Base::retError("语音转文字失败", $res);
'model' => 'whisper-1',
]);
$cacheKey = "openAItranscriptions::" . md5($filePath . '_' . Base::array2json($extra) . '_' . Base::array2json($extParams));
$result = Cache::remember($cacheKey, Carbon::now()->addDays(), function() use ($extra, $post) {
$res = Ihttp::ihttp_request('https://api.openai.com/v1/audio/transcriptions', $post, $extra, 15);
if (Base::isError($res)) {
return Base::retError("语音转文字失败", $res);
}
$resData = Base::json2array($res['data']);
if (empty($resData['text'])) {
return Base::retError("语音转文字失败", $resData);
}
return Base::retSuccess("success", $resData['text']);
});
if (Base::isError($result)) {
Cache::forget($cacheKey);
}
$resData = Base::json2array($res['data']);
if (empty($resData['text'])) {
return Base::retError("语音转文字失败", $resData);
}
//
return Base::retSuccess("success", $resData['text']);
return $result;
}
/**
@@ -74,18 +77,78 @@ class Extranet
];
if ($aibotSetting['openai_agency']) {
$extra['CURLOPT_PROXY'] = $aibotSetting['openai_agency'];
if (str_contains($aibotSetting['openai_agency'], 'socks')) {
$extra['CURLOPT_PROXYTYPE'] = CURLPROXY_SOCKS5;
} else {
$extra['CURLOPT_PROXYTYPE'] = CURLPROXY_HTTP;
}
$extra['CURLOPT_PROXYTYPE'] = str_contains($aibotSetting['openai_agency'], 'socks') ? CURLPROXY_SOCKS5 : CURLPROXY_HTTP;
}
$res = Ihttp::ihttp_request('https://api.openai.com/v1/chat/completions', json_encode([
"model" => "gpt-3.5-turbo",
$post = json_encode([
"model" => "gpt-4o-mini",
"messages" => [
[
"role" => "system",
"content" => "你是一个专业的翻译器,翻译的结果尽量符合“项目任务管理系统”的使用,并且翻译的结果不用额外添加换行尽量保持原格式,将提供的文本翻译成“{$targetLanguage}”语言。"
"content" => <<<EOF
你是一名专业翻译人员,请将 <translation_original_text> 标签内的内容翻译为{$targetLanguage}。
翻译要求:
- 翻译结果需符合“项目任务管理系统”的专业术语和使用场景。
- 保持原文格式、结构和排版不变。
- 语言表达准确、简洁,符合项目管理领域的行业规范。
- 注意专业术语的一致性和连贯性。
EOF
],
[
"role" => "user",
"content" => "<translation_original_text>{$text}</translation_original_text>"
]
]
]);
$cacheKey = "openAItranslations::" . md5(Base::array2json($extra) . '_' . Base::array2json($post));
$result = Cache::remember($cacheKey, Carbon::now()->addDays(), function() use ($extra, $post) {
$res = Ihttp::ihttp_request('https://api.openai.com/v1/chat/completions', $post, $extra, 15);
if (Base::isError($res)) {
return Base::retError("翻译失败", $res);
}
$resData = Base::json2array($res['data']);
if (empty($resData['choices'])) {
return Base::retError("翻译失败", $resData);
}
$result = $resData['choices'][0]['message']['content'];
$result = preg_replace('/^\"|\"$/', '', trim($result));
$result = preg_replace('/<\/*translation_original_text>/', '', trim($result));
if (empty($result)) {
return Base::retError("翻译失败", $result);
}
return Base::retSuccess("success", $result);
});
if (Base::isError($result)) {
Cache::forget($cacheKey);
}
return $result;
}
/**
* 通过 openAI 生成标题
* @param $text
* @return array
*/
public static function openAIGenerateTitle($text)
{
$aibotSetting = Base::setting('aibotSetting');
if (empty($aibotSetting['openai_key'])) {
return Base::retError("AI接口未配置");
}
$extra = [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $aibotSetting['openai_key'],
];
if ($aibotSetting['openai_agency']) {
$extra['CURLOPT_PROXY'] = $aibotSetting['openai_agency'];
$extra['CURLOPT_PROXYTYPE'] = str_contains($aibotSetting['openai_agency'], 'socks') ? CURLPROXY_SOCKS5 : CURLPROXY_HTTP;
}
$res = Ihttp::ihttp_request('https://api.openai.com/v1/chat/completions', json_encode([
"model" => "gpt-4o-mini",
"messages" => [
[
"role" => "system",
"content" => "你是一个专业的标题生成器,擅长为对话生成简洁的标题,请将提供的文本生成一个标题。"
],
[
"role" => "user",
@@ -94,20 +157,61 @@ class Extranet
]
]), $extra, 15);
if (Base::isError($res)) {
return Base::retError("翻译失败", $res);
return Base::retError("生成失败", $res);
}
$resData = Base::json2array($res['data']);
if (empty($resData['choices'])) {
return Base::retError("翻译失败", $resData);
return Base::retError("生成失败", $resData);
}
$result = $resData['choices'][0]['message']['content'];
$result = preg_replace('/^\"|\"$/', '', $result);
if (empty($result)) {
return Base::retError("翻译失败", $result);
return Base::retError("生成失败", $result);
}
return Base::retSuccess("success", $result);
}
/**
* 获取 ollama 模型
* @param $baseUrl
* @param $key
* @param $agency
* @return array
*/
public static function ollamaModels($baseUrl, $key = null, $agency = null)
{
$extra = [
'Content-Type' => 'application/json',
];
if ($key) {
$extra['Authorization'] = 'Bearer ' . $key;
}
if ($agency) {
$extra['CURLOPT_PROXY'] = $agency;
$extra['CURLOPT_PROXYTYPE'] = str_contains($agency, 'socks') ? CURLPROXY_SOCKS5 : CURLPROXY_HTTP;
}
$res = Ihttp::ihttp_request(rtrim($baseUrl, '/') . '/api/tags', [], $extra, 15);
if (Base::isError($res)) {
return Base::retError("获取失败", $res);
}
$resData = Base::json2array($res['data']);
if (empty($resData['models'])) {
return Base::retError("获取失败", $resData);
}
$models = [];
foreach ($resData['models'] as $model) {
if ($model['name'] !== $model['model']) {
$models[] = "{$model['model']} | {$model['name']}";
} else {
$models[] = $model['model'];
}
}
return Base::retSuccess("success", [
'models' => $models,
'original' => $resData['models']
]);
}
/**
* 获取IP地址经纬度
* @param string $ip

View File

@@ -220,11 +220,11 @@ class Image
* @param string $savePath 保存路径
* @param int $width 宽度
* @param int $height 高度
* @param int $quality 压缩质量0-100, 0 为不压缩
* @param int|bool $quality 压缩质量0-100, 0 为不压缩true 为从系统设置里面获取
* @param string $mode 模式percentage|cover|contain
* @return string|null 成功返回图片后缀,失败返回 false
*/
public static function thumbImage(string $imagePath, string $savePath, int $width, int $height, int $quality = 0, string $mode = 'percentage'): ?string
public static function thumbImage(string $imagePath, string $savePath, int $width, int $height, int|bool $quality = 0, string $mode = 'percentage'): ?string
{
if (!file_exists($imagePath)) {
return null;
@@ -237,7 +237,7 @@ class Image
$image = new Image($imagePath);
$image->thumb($width, $height, $mode);
$image->saveTo($savePath);
if ($quality > 0) {
if ($quality) {
Image::compressImage($savePath, $quality);
}
if ($savePath != $imagePath && filesize($savePath) >= filesize($imagePath)) {
@@ -253,14 +253,18 @@ class Image
/**
* 压缩图片(如果压缩后的图片比原图还大那就直接使用原图)
* @param array|string $path 图片路径如果是数组第1个元素为原图路径第2个元素为保存路径
* @param int $quality 压缩质量0-100
* @param int|bool $quality 压缩质量0-100,如果为 true则从系统设置里面获取
* @param float $minSize 最小尺寸小于这个尺寸不压缩单位KB
* @return bool
*/
public static function compressImage(array|string $path, int $quality = 100, float $minSize = 5): bool
public static function compressImage(array|string $path, int|bool $quality = true, float $minSize = 5): bool
{
if (Base::settingFind("system", "image_compress") === 'close') {
return false;
if ($quality === true) {
$setting = Base::setting("system");
if ($setting['image_compress'] === 'close') {
return false;
}
$quality = $setting['image_quality'];
}
if (is_array($path)) {
$imagePath = $path[0];
@@ -272,7 +276,7 @@ class Image
if (!file_exists($imagePath)) {
return false;
}
$quality = min(max($quality, 1), 100);
$quality = min(max(intval($quality), 1), 100);
$imageSize = filesize($imagePath);
if ($minSize > 0 && $imageSize < $minSize * 1024) {
return false;

136
app/Module/MsgTool.php Normal file
View File

@@ -0,0 +1,136 @@
<?php
namespace App\Module;
use DOMDocument;
use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Exception\CommonMarkException;
use League\HTMLToMarkdown\HtmlConverter;
class MsgTool
{
/**
* 截取文本并保持标签完整性
*
* @param string $text 要截取的文本
* @param int $length 截取长度
* @param string $type 文本类型 (htm 或 md)
* @return string 处理后的文本
*/
public static function truncateText($text, $length, $type = 'htm')
{
if (empty($text) || mb_strlen($text) <= $length) {
return $text;
}
$isMd = strtolower($type) === 'md';
$placeholders = [];
// 如果是Markdown先处理特殊标记及转换为HTML
if ($isMd) {
// 处理特殊标记
$pattern = '/:::\s*reasoning\s+(.*?)\s*:::/s';
$counter = 0;
$text = preg_replace_callback($pattern, function($matches) use ($type, $length, &$placeholders, &$counter) {
// 使用更简短的占位符避免被markdown解析
$placeholder = "@PH::{$counter}::PH@";
$placeholders[$placeholder] = "::: reasoning\n" . self::truncateText($matches[1], $length, $type) . "\n:::";
$counter++;
return $placeholder;
}, $text);
// 转换为HTML
try {
$converter = new CommonMarkConverter();
$text = $converter->convert($text);
} catch (CommonMarkException) {
return "";
}
}
// 创建DOM文档
$dom = new DOMDocument('1.0', 'UTF-8');
libxml_use_internal_errors(true);
$dom->loadHTML(mb_convert_encoding($text, 'HTML-ENTITIES', 'UTF-8'));
libxml_clear_errors();
// 获取body元素
$body = $dom->getElementsByTagName('body')->item(0);
$truncatedHtml = '';
$currentLength = 0;
// 递归函数来遍历节点并截取内容
self::traverseNodes($body, $currentLength, $length, $truncatedHtml);
// 如果是Markdown转换回Markdown及还原特殊标记
if ($isMd) {
// 转换回Markdown
try {
$converter = new HtmlConverter();
$truncatedHtml = $converter->convert($truncatedHtml);
} catch (\Exception) {
return "";
}
// 还原特殊标记
if (!empty($placeholders)) {
$truncatedHtml = preg_replace('/@P?H?:*\s*$/', '', $truncatedHtml);
$preCount = substr_count($truncatedHtml, '@PH::');
$sufCount = substr_count($truncatedHtml, '::PH@');
$diffCount = $preCount - $sufCount;
if ($diffCount > 0) {
$truncatedHtml .= str_repeat('::PH@', $diffCount);
}
$truncatedHtml = strtr($truncatedHtml, $placeholders);
}
}
return $truncatedHtml;
}
/**
* 递归遍历节点
* @param $node
* @param $currentLength
* @param $length
* @param $truncatedHtml
* @return void
*/
private static function traverseNodes($node, &$currentLength, $length, &$truncatedHtml)
{
foreach ($node->childNodes as $child) {
if ($currentLength >= $length) {
break;
}
if ($child->nodeType === XML_TEXT_NODE) {
$textContent = $child->textContent;
$remainingLength = $length - $currentLength;
if (mb_strlen($textContent) > $remainingLength) {
$truncatedHtml .= htmlspecialchars(mb_substr($textContent, 0, $remainingLength) . '...');
$currentLength += $remainingLength;
} else {
$truncatedHtml .= htmlspecialchars($textContent);
$currentLength += mb_strlen($textContent);
}
} elseif ($child->nodeType === XML_ELEMENT_NODE) {
$truncatedHtml .= '<' . $child->nodeName;
// 添加属性
if ($child->hasAttributes()) {
foreach ($child->attributes as $attr) {
$truncatedHtml .= ' ' . $attr->nodeName . '="' . htmlspecialchars($attr->nodeValue) . '"';
}
}
$truncatedHtml .= '>';
self::traverseNodes($child, $currentLength, $length, $truncatedHtml);
if ($currentLength < $length || $child->firstChild) {
$truncatedHtml .= '</' . $child->nodeName . '>';
}
}
}
}
}

View File

@@ -0,0 +1,259 @@
<?php
namespace App\Module;
use Exception;
use PhpOffice\PhpWord\IOFactory as WordIOFactory;
use PhpOffice\PhpSpreadsheet\IOFactory as SpreadsheetIOFactory;
use PhpOffice\PhpPresentation\IOFactory as PresentationIOFactory;
use Illuminate\Support\Facades\File as FileFacade;
class TextExtractor
{
private string $filePath;
private string $fileMimeType;
private string $fileExtension;
/**
* @param string $filePath
* @throws Exception
*/
public function __construct(string $filePath)
{
if (!file_exists($filePath)) {
throw new Exception("File does not exist: {$filePath}");
}
$this->filePath = $filePath;
$this->fileMimeType = FileFacade::mimeType($filePath);
$this->fileExtension = $this->detectFileType();
}
/**
* 从文件中提取文本
* @return string
* @throws Exception
*/
public function extractContent(): string
{
return match ($this->fileExtension) {
// Word文档
'docx' => $this->parseWordDocument(),
// Excel文档
'xlsx', 'xls', 'csv' => $this->parseSpreadsheet(),
// PowerPoint文档
'ppt', 'pptx' => $this->parsePresentation(),
// PDF文档
'pdf' => $this->parsePdf(),
// RTF文档
'rtf' => $this->parseRtf(),
// 其他文本文件
default => $this->parseOther(),
};
}
/**
* 获取文件类型
* @return string
*/
private function detectFileType(): string
{
return match ($this->fileMimeType) {
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
'application/vnd.ms-excel' => 'xls',
'text/csv', 'application/csv' => 'csv',
'application/vnd.ms-powerpoint' => 'ppt',
'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx',
'application/pdf' => 'pdf',
'application/rtf', 'text/rtf' => 'rtf',
default => strtolower(pathinfo($this->filePath, PATHINFO_EXTENSION)),
};
}
/**
* Parse Word documents (.doc, .docx)
* @return string
*/
private function parseWordDocument(): string
{
$phpWord = WordIOFactory::load($this->filePath);
$text = '';
// Extract text from each section
foreach ($phpWord->getSections() as $section) {
foreach ($section->getElements() as $element) {
if (method_exists($element, 'getText')) {
$text .= $element->getText() . "\n";
} elseif (method_exists($element, 'getElements')) {
foreach ($element->getElements() as $childElement) {
if (method_exists($childElement, 'getText')) {
$text .= $childElement->getText() . "\n";
}
}
}
}
}
return $text;
}
/**
* Parse spreadsheet files (.xlsx, .xls, .csv)
* @return string
*/
private function parseSpreadsheet(): string
{
$spreadsheet = SpreadsheetIOFactory::load($this->filePath);
$text = '';
// Extract text from all worksheets
foreach ($spreadsheet->getWorksheetIterator() as $worksheet) {
$text .= 'Worksheet: ' . $worksheet->getTitle() . "\n";
foreach ($worksheet->getRowIterator() as $row) {
$cellIterator = $row->getCellIterator();
$cellIterator->setIterateOnlyExistingCells(false);
$rowText = '';
foreach ($cellIterator as $cell) {
$value = $cell->getValue();
if (!empty($value)) {
$rowText .= $value . "\t";
}
}
if (!empty(trim($rowText))) {
$text .= trim($rowText) . "\n";
}
}
$text .= "\n";
}
return $text;
}
/**
* Parse presentation files (.ppt, .pptx)
* @return string
* @throws Exception
*/
private function parsePresentation(): string
{
$presentation = PresentationIOFactory::load($this->filePath);
$text = '';
// Extract text from all slides
foreach ($presentation->getAllSlides() as $slide) {
foreach ($slide->getShapeCollection() as $shape) {
if ($shape instanceof \PhpOffice\PhpPresentation\Shape\RichText) {
foreach ($shape->getParagraphs() as $paragraph) {
foreach ($paragraph->getRichTextElements() as $element) {
$text .= $element->getText();
}
$text .= "\n";
}
}
}
$text .= "\n";
}
return $text;
}
/**
* Parse PDF files (requires additional library like Smalot\PdfParser)
* @return string
* @throws Exception
*/
private function parsePdf(): string
{
// You'll need to install the Smalot PDF Parser: composer require smalot/pdfparser
if (!class_exists('\Smalot\PdfParser\Parser')) {
throw new \Exception("PDF Parser not available. Install with: composer require smalot/pdfparser");
}
$parser = new \Smalot\PdfParser\Parser();
$pdf = $parser->parseFile($this->filePath);
return $pdf->getText();
}
/**
* Parse RTF files
* @return string
*/
private function parseRtf(): string
{
// Simple RTF to text conversion
$content = file_get_contents($this->filePath);
// Remove RTF control words and groups
$content = preg_replace('/\\\\([a-z]{1,32})(-?[0-9]{1,10})?[ ]?/i', '', $content);
$content = preg_replace('/\\\\([^a-z]|[a-z]{33,})/i', '', $content);
$content = preg_replace('/\{\*?\\\\[^{}]*\}/', '', $content);
$content = preg_replace('/\{[\r\n]*\}/', '', $content);
// Convert special characters
$content = preg_replace('/\\\\\'([0-9a-f]{2})/i', '', $content);
// Remove remaining curly braces
$content = str_replace(['{', '}'], '', $content);
return $content ?: '';
}
/**
* Parse Other(text) files
* @return string
* @throws Exception
*/
private function parseOther(): string
{
$isBinary = !str_contains($this->fileMimeType, 'text/')
&& !str_contains($this->fileMimeType, 'application/json')
&& !str_contains($this->fileMimeType, 'application/xml');
if ($isBinary) {
throw new Exception("Unable to read the text content of this type of file");
}
return file_get_contents($this->filePath);
}
/** ********************************************************************* */
/** ********************************************************************* */
/** ********************************************************************* */
/**
* 获取文件内容
* @param $filePath
* @param int $fileMaxSize 最大文件大小单位字节默认1024KB
* @param int $contentMaxSize 最大内容大小单位字节默认300KB
* @return array
*/
public static function extractFile($filePath, int $fileMaxSize = 1024, int $contentMaxSize = 300): array
{
if (!file_exists($filePath) || !is_file($filePath)) {
return Base::retError("Failed to read contents of {$filePath}");
}
if (filesize($filePath) > $fileMaxSize * 1024) {
return Base::retError("File size exceeds " . Base::readableBytes($fileMaxSize * 1024) . ", unable to display content");
}
try {
$extractor = new self($filePath);
$content = $extractor->extractContent();
if (strlen($content) > $contentMaxSize * 1024) {
return Base::retError("Content size exceeds " . Base::readableBytes($contentMaxSize * 1024) . ", unable to display content");
}
return Base::retSuccess("success", $content);
} catch (Exception $e) {
return Base::retError($e->getMessage());
}
}
}

View File

@@ -92,7 +92,7 @@ class ProjectTaskObserver
}
$array = [];
if (in_array('task', $dataType)) {
$array = array_merge($array, ProjectTaskUser::whereTaskId($projectTask->id)->pluck('userid')->toArray());
$array = array_merge($array, ProjectTaskUser::whereTaskId($projectTask->id)->orWhere('task_pid' ,$projectTask->id)->pluck('userid')->toArray());
}
if (in_array('visibility', $dataType)) {
$array = array_merge($array, ProjectTaskVisibilityUser::whereTaskId($projectTask->id)->pluck('userid')->toArray());
@@ -121,5 +121,6 @@ class ProjectTaskObserver
Deleted::forget('projectTask', $projectTask->id, $forgetUserids);
break;
}
ProjectTask::whereParentId($projectTask->id)->change(['visibility' => $projectTask->visibility]);
}
}

View File

@@ -17,6 +17,9 @@ class ProjectTaskUserObserver
public function created(ProjectTaskUser $projectTaskUser)
{
Deleted::forget('projectTask', $projectTaskUser->task_id, $projectTaskUser->userid);
if ($projectTaskUser->task_pid) {
Deleted::forget('projectTask', $projectTaskUser->task_pid, $projectTaskUser->userid);
}
}
/**

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Observers;
use App\Models\WebSocketDialogMsg;
use App\Module\ElasticSearch\ElasticSearchUserMsg;
class WebSocketDialogMsgObserver
{
/**
* Handle the WebSocketDialogMsg "created" event.
*
* @param \App\Models\WebSocketDialogMsg $webSocketDialogMsg
* @return void
*/
public function created(WebSocketDialogMsg $webSocketDialogMsg)
{
ElasticSearchUserMsg::syncMsg($webSocketDialogMsg);
}
/**
* Handle the WebSocketDialogMsg "updated" event.
*
* @param \App\Models\WebSocketDialogMsg $webSocketDialogMsg
* @return void
*/
public function updated(WebSocketDialogMsg $webSocketDialogMsg)
{
ElasticSearchUserMsg::syncMsg($webSocketDialogMsg);
}
/**
* Handle the WebSocketDialogMsg "deleted" event.
*
* @param \App\Models\WebSocketDialogMsg $webSocketDialogMsg
* @return void
*/
public function deleted(WebSocketDialogMsg $webSocketDialogMsg)
{
ElasticSearchUserMsg::deleteMsg($webSocketDialogMsg);
}
/**
* Handle the WebSocketDialogMsg "restored" event.
*
* @param \App\Models\WebSocketDialogMsg $webSocketDialogMsg
* @return void
*/
public function restored(WebSocketDialogMsg $webSocketDialogMsg)
{
//
}
/**
* Handle the WebSocketDialogMsg "force deleted" event.
*
* @param \App\Models\WebSocketDialogMsg $webSocketDialogMsg
* @return void
*/
public function forceDeleted(WebSocketDialogMsg $webSocketDialogMsg)
{
//
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Observers;
use App\Models\Deleted;
use App\Models\WebSocketDialogUser;
use App\Module\ElasticSearch\ElasticSearchUserMsg;
use Carbon\Carbon;
class WebSocketDialogUserObserver
@@ -29,6 +30,7 @@ class WebSocketDialogUserObserver
}
}
Deleted::forget('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
ElasticSearchUserMsg::syncUser($webSocketDialogUser);
}
/**
@@ -39,7 +41,7 @@ class WebSocketDialogUserObserver
*/
public function updated(WebSocketDialogUser $webSocketDialogUser)
{
//
ElasticSearchUserMsg::syncUser($webSocketDialogUser);
}
/**
@@ -51,6 +53,7 @@ class WebSocketDialogUserObserver
public function deleted(WebSocketDialogUser $webSocketDialogUser)
{
Deleted::record('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
ElasticSearchUserMsg::deleteUser($webSocketDialogUser);
}
/**

View File

@@ -7,11 +7,13 @@ use App\Models\ProjectTask;
use App\Models\ProjectTaskUser;
use App\Models\ProjectUser;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialogUser;
use App\Observers\ProjectObserver;
use App\Observers\ProjectTaskObserver;
use App\Observers\ProjectTaskUserObserver;
use App\Observers\ProjectUserObserver;
use App\Observers\WebSocketDialogMsgObserver;
use App\Observers\WebSocketDialogObserver;
use App\Observers\WebSocketDialogUserObserver;
use Illuminate\Auth\Events\Registered;
@@ -43,6 +45,7 @@ class EventServiceProvider extends ServiceProvider
ProjectTaskUser::observe(ProjectTaskUserObserver::class);
ProjectUser::observe(ProjectUserObserver::class);
WebSocketDialog::observe(WebSocketDialogObserver::class);
WebSocketDialogMsg::observe(WebSocketDialogMsgObserver::class);
WebSocketDialogUser::observe(WebSocketDialogUserObserver::class);
}
}

View File

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

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

View File

@@ -21,25 +21,62 @@ class AutoArchivedTask extends AbstractTask
}
public function start()
{
$this->systemAutoArchived();
$this->projectAutoArchived();
}
/**
* 处理已完成未归档的任务(系统默认)
*/
private function systemAutoArchived()
{
$setting = Base::setting('system');
if ($setting['auto_archived'] === 'open') {
$archivedDay = floatval($setting['archived_day']);
if ($archivedDay > 0) {
$archivedDay = min(100, $archivedDay);
$archivedTime = Carbon::now()->subDays($archivedDay);
//获取已完成未归档的任务
$taskLists = ProjectTask::whereNotNull('complete_at')
->where('complete_at', '<=', $archivedTime)
->where('archived_userid', 0)
->whereNull('archived_at')
->take(100)
->get();
/** @var ProjectTask $task */
foreach ($taskLists AS $task) {
$task->archivedTask(Carbon::now(), true);
}
}
if ($setting['auto_archived'] !== 'open') {
return;
}
$archivedDay = min(365, floatval($setting['archived_day']));
if ($archivedDay <= 0) {
return;
}
$taskLists = ProjectTask::select('project_tasks.*')
->join('projects', 'projects.id', '=', 'project_tasks.project_id')
->whereNotNull('project_tasks.complete_at')
->where('project_tasks.complete_at', '<=', Carbon::now()->subDays($archivedDay))
->where('project_tasks.archived_userid', 0)
->whereNull('project_tasks.archived_at')
->where('projects.archive_method', '!=', 'custom')
->take(100)
->get();
/** @var ProjectTask $task */
foreach ($taskLists as $task) {
$task->archivedTask(Carbon::now(), true);
}
}
/**
* 处理已完成未归档的任务(项目自定义)
*/
private function projectAutoArchived()
{
// 获取设置了自定义归档的项目的任务
$prefix = \DB::getTablePrefix();
$taskLists = ProjectTask::select('project_tasks.*')
->join('projects', 'projects.id', '=', 'project_tasks.project_id')
->whereNotNull('project_tasks.complete_at')
->where('project_tasks.archived_userid', 0)
->whereNull('project_tasks.archived_at')
->where('projects.archive_method', 'custom')
->whereRaw("DATEDIFF(NOW(), {$prefix}project_tasks.complete_at) >= {$prefix}projects.archive_days")
->with(['project' => function ($query) {
$query->select('id', 'archive_days');
}])
->take(100)
->get();
/** @var ProjectTask $task */
foreach ($taskLists as $task) {
$task->archivedTask(Carbon::now(), true);
}
}

View File

@@ -2,14 +2,23 @@
namespace App\Tasks;
use App\Models\FileContent;
use App\Models\Project;
use App\Models\ProjectTask;
use App\Models\Report;
use App\Models\User;
use App\Models\UserBot;
use App\Models\UserDepartment;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogConfig;
use App\Models\WebSocketDialogMsg;
use App\Module\Base;
use App\Module\Doo;
use App\Module\Ihttp;
use App\Module\TextExtractor;
use Carbon\Carbon;
use Exception;
use League\HTMLToMarkdown\HtmlConverter;
use DB;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
@@ -22,17 +31,19 @@ use DB;
*/
class BotReceiveMsgTask extends AbstractTask
{
protected $userid;
protected $msgId;
protected $mention;
protected $client = [];
protected $userid; // 机器人ID
protected $msgId; // 消息ID
protected $mention; // 是否提及
protected $mentionOther; // 是否提及其他人
protected $client = []; // 客户端信息(版本、语言、平台)
public function __construct($userid, $msgId, $mention, $client = [])
public function __construct($userid, $msgId, $mentions, $client = [])
{
parent::__construct(...func_get_args());
$this->userid = $userid;
$this->msgId = $msgId;
$this->mention = $mention;
$this->mention = array_intersect([$userid], $mentions) ? 1 : 0; // 是否提及(不含@所有人)
$this->mentionOther = array_diff($mentions, [0, $userid]) ? 1 : 0; // 是否提及其他人
$this->client = is_array($client) ? $client : [];
}
@@ -42,12 +53,14 @@ class BotReceiveMsgTask extends AbstractTask
if (empty($botUser)) {
return;
}
$msg = WebSocketDialogMsg::find($this->msgId);
$msg = WebSocketDialogMsg::with(['user'])->find($this->msgId);
if (empty($msg)) {
return;
}
$msg->readSuccess($botUser->userid);
$this->botManagerReceive($msg, $botUser);
if (!$msg->user?->bot) {
$this->botReceiveBusiness($msg, $botUser);
}
}
public function end()
@@ -56,16 +69,15 @@ class BotReceiveMsgTask extends AbstractTask
}
/**
* 机器人管理处理消息
* 机器人处理消息
* @param WebSocketDialogMsg $msg
* @param User $botUser
* @return void
*/
private function botManagerReceive(WebSocketDialogMsg $msg, User $botUser)
private function botReceiveBusiness(WebSocketDialogMsg $msg, User $botUser)
{
// 位置消息
// 位置消息(仅支持签到机器人)
if ($msg->type === 'location') {
// 签到机器人
if ($botUser->email === 'check-in@bot.system') {
$content = UserBot::checkinBotQuickMsg('locat-checkin', $msg->userid, $msg->msg);
if ($content) {
@@ -78,36 +90,39 @@ class BotReceiveMsgTask extends AbstractTask
return;
}
// 文本消息
if ($msg->type !== 'text') {
// 提取指令
try {
$command = $this->extractCommand($msg, $botUser, $this->mention);
if (empty($command)) {
return;
}
} catch (Exception $e) {
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'template', [
'type' => 'content',
'content' => $e->getMessage() ?: "指令解析失败。",
], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
return;
}
$original = $msg->msg['text'];
if ($this->mention) {
$original = preg_replace("/<span class=\"mention user\" data-id=\"(\d+)\">(.*?)<\/span>/", "", $original);
}
if (preg_match("/<span[^>]*?data-quick-key=([\"'])([^\"']+?)\\1[^>]*?>(.*?)<\/span>/is", $original, $match)) {
$command = $match[2];
if (str_starts_with($command, '%3A.')) {
$command = ":" . substr($command, 4);
}
} else {
$command = trim(strip_tags($original));
}
//
// 查询会话
$dialog = WebSocketDialog::find($msg->dialog_id);
if (empty($dialog)) {
return;
}
// 推送Webhook
if ($command
&& !str_starts_with($command, '/')
&& ($dialog->type === 'user' || $this->mention)) {
$this->botManagerWebhook($command, $msg, $botUser, $dialog);
// 如果是群聊,未提及丹提及其他人
if ($dialog->type === 'group' && !$this->mention && $this->mentionOther) {
return;
}
// 推送Webhook
$this->botWebhookBusiness($command, $msg, $botUser, $dialog);
// 仅支持用户会话
if ($dialog->type !== 'user') {
return;
}
// 签到机器人
if ($botUser->email === 'check-in@bot.system') {
$content = UserBot::checkinBotQuickMsg($command, $msg->userid);
@@ -118,6 +133,7 @@ class BotReceiveMsgTask extends AbstractTask
], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
}
}
// 隐私机器人
if ($botUser->email === 'anon-msg@bot.system') {
$array = UserBot::anonBotQuickMsg($command);
@@ -129,8 +145,10 @@ class BotReceiveMsgTask extends AbstractTask
], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
}
}
// 管理机器人
if (str_starts_with($command, '/')) {
// 判断是否是机器人管理员
if ($botUser->email === 'bot-manager@bot.system') {
$isManager = true;
} elseif (UserBot::whereBotId($botUser->userid)->whereUserid($msg->userid)->exists()) {
@@ -142,7 +160,8 @@ class BotReceiveMsgTask extends AbstractTask
], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
return;
}
//
// 指令处理
$array = Base::newTrim(explode(" ", "{$command} "));
$type = $array[0];
$data = [];
@@ -179,7 +198,7 @@ class BotReceiveMsgTask extends AbstractTask
case '/hello':
case '/info':
$botId = $isManager ? $array[1] : $botUser->userid;
$data = $this->botManagerOne($botId, $msg->userid);
$data = $this->botOne($botId, $msg->userid);
if (!$data) {
$content = "机器人不存在。";
}
@@ -189,32 +208,11 @@ class BotReceiveMsgTask extends AbstractTask
* 创建
*/
case '/newbot':
if (User::select(['users.*'])
->join('user_bots', 'users.userid', '=', 'user_bots.bot_id')
->where('users.bot', 1)
->where('user_bots.userid', $msg->userid)
->count() >= 50) {
$content = "超过最大创建数量。";
break;
}
if (strlen($array[1]) < 2 || strlen($array[1]) > 20) {
$content = "机器人名称由2-20个字符组成。";
break;
}
$data = User::botGetOrCreate("user-" . Base::generatePassword(), [
'nickname' => $array[1]
], $msg->userid);
if (empty($data)) {
$content = "创建失败。";
break;
}
$dialog = WebSocketDialog::checkUserDialog($data, $msg->userid);
if ($dialog) {
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => '/hello',
'title' => '创建成功。',
'data' => $data,
], $data->userid); // todo 未能在任务end事件来发送任务
$res = UserBot::newbot($msg->userid, $array[1]);
if (Base::isError($res)) {
$content = $res['msg'];
} else {
$data = $res['data'];
}
break;
@@ -228,7 +226,7 @@ class BotReceiveMsgTask extends AbstractTask
$content = "机器人名称由2-20个字符组成。";
break;
}
$data = $this->botManagerOne($botId, $msg->userid);
$data = $this->botOne($botId, $msg->userid);
if ($data) {
$data->nickname = $nameString;
$data->az = Base::getFirstCharter($nameString);
@@ -245,7 +243,7 @@ class BotReceiveMsgTask extends AbstractTask
*/
case '/deletebot':
$botId = $isManager ? $array[1] : $botUser->userid;
$data = $this->botManagerOne($botId, $msg->userid);
$data = $this->botOne($botId, $msg->userid);
if ($data) {
$data->deleteUser('delete bot');
} else {
@@ -258,7 +256,7 @@ class BotReceiveMsgTask extends AbstractTask
*/
case '/token':
$botId = $isManager ? $array[1] : $botUser->userid;
$data = $this->botManagerOne($botId, $msg->userid);
$data = $this->botOne($botId, $msg->userid);
if ($data) {
User::generateToken($data);
} else {
@@ -271,7 +269,7 @@ class BotReceiveMsgTask extends AbstractTask
*/
case '/revoke':
$botId = $isManager ? $array[1] : $botUser->userid;
$data = $this->botManagerOne($botId, $msg->userid);
$data = $this->botOne($botId, $msg->userid);
if ($data) {
$data->encrypt = Base::generatePassword(6);
$data->password = Doo::md5s(Base::generatePassword(32), $data->encrypt);
@@ -287,7 +285,7 @@ class BotReceiveMsgTask extends AbstractTask
case '/clearday':
$botId = $isManager ? $array[1] : $botUser->userid;
$clearDay = $isManager ? $array[2] : $array[1];
$data = $this->botManagerOne($botId, $msg->userid);
$data = $this->botOne($botId, $msg->userid);
if ($data) {
$userBot = UserBot::whereBotId($botId)->whereUserid($msg->userid)->first();
if ($userBot) {
@@ -308,7 +306,7 @@ class BotReceiveMsgTask extends AbstractTask
case '/webhook':
$botId = $isManager ? $array[1] : $botUser->userid;
$webhookUrl = $isManager ? $array[2] : $array[1];
$data = $this->botManagerOne($botId, $msg->userid);
$data = $this->botOne($botId, $msg->userid);
if (strlen($webhookUrl) > 255) {
$content = "webhook地址最长仅支持255个字符。";
} elseif ($data) {
@@ -331,7 +329,7 @@ class BotReceiveMsgTask extends AbstractTask
case '/dialog':
$botId = $isManager ? $array[1] : $botUser->userid;
$nameKey = $isManager ? $array[2] : $array[1];
$data = $this->botManagerOne($botId, $msg->userid);
$data = $this->botOne($botId, $msg->userid);
if ($data) {
$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'])
@@ -357,8 +355,8 @@ class BotReceiveMsgTask extends AbstractTask
}
break;
}
//
// 回复消息
if ($content) {
$msgData = [
'type' => 'content',
@@ -403,119 +401,94 @@ class BotReceiveMsgTask extends AbstractTask
* @param WebSocketDialog $dialog
* @return void
*/
private function botManagerWebhook(string $command, WebSocketDialogMsg $msg, User $botUser, WebSocketDialog $dialog)
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;
switch ($botUser->email) {
// ChatGPT 机器人
case 'ai-openai@bot.system':
$setting = Base::setting('aibotSetting');
$webhookUrl = "{$serverUrl}/ai/openai/send";
$extras = [
'openai_key' => $setting['openai_key'],
'openai_agency' => $setting['openai_agency'],
'openai_model' => $setting['openai_model'],
'server_url' => $serverUrl,
'chunk_size' => 7,
];
if (empty($extras['openai_key'])) {
if ($botUser->isAiBot($type)) {
// AI机器人
$setting = Base::setting('aibotSetting');
$extras = [
'model_type' => match ($type) {
'qianwen' => 'qwen',
default => $type,
},
'model_name' => $setting[$type . '_model'],
'system_message' => $setting[$type . '_system'],
'api_key' => $setting[$type . '_key'],
'base_url' => $setting[$type . '_base_url'],
'agency' => $setting[$type . '_agency'],
'server_url' => $serverUrl,
];
if ($setting[$type . '_temperature']) {
$extras['temperature'] = floatval($setting[$type . '_temperature']);
}
if ($msg->msg['model_name']) {
$extras['model_name'] = $msg->msg['model_name'];
}
if (preg_match("/(.*?)(\s+|\s*[_-]\s*)(think|thinking|reasoning)\s*$/", $extras['model_name'], $match)) {
$extras['model_name'] = $match[1];
$extras['max_tokens'] = 20000;
$extras['thinking'] = 4096;
$extras['temperature'] = 1.0;
}
if ($dialog->session_id) {
$extras['context_key'] = 'session_' . $dialog->session_id;
}
if ($type === 'wenxin') {
$extras['api_key'] .= ':' . $setting['wenxin_secret'];
}
if ($type === 'ollama') {
if (empty($extras['base_url'])) {
$errorContent = '机器人未启用。';
} elseif (in_array($this->client['platform'], ['win', 'mac', 'web'])
&& !Base::judgeClientVersion("0.29.11", $this->client['version'])) {
$errorContent = '当前客户端版本低所需版本≥v0.29.11)。';
}
break;
// Claude 机器人
case 'ai-claude@bot.system':
$setting = Base::setting('aibotSetting');
$webhookUrl = "{$serverUrl}/ai/claude/send";
$extras = [
'claude_token' => $setting['claude_token'],
'claude_agency' => $setting['claude_agency'],
'server_url' => $serverUrl,
];
if (empty($extras['claude_token'])) {
$errorContent = '机器人未启用。';
} elseif (in_array($this->client['platform'], ['win', 'mac', 'web'])
&& !Base::judgeClientVersion("0.29.11", $this->client['version'])) {
$errorContent = '当前客户端版本低所需版本≥v0.29.11)。';
if (empty($extras['api_key'])) {
$extras['api_key'] = Base::strRandom(6);
}
break;
// Wenxin 机器人
case 'ai-wenxin@bot.system':
$setting = Base::setting('aibotSetting');
$webhookUrl = "{$serverUrl}/ai/wenxin/send";
$extras = [
'wenxin_key' => $setting['wenxin_key'],
'wenxin_secret' => $setting['wenxin_secret'],
'wenxin_model' => $setting['wenxin_model'],
'server_url' => $serverUrl,
];
if (empty($extras['wenxin_key'])) {
$errorContent = '机器人未启用。';
} elseif (in_array($this->client['platform'], ['win', 'mac', 'web'])
&& !Base::judgeClientVersion("0.29.11", $this->client['version'])) {
$errorContent = '当前客户端版本低所需版本≥v0.29.12)。';
}
if (empty($extras['api_key'])) {
$errorContent = '机器人未启用。';
}
if (in_array($this->client['platform'], ['win', 'mac', 'web']) && !Base::judgeClientVersion("0.41.11", $this->client['version'])) {
$errorContent = '当前客户端版本低所需版本≥v0.41.11)。';
}
if ($msg->reply_id > 0) {
$replyCommand = $this->extractReplyCommand($msg->reply_id, $botUser);
if (Base::isError($replyCommand)) {
$errorContent = $replyCommand['msg'];
} else {
$command = <<<EOF
<quoted_content>
{$replyCommand['data']}
</quoted_content>
The content within the above quoted_content tags is a citation.
{$command}
EOF;
}
break;
// QianWen 机器人
case 'ai-qianwen@bot.system':
$setting = Base::setting('aibotSetting');
$webhookUrl = "{$serverUrl}/ai/qianwen/send";
$extras = [
'qianwen_key' => $setting['qianwen_key'],
'qianwen_model' => $setting['qianwen_model'],
'server_url' => $serverUrl,
];
if (empty($extras['qianwen_key'])) {
$errorContent = '机器人未启用。';
} elseif (in_array($this->client['platform'], ['win', 'mac', 'web'])
&& !Base::judgeClientVersion("0.29.11", $this->client['version'])) {
$errorContent = '当前客户端版本低所需版本≥v0.29.12)。';
}
$this->AIGenerateSystemMessage($msg->userid, $dialog, $extras);
$webhookUrl = "{$serverUrl}/ai/chat";
} else {
// 用户机器人
if ($botUser->isUserBot() && str_starts_with($command, '/')) {
// 用户机器人不处理指令类型命令
return;
}
if ($msg->reply_id > 0) {
$replyCommand = $this->extractReplyCommand($msg->reply_id, $botUser);
if (Base::isSuccess($replyCommand)) {
$replyText = $replyCommand['data'] ?: '';
}
break;
// Gemini 机器人
case 'ai-gemini@bot.system':
$setting = Base::setting('aibotSetting');
$webhookUrl = "{$serverUrl}/ai/gemini/send";
$extras = [
'gemini_key' => $setting['gemini_key'],
'gemini_model' => $setting['gemini_model'],
'gemini_agency' => $setting['gemini_agency'],
'gemini_timeout' => 20,
'server_url' => $serverUrl,
];
if (empty($extras['gemini_key'])) {
$errorContent = '机器人未启用。';
} elseif (in_array($this->client['platform'], ['win', 'mac', 'web'])
&& !Base::judgeClientVersion("0.29.11", $this->client['version'])) {
$errorContent = '当前客户端版本低所需版本≥v0.29.12)。';
}
break;
// 智谱清言 机器人
case 'ai-zhipu@bot.system':
$setting = Base::setting('aibotSetting');
$webhookUrl = "{$serverUrl}/ai/zhipu/send";
$extras = [
'zhipu_key' => $setting['zhipu_key'],
'zhipu_model' => $setting['zhipu_model'],
'server_url' => $serverUrl,
];
if (empty($extras['zhipu_key'])) {
$errorContent = '机器人未启用。';
} elseif (in_array($this->client['platform'], ['win', 'mac', 'web'])
&& !Base::judgeClientVersion("0.29.11", $this->client['version'])) {
$errorContent = '当前客户端版本低所需版本≥v0.29.12)。';
}
break;
// 其他机器人
default:
$userBot = UserBot::whereBotId($botUser->userid)->first();
$webhookUrl = $userBot?->webhook_url;
break;
}
$userBot = UserBot::whereBotId($botUser->userid)->first();
$webhookUrl = $userBot?->webhook_url;
}
if ($errorContent) {
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'template', [
@@ -531,6 +504,7 @@ class BotReceiveMsgTask extends AbstractTask
try {
$data = [
'text' => $command,
'reply_text' => $replyText,
'token' => User::generateToken($botUser),
'dialog_id' => $dialog->id,
'dialog_type' => $dialog->type,
@@ -541,7 +515,7 @@ class BotReceiveMsgTask extends AbstractTask
'version' => Base::getVersion(),
'extras' => Base::array2json($extras)
];
$res = Ihttp::ihttp_post($webhookUrl, $data);
$res = Ihttp::ihttp_post($webhookUrl, $data, 30);
if ($userBot) {
$userBot->webhook_num++;
$userBot->save();
@@ -563,11 +537,12 @@ class BotReceiveMsgTask extends AbstractTask
}
/**
* 获取机器人信息
* @param $botId
* @param $userid
* @return User
*/
private function botManagerOne($botId, $userid)
private function botOne($botId, $userid)
{
$botId = intval($botId);
$userid = intval($userid);
@@ -587,4 +562,224 @@ class BotReceiveMsgTask extends AbstractTask
}
return null;
}
/**
* 提取消息指令(提取消息内容)
* @param WebSocketDialogMsg $msg
* @param User $botUser
* @param bool $mention
* @return string
* @throws Exception
*/
private function extractCommand(WebSocketDialogMsg $msg, User $botUser, bool $mention = false)
{
if ($msg->type !== 'text') {
return '';
}
$original = $msg->msg['text'] ?: '';
if ($mention) {
$original = preg_replace("/<span class=\"mention user\" data-id=\"(\d+)\">(.*?)<\/span>/", "", $original);
}
if (preg_match("/<span[^>]*?data-quick-key=([\"'])([^\"']+?)\\1[^>]*?>(.*?)<\/span>/is", $original, $match)) {
$command = $match[2];
if (str_starts_with($command, '%3A.')) {
$command = ":" . substr($command, 4);
}
return $command;
}
if ($botUser->isAiBot()) {
// AI 机器人
$contents = [];
if (preg_match_all("/<span class=\"mention task\" data-id=\"(\d+)\">(.*?)<\/span>/", $original, $match)) {
// 任务
$taskIds = Base::newIntval($match[1]);
foreach ($taskIds as $index => $taskId) {
$taskInfo = ProjectTask::with(['content'])->whereId($taskId)->first();
if (!$taskInfo) {
throw new Exception("任务不存在或已被删除");
}
$taskName = addslashes($taskInfo->name) . " (ID:{$taskId})";
$taskContext = implode("\n", $taskInfo->AIContext());
$contents[] = "<task_content path=\"{$taskName}\">\n{$taskContext}\n</task_content>";
$original = str_replace($match[0][$index], "'{$taskName}' (see below for task_content tag)", $original);
}
}
if (preg_match_all("/<a class=\"mention ([^'\"]*)\" href=\"([^\"']+?)\"[^>]*?>[~%]([^>]*)<\/a>/", $original, $match)) {
// 文件、报告
$urlPaths = $match[2];
foreach ($urlPaths as $index => $urlPath) {
$pathTag = null;
$pathName = null;
$pathContent = null;
// 文件
if (preg_match("/single\/file\/(.*?)$/", $urlPath, $fileMatch)) {
$fileInfo = FileContent::idOrCodeToContent($fileMatch[1]);
if (!$fileInfo || !isset($fileInfo->content['url'])) {
throw new Exception("文件不存在或已被删除");
}
$urlPath = public_path($fileInfo->content['url']);
if (!file_exists($urlPath)) {
throw new Exception("文件不存在或已被删除");
}
$fileResult = TextExtractor::extractFile($urlPath);
if (Base::isError($fileResult)) {
throw new Exception("文件读取失败:" . $fileResult['msg']);
}
$pathTag = "file_content";
$pathName = addslashes($match[3][$index]) . " (ID:{$fileInfo->id})";
$pathContent = $fileResult['data'];
}
// 报告
elseif (preg_match("/single\/report\/detail\/(.*?)$/", $urlPath, $reportMatch)) {
$reportInfo = Report::idOrCodeToContent($reportMatch[1]);
if (!$reportInfo) {
throw new Exception("报告不存在或已被删除");
}
$pathTag = "report_content";
$pathName = addslashes($match[3][$index]) . " (ID:{$reportInfo->id})";
$pathContent = $reportInfo->content;
}
if ($pathTag) {
$contents[] = "<{$pathTag} path=\"{$pathName}\">\n{$pathContent}\n</{$pathTag}>";
$original = str_replace($match[0][$index], "'{$pathName}' (see below for {$pathTag} tag)", $original);
}
}
}
$original = Base::html2markdown($original);
if ($contents) {
// 添加tag内容
$original .= "\n\n" . implode("\n\n", $contents);
}
return $original;
} elseif ($botUser->isUserBot()) {
// 用户机器人
return Base::html2markdown($original);
} else {
// 其他机器人(系统)
return trim(strip_tags($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;
}
}
return Base::retSuccess('success', $replyCommand);
}
/**
* 生成AI系统提示词
* @param int|null $userid
* @param WebSocketDialog $dialog
* @param array $extras
* @return void
*/
private function AIGenerateSystemMessage(int|null $userid, WebSocketDialog $dialog, array &$extras)
{
$system_messages = [];
switch ($dialog->type) {
case "user":
$aiPrompt = WebSocketDialogConfig::where([
'dialog_id' => $dialog->id,
'userid' => $userid,
'type' => 'ai_prompt',
])->value('value');
if ($aiPrompt) {
$extras['system_message'] = $aiPrompt;
}
break;
case "group":
switch ($dialog->group_type) {
case 'user':
break;
case 'project':
$projectInfo = Project::whereDialogId($dialog->id)->first();
if ($projectInfo) {
$projectDesc = $projectInfo->desc ?: "-";
$projectStatus = $projectInfo->archived_at ? '已归档' : '正在进行中';
$system_messages[] = <<<EOF
当前我在项目【{$projectInfo->name}】中
项目描述:{$projectDesc}
项目状态:{$projectStatus}
如果你判断我想要或需要添加任务,请按照以下格式回复:
::: create-task-list
title: 任务标题1
desc: 任务描述1
title: 任务标题2
desc: 任务描述2
:::
EOF;
}
break;
case 'task':
$taskInfo = ProjectTask::with(['content'])->whereDialogId($dialog->id)->first();
if ($taskInfo) {
$taskContext = implode("\n", $taskInfo->AIContext());
$system_messages[] = <<<EOF
当前我在任务【{$taskInfo->name}】中
当前时间:{$taskInfo->updated_at}
任务ID{$taskInfo->id}
{$taskContext}
如果你判断我想要或需要添加子任务,请按照以下格式回复:
::: create-subtask-list
title: 子任务标题1
title: 子任务标题2
:::
EOF;
}
break;
case 'department':
$userDepartment = UserDepartment::whereDialogId($dialog->id)->first();
if ($userDepartment) {
$system_messages[] = "当前我在【{$userDepartment->name}】的部门群聊中";
}
break;
case 'all':
$system_messages[] = "当前我在【全体成员】的群聊中";
break;
}
break;
}
if ($extras['system_message']) {
array_unshift($system_messages, $extras['system_message']);
}
if ($system_messages) {
$extras['system_message'] = implode("\n\n====\n\n", Base::newTrim($system_messages));
}
}
}

View File

@@ -2,12 +2,12 @@
namespace App\Tasks;
use App\Models\ApproveProcInstHistory;
use App\Models\User;
use App\Models\UserCheckinRecord;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Module\Base;
use App\Module\Doo;
use App\Module\Extranet;
use App\Module\Timer;
use Cache;
@@ -82,6 +82,9 @@ class CheckinRemindTask extends AbstractTask
if (!UserCheckinRecord::whereUserid($user->userid)->where('created_at', '>', Carbon::now()->subDays(3))->exists()) {
continue; // 3天内没有打卡
}
if (ApproveProcInstHistory::userIsLeave($user->userid)) {
continue; // 请假、外出
}
$dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid);
if ($dialog) {
if ($type === 'exceed') {

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Tasks;
use Carbon\Carbon;
use Illuminate\Support\Facades\Cache;
/**
* 同步聊天数据到Elasticsearch
*/
class ElasticSearchSyncTask extends AbstractTask
{
public function __construct()
{
parent::__construct();
}
public function start()
{
// 120分钟执行一次
$time = intval(Cache::get("ElasticSearchSyncTask:Time"));
if (time() - $time < 120 * 60) {
return;
}
// 执行开始120分钟后缓存标记失效
Cache::put("ElasticSearchSyncTask:Time", time(), Carbon::now()->addMinutes(120));
// 开始执行同步
@shell_exec("php /var/www/artisan elasticsearch:sync-dialog-user-msg --i");
// 执行完成5分钟后缓存标记失效5分钟任务可重复执行
Cache::put("ElasticSearchSyncTask:Time", time(), Carbon::now()->addMinutes(5));
}
public function end()
{
}
}

View File

@@ -1,7 +1,8 @@
<?php
namespace App\Tasks;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Models\Setting;
use App\Models\User;
use App\Models\WebSocketDialogMsg;
@@ -106,9 +107,9 @@ class EmailNoticeTask extends AbstractTask
->groupBy('web_socket_dialog_msg_reads.userid');
// 分批处理用户的未读消息
$query->chunk(self::CHUNK_SIZE, function($users) use ($dialogType, $startTime, $endTime) {
$query->chunk(self::CHUNK_SIZE, function($users) use ($dialogType) {
foreach ($users as $userData) {
$this->sendUserEmail($userData->userid, $dialogType, $startTime, $endTime);
$this->sendUserEmail($userData->userid, $dialogType);
}
});
@@ -121,7 +122,7 @@ class EmailNoticeTask extends AbstractTask
/**
* 发送用户的未读消息邮件
*/
private function sendUserEmail(int $userId, string $dialogType, Carbon $startTime, Carbon $endTime): void
private function sendUserEmail(int $userId, string $dialogType): void
{
// 验证用户
$user = User::whereDisableAt(null)->find($userId);
@@ -130,7 +131,7 @@ class EmailNoticeTask extends AbstractTask
}
// 获取未读消息
$messages = $this->getUnreadMessages($userId, $dialogType, $startTime, $endTime);
$messages = $this->getUnreadMessages($userId, $dialogType);
if ($messages->isEmpty()) {
return;
}
@@ -156,7 +157,7 @@ class EmailNoticeTask extends AbstractTask
/**
* 获取用户的未读消息
*/
private function getUnreadMessages($userId, $dialogType, Carbon $startTime, Carbon $endTime)
private function getUnreadMessages($userId, $dialogType)
{
return WebSocketDialogMsg::select([
'web_socket_dialog_msgs.*',
@@ -171,7 +172,6 @@ class EmailNoticeTask extends AbstractTask
'web_socket_dialog_msgs.dialog_type' => $dialogType
])
->whereNull('r.read_at')
->whereBetween('web_socket_dialog_msgs.created_at', [$startTime, $endTime])
->whereIn('web_socket_dialog_msgs.type', self::ALLOWED_MSG_TYPES)
->orderBy('web_socket_dialog_msgs.created_at')
->limit(self::CHUNK_SIZE)

View File

@@ -119,14 +119,20 @@ class WebSocketDialogMsgTask extends AbstractTask
$mention = array_intersect([0, $userid], $mentions) ? 1 : 0;
$silence = $mention ? false : $silence;
$dot = $msg->type === 'record' ? 1 : 0;
WebSocketDialogMsgRead::createInstance([
$msgRead = WebSocketDialogMsgRead::createInstance([
'dialog_id' => $msg->dialog_id,
'msg_id' => $msg->id,
'userid' => $userid,
'mention' => $mention,
'silence' => $silence,
'dot' => $dot,
])->saveOrIgnore();
]);
if ($msgRead->saveOrIgnore()) {
if ($dialog->session_id && $dialog->session_id != $msg->session_id) {
$msgRead->read_at = Carbon::now();
$msgRead->save();
}
}
$array[$userid] = [
'userid' => $userid,
'mention' => $mention,
@@ -137,7 +143,7 @@ class WebSocketDialogMsgTask extends AbstractTask
// 机器人收到消处理
$botUser = User::whereUserid($userid)->whereBot(1)->first();
if ($botUser) {
$this->endArray[] = new BotReceiveMsgTask($botUser->userid, $msg->id, $mention, $this->client);
$this->endArray[] = new BotReceiveMsgTask($botUser->userid, $msg->id, $mentions, $this->client);
}
}
}

View File

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

4
bin/version.js vendored

File diff suppressed because one or more lines are too long

122
cmd
View File

@@ -156,9 +156,9 @@ run_electron() {
npm install
fi
if [ ! -d "./electron/node_modules" ]; then
pushd electron
pushd electron || exit
npm install
popd
popd || exit
fi
#
if [ -d "./electron/dist" ]; then
@@ -178,8 +178,9 @@ run_electron() {
run_exec() {
local container=$1
local cmd=$2
local name=`docker_name $container`
shift 1
local cmd=$@
local name=$(docker_name "$container")
if [ -z "$name" ]; then
error "没有找到 $container 容器!"
exit 1
@@ -322,15 +323,26 @@ https_auto() {
if [[ "$restart_nginx" == "y" ]]; then
$COMPOSE up -d
fi
docker run -it --rm -v $(pwd):/work nginx:alpine sh "/work/bin/https"
docker run -it --rm -v $(pwd):/work nginx:alpine sh /work/bin/https install
if [[ 0 -eq $? ]]; then
run_exec nginx "nginx -s reload"
fi
new_job="* 6 * * * docker run -it --rm -v $(pwd):/work nginx:alpine sh /work/bin/https renew"
current_crontab=$(crontab -l 2>/dev/null)
if ! echo "$current_crontab" | grep -v "https renew"; then
echo "任务已存在,无需添加。"
else
crontab -l |{
cat
echo "$new_job"
} | crontab -
echo "任务已添加。"
fi
}
env_get() {
local key=$1
local value=`cat ${cur_path}/.env | grep "^$key=" | awk -F '=' '{print $2}'`
local value=`cat ${cur_path}/.env | grep "^$key=" | awk -F '=' '{print $2}' | tr -d '\r\n'`
echo "$value"
}
@@ -405,14 +417,53 @@ if [ $# -gt 0 ]; then
rm -rf vendor
rm -rf composer.lock
fi
mkdir -p "${cur_path}/docker/log/supervisor"
mkdir -p "${cur_path}/docker/mysql/data"
chmod -R 775 "${cur_path}/docker/log/supervisor"
chmod -R 775 "${cur_path}/docker/mysql/data"
# 目录权限
volumes=(
"docker/log/supervisor"
"docker/mysql/data"
"docker/office/logs"
"docker/office/data"
"docker/es/data"
)
cmda=""
cmdb=""
for vol in "${volumes[@]}"; do
tmp_path="${cur_path}/${vol}"
mkdir -p "${tmp_path}"
chmod -R 775 "${tmp_path}"
rm -f "${tmp_path}/dootask.lock"
cmda="${cmda} -v ${tmp_path}:/usr/share/${vol}"
cmdb="${cmdb} touch /usr/share/${vol}/dootask.lock &&"
done
# 目录权限检测
remaining=10
while true; do
((remaining=$remaining-1))
writable="yes"
docker run --rm ${cmda} nginx:alpine sh -c "${cmdb} touch /usr/share/docker/dootask.lock" &> /dev/null
for vol in "${volumes[@]}"; do
if [ ! -f "${vol}/dootask.lock" ]; then
if [ $remaining -lt 0 ]; then
error "目录【${vol}】权限不足!"
exit 1
else
writable="no"
break
fi
fi
done
if [ "$writable" == "yes" ]; then
break
else
sleep 3
fi
done
# 设置ES索引后缀
env_set ES_INDEX_SUFFIX "$(rand_string 6)"
# 启动容器
[[ "$(arg_get port)" -gt 0 ]] && env_set APP_PORT "$(arg_get port)"
$COMPOSE up php -d
# 安装composer依赖
# 安装PHP依赖
run_exec php "composer install"
if [ ! -f "${cur_path}/vendor/autoload.php" ]; then
run_exec php "composer config repo.packagist composer https://packagist.phpcomposer.com"
@@ -424,45 +475,32 @@ if [ $# -gt 0 ]; then
exit 1
fi
[[ -z "$(env_get APP_KEY)" ]] && run_exec php "php artisan key:generate"
# 设置生产模式
switch_debug "false"
# 检查数据库
remaining=20
while [ ! -f "${cur_path}/docker/mysql/data/$(env_get DB_DATABASE)/db.opt" ]; do
((remaining=$remaining-1))
if [ $remaining -lt 0 ]; then
error "数据库初始化失败!"
exit 1
fi
chmod -R 775 "${cur_path}/docker/mysql/data"
done
# 数据库迁移
remaining=20
while [ ! -f "${cur_path}/docker/mysql/data/$(env_get DB_DATABASE)/$(env_get DB_PREFIX)migrations.ibd" ]; do
((remaining=$remaining-1))
if [ $remaining -lt 0 ]; then
error "数据库安装失败!"
exit 1
fi
sleep 3
run_exec php "php artisan migrate --seed"
done
# 设置初始化密码
res=`run_exec mariadb "sh /etc/mysql/repassword.sh"`
run_exec php "php artisan migrate --seed"
# 启动其他容器
$COMPOSE up -d
restart_php
success "安装完成"
info "地址: http://${GreenBG}127.0.0.1:$(env_get APP_PORT)${Font}"
info "$res"
# 设置初始化密码
run_exec mariadb "sh /etc/mysql/repassword.sh"
elif [[ "$1" == "update" ]]; then
shift 1
if [[ "$@" != "nobackup" ]]; then
run_mysql backup
fi
if [[ -z "$(arg_get local)" ]]; then
git fetch --all
git reset --hard origin/$(git branch | sed -n -e 's/^\* \(.*\)/\1/p')
current_branch=$(git branch | sed -n -e 's/^\* \(.*\)/\1/p')
db_changes=$(git diff --name-only HEAD..origin/$current_branch | grep -E "^database/")
if [[ -n "$db_changes" ]]; then
info "数据库有迁移变动,执行数据库备份..."
run_mysql backup
fi
git reset --hard origin/$current_branch
git pull
run_exec php "composer update"
else
info "执行数据库备份..."
run_mysql backup
fi
run_exec php "php artisan migrate"
run_exec nginx "nginx -s reload"
@@ -513,7 +551,7 @@ if [ $# -gt 0 ]; then
success "修改成功"
elif [[ "$1" == "repassword" ]]; then
shift 1
run_exec mariadb "sh /etc/mysql/repassword.sh \"$@\""
run_exec mariadb "sh /etc/mysql/repassword.sh $@"
elif [[ "$1" == "serve" ]] || [[ "$1" == "dev" ]] || [[ "$1" == "development" ]]; then
shift 1
run_compile dev
@@ -539,9 +577,9 @@ if [ $# -gt 0 ]; then
elif [[ "$1" == "npm" ]]; then
shift 1
npm $@
cd electron
pushd electron || exit
npm $@
cd ..
popd || exit
docker run --rm -it -v ${cur_path}/resources/mobile:/work -w /work --entrypoint=/bin/bash node:16 -c "npm $@"
elif [[ "$1" == "doc" ]]; then
shift 1

View File

@@ -10,6 +10,8 @@
"require": {
"php": "^8.0",
"ext-curl": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-gd": "*",
"ext-imagick": "*",
"ext-json": "*",
@@ -18,6 +20,7 @@
"ext-simplexml": "*",
"ext-zip": "*",
"directorytree/ldaprecord-laravel": "^2.7",
"elasticsearch/elasticsearch": "^8.17",
"fideloper/proxy": "^4.4.1",
"firebase/php-jwt": "^6.9",
"fruitcake/laravel-cors": "^2.0.4",
@@ -28,12 +31,16 @@
"laravel/tinker": "^v2.6.1",
"laravolt/avatar": "^5.1",
"league/commonmark": "^2.5",
"league/html-to-markdown": "^5.1",
"maatwebsite/excel": "^3.1.31",
"madnest/madzipper": "^v1.1.0",
"mews/captcha": "^3.2.6",
"orangehill/iseed": "^3.0.1",
"overtrue/pinyin": "^4.0",
"phpoffice/phppresentation": "^1.1",
"phpoffice/phpword": "^1.3",
"predis/predis": "^1.1.7",
"smalot/pdfparser": "^2.11",
"symfony/mailer": "^6.0"
},
"require-dev": {
@@ -82,7 +89,10 @@
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true
"sort-packages": true,
"allow-plugins": {
"php-http/discovery": true
}
},
"minimum-stability": "dev",
"prefer-stable": true,

1835
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddArchiveFieldsToPreProjectsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('projects', function (Blueprint $table) {
if (!Schema::hasColumn('projects', 'archive_method')) {
$table->after('personal', function ($table) {
$table->string('archive_method', 20)->nullable()->default('system')->comment('自动归档方式');
$table->integer('archive_days')->nullable()->default(30)->comment('自动归档天数');
});
$table->index('archive_method');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('projects', function (Blueprint $table) {
$table->dropIndex('idx_projects_archive_method');
$table->dropColumn([
'archive_method',
'archive_days'
]);
});
}
}

View File

@@ -0,0 +1,44 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateProjectTaskTemplatesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (!Schema::hasTable('project_task_templates')) {
Schema::create('project_task_templates', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('project_id')->index()->comment('项目ID');
$table->string('name', 100)->comment('模板名称');
$table->string('title', 255)->nullable()->comment('任务标题');
$table->text('content')->nullable()->comment('任务内容');
$table->unsignedTinyInteger('sort')->default(0)->comment('排序');
$table->boolean('is_default')->default(false)->comment('是否默认模板');
$table->unsignedBigInteger('userid')->index()->comment('创建人');
$table->timestamps();
// 外键约束
$table->foreign('project_id')->references('id')->on('projects')->onDelete('cascade');
$table->foreign('userid')->references('userid')->on('users');
});
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('project_task_templates');
}
}

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateWebSocketDialogConfigsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (!Schema::hasTable('web_socket_dialog_configs')) {
Schema::create('web_socket_dialog_configs', function (Blueprint $table) {
$table->id();
$table->bigInteger('dialog_id')->unsigned()->index()->comment('对话ID');
$table->bigInteger('userid')->unsigned()->index()->comment('用户ID');
$table->string('type', 50)->default('')->comment('配置类型');
$table->text('value')->nullable()->comment('配置值');
$table->timestamps();
});
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('web_socket_dialog_configs');
}
}

View File

@@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateProjectTagsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasTable('project_tags')) {
return;
}
Schema::create('project_tags', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('project_id')->index()->comment('项目ID');
$table->string('name', 50)->comment('标签名称');
$table->string('desc', 255)->nullable()->comment('标签描述');
$table->string('color', 20)->nullable()->default('')->comment('颜色');
$table->unsignedBigInteger('userid')->index()->comment('创建人');
$table->timestamps();
// 外键约束
$table->foreign('project_id')->references('id')->on('projects')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('project_tags');
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddVersionToUmengAlias extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('umeng_alias', function (Blueprint $table) {
if (!Schema::hasColumn('umeng_alias', 'version')) {
$table->string('version', 50)->nullable()->default('')->after('device')->comment('应用版本号');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('umeng_alias', function (Blueprint $table) {
if (Schema::hasColumn('umeng_alias', 'version')) {
$table->dropColumn('version');
}
});
}
}

View File

@@ -0,0 +1,38 @@
<?php
use Carbon\Carbon;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
class UpdateProjectTasksSubtaskProjectIdAndColumnId extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$prefix = DB::getTablePrefix();
$now = Carbon::now();
DB::statement("
UPDATE {$prefix}project_tasks AS subtask
INNER JOIN {$prefix}project_tasks AS parent ON subtask.parent_id = parent.id
SET
subtask.project_id = parent.project_id,
subtask.column_id = parent.column_id,
subtask.updated_at = '{$now}'
WHERE subtask.parent_id > 0
");
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
// No need for down operation as this is a data correction
}
}

View File

@@ -0,0 +1,37 @@
<?php
use Carbon\Carbon;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
class UpdateProjectTasksSubtaskVisibility extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$prefix = DB::getTablePrefix();
$now = Carbon::now();
DB::statement("
UPDATE {$prefix}project_tasks AS subtask
INNER JOIN {$prefix}project_tasks AS parent ON subtask.parent_id = parent.id
SET
subtask.visibility = parent.visibility,
subtask.updated_at = '{$now}'
WHERE subtask.parent_id > 0
");
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
// No need for down operation as this is a data correction
}
}

View File

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

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class WebSocketDialogMsgsAddSessionId extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
if (!Schema::hasColumn('web_socket_dialog_msgs', 'session_id')) {
$table->bigInteger('session_id')->index()->nullable()->default(0)->after('dialog_type')->comment('会话ID');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
$table->dropColumn('session_id');
});
}
}

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class WebSocketDialogsAddSessionId extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('web_socket_dialogs', function (Blueprint $table) {
if (!Schema::hasColumn('web_socket_dialogs', 'session_id')) {
$table->bigInteger('session_id')->index()->nullable()->default(0)->after('group_type')->comment('会话ID');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('web_socket_dialogs', function (Blueprint $table) {
$table->dropColumn('session_id');
});
}
}

View File

@@ -0,0 +1,58 @@
<?php
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialogSession;
use App\Module\Base;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateWebSocketDialogSessionsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasTable('web_socket_dialog_sessions')) {
return;
}
Schema::create('web_socket_dialog_sessions', function (Blueprint $table) {
$table->id();
$table->bigInteger('dialog_id')->unsigned()->index()->comment('对话ID');
$table->string('title', 255)->default('')->comment('会话标题');
$table->timestamps();
});
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.email'])
->join('web_socket_dialog_users as du', 'web_socket_dialogs.id', '=', 'du.dialog_id')
->join('users as u', 'du.userid', '=', 'u.userid')
->where('u.email', 'like', 'ai-%@bot.system')
->where('web_socket_dialogs.type', 'user')
->get();
foreach ($list as $item) {
$title = WebSocketDialogMsg::whereDialogId($item->id)->where('key', '!=', '')->orderBy('id')->value('key');
$session = WebSocketDialogSession::createInstance([
'dialog_id' => $item->id,
'title' => $title ? Base::cutStr($title, 100) : 'Unknown',
'created_at' => $item->created_at,
]);
$session->save();
$item->session_id = $session->id;
$item->save();
WebSocketDialogMsg::whereDialogId($item->id)->update(['session_id' => $session->id]);
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('web_socket_dialog_sessions');
}
}

View File

@@ -0,0 +1,50 @@
<?php
use App\Models\Setting;
use App\Module\Base;
use Illuminate\Database\Migrations\Migration;
class UpdateAiModelsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$row = Setting::whereName('aibotSetting')->first();
if (empty($row)) {
return;
}
$value = Base::json2array($row->getRawOriginal('setting'));
foreach ($value as $key => $item) {
if (str_ends_with($key, '_models')) {
$value[$key] = preg_replace('/\s*:\s*/', ' | ', $item);
}
}
$row->setting = Base::array2json($value);
$row->save();
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
$row = Setting::whereName('aibotSetting')->first();
if (empty($row)) {
return;
}
$value = Base::json2array($row->getRawOriginal('setting'));
foreach ($value as $key => $item) {
if (str_ends_with($key, '_models')) {
$value[$key] = preg_replace('/\s*\|\s*/', ': ', $item);
}
}
$row->setting = Base::array2json($value);
$row->save();
}
}

View File

@@ -0,0 +1,54 @@
<?php
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialogSession;
use App\Module\Base;
use Illuminate\Database\Migrations\Migration;
class UpdateWebSocketDialogMsgsSessionId extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.email'])
->join('web_socket_dialog_users as du', 'web_socket_dialogs.id', '=', 'du.dialog_id')
->join('users as u', 'du.userid', '=', 'u.userid')
->where('u.email', 'like', 'ai-%@bot.system')
->where('web_socket_dialogs.type', 'user')
->get();
foreach ($list as $item) {
$msg = WebSocketDialogMsg::whereDialogId($item->id)->whereSessionId(0)->orderBy('id')->first();
if ($msg || empty($item->session_id)) {
$title = $msg?->key;
$session = WebSocketDialogSession::createInstance([
'dialog_id' => $item->id,
'title' => $title ? Base::cutStr($title, 100) : 'Unknown',
'created_at' => $item->created_at,
]);
$session->save();
if (empty($item->session_id)) {
$item->session_id = $session->id;
$item->save();
}
if ($msg) {
WebSocketDialogMsg::whereDialogId($item->id)->whereSessionId(0)->update(['session_id' => $session->id]);
}
}
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateReportLinksTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('report_links', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('rid')->nullable()->default(0)->index()->comment('报告ID');
$table->integer('num')->nullable()->default(0)->comment('累计访问');
$table->string('code')->nullable()->default('')->comment('链接码');
$table->bigInteger('userid')->nullable()->default(0)->index()->comment('会员ID');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('report_links');
}
}

View File

@@ -2,7 +2,7 @@ services:
php:
container_name: "dootask-php-${APP_ID}"
image: "kuaifan/php:swoole-8.0.rc18"
shm_size: "2gb"
shm_size: 2G
ulimits:
core:
soft: 0
@@ -25,8 +25,10 @@ services:
extnetwork:
ipv4_address: "${APP_IPPR}.2"
depends_on:
- redis
- mariadb
mariadb:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
nginx:
@@ -41,20 +43,16 @@ services:
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.3"
links:
- php
- office
- fileview
- drawio-webapp
- drawio-export
- minder
- okr
- ai
restart: unless-stopped
redis:
container_name: "dootask-redis-${APP_ID}"
image: "redis:alpine"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.4"
@@ -68,12 +66,16 @@ services:
- ./docker/mysql/conf.d:/etc/mysql/conf.d
- ./docker/mysql/data:/var/lib/mysql
environment:
TZ: "${TIMEZONE:-PRC}"
MYSQL_PREFIX: "${DB_PREFIX}"
MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}"
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"
@@ -81,11 +83,13 @@ services:
office:
container_name: "dootask-office-${APP_ID}"
image: "onlyoffice/documentserver:8.2.1.1"
image: "onlyoffice/documentserver:8.2.2.1"
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
@@ -101,7 +105,7 @@ services:
fileview:
container_name: "dootask-fileview-${APP_ID}"
image: "kuaifan/fileview:4.2.0-SNAPSHOT-RC25"
image: "kuaifan/fileview:4.4.0-3"
environment:
KK_CONTEXT_PATH: "/fileview"
KK_OFFICE_PREVIEW_SWITCH_DISABLED: true
@@ -114,7 +118,7 @@ services:
drawio-webapp:
container_name: "dootask-drawio-webapp-${APP_ID}"
image: "jgraph/drawio:20.8.20"
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
@@ -124,8 +128,6 @@ services:
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.8"
depends_on:
- drawio-export
restart: unless-stopped
drawio-export:
@@ -162,13 +164,15 @@ services:
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.11"
depends_on:
- mariadb
restart: unless-stopped
ai:
container_name: "dootask-ai-${APP_ID}"
image: "kuaifan/dooai:0.2.1"
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"
@@ -179,7 +183,7 @@ services:
image: "kuaifan/doookr:0.4.5"
environment:
TZ: "${TIMEZONE:-PRC}"
DOO_TASK_URL: "http://${APP_IPPR}.3"
DOO_TASK_URL: "http://nginx"
MYSQL_HOST: "${DB_HOST}"
MYSQL_PORT: "${DB_PORT}"
MYSQL_DBNAME: "${DB_DATABASE}"
@@ -191,8 +195,6 @@ services:
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.13"
depends_on:
- mariadb
restart: unless-stopped
face:
@@ -209,14 +211,27 @@ services:
MYSQL_PASSWORD: "${DB_PASSWORD}"
MYSQL_DB_NAME: "${DB_DATABASE}"
DB_PREFIX: "${DB_PREFIX}"
REPORT_API: "http://${APP_IPPR}.3:80/api/public/checkin/report"
depends_on:
- mariadb
REPORT_API: "http://nginx/api/public/checkin/report"
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.14"
restart: unless-stopped
es:
container_name: "dootask-es-${APP_ID}"
image: "elasticsearch:8.17.2"
volumes:
- ./docker/es/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
- ./docker/es/data:/usr/share/elasticsearch/data
environment:
discovery.type: single-node
xpack.security.enabled: false
ES_JAVA_OPTS: "-Xms1g -Xmx1g"
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.15"
restart: unless-stopped
networks:
extnetwork:
name: "dootask-networks-${APP_ID}"

View File

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

View File

@@ -8,23 +8,23 @@
<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
<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 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)
<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://desk.draw.io/support/solutions/articles/16000042546-what-url-parameters-are-supported
* https://www.drawio.com/doc/faq/supported-url-parameters
*
* Parameters for developers:
*
@@ -44,27 +44,27 @@
{
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;
@@ -75,9 +75,10 @@
// ignore
}
}
// Global variable for desktop
var mxIsElectron = window && window.process && window.process.type;
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')
@@ -85,21 +86,21 @@
(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)
{
@@ -108,7 +109,7 @@
}
})();
}
/**
* Adds meta tag to the page.
*/
@@ -117,15 +118,15 @@
try
{
var s = document.createElement('meta');
if (name != null)
if (name != null)
{
s.setAttribute('name', name);
}
s.setAttribute('content', content);
if (httpEquiv != null)
if (httpEquiv != null)
{
s.setAttribute('http-equiv', httpEquiv);
}
@@ -138,14 +139,14 @@
// 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)
{
@@ -158,16 +159,16 @@
{
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'))
@@ -185,9 +186,9 @@
onError('Failed to load ' + src, e);
};
}
var t = document.getElementsByTagName('script')[0];
if (t != null)
{
t.parentNode.insertBefore(s, t);
@@ -209,11 +210,11 @@
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)
*/
@@ -225,13 +226,13 @@
if (mxIsElectron)
{
// mxmeta(null, 'default-src \'self\' \'unsafe-inline\'; connect-src \'self\' https://*.draw.io https://fonts.googleapis.com https://fonts.gstatic.com; img-src * data:; media-src *; font-src *; style-src-elem \'self\' \'unsafe-inline\' https://fonts.googleapis.com', 'Content-Security-Policy');
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';
@@ -242,34 +243,30 @@
}
var mxScriptsLoaded = false, mxWinLoaded = false;
function checkAllLoaded()
{
if (mxScriptsLoaded && mxWinLoaded)
{
App.main();
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 = document.location.protocol + '//devhost.jgraph.com/drawio/src/main';
var mxDevUrl = '';
// Used to request draw.io sources in dev mode
var drawDevUrl = document.location.protocol + '//devhost.jgraph.com/drawio/src/main/webapp/';
var geBasePath = drawDevUrl + '/js/grapheditor';
var mxBasePath = mxDevUrl + '/mxgraph';
var drawDevUrl = '';
var geBasePath = 'js/grapheditor';
var mxBasePath = 'mxgraph/src';
if (document.location.protocol == 'file:')
{
geBasePath = './js/grapheditor';
mxBasePath = './mxgraph';
drawDevUrl = './';
// Forces includes for dev environment in node.js
mxForceIncludes = true;
}
@@ -280,19 +277,19 @@
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
@@ -300,18 +297,18 @@
(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)
{
@@ -332,14 +329,14 @@
});
});
}
else if (!supportedDomain)
else if (!supportedDomain || navigator.onLine)
{
mxscript('js/PostConfig.js');
}
});
};
if (!supportedDomain || mxIsElectron)
if (!supportedDomain || mxIsElectron || navigator.onLine)
{
mxscript('js/PreConfig.js', loadAppJS);
}
@@ -354,19 +351,17 @@
window.onerror = function()
{
var status = document.getElementById('geStatus');
if (status != null)
{
status.innerHTML = 'Page could not be loaded. Please try refreshing.';
}
};
</script>
<link rel="chrome-webstore-item" href="https://chrome.google.com/webstore/detail/plgmlhohecdddhbmmkncjdmlhcmaachm">
<link rel="apple-touch-icon" sizes="180x180" href="images/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="images/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="images/favicon-16x16.png">
<link rel="mask-icon" href="images/safari-pinned-tab.svg" color="#d89000">
<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">
@@ -378,7 +373,7 @@
color:#606060;
}
.geBlock {
display: none;
display: none;
z-index:-3;
margin:100px;
margin-top:40px;
@@ -443,7 +438,7 @@
<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,
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>
@@ -452,7 +447,7 @@
</p>
</div>
</div>
<script type="text/javascript">
<script id="geMain" type="text/javascript">
/**
* Main
*/

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

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

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

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

View File

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

View File

@@ -1,27 +1,99 @@
#!/bin/sh
#
# 重置用户密码脚本
#
# 使用方法:
# ./repassword.sh [账号标识符] [自定义密码]
#
# 参数说明:
# [账号标识符]: 可选可以是用户ID(纯数字)或邮箱地址。不提供时默认为第一个管理员用户
# [自定义密码]: 可选,指定要设置的新密码。不提供时会自动生成随机密码
#
# 使用示例:
# ./repassword.sh # 重置第一个管理员用户密码(随机生成)
# ./repassword.sh 123 # 重置ID=123的用户密码(随机生成)
# ./repassword.sh user@example.com # 重置邮箱为user@example.com的用户密码(随机生成)
# ./repassword.sh 123 newpass # 重置ID=123的用户密码为"newpass"
# ./repassword.sh user@example.com newpass # 重置邮箱为user@example.com的用户密码为"newpass"
#
new_password=$1
account_identifier=$1
custom_password=$2
GreenBG="\033[42;37m"
RedBG="\033[41;37m"
Font="\033[0m"
# 生成随机密码
new_encrypt=$(date +%s%N | md5sum | awk '{print $1}' | cut -c 1-6)
if [ -z "$new_password" ]; then
if [ -z "$custom_password" ]; then
new_password=$(date +%s%N | md5sum | awk '{print $1}' | cut -c 1-16)
else
new_password=$custom_password
fi
md5_password=$(echo -n `echo -n $new_password | md5sum | awk '{print $1}'`$new_encrypt | md5sum | awk '{print $1}')
content=$(echo "select \`email\` from ${MYSQL_PREFIX}users where \`userid\`=1;" | mysql -u$MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE)
account=$(echo "$content" | sed -n '2p')
# 构建查询条件
if [ -z "$account_identifier" ]; then
# 默认查询第一个管理员
admin_query=$(echo "SELECT userid FROM ${MYSQL_PREFIX}users WHERE identity LIKE '%,admin,%' ORDER BY userid LIMIT 1;" | mysql -u$MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE)
identifier_value=$(echo "$admin_query" | sed -n '2p')
if [ -z "$identifier_value" ]; then
echo "${RedBG}错误:未找到管理员用户!${Font}"
exit 1
fi
where_field="userid"
identifier_type="管理员ID"
else
# 检查是否为纯数字ID
# 使用更兼容的 shell 语法检查是否为纯数字
case "$account_identifier" in
''|*[!0-9]*)
# 非纯数字,视为邮箱
where_field="email"
identifier_type="邮箱"
identifier_value="$account_identifier"
;;
*)
# 纯数字视为ID
where_field="userid"
identifier_type="ID"
identifier_value="$account_identifier"
;;
esac
fi
# 构建 WHERE 条件(为邮箱添加引号)
if [ "$where_field" = "email" ]; then
where_condition="where $where_field='$identifier_value'"
else
where_condition="where $where_field=$identifier_value"
fi
# 查询用户信息
content=$(echo "select userid,email from ${MYSQL_PREFIX}users $where_condition;" | mysql -u$MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE)
# 提取用户ID和邮箱
user_id=$(echo "$content" | sed -n '2p' | awk '{print $1}')
account=$(echo "$content" | sed -n '2p' | awk '{print $2}')
if [ -z "$account" ]; then
echo "错误:账号不存在!"
echo "${RedBG}错误:${identifier_type} '${identifier_value}' 的账号不存在!${Font}"
exit 1
fi
# 更新密码
mysql -u$MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE <<EOF
update ${MYSQL_PREFIX}users set \`encrypt\`='${new_encrypt}',\`password\`='${md5_password}' where \`userid\`=1;
update ${MYSQL_PREFIX}users set encrypt='${new_encrypt}',password='${md5_password}' $where_condition;
EOF
echo "账号: ${GreenBG}${account}${Font}"
# 只在 identifier_type="ID" 时才输出ID
if [ "$identifier_type" = "ID" ]; then
echo "ID: ${GreenBG}${user_id}${Font}"
fi
# 输出邮箱和密码
echo "邮箱: ${GreenBG}${account}${Font}"
echo "密码: ${GreenBG}${new_password}${Font}"

View File

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

View File

@@ -78,125 +78,7 @@ server {
proxy_pass http://service;
}
location /fileview {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-Host $the_host;
proxy_set_header X-Forwarded-Proto $the_scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header Server-Protocol $server_protocol;
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
proxy_pass http://fileview:8012;
}
location /office/ {
proxy_http_version 1.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-Host $the_host/office;
proxy_set_header X-Forwarded-Proto $the_scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header Server-Protocol $server_protocol;
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 3600s;
proxy_pass http://office/;
}
location /drawio/webapp/ {
proxy_http_version 1.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-Host $the_host/drawio/webapp;
proxy_set_header X-Forwarded-Proto $the_scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header Server-Protocol $server_protocol;
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://drawio-webapp:8080/;
}
location /drawio/export/ {
proxy_http_version 1.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-Host $the_host/drawio/export;
proxy_set_header X-Forwarded-Proto $the_scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header Server-Protocol $server_protocol;
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://drawio-export:8000/;
}
location /minder/ {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-Host $the_host;
proxy_set_header X-Forwarded-Proto $the_scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header Server-Protocol $server_protocol;
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
proxy_pass http://minder/;
}
# 审批
location /approve/ {
proxy_pass http://approve/;
}
location /approve/api/ {
auth_request /approveAuth;
proxy_pass http://approve/api/;
}
location /approveAuth {
internal;
proxy_set_header Content-Type "application/json";
proxy_set_header Content-Length $request_length;
proxy_pass http://service/api/approve/verifyToken;
}
# OKR
location /apps/okr/ {
proxy_pass http://okr:5566/apps/okr/;
}
# AI
location /ai/ {
proxy_http_version 1.1;
proxy_set_header Scheme $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://ai:8881/;
}
include /etc/nginx/conf.d/location/*.conf;
}
include /etc/nginx/conf.d/conf.d/*.conf;

View File

@@ -0,0 +1,12 @@
location /ai/ {
proxy_http_version 1.1;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 3600s;
proxy_set_header Scheme $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://ai:5001/;
}

View File

@@ -0,0 +1,13 @@
location /approve/ {
proxy_pass http://approve/;
}
location /approve/api/ {
auth_request /approveAuth;
proxy_pass http://approve/api/;
}
location /approveAuth {
internal;
proxy_set_header Content-Type "application/json";
proxy_set_header Content-Length $request_length;
proxy_pass http://service/api/approve/verifyToken;
}

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