Compare commits

...

23 Commits
v1.8.69 ... pro

Author SHA1 Message Date
kuaifan
08da0b8976 fix: 修复项目目录和静态文件权限 2026-07-24 16:20:10 +08:00
kuaifan
b914a99b62 fix(dashboard): 更新仪表盘警告信息样式,使用粘性定位 2026-07-21 19:38:40 +08:00
kuaifan
4c9951c667 feat: 默认开启部门负责人视角 2026-07-21 09:06:36 +00:00
kuaifan
93c604a554 release: v1.8.89 2026-07-20 14:21:51 +00:00
kuaifan
e4e1cd9b82 fix(dashboard): 优化移动端个人视角布局 2026-07-20 14:08:12 +00:00
kuaifan
6f22fd2706 fix(ai-assistant): respect model visibility settings 2026-07-20 10:48:33 +00:00
kuaifan
44bd541d00 fix: 仪表盘待完成默认展开 2026-07-20 09:13:32 +00:00
kuaifan
95b20325be fix(i18n): normalize common action translations 2026-07-20 09:06:37 +00:00
kuaifan
df4658cddf feat(i18n): support contextual translation keys 2026-07-20 07:25:10 +00:00
kuaifan
df6c1fc04d refactor(i18n): 完成前端国际化改造 2026-07-20 02:35:07 +00:00
kuaifan
c9b3515aef refactor(manticore): 简化向量批量分块收尾 2026-07-19 07:01:19 +00:00
kuaifan
48c2c16111 docs(ai-kb): 同步仪表盘与负责人视角 2026-07-19 00:21:42 +00:00
kuaifan
71ca6ba249 feat(dashboard): 完善负责人视角界面与范围切换 2026-07-18 23:43:10 +00:00
kuaifan
7ad27e3326 feat(dashboard): 增加负责人视角真实数据接口 2026-07-18 23:42:58 +00:00
kuaifan
4b92ecb422 feat(dashboard): 完善个人视角空状态 2026-07-18 04:47:12 +00:00
kuaifan
6666605f7d feat: 完善管理后台团队仪表盘 2026-07-18 01:13:43 +00:00
kuaifan
844db09c12 fix(dialog): 撤回占位预存防闪动,重新编辑后占位保留至过期
- 确认撤回时先预存占位记录(原消息在列表时被去重压制不显示),消息移除的同一次重算中占位原地顶上,消除 WS 推送与接口响应竞争导致的先删后加闪动
- 撤回失败且消息仍在列表时才清除预存记录,兼容"接口报错但服务端实际已撤回"的情况
- 点"重新编辑"不再删除占位,保留至 5 分钟过期,期间可反复回填(对齐微信行为),同步更新 ai-kb 文档
2026-07-16 22:11:04 +00:00
kuaifan
f7bc724e13 feat(dialog): 撤回文本消息后本地显示"重新编辑"占位
- 撤回自己的文本/MD 消息后,原位置显示仅本人可见的"你撤回了一条消息 重新编辑"占位(仿微信)
- 占位纯本地存储(IndexedDB 持久化,Electron 多窗口 syncDispatch 同步),5 分钟过期自动清除
- 点"重新编辑"回填原内容到输入框(富文本还原 @提及/表情,先退出引用/编辑模式),占位随即移除
- 占位沿用原消息 id 排序定位,仅在已加载消息范围内合并显示,不干扰翻页与已读上报
- 同步更新 ai-kb recall 文档与前端语言文件
2026-07-16 11:20:37 +00:00
kuaifan
966c7b4ef0 feat(microapp): iframe 同源 HTTP 5xx 检测与内联错误层,错误态强制显示胶囊
- iframe.vue 读 responseStatus 检测同源 5xx(宁可漏判不误判),与原生 error 统一走 load-error
- index.vue 新增可读的内联错误层(重试/关闭),覆盖深色下被反色成一条线的错误页
- 检测到错误时强制显示胶囊,保证关闭/更多入口
2026-07-16 06:26:39 +00:00
kuaifan
f8eaab5a50 fix(search): 评审修复——重建自愈加探针表与锁内复检,向量模型变更自动重建
- 重建前用探针表验证 ai 端点,探不通不动真表不写 marker(下次自愈重试)
- 锁内复检 marker,消除并发进程重复重建窗口
- 检测 embedding 模型变更(vector:model 对比查询侧缓存)自动触发重建
- Ihttp 返回体 code 非 0/200 时视为失败(401/500 不再被当成功)
- 查询向量缓存键掺入模型指纹,换模型后不再命中旧维度缓存
- 批量写入按行数+字节双预算分块、块内延迟渲染 SQL,降低峰值内存
- user 同步加脏检查,字段未变不重复触发向量重算
- 日志截断超长 SQL;文档与注释同步 KB_INGEST_TOKEN 退役、截断长度对齐
2026-07-15 14:17:01 +00:00
kuaifan
746337c2f6 feat(search): 语义搜索迁移至 Manticore Auto Embeddings,向量走 ai 插件免费模型
- embedding 统一改走 ai 插件 /embeddings(免费向量模型,1024 维),删除第三方 key 分支
- 5 张向量表启用引擎 Auto Embeddings(MODEL_NAME/FROM/API_URL),写入/更新时引擎自动生成向量
- 表定义使用派生密钥 sha256(APP_KEY:embeddings),避免主密钥进入引擎元数据
- 写入改为 REPLACE(单行原子)与多行批量 REPLACE(整语句一次向量化调用,失败回退逐行)
- 结构指纹 vector:schema 标记:版本/模型/端点/APP_KEY 变化时自动清空重建并重置同步指针
- 退役 PHP 向量管道:删除 generate-vectors 命令、双指针、withVector 分支与批量向量生成方法
- 根治缺陷:全文重同步不再抹掉向量;内容编辑后向量由引擎自动重算,不再过期
2026-07-15 10:45:20 +00:00
kuaifan
e4fbf9693a fix(http): Ihttp 响应头解析前初始化 headers 数组,消除未定义索引告警 2026-07-15 10:45:01 +00:00
kuaifan
13ba1d3bd7 feat(file): 我的文件纳入已共享文件,修复板块切换缓存丢失
- 「我的文件」改为展示我拥有的全部(含已共享出去的),与「共享文件」在“我共享的”上刻意重叠
- 缓存改为 stale-while-revalidate:按板块懒加载,切回该板块/目录先读缓存再拉接口静态刷新
- getFiles 清理过期行按 scope 限定范围,避免刷新一个板块误删另一板块(同为 pid=0)的缓存
- 微调 file-tabs 来源筛选字号与面包屑上边距
- 同步 ai-kb 文件入口说明(last_verified v1.8.69)
2026-07-14 12:16:22 +00:00
286 changed files with 10969 additions and 4546 deletions

View File

