Compare commits

...

327 Commits

Author SHA1 Message Date
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
474 changed files with 22353 additions and 5923 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,7 +299,7 @@ jobs:
env:
PUBLISH_KEY: ${{ secrets.PUBLISH_KEY }}
run: |
pushd electron
pushd electron || exit
npm install
popd
popd || exit
node ./electron/build.js published

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,8 +12,12 @@ use App\Models\File;
use App\Models\User;
use App\Module\Base;
use App\Module\Timer;
use App\Models\Setting;
use App\Module\Extranet;
use App\Module\ElasticSearch\ElasticSearchUserMsg;
use App\Module\TimeRange;
use App\Module\MsgTool;
use App\Module\Table\OnlineData;
use App\Models\FileContent;
use App\Models\AbstractModel;
use App\Models\WebSocketDialog;
@@ -23,6 +27,7 @@ use App\Models\WebSocketDialogConfig;
use App\Models\WebSocketDialogMsgRead;
use App\Models\WebSocketDialogMsgTodo;
use App\Models\WebSocketDialogMsgTranslate;
use App\Models\WebSocketDialogSession;
use Hhxsv5\LaravelS\Swoole\Task\Task;
/**
@@ -168,28 +173,15 @@ class DialogController extends AbstractController
}
// 搜索消息会话
if (count($list) < 20) {
$prefix = DB::getTablePrefix();
if (preg_match('/[+\-><()~*"@]/', $key)) {
$against = "\"{$key}\"";
} else {
$against = "*{$key}*";
$searchResults = ElasticSearchUserMsg::searchByKeyword($user->userid, $key, 20 - count($list));
if ($searchResults) {
foreach ($searchResults as $item) {
if ($dialog = WebSocketDialog::find($item['id'])) {
$dialog = array_merge($dialog->toArray(), $item);
$list[] = WebSocketDialog::synthesizeData($dialog, $user->userid);
}
}
}
$msgs = DB::table('web_socket_dialog_users as u')
->select(['d.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at', 'm.id as search_msg_id'])
->join('web_socket_dialogs as d', 'u.dialog_id', '=', 'd.id')
->join('web_socket_dialog_msgs as m', 'm.dialog_id', '=', 'd.id')
->where('u.userid', $user->userid)
->where('m.bot', 0)
->whereNull('d.deleted_at')
->whereRaw("MATCH({$prefix}m.key) AGAINST('{$against}' IN BOOLEAN MODE)")
->orderByDesc('m.id')
->take(20 - count($list))
->get()
->map(function($item) use ($user) {
return WebSocketDialog::synthesizeData($item, $user->userid);
})
->all();
$list = array_merge($list, $msgs);
}
//
return Base::retSuccess('success', $list);
@@ -256,6 +248,9 @@ class DialogController extends AbstractController
->where('d.id', $dialog_id)
->whereNull('d.deleted_at')
->first();
if (empty($item)) {
return Base::retError('不在成员列表内');
}
return Base::retSuccess('success', WebSocketDialog::synthesizeData($item, $user->userid));
}
@@ -288,6 +283,9 @@ class DialogController extends AbstractController
$array = array_filter($data->toArray(), function ($item) {
return $item['userid'] > 0;
});
foreach ($array as &$item) {
$item['online'] = $item['bot'] || OnlineData::live($item['userid']) > 0;
}
} else {
$data = WebSocketDialogUser::select(['web_socket_dialog_users.*', 'users.bot'])
->join('users', 'web_socket_dialog_users.userid', '=', 'users.userid')
@@ -531,6 +529,9 @@ class DialogController extends AbstractController
->on('read.msg_id', '=', 'web_socket_dialog_msgs.id');
})->where('web_socket_dialog_msgs.dialog_id', $dialog_id);
//
if ($dialog->session_id > 0) {
$builder->whereSessionId($dialog->session_id);
}
if ($msg_type) {
if ($msg_type === 'tag') {
$builder->where('tag', '>', 0);
@@ -712,7 +713,44 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/one 15. 获取单条消息
* @api {get} api/dialog/msg/esearch 15. 搜索消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__esearch
*
* @apiParam {String} key 搜索关键词
* @apiParam {Number} [pagesize] 每页显示数量,默认:20最大:50
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__esearch()
{
$user = User::auth();
//
$key = trim(Request::input('key'));
$list = [];
//
$searchResults = ElasticSearchUserMsg::searchByKeyword($user->userid, $key, Base::getPaginate(50, 20));
if ($searchResults) {
foreach ($searchResults as $item) {
if ($dialog = WebSocketDialog::find($item['id'])) {
$dialog = array_merge($dialog->toArray(), $item);
$list[] = WebSocketDialog::synthesizeData($dialog, $user->userid);
}
}
}
//
return Base::retSuccess('success', [
'data' => $list,
]);
}
/**
* @api {get} api/dialog/msg/one 16. 获取单条消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -741,7 +779,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/dot 16. 聊天消息去除点
* @api {get} api/dialog/msg/dot 17. 聊天消息去除点
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -774,7 +812,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/read 17. 已读聊天消息
* @api {get} api/dialog/msg/read 18. 已读聊天消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -845,7 +883,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/unread 18. 获取未读消息数据
* @api {get} api/dialog/msg/unread 19. 获取未读消息数据
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -888,7 +926,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/checked 19. 设置消息checked
* @api {get} api/dialog/msg/checked 20. 设置消息checked
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -954,7 +992,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/stream 20. 通知成员监听消息
* @api {post} api/dialog/msg/stream 21. 通知成员监听消息
*
* @apiDescription 通知指定会员EventSource监听流动消息
* @apiVersion 1.0.0
@@ -998,7 +1036,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/sendtext 21. 发送消息
* @api {post} api/dialog/msg/sendtext 22. 发送消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1023,6 +1061,7 @@ class DialogController extends AbstractController
* @apiParam {String} [silence] 是否静默发送
* - no: 正常发送(默认)
* - yes: 静默发送
* @apiParam {String} [model_name] 模型名称仅AI机器人支持
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -1043,6 +1082,7 @@ class DialogController extends AbstractController
$key = trim(Request::input('key'));
$text_type = strtolower(trim(Request::input('text_type')));
$silence = in_array(strtolower(trim(Request::input('silence'))), ['yes', 'true', '1']);
$model_name = trim(Request::input('model_name'));
$markdown = in_array($text_type, ['md', 'markdown']);
//
$result = [];
@@ -1053,6 +1093,9 @@ class DialogController extends AbstractController
//
if ($update_id > 0) {
$action = $update_mark ? "update-$update_id" : "change-$update_id";
if (!($user->bot || $user->isAdmin())) {
Setting::validateMsgLimit('edit', $update_id);
}
} elseif ($reply_id > 0) {
$action = "reply-$reply_id";
if ($reply_check === 'yes') {
@@ -1091,27 +1134,46 @@ class DialogController extends AbstractController
if (empty($size)) {
return Base::retError('消息发送保存失败');
}
$ext = $markdown ? 'md' : 'htm';
$fileData = [
'name' => "LongText-{$strlen}.{$ext}",
'size' => $size,
'file' => $file,
'path' => $path,
'url' => Base::fillUrl($path),
'thumb' => '',
'width' => -1,
'height' => -1,
'ext' => $ext,
$type = $markdown ? 'md' : 'htm';
$desc = $text;
if ($markdown) {
$desc = preg_replace("/:::\s*reasoning[\s\S]*?:::/", "", $desc);
$desc = Base::markdown2html($desc);
}
$desc = strip_tags($desc);
$desc = mb_substr(WebSocketDialogMsg::filterEscape($desc), 0, 200);
$text = MsgTool::truncateText($text, 500, $type);
$msgData = [
'type' => $type, // 内容类型
'desc' => $desc, // 描述内容
'text' => $text, // 简要内容
'file' => [
'name' => "LongText-{$strlen}.{$type}",
'size' => $size,
'file' => $file,
'path' => $path,
'url' => Base::fillUrl($path),
'thumb' => '',
'width' => -1,
'height' => -1,
'ext' => $type,
],
];
if (empty($key)) {
$key = mb_substr(strip_tags($text), 0, 200);
$key = $desc;
}
$result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'file', $fileData, $user->userid, false, false, $silence, $key);
if ($model_name) {
$msgData['model_name'] = $model_name;
}
$result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'longtext', $msgData, $user->userid, false, false, $silence, $key);
} else {
$msgData = ['text' => $text];
if ($markdown) {
$msgData['type'] = 'md';
}
if ($model_name) {
$msgData['model_name'] = $model_name;
}
$result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'text', $msgData, $user->userid, false, false, $silence, $key);
}
}
@@ -1119,7 +1181,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/sendnotice 22. 发送通知
* @api {post} api/dialog/msg/sendnotice 23. 发送通知
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1172,7 +1234,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/sendtemplate 23. 发送模板消息
* @api {post} api/dialog/msg/sendtemplate 24. 发送模板消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1241,7 +1303,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/sendrecord 24. 发送语音
* @api {post} api/dialog/msg/sendrecord 25. 发送语音
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1289,7 +1351,79 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/sendfile 25. 文件上传
* @api {post} api/dialog/msg/convertrecord 26. 录音转文字
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__convertrecord
*
* @apiParam {String} base64 语音base64
* @apiParam {Number} duration 语音时长(毫秒)
* @apiParam {String} [language] 识别语言
* - 比如zh
* - 默认:自动识别
* - 格式:符合 ISO_639 标准
* - 此参数不一定起效果AI会根据语音和language参考翻译识别结果
* @apiParam {String} [translate] 翻译识别结果
* - 比如zh
* - 默认:不翻译结果
* - 格式:符合 ISO_639 标准
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__convertrecord()
{
$user = User::auth();
$user->checkChatInformation();
//
$path = "uploads/tmp/chat/" . date("Ym") . "/" . $user->userid . "/";
$base64 = Request::input('base64');
$language = Request::input('language');
$translate = Request::input('translate');
$duration = intval(Request::input('duration'));
if ($duration < 600) {
return Base::retError('说话时间太短');
}
// 保存录音
$data = Base::record64save([
"base64" => $base64,
"path" => $path,
]);
if (Base::isError($data)) {
return Base::retError($data['msg']);
}
$recordData = $data['data'];
// 转文字
$extParams = [];
if ($language) {
$extParams = [
'language' => $language === 'zh-CHT' ? 'zh' : $language,
'prompt' => "将此语音识别为“" . Doo::getLanguages($language) . "”。",
];
}
$result = Extranet::openAItranscriptions($recordData['file'], $extParams);
if (Base::isError($result)) {
return $result;
}
if (strlen($result['data']) < 1) {
return Base::retError('转文字失败');
}
// 翻译
if ($translate) {
$result = Extranet::openAItranslations($result['data'], Doo::getLanguages($translate));
if (Base::isError($result)) {
return $result;
}
}
// 返回
return $result;
}
/**
* @api {post} api/dialog/msg/sendfile 27. 文件上传
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1321,7 +1455,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/sendfiles 26. 群发文件上传
* @api {post} api/dialog/msg/sendfiles 28. 群发文件上传
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1377,7 +1511,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/sendfileid 27. 通过文件ID发送文件
* @api {get} api/dialog/msg/sendfileid 29. 通过文件ID发送文件
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1387,6 +1521,7 @@ class DialogController extends AbstractController
* @apiParam {Number} file_id 消息ID
* @apiParam {Array} dialogids 转发给的对话ID
* @apiParam {Array} userids 转发给的成员ID
* @apiParam {String} leave_message 转发留言
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -1399,55 +1534,24 @@ class DialogController extends AbstractController
$file_id = intval(Request::input("file_id"));
$dialogids = Request::input('dialogids');
$userids = Request::input('userids');
$leave_message = Request::input('leave_message');
//
if (empty($dialogids) && empty($userids)) {
return Base::retError("请选择转发对话或成员");
return Base::retError("请选择对话或成员");
}
//
$file = File::permissionFind($file_id, $user);
$fileLink = $file->getShareLink($user->userid);
$fileMsg = "<a class=\"mention file\" href=\"{{RemoteURL}}single/file/{$fileLink['code']}\" target=\"_blank\">~{$file->getNameAndExt()}</a>";
$fileMsg = "<p><a class=\"mention file\" href=\"{{RemoteURL}}single/file/{$fileLink['code']}\" target=\"_blank\">~{$file->getNameAndExt()}</a></p>";
if ($leave_message) {
$fileMsg .= "<p>{$leave_message}</p>";
}
//
return AbstractModel::transaction(function() use ($user, $fileMsg, $userids, $dialogids) {
$msgs = [];
$already = [];
if ($dialogids) {
if (!is_array($dialogids)) {
$dialogids = [$dialogids];
}
foreach ($dialogids as $dialogid) {
$res = WebSocketDialogMsg::sendMsg(null, $dialogid, 'text', ['text' => $fileMsg], $user->userid);
if (Base::isSuccess($res)) {
$msgs[] = $res['data'];
$already[] = $dialogid;
}
}
}
if ($userids) {
if (!is_array($userids)) {
$userids = [$userids];
}
foreach ($userids as $userid) {
if (!User::whereUserid($userid)->exists()) {
continue;
}
$dialog = WebSocketDialog::checkUserDialog($user, $userid);
if ($dialog && !in_array($dialog->id, $already)) {
$res = WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $fileMsg], $user->userid);
if (Base::isSuccess($res)) {
$msgs[] = $res['data'];
}
}
}
}
return Base::retSuccess('发送成功', [
'msgs' => $msgs
]);
});
return WebSocketDialogMsg::sendMsgBatch($user, $userids, $dialogids, $fileMsg);
}
/**
* @api {post} api/dialog/msg/sendanon 28. 发送匿名消息
* @api {post} api/dialog/msg/sendanon 30. 发送匿名消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1503,7 +1607,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/sendlocation 29. 发送位置消息
* @api {post} api/dialog/msg/sendlocation 31. 发送位置消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1563,7 +1667,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/readlist 30. 获取消息阅读情况
* @api {get} api/dialog/msg/readlist 32. 获取消息阅读情况
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1592,7 +1696,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/detail 31. 消息详情
* @api {get} api/dialog/msg/detail 33. 消息详情
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1634,13 +1738,25 @@ class DialogController extends AbstractController
$msg = File::formatFileData($msg);
$data['content'] = $msg['content'];
$data['file_mode'] = $msg['file_mode'];
} elseif ($data['type'] == 'longtext') {
$data['content'] = [
'type' => 'htm',
'content' => Doo::translate("内容不存在")
];
if (isset($data['msg']['file']['path'])) {
$filePath = public_path($data['msg']['file']['path']);
if (file_exists($filePath)) {
$data['content']['type'] = $data['msg']['type'];
$data['content']['content'] = file_get_contents($filePath);
}
}
}
//
return Base::retSuccess('success', $data);
}
/**
* @api {get} api/dialog/msg/download 32. 文件下载
* @api {get} api/dialog/msg/download 34. 文件下载
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1677,11 +1793,11 @@ class DialogController extends AbstractController
}
//
$filePath = public_path($array['path']);
return Base::BinaryFileResponse($filePath, $array['name']);
return Base::DownloadFileResponse($filePath, $array['name']);
}
/**
* @api {get} api/dialog/msg/withdraw 33. 聊天消息撤回
* @api {get} api/dialog/msg/withdraw 35. 聊天消息撤回
*
* @apiDescription 消息撤回限制24小时内需要token身份
* @apiVersion 1.0.0
@@ -1702,12 +1818,15 @@ class DialogController extends AbstractController
if (empty($msg)) {
return Base::retError("消息不存在或已被删除");
}
if (!($user->bot || $user->isAdmin())) {
Setting::validateMsgLimit('rev', $msg);
}
$msg->withdrawMsg();
return Base::retSuccess("success");
}
/**
* @api {get} api/dialog/msg/voice2text 34. 语音消息转文字
* @api {get} api/dialog/msg/voice2text 36. 语音消息转文字
*
* @apiDescription 将语音消息转文字需要token身份
* @apiVersion 1.0.0
@@ -1759,7 +1878,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/translation 35. 翻译消息
* @api {get} api/dialog/msg/translation 37. 翻译消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1767,6 +1886,8 @@ class DialogController extends AbstractController
* @apiName msg__translation
*
* @apiParam {Number} msg_id 消息ID
* @apiParam {Number} [force] 强制翻译1是、0否
* - 默认不强制翻译,已翻译过的消息不再翻译
* @apiParam {String} [language] 目标语言,默认当前语言
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
@@ -1778,6 +1899,7 @@ class DialogController extends AbstractController
User::auth();
//
$msg_id = intval(Request::input("msg_id"));
$force = intval(Request::input("force"));
$language = Base::inputOrHeader('language');
$targetLanguage = Doo::getLanguages($language);
//
@@ -1795,13 +1917,20 @@ class DialogController extends AbstractController
//
$row = WebSocketDialogMsgTranslate::whereMsgId($msg_id)->whereLanguage($language)->first();
if ($row) {
return Base::retSuccess("success", $row->only(['msg_id', 'language', 'content']));
if ($force) {
$row->delete();
} else {
return Base::retSuccess("success", $row->only(['msg_id', 'language', 'content']));
}
}
//
$msgData = Base::json2array($msg->getRawOriginal('msg'));
if (empty($msgData['text'])) {
return Base::retError("消息内容为空");
}
if ($msg->type === 'text' && $msgData['type'] === 'md') {
$msgData['text'] = preg_replace('/:::\s*reasoning.*?:::/s', '', $msgData['text']);
}
$res = Extranet::openAItranslations($msgData['text'], $targetLanguage);
if (Base::isError($res)) {
return $res;
@@ -1818,7 +1947,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/mark 36. 消息标记操作
* @api {get} api/dialog/msg/mark 38. 消息标记操作
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1882,7 +2011,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/silence 37. 消息免打扰
* @api {get} api/dialog/msg/silence 39. 消息免打扰
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1945,7 +2074,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/forward 38. 转发消息给
* @api {get} api/dialog/msg/forward 40. 转发消息给
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1956,7 +2085,7 @@ class DialogController extends AbstractController
* @apiParam {Array} dialogids 转发给的对话ID
* @apiParam {Array} userids 转发给的成员ID
* @apiParam {Number} show_source 是否显示原发送者信息
* @apiParam {Array} leave_message 转发留言
* @apiParam {String} leave_message 转发留言
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -1973,7 +2102,7 @@ class DialogController extends AbstractController
$leave_message = Request::input('leave_message');
//
if (empty($dialogids) && empty($userids)) {
return Base::retError("请选择转发对话或成员");
return Base::retError("请选择对话或成员");
}
//
$msg = WebSocketDialogMsg::whereId($msg_id)->first();
@@ -1986,7 +2115,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/emoji 39. emoji回复
* @api {get} api/dialog/msg/emoji 41. emoji回复
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2021,7 +2150,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/tag 40. 标注/取消标注
* @api {get} api/dialog/msg/tag 42. 标注/取消标注
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2050,7 +2179,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/todo 41. 设待办/取消待办
* @api {get} api/dialog/msg/todo 43. 设待办/取消待办
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2093,7 +2222,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/todolist 42. 获取消息待办情况
* @api {get} api/dialog/msg/todolist 44. 获取消息待办情况
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2123,7 +2252,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/done 43. 完成待办
* @api {get} api/dialog/msg/done 45. 完成待办
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2176,7 +2305,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/color 44. 设置颜色
* @api {get} api/dialog/msg/color 46. 设置颜色
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2217,7 +2346,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/group/add 45. 新增群组
* @api {get} api/dialog/group/add 47. 新增群组
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2279,7 +2408,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/group/edit 46. 修改群组
* @api {get} api/dialog/group/edit 48. 修改群组
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2341,7 +2470,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/group/adduser 47. 添加群成员
* @api {get} api/dialog/group/adduser 49. 添加群成员
*
* @apiDescription 需要token身份
* - 有群主时:只有群主可以邀请
@@ -2377,7 +2506,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/group/deluser 48. 移出(退出)群成员
* @api {get} api/dialog/group/deluser 50. 移出(退出)群成员
*
* @apiDescription 需要token身份
* - 只有群主、邀请人可以踢人
@@ -2421,7 +2550,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/group/transfer 49. 转让群组
* @api {get} api/dialog/group/transfer 51. 转让群组
*
* @apiDescription 需要token身份
* - 只有群主且是个人类型群可以解散
@@ -2470,7 +2599,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/group/disband 50. 解散群组
* @api {get} api/dialog/group/disband 52. 解散群组
*
* @apiDescription 需要token身份
* - 只有群主且是个人类型群可以解散
@@ -2498,7 +2627,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/group/searchuser 51. 搜索个人群(仅限管理员)
* @api {get} api/dialog/group/searchuser 53. 搜索个人群(仅限管理员)
*
* @apiDescription 需要token身份用于创建部门搜索个人群组
* @apiVersion 1.0.0
@@ -2527,7 +2656,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/okr/add 52. 创建OKR评论会话
* @api {post} api/dialog/okr/add 54. 创建OKR评论会话
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2566,7 +2695,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/okr/push 53. 推送OKR相关信息
* @api {post} api/dialog/okr/push 55. 推送OKR相关信息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2602,7 +2731,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/wordchain 54. 发送接龙消息
* @api {post} api/dialog/msg/wordchain 56. 发送接龙消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2688,7 +2817,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/vote 55. 发起投票
* @api {post} api/dialog/msg/vote 57. 发起投票
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2804,7 +2933,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/top 56. 置顶/取消置顶
* @api {get} api/dialog/msg/top 58. 置顶/取消置顶
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2864,7 +2993,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/topinfo 57. 获取置顶消息
* @api {get} api/dialog/msg/topinfo 59. 获取置顶消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2891,7 +3020,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/applied 58. 标记消息已应用
* @api {get} api/dialog/msg/applied 60. 标记消息已应用
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2940,7 +3069,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/sticker/search 59. 搜索在线表情
* @api {get} api/dialog/sticker/search 61. 搜索在线表情
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2964,7 +3093,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/config 60. 获取会话配置
* @api {get} api/dialog/config 62. 获取会话配置
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -3000,7 +3129,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/config/save 61. 保存会话配置
* @api {post} api/dialog/config/save 63. 保存会话配置
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -3038,10 +3167,138 @@ class DialogController extends AbstractController
]
)) {
WebSocketDialogMsg::sendMsg(null, $dialog_id, 'notice', [
'notice' => $value ? ("修改提示词:" . $value) : "取消提示词",
'notice' => $value ? ("修改提示词:" . Base::cutStr($value, 100)) : "取消提示词",
], User::userid(), true, true);
}
return Base::retSuccess('保存成功');
}
/**
* @api {get} api/dialog/session/create 64. AI-开启新会话
*
* @apiDescription 需要token身份仅限与AI用户会话
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName session_create
*
* @apiParam {Number} dialog_id 对话ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function session__create()
{
User::auth();
//
$dialog_id = intval(Request::input('dialog_id'));
//
$dialog = WebSocketDialog::checkDialog($dialog_id);
//
if ($dialog->type != 'user') {
return Base::retError('当前对话不支持');
}
//
$hasAiUser = WebSocketDialogUser::join('users as u', 'web_socket_dialog_users.userid', '=', 'u.userid')
->where('dialog_id', $dialog->id)
->where('u.email', 'like', 'ai-%@bot.system')
->exists();
if (!$hasAiUser) {
return Base::retError('当前对话不支持');
}
//
$session = WebSocketDialogSession::whereDialogId($dialog->id)
->whereTitle('')
->first();
if ($session) {
$dialog->session_id = $session->id;
$dialog->save();
return Base::retSuccess('success', $session);
}
//
$session = WebSocketDialogSession::create([
'dialog_id' => $dialog->id,
'status' => 1,
'title' => '',
]);
$session->save();
$dialog->session_id = $session->id;
$dialog->save();
//
return Base::retSuccess('success', $session);
}
/**
* @api {get} api/dialog/session/list 65. AI-获取会话列表
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName session_list
*
* @apiParam {Number} dialog_id 对话ID
*
* @apiParam {Number} [page] 当前页,默认:1
* @apiParam {Number} [pagesize] 每页显示数量,默认:20最大:50
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function session__list()
{
User::auth();
//
$dialog_id = intval(Request::input('dialog_id'));
//
$dialog = WebSocketDialog::checkDialog($dialog_id);
//
$sessions = WebSocketDialogSession::whereDialogId($dialog->id)
->orderByDesc('id')
->paginate(Base::getPaginate(100, 10));
$sessions->transform(function ($item) use ($dialog) {
if ($item->id === $dialog->session_id) {
$item->is_open = 1;
} else {
$item->is_open = 0;
}
return $item;
});
//
return Base::retSuccess('success', $sessions);
}
/**
* @api {get} api/dialog/session/open 66. AI-打开会话
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName session_open
*
* @apiParam {Number} session_id 会话ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function session__open()
{
User::auth();
//
$session_id = intval(Request::input('session_id'));
//
$session = WebSocketDialogSession::whereId($session_id)->first();
if (empty($session)) {
return Base::retError('会话不存在或已被删除');
}
//
$dialog = WebSocketDialog::checkDialog($session->dialog_id);
//
$dialog->session_id = $session->id;
$dialog->save();
//
return Base::retSuccess('success', $session);
}
}

View File

@@ -87,6 +87,7 @@ class FileController extends AbstractController
}
return Base::retError($msg, $data);
}
$fileLink->increment("num");
} else {
return Base::retError('参数错误');
}
@@ -106,6 +107,7 @@ class FileController extends AbstractController
*
* @apiParam {String} [link] 通过分享地址搜索https://t.hitosea.com/single/file/ODcwOCwzOSxpa0JBS2lmVQ==
* @apiParam {String} [key] 关键词
* @apiParam {Number} [take] 获取数量默认50最大100
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -118,7 +120,7 @@ class FileController extends AbstractController
$link = trim(Request::input('link'));
$key = trim(Request::input('key'));
$id = 0;
$take = 50;
$take = Base::getPaginate(100, 50, 'take');
if (preg_match("/\/single\/file\/(.*?)$/i", $link, $match)) {
$id = intval(FileLink::whereCode($match[1])->value('file_id'));
$take = 1;
@@ -301,6 +303,7 @@ class FileController extends AbstractController
//
$userid = $user->userid;
if ($row->pid > 0) {
File::permissionFind($row->pid, $user, 1);
$userid = intval(File::whereId($row->pid)->value('userid'));
}
//
@@ -663,7 +666,7 @@ class FileController extends AbstractController
//
if ($status === 2) {
$parse = parse_url($url);
$from = 'http://' . env('APP_IPPR') . '.3' . $parse['path'] . '?' . $parse['query'];
$from = 'http://nginx' . $parse['path'] . '?' . $parse['query'];
$path = 'uploads/file/' . $file->type . '/' . date("Ym") . '/' . $file->id . '/' . $key;
$save = public_path($path);
Base::makeDir(dirname($save));

View File

@@ -941,7 +941,9 @@ class ProjectController extends AbstractController
* @apiName task__lists
*
* @apiParam {Object} [keys] 搜索条件
* - keys.name: ID、任务名称
* - keys.name: ID、任务名称、任务描述
* - keys.tag: 标签名称
* - keys.status: 任务状态 (completed: 已完成、uncompleted: 未完成、flow-xx: 流程状态ID)
*
* @apiParam {Number} [project_id] 项目ID
* @apiParam {Number} [parent_id] 主任务IDproject_id && parent_id ≤ 0 时 仅查询自己参与的任务)
@@ -994,7 +996,29 @@ class ProjectController extends AbstractController
if (Base::isNumber($keys['name'])) {
$builder->where("project_tasks.id", intval($keys['name']));
} else {
$builder->where("project_tasks.name", "like", "%{$keys['name']}%");
$builder->where(function ($query) use ($keys) {
$query->where("project_tasks.name", "like", "%{$keys['name']}%");
$query->orWhere("project_tasks.desc", "like", "%{$keys['name']}%");
});
}
}
if ($keys['tag']) {
$builder->whereHas('taskTag', function ($query) use ($keys) {
$query->where('project_task_tags.name', $keys['tag']);
});
}
if ($keys['status']) {
if ($keys['status'] == 'completed') {
$builder->whereNotNull('project_tasks.complete_at');
} elseif ($keys['status'] == 'uncompleted') {
$builder->whereNull('project_tasks.complete_at');
} elseif (str_starts_with($keys['status'], 'flow-')) {
$flow = str_replace('flow-', '', $keys['status']);
if (Base::isNumber($flow)) {
$builder->where('project_tasks.flow_item_id', intval($flow));
} elseif ($flow) {
$builder->where('project_tasks.flow_item_name', 'like', "%{$flow}%");
}
}
}
//
@@ -1058,20 +1082,20 @@ class ProjectController extends AbstractController
$query->where('project_users.owner', 1);
$query->where('project_users.userid', $userid);
});
$builder->leftJoin('project_task_users as project_sub_task_users', function ($query) use($userid) {
$query->on('project_sub_task_users.task_pid', '=', 'project_tasks.parent_id');
$query->where('project_sub_task_users.userid', $userid);
});
$builder->leftJoin('project_task_visibility_users', function ($query) use($userid) {
$query->on('project_task_visibility_users.task_id', '=', 'project_tasks.id');
$query->where('project_task_visibility_users.userid', $userid);
});
$builder->leftJoin('project_task_visibility_users as project_sub_task_visibility_users', function ($query) use($userid) {
$query->on('project_sub_task_visibility_users.task_id', '=', 'project_tasks.parent_id');
$query->where('project_sub_task_visibility_users.userid', $userid);
});
$builder->where(function ($query) use ($userid) {
$query->where("project_tasks.visibility", 1);
$query->orWhere("project_users.userid", $userid);
$query->orWhere("project_task_users.userid", $userid);
$query->orWhere("project_task_visibility_users.userid", $userid);
$query->orWhere("project_sub_task_users.userid", $userid);
$query->orWhere("project_sub_task_visibility_users.userid", $userid);
});
// 优化子查询汇总
$builder->leftJoinSub(function ($query) {
@@ -1159,6 +1183,7 @@ class ProjectController extends AbstractController
$list = ProjectTask::with(['taskUser'])
->select([
'projects.name as project_name',
'project_tasks.project_id',
'project_tasks.id',
'project_tasks.name',
'project_tasks.start_at',
@@ -1852,7 +1877,7 @@ class ProjectController extends AbstractController
}
//
$filePath = public_path($file->getRawOriginal('path'));
return Base::BinaryFileResponse($filePath, $file->name);
return Base::DownloadFileResponse($filePath, $file->name);
}
/**
@@ -2297,8 +2322,8 @@ class ProjectController extends AbstractController
* @apiGroup project
* @apiName task__flow
*
* @apiParam {Number} task_id 任务ID
* @apiParam {Number} project_id 项目ID - 存在时只返回这个项目的
* @apiParam {Number} [task_id] 任务ID
* @apiParam {Number} [project_id] 项目ID存在时只返回这个项目的工作流,主要用于任务移动到其他项目时)
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -2399,6 +2424,7 @@ class ProjectController extends AbstractController
*/
public function task__move()
{
Base::checkClientVersion('0.42.0');
User::auth();
//
$task_id = intval(Request::input('task_id'));
@@ -2436,9 +2462,26 @@ class ProjectController extends AbstractController
//
$task->moveTask($project_id, $column_id, $flow_item_id, $owner, $assist, $completeAt);
//
$task = ProjectTask::userTask($task_id);
$data = [];
$mainTask = ProjectTask::userTask($task_id)?->toArray();
if ($mainTask) {
$mainTask['column_name'] = ProjectColumn::whereId($mainTask['column_id'])->value('name');
$mainTask['project_name'] = Project::whereId($mainTask['project_id'])->value('name');
$data[] = $mainTask;
//
$subTasks = ProjectTask::whereParentId($task_id)->get();
foreach ($subTasks as $subTask) {
$data[] = [
'id' => $subTask->id,
'project_id' => $subTask->project_id,
'column_id' => $subTask->column_id,
'column_name' => $mainTask['column_name'],
'project_name' => $mainTask['project_name'],
];
}
}
//
return Base::retSuccess('移动成功', $task);
return Base::retSuccess('移动成功', $data);
}
/**
@@ -2567,7 +2610,7 @@ class ProjectController extends AbstractController
$builder->with(['projectTask:id,parent_id,name'])->whereProjectId($project->id)->whereTaskOnly(0);
}
//
$list = $builder->orderByDesc('created_at')->paginate(Base::getPaginate(100, 20));
$list = $builder->orderByDesc('created_at')->orderByDesc('id')->paginate(Base::getPaginate(100, 20));
$list->transform(function (ProjectLog $log) use ($task_id) {
$timestamp = Carbon::parse($log->created_at)->timestamp;
if ($task_id === 0) {

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,70 @@ class ReportController extends AbstractController
}
/**
* @api {get} api/report/last_submitter 07. 获取最后一次提交的接收人
* @api {get} api/report/share 07. 分享报告到消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup report
* @apiName share
*
* @apiParam {Number} id 报告id
* @apiParam {Array} dialogids 转发给的对话ID
* @apiParam {Array} userids 转发给的成员ID
* @apiParam {String} leave_message 转发留言
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function share()
{
$user = User::auth();
//
$id = Request::input('id');
$dialogids = Request::input('dialogids');
$userids = Request::input('userids');
$leave_message = Request::input('leave_message');
//
if (is_array($id)) {
if (count(Base::arrayRetainInt($id)) > 20) {
return Base::retError("最多只能操作20条数据");
}
$builder = Report::whereIn("id", Base::arrayRetainInt($id));
} else {
$builder = Report::whereId(intval($id));
}
$reportMsgs = [];
$builder ->chunkById(100, function ($list) use (&$reportMsgs, $user) {
/** @var Report $item */
foreach ($list as $item) {
$reportLink = ReportLink::generateLink($item->id, $user->userid);
$reportMsgs[] = "<a class=\"mention report\" href=\"{{RemoteURL}}single/report/detail/{$reportLink['code']}\" target=\"_blank\">%{$item->title}</a>";
}
});
if (empty($reportMsgs)) {
return Base::retError("报告不存在或已被删除");
}
$reportTag = count($reportMsgs) > 1 ? 'li' : 'p';
$reportMsgs = array_map(function ($item) use ($reportTag) {
return "<{$reportTag}>{$item}</{$reportTag}>";
}, $reportMsgs);
if ($reportTag === 'li') {
array_unshift($reportMsgs, "<ol>");
$reportMsgs[] = "</ol>";
}
if ($leave_message) {
$reportMsgs[] = "<p>{$leave_message}</p>";
}
$msgText = implode("", $reportMsgs);
//
return WebSocketDialogMsg::sendMsgBatch($user, $userids, $dialogids, $msgText);
}
/**
* @api {get} api/report/last_submitter 08. 获取最后一次提交的接收人
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup report
* @apiName last_submitter
@@ -505,8 +614,9 @@ class ReportController extends AbstractController
}
/**
* @api {get} api/report/unread 08. 获取未读
* @api {get} api/report/unread 09. 获取未读
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup report
* @apiName unread
@@ -529,8 +639,9 @@ class ReportController extends AbstractController
}
/**
* @api {get} api/report/read 09. 标记汇报已读,可批量
* @api {get} api/report/read 10. 标记汇报已读,可批量
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup report
* @apiName read

View File

@@ -41,7 +41,7 @@ class SystemController extends AbstractController
* @apiParam {String} type
* - get: 获取(默认)
* - all: 获取所有(需要管理员权限)
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'temp_account_alias', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'voice2text', 'translation', 'e2e_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'all_group_mute', 'all_group_autoin', 'user_private_chat_mute', 'user_group_chat_mute', 'system_alias', 'image_compress', 'image_quality', 'image_save_local', 'start_home']
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'temp_account_alias', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'voice2text', 'translation', 'e2e_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'all_group_mute', 'all_group_autoin', 'user_private_chat_mute', 'user_group_chat_mute', 'system_alias', 'system_welcome', 'image_compress', 'image_quality', 'image_save_local', 'start_home']
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -71,6 +71,8 @@ class SystemController extends AbstractController
'voice2text',
'translation',
'e2e_message',
'msg_rev_limit',
'msg_edit_limit',
'auto_archived',
'archived_day',
'task_visible',
@@ -80,6 +82,7 @@ class SystemController extends AbstractController
'user_private_chat_mute',
'user_group_chat_mute',
'system_alias',
'system_welcome',
'image_compress',
'image_quality',
'image_save_local',
@@ -108,6 +111,9 @@ class SystemController extends AbstractController
if ($all['system_alias'] == env('APP_NAME')) {
$all['system_alias'] = '';
}
if ($all['system_welcome'] == '欢迎您,{username}') {
$all['system_welcome'] = '';
}
$setting = Base::setting('system', Base::newTrim($all));
} else {
$setting = Base::setting('system');
@@ -131,6 +137,8 @@ class SystemController extends AbstractController
$setting['voice2text'] = $setting['voice2text'] ?: 'close';
$setting['translation'] = $setting['translation'] ?: 'close';
$setting['e2e_message'] = $setting['e2e_message'] ?: 'close';
$setting['msg_rev_limit'] = $setting['msg_rev_limit'] ?: '';
$setting['msg_edit_limit'] = $setting['msg_edit_limit'] ?: '';
$setting['auto_archived'] = $setting['auto_archived'] ?: 'close';
$setting['archived_day'] = floatval($setting['archived_day']) ?: 7;
$setting['task_visible'] = $setting['task_visible'] ?: 'close';
@@ -283,6 +291,8 @@ class SystemController extends AbstractController
* @apiParam {String} type
* - get: 获取(默认)
* - save: 保存设置(参数:[...]
* @apiParam {String} filter 过滤字段(可选)
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
@@ -292,6 +302,7 @@ class SystemController extends AbstractController
User::auth('admin');
//
$type = trim(Request::input('type'));
$filter = trim(Request::input('filter'));
$setting = Base::setting('aibotSetting');
if ($type == 'save') {
if (env("SYSTEM_SETTING") == 'disabled') {
@@ -306,10 +317,18 @@ class SystemController extends AbstractController
}
$setting = Base::setting('aibotSetting', Base::newTrim($setting));
}
if ($filter) {
$setting = array_filter($setting, function($value, $key) use ($filter) {
return str_starts_with($key, $filter);
}, ARRAY_FILTER_USE_BOTH);
}
//
if (env("SYSTEM_SETTING") == 'disabled') {
foreach ($setting as $key => $item) {
if (str_contains($key, '_key')) {
if (empty($item)) {
continue;
}
if (str_ends_with($key, '_key') || str_ends_with($key, '_secret')) {
$setting[$key] = substr($item, 0, 4) . str_repeat('*', strlen($item) - 8) . substr($item, -4);
}
}
@@ -319,7 +338,66 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/setting/checkin 05. 获取签到设置、保存签到设置(限管理员)
* @api {get} api/system/setting/aibot_models 05. 获取AI模型
*
* @apiDescription 获取所有AI机器人模型设置
* @apiVersion 1.0.0
* @apiGroup system
* @apiName aibot_models
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function setting__aibot_models()
{
$setting = Base::setting('aibotSetting');
$setting = array_filter($setting, function($value, $key) {
return str_ends_with($key, '_models') || str_ends_with($key, '_model');
}, ARRAY_FILTER_USE_BOTH);
return Base::retSuccess('success', $setting ?: json_decode('{}'));
}
/**
* @api {get} api/system/setting/aibot_defmodels 06. 获取AI默认模型
*
* @apiDescription 获取AI机器人默认模型
* @apiVersion 1.0.0
* @apiGroup system
* @apiName setting__aibot_defmodels
*
* @apiParam {String} type AI类型
* @apiParam {String} [base_url] 基础URL仅 type=ollama 时有效)
* @apiParam {String} [key] Key仅 type=ollama 时有效)
* @apiParam {String} [agency] 使用代理(仅 type=ollama 时有效)
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function setting__aibot_defmodels()
{
$type = trim(Request::input('type'));
if ($type == 'ollama') {
$baseUrl = trim(Request::input('base_url'));
$key = trim(Request::input('key'));
$agency = trim(Request::input('agency'));
if (empty($baseUrl)) {
return Base::retError('请先填写 Base URL');
}
return Extranet::ollamaModels($baseUrl, $key, $agency);
}
$models = Setting::AIDefaultModels($type);
if (empty($models)) {
return Base::retError('未找到默认模型');
}
return Base::retSuccess('success', [
'models' => $models
]);
}
/**
* @api {get} api/system/setting/checkin 07. 获取签到设置、保存签到设置(限管理员)
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -425,7 +503,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/setting/apppush 06. 获取APP推送设置、保存APP推送设置限管理员
* @api {get} api/system/setting/apppush 08. 获取APP推送设置、保存APP推送设置限管理员
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -470,7 +548,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/setting/thirdaccess 07. 第三方帐号(限管理员)
* @api {get} api/system/setting/thirdaccess 09. 第三方帐号(限管理员)
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -540,7 +618,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/setting/file 08. 文件设置(限管理员)
* @api {get} api/system/setting/file 10. 文件设置(限管理员)
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -580,7 +658,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/demo 09. 获取演示帐号
* @api {get} api/system/demo 11. 获取演示帐号
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -604,7 +682,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/priority 10. 任务优先级
* @api {post} api/system/priority 12. 任务优先级
*
* @apiDescription 获取任务优先级、保存任务优先级
* @apiVersion 1.0.0
@@ -653,7 +731,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/column/template 11. 创建项目模板
* @api {post} api/system/column/template 13. 创建项目模板
*
* @apiDescription 获取创建项目模板、保存创建项目模板
* @apiVersion 1.0.0
@@ -700,7 +778,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/license 12. License
* @api {post} api/system/license 14. License
*
* @apiDescription 获取License信息、保存License限管理员
* @apiVersion 1.0.0
@@ -769,7 +847,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/info 13. 获取终端详细信息
* @api {get} api/system/get/info 15. 获取终端详细信息
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -798,7 +876,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/ip 14. 获取IP地址
* @api {get} api/system/get/ip 16. 获取IP地址
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -813,7 +891,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/cnip 15. 是否中国IP地址
* @api {get} api/system/get/cnip 17. 是否中国IP地址
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -830,7 +908,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/ipgcj02 16. 获取IP地址经纬度
* @api {get} api/system/get/ipgcj02 18. 获取IP地址经纬度
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -847,7 +925,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/ipinfo 17. 获取IP地址详细信息
* @api {get} api/system/get/ipinfo 19. 获取IP地址详细信息
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -864,7 +942,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/imgupload 18. 上传图片
* @api {post} api/system/imgupload 20. 上传图片
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -930,7 +1008,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/imgview 19. 浏览图片空间
* @api {get} api/system/get/imgview 21. 浏览图片空间
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1027,7 +1105,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/fileupload 20. 上传文件
* @api {post} api/system/fileupload 22. 上传文件
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1071,7 +1149,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/updatelog 21. 获取更新日志
* @api {get} api/system/get/updatelog 23. 获取更新日志
*
* @apiDescription 获取更新日志
* @apiVersion 1.0.0
@@ -1104,7 +1182,7 @@ class SystemController extends AbstractController
if ($logResults) {
$logVersion = $logResults[0]['title'];
$logContent = implode("\n", array_map(function($item) {
return "## [{$item['title']}]" . $item['content'];
return "## {$item['title']}" . $item['content'];
}, $logResults));
}
return Base::retSuccess('success', [
@@ -1114,7 +1192,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/email/check 22. 邮件发送测试(限管理员)
* @api {get} api/system/email/check 24. 邮件发送测试(限管理员)
*
* @apiDescription 测试配置邮箱是否能发送邮件
* @apiVersion 1.0.0
@@ -1160,7 +1238,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/checkin/export 23. 导出签到数据(限管理员)
* @api {get} api/system/checkin/export 25. 导出签到数据(限管理员)
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -1329,7 +1407,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/checkin/down 24. 下载导出的签到数据
* @api {get} api/system/checkin/down 26. 下载导出的签到数据
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -1355,7 +1433,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/version 25. 获取版本号
* @api {get} api/system/version 27. 获取版本号
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -1392,7 +1470,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/prefetch 26. 预加载的资源
* @api {get} api/system/prefetch 28. 预加载的资源
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -1432,7 +1510,7 @@ class SystemController extends AbstractController
}
// 添加office资源
$officePath = '';
$officeApi = 'http://' . env('APP_IPPR') . '.6/web-apps/apps/api/documents/api.js';
$officeApi = 'http://office/web-apps/apps/api/documents/api.js';
$content = @file_get_contents($officeApi);
if ($content) {
if (preg_match("/const\s+ver\s*=\s*'\/*([^']+)'/", $content, $matches)) {

View File

@@ -1185,12 +1185,15 @@ class UsersController extends AbstractController
'alias' => $data['alias'],
'platform' => Base::platform(),
];
$version = $data['appVersion'] ? ($data['appVersionName'] . " ({$data['appVersion']})") : '';
$isNotified = trim($data['isNotified']) === 'true' || $data['isNotified'] === true ? 1 : intval($data['isNotified']);
$row = UmengAlias::where($inArray);
if ($row->exists()) {
$row->update([
'ua' => $data['userAgent'],
'device' => $data['deviceModel'],
'is_notified' => intval($data['isNotified']),
'version' => $version,
'is_notified' => $isNotified,
'updated_at' => Carbon::now()
]);
return Base::retSuccess('别名已存在');
@@ -1198,7 +1201,8 @@ class UsersController extends AbstractController
$row = UmengAlias::createInstance(array_merge($inArray, [
'ua' => $data['userAgent'],
'device' => $data['deviceModel'],
'is_notified' => intval($data['isNotified']),
'version' => $version,
'is_notified' => $isNotified,
]));
if ($row->save()) {
return Base::retSuccess('添加成功');
@@ -1649,19 +1653,22 @@ class UsersController extends AbstractController
if (empty($parentDepartment)) {
return Base::retError('上级部门不存在或已被删除');
}
if ($parentDepartment->parent_id > 0) {
return Base::retError('上级部门层级错误');
if (count($parentDepartment->parents()) > 2) {
return Base::retError('部门层级最多只能创建3级');
}
if (UserDepartment::whereParentId($parent_id)->count() > 20) {
if ($id > 0 && UserDepartment::whereParentId($id)->whereId($parent_id)->exists()) {
return Base::retError('不能选择自己的子部门作为上级部门');
}
if (UserDepartment::whereParentId($parent_id)->count() >= 20) {
return Base::retError('每个部门最多只能创建20个子部门');
}
if ($id > 0 && UserDepartment::whereParentId($id)->exists()) {
return Base::retError('含有子部门无法修改上级部门');
}
}
if (empty($owner_userid) || !User::whereUserid($owner_userid)->exists()) {
return Base::retError('请选择正确的部门负责人');
}
if (UserDepartment::whereOwnerUserid($owner_userid)->count() >= 10) {
return Base::retError('每个用户最多只能负责10个部门');
}
//
$userDepartment->saveDepartment([
'name' => $name,
@@ -1670,7 +1677,7 @@ class UsersController extends AbstractController
], $dialog_useid);
Cache::forever("UserDepartment::rand", Base::generatePassword());
//
return Base::retSuccess($parent_id > 0 ? '保存成功' : '新建成功');
return Base::retSuccess($id > 0 ? '保存成功' : '新建成功');
}
/**
@@ -1697,6 +1704,9 @@ class UsersController extends AbstractController
if (empty($userDepartment)) {
return Base::retError('部门不存在或已被删除');
}
if (UserDepartment::whereParentId($id)->exists()) {
return Base::retError('含有子部门无法删除');
}
$userDepartment->deleteDepartment();
Cache::forever("UserDepartment::rand", Base::generatePassword());
//
@@ -1918,7 +1928,51 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/bot/info 32. 机器人信息
* @api {get} api/users/bot/list 32. 机器人列表
*
* @apiDescription 需要token身份获取我的机器人列表
* @apiVersion 1.0.0
* @apiGroup users
* @apiName bot__list
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function bot__list()
{
// 获取当前认证用户
$user = User::auth();
// 使用连表查询一次性获取所有机器人数据
$bots = User::join('user_bots', 'user_bots.bot_id', '=', 'users.userid')
->where('user_bots.userid', $user->userid)
->select([
'users.userid',
'users.nickname',
'users.userimg',
'user_bots.clear_day',
'user_bots.webhook_url'
])
->orderByDesc('id')
->get()
->toArray();
foreach ($bots as &$bot) {
$bot['id'] = $bot['userid'];
$bot['name'] = $bot['nickname'];
$bot['avatar'] = $bot['userimg'];
$bot['system_name'] = UserBot::systemBotName($bot['name']);
unset($bot['userid'], $bot['nickname'], $bot['userimg']);
}
// 返回成功响应将机器人列表包装在list字段中
return Base::retSuccess('success', [
'list' => $bots
]);
}
/**
* @api {get} api/users/bot/info 33. 机器人信息
*
* @apiDescription 需要token身份获取我的机器人信息
* @apiVersion 1.0.0
@@ -1969,14 +2023,14 @@ class UsersController extends AbstractController
}
/**
* @api {post} api/users/bot/edit 33. 编辑机器人
* @api {post} api/users/bot/edit 34. 添加、编辑机器人
*
* @apiDescription 需要token身份编辑 我的机器人 或 管理员修改系统机器人 信息
* @apiVersion 1.0.0
* @apiGroup users
* @apiName bot__edit
*
* @apiParam {Number} id 机器人ID
* @apiParam {Number} [id] 机器人ID(编辑时必填,留空为添加)
* @apiParam {String} [name] 机器人名称
* @apiParam {String} [avatar] 机器人头像
* @apiParam {Number} [clear_day] 清理天数(仅 我的机器人)
@@ -1991,10 +2045,19 @@ class UsersController extends AbstractController
$user = User::auth();
//
$botId = intval(Request::input('id'));
$botUser = User::whereUserid($botId)->whereBot(1)->first();
if (empty($botUser)) {
return Base::retError('机器人不存在');
if (empty($botId)) {
$res = UserBot::newbot($user->userid, trim(Request::input('name')));
if (Base::isError($res)) {
return $res;
}
$botUser = $res['data'];
} else {
$botUser = User::whereUserid($botId)->whereBot(1)->first();
if (empty($botUser)) {
return Base::retError('机器人不存在');
}
}
//
$userBot = UserBot::whereBotId($botUser->userid)->whereUserid($user->userid)->first();
if (empty($userBot)) {
if (UserBot::systemBotName($botUser->email)) {
@@ -2051,11 +2114,61 @@ class UsersController extends AbstractController
$data['clear_day'] = $userBot->clear_day;
$data['webhook_url'] = $userBot->webhook_url;
}
return Base::retSuccess('修改成功', $data);
return Base::retSuccess($botId ? '修改成功' : '添加成功', $data);
}
/**
* @api {get} api/users/share/list 34. 获取分享列表
* @api {get} api/users/bot/delete 35. 删除机器人
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup users
* @apiName bot__delete
*
* @apiParam {Number} id 机器人ID
* @apiParam {String} remark 删除备注
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function bot__delete()
{
$user = User::auth();
//
$botId = intval(Request::input('id'));
$remark = trim(Request::input('remark'));
//
if (empty($remark)) {
return Base::retError('请输入删除备注');
}
if (mb_strlen($remark) > 255) {
return Base::retError('删除备注长度限制255个字');
}
//
$botUser = User::whereUserid($botId)->whereBot(1)->first();
if (empty($botUser)) {
return Base::retError('机器人不存在');
}
$userBot = UserBot::whereBotId($botUser->userid)->whereUserid($user->userid)->first();
if (empty($userBot)) {
if (UserBot::systemBotName($botUser->email)) {
// 系统机器人(仅限管理员)
return Base::retError('系统机器人不能删除');
} else {
// 其他用户的机器人(仅限主人)
return Base::retError('不是你的机器人');
}
}
//
if (!$botUser->deleteUser($remark)) {
return Base::retError('删除失败');
}
return Base::retSuccess('删除成功');
}
/**
* @api {get} api/users/share/list 36. 获取分享列表
*
* @apiVersion 1.0.0
* @apiGroup users
@@ -2140,7 +2253,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/annual/report 35. 年度报告
* @api {get} api/users/annual/report 37. 年度报告
*
* @apiVersion 1.0.0
* @apiGroup users

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";
}
@@ -490,7 +493,7 @@ class IndexController extends InvokeController
if (in_array($ext, File::localExt)) {
$url = Base::fillUrl($path);
} else {
$url = 'http://' . env('APP_IPPR') . '.3/' . $path;
$url = 'http://nginx/' . $path;
}
$url = Base::urlAddparameter($url, [
'fullfilename' => Base::rightDelete($name, '.' . $ext) . '_' . filemtime($file) . '.' . $ext

View File

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

View File

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

View File

@@ -79,9 +79,28 @@ class File extends AbstractModel
* office文件
*/
const officeExt = [
'doc', 'docx',
'xls', 'xlsx',
'ppt', 'pptx',
// 文本文件
'doc', 'docx', // Microsoft Word 文档
'dot', 'dotx', // Word 模板
'odt', // OpenDocument 文本格式
'ott', // OpenDocument 文本模板
'rtf', // 富文本格式
// 电子表格
'xls', 'xlsx', // Microsoft Excel 电子表格
'xlsm', // Excel 含宏的工作簿
'xlt', 'xltx', // Excel 模板
'ods', // OpenDocument 电子表格格式
'ots', // OpenDocument 电子表格模板
'csv', // 逗号分隔值
'tsv', // 制表符分隔值
// 演示文稿
'ppt', 'pptx', // Microsoft PowerPoint 演示文稿
'pps', 'ppsx', // PowerPoint 幻灯片放映
'pot', 'potx', // PowerPoint 模板
'odp', // OpenDocument 演示文稿格式
'otp', // OpenDocument 演示文稿模板
];
/**
@@ -264,9 +283,9 @@ class File extends AbstractModel
'text', 'md', 'markdown' => 'document',
'drawio' => 'drawio',
'mind' => 'mind',
'doc', 'docx' => "word",
'xls', 'xlsx' => "excel",
'ppt', 'pptx' => "ppt",
'doc', 'docx', 'dot', 'dotx', 'odt', 'ott', 'rtf' => "word",
'xls', 'xlsx', 'xlsm', 'xlt', 'xltx', 'ods', 'ots', 'csv', 'tsv' => "excel",
'ppt', 'pptx', 'pps', 'ppsx', 'pot', 'potx', 'odp', 'otp' => "ppt",
'wps' => "wps",
'jpg', 'jpeg', 'webp', 'png', 'gif', 'bmp', 'ico', 'raw', 'svg' => "picture",
'rar', 'zip', 'jar', '7-zip', 'tar', 'gzip', '7z', 'gz', 'apk', 'dmg' => "archive",
@@ -706,9 +725,9 @@ class File extends AbstractModel
* @param int $permission
* @return File
*/
public static function permissionFind(int $id, $user, int $limit = 0, int &$permission = -1)
public static function permissionFind($id, $user, int $limit = 0, int &$permission = -1)
{
$file = File::find($id);
$file = File::find(intval($id));
if (empty($file)) {
throw new ApiException('文件不存在或已被删除');
}

View File

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

View File

@@ -219,7 +219,11 @@ class Project extends AbstractModel
'userid' => $userid,
], [
'important' => 1
]);
], function () use ($userid) {
return [
'bot' => User::isBot($userid) ? 1 : 0,
];
});
}
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->whereImportant(1)->remove();
});
@@ -415,6 +419,7 @@ class Project extends AbstractModel
$hasStart = false;
$hasEnd = false;
$upTaskList = [];
$projectUserids = $this->relationUserids();
foreach ($flows as $item) {
$id = intval($item['id']);
$turns = Base::arrayRetainInt($item['turns'] ?: [], true);
@@ -431,6 +436,12 @@ class Project extends AbstractModel
if ($userlimit && empty($userids)) {
throw new ApiException("状态[{$item['name']}]设置错误,设置限制负责人时必须填写状态负责人");
}
foreach ($userids as $userid) {
if (!in_array($userid, $projectUserids)) {
$nickname = User::userid2nickname($userid);
throw new ApiException("状态[{$item['name']}]设置错误,状态负责人[{$nickname}]不在项目成员内");
}
}
$flow = ProjectFlowItem::updateInsert([
'id' => $id,
'project_id' => $this->id,

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]
@@ -1180,7 +1199,11 @@ class ProjectTask extends AbstractModel
'userid' => $userid,
], [
'important' => 1
]);
], function () use ($userid) {
return [
'bot' => User::isBot($userid) ? 1 : 0,
];
});
}
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->whereImportant(1)->remove();
});
@@ -1343,6 +1366,16 @@ class ProjectTask extends AbstractModel
if (!$this->hasOwner()) {
throw new ApiException('请先领取任务');
}
if ($this->overdue) {
$this->addLog("{任务}超期未完成", [
'cache' => [
'task_at' => $this->start_at . '~' . $this->end_at,
'over_sec' => Carbon::now()->diffInSeconds($this->end_at),
'owners' => $this->taskUser->where('owner', 1)->pluck('userid')->toArray(),
'assists' => $this->taskUser->where('owner', 0)->pluck('userid')->toArray(),
]
]);
}
if (empty($complete_name)) {
$complete_name = '已完成';
}
@@ -1833,6 +1866,11 @@ class ProjectTask extends AbstractModel
$taskUser->save();
}
}
// 子任务
ProjectTask::whereParentId($this->id)->change([
'project_id' => $projectId,
'column_id' => $columnId,
]);
//
if ($flowItemId) {
$flowItem = projectFlowItem::whereProjectId($projectId)->whereId($flowItemId)->first();
@@ -1860,6 +1898,67 @@ class ProjectTask extends AbstractModel
return true;
}
/**
* 生成AI上下文
* @return array
*/
public function AIContext()
{
$contexts = [];
if ($this->archived_at) {
$contexts[] = "任务状态:已归档";
$contexts[] = "归档时间:" . $this->archived_at;
} elseif ($this->complete_at) {
$contexts[] = "任务状态:已完成";
$contexts[] = "完成时间:" . $this->complete_at;
} elseif ($this->end_at && Carbon::parse($this->end_at)->lt(Carbon::now())) {
$contexts[] = "任务状态:已过期";
$contexts[] = "任务截止时间:" . $this->end_at;
} else {
$contexts[] = "任务状态:进行中";
if ($this->start_at) {
$contexts[] = "任务开始时间:" . $this->start_at;
}
if ($this->end_at) {
$contexts[] = "任务截止时间:" . $this->end_at;
}
}
$contexts[] = "当前系统时间:" . Carbon::now()->toDateTimeString();
if ($this->content) {
$taskDesc = $this->content?->getContentInfo();
if ($taskDesc) {
$converter = new HtmlConverter(['strip_tags' => true]);
$descContent = Base::cutStr($converter->convert($taskDesc['content']), 2000);
$contexts[] = <<<EOF
任务描述:
```md
{$descContent}
```
EOF;
}
}
$subTask = ProjectTask::select(['id', 'name', 'complete_at', 'end_at'])->whereParentId($this->id)->get();
if ($subTask->isNotEmpty()) {
$subTaskContent = $subTask->map(function($item) {
if ($item->complete_at) {
$status = " (已完成)";
} elseif ($item->end_at && Carbon::parse($item->end_at)->lt(Carbon::now())) {
$status = " (已过期)";
} else {
$status = " (进行中)";
}
return " - {$item->name} {$status}";
})->join("\n");
if ($subTaskContent) {
$contexts[] = <<<EOF
子任务列表:
{$subTaskContent}
EOF;
}
}
return $contexts;
}
/**
* 获取任务
* @param $task_id

View File

@@ -16,7 +16,7 @@ namespace App\Models;
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Project $project
* @property-read \App\Models\User|null $user
* @property-read \App\Models\User $user
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)

View File

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

View File

@@ -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
@@ -65,20 +68,34 @@ class Setting extends AbstractModel
$value['claude_key'] = $value['claude_token'];
}
$array = [];
$aiList = ['openai', 'claude', 'gemini', 'zhipu', 'qianwen', 'wenxin'];
$fieldList = ['key', 'model', 'agency', 'system', 'secret'];
$aiList = ['openai', 'claude', 'deepseek', 'gemini', 'grok', 'ollama', 'zhipu', 'qianwen', 'wenxin'];
$fieldList = ['key', 'secret', 'models', 'model', 'base_url', 'agency', 'temperature', 'system'];
foreach ($aiList as $aiName) {
foreach ($fieldList as $fieldName) {
$key = $aiName . '_' . $fieldName;
$array[$key] = $value[$key] ?: match ($key) {
'openai_model' => 'gpt-4o-mini',
'claude_model' => 'claude-3-5-sonnet-latest',
'gemini_model' => 'gemini-1.5-flash',
'zhipu_model' => 'glm-4',
'qianwen_model' => 'qwen-turbo',
'wenxin_model' => 'ernie-4.0-8k',
default => '',
};
$content = $value[$key] ? trim($value[$key]) : '';
switch ($fieldName) {
case 'models':
if ($content) {
$content = explode("\n", $content);
$content = array_filter($content);
}
if (empty($content)) {
$content = self::AIDefaultModels($aiName);
}
$content = implode("\n", $content);
break;
case 'model':
$models = Setting::AIModels2Array($array[$key . 's'], true);
$content = in_array($content, $models) ? $content : ($models[0] ?? '');
break;
case 'temperature':
if ($content) {
$content = floatval(min(1, max(0, floatval($content) ?: 0.7)));
}
break;
}
$array[$key] = $content;
}
}
$value = $array;
@@ -98,6 +115,121 @@ class Setting extends AbstractModel
return !!$array[$ai . '_key'];
}
/**
* AI默认模型
* @param string $ai
* @return array
*/
public static function AIDefaultModels($ai = 'openai')
{
return match ($ai) {
'openai' => [
'gpt-4 | GPT-4',
'gpt-4-turbo | GPT-4 Turbo',
'gpt-4o | GPT-4o',
'gpt-4o-mini | GPT-4o Mini',
'o1 | GPT-o1',
'o1-mini | GPT-o1 Mini',
'o3-mini | GPT-o3 Mini',
'gpt-3.5-turbo | GPT-3.5 Turbo',
'gpt-3.5-turbo-16k | GPT-3.5 Turbo 16K',
'gpt-3.5-turbo-0125 | GPT-3.5 Turbo 0125',
'gpt-3.5-turbo-1106 | GPT-3.5 Turbo 1106'
],
'claude' => [
'claude-3-5-sonnet-latest | Claude 3.5 Sonnet',
'claude-3-5-sonnet-20241022 | Claude 3.5 Sonnet 20241022',
'claude-3-5-haiku-latest | Claude 3.5 Haiku',
'claude-3-5-haiku-20241022 | Claude 3.5 Haiku 20241022',
'claude-3-opus-latest | Claude 3 Opus',
'claude-3-opus-20240229 | Claude 3 Opus 20240229',
'claude-3-haiku-20240307 | Claude 3 Haiku 20240307',
'claude-2.1 | Claude 2.1',
'claude-2.0 | Claude 2.0'
],
'deepseek' => [
'deepseek-chat | DeepSeek V3',
'deepseek-reasoner | DeepSeek R1'
],
'gemini' => [
'gemini-2.0-flash | Gemini 2.0 Flash',
'gemini-2.0-flash-lite-preview-02-05 | Gemini 2.0 Flash-Lite Preview',
'gemini-1.5-flash | Gemini 1.5 Flash',
'gemini-1.5-flash-8b | Gemini 1.5 Flash 8B',
'gemini-1.5-pro | Gemini 1.5 Pro',
'gemini-1.0-pro | Gemini 1.0 Pro'
],
'grok' => [
'grok-2-vision-1212 | Grok 2 Vision 1212',
'grok-2-vision | Grok 2 Vision',
'grok-2-vision-latest | Grok 2 Vision Latest',
'grok-2-1212 | Grok 2 1212',
'grok-2 | Grok 2',
'grok-2-latest | Grok 2 Latest',
'grok-vision-beta | Grok Vision Beta',
'grok-beta | Grok Beta',
],
'zhipu' => [
'glm-4 | GLM-4',
'glm-4-plus | GLM-4 Plus',
'glm-4-air | GLM-4 Air',
'glm-4-airx | GLM-4 AirX',
'glm-4-long | GLM-4 Long',
'glm-4-flash | GLM-4 Flash',
'glm-4v | GLM-4V',
'glm-4v-plus | GLM-4V Plus',
'glm-3-turbo | GLM-3 Turbo'
],
'qianwen' => [
'qwen-max | QWEN Max',
'qwen-max-latest | QWEN Max Latest',
'qwen-turbo | QWEN Turbo',
'qwen-turbo-latest | QWEN Turbo Latest',
'qwen-plus | QWEN Plus',
'qwen-plus-latest | QWEN Plus Latest',
'qwen-long | QWEN Long'
],
'wenxin' => [
'ernie-4.0-8k | Ernie 4.0 8K',
'ernie-4.0-8k-latest | Ernie 4.0 8K Latest',
'ernie-4.0-turbo-128k | Ernie 4.0 Turbo 128K',
'ernie-4.0-turbo-8k | Ernie 4.0 Turbo 8K',
'ernie-3.5-128k | Ernie 3.5 128K',
'ernie-3.5-8k | Ernie 3.5 8K',
'ernie-speed-128k | Ernie Speed 128K',
'ernie-speed-8k | Ernie Speed 8K',
'ernie-lite-8k | Ernie Lite 8K',
'ernie-tiny-8k | Ernie Tiny 8K'
],
default => [],
};
}
/**
* AI模型转数组
* @param $models
* @param bool $retValue
* @return array
*/
public static function AIModels2Array($models, $retValue = false)
{
$list = is_array($models) ? $models : explode("\n", $models);
$array = [];
foreach ($list as $item) {
$arr = Base::newTrim(explode('|', $item . '|'));
if ($arr[0]) {
$array[] = [
'value' => $arr[0],
'label' => $arr[1] ?: $arr[0]
];
}
}
if ($retValue) {
return array_column($array, 'value');
}
return $array;
}
/**
* 验证邮箱地址(过滤忽略地址)
* @param $array
@@ -134,4 +266,36 @@ class Setting extends AbstractModel
}
return $array;
}
/**
* 验证消息限制
* @param $type
* @param $msg
* @return void
*/
public static function validateMsgLimit($type, $msg)
{
$keyName = 'msg_edit_limit';
$error = '此消息不可修改';
if ($type == 'rev') {
$keyName = 'msg_rev_limit';
$error = '此消息不可撤回';
}
$limitNum = intval(Base::settingFind('system', $keyName, 0));
if ($limitNum <= 0) {
return;
}
if ($msg instanceof WebSocketDialogMsg) {
$dialogMsg = $msg;
} else {
$dialogMsg = WebSocketDialogMsg::find($msg);
}
if (!$dialogMsg) {
return;
}
$limitTime = Carbon::parse($dialogMsg->created_at)->addMinutes($limitNum);
if ($limitTime->lt(Carbon::now())) {
throw new ApiException('已超过' . Doo::translate(Base::forumMinuteDay($limitNum)) . '' . $error);
}
}
}

