Compare commits

...

2297 Commits

Author SHA1 Message Date
kuaifan
20c3fa91fb refactor(https): 协议识别下沉到 nginx,TrustProxies 只信 X-Forwarded-Proto
- nginx 经 APP_SCHEME 环境变量(envsubst 模板)统一控制 X-Forwarded-Proto
- TrustProxies 信任内网代理但仅采信 X-Forwarded-Proto,防 Host 注入
- 移除 WebApi 中间件的硬编码强制 https
- getSchemeAndHost 优先用当前请求 scheme/host,保留非请求上下文兜底
- cmd https 切换后改用 compose up -d 重建 nginx 容器使 envsubst 生效

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 01:52:38 +00:00
kuaifan
c03867304e release: v1.7.90
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 23:06:30 +00:00
kuaifan
b595120d62 fix(base): readableBytes 补类型声明并修正拼接类型告警
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 22:06:19 +00:00
kuaifan
8e66f0bfb3 feat(dialog): 管理员可设置全员群名称
- 后端 group__edit 放开全员群改名(仅系统管理员 admin=1)
- formatData/getGroupName 用 ALL_GROUP_DEFAULT_NAME 哨兵区分"未自定义",
  避免回退逻辑被默认种子名短路导致 i18n 丢失
- 前端 canModifyName/编辑入口对全员群管理员放开,改名请求带 admin=1

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 21:57:49 +00:00
kuaifan
e9ea1adc5d chore(electron): drawio submodule 升级 24.7.17 -> 30.0.4
桌面端打包以 resources/drawio submodule 为完整基底,再覆盖 AppStore 下载的
drawio 插件定制文件。将基底从 jgraph/drawio v24.7.17 升到 v30.0.4,与
system-plugins 的 drawio 30.0.4 插件对齐(30.0.4 的 bootstrap.js 仍加载
PreConfig.js,electron/drawio.js 的 EXPORT_URL 注入无需改动)。

注意:桌面端重新打包需在 drawio 插件 30.0.4 发布到 AppStore 之后进行,
否则 build 时下载的「latest」仍是 24.7.17,会与 30.0.4 基底版本错配。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 21:57:37 +00:00
kuaifan
2eee171a50 feat(project): 系统设置新增「创建项目」权限开关
在系统设置「项目相关」新增「创建项目」权限范围(复选):
所有人 / 部门负责人(含部门管理员)/ 指定人员,三者与「所有人」互斥;
系统管理员始终可创建,不受开关限制。未授权用户隐藏「新建项目」入口
(顶部下拉、快捷键、移动端、应用宫格),后端 Project::userCanCreate()
兜底拦截,个人项目(注册自动创建)不受限。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 21:57:24 +00:00
kuaifan
fd6a8a3650 feat(user): 会员卡片支持查看该会员参与的项目和任务
- 新增权限闸门 UserDepartment::userWorksContext(本人/管理员/部门负责人只读,排除机器人与系统账号)
- 新增接口 project/user/projects、project/user/tasks、project/user/counts
- users/extra 返回 works_visible 标记控制入口显隐
- 会员卡片新增「项目与任务」入口,弹出 UserWorksModal(项目/待办/已完成三 Tab、角标计数、工作流状态徽章、懒加载)
- 部门只读视角下任务仅展示全员可见(visibility=1),与 findForDepartmentView 对齐
- 补充 i18n 文案与暗色样式

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:24:50 +00:00
kuaifan
84a90b7760 feat(approve): 审批详情支持删除审批
- ApproveController 新增 process__delById 代理,转发至审批插件 process/delById;
  服务端注入 is_admin(仅发起人或管理员可删)
- 审批详情页新增「删除」按钮(仅已结束的审批可见),删除后独立路由返回上一页、
  嵌入模式刷新列表

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 10:44:37 +00:00
kuaifan
7335c59b68 chore(release): 移除被 dootask-release 技能取代的翻译/版本脚本
- 删 language/translate.php(OpenAI 翻译)+ composer.json/lock + README
- 删 bin/version.js(git-cliff + OpenAI 更新日志)+ cliff.toml
- package.json 去掉 version/translate 两条 script,cmd 去掉 translate 子命令
- README_PUBLISH 指向 dootask-release 技能

翻译/版本号/更新日志改由 dootask-release 技能完成;CI 不受影响(只读 package.json version + CHANGELOG.md)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 07:54:37 +00:00
kuaifan
035c9d9d3d feat(skill): 新增 dootask-release 发版技能
翻译与更新日志在技能内直接产出,版本号计算/差异检测/语言文件生成等
机械步骤交给本地脚本(language.php、version_bump.js,host 直跑、不进容器)。
language.php 用 php 以字节级对齐项目原生产物;脚本相对自身定位项目根,与 cwd 无关。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 07:19:23 +00:00
kuaifan
36da18af79 release: v1.7.81 2026-06-03 02:20:11 +00:00
kuaifan
363badbc97 chore(appstore): 升级 appstore 镜像至 0.4.3
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 01:54:46 +00:00
kuaifan
9be6265220 fix(download): 大文件下载改用 BinaryFileResponse 走 sendfile
StreamedResponse 在 LaravelS/Swoole 下被 DynamicResponse 用 ob_start/
ob_get_clean 整体缓冲进 PHP 内存,约 700MB 文件会撞 memory_limit 导致
下载失败;且每次请求对整文件 md5_file 生成 ETag 开销巨大。

改为返回 BinaryFileResponse,由 LaravelS StaticResponse 走 Swoole 原生
sendfile(),OS 级零拷贝、不占 PHP 内存,可支持任意大小文件。去掉 ETag
全文件哈希改用 mtime。Swoole 环境下关闭 Range 分段(sendfile 只能整文件
发送,避免 206 头与整文件 body 错位),非 Swoole 环境保留原生 Range。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 10:37:52 +00:00
kuaifan
be53e6c6ac feat(skill): 新增 dootask-backup 数据备份技能
- 备份数据库(必须) + public/uploads(排除 tmp,可选) + docker/appstore/config(可选)
- 汇总到 tmp/ 临时目录并附 README 说明,打包到 backup/ 按日期命名
- 只读取源数据、绝不删改,失败即停
- .gitignore 忽略 /backup,避免归档进入版本库

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 08:04:29 +00:00
kuaifan
4eab130313 feat(electron-mcp): 对齐 dootask-mcp,新增 send_task_ai_message 等
- 新增 send_task_ai_message 工具(dialog/msg/send_ai_assistant),支持
  自定义发送者昵称 nickname(≤20)与 silence
- complete_task 增加 flow_item_id 参数及多结束状态(-4005)重选处理
- update_task 增加 flow_item_id 参数及多结束/开始状态(-4005/-4006)处理
- request() 捕获 ret/data 并对 -4005/-4006 放行交工具处理(向后兼容)
- 同步头部工具清单注释(27→29)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 07:10:36 +00:00
kuaifan
c706c515ee fix(dialog): AI 助手消息推送补全发送者身份
AI 助手为虚拟用户(userid=-1)无会员记录,userid2nickname 返回空串,
导致群聊推送拼成 ": 内容"。友盟 App 推送与前端桌面/移动通知改为取
msg.nickname 或默认"AI 助手"作为发送者名,显示为 "名称: 内容"。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 06:10:02 +00:00
kuaifan
8a576595ce feat(dialog): send_ai_assistant 支持自定义发送者昵称
接口新增可选 nickname 参数(最长20字),写入消息体 msg.nickname;
前端群聊昵称与回复预览对 AI 助手消息(userid=-1)显示自定义昵称,
留空回退默认"AI 助手"。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 06:09:50 +00:00
kuaifan
8c809bbff1 feat(team): 团队管理支持标记成员邮箱认证状态
- 成员行操作菜单新增「标记邮箱为已认证/未认证」,复用 users.email_verity
  字段与 api/users/operation 接口,新增 setverity/clearverity 操作类型
- 创建用户:邮箱下方新增「标记邮箱为已认证」复选框(默认勾选),
  「首次登录需改密」复选框移到初始密码下方
- 批量导入:预览列表邮箱右侧显示主题色已认证图标(错误行不显示),
  支持勾选行后批量标记已/未认证;部门与认证批量行加标签对齐、
  三个批量按钮样式随选中状态统一
- createByAdmin 新增 emailVerity 选项,createuser/import 透传逐行认证状态
- 新增导入预览默认认证状态单测

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 05:49:33 +00:00
kuaifan
08ed396444 feat(skill): 新增安装/更新/修复权限技能,四技能描述精简为命令触发
- dootask-install:前置检查 + sudo ./cmd install(建库 + migrate --seed)
- dootask-update:前置检查 + sudo ./cmd update,本地改动停下交用户决定
- dootask-fix-permission:对齐 install 赋权逻辑,chown + chmod 775,赋权不删数据
- 四个技能均为命令显式触发,description 去掉关键词堆砌,精简为一句功能说明

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 02:14:36 +00:00
kuaifan
f5eb84589f docs(skill): release 改名 dootask-release,补 CI 确认/iOS/uploads 赋权
- 新增 push 后确认 Publish 工作流、可选 iOS 发布(gh workflow run)
- 构建 EACCES:改为 chown 赋权整个 public/uploads(不删数据),删除仅限 tmp
- 同步 Red Flags;目录与 name 统一为 dootask-release

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 23:19:29 +00:00
kuaifan
daca384822 feat(todo): 待办设置权限放开系统管理员(任意群可设/取消他人待办)
- checkTodoOwnerPermission 最高优先级放行系统管理员,覆盖无群主的全员群等场景
- 同步设/取消待办、到点提醒接口报错文案与系统设置描述
- 补充管理员放行测试(user/project/全员群)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 15:02:20 +00:00
kuaifan
0a6e944a9a
Merge pull request #301 from robertsilen/pro
docs: mention MariaDB in README
2026-06-01 22:20:35 +08:00
kuaifan
e0d1b08e89 release: v1.7.67 2026-06-01 13:04:00 +00:00
kuaifan
6b54b7b1c5 feat(todo): 聊天待办支持提醒时间(到点引用原消息+@提及)
给消息待办增加可选「提醒时间」,到点由 todo-alert 机器人对原消息发起
reply、正文 @ 仍在群内的被指派成员,完全复用原生回复/提及链路(定向未读、
红点、绕过会话免打扰、App 推送);被指派人全部退群则跳过发送并标记已提醒。
设/改/取消提醒的权限沿用 todo_set_permission 开关与 checkTodoOwnerPermission。

后端:
- 迁移:web_socket_dialog_msg_todos 增加 remind_at/reminded_at 及索引,
  注册为日期字段
- WebSocketDialogMsgTodo::dueReminders() 选取到点(未提醒/未完成)待办(limit 500)
- WebSocketDialogMsg::setTodoRemind() 纯数据写入(改时间重置 reminded_at),
  接入 toggleTodoMsg($remindAt) 与 msg__todo 透传
- 接口 msg__todoremind 设置/修改/取消提醒(权限闸门、消息类型校验、
  pushMsg 同步 todo_done)
- TodoRemindTask 到点按消息发提醒(reminded_at 防重复、迟发补发、原消息/
  会话删除兜底),buildRemindText 生成 <span class="mention user"> 文本,
  接入 crontab;登记 todo-alert 机器人
- msgJoinGroup 从提醒文本中提取被 @ 成员

前端:
- 设待办弹窗新增「提醒时间」(预设 + 自定义 DatePicker)
- 待办详情浮层每条待办可查看/修改/取消提醒:DatePicker on-clear「清空」
  二次确认后取消,无时间时仅关闭面板不发请求
- 待办浮层窄屏(≤500px)改为 待办/完成 tab 切换,宽屏维持双列;列表为空
  展示空状态占位;提醒时间用 Icon 替换 emoji
- 时间读写对齐项目任务时间的时区约定

测试:tests/Feature/TodoRemindTest(数据/选取/写入/权限决策/buildRemindText/
text mention 提取),TodoSetPermissionTest 无回归。

任务 #124 后续增强。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 12:08:34 +00:00
kuaifan
adc7fb0d07 docs(claude): 补充非 REST 路由最多两段的限制说明
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 09:52:27 +00:00
kuaifan
f969c8145c fix(cmd): env_set 值未变化时跳过写入,避免无谓重写 .env
env_set 原先对已存在的键无条件 sed -i 重写 .env,即使新值与当前值相同也会
改变文件 mtime。开发模式下 vite 监听 .env,任何文件事件都会触发整服务重启,
导致前端反复刷新。写入前复用 env_get 比较,值未变化则直接返回。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 01:27:42 +00:00
kuaifan
20b5daba50 feat(manage): 团队管理支持管理员创建/批量导入员工账号(含部门、职位)
单个创建:邮箱/昵称/初始密码 + 可选首登改密、职位、部门(多选,选子部门自动补选上级,并加入对应部门群)。

批量导入:上传 Excel/CSV → 预览逐行校验 → 确认后导入。职位为模板第4列(选填,逐行解析校验),部门在预览表按行勾选后由底部设置部门到选中写入;导入按行返回结果(全成功关弹窗+成功提示;含失败留弹窗显示失败明细;仅 success>0 才刷新列表)。

后端:User::createByAdmin 选项数组化 + 校验助手 assertValidProfession/assertValidDepartments;importUsers 逐行 department/profession;UsersController createuser/import;UserImport/UserImportTemplate(含职位列)。

测试:tests/Feature/AdminCreateUserTest、tests/Unit/UserImportParseTest。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 01:26:34 +00:00
kuaifan
aa2e0acaba feat(dialog): 系统设置支持禁止其他人员设置/取消聊天待办
新增系统级开关 todo_set_permission(open=允许默认 / close=禁止)。
开关为禁止时,仅本人、群主/群管理员、项目负责人/项目管理员、任务负责人
可设置或取消聊天消息待办,其他人由后端拦截;默认允许,保持现有行为。

- SystemController::setting 接入开关读写(白名单 + 默认 open)
- WebSocketDialog::checkTodoOwnerPermission 角色判断(复用 isOwner 等)
- WebSocketDialogMsg::toggleTodoMsg 内权限闸门:close 且影响到他人且
  非放行角色时 retError;仅影响自己始终放行;open 时行为零变化
- SystemSetting.vue「消息相关」新增「待办设置权限」开关 UI
- 国际化文案(original-api.txt / original-web.txt)
- TodoSetPermissionTest 覆盖角色判断、闸门决策及真实拦截路径(8 用例)

任务 #124。系统后台 admin 不特殊放行;「完成待办」不在本次范围。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 15:12:46 +00:00
kuaifan
e57736bcc1 docs: 统一语言偏好为整段回复使用简体中文
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 23:40:53 +00:00
kuaifan
a8db8dde7b i18n(task): 任务只读提示文案改为"负责人视角"
将部门只读提示从"当前为负责人,并参与讨论,但不能编辑任务。"
调整为"当前为负责人视角,并参与讨论,但不能编辑任务。",
并同步更新 original-web.txt、translate.json 及各语言编译产物。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 23:32:24 +00:00
Robert Silén
635f6e5d5a
Merge branch 'kuaifan:pro' into pro 2026-05-25 15:04:30 +03:00
Robert Silén
4875574c6e add MariaDB to README 2026-05-25 15:04:16 +03:00
kuaifan
b1d5652bc7 refactor(electron): 发布存储从自建服务迁移到 Cloudflare R2
替换 UPLOAD_TOKEN/UPLOAD_URL 为 R2(S3 兼容)对象存储:
- 新增 r2.js 封装上传/复制/删除/列举等操作
- 新增 release-index.js 从文件名解析平台/架构生成下载索引
- CI 环境变量同步切换为 R2_* 系列

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 10:31:46 +00:00
kuaifan
025f45df0a feat(upload): 添加上传进度显示和错误处理,记录上传耗时 2026-05-22 14:55:34 +08:00
kuaifan
981a5c9f0f ci: resolve iOS build number from App Store Connect 2026-05-22 10:13:25 +08:00
kuaifan
88cfd40abe ci: fix iOS archive signing 2026-05-22 08:32:13 +08:00
kuaifan
cdcf0ff5f3 release: v1.7.55
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 23:07:02 +00:00
kuaifan
42e355149c chore(i18n): 补全 v1.7.29 后遗漏的原文并去重
补登 v1.7.29 至今新增的用户可见文本:
- original-web.txt 新增 16 条(部门负责人视角、项目管理员相关等)
- original-api.txt 新增 7 条(项目负责人/成员校验、群成员移出等错误消息)
同时清理两文件历史重复内容行(web -18、api -26),
translate.php 读取时本就 array_unique,去重不影响翻译产物。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 22:49:44 +00:00
kuaifan
518364d70d feat(translate): 支持通过 OPENAI_API_MODEL 环境变量配置翻译模型
翻译脚本不再硬编码 gpt-5.2,改为从 .env 读取 OPENAI_API_MODEL,
便于切换不同模型。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:22:11 +00:00
kuaifan
f25340c0b3 ci: update iOS publish workflow 2026-05-21 23:57:45 +08:00
kuaifan
24f607f442 fix: hide horizontal overflow in user detail 2026-05-21 19:50:38 +08:00
kuaifan
6fbddbe77c fix: run app publish in disposable eeui container 2026-05-21 19:49:50 +08:00
kuaifan
21ba2665b9 chore: 添加 .agents 到 .claude 的软链接 2026-05-21 18:17:25 +08:00
kuaifan
0888f599a4 feat(manage): 管理页侧边栏支持拖拽调整宽度并修复菜单条件渲染
- 新增 ResizeLine 组件实现侧边栏宽度拖拽调整,范围200-420px,持久化至 localStorage
- 修复 v-for/v-else-if 同级指令优先级问题,将 v-else-if 提升至 template 包裹层

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 09:41:00 +00:00
kuaifan
ef7293704b refactor(manage): 收口部门负责人ID规范化逻辑并简化后端对话可见性校验
- 后端:任务群/项目群统一按项目级共享判断,不再区分任务可见性
- 前端:新增 department/owner/ids/save mutation 及 normalizeIntArray 工具函数
- 前端:departmentOwnerReadonlyUrls 从 action 局部变量提升至 state
- 前端:修复 TaskDetail 提示文本多余空格

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 06:06:39 +00:00
kuaifan
8cd4669b90 refactor(manage): 部门负责人只读视角统一使用禁用态UserSelect组件
用 disabled 属性的 UserSelect 替代独立的 UserAvatar 只读展示,
消除双份渲染逻辑,负责人/协助人员/可见人员统一使用同一组件路径。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 05:01:15 +00:00
kuaifan
7f7a82b4b8 feat(manage): 部门负责人视角支持项目级可见开关并尊重任务可见性
新增项目级"负责人视角"开关(projects.department_owner_view,默认开启),
项目负责人可关闭,关闭后该项目及其群聊对部门负责人视角隐藏。同时将负责人
只读视角调整为尊重任务可见性:仅"全员可见"任务可被查看/进入任务群,指定
成员可见的任务仅对被指定成员开放。

- 新增 projects.department_owner_view 字段(migration)
- ProjectController::update 支持读写该开关
- UserDepartment::ownerViewContext 过滤已关闭项目,并合并为单次 JOIN 查询
- ProjectTask::findForDepartmentView / task__one / tasks 列表尊重任务可见性
- WebSocketDialog::checkDialog 任务群按可见性放行
- 前端项目设置新增开关(仅系统开启该功能时显示)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 04:46:11 +00:00
kuaifan
0863e5529a feat(manage): 实现部门负责人视角,支持只读查看部门成员项目与任务
部门负责人/部门管理员可通过系统配置开启,选择管理部门后只读查看
本部门及下级部门成员的全部项目和任务。前端自动根据 department_readonly
标记禁用编辑操作,后端统一注入负责人视角上下文控制数据访问边界。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 00:14:36 +00:00
kuaifan
e0ad8ce6c1 docs: 添加 AGENTS.md 项目指南文件 2026-05-21 00:13:09 +00:00
kuaifan
9f4e5a8335 fix(project): 修正AI自动分析开关状态判断变量名
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 08:18:28 +00:00
kuaifan
587db459bf feat(dialog): 聊天消息 Markdown 表格单元格强制不换行
DialogMarkdown 根节点新增 .dialog-markdown 类,统一规则放 markdown.less,删除 dialog-wrapper.scss 里仅覆盖 thead th 的旧局部规则。所有走 DialogMarkdown 组件的入口(聊天消息、bot 模板、单条消息分享页、AI 助手)一次性生效;任务描述、报告编辑等直接调用 MarkdownConver 的场景不受影响。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:40:57 +00:00
kuaifan
5b87714acf feat(project): 项目归档设置选择系统默认时显示规则提示
当 archive_method 为 'system' 时显示提示文案,告知用户将按系统设置的自动归档规则执行,避免用户误以为未生效。
2026-05-11 10:03:25 +00:00
kuaifan
bc54ac9462 feat(docs): 更新开发命令说明,明确AI不应主动执行的命令 2026-05-11 03:45:52 +00:00
kuaifan
7e5b31cfb2 feat(template): 添加共享模板功能,支持项目间模板使用控制 2026-05-11 03:26:59 +00:00
kuaifan
d81b4ed273 refactor: 优化API文档注释格式;调整AbstractModel方法注释 2026-05-11 02:50:14 +00:00
kuaifan
0c1a913134 feat(TaskAdd): 优化任务添加界面,调整模板浏览器和加载提示样式
Co-authored-by: Copilot <copilot@github.com>
2026-05-11 02:03:50 +00:00
kuaifan
7dc641e69e feat(template): 添加跨项目任务模板支持,增加使用统计和搜索功能 2026-05-11 01:13:54 +00:00
kuaifan
18336c870e feat(docs): 添加 Playwright 测试结果存放说明 2026-05-09 15:36:31 +00:00
kuaifan
e43588c3b2 fix(multi-owner): close permission lifecycle gaps 2026-05-09 12:31:54 +00:00
kuaifan
64649b514e feat(multi-owner): 群/项目/部门支持主+副双负责人体系 2026-05-09 12:29:38 +00:00
kuaifan
24710289e1 feat(multi-owner): 群/项目/部门支持主+副双负责人体系
- 群组:新增 web_socket_dialog_users.role(1=主、2=副),主可任命/罢免副群主,副可邀请/移出普通成员
- 项目:project_users.owner 扩展为 0/1/2(成员/主/副),主独占转让和删除,副共享日常管理;任务可见性、通知、分配等下游逻辑统一用「主+副」
- 部门:新增 user_department_owners 表存储副负责人;部门群同步副群主,赋予群管理员权限
- 转移用户时副身份不替补、降级为普通成员
- 配套 migration/backfill、API、前端 UI、i18n 词条与三项 Feature 测试
- .gitignore 忽略 .playwright-mcp/
2026-05-03 00:05:31 +00:00
kuaifan
2a3f05e06f docs(ai): 注释模型名思考标记剥离规则
说明 think/thinking/reasoning 后缀的支持写法(空格、- 、_、括号),便于后续维护识别匹配范围。
2026-05-03 00:03:32 +00:00
kuaifan
0d31106b0f release: v1.7.29
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 01:23:51 +00:00
kuaifan
fbd1c829a1 fix(ai): AI助手图片压缩阈值从1024提升到1568,减少长图模糊
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 05:12:16 +00:00
kuaifan
82d2ca6360 feat(ai): AI助手聊天记录服务端持久化
- 图片缓存不再二次压缩,用户预览与AI收到的图片质量一致
- 新增 ai_assistant_sessions 表及 AiAssistantSession 模型
- 新增会话 API:session/list、session/save、session/delete
- 前端会话存储从 IndexedDB 切换为服务端 API,图片落盘到 uploads/assistant/{YYYYMM}/{user_id}/
- saveSessionStore 添加防抖,删除会话时同步清理内存缓存

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 04:22:35 +00:00
kuaifan
717e520556 fix(ldap): 修复 AD 环境下用户搜索失败和密码策略冲突
- objectClasses 移除 inetOrgPerson 和 organizationalPerson,仅保留 person + top
  AD 用户的 objectClass 是 user 而非 inetOrgPerson,导致 LdapRecord 搜索过滤不到用户
- LDAP 用户首次创建本地账号时使用随机密码,避免 LDAP 密码不满足本地密码策略

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 02:05:14 +00:00
kuaifan
c8ddb511cf feat(ci): 添加 Gitee 同步工作流 2026-04-16 22:26:06 +00:00
kuaifan
caf728de8d feat(ci): 添加 iOS 手动构建并提交 App Store 工作流
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 22:13:22 +00:00
kuaifan
a7cd4d7fa8 release: v1.7.23
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:02:06 +00:00
kuaifan
ddc0046e24 chore(claude): 将 /release 命令转换为 skill
- 新增 .claude/skills/release/SKILL.md:CSO 描述、前置检查、三步发布流程
- 删除 .claude/commands/release.md
- 补充基线测试暴露的反模式约束(禁止自动修复脏工作区、禁止 git tag、禁止 git add -A)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 12:13:29 +00:00
kuaifan
1059630b9d feat(ldap): 支持非邮箱用户名登录,完善 AD 兼容性
- 登录页放宽校验:登录模式允许任意账号格式,注册模式仍强制邮箱
- 登录属性新增 userPrincipalName 选项(AD 常用且通常是邮箱格式)
- LDAP 用户缺少邮箱属性时返回明确错误提示,替代误导性的"请输入正确的邮箱地址"
- LDAP 登录合并已有本地账号时记录 info 日志,便于审计

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 11:48:40 +00:00
kuaifan
e1c1fc030f release: v1.7.20
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 04:25:07 +00:00
kuaifan
09edb14d56 fix(ldap): 使用 LDAP Bind 认证替代 userPassword 查询,兼容 Active Directory
- 认证方式从 userPassword 属性过滤改为标准 LDAP Bind,兼容所有 LDAP 服务器
- 新增可配置的登录属性(cn/uid/mail/sAMAccountName),AD 用户选 sAMAccountName 即可
- 移除 posixAccount objectClass,兼容 AD 目录结构
- 同步创建用户时移除 POSIX 专属属性,添加 mail 属性
- 用户查找改用 findByEmail 按 mail/cn/uid/userPrincipalName 依次匹配
- initConfig 从静态变量缓存改为 RequestContext 请求级缓存,修复 Swoole 下配置变更不生效的问题
- 默认登录属性为 cn,与旧版本行为一致,确保向后兼容

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 09:18:36 +00:00
kuaifan
f27cef2d66 fix(build): exit with code 1 on upload/release failure
WebsitePublisher methods now throw on failure instead of silently
continuing. CLI entry points catch errors and exit(1) so GitHub
Actions correctly marks the job as failed.
2026-04-06 05:28:56 +00:00
kuaifan
07a2e6df29 fix(build): put version field before file in upload form data 2026-04-06 03:38:17 +00:00
kuaifan
f521f0df65 fix(build): print detailed error info on upload failure
Show error code/message in retry warnings and failure logs
to help diagnose upload issues in CI.
2026-04-06 03:16:32 +00:00
kuaifan
a67fcd6f02 feat: connect publish pipeline to dootask-website API
- Refactor build.js: replace androidUpload/genericPublish/published with
  unified WebsitePublisher class using Authorization Bearer auth
- New CLI commands: upload-changelog, release
- Update auto-update URL to /api/download/update (legacy compat on website)
- publish.yml: use CHANGELOG.md for GitHub Release body, replace
  PUBLISH_KEY with UPLOAD_TOKEN + UPLOAD_URL
2026-04-05 23:04:34 +00:00
kuaifan
d17f404853 release: v1.7.14
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 12:43:58 +00:00
kuaifan
8def4addc4 fix(chat): 修复 AI 助手(userid=-1)在多处显示异常的问题
在 UserAvatar 组件中统一处理 AI 助手虚拟用户,避免各组件重复判断;
同时修复 @提及、回复引用、转发消息等场景下的 undefined 和空白显示问题,
并过滤批量用户请求中的无效 userid。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 12:34:27 +00:00
kuaifan
0ecaf9740f feat(i18n): 添加用户编辑和生日相关翻译原文
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 09:59:54 +00:00
kuaifan
bc75680ee9 feat: 添加 /release 发布流程 skill
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 09:51:20 +00:00
kuaifan
6a71964592 feat(dialog): 重构合并转发功能
- 合并转发消息体改为存储 msg_ids + preview,不再存储完整消息列表
- 新增 mergedetail API 按需加载合并转发详情
- 详情展示从 Modal 改为 DrawerOverlay,支持完整消息渲染
- 统一不可转发消息类型过滤(tag/top/todo/notice/word-chain/vote/template)
- 合并转发标题改为前端国际化拼接
- DialogWrapper 支持 staticMsgs 静态模式用于详情渲染
- 优化多选操作栏和转发确认界面样式
2026-04-05 09:31:41 +00:00
kuaifan
00a2ea3d2f docs: 精简 CLAUDE.md 国际化规范
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 09:08:47 +00:00
kuaifan
95e97333b4 feat(translate): 支持自定义 OPENAI_BASE_URL 配置
在翻译脚本和版本发布脚本中增加 OPENAI_BASE_URL 环境变量支持,
允许用户配置自定义的 OpenAI API 地址。自动处理 /v1 路径重复问题。
2026-04-05 09:02:04 +00:00
kuaifan
9e65500748 refactor(ai): 简化AI模块逻辑 2026-04-04 23:18:21 +00:00
kuaifan
a2acd6f6e4 feat(install): 安装时检测 APP_ID 是否与其他实例冲突
防止复制项目目录到另一个位置安装时,因 APP_ID 相同导致容器名和网络冲突。
通过 docker inspect 对比容器挂载路径与当前工作目录判断。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 08:33:48 +00:00
kuaifan
ee96730268 feat(install): 安装和修改端口时检测端口是否被占用
通过 Docker 试绑定端口的方式检测占用,避免安装流程走到最后才因端口冲突失败。
仅在首次安装或端口变更时检测,重装且端口不变时跳过。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 08:23:17 +00:00
kuaifan
f925f238dd chore(appstore): 升级 appstore 镜像版本至 0.4.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 08:23:10 +00:00
kuaifan
39c6ca3e8c feat(env): 在设置环境变量时确保.env文件存在 2026-04-04 01:38:39 +00:00
kuaifan
c798faa8db feat(migration): 添加表存在检查以避免重复创建表 2026-04-04 00:58:45 +00:00
kuaifan
ed2f843815 feat(middleware): 优化 WebApi 中的 HTTPS 强制设置逻辑 2026-04-04 07:48:04 +08:00
kuaifan
984b98e4fc feat(task): 实现消息合并转发功能,支持批量选择和转发消息 2026-04-04 07:43:26 +08:00
kuaifan
4b32472d64 feat(task): 增加AI自动分析开关(系统级+项目级)
系统设置新增 task_ai_auto_analyze 开关控制全局AI任务分析;项目设置新增 ai_auto_analyze 开关,系统关闭时项目无法开启。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 17:51:38 +08:00
kuaifan
fc171bc71f chore: 更新子项目提交哈希 2026-04-02 14:34:06 +08:00
kuaifan
cc80fa83e0 chore: 删除 Graphiti 长期记忆集成文档 2026-03-31 16:23:52 +08:00
kuaifan
782ba4a151 docs: optimize CLAUDE.md — remove discoverable content, add critical gotchas
Remove self-description header, 38-line command listing, directory trees,
and standard Laravel patterns that Claude can infer from code.

Add 6 project-specific gotchas Claude would get wrong: non-REST routing
(InvokeController), custom response envelope (Base::retSuccess/retError),
AbstractModel::createInstance, Doo::userId auth, manual validation (no
FormRequest), and Swoole Task (not Laravel Queue).

122 lines → 46 lines.
2026-03-13 10:49:03 +00:00
kuaifan
04708cedb6 feat(task): 增加解除任务关联功能
支持用户在任务详情中解除误关联的任务,权限与修改任务一致(项目负责人、任务负责人、任务协助人)。

- 新增 ProjectTaskRelation::deleteRelation() 删除双向关联并推送 WebSocket
- 新增 API POST /api/project/task/related/delete 接口
- 前端关联任务列表 hover 显示删除按钮,点击确认后解除关联

Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-09 06:38:13 +00:00
kuaifan
4068966700 feat(auth): token/expire 接口支持 refresh 参数刷新 token
- token/expire 接口新增可选参数 refresh=1,当 token 剩余有效期不足总有效期
  的 1/3 时返回新 token
- 将 users/info 移动端的硬编码 7 天刷新阈值统一改为总有效期的 1/3

Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-04 14:49:41 +00:00
kuaifan
3ce8cf381a chore: 更新子项目提交哈希 2026-03-04 11:40:00 +00:00
kuaifan
f78d3f3aff feat(dialog): add send_ai_assistant endpoint for AI assistant identity messaging
New endpoint POST api/dialog/msg/send_ai_assistant sends messages
as the AI assistant identity (userid=-1). Supports both dialog_id
(direct) and task_id (with auto-creation) parameters.

Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-22 09:05:56 +00:00
kuaifan
c60dff0950 feat(api): add with_extend param to task/lists endpoint
Supports optional `with_extend` query parameter (comma-separated).
When `project_name` or `column_name` is included, the API returns
these fields inline with each task via eager loading.

Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-22 02:44:05 +00:00
kuaifan
f2d49ee104 feat(task): 支持根据项目所有者筛选任务 2026-02-22 01:45:14 +00:00
kuaifan
a248d81230 build 2026-01-26 08:13:38 +08:00
kuaifan
1ac6bad2bb fix(task): 修复工作流切换时完成状态处理逻辑
- 恢复工作流切换时通过 $data['complete_at'] 设置完成状态,确保走统一处理入口
- 修复工作流切换时主任务完成状态校验被跳过的问题
- 修复工作流切换时 $updateMarking['is_update_project'] 未设置的问题
- checkAndAutoSetFlowItem 仅在用户单独提交 complete_at 时调用

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-01-22 07:34:09 +00:00
kuaifan
37de721df9 feat(task): 前端支持多工作流状态选择
- 处理 -4005/-4006 错误码,弹出工作流状态选择菜单
- 新增 showFlowItemSelector 方法展示可选状态列表
- 选择状态后自动更新任务的 flow_item_id 和 complete_at

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 05:32:27 +00:00
kuaifan
773eead827 feat(ai): AI 任务建议支持多语言输出
- 新增 getUserLanguageInfo 方法获取用户语言偏好
- 新增 getLocalizedTitles 方法,支持 9 种语言的标题和提示文案
- 调整 AI Prompt,根据用户语言输出对应语言的建议内容
- 相似任务检测阈值从 0.7 调整为 0.5
- 完善方法注释文档

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 05:32:20 +00:00
kuaifan
c4dd04ccb6 fix(task): 修复任务完成/取消完成时工作流状态自动切换逻辑
- 重构 flow_item_id 变更时的完成状态处理,使用 completeTask 方法替代直接赋值
- 新增 checkAndAutoSetFlowItem 方法,支持自动设置唯一的开始/结束状态
- 存在多个开始/结束状态时抛出带状态列表的错误(-4005/-4006),由前端引导用户选择
- 修复 complete_at 与 flow_item_id 同时存在时的重复处理问题

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 05:30:32 +00:00
kuaifan
2cdde37069 fix(observer): 修复 UserObserver 调用 private 方法 authInfo() 的错误
将 User::authInfo() 改为 User::userid(),因为 authInfo() 是 private 方法,
Observer 无法访问。userid() 是 public 方法,内部会正确调用 authInfo()。

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-01-21 16:58:11 +00:00
kuaifan
f68f759418 fix(ai): 更新提示信息为本地化文本 2026-01-21 15:56:03 +00:00
kuaifan
801d0b24ab perf(ai): 缩短 AI 任务分析延迟时间至 10 秒
将 AiTaskLoopTask 的 DELAY_SECONDS 从 60 秒减少到 10 秒,
使新建任务更快获得 AI 建议。

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-01-21 15:30:07 +00:00
kuaifan
29be29b9cf feat(ai): 优化 AI 提示词并完善建议交互功能
- 优化后端提示词:描述生成、子任务拆分、负责人推荐,新增栏目信息,去掉无效的 similar_count
- 优化前端提示词:去掉硬性字数限制,即时消息改为简短输出
- 新增 :::ai-action{...}::: 语法处理,支持单独采纳/忽略 assignee 和 similar
- 采纳/忽略后更新消息状态显示
- 负责人改为追加模式,保留现有负责人
- 新增任务关联功能,similar 采纳时自动创建双向关联
- 相似度阈值从 0.7 调整为 0.5,搜索结果增加到 200

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-01-21 15:30:07 +00:00
kuaifan
c253044f61 fix(ai): 更新 AI 助手头像显示逻辑和样式 2026-01-21 15:30:07 +00:00
kuaifan
9acf7d2046 fix(ai): 调整 AI 建议执行条件
1. subtasks: 标题长度阈值从 10 改为 5
2. similar: 启用向量搜索查找相似任务

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-01-21 15:30:07 +00:00
kuaifan
3911af7b51 fix(ai): 修复描述格式和负责人重复问题
1. 描述建议:AI 返回 Markdown,前端用 MarkdownConver 转 HTML
2. 负责人推荐:排除已分配的任务成员
3. 解析负责人推荐时去重,防止 AI 返回重复用户

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-01-21 15:30:07 +00:00
kuaifan
6b722b7ed7 fix(ai): 修正 AiTaskLoopTask 中 Apps 类的命名空间
App\Models\Apps -> App\Module\Apps

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-01-21 15:30:07 +00:00
kuaifan
6a00b87f72 fix(ai): 修正 API 路由地址格式
将 ai-apply/ai-dismiss 改为 ai_apply/ai_dismiss,
匹配 Laravel 路由方法命名转换规则(task__ai_apply -> ai_apply)

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-01-21 15:30:07 +00:00
kuaifan
0a97039d75 refactor(ai): 重构 AI 建议功能并完善向量搜索
1. 重构 task__ai_apply 接口:移除业务逻辑,仅负责状态更新和日志记录,
   返回建议数据由前端调用现有接口处理(taskUpdate/taskAddSub)

2. 实现 searchSimilarByEmbedding 向量搜索:
   - 使用 ManticoreBase::taskVectorSearch 进行向量搜索
   - 按 project_id 过滤同项目任务
   - 排除当前任务及其子任务
   - 设置 0.7 相似度阈值,最多返回 5 个结果

3. 更新 AI 助手头像:将文字 "AI" 替换为 SVG 图标

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-01-21 15:30:06 +00:00
kuaifan
cb56a01622 fix(ai): fix URL parsing for ai-apply/ai-dismiss links
The regex pattern (\w+) didn't match 'ai-apply' or 'ai-dismiss' because
\w doesn't include hyphens, causing all AI suggestion buttons to fail.

Fix by handling AI links before the regex match using startsWith().
Remove dead switch cases that were never reached.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-01-21 15:30:06 +00:00
kuaifan
452af4bd2f fix(ai): address issues from second code review
- Add STATUS_APPLIED and STATUS_DISMISSED constants to model
- Add markApplied() and markDismissed() methods
- Update event status after apply/dismiss actions (prevent duplicate ops)
- Validate related_task_id exists and user has permission
- Filter empty or overly long subtask names before creation

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-01-21 15:30:06 +00:00
kuaifan
75073d4320 fix(ai): address security and robustness issues from code review
Security fixes:
- Add escapeUserInput() to prevent Prompt injection via user input
- Validate msgId belongs to dialogId in updateMessageStatus()
- Add type parameter whitelist validation in ai-apply/ai-dismiss
- Add event record validation in task__ai_dismiss

Robustness fixes:
- Use atomic update for markProcessing to prevent concurrent processing
- Add subtask count limit check before creation (max 50)
- Disable similar task feature until vector search is implemented
- Fix Promise anti-pattern in frontend actions

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-01-21 15:30:06 +00:00
kuaifan
d4d7a0d69f feat(ai): add AI::invoke() method for task suggestions
- Add generic invoke() static method to AI module for custom chat completion
- Fix AiTaskSuggestion::callAi() to properly handle AI::invoke() response
- Fix findSimilarTasks() to properly handle AI::getEmbedding() response

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 15:30:06 +00:00
kuaifan
165ad03024 feat(ai): add ai-apply/ai-dismiss protocol handlers 2026-01-21 15:30:06 +00:00
kuaifan
3603cf9889 feat(ai): display AI assistant avatar for userid=-1
When a message has userid=-1 (AI assistant), display a special AI avatar
with gradient styling instead of the regular UserAvatar component.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 15:30:06 +00:00
kuaifan
027662ebab feat(ai): add ai-apply and ai-dismiss API endpoints 2026-01-21 15:30:06 +00:00
kuaifan
106465b932 feat(ai): add AiTaskLoopTask timer and register to crontab 2026-01-21 15:30:06 +00:00
kuaifan
eef4c6fbe5 feat(ai): add AiTaskAnalyzeTask async task 2026-01-21 15:30:06 +00:00
kuaifan
916ae97ca7 feat(ai): add AiTaskSuggestion module with prompt templates
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 15:30:06 +00:00
kuaifan
841405505d feat(ai): add ProjectTaskAiEvent model 2026-01-21 15:30:06 +00:00
kuaifan
22a653bb0f feat(ai): add project_task_ai_events migration 2026-01-21 15:30:06 +00:00
kuaifan
3482e4b1a8 fix(file): 修复日期格式文件名被误转换导致创建失败的问题
newDateString 函数在处理请求参数时会将所有符合日期格式的字符串
(如 "2026-01-15")转换为完整日期时间格式("2026-01-15 00:00:00"),
导致文件名中出现冒号,触发后端文件名校验错误。

修复方案:
- 直接调用时(key=null),保持原有行为用于显示格式化
- 递归处理对象属性时,仅对白名单字段(times、*_at)进行转换
- 其他字段(如 name)保持原值不转换

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 22:33:45 +00:00
kuaifan
9097369b0c fix(ai-assistant): 修复图片预览调用不存在方法的错误
将 $A.previewFile 替换为 this.$store.dispatch("previewImage"),
解决 TypeError: $A.previewFile is not a function 错误。

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 14:58:37 +00:00
kuaifan
95c6b53f10 fix(ai-assistant): 优化图片压缩逻辑避免重复质量压缩
- 新增 forceCompress 参数控制是否强制质量压缩
- compressImageForAI: 始终进行质量压缩(发送给 AI)
- saveImageToCache: 仅在需要缩小尺寸时才压缩(避免已压缩图片被重复压缩)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 14:52:57 +00:00
kuaifan
f7d5040b02 feat(ai-assistant): 支持拖放和粘贴上传图片
- 新增拖放上传:可将图片拖放到对话窗口任意位置
- 新增粘贴上传:在输入框中可直接粘贴剪贴板图片
- 提取 handleImageFiles 通用方法供多种上传方式复用
- 添加拖放时的视觉反馈(虚线边框 + 提示遮罩)
- 使用计数器方式正确处理嵌套元素和拖出窗口的情况

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 14:45:38 +00:00
kuaifan
26b7f83d35 no message 2026-01-20 14:45:02 +00:00
kuaifan
07b99c6e75 fix(ai-assistant): 修复 SSE 连接失败时状态未正确更新的问题
当 SSE 连接一开始就失败时,响应状态保持 'waiting' 而非 'streaming',
导致 onFailed 回调不会更新状态,UI 一直显示 loading。

现在同时处理 'streaming' 和 'waiting' 状态,并标记为错误状态显示失败提示。

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 14:20:50 +00:00
kuaifan
cb5e7e2cc7 refactor(ai): 优化 AI 提示词构建逻辑
- withLanguagePreferencePrompt: 修复无语言标签时占位符未添加的问题
- handleBeforeSend: 简化操作会话提示词,移除冗余的工具名称说明

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 13:47:25 +00:00
kuaifan
2180998e81 feat(ai-assistant): 添加图片发送功能支持多模态对话
- 支持上传图片并压缩(当前消息 1024px,历史 512px)
- 图片独立缓存存储,使用占位符 [IMG:xxx] 替代 base64
- 新增 prompt-image.vue 组件展示历史图片缩略图
- 后端 AI.php 支持多模态消息格式处理
- 添加图片缓存清理机制(删除会话时同步清理)
- 优化 parsePromptContent 避免重复调用
- 会话标题自动过滤图片占位符

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 09:31:34 +00:00
kuaifan
478876ddc1 feat(workflow): 在工作流配置中添加规则摘要展示
在工作流展开后的配置表格上方添加规则摘要区块,根据实际配置动态展示:
- 状态负责人规则:区分添加模式、流转模式、剔除模式的不同描述
- 限制负责人规则:显示仅限任务负责人和项目管理员修改状态
- 关联列表规则:显示流转时自动移动至指定列表

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 01:55:00 +00:00
kuaifan
ae021fd148 fix(push): 修复友盟延迟推送已读检查失效的问题
消息ID取值路径错误,导致延迟推送时无法正确判断消息已读状态,
用户在PC端阅读消息后APP仍会收到重复推送。

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 00:44:45 +00:00
kuaifan
f36317b081 fix(operation-client): 修正 WebSocket 路径格式并优化连接 URL 生成 2026-01-19 10:52:22 +08:00
kuaifan
066a5a619c build 2026-01-19 09:43:37 +08:00
kuaifan
654793156d feat(micro-app): 添加额外的事件发射器方法以支持动态事件处理 2026-01-19 09:19:27 +08:00
kuaifan
ba65378c6b fix(package): 更新 view-design-hi 依赖版本至 4.7.0-80 2026-01-19 01:14:05 +00:00
kuaifan
cb6c50b071 fix(ai-assistant): 修复弹窗和下拉菜单被其他弹窗遮挡的问题
- 使用 window.modalTransferIndex + 1000 作为动态 z-index
  - 添加定时刷新机制:弹窗可见时 5 秒刷新,不可见时 20 秒刷新
  - modal.vue 通过 zIndex prop 接收并应用 z-index
  - float-button.vue 通过 $parent.topZIndex 获取 z-index
  - Dropdown 和 Select 使用 ViewUI 新增的 z-index prop
2026-01-19 01:13:18 +00:00
kuaifan
2cb67fafe7 feat(ai-assistant): 支持任务弹窗和对话弹窗的场景检测
- 在 page-context.js 的 getPageContext 和 getSceneKey 函数中优先检测弹窗状态
  - 当 taskId > 0 时使用 single-task 上下文
  - 当 dialogModalShow && dialogId > 0 时使用 single-dialog 上下文
  - 在 welcome-prompts.js 中添加弹窗场景检测逻辑
  - 提取 formatPrompts 辅助函数减少代码重复
  - 在 index.vue 的 welcomePromptsKey 中监听 taskId 和 dialogModalShow 变化
2026-01-18 23:52:26 +00:00
kuaifan
8eaba6f364 fix(ai-assistant): 优化流式响应期间的 loading 状态显示
- 修改 loading 显示条件,streaming 状态时继续显示 loading icon
  - SSEClient 添加可选的 onFailed 回调,处理连接失败情况
  - 修复 done 事件处理,确保状态正确转为 completed
  - 解决工具调用期间 loading 动画过早消失的问题
2026-01-18 14:44:03 +00:00
kuaifan
c4f0fb5a3d feat(ai-assistant): 合并连续工具使用的显示
在 Markdown 渲染前预处理文本,将连续的 tool-use 标签合并为一行显示:
  - 连续相同工具显示计数(如 get_page_context x 2)
  - 不同工具用逗号分隔
  - 工具间的空行不会打断合并
2026-01-18 13:36:53 +00:00
kuaifan
59ad79fa58 feat(ai-assistant): 支持上下键切换历史输入
- 按 ↑ 键切换到上一条历史输入(光标在第一行时生效)
  - 按 ↓ 键切换到下一条历史输入(光标在最后一行时生效)
  - 历史记录使用 IndexedDB 持久化存储,最多保存 50 条
  - 重复输入会移动到末尾而非重复添加
  - 弹窗关闭时自动重置导航状态
2026-01-18 13:20:13 +00:00
kuaifan
c65f0276bd feat(ai-assistant): 支持编辑历史问题并重新发送
- 鼠标悬停历史问题时显示编辑图标
  - 点击编辑后在原位置显示内联编辑器
  - 支持 Enter 发送、Shift+Enter 换行、Esc 取消
  - 发送后删除该问题及之后的对话历史,重新发送编辑后的问题
  - 正确处理中文输入法组合状态,避免误触发提交
2026-01-18 12:56:16 +00:00
kuaifan
f8b335a003 feat(ai-assistant): 增加元素向量匹配与关键词搜索能力
- 新增后端 match_elements API,使用向量相似度匹配页面元素
  - 页面上下文采集支持关键词过滤,按 name/aria-label/placeholder/title 匹配
  - 关键词匹配失败时自动降级为向量搜索
  - 改进 findElementByRef 函数,使用 selector + name 双重匹配提高准确性
2026-01-18 11:50:27 +00:00
kuaifan
0ac4b546ba feat(ai-assistant): 实现 AI 前端操作能力
新增三个 MCP 工具的前端支持:
  - get_page_context: 基于 ARIA 角色收集页面元素,支持分页和区域筛选
  - execute_action: 执行导航操作(打开任务/对话、切换项目/页面)
  - execute_element_action: 元素级操作(click/type/select/focus/scroll/hover)

  新增文件:
  - operation-client.js: WebSocket 客户端,处理与 MCP Server 的通信
  - page-context-collector.js: 页面上下文收集器,ref 系统和 cursor:pointer 扫描
  - action-executor.js: 操作执行器,支持智能解析如 open_task_123
  - operation-module.js: 模块编排,整合上述模块

  修改文件:
  - float-button.vue: 集成 operation-module,AI 助手打开时启用
  - index.vue: 发射关闭事件供 float-button 监听
2026-01-18 01:35:13 +00:00
kuaifan
07a41ca0ac feat(ai-assistant): 扩充提示词库并优化随机选择策略
- 为提示词增加 type(query/action/sync/review)和 pin 属性
  - 新增 selectPrompts 函数:优先展示 pin 提示,按类型多样化抽样
  - 各场景提示词数量扩充 2-3 倍,覆盖更多常见操作
  - 部分场景使用动态数据(如 taskName、userName、groupName)个性化提示
2026-01-17 02:24:42 +00:00
kuaifan
347465fc4d feat(ai-assistant): 按场景隔离会话存储
- 将 sessionStore 从对象改为数组,每个场景独立存储
  - sessionCacheKey 改为 sessionCacheKeyPrefix,拼接场景 key 动态生成
  - initSession 改为异步方法,切换场景时按需加载对应数据
  - 使用防抖更新 displayWelcomePrompts,避免场景切换时闪屏
  - 修复输入框文字颜色样式
2026-01-17 02:24:31 +00:00
kuaifan
acb9cd317c feat(ai-assistant): 增加 SVG 图标和随机选择提示功能 2026-01-16 14:42:04 +00:00
kuaifan
b7213f8c47 feat(ai-assistant): 添加全屏切换功能
- 添加全屏按钮,支持点击或双击标题栏切换全屏
  - 全屏时禁用拖动和调整大小
  - 全屏状态下占满视口(保留 12px 边距)
  - 关闭窗口时自动退出全屏状态
2026-01-16 10:26:57 +00:00
kuaifan
a3caf5ebdf feat(ai-assistant): 支持拖动边缘调整聊天窗口大小
- 添加 8 个方向的调整大小控制点(四边 + 四角)
  - 支持从任意边缘或角落拖动调整窗口尺寸
  - 尺寸自动保存到 IndexedDB,下次打开时恢复
  - 窗口大小限制:最小 380×400,最大 800×900
  - 视口尺寸变化时自动调整窗口大小和位置
2026-01-16 10:24:41 +00:00
kuaifan
87dd07ef23 feat(ai-assistant): 基于场景标识管理会话恢复
- 新增 getSceneKey 函数,根据路由和实体生成唯一场景标识
  - 会话初始化改为按 sceneKey 匹配历史记录,相同场景恢复会话
  - 统一全局 AI 助手打开方式,manage.vue 通过事件触发 float-button
  - resumeSession 超时时间统一为 86400 秒(1天)
2026-01-16 08:49:25 +00:00
kuaifan
0cefb7eaff feat(task): 兼容 start_at/end_at 参数,统一转换为 times
- 新增 ProjectTask::normalizeTimes() 方法统一处理时间参数
  - 支持只传 end_at 时自动补充 start_at
  - 支持只传 start_at 时保留已有 end_at
2026-01-16 08:37:32 +00:00
kuaifan
ff87de9f44 feat(manage): 优化快捷键事件处理 2026-01-16 08:28:39 +00:00
kuaifan
22de7de87c feat(manage): 优化新建菜单并添加 AI 助手快捷键
- 主按钮从「新建项目」改为「新建任务」
  - 下拉菜单首位添加「AI 助手」选项(需安装 AI 插件)
  - 添加 Ctrl/Cmd+I 快捷键打开 AI 助手
  - 键盘设置页面同步显示 AI 助手快捷键
2026-01-16 07:46:50 +00:00
kuaifan
53dd9dca0f feat(ai-assistant): 浮动按钮支持拖拽到边缘自动收起
- 拖拽按钮到屏幕边缘(≤12px)松开后自动收起为窄条
  - 鼠标悬停窄条时自动展开,离开 1 秒后收起
  - 点击收起状态的窄条直接打开 AI 助手
  - 收起/展开过渡动画平滑,按钮中心位置保持不变
  - 仅在 AI 插件安装后显示浮动按钮
2026-01-16 07:46:41 +00:00
kuaifan
12d6bbea19 feat(mcp): 增强文件工具支持文本内容读取
- get_file_detail: 添加 with_content 参数提取文本
  - 新增 fetch_file_content 工具通过路径获取内容
2026-01-16 01:41:36 +00:00
kuaifan
23b06327d6 feat(file): 添加文件内容提取 API 支持分页读取
- FileController: 新增 fetch API 通过路径获取文本内容
  - FileController: one API 支持 with_text 参数提取文本
  - ManticoreFile: 实现分页提取 extractFileContentPaginated
  - TextExtractor: 添加 truncate 参数支持内容截取
2026-01-16 01:41:28 +00:00
kuaifan
6c22e373f7 build 2026-01-16 03:11:32 +08:00
kuaifan
4ebbb387ee no message 2026-01-16 03:08:25 +08:00
kuaifan
9234fe3ed1 feat(ai-assistant): 添加欢迎界面快捷提示功能和交互优化
主要变更:
  - 新增场景化快捷提示,根据页面类型显示相关操作建议
  - 重新设计欢迎界面 UI,支持图标和可点击的提示卡片
  - 修复浮动按钮点击判断逻辑(移动距离<5px 且 按下时间<200ms)
  - 优化加载状态显示,移除冗余文案
  - 支持 base64 编码格式的文件链接
2026-01-16 02:31:13 +08:00
kuaifan
70be6619e9 refactor(chat-input): 简化任务搜索逻辑
移除项目 ID 筛选条件,统一使用 scope: 'all_project' 搜索所有项目的任务。
2026-01-16 01:23:59 +08:00
kuaifan
c8c27e808f fix(chat-input): 修复 @ 提及下拉框层级问题
设置 mention 下拉容器的 zIndex 为 modalTransferIndex + 1000,
  确保在弹窗等高层级元素中正常显示。
2026-01-16 01:14:19 +08:00
kuaifan
9cb8c92492 fix(electron): 修复客户端 loadHash 域名判断逻辑
修复当 mainDomain 为 "public" 时无法正确判断域名的问题,
  改为从缓存的 cacheServerUrl 获取实际域名进行比较。
  同时修正跳转时错误使用 url 变量的问题,改为正确的 loadHash。
2026-01-16 01:08:03 +08:00
kuaifan
f4f9ee1d3d fix(ai-assistant): 修复深色模式反转样式和交互优化
- 将 no-dark-content 类从容器移动到 SVG 元素,修复深色模式样式问题
  - 添加深色模式反转时的悬浮按钮和聊天窗口样式适配
  - 支持 Escape 键关闭聊天模式窗口
  - 移除多余空白行
2026-01-16 01:07:54 +08:00
kuaifan
138336711f no message 2026-01-16 00:20:52 +08:00
kuaifan
2163bb0bff fix(electron): 修复客户端下载功能无法启动的问题
- 将 onRenderer 参数从 mainWindow 改为 getMainWindow 函数,解决模块加载时 mainWindow 为 null 导致下载无法触发的问题
  - 处理 InterruptedError 错误,避免下载中断时抛出未处理异常
2026-01-16 00:20:52 +08:00
kuaifan
bc460f0da8 fix(ai-assistant): 修复 SSE 流式响应 done 事件错误处理
- 解析 done 事件的 payload 检查是否携带错误信息
  - 移除错误提示中对 response.error 的直接展示
2026-01-15 16:18:53 +00:00
kuaifan
ad66811f49 refactor(ai-assistant): 重构页面上下文配置,支持更多页面类型
- 简化上下文提示词,移除能力范围描述
  - 新增多个独立页面上下文支持:单任务、单对话、单文件、工作汇报等
  - 传递路由参数给上下文函数,以获取实体 ID
  - 移除不必要的 title 属性
2026-01-15 16:18:42 +00:00
kuaifan
70ad8c394a feat(ai-assistant): 添加聊天窗口模式和页面上下文感知
- 新增 chat 显示模式,支持可拖拽的悬浮聊天窗口
  - 新增 page-context.js,根据当前路由提供针对性系统提示词
  - 优化浮动按钮:添加淡入淡出动画、修复右键菜单拖动问题、更新配色
  - 重构 Modal 为独立组件,支持 modal/chat 双模式切换
  - 恢复会话时自动滚动到底部
2026-01-15 15:06:38 +00:00
kuaifan
32ffecb905 feat(ai-assistant): 为各场景添加自定义标题并优化浮动按钮显示
- 为项目创建、任务创建、消息编写、汇报编辑、汇报分析场景的 AI 助手添加专属标题
  - 在模态框显示时自动隐藏浮动按钮,避免 UI 重叠
2026-01-15 10:48:56 +00:00
kuaifan
b794ba7a6b refactor(ui): 优化客户端下载入口位置
- 将仪表盘页面的客户端下载链接移至右上角用户菜单
  - 登录页保留右下角客户端下载链接
  - 新增 clientDownloadUrl 全局状态,统一管理下载地址
  - AI 浮动按钮在登录页不显示
2026-01-15 09:09:58 +00:00
kuaifan
07360a8d2c feat(manticore): 添加同步失败自动重试机制
- 新增 ManticoreSyncFailure 模型记录同步失败的条目
  - 添加 RetryManticoreSync 命令实现失败重试逻辑
  - ManticoreBase 增加 runWithRetry 包装器,连接断开时自动重连
  - 统一 deleteVector 方法,减少重复代码
  - 修复 quoteValue 传入非字符串的类型问题
2026-01-15 08:28:55 +00:00
kuaifan
fb7731ddcd feat(ai-assistant): 添加全局浮动按钮入口
- 新增 float-button.vue 组件,支持拖拽定位和位置持久化
  - 将 AIAssistant.vue 重构为目录结构(index.vue + float-button.vue)
  - 浮动按钮位置基于四角存储,窗口缩放时保持相对位置
  - 点击浮动按钮打开 AI 助手对话框
2026-01-15 08:18:34 +00:00
kuaifan
13a25e3011 fix(manticore): 修复向量表插入时的 SQL 语法错误
- 新增 executeRaw() 方法直接执行 SQL,避免 prepared statement 解析问题
  - 新增 quoteValue() 方法安全转义 SQL 值
  - 新增通用 upsertVector() 方法统一处理所有向量表插入
  - 简化 upsertMsgVector/TaskVector/FileVector/ProjectVector/UserVector 为单行调用
  - 统一 NUMERIC_FIELDS 常量,消除代码重复
  - 更新 batchUpdateVectors() 使用统一常量
2026-01-15 00:47:33 +00:00
kuaifan
055cf53738 build 2026-01-14 22:31:27 +08:00
kuaifan
cb414b48f6 refactor: 优化窗口关闭拦截机制,采用声明式注册
- 将 onBeforeUnload 从 utils.js 移至 web-tab-manager.js
- 新增声明式拦截注册机制,前端通过 registerCloseInterceptor 声明需要拦截
- 仅对已声明拦截的页面执行 JS 检查,未声明的直接关闭
- 添加 5 秒超时保护,防止网页卡死导致无法关闭窗口
- 修复 command+w 快捷键关闭整个窗口而非当前 tab 的问题
2026-01-14 22:29:36 +08:00
kuaifan
1c27719ac4 no message 2026-01-14 20:15:48 +08:00
kuaifan
ec33327408 fix: 修复文件夹上传时数据库死锁问题
使用 Redis 分布式锁对同一用户往相同父目录的上传请求进行排队,
  避免并发上传导致的 MySQL 死锁错误 (SQLSTATE[40001])
2026-01-14 11:44:47 +00:00
kuaifan
c2c27a684b feat: 复制/周期任务时复制子任务并重置状态
- 复制任务时同时复制子任务,子任务状态重置为未完成
  - 周期任务生成时,子任务状态重置为未完成并映射到 start 工作流
  - 新增 getProjectFlowItems 方法获取项目工作流状态
  - 新增 formatFlowItemName 方法格式化工作流状态名称
  - 新增 copySubTasks 方法复制子任务到新父任务
  - 新增 moveSubTasks 方法移动子任务,重构 moveTask 复用代码
2026-01-14 11:31:28 +00:00
kuaifan
224703a6d0 feat: 支持输入法组合状态,优化输入框键盘事件处理 2026-01-14 10:11:28 +00:00
kuaifan
dd20711c04 refactor: 移除冗余日志记录,优化代码可读性 2026-01-14 09:41:06 +00:00
kuaifan
3a2b7b1400 feat: 新增 AI 提示词占位符与用户上下文注入
- 新增 PromptPlaceholder 模块,负责构建用户上下文和条件性提示块
  - 用户上下文包含:基础信息、部门、同事印象、场景角色、任务列表
  - 前端使用 {{SYSTEM_OPTIONAL_PROMPTS}} 占位符,后端统一替换为实际内容
  - 重构 BotReceiveMsgTask 和 ai.js,复用 PromptPlaceholder 逻辑
  - 任务列表支持智能排序:逾期优先 → 最近活跃 → 负责人优先
2026-01-14 09:33:20 +00:00
kuaifan
792989a504 refactor: 统一 webTab 事件分发逻辑
新增 dispatchToTabBar() 函数,封装 window 模式检查逻辑:
  - window 模式无标签栏,跳过 executeJavaScript 调用
  - 避免 did-stop-loading 监听器累积导致 MaxListenersExceededWarning
  - 统一 14 处调用点,提升代码一致性和可维护性
2026-01-14 13:41:28 +08:00
kuaifan
c0183e62fb style: 统一 webTab 主题配色风格
- 深色模式:背景 #202124,活跃Tab #323639,文字 #D6D6D7
  - 浅色模式:背景 #F1F3F4,活跃Tab #FFFFFF,文字 #5F6368
  - 同步更新 WebView 默认背景色和加载页背景色
  - 更新 earth 图标选中态颜色适配新主题
  - 删除未使用的 link 图标资源
  - 语言切换时重建预加载池
2026-01-14 11:50:15 +08:00
kuaifan
ce5bb5f187 refactor: 统一 webTab 背景色设置逻辑
- 移除 createWebTabView 中冗余的深色/浅色主题背景色判断分支
  - 统一使用 utils.getDefaultBackgroundColor() 获取默认背景色
  - 移除 did-stop-loading 事件中不必要的背景色重置逻辑
2026-01-14 10:14:31 +08:00
kuaifan
a34b0c88d5 refactor: 优化 webTab 管理和状态同步
- 封装 safeCloseWebTab 方法,复用标签关闭时的未保存数据检查逻辑
  - 添加 recreatePreloadPool,支持主题切换后重建预加载池
  - broadcastCommand 扩展到 webTab views,确保子窗口收到同步消息
  - 修复 synchTheme 和 saveDialogDraft 的跨窗口参数传递
  - IDBDel 返回 Promise 并正确 await
2026-01-14 10:11:41 +08:00
kuaifan
9c7ec58bb6 no message 2026-01-14 09:14:35 +08:00
kuaifan
067a736b57 fix: 恢复窗口/标签关闭时的未保存数据检查
恢复 onBeforeUnload 功能,防止关闭窗口或标签时丢失未保存的数据:
  - 快捷键关闭:检查当前激活标签的 onBeforeUnload
  - 点击窗口关闭按钮:依次检查所有标签,遇到拦截时激活对应标签
  - 点击 tab 关闭按钮:检查对应标签的 onBeforeUnload
  - 重构 close 事件处理,使用 early return 简化代码结构
2026-01-13 14:49:59 +00:00
kuaifan
f8f08c9d0d no message 2026-01-13 14:48:05 +00:00
kuaifan
4f2d382fd6 fix: 移除 Markdown 消息中的工具使用标签 2026-01-13 12:57:54 +00:00
kuaifan
42e4ddbd17 fix: 修复权限级联同步缺口
修复 Manticore 搜索索引在特定场景下 allowed_users 权限未能正确同步的问题:

  Observer.updated 补充:
  - ProjectUserObserver: 处理项目成员移交时的权限级联
  - ProjectTaskUserObserver: 处理任务成员移交时的权限更新

  批量操作绕过 Observer 修复(delete → remove):
  - FileUser: deleteFileAll/deleteFileUser 方法
  - ProjectTask: 可见性设置时的批量删除
  - ProjectController: 子任务升级和任务复制时的批量删除

  文件批量更新封装:
  - File 新增 updateChildFilesUserid() 方法,统一处理子文件 userid
    更新及 Manticore 同步
2026-01-13 11:55:45 +00:00
kuaifan
3026cd698f feat: 添加文本换行样式以改善审批详情的可读性 2026-01-13 10:54:13 +00:00
kuaifan
47c53a18fa fix: 修复跨项目移动任务时子任务工作流状态未更新的问题
跨项目移动任务时,子任务的 flow_item_id 和 flow_item_name 没有被正确更新,
  导致子任务在新项目中显示的工作流状态与新项目的工作流不匹配。
2026-01-13 10:50:42 +00:00
kuaifan
22926e19cd refactor: 统一 dootask:// 链接处理与资源格式指南
- 将 dootask:// 协议链接处理逻辑从 AIAssistant 迁移到 DialogMarkdown 组件
  - 新增 beforeNavigate prop 支持导航前回调(如关闭弹窗)
  - 后端 BotReceiveMsgTask 添加条件性资源格式指南提示词
  - 前端 ai.js 新增 SEARCH_AI_SYSTEM_PROMPT 和 DOOTASK_RESOURCE_FORMAT_GUIDE
  - SearchBox 改用统一的 SEARCH_AI_SYSTEM_PROMPT 常量
  - 重构 ai.js 代码组织,添加注释说明各常量用途
2026-01-13 10:31:31 +00:00
kuaifan
495b25e2b1 feat: 增强 MCP 配置助手,支持多种 AI 工具
- 新增 Tabs 组件展示多种 AI 工具的配置方式
  - 支持 Claude Code、Cursor、VS Code、Windsurf、Claude Desktop、
    Codex、Kiro、Trae、Antigravity、Opencode 等工具
  - 丰富使用示例,按任务管理、项目查询、工作汇报、团队协作、
    文件查找等分类展示
  - 优化国际化支持,使用 t() 函数替代 $L() 实现中英双语
2026-01-13 08:56:20 +00:00
kuaifan
01908b7c48 no message 2026-01-13 03:56:56 +00:00
kuaifan
b138dc580d refactor: 重构 MCP 工具并新增搜索功能
主要变更:
  - 新增 search_dialogs 工具,支持按名称搜索群聊或联系人
  - 新增 intelligent_search 统一搜索工具,支持任务/项目/文件/联系人/消息
  - 重构 send_message 工具,支持 dialog_id 或 userid 两种方式
  - 重构 get_message_list 工具,支持 dialog_id 或 userid
  - 优化 get_project 并行获取项目详情和列信息
  - 统一返回字段命名 (id -> task_id/project_id/file_id/report_id)
  - 修正 HTTP 方法 (POST 用于 add/remove 操作)
  - 精简工具描述文案
2026-01-13 02:23:14 +00:00
kuaifan
78b14f4aad feat: 添加 dialog_only 参数支持仅搜索对话
在 dialog/search 接口中增加 dialog_only 可选参数,
  启用后仅搜索会话和联系人,跳过消息内容搜索。
2026-01-12 15:03:04 +00:00
kuaifan
60387aa521 refactor: 优化注释 2026-01-12 09:09:24 +00:00
kuaifan
633826cb89 refactor: 迁移到 navigationHistory API
将已废弃的 webContents 导航方法迁移到新的 navigationHistory API:
  - canGoBack() → navigationHistory.canGoBack()
  - canGoForward() → navigationHistory.canGoForward()
  - goBack() → navigationHistory.goBack()
  - goForward() → navigationHistory.goForward()
2026-01-12 07:27:18 +00:00
kuaifan
cf6d180fc5 feat: 添加 webTab 预加载池机制
引入预加载池以优化 webTab 首屏加载性能:
  - 应用启动后延迟创建预加载 view,避免影响主窗口
  - 新建 tab 时优先复用已预加载的 view
  - 取走后自动延迟补充,保持池容量
  - 应用退出前清理预加载资源
2026-01-12 06:55:57 +00:00
kuaifan
0d85174250 feat: 添加 favicon 双层缓存机制
实现仿 Chrome 的 favicon 缓存系统:
  - 第一层:域名缓存 - 导航开始时立即查询,快速显示 favicon
  - 第二层:URL 缓存 - favicon URL 精确匹配
  - 支持内存缓存 + 文件持久化,应用启动时自动清理 30 天过期缓存
2026-01-12 05:40:57 +00:00
kuaifan
925449c66a refactor: 抽离 webTab 窗口管理为独立模块
将 electron.js 中 1000+ 行的 webTab 窗口管理逻辑抽离到
  electron/lib/web-tab-manager.js,提升代码可维护性
2026-01-12 05:15:32 +00:00
kuaifan
cd58b418af refactor: 新增 updateWindow 接口并移除废弃的预加载窗口调用
- 新增 updateWindow IPC handler,支持窗口/标签页内部导航时更新 URL 和名称
  - 将前端 updateChildWindow 调用替换为 updateWindow
  - 移除 reloadPreloadWindow 调用(预加载窗口已删除)
2026-01-12 01:44:34 +00:00
kuaifan
4cfc5e6024 refactor: 移除 userAgent 相关代码以简化窗口管理逻辑 2026-01-12 09:07:10 +08:00
kuaifan
7321ab06f0 refactor: 优化窗口尺寸和位置管理逻辑 2026-01-12 09:02:58 +08:00
kuaifan
790f5d4838 refactor: 统一 Electron 子窗口与标签页窗口管理
将原有独立子窗口 (childWindow) 和标签页窗口 (webTabWindow) 合并为统一的
  窗口管理系统,通过 mode 参数区分窗口类型:
  - mode='tab': 标签页模式(有导航栏,默认)
  - mode='window': 独立窗口模式(无导航栏)

  主要变更:
  - 移除 createChildWindow、preCreateChildWindow 等独立窗口相关代码
  - 扩展 createWebTabWindow 支持 mode 参数
  - 简化前端 openWindow 调用,将 config 对象扁平化为顶层参数
  - 更新所有调用点使用新的统一接口
2026-01-11 21:13:55 +00:00
kuaifan
731dbc5507 feat: 标签页新增更多菜单功能
- 新增更多菜单按钮替代原浏览器打开按钮
  - 实现重新加载、复制链接地址、默认浏览器打开功能
  - 实现将标签页移至新窗口功能
  - 实现打印功能
  - 菜单支持根据当前 URL 类型动态启用/禁用选项
  - 添加相关国际化文案
2026-01-10 16:35:19 +00:00
kuaifan
3b1dce6d67 feat: 标签页新增更多菜单按钮
- 将原浏览器打开按钮替换为更多菜单按钮
  - 添加 more.svg 图标并调整样式
  - 实现 webTabShowMenu 通信接口及菜单框架
2026-01-10 15:47:43 +00:00
kuaifan
4929d44ce7 refactor: 优化标签页加载状态管理与 URL 加载逻辑
- 新增 loadContentUrl 方法统一处理完整 URL 和相对路径的加载
  - 优化标签页加载状态,忽略 SPA 路由切换(isSameDocument),避免频繁闪烁
  - 添加定时检查器确保加载状态正确停止
  - windowClose/windowDestroy 支持识别 tab 页面发送者,仅关闭对应标签
  - 子窗口重启过程中不再意外销毁窗口
  - 微应用打开标签页时传递标题信息
  - isLocalHost 对空 URL 和相对路径返回 true
2026-01-10 15:44:58 +00:00
kuaifan
ce42c2a660 refactor(frontend): 统一域名获取与比较逻辑
- 新增 mainDomain() 函数,简化 mainUrl 域名获取
  - 新增 removeMainUrlPrefix() 函数,用于移除 URL 的服务器域名前缀
  - getDomain() 返回值统一转为小写,确保域名比较不受大小写影响
  - 将多处 getDomain(mainUrl()) 调用替换为 mainDomain(),提升代码可读性
2026-01-10 05:48:25 +00:00
kuaifan
16d5ffd4f9 refactor: 统一客户端窗口打开接口并支持标签页名称复用
- 合并 openChildWindow 和 openWebTabWindow 为统一的 openWindow 接口
  - 新增 webTabNameMap 映射,支持按名称查找和复用已存在的标签页
  - 标签页增加 name、titleFixed 元数据支持
  - 窗口间转移标签时同步更新名称映射
  - 重构前端 actions,统一使用 openWindow 方法,通过 mode 参数区分窗口/标签模式
  - 更新所有调用点使用新的统一接口
2026-01-10 02:08:36 +00:00
kuaifan
fc74e0d952 feat: 标签页拖拽合并时支持插入到鼠标所在位置
- getAllWebTabWindowsInfo 增加返回 tabCount 用于计算标签位置
  - attachToWindow 根据鼠标 screenX 和目标窗口标签信息计算插入位置
  - 拖拽标签合并到其他窗口时插入到鼠标位置而非总在末尾
2026-01-09 15:17:21 +00:00
kuaifan
089f219280 feat: 标签页拖拽创建新窗口时窗口定位优化及 favicon 验证
- 优化拖拽标签创建新窗口时的位置计算,使用 setPosition 确保窗口出现在鼠标位置
  - 重构 createWebTabWindowInstance 函数,仅在明确指定 x/y 时设置窗口坐标
  - 新增 fetchFaviconAsBase64 工具函数,在主进程验证 favicon 并转为 base64
  - favicon 验证后再保存和传递给前端,确保拖拽后 icon 状态与原窗口一致
  - 简化前端 favicon 处理逻辑,移除重复的图片验证代码
2026-01-09 13:58:22 +00:00
kuaifan
9d62ec1ec1 feat: 添加标签页拖拽排序功能
- 引入 Sortable.js 库以支持标签页的拖拽排序
- 实现标签页的动态插入和顺序重排
- 更新样式以适应拖拽效果
- 增加 IPC 通信以同步标签页顺序变化
- 优化标签页创建和关闭逻辑,提升用户体验
2026-01-09 15:46:02 +08:00
kuaifan
5a4e51d1e0 no message 2026-01-08 14:18:45 +00:00
kuaifan
f0982d7d9a efactor: 拆分 electron 主进程代码为独立模块
将 electron.js 中的 PDF 导出、渲染器辅助函数和工具函数拆分为独立模块:
  - electron/lib/pdf-export.js: PDF 导出相关功能
  - electron/lib/renderer.js: 渲染器辅助函数
  - electron/lib/other.js: 平台检测和 URL 验证常量

  此重构提高了代码可维护性,减少了主文件的复杂度。
2026-01-08 13:54:55 +00:00
kuaifan
1ac3a4cc96 feat: 添加 user_update hook 事件并重构用户生命周期 hook
- 新增 user_update 事件,当用户基本信息变更时触发
  - 扩展 dispatchUserHook payload 包含完整用户信息(tel、profession、birthday、address、introduction、departments)
  - 将 user_onboard/user_offboard/user_update hook 触发逻辑集中到 UserObserver
  - 区分 profile_update(用户自己修改)和 admin_update(管理员修改)事件类型
  - 修复 User::reg() 中 Manticore 索引同步遗漏问题
  - 排除机器人账号的 hook 触发
2026-01-08 11:31:16 +00:00
kuaifan
7f9c42d3d8 no message 2026-01-07 04:11:42 +00:00
kuaifan
4e99e398d6 feat: 添加动态时间提示和自动校正功能
- 在"最早可提前"下方动态显示最早可签到时间
  - 在"最晚可延后"下方动态显示最晚可签到时间(跨天显示"次日"前缀)
  - 输入值变化时自动校正到临界值,防止时间重叠
  - 调整表单布局支持换行显示提示信息
2026-01-07 04:11:32 +00:00
kuaifan
395fc155ce feat: 使用用户头像作为封面背景
在用户详情弹窗的顶部封面区域,使用用户头像作为模糊背景,
提升视觉效果和个性化体验。

- 将用户头像通过 CSS 变量传递给封面区域
- 添加背景模糊滤镜和缩放效果
- 修复容器溢出问题
2026-01-07 03:11:34 +00:00
kuaifan
6bdefc4f03 feat: 支持跨天打卡和时间重叠验证
- 允许签到"最晚可延后"时间超过 23:59:59,支持员工凌晨下班打卡
  - 凌晨打卡记录自动归属前一天
  - 前后端新增提前/延后时间重叠验证,防止产生歧义时间窗口
  - 优化导出逻辑以正确处理跨天打卡记录
  - 打卡消息提示归属日期信息
2026-01-06 12:31:41 +00:00
kuaifan
d4547cbe97 refactor: 移除语言偏好部分,简化文档内容 2026-01-06 08:57:38 +00:00
kuaifan
c9a0b7481a feat: 统一用户编辑入口为独立弹窗组件
- 新增 UserEditModal 组件,整合昵称、电话、职位、邮箱、密码、部门、个人简介、个性标签编辑
  - 签到模式下支持编辑人脸图片和 MAC 地址,并高亮显示相关字段
  - TeamManagement 移除分散的编辑入口(快捷修改、修改邮箱/密码/部门/人脸/MAC 等菜单)
  - 简化 operationUser 方法,移除冗余的 data/watch/methods
2026-01-06 08:55:04 +00:00
kuaifan
f496bc5fca feat: Optimize search functionality and AI module integration
- Refactor Manticore search classes for better performance
- Update AI module with enhanced processing capabilities
- Improve Apps module functionality
- Enhance SearchBox Vue component with new features
2026-01-06 07:25:23 +00:00
kuaifan
4ba02b9dce feat: 优化 remove_by_network 函数以批量删除容器并处理空容器情况 2026-01-06 02:13:15 +00:00
kuaifan
f821e5ad28 refactor: 移除缓存写入逻辑并简化未获取向量填充过程 2026-01-05 12:10:17 +00:00
kuaifan
425f7b6f79 fix: 修复多标签窗口关闭后事件回调导致的崩溃 2026-01-05 09:36:22 +00:00
kuaifan
61d7970b6a feat: 更新 remove_by_network 函数以删除所有状态的容器并等待网络清空 2026-01-05 09:35:39 +00:00
kuaifan
1aa9984535 fix: 会话列表待办完成消息显示最后完成者 2026-01-05 06:31:14 +00:00
kuaifan
8ab810c670 feat: 将 Manticore 相关检查更新为使用 "search" 应用 2026-01-05 05:51:48 +00:00
kuaifan
5cc3d60e15 feat: 添加交互规范,建议在提问时附带具体选项以帮助用户决策 2026-01-05 02:27:18 +00:00
kuaifan
42a2eb56c7 feat: 升级语音识别模型并优化转写逻辑
- 语音识别模型从 whisper-1 升级到 gpt-4o-mini-transcribe
   - 根据用户语言设置自动添加简繁体中文提示词
   - 录音转文字新增 dialog_id 参数,支持获取对话上下文提高识别准确率
   - 移除前端语言手动选择功能,简化用户操作
   - 添加参数空值保护
   - 优化 reasoning_effort 参数逻辑,区分 gpt-5 和 gpt-5.1+ 版本
2026-01-05 02:26:36 +00:00
kuaifan
4b0f4e388c feat: 优化 Manticore 相关描述 2026-01-04 13:30:03 +00:00
kuaifan
31045b3808 feat: 更新 Manticore 数据库插入逻辑,添加 userid 和 tags 字段;在 WebSocket 消息删除时同步 Manticore 2026-01-04 07:48:53 +00:00
kuaifan
a95f22bf42 feat: 添加 ManticoreSyncTask 的去重功能,优化任务投递逻辑 2026-01-04 07:48:32 +00:00
kuaifan
fa84f92577 feat: 添加 ProjectTaskContentObserver 以处理任务内容的创建、更新和删除事件 2026-01-04 07:24:36 +00:00
kuaifan
90a5624877 feat: 添加用户标签功能,更新用户索引以支持标签创建、更新和删除事件 2026-01-04 07:13:13 +00:00
kuaifan
f42250b8b7 feat: 重构文件管理界面,优化文件操作区域布局和样式 2026-01-04 06:13:44 +00:00
kuaifan
b9809d207d feat: 添加同步 responseSeed 方法,避免与已有响应 localId 冲突 2026-01-04 01:40:25 +00:00
kuaifan
0d8e10b60e feat: 优化 IDBClear 方法,支持保留指定键的缓存项 2026-01-04 01:40:13 +00:00
kuaifan
501ff21e55 feat: 添加数值类型转换功能,确保查询结果中的数值类型一致性 2026-01-04 00:29:29 +00:00
kuaifan
4759e28a56 feat: 在 DialogWrapper 组件中添加 search_type 属性以支持文本搜索 2026-01-03 23:20:56 +00:00
kuaifan
bd7841ac05 feat: 添加 TTY 参数检测,优化 Docker 命令执行 2026-01-03 23:09:59 +00:00
kuaifan
ea0d27fdea feat: 添加 Manticore 同步命令通用锁机制,优化信号处理与锁管理 2026-01-03 23:09:50 +00:00
kuaifan
610979f30b feat: Enhance Manticore sync commands with incremental processing and sleep options
- Updated sync commands (SyncFileToManticore, SyncMsgToManticore, SyncProjectToManticore, SyncTaskToManticore, SyncUserToManticore) to support continuous incremental updates until completion.
- Added --sleep option to allow a pause between batches in incremental mode.
- Improved signal handling to allow graceful shutdown during processing.
- Adjusted lock duration to 30 minutes for long-running processes.
- Enhanced logging for better visibility of sync progress and completion.
- Updated ManticoreSyncTask to ensure commands run continuously and check for new data every 2 minutes.
2026-01-03 22:41:49 +00:00
kuaifan
9a8304d595 feat: 增强 Manticore 向量更新逻辑,记录更新失败的 ID 2026-01-03 21:59:44 +00:00
kuaifan
e020a80020 feat: Add batch embedding retrieval and vector update methods for Manticore integration
- Implemented `getBatchEmbeddings` method in AI module for retrieving embeddings for multiple texts.
- Added vector update methods for messages, files, tasks, projects, and users in ManticoreBase.
- Enhanced ManticoreFile, ManticoreMsg, ManticoreProject, ManticoreTask, and ManticoreUser to support vector generation during sync operations.
- Introduced `generateVectorsBatch` methods for batch processing of vector generation in Manticore modules.
- Updated ManticoreSyncTask to handle incremental updates and vector generation asynchronously.
2026-01-03 15:19:23 +00:00
kuaifan
7a21a2d800 refactor: 统一搜索接口,移除 dialog/msg/search
- 前端 DialogWrapper.vue 改用 search/message 接口
  - 删除 DialogController::msg__search 方法
  - search/message 已完全覆盖原接口功能
2026-01-03 13:04:40 +00:00
kuaifan
ec0db3a76c refactor: 提取搜索逻辑到 Model Scope
- User: 新增 scopeSearchByKeyword
  - Project: 新增 scopeSearchByKeyword
  - ProjectTask: 新增 scopeSearchByKeyword
  - File: 新增 scopeSearchByKeyword, scopeSharedToUser
  - WebSocketDialogMsg: 新增 scopeSearchByKeyword, scopeAccessibleByUser
  - SearchController: 使用新的 Model Scope 简化 MySQL 回退逻辑
2026-01-03 07:58:11 +00:00
kuaifan
67fc0781e5 feat: 添加 Claude Code 配置文件
- 创建 CLAUDE.md 项目指南
  - 添加 .claude/rules/graphiti.md Graphiti 长期记忆集成规则
2026-01-03 07:33:35 +00:00
kuaifan
79c2ba140c feat: 更新搜索功能,统一搜索接口,优化请求参数 2026-01-03 04:42:15 +00:00
kuaifan
908171a977 feat: 新增对话ID参数支持,优化搜索功能以支持对话过滤 2026-01-03 03:59:51 +00:00
kuaifan
a52dc14369 feat: Enhance AIAssistant and SearchBox components with improved link handling and search functionality
- Updated AIAssistant to support parsing of additional message links in the format dootask://message/id1/id2.
- Modified search methods in SearchBox to streamline API calls and remove AI search logic, improving performance and clarity.
- Cleaned up unused AI search code and adjusted search result handling for better data presentation.
- Updated documentation to reflect new link formats for tasks, projects, files, and messages.
2026-01-02 09:48:52 +00:00
kuaifan
1e94ce501e refactor: 移除 ZincSearch,统一使用 Manticore Search
- 删除 ZincSearch 模块、任务、命令
- 对话消息搜索改用 ManticoreMsg::searchDialogs
- 移除 Observer 中的 ZincSearch 同步
- 移除定时任务中的 ZincSearch 同步
- 更新项目文档
2026-01-02 07:25:14 +00:00
kuaifan
7a5ef3a491 feat: 新增消息搜索功能
- 新增 msg_vectors 表,支持消息全文/向量/混合搜索
- 采用 MVA 权限方案,allowed_users 内联存储
- 新增 /api/search/message API
- 新增 manticore:sync-msgs 同步命令
- Observer 触发消息创建/更新/删除同步
- Observer 触发对话成员变更时更新 allowed_users
2026-01-02 06:46:18 +00:00
kuaifan
c08323e1ea feat: 迁移至 MVA 权限方案
- 表结构:为 file/project/task_vectors 添加 allowed_users MULTI 字段
- 删除关系表:file_users, project_users, task_users
- 搜索:使用 allowed_users = userid 进行权限过滤
- 同步:sync 时自动计算并写入 allowed_users
- 级联:项目成员变更异步级联 v=1 任务,任务成员变更递归更新子任务
- 覆盖场景:visibility/parent_id/project_id 变更、子任务升级主任务等
2026-01-02 02:03:21 +00:00
kuaifan
fdf5ceeaab feat: Enhance Manticore integration and AI model support
- Added support for specifying vector dimensions in AI payloads for compatible vendors.
- Updated default AI model from 'text-embedding-ada-002' to 'text-embedding-3-small'.
- Refactored ManticoreBase to bind parameters explicitly for PDO statements, improving type handling.
- Adjusted SQL queries across Manticore modules to remove content previews and ensure inline vector values.
- Updated content preview handling in ManticoreFile, ManticoreProject, ManticoreTask, and ManticoreUser to use substrings for better data management.
2026-01-01 08:59:54 +00:00
kuaifan
48ef4cfdef refactor: 使用 Manticore Search 替换 SeekDB 2026-01-01 03:17:27 +00:00
kuaifan
10c6177a9f no message 2025-12-31 16:55:33 +00:00
kuaifan
0362c83e77 feat: 支持 AI 助手输入框回车快捷操作
- 新增 onInputKeydown 方法:支持回车发送、Shift+Enter 换行,提升输入体验。
- 更新输入框组件,绑定键盘事件,实现更流畅的交互。
- 自动聚焦输入框,提升用户体验。
2025-12-31 09:57:34 +00:00
kuaifan
1af29837e2 feat: 增加增量同步功能以优化 SeekDB 用户关系同步
- 在 SyncFileToSeekDB、SyncProjectToSeekDB 和 SyncTaskToSeekDB 中实现增量同步逻辑,支持只同步新增的用户关系。
- 新增 syncFileUsersIncremental、syncProjectUsersIncremental 和 syncTaskUsersIncremental 方法,提升数据同步效率。
- 更新相关命令行输出信息,以清晰指示同步状态和进度。
2025-12-31 09:28:10 +00:00
kuaifan
986c4871df feat: Enhance AI Assistant with session management and improved UI
- Added session management capabilities to the AI Assistant, allowing users to create, load, and delete sessions.
- Improved modal UI with a new header for session actions and a footer for model selection.
- Updated input handling to support dynamic loading of session data and improved response formatting.
- Enhanced search functionality in various components to utilize the AI Assistant for generating content based on user input.
2025-12-31 08:47:03 +00:00
kuaifan
fe7a2a0e73 feat: 扩展 SeekDB 支持联系人、项目、任务的 AI 搜索
- 合并 SeekDBFileSyncTask 到 SeekDBSyncTask
- 统一 AI 搜索 API 入口
2025-12-30 07:48:00 +00:00
kuaifan
23faf28f7f feat: 集成 SeekDB AI 搜索引擎实现文件内容搜索 2025-12-30 05:49:26 +00:00
kuaifan
a8d4f261a4 no message 2025-12-30 05:49:18 +00:00
kuaifan
a336fd4a1a feat: omit content from report list APIs 2025-12-30 01:58:03 +00:00
kuaifan
8759e6fd7e build 2025-12-30 09:20:59 +08:00
kuaifan
92d23014a7 fix: avoid opening blank dialog window when dialogId is 0 2025-12-29 16:22:06 +00:00
kuaifan
7c3f33ea0d fix: avoid mutating task getter arrays in mention list 2025-12-29 16:01:37 +00:00
kuaifan
16a55de6f1 feat: 增强搜索功能,支持通过 ID、名称和其他字段搜索任务、文件和报告 2025-12-29 15:43:50 +00:00
kuaifan
869ac7d316 feat: 更新 appstore 镜像版本至 0.3.8 2025-12-27 10:29:51 +00:00
kuaifan
55303689ea feat: support configurable default priority 2025-12-26 02:42:47 +00:00
kuaifan
c69123ac92 no message 2025-12-24 09:49:21 +00:00
kuaifan
7bce5f1c1f feat: 添加迁移脚本以为相关表添加索引 2025-12-24 09:18:48 +00:00
kuaifan
989660969c feat: 添加迁移脚本以反转待办消息中的用户ID顺序 2025-12-24 07:11:01 +00:00
kuaifan
862acd0776 fix: 修复行前缀检测逻辑,确保正确判断空行 2025-12-24 06:30:43 +00:00
kuaifan
3b3ffd494f feat: 规范以斜杠开头的命令 2025-12-24 06:10:39 +00:00
kuaifan
6cf8290565 feat: 增强斜杠命令支持,添加机器人命令和行首检测功能 2025-12-24 05:58:48 +00:00
kuaifan
230ebbcfb9 feat: support slash trigger for mention/task/file/report 2025-12-24 00:59:31 +00:00
kuaifan
dc77f1cda1 build 2025-12-23 09:51:18 +08:00
kuaifan
1f791b528a fix: 更新对话ID和场景信息的描述,增加字段标识 2025-12-23 01:40:53 +00:00
kuaifan
1459d953ed feat: 更新获取消息列表MCP工具的描述,增强功能说明 2025-12-22 03:44:33 +00:00
kuaifan
719a36b275 chore: update mobile subproject commit reference 2025-12-19 22:35:57 +08:00
kuaifan
0b7a3046fe fix: align parent task subtask progress with task detail (include archived, exclude deleted) 2025-12-19 21:36:00 +08:00
kuaifan
203d107d68 fix: skip loading related tasks for subtasks to prevent request spam 2025-12-19 19:37:07 +08:00
kuaifan
17fd7f02a6 build 2025-12-19 09:13:49 +08:00
kuaifan
57ea4f2b6f feat: 自定义应用菜单新增 immersive 沉浸式开关 2025-12-19 01:07:02 +00:00
kuaifan
df431eea46 no message 2025-12-18 23:12:53 +00:00
kuaifan
ad9dd6330f feat: merge todo done notices and render done_userids 2025-12-18 23:03:11 +00:00
kuaifan
df9d291f98 feat: 优化群组资料修改逻辑,增加权限判断和名称修改提示 2025-12-18 21:53:04 +00:00
kuaifan
0cf7fc2ed2 feat: replace group name quick edit with modify trigger 2025-12-18 21:42:15 +00:00
kuaifan
e8f82baa99 feat: 添加 urlType 字段以兼容旧版本微应用配置 2025-12-18 21:06:49 +00:00
kuaifan
353a05f344 feat: 优化 openMicroApp 方法,增强参数校验和微应用 ID 解析逻辑 2025-12-18 20:59:44 +00:00
kuaifan
d94ebfe04c feat: 添加解析类型的方法,优化微应用配置逻辑 2025-12-18 08:26:42 +00:00
kuaifan
52913abb4f feat: 更新 appstore 镜像版本至 0.3.7 2025-12-18 02:47:39 +00:00
kuaifan
d77406951d feat: 更新微应用菜单配置,统一使用类型字段替代URL类型字段 2025-12-18 02:44:37 +00:00
kuaifan
8c23192eeb build 2025-12-17 09:30:53 +08:00
kuaifan
078c9c198d feat: 更新 appstore 镜像版本至 0.3.6 2025-12-16 11:32:33 +00:00
kuaifan
6cfe2d226a feat: 增加获取胶囊可见性的方法,优化胶囊显示逻辑 2025-12-16 11:31:50 +00:00
kuaifan
fee1c12357 feat: 添加导航功能,支持快捷键和鼠标手势操作 2025-12-16 18:36:11 +08:00
kuaifan
a6385b699e fix: 修复在某些情况下无法打开微应用的问题 2025-12-14 22:36:14 +00:00
kuaifan
718ed8953f no message 2025-12-14 00:23:04 +00:00
kuaifan
a1eea77b9e feat: 更新 appstore 镜像版本至 0.3.5 2025-12-12 07:12:07 +00:00
kuaifan
6eb08ac09b build 2025-12-11 10:28:18 +08:00
kuaifan
20fc2b073b no message 2025-12-11 02:09:59 +00:00
kuaifan
8c4b9e8d12 feat: 优化项目/报告控制器及任务模型 2025-12-11 02:06:13 +00:00
kuaifan
8d187f5cfc feat: 优化周报/日报模板的已完成与未完成任务规则 2025-12-11 01:35:10 +00:00
kuaifan
db07a96e97 fix: 修复任务导出状态判断及状态高亮列错位问题 2025-12-11 01:13:03 +00:00
kuaifan
7acc9227ff fix: 修复任务统计导出漏掉无计划时间已完成任务的问题 2025-12-11 00:43:54 +00:00
kuaifan
c3a71e5b07 feat: 更新 appstore 镜像版本至 0.3.4 2025-12-10 02:01:43 +00:00
kuaifan
ac9e1e5e67 feat: call appstore user lifecycle hooks from main app 2025-12-09 10:30:23 +00:00
kuaifan
c668340661 feat: 优化消息推送逻辑 2025-12-05 02:10:37 +00:00
kuaifan
ee9b6248bb fix(electron): cleanup child windows by instance instead of name 2025-12-04 11:18:47 +00:00
kuaifan
01c7f7250b fix: 修复关闭应用时加载状态未正确更新的问题 2025-12-03 12:48:33 +00:00
kuaifan
2abc5976f9 fix: 更新 iframe 的 sandbox 属性以增强安全性 2025-12-02 12:03:54 +00:00
kuaifan
3e468c74e4 fix: 修改微模态框的最小高度设置 2025-12-02 11:46:46 +00:00
kuaifan
4ef78d2c81 feat: 添加点击消息打开微应用功能 2025-12-02 06:29:45 +00:00
kuaifan
4621222fa3 build 2025-11-30 12:18:18 +08:00
kuaifan
be860f9968 fix: load fastmcp via dynamic import in electron MCP 2025-11-30 12:13:31 +08:00
kuaifan
fe0b8aed20 no message 2025-11-28 22:09:55 +00:00
kuaifan
f0e844c308 feat: 添加个人任务上限设置,限制负责人或协助人的未完成任务数量 2025-11-28 11:05:08 +00:00
kuaifan
6a7cc95b23 feat: 添加颜色工具函数,支持颜色反转和解析 2025-11-28 09:35:01 +00:00
kuaifan
7fd90b9ceb feat: 添加对话框顶部消息样式 2025-11-28 08:58:14 +00:00
kuaifan
43577073e6 fix: 调整各组件最大高度计算,考虑状态栏和导航栏高度 2025-11-28 02:27:03 +00:00
kuaifan
faeeb09a4a fix: 修复微模态组件的样式,调整为固定定位以适应全屏显示 2025-11-28 01:33:49 +00:00
kuaifan
d88349b6f7 feat: 使用 CSS 变量动态调整窗口高度,优化各组件的最大高度设置 2025-11-28 01:33:35 +00:00
kuaifan
ff53e1fac3 fix: enforce positive rounded size in normalizeSize 2025-11-27 10:40:45 +08:00
kuaifan
cf4894b7c3 no message 2025-11-27 02:24:40 +00:00
kuaifan
678dfd2d5c feat: 更新 appstore 镜像版本 2025-11-27 02:24:34 +00:00
kuaifan
bf4a62ae04 feat: 更新文档,添加前端弹窗文案处理说明 2025-11-24 01:23:39 +00:00
kuaifan
7e6f3f92cf feat: 添加 URL 输入提示,优化 iframe 测试功能的用户体验 2025-11-24 01:23:22 +00:00
kuaifan
df382dafb4 no message 2025-11-24 00:38:16 +00:00
kuaifan
10925d3a47 no message 2025-11-20 06:19:29 +00:00
kuaifan
66252072c7 feat: 添加 iframe 测试功能,支持通过 URL 加载外部内容 2025-11-20 06:18:56 +00:00
kuaifan
29918882bd no message 2025-11-19 07:54:56 +00:00
kuaifan
4983fe8feb feat: 添加自定义微应用菜单功能,支持管理员配置和保存菜单项 2025-11-19 07:54:47 +00:00
kuaifan
f65da118d7 feat: 更新 appstore 镜像版本至 0.3.2 2025-11-15 09:17:38 +00:00
kuaifan
a86bd9a05e fix: 修复桌面端部分机器新窗口任务报错的情况 2025-11-14 09:48:10 +00:00
kuaifan
f2719eb742 feat: 更新助手默认模型为 gpt-5.1-mini 2025-11-14 01:20:41 +00:00
kuaifan
4f9ee1dfa9 no message 2025-11-14 01:17:48 +00:00
kuaifan
e6ad1218bc feat: 添加一键归档列表中已完成任务 2025-11-14 01:15:19 +00:00
kuaifan
dd2cd1df9a feat: 更新 OnlyOffice 组件的主题名称;优化文件管理页面的列表渲染;调整抽屉和文件内容的圆角样式 2025-11-13 06:20:21 +08:00
kuaifan
6dcbe8ba38 build 2025-11-12 16:46:33 +08:00
kuaifan
360d4dbbe2 no message 2025-11-12 07:18:54 +00:00
kuaifan
2f32b53d19 feat: 修改 getDomain 函数以支持可选的小写转换参数;更新 getObject 函数的默认值 2025-11-12 07:07:00 +00:00
kuaifan
6a3e3c3753 feat: AI 助手增加最大响应数至50,并添加上下文窗口大小设置 2025-11-12 01:23:34 +00:00
kuaifan
5ad08d8d36 no message 2025-11-12 01:06:36 +00:00
kuaifan
b892d92614 build 2025-11-12 07:11:38 +08:00
kuaifan
b259f083d4 no message 2025-11-12 07:05:46 +08:00
kuaifan
38aa9fe2fb build 2025-11-12 00:30:39 +08:00
kuaifan
863dd3a53e no message 2025-11-11 22:42:45 +08:00
kuaifan
bea5058df8 feat: 优化错误处理逻辑,简化错误消息输出 2025-11-11 21:49:09 +08:00
kuaifan
31c157f58f no message 2025-11-11 21:40:34 +08:00
kuaifan
8af6887daa feat: 优化WebSocketDialogMsg和BotReceiveMsgTask中的消息格式,统一中文标点,增强可读性 2025-11-11 13:05:04 +00:00
kuaifan
eb9b7b4f86 feat: 更新MCP工具描述 2025-11-11 07:16:04 +00:00
kuaifan
cf78766a37 feat: 移除未使用的消息处理函数和Markdown插件任务创建功能,优化代码结构 2025-11-11 05:42:02 +00:00
kuaifan
944824b552 feat: 移除未使用的函数和代码,优化BotReceiveMsgTask和WebSocketDialogMsg的消息处理逻辑 2025-11-11 05:31:59 +00:00
kuaifan
477bb1ac8f feat: MCP增加文件管理功能,支持获取文件访问URL、文件列表和文件搜索 2025-11-11 05:23:00 +00:00
kuaifan
29df864ecb feat: MCP增加工作报告相关功能,包括获取汇报列表、获取汇报详情、生成汇报模板、创建汇报及标记已读/未读状态 2025-11-11 02:24:35 +00:00
kuaifan
bcf897b7e0 no message 2025-11-10 23:03:42 +00:00
kuaifan
e63890c755 feat: 重构隐私政策页面,优化结构和样式,增强可读性 2025-11-10 23:01:39 +00:00
kuaifan
f3725215bd feat: 简化长按指令的参数配置 2025-11-10 22:43:25 +00:00
kuaifan
c43e305ea7 feat: 优化AI输出语言策略提示词 2025-11-10 22:36:37 +00:00
kuaifan
b9215e2410 feat: 添加语言偏好提示功能到AI系统提示 2025-11-10 16:46:29 +00:00
kuaifan
19d79ab055 feat: 优化触摸设备交互
- 触摸设备取消拖动选中文件
2025-11-10 16:14:01 +00:00
kuaifan
64d4492806 feat: 优化AI助手响应构建
- 增加剔除推理块功能
2025-11-10 16:13:05 +00:00
kuaifan
0790eae8c6 no message 2025-11-10 15:20:31 +00:00
kuaifan
e10e2c27c1 feat: 优化导出菜单交互 2025-11-10 07:59:52 +00:00
kuaifan
d30b38d4b9 feat: 添加应用排序功能 2025-11-10 07:47:00 +00:00
kuaifan
f6e4ed7c60 no message
- 添加AI助手流式会话凭证生成方法
- 优化AI助手模型获取逻辑
- 更新相关接口调用
2025-11-09 22:20:38 +00:00
kuaifan
7a6bbfac75 feat: 更新AI模块的transcriptions方法,增加扩展请求头参数,优化语音识别功能 2025-11-09 04:43:17 +00:00
kuaifan
425d6f9a06 feat: 移除冗余的AI助手设置方法,优化AI模块的模型配置逻辑 2025-11-09 04:28:51 +00:00
kuaifan
58c760bb77 no message 2025-11-09 02:14:27 +00:00
kuaifan
3ffdce5e7a no message 2025-11-08 23:54:18 +00:00
kuaifan
8e518a044a feat: 优化AI助手输出界面,简化状态显示逻辑,增强用户交互体验 2025-11-08 23:43:06 +00:00
kuaifan
a5adbf80a9 feat: 重构报告分析功能,更新API接口,移除冗余代码,优化分析逻辑 2025-11-08 22:18:59 +00:00
kuaifan
0b6c478b4f feat: 优化报告AI整理功能,优化报告编辑逻辑,移除冗余代码 2025-11-08 21:53:02 +00:00
kuaifan
0434bde16f feat: 移除冗余的AI任务和项目生成逻辑,优化代码结构 2025-11-08 21:52:26 +00:00
kuaifan
0deb3113b5 feat: 引入文本提取功能,优化AI内容解析逻辑,移除冗余代码 2025-11-08 20:42:21 +00:00
kuaifan
ecb52c76b9 feat: 完善AI助手功能 2025-11-08 08:57:22 +00:00
kuaifan
69c66053b7 feat: 完善AI助手功能,新增消息提示词整理接口,优化流式消息处理逻辑,移除冗余数据表和相关代码 2025-11-07 22:25:45 +00:00
kuaifan
892ad395a7 feat: 添加额外数据处理,优化AI助手消息生成与发送逻辑 2025-11-07 20:38:06 +00:00
kuaifan
e801c09c0f feat: 增强AI助手响应处理,支持流式输出和模型缓存 2025-11-07 08:13:51 +00:00
kuaifan
ad560a8555 feat: 增强流消息处理,支持回应和会话ID 2025-11-07 08:13:41 +00:00
kuaifan
e75aa5c2b9 feat: 创建新 AI 会话时将旧会话消息批量标记已读 2025-11-07 07:54:04 +00:00
kuaifan
e83fd7af1b feat: 优化 AI 助手,支持自定义模型 2025-11-07 07:01:15 +00:00
kuaifan
eaec8ef994 no message 2025-11-07 01:00:30 +00:00
kuaifan
3339e6b442 feat: 添加文件列表滚动事件处理,优化右键菜单显示逻辑 2025-11-07 01:00:22 +00:00
kuaifan
4c2425c758 feat: 优化链接获取逻辑 2025-11-06 14:53:16 +00:00
kuaifan
80d1e6469e no message 2025-11-06 14:23:39 +00:00
kuaifan
2fad6394ee no message 2025-11-06 14:03:58 +00:00
kuaifan
4bfe33a37f feat: 优化打开会话事件接口,优化机器人webhook逻辑
- 新增 `open__event` 方法用于处理打开会话事件
- 移除旧的 `open__webhook` 方法
- 更新前端调用逻辑,使用新的事件接口
- 优化 webhook 事件推送逻辑,简化参数传递
2025-11-06 13:59:10 +00:00
kuaifan
130c8bf3b1
Merge pull request #289 from nightcp/dev
feat: 调整机器人webhook事件
2025-11-06 15:24:06 +08:00
kuaifan
b9df277104 no message 2025-11-06 07:16:29 +00:00
kuaifan
97e1f321ca feat: 优化长文本预览组件 2025-11-06 07:00:11 +00:00
王昱
4933930afd feat: 调整机器人webhook事件
- 可取消接收消息事件
- 打开机器人会话窗口时推送webhook消息,相同机器人消息缓存1分钟
2025-11-06 04:08:39 +00:00
kuaifan
ab4640382d feat: 添加会员扩展信息接口,优化用户详情和个人设置页面 2025-11-06 02:01:15 +00:00
kuaifan
e4cfa4b405 feat: 优化个性标签 2025-11-05 22:19:45 +00:00
kuaifan
789062e85e
Merge pull request #288 from xxyijixx/dev-profile
Dev profile
2025-11-05 17:11:46 +08:00
kuaifan
5370bee369 Merge branch 'dev' into pro
# Conflicts:
#	CHANGELOG.md
#	cmd
#	package.json
#	public/js/build/404.5645cb91.js
#	public/js/build/404.9598cd97.js
#	public/js/build/404.a5736629.js
#	public/js/build/AceEditor.8747edb1.js
#	public/js/build/AceEditor.af35593f.js
#	public/js/build/AceEditor.e7f5b602.js
#	public/js/build/DialogWrapper.0c7cd033.js
#	public/js/build/DialogWrapper.64072671.js
#	public/js/build/DialogWrapper.7fcb5b27.js
#	public/js/build/Drawio.2ca59c31.js
#	public/js/build/Drawio.6691a6ef.js
#	public/js/build/Drawio.e3576e4e.js
#	public/js/build/FileContent.3a899bcc.js
#	public/js/build/FileContent.c311c89c.js
#	public/js/build/FileContent.d8e600e1.js
#	public/js/build/FilePreview.87ca99d9.js
#	public/js/build/FilePreview.f8134ee5.js
#	public/js/build/FilePreview.f9f90ff4.js
#	public/js/build/IFrame.02598edc.js
#	public/js/build/IFrame.2a7489ee.js
#	public/js/build/IFrame.be9780e1.js
#	public/js/build/ImgUpload.29e2d88d.js
#	public/js/build/ImgUpload.a4eff264.js
#	public/js/build/ImgUpload.e96999cf.js
#	public/js/build/Minder.2bce6c16.js
#	public/js/build/Minder.b1d1145f.js
#	public/js/build/Minder.f5bc5aca.js
#	public/js/build/OnlyOffice.31e7af4f.js
#	public/js/build/OnlyOffice.574ad560.js
#	public/js/build/OnlyOffice.9ce921ed.js
#	public/js/build/ReportEdit.5eb3a319.js
#	public/js/build/ReportEdit.9141bb93.js
#	public/js/build/ReportEdit.e3369e09.js
#	public/js/build/SearchButton.906cea81.js
#	public/js/build/SearchButton.cf201525.js
#	public/js/build/SearchButton.d41addb6.js
#	public/js/build/TEditor.7b9a9d91.js
#	public/js/build/TEditor.971af80f.js
#	public/js/build/TEditor.cc94d929.js
#	public/js/build/TaskDetail.38815236.js
#	public/js/build/TaskDetail.d1a9952e.js
#	public/js/build/TaskDetail.dfd78b4a.js
#	public/js/build/add.0cfbdd9e.js
#	public/js/build/add.3673f91c.js
#	public/js/build/add.423bc480.js
#	public/js/build/application.005cc174.js
#	public/js/build/application.5587ac3b.js
#	public/js/build/application.5b8f123b.js
#	public/js/build/apps.4e0bf65b.js
#	public/js/build/apps.b0a3d4f5.js
#	public/js/build/apps.f77a8c4e.js
#	public/js/build/calendar.31470aa0.js
#	public/js/build/calendar.ad5d85d5.js
#	public/js/build/calendar.e08e7575.js
#	public/js/build/checkin.5d4c364e.js
#	public/js/build/checkin.ab08f01e.js
#	public/js/build/checkin.c05284a9.js
#	public/js/build/dashboard.7cced7be.js
#	public/js/build/dashboard.c82415db.js
#	public/js/build/dashboard.f6ed8299.js
#	public/js/build/dayjs.495f600d.js
#	public/js/build/dayjs.71653272.js
#	public/js/build/dayjs.cf033d87.js
#	public/js/build/delete.4072c68f.js
#	public/js/build/delete.5f06c51d.js
#	public/js/build/delete.b26aa3fd.js
#	public/js/build/device.4cff22ad.js
#	public/js/build/device.66a7e05a.js
#	public/js/build/device.a13f3ef0.js
#	public/js/build/dialog.97b951ce.js
#	public/js/build/dialog.e9f6d55f.js
#	public/js/build/dialog.eb7b795a.js
#	public/js/build/editor.18a511b5.js
#	public/js/build/editor.2cca497c.js
#	public/js/build/editor.e034df4e.js
#	public/js/build/email.0643f86b.js
#	public/js/build/email.1d00cb0c.js
#	public/js/build/email.d95a35c0.js
#	public/js/build/file.4fe82c29.js
#	public/js/build/file.684a63df.js
#	public/js/build/file.9dceb82f.js
#	public/js/build/fileMsg.0a0029c2.js
#	public/js/build/fileMsg.1f4ecb0f.js
#	public/js/build/fileMsg.f99b6f61.js
#	public/js/build/fileTask.72914205.js
#	public/js/build/fileTask.bf35fb6b.js
#	public/js/build/fileTask.f4356f14.js
#	public/js/build/index.236af26f.js
#	public/js/build/index.299c9f99.js
#	public/js/build/index.2ffa8f9e.js
#	public/js/build/index.7d6e1bbe.js
#	public/js/build/index.94a5d2da.css
#	public/js/build/index.af34aeb9.js
#	public/js/build/index.b0ae9460.js
#	public/js/build/index.b69b5f25.js
#	public/js/build/index.b71c2859.js
#	public/js/build/index.c3968cad.js
#	public/js/build/index.d1ae44be.js
#	public/js/build/index.e07db7f9.css
#	public/js/build/index.edee4b6e.css
#	public/js/build/index.ef9e1e57.js
#	public/js/build/index.fe32159a.js
#	public/js/build/jquery.0909250e.js
#	public/js/build/jquery.16b446fd.js
#	public/js/build/jquery.27f590f5.js
#	public/js/build/keyboard.3f5b3ac6.js
#	public/js/build/keyboard.5de3dd2c.js
#	public/js/build/keyboard.c3ef7d49.js
#	public/js/build/language.1fadd54c.js
#	public/js/build/language.8bb72294.js
#	public/js/build/language.f3d03ece.js
#	public/js/build/license.21482fde.js
#	public/js/build/license.60871496.js
#	public/js/build/license.add318a7.js
#	public/js/build/localforage.65ac7a2a.js
#	public/js/build/localforage.be4775a0.js
#	public/js/build/localforage.dd58f5ac.js
#	public/js/build/login.7560afa5.js
#	public/js/build/login.75b3978c.js
#	public/js/build/login.aa163163.js
#	public/js/build/meeting.a60d7e8d.js
#	public/js/build/meeting.aa5510c7.js
#	public/js/build/meeting.fdb9793b.js
#	public/js/build/password.267357fd.js
#	public/js/build/password.749ce44d.js
#	public/js/build/password.e6d81eb1.js
#	public/js/build/personal.69279937.js
#	public/js/build/personal.a27cef8e.js
#	public/js/build/personal.c613af3c.js
#	public/js/build/preload.5827bd38.js
#	public/js/build/preload.8ec61a5b.js
#	public/js/build/preload.c6189d87.js
#	public/js/build/preview.29e49902.js
#	public/js/build/preview.7329f0f4.js
#	public/js/build/preview.b452b0ee.js
#	public/js/build/preview.c64402ed.js
#	public/js/build/preview.ec796a92.js
#	public/js/build/preview.ec85a43c.js
#	public/js/build/pro.2128a514.js
#	public/js/build/pro.213d8da6.js
#	public/js/build/pro.9fb60d27.js
#	public/js/build/projectInvite.0b3bf524.js
#	public/js/build/projectInvite.393920f8.js
#	public/js/build/projectInvite.e9cee390.js
#	public/js/build/reportDetail.2db50632.js
#	public/js/build/reportDetail.90aaf973.js
#	public/js/build/reportDetail.d93cc650.js
#	public/js/build/reportEdit.84a81076.js
#	public/js/build/reportEdit.8baf23d4.js
#	public/js/build/reportEdit.d008dd34.js
#	public/js/build/swipe.0c72cce1.js
#	public/js/build/swipe.4567bb5d.js
#	public/js/build/swipe.92aebd0c.js
#	public/js/build/system.67c1b700.js
#	public/js/build/system.c45c70de.js
#	public/js/build/system.f3384133.js
#	public/js/build/task.1b9e0e77.js
#	public/js/build/task.a445c89e.js
#	public/js/build/task.d43091db.js
#	public/js/build/taskContent.20b80714.js
#	public/js/build/taskContent.3ebbd2f9.js
#	public/js/build/taskContent.9dc7a121.js
#	public/js/build/theme.72d103d1.js
#	public/js/build/theme.7f1b2ffd.js
#	public/js/build/theme.df79fe8f.js
#	public/js/build/token.0ecffef5.js
#	public/js/build/token.a7f5ccf5.js
#	public/js/build/token.ece75257.js
#	public/js/build/validEmail.1462dd30.js
#	public/js/build/validEmail.17a3e0d2.js
#	public/js/build/validEmail.ee19c1f3.js
#	public/js/build/version.137935c7.js
#	public/js/build/version.1441c1fd.js
#	public/js/build/version.b0154505.js
#	public/js/build/video.03b62c93.js
#	public/js/build/video.2dc7f3c6.js
#	public/js/build/video.531c68e2.js
#	public/js/build/view.18713f1b.js
#	public/js/build/view.7770155e.js
#	public/js/build/view.8c6a0cc1.js
#	public/manifest.json
2025-11-05 16:55:17 +08:00
kuaifan
2f972488a1
Merge pull request #287 from nightcp/dev
feat: 优化用户机器人 webhook 逻辑
2025-11-05 16:30:37 +08:00
kuaifan
6f7656802f no message 2025-11-05 06:20:04 +00:00
kuaifan
7d98c5493e feat: 添加AI整理工作汇报功能 2025-11-05 04:02:29 +00:00
kuaifan
e0443aa336 feat: 添加AI分析工作汇报功能 2025-11-05 04:02:06 +00:00
kuaifan
39ff0d1516 feat: 将AI助手从gpt-5-nano更改为gpt-5-mini 2025-11-05 01:58:24 +00:00
kuaifan
1b9c0ee4b8 feat: 优化AI助手入口 2025-11-05 01:55:59 +00:00
kuaifan
d48287f93a feat: 添加判断是否为iPad的功能,并在预加载时处理安全区域 2025-11-04 13:08:23 +08:00
kuaifan
717e87cfa9 feat: 更新抽屉样式以支持横屏模式下的最大宽度设置 2025-11-04 13:06:19 +08:00
kuaifan
708b488af8 fix: 修复android分享页面元素重叠的情况 2025-11-03 16:56:20 +08:00
kuaifan
d60d3f374b feat: 调整对话框尺寸计算,避免发送消息失败的情况 2025-11-03 14:46:46 +08:00
kuaifan
8b87a2bc40 feat: 添加聊天输入历史记录功能 2025-11-03 02:12:05 +00:00
kuaifan
d0da517503 no message 2025-11-03 00:43:28 +00:00
kuaifan
754036c472 build 2025-11-03 08:05:35 +08:00
kuaifan
720438fd91 Merge commit '96106498d8c480c3ea7ec493bfb063450e11b7b5' into pro 2025-11-03 08:00:22 +08:00
kuaifan
ba76df1b00 no message 2025-11-03 08:00:15 +08:00
kuaifan
44d85c2864 feat: 增加对应用平台的 overscroll-behavior 设置
- 优化iOS15滚动超限的情况
2025-11-03 07:51:44 +08:00
kuaifan
1c8b73a381 feat: 重构胶囊缓存逻辑,增加设置和移除缓存的方法 2025-11-03 01:29:34 +08:00
kuaifan
b445af932c feat: 更新消息推送逻辑 2025-11-03 00:45:34 +08:00
kuaifan
5121739fe4 feat: 优化应用激活逻辑,增加 IndexedDB 测试失败时的提前返回处理 2025-11-03 00:34:32 +08:00
kuaifan
96106498d8 feat: 添加Umeng日志模型及数据库迁移 2025-11-01 16:15:32 +00:00
kuaifan
0116d92021 feat: 给支持角标的Android设备推送添加角标 2025-11-01 16:15:25 +00:00
kuaifan
43746634a5 no message 2025-10-31 08:27:44 +00:00
kuaifan
5183786fb0 no message 2025-10-30 20:04:41 +00:00
kuaifan
5ba0eed721 no message 2025-10-29 00:15:45 +00:00
kuaifan
7d08c735ef no message 2025-10-28 11:35:36 +00:00
kuaifan
e3067b685c no message 2025-10-28 09:23:41 +00:00
kuaifan
b219ca4c1c no message 2025-10-27 20:57:42 +00:00
kuaifan
9e5d16ff16 feat: 添加 MCP 服务器类型为 streamable-http 2025-10-27 02:49:53 +00:00
kuaifan
da630458e1 fix: 修复任务操作无法点击确定 2025-10-27 02:45:29 +00:00
kuaifan
ee2eceffb0 build 2025-10-27 06:39:25 +08:00
kuaifan
c8d22e7b5f no message 2025-10-27 06:35:27 +08:00
kuaifan
342e8725bd feat: 更新 MCP 服务器配置和工具 2025-10-27 06:34:47 +08:00
kuaifan
3ced00de1f no message 2025-10-27 06:34:47 +08:00
kuaifan
7fa075fa75 no message 2025-10-26 09:59:37 +08:00
kuaifan
95ca496691 feat: 优化获取任务子任务数据相关逻辑 2025-10-26 09:30:24 +08:00
kuaifan
50b1d93f08 no message 2025-10-26 09:21:58 +08:00
kuaifan
8958f2f234 feat: 添加MCP服务器状态切换功能 2025-10-25 16:39:50 +08:00
kuaifan
00b4d6a748 no message 2025-10-25 10:46:01 +08:00
kuaifan
f4de0d8276 feat: 更新MCP工具,添加项目管理功能及任务创建、更新接口 2025-10-25 10:45:46 +08:00
kuaifan
cfa749f4f3 feat: 优化时间范围参数 2025-10-24 23:48:35 +08:00
kuaifan
eeaff08673 feat: 桌面端添加MCP服务 2025-10-24 23:48:18 +08:00
kuaifan
0475e88dc2 feat: 添加任务移动权限检查以增强项目任务管理 2025-10-24 06:35:22 +00:00
kuaifan
e1f73a4639 feat: 为列表项添加最小高度以改善可读性 2025-10-24 05:42:57 +00:00
kuaifan
e2296a6f64 feat: 添加子任务升级为主任务功能 2025-10-24 05:38:54 +00:00
kuaifan
1a6abf4e1b feat: 在安装和更新函数中添加sudo检查 2025-10-24 03:34:22 +00:00
kuaifan
315851eb5f feat: 优化数据库还原功能
- 支持通过编号选择备份文件
2025-10-23 22:55:29 +00:00
kuaifan
0b99b4a9a0 fix: 修复用户选择在输入法预输入时误删已选项 2025-10-23 06:07:24 +00:00
王昱
66002ff401
Merge branch 'kuaifan:dev' into dev 2025-10-22 17:30:34 +08:00
nightcp
bdfc8bdd0c feat: 添加机器人消息推送参数文档,增强 webhook 事件说明 2025-10-22 17:29:32 +08:00
nightcp
98e4668969 feat: 优化用户机器人 webhook 逻辑 2025-10-21 13:53:16 +08:00
kuaifan
e8235dd0a2 feat: 优化已读消息标记逻辑,提升性能和可读性 2025-10-17 00:41:38 +00:00
kuaifan
123c74de46 feat: 优化开发环境配置 2025-10-16 23:56:48 +00:00
yatgei
c92b9bf0fb feat: 在用户详情组件中添加创建群组按钮功能 2025-10-14 18:29:21 +08:00
yatgei
b4cbfd2ae9 feat: 更新用户详情组件样式,调整布局和颜色 2025-10-14 14:01:03 +08:00
yatgei
dd7eee277e feat: 添加共同群组对话框组件并在用户详情中集成 2025-10-13 18:22:25 +08:00
kuaifan
ab76185434 feat: 优化个人资料卡片 2025-10-13 06:56:44 +00:00
kuaifan
6d97bf1e88 feat: 添加个性标签管理功能 2025-10-12 23:02:34 +00:00
kuaifan
49701fcd09 feat: 会员资料窗口添加创建群组按钮 2025-10-12 15:15:34 +00:00
kuaifan
40f04d9860 feat: 添加用户生日、地址和个人简介 2025-10-12 15:07:10 +00:00
kuaifan
d58dd25dbb feat: 添加用户生日、地址和个人简介 2025-10-12 15:05:05 +00:00
kuaifan
9b2731607b feat: 优化开发环境配置 2025-10-11 10:42:49 +00:00
kuaifan
a8d2d6f13f feat: 优化开发环境配置 2025-10-11 02:53:17 +00:00
kuaifan
7c21782ab5 no message 2025-10-08 04:34:31 +00:00
kuaifan
f59bdaf5e0 feat: 添加用户机器人 webhook 事件配置,优化相关逻辑 2025-09-30 04:25:50 +00:00
kuaifan
9419ddd174 no message 2025-09-29 09:19:28 +08:00
kuaifan
0666a8f5c2 feat: 优化任务可见性推送逻辑 2025-09-29 09:04:31 +08:00
kuaifan
81c019105c no message 2025-09-28 10:40:48 +08:00
kuaifan
6584259454 build 2025-09-28 08:38:48 +08:00
kuaifan
03d0f56095 no message 2025-09-28 08:16:53 +08:00
kuaifan
6ffd169784 build 2025-09-28 06:54:05 +08:00
kuaifan
406f64a7c5 no message 2025-09-28 06:46:19 +08:00
kuaifan
1353a2c4c9 no message 2025-09-28 06:34:35 +08:00
kuaifan
fb88f3bd96 no message 2025-09-28 06:33:38 +08:00
kuaifan
22b3598704 feat: 优化共同群聊计数缓存 2025-09-28 06:28:24 +08:00
kuaifan
b62c580d5e no message 2025-09-28 05:55:02 +08:00
kuaifan
6a63ceaecc fix: 编辑器快捷键保存重复 2025-09-28 05:19:27 +08:00
kuaifan
591f9e61fb no message 2025-09-27 17:48:43 +08:00
kuaifan
7011c81bcd feat: 优化自动归档逻辑
- 子任务不自动归档
2025-09-27 16:38:44 +08:00
kuaifan
3cf7055122 feat: 添加任务关联功能 2025-09-27 15:53:58 +08:00
kuaifan
aba31eda83 no message 2025-09-27 07:09:08 +08:00
kuaifan
1b30582dd9 feat: 添加emoji表情删除按钮 2025-09-26 20:18:12 +08:00
kuaifan
0fb66358cc feat: 优化对话搜索时的选择状态管理 2025-09-26 19:29:13 +08:00
kuaifan
e226f444f7 feat: 优化部门选择逻辑
- 支持自动添加父级部门
2025-09-26 19:21:26 +08:00
kuaifan
95bf70f568 no message 2025-09-26 19:02:09 +08:00
kuaifan
a6597b44c3 feat: 优化Ai提示词 2025-09-26 19:00:10 +08:00
kuaifan
51c01c5445 feat: 添加文件缩略图显示 2025-09-26 14:00:53 +08:00
kuaifan
161bf75a1d feat: 添加文件拖拽选择功能 2025-09-26 13:32:11 +08:00
kuaifan
2f16e2c608 feat: 添加文件预览功能和优化文件打开逻辑 2025-09-26 12:13:38 +08:00
kuaifan
aea2e79b37 no message 2025-09-25 16:36:11 +08:00
kuaifan
f433d13a2f feat: 优化透明模式样式 2025-09-25 09:04:35 +08:00
kuaifan
e9abf6ed05 no message 2025-09-25 06:05:09 +08:00
kuaifan
0c32b25ddf no message 2025-09-25 00:14:14 +08:00
kuaifan
a03dec91c5 feat: 添加任务复制功能 2025-09-24 23:49:22 +08:00
kuaifan
7c5a966944 no message 2025-09-24 21:00:31 +08:00
kuaifan
652dc0953b feat: 添加任务模板排序功能
- 在 ProjectController 中新增 task__template_sort 方法,支持项目任务模板的排序
- 更新前端组件以支持拖拽调整任务模板顺序
- 新增数据库迁移以填充任务模板的排序字段
- 优化样式以提升用户体验
2025-09-24 20:49:09 +08:00
kuaifan
03860a6dce feat: 添加标签排序功能
- 在 ProjectController 中新增 tag__sort 方法,支持项目标签的排序
- 更新 ProjectTag 模型,添加排序字段
- 新增数据库迁移以添加标签排序字段
- 更新前端组件,支持拖拽调整标签顺序
- 优化样式以提升用户体验
2025-09-24 20:31:54 +08:00
kuaifan
c6bee25264 fix: 优化用户交接人选择逻辑
- 更新 UsersController 中的交接人选择逻辑,确保在选择交接人时进行有效性检查
- 修改前端 TeamManagement 组件,添加交接人选择提示信息
- 确保在提交数据时正确处理交接人 ID 的格式
2025-09-24 19:06:50 +08:00
kuaifan
068de0fa9f fix: 优化文件访问权限检查逻辑
- 移除冗余的游客访问权限检查代码
- 简化用户认证逻辑,确保在文件不允许游客访问时强制用户登录
- 更新返回数据结构,移除不再使用的 is_guest_access 字段
2025-09-24 19:00:07 +08:00
kuaifan
4b45d5ca26 feat: 添加会话重命名功能
- 在 DialogController 中新增 session__rename 方法,支持用户重命名会话
- 更新前端组件 DialogSessionHistory.vue,添加重命名按钮及相关逻辑
- 修改样式以支持重命名功能的交互效果
- 优化用户体验,确保重命名操作的流畅性
2025-09-24 18:39:25 +08:00
kuaifan
a268391e68 feat: 添加收藏备注功能
- 在 UsersController 中新增 favorite__remark 方法,支持用户修改收藏的备注
- 在 UserFavorite 模型中添加更新备注的逻辑
- 新增数据库迁移以添加备注字段
- 更新前端组件以支持备注的显示和编辑
- 优化收藏操作的用户体验
2025-09-24 18:15:03 +08:00
kuaifan
89bdd86f14 fix: 更新消息预览文本获取方法
- 将获取消息预览文本的方法从 previewTextMsg 更新为 previewMsg,以适应新的消息结构
- 确保在处理消息时使用最新的预览文本获取逻辑
2025-09-24 16:45:08 +08:00
kuaifan
e533bd7e35 no message 2025-09-24 15:48:48 +08:00
kuaifan
09ed978e80 no message 2025-09-24 11:54:01 +08:00
kuaifan
4b106e1f41 feat: 添加最近访问记录功能
- 在 UsersController 中新增获取和删除最近访问记录的接口
- 在相关控制器中记录用户最近访问的任务、文件和消息文件
- 新增 RecentManagement 组件,展示用户最近访问的记录
- 更新样式和图标以提升用户体验
2025-09-24 09:51:13 +08:00
kuaifan
feeeb26d94 no message 2025-09-23 19:39:13 +08:00
kuaifan
bef0d2d992 feat: 增强用户部门成员管理功能
- 在 UsersController 中新增逻辑,自动将缺失的部门成员加入 WebSocket 对话组
- 优化部门成员同步流程,提升用户体验
2025-09-23 18:49:02 +08:00
kuaifan
6e6bd8a6be build 2025-09-23 17:44:09 +08:00
kuaifan
631fa0db4e feat: 添加数据导出功能及相关样式
- 在管理页面中新增数据导出功能,支持导出任务、超期任务、审批数据和签到数据
- 更新应用页面,添加导出管理的弹出菜单
- 新增导出相关的 SVG 图标
- 优化样式以提升用户体验
2025-09-23 16:41:07 +08:00
kuaifan
65d30b7a30 no message 2025-09-23 15:32:01 +08:00
kuaifan
5ba5f27ca7 no message 2025-09-23 15:03:29 +08:00
kuaifan
acc437bf2d fix: 重置成功登录流程后的认证异常标志
- 在 actions.js 中添加逻辑,确保在成功登录后重置 ajaxAuthException 状态
- 优化用户认证体验,避免异常状态影响后续操作
2025-09-23 14:41:46 +08:00
kuaifan
5fd2505a33 feat: 优化 AI 生成交互体验
- 移除不必要的 loading 状态,简化用户交互
- 在项目和任务生成中添加取消功能,提升用户体验
- 更新相关组件以支持取消操作,确保生成过程的灵活性
2025-09-23 14:41:34 +08:00
kuaifan
7f6abc331b feat: 添加 AI 助手生成消息功能
- 在 DialogController 中新增 msg__ai_generate 接口,支持根据用户需求自动生成聊天消息
- 在 AI 模块中实现 generateMessage 方法,处理消息生成逻辑
- 更新前端 ChatInput 组件,添加 AI 生成按钮,集成消息生成请求
- 增强用户交互体验,支持输入消息主题和要点
2025-09-23 14:05:01 +08:00
kuaifan
c190aab8b9 feat: 添加 AI 助手生成项目功能
- 在 ProjectController 中新增 ai__generate 接口,支持根据用户需求自动生成项目名称及任务列表
- 在 AI 模块中实现 generateProject 方法,处理项目生成逻辑
- 更新前端管理页面,添加 AI 生成按钮,集成项目生成请求
- 增强样式以提升用户体验
2025-09-23 13:43:46 +08:00
kuaifan
0f71abdac3 no message 2025-09-23 13:13:52 +08:00
kuaifan
8ddc507bd5 feat: 添加 AI 助手生成任务功能
- 在 ProjectController 中新增 ai_generate 接口,支持根据用户输入生成任务标题和详细描述
- 在 AI 模块中实现 generateTask 方法,处理任务生成逻辑
- 更新前端 TaskAdd 组件,添加 AI 生成按钮,集成任务生成请求
- 优化 TEditor 和 TEditorTask 组件,支持设置内容格式
- 增强样式以提升用户体验
2025-09-23 13:11:33 +08:00
kuaifan
1c4bae2d91 no message 2025-09-23 10:16:57 +08:00
kuaifan
73ca4b1ea5 feat: 扩展收藏功能,支持消息类型的收藏
- 在 UserFavorite 模型中添加消息类型常量
- 更新 UsersController,支持消息的收藏、切换和状态检查
- 修改前端 Vue 组件以实现消息的收藏操作和状态显示
- 优化收藏管理界面,支持消息类型的展示与处理
2025-09-23 09:48:06 +08:00
kuaifan
18a922b5cd feat: 重构收藏功能,优化状态检查与切换逻辑
- 将文件、项目和任务的收藏状态切换逻辑统一为 toggleFavorite 方法
- 添加 checkFavoriteStatus 方法以简化收藏状态检查
- 更新相关 Vue 组件以使用新的状态管理方法,提升代码可读性和维护性
- 优化上下文菜单和操作逻辑,确保收藏状态的实时更新
2025-09-23 08:59:15 +08:00
kuaifan
11b98978c1 feat: 增强文件和项目的收藏功能
- 在 UserFavorite 模型中添加文件的 pid 字段以支持层级结构
- 更新前端 Vue 组件以实现文件和项目的收藏状态切换
- 添加检查文件和项目收藏状态的功能
- 优化上下文菜单以支持收藏操作
2025-09-22 16:35:57 +08:00
kuaifan
379d3811a8 feat: 添加用户收藏功能
- 在 UsersController 中新增获取、切换、清理用户收藏的 API 接口
- 创建 UserFavorite 模型以管理用户的收藏记录
- 更新前端 Vue 组件以支持收藏管理界面和交互
- 添加相关样式以美化收藏管理界面
2025-09-22 16:09:33 +08:00
kuaifan
0401b8a6e6 feat: 添加任务浏览历史功能
- 在 UsersController 中新增获取、记录和清理任务浏览历史的 API 接口
- 创建 UserTaskBrowse 模型以管理用户的任务浏览记录
- 更新前端 Vue 组件以支持任务浏览历史的加载和显示
- 移除不再使用的本地缓存逻辑,直接通过 API 进行数据交互
2025-09-22 07:10:12 +08:00
kuaifan
6148b996d8 no message 2025-09-22 06:27:57 +08:00
kuaifan
39781c9cd7 feat: 优化消息传递处理逻辑
- 在 DialogWrapper 组件中添加 handlerMsgTransfer 方法以简化消息传递逻辑
- 更新 TaskDetail 组件以直接使用状态管理中的 dialogMsgTransfer 数据
2025-09-22 06:01:56 +08:00
kuaifan
18758a1614 no message 2025-09-22 06:01:36 +08:00
kuaifan
b044d8d90e feat: 添加部门成员同步功能
- 在 UsersController 中新增同步部门成员的 API 接口
- 在 UserDepartment 模型中添加递归获取子部门 ID 的方法
- 在前端 TeamManagement 组件中添加同步部门成员的操作选项
2025-09-22 05:07:45 +08:00
kuaifan
02e56f87bc perf: 优化群聊消息AI处理逻辑
- 添加获取最近聊天记录功能
2025-09-21 20:00:23 +08:00
kuaifan
d9b9ee221b no message 2025-09-21 15:43:00 +08:00
kuaifan
21ec9188ca fix: 添加异常处理以确保提及格式转换的稳定性 2025-09-20 17:04:38 +08:00
kuaifan
4d768becf5 fix: 更新应用商店镜像版本至0.2.9 2025-09-20 17:04:29 +08:00
kuaifan
a27049386b no message 2025-09-20 15:11:42 +08:00
kuaifan
b23e3d7359 feat: 添加下载功能的等待状态支持 2025-09-20 14:04:44 +08:00
kuaifan
7660164583 fix: 修复在列表中未找到当前图像时的处理逻辑 2025-09-20 07:30:45 +08:00
kuaifan
5e1f3c5564 feat: 添加文件游客访问权限功能 2025-09-19 19:10:58 +08:00
kuaifan
197fa9c01c build 2025-09-02 07:27:42 +08:00
kuaifan
554e3d0c2f no message 2025-08-27 17:10:17 +08:00
kuaifan
b800cde34d no message 2025-08-26 20:21:19 +08:00
kuaifan
775fdd2be0 fix: 无法修改群组名称的问题 2025-08-26 20:19:41 +08:00
kuaifan
7908ae4258 no message 2025-08-20 18:25:51 +08:00
kuaifan
bfbd8229a1 no message 2025-08-20 16:53:29 +08:00
kuaifan
afbf8dedbf no message 2025-08-20 16:21:26 +08:00
kuaifan
569912abef no message 2025-08-20 13:39:58 +08:00
kuaifan
7c94f6bc9a perf: 支持项目调整排序 2025-08-20 08:36:19 +08:00
kuaifan
b825b5b063 perf: 支持项目调整排序 2025-08-19 23:12:10 +08:00
kuaifan
50098b5e70 no message 2025-08-19 22:19:46 +08:00
kuaifan
e237b4db1c perf: 支持项目调整排序 2025-08-19 22:18:18 +08:00
kuaifan
2a25cf3bbd no message 2025-08-19 21:43:17 +08:00
kuaifan
02275bb417 perf: 支持项目调整排序 2025-08-19 21:19:45 +08:00
kuaifan
788cae3efe no message 2025-08-19 20:06:46 +08:00
kuaifan
0dec70c53a no message 2025-08-19 20:06:38 +08:00
kuaifan
f534f012d2 perf: 优化错误页 2025-08-19 18:01:21 +08:00
kuaifan
bb83875c99 feat: 添加内置浏览器导航功能 2025-08-19 18:01:21 +08:00
kuaifan
d048aa33f7 no message 2025-08-19 18:01:21 +08:00
kuaifan
8f3e250073 perf: 优化输入框工具栏 2025-08-19 18:01:21 +08:00
kuaifan
63a792d169 no message 2025-08-19 18:01:21 +08:00
kuaifan
eb3524a22d
Merge pull request #280 from nightcp/fix-ganntt-timeline-error
fix(gantt): 修复甘特图时间轴计算错误
2025-08-19 18:00:43 +08:00
nightcp
f657a24a1a fix(gantt): 修复甘特图时间轴计算错误
Closes #272
2025-08-18 21:05:26 +08:00
kuaifan
a5228448d7
Merge pull request #278 from puzzle9/pro
fix: 修复 supervisor crontab 运行状态错误
2025-08-18 17:07:50 +08:00
kuaifan
1ec4796f72 feat: 添加查看共同的群 2025-08-18 09:45:39 +08:00
kuaifan
6964158cf6 perf: 优化任务模板、任务标签 2025-08-18 08:31:22 +08:00
kuaifan
4fc4dd1b16 no message 2025-08-18 05:28:37 +08:00
kuaifan
3e851f0c3c build 2025-08-15 07:54:34 +08:00
kuaifan
b8befaa973 no message 2025-08-15 07:43:45 +08:00
kuaifan
b05046af29 perf: 优化下载工具 2025-08-15 07:22:20 +08:00
kuaifan
eecc6c9e53 perf: 优化下载工具 2025-08-15 01:02:40 +08:00
kuaifan
d4e754d601 perf: 优化下载工具 2025-08-15 00:27:34 +08:00
kuaifan
a8a54593e2 perf: 优化下载工具 2025-08-14 23:11:37 +08:00
kuaifan
5bbffc4f5c perf: 优化下载工具 2025-08-14 20:31:55 +08:00
kuaifan
0833018399 perf: 优化下载工具 2025-08-14 16:50:48 +08:00
kuaifan
f6850fc795 perf: 优化下载工具 2025-08-14 16:50:42 +08:00
kuaifan
c0b4674568 no message 2025-08-14 11:56:51 +08:00
puzzle
5a8996d90a fix: 修复 supervisor crontab 运行状态错误 2025-08-14 03:08:06 +08:00
kuaifan
548b30e5b3 build 2025-08-12 08:05:11 +08:00
kuaifan
80f9329004 no message 2025-08-12 07:38:40 +08:00
kuaifan
f672280236 no message 2025-08-12 07:37:52 +08:00
kuaifan
90a4a01de7 no message 2025-08-11 22:35:58 +08:00
kuaifan
09cebb90fe fix: 修复应用加载中无法点击胶囊 2025-08-11 20:23:55 +08:00
kuaifan
70389aab3d no message 2025-08-11 16:47:53 +08:00
kuaifan
d9132a722f build 2025-08-11 07:03:29 +08:00
kuaifan
ea7a4e46e0 no message 2025-08-11 06:59:38 +08:00
kuaifan
07b91058af perf: 优化粘贴提及消息 2025-08-11 06:55:55 +08:00
kuaifan
c27ace6a6a perf: 优化消息类型的判断 2025-08-11 05:52:09 +08:00
kuaifan
1c0a5b17ca no message 2025-08-11 05:15:49 +08:00
kuaifan
9b12a829d2 perf: 签到记录窗口添加打开签到机器人 2025-08-11 05:15:35 +08:00
kuaifan
0f41172468 perf: 文件名长度限制最长为100字 2025-08-10 21:54:31 +08:00
kuaifan
8597705a77 fix: 无法打包文件加载的情况 2025-08-10 21:00:35 +08:00
kuaifan
3f733ce857 perf: 允许打包下载一个文件夹 2025-08-10 20:14:53 +08:00
kuaifan
40f8ec77b8 no message 2025-08-10 19:48:03 +08:00
kuaifan
0af967d6c9 perf: 优化桌面端出现打开久之后访问错误的情况 2025-08-09 23:38:36 +08:00
kuaifan
f6d43c9f39 perf: 优化桌面端出现打开久之后访问错误的情况 2025-08-09 13:49:53 +08:00
kuaifan
70b0538dd5 no message 2025-08-09 10:03:45 +08:00
kuaifan
439262b930 no message 2025-08-09 09:11:16 +08:00
kuaifan
968b2587ae feat: 添加 setCapsuleConfig 方法以更新胶囊配置 2025-08-09 00:18:57 +08:00
kuaifan
15f471a032 no message 2025-08-08 22:00:54 +08:00
kuaifan
5175157ba6 no message 2025-08-08 18:23:30 +08:00
kuaifan
e51e8f7196 perf: 优化抽屉样式 2025-08-08 17:20:34 +08:00
kuaifan
00b34fda42 no message 2025-08-08 12:45:07 +08:00
kuaifan
b34fabab54 no message 2025-08-08 12:21:09 +08:00
kuaifan
487c7e2824 perf: 更新应用胶囊配置和优化微应用加载 2025-08-08 11:48:22 +08:00
kuaifan
46c79a8772 perf: 更新应用胶囊配置和优化微应用加载 2025-08-08 11:48:17 +08:00
kuaifan
bfb4144e57 perf: 优化 css 语法 2025-08-08 07:58:49 +08:00
kuaifan
dc1bb72070 perf: 优化抽屉窗口 2025-08-07 14:53:09 +08:00
kuaifan
8e084d2362 perf: 优化抽屉窗口 2025-08-07 14:27:00 +08:00
kuaifan
d5a75f887d perf: 优化抽屉窗口 2025-08-07 14:22:00 +08:00
kuaifan
710609e98b perf: 优化微应用 2025-08-06 22:27:32 +08:00
kuaifan
b73ab76bfb perf: 优化微应用 2025-08-06 16:51:21 +08:00
kuaifan
27b64df870 no message 2025-08-06 11:04:22 +08:00
kuaifan
eabb897f96 no message 2025-08-05 19:12:28 +08:00
kuaifan
68c5e47bad feat: 添加应用移动端胶囊布局 2025-08-05 18:38:54 +08:00
kuaifan
2ae5af7019 no message 2025-08-05 11:12:56 +08:00
kuaifan
860d1ca9b3 perf: 优化微应用关闭窗口逻辑 2025-08-05 10:13:00 +08:00
kuaifan
66a9d1f25e no message 2025-08-05 07:57:38 +08:00
kuaifan
bbfeedcdb3 perf: 优化消息重复 2025-08-05 07:09:13 +08:00
kuaifan
079e273edb fix: 修复@弹窗无法滚动 2025-08-05 06:48:20 +08:00
kuaifan
393aab4c4b no message 2025-08-04 22:00:55 +08:00
kuaifan
4f2bf7549c no message 2025-08-04 06:02:22 +08:00
kuaifan
acdf23571c no message 2025-08-01 13:03:03 +08:00
kuaifan
62ec634db3 build 2025-08-01 12:51:36 +08:00
kuaifan
c53e978106 no message 2025-08-01 12:47:49 +08:00
kuaifan
a7fa757d0d fix: 表格消息文字颜色冲突 2025-08-01 12:46:10 +08:00
kuaifan
5fb1bd4175 feat: 添加待办完成状态的支持 2025-08-01 12:33:00 +08:00
kuaifan
e792ab7b4d feat: 工作流支持自定义颜色 2025-08-01 11:27:00 +08:00
kuaifan
02544d29fd no message 2025-08-01 08:23:35 +08:00
kuaifan
20acbd0331 no message 2025-07-31 16:15:18 +08:00
kuaifan
115b4aacb8 fix: 修复无法导出的问题 2025-07-31 15:27:17 +08:00
kuaifan
8746caab06 feat: 重构基础模块 2025-07-31 14:26:06 +08:00
kuaifan
625648c908 feat: 更新请求上下文处理 2025-07-31 11:06:23 +08:00
kuaifan
734b5f9534 build 2025-07-31 07:35:12 +08:00
kuaifan
a0579318bd no message 2025-07-30 21:55:49 +08:00
kuaifan
a437e3cbd3 no message 2025-07-30 21:25:04 +08:00
kuaifan
1b242dc04e perf: 优化错误提示 2025-07-30 20:33:27 +08:00
kuaifan
a1a51914a2 feat: 优化请求上下文处理 2025-07-30 18:57:35 +08:00
kuaifan
f6cab9b5a9 no message 2025-07-30 18:57:35 +08:00
kuaifan
a3649c04e2 perf: 优化应用菜单 2025-07-30 18:57:35 +08:00
kuaifan
a562bfdb08 no message 2025-07-29 21:57:24 +08:00
kuaifan
8ffe64ad8e no message 2025-07-29 17:23:08 +08:00
kuaifan
a116d06d61 no message 2025-07-29 17:15:36 +08:00
kuaifan
c26f73a5a8 no message 2025-07-29 16:58:33 +08:00
kuaifan
f5847a57c1 fix: 修复无法删除webhook的问题 2025-07-29 16:30:30 +08:00
kuaifan
fe9d23a0ff no message 2025-07-29 16:22:37 +08:00
kuaifan
cdc27004bf perf: 优化机器人消息接收处理任务 2025-07-29 15:13:24 +08:00
kuaifan
b914164a77 no message 2025-07-28 10:26:00 +08:00
kuaifan
35e58f90bc perf: 签到新增高德和腾讯地图 2025-07-28 08:46:54 +08:00
kuaifan
16d360c582 perf: 签到新增高德和腾讯地图 2025-07-28 06:22:28 +08:00
kuaifan
4c075b4d11 perf: 签到新增高德和腾讯地图 2025-07-28 06:22:20 +08:00
kuaifan
8c9c1c5afa no message 2025-07-28 05:39:50 +08:00
kuaifan
d093163cd4 perf: 优化国际化 2025-07-26 15:18:00 +08:00
kuaifan
9bd6fcefd3 perf: 优化 AI 设置 2025-07-26 15:14:15 +08:00
kuaifan
5139947643 perf: 优化 AI 设置 2025-07-26 14:24:58 +08:00
kuaifan
01ff10385a perf: 优化 AI 设置 2025-07-26 12:01:37 +08:00
kuaifan
9969c3a7ac perf: 优化应用弹窗
- 优化应用弹窗工具栏
- 优化应用弹窗全屏
2025-07-26 10:45:31 +08:00
kuaifan
f7ed2ec3e3 no message 2025-07-25 23:33:48 +08:00
kuaifan
fedeeb3076 perf: 优化会员选择器 2025-07-25 19:31:24 +08:00
kuaifan
8157c27529 perf: 优化会员搜索接口 2025-07-25 16:11:10 +08:00
kuaifan
0eba0c6a4b perf: 优化提及窗口 2025-07-25 15:52:04 +08:00
kuaifan
13fb9db52b perf: 优化国际化 2025-07-25 14:20:35 +08:00
kuaifan
f6818ba880 perf: 优化机器人消息 2025-07-25 14:06:07 +08:00
kuaifan
dbf3b3cc79 no message 2025-07-25 14:01:41 +08:00
kuaifan
24534069da perf: 优化机器人消息 2025-07-25 14:01:34 +08:00
kuaifan
4cec0a7350 perf: 机器人支持新会话 2025-07-25 11:38:51 +08:00
kuaifan
0b86fa7bee perf: 机器人支持新会话 2025-07-25 11:25:02 +08:00
kuaifan
b406e22695 no message 2025-07-23 19:11:58 +08:00
kuaifan
3fca783dd8 perf: 优化应用方法 2025-07-23 12:04:17 +08:00
kuaifan
6de4865052 fix: 用户头像加载失败的情况 2025-07-22 19:59:05 +08:00
kuaifan
facc2fab24 no message 2025-07-20 20:18:56 +08:00
kuaifan
ddc0931e90 perf: 机器人 webhook 添加用户信息 2025-07-20 20:18:51 +08:00
kuaifan
d5d32038f5 perf: 优化应用 2025-07-19 10:38:59 +08:00
kuaifan
a20edd9bec no message 2025-07-18 16:41:22 +08:00
kuaifan
3da90337ef build 2025-07-18 13:59:48 +08:00
kuaifan
9633f7644e fix: 修复客户度右键复制图片失败的情况 2025-07-18 13:20:08 +08:00
kuaifan
a19cf0e1c3 fix: 修复部分emoji表情无法提交的情况 2025-07-18 13:06:01 +08:00
kuaifan
1a841c4b5d perf: 优化预览消息 2025-07-18 13:05:27 +08:00
kuaifan
937e7ba154 perf: 优化应用参数 2025-07-18 11:13:17 +08:00
kuaifan
4cc0c85a6c perf: 优化应用参数 2025-07-18 09:43:48 +08:00
kuaifan
943941e0f6 perf: 优化应用参数 2025-07-18 08:25:47 +08:00
kuaifan
b160021e67 build 2025-07-17 16:07:08 +08:00
kuaifan
1bcc035979 no message 2025-07-17 16:03:18 +08:00
kuaifan
ef67dc144f fix: 修复机器人发送消息接口 2025-07-16 23:14:45 +08:00
kuaifan
cc96fcd6a0 fix: 修复应用无法在窗口独立显示 2025-07-16 22:38:02 +08:00
kuaifan
d1f00b2d48 fix: 修复机器人发送消息接口 2025-07-16 22:36:49 +08:00
kuaifan
2fe28d2335 build 2025-07-16 13:42:37 +08:00
kuaifan
f9276f4d83 perf: 优化应用 2025-07-16 08:08:22 +08:00
kuaifan
099004a080 no message 2025-07-16 07:18:50 +08:00
kuaifan
cc1df8d7d0 perf: 优化应用 2025-07-15 19:58:08 +08:00
kuaifan
686a2e4fff perf: 优化创建新会话数据 2025-07-15 19:10:00 +08:00
kuaifan
e98fe3eec5 fix: 转发消息同时留言时ai会回复两条的情况 2025-07-15 19:03:46 +08:00
kuaifan
fc34ff38d3 perf: 优化应用 2025-07-15 17:52:06 +08:00
kuaifan
b6b44b3782 no message 2025-07-15 17:33:56 +08:00
kuaifan
c906636776 perf: 优化应用 2025-07-15 17:29:09 +08:00
kuaifan
db282d1a04 perf: 新增使用系统机器人发送消息 2025-07-15 17:26:26 +08:00
kuaifan
898656963d perf: 优化应用中心 2025-07-15 14:26:42 +08:00
kuaifan
6426e0238a fix: 修复应用 {system_theme} 参数无效的问题 2025-07-14 22:57:23 +08:00
kuaifan
08e8faf3ff fix: 修复应用 selectUsers 方法的问题 2025-07-14 22:57:22 +08:00
kuaifan
d21adf6004 perf: 获取我的部门列表接口 2025-07-14 22:29:50 +08:00
kuaifan
c3ac7dd1ab fix: 修复应用地址转换不正确的问题 2025-07-10 20:21:52 +08:00
kuaifan
f1cfba3ad8 build 2025-07-09 22:45:46 +08:00
kuaifan
1ceed3461c perf: 优化应用商城 2025-07-09 22:43:50 +08:00
kuaifan
d25c26156d no message 2025-07-08 20:16:35 +08:00
kuaifan
d75c22114c perf: 优化一些样式 2025-07-08 18:46:54 +08:00
kuaifan
05a754f446 perf: 优化一些样式 2025-07-08 17:18:45 +08:00
kuaifan
b5e6eff65d perf: 优化桌面端服务 2025-07-08 16:34:40 +08:00
kuaifan
eaa8ae66db no message 2025-07-08 11:08:13 +08:00
kuaifan
5e4a08538b perf: 优化标签选择 2025-07-08 10:57:09 +08:00
kuaifan
b01a54437a perf: 优化标签操作日志 2025-07-08 10:41:41 +08:00
kuaifan
5f0fc78f30 perf: 优化标签操作日志 2025-07-08 07:56:15 +08:00
kuaifan
325dc5e2fe fix: 修复修改删除标签未同步任务标签的问题 2025-07-08 07:50:15 +08:00
kuaifan
a15b29122e perf: 支持管理自己创建的标签 2025-07-08 06:50:43 +08:00
kuaifan
074ccc8aab perf: 调整项目最多支持添加50个模板、100个标签 2025-07-08 06:04:56 +08:00
kuaifan
3809046fbc fix: 修复部分屏幕无法完全显示项目管理员菜单 2025-07-08 06:04:10 +08:00
kuaifan
83ceb3264f perf: 优化翻译 2025-07-07 21:42:17 +08:00
kuaifan
9055858d55 perf: 优化邀请加入项目 2025-07-07 21:33:10 +08:00
kuaifan
2a465b5f1d perf: 优化项目邀请链接 2025-07-07 20:46:42 +08:00
kuaifan
b4101f856a fix: 修复项目成员无法认领任务的情况 2025-07-07 20:34:24 +08:00
kuaifan
44baa743c0 perf: 优化聊天发送会员、任务、文件支持搜索ID 2025-07-07 18:10:37 +08:00
kuaifan
46dd449b2f perf: 优化发送消息结果 2025-07-07 17:42:44 +08:00
kuaifan
f21d45e697 perf: 优化通知内容 2025-07-07 14:52:24 +08:00
kuaifan
1e0a19ea7a no message 2025-07-06 11:34:22 +08:00
kuaifan
dcfa47291e perf: 优化群消息推送内容 2025-07-06 11:34:16 +08:00
kuaifan
bd32c9555e build 2025-07-05 13:15:19 +08:00
kuaifan
f8f612544e no message 2025-07-05 10:13:36 +08:00
kuaifan
1c0271f55e no message 2025-07-04 23:17:59 +08:00
kuaifan
a10bc74de1 perf: 优化应用商城
- 支持 background 参数
- iframe 模式添加安全距离
- iframe 支持 dootask/tools
2025-07-04 19:21:31 +08:00
kuaifan
958ef80602 build 2025-06-29 22:12:03 +08:00
kuaifan
124b63f325 perf: 优化客户端缓存 2025-06-29 21:58:43 +08:00
kuaifan
40f5ba5004 fix: 修复客户端无法打开部分应用的问题 2025-06-29 21:57:46 +08:00
kuaifan
32f30826b9 perf: 优化已知问题 2025-06-18 20:25:56 +08:00
kuaifan
b4aa8b37ea perf: 优化iPadOS兼容性 2025-06-18 15:35:37 +08:00
kuaifan
8368bbec47 perf: 优化iPadOS兼容性 2025-06-18 15:35:37 +08:00
kuaifan
618e482507 perf: 优化iPadOS兼容性 2025-06-18 15:35:30 +08:00
kuaifan
43711a1a59 perf: 优化iPadOS兼容性 2025-06-18 15:35:23 +08:00
kuaifan
bbe071545d perf: 优化设备登录 2025-06-17 00:17:04 +08:00
kuaifan
4710479b46 add tmp log 2025-06-16 21:08:42 +08:00
kuaifan
c28a375b5d build 2025-06-05 07:28:49 +08:00
kuaifan
750d3429e0 feat: 微应用支持iframe模式 2025-06-05 07:15:34 +08:00
kuaifan
4c34fe9b85 fix: 修复应用商店参数失效问题 2025-06-04 16:27:40 +08:00
kuaifan
34c56980d4 no message 2025-06-04 15:29:04 +08:00
kuaifan
a5b8609df1 perf: 优化导出签到功能 2025-06-04 15:14:31 +08:00
kuaifan
1fd7f0314a perf: 优化导出审批功能 2025-06-04 15:08:31 +08:00
kuaifan
25e82d690e perf: 优化导出任务功能 2025-06-04 14:58:54 +08:00
kuaifan
31879cb60b build 2025-06-04 07:22:43 +08:00
kuaifan
489e5b551c perf: 优化本地资源加载方式 2025-06-04 07:02:19 +08:00
kuaifan
e29bd01f68 no message 2025-06-03 21:38:53 +08:00
kuaifan
e5d9140aa0 perf: 优化微应用参数变量的支持 2025-06-03 21:38:22 +08:00
kuaifan
09f5cca948 fix: 修复已经在消息中打开项目对话时无法在其他地方打开项目沟通 2025-06-03 20:10:22 +08:00
kuaifan
405e09fdf4 fix: 修复搜索标签后搜索框消失的情况 2025-06-03 14:48:04 +08:00
kuaifan
43624e9b7b fix: 修复部分标签背景色不显示的情况 2025-06-03 14:47:42 +08:00
kuaifan
29ed0ad05a build 2025-06-02 19:34:59 +08:00
kuaifan
0aafe79c65 no message 2025-06-02 18:36:33 +08:00
kuaifan
802afd592c fix: 修复客户端无法打开工作报告 2025-06-02 18:25:19 +08:00
kuaifan
ac1644e32d fix: 修复部分机子无法打开OKR的情况 2025-06-02 18:14:12 +08:00
kuaifan
5a1f130bec fix: 修复客户端无法打开工作报告 2025-06-02 18:13:51 +08:00
kuaifan
678868153a fix: 修复客户端无法打开工作报告 2025-06-02 10:06:22 +08:00
kuaifan
991f050dbb build 2025-05-31 22:53:46 +08:00
kuaifan
be04355685 no message 2025-05-31 10:30:47 +08:00
kuaifan
762b6b3f3b no message 2025-05-30 19:11:19 +08:00
kuaifan
71d9cbdce6 no message 2025-05-30 15:10:13 +08:00
kuaifan
d995ef19b5 no message 2025-05-30 09:25:28 +08:00
Pang
bf80e4b02b feat: 桌面端使用web服务启动 2025-05-30 07:33:14 +08:00
Pang
64a047cd7c feat: 桌面端使用web服务启动 2025-05-30 07:13:19 +08:00
kuaifan
566421003c build 2025-05-29 08:52:24 +08:00
kuaifan
198da7608d no message 2025-05-29 08:49:37 +08:00
kuaifan
0a7edb219e no message 2025-05-29 02:02:21 +08:00
kuaifan
65c20f2211 fix: 修复移动端审批列表无法滚动到底部的情况 2025-05-29 01:51:24 +08:00
kuaifan
144767152a no message 2025-05-29 01:44:06 +08:00
kuaifan
82be52be52 no message 2025-05-29 01:35:28 +08:00
kuaifan
46c8caa627 no message 2025-05-29 01:35:28 +08:00
kuaifan
0027e838a0 no message 2025-05-29 01:35:28 +08:00
kuaifan
7e846e2a58 no message 2025-05-29 01:35:28 +08:00
kuaifan
de7f55bb97 no message 2025-05-29 01:35:28 +08:00
kuaifan
2ac9a59469 no message 2025-05-29 01:35:28 +08:00
kuaifan
bdb4014b94 no message 2025-05-29 01:35:28 +08:00
weifashi
9509bd1510 fix: 修复重复周期 子任务没有复制过去 2025-05-28 22:11:56 +08:00
kuaifan
3f11770baa no message 2025-05-28 15:23:59 +08:00
kuaifan
259a040b7e no message 2025-05-28 11:42:42 +08:00
kuaifan
ccb14630f7 no message 2025-05-28 10:13:05 +08:00
kuaifan
4aa865a60f no message 2025-05-28 07:23:20 +08:00
kuaifan
c7ea7b057c build 2025-05-28 07:05:27 +08:00
kuaifan
91dbbd46c0 no message 2025-05-27 16:34:02 +08:00
kuaifan
e5146077eb no message 2025-05-27 09:18:34 +08:00
kuaifan
76918bf973 no message 2025-05-27 07:32:48 +08:00
kuaifan
b7d3e69f87 no message 2025-05-26 23:44:16 +08:00
kuaifan
d7bccfd267 no message 2025-05-26 23:15:26 +08:00
kuaifan
2a25917e41 no message 2025-05-26 23:04:00 +08:00
kuaifan
d73239b274 build 2025-05-26 22:45:59 +08:00
kuaifan
9f6fffbe6b feat: 新增应用商店 2025-05-26 21:19:51 +08:00
kuaifan
72f7ff3df5 no message 2025-05-26 21:02:06 +08:00
kuaifan
2af1dba8dc no message 2025-05-26 20:54:06 +08:00
kuaifan
ca353d747b no message 2025-05-26 20:26:40 +08:00
kuaifan
1589d4df1c no message 2025-05-26 20:12:51 +08:00
kuaifan
b3abe8af9c no message 2025-05-26 17:45:16 +08:00
kuaifan
c178e36f9b no message 2025-05-26 15:55:42 +08:00
kuaifan
d93092de99 no message 2025-05-26 15:45:37 +08:00
kuaifan
790b05880a no message 2025-05-25 18:26:32 +08:00
kuaifan
f34766ade0 no message 2025-05-25 08:46:25 +08:00
kuaifan
0e1d5e802c no message 2025-05-25 08:09:57 +08:00
kuaifan
db526dfcc8 no message 2025-05-25 00:09:14 +08:00
kuaifan
c18db60e80 no message 2025-05-24 23:48:21 +08:00
kuaifan
b579a6ade2 no message 2025-05-24 21:04:13 +08:00
kuaifan
9d1d642734 no message 2025-05-24 20:28:36 +08:00
kuaifan
261c051052 no message 2025-05-24 19:40:36 +08:00
kuaifan
e499e2d0dc no message 2025-05-24 19:21:26 +08:00
kuaifan
b860b6f389 no message 2025-05-24 19:09:40 +08:00
kuaifan
05d5d5a967 no message 2025-05-24 19:06:14 +08:00
kuaifan
74ba1cc723 no message 2025-05-24 18:26:48 +08:00
kuaifan
f2042efdc2 no message 2025-05-24 18:10:34 +08:00
kuaifan
6b7e7fa1e4 no message 2025-05-24 17:32:40 +08:00
kuaifan
6677e6e74f no message 2025-05-24 16:53:48 +08:00
kuaifan
c3994ddbea no message 2025-05-24 16:05:25 +08:00
kuaifan
a981ff2f6c no message 2025-05-24 15:57:31 +08:00
kuaifan
3e0ba398d4 no message 2025-05-24 15:42:37 +08:00
kuaifan
aa4f7c8536 no message 2025-05-24 14:39:54 +08:00
kuaifan
959f9454d8 no message 2025-05-24 13:30:01 +08:00
kuaifan
6b72a309f5 no message 2025-05-24 07:49:15 +08:00
kuaifan
c388fe373d no message 2025-05-24 07:37:08 +08:00
kuaifan
270ddc6487 feat: 检查应用是否已安装 2025-05-23 13:39:57 +08:00
kuaifan
5ccaa8f106 perf: 更新AI默认模型列表 2025-05-23 13:03:52 +08:00
王昱
1d92c2668d feat: 检查应用是否已安装 2025-05-23 12:40:33 +08:00
kuaifan
6e03a05e6d no message 2025-05-23 11:30:31 +08:00
kuaifan
2905059947 no message 2025-05-23 11:17:05 +08:00
kuaifan
1df927f771 no message 2025-05-22 23:01:07 +08:00
kuaifan
bd8b6d0319 no message 2025-05-21 16:23:11 +08:00
kuaifan
fef39b2720 no message 2025-05-20 20:07:16 +08:00
kuaifan
7de433c5fc no message 2025-05-20 17:53:49 +08:00
kuaifan
0a711f2656 no message 2025-05-18 17:06:11 +08:00
kuaifan
04b6b8aa8a no message 2025-05-18 13:25:13 +08:00
kuaifan
58f286efe4 no message 2025-05-18 11:30:42 +08:00
kuaifan
a01b292eb3 no message 2025-05-18 11:00:06 +08:00
kuaifan
18c608ad7e no message 2025-05-18 00:20:23 +08:00
kuaifan
8d144f4e12 no message 2025-05-17 23:54:24 +08:00
kuaifan
7f895bfbec no message 2025-05-17 23:25:09 +08:00
kuaifan
b0c356fa9b no message 2025-05-17 23:12:47 +08:00
kuaifan
79defdc3f3 no message 2025-05-17 22:46:10 +08:00
kuaifan
b2d9568deb no message 2025-05-17 20:56:54 +08:00
kuaifan
a130c049bf no message 2025-05-17 13:53:07 +08:00
kuaifan
8904039515 no message 2025-05-17 09:52:52 +08:00
kuaifan
7d28181b16 no message 2025-05-17 09:09:59 +08:00
kuaifan
98e4c81b9b no message 2025-05-17 07:09:47 +08:00
kuaifan
10f5af5f09 no message 2025-05-17 02:03:49 +08:00
kuaifan
18ffad5de5 no message 2025-05-16 20:24:43 +08:00
kuaifan
428db42140 no message 2025-05-16 08:23:58 +08:00
kuaifan
6e5426764e no message 2025-05-15 23:16:10 +08:00
kuaifan
f3cfcc650c no message 2025-05-15 23:04:09 +08:00
kuaifan
fc88573c9d no message 2025-05-15 22:53:48 +08:00
kuaifan
5cf1c3d14f no message 2025-05-15 22:44:41 +08:00
kuaifan
79f15cc34d no message 2025-05-15 21:37:18 +08:00
kuaifan
775cab1080 no message 2025-05-15 21:26:14 +08:00
kuaifan
3e20e7d0ce no message 2025-05-15 21:02:42 +08:00
kuaifan
54407e0a60 no message 2025-05-15 20:00:17 +08:00
kuaifan
ef696391d8 no message 2025-05-15 19:42:16 +08:00
kuaifan
0c34df290e no message 2025-05-15 17:43:04 +08:00
kuaifan
04d31bd814 no message 2025-05-15 16:56:08 +08:00
kuaifan
9888d9f59e no message 2025-05-15 16:44:58 +08:00
kuaifan
3bb1bf0967 no message 2025-05-15 16:01:45 +08:00
kuaifan
dfbcb1f45c no message 2025-05-15 15:54:58 +08:00
kuaifan
12ecf4de40 no message 2025-05-15 15:39:03 +08:00
kuaifan
7be1171004 no message 2025-05-15 15:32:52 +08:00
kuaifan
2bb646d150 no message 2025-05-15 15:03:49 +08:00
kuaifan
e7749b2dff no message 2025-05-15 09:22:18 +08:00
kuaifan
434d8eabc8 no message 2025-05-15 08:09:56 +08:00
kuaifan
0a14219112 no message 2025-05-15 00:39:25 +08:00
kuaifan
5b811df8ee no message 2025-05-15 00:20:41 +08:00
kuaifan
bc264109f3 no message 2025-05-14 23:55:00 +08:00
kuaifan
9c29c1ca9b no message 2025-05-13 12:55:08 +08:00
kuaifan
fe4f62ff8d no message 2025-05-13 09:51:21 +08:00
kuaifan
35dfb9d1ff no message 2025-05-13 01:25:24 +08:00
kuaifan
3809aca09d no message 2025-05-12 14:15:42 +08:00
kuaifan
bcd5bb5009 no message 2025-05-12 14:08:53 +08:00
kuaifan
0d7cc6a386 no message 2025-05-12 13:47:05 +08:00
kuaifan
12265699b3 no message 2025-05-12 13:32:20 +08:00
kuaifan
783c0356c7 no message 2025-05-12 13:13:23 +08:00
kuaifan
7ef31bc0b5 no message 2025-05-12 12:14:30 +08:00
kuaifan
467f2368dd no message 2025-05-12 09:49:31 +08:00
kuaifan
2cfcb081a2 no message 2025-05-12 08:33:51 +08:00
kuaifan
1829ac851d no message 2025-05-11 10:07:38 +08:00
kuaifan
2e715004ae no message 2025-05-11 00:39:15 +08:00
kuaifan
45ee092593 no message 2025-05-10 21:31:48 +08:00
kuaifan
20e543f721 no message 2025-05-10 12:32:07 +08:00
kuaifan
b2148eb656 no message 2025-05-09 18:13:45 +08:00
kuaifan
efab4fb41b no message 2025-05-09 10:43:36 +08:00
kuaifan
9ac3fd3615 no message 2025-05-09 10:16:59 +08:00
kuaifan
14bc7a0f76 no message 2025-05-09 09:53:46 +08:00
kuaifan
26727fea17 no message 2025-05-09 07:33:24 +08:00
kuaifan
34f8d4c2a6 no message 2025-05-09 00:54:10 +08:00
kuaifan
02708807bd no message 2025-05-09 00:39:21 +08:00
kuaifan
176e5de531 no message 2025-05-08 23:38:02 +08:00
kuaifan
b9180a4426 no message 2025-05-08 23:02:31 +08:00
kuaifan
959b30c788 no message 2025-05-08 20:36:26 +08:00
kuaifan
df79ef59ea no message 2025-05-08 20:05:46 +08:00
kuaifan
4dc1f01cf0 no message 2025-05-08 20:01:16 +08:00
kuaifan
cb17110562 no message 2025-05-08 19:46:10 +08:00
kuaifan
4424e4f9be no message 2025-05-08 16:46:39 +08:00
kuaifan
fb641ac960 no message 2025-05-08 15:53:30 +08:00
kuaifan
a5faa378b0 no message 2025-05-08 13:07:59 +08:00
kuaifan
66ea277a59 no message 2025-05-08 10:34:47 +08:00
kuaifan
006bc6ceda no message 2025-05-08 07:11:58 +08:00
kuaifan
aef3e869dc no message 2025-05-07 20:28:52 +08:00
kuaifan
9c46d28871 no message 2025-05-07 17:53:21 +08:00
kuaifan
1fc141050f no message 2025-05-07 13:53:12 +08:00
kuaifan
1e45d199e2 no message 2025-05-07 13:32:50 +08:00
kuaifan
3018f3653c no message 2025-05-07 12:21:09 +08:00
kuaifan
1c5b856800 no message 2025-05-07 09:41:25 +08:00
kuaifan
f53a5ea6c1 no message 2025-05-07 08:27:22 +08:00
kuaifan
a608734be9 no message 2025-05-06 23:33:00 +08:00
kuaifan
e52523f903 no message 2025-05-06 22:41:43 +08:00
kuaifan
82778014b8 no message 2025-05-06 22:12:38 +08:00
kuaifan
cb4b9a361f no message 2025-05-06 16:13:37 +08:00
kuaifan
3a337940d1 no message 2025-05-06 16:13:09 +08:00
kuaifan
9abdafb905 no message 2025-05-06 09:55:42 +08:00
kuaifan
cd494b52a4 no message 2025-05-06 09:19:21 +08:00
kuaifan
5581d1431b no message 2025-05-06 04:36:11 +08:00
kuaifan
6539b14ecf no message 2025-05-06 03:37:28 +08:00
kuaifan
45b30e4a33 no message 2025-05-05 12:08:20 +08:00
kuaifan
ff48d543e7 no message 2025-05-05 11:19:47 +08:00
kuaifan
6562b74130 no message 2025-05-05 09:38:23 +08:00
kuaifan
23a68370b4 no message 2025-05-05 08:25:21 +08:00
kuaifan
cc9f346d49 no message 2025-05-05 08:04:09 +08:00
kuaifan
8c18865138 no message 2025-05-05 06:31:01 +08:00
kuaifan
db3a17a2c8 no message 2025-05-05 00:17:45 +08:00
kuaifan
5a0d1ac0c0 no message 2025-05-04 22:45:07 +08:00
kuaifan
5414accc6c no message 2025-05-04 12:57:04 +08:00
kuaifan
c415ace453 no message 2025-05-01 12:30:20 +08:00
kuaifan
bf34beec20 no message 2025-05-01 11:55:15 +08:00
kuaifan
f4d459af7f fix: 修复录音文件转文字后无法切换翻译的问题 2025-05-01 11:49:22 +08:00
kuaifan
508cc2bd91 no message 2025-04-24 22:40:33 +08:00
kuaifan
35b7e3a289 no message 2025-04-24 21:22:04 +08:00
kuaifan
fc907d23a7 no message 2025-04-24 20:40:52 +08:00
kuaifan
45e663fcf8 no message 2025-04-24 20:15:10 +08:00
kuaifan
b00c6a9268 no message 2025-04-24 09:16:18 +08:00
kuaifan
ad7b0cd834 no message 2025-04-24 09:11:41 +08:00
kuaifan
eccb3e2825 no message 2025-04-24 08:57:43 +08:00
kuaifan
f5a343f358 no message 2025-04-24 07:00:09 +08:00
kuaifan
f8b65a5546 no message 2025-04-23 22:46:52 +08:00
kuaifan
0f0b9c5551 no message 2025-04-23 14:50:28 +08:00
kuaifan
41ab11e7b4 build 2025-04-22 23:02:38 +08:00
kuaifan
9949e7c8d4 no message 2025-04-22 22:11:36 +08:00
kuaifan
9e36d84f19 no message 2025-04-22 22:07:39 +08:00
kuaifan
ada526fa63 no message 2025-04-22 21:37:13 +08:00
kuaifan
ca65eb907d no message 2025-04-22 21:04:17 +08:00
kuaifan
7f2a0dd3e8 no message 2025-04-22 20:39:38 +08:00
kuaifan
6e34409225 no message 2025-04-22 18:12:04 +08:00
kuaifan
4af354e918 no message 2025-04-22 16:32:06 +08:00
kuaifan
207d0caf2a no message 2025-04-22 15:56:35 +08:00
kuaifan
f7487d22d5 no message 2025-04-22 15:52:47 +08:00
kuaifan
3c10976aff no message 2025-04-22 15:00:29 +08:00
kuaifan
ec8e144655 no message 2025-04-22 12:35:41 +08:00
kuaifan
ae5ccfd775 no message 2025-04-22 11:48:54 +08:00
kuaifan
9d9e22451d no message 2025-04-22 09:37:48 +08:00
kuaifan
e68daf870f no message 2025-04-22 09:06:29 +08:00
kuaifan
534e95f86c no message 2025-04-22 00:19:36 +08:00
kuaifan
077713003f feat: 添加删除附件日志记录 2025-04-21 22:56:51 +08:00
kuaifan
35fd8e62ac no message 2025-04-21 22:48:26 +08:00
kuaifan
b7c2ddd59d perf: 优化从任务页面发送消息 2025-04-21 21:29:15 +08:00
kuaifan
f931567f56 perf: 优化已归档/已删除任务列表支持按状态检索 2025-04-21 20:14:31 +08:00
kuaifan
8545e0692c fix: 修复任务详情查看历史空白的情况 2025-04-21 19:53:17 +08:00
kuaifan
19eb05269b fix: 修复我的机器人不回复的情况 2025-04-21 18:28:56 +08:00
kuaifan
92d757662a perf: 优化长按消息菜单位置 2025-04-21 14:50:18 +08:00
kuaifan
1b1406a4d9 perf: 优化登录设备名称 2025-04-21 14:49:36 +08:00
kuaifan
03fc19f070 perf: 优化登录设备名称 2025-04-21 14:26:31 +08:00
kuaifan
ffa09f1b29 fix: 修复设待办后数据不立即显示的问题 2025-04-21 13:46:42 +08:00
kuaifan
ff9a1523fd no message 2025-04-21 12:40:49 +08:00
kuaifan
8a7e5c0830 no message 2025-04-21 12:19:20 +08:00
kuaifan
8e90ad69b1 no message 2025-04-21 12:06:47 +08:00
kuaifan
52c389edd8 no message 2025-04-21 12:06:47 +08:00
kuaifan
f7206c1603 no message 2025-04-21 12:06:35 +08:00
kuaifan
86d9baa503 no message 2025-04-21 08:36:49 +08:00
kuaifan
92c4565590 no message 2025-04-21 01:54:50 +08:00
kuaifan
c51870ff79 build 2025-04-21 00:56:08 +08:00
kuaifan
182f061354 no message 2025-04-20 23:10:31 +08:00
kuaifan
80507cab27 no message 2025-04-20 19:37:12 +08:00
kuaifan
f801ae9b63 no message 2025-04-20 17:53:32 +08:00
kuaifan
977173d987 no message 2025-04-20 09:40:46 +08:00
kuaifan
cd0fcb903f no message 2025-04-20 09:19:46 +08:00
kuaifan
7bae1d9537 feat: 新增系统分享搜索功能 2025-04-20 00:24:33 +08:00
kuaifan
b43cbb7afe no message 2025-04-19 22:06:07 +08:00
kuaifan
72982387cc no message 2025-04-19 21:39:02 +08:00
kuaifan
ff0245840a no message 2025-04-19 21:33:10 +08:00
kuaifan
c55f64e209 no message 2025-04-19 21:21:30 +08:00
kuaifan
a4cb5d1b14 no message 2025-04-19 19:48:24 +08:00
kuaifan
13e1415355 no message 2025-04-19 19:11:05 +08:00
kuaifan
7b49d66a8e no message 2025-04-19 19:06:34 +08:00
kuaifan
63c6e12aca no message 2025-04-19 16:57:56 +08:00
kuaifan
b64d4fd96f no message 2025-04-19 07:57:43 +08:00
kuaifan
dda603c7d8 perf: 优化通用菜单 2025-04-19 01:09:29 +08:00
kuaifan
e22de5cba1 no message 2025-04-18 22:46:19 +08:00
kuaifan
bdabfdcb3d perf: 优化视频压缩 2025-04-18 22:28:24 +08:00
kuaifan
00a8514245 no message 2025-04-18 21:44:33 +08:00
kuaifan
94fd3197b3 no message 2025-04-18 20:26:35 +08:00
kuaifan
7957353c3f no message 2025-04-18 19:25:13 +08:00
kuaifan
b3b7589db3 no message 2025-04-18 14:49:05 +08:00
kuaifan
5aed9ce29e perf: 优化全文搜索 2025-04-18 13:56:11 +08:00
kuaifan
924f0a9f7c perf: 优化全文搜索 2025-04-18 12:40:32 +08:00
kuaifan
7a7cd72db9 perf: 优化全文搜索 2025-04-18 11:59:28 +08:00
kuaifan
e9e9bab479 perf: 优化全文搜索 2025-04-18 01:45:03 +08:00
kuaifan
f258dcfca2 perf: 优化全文搜索 2025-04-18 00:46:59 +08:00
kuaifan
fe84f812e7 perf: 优化全文搜索 2025-04-17 22:14:38 +08:00
kuaifan
9eba376976 perf: 优化全文搜索 2025-04-17 21:55:14 +08:00
kuaifan
462705c4ed perf: 优化全文搜索 2025-04-17 16:45:13 +08:00
kuaifan
a2533ce7f9 perf: 优化全文搜索 2025-04-17 13:04:45 +08:00
kuaifan
dbf42c51a4 perf: 优化全文搜索 2025-04-17 12:45:47 +08:00
kuaifan
f61e7caf2b perf: 优化全文搜索 2025-04-17 12:27:21 +08:00
kuaifan
679c2070c1 perf: 优化全文搜索 2025-04-17 11:14:11 +08:00
kuaifan
92d46e1da3 no message 2025-04-17 10:34:32 +08:00
kuaifan
7ab94205e4 no message 2025-04-17 10:09:28 +08:00
kuaifan
ab616c5d32 perf: 优化长按菜单 2025-04-17 09:46:57 +08:00
kuaifan
8f2f68dffc no message 2025-04-17 09:45:48 +08:00
kuaifan
18b7e17e95 no message 2025-04-16 21:34:33 +08:00
kuaifan
cca2298d3a no message 2025-04-16 19:47:39 +08:00
kuaifan
f3683bcc84 no message 2025-04-16 13:00:36 +08:00
kuaifan
fa2959515e no message 2025-04-16 08:49:33 +08:00
kuaifan
7ab5ddc408 no message 2025-04-15 00:24:54 +08:00
kuaifan
f273858248 build 2025-04-15 00:06:25 +08:00
kuaifan
ca8f7374da no message 2025-04-14 23:59:23 +08:00
kuaifan
ff1dce833a no message 2025-04-14 22:08:59 +08:00
kuaifan
d3d5a7bade no message 2025-04-14 19:48:21 +08:00
kuaifan
f5d6702472 no message 2025-04-14 18:23:04 +08:00
kuaifan
3db687ad40 no message 2025-04-14 17:30:59 +08:00
kuaifan
a5cb958398 perf: 优化移动任务 2025-04-14 15:50:20 +08:00
kuaifan
9e522091c6 no message 2025-04-14 15:29:53 +08:00
kuaifan
79f256976e no message 2025-04-14 14:24:00 +08:00
kuaifan
b560c0bafd feat: 新增任务发送功能 2025-04-14 13:40:46 +08:00
kuaifan
bd157d305e fix: 修复调整任务排序后出现空白的情况 2025-04-14 13:22:11 +08:00
kuaifan
923016197a perf: 优化自己的对话不限修改撤回时间 2025-04-14 13:18:38 +08:00
kuaifan
dcf96e2bf5 perf: 优化访问链接 2025-04-14 13:02:04 +08:00
kuaifan
d4697cb203 perf: 优化访问链接 2025-04-14 12:00:54 +08:00
kuaifan
6e6a50b46e no message 2025-04-14 09:39:47 +08:00
kuaifan
b9830bc64a no message 2025-04-14 08:00:02 +08:00
kuaifan
7c501cec45 no message 2025-04-13 14:59:37 +08:00
kuaifan
add23934ca no message 2025-04-13 13:04:03 +08:00
kuaifan
a8b798b00c no message 2025-04-13 11:33:43 +08:00
kuaifan
b522b1de05 no message 2025-04-13 11:19:37 +08:00
kuaifan
3660cbd450 no message 2025-04-13 10:50:47 +08:00
kuaifan
50f8bb8721 feat: 新增会员详情窗口 2025-04-13 10:50:40 +08:00
kuaifan
e1a2d90382 no message 2025-04-13 09:16:19 +08:00
kuaifan
d8872f215b no message 2025-04-13 00:18:39 +08:00
kuaifan
484bc6ea39 no message 2025-04-12 19:45:14 +08:00
kuaifan
7d1979f067 perf: 优化日历 2025-04-12 19:45:06 +08:00
kuaifan
6927c0b30b no message 2025-04-12 18:54:39 +08:00
kuaifan
aa74c5ccaf perf: 优化长按事件 2025-04-12 18:42:27 +08:00
kuaifan
e3d0f571d2 no message 2025-04-12 17:47:43 +08:00
kuaifan
d03dabdfdf perf: 优化日历 2025-04-12 17:40:05 +08:00
kuaifan
fc339ae55f no message 2025-04-12 17:39:23 +08:00
kuaifan
a0aa04fd8c no message 2025-04-12 15:08:33 +08:00
kuaifan
6dc5ae1ae4 perf: 优化移动端任务窗口布局 2025-04-12 13:20:17 +08:00
kuaifan
df02a6b50f no message 2025-04-12 11:55:43 +08:00
kuaifan
9e4f733c28 no message 2025-04-12 09:11:58 +08:00
kuaifan
1175b330f5 no message 2025-04-11 19:17:26 +08:00
kuaifan
3cb9fff07f perf: 优化长按操作 2025-04-11 13:47:53 +08:00
kuaifan
bfdb72dd0a no message 2025-04-11 09:55:18 +08:00
kuaifan
5489462f90 no message 2025-04-11 09:02:16 +08:00
kuaifan
94ac3c3922 no message 2025-04-10 17:24:51 +08:00
kuaifan
bf75946e14 no message 2025-04-10 17:06:12 +08:00
kuaifan
b2a70e0cce perf: 优化转发确认选项保持上一次选择 2025-04-10 17:05:42 +08:00
kuaifan
83780f9bcd feat: 添加从团队管理打开会话窗口 2025-04-10 17:05:03 +08:00
kuaifan
bfb9795913 no message 2025-04-10 16:50:11 +08:00
kuaifan
208598a6df no message 2025-04-10 16:02:01 +08:00
kuaifan
6c79753051 no message 2025-04-10 11:57:56 +08:00
kuaifan
095a238fff perf: 优化移动端布局 2025-04-10 11:29:31 +08:00
kuaifan
ebbde8afd3 perf: 优化移动端布局 2025-04-10 11:13:19 +08:00
kuaifan
bba5bb7411 fix: 修复移动任务时负责人和协助人可以同时选择的情况 2025-04-10 11:06:29 +08:00
kuaifan
9c155c6cf5 perf: 优化禁止选择会员效果 2025-04-10 11:05:50 +08:00
kuaifan
19da7a74df perf: 优化长按菜单位置 2025-04-10 10:47:12 +08:00
kuaifan
f5d76fd5ff perf: 优化移动端打开会话等待效果 2025-04-10 10:40:40 +08:00
kuaifan
77940c9430 perf: 优化长按菜单位置 2025-04-10 07:46:32 +08:00
kuaifan
54a42a14b6 perf: 优化会议弹窗 2025-04-10 07:45:51 +08:00
kuaifan
52faf7884b perf: 任务详情点任务聊天时不要发送消息 2025-04-10 07:27:10 +08:00
kuaifan
841ed4e682 perf: 优化移动端布局 2025-04-09 23:25:48 +08:00
kuaifan
bc417b9eea perf: 优化移动端布局 2025-04-09 19:12:12 +08:00
kuaifan
da7dc477c8 no message 2025-04-09 13:47:26 +08:00
kuaifan
6c519ebd61 no message 2025-04-08 21:43:41 +08:00
kuaifan
88e859817b no message 2025-04-08 15:34:05 +08:00
kuaifan
f5dd36260f perf: 优化国际化 2025-04-08 15:33:08 +08:00
kuaifan
a5325b84ae no message 2025-04-08 15:04:00 +08:00
kuaifan
7095c9e71e no message 2025-04-08 14:43:56 +08:00
kuaifan
fb4373c83a fix: 修复无法从任务页面打开聊天的情况 2025-04-08 14:31:01 +08:00
kuaifan
dd59a1aebb no message 2025-04-08 12:38:30 +08:00
kuaifan
6f7edd0b40 fix: 修复移动端焦点抖动的问题 2025-04-08 08:49:30 +08:00
kuaifan
397421010e build 2025-04-07 23:11:32 +08:00
kuaifan
8872b0519d no message 2025-04-07 23:09:24 +08:00
kuaifan
83d3b3ffbf fix: 修复部分页面出现空白的情况 2025-04-07 20:09:45 +08:00
kuaifan
d1702bd62c no message 2025-04-07 18:26:02 +08:00
kuaifan
928235eac8 fix: 修复输入框无法点击添加链接的情况 2025-04-07 16:22:36 +08:00
kuaifan
3bdfaab158 perf: 优化数据结构 2025-04-07 14:32:24 +08:00
kuaifan
d7902b4d08 perf: 优化数据结构 2025-04-07 14:06:27 +08:00
kuaifan
3334abfb8f no message 2025-04-07 12:16:33 +08:00
kuaifan
3e44e584c0 perf: 优化数据结构 2025-04-07 11:41:01 +08:00
kuaifan
195a305fc3 perf: 优化数据结构 2025-04-07 11:35:19 +08:00
kuaifan
cedffd17b3 perf: 优化图片存储名 2025-04-07 11:10:17 +08:00
kuaifan
59b29014d9 fix: 修复AI机器人不存在的情况 2025-04-07 09:05:17 +08:00
kuaifan
06db036e4a build 2025-04-07 08:49:39 +08:00
kuaifan
617e0837c9 no message 2025-04-07 08:46:09 +08:00
kuaifan
7a275bd802 perf: 优化数据结构 2025-04-07 08:30:06 +08:00
kuaifan
83f58eae68 perf: 优化数据结构 2025-04-07 06:55:35 +08:00
kuaifan
19815fe27d no message 2025-04-07 06:47:59 +08:00
kuaifan
0f75556bed perf: 优化数据结构 2025-04-07 06:15:17 +08:00
kuaifan
dc0f925d24 no message 2025-04-07 01:21:50 +08:00
kuaifan
c5948c4171 feat: 新增转发至AI开启新会话 2025-04-07 01:20:58 +08:00
kuaifan
d144b06c1f perf: 优化数据结构 2025-04-06 23:43:18 +08:00
kuaifan
92dfea677b perf: 优化数据结构 2025-04-06 23:14:20 +08:00
kuaifan
7b5867e2c0 perf: 优化数据结构 2025-04-04 09:18:51 +08:00
kuaifan
b643fe56d5 perf: 优化数据结构 2025-04-04 08:22:18 +08:00
kuaifan
38fa72e9da perf: 优化数据结构 2025-04-03 22:35:46 +08:00
kuaifan
82fddefc94 perf: 优化消息窗口显示 2025-04-03 22:12:29 +08:00
kuaifan
0c9c9cb90a perf: 优化消息窗口显示 2025-04-03 10:01:02 +08:00
kuaifan
38b50a8a84 perf: 优化消息窗口显示 2025-04-02 20:35:03 +08:00
kuaifan
0f250dbafd perf: 优化目录结构 2025-04-02 19:17:03 +08:00
kuaifan
168650649f perf: 优化日历 2025-04-02 18:48:25 +08:00
kuaifan
52babf82ae perf: 优化任务时间范围选择 2025-03-31 23:49:11 +08:00
kuaifan
0c8517667f build 2025-03-30 11:13:39 +08:00
kuaifan
77d105cb9f no message 2025-03-30 10:45:24 +08:00
kuaifan
8af33ea66a perf: 优化消息窗口 2025-03-30 10:13:43 +08:00
kuaifan
a57740e14e perf: 优化消息窗口 2025-03-30 08:56:22 +08:00
kuaifan
82230d70a5 perf: 优化消息窗口 2025-03-30 08:50:50 +08:00
kuaifan
15f3f9c0e5 perf: 优化消息窗口 2025-03-30 07:51:13 +08:00
kuaifan
7fc328492b no message 2025-03-29 22:09:07 +08:00
kuaifan
81cedca590 no message 2025-03-29 14:11:54 +08:00
kuaifan
df4e00e23f perf: 优化消息长按菜单 2025-03-29 14:11:47 +08:00
kuaifan
ad70f23a05 perf: 优化内置浏览器 2025-03-29 13:55:16 +08:00
kuaifan
c93beb27fd no message 2025-03-29 12:44:20 +08:00
kuaifan
41da2231ed no message 2025-03-29 12:33:12 +08:00
kuaifan
9d9213fbdb feat: 添加移动端提示可能要发送的图片 2025-03-29 00:23:49 +08:00
kuaifan
50106d19e8 fix: 修复未读数错误暴增的情况 2025-03-28 19:44:43 +08:00
kuaifan
62d1e676bd perf: 优化App隐私政策提示 2025-03-28 19:35:12 +08:00
kuaifan
0b7d49785c no message 2025-03-28 18:45:09 +08:00
kuaifan
40736c4a05 no message 2025-03-28 18:30:53 +08:00
kuaifan
1f0ab02702 feat: 添加移动端提示可能要发送的图片 2025-03-28 17:01:07 +08:00
kuaifan
21aa4f7b2b fix: 修复地址可能存在localhost的情况 2025-03-28 15:45:13 +08:00
kuaifan
43d0a85061 feat: 添加移动端提示可能要发送的图片 2025-03-28 14:07:33 +08:00
kuaifan
8bdd31ae67 no message 2025-03-28 14:07:26 +08:00
kuaifan
b78f93979d no message 2025-03-27 20:46:12 +08:00
kuaifan
8d6b4a1d2e feat: 添加移动端提示可能要发送的图片 2025-03-27 20:46:03 +08:00
kuaifan
7630c83ae0 perf: 优化对话独立窗口显示 2025-03-27 20:25:51 +08:00
kuaifan
c7c47aff5a fix: 修复消息编辑和发布时序号对不上 2025-03-27 18:09:07 +08:00
kuaifan
72e475cb84 fix: 修复草稿出现上一次内容的情况 2025-03-27 17:03:26 +08:00
kuaifan
f750a6aec2 no message 2025-03-26 23:50:41 +08:00
kuaifan
0dde37e1f1 no message 2025-03-26 18:57:19 +08:00
kuaifan
a2066fc137 no message 2025-03-26 15:26:07 +08:00
kuaifan
f652f35c3a perf: 优化移动端选择交互 2025-03-26 14:17:15 +08:00
kuaifan
562697da27 fix: 修复本地群消息通知没有会员昵称的问题 2025-03-25 18:31:44 +08:00
kuaifan
d23d77ff90 perf: 优化移动端选中消息文本 2025-03-25 18:14:40 +08:00
kuaifan
119443cc88 no message 2025-03-25 17:36:21 +08:00
kuaifan
27ae831799 perf: 优化移动端选中消息文本 2025-03-25 17:35:47 +08:00
kuaifan
b482947207 perf: 优化撤回消息逻辑 2025-03-25 16:39:49 +08:00
kuaifan
6ebc89695a perf: 优化移动端选中消息文本 2025-03-25 16:38:19 +08:00
kuaifan
a65dfec7a8 perf: 优化提及搜索 2025-03-24 21:53:04 +08:00
kuaifan
a0cd79e587 fix: 修复了拉人进群无法踢出去的问题 2025-03-24 21:33:04 +08:00
kuaifan
8fe1e2fee4 no message 2025-03-24 21:13:17 +08:00
kuaifan
3cc9f7bc40 fix: 提及出现白色字的情况 2025-03-24 21:05:25 +08:00
kuaifan
8d3d5025ed no message 2025-03-24 20:39:53 +08:00
kuaifan
a49c0aea47 perf: 优化机器人Webhook消息 2025-03-24 20:34:28 +08:00
kuaifan
d366cf9885 build 2025-03-23 23:24:48 +08:00
kuaifan
be53afe6b4 no message 2025-03-22 19:24:28 +08:00
kuaifan
cdd980112d no message 2025-03-22 19:02:00 +08:00
kuaifan
bca284969d no message 2025-03-22 18:44:09 +08:00
kuaifan
dd899a3e13 feat: 添加我的机器人管理 2025-03-22 18:19:39 +08:00
kuaifan
d6ca66aa2f no message 2025-03-21 22:16:47 +08:00
kuaifan
20ba671cd3 no message 2025-03-21 13:37:27 +08:00
kuaifan
672795ac49 perf: 优化初始化逻辑 2025-03-21 13:37:21 +08:00
kuaifan
9716d7fe43 perf: 优化docker配置 2025-03-21 11:34:09 +08:00
kuaifan
193ad8d902 build 2025-03-21 09:06:41 +08:00
kuaifan
a87f903c50 no message 2025-03-21 09:04:58 +08:00
kuaifan
82f154a229 no message 2025-03-21 00:18:09 +08:00
kuaifan
bee36801ab feat: 新增独立窗口打开会话 2025-03-21 00:09:16 +08:00
kuaifan
37f379c890 no message 2025-03-20 23:18:04 +08:00
kuaifan
88b995ca9c perf: 优化AI支持文件类型 2025-03-20 15:34:31 +08:00
kuaifan
919289c5ca fix: 修复搜索结果显示即将到期 2025-03-20 14:51:13 +08:00
kuaifan
0535b56766 build 2025-03-19 23:37:39 +08:00
kuaifan
6afd413b87 no message 2025-03-19 23:35:02 +08:00
kuaifan
4818409329 perf: 优化AI解析文件 2025-03-19 23:33:17 +08:00
kuaifan
919b652a06 perf: 优化AI解析文件 2025-03-19 22:49:03 +08:00
kuaifan
15d3ec9d81 perf: 优化 WebSocket 消息 2025-03-19 22:01:07 +08:00
kuaifan
e0be6e429e no message 2025-03-19 21:04:05 +08:00
kuaifan
8d24be914d perf: 优化数据 2025-03-19 15:14:23 +08:00
kuaifan
8bbe9c97e9 perf: 优化数据 2025-03-19 12:53:44 +08:00
kuaifan
ccbd904a3f build 2025-03-19 08:51:43 +08:00
kuaifan
4ed3db7e41 fix: 修复查看待办图片不符的情况 2025-03-19 08:48:51 +08:00
kuaifan
65ced28004 perf: 优化数据 2025-03-18 23:43:36 +08:00
kuaifan
4c282962b3 perf: 优化未读消息数 2025-03-18 18:48:39 +08:00
kuaifan
c64c436b9f perf: 优化搜索组件 2025-03-18 18:36:20 +08:00
kuaifan
378e270f41 no message 2025-03-18 17:45:53 +08:00
kuaifan
7217bd7d1a perf: 已归档/已删除任务列表支持按状态检索 2025-03-18 17:43:21 +08:00
kuaifan
ff2461d89d no message 2025-03-18 12:01:46 +08:00
kuaifan
0eb3430c14 perf: 优化消息流效果 2025-03-18 11:38:58 +08:00
kuaifan
da7c1e40e3 no message 2025-03-18 10:29:05 +08:00
kuaifan
8c9e928ddc no message 2025-03-18 01:52:18 +08:00
kuaifan
477aef7db6 perf: 优化AI上下文 2025-03-18 01:49:24 +08:00
kuaifan
cc97d9f1ea perf: 优化工作流获取 2025-03-17 20:31:30 +08:00
kuaifan
ee6cf05a92 perf: 优化转发功能 2025-03-17 10:15:12 +08:00
kuaifan
575db58476 build 2025-03-16 23:43:51 +08:00
kuaifan
986a2f8cbb no message 2025-03-16 23:28:12 +08:00
kuaifan
0b1da914cd no message 2025-03-16 23:06:45 +08:00
kuaifan
04acd7c56d feat: 可点击标注图标查看标注人员 2025-03-16 22:20:39 +08:00
kuaifan
4430d85242 perf: 优化转发消息 2025-03-16 21:51:19 +08:00
kuaifan
55ade32589 feat: 支持分享工作报告到消息 2025-03-16 21:39:12 +08:00
kuaifan
0ffbaaaeaa perf: 优化转发消息 2025-03-16 21:08:04 +08:00
kuaifan
62b40ddb84 perf: 优化转发消息 2025-03-16 20:43:09 +08:00
kuaifan
2d5ce87605 feat: 支持AI分析工作报告 2025-03-16 00:55:45 +08:00
kuaifan
7ca0bc5960 feat: 支持使用%发送工作报告 2025-03-15 17:53:21 +08:00
kuaifan
021c09e426 feat: 支持使用%发送工作报告 2025-03-15 17:06:47 +08:00
kuaifan
75db81f2f9 feat: 支持使用%发送工作报告 2025-03-15 15:59:06 +08:00
kuaifan
f162617765 no message 2025-03-14 23:14:44 +08:00
kuaifan
b7d10a4c58 perf: 优化工作报告列表 2025-03-14 23:12:40 +08:00
kuaifan
79ca1aea02 no message 2025-03-14 22:41:26 +08:00
kuaifan
957201804c feat: 新增自定义撤回及修改消息时限 2025-03-14 21:07:44 +08:00
kuaifan
cf5e126eaa perf: 优化引用消息 2025-03-14 19:53:00 +08:00
kuaifan
69fc0a118b no message 2025-03-14 15:01:35 +08:00
kuaifan
4dacc26567 perf: 优化全局提示 2025-03-14 14:50:32 +08:00
kuaifan
7de1ed7d45 no message 2025-03-14 12:11:08 +08:00
kuaifan
ab47f01625 perf: 优化全局提示 2025-03-14 12:07:49 +08:00
kuaifan
13c4fa4f1f no message 2025-03-14 09:25:33 +08:00
kuaifan
173631f115 no message 2025-03-14 09:11:04 +08:00
kuaifan
8462e9c097 no message 2025-03-14 07:28:29 +08:00
kuaifan
3c9447e1b6 no message 2025-03-13 22:01:46 +08:00
kuaifan
38eaf2eb02 perf: 优化草稿消息 2025-03-13 21:35:05 +08:00
kuaifan
c8364ed17b perf: 优化草稿消息 2025-03-13 20:54:35 +08:00
kuaifan
ba52738904 perf: 优化草稿消息 2025-03-13 20:37:41 +08:00
kuaifan
4061ae4275 perf: 优化草稿消息 2025-03-13 20:18:34 +08:00
kuaifan
82afb5b150 no message 2025-03-13 17:27:13 +08:00
kuaifan
e1203f0c8d no message 2025-03-13 17:12:11 +08:00
kuaifan
e6f6b3fee2 fix: 工作流存在已离职人员 2025-03-13 15:02:53 +08:00
kuaifan
e5efcd3d26 no message 2025-03-13 14:36:00 +08:00
kuaifan
bf45587c80 no message 2025-03-13 13:25:16 +08:00
kuaifan
29a0d22938 build 2025-03-13 01:40:11 +08:00
kuaifan
635cc04c50 perf: 优化消息定位 2025-03-13 01:38:35 +08:00
kuaifan
bc5343652b perf: 优化消息性能 2025-03-12 22:41:41 +08:00
kuaifan
03f140fe3b fix: 看不到未读消息定位提醒 2025-03-12 14:19:07 +08:00
kuaifan
3e4a119f61 build 2025-03-12 00:18:12 +08:00
kuaifan
3b7bcbc14a no message 2025-03-11 17:41:12 +08:00
kuaifan
3c49e96e02 no message 2025-03-11 16:24:31 +08:00
kuaifan
5be209ab59 no message 2025-03-11 15:53:53 +08:00
kuaifan
56ea048ab3 no message 2025-03-11 10:21:44 +08:00
kuaifan
9fc0bd0439 no message 2025-03-10 23:12:28 +08:00
kuaifan
1c2798cbf4 perf: 优化消息定位 2025-03-10 23:06:30 +08:00
kuaifan
9d8af2eaab perf: 优化MD消息 2025-03-10 22:38:42 +08:00
kuaifan
bba1e0d12f fix: 会话内消息搜索布局错位 2025-03-10 20:52:52 +08:00
kuaifan
c060e60e4a fix: 流程设置翻译不统一 2025-03-10 20:51:58 +08:00
kuaifan
1c504bd899 no message 2025-03-10 00:12:05 +08:00
kuaifan
b617648bd8 no message 2025-03-09 23:46:01 +08:00
kuaifan
e849c7a34f build 2025-03-09 23:11:14 +08:00
kuaifan
f6dd1ce98e no message 2025-03-09 23:02:35 +08:00
kuaifan
9c78db8d45 no message 2025-03-09 22:52:38 +08:00
kuaifan
5154348cf9 perf: 优化发送语音效果 2025-03-09 22:41:15 +08:00
kuaifan
4521cea3b4 perf: 优化发送语音效果 2025-03-09 19:54:09 +08:00
kuaifan
0ff1ac7743 no message 2025-03-09 18:54:33 +08:00
kuaifan
277a751ed4 no message 2025-03-09 18:40:03 +08:00
kuaifan
96be2a86ca no message 2025-03-09 18:17:18 +08:00
kuaifan
f28bff569a no message 2025-03-09 15:49:56 +08:00
kuaifan
e34aa77a54 perf: 录音转文字支持自定义语言 2025-03-09 15:32:38 +08:00
kuaifan
e53b65496f perf: 录音转文字支持自定义语言 2025-03-09 11:33:37 +08:00
kuaifan
f6ee630615 no message 2025-03-08 19:11:17 +08:00
kuaifan
ec2e1e3152 no message 2025-03-08 17:56:09 +08:00
kuaifan
6cffe9baed perf: 优化ES模块 2025-03-08 16:39:35 +08:00
kuaifan
b63df27409 perf: 优化emoji表情 2025-03-08 15:50:35 +08:00
kuaifan
617c466ac0 perf: 按住Ctrl/Command键可连续选择表情 2025-03-08 12:13:25 +08:00
kuaifan
ed8e443f3a perf: 优化ES模块 2025-03-08 12:12:48 +08:00
kuaifan
58cb49b125 perf: 优化ES模块 2025-03-08 10:15:58 +08:00
kuaifan
7dd5baa9ec fix: 定位签到失败的问题 2025-03-07 23:43:39 +08:00
kuaifan
bbf9107560 perf: md消息支持html代码 2025-03-07 23:23:51 +08:00
kuaifan
be527355ee no message 2025-03-07 22:15:27 +08:00
kuaifan
c866500120 perf: 优化脚本 2025-03-07 16:18:06 +08:00
kuaifan
3e2a40aaa0 perf: 优化安装命令 2025-03-07 15:13:19 +08:00
kuaifan
eef9fa56c6 perf: 优化ES索引名称 2025-03-07 12:57:37 +08:00
zzzzzhy
945d84dbc4 添加es证书配置 2025-03-07 12:57:37 +08:00
kuaifan
d353d33107 no message 2025-03-07 12:57:37 +08:00
kuaifan
f54bad5d79 no message 2025-03-06 16:16:43 +08:00
kuaifan
b605c70e91 no message 2025-03-06 14:52:40 +08:00
kuaifan
1752e88c42 no message 2025-03-05 15:19:38 +08:00
kuaifan
e2718a39a0 no message 2025-03-05 10:19:28 +08:00
kuaifan
25298ac69e build 2025-03-05 08:33:52 +08:00
kuaifan
cf9f389f75 no message 2025-03-05 08:23:08 +08:00
kuaifan
567c75830a perf: 新增录音转文字 2025-03-05 01:52:37 +08:00
kuaifan
7b1d352c95 perf: 新增录音转文字 2025-03-05 01:22:12 +08:00
kuaifan
4fa54381a6 no message 2025-03-04 20:12:32 +08:00
kuaifan
9c91f7cf83 no message 2025-03-04 19:56:13 +08:00
kuaifan
edd5cd1ca1 perf: 优化数据排序 2025-03-04 19:24:13 +08:00
kuaifan
f2ec6ad05e no message 2025-03-04 18:30:23 +08:00
kuaifan
a04ef4ac38 no message 2025-03-04 16:58:17 +08:00
kuaifan
43b3d1d379 no message 2025-03-04 16:46:45 +08:00
kuaifan
b65fdeacc2 no message 2025-03-04 09:33:26 +08:00
kuaifan
622fe1e5d9 no message 2025-03-04 08:44:11 +08:00
kuaifan
a6c7c0c7ad fix: 全屏预览图片关闭窗口 2025-03-04 07:38:06 +08:00
kuaifan
e5c8748b75 no message 2025-03-04 06:35:32 +08:00
kuaifan
f096d71cc1 no message 2025-03-03 23:45:42 +08:00
kuaifan
d73a152a36 no message 2025-03-03 22:48:09 +08:00
kuaifan
f4e6fd060e no message 2025-03-03 21:00:04 +08:00
kuaifan
c78ca1de5d no message 2025-03-03 18:11:32 +08:00
kuaifan
2b219c7256 no message 2025-03-03 14:53:45 +08:00
kuaifan
6ffa651742 no message 2025-03-03 13:16:03 +08:00
kuaifan
cb3b22a4bf fix: 点击排序导致任务不显示的情况 2025-03-03 12:51:33 +08:00
kuaifan
145bfdb0e9 no message 2025-03-03 12:49:04 +08:00
kuaifan
8c7b0c502d no message 2025-03-03 11:59:02 +08:00
kuaifan
684bf12a5c no message 2025-03-03 10:27:56 +08:00
kuaifan
aaa75aff14 build 2025-03-03 08:32:50 +08:00
kuaifan
f03600bd65 perf: 添加全局搜索功能 2025-03-03 08:20:17 +08:00
kuaifan
1c4c4fe3fb perf: 添加全局搜索功能 2025-03-03 07:01:49 +08:00
kuaifan
5e46b2cd1a perf: 添加全局搜索功能 2025-03-02 23:43:18 +08:00
kuaifan
027db7c0ec perf: 添加全局搜索功能 2025-03-02 19:00:09 +08:00
kuaifan
5bb17ddc6b no message 2025-03-02 18:59:54 +08:00
kuaifan
e8edd74bc3 no message 2025-03-01 23:59:51 +08:00
kuaifan
ed064a825a perf: 优化消息搜索 2025-03-01 23:59:14 +08:00
kuaifan
32c232a0b5 perf: 优化消息搜索 2025-03-01 20:54:02 +08:00
kuaifan
c2fd747c45 no message 2025-03-01 20:39:13 +08:00
kuaifan
9148853f2c perf: 团队管理支持调整部门区域尺寸 2025-03-01 14:51:07 +08:00
kuaifan
23d0f50a3d perf: 任务详情支持调整聊天区域尺寸 2025-03-01 14:50:44 +08:00
kuaifan
36cdf87bfe perf: 优化团队部门支持3级部门 2025-03-01 13:41:10 +08:00
kuaifan
cfd2e1fd7b perf: 可见群组ID 2025-03-01 12:06:03 +08:00
kuaifan
3cafac99ff perf: 支持在团队管理打开群聊 2025-03-01 11:54:20 +08:00
kuaifan
1dd4e8da71 perf: 优化回复消息自动@逻辑 2025-03-01 11:19:50 +08:00
kuaifan
543015a36e perf: 转发预览隐藏表情回应部分 2025-03-01 10:14:51 +08:00
kuaifan
2efdfc4b1f no message 2025-03-01 00:34:43 +08:00
kuaifan
7234d9307e no message 2025-03-01 00:21:21 +08:00
kuaifan
769ce1ce7c no message 2025-02-28 22:49:45 +08:00
kuaifan
62c1d5783e perf: 优化任务日志 2025-02-28 22:38:11 +08:00
kuaifan
a6bd4a2ffe no message 2025-02-28 21:24:09 +08:00
kuaifan
f1a9077b7e no message 2025-02-28 21:20:36 +08:00
kuaifan
2c3e80bd8f perf: 已删除任务支持按标签搜索 2025-02-28 21:07:54 +08:00
kuaifan
e52d066fb0 perf: 归档任务支持按标签搜索 2025-02-28 21:07:32 +08:00
kuaifan
5279d57018 perf: 项目面板添加按标签筛选 2025-02-28 20:54:25 +08:00
kuaifan
25e5eb4427 perf: 优化 AI 提示词 2025-02-26 20:50:17 +08:00
kuaifan
b01d5ce8c4 perf: 优化 AI 设置 2025-02-26 20:12:23 +08:00
kuaifan
ff41f5c041 no message 2025-02-26 11:58:53 +08:00
kuaifan
dd0770a93f no message 2025-02-25 21:36:55 +08:00
kuaifan
9a3e76fff3 no message 2025-02-25 21:08:54 +08:00
kuaifan
7c867578ee build 2025-02-25 21:08:40 +08:00
kuaifan
d543c27000 perf: 工作报告支持查看仅未读 2025-02-25 20:39:22 +08:00
kuaifan
a8be330baa perf: AI 支持引用文件 2025-02-25 20:31:13 +08:00
kuaifan
c128c58110 perf: 优化图文消息 2025-02-25 19:51:07 +08:00
kuaifan
e32a3887cd perf: 优化文本信息复制 2025-02-25 17:47:46 +08:00
kuaifan
94932c7486 perf: 优化图文消息 2025-02-25 17:38:12 +08:00
kuaifan
a1920745fb perf: 优化样式 2025-02-25 11:43:48 +08:00
kuaifan
51e8f9555e perf: 无法再AI机器人页面看到模型的问题 2025-02-25 11:37:51 +08:00
kuaifan
213ab8418b fix: 首次跟ai聊天没有记录的问题 2025-02-25 11:14:41 +08:00
kuaifan
707f1dd6cb no message 2025-02-24 10:41:35 +08:00
kuaifan
125ce036cd perf: 优化MD消息过长处理 2025-02-24 09:12:12 +08:00
kuaifan
172c562a71 build 2025-02-24 00:08:50 +08:00
kuaifan
80bbe6711c no message 2025-02-24 00:07:31 +08:00
kuaifan
3f56c64086 perf: 优化AI支持分析指定文件 2025-02-23 23:55:02 +08:00
kuaifan
e6167119e0 no message 2025-02-22 20:27:27 +08:00
kuaifan
368fae5f32 perf: 支持在AI对话中直接引用任务提问 2025-02-22 20:27:10 +08:00
kuaifan
6ae46cf7bb perf: 支持在AI对话中直接引用任务提问 2025-02-22 17:49:11 +08:00
kuaifan
e97806c85b no message 2025-02-22 17:49:04 +08:00
kuaifan
f31e88bed1 no message 2025-02-22 12:14:03 +08:00
kuaifan
6bd20038f9 no message 2025-02-22 11:29:46 +08:00
kuaifan
30cfb1200d no message 2025-02-22 11:26:16 +08:00
kuaifan
154e0039d1 perf: 优化 AI 参数 2025-02-22 11:13:16 +08:00
kuaifan
a8f3b02ee7 perf: 优化 Ollama AI 2025-02-22 01:29:54 +08:00
kuaifan
b3e83e13bc perf: 优化设置 2025-02-22 00:59:52 +08:00
kuaifan
d0a0e77c44 perf: 优化设置 2025-02-22 00:45:20 +08:00
kuaifan
a14896307f no message 2025-02-21 23:26:54 +08:00
kuaifan
976b300277 perf: 优化AI设置 2025-02-21 23:26:14 +08:00
kuaifan
ccbd873204 perf: 优化AI设置 2025-02-21 22:15:59 +08:00
kuaifan
9c1482f9e9 feat: 添加 Grok AI、Ollama AI 2025-02-21 17:04:59 +08:00
kuaifan
5a7f4efa91 feat: 添加 Grok AI、Ollama AI 2025-02-21 12:08:54 +08:00
kuaifan
f78c4a1fb0 perf: 优化AI设置 2025-02-21 11:37:39 +08:00
kuaifan
db6500369f perf: 优化AI消息 2025-02-20 01:12:48 +08:00
kuaifan
9e4beaa317 perf: 表情回复时更新对话列表 2025-02-14 21:14:15 +08:00
kuaifan
afd021737a build 2025-02-14 20:41:04 +08:00
kuaifan
3982ed56f7 no message 2025-02-14 15:15:36 +08:00
kuaifan
df4a01a7f9 perf: onlyoffice 支持打开超过100m的文件 2025-02-14 15:07:55 +08:00
kuaifan
a6fac96ec1 perf: 优化点击上传列表效果 2025-02-14 15:06:17 +08:00
kuaifan
8ed9186ff4 perf: AI支持自定义模型列表 2025-02-14 01:07:32 +08:00
kuaifan
821df75d4b fix: 撤回消息是消息列表不更新的情况 2025-02-13 21:21:32 +08:00
kuaifan
0c09a2445c build 2025-02-12 21:40:21 +08:00
kuaifan
e6983e858d no message 2025-02-12 21:33:10 +08:00
kuaifan
f8b69df955 fix: 修复偶现的是子窗口出现身份丢失的情况 2025-02-12 21:09:54 +08:00
kuaifan
15370a93c7 perf: 优化查看长消息内容 2025-02-12 20:46:18 +08:00
kuaifan
bc18aeeadc no message 2025-02-11 15:11:23 +08:00
kuaifan
a1f143b0aa build 2025-02-11 07:53:53 +09:00
kuaifan
c13fe9d590 perf: 优化审批功能 2025-02-11 03:33:58 +09:00
kuaifan
50203fbcb3 perf: AI机器人支持多会话 2025-02-11 03:12:25 +09:00
kuaifan
ffe7ebf711 perf: AI机器人支持多会话 2025-02-11 02:41:01 +09:00
kuaifan
f0b5e0c3b9 perf: AI机器人支持多会话 2025-02-10 18:45:35 +09:00
kuaifan
501235ef12 perf: AI机器人支持多会话 2025-02-10 16:48:08 +09:00
kuaifan
da0fa31181 perf: AI机器人支持多会话 2025-02-10 15:53:14 +09:00
kuaifan
0272933f70 perf: AI机器人支持多会话 2025-02-10 15:43:02 +09:00
kuaifan
30d88761b4 perf: AI机器人支持多会话 2025-02-10 12:39:36 +09:00
kuaifan
fb286cea3c perf: AI机器人支持自定义模型 2025-02-08 12:55:11 +09:00
kuaifan
6bcc7b6c49 perf: AI机器人支持多会话 2025-02-07 16:44:02 +09:00
kuaifan
6338a44cc1 no message 2025-02-07 16:41:52 +09:00
kuaifan
ae4680f20c build 2025-02-07 05:07:20 +09:00
kuaifan
2841874417 no message 2025-02-07 05:07:10 +09:00
kuaifan
b6a4e6b4de perf: 支持下载聊天引用的文件 2025-02-07 04:55:58 +09:00
kuaifan
34cfd1e344 perf: 优化翻译消息 2025-02-07 04:38:23 +09:00
kuaifan
b467dc55e5 perf: 支持显示思考过程 2025-02-05 15:38:07 +09:00
kuaifan
9fd8d44a6e build 2025-02-05 01:37:41 +09:00
kuaifan
64262134c4 perf: 支持自定义仪表盘欢迎词 2025-02-05 01:33:32 +09:00
kuaifan
0019c9ef41 build 2025-02-04 13:14:43 +09:00
kuaifan
2676ebd047 no message 2025-02-04 13:10:46 +09:00
kuaifan
97cdd56110 fix: 跨地区发消息出现消息过期的情况 2025-02-04 13:10:46 +09:00
kuaifan
d973451bdc feat: 添加 DeepSeek AI 2025-02-04 13:10:46 +09:00
kuaifan
80313f613e perf: ChatGPT 支持自定义 Base URL 2025-02-04 13:10:46 +09:00
kuaifan
5c564524a3
Merge pull request #247 from zzzzzhy/patch-2
定时任务判断fix
2025-01-17 20:24:22 +08:00
zzzzzhy
e081fbd92b
定时任务判断fix
修复定时更新https证书任务判断逻辑
2025-01-17 14:15:24 +08:00
kuaifan
0ecc20472a
Merge pull request #246 from zzzzzhy/patch-1
安装软件依赖
2025-01-16 22:35:38 +08:00
zzzzzhy
b51052f0c6
安装软件依赖 2025-01-16 20:14:08 +08:00
kuaifan
cb106e42ee
Merge pull request #245 from zzzzzhy/renew_cert
feat:添加https证书自动更新
2025-01-16 19:54:17 +08:00
zzzzzhy
52f9495ff8 feat:添加https证书自动更新 2025-01-16 06:44:26 +00:00
kuaifan
440b633bad perf: 优化仪表盘任务更新规则 2025-01-15 15:41:38 +08:00
kuaifan
a07913181a fix: 多线程下载文件损坏的问题 2025-01-15 15:27:39 +08:00
kuaifan
34ffd96c86 no message 2025-01-13 10:56:30 +08:00
kuaifan
46a623b430 perf: 优化仪表盘任务更新规则 2025-01-13 10:56:29 +08:00
kuaifan
c16e37023c fix: 多线程下载文件损坏的问题 2025-01-13 10:56:29 +08:00
kuaifan
1cb0cdf540
Merge pull request #242 from nightcp/fix-report-sign-repeat 2025-01-04 20:48:09 +08:00
nightcp
073d03a882 fix: 修复新建周报或日报唯一标识重复 2025-01-04 19:58:37 +08:00
kuaifan
30b9276ab4 no message 2025-01-03 08:19:55 +08:00
kuaifan
76c8b4a4c6 no message 2025-01-03 04:48:38 +08:00
kuaifan
9ea4781d93 perf: 更新小海豚表情包 2025-01-03 03:56:22 +08:00
kuaifan
07d583f73f perf: 优化任务时间冲突提示 2025-01-02 20:25:47 +08:00
kuaifan
12c74aef7a no message 2025-01-02 14:56:30 +08:00
kuaifan
64b10e3060 perf: 优化消息 2025-01-02 14:54:22 +08:00
kuaifan
ab2b29f267 perf: 群聊总人数排除机器人 2025-01-02 14:23:23 +08:00
kuaifan
be9a968ad9 perf: 群聊总人数排除机器人 2025-01-02 13:41:09 +08:00
kuaifan
5f87067a75 fix: 部分电脑无法复制的问题 2025-01-02 12:48:35 +08:00
weifs
ef273bd9dd fix: 修复任务可见性 - 任务重覆获取, 子任务负责人看不到任务问题 2024-12-24 00:11:23 +08:00
weifs
0737a9fae7 fix: 修复任务可见性 - 任务重覆获取, 子任务负责人看不到任务问题 2024-12-23 23:19:22 +08:00
kuaifan
727d7e1d81 build 2024-12-23 11:53:40 +08:00
kuaifan
87e8589aea no message 2024-12-23 11:47:39 +08:00
kuaifan
b13758d3e9 perf: 优化任务面板 2024-12-22 17:45:58 +08:00
kuaifan
14775e2861 perf: 优化子任务的可见性 2024-12-21 22:20:39 +08:00
kuaifan
94af3822d8 perf: 优化客户端 2024-12-21 15:33:01 +08:00
kuaifan
07254c9f27 perf: 优化会议 2024-12-21 11:57:48 +08:00
kuaifan
a99c2f6944 perf: 优化客户端 2024-12-21 11:47:49 +08:00
kuaifan
f9540b08cd perf: 优化会议 2024-12-20 21:24:17 +08:00
kuaifan
34af77eb6d no message 2024-12-20 20:01:42 +08:00
kuaifan
cf3f22776c perf: 优化会员搜索 2024-12-20 19:59:18 +08:00
kuaifan
5bebc8b5ee perf: 优化打开会话 2024-12-20 19:41:00 +08:00
kuaifan
8a4b0c57f9 perf: 优化会议 2024-12-20 19:25:05 +08:00
kuaifan
1acfd7ee34 no message 2024-12-20 09:22:14 +08:00
kuaifan
a29661c54d perf: 优化会议 2024-12-20 09:01:17 +08:00
kuaifan
90558d5ece no message 2024-12-18 20:37:16 +08:00
kuaifan
e6c7007be5 perf: 优化项目面板任务加载 2024-12-18 20:14:35 +08:00
kuaifan
16d0d1687f perf: 优化客户端加载 2024-12-18 19:56:36 +08:00
kuaifan
95ab44d118 perf: 优化客户端加载 2024-12-18 15:23:25 +08:00
kuaifan
e541757b76 no message 2024-12-18 15:22:13 +08:00
kuaifan
f422aea330 fix: 移交账号后工作流的负责人没有更新 2024-12-18 00:25:04 +08:00
kuaifan
d5eb3716aa no message 2024-12-18 00:25:04 +08:00
yijixx
7fb854fb48 feat: 替换网页的资源为本地资源 2024-12-17 17:01:17 +08:00
kuaifan
60b5ecdcd7 fix: 全屏预览时深色皮肤反色的情况 2024-12-17 09:11:12 +08:00
kuaifan
6cce7d31ff build 2024-12-17 08:49:40 +08:00
kuaifan
46f5dd99a6 perf: 优化对话阅读状况 2024-12-17 08:47:05 +08:00
kuaifan
9753dec996 perf: 优化表情回复 2024-12-17 08:41:46 +08:00
kuaifan
53f2e07178 build 2024-12-17 00:14:13 +08:00
kuaifan
3aa2c604d8 perf: 优化桌面端数据处理 2024-12-17 00:07:50 +08:00
kuaifan
d8fbf36e00 perf: 优化资源 2024-12-16 23:29:05 +08:00
kuaifan
008653e3d9 fix: 桌面端查看表情图片缩略图显示错误 2024-12-16 21:23:18 +08:00
kuaifan
23188777fe fix: 项目面板任务不显示的情况 2024-12-16 21:15:30 +08:00
kuaifan
8eb0a49ee6 perf: 优化数据流 2024-12-16 17:04:45 +08:00
kuaifan
207f09a4af fix: 修复移动任务子任务不跟随的情况 2024-12-16 16:07:39 +08:00
kuaifan
69120c5045 build 2024-12-15 23:39:27 +08:00
kuaifan
b8143d1a9b no message 2024-12-15 23:24:38 +08:00
kuaifan
f7eab5893a perf: AI创建任务确认 2024-12-15 22:53:26 +08:00
kuaifan
5fc598a220 no message 2024-12-15 22:45:27 +08:00
kuaifan
783c21ad18 perf: 优化项目面板 2024-12-15 22:27:05 +08:00
kuaifan
a1ce6e6928 perf: 优化项目面板 2024-12-15 09:39:18 +08:00
kuaifan
8cbae629a5 perf: 优化项目面板 2024-12-14 18:51:46 +08:00
kuaifan
da7e832f21 fix: 复制文件权限判断 2024-12-14 18:41:30 +08:00
kuaifan
a572ba0523 perf: 优化项目面板 2024-12-14 18:38:47 +08:00
kuaifan
85a20168dc build 2024-12-13 23:28:46 +08:00
kuaifan
25be9c0fef perf: 优化子任务上下文 2024-12-13 23:19:20 +08:00
kuaifan
a8c890ba51 perf: 优化子任务时间调整 2024-12-13 19:28:05 +08:00
kuaifan
11628b98ca no message 2024-12-13 16:15:47 +08:00
kuaifan
4ae6ca945b no message 2024-12-13 16:00:21 +08:00
kuaifan
49aa1434aa perf: 优化超长文本信息 2024-12-13 15:49:31 +08:00
kuaifan
9e92c61fbf no message 2024-12-13 15:49:31 +08:00
kuaifan
c84111b6b9 perf: 记录版本信息 2024-12-13 15:49:31 +08:00
kuaifan
3a2fcdd18a no message 2024-12-13 15:49:30 +08:00
kuaifan
84a800f69b fix: @在线状态不正确 2024-12-13 15:49:30 +08:00
kuaifan
77e08aa048 no message 2024-12-13 15:49:30 +08:00
kuaifan
0d6fd903f1 perf: 支持更多办公文件格式 2024-12-13 09:47:52 +08:00
kuaifan
bcc74dd927 perf: 请假或外出时取消打卡提醒 2024-12-13 01:00:31 +08:00
kuaifan
dd0720afa7 no message 2024-12-13 00:27:41 +08:00
kuaifan
a06a4095b6 no message 2024-12-12 23:10:33 +08:00
kuaifan
29bc009c07 perf: 图片容错处理 2024-12-12 22:36:25 +08:00
kuaifan
520d2a0e20 no message 2024-12-12 22:01:01 +08:00
kuaifan
dbeb9dd561 perf: 优化全局监听事件 2024-12-12 14:14:51 +08:00
kuaifan
5b02d8008f perf: 优化数据流消息 2024-12-12 13:45:48 +08:00
kuaifan
a032c6114f no message 2024-12-12 12:55:29 +08:00
kuaifan
69ec4966d5 build 2024-12-12 00:44:59 +08:00
kuaifan
87fab80ea3 no message 2024-12-12 00:43:08 +08:00
kuaifan
bd2dabe851 no message 2024-12-12 00:30:25 +08:00
kuaifan
4a45d69e5b no message 2024-12-11 23:26:22 +08:00
kuaifan
e15bea9342 fix: 修复使用AI创建任务顺序错误的问题 2024-12-11 22:36:57 +08:00
kuaifan
7132413837 no message 2024-12-11 22:17:51 +08:00
kuaifan
c51116acaa build 2024-12-11 00:26:15 +08:00
kuaifan
002776f15e no message 2024-12-10 23:57:58 +08:00
kuaifan
c7f5c62e71 no message 2024-12-10 23:41:31 +08:00
kuaifan
3c57cf8d81 no message 2024-12-10 23:27:27 +08:00
kuaifan
f29bf3640a no message 2024-12-10 20:43:32 +08:00
kuaifan
07663dea6c no message 2024-12-10 15:46:41 +08:00
kuaifan
0ddb696e90 perf: 优化媒体播放 2024-12-10 15:20:39 +08:00
kuaifan
cc0a6d4706 no message 2024-12-10 13:08:28 +08:00
kuaifan
4c0ecc8f07 no message 2024-12-10 09:09:19 +08:00
kuaifan
d50c8ce691 perf: 优化临时会话的消息推送 2024-12-10 07:33:58 +08:00
kuaifan
8aa66661ac no message 2024-12-10 07:32:57 +08:00
kuaifan
00a9b3b57b perf: 优化任务时间显示 2024-12-10 07:32:47 +08:00
kuaifan
3896d08207 build 2024-12-09 07:39:56 +08:00
kuaifan
9b736c99f8 build 2024-12-09 07:35:10 +08:00
kuaifan
129d7e5850 no message 2024-12-09 00:21:54 +08:00
kuaifan
c2b26ffe6e no message 2024-12-09 00:16:00 +08:00
kuaifan
9b01e076f5 no message 2024-12-09 00:03:11 +08:00
kuaifan
88553872fc perf: upgrade office 2024-12-08 23:57:44 +08:00
kuaifan
2b8de4c028 Merge commit '646a5e3b28b5fcaddd8f8618685cab6fdd07eef7' into pro 2024-12-08 23:41:51 +08:00
kuaifan
24c5200a90 perf: 添加项目任务标签功能 2024-12-08 23:34:39 +08:00
kuaifan
bca0410a08 no message 2024-12-08 19:19:41 +08:00
kuaifan
42234be5cf perf: 添加项目任务标签功能 2024-12-08 18:56:01 +08:00
kuaifan
8e108e2d38 perf: 添加项目任务标签功能 2024-12-08 17:13:17 +08:00
kuaifan
248b0ce196 no message 2024-12-08 16:13:46 +08:00
kuaifan
d25ee3c234 no message 2024-12-07 19:58:43 +08:00
kuaifan
8ea1234596 no message 2024-12-07 12:53:39 +08:00
kuaifan
32530e5dc9 perf: 添加项目任务标签功能 2024-12-07 12:06:01 +08:00
kuaifan
952d060e2f no message 2024-12-07 11:55:28 +08:00
kuaifan
712f9e07b7 perf: Upgrade drawio 2024-12-07 01:31:49 +08:00
kuaifan
03cd6e79bb no message 2024-12-06 22:45:54 +08:00
kuaifan
cbd9e8a33c perf: 优化AI创建任务 2024-12-06 19:49:45 +08:00
kuaifan
13222fbe9a perf: 优化已读数据 2024-12-06 17:57:38 +08:00
kuaifan
4b89eb88bd perf: 优化AI群聊 2024-12-06 16:03:42 +08:00
spylecym
646a5e3b28 fix: 修复官网-帮助中心图片替换 2024-12-06 15:39:09 +08:00
kuaifan
08153cd99b perf: 支持AI在项目群里创建任务 2024-12-06 07:16:22 +08:00
kuaifan
61ebbac333 perf: 优化AI上下文 2024-12-05 11:15:28 +08:00
kuaifan
d63c1f156f perf: 优化AI上下文 2024-12-05 07:12:51 +08:00
kuaifan
a4548e2cba fix: 可见非共享文件夹的情况 2024-12-05 00:21:47 +08:00
kuaifan
77a3f2027e perf: 优化客户端会议打开速度 2024-12-05 00:00:59 +08:00
kuaifan
ecf0c78993 no message 2024-12-04 13:57:33 +08:00
kuaifan
1a0c1e3306 no message 2024-12-04 13:42:54 +08:00
kuaifan
506207d3ba no message 2024-12-03 14:25:26 +08:00
kuaifan
76bf46c152 perf: 支持通过接口发送通知和模板消息 2024-12-03 14:25:04 +08:00
kuaifan
96c64fbb91 no message 2024-12-03 13:12:48 +08:00
kuaifan
7fedb7d275 perf: 优化仪表盘任务避免重复统计 2024-12-03 09:14:55 +08:00
kuaifan
c16f316200 no message 2024-12-03 08:32:18 +08:00
kuaifan
4c5c071b21 no message 2024-12-03 08:30:56 +08:00
kuaifan
df917001d3 perf: 支持自定义AI个人提示词 2024-12-03 08:28:13 +08:00
kuaifan
65e75f974d perf: 优化客户端媒体浏览器 2024-12-02 20:50:10 +08:00
kuaifan
8afc1db72f perf: 优化客户端媒体浏览器 2024-12-02 18:41:35 +08:00
kuaifan
71f13a0b50 perf: 支持自定义上传图片压缩质量 2024-12-02 10:40:55 +08:00
kuaifan
4f57b195a8 perf: 优化与离职账号聊天 2024-12-02 09:11:25 +08:00
kuaifan
aa1ea41c5d perf: 优化邮件通知 2024-12-02 08:46:40 +08:00
kuaifan
b45058de72 perf: 优化未设置优先级的显示 2024-12-02 08:40:44 +08:00
kuaifan
576ab9a268 no message 2024-12-02 08:35:45 +08:00
kuaifan
e3312c97a7 perf: 添加任务模板 2024-12-02 08:34:22 +08:00
kuaifan
6bafa0a6dd perf: 添加任务模板 2024-12-02 00:55:48 +08:00
kuaifan
153d26ffcd perf: 添加任务模板 2024-12-01 23:50:01 +08:00
kuaifan
74fecdd941 perf: 添加任务模板 2024-12-01 23:09:03 +08:00
kuaifan
902844e008 perf: 添加任务模板 2024-12-01 20:54:38 +08:00
kuaifan
e78d850138 perf: 添加任务模板 2024-12-01 20:06:43 +08:00
kuaifan
94cefe52dd perf: 项目可自定义任务归档时间 2024-12-01 12:48:12 +08:00
kuaifan
a011f82912 perf: 优化快速添加任务 2024-12-01 02:46:42 +08:00
kuaifan
a160b2a471 perf: 支持通过职位名称搜索成员 2024-12-01 01:52:29 +08:00
kuaifan
396144f3fb perf: 会话页面支持查看头像 2024-12-01 01:25:59 +08:00
kuaifan
ff0fadc0c1 fix: 审批导致图片显示错误 2024-12-01 01:18:08 +08:00
kuaifan
65ec3a10bf perf: 优化文件列表 2024-12-01 01:08:21 +08:00
kuaifan
01c721c7e0 perf: 更新桌面客户端框架 2024-12-01 00:48:06 +08:00
kuaifan
d9aadb4f30 perf: 优化主题变化逻辑 2024-11-30 23:51:13 +08:00
kuaifan
964611eba4 fix: win子窗口无法激活的情况 2024-11-30 23:41:42 +08:00
kuaifan
98d2627036 no message 2024-11-30 14:18:15 +08:00
kuaifan
ba64540743 perf: 更新 AI 支持更多模型和支持提示词 2024-11-30 13:32:10 +08:00
kuaifan
62c50bb4e6 perf: 更新 AI 支持更多模型和支持提示词 2024-11-30 12:01:29 +08:00
kuaifan
0d4b005f4e perf: 更新 AI 支持更多模型和支持提示词 2024-11-30 08:26:13 +08:00
kuaifan
61b1206091 Merge commit '3f5c85b434d334f3a60fa5cabd3c933c4fafa28d' into pro
# Conflicts:
#	resources/mobile
2024-11-25 19:15:12 +08:00
kuaifan
2d37faea1d no message 2024-11-25 16:28:42 +08:00
kuaifan
c0a0f34ff4 build 2024-11-25 04:00:32 +08:00
kuaifan
e983677e57 no message 2024-11-25 03:29:11 +08:00
kuaifan
a813809fc6 build 2024-11-23 09:06:19 +08:00
kuaifan
f28b99b516 fix: 修复退出群组不完全的问题 2024-11-23 09:03:35 +08:00
kuaifan
bab37530e4 perf: 优化会话成员列表查询 2024-11-23 00:11:24 +08:00
kuaifan
fb24c63e7f perf: 重复添加任务的情况 2024-11-22 23:35:10 +08:00
kuaifan
65b8e2270e perf: 重复添加任务列表的情况 2024-11-22 23:31:02 +08:00
kuaifan
bfb2db8a3f no message 2024-11-22 23:23:21 +08:00
kuaifan
fe8deb98a2 perf: 优化消息样式 2024-11-22 23:11:27 +08:00
kuaifan
cee2458370 no message 2024-11-22 17:43:00 +08:00
kuaifan
764bf6dd55 fix: 修复退出还能收到推送的情况 2024-11-22 17:23:54 +08:00
kuaifan
88aee1e3bf fix: 修复账号被禁用之后还能收到推送和邮件 2024-11-22 16:50:13 +08:00
weifs
3f5c85b434 perf: 优化表情滚动条 2024-11-22 09:08:30 +08:00
weifashi
d34bff28c5 perf: 优化表情滚动条 2024-11-21 23:56:22 +08:00
kuaifan
bcc7d6d35c build 2024-11-21 21:21:21 +08:00
kuaifan
f3f0dec87b no message 2024-11-21 21:11:35 +08:00
kuaifan
bf4c4df939 fix: 任务首次聊天发表情失败的情况 2024-11-21 21:02:36 +08:00
kuaifan
51efb07c17 perf: 优化websocket消息 2024-11-21 20:02:02 +08:00
kuaifan
20e13ee9eb no message 2024-11-21 17:10:46 +08:00
kuaifan
b1b4ef926f perf: 优化快捷选择 2024-11-21 16:02:53 +08:00
kuaifan
43e6b4dc2f perf: 延期任务支持快选时间 2024-11-21 15:24:48 +08:00
kuaifan
906d87f43f no message 2024-11-21 09:30:08 +08:00
weifashi
8f622dd6a5 Merge branch 'pro' into o-pro 2024-11-21 00:52:22 +08:00
kuaifan
ec8d48292e perf: 优化消息阅读机制 2024-11-20 20:44:53 +08:00
kuaifan
1882a7baba build 2024-11-20 16:59:23 +08:00
kuaifan
d4cccbeb09 fix: AI聊天缺少最后一句话的情况 2024-11-20 16:57:35 +08:00
kuaifan
1f187ba8fb fix: 文件打包下载 2024-11-20 16:35:11 +08:00
kuaifan
9c4ff466a4 perf: 新增文件打包下载权限设置 2024-11-20 16:17:30 +08:00
kuaifan
e5c3cf6adb perf: 升级electron框架 2024-11-20 15:42:37 +08:00
kuaifan
02fd214b33 perf: 优化深色主题 2024-11-20 15:35:48 +08:00
kuaifan
7fbd3bc760 perf: 优化深色主题下调整浏览器窗口显示白边的情况 2024-11-20 11:59:42 +08:00
kuaifan
1ddb88a3a6 perf: 优化表情包资源 2024-11-20 09:03:43 +08:00
kuaifan
6edd4451c5 no message 2024-11-19 21:33:03 +08:00
kuaifan
1e83c7442a perf: 优化客户端子窗口 2024-11-19 20:32:04 +08:00
kuaifan
91533c5cac perf: 优化项目列表 2024-11-19 19:45:14 +08:00
kuaifan
a2ee1135dd perf: 优化录制语音消息 2024-11-19 18:39:52 +08:00
kuaifan
0ec255ed60 perf: 优化任务内容 2024-11-19 18:39:24 +08:00
kuaifan
a19d11061f perf: 优化任务提交添加继续 2024-11-19 11:53:04 +08:00
kuaifan
a730c95492 perf: 移动端审批窗口点击人员头像直接进入会话 2024-11-19 11:35:10 +08:00
kuaifan
89a50fd389 no message 2024-11-19 11:22:00 +08:00
kuaifan
82a340d576 perf: 新增系统别名设置 2024-11-19 11:19:37 +08:00
kuaifan
c952da669c no message 2024-11-19 10:09:19 +08:00
kuaifan
24cec7016a build 2024-11-19 01:42:09 +08:00
kuaifan
2c407bea78 no message 2024-11-18 23:50:20 +08:00
kuaifan
fba98db7cb fix: 任务内容保存后图片消失的情况 2024-11-18 23:25:42 +08:00
kuaifan
00eb8f7b01 fix: 修复上传超大尺寸图片 2024-11-18 22:36:23 +08:00
kuaifan
1055daa0e3 perf: 优化窗口加载速度 2024-11-18 21:56:04 +08:00
kuaifan
928145214d perf: 优化窗口加载速度 2024-11-18 21:33:23 +08:00
kuaifan
56e52a7dfd perf: 优化国际化 2024-11-18 17:15:01 +08:00
kuaifan
479d3e3f39 no message 2024-11-18 16:59:54 +08:00
kuaifan
ad3e773f27 perf: 优化图片上传 2024-11-18 15:09:22 +08:00
kuaifan
42c77db1d4 perf: 优化用户在线状态 2024-11-18 13:33:32 +08:00
kuaifan
11ea2d3697 perf: 优化小屏幕登录页 2024-11-18 12:15:00 +08:00
kuaifan
d0b54ab27c perf: 优化本地资源 2024-11-18 11:35:38 +08:00
kuaifan
5b9b6ed966 perf: 优化本地资源 2024-11-18 10:50:06 +08:00
kuaifan
fc192891b7 build 2024-11-18 00:59:44 +08:00
kuaifan
14f54e9df4 perf: 优化iOS上传图片颠倒的问题 2024-11-17 23:59:55 +08:00
kuaifan
07a290dbf9 perf: 优化桌面端通知图标 2024-11-17 23:35:31 +08:00
kuaifan
694f9a37a5 perf: 优化资源预取 2024-11-17 17:14:26 +08:00
kuaifan
13e58c63f4 perf: 优化emoji表情回复的判断 2024-11-17 13:22:50 +08:00
kuaifan
67c79bf565 perf: 更新office组件 2024-11-17 11:25:55 +08:00
kuaifan
428b72ef3d perf: 优化审批功能 2024-11-17 10:56:50 +08:00
kuaifan
b78d92b387 perf: 优化客户端升级 2024-11-16 11:16:16 +08:00
kuaifan
a09f2038ee perf: 优化客户端升级 2024-11-15 11:58:47 +08:00
kuaifan
fbb74e09e8 perf: 优化客户端升级 2024-11-15 09:59:41 +08:00
kuaifan
ca1028921a perf: 优化客户端升级 2024-11-15 00:23:11 +08:00
kuaifan
0fd37e4c05 build 2024-11-14 22:04:18 +08:00
kuaifan
f3d9e3376e perf: 优化客户端升级 2024-11-14 21:46:52 +08:00
kuaifan
9296008ecc perf: 优化客户端升级 2024-11-14 20:09:27 +08:00
kuaifan
ee7a1bd99c perf: 优化客户端升级 2024-11-14 17:33:54 +08:00
kuaifan
21eab03684 perf: 优化客户端升级 2024-11-14 16:34:48 +08:00
kuaifan
da066e40ce perf: 优化客户端升级 2024-11-14 14:19:50 +08:00
kuaifan
a219b7b6ee perf: 优化客户端升级 2024-11-14 13:55:01 +08:00
kuaifan
85c4ed6399 perf: 优化客户端升级 2024-11-14 13:03:16 +08:00
kuaifan
fa42194d15 perf: 优化客户端升级 2024-11-14 12:53:18 +08:00
kuaifan
e574e728d4 perf: 优化客户端升级 2024-11-14 11:40:38 +08:00
kuaifan
2ca35e4458 perf: 优化客户端升级 2024-11-14 00:33:44 +08:00
kuaifan
99027858d9 perf: 优化客户端 2024-11-13 01:07:23 +08:00
kuaifan
e7fcb47e81 perf: 优化签到错误提示 2024-11-12 21:16:12 +08:00
kuaifan
02d6dcd592 perf: 优化图片选择器 2024-11-12 21:15:49 +08:00
kuaifan
6e0a575da9 no message 2024-11-12 20:05:29 +08:00
kuaifan
93387c289e no message 2024-11-12 19:52:19 +08:00
kuaifan
1227a05e2d perf: 优化邮件通知 2024-11-12 19:52:12 +08:00
kuaifan
9f00047fdd perf: 优化本地资源 2024-11-12 12:34:45 +08:00
kuaifan
9bc3e56c79 perf: 修复iOS下载中文名乱码的问题 2024-11-12 10:42:40 +08:00
kuaifan
508aaef303 build 2024-11-12 00:22:45 +08:00
kuaifan
efd44a5da1 no message 2024-11-11 23:37:19 +08:00
kuaifan
0c70613865 perf: 优化初始化数据 2024-11-11 23:31:20 +08:00
kuaifan
6fda0bd548 no message 2024-11-11 17:49:46 +08:00
kuaifan
77224c3726 perf: 优化一处定位签到的问题 2024-11-11 17:11:54 +08:00
kuaifan
f25d72e4f5 perf: 升级海豚表情包 2024-11-11 17:11:08 +08:00
kuaifan
34603ff96e fix: iOS16-无法打开定位签到的问题 2024-11-11 12:11:01 +08:00
kuaifan
812232b945 perf: 优化新窗口链接打开逻辑 2024-11-11 09:50:15 +08:00
kuaifan
bd7228a378 perf: 添加会议机器人快捷菜单 2024-11-11 09:49:53 +08:00
kuaifan
ab61715973 build 2024-11-11 00:53:13 +08:00
kuaifan
095f461cfd perf: 优化使用默认浏览器打开链接 2024-11-11 00:06:19 +08:00
kuaifan
047771e6f8 no message 2024-11-10 23:09:07 +08:00
kuaifan
e2cec420fa no message 2024-11-10 14:11:48 +08:00
kuaifan
35e55b8677 perf: 添加定位签到 2024-11-10 13:11:30 +08:00
kuaifan
1b0ec71d93 perf: 优化打开会议 2024-11-10 12:20:53 +08:00
kuaifan
c6c735bbe8 perf: 优化打开会议 2024-11-10 11:00:56 +08:00
kuaifan
d5bc7d4051 perf: 优化打开会话逻辑 2024-11-10 08:32:47 +08:00
kuaifan
74405f1a2a perf: 添加定位签到 2024-11-10 08:19:10 +08:00
kuaifan
016bc41180 no message 2024-11-09 09:47:22 +08:00
kuaifan
e5df3e6746 perf: 添加定位签到 2024-11-09 09:39:03 +08:00
kuaifan
13fb884387 perf: 添加定位签到 2024-11-09 08:35:14 +08:00
kuaifan
3b9c9872ca perf: 添加定位签到 2024-11-08 21:46:07 +08:00
kuaifan
2fc329a403 dev: 优化开发环境 2024-11-08 10:45:23 +08:00
kuaifan
8ca1ef3b50 fix: 翻译聊天内容参数错误 2024-11-08 10:45:09 +08:00
kuaifan
f7dd9f852f build 2024-11-07 23:10:15 +08:00
kuaifan
4a9ed730c6 no message 2024-11-07 23:07:37 +08:00
kuaifan
a023c0b8bf fix: 无法打开项目的情况 2024-11-07 22:26:21 +08:00
kuaifan
ff38be3187 no message 2024-11-07 20:58:04 +08:00
kuaifan
9ffb2de2c8 feat: 添加定位签到 2024-11-07 18:36:55 +08:00
kuaifan
dcd87f86f1 perf: 优化从审批点击头像发起会话 2024-11-07 09:06:13 +08:00
kuaifan
d149c16713 fix: 搜索特殊字符报错的情况 2024-11-07 08:53:47 +08:00
kuaifan
1d99022ca3 build 2024-11-07 01:55:28 +08:00
kuaifan
bc85da49e3 perf: 图片浏览 2024-11-07 01:44:42 +08:00
kuaifan
18e1240775 build 2024-11-06 23:29:07 +08:00
kuaifan
e149e276d5 perf: 优化会话搜索 2024-11-06 23:22:27 +08:00
kuaifan
02654c8327 perf: 优化国际化语言 2024-11-06 20:36:00 +08:00
kuaifan
dace1dd1f3 perf: 优化消息已读逻辑 2024-11-06 20:16:46 +08:00
kuaifan
c46fd080df perf: 优化国际化语言 2024-11-05 22:43:58 +08:00
kuaifan
ef2230a331 no message 2024-11-05 15:13:40 +08:00
kuaifan
2ecd0584aa build 2024-11-05 11:30:54 +08:00
kuaifan
65c398880b perf: 优化app新版本提示 2024-11-05 10:27:57 +08:00
kuaifan
5962a593da no message 2024-11-04 20:29:36 +08:00
kuaifan
67baddf7a8 perf: 优化文字头像 2024-11-04 20:11:18 +08:00
kuaifan
ceb4fc8292 perf: 优化修改任务load 2024-11-04 11:41:04 +08:00
kuaifan
c51135a4cc fix: 修复会话内加载待办为空的情况 2024-11-03 21:58:37 +08:00
kuaifan
b2b4f593ce build 2024-11-03 09:07:32 +08:00
kuaifan
a95504bbf1 perf: 优化预览消息 2024-11-03 08:23:41 +08:00
kuaifan
6ed0e14fe0 perf: 优化移动端输入法换行 2024-11-03 08:17:25 +08:00
kuaifan
257e69268b fix: 无法清理数据缓存的情况 2024-11-03 07:35:24 +08:00
kuaifan
7e951196bf no message 2024-11-02 11:00:52 +08:00
kuaifan
501872e8d2 perf: 审批消息预览图片 2024-11-02 10:50:20 +08:00
kuaifan
87e46ec5a5 perf: 删除冗余字段 2024-11-02 10:01:42 +08:00
kuaifan
ebe953cf63 perf: 优化索引 2024-11-02 09:44:47 +08:00
kuaifan
cbfcdbf836 perf: 优化国际化语言 2024-11-02 09:40:10 +08:00
kuaifan
bd15915648 perf: 优化预览消息 2024-11-02 08:21:29 +08:00
kuaifan
312acdab51 perf: 优化预览消息 2024-11-01 21:18:04 +08:00
kuaifan
4ba9cc88dd perf: 优化会话查询 2024-11-01 19:20:35 +08:00
kuaifan
239013a2cb perf: 优化国际化语言 2024-11-01 10:53:51 +08:00
kuaifan
85412ea4b7 no message 2024-10-31 23:29:28 +08:00
kuaifan
cfda858d87 fix: 目录拼错的情况 2024-10-31 23:07:13 +08:00
kuaifan
df8fdd56ba build 2024-10-31 20:44:13 +08:00
kuaifan
698d03f77e fix: 设置子任务时间主任务出现1970的情况 2024-10-31 19:42:41 +08:00
kuaifan
3e15a3341c no message 2024-10-31 19:26:02 +08:00
kuaifan
d8a25e75d7 perf: 优化国际化语言 2024-10-31 16:39:08 +08:00
kuaifan
42f69124aa fix: 消息溢出的情况 2024-10-30 20:28:31 +08:00
kuaifan
621726ab3b feat: 消息翻译支持切换语言 2024-10-30 20:22:35 +08:00
kuaifan
cce7523f45 perf: 审批支持点击头像进入私聊 2024-10-30 16:03:18 +08:00
kuaifan
5e6a62376a no message 2024-10-30 15:45:37 +08:00
kuaifan
b03fb9f1de perf: 优化删除临时文件 2024-10-30 15:41:33 +08:00
kuaifan
1a7591314f perf: 优化缩略图 2024-10-30 14:14:58 +08:00
kuaifan
b8852f821c perf: 优化缩略图 2024-10-30 13:57:38 +08:00
kuaifan
6ebca3befa no message 2024-10-30 12:53:14 +08:00
kuaifan
8db34c6ee6 perf: 优化缩略图 2024-10-30 12:50:35 +08:00
kuaifan
d799c06017 perf: 优化缩略图 2024-10-30 09:37:54 +08:00
kuaifan
50a7950ccd no message 2024-10-29 19:02:23 +08:00
kuaifan
a393dec0a0 perf: 优化缩略图 2024-10-29 19:02:11 +08:00
kuaifan
423aad4179 build 2024-10-28 22:57:17 +08:00
kuaifan
80d10051cf no message 2024-10-28 21:18:24 +08:00
kuaifan
b1776c82ad perf: 优化长按消息菜单显示逻辑 2024-10-28 21:18:24 +08:00
kuaifan
36f313380e no message 2024-10-28 21:18:24 +08:00
kuaifan
7df9c37850 perf: 优化会话全屏输入功能菜单固定下方 2024-10-28 21:18:24 +08:00
kuaifan
9001c51bea perf: 优化聊天输入时页面乱滚动的情况 2024-10-28 21:18:24 +08:00
kuaifan
99757fc947 no message 2024-10-28 21:18:24 +08:00
kuaifan
8e9ff1116a perf: 优化导出统计国际化 2024-10-28 21:18:24 +08:00
kuaifan
f1df4e07d2 no message 2024-10-28 21:18:24 +08:00
kuaifan
3e3799074a perf: 优化会话全屏输入功能菜单固定下方 2024-10-28 21:18:24 +08:00
kuaifan
ae0ee590e4 perf: 支持会员选择窗标题省略号点击查看全标题 2024-10-28 21:18:24 +08:00
kuaifan
988a9b0606 perf: 任务内容加载太久显示load 2024-10-28 21:18:24 +08:00
kuaifan
7a457e4364 perf: 任务日志显示子任务关联 2024-10-28 21:18:24 +08:00
kuaifan
2edbe4fb3f fix: 无法设置修改子任务时间的情况 2024-10-28 21:18:24 +08:00
kuaifan
8eaff830ad perf: 审批评论图片浏览可滑动连续查看 2024-10-28 21:18:24 +08:00
kuaifan
7fdc7a47e3 perf: 审批评论优化显示缩略图 2024-10-28 21:18:24 +08:00
kuaifan
0e821d1c84 perf: 任务变化通知加上任务标题 2024-10-28 21:18:24 +08:00
kuaifan
c23de08cf5 perf: 新任务提醒区分协助还是负责 2024-10-28 21:18:24 +08:00
kuaifan
a6acb7ea0d perf: 优化审批通知标题 2024-10-28 21:18:24 +08:00
kuaifan
0c64cf0546 faet: 新增文本消息长按翻译功能 2024-10-28 21:18:24 +08:00
kuaifan
a4a9ab8d2d perf: 优化审批通知标题 2024-10-28 21:18:23 +08:00
kuaifan
19a1ae9bec perf: 优化推送预览 2024-10-28 21:18:23 +08:00
kuaifan
36cb8290f4 perf: 优化md标题样式 2024-10-28 21:18:23 +08:00
kuaifan
244991e8e8 perf: 新增查看更新日志 2024-10-28 21:18:23 +08:00
gwok
d6a7c19cbf fix: 判断广告页逻辑错误 2024-10-28 21:13:30 +08:00
gwok
64906a827d fix: 下载页控制台报错处理 2024-10-28 21:05:07 +08:00
gwok
da53306a2c style: 推广页样式调整 2024-10-28 18:21:20 +08:00
kuaifan
48515d7caf build 2024-10-25 00:18:53 +08:00
kuaifan
1f6ef62499 perf: 优化国际化、优化显示 2024-10-24 23:42:20 +08:00
kuaifan
6b4b88aba7 no message 2024-10-24 12:31:51 +08:00
kuaifan
fadff146b4 build 2024-10-24 12:12:23 +08:00
kuaifan
01feacfe54 no message 2024-10-24 11:48:44 +08:00
kuaifan
d6ddc5ff88 perf: 优化人脸签到设置 2024-10-24 10:55:58 +08:00
kuaifan
287b6b396d perf: 优化消息搜索速度 2024-10-24 07:32:31 +08:00
kuaifan
b976f294f9 perf: 优化显示 2024-10-23 18:38:32 +08:00
kuaifan
dce48bd0cb fix: 周报默认内容已完成工作负责人不显示的情况 2024-10-23 16:31:53 +08:00
kuaifan
ab84235890 no message 2024-10-23 16:14:42 +08:00
kuaifan
7445ac3a39 perf: 优化图片压缩 2024-10-23 15:37:00 +08:00
kuaifan
f9ceb3e2d8 perf: 优化显示 2024-10-23 11:30:48 +08:00
kuaifan
8bb7b60055 perf: 优化显示 2024-10-23 10:56:02 +08:00
kuaifan
190211a467 no message 2024-10-23 00:06:20 +08:00
kuaifan
8a6868e811 perf: 优化显示 2024-10-22 22:37:12 +08:00
kuaifan
6aa868c8d8 fix: 无法清除计划时间 2024-10-22 21:52:21 +08:00
kuaifan
4dfa1c8efc fix: 选择时间起始不正确的问题 2024-10-22 21:32:58 +08:00
kuaifan
e2e7bc8d72 fix: 修复iOS日历无法正常显示的情况 2024-10-22 20:39:09 +08:00
kuaifan
a97d78bbf4 no message 2024-10-22 20:12:58 +08:00
kuaifan
22dbd288df perf: 优化cmd命令 2024-10-22 14:54:57 +08:00
kuaifan
4685cdcd3c fix: 签到信息预览错误 2024-10-22 14:51:02 +08:00
kuaifan
f792b3d983 build 2024-10-22 12:45:29 +08:00
kuaifan
adc94cef90 no message 2024-10-22 11:27:30 +08:00
kuaifan
e639cfbc2f perf: 优化显示效果 2024-10-22 11:27:24 +08:00
kuaifan
e520cd9020 build 2024-10-22 00:23:13 +08:00
kuaifan
daf8d15f45 perf: 升级onlyoffice 2024-10-21 22:40:50 +08:00
kuaifan
0e473ceacc no message 2024-10-21 20:19:04 +08:00
kuaifan
873bd0ed88 fix: 推送失败的情况 2024-10-21 19:05:24 +08:00
kuaifan
58b7853d63 perf: 优化人脸签到功能 2024-10-21 18:03:23 +08:00
kuaifan
2284788366 Merge commit '814a488801b328daf67f86c33ac422704303dceb' into pro
# Conflicts:
#	app/Http/Controllers/Api/SystemController.php
#	public/site/en/about.html
#	public/site/en/download.html
#	public/site/en/help.html
#	public/site/en/index.html
#	public/site/en/log.html
#	public/site/en/price.html
#	public/site/en/product.html
#	public/site/en/solutions.html
#	public/site/zh/about.html
#	public/site/zh/download.html
#	public/site/zh/help.html
#	public/site/zh/index.html
#	public/site/zh/log.html
#	public/site/zh/price.html
#	public/site/zh/product.html
#	public/site/zh/solutions.html
#	resources/mobile
2024-10-21 14:14:45 +08:00
kuaifan
d1766e52b6 Merge commit 'e3f5fb323ad00b6804acf265eec1fc5040d05a81' into pro 2024-10-21 13:33:44 +08:00
kuaifan
fdd5e36d19 no message 2024-10-21 12:53:03 +08:00
kuaifan
4fe4dc8c6e pref: 优化加载通讯录数量 2024-10-20 19:53:11 +08:00
kuaifan
a3202cbead no message 2024-10-20 18:39:18 +08:00
kuaifan
e8b03ae565 fix: 导出签到数据快速选择时间 2024-10-20 01:53:52 +08:00
kuaifan
829e3982d2 no message 2024-10-20 01:53:20 +08:00
kuaifan
07c5f586b0 no message 2024-10-19 23:49:28 +08:00
kuaifan
2ebaeb3453 no message 2024-10-19 22:13:44 +08:00
kuaifan
5660be12f6 no message 2024-10-19 17:15:19 +08:00
kuaifan
3cd00e1343 no message 2024-10-19 11:09:17 +08:00
kuaifan
f983146501 fix: 搜索区域无法回车搜索的问题 2024-10-18 22:10:48 +08:00
kuaifan
6cf64ce538 perf: 优化继续添加任务数据处理 2024-10-18 22:01:20 +08:00
kuaifan
47a7876505 fix: 未领任务提醒机器人无须加入项目 2024-10-18 22:00:15 +08:00
kuaifan
3f5ac55753 perf: 优化翻译 2024-10-18 21:58:55 +08:00
kuaifan
a33d95f2c1 perf: 更新gpt的一些模型 2024-10-18 15:44:14 +08:00
kuaifan
1128db184e no message 2024-10-17 14:03:51 +08:00
kuaifan
153fd6c569 perf: 优化消息组件 2024-10-17 13:06:55 +08:00
kuaifan
c9d002c1cd perf: 优化消息组件 2024-10-16 18:40:53 +08:00
kuaifan
e0a108eb2e perf: 优化消息组件 2024-10-16 14:49:33 +08:00
kuaifan
ae587950b9 perf: 优化消息组件 2024-10-15 21:23:42 +08:00
kuaifan
e956a03098 no message 2024-10-14 16:58:40 +08:00
kuaifan
1702aab538 no message 2024-10-14 16:24:47 +08:00
kuaifan
3c67b49d08 perf: 优化后端翻译 2024-10-11 16:49:45 +08:00
kuaifan
d58246b255 perf: 优化创建任务提示时间冲突的逻辑 2024-10-11 16:03:07 +08:00
yijixx
814a488801 docs: 更新docker-compose 2024-10-11 14:04:42 +08:00
yijixx
e029b39eb9 perf: 人脸打卡配置 2024-10-11 11:33:01 +08:00
yijixx
a8361299c7 perf: 签到设置保存 2024-10-11 11:14:13 +08:00
gwok
e3f5fb323a fix:修复导出签到数据中这个月和上个月时间显示不准确的问题 2024-10-08 11:24:50 +08:00
yijixx
be262c3a69 perf: 签到设备显示 2024-10-08 09:32:52 +08:00
kuaifan
a4525d4519 fix: 日历中总是显示时间相差一个月 2024-10-03 09:27:58 +08:00
yijixx
4f6034457f perf: 打卡标签页 2024-09-30 18:34:42 +08:00
yijixx
5413457b6b feat: 支持人脸打卡设备 2024-09-29 17:31:15 +08:00
gwokwong
977cf61b50 feat:推广页点击联系我们展示企业微信二维码 2024-09-24 17:12:26 +08:00
gwokwong
8c8c5b04d5 feat:新增推广页 2024-09-24 12:54:27 +08:00
kuaifan
620465d62a no message 2024-09-23 09:48:08 +03:00
kuaifan
a80e0d4c45 build 2024-09-23 01:37:55 +03:00
kuaifan
0ab6e6ca8d perf: 优化翻译 2024-09-23 01:34:06 +03:00
kuaifan
dcd41b4be2 perf: 优化时间组件 2024-09-22 23:19:03 +03:00
kuaifan
33cd9358c0 perf: 优化时间组件 2024-09-22 17:27:54 +03:00
kuaifan
51a3ad25d1 perf: 优化时间组件 2024-09-22 15:39:02 +03:00
kuaifan
f586938fe9 perf: 优化时间组件 2024-09-22 15:21:54 +03:00
kuaifan
912d229bdd perf: 优化时间组件 2024-09-22 11:16:50 +03:00
kuaifan
a93345afbd perf: 优化时间组件 2024-09-22 00:43:48 +03:00
kuaifan
a7bd0e0dac perf: 优化时间组件 2024-09-20 21:46:28 +03:00
kuaifan
e2fd37fe24 perf: 优化日历样式 2024-09-19 23:22:57 +03:00
kuaifan
6e6397fc91 no message 2024-09-19 15:55:55 +08:00
kuaifan
45c20dbed9 fix: 修复表情回应一处报错 2024-09-19 06:58:17 +08:00
kuaifan
594c19da03 perf: 手机端消息菜单居中 2024-09-19 06:47:16 +08:00
kuaifan
9251ccbb12 perf: 优化数据库外部访问方式 2024-09-19 06:33:17 +08:00
kuaifan
34305a1285 fix: 任务到期时间不变颜色 2024-09-19 00:42:57 +08:00
kuaifan
ccc60dfd77 perf: 优化消息选择文本 2024-09-18 23:17:18 +08:00
kuaifan
b7da689955 fix: 聊天提及内容错位的情况 2024-09-18 18:56:14 +08:00
kuaifan
0598a36b19 perf: 优化表情搜索 2024-09-18 18:55:17 +08:00
kuaifan
947e106f19 fix: 首次修改任务时间不提示时间冲突的问题 2024-09-18 17:38:14 +08:00
kuaifan
81957c9396 fix: 添加任务时不设置时间无须提示任务冲突 2024-09-18 17:13:24 +08:00
kuaifan
d54c86cec9 perf: 任务描述再次点击隐藏菜单 2024-09-18 13:24:24 +08:00
kuaifan
c17eca28fa fix: 负责人修改后不显示在仪表盘的情况 2024-09-17 00:02:26 +08:00
kuaifan
9a69f3b019 no message 2024-09-16 23:31:19 +08:00
kuaifan
c39fc80c02 fix: 添加任务选择今天时间无效的情况 2024-09-16 10:09:42 +03:00
kuaifan
b0eead121a perf: 优化工作包括模板 2024-09-16 09:43:41 +03:00
kuaifan
511b98ab5b perf: 仪表盘任务列表支持折叠 2024-09-16 08:51:26 +03:00
weifs
a69b01ecf5 fix: 修复url-token登录异常问题 2024-09-13 14:01:27 +08:00
kuaifan
a967493d77 perf: 新增修改任务时间权限 2024-09-13 06:41:20 +03:00
kuaifan
050c9702d8 perf: 优化子任务读取失败 2024-09-13 05:40:44 +03:00
spylecym
0d23b973de feat: 官网侧边导航按钮新增谷歌分析事件追踪 2024-09-06 16:44:16 +08:00
spylecym
fc3170369b feat: 官网侧边导航按钮新增谷歌分析事件追踪 2024-09-06 10:35:00 +08:00
kuaifan
647f7fdc7d build 2024-09-01 19:55:57 +08:00
kuaifan
8c3cd379a2 no message 2024-09-01 19:52:41 +08:00
wfs
cf9051412a fix: 修复任务可见性为非项目人员时项目负责人不可见的bug 2024-09-01 15:53:21 +08:00
wfs
6db0ff5647 Merge branch 'pro2' into pro
# Conflicts:
#	public/site/js/googleAds.js
#	public/site/js/googleAnalyze.js
2024-09-01 15:00:49 +08:00
spylecym
9ce127df86 feat: 网页右下角导航改为点击显示以及手机端点击拨打电话直接拨号 2024-08-26 17:24:28 +08:00
spylecym
20eec62fde feat: 网页右下角导航改为点击显示以及手机端点击拨打电话直接拨号 2024-08-22 11:30:30 +08:00
wfs
effc8ce43f feat: 更改审批版本 2024-08-14 01:10:22 +08:00
wfs
ced25e0cd2 perf: 1.优化审批流程-审批人审核过后自动通过 2. 优化审批评论图片可以左右滑动查看 2024-08-14 00:24:49 +08:00
wfs
72c70fe494 Merge branch 'pro2' into pro
# Conflicts:
#	public/site/en/about.html
#	public/site/en/cookie.html
#	public/site/en/download.html
#	public/site/en/help.html
#	public/site/en/index.html
#	public/site/en/log.html
#	public/site/en/price.html
#	public/site/en/privacy.html
#	public/site/en/product.html
#	public/site/en/solutions.html
#	public/site/zh/about.html
#	public/site/zh/cookie.html
#	public/site/zh/download.html
#	public/site/zh/help.html
#	public/site/zh/index.html
#	public/site/zh/log.html
#	public/site/zh/price.html
#	public/site/zh/privacy.html
#	public/site/zh/product.html
#	public/site/zh/solutions.html
#	resources/mobile
2024-08-13 21:39:57 +08:00
spylecym
dc062a44e1 fix: 修改谷歌分析以及谷歌推广文件命名 2024-08-13 15:06:45 +08:00
spylecym
dff22272b5 fix: 修改谷歌分析代码 2024-08-13 14:18:59 +08:00
spylecym
a0a1e03b53 fix: 修改谷歌分析代码 2024-08-13 11:51:47 +08:00
spylecym
3915c065fe feat: 官网新增谷歌分析代码 2024-08-12 10:06:44 +08:00
spylecym
dc71a779e0 fix: 删除打印 2024-08-12 10:06:44 +08:00
spylecym
b56ba93634 feat: 页面新增谷歌分析 2024-08-12 10:06:44 +08:00
spylecym
2926472f7d fix: 修改关于我们页面公司介绍文案 2024-08-12 10:06:44 +08:00
spylecym
a253d42f10 feat: 官网新增谷歌分析代码 2024-08-12 09:12:30 +08:00
spylecym
700d566255 fix: 删除打印 2024-08-09 16:42:26 +08:00
spylecym
fbbace90aa feat: 页面新增谷歌分析 2024-08-09 16:41:27 +08:00
kuaifan
6b5f7e780c build 2024-08-06 21:16:27 +08:00
kuaifan
79d4932bee perf: 优化聊天视频预览 2024-08-06 18:45:08 +08:00
kuaifan
e8af0f2ea6 fix: 无法下载大文件 2024-08-06 18:24:49 +08:00
kuaifan
f1ecf33ce7 perf: 优化打包下载 2024-08-06 13:23:28 +08:00
kuaifan
18eaf56ff9 perf: 支持上传mov、webm视频 2024-08-06 13:19:25 +08:00
spylecym
75f15ccc96 fix: 修改关于我们页面公司介绍文案 2024-06-13 16:51:27 +08:00
weifs
3f17e91f72 Merge branch 'pro' of github.com:hitosea/dootask into pro
# Conflicts:
#	resources/mobile
2024-06-06 09:37:01 +08:00
kuaifan
ee6eddf308 build 2024-06-04 22:12:51 +08:00
kuaifan
da84f15e9f no message 2024-06-04 21:08:10 +08:00
kuaifan
62f4d43bd9 perf: 通讯录菜单添加会议 2024-06-04 20:19:14 +08:00
kuaifan
376120b6d0 perf: 优化文件里预览图片 2024-06-04 20:11:48 +08:00
kuaifan
ff872c7dce perf: 优化消息描述 2024-06-04 19:50:12 +08:00
kuaifan
a834481d32 fix: 切换对话之后无法通过右键@ 2024-06-04 19:33:38 +08:00
kuaifan
4c5d3bd43e build 2024-06-03 19:39:37 +08:00
weifs
b737b841f5 Merge branch 'kuaifan-pro' into pro 2024-06-03 19:30:25 +08:00
weifs
0c5500edd4 Merge branch 'kuaifan-pro' into pro 2024-06-03 15:18:06 +08:00
weifs
990a40e4e4 feat: 操作人员离职对okr的移交处理 2024-06-03 15:14:19 +08:00
kuaifan
5eb2124b06 build 2024-06-03 14:07:14 +08:00
kuaifan
20d8180347 Merge commit '008040d96c73051fca6db388b35077a0ca7b5b5b' into pro 2024-06-03 14:00:10 +08:00
kuaifan
49203c15a7 perf: 优化视频播放 2024-06-03 11:32:12 +08:00
weifs
008040d96c fix: 如果项目没有流程,无法选择移动后的状态,也没办法确定移动 2024-06-03 11:13:01 +08:00
kuaifan
1a36044de2 no message 2024-06-02 10:56:18 +08:00
kuaifan
4886edc684 perf: 新增通讯录菜单 2024-06-02 10:52:59 +08:00
kuaifan
617fe902a4 perf: 优化快捷创建群组 2024-06-02 10:41:33 +08:00
kuaifan
78ad3468ae build 2024-06-01 17:34:55 +08:00
kuaifan
b58de926b2 no message 2024-06-01 17:24:07 +08:00
kuaifan
7c4c7eea9c perf: 优化回复消息自动@逻辑 2024-06-01 14:43:59 +08:00
kuaifan
eb7d93af87 perf: 甘特图兼容移动端 2024-06-01 10:19:35 +08:00
kuaifan
956b68a545 no message 2024-06-01 08:35:42 +08:00
kuaifan
79065a7675 build 2024-05-31 20:14:06 +08:00
kuaifan
d3514a0334 no message 2024-05-31 18:14:51 +08:00
kuaifan
bb163605af perf: 团队管理选择离职时根据离职时间排序 2024-05-31 13:44:51 +08:00
kuaifan
afcbd6af92 perf: 移动端支持快速编辑描述选择 2024-05-31 13:24:43 +08:00
kuaifan
13edea3449 perf: 优化离职数据移交 2024-05-31 12:38:37 +08:00
kuaifan
6cdcd4e0dc no message 2024-05-30 23:10:02 +08:00
kuaifan
efce884494 perf: 优化举报功能 2024-05-30 22:56:51 +08:00
kuaifan
dcffeded9a fix: 更新可见性后仍存在对话会话列表 2024-05-30 22:10:52 +08:00
kuaifan
e24b6806da perf: 优化网络重连后会话数据逻辑 2024-05-30 19:02:58 +08:00
kuaifan
bc7874a3a0 perf: 优化甘特图移动端交互 2024-05-30 18:27:54 +08:00
kuaifan
d348871b0c perf: 优化md编辑器 2024-05-30 14:50:13 +08:00
kuaifan
f9ee740a8c Merge commit '58ca285edf93b744a6eb358610b73f7e385aa5cf' into pro 2024-05-29 16:02:24 +08:00
kuaifan
c0a90ae89d fix: 消息输入框回复冲突 2024-05-21 15:13:25 +08:00
weifs
58ca285edf feat: 更新approve容器 2024-05-21 11:58:46 +08:00
weifs
325f8c0f7e Merge branch 'kuaifan-pro' into pro 2024-05-21 11:24:52 +08:00
zzw
829fe7e4ba
fix:调整智谱清言描述信息 2024-05-15 14:54:54 +08:00
zzw
a6a18a0ee4
feat:添加智谱清言机器人 2024-05-15 14:22:17 +08:00
kuaifan
eda9eb08d5 no message 2024-05-12 13:16:39 +09:00
kuaifan
4625ae7548 build 2024-05-12 00:18:34 +09:00
kuaifan
186d3b0d79 perf: 优化查看任务附件菜单 2024-05-11 23:50:28 +09:00
kuaifan
7bf5805714 perf: 独立窗口未激活阅读逻辑 2024-05-11 23:09:33 +09:00
kuaifan
19604c46f0 no message 2024-05-11 22:49:47 +09:00
kuaifan
a77a32d64e perf: 新消息在会话列表时间与消息里不一致 2024-05-11 22:13:37 +09:00
kuaifan
53145f0ca2 no message 2024-05-11 12:35:52 +09:00
kuaifan
6880baa6a4 perf: 优化Android点击发送按钮效果 2024-05-11 12:34:27 +09:00
kuaifan
2bda6bf668 perf: 支持修改消息待办 2024-05-11 10:53:32 +09:00
kuaifan
80fe978454 no message 2024-05-11 09:13:26 +09:00
kuaifan
94a30ea940 perf: 延期任务时间支持按天 2024-05-11 00:40:59 +09:00
kuaifan
f9d1aa93c4 perf: 优化待办消息样式 2024-05-10 23:52:41 +09:00
kuaifan
d3bda0d869 perf: 优化移动端子任务列表显示 2024-05-10 23:26:12 +09:00
kuaifan
426fa63288 perf: 语音消息转文字 2024-05-10 22:23:45 +09:00
kuaifan
bf46a00937 perf: 更新语音消息插件 2024-05-10 11:53:14 +09:00
kuaifan
0fce0c2386 perf: 优化设置样式 2024-05-09 16:08:44 +09:00
kuaifan
77843ccdee build 2024-05-02 00:55:18 +08:00
kuaifan
b86edcfa96 build 2024-05-01 23:22:56 +08:00
kuaifan
5fb242024a Merge commit 'd7d8ee481e720624225511e102773d1f2fc68e41' into pro 2024-05-01 15:55:13 +08:00
kuaifan
abbfbb85e6 no message 2024-05-01 15:20:15 +08:00
kuaifan
37407cdbac perf: 优化转发消息数据显示 2024-05-01 15:17:58 +08:00
kuaifan
f1f96bda4e build 2024-05-01 14:18:57 +08:00
kuaifan
6f38c4efdd feat: 语音消息未阅读红点提示 2024-05-01 12:02:12 +08:00
kuaifan
e325698899 perf: ipad 发送消息后出现页面跳动的情况 2024-05-01 10:52:52 +08:00
kuaifan
ed36d622ec perf: 仪表盘隐藏未到开始时间的任务 2024-04-30 16:46:04 +08:00
kuaifan
dabe1376c3 perf: 优化查看任务修改历史 2024-04-30 16:39:45 +08:00
kuaifan
199fd4462e perf: 优化聊天工具栏样式 2024-04-30 14:50:57 +08:00
kuaifan
85a7776159 perf: 优化更新聊天中的待办 2024-04-30 13:37:47 +08:00
weifs
d7d8ee481e feat: 修复移动任务中选完成进行移动没有设置完成时间的bug 2024-04-29 20:59:24 +08:00
Pang
875da9fbe5 perf: 优化图标功能提示 2024-04-28 23:02:40 +08:00
spylecym
2bd8199d88 fix: 修复官网帮助中心英文页面头部导航缺失问题 2024-04-28 18:19:21 +08:00
spylecym
ca490f3e96 feat: 新增右侧底部导航 2024-04-28 18:14:35 +08:00
Pang
b81f2f0675 fix: 安装系统部分情况没有数据 2024-04-26 19:46:27 +08:00
weifs
aef23dda13 fix: 修复举报样式错乱 2024-04-26 09:59:18 +08:00
weifs
693fa46688 perf: 审批和任务通知优化 2024-04-25 11:24:30 +08:00
weifs
30676fb761 feat: 添加举报功能 2024-04-24 19:22:36 +08:00
weifs
ac6bdc07ec perf: 优化按钮没有对应类型,控制台报错 2024-04-24 11:34:31 +08:00
kuaifan
f6afdd6604 build 2024-04-23 16:28:14 +08:00
kuaifan
856037c3c9 fix: 任务描述保存图片失败 2024-04-23 16:21:01 +08:00
kuaifan
3203da411d 优化android自动编译 2024-04-23 15:57:44 +08:00
kuaifan
a6708a26a6 perf: 转发消息至群聊时支持@留言 2024-04-23 11:04:43 +08:00
kuaifan
053daa621b perf: 自动发布Android 2024-04-22 18:30:08 +08:00
Pang
a16f5fca07 build 2024-04-22 10:23:43 +08:00
Pang
cfdb6e2a93 fix: 上一版本导致的无法@ 2024-04-22 10:16:37 +08:00
Pang
73261da19b perf: 优化代码 2024-04-22 10:00:26 +08:00
Pang
71f48a4f7c perf: 优化查看文件历史 2024-04-22 09:39:19 +08:00
Pang
dbdb805269 perf: 支持查看任务描述修改历史 2024-04-22 09:38:39 +08:00
Pang
bd61b8c948 perf: 任务描述支持清单 2024-04-22 05:05:07 +08:00
kuaifan
5e6a21ddc5 docs 2024-04-20 21:04:08 +08:00
kuaifan
ccc8170ec7 build 2024-04-20 21:02:10 +08:00
kuaifan
d4bfbb81d8 fix: 修复关闭侧边回复窗口导致会话不正常的情况 2024-04-20 19:38:18 +08:00
kuaifan
a46ffa1089 perf: 消息内容支持待办列表 2024-04-20 19:13:38 +08:00
kuaifan
182e5a6974 perf: 优化自动识别发送消息类型 2024-04-20 16:04:51 +08:00
kuaifan
8ca021df6a perf: 聊天输入框粘贴格式优化 2024-04-20 16:04:22 +08:00
kuaifan
106c011f6b perf: 优化网络错误提示 2024-04-19 15:39:04 +08:00
kuaifan
76664c61c4 build 2024-04-18 12:21:32 +08:00
kuaifan
24839f960f fix: 解决 Unable to preventDefault inside passive event listener 报错 2024-04-18 12:16:42 +08:00
kuaifan
ce7d3f8475 fix: 截图粘贴出现两张图的情况 2024-04-18 12:15:50 +08:00
kuaifan
5e8a6af74c fix: 聊天输入中文过程跟placeholder内容叠加的问题 2024-04-18 12:15:23 +08:00
kuaifan
23a363aeea build 2024-04-17 23:59:29 +08:00
kuaifan
6cbf2bbada perf: 下载pdf使用自带浏览器 2024-04-17 10:21:35 +08:00
kuaifan
ee9cf0a6b6 perf: 优化消息加载中效果 2024-04-17 09:32:53 +08:00
Pang
8d39b4aa0d perf: 审批内容禁止转发 2024-04-17 08:10:08 +08:00
Pang
3c93ad18b2 perf: 滑动快捷表情选择 2024-04-17 07:46:43 +08:00
kuaifan
cc125cc292 perf: 优化聊天输入框 2024-04-17 00:34:12 +08:00
kuaifan
6823d87198 build 2024-04-11 11:52:36 +08:00
kuaifan
288e857321 perf: update chat editor 2024-04-11 11:51:10 +08:00
kuaifan
73d1950d97 fix: added non-passive event listener to a scroll-blocking 'touchstart' event 2024-04-11 11:50:07 +08:00
kuaifan
ee2b047e5d no message 2024-04-11 10:16:00 +08:00
kuaifan
ae83fce524 perf: 优化机器人回复 2024-04-11 10:12:50 +08:00
kuaifan
985c5ff54b perf: 优化android体验 2024-04-11 08:48:55 +08:00
Pang
fe5f56e98b build 2024-04-09 23:35:22 +08:00
Pang
40ef700e5a perf: 优化使用默认浏览器打开规则 2024-04-09 20:46:56 +08:00
Pang
8661c28d10 build 2024-04-09 19:34:19 +08:00
Pang
9edddc461d perf: 优化聊天图片上传 2024-04-09 09:05:36 +08:00
kuaifan
2fbb640bc8 build 2024-04-08 17:00:39 +08:00
kuaifan
a03050bc7b perf: 临时帐号别名 2024-04-08 16:53:24 +08:00
kuaifan
654a90626e perf: tab icon load error 2024-04-08 14:06:45 +08:00
Pang
9acafed459 perf: 优化会议 2024-04-08 09:37:46 +08:00
Pang
b7dcb543f6 perf: 创建会议不需要加入机器人 2024-04-08 08:58:16 +08:00
Pang
e2768f7f20 perf: 暗黑模式下窗口背景色兼容问题 2024-04-08 08:31:51 +08:00
Pang
cda2d0da27 perf: 优化网络检查 2024-04-08 08:13:30 +08:00
Pang
c61815db3a perf: 客户端会议优化 2024-04-08 07:51:57 +08:00
Pang
9390965a0c no message 2024-04-06 11:58:45 +08:00
Pang
0688feefb1 build 2024-04-04 10:33:51 +08:00
Pang
93c8d86caf perf: 优化会议室 2024-04-04 10:31:12 +08:00
Pang
540bff89cf no message 2024-04-04 08:42:41 +08:00
Pang
41c09b3838 perf: 优化会议室 2024-04-04 07:40:57 +08:00
Pang
0a26361724 no message 2024-04-04 07:40:38 +08:00
kuaifan
ee9ad65e18 build 2024-04-03 10:32:00 +08:00
kuaifan
db6b571cfb fix: 部分情况出现注册失败 2024-04-03 10:14:41 +08:00
Pang
bfe359c440 perf: 优化数据读取机制 2024-04-03 07:53:44 +08:00
Pang
ee8f67793a fix: 最小化阅读窗口新建窗口不自动激活 2024-04-03 07:13:35 +08:00
Pang
629fe79c61 fix: 独立窗口不更新消息 2024-04-03 07:13:35 +08:00
Pang
9ae278d622 perf: 优化缓存规则 2024-04-03 07:13:35 +08:00
Pang
3417d68609 perf: 优化完成待办数据推送 2024-04-03 07:13:35 +08:00
weifs
f757749282 perf: 评论审批图片和投票深色按钮 2024-03-26 17:43:08 +08:00
kuaifan
ea40e95cae build 2024-03-26 14:19:02 +08:00
kuaifan
eb066f52fe perf: 优化任务日志内容 2024-03-26 13:52:24 +08:00
kuaifan
b7007135cb perf: 查看版本免请求接口 2024-03-26 13:28:00 +08:00
kuaifan
a7bd403b2c perf: 添加任务时选择任务位置内容溢出 2024-03-26 13:22:25 +08:00
Pang
59c7b148dd perf: 消息支持style 2024-03-26 10:17:49 +08:00
Pang
c67f52e960 perf: 回复消息列表隐藏顶部loading 2024-03-26 08:56:20 +08:00
Pang
f311625060 fix: 修改回复、转发消息后引用的部分消失 2024-03-26 08:51:40 +08:00
kuaifan
d3c08f8d90 perf: 支持FCM推送 2024-03-21 00:37:38 +09:00
kuaifan
2bc655d7ef perf: 设置华为推送自分类 2024-03-20 15:52:48 +09:00
kuaifan
d2b8d0372e perf(client): 优化添加任务可见性点击效果 2024-03-20 13:44:22 +09:00
kuaifan
40b637b16e build 2024-03-20 02:53:37 +09:00
kuaifan
6e68f399b4 perf: 优化数据结构 2024-03-20 02:49:44 +09:00
kuaifan
0be6c70e92 Merge commit '6c2d8fc16313234bbacb4ad4d7f8637b71025a26' into pro 2024-03-20 02:03:26 +09:00
weifs
6c2d8fc163 perf: 接龙优化为清空内容默认删除 2024-03-19 18:41:41 +08:00
kuaifan
a8193b8feb build 2024-03-19 16:32:39 +09:00
kuaifan
34159caf22 Merge commit 'd12c0c42072452de4c99ef55c5915edb108dd2ef' into pro 2024-03-19 16:04:26 +09:00
kuaifan
c75f406459 no message 2024-03-19 15:57:24 +09:00
kuaifan
99dca06d44 perf: 支持取消发送中的消息 2024-03-19 15:57:07 +09:00
weifs
d12c0c4207 perf: 1. 强化接龙接口本地时间戳问题 2. 接龙消息点展开按钮后做缓存处理 2024-03-19 14:38:15 +08:00
kuaifan
915a5ed7d5 fix: 关闭文件后无法再次打开 2024-03-19 12:07:54 +09:00
kuaifan
7bfc43c85f no message 2024-03-19 04:06:45 +09:00
kuaifan
77ea022ddf build 2024-03-19 03:23:09 +09:00
kuaifan
93578f93f4 Merge commit 'cbbd50a2e320ca0427474fb2921f2b93a5ad2c14' into pro 2024-03-19 03:17:19 +09:00
kuaifan
f129615ebe no message 2024-03-19 03:15:38 +09:00
kuaifan
0e5b44baad perf: 自动识别md格式发送 2024-03-19 03:15:19 +09:00
kuaifan
3596475790 fix: 消息太长导致菜单无法正常显示 2024-03-19 03:14:09 +09:00
kuaifan
6218521dea fix: 项目数量不正确的情况 2024-03-19 02:29:06 +09:00
kuaifan
65db8b5703 fix: 部分未读和待办信息不显示的情况 2024-03-19 02:21:48 +09:00
kuaifan
f5ff9a3648 perf: 优化回复、转发消息数据结构 2024-03-18 19:44:20 +09:00
weifs
cbbd50a2e3 fix: 审批中心修复loadIng效果 2024-03-18 14:12:19 +08:00
kuaifan
b04647e65a build 2024-03-17 17:50:27 +09:00
kuaifan
d34d94faa6 perf: 优化iOS端数据读取失败的情况 2024-03-17 17:21:29 +09:00
kuaifan
4038d9560f perf: 回复消息时自动@提及 2024-03-17 17:00:05 +09:00
kuaifan
006fc43498 perf: 优化会话数据结构 2024-03-17 16:23:57 +09:00
kuaifan
47c9b2e1b0 build 2024-03-15 12:51:10 +09:00
kuaifan
dc3e5f0a59 perf: 聊天文件发送进度 2024-03-15 12:48:33 +09:00
kuaifan
01bda83fcd build 2024-03-15 11:24:46 +09:00
kuaifan
9ecb9c68fb perf: 拨打电话确认提示 2024-03-15 11:16:20 +09:00
kuaifan
4612d5180a no message 2024-03-15 11:15:58 +09:00
kuaifan
cfb653796c perf: 优化预加载文件 2024-03-15 10:58:19 +09:00
kuaifan
d00cd5cb26 fix: 子窗口出现重新登录的情况 2024-03-15 10:19:24 +09:00
kuaifan
285a62c87e no message 2024-03-15 02:15:03 +09:00
kuaifan
bcb0c6bc77 build 2024-03-14 13:48:38 +09:00
kuaifan
d1ab2d98eb no message 2024-03-14 13:45:01 +09:00
kuaifan
c3d5328154 perf: 优化接口时间 2024-03-14 13:44:46 +09:00
kuaifan
fc30588014 build 2024-03-14 08:15:46 +09:00
kuaifan
65b02001b2 no message 2024-03-14 08:01:28 +09:00
kuaifan
cd011a172f perf: 优化审批对话按钮配色 2024-03-14 08:01:17 +09:00
kuaifan
bf913d9eff fix: 回复消息点击到原文无效 2024-03-14 07:26:19 +09:00
kuaifan
c2dd15fca1 no message 2024-03-14 02:55:42 +09:00
kuaifan
b267863b58 build 2024-03-13 14:46:32 +09:00
kuaifan
d189fb100a Merge commit 'c6568969c7b2d538d27cb4ca0ee412d4dbdceb56' into pro 2024-03-13 14:39:39 +09:00
kuaifan
dc6c5bef26 no message 2024-03-13 14:38:49 +09:00
kuaifan
7208d51644 perf: 优化文件功能按钮 2024-03-13 14:38:38 +09:00
kuaifan
16359a968d perf: 文件上传支持覆盖上传 2024-03-13 14:25:15 +09:00
kuaifan
d553f77533 perf: 优化app等比显示 2024-03-13 11:34:27 +09:00
kuaifan
bc25f5dfdf perf: 优化发送文件预览 2024-03-13 10:58:46 +09:00
kuaifan
d40028340c perf: 消息发送中禁止右键菜单 2024-03-13 08:03:58 +09:00
kuaifan
4194d1cddd perf: 部分搜索框图标抖动 2024-03-13 07:57:41 +09:00
kuaifan
1fdd532133 perf: 优化复制功能 2024-03-13 07:49:12 +09:00
kuaifan
71c62a3772 fix: 客户端无法打开excel文件 2024-03-13 07:32:16 +09:00
kuaifan
9be6cd5148 perf: 优化pdf文件预览 2024-03-13 07:14:27 +09:00
wfs
c6568969c7 style: 调整代码格式 2024-03-13 00:26:13 +08:00
wfs
f5b1a6ab05 fix: 修复投票实名逻辑 2024-03-13 00:25:10 +08:00
wfs
5efe659cf5 perf: 优化投票接口,加上事务锁 2024-03-13 00:17:23 +08:00
wfs
b254fd5ce2 perf: 优化接龙接口,加上事务锁 2024-03-12 23:49:05 +08:00
kuaifan
631a0ffff4 no message 2024-03-13 00:42:15 +09:00
kuaifan
8b11e9a19e perf: 优化转发消息样式 2024-03-13 00:42:06 +09:00
weifs
f6b006b000 feat: 升级okr 2024-03-11 18:07:37 +08:00
weifs
3a26f420b8 perf: 接龙接口-强化排序 2024-03-11 17:25:40 +08:00
weifs
0919e415ec perf: 审批按钮色微调 2024-03-11 16:57:45 +08:00
weifs
030a07698d perf: 统一审批中心的按钮色 2024-03-11 16:36:12 +08:00
weifs
a7f2582df7 perf: 转发会议亮色皮肤问题,转发文件宽度铺满 2024-03-11 10:39:53 +08:00
Pang
5f0a0e0371 no message 2024-03-10 22:13:21 +08:00
Pang
28717fd0c7 perf: 优化app数据交互 2024-03-10 14:44:55 +08:00
Pang
7014ea176a build 2024-03-10 12:12:30 +08:00
Pang
b4f2da66be fix: 修复搜索偶尔无效的情况 2024-03-10 11:03:34 +08:00
Pang
b53462cf6e build 2024-03-10 00:39:01 +08:00
Pang
8b40364722 no message 2024-03-10 00:36:07 +08:00
Pang
6ee1824410 perf: 优化文件预览 2024-03-09 17:38:33 +08:00
Pang
f63c2da37a no message 2024-03-09 10:44:36 +08:00
Pang
9be0642ba5 perf: 滑动列表自动隐藏键盘 2024-03-09 10:35:44 +08:00
Pang
55a922c7b3 perf: 优化时间格式 2024-03-09 10:16:09 +08:00
kuaifan
50893929d6 perf: 适配nodejs 20 2024-03-08 17:04:56 +08:00
weifs
03c94e791a feat: 升级okr 2024-03-08 11:03:16 +08:00
weifs
96bb554813 feat: 升级okr 2024-03-08 10:48:33 +08:00
Pang
1bc77de144 build 2024-03-07 23:13:44 +08:00
Pang
aa07c78fc8 perf: 修改消息换行优化 2024-03-07 23:13:24 +08:00
kuaifan
52dda88d40 build 2024-03-07 22:47:04 +08:00
kuaifan
c555b309bd feat: 新增不显示会话功能 2024-03-07 21:05:46 +08:00
kuaifan
0a51225762 Merge commit '3d725ddeef1f86eb02ff2b33435c23bc5e6da8de' into pro 2024-03-07 19:26:49 +08:00
kuaifan
fab49b1dda no message 2024-03-07 19:22:41 +08:00
kuaifan
57e422f2d3 perf: 优化预加载资源 2024-03-07 15:54:55 +08:00
kuaifan
50a1a3147e perf: 优化pdf文件预览 2024-03-07 15:54:26 +08:00
kuaifan
277115a30f perf: 优化签到消息 2024-03-07 15:53:31 +08:00
kuaifan
7464de3adc fix: 部分手机出现非正常滚动到底部的情况 2024-03-07 15:52:47 +08:00
HEXIANG
3d725ddeef fix:dootask官网标题 2024-03-07 15:17:25 +08:00
Pang
38d8f289e4 no message 2024-03-07 08:27:47 +08:00
Pang
edfd6e6de2 perf: 优化登录 2024-03-07 07:17:24 +08:00
Pang
a68ab6512e Merge commit '8383b88a448501cfdb32b79b6e2ccafedadf5e97' into pro 2024-03-06 22:03:34 +08:00
HEXIANG
8383b88a44 fix:dootask官网调整 2024-03-06 18:01:50 +08:00
kuaifan
27ff24f44e perf: 优化安装脚本 2024-03-06 17:18:12 +08:00
weifs
b111ecb227 fix: 修复导出任务统计没有按创建时间来的bug 2024-03-06 14:00:13 +08:00
kuaifan
ac17952cd3 perf: 优化消息时间格式 2024-03-06 12:01:18 +08:00
kuaifan
e24978fdd7 perf: 优化app功能 2024-03-06 11:47:05 +08:00
weifs
a4d7579e3f feat: 升级okr 2024-03-06 10:23:47 +08:00
Pang
52171b794a build 2024-03-06 02:44:16 +08:00
Pang
3c33f02e9d Merge commit '0968c43f61e0183aaf47e38a482d037bc33fc434' into pro 2024-03-06 01:08:59 +08:00
Pang
0a8823c40b no message 2024-03-06 01:05:30 +08:00
Pang
a3f7e71638 translate 2024-03-06 01:01:29 +08:00
Pang
7ebf4fb9ce perf: 默认映射443端口 2024-03-06 01:01:12 +08:00
kuaifan
c96bad3cdf perf: 优化子窗口 2024-03-06 00:30:30 +08:00
weifs
0968c43f61 feat: 发起投票功能添加缓存记录选中效果 2024-03-05 17:50:13 +08:00
weifs
ae147c76ff perf: 优化发布接口 删除目录的逻辑 2024-03-05 15:22:40 +08:00
weifs
0e916a2804 feat: 导出的签到数据和审批数据换成xlsx,因老版本的xls会出现兼容性问题 2024-03-05 15:19:12 +08:00
weifs
494565e131 perf: 优化发布接口 删除目录的逻辑 2024-03-04 09:51:35 +08:00
kuaifan
c4430e1a6c build 2024-03-02 14:56:12 +08:00
kuaifan
26adfa11bf Merge commit '6230bf94c5abfa7e67c6226e0b0dc088d80ff055' into pro 2024-03-02 14:39:47 +08:00
kuaifan
69ec57669e perf: 更新说明文档 2024-03-02 14:11:52 +08:00
kuaifan
3556133585 perf: 优化客户端 2024-03-02 14:11:33 +08:00
kuaifan
a65181757d perf: 默认关闭端到端加密传输 2024-03-02 13:21:01 +08:00
kuaifan
42d39a830e fix: 修改消息导致最后消息改变 2024-03-02 13:07:27 +08:00
kuaifan
2e70c9617c fix: 显示无关系的子任务、指定成员可见消息推送 2024-03-02 12:52:16 +08:00
zzw
6230bf94c5
fix:调整gemini机器人设置参数 2024-03-01 17:47:41 +08:00
kuaifan
47832ececb Merge commit '76570e2f1b72de50cb54c36229447eea69c59940' into pro 2024-03-01 16:02:36 +08:00
kuaifan
60e6003485 build 2024-02-29 19:47:45 +08:00
kuaifan
9133f289b4 perf: 优化ai机器人 2024-02-29 19:39:05 +08:00
gwok
76570e2f1b fix:修复左侧滚动菜单不跟随右侧正文自动上滑 2024-02-29 18:25:26 +08:00
weifs
c9234a4b49 feat: 升级okr 2024-02-29 16:22:25 +08:00
weifs
c1361fadda perf: okr和审批优化 2024-02-29 15:26:45 +08:00
zzw
ec7af94f71
fix:调整发送参数逻辑 2024-02-29 11:39:30 +08:00
kuaifan
81690d6ce9 build 2024-02-28 15:33:33 +08:00
kuaifan
236b57864b Merge commit 'e3ce3bcfbe1d251c791fddbf2627b35669bdd2df' into pro 2024-02-28 15:21:43 +08:00
kuaifan
22259ec34d build 2024-02-28 15:15:25 +08:00
kuaifan
5a4700753a perf: 优化pdf在线预览 2024-02-28 15:04:56 +08:00
kuaifan
cc862741dc fix: 推送标题存在换行时不显示 2024-02-28 15:04:31 +08:00
Pang
779b32e8ad feat: 优化内置浏览器 2024-02-28 15:03:56 +08:00
zzw
e3ce3bcfbe
feat:gemini机器人添加代理参数 2024-02-28 14:37:18 +08:00
zzw
673053f181
feat:集成geminiAI 2024-02-27 18:23:07 +08:00
Pang
b6eb77ae63 no message 2024-02-24 09:18:12 +08:00
kuaifan
0e63255a7f build 2024-02-23 22:11:43 +08:00
kuaifan
f42408a363 no message 2024-02-23 22:11:43 +08:00
kuaifan
897fc51ce3 build 2024-02-23 22:11:43 +08:00
Pang
6848b126c5 perf: 优化客户端打开服务器链接 2024-02-23 22:11:43 +08:00
Pang
0ed9afd1bd no message 2024-02-23 22:11:43 +08:00
weifs
26cca8298f fix: 修复下载文件大小为0时报错 2024-02-23 13:53:03 +08:00
weifs
58407af2ba fix: 更改其他版本的链接 2024-02-23 09:40:00 +08:00
spylecym
3a0473a74f fix: 修复价格页面样式 2024-02-23 09:13:00 +08:00
weifs
6e5124fe22 fix: 发布接口,调整缓存时间为两小时 2024-02-22 19:19:50 +08:00
weifs
02bd022c62 feat: 发布接口只保留最近两个版本 2024-02-22 19:18:04 +08:00
weifs
f2538884ea perf: 签到设置,有些客户服务器安全体系会拦截 curl -sSL 关键字,优化为cmd不传值 2024-02-22 11:07:54 +08:00
weifs
8d121d4056 perf: 签到设置,有些客户服务器安全体系会拦截 curl -sSL 关键字,优化为base64返回 2024-02-22 11:02:32 +08:00
weifs
96438604ee perf: 升级okr 2024-02-21 17:30:59 +08:00
ganzizi
63ccd675d0 fix: 修复okr定时处理信息不发送 2024-02-21 16:05:32 +08:00
weifs
2c08145c40 fix: 项目已归档,任务面板也没有这三个任务,但是每次新增报告,都会弹任务出来 2024-02-21 14:42:30 +08:00
spylecym
12effb5738 fix: 修改下载页面按钮布局样式 2024-02-20 15:52:32 +08:00
weifs
91bfb989be feat: 官网添加其他版本的按钮 2024-02-20 15:05:45 +08:00
weifs
192de79fea feat: 统一表为utf8mb4_unicode_ci 2024-02-20 15:00:38 +08:00
Pang
82063f1b21 build 2024-02-06 07:27:43 +08:00
kuaifan
02b263439b perf: 升级okr容器 2024-02-05 21:26:53 +08:00
kuaifan
7efaf3bb32 perf: 新增禁止私聊、群聊功能 2024-02-05 21:24:45 +08:00
kuaifan
7dd5b082cf perf: 更换笑话和鸡汤接口 2024-02-05 13:33:42 +08:00
Pang
6320eaa3ac build 2024-01-25 21:52:57 +08:00
Pang
85b88b6b61 no message 2024-01-21 20:20:02 +08:00
Pang
f285665f90 build 2024-01-21 16:24:29 +08:00
Pang
8f4399dc2f perf: 修复一些问题 2024-01-21 16:12:24 +08:00
Pang
c8b8cc578d build 2024-01-19 14:38:01 +08:00
Pang
a142f52113 perf: iOS打开键盘时看不见通知的情况 2024-01-18 18:36:09 +08:00
Pang
aa666a9662 build 2024-01-18 13:16:08 +08:00
Pang
0a4ac6abb7 perf: 优化系统参数 2024-01-17 23:22:13 +08:00
Pang
96b0cb8aa0 perf: 优化菜单显示位置 2024-01-16 11:15:19 +08:00
Pang
b3a30720fa build 2024-01-16 00:56:30 +08:00
kuaifan
b711605bdc perf: 优化获取最近消息 2024-01-16 00:50:53 +08:00
kuaifan
31efee2e97 perf: 优化请求时间 2024-01-15 22:11:24 +08:00
kuaifan
569af135bd no message 2024-01-15 21:27:02 +08:00
kuaifan
2975a0eaf9 perf: 优化触摸长按和右键菜单共存 2024-01-15 21:06:14 +08:00
kuaifan
d4ee87f324 fix: 部分机型首次打开聊天窗口不显示聊天记录的问题 2024-01-15 21:05:40 +08:00
kuaifan
c676a3037c build 2024-01-15 14:26:53 +08:00
kuaifan
e4790062c8 no message 2024-01-15 14:15:35 +08:00
Pang
bb8a6982d0 build 2024-01-15 08:58:38 +08:00
Pang
80af98111b perf: 优化发送消息接口 2024-01-14 20:12:21 +08:00
Pang
9a69d20949 perf: 优化搜索提示 2024-01-14 15:07:43 +08:00
Pang
5e52996a9e pref: 优化消息列表 2024-01-14 14:28:29 +08:00
Pang
33d22d4970 build 2024-01-14 01:19:13 +08:00
Pang
170473fb2d Merge commit 'c1b63af5f5408d5ce9c1d45560480a6fd4bd52a9' into pro 2024-01-13 23:54:59 +08:00
Pang
67ccaea41e no message 2024-01-13 23:54:39 +08:00
Pang
67d7e81ffa no message 2024-01-13 23:54:30 +08:00
Pang
1788b40431 fix: 重复通知 2024-01-13 23:47:13 +08:00
Pang
7f432cefb9 perf: 优化消息保存覆盖 2024-01-13 22:42:30 +08:00
Pang
57e8c9c7cd pref: 优化消息列表 2024-01-13 22:26:48 +08:00
weifashi
c1b63af5f5 fix: 修复投票进度的算法 2024-01-13 22:12:06 +08:00
Pang
cf7f245a49 perf: 优化消息保存覆盖 2024-01-13 00:14:21 +08:00
Pang
4824f30950 no message 2024-01-13 00:07:06 +08:00
Pang
88fb1d8e62 perf: 优化消息保存覆盖 2024-01-12 23:48:22 +08:00
Pang
e67ce9a438 perf: 优化快捷表情发送消息时关闭延迟的问题 2024-01-12 23:48:22 +08:00
Pang
976b9690d2 pref: 优化消息列表 2024-01-12 23:48:22 +08:00
weifashi
36735ace50 perf: 升级okr 2024-01-12 10:04:13 +08:00
kuaifan
3aeea13526 build 2024-01-11 23:52:25 +08:00
kuaifan
6f33c3f5d6 Merge commit 'e53242613b634916bb2f7939bc260ee683c2236e' into pro 2024-01-11 23:48:39 +08:00
kuaifan
53aab1ed0f build 2024-01-11 23:43:59 +08:00
kuaifan
b209040978 no message 2024-01-11 22:45:10 +08:00
kuaifan
e74aeb9393 no message 2024-01-10 23:14:42 +08:00
weifashi
e53242613b perf: 年度报告接口 - 查询条件优化 2024-01-10 18:59:47 +08:00
kuaifan
bea7ba00f0 no message 2024-01-10 14:23:38 +08:00
weifashi
24d90b93e2 perf: 升级okr, ai 2024-01-10 01:20:54 +08:00
kuaifan
f380b0433d build 2024-01-09 20:21:12 +08:00
kuaifan
f7df6408ed no message 2024-01-09 20:19:56 +08:00
kuaifan
10a77ee2a9 Merge commit '5a44076859d807d61aa44a06364c619a6b877f07' into pro 2024-01-09 18:57:52 +08:00
kuaifan
d5db894891 no message 2024-01-09 18:57:36 +08:00
weifashi
5a44076859 perf: 年度汇报接口返回用户头像 2024-01-09 17:26:03 +08:00
kuaifan
e78513cb80 no message 2024-01-09 14:44:13 +08:00
Pang
2860c4cbe6 no message 2024-01-09 07:54:12 +08:00
weifashi
ebce9fa596 feat: 更新okr 2024-01-08 22:58:36 +08:00
kuaifan
8080d0bb4e no message 2024-01-08 19:10:29 +08:00
kuaifan
221e42d02b no message 2024-01-08 17:20:32 +08:00
kuaifan
e06fd21a4b no message 2024-01-08 16:29:08 +08:00
kuaifan
f42036c104 no message 2024-01-08 12:30:56 +08:00
weifashi
937bc4ead3 perf: 年度报告接口 - 增加用户信息字段返回 2024-01-08 09:32:28 +08:00
weifashi
322a855ba2 perf: 去掉未使用的引用 2024-01-08 01:26:37 +08:00
weifashi
7c4d537d67 perf: 去掉未使用的引用 2024-01-08 01:25:44 +08:00
weifashi
b78e4240cb feat: 添加年度报告接口 2024-01-08 01:22:58 +08:00
weifashi
4f663dd761 feat: 添加年度报告接口 2024-01-08 01:20:10 +08:00
Pang
b3bd5aded5 perf: 优化滑动返回动画效果 2024-01-08 00:25:37 +08:00
Pang
7714c53085 perf: 消息置顶滚动恢复 2024-01-08 00:15:35 +08:00
kuaifan
3a74cdc98b perf: 消息置顶滚动恢复 2024-01-07 14:26:42 +08:00
kuaifan
3631f511d4 perf: 优化消息Load效果 2024-01-07 14:25:35 +08:00
Pang
5f7d528d9d build 2024-01-05 07:02:46 +08:00
Pang
85ceb8b938 Merge commit 'fb8d759103d8468e6ef51f18d1c5f31a2b3e6a89' into pro 2024-01-05 06:55:29 +08:00
Pang
b4b268a4d7 no message 2024-01-05 06:55:15 +08:00
Pang
4b39f13fa9 build 2024-01-05 00:42:08 +08:00
Pang
4abcec08f4 perf: 消息首次加载数据优化 2024-01-05 00:38:52 +08:00
Pang
4144f92631 fix: 消息阅读回馈 2024-01-04 20:14:26 +08:00
weifashi
fb8d759103 perf: 优化打包下载 2024-01-04 12:02:29 +08:00
kuaifan
e215cda700 build 2024-01-04 00:07:27 +08:00
kuaifan
846fdcf145 perf: 优化打包下载 2024-01-03 20:48:49 +08:00
kuaifan
ecdabc668d Merge commit 'ada88a1c02740846547eb2c3eaf37639006b3b57' into pro 2024-01-03 19:34:27 +08:00
kuaifan
e8839974d4 no message 2024-01-03 19:23:18 +08:00
kuaifan
2a864b6617 no message 2024-01-03 18:50:27 +08:00
weifashi
ada88a1c02 perf: 去掉无用引用 2024-01-03 15:09:36 +08:00
weifashi
8fe16416f9 perf: 优化报告未读接口 2024-01-03 15:07:47 +08:00
kuaifan
0daf06c06d no message 2024-01-03 14:29:42 +08:00
Pang
3b697e7400 no message 2024-01-02 11:45:01 +08:00
Pang
a543f8716b perf: 优化图片显示 2023-12-30 16:16:58 +08:00
kuaifan
63703a029f perf: 优化代码 2023-12-30 02:11:32 +08:00
weifashi
22415e6c61 fix: 修复置顶人员 2023-12-29 17:05:57 +08:00
kuaifan
1a69e76fe7 build 2023-12-29 16:39:26 +08:00
kuaifan
7f916c4770 perf: 优化代码 2023-12-29 16:39:07 +08:00
weifashi
f76d36a74b perf: 代码整理 2023-12-29 11:45:42 +08:00
weifashi
ab0539a263 perf: 优化待审批流程数量接口 2023-12-29 00:35:35 +08:00
weifashi
4104dea68e fix: 修复高危bug 2023-12-28 23:15:22 +08:00
weifashi
5aded9daa3 perf: 代码优化 2023-12-28 22:10:56 +08:00
weifashi
91d5bd80ff Merge branch 'kuaifan/pro' into pro 2023-12-28 22:08:52 +08:00
kuaifan
40d56a0155 build 2023-12-28 21:09:30 +08:00
kuaifan
54117fe51a perf: 优化未读消息提示 2023-12-28 20:48:23 +08:00
kuaifan
fbd662e400 perf: 优化预览消息 2023-12-28 20:48:00 +08:00
kuaifan
ccb31a81f8 perf: 优化缓存数据 2023-12-28 17:44:17 +08:00
weifashi
dbb9366de6 perf: 代码优化 2023-12-28 12:02:39 +08:00
weifashi
6d7a4edae3 perf: 代码整理 2023-12-26 23:54:12 +08:00
weifashi
632068a74c perf: 任务可见性用户 - 分表优化 2023-12-26 23:28:32 +08:00
weifashi
4e78920f99 perf: 代码命名优化 2023-12-26 20:53:59 +08:00
weifashi
fdc85bbcbf fix: 1.修复可见效数据取值,2.修复设置可见效指定人员不成功 2023-12-26 20:52:03 +08:00
weifashi
67dafae9d6 perf: 移动任务后,对应项目路径也要更改显示 2023-12-26 17:16:57 +08:00
weifashi
989e5a5f9d perf: 升级okr容器 2023-12-26 17:06:35 +08:00
weifashi
a7e5bd0b80 perf: 导出任务统计 - 下载地址换成按钮 2023-12-26 16:58:12 +08:00
weifashi
da131746be Merge branch 'pro' into wfs-msg-top
# Conflicts:
#	resources/assets/js/pages/manage/components/DialogWrapper.vue
#	resources/assets/sass/components/user-select.scss
#	resources/assets/sass/pages/components/dialog-wrapper.scss
2023-12-26 16:29:08 +08:00
kuaifan
8a7e80fe86 build 2023-12-26 00:24:26 +08:00
kuaifan
865dc61cd1 fix: 修复已知问题 2023-12-25 22:19:42 +08:00
kuaifan
c8b96a8bce build 2023-12-25 14:15:52 +08:00
kuaifan
5546dbaa0e perf: 优化会话列表 2023-12-25 14:15:45 +08:00
Pang
fd6312408b build 2023-12-24 21:21:18 +08:00
Pang
4f4c6de8a2 perf: 优化录音load效果 2023-12-24 20:28:02 +08:00
Pang
4506ba8cd3 perf: 优化消息列表 2023-12-24 19:30:36 +08:00
Pang
9300e9fd9a perf: 优化应用图标 2023-12-24 11:36:20 +08:00
Pang
a4eb8317da fix: 撤回消息不删除消息的情况 2023-12-24 11:04:19 +08:00
Pang
0e819de1bc perf: 升级okr容器 2023-12-24 10:47:30 +08:00
Pang
9800f9e3da perf: 优化用户选择器 2023-12-24 10:47:07 +08:00
Pang
a0f6a17005 perf: 优化对话列表接口数据 2023-12-24 10:46:50 +08:00
Pang
6087c7fed0 perf: 优化未读消息提示动画 2023-12-23 19:24:30 +08:00
Pang
3fa0b472d2 perf: 优化消息更新机制 2023-12-23 19:20:41 +08:00
Pang
1ce96ddae6 perf: 优化缓存 2023-12-23 12:19:35 +08:00
kuaifan
d4ef140c8e perf: 优化用户选择器 2023-12-23 11:58:54 +08:00
kuaifan
7de575e236 no message 2023-12-22 12:26:51 +08:00
kuaifan
f0f0883a88 build 2023-12-22 11:24:36 +08:00
kuaifan
c1695a78d6 perf: 优化任务修改 2023-12-22 11:09:43 +08:00
weifashi
15e37eded3 fix: 更新导致的小问题 2023-12-22 10:01:27 +08:00
weifashi
59aa854470 feat: 消息置顶功能 - 50% 2023-12-21 17:20:42 +08:00
1860 changed files with 167376 additions and 124753 deletions

1
.agents Symbolic link
View File

@ -0,0 +1 @@
.claude

View File

@ -0,0 +1,119 @@
---
description: 备份 DooTask 数据:数据库(必须)+ public/uploads排除 tmp可选+ docker/appstore/config可选。汇总到临时目录并附 README 说明,打包到 backup/ 按日期命名。只读取源数据、绝不删改,失败即停。
---
# DooTask 数据备份
**刚性技能**——前置检查 → 选可选项 → 确认 → 执行 → 报告。只读取源数据生成归档,**绝不删除或修改任何源数据/既有备份**。任何一步失败立即停止。
## 备份范围
| 项 | 来源 | 是否必须 | 说明 |
|----|------|---------|------|
| 数据库 | `./cmd mysql backup` 产出的 `.sql.gz` | **必须** | 脚本内部用 mysqldump 导出当前库 |
| 上传文件 | `public/uploads`**排除 `public/uploads/tmp`** | 可选 | 头像/聊天/任务/文件等真实上传数据;`tmp` 是临时目录,可重建,不备份 |
| 应用配置 | `docker/appstore/config` | 可选 | 应用市场各应用的配置;含 **root 属主子目录**,收集时可能需 sudo |
> `docker/appstore/apps` **不在备份范围**——可从应用市场重新安装,无需备份。
## 前置检查(全部通过才能继续)
1. **工作目录**:在项目根(存在 `cmd``docker-compose.yml`
2. **数据库容器**`mariadb` 容器在跑DB 备份依赖它;不在则提示用户先 `./cmd up` 起服务)
3. **磁盘空间**:确认 `backup/` 所在盘空间足够(数据库 dump 可能较大)
4. **选可选项**:询问用户本次是否包含 `public/uploads``docker/appstore/config`**默认两个都含**
检查通过、可选项确定后,汇报本次将备份哪些项,**向用户确认一次**再执行。
## 执行
用一个统一时间戳贯穿全程:`TS=$(date +%Y%m%d_%H%M%S)`,临时目录 `WORK="tmp/dootask-backup-${TS}"`
### 1) 建临时工作目录
```shell
mkdir -p "$WORK"
```
`tmp/` 已被 gitignore安全
### 2) 数据库(必须)
```shell
./cmd mysql backup
```
脚本会把 dump 写到 `docker/mysql/backup/<库名>_<时间戳>.sql.gz` 并打印「备份文件:...」。**取该次产出的最新 dump** 复制进工作目录(不用关心它原始落在哪):
```shell
DB_FILE=$(ls -t docker/mysql/backup/*.sql.gz | head -1)
cp "$DB_FILE" "$WORK/"
```
### 3) public/uploads可选排除 tmp
```shell
rsync -a --exclude='tmp' public/uploads/ "$WORK/uploads/"
```
> 无 rsync 时用 tar 管道:`mkdir -p "$WORK/uploads" && tar cf - --exclude='./tmp' -C public/uploads . | tar xf - -C "$WORK/uploads"`
### 4) docker/appstore/config可选
```shell
cp -a docker/appstore/config "$WORK/appstore-config"
```
> 含 root 属主子目录,若报 `permission denied`:改用 `sudo cp -a ...`,随后把整个工作目录属主归还当前用户,保证后续打包/清理不受阻:
> ```shell
> sudo chown -R "$(id -u):$(id -g)" "$WORK"
> ```
### 5) 写 README.md备份说明
`$WORK/README.md` 写明本次备份信息,便于日后识别与还原。模板:
```markdown
# DooTask 备份 — <TS>
- 备份时间:<人类可读时间>
- DooTask 版本:<取自 package.json version>
- 包含内容:
- 数据库:<DB dump 文件名>(来源 mysqldump 当前库)
- 上传文件uploads/(来源 public/uploads已排除 tmp ← 未选则写「未包含」
- 应用配置appstore-config/(来源 docker/appstore/config ← 未选则写「未包含」
- 各项大小:<du -sh 列出工作目录内各项>
## 还原提示
- 数据库:`gunzip < <db>.sql.gz | mysql -u<user> -p<pass> <库名>`,或用 `./cmd mysql recovery` 选对应文件还原。
- 上传文件:将 uploads/ 内容覆盖回项目 public/uploads/。
- 应用配置:将 appstore-config/ 覆盖回 docker/appstore/config/。
```
### 6) 打包到 backup/,清理临时目录
```shell
mkdir -p backup
tar czf "backup/dootask_backup_${TS}.tar.gz" -C tmp "dootask-backup-${TS}"
rm -rf "$WORK"
```
## 报告
向用户报告:
- 最终归档路径:`backup/dootask_backup_<TS>.tar.gz`
- 归档大小(`ls -lh`
- 实际包含了哪些项(数据库 + 视选择含/不含 uploads、appstore-config
## 失败处理
- 任何步骤失败立即停止,原样报告错误
- **不要**自动重试、不要静默跳过某一项(可选项是否包含由前置确认决定,不在执行中临时变更)
- DB 备份失败(如 mariadb 未运行)→ 停止,提示用户起服务后重试
- 打包前若工作目录有 root 属主残留导致 tar/rm 失败 → `sudo chown` 归还属主后继续,不要删源数据
## 禁止项
| 错误做法 | 正确做法 |
|---------|---------|
| 为"省空间"删除源数据或既有备份 | 只读取源数据生成归档,源数据一律不动 |
| 备份 `public/uploads/tmp` | 排除 tmp临时、可重建 |
| 把 `docker/appstore/apps` 也打进去 | 不在范围,可从应用市场重装 |
| 遇 config 的 root 子目录就跳过该项 | `sudo` 收集后 chown 归还,完整备份 |
| 不写 README 直接打包 | 每个归档自带 README便于日后识别还原 |
| 把归档写进 git | 归档放 `backup/`(已 gitignore不提交 |
## Red Flags —— 出现这些念头立即停下
- "源数据太大,删点旧的再备份" → 不,备份只读不删
- "config 有 root 目录,跳过算了" → 不sudo 收集后归还属主
- "apps 也一起备了更全" → 不apps 不在范围
- "tmp 里临时文件顺手也备了" → 不,明确排除 `public/uploads/tmp`

View File

@ -0,0 +1,76 @@
---
name: dootask-fix-permission
description: 修复 DooTask 可写目录bootstrap/cache、docker、public、storage的属主/权限chown 回当前用户 + 目录 chmod 775对齐 install 的赋权逻辑,赋权不删数据。
---
# DooTask 目录权限修复
容器内进程常以 **root** 写入挂载目录(`storage``public/uploads``bootstrap/cache` 等),导致宿主机当前用户对这些文件**没有写权限**,进而触发:
- `./cmd install` 报「目录【xxx】权限不足」/ 目录权限检测失败
- `./cmd build`vite`EACCES: permission denied, copyfile`(复制 `public/uploads/...` 时)
- Laravel 运行时写 `storage`/`bootstrap/cache` 失败
本技能**对齐 `./cmd install` 的目录赋权逻辑**:对四个可写目录做 `chmod 775`(目录)+ `chown` 回当前用户。
## 适用目录
与 install 一致的四个:
```
bootstrap/cache
docker
public # 含 public/uploads真实上传数据
storage
```
## 核心原则:赋权,不删数据
`public/uploads` 含真实上传文件(头像、附件等)。**永远优先 `chown` 改属主,不要删数据。** 即便用户说"清理一下",也只允许清临时目录 `public/uploads/tmp`**切勿**删 uploads 下其他内容。
## 前置检查
1. **工作目录**:在项目根(存在 `cmd` 且这四个目录在)
2. **sudo**:改属主需 root当前文件多为 root 属主)。本机一般可免密 sudo不行则经 docker 以 root 改权限
3. 确认要修的范围:默认四个目录全修;若用户只想解 build 报错,也可只针对 `public`(含 `public/uploads`
检查通过后汇报将执行的命令,**向用户确认一次**再执行。
## 执行
确认后执行(属主修回当前用户,目录权限 775
```shell
# 1) 属主修回当前用户(递归)
sudo chown -R "$(id -u):$(id -g)" bootstrap/cache docker public storage
# 2) 目录权限 775仅目录对齐 install 的 `find -type d -exec chmod 775`
find bootstrap/cache docker public storage -type d -exec chmod 775 {} \;
```
> 只想解 build 的 uploads 报错时,可只对 `public`
> ```shell
> sudo chown -R "$(id -u):$(id -g)" public/uploads
> ```
执行后报告:改了哪些目录、属主/权限现状(可 `ls -ld` 抽查),并提示用户可重试之前失败的 install/build/update。
## 失败处理
- `chown` 报权限不足 → 当前用户无 sudo 权限,提示用户用有 root 权限的账户,或经 docker 以 root 执行;不要静默跳过
- 任何步骤失败立即停止报告,不自动重试
## 禁止项
| 错误做法 | 正确做法 |
|---------|---------|
| build 报 uploads EACCES 就 `rm` 删文件 | `chown` 修属主,保留数据 |
| 删整个 `public/uploads` 清场 | 最多清 `public/uploads/tmp`,别碰真实上传数据 |
| 对文件无差别 `chmod 777` | 目录 `chmod 775` + `chown` 回当前用户即可 |
| 不加 sudo 直接 chown root 文件 | 改属主需 root |
## Red Flags —— 出现这些念头立即停下
- "uploads 复制失败,删掉再 build" → 不,`chown` 赋权,不丢数据
- "777 一把梭最省事" → 不,按 install 的 775目录+ chown
- "权限不够就跳过这个目录" → 不,报告交用户处理 sudo

View File

@ -0,0 +1,74 @@
---
name: dootask-install
description: 首次部署 DooTask前置检查后执行 `sudo ./cmd install`(建库 + migrate --seed 的重操作),刚性流程、单次确认、失败即停。
---
# DooTask 安装流程
**刚性技能**——前置检查 → 向用户确认一次 → 执行 → 报告结果。任何一步失败立即停止。
## 核心原则
**违反字面规则 = 违反流程精神。** 不要擅自增加、省略、合并步骤,不要为"省事"绕过 sudo 或确认。
`./cmd install` 已把整套安装封装为单条命令(赋权→起容器→`composer install``key:generate``migrate --seed``up -d`)。本技能的职责是**安装前把关、选对参数、执行前确认、已知失败处理**,而不是把脚本逻辑拆开重做。
## 前置检查(全部通过才能继续)
执行前依次确认:
1. **工作目录**:必须在项目根(存在 `cmd``docker-compose.yml``.env.docker`
2. **Docker**`docker``docker-compose`/`docker compose`(v2+) 可用且 daemon 在跑(脚本 `check_docker` 也会查,但提前确认能更早报错)
3. **Node.js ≥ 20**(脚本 `check_node` 会查)
4. **APP_ID 不冲突**:若 `.env` 已有 `APP_ID` 且被其他实例占用,脚本 `check_instance` 会报错——此时**停止**,提示用户先清空 `.env` 里的 `APP_ID``APP_IPPR` 再装
5. **sudo**`./cmd install` 需 root`check_sudo`),用 `sudo ./cmd install` 执行
⚠️ **这是重操作**:会创建数据库并执行 `migrate --seed`(灌入种子数据)。在已有数据的环境上重装前务必和用户确认,避免覆盖。
检查通过后汇报结果,**向用户确认一次**再执行。
## 参数选择
| 参数 | 作用 | 何时用 |
|------|------|--------|
| `--port <端口>` | 指定 HTTP 端口(脚本会做端口占用检测) | 用户要自定义端口,或默认端口被占 |
| `--relock` | 删除 `node_modules`/`package-lock.json`/`vendor`/`composer.lock` 后重装 | **谨慎**:仅在依赖锁损坏、用户明确要求重建锁时用,会拖慢安装 |
不确定时不要自作主张加参数,按需询问用户。
## 执行
确认后执行(按用户选择带上参数):
```shell
sudo ./cmd install
# 或: sudo ./cmd install --port 8080
```
成功后脚本会输出访问地址并调用 `repassword.sh`。执行完向用户报告:访问地址(`http://127.0.0.1:<APP_PORT>`)、以及数据库密码提示。
## 失败处理
- 任何步骤失败立即停止,原样报告错误信息
- **不要**自动重试,**不要**自动跳过
- 常见失败与对应处理:
- `APP_IDxxx已被其他实例使用` → 停止,让用户清空 `.env``APP_ID`/`APP_IPPR` 再装
- `端口 xxx 已被占用` → 停止,让用户换 `--port`
- `目录【xxx】权限不足` / 目录权限检测失败 → 这是目录属主/权限问题,引导用户用 **dootask-fix-permission** 技能修复后重装
- `安装依赖失败`composer→ 报告,交用户决定(常因网络/镜像源)
## 禁止项
| 错误做法 | 正确做法 |
|---------|---------|
| 不加 sudo 直接 `./cmd install` | 用 `sudo ./cmd install`(脚本强制 root |
| 失败后"我再试一次"或自动跳过 | 立即停止,交还用户 |
| 在已有数据环境上不问就重装 | 先确认会 `migrate --seed`,可能影响现有数据 |
| 遇权限报错自己乱 `chmod`/`chown` | 走 dootask-fix-permission 技能统一处理 |
| 不问就加 `--relock` | 默认不加;仅用户明确要求或锁损坏时用 |
## Red Flags —— 出现这些念头立即停下
- "端口/权限报错了我顺手帮 TA 改一下别的" → 停下,只处理本次报的问题,按指引走对应技能
- "种子数据应该没事,直接重装" → 不,先确认是否会覆盖现有数据
- "sudo 麻烦,先试试不加" → 不install 必须 root

View File

@ -0,0 +1,204 @@
---
name: dootask-release
description: 从 `pro` 分支发布 DooTask 前端新版本:翻译 → 版本号/更新日志 → 构建 → 提交推送,刚性顺序、每步确认、失败即停。
---
# DooTask 发布流程
**刚性技能**——严格按顺序执行,每步向用户确认,任何一步失败立即停止。
## 核心原则
按固定顺序执行不增删、合并或重排步骤。翻译Step 1和更新日志Step 2由你直接产出脚本只做确定性机械工作算版本号、检测差异、字节级生成语言文件
## 前置检查(全部通过才能继续)
执行任何发布步骤前,依次检查:
1. **分支**:必须是 `pro`,否则停止,提示用户切换
2. **工作区**`git status` 必须干净(无未提交变更、无未跟踪文件),否则**停止**并交由用户处理
3. **Node.js**`node --version` 必须 ≥ 20
4. **PHP**`php --version` 必须可用Step 1 的脚本依赖本地 php无需容器。若 host 无 php停止并提示用户
检查通过后汇报结果,用户确认后再开始执行。
## 发布步骤
**每步执行前**向用户确认;**每步执行后**报告结果。
开始前先把这份清单复制到你的回复里,逐项勾选、跟踪进度:
```
发布进度:
- [ ] 前置检查(分支 pro / 工作区干净 / node≥20 / php 可用)
- [ ] Step 1 翻译diff → 翻译 → apply → generate
- [ ] Step 2 版本号 + CHANGELOG
- [ ] Step 3 构建(./cmd prod
- [ ] 汇总变更 → 用户确认 → commit + push
- [ ] 确认 GitHub Actions Publish 工作流 success
```
---
### Step 1: 翻译
多语言数据流:`language/original-{web,api}.txt`(原文/简体中文)→ 经翻译写入 `language/translate.json`(含 9 种语言)→ 生成 `public/language/{web,api}/*`
**1.1 检测差异**
```shell
php .claude/skills/dootask-release/scripts/language.php diff
```
输出 JSON
- `regexErrorCount > 0`translate.json **已有条目**的占位符与某语言值不一致 → **停止**,报告 `regexErrors`,交用户修复(这是历史数据问题,不要自行猜测修改)
- `redundantCount > 0`translate.json 里有、但原文已删除的条目 → 仅作提示apply 时会自动剔除,不致命)
- `needsCount == 0`:无新文案 → **跳到 1.4 直接生成**
- `needsCount > 0``needs` 数组即待翻译清单,每项 `key` 已转成占位符形式(如 `(%T1)`)→ 进入 1.2
**1.2 翻译**
`needs` 里的每个 `key`,翻成 8 种语言(`zh` 留空、`key` 原样保留):`zh-CHT` `en` `ko` `ja` `de` `fr` `id` `ru`
要求:贴合「项目任务管理系统」语境;占位符 `(%T1)`/`(%M1)` 等原样保留、不可增删改,位置可随目标语言语序调整:
| 原文 | 翻成英语 |
|---|---|
| (%T1)的周报[(%T2)][(%T3)月第(%T4)周] | Weekly report of (%T1) [(%T2)] [Week (%T4) of month (%T3)] |
| (%T1)提交的「(%M2)」待你审批 | '(%M2)' submitted by (%T1) is waiting for your approval |
把结果写成一个 JSON 数组文件(建议放 `/tmp/dootask-release-translated.json`,避免污染工作区),每个元素含全部 10 个字段,顺序为:
`key, zh, zh-CHT, en, ko, ja, de, fr, id, ru``zh``""`)。
```json
[
{"key":"...(%T1)...","zh":"","zh-CHT":"...","en":"...","ko":"...","ja":"...","de":"...","fr":"...","id":"...","ru":"..."}
]
```
**1.3 合并进 translate.json**
```shell
php .claude/skills/dootask-release/scripts/language.php apply /tmp/dootask-release-translated.json
```
脚本会校验字段完整性与占位符完整性、追加新条目、剔除冗余项,并按项目原生格式写回 `translate.json`。任一条不合格会报错停止,按提示修正翻译后重试。
**1.4 生成前端/后端语言文件**
```shell
php .claude/skills/dootask-release/scripts/language.php generate
```
`translate.json` 字节级重新生成 `public/language/web/*.js``public/language/api/*.json`(排序/转义与项目原生工具完全一致,正常情况下 diff 只包含本次新增条目)。
**1.5 报告**:用 `git status --short language public/language` 汇总本步改动,向用户报告新增了多少条翻译。
---
### Step 2: 版本号 + 更新日志
**2.1 计算并写入版本号**
```shell
node .claude/skills/dootask-release/scripts/version_bump.js
```
脚本据 git 历史算出新 `version``codeVerson` 并写入 `package.json`,输出 JSON 含:`version``prevVersion``changelogRange`(如 `<上次release提交>..HEAD`,用于下一步圈定本次更新范围)。
**2.2 撰写 CHANGELOG**
读取本次区间的提交:
```shell
git log <changelogRange> --stat
```
`--stat` 会带上每个提交的完整描述正文 + 改动文件清单;光看标题不够时用 `git show <hash>` 看具体代码改动。
`CHANGELOG.md` 现有格式,在文件顶部 `# Changelog` 说明段之后、紧挨上一个 `## [...]` 之前,插入新版本区段:
```markdown
## [<version>]
### Features
- ...
### Bug Fixes
- ...
### Performance
- ...
```
撰写要求(对齐项目历史风格):
- 小节标题用**英文 Title Case**`Features` / `Bug Fixes` / `Performance` / `Documentation` / `Security` / `Miscellaneous`**不要译成中文****没有内容的小节整段省略**。
- 条目正文用**通俗友好的简体中文**,面向**普通用户**描述更新带来的直接好处,**避免技术术语**(如 refactor、merge branch、commit lint、bump deps 等)。
- 过滤掉对用户无意义的提交(纯构建/依赖/CI/合并提交、本技能自身的脚手架改动等)。
- 仅凭提交标题无法判断是否对用户有价值时,结合提交的完整描述正文和实际代码改动(`git show <hash>`)再决定,不要只看一行就下结论。
- 合并相似项;每个小节内**按用户价值与影响范围排序,重要的在前**。
**2.3 报告**:展示新版本号与你写的 changelog 区段,请用户过目。
---
### Step 3: 构建前端
```shell
./cmd prod
```
构建前端生产版本。用 `./cmd prod`,不要换成裸跑 vite它还负责 node 检查、清 `public/js/build`、debug 切换)。
> **已知失败**build 报 `public/uploads/...``EACCES: permission denied, copyfile`,是 vite 复制 `public/` 时撞到 root 属主的运行时上传文件(不限于 `tmp``avatar` 等都可能)。补救是赋权、不是删数据——把 uploads 属主改回当前用户后重试:
> ```shell
> sudo chown -R "$(id -u):$(id -g)" public/uploads
> ```
> `public/uploads` 是真实上传数据,**不要删**;即便要清也只清 `public/uploads/tmp`
---
## 最终:提交并推送
所有步骤完成后:
1. 通过 `git diff` + `git status` 汇总所有变更,向用户报告摘要
2. **询问用户是否提交并推送**
3. 用户明确确认后才执行 `git add``git commit``git push`
4. 未确认一律不执行
提交规范:
- 提交信息使用 `release: v<新版本号>`(与历史一致,参见 `git log --oneline | grep '^release:'`
- **只 add 本次发布相关改动**,按文件名/目录显式添加(例如 `git add package.json CHANGELOG.md language/translate.json public/language public/js`),不要用 `git add -A` / `git add .`,以免卷入未跟踪的本地实验文件
- 不打 git tag现行发布流程不使用 tag
- 确认前先核对:`/tmp/dootask-release-translated.json` 等临时文件不在仓库内,工作区不应残留发布无关的未跟踪文件
## push 之后确认发布工作流CI 才是真正出包)
push 到 `pro` 只是触发器,真正的构建/出包由 GitHub Actions 完成——**push 成功 ≠ 发布完成**
- **Publish**`.github/workflows/publish.yml`push→pro 触发)跑完才算出包;成功后会自动触发 **Sync to Gitee**(镜像同步)。
- push 完成后**主动确认** Publish 工作流 `conclusion=success`。优先用 `gh`(未装可临时装;公开仓库也可用 GitHub REST API 免鉴权读取 runs
```shell
gh run list --workflow=publish.yml -R kuaifan/dootask -L 1
gh run view <run-id> -R kuaifan/dootask --json status,conclusion,url
```
- 工作流仍在跑时,挂后台轮询、结束即通知用户,**不要在前台死等**。
### iOS 发布(询问后决定)
`ios-publish.yml` 是**独立的手动工作流**`workflow_dispatch`),不随 push 触发。Publish 成功后,用 options 或 AskUserQuestion 形式提问是否同时发布 iOS选项发布 iOS / 不发布):
- 选「发布 iOS」才执行
```shell
gh workflow run ios-publish.yml --ref pro -R kuaifan/dootask
```
`gh` 已登录且 token 含 `workflow` 权限;触发后可挂后台轮询结果。
- 选「不发布」则结束。
## 失败处理
任何步骤失败立即停止、报告错误信息,交用户决定;不要自动重试或跳过。

View File

@ -0,0 +1,239 @@
<?php
// DooTask 发布——翻译流水线(纯本地 phphost 直接跑,不进容器、不调 OpenAI、不需 autoload
// 逐行对齐 language/translate.php 的检测/保存/生成逻辑,唯独把"调用外部模型翻译"那一段抽走,
// 翻译改在技能流程内完成。用 php 而非 node 的唯一原因array_multisort + json_encode
// 的逐字节产物必须与项目原生工具一致,否则每次发版都会产生大面积排序/转义噪声 diff已验证 host php 可字节级复现)。
//
// 子命令:
// language.php diff
// —— 输出 JSONneeds(待翻译key 已转成 (%T1)/(%M1) 形式) / redundants(冗余,提示) / regexErrors(占位符错乱,致命)
// language.php apply <translated.json>
// —— 把新翻译合并进 translate.json追加 + 剔除冗余),不生成 public 文件
// language.php generate
// —— 由 translate.json 重新生成 public/language/{web,api}/*
//
// 项目根相对脚本自身定位(脚本固定在 <root>/.claude/skills/dootask-release/scripts/),与调用时的 cwd 无关。
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
$ROOT = dirname(__DIR__, 4);
$LANG_DIR = $ROOT . '/language';
$LANG_FIELDS = ['key', 'zh', 'zh-CHT', 'en', 'ko', 'ja', 'de', 'fr', 'id', 'ru'];
if (!is_dir($LANG_DIR)) {
fwrite(STDERR, "未找到 language 目录($LANG_DIR。\n");
exit(1);
}
chdir($LANG_DIR);
$cmd = $argv[1] ?? '';
// ---- 公共:读取 original-*.txt ----
function read_generateds(): array
{
$originals = [];
$generateds = [];
foreach (['web', 'api'] as $type) {
$content = file_exists("original-{$type}.txt") ? file_get_contents("original-{$type}.txt") : "";
$array = array_values(array_filter(array_unique(explode("\n", $content))));
$generateds[$type] = $array;
$originals = array_merge($originals, $array);
}
return [$originals, $generateds];
}
// ---- 公共:构建 translations 映射normalizedKey -> obj并收集冗余/占位符错乱 ----
function build_translations(array $originals): array
{
$translations = [];
$redundants = [];
$regrror = [];
if (!file_exists("translate.json")) {
fwrite(STDERR, "translate.json not exists\n");
exit(1);
}
$tmps = json_decode(file_get_contents("translate.json"), true);
foreach ($tmps as $obj) {
if (!isset($obj['key'])) {
continue;
}
$currentKey = $obj['key'];
$originalKey = preg_replace(["/\(%T\d+\)/", "/\(%M\d+\)/"], ["(*)", "(**)"], $currentKey);
if (!in_array($originalKey, $originals)) {
$redundants[$originalKey] = $obj;
continue;
}
$translations[$originalKey] = $obj;
if (preg_match_all('/\(%[TM]\d+\)/', $currentKey, $matches)) {
foreach ($matches[0] as $match) {
foreach ($obj as $k => $v) {
if (empty($v)) {
continue;
}
if (!str_contains($v, $match)) {
$regrror[$originalKey] = ['key' => $currentKey, 'field' => $k, 'value' => $v, 'match' => $match];
continue 2;
}
}
}
}
}
return [$translations, $redundants, $regrror];
}
// ---- 公共:由 translate.json + originals 重新生成 public 文件 ----
function generate(array $generateds, array $translations): void
{
foreach ($generateds as $type => $array) {
$datas = [];
foreach ($array as $text) {
$text = trim($text);
if (isset($translations[$text])) {
$datas[] = $translations[$text];
}
}
$inOrder = [];
foreach ($datas as $index => $item) {
if (preg_match('/\(%[TM]\d+\)/', $item['key'])) {
$inOrder[$index] = strlen($item['key']);
} else {
$inOrder[$index] = strlen($item['key']) + 10000000000;
}
}
array_multisort($inOrder, SORT_DESC, $datas);
$results = [];
foreach ($datas as $items) {
foreach ($items as $kk => $item) {
$results[$kk][] = $item;
}
}
if ($type === 'api') {
if (!is_dir("../public/language/api")) {
mkdir("../public/language/api", 0777, true);
}
foreach ($results as $kk => $item) {
file_put_contents("../public/language/api/$kk.json", json_encode($item, JSON_UNESCAPED_UNICODE));
}
} elseif ($type === 'web') {
if (!is_dir("../public/language/web")) {
mkdir("../public/language/web", 0777, true);
}
foreach ($results as $kk => $item) {
file_put_contents("../public/language/web/$kk.js", "if(typeof window.LANGUAGE_DATA===\"undefined\")window.LANGUAGE_DATA={};window.LANGUAGE_DATA[\"{$kk}\"]=" . json_encode($item, JSON_UNESCAPED_UNICODE));
}
}
echo "[$type] total: " . count($results['key']) . "\n";
}
}
if ($cmd === 'diff') {
[$originals, $generateds] = read_generateds();
[$translations, $redundants, $regrror] = build_translations($originals);
// 需要翻译的数据(对齐 translate.php 150-169占位符按单一计数器编号
$needs = [];
foreach ($originals as $text) {
$key = trim($text);
if ($key === '') {
continue;
}
if (!isset($translations[$key])) {
$needs[$key] = $key;
}
}
$needsOut = [];
foreach ($needs as $key) {
$c = 1;
$converted = preg_replace_callback('/\((\*+)\)/', function ($m) use (&$c) {
$label = strlen($m[1]) > 1 ? "M" : "T";
return "(%" . $label . $c++ . ")";
}, $key);
$needsOut[] = ['key' => $converted];
}
echo json_encode([
'needsCount' => count($needsOut),
'redundantCount' => count($redundants),
'regexErrorCount' => count($regrror),
'needs' => $needsOut,
'redundants' => array_keys($redundants),
'regexErrors' => array_values($regrror),
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
if (count($regrror) > 0) {
exit(2); // 已有数据占位符错乱,需先修复
}
exit(0);
}
if ($cmd === 'apply') {
$file = $argv[2] ?? '';
if ($file === '' || !file_exists($file)) {
fwrite(STDERR, "用法apply <translated.json>(文件不存在)\n");
exit(1);
}
[$originals, $generateds] = read_generateds();
[$translations, $redundants, $regrror] = build_translations($originals);
if (count($regrror) > 0) {
fwrite(STDERR, "translate.json 已有条目占位符错乱,请先修复再发版。\n");
exit(2);
}
$incoming = json_decode(file_get_contents($file), true);
if (!is_array($incoming)) {
fwrite(STDERR, "translated.json 必须是数组\n");
exit(1);
}
$added = 0;
foreach ($incoming as $raw) {
foreach ($GLOBALS['LANG_FIELDS'] as $f) {
if (!array_key_exists($f, $raw)) {
fwrite(STDERR, "新翻译缺字段 \"$f\"" . json_encode($raw, JSON_UNESCAPED_UNICODE) . "\n");
exit(1);
}
}
// 占位符完整性key 里每个 (%T1)/(%M1) 必须出现在每个非空语言值里
if (preg_match_all('/\(%[TM]\d+\)/', $raw['key'], $m)) {
foreach ($m[0] as $match) {
foreach ($GLOBALS['LANG_FIELDS'] as $f) {
if ($f === 'key' || $f === 'zh') {
continue;
}
if (empty($raw[$f])) {
continue;
}
if (!str_contains($raw[$f], $match)) {
fwrite(STDERR, "占位符 $match 在字段 \"$f\" 缺失:{$raw['key']}\n");
exit(1);
}
}
}
}
// 规范化:固定字段顺序 + zh 置空
$item = [];
foreach ($GLOBALS['LANG_FIELDS'] as $f) {
$item[$f] = $f === 'zh' ? '' : $raw[$f];
}
$originalKey = preg_replace(["/\(%T\d+\)/", "/\(%M\d+\)/"], ["(*)", "(**)"], $item['key']);
$translations[$originalKey] = $item;
$added++;
}
// array_values现有条目去冗余在前新条目追加在后
file_put_contents("translate.json", json_encode(array_values($translations), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
echo json_encode([
'added' => $added,
'total' => count($translations),
'droppedRedundant' => count($redundants),
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
exit(0);
}
if ($cmd === 'generate') {
[$originals, $generateds] = read_generateds();
[$translations] = build_translations($originals);
generate($generateds, $translations);
exit(0);
}
fwrite(STDERR, "未知子命令:'$cmd'。可用diff | apply <file> | generate\n");
exit(1);

View File

@ -0,0 +1,47 @@
#!/usr/bin/env node
// 计算并写入新版本号到 package.jsonversion + codeVerson算法对齐 bin/version.js。
// 不生成 CHANGELOG在技能流程内撰写只输出版本号与 changelog 的提交区间。
//
// 项目根相对脚本自身定位(脚本固定在 <root>/.claude/skills/dootask-release/scripts/),与调用时的 cwd 无关。
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const ROOT = path.resolve(__dirname, '../../../..');
const pkgFile = path.join(ROOT, 'package.json');
const verOffset = 6394; // 版本号偏移量(与 bin/version.js 一致)
const codeOffset = 35; // 代码版本号偏移量
function git(cmd) {
return execSync(cmd, { cwd: ROOT, maxBuffer: 1024 * 1024 * 10 }).toString().trim();
}
const verCount = parseInt(git('git rev-list --count HEAD'), 10);
const codeCount = parseInt(git("git tag --merged pro -l 'v*' | wc -l"), 10);
const num = verOffset + verCount;
if (Number.isNaN(num)) {
console.error(`版本计算失败rev-list count=${verCount}`);
process.exit(1);
}
const version = `${Math.floor(num / 10000)}.${Math.floor((num % 10000) / 100)}.${Math.floor(num % 100)}`;
const codeVersion = codeOffset + codeCount;
let pkg = fs.readFileSync(pkgFile, 'utf8');
const prevVersion = (pkg.match(/"version":\s*"(.*?)"/) || [])[1] || '';
pkg = pkg.replace(/"version":\s*"(.*?)"/, `"version": "${version}"`);
pkg = pkg.replace(/"codeVerson":(.*?)(,|$)/, `"codeVerson": ${codeVersion}$2`);
fs.writeFileSync(pkgFile, pkg, 'utf8');
// 上一个 release 提交作为 changelog 区间下界
let prevReleaseCommit = '';
try {
prevReleaseCommit = git("git log --grep='^release: v' -n 1 --pretty=format:%H");
} catch (e) { /* ignore */ }
console.log(JSON.stringify({
version,
codeVersion,
prevVersion,
prevReleaseCommit,
changelogRange: prevReleaseCommit ? `${prevReleaseCommit}..HEAD` : '(未找到上一个 release 提交,需人工确定区间)',
}, null, 2));

View File

@ -0,0 +1,83 @@
---
name: dootask-update
description: 更新已部署的 DooTask前置检查后执行 `sudo ./cmd update`(拉代码 + composer + 迁移 + 重启),本地有改动时停下交用户决定,不自动强制、失败即停。
---
# DooTask 更新流程
**刚性技能**——前置检查 → 向用户确认一次 → 执行 → 报告结果。任何一步失败立即停止。
## 核心原则
**违反字面规则 = 违反流程精神。** 不要擅自加步骤、绕过 sudo/确认,**尤其不要替用户决定强制更新**(会丢本地改动)。
`./cmd update` 已封装整套更新(检测本地改动→`git fetch`→必要时备份库→`git pull/reset``composer install``migrate`→重启 php+nginx→写 `UPDATE_TIME`)。本技能职责是**更新前把关、选对参数、处理本地改动这一关键岔路、执行前确认**。
## 前置检查(全部通过才能继续)
1. **已安装**:必须存在 `vendor/autoload.php`(脚本会查,没装则报"请先执行安装命令"——此时引导用户走 dootask-install
2. **工作目录**:在项目根
3. **当前分支 / 目标分支**:默认更新当前分支;用户要切分支用 `--branch <分支>`。若用户没说,确认是否就更新当前分支
4. **本地改动**(关键):`git status` 看是否有未提交改动
5. **sudo**`sudo ./cmd update` 需 root
检查通过后汇报结果,**向用户确认一次**再执行。
## 关键岔路:本地有改动
脚本检测到本地改动时会询问是否强制更新。**强制更新 = `git reset --hard origin/<分支>`,会丢弃所有本地改动。**
- 发现本地有改动 → **停下**,把改动清单报告用户,让**用户决定**:先提交/暂存改动,还是确认强制更新
- **不要**替用户选 `--force`
- 只有用户明确说"丢掉改动强制更新"时,才带 `--force`
## 参数选择
| 参数 | 作用 | 何时用 |
|------|------|--------|
| `--branch <分支>` | 切到指定分支再更新 | 用户要换分支(如切 `dev`/`pro` |
| `--force` | 强制更新:`git checkout -f` + `git reset --hard` | **危险**:仅用户明确接受"丢弃本地改动"后 |
| `--local` | 本地更新模式:只备份库 + `migrate` + 重启,不拉远程代码 | 代码已就位(如手动改过/CI 拉过),只需迁移+重启 |
## 数据库
- 远程模式下,脚本检测到 `database/` 目录有迁移变动会**自动备份数据库**再继续——这是脚本内置的,无需手动。
- 但若是大版本升级或用户在意数据,执行前提醒用户:本次可能含库迁移,已有自动备份兜底;如需可先 `./cmd mysql backup` 额外备份。
## 执行
确认(含本地改动决策)后执行:
```shell
sudo ./cmd update
# 切分支: sudo ./cmd update --branch pro
# 强制(丢改动,用户确认后): sudo ./cmd update --force
# 本地模式: sudo ./cmd update --local
```
成功后报告:更新到的分支、是否做了库备份/迁移、服务是否重启完成。
## 失败处理
- 任何步骤失败立即停止,原样报告错误
- **不要**自动重试、不要自动跳过、不要因为 `git pull` 失败就自己改成 `--force`
- 常见失败:
- `请先执行安装命令` → 走 dootask-install
- `代码拉取失败,可能存在冲突` → 报告,让用户决定是否 `--force`(丢改动)或先处理冲突
- 重启服务失败 → 脚本会尝试 `down` 后重起;若仍失败,报告交用户
## 禁止项
| 错误做法 | 正确做法 |
|---------|---------|
| 检测到本地改动就自动 `--force` | 停下,报告改动,交用户决定 |
| `git pull` 失败就自动改用 `--force` | 报告冲突,交用户 |
| 不加 sudo | `sudo ./cmd update` |
| 未装就更新 | 先走 dootask-install |
| 失败后自动重试/跳过 | 立即停止 |
## Red Flags —— 出现这些念头立即停下
- "有点本地改动,强制更新一下就好了" → 不,`--force` 会丢改动,必须用户拍板
- "拉取冲突了,我 reset 一下" → 不,交用户决定
- "已经装过了吧,直接更新" → 先确认 `vendor/autoload.php`

View File

@ -10,13 +10,14 @@ APP_URL=http://localhost
APP_ID=
APP_IPPR=
APP_PORT=2222
APP_SSL_PORT=
APP_DEV_PORT=
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
@ -33,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
@ -56,9 +57,6 @@ PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
JUKE_KEY_JOKE=
JUKE_KEY_SOUP=
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

432
.github/workflows/ios-publish.yml vendored Normal file
View File

@ -0,0 +1,432 @@
name: "iOS Publish"
# Required GitHub Secrets:
#
# IOS_CERTIFICATE_BASE64 - Apple distribution certificate (.p12) encoded in base64
# IOS_CERTIFICATE_PASSWORD - Password for the .p12 certificate
# IOS_PROVISION_PROFILE_BASE64 - App Store provisioning profile (.mobileprovision) encoded in base64
# IOS_SHARE_EXTENSION_PROVISION_PROFILE_BASE64 - Share extension App Store provisioning profile (.mobileprovision) encoded in base64
# ASC_API_KEY_P8_BASE64 - App Store Connect API key (.p8) encoded in base64
# ASC_API_KEY_ID - App Store Connect API Key ID
# ASC_ISSUER_ID - App Store Connect Issuer ID
on:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ios-publish-${{ github.ref }}
cancel-in-progress: false
jobs:
prepare-assets:
name: Prepare iOS Assets
runs-on: ubuntu-latest
timeout-minutes: 30
outputs:
version: ${{ steps.get-version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Get version from package.json
id: get-version
run: |
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Version: $VERSION"
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm install
- name: Install electron dependencies
run: |
pushd electron
npm install
popd
- name: Init mobile submodule
run: |
git submodule init
git submodule update --remote "resources/mobile"
- name: Build app assets
run: ./cmd appbuild publish
- name: Upload iOS platform artifacts
uses: actions/upload-artifact@v4
with:
name: ios-platform
path: resources/mobile/platforms/ios/
retention-days: 1
build-ios:
name: Build & Submit iOS
needs: prepare-assets
runs-on: macos-26
timeout-minutes: 60
environment: build
steps:
- uses: actions/checkout@v4
- name: Init mobile submodule
run: |
git submodule init
git submodule update --remote "resources/mobile"
- name: Download prepared assets
uses: actions/download-artifact@v4
with:
name: ios-platform
path: resources/mobile/platforms/ios/
- name: Select Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: Install CocoaPods
run: |
if [ -f "resources/mobile/platforms/ios/eeuiApp/Podfile" ]; then
cd resources/mobile/platforms/ios/eeuiApp
pod install
fi
- name: Import signing certificate
env:
IOS_CERTIFICATE_BASE64: ${{ secrets.IOS_CERTIFICATE_BASE64 }}
IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
run: |
# Create temporary keychain
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
KEYCHAIN_PASSWORD=$(openssl rand -hex 20)
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
# Import certificate
CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12
echo "$IOS_CERTIFICATE_BASE64" | base64 --decode > "$CERTIFICATE_PATH"
security import "$CERTIFICATE_PATH" \
-P "$IOS_CERTIFICATE_PASSWORD" \
-A \
-t cert \
-f pkcs12 \
-k "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH"
- name: Import provisioning profile
env:
IOS_PROVISION_PROFILE_BASE64: ${{ secrets.IOS_PROVISION_PROFILE_BASE64 }}
IOS_SHARE_EXTENSION_PROVISION_PROFILE_BASE64: ${{ secrets.IOS_SHARE_EXTENSION_PROVISION_PROFILE_BASE64 }}
run: |
set -euo pipefail
APP_PROFILE_PATH=$RUNNER_TEMP/app.mobileprovision
SHARE_PROFILE_PATH=$RUNNER_TEMP/share-extension.mobileprovision
APP_PROFILE_PLIST=$RUNNER_TEMP/app-profile.plist
SHARE_PROFILE_PLIST=$RUNNER_TEMP/share-extension-profile.plist
echo "$IOS_PROVISION_PROFILE_BASE64" | base64 --decode > "$APP_PROFILE_PATH"
echo "$IOS_SHARE_EXTENSION_PROVISION_PROFILE_BASE64" | base64 --decode > "$SHARE_PROFILE_PATH"
security cms -D -i "$APP_PROFILE_PATH" > "$APP_PROFILE_PLIST"
security cms -D -i "$SHARE_PROFILE_PATH" > "$SHARE_PROFILE_PLIST"
APP_PROFILE_NAME=$(/usr/libexec/PlistBuddy -c "Print :Name" "$APP_PROFILE_PLIST")
SHARE_PROFILE_NAME=$(/usr/libexec/PlistBuddy -c "Print :Name" "$SHARE_PROFILE_PLIST")
IOS_TEAM_ID=$(/usr/libexec/PlistBuddy -c "Print :TeamIdentifier:0" "$APP_PROFILE_PLIST")
APP_PROFILE_APP_ID=$(/usr/libexec/PlistBuddy -c "Print :Entitlements:application-identifier" "$APP_PROFILE_PLIST")
SHARE_PROFILE_APP_ID=$(/usr/libexec/PlistBuddy -c "Print :Entitlements:application-identifier" "$SHARE_PROFILE_PLIST")
if [ "$APP_PROFILE_APP_ID" != "$IOS_TEAM_ID.com.dootask.task" ]; then
echo "Expected app profile for $IOS_TEAM_ID.com.dootask.task, got $APP_PROFILE_APP_ID"
exit 1
fi
if [ "$SHARE_PROFILE_APP_ID" != "$IOS_TEAM_ID.com.dootask.task.shareExtension" ]; then
echo "Expected share extension profile for $IOS_TEAM_ID.com.dootask.task.shareExtension, got $SHARE_PROFILE_APP_ID"
exit 1
fi
if ! /usr/libexec/PlistBuddy -c "Print :Entitlements:aps-environment" "$APP_PROFILE_PLIST" >/dev/null; then
echo "The DooTask app profile must include Push Notifications."
exit 1
fi
if ! /usr/libexec/PlistBuddy -c "Print :Entitlements:com.apple.security.application-groups" "$APP_PROFILE_PLIST" | grep -q "group.im.dootask"; then
echo "The DooTask app profile must include App Group group.im.dootask."
exit 1
fi
if ! /usr/libexec/PlistBuddy -c "Print :Entitlements:com.apple.security.application-groups" "$SHARE_PROFILE_PLIST" | grep -q "group.im.dootask"; then
echo "The share extension profile must include App Group group.im.dootask."
exit 1
fi
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp "$APP_PROFILE_PATH" ~/Library/MobileDevice/Provisioning\ Profiles/
cp "$SHARE_PROFILE_PATH" ~/Library/MobileDevice/Provisioning\ Profiles/
echo "APP_PROFILE_NAME=$APP_PROFILE_NAME" >> $GITHUB_ENV
echo "SHARE_PROFILE_NAME=$SHARE_PROFILE_NAME" >> $GITHUB_ENV
echo "IOS_TEAM_ID=$IOS_TEAM_ID" >> $GITHUB_ENV
- name: Configure manual signing
run: |
set -euo pipefail
ruby <<'RUBY'
require 'xcodeproj'
project_path = 'resources/mobile/platforms/ios/eeuiApp/eeuiApp.xcodeproj'
project = Xcodeproj::Project.open(project_path)
{
'DooTask' => ENV.fetch('APP_PROFILE_NAME'),
'ShareExtension' => ENV.fetch('SHARE_PROFILE_NAME')
}.each do |target_name, profile_name|
target = project.targets.find { |item| item.name == target_name }
abort "Target #{target_name} not found in #{project_path}" unless target
target.build_configurations.each do |config|
next unless config.name == 'Release'
config.build_settings['CODE_SIGN_STYLE'] = 'Manual'
config.build_settings['DEVELOPMENT_TEAM'] = ENV.fetch('IOS_TEAM_ID')
config.build_settings['CODE_SIGN_IDENTITY'] = 'Apple Distribution'
config.build_settings['PROVISIONING_PROFILE_SPECIFIER'] = profile_name
end
end
project.save
RUBY
- name: Resolve iOS build number
env:
ASC_API_KEY_ID: ${{ secrets.ASC_API_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_API_KEY_P8_BASE64: ${{ secrets.ASC_API_KEY_P8_BASE64 }}
run: |
set -euo pipefail
ruby <<'RUBY'
require 'base64'
require 'json'
require 'net/http'
require 'openssl'
require 'uri'
BUNDLE_ID = 'com.dootask.task'
VERSION_CONFIG_PATH = 'resources/mobile/platforms/ios/eeuiApp/Config/Version.xcconfig'
def base64url(value)
Base64.urlsafe_encode64(value).delete('=')
end
def jwt_es256_signature(private_key, unsigned)
der_signature = private_key.sign('SHA256', unsigned)
sequence = OpenSSL::ASN1.decode(der_signature)
sequence.value.map { |integer|
integer.value.to_s(2).rjust(32, "\0")[-32, 32]
}.join
end
def asc_token
key_id = ENV.fetch('ASC_API_KEY_ID')
issuer_id = ENV.fetch('ASC_ISSUER_ID')
private_key = OpenSSL::PKey.read(Base64.decode64(ENV.fetch('ASC_API_KEY_P8_BASE64')))
now = Time.now.to_i
header = { alg: 'ES256', kid: key_id, typ: 'JWT' }
payload = {
iss: issuer_id,
iat: now,
exp: now + 20 * 60,
aud: 'appstoreconnect-v1'
}
unsigned = "#{base64url(header.to_json)}.#{base64url(payload.to_json)}"
signature = jwt_es256_signature(private_key, unsigned)
"#{unsigned}.#{base64url(signature)}"
end
def asc_get(path, params, token)
uri = URI::HTTPS.build(
host: 'api.appstoreconnect.apple.com',
path: path,
query: URI.encode_www_form(params)
)
request_uri = uri
loop do
response = Net::HTTP.start(request_uri.host, request_uri.port, use_ssl: true) do |http|
request = Net::HTTP::Get.new(request_uri)
request['Authorization'] = "Bearer #{token}"
http.request(request)
end
unless response.is_a?(Net::HTTPSuccess)
abort "App Store Connect API request failed: #{response.code} #{response.body}"
end
parsed = JSON.parse(response.body)
yield parsed
next_link = parsed.dig('links', 'next')
break unless next_link
request_uri = URI(next_link)
end
end
token = asc_token
app_id = nil
asc_get('/v1/apps', { 'filter[bundleId]' => BUNDLE_ID, 'limit' => 1 }, token) do |page|
app_id = page.fetch('data').first&.fetch('id')
end
abort "App Store Connect app not found for bundle id #{BUNDLE_ID}" unless app_id
existing_versions = []
asc_get('/v1/builds', {
'filter[app]' => app_id,
'fields[builds]' => 'version',
'limit' => 200
}, token) do |page|
existing_versions.concat(
page.fetch('data').map { |build| build.dig('attributes', 'version').to_s }
)
end
max_build_number = existing_versions
.select { |version| version.match?(/\A\d+\z/) }
.map(&:to_i)
.max || 0
next_build_number = max_build_number + 1
config_content = File.exist?(VERSION_CONFIG_PATH) ? File.read(VERSION_CONFIG_PATH) : ''
if config_content.match?(/^VERSION_CODE\s*=/)
config_content = config_content.gsub(/^VERSION_CODE\s*=.*$/, "VERSION_CODE = #{next_build_number}")
else
config_content = "#{config_content.rstrip}\nVERSION_CODE = #{next_build_number}\n"
end
File.write(VERSION_CONFIG_PATH, config_content)
File.open(ENV.fetch('GITHUB_ENV'), 'a') { |file| file.puts "IOS_BUILD_NUMBER=#{next_build_number}" }
puts "Latest App Store Connect build number: #{max_build_number}"
puts "Resolved iOS build number: #{next_build_number}"
RUBY
- name: Build archive
run: |
set -euo pipefail
cd resources/mobile/platforms/ios/eeuiApp
xcodebuild archive \
-workspace eeuiApp.xcworkspace \
-scheme eeuiApp \
-configuration Release \
-destination "generic/platform=iOS" \
-archivePath $RUNNER_TEMP/eeuiApp.xcarchive \
-allowProvisioningUpdates \
DEVELOPMENT_TEAM=$IOS_TEAM_ID \
CODE_SIGN_IDENTITY="Apple Distribution" \
CODE_SIGN_STYLE=Manual \
| xcpretty
if [ ! -d "$RUNNER_TEMP/eeuiApp.xcarchive" ]; then
echo "Archive was not created at $RUNNER_TEMP/eeuiApp.xcarchive"
exit 1
fi
- name: Export IPA
run: |
set -euo pipefail
cd resources/mobile/platforms/ios/eeuiApp
# Generate ExportOptions.plist
cat > $RUNNER_TEMP/ExportOptions.plist << PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string>
<key>signingStyle</key>
<string>manual</string>
<key>teamID</key>
<string>${IOS_TEAM_ID}</string>
<key>provisioningProfiles</key>
<dict>
<key>com.dootask.task</key>
<string>${APP_PROFILE_NAME}</string>
<key>com.dootask.task.shareExtension</key>
<string>${SHARE_PROFILE_NAME}</string>
</dict>
<key>uploadBitcode</key>
<false/>
<key>uploadSymbols</key>
<true/>
</dict>
</plist>
PLIST
xcodebuild -exportArchive \
-archivePath $RUNNER_TEMP/eeuiApp.xcarchive \
-exportOptionsPlist $RUNNER_TEMP/ExportOptions.plist \
-exportPath $RUNNER_TEMP/ipa-output \
-allowProvisioningUpdates \
| xcpretty
- name: Submit to App Store Connect
env:
ASC_API_KEY_ID: ${{ secrets.ASC_API_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_API_KEY_P8_BASE64: ${{ secrets.ASC_API_KEY_P8_BASE64 }}
run: |
set -euo pipefail
# Prepare API key
mkdir -p ~/private_keys
echo "$ASC_API_KEY_P8_BASE64" | base64 --decode > ~/private_keys/AuthKey_${ASC_API_KEY_ID}.p8
# Find and upload IPA
IPA_PATH=$(find $RUNNER_TEMP/ipa-output -name "*.ipa" | head -1)
if [ -z "$IPA_PATH" ]; then
echo "No IPA file found in $RUNNER_TEMP/ipa-output"
exit 1
fi
echo "Uploading: $IPA_PATH"
xcrun altool --upload-app \
-f "$IPA_PATH" \
--type ios \
--apiKey "$ASC_API_KEY_ID" \
--apiIssuer "$ASC_ISSUER_ID"
- name: Clean up
if: always()
run: |
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db 2>/dev/null || true
rm -f $RUNNER_TEMP/certificate.p12
rm -f $RUNNER_TEMP/app.mobileprovision
rm -f $RUNNER_TEMP/share-extension.mobileprovision
rm -f $RUNNER_TEMP/app-profile.plist
rm -f $RUNNER_TEMP/share-extension-profile.plist
rm -rf ~/private_keys

View File

@ -1,33 +0,0 @@
name: Publish Desktop
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: macos-latest
environment: build
if: startsWith(github.event.ref, 'refs/tags/v')
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Use Node.js 16.x
uses: actions/setup-node@v1
with:
node-version: 16.x
- name: Build
env:
APPLEID: ${{ secrets.APPLEID }}
APPLEIDPASS: ${{ secrets.APPLEIDPASS }}
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
DP_KEY: ${{ secrets.DP_KEY }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GH_REPOSITORY: ${{ github.repository }}
run: ./cmd electron all

286
.github/workflows/publish.yml vendored Normal file
View File

@ -0,0 +1,286 @@
name: "Publish"
on:
push:
branches:
- "pro"
jobs:
check-version:
permissions:
contents: read
runs-on: ubuntu-latest
outputs:
should_release: ${{ steps.check-tag.outputs.should_release }}
version: ${{ steps.get-version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Get version from package.json
id: get-version
run: |
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Check if tag exists
id: check-tag
run: |
VERSION=${{ steps.get-version.outputs.version }}
if git ls-remote --tags origin | grep -q "refs/tags/v${VERSION}$"; then
echo "This version v${VERSION} has been released"
echo "should_release=false" >> $GITHUB_OUTPUT
else
echo "Version v${VERSION} has not been released, continue building"
echo "should_release=true" >> $GITHUB_OUTPUT
fi
create-release:
needs: check-version
if: needs.check-version.outputs.should_release == 'true'
permissions:
contents: write
runs-on: ubuntu-latest
outputs:
release_id: ${{ steps.create-release.outputs.result }}
steps:
- uses: actions/checkout@v4
- name: Create Release
id: create-release
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const version = '${{ needs.check-version.outputs.version }}';
// 从 CHANGELOG.md 提取当前版本段落
let changelog = '';
const changelogPath = 'CHANGELOG.md';
if (fs.existsSync(changelogPath)) {
const content = fs.readFileSync(changelogPath, 'utf-8');
const regex = new RegExp(`## \\[${version.replace(/\./g, '\\.')}\\][\\s\\S]*?(?=\\n## \\[|$)`);
const match = content.match(regex);
if (match) {
changelog = match[0].trim();
}
}
// 创建 release
const { data } = await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: `v${version}`,
name: version,
body: changelog || 'No significant changes in this release.',
draft: true,
prerelease: false
})
return data.id
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:
fail-fast: false
matrix:
include:
- platform: "macos-latest"
build_type: "mac"
- platform: "ubuntu-latest"
build_type: "android"
- platform: "windows-latest"
build_type: "windows"
runs-on: ${{ matrix.platform }}
environment: build
steps:
- uses: actions/checkout@v4
- name: Use Node.js 20.x
uses: actions/setup-node@v4
with:
node-version: 20.x
# Android 构建步骤
- name: (Android) Build Js
if: matrix.build_type == 'android'
uses: nick-fields/retry@v2
with:
timeout_minutes: 10
max_attempts: 3
command: |
git submodule init
git submodule update --remote "resources/mobile"
./cmd appbuild publish
- name: (Android) Setup JDK 11
if: matrix.build_type == 'android'
uses: actions/setup-java@v3
with:
distribution: "zulu"
java-version: "11"
- name: (Android) Build App
if: matrix.build_type == 'android'
uses: nick-fields/retry@v2
with:
timeout_minutes: 20
max_attempts: 5
command: |
cd resources/mobile/platforms/android/eeuiApp
chmod +x ./gradlew
./gradlew assembleRelease --quiet
- name: (Android) Upload File
if: matrix.build_type == 'android'
env:
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
R2_BUCKET: ${{ secrets.R2_BUCKET }}
R2_PUBLIC_URL: ${{ secrets.R2_PUBLIC_URL }}
run: |
node ./electron/build.js android-upload
- name: (Android) Upload Release
if: matrix.build_type == 'android'
uses: actions/github-script@v7
env:
RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
with:
script: |
const fs = require('fs');
const path = require('path');
const globby = require('globby');
// 查找 APK 文件
const files = await globby('resources/mobile/platforms/android/eeuiApp/app/build/outputs/apk/release/*.apk');
for (const file of files) {
const data = await fs.promises.readFile(file);
await github.rest.repos.uploadReleaseAsset({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: process.env.RELEASE_ID,
name: path.basename(file),
data: data
});
}
# Mac 构建步骤
- name: (Mac) Build Client
if: matrix.build_type == 'mac'
env:
APPLEID: ${{ secrets.APPLEID }}
APPLEIDPASS: ${{ secrets.APPLEIDPASS }}
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
R2_BUCKET: ${{ secrets.R2_BUCKET }}
R2_PUBLIC_URL: ${{ secrets.R2_PUBLIC_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
./cmd electron mac
# Windows 构建步骤
- name: (Windows) Build Client
if: matrix.build_type == 'windows'
env:
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
R2_BUCKET: ${{ secrets.R2_BUCKET }}
R2_PUBLIC_URL: ${{ secrets.R2_PUBLIC_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
shell: bash
run: |
./cmd electron win
publish-release:
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
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Publish Release
uses: actions/github-script@v7
env:
RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
with:
script: |
github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: process.env.RELEASE_ID,
draft: false,
prerelease: false
})
- name: Upload Changelog & Publish to Website
env:
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
R2_BUCKET: ${{ secrets.R2_BUCKET }}
R2_PUBLIC_URL: ${{ secrets.R2_PUBLIC_URL }}
run: |
pushd electron || exit
npm install
popd || exit
node ./electron/build.js upload-changelog
node ./electron/build.js release

45
.github/workflows/sync-gitee.yml vendored Normal file
View File

@ -0,0 +1,45 @@
name: "Sync to Gitee"
# Required GitHub Secrets:
#
# GITEE_SSH_PRIVATE_KEY - SSH private key with push access to gitee.com/aipaw/dootask
on:
workflow_run:
workflows: ["Publish"]
types:
- completed
jobs:
sync:
name: Push to Gitee
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup SSH key
env:
GITEE_SSH_PRIVATE_KEY: ${{ secrets.GITEE_SSH_PRIVATE_KEY }}
run: |
mkdir -p ~/.ssh
echo "$GITEE_SSH_PRIVATE_KEY" > ~/.ssh/gitee_key
chmod 600 ~/.ssh/gitee_key
cat >> ~/.ssh/config << EOF
Host gitee.com
HostName gitee.com
IdentityFile ~/.ssh/gitee_key
StrictHostKeyChecking no
EOF
- name: Push to Gitee
run: |
git remote add gitee git@gitee.com:aipaw/dootask.git
git push gitee pro
- name: Clean up
if: always()
run: rm -rf ~/.ssh/gitee_key

57
.gitignore vendored
View File

@ -1,28 +1,67 @@
# Dependencies
/node_modules
/vendor
# Build and temporary files
/build
/public/hot
/public/tmp
/tmp
/backup
# Uploads and user-generated content
/public/summary
/public/uploads/*
/public/.well-known
/public/.user.ini
/storage/*.key
# Storage and configuration
/config/LICENSE
/vendor
/build
/tmp
._*
/storage/*.key
# Environment and configuration
.env
vars.yaml
# IDE and editor files
.cursor/*
!.cursor/rules/
!.cursor/rules/**
.idea
.vscode
.windsurfrules
# Development tools
.vagrant
.phpunit.result.cache
Homestead.json
Homestead.yaml
# Development file
/index.html
# Testing
.phpunit.result.cache
test.*
# Logs and debug files
npm-debug.log
yarn-error.log
test.*
# Lock files
dootask.lock
package-lock.json
# Laravel/Swoole specific
laravels-timer-process.pid
.DS_Store
vars.yaml
laravels.conf
laravels.pid
# System files
._*
.DS_Store
# Documentation
README_LOCAL.md
# playwright
.playwright-mcp/

View File

@ -1,13 +0,0 @@
# This configuration file was automatically generated by Gitpod.
# Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file)
# and commit this file to your remote git repository to share the goodness with others.
tasks:
- init: sudo ./cmd install
command: ./cmd dev
ports:
- port: 2222
visibility: public
- port: 22222
visibility: public

170
.prefetch Normal file
View File

@ -0,0 +1,170 @@
office/web-apps/apps/api/documents/api.js?hash={version}
office/{path}/fonts/000
office/{path}/fonts/001
office/{path}/fonts/002
office/{path}/fonts/020
office/{path}/fonts/022
office/{path}/fonts/023
office/{path}/fonts/024
office/{path}/fonts/027
office/{path}/fonts/028
office/{path}/fonts/029
office/{path}/fonts/030
office/{path}/fonts/036
office/{path}/fonts/037
office/{path}/fonts/038
office/{path}/fonts/039
office/{path}/fonts/050
office/{path}/fonts/051
office/{path}/fonts/052
office/{path}/fonts/053
office/{path}/fonts/058
office/{path}/fonts/059
office/{path}/fonts/060
office/{path}/fonts/061
office/{path}/fonts/062
office/{path}/fonts/063
office/{path}/fonts/064
office/{path}/fonts/065
office/{path}/fonts/066
office/{path}/fonts/067
office/{path}/fonts/068
office/{path}/fonts/069
office/{path}/fonts/070
office/{path}/fonts/071
office/{path}/fonts/072
office/{path}/fonts/073
office/{path}/fonts/074
office/{path}/fonts/075
office/{path}/fonts/076
office/{path}/fonts/077
office/{path}/fonts/078
office/{path}/fonts/079
office/{path}/fonts/080
office/{path}/fonts/081
office/{path}/fonts/086
office/{path}/fonts/091
office/{path}/fonts/092
office/{path}/fonts/093
office/{path}/fonts/094
office/{path}/fonts/095
office/{path}/fonts/096
office/{path}/fonts/097
office/{path}/fonts/098
office/{path}/fonts/099
office/{path}/fonts/100
office/{path}/fonts/101
office/{path}/fonts/102
office/{path}/fonts/103
office/{path}/fonts/131
office/{path}/fonts/132
office/{path}/fonts/133
office/{path}/fonts/134
office/{path}/fonts/135
office/{path}/fonts/136
office/{path}/fonts/137
office/{path}/fonts/138
office/{path}/fonts/139
office/{path}/fonts/140
office/{path}/fonts/141
office/{path}/fonts/142
office/{path}/fonts/143
office/{path}/fonts/145
office/{path}/fonts/147
office/{path}/fonts/152
office/{path}/fonts/154
office/{path}/fonts/177
office/{path}/fonts/178
office/{path}/fonts/179
office/{path}/fonts/180
office/{path}/fonts/181
office/{path}/fonts/182
office/{path}/fonts/183
office/{path}/fonts/184
office/{path}/fonts/185
office/{path}/fonts/186
office/{path}/fonts/187
office/{path}/fonts/188
office/{path}/fonts/189
office/{path}/fonts/190
office/{path}/fonts/191
office/{path}/fonts/192
office/{path}/fonts/193
office/{path}/fonts/198
office/{path}/fonts/199
office/{path}/fonts/200
office/{path}/fonts/201
office/{path}/fonts/202
office/{path}/fonts/203
office/{path}/fonts/204
office/{path}/fonts/205
office/{path}/fonts/206
office/{path}/fonts/207
office/{path}/fonts/208
office/{path}/fonts/209
office/{path}/fonts/210
office/{path}/fonts/211
office/{path}/fonts/212
office/{path}/fonts/214
office/{path}/fonts/215
office/{path}/fonts/216
office/{path}/fonts/217
office/{path}/sdkjs/cell/sdk-all-min.js
office/{path}/sdkjs/cell/sdk-all.js
office/{path}/sdkjs/common/AllFonts.js
office/{path}/sdkjs/common/AllFonts.js
office/{path}/sdkjs/common/AllFonts.js
office/{path}/sdkjs/common/Charts/ChartStyles.js
office/{path}/sdkjs/common/Charts/ChartStyles.js
office/{path}/sdkjs/common/Charts/ChartStyles.js
office/{path}/sdkjs/common/Images/fonts_thumbnail_ea@2x.png.bin
office/{path}/sdkjs/common/Images/fonts_thumbnail_ea@2x.png.bin
office/{path}/sdkjs/common/Images/fonts_thumbnail_ea@2x.png.bin
office/{path}/sdkjs/common/libfont/engine/fonts.js
office/{path}/sdkjs/common/libfont/engine/fonts.js
office/{path}/sdkjs/common/libfont/engine/fonts.js
office/{path}/sdkjs/common/libfont/engine/fonts.wasm
office/{path}/sdkjs/common/libfont/engine/fonts.wasm
office/{path}/sdkjs/common/libfont/engine/fonts.wasm
office/{path}/sdkjs/slide/sdk-all-min.js
office/{path}/sdkjs/slide/sdk-all.js
office/{path}/sdkjs/word/sdk-all-min.js
office/{path}/sdkjs/word/sdk-all.js
office/{path}/web-apps/apps/documenteditor/main/app.js
office/{path}/web-apps/apps/documenteditor/main/code.js
office/{path}/web-apps/apps/documenteditor/main/locale/zh.json
office/{path}/web-apps/apps/documenteditor/main/resources/css/app.css
office/{path}/web-apps/apps/documenteditor/main/resources/img/iconssmall@2.5x.svg
office/{path}/web-apps/apps/presentationeditor/main/app.js
office/{path}/web-apps/apps/presentationeditor/main/code.js
office/{path}/web-apps/apps/presentationeditor/main/locale/zh.json
office/{path}/web-apps/apps/presentationeditor/main/resources/css/app.css
office/{path}/web-apps/apps/presentationeditor/main/resources/img/iconsbig@2.5x.svg
office/{path}/web-apps/apps/presentationeditor/main/resources/img/iconsbig@2x.png
office/{path}/web-apps/apps/presentationeditor/main/resources/img/iconssmall@2.5x.svg
office/{path}/web-apps/apps/spreadsheeteditor/main/app.js
office/{path}/web-apps/apps/spreadsheeteditor/main/code.js
office/{path}/web-apps/apps/spreadsheeteditor/main/locale/zh.json
office/{path}/web-apps/apps/spreadsheeteditor/main/resources/css/app.css
office/{path}/web-apps/apps/spreadsheeteditor/main/resources/formula-lang/zh_desc.json
office/{path}/web-apps/apps/spreadsheeteditor/main/resources/img/iconssmall@2.5x.svg
office/{path}/web-apps/apps/spreadsheeteditor/main/resources/img/iconssmall@2x.png
office/{path}/web-apps/vendor/xregexp/xregexp-all-min.js
office/{path}/web-apps/vendor/xregexp/xregexp-all-min.js
office/{path}/web-apps/vendor/xregexp/xregexp-all-min.js
drawio/webapp/js/app.min.js
drawio/webapp/js/extensions.min.js
drawio/webapp/js/shapes-14-6-5.min.js
drawio/webapp/js/stencils.min.js
drawio/webapp/math/es5/core.js
drawio/webapp/math/es5/input/asciimath.js
drawio/webapp/math/es5/input/tex.js
drawio/webapp/math/es5/output/svg.js
drawio/webapp/math/es5/output/svg/fonts/tex.js
drawio/webapp/styles/grapheditor.css
minder/css/chunk-vendors.fe9c56c6.css
minder/js/app.aa385de3.js
minder/js/chunk-vendors.cc7455b8.js

1
AGENTS.md Symbolic link
View File

@ -0,0 +1 @@
CLAUDE.md

File diff suppressed because it is too large Load Diff

62
CLAUDE.md Normal file
View File

@ -0,0 +1,62 @@
## 项目概述
Laravel 8 (LaravelS/Swoole) + Vue 2 (Vite) + Electron。开源任务/项目管理系统。
## 开发命令
所有命令通过 `./cmd` 脚本执行(不要直接运行 `php artisan` 等):
- `./cmd artisan ...` / `./cmd composer ...` / `./cmd php ...` — PHP 相关命令
### AI 不要主动执行的命令
以下命令仅由用户人工触发AI 不要主动跑——包括"任务完成后 sanity check"、"看下能不能编译"等场景:
- `./cmd dev` — 用户已自行运行 dev server改完会自己 reloadAI 再跑会争抢进程
- `./cmd prod` / `./cmd build` — 发版才用,走 `/release` 流程
前端代码改动只做 Edit/Write不要为了"验证"启动 dev server。用户明确说"跑一下 / 出包"时除外。
## Gotchas
### LaravelS/Swoole
- **避免在静态属性、单例、全局变量中存储请求级状态**——请求间共享进程,会导致数据串联和内存泄漏
- 构造函数、服务提供者、`boot()` 方法不会在每个请求重新执行
- 配置/路由变更需要 `./cmd php restart` 或容器重启才能生效
- 长生命周期逻辑WebSocket、定时器应复用现有模式避免阻塞协程/事件循环
### 后端
- **非 REST 路由**API 控制器(继承 `InvokeController`)在 `routes/web.php` 按资源注册路由URL 段映射为控制器方法(如 `api/project/lists``lists()`,带 action 则用双下划线:`api/project/invite/join``invite__join()`
- 路由最多两段:方法名最多一个双下划线(`method__action`),不支持 `method__action__xxx`(无对应路由,访问 404
- **响应格式**:统一使用 `Base::retSuccess($msg, $data)` / `Base::retError($msg)`,返回 `{"ret": 1, "msg": "...", "data": {...}}`——不要用 `response()->json()`
- 业务异常通过 `App\Exceptions\ApiException` 抛出,不要用通用 Exception
- 模型继承 `AbstractModel`,使用 `Model::createInstance($params)` 创建——不要用 `new Model()``Model::create()`
- 认证使用 `Doo::userId()`——不要用 `auth()->user()`
- 参数校验在控制器方法中手动进行——不要创建 FormRequest 类
- 异步任务使用 Swoole Task`app/Tasks/`)——不要用 Laravel Queue
- `app/Module/` 存放跨控制器/跨模型的业务逻辑(非标准 Laravel 目录)
- 所有表结构变更必须通过 Laravel migration禁止直接改库
### 前端
- API 调用使用 `store.dispatch("call", params)`,不要在组件中直接 axios/fetch
- `$A.modalXXX``$A.messageXXX``$A.noticeXXX` 内部自动处理 `$L` 翻译,调用方不要额外包 `$L`。仅当传入 `language: false` 时由调用方自行处理翻译
### 国际化
- 新增用户可见文本须追加原文(简体中文)到:前端 `language/original-web.txt`,后端 `language/original-api.txt`(去重)
- 前端翻译用 `$L("文本")`,动态值用 `(*)` 占位:`$L('共(*)条', n)`——禁止拼接翻译
## Playwright 测试
- Playwright 测试结果放在 `tests/playwright-results/`,包含测试环境、测试用例、结果截图等信息
## 交互规范
- **提问时附带建议**:当需要向用户提问或请求澄清时,应同时提供具体的建议选项或推荐方案,帮助用户快速决策,而非仅抛出开放式问题
## 语言偏好
- 回复一律使用简体中文,除非用户明确要求其他语言

146
README.md
View File

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

View File

@ -1,21 +0,0 @@
# 客户端说明
## 1、App客户端
#### 1.1、说明
目录 `resources/mobile`,使用`eeui.app`框架遵从eeui的开发文档进行打包开发app
#### 1.2、编译App
1. 在项目目录执行 `./cmd appbuild` 编译
2. 进入 `resources/mobile` eeui框架内打包Android或iOS应用
## 2、PC/Mac客户端
#### 2.1、说明
目录 `electron`,使用`electron`框架遵从electron的开发文档进行打包客户端
#### 2.2、编译客户端
在项目目录执行 `./cmd electron` 根据提示编译

View File

@ -1,34 +1,44 @@
# Install (Docker)
# DooTask - 开源任务管理系统
**[English](./README.md)** | 中文文档
- [截图预览](README_PREVIEW.md)
- [截图预览](./README_PREVIEW.md)
- [演示站点](http://www.dootask.com/)
**QQ交流群**
- QQ群号: `546574618`
## 📍 0.x 迁移到 1.x
- 升级时请务必备份好数据!
- 如果升级失败请尝试执行 `./cmd update` 重试几次。
- 如果升级中出现 `没有找到 xxx 容器` 的提示,请运行 `./cmd reup` 后再执行 `./cmd update`
- 如果升级后出现502错误请运行 `./cmd reup` 重启服务即可。
- 如果升级后出现 `应用「xxx」未安装` 的提示,请使用管理员账号进入应用商店安装相关应用。
## 安装程序
- 必须安装:`Docker v20.10+``Docker Compose v2.0+`
- 支持环境:`Centos/Debian/Ubuntu/macOS`
- 支持环境:`Centos/Debian/Ubuntu/macOS` 等 linux/unix 系统
- 硬件建议2核4G以上
- 数据库MariaDB默认 Docker Compose 中的 `mariadb` 服务)
- 特别说明Windows 可以使用 WSL2 安装 Linux 环境后再安装 DooTask。
### 部署项目Pro版
### 部署项目
```bash
# 1、克隆项目到您的本地或服务器
# 通过github克隆项目
git clone -b pro --depth=1 https://github.com/kuaifan/dootask.git
git clone --depth=1 https://github.com/kuaifan/dootask.git
# 或者你也可以使用gitee
git clone -b pro --depth=1 https://gitee.com/aipaw/dootask.git
git clone --depth=1 https://gitee.com/aipaw/dootask.git
# 2、进入目录
cd dootask
# 3、一键安装项目自定义端口安装 ./cmd install --port 2222
# 3、一键安装项目自定义端口安装,如:./cmd install --port 80
./cmd install
```
@ -42,54 +52,44 @@ cd dootask
### 更换端口
```bash
./cmd port 2222
```
### 更换URL
```bash
# 此地址仅影响邮件回复功能
./cmd url {域名地址}
# 例如:
./cmd url https://domain.com
# 此方法仅更换http端口更换https端口请阅读下面SSL配置
./cmd port 80
```
### 停止服务
```bash
./cmd stop
./cmd down
```
# 一旦应用程序被设置,无论何时你想要启动服务器(如果它被停止)运行以下命令
./cmd start
### 启动服务
```bash
./cmd up
```
### 开发编译
请确保你已经安装了 `NodeJs 20+`
```bash
# 开发模式仅限macOS
# 开发模式
./cmd dev
# 编译项目仅限macOS
# 编译项目(这是网页端的,客户端请参考“.github/workflows/publish.yml”文件
./cmd prod
```
### SSL 配置
### 运行命令的快捷方式
#### 方法1自动配置
```bash
# 你可以使用以下命令来执行
./cmd artisan "your command" # 运行 artisan 命令
./cmd php "your command" # 运行 php 命令
./cmd nginx "your command" # 运行 nginx 命令
./cmd redis "your command" # 运行 redis 命令
./cmd composer "your command" # 运行 composer 命令
./cmd supervisorctl "your command" # 运行 supervisorctl 命令
./cmd test "your command" # 运行 phpunit 命令
./cmd mysql "your command" # 运行 mysql 命令 (backup: 备份数据库recovery: 还原数据库)
```bash
# 执行指令,根据提示执行即可
./cmd https
```
### NGINX 代理 SSL
#### 方法2Nginx 代理配置
```bash
# 1、Nginx 代理配置添加
@ -97,8 +97,8 @@ proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 2、在项目下运行命令
./cmd https
# 2、执行指令(如果取消 Nginx 代理配置请运行:./cmd https close
./cmd https agent
```
## 升级更新
@ -106,42 +106,45 @@ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
**注意:在升级之前请备份好你的数据!**
```bash
# 方法1在项目下运行命令
./cmd update
# 或者方法2如果方法1失败请使用此方法
git pull
./cmd mysql backup
./cmd uninstall
./cmd install
./cmd mysql recovery
```
* 跨越大版本升级失败时请重试执行一次。
* 如果升级后出现502请运行 `./cmd restart` 重启服务即可。
* 如果升级后出现502请运行 `./cmd reup` 重启服务即可。
## 迁移项目
在新项目安装好之后按照以下步骤完成项目迁移:
1、备份数据库
1、备份 MariaDB 数据库
```bash
# 在旧的项目下运行命
# 在旧的项目下执行指
./cmd mysql backup
```
2、将`数据库备份文件``public/uploads`目录拷贝至新项目
> `./cmd mysql` 为 CLI 子命令名称,实际操作的是 MariaDB 容器。
2、将旧项目以下文件和目录拷贝至新项目同路径位置
- `数据库备份文件`
- `docker/appstore`
- `public/uploads`
3、还原数据库至新项目
```bash
# 在新的项目下运行命令
# 在新的项目下执行指
./cmd mysql recovery
```
## 卸载项目
```bash
# 在项目下运行命令
./cmd uninstall
```
### 更多指令
```bash
./cmd help
```

View File

@ -1,26 +1,31 @@
# 发布说明
# 发布
## 发布前
## 准备工作
1. 添加环境变量 `APPLEID``APPLEIDPASS` 用于公证
2. 添加环境变量 `CSC_LINK``CSC_KEY_PASSWORD` 用于签名
3. 添加环境变量 `GH_TOKEN`、`GH_REPOSITORY` 用于发布到GitHub
4. 添加环境变量 `DP_KEY` 用于发布到私有服务器
3. 添加环境变量 `GITHUB_TOKEN`、`GITHUB_REPOSITORY` 用于发布到GitHubGitHub Actions 发布不需要)
4. 添加环境变量 `PUBLISH_KEY` 用于发布到私有服务器
## 通过 GitHub Actions 发布
## 发布版本
1. 执行 `npm run version` 生成版本
2. 执行 `npm run build` 编译前端
3. 执行 `git commit` 提交并推送
4. 添加并推送标签
> 翻译、版本号、更新日志改由 `dootask-release` 技能完成(见 `.claude/skills/dootask-release/`)。
## 本地发布
```shell
npm run build # 编译前端
```
1. 执行 `npm run version` 生成版本
2. 执行 `npm run build` 编译前端
3. 执行 `./cmd electron` 相关操作
说明:
## 编译App
- 执行 `npm run build` 作用是生成网页端;
- 客户端 (Windows、Mac、Android) 会通过 GitHub Actions 自动生成并发布所以如果要自动发布只需要提交git并推送即可
- 如果想手动生成客户端执行 `./cmd electron` 根据提示选择操作。
1. 执行 `./cmd appbuild``./cmd appbuild setting` 编译
2. 进入 `resources/mobile` eeui框架内打包Android或iOS应用
## 编译 App
```shell
./cmd appbuild publish # 编译生成App需要的资源
```
编译完后进入 `resources/mobile` EEUI框架目录内打包 Android 或 iOS 应用Android 以实现 GitHub Actions 自动发布)

View File

@ -16045,7 +16045,7 @@
/**
*
*
* @see \Maatwebsite\Excel\Mixins\DownloadCollection::downloadExcel()
* @see \Maatwebsite\Excel\Mixins\DownloadCollectionMixin::downloadExcel()
* @param string $fileName
* @param string|null $writerType
* @param mixed $withHeadings
@ -16059,7 +16059,7 @@
/**
*
*
* @see \Maatwebsite\Excel\Mixins\StoreCollection::storeExcel()
* @see \Maatwebsite\Excel\Mixins\StoreCollectionMixin::storeExcel()
* @param string $filePath
* @param string|null $disk
* @param string|null $writerType
@ -16439,6 +16439,247 @@
}
}
namespace Laravolt\Avatar {
/**
*
*
*/
class Facade {
/**
*
*
* @static
*/
public static function setGenerator($generator)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->setGenerator($generator);
}
/**
*
*
* @static
*/
public static function create($name)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->create($name);
}
/**
*
*
* @static
*/
public static function applyTheme($config)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->applyTheme($config);
}
/**
*
*
* @static
*/
public static function addTheme($name, $config)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->addTheme($name, $config);
}
/**
*
*
* @static
*/
public static function toBase64()
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->toBase64();
}
/**
*
*
* @static
*/
public static function save($path, $quality = 90)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->save($path, $quality);
}
/**
*
*
* @static
*/
public static function toSvg()
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->toSvg();
}
/**
*
*
* @static
*/
public static function toGravatar($param = null)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->toGravatar($param);
}
/**
*
*
* @static
*/
public static function getInitial()
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->getInitial();
}
/**
*
*
* @static
*/
public static function getImageObject()
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->getImageObject();
}
/**
*
*
* @static
*/
public static function buildAvatar()
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->buildAvatar();
}
/**
*
*
* @static
*/
public static function getAttribute($key)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->getAttribute($key);
}
/**
*
*
* @static
*/
public static function setTheme($theme)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->setTheme($theme);
}
/**
*
*
* @static
*/
public static function setBackground($hex)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->setBackground($hex);
}
/**
*
*
* @static
*/
public static function setForeground($hex)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->setForeground($hex);
}
/**
*
*
* @static
*/
public static function setDimension($width, $height = null)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->setDimension($width, $height);
}
/**
*
*
* @static
*/
public static function setFontSize($size)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->setFontSize($size);
}
/**
*
*
* @static
*/
public static function setFontFamily($font)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->setFontFamily($font);
}
/**
*
*
* @static
*/
public static function setBorder($size, $color, $radius = 0)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->setBorder($size, $color, $radius);
}
/**
*
*
* @static
*/
public static function setBorderRadius($radius)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->setBorderRadius($radius);
}
/**
*
*
* @static
*/
public static function setShape($shape)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->setShape($shape);
}
/**
*
*
* @static
*/
public static function setChars($chars)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->setChars($chars);
}
/**
*
*
* @static
*/
public static function setFont($font)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->setFont($font);
}
}
}
namespace Maatwebsite\Excel\Facades {
@ -16467,9 +16708,10 @@
/**
*
*
* @param string|null $disk Fallback for usage with named properties
* @param object $export
* @param string $filePath
* @param string|null $disk
* @param string|null $diskName
* @param string $writerType
* @param mixed $diskOptions
* @return bool
@ -16477,10 +16719,10 @@
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
* @static
*/
public static function store($export, $filePath, $diskName = null, $writerType = null, $diskOptions = [])
public static function store($export, $filePath, $diskName = null, $writerType = null, $diskOptions = [], $disk = null)
{
/** @var \Maatwebsite\Excel\Excel $instance */
return $instance->store($export, $filePath, $diskName, $writerType, $diskOptions);
return $instance->store($export, $filePath, $diskName, $writerType, $diskOptions, $disk);
}
/**
*
@ -16698,7 +16940,7 @@
* @param $pathToFile string The file to open
* @param \Madnest\Madzipper\Repositories\RepositoryInterface|string $type The type of the archive, defaults to zip, possible are zip, phar
* @throws \RuntimeException
* @throws \Exception
* @throws Exception
* @throws \InvalidArgumentException
* @return \Madnest\Madzipper\Madzipper Madzipper instance
* @static
@ -16712,7 +16954,7 @@
* Create a new zip archive or open an existing one.
*
* @param string $pathToFile
* @throws \Exception
* @throws Exception
* @return self
* @static
*/
@ -16725,7 +16967,7 @@
* Create a new phar file or open one.
*
* @param string $pathToFile
* @throws \Exception
* @throws Exception
* @return self
* @static
*/
@ -16738,7 +16980,7 @@
* Create a new rar file or open one.
*
* @param string $pathToFile
* @throws \Exception
* @throws Exception
* @return self
* @static
*/
@ -16755,7 +16997,7 @@
* @param $path string The path to extract to
* @param array $files An array of files
* @param int $methodFlags The Method the files should be treated
* @throws \Exception
* @throws Exception
* @return void
* @static
*/
@ -16782,7 +17024,7 @@
* Gets the content of a single file if available.
*
* @param $filePath string The full path (including all folders) of the file in the zip
* @throws \Exception
* @throws Exception
* @return mixed returns the content or throws an exception
* @static
*/
@ -18774,6 +19016,64 @@ namespace {
/**
*
*
* @see \Maatwebsite\Excel\Mixins\DownloadQueryMacro::__invoke()
* @param string $fileName
* @param string|null $writerType
* @param mixed $withHeadings
* @static
*/
public static function downloadExcel($fileName, $writerType = null, $withHeadings = false)
{
return \Illuminate\Database\Eloquent\Builder::downloadExcel($fileName, $writerType, $withHeadings);
}
/**
*
*
* @see \Maatwebsite\Excel\Mixins\StoreQueryMacro::__invoke()
* @param string $filePath
* @param string|null $disk
* @param string|null $writerType
* @param mixed $withHeadings
* @static
*/
public static function storeExcel($filePath, $disk = null, $writerType = null, $withHeadings = false)
{
return \Illuminate\Database\Eloquent\Builder::storeExcel($filePath, $disk, $writerType, $withHeadings);
}
/**
*
*
* @see \Maatwebsite\Excel\Mixins\ImportMacro::__invoke()
* @param string $filename
* @param string|null $disk
* @param string|null $readerType
* @static
*/
public static function import($filename, $disk = null, $readerType = null)
{
return \Illuminate\Database\Eloquent\Builder::import($filename, $disk, $readerType);
}
/**
*
*
* @see \Maatwebsite\Excel\Mixins\ImportAsMacro::__invoke()
* @param string $filename
* @param callable $mapping
* @param string|null $disk
* @param string|null $readerType
* @static
*/
public static function importAs($filename, $mapping, $disk = null, $readerType = null)
{
return \Illuminate\Database\Eloquent\Builder::importAs($filename, $mapping, $disk, $readerType);
}
/**
*
*
* @see \App\Providers\AppServiceProvider::boot()
* @static
*/
@ -20814,6 +21114,7 @@ namespace {
class View extends \Illuminate\Support\Facades\View {}
class Flare extends \Facade\Ignition\Facades\Flare {}
class Image extends \Intervention\Image\Facades\Image {}
class Avatar extends \Laravolt\Avatar\Facade {}
class Excel extends \Maatwebsite\Excel\Facades\Excel {}
class Madzipper extends \Madnest\Madzipper\Facades\Madzipper {}
class Captcha extends \Mews\Captcha\Facades\Captcha {}

View File

@ -0,0 +1,205 @@
<?php
namespace App\Console\Commands;
use App\Console\Commands\Traits\ManticoreSyncLock;
use App\Models\File;
use App\Models\Project;
use App\Models\ProjectTask;
use App\Models\User;
use App\Models\WebSocketDialogMsg;
use App\Module\Apps;
use App\Module\Manticore\ManticoreFile;
use App\Module\Manticore\ManticoreKeyValue;
use App\Module\Manticore\ManticoreMsg;
use App\Module\Manticore\ManticoreProject;
use App\Module\Manticore\ManticoreTask;
use App\Module\Manticore\ManticoreUser;
use Illuminate\Console\Command;
/**
* 异步向量生成命令
*
* 用于后台批量生成已索引数据的向量,与全文索引解耦
* 使用双指针追踪sync:xxxLastId全文已同步 vector:xxxLastId向量已生成
*
* 运行模式:
* - 持续处理直到所有待处理数据完成
* - 每批处理完成后休眠几秒,避免 API 过载
* - 定时器只作为兜底触发机制
*/
class GenerateManticoreVectors extends Command
{
use ManticoreSyncLock;
protected $signature = 'manticore:generate-vectors
{--type=all : 类型 (msg/file/task/project/user/all)}
{--batch=50 : 每批 embedding 数量}
{--sleep=3 : 每批处理后休眠秒数}
{--reset : 重置向量进度指针}';
protected $description = '批量生成 Manticore 已索引数据的向量';
/**
* 类型配置
*/
private const TYPE_CONFIG = [
'msg' => [
'syncKey' => 'sync:manticoreMsgLastId',
'vectorKey' => 'vector:manticoreMsgLastId',
'class' => ManticoreMsg::class,
'model' => WebSocketDialogMsg::class,
'idField' => 'id',
],
'file' => [
'syncKey' => 'sync:manticoreFileLastId',
'vectorKey' => 'vector:manticoreFileLastId',
'class' => ManticoreFile::class,
'model' => File::class,
'idField' => 'id',
],
'task' => [
'syncKey' => 'sync:manticoreTaskLastId',
'vectorKey' => 'vector:manticoreTaskLastId',
'class' => ManticoreTask::class,
'model' => ProjectTask::class,
'idField' => 'id',
],
'project' => [
'syncKey' => 'sync:manticoreProjectLastId',
'vectorKey' => 'vector:manticoreProjectLastId',
'class' => ManticoreProject::class,
'model' => Project::class,
'idField' => 'id',
],
'user' => [
'syncKey' => 'sync:manticoreUserLastId',
'vectorKey' => 'vector:manticoreUserLastId',
'class' => ManticoreUser::class,
'model' => User::class,
'idField' => 'userid',
],
];
public function handle(): int
{
if (!Apps::isInstalled("search")) {
$this->error("应用「Manticore Search」未安装");
return 1;
}
if (!Apps::isInstalled("ai")) {
$this->error("应用「AI」未安装无法生成向量");
return 1;
}
$this->registerSignalHandlers();
if (!$this->acquireLock()) {
return 1;
}
$type = $this->option('type');
$batchSize = intval($this->option('batch'));
$sleepSeconds = intval($this->option('sleep'));
$reset = $this->option('reset');
if ($type === 'all') {
$types = array_keys(self::TYPE_CONFIG);
} else {
if (!isset(self::TYPE_CONFIG[$type])) {
$this->error("未知类型: {$type}。可用类型: msg, file, task, project, user, all");
$this->releaseLock();
return 1;
}
$types = [$type];
}
// 持续处理直到所有类型都没有待处理数据
$round = 0;
do {
$round++;
$totalPending = 0;
foreach ($types as $t) {
if ($this->shouldStop) {
break;
}
$pending = $this->processType($t, $batchSize, $reset && $round === 1);
$totalPending += $pending;
}
// 如果还有待处理数据,休眠后继续
if ($totalPending > 0 && !$this->shouldStop) {
$this->info("\n--- 第 {$round} 轮完成,剩余 {$totalPending} 条待处理,{$sleepSeconds} 秒后继续 ---\n");
sleep($sleepSeconds);
$this->setLock(); // 刷新锁
}
} while ($totalPending > 0 && !$this->shouldStop);
$this->info("\n向量生成完成(共 {$round} 轮)");
$this->releaseLock();
return 0;
}
/**
* 处理单个类型的向量生成(每次处理一批)
*
* @param string $type 类型
* @param int $batchSize 每批数量
* @param bool $reset 是否重置进度
* @return int 剩余待处理数量
*/
private function processType(string $type, int $batchSize, bool $reset): int
{
$config = self::TYPE_CONFIG[$type];
// 获取进度指针
$syncLastId = intval(ManticoreKeyValue::get($config['syncKey'], 0));
$vectorLastId = $reset ? 0 : intval(ManticoreKeyValue::get($config['vectorKey'], 0));
if ($reset) {
ManticoreKeyValue::set($config['vectorKey'], 0);
$this->info("[{$type}] 已重置向量进度指针");
}
// 计算待处理范围
$pendingCount = $syncLastId - $vectorLastId;
if ($pendingCount <= 0) {
return 0;
}
// 获取待处理的 ID 列表(每次处理 batchSize * 5 条,让 generateVectorsBatch 内部再分批调用 API
$modelClass = $config['model'];
$idField = $config['idField'];
$fetchCount = $batchSize * 5;
$ids = $modelClass::where($idField, '>', $vectorLastId)
->where($idField, '<=', $syncLastId)
->orderBy($idField)
->limit($fetchCount)
->pluck($idField)
->toArray();
if (empty($ids)) {
return 0;
}
// 批量生成向量
$manticoreClass = $config['class'];
$successCount = $manticoreClass::generateVectorsBatch($ids, $batchSize);
$currentLastId = end($ids);
// 更新向量进度指针
ManticoreKeyValue::set($config['vectorKey'], $currentLastId);
$remaining = $pendingCount - count($ids);
$this->info("[{$type}] 处理 " . count($ids) . " 条,成功 {$successCount}ID: {$vectorLastId} -> {$currentLastId},剩余 {$remaining}");
// 刷新锁
$this->setLock();
return max(0, $remaining);
}
}

View File

@ -0,0 +1,188 @@
<?php
namespace App\Console\Commands;
use App\Console\Commands\Traits\ManticoreSyncLock;
use App\Models\File;
use App\Models\ManticoreSyncFailure;
use App\Models\Project;
use App\Models\ProjectTask;
use App\Models\User;
use App\Models\WebSocketDialogMsg;
use App\Module\Apps;
use App\Module\Manticore\ManticoreBase;
use App\Module\Manticore\ManticoreFile;
use App\Module\Manticore\ManticoreMsg;
use App\Module\Manticore\ManticoreProject;
use App\Module\Manticore\ManticoreTask;
use App\Module\Manticore\ManticoreUser;
use Illuminate\Console\Command;
class RetryManticoreSync extends Command
{
use ManticoreSyncLock;
protected $signature = 'manticore:retry-failures {--limit=100 : 每次处理的最大数量} {--stats : 显示统计信息}';
protected $description = '重试 Manticore 同步失败的记录';
public function handle(): int
{
if (!Apps::isInstalled("search")) {
$this->error("应用「Manticore Search」未安装");
return 1;
}
// 显示统计信息
if ($this->option('stats')) {
$this->showStats();
return 0;
}
$this->registerSignalHandlers();
if (!$this->acquireLock()) {
return 1;
}
$this->info('开始重试失败的同步任务...');
$limit = intval($this->option('limit'));
$failures = ManticoreSyncFailure::getPendingRetries($limit);
if ($failures->isEmpty()) {
$this->info('无待重试的记录');
$this->releaseLock();
return 0;
}
$this->info("找到 {$failures->count()} 条待重试记录");
$successCount = 0;
$failCount = 0;
foreach ($failures as $failure) {
if ($this->shouldStop) {
$this->info('收到停止信号,退出处理');
break;
}
$this->setLock();
$result = $this->retryOne($failure);
if ($result) {
$successCount++;
$this->info(" [成功] {$failure->data_type}:{$failure->data_id} ({$failure->action})");
} else {
$failCount++;
$this->warn(" [失败] {$failure->data_type}:{$failure->data_id} ({$failure->action}) - 第 {$failure->retry_count}");
}
}
$this->info("\n重试完成: 成功 {$successCount}, 失败 {$failCount}");
$this->releaseLock();
return 0;
}
/**
* 重试单条失败记录
*/
private function retryOne(ManticoreSyncFailure $failure): bool
{
$type = $failure->data_type;
$id = $failure->data_id;
$action = $failure->action;
try {
if ($action === 'delete') {
// 删除操作直接调用通用删除方法
return ManticoreBase::deleteVector($type, $id);
}
// sync 操作需要根据类型获取模型并同步
return $this->retrySyncByType($type, $id);
} catch (\Throwable $e) {
// 记录失败(会自动更新重试次数和时间)
ManticoreSyncFailure::recordFailure($type, $id, $action, $e->getMessage());
return false;
}
}
/**
* 根据类型重试同步
*/
private function retrySyncByType(string $type, int $id): bool
{
switch ($type) {
case 'msg':
$model = WebSocketDialogMsg::find($id);
if (!$model) {
// 数据已删除,移除失败记录
ManticoreSyncFailure::removeSuccess($type, $id, 'sync');
return true;
}
return ManticoreMsg::sync($model);
case 'file':
$model = File::find($id);
if (!$model) {
ManticoreSyncFailure::removeSuccess($type, $id, 'sync');
return true;
}
return ManticoreFile::sync($model);
case 'task':
$model = ProjectTask::find($id);
if (!$model) {
ManticoreSyncFailure::removeSuccess($type, $id, 'sync');
return true;
}
return ManticoreTask::sync($model);
case 'project':
$model = Project::find($id);
if (!$model) {
ManticoreSyncFailure::removeSuccess($type, $id, 'sync');
return true;
}
return ManticoreProject::sync($model);
case 'user':
$model = User::find($id);
if (!$model) {
ManticoreSyncFailure::removeSuccess($type, $id, 'sync');
return true;
}
return ManticoreUser::sync($model);
default:
return false;
}
}
/**
* 显示统计信息
*/
private function showStats(): void
{
$stats = ManticoreSyncFailure::getStats();
$this->info('Manticore 同步失败统计:');
$this->info(" 总数: {$stats['total']}");
if (!empty($stats['by_type'])) {
$this->info(' 按类型:');
foreach ($stats['by_type'] as $type => $count) {
$this->info(" - {$type}: {$count}");
}
}
if (!empty($stats['by_action'])) {
$this->info(' 按操作:');
foreach ($stats['by_action'] as $action => $count) {
$this->info(" - {$action}: {$count}");
}
}
}
}

View File

@ -0,0 +1,155 @@
<?php
namespace App\Console\Commands;
use App\Console\Commands\Traits\ManticoreSyncLock;
use App\Models\File;
use App\Module\Apps;
use App\Module\Manticore\ManticoreFile;
use App\Module\Manticore\ManticoreKeyValue;
use Illuminate\Console\Command;
class SyncFileToManticore extends Command
{
use ManticoreSyncLock;
/**
* 更新数据
* --f: 全量更新 (默认)
* --i: 增量更新
*
* 清理数据
* --c: 清除索引
*
* 其他选项
* --sleep: 每批处理完成后休眠秒数
*/
protected $signature = 'manticore:sync-files {--f} {--i} {--c} {--batch=100} {--sleep=3}';
protected $description = '同步文件数据到 Manticore Search';
public function handle(): int
{
if (!Apps::isInstalled("search")) {
$this->error("应用「Manticore Search」未安装");
return 1;
}
$this->registerSignalHandlers();
if (!$this->acquireLock()) {
return 1;
}
// 清除索引
if ($this->option('c')) {
$this->info('清除索引...');
ManticoreKeyValue::clear();
ManticoreFile::clear();
$this->info("索引删除成功");
$this->releaseLock();
return 0;
}
$this->info('开始同步文件数据...');
$this->syncFiles();
$this->info("\n同步完成");
$this->releaseLock();
return 0;
}
/**
* 同步文件数据
*/
private function syncFiles(): void
{
$lastKey = "sync:manticoreFileLastId";
$isIncremental = $this->option('i');
$sleepSeconds = intval($this->option('sleep'));
$batchSize = $this->option('batch');
$maxFileSize = ManticoreFile::getMaxFileSize();
$round = 0;
do {
$round++;
$lastId = $isIncremental ? intval(ManticoreKeyValue::get($lastKey, 0)) : 0;
if ($round === 1) {
if ($lastId > 0) {
$this->info("\n增量同步文件数据从ID {$lastId} 开始)...");
} else {
$this->info("\n全量同步文件数据...");
}
}
$count = File::where('id', '>', $lastId)
->where('type', '!=', 'folder')
->where('size', '<=', $maxFileSize)
->count();
if ($count === 0) {
if ($round === 1) {
$this->info("无待同步数据");
}
break;
}
$this->info("[第 {$round} 轮] 待同步 {$count} 个文件");
$num = 0;
$total = 0;
do {
if ($this->shouldStop) {
break;
}
$files = File::where('id', '>', $lastId)
->where('type', '!=', 'folder')
->where('size', '<=', $maxFileSize)
->orderBy('id')
->limit($batchSize)
->get();
if ($files->isEmpty()) {
break;
}
$num += count($files);
$progress = $count > 0 ? round($num / $count * 100, 2) : 100;
$this->info("{$num}/{$count} ({$progress}%) 文件ID {$files->first()->id} ~ {$files->last()->id}");
$this->setLock();
$syncCount = ManticoreFile::batchSync($files);
$total += $syncCount;
$lastId = $files->last()->id;
ManticoreKeyValue::set($lastKey, $lastId);
} while (count($files) == $batchSize && !$this->shouldStop);
$this->info("[第 {$round} 轮] 完成,同步 {$total}最后ID {$lastId}");
if ($isIncremental && !$this->shouldStop) {
$newCount = File::where('id', '>', $lastId)
->where('type', '!=', 'folder')
->where('size', '<=', $maxFileSize)
->count();
if ($newCount > 0) {
$this->info("发现 {$newCount} 个新文件,{$sleepSeconds} 秒后继续...");
sleep($sleepSeconds);
continue;
}
}
break;
} while (!$this->shouldStop);
$this->info("同步文件结束(共 {$round} 轮)- 最后ID: " . ManticoreKeyValue::get($lastKey, 0));
$this->info("已索引文件数量: " . ManticoreFile::getIndexedCount());
}
}

View File

@ -0,0 +1,232 @@
<?php
namespace App\Console\Commands;
use App\Console\Commands\Traits\ManticoreSyncLock;
use App\Models\WebSocketDialogMsg;
use App\Module\Apps;
use App\Module\Manticore\ManticoreMsg;
use App\Module\Manticore\ManticoreKeyValue;
use Illuminate\Console\Command;
class SyncMsgToManticore extends Command
{
use ManticoreSyncLock;
/**
* 更新数据
* --f: 全量更新 (默认)
* --i: 增量更新
*
* 清理数据
* --c: 清除索引
*
* 其他选项
* --dialog: 指定对话ID
* --sleep: 每批处理完成后休眠秒数
*/
protected $signature = 'manticore:sync-msgs {--f} {--i} {--c} {--batch=100} {--dialog=} {--sleep=3}';
protected $description = '同步消息数据到 Manticore Search';
public function handle(): int
{
if (!Apps::isInstalled("search")) {
$this->error("应用「Manticore Search」未安装");
return 1;
}
$this->registerSignalHandlers();
if (!$this->acquireLock()) {
return 1;
}
// 清除索引
if ($this->option('c')) {
$this->info('清除索引...');
ManticoreMsg::clear();
$this->info("索引删除成功");
$this->releaseLock();
return 0;
}
$dialogId = $this->option('dialog') ? intval($this->option('dialog')) : 0;
if ($dialogId > 0) {
$this->info("开始同步对话 {$dialogId} 的消息数据...");
$this->syncDialogMsgs($dialogId);
} else {
$this->info('开始同步消息数据...');
$this->syncMsgs();
}
$this->info("\n同步完成");
$this->releaseLock();
return 0;
}
/**
* 同步所有消息
*/
private function syncMsgs(): void
{
$lastKey = "sync:manticoreMsgLastId";
$isIncremental = $this->option('i');
$sleepSeconds = intval($this->option('sleep'));
$batchSize = $this->option('batch');
$round = 0;
// 持续处理循环(增量模式下)
do {
$round++;
$lastId = $isIncremental ? intval(ManticoreKeyValue::get($lastKey, 0)) : 0;
if ($round === 1) {
if ($lastId > 0) {
$this->info("\n增量同步消息数据从ID {$lastId} 开始)...");
} else {
$this->info("\n全量同步消息数据...");
}
}
// 构建基础查询条件
$count = WebSocketDialogMsg::where('id', '>', $lastId)
->whereNull('deleted_at')
->where('bot', '!=', 1)
->whereNotNull('key')
->where('key', '!=', '')
->whereIn('type', ManticoreMsg::INDEXABLE_TYPES)
->count();
if ($count === 0) {
if ($round === 1) {
$this->info("无待同步数据");
}
break;
}
$this->info("[第 {$round} 轮] 待同步 {$count} 条消息");
$num = 0;
$total = 0;
do {
if ($this->shouldStop) {
break;
}
$msgs = WebSocketDialogMsg::where('id', '>', $lastId)
->whereNull('deleted_at')
->where('bot', '!=', 1)
->whereNotNull('key')
->where('key', '!=', '')
->whereIn('type', ManticoreMsg::INDEXABLE_TYPES)
->orderBy('id')
->limit($batchSize)
->get();
if ($msgs->isEmpty()) {
break;
}
$num += count($msgs);
$progress = $count > 0 ? round($num / $count * 100, 2) : 100;
$this->info("{$num}/{$count} ({$progress}%) 消息ID {$msgs->first()->id} ~ {$msgs->last()->id}");
$this->setLock();
$syncCount = ManticoreMsg::batchSync($msgs);
$total += $syncCount;
$lastId = $msgs->last()->id;
ManticoreKeyValue::set($lastKey, $lastId);
} while (count($msgs) == $batchSize && !$this->shouldStop);
$this->info("[第 {$round} 轮] 完成,同步 {$total}最后ID {$lastId}");
// 增量模式下,检查是否有新数据,有则继续
if ($isIncremental && !$this->shouldStop) {
$newCount = WebSocketDialogMsg::where('id', '>', $lastId)
->whereNull('deleted_at')
->where('bot', '!=', 1)
->whereNotNull('key')
->where('key', '!=', '')
->whereIn('type', ManticoreMsg::INDEXABLE_TYPES)
->count();
if ($newCount > 0) {
$this->info("发现 {$newCount} 条新数据,{$sleepSeconds} 秒后继续...");
sleep($sleepSeconds);
continue;
}
}
break; // 非增量模式或无新数据,退出循环
} while (!$this->shouldStop);
$this->info("同步消息结束(共 {$round} 轮)- 最后ID: " . ManticoreKeyValue::get($lastKey, 0));
$this->info("已索引消息数量: " . ManticoreMsg::getIndexedCount());
}
/**
* 同步指定对话的消息
*
* @param int $dialogId 对话ID
*/
private function syncDialogMsgs(int $dialogId): void
{
$this->info("\n同步对话 {$dialogId} 的消息数据...");
$baseQuery = WebSocketDialogMsg::where('dialog_id', $dialogId)
->whereNull('deleted_at')
->where('bot', '!=', 1)
->whereNotNull('key')
->where('key', '!=', '')
->whereIn('type', ManticoreMsg::INDEXABLE_TYPES);
$num = 0;
$count = $baseQuery->count();
$batchSize = $this->option('batch');
$lastId = 0;
$total = 0;
$lastNum = 0;
do {
$msgs = WebSocketDialogMsg::where('dialog_id', $dialogId)
->where('id', '>', $lastId)
->whereNull('deleted_at')
->where('bot', '!=', 1)
->whereNotNull('key')
->where('key', '!=', '')
->whereIn('type', ManticoreMsg::INDEXABLE_TYPES)
->orderBy('id')
->limit($batchSize)
->get();
if ($msgs->isEmpty()) {
break;
}
$num += count($msgs);
$progress = $count > 0 ? round($num / $count * 100, 2) : 100;
if ($progress < 100) {
$progress = number_format($progress, 2);
}
$this->info("{$num}/{$count} ({$progress}%) 正在同步消息ID {$msgs->first()->id} ~ {$msgs->last()->id} ({$total}|{$lastNum})");
$this->setLock();
$lastNum = ManticoreMsg::batchSync($msgs);
$total += $lastNum;
$lastId = $msgs->last()->id;
} while (count($msgs) == $batchSize);
$this->info("同步对话 {$dialogId} 消息结束");
$this->info("该对话已索引消息数量: " . \App\Module\Manticore\ManticoreBase::getDialogIndexedMsgCount($dialogId));
}
}

View File

@ -0,0 +1,146 @@
<?php
namespace App\Console\Commands;
use App\Console\Commands\Traits\ManticoreSyncLock;
use App\Models\Project;
use App\Module\Apps;
use App\Module\Manticore\ManticoreProject;
use App\Module\Manticore\ManticoreKeyValue;
use Illuminate\Console\Command;
class SyncProjectToManticore extends Command
{
use ManticoreSyncLock;
/**
* 更新数据
* --f: 全量更新 (默认)
* --i: 增量更新
*
* 清理数据
* --c: 清除索引
*
* 其他选项
* --sleep: 每批处理完成后休眠秒数
*/
protected $signature = 'manticore:sync-projects {--f} {--i} {--c} {--batch=100} {--sleep=3}';
protected $description = '同步项目数据到 Manticore Search';
public function handle(): int
{
if (!Apps::isInstalled("search")) {
$this->error("应用「Manticore Search」未安装");
return 1;
}
$this->registerSignalHandlers();
if (!$this->acquireLock()) {
return 1;
}
if ($this->option('c')) {
$this->info('清除索引...');
ManticoreProject::clear();
$this->info("索引删除成功");
$this->releaseLock();
return 0;
}
$this->info('开始同步项目数据...');
$this->syncProjects();
$this->info("\n同步完成");
$this->releaseLock();
return 0;
}
private function syncProjects(): void
{
$lastKey = "sync:manticoreProjectLastId";
$isIncremental = $this->option('i');
$sleepSeconds = intval($this->option('sleep'));
$batchSize = $this->option('batch');
$round = 0;
do {
$round++;
$lastId = $isIncremental ? intval(ManticoreKeyValue::get($lastKey, 0)) : 0;
if ($round === 1) {
if ($lastId > 0) {
$this->info("\n增量同步项目数据从ID {$lastId} 开始)...");
} else {
$this->info("\n全量同步项目数据...");
}
}
$count = Project::where('id', '>', $lastId)
->whereNull('archived_at')
->count();
if ($count === 0) {
if ($round === 1) {
$this->info("无待同步数据");
}
break;
}
$this->info("[第 {$round} 轮] 待同步 {$count} 个项目");
$num = 0;
$total = 0;
do {
if ($this->shouldStop) {
break;
}
$projects = Project::where('id', '>', $lastId)
->whereNull('archived_at')
->orderBy('id')
->limit($batchSize)
->get();
if ($projects->isEmpty()) {
break;
}
$num += count($projects);
$progress = $count > 0 ? round($num / $count * 100, 2) : 100;
$this->info("{$num}/{$count} ({$progress}%) 项目ID {$projects->first()->id} ~ {$projects->last()->id}");
$this->setLock();
$syncCount = ManticoreProject::batchSync($projects);
$total += $syncCount;
$lastId = $projects->last()->id;
ManticoreKeyValue::set($lastKey, $lastId);
} while (count($projects) == $batchSize && !$this->shouldStop);
$this->info("[第 {$round} 轮] 完成,同步 {$total}最后ID {$lastId}");
if ($isIncremental && !$this->shouldStop) {
$newCount = Project::where('id', '>', $lastId)
->whereNull('archived_at')
->count();
if ($newCount > 0) {
$this->info("发现 {$newCount} 个新项目,{$sleepSeconds} 秒后继续...");
sleep($sleepSeconds);
continue;
}
}
break;
} while (!$this->shouldStop);
$this->info("同步项目结束(共 {$round} 轮)- 最后ID: " . ManticoreKeyValue::get($lastKey, 0));
$this->info("已索引项目数量: " . ManticoreProject::getIndexedCount());
}
}

View File

@ -0,0 +1,149 @@
<?php
namespace App\Console\Commands;
use App\Console\Commands\Traits\ManticoreSyncLock;
use App\Models\ProjectTask;
use App\Module\Apps;
use App\Module\Manticore\ManticoreTask;
use App\Module\Manticore\ManticoreKeyValue;
use Illuminate\Console\Command;
class SyncTaskToManticore extends Command
{
use ManticoreSyncLock;
/**
* 更新数据
* --f: 全量更新 (默认)
* --i: 增量更新
*
* 清理数据
* --c: 清除索引
*
* 其他选项
* --sleep: 每批处理完成后休眠秒数
*/
protected $signature = 'manticore:sync-tasks {--f} {--i} {--c} {--batch=100} {--sleep=3}';
protected $description = '同步任务数据到 Manticore Search';
public function handle(): int
{
if (!Apps::isInstalled("search")) {
$this->error("应用「Manticore Search」未安装");
return 1;
}
$this->registerSignalHandlers();
if (!$this->acquireLock()) {
return 1;
}
if ($this->option('c')) {
$this->info('清除索引...');
ManticoreTask::clear();
$this->info("索引删除成功");
$this->releaseLock();
return 0;
}
$this->info('开始同步任务数据...');
$this->syncTasks();
$this->info("\n同步完成");
$this->releaseLock();
return 0;
}
private function syncTasks(): void
{
$lastKey = "sync:manticoreTaskLastId";
$isIncremental = $this->option('i');
$sleepSeconds = intval($this->option('sleep'));
$batchSize = $this->option('batch');
$round = 0;
do {
$round++;
$lastId = $isIncremental ? intval(ManticoreKeyValue::get($lastKey, 0)) : 0;
if ($round === 1) {
if ($lastId > 0) {
$this->info("\n增量同步任务数据从ID {$lastId} 开始)...");
} else {
$this->info("\n全量同步任务数据...");
}
}
$count = ProjectTask::where('id', '>', $lastId)
->whereNull('archived_at')
->whereNull('deleted_at')
->count();
if ($count === 0) {
if ($round === 1) {
$this->info("无待同步数据");
}
break;
}
$this->info("[第 {$round} 轮] 待同步 {$count} 个任务");
$num = 0;
$total = 0;
do {
if ($this->shouldStop) {
break;
}
$tasks = ProjectTask::where('id', '>', $lastId)
->whereNull('archived_at')
->whereNull('deleted_at')
->orderBy('id')
->limit($batchSize)
->get();
if ($tasks->isEmpty()) {
break;
}
$num += count($tasks);
$progress = $count > 0 ? round($num / $count * 100, 2) : 100;
$this->info("{$num}/{$count} ({$progress}%) 任务ID {$tasks->first()->id} ~ {$tasks->last()->id}");
$this->setLock();
$syncCount = ManticoreTask::batchSync($tasks);
$total += $syncCount;
$lastId = $tasks->last()->id;
ManticoreKeyValue::set($lastKey, $lastId);
} while (count($tasks) == $batchSize && !$this->shouldStop);
$this->info("[第 {$round} 轮] 完成,同步 {$total}最后ID {$lastId}");
if ($isIncremental && !$this->shouldStop) {
$newCount = ProjectTask::where('id', '>', $lastId)
->whereNull('archived_at')
->whereNull('deleted_at')
->count();
if ($newCount > 0) {
$this->info("发现 {$newCount} 个新任务,{$sleepSeconds} 秒后继续...");
sleep($sleepSeconds);
continue;
}
}
break;
} while (!$this->shouldStop);
$this->info("同步任务结束(共 {$round} 轮)- 最后ID: " . ManticoreKeyValue::get($lastKey, 0));
$this->info("已索引任务数量: " . ManticoreTask::getIndexedCount());
}
}

View File

@ -0,0 +1,149 @@
<?php
namespace App\Console\Commands;
use App\Console\Commands\Traits\ManticoreSyncLock;
use App\Models\User;
use App\Module\Apps;
use App\Module\Manticore\ManticoreUser;
use App\Module\Manticore\ManticoreKeyValue;
use Illuminate\Console\Command;
class SyncUserToManticore extends Command
{
use ManticoreSyncLock;
/**
* 更新数据
* --f: 全量更新 (默认)
* --i: 增量更新
*
* 清理数据
* --c: 清除索引
*
* 其他选项
* --sleep: 每批处理完成后休眠秒数
*/
protected $signature = 'manticore:sync-users {--f} {--i} {--c} {--batch=100} {--sleep=3}';
protected $description = '同步用户数据到 Manticore Search';
public function handle(): int
{
if (!Apps::isInstalled("search")) {
$this->error("应用「Manticore Search」未安装");
return 1;
}
$this->registerSignalHandlers();
if (!$this->acquireLock()) {
return 1;
}
if ($this->option('c')) {
$this->info('清除索引...');
ManticoreUser::clear();
$this->info("索引删除成功");
$this->releaseLock();
return 0;
}
$this->info('开始同步用户数据...');
$this->syncUsers();
$this->info("\n同步完成");
$this->releaseLock();
return 0;
}
private function syncUsers(): void
{
$lastKey = "sync:manticoreUserLastId";
$isIncremental = $this->option('i');
$sleepSeconds = intval($this->option('sleep'));
$batchSize = $this->option('batch');
$round = 0;
do {
$round++;
$lastId = $isIncremental ? intval(ManticoreKeyValue::get($lastKey, 0)) : 0;
if ($round === 1) {
if ($lastId > 0) {
$this->info("\n增量同步用户数据从ID {$lastId} 开始)...");
} else {
$this->info("\n全量同步用户数据...");
}
}
$count = User::where('userid', '>', $lastId)
->where('bot', 0)
->whereNull('disable_at')
->count();
if ($count === 0) {
if ($round === 1) {
$this->info("无待同步数据");
}
break;
}
$this->info("[第 {$round} 轮] 待同步 {$count} 个用户");
$num = 0;
$total = 0;
do {
if ($this->shouldStop) {
break;
}
$users = User::where('userid', '>', $lastId)
->where('bot', 0)
->whereNull('disable_at')
->orderBy('userid')
->limit($batchSize)
->get();
if ($users->isEmpty()) {
break;
}
$num += count($users);
$progress = $count > 0 ? round($num / $count * 100, 2) : 100;
$this->info("{$num}/{$count} ({$progress}%) 用户ID {$users->first()->userid} ~ {$users->last()->userid}");
$this->setLock();
$syncCount = ManticoreUser::batchSync($users);
$total += $syncCount;
$lastId = $users->last()->userid;
ManticoreKeyValue::set($lastKey, $lastId);
} while (count($users) == $batchSize && !$this->shouldStop);
$this->info("[第 {$round} 轮] 完成,同步 {$total}最后ID {$lastId}");
if ($isIncremental && !$this->shouldStop) {
$newCount = User::where('userid', '>', $lastId)
->where('bot', 0)
->whereNull('disable_at')
->count();
if ($newCount > 0) {
$this->info("发现 {$newCount} 个新用户,{$sleepSeconds} 秒后继续...");
sleep($sleepSeconds);
continue;
}
}
break;
} while (!$this->shouldStop);
$this->info("同步用户结束(共 {$round} 轮)- 最后ID: " . ManticoreKeyValue::get($lastKey, 0));
$this->info("已索引用户数量: " . ManticoreUser::getIndexedCount());
}
}

View File

@ -0,0 +1,90 @@
<?php
namespace App\Console\Commands\Traits;
use Cache;
/**
* Manticore 同步命令通用锁机制
*
* 提供:
* - 锁的获取、设置、释放
* - 信号处理(优雅退出)
* - 通用的命令初始化检查
*/
trait ManticoreSyncLock
{
private bool $shouldStop = false;
/**
* 获取锁信息
*/
private function getLock(): ?array
{
$lockKey = $this->getLockKey();
return Cache::has($lockKey) ? Cache::get($lockKey) : null;
}
/**
* 设置锁30分钟有效期持续处理时需不断刷新
*/
private function setLock(): void
{
$lockKey = $this->getLockKey();
Cache::put($lockKey, ['started_at' => date('Y-m-d H:i:s')], 1800);
}
/**
* 释放锁
*/
private function releaseLock(): void
{
$lockKey = $this->getLockKey();
Cache::forget($lockKey);
}
/**
* 获取锁的缓存键
*/
private function getLockKey(): string
{
return md5($this->signature);
}
/**
* 信号处理器SIGINT/SIGTERM
*/
public function handleSignal(int $signal): void
{
$this->info("\n收到信号,将在当前批次完成后退出...");
$this->shouldStop = true;
}
/**
* 注册信号处理器
*/
private function registerSignalHandlers(): void
{
if (extension_loaded('pcntl')) {
pcntl_async_signals(true);
pcntl_signal(SIGINT, [$this, 'handleSignal']);
pcntl_signal(SIGTERM, [$this, 'handleSignal']);
}
}
/**
* 检查命令是否可以启动(锁检查)
*
* @return bool 返回 true 表示可以启动false 表示已被占用
*/
private function acquireLock(): bool
{
$lockInfo = $this->getLock();
if ($lockInfo) {
$this->error("命令已在运行中,开始时间: {$lockInfo['started_at']}");
return false;
}
$this->setLock();
return true;
}
}

View File

@ -1,27 +0,0 @@
<?php
namespace App\Events;
use Hhxsv5\LaravelS\Swoole\Events\ServerStartInterface;
use Swoole\Http\Server;
class ServerStartEvent implements ServerStartInterface
{
public function __construct()
{
}
public function handle(Server $server)
{
$server->startMsecTime = $this->msecTime();
}
private function msecTime()
{
list($msec, $sec) = explode(' ', microtime());
$time = explode(".", $sec . ($msec * 1000));
return $time[0];
}
}

View File

@ -3,7 +3,7 @@
namespace App\Events;
use App\Models\WebSocket;
use Cache;
use App\Services\RequestContext;
use Hhxsv5\LaravelS\Swoole\Events\WorkerStartInterface;
use Swoole\Http\Server;
@ -16,9 +16,16 @@ class WorkerStartEvent implements WorkerStartInterface
public function handle(Server $server, $workerId)
{
if (isset($server->startMsecTime) && Cache::get("swooleServerStartMsecTime") != $server->startMsecTime) {
Cache::forever("swooleServerStartMsecTime", $server->startMsecTime);
WebSocket::query()->delete();
// 仅在Worker进程启动时执行一次初始化代码
$initTable = app('swoole')->initFlagTable;
if ($initTable->incr('init_flag', 'value') === 1) {
$this->handleFirstWorkerTasks();
}
}
private function handleFirstWorkerTasks()
{
WebSocket::query()->delete();
RequestContext::clearBaseUrlCache();
}
}

View File

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

View File

@ -3,9 +3,11 @@
namespace App\Exceptions;
use App\Module\Base;
use App\Module\Image;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;
class Handler extends ExceptionHandler
@ -51,6 +53,11 @@ class Handler extends ExceptionHandler
*/
public function render($request, Throwable $e)
{
if ($e instanceof NotFoundHttpException) {
if ($result = $this->ImagePathHandler($request)) {
return $result;
}
}
if ($e instanceof ApiException) {
return response()->json(Base::retError($e->getMessage(), $e->getData(), $e->getCode()));
} elseif ($e instanceof ModelNotFoundException) {
@ -67,7 +74,7 @@ class Handler extends ExceptionHandler
public function report(Throwable $e)
{
if ($e instanceof ApiException) {
if ($e->getCode() !== -1) {
if ($e->isWriteLog()) {
Log::error($e->getMessage(), [
'code' => $e->getCode(),
'data' => $e->getData(),
@ -78,4 +85,157 @@ class Handler extends ExceptionHandler
parent::report($e);
}
}
/**
* 图片路径处理
* @param $request
* @return \Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\BinaryFileResponse|null
*/
private function ImagePathHandler($request)
{
$path = $request->path();
// 处理图片
$patternCrop = '/^(uploads\/.*\.(png|jpg|jpeg))\/crop\/([^\/]+)$/';
$patternThumb = '/^(uploads\/.*)_thumb\.(png|jpg|jpeg)$/';
$matchesCrop = null;
$matchesThumb = null;
if (preg_match($patternCrop, $path, $matchesCrop) || preg_match($patternThumb, $path, $matchesThumb)) {
// 获取参数
if ($matchesCrop) {
$file = $matchesCrop[1];
$ext = $matchesCrop[2];
$rules = preg_replace('/\s+/', '', $matchesCrop[3]);
$rules = str_replace(['=', '&'], [':', ','], $rules);
$rules = explode(',', $rules);
} elseif ($matchesThumb) {
$file = $matchesThumb[1];
$ext = $matchesThumb[2];
$rules = ['percentage:320x0'];
} else {
return null;
}
if (empty($rules)) {
return null;
}
// 提取年月
$Ym = date("Ym");
if (preg_match('/\/(\d{6})\//', $file, $ms)) {
$Ym = $ms[1];
}
// 文件存在直接返回
$dirName = str_replace(['/', '.'], '_', $file);
$fileName = str_replace([':', ','], ['-', '_'], implode(',', $rules)) . '.' . $ext;
$savePath = public_path('uploads/tmp/crop/' . $Ym . '/' . $dirName . '/' . $fileName);
if (file_exists($savePath)) {
// 设置头部声明图片缓存
return response()->file($savePath, [
'Pragma' => 'public',
'Cache-Control' => 'max-age=1814400',
'Expires' => gmdate('D, d M Y H:i:s', time() + 1814400) . ' GMT',
'Last-Modified' => gmdate('D, d M Y H:i:s', filemtime($savePath)) . ' GMT',
'ETag' => md5_file($savePath)
]);
}
// 文件不存在处理
$sourcePath = public_path($file);
if (!file_exists($sourcePath)) {
return null;
}
// 判断删除多余文件
$saveDir = dirname($savePath);
if (is_dir($saveDir)) {
$items = glob($saveDir . '/*');
if (count($items) > 5) {
usort($items, function ($a, $b) {
return filemtime($b) - filemtime($a);
});
$itemsToDelete = array_slice($items, 5);
foreach ($itemsToDelete as $item) {
if (is_file($item)) {
unlink($item);
}
}
}
} else {
Base::makeDir($saveDir);
}
// 处理图片
try {
$handle = 0;
$image = new Image($sourcePath);
foreach ($rules as $rule) {
if (!str_contains($rule, ':')) {
continue;
}
[$type, $value] = explode(':', $rule);
if (!in_array($type, ['ratio', 'size', 'percentage', 'cover', 'contain'])) {
continue;
}
switch ($type) {
// 按比例裁剪
case 'ratio':
if (is_numeric($value)) {
$image->ratioCrop($value);
$handle++;
}
break;
// 按尺寸缩放
case 'size':
$size = Base::newIntval(explode('x', $value));
if (count($size) === 2) {
$image->resize($size[0], $size[1]);
$handle++;
}
break;
// 按尺寸缩放
case 'percentage':
case 'cover':
case 'contain':
$size = Base::newIntval(explode('x', $value));
if (count($size) === 2) {
$image->thumb($size[0], $size[1], $type);
$handle++;
}
break;
}
}
if ($handle > 0) {
$image->saveTo($savePath);
Image::compressImage($savePath, 80);
return response()->file($savePath, [
'Pragma' => 'public',
'Cache-Control' => 'max-age=1814400',
'Expires' => gmdate('D, d M Y H:i:s', time() + 1814400) . ' GMT',
'Last-Modified' => gmdate('D, d M Y H:i:s', filemtime($savePath)) . ' GMT',
'ETag' => md5_file($savePath)
]);
} else {
$image->destroy();
}
} 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

@ -3,7 +3,7 @@
if (!function_exists('asset_main')) {
function asset_main($path, $secure = null)
{
return preg_replace("/^https*:\/\//", "//", app('url')->asset($path, $secure));
return preg_replace("/^https?:\/\//", "//", app('url')->asset($path, $secure));
}
}

View File

@ -3,22 +3,27 @@
namespace App\Http\Controllers\Api;
use Request;
use Session;
use Response;
use Madzipper;
use Carbon\Carbon;
use App\Module\Down;
use App\Models\User;
use App\Module\Base;
use App\Module\Doo;
use App\Module\Timer;
use App\Module\Ihttp;
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;
use App\Module\Apps;
use App\Module\BillMultipleExport;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Swoole\Coroutine;
/**
* @apiDefine approve
@ -28,16 +33,19 @@ use Hhxsv5\LaravelS\Swoole\Task\Task;
class ApproveController extends AbstractController
{
private $flow_url = '';
public function __construct()
{
Apps::isInstalledThrow('approve');
$this->flow_url = env('FLOW_URL') ?: 'http://approve';
}
/**
* @api {get} api/approve/verifyToken 01. 验证APi登录
* @api {get} api/approve/verifyToken 验证APi登录
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup users
* @apiGroup approve
* @apiName verifyToken
*
* @apiSuccess {String} version
@ -55,7 +63,7 @@ class ApproveController extends AbstractController
}
/**
* @api {post} api/approve/procdef/all 02. 查询流程定义
* @api {post} api/approve/procdef/all 查询流程定义
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -72,7 +80,7 @@ class ApproveController extends AbstractController
{
User::auth();
$data['name'] = Request::input('name');
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/procdef/findAll', json_encode($data));
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/procdef/findAll', json_encode($data));
$procdef = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$procdef || $procdef['status'] != 200 || $ret['ret'] == 0) {
// info($ret);
@ -82,7 +90,7 @@ class ApproveController extends AbstractController
}
/**
* @api {get} api/approve/procdef/del 03. 删除流程定义
* @api {get} api/approve/procdef/del 删除流程定义
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -99,7 +107,7 @@ class ApproveController extends AbstractController
{
User::auth('admin');
$data['id'] = Request::input('id');
$ret = Ihttp::ihttp_get($this->flow_url.'/api/v1/workflow/procdef/delById?'.http_build_query($data));
$ret = Ihttp::ihttp_get($this->flow_url . '/api/v1/workflow/procdef/delById?' . http_build_query($data));
$procdef = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$procdef || $procdef['status'] != 200) {
return Base::retError($procdef['message'] ?? '删除失败');
@ -108,7 +116,7 @@ class ApproveController extends AbstractController
}
/**
* @api {post} api/approve/process/start 04. 启动流程(审批中)
* @api {post} api/approve/process/start 启动流程(审批中)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -132,7 +140,7 @@ class ApproveController extends AbstractController
//
$var = json_decode(Request::input('var'), true);
$data['var'] = $var;
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/start', json_encode(Base::arrayKeyToCamel($data)));
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/start', json_encode(Base::arrayKeyToCamel($data)));
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
return Base::retError($process['message'] ?? '启动失败');
@ -171,7 +179,7 @@ class ApproveController extends AbstractController
}
/**
* @api {post} api/approve/process/addGlobalComment 05. 添加全局评论
* @api {post} api/approve/process/addGlobalComment 添加全局评论
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -194,7 +202,7 @@ class ApproveController extends AbstractController
$processInst = $this->getProcessById($data['proc_inst_id']);
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/addGlobalComment', json_encode(Base::arrayKeyToCamel($data)));
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/addGlobalComment', json_encode(Base::arrayKeyToCamel($data)));
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
return Base::retError($process['message'] ?? '添加失败');
@ -202,11 +210,11 @@ class ApproveController extends AbstractController
// 推送通知
$botUser = User::botGetOrCreate('approval-alert');
foreach ( $processInst['userids'] as $id) {
if($id != $user->userid){
foreach ($processInst['userids'] as $id) {
if ($id != $user->userid) {
$dialog = WebSocketDialog::checkUserDialog($botUser, $id);
$processInst['comment_user_id'] = $user->userid;
$processInst['comment_content'] = json_decode($data['content'],true)['content'];
$processInst['comment_contents'] = json_decode($data['content'], true) ?? [];
$this->approveMsg('approve_comment_notifier', $dialog, $botUser, $processInst, $processInst);
}
}
@ -216,7 +224,7 @@ class ApproveController extends AbstractController
}
/**
* @api {post} api/approve/task/complete 06. 审批
* @api {post} api/approve/task/complete 审批
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -238,7 +246,7 @@ class ApproveController extends AbstractController
$data['task_id'] = intval(Request::input('task_id'));
$data['pass'] = Request::input('pass');
$data['comment'] = Request::input('comment');
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/task/complete', json_encode(Base::arrayKeyToCamel($data)));
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/task/complete', json_encode(Base::arrayKeyToCamel($data)));
$task = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$task || $task['status'] != 200) {
return Base::retError($task['message'] ?? '审批失败');
@ -261,12 +269,12 @@ class ApproveController extends AbstractController
$this->approveMsg('approve_reviewer', $dialog, $botUser, $val, $process, $pass);
}
// 发起人
if($process['is_finished'] == true) {
if ($process['is_finished']) {
$dialog = WebSocketDialog::checkUserDialog($botUser, $process['start_user_id']);
if (!empty($dialog)) {
$this->approveMsg('approve_submitter', $dialog, $botUser, ['userid' => $data['userid']], $process, $pass);
}
}else if ($process['candidate']) {
} else if ($process['candidate']) {
// 下个审批人
$userid = explode(',', $process['candidate']);
$toUser = User::whereIn('userid', $userid)->get()->toArray();
@ -278,12 +286,12 @@ class ApproveController extends AbstractController
if (empty($dialog)) {
continue;
}
$this->approveMsg('approve_reviewer', $dialog, $botUser, $val, $process,'start');
$this->approveMsg('approve_reviewer', $dialog, $botUser, $val, $process, 'start');
}
}
// 抄送人
$notifier = $this->handleProcessNode($process, $task['step']);
$notifier = $this->handleProcessNode($process);
if ($notifier && $pass == 'pass') {
foreach ($notifier as $val) {
$dialog = WebSocketDialog::checkUserDialog($botUser, $val['target_id']);
@ -292,11 +300,11 @@ class ApproveController extends AbstractController
}
}
}
return Base::retSuccess( $pass == 'pass' ? '已通过' : '已拒绝', $task);
return Base::retSuccess($pass == 'pass' ? '已通过' : '已拒绝', $task);
}
/**
* @api {post} api/approve/task/withdraw 07. 撤回
* @api {post} api/approve/task/withdraw 撤回
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -316,7 +324,7 @@ class ApproveController extends AbstractController
$data['userid'] = (string)$user->userid;
$data['task_id'] = intval(Request::input('task_id'));
$data['proc_inst_id'] = intval(Request::input('proc_inst_id'));
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/task/withdraw', json_encode(Base::arrayKeyToCamel($data)));
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/task/withdraw', json_encode(Base::arrayKeyToCamel($data)));
$task = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$task || $task['status'] != 200) {
return Base::retError($task['message'] ?? '撤回失败');
@ -341,7 +349,38 @@ class ApproveController extends AbstractController
}
/**
* @api {post} api/approve/process/findTask 08. 查询需要我审批的流程(审批中)
* @api {post} api/approve/process/delById 删除审批(流程实例)
*
* @apiDescription 需要token身份仅可删除已结束的审批且仅发起人或管理员可删
* @apiVersion 1.0.0
* @apiGroup approve
* @apiName process__delById
*
* @apiQuery {Number} proc_inst_id 流程实例ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function process__delById()
{
$user = User::auth();
$data['userid'] = (string)$user->userid;
$data['proc_inst_id'] = intval(Request::input('proc_inst_id'));
$data['is_admin'] = $user->isAdmin();
if ($data['proc_inst_id'] <= 0) {
return Base::retError('参数错误');
}
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/delById', json_encode(Base::arrayKeyToCamel($data)));
$task = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$task || $task['status'] != 200) {
return Base::retError($task['message'] ?? '删除失败');
}
return Base::retSuccess('已删除');
}
/**
* @api {post} api/approve/process/findTask 查询需要我审批的流程(审批中)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -366,7 +405,7 @@ class ApproveController extends AbstractController
$data['sort'] = Request::input('sort');
$data['pageIndex'] = intval(Request::input('page'));
$data['pageSize'] = intval(Request::input('page_size'));
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/findTask', json_encode(Base::arrayKeyToCamel($data)));
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/findTask', json_encode(Base::arrayKeyToCamel($data)));
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
return Base::retError($process['message'] ?? '查询失败');
@ -380,11 +419,11 @@ class ApproveController extends AbstractController
}
$val['userimg'] = User::getAvatar($info->userid, $info->userimg, $info->email, $info->nickname);
}
return Base::retSuccess('success',$res);
return Base::retSuccess('success', $res);
}
/**
* @api {post} api/approve/process/startByMyselfAll 09. 查询我启动的流程(全部)
* @api {post} api/approve/process/startByMyselfAll 查询我启动的流程(全部)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -409,7 +448,7 @@ class ApproveController extends AbstractController
$data['state'] = intval(Request::input('state')); //状态
$data['pageIndex'] = intval(Request::input('page'));
$data['pageSize'] = intval(Request::input('page_size'));
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/startByMyselfAll', json_encode($data));
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/startByMyselfAll', json_encode($data));
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
return Base::retError($process['message'] ?? '查询失败');
@ -427,7 +466,7 @@ class ApproveController extends AbstractController
}
/**
* @api {post} api/approve/process/startByMyself 10. 查询我启动的流程(审批中)
* @api {post} api/approve/process/startByMyself 查询我启动的流程(审批中)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -447,7 +486,7 @@ class ApproveController extends AbstractController
$data['userid'] = (string)$user->userid;
$data['pageIndex'] = intval(Request::input('page'));
$data['pageSize'] = intval(Request::input('page_size'));
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/startByMyself', json_encode($data));
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/startByMyself', json_encode($data));
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
return Base::retError($process['message'] ?? '查询失败');
@ -465,7 +504,7 @@ class ApproveController extends AbstractController
}
/**
* @api {post} api/approve/process/findProcNotify 11. 查询抄送我的流程(审批中)
* @api {post} api/approve/process/findProcNotify 查询抄送我的流程(审批中)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -491,7 +530,7 @@ class ApproveController extends AbstractController
$data['pageIndex'] = intval(Request::input('page'));
$data['pageSize'] = intval(Request::input('page_size'));
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/findProcNotify', json_encode($data));
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/findProcNotify', json_encode($data));
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
return Base::retError($process['message'] ?? '查询失败');
@ -509,7 +548,7 @@ class ApproveController extends AbstractController
}
/**
* @api {get} api/approve/identitylink/findParticipant 12. 查询流程实例的参与者(审批中)
* @api {get} api/approve/identitylink/findParticipant 查询流程实例的参与者(审批中)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -526,7 +565,7 @@ class ApproveController extends AbstractController
{
User::auth();
$proc_inst_id = Request::input('proc_inst_id');
$ret = Ihttp::ihttp_get($this->flow_url.'/api/v1/workflow/identitylink/findParticipant?procInstId=' . $proc_inst_id);
$ret = Ihttp::ihttp_get($this->flow_url . '/api/v1/workflow/identitylink/findParticipant?procInstId=' . $proc_inst_id);
$identitylink = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$identitylink || $identitylink['status'] != 200) {
return Base::retError($identitylink['message'] ?? '查询失败');
@ -544,7 +583,7 @@ class ApproveController extends AbstractController
}
/**
* @api {post} api/approve/procHistory/findTask 13. 查询需要我审批的流程(已结束)
* @api {post} api/approve/procHistory/findTask 查询需要我审批的流程(已结束)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -569,7 +608,7 @@ class ApproveController extends AbstractController
$data['sort'] = Request::input('sort');
$data['pageIndex'] = intval(Request::input('page'));
$data['pageSize'] = intval(Request::input('page_size'));
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/procHistory/findTask', json_encode(Base::arrayKeyToCamel($data)));
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/procHistory/findTask', json_encode(Base::arrayKeyToCamel($data)));
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
return Base::retError($process['message'] ?? '查询失败');
@ -587,7 +626,7 @@ class ApproveController extends AbstractController
}
/**
* @api {post} api/approve/procHistory/startByMyself 14. 查询我启动的流程(已结束)
* @api {post} api/approve/procHistory/startByMyself 查询我启动的流程(已结束)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -607,7 +646,7 @@ class ApproveController extends AbstractController
$data['userid'] = (string)$user->userid;
$data['pageIndex'] = intval(Request::input('page'));
$data['pageSize'] = intval(Request::input('page_size'));
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/procHistory/startByMyself', json_encode($data));
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/procHistory/startByMyself', json_encode($data));
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
return Base::retError($process['message'] ?? '查询失败');
@ -625,7 +664,7 @@ class ApproveController extends AbstractController
}
/**
* @api {post} api/approve/procHistory/findProcNotify 15. 查询抄送我的流程(已结束)
* @api {post} api/approve/procHistory/findProcNotify 查询抄送我的流程(已结束)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -651,7 +690,7 @@ class ApproveController extends AbstractController
$data['pageIndex'] = intval(Request::input('page'));
$data['pageSize'] = intval(Request::input('page_size'));
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/procHistory/findProcNotify', json_encode($data));
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/procHistory/findProcNotify', json_encode($data));
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
return Base::retError($process['message'] ?? '查询失败');
@ -669,7 +708,7 @@ class ApproveController extends AbstractController
}
/**
* @api {get} api/approve/identitylinkHistory/findParticipant 16. 查询流程实例的参与者(已结束)
* @api {get} api/approve/identitylinkHistory/findParticipant 查询流程实例的参与者(已结束)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -686,7 +725,7 @@ class ApproveController extends AbstractController
{
User::auth();
$proc_inst_id = Request::input('proc_inst_id');
$ret = Ihttp::ihttp_get($this->flow_url.'/api/v1/workflow/identitylinkHistory/findParticipant?procInstId=' . $proc_inst_id);
$ret = Ihttp::ihttp_get($this->flow_url . '/api/v1/workflow/identitylinkHistory/findParticipant?procInstId=' . $proc_inst_id);
$identitylink = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$identitylink || $identitylink['status'] != 200) {
return Base::retError($identitylink['message'] ?? '查询失败');
@ -704,7 +743,7 @@ class ApproveController extends AbstractController
}
/**
* @api {get} api/approve/process/detail 17. 根据流程ID查询流程详情
* @api {get} api/approve/process/detail 根据流程ID查询流程详情
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -726,7 +765,7 @@ class ApproveController extends AbstractController
}
/**
* @api {post} api/approve/export 18. 导出数据
* @api {post} api/approve/export 导出数据
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -750,146 +789,207 @@ class ApproveController extends AbstractController
$data['isFinished'] = intval(Request::input('is_finished')); //是否完成
$date = Request::input('date');
$data['startTime'] = $date[0]; //开始时间
$data['endTime'] =Carbon::parse($date[1])->addDay()->toDateString(); //结束时间 + 1天
$data['endTime'] = Carbon::parse($date[1])->addDay()->toDateString(); //结束时间 + 1天
//
if (empty($name) || empty($date)) {
return Base::retError('参数错误');
}
if (!(is_array($date) && Base::isDate($date[0]) && Base::isDate($date[1]))) {
if (!(is_array($date) && Timer::isDate($date[0]) && Timer::isDate($date[1]))) {
return Base::retError('日期选择错误');
}
if (Carbon::parse($date[1])->timestamp - Carbon::parse($date[0])->timestamp > 35 * 86400) {
return Base::retError('日期范围限制最大35天');
}
//
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/findAllProcIns', json_encode($data));
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
return Base::retError($process['message'] ?? '查询失败');
$botUser = User::botGetOrCreate('system-msg');
if (empty($botUser)) {
return Base::retError('系统机器人不存在');
}
$dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid);
//
$res = Base::arrayKeyToUnderline($process['data']);
//
$headings = [];
$headings[] = '申请编号';
$headings[] = '标题';
$headings[] = '申请状态';
$headings[] = '发起时间';
$headings[] = '完成时间';
$headings[] = '发起人工号';
$headings[] = '发起人User ID';
$headings[] = '发起人姓名';
$headings[] = '发起人部门';
$headings[] = '发起人部门ID';
$headings[] = '部门负责人';
$headings[] = '历史审批人';
$headings[] = '历史办理人';
$headings[] = '审批记录';
$headings[] = '当前处理人';
$headings[] = '审批节点';
$headings[] = '审批人数';
$headings[] = '审批耗时';
$headings[] = '假期类型';
$headings[] = '开始时间';
$headings[] = '结束时间';
$headings[] = '时长';
$headings[] = '请假事由';
$headings[] = '请假单位';
//
$sheets = [];
$datas = [];
foreach ($res as $val) {
$doo = Doo::load();
go(function () use ($doo, $data, $user, $botUser, $dialog) {
Coroutine::sleep(1);
//
$nickname = Base::filterEmoji($val['start_user_name']);
$participant = $this->getUserProcessParticipantById($val['id']); // 获取参与人
$participant = $this->handleParticipant($val, $participant['data']); // 处理参与人返回数据
//
$job_number = ''; // 发起人工号
$department_leader = User::userid2nickname(UserDepartment::find(1, ['owner_userid'])['owner_userid']); // 部门负责人
$historical_approver = $participant['historical_approver'] ?? ''; // 历史审批人
$historical_agent = ''; // 历史办理人
$approval_record = $participant['approval_record'] ?? ''; // 审批记录
$current_handler = !$val['is_finished'] ? implode(',', User::whereIn('userid', explode(';', $val['candidate']))->pluck('nickname')->toArray()) : ''; // 当前处理人
$approved_node = $participant['approved_node'] ?? 0; // 审批节点
$approved_num = $participant['approved_num'] ?? 0; // 审批人数
// 计算审批耗时
$startTime = Carbon::parse($val['start_time'])->timestamp;
$endTime = $val['end_time'] ? Carbon::parse($val['end_time'])->timestamp : time();
$approval_time = Base::timeDiff($startTime, $endTime); // 审批耗时
// 计算时长
$varStartTime = Carbon::parse($val['var']['start_time']);
$varEndTime = Carbon::parse($val['var']['end_time']);
$duration = $varEndTime->floatDiffInHours($varStartTime);
$duration_unit = '小时'; // 时长单位
$datas[] = [
$val['id'], // 申请编号
$val['proc_def_name'], // 标题
$this->getStateDescription($val['state']), // 申请状态
$val['start_time'], // 发起时间
$val['end_time'], // 完成时间
$job_number, // 发起人工号
$val['start_user_id'], // 发起人User ID
$nickname, // 发起人姓名
$val['department'], // 发起人部门
$val['department_id'], // 发起人部门ID
$department_leader, // 部门负责人
$historical_approver, // 历史审批人
$historical_agent, // 历史办理人
$approval_record, // 审批记录
$current_handler, // 当前处理人
$approved_node, // 审批节点
$approved_num, // 审批人数
$approval_time, // 审批耗时
$val['var']['type'], // 假期类型
$val['var']['start_time'], // 开始时间
$val['var']['end_time'], // 结束时间
$duration, // 时长
$val['var']['description'], // 请假事由
$duration_unit, // 请假单位
$content = [];
$content[] = [
'content' => '导出审批数据已完成',
'style' => 'font-weight: bold;padding-bottom: 4px;',
];
}
if (empty($datas)) {
return Base::retError('没有任何数据');
}
//
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/findAllProcIns', json_encode($data));
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
$content[] = [
'content' => $process['message'] ?? '查询失败',
'style' => 'color: #ff0000;',
];
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'title' => $content[0]['content'],
'content' => $content,
], $botUser->userid, true, false, true);
return;
}
//
$res = Base::arrayKeyToUnderline($process['data']);
//
$headings = [];
$headings[] = $doo->translate('申请编号');
$headings[] = $doo->translate('标题');
$headings[] = $doo->translate('申请状态');
$headings[] = $doo->translate('发起时间');
$headings[] = $doo->translate('完成时间');
$headings[] = $doo->translate('发起人工号');
$headings[] = $doo->translate('发起人User ID');
$headings[] = $doo->translate('发起人姓名');
$headings[] = $doo->translate('发起人部门');
$headings[] = $doo->translate('发起人部门ID');
$headings[] = $doo->translate('部门负责人');
$headings[] = $doo->translate('历史审批人');
$headings[] = $doo->translate('历史办理人');
$headings[] = $doo->translate('审批记录');
$headings[] = $doo->translate('当前处理人');
$headings[] = $doo->translate('审批节点');
$headings[] = $doo->translate('审批人数');
$headings[] = $doo->translate('审批耗时');
$headings[] = $doo->translate('假期类型');
$headings[] = $doo->translate('开始时间');
$headings[] = $doo->translate('结束时间');
$headings[] = $doo->translate('时长');
$headings[] = $doo->translate('请假事由');
$headings[] = $doo->translate('请假单位');
//
$datas = [];
foreach ($res as $val) {
//
$nickname = Base::filterEmoji($val['start_user_name']);
$participant = $this->getUserProcessParticipantById($val['id']); // 获取参与人
$participant = $this->handleParticipant($val, $participant['data']); // 处理参与人返回数据
//
$job_number = ''; // 发起人工号
$department_leader = User::userid2nickname(UserDepartment::find(1, ['owner_userid'])['owner_userid']); // 部门负责人
$historical_approver = $participant['historical_approver'] ?? ''; // 历史审批人
$historical_agent = ''; // 历史办理人
$approval_record = $participant['approval_record'] ?? ''; // 审批记录
$current_handler = !$val['is_finished'] ? implode(',', User::whereIn('userid', explode(';', $val['candidate']))->pluck('nickname')->toArray()) : ''; // 当前处理人
$approved_node = $participant['approved_node'] ?? 0; // 审批节点
$approved_num = $participant['approved_num'] ?? 0; // 审批人数
// 计算审批耗时
$startTime = Carbon::parse($val['start_time'])->timestamp;
$endTime = $val['end_time'] ? Carbon::parse($val['end_time'])->timestamp : time();
$approval_time = $doo->translate(Timer::timeDiff($startTime, $endTime)); // 审批耗时
// 计算时长
$varStartTime = Carbon::parse($val['var']['start_time']);
$varEndTime = Carbon::parse($val['var']['end_time']);
$duration = $varEndTime->floatDiffInHours($varStartTime);
$duration_unit = $doo->translate('小时'); // 时长单位
$datas[] = [
$val['id'], // 申请编号
$val['proc_def_name'], // 标题
$this->getStateDescription($val['state']), // 申请状态
$val['start_time'], // 发起时间
$val['end_time'], // 完成时间
$job_number, // 发起人工号
$val['start_user_id'], // 发起人User ID
$nickname, // 发起人姓名
$val['department'], // 发起人部门
$val['department_id'], // 发起人部门ID
$department_leader, // 部门负责人
$historical_approver, // 历史审批人
$historical_agent, // 历史办理人
$approval_record, // 审批记录
$current_handler, // 当前处理人
$approved_node, // 审批节点
$approved_num, // 审批人数
$approval_time, // 审批耗时
$val['var']['type'], // 假期类型
$val['var']['start_time'], // 开始时间
$val['var']['end_time'], // 结束时间
$duration, // 时长
$val['var']['description'], // 请假事由
$duration_unit, // 请假单位
];
}
if (empty($datas)) {
$content[] = [
'content' => '没有任何数据',
'style' => 'color: #ff0000;',
];
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'title' => $content[0]['content'],
'content' => $content,
], $botUser->userid, true, false, true);
return;
}
//
$title = $doo->translate("审批记录");
$sheets = [
BillExport::create()->setTitle($title)->setHeadings($headings)->setData($datas)->setStyles(["A1:Y1" => ["font" => ["bold" => true]]])
];
//
$fileName = $title . '_' . Timer::time() . '.xlsx';
$filePath = "temp/approve/export/" . date("Ym", Timer::time());
$export = new BillMultipleExport($sheets);
$res = $export->store($filePath . "/" . $fileName);
if ($res != 1) {
$content[] = [
'content' => "导出失败,{$fileName}",
'style' => 'color: #ff0000;',
];
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'title' => $content[0]['content'],
'content' => $content,
], $botUser->userid, true, false, true);
return;
}
$xlsPath = storage_path("app/" . $filePath . "/" . $fileName);
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xlsx') . ".zip";
$zipPath = storage_path($zipFile);
if (file_exists($zipPath)) {
Base::deleteDirAndFile($zipPath, true);
}
try {
Madzipper::make($zipPath)->add($xlsPath)->close();
} catch (\Throwable) {
}
//
if (file_exists($zipPath)) {
$key = Down::cache_encode([
'file' => $zipFile,
]);
$fileUrl = Base::fillUrl('api/approve/down?key=' . $key);
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'file_download',
'title' => '导出审批数据已完成',
'name' => $fileName,
'size' => filesize($zipPath),
'url' => $fileUrl,
], $botUser->userid, true, false, true);
} else {
$content[] = [
'content' => "打包失败,请稍后再试...",
'style' => 'color: #ff0000;',
];
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'title' => $content[0]['content'],
'content' => $content,
], $botUser->userid, true, false, true);
}
});
//
$title = "Sheet1";
$sheets = [
BillExport::create()->setTitle($title)->setHeadings($headings)->setData($datas)->setStyles(["A1:Y1" => ["font" => ["bold" => true]]])
];
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'content' => '正在导出审批数据,请稍等...',
], $botUser->userid, true, false, true);
//
$fileName = '审批记录_' . Base::time() . '.xls';
$filePath = "temp/approve/export/" . date("Ym", Base::time());
$export = new BillMultipleExport($sheets);
$res = $export->store($filePath . "/" . $fileName);
if ($res != 1) {
return Base::retError('导出失败,' . $fileName . '');
}
$xlsPath = storage_path("app/" . $filePath . "/" . $fileName);
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xls') . ".zip";
$zipPath = storage_path($zipFile);
if (file_exists($zipPath)) {
Base::deleteDirAndFile($zipPath, true);
}
try {
Madzipper::make($zipPath)->add($xlsPath)->close();
} catch (\Throwable) {
}
//
if (file_exists($zipPath)) {
$base64 = base64_encode(Base::array2string([
'file' => $zipFile,
]));
Session::put('approve::export:userid', $user->userid);
return Base::retSuccess('success', [
'size' => Base::twoFloat(filesize($zipPath) / 1024, true),
'url' => Base::fillUrl('api/approve/down?key=' . urlencode($base64)),
]);
} else {
return Base::retError('打包失败,请稍后再试...');
}
return Base::retSuccess('success');
}
function getStateDescription($state) {
function getStateDescription($state)
{
$state_map = array(
0 => '全部',
1 => '审批中',
@ -897,14 +997,14 @@ class ApproveController extends AbstractController
3 => '拒绝',
4 => '撤回'
);
return isset($state_map[$state]) ? $state_map[$state] : '';
return $state_map[$state] ?? '';
}
/**
* @api {get} api/approve/down 19. 下载导出的审批数据
* @api {get} api/approve/down 下载导出的审批数据
*
* @apiVersion 1.0.0
* @apiGroup system
* @apiGroup approve
* @apiName down
*
* @apiParam {String} key 通过export接口得到的下载钥匙
@ -913,15 +1013,10 @@ class ApproveController extends AbstractController
*/
public function down()
{
$userid = Session::get('approve::export:userid');
if (empty($userid)) {
return Base::ajaxError("请求已过期,请重新导出!", [], 0, 502);
}
//
$array = Base::string2array(base64_decode(urldecode(Request::input('key'))));
$array = Down::cache_decode();
$file = $array['file'];
if (empty($file) || !file_exists(storage_path($file))) {
return Base::ajaxError("文件不存在!", [], 0, 502);
return Base::ajaxError("文件不存在!", [], 0, 403);
}
return Response::download(storage_path($file));
}
@ -951,20 +1046,19 @@ class ApproveController extends AbstractController
}
// 审批记录
$name = $val['username'] . '|';
$call = $val['step'] == 0 ? '发起审批'. '|' : '同意' . '|';
$time =$val['step'] == 0 ? $process['start_time'] . '|' : '';
$call = $val['step'] == 0 ? '发起审批' . '|' : '同意' . '|';
$time = $val['step'] == 0 ? $process['start_time'] . '|' : '';
$comment = $val['step'] == 0 ? '' : ($val['comment'] ?? '') . '|';
$res['approval_record'] .= $name . $call . $time . $comment;
}
}
$res['historical_approver'] = trim(implode(';', $historical_approver), ';');
$res['historical_approver'] = trim(implode(';', $historical_approver), ';');
$res['approved_node'] = $approved_node;
$res['approved_num'] = $approved_num;
$res['historical_agent'] = $res['historical_approver'];
return $res;
}
// 审批机器人消息
public function approveMsg($type, $dialog, $botUser, $toUser, $process, $action = null)
{
@ -976,50 +1070,71 @@ class ApproveController extends AbstractController
'department' => $process['department'],
'type' => $process['var']['type'],
'start_time' => $process['var']['start_time'],
'start_day_of_week' => '周'.Base::getTimeWeek(Carbon::parse($process['var']['start_time'])->timestamp),
'start_day_of_week' => '周' . Timer::getWeek(Carbon::parse($process['var']['start_time'])->timestamp),
'end_time' => $process['var']['end_time'],
'end_day_of_week' => '周'.Base::getTimeWeek(Carbon::parse($process['var']['end_time'])->timestamp),
'end_day_of_week' => '周' . Timer::getWeek(Carbon::parse($process['var']['end_time'])->timestamp),
'description' => $process['var']['description'],
'comment_nickname' => $process['comment_user_id'] ? User::userid2nickname($process['comment_user_id']) : '',
'comment_content' => $process['comment_content'] ?? ''
'comment_content' => $process['comment_contents']['content'] ?? '',
'comment_pictures' => $process['comment_contents']['pictures'] ?? []
];
$text = view('push.bot', ['type' => $type, 'action' => $action, 'is_finished' => $process['is_finished'], 'data' => (object)$data])->render();
$text = preg_replace("/^\x20+/", "", $text);
$text = preg_replace("/\n\x20+/", "\n", $text);
$msg_action = null;
$thumb = null;
if ($type === 'approve_reviewer') {
$thumb = $process['var']['other'];
} elseif ($type === 'approve_comment_notifier') {
$thumb = $data['comment_pictures'] ? $data['comment_pictures'][0] : null;
}
if ($thumb && file_exists(public_path($thumb))) {
$imageSize = getimagesize(public_path($thumb));
$data['thumb'] = [
'url' => Base::fillUrl($thumb),
'width' => $imageSize[0],
'height' => $imageSize[1]
];
}
$msgAction = null;
$msgData = [
'type' => $type,
'action' => $action,
'is_finished' => $process['is_finished'],
'data' => $data
];
$msgData['title'] = match ($type) {
'approve_reviewer' => $data['nickname'] . " 提交的「{$data['proc_def_name']}」待你审批",
'approve_notifier' => "抄送 {$data['nickname']} 提交的「{$data['proc_def_name']}」记录",
'approve_comment_notifier' => $data['comment_nickname'] . " 评论了 {$data['nickname']} 的「{$data['proc_def_name']}」审批",
'approve_submitter' => $action == 'pass' ? "您发起的「{$data['proc_def_name']}」已通过" : "您发起的「{$data['proc_def_name']}」被 {$data['nickname']} 拒绝",
default => '不支持的指令',
};
if ($action == 'withdraw' || $action == 'pass' || $action == 'refuse') {
// 任务完成,给发起人发送消息
if($type == 'approve_submitter' && $action != 'withdraw'){
return WebSocketDialogMsg::sendMsg($msg_action, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, true);
if ($type == 'approve_submitter' && $action != 'withdraw') {
return WebSocketDialogMsg::sendMsg($msgAction, $dialog->id, 'template', $msgData, $botUser->userid, false, false, true);
}
// 查找最后一条消息msg_id
$msg_action = 'update-'.$toUser['msg_id'];
$msgAction = 'change-' . $toUser['msg_id'];
}
//
try {
$msg = WebSocketDialogMsg::sendMsg($msg_action, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, true);
// 关联信息
if ($action == 'start') {
$proc_msg = new ApproveProcMsg();
$proc_msg->proc_inst_id = $process['id'];
$proc_msg->msg_id = $msg['data']->id;
$proc_msg->userid = $toUser['userid'];
$proc_msg->save();
}
// 更新工作报告 未读数量
if($type == 'approve_reviewer' && $toUser['userid']){
$params = [
'userid' => [ $toUser['userid'], User::auth()->userid() ],
'msg' => [
'type' => 'approve',
'action' => 'unread',
'userid' => $toUser['userid'],
]
];
Task::deliver(new PushTask($params, false));
}
} catch (\Throwable $th) {
//throw $th;
$msg = WebSocketDialogMsg::sendMsg($msgAction, $dialog->id, 'template', $msgData, $process['start_user_id'], false, false, true);
// 关联信息
if ($action == 'start') {
$proc_msg = new ApproveProcMsg();
$proc_msg->proc_inst_id = $process['id'];
$proc_msg->msg_id = $msg['data']->id;
$proc_msg->userid = $toUser['userid'];
$proc_msg->save();
}
// 更新审批 未读数量
if ($type == 'approve_reviewer' && $toUser['userid']) {
$params = [
'userid' => [$toUser['userid'], User::userid()],
'msg' => [
'type' => 'approve',
'action' => 'unread',
'userid' => $toUser['userid'],
]
];
Task::deliver(new PushTask($params, false));
}
return true;
}
@ -1028,7 +1143,7 @@ class ApproveController extends AbstractController
public function getProcessById($id)
{
$data['id'] = intval($id);
$ret = Ihttp::ihttp_get($this->flow_url."/api/v1/workflow/process/findById?".http_build_query($data));
$ret = Ihttp::ihttp_get($this->flow_url . "/api/v1/workflow/process/findById?" . http_build_query($data));
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
throw new ApiException($process['message'] ?? '查询失败');
@ -1047,15 +1162,16 @@ class ApproveController extends AbstractController
$val['node_user_list'][$k]['userimg'] = User::getAvatar($info->userid, $info->userimg, $info->email, $info->nickname);
$res['userids'][] = $item['target_id'];
}
}else if($val['aprover_id']){
} else if ($val['aprover_id']) {
$info = User::whereUserid($val['aprover_id'])->first();
$val['userimg'] = $info ? User::getAvatar($info->userid, $info->userimg, $info->email, $info->nickname) : '';
$res['userids'][] = $val['aprover_id'];
}
}
// 全局评论
if(isset($res['global_comments'])){
foreach ($res['global_comments'] as $k => &$globalComment) {
unset($res['global_comment']);
if (isset($res['global_comments'])) {
foreach ($res['global_comments'] as $k => $globalComment) {
$info = User::whereUserid($globalComment['user_id'])->first();
if (!$info) {
continue;
@ -1063,6 +1179,8 @@ class ApproveController extends AbstractController
$res['global_comments'][$k]['userimg'] = User::getAvatar($info->userid, $info->userimg, $info->email, $info->nickname);
$res['global_comments'][$k]['nickname'] = $info->nickname;
}
} else {
$res['global_comments'] = [];
}
$info = User::whereUserid($res['start_user_id'])->first();
$res['userimg'] = $info ? User::getAvatar($info->userid, $info->userimg, $info->email, $info->nickname) : '';
@ -1095,7 +1213,7 @@ class ApproveController extends AbstractController
public function getUserProcessParticipantById($id)
{
$data['procInstId'] = intval($id);
$ret = Ihttp::ihttp_get($this->flow_url."/api/v1/workflow/identitylink/findParticipantAll?".http_build_query($data));
$ret = Ihttp::ihttp_get($this->flow_url . "/api/v1/workflow/identitylink/findParticipantAll?" . http_build_query($data));
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
throw new ApiException($process['message'] ?? '查询失败');
@ -1105,10 +1223,10 @@ class ApproveController extends AbstractController
/**
* @api {get} api/approve/user/status 20. 获取用户审批状态
* @api {get} api/approve/user/status 获取用户审批状态
*
* @apiVersion 1.0.0
* @apiGroup system
* @apiGroup approve
* @apiName user__status
*
* @apiParam {String} userid
@ -1119,14 +1237,32 @@ 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', isset($procdef['data']["proc_def_name"]) ? $procdef['data']["proc_def_name"] : '');
}
return Base::retSuccess('success', '');
$userid = intval(Request::input('userid'));
$status = ApproveProcInstHistory::getUserApprovalStatus($userid);
return Base::retSuccess('success', $status);
}
/**
* @api {get} api/approve/process/doto 查询需要我审批的流程数量
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup approve
* @apiName process__doto
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function process__doto()
{
$user = User::auth();
$ret = Ihttp::ihttp_get($this->flow_url . '/api/v1/workflow/process/findTaskTotal?userid=' . $user->userid);
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
return Base::retError($process['message'] ?? '查询失败');
}
return Base::retSuccess('success', $process['data']);
}
}

View File

@ -0,0 +1,307 @@
<?php
namespace App\Http\Controllers\Api;
use App\Models\AiAssistantSession;
use App\Models\User;
use App\Module\AI;
use App\Module\Apps;
use App\Module\Base;
use Request;
/**
* @apiDefine assistant
*
* 助手
*/
class AssistantController extends AbstractController
{
public function __construct()
{
Apps::isInstalledThrow('ai');
}
/**
* @api {post} api/assistant/auth 生成授权码
*
* @apiDescription 需要token身份生成 AI 流式会话的 stream_key
* @apiVersion 1.0.0
* @apiGroup assistant
* @apiName auth
*
* @apiParam {String} model_type 模型类型
* @apiParam {String} model_name 模型名称
* @apiParam {JSON} context 上下文数组
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
* @apiSuccess {String} data.stream_key 流式会话凭证
*/
public function auth()
{
$user = User::auth();
$user->checkChatInformation();
$modelType = trim(Request::input('model_type', ''));
$modelName = trim(Request::input('model_name', ''));
$contextInput = Request::input('context', []);
return AI::createStreamKey($modelType, $modelName, $contextInput);
}
/**
* @api {get} api/assistant/models 获取AI模型
*
* @apiDescription 获取所有AI机器人模型设置
* @apiVersion 1.0.0
* @apiGroup assistant
* @apiName models
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function 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 {post} api/assistant/match-elements 元素向量匹配
*
* @apiDescription 通过向量相似度匹配页面元素,用于智能查找与查询语义相关的元素
* @apiVersion 1.0.0
* @apiGroup assistant
* @apiName match_elements
*
* @apiParam {String} query 搜索关键词
* @apiParam {Array} elements 元素列表,每个元素包含 ref name 字段
* @apiParam {Number} [top_k=10] 返回的匹配数量最大50
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
* @apiSuccess {Array} data.matches 匹配结果数组,按相似度降序排列
*/
public function match_elements()
{
User::auth();
$query = trim(Request::input('query', ''));
$elements = Request::input('elements', []);
$topK = min(intval(Request::input('top_k', 10)), 50);
if (empty($query) || empty($elements)) {
return Base::retError('参数不能为空');
}
// 获取查询向量
$queryResult = AI::getEmbedding($query);
if (Base::isError($queryResult)) {
return $queryResult;
}
$queryVector = $queryResult['data'];
// 计算相似度并排序
$scored = [];
foreach ($elements as $el) {
$name = $el['name'] ?? '';
if (empty($name)) {
continue;
}
$elResult = AI::getEmbedding($name);
if (Base::isError($elResult)) {
continue;
}
$similarity = $this->cosineSimilarity($queryVector, $elResult['data']);
$scored[] = [
'element' => $el,
'similarity' => $similarity,
];
}
// 按相似度降序排序
usort($scored, fn($a, $b) => $b['similarity'] <=> $a['similarity']);
return Base::retSuccess('success', [
'matches' => array_slice($scored, 0, $topK),
]);
}
/**
* 计算两个向量的余弦相似度
*/
private function cosineSimilarity(array $a, array $b): float
{
$dotProduct = 0;
$normA = 0;
$normB = 0;
$count = count($a);
for ($i = 0; $i < $count; $i++) {
$dotProduct += $a[$i] * $b[$i];
$normA += $a[$i] * $a[$i];
$normB += $b[$i] * $b[$i];
}
$denominator = sqrt($normA) * sqrt($normB);
if ($denominator == 0) {
return 0;
}
return $dotProduct / $denominator;
}
/**
* 获取会话列表
*/
public function session__list()
{
$user = User::auth();
$sessionKey = trim(Request::input('session_key', 'default'));
$sessions = AiAssistantSession::where('userid', $user->userid)
->where('session_key', $sessionKey)
->orderByDesc('updated_at')
->get();
$list = [];
foreach ($sessions as $session) {
$data = Base::json2array($session->data);
$images = Base::json2array($session->images);
foreach ($images as $imageId => $path) {
$images[$imageId] = Base::fillUrl($path);
}
$list[] = [
'id' => $session->session_id,
'title' => $session->title,
'responses' => $data,
'images' => $images,
'sceneKey' => $session->scene_key,
'createdAt' => $session->created_at ? $session->created_at->getTimestampMs() : 0,
'updatedAt' => $session->updated_at ? $session->updated_at->getTimestampMs() : 0,
];
}
return Base::retSuccess('success', $list);
}
/**
* 保存会话
*/
public function session__save()
{
$user = User::auth();
$sessionKey = trim(Request::input('session_key', 'default'));
$sessionId = trim(Request::input('session_id', ''));
$sceneKey = trim(Request::input('scene_key', ''));
$title = trim(Request::input('title', ''));
$data = Request::input('data', []);
$newImages = Request::input('new_images', []);
if (empty($sessionId)) {
return Base::retError('session_id 不能为空');
}
$newImageUrls = [];
if (is_array($newImages)) {
$path = 'uploads/assistant/' . date('Ym') . '/' . $user->userid . '/';
foreach ($newImages as $img) {
$imageId = $img['imageId'] ?? '';
$dataUrl = $img['dataUrl'] ?? '';
if (empty($imageId) || empty($dataUrl)) {
continue;
}
$result = Base::image64save([
'image64' => $dataUrl,
'path' => $path,
'autoThumb' => false,
]);
if (Base::isSuccess($result)) {
$newImageUrls[$imageId] = $result['data']['path'];
}
}
}
$session = AiAssistantSession::where('userid', $user->userid)
->where('session_key', $sessionKey)
->where('session_id', $sessionId)
->first();
$imageMap = $newImageUrls;
if ($session) {
$existingImages = Base::json2array($session->images);
$imageMap = array_merge($existingImages, $newImageUrls);
}
$session = AiAssistantSession::createInstance([
'userid' => $user->userid,
'session_key' => $sessionKey,
'session_id' => $sessionId,
'scene_key' => $sceneKey,
'title' => mb_substr($title, 0, 255),
'data' => Base::array2json(is_array($data) ? $data : []),
'images' => Base::array2json($imageMap),
], $session?->id);
$session->save();
// 仅返回本次新增的图片URL
$urls = [];
foreach ($newImageUrls as $imageId => $path) {
$urls[$imageId] = Base::fillUrl($path);
}
return Base::retSuccess('success', [
'image_urls' => $urls,
]);
}
/**
* 删除会话
*/
public function session__delete()
{
$user = User::auth();
$sessionKey = trim(Request::input('session_key', 'default'));
$sessionId = trim(Request::input('session_id', ''));
$clearAll = Request::input('clear_all', false);
$query = AiAssistantSession::where('userid', $user->userid)
->where('session_key', $sessionKey);
if ($clearAll) {
$sessions = $query->get();
foreach ($sessions as $session) {
$this->deleteSessionImages($session);
}
$query->delete();
} else {
if (empty($sessionId)) {
return Base::retError('session_id 不能为空');
}
$session = $query->where('session_id', $sessionId)->first();
if ($session) {
$this->deleteSessionImages($session);
$session->delete();
}
}
return Base::retSuccess('success');
}
private function deleteSessionImages(AiAssistantSession $session)
{
$images = Base::json2array($session->images);
foreach ($images as $path) {
$fullPath = public_path($path);
if (file_exists($fullPath)) {
@unlink($fullPath);
}
}
}
}

View File

@ -0,0 +1,196 @@
<?php
namespace App\Http\Controllers\Api;
use Request;
use App\Models\User;
use App\Module\Base;
use App\Models\Complaint;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
/**
* @apiDefine complaint
*
* 投诉
*/
class ComplaintController extends AbstractController
{
/**
* @api {get} api/complaint/lists 获取举报投诉列表
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup complaint
* @apiName lists
*
* @apiParam {Number} [type] 类型
* @apiParam {Number} [status] 状态
*
* @apiParam {Number} [page] 当前页,默认:1
* @apiParam {Number} [pagesize] 每页显示数量,默认:50,最大:100
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*
* @apiSuccessExample {json} Success-Response-Data:
* {
* "current_page": 1,
* "data": [
* {
* "id": 1,
* "dialog_id": 100,
* "userid": 1,
* "type": 1,
* "reason": "举报原因",
* "imgs": [],
* "status": 0,
* "created_at": "2025-01-01 00:00:00",
* "updated_at": "2025-01-01 00:00:00"
* }
* ],
* "first_page_url": "http://example.com/api/complaint/lists?page=1",
* "from": 1,
* "last_page": 1,
* "last_page_url": "http://example.com/api/complaint/lists?page=1",
* "next_page_url": null,
* "path": "http://example.com/api/complaint/lists",
* "per_page": 50,
* "prev_page_url": null,
* "to": 1,
* "total": 1
* }
*/
public function lists()
{
$user = User::auth();
$user->identity('admin');
//
$type = intval(Request::input('type'));
$status = Request::input('status');
//
$complaints = Complaint::query()
->when($type, function($q) use($type) {
$q->where('type', $type);
})
->when($status != "", function($q) use($status) {
$q->where('status', $status);
})
->orderByDesc('id')
->paginate(Base::getPaginate(100, 50));
//
return Base::retSuccess('success', $complaints);
}
/**
* @api {post} api/complaint/submit 举报投诉
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup complaint
* @apiName submit
*
* @apiBody {Number} dialog_id 对话ID
* @apiBody {Number} type 类型
* @apiBody {String} reason 原因
* @apiBody {Object[]} [imgs] 图片数组(可选)
* @apiBody {String} imgs.path 图片路径
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*
* @apiSuccessExample {json} Success-Response-Data:
* []
*/
public function submit()
{
$user = User::auth();
//
$dialog_id = intval(Request::input('dialog_id'));
$type = intval(Request::input('type'));
$reason = trim(Request::input('reason'));
$imgs = Request::input('imgs');
//
WebSocketDialog::checkDialog($dialog_id);
//
if (!$type) {
return Base::retError('请选择举报类型');
}
if (!$reason) {
return Base::retError('请填写举报原因');
}
//
$report_imgs = [];
if (!empty($imgs) && is_array($imgs)) {
foreach ($imgs as $img) {
$report_imgs[] = Base::unFillUrl($img['path']);
}
}
//
Complaint::createInstance([
'dialog_id' => $dialog_id,
'userid' => $user->userid,
'type' => $type,
'reason' => $reason,
'imgs' => $report_imgs,
])->save();
// 通知管理员
$botUser = User::botGetOrCreate('system-msg');
User::where("identity", "like", "%,admin,%")
->orderByDesc('line_at')
->take(10)
->get()
->each(function ($adminUser) use ($reason, $botUser) {
$dialog = WebSocketDialog::checkUserDialog($botUser, $adminUser->userid);
if ($dialog) {
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'title' => '收到新的举报信息',
'content' => "收到新的举报信息:{$reason} (请前往应用查看详情)"
], $botUser->userid);
}
});
//
return Base::retSuccess('success');
}
/**
* @api {post} api/complaint/action 举报投诉 - 操作
*
* @apiDescription 需要token身份管理员权限
* @apiVersion 1.0.0
* @apiGroup complaint
* @apiName action
*
* @apiBody {Number} id 投诉ID
* @apiBody {String} type 操作类型handle=已处理delete=删除
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*
* @apiSuccessExample {json} Success-Response-Data:
* []
*/
public function action()
{
$user = User::auth();
$user->identity('admin');
//
$id = intval(Request::input('id'));
$type = trim(Request::input('type'));
//
if ($type == 'handle') {
Complaint::whereId($id)->update([
"status" => 1
]);
}
if ($type == 'delete') {
Complaint::whereId($id)->delete();
}
//
return Base::retSuccess('success');
}
}

File diff suppressed because it is too large Load Diff

View File

@ -11,8 +11,14 @@ use App\Models\FileContent;
use App\Models\FileLink;
use App\Models\FileUser;
use App\Models\User;
use App\Models\UserRecentItem;
use App\Module\Base;
use App\Module\Down;
use App\Module\Lock;
use App\Module\Timer;
use App\Module\Ihttp;
use App\Module\Manticore\ManticoreFile;
use Response;
use Swoole\Coroutine;
use Carbon\Carbon;
use Redirect;
@ -27,7 +33,7 @@ use ZipArchive;
class FileController extends AbstractController
{
/**
* @api {get} api/file/lists 01. 获取文件列表
* @api {get} api/file/lists 获取文件列表
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -44,14 +50,13 @@ class FileController extends AbstractController
{
$user = User::auth();
//
$data = Request::all();
$pid = intval($data['pid']);
$pid = intval(Request::input('pid'));
//
return Base::retSuccess('success', (new File)->getFileList($user, $pid));
}
/**
* @api {get} api/file/one 02. 获取单条数据
* @api {get} api/file/one 获取单条数据
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -61,6 +66,14 @@ class FileController extends AbstractController
* @apiParam {Number|String} id
* - Number 文件ID需要登录
* - String 链接码(不需要登录,用于预览)
* @apiParam {String} [with_url] 是否返回文件访问URL
* - no: 不返回(默认)
* - yes: 返回content_url字段
* @apiParam {String} [with_text] 是否提取文件文本内容用于AI阅读支持分页
* - no: 不提取(默认)
* - yes: 提取文本内容,支持 docx/xlsx/pptx/pdf/txt 等格式
* @apiParam {Number} [text_offset] with_text=yes时有效文本起始位置字符数默认0
* @apiParam {Number} [text_limit] with_text=yes时有效文本获取长度字符数默认50000最大200000
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@ -69,11 +82,15 @@ class FileController extends AbstractController
public function one()
{
$id = Request::input('id');
$with_url = Request::input('with_url', 'no');
$with_text = Request::input('with_text', 'no');
$text_offset = intval(Request::input('text_offset', 0));
$text_limit = intval(Request::input('text_limit', 50000));
//
$permission = 0;
if (Base::isNumber($id)) {
$user = User::auth();
$file = File::permissionFind(intval($id), $user, 0, $permission);
$file = File::permissionFind(intval($id), $user, $with_url === 'yes' ? 1 : 0, $permission);
} elseif ($id) {
$fileLink = FileLink::whereCode($id)->first();
$file = $fileLink?->file;
@ -85,25 +102,87 @@ class FileController extends AbstractController
}
return Base::retError($msg, $data);
}
// 如果文件不允许游客访问,则需要登录
if (!$file->guest_access) {
User::auth();
}
$fileLink->increment("num");
} else {
return Base::retError('参数错误');
}
//
$array = $file->toArray();
$array['permission'] = $permission;
// 如果请求返回文件URL
if ($with_url === 'yes') {
$array['content_url'] = FileContent::getFileUrl($file->id);
}
// 如果请求提取文本内容
if ($with_text === 'yes') {
$array['text_content'] = ManticoreFile::extractFileContentPaginated($file, $text_offset, $text_limit);
}
return Base::retSuccess('success', $array);
}
/**
* @api {get} api/file/search 03. 搜索文件列表
* @api {get} api/file/fetch 通过路径获取文件文本内容
*
* @apiDescription 需要token身份
* @apiDescription 用于 MCP/AI 工具通过文件路径获取内容,支持分页获取大文件
* @apiVersion 1.0.0
* @apiGroup file
* @apiName fetch
*
* @apiParam {String} path 文件路径(相对于系统根目录,如 uploads/file/...
* @apiParam {Number} [offset] 起始位置字符数默认0
* @apiParam {Number} [limit] 获取长度字符数默认50000最大200000
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
* - content: 文本内容
* - total_length: 完整内容总长度
* - offset: 当前起始位置
* - limit: 本次获取长度
* - has_more: 是否还有更多内容
*/
public function fetch()
{
User::auth();
//
$path = trim(Request::input('path'));
$offset = intval(Request::input('offset', 0));
$limit = intval(Request::input('limit', 50000));
if (empty($path)) {
return Base::retError('参数错误path 不能为空');
}
// 直接传入路径ManticoreFile 内部处理 URL 解析
$result = ManticoreFile::extractFileContentPaginated($path, $offset, $limit);
if (isset($result['error'])) {
return Base::retError($result['error']);
}
return Base::retSuccess('success', $result);
}
/**
* @api {get} api/file/search 搜索文件列表
*
* @apiDescription 需要token身份仅搜索文件名AI 内容搜索请使用 api/search/file
* @apiVersion 1.0.0
* @apiGroup file
* @apiName search
*
* @apiParam {String} [link] 通过分享地址搜索https://t.hitosea.com/single/file/ODcwOCwzOSxpa0JBS2lmVQ==
* @apiParam {String} [key] 关键词
* @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 返回信息(错误描述)
@ -116,7 +195,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;
@ -124,13 +203,20 @@ class FileController extends AbstractController
return Base::retSuccess('success', []);
}
}
// 搜索自己的
$builder = File::whereUserid($user->userid);
if ($id) {
$builder->where("id", $id);
}
if ($key) {
$builder->where("name", "like", "%{$key}%");
if (!$id && Base::isNumber($key)) {
$builder->where(function ($query) use ($key) {
$query->where("id", $key)->orWhere("name", "like", "%{$key}%");
});
} else {
$builder->where("name", "like", "%{$key}%");
}
}
$array = $builder->take($take)->get()->toArray();
// 搜索共享的
@ -149,7 +235,13 @@ class FileController extends AbstractController
$builder->where("id", $id);
}
if ($key) {
$builder->where("name", "like", "%{$key}%");
if (Base::isNumber($key)) {
$builder->where(function ($query) use ($key) {
$query->where("id", $key)->orWhere("name", "like", "%{$key}%");
});
} else {
$builder->where("name", "like", "%{$key}%");
}
}
$list = $builder->take($take)->get();
if ($list->isNotEmpty()) {
@ -167,7 +259,7 @@ class FileController extends AbstractController
}
/**
* @api {get} api/file/add 04. 添加、修改文件()
* @api {get} api/file/add 添加、修改文件()
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -193,8 +285,8 @@ class FileController extends AbstractController
$pid = intval(Request::input('pid'));
if (mb_strlen($name) < 2) {
return Base::retError('文件名称不可以少于2个字');
} elseif (mb_strlen($name) > 32) {
return Base::retError('文件名称最多只能设置32个字');
} elseif (mb_strlen($name) > 100) {
return Base::retError('文件名称最多只能设置100个字');
}
$tmpName = preg_replace("/[\\\\\/:*?\"<>|]/", '', $name);
if ($tmpName != $name) {
@ -276,7 +368,7 @@ class FileController extends AbstractController
}
/**
* @api {get} api/file/copy 05. 复制文件()
* @api {get} api/file/copy 复制文件()
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -299,6 +391,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'));
}
//
@ -336,7 +429,7 @@ class FileController extends AbstractController
}
/**
* @api {get} api/file/move 06. 移动文件()
* @api {get} api/file/move 移动文件()
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -383,7 +476,7 @@ class FileController extends AbstractController
throw new ApiException("{$file->name} 内含有共享文件,无法移动到另一个共享文件夹内");
}
$file->userid = $toShareFile->userid;
File::where('pids', 'LIKE', "%,{$file->id},%")->update(['userid' => $toShareFile->userid]);
$file->updateChildFilesUserid($toShareFile->userid);
}
//
$tmpId = $pid;
@ -395,7 +488,7 @@ class FileController extends AbstractController
}
} else {
$file->userid = $user->userid;
File::where('pids', 'LIKE', "%,{$file->id},%")->update(['userid' => $user->userid]);
$file->updateChildFilesUserid($user->userid);
}
//
$file->pid = $pid;
@ -411,7 +504,7 @@ class FileController extends AbstractController
}
/**
* @api {get} api/file/remove 07. 删除文件()
* @api {get} api/file/remove 删除文件()
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -450,7 +543,7 @@ class FileController extends AbstractController
}
/**
* @api {get} api/file/content 08. 获取文件内容
* @api {get} api/file/content 获取文件内容
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -498,6 +591,10 @@ class FileController extends AbstractController
return Base::retError('参数错误');
}
//
if ($down == 'no') {
File::isNeedInstallApp($file->type);
}
//
if ($only_update_at == 'yes') {
return Base::retSuccess('success', [
'id' => $file->id,
@ -510,6 +607,16 @@ class FileController extends AbstractController
$builder->whereId($history_id);
}
$content = $builder->orderByDesc('id')->first();
if (isset($user)) {
UserRecentItem::record(
$user->userid,
UserRecentItem::TYPE_FILE,
$file->id,
UserRecentItem::SOURCE_FILESYSTEM,
intval($file->pid)
);
}
if ($down === 'preview') {
return Redirect::to(FileContent::formatPreview($file, $content?->content));
}
@ -517,7 +624,7 @@ class FileController extends AbstractController
}
/**
* @api {get} api/file/content/save 09. 保存文件内容
* @api {get} api/file/content/save 保存文件内容
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -557,7 +664,7 @@ class FileController extends AbstractController
}
}
$text = strip_tags($data['content']);
if ($isRep == true) {
if ($isRep) {
$content = Base::array2json($data);
}
}
@ -572,10 +679,12 @@ class FileController extends AbstractController
$contentArray = Base::json2array($content);
$contentString = $contentArray['xml'];
$file->ext = 'drawio';
File::isNeedInstallApp($file->type);
break;
case 'mind':
$contentString = $content;
$file->ext = 'mind';
File::isNeedInstallApp($file->type);
break;
case 'txt':
case 'code':
@ -610,9 +719,9 @@ class FileController extends AbstractController
}
/**
* @api {get} api/file/office/token 10. 获取token
* @api {get} api/file/office/token 获取token
*
* @apiDescription 需要token身份
* @apiDescription 用于生成office在线编辑的token
* @apiVersion 1.0.0
* @apiGroup file
* @apiName office__token
@ -625,7 +734,7 @@ class FileController extends AbstractController
*/
public function office__token()
{
User::auth();
File::isNeedInstallApp('office');
//
$config = Request::input('config');
$token = \Firebase\JWT\JWT::encode($config, env('APP_KEY') ,'HS256');
@ -635,7 +744,7 @@ class FileController extends AbstractController
}
/**
* @api {get} api/file/content/office 11. 保存文件内容office
* @api {get} api/file/content/office 保存文件内容office
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -652,6 +761,8 @@ class FileController extends AbstractController
{
$user = User::auth();
//
File::isNeedInstallApp('office');
//
$id = intval(Request::input('id'));
$status = intval(Request::input('status'));
$key = Request::input('key');
@ -661,7 +772,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));
@ -689,7 +800,7 @@ class FileController extends AbstractController
}
/**
* @api {get} api/file/content/upload 12. 保存文件内容(上传文件)
* @api {get} api/file/content/upload 保存文件内容(上传文件)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -697,6 +808,9 @@ class FileController extends AbstractController
* @apiName content__upload
*
* @apiParam {Number} [pid] 父级ID
* @apiParam {Number} [cover] 覆盖已存在的文件
* - 0:不覆盖,保留两者(默认)
* - 1:覆盖
* @apiParam {String} [files] 文件名
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
@ -707,13 +821,24 @@ class FileController extends AbstractController
{
$user = User::auth();
$pid = intval(Request::input('pid'));
$webkitRelativePath = Request::input('webkitRelativePath');
$data = (new File)->contentUpload($user, $pid, $webkitRelativePath);
return Base::retSuccess($data['data']['name'] . ' 上传成功', $data['addItem']);
// 同一用户往相同父目录上传时排队,避免并发导致数据库死锁
try {
return Lock::withLock("file:upload:{$user->userid}:{$pid}", function () use ($user, $pid) {
$overwrite = intval(Request::input('cover'));
$webkitRelativePath = Request::input('webkitRelativePath');
$data = (new File)->contentUpload($user, $pid, $webkitRelativePath, $overwrite);
return Base::retSuccess($data['data']['name'] . ' 上传成功', $data['addItem']);
}, 120000, 120000);
} catch (\Exception $e) {
if (str_contains($e->getMessage(), 'Failed to acquire lock')) {
throw new ApiException('上传繁忙,请稍后再试');
}
throw $e;
}
}
/**
* @api {get} api/file/content/history 13. 获取内容历史
* @api {get} api/file/content/history 获取内容历史
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -745,7 +870,7 @@ class FileController extends AbstractController
}
/**
* @api {get} api/file/content/restore 14. 恢复文件历史
* @api {get} api/file/content/restore 恢复文件历史
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -768,6 +893,8 @@ class FileController extends AbstractController
//
$file = File::permissionFind($id, $user);
//
File::isNeedInstallApp($file->type);
//
$history = FileContent::whereFid($file->id)->whereId($history_id)->first();
if (empty($history)) {
return Base::retError('历史数据不存在或已被删除');
@ -785,7 +912,7 @@ class FileController extends AbstractController
}
/**
* @api {get} api/file/share 15. 获取共享信息
* @api {get} api/file/share 获取共享信息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -821,7 +948,7 @@ class FileController extends AbstractController
}
/**
* @api {get} api/file/share/update 16. 设置共享
* @api {get} api/file/share/update 设置共享
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -911,7 +1038,7 @@ class FileController extends AbstractController
}
/**
* @api {get} api/file/share/out 17. 退出共享
* @api {get} api/file/share/out 退出共享
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -945,7 +1072,7 @@ class FileController extends AbstractController
}
/**
* @api {get} api/file/link 18. 获取链接
* @api {get} api/file/link 获取链接
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -956,6 +1083,9 @@ class FileController extends AbstractController
* @apiParam {String} refresh 刷新链接
* - no: 只获取(默认)
* - yes: 刷新链接,之前的将失效
* @apiParam {String} guest_access 是否允许游客访问
* - no: 不允许(默认)
* - yes: 允许游客访问
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@ -967,15 +1097,22 @@ class FileController extends AbstractController
//
$id = intval(Request::input('id'));
$refresh = Request::input('refresh', 'no');
$guestAccess = Request::input('guest_access', 'no');
//
$file = File::permissionFind($id, $user);
// 更新文件的游客访问权限
$file->guest_access = $guestAccess === 'yes' ? 1 : 0;
$file->save();
$fileLink = $file->getShareLink($user->userid, $refresh == 'yes');
$fileLink['guest_access'] = $file->guest_access;
//
return Base::retSuccess('success', $fileLink);
}
/**
* @api {get} api/file/download/pack 19. 打包文件
* @api {get} api/file/download/pack 打包文件
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -991,9 +1128,49 @@ class FileController extends AbstractController
*/
public function download__pack()
{
if (Request::has('key')) {
$array = Down::cache_decode();
$file = $array['file'];
if (empty($file) || !file_exists(storage_path($file))) {
return Base::ajaxError("文件不存在!", [], 0, 403);
}
return Response::download(storage_path($file));
}
$user = User::auth();
if ($user->isTemp()) {
return Base::retError('无法打包下载');
}
$setting = Base::setting('fileSetting');
switch ($setting['permission_pack_type']) {
case 'admin':
if (!$user->isAdmin()) {
return Base::retError('此功能仅管理员可用');
}
break;
case 'appointAllow':
if (!in_array($user->userid, $setting['permission_pack_userids'])) {
return Base::retError('此功能仅指定用户可用');
}
break;
case 'appointProhibit':
if (in_array($user->userid, $setting['permission_pack_userids'])) {
return Base::retError('此功能已禁止使用');
}
break;
}
$ids = Request::input('ids');
$downName = Request::input('name');
$fileName = Request::input('name');
$fileName = preg_replace("/[\/\\\:\*\?\"\<\>\|]/", "", $fileName);
if (empty($fileName)) {
$fileName = 'Package_' . $user->userid;
}
$fileName .= '_' . Timer::time() . '.zip';
$filePath = "temp/file/pack/" . date("Ym", Timer::time());
$zipFile = "app/" . $filePath . "/" . $fileName;
$zipPath = storage_path($zipFile);
if (!is_array($ids) || empty($ids)) {
return Base::retError('请选择下载的文件或文件夹');
@ -1001,9 +1178,12 @@ class FileController extends AbstractController
if (count($ids) > 100) {
return Base::retError('一次最多可以下载100个文件或文件夹');
}
if (count($ids) > 100) {
return Base::retError('一次最多可以下载100个文件或文件夹');
$botUser = User::botGetOrCreate('system-msg');
if (empty($botUser)) {
return Base::retError('系统机器人不存在');
}
$dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid);
$files = [];
$totalSize = 0;
@ -1017,25 +1197,30 @@ class FileController extends AbstractController
return Base::retError('文件总大小已超过1GB请分批下载');
}
$key = Down::cache_encode([
'file' => $zipFile,
]);
$fileUrl = Base::fillUrl('api/file/download/pack?key=' . $key);
$zip = new \ZipArchive();
$zipName = 'tmp/file/' . date("Ym") . '/' . $user->userid . '/' . $downName;
$zipPath = public_path($zipName);
Base::makeDir(dirname($zipPath));
if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
return Base::retError('创建压缩文件失败');
}
go(function() use ($user, $zip, $files, $downName, $zipName) {
$userid = $user->userid;
go(function () use ($userid, $zipPath, $fileUrl, $zip, $files, $fileName, $botUser, $dialog) {
Coroutine::sleep(0.1);
// 压缩进度
$progress = 0;
$zip->registerProgressCallback(0.05, function($ratio) use ($downName, &$progress) {
$zip->registerProgressCallback(0.05, function ($ratio) use ($userid, $fileUrl, $fileName, &$progress) {
$progress = round($ratio * 100);
File::filePushMsg('compress', [
'name'=> $downName,
File::pushMsgSimple('compress', [
'name' => $fileName,
'url' => $fileUrl,
'progress' => $progress
]);
], $userid);
});
//
foreach ($files as $file) {
@ -1044,52 +1229,24 @@ class FileController extends AbstractController
$zip->close();
//
if ($progress < 100) {
File::filePushMsg('compress', [
'name'=> $downName,
File::pushMsgSimple('compress', [
'name' => $fileName,
'url' => $fileUrl,
'progress' => 100
]);
], $userid);
}
//
$botUser = User::botGetOrCreate('system-msg');
if (empty($botUser)) {
return;
}
if ($dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid)) {
$text = "<b>文件下载打包已完成。</b>";
$text .= "\n\n";
$text .= "文件名:{$downName}";
$text .= "\n";
$text .= "下载地址:".Base::fillUrl($zipName);
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, true);
}
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'file_download',
'title' => '文件下载打包已完成',
'name' => $fileName,
'size' => filesize($zipPath),
'url' => $fileUrl,
], $botUser->userid, false, false, true);
});
return Base::retSuccess('success');
}
/**
* @api {get} api/file/download/confirm 20. 确认下载
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup file
* @apiName download__confirm
*
* @apiParam {String} [name] 下载文件名
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function download__confirm()
{
$user = User::auth();
$downName = Request::input('name');
$zipName = 'tmp/file/' . date("Ym") . '/' . $user->userid . '/' . $downName;
$zipPath = public_path($zipName);
if (!file_exists($zipPath)) {
abort(403, "The file does not exist.");
}
return response()->download($zipPath);
return Base::retSuccess('success', [
'name' => $fileName,
'url' => $fileUrl,
]);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -2,15 +2,8 @@
namespace App\Http\Controllers\Api;
use App\Models\User;
use App\Models\UserBot;
use App\Models\UserCheckinMac;
use App\Models\UserCheckinRecord;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Module\Base;
use Cache;
use Carbon\Carbon;
use Request;
/**
@ -72,7 +65,9 @@ class PublicController extends AbstractController
}
/**
* {post} 签到 - 路由器openwrt上报
* {post} 签到 - 上报
* - 1、路由器openwrt签到上报
* - 2、考勤机签到上报
*
* @apiParam {String} key
* @apiParam {String} mac 使用逗号分割多个
@ -85,20 +80,30 @@ class PublicController extends AbstractController
$key = trim(Request::input('key'));
$mac = trim(Request::input('mac'));
$time = intval(Request::input('time'));
$type = trim(Request::input('type'));
//
$setting = Base::setting('checkinSetting');
if ($setting['open'] !== 'open') {
return 'function off';
}
if (!in_array('auto', $setting['modes'])) {
return 'mode off';
}
if ($key != $setting['key']) {
return 'key error';
}
if ($error = UserBot::checkinBotCheckin($mac, $time)) {
return $error;
$alreadyTip = false;
if ($type === 'face') {
if (!in_array('face', $setting['modes'])) {
return 'mode off';
}
if ($key != $setting['face_key']) {
return 'key error';
}
$alreadyTip = $setting['face_retip'] === 'open';
} else {
if (!in_array('auto', $setting['modes'])) {
return 'mode off';
}
if ($key != $setting['key']) {
return 'key error';
}
}
UserBot::checkinBotCheckin($mac, $time, $alreadyTip);
return 'success';
}
}

View File

@ -6,14 +6,16 @@ use App\Exceptions\ApiException;
use App\Models\AbstractModel;
use App\Models\ProjectTask;
use App\Models\Report;
use App\Models\ReportAnalysis;
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;
use Carbon\Carbon;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Validation\Rule;
use Request;
@ -27,13 +29,15 @@ use Illuminate\Support\Facades\Validator;
class ReportController extends AbstractController
{
/**
* @api {get} api/report/my 01. 我发送的汇报
* @api {get} api/report/my 我发送的汇报
*
* @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
@ -47,15 +51,31 @@ class ReportController extends AbstractController
{
$user = User::auth();
//
$builder = Report::with(['receivesUser'])->whereUserid($user->userid);
$builder = Report::with(['receivesUser'])
->select(Report::LIST_FIELDS)
->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']}%");
});
} elseif (Base::isNumber($keys['key'])) {
$builder->where(function ($query) use ($keys) {
$query->where("id", intval($keys['key']))
->orWhere("title", "LIKE", "%{$keys['key']}%");
});
} else {
$builder->where("title", "LIKE", "%{$keys['key']}%");
}
}
if (in_array($keys['type'], [Report::WEEKLY, Report::DAILY])) {
$builder->whereType($keys['type']);
}
if (is_array($keys['created_at'])) {
if ($keys['created_at'][0] > 0) $builder->where('created_at', '>=', date('Y-m-d H:i:s', Base::dayTimeF($keys['created_at'][0])));
if ($keys['created_at'][1] > 0) $builder->where('created_at', '<=', date('Y-m-d H:i:s', Base::dayTimeE($keys['created_at'][1])));
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());
}
}
$list = $builder->orderByDesc('created_at')->paginate(Base::getPaginate(50, 20));
@ -63,15 +83,18 @@ class ReportController extends AbstractController
}
/**
* @api {get} api/report/receive 02. 我接收的汇报
* @api {get} api/report/receive 我接收的汇报
*
* @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
@ -83,39 +106,59 @@ class ReportController extends AbstractController
public function receive(): array
{
$user = User::auth();
$builder = Report::with(['receivesUser']);
$builder = Report::with(['receivesUser'])
->select(Report::LIST_FIELDS);
$builder->whereHas("receivesUser", function ($query) use ($user) {
$query->where("report_receives.userid", $user->userid);
});
$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(function ($query) use ($keys) {
$query->where("userid", intval($keys['key']))
->orWhere("id", intval($keys['key']))
->orWhere("title", "LIKE", "%{$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', '>=', date('Y-m-d H:i:s', Base::dayTimeF($keys['created_at'][0])));
if ($keys['created_at'][1] > 0) $builder->where('created_at', '<=', date('Y-m-d H:i:s', Base::dayTimeE($keys['created_at'][1])));
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());
}
}
$list = $builder->orderByDesc('created_at')->paginate(Base::getPaginate(50, 20));
if ($list->items()) {
foreach ($list->items() as $item) {
$item->receive_time = ReportReceive::query()->whereRid($item["id"])->whereUserid($user->userid)->value("receive_time");
$item->receive_at = ReportReceive::query()->whereRid($item["id"])->whereUserid($user->userid)->value("receive_at");
}
}
return Base::retSuccess('success', $list);
}
/**
* @api {get} api/report/store 03. 保存并发送工作汇报
* @api {get} api/report/store 保存并发送工作汇报
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup report
* @apiName store
@ -175,7 +218,7 @@ class ReportController extends AbstractController
foreach ($input["receive"] as $userid) {
$input["receive_content"][] = [
"receive_time" => Carbon::now()->toDateTimeString(),
"receive_at" => Carbon::now()->toDateTimeString(),
"userid" => $userid,
"read" => 0,
];
@ -191,7 +234,6 @@ class ReportController extends AbstractController
$report->updateInstance([
"title" => $input["title"],
"type" => $input["type"],
"content" => htmlspecialchars($input["content"]),
]);
} else {
// 生成唯一标识
@ -205,11 +247,25 @@ class ReportController extends AbstractController
"title" => $input["title"],
"type" => $input["type"],
"userid" => $user->userid,
"content" => htmlspecialchars($input["content"]),
]);
}
$report->save();
// 保存内容
$content = $input["content"];
preg_match_all("/<img\s+src=\"data:image\/(png|jpg|jpeg|webp);base64,(.*?)\"/s", $content, $matchs);
foreach ($matchs[2] as $key => $text) {
$tmpPath = "uploads/report/" . Carbon::parse($report->created_at)->format("Ym") . "/" . $report->id . "/attached/";
Base::makeDir(public_path($tmpPath));
$tmpPath .= md5($text) . "." . $matchs[1][$key];
if (Base::saveContentImage(public_path($tmpPath), base64_decode($text))) {
$paramet = getimagesize(public_path($tmpPath));
$content = str_replace($matchs[0][$key], '<img src="' . Base::fillUrl($tmpPath) . '" original-width="' . $paramet[0] . '" original-height="' . $paramet[1] . '"', $content);
}
}
$report->content = htmlspecialchars($content);
$report->save();
// 删除关联
$report->Receives()->delete();
if ($input["receive_content"]) {
@ -239,8 +295,9 @@ class ReportController extends AbstractController
}
/**
* @api {get} api/report/template 04. 生成汇报模板
* @api {get} api/report/template 生成汇报模板
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup report
* @apiName template
@ -260,6 +317,7 @@ class ReportController extends AbstractController
$offset = abs(intval(Request::input("offset", 0)));
$id = intval(Request::input("offset", 0));
$now_dt = trim(Request::input("date")) ? Carbon::parse(Request::input("date")) : Carbon::now();
// 获取开始时间
if ($type === Report::DAILY) {
$start_time = Carbon::today();
@ -281,9 +339,18 @@ class ReportController extends AbstractController
$start_time->startOfWeek();
$end_time = Carbon::instance($start_time)->endOfWeek();
}
// 周报时预计算下一周期时间范围(下周)
$next_start_time = null;
$next_end_time = null;
if ($type === Report::WEEKLY) {
$next_start_time = Carbon::instance($start_time)->copy()->addWeek();
$next_end_time = Carbon::instance($end_time)->copy()->addWeek();
}
// 生成唯一标识
$sign = Report::generateSign($type, 0, Carbon::instance($start_time));
$one = Report::whereSign($sign)->whereType($type)->first();
// 如果已经提交了相关汇报
if ($one && $id > 0) {
return Base::retSuccess('success', [
@ -294,8 +361,16 @@ class ReportController extends AbstractController
]);
}
// 表格头部
$labels = [
Doo::translate('项目'),
Doo::translate('任务'),
Doo::translate('负责人'),
Doo::translate('备注'),
];
// 已完成的任务
$completeContent = "";
$completeDatas = [];
$complete_task = ProjectTask::query()
->whereNotNull("complete_at")
->whereBetween("complete_at", [$start_time->toDateTimeString(), $end_time->toDateTimeString()])
@ -306,57 +381,109 @@ class ReportController extends AbstractController
->get();
if ($complete_task->isNotEmpty()) {
foreach ($complete_task as $task) {
// 排除取消态任务:不将已取消任务计入“已完成工作”
if (ProjectTask::isCanceledFlowName($task->flow_item_name)) {
continue;
}
$complete_at = Carbon::parse($task->complete_at);
$pre = $type == Report::WEEKLY ? ('<span>[' . Doo::translate('周' . ['日', '一', '二', '三', '四', '五', '六'][$complete_at->dayOfWeek]) . ']</span>&nbsp;') : '';
$completeContent .= "<li>{$pre}[{$task->project->name}] {$task->name}</li>";
$remark = $type == Report::WEEKLY ? ('<div style="text-align:center">[' . Doo::translate('周' . ['日', '一', '二', '三', '四', '五', '六'][$complete_at->dayOfWeek]) . ']</div>') : '&nbsp;';
$completeDatas[] = [
$task->project->name,
$task->name,
$task->taskUser->where("owner", 1)->map(function ($item) {
return User::userid2nickname($item->userid);
})->implode(", "),
$remark,
];
}
} else {
$completeContent = '<li>&nbsp;</li>';
}
// 未完成的任务
$unfinishedContent = "";
$unfinished_task = ProjectTask::query()
->whereNull("complete_at")
->whereNotNull("start_at")
->where("end_at", "<", $end_time->toDateTimeString())
->whereHas("taskUser", function ($query) use ($user) {
$query->where("userid", $user->userid);
})
->orderByDesc("id")
->get();
$unfinishedDatas = [];
$unfinished_task = ProjectTask::buildUnfinishedTaskQuery($user->userid, $start_time, $end_time, true)->get();
if ($unfinished_task->isNotEmpty()) {
foreach ($unfinished_task as $task) {
empty($task->end_at) || $end_at = Carbon::parse($task->end_at);
$pre = (!empty($end_at) && $end_at->lt($now_dt)) ? '<span style="color:#ff0000;">[' . Doo::translate('超期') . ']</span>&nbsp;' : '';
$unfinishedContent .= "<li>{$pre}[{$task->project->name}] {$task->name}</li>";
$remark = (!empty($end_at) && $end_at->lt($now_dt)) ? '<div style="color:#ff0000;text-align:center">[' . Doo::translate('超期') . ']</div>' : '&nbsp;';
$unfinishedDatas[] = [
$task->project->name,
$task->name,
$task->taskUser->where("owner", 1)->map(function ($item) {
return User::userid2nickname($item->userid);
})->implode(", "),
$remark,
];
}
} else {
$unfinishedContent = '<li>&nbsp;</li>';
}
// 生成标题
if ($type === Report::WEEKLY) {
$title = $user->nickname . "的周报[" . $start_time->format("m/d") . "-" . $end_time->format("m/d") . "]";
$title .= "[" . $start_time->month . "月第" . $start_time->weekOfMonth . "周]";
$unfinishedTitle = '本周未完成的工作';
} else {
$title = $user->nickname . "的日报[" . $start_time->format("Y/m/d") . "]";
$unfinishedTitle = '今日未完成的工作';
}
$title = Doo::translate($title);
// 生成内容
$content = '<h2>' . Doo::translate('已完成工作') . '</h2><ol>' .
$completeContent . '</ol><h2>' .
Doo::translate('未完成的工作') . '</h2><ol>' .
$unfinishedContent . '</ol>';
$contents = [];
$contents[] = '<h2>' . Doo::translate('已完成工作') . '</h2>';
$contents[] = view('report', [
'labels' => $labels,
'datas' => $completeDatas,
])->render();
$contents[] = '<p>&nbsp;</p>';
$contents[] = '<h2>' . Doo::translate($unfinishedTitle) . '</h2>';
$contents[] = view('report', [
'labels' => $labels,
'datas' => $unfinishedDatas,
])->render();
if ($type === Report::WEEKLY) {
$content .= "<h2>" . Doo::translate("下周拟定计划") . "[" . $start_time->addWeek()->format("m/d") . "-" . $end_time->addWeek()->format("m/d") . "]</h2><ol><li>&nbsp;</li></ol>";
// 下周拟定计划:基于下周时间范围预生成候选任务
$nextPlanDatas = [];
if ($next_start_time && $next_end_time) {
$next_tasks = ProjectTask::buildUnfinishedTaskQuery($user->userid, $next_start_time, $next_end_time, false)->get();
if ($next_tasks->isNotEmpty()) {
foreach ($next_tasks as $task) {
$planTime = '-';
if ($task->start_at || $task->end_at) {
$startText = $task->start_at ? Carbon::parse($task->start_at)->format('Y-m-d H:i') : '';
$endText = $task->end_at ? Carbon::parse($task->end_at)->format('Y-m-d H:i') : '';
$planTime = trim($startText . ($endText ? (' ~ ' . $endText) : ''));
}
$nextPlanDatas[] = [
'[' . $task->project->name . '] ' . $task->name,
$planTime,
$task->taskUser->where("owner", 1)->map(function ($item) {
return User::userid2nickname($item->userid);
})->implode(", "),
];
}
}
}
$contents[] = '<p>&nbsp;</p>';
$contents[] = "<h2>" . Doo::translate("下周拟定计划") . "[" . $next_start_time->format("m/d") . "-" . $next_end_time->format("m/d") . "]</h2>";
$contents[] = view('report', [
'labels' => [
Doo::translate('计划描述'),
Doo::translate('计划时间'),
Doo::translate('负责人'),
],
'datas' => $nextPlanDatas,
])->render();
}
$data = [
"time" => $start_time->toDateTimeString(),
"sign" => $sign,
"title" => $title,
"content" => $content,
"complete_task" => $complete_task,
"unfinished_task" => $unfinished_task,
"content" => implode("", $contents),
];
if ($one) {
$data['id'] = $one->id;
}
@ -364,13 +491,15 @@ class ReportController extends AbstractController
}
/**
* @api {get} api/report/detail 05. 报告详情
* @api {get} api/report/detail 报告详情
*
* @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 返回信息(错误描述)
@ -379,30 +508,145 @@ 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");
}
$analysis = ReportAnalysis::query()
->whereRid($one->id)
->whereUserid($user->userid)
->first();
if ($analysis) {
$updatedAt = $analysis->updated_at ? $analysis->updated_at->toDateTimeString() : null;
$one->setAttribute('ai_analysis', [
'id' => $analysis->id,
'text' => $analysis->analysis_text,
'model' => $analysis->model,
'updated_at' => $updatedAt,
]);
} else {
$one->setAttribute('ai_analysis', null);
}
return Base::retSuccess("success", $one);
}
/**
* @api {get} api/report/mark 06. 标记已读/未读
* @api {post} api/report/analysave 保存工作汇报 AI 分析
*
* @apiDescription 需要token身份仅支持报告提交人或接收人保存分析
* @apiVersion 1.0.0
* @apiGroup report
* @apiName analysave
*
* @apiParam {Number} id 报告ID
* @apiParam {String} text 分析内容Markdown
* @apiParam {String} [model] 分析使用的模型标识(可选)
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
* @apiSuccess {Number} data.id 分析记录ID
* @apiSuccess {String} data.text 分析内容Markdown
* @apiSuccess {String} data.updated_at 最近更新时间
*/
public function analysave(): array
{
$user = User::auth();
$id = intval(Request::input("id"));
if ($id <= 0) {
return Base::retError("缺少ID参数");
}
$text = trim((string)Request::input('text', ''));
if ($text === '') {
return Base::retError("分析内容不能为空");
}
$model = trim((string)Request::input('model', ''));
$report = Report::getOne($id);
if (!$this->userCanAccessReport($report, $user)) {
return Base::retError("无权访问该工作汇报");
}
$analysis = ReportAnalysis::query()
->whereRid($report->id)
->whereUserid($user->userid)
->first();
if (!$analysis) {
$analysis = ReportAnalysis::fillInstance([
'rid' => $report->id,
'userid' => $user->userid,
]);
}
$viewerRole = $user->profession ?: (is_array($user->identity) && !empty($user->identity) ? implode('/', $user->identity) : null);
$focusMeta = null;
$focus = Request::input('focus');
if (is_array($focus)) {
$focusMeta = array_filter(array_map('trim', $focus));
} elseif (is_string($focus) && trim($focus) !== '') {
$focusMeta = [trim($focus)];
}
$meta = array_filter([
'viewer_role' => $viewerRole,
'viewer_name' => $user->nickname ?? null,
'focus' => $focusMeta,
], function ($value) {
if (is_array($value)) {
return !empty($value);
}
return $value !== null && $value !== '';
});
$analysis->updateInstance([
'model' => $model,
'analysis_text' => $text,
'meta' => $meta,
]);
$analysis->save();
$analysis->refresh();
return Base::retSuccess("success", [
'id' => $analysis->id,
'text' => $analysis->analysis_text,
'model' => $analysis->model,
'updated_at' => $analysis->updated_at ? $analysis->updated_at->toDateTimeString() : null,
]);
}
/**
* @api {get} api/report/mark 标记已读/未读
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup report
* @apiName mark
@ -443,8 +687,71 @@ class ReportController extends AbstractController
}
/**
* @api {get} api/report/last_submitter 07. 获取最后一次提交的接收人
* @api {get} api/report/share 分享报告到消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup report
* @apiName share
*
* @apiParam {Number} id 报告id
* @apiParam {Array} dialogids 转发给的对话ID
* @apiParam {Array} userids 转发给的成员ID
* @apiParam {String} leave_message 转发留言
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function share()
{
$user = User::auth();
//
$id = Request::input('id');
$dialogids = Request::input('dialogids');
$userids = Request::input('userids');
$leave_message = Request::input('leave_message');
//
if (is_array($id)) {
if (count(Base::arrayRetainInt($id)) > 20) {
return Base::retError("最多只能操作20条数据");
}
$builder = Report::whereIn("id", Base::arrayRetainInt($id));
} else {
$builder = Report::whereId(intval($id));
}
$reportMsgs = [];
$builder ->chunkById(100, function ($list) use (&$reportMsgs, $user) {
/** @var Report $item */
foreach ($list as $item) {
$reportLink = ReportLink::generateLink($item->id, $user->userid);
$reportMsgs[] = "<a class=\"mention report\" href=\"{{RemoteURL}}single/report/detail/{$reportLink['code']}\" target=\"_blank\">%{$item->title}</a>";
}
});
if (empty($reportMsgs)) {
return Base::retError("报告不存在或已被删除");
}
$reportTag = count($reportMsgs) > 1 ? 'li' : 'p';
$reportAttr = $reportTag === 'li' ? ' data-list="ordered"' : '';
$reportMsgs = array_map(function ($item) use ($reportAttr, $reportTag) {
return "<{$reportTag}{$reportAttr}>{$item}</{$reportTag}>";
}, $reportMsgs);
if ($reportTag === 'li') {
array_unshift($reportMsgs, "<ol>");
$reportMsgs[] = "</ol>";
}
if ($leave_message) {
$reportMsgs[] = "<p>{$leave_message}</p>";
}
$msgText = implode("", $reportMsgs);
//
return WebSocketDialogMsg::sendMsgBatch($user, $userids, $dialogids, $msgText);
}
/**
* @api {get} api/report/last_submitter 获取最后一次提交的接收人
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup report
* @apiName last_submitter
@ -460,8 +767,9 @@ class ReportController extends AbstractController
}
/**
* @api {get} api/report/unread 08. 获取未读
* @api {get} api/report/unread 获取未读
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup report
* @apiName unread
@ -474,15 +782,19 @@ class ReportController extends AbstractController
{
$user = User::auth();
//
$data = Report::whereHas("Receives", function (Builder $query) use ($user) {
$query->where("userid", $user->userid)->where("read", 0);
})->orderByDesc('created_at')->paginate(Base::getPaginate(50, 20));
return Base::retSuccess("success", $data);
$total = Report::select('reports.id')
->join('report_receives', 'report_receives.rid', '=', 'reports.id')
->where('report_receives.userid', $user->userid)
->where('report_receives.read', 0)
->count();
//
return Base::retSuccess("success", compact("total"));
}
/**
* @api {get} api/report/read 09. 标记汇报已读,可批量
* @api {get} api/report/read 标记汇报已读,可批量
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup report
* @apiName read
@ -518,4 +830,22 @@ class ReportController extends AbstractController
}
return Base::retSuccess("success", $data);
}
/**
* 判断当前用户是否有权限查看/分析指定工作汇报
* @param Report $report
* @param User $user
* @return bool
*/
protected function userCanAccessReport(Report $report, User $user): bool
{
if ($report->userid === $user->userid) {
return true;
}
return ReportReceive::query()
->whereRid($report->id)
->whereUserid($user->userid)
->exists();
}
}

View File

@ -0,0 +1,619 @@
<?php
namespace App\Http\Controllers\Api;
use Request;
use App\Models\File;
use App\Models\Project;
use App\Models\ProjectTask;
use App\Models\User;
use App\Models\UserTag;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Module\Base;
use App\Module\Apps;
use App\Module\Manticore\ManticoreFile;
use App\Module\Manticore\ManticoreUser;
use App\Module\Manticore\ManticoreProject;
use App\Module\Manticore\ManticoreTask;
use App\Module\Manticore\ManticoreMsg;
/**
* @apiDefine search
*
* 智能搜索
*/
class SearchController extends AbstractController
{
/**
* @api {get} api/search/contact 搜索联系人
*
* @apiDescription 需要token身份优先使用 Manticore Search未安装则使用 MySQL 搜索
* @apiVersion 1.0.0
* @apiGroup search
* @apiName contact
*
* @apiParam {String} key 搜索关键词
* @apiParam {String} [search_type] 搜索类型text/vector/hybrid默认hybrid Manticore 有效)
* @apiParam {Number} [take] 获取数量默认20最大50
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function contact()
{
User::auth();
$key = trim(Request::input('key'));
$searchType = Request::input('search_type', 'hybrid');
$take = Base::getPaginate(50, 20, 'take');
if (empty($key)) {
return Base::retSuccess('success', []);
}
// 优先使用 Manticore 搜索
if (Apps::isInstalled('search')) {
$results = ManticoreUser::search($key, $searchType, $take);
// 补充用户完整信息
$userids = array_column($results, 'userid');
if (!empty($userids)) {
$users = User::whereIn('userid', $userids)
->select(User::$basicField)
->get()
->keyBy('userid');
foreach ($results as &$item) {
$userData = $users->get($item['userid']);
if ($userData) {
// 标签直接从 Manticore 搜索结果获取(空格分隔的字符串转数组)
$tagsStr = $item['tags'] ?? '';
$searchTags = !empty($tagsStr) ? preg_split('/\s+/', trim($tagsStr)) : [];
$item = array_merge($userData->toArray(), [
'relevance' => $item['relevance'] ?? 0,
'introduction_preview' => $item['introduction_preview'] ?? null,
'search_tags' => $searchTags,
]);
}
}
}
} else {
// MySQL 回退搜索
$results = $this->searchContactByMysql($key, $take);
}
return Base::retSuccess('success', $results);
}
/**
* MySQL 回退搜索联系人
*
* @param string $key 搜索关键词
* @param int $take 获取数量
* @return array
*/
private function searchContactByMysql(string $key, int $take): array
{
$users = User::select(User::$basicField)
->where('bot', 0)
->whereNull('disable_at')
->searchByKeyword($key)
->orderByDesc('line_at')
->take($take)
->get();
// 获取用户标签
$userids = $users->pluck('userid')->toArray();
$userTags = $this->getUserTagsMap($userids);
return $users->map(function ($user) use ($userTags) {
return array_merge($user->toArray(), [
'relevance' => 0,
'introduction_preview' => null,
'search_tags' => $userTags[$user->userid] ?? [],
]);
})->toArray();
}
/**
* @api {get} api/search/project 搜索项目
*
* @apiDescription 需要token身份优先使用 Manticore Search未安装则使用 MySQL 搜索
* @apiVersion 1.0.0
* @apiGroup search
* @apiName project
*
* @apiParam {String} key 搜索关键词
* @apiParam {String} [search_type] 搜索类型text/vector/hybrid默认hybrid Manticore 有效)
* @apiParam {Number} [take] 获取数量默认20最大50
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function project()
{
$user = User::auth();
$key = trim(Request::input('key'));
$searchType = Request::input('search_type', 'hybrid');
$take = Base::getPaginate(50, 20, 'take');
if (empty($key)) {
return Base::retSuccess('success', []);
}
// 优先使用 Manticore 搜索
if (Apps::isInstalled('search')) {
$results = ManticoreProject::search($user->userid, $key, $searchType, $take);
// 补充项目完整信息
$projectIds = array_column($results, 'project_id');
if (!empty($projectIds)) {
$projects = Project::whereIn('id', $projectIds)
->get()
->keyBy('id');
foreach ($results as &$item) {
$projectData = $projects->get($item['project_id']);
if ($projectData) {
$item = array_merge($projectData->toArray(), [
'relevance' => $item['relevance'] ?? 0,
'desc_preview' => $item['desc_preview'] ?? null,
]);
}
}
}
} else {
// MySQL 回退搜索
$results = $this->searchProjectByMysql($user->userid, $key, $take);
}
return Base::retSuccess('success', $results);
}
/**
* MySQL 回退搜索项目
*
* @param int $userid 用户ID
* @param string $key 搜索关键词
* @param int $take 获取数量
* @return array
*/
private function searchProjectByMysql(int $userid, string $key, int $take): array
{
$projects = Project::authData()
->whereNull('projects.archived_at')
->searchByKeyword($key)
->orderByDesc('projects.id')
->take($take)
->get();
return $projects->map(function ($project) {
$array = $project->toArray();
$array['relevance'] = 0;
$array['desc_preview'] = null;
return $array;
})->toArray();
}
/**
* @api {get} api/search/task 搜索任务
*
* @apiDescription 需要token身份优先使用 Manticore Search未安装则使用 MySQL 搜索
* @apiVersion 1.0.0
* @apiGroup search
* @apiName task
*
* @apiParam {String} key 搜索关键词
* @apiParam {String} [search_type] 搜索类型text/vector/hybrid默认hybrid Manticore 有效)
* @apiParam {Number} [take] 获取数量默认20最大50
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function task()
{
$user = User::auth();
$key = trim(Request::input('key'));
$searchType = Request::input('search_type', 'hybrid');
$take = Base::getPaginate(50, 20, 'take');
if (empty($key)) {
return Base::retSuccess('success', []);
}
// 优先使用 Manticore 搜索
if (Apps::isInstalled('search')) {
$results = ManticoreTask::search($user->userid, $key, $searchType, $take);
// 补充任务完整信息
$taskIds = array_column($results, 'task_id');
if (!empty($taskIds)) {
$tasks = ProjectTask::with(['taskUser', 'taskTag'])
->whereIn('id', $taskIds)
->get()
->keyBy('id');
foreach ($results as &$item) {
$taskData = $tasks->get($item['task_id']);
if ($taskData) {
$item = array_merge($taskData->toArray(), [
'relevance' => $item['relevance'] ?? 0,
'desc_preview' => $item['desc_preview'] ?? null,
'content_preview' => $item['content_preview'] ?? null,
]);
}
}
}
} else {
// MySQL 回退搜索
$results = $this->searchTaskByMysql($user->userid, $key, $take);
}
return Base::retSuccess('success', $results);
}
/**
* MySQL 回退搜索任务
*
* @param int $userid 用户ID
* @param string $key 搜索关键词
* @param int $take 获取数量
* @return array
*/
private function searchTaskByMysql(int $userid, string $key, int $take): array
{
$tasks = ProjectTask::with(['taskUser', 'taskTag'])
->whereIn('project_tasks.project_id', function ($query) use ($userid) {
$query->select('project_id')
->from('project_users')
->where('userid', $userid);
})
->whereNull('project_tasks.archived_at')
->whereNull('project_tasks.deleted_at')
->searchByKeyword($key)
->orderByDesc('project_tasks.id')
->take($take)
->get();
return $tasks->map(function ($task) {
$array = $task->toArray();
$array['relevance'] = 0;
$array['desc_preview'] = null;
$array['content_preview'] = null;
return $array;
})->toArray();
}
/**
* @api {get} api/search/file 搜索文件
*
* @apiDescription 需要token身份优先使用 Manticore Search未安装则使用 MySQL 搜索
* @apiVersion 1.0.0
* @apiGroup search
* @apiName file
*
* @apiParam {String} key 搜索关键词
* @apiParam {String} [search_type] 搜索类型text/vector/hybrid默认hybrid Manticore 有效)
* @apiParam {Number} [take] 获取数量默认20最大50
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function file()
{
$user = User::auth();
$key = trim(Request::input('key'));
$searchType = Request::input('search_type', 'hybrid');
$take = Base::getPaginate(50, 20, 'take');
if (empty($key)) {
return Base::retSuccess('success', []);
}
// 优先使用 Manticore 搜索
if (Apps::isInstalled('search')) {
$results = ManticoreFile::search($user->userid, $key, $searchType, 0, $take);
// 补充文件完整信息
$fileIds = array_column($results, 'file_id');
if (!empty($fileIds)) {
$files = File::whereIn('id', $fileIds)
->get()
->keyBy('id');
$formattedResults = [];
foreach ($results as $item) {
$fileData = $files->get($item['file_id']);
if ($fileData) {
$formattedResults[] = array_merge($fileData->toArray(), [
'relevance' => $item['relevance'] ?? 0,
'content_preview' => $item['content_preview'] ?? null,
]);
}
}
return Base::retSuccess('success', $formattedResults);
}
return Base::retSuccess('success', []);
} else {
// MySQL 回退搜索
$results = $this->searchFileByMysql($user->userid, $key, $take);
return Base::retSuccess('success', $results);
}
}
/**
* MySQL 回退搜索文件
*
* @param int $userid 用户ID
* @param string $key 搜索关键词
* @param int $take 获取数量
* @return array
*/
private function searchFileByMysql(int $userid, string $key, int $take): array
{
$results = [];
// 搜索用户自己的文件
$ownFiles = File::where('userid', $userid)
->searchByKeyword($key)
->take($take)
->get();
foreach ($ownFiles as $file) {
$results[] = array_merge($file->toArray(), [
'relevance' => 0,
'content_preview' => null,
]);
}
// 搜索共享给用户的文件
$remaining = $take - count($results);
if ($remaining > 0) {
$sharedFiles = File::sharedToUser($userid)
->searchByKeyword($key)
->take($remaining)
->get();
foreach ($sharedFiles as $file) {
$temp = $file->toArray();
if ($file->pshare === $file->id) {
$temp['pid'] = 0;
}
$temp['relevance'] = 0;
$temp['content_preview'] = null;
$results[] = $temp;
}
}
return $results;
}
/**
* @api {get} api/search/message 搜索消息
*
* @apiDescription 需要token身份优先使用 Manticore Search未安装则使用 MySQL 搜索
* @apiVersion 1.0.0
* @apiGroup search
* @apiName message
*
* @apiParam {String} key 搜索关键词
* @apiParam {String} [search_type] 搜索类型text/vector/hybrid默认hybrid Manticore 有效)
* @apiParam {Number} [take] 获取数量默认20最大50
* @apiParam {String} [mode] 返回模式message/position/dialog默认message
* - message: 返回消息详细信息
* - position: 只返回消息ID
* - dialog: 返回对话级数据
* @apiParam {Number} [dialog_id] 对话ID筛选指定对话内的消息
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function message()
{
$user = User::auth();
$key = trim(Request::input('key'));
$searchType = Request::input('search_type', 'hybrid');
$take = Base::getPaginate(50, 20, 'take');
$mode = Request::input('mode', 'message');
$dialogId = intval(Request::input('dialog_id', 0));
// 验证 mode 参数
if (!in_array($mode, ['message', 'position', 'dialog'])) {
$mode = 'message';
}
if (empty($key)) {
return Base::retSuccess('success', []);
}
// 如果指定了 dialog_id需要验证用户有权限访问该对话
if ($dialogId > 0) {
WebSocketDialog::checkDialog($dialogId);
}
// 优先使用 Manticore 搜索
if (Apps::isInstalled('search')) {
$results = ManticoreMsg::search($user->userid, $key, $searchType, 0, $take, $dialogId);
} else {
// MySQL 回退搜索
$results = $this->searchMessageByMysql($user->userid, $key, $take, $dialogId);
}
// 根据 mode 返回不同格式的数据
return $this->formatMessageResults($results, $mode, $user->userid);
}
/**
* MySQL 回退搜索消息
*
* @param int $userid 用户ID
* @param string $key 搜索关键词
* @param int $take 获取数量
* @param int $dialogId 对话ID0表示不限制
* @return array
*/
private function searchMessageByMysql(int $userid, string $key, int $take, int $dialogId = 0): array
{
$builder = WebSocketDialogMsg::select([
'id as msg_id',
'dialog_id',
'userid',
'type',
'msg',
'created_at',
])
->accessibleByUser($userid)
->where('bot', 0)
->searchByKeyword($key);
if ($dialogId > 0) {
$builder->where('dialog_id', $dialogId);
}
$items = $builder->orderByDesc('id')
->limit($take)
->get();
return $items->map(function ($item) {
return [
'msg_id' => $item->msg_id,
'dialog_id' => $item->dialog_id,
'userid' => $item->userid,
'type' => $item->type,
'msg' => $item->msg,
'created_at' => $item->created_at,
'relevance' => 0,
'content_preview' => null,
];
})->toArray();
}
/**
* 格式化消息搜索结果
*
* @param array $results 搜索结果
* @param string $mode 返回模式
* @param int $userid 用户ID
* @return \Illuminate\Http\JsonResponse
*/
private function formatMessageResults(array $results, string $mode, int $userid)
{
switch ($mode) {
case 'position':
// 只返回消息ID
$data = array_column($results, 'msg_id');
return Base::retSuccess('success', compact('data'));
case 'dialog':
// 返回对话级数据
$list = [];
$seenDialogs = [];
foreach ($results as $item) {
$dialogIdFromResult = $item['dialog_id'];
// 每个对话只返回一次
if (isset($seenDialogs[$dialogIdFromResult])) {
continue;
}
$seenDialogs[$dialogIdFromResult] = true;
if ($dialog = WebSocketDialog::find($dialogIdFromResult)) {
$dialogData = array_merge($dialog->toArray(), [
'search_msg_id' => $item['msg_id'],
]);
$list[] = WebSocketDialog::synthesizeData($dialogData, $userid);
}
}
return Base::retSuccess('success', ['data' => $list]);
case 'message':
default:
// 返回消息详细信息(默认行为)
$msgIds = array_column($results, 'msg_id');
if (!empty($msgIds)) {
$msgs = WebSocketDialogMsg::whereIn('id', $msgIds)
->with(['user' => function ($query) {
$query->select(User::$basicField);
}])
->get()
->keyBy('id');
// 创建结果映射以保持原始顺序和额外字段
$resultsMap = [];
foreach ($results as $item) {
$resultsMap[$item['msg_id']] = $item;
}
$formattedResults = [];
foreach ($msgIds as $msgId) {
$msgData = $msgs->get($msgId);
$originalItem = $resultsMap[$msgId] ?? [];
if ($msgData) {
$formattedResults[] = [
'id' => $msgData->id,
'msg_id' => $msgData->id,
'dialog_id' => $msgData->dialog_id,
'userid' => $msgData->userid,
'type' => $msgData->type,
'msg' => $msgData->msg,
'created_at' => $msgData->created_at,
'user' => $msgData->user,
'relevance' => $originalItem['relevance'] ?? 0,
'content_preview' => $originalItem['content_preview'] ?? null,
];
}
}
return Base::retSuccess('success', $formattedResults);
}
return Base::retSuccess('success', []);
}
}
/**
* 批量获取用户标签映射
*
* @param array $userids 用户ID数组
* @return array 用户ID => 标签名称数组的映射
*/
private function getUserTagsMap(array $userids): array
{
if (empty($userids)) {
return [];
}
// 获取所有用户的标签(带认可数)
$tags = UserTag::whereIn('user_id', $userids)
->withCount('recognitions')
->get();
// 按用户分组,每个用户取 Top 10 标签
$result = [];
foreach ($userids as $userid) {
$result[$userid] = [];
}
$userTags = $tags->groupBy('user_id');
foreach ($userTags as $userid => $tagCollection) {
$result[$userid] = $tagCollection
->sortByDesc('recognitions_count')
->take(10)
->pluck('name')
->values()
->toArray();
}
return $result;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
<?php
namespace App\Http\Controllers\Api;
/**
* 测试
*/
class TestController extends AbstractController
{
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,378 @@
# apiDoc 参数标签说明(完整速查)
apiDoc 使用内联注释为 RESTful API 自动生成文档。
以下为所有官方支持的参数与其说明。
---
## @api
**定义 API 方法的基本信息**
```js
@api {method} path title
```
- **method**:请求方法,如 `GET``POST``PUT``DELETE`
- **path**:请求路径,例如 `/user/:id`
- **title**:简短标题(显示在文档中)
📘 示例:
```js
@api {get} /user/:id Get user info
```
---
## @apiBody
**定义请求体参数**
```js
@apiBody [{type}] [field=defaultValue] [description]
```
- `{type}` 参数类型(如 String, Number, Object, String[]
- `[field]` 可选字段(方括号表示可选)
- `=defaultValue` 默认值
- `description` 参数说明
📘 示例:
```js
@apiBody {String} lastname Mandatory Lastname.
@apiBody {Object} [address] Optional address object.
@apiBody {String} [address[city]] Optional city.
```
---
## @apiDefine
**定义可复用的文档块**
```js
@apiDefine name [title] [description]
```
- `name`:唯一标识
- `title`:简短标题
- `description`:多行描述
📘 示例:
```js
@apiDefine MyError
@apiError UserNotFound The <code>id</code> of the User was not found.
```
---
## @apiDeprecated
**标记接口为弃用状态**
```js
@apiDeprecated [text]
```
- `text`:提示文本,可带链接到新方法
📘 示例:
```js
@apiDeprecated use now (#User:GetDetails)
```
---
## @apiDescription
**描述接口详细说明**
```js
@apiDescription text
```
📘 示例:
```js
@apiDescription This is the Description.
It is multiline capable.
```
---
## @apiError
**定义错误返回参数**
```js
@apiError [(group)] [{type}] field [description]
```
📘 示例:
```js
@apiError UserNotFound The id of the User was not found.
```
---
## @apiErrorExample
**定义错误返回示例**
```js
@apiErrorExample [{type}] [title]
example
```
📘 示例:
```js
@apiErrorExample {json} Error-Response:
HTTP/1.1 404 Not Found
{ "error": "UserNotFound" }
```
---
## @apiExample
**定义接口使用示例**
```js
@apiExample [{type}] title
example
```
📘 示例:
```js
@apiExample {curl} Example usage:
curl -i http://localhost/user/4711
```
---
## @apiGroup
**定义所属分组**
```js
@apiGroup name
```
📘 示例:
```js
@apiGroup User
```
---
## @apiHeader
**定义请求头参数**
```js
@apiHeader [(group)] [{type}] [field=defaultValue] [description]
```
📘 示例:
```js
@apiHeader {String} access-key Users unique access-key.
```
---
## @apiHeaderExample
**定义请求头示例**
```js
@apiHeaderExample [{type}] [title]
example
```
📘 示例:
```js
@apiHeaderExample {json} Header-Example:
{
"Accept-Encoding": "gzip, deflate"
}
```
---
## @apiIgnore
**忽略当前文档块**
```js
@apiIgnore [hint]
```
📘 示例:
```js
@apiIgnore Not finished method
```
---
## @apiName
**定义接口唯一名称**
```js
@apiName name
```
📘 示例:
```js
@apiName GetUser
```
---
## @apiParam
**定义请求参数**
```js
@apiParam [(group)] [{type}] [field=defaultValue] [description]
```
📘 示例:
```js
@apiParam {Number} id Users unique ID.
@apiParam {String} [firstname] Optional firstname.
@apiParam {String} country="DE" Mandatory with default.
```
---
## @apiParamExample
**定义参数请求示例**
```js
@apiParamExample [{type}] [title]
example
```
📘 示例:
```js
@apiParamExample {json} Request-Example:
{ "id": 4711 }
```
---
## @apiPermission
**定义权限要求**
```js
@apiPermission name
```
📘 示例:
```js
@apiPermission admin
```
---
## @apiPrivate
**标记接口为私有(可过滤)**
```js
@apiPrivate
```
---
## @apiQuery
**定义查询参数(?query**
```js
@apiQuery [{type}] [field=defaultValue] [description]
```
📘 示例:
```js
@apiQuery {Number} id Users unique ID.
@apiQuery {String} [sort="asc"] Sort order.
```
---
## @apiSampleRequest
**定义接口测试请求 URL**
```js
@apiSampleRequest url
```
📘 示例:
```js
@apiSampleRequest http://test.github.com/some_path/
```
---
## @apiSuccess
**定义成功返回参数**
```js
@apiSuccess [(group)] [{type}] field [description]
```
📘 示例:
```js
@apiSuccess {String} firstname Firstname of the User.
@apiSuccess {String} lastname Lastname of the User.
```
---
## @apiSuccessExample
**定义成功返回示例**
```js
@apiSuccessExample [{type}] [title]
example
```
📘 示例:
```js
@apiSuccessExample {json} Success-Response:
HTTP/1.1 200 OK
{ "firstname": "John", "lastname": "Doe" }
```
---
## @apiUse
**引用定义块(@apiDefine**
```js
@apiUse name
```
📘 示例:
```js
@apiDefine MySuccess
@apiSuccess {String} firstname User firstname.
@apiUse MySuccess
```
---
## @apiVersion
**定义接口版本**
```js
@apiVersion version
```
📘 示例:
```js
@apiVersion 1.6.2
```
---
# 附录:常用标签速查表
| 标签 | 作用 | 示例 |
|------|------|------|
| `@api` | 定义接口 | `@api {get} /user/:id` |
| `@apiName` | 唯一名称 | `@apiName GetUser` |
| `@apiGroup` | 所属分组 | `@apiGroup User` |
| `@apiParam` | 请求参数 | `@apiParam {Number} id Users unique ID.` |
| `@apiBody` | 请求体参数 | `@apiBody {String} name Username.` |
| `@apiQuery` | 查询参数 | `@apiQuery {String} keyword Search term.` |
| `@apiHeader` | Header 参数 | `@apiHeader {String} token Auth token.` |
| `@apiSuccess` | 成功返回字段 | `@apiSuccess {String} name Username.` |
| `@apiError` | 错误返回字段 | `@apiError NotFound User not found.` |
| `@apiVersion` | 版本号 | `@apiVersion 1.0.0` |

View File

@ -1,89 +1,137 @@
<?php
/**
* 给apidoc项目增加顺序编号
* 给apidoc项目增加顺序编号 / 支持恢复
*/
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
$path = dirname(__FILE__). '/';
$lists = scandir($path);
//
foreach ($lists AS $item) {
$fillPath = $path . $item;
if (str_ends_with($fillPath, 'Controller.php')) {
$content = file_get_contents($fillPath);
preg_match_all("/\* @api \{(.+?)\} (.*?)\n/i", $content, $matchs);
$i = 1;
foreach ($matchs[2] AS $key=>$text) {
if (in_array(strtolower($matchs[1][$key]), array('get', 'post'))) {
$expl = explode(" ", __sRemove($text));
$end = $expl[1];
if ($expl[2]) {
$end = '';
foreach ($expl AS $k=>$v) { if ($k >= 2) { $end.= " ".$v; } }
}
$newtext = "* @api {".$matchs[1][$key]."} ".$expl[0]." ".__zeroFill($i, 2).". ".trim($end);
$content = str_replace("* @api {".$matchs[1][$key]."} ".$text, $newtext, $content);
$i++;
//
echo $newtext;
echo "\r\n";
}
}
if ($i > 1) {
file_put_contents($fillPath, $content);
}
}
}
echo "Success \n";
const NUMBER_WIDTH = 2;
/** ************************************************************** */
/** ************************************************************** */
/** ************************************************************** */
$isRestore = isset($argv[1]) && strtolower($argv[1]) === 'restore';
/**
* 替换所有空格
* @param $str
* @return mixed
*/
function __sRemove($str) {
$str = str_replace(" ", " ", $str);
if (__strExists($str, " ")) {
return __sRemove($str);
}
return $str;
$basePath = dirname(__FILE__) . '/';
$controllerFiles = glob($basePath . '*Controller.php');
if (!$controllerFiles) {
echo "No Controller.php files found\n";
exit(0);
}
foreach ($controllerFiles as $filePath) {
$original = file_get_contents($filePath);
[$updated, $linesChanged] = processFile($original, $isRestore);
if (count($linesChanged) === 0) {
continue;
}
file_put_contents($filePath, $updated);
foreach ($linesChanged as $line) {
echo $line . "\n";
}
}
echo $isRestore ? "Restore Success \n" : "Success \n";
/**
* 是否包含字符
* @param $string
* @param $find
* @return bool
* 处理单个文件内容
*
* @param string $content
* @param bool $restore
* @return array{string, array<int, string>}
*/
function __strExists($string, $find)
function processFile(string $content, bool $restore): array
{
return str_contains($string, $find);
$lineChanges = [];
$counter = 1;
$pattern = '/\* @api \{([^\}]+)\}\s+([^\s]+)([^\r\n]*)(\r?\n)/';
$updated = preg_replace_callback(
$pattern,
function (array $matches) use ($restore, &$counter, &$lineChanges) {
$method = trim($matches[1]);
if (!in_array(strtolower($method), ['get', 'post'], true)) {
return $matches[0];
}
$endpoint = trim($matches[2]);
$suffix = normalizeDescription(stripExistingNumbering($matches[3]));
if (!$restore) {
$numberedSuffix = formatNumber($counter) . '.';
if ($suffix !== '') {
$numberedSuffix .= ' ' . $suffix;
}
$counter++;
} else {
$numberedSuffix = $suffix;
}
$newLine = renderAnnotation($method, $endpoint, $numberedSuffix);
if ($newLine !== rtrim($matches[0], "\r\n")) {
$lineChanges[] = $newLine;
}
return $newLine . $matches[4];
},
$content
);
if ($updated === null) {
return [$content, []];
}
return [$updated, $lineChanges];
}
/**
* @param string $str 补零
* @param int $length
* @param int $after
* @return bool|string
* 生成格式化后的注释行
*/
function __zeroFill($str, $length = 0, $after = 1) {
if (strlen($str) >= $length) {
return $str;
function renderAnnotation(string $method, string $endpoint, string $suffix = ''): string
{
$line = "* @api {" . $method . "} " . $endpoint;
if ($suffix !== '') {
if ($suffix[0] !== ' ') {
$line .= ' ';
}
$line .= $suffix;
}
$_str = '';
for ($i = 0; $i < $length; $i++) {
$_str .= '0';
}
if ($after) {
$_ret = substr($_str . $str, $length * -1);
} else {
$_ret = substr($str . $_str, 0, $length);
}
return $_ret;
return $line;
}
/**
* 移除已有编号部分
*/
function stripExistingNumbering(string $text): string
{
$trimmed = ltrim($text);
$pattern = '/^\d+\.\s*/';
return preg_replace($pattern, '', $trimmed) ?? $trimmed;
}
/**
* 压缩多余空格
*/
function normalizeDescription(string $text): string
{
$text = trim($text);
if ($text === '') {
return '';
}
return preg_replace('/\s+/', ' ', $text) ?? $text;
}
/**
* 生成固定宽度的数字
*/
function formatNumber(int $number): string
{
return str_pad((string) $number, NUMBER_WIDTH, '0', STR_PAD_LEFT);
}

View File

@ -8,20 +8,25 @@ use Request;
use Redirect;
use Response;
use App\Models\File;
use App\Module\Doo;
use App\Module\Base;
use App\Tasks\LoopTask;
use App\Module\Extranet;
use App\Tasks\AppPushTask;
use App\Module\RandomColor;
use App\Tasks\LoopTask;
use App\Tasks\AppPushTask;
use App\Tasks\JokeSoupTask;
use App\Tasks\DeleteTmpTask;
use App\Tasks\EmailNoticeTask;
use App\Tasks\AutoArchivedTask;
use App\Tasks\DeleteBotMsgTask;
use App\Tasks\CheckinRemindTask;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use App\Tasks\CloseMeetingRoomTask;
use App\Tasks\ManticoreSyncTask;
use App\Tasks\UnclaimedTaskRemindTask;
use LasseRafn\InitialAvatarGenerator\InitialAvatar;
use App\Tasks\TodoRemindTask;
use App\Tasks\AiTaskLoopTask;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Laravolt\Avatar\Avatar;
/**
@ -37,9 +42,8 @@ class IndexController extends InvokeController
if ($action) {
$app .= "__" . $action;
}
if ($app === 'manifest.txt') {
$app = 'manifest';
$child = 'txt';
if ($app == 'default') {
return '';
}
if (!method_exists($this, $app)) {
$app = method_exists($this, $method) ? $method : 'main';
@ -59,58 +63,21 @@ class IndexController extends InvokeController
$array = Base::json2array(file_get_contents($hotFile));
$style = null;
$script = preg_replace("/^(\/\/(.*?))(:\d+)?\//i", "$1:" . $array['APP_DEV_PORT'] . "/", asset_main("resources/assets/js/app.js"));
$proxyUri = Base::liveEnv('VSCODE_PROXY_URI');
if (is_string($proxyUri) && preg_match('/^https?:\/\//i', $proxyUri)) {
$script = preg_replace('/^(https?:\/\/|\/\/)[^\/]+/', rtrim($proxyUri, '/'), $script, 1);
}
} else {
$array = Base::json2array(file_get_contents($manifestFile));
$style = asset_main($array['resources/assets/js/app.js']['css'][0]);
$script = asset_main($array['resources/assets/js/app.js']['file']);
}
return response()->view('main', [
'system_alias' => Base::settingFind('system', 'system_alias', 'WebPage'),
'version' => Base::getVersion(),
'style' => $style,
'script' => $script,
])->header('Link', "<" . url('manifest.txt') . ">; rel=\"prefetch\"");
}
/**
* Manifest
* @param $child
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response|string
*/
public function manifest($child = '')
{
if (empty($child)) {
$murl = url('manifest.txt');
return response($murl)->header('Link', "<{$murl}>; rel=\"prefetch\"");
}
$array = [
"office/web-apps/apps/api/documents/api.js?hash=" . Base::getVersion(),
"office/7.5.1-23/web-apps/vendor/requirejs/require.js",
"office/7.5.1-23/web-apps/apps/api/documents/api.js",
"office/7.5.1-23/sdkjs/common/AllFonts.js",
"office/7.5.1-23/web-apps/vendor/xregexp/xregexp-all-min.js",
"office/7.5.1-23/web-apps/vendor/sockjs/sockjs.min.js",
"office/7.5.1-23/web-apps/vendor/jszip/jszip.min.js",
"office/7.5.1-23/web-apps/vendor/jszip-utils/jszip-utils.min.js",
"office/7.5.1-23/sdkjs/common/libfont/wasm/fonts.js",
"office/7.5.1-23/sdkjs/common/Charts/ChartStyles.js",
"office/7.5.1-23/sdkjs/slide/themes//themes.js",
"office/7.5.1-23/web-apps/apps/presentationeditor/main/app.js",
"office/7.5.1-23/sdkjs/slide/sdk-all-min.js",
"office/7.5.1-23/sdkjs/slide/sdk-all.js",
"office/7.5.1-23/web-apps/apps/documenteditor/main/app.js",
"office/7.5.1-23/sdkjs/word/sdk-all-min.js",
"office/7.5.1-23/sdkjs/word/sdk-all.js",
"office/7.5.1-23/web-apps/apps/spreadsheeteditor/main/app.js",
"office/7.5.1-23/sdkjs/cell/sdk-all-min.js",
"office/7.5.1-23/sdkjs/cell/sdk-all.js",
];
foreach ($array as &$item) {
$item = url($item);
}
return implode(PHP_EOL, $array);
]);
}
/**
@ -122,9 +89,18 @@ class IndexController extends InvokeController
return Redirect::to(Base::fillUrl('api/system/version'), 301);
}
/**
* 健康检查
* @return string
*/
public function health()
{
return "ok";
}
/**
* 头像
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response|\Symfony\Component\HttpFoundation\BinaryFileResponse
*/
public function avatar()
{
@ -132,34 +108,120 @@ class IndexController extends InvokeController
if ($segment && preg_match('/.*?\.png$/i', $segment)) {
$name = substr($segment, 0, -4);
} else {
$name = Request::input('name', 'H');
$name = Request::input('name', 'D');
}
$size = Request::input('size', 128);
$color = Request::input('color');
$background = Request::input('background');
// 移除各种括号及其内容
$pattern = '/[(\[【{<<『「](.*?)[)\]】}>>』」]/u';
$name = preg_replace($pattern, '', $name) ?: preg_replace($pattern, '$1', $name);
// 移除常见标识词(不区分大小写)
$filterWords = [
// 测试相关
'测试', '测试号', '测试账号', '内测', '体验', '试用', 'test', 'testing', 'beta',
// 账号相关
'账号', '帐号', '账户', '帐户', 'account', 'acc', 'id', 'uid',
// 临时标识
'临时', '暂用', '备用', '主号', '副号', '小号', '大号', 'temp', 'temporary', 'backup',
// 系统相关
'系统', '管理员', 'admin', 'administrator', 'system', 'sys', 'root',
// 用户相关
'用户', 'user', '会员', 'member', 'vip', 'svip', 'mvip', 'premium',
// 官方相关
'官方', '正式', '认证', 'official', 'verified', 'auth',
// 客服相关
'客服', '售后', '服务', 'service', 'support', 'helper', 'assistant',
// 游戏相关
'game', 'gaming', 'player', 'gamer',
// 社交媒体相关
'ins', 'instagram', 'fb', 'facebook', 'tiktok', 'tweet', 'weibo', 'wechat',
// 常见后缀
'official', 'real', 'fake', 'copy', 'channel', 'studio', 'team', 'group',
// 职业相关
'dev', 'developer', 'designer', 'artist', 'writer', 'editor',
// 其他
'bot', 'robot', 'auto', 'anonymous', 'guest', 'default', 'new', 'old'
];
$filterWords = array_map(function ($word) {
return preg_quote($word, '/');
}, $filterWords);
$name = preg_replace('/' . implode('|', $filterWords) . '/iu', '', $name) ?: $name;
// 移除分隔符和特殊字符
$filterSymbols = [
// 常见分隔符
'-', '_', '=', '+', '/', '\\', '|',
'~', '@', '#', '$', '%', '^', '&', '*',
// 空格类字符
' ', ' ', "\t", "\n", "\r",
// 标点符号(中英文)
'。', '', '、', '', '', '', '',
'', '…', '‥', '', '″', '℃',
'.', ',', ';', ':', '?', '!',
// 引号类(修正版)
'"', "'", '', '', '“', '”', '`',
// 特殊符号
'★', '☆', '○', '●', '◎', '◇', '◆',
'□', '■', '△', '▲', '▽', '▼',
'♀', '♂', '♪', '♫', '♯', '♭', '♬',
'→', '←', '↑', '↓', '↖', '↗', '↙', '↘',
'√', '×', '÷', '±', '∵', '∴',
'♠', '♥', '♣', '♦',
// emoji 表情符号范围
'\x{1F300}-\x{1F9FF}',
'\x{2600}-\x{26FF}',
'\x{2700}-\x{27BF}',
'\x{1F900}-\x{1F9FF}',
'\x{1F600}-\x{1F64F}'
];
$filterSymbols = array_map(function ($symbol) {
return preg_quote($symbol, '/');
}, $filterSymbols);
$name = preg_replace('/[' . implode('', $filterSymbols) . ']/u', '', $name) ?: $name;
//
if (preg_match('/^[\x{4e00}-\x{9fa5}]+$/u', $name)) {
$name = mb_substr($name, mb_strlen($name) - 2);
}
if (empty($name)) {
$name = 'D';
}
if (empty($color)) {
$color = '#ffffff';
$cacheKey = "avatarBackgroundColor::" . md5($name);
$background = Cache::rememberForever($cacheKey, function() {
$background = Cache::rememberForever($cacheKey, function () {
return RandomColor::one(['luminosity' => 'dark']);
});
}
//
$avatar = new InitialAvatar();
$content = $avatar->name($name)
->size($size)
->color($color)
->background($background)
->fontSize(0.35)
->autoFont()
->generate()
->stream('png', 100);
$path = public_path('uploads/tmp/avatar/' . substr(md5($name), 0, 2));
$file = Base::joinPath($path, md5($name) . '.png');
if (file_exists($file)) {
return response()->file($file, [
'Pragma' => 'public',
'Cache-Control' => 'max-age=1814400',
'Content-type' => 'image/png',
'Expires' => gmdate('D, d M Y H:i:s \G\M\T', time() + 1814400),
]);
}
Base::makeDir($path);
//
return response($content)
$avatar = new Avatar([
'shape' => 'square',
'width' => $size,
'height' => $size,
'chars' => 2,
'fontSize' => $size / 2.9,
'uppercase' => true,
'fonts' => [resource_path('assets/statics/fonts/Source_Han_Sans_SC_Regular.otf')],
'foregrounds' => [$color],
'backgrounds' => [$background],
'border' => [
'size' => 0,
'color' => 'foreground',
'radius' => 0,
],
]);
return response($avatar->create($name)->save($file))
->header('Pragma', 'public')
->header('Cache-Control', 'max-age=1814400')
->header('Content-type', 'image/png')
@ -192,11 +254,13 @@ class IndexController extends InvokeController
// App推送
Task::deliver(new AppPushTask());
// 删除过期的临时表数据
Task::deliver(new DeleteTmpTask('wg_tmp_msgs', 1));
Task::deliver(new DeleteTmpTask('task_worker', 12));
Task::deliver(new DeleteTmpTask('tmp_msgs', 1));
Task::deliver(new DeleteTmpTask('tmp'));
Task::deliver(new DeleteTmpTask('task_worker', 12));
Task::deliver(new DeleteTmpTask('file'));
Task::deliver(new DeleteTmpTask('file_pack'));
Task::deliver(new DeleteTmpTask('tmp_file', 24));
Task::deliver(new DeleteTmpTask('user_device', 24));
Task::deliver(new DeleteTmpTask('umeng_log', 24 * 3));
// 删除机器人消息
Task::deliver(new DeleteBotMsgTask());
// 周期任务
@ -207,6 +271,14 @@ class IndexController extends InvokeController
Task::deliver(new JokeSoupTask());
// 未领取任务通知
Task::deliver(new UnclaimedTaskRemindTask());
// 待办提醒
Task::deliver(new TodoRemindTask());
// 关闭会议室
Task::deliver(new CloseMeetingRoomTask());
// Manticore Search 同步
Task::deliver(new ManticoreSyncTask());
// AI 任务建议
Task::deliver(new AiTaskLoopTask());
return "success";
}
@ -222,80 +294,127 @@ class IndexController extends InvokeController
if (strtolower($name) === 'latest') {
$name = $latestVersion;
}
// 上传
// 上传header 中包含 publish-version
if (preg_match("/^\d+\.\d+\.\d+$/", $publishVersion)) {
// 判断密钥
$publishKey = Request::header('publish-key');
if ($publishKey !== env('APP_KEY')) {
return Base::retError("key error");
}
if (version_compare($publishVersion, $latestVersion) > -1) { // 限制上传版本必须 ≥ 当前版本
$publishPath = "uploads/desktop/{$publishVersion}/";
$res = Base::upload([
"file" => Request::file('file'),
"type" => 'desktop',
"path" => $publishPath,
"fileName" => true
]);
if (Base::isSuccess($res)) {
file_put_contents($latestFile, $publishVersion);
// 判断版本
$action = Request::get('action');
$draftPath = "uploads/desktop-draft/{$publishVersion}/";
if ($action === 'release') {
// 将草稿版本发布为正式版本
$draftPath = public_path($draftPath);
$releasePath = public_path("uploads/desktop/{$publishVersion}/");
if (!file_exists($draftPath)) {
return Base::retError("draft version not exists");
}
return $res;
if (file_exists($releasePath)) {
Base::deleteDirAndFile($releasePath);
}
Base::copyDirectory($draftPath, $releasePath);
file_put_contents($latestFile, $publishVersion);
// 删除旧版本
Base::deleteDirAndFile(public_path("uploads/desktop-draft"));
$dirs = Base::recursiveDirs(public_path("uploads/desktop"), false);
sort($dirs);
$num = 0;
foreach ($dirs as $dir) {
if (!preg_match("/\/\d+\.\d+\.\d+$/", $dir)) {
continue;
}
$num++;
if ($num < 5) {
continue; // 保留最新的5个版本
}
if (filemtime($dir) > time() - 3600 * 24 * 30) {
continue; // 保留最近30天的版本
}
Base::deleteDirAndFile($dir);
}
return Base::retSuccess('success');
}
// 上传草稿版本
return Base::upload([
"file" => Request::file('file'),
"type" => 'publish',
"path" => $draftPath,
"saveName" => true,
]);
}
// 列表
if (preg_match("/^\d+\.\d+\.\d+$/", $name)) {
$path = "uploads/desktop/{$name}";
$dirPath = public_path($path);
$lists = Base::readDir($dirPath);
// 列表(访问路径 desktop/publish/{version}
if (preg_match("/^v*(\d+\.\d+\.\d+)$/", $name, $match)) {
$paths = [
"uploads/desktop/{$match[1]}/",
"uploads/desktop/v{$match[1]}/",
"uploads/desktop-draft/{$match[1]}/",
"uploads/desktop-draft/v{$match[1]}/",
];
$avaiPath = null;
foreach ($paths as $path) {
$dirPath = public_path($path);
$isDraft = str_contains($path, 'draft');
if (is_dir($dirPath)) {
$avaiPath = $path;
break;
}
}
abort_if(empty($avaiPath), 404);
$lists = Base::recursiveFiles($dirPath, false);
$files = [];
foreach ($lists as $file) {
if (str_ends_with($file, '.yml') || str_ends_with($file, '.yaml')) {
if (preg_match('/\.(zip|yml|yaml|blockmap)$/i', $file) || str_ends_with($file, '-win.exe')) {
continue;
}
$fileName = Base::leftDelete($file, $dirPath);
$fileName = basename($file, $dirPath);
$fileSize = filesize($file);
$files[] = [
'name' => substr($fileName, 1),
'name' => $fileName,
'time' => date("Y-m-d H:i:s", filemtime($file)),
'size' => Base::readableBytes(filesize($file)),
'url' => Base::fillUrl($path . $fileName),
'size' => $fileSize > 0 ? Base::readableBytes($fileSize) : 0,
'url' => Base::fillUrl(Base::joinPath($avaiPath, $fileName)),
];
}
$otherVersion = [];
$dirs = Base::recursiveDirs(public_path("uploads/desktop"), false);
foreach ($dirs as $dir) {
if (!preg_match("/\/\d+\.\d+\.\d+$/", $dir)) {
continue;
}
$version = basename($dir);
if ($version === $match[1]) {
continue;
}
$otherVersion[] = [
'version' => $version,
'url' => Base::fillUrl("desktop/publish/{$version}"),
];
}
//
$path = "uploads/android";
$dirPath = public_path($path);
$lists = Base::readDir($dirPath);
$apkFile = null;
foreach ($lists as $file) {
if (!str_ends_with($file, '.apk')) {
continue;
}
if ($apkFile && strtotime($apkFile['time']) > filemtime($file)) {
continue;
}
$fileName = Base::leftDelete($file, $dirPath);
$apkFile = [
'name' => substr($fileName, 1),
'time' => date("Y-m-d H:i:s", filemtime($file)),
'size' => Base::readableBytes(filesize($file)),
'url' => Base::fillUrl($path . $fileName),
];
}
if ($apkFile) {
$files = array_merge([$apkFile], $files);
}
return view('desktop', ['version' => $name, 'files' => $files]);
return view('desktop', [
'system_alias' => Base::settingFind('system', 'system_alias', 'WebPage'),
'version' => $match[1],
'files' => $files,
'is_draft' => $isDraft,
'latest_version' => $latestVersion,
'other_version' => array_reverse($otherVersion),
]);
}
// 下载
if ($name && file_exists($latestFile)) {
$publishVersion = file_get_contents($latestFile);
if (preg_match("/^\d+\.\d+\.\d+$/", $publishVersion)) {
$filePath = public_path("uploads/desktop/{$publishVersion}/{$name}");
if (file_exists($filePath)) {
return Response::download($filePath);
}
// 下载Latest 版本内的文件,访问路径 desktop/publish/{fileName}
if ($name) {
$filePath = public_path("uploads/desktop/{$latestVersion}/{$name}");
if (file_exists($filePath)) {
return Response::download($filePath);
}
}
return abort(404);
// 404
abort(404);
}
/**
@ -321,125 +440,77 @@ class IndexController extends InvokeController
$data = parse_url($key);
$path = Arr::get($data, 'path');
$file = public_path($path);
// 防止 ../ 穿越获取到系统文件
abort_if(!str_starts_with(realpath($file), public_path()), 404);
// 如果文件不存在,直接返回 404
abort_if(!file_exists($file), 404);
//
if (file_exists($file)) {
parse_str($data['query'], $query);
$name = Arr::get($query, 'name');
$ext = strtolower(Arr::get($query, 'ext'));
$userAgent = strtolower(Request::server('HTTP_USER_AGENT'));
if ($ext === 'pdf'
&& (str_contains($userAgent, 'electron') || str_contains($userAgent, 'chrome'))) {
parse_str($data['query'], $query);
$name = Arr::get($query, 'name');
$ext = strtolower(Arr::get($query, 'ext'));
$userAgent = strtolower(Request::server('HTTP_USER_AGENT'));
if ($ext === 'pdf') {
// 文件超过 10m 不支持在线预览,提示下载
if (filesize($file) > 10 * 1024 * 1024) {
return view('download', [
'system_alias' => Base::settingFind('system', 'system_alias', 'WebPage'),
'name' => $name,
'size' => Base::readableBytes(filesize($file)),
'url' => Base::fillUrl($path),
'button' => Doo::translate('点击下载'),
]);
}
// 浏览器类型
$browser = 'none';
if (str_contains($userAgent, 'chrome') || str_contains($userAgent, 'android_kuaifan_eeui')) {
$browser = str_contains($userAgent, 'android_kuaifan_eeui') ? 'android-mobile' : 'chrome-desktop';
} elseif (str_contains($userAgent, 'safari') || str_contains($userAgent, 'ios_kuaifan_eeui')) {
$browser = str_contains($userAgent, 'ios_kuaifan_eeui') ? 'safari-mobile' : 'safari-desktop';
}
// electron 直接在线预览查看
if (str_contains($userAgent, 'electron') || str_contains($browser, 'desktop')) {
return Response::download($file, $name, [
'Content-Type' => 'application/pdf'
], 'inline');
}
//
if (in_array($ext, File::localExt)) {
$url = Base::fillUrl($path);
} else {
$url = 'http://' . env('APP_IPPR') . '.3/' . $path;
// EEUI App 直接在线预览查看
if (Base::isEEUIApp() && Base::judgeClientVersion("0.34.47")) {
if ($browser === 'safari-mobile') {
$redirectUrl = Base::fillUrl($path);
return <<<EOF
<script>
window.top.postMessage({
action: "eeuiAppSendMessage",
data: [
{
action: 'setPageData', // 设置页面数据
data: {
showProgress: true,
titleFixed: true,
urlFixed: true,
}
},
{
action: 'createTarget', // 创建目标(访问新地址)
url: "{$redirectUrl}",
}
]
}, "*")
</script>
EOF;
}
}
if ($ext !== 'pdf') {
$url = Base::urlAddparameter($url, [
'fullfilename' => $name . '.' . $ext
]);
}
$toUrl = Base::fillUrl("fileview/onlinePreview?url=" . urlencode(base64_encode($url)));
return Redirect::to($toUrl, 301);
}
return abort(404);
}
/**
* 设置语言和皮肤
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function setting__theme_language()
{
return view('setting', [
'theme' => Request::input('theme'),
'language' => Request::input('language')
//
if (in_array($ext, File::localExt)) {
$url = Base::fillUrl($path);
} else {
$url = 'http://nginx/' . $path;
}
$url = Base::urlAddparameter($url, [
'fullfilename' => Base::rightDelete($name, '.' . $ext) . '_' . filemtime($file) . '.' . $ext
]);
}
/**
* 提取所有中文
* @return array|string
*/
public function allcn()
{
if (!Base::is_internal_ip(Base::getIp())) {
// 限制内网访问
return "Forbidden Access";
}
$list = Base::readDir(resource_path());
$array = [];
foreach ($list as $item) {
$content = file_get_contents($item);
preg_match_all("/\\\$L\((.*?)\)/", $content, $matchs);
if ($matchs) {
foreach ($matchs[1] as $text) {
$array[trim(trim($text, '"'), "'")] = trim(trim($text, '"'), "'");
}
}
}
return array_values($array);
}
/**
* 提取所有中文
* @return array|string
*/
public function allcn__php()
{
if (!Base::is_internal_ip(Base::getIp())) {
// 限制内网访问
return "Forbidden Access";
}
$list = Base::readDir(app_path());
$array = [];
foreach ($list as $item) {
$content = file_get_contents($item);
preg_match_all("/(retSuccess|retError|ApiException)\((.*?)[,|)]/", $content, $matchs);
if ($matchs) {
foreach ($matchs[2] as $text) {
$array[trim(trim($text, '"'), "'")] = trim(trim($text, '"'), "'");
}
}
}
return array_values($array);
}
/**
* 提取所有中文
* @return array|string
*/
public function allcn__all()
{
if (!Base::is_internal_ip(Base::getIp())) {
// 限制内网访问
return "Forbidden Access";
}
$list = array_merge(Base::readDir(app_path()), Base::readDir(resource_path()));
$array = [];
foreach ($list as $item) {
if (Base::rightExists($item, ".php") || Base::rightExists($item, ".vue") || Base::rightExists($item, ".js")) {
$content = file_get_contents($item);
preg_match_all("/(['\"])(.*?)[\u{4e00}-\u{9fa5}\u{FE30}-\u{FFA0}]+([\s\S]((?!\n).)*)\\1/u", $content, $matchs);
if ($matchs) {
foreach ($matchs[0] as $text) {
$tmp = preg_replace("/\/\/(.*?)$/", "", $text);
$tmp = preg_replace("/\/\/(.*?)\n/", "", $tmp);
$tmp = str_replace("", "", $tmp);
if (!preg_match("/[\u{4e00}-\u{9fa5}\u{FE30}-\u{FFA0}]/u", $tmp)){
continue; // 没有中文
}
$val = trim(trim($text, '"'), "'");
$array[md5($val)] = $val;
}
}
}
}
return implode("\n", array_values($array));
$redirectUrl = Base::fillUrl("fileview/onlinePreview?url=" . urlencode(base64_encode($url)));
return Redirect::to($redirectUrl, 301);
}
}

View File

@ -2,10 +2,7 @@
namespace App\Http\Controllers;
use App\Models\User;
use App\Module\Base;
use App\Tasks\IhttpTask;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
@ -32,24 +29,7 @@ class InvokeController extends BaseController
$msg = "404 not found (" . str_replace("__", "/", $app) . ").";
return Base::ajaxError($msg);
}
// 使用websocket请求
$apiWebsocket = Request::header('Api-Websocket');
if ($apiWebsocket) {
$userid = User::userid();
if ($userid > 0) {
$url = 'http://127.0.0.1:' . env('LARAVELS_LISTEN_PORT') . Request::getRequestUri();
$task = new IhttpTask($url, Request::post(), [
'Content-Type' => Request::header('Content-Type'),
'language' => Request::header('language'),
'token' => Request::header('token'),
]);
$task->setApiWebsocket($apiWebsocket);
$task->setApiUserid($userid);
Task::deliver($task);
return Base::retSuccess('wait');
}
}
// 正常请求
//
$res = $this->__before($method, $action);
if ($res === true || Base::isSuccess($res)) {
return $this->$app();

View File

@ -10,14 +10,19 @@ class TrustProxies extends Middleware
/**
* The trusted proxies for this application.
*
* PHPSwoole只在内网被 nginx 访问,外部无法直连,故信任内网代理。
*
* @var array|string|null
*/
protected $proxies;
protected $proxies = '*';
/**
* The headers that should be used to detect proxies.
*
* 只采信 X-Forwarded-Protonginx 已用 $the_scheme 覆盖该头(值由 nginx 控制),
* 据此让 url() 实时跟随 httpshost/for 一律不信,避免 Host 注入与 IP 伪造。
*
* @var int
*/
protected $headers = Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO | Request::HEADER_X_FORWARDED_AWS_ELB;
protected $headers = Request::HEADER_X_FORWARDED_PROTO;
}

View File

@ -4,7 +4,10 @@ namespace App\Http\Middleware;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Module\Base;
use App\Module\Doo;
use App\Services\RequestContext;
use Cache;
use Closure;
class WebApi
@ -18,11 +21,23 @@ class WebApi
*/
public function handle($request, Closure $next)
{
global $_A;
$_A = [];
// 记录请求信息
RequestContext::set('start_time', microtime(true));
RequestContext::set('header_language', $request->header('language'));
// 更新请求的基本URL
RequestContext::updateBaseUrl($request);
// 加载Doo类
Doo::load();
// 记录 PC 端活跃时间
$userid = Doo::userId();
if ($userid > 0 && Base::isPc()) {
Cache::put("user_pc_active:{$userid}", time(), 60);
}
// 解密请求内容
$encrypt = Doo::pgpParseStr($request->header('encrypt'));
if ($request->isMethod('post')) {
$version = $request->header('version');
@ -41,12 +56,7 @@ class WebApi
}
}
// 强制 https
$APP_SCHEME = env('APP_SCHEME', 'auto');
if (in_array(strtolower($APP_SCHEME), ['https', 'on', 'ssl', '1', 'true', 'yes'], true)) {
$request->setTrustedProxies([$request->getClientIp()], $request::HEADER_X_FORWARDED_PROTO);
}
// 执行下一个中间件
$response = $next($request);
// 加密返回内容
@ -57,6 +67,16 @@ class WebApi
}
}
// 返回响应
return $response;
}
/**
* @return void
*/
public function terminate()
{
// 请求结束后清理上下文
RequestContext::clean();
}
}

View File

@ -2,8 +2,10 @@
namespace App\Ldap;
use App\Exceptions\ApiException;
use App\Models\User;
use App\Module\Base;
use App\Services\RequestContext;
use LdapRecord\Configuration\ConfigurationException;
use LdapRecord\Container;
use LdapRecord\LdapRecordException;
@ -11,20 +13,18 @@ use LdapRecord\Models\Model;
class LdapUser extends Model
{
protected static $init = null;
/**
* The object classes of the LDAP model.
*
* @var array
*/
public static $objectClasses = [
'inetOrgPerson',
'organizationalPerson',
'person',
'top',
'posixAccount',
];
private static $emailAttrs = ['mail', 'cn', 'uid', 'userPrincipalName'];
/**
* @return mixed|null
*/
@ -68,19 +68,29 @@ class LdapUser extends Model
return Base::settingFind('thirdAccessSetting', 'ldap_sync_local') === 'open';
}
/**
* 获取登录属性名
* @return string
*/
public static function getLoginAttr(): string
{
$attr = Base::settingFind('thirdAccessSetting', 'ldap_login_attr');
return in_array($attr, ['cn', 'uid', 'mail', 'sAMAccountName', 'userPrincipalName']) ? $attr : 'cn';
}
/**
* 初始化配置
* @return bool
*/
public static function initConfig()
{
if (is_bool(self::$init)) {
return self::$init;
if (RequestContext::has('ldap_init')) {
return RequestContext::get('ldap_init');
}
//
$setting = Base::setting('thirdAccessSetting');
if ($setting['ldap_open'] !== 'open') {
return self::$init = false;
return RequestContext::save('ldap_init', false);
}
//
$connection = Container::getDefaultConnection();
@ -92,15 +102,15 @@ class LdapUser extends Model
"username" => $setting['ldap_user_dn'],
"password" => $setting['ldap_password'],
]);
return self::$init = true;
return RequestContext::save('ldap_init', true);
} catch (ConfigurationException $e) {
info($e->getMessage());
return self::$init = false;
return RequestContext::save('ldap_init', false);
}
}
/**
* 获取
* 通过管理员绑定搜索用户,然后用用户 DN Bind 认证
* @param $username
* @param $password
* @return Model|null
@ -111,16 +121,68 @@ class LdapUser extends Model
return null;
}
try {
return self::static()
->where([
'cn' => $username,
'userPassword' => $password
])->first();
$loginAttr = self::getLoginAttr();
$row = self::static()
->whereRaw($loginAttr, '=', $username)
->first();
if (!$row) {
return null;
}
$connection = Container::getDefaultConnection();
if (!$connection->auth()->attempt($row->getDn(), $password)) {
return null;
}
// Swoole 下连接共享,必须恢复管理员绑定
$connection->auth()->attempt(
$connection->getConfiguration()->get('username'),
$connection->getConfiguration()->get('password')
);
return $row;
} catch (\Exception $e) {
info("[LDAP] auth fail: " . $e->getMessage());
return null;
}
}
/**
* 通过邮箱查找 LDAP 用户
* @param $email
* @return Model|null
*/
public static function findByEmail($email): ?Model
{
if (!self::initConfig()) {
return null;
}
try {
foreach (self::$emailAttrs as $attr) {
$row = self::static()->whereRaw($attr, '=', $email)->first();
if ($row) {
return $row;
}
}
return null;
} catch (\Exception) {
return null;
}
}
/**
* 获取用户的邮箱(从 LDAP 记录中提取)
* @param Model $row
* @return string|null
*/
public static function getUserEmail(Model $row): ?string
{
foreach (self::$emailAttrs as $attr) {
$val = $row->getFirstAttribute($attr);
if ($val && Base::isEmail($val)) {
return $val;
}
}
return null;
}
/**
* 登录
* @param $username
@ -138,7 +200,18 @@ class LdapUser extends Model
return null;
}
if (empty($user)) {
$user = User::reg($username, $password);
$email = self::getUserEmail($row);
if (empty($email)) {
throw new ApiException('LDAP 用户缺少邮箱属性,请联系管理员配置');
}
$user = User::whereEmail($email)->first();
if (empty($user)) {
// LDAP 用户通过 LDAP 认证,本地密码用随机值以满足密码策略
$localPassword = Base::generatePassword(16) . 'Aa1!';
$user = User::reg($email, $localPassword);
} elseif (!$user->isLdap()) {
info("[LDAP] merged with existing local account: userid={$user->userid}, email={$email}");
}
}
if ($user) {
$userimg = $row->getPhoto();
@ -173,7 +246,7 @@ class LdapUser extends Model
}
//
if (self::isSyncLocal()) {
$row = self::userFirst($user->email, $password);
$row = self::findByEmail($user->email);
if ($row) {
return;
}
@ -184,17 +257,18 @@ class LdapUser extends Model
} else {
$userimg = '';
}
self::static()->create([
$attrs = [
'cn' => $user->email,
'gidNumber' => 0,
'homeDirectory' => '/home/ldap/dootask/' . env("APP_NAME"),
'sn' => $user->email,
'uid' => $user->email,
'uidNumber' => $user->userid,
'userPassword' => $password,
'displayName' => $user->nickname,
'jpegPhoto' => $userimg,
]);
'mail' => $user->email,
];
if ($userimg) {
$attrs['jpegPhoto'] = $userimg;
}
self::static()->create($attrs);
$user->identity = Base::arrayImplode(array_merge(array_diff($user->identity, ['ldap']), ['ldap']));
$user->save();
} catch (LdapRecordException $e) {
@ -205,11 +279,11 @@ class LdapUser extends Model
/**
* 更新
* @param $username
* @param $email
* @param $array
* @return void
*/
public static function userUpdate($username, $array)
public static function userUpdate($email, $array)
{
if (empty($array)) {
return;
@ -218,10 +292,7 @@ class LdapUser extends Model
return;
}
try {
$row = self::static()
->where([
'cn' => $username,
])->first();
$row = self::findByEmail($email);
$row?->update($array);
} catch (\Exception $e) {
info("[LDAP] update fail: " . $e->getMessage());
@ -230,19 +301,16 @@ class LdapUser extends Model
/**
* 删除
* @param $username
* @param $email
* @return void
*/
public static function userDelete($username)
public static function userDelete($email)
{
if (!self::initConfig()) {
return;
}
try {
$row = self::static()
->where([
'cn' => $username,
])->first();
$row = self::findByEmail($email);
$row?->delete();
} catch (\Exception $e) {
info("[LDAP] delete fail: " . $e->getMessage());

View File

@ -20,9 +20,7 @@ use Illuminate\Support\Facades\DB;
* @method static \Illuminate\Database\Eloquent\Model|object|static|null cancelAppend()
* @method static \Illuminate\Database\Eloquent\Model|object|static|null cancelHidden()
* @method static \Illuminate\Database\Eloquent\Builder|static with($relations)
* @method static \Illuminate\Database\Query\Builder|static select($columns = [])
* @method static \Illuminate\Database\Query\Builder|static whereIn($column, $values, $boolean = 'and', $not = false)
* @method static \Illuminate\Database\Query\Builder|static whereNotIn($column, $values, $boolean = 'and')
* @method static \Illuminate\Pagination\LengthAwarePaginator paginate(callable $callback)
* @method int change(array $array)
* @method int remove()
* @mixin \Eloquent
@ -34,6 +32,28 @@ class AbstractModel extends Model
const ID = 'id';
protected $dates = [
'top_at',
'last_at',
'start_at',
'end_at',
'archived_at',
'complete_at',
'loop_at',
'receive_at',
'line_at',
'disable_at',
'clear_at',
'read_at',
'done_at',
'remind_at',
'reminded_at',
'created_at',
'updated_at',
'deleted_at',
@ -131,6 +151,25 @@ class AbstractModel extends Model
return $date->format($this->dateFormat ?: 'Y-m-d H:i:s');
}
/**
* 通过模型创建实例
* @param array $param
* @param bool $force
* @return static
*/
public static function fillInstance(array $param = [], bool $force = true)
{
$instance = new static;
if ($param) {
if ($force) {
$instance->forceFill($param);
} else {
$instance->fill($param);
}
}
return $instance;
}
/**
* 创建/更新数据
* @param array $param
@ -189,24 +228,44 @@ class AbstractModel extends Model
/**
* 数据库更新或插入
* @param $where
* @param array $update 存在时更新的内容
* @param array $insert 不存在时插入的内容,如果没有则插入更新内容
* @param bool $isInsert 是否是插入数据
* @param array $where 查询条件
* @param array|\Closure $update 存在时更新的内容
* @param array|\Closure $insert 不存在时插入的内容,如果没有则插入更新内容
* @param bool $isInsert 是否是插入数据
* @param bool|null $lockForUpdate 是否加锁true:加锁false:不加锁null:在事务中会自动加锁)
* @return AbstractModel|\Illuminate\Database\Eloquent\Builder|Model|object|static|null
*/
public static function updateInsert($where, $update = [], $insert = [], &$isInsert = true)
public static function updateInsert($where, $update = [], $insert = [], &$isInsert = true, $lockForUpdate = null)
{
$row = static::where($where)->first();
$query = static::where($where);
if ($lockForUpdate === null) {
$lockForUpdate = \DB::transactionLevel() > 0;
}
if ($lockForUpdate) {
$query->lockForUpdate();
}
$row = $query->first();
if (empty($row)) {
$row = new static;
$array = array_merge($where, $insert ?: $update);
if ($insert instanceof \Closure) {
$insert = $insert();
}
if (empty($insert)) {
if ($update instanceof \Closure) {
$update = $update();
}
$insert = $update;
}
$array = array_merge($where, $insert);
if (isset($array[$row->primaryKey])) {
unset($array[$row->primaryKey]);
}
$row->updateInstance($array);
$isInsert = true;
} elseif ($update) {
if ($update instanceof \Closure) {
$update = $update();
}
$row->updateInstance($update);
$isInsert = false;
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Models;
/**
* AI 助手会话
*
* @property int $id
* @property int $userid
* @property string $session_key
* @property string $session_id
* @property string $scene_key
* @property string $title
* @property string|null $data
* @property string|null $images
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*/
class AiAssistantSession extends AbstractModel
{
protected $table = 'ai_assistant_sessions';
}

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

@ -11,9 +11,15 @@ namespace App\Models;
* @property int|null $msg_id 消息ID
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @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|ApproveProcMsg newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereMsgId($value)

41
app/Models/Complaint.php Normal file
View File

@ -0,0 +1,41 @@
<?php
namespace App\Models;
/**
* App\Models\Complaint
*
* @property int $id
* @property int|null $dialog_id 对话ID
* @property int|null $userid 举报人id
* @property int|null $type 举报类型
* @property string|null $reason 举报原因
* @property string|null $imgs 举报图片
* @property int|null $status 状态 0待处理、1已处理、2已删除
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @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|Complaint newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Complaint newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Complaint query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|Complaint whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Complaint whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|Complaint whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|Complaint whereImgs($value)
* @method static \Illuminate\Database\Eloquent\Builder|Complaint whereReason($value)
* @method static \Illuminate\Database\Eloquent\Builder|Complaint whereStatus($value)
* @method static \Illuminate\Database\Eloquent\Builder|Complaint whereType($value)
* @method static \Illuminate\Database\Eloquent\Builder|Complaint whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Complaint whereUserid($value)
* @mixin \Eloquent
*/
class Complaint extends AbstractModel
{
}

View File

@ -12,9 +12,15 @@ use Carbon\Carbon;
* @property int|null $did 删除的数据ID
* @property int|null $userid 关系会员ID
* @property \Illuminate\Support\Carbon|null $created_at
* @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|Deleted newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Deleted newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Deleted query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|Deleted whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Deleted whereDid($value)
* @method static \Illuminate\Database\Eloquent\Builder|Deleted whereId($value)

View File

@ -3,8 +3,11 @@
namespace App\Models;
use Request;
use App\Module\Apps;
use App\Module\Base;
use App\Tasks\PushTask;
use App\Tasks\ManticoreSyncTask;
use App\Observers\AbstractObserver;
use App\Exceptions\ApiException;
use Illuminate\Support\Facades\DB;
use Hhxsv5\LaravelS\Swoole\Task\Task;
@ -23,20 +26,30 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property int|null $size 大小(B)
* @property int|null $userid 拥有者ID
* @property int|null $share 是否共享
* @property int|null $guest_access 是否允许游客访问
* @property int|null $pshare 所属分享ID
* @property int|null $created_id 创建者
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @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|File newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|File newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|File onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|File query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|File searchByKeyword(string $keyword)
* @method static \Illuminate\Database\Eloquent\Builder|File sharedToUser(int $userid)
* @method static \Illuminate\Database\Eloquent\Builder|File whereCid($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereCreatedId($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereExt($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereGuestAccess($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|File wherePid($value)
@ -73,9 +86,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 演示文稿模板
];
/**
@ -92,7 +124,7 @@ class File extends AbstractModel
'jpg', 'jpeg', 'webp', 'png', 'gif', 'bmp', 'ico', 'raw',
'tif', 'tiff',
'mp3', 'wav', 'mp4', 'flv',
'avi', 'mov', 'wmv', 'mkv', '3gp', 'rm',
// 'avi', 'mov', 'wmv', 'mkv', '3gp', 'rm', // 这一排是要转换的,无法使用本地播放
];
/**
@ -100,11 +132,52 @@ class File extends AbstractModel
*/
const zipMaxSize = 1024 * 1024 * 1024; // 1G
/**
* 按关键词搜索文件Scope
* 支持文件ID纯数字、文件名
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $keyword 搜索关键词
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeSearchByKeyword($query, string $keyword)
{
if (is_numeric($keyword)) {
return $query->where(function ($q) use ($keyword) {
$q->where("id", intval($keyword))
->orWhere("name", "like", "%{$keyword}%");
});
}
return $query->where("name", "like", "%{$keyword}%");
}
/**
* 筛选用户可访问的共享文件Scope
* 不包括用户自己的文件,仅返回他人共享给该用户的文件
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param int $userid 用户ID
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeSharedToUser($query, int $userid)
{
return $query->whereIn('pshare', function ($subQuery) use ($userid) {
$subQuery->select('files.id')
->from('files')
->join('file_users', 'files.id', '=', 'file_users.file_id')
->where('files.userid', '!=', $userid)
->where(function ($q) use ($userid) {
$q->whereIn('file_users.userid', [0, $userid]);
});
});
}
/**
* 获取文件列表
* @param user $user
* @param int $pid
* @param string $type
* @param bool $isGetparent
* @return array
*/
public function getFileList($user, int $pid, $type = "all", $isGetparent = true)
@ -112,7 +185,7 @@ class File extends AbstractModel
$permission = 1000;
$userids = $user->isTemp() ? [$user->userid] : [0, $user->userid];
$builder = File::wherePid($pid)
->when($type=='dir',function($q){
->when($type == 'dir', function ($q) {
$q->whereType('folder');
});
if ($pid > 0) {
@ -128,7 +201,7 @@ class File extends AbstractModel
//
if ($pid > 0) {
// 遍历获取父级
if($isGetparent){
if ($isGetparent) {
while ($pid > 0) {
$file = File::whereId($pid)->first();
if (empty($file)) {
@ -166,8 +239,8 @@ class File extends AbstractModel
->whereIn('file_users.userid', $userids)
->groupBy('files.id')
->take(100)
->when($type=='dir',function($q){
$q->where('files.type','folder');
->when($type == 'dir', function ($q) {
$q->where('files.type', 'folder');
})
->get();
if ($list->isNotEmpty()) {
@ -190,9 +263,10 @@ class File extends AbstractModel
* @param user $user
* @param int $pid
* @param string $webkitRelativePath
* @param bool $overwrite
* @return array
*/
public function contentUpload($user, int $pid, $webkitRelativePath)
public function contentUpload($user, int $pid, $webkitRelativePath, $overwrite = false)
{
$userid = $user->userid;
if ($pid > 0) {
@ -238,14 +312,13 @@ class File extends AbstractModel
}
}
//
$setting = Base::setting('system');
$path = 'uploads/tmp/' . date("Ym") . '/';
$path = 'uploads/tmp/file/' . date("Ym") . '/';
$data = Base::upload([
"file" => Request::file('files'),
"type" => 'more',
"autoThumb" => false,
"path" => $path,
"size" => ($setting['file_upload_limit'] ?: 0) * 1024
"quality" => true
]);
if (Base::isError($data)) {
throw new ApiException($data['msg']);
@ -256,9 +329,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",
@ -283,17 +356,25 @@ class File extends AbstractModel
if ($data['ext'] == 'markdown') {
$data['ext'] = 'md';
}
$file = File::createInstance([
$file = null;
$params = [
'pid' => $pid,
'name' => Base::rightDelete($data['name'], '.' . $data['ext']),
'type' => $type,
'ext' => $data['ext'],
'userid' => $userid,
'created_id' => $user->userid,
]);
$file->handleDuplicateName();
];
if ($overwrite) {
$file = self::wherePid($params['pid'])->whereExt($params['ext'])->whereName($params['name'])->first();
}
if (!$file) {
$overwrite = false;
$file = File::createInstance($params);
$file->handleDuplicateName();
}
// 开始创建
return AbstractModel::transaction(function () use ($addItem, $webkitRelativePath, $type, $user, $data, $file) {
return AbstractModel::transaction(function () use ($overwrite, $addItem, $webkitRelativePath, $type, $user, $data, $file) {
$file->size = $data['size'] * 1024;
$file->saveBeforePP();
//
@ -321,11 +402,12 @@ class File extends AbstractModel
$tmpRow->pushMsg('add', $tmpRow);
//
$data = File::handleImageUrl($tmpRow->toArray());
$data['full_name'] = $webkitRelativePath ?: $data['name'];
$data['full_name'] = $webkitRelativePath ?: ($data['name'] . '.' . $data['ext']);
$data['overwrite'] = $overwrite ? 1 : 0;
//
$addItem[] = $data;
return ['data'=>$data,'addItem'=>$addItem];
return ['data' => $data, 'addItem' => $addItem];
});
}
@ -336,7 +418,8 @@ class File extends AbstractModel
*/
public function getPermission(array $userids)
{
if (in_array($this->userid, $userids) || in_array($this->created_id, $userids)) {
$validUserIds = array_filter($userids);
if (in_array($this->userid, $validUserIds) || in_array($this->created_id, $validUserIds)) {
// ① 自己的文件夹 或 自己创建的文件夹
return 1000;
}
@ -544,6 +627,26 @@ class File extends AbstractModel
return true;
}
/**
* 批量更新子文件的 userid 并同步到 Manticore
* @param int $userid 新的 userid
* @return int 更新的文件数量
*/
public function updateChildFilesUserid(int $userid): int
{
self::where('pids', 'like', "%,{$this->id},%")->update(['userid' => $userid]);
// 批量 update 绕过 Observer手动触发 Manticore 同步
$childFileIds = self::where('pids', 'like', "%,{$this->id},%")
->where('type', '!=', 'folder')
->pluck('id')
->toArray();
foreach ($childFileIds as $childFileId) {
AbstractObserver::taskDeliver(new ManticoreSyncTask('file_sync', ['id' => $childFileId]));
}
return count($childFileIds);
}
/**
* 获取文件分享链接
* @param $userid
@ -604,6 +707,29 @@ class File extends AbstractModel
Task::deliver($task);
}
/**
* 文件推送消息
* @param $action
* @param array|null $data 发送内容
* @param int $userid 会员ID
*/
public static function pushMsgSimple($action, $data, $userid)
{
if (empty($data) || empty($userid)) {
return;
}
$msg = [
'type' => 'file',
'action' => $action,
'data' => $data,
];
$params = [
'userid' => $userid,
'msg' => $msg
];
Task::deliver(new PushTask($params));
}
/**
* 获取推送会员
* @param $action
@ -647,7 +773,7 @@ class File extends AbstractModel
/**
* code获取文件ID、名称
* @param $code
* @return File
* @return File|null
*/
public static function code2IdName($code) {
$arr = explode(",", base64_decode($code));
@ -688,9 +814,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('文件不存在或已被删除');
}
@ -919,26 +1045,39 @@ class File extends AbstractModel
}
/**
* 文件推送消息
* @param $action
* @param array|null $data 发送内容
* @param array $userid 会员ID
* 根据文件类型判断是否需要安装应用
* @param $type
* @return void
*/
public static function filePushMsg($action, $data = null, $userid = null)
public static function isNeedInstallApp($type): void
{
$userid = User::auth()->userid();
if (empty($userid)) {
return;
// 文件类型与应用的映射配置
$fileTypeAppMapping = [
// Office 应用映射
[
'types' => ['word', 'excel', 'ppt', 'docx', 'xlsx', 'pptx'],
'app_id' => 'office',
'app_name' => 'OnlyOffice'
],
// Drawio 应用映射
[
'types' => ['drawio'],
'app_id' => 'drawio',
'app_name' => 'Drawio'
],
// Minder 应用映射
[
'types' => ['mind'],
'app_id' => 'minder',
'app_name' => 'Minder'
]
];
// 遍历配置检查是否需要安装应用
foreach ($fileTypeAppMapping as $config) {
if (in_array($type, $config['types'])) {
Apps::isInstalledThrow($config['app_id']);
}
}
$msg = [
'type' => 'file',
'action' => $action,
'data' => $data,
];
$params = [
'userid' => $userid,
'msg' => $msg
];
Task::deliver(new PushTask($params));
}
}

View File

@ -2,9 +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
@ -18,10 +19,16 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @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|FileContent newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|FileContent newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|FileContent onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|FileContent query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|FileContent whereContent($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileContent whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileContent whereDeletedAt($value)
@ -70,7 +77,7 @@ class FileContent extends AbstractModel
'name' => $name,
'ext' => $fileExt
]));
return Base::fillUrl("online/preview/{$name}?key={$key}");
return Base::fillUrl("online/preview/{$name}?key={$key}&version=" . Base::getVersion() . "&__=" . Timer::msecTime());
}
/**
@ -97,10 +104,10 @@ class FileContent extends AbstractModel
/**
* 获取格式内容(或下载)
* @param File $file
* @param $file
* @param $content
* @param $download
* @return array|\Symfony\Component\HttpFoundation\StreamedResponse
* @return array|StreamedResponse
*/
public static function formatContent($file, $content, $download = false)
{
@ -112,7 +119,7 @@ class FileContent extends AbstractModel
} else {
$filePath = public_path($content['url']);
}
return Base::streamDownload($filePath, $name);
return Base::DownloadFileResponse($filePath, $name);
}
if (empty($content)) {
$content = match ($file->type) {
@ -122,9 +129,7 @@ class FileContent extends AbstractModel
],
default => json_decode('{}'),
};
if ($download) {
abort(403, "This file is empty.");
}
abort_if($download, 403, "This file is empty.");
} else {
$path = $content['url'];
if ($file->ext) {
@ -140,13 +145,51 @@ class FileContent extends AbstractModel
}
if ($download) {
$filePath = public_path($path);
if (isset($filePath)) {
return Base::streamDownload($filePath, $name);
} else {
abort(403, "This file not support download.");
}
abort_if(!isset($filePath),403, "This file not support download.");
return Base::DownloadFileResponse($filePath, $name);
}
}
return Base::retSuccess('success', [ 'content' => $content ]);
}
/**
* 获取文件访问URL
* @param int $fileId 文件ID
* @return string|null 返回完整的文件URL如果文件无内容则返回null
*/
public static function getFileUrl($fileId)
{
$content = self::whereFid($fileId)->orderByDesc('id')->first();
if ($content) {
$contentData = Base::json2array($content->content ?: []);
if (!empty($contentData['url'])) {
return Base::fillUrl($contentData['url']);
}
}
return null;
}
/**
* 获取文件内容
* @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

@ -15,9 +15,15 @@ use App\Module\Base;
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\File|null $file
* @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|FileLink newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|FileLink newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|FileLink query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereCode($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereFileId($value)

View File

@ -12,9 +12,15 @@ namespace App\Models;
* @property int|null $permission 权限0只读1读写
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @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|FileUser newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|FileUser newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|FileUser query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|FileUser whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileUser whereFileId($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileUser whereId($value)
@ -39,7 +45,7 @@ class FileUser extends AbstractModel
} else {
FileLink::whereFileId($file_id)->delete();
}
FileUser::whereFileId($file_id)->delete();
FileUser::whereFileId($file_id)->remove();
});
}
/**
@ -52,7 +58,7 @@ class FileUser extends AbstractModel
{
return AbstractModel::transaction(function() use ($userid, $file_id) {
FileLink::whereFileId($file_id)->whereUserid($userid)->delete();
return self::whereFileId($file_id)->whereUserid($userid)->delete();
return self::whereFileId($file_id)->whereUserid($userid)->remove();
});
}
}

View File

@ -0,0 +1,132 @@
<?php
namespace App\Models;
/**
* Manticore 同步失败记录
*
* @property int $id
* @property string $data_type 数据类型: msg/file/task/project/user
* @property int $data_id 数据ID
* @property string $action 操作类型: sync/delete
* @property string|null $error_message 错误信息
* @property int $retry_count 重试次数
* @property \Carbon\Carbon|null $last_retry_at 最后重试时间
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*/
class ManticoreSyncFailure extends AbstractModel
{
protected $table = 'manticore_sync_failures';
protected $fillable = [
'data_type',
'data_id',
'action',
'error_message',
'retry_count',
'last_retry_at',
];
protected $dates = [
'last_retry_at',
'created_at',
'updated_at',
];
/**
* 记录同步失败
*
* @param string $dataType 数据类型
* @param int $dataId 数据ID
* @param string $action 操作类型 sync/delete
* @param string $errorMessage 错误信息
*/
public static function recordFailure(string $dataType, int $dataId, string $action, string $errorMessage = ''): void
{
self::updateOrCreate(
[
'data_type' => $dataType,
'data_id' => $dataId,
'action' => $action,
],
[
'error_message' => mb_substr($errorMessage, 0, 500),
'retry_count' => \DB::raw('retry_count + 1'),
'last_retry_at' => now(),
]
);
}
/**
* 删除成功记录
*
* @param string $dataType 数据类型
* @param int $dataId 数据ID
* @param string $action 操作类型
*/
public static function removeSuccess(string $dataType, int $dataId, string $action): void
{
self::where('data_type', $dataType)
->where('data_id', $dataId)
->where('action', $action)
->delete();
}
/**
* 获取待重试的记录
* 根据重试次数决定间隔1次=1分钟2次=5分钟3次=15分钟4次+=30分钟
*
* @param int $limit 数量限制
* @return \Illuminate\Database\Eloquent\Collection
*/
public static function getPendingRetries(int $limit = 100)
{
return self::where(function ($query) {
$query->whereNull('last_retry_at')
->orWhere(function ($q) {
// 根据重试次数决定间隔
$q->where(function ($sub) {
// 重试1次等待1分钟
$sub->where('retry_count', 1)
->where('last_retry_at', '<', now()->subMinutes(1));
})->orWhere(function ($sub) {
// 重试2次等待5分钟
$sub->where('retry_count', 2)
->where('last_retry_at', '<', now()->subMinutes(5));
})->orWhere(function ($sub) {
// 重试3次等待15分钟
$sub->where('retry_count', 3)
->where('last_retry_at', '<', now()->subMinutes(15));
})->orWhere(function ($sub) {
// 重试4次以上等待30分钟
$sub->where('retry_count', '>=', 4)
->where('last_retry_at', '<', now()->subMinutes(30));
});
});
})
->orderBy('last_retry_at')
->limit($limit)
->get();
}
/**
* 获取统计信息
*
* @return array
*/
public static function getStats(): array
{
return [
'total' => self::count(),
'by_type' => self::selectRaw('data_type, COUNT(*) as count')
->groupBy('data_type')
->pluck('count', 'data_type')
->toArray(),
'by_action' => self::selectRaw('action, COUNT(*) as count')
->groupBy('action')
->pluck('count', 'action')
->toArray(),
];
}
}

View File

@ -16,11 +16,17 @@ use Illuminate\Support\Carbon;
* @property int|null $userid 创建人
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property string|null $end_at
* @property Carbon|null $end_at
* @property Carbon|null $deleted_at
* @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|Meeting newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Meeting newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Meeting query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereChannel($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereDeletedAt($value)
@ -44,12 +50,12 @@ class Meeting extends AbstractModel
public function getShareLink()
{
$code = base64_encode("{$this->meetingid}" . Base::generatePassword());
Cache::put(self::CACHE_KEY.'_'.$code, [
Cache::put(self::CACHE_KEY . '_' . $code, [
'id' => $this->id,
'meetingid' => $this->meetingid,
'channel' => $this->channel,
], Carbon::now()->addHours(self::CACHE_EXPIRED_TIME));
return Base::fillUrl("meeting/{$this->meetingid}/".$code);
return Base::fillUrl("meeting/{$this->meetingid}/" . $code);
}
/**
@ -58,19 +64,19 @@ class Meeting extends AbstractModel
*/
public static function getShareInfo($code)
{
if(Cache::has(self::CACHE_KEY.'_'.$code)){
return Cache::get(self::CACHE_KEY.'_'.$code);
if (Cache::has(self::CACHE_KEY . '_' . $code)) {
return Cache::get(self::CACHE_KEY . '_' . $code);
}
return null;
}
/**
* 保存访客信息
* @return mixed
* @return void
*/
public static function setTouristInfo($data)
{
Cache::put(Meeting::CACHE_KEY.'_'.$data['uid'], [
Cache::put(Meeting::CACHE_KEY . '_' . $data['uid'], [
'uid' => $data['uid'],
'userimg' => $data['userimg'],
'nickname' => $data['nickname'],
@ -83,8 +89,8 @@ class Meeting extends AbstractModel
*/
public static function getTouristInfo($touristId)
{
if(Cache::has(Meeting::CACHE_KEY.'_'.$touristId)){
return Cache::get(Meeting::CACHE_KEY.'_'.$touristId);
if (Cache::has(Meeting::CACHE_KEY . '_' . $touristId)) {
return Cache::get(Meeting::CACHE_KEY . '_' . $touristId);
}
return null;
}

34
app/Models/MeetingMsg.php Normal file
View File

@ -0,0 +1,34 @@
<?php
namespace App\Models;
/**
* App\Models\MeetingMsg
*
* @property int $id
* @property string|null $meetingid 会议ID
* @property int|null $dialog_id 对话ID
* @property int|null $msg_id 消息ID
* @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|MeetingMsg newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|MeetingMsg newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|MeetingMsg query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|MeetingMsg whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|MeetingMsg whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|MeetingMsg whereMeetingid($value)
* @method static \Illuminate\Database\Eloquent\Builder|MeetingMsg whereMsgId($value)
* @mixin \Eloquent
*/
class MeetingMsg extends AbstractModel
{
function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->timestamps = false;
}
}

View File

@ -20,11 +20,15 @@ use Request;
* @property string|null $desc 描述、备注
* @property int|null $userid 创建人
* @property int|null $personal 是否个人项目
* @property string|null $archive_method 自动归档方式
* @property int|null $archive_days 自动归档天数
* @property string|null $ai_auto_analyze AI自动分析
* @property string|null $task_template_share 共享模板开关
* @property string|null $department_owner_view 部门负责人视角可见开关
* @property string|null $user_simple 成员总数|1,2,3
* @property int|null $dialog_id 聊天会话ID
* @property string|null $archived_at 归档时间
* @property \Illuminate\Support\Carbon|null $archived_at 归档时间
* @property int|null $archived_userid 归档会员
* @property int|null $is_fixed 是否固定
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
@ -37,10 +41,19 @@ use Request;
* @property-read int|null $project_user_count
* @method static \Illuminate\Database\Eloquent\Builder|Project allData($userid = null)
* @method static \Illuminate\Database\Eloquent\Builder|Project authData($userid = null, $owner = null)
* @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|Project newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Project newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Project onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|Project query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|Project searchByKeyword(string $keyword)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereArchiveDays($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereArchiveMethod($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereArchivedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereArchivedUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereCreatedAt($value)
@ -48,7 +61,6 @@ use Request;
* @method static \Illuminate\Database\Eloquent\Builder|Project whereDesc($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereIsFixed($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project wherePersonal($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereUpdatedAt($value)
@ -68,6 +80,7 @@ class Project extends AbstractModel
protected $appends = [
'owner_userid',
'deputy_userids',
];
/**
@ -83,6 +96,58 @@ class Project extends AbstractModel
return $this->appendattrs['owner_userid'];
}
/**
* 项目管理员 userid 列表
* @return array
*/
public function getDeputyUseridsAttribute(): array
{
if (empty($this->id)) {
return [];
}
return ProjectUser::whereProjectId($this->id)
->whereOwner(ProjectUser::OWNER_DEPUTY)
->pluck('userid')
->map(fn($v) => (int)$v)
->toArray();
}
/**
* 是否项目负责人(与 project_users.owner=1 一致)
*/
public function isPrimaryOwner($userid): bool
{
if (empty($this->id) || $userid <= 0) {
return false;
}
return ProjectUser::whereProjectId($this->id)
->whereUserid($userid)
->whereOwner(ProjectUser::OWNER_PRIMARY)
->exists();
}
/**
* 是否项目管理员(与 project_users.owner=2 一致)
*/
public function isDeputyOwner($userid): bool
{
if (empty($this->id) || $userid <= 0) {
return false;
}
return ProjectUser::whereProjectId($this->id)
->whereUserid($userid)
->whereOwner(ProjectUser::OWNER_DEPUTY)
->exists();
}
/**
* 是否负责人(含项目管理员)
*/
public function isOwner($userid): bool
{
return $this->isPrimaryOwner($userid) || $this->isDeputyOwner($userid);
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
@ -121,6 +186,7 @@ class Project extends AbstractModel
'projects.*',
'project_users.owner',
'project_users.top_at',
'project_users.sort',
])
->leftJoin('project_users', function ($leftJoin) use ($userid) {
$leftJoin
@ -145,6 +211,7 @@ class Project extends AbstractModel
'projects.*',
'project_users.owner',
'project_users.top_at',
'project_users.sort',
])
->join('project_users', 'projects.id', '=', 'project_users.project_id')
->where('project_users.userid', $userid);
@ -154,6 +221,18 @@ class Project extends AbstractModel
return $query;
}
/**
* 按关键词搜索项目Scope
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $keyword 搜索关键词
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeSearchByKeyword($query, string $keyword)
{
return $query->where("projects.name", "like", "%{$keyword}%");
}
/**
* 获取任务统计数据
* @param $userid
@ -204,16 +283,40 @@ class Project extends AbstractModel
return;
}
AbstractModel::transaction(function() {
$userids = $this->relationUserids();
// 拉所有项目成员 + 各自 owner 值
$userOwnerMap = ProjectUser::whereProjectId($this->id)
->pluck('owner', 'userid');
$userids = $userOwnerMap->keys()->map(fn($v) => (int)$v)->toArray();
foreach ($userids as $userid) {
$owner = (int)$userOwnerMap[$userid];
// 巧合:编码完全一致 owner 0/1/2 → role 0/1/2
$role = $owner;
WebSocketDialogUser::updateInsert([
'dialog_id' => $this->dialog_id,
'userid' => $userid,
], [
'important' => 1
]);
'important' => 1,
'role' => $role,
], function () use ($userid, $role) {
return [
'important' => 1,
'role' => $role,
'bot' => User::isBot($userid) ? 1 : 0,
];
});
}
WebSocketDialogUser::whereDialogId($this->dialog_id)
->whereNotIn('userid', $userids)
->whereImportant(1)
->remove();
// 同步 dialog.owner_id 到主负责人owner=1前端「群主」标签依赖此字段
// 必须随项目主负责人变更(含用户离职转移)一起刷新,否则会显示已离职用户
$primaryUserid = $userOwnerMap->search(ProjectUser::OWNER_PRIMARY);
if ($primaryUserid !== false && (int)$primaryUserid > 0) {
WebSocketDialog::whereId($this->dialog_id)
->where('owner_id', '!=', (int)$primaryUserid)
->update(['owner_id' => (int)$primaryUserid]);
}
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->whereImportant(1)->remove();
});
}
@ -321,44 +424,65 @@ class Project extends AbstractModel
/**
* 推送消息
* @param string $action
* @param array|self $data 发送内容,默认为[id=>项目ID]
* @param array|self $data 推送内容
* @param array $userid 指定会员,默认为项目所有成员
*/
public function pushMsg($action, $data = null, $userid = null)
{
if ($data === null) {
$data = ['id' => $this->id];
} elseif ($data instanceof self) {
// 处理数据
if ($data instanceof self) {
$data = $data->toArray();
}
//
$array = [$userid, []];
$data = is_array($data) ? $data : [];
$data['id'] = $this->id;
$data['name'] = $this->name;
$data['desc'] = $this->desc;
// 处理接收用户
$recipients = [$userid, []];
if ($userid === null) {
$array[0] = $this->relationUserids();
$recipients[0] = $this->relationUserids();
} elseif (!is_array($userid)) {
$array[0] = [$userid];
$recipients[0] = [$userid];
}
//
// 移除不需要的字段
unset($data['top_at']);
// 处理所有者权限
if (isset($data['owner'])) {
$owners = ProjectUser::whereProjectId($data['id'])->whereOwner(1)->pluck('userid')->toArray();
$array = [array_intersect($array[0], $owners), array_diff($array[0], $owners)];
$owners = ProjectUser::whereProjectId($data['id'])
->whereIn('owner', [ProjectUser::OWNER_PRIMARY, ProjectUser::OWNER_DEPUTY])
->pluck('userid')
->toArray();
$recipients = [
array_intersect($recipients[0], $owners),
array_diff($recipients[0], $owners)
];
}
//
foreach ($array as $index => $item) {
// 发送推送
foreach ($recipients as $index => $userids) {
if (empty($userids)) {
continue;
}
if ($index > 0) {
$data['owner'] = 0;
}
$params = [
'ignoreFd' => Request::header('fd'),
'userid' => array_values($item),
'userid' => array_values($userids),
'msg' => [
'type' => 'project',
'action' => $action,
'data' => $data,
]
];
$task = new PushTask($params, false);
Task::deliver($task);
Task::deliver(new PushTask($params, false));
}
}
@ -386,29 +510,38 @@ class Project extends AbstractModel
$hasStart = false;
$hasEnd = false;
$upTaskList = [];
$projectUserids = $this->relationUserids();
foreach ($flows as $item) {
$id = intval($item['id']);
$name = trim(str_replace('|', '·', $item['name']));
$turns = Base::arrayRetainInt($item['turns'] ?: [], true);
$userids = Base::arrayRetainInt($item['userids'] ?: [], true);
$usertype = trim($item['usertype']);
$userlimit = intval($item['userlimit']);
$columnid = intval($item['columnid']);
if ($usertype == 'replace' && empty($userids)) {
throw new ApiException("状态[{$item['name']}]设置错误,设置流转模式时必须填写状态负责人");
throw new ApiException("状态[{$name}]设置错误,设置流转模式时必须填写状态负责人");
}
if ($usertype == 'merge' && empty($userids)) {
throw new ApiException("状态[{$item['name']}]设置错误,设置剔除模式时必须填写状态负责人");
throw new ApiException("状态[{$name}]设置错误,设置剔除模式时必须填写状态负责人");
}
if ($userlimit && empty($userids)) {
throw new ApiException("状态[{$item['name']}]设置错误,设置限制负责人时必须填写状态负责人");
throw new ApiException("状态[{$name}]设置错误,设置限制负责人时必须填写状态负责人");
}
foreach ($userids as $userid) {
if (!in_array($userid, $projectUserids)) {
$nickname = User::userid2nickname($userid);
throw new ApiException("状态[{$name}]设置错误,状态负责人[{$nickname}]不在项目成员内");
}
}
$flow = ProjectFlowItem::updateInsert([
'id' => $id,
'project_id' => $this->id,
'flow_id' => $projectFlow->id,
], [
'name' => trim($item['name']),
'name' => $name,
'status' => trim($item['status']),
'color' => trim($item['color']),
'sort' => intval($item['sort']),
'turns' => $turns,
'userids' => $userids,
@ -428,7 +561,7 @@ class Project extends AbstractModel
$hasEnd = true;
}
if (!$isInsert) {
$upTaskList[$flow->id] = $flow->status . "|" . $flow->name;
$upTaskList[$flow->id] = $flow->status . "|" . $flow->name . "|" . $flow->color;
}
}
}
@ -472,6 +605,38 @@ class Project extends AbstractModel
});
}
/**
* 判断用户是否有权限创建项目(依据系统设置「项目创建权限」)
* @param int $userid
* @return bool
*/
public static function userCanCreate($userid)
{
// 范围已在 Setting::getSettingAttribute() 归一化(默认 ['all']
$modes = Base::settingFind('system', 'project_add_permission', ['all']);
// 「所有人」:放行(与具体用户无关,避免未携带身份时被误判为无权)
if (in_array('all', $modes)) {
return true;
}
$user = User::find(intval($userid));
if (empty($user)) {
return false;
}
// 系统管理员始终可创建项目(不受开关限制)
if ($user->isAdmin()) {
return true;
}
// 部门负责人/部门管理员
if (in_array('departmentOwner', $modes) && UserDepartment::getManagedDepartments($user->userid)->isNotEmpty()) {
return true;
}
// 指定人员
if (in_array('appoint', $modes)) {
return in_array($user->userid, Base::settingFind('system', 'project_add_userids', []));
}
return false;
}
/**
* 创建项目
* @param $params
@ -488,6 +653,10 @@ class Project extends AbstractModel
$desc = trim(Arr::get($params, 'desc', ''));
$flow = trim(Arr::get($params, 'flow', 'close'));
$isPersonal = intval(Arr::get($params, 'personal'));
// 个人项目为系统自动创建,不受创建权限限制
if (!$isPersonal && !self::userCanCreate($userid)) {
return Base::retError('当前仅指定人员可以创建项目');
}
if (mb_strlen($name) < 2) {
return Base::retError('项目名称不可以少于2个字');
} elseif (mb_strlen($name) > 32) {
@ -541,7 +710,7 @@ class Project extends AbstractModel
$column['project_id'] = $project->id;
ProjectColumn::createInstance($column)->save();
}
$dialog = WebSocketDialog::createGroup($project->name, $project->userid, 'project');
$dialog = WebSocketDialog::createGroup($project->name, $project->userid, 'project', $project->userid);
if (empty($dialog)) {
throw new ApiException('创建项目聊天室失败');
}
@ -549,7 +718,7 @@ class Project extends AbstractModel
$project->save();
//
if ($flow == 'open') {
$project->addFlow(Base::json2array('[{"id":-10,"name":"待处理","status":"start","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-11,"name":"进行中","status":"progress","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-12,"name":"待测试","status":"test","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-13,"name":"已完成","status":"end","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-14,"name":"已取消","status":"end","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0}]'));
$project->addFlow(Base::json2array('[{"id":-10,"name":"待处理","status":"start","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-11,"name":"进行中","status":"progress","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-12,"name":"待测试","status":"test","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-13,"name":"已完成","status":"end","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-14,"name":"已取消","status":"end","color":"#999999","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0}]'));
}
});
//
@ -563,7 +732,9 @@ class Project extends AbstractModel
* 获取项目信息(用于判断会员是否存在项目内)
* @param int $project_id
* @param null|bool $archived true:仅限未归档, false:仅限已归档, null:不限制
* @param null|bool $mustOwner true:仅限项目负责人, false:仅限非项目负责人, null:不限制
* @param null|bool|string $mustOwner true:负责人或项目管理员都可(共享操作);
* 'primary':仅负责人(转让/删除/任命项目管理员等独占操作);
* false:仅限非负责人null:不限制
* @return self
*/
public static function userProject($project_id, $archived = true, $mustOwner = null)
@ -581,9 +752,39 @@ class Project extends AbstractModel
if ($mustOwner === true && !$project->owner) {
throw new ApiException('仅限项目负责人操作', [ 'project_id' => $project_id ]);
}
if ($mustOwner === 'primary' && (int)$project->owner !== 1) {
throw new ApiException('仅限项目负责人操作', [ 'project_id' => $project_id ]);
}
if ($mustOwner === false && $project->owner) {
throw new ApiException('禁止项目负责人操作', [ 'project_id' => $project_id ]);
}
return $project;
}
/**
* 获取项目(含部门负责人只读视角兜底)
* @param int $project_id
* @param null|bool $archived true:仅限未归档, false:仅限已归档, null:不限制
* @param null|bool|string $mustOwner 仅限 null 时尝试部门只读视角
* @return self
*/
public static function findForDepartmentView($project_id, $archived = true, $mustOwner = null)
{
$user = User::auth();
$departmentView = UserDepartment::ownerViewContext($user, true);
if (UserDepartment::isDepartmentReadonlyProject($departmentView, intval($project_id)) && $mustOwner === null) {
$project = self::allData()->where('projects.id', intval($project_id))->first();
if (empty($project)) {
throw new ApiException('项目不存在或已被删除', [ 'project_id' => $project_id ], -4001);
}
if ($archived === true && $project->archived_at != null) {
throw new ApiException('项目已归档', [ 'project_id' => $project_id ], -4001);
}
if ($archived === false && $project->archived_at == null) {
throw new ApiException('项目未归档', [ 'project_id' => $project_id ]);
}
return $project;
}
return self::userProject($project_id, $archived, $mustOwner);
}
}

View File

@ -22,10 +22,16 @@ use Request;
* @property-read \App\Models\Project|null $project
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectTask> $projectTask
* @property-read int|null $project_task_count
* @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|ProjectColumn newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn whereColor($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn whereDeletedAt($value)

View File

@ -2,8 +2,6 @@
namespace App\Models;
use App\Module\Base;
/**
* App\Models\ProjectFlow
*
@ -14,9 +12,15 @@ use App\Module\Base;
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectFlowItem> $projectFlowItem
* @property-read int|null $project_flow_item_count
* @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|ProjectFlow newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow whereName($value)

View File

@ -12,6 +12,7 @@ use App\Module\Base;
* @property int|null $flow_id 流程ID
* @property string|null $name 名称
* @property string|null $status 状态
* @property string|null $color 自定义颜色
* @property array $turns 可流转
* @property array $userids 状态负责人ID
* @property string|null $usertype 流转模式
@ -21,9 +22,16 @@ use App\Module\Base;
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\ProjectFlow|null $projectFlow
* @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|ProjectFlowItem newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereColor($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereColumnid($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereFlowId($value)

View File

@ -13,9 +13,15 @@ namespace App\Models;
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read bool $already
* @property-read \App\Models\Project|null $project
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite whereCode($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite whereId($value)

View File

@ -11,6 +11,7 @@ use App\Module\Base;
* @property int|null $project_id 项目ID
* @property int|null $column_id 列表ID
* @property int|null $task_id 任务ID
* @property int|null $task_only 仅任务日志0否1是
* @property int|null $userid 会员ID
* @property string|null $detail 详细信息
* @property array $record 记录数据
@ -18,9 +19,15 @@ use App\Module\Base;
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\ProjectTask|null $projectTask
* @property-read \App\Models\User|null $user
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereColumnId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereDetail($value)
@ -28,12 +35,16 @@ use App\Module\Base;
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereProjectId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereRecord($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereTaskId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereTaskOnly($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereUserid($value)
* @mixin \Eloquent
*/
class ProjectLog extends AbstractModel
{
protected $hidden = [
'task_only',
];
/**
* @param $value

View File

@ -4,19 +4,25 @@ namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use App\Module\Doo;
/**
* App\Models\ProjectPermission
*
* @property int $id
* @property int|null $project_id 项目ID
* @property string|null $permissions 权限
* @property array $permissions 权限
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Project|null $project
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission wherePermissions($value)
@ -33,6 +39,7 @@ class ProjectPermission extends AbstractModel
const TASK_LIST_SORT = 'task_list_sort'; // 列表排序
const TASK_ADD = 'task_add'; // 任务添加
const TASK_UPDATE = 'task_update'; // 任务更新
const TASK_TIME = 'task_time'; // 任务时间
const TASK_STATUS = 'task_status'; // 任务状态
const TASK_REMOVE = 'task_remove'; // 任务删除
const TASK_ARCHIVED = 'task_archived'; // 任务归档
@ -64,7 +71,7 @@ class ProjectPermission extends AbstractModel
/**
* 权限
* @param $value
* @return string
* @return array
*/
public function getPermissionsAttribute($value)
{
@ -105,7 +112,8 @@ class ProjectPermission extends AbstractModel
self::TASK_LIST_REMOVE => [self::PERMISSIONS['project_leader']],
self::TASK_LIST_SORT => $projectTaskList,
self::TASK_ADD => $projectTaskList,
self::TASK_UPDATE => [self::PERMISSIONS['project_leader'], self::PERMISSIONS['task_leader'], self::PERMISSIONS['task_assist']],
self::TASK_UPDATE => $taskUpdate = [self::PERMISSIONS['project_leader'], self::PERMISSIONS['task_leader'], self::PERMISSIONS['task_assist']],
self::TASK_TIME => $taskUpdate,
self::TASK_STATUS => $taskStatus = [self::PERMISSIONS['project_leader'], self::PERMISSIONS['task_leader']],
self::TASK_REMOVE => $taskStatus,
self::TASK_ARCHIVED => $taskStatus,
@ -120,8 +128,8 @@ class ProjectPermission extends AbstractModel
/**
* 更新项目权限
*
* @param int $projectId
* @param array $permissions
* @param int $projectId
* @param $newPermissions
* @return ProjectPermission
*/
public static function updatePermissions($projectId, $newPermissions)
@ -138,9 +146,9 @@ class ProjectPermission extends AbstractModel
/**
* 检查用户是否有执行特定动作的权限
* @param string $action 动作名称
* @param Project $project 项目实例
* @param ProjectTask $task 任务实例
* @param string $action 动作名称
* @param ProjectTask|null $task 任务实例
* @return bool
*/
public static function userTaskPermission(Project $project, $action, ProjectTask $task = null)
@ -148,13 +156,14 @@ class ProjectPermission extends AbstractModel
$userid = User::userid();
$permissions = self::getPermission($project->id, $action);
switch ($action) {
// 任务添加,任务更新, 任务状态, 任务删除, 任务完成, 任务归档, 任务移动
// 任务添加,任务更新, 任务状态, 任务删除, 任务完成, 任务归档, 任务移动
case self::TASK_LIST_ADD:
case self::TASK_LIST_UPDATE:
case self::TASK_LIST_REMOVE:
case self::TASK_LIST_SORT:
case self::TASK_ADD:
case self::TASK_UPDATE:
case self::TASK_TIME:
case self::TASK_STATUS:
case self::TASK_REMOVE:
case self::TASK_ARCHIVED:
@ -190,7 +199,7 @@ class ProjectPermission extends AbstractModel
$desc = [];
rsort($permissions);
foreach ($permissions as $permission) {
$desc[] = self::PERMISSIONS_DESC[$permission];
$desc[] = Doo::translate(self::PERMISSIONS_DESC[$permission]);
}
$desc = array_reverse($desc);
throw new ApiException(sprintf("仅限%s操作", implode('、', $desc)));

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

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,154 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* App\Models\ProjectTaskAiEvent
*
* @property int $id
* @property int $task_id 任务ID
* @property string $event_type 事件类型
* @property string $status 状态
* @property int $retry_count 重试次数
* @property array|null $result 执行结果
* @property string|null $error 错误信息
* @property int $msg_id 消息ID
* @property \Illuminate\Support\Carbon|null $executed_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
*/
class ProjectTaskAiEvent extends AbstractModel
{
const EVENT_DESCRIPTION = 'description';
const EVENT_SUBTASKS = 'subtasks';
const EVENT_ASSIGNEE = 'assignee';
const EVENT_SIMILAR = 'similar';
const STATUS_PENDING = 'pending';
const STATUS_PROCESSING = 'processing';
const STATUS_COMPLETED = 'completed';
const STATUS_FAILED = 'failed';
const STATUS_SKIPPED = 'skipped';
const STATUS_APPLIED = 'applied';
const STATUS_DISMISSED = 'dismissed';
const MAX_RETRY = 3;
protected $table = 'project_task_ai_events';
protected $fillable = [
'task_id',
'event_type',
'status',
'retry_count',
'result',
'error',
'msg_id',
'executed_at',
];
protected $casts = [
'result' => 'array',
'executed_at' => 'datetime',
];
/**
* 关联任务
*/
public function task(): BelongsTo
{
return $this->belongsTo(ProjectTask::class, 'task_id', 'id');
}
/**
* 获取所有事件类型
*/
public static function getEventTypes(): array
{
return [
self::EVENT_DESCRIPTION,
self::EVENT_SUBTASKS,
self::EVENT_ASSIGNEE,
self::EVENT_SIMILAR,
];
}
/**
* 标记为处理中
*/
public function markProcessing(): bool
{
return $this->update([
'status' => self::STATUS_PROCESSING,
]);
}
/**
* 标记为完成
*/
public function markCompleted(array $result, int $msgId = 0): bool
{
return $this->update([
'status' => self::STATUS_COMPLETED,
'result' => $result,
'msg_id' => $msgId,
'executed_at' => now(),
]);
}
/**
* 标记为失败
*/
public function markFailed(string $error): bool
{
return $this->update([
'status' => self::STATUS_FAILED,
'retry_count' => $this->retry_count + 1,
'error' => $error,
'executed_at' => now(),
]);
}
/**
* 标记为跳过
*/
public function markSkipped(string $reason = ''): bool
{
return $this->update([
'status' => self::STATUS_SKIPPED,
'error' => $reason,
'executed_at' => now(),
]);
}
/**
* 是否可以重试
*/
public function canRetry(): bool
{
return $this->status === self::STATUS_FAILED
&& $this->retry_count < self::MAX_RETRY;
}
/**
* 标记为已采纳
*/
public function markApplied(): bool
{
return $this->update([
'status' => self::STATUS_APPLIED,
]);
}
/**
* 标记为已忽略
*/
public function markDismissed(): bool
{
return $this->update([
'status' => self::STATUS_DISMISSED,
]);
}
}

View File

@ -11,24 +11,33 @@ use App\Exceptions\ApiException;
* @property int $id
* @property int|null $project_id 项目ID
* @property int|null $task_id 任务ID
* @property int|null $userid 用户ID
* @property string|null $desc 内容描述
* @property string|null $content 内容
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @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|ProjectTaskContent newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent whereContent($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent whereDesc($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent whereProjectId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent whereTaskId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent whereUserid($value)
* @mixin \Eloquent
*/
class ProjectTaskContent extends AbstractModel
{
protected $hidden = [
'created_at',
'updated_at',
];
@ -44,8 +53,8 @@ class ProjectTaskContent extends AbstractModel
$array = $this->toArray();
$array['content'] = file_get_contents($filePath) ?: '';
if ($array['content']) {
$replace = Base::fillUrl('uploads/task');
$array['content'] = str_replace('{{RemoteURL}}uploads/task', $replace, $array['content']);
$replace = Base::fillUrl('uploads');
$array['content'] = str_replace('{{RemoteURL}}uploads', $replace, $array['content']);
}
return $array;
}
@ -65,7 +74,7 @@ class ProjectTaskContent extends AbstractModel
$oldContent = $content;
$path = 'uploads/task/content/' . date("Ym") . '/' . $task_id . '/';
//
preg_match_all("/<img\s+src=\"data:image\/(png|jpg|jpeg|webp);base64,(.*?)\"/s", $content, $matchs);
preg_match_all('/<img[^>]*?src=\\\\?["\']data:image\/(png|jpg|jpeg|webp);base64,(.*?)\\\\?["\']/s', $content, $matchs);
foreach ($matchs[2] as $key => $text) {
$tmpPath = $path . 'attached/';
Base::makeDir(public_path($tmpPath));
@ -75,8 +84,14 @@ class ProjectTaskContent extends AbstractModel
$content = str_replace($matchs[0][$key], '<img src="{{RemoteURL}}' . $tmpPath . '" original-width="' . $paramet[0] . '" original-height="' . $paramet[1] . '"', $content);
}
}
$pattern = '/<img(.*?)src=("|\')https*:\/\/(.*?)\/(uploads\/task\/content\/(.*?))\2/is';
$content = preg_replace($pattern, '<img$1src=$2{{RemoteURL}}$4$2', $content);
preg_match_all('/(<img[^>]*?src=\\\\?["\'])(https?:\/\/[^\/]+\/)(uploads\/[^\s"\'>]+)(\\\\?["\'][^>]*?>)/i', $content, $matches);
foreach ($matches[0] as $key => $fullMatch) {
$filePath = public_path($matches[3][$key]);
if (file_exists($filePath)) {
$replacement = $matches[1][$key] . '{{RemoteURL}}' . $matches[3][$key] . $matches[4][$key];
$content = str_replace($fullMatch, $replacement, $content);
}
}
//
$filePath = $path . md5($content);
$publicPath = public_path($filePath);

View File

@ -22,9 +22,15 @@ use Cache;
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read int $height
* @property-read int $width
* @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|ProjectTaskFile newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFile newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFile query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFile whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFile whereDownload($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFile whereExt($value)

View File

@ -14,9 +14,15 @@ namespace App\Models;
* @property string|null $after_flow_item_name (变化后)工作流状态名称
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @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|ProjectTaskFlowChange newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereAfterFlowItemId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereAfterFlowItemName($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereBeforeFlowItemId($value)

View File

@ -17,10 +17,16 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @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|ProjectTaskPushLog newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereId($value)

View File

@ -0,0 +1,223 @@
<?php
namespace App\Models;
use App\Module\Base;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* App\Models\ProjectTaskRelation
*
* @property int $id
* @property int $task_id 任务ID
* @property int $related_task_id 关联任务ID
* @property string $direction 关系方向: mention/mentioned_by
* @property int|null $dialog_id 来源会话ID
* @property int|null $msg_id 来源消息ID
* @property int|null $userid 提及人
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\ProjectTask|null $relatedTask
* @property-read \App\Models\ProjectTask|null $task
* @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|ProjectTaskRelation newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation whereDirection($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation whereMsgId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation whereRelatedTaskId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation whereTaskId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation whereUserid($value)
* @mixin \Eloquent
*/
class ProjectTaskRelation extends AbstractModel
{
public const DIRECTION_MENTION = 'mention';
public const DIRECTION_MENTIONED_BY = 'mentioned_by';
protected $fillable = [
'task_id',
'related_task_id',
'direction',
'dialog_id',
'msg_id',
'userid',
];
public function task(): BelongsTo
{
return $this->belongsTo(ProjectTask::class, 'task_id');
}
public function relatedTask(): BelongsTo
{
return $this->belongsTo(ProjectTask::class, 'related_task_id');
}
/**
* 创建双向任务关联
*
* @param int $sourceTaskId 源任务ID
* @param int $targetTaskId 目标任务ID
* @param int|null $dialogId 来源对话ID
* @param int|null $msgId 来源消息ID
* @param int|null $userid 操作人
* @param bool $push 是否推送更新
* @return bool 是否创建成功
*/
public static function createRelation(
int $sourceTaskId,
int $targetTaskId,
?int $dialogId = null,
?int $msgId = null,
?int $userid = null,
bool $push = true
): bool {
if ($sourceTaskId === $targetTaskId) {
return false;
}
$sourceTask = ProjectTask::with('project')->find($sourceTaskId);
$targetTask = ProjectTask::with('project')->find($targetTaskId);
if (!$sourceTask || !$targetTask) {
return false;
}
if ($sourceTask->deleted_at || $targetTask->deleted_at) {
return false;
}
// 创建正向关联:源任务提及目标任务
$mentionRelation = static::updateOrCreate(
[
'task_id' => $sourceTaskId,
'related_task_id' => $targetTaskId,
'direction' => self::DIRECTION_MENTION,
],
[
'dialog_id' => $dialogId,
'msg_id' => $msgId,
'userid' => $userid,
]
);
// 创建反向关联:目标任务被源任务提及
$reverseRelation = static::updateOrCreate(
[
'task_id' => $targetTaskId,
'related_task_id' => $sourceTaskId,
'direction' => self::DIRECTION_MENTIONED_BY,
],
[
'dialog_id' => $dialogId,
'msg_id' => $msgId,
'userid' => $userid,
]
);
// 推送关联更新
if ($push) {
$needPush = $mentionRelation->wasRecentlyCreated || $mentionRelation->wasChanged()
|| $reverseRelation->wasRecentlyCreated || $reverseRelation->wasChanged();
if ($needPush) {
if ($sourceTask->project) {
$sourceTask->pushMsg('relation', null, null, false);
}
if ($targetTask->project) {
$targetTask->pushMsg('relation', null, null, false);
}
}
}
return true;
}
/**
* 删除双向任务关联
*
* @param int $taskId 任务ID
* @param int $relatedTaskId 关联任务ID
* @return bool 是否删除成功
*/
public static function deleteRelation(int $taskId, int $relatedTaskId): bool
{
// 删除正向关联
$deleted1 = static::whereTaskId($taskId)
->whereRelatedTaskId($relatedTaskId)
->delete();
// 删除反向关联
$deleted2 = static::whereTaskId($relatedTaskId)
->whereRelatedTaskId($taskId)
->delete();
if ($deleted1 || $deleted2) {
// 推送关联更新
$sourceTask = ProjectTask::with('project')->find($taskId);
$targetTask = ProjectTask::with('project')->find($relatedTaskId);
if ($sourceTask?->project) {
$sourceTask->pushMsg('relation', null, null, false);
}
if ($targetTask?->project) {
$targetTask->pushMsg('relation', null, null, false);
}
return true;
}
return false;
}
public static function recordMentionsFromMessage(WebSocketDialogMsg $msg): void
{
if ($msg->type !== 'text') {
return;
}
$payload = $msg->msg;
if (!is_array($payload)) {
$payload = Base::json2array($msg->getRawOriginal('msg'));
}
$text = $payload['text'] ?? '';
if (!$text || !preg_match_all('/<span class="mention task" data-id="(\d+)">#?(.*?)<\/span>/i', $text, $matches)) {
return;
}
$targetIds = array_values(array_unique(array_filter(array_map('intval', $matches[1] ?? []))));
if (empty($targetIds)) {
return;
}
$sourceTaskIds = ProjectTask::whereDialogId($msg->dialog_id)
->whereNull('deleted_at')
->pluck('id')
->toArray();
if (empty($sourceTaskIds)) {
return;
}
foreach ($sourceTaskIds as $sourceTaskId) {
foreach ($targetIds as $targetId) {
self::createRelation(
$sourceTaskId,
$targetId,
$msg->dialog_id,
$msg->id,
$msg->userid
);
}
}
}
}

View File

@ -12,9 +12,15 @@ namespace App\Models;
* @property string|null $color 颜色
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @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|ProjectTaskTag newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTag newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTag query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTag whereColor($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTag whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTag whereId($value)

View File

@ -0,0 +1,103 @@
<?php
namespace App\Models;
/**
* App\Models\ProjectTaskTemplate
*
* @property int $id
* @property int $project_id 项目ID
* @property string $name 模板名称
* @property string|null $title 任务标题
* @property string|null $content 任务内容
* @property int $sort 排序
* @property int $is_default 是否默认模板
* @property int $userid 创建人
* @property int $use_count 累计使用次数
* @property \Illuminate\Support\Carbon|null $last_used_at 最近一次使用时间
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Project $project
* @property-read \App\Models\User $user
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereContent($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereIsDefault($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereProjectId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereSort($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereTitle($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereUserid($value)
* @mixin \Eloquent
*/
class ProjectTaskTemplate extends AbstractModel
{
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'project_id',
'name',
'title',
'content',
'sort',
'is_default',
'userid',
'use_count',
'last_used_at'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'last_used_at' => 'datetime',
];
/**
* 关联项目
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function project()
{
return $this->belongsTo(Project::class);
}
/**
* 关联创建者
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class, 'userid');
}
/**
* 原子递增使用次数并刷新最近使用时间。
*/
public function incrementUsage(): void
{
$this->newQuery()
->where('id', $this->id)
->update([
'use_count' => \DB::raw('use_count + 1'),
'last_used_at' => now(),
]);
}
}

View File

@ -14,9 +14,15 @@ namespace App\Models;
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\ProjectTask|null $projectTask
* @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|ProjectTaskUser newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskUser newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskUser query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskUser whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskUser whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskUser whereOwner($value)
@ -46,7 +52,7 @@ class ProjectTaskUser extends AbstractModel
*/
public static function transfer($originalUserid, $newUserid)
{
self::whereUserid($originalUserid)->chunk(100, function ($list) use ($originalUserid, $newUserid) {
self::whereUserid($originalUserid)->chunkById(100, function ($list) use ($originalUserid, $newUserid) {
$tastIds = [];
/** @var self $item */
foreach ($list as $item) {
@ -62,7 +68,18 @@ class ProjectTaskUser extends AbstractModel
$item->save();
}
if ($item->projectTask) {
$item->projectTask->addLog("移交{任务}身份", ['userid' => [$originalUserid, ' => ', $newUserid]]);
$item->projectTask->addLog("移交{任务}身份", [
'change' => [
[
'type' => 'user',
'data' => $originalUserid,
],
[
'type' => 'user',
'data' => $newUserid,
]
],
], 0, 1);
if (!in_array($item->task_pid, $tastIds)) {
$tastIds[] = $item->task_pid;
$item->projectTask->syncDialogUser();

View File

@ -0,0 +1,43 @@
<?php
namespace App\Models;
/**
* App\Models\ProjectTaskVisibilityUser
*
* @property int $id
* @property int|null $project_id 项目ID
* @property int|null $task_id 任务ID
* @property int|null $userid 成员ID
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\ProjectTask|null $projectTask
* @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|ProjectTaskVisibilityUser newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser whereProjectId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser whereTaskId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser whereUserid($value)
* @mixin \Eloquent
*/
class ProjectTaskVisibilityUser extends AbstractModel
{
/**
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function projectTask(): \Illuminate\Database\Eloquent\Relations\HasOne
{
return $this->hasOne(ProjectTask::class, 'id', 'task_id');
}
}

View File

@ -2,6 +2,8 @@
namespace App\Models;
use App\Module\Base;
/**
* App\Models\ProjectUser
*
@ -9,17 +11,25 @@ namespace App\Models;
* @property int|null $project_id 项目ID
* @property int|null $userid 成员ID
* @property int|null $owner 是否负责人
* @property string|null $top_at 置顶时间
* @property \Illuminate\Support\Carbon|null $top_at 置顶时间
* @property int|null $sort 排序(ASC)
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Project|null $project
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereOwner($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereProjectId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereSort($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereTopAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereUserid($value)
@ -27,6 +37,36 @@ namespace App\Models;
*/
class ProjectUser extends AbstractModel
{
/** @var int 普通成员编码 */
const OWNER_MEMBER = 0;
/** @var int 项目负责人编码 */
const OWNER_PRIMARY = 1;
/** @var int 项目管理员编码 */
const OWNER_DEPUTY = 2;
/**
* 是否项目负责人owner=1
*/
public function isPrimaryOwner(): bool
{
return (int)$this->owner === self::OWNER_PRIMARY;
}
/**
* 是否项目管理员owner=2
*/
public function isDeputyOwner(): bool
{
return (int)$this->owner === self::OWNER_DEPUTY;
}
/**
* 是否负责人(含项目管理员)
*/
public function isOwner(): bool
{
return $this->isPrimaryOwner() || $this->isDeputyOwner();
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasOne
@ -44,17 +84,26 @@ 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();
if ($row) {
// 已存在则删除原数据,判断改变已存在的数据
$row->owner = max($row->owner, $item->owner);
// 已存在仅当离职用户是项目负责人owner=1时把接收人升为项目负责人
// 离职用户是项目管理员owner=2时不传项目管理员身份给接收人spec项目管理员不替补
if ((int)$item->owner === self::OWNER_PRIMARY) {
$row->owner = self::OWNER_PRIMARY;
}
// owner=2/0保留接收人原有 owner 值不变
$row->save();
$item->delete();
} else {
// 不存在则改变原数据
// 不存在:转移时如果离职用户是项目管理员,降级为普通成员(不带项目管理员身份过户给接收人)
if ((int)$item->owner === self::OWNER_DEPUTY) {
$item->owner = self::OWNER_MEMBER;
}
$item->userid = $newUserid;
$item->save();
}
@ -64,11 +113,36 @@ class ProjectUser extends AbstractModel
$item->project->name = "{$name}{$item->project->name}";
$item->project->save();
}
$item->project->addLog("移交项目身份", ['userid' => [$originalUserid, ' => ', $newUserid]]);
$item->project->addLog("移交项目身份", [
'change' => [
[
'type' => 'user',
'data' => $originalUserid
],
[
'type' => 'user',
'data' => $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;
@ -10,6 +11,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use JetBrains\PhpStorm\Pure;
/**
@ -25,13 +27,22 @@ use JetBrains\PhpStorm\Pure;
* @property string $sign 汇报唯一标识
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ReportReceive> $Receives
* @property-read int|null $receives_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ReportAnalysis> $aiAnalyses
* @property-read int|null $ai_analyses_count
* @property-read \App\Models\ReportAnalysis|null $aiAnalysis
* @property-read mixed $receives
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\User> $receivesUser
* @property-read int|null $receives_user_count
* @property-read \App\Models\User|null $sendUser
* @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 Builder|Report newModelQuery()
* @method static Builder|Report newQuery()
* @method static Builder|Report query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static Builder|Report whereContent($value)
* @method static Builder|Report whereCreatedAt($value)
* @method static Builder|Report whereId($value)
@ -48,6 +59,15 @@ class Report extends AbstractModel
const WEEKLY = "weekly";
const DAILY = "daily";
public const LIST_FIELDS = [
'id',
'title',
'type',
'userid',
'sign',
'created_at',
'updated_at',
];
protected $fillable = [
"title",
@ -68,7 +88,17 @@ class Report extends AbstractModel
public function receivesUser(): BelongsToMany
{
return $this->belongsToMany(User::class, ReportReceive::class, "rid", "userid")
->withPivot("receive_time", "read");
->withPivot("receive_at", "read");
}
public function aiAnalyses(): HasMany
{
return $this->hasMany(ReportAnalysis::class, 'rid');
}
public function aiAnalysis(): HasOne
{
return $this->hasOne(ReportAnalysis::class, 'rid');
}
public function sendUser()
@ -76,15 +106,6 @@ class Report extends AbstractModel
return $this->hasOne(User::class, "userid", "userid");
}
public function getTypeAttribute($value): string
{
return match ($value) {
Report::WEEKLY => "周报",
Report::DAILY => "日报",
default => "",
};
}
public function getContentAttribute($value): string
{
return htmlspecialchars_decode($value);
@ -98,6 +119,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
@ -142,12 +181,12 @@ class Report extends AbstractModel
// 如果设置了周期偏移量
empty( $offset ) || $now_dt->subWeeks( abs( $offset ) );
$now_dt->startOfWeek(); // 设置为当周第一天
return $now_dt->year . $now_dt->weekOfYear;
return now()->year . $now_dt->weekOfYear;
},
Report::DAILY => function() use ($now_dt, $offset) {
// 如果设置了周期偏移量
empty( $offset ) || $now_dt->subDays( abs( $offset ) );
return $now_dt->format("Ymd");
return now()->format("Ymd");
},
default => "",
};

View File

@ -0,0 +1,58 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* App\Models\ReportAnalysis
*
* @property int $id
* @property int $rid 报告ID
* @property int $userid 生成分析的会员ID
* @property string $model 使用的模型名称
* @property string $analysis_text AI 分析的原始文本Markdown
* @property array|null $meta 额外的上下文信息
* @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|ReportAnalysis newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ReportAnalysis newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ReportAnalysis query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|ReportAnalysis whereAnalysisText($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportAnalysis whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportAnalysis whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportAnalysis whereMeta($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportAnalysis whereModel($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportAnalysis whereRid($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportAnalysis whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportAnalysis whereUserid($value)
* @mixin \Eloquent
*/
class ReportAnalysis extends AbstractModel
{
protected $table = 'report_ai_analyses';
protected $fillable = [
'rid',
'userid',
'model',
'analysis_text',
'meta',
];
protected $casts = [
'meta' => 'array',
];
public function report(): BelongsTo
{
return $this->belongsTo(Report::class, 'rid');
}
}

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

@ -10,15 +10,21 @@ use Illuminate\Database\Eloquent\Model;
*
* @property int $id
* @property int $rid
* @property string|null $receive_time 接收时间
* @property \Illuminate\Support\Carbon|null $receive_at 接收时间
* @property int $userid 接收人
* @property int $read 是否已读
* @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|ReportReceive newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive whereRead($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive whereReceiveTime($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive whereReceiveAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive whereRid($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive whereUserid($value)
* @mixin \Eloquent
@ -32,7 +38,7 @@ class ReportReceive extends AbstractModel
protected $fillable = [
"rid",
"receive_time",
"receive_at",
"userid",
"read",
];

View File

@ -2,7 +2,12 @@
namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use App\Module\Doo;
use App\Module\Timer;
use App\Module\AI;
use Carbon\Carbon;
/**
* App\Models\Setting
@ -10,12 +15,18 @@ use App\Module\Base;
* @property int $id
* @property string|null $name
* @property string|null $desc 参数描述、备注
* @property string|null $setting
* @property array $setting
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
* @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|Setting newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Setting newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Setting query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|Setting whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Setting whereDesc($value)
* @method static \Illuminate\Database\Eloquent\Builder|Setting whereId($value)
@ -26,6 +37,412 @@ use App\Module\Base;
*/
class Setting extends AbstractModel
{
/**
* 格式化设置参数
* @param $value
* @return array
*/
public function getSettingAttribute($value)
{
if (is_array($value)) {
return $value;
}
$value = Base::json2array($value);
switch ($this->name) {
// 系统设置
case 'system':
$value['system_alias'] = $value['system_alias'] ?: env('APP_NAME');
$value['image_compress'] = $value['image_compress'] ?: 'open';
$value['image_quality'] = min(100, max(0, intval($value['image_quality']) ?: 90));
$value['image_save_local'] = $value['image_save_local'] ?: 'open';
$value['task_user_limit'] = min(2000, max(1, intval($value['task_user_limit']) ?: 500));
if (!is_array($value['task_default_time']) || count($value['task_default_time']) != 2 || !Timer::isTime($value['task_default_time'][0]) || !Timer::isTime($value['task_default_time'][1])) {
$value['task_default_time'] = ['09:00', '18:00'];
}
// 项目创建权限范围all/departmentOwner/appoint默认 all+ 指定人员
$value['project_add_permission'] = array_values(array_intersect(
is_array($value['project_add_permission'] ?? null) ? $value['project_add_permission'] : [],
['all', 'departmentOwner', 'appoint']
)) ?: ['all'];
$value['project_add_userids'] = is_array($value['project_add_userids'] ?? null)
? array_values(array_unique(array_filter(array_map('intval', $value['project_add_userids']))))
: [];
break;
// 文件设置
case 'fileSetting':
$value['permission_pack_type'] = $value['permission_pack_type'] ?: 'all';
$value['permission_pack_userids'] = is_array($value['permission_pack_userids']) ? $value['permission_pack_userids'] : [];
break;
// AI 机器人设置
case 'aibotSetting':
if (!empty($value['claude_token']) && empty($value['claude_key'])) {
$value['claude_key'] = $value['claude_token'];
}
$array = [];
$aiList = ['openai', 'claude', 'deepseek', 'gemini', 'grok', 'ollama', 'zhipu', 'qianwen', 'wenxin'];
$fieldList = ['key', 'secret', 'models', 'model', 'base_url', 'agency', 'temperature', 'system'];
foreach ($aiList as $aiName) {
foreach ($fieldList as $fieldName) {
$key = $aiName . '_' . $fieldName;
$content = !empty($value[$key]) ? trim($value[$key]) : '';
switch ($fieldName) {
case 'models':
if ($content) {
$content = explode("\n", $content);
$content = array_filter($content);
}
$content = is_array($content) ? implode("\n", $content) : '';
break;
case 'model':
$models = Setting::AIBotModels2Array($array[$key . 's'], true);
$content = in_array($content, $models) ? $content : ($models[0] ?? '');
break;
case 'temperature':
if ($content) {
$content = floatval(min(1, max(0, floatval($content) ?: 0.7)));
}
break;
}
$array[$key] = $content;
}
}
$value = $array;
break;
}
return $value;
}
/**
* 规范任务优先级设置(确保字段完整且仅有一个默认项)
* @param mixed $list
* @return array<int, array{name:string,color:string,days:int,priority:int,is_default:int}>
*/
public static function normalizeTaskPriorityList($list)
{
if (!is_array($list)) {
return [];
}
$normalized = [];
$defaultIndex = null;
foreach ($list as $item) {
if (!is_array($item)) {
continue;
}
$name = trim((string)($item['name'] ?? ''));
$color = trim((string)($item['color'] ?? ''));
$priority = intval($item['priority'] ?? 0);
if ($name === '' || $color === '' || $priority <= 0) {
continue;
}
$days = intval($item['days'] ?? 0);
$isDefault = !empty($item['is_default']) || !empty($item['default']);
if ($defaultIndex === null && $isDefault) {
$defaultIndex = count($normalized);
}
$normalized[] = [
'name' => $name,
'color' => $color,
'days' => $days,
'priority' => $priority,
'is_default' => $isDefault ? 1 : 0,
];
}
if (!empty($normalized)) {
$defaultIndex = $defaultIndex ?? 0;
foreach ($normalized as $i => $row) {
$normalized[$i]['is_default'] = $i === $defaultIndex ? 1 : 0;
}
}
return array_values($normalized);
}
/**
* 获取默认任务优先级(来自 settings.priority
* @param array|null $list
* @return array|null
*/
public static function getDefaultTaskPriorityItem($list = null)
{
$list = $list ?? Base::setting('priority');
$list = self::normalizeTaskPriorityList($list);
if (empty($list)) {
return null;
}
foreach ($list as $item) {
if (!empty($item['is_default'])) {
return $item;
}
}
return $list[0];
}
/**
* 是否开启 AI 助手
* @return bool
*/
public static function AIOpen()
{
$setting = Base::setting('aibotSetting');
if (!is_array($setting) || empty($setting)) {
return false;
}
foreach (AI::TEXT_MODEL_PRIORITY as $vendor) {
if (self::isAIBotVendorEnabled($setting, $vendor)) {
return true;
}
}
return false;
}
/**
* 判断 AI 机器人厂商是否启用
* @param array $setting
* @param string $vendor
* @return bool
*/
protected static function isAIBotVendorEnabled(array $setting, string $vendor): bool
{
$key = trim((string)($setting[$vendor . '_key'] ?? ''));
return match ($vendor) {
'ollama' => $key !== '' || !empty($setting['ollama_base_url']),
default => $key !== '',
};
}
/**
* AI 机器人模型转数组
* @param $models
* @param bool $retValue
* @return array
*/
public static function AIBotModels2Array($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 $list
* @return array
*/
public static function normalizeCustomMicroApps($list)
{
if (!is_array($list)) {
return [];
}
$apps = [];
foreach ($list as $item) {
$app = self::normalizeCustomMicroAppItem($item);
if ($app) {
$apps[] = $app;
}
}
return $apps;
}
/**
* 根据用户身份过滤可见的自定义微应用
* @param array $apps
* @param \App\Models\User|null $user
* @return array
*/
public static function filterCustomMicroAppsForUser(array $apps, $user)
{
if (empty($apps)) {
return [];
}
$isAdmin = $user ? $user->isAdmin() : false;
$userId = $user ? intval($user->userid) : 0;
$filtered = [];
foreach ($apps as $app) {
$visible = self::normalizeCustomMicroVisible($app['visible_to'] ?? ['admin']);
if (!self::isCustomMicroVisibleTo($visible, $isAdmin, $userId)) {
continue;
}
if (empty($app['menu_items']) || !is_array($app['menu_items'])) {
continue;
}
$menus = array_values(array_filter($app['menu_items'], function ($menu) use ($isAdmin, $userId) {
if (!isset($menu['visible_to'])) {
return true;
}
$visible = self::normalizeCustomMicroVisible($menu['visible_to']);
return self::isCustomMicroVisibleTo($visible, $isAdmin, $userId);
}));
if (empty($menus)) {
continue;
}
$app['menu_items'] = $menus;
$filtered[] = $app;
}
return $filtered;
}
/**
* 将存储结构转换成 appstore 接口同款格式
* @param array $apps
* @return array
*/
public static function formatCustomMicroAppsForResponse(array $apps)
{
return array_values(array_map(function ($app) {
unset($app['visible_to']);
if (!empty($app['menu_items']) && is_array($app['menu_items'])) {
$app['menu_items'] = array_values(array_map(function ($menu) {
$menu['keep_alive'] = isset($menu['keep_alive']) ? (bool)$menu['keep_alive'] : true;
$menu['disable_scope_css'] = (bool)($menu['disable_scope_css'] ?? false);
$menu['auto_dark_theme'] = isset($menu['auto_dark_theme']) ? (bool)$menu['auto_dark_theme'] : true;
$menu['transparent'] = (bool)($menu['transparent'] ?? false);
if (isset($menu['visible_to'])) {
unset($menu['visible_to']);
}
return $menu;
}, $app['menu_items']));
}
return $app;
}, $apps));
}
/**
* 规范自定义微应用
* @param array $item
* @return array|null
*/
protected static function normalizeCustomMicroAppItem($item)
{
if (!is_array($item)) {
return null;
}
$id = trim($item['id'] ?? '');
if ($id === '') {
return null;
}
$name = Base::newTrim($item['name'] ?? '');
$version = Base::newTrim($item['version'] ?? '') ?: 'custom';
$menuItems = [];
if (isset($item['menu_items']) && is_array($item['menu_items'])) {
$menuItems = $item['menu_items'];
} elseif (isset($item['menu']) && is_array($item['menu'])) {
$menuItems = [$item['menu']];
}
if (empty($menuItems)) {
return null;
}
$normalizedMenus = [];
foreach ($menuItems as $menu) {
$formattedMenu = self::normalizeCustomMicroMenuItem($menu, $name ?: $id);
if ($formattedMenu) {
$normalizedMenus[] = $formattedMenu;
}
}
if (empty($normalizedMenus)) {
return null;
}
return Base::newTrim([
'id' => $id,
'name' => $name,
'version' => $version,
'menu_items' => $normalizedMenus,
'visible_to' => self::normalizeCustomMicroVisible($item['visible_to'] ?? 'admin'),
]);
}
/**
* 规范自定义微应用菜单项
* @param array $menu
* @param string $fallbackLabel
* @return array|null
*/
protected static function normalizeCustomMicroMenuItem($menu, $fallbackLabel = '')
{
if (!is_array($menu)) {
return null;
}
$url = trim($menu['url'] ?? '');
if ($url === '') {
return null;
}
$location = trim($menu['location'] ?? 'application');
$label = trim($menu['label'] ?? $fallbackLabel);
$type = strtolower(trim($menu['type'] ?? 'iframe'));
$payload = [
'location' => $location,
'label' => $label,
'icon' => Base::newTrim($menu['icon'] ?? ''),
'url' => $url,
'type' => $type,
'keep_alive' => isset($menu['keep_alive']) ? (bool)$menu['keep_alive'] : true,
'disable_scope_css' => (bool)($menu['disable_scope_css'] ?? false),
'auto_dark_theme' => isset($menu['auto_dark_theme']) ? (bool)$menu['auto_dark_theme'] : true,
'transparent' => (bool)($menu['transparent'] ?? false),
];
if (!empty($menu['background'])) {
$payload['background'] = Base::newTrim($menu['background']);
}
if (!empty($menu['capsule']) && is_array($menu['capsule'])) {
$payload['capsule'] = Base::newTrim($menu['capsule']);
}
return $payload;
}
/**
* 规范自定义微应用可见范围
* @param mixed $value
* @return array
*/
protected static function normalizeCustomMicroVisible($value)
{
if (is_array($value)) {
$list = array_filter(array_map('trim', $value));
} else {
$list = array_filter(array_map('trim', explode(',', (string)$value)));
}
if (empty($list)) {
return ['admin'];
}
if (in_array('all', $list)) {
return ['all'];
}
return array_values($list);
}
/**
* 判断自定义微应用是否可见
* @param array $visible
* @param bool $isAdmin
* @param int $userId
* @return bool
*/
protected static function isCustomMicroVisibleTo(array $visible, bool $isAdmin, int $userId)
{
if (in_array('all', $visible)) {
return true;
}
if ($isAdmin && in_array('admin', $visible)) {
return true;
}
if ($userId > 0 && in_array((string)$userId, $visible, true)) {
return true;
}
return false;
}
/**
* 验证邮箱地址(过滤忽略地址)
* @param $array
@ -62,4 +479,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('已超过' . Base::forumMinuteDay($limitNum) . '' . $error);
}
}
}

View File

@ -10,15 +10,21 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property int $id
* @property string|null $args
* @property string|null $error
* @property string|null $start_at 开始时间
* @property string|null $end_at 结束时间
* @property \Illuminate\Support\Carbon|null $start_at 开始时间
* @property \Illuminate\Support\Carbon|null $end_at 结束时间
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @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|TaskWorker newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereArgs($value)
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereDeletedAt($value)

View File

@ -11,9 +11,15 @@ namespace App\Models;
* @property string|null $content
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @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|Tmp newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Tmp newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Tmp query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|Tmp whereContent($value)
* @method static \Illuminate\Database\Eloquent\Builder|Tmp whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Tmp whereId($value)

View File

@ -14,23 +14,109 @@ use Hedeqiang\UMeng\IOS;
* @property int|null $userid 会员ID
* @property string|null $alias 别名
* @property string|null $platform 平台类型
* @property string|null $device 设备类型
* @property string|null $device_hash 设备哈希值用于关联UserDevice表
* @property string|null $version 应用版本号
* @property string|null $ua userAgent
* @property int|null $is_notified 通知权限
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @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|UmengAlias newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereAlias($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereDevice($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereDeviceHash($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereIsNotified($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias wherePlatform($value)
* @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
{
protected $table = 'umeng_alias';
private static $waitSend = [];
/**
* 推送消息
* @param $push
* @return void
*/
private static function sendTask($push = null)
{
if ($push) {
self::$waitSend[] = $push;
}
if (!self::$waitSend) {
return;
}
$first = array_shift(self::$waitSend);
if (empty($first)) {
return;
}
$instance = null;
$responsePayload = null;
try {
switch ($first['platform']) {
case 'ios':
$instance = new IOS($first['config']);
break;
case 'android':
$instance = new Android($first['config']);
break;
default:
return;
}
$responsePayload = $instance->send($first['data']);
} catch (\Exception $e) {
$responsePayload = [
'error' => $e->getMessage(),
];
$first['retry'] = intval($first['retry'] ?? 0) + 1;
if ($first['retry'] > 3) {
info("[PushMsg] fail: " . $e->getMessage());
} else {
info("[PushMsg] retry ({$first['retry']}): " . $e->getMessage());
self::$waitSend[] = $first;
}
} finally {
if ($instance !== null) {
UmengLog::create([
'request' => Base::array2json($first['data']),
'response' => Base::array2json($responsePayload),
]);
}
self::sendTask();
}
}
/**
* 推送内容处理
* @param $string
* @return string
*/
private static function specialCharacters($string)
{
return str_replace(["\r\n", "\r", "\n"], '', $string);
}
/**
* 获取推送配置
* @return array|false
@ -64,79 +150,98 @@ class UmengAlias extends AbstractModel
* @param string $alias
* @param string $platform
* @param array $array [title, subtitle, body, description, extra, seconds, badge]
* @return array|false
* @return void
*/
public static function pushMsgToAlias($alias, $platform, $array)
private static function pushMsgToAlias($alias, $platform, $array)
{
$config = self::getPushConfig();
if ($config === false) {
return false;
return;
}
//
$title = $array['title'] ?: ''; // 标题
$subtitle = $array['subtitle'] ?: ''; // 副标题iOS
$body = $array['body'] ?: ''; // 通知内容
$title = self::specialCharacters($array['title'] ?: ''); // 标题
$subtitle = self::specialCharacters($array['subtitle'] ?: ''); // 副标题iOS
$body = self::specialCharacters($array['body'] ?: ''); // 通知内容
$description = $array['description'] ?: 'no description'; // 描述
$extra = is_array($array['extra']) ? $array['extra'] : []; // 额外参数
$seconds = intval($array['seconds']) ?: 86400; // 有效时间(单位:秒)
$badge = intval($array['badge']) ?: 0; // 角标数iOS
$badge = intval($array['badge']) ?: 0; // 角标数
//
switch ($platform) {
case 'ios':
if (!isset($config['iOS'])) {
return false;
return;
}
$ios = new IOS($config);
return $ios->send([
'description' => $description,
'payload' => array_merge([
'aps' => [
'alert' => [
'title' => $title,
'subtitle' => $subtitle,
'body' => $body,
self::sendTask([
'platform' => $platform,
'config' => $config,
'data' => [
'description' => $description,
'payload' => array_merge([
'aps' => [
'alert' => [
'title' => $title,
'subtitle' => $subtitle,
'body' => $body,
],
'sound' => 'default',
'badge' => $badge,
],
'sound' => 'default',
'badge' => $badge,
], $extra),
'type' => 'customizedcast',
'alias_type' => 'userid',
'alias' => $alias,
'policy' => [
'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(),
],
], $extra),
'type' => 'customizedcast',
'alias_type' => 'userid',
'alias' => $alias,
'policy' => [
'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(),
],
]
]);
break;
case 'android':
if (!isset($config['Android'])) {
return false;
return;
}
$android = new Android($config);
return $android->send([
'description' => $description,
'payload' => array_merge([
'display_type' => 'notification',
'body' => [
'ticker' => $title,
'text' => $body,
'title' => $title,
'after_open' => 'go_app',
'play_sound' => true,
self::sendTask([
'platform' => $platform,
'config' => $config,
'data' => [
'description' => $description,
'payload' => array_merge([
'display_type' => 'notification',
'body' => [
'ticker' => $title,
'text' => $body,
'title' => $title,
'after_open' => 'go_app',
'play_sound' => true,
'set_badge' => min(99, $badge),
],
], $extra),
'type' => 'customizedcast',
'alias_type' => 'userid',
'alias' => $alias,
'mipush' => true,
'mi_activity' => 'app.eeui.umeng.activity.MfrMessageActivity',
'policy' => [
'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(),
],
], $extra),
'type' => 'customizedcast',
'alias_type' => 'userid',
'alias' => $alias,
'mipush' => true,
'mi_activity' => 'app.eeui.umeng.activity.MfrMessageActivity',
'policy' => [
'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(),
'category' => 1,
'channel_properties' => [
'main_activity' => 'com.dootask.task.WelcomeActivity',
'oppo_channel_id' => 'dootask',
'vivo_category' => 'IM',
'huawei_channel_importance' => 'NORMAL',
'huawei_channel_category' => 'IM',
'channel_fcm' => 0,
],
'local_properties' => [
'importance' => 'IMPORTANCE_DEFAULT',
'category' => 'CATEGORY_MESSAGE',
]
]
]);
default:
return false;
break;
}
}
@ -163,7 +268,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());
}
}
}
});

32
app/Models/UmengLog.php Normal file
View File

@ -0,0 +1,32 @@
<?php
namespace App\Models;
/**
* App\Models\UmengLog
*
* @property int $id
* @property string|null $request 请求参数
* @property string|null $response 推送返回
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @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|UmengLog newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UmengLog newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UmengLog query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|UmengLog whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengLog whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengLog whereRequest($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengLog whereResponse($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengLog whereUpdatedAt($value)
* @mixin \Eloquent
*/
class UmengLog extends AbstractModel
{
protected $guarded = [];
}

View File

@ -2,10 +2,14 @@
namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use App\Module\Doo;
use App\Module\Apps;
use App\Module\Table\OnlineData;
use App\Observers\AbstractObserver;
use App\Services\RequestContext;
use App\Tasks\ManticoreSyncTask;
use Cache;
use Carbon\Carbon;
@ -21,27 +25,40 @@ use Carbon\Carbon;
* @property string|null $tel 联系电话
* @property string $nickname 昵称
* @property string|null $profession 职位/职称
* @property string|null $birthday 生日
* @property string|null $address 地址
* @property string|null $introduction 个人简介
* @property string $userimg 头像
* @property string|null $encrypt
* @property string|null $password 登录密码
* @property int|null $changepass 登录需要修改密码
* @property int|null $login_num 累计登录次数
* @property string|null $last_ip 最后登录IP
* @property string|null $last_at 最后登录时间
* @property \Illuminate\Support\Carbon|null $last_at 最后登录时间
* @property string|null $line_ip 最后在线IP接口
* @property string|null $line_at 最后在线时间(接口)
* @property \Illuminate\Support\Carbon|null $line_at 最后在线时间(接口)
* @property int|null $task_dialog_id 最后打开的任务会话ID
* @property string|null $created_ip 注册IP
* @property string|null $disable_at 禁用时间(离职时间)
* @property \Illuminate\Support\Carbon|null $disable_at 禁用时间(离职时间)
* @property int|null $email_verity 邮箱是否已验证
* @property int|null $bot 是否机器人
* @property string|null $lang 语言首选项
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @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 \Database\Factories\UserFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
* @method static \Illuminate\Database\Eloquent\Builder|User newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|User newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|User query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|User searchByKeyword(string $keyword)
* @method static \Illuminate\Database\Eloquent\Builder|User whereAddress($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereAz($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereBirthday($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereBot($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereChangepass($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereCreatedAt($value)
@ -52,6 +69,8 @@ use Carbon\Carbon;
* @method static \Illuminate\Database\Eloquent\Builder|User whereEmailVerity($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereEncrypt($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereIdentity($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereIntroduction($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereLang($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereLastAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereLastIp($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereLineAt($value)
@ -70,6 +89,8 @@ use Carbon\Carbon;
*/
class User extends AbstractModel
{
const IMPORT_MAX = 500;
protected $primaryKey = 'userid';
protected $hidden = [
@ -89,7 +110,13 @@ class User extends AbstractModel
*/
public function getNicknameAttribute($value)
{
return $value ?: Base::cardFormat($this->email);
if ($value) {
if (UserBot::isSystemBot($this->email)) {
return Doo::translate($value);
}
return $value;
}
return Base::formatName($this->email);
}
/**
@ -144,7 +171,7 @@ class User extends AbstractModel
});
$array = [];
foreach ($list as $item) {
$array[] = $item['name'] . ($item['owner_userid'] === $this->userid ? '(M)' : '');
$array[] = $item['name'] . ($item['owner_userid'] === $this->userid ? ' (M)' : '');
}
return implode(', ', $array);
}
@ -157,10 +184,9 @@ class User extends AbstractModel
return UserDepartment::where('owner_userid', $this->userid)->exists();
}
/**
* 获取机器人所有者
* @return int|mixed
* @return int
*/
public function getBotOwner()
{
@ -168,9 +194,9 @@ class User extends AbstractModel
return 0;
}
$key = "userBotOwner::" . $this->userid;
return Cache::remember($key, now()->addMonth(), function() {
return intval(Cache::remember($key, now()->addMonth(), function() {
return intval(UserBot::whereBotId($this->userid)->value('userid')) ?: $this->userid;
});
}));
}
/**
@ -179,7 +205,7 @@ class User extends AbstractModel
*/
public function getOnlineStatus()
{
$online = $this->bot || Cache::get("User::online:" . $this->userid) === "on";
$online = $this->bot || OnlineData::live($this->userid) > 0;
if ($online) {
return true;
}
@ -206,10 +232,14 @@ class User extends AbstractModel
/**
* 返回是否禁用帐号(离职)
* @param bool $incAt 是否包含禁用时间
* @return bool
*/
public function isDisable()
public function isDisable($incAt = false)
{
if ($incAt) {
return in_array('disable', $this->identity) || $this->disable_at;
}
return in_array('disable', $this->identity);
}
@ -222,6 +252,31 @@ class User extends AbstractModel
return in_array('admin', $this->identity);
}
/**
* 返回是否AI机器人
* @return bool
*/
public function isAiBot(&$aiName = '')
{
if (preg_match('/^ai-(.*?)@bot\.system$/', $this->email, $matches)) {
$aiName = $matches[1];
return true;
}
return false;
}
/**
* 返回是否用户机器人
* @return bool
*/
public function isUserBot()
{
if (preg_match('/^user-(.*?)@bot\.system$/', $this->email)) {
return true;
}
return false;
}
/**
* 判断是否管理员
*/
@ -267,7 +322,7 @@ class User extends AbstractModel
*/
public function deleteUser($reason)
{
return AbstractModel::transaction(function () use ($reason) {
$ret = AbstractModel::transaction(function () use ($reason) {
// 删除原因
$userDelete = UserDelete::createInstance([
'operator' => User::userid(),
@ -288,6 +343,27 @@ class User extends AbstractModel
//
return $this->delete();
});
return $ret;
}
/**
* 检查发送聊天内容前必须设置昵称、电话
* @return void
*/
public function checkChatInformation()
{
if ($this->bot) {
return;
}
$chatInformation = Base::settingFind('system', 'chat_information');
if ($chatInformation == 'required') {
if (empty($this->getRawOriginal('nickname'))) {
throw new ApiException('请设置昵称', [], -2);
}
if (empty($this->getRawOriginal('tel'))) {
throw new ApiException('请设置联系电话', [], -3);
}
}
}
/** ***************************************************************************************** */
@ -341,7 +417,295 @@ class User extends AbstractModel
$dialog?->joinGroup($user->userid, 0);
}
}
return $user->find($user->userid);
$createdUser = $user->find($user->userid);
if (!$createdUser->bot) {
// Manticore 索引同步
AbstractObserver::taskDeliver(new ManticoreSyncTask('user_sync', $createdUser->toArray()));
// 触发 user_onboard hook
Apps::dispatchUserHook($createdUser, 'user_onboard', 'onboard');
}
return $createdUser;
}
/**
* 管理员创建员工账号(复用注册逻辑,强制正式身份,可选首登改密 / 部门 / 职位)
* @param string $email
* @param string $password
* @param string $nickname
* @param array $options changePass(bool,默认true) / emailVerity(bool,默认false,标记邮箱已认证) / department(int[]) / profession(string)
* @return self
* @throws ApiException
*/
public static function createByAdmin(string $email, $password, string $nickname, array $options = []): self
{
$nickname = trim($nickname);
if (mb_strlen($nickname) < 2 || mb_strlen($nickname) > 20) {
throw new ApiException('昵称需为2-20个字');
}
$changePass = ($options['changePass'] ?? true) ? 1 : 0;
$emailVerity = ($options['emailVerity'] ?? false) ? 1 : 0;
$profession = trim((string)($options['profession'] ?? ''));
// 校验前置reg 之前快速失败,且可在无 Swoole 环境单测)
self::assertValidProfession($profession);
$departmentIds = self::assertValidDepartments($options['department'] ?? []);
// 复用 reg邮箱校验/查重、passwordPolicy、Doo::userCreate、az/pinyin、全员群、索引同步、user_onboard hook
$user = self::reg($email, $password, ['nickname' => $nickname]);
// 管理员显式创建的账号视为正式员工,去除系统 reg_identity 可能带上的 temp
if (in_array('temp', $user->identity)) {
$user->identity = Base::arrayImplode(array_diff($user->identity, ['temp']));
}
$user->changepass = $changePass; // 复用现有首登强制改密机制
$user->email_verity = $emailVerity; // 管理员可在创建时直接标记邮箱认证状态
if ($profession !== '') {
$user->profession = $profession;
}
if ($departmentIds) {
$user->department = Base::arrayImplode($departmentIds);
}
$user->save();
// 设置了部门 → 加入对应部门群(复刻 operation 的 type=department 入群逻辑)
if ($departmentIds) {
$departments = UserDepartment::whereIn('id', $departmentIds)->get();
foreach ($departments as $department) {
try {
if ($department->dialog_id > 0 && $dialog = WebSocketDialog::find($department->dialog_id)) {
$dialog->joinGroup([$user->userid], 0, true);
$dialog->pushMsg("groupJoin", null, [$user->userid]);
}
} catch (\Throwable $e) {
// 部门入群为尽力投递:单个部门失败不影响账号创建与其他部门
\Log::warning('createByAdmin: 部门入群失败', [
'userid' => $user->userid,
'department_id' => $department->id,
'error' => $e->getMessage(),
]);
}
}
}
return $user;
}
/**
* 将上传表格Excel::toArray 的二维数组)归一化为导入行
* @param array $sheet
* @return array [{line, email, nickname, password}]
*/
public static function parseImportRows(array $sheet): array
{
$rows = [];
foreach ($sheet as $index => $cells) {
if ($index === 0) {
continue; // 表头
}
$email = trim((string)($cells[0] ?? ''));
$nickname = trim((string)($cells[1] ?? ''));
$password = trim((string)($cells[2] ?? ''));
$profession = trim((string)($cells[3] ?? ''));
if ($email === '' && $nickname === '' && $password === '') {
continue; // 空行(仅职位有值也视为空行跳过)
}
$rows[] = [
'line' => $index + 1, // 电子表格行号(从 1 开始)
'email' => $email,
'nickname' => $nickname,
'password' => $password,
'profession' => $profession,
];
}
return $rows;
}
/**
* 校验单条导入行
* @param array $row ['email'=>,'nickname'=>,'password'=>,'profession'=>(选填)]
* @return string|null 错误文案null 表示通过
*/
public static function validateImportRow(array $row): ?string
{
$email = trim((string)($row['email'] ?? ''));
$nickname = trim((string)($row['nickname'] ?? ''));
$password = trim((string)($row['password'] ?? ''));
if ($email === '' || $nickname === '' || $password === '') {
return '邮箱、昵称、初始密码均为必填';
}
if (!Base::isEmail($email)) {
return '邮箱格式不正确';
}
if (mb_strlen($nickname) < 2 || mb_strlen($nickname) > 20) {
return '昵称需为2-20个字';
}
try {
self::passwordPolicy($password);
} catch (ApiException $e) {
return $e->getMessage();
}
// 职位/职称选填,填写则校验 2-20 字
try {
self::assertValidProfession((string)($row['profession'] ?? ''));
} catch (ApiException $e) {
return $e->getMessage();
}
return null;
}
/**
* 校验职位/职称:非空时必须 2-20 字(复用 operation 的现有文案)
* @param string $profession
* @return void
* @throws ApiException
*/
public static function assertValidProfession(string $profession): void
{
$profession = trim($profession);
if ($profession === '') {
return;
}
if (mb_strlen($profession) < 2) {
throw new ApiException('职位/职称不可以少于2个字');
}
if (mb_strlen($profession) > 20) {
throw new ApiException('职位/职称最多只能设置20个字');
}
}
/**
* 规整并校验部门 ID 列表:转正整数去重、最多 10 个、且每个必须存在
* @param mixed $ids
* @return int[]
* @throws ApiException
*/
public static function assertValidDepartments($ids): array
{
if (!is_array($ids)) {
$ids = [];
}
$ids = array_values(array_unique(array_filter(array_map('intval', $ids), fn($v) => $v > 0)));
if (count($ids) > 10) {
throw new ApiException('最多只可加入10个部门');
}
if ($ids) {
$existing = UserDepartment::whereIn('id', $ids)->pluck('id')->map(fn($v) => (int)$v)->all();
if (count($existing) < count($ids)) {
throw new ApiException('修改部门不存在');
}
}
return $ids;
}
/**
* 批量导入用户(部门/职位逐行department 来自前端逐行设置profession 来自 Excel 行)
* @param array $rows 每行含 email/nickname/password/profession可选 department(int[])
* @param bool $changePass 是否要求首登改密(对本批所有账号生效)
* @return array ['total'=>int, 'success'=>int, 'failed'=>[['line','email','reason']]]
* @throws ApiException 行数超限
*/
public static function importUsers(array $rows, bool $changePass = true): array
{
if (count($rows) > self::IMPORT_MAX) {
throw new ApiException('单次最多导入' . self::IMPORT_MAX . '条');
}
$success = 0;
$failed = [];
$seen = [];
foreach ($rows as $row) {
$error = self::validateImportRow($row);
if ($error === null) {
$emailLower = strtolower(trim((string)$row['email']));
if (isset($seen[$emailLower])) {
$error = '文件内邮箱重复';
} else {
$seen[$emailLower] = true;
}
}
if ($error === null) {
try {
self::createByAdmin($row['email'], $row['password'], $row['nickname'], [
'changePass' => $changePass,
'emailVerity' => !empty($row['email_verity']),
'department' => $row['department'] ?? [],
'profession' => $row['profession'] ?? '',
]);
$success++;
continue;
} catch (ApiException $e) {
$error = $e->getMessage();
}
}
$failed[] = [
'line' => $row['line'] ?? 0,
'email' => $row['email'] ?? '',
'reason' => $error,
];
}
return [
'total' => count($rows),
'success' => $success,
'failed' => $failed,
];
}
/**
* 批量导入预览(只解析+校验,不创建任何账号)
* 逐行判定 ok/error必填/邮箱格式/昵称长度/密码策略、文件内邮箱重复、系统中邮箱已存在
* @param array $rows parseImportRows 的输出
* @return array ['total'=>int,'valid'=>int,'invalid'=>int,'rows'=>[['line','email','nickname','password','status','reason']]]
*/
public static function importPreview(array $rows): array
{
if (count($rows) > self::IMPORT_MAX) {
throw new ApiException('单次最多导入' . self::IMPORT_MAX . '条');
}
// 预查系统中已存在的邮箱(小写比较)
$emails = [];
foreach ($rows as $row) {
$e = strtolower(trim((string)($row['email'] ?? '')));
if ($e !== '') {
$emails[$e] = true;
}
}
$existing = [];
if ($emails) {
foreach (self::whereIn('email', array_keys($emails))->pluck('email') as $em) {
$existing[strtolower($em)] = true;
}
}
$seen = [];
$valid = 0;
$list = [];
foreach ($rows as $row) {
$reason = self::validateImportRow($row);
$emailLower = strtolower(trim((string)($row['email'] ?? '')));
if ($reason === null) {
if (isset($seen[$emailLower])) {
$reason = '文件内邮箱重复';
} else {
$seen[$emailLower] = true;
if (isset($existing[$emailLower])) {
$reason = '邮箱地址已存在';
}
}
}
$ok = $reason === null;
if ($ok) {
$valid++;
}
$list[] = [
'line' => $row['line'] ?? 0,
'email' => $row['email'] ?? '',
'nickname' => $row['nickname'] ?? '',
'password' => $row['password'] ?? '',
'profession' => $row['profession'] ?? '',
'email_verity' => 1, // 默认标记为已认证,前端可在预览中按行调整
'status' => $ok ? 'ok' : 'error',
'reason' => $reason ?? '',
];
}
return [
'total' => count($rows),
'valid' => $valid,
'invalid' => count($rows) - $valid,
'rows' => $list,
];
}
/**
@ -379,13 +743,15 @@ class User extends AbstractModel
{
$user = self::authInfo();
if (!$user) {
if (Base::token()) {
$token = Base::token();
if ($token) {
UserDevice::forget($token);
throw new ApiException('身份已失效,请重新登录', [], -1);
} else {
throw new ApiException('请登录后继续...', [], -1);
}
}
if (in_array('disable', $user->identity)) {
if ($user->isDisable()) {
throw new ApiException('帐号已停用...', [], -1);
}
if ($identity) {
@ -400,27 +766,47 @@ class User extends AbstractModel
*/
private static function authInfo()
{
global $_A;
if (isset($_A["__static_auth"])) {
return $_A["__static_auth"];
if (RequestContext::has('auth')) {
// 缓存
return RequestContext::get('auth');
}
if (Doo::userId() > 0
&& !Doo::userExpired()
&& $user = self::whereUserid(Doo::userId())->whereEmail(Doo::userEmail())->whereEncrypt(Doo::userEncrypt())->first()) {
$upArray = [];
if (Base::getIp() && $user->line_ip != Base::getIp()) {
$upArray['line_ip'] = Base::getIp();
}
if (Carbon::parse($user->line_at)->addSeconds(30)->lt(Carbon::now())) {
$upArray['line_at'] = Carbon::now();
}
if ($upArray) {
$user->updateInstance($upArray);
$user->save();
}
return $_A["__static_auth"] = $user;
if (Doo::userId() <= 0) {
// 没有登录
return RequestContext::save('auth', false);
}
return $_A["__static_auth"] = false;
if (Doo::userExpired()) {
// 登录过期
return RequestContext::save('auth', false);
}
if (!UserDevice::check()) {
// token 不存在
return RequestContext::save('auth', false);
}
$user = self::whereUserid(Doo::userId())->whereEmail(Doo::userEmail())->whereEncrypt(Doo::userEncrypt())->first();
if (!$user) {
// 登录信息不匹配
return RequestContext::save('auth', false);
}
// 更新登录信息
$upArray = [];
if (Base::getIp() && $user->line_ip != Base::getIp()) {
$upArray['line_ip'] = Base::getIp();
}
if (Carbon::parse($user->line_at)->addSeconds(30)->lt(Carbon::now())) {
$upArray['line_at'] = Carbon::now();
}
$headerLanguage = RequestContext::get('header_language');
if (empty($user->lang) || $headerLanguage) {
if (Doo::checkLanguage($headerLanguage) && $user->lang != $headerLanguage) {
$upArray['lang'] = $headerLanguage;
}
}
if ($upArray) {
$user->updateInstance($upArray);
$user->save();
}
return RequestContext::save('auth', $user);
}
/**
@ -444,32 +830,48 @@ class User extends AbstractModel
} else {
$token = Doo::userToken();
}
UserDevice::record($token);
unset($userinfo->encrypt);
unset($userinfo->password);
return $userinfo->token = $token;
}
/**
* 生成无设备的 token主要用于接口调用 token 不检查设备是否存在)
* @param self $userinfo
* @param $ttl
* @return mixed
*/
public static function generateTokenNoDevice($userinfo, $ttl)
{
$key = 'user_token_no_device_' . $userinfo->userid;
return Cache::remember($key, $ttl, function () use ($userinfo, $ttl) {
$token = Doo::tokenEncode($userinfo->userid, $userinfo->email, $userinfo->encrypt);
Cache::put(UserDevice::ck(md5($token)), $userinfo->userid, $ttl);
return $token;
});
}
/**
* userid 获取 基础信息
* @param int $userid 会员ID
* @return self
*/
public static function userid2basic($userid)
public static function userid2basic($userid, $addField = [])
{
global $_A;
if (empty($userid)) {
return null;
}
$userid = intval($userid);
if (isset($_A["__static_userid2basic_" . $userid])) {
return $_A["__static_userid2basic_" . $userid];
if (RequestContext::has("userid2basic_" . $userid)) {
return RequestContext::get("userid2basic_" . $userid);
}
$userInfo = self::whereUserid($userid)->select(User::$basicField)->first();
$userInfo = self::whereUserid($userid)->select(array_merge(User::$basicField, $addField))->first();
if ($userInfo) {
$userInfo->online = $userInfo->getOnlineStatus();
$userInfo->department_name = $userInfo->getDepartmentName();
}
return $_A["__static_userid2basic_" . $userid] = ($userInfo ?: []);
return RequestContext::save("userid2basic_" . $userid, $userInfo ?: []);
}
@ -507,6 +909,16 @@ class User extends AbstractModel
}
}
/**
* 临时帐号别名
* @return mixed|string
*/
public static function tempAccountAlias()
{
$alias = Base::settingFind('system', 'temp_account_alias');
return $alias ?: Doo::translate("临时帐号");
}
/**
* 获取头像
* @param $userid
@ -539,6 +951,16 @@ 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':
return url("images/avatar/default_bot.png");
case 'meeting-alert@bot.system':
@ -611,16 +1033,67 @@ class User extends AbstractModel
])->save();
}
//
$update['nickname'] = UserBot::systemBotName($email);
if (empty($update['nickname'])) {
$update['nickname'] = UserBot::systemBotName($email);
}
}
if ($update) {
$botUser->updateInstance($update);
if (isset($update['nickname'])) {
if (isset($update['nickname']) && $botUser->nickname != $update['nickname']) {
$botUser->az = Base::getFirstCharter($botUser->nickname);
$botUser->pinyin = Base::cn2pinyin($botUser->nickname);
}
$botUser->updateInstance($update);
$botUser->save();
}
return $botUser;
}
/**
* 是否机器人
* @param $userid
* @return bool
*/
public static function isBot($userid)
{
if (empty($userid)) {
return false;
}
// 这个不会有变化,所以可以使用永久缓存
return (bool)Cache::rememberForever('is-bot-user-' . $userid, function () use ($userid) {
return (bool)User::find($userid)?->bot;
});
}
/**
* 按关键词搜索用户Scope
* 支持:邮箱(含@、用户ID纯数字、昵称/拼音/职业
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $keyword 搜索关键词
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeSearchByKeyword($query, string $keyword)
{
if (str_contains($keyword, "@")) {
// 包含 @ 按邮箱搜索
return $query->where("email", "like", "%{$keyword}%");
}
if (is_numeric($keyword)) {
// 纯数字匹配用户ID 或 昵称/拼音/职业
return $query->where(function ($q) use ($keyword) {
$q->where("userid", intval($keyword))
->orWhere("nickname", "like", "%{$keyword}%")
->orWhere("pinyin", "like", "%{$keyword}%")
->orWhere("profession", "like", "%{$keyword}%");
});
}
// 普通文本:搜索昵称/拼音/职业
return $query->where(function ($q) use ($keyword) {
$q->where("nickname", "like", "%{$keyword}%")
->orWhere("pinyin", "like", "%{$keyword}%")
->orWhere("profession", "like", "%{$keyword}%");
});
}
}

102
app/Models/UserAppSort.php Normal file
View File

@ -0,0 +1,102 @@
<?php
namespace App\Models;
/**
* App\Models\UserAppSort
*
* @property int $id
* @property int $userid 用户ID
* @property array|null $sorts 排序配置
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @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|UserAppSort newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort whereSorts($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort whereUserid($value)
* @mixin \Eloquent
*/
class UserAppSort extends AbstractModel
{
protected $fillable = [
'userid',
'sorts',
];
protected $casts = [
'sorts' => 'array',
];
/**
* 获取用户排序配置
* @param int $userid
* @return array
*/
public static function getSorts(int $userid): array
{
$record = static::whereUserid($userid)->first();
if (!$record) {
return self::normalizeSorts([]);
}
return self::normalizeSorts($record->sorts);
}
/**
* 保存排序配置
* @param int $userid
* @param array $sorts
* @return static
*/
public static function saveSorts(int $userid, array $sorts): self
{
return static::updateOrCreate(
['userid' => $userid],
['sorts' => self::normalizeSorts($sorts)]
);
}
/**
* 规范化排序数据
* @param mixed $sorts
* @return array
*/
public static function normalizeSorts($sorts): array
{
$result = [
'base' => [],
'admin' => [],
];
if (!is_array($sorts)) {
return $result;
}
foreach (['base', 'admin'] as $group) {
$list = $sorts[$group] ?? [];
if (!is_array($list)) {
$list = [];
}
$normalized = [];
foreach ($list as $value) {
if (!is_string($value)) {
continue;
}
$value = trim($value);
if ($value === '') {
continue;
}
$normalized[] = $value;
}
$result[$group] = array_values(array_unique($normalized));
}
return $result;
}
}

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