@ -1,64 +1,79 @@
--- ---
name: dootask-fix-permission name: dootask-fix-permission
description: 修复 DooTask 可写目录bootstrap/cache、docker、public、storage的属主/权限chown 回当前用户 + 目录 chmod 775对齐 install 的赋权逻辑,赋权不删数据。 description: 修复 DooTask 整个项目的目录和文件权限:根目录 chmod 755bootstrap/cache、docker、public、storage chown 回调用用户且目录 chmod 775public 文件补充所有用户读权限。用于 Nginx 静态文件 403/Permission denied、install/build EACCES 或可写目录检测失败;优先使用 sudo ./cmd permission,赋权不删数据。
--- ---
# DooTask 目录权限修复 # DooTask 目录权限修复
容器内进程常以 **root** 写入挂载目录(`storage``public/uploads``bootstrap/cache` 等),导致宿主机当前用户对这些文件**没有写权限**,进而触发 项目根目录如果缺少 `x` 穿越权限Nginx 即使看到 `public` 中的文件也无法访问。容器内进程常以 **root** 写入挂载目录(`storage``public/uploads``bootstrap/cache` 等),导致宿主机当前用户没有写权限。常见现象
- Nginx 日志出现 `stat() failed (13: Permission denied)`,静态文件请求被回退到 Laravel 首页
- `./cmd install` 报「目录【xxx】权限不足」/目录权限检测失败 - `./cmd install` 报「目录【xxx】权限不足」/目录权限检测失败
- `./cmd build`vite`EACCES: permission denied, copyfile`(复制 `public/uploads/...` 时) - `./cmd build`vite`EACCES: permission denied, copyfile`
- Laravel 运行时写 `storage`/`bootstrap/cache` 失败 - Laravel 运行时写 `storage`/`bootstrap/cache` 失败
本技能**对齐 `./cmd install` 的目录赋权逻辑**:对四个可写目录做 `chmod 775`(目录)+ `chown` 回当前用户 对齐 `./cmd permission`/`./cmd install` 的赋权逻辑:项目根目录设为 `755`;四个可写目录做 `chmod 775`(仅目录)+ `chown` 回调用 sudo 的用户;`public` 普通文件用 `a+r` 补充 Nginx 所需读权限
## 适用目录 ## 适用目录
与 install 一致的四个: ```text
. # 项目根目录,只修本层为 755
```
bootstrap/cache bootstrap/cache
docker docker
public # 含 public/uploads真实上传数据 public # 目录 775普通文件 a+r含真实上传数据
storage storage
``` ```
## 核心原则:赋权,不删数据 ## 核心原则:赋权,不删数据
`public/uploads` 含真实上传文件(头像、附件等)**永远优先 `chown` 改属主,不要删数据。** 即便用户说"清理一下",也只允许清临时目录 `public/uploads/tmp`**切勿**删 uploads 下其他内容。 `public/uploads` 含真实上传文件。永远优先 `chown` 改属主,不要删数据。即便用户说“清理一下”,也只允许清理临时目录 `public/uploads/tmp`,切勿删除 uploads 下其他内容。
## 前置检查 ## 前置检查
1. **工作目录**:在项目根(存在 `cmd` 且这四个目录在) 1. 在项目根目录执行,确认存在 `cmd` 和上述四个目录。
2. **sudo**:改属主需 root当前文件多为 root 属主)。本机一般可免密 sudo不行则经 docker 以 root 改权限 2. 用 `ls -ld .` 检查项目根目录是否缺少组/其他用户的 `x` 穿越权限。
3. 确认要修的范围:默认四个目录全修;若用户只想解 build 报错,也可只针对 `public`(含 `public/uploads` 3. 确认可使用 sudo改 root 属主的文件或目录需要 root 权限。
4. 用 `find public -type f ! -perm -004 -print` 检查 Nginx 用户可能无法读取的静态文件。
5. 默认修复项目根目录、四个可写目录和 `public` 文件;若用户只想解决 build 的 uploads 报错,可只处理 `public/uploads`
检查通过后汇报将执行的命令,**向用户确认一次**再执行。 检查通过后汇报将执行的命令,向用户确认一次再执行。
## 执行 ## 执行
确认后执行(属主修回当前用户,目录权限 775 确认后优先执行独立权限修复命令(不依赖 Docker 正在运行
```shell ```shell
# 1) 属主修回当前用户(递归) sudo ./cmd permission
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` 旧版 `cmd` 没有 `permission` 命令时,手动执行:
> ```shell
> sudo chown -R "$(id -u):$(id -g)" public/uploads
> ```
执行后报告:改了哪些目录、属主/权限现状(可 `ls -ld` 抽查),并提示用户可重试之前失败的 install/build/update。 ```shell
# 1) 保证 Nginx 可穿越项目根目录(不递归)
sudo chmod 755 .
# 2) 可写目录属主修回当前用户(递归)
sudo chown -R "$(id -u):$(id -g)" bootstrap/cache docker public storage
# 3) 可写目录权限 775仅目录
find bootstrap/cache docker public storage -type d -exec chmod 775 {} \;
# 4) public 普通文件只补充读权限,保留现有写入/执行位
find public -type f -exec chmod a+r {} \;
```
只想解决 build 的 uploads 报错时,可只执行:
```shell
sudo chown -R "$(id -u):$(id -g)" public/uploads
```
执行后用 `ls -ld . bootstrap/cache docker public storage` 抽查目录,并用 `find public -type f ! -perm -004 -print` 确认不再有缺少 others 读权限的静态文件。然后重试之前失败的静态文件访问、install/build/update。
## 失败处理 ## 失败处理
- `chown` 报权限不足 → 当前用户无 sudo 权限,提示用户用有 root 权限的账户,或经 docker 以 root 执行;不要静默跳过 - `chmod`/`chown` 报权限不足:立即停止,提示使用有 root 权限的账户,或经 docker 以 root 执行;不要静默跳过
- 任何步骤失败立即停止报告,不自动重试 - 任何步骤失败立即停止报告,不自动重试
## 禁止项 ## 禁止项
@ -66,11 +81,15 @@ find bootstrap/cache docker public storage -type d -exec chmod 775 {} \;
|---------|---------| |---------|---------|
| build 报 uploads EACCES 就 `rm` 删文件 | `chown` 修属主,保留数据 | | build 报 uploads EACCES 就 `rm` 删文件 | `chown` 修属主,保留数据 |
| 删整个 `public/uploads` 清场 | 最多清 `public/uploads/tmp`,别碰真实上传数据 | | 删整个 `public/uploads` 清场 | 最多清 `public/uploads/tmp`,别碰真实上传数据 |
| 对文件无差别 `chmod 777` | 目录 `chmod 775` + `chown` 回当前用户即可 | | 对文件无差别 `chmod 777` | 可写目录 `chmod 775` + `chown` 回当前用户 |
| 把 `public` 所有文件强制改为 `644` | 用 `chmod a+r` 只补读权限,保留现有权限位 |
| 递归 `chmod 755` 整个项目 | 只对项目根目录执行 `chmod 755 .` |
| 不加 sudo 直接 chown root 文件 | 改属主需 root | | 不加 sudo 直接 chown root 文件 | 改属主需 root |
## Red Flags —— 出现这些念头立即停下 ## Red Flags
- "uploads 复制失败,删掉再 build" → 不,`chown` 赋权,不丢数据 - “uploads 复制失败,删掉再 build” → 不,`chown` 赋权,不丢数据。
- "777 一把梭最省事" → 不,按 install 的 775目录+ chown - “777 一把梭最省事” → 不,按 install 的 775目录+ chown。
- "权限不够就跳过这个目录" → 不,报告交用户处理 sudo - “根目录不可穿越,递归 chmod 全仓库” → 不,只修项目根目录为 755。
- “静态文件 403给整个项目所有文件加读权限” → 不,只对 `public` 普通文件执行 `a+r`
- “权限不够就跳过这个目录” → 不,报告交用户处理 sudo。

View File

@ -44,6 +44,8 @@ description: 从 `pro` 分支发布 DooTask 前端新版本:翻译 → 版本
多语言数据流:`language/original-{web,api}.txt`(原文/简体中文)→ 经翻译写入 `language/translate.json`(含 9 种语言)→ 生成 `public/language/{web,api}/*` 多语言数据流:`language/original-{web,api}.txt`(原文/简体中文)→ 经翻译写入 `language/translate.json`(含 9 种语言)→ 生成 `public/language/{web,api}/*`
上下文翻译键用于保持短文案的界面宽度,同时区分不同语义,格式固定为 `[lower_snake_case].原文`,例如 `[weekday].一``[task_unit].个`。上下文前缀只参与翻译查找,不参与显示;运行时完整键缺失时会回退普通原文键。普通 key 的 `zh` 留空,上下文 key 的 `zh` 必须填写去掉前缀后的原文。
**1.1 检测差异** **1.1 检测差异**
```shell ```shell
@ -51,14 +53,15 @@ php .claude/skills/dootask-release/scripts/language.php diff
``` ```
输出 JSON 输出 JSON
- `formatErrorCount > 0`translate.json **已有条目**含 raw `(*)`/`(**)` key、字段结构错误、非法/不连续的参数编号或规范化重复 → **停止**,报告 `formatErrors`,交用户修复
- `regexErrorCount > 0`translate.json **已有条目**的占位符与某语言值不一致 → **停止**,报告 `regexErrors`,交用户修复(这是历史数据问题,不要自行猜测修改) - `regexErrorCount > 0`translate.json **已有条目**的占位符与某语言值不一致 → **停止**,报告 `regexErrors`,交用户修复(这是历史数据问题,不要自行猜测修改)
- `redundantCount > 0`translate.json 里有、但原文已删除的条目 → 仅作提示apply 时会自动剔除,不致命) - `redundantCount > 0`translate.json 里有、但原文已删除的条目 → 仅作提示apply 时会自动剔除,不致命)
- `needsCount == 0`:无新文案 → **跳到 1.4 直接生成** - `needsCount == 0`:无新文案 → **跳到 1.4 直接生成**
- `needsCount > 0``needs` 数组即待翻译清单,每项 `key` 已转成占位符形式(如 `(%T1)`)→ 进入 1.2 - `needsCount > 0``needs` 数组即待翻译清单,每项 `key` 已转成占位符形式(如 `(%T1)`;上下文 key 还会带自动填充的 `zh` → 进入 1.2
**1.2 翻译** **1.2 翻译**
`needs` 里的每个 `key`,翻成 8 种语言(`zh` 留空、`key` 原样保留):`zh-CHT` `en` `ko` `ja` `de` `fr` `id` `ru` `needs` 里的每个 `key`,翻成 8 种语言(`key` 原样保留):`zh-CHT` `en` `ko` `ja` `de` `fr` `id` `ru`普通 key 的 `zh` 留空;上下文 key 的 `zh` 使用 `needs` 已给出的原文,不得留空或改写。
要求:贴合「项目任务管理系统」语境;占位符 `(%T1)`/`(%M1)` 等原样保留、不可增删改,位置可随目标语言语序调整: 要求:贴合「项目任务管理系统」语境;占位符 `(%T1)`/`(%M1)` 等原样保留、不可增删改,位置可随目标语言语序调整:
@ -68,7 +71,7 @@ php .claude/skills/dootask-release/scripts/language.php diff
| (%T1)提交的「(%M2)」待你审批 | '(%M2)' submitted by (%T1) is waiting for your approval | | (%T1)提交的「(%M2)」待你审批 | '(%M2)' submitted by (%T1) is waiting for your approval |
把结果写成一个 JSON 数组文件(建议放 `/tmp/dootask-release-translated.json`,避免污染工作区),每个元素含全部 10 个字段,顺序为: 把结果写成一个 JSON 数组文件(建议放 `/tmp/dootask-release-translated.json`,避免污染工作区),每个元素含全部 10 个字段,顺序为:
`key, zh, zh-CHT, en, ko, ja, de, fr, id, ru``zh``""` `key, zh, zh-CHT, en, ko, ja, de, fr, id, ru`
```json ```json
[ [
@ -76,13 +79,21 @@ php .claude/skills/dootask-release/scripts/language.php diff
] ]
``` ```
上下文短词示例:
```json
[
{"key":"[task_unit].个","zh":"个","zh-CHT":"個","en":"tasks","ko":"개 작업","ja":"タスク","de":"Aufgaben","fr":"tâches","id":"tugas","ru":"задач"}
]
```
**1.3 合并进 translate.json** **1.3 合并进 translate.json**
```shell ```shell
php .claude/skills/dootask-release/scripts/language.php apply /tmp/dootask-release-translated.json php .claude/skills/dootask-release/scripts/language.php apply /tmp/dootask-release-translated.json
``` ```
脚本会校验字段完整性与占位符完整性、追加新条目、剔除冗余项,并按项目原生格式写回 `translate.json`。任一条不合格会报错停止,按提示修正翻译后重试。 脚本会校验字段完整性、占位符完整性与上下文键格式,追加新条目、剔除冗余项,并按项目原生格式写回 `translate.json`参数化 key 必须使用 `(%T1)`/`(%M1)` 形式,禁止输入 raw `(*)`/`(**)`;编号须按出现顺序从 1 连续递增,各语言值可调整占位符顺序,但占位符的类型、编号和数量必须与 key 完全一致。上下文 key 必须使用 `[lower_snake_case].原文`,且 `zh` 必须与原文部分完全一致。输入 JSON 内相同或规范化后相同的 key 会被拒绝;已存在于 `translate.json` 的非待补项也会被拒绝,避免覆盖歧义。任一条不合格会明确报错停止,按提示修正翻译后重试。
**1.4 生成前端/后端语言文件** **1.4 生成前端/后端语言文件**
@ -90,7 +101,7 @@ php .claude/skills/dootask-release/scripts/language.php apply /tmp/dootask-relea
php .claude/skills/dootask-release/scripts/language.php generate php .claude/skills/dootask-release/scripts/language.php generate
``` ```
`translate.json` 字节级重新生成 `public/language/web/*.js``public/language/api/*.json`(排序/转义与项目原生工具完全一致,正常情况下 diff 只包含本次新增条目)。 `translate.json` 字节级重新生成 `public/language/web/*.js``public/language/api/*.json`(排序/转义与项目原生工具完全一致,正常情况下 diff 只包含本次新增条目)。生成前会再次执行格式与占位符校验;存在 raw `(*)`/`(**)` key 或占位符错误时不会写入生成文件。
**1.5 报告**:用 `git status --short language public/language` 汇总本步改动,向用户报告新增了多少条翻译。 **1.5 报告**:用 `git status --short language public/language` 汇总本步改动,向用户报告新增了多少条翻译。

View File

@ -6,7 +6,8 @@
// //
// 子命令: // 子命令:
// language.php diff // language.php diff
// —— 输出 JSONneeds(待翻译key 已转成 (%T1)/(%M1) 形式) / redundants(冗余,提示) / regexErrors(占位符错乱,致命) // —— 输出 JSONneeds(待翻译key 已转成 (%T1)/(%M1) 形式) / redundants(冗余,提示)
// / formatErrors(raw 占位符、字段或编号错误,致命) / regexErrors(各语言占位符错乱,致命)
// language.php apply <translated.json> // language.php apply <translated.json>
// —— 把新翻译合并进 translate.json追加 + 剔除冗余),不生成 public 文件 // —— 把新翻译合并进 translate.json追加 + 剔除冗余),不生成 public 文件
// language.php generate // language.php generate
@ -41,43 +42,169 @@ function read_generateds(): array
return [$originals, $generateds]; return [$originals, $generateds];
} }
// ---- 公共:构建 translations 映射normalizedKey -> obj并收集冗余/占位符错乱 ---- // ---- 公共:占位符与条目结构校验 ----
function parameter_tokens(string $value): array
{
preg_match_all('/\(%[TM]\d+\)/', $value, $matches);
return $matches[0];
}
function normalize_key(string $key): string
{
return preg_replace(["/\(%T\d+\)/", "/\(%M\d+\)/"], ["(*)", "(**)"], $key);
}
function context_source_key(string $key): ?string
{
if (preg_match('/^\[([a-z][a-z0-9_]*)]\.([\s\S]+)$/', $key, $matches)) {
return $matches[2];
}
return null;
}
function validate_original_contexts(array $originals): array
{
$errors = [];
foreach ($originals as $index => $key) {
if (preg_match('/^\[[^\]]+]\./', $key) && context_source_key($key) === null) {
$errors[] = [
'location' => "originals[index $index]",
'message' => "上下文翻译键格式错误:$key",
];
}
}
return $errors;
}
function validate_entry(array $obj, string $location, bool $requireTranslations): array
{
$formatErrors = [];
$regexErrors = [];
foreach ($GLOBALS['LANG_FIELDS'] as $field) {
if (!array_key_exists($field, $obj)) {
$formatErrors[] = ['location' => "$location.$field", 'message' => "缺少字段 $field"];
} elseif (!is_string($obj[$field])) {
$formatErrors[] = ['location' => "$location.$field", 'message' => "字段 $field 必须是字符串"];
} elseif ($requireTranslations && $field !== 'key' && $field !== 'zh' && $obj[$field] === '') {
$formatErrors[] = ['location' => "$location.$field", 'message' => "字段 $field 不得为空"];
}
}
if (!isset($obj['key']) || !is_string($obj['key'])) {
return [$formatErrors, $regexErrors];
}
$key = $obj['key'];
if (preg_match('/^\[[^\]]+]\./', $key)) {
$sourceKey = context_source_key($key);
if ($sourceKey === null) {
$formatErrors[] = ['location' => "$location.key", 'message' => "上下文翻译键格式错误:$key"];
} elseif (($obj['zh'] ?? null) !== $sourceKey) {
$formatErrors[] = [
'location' => "$location.zh",
'message' => "上下文翻译键必须填写原文:$sourceKey",
];
}
}
if (preg_match('/\(\*{1,2}\)/', $key)) {
$formatErrors[] = [
'location' => "$location.key",
'message' => "key 不得包含 raw (*)/(**),必须使用 (%T1)/(%M1)$key",
];
}
$withoutValidParameters = preg_replace('/\(%[TM]\d+\)/', '', $key);
if (str_contains($withoutValidParameters, '(%')) {
$formatErrors[] = ['location' => "$location.key", 'message' => "存在非法参数占位符:$key"];
}
$keyTokens = parameter_tokens($key);
foreach ($keyTokens as $index => $token) {
preg_match('/\d+/', $token, $number);
if ((int)$number[0] !== $index + 1) {
$formatErrors[] = [
'location' => "$location.key",
'message' => "参数编号必须按出现顺序从 1 连续递增:$key",
];
break;
}
}
$expected = $keyTokens;
sort($expected);
foreach ($GLOBALS['LANG_FIELDS'] as $field) {
if ($field === 'key' || !isset($obj[$field]) || !is_string($obj[$field]) || $obj[$field] === '') {
continue;
}
$actual = parameter_tokens($obj[$field]);
sort($actual);
if ($actual !== $expected) {
$regexErrors[] = [
'location' => "$location.$field",
'key' => $key,
'field' => $field,
'value' => $obj[$field],
'expected' => $expected,
'actual' => $actual,
'message' => "参数占位符缺失、类型或编号不一致",
];
}
}
return [$formatErrors, $regexErrors];
}
function print_validation_errors(array $formatErrors, array $regexErrors): void
{
foreach ($formatErrors as $error) {
fwrite(STDERR, "格式错误 {$error['location']}{$error['message']}\n");
}
foreach ($regexErrors as $error) {
fwrite(STDERR, "占位符错误 {$error['location']}{$error['message']}key={$error['key']}\n");
}
}
// ---- 公共:构建 translations 映射normalizedKey -> obj并收集冗余/格式/占位符错乱 ----
function build_translations(array $originals): array function build_translations(array $originals): array
{ {
$translations = []; $translations = [];
$redundants = []; $redundants = [];
$regrror = []; $regexErrors = [];
$formatErrors = validate_original_contexts($originals);
if (!file_exists("translate.json")) { if (!file_exists("translate.json")) {
fwrite(STDERR, "translate.json not exists\n"); fwrite(STDERR, "translate.json not exists\n");
exit(1); exit(1);
} }
$tmps = json_decode(file_get_contents("translate.json"), true); $tmps = json_decode(file_get_contents("translate.json"), true);
foreach ($tmps as $obj) { if (!is_array($tmps)) {
if (!isset($obj['key'])) { $formatErrors[] = ['location' => 'translate.json', 'message' => '根数据必须是 JSON 数组'];
return [$translations, $redundants, $regexErrors, $formatErrors];
}
foreach ($tmps as $index => $obj) {
$location = "translate.json[index $index]";
if (!is_array($obj)) {
$formatErrors[] = ['location' => $location, 'message' => '条目必须是对象'];
continue;
}
[$entryFormatErrors, $entryRegexErrors] = validate_entry($obj, $location, false);
$formatErrors = array_merge($formatErrors, $entryFormatErrors);
$regexErrors = array_merge($regexErrors, $entryRegexErrors);
if (!isset($obj['key']) || !is_string($obj['key'])) {
continue; continue;
} }
$currentKey = $obj['key']; $currentKey = $obj['key'];
$originalKey = preg_replace(["/\(%T\d+\)/", "/\(%M\d+\)/"], ["(*)", "(**)"], $currentKey); $originalKey = normalize_key($currentKey);
if (!in_array($originalKey, $originals)) { if (!in_array($originalKey, $originals)) {
$redundants[$originalKey] = $obj; $redundants[$originalKey] = $obj;
continue; continue;
} }
$translations[$originalKey] = $obj; if (isset($translations[$originalKey])) {
if (preg_match_all('/\(%[TM]\d+\)/', $currentKey, $matches)) { $formatErrors[] = [
foreach ($matches[0] as $match) { 'location' => $location,
foreach ($obj as $k => $v) { 'message' => "规范化后 key 重复:$originalKey",
if (empty($v)) { ];
continue; continue;
} }
if (!str_contains($v, $match)) { $translations[$originalKey] = $obj;
$regrror[$originalKey] = ['key' => $currentKey, 'field' => $k, 'value' => $v, 'match' => $match];
continue 2;
} }
} return [$translations, $redundants, $regexErrors, $formatErrors];
}
}
}
return [$translations, $redundants, $regrror];
} }
// ---- 公共:由 translate.json + originals 重新生成 public 文件 ---- // ---- 公共:由 translate.json + originals 重新生成 public 文件 ----
@ -127,7 +254,7 @@ function generate(array $generateds, array $translations): void
if ($cmd === 'diff') { if ($cmd === 'diff') {
[$originals, $generateds] = read_generateds(); [$originals, $generateds] = read_generateds();
[$translations, $redundants, $regrror] = build_translations($originals); [$translations, $redundants, $regexErrors, $formatErrors] = build_translations($originals);
// 需要翻译的数据(对齐 translate.php 150-169占位符按单一计数器编号 // 需要翻译的数据(对齐 translate.php 150-169占位符按单一计数器编号
$needs = []; $needs = [];
@ -147,20 +274,27 @@ if ($cmd === 'diff') {
$label = strlen($m[1]) > 1 ? "M" : "T"; $label = strlen($m[1]) > 1 ? "M" : "T";
return "(%" . $label . $c++ . ")"; return "(%" . $label . $c++ . ")";
}, $key); }, $key);
$needsOut[] = ['key' => $converted]; $need = ['key' => $converted];
$contextSource = context_source_key($converted);
if ($contextSource !== null) {
$need['zh'] = $contextSource;
}
$needsOut[] = $need;
} }
echo json_encode([ echo json_encode([
'needsCount' => count($needsOut), 'needsCount' => count($needsOut),
'redundantCount' => count($redundants), 'redundantCount' => count($redundants),
'regexErrorCount' => count($regrror), 'formatErrorCount' => count($formatErrors),
'regexErrorCount' => count($regexErrors),
'needs' => $needsOut, 'needs' => $needsOut,
'redundants' => array_keys($redundants), 'redundants' => array_keys($redundants),
'regexErrors' => array_values($regrror), 'formatErrors' => array_values($formatErrors),
'regexErrors' => array_values($regexErrors),
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n"; ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
if (count($regrror) > 0) { if (count($formatErrors) > 0 || count($regexErrors) > 0) {
exit(2); // 已有数据占位符错乱,需先修复 exit(2); // 已有数据格式或占位符错乱,需先修复
} }
exit(0); exit(0);
} }
@ -172,9 +306,10 @@ if ($cmd === 'apply') {
exit(1); exit(1);
} }
[$originals, $generateds] = read_generateds(); [$originals, $generateds] = read_generateds();
[$translations, $redundants, $regrror] = build_translations($originals); [$translations, $redundants, $regexErrors, $formatErrors] = build_translations($originals);
if (count($regrror) > 0) { if (count($formatErrors) > 0 || count($regexErrors) > 0) {
fwrite(STDERR, "translate.json 已有条目占位符错乱,请先修复再发版。\n"); print_validation_errors($formatErrors, $regexErrors);
fwrite(STDERR, "translate.json 已有条目格式或占位符错误,请先修复再发版。\n");
exit(2); exit(2);
} }
@ -183,45 +318,49 @@ if ($cmd === 'apply') {
fwrite(STDERR, "translated.json 必须是数组\n"); fwrite(STDERR, "translated.json 必须是数组\n");
exit(1); exit(1);
} }
$added = 0; $originalSet = array_flip($originals);
foreach ($incoming as $raw) { $incomingSeen = [];
foreach ($GLOBALS['LANG_FIELDS'] as $f) { $itemsToAdd = [];
if (!array_key_exists($f, $raw)) { foreach ($incoming as $index => $raw) {
fwrite(STDERR, "新翻译缺字段 \"$f\"" . json_encode($raw, JSON_UNESCAPED_UNICODE) . "\n"); if (!is_array($raw)) {
fwrite(STDERR, "新翻译 translated.json[index $index] 必须是对象\n");
exit(1); exit(1);
} }
} [$incomingFormatErrors, $incomingRegexErrors] = validate_entry($raw, "translated.json[index $index]", true);
// 占位符完整性key 里每个 (%T1)/(%M1) 必须出现在每个非空语言值里 if (count($incomingFormatErrors) > 0 || count($incomingRegexErrors) > 0) {
if (preg_match_all('/\(%[TM]\d+\)/', $raw['key'], $m)) { print_validation_errors($incomingFormatErrors, $incomingRegexErrors);
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); exit(1);
} }
} // 规范化:固定字段顺序;上下文翻译键保留 zh 作为实际显示原文
}
}
// 规范化:固定字段顺序 + zh 置空
$item = []; $item = [];
foreach ($GLOBALS['LANG_FIELDS'] as $f) { foreach ($GLOBALS['LANG_FIELDS'] as $f) {
$item[$f] = $f === 'zh' ? '' : $raw[$f]; $item[$f] = $f === 'zh' && context_source_key($raw['key']) === null ? '' : $raw[$f];
} }
$originalKey = preg_replace(["/\(%T\d+\)/", "/\(%M\d+\)/"], ["(*)", "(**)"], $item['key']); $originalKey = normalize_key($item['key']);
if (!isset($originalSet[$originalKey])) {
fwrite(STDERR, "新翻译 key 不在 original-web.txt/original-api.txt{$item['key']}\n");
exit(1);
}
if (isset($incomingSeen[$originalKey])) {
fwrite(STDERR, "新翻译输入内部重复translated.json[index {$incomingSeen[$originalKey]}] 与 translated.json[index $index] 规范化后均为「$originalKey」\n");
exit(1);
}
if (isset($translations[$originalKey])) {
fwrite(STDERR, "新翻译 key 已存在于 translate.json非待补项或存在覆盖歧义{$item['key']}(规范化:$originalKey\n");
exit(1);
}
$incomingSeen[$originalKey] = $index;
$itemsToAdd[$originalKey] = $item;
}
foreach ($itemsToAdd as $originalKey => $item) {
$translations[$originalKey] = $item; $translations[$originalKey] = $item;
$added++;
} }
// array_values现有条目去冗余在前新条目追加在后 // array_values现有条目去冗余在前新条目追加在后
file_put_contents("translate.json", json_encode(array_values($translations), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); file_put_contents("translate.json", json_encode(array_values($translations), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
echo json_encode([ echo json_encode([
'added' => $added, 'added' => count($itemsToAdd),
'total' => count($translations), 'total' => count($translations),
'droppedRedundant' => count($redundants), 'droppedRedundant' => count($redundants),
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n"; ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
@ -230,7 +369,12 @@ if ($cmd === 'apply') {
if ($cmd === 'generate') { if ($cmd === 'generate') {
[$originals, $generateds] = read_generateds(); [$originals, $generateds] = read_generateds();
[$translations] = build_translations($originals); [$translations, $redundants, $regexErrors, $formatErrors] = build_translations($originals);
if (count($formatErrors) > 0 || count($regexErrors) > 0) {
print_validation_errors($formatErrors, $regexErrors);
fwrite(STDERR, "translate.json 存在格式或占位符错误,已停止生成。\n");
exit(2);
}
generate($generateds, $translations); generate($generateds, $translations);
exit(0); exit(0);
} }

View File

@ -36,10 +36,8 @@ jobs:
- name: ESLint - name: ESLint
run: npm run lint run: npm run lint
# 存量缺失文案 93 条(见 scripts/check-language.mjs 输出),清零后移除 continue-on-error 改为强制
- name: Language Check - name: Language Check
run: npm run check:lang run: npm run check:lang
continue-on-error: true
phpunit: phpunit:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -2,6 +2,26 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [1.8.89]
### Features
- 管理后台仪表盘新增负责人视角,可按部门查看团队任务;个人视角的待办、逾期、今日和即将到期任务展示也更清晰。
- 语义搜索升级为更智能的自动向量检索,搜索相关内容更准确,内容更新后会自动保持索引同步。
- 撤回聊天文字或 Markdown 消息后支持「重新编辑」,可在有效期内恢复原内容继续修改。
- 微应用加载失败时会显示清晰的错误提示,并提供重试和关闭入口。
- 「我的文件」现在包含已共享文件,切换板块时优先展示缓存内容,查找文件更顺畅。
### Bug Fixes
- 修复 AI 助手模型隐藏设置未完全生效的问题,正在使用的模型被隐藏时会自动切换到可用模型。
- 修复仪表盘待完成分组默认展开状态不一致的问题。
- 修复撤回消息、文件切换及部分网络异常场景下的显示和状态同步问题。
### Performance
- 优化仪表盘移动端布局与任务分组样式,减少页面拥挤,浏览和操作更顺手。
## [1.8.69] ## [1.8.69]
### Features ### Features

View File

@ -68,6 +68,7 @@ Laravel 13 (LaravelS/Swoole, PHP 8.4) + Vue 2 (Vite) + Electron。开源任务/
- 新增用户可见文本须追加原文(简体中文)到:前端 `language/original-web.txt`,后端 `language/original-api.txt`(去重) - 新增用户可见文本须追加原文(简体中文)到:前端 `language/original-web.txt`,后端 `language/original-api.txt`(去重)
- 前端翻译用 `$L("文本")`,动态值用 `(*)` 占位:`$L('共(*)条', n)`——禁止拼接翻译 - 前端翻译用 `$L("文本")`,动态值用 `(*)` 占位:`$L('共(*)条', n)`——禁止拼接翻译
- 单字或短词因上下文不同可能产生歧义时,使用 `[lower_snake_case].原文` 上下文键,例如 `$L('[weekday].一')``$L('[task_unit].个')`;前缀只参与翻译查找,界面仍显示原文。此类键须完整登记到 `original-web.txt`,且 `translate.json``zh` 必须填写去除前缀后的原文;普通键的 `zh` 仍留空
## ai-kb 同步规则 ## ai-kb 同步规则

View File

@ -1,205 +0,0 @@
<?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,86 @@
<?php
namespace App\Http\Controllers\Api;
use App\Models\User;
use App\Module\Base;
use App\Module\DashboardTeam;
use Request;
/**
* @apiDefine dashboard
*
* 仪表盘
*/
class DashboardController extends AbstractController
{
/**
* @api {get} api/dashboard/team/stats 负责人视角统计
*
* @apiDescription 需要token身份。返回所选管理部门含下级部门的团队任务统计。
* @apiVersion 1.0.0
* @apiGroup dashboard
* @apiName team__stats
*
* @apiParam {String} [department_owner_ids] 所选管理部门ID逗号分隔不传表示全部
* @apiParam {Number} [refresh] 1 时主动刷新当前统计缓存
*
* @apiSuccess {Number} ret 返回状态码
* @apiSuccess {String} msg 返回信息
* @apiSuccess {Object} data 团队统计数据
*/
public function team__stats()
{
$user = User::auth();
$context = DashboardTeam::context($user, Request::input('department_owner_ids'));
return Base::retSuccess('success', DashboardTeam::stats($context, intval(Request::input('refresh')) === 1));
}
/**
* @api {get} api/dashboard/team/tasks 负责人视角任务列表
*
* @apiDescription 需要token身份。按关注类型、成员或优先级分页返回团队任务。
* @apiVersion 1.0.0
* @apiGroup dashboard
* @apiName team__tasks
*
* @apiParam {String} [department_owner_ids] 所选管理部门ID逗号分隔不传表示全部
* @apiParam {String} [type] 任务类型uncompleted/overdue/soon/hi/noowner
* @apiParam {Number} [member_id] 成员ID传入后优先于 type
* @apiParam {Number} [level] 优先级;-1 表示未设置,传入后优先于 type
* @apiParam {Number} [page] 当前页
* @apiParam {Number} [pagesize] 每页数量默认20最大50
*
* @apiSuccess {Number} ret 返回状态码
* @apiSuccess {String} msg 返回信息
* @apiSuccess {Object} data 分页任务数据
*/
public function team__tasks()
{
$user = User::auth();
$context = DashboardTeam::context($user, Request::input('department_owner_ids'));
$memberId = intval(Request::input('member_id'));
$levelValue = Request::input('level');
$level = $levelValue !== null && $levelValue !== '' ? intval($levelValue) : null;
$type = trim((string)Request::input('type'));
if ($memberId > 0) {
if (!in_array($memberId, $context['member_userids'], true)) {
return Base::retError('参数错误');
}
} elseif ($level !== null) {
if ($level !== -1 && !in_array($level, DashboardTeam::priorityLevels(), true)) {
return Base::retError('参数错误');
}
} elseif (!in_array($type, ['uncompleted', 'overdue', 'soon', 'hi', 'noowner'], true)) {
return Base::retError('参数错误');
}
return Base::retSuccess('success', DashboardTeam::tasks($context, [
'type' => $type,
'member_id' => $memberId,
'level' => $level,
]));
}
}

View File

@ -154,7 +154,7 @@ class SystemController extends AbstractController
$setting['unclaimed_task_reminder'] = $setting['unclaimed_task_reminder'] ?: 'close'; $setting['unclaimed_task_reminder'] = $setting['unclaimed_task_reminder'] ?: 'close';
$setting['unclaimed_task_reminder_time'] = $setting['unclaimed_task_reminder_time'] ?: ''; $setting['unclaimed_task_reminder_time'] = $setting['unclaimed_task_reminder_time'] ?: '';
$setting['task_ai_auto_analyze'] = $setting['task_ai_auto_analyze'] ?: 'open'; $setting['task_ai_auto_analyze'] = $setting['task_ai_auto_analyze'] ?: 'open';
$setting['department_owner_project_view'] = $setting['department_owner_project_view'] ?: 'close'; $setting['department_owner_project_view'] = $setting['department_owner_project_view'] ?: 'open';
$setting['server_timezone'] = config('app.timezone'); $setting['server_timezone'] = config('app.timezone');
$setting['server_version'] = Base::getVersion(); $setting['server_version'] = Base::getVersion();
// 指定人员名单仅管理员可见 // 指定人员名单仅管理员可见

View File

@ -418,7 +418,7 @@ class UsersController extends AbstractController
public function info__managed_departments() public function info__managed_departments()
{ {
$user = User::auth(); $user = User::auth();
if (Base::settingFind('system', 'department_owner_project_view', 'close') !== 'open') { if (Base::settingFind('system', 'department_owner_project_view', 'open') !== 'open') {
return Base::retSuccess('success', []); return Base::retSuccess('success', []);
} }
return Base::retSuccess('success', UserDepartment::getManagedDepartments($user->userid)); return Base::retSuccess('success', UserDepartment::getManagedDepartments($user->userid));

View File

@ -230,13 +230,10 @@ class File extends AbstractModel
} else { } else {
// 根目录:按板块拆分 // 根目录:按板块拆分
$array = []; $array = [];
// 我的文件mine 仅私有 share=0all 含全部我的) // 我的文件我拥有的全部含已共享出去的mine 与 all 一致
if ($scope === 'mine' || $scope === 'all') { if ($scope === 'mine' || $scope === 'all') {
$mine = File::wherePid(0) $mine = File::wherePid(0)
->whereUserid($user->userid) ->whereUserid($user->userid)
->when($scope === 'mine', function ($q) {
$q->where('share', 0);
})
->when($type == 'dir', function ($q) { ->when($type == 'dir', function ($q) {
$q->whereType('folder'); $q->whereType('folder');
}) })

View File

@ -90,6 +90,24 @@ class ManticoreSyncFailure extends AbstractModel
->delete(); ->delete();
} }
/**
* 批量清除同步成功记录(供批量写入路径使用,避免逐条删除)
*
* @param string $dataType 数据类型
* @param array $dataIds 数据ID列表
* @param string $action 操作类型
*/
public static function removeSuccessBatch(string $dataType, array $dataIds, string $action): void
{
if (empty($dataIds)) {
return;
}
self::where('data_type', $dataType)
->whereIn('data_id', $dataIds)
->where('action', $action)
->delete();
}
/** /**
* 获取待重试的记录 * 获取待重试的记录
* 根据重试次数决定间隔1次=1分钟2次=5分钟3次=15分钟4次+=30分钟 * 根据重试次数决定间隔1次=1分钟2次=5分钟3次=15分钟4次+=30分钟

View File

@ -561,7 +561,7 @@ class UserDepartment extends AbstractModel
'own_project_ids' => [], 'own_project_ids' => [],
'own_project_id_map' => [], 'own_project_id_map' => [],
]; ];
if ($ids === null || $ids === '' || Base::settingFind('system', 'department_owner_project_view', 'close') !== 'open') { if ($ids === null || $ids === '' || Base::settingFind('system', 'department_owner_project_view', 'open') !== 'open') {
return $empty; return $empty;
} }
$memberUserids = self::getManagedMemberUserids($user->userid, $ids); $memberUserids = self::getManagedMemberUserids($user->userid, $ids);
@ -653,7 +653,7 @@ class UserDepartment extends AbstractModel
return $result; return $result;
} }
// 部门负责人只读视角 // 部门负责人只读视角
if (Base::settingFind('system', 'department_owner_project_view', 'close') !== 'open') { if (Base::settingFind('system', 'department_owner_project_view', 'open') !== 'open') {
return $result; return $result;
} }
$memberUserids = self::getManagedMemberUserids($viewer->userid, 'all'); $memberUserids = self::getManagedMemberUserids($viewer->userid, 'all');

View File

@ -900,7 +900,84 @@ class AI
} }
/** /**
* 通过 OpenAI 兼容接口获取文本的 Embedding 向量 * 调用 ai 插件的 /embeddings 端点批量向量化(免费向量模型,零配置)。
*
* 走主程序 ai 插件的内网调用,共享主程序 APP_KEY 鉴权;
* 向量维度由 ai 插件的模型决定,主程序不再传 dimensions。
*
* @param array $texts 文本数组
* @return array retSuccess(data=向量数组的数组,与输入同序,缺失位置为 []) / retError
*/
protected static function requestPluginEmbeddings(array $texts)
{
$texts = array_values($texts);
$count = count($texts);
if ($count === 0) {
return Base::retSuccess("success", []);
}
$post = json_encode(["input" => $texts]);
$headers = [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . (string) config('app.key'),
];
$timeout = $count > 1 ? 120 : 30;
$res = Ihttp::ihttp_request(self::embeddingsUrl(), $post, $headers, $timeout);
if (Base::isError($res)) {
return Base::retError("Embedding 接口请求失败", $res);
}
$resData = Base::json2array($res['data']);
// 先识别端点的业务错误Ihttp 对 401/500 等带响应体的状态同样返回成功标志,
// 必须按响应里的 code 字段判错,否则鉴权错配等故障只会报"格式错误"难以定位
$resCode = intval($resData['code'] ?? 0);
if ($resCode !== 0 && $resCode !== 200) {
return Base::retError("Embedding 接口错误 [{$resCode}]: " . ($resData['error'] ?? 'unknown'), $resData);
}
if (empty($resData['data']) || !is_array($resData['data'])) {
return Base::retError("Embedding 接口返回数据格式错误", $resData);
}
// 记录当前实际生效的向量模型(查询缓存键与模型变化检测依赖它)
$model = (string) ($resData['model'] ?? '');
if ($model !== '' && Cache::get('ai:embedding_model') !== $model) {
Cache::forever('ai:embedding_model', $model);
}
// 按 index 回填,保证与输入顺序对齐,缺失位置留 []
$vectors = array_fill(0, $count, []);
foreach ($resData['data'] as $item) {
$idx = $item['index'] ?? null;
if ($idx === null || !isset($vectors[$idx])) {
continue;
}
$embedding = $item['embedding'] ?? [];
if (is_array($embedding) && !empty($embedding)) {
$vectors[$idx] = $embedding;
}
}
return Base::retSuccess("success", $vectors);
}
/**
* ai 插件 /embeddings 端点地址
*
* 查询侧(本类)与 Manticore 表定义ManticoreBase::vectorColumnDDL共用的唯一来源
* 两侧必须指向同一端点,否则查询向量与存量向量来自不同模型导致语义搜索错乱。
*/
public static function embeddingsUrl(): string
{
$host = config('dootask.ai_host', 'ai');
$port = (int) config('dootask.ai_port', 5001);
return "http://{$host}:{$port}/embeddings";
}
/**
* 通过 ai 插件的免费向量模型获取文本的 Embedding 向量
* *
* @param string $text 需要转换的文本 * @param string $text 需要转换的文本
* @param bool $noCache 是否禁用缓存 * @param bool $noCache 是否禁用缓存
@ -916,50 +993,24 @@ class AI
return Base::retError('文本内容不能为空'); return Base::retError('文本内容不能为空');
} }
// 截断过长的文本(OpenAI 限制 8191 tokens约 32K 字符) // 截断过长的文本(约 32K 字符)
$text = mb_substr($text, 0, 30000); $text = mb_substr($text, 0, 30000);
$cacheKey = "openAIEmbedding::" . md5($text); // 缓存键带上当前生效的模型标识:切换向量模型后旧查询缓存自动失效,
// 避免最长 7 天内用旧模型向量去搜新模型索引(模型未知时为空串)
$model = (string) Cache::get('ai:embedding_model', '');
$cacheKey = "embeddingV2::" . md5($model . '|' . $text);
if ($noCache) { if ($noCache) {
Cache::forget($cacheKey); Cache::forget($cacheKey);
} }
$provider = self::resolveEmbeddingProvider(); $result = Cache::remember($cacheKey, Carbon::now()->addDays(7), function () use ($text) {
if (!$provider) { $res = self::requestPluginEmbeddings([$text]);
return Base::retError("请先在「AI 助手」设置中配置支持 Embedding 的 AI 服务");
}
$result = Cache::remember($cacheKey, Carbon::now()->addDays(7), function () use ($text, $provider) {
$payload = [
"model" => $provider['model'],
"input" => $text,
];
// 统一向量维度为 1536与 Manticore 配置一致)
// OpenAI、智谱等支持 dimensions 参数的厂商需要显式指定
$supportsDimensions = in_array($provider['vendor'], ['openai', 'zhipu']);
if ($supportsDimensions) {
$payload['dimensions'] = 1536;
}
$post = json_encode($payload);
$ai = new self($post);
$ai->setProvider($provider);
$ai->setUrlPath('/embeddings');
$ai->setTimeout(30);
$res = $ai->request(true);
if (Base::isError($res)) { if (Base::isError($res)) {
return Base::retError("Embedding 请求失败", $res); return $res;
} }
$resData = Base::json2array($res['data']); $embedding = $res['data'][0] ?? [];
if (empty($resData['data'][0]['embedding'])) {
return Base::retError("Embedding 接口返回数据格式错误", $resData);
}
$embedding = $resData['data'][0]['embedding'];
if (!is_array($embedding) || empty($embedding)) { if (!is_array($embedding) || empty($embedding)) {
return Base::retError("Embedding 向量为空"); return Base::retError("Embedding 向量为空");
} }
@ -973,193 +1024,4 @@ class AI
return $result; return $result;
} }
/**
* 批量获取文本的 Embedding 向量
* OpenAI API 原生支持批量输入,一次请求处理多个文本
*
* @param array $texts 文本数组(最多 100 条)
* @param bool $noCache 是否禁用缓存
* @return array 返回结果,成功时 data 为向量数组的数组(与输入顺序对应)
*/
public static function getBatchEmbeddings(array $texts, $noCache = false)
{
if (!Apps::isInstalled('ai')) {
return Base::retError('应用「AI Assistant」未安装');
}
if (empty($texts)) {
return Base::retSuccess("success", []);
}
// 限制批量大小
// OpenAI 限制:最多 2048 条,单次请求合计最多 300,000 tokens
// 这里限制 500 条,假设平均每条 500 tokens合计 250,000 tokens
$texts = array_slice($texts, 0, 500);
// 准备结果数组,并检查缓存
$results = [];
$uncachedTexts = [];
$uncachedIndices = [];
foreach ($texts as $index => $text) {
if (empty($text)) {
$results[$index] = [];
continue;
}
// 截断过长的文本
$text = mb_substr($text, 0, 30000);
$texts[$index] = $text; // 更新截断后的文本
$cacheKey = "openAIEmbedding::" . md5($text);
if ($noCache) {
Cache::forget($cacheKey);
}
// 检查缓存
if (!$noCache && Cache::has($cacheKey)) {
$cached = Cache::get($cacheKey);
if (Base::isSuccess($cached)) {
$results[$index] = $cached['data'];
continue;
}
}
// 未命中缓存,加入待请求列表
$uncachedTexts[] = $text;
$uncachedIndices[] = $index;
}
// 如果所有文本都在缓存中
if (empty($uncachedTexts)) {
// 按原始顺序返回
ksort($results);
return Base::retSuccess("success", array_values($results));
}
// 获取 provider
$provider = self::resolveEmbeddingProvider();
if (!$provider) {
return Base::retError("请先在「AI 助手」设置中配置支持 Embedding 的 AI 服务");
}
// 构建批量请求
$payload = [
"model" => $provider['model'],
"input" => $uncachedTexts,
];
$supportsDimensions = in_array($provider['vendor'], ['openai', 'zhipu']);
if ($supportsDimensions) {
$payload['dimensions'] = 1536;
}
$post = json_encode($payload);
$ai = new self($post);
$ai->setProvider($provider);
$ai->setUrlPath('/embeddings');
$ai->setTimeout(120); // 批量请求需要更长超时
$res = $ai->request(true);
if (Base::isError($res)) {
return Base::retError("批量 Embedding 请求失败", $res);
}
$resData = Base::json2array($res['data']);
if (empty($resData['data'])) {
return Base::retError("Embedding 接口返回数据格式错误", $resData);
}
// 处理返回的向量并写入缓存
foreach ($resData['data'] as $item) {
$itemIndex = $item['index'] ?? null;
if ($itemIndex === null || !isset($uncachedIndices[$itemIndex])) {
continue;
}
$originalIndex = $uncachedIndices[$itemIndex];
$embedding = $item['embedding'] ?? [];
if (!empty($embedding) && is_array($embedding)) {
$results[$originalIndex] = $embedding;
} else {
$results[$originalIndex] = [];
}
}
// 填充未获取到向量的位置
foreach ($uncachedIndices as $originalIndex) {
if (!isset($results[$originalIndex])) {
$results[$originalIndex] = [];
}
}
// 按原始顺序返回
ksort($results);
return Base::retSuccess("success", array_values($results));
}
/**
* 获取 Embedding 模型配置
*
* @return array|null
*/
protected static function resolveEmbeddingProvider()
{
$setting = Base::setting('aibotSetting');
if (!is_array($setting)) {
$setting = [];
}
// 优先使用 OpenAI支持 embedding 接口)
$key = trim((string)($setting['openai_key'] ?? ''));
if ($key !== '') {
$baseUrl = trim((string)($setting['openai_base_url'] ?? ''));
$baseUrl = $baseUrl ?: 'https://api.openai.com/v1';
$agency = trim((string)($setting['openai_agency'] ?? ''));
return [
'vendor' => 'openai',
'model' => 'text-embedding-3-small',
'api_key' => $key,
'base_url' => rtrim($baseUrl, '/'),
'agency' => $agency,
];
}
$vendorDefaults = [
'deepseek' => [
'base_url' => 'https://api.deepseek.com',
'model' => 'deepseek-embedding',
],
'zhipu' => [
'base_url' => 'https://open.bigmodel.cn/api/paas/v4',
'model' => 'embedding-3',
],
];
// 尝试其他支持 embedding 的服务(如 deepseek、zhipu、qianwen 等)
foreach ($vendorDefaults as $vendor => $defaults) {
$key = trim((string)($setting[$vendor . '_key'] ?? ''));
if ($key !== '') {
$baseUrl = trim((string)($setting[$vendor . '_base_url'] ?? ''));
$baseUrl = $baseUrl ?: $defaults['base_url']; // 使用配置或默认值
$agency = trim((string)($setting[$vendor . '_agency'] ?? ''));
return [
'vendor' => $vendor,
'model' => $defaults['model'],
'api_key' => $key,
'base_url' => rtrim($baseUrl, '/'),
'agency' => $agency,
];
}
}
return null;
}
} }

View File

@ -110,7 +110,7 @@ class Apps
* 获取(必要时生成并持久化)应用的独立密钥 APP_SECRET。 * 获取(必要时生成并持久化)应用的独立密钥 APP_SECRET。
* *
* 与全局 APP_KEY 不同APP_SECRET 每个已安装应用独立、唯一,持久化在应用自身的 * 与全局 APP_KEY 不同APP_SECRET 每个已安装应用独立、唯一,持久化在应用自身的
* docker/appstore/config/{appid}/config.yml KB_INGEST_TOKEN 等每应用参数同源), * docker/appstore/config/{appid}/config.yml其它每应用安装参数同源),
* appstore 安装链路按内置 compose 变量 APP_SECRET 注入插件容器。 * appstore 安装链路按内置 compose 变量 APP_SECRET 注入插件容器。
* 此处主程序侧负责生成/持久化与校验;首次需要时若不存在则惰性生成,保证主程序可独立验证。 * 此处主程序侧负责生成/持久化与校验;首次需要时若不存在则惰性生成,保证主程序可独立验证。
* *

View File

@ -0,0 +1,538 @@
<?php
namespace App\Module;
use App\Exceptions\ApiException;
use App\Models\ProjectUser;
use App\Models\Setting;
use App\Models\User;
use App\Models\UserDepartment;
use Carbon\Carbon;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
/**
* 仪表盘负责人视角数据聚合。
*
* 口径:所选管理部门(含下级部门)的在职成员所参与、且允许负责人查看的未归档项目;
* 仅统计主任务、全员可见任务,任务需由范围内成员负责或当前没有负责人。
*/
class DashboardTeam
{
public const SOON_DAYS = 3;
public const HIGH_PRIORITY_COUNT = 2;
public const STATS_CACHE_SECONDS = 30;
/**
* 解析并校验负责人视角范围。
*/
public static function context(User $user, $selectedDepartmentIds = null): array
{
if (Base::settingFind('system', 'department_owner_project_view', 'open') !== 'open') {
throw new ApiException('未开启部门负责人视角功能');
}
$managedIds = UserDepartment::getManagedDepartments($user->userid)
->pluck('id')
->map(fn($id) => intval($id))
->values()
->toArray();
if (empty($managedIds)) {
throw new ApiException('没有可查看的部门数据');
}
$selectedIds = self::normalizeSelectedDepartmentIds($selectedDepartmentIds, $managedIds);
if (empty($selectedIds)) {
throw new ApiException('没有可查看的部门数据');
}
$departmentIds = self::expandDepartmentIds($selectedIds);
$members = self::loadMembers($departmentIds);
$memberUserids = $members->pluck('userid')->map(fn($id) => intval($id))->values()->toArray();
$projectIds = self::loadProjectIds($memberUserids);
$ownProjectIds = empty($projectIds) ? [] : ProjectUser::whereUserid($user->userid)
->whereIn('project_id', $projectIds)
->pluck('project_id')
->map(fn($id) => intval($id))
->unique()
->values()
->toArray();
return [
'viewer_userid' => intval($user->userid),
'selected_department_ids' => $selectedIds,
'department_ids' => $departmentIds,
'member_userids' => $memberUserids,
'member_map' => $members->keyBy('userid')->map(fn(User $member) => [
'userid' => intval($member->userid),
'nickname' => $member->nickname,
'userimg' => $member->userimg,
])->toArray(),
'project_ids' => $projectIds,
'own_project_id_map' => array_fill_keys($ownProjectIds, true),
];
}
/**
* 团队统计。
*/
public static function stats(array $context, bool $refresh = false): array
{
$cacheKey = 'dashboard:team:stats:v3:' . $context['viewer_userid'] . ':' . sha1(json_encode([
$context['selected_department_ids'],
$context['department_ids'],
$context['member_userids'],
$context['project_ids'],
]));
if ($refresh) {
Cache::forget($cacheKey);
}
return Cache::remember($cacheKey, now()->addSeconds(self::STATS_CACHE_SECONDS), function () use ($context) {
$now = Carbon::now();
$taskAlias = DB::getTablePrefix() . 't';
$soonEnd = $now->clone()->addDays(self::SOON_DAYS)->endOfDay();
$weekStart = $now->clone()->startOfWeek(Carbon::MONDAY)->startOfDay();
$nextWeekStart = $weekStart->clone()->addWeek();
$lastWeekStart = $weekStart->clone()->subWeek();
$base = self::baseTaskBuilder($context);
$core = (clone $base)->selectRaw("
SUM(CASE WHEN {$taskAlias}.complete_at IS NULL THEN 1 ELSE 0 END) AS stat_uncompleted,
SUM(CASE WHEN {$taskAlias}.complete_at IS NULL AND {$taskAlias}.end_at IS NOT NULL AND {$taskAlias}.end_at < ? THEN 1 ELSE 0 END) AS stat_overdue,
SUM(CASE WHEN {$taskAlias}.complete_at IS NULL AND {$taskAlias}.end_at IS NOT NULL AND {$taskAlias}.end_at >= ? AND {$taskAlias}.end_at <= ? THEN 1 ELSE 0 END) AS stat_due_soon,
SUM(CASE WHEN {$taskAlias}.complete_at >= ? AND {$taskAlias}.complete_at < ? THEN 1 ELSE 0 END) AS stat_week_completed,
SUM(CASE WHEN {$taskAlias}.complete_at >= ? AND {$taskAlias}.complete_at < ? THEN 1 ELSE 0 END) AS stat_last_week_completed
", [
$now->toDateTimeString(),
$now->toDateTimeString(),
$soonEnd->toDateTimeString(),
$weekStart->toDateTimeString(),
$nextWeekStart->toDateTimeString(),
$lastWeekStart->toDateTimeString(),
$weekStart->toDateTimeString(),
])->first();
$noOwner = (clone $base)
->whereNull('t.complete_at')
->whereNotExists(self::ownerExistsQuery('t.id'))
->count();
$members = self::memberDistribution($context, $now);
return [
'generated_at' => $now->toDateTimeString(),
'member_count' => count($context['member_userids']),
'blocks' => [
'uncompleted' => intval($core->stat_uncompleted ?? 0),
'overdue' => intval($core->stat_overdue ?? 0),
'overdue_owner_count' => count(array_filter($members, fn($member) => $member['overdue'] > 0)),
'due_soon' => intval($core->stat_due_soon ?? 0),
'week_completed' => intval($core->stat_week_completed ?? 0),
'last_week_completed' => intval($core->stat_last_week_completed ?? 0),
'no_owner' => intval($noOwner),
],
'priority' => self::priorityDistribution($context),
'members' => $members,
'high_levels' => self::highPriorityLevels(),
];
});
}
/**
* 分页任务列表。
*/
public static function tasks(array $context, array $filters)
{
$now = Carbon::now();
$soonEnd = $now->clone()->addDays(self::SOON_DAYS)->endOfDay();
$memberId = intval($filters['member_id'] ?? 0);
$level = $filters['level'] ?? null;
$type = $filters['type'] ?? '';
$builder = self::baseTaskBuilder($context)->whereNull('t.complete_at');
if ($memberId > 0) {
$builder->whereExists(self::ownerExistsQuery('t.id', [$memberId]));
} elseif ($level !== null) {
if (intval($level) === -1) {
$levels = self::priorityLevels();
if (!empty($levels)) {
$builder->whereNotIn('t.p_level', $levels);
}
} else {
$builder->where('t.p_level', intval($level));
}
} else {
switch ($type) {
case 'overdue':
$builder->whereNotNull('t.end_at')->where('t.end_at', '<', $now);
break;
case 'soon':
$builder->whereNotNull('t.end_at')->whereBetween('t.end_at', [$now, $soonEnd]);
break;
case 'hi':
$levels = self::highPriorityLevels();
$builder->whereIn('t.p_level', $levels ?: [-1]);
break;
case 'noowner':
$builder->whereNotExists(self::ownerExistsQuery('t.id'));
break;
}
}
$taskAlias = DB::getTablePrefix() . 't';
$builder->leftJoin('project_flow_items as fi', 'fi.id', '=', 't.flow_item_id')
->select([
't.id',
't.parent_id',
't.project_id',
't.column_id',
't.name',
't.end_at',
't.p_level',
't.p_name',
't.p_color',
't.flow_item_id',
't.flow_item_name',
'p.name as project_name',
'fi.status as flow_item_status',
'fi.color as flow_item_color',
])
->orderByRaw("{$taskAlias}.end_at IS NULL")
->orderBy('t.end_at')
->orderByDesc('t.id');
$list = $builder->paginate(Base::getPaginate(50, 20));
$taskIds = $list->getCollection()->pluck('id')->map(fn($id) => intval($id))->toArray();
$owners = self::taskOwners($taskIds, $context['member_userids'], $memberId);
$ownProjectMap = $context['own_project_id_map'];
$list->setCollection($list->getCollection()->map(function ($task) use ($owners, $ownProjectMap) {
$item = (array)$task;
$item['id'] = intval($item['id']);
$item['parent_id'] = intval($item['parent_id']);
$item['project_id'] = intval($item['project_id']);
$item['column_id'] = intval($item['column_id']);
$item['p_level'] = intval($item['p_level']);
$flowParts = explode('|', $item['flow_item_name'] ?: '');
if (count($flowParts) >= 2) {
$item['flow_item_status'] = $item['flow_item_status'] ?: ($flowParts[0] ?? '');
$item['flow_item_name'] = $flowParts[1] ?? $item['flow_item_name'];
$item['flow_item_color'] = $item['flow_item_color'] ?: ($flowParts[2] ?? '');
}
$item['owners'] = $owners[$item['id']] ?? [];
$item['owner'] = $item['owners'][0] ?? null;
$item['department_readonly'] = !isset($ownProjectMap[$item['project_id']]);
return $item;
}));
return $list;
}
/**
* 当前系统优先级ID列表保持设置顺序
* @return array<int>
*/
public static function priorityLevels(): array
{
return array_map(fn($item) => intval($item['priority']), self::priorityList());
}
/**
* 基础任务范围:主任务、全员可见、未归档项目,且由团队成员负责或没有负责人。
*/
protected static function baseTaskBuilder(array $context): Builder
{
$projectIds = $context['project_ids'] ?: [0];
$memberUserids = $context['member_userids'];
return DB::table('project_tasks as t')
->join('projects as p', 'p.id', '=', 't.project_id')
->whereIn('t.project_id', $projectIds)
->where('t.parent_id', 0)
->where('t.visibility', 1)
->whereNull('t.archived_at')
->whereNull('t.deleted_at')
->whereNull('p.archived_at')
->whereNull('p.deleted_at')
->where(function (Builder $query) {
$query->where('p.department_owner_view', '<>', 'close')
->orWhereNull('p.department_owner_view');
})
->where(function (Builder $query) use ($memberUserids) {
if (empty($memberUserids)) {
$query->whereRaw('1 = 0');
return;
}
$query->whereExists(self::ownerExistsQuery('t.id', $memberUserids))
->orWhereNotExists(self::ownerExistsQuery('t.id'));
});
}
/**
* 负责人存在子查询。
*/
protected static function ownerExistsQuery(string $taskColumn, ?array $userids = null): callable
{
return function (Builder $query) use ($taskColumn, $userids) {
$query->selectRaw('1')
->from('project_task_users as owner_scope')
->whereColumn('owner_scope.task_id', $taskColumn)
->where('owner_scope.owner', 1);
if ($userids !== null) {
$query->whereIn('owner_scope.userid', $userids ?: [0]);
}
};
}
/**
* 优先级分布(包含无负责人任务)。
*/
protected static function priorityDistribution(array $context): array
{
$taskAlias = DB::getTablePrefix() . 't';
$rows = self::baseTaskBuilder($context)
->whereNull('t.complete_at')
->selectRaw("{$taskAlias}.p_level, COUNT(*) AS stat_num")
->groupBy('t.p_level')
->pluck('stat_num', 't.p_level');
$result = [];
$matched = [];
foreach (self::priorityList() as $item) {
$level = intval($item['priority']);
$matched[] = $level;
$result[] = [
'level' => $level,
'name' => $item['name'],
'color' => $item['color'],
'num' => intval($rows->get($level, 0)),
];
}
$unset = 0;
foreach ($rows as $level => $num) {
if (!in_array(intval($level), $matched, true)) {
$unset += intval($num);
}
}
if ($unset > 0) {
$result[] = [
'level' => -1,
'name' => '',
'color' => '#c5c8ce',
'num' => $unset,
];
}
return $result;
}
/**
* 成员任务分配。流程阶段与超期风险独立统计,无工作流的普通未完成任务归入待处理;
* 多负责人任务会分别计入每个负责人的工作量,团队总数仍按任务去重。
*/
protected static function memberDistribution(array $context, Carbon $now): array
{
$memberUserids = $context['member_userids'];
if (empty($memberUserids) || empty($context['project_ids'])) {
return array_values(array_map(fn($member) => array_merge($member, [
'total' => 0,
'overdue' => 0,
'segments' => ['progress' => 0, 'start' => 0, 'test' => 0],
]), $context['member_map']));
}
$nowText = $now->toDateTimeString();
$prefix = DB::getTablePrefix();
$taskAlias = $prefix . 't';
$taskUserAlias = $prefix . 'tu';
$flowAlias = $prefix . 'fi';
$rows = DB::table('project_task_users as tu')
->join('project_tasks as t', 't.id', '=', 'tu.task_id')
->join('projects as p', 'p.id', '=', 't.project_id')
->leftJoin('project_flow_items as fi', 'fi.id', '=', 't.flow_item_id')
->whereIn('tu.userid', $memberUserids)
->where('tu.owner', 1)
->whereIn('t.project_id', $context['project_ids'])
->where('t.parent_id', 0)
->where('t.visibility', 1)
->whereNull('t.complete_at')
->whereNull('t.archived_at')
->whereNull('t.deleted_at')
->whereNull('p.archived_at')
->whereNull('p.deleted_at')
->where(function (Builder $query) {
$query->where('p.department_owner_view', '<>', 'close')
->orWhereNull('p.department_owner_view');
})
->selectRaw("
{$taskUserAlias}.userid,
COUNT(DISTINCT {$taskAlias}.id) AS stat_total,
COUNT(DISTINCT CASE WHEN {$taskAlias}.end_at IS NOT NULL AND {$taskAlias}.end_at < ? THEN {$taskAlias}.id END) AS stat_overdue,
COUNT(DISTINCT CASE WHEN {$flowAlias}.status = 'progress' THEN {$taskAlias}.id END) AS stat_progress,
COUNT(DISTINCT CASE WHEN {$flowAlias}.status = 'test' THEN {$taskAlias}.id END) AS stat_test,
COUNT(DISTINCT CASE WHEN {$flowAlias}.status IS NULL OR {$flowAlias}.status NOT IN ('progress', 'test') THEN {$taskAlias}.id END) AS stat_start
", [$nowText])
->groupBy('tu.userid')
->get()
->keyBy(fn($row) => intval($row->userid));
$members = [];
foreach ($context['member_map'] as $userid => $member) {
$row = $rows->get(intval($userid));
$members[] = array_merge($member, [
'total' => intval($row->stat_total ?? 0),
'overdue' => intval($row->stat_overdue ?? 0),
'segments' => [
'progress' => intval($row->stat_progress ?? 0),
'start' => intval($row->stat_start ?? 0),
'test' => intval($row->stat_test ?? 0),
],
]);
}
usort($members, function ($a, $b) {
return $b['overdue'] <=> $a['overdue']
?: $b['total'] <=> $a['total']
?: $a['userid'] <=> $b['userid'];
});
return $members;
}
/**
* 任务负责人数据,范围内成员优先展示;成员筛选时将该成员置首。
*/
protected static function taskOwners(array $taskIds, array $memberUserids, int $preferredUserid = 0): array
{
if (empty($taskIds)) {
return [];
}
$rows = DB::table('project_task_users')
->whereIn('task_id', $taskIds)
->where('owner', 1)
->orderBy('id')
->get(['task_id', 'userid']);
$userids = $rows->pluck('userid')->map(fn($id) => intval($id))->unique()->values()->toArray();
$users = empty($userids) ? collect() : User::select(User::$basicField)
->whereIn('userid', $userids)
->get()
->keyBy('userid');
$memberMap = array_fill_keys($memberUserids, true);
$owners = [];
foreach ($rows as $row) {
$userid = intval($row->userid);
$user = $users->get($userid);
$owners[intval($row->task_id)][] = [
'userid' => $userid,
'nickname' => $user?->nickname ?? '',
];
}
foreach ($owners as &$list) {
usort($list, function ($a, $b) use ($memberMap, $preferredUserid) {
if ($preferredUserid > 0) {
$preferred = ($b['userid'] === $preferredUserid) <=> ($a['userid'] === $preferredUserid);
if ($preferred !== 0) {
return $preferred;
}
}
$managed = isset($memberMap[$b['userid']]) <=> isset($memberMap[$a['userid']]);
return $managed !== 0 ? $managed : $a['userid'] <=> $b['userid'];
});
}
unset($list);
return $owners;
}
/**
* 在全部部门中一次性展开下级部门,避免递归 N+1
*/
protected static function expandDepartmentIds(array $selectedIds): array
{
$children = UserDepartment::select(['id', 'parent_id'])
->get()
->groupBy(fn(UserDepartment $department) => intval($department->parent_id));
$result = [];
$queue = array_values(array_unique(array_map('intval', $selectedIds)));
while (!empty($queue)) {
$departmentId = array_shift($queue);
if ($departmentId <= 0 || isset($result[$departmentId])) {
continue;
}
$result[$departmentId] = true;
foreach ($children->get($departmentId, collect()) as $child) {
$queue[] = intval($child->id);
}
}
return array_keys($result);
}
/**
* 当前范围内的在职非机器人成员。users.department 为现有权限口径的权威数据源。
*/
protected static function loadMembers(array $departmentIds): Collection
{
if (empty($departmentIds)) {
return collect();
}
return User::select(User::$basicField)
->whereNull('disable_at')
->where('bot', 0)
->where(function ($query) use ($departmentIds) {
foreach ($departmentIds as $departmentId) {
$query->orWhere('department', 'like', "%,{$departmentId},%");
}
})
->orderBy('userid')
->get();
}
/**
* 成员参与且允许负责人查看的活动项目。
*/
protected static function loadProjectIds(array $memberUserids): array
{
if (empty($memberUserids)) {
return [];
}
return DB::table('project_users as pu')
->join('projects as p', 'p.id', '=', 'pu.project_id')
->whereIn('pu.userid', $memberUserids)
->whereNull('p.archived_at')
->whereNull('p.deleted_at')
->where(function (Builder $query) {
$query->where('p.department_owner_view', '<>', 'close')
->orWhereNull('p.department_owner_view');
})
->distinct()
->orderBy('p.id')
->pluck('p.id')
->map(fn($id) => intval($id))
->toArray();
}
protected static function normalizeSelectedDepartmentIds($selectedIds, array $managedIds): array
{
if ($selectedIds === null || $selectedIds === '' || $selectedIds === 'all' || $selectedIds === []) {
return $managedIds;
}
if (!is_array($selectedIds)) {
$selectedIds = explode(',', (string)$selectedIds);
}
return array_values(array_unique(array_intersect(
array_map('intval', $selectedIds),
$managedIds
)));
}
protected static function highPriorityLevels(): array
{
return array_slice(self::priorityLevels(), 0, self::HIGH_PRIORITY_COUNT);
}
protected static function priorityList(): array
{
return Setting::normalizeTaskPriorityList(Base::setting('priority'));
}
}

View File

@ -196,13 +196,14 @@ class Ihttp
$rlt['status'] = $matches[3]; $rlt['status'] = $matches[3];
$rlt['responseline'] = $split2[0]; $rlt['responseline'] = $split2[0];
$header = explode("\r\n", $split2[1]); $header = explode("\r\n", $split2[1]);
$rlt['headers'] = [];
$isgzip = false; $isgzip = false;
$ischunk = false; $ischunk = false;
foreach ($header as $v) { foreach ($header as $v) {
$row = explode(':', $v); $row = explode(':', $v);
$key = trim($row[0]); $key = trim($row[0]);
$value = trim(substr($v, strlen($row[0]) + 1)); $value = trim(substr($v, strlen($row[0]) + 1));
if (is_array($rlt['headers'][$key])) { if (isset($rlt['headers'][$key]) && is_array($rlt['headers'][$key])) {
$rlt['headers'][$key][] = $value; $rlt['headers'][$key][] = $value;
} elseif (!empty($rlt['headers'][$key])) { } elseif (!empty($rlt['headers'][$key])) {
$temp = $rlt['headers'][$key]; $temp = $rlt['headers'][$key];

View File

@ -8,6 +8,7 @@ use App\Module\Base;
use App\Module\AI; use App\Module\AI;
use PDO; use PDO;
use PDOException; use PDOException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
/** /**
@ -21,6 +22,30 @@ class ManticoreBase
private static ?PDO $pdo = null; private static ?PDO $pdo = null;
private static bool $initialized = false; private static bool $initialized = false;
/**
* 向量表结构版本;修改表结构/向量列参数时递增,触发已部署实例自动重建
*/
private const SCHEMA_VERSION = 1;
/**
* Auto Embeddings MODEL_NAME。必须用 Manticore 不认识的名字:
* 已知名字(如 text-embedding-ada-002)会按引擎硬编码维度校验,
* 未知名字才会在建表时向 API_URL 探测真实维度(免费模型为 1024)。
* 实际模型由 ai 插件的 EMBEDDING_MODEL 决定,此处仅为路由标签。
*/
private const EMBEDDING_MODEL_NAME = 'openai/qwen3-embedding';
/**
* 批量写入分块上限行数与字节预算Manticore max_allowed_packet 默认 128MB取保守值
*/
private const BATCH_CHUNK_ROWS = 30;
private const BATCH_CHUNK_BYTES = 8388608;
/**
* 5 张向量表名(键值即 VECTOR_TABLE_CONFIG type
*/
private const VECTOR_TABLES = ['msg', 'file', 'task', 'project', 'user'];
private string $host; private string $host;
private int $port; private int $port;
@ -70,13 +95,169 @@ class ManticoreBase
/** /**
* 初始化表结构 * 初始化表结构
*
* 向量列使用 Manticore Auto EmbeddingsMODEL_NAME/API_URL 指向 ai 插件 /embeddings
* 引擎在写入/更新行时自动按 FROM 字段生成向量,无需 PHP 侧生成。
* key_values 中的 vector:schema 标记记录当前结构指纹(结构版本/模型/端点/APP_KEY 哈希),
* 不匹配首次安装、老版本升级、APP_KEY 轮换)即整体重建并重置同步指针,触发全量重灌。
*/ */
private function initializeTables(PDO $pdo): void private function initializeTables(PDO $pdo): void
{ {
try { try {
// 创建文件向量表 // 键值表必须最先建(用于读取/持久化结构标记与同步指针)
// charset_table='non_cjk, cjk' 同时支持英文和中日韩文字
$pdo->exec(" $pdo->exec("
CREATE TABLE IF NOT EXISTS key_values (
id BIGINT,
k STRING,
v TEXT
)
");
$expected = self::schemaMarker();
if (self::kvGetPdo($pdo, 'vector:schema') === $expected
&& self::allVectorTablesExist($pdo)
&& !self::embeddingModelChanged($pdo)) {
return;
}
// 并发进程只允许一个执行重建;抢锁失败的进程本轮写入失败会进重试队列,无碍
$lock = Cache::lock('manticore:schema-rebuild', 300);
if (!$lock->get()) {
return;
}
try {
// 拿到锁时可能另一进程刚完成重建marker 写入在锁内),先复检避免重复重建
if (self::kvGetPdo($pdo, 'vector:schema') === $expected
&& self::allVectorTablesExist($pdo)
&& !self::embeddingModelChanged($pdo)) {
return;
}
// 先用一次性探针表验证 ai 端点可用CREATE 会向端点探测维度、未就绪则抛错),
// 通过后才 DROP 现有表——避免 ai 未就绪时销毁旧索引后建不回来
$pdo->exec("DROP TABLE IF EXISTS _schema_probe");
$pdo->exec("CREATE TABLE _schema_probe (t TEXT, " . self::vectorColumnDDL('t') . ")");
$pdo->exec("DROP TABLE IF EXISTS _schema_probe");
foreach (self::vectorTableDDLs() as $table => $ddl) {
$pdo->exec("DROP TABLE IF EXISTS {$table}");
$pdo->exec($ddl);
}
self::resetSyncPointersPdo($pdo);
// 清理旧向量管道遗留键vector:dim 与 vector:*LastId 指针)
$legacy = ["'vector:dim'"];
foreach (self::VECTOR_TABLES as $t) {
$legacy[] = "'vector:manticore" . ucfirst($t) . "LastId'";
}
$pdo->exec("DELETE FROM key_values WHERE k IN (" . implode(',', $legacy) . ")");
self::rememberEmbeddingModel($pdo);
// marker 最后写入且在锁内:写入即代表重建完整成功
self::kvSetPdo($pdo, 'vector:schema', $expected);
Log::info("Manticore vector tables rebuilt for auto-embeddings schema {$expected}");
} catch (\Throwable $e) {
// 重建失败(如 ai 插件未就绪/未升级):不写 marker下个进程重试可自愈
Log::error('Manticore schema rebuild failed: ' . $e->getMessage());
} finally {
$lock->release();
}
} catch (\Throwable $e) {
Log::error('Manticore initializeTables failed: ' . $e->getMessage());
}
}
/**
* ai 插件向量化端点地址(唯一来源在 AI::embeddingsUrl查询侧与表定义共用
*/
private static function embeddingsApiUrl(): string
{
return AI::embeddingsUrl();
}
/**
* 写入表定义的派生密钥sha256(APP_KEY:embeddings),与 ai 插件约定一致。
* 不直接写 APP_KEYLaravel 主密钥),避免其落入搜索引擎元数据/数据卷;
* 派生值不可反推,且仅授予 /embeddings 调用权限。
*/
private static function embeddingsApiKey(): string
{
return hash('sha256', config('app.key') . ':embeddings');
}
/**
* 当前向量表结构指纹:结构版本/模型名/端点/APP_KEY 哈希任一变化都会触发整体重建
*/
private static function schemaMarker(): string
{
return md5(self::SCHEMA_VERSION . '|' . self::EMBEDDING_MODEL_NAME . '|'
. self::embeddingsApiUrl() . '|' . hash('sha256', (string) config('app.key')));
}
/**
* 检测 ai 插件实际生效的向量模型是否与建表时不一致(不一致需重建,否则维度可能不匹配)。
*
* 实际模型EMBEDDING_MODEL env对主程序不可见由查询侧在成功请求后
* 写入缓存 ai:embedding_model建表时的模型记录在 key_values vector:model。
* 任一侧未知时不触发(返回 false),已知且存量缺失时顺手补记。
*/
private static function embeddingModelChanged(PDO $pdo): bool
{
$live = (string) Cache::get('ai:embedding_model', '');
if ($live === '') {
return false;
}
$stored = self::kvGetPdo($pdo, 'vector:model');
if ($stored === null || $stored === '') {
// 旧部署/首次:补记当前模型,不触发重建
self::kvSetPdo($pdo, 'vector:model', $live);
return false;
}
return $stored !== $live;
}
/**
* 重建成功后记录当前生效的向量模型(优先取查询侧维护的缓存值)
*/
private static function rememberEmbeddingModel(PDO $pdo): void
{
$live = (string) Cache::get('ai:embedding_model', '');
if ($live !== '') {
self::kvSetPdo($pdo, 'vector:model', $live);
} else {
// 未知则清掉存量,待查询侧探得后由 embeddingModelChanged 补记
$pdo->exec("DELETE FROM key_values WHERE k = 'vector:model'");
}
}
/**
* 生成 Auto Embeddings 向量列定义(引擎按 FROM 字段自动生成/更新向量)
*
* 注意不写 KNN_DIMS MODEL_NAME 互斥),维度由引擎建表时向端点探测。
*
* @param string $from 参与向量化的字段(镜像旧 PHP 管道的拼接字段,逗号分隔)
*/
private static function vectorColumnDDL(string $from): string
{
return "content_vector float_vector knn_type='hnsw' hnsw_similarity='cosine'"
. " MODEL_NAME='" . self::EMBEDDING_MODEL_NAME . "'"
. " FROM='{$from}'"
. " API_KEY='" . self::embeddingsApiKey() . "'"
. " API_URL='" . self::embeddingsApiUrl() . "'"
. " API_TIMEOUT='60'";
}
/**
* 生成 5 张向量表的建表语句
*
* charset_table='non_cjk, cjk' 同时支持英文和中日韩文字
*
* @return array [table => DDL]
*/
private static function vectorTableDDLs(): array
{
$tail = "\n ) charset_table='non_cjk, cjk' morphology='icu_chinese'";
return [
'file_vectors' => "
CREATE TABLE IF NOT EXISTS file_vectors ( CREATE TABLE IF NOT EXISTS file_vectors (
id BIGINT, id BIGINT,
file_id BIGINT, file_id BIGINT,
@ -87,21 +268,8 @@ class ManticoreBase
file_ext STRING, file_ext STRING,
content TEXT, content TEXT,
allowed_users MULTI, allowed_users MULTI,
content_vector float_vector knn_type='hnsw' knn_dims='1536' hnsw_similarity='cosine' " . self::vectorColumnDDL('file_name,content') . $tail,
) charset_table='non_cjk, cjk' morphology='icu_chinese' 'user_vectors' => "
");
// 创建键值存储表
$pdo->exec("
CREATE TABLE IF NOT EXISTS key_values (
id BIGINT,
k STRING,
v TEXT
)
");
// 创建用户向量表
$pdo->exec("
CREATE TABLE IF NOT EXISTS user_vectors ( CREATE TABLE IF NOT EXISTS user_vectors (
id BIGINT, id BIGINT,
userid BIGINT, userid BIGINT,
@ -110,12 +278,8 @@ class ManticoreBase
profession TEXT, profession TEXT,
tags TEXT, tags TEXT,
introduction TEXT, introduction TEXT,
content_vector float_vector knn_type='hnsw' knn_dims='1536' hnsw_similarity='cosine' " . self::vectorColumnDDL('nickname,email,profession,tags,introduction') . $tail,
) charset_table='non_cjk, cjk' morphology='icu_chinese' 'project_vectors' => "
");
// 创建项目向量表
$pdo->exec("
CREATE TABLE IF NOT EXISTS project_vectors ( CREATE TABLE IF NOT EXISTS project_vectors (
id BIGINT, id BIGINT,
project_id BIGINT, project_id BIGINT,
@ -124,12 +288,8 @@ class ManticoreBase
project_name TEXT, project_name TEXT,
project_desc TEXT, project_desc TEXT,
allowed_users MULTI, allowed_users MULTI,
content_vector float_vector knn_type='hnsw' knn_dims='1536' hnsw_similarity='cosine' " . self::vectorColumnDDL('project_name,project_desc') . $tail,
) charset_table='non_cjk, cjk' morphology='icu_chinese' 'task_vectors' => "
");
// 创建任务向量表
$pdo->exec("
CREATE TABLE IF NOT EXISTS task_vectors ( CREATE TABLE IF NOT EXISTS task_vectors (
id BIGINT, id BIGINT,
task_id BIGINT, task_id BIGINT,
@ -140,12 +300,8 @@ class ManticoreBase
task_desc TEXT, task_desc TEXT,
task_content TEXT, task_content TEXT,
allowed_users MULTI, allowed_users MULTI,
content_vector float_vector knn_type='hnsw' knn_dims='1536' hnsw_similarity='cosine' " . self::vectorColumnDDL('task_name,task_desc,task_content') . $tail,
) charset_table='non_cjk, cjk' morphology='icu_chinese' 'msg_vectors' => "
");
// 创建消息向量表
$pdo->exec("
CREATE TABLE IF NOT EXISTS msg_vectors ( CREATE TABLE IF NOT EXISTS msg_vectors (
id BIGINT, id BIGINT,
msg_id BIGINT, msg_id BIGINT,
@ -155,13 +311,69 @@ class ManticoreBase
content TEXT, content TEXT,
allowed_users MULTI, allowed_users MULTI,
created_at BIGINT, created_at BIGINT,
content_vector float_vector knn_type='hnsw' knn_dims='1536' hnsw_similarity='cosine' " . self::vectorColumnDDL('content') . $tail,
) charset_table='non_cjk, cjk' morphology='icu_chinese' ];
"); }
// Tables initialized successfully /**
} catch (PDOException $e) { * 检查 5 张向量表是否都已存在
// 表可能已存在,忽略初始化错误 */
private static function allVectorTablesExist(PDO $pdo): bool
{
try {
$stmt = $pdo->query("SHOW TABLES");
$existing = [];
foreach ($stmt->fetchAll(PDO::FETCH_NUM) as $row) {
$existing[$row[0]] = true;
}
foreach (array_keys(self::vectorTableDDLs()) as $table) {
if (!isset($existing[$table])) {
return false;
}
}
return true;
} catch (\Throwable $e) {
return false;
}
}
/**
* 直接用 PDO 读取 key_values避免初始化期通过 ManticoreKeyValue 造成递归)
*/
private static function kvGetPdo(PDO $pdo, string $key): ?string
{
try {
$stmt = $pdo->prepare("SELECT v FROM key_values WHERE k = ?");
$stmt->execute([$key]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return ($row && isset($row['v'])) ? (string)$row['v'] : null;
} catch (\Throwable $e) {
return null;
}
}
/**
* 直接用 PDO 写入 key_values ManticoreKeyValue::set id 规则)
*/
private static function kvSetPdo(PDO $pdo, string $key, string $value): void
{
try {
$del = $pdo->prepare("DELETE FROM key_values WHERE k = ?");
$del->execute([$key]);
$ins = $pdo->prepare("INSERT INTO key_values (id, k, v) VALUES (?, ?, ?)");
$ins->execute([abs(crc32($key)), $key, $value]);
} catch (\Throwable $e) {
// 忽略
}
}
/**
* 重置全文同步进度指针sync:*),触发全量重灌(向量由引擎随行自动生成)
*/
private static function resetSyncPointersPdo(PDO $pdo): void
{
foreach (self::VECTOR_TABLES as $t) {
self::kvSetPdo($pdo, "sync:manticore" . ucfirst($t) . "LastId", '0');
} }
} }
@ -256,13 +468,17 @@ class ManticoreBase
*/ */
public function executeRaw(string $sql): bool public function executeRaw(string $sql): bool
{ {
// 日志上下文只保留 SQL 前 2KB多行批量语句可达数 MB完整写入会淹没日志
$sqlPreview = strlen($sql) > 2048
? substr($sql, 0, 2048) . ' ...[+' . (strlen($sql) - 2048) . ' bytes]'
: $sql;
return $this->runWithRetry( return $this->runWithRetry(
function (PDO $pdo) use ($sql) { function (PDO $pdo) use ($sql) {
$pdo->exec($sql); $pdo->exec($sql);
return true; return true;
}, },
false, false,
['sql' => $sql] ['sql' => $sqlPreview]
); );
} }
@ -1810,10 +2026,10 @@ class ManticoreBase
]; ];
/** /**
* 通用向量插入方法 * 通用单行写入方法REPLACE向量由引擎 Auto Embeddings 自动生成)
* *
* 使用 executeRaw 直接执行 SQL避免 Manticore prepared statement * 使用 executeRaw 直接执行 SQL避免 Manticore prepared statement
* 无法解析 MVA 和向量字段括号语法的问题。 * 无法解析 MVA 字段括号语法的问题。
* *
* @param string $type 类型: msg/file/task/project/user * @param string $type 类型: msg/file/task/project/user
* @param array $data 数据,键名对应字段名 * @param array $data 数据,键名对应字段名
@ -1828,8 +2044,6 @@ class ManticoreBase
$config = self::VECTOR_TABLE_CONFIG[$type]; $config = self::VECTOR_TABLE_CONFIG[$type];
$table = $config['table']; $table = $config['table'];
$pk = $config['pk']; $pk = $config['pk'];
$fields = $config['fields'];
$mvaFields = $config['mva_fields'];
// 检查主键 // 检查主键
$pkValue = $data[$pk] ?? 0; $pkValue = $data[$pk] ?? 0;
@ -1838,44 +2052,11 @@ class ManticoreBase
} }
$instance = new self(); $instance = new self();
[$fieldList, $valueList] = $instance->buildRowValues($config, $data);
// 先删除已存在的记录 // REPLACE 按 id 原子替换整行,向量列由引擎按 FROM 字段自动重新生成。
$instance->execute("DELETE FROM {$table} WHERE {$pk} = ?", [$pkValue]); // 前提:所有 upsertXxxVector 均强制 id = 主键值,故 REPLACE(按 id) 与按主键去重等价
$sql = "REPLACE INTO {$table} (" . implode(', ', $fieldList) . ") VALUES (" . implode(', ', $valueList) . ")";
// 构建字段列表和值
$fieldList = [];
$valueList = [];
// 处理普通字段
foreach ($fields as $field) {
$fieldList[] = $field;
$value = $data[$field] ?? ($field === 'created_at' ? time() : (in_array($field, self::NUMERIC_FIELDS) ? 0 : ''));
if (in_array($field, self::NUMERIC_FIELDS)) {
$valueList[] = (int)$value;
} else {
$valueList[] = $instance->quoteValue((string)$value);
}
}
// 处理 MVA 字段
foreach ($mvaFields as $mvaField) {
$fieldList[] = $mvaField;
$mvaData = $data[$mvaField] ?? [];
$valueList[] = !empty($mvaData)
? '(' . implode(',', array_map('intval', $mvaData)) . ')'
: '()';
}
// 处理向量字段
$vectorValue = $data['content_vector'] ?? null;
if ($vectorValue) {
$fieldList[] = 'content_vector';
$valueList[] = str_replace(['[', ']'], ['(', ')'], $vectorValue);
}
// 构建并执行 SQL
$sql = "INSERT INTO {$table} (" . implode(', ', $fieldList) . ") VALUES (" . implode(', ', $valueList) . ")";
$result = $instance->executeRaw($sql); $result = $instance->executeRaw($sql);
@ -1885,12 +2066,46 @@ class ManticoreBase
ManticoreSyncFailure::removeSuccess($type, $pkValue, 'sync'); ManticoreSyncFailure::removeSuccess($type, $pkValue, 'sync');
} else { } else {
// 失败则记录 // 失败则记录
ManticoreSyncFailure::recordFailure($type, $pkValue, 'sync', "INSERT failed for {$table}"); ManticoreSyncFailure::recordFailure($type, $pkValue, 'sync', "REPLACE failed for {$table}");
} }
return $result; return $result;
} }
/**
* 构建一行数据的字段列表与内联值(普通字段 + MVA 字段,向量列由引擎自动生成,不在此列)
*
* @param array $config VECTOR_TABLE_CONFIG 中的类型配置
* @param array $data 行数据
* @return array [fieldList, valueList]
*/
private function buildRowValues(array $config, array $data): array
{
$fieldList = [];
$valueList = [];
foreach ($config['fields'] as $field) {
$fieldList[] = $field;
$value = $data[$field] ?? ($field === 'created_at' ? time() : (in_array($field, self::NUMERIC_FIELDS) ? 0 : ''));
if (in_array($field, self::NUMERIC_FIELDS)) {
$valueList[] = (int)$value;
} else {
$valueList[] = $this->quoteValue((string)$value);
}
}
foreach ($config['mva_fields'] as $mvaField) {
$fieldList[] = $mvaField;
$mvaData = $data[$mvaField] ?? [];
$valueList[] = !empty($mvaData)
? '(' . implode(',', array_map('intval', $mvaData)) . ')'
: '()';
}
return [$fieldList, $valueList];
}
/** /**
* 通用向量删除方法 * 通用向量删除方法
* *
@ -1924,179 +2139,92 @@ class ManticoreBase
} }
/** /**
* 通用批量更新向量方法(高性能版本) * 通用批量写入方法:每块一条多行 REPLACE向量由引擎 Auto Embeddings 自动生成
* *
* 优化:将 N 条记录的 3N 次操作减少为 N+2 次操作 * 引擎对一条多行语句只调用一次向量化接口(实测 30 0.9s
* 1. 批量 SELECT 获取现有记录 (1) * 多行语句失败是原子的;整块失败时回退逐行 upsertVector
* 2. 预构建所有 INSERT SQL验证数据完整性 * 使单条坏行不毒化整批、且失败按真实主键记入重试表。
* 3. 批量 DELETE 删除旧记录 (1)
* 4. 逐条 INSERT 新记录带向量 (N次因向量字段无法批量绑定)
* *
* @param string $type 类型: msg/file/task/project/user * @param string $type 类型: msg/file/task/project/user
* @param array $vectorData 向量数据 [pk_value => vectorStr, ...] * @param array $rows 行数据数组(与 upsertVector $data 同构,需含 id 与主键)
* @return int 成功更新的数量 * @return int 成功写入的数量
*/ */
public static function batchUpdateVectors(string $type, array $vectorData): int public static function batchUpsertVectors(string $type, array $rows): int
{ {
if (empty($vectorData) || !isset(self::VECTOR_TABLE_CONFIG[$type])) { if (empty($rows) || !isset(self::VECTOR_TABLE_CONFIG[$type])) {
return 0; return 0;
} }
$config = self::VECTOR_TABLE_CONFIG[$type]; $config = self::VECTOR_TABLE_CONFIG[$type];
$table = $config['table']; $table = $config['table'];
$pk = $config['pk']; $pk = $config['pk'];
$fields = $config['fields'];
$mvaFields = $config['mva_fields'];
$instance = new self(); $instance = new self();
$ids = array_keys($vectorData);
// 1. 批量查询现有记录 // 剔除无主键行;按内容长度估算分块(不在此渲染 SQL块内惰性渲染以压低内存峰值
$placeholders = implode(',', array_fill(0, count($ids), '?')); $pending = [];
$existingRows = $instance->query( foreach ($rows as $row) {
"SELECT * FROM {$table} WHERE {$pk} IN ({$placeholders})", if (($row[$pk] ?? 0) <= 0) {
$ids
);
if (empty($existingRows)) {
return 0;
}
// 建立 pk => row 的映射
$existingMap = [];
foreach ($existingRows as $row) {
$existingMap[$row[$pk]] = $row;
}
$idsToUpdate = array_keys($existingMap);
if (empty($idsToUpdate)) {
return 0;
}
// 2. 预构建所有 INSERT 语句(在删除前验证数据完整性)
$insertStatements = [];
foreach ($idsToUpdate as $pkValue) {
$existing = $existingMap[$pkValue];
$vectorStr = $vectorData[$pkValue] ?? null;
if (empty($vectorStr)) {
continue; continue;
} }
$bytes = 64;
// Manticore 向量使用 () 格式 foreach ($row as $value) {
$vectorStr = str_replace(['[', ']'], ['(', ')'], $vectorStr); if (is_string($value)) {
$bytes += strlen($value);
// 构建字段列表和值(直接内联值,不使用参数绑定)
$fieldList = $fields;
$quotedValues = [];
foreach ($fields as $field) {
$value = $existing[$field] ?? null;
// 处理默认值:数值字段用 0时间戳字段用当前时间其他用空字符串
if ($value === null) {
if ($field === 'created_at') {
$value = time();
} elseif (in_array($field, self::NUMERIC_FIELDS)) {
$value = 0;
} else {
$value = '';
} }
} }
// 根据字段类型处理值 $pending[] = ['pk' => $row[$pk], 'bytes' => $bytes, 'row' => $row];
if (in_array($field, self::NUMERIC_FIELDS)) {
$quotedValues[] = (int)$value;
} else {
$quotedValues[] = $instance->quoteValue((string)$value);
} }
} if (empty($pending)) {
// 构建 MVA 字段
$mvaValuesStr = [];
foreach ($mvaFields as $mvaField) {
$fieldList[] = $mvaField;
$mvaValuesStr[] = !empty($existing[$mvaField])
? '(' . $existing[$mvaField] . ')'
: '()';
}
// 添加向量字段
$fieldList[] = 'content_vector';
// 构建 SQL所有值直接内联使用 executeRaw 避免 prepared statement 解析问题)
$allValues = implode(', ', array_merge($quotedValues, $mvaValuesStr, [$vectorStr]));
$sql = "INSERT INTO {$table} (" . implode(', ', $fieldList) . ") VALUES ({$allValues})";
$insertStatements[] = ['sql' => $sql, 'pk' => $pkValue];
}
// 如果没有有效的插入语句,直接返回
if (empty($insertStatements)) {
return 0; return 0;
} }
// 3. 批量删除旧记录(只删除有有效向量的记录) // 分块:行数上限 + 字节预算(文件内容可达 10 万字符/行)
$validPks = array_column($insertStatements, 'pk'); $chunks = [];
$deletePlaceholders = implode(',', array_fill(0, count($validPks), '?')); $current = [];
$instance->execute( $currentBytes = 0;
"DELETE FROM {$table} WHERE {$pk} IN ({$deletePlaceholders})", foreach ($pending as $item) {
$validPks if (!empty($current)
); && (count($current) >= self::BATCH_CHUNK_ROWS || $currentBytes + $item['bytes'] > self::BATCH_CHUNK_BYTES)) {
$chunks[] = $current;
$current = [];
$currentBytes = 0;
}
$current[] = $item;
$currentBytes += $item['bytes'];
}
$chunks[] = $current;
// 4. 逐条插入新记录(使用 executeRaw 避免 prepared statement 解析问题)
$successCount = 0; $successCount = 0;
foreach ($insertStatements as $stmt) { foreach ($chunks as $chunk) {
if ($instance->executeRaw($stmt['sql'])) { // 块内渲染,执行后即释放,内存峰值 = 原始行 + 单块 SQL
$successCount++; $fieldListRef = null;
// 成功则删除失败记录(如果有) $valuesSql = [];
ManticoreSyncFailure::removeSuccess($type, $stmt['pk'], 'sync'); foreach ($chunk as $item) {
[$fieldList, $valueList] = $instance->buildRowValues($config, $item['row']);
$fieldListRef = $fieldList;
$valuesSql[] = '(' . implode(', ', $valueList) . ')';
}
$sql = "REPLACE INTO {$table} (" . implode(', ', $fieldListRef) . ") VALUES "
. implode(', ', $valuesSql);
unset($valuesSql);
$ok = $instance->executeRaw($sql);
unset($sql);
if ($ok) {
$successCount += count($chunk);
ManticoreSyncFailure::removeSuccessBatch($type, array_column($chunk, 'pk'), 'sync');
} else { } else {
// 失败则记录 // 整块失败回退逐行REPLACE 幂等,重复写已生效行无害)
ManticoreSyncFailure::recordFailure($type, $stmt['pk'], 'sync', "Batch INSERT failed for {$table}"); foreach ($chunk as $item) {
if (self::upsertVector($type, $item['row'])) {
$successCount++;
}
}
} }
} }
return $successCount; return $successCount;
} }
/**
* 批量更新消息向量(兼容方法)
*/
public static function batchUpdateMsgVectors(array $vectorData): int
{
return self::batchUpdateVectors('msg', $vectorData);
}
/**
* 批量更新文件向量
*/
public static function batchUpdateFileVectors(array $vectorData): int
{
return self::batchUpdateVectors('file', $vectorData);
}
/**
* 批量更新任务向量
*/
public static function batchUpdateTaskVectors(array $vectorData): int
{
return self::batchUpdateVectors('task', $vectorData);
}
/**
* 批量更新项目向量
*/
public static function batchUpdateProjectVectors(array $vectorData): int
{
return self::batchUpdateVectors('project', $vectorData);
}
/**
* 批量更新用户向量
*/
public static function batchUpdateUserVectors(array $vectorData): int
{
return self::batchUpdateVectors('user', $vectorData);
}
// ============================== // ==============================
// 通用工具方法 // 通用工具方法
// ============================== // ==============================

View File

@ -8,7 +8,6 @@ use App\Models\FileUser;
use App\Module\Apps; use App\Module\Apps;
use App\Module\Base; use App\Module\Base;
use App\Module\TextExtractor; use App\Module\TextExtractor;
use App\Module\AI;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
/** /**
@ -38,7 +37,9 @@ class ManticoreFile
public const SEARCHABLE_TYPES = ['document', 'word', 'excel', 'ppt', 'txt', 'md', 'text', 'code']; public const SEARCHABLE_TYPES = ['document', 'word', 'excel', 'ppt', 'txt', 'md', 'text', 'code'];
/** /**
* 最大内容长度(字符)- 提取后的文本内容限制 * 最大内容长度(字符)- 提取后的文本内容限制(服务全文检索范围)。
* 注意:向量化输入在 ai 插件侧另有 30000 字符上限main.py _EMBEDDING_INPUT_MAX_CHARS
* 超出部分只参与全文检索、不参与语义向量。
*/ */
public const MAX_CONTENT_LENGTH = 100000; // 100K 字符 public const MAX_CONTENT_LENGTH = 100000; // 100K 字符
@ -214,13 +215,12 @@ class ManticoreFile
// ============================== // ==============================
/** /**
* 同步单个文件到 Manticore allowed_users * 同步单个文件到 Manticore allowed_users,向量由引擎 Auto Embeddings 自动生成
* *
* @param File $file 文件模型 * @param File $file 文件模型
* @param bool $withVector 是否同时生成向量(默认 false,向量由后台任务生成)
* @return bool 是否成功 * @return bool 是否成功
*/ */
public static function sync(File $file, bool $withVector = false): bool public static function sync(File $file): bool
{ {
if (!Apps::isInstalled("search")) { if (!Apps::isInstalled("search")) {
return false; return false;
@ -240,42 +240,7 @@ class ManticoreFile
} }
try { try {
// 提取文件内容 return ManticoreBase::upsertFileVector(self::buildRow($file));
$content = self::extractFileContent($file);
// 限制提取后的内容长度
$content = mb_substr($content, 0, self::MAX_CONTENT_LENGTH);
// 只有明确要求时才生成向量(默认不生成,由后台任务处理)
$embedding = null;
if ($withVector && Apps::isInstalled('ai')) {
// 向量内容包含文件名和文件内容
$vectorContent = self::buildVectorContent($file->name, $content);
if (!empty($vectorContent)) {
$embeddingResult = ManticoreBase::getEmbedding($vectorContent);
if (!empty($embeddingResult)) {
$embedding = '[' . implode(',', $embeddingResult) . ']';
}
}
}
// 获取文件的 allowed_users
$allowedUsers = self::getAllowedUsers($file);
// 写入 Manticore含 allowed_users
$result = ManticoreBase::upsertFileVector([
'file_id' => $file->id,
'userid' => $file->userid,
'pshare' => $file->pshare ?? 0,
'file_name' => $file->name,
'file_type' => $file->type,
'file_ext' => $file->ext,
'content' => $content,
'content_vector' => $embedding,
'allowed_users' => $allowedUsers,
]);
return $result;
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('Manticore sync error: ' . $e->getMessage(), [ Log::error('Manticore sync error: ' . $e->getMessage(), [
'file_id' => $file->id, 'file_id' => $file->id,
@ -285,6 +250,30 @@ class ManticoreFile
} }
} }
/**
* 构建文件索引行数据(含提取的文件内容与 allowed_users
*
* @param File $file 文件模型
* @return array 行数据
*/
private static function buildRow(File $file): array
{
// 提取文件内容并限制长度
$content = mb_substr(self::extractFileContent($file), 0, self::MAX_CONTENT_LENGTH);
return [
'id' => $file->id,
'file_id' => $file->id,
'userid' => $file->userid,
'pshare' => $file->pshare ?? 0,
'file_name' => $file->name,
'file_type' => $file->type,
'file_ext' => $file->ext,
'content' => $content,
'allowed_users' => self::getAllowedUsers($file),
];
}
/** /**
* 根据文件扩展名获取最大文件大小限制 * 根据文件扩展名获取最大文件大小限制
* *
@ -317,25 +306,41 @@ class ManticoreFile
} }
/** /**
* 批量同步文件 * 批量同步文件(每块一条多行 REPLACE向量由引擎自动生成
* *
* @param iterable $files 文件列表 * @param iterable $files 文件列表
* @param bool $withVector 是否同时生成向量
* @return int 成功同步的数量 * @return int 成功同步的数量
*/ */
public static function batchSync(iterable $files, bool $withVector = false): int public static function batchSync(iterable $files): int
{ {
if (!Apps::isInstalled("search")) { if (!Apps::isInstalled("search")) {
return 0; return 0;
} }
$count = 0; $count = 0;
$rows = [];
foreach ($files as $file) { foreach ($files as $file) {
if (self::sync($file, $withVector)) { // 文件夹不索引
if ($file->type === 'folder') {
$count++; $count++;
continue;
}
// 超限文件删除旧索引
if ($file->size > self::getMaxFileSizeByExt($file->ext)) {
self::delete($file->id);
$count++;
continue;
}
try {
$rows[] = self::buildRow($file);
} catch (\Exception $e) {
Log::error('Manticore file batchSync build error: ' . $e->getMessage(), [
'file_id' => $file->id,
]);
} }
} }
return $count;
return $count + ManticoreBase::batchUpsertVectors('file', $rows);
} }
/** /**
@ -497,27 +502,6 @@ class ManticoreFile
return $result['data'] ?? ''; return $result['data'] ?? '';
} }
/**
* 构建用于生成向量的内容
* 包含文件名和文件内容,确保语义搜索能匹配文件名
*
* @param string $fileName 文件名
* @param string $content 文件内容
* @return string 用于生成向量的文本
*/
private static function buildVectorContent(string $fileName, string $content): string
{
$parts = [];
if (!empty($fileName)) {
$parts[] = $fileName;
}
if (!empty($content)) {
$parts[] = $content;
}
return implode(' ', $parts);
}
/** /**
* 清空所有索引 * 清空所有索引
@ -577,94 +561,4 @@ class ManticoreFile
return false; return false;
} }
} }
// ==============================
// 批量向量生成方法
// ==============================
/**
* 批量生成文件向量
* 用于后台异步处理,将已索引文件的向量批量生成
*
* @param array $fileIds 文件ID数组
* @param int $batchSize 每批 embedding 数量默认20
* @return int 成功处理的数量
*/
public static function generateVectorsBatch(array $fileIds, int $batchSize = 20): int
{
if (!Apps::isInstalled("search") || !Apps::isInstalled("ai") || empty($fileIds)) {
return 0;
}
try {
// 1. 查询文件信息
$files = File::whereIn('id', $fileIds)
->where('type', '!=', 'folder')
->get();
if ($files->isEmpty()) {
return 0;
}
// 2. 提取每个文件的内容(包含文件名)
$fileContents = [];
foreach ($files as $file) {
// 检查文件大小限制
$maxSize = self::getMaxFileSizeByExt($file->ext);
if ($file->size > $maxSize) {
continue;
}
$content = self::extractFileContent($file);
// 向量内容包含文件名和文件内容
$vectorContent = self::buildVectorContent($file->name, $content);
if (!empty($vectorContent)) {
// 限制内容长度
$vectorContent = mb_substr($vectorContent, 0, self::MAX_CONTENT_LENGTH);
$fileContents[$file->id] = $vectorContent;
}
}
if (empty($fileContents)) {
return 0;
}
// 3. 分批处理
$successCount = 0;
$chunks = array_chunk($fileContents, $batchSize, true);
foreach ($chunks as $chunk) {
$texts = array_values($chunk);
$ids = array_keys($chunk);
// 4. 批量获取 embedding
$result = AI::getBatchEmbeddings($texts);
if (!Base::isSuccess($result) || empty($result['data'])) {
continue;
}
$embeddings = $result['data'];
// 5. 构建批量更新数据
$vectorData = [];
foreach ($ids as $index => $fileId) {
if (!isset($embeddings[$index]) || empty($embeddings[$index])) {
continue;
}
$vectorData[$fileId] = '[' . implode(',', $embeddings[$index]) . ']';
}
// 6. 批量更新向量
if (!empty($vectorData)) {
$batchCount = ManticoreBase::batchUpdateFileVectors($vectorData);
$successCount += $batchCount;
}
}
return $successCount;
} catch (\Exception $e) {
Log::error('ManticoreFile generateVectorsBatch error: ' . $e->getMessage());
return 0;
}
}
} }

View File

@ -5,8 +5,6 @@ namespace App\Module\Manticore;
use App\Models\WebSocketDialogMsg; use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialogUser; use App\Models\WebSocketDialogUser;
use App\Module\Apps; use App\Module\Apps;
use App\Module\Base;
use App\Module\AI;
use Carbon\Carbon; use Carbon\Carbon;
use DB; use DB;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
@ -39,7 +37,8 @@ class ManticoreMsg
public const INDEXABLE_TYPES = ['text', 'file', 'record', 'meeting', 'vote']; public const INDEXABLE_TYPES = ['text', 'file', 'record', 'meeting', 'vote'];
/** /**
* 最大内容长度(字符) * 最大内容长度(字符)。向量化输入在 ai 插件侧另有 30000 字符上限
* main.py _EMBEDDING_INPUT_MAX_CHARS超出部分只参与全文检索
*/ */
public const MAX_CONTENT_LENGTH = 50000; // 50K 字符 public const MAX_CONTENT_LENGTH = 50000; // 50K 字符
@ -328,13 +327,12 @@ class ManticoreMsg
// ============================== // ==============================
/** /**
* 同步单个消息到 Manticore allowed_users * 同步单个消息到 Manticore allowed_users,向量由引擎 Auto Embeddings 自动生成
* *
* @param WebSocketDialogMsg $msg 消息模型 * @param WebSocketDialogMsg $msg 消息模型
* @param bool $withVector 是否同时生成向量(默认 false,向量由后台任务生成)
* @return bool 是否成功 * @return bool 是否成功
*/ */
public static function sync(WebSocketDialogMsg $msg, bool $withVector = false): bool public static function sync(WebSocketDialogMsg $msg): bool
{ {
if (!Apps::isInstalled("search")) { if (!Apps::isInstalled("search")) {
return false; return false;
@ -347,37 +345,7 @@ class ManticoreMsg
} }
try { try {
// 提取消息内容(使用 key 字段) return ManticoreBase::upsertMsgVector(self::buildRow($msg));
$content = $msg->key ?? '';
// 限制内容长度
$content = mb_substr($content, 0, self::MAX_CONTENT_LENGTH);
// 只有明确要求时才生成向量(默认不生成,由后台任务处理)
$embedding = null;
if ($withVector && !empty($content) && Apps::isInstalled('ai')) {
$embeddingResult = ManticoreBase::getEmbedding($content);
if (!empty($embeddingResult)) {
$embedding = '[' . implode(',', $embeddingResult) . ']';
}
}
// 获取消息的 allowed_users
$allowedUsers = self::getAllowedUsers($msg);
// 写入 Manticore含 allowed_users
$result = ManticoreBase::upsertMsgVector([
'msg_id' => $msg->id,
'dialog_id' => $msg->dialog_id,
'userid' => $msg->userid,
'msg_type' => $msg->type,
'content' => $content,
'content_vector' => $embedding,
'allowed_users' => $allowedUsers,
'created_at' => $msg->created_at ? $msg->created_at->timestamp : time(),
]);
return $result;
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('Manticore msg sync error: ' . $e->getMessage(), [ Log::error('Manticore msg sync error: ' . $e->getMessage(), [
'msg_id' => $msg->id, 'msg_id' => $msg->id,
@ -388,106 +356,59 @@ class ManticoreMsg
} }
/** /**
* 批量同步消息 * 构建消息索引行数据(含 allowed_users
*
* @param WebSocketDialogMsg $msg 消息模型
* @return array 行数据
*/
private static function buildRow(WebSocketDialogMsg $msg): array
{
// 提取消息内容(使用 key 字段)并限制长度
$content = mb_substr($msg->key ?? '', 0, self::MAX_CONTENT_LENGTH);
return [
'id' => $msg->id,
'msg_id' => $msg->id,
'dialog_id' => $msg->dialog_id,
'userid' => $msg->userid,
'msg_type' => $msg->type,
'content' => $content,
'allowed_users' => self::getAllowedUsers($msg),
'created_at' => $msg->created_at ? $msg->created_at->timestamp : time(),
];
}
/**
* 批量同步消息(每块一条多行 REPLACE向量由引擎自动生成
* *
* @param iterable $msgs 消息列表 * @param iterable $msgs 消息列表
* @param bool $withVector 是否同时生成向量
* @return int 成功同步的数量 * @return int 成功同步的数量
*/ */
public static function batchSync(iterable $msgs, bool $withVector = false): int public static function batchSync(iterable $msgs): int
{ {
if (!Apps::isInstalled("search")) { if (!Apps::isInstalled("search")) {
return 0; return 0;
} }
$count = 0; $count = 0;
$rows = [];
foreach ($msgs as $msg) { foreach ($msgs as $msg) {
if (self::sync($msg, $withVector)) { if (!self::shouldIndex($msg)) {
if (ManticoreBase::deleteMsgVector($msg->id)) {
$count++; $count++;
} }
}
return $count;
}
/**
* 批量生成向量(供后台任务调用)
*
* @param array $msgIds 消息ID数组
* @param int $batchSize 每批 embedding 数量
* @return int 成功生成向量的数量
*/
public static function generateVectorsBatch(array $msgIds, int $batchSize = 20): int
{
if (!Apps::isInstalled("search") || !Apps::isInstalled('ai') || empty($msgIds)) {
return 0;
}
$count = 0;
// 分批处理
foreach (array_chunk($msgIds, $batchSize) as $batchIds) {
// 获取消息
$msgs = WebSocketDialogMsg::whereIn('id', $batchIds)
->whereIn('type', self::INDEXABLE_TYPES)
->where('bot', '!=', 1)
->whereNotNull('key')
->where('key', '!=', '')
->get()
->keyBy('id');
if ($msgs->isEmpty()) {
continue; continue;
} }
try {
// 准备文本 $rows[] = self::buildRow($msg);
$texts = []; } catch (\Exception $e) {
$idsArray = []; Log::error('Manticore msg batchSync build error: ' . $e->getMessage(), [
foreach ($batchIds as $id) { 'msg_id' => $msg->id,
if (isset($msgs[$id])) { ]);
$content = mb_substr($msgs[$id]->key ?? '', 0, self::MAX_CONTENT_LENGTH);
if (!empty($content)) {
$texts[] = $content;
$idsArray[] = $id;
}
}
}
if (empty($texts)) {
continue;
}
// 批量获取 embeddings
$result = AI::getBatchEmbeddings($texts);
if (Base::isError($result)) {
continue;
}
$embeddings = $result['data'] ?? [];
// 构建批量更新数据 [msg_id => vectorStr]
$vectorData = [];
foreach ($embeddings as $index => $embedding) {
if (empty($embedding) || !is_array($embedding)) {
continue;
}
$msgId = $idsArray[$index] ?? null;
if (!$msgId) {
continue;
}
$vectorData[$msgId] = '[' . implode(',', $embedding) . ']';
}
// 批量更新向量(优化:减少数据库操作次数)
if (!empty($vectorData)) {
$batchCount = ManticoreBase::batchUpdateMsgVectors($vectorData);
$count += $batchCount;
} }
} }
return $count; return $count + ManticoreBase::batchUpsertVectors('msg', $rows);
} }
/** /**

View File

@ -5,8 +5,6 @@ namespace App\Module\Manticore;
use App\Models\Project; use App\Models\Project;
use App\Models\ProjectUser; use App\Models\ProjectUser;
use App\Module\Apps; use App\Module\Apps;
use App\Module\Base;
use App\Module\AI;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
/** /**
@ -122,13 +120,12 @@ class ManticoreProject
} }
/** /**
* 同步单个项目到 Manticore allowed_users * 同步单个项目到 Manticore allowed_users,向量由引擎 Auto Embeddings 自动生成
* *
* @param Project $project 项目模型 * @param Project $project 项目模型
* @param bool $withVector 是否同时生成向量(默认 false,向量由后台任务生成)
* @return bool 是否成功 * @return bool 是否成功
*/ */
public static function sync(Project $project, bool $withVector = false): bool public static function sync(Project $project): bool
{ {
if (!Apps::isInstalled("search")) { if (!Apps::isInstalled("search")) {
return false; return false;
@ -140,33 +137,7 @@ class ManticoreProject
} }
try { try {
// 构建用于搜索的文本内容 return ManticoreBase::upsertProjectVector(self::buildRow($project));
$searchableContent = self::buildSearchableContent($project);
// 只有明确要求时才生成向量(默认不生成,由后台任务处理)
$embedding = null;
if ($withVector && !empty($searchableContent) && Apps::isInstalled('ai')) {
$embeddingResult = ManticoreBase::getEmbedding($searchableContent);
if (!empty($embeddingResult)) {
$embedding = '[' . implode(',', $embeddingResult) . ']';
}
}
// 获取项目成员列表(作为 allowed_users
$allowedUsers = self::getAllowedUsers($project->id);
// 写入 Manticore含 allowed_users
$result = ManticoreBase::upsertProjectVector([
'project_id' => $project->id,
'userid' => $project->userid ?? 0,
'personal' => $project->personal ?? 0,
'project_name' => $project->name ?? '',
'project_desc' => $project->desc ?? '',
'content_vector' => $embedding,
'allowed_users' => $allowedUsers,
]);
return $result;
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('Manticore project sync error: ' . $e->getMessage(), [ Log::error('Manticore project sync error: ' . $e->getMessage(), [
'project_id' => $project->id, 'project_id' => $project->id,
@ -177,45 +148,55 @@ class ManticoreProject
} }
/** /**
* 构建可搜索的文本内容 * 构建项目索引行数据(含 allowed_users
* *
* @param Project $project 项目模型 * @param Project $project 项目模型
* @return string 可搜索的文本 * @return array 行数据
*/ */
private static function buildSearchableContent(Project $project): string private static function buildRow(Project $project): array
{ {
$parts = []; return [
'id' => $project->id,
if (!empty($project->name)) { 'project_id' => $project->id,
$parts[] = $project->name; 'userid' => $project->userid ?? 0,
} 'personal' => $project->personal ?? 0,
if (!empty($project->desc)) { 'project_name' => $project->name ?? '',
$parts[] = $project->desc; 'project_desc' => $project->desc ?? '',
} 'allowed_users' => self::getAllowedUsers($project->id),
];
return implode(' ', $parts);
} }
/** /**
* 批量同步项目 * 批量同步项目(每块一条多行 REPLACE向量由引擎自动生成
* *
* @param iterable $projects 项目列表 * @param iterable $projects 项目列表
* @param bool $withVector 是否同时生成向量
* @return int 成功同步的数量 * @return int 成功同步的数量
*/ */
public static function batchSync(iterable $projects, bool $withVector = false): int public static function batchSync(iterable $projects): int
{ {
if (!Apps::isInstalled("search")) { if (!Apps::isInstalled("search")) {
return 0; return 0;
} }
$count = 0; $count = 0;
$rows = [];
foreach ($projects as $project) { foreach ($projects as $project) {
if (self::sync($project, $withVector)) { if ($project->archived_at) {
if (self::delete($project->id)) {
$count++; $count++;
} }
continue;
} }
return $count; try {
$rows[] = self::buildRow($project);
} catch (\Exception $e) {
Log::error('Manticore project batchSync build error: ' . $e->getMessage(), [
'project_id' => $project->id,
]);
}
}
return $count + ManticoreBase::batchUpsertVectors('project', $rows);
} }
/** /**
@ -286,84 +267,4 @@ class ManticoreProject
return false; return false;
} }
} }
// ==============================
// 批量向量生成方法
// ==============================
/**
* 批量生成项目向量
* 用于后台异步处理,将已索引项目的向量批量生成
*
* @param array $projectIds 项目ID数组
* @param int $batchSize 每批 embedding 数量默认20
* @return int 成功处理的数量
*/
public static function generateVectorsBatch(array $projectIds, int $batchSize = 20): int
{
if (!Apps::isInstalled("search") || !Apps::isInstalled("ai") || empty($projectIds)) {
return 0;
}
try {
// 1. 查询项目信息
$projects = Project::whereIn('id', $projectIds)
->whereNull('archived_at')
->get();
if ($projects->isEmpty()) {
return 0;
}
// 2. 提取每个项目的内容
$projectContents = [];
foreach ($projects as $project) {
$searchableContent = self::buildSearchableContent($project);
if (!empty($searchableContent)) {
$projectContents[$project->id] = $searchableContent;
}
}
if (empty($projectContents)) {
return 0;
}
// 3. 分批处理
$successCount = 0;
$chunks = array_chunk($projectContents, $batchSize, true);
foreach ($chunks as $chunk) {
$texts = array_values($chunk);
$ids = array_keys($chunk);
// 4. 批量获取 embedding
$result = AI::getBatchEmbeddings($texts);
if (!Base::isSuccess($result) || empty($result['data'])) {
continue;
}
$embeddings = $result['data'];
// 5. 构建批量更新数据
$vectorData = [];
foreach ($ids as $index => $projectId) {
if (!isset($embeddings[$index]) || empty($embeddings[$index])) {
continue;
}
$vectorData[$projectId] = '[' . implode(',', $embeddings[$index]) . ']';
}
// 6. 批量更新向量
if (!empty($vectorData)) {
$batchCount = ManticoreBase::batchUpdateProjectVectors($vectorData);
$successCount += $batchCount;
}
}
return $successCount;
} catch (\Exception $e) {
Log::error('ManticoreProject generateVectorsBatch error: ' . $e->getMessage());
return 0;
}
}
} }

View File

@ -9,7 +9,6 @@ use App\Models\ProjectTaskVisibilityUser;
use App\Models\ProjectUser; use App\Models\ProjectUser;
use App\Module\Apps; use App\Module\Apps;
use App\Module\Base; use App\Module\Base;
use App\Module\AI;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
/** /**
@ -42,7 +41,8 @@ use Illuminate\Support\Facades\Log;
class ManticoreTask class ManticoreTask
{ {
/** /**
* 最大内容长度(字符) * 最大内容长度(字符)。向量化输入在 ai 插件侧另有 30000 字符上限
* main.py _EMBEDDING_INPUT_MAX_CHARS超出部分只参与全文检索
*/ */
public const MAX_CONTENT_LENGTH = 50000; // 50K 字符 public const MAX_CONTENT_LENGTH = 50000; // 50K 字符
@ -189,13 +189,12 @@ class ManticoreTask
// ============================== // ==============================
/** /**
* 同步单个任务到 Manticore allowed_users * 同步单个任务到 Manticore allowed_users,向量由引擎 Auto Embeddings 自动生成
* *
* @param ProjectTask $task 任务模型 * @param ProjectTask $task 任务模型
* @param bool $withVector 是否同时生成向量(默认 false,向量由后台任务生成)
* @return bool 是否成功 * @return bool 是否成功
*/ */
public static function sync(ProjectTask $task, bool $withVector = false): bool public static function sync(ProjectTask $task): bool
{ {
if (!Apps::isInstalled("search")) { if (!Apps::isInstalled("search")) {
return false; return false;
@ -207,38 +206,7 @@ class ManticoreTask
} }
try { try {
// 获取任务详细内容 return ManticoreBase::upsertTaskVector(self::buildRow($task));
$taskContent = self::getTaskContent($task);
// 构建用于搜索的文本内容
$searchableContent = self::buildSearchableContent($task, $taskContent);
// 只有明确要求时才生成向量(默认不生成,由后台任务处理)
$embedding = null;
if ($withVector && !empty($searchableContent) && Apps::isInstalled('ai')) {
$embeddingResult = ManticoreBase::getEmbedding($searchableContent);
if (!empty($embeddingResult)) {
$embedding = '[' . implode(',', $embeddingResult) . ']';
}
}
// 获取任务的 allowed_users
$allowedUsers = self::getAllowedUsers($task);
// 写入 Manticore含 allowed_users
$result = ManticoreBase::upsertTaskVector([
'task_id' => $task->id,
'project_id' => $task->project_id ?? 0,
'userid' => $task->userid ?? 0,
'visibility' => $task->visibility ?? 1,
'task_name' => $task->name ?? '',
'task_desc' => $task->desc ?? '',
'task_content' => $taskContent,
'content_vector' => $embedding,
'allowed_users' => $allowedUsers,
]);
return $result;
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('Manticore task sync error: ' . $e->getMessage(), [ Log::error('Manticore task sync error: ' . $e->getMessage(), [
'task_id' => $task->id, 'task_id' => $task->id,
@ -248,6 +216,27 @@ class ManticoreTask
} }
} }
/**
* 构建任务索引行数据(含详细内容与 allowed_users
*
* @param ProjectTask $task 任务模型
* @return array 行数据
*/
private static function buildRow(ProjectTask $task): array
{
return [
'id' => $task->id,
'task_id' => $task->id,
'project_id' => $task->project_id ?? 0,
'userid' => $task->userid ?? 0,
'visibility' => $task->visibility ?? 1,
'task_name' => $task->name ?? '',
'task_desc' => $task->desc ?? '',
'task_content' => self::getTaskContent($task),
'allowed_users' => self::getAllowedUsers($task),
];
}
/** /**
* 获取任务详细内容 * 获取任务详细内容
* *
@ -311,49 +300,36 @@ class ManticoreTask
} }
/** /**
* 构建可搜索的文本内容 * 批量同步任务(每块一条多行 REPLACE向量由引擎自动生成
*
* @param ProjectTask $task 任务模型
* @param string $taskContent 任务详细内容
* @return string 可搜索的文本
*/
private static function buildSearchableContent(ProjectTask $task, string $taskContent): string
{
$parts = [];
if (!empty($task->name)) {
$parts[] = $task->name;
}
if (!empty($task->desc)) {
$parts[] = $task->desc;
}
if (!empty($taskContent)) {
$parts[] = $taskContent;
}
return implode(' ', $parts);
}
/**
* 批量同步任务
* *
* @param iterable $tasks 任务列表 * @param iterable $tasks 任务列表
* @param bool $withVector 是否同时生成向量
* @return int 成功同步的数量 * @return int 成功同步的数量
*/ */
public static function batchSync(iterable $tasks, bool $withVector = false): int public static function batchSync(iterable $tasks): int
{ {
if (!Apps::isInstalled("search")) { if (!Apps::isInstalled("search")) {
return 0; return 0;
} }
$count = 0; $count = 0;
$rows = [];
foreach ($tasks as $task) { foreach ($tasks as $task) {
if (self::sync($task, $withVector)) { if ($task->archived_at || $task->deleted_at) {
if (self::delete($task->id)) {
$count++; $count++;
} }
continue;
} }
return $count; try {
$rows[] = self::buildRow($task);
} catch (\Exception $e) {
Log::error('Manticore task batchSync build error: ' . $e->getMessage(), [
'task_id' => $task->id,
]);
}
}
return $count + ManticoreBase::batchUpsertVectors('task', $rows);
} }
/** /**
@ -506,88 +482,4 @@ class ManticoreTask
Log::error('Manticore cascadeToChildren error: ' . $e->getMessage(), ['task_id' => $taskId]); Log::error('Manticore cascadeToChildren error: ' . $e->getMessage(), ['task_id' => $taskId]);
} }
} }
// ==============================
// 批量向量生成方法
// ==============================
/**
* 批量生成任务向量
* 用于后台异步处理,将已索引任务的向量批量生成
*
* @param array $taskIds 任务ID数组
* @param int $batchSize 每批 embedding 数量默认20
* @return int 成功处理的数量
*/
public static function generateVectorsBatch(array $taskIds, int $batchSize = 20): int
{
if (!Apps::isInstalled("search") || !Apps::isInstalled("ai") || empty($taskIds)) {
return 0;
}
try {
// 1. 查询任务信息
$tasks = ProjectTask::whereIn('id', $taskIds)
->whereNull('deleted_at')
->whereNull('archived_at')
->get();
if ($tasks->isEmpty()) {
return 0;
}
// 2. 提取每个任务的内容
$taskContents = [];
foreach ($tasks as $task) {
$taskContent = self::getTaskContent($task);
$searchableContent = self::buildSearchableContent($task, $taskContent);
if (!empty($searchableContent)) {
// 限制内容长度
$searchableContent = mb_substr($searchableContent, 0, self::MAX_CONTENT_LENGTH);
$taskContents[$task->id] = $searchableContent;
}
}
if (empty($taskContents)) {
return 0;
}
// 3. 分批处理
$successCount = 0;
$chunks = array_chunk($taskContents, $batchSize, true);
foreach ($chunks as $chunk) {
$texts = array_values($chunk);
$ids = array_keys($chunk);
// 4. 批量获取 embedding
$result = AI::getBatchEmbeddings($texts);
if (!Base::isSuccess($result) || empty($result['data'])) {
continue;
}
$embeddings = $result['data'];
// 5. 构建批量更新数据
$vectorData = [];
foreach ($ids as $index => $taskId) {
if (!isset($embeddings[$index]) || empty($embeddings[$index])) {
continue;
}
$vectorData[$taskId] = '[' . implode(',', $embeddings[$index]) . ']';
}
// 6. 批量更新向量
if (!empty($vectorData)) {
$batchCount = ManticoreBase::batchUpdateTaskVectors($vectorData);
$successCount += $batchCount;
}
}
return $successCount;
} catch (\Exception $e) {
Log::error('ManticoreTask generateVectorsBatch error: ' . $e->getMessage());
return 0;
}
}
} }

View File

@ -5,8 +5,6 @@ namespace App\Module\Manticore;
use App\Models\User; use App\Models\User;
use App\Models\UserTag; use App\Models\UserTag;
use App\Module\Apps; use App\Module\Apps;
use App\Module\Base;
use App\Module\AI;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
/** /**
@ -123,13 +121,12 @@ class ManticoreUser
} }
/** /**
* 同步单个用户到 Manticore * 同步单个用户到 Manticore(向量由引擎 Auto Embeddings 按行内文本自动生成)
* *
* @param User $user 用户模型 * @param User $user 用户模型
* @param bool $withVector 是否同时生成向量(默认 false,向量由后台任务生成)
* @return bool 是否成功 * @return bool 是否成功
*/ */
public static function sync(User $user, bool $withVector = false): bool public static function sync(User $user): bool
{ {
if (!Apps::isInstalled("search")) { if (!Apps::isInstalled("search")) {
return false; return false;
@ -146,33 +143,13 @@ class ManticoreUser
} }
try { try {
// 获取用户标签Top 10 $row = self::buildRow($user);
$tags = self::getUserTags($user->userid); // 脏检查:与已索引行完全一致则跳过。标签点赞/识别等高频事件经常不改变
// Top-10 标签文本,跳过可省一次真实的向量化调用与整行重写
// 构建用于搜索的文本内容 if (self::rowUnchanged($row)) {
$searchableContent = self::buildSearchableContent($user, $tags); return true;
// 只有明确要求时才生成向量(默认不生成,由后台任务处理)
$embedding = null;
if ($withVector && !empty($searchableContent) && Apps::isInstalled('ai')) {
$embeddingResult = ManticoreBase::getEmbedding($searchableContent);
if (!empty($embeddingResult)) {
$embedding = '[' . implode(',', $embeddingResult) . ']';
} }
} return ManticoreBase::upsertUserVector($row);
// 写入 Manticore
$result = ManticoreBase::upsertUserVector([
'userid' => $user->userid,
'nickname' => $user->nickname ?? '',
'email' => $user->email ?? '',
'profession' => $user->profession ?? '',
'tags' => $tags,
'introduction' => $user->introduction ?? '',
'content_vector' => $embedding,
]);
return $result;
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('Manticore user sync error: ' . $e->getMessage(), [ Log::error('Manticore user sync error: ' . $e->getMessage(), [
'userid' => $user->userid, 'userid' => $user->userid,
@ -183,55 +160,81 @@ class ManticoreUser
} }
/** /**
* 构建可搜索的文本内容 * 判断待写入行与当前已索引行是否完全一致(文本字段逐一比较)
*
* @param User $user 用户模型
* @param string $tags 用户标签(空格分隔)
* @return string 可搜索的文本
*/ */
private static function buildSearchableContent(User $user, string $tags = ''): string private static function rowUnchanged(array $row): bool
{ {
$parts = []; $existing = (new ManticoreBase())->queryOne(
"SELECT nickname, email, profession, tags, introduction FROM user_vectors WHERE userid = ?",
if (!empty($user->nickname)) { [$row['userid']]
$parts[] = $user->nickname; );
if (!$existing) {
return false;
} }
if (!empty($user->email)) { foreach (['nickname', 'email', 'profession', 'tags', 'introduction'] as $field) {
$parts[] = $user->email; if ((string) ($existing[$field] ?? '') !== (string) $row[$field]) {
return false;
} }
if (!empty($user->profession)) {
$parts[] = $user->profession;
} }
if (!empty($tags)) { return true;
$parts[] = $tags;
}
if (!empty($user->introduction)) {
$parts[] = $user->introduction;
}
return implode(' ', $parts);
} }
/** /**
* 批量同步用户 * 构建用户索引行数据(含标签 Top 10
*
* @param User $user 用户模型
* @return array 行数据
*/
private static function buildRow(User $user): array
{
$tags = self::getUserTags($user->userid);
return [
'id' => $user->userid,
'userid' => $user->userid,
'nickname' => $user->nickname ?? '',
'email' => $user->email ?? '',
'profession' => $user->profession ?? '',
'tags' => $tags,
'introduction' => $user->introduction ?? '',
];
}
/**
* 批量同步用户(每块一条多行 REPLACE向量由引擎自动生成
* *
* @param iterable $users 用户列表 * @param iterable $users 用户列表
* @param bool $withVector 是否同时生成向量
* @return int 成功同步的数量 * @return int 成功同步的数量
*/ */
public static function batchSync(iterable $users, bool $withVector = false): int public static function batchSync(iterable $users): int
{ {
if (!Apps::isInstalled("search")) { if (!Apps::isInstalled("search")) {
return 0; return 0;
} }
$count = 0; $count = 0;
$rows = [];
foreach ($users as $user) { foreach ($users as $user) {
if (self::sync($user, $withVector)) { if ($user->bot) {
$count++;
continue;
}
if ($user->disable_at) {
if (self::delete($user->userid)) {
$count++; $count++;
} }
continue;
} }
return $count; try {
$rows[] = self::buildRow($user);
} catch (\Exception $e) {
Log::error('Manticore user batchSync build error: ' . $e->getMessage(), [
'userid' => $user->userid,
]);
}
}
return $count + ManticoreBase::batchUpsertVectors('user', $rows);
} }
/** /**
@ -276,87 +279,4 @@ class ManticoreUser
return ManticoreBase::getIndexedUserCount(); return ManticoreBase::getIndexedUserCount();
} }
// ==============================
// 批量向量生成方法
// ==============================
/**
* 批量生成用户向量
* 用于后台异步处理,将已索引用户的向量批量生成
*
* @param array $userIds 用户ID数组
* @param int $batchSize 每批 embedding 数量默认20
* @return int 成功处理的数量
*/
public static function generateVectorsBatch(array $userIds, int $batchSize = 20): int
{
if (!Apps::isInstalled("search") || !Apps::isInstalled("ai") || empty($userIds)) {
return 0;
} }
try {
// 1. 查询用户信息
$users = User::whereIn('userid', $userIds)
->where('bot', 0)
->whereNull('disable_at')
->get();
if ($users->isEmpty()) {
return 0;
}
// 2. 提取每个用户的内容(包含标签)
$userContents = [];
foreach ($users as $user) {
$tags = self::getUserTags($user->userid);
$searchableContent = self::buildSearchableContent($user, $tags);
if (!empty($searchableContent)) {
$userContents[$user->userid] = $searchableContent;
}
}
if (empty($userContents)) {
return 0;
}
// 3. 分批处理
$successCount = 0;
$chunks = array_chunk($userContents, $batchSize, true);
foreach ($chunks as $chunk) {
$texts = array_values($chunk);
$ids = array_keys($chunk);
// 4. 批量获取 embedding
$result = AI::getBatchEmbeddings($texts);
if (!Base::isSuccess($result) || empty($result['data'])) {
continue;
}
$embeddings = $result['data'];
// 5. 构建批量更新数据
$vectorData = [];
foreach ($ids as $index => $userid) {
if (!isset($embeddings[$index]) || empty($embeddings[$index])) {
continue;
}
$vectorData[$userid] = '[' . implode(',', $embeddings[$index]) . ']';
}
// 6. 批量更新向量
if (!empty($vectorData)) {
$batchCount = ManticoreBase::batchUpdateUserVectors($vectorData);
$successCount += $batchCount;
}
}
return $successCount;
} catch (\Exception $e) {
Log::error('ManticoreUser generateVectorsBatch error: ' . $e->getMessage());
return 0;
}
}
}

View File

@ -235,11 +235,8 @@ class ManticoreSyncTask extends AbstractTask
} }
Cache::put("ManticoreSyncTask:CheckTime", time(), Carbon::now()->addMinutes(5)); Cache::put("ManticoreSyncTask:CheckTime", time(), Carbon::now()->addMinutes(5));
// 执行增量全文索引同步 // 执行增量全文索引同步(向量由 Manticore Auto Embeddings 随行自动生成)
$this->runIncrementalSync(); $this->runIncrementalSync();
// 执行向量生成
$this->runVectorGeneration();
} }
/** /**
@ -261,22 +258,6 @@ class ManticoreSyncTask extends AbstractTask
@shell_exec("php /var/www/artisan manticore:retry-failures 2>&1 &"); @shell_exec("php /var/www/artisan manticore:retry-failures 2>&1 &");
} }
/**
* 执行向量生成(兜底触发)
*
* 命令内部有锁机制,如果已在运行会自动跳过
* 命令会持续处理直到无待处理数据,然后自动退出
*/
private function runVectorGeneration(): void
{
if (!Apps::isInstalled("ai")) {
return;
}
// 启动向量生成命令
@shell_exec("php /var/www/artisan manticore:generate-vectors --type=all --batch=50 2>&1 &");
}
public function end() public function end()
{ {
} }

71
cmd
View File

@ -63,6 +63,8 @@ msg() {
"端口 (*) 已被占用,请指定其他端口") out="Port (*) is already in use, please specify another port" ;; "端口 (*) 已被占用,请指定其他端口") out="Port (*) is already in use, please specify another port" ;;
"目录权限检测失败!请检查目录权限设置") out="Directory permission check failed! Please check directory permissions" ;; "目录权限检测失败!请检查目录权限设置") out="Directory permission check failed! Please check directory permissions" ;;
"目录【(*)】权限不足!") out="Directory [(*)] is not writable!" ;; "目录【(*)】权限不足!") out="Directory [(*)] is not writable!" ;;
"项目权限修复失败") out="Failed to repair project permissions" ;;
"项目权限修复完成") out="Project permissions repaired" ;;
"安装依赖失败") out="Failed to install dependencies" ;; "安装依赖失败") out="Failed to install dependencies" ;;
"安装依赖失败,请重试!") out="Failed to install dependencies, please retry!" ;; "安装依赖失败,请重试!") out="Failed to install dependencies, please retry!" ;;
"生成密钥失败") out="Failed to generate app key" ;; "生成密钥失败") out="Failed to generate app key" ;;
@ -227,6 +229,49 @@ check_sudo() {
fi fi
} }
# 修复项目权限
permission_fix() {
local owner_uid="${SUDO_UID:-$(id -u)}"
local owner_gid="${SUDO_GID:-$(id -g)}"
volumes=(
"bootstrap/cache"
"docker"
"public"
"storage"
)
chmod 755 "${WORK_DIR}"
if [ $? -ne 0 ]; then
error "$(msg '项目权限修复失败')"
exit 1
fi
for vol in "${volumes[@]}"; do
tmp_path="${WORK_DIR}/${vol}"
mkdir -p "${tmp_path}"
if [ $? -ne 0 ]; then
error "$(msg '项目权限修复失败')"
exit 1
fi
chown -R "${owner_uid}:${owner_gid}" "${tmp_path}"
if [ $? -ne 0 ]; then
error "$(msg '项目权限修复失败')"
exit 1
fi
find "${tmp_path}" -type d -exec chmod 775 {} \;
if [ $? -ne 0 ]; then
error "$(msg '项目权限修复失败')"
exit 1
fi
done
find "${WORK_DIR}/public" -type f -exec chmod a+r {} \;
if [ $? -ne 0 ]; then
error "$(msg '项目权限修复失败')"
exit 1
fi
}
# 检查docker、docker-compose # 检查docker、docker-compose
check_docker() { check_docker() {
docker --version &> /dev/null docker --version &> /dev/null
@ -585,6 +630,7 @@ DooTask 管理脚本
install 安装 DooTask (支持 --port <端口> --relock) install 安装 DooTask (支持 --port <端口> --relock)
update 更新 DooTask (支持 --branch <分支> --force --local) update 更新 DooTask (支持 --branch <分支> --force --local)
uninstall 卸载 DooTask uninstall 卸载 DooTask
permission 修复整个项目的权限
⚙️ 配置管理: ⚙️ 配置管理:
port <端口> 修改服务端口 port <端口> 修改服务端口
@ -621,6 +667,7 @@ DooTask 管理脚本
示例: 示例:
./cmd install --port 8080 安装并指定端口 8080 ./cmd install --port 8080 安装并指定端口 8080
./cmd update --branch dev 切换到 dev 分支并更新 ./cmd update --branch dev 切换到 dev 分支并更新
./cmd permission 修复整个项目的权限
./cmd mysql backup 备份数据库 ./cmd mysql backup 备份数据库
./cmd artisan migrate 执行数据库迁移 ./cmd artisan migrate 执行数据库迁移
EOF EOF
@ -634,6 +681,7 @@ Usage: ./cmd <command> [options]
install Install DooTask (supports --port <port> --relock) install Install DooTask (supports --port <port> --relock)
update Update DooTask (supports --branch <branch> --force --local) update Update DooTask (supports --branch <branch> --force --local)
uninstall Uninstall DooTask uninstall Uninstall DooTask
permission Repair permissions for the whole project
⚙️ Configuration: ⚙️ Configuration:
port <port> Change service port port <port> Change service port
@ -670,6 +718,7 @@ Usage: ./cmd <command> [options]
Examples: Examples:
./cmd install --port 8080 Install on port 8080 ./cmd install --port 8080 Install on port 8080
./cmd update --branch dev Switch to dev branch and update ./cmd update --branch dev Switch to dev branch and update
./cmd permission Repair permissions for the whole project
./cmd mysql backup Back up database ./cmd mysql backup Back up database
./cmd artisan migrate Run database migration ./cmd artisan migrate Run database migration
EOF EOF
@ -714,20 +763,12 @@ handle_install() {
rm -rf node_modules package-lock.json vendor composer.lock rm -rf node_modules package-lock.json vendor composer.lock
fi fi
# 目录权限设置 # 目录和静态文件权限设置
volumes=( permission_fix
"bootstrap/cache"
"docker"
"public"
"storage"
)
cmda="" cmda=""
cmdb="" cmdb=""
for vol in "${volumes[@]}"; do for vol in "${volumes[@]}"; do
tmp_path="${WORK_DIR}/${vol}" tmp_path="${WORK_DIR}/${vol}"
mkdir -p "${tmp_path}"
find "${tmp_path}" -type d -exec chmod 775 {} \;
rm -f "${tmp_path}/dootask.lock" rm -f "${tmp_path}/dootask.lock"
cmda="${cmda} -v ${tmp_path}:/usr/share/${vol}" cmda="${cmda} -v ${tmp_path}:/usr/share/${vol}"
cmdb="${cmdb} touch /usr/share/${vol}/dootask.lock &&" cmdb="${cmdb} touch /usr/share/${vol}/dootask.lock &&"
@ -962,8 +1003,8 @@ if [[ "$1" == "help" ]] || [[ "$1" == "--help" ]] || [[ "$1" == "-h" ]] || [[ $#
exit 0 exit 0
fi fi
# 非electron命令需要检查Docker环境 # 非electron和permission命令需要检查Docker环境
if [[ "$1" != "electron" ]]; then if [[ "$1" != "electron" ]] && [[ "$1" != "permission" ]]; then
check_docker check_docker
env_init env_init
fi fi
@ -982,6 +1023,12 @@ case "$1" in
shift 1 shift 1
handle_uninstall handle_uninstall
;; ;;
"permission")
shift 1
check_sudo
permission_fix
success "$(msg '项目权限修复完成')"
;;
"port") "port")
shift 1 shift 1
check_port "$1" "$(env_get APP_PORT)" check_port "$1" "$(env_get APP_PORT)"

View File

@ -26,6 +26,12 @@ return [
// Manticore 全文搜索服务端口ManticoreBase // Manticore 全文搜索服务端口ManticoreBase
'search_port' => env('SEARCH_PORT', 9306), 'search_port' => env('SEARCH_PORT', 9306),
// AI 插件服务主机AI::getEmbedding 走 ai 插件 /embeddings 免费向量模型)
'ai_host' => env('AI_HOST', 'ai'),
// AI 插件服务端口AI::getEmbedding
'ai_port' => env('AI_PORT', 5001),
// 文件回收站自动清空天数DeleteTmpTask // 文件回收站自动清空天数DeleteTmpTask
'auto_empty_file_recycle' => env('AUTO_EMPTY_FILE_RECYCLE', 365), 'auto_empty_file_recycle' => env('AUTO_EMPTY_FILE_RECYCLE', 365),

View File

@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::table('project_tasks', function (Blueprint $table) {
$table->index(
['project_id', 'parent_id', 'visibility', 'archived_at', 'deleted_at', 'complete_at', 'end_at'],
'idx_pt_dashboard_due'
);
$table->index(
['project_id', 'parent_id', 'visibility', 'archived_at', 'deleted_at', 'complete_at', 'p_level'],
'idx_pt_dashboard_priority'
);
});
Schema::table('project_task_users', function (Blueprint $table) {
$table->index(['task_id', 'owner', 'userid'], 'idx_ptu_task_owner_user');
$table->index(['userid', 'owner', 'task_id'], 'idx_ptu_user_owner_task');
});
}
public function down()
{
Schema::table('project_tasks', function (Blueprint $table) {
$table->dropIndex('idx_pt_dashboard_due');
$table->dropIndex('idx_pt_dashboard_priority');
});
Schema::table('project_task_users', function (Blueprint $table) {
$table->dropIndex('idx_ptu_task_owner_user');
$table->dropIndex('idx_ptu_user_owner_task');
});
}
};

View File

@ -12,6 +12,7 @@
"allowJs": true, "allowJs": true,
"checkJs": false, "checkJs": false,
"jsx": "preserve", "jsx": "preserve",
"ignoreDeprecations": "6.0",
"resolveJsonModule": true, "resolveJsonModule": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"esModuleInterop": true, "esModuleInterop": true,

View File

@ -1041,3 +1041,5 @@ scene 暂未实现:(*)
upload_id 不能为空 upload_id 不能为空
合并繁忙,请稍后再试 合并繁忙,请稍后再试
仅项目负责人或任务相关成员删除 仅项目负责人或任务相关成员删除
未开启部门负责人视角功能
没有可查看的部门数据

View File

@ -1,10 +1,5 @@
模版满足多种团队协作场景,同时支持自定义模版,满足团队个性化场景管理需求,可直观的查看项目的进展情况,团队协作更方便。
2、若你是团队的所有者请在删除您的帐号前转移所有权。例如该帐号所创建的项目可将项目移交他人或删除项目以及文件夹。 2、若你是团队的所有者请在删除您的帐号前转移所有权。例如该帐号所创建的项目可将项目移交他人或删除项目以及文件夹。
汇集文档、电子表格、思维笔记等多种在线工具,汇聚企业知识资源于一处,支持多人实时协同编辑,让团队协作更便捷。
多平台应用支持,打开客户端即可跟进项目任务进度, 同时让你在工作中每一个步骤都能拥有更高效愉悦的体验。
1、您将无法查看该帐号内的任何信息包括帐号信息、文件记录、聊天记录、项目信息、团队成员信息等。 1、您将无法查看该帐号内的任何信息包括帐号信息、文件记录、聊天记录、项目信息、团队成员信息等。
通过灵活的任务日历,轻松安排每一天的日程,把任务拆解到每天,让工作目标更清晰,时间分配更合理。
针对项目和任务建立群组,工作问题可及时沟通,促进团队快速协作,提高团队工作效率。
帐号删除后,该帐号将无法正常登录且无法恢复,帐号下的所有数据也将被删除。 帐号删除后,该帐号将无法正常登录且无法恢复,帐号下的所有数据也将被删除。
复杂大于或等于6个字符包含数字、字母大小写或者特殊字符。 复杂大于或等于6个字符包含数字、字母大小写或者特殊字符。
不会向忽略的邮箱地址发送邮件,可使用换行分割多个地址。 不会向忽略的邮箱地址发送邮件,可使用换行分割多个地址。
@ -24,7 +19,6 @@
开启后可以直接使用 LDAP 帐号密码登录 开启后可以直接使用 LDAP 帐号密码登录
必填:发送聊天内容前必须设置昵称。 必填:发送聊天内容前必须设置昵称。
移除成员负责的任务将变成无负责人, 移除成员负责的任务将变成无负责人,
首页底部:首页底部网站备案号等信息
为确保帐号安全,请确认是本人操作 为确保帐号安全,请确认是本人操作
修改邮箱和删除帐号需要邮箱验证码 修改邮箱和删除帐号需要邮箱验证码
关闭签到功能再开启需要重新安装。 关闭签到功能再开启需要重新安装。
@ -42,7 +36,6 @@
+ 输入子任务,回车添加子任务 + 输入子任务,回车添加子任务
4、请保证帐号未被暂停使用。 4、请保证帐号未被暂停使用。
只能设置单个状态为验收/测试 只能设置单个状态为验收/测试
以下是你当前的任务统计数据
可通过此链接直接加入项目。 可通过此链接直接加入项目。
项目不存在或不在成员列表内 项目不存在或不在成员列表内
简单大于或等于6个字符。 简单大于或等于6个字符。
@ -54,14 +47,12 @@
任务列表不存在或已被删除 任务列表不存在或已被删除
密码错误,请输入正确密码 密码错误,请输入正确密码
开放:所有人都可以发言。 开放:所有人都可以发言。
强大易用的协同创作云文档
注销前,请确认一下事项 注销前,请确认一下事项
签到前后时间收到消息通知 签到前后时间收到消息通知
签到提醒对象3天内有签到的成员法定工作日 签到提醒对象3天内有签到的成员法定工作日
该状态下任务自动标记完成 该状态下任务自动标记完成
输入您的信息以创建帐户。 输入您的信息以创建帐户。
仅支持Openwrt系统的路由器 仅支持Openwrt系统的路由器
便捷易用的项目管理模板
允许成员自己修改MAC地址 允许成员自己修改MAC地址
可设置多个状态为进行中 可设置多个状态为进行中
可通过此链接浏览文件。 可通过此链接浏览文件。
@ -70,7 +61,6 @@
注意:离职操作不可逆! 注意:离职操作不可逆!
请输入正确的邮箱地址! 请输入正确的邮箱地址!
项目负责人无法退出项目 项目负责人无法退出项目
高效便捷的团队沟通工具
以 http:// 或 https:// 开头 以 http:// 或 https:// 开头
请开启您PHP环境的openssl 请开启您PHP环境的openssl
两次密码输入不一致! 两次密码输入不一致!
@ -90,17 +80,13 @@
仅限项目负责人修改 仅限项目负责人修改
仅限项目负责人删除 仅限项目负责人删除
仅限项目负责人操作 仅限项目负责人操作
任务描述,回车创建
你好,扫码确认登录 你好,扫码确认登录
你确定要登出系统吗? 你确定要登出系统吗?
你确认领取任务吗? 你确认领取任务吗?
列表名称,回车创建 列表名称,回车创建
同步修改子任务时间 同步修改子任务时间
子任务不支持此功能 子任务不支持此功能
最后在线于很久以前
注意此操作不可逆! 注意此操作不可逆!
浏览图片空间的图片
清晰直观的任务日历
确认密码输入不一致 确认密码输入不一致
请先修改登录密码! 请先修改登录密码!
请填写正确的旧密码 请填写正确的旧密码
@ -108,7 +94,6 @@
请输入正确的邀请码 请输入正确的邀请码
请输入正确的验证码 请输入正确的验证码
请重新输入新密码! 请重新输入新密码!
轻量级任务管理工具
输入群名称(选填) 输入群名称(选填)
电话长度至少6位 电话长度至少6位
不支持复制文件夹 不支持复制文件夹
@ -139,8 +124,6 @@
超出文件大小限制 超出文件大小限制
输入您的电子邮件 输入您的电子邮件
选择任务协助人员 选择任务协助人员
选择转发指定成员
选择转发最近聊天
邮箱、昵称、职位 邮箱、昵称、职位
请输入会议频道ID 请输入会议频道ID
第二次任务提醒 第二次任务提醒
@ -158,9 +141,7 @@
帐号或密码错误 帐号或密码错误
我的待完成任务 我的待完成任务
拖动到这里发送 拖动到这里发送
支持多平台应用
文件格式不正确 文件格式不正确
最后在线于刚刚
最近打开的任务 最近打开的任务
未保存计划时间 未保存计划时间
正在上传文件... 正在上传文件...
@ -263,14 +244,9 @@ ID、名称、描述...
验证码已失效 验证码已失效
使用 SSO 登录 使用 SSO 登录
PPT 演示文稿 PPT 演示文稿
同步滚动:关
同步滚动:开
Excel 工作表 Excel 工作表
html转markdown
上传文件夹 上传文件夹
不是发送人 不是发送人
今日待完成
代码块主题
任务不存在 任务不存在
任务优先级 任务优先级
任务已完成 任务已完成
@ -296,7 +272,6 @@ html转markdown
工作流设置 工作流设置
已删除任务 已删除任务
已完成任务 已完成任务
已完成列表
已归档任务 已归档任务
开启工作流 开启工作流
归档的任务 归档的任务
@ -314,7 +289,6 @@ html转markdown
最晚可延后 最晚可延后
服务器版本 服务器版本
未完成任务 未完成任务
未完成列表
未开放注册 未开放注册
未知的消息 未知的消息
标记未完成 标记未完成
@ -357,6 +331,21 @@ SMTP服务器
任务提醒 任务提醒
职位/职称 职位/职称
验收/测试 验收/测试
(*) 超期
未来 (*) 天内暂无到期任务
暂无高优先级任务
所有任务均已分配负责人
当前筛选条件下暂无任务
可以切换上方条件查看其他任务
数据更新说明
页面统计数据在 (*) 秒内复用,重点关注任务列表除外。
上次更新:(*)
立即刷新
刷新成功
查看全部 (*) 个优先级
任务概况
流程阶段
未完成总数
上传图片 上传图片
上传失败 上传失败
上传成功 上传成功
@ -392,14 +381,11 @@ SMTP服务器
允许修改 允许修改
允许注册 允许注册
全员群组 全员群组
全屏查看
全屏编辑
全部文件 全部文件
全部禁言 全部禁言
共享权限 共享权限
共享设置 共享设置
准备发布 准备发布
分屏显示
创建时间 创建时间
创建群组 创建群组
创建项目 创建项目
@ -560,7 +546,6 @@ SMTP服务器
紧凑经典 紧凑经典
经典天盘 经典天盘
结束状态 结束状态
缩小查看
群组设置 群组设置
聊天昵称 聊天昵称
聊天资料 聊天资料
@ -628,7 +613,6 @@ LDAP 地址
LDAP 端口 LDAP 端口
MD编辑器 MD编辑器
Word 文档 Word 文档
导出XLSX
MAC地址 MAC地址
扫一扫 扫一扫
上个月 上个月
@ -648,9 +632,6 @@ MAC地址
到期后 到期后
天空蓝 天空蓝
子任务 子任务
导出CSV
导出TXT
导出XLS
已删除 已删除
已取消 已取消
已完成 已完成
@ -870,36 +851,19 @@ Pro版
颜色 颜色
黄色 黄色
默认 默认
(*)秒 (*)秒
(*)是一款轻量级的开源在线项目任务管理工具提供各类文档协作工具、在线思维导图、在线流程图、项目管理、任务分发、即时IM文件管理等工具。 (*)是一款轻量级的开源在线项目任务管理工具提供各类文档协作工具、在线思维导图、在线流程图、项目管理、任务分发、即时IM文件管理等工具。
(*)负责的部门、项目、任务和文件将移交给交接人;同时退出所有群(如果是群主则转让给交接人)
流转到【(*)】时,[任务负责人] 和 [项目管理员] 可以修改状态。 流转到【(*)】时,[任务负责人] 和 [项目管理员] 可以修改状态。
文件(*)格式不正确,请上传(*)格式的图片。 文件(*)格式不正确,请上传(*)格式的图片。
流转到【(*)】时添加状态负责人至任务负责人。 流转到【(*)】时添加状态负责人至任务负责人。
每个文件夹里最多只能创建(*)个文件或文件夹 每个文件夹里最多只能创建(*)个文件或文件夹
文件(*)格式不正确,仅支持上传:(*) 文件(*)格式不正确,仅支持上传:(*)
正在进行帐号【(*)】MAC地址修改。 正在进行帐号【(*)】MAC地址修改。
正在进行帐号【(*)】离职操作。
正在进行帐号【(*)】部门修改。 正在进行帐号【(*)】部门修改。
文件大小超限,最大限制:(*)KB 文件大小超限,最大限制:(*)KB
职位/职称最多只能设置(*)个字 职位/职称最多只能设置(*)个字
任务描述最多只能设置(*)个字 任务描述最多只能设置(*)个字
你确定要归档项目【(*)】吗? 你确定要归档项目【(*)】吗?
文件(*)太大,不能超过:(*)'
文件名称最多只能设置(*)个字 文件名称最多只能设置(*)个字
文件格式错误,限制类型:(*) 文件格式错误,限制类型:(*)
项目介绍最多只能设置(*)个字 项目介绍最多只能设置(*)个字
@ -913,8 +877,6 @@ Pro版
消息内容最大不能超过(*)字 消息内容最大不能超过(*)字
项目列表最多不能超过(*)个 项目列表最多不能超过(*)个
项目名称不可以少于(*)个字 项目名称不可以少于(*)个字
最多只能上传(*)张图片。
最多只能选择(*)张图片。
密码最多只能设置(*)位数 密码最多只能设置(*)位数
密码设置不能小于(*)位数 密码设置不能小于(*)位数
描述最多只能设置(*)个字 描述最多只能设置(*)个字
@ -1044,8 +1006,6 @@ Pro版
删除人员 删除人员
删除的任务 删除的任务
你确定要还原删除吗? 你确定要还原删除吗?
转换成markdown
请输入html代码...
请输入License... 请输入License...
详细信息 详细信息
域名 域名
@ -1347,9 +1307,7 @@ APP 推送
你确定取消待办吗? 你确定取消待办吗?
取消成功 取消成功
请等待打包完成 请等待打包完成
选择一个项目查看更多任务
首页 首页
无相关数据
权限设置 权限设置
任务列权限 任务列权限
添加列 添加列
@ -1374,7 +1332,6 @@ APP 推送
接龙 接龙
参与接龙 参与接龙
发起接龙 发起接龙
发起接龙,参与接龙目前共(*)人 发起接龙,参与接龙目前共(*)人
请输入接龙主题 请输入接龙主题
请输入接龙内容 请输入接龙内容
@ -1382,7 +1339,6 @@ APP 推送
重复内容将不再计入接龙结果 重复内容将不再计入接龙结果
返回编辑 返回编辑
继续发送 继续发送
接龙结果 接龙结果
选择群组发起接龙 选择群组发起接龙
来自 来自
@ -1399,7 +1355,6 @@ APP 推送
单选 单选
请选择后投票 请选择后投票
立即投票 立即投票
再次发送 再次发送
再次发送投票? 再次发送投票?
结束投票 结束投票
@ -1722,11 +1677,11 @@ WiFi签到延迟时长为±1分钟。
注意:此操作不可恢复,部门下的成员将移至默认部门。 注意:此操作不可恢复,部门下的成员将移至默认部门。
维护中... 维护中...
(*)评论了(*)的「(**)」审批 (*)评论了(*)的「(*)」审批
抄送(*)提交的「(**)」记录 抄送(*)提交的「(*)」记录
(*)提交的「(**)」待你审批 (*)提交的「(*)」待你审批
您发起的「(**)」已通过 您发起的「(*)」已通过
您发起的「(**)」被(*)拒绝 您发起的「(*)」被(*)拒绝
翻译 翻译
从不 从不
@ -1786,7 +1741,6 @@ WiFi签到延迟时长为±1分钟。
签到半径设置 签到半径设置
半径 半径
经度 经度
纬度 纬度
地图类型 地图类型
@ -2092,7 +2046,6 @@ OKR群组
邀请地址不存在或已被删除! 邀请地址不存在或已被删除!
会话名称 会话名称
结果 结果
命令 命令
接口地址 接口地址
@ -2336,7 +2289,6 @@ AI 消息助手
留空则不修改密码 留空则不修改密码
职位 职位
请输入电话号码 请输入电话号码
正在编辑帐号【ID:(*)】的信息。
编辑用户信息 编辑用户信息
AI任务分析 AI任务分析
@ -2531,3 +2483,176 @@ AI任务分析
共享文件 共享文件
我共享的 我共享的
共享给我的 共享给我的
服务器返回错误HTTP (*)
你撤回了一条消息
重新编辑
四象限
个人视角
部门任务总览
(*) 人
最久 (*) 天
今天 (*) 项到期
还有 (*) 项
待完成
我协助的
(*)月(*)日
有 (*) 项已超期,建议先处理
有 (*) 项今日到期
今日无到期任务
团队有 (*) 项已超期,涉及 (*) 人
团队有 (*) 项已超期
团队本周已完成 (*) 项
查看任务
涉及 (*) 人
(*) 天内到期
本周完成
较上周
与上周持平
成员任务分配
(*) 项
未分配
查看全部 (*) 人
优先级分布
未完成 (*) 项
点击任一档位可下钻到任务列表
高优先级
重点关注任务
未分配负责人
待分配
全部显示完毕
逾期 (*) 天
逾期 (*) 小时
夜深了,(*)
早上好,(*)
中午好,(*)
下午好,(*)
晚上好,(*)
服务器时间
暂无已超期任务
暂无今日到期任务
暂无待完成任务
暂无协助的任务
没有超期任务,保持住
今天只有这一项,处理完就轻松了
数量较多,仅显示最近 (*) 项,展开其余 (*) 项
我的部门
待开始
暂无待开始任务
太棒了,任务全部清空
本周完成了 (*) 项任务
欢迎使用 DooTask
项目是任务与协作的起点,创建一个开始
创建第一个项目
已有团队?请同事把你加入项目即可
近期完成
本周 (*) 项
当前没有待处理任务
可以新建一项任务,或等待新的工作安排
选择需要统计的部门,包含所选部门及其所有下级部门。
部门范围与负责人仪表盘保持一致;开启后,可只读查看范围内成员参与的项目和任务。
项目负责人视角
开启后在项目列表中显示额外的只读项目
选择团队范围
在项目列表中启用
+(*)位
(*)条回复
AgoraIO 声网
会话中断
确定要清空所有历史会话吗?
新窗口打开
组件加载失败!
没有更新描述。
已全选
已选部分
你的姓名
请输入你的姓名
会议中
未命名应用
应用 ID
应用名称
菜单标题
菜单位置
应用中心 - 常用
应用中心 - 管理
主导航
可见范围
仅管理员
图标地址
菜单 URL
背景颜色
保持激活状态
自动暗黑模式
沉浸式
透明背景
禁用作用域样式
可能要发的照片
其他任务
点击查看
处理
没有符合条件的数据
正常
暂无介绍
关联子任务
最多只能选择(*)个标签
来源
未知
分享时间
分享人
标题
任务状态
已还原
下个周期
LDAP 用户禁止修改邮箱
共(*)个
标签名称最多只能设置(*)个字
添加失败
未开启通知权限
使用端到端加密传输数据。
删除前,请确认以下事项:
已清楚风险,确定删除
只能输入字母或数字
授权
不支持单独查看此消息
报告详情
验证邮箱
您的邮箱已通过验证
今后您可以通过此邮箱重置您的帐号密码
链接已过期,已重新发送
请设置昵称
请设置联系电话
文件 (*) 上传失败 (*)
文件 (*) 格式不正确,请上传 jpg、jpeg、webp、gif、png 格式的图片。
文件 (*) 太大,不能超过:(*)
最多只能上传 (*) 张图片。
最多只能选择 (*) 张图片。
文件 (*) 上传失败,(*)
文件 (*) 格式不正确,仅支持上传:(*)
很久以前
@我的消息
确定要删除记录"(*)"吗?
添加(*)
共(*)个文件仅显示最新50个
正在进行帐号【ID:(*), (*)】离职操作。
(*) 负责的部门、项目、任务和文件将移交给交接人;同时退出所有群(如果是群主则转让给交接人)
正在编辑帐号【ID:(*), (*)】的信息。
打包下载(*)
[calendar_view].日
[calendar_view].周
[calendar_view].月
[day_unit].天
[distance_unit].米
[example_label].例
[initiator_label].由
[recurrence_prefix].每
[self_sender].你
[task_unit].个
[task_unit].项
[todo_target].给
[vote_unit].票
[weekday].日
[weekday].一
[weekday].二
[weekday].三
[weekday].四
[weekday].五
[weekday].六

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{ {
"name": "DooTask", "name": "DooTask",
"version": "1.8.69", "version": "1.8.89",
"codeVerson": 240, "codeVerson": 241,
"description": "DooTask is task management system.", "description": "DooTask is task management system.",
"scripts": { "scripts": {
"start": "./cmd dev", "start": "./cmd dev",

View File

@ -1 +1 @@
import{n as m}from"./app.ec83cdf6.js";import"./jquery.020cc780.js";import"./@babel.9410f858.js";import"./dayjs.5bb9b94f.js";import"./localforage.982e07b7.js";import"./markdown-it.0450edb4.js";import"./mdurl.ce6c1dd8.js";import"./uc.micro.8d343c98.js";import"./entities.48a44fec.js";import"./linkify-it.c5e8196e.js";import"./punycode.js.4b3f125a.js";import"./highlight.js.cbbfb885.js";import"./markdown-it-link-attributes.e1d5d151.js";import"./@traptitech.acea8861.js";import"./vue.adba9046.js";import"./vuex.cc7cb26e.js";import"./openpgp_hi.15f91b1d.js";import"./axios.37c7f908.js";import"./mitt.1ea0a2a3.js";import"./quill-hi.ca2ea0cc.js";import"./parchment.d5c5924e.js";import"./quill-delta.385a10bf.js";import"./fast-diff.f17881f3.js";import"./lodash.clonedeep.3cc09a31.js";import"./lodash.isequal.dbdc2157.js";import"./eventemitter3.78b735ad.js";import"./lodash-es.76e3a28b.js";import"./quill-mention-hi.4eeb5a2d.js";import"./view-design-hi.f1128b4d.js";import"./html-to-md.f297036e.js";import"./lodash.8fcd6fd4.js";import"./vue-router.2d566cd7.js";import"./vue-clipboard2.fd43a5bc.js";import"./clipboard.37b37361.js";import"./vuedraggable.f464b992.js";import"./sortablejs.3488b922.js";import"./vue-resize-observer.5af23a43.js";import"./element-sea.f8a64907.js";import"./deepmerge.cecf392e.js";import"./resize-observer-polyfill.5d591c5f.js";import"./throttle-debounce.7c3948b2.js";import"./babel-helper-vue-jsx-merge-props.5ed215c3.js";import"./normalize-wheel.2a034b9f.js";import"./async-validator.dca2b951.js";import"./babel-runtime.4773988a.js";import"./core-js.314b4a1d.js";var p=function(){var t=this,r=t.$createElement;return t._self._c,t._m(0)},e=[function(){var t=this,r=t.$createElement,i=t._self._c||r;return i("div",{staticClass:"page-404"},[i("div",{staticClass:"flex-center position-ref full-height"},[i("div",{staticClass:"code"},[t._v("404")]),i("div",{staticClass:"message"},[t._v("Not Found")])])])}];const s={},o={};var _=m(s,p,e,!1,n,"7d7154a8",null,null);function n(t){for(let r in o)this[r]=o[r]}var it=function(){return _.exports}();export{it as default}; import{n as m}from"./app.1e00fc89.js";import"./jquery.99163cb9.js";import"./@babel.9410f858.js";import"./dayjs.0bb0f368.js";import"./localforage.6e50a401.js";import"./markdown-it.0450edb4.js";import"./mdurl.ce6c1dd8.js";import"./uc.micro.8d343c98.js";import"./entities.48a44fec.js";import"./linkify-it.c5e8196e.js";import"./punycode.js.4b3f125a.js";import"./highlight.js.cbbfb885.js";import"./markdown-it-link-attributes.e1d5d151.js";import"./@traptitech.acea8861.js";import"./vue.adba9046.js";import"./vuex.cc7cb26e.js";import"./openpgp_hi.15f91b1d.js";import"./axios.37c7f908.js";import"./mitt.1ea0a2a3.js";import"./quill-hi.ca2ea0cc.js";import"./parchment.d5c5924e.js";import"./quill-delta.385a10bf.js";import"./fast-diff.f17881f3.js";import"./lodash.clonedeep.3cc09a31.js";import"./lodash.isequal.dbdc2157.js";import"./eventemitter3.78b735ad.js";import"./lodash-es.76e3a28b.js";import"./quill-mention-hi.4eeb5a2d.js";import"./view-design-hi.f1128b4d.js";import"./html-to-md.f297036e.js";import"./lodash.8fcd6fd4.js";import"./vue-router.2d566cd7.js";import"./vue-clipboard2.fd43a5bc.js";import"./clipboard.37b37361.js";import"./vuedraggable.f464b992.js";import"./sortablejs.3488b922.js";import"./vue-resize-observer.5af23a43.js";import"./element-sea.f8a64907.js";import"./deepmerge.cecf392e.js";import"./resize-observer-polyfill.5d591c5f.js";import"./throttle-debounce.7c3948b2.js";import"./babel-helper-vue-jsx-merge-props.5ed215c3.js";import"./normalize-wheel.2a034b9f.js";import"./async-validator.dca2b951.js";import"./babel-runtime.4773988a.js";import"./core-js.314b4a1d.js";var p=function(){var t=this,r=t.$createElement;return t._self._c,t._m(0)},e=[function(){var t=this,r=t.$createElement,i=t._self._c||r;return i("div",{staticClass:"page-404"},[i("div",{staticClass:"flex-center position-ref full-height"},[i("div",{staticClass:"code"},[t._v("404")]),i("div",{staticClass:"message"},[t._v("Not Found")])])])}];const s={},o={};var _=m(s,p,e,!1,n,"7d7154a8",null,null);function n(t){for(let r in o)this[r]=o[r]}var it=function(){return _.exports}();export{it as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
.checkin-field .ivu-form-item-label{color:#f90;font-weight:500}.checkin-mac-header[data-v-eb58f07c]{margin-bottom:8px;font-weight:500;color:#606266}.checkin-mac-item[data-v-eb58f07c]{margin-bottom:8px}.checkin-mac-item .ivu-col[data-v-eb58f07c]{padding-right:8px}.checkin-mac-item .ivu-col[data-v-eb58f07c]:last-child{padding-right:0}.checkin-mac-del[data-v-eb58f07c]{display:flex;align-items:center;justify-content:center;cursor:pointer;color:red}.checkin-mac-del[data-v-eb58f07c]:hover{opacity:.8}.form-tip[data-v-eb58f07c]{font-size:12px;color:#999;margin-top:4px}.user-tags-preview[data-v-eb58f07c]{display:flex;align-items:center;flex-wrap:wrap;gap:8px;min-height:32px}.user-tags-preview .tag-pill[data-v-eb58f07c]{cursor:pointer;padding:6px 12px;border-radius:12px;font-size:13px;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:#f5f5f5;color:#606266;line-height:14px;height:26px;max-width:160px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.user-tags-preview .tag-pill.is-recognized[data-v-eb58f07c]{color:#67c23a}.user-tags-preview .tag-pill span[data-v-eb58f07c]{padding-left:8px;position:relative}.user-tags-preview .tag-pill span[data-v-eb58f07c]:before{content:"";position:absolute;left:2px;top:50%;transform:translateY(-50%);width:2px;height:2px;border-radius:50%;background-color:currentColor}.user-tags-preview .tags-empty[data-v-eb58f07c]{color:#909399}.user-tags-preview .tags-total[data-v-eb58f07c]{color:#909399;font-size:12px}.user-tags-preview .manage-button[data-v-eb58f07c]{margin-left:auto;display:inline-flex;align-items:center;gap:4px}.import-user-modal .import-tip[data-v-9d8f7ae8]{color:#808695;margin-bottom:12px}.import-user-modal .import-actions[data-v-9d8f7ae8]{display:flex;gap:12px;align-items:center}.import-user-modal .import-option[data-v-9d8f7ae8]{margin-top:12px}.import-user-modal .import-batch-label[data-v-9d8f7ae8]{flex-shrink:0;min-width:64px;color:#515a6e}.import-user-modal .import-setdept[data-v-9d8f7ae8]{display:flex;align-items:center;gap:8px;margin-top:12px}.import-user-modal .import-setdept .import-setdept-select[data-v-9d8f7ae8]{width:auto}.import-user-modal .import-setverity[data-v-9d8f7ae8]{display:flex;align-items:center;gap:8px;margin-top:12px}.import-user-modal .import-preview[data-v-9d8f7ae8],.import-user-modal .import-result[data-v-9d8f7ae8]{margin-top:16px}.import-user-modal[data-v-9d8f7ae8] .ivu-table-cell{white-space:nowrap}.import-user-modal[data-v-9d8f7ae8] .pwd-cell{cursor:pointer;letter-spacing:1px;-webkit-user-select:none;-moz-user-select:none;user-select:none}.import-user-modal[data-v-9d8f7ae8] .pwd-cell:hover{color:#2d8cf0}.import-user-modal[data-v-9d8f7ae8] .import-row-error td{background-color:#fff2f0} .checkin-field .ivu-form-item-label{color:#f90;font-weight:500}.checkin-mac-header[data-v-7e50b959]{margin-bottom:8px;font-weight:500;color:#606266}.checkin-mac-item[data-v-7e50b959]{margin-bottom:8px}.checkin-mac-item .ivu-col[data-v-7e50b959]{padding-right:8px}.checkin-mac-item .ivu-col[data-v-7e50b959]:last-child{padding-right:0}.checkin-mac-del[data-v-7e50b959]{display:flex;align-items:center;justify-content:center;cursor:pointer;color:red}.checkin-mac-del[data-v-7e50b959]:hover{opacity:.8}.form-tip[data-v-7e50b959]{font-size:12px;color:#999;margin-top:4px}.user-tags-preview[data-v-7e50b959]{display:flex;align-items:center;flex-wrap:wrap;gap:8px;min-height:32px}.user-tags-preview .tag-pill[data-v-7e50b959]{cursor:pointer;padding:6px 12px;border-radius:12px;font-size:13px;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:#f5f5f5;color:#606266;line-height:14px;height:26px;max-width:160px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.user-tags-preview .tag-pill.is-recognized[data-v-7e50b959]{color:#67c23a}.user-tags-preview .tag-pill span[data-v-7e50b959]{padding-left:8px;position:relative}.user-tags-preview .tag-pill span[data-v-7e50b959]:before{content:"";position:absolute;left:2px;top:50%;transform:translateY(-50%);width:2px;height:2px;border-radius:50%;background-color:currentColor}.user-tags-preview .tags-empty[data-v-7e50b959]{color:#909399}.user-tags-preview .tags-total[data-v-7e50b959]{color:#909399;font-size:12px}.user-tags-preview .manage-button[data-v-7e50b959]{margin-left:auto;display:inline-flex;align-items:center;gap:4px}.import-user-modal .import-tip[data-v-9d8f7ae8]{color:#808695;margin-bottom:12px}.import-user-modal .import-actions[data-v-9d8f7ae8]{display:flex;gap:12px;align-items:center}.import-user-modal .import-option[data-v-9d8f7ae8]{margin-top:12px}.import-user-modal .import-batch-label[data-v-9d8f7ae8]{flex-shrink:0;min-width:64px;color:#515a6e}.import-user-modal .import-setdept[data-v-9d8f7ae8]{display:flex;align-items:center;gap:8px;margin-top:12px}.import-user-modal .import-setdept .import-setdept-select[data-v-9d8f7ae8]{width:auto}.import-user-modal .import-setverity[data-v-9d8f7ae8]{display:flex;align-items:center;gap:8px;margin-top:12px}.import-user-modal .import-preview[data-v-9d8f7ae8],.import-user-modal .import-result[data-v-9d8f7ae8]{margin-top:16px}.import-user-modal[data-v-9d8f7ae8] .ivu-table-cell{white-space:nowrap}.import-user-modal[data-v-9d8f7ae8] .pwd-cell{cursor:pointer;letter-spacing:1px;-webkit-user-select:none;-moz-user-select:none;user-select:none}.import-user-modal[data-v-9d8f7ae8] .pwd-cell:hover{color:#2d8cf0}.import-user-modal[data-v-9d8f7ae8] .import-row-error td{background-color:#fff2f0}

View File

@ -1 +0,0 @@
import{m as i}from"./vuex.cc7cb26e.js";import{n as o}from"./app.ec83cdf6.js";var d=function(){var t=this,a=t.$createElement,e=t._self._c||a;return e("Modal",{attrs:{value:t.value,title:t.$L("\u8D1F\u8D23\u4EBA\u89C6\u89D2"),"mask-closable":!1,width:"520"},on:{input:function(s){return t.$emit("input",s)}}},[e("div",{staticClass:"department-owner-view-modal"},[e("Alert",{attrs:{type:"info","show-icon":""}},[t._v(" "+t._s(t.$L("\u53EF\u67E5\u770B\u6240\u9009\u90E8\u95E8\u53CA\u6240\u6709\u4E0B\u7EA7\u90E8\u95E8\u6210\u5458\u53C2\u4E0E\u7684\u9879\u76EE\u548C\u4EFB\u52A1\uFF0C\u4EC5\u652F\u6301\u53EA\u8BFB\u67E5\u770B\u3002"))+" ")]),t.managedDepartments.length>1?e("div",{staticClass:"department-owner-view-actions"},[e("a",{attrs:{href:"javascript:void(0)"},on:{click:function(s){t.draftIds=[]}}},[t._v(t._s(t.$L("\u6E05\u7A7A")))]),e("a",{attrs:{href:"javascript:void(0)"},on:{click:function(s){t.draftIds=t.managedDepartments.map(function(n){return n.id})}}},[t._v(t._s(t.$L("\u5168\u9009")))]),e("a",{attrs:{href:"javascript:void(0)"},on:{click:t.reverseDraft}},[t._v(t._s(t.$L("\u53CD\u9009")))])]):t._e(),e("CheckboxGroup",{staticClass:"department-owner-view-list",model:{value:t.draftIds,callback:function(s){t.draftIds=s},expression:"draftIds"}},t._l(t.managedDepartments,function(s){return e("div",{key:s.id,class:["department-owner-view-item",t.draftIds.includes(s.id)?"active":""],on:{click:function(n){return t.toggleDraft(s.id)}}},[e("div",{staticClass:"department-owner-view-icon"},[e("i",{staticClass:"taskfont"},[t._v("\uE75C")])]),e("div",{staticClass:"department-owner-view-name"},[t._v(t._s(s.name))]),e("Checkbox",{staticClass:"department-owner-view-checkbox",attrs:{label:s.id},nativeOn:{click:function(n){n.stopPropagation()}}},[e("span")])],1)}),0)],1),e("div",{staticClass:"adaption",attrs:{slot:"footer"},slot:"footer"},[e("Button",{attrs:{type:"default",disabled:t.applyLoading},on:{click:function(s){return t.$emit("input",!1)}}},[t._v(t._s(t.$L("\u53D6\u6D88")))]),e("Button",{attrs:{type:"primary",loading:t.applyLoading},on:{click:t.apply}},[t._v(t._s(t.$L("\u786E\u5B9A")))])],1)])},l=[];const c={name:"DepartmentOwnerView",props:{value:Boolean},data(){return{draftIds:[],applyLoading:!1}},computed:{...i(["userInfo","cacheDepartmentOwnerIds"]),managedDepartments(){return(this.userInfo.managed_departments||[]).map(t=>({...t,id:parseInt(t.id)}))}},watch:{value:{immediate:!0,handler(t){t?this.draftIds=(this.cacheDepartmentOwnerIds||[]).slice():this.applyLoading=!1}}},methods:{toggleDraft(t){t=parseInt(t);const a=this.draftIds.indexOf(t);a>-1?this.draftIds.splice(a,1):this.draftIds.push(t)},reverseDraft(){const t=this.draftIds.map(a=>parseInt(a));this.draftIds=this.managedDepartments.map(a=>a.id).filter(a=>!t.includes(a))},async apply(){if(!this.applyLoading){this.applyLoading=!0;try{await this.$store.dispatch("setDepartmentOwnerIds",this.draftIds),this.$emit("input",!1)}catch(t){$A.modalError((t==null?void 0:t.msg)||this.$L("\u5207\u6362\u5931\u8D25"))}finally{this.applyLoading=!1}}}}},r={};var p=o(c,d,l,!1,f,"624ab3e4",null,null);function f(t){for(let a in r)this[a]=r[a]}var u=function(){return p.exports}();export{u as D};

View File

@ -0,0 +1 @@
import{m as i}from"./vuex.cc7cb26e.js";import{n as d}from"./app.1e00fc89.js";var o=function(){var t=this,a=t.$createElement,e=t._self._c||a;return e("Modal",{attrs:{value:t.value,title:t.modalTitle,"mask-closable":!1,width:"520"},on:{input:function(s){return t.$emit("input",s)}}},[e("div",{staticClass:"department-owner-view-modal"},[e("Alert",{attrs:{type:"info","show-icon":""}},[t.scopeOnly?[t._v(t._s(t.$L("\u9009\u62E9\u9700\u8981\u7EDF\u8BA1\u7684\u90E8\u95E8\uFF0C\u5305\u542B\u6240\u9009\u90E8\u95E8\u53CA\u5176\u6240\u6709\u4E0B\u7EA7\u90E8\u95E8\u3002")))]:[t._v(t._s(t.$L("\u90E8\u95E8\u8303\u56F4\u4E0E\u8D1F\u8D23\u4EBA\u4EEA\u8868\u76D8\u4FDD\u6301\u4E00\u81F4\uFF1B\u5F00\u542F\u540E\uFF0C\u53EF\u53EA\u8BFB\u67E5\u770B\u8303\u56F4\u5185\u6210\u5458\u53C2\u4E0E\u7684\u9879\u76EE\u548C\u4EFB\u52A1\u3002")))]],2),t.scopeOnly?t._e():e("div",{staticClass:"department-owner-view-switch"},[e("div",[e("strong",[t._v(t._s(t.$L("\u5728\u9879\u76EE\u5217\u8868\u4E2D\u542F\u7528")))]),e("span",{staticClass:"department-owner-view-switch-desc"},[t._v(t._s(t.$L("\u5F00\u542F\u540E\u5728\u9879\u76EE\u5217\u8868\u4E2D\u663E\u793A\u989D\u5916\u7684\u53EA\u8BFB\u9879\u76EE")))])]),e("iSwitch",{model:{value:t.draftProjectViewEnabled,callback:function(s){t.draftProjectViewEnabled=s},expression:"draftProjectViewEnabled"}})],1),t.scopeOnly||t.draftProjectViewEnabled?[t.managedDepartments.length>1?e("div",{staticClass:"department-owner-view-actions"},[e("a",{attrs:{href:"javascript:void(0)"},on:{click:function(s){t.draftIds=[]}}},[t._v(t._s(t.$L("\u6E05\u7A7A")))]),e("a",{attrs:{href:"javascript:void(0)"},on:{click:function(s){t.draftIds=t.managedDepartments.map(function(n){return n.id})}}},[t._v(t._s(t.$L("\u5168\u9009")))]),e("a",{attrs:{href:"javascript:void(0)"},on:{click:t.reverseDraft}},[t._v(t._s(t.$L("\u53CD\u9009")))])]):t._e(),e("CheckboxGroup",{staticClass:"department-owner-view-list",model:{value:t.draftIds,callback:function(s){t.draftIds=s},expression:"draftIds"}},t._l(t.managedDepartments,function(s){return e("div",{key:s.id,class:["department-owner-view-item",t.draftIds.includes(s.id)?"active":""],on:{click:function(n){return t.toggleDraft(s.id)}}},[e("div",{staticClass:"department-owner-view-icon"},[e("i",{staticClass:"taskfont"},[t._v("\uE75C")])]),e("div",{staticClass:"department-owner-view-name"},[t._v(t._s(s.name))]),e("Checkbox",{staticClass:"department-owner-view-checkbox",attrs:{label:s.id},nativeOn:{click:function(n){n.stopPropagation()}}},[e("span")])],1)}),0)]:t._e()],2),e("div",{staticClass:"adaption",attrs:{slot:"footer"},slot:"footer"},[e("Button",{attrs:{type:"default",disabled:t.applyLoading},on:{click:function(s){return t.$emit("input",!1)}}},[t._v(t._s(t.$L("\u53D6\u6D88")))]),e("Button",{attrs:{type:"primary",disabled:t.draftIds.length===0,loading:t.applyLoading},on:{click:t.apply}},[t._v(t._s(t.$L("\u786E\u5B9A")))])],1)])},l=[];const c={name:"DepartmentOwnerView",props:{value:Boolean,scopeOnly:Boolean},data(){return{draftIds:[],draftProjectViewEnabled:!1,applyLoading:!1}},computed:{...i(["userInfo","cacheDepartmentOwnerIds","departmentOwnerProjectViewEnabled"]),modalTitle(){return this.scopeOnly?this.$L("\u9009\u62E9\u56E2\u961F\u8303\u56F4"):this.$L("\u9879\u76EE\u8D1F\u8D23\u4EBA\u89C6\u89D2")},managedDepartments(){return(this.userInfo.managed_departments||[]).map(t=>({...t,id:parseInt(t.id)}))}},watch:{value:{immediate:!0,handler(t){if(t){const a=(this.cacheDepartmentOwnerIds||[]).slice();this.draftIds=a.length>0?a:this.managedDepartments.map(e=>e.id),this.draftProjectViewEnabled=this.departmentOwnerProjectViewEnabled}else this.applyLoading=!1}}},methods:{toggleDraft(t){t=parseInt(t);const a=this.draftIds.indexOf(t);a>-1?this.draftIds.splice(a,1):this.draftIds.push(t)},reverseDraft(){const t=this.draftIds.map(a=>parseInt(a));this.draftIds=this.managedDepartments.map(a=>a.id).filter(a=>!t.includes(a))},async apply(){if(!this.applyLoading){this.applyLoading=!0;try{await this.$store.dispatch("setDepartmentOwnerIds",this.scopeOnly?this.draftIds:{ids:this.draftIds,projectViewEnabled:this.draftProjectViewEnabled}),this.$emit("input",!1)}catch(t){$A.modalError((t==null?void 0:t.msg)||this.$L("\u5207\u6362\u5931\u8D25"))}finally{this.applyLoading=!1}}}}},r={};var p=d(c,o,l,!1,f,"61d09b1f",null,null);function f(t){for(let a in r)this[a]=r[a]}var u=function(){return p.exports}();export{u as D};

View File

@ -0,0 +1 @@
.department-owner-view-modal .department-owner-view-actions[data-v-61d09b1f]{display:flex;justify-content:flex-end;gap:14px;margin:12px 8px 0}.department-owner-view-modal .department-owner-view-switch[data-v-61d09b1f]{display:flex;align-items:center;justify-content:space-between;gap:16px;margin:14px 8px 4px}.department-owner-view-modal .department-owner-view-switch>div[data-v-61d09b1f]{display:flex;flex-direction:column;gap:3px;min-width:0}.department-owner-view-modal .department-owner-view-switch strong[data-v-61d09b1f]{font-weight:500}.department-owner-view-modal .department-owner-view-switch .department-owner-view-switch-desc[data-v-61d09b1f]{color:#999;font-size:12px;line-height:18px}.department-owner-view-modal .department-owner-view-list[data-v-61d09b1f]{display:flex;flex-direction:column;margin-top:10px}.department-owner-view-modal .department-owner-view-item[data-v-61d09b1f]{display:flex;align-items:center;padding:10px 12px;cursor:pointer}.department-owner-view-modal .department-owner-view-icon[data-v-61d09b1f]{width:28px;height:28px;border-radius:50%;background-color:#5bc7b0;color:#fff;display:flex;align-items:center;justify-content:center;margin-right:10px}.department-owner-view-modal .department-owner-view-name[data-v-61d09b1f]{flex:1}.department-owner-view-modal .department-owner-view-checkbox[data-v-61d09b1f]{margin-right:0}

View File

@ -1 +0,0 @@
.department-owner-view-modal .department-owner-view-actions[data-v-624ab3e4]{display:flex;justify-content:flex-end;gap:14px;margin:12px 8px 0}.department-owner-view-modal .department-owner-view-list[data-v-624ab3e4]{display:flex;flex-direction:column;margin-top:10px}.department-owner-view-modal .department-owner-view-item[data-v-624ab3e4]{display:flex;align-items:center;padding:10px 12px;cursor:pointer}.department-owner-view-modal .department-owner-view-icon[data-v-624ab3e4]{width:28px;height:28px;border-radius:50%;background-color:#5bc7b0;color:#fff;display:flex;align-items:center;justify-content:center;margin-right:10px}.department-owner-view-modal .department-owner-view-name[data-v-624ab3e4]{flex:1}.department-owner-view-modal .department-owner-view-checkbox[data-v-624ab3e4]{margin-right:0}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
public/js/build/Drawio.6950c35e.css vendored Normal file
View File

@ -0,0 +1 @@
.drawio-content[data-v-86e491c2]{position:absolute;top:0;left:0;width:100%;height:100%}.drawio-content .drawio-iframe[data-v-86e491c2]{position:absolute;top:0;left:0;width:100%;height:100%;background:0 0;border:0;float:none;margin:-1px 0 0;max-width:none;outline:0;padding:0}.drawio-content .drawio-loading[data-v-86e491c2]{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}

View File

@ -1 +0,0 @@
.drawio-content[data-v-39021859]{position:absolute;top:0;left:0;width:100%;height:100%}.drawio-content .drawio-iframe[data-v-39021859]{position:absolute;top:0;left:0;width:100%;height:100%;background:0 0;border:0;float:none;margin:-1px 0 0;max-width:none;outline:0;padding:0}.drawio-content .drawio-loading[data-v-39021859]{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}

View File

@ -1 +1 @@
import{m as s}from"./vuex.cc7cb26e.js";import{I as m}from"./IFrame.c23602b5.js";import{n as p,l as o}from"./app.ec83cdf6.js";import"./jquery.020cc780.js";import"./@babel.9410f858.js";import"./dayjs.5bb9b94f.js";import"./localforage.982e07b7.js";import"./markdown-it.0450edb4.js";import"./mdurl.ce6c1dd8.js";import"./uc.micro.8d343c98.js";import"./entities.48a44fec.js";import"./linkify-it.c5e8196e.js";import"./punycode.js.4b3f125a.js";import"./highlight.js.cbbfb885.js";import"./markdown-it-link-attributes.e1d5d151.js";import"./@traptitech.acea8861.js";import"./vue.adba9046.js";import"./openpgp_hi.15f91b1d.js";import"./axios.37c7f908.js";import"./mitt.1ea0a2a3.js";import"./quill-hi.ca2ea0cc.js";import"./parchment.d5c5924e.js";import"./quill-delta.385a10bf.js";import"./fast-diff.f17881f3.js";import"./lodash.clonedeep.3cc09a31.js";import"./lodash.isequal.dbdc2157.js";import"./eventemitter3.78b735ad.js";import"./lodash-es.76e3a28b.js";import"./quill-mention-hi.4eeb5a2d.js";import"./view-design-hi.f1128b4d.js";import"./html-to-md.f297036e.js";import"./lodash.8fcd6fd4.js";import"./vue-router.2d566cd7.js";import"./vue-clipboard2.fd43a5bc.js";import"./clipboard.37b37361.js";import"./vuedraggable.f464b992.js";import"./sortablejs.3488b922.js";import"./vue-resize-observer.5af23a43.js";import"./element-sea.f8a64907.js";import"./deepmerge.cecf392e.js";import"./resize-observer-polyfill.5d591c5f.js";import"./throttle-debounce.7c3948b2.js";import"./babel-helper-vue-jsx-merge-props.5ed215c3.js";import"./normalize-wheel.2a034b9f.js";import"./async-validator.dca2b951.js";import"./babel-runtime.4773988a.js";import"./core-js.314b4a1d.js";var l=function(){var t=this,e=t.$createElement,i=t._self._c||e;return i("div",{staticClass:"drawio-content"},[i("IFrame",{ref:"frame",staticClass:"drawio-iframe",attrs:{src:t.url},on:{"on-message":t.onMessage}}),t.loadIng?i("div",{staticClass:"drawio-loading"},[i("Loading")],1):t._e()],1)},d=[];const u={name:"Drawio",components:{IFrame:m},props:{value:{type:Object,default:function(){return{}}},title:{type:String,default:""},readOnly:{type:Boolean,default:!1}},data(){return{loadIng:!0,url:null,bakData:""}},created(){let t=o;switch(o){case"zh-CHT":t="zh-tw";break}let e=this.readOnly?1:0,i=this.readOnly?0:1,n=this.themeName==="dark"?"dark":"kennedy",r=`?title=${this.title?encodeURIComponent(this.title):""}&chrome=${i}&lightbox=${e}&ui=${n}&lang=${t}&offline=1&pwa=0&embed=1&noLangIcon=1&noExitBtn=1&noSaveBtn=1&saveAndExit=0&spin=1&proto=json`;this.$Electron?this.url=$A.originUrl(`drawio/webapp/index.html${r}`):this.url=$A.mainUrl(`drawio/webapp/${r}`)},mounted(){window.addEventListener("message",this.handleMessage)},beforeDestroy(){window.removeEventListener("message",this.handleMessage)},watch:{value:{handler(t){this.bakData!=$A.jsonStringify(t)&&(this.bakData=$A.jsonStringify(t),this.updateContent())},deep:!0}},computed:{...s(["themeName"])},methods:{formatZoom(t){return t+"%"},updateContent(){this.$refs.frame.postMessage(JSON.stringify({action:"load",autosave:1,xml:this.value.xml}))},onMessage(t){switch(t.event){case"init":this.loadIng=!1,this.updateContent();break;case"load":typeof this.value.xml=="undefined"&&this.$refs.frame.postMessage(JSON.stringify({action:"template"}));break;case"autosave":const e={xml:t.xml};this.bakData=$A.jsonStringify(e),this.$emit("input",e);break;case"save":this.$emit("saveData");break}}}},a={};var c=p(u,l,d,!1,h,"39021859",null,null);function h(t){for(let e in a)this[e]=a[e]}var pt=function(){return c.exports}();export{pt as default}; import{m as s}from"./vuex.cc7cb26e.js";import{I as m}from"./IFrame.02468084.js";import{n as p,l as o}from"./app.1e00fc89.js";import"./jquery.99163cb9.js";import"./@babel.9410f858.js";import"./dayjs.0bb0f368.js";import"./localforage.6e50a401.js";import"./markdown-it.0450edb4.js";import"./mdurl.ce6c1dd8.js";import"./uc.micro.8d343c98.js";import"./entities.48a44fec.js";import"./linkify-it.c5e8196e.js";import"./punycode.js.4b3f125a.js";import"./highlight.js.cbbfb885.js";import"./markdown-it-link-attributes.e1d5d151.js";import"./@traptitech.acea8861.js";import"./vue.adba9046.js";import"./openpgp_hi.15f91b1d.js";import"./axios.37c7f908.js";import"./mitt.1ea0a2a3.js";import"./quill-hi.ca2ea0cc.js";import"./parchment.d5c5924e.js";import"./quill-delta.385a10bf.js";import"./fast-diff.f17881f3.js";import"./lodash.clonedeep.3cc09a31.js";import"./lodash.isequal.dbdc2157.js";import"./eventemitter3.78b735ad.js";import"./lodash-es.76e3a28b.js";import"./quill-mention-hi.4eeb5a2d.js";import"./view-design-hi.f1128b4d.js";import"./html-to-md.f297036e.js";import"./lodash.8fcd6fd4.js";import"./vue-router.2d566cd7.js";import"./vue-clipboard2.fd43a5bc.js";import"./clipboard.37b37361.js";import"./vuedraggable.f464b992.js";import"./sortablejs.3488b922.js";import"./vue-resize-observer.5af23a43.js";import"./element-sea.f8a64907.js";import"./deepmerge.cecf392e.js";import"./resize-observer-polyfill.5d591c5f.js";import"./throttle-debounce.7c3948b2.js";import"./babel-helper-vue-jsx-merge-props.5ed215c3.js";import"./normalize-wheel.2a034b9f.js";import"./async-validator.dca2b951.js";import"./babel-runtime.4773988a.js";import"./core-js.314b4a1d.js";var l=function(){var t=this,e=t.$createElement,i=t._self._c||e;return i("div",{staticClass:"drawio-content"},[i("IFrame",{ref:"frame",staticClass:"drawio-iframe",attrs:{src:t.url},on:{"on-message":t.onMessage}}),t.loadIng?i("div",{staticClass:"drawio-loading"},[i("Loading")],1):t._e()],1)},d=[];const u={name:"Drawio",components:{IFrame:m},props:{value:{type:Object,default:function(){return{}}},title:{type:String,default:""},readOnly:{type:Boolean,default:!1}},data(){return{loadIng:!0,url:null,bakData:""}},created(){let t=o;switch(o){case"zh-CHT":t="zh-tw";break}let e=this.readOnly?1:0,i=this.readOnly?0:1,n=this.themeName==="dark"?"dark":"kennedy",r=`?title=${this.title?encodeURIComponent(this.title):""}&chrome=${i}&lightbox=${e}&ui=${n}&lang=${t}&offline=1&pwa=0&embed=1&noLangIcon=1&noExitBtn=1&noSaveBtn=1&saveAndExit=0&spin=1&proto=json`;this.$Electron?this.url=$A.originUrl(`drawio/webapp/index.html${r}`):this.url=$A.mainUrl(`drawio/webapp/${r}`)},mounted(){window.addEventListener("message",this.handleMessage)},beforeDestroy(){window.removeEventListener("message",this.handleMessage)},watch:{value:{handler(t){this.bakData!=$A.jsonStringify(t)&&(this.bakData=$A.jsonStringify(t),this.updateContent())},deep:!0}},computed:{...s(["themeName"])},methods:{formatZoom(t){return t+"%"},updateContent(){this.$refs.frame.postMessage(JSON.stringify({action:"load",autosave:1,xml:this.value.xml}))},onMessage(t){switch(t.event){case"init":this.loadIng=!1,this.updateContent();break;case"load":typeof this.value.xml=="undefined"&&this.$refs.frame.postMessage(JSON.stringify({action:"template"}));break;case"autosave":const e={xml:t.xml};this.bakData=$A.jsonStringify(e),this.$emit("input",e);break;case"save":this.$emit("saveData");break}}}},a={};var c=p(u,l,d,!1,h,"86e491c2",null,null);function h(t){for(let e in a)this[e]=a[e]}var pt=function(){return c.exports}();export{pt as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
import{n}from"./app.ec83cdf6.js";var i=function(){var e=this,s=e.$createElement,r=e._self._c||s;return r("iframe",{directives:[{name:"show",rawName:"v-show",value:e.src,expression:"src"}],ref:"iframe",attrs:{src:e.src}})},a=[];const o={name:"IFrame",props:{src:{type:String,default:""}},mounted(){this.$refs.iframe.addEventListener("load",this.handleLoad),window.addEventListener("message",this.handleMessage)},beforeDestroy(){this.$refs.iframe.removeEventListener("load",this.handleLoad),window.removeEventListener("message",this.handleMessage)},methods:{handleLoad(){this.$emit("on-load")},handleMessage({data:e,source:s}){var r;s===((r=this.$refs.iframe)==null?void 0:r.contentWindow)&&(e=$A.jsonParse(e),e.source==="fileView"&&e.action==="picture"&&this.$store.dispatch("previewImage",{index:e.params.index,list:e.params.array}),this.$emit("on-message",e))},postMessage(e,s="*"){this.$refs.iframe&&this.$refs.iframe.contentWindow.postMessage(e,s)}}},t={};var m=n(o,i,a,!1,c,null,null,null);function c(e){for(let s in t)this[s]=t[s]}var l=function(){return m.exports}();export{l as I}; import{n}from"./app.1e00fc89.js";var i=function(){var e=this,s=e.$createElement,r=e._self._c||s;return r("iframe",{directives:[{name:"show",rawName:"v-show",value:e.src,expression:"src"}],ref:"iframe",attrs:{src:e.src}})},a=[];const o={name:"IFrame",props:{src:{type:String,default:""}},mounted(){this.$refs.iframe.addEventListener("load",this.handleLoad),window.addEventListener("message",this.handleMessage)},beforeDestroy(){this.$refs.iframe.removeEventListener("load",this.handleLoad),window.removeEventListener("message",this.handleMessage)},methods:{handleLoad(){this.$emit("on-load")},handleMessage({data:e,source:s}){var r;s===((r=this.$refs.iframe)==null?void 0:r.contentWindow)&&(e=$A.jsonParse(e),e.source==="fileView"&&e.action==="picture"&&this.$store.dispatch("previewImage",{index:e.params.index,list:e.params.array}),this.$emit("on-message",e))},postMessage(e,s="*"){this.$refs.iframe&&this.$refs.iframe.contentWindow.postMessage(e,s)}}},t={};var m=n(o,i,a,!1,c,null,null,null);function c(e){for(let s in t)this[s]=t[s]}var l=function(){return m.exports}();export{l as I};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
.component-only-office[data-v-60cfc883]{position:absolute;top:0;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center}.component-only-office .placeholder[data-v-60cfc883]{flex:1;width:100%;height:100%}.component-only-office .office-loading[data-v-60cfc883]{position:absolute;top:0;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center;z-index:2}.component-only-office .load-error{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1;padding:8px;display:flex;align-items:center}.component-only-office .load-error .ivu-alert-icon{position:static;margin-right:8px;margin-left:4px} .component-only-office[data-v-5e3aa26e]{position:absolute;top:0;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center}.component-only-office .placeholder[data-v-5e3aa26e]{flex:1;width:100%;height:100%}.component-only-office .office-loading[data-v-5e3aa26e]{position:absolute;top:0;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center;z-index:2}.component-only-office .load-error{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1;padding:8px;display:flex;align-items:center}.component-only-office .load-error .ivu-alert-icon{position:static;margin-right:8px;margin-left:4px}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
import{n as r}from"./app.ec83cdf6.js";var a=function(){var t=this,n=t.$createElement,e=t._self._c||n;return t.windowTouch?e("div",[e("Button",{attrs:{loading:t.loading,type:"primary",icon:"ios-search"},on:{click:t.onSearch}},[t._v(t._s(t.$L("\u641C\u7D22")))]),t.filtering?e("Button",{attrs:{type:"text"},on:{click:t.onCancelFilter}},[t._v(t._s(t.$L("\u53D6\u6D88\u7B5B\u9009")))]):e("Button",{attrs:{loading:t.loading,type:"text",icon:"md-refresh"},on:{click:t.onRefresh}},[t._v(t._s(t.$L("\u5237\u65B0")))])],1):e("Tooltip",{attrs:{theme:"light",placement:t.placement,"transfer-class-name":"search-button-clear",transfer:""}},[e("Button",{attrs:{loading:t.loading,type:"primary",icon:"ios-search"},on:{click:t.onSearch}},[t._v(t._s(t.$L("\u641C\u7D22")))]),e("div",{attrs:{slot:"content"},slot:"content"},[t.filtering?e("Button",{attrs:{type:"text"},on:{click:t.onCancelFilter}},[t._v(t._s(t.$L("\u53D6\u6D88\u7B5B\u9009")))]):e("Button",{attrs:{loading:t.loading,type:"text"},on:{click:t.onRefresh}},[t._v(t._s(t.$L("\u5237\u65B0")))])],1)],1)},i=[];const l={name:"SearchButton",props:{loading:{type:Boolean,default:!1},filtering:{type:Boolean,default:!1},placement:{type:String,default:"bottom"}},methods:{onSearch(){this.$emit("search")},onRefresh(){this.$emit("refresh")},onCancelFilter(){this.$emit("cancelFilter")}}},o={};var s=r(l,a,i,!1,c,null,null,null);function c(t){for(let n in o)this[n]=o[n]}var h=function(){return s.exports}();export{h as S}; import{n as r}from"./app.1e00fc89.js";var a=function(){var t=this,n=t.$createElement,e=t._self._c||n;return t.windowTouch?e("div",[e("Button",{attrs:{loading:t.loading,type:"primary",icon:"ios-search"},on:{click:t.onSearch}},[t._v(t._s(t.$L("\u641C\u7D22")))]),t.filtering?e("Button",{attrs:{type:"text"},on:{click:t.onCancelFilter}},[t._v(t._s(t.$L("\u53D6\u6D88\u7B5B\u9009")))]):e("Button",{attrs:{loading:t.loading,type:"text",icon:"md-refresh"},on:{click:t.onRefresh}},[t._v(t._s(t.$L("\u5237\u65B0")))])],1):e("Tooltip",{attrs:{theme:"light",placement:t.placement,"transfer-class-name":"search-button-clear",transfer:""}},[e("Button",{attrs:{loading:t.loading,type:"primary",icon:"ios-search"},on:{click:t.onSearch}},[t._v(t._s(t.$L("\u641C\u7D22")))]),e("div",{attrs:{slot:"content"},slot:"content"},[t.filtering?e("Button",{attrs:{type:"text"},on:{click:t.onCancelFilter}},[t._v(t._s(t.$L("\u53D6\u6D88\u7B5B\u9009")))]):e("Button",{attrs:{loading:t.loading,type:"text"},on:{click:t.onRefresh}},[t._v(t._s(t.$L("\u5237\u65B0")))])],1)],1)},i=[];const l={name:"SearchButton",props:{loading:{type:Boolean,default:!1},filtering:{type:Boolean,default:!1},placement:{type:String,default:"bottom"}},methods:{onSearch(){this.$emit("search")},onRefresh(){this.$emit("refresh")},onCancelFilter(){this.$emit("cancelFilter")}}},o={};var s=r(l,a,i,!1,c,null,null,null);function c(t){for(let n in o)this[n]=o[n]}var h=function(){return s.exports}();export{h as S};

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
.task-editor[data-v-4e70a0a5]{position:relative;word-break:break-all}.task-editor[data-v-4e70a0a5] .mce-content-body,.task-editor[data-v-4e70a0a5] .task-editor-content{line-height:1.6}.task-editor[data-v-4e70a0a5] p{margin:.3em 0}.task-editor[data-v-4e70a0a5] blockquote,.task-editor[data-v-4e70a0a5] pre,.task-editor[data-v-4e70a0a5] ul,.task-editor[data-v-4e70a0a5] ol{margin:1em 0}.task-editor[data-v-4e70a0a5] ul,.task-editor[data-v-4e70a0a5] ol{margin-left:1.5em;padding-left:1.5em}.task-editor[data-v-4e70a0a5] li{margin:.25em 0}.task-editor[data-v-4e70a0a5] h1{margin:.67em 0}.task-editor[data-v-4e70a0a5] h2{margin:.83em 0}.task-editor[data-v-4e70a0a5] h3{margin:1em 0}.task-editor[data-v-4e70a0a5] h4{margin:1.33em 0}.task-editor[data-v-4e70a0a5] h5{margin:1.67em 0}.task-editor[data-v-4e70a0a5] h6{margin:2.33em 0}.task-editor .task-editor-operate[data-v-4e70a0a5]{position:absolute;top:0;left:0;width:1px;opacity:0;visibility:hidden;pointer-events:none}.task-tag-select[data-v-e09d999e]{width:100%;display:flex;flex-direction:column}.task-tag-select.no-search .search-box[data-v-e09d999e]{display:none}.task-tag-select.no-search .tag-list .tag-item[data-v-e09d999e]:first-child{margin-top:0}.task-tag-select .search-box[data-v-e09d999e]{padding-bottom:8px;border-bottom:1px solid #eee}.task-tag-select .search-box .search-input[data-v-e09d999e]{width:100%;height:34px;padding:0 12px;border:1px solid #dcdfe6;border-radius:4px;outline:none}.task-tag-select .search-box .search-input[data-v-e09d999e]:focus{border-color:#84c56a}.task-tag-select .tag-list[data-v-e09d999e]{flex:1;overflow-y:auto;max-height:300px;margin:0 -12px;padding:0 12px}.task-tag-select .tag-list .tag-item[data-v-e09d999e]{display:flex;align-items:flex-start;padding:8px 12px;cursor:pointer;border-radius:6px;margin-bottom:6px}.task-tag-select .tag-list .tag-item[data-v-e09d999e]:first-child{margin-top:12px}.task-tag-select .tag-list .tag-item[data-v-e09d999e]:last-child{margin-bottom:12px}.task-tag-select .tag-list .tag-item[data-v-e09d999e]:hover{background-color:#f5f7fa}.task-tag-select .tag-list .tag-item.is-selected[data-v-e09d999e]{background-color:#ecf5ff}.task-tag-select .tag-list .tag-item .tag-color[data-v-e09d999e]{width:16px;height:16px;border-radius:4px;margin-right:8px;margin-top:2px}.task-tag-select .tag-list .tag-item .tag-info[data-v-e09d999e]{flex:1}.task-tag-select .tag-list .tag-item .tag-info .tag-name[data-v-e09d999e]{line-height:20px;font-size:14px;color:#303133}.task-tag-select .tag-list .tag-item .tag-info .tag-desc[data-v-e09d999e]{font-size:12px;color:#909399;margin-top:2px}.task-tag-select .tag-list .tag-item .tag-check[data-v-e09d999e]{color:#84c56a;margin-left:12px;height:20px;display:flex;align-items:center}.task-tag-select .tag-list .no-data[data-v-e09d999e]{text-align:center;color:#909399;padding:24px 0;margin-bottom:12px}.task-tag-select .footer-box[data-v-e09d999e]{border-top:1px solid #eee;padding-top:8px}.task-tag-select .footer-box .add-button[data-v-e09d999e]{display:flex;align-items:center;justify-content:center;padding:4px 0 2px;cursor:pointer;color:#84c56a;border-radius:6px;transition:color .2s}.task-tag-select .footer-box .add-button[data-v-e09d999e]:hover{color:#a2d98d}.task-tag-select .footer-box .add-button i[data-v-e09d999e]{margin-right:4px}.task-content-history .ivu-page[data-v-a0030d34]{margin-top:12px;display:flex;align-items:center;justify-content:center} .task-editor[data-v-4e70a0a5]{position:relative;word-break:break-all}.task-editor[data-v-4e70a0a5] .mce-content-body,.task-editor[data-v-4e70a0a5] .task-editor-content{line-height:1.6}.task-editor[data-v-4e70a0a5] p{margin:.3em 0}.task-editor[data-v-4e70a0a5] blockquote,.task-editor[data-v-4e70a0a5] pre,.task-editor[data-v-4e70a0a5] ul,.task-editor[data-v-4e70a0a5] ol{margin:1em 0}.task-editor[data-v-4e70a0a5] ul,.task-editor[data-v-4e70a0a5] ol{margin-left:1.5em;padding-left:1.5em}.task-editor[data-v-4e70a0a5] li{margin:.25em 0}.task-editor[data-v-4e70a0a5] h1{margin:.67em 0}.task-editor[data-v-4e70a0a5] h2{margin:.83em 0}.task-editor[data-v-4e70a0a5] h3{margin:1em 0}.task-editor[data-v-4e70a0a5] h4{margin:1.33em 0}.task-editor[data-v-4e70a0a5] h5{margin:1.67em 0}.task-editor[data-v-4e70a0a5] h6{margin:2.33em 0}.task-editor .task-editor-operate[data-v-4e70a0a5]{position:absolute;top:0;left:0;width:1px;opacity:0;visibility:hidden;pointer-events:none}.task-tag-select[data-v-5700cf7a]{width:100%;display:flex;flex-direction:column}.task-tag-select.no-search .search-box[data-v-5700cf7a]{display:none}.task-tag-select.no-search .tag-list .tag-item[data-v-5700cf7a]:first-child{margin-top:0}.task-tag-select .search-box[data-v-5700cf7a]{padding-bottom:8px;border-bottom:1px solid #eee}.task-tag-select .search-box .search-input[data-v-5700cf7a]{width:100%;height:34px;padding:0 12px;border:1px solid #dcdfe6;border-radius:4px;outline:none}.task-tag-select .search-box .search-input[data-v-5700cf7a]:focus{border-color:#84c56a}.task-tag-select .tag-list[data-v-5700cf7a]{flex:1;overflow-y:auto;max-height:300px;margin:0 -12px;padding:0 12px}.task-tag-select .tag-list .tag-item[data-v-5700cf7a]{display:flex;align-items:flex-start;padding:8px 12px;cursor:pointer;border-radius:6px;margin-bottom:6px}.task-tag-select .tag-list .tag-item[data-v-5700cf7a]:first-child{margin-top:12px}.task-tag-select .tag-list .tag-item[data-v-5700cf7a]:last-child{margin-bottom:12px}.task-tag-select .tag-list .tag-item[data-v-5700cf7a]:hover{background-color:#f5f7fa}.task-tag-select .tag-list .tag-item.is-selected[data-v-5700cf7a]{background-color:#ecf5ff}.task-tag-select .tag-list .tag-item .tag-color[data-v-5700cf7a]{width:16px;height:16px;border-radius:4px;margin-right:8px;margin-top:2px}.task-tag-select .tag-list .tag-item .tag-info[data-v-5700cf7a]{flex:1}.task-tag-select .tag-list .tag-item .tag-info .tag-name[data-v-5700cf7a]{line-height:20px;font-size:14px;color:#303133}.task-tag-select .tag-list .tag-item .tag-info .tag-desc[data-v-5700cf7a]{font-size:12px;color:#909399;margin-top:2px}.task-tag-select .tag-list .tag-item .tag-check[data-v-5700cf7a]{color:#84c56a;margin-left:12px;height:20px;display:flex;align-items:center}.task-tag-select .tag-list .no-data[data-v-5700cf7a]{text-align:center;color:#909399;padding:24px 0;margin-bottom:12px}.task-tag-select .footer-box[data-v-5700cf7a]{border-top:1px solid #eee;padding-top:8px}.task-tag-select .footer-box .add-button[data-v-5700cf7a]{display:flex;align-items:center;justify-content:center;padding:4px 0 2px;cursor:pointer;color:#84c56a;border-radius:6px;transition:color .2s}.task-tag-select .footer-box .add-button[data-v-5700cf7a]:hover{color:#a2d98d}.task-tag-select .footer-box .add-button i[data-v-5700cf7a]{margin-right:4px}.task-content-history .ivu-page[data-v-a0030d34]{margin-top:12px;display:flex;align-items:center;justify-content:center}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

7
public/js/build/app.fcecaabb.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.map-select-container[data-v-b1904dc8]{display:flex;gap:20px;height:500px}@media (width < 768px){.map-select-container[data-v-b1904dc8]{flex-direction:column;height:700px}.map-select-container .map-radius-control[data-v-b1904dc8]{width:100%;border-left:0;padding-left:0}}.map-select-iframe-container[data-v-b1904dc8]{flex:1}.map-select-point-iframe[data-v-b1904dc8]{width:100%;height:100%;border:0;border-radius:12px}.map-radius-control[data-v-b1904dc8]{width:280px;border-left:1px solid #e8e8e8;padding-left:20px;display:flex;flex-direction:column}.radius-control-header[data-v-b1904dc8]{margin-bottom:15px}.radius-control-header h4[data-v-b1904dc8]{margin:0;font-size:16px;font-weight:600;color:#333}.radius-control-body[data-v-b1904dc8]{flex:1;display:flex;flex-direction:column}.location-info[data-v-b1904dc8]{margin:15px 0;padding:12px;background:#f8f9fa;border-radius:6px}.info-item[data-v-b1904dc8]{display:flex;justify-content:space-between;margin-bottom:8px;font-size:14px}.info-item[data-v-b1904dc8]:last-child{margin-bottom:0}.info-label[data-v-b1904dc8]{color:#666}.info-value[data-v-b1904dc8]{color:#333;font-weight:500}.radius-control-tip[data-v-b1904dc8]{background:#f0f8ff;padding:12px;border-radius:6px;border-left:3px solid #007cff;font-size:13px;color:#333;line-height:1.4;margin-top:auto}

View File

@ -1 +0,0 @@
.map-select-container[data-v-6135b0b6]{display:flex;gap:20px;height:500px}@media (width < 768px){.map-select-container[data-v-6135b0b6]{flex-direction:column;height:700px}.map-select-container .map-radius-control[data-v-6135b0b6]{width:100%;border-left:0;padding-left:0}}.map-select-iframe-container[data-v-6135b0b6]{flex:1}.map-select-point-iframe[data-v-6135b0b6]{width:100%;height:100%;border:0;border-radius:12px}.map-radius-control[data-v-6135b0b6]{width:280px;border-left:1px solid #e8e8e8;padding-left:20px;display:flex;flex-direction:column}.radius-control-header[data-v-6135b0b6]{margin-bottom:15px}.radius-control-header h4[data-v-6135b0b6]{margin:0;font-size:16px;font-weight:600;color:#333}.radius-control-body[data-v-6135b0b6]{flex:1;display:flex;flex-direction:column}.location-info[data-v-6135b0b6]{margin:15px 0;padding:12px;background:#f8f9fa;border-radius:6px}.info-item[data-v-6135b0b6]{display:flex;justify-content:space-between;margin-bottom:8px;font-size:14px}.info-item[data-v-6135b0b6]:last-child{margin-bottom:0}.info-label[data-v-6135b0b6]{color:#666}.info-value[data-v-6135b0b6]{color:#333;font-weight:500}.radius-control-tip[data-v-6135b0b6]{background:#f0f8ff;padding:12px;border-radius:6px;border-left:3px solid #007cff;font-size:13px;color:#333;line-height:1.4;margin-top:auto}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
import{m}from"./vuex.cc7cb26e.js";import{M as e}from"./index.4aac8251.js";import{n as a}from"./app.ec83cdf6.js";import"./vue.adba9046.js";import"./@babel.9410f858.js";import"./view-design-hi.f1128b4d.js";import"./@micro-zoe.39406924.js";import"./DialogWrapper.c0d27c52.js";import"./chunkedUpload.cf92914b.js";import"./axios.37c7f908.js";import"./spark-md5.0443325f.js";import"./index.3118da17.js";import"./vue-virtual-scroll-list-hi.74ad83f0.js";import"./lodash.8fcd6fd4.js";import"./ImgUpload.a61f7e6a.js";import"./webhook.378987f3.js";import"./jquery.020cc780.js";import"./dayjs.5bb9b94f.js";import"./localforage.982e07b7.js";import"./markdown-it.0450edb4.js";import"./mdurl.ce6c1dd8.js";import"./uc.micro.8d343c98.js";import"./entities.48a44fec.js";import"./linkify-it.c5e8196e.js";import"./punycode.js.4b3f125a.js";import"./highlight.js.cbbfb885.js";import"./markdown-it-link-attributes.e1d5d151.js";import"./@traptitech.acea8861.js";import"./openpgp_hi.15f91b1d.js";import"./mitt.1ea0a2a3.js";import"./quill-hi.ca2ea0cc.js";import"./parchment.d5c5924e.js";import"./quill-delta.385a10bf.js";import"./fast-diff.f17881f3.js";import"./lodash.clonedeep.3cc09a31.js";import"./lodash.isequal.dbdc2157.js";import"./eventemitter3.78b735ad.js";import"./lodash-es.76e3a28b.js";import"./quill-mention-hi.4eeb5a2d.js";import"./html-to-md.f297036e.js";import"./vue-router.2d566cd7.js";import"./vue-clipboard2.fd43a5bc.js";import"./clipboard.37b37361.js";import"./vuedraggable.f464b992.js";import"./sortablejs.3488b922.js";import"./vue-resize-observer.5af23a43.js";import"./element-sea.f8a64907.js";import"./deepmerge.cecf392e.js";import"./resize-observer-polyfill.5d591c5f.js";import"./throttle-debounce.7c3948b2.js";import"./babel-helper-vue-jsx-merge-props.5ed215c3.js";import"./normalize-wheel.2a034b9f.js";import"./async-validator.dca2b951.js";import"./babel-runtime.4773988a.js";import"./core-js.314b4a1d.js";var n=function(){var t=this,o=t.$createElement,r=t._self._c||o;return r("MicroApps",{ref:"app",attrs:{"window-type":"popout"}})},s=[];const u={components:{MicroApps:e},computed:{...m(["userIsAdmin"])},async mounted(){const{name:t}=this.$route.params;if(!t){$A.modalError("\u5E94\u7528\u4E0D\u5B58\u5728");return}if(t==="iframe-test"){if(!this.userIsAdmin){$A.modalError("\u4EC5\u7BA1\u7406\u5458\u53EF\u4F7F\u7528\u6B64\u529F\u80FD");return}let{url:r}=this.$route.query;if(!r){if(r=await this.promptIframeUrl(),!r)return;this.$router.replace({path:this.$route.path,query:{...this.$route.query,url:r}}).catch(()=>{})}await this.$refs.app.onOpen({id:"iframe-test",name:"iframe-test",url:r,type:"iframe",transparent:!0,keep_alive:!1});return}const o=(await $A.IDBArray("cacheMicroApps")).reverse().find(r=>r.name===t);if(!o){$A.modalError("\u5E94\u7528\u4E0D\u5B58\u5728");return}await this.$refs.app.onOpen(o)},methods:{promptIframeUrl(){return new Promise((t,o)=>{$A.modalInput({title:this.$L("\u8BF7\u8F93\u5165 URL"),placeholder:"https://example.com",onOk:r=>{const i=(r||"").trim();if(!i)return this.$L("URL\u4E0D\u80FD\u4E3A\u7A7A");t(i)},onCancel:()=>o()})}).catch(()=>null)}}},p={};var c=a(u,n,s,!1,l,null,null,null);function l(t){for(let o in p)this[o]=p[o]}var hr=function(){return c.exports}();export{hr as default}; import{m}from"./vuex.cc7cb26e.js";import{M as e}from"./index.4ff51c76.js";import{n as a}from"./app.1e00fc89.js";import"./vue.adba9046.js";import"./@babel.9410f858.js";import"./view-design-hi.f1128b4d.js";import"./@micro-zoe.39406924.js";import"./DialogWrapper.d635cc01.js";import"./chunkedUpload.b0e8368a.js";import"./axios.37c7f908.js";import"./spark-md5.0443325f.js";import"./index.978d6d4c.js";import"./vue-virtual-scroll-list-hi.74ad83f0.js";import"./lodash.8fcd6fd4.js";import"./ImgUpload.be73943e.js";import"./webhook.378987f3.js";import"./jquery.99163cb9.js";import"./dayjs.0bb0f368.js";import"./localforage.6e50a401.js";import"./markdown-it.0450edb4.js";import"./mdurl.ce6c1dd8.js";import"./uc.micro.8d343c98.js";import"./entities.48a44fec.js";import"./linkify-it.c5e8196e.js";import"./punycode.js.4b3f125a.js";import"./highlight.js.cbbfb885.js";import"./markdown-it-link-attributes.e1d5d151.js";import"./@traptitech.acea8861.js";import"./openpgp_hi.15f91b1d.js";import"./mitt.1ea0a2a3.js";import"./quill-hi.ca2ea0cc.js";import"./parchment.d5c5924e.js";import"./quill-delta.385a10bf.js";import"./fast-diff.f17881f3.js";import"./lodash.clonedeep.3cc09a31.js";import"./lodash.isequal.dbdc2157.js";import"./eventemitter3.78b735ad.js";import"./lodash-es.76e3a28b.js";import"./quill-mention-hi.4eeb5a2d.js";import"./html-to-md.f297036e.js";import"./vue-router.2d566cd7.js";import"./vue-clipboard2.fd43a5bc.js";import"./clipboard.37b37361.js";import"./vuedraggable.f464b992.js";import"./sortablejs.3488b922.js";import"./vue-resize-observer.5af23a43.js";import"./element-sea.f8a64907.js";import"./deepmerge.cecf392e.js";import"./resize-observer-polyfill.5d591c5f.js";import"./throttle-debounce.7c3948b2.js";import"./babel-helper-vue-jsx-merge-props.5ed215c3.js";import"./normalize-wheel.2a034b9f.js";import"./async-validator.dca2b951.js";import"./babel-runtime.4773988a.js";import"./core-js.314b4a1d.js";var n=function(){var t=this,o=t.$createElement,r=t._self._c||o;return r("MicroApps",{ref:"app",attrs:{"window-type":"popout"}})},s=[];const u={components:{MicroApps:e},computed:{...m(["userIsAdmin"])},async mounted(){const{name:t}=this.$route.params;if(!t){$A.modalError("\u5E94\u7528\u4E0D\u5B58\u5728");return}if(t==="iframe-test"){if(!this.userIsAdmin){$A.modalError("\u4EC5\u7BA1\u7406\u5458\u53EF\u4F7F\u7528\u6B64\u529F\u80FD");return}let{url:r}=this.$route.query;if(!r){if(r=await this.promptIframeUrl(),!r)return;this.$router.replace({path:this.$route.path,query:{...this.$route.query,url:r}}).catch(()=>{})}await this.$refs.app.onOpen({id:"iframe-test",name:"iframe-test",url:r,type:"iframe",transparent:!0,keep_alive:!1});return}const o=(await $A.IDBArray("cacheMicroApps")).reverse().find(r=>r.name===t);if(!o){$A.modalError("\u5E94\u7528\u4E0D\u5B58\u5728");return}await this.$refs.app.onOpen(o)},methods:{promptIframeUrl(){return new Promise((t,o)=>{$A.modalInput({title:this.$L("\u8BF7\u8F93\u5165 URL"),placeholder:"https://example.com",onOk:r=>{const i=(r||"").trim();if(!i)return this.$L("URL\u4E0D\u80FD\u4E3A\u7A7A");t(i)},onCancel:()=>o()})}).catch(()=>null)}}},p={};var c=a(u,n,s,!1,l,null,null,null);function l(t){for(let o in p)this[o]=p[o]}var hr=function(){return c.exports}();export{hr as default};

1
public/js/build/calendar.b1e80cc6.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
public/js/build/checkin.c10b2ea3.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
import{a as k}from"./axios.37c7f908.js";import{S as x}from"./spark-md5.0443325f.js";import{s as D,l as O}from"./app.ec83cdf6.js";const j=10*1024*1024,U=3,M=3,C="chunked_upload:",P=new Set(["\u4E0A\u4F20\u4F1A\u8BDD\u4E0D\u5B58\u5728\u6216\u5DF2\u8FC7\u671F","\u4E0A\u4F20\u4F1A\u8BDD\u5F52\u5C5E\u9519\u8BEF","\u5206\u7247\u5E8F\u53F7\u8D85\u51FA\u8303\u56F4","\u5206\u7247\u5927\u5C0F\u4E0D\u7B26\u5408\u9884\u671F","\u672B\u5C3E\u5206\u7247\u5927\u5C0F\u4E0D\u7B26\u5408\u9884\u671F"]);function H(t,e){return t==="hashing"?Math.round(e*.05):t==="uploading"?5+Math.round(e*.9):95+Math.round(e*.05)}async function G({file:t,scene:e,sceneParams:d={},onProgress:a,onStart:i,signal:r}){const n=typeof a=="function"?a:()=>{},o=(s,h)=>n(H(s,h),s);o("hashing",0);const w=await B(t,r,(s,h)=>{o("hashing",Math.min(99,Math.round(s/h*100)))});if(o("hashing",100),r&&r.aborted)throw new Error("aborted");const c=await S("upload/init",{hash:w,size:t.size,name:t.name,scene:e,scene_params:d},r);if(c.ret!==1)throw new Error(c.msg||"\u4E0A\u4F20\u521D\u59CB\u5316\u5931\u8D25");if(c.data&&c.data.done)return o("uploading",100),o("merging",100),c.data;const{upload_id:p,chunk_size:l,chunk_count:g,received:A}=c.data;if(F(w,p,e),typeof i=="function")try{i(p)}catch{}const y=new Set((A||[]).map(Number)),f=[];for(let s=0;s<g;s++)y.has(s)||f.push(s);let E=y.size;const b=()=>{o("uploading",Math.min(99,Math.round(E/g*100)))};b();let N=0,u=null;const z=async()=>{for(;;){if(u)return;if(r&&r.aborted){u=new Error("aborted");return}const s=N++;if(s>=f.length)return;const h=f[s],_=h*l,L=Math.min(_+l,t.size),T=t.slice(_,L);try{await $(p,h,T,r),E++,b()}catch(v){u||(u=v);return}}},I=Math.min(U,f.length||1);if(await Promise.all(Array.from({length:I},()=>z())),u)throw u;o("uploading",100),o("merging",0);const m=await S("upload/merge",{upload_id:p},r);if(m.ret!==1)throw new Error(m.msg||"\u5408\u5E76\u5931\u8D25");return o("merging",100),X(w),m.data}async function $(t,e,d,a){let i;for(let r=0;r<=M;r++){if(a&&a.aborted)throw new Error("aborted");try{const n=new FormData;n.append("upload_id",t),n.append("index",String(e)),n.append("blob",d);const o=await k.post(window.$A.apiUrl("upload/chunk"),n,{headers:R(),signal:a});if(o.data&&o.data.ret===1)return o.data.data;i=new Error(o.data&&o.data.msg||"\u5206\u7247\u4E0A\u4F20\u5931\u8D25")}catch(n){if(a&&a.aborted)throw n;i=n}if(P.has(i.message))throw i;r<M&&await new Promise(n=>setTimeout(n,500*Math.pow(2,r)))}throw i||new Error("\u5206\u7247\u91CD\u8BD5\u8017\u5C3D")}async function S(t,e,d){return(await k.post(window.$A.apiUrl(t),e,{headers:{...R(),"Content-Type":"application/json"},signal:d})).data}function R(){return{token:D.state.userToken,language:O,platform:window.$A.Platform,version:window.systemInfo&&window.systemInfo.version||"0.0.1",fd:window.$A.getSessionStorageString("userWsFd")}}async function B(t,e,d){const a=new x.ArrayBuffer,i=2*1024*1024;for(let r=0;r<t.size;r+=i){if(e&&e.aborted)throw new Error("aborted");const n=Math.min(r+i,t.size),o=await t.slice(r,n).arrayBuffer();a.append(o),d(n,t.size),await new Promise(w=>setTimeout(w,0))}return a.end().toLowerCase()}function F(t,e,d){try{const a={upload_id:e,scene:d,t:Date.now()};localStorage.setItem(C+t,JSON.stringify(a))}catch{}}function X(t){try{localStorage.removeItem(C+t)}catch{}}export{j as C,G as c}; import{a as k}from"./axios.37c7f908.js";import{S as x}from"./spark-md5.0443325f.js";import{s as D,l as O}from"./app.1e00fc89.js";const j=10*1024*1024,U=3,M=3,C="chunked_upload:",P=new Set(["\u4E0A\u4F20\u4F1A\u8BDD\u4E0D\u5B58\u5728\u6216\u5DF2\u8FC7\u671F","\u4E0A\u4F20\u4F1A\u8BDD\u5F52\u5C5E\u9519\u8BEF","\u5206\u7247\u5E8F\u53F7\u8D85\u51FA\u8303\u56F4","\u5206\u7247\u5927\u5C0F\u4E0D\u7B26\u5408\u9884\u671F","\u672B\u5C3E\u5206\u7247\u5927\u5C0F\u4E0D\u7B26\u5408\u9884\u671F"]);function H(t,e){return t==="hashing"?Math.round(e*.05):t==="uploading"?5+Math.round(e*.9):95+Math.round(e*.05)}async function G({file:t,scene:e,sceneParams:d={},onProgress:a,onStart:i,signal:r}){const n=typeof a=="function"?a:()=>{},o=(s,h)=>n(H(s,h),s);o("hashing",0);const w=await B(t,r,(s,h)=>{o("hashing",Math.min(99,Math.round(s/h*100)))});if(o("hashing",100),r&&r.aborted)throw new Error("aborted");const c=await S("upload/init",{hash:w,size:t.size,name:t.name,scene:e,scene_params:d},r);if(c.ret!==1)throw new Error(c.msg||"\u4E0A\u4F20\u521D\u59CB\u5316\u5931\u8D25");if(c.data&&c.data.done)return o("uploading",100),o("merging",100),c.data;const{upload_id:p,chunk_size:l,chunk_count:g,received:A}=c.data;if(F(w,p,e),typeof i=="function")try{i(p)}catch{}const y=new Set((A||[]).map(Number)),f=[];for(let s=0;s<g;s++)y.has(s)||f.push(s);let E=y.size;const b=()=>{o("uploading",Math.min(99,Math.round(E/g*100)))};b();let N=0,u=null;const z=async()=>{for(;;){if(u)return;if(r&&r.aborted){u=new Error("aborted");return}const s=N++;if(s>=f.length)return;const h=f[s],_=h*l,L=Math.min(_+l,t.size),T=t.slice(_,L);try{await $(p,h,T,r),E++,b()}catch(v){u||(u=v);return}}},I=Math.min(U,f.length||1);if(await Promise.all(Array.from({length:I},()=>z())),u)throw u;o("uploading",100),o("merging",0);const m=await S("upload/merge",{upload_id:p},r);if(m.ret!==1)throw new Error(m.msg||"\u5408\u5E76\u5931\u8D25");return o("merging",100),X(w),m.data}async function $(t,e,d,a){let i;for(let r=0;r<=M;r++){if(a&&a.aborted)throw new Error("aborted");try{const n=new FormData;n.append("upload_id",t),n.append("index",String(e)),n.append("blob",d);const o=await k.post(window.$A.apiUrl("upload/chunk"),n,{headers:R(),signal:a});if(o.data&&o.data.ret===1)return o.data.data;i=new Error(o.data&&o.data.msg||"\u5206\u7247\u4E0A\u4F20\u5931\u8D25")}catch(n){if(a&&a.aborted)throw n;i=n}if(P.has(i.message))throw i;r<M&&await new Promise(n=>setTimeout(n,500*Math.pow(2,r)))}throw i||new Error("\u5206\u7247\u91CD\u8BD5\u8017\u5C3D")}async function S(t,e,d){return(await k.post(window.$A.apiUrl(t),e,{headers:{...R(),"Content-Type":"application/json"},signal:d})).data}function R(){return{token:D.state.userToken,language:O,platform:window.$A.Platform,version:window.systemInfo&&window.systemInfo.version||"0.0.1",fd:window.$A.getSessionStorageString("userWsFd")}}async function B(t,e,d){const a=new x.ArrayBuffer,i=2*1024*1024;for(let r=0;r<t.size;r+=i){if(e&&e.aborted)throw new Error("aborted");const n=Math.min(r+i,t.size),o=await t.slice(r,n).arrayBuffer();a.append(o),d(n,t.size),await new Promise(w=>setTimeout(w,0))}return a.end().toLowerCase()}function F(t,e,d){try{const a={upload_id:e,scene:d,t:Date.now()};localStorage.setItem(C+t,JSON.stringify(a))}catch{}}function X(t){try{localStorage.removeItem(C+t)}catch{}}export{j as C,G as c};

1
public/js/build/dashboard.80e25fd2.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
public/js/build/delete.065d4ffc.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
import{n as l}from"./app.ec83cdf6.js";import"./jquery.020cc780.js";import"./@babel.9410f858.js";import"./dayjs.5bb9b94f.js";import"./localforage.982e07b7.js";import"./markdown-it.0450edb4.js";import"./mdurl.ce6c1dd8.js";import"./uc.micro.8d343c98.js";import"./entities.48a44fec.js";import"./linkify-it.c5e8196e.js";import"./punycode.js.4b3f125a.js";import"./highlight.js.cbbfb885.js";import"./markdown-it-link-attributes.e1d5d151.js";import"./@traptitech.acea8861.js";import"./vue.adba9046.js";import"./vuex.cc7cb26e.js";import"./openpgp_hi.15f91b1d.js";import"./axios.37c7f908.js";import"./mitt.1ea0a2a3.js";import"./quill-hi.ca2ea0cc.js";import"./parchment.d5c5924e.js";import"./quill-delta.385a10bf.js";import"./fast-diff.f17881f3.js";import"./lodash.clonedeep.3cc09a31.js";import"./lodash.isequal.dbdc2157.js";import"./eventemitter3.78b735ad.js";import"./lodash-es.76e3a28b.js";import"./quill-mention-hi.4eeb5a2d.js";import"./view-design-hi.f1128b4d.js";import"./html-to-md.f297036e.js";import"./lodash.8fcd6fd4.js";import"./vue-router.2d566cd7.js";import"./vue-clipboard2.fd43a5bc.js";import"./clipboard.37b37361.js";import"./vuedraggable.f464b992.js";import"./sortablejs.3488b922.js";import"./vue-resize-observer.5af23a43.js";import"./element-sea.f8a64907.js";import"./deepmerge.cecf392e.js";import"./resize-observer-polyfill.5d591c5f.js";import"./throttle-debounce.7c3948b2.js";import"./babel-helper-vue-jsx-merge-props.5ed215c3.js";import"./normalize-wheel.2a034b9f.js";import"./async-validator.dca2b951.js";import"./babel-runtime.4773988a.js";import"./core-js.314b4a1d.js";var m=function(){var t=this,r=t.$createElement,i=t._self._c||r;return i("div",{staticClass:"setting-device"},[i("ul",[t.loadIng>0&&t.devices.length===0?i("li",{staticClass:"loading"},[i("Loading")],1):t._l(t.devices,function(e){return i("li",{key:e.id},[i("div",{staticClass:"icon"},[i("span",{class:t.getIcon(e.detail)})]),i("div",{staticClass:"info"},[i("div",{staticClass:"title"},[i("span",{staticClass:"name"},[t._v(t._s(t.getName(e.detail)))]),i("span",{staticClass:"device"},[t._v(t._s(t.getOs(e.detail)))])]),i("div",{staticClass:"time"},[i("EPopover",{attrs:{placement:"bottom-start",trigger:"click"}},[i("div",{staticClass:"setting-device-popover"},[i("p",[t._v(t._s(t.$L("\u767B\u5F55\u65F6\u95F4"))+": "+t._s(e.created_at))]),i("p",[t._v(t._s(t.$L("\u66F4\u65B0\u65F6\u95F4"))+": "+t._s(e.updated_at))]),i("p",[t._v(t._s(t.$L("\u8FC7\u671F\u65F6\u95F4"))+": "+t._s(e.expired_at))])]),i("span",{attrs:{slot:"reference"},slot:"reference"},[t._v(t._s(e.updated_at))])])],1)]),i("div",[e.is_current?i("span",{staticClass:"current"},[t._v(t._s(t.$L("\u5F53\u524D\u8BBE\u5907")))]):i("Button",{on:{click:function(o){return t.onLogout(e)}}},[t._v(t._s(t.$L("\u9000\u51FA\u767B\u5F55")))])],1)])})],2)])},p=[];const c={name:"SettingDevice",data(){return{loadIng:0,devices:[]}},mounted(){this.getDeviceList()},methods:{getDeviceList(){this.loadIng++,this.$store.dispatch("call",{url:"users/device/list"}).then(({data:t})=>{this.devices=t.list,typeof this.$parent.updateDeviceCount=="function"&&this.$parent.updateDeviceCount(this.devices.length)}).catch(({msg:t})=>{$A.modalError(t),this.devices=[]}).finally(()=>{this.loadIng--})},getIcon({app_type:t,app_name:r}){return/ios/i.test(t)?/ipad/i.test(r)?"tablet":/iphone/i.test(r)?"phone":"apple":/android/i.test(t)?/(tablet|phablet)/i.test(r)?"tablet":"android":/mac/i.test(t)?"macos":/win/i.test(t)?"window":"web"},getName({app_brand:t,app_model:r,device_name:i,app_type:e,app_name:o,browser:a}){const s=[];if(/web/i.test(e))s.push(a,this.$L("\u6D4F\u89C8\u5668"));else{if(i)return i;t?s.push(t,r):s.push(o||e,this.$L("\u5BA2\u6237\u7AEF"))}return s.join(" ")},getOs({app_os:t,os:r}){return t||r},onLogout(t){$A.modalConfirm({title:"\u9000\u51FA\u767B\u5F55",content:"\u662F\u5426\u5728\u8BE5\u8BBE\u5907\u4E0A\u9000\u51FA\u767B\u5F55\uFF1F",loading:!0,onOk:()=>new Promise((r,i)=>{this.$store.dispatch("call",{url:"users/device/logout",data:{id:t.id}}).then(({msg:e})=>{r(e),this.getDeviceList()}).catch(({msg:e})=>{i(e)})})})}}},n={};var u=l(c,m,p,!1,d,null,null,null);function d(t){for(let r in n)this[r]=n[r]}var nt=function(){return u.exports}();export{nt as default}; import{n as l}from"./app.1e00fc89.js";import"./jquery.99163cb9.js";import"./@babel.9410f858.js";import"./dayjs.0bb0f368.js";import"./localforage.6e50a401.js";import"./markdown-it.0450edb4.js";import"./mdurl.ce6c1dd8.js";import"./uc.micro.8d343c98.js";import"./entities.48a44fec.js";import"./linkify-it.c5e8196e.js";import"./punycode.js.4b3f125a.js";import"./highlight.js.cbbfb885.js";import"./markdown-it-link-attributes.e1d5d151.js";import"./@traptitech.acea8861.js";import"./vue.adba9046.js";import"./vuex.cc7cb26e.js";import"./openpgp_hi.15f91b1d.js";import"./axios.37c7f908.js";import"./mitt.1ea0a2a3.js";import"./quill-hi.ca2ea0cc.js";import"./parchment.d5c5924e.js";import"./quill-delta.385a10bf.js";import"./fast-diff.f17881f3.js";import"./lodash.clonedeep.3cc09a31.js";import"./lodash.isequal.dbdc2157.js";import"./eventemitter3.78b735ad.js";import"./lodash-es.76e3a28b.js";import"./quill-mention-hi.4eeb5a2d.js";import"./view-design-hi.f1128b4d.js";import"./html-to-md.f297036e.js";import"./lodash.8fcd6fd4.js";import"./vue-router.2d566cd7.js";import"./vue-clipboard2.fd43a5bc.js";import"./clipboard.37b37361.js";import"./vuedraggable.f464b992.js";import"./sortablejs.3488b922.js";import"./vue-resize-observer.5af23a43.js";import"./element-sea.f8a64907.js";import"./deepmerge.cecf392e.js";import"./resize-observer-polyfill.5d591c5f.js";import"./throttle-debounce.7c3948b2.js";import"./babel-helper-vue-jsx-merge-props.5ed215c3.js";import"./normalize-wheel.2a034b9f.js";import"./async-validator.dca2b951.js";import"./babel-runtime.4773988a.js";import"./core-js.314b4a1d.js";var m=function(){var t=this,r=t.$createElement,i=t._self._c||r;return i("div",{staticClass:"setting-device"},[i("ul",[t.loadIng>0&&t.devices.length===0?i("li",{staticClass:"loading"},[i("Loading")],1):t._l(t.devices,function(e){return i("li",{key:e.id},[i("div",{staticClass:"icon"},[i("span",{class:t.getIcon(e.detail)})]),i("div",{staticClass:"info"},[i("div",{staticClass:"title"},[i("span",{staticClass:"name"},[t._v(t._s(t.getName(e.detail)))]),i("span",{staticClass:"device"},[t._v(t._s(t.getOs(e.detail)))])]),i("div",{staticClass:"time"},[i("EPopover",{attrs:{placement:"bottom-start",trigger:"click"}},[i("div",{staticClass:"setting-device-popover"},[i("p",[t._v(t._s(t.$L("\u767B\u5F55\u65F6\u95F4"))+": "+t._s(e.created_at))]),i("p",[t._v(t._s(t.$L("\u66F4\u65B0\u65F6\u95F4"))+": "+t._s(e.updated_at))]),i("p",[t._v(t._s(t.$L("\u8FC7\u671F\u65F6\u95F4"))+": "+t._s(e.expired_at))])]),i("span",{attrs:{slot:"reference"},slot:"reference"},[t._v(t._s(e.updated_at))])])],1)]),i("div",[e.is_current?i("span",{staticClass:"current"},[t._v(t._s(t.$L("\u5F53\u524D\u8BBE\u5907")))]):i("Button",{on:{click:function(o){return t.onLogout(e)}}},[t._v(t._s(t.$L("\u9000\u51FA\u767B\u5F55")))])],1)])})],2)])},p=[];const c={name:"SettingDevice",data(){return{loadIng:0,devices:[]}},mounted(){this.getDeviceList()},methods:{getDeviceList(){this.loadIng++,this.$store.dispatch("call",{url:"users/device/list"}).then(({data:t})=>{this.devices=t.list,typeof this.$parent.updateDeviceCount=="function"&&this.$parent.updateDeviceCount(this.devices.length)}).catch(({msg:t})=>{$A.modalError(t),this.devices=[]}).finally(()=>{this.loadIng--})},getIcon({app_type:t,app_name:r}){return/ios/i.test(t)?/ipad/i.test(r)?"tablet":/iphone/i.test(r)?"phone":"apple":/android/i.test(t)?/(tablet|phablet)/i.test(r)?"tablet":"android":/mac/i.test(t)?"macos":/win/i.test(t)?"window":"web"},getName({app_brand:t,app_model:r,device_name:i,app_type:e,app_name:o,browser:a}){const s=[];if(/web/i.test(e))s.push(a,this.$L("\u6D4F\u89C8\u5668"));else{if(i)return i;t?s.push(t,r):s.push(o||e,this.$L("\u5BA2\u6237\u7AEF"))}return s.join(" ")},getOs({app_os:t,os:r}){return t||r},onLogout(t){$A.modalConfirm({title:"\u9000\u51FA\u767B\u5F55",content:"\u662F\u5426\u5728\u8BE5\u8BBE\u5907\u4E0A\u9000\u51FA\u767B\u5F55\uFF1F",loading:!0,onOk:()=>new Promise((r,i)=>{this.$store.dispatch("call",{url:"users/device/logout",data:{id:t.id}}).then(({msg:e})=>{r(e),this.getDeviceList()}).catch(({msg:e})=>{i(e)})})})}}},n={};var u=l(c,m,p,!1,d,null,null,null);function d(t){for(let r in n)this[r]=n[r]}var nt=function(){return u.exports}();export{nt as default};

View File

@ -1 +1 @@
import{D as p}from"./DialogWrapper.c0d27c52.js";import{m}from"./vuex.cc7cb26e.js";import{n as a}from"./app.ec83cdf6.js";import"./chunkedUpload.cf92914b.js";import"./axios.37c7f908.js";import"./spark-md5.0443325f.js";import"./index.3118da17.js";import"./vue-virtual-scroll-list-hi.74ad83f0.js";import"./@babel.9410f858.js";import"./vue.adba9046.js";import"./lodash.8fcd6fd4.js";import"./ImgUpload.a61f7e6a.js";import"./webhook.378987f3.js";import"./jquery.020cc780.js";import"./dayjs.5bb9b94f.js";import"./localforage.982e07b7.js";import"./markdown-it.0450edb4.js";import"./mdurl.ce6c1dd8.js";import"./uc.micro.8d343c98.js";import"./entities.48a44fec.js";import"./linkify-it.c5e8196e.js";import"./punycode.js.4b3f125a.js";import"./highlight.js.cbbfb885.js";import"./markdown-it-link-attributes.e1d5d151.js";import"./@traptitech.acea8861.js";import"./openpgp_hi.15f91b1d.js";import"./mitt.1ea0a2a3.js";import"./quill-hi.ca2ea0cc.js";import"./parchment.d5c5924e.js";import"./quill-delta.385a10bf.js";import"./fast-diff.f17881f3.js";import"./lodash.clonedeep.3cc09a31.js";import"./lodash.isequal.dbdc2157.js";import"./eventemitter3.78b735ad.js";import"./lodash-es.76e3a28b.js";import"./quill-mention-hi.4eeb5a2d.js";import"./view-design-hi.f1128b4d.js";import"./html-to-md.f297036e.js";import"./vue-router.2d566cd7.js";import"./vue-clipboard2.fd43a5bc.js";import"./clipboard.37b37361.js";import"./vuedraggable.f464b992.js";import"./sortablejs.3488b922.js";import"./vue-resize-observer.5af23a43.js";import"./element-sea.f8a64907.js";import"./deepmerge.cecf392e.js";import"./resize-observer-polyfill.5d591c5f.js";import"./throttle-debounce.7c3948b2.js";import"./babel-helper-vue-jsx-merge-props.5ed215c3.js";import"./normalize-wheel.2a034b9f.js";import"./async-validator.dca2b951.js";import"./babel-runtime.4773988a.js";import"./core-js.314b4a1d.js";var e=function(){var t=this,o=t.$createElement,r=t._self._c||o;return r("div",{staticClass:"electron-dialog"},[r("PageTitle",{attrs:{title:t.dialogData.name}}),t.dialogId>0?r("DialogWrapper",{attrs:{dialogId:t.dialogId}}):t._e()],1)},n=[];const s={components:{DialogWrapper:p},computed:{...m(["cacheDialogs"]),dialogId(){const{dialogId:t}=this.$route.params;return parseInt(/^\d+$/.test(t)?t:0)},dialogData(){return this.cacheDialogs.find(({id:t})=>t===this.dialogId)||{}}}},i={};var l=a(s,e,n,!1,d,"4f6d7c8a",null,null);function d(t){for(let o in i)this[o]=i[o]}var dt=function(){return l.exports}();export{dt as default}; import{D as p}from"./DialogWrapper.d635cc01.js";import{m}from"./vuex.cc7cb26e.js";import{n as a}from"./app.1e00fc89.js";import"./chunkedUpload.b0e8368a.js";import"./axios.37c7f908.js";import"./spark-md5.0443325f.js";import"./index.978d6d4c.js";import"./vue-virtual-scroll-list-hi.74ad83f0.js";import"./@babel.9410f858.js";import"./vue.adba9046.js";import"./lodash.8fcd6fd4.js";import"./ImgUpload.be73943e.js";import"./webhook.378987f3.js";import"./jquery.99163cb9.js";import"./dayjs.0bb0f368.js";import"./localforage.6e50a401.js";import"./markdown-it.0450edb4.js";import"./mdurl.ce6c1dd8.js";import"./uc.micro.8d343c98.js";import"./entities.48a44fec.js";import"./linkify-it.c5e8196e.js";import"./punycode.js.4b3f125a.js";import"./highlight.js.cbbfb885.js";import"./markdown-it-link-attributes.e1d5d151.js";import"./@traptitech.acea8861.js";import"./openpgp_hi.15f91b1d.js";import"./mitt.1ea0a2a3.js";import"./quill-hi.ca2ea0cc.js";import"./parchment.d5c5924e.js";import"./quill-delta.385a10bf.js";import"./fast-diff.f17881f3.js";import"./lodash.clonedeep.3cc09a31.js";import"./lodash.isequal.dbdc2157.js";import"./eventemitter3.78b735ad.js";import"./lodash-es.76e3a28b.js";import"./quill-mention-hi.4eeb5a2d.js";import"./view-design-hi.f1128b4d.js";import"./html-to-md.f297036e.js";import"./vue-router.2d566cd7.js";import"./vue-clipboard2.fd43a5bc.js";import"./clipboard.37b37361.js";import"./vuedraggable.f464b992.js";import"./sortablejs.3488b922.js";import"./vue-resize-observer.5af23a43.js";import"./element-sea.f8a64907.js";import"./deepmerge.cecf392e.js";import"./resize-observer-polyfill.5d591c5f.js";import"./throttle-debounce.7c3948b2.js";import"./babel-helper-vue-jsx-merge-props.5ed215c3.js";import"./normalize-wheel.2a034b9f.js";import"./async-validator.dca2b951.js";import"./babel-runtime.4773988a.js";import"./core-js.314b4a1d.js";var e=function(){var t=this,o=t.$createElement,r=t._self._c||o;return r("div",{staticClass:"electron-dialog"},[r("PageTitle",{attrs:{title:t.dialogData.name}}),t.dialogId>0?r("DialogWrapper",{attrs:{dialogId:t.dialogId}}):t._e()],1)},n=[];const s={components:{DialogWrapper:p},computed:{...m(["cacheDialogs"]),dialogId(){const{dialogId:t}=this.$route.params;return parseInt(/^\d+$/.test(t)?t:0)},dialogData(){return this.cacheDialogs.find(({id:t})=>t===this.dialogId)||{}}}},i={};var l=a(s,e,n,!1,d,"4f6d7c8a",null,null);function d(t){for(let o in i)this[o]=i[o]}var dt=function(){return l.exports}();export{dt as default};

File diff suppressed because one or more lines are too long

1
public/js/build/editor.2d3fffc9.css vendored Normal file
View File

@ -0,0 +1 @@
.vmeditor-wrapper[data-v-0e1ab98a],.vmeditor-wrapper .vmeditor-editor[data-v-0e1ab98a]{width:100%;height:100%}.vmeditor-upload-control[data-v-0e1ab98a]{display:none;width:0;height:0;overflow:hidden}.vmeditor-icon-loading[data-v-0e1ab98a]{font-size:24px;margin-bottom:6px}

View File

@ -1 +0,0 @@
.vmeditor-wrapper[data-v-249e187e],.vmeditor-wrapper .vmeditor-editor[data-v-249e187e]{width:100%;height:100%}.vmeditor-upload-control[data-v-249e187e]{display:none;width:0;height:0;overflow:hidden}.vmeditor-icon-loading[data-v-249e187e]{font-size:24px;margin-bottom:6px}

1
public/js/build/email.7917046e.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
import n from"./FileContent.5de71cef.js";import m from"./FilePreview.fdee78e4.js";import{n as l}from"./app.ec83cdf6.js";import"./openpgp_hi.15f91b1d.js";import"./IFrame.c23602b5.js";import"./jquery.020cc780.js";import"./@babel.9410f858.js";import"./dayjs.5bb9b94f.js";import"./localforage.982e07b7.js";import"./markdown-it.0450edb4.js";import"./mdurl.ce6c1dd8.js";import"./uc.micro.8d343c98.js";import"./entities.48a44fec.js";import"./linkify-it.c5e8196e.js";import"./punycode.js.4b3f125a.js";import"./highlight.js.cbbfb885.js";import"./markdown-it-link-attributes.e1d5d151.js";import"./@traptitech.acea8861.js";import"./vue.adba9046.js";import"./vuex.cc7cb26e.js";import"./axios.37c7f908.js";import"./mitt.1ea0a2a3.js";import"./quill-hi.ca2ea0cc.js";import"./parchment.d5c5924e.js";import"./quill-delta.385a10bf.js";import"./fast-diff.f17881f3.js";import"./lodash.clonedeep.3cc09a31.js";import"./lodash.isequal.dbdc2157.js";import"./eventemitter3.78b735ad.js";import"./lodash-es.76e3a28b.js";import"./quill-mention-hi.4eeb5a2d.js";import"./view-design-hi.f1128b4d.js";import"./html-to-md.f297036e.js";import"./lodash.8fcd6fd4.js";import"./vue-router.2d566cd7.js";import"./vue-clipboard2.fd43a5bc.js";import"./clipboard.37b37361.js";import"./vuedraggable.f464b992.js";import"./sortablejs.3488b922.js";import"./vue-resize-observer.5af23a43.js";import"./element-sea.f8a64907.js";import"./deepmerge.cecf392e.js";import"./resize-observer-polyfill.5d591c5f.js";import"./throttle-debounce.7c3948b2.js";import"./babel-helper-vue-jsx-merge-props.5ed215c3.js";import"./normalize-wheel.2a034b9f.js";import"./async-validator.dca2b951.js";import"./babel-runtime.4773988a.js";import"./core-js.314b4a1d.js";var s=function(){var t=this,e=t.$createElement,i=t._self._c||e;return i("div",{staticClass:"single-file"},[i("PageTitle",{attrs:{title:t.pageName}}),t.loadIng>0?i("Loading"):t.fileInfo?[t.isPreview?i("FilePreview",{attrs:{code:t.code,file:t.fileInfo,historyId:t.historyId,headerShow:!t.$isEEUIApp}}):i("FileContent",{attrs:{file:t.fileInfo},model:{value:t.fileShow,callback:function(r){t.fileShow=r},expression:"fileShow"}})]:t._e()],2)},p=[];const a={components:{FilePreview:m,FileContent:n},data(){return{loadIng:0,code:null,fileShow:!0,fileInfo:null}},mounted(){},computed:{historyId(){return this.$route.query?$A.runNum(this.$route.query.history_id):0},isPreview(){return this.windowPortrait||this.code||this.historyId>0||this.fileInfo&&this.fileInfo.permission===0},pageName(){return this.$route.query&&this.$route.query.history_title?this.$route.query.history_title:this.fileInfo?`${this.fileInfo.name} [${this.fileInfo.created_at}]`:""}},watch:{$route:{handler(){this.getInfo()},immediate:!0}},methods:{getInfo(){let{codeOrFileId:t}=this.$route.params,e={id:t};if(/^\d+$/.test(t))this.code=null;else if(t)this.code=t;else return;setTimeout(i=>{this.loadIng++},600),this.$store.dispatch("call",{url:"file/one",data:e}).then(({data:i})=>{this.fileInfo=i}).catch(({msg:i})=>{$A.modalError({content:i,onOk:()=>{window.close()}})}).finally(i=>{this.loadIng--})}}},o={};var f=l(a,s,p,!1,u,"662d0b64",null,null);function u(t){for(let e in o)this[e]=o[e]}var st=function(){return f.exports}();export{st as default}; import n from"./FileContent.60bd2228.js";import m from"./FilePreview.54755748.js";import{n as l}from"./app.1e00fc89.js";import"./openpgp_hi.15f91b1d.js";import"./IFrame.02468084.js";import"./jquery.99163cb9.js";import"./@babel.9410f858.js";import"./dayjs.0bb0f368.js";import"./localforage.6e50a401.js";import"./markdown-it.0450edb4.js";import"./mdurl.ce6c1dd8.js";import"./uc.micro.8d343c98.js";import"./entities.48a44fec.js";import"./linkify-it.c5e8196e.js";import"./punycode.js.4b3f125a.js";import"./highlight.js.cbbfb885.js";import"./markdown-it-link-attributes.e1d5d151.js";import"./@traptitech.acea8861.js";import"./vue.adba9046.js";import"./vuex.cc7cb26e.js";import"./axios.37c7f908.js";import"./mitt.1ea0a2a3.js";import"./quill-hi.ca2ea0cc.js";import"./parchment.d5c5924e.js";import"./quill-delta.385a10bf.js";import"./fast-diff.f17881f3.js";import"./lodash.clonedeep.3cc09a31.js";import"./lodash.isequal.dbdc2157.js";import"./eventemitter3.78b735ad.js";import"./lodash-es.76e3a28b.js";import"./quill-mention-hi.4eeb5a2d.js";import"./view-design-hi.f1128b4d.js";import"./html-to-md.f297036e.js";import"./lodash.8fcd6fd4.js";import"./vue-router.2d566cd7.js";import"./vue-clipboard2.fd43a5bc.js";import"./clipboard.37b37361.js";import"./vuedraggable.f464b992.js";import"./sortablejs.3488b922.js";import"./vue-resize-observer.5af23a43.js";import"./element-sea.f8a64907.js";import"./deepmerge.cecf392e.js";import"./resize-observer-polyfill.5d591c5f.js";import"./throttle-debounce.7c3948b2.js";import"./babel-helper-vue-jsx-merge-props.5ed215c3.js";import"./normalize-wheel.2a034b9f.js";import"./async-validator.dca2b951.js";import"./babel-runtime.4773988a.js";import"./core-js.314b4a1d.js";var s=function(){var t=this,e=t.$createElement,i=t._self._c||e;return i("div",{staticClass:"single-file"},[i("PageTitle",{attrs:{title:t.pageName}}),t.loadIng>0?i("Loading"):t.fileInfo?[t.isPreview?i("FilePreview",{attrs:{code:t.code,file:t.fileInfo,historyId:t.historyId,headerShow:!t.$isEEUIApp}}):i("FileContent",{attrs:{file:t.fileInfo},model:{value:t.fileShow,callback:function(r){t.fileShow=r},expression:"fileShow"}})]:t._e()],2)},p=[];const a={components:{FilePreview:m,FileContent:n},data(){return{loadIng:0,code:null,fileShow:!0,fileInfo:null}},mounted(){},computed:{historyId(){return this.$route.query?$A.runNum(this.$route.query.history_id):0},isPreview(){return this.windowPortrait||this.code||this.historyId>0||this.fileInfo&&this.fileInfo.permission===0},pageName(){return this.$route.query&&this.$route.query.history_title?this.$route.query.history_title:this.fileInfo?`${this.fileInfo.name} [${this.fileInfo.created_at}]`:""}},watch:{$route:{handler(){this.getInfo()},immediate:!0}},methods:{getInfo(){let{codeOrFileId:t}=this.$route.params,e={id:t};if(/^\d+$/.test(t))this.code=null;else if(t)this.code=t;else return;setTimeout(i=>{this.loadIng++},600),this.$store.dispatch("call",{url:"file/one",data:e}).then(({data:i})=>{this.fileInfo=i}).catch(({msg:i})=>{$A.modalError({content:i,onOk:()=>{window.close()}})}).finally(i=>{this.loadIng--})}}},o={};var f=l(a,s,p,!1,u,"662d0b64",null,null);function u(t){for(let e in o)this[e]=o[e]}var st=function(){return f.exports}();export{st as default};

1
public/js/build/file.588ec774.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
public/js/build/index.4ff51c76.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
import{n as m}from"./app.ec83cdf6.js";import"./jquery.020cc780.js";import"./@babel.9410f858.js";import"./dayjs.5bb9b94f.js";import"./localforage.982e07b7.js";import"./markdown-it.0450edb4.js";import"./mdurl.ce6c1dd8.js";import"./uc.micro.8d343c98.js";import"./entities.48a44fec.js";import"./linkify-it.c5e8196e.js";import"./punycode.js.4b3f125a.js";import"./highlight.js.cbbfb885.js";import"./markdown-it-link-attributes.e1d5d151.js";import"./@traptitech.acea8861.js";import"./vue.adba9046.js";import"./vuex.cc7cb26e.js";import"./openpgp_hi.15f91b1d.js";import"./axios.37c7f908.js";import"./mitt.1ea0a2a3.js";import"./quill-hi.ca2ea0cc.js";import"./parchment.d5c5924e.js";import"./quill-delta.385a10bf.js";import"./fast-diff.f17881f3.js";import"./lodash.clonedeep.3cc09a31.js";import"./lodash.isequal.dbdc2157.js";import"./eventemitter3.78b735ad.js";import"./lodash-es.76e3a28b.js";import"./quill-mention-hi.4eeb5a2d.js";import"./view-design-hi.f1128b4d.js";import"./html-to-md.f297036e.js";import"./lodash.8fcd6fd4.js";import"./vue-router.2d566cd7.js";import"./vue-clipboard2.fd43a5bc.js";import"./clipboard.37b37361.js";import"./vuedraggable.f464b992.js";import"./sortablejs.3488b922.js";import"./vue-resize-observer.5af23a43.js";import"./element-sea.f8a64907.js";import"./deepmerge.cecf392e.js";import"./resize-observer-polyfill.5d591c5f.js";import"./throttle-debounce.7c3948b2.js";import"./babel-helper-vue-jsx-merge-props.5ed215c3.js";import"./normalize-wheel.2a034b9f.js";import"./async-validator.dca2b951.js";import"./babel-runtime.4773988a.js";import"./core-js.314b4a1d.js";var e=function(){var t=this,o=t.$createElement,i=t._self._c||o;return i("div")},n=[];const p={data(){return{}},mounted(){if(/^https?:/i.test(window.location.protocol)){let t=null;if(this.$router.mode==="hash"?$A.stringLength(window.location.pathname)>2&&(t=`${window.location.origin}/#${window.location.pathname}${window.location.search}`):this.$router.mode==="history"&&$A.strExists(window.location.href,"/#/")&&(t=window.location.href.replace("/#/","/")),t)throw this.$store.dispatch("userUrl",t).then(o=>{window.location.href=o}),SyntaxError()}},activated(){this.start()},methods:{start(){this.userId>0?this.goForward({name:"manage-dashboard"},!0):this.goForward({name:"login"},!0)}}},r={};var a=m(p,e,n,!1,s,null,null,null);function s(t){for(let o in r)this[o]=r[o]}var rt=function(){return a.exports}();export{rt as default}; import{n as m}from"./app.1e00fc89.js";import"./jquery.99163cb9.js";import"./@babel.9410f858.js";import"./dayjs.0bb0f368.js";import"./localforage.6e50a401.js";import"./markdown-it.0450edb4.js";import"./mdurl.ce6c1dd8.js";import"./uc.micro.8d343c98.js";import"./entities.48a44fec.js";import"./linkify-it.c5e8196e.js";import"./punycode.js.4b3f125a.js";import"./highlight.js.cbbfb885.js";import"./markdown-it-link-attributes.e1d5d151.js";import"./@traptitech.acea8861.js";import"./vue.adba9046.js";import"./vuex.cc7cb26e.js";import"./openpgp_hi.15f91b1d.js";import"./axios.37c7f908.js";import"./mitt.1ea0a2a3.js";import"./quill-hi.ca2ea0cc.js";import"./parchment.d5c5924e.js";import"./quill-delta.385a10bf.js";import"./fast-diff.f17881f3.js";import"./lodash.clonedeep.3cc09a31.js";import"./lodash.isequal.dbdc2157.js";import"./eventemitter3.78b735ad.js";import"./lodash-es.76e3a28b.js";import"./quill-mention-hi.4eeb5a2d.js";import"./view-design-hi.f1128b4d.js";import"./html-to-md.f297036e.js";import"./lodash.8fcd6fd4.js";import"./vue-router.2d566cd7.js";import"./vue-clipboard2.fd43a5bc.js";import"./clipboard.37b37361.js";import"./vuedraggable.f464b992.js";import"./sortablejs.3488b922.js";import"./vue-resize-observer.5af23a43.js";import"./element-sea.f8a64907.js";import"./deepmerge.cecf392e.js";import"./resize-observer-polyfill.5d591c5f.js";import"./throttle-debounce.7c3948b2.js";import"./babel-helper-vue-jsx-merge-props.5ed215c3.js";import"./normalize-wheel.2a034b9f.js";import"./async-validator.dca2b951.js";import"./babel-runtime.4773988a.js";import"./core-js.314b4a1d.js";var e=function(){var t=this,o=t.$createElement,i=t._self._c||o;return i("div")},n=[];const p={data(){return{}},mounted(){if(/^https?:/i.test(window.location.protocol)){let t=null;if(this.$router.mode==="hash"?$A.stringLength(window.location.pathname)>2&&(t=`${window.location.origin}/#${window.location.pathname}${window.location.search}`):this.$router.mode==="history"&&$A.strExists(window.location.href,"/#/")&&(t=window.location.href.replace("/#/","/")),t)throw this.$store.dispatch("userUrl",t).then(o=>{window.location.href=o}),SyntaxError()}},activated(){this.start()},methods:{start(){this.userId>0?this.goForward({name:"manage-dashboard"},!0):this.goForward({name:"login"},!0)}}},r={};var a=m(p,e,n,!1,s,null,null,null);function s(t){for(let o in r)this[o]=r[o]}var rt=function(){return a.exports}();export{rt as default};

File diff suppressed because one or more lines are too long

1
public/js/build/index.978d6d4c.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
import{_ as m}from"./openpgp_hi.15f91b1d.js";import{e as n}from"./index.40a8e116.js";import{n as p}from"./app.ec83cdf6.js";import"./jquery.020cc780.js";import"./@babel.9410f858.js";import"./dayjs.5bb9b94f.js";import"./localforage.982e07b7.js";import"./markdown-it.0450edb4.js";import"./mdurl.ce6c1dd8.js";import"./uc.micro.8d343c98.js";import"./entities.48a44fec.js";import"./linkify-it.c5e8196e.js";import"./punycode.js.4b3f125a.js";import"./highlight.js.cbbfb885.js";import"./markdown-it-link-attributes.e1d5d151.js";import"./@traptitech.acea8861.js";import"./vue.adba9046.js";import"./vuex.cc7cb26e.js";import"./axios.37c7f908.js";import"./mitt.1ea0a2a3.js";import"./quill-hi.ca2ea0cc.js";import"./parchment.d5c5924e.js";import"./quill-delta.385a10bf.js";import"./fast-diff.f17881f3.js";import"./lodash.clonedeep.3cc09a31.js";import"./lodash.isequal.dbdc2157.js";import"./eventemitter3.78b735ad.js";import"./lodash-es.76e3a28b.js";import"./quill-mention-hi.4eeb5a2d.js";import"./view-design-hi.f1128b4d.js";import"./html-to-md.f297036e.js";import"./lodash.8fcd6fd4.js";import"./vue-router.2d566cd7.js";import"./vue-clipboard2.fd43a5bc.js";import"./clipboard.37b37361.js";import"./vuedraggable.f464b992.js";import"./sortablejs.3488b922.js";import"./vue-resize-observer.5af23a43.js";import"./element-sea.f8a64907.js";import"./deepmerge.cecf392e.js";import"./resize-observer-polyfill.5d591c5f.js";import"./throttle-debounce.7c3948b2.js";import"./babel-helper-vue-jsx-merge-props.5ed215c3.js";import"./normalize-wheel.2a034b9f.js";import"./async-validator.dca2b951.js";import"./babel-runtime.4773988a.js";import"./core-js.314b4a1d.js";var a=function(){var t=this,o=t.$createElement,i=t._self._c||o;return t.ready?i("VEditor",{attrs:{leftToolbar:t.leftToolbar,rightToolbar:t.rightToolbar,tocNavPositionRight:t.tocNavPositionRight,includeLevel:t.includeLevel},model:{value:t.content,callback:function(e){t.content=e},expression:"content"}}):i("Loading")},s=[];const l={name:"VMEditor",mixins:[n],components:{VEditor:()=>m(()=>import("./editor.4afe3108.js"),["js/build/editor.4afe3108.js","js/build/editor.90492550.css","js/build/@kangc.b5fe0a56.js","js/build/@kangc.d8464d83.css","js/build/@babel.9410f858.js","js/build/vue.adba9046.js","js/build/copy-to-clipboard.a53c061d.js","js/build/toggle-selection.d2487283.js","js/build/prismjs.94ec9288.js","js/build/app.ec83cdf6.js","js/build/app.53b7eb8b.css","js/build/jquery.020cc780.js","js/build/dayjs.5bb9b94f.js","js/build/localforage.982e07b7.js","js/build/markdown-it.0450edb4.js","js/build/mdurl.ce6c1dd8.js","js/build/uc.micro.8d343c98.js","js/build/entities.48a44fec.js","js/build/linkify-it.c5e8196e.js","js/build/punycode.js.4b3f125a.js","js/build/highlight.js.cbbfb885.js","js/build/markdown-it-link-attributes.e1d5d151.js","js/build/@traptitech.acea8861.js","js/build/vuex.cc7cb26e.js","js/build/openpgp_hi.15f91b1d.js","js/build/axios.37c7f908.js","js/build/mitt.1ea0a2a3.js","js/build/quill-hi.ca2ea0cc.js","js/build/parchment.d5c5924e.js","js/build/quill-delta.385a10bf.js","js/build/fast-diff.f17881f3.js","js/build/lodash.clonedeep.3cc09a31.js","js/build/lodash.isequal.dbdc2157.js","js/build/eventemitter3.78b735ad.js","js/build/lodash-es.76e3a28b.js","js/build/quill-mention-hi.4eeb5a2d.js","js/build/view-design-hi.f1128b4d.js","js/build/html-to-md.f297036e.js","js/build/lodash.8fcd6fd4.js","js/build/vue-router.2d566cd7.js","js/build/vue-clipboard2.fd43a5bc.js","js/build/clipboard.37b37361.js","js/build/vuedraggable.f464b992.js","js/build/sortablejs.3488b922.js","js/build/vue-resize-observer.5af23a43.js","js/build/element-sea.f8a64907.js","js/build/deepmerge.cecf392e.js","js/build/resize-observer-polyfill.5d591c5f.js","js/build/throttle-debounce.7c3948b2.js","js/build/babel-helper-vue-jsx-merge-props.5ed215c3.js","js/build/normalize-wheel.2a034b9f.js","js/build/async-validator.dca2b951.js","js/build/babel-runtime.4773988a.js","js/build/core-js.314b4a1d.js","js/build/codemirror.9d10b9e4.js","js/build/codemirror.9ace6687.css","js/build/index.40a8e116.js","js/build/ImgUpload.a61f7e6a.js","js/build/chunkedUpload.cf92914b.js","js/build/spark-md5.0443325f.js"])},data(){return{ready:!1,content:""}},async mounted(){await $A.loadScriptS(["js/katex/katex.min.js","js/katex/katex.min.css","js/mermaid.min.js"]),this.ready=!0},watch:{value:{handler(t){t==null&&(t=""),this.content=t},immediate:!0},content(t){this.$emit("input",t)}}},r={};var c=p(l,a,s,!1,_,null,null,null);function _(t){for(let o in r)this[o]=r[o]}var nt=function(){return c.exports}();export{nt as default}; import{_ as m}from"./openpgp_hi.15f91b1d.js";import{e as n}from"./index.40a8e116.js";import{n as p}from"./app.1e00fc89.js";import"./jquery.99163cb9.js";import"./@babel.9410f858.js";import"./dayjs.0bb0f368.js";import"./localforage.6e50a401.js";import"./markdown-it.0450edb4.js";import"./mdurl.ce6c1dd8.js";import"./uc.micro.8d343c98.js";import"./entities.48a44fec.js";import"./linkify-it.c5e8196e.js";import"./punycode.js.4b3f125a.js";import"./highlight.js.cbbfb885.js";import"./markdown-it-link-attributes.e1d5d151.js";import"./@traptitech.acea8861.js";import"./vue.adba9046.js";import"./vuex.cc7cb26e.js";import"./axios.37c7f908.js";import"./mitt.1ea0a2a3.js";import"./quill-hi.ca2ea0cc.js";import"./parchment.d5c5924e.js";import"./quill-delta.385a10bf.js";import"./fast-diff.f17881f3.js";import"./lodash.clonedeep.3cc09a31.js";import"./lodash.isequal.dbdc2157.js";import"./eventemitter3.78b735ad.js";import"./lodash-es.76e3a28b.js";import"./quill-mention-hi.4eeb5a2d.js";import"./view-design-hi.f1128b4d.js";import"./html-to-md.f297036e.js";import"./lodash.8fcd6fd4.js";import"./vue-router.2d566cd7.js";import"./vue-clipboard2.fd43a5bc.js";import"./clipboard.37b37361.js";import"./vuedraggable.f464b992.js";import"./sortablejs.3488b922.js";import"./vue-resize-observer.5af23a43.js";import"./element-sea.f8a64907.js";import"./deepmerge.cecf392e.js";import"./resize-observer-polyfill.5d591c5f.js";import"./throttle-debounce.7c3948b2.js";import"./babel-helper-vue-jsx-merge-props.5ed215c3.js";import"./normalize-wheel.2a034b9f.js";import"./async-validator.dca2b951.js";import"./babel-runtime.4773988a.js";import"./core-js.314b4a1d.js";var a=function(){var t=this,o=t.$createElement,i=t._self._c||o;return t.ready?i("VEditor",{attrs:{leftToolbar:t.leftToolbar,rightToolbar:t.rightToolbar,tocNavPositionRight:t.tocNavPositionRight,includeLevel:t.includeLevel},model:{value:t.content,callback:function(e){t.content=e},expression:"content"}}):i("Loading")},s=[];const l={name:"VMEditor",mixins:[n],components:{VEditor:()=>m(()=>import("./editor.25a751b5.js"),["js/build/editor.25a751b5.js","js/build/editor.2d3fffc9.css","js/build/@kangc.b5fe0a56.js","js/build/@kangc.d8464d83.css","js/build/@babel.9410f858.js","js/build/vue.adba9046.js","js/build/copy-to-clipboard.a53c061d.js","js/build/toggle-selection.d2487283.js","js/build/prismjs.94ec9288.js","js/build/app.1e00fc89.js","js/build/app.fcecaabb.css","js/build/jquery.99163cb9.js","js/build/dayjs.0bb0f368.js","js/build/localforage.6e50a401.js","js/build/markdown-it.0450edb4.js","js/build/mdurl.ce6c1dd8.js","js/build/uc.micro.8d343c98.js","js/build/entities.48a44fec.js","js/build/linkify-it.c5e8196e.js","js/build/punycode.js.4b3f125a.js","js/build/highlight.js.cbbfb885.js","js/build/markdown-it-link-attributes.e1d5d151.js","js/build/@traptitech.acea8861.js","js/build/vuex.cc7cb26e.js","js/build/openpgp_hi.15f91b1d.js","js/build/axios.37c7f908.js","js/build/mitt.1ea0a2a3.js","js/build/quill-hi.ca2ea0cc.js","js/build/parchment.d5c5924e.js","js/build/quill-delta.385a10bf.js","js/build/fast-diff.f17881f3.js","js/build/lodash.clonedeep.3cc09a31.js","js/build/lodash.isequal.dbdc2157.js","js/build/eventemitter3.78b735ad.js","js/build/lodash-es.76e3a28b.js","js/build/quill-mention-hi.4eeb5a2d.js","js/build/view-design-hi.f1128b4d.js","js/build/html-to-md.f297036e.js","js/build/lodash.8fcd6fd4.js","js/build/vue-router.2d566cd7.js","js/build/vue-clipboard2.fd43a5bc.js","js/build/clipboard.37b37361.js","js/build/vuedraggable.f464b992.js","js/build/sortablejs.3488b922.js","js/build/vue-resize-observer.5af23a43.js","js/build/element-sea.f8a64907.js","js/build/deepmerge.cecf392e.js","js/build/resize-observer-polyfill.5d591c5f.js","js/build/throttle-debounce.7c3948b2.js","js/build/babel-helper-vue-jsx-merge-props.5ed215c3.js","js/build/normalize-wheel.2a034b9f.js","js/build/async-validator.dca2b951.js","js/build/babel-runtime.4773988a.js","js/build/core-js.314b4a1d.js","js/build/codemirror.9d10b9e4.js","js/build/codemirror.9ace6687.css","js/build/index.40a8e116.js","js/build/ImgUpload.be73943e.js","js/build/chunkedUpload.b0e8368a.js","js/build/spark-md5.0443325f.js"])},data(){return{ready:!1,content:""}},async mounted(){await $A.loadScriptS(["js/katex/katex.min.js","js/katex/katex.min.css","js/mermaid.min.js"]),this.ready=!0},watch:{value:{handler(t){t==null&&(t=""),this.content=t},immediate:!0},content(t){this.$emit("input",t)}}},r={};var c=p(l,a,s,!1,_,null,null,null);function _(t){for(let o in r)this[o]=r[o]}var nt=function(){return c.exports}();export{nt as default};

File diff suppressed because one or more lines are too long

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