View File

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

View File

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

View File

@@ -83,10 +83,13 @@ class UserBot extends AbstractModel
'approval-alert' => '审批',
'ai-openai' => 'ChatGPT',
'ai-claude' => 'Claude',
'ai-wenxin' => '文心一言',
'ai-qianwen' => '通义千问',
'ai-deepseek' => 'DeepSeek',
'ai-gemini' => 'Gemini',
'ai-grok' => 'Grok',
'ai-ollama' => 'Ollama',
'ai-zhipu' => '智谱清言',
'ai-qianwen' => '通义千问',
'ai-wenxin' => '文心一言',
'bot-manager' => '机器人管理',
'meeting-alert' => '会议通知',
'okr-alert' => 'OKR提醒',
@@ -187,11 +190,35 @@ class UserBot extends AbstractModel
];
default:
if (preg_match('/^ai-(.*?)@bot\.system$/', $email)) {
return [
[
if (preg_match('/^ai-(.*?)@bot\.system$/', $email, $match)) {
if (!Base::judgeClientVersion('0.42.62')) {
return [
'key' => '%3A.clear',
'label' => Doo::translate('清空上下文')
];
}
$aibotSetting = Base::setting('aibotSetting');
$aibotModel = $aibotSetting[$match[1] . '_model'];
$aibotModels = Setting::AIModels2Array($aibotSetting[$match[1] . '_models']);
if (empty($aibotModels)) {
return [];
}
return [
[
'key' => '~ai-model-select',
'label' => Doo::translate('选择模型'),
'config' => [
'model' => $aibotModel,
'models' => $aibotModels
]
],
[
'key' => '~ai-session-create',
'label' => Doo::translate('开启新会话'),
],
[
'key' => '~ai-session-history',
'label' => Doo::translate('历史会话'),
]
];
}
@@ -425,4 +452,39 @@ class UserBot extends AbstractModel
default => [],
};
}
/**
* 创建我的机器人
* @param $userid
* @param $botName
* @return array
*/
public static function newbot($userid, $botName)
{
if (User::select(['users.*'])
->join('user_bots', 'users.userid', '=', 'user_bots.bot_id')
->where('users.bot', 1)
->where('user_bots.userid', $userid)
->count() >= 50) {
return Base::retError("超过最大创建数量。");
}
if (strlen($botName) < 2 || strlen($botName) > 20) {
return Base::retError("机器人名称由2-20个字符组成。");
}
$data = User::botGetOrCreate("user-" . Base::generatePassword(), [
'nickname' => $botName
], $userid);
if (empty($data)) {
return Base::retError("创建失败。");
}
$dialog = WebSocketDialog::checkUserDialog($data, $userid);
if ($dialog) {
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => '/hello',
'title' => '创建成功。',
'data' => $data,
], $data->userid);
}
return Base::retSuccess("创建成功。", $data);
}
}

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();
// 最后消息
@@ -330,6 +334,9 @@ class WebSocketDialog extends AbstractModel
}
break;
}
if (empty($data['pinyin'])) {
$data['pinyin'] = Base::cn2pinyin($data['name']);
}
// 已存在的消息类型
if ($hasData === true) {
@@ -398,6 +405,26 @@ class WebSocketDialog extends AbstractModel
return $data;
}
/**
* 获取对话人数
* @param $dialogId
* @return array
*/
public static function generatePeople($dialogId)
{
$counts = WebSocketDialogUser::whereDialogId($dialogId)
->groupBy('bot')
->selectRaw('bot, COUNT(*) as count')
->pluck('count', 'bot');
$userCount = $counts->get(0, 0); // 非机器人数量
$botCount = $counts->get(1, 0); // 机器人数量
return [
'people' => $userCount + $botCount,
'people_user' => $userCount,
'people_bot' => $botCount,
];
}
/**
* 加入聊天室
* @param int|array $userid 加入的会员ID或会员ID组
@@ -420,7 +447,11 @@ class WebSocketDialog extends AbstractModel
WebSocketDialogUser::updateInsert([
'dialog_id' => $this->id,
'userid' => $value,
], $updateData, [], $isInsert);
], $updateData, function() use ($value) {
return [
'bot' => User::isBot($value) ? 1 : 0,
];
}, $isInsert);
if ($isInsert) {
WebSocketDialogMsg::sendMsg(null, $this->id, 'notice', [
'notice' => User::userid2nickname($value) . " 已加入群组"
@@ -429,10 +460,9 @@ class WebSocketDialog extends AbstractModel
}
}
});
$this->pushMsg("groupUpdate", [
'id' => $this->id,
'people' => WebSocketDialogUser::whereDialogId($this->id)->count()
]);
$data = WebSocketDialog::generatePeople($this->id);
$data['id'] = $this->id;
$this->pushMsg("groupUpdate", $data);
return true;
}
@@ -489,10 +519,9 @@ class WebSocketDialog extends AbstractModel
});
});
//
$this->pushMsg("groupUpdate", [
'id' => $this->id,
'people' => WebSocketDialogUser::whereDialogId($this->id)->count()
]);
$data = WebSocketDialog::generatePeople($this->id);
$data['id'] = $this->id;
$this->pushMsg("groupUpdate", $data);
}
/**
@@ -741,6 +770,17 @@ class WebSocketDialog extends AbstractModel
'dialog_id' => $dialog->id,
'userid' => $receiver,
])->save();
//
if ($user->isAiBot() || User::find($receiver)?->isAiBot()) {
$session = WebSocketDialogSession::create([
'dialog_id' => $dialog->id,
'status' => 1,
'title' => '',
]);
$session->save();
$dialog->session_id = $session->id;
$dialog->save();
}
return $dialog;
});
}

View File

@@ -13,7 +13,7 @@ namespace App\Models;
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\WebSocketDialog|null $dialog
* @property-read \App\Models\User|null $user
* @property-read \App\Models\User $user
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)

View File

@@ -18,6 +18,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property int $id
* @property int|null $dialog_id 对话ID
* @property string|null $dialog_type 对话类型
* @property int|null $session_id 会话ID
* @property int|null $userid 发送会员ID
* @property string|null $type 消息类型
* @property string|null $mtype 消息类型(用于搜索)
@@ -69,6 +70,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereReplyId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereReplyNum($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereSend($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereSessionId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereTag($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereTodo($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereType($value)
@@ -303,7 +305,12 @@ class WebSocketDialogMsg extends AbstractModel
];
//
$dialog = WebSocketDialog::find($this->dialog_id);
$dialog?->pushMsg('update', $resData);
if ($dialog) {
$dialog->pushMsg('update', $resData);
WebSocketDialogUser::whereDialogId($dialog->id)->change([
'updated_at' => Carbon::now()->toDateTimeString('millisecond'),
]);
}
//
return Base::retSuccess('success', $resData);
}
@@ -532,10 +539,6 @@ class WebSocketDialogMsg extends AbstractModel
*/
public function withdrawMsg()
{
$send_dt = Carbon::parse($this->created_at)->addDay();
if ($send_dt->lt(Carbon::now())) {
throw new ApiException('已超过24小时此消息不能撤回');
}
AbstractModel::transaction(function() {
$deleteRead = WebSocketDialogMsgRead::whereMsgId($this->id)->whereNull('read_at')->delete(); // 未阅读记录不需要软删除,直接删除即可
$this->delete();
@@ -592,6 +595,9 @@ class WebSocketDialogMsg extends AbstractModel
case 'text':
return self::previewTextMsg($data['msg'], $preserveHtml);
case 'longtext':
return $data['msg']['desc'] ? Base::cutStr($data['msg']['desc'], 50) : ("[" . Doo::translate("长文本") . "]");
case 'vote':
$action = Doo::translate("投票");
return "[{$action}] " . self::previewTextMsg($data['msg'], $preserveHtml);
@@ -654,6 +660,10 @@ class WebSocketDialogMsg extends AbstractModel
$text = $msgData['text'] ?? '';
if (!$text) return '';
if ($msgData['type'] === 'md') {
$text = preg_replace("/:::\s*reasoning[\s\S]*?:::/", "", $text);
if (preg_match('/:::\s*reasoning\s+/', $text)) {
return Doo::translate('思考中...');
}
$text = Base::markdown2html($text);
$text = self::previewConvertTaskList($text);
}
@@ -753,9 +763,15 @@ class WebSocketDialogMsg extends AbstractModel
$key = '';
switch ($this->type) {
case 'text':
if (!preg_match("/<span[^>]*?data-quick-key=([\"'])([^\"']+?)\\1[^>]*?>/is", $this->msg['text'])) {
$key = strip_tags($this->msg['text']);
if (preg_match("/<span[^>]*?data-quick-key=([\"'])([^\"']+?)\\1[^>]*?>/i", $this->msg['text'])) {
break;
}
$key = $this->msg['text'];
if ($this->msg['type'] === 'md') {
$key = preg_replace("/:::\s*reasoning[\s\S]*?:::/", "", $key);
$key = Base::markdown2html($key);
}
$key = strip_tags($key);
break;
case 'vote':
@@ -774,14 +790,24 @@ class WebSocketDialogMsg extends AbstractModel
break;
}
}
$key = str_replace(["&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
@@ -869,8 +895,16 @@ class WebSocketDialogMsg extends AbstractModel
$imageSaveLocal = Base::settingFind("system", "image_save_local");
preg_match_all("/<img[^>]*?src=([\"'])(.*?(png|jpg|jpeg|webp|gif).*?)\\1[^>]*?>/is", $text, $matchs);
foreach ($matchs[2] as $key => $str) {
$parsed = parse_url($str);
if (str_starts_with($parsed['path'], "/uploads/")) {
$relativePath = ltrim($parsed['path'], "/");
$relativePath = Base::thumbRestore($relativePath);
if (file_exists(public_path($relativePath))) {
$str = "{{RemoteURL}}{$relativePath}";
}
}
if ($imageSaveLocal === 'close') {
$imageSize = getimagesize($str);
$imageSize = @getimagesize($str);
if ($imageSize === false) {
$imageSize = ["auto", "auto"];
}
@@ -905,7 +939,7 @@ class WebSocketDialogMsg extends AbstractModel
}
}
}
// @成员、#任务、~文件
// @成员、#任务、~文件、%报告
preg_match_all("/<span\s+class=\"mention\"(.*?)>.*?<\/span>.*?<\/span>.*?<\/span>/s", $text, $matchs);
foreach ($matchs[1] as $key => $str) {
preg_match("/data-denotation-char=\"(.*?)\"/", $str, $matchChar);
@@ -913,6 +947,7 @@ class WebSocketDialogMsg extends AbstractModel
preg_match("/data-value=\"(.*?)\"/s", $str, $matchValye);
$keyId = $matchId[1];
if ($matchChar[1] === "~") {
// 文件特殊处理
if (Base::isNumber($keyId)) {
$file = File::permissionFind($keyId, User::auth());
if ($file->type == 'folder') {
@@ -928,6 +963,19 @@ class WebSocketDialogMsg extends AbstractModel
throw new ApiException('文件分享错误');
}
}
} elseif ($matchChar[1] === "%") {
// 报告特殊处理
if (Base::isNumber($keyId)) {
$reportLink = ReportLink::generateLink($keyId, User::userid());
$keyId = $reportLink['code'];
} else {
preg_match("/\/single\/report\/detail\/(.*?)$/i", $keyId, $match);
if ($match && strlen($match[1]) >= 8) {
$keyId = $match[1];
} else {
throw new ApiException('报告分享错误');
}
}
}
$text = str_replace($matchs[0][$key], "[:{$matchChar[1]}:{$keyId}:{$matchValye[1]}:]", $text);
}
@@ -956,31 +1004,18 @@ class WebSocketDialogMsg extends AbstractModel
foreach ($matchs[0] as $key => $str) {
$herf = $matchs[2][$key];
$title = $matchs[3][$key] ?: $herf;
preg_match("/\/single\/file\/(.*?)$/i", strip_tags($title), $match);
if ($match && strlen($match[1]) >= 8) {
$file = File::select(['files.id', 'files.name', 'files.ext'])->join('file_links as L', 'files.id', '=', 'L.file_id')->where('L.code', $match[1])->first();
if ($file && $file->name) {
$name = $file->ext ? "{$file->name}.{$file->ext}" : $file->name;
$text = str_replace($str, "[:~:{$match[1]}:{$name}:]", $text);
continue;
}
if (self::formatLink($str, strip_tags($title), $text)) {
continue;
}
$herf = base64_encode($herf);
$title = base64_encode($title);
$text = str_replace($str, "[:LINK:{$herf}:{$title}:]", $text);
}
// 文件分享链接
// 分享链接
preg_match_all("/(https?:\/\/)((\w|=|\?|\.|\/|&|-|:|\+|%|;|#|@|,|!)+)/i", $text, $matchs);
if ($matchs) {
foreach ($matchs[0] as $str) {
preg_match("/\/single\/file\/(.*?)$/i", $str, $match);
if ($match && strlen($match[1]) >= 8) {
$file = File::select(['files.id', 'files.name', 'files.ext'])->join('file_links as L', 'files.id', '=', 'L.file_id')->where('L.code', $match[1])->first();
if ($file && $file->name) {
$name = $file->ext ? "{$file->name}.{$file->ext}" : $file->name;
$text = str_replace($str, "[:~:{$match[1]}:{$name}:]", $text);
}
}
self::formatLink($str, $str, $text);
}
}
// 过滤标签
@@ -1010,10 +1045,41 @@ class WebSocketDialogMsg extends AbstractModel
$text = preg_replace("/\[:@:(.*?):(.*?):\]/i", "<span class=\"mention user\" data-id=\"$1\">@$2</span>", $text);
$text = preg_replace("/\[:#:(.*?):(.*?):\]/is", "<span class=\"mention task\" data-id=\"$1\">#$2</span>", $text);
$text = preg_replace("/\[:~:(.*?):(.*?):\]/i", "<a class=\"mention file\" href=\"{{RemoteURL}}single/file/$1\" target=\"_blank\">~$2</a>", $text);
$text = preg_replace("/\[:%:(.*?):(.*?):\]/i", "<a class=\"mention report\" href=\"{{RemoteURL}}single/report/detail/$1\" target=\"_blank\">%$2</a>", $text);
$text = preg_replace("/\[:QUICK:(.*?):(.*?):\]/i", "<span data-quick-key=\"$1\">$2</span>", $text);
return preg_replace("/^(<p><\/p>)+|(<p><\/p>)+$/i", "", $text);
}
/**
* 链接转换处理
* @param $search
* @param $subject
* @param $content
* @return bool
*/
public static function formatLink($search, $subject, &$content)
{
$ret = false;
preg_match("/\/single\/file\/(.*?)$/i", $subject, $match);
if ($match && strlen($match[1]) >= 8) {
$file = File::select(['files.id', 'files.name', 'files.ext'])->join('file_links as L', 'files.id', '=', 'L.file_id')->where('L.code', $match[1])->first();
if ($file && $file->name) {
$name = $file->ext ? "{$file->name}.{$file->ext}" : $file->name;
$content = str_replace($search, "[:~:{$match[1]}:{$name}:]", $content);
$ret = true;
}
}
preg_match("/\/single\/report\/detail\/(.*?)$/i", $subject, $match);
if ($match && strlen($match[1]) >= 8) {
$report = Report::select(['reports.id', 'reports.title'])->join('report_links as L', 'reports.id', '=', 'L.rid')->where('L.code', $match[1])->first();
if ($report && $report->title) {
$content = str_replace($search, "[:%:{$match[1]}:{$report->title}:]", $content);
$ret = true;
}
}
return $ret;
}
/**
* 发送消息、修改消息
* @param string $action 动作
@@ -1123,6 +1189,7 @@ class WebSocketDialogMsg extends AbstractModel
}
//
$updateData = [
'type' => $type,
'mtype' => $mtype,
'link' => $link,
'msg' => array_merge($oldMsg, $msg),
@@ -1165,6 +1232,7 @@ class WebSocketDialogMsg extends AbstractModel
$dialogMsg = self::createInstance([
'dialog_id' => $dialog_id,
'dialog_type' => $dialog->type,
'session_id' => $dialog->session_id,
'reply_id' => $reply_id,
'forward_id' => $forward_id,
'userid' => $sender,
@@ -1179,6 +1247,8 @@ class WebSocketDialogMsg extends AbstractModel
$dialogMsg->send = 1;
$dialogMsg->generateKeyAndSave($search_key);
//
WebSocketDialogSession::updateTitle($dialogMsg->session_id, $dialogMsg);
//
if ($dialogMsg->type === 'meeting') {
MeetingMsg::createInstance([
'meetingid' => $dialogMsg->msg['meetingid'],
@@ -1210,6 +1280,55 @@ class WebSocketDialogMsg extends AbstractModel
}
}
/**
* 批量发送消息
* @param User $user 发送的会员
* @param array $userids 接收的会员ID
* @param array $dialogids 接收的会话ID
* @param string $msgText 发送的消息
* @return array
*/
public static function sendMsgBatch($user, $userids, $dialogids, $msgText)
{
return AbstractModel::transaction(function() use ($user, $userids, $dialogids, $msgText) {
$msgs = [];
$already = [];
if ($dialogids) {
if (!is_array($dialogids)) {
$dialogids = [$dialogids];
}
foreach ($dialogids as $dialogid) {
$res = WebSocketDialogMsg::sendMsg(null, $dialogid, 'text', ['text' => $msgText], $user->userid);
if (Base::isSuccess($res)) {
$msgs[] = $res['data'];
$already[] = $dialogid;
}
}
}
if ($userids) {
if (!is_array($userids)) {
$userids = [$userids];
}
foreach ($userids as $userid) {
if (!User::whereUserid($userid)->exists()) {
continue;
}
$dialog = WebSocketDialog::checkUserDialog($user, $userid);
if ($dialog && !in_array($dialog->id, $already)) {
$res = WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $msgText], $user->userid);
if (Base::isSuccess($res)) {
$msgs[] = $res['data'];
}
}
}
}
return Base::retSuccess('发送成功', [
'msgs' => $msgs
]);
});
}
/**
* 将被@的人加入群
* @param WebSocketDialog $dialog 对话

View File

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

View File

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

View File

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

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

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

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

View File

@@ -2,19 +2,24 @@
namespace App\Tasks;
use App\Models\FileContent;
use App\Models\Project;
use App\Models\ProjectTask;
use App\Models\Report;
use App\Models\User;
use App\Models\UserBot;
use App\Models\UserDepartment;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogConfig;
use App\Models\WebSocketDialogMsg;
use App\Module\Base;
use App\Module\Doo;
use App\Module\Ihttp;
use App\Module\TextExtractor;
use Carbon\Carbon;
use DB;
use Exception;
use League\HTMLToMarkdown\HtmlConverter;
use DB;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
@@ -86,8 +91,16 @@ class BotReceiveMsgTask extends AbstractTask
}
// 提取指令
$command = $this->extractCommand($msg, $this->mention);
if (empty($command)) {
try {
$command = $this->extractCommand($msg, $botUser->isAiBot(), $this->mention);
if (empty($command)) {
return;
}
} catch (Exception $e) {
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'template', [
'type' => 'content',
'content' => $e->getMessage() ?: "指令解析失败。",
], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
return;
}
@@ -195,32 +208,11 @@ class BotReceiveMsgTask extends AbstractTask
* 创建
*/
case '/newbot':
if (User::select(['users.*'])
->join('user_bots', 'users.userid', '=', 'user_bots.bot_id')
->where('users.bot', 1)
->where('user_bots.userid', $msg->userid)
->count() >= 50) {
$content = "超过最大创建数量。";
break;
}
if (strlen($array[1]) < 2 || strlen($array[1]) > 20) {
$content = "机器人名称由2-20个字符组成。";
break;
}
$data = User::botGetOrCreate("user-" . Base::generatePassword(), [
'nickname' => $array[1]
], $msg->userid);
if (empty($data)) {
$content = "创建失败。";
break;
}
$dialog = WebSocketDialog::checkUserDialog($data, $msg->userid);
if ($dialog) {
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => '/hello',
'title' => '创建成功。',
'data' => $data,
], $data->userid); // todo 未能在任务end事件来发送任务
$res = UserBot::newbot($msg->userid, $array[1]);
if (Base::isError($res)) {
$content = $res['msg'];
} else {
$data = $res['data'];
}
break;
@@ -411,14 +403,13 @@ class BotReceiveMsgTask extends AbstractTask
*/
private function botWebhookBusiness(string $command, WebSocketDialogMsg $msg, User $botUser, WebSocketDialog $dialog)
{
$serverUrl = 'http://' . env('APP_IPPR') . '.3';
$serverUrl = 'http://nginx';
$userBot = null;
$extras = [];
$errorContent = null;
if (preg_match('/^ai-(.*?)@bot\.system$/', $botUser->email, $matches)) {
if ($botUser->isAiBot($type)) {
// AI机器人
$setting = Base::setting('aibotSetting');
$type = $matches[1];
$extras = [
'model_type' => match ($type) {
'qianwen' => 'qwen',
@@ -427,33 +418,85 @@ class BotReceiveMsgTask extends AbstractTask
'model_name' => $setting[$type . '_model'],
'system_message' => $setting[$type . '_system'],
'api_key' => $setting[$type . '_key'],
'base_url' => $setting[$type . '_base_url'],
'agency' => $setting[$type . '_agency'],
'server_url' => $serverUrl,
];
if ($setting[$type . '_temperature']) {
$extras['temperature'] = floatval($setting[$type . '_temperature']);
}
if ($msg->msg['model_name']) {
$extras['model_name'] = $msg->msg['model_name'];
}
if (preg_match("/(.*?)(\s+|\s*[_-]\s*)(think|thinking|reasoning)\s*$/", $extras['model_name'], $match)) {
$extras['model_name'] = $match[1];
$extras['max_tokens'] = 20000;
$extras['thinking'] = 4096;
$extras['temperature'] = 1.0;
}
if ($dialog->session_id) {
$extras['context_key'] = 'session_' . $dialog->session_id;
}
if ($type === 'wenxin') {
$extras['api_key'] .= ':' . $setting['wenxin_secret'];
}
if ($type === 'ollama') {
if (empty($extras['base_url'])) {
$errorContent = '机器人未启用。';
}
if (empty($extras['api_key'])) {
$extras['api_key'] = Base::strRandom(6);
}
}
if (empty($extras['api_key'])) {
$errorContent = '机器人未启用。';
}
if (in_array($this->client['platform'], ['win', 'mac', 'web']) && !Base::judgeClientVersion("0.41.11", $this->client['version'])) {
$errorContent = '当前客户端版本低所需版本≥v0.41.11)。';
}
if ($msg->reply_id > 0) {
$replyMsg = WebSocketDialogMsg::find($msg->reply_id);
$replyCommand = '';
$replyCommand = null;
if ($replyMsg) {
$replyCommand = $this->extractCommand($replyMsg);
if ($replyCommand) {
$replyCommand = Base::cutStr($replyCommand, 200) . "\n\n ------------------ Reference above ------------------ \n\n";
switch ($replyMsg->type) {
case 'text':
try {
$replyCommand = $this->extractCommand($replyMsg, true);
} catch (Exception) {
$errorContent = "引用消息解析失败。";
}
break;
case 'file':
$msgData = Base::json2array($replyMsg->getRawOriginal('msg'));
$fileResult = TextExtractor::extractFile(public_path($msgData['path']));
if (Base::isError($fileResult)) {
$errorContent = $fileResult['msg'];
} else {
$replyCommand = $fileResult['data'];
}
break;
}
}
$command = $replyCommand . $command;
if ($replyCommand) {
$command = <<<EOF
<quoted_content>
{$replyCommand}
</quoted_content>
The content within the above quoted_content tags is a citation.
{$command}
EOF;
}
}
$this->AIGenerateSystemMessageOrBeforeText($msg->userid, $dialog, $extras);
$this->AIGenerateSystemMessage($msg->userid, $dialog, $extras);
$webhookUrl = "{$serverUrl}/ai/chat";
} else {
// 用户机器人
if (str_starts_with($command, '/')) {
return;
}
$userBot = UserBot::whereBotId($botUser->userid)->first();
$webhookUrl = $userBot?->webhook_url;
}
@@ -532,15 +575,18 @@ class BotReceiveMsgTask extends AbstractTask
/**
* 提取消息指令(提取消息内容)
* @param WebSocketDialogMsg $msg
* @param bool $isAiBot
* @param bool $mention
* @return string
* @throws Exception
*/
private function extractCommand(WebSocketDialogMsg $msg, bool $mention = false)
private function extractCommand(WebSocketDialogMsg $msg, bool $isAiBot = false, bool $mention = false)
{
if ($msg->type !== 'text') {
return '';
}
$original = $msg->msg['text'];
$original = $msg->msg['text'] ?: '';
if ($mention) {
$original = preg_replace("/<span class=\"mention user\" data-id=\"(\d+)\">(.*?)<\/span>/", "", $original);
}
@@ -549,26 +595,94 @@ class BotReceiveMsgTask extends AbstractTask
if (str_starts_with($command, '%3A.')) {
$command = ":" . substr($command, 4);
}
} else {
$command = trim(strip_tags($original));
return $command;
}
if (empty($command)) {
return '';
if (!$isAiBot) {
return trim(strip_tags($original));
}
return $command;
$contents = [];
// 任务
if (preg_match_all("/<span class=\"mention task\" data-id=\"(\d+)\">(.*?)<\/span>/", $original, $match)) {
$taskIds = Base::newIntval($match[1]);
foreach ($taskIds as $index => $taskId) {
$taskInfo = ProjectTask::with(['content'])->whereId($taskId)->first();
if (!$taskInfo) {
throw new Exception("任务不存在或已被删除");
}
$taskName = addslashes($taskInfo->name) . " (ID:{$taskId})";
$taskContext = implode("\n", $taskInfo->AIContext());
$contents[] = "<task_content path=\"{$taskName}\">\n{$taskContext}\n</task_content>";
$original = str_replace($match[0][$index], "'{$taskName}' (see below for task_content tag)", $original);
}
}
// 文件、报告
if (preg_match_all("/<a class=\"mention ([^'\"]*)\" href=\"([^\"']+?)\"[^>]*?>[~%]([^>]*)<\/a>/", $original, $match)) {
$urlPaths = $match[2];
foreach ($urlPaths as $index => $urlPath) {
$pathTag = null;
$pathName = null;
$pathContent = null;
// 文件
if (preg_match("/single\/file\/(.*?)$/", $urlPath, $fileMatch)) {
$fileInfo = FileContent::idOrCodeToContent($fileMatch[1]);
if (!$fileInfo || !isset($fileInfo->content['url'])) {
throw new Exception("文件不存在或已被删除");
}
$urlPath = public_path($fileInfo->content['url']);
if (!file_exists($urlPath)) {
throw new Exception("文件不存在或已被删除");
}
$fileResult = TextExtractor::extractFile($urlPath);
if (Base::isError($fileResult)) {
throw new Exception("文件读取失败:" . $fileResult['msg']);
}
$pathTag = "file_content";
$pathName = addslashes($match[3][$index]) . " (ID:{$fileInfo->id})";
$pathContent = $fileResult['data'];
}
// 报告
elseif (preg_match("/single\/report\/detail\/(.*?)$/", $urlPath, $reportMatch)) {
$reportInfo = Report::idOrCodeToContent($reportMatch[1]);
if (!$reportInfo) {
throw new Exception("报告不存在或已被删除");
}
$pathTag = "report_content";
$pathName = addslashes($match[3][$index]) . " (ID:{$reportInfo->id})";
$pathContent = $reportInfo->content;
}
if ($pathTag) {
$contents[] = "<{$pathTag} path=\"{$pathName}\">\n{$pathContent}\n</{$pathTag}>";
$original = str_replace($match[0][$index], "'{$pathName}' (see below for {$pathTag} tag)", $original);
}
}
}
if ($msg->msg['type'] !== 'md') {
// 转换为Markdown
try {
$converter = new HtmlConverter();
$original = $converter->convert($original);
} catch (\Exception) {
throw new Exception("Failed to convert HTML to Markdown");
}
}
if ($contents) {
// 添加tag内容
$original .= "\n\n" . implode("\n\n", $contents);
}
return $original ?: '';
}
/**
* 生成AI系统提示词或前置消息
* 生成AI系统提示词
* @param int|null $userid
* @param WebSocketDialog $dialog
* @param array $extras
* @return void
*/
private function AIGenerateSystemMessageOrBeforeText(int|null $userid, WebSocketDialog $dialog, array &$extras)
private function AIGenerateSystemMessage(int|null $userid, WebSocketDialog $dialog, array &$extras)
{
$system_message = null;
$before_text = [];
$system_messages = [];
switch ($dialog->type) {
case "user":
$aiPrompt = WebSocketDialogConfig::where([
@@ -577,7 +691,7 @@ class BotReceiveMsgTask extends AbstractTask
'type' => 'ai_prompt',
])->value('value');
if ($aiPrompt) {
$system_message = $aiPrompt;
$extras['system_message'] = $aiPrompt;
}
break;
case "group":
@@ -585,14 +699,16 @@ class BotReceiveMsgTask extends AbstractTask
case 'user':
break;
case 'project':
$projectInfo = Project::select(['id', 'name', 'archived_at', 'deleted_at'])->whereDialogId($dialog->id)->first();
$projectInfo = Project::whereDialogId($dialog->id)->first();
if ($projectInfo) {
$before_text[] = "当前我在项目【{$projectInfo->name}】中";
if ($projectInfo->desc) {
$before_text[] = "项目描述:{$projectInfo->desc}";
}
$before_text[] = <<<EOF
如果你判断我想要添加任务,请按照以下格式回复:
$projectDesc = $projectInfo->desc ?: "-";
$projectStatus = $projectInfo->archived_at ? '已归档' : '正在进行中';
$system_messages[] = <<<EOF
当前我在项目【{$projectInfo->name}】中
项目描述:{$projectDesc}
项目状态:{$projectStatus}
如果你判断我想要或需要添加任务,请按照以下格式回复:
::: create-task-list
title: 任务标题1
@@ -605,24 +721,16 @@ class BotReceiveMsgTask extends AbstractTask
}
break;
case 'task':
$taskInfo = ProjectTask::with(['content'])->select(['id', 'name', 'complete_at', 'archived_at', 'deleted_at'])->whereDialogId($dialog->id)->first();
$taskInfo = ProjectTask::with(['content'])->whereDialogId($dialog->id)->first();
if ($taskInfo) {
$before_text[] = "当前我在任务【{$taskInfo->name}】中";
if ($taskInfo->content) {
$taskDesc = $taskInfo->content?->getContentInfo();
if ($taskDesc) {
$converter = new HtmlConverter(['strip_tags' => true]);
$descContent = Base::cutStr($converter->convert($taskDesc['content']), 2000);
$before_text[] = <<<EOF
任务描述
```md
{$descContent}
```
EOF;
}
}
$before_text[] = <<<EOF
如果你判断我想要添加子任务,请按照以下格式回复:
$taskContext = implode("\n", $taskInfo->AIContext());
$system_messages[] = <<<EOF
当前我在任务【{$taskInfo->name}】中
当前时间:{$taskInfo->updated_at}
任务ID{$taskInfo->id}
{$taskContext}
如果你判断我想要或需要添加子任务,请按照以下格式回复
::: create-subtask-list
title: 子任务标题1
@@ -631,17 +739,23 @@ class BotReceiveMsgTask extends AbstractTask
EOF;
}
break;
case 'department':
$userDepartment = UserDepartment::whereDialogId($dialog->id)->first();
if ($userDepartment) {
$system_messages[] = "当前我在【{$userDepartment->name}】的部门群聊中";
}
break;
case 'all':
$before_text[] = "当前我团队【全体成员】的群聊中";
$system_messages[] = "当前我【全体成员】的群聊中";
break;
}
break;
}
if ($system_message) {
$extras['system_message'] = $system_message;
if ($extras['system_message']) {
array_unshift($system_messages, $extras['system_message']);
}
if ($before_text) {
$extras['before_text'] = Base::newTrim($before_text);
if ($system_messages) {
$extras['system_message'] = implode("\n\n====\n\n", Base::newTrim($system_messages));
}
}
}

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

@@ -119,14 +119,20 @@ class WebSocketDialogMsgTask extends AbstractTask
$mention = array_intersect([0, $userid], $mentions) ? 1 : 0;
$silence = $mention ? false : $silence;
$dot = $msg->type === 'record' ? 1 : 0;
WebSocketDialogMsgRead::createInstance([
$msgRead = WebSocketDialogMsgRead::createInstance([
'dialog_id' => $msg->dialog_id,
'msg_id' => $msg->id,
'userid' => $userid,
'mention' => $mention,
'silence' => $silence,
'dot' => $dot,
])->saveOrIgnore();
]);
if ($msgRead->saveOrIgnore()) {
if ($dialog->session_id && $dialog->session_id != $msg->session_id) {
$msgRead->read_at = Carbon::now();
$msgRead->save();
}
}
$array[$userid] = [
'userid' => $userid,
'mention' => $mention,

View File

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

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",
@@ -34,7 +37,10 @@
"mews/captcha": "^3.2.6",
"orangehill/iseed": "^3.0.1",
"overtrue/pinyin": "^4.0",
"phpoffice/phppresentation": "^1.1",
"phpoffice/phpword": "^1.3",
"predis/predis": "^1.1.7",
"smalot/pdfparser": "^2.11",
"symfony/mailer": "^6.0"
},
"require-dev": {
@@ -83,7 +89,10 @@
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true
"sort-packages": true,
"allow-plugins": {
"php-http/discovery": true
}
},
"minimum-stability": "dev",
"prefer-stable": true,

1451
composer.lock generated

File diff suppressed because it is too large Load Diff

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"
@@ -73,6 +71,11 @@ services:
MYSQL_DATABASE: "${DB_DATABASE}"
MYSQL_USER: "${DB_USERNAME}"
MYSQL_PASSWORD: "${DB_PASSWORD}"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "${DB_USERNAME}", "-p${DB_PASSWORD}"]
interval: 5s
timeout: 5s
retries: 5
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.5"
@@ -84,7 +87,9 @@ services:
volumes:
- ./docker/office/logs:/var/log/onlyoffice
- ./docker/office/data:/var/www/onlyoffice/Data
- ./docker/office/etc/documentserver/default.json:/etc/onlyoffice/documentserver/default.json
- ./docker/office/resources/require.js:/var/www/onlyoffice/documentserver/web-apps/vendor/requirejs/require.js
- ./docker/office/resources/common/main/resources/img/header:/var/www/onlyoffice/documentserver/web-apps/apps/common/main/resources/img/header
- ./docker/office/resources/documenteditor/main/resources/css/app.css:/var/www/onlyoffice/documentserver/web-apps/apps/documenteditor/main/resources/css/app.css
- ./docker/office/resources/documenteditor/mobile/css/526.caf35c11a8d72ca5ac85.css:/var/www/onlyoffice/documentserver/web-apps/apps/documenteditor/mobile/css/526.caf35c11a8d72ca5ac85.css
- ./docker/office/resources/presentationeditor/main/resources/css/app.css:/var/www/onlyoffice/documentserver/web-apps/apps/presentationeditor/main/resources/css/app.css
@@ -100,7 +105,7 @@ services:
fileview:
container_name: "dootask-fileview-${APP_ID}"
image: "kuaifan/fileview:4.2.0-SNAPSHOT-RC25"
image: "kuaifan/fileview:4.4.0-3"
environment:
KK_CONTEXT_PATH: "/fileview"
KK_OFFICE_PREVIEW_SWITCH_DISABLED: true
@@ -123,8 +128,6 @@ services:
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.8"
depends_on:
- drawio-export
restart: unless-stopped
drawio-export:
@@ -161,21 +164,18 @@ services:
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.11"
depends_on:
- mariadb
restart: unless-stopped
ai:
container_name: "dootask-ai-${APP_ID}"
image: "kuaifan/dootask-ai:0.2.0"
image: "kuaifan/dootask-ai:0.3.5"
environment:
REDIS_HOST: "${REDIS_HOST}"
REDIS_PORT: "${REDIS_PORT}"
TIMEOUT: 600
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.12"
depends_on:
- redis
restart: unless-stopped
okr:
@@ -183,7 +183,7 @@ services:
image: "kuaifan/doookr:0.4.5"
environment:
TZ: "${TIMEZONE:-PRC}"
DOO_TASK_URL: "http://${APP_IPPR}.3"
DOO_TASK_URL: "http://nginx"
MYSQL_HOST: "${DB_HOST}"
MYSQL_PORT: "${DB_PORT}"
MYSQL_DBNAME: "${DB_DATABASE}"
@@ -195,8 +195,6 @@ services:
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.13"
depends_on:
- mariadb
restart: unless-stopped
face:
@@ -213,14 +211,27 @@ services:
MYSQL_PASSWORD: "${DB_PASSWORD}"
MYSQL_DB_NAME: "${DB_DATABASE}"
DB_PREFIX: "${DB_PREFIX}"
REPORT_API: "http://${APP_IPPR}.3:80/api/public/checkin/report"
depends_on:
- mariadb
REPORT_API: "http://nginx/api/public/checkin/report"
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.14"
restart: unless-stopped
es:
container_name: "dootask-es-${APP_ID}"
image: "elasticsearch:8.17.2"
volumes:
- ./docker/es/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
- ./docker/es/data:/usr/share/elasticsearch/data
environment:
discovery.type: single-node
xpack.security.enabled: false
ES_JAVA_OPTS: "-Xms1g -Xmx1g"
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.15"
restart: unless-stopped
networks:
extnetwork:
name: "dootask-networks-${APP_ID}"

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,35 @@
location /drawio/webapp/ {
proxy_http_version 1.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-Host $the_host/drawio/webapp;
proxy_set_header X-Forwarded-Proto $the_scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header Server-Protocol $server_protocol;
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://drawio-webapp:8080/;
}
location /drawio/export/ {
proxy_http_version 1.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-Host $the_host/drawio/export;
proxy_set_header X-Forwarded-Proto $the_scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header Server-Protocol $server_protocol;
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://drawio-export:8000/;
}

View File

@@ -0,0 +1,16 @@
location /fileview {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-Host $the_host;
proxy_set_header X-Forwarded-Proto $the_scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header Server-Protocol $server_protocol;
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
proxy_pass http://fileview:8012;
}

View File

@@ -0,0 +1,16 @@
location /minder/ {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-Host $the_host;
proxy_set_header X-Forwarded-Proto $the_scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header Server-Protocol $server_protocol;
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
proxy_pass http://minder/;
}

View File

@@ -0,0 +1,20 @@
location /office/ {
proxy_http_version 1.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-Host $the_host/office;
proxy_set_header X-Forwarded-Proto $the_scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header Server-Protocol $server_protocol;
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 3600s;
proxy_pass http://office/;
}

View File

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

View File

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

View File

@@ -0,0 +1,508 @@
{
"statsd": {
"useMetrics": false,
"host": "localhost",
"port": "8125",
"prefix": "ds."
},
"log": {
"filePath": "",
"options": {
"replaceConsole": true
}
},
"queue": {
"type": "rabbitmq",
"visibilityTimeout": 300,
"retentionPeriod": 900
},
"email": {
"smtpServerConfiguration": {
"host": "localhost",
"port": 587,
"auth": {
"user": "onlyoffice",
"pass": "onlyoffice"
}
},
"connectionConfiguration": {
"disableFileAccess": false,
"disableUrlAccess": false
},
"contactDefaults": {
"from": "from@example.com",
"to": "to@example.com"
}
},
"notification": {
"rules": {
"licenseExpirationWarning": {
"enable": false,
"transportType": [
"email"
],
"template": {
"title": "%s Docs license expiration warning",
"body": "Attention! Your license is about to expire on %s.\nUpon reaching this date, you will no longer be entitled to receive personal technical support and install new Docs versions released after this date."
},
"policies": {
"repeatInterval": "1d"
}
},
"licenseExpirationError": {
"enable": false,
"transportType": [
"email"
],
"template": {
"title": "%s Docs license expiration warning",
"body": "Attention! Your license expired on %s.\nYou are no longer entitled to receive personal technical support and install new Docs versions released after this date.\nPlease contact sales@onlyoffice.com to discuss license renewal."
},
"policies": {
"repeatInterval": "1d"
}
},
"licenseLimitEdit": {
"enable": false,
"transportType": [
"email"
],
"template": {
"title": "%s Docs license connection limit warning",
"body": "Attention! You have reached %s%% of the %s limit set by your license."
},
"policies": {
"repeatInterval": "1h"
}
},
"licenseLimitLiveViewer": {
"enable": false,
"transportType": [
"email"
],
"template": {
"title": "%s Docs license connection limit warning",
"body": "Attention! You have reached %s%% of the live viewer %s limit set by your license."
},
"policies": {
"repeatInterval": "1h"
}
}
}
},
"storage": {
"name": "storage-fs",
"fs": {
"folderPath": "",
"urlExpires": 900,
"secretString": "verysecretstring"
},
"region": "",
"endpoint": "http://localhost/s3",
"bucketName": "cache",
"storageFolderName": "files",
"cacheFolderName": "data",
"urlExpires": 604800,
"accessKeyId": "AKID",
"secretAccessKey": "SECRET",
"sslEnabled": false,
"s3ForcePathStyle": true,
"externalHost": ""
},
"persistentStorage": {
},
"rabbitmq": {
"url": "amqp://guest:guest@localhost:5672",
"socketOptions": {},
"exchangepubsub": "ds.pubsub",
"queueconverttask": "ds.converttask",
"queueconvertresponse": "ds.convertresponse",
"exchangeconvertdead": "ds.exchangeconvertdead",
"queueconvertdead": "ds.convertdead",
"queuedelayed": "ds.delayed"
},
"activemq": {
"connectOptions": {
"port": 5672,
"host": "localhost",
"reconnect": false
},
"queueconverttask": "ds.converttask",
"queueconvertresponse": "ds.convertresponse",
"queueconvertdead": "ActiveMQ.DLQ",
"queuedelayed": "ds.delayed",
"topicpubsub": "ds.pubsub"
},
"dnscache": {
"enable" : true,
"ttl" : 300,
"cachesize" : 1000
},
"openpgpjs": {
"config": {
},
"encrypt": {
"passwords": ["verysecretstring"]
},
"decrypt": {
"passwords": ["verysecretstring"]
}
},
"aesEncrypt": {
"config": {
"keyByteLength": 32,
"saltByteLength": 64,
"initializationVectorByteLength": 16,
"iterationsByteLength": 5
},
"secret": "verysecretstring"
},
"bottleneck": {
"getChanges": {
}
},
"win-ca": {
"inject": "+"
},
"wopi": {
"enable": false,
"host" : "",
"htmlTemplate" : "../../web-apps/apps/api/wopi",
"wopiZone" : "external-http",
"favIconUrlWord" : "/web-apps/apps/documenteditor/main/resources/img/favicon.ico",
"favIconUrlCell" : "/web-apps/apps/spreadsheeteditor/main/resources/img/favicon.ico",
"favIconUrlSlide" : "/web-apps/apps/presentationeditor/main/resources/img/favicon.ico",
"favIconUrlPdf" : "/web-apps/apps/pdfeditor/main/resources/img/favicon.ico",
"fileInfoBlockList" : ["FileUrl"],
"pdfView": ["djvu", "xps", "oxps"],
"pdfEdit": ["pdf"],
"forms": ["pdf"],
"wordView": ["doc", "dotm", "dot", "fodt", "ott", "rtf", "mht", "mhtml", "html", "htm", "xml", "epub", "fb2", "sxw", "stw", "wps", "wpt", "docxf", "oform"],
"wordEdit": ["docx", "dotx", "docm", "odt", "txt"],
"cellView": ["xls", "xlsb", "xltm", "xlt", "fods", "ots", "sxc", "xml", "et", "ett"],
"cellEdit": ["xlsx", "xltx", "xlsm", "ods", "csv"],
"slideView": ["ppt", "ppsx", "ppsm", "pps", "potm", "pot", "fodp", "otp", "sxi", "dps", "dpt"],
"slideEdit": ["pptx", "potx", "pptm", "odp"],
"publicKey": "BgIAAACkAABSU0ExAAgAAAEAAQBpTpiJQ2hD8plpGTfEEmcq4IKyr31HikXpuVSBraMfqyodn2PGXBJ3daNSmdPOc0Nz4HO9Auljn8YYXDPBdpiABptSKvEDPF23Q+Qytg0+vCRyondyBcW91w7KLzXce3fnk8ZfJ8QtbZPL9m11wJIWZueQF+l0HKYx4lty+nccbCanytFTADkGQ3SnmExGEF3rBz6I9+OcrDDK9NKPJgEmCiuyei/d4XbPgKls3EIG0h38X5mVF2VytfWm2Yu850B6z3N4MYhj4b4vsYT62zEC4pMRUeb8dIBy4Jsmr3avtmeO00MUH6DVyPC8nirixj2YIOPKk13CdVqGDSXA3cvl",
"modulus": "5cvdwCUNhlp1wl2TyuMgmD3G4iqevPDI1aAfFEPTjme2r3avJpvgcoB0/OZREZPiAjHb+oSxL77hY4gxeHPPekDnvIvZpvW1cmUXlZlf/B3SBkLcbKmAz3bh3S96sisKJgEmj9L0yjCsnOP3iD4H610QRkyYp3RDBjkAU9HKpyZsHHf6clviMaYcdOkXkOdmFpLAdW32y5NtLcQnX8aT53d73DUvyg7XvcUFcneiciS8Pg22MuRDt108A/EqUpsGgJh2wTNcGMafY+kCvXPgc0NzztOZUqN1dxJcxmOfHSqrH6OtgVS56UWKR32vsoLgKmcSxDcZaZnyQ2hDiZhOaQ==",
"exponent": 65537,
"privateKey": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDly93AJQ2GWnXC\nXZPK4yCYPcbiKp688MjVoB8UQ9OOZ7avdq8mm+BygHT85lERk+ICMdv6hLEvvuFj\niDF4c896QOe8i9mm9bVyZReVmV/8HdIGQtxsqYDPduHdL3qyKwomASaP0vTKMKyc\n4/eIPgfrXRBGTJindEMGOQBT0cqnJmwcd/pyW+Ixphx06ReQ52YWksB1bfbLk20t\nxCdfxpPnd3vcNS/KDte9xQVyd6JyJLw+DbYy5EO3XTwD8SpSmwaAmHbBM1wYxp9j\n6QK9c+BzQ3PO05lSo3V3ElzGY58dKqsfo62BVLnpRYpHfa+yguAqZxLENxlpmfJD\naEOJmE5pAgMBAAECggEALiL+RKOr0Xu8BOgQ0j1DwA03LxVrhXe6etmJI+JySTcd\ngKENjWziZVrRIi2DvUm5qMMl7WhSwslKK1eexxZJY7xASqSxcEoIwgz17T07/jxm\nfIdUBiUKDZ1Kv8PWmIr3oKW+fkXWi/m1zlIe0qXRpTmsGNEsHQLEqi0rmaiXTXOR\n/2Ldwi6kZR3sWFx97YS4Mx/pueGJTXEai6AVEZzN5Gog6xD8HXR1Rvq+hhd+MocG\nfnU4HgilKRfoJlWd9FOscgSufKG0L3ViO4fSKU46l5aullDYUk5ECMWiwuKSqSE7\nqD45jI3mbOre7S4u3S3TWdD3lzwiXL49LdwKlEC4mQKBgQD0sLr0GH4Wr+QX2xJE\nuA/Cb8QW41l8iSCBTRZZR/sJOd+o3rbcVidlzO/EbZblXG4ZPDmRjgBCGKIP5EZi\n0DsL+Wv32WOo44LpxJGhqExbm0H1iZ1zZ97l0P8fvIhHE42gmaLToOIGDhPSXGvv\nzlqOHbGbq4jsERc1jp1bej5q6wKBgQDwaueIc4pRchH98QYidcyr8Vwg9KhbnfYX\ny3W4RPlZtBdF34iJaio+ASzugo/zy1RTcVrsCskYWXyKDUQz1yu0iCng+fDCUnTm\nXGmEoEGNhk4vTJOt7hBav1/Ja/dUipGf6mXUuanwJ0e+1/Et/B0ah5X1Um5AyNZI\nM+SyRz3u+wKBgQCjvtUNXoqaghCBCmB6TjZ1prexnWkYFugCv2SSUMIk1W7gIlJ6\ntsjcrj1R1Qii6qzfBFd+GWoA0V06h0e2/qRVCg//p6GytrW33IycgvS+ZPLJ7tLI\nFR2r66WfRlpoPiSL8eRt/P7kkG0hXCn7K7ub2TEu/Ka/W1yNwad6PR8iCwKBgQC8\nXcZSrtQsxAc8w99emJVoEo9wcsCGJ9ltA0iUu9XyZpvlbyJ3J+s48YrWxQ0sop7L\nUgE+96Rfo51kPMi3JVtk81p8ntf4KMrWwokaFMXHsPcJMCJ1IBVIRLE0C5eZcYhv\nlyN57I4tT1lzOZYJxYK4Cot/zrn7oF/j6mTBGfh4iQKBgQCiJMUxRz01/czH/XSX\ngo3dVbHQ4FEOufWnE3Eb93S8r0/eq1RM118rb0TqzuiadW2xYDU4nucWQlrlmq0d\nFY/m+Hy97pqyk6jmoU5I/D+ssBIoYHWLnH9/xfvDEk2JGSJSHtzu0D4EDC/rgQ49\nMbYsO5oUrF8tPlhj5vzbf3GKLA==\n-----END PRIVATE KEY-----\n",
"publicKeyOld": "BgIAAACkAABSU0ExAAgAAAEAAQBpTpiJQ2hD8plpGTfEEmcq4IKyr31HikXpuVSBraMfqyodn2PGXBJ3daNSmdPOc0Nz4HO9Auljn8YYXDPBdpiABptSKvEDPF23Q+Qytg0+vCRyondyBcW91w7KLzXce3fnk8ZfJ8QtbZPL9m11wJIWZueQF+l0HKYx4lty+nccbCanytFTADkGQ3SnmExGEF3rBz6I9+OcrDDK9NKPJgEmCiuyei/d4XbPgKls3EIG0h38X5mVF2VytfWm2Yu850B6z3N4MYhj4b4vsYT62zEC4pMRUeb8dIBy4Jsmr3avtmeO00MUH6DVyPC8nirixj2YIOPKk13CdVqGDSXA3cvl",
"modulusOld": "5cvdwCUNhlp1wl2TyuMgmD3G4iqevPDI1aAfFEPTjme2r3avJpvgcoB0/OZREZPiAjHb+oSxL77hY4gxeHPPekDnvIvZpvW1cmUXlZlf/B3SBkLcbKmAz3bh3S96sisKJgEmj9L0yjCsnOP3iD4H610QRkyYp3RDBjkAU9HKpyZsHHf6clviMaYcdOkXkOdmFpLAdW32y5NtLcQnX8aT53d73DUvyg7XvcUFcneiciS8Pg22MuRDt108A/EqUpsGgJh2wTNcGMafY+kCvXPgc0NzztOZUqN1dxJcxmOfHSqrH6OtgVS56UWKR32vsoLgKmcSxDcZaZnyQ2hDiZhOaQ==",
"exponentOld": 65537,
"privateKeyOld": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDly93AJQ2GWnXC\nXZPK4yCYPcbiKp688MjVoB8UQ9OOZ7avdq8mm+BygHT85lERk+ICMdv6hLEvvuFj\niDF4c896QOe8i9mm9bVyZReVmV/8HdIGQtxsqYDPduHdL3qyKwomASaP0vTKMKyc\n4/eIPgfrXRBGTJindEMGOQBT0cqnJmwcd/pyW+Ixphx06ReQ52YWksB1bfbLk20t\nxCdfxpPnd3vcNS/KDte9xQVyd6JyJLw+DbYy5EO3XTwD8SpSmwaAmHbBM1wYxp9j\n6QK9c+BzQ3PO05lSo3V3ElzGY58dKqsfo62BVLnpRYpHfa+yguAqZxLENxlpmfJD\naEOJmE5pAgMBAAECggEALiL+RKOr0Xu8BOgQ0j1DwA03LxVrhXe6etmJI+JySTcd\ngKENjWziZVrRIi2DvUm5qMMl7WhSwslKK1eexxZJY7xASqSxcEoIwgz17T07/jxm\nfIdUBiUKDZ1Kv8PWmIr3oKW+fkXWi/m1zlIe0qXRpTmsGNEsHQLEqi0rmaiXTXOR\n/2Ldwi6kZR3sWFx97YS4Mx/pueGJTXEai6AVEZzN5Gog6xD8HXR1Rvq+hhd+MocG\nfnU4HgilKRfoJlWd9FOscgSufKG0L3ViO4fSKU46l5aullDYUk5ECMWiwuKSqSE7\nqD45jI3mbOre7S4u3S3TWdD3lzwiXL49LdwKlEC4mQKBgQD0sLr0GH4Wr+QX2xJE\nuA/Cb8QW41l8iSCBTRZZR/sJOd+o3rbcVidlzO/EbZblXG4ZPDmRjgBCGKIP5EZi\n0DsL+Wv32WOo44LpxJGhqExbm0H1iZ1zZ97l0P8fvIhHE42gmaLToOIGDhPSXGvv\nzlqOHbGbq4jsERc1jp1bej5q6wKBgQDwaueIc4pRchH98QYidcyr8Vwg9KhbnfYX\ny3W4RPlZtBdF34iJaio+ASzugo/zy1RTcVrsCskYWXyKDUQz1yu0iCng+fDCUnTm\nXGmEoEGNhk4vTJOt7hBav1/Ja/dUipGf6mXUuanwJ0e+1/Et/B0ah5X1Um5AyNZI\nM+SyRz3u+wKBgQCjvtUNXoqaghCBCmB6TjZ1prexnWkYFugCv2SSUMIk1W7gIlJ6\ntsjcrj1R1Qii6qzfBFd+GWoA0V06h0e2/qRVCg//p6GytrW33IycgvS+ZPLJ7tLI\nFR2r66WfRlpoPiSL8eRt/P7kkG0hXCn7K7ub2TEu/Ka/W1yNwad6PR8iCwKBgQC8\nXcZSrtQsxAc8w99emJVoEo9wcsCGJ9ltA0iUu9XyZpvlbyJ3J+s48YrWxQ0sop7L\nUgE+96Rfo51kPMi3JVtk81p8ntf4KMrWwokaFMXHsPcJMCJ1IBVIRLE0C5eZcYhv\nlyN57I4tT1lzOZYJxYK4Cot/zrn7oF/j6mTBGfh4iQKBgQCiJMUxRz01/czH/XSX\ngo3dVbHQ4FEOufWnE3Eb93S8r0/eq1RM118rb0TqzuiadW2xYDU4nucWQlrlmq0d\nFY/m+Hy97pqyk6jmoU5I/D+ssBIoYHWLnH9/xfvDEk2JGSJSHtzu0D4EDC/rgQ49\nMbYsO5oUrF8tPlhj5vzbf3GKLA==\n-----END PRIVATE KEY-----\n",
"refreshLockInterval": "10m",
"dummy" : {
"enable": false,
"sampleFilePath": ""
}
},
"tenants": {
"baseDir": "",
"baseDomain": "",
"filenameConfig": "config.json",
"filenameSecret": "secret.key",
"filenameLicense": "license.lic",
"defaultTenant": "localhost",
"cache" : {
"stdTTL": 300,
"checkperiod": 60,
"useClones": false
}
},
"externalRequest": {
"directIfIn" : {
"allowList": [],
"jwtToken": true
},
"action": {
"allow": true,
"blockPrivateIP": true,
"proxyUrl": "",
"proxyUser": {
"username": "",
"password": ""
},
"proxyHeaders": {
}
}
},
"services": {
"CoAuthoring": {
"server": {
"port": 8000,
"workerpercpu": 1,
"mode": "development",
"limits_tempfile_upload": 104857600,
"limits_image_size": 26214400,
"limits_image_download_timeout": {
"connectionAndInactivity": "2m",
"wholeCycle": "2m"
},
"callbackRequestTimeout": {
"connectionAndInactivity": "10m",
"wholeCycle": "10m"
},
"healthcheckfilepath": "../public/healthcheck.docx",
"savetimeoutdelay": 5000,
"edit_singleton": false,
"forgottenfiles": "forgotten",
"forgottenfilesname": "output",
"maxRequestChanges": 20000,
"openProtectedFile": true,
"isAnonymousSupport": true,
"editorDataStorage": "editorDataMemory",
"editorStatStorage": "",
"assemblyFormatAsOrigin": true,
"newFileTemplate" : "../../document-templates/new",
"downloadFileAllowExt": ["pdf", "xlsx"],
"tokenRequiredParams": true,
"forceSaveUsingButtonWithoutChanges": false
},
"requestDefaults": {
"headers": {
"User-Agent": "Node.js/6.13",
"Connection": "Keep-Alive"
},
"gzip": true,
"rejectUnauthorized": true
},
"autoAssembly": {
"enable": false,
"interval": "5m",
"step": "1m"
},
"utils": {
"utils_common_fontdir": "null",
"utils_fonts_search_patterns": "*.ttf;*.ttc;*.otf",
"limits_image_types_upload": "jpg;jpeg;jpe;png;gif;bmp;svg;tiff;tif"
},
"sql": {
"type": "postgres",
"tableChanges": "doc_changes",
"tableResult": "task_result",
"dbHost": "localhost",
"dbPort": 5432,
"dbName": "onlyoffice",
"dbUser": "onlyoffice",
"dbPass": "onlyoffice",
"charset": "utf8",
"connectionlimit": 10,
"max_allowed_packet": 1048575,
"pgPoolExtraOptions": {
"idleTimeoutMillis": 30000,
"maxLifetimeSeconds ": 60000,
"statement_timeout ": 60000,
"query_timeout ": 60000,
"connectionTimeoutMillis": 60000
},
"damengExtraOptions": {
"columnNameUpperCase": false,
"columnNameCase": "lower",
"connectTimeout": 60000,
"loginEncrypt": false,
"localTimezone": 0,
"poolTimeout": 60,
"socketTimeout": 60000,
"queueTimeout": 60000
},
"oracleExtraOptions": {
"connectTimeout": 60
},
"msSqlExtraOptions": {
"options": {
"encrypt": false,
"trustServerCertificate": true
},
"pool": {
"idleTimeoutMillis": 30000
}
},
"mysqlExtraOptions": {
"connectTimeout": 60000,
"queryTimeout": 60000
}
},
"redis": {
"name": "redis",
"prefix": "ds:",
"host": "127.0.0.1",
"port": 6379,
"options": {},
"optionsCluster": {},
"iooptions": {
"lazyConnect": true
},
"iooptionsClusterNodes": [
],
"iooptionsClusterOptions": {
"lazyConnect": true
}
},
"pubsub": {
"maxChanges": 1000
},
"expire": {
"saveLock": 60,
"presence": 300,
"locks": 604800,
"changeindex": 86400,
"lockDoc": 30,
"message": 86400,
"lastsave": 604800,
"forcesave": 604800,
"forcesaveLock": 5000,
"saved": 3600,
"documentsCron": "0 */2 * * * *",
"files": 86400,
"filesCron": "00 00 */1 * * *",
"filesremovedatonce": 100,
"sessionidle": "1h",
"sessionabsolute": "30d",
"sessionclosecommand": "2m",
"pemStdTTL": "1h",
"pemCheckPeriod": "10m",
"updateVersionStatus": "5m",
"monthUniqueUsers": "1y"
},
"ipfilter": {
"rules": [{"address": "*", "allowed": true}],
"useforrequest": false,
"errorcode": 403
},
"request-filtering-agent" : {
"allowPrivateIPAddress": false,
"allowMetaIPAddress": false
},
"secret": {
"browser": {"string": "secret", "file": ""},
"inbox": {"string": "secret", "file": ""},
"outbox": {"string": "secret", "file": ""},
"session": {"string": "secret", "file": ""}
},
"token": {
"enable": {
"browser": false,
"request": {
"inbox": false,
"outbox": false
}
},
"browser": {
"secretFromInbox": true
},
"inbox": {
"header": "Authorization",
"prefix": "Bearer ",
"inBody": false
},
"outbox": {
"header": "Authorization",
"prefix": "Bearer ",
"algorithm": "HS256",
"expires": "5m",
"inBody": false,
"urlExclusionRegex": ""
},
"session": {
"algorithm": "HS256",
"expires": "30d"
},
"verifyOptions": {
"clockTolerance": 60
}
},
"plugins": {
"uri": "/sdkjs-plugins",
"autostart": []
},
"themes": {
"uri": "/web-apps/apps/common/main/resources/themes"
},
"editor":{
"spellcheckerUrl": "",
"reconnection":{
"attempts": 50,
"delay": "2s"
},
"binaryChanges": false,
"websocketMaxPayloadSize": "1.5MB",
"maxChangesSize": "0mb"
},
"sockjs": {
"sockjs_url": "",
"disable_cors": true,
"websocket": true
},
"socketio": {
"connection": {
"path": "/doc/",
"serveClient": false,
"pingTimeout": 20000,
"pingInterval": 25000,
"maxHttpBufferSize": 1e8
}
},
"callbackBackoffOptions": {
"retries": 0,
"timeout":{
"factor": 2,
"minTimeout": 1000,
"maxTimeout": 2147483647,
"randomize": false
},
"httpStatus": "429,500-599"
}
}
},
"license" : {
"license_file": "",
"warning_limit_percents": 70,
"packageType": 0,
"warning_license_expiration": "30d"
},
"FileConverter": {
"converter": {
"maxDownloadBytes": 1048576000,
"downloadTimeout": {
"connectionAndInactivity": "2m",
"wholeCycle": "2m"
},
"downloadAttemptMaxCount": 3,
"downloadAttemptDelay": 1000,
"maxprocesscount": 1,
"fontDir": "null",
"presentationThemesDir": "null",
"x2tPath": "null",
"docbuilderPath": "null",
"args": "",
"spawnOptions": {},
"errorfiles": "",
"streamWriterBufferSize": 8388608,
"maxRedeliveredCount": 2,
"inputLimits": [
{
"type": "docx;dotx;docm;dotm",
"zip": {
"uncompressed": "500MB",
"template": "*.xml"
}
},
{
"type": "xlsx;xltx;xlsm;xltm",
"zip": {
"uncompressed": "3000MB",
"template": "*.xml"
}
},
{
"type": "pptx;ppsx;potx;pptm;ppsm;potm",
"zip": {
"uncompressed": "500MB",
"template": "*.xml"
}
}
]
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
<svg xmlns="http://www.w3.org/2000/svg" width="85" height="20" fill="none">
</svg>

After

Width:  |  Height:  |  Size: 82 B

View File

@@ -0,0 +1,2 @@
<svg xmlns="http://www.w3.org/2000/svg" width="85" height="20" fill="none">
</svg>

After

Width:  |  Height:  |  Size: 83 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="28" height="60"><symbol id="svg-icon-crypted" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 3C7 5 4 5 4 5v3.5c0 2 .563 7.477 6 8.5 5.436-1.023 6-6.5 6-8.5V5s-3 0-6-2m4.023 4.965-1.046-.93-3.55 3.993-2.479-2.066-.896 1.076 3.52 2.934z" clip-rule="evenodd"/></symbol><symbol id="svg-icon-users" viewBox="0 0 28 20"><path fill-rule="evenodd" d="M27 10a9 9 0 1 1-18 0 9 9 0 0 1 18 0m1 0c0 5.523-4.477 10-10 10a9.96 9.96 0 0 1-6.798-2.666 8 8 0 1 1 0-14.667A9.96 9.96 0 0 1 18 0c5.523 0 10 4.477 10 10M10.451 3.441A7 7 0 0 0 8 3a7 7 0 0 0-5.69 11.079 11 11 0 0 1 1.124-.76C4.544 12.666 6.132 12 8 12q.1 0 .2.003A10 10 0 0 1 8.05 11H8a3 3 0 1 1 1.198-5.751 10 10 0 0 1 1.253-1.808M8.767 6.152A2 2 0 0 0 8 6a2 2 0 1 0 0 4 10 10 0 0 1 .767-3.848m-.304 6.864A7 7 0 0 0 8 13c-1.632 0-3.043.584-4.059 1.181a10 10 0 0 0-.99.667A6.98 6.98 0 0 0 8 17a7 7 0 0 0 2.451-.441 10 10 0 0 1-1.988-3.543" clip-rule="evenodd"/></symbol></svg>

After

Width:  |  Height:  |  Size: 994 B

1
electron/.gitignore vendored
View File

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

159
electron/build.js vendored
View File

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

View File

@@ -33,27 +33,25 @@ contextBridge.exposeInMainWorld(
request: (msg, callback, error) => {
msg.reqId = reqId++;
reqInfo[msg.reqId] = {callback: callback, error: error};
//TODO Maybe a special function for this better than this hack?
//File watch special case where the callback is called multiple times
if (msg.action == 'watchFile') {
fileChangedListeners[msg.path] = msg.listener;
delete msg.listener;
}
ipcRenderer.send('rendererReq', msg);
},
registerMsgListener: function (action, callback) {
ipcRenderer.on(action, function (event, args) {
callback(args);
});
},
sendMessage: function (action, args) {
ipcRenderer.send(action, args);
},
sendAsync: function (action, args) {
return ipcRenderer.invoke(action, args)
},
listener: function (action, callback) {
ipcRenderer.on(action, function (event, args) {
callback(args);
});
},
listenOnce: function (action, callback) {
ipcRenderer.once(action, function (event, args) {
callback(args);

92
electron/electron.js vendored
View File

@@ -163,7 +163,14 @@ function createMainWindow() {
if (!willQuitApp) {
utils.onBeforeUnload(event, mainWindow).then(() => {
if (['darwin', 'win32'].includes(process.platform)) {
mainWindow.hide();
if (mainWindow.isFullScreen()) {
mainWindow.once('leave-full-screen', () => {
mainWindow.hide();
})
mainWindow.setFullScreen(false)
} else {
mainWindow.hide();
}
} else {
app.quit();
}
@@ -434,7 +441,7 @@ function createChildWindow(args) {
if (/^https?:/i.test(hash)) {
browser.loadURL(hash).then(_ => { }).catch(_ => { })
} else if (isPreload) {
browser.webContents.executeJavaScript(`if(typeof $A.goForward === 'function'){$A.goForward('${hash}')}else{throw new Error('no function')}`, true).catch(() => {
browser.webContents.executeJavaScript(`if(typeof window.__initializeApp === 'function'){window.__initializeApp('${hash}')}else{throw new Error('no function')}`, true).catch(() => {
utils.loadUrlOrFile(browser, devloadUrl, hash)
});
} else {
@@ -497,8 +504,15 @@ function createMediaWindow(args, type = 'image') {
mediaWindow.addListener('close', event => {
if (!willQuitApp) {
event.preventDefault()
mediaWindow.webContents.send('on-close');
mediaWindow.hide();
if (mediaWindow.isFullScreen()) {
mediaWindow.once('leave-full-screen', () => {
mediaWindow.hide();
})
mediaWindow.setFullScreen(false)
} else {
mediaWindow.webContents.send('on-close');
mediaWindow.hide();
}
}
})
@@ -556,8 +570,6 @@ function createWebTabWindow(args) {
// 创建父级窗口
if (!webTabWindow) {
let config = Object.assign(args.config || {}, userConf.get('webTabWindow', {}));
let webPreferences = args.webPreferences || {};
const titleBarOverlay = {
height: webTabHeight
}
@@ -577,15 +589,15 @@ function createWebTabWindow(args) {
autoHideMenuBar: true,
titleBarStyle: 'hidden',
titleBarOverlay,
backgroundColor: nativeTheme.shouldUseDarkColors ? '#3B3B3D' : '#EFF0F4',
webPreferences: Object.assign({
backgroundColor: nativeTheme.shouldUseDarkColors ? '#575757' : '#FFFFFF',
webPreferences: {
preload: path.join(__dirname, 'electron-preload.js'),
webSecurity: true,
nodeIntegration: true,
contextIsolation: true,
nativeWindowOpen: true
}, webPreferences),
}, config))
},
}, userConf.get('webTabWindow', {})))
webTabWindow.on('resize', () => {
resizeWebTab(0)
@@ -650,15 +662,20 @@ function createWebTabWindow(args) {
webTabWindow.show();
// 创建 tab 子窗口
const browserView = new BrowserView({
const viewOptions = Object.assign({
useHTMLTitleAndIcon: true,
useLoadingView: true,
useErrorView: true,
webPreferences: {
preload: path.join(__dirname, 'electron-preload.js'),
}
})
if (nativeTheme.shouldUseDarkColors) {
}, args.config || {})
viewOptions.webPreferences = Object.assign({
preload: path.join(__dirname, 'electron-preload.js'),
nodeIntegration: true,
contextIsolation: true
}, args.webPreferences || {})
const browserView = new BrowserView(viewOptions)
if (args.backgroundColor) {
browserView.setBackgroundColor(args.backgroundColor)
} else if (nativeTheme.shouldUseDarkColors) {
browserView.setBackgroundColor('#575757')
} else {
browserView.setBackgroundColor('#FFFFFF')
@@ -871,7 +888,7 @@ function monitorThemeChanges() {
preloadWindow?.setBackgroundColor(backgroundColor);
mediaWindow?.setBackgroundColor(backgroundColor);
childWindow.some(({browser}) => browser.setBackgroundColor(backgroundColor))
webTabWindow?.setBackgroundColor(nativeTheme.shouldUseDarkColors ? '#3B3B3D' : '#EFF0F4')
webTabWindow?.setBackgroundColor(nativeTheme.shouldUseDarkColors ? '#575757' : '#FFFFFF')
// 通知所有窗口
BrowserWindow.getAllWindows().forEach(window => {
window.webContents.send('systemThemeChanged', {
@@ -1244,13 +1261,17 @@ ipcMain.on('windowMax', (event) => {
})
/**
* 给主窗口发送信息
* @param args {channel, data}
* 给所有窗口广播指令(除了本身)
* @param args {type, payload}
*/
ipcMain.on('sendForwardMain', (event, args) => {
if (mainWindow) {
mainWindow.webContents.send(args.channel, args.data)
}
ipcMain.on('broadcastCommand', (event, args) => {
const channel = args.channel || args.command
const payload = args.payload || args.data
BrowserWindow.getAllWindows().forEach(window => {
if (window.webContents.id !== event.sender.id) {
window.webContents.send(channel, payload)
}
})
event.returnValue = "ok"
})
@@ -1394,15 +1415,17 @@ ipcMain.handle('getStore', (event, args) => {
//================================================================
let autoUpdating = 0
autoUpdater.logger = loger
autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = true
autoUpdater.on('update-available', info => {
mainWindow.webContents.send("updateAvailable", info)
})
autoUpdater.on('update-downloaded', info => {
mainWindow.webContents.send("updateDownloaded", info)
})
if (autoUpdater) {
autoUpdater.logger = loger
autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = true
autoUpdater.on('update-available', info => {
mainWindow.webContents.send("updateAvailable", info)
})
autoUpdater.on('update-downloaded', info => {
mainWindow.webContents.send("updateDownloaded", info)
})
}
/**
* 检查更新
@@ -1412,6 +1435,9 @@ ipcMain.on('updateCheckAndDownload', (event, args) => {
if (autoUpdating + 3600 > utils.dayjs().unix()) {
return // 限制1小时仅执行一次
}
if (!autoUpdater) {
return
}
if (args.provider) {
autoUpdater.setFeedURL(args)
}
@@ -1477,7 +1503,7 @@ ipcMain.on('updateQuitAndInstall', (event, args) => {
// 退出并安装更新
setTimeout(_ => {
mainWindow.hide()
autoUpdater.quitAndInstall(true, true)
autoUpdater?.quitAndInstall(true, true)
}, 600)
})

View File

@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
<meta name="renderer" content="webkit">
<meta name="format-detection" content="telephone=no" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<title></title>
<!--style-->

View File

@@ -12,6 +12,7 @@ exports.default = async function notarizing(context) {
}
return await notarize({
tool: "notarytool",
appBundleId: config.build.appId,
appPath: `${appOutDir}/${appName}.app`,
appleId: APPLEID,

View File

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

View File

@@ -37,13 +37,16 @@
const {ipcRenderer} = require('electron');
const thumbnailUrl = (url) => {
url = `${url}`
.replace(/_thumb\.(png|jpg|jpeg)$/, '')
.replace(/\/crop\/([^\/]+)$/, '')
if (!/^https?:\/\/[^\/]+\/uploads\//.test(url)) {
return url
}
const crops = {
ratio: 3,
percentage: '320x0'
}
url = `${url}`
.replace(/_thumb\.(png|jpg|jpeg)$/, '')
.replace(/\/crop\/([^\/]+)$/, '')
return url + "/crop/" + Object.keys(crops).map(key => {
return `${key}:${crops[key]}`
}).join(",")

View File

@@ -14,7 +14,7 @@
消息内容最大不能超过(*)字
消息发送保存失败
说话时间太短
请选择转发对话或成员
请选择对话或成员
发送成功
不是发送人
文件不存在
@@ -171,9 +171,9 @@ LDAP 用户禁止修改邮箱
部门不存在或已被删除
最多只能创建(*)个部门
上级部门不存在或已被删除
上级部门层级错误
部门层级最多只能创建(*)级
每个部门最多只能创建(*)个子部门
含有子部门无法修改上级部门
含有子部门无法删除
请选择正确的部门负责人
新建成功
此功能未开启,请联系管理员开启
@@ -255,7 +255,8 @@ LDAP 用户禁止修改邮箱
此消息不支持设待办
仅支持设此待办人员【(*)】取消
转发成功
已超过(*)小时,此消息不撤回
已超过(*),此消息不撤回
已超过(*),此消息不可修改
文件分享错误
获取会话失败
消息不存在
@@ -467,6 +468,7 @@ OKR提醒
缺卡提醒
打卡提醒
任务待领取
指令解析失败。
非常抱歉,我不是你的机器人,无法完成你的指令。
您没有创建机器人。
机器人不存在。
@@ -489,6 +491,7 @@ webhook地址最长仅支持255个字符。
不支持的指令
机器人未启用。
当前客户端版本低(所需版本≥(*))。
引用消息解析失败。
审批结果
审批评论通知
审批通知
@@ -802,3 +805,42 @@ webhook地址最长仅支持255个字符。
更新子任务标签
AI机器人不存在
内容不存在
长文本
选择模型
当前对话不支持
会话不存在或已被删除
开启新会话
历史会话
未找到默认模型
思考中...
请先填写 Base URL
获取失败
任务超期未完成
每个用户最多只能负责(*)个部门
不能选择自己的子部门作为上级部门
转文字失败
状态[(*)]设置错误,状态负责人[(*)]不在项目成员内
(*)天(*)小时(*)分钟
(*)天(*)小时
(*)天(*)分钟
(*)天
(*)小时(*)分钟
(*)小时
(*)分钟
任务不存在或已被删除
文件不存在或已被删除
报告不存在或已被删除
文件读取失败:(*)
请输入删除备注
删除备注长度限制(*)个字
系统机器人不能删除

View File

@@ -547,7 +547,6 @@ SMTP服务器
空白模板
立即上传
立即升级
立即登录
签到功能
签到数据
签到日期
@@ -1025,7 +1024,6 @@ Pro版
隐藏共享文件
单聊
显示文件
ID、任务名...
仅显示我的
语音
群头像
@@ -1221,7 +1219,6 @@ OKR 结果分析
AI 机器人
任务相关
请填写名称!
访问OpenAI网站查看
使用代理
支持 http 或 socks 代理
例如http://proxy.com 或 socks5://proxy.com
@@ -1359,7 +1356,6 @@ APP 推送
搜索项目名称
服务器版本过低,请升级服务器。
不显示原发送者信息
转发给
留言
多选
@我的
@@ -1520,6 +1516,7 @@ License Key
网络异常,请重试。
请求失败,请重试。
任务待领取
指令解析失败。
非常抱歉,我不是你的机器人,无法完成你的指令。
您没有创建机器人。
机器人不存在。
@@ -1545,6 +1542,7 @@ API接口文档
不支持的指令
机器人未启用。
当前客户端版本低(所需版本≥(*))。
引用消息解析失败。
审批结果
审批评论通知
审批通知
@@ -1705,7 +1703,6 @@ WiFi签到延迟时长为±1分钟。
选择群组
输入关键词搜索群
仅支持选择个人群转为部门群
含有子部门无法修改上级部门
删除部门
你确定要删除【(*)】部门吗?
注意:此操作不可恢复,部门下的成员将移至默认部门。
@@ -1903,3 +1900,129 @@ WiFi签到延迟时长为±1分钟。
请选择示例标签
全部保存成功
消息详情
长文本
你确定要创建任务吗?
你确定要创建子任务吗?
正在处理,请稍后再试...
开启麦克风失败!
开启摄像头失败!
群机器人
群成员 ((*)人)
此消息已经过期
DeepSeek大语言模型算法是北京深度求索人工智能基础技术研究有限公司推出的深度合成服务算法。
API请求的基础URL路径如果没有请留空
欢迎词
仪表盘欢迎词,(*)代表用户昵称
思考过程
隐藏翻译
重新翻译
当前客户端不支持该指令
默认模型
与(*)会话历史
当前
新会话
模型温度,低则保守,高则多样
例如0.7范围0-1默认0.7
模型列表
一行一个模型名称
请选择默认模型
可选数据来自模型列表
使用默认模型列表
未知操作
获取失败
获取成功
Grok是由xAI开发的生成式人工智能聊天机器人旨在通过实时回答用户问题来提供帮助。
Ollama 是一个轻量级、可扩展的框架,旨在让用户能够在本地机器上构建和运行大型语言模型。
AI 列表
AI 设置
思考中...
请先填写 Base URL
例如http://proxy.com
API请求的URL路径
如果没有请留空
获取本地模型列表
任务超期未完成
打开部门群
群组 ID
最多选择(*)个部门
已共享
联系人
未搜到跟「(*)」相关的结果
暂无相关结果
请输入关键字
请输入关键字搜索
正在拼命搜索...
加载失败,请重启软件
发送原语音
转文字失败
请稍后再试...
选择识别语言
自动识别
选择翻译结果
不翻译结果
即将到期
创建任务
项目已归档,无法查看
默认90
出差
仅未读
仅已读
汇报状态
无法录音NotFoundError: Requested device not found无可用麦克风
关闭窗口
关闭提示
网络连接失败
重试
检查
撤回消息限制
消息发出后的可撤回时长。
系统管理员除外
修改消息限制
消息发出后的可修改时长。
汇报部门
确认转发
确认发送
分享到消息
分享报告到消息
确认分享
每次最多分享(*)个
标注人员:(*) (ID: (*))
标注人员不存在
附言
任务不存在或已被删除
文件不存在或已被删除
报告不存在或已被删除
文件读取失败:(*)
独立窗口显示
在消息中显示
添加机器人
消息保留
您没有创建机器人
清理时间
请输入备注原因
删除机器人:(*)

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