mirror of
https://github.com/kuaifan/dootask.git
synced 2026-06-26 09:12:14 +00:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4904bdbe2 | ||
|
|
b2d54576d0 | ||
|
|
da36d3b319 | ||
|
|
e5a88c2957 | ||
|
|
0896f09878 | ||
|
|
7ca85bfe6b | ||
|
|
0c75d52be0 | ||
|
|
bc8a2c9ded | ||
|
|
889aca311a | ||
|
|
fa9e56944a | ||
|
|
87e05ef9c9 | ||
|
|
7c6b8ce6f4 | ||
|
|
7c6dfe8a25 | ||
|
|
389cb6d709 | ||
|
|
4ca7fc10d1 | ||
|
|
6a4f815d5a | ||
|
|
31729933be | ||
|
|
4a6403d17f | ||
|
|
f8ad608c54 | ||
|
|
97718bc22a | ||
|
|
aa872773f5 | ||
|
|
8e591503dd | ||
|
|
7453b79c8d | ||
|
|
7844f5500b | ||
|
|
f0e48d98e4 | ||
|
|
333f9caa5e | ||
|
|
135b419572 | ||
|
|
a19822a617 | ||
|
|
ac2d4c6c4f | ||
|
|
a3d5bdf93c | ||
|
|
b0a4fe6646 | ||
|
|
97bd58312e | ||
|
|
2f8dee44c2 | ||
|
|
468abe9902 | ||
|
|
27c65cc582 | ||
|
|
3f5078ec9b | ||
|
|
cf6f6041b6 | ||
|
|
e17a520599 | ||
|
|
b544c9d7f6 | ||
|
|
8d9082f7a1 | ||
|
|
6bbcb702dc | ||
|
|
53dadabca0 | ||
|
|
5d2701b0be | ||
|
|
c3ebfcefd1 | ||
|
|
04aa60b574 | ||
|
|
383664aef7 | ||
|
|
8fb6d331f8 | ||
|
|
cbe00f1284 | ||
|
|
645cb02757 | ||
|
|
bfa0920579 | ||
|
|
88fed0744c | ||
|
|
e74142e58d | ||
|
|
da095a1a80 | ||
|
|
9b41330413 | ||
|
|
39b9a72b16 | ||
|
|
4de6c69972 | ||
|
|
e6ef85e176 | ||
|
|
ec1ab31b0e | ||
|
|
af206480fb | ||
|
|
f6067d1bd5 | ||
|
|
20c3fa91fb |
53
.claude/hooks/php-stan-check.sh
Executable file
53
.claude/hooks/php-stan-check.sh
Executable file
@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Claude Code PostToolUse hook:Edit/Write 改动 app/ 下的 PHP 文件后,自动在 PHP 容器内
|
||||||
|
# 对该文件跑 phpstan 单文件分析。失败时 exit 2,把错误回灌给 Claude 修复。
|
||||||
|
# 任何环境不满足(无 python3 / 容器未运行 / 未装 phpstan)都静默放行,绝不阻塞编辑。
|
||||||
|
set -u
|
||||||
|
|
||||||
|
INPUT=$(cat)
|
||||||
|
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
||||||
|
|
||||||
|
command -v python3 >/dev/null 2>&1 || exit 0
|
||||||
|
command -v docker >/dev/null 2>&1 || exit 0
|
||||||
|
|
||||||
|
FILE_PATH=$(printf '%s' "$INPUT" | python3 -c "import sys,json;print(json.load(sys.stdin).get('tool_input',{}).get('file_path',''))" 2>/dev/null) || exit 0
|
||||||
|
[ -n "$FILE_PATH" ] || exit 0
|
||||||
|
|
||||||
|
case "$FILE_PATH" in
|
||||||
|
*.php) ;;
|
||||||
|
*) exit 0 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
REL_PATH="${FILE_PATH#"$PROJECT_DIR"/}"
|
||||||
|
case "$REL_PATH" in
|
||||||
|
app/*) ;;
|
||||||
|
*) exit 0 ;;
|
||||||
|
esac
|
||||||
|
[ -f "$PROJECT_DIR/$REL_PATH" ] || exit 0
|
||||||
|
|
||||||
|
# 定位挂载本项目的 PHP 容器:
|
||||||
|
# ① 环境变量 DOOTASK_PHP_CONTAINER;② .env 的 APP_ID;③ 扫描 /var/www 挂载源为本项目的容器
|
||||||
|
CONTAINER="${DOOTASK_PHP_CONTAINER:-}"
|
||||||
|
if [ -z "$CONTAINER" ] && [ -f "$PROJECT_DIR/.env" ]; then
|
||||||
|
APP_ID=$(grep -E '^APP_ID=' "$PROJECT_DIR/.env" 2>/dev/null | head -1 | cut -d= -f2 | tr -d '"' | tr -d "'")
|
||||||
|
if [ -n "$APP_ID" ] && docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "dootask-php-$APP_ID"; then
|
||||||
|
CONTAINER="dootask-php-$APP_ID"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ -z "$CONTAINER" ]; then
|
||||||
|
RUNNING=$(docker ps -q 2>/dev/null)
|
||||||
|
[ -n "$RUNNING" ] && CONTAINER=$(docker inspect --format '{{.Name}}|{{range .Mounts}}{{if eq .Destination "/var/www"}}{{.Source}}{{end}}{{end}}' $RUNNING 2>/dev/null \
|
||||||
|
| awk -F'|' -v dir="$PROJECT_DIR" '$2 == dir {gsub(/^\//, "", $1); print $1; exit}')
|
||||||
|
fi
|
||||||
|
[ -n "$CONTAINER" ] || exit 0
|
||||||
|
docker exec "$CONTAINER" test -f /var/www/vendor/bin/phpstan 2>/dev/null || exit 0
|
||||||
|
|
||||||
|
OUTPUT=$(docker exec "$CONTAINER" sh -c "cd /var/www && php vendor/bin/phpstan analyse --no-progress --error-format=raw --memory-limit=-1 '$REL_PATH'" 2>&1)
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
{
|
||||||
|
echo "phpstan 检查未通过($REL_PATH),请修复以下问题:"
|
||||||
|
printf '%s\n' "$OUTPUT" | grep -v '^Note:' | tail -30
|
||||||
|
} >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
16
.claude/settings.json
Normal file
16
.claude/settings.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"PostToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "Edit|Write",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/php-stan-check.sh",
|
||||||
|
"timeout": 120
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@ -92,7 +92,7 @@ jobs:
|
|||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
php-version: '8.0'
|
php-version: '8.4'
|
||||||
extensions: mbstring, intl, gd, xml, zip, swoole
|
extensions: mbstring, intl, gd, xml, zip, swoole
|
||||||
tools: composer:v2
|
tools: composer:v2
|
||||||
|
|
||||||
|
|||||||
81
.github/workflows/tests.yml
vendored
Normal file
81
.github/workflows/tests.yml
vendored
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
name: Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [pro, master, dev]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
static-checks:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.4'
|
||||||
|
extensions: mbstring, intl, gd, xml, zip, swoole, redis
|
||||||
|
tools: composer:v2
|
||||||
|
|
||||||
|
- name: Install Composer Dependencies
|
||||||
|
run: composer install --prefer-dist --no-interaction
|
||||||
|
|
||||||
|
- name: PHPStan
|
||||||
|
run: composer stan
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Install NPM Dependencies
|
||||||
|
run: npm install --no-audit --no-fund
|
||||||
|
|
||||||
|
- name: ESLint
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
# 存量缺失文案 93 条(见 scripts/check-language.mjs 输出),清零后移除 continue-on-error 改为强制
|
||||||
|
- name: Language Check
|
||||||
|
run: npm run check:lang
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
phpunit:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
services:
|
||||||
|
mariadb:
|
||||||
|
image: mariadb:10.7.3
|
||||||
|
env:
|
||||||
|
MYSQL_ROOT_PASSWORD: test
|
||||||
|
MYSQL_DATABASE: dootask
|
||||||
|
ports:
|
||||||
|
- 3306:3306
|
||||||
|
options: >-
|
||||||
|
--health-cmd="mysqladmin ping -h localhost -ptest"
|
||||||
|
--health-interval=10s
|
||||||
|
--health-timeout=5s
|
||||||
|
--health-retries=10
|
||||||
|
redis:
|
||||||
|
image: redis:alpine
|
||||||
|
ports:
|
||||||
|
- 6379:6379
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# 用项目自身的 PHP 镜像跑测试(内含 /usr/lib/doo/doo.so FFI 库,裸 runner 无法运行)
|
||||||
|
- name: Run PHPUnit in DooTask PHP image
|
||||||
|
run: |
|
||||||
|
docker run --rm --network host -v "$GITHUB_WORKSPACE":/var/www -w /var/www \
|
||||||
|
kuaifan/php:swoole-8.4 sh -c '
|
||||||
|
composer install --prefer-dist --no-interaction &&
|
||||||
|
cp .env.example .env &&
|
||||||
|
sed -i "s/^DB_HOST=.*/DB_HOST=127.0.0.1/" .env &&
|
||||||
|
sed -i "s/^DB_DATABASE=.*/DB_DATABASE=dootask/" .env &&
|
||||||
|
sed -i "s/^DB_PASSWORD=.*/DB_PASSWORD=test/" .env &&
|
||||||
|
sed -i "s/^REDIS_HOST=.*/REDIS_HOST=127.0.0.1/" .env &&
|
||||||
|
php artisan key:generate &&
|
||||||
|
php artisan migrate --seed --force &&
|
||||||
|
php vendor/bin/phpunit
|
||||||
|
'
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -65,3 +65,4 @@ README_LOCAL.md
|
|||||||
|
|
||||||
# playwright
|
# playwright
|
||||||
.playwright-mcp/
|
.playwright-mcp/
|
||||||
|
/.phpunit.cache
|
||||||
|
|||||||
@ -158,11 +158,6 @@ drawio/webapp/js/app.min.js
|
|||||||
drawio/webapp/js/extensions.min.js
|
drawio/webapp/js/extensions.min.js
|
||||||
drawio/webapp/js/shapes-14-6-5.min.js
|
drawio/webapp/js/shapes-14-6-5.min.js
|
||||||
drawio/webapp/js/stencils.min.js
|
drawio/webapp/js/stencils.min.js
|
||||||
drawio/webapp/math/es5/core.js
|
|
||||||
drawio/webapp/math/es5/input/asciimath.js
|
|
||||||
drawio/webapp/math/es5/input/tex.js
|
|
||||||
drawio/webapp/math/es5/output/svg.js
|
|
||||||
drawio/webapp/math/es5/output/svg/fonts/tex.js
|
|
||||||
drawio/webapp/styles/grapheditor.css
|
drawio/webapp/styles/grapheditor.css
|
||||||
|
|
||||||
minder/css/chunk-vendors.fe9c56c6.css
|
minder/css/chunk-vendors.fe9c56c6.css
|
||||||
|
|||||||
28
CHANGELOG.md
28
CHANGELOG.md
@ -2,6 +2,34 @@
|
|||||||
|
|
||||||
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.45]
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- AI 助手全面升级:现在能直接带你跳转页面、协助完成操作;回复可一键复制、查看时间、点赞或点踩反馈;较长的提问支持点击展开查看全部内容;对话浮窗支持手势返回 / ESC 快捷关闭,并完善了手机端适配与流式回复体验。
|
||||||
|
- AI 助手接入产品知识库,回答更贴合 DooTask 的实际功能与使用方法。
|
||||||
|
- 新增官方 AI 服务「Doo AI」,并支持为不同模型设置思考深度,按需选择更合适的模型。
|
||||||
|
- 新增「在线授权」:通过邮箱验证码即可自助开通或申请试用,到期自动续期;在线授权与离线授权可一键切换、互不冲突,授权状态一目了然。
|
||||||
|
- 手机端聊天默认显示发送按钮,发送消息更顺手。
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- 修复使用中文、日文等输入法打字时,回车或删除键偶尔会误触发送 / 提交的问题。
|
||||||
|
- 修复深色主题下部分微应用自定义背景色显示异常的问题。
|
||||||
|
- 修复在独立窗口打开的微应用,关闭应用后窗口未一同关闭的问题。
|
||||||
|
- 修复个别微应用打开时报错、无法正常加载的问题。
|
||||||
|
- 修复部分反向代理 / HTTPS 环境下访问地址协议识别错误的问题。
|
||||||
|
- 优化标签输入框,支持多种分隔符录入,输入更顺畅。
|
||||||
|
- 优化邮件发送的稳定性,修复发信超时判断不准确的问题。
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- 全面升级底层运行框架(Laravel 13 + PHP 8.4),整体运行更快、更稳定、更安全。
|
||||||
|
|
||||||
|
### Miscellaneous
|
||||||
|
|
||||||
|
- 内置审批功能已从主程序移除,改由应用中心的审批应用 / 微应用提供;原有审批能力可通过安装对应应用继续使用。
|
||||||
|
|
||||||
## [1.7.90]
|
## [1.7.90]
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
30
CLAUDE.md
30
CLAUDE.md
@ -1,6 +1,6 @@
|
|||||||
## 项目概述
|
## 项目概述
|
||||||
|
|
||||||
Laravel 8 (LaravelS/Swoole) + Vue 2 (Vite) + Electron。开源任务/项目管理系统。
|
Laravel 13 (LaravelS/Swoole, PHP 8.4) + Vue 2 (Vite) + Electron。开源任务/项目管理系统。
|
||||||
|
|
||||||
## 开发命令
|
## 开发命令
|
||||||
|
|
||||||
@ -17,11 +17,31 @@ Laravel 8 (LaravelS/Swoole) + Vue 2 (Vite) + Electron。开源任务/项目管
|
|||||||
|
|
||||||
前端代码改动只做 Edit/Write,不要为了"验证"启动 dev server。用户明确说"跑一下 / 出包"时除外。
|
前端代码改动只做 Edit/Write,不要为了"验证"启动 dev server。用户明确说"跑一下 / 出包"时除外。
|
||||||
|
|
||||||
|
### 质量门禁(改完代码必须自查,CI 同步在跑,见 .github/workflows/tests.yml)
|
||||||
|
|
||||||
|
- `./cmd composer stan` — phpstan(level 1 + baseline,存量已封存,新增错误必须清零)
|
||||||
|
- `npm run lint` — ESLint(error 必须为 0;warn 是存量遗留,见 eslint.config.mjs 注释)
|
||||||
|
- `npm run check:lang` — 校验前端 `$L()` 字面量是否已登记到 `language/original-web.txt`
|
||||||
|
- 改动控制器 public 方法或路由后跑 `./cmd artisan doc:api-map` 重新生成对照表
|
||||||
|
|
||||||
|
## 代码检索地图(先查表,再 grep)
|
||||||
|
|
||||||
|
- API URL ↔ 控制器方法对照:`routes/api-map.md`(生成式文件,勿手改)
|
||||||
|
- 前端事件总线(mitt)收发对照:`docs/events-map.md`(`npm run events:map` 重新生成)
|
||||||
|
- `$A` / `$L` 全局工具类型声明:`types/dootask-globals.d.ts`(新增 `$A` 方法须同步此文件)
|
||||||
|
|
||||||
|
## 架构增量规则(只约束新增代码,存量"动到哪迁到哪")
|
||||||
|
|
||||||
|
- **巨型文件冻结**:不再往 `ProjectController`、`UsersController`、`DialogController`、`app/Module/Base.php`、`resources/assets/js/store/actions.js` 新增方法/函数;新功能领域开新控制器或新模块文件(动态路由天然支持多控制器)
|
||||||
|
- **业务编排归层**:跨模型的业务流程写在 `app/Module/`(或 `app/Services/`),模型只保留数据访问与自身状态变更;Swoole Task 只做投递与调用,不直接编排业务
|
||||||
|
- **配置读取**:业务代码禁止直接 `env()`,统一走 `config()`(项目自有配置集中在 `config/dootask.php`)
|
||||||
|
|
||||||
## Gotchas
|
## Gotchas
|
||||||
|
|
||||||
### LaravelS/Swoole
|
### LaravelS/Swoole
|
||||||
|
|
||||||
- **避免在静态属性、单例、全局变量中存储请求级状态**——请求间共享进程,会导致数据串联和内存泄漏
|
- **避免在静态属性、单例、全局变量中存储请求级状态**——请求间共享进程,会导致数据串联和内存泄漏
|
||||||
|
- 要存请求级状态,用 `RequestContext::save('key', $value)` / `RequestContext::get('key')`(参考 `User::authInfo()` 的用法,见 `app/Services/RequestContext.php`)
|
||||||
- 构造函数、服务提供者、`boot()` 方法不会在每个请求重新执行
|
- 构造函数、服务提供者、`boot()` 方法不会在每个请求重新执行
|
||||||
- 配置/路由变更需要 `./cmd php restart` 或容器重启才能生效
|
- 配置/路由变更需要 `./cmd php restart` 或容器重启才能生效
|
||||||
- 长生命周期逻辑(WebSocket、定时器)应复用现有模式,避免阻塞协程/事件循环
|
- 长生命周期逻辑(WebSocket、定时器)应复用现有模式,避免阻塞协程/事件循环
|
||||||
@ -49,6 +69,14 @@ Laravel 8 (LaravelS/Swoole) + 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)`——禁止拼接翻译
|
||||||
|
|
||||||
|
## ai-kb 同步规则
|
||||||
|
|
||||||
|
`resources/ai-kb/` 是产品内 AI 助手 RAG 检索的功能知识库(目录结构、写作规范、索引机制见其 `README.md` 与 `_schema/`)。
|
||||||
|
|
||||||
|
- **同步时机**:改动用户可见的功能/菜单/按钮/流程/字段、API 行为(错误码、参数含义、返回结构)、插件/微应用、权限/角色定义时,必须在同一次提交中同步更新 ai-kb,不要把 ai-kb 改动单独拆成一个提交
|
||||||
|
- **怎么改**:在 `_meta/feature-map.yaml` 找到对应 feature 的 chunk 清单,按 `_schema/chunk-style.md` 与 `_schema/frontmatter.md` 修改或新建 chunk,并把 frontmatter 的 `last_verified` 更新为当前主程序版本号
|
||||||
|
- **改完即止**:无需触发任何索引操作,插件容器启动时会自动对账收敛
|
||||||
|
|
||||||
## Playwright 测试
|
## Playwright 测试
|
||||||
|
|
||||||
- Playwright 测试结果放在 `tests/playwright-results/`,包含测试环境、测试用例、结果截图等信息
|
- Playwright 测试结果放在 `tests/playwright-results/`,包含测试环境、测试用例、结果截图等信息
|
||||||
|
|||||||
27
README.md
27
README.md
@ -9,14 +9,6 @@ English | **[中文文档](./README_CN.md)**
|
|||||||
|
|
||||||
- Group Number: `546574618`
|
- Group Number: `546574618`
|
||||||
|
|
||||||
## 📍 Migration from 0.x to 1.x
|
|
||||||
|
|
||||||
- Please ensure to back up your data before upgrading!
|
|
||||||
- If the upgrade fails, try running `./cmd update` multiple times.
|
|
||||||
- If you encounter "Container xxx not found" during upgrade, run `./cmd reup` and then execute `./cmd update`.
|
|
||||||
- If you see a 502 error after upgrading, run `./cmd reup` to restart the services.
|
|
||||||
- If you encounter "Application 'xxx' not installed" after upgrading, log in with the admin account and install the relevant applications from the App Store.
|
|
||||||
|
|
||||||
## Installation Requirements
|
## Installation Requirements
|
||||||
|
|
||||||
- Required: `Docker v20.10+` and `Docker Compose v2.0+`
|
- Required: `Docker v20.10+` and `Docker Compose v2.0+`
|
||||||
@ -27,6 +19,16 @@ English | **[中文文档](./README_CN.md)**
|
|||||||
|
|
||||||
### Deploy Project
|
### Deploy Project
|
||||||
|
|
||||||
|
**Option 1: One-line script (recommended)**
|
||||||
|
|
||||||
|
Run it in an empty directory to clone and install automatically; run it inside an existing installation to check and upgrade:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/kuaifan/dootask/pro/bin/install | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 2: Manual deployment**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1、Clone the project to your local machine or server
|
# 1、Clone the project to your local machine or server
|
||||||
|
|
||||||
@ -105,11 +107,18 @@ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|||||||
|
|
||||||
**Note: Please backup your data before upgrading!**
|
**Note: Please backup your data before upgrading!**
|
||||||
|
|
||||||
|
Recommended: use the one-line script (run it inside an existing installation; it pulls the latest code and finishes the upgrade in a single run):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/kuaifan/dootask/pro/bin/install | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use the local command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./cmd update
|
./cmd update
|
||||||
```
|
```
|
||||||
|
|
||||||
* Please retry if upgrade fails across major versions.
|
|
||||||
* If you encounter 502 errors after upgrade, run `./cmd reup` to restart services.
|
* If you encounter 502 errors after upgrade, run `./cmd reup` to restart services.
|
||||||
|
|
||||||
## Project Migration
|
## Project Migration
|
||||||
|
|||||||
27
README_CN.md
27
README_CN.md
@ -9,14 +9,6 @@
|
|||||||
|
|
||||||
- QQ群号: `546574618`
|
- QQ群号: `546574618`
|
||||||
|
|
||||||
## 📍 0.x 迁移到 1.x
|
|
||||||
|
|
||||||
- 升级时请务必备份好数据!
|
|
||||||
- 如果升级失败请尝试执行 `./cmd update` 重试几次。
|
|
||||||
- 如果升级中出现 `没有找到 xxx 容器` 的提示,请运行 `./cmd reup` 后再执行 `./cmd update`。
|
|
||||||
- 如果升级后出现502错误请运行 `./cmd reup` 重启服务即可。
|
|
||||||
- 如果升级后出现 `应用「xxx」未安装` 的提示,请使用管理员账号进入应用商店安装相关应用。
|
|
||||||
|
|
||||||
## 安装程序
|
## 安装程序
|
||||||
|
|
||||||
- 必须安装:`Docker v20.10+` 和 `Docker Compose v2.0+`
|
- 必须安装:`Docker v20.10+` 和 `Docker Compose v2.0+`
|
||||||
@ -27,6 +19,16 @@
|
|||||||
|
|
||||||
### 部署项目
|
### 部署项目
|
||||||
|
|
||||||
|
**方式一:一键脚本(推荐)**
|
||||||
|
|
||||||
|
在空目录中执行即自动克隆并安装;在已安装目录中执行则自动检查并升级:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/kuaifan/dootask/pro/bin/install | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
**方式二:手动部署**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1、克隆项目到您的本地或服务器
|
# 1、克隆项目到您的本地或服务器
|
||||||
|
|
||||||
@ -105,11 +107,18 @@ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|||||||
|
|
||||||
**注意:在升级之前请备份好你的数据!**
|
**注意:在升级之前请备份好你的数据!**
|
||||||
|
|
||||||
|
推荐使用一键脚本升级(在已安装目录中执行,自动拉取最新代码并完成升级,无需重复执行):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/kuaifan/dootask/pro/bin/install | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
或使用本地命令:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./cmd update
|
./cmd update
|
||||||
```
|
```
|
||||||
|
|
||||||
* 跨越大版本升级失败时请重试执行一次。
|
|
||||||
* 如果升级后出现502请运行 `./cmd reup` 重启服务即可。
|
* 如果升级后出现502请运行 `./cmd reup` 重启服务即可。
|
||||||
|
|
||||||
## 迁移项目
|
## 迁移项目
|
||||||
|
|||||||
31032
_ide_helper.php
31032
_ide_helper.php
File diff suppressed because it is too large
Load Diff
143
app/Console/Commands/DocApiMap.php
Normal file
143
app/Console/Commands/DocApiMap.php
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use ReflectionClass;
|
||||||
|
use ReflectionMethod;
|
||||||
|
|
||||||
|
class DocApiMap extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'doc:api-map';
|
||||||
|
protected $description = '生成 API 路由对照表(routes/api-map.md)';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$controllers = $this->collectControllers();
|
||||||
|
if (empty($controllers)) {
|
||||||
|
$this->error('未从路由中解析到任何 api 控制器');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = 0;
|
||||||
|
$sections = [];
|
||||||
|
foreach ($controllers as $prefix => $class) {
|
||||||
|
$rows = $this->collectMethods($prefix, $class);
|
||||||
|
$total += count($rows);
|
||||||
|
$sections[] = $this->renderSection($prefix, $class, $rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = base_path('routes/api-map.md');
|
||||||
|
file_put_contents($path, $this->renderHeader($total) . implode("\n", $sections));
|
||||||
|
|
||||||
|
$this->info("已生成: routes/api-map.md(控制器 " . count($controllers) . " 个,接口 {$total} 个)");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从已注册路由中收集 api 前缀与控制器的映射
|
||||||
|
* 匹配 routes/web.php 中的动态路由:api/{prefix}/{method}
|
||||||
|
* @return array [prefix => 控制器类名]
|
||||||
|
*/
|
||||||
|
private function collectControllers(): array
|
||||||
|
{
|
||||||
|
$controllers = [];
|
||||||
|
foreach (Route::getRoutes() as $route) {
|
||||||
|
if (!preg_match('/^api\/(\w+)\/\{method}$/', $route->uri())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
preg_match('/^api\/(\w+)\/\{method}$/', $route->uri(), $match);
|
||||||
|
$class = $route->getAction('controller');
|
||||||
|
if ($class && class_exists($class)) {
|
||||||
|
$controllers[$match[1]] = $class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $controllers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 反射收集控制器的接口方法
|
||||||
|
* @param string $prefix 路由前缀(如 project)
|
||||||
|
* @param string $class 控制器类名
|
||||||
|
* @return array [['url' => ..., 'method' => ..., 'http' => ..., 'title' => ...], ...]
|
||||||
|
*/
|
||||||
|
private function collectMethods(string $prefix, string $class): array
|
||||||
|
{
|
||||||
|
$rows = [];
|
||||||
|
$reflection = new ReflectionClass($class);
|
||||||
|
foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
|
||||||
|
// 仅保留本类声明的实例方法,排除 __invoke/__before/__construct 等魔术/框架方法
|
||||||
|
if ($method->getDeclaringClass()->getName() !== $class
|
||||||
|
|| $method->isStatic()
|
||||||
|
|| str_starts_with($method->getName(), '__')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
[$http, $title] = $this->parseApiDoc($method);
|
||||||
|
$rows[] = [
|
||||||
|
'url' => "api/{$prefix}/" . str_replace('__', '/', $method->getName()),
|
||||||
|
'method' => $method->getName() . '()',
|
||||||
|
'http' => $http,
|
||||||
|
'title' => $title,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析方法 docblock 中的 @api 注释行
|
||||||
|
* 格式如:@api {get} api/project/lists 获取项目列表
|
||||||
|
* @return array [HTTP 方法, 标题],无 @api 注释时为 ['any', '']
|
||||||
|
*/
|
||||||
|
private function parseApiDoc(ReflectionMethod $method): array
|
||||||
|
{
|
||||||
|
$doc = $method->getDocComment();
|
||||||
|
if ($doc && preg_match('/@api\s+\{(\w+)}\s+(\S+)(?:[ \t]+(.+))?/', $doc, $match)) {
|
||||||
|
return [strtolower($match[1]), trim($match[3] ?? '')];
|
||||||
|
}
|
||||||
|
return ['any', ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成文件头说明
|
||||||
|
*/
|
||||||
|
private function renderHeader(int $total): string
|
||||||
|
{
|
||||||
|
return <<<MD
|
||||||
|
# API 路由对照表
|
||||||
|
|
||||||
|
> 此文件由 `php artisan doc:api-map` 生成,勿手改。
|
||||||
|
|
||||||
|
接口总数:{$total}
|
||||||
|
|
||||||
|
## 路由规则
|
||||||
|
|
||||||
|
API 使用动态路由(见 `routes/web.php`),URL 段映射为控制器方法名:
|
||||||
|
|
||||||
|
- `api/{controller}/{method}` → `{method}()`,如 `api/project/lists` → `ProjectController::lists()`
|
||||||
|
- `api/{controller}/{method}/{action}` → `{method}__{action}()`(双下划线连接),如 `api/project/invite/join` → `ProjectController::invite__join()`
|
||||||
|
- 路由最多两段,方法名最多一个双下划线
|
||||||
|
|
||||||
|
|
||||||
|
MD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成单个控制器的表格段落
|
||||||
|
*/
|
||||||
|
private function renderSection(string $prefix, string $class, array $rows): string
|
||||||
|
{
|
||||||
|
$short = class_basename($class);
|
||||||
|
$lines = [
|
||||||
|
"## {$prefix}({$short})",
|
||||||
|
'',
|
||||||
|
'| URL | 方法名 | HTTP | 说明 |',
|
||||||
|
'| --- | --- | --- | --- |',
|
||||||
|
];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$lines[] = "| {$row['url']} | {$row['method']} | {$row['http']} | {$row['title']} |";
|
||||||
|
}
|
||||||
|
$lines[] = '';
|
||||||
|
return implode("\n", $lines);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
app/Console/Commands/OnlineLicenseRenew.php
Normal file
29
app/Console/Commands/OnlineLicenseRenew.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Module\OnlineLicense;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线授权续期(容器内独立进程按小时调用,无需 LARAVELS_TIMER、不经过 HTTP 转发)。
|
||||||
|
*
|
||||||
|
* 由 php 容器 supervisor 程序 [program:license] 循环调用:
|
||||||
|
* while true; do php artisan online-license:renew; sleep 3600; done
|
||||||
|
*/
|
||||||
|
class OnlineLicenseRenew extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'online-license:renew';
|
||||||
|
protected $description = '在线授权:本地状态机推进 + 租约将尽时自动续期';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
if (!OnlineLicense::enabled()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
OnlineLicense::cron();
|
||||||
|
$status = OnlineLicense::status();
|
||||||
|
$this->info('online-license: ' . ($status['status'] ?? 'offline') . ' lease=' . ($status['lease_expired_at'] ?? '-'));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -52,9 +52,18 @@ trait ManticoreSyncLock
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 信号处理器(SIGINT/SIGTERM)
|
* 信号处理器(SIGINT/SIGTERM),签名须兼容 Symfony Console 的 Command::handleSignal
|
||||||
*/
|
*/
|
||||||
public function handleSignal(int $signal): void
|
public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
|
||||||
|
{
|
||||||
|
$this->markShouldStop();
|
||||||
|
return false; // 继续执行,由批次循环优雅退出
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记优雅退出(pcntl 回调第二参是 siginfo,不能直接复用 handleSignal)
|
||||||
|
*/
|
||||||
|
private function markShouldStop(): void
|
||||||
{
|
{
|
||||||
$this->info("\n收到信号,将在当前批次完成后退出...");
|
$this->info("\n收到信号,将在当前批次完成后退出...");
|
||||||
$this->shouldStop = true;
|
$this->shouldStop = true;
|
||||||
@ -67,8 +76,8 @@ trait ManticoreSyncLock
|
|||||||
{
|
{
|
||||||
if (extension_loaded('pcntl')) {
|
if (extension_loaded('pcntl')) {
|
||||||
pcntl_async_signals(true);
|
pcntl_async_signals(true);
|
||||||
pcntl_signal(SIGINT, [$this, 'handleSignal']);
|
pcntl_signal(SIGINT, fn () => $this->markShouldStop());
|
||||||
pcntl_signal(SIGTERM, [$this, 'handleSignal']);
|
pcntl_signal(SIGTERM, fn () => $this->markShouldStop());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,41 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Console;
|
|
||||||
|
|
||||||
use Illuminate\Console\Scheduling\Schedule;
|
|
||||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
|
||||||
|
|
||||||
class Kernel extends ConsoleKernel
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The Artisan commands provided by your application.
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $commands = [
|
|
||||||
//
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Define the application's command schedule.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Console\Scheduling\Schedule $schedule
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function schedule(Schedule $schedule)
|
|
||||||
{
|
|
||||||
// $schedule->command('inspire')->hourly();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the commands for the application.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function commands()
|
|
||||||
{
|
|
||||||
$this->load(__DIR__.'/Commands');
|
|
||||||
|
|
||||||
require base_path('routes/console.php');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -4,94 +4,18 @@ namespace App\Exceptions;
|
|||||||
|
|
||||||
use App\Module\Base;
|
use App\Module\Base;
|
||||||
use App\Module\Image;
|
use App\Module\Image;
|
||||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|
||||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|
||||||
use Throwable;
|
|
||||||
|
|
||||||
class Handler extends ExceptionHandler
|
/**
|
||||||
|
* 图片路径处理(原 Exceptions\Handler::ImagePathHandler,新结构下由 bootstrap/app.php
|
||||||
|
* 的 withExceptions 在 NotFoundHttpException 时调用)
|
||||||
|
*/
|
||||||
|
class ImagePathHandler
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* A list of the exception types that are not reported.
|
* @param \Illuminate\Http\Request $request
|
||||||
*
|
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse|null 命中返回图片响应,未命中返回 null(继续默认 404)
|
||||||
* @var array
|
|
||||||
*/
|
*/
|
||||||
protected $dontReport = [
|
public static function render($request)
|
||||||
//
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A list of the inputs that are never flashed for validation exceptions.
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $dontFlash = [
|
|
||||||
'current_password',
|
|
||||||
'password',
|
|
||||||
'password_confirmation',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the exception handling callbacks for the application.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function register()
|
|
||||||
{
|
|
||||||
$this->reportable(function (Throwable $e) {
|
|
||||||
//
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将异常转换为 HTTP 响应。
|
|
||||||
* @param $request
|
|
||||||
* @param Throwable $e
|
|
||||||
* @return array|\Illuminate\Http\JsonResponse|\Illuminate\Http\Response|\Symfony\Component\HttpFoundation\Response
|
|
||||||
* @throws Throwable
|
|
||||||
*/
|
|
||||||
public function render($request, Throwable $e)
|
|
||||||
{
|
|
||||||
if ($e instanceof NotFoundHttpException) {
|
|
||||||
if ($result = $this->ImagePathHandler($request)) {
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($e instanceof ApiException) {
|
|
||||||
return response()->json(Base::retError($e->getMessage(), $e->getData(), $e->getCode()));
|
|
||||||
} elseif ($e instanceof ModelNotFoundException) {
|
|
||||||
return response()->json(Base::retError('Interface error'));
|
|
||||||
}
|
|
||||||
return parent::render($request, $e);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重写report优雅记录
|
|
||||||
* @param Throwable $e
|
|
||||||
* @throws Throwable
|
|
||||||
*/
|
|
||||||
public function report(Throwable $e)
|
|
||||||
{
|
|
||||||
if ($e instanceof ApiException) {
|
|
||||||
if ($e->isWriteLog()) {
|
|
||||||
Log::error($e->getMessage(), [
|
|
||||||
'code' => $e->getCode(),
|
|
||||||
'data' => $e->getData(),
|
|
||||||
'exception' => ' at ' . $e->getFile() . ':' . $e->getLine()
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
parent::report($e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 图片路径处理
|
|
||||||
* @param $request
|
|
||||||
* @return \Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\BinaryFileResponse|null
|
|
||||||
*/
|
|
||||||
private function ImagePathHandler($request)
|
|
||||||
{
|
{
|
||||||
$path = $request->path();
|
$path = $request->path();
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -2,11 +2,17 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Api;
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Models\AiAssistantFeedback;
|
||||||
|
use App\Models\AiAssistantSearchLog;
|
||||||
use App\Models\AiAssistantSession;
|
use App\Models\AiAssistantSession;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Models\WebSocket;
|
||||||
use App\Module\AI;
|
use App\Module\AI;
|
||||||
use App\Module\Apps;
|
use App\Module\Apps;
|
||||||
use App\Module\Base;
|
use App\Module\Base;
|
||||||
|
use App\Tasks\PushTask;
|
||||||
|
use Cache;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use Request;
|
use Request;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,6 +38,8 @@ class AssistantController extends AbstractController
|
|||||||
* @apiParam {String} model_type 模型类型
|
* @apiParam {String} model_type 模型类型
|
||||||
* @apiParam {String} model_name 模型名称
|
* @apiParam {String} model_name 模型名称
|
||||||
* @apiParam {JSON} context 上下文数组
|
* @apiParam {JSON} context 上下文数组
|
||||||
|
* @apiParam {String} [locale] ai-kb 检索语种:zh、en(缺省取请求语言 language,包含 zh 视为 zh,否则 en)
|
||||||
|
* @apiParam {String} [session_id] 前端会话ID(透传给 AI 服务作 context_key,用于检索打点关联)
|
||||||
*
|
*
|
||||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
@ -46,8 +54,21 @@ class AssistantController extends AbstractController
|
|||||||
$modelType = trim(Request::input('model_type', ''));
|
$modelType = trim(Request::input('model_type', ''));
|
||||||
$modelName = trim(Request::input('model_name', ''));
|
$modelName = trim(Request::input('model_name', ''));
|
||||||
$contextInput = Request::input('context', []);
|
$contextInput = Request::input('context', []);
|
||||||
|
$locale = trim(Request::input('locale', '')) ?: trim(Base::headerOrInput('language'));
|
||||||
|
$locale = str_contains(strtolower($locale), 'zh') ? 'zh' : 'en';
|
||||||
|
$contextKey = mb_substr(trim(Request::input('session_id', '')), 0, 100);
|
||||||
|
|
||||||
return AI::createStreamKey($modelType, $modelName, $contextInput);
|
// 当前用户 WebSocket fd:供 AI 经 doo page 操作本人浏览器(页面操作用)。
|
||||||
|
// 复用 operation__dispatch 同款归属校验:在表即在线、归属即本人,否则置 0。
|
||||||
|
$fd = intval(Base::headerOrInput('fd'));
|
||||||
|
if ($fd > 0 && intval(WebSocket::whereFd($fd)->value('userid')) !== intval($user->userid)) {
|
||||||
|
$fd = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 灰度判定(参考 config/ai.php):总开关 + canary 白名单
|
||||||
|
$ragEnabled = AI::ragEnabledFor((int) $user->userid);
|
||||||
|
|
||||||
|
return AI::createStreamKey($modelType, $modelName, $contextInput, $locale, $ragEnabled, $contextKey, $fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -157,6 +178,247 @@ class AssistantController extends AbstractController
|
|||||||
return $dotProduct / $denominator;
|
return $dotProduct / $denominator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} api/assistant/log/search 记录帮助知识库检索日志
|
||||||
|
*
|
||||||
|
* @apiDescription 需要token身份(AI 插件透传用户 token 服务端回调)。记录一次 search_help_docs 检索,用于分析检索质量、反哺 ai-kb 内容迭代
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup assistant
|
||||||
|
* @apiName log__search
|
||||||
|
*
|
||||||
|
* @apiParam {String} query 检索query
|
||||||
|
* @apiParam {String} [locale] 语种 zh|en
|
||||||
|
* @apiParam {String} [source] 来源 chat|invoke
|
||||||
|
* @apiParam {String} [context_key] 上下文标识
|
||||||
|
* @apiParam {Number} [dialog_id] 对话ID
|
||||||
|
* @apiParam {Array} [source_ids] 命中source id列表
|
||||||
|
* @apiParam {Number} [top_score] 最高相似度
|
||||||
|
* @apiParam {Number} [result_count] 命中数量
|
||||||
|
* @apiParam {Number} [duration_ms] 检索耗时毫秒
|
||||||
|
*
|
||||||
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
|
*/
|
||||||
|
public function log__search()
|
||||||
|
{
|
||||||
|
$user = User::auth();
|
||||||
|
|
||||||
|
$query = mb_substr(trim(Request::input('query', '')), 0, 500);
|
||||||
|
$locale = trim(Request::input('locale', ''));
|
||||||
|
$source = trim(Request::input('source', ''));
|
||||||
|
$contextKey = mb_substr(trim(Request::input('context_key', '')), 0, 191);
|
||||||
|
$dialogId = intval(Request::input('dialog_id', 0));
|
||||||
|
$sourceIds = Request::input('source_ids', []);
|
||||||
|
$topScore = floatval(Request::input('top_score', 0));
|
||||||
|
$resultCount = intval(Request::input('result_count', 0));
|
||||||
|
$durationMs = intval(Request::input('duration_ms', 0));
|
||||||
|
|
||||||
|
if ($query === '') {
|
||||||
|
return Base::retError('参数错误');
|
||||||
|
}
|
||||||
|
if (!in_array($source, ['chat', 'invoke'])) {
|
||||||
|
$source = '';
|
||||||
|
}
|
||||||
|
if (!is_array($sourceIds)) {
|
||||||
|
$sourceIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$log = AiAssistantSearchLog::createInstance([
|
||||||
|
'userid' => $user->userid,
|
||||||
|
'dialog_id' => max(0, $dialogId),
|
||||||
|
'context_key' => $contextKey,
|
||||||
|
'source' => $source,
|
||||||
|
'query' => $query,
|
||||||
|
'locale' => in_array($locale, ['zh', 'en']) ? $locale : '',
|
||||||
|
'source_ids' => Base::array2json(array_slice(array_values($sourceIds), 0, 10)),
|
||||||
|
'top_score' => max(0, min(1, $topScore)),
|
||||||
|
'result_count' => max(0, $resultCount),
|
||||||
|
'duration_ms' => max(0, $durationMs),
|
||||||
|
'empty' => $resultCount > 0 ? 0 : 1,
|
||||||
|
]);
|
||||||
|
$log->save();
|
||||||
|
|
||||||
|
return Base::retSuccess('success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} api/assistant/feedback/save 保存回复反馈
|
||||||
|
*
|
||||||
|
* @apiDescription 需要token身份。保存用户对一条 AI 回复的 👍/👎 反馈,同一条回复可改票(覆盖更新);传空 feedback 表示取消反馈(删除记录)
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup assistant
|
||||||
|
* @apiName feedback__save
|
||||||
|
*
|
||||||
|
* @apiParam {String} session_key 场景分类key
|
||||||
|
* @apiParam {String} session_id 前端会话ID
|
||||||
|
* @apiParam {Number} local_id 回复条目localId
|
||||||
|
* @apiParam {String} feedback like|dislike,空字符串表示取消反馈
|
||||||
|
* @apiParam {String} [prompt] 用户问题
|
||||||
|
* @apiParam {String} [answer] 回复摘录
|
||||||
|
* @apiParam {Array} [source_ids] 回复引用的kb source id列表
|
||||||
|
* @apiParam {String} [model] 模型名
|
||||||
|
*
|
||||||
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
|
* @apiSuccess {Object} data 返回数据
|
||||||
|
* @apiSuccess {String} data.feedback 已保存的反馈值
|
||||||
|
*/
|
||||||
|
public function feedback__save()
|
||||||
|
{
|
||||||
|
$user = User::auth();
|
||||||
|
|
||||||
|
$sessionKey = mb_substr(trim(Request::input('session_key', 'default')), 0, 100);
|
||||||
|
$sessionId = mb_substr(trim(Request::input('session_id', '')), 0, 100);
|
||||||
|
$localId = intval(Request::input('local_id', 0));
|
||||||
|
$feedback = trim(Request::input('feedback', ''));
|
||||||
|
$prompt = mb_substr(trim(Request::input('prompt', '')), 0, 1000);
|
||||||
|
$answer = mb_substr(trim(Request::input('answer', '')), 0, 2000);
|
||||||
|
$sourceIds = Request::input('source_ids', []);
|
||||||
|
$model = mb_substr(trim(Request::input('model', '')), 0, 100);
|
||||||
|
|
||||||
|
if (empty($sessionId) || $localId <= 0) {
|
||||||
|
return Base::retError('参数错误');
|
||||||
|
}
|
||||||
|
if (!in_array($feedback, ['', 'like', 'dislike'])) {
|
||||||
|
return Base::retError('反馈类型错误');
|
||||||
|
}
|
||||||
|
if (!is_array($sourceIds)) {
|
||||||
|
$sourceIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$exist = AiAssistantFeedback::where('userid', $user->userid)
|
||||||
|
->where('session_key', $sessionKey)
|
||||||
|
->where('session_id', $sessionId)
|
||||||
|
->where('local_id', $localId)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
// 空反馈表示取消:删除已有记录
|
||||||
|
if ($feedback === '') {
|
||||||
|
$exist?->delete();
|
||||||
|
return Base::retSuccess('success', [
|
||||||
|
'feedback' => '',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = AiAssistantFeedback::createInstance([
|
||||||
|
'userid' => $user->userid,
|
||||||
|
'session_key' => $sessionKey,
|
||||||
|
'session_id' => $sessionId,
|
||||||
|
'local_id' => $localId,
|
||||||
|
'feedback' => $feedback,
|
||||||
|
'prompt' => $prompt,
|
||||||
|
'answer' => $answer,
|
||||||
|
'answer_digest' => md5($answer),
|
||||||
|
'source_ids' => Base::array2json(array_slice(array_values($sourceIds), 0, 10)),
|
||||||
|
'model' => $model,
|
||||||
|
], $exist?->id);
|
||||||
|
$row->save();
|
||||||
|
|
||||||
|
return Base::retSuccess('success', [
|
||||||
|
'feedback' => $feedback,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} api/assistant/operation/dispatch 派发页面操作
|
||||||
|
*
|
||||||
|
* @apiDescription 需要token身份。通过用户常驻 WebSocket(/ws)向其浏览器派发一次页面操作(获取页面上下文 / 执行动作 / 操作元素),由前端 AI 助手执行后经 operationResult 回传,结果写入缓存供 operation/result 轮询取走。复用主程序 /ws,无需为页面操作另开 WebSocket。
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup assistant
|
||||||
|
* @apiName operation__dispatch
|
||||||
|
*
|
||||||
|
* @apiParam {Number} fd 目标会话 fd(须为当前用户在线的 WebSocket 连接)
|
||||||
|
* @apiParam {String} action 操作类型,如 get_page_context|execute_action|execute_element_action
|
||||||
|
* @apiParam {Object} [payload] 操作参数
|
||||||
|
*
|
||||||
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
|
* @apiSuccess {Object} data 返回数据
|
||||||
|
* @apiSuccess {String} data.requestId 本次操作的请求ID,用于轮询 operation/result
|
||||||
|
*/
|
||||||
|
public function operation__dispatch()
|
||||||
|
{
|
||||||
|
$user = User::auth();
|
||||||
|
|
||||||
|
$fd = intval(Base::headerOrInput('fd'));
|
||||||
|
$action = trim(Request::input('action', ''));
|
||||||
|
$payload = Request::input('payload', []);
|
||||||
|
|
||||||
|
if ($fd <= 0 || $action === '') {
|
||||||
|
return Base::retError('参数错误');
|
||||||
|
}
|
||||||
|
if (!is_array($payload)) {
|
||||||
|
$payload = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// fd 归属校验:在表即在线,归属即本人
|
||||||
|
$ownerId = WebSocket::whereFd($fd)->value('userid');
|
||||||
|
if (intval($ownerId) !== intval($user->userid)) {
|
||||||
|
return Base::retError('会话不存在或无权限');
|
||||||
|
}
|
||||||
|
|
||||||
|
$requestId = Str::random(24);
|
||||||
|
|
||||||
|
// 精确推送到该 fd,不补发离线消息
|
||||||
|
PushTask::push([
|
||||||
|
'fd' => $fd,
|
||||||
|
'msg' => [
|
||||||
|
'type' => 'operation',
|
||||||
|
'data' => [
|
||||||
|
'requestId' => $requestId,
|
||||||
|
'action' => $action,
|
||||||
|
'payload' => $payload,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
], false);
|
||||||
|
|
||||||
|
return Base::retSuccess('success', [
|
||||||
|
'requestId' => $requestId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} api/assistant/operation/result 取页面操作结果
|
||||||
|
*
|
||||||
|
* @apiDescription 需要token身份。轮询取走 operation/dispatch 派发的一次页面操作结果(取走即删);未回传时返回 status=pending。
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup assistant
|
||||||
|
* @apiName operation__result
|
||||||
|
*
|
||||||
|
* @apiParam {String} request_id 操作请求ID
|
||||||
|
*
|
||||||
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
|
* @apiSuccess {Object} data 返回数据
|
||||||
|
* @apiSuccess {String} data.status ready|pending
|
||||||
|
*/
|
||||||
|
public function operation__result()
|
||||||
|
{
|
||||||
|
$user = User::auth();
|
||||||
|
|
||||||
|
$requestId = trim(Base::headerOrInput('request_id'));
|
||||||
|
if ($requestId === '') {
|
||||||
|
return Base::retError('参数错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = Cache::get("ai_op_result:{$requestId}");
|
||||||
|
if (!is_array($row)) {
|
||||||
|
return Base::retSuccess('success', ['status' => 'pending']);
|
||||||
|
}
|
||||||
|
// 命中后校验归属再取走,避免越权读取他人结果
|
||||||
|
if (intval($row['userid']) !== intval($user->userid)) {
|
||||||
|
return Base::retError('无权限');
|
||||||
|
}
|
||||||
|
Cache::forget("ai_op_result:{$requestId}");
|
||||||
|
|
||||||
|
return Base::retSuccess('success', [
|
||||||
|
'status' => 'ready',
|
||||||
|
'success' => !empty($row['success']),
|
||||||
|
'result' => $row['result'] ?? null,
|
||||||
|
'error' => $row['error'] ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取会话列表
|
* 获取会话列表
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1257,6 +1257,51 @@ class DialogController extends AbstractController
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} api/dialog/msg/sendapprove 发送审批通知卡片
|
||||||
|
*
|
||||||
|
* @apiDescription 需要token身份。以「审批助手」机器人身份向指定用户发送审批模板卡片
|
||||||
|
* (由 approve 插件调用,卡片仅展示、不与旧审批系统有数据关联)。
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup dialog
|
||||||
|
* @apiName msg__sendapprove
|
||||||
|
*
|
||||||
|
* @apiParam {Number} to_userid 接收用户ID
|
||||||
|
* @apiParam {String} type 卡片类型:approve_reviewer / approve_notifier / approve_submitter / approve_comment_notifier
|
||||||
|
* @apiParam {String} [action] 动作:start / pass / refuse / withdraw(按类型取用)
|
||||||
|
* @apiParam {Number} [is_finished] 是否已结束(0/1)
|
||||||
|
* @apiParam {Object} data 卡片数据
|
||||||
|
* @apiParam {String} [title] 消息标题(会话列表预览用)
|
||||||
|
*/
|
||||||
|
public function msg__sendapprove()
|
||||||
|
{
|
||||||
|
$user = User::auth();
|
||||||
|
$toUserid = intval(Request::input('to_userid'));
|
||||||
|
$type = trim(Request::input('type'));
|
||||||
|
$action = trim(Request::input('action'));
|
||||||
|
$isFinished = intval(Request::input('is_finished'));
|
||||||
|
$data = Base::json2array(Request::input('data'));
|
||||||
|
$title = trim(Request::input('title'));
|
||||||
|
//
|
||||||
|
$allow = ['approve_reviewer', 'approve_notifier', 'approve_submitter', 'approve_comment_notifier'];
|
||||||
|
if ($toUserid <= 0 || !in_array($type, $allow)) {
|
||||||
|
return Base::retError('参数错误');
|
||||||
|
}
|
||||||
|
$botUser = User::botGetOrCreate('approval-alert');
|
||||||
|
$dialog = WebSocketDialog::checkUserDialog($botUser, $toUserid);
|
||||||
|
if (empty($dialog)) {
|
||||||
|
return Base::retError('无法创建对话');
|
||||||
|
}
|
||||||
|
$msgData = [
|
||||||
|
'type' => $type,
|
||||||
|
'action' => $action ?: null,
|
||||||
|
'is_finished' => $isFinished,
|
||||||
|
'data' => $data,
|
||||||
|
'title' => $title,
|
||||||
|
];
|
||||||
|
return WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', $msgData, $botUser->userid, false, false, true);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {post} api/dialog/msg/sendrecord 发送语音
|
* @api {post} api/dialog/msg/sendrecord 发送语音
|
||||||
*
|
*
|
||||||
@ -2125,6 +2170,9 @@ class DialogController extends AbstractController
|
|||||||
$msg_id = intval(Request::input("msg_id"));
|
$msg_id = intval(Request::input("msg_id"));
|
||||||
$force = intval(Request::input("force"));
|
$force = intval(Request::input("force"));
|
||||||
$language = Base::inputOrHeader('language');
|
$language = Base::inputOrHeader('language');
|
||||||
|
if (empty($language)) {
|
||||||
|
return Base::retError("参数错误");
|
||||||
|
}
|
||||||
$targetLanguage = Doo::getLanguages($language);
|
$targetLanguage = Doo::getLanguages($language);
|
||||||
//
|
//
|
||||||
if (empty($targetLanguage)) {
|
if (empty($targetLanguage)) {
|
||||||
@ -3042,7 +3090,7 @@ class DialogController extends AbstractController
|
|||||||
*/
|
*/
|
||||||
public function group__transfer()
|
public function group__transfer()
|
||||||
{
|
{
|
||||||
if (!Base::is_internal_ip(Base::getIp()) || Request::input("key") !== env('APP_KEY')) {
|
if (!Base::is_internal_ip(Base::getIp()) || Request::input("key") !== config('app.key')) {
|
||||||
$user = User::auth();
|
$user = User::auth();
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
@ -3352,7 +3400,7 @@ class DialogController extends AbstractController
|
|||||||
*/
|
*/
|
||||||
public function okr__push()
|
public function okr__push()
|
||||||
{
|
{
|
||||||
if (!Base::is_internal_ip(Base::getIp()) || Request::input("key") !== env('APP_KEY')) {
|
if (!Base::is_internal_ip(Base::getIp()) || Request::input("key") !== config('app.key')) {
|
||||||
User::auth();
|
User::auth();
|
||||||
}
|
}
|
||||||
$text = trim(Request::input('text'));
|
$text = trim(Request::input('text'));
|
||||||
|
|||||||
@ -737,7 +737,10 @@ class FileController extends AbstractController
|
|||||||
File::isNeedInstallApp('office');
|
File::isNeedInstallApp('office');
|
||||||
//
|
//
|
||||||
$config = Request::input('config');
|
$config = Request::input('config');
|
||||||
$token = \Firebase\JWT\JWT::encode($config, env('APP_KEY') ,'HS256');
|
if (!is_array($config)) {
|
||||||
|
return Base::retError('参数错误');
|
||||||
|
}
|
||||||
|
$token = \Firebase\JWT\JWT::encode($config, config('app.key') ,'HS256');
|
||||||
return Base::retSuccess('成功', [
|
return Base::retSuccess('成功', [
|
||||||
'token' => $token
|
'token' => $token
|
||||||
]);
|
]);
|
||||||
|
|||||||
95
app/Http/Controllers/Api/LicenseController.php
Normal file
95
app/Http/Controllers/Api/LicenseController.php
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Module\Base;
|
||||||
|
use App\Module\OnlineLicense;
|
||||||
|
use Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线授权客户端(与 SystemController::license 的离线粘贴并存)。
|
||||||
|
*
|
||||||
|
* 动态路由(routes/web.php):
|
||||||
|
* api/license/email/send -> email__send()
|
||||||
|
* api/license/login -> login()
|
||||||
|
* api/license/trial -> trial()
|
||||||
|
* api/license/status -> status()
|
||||||
|
* api/license/refresh -> refresh()
|
||||||
|
* api/license/logout -> logout()
|
||||||
|
*/
|
||||||
|
class LicenseController extends AbstractController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 发送邮箱验证码(登录与试用共用)
|
||||||
|
*/
|
||||||
|
public function email__send()
|
||||||
|
{
|
||||||
|
User::auth('admin');
|
||||||
|
$email = trim(Request::input('email'));
|
||||||
|
if ($email === '') {
|
||||||
|
return Base::retError('请输入邮箱');
|
||||||
|
}
|
||||||
|
$masked = OnlineLicense::emailSend($email);
|
||||||
|
return Base::retSuccess('验证码已发送', ['email' => $masked]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮箱 + 验证码登录并签发在线授权
|
||||||
|
*/
|
||||||
|
public function login()
|
||||||
|
{
|
||||||
|
User::auth('admin');
|
||||||
|
$email = trim(Request::input('email'));
|
||||||
|
$code = trim(Request::input('code'));
|
||||||
|
if ($email === '' || $code === '') {
|
||||||
|
return Base::retError('请输入邮箱和验证码');
|
||||||
|
}
|
||||||
|
$data = OnlineLicense::login($email, $code);
|
||||||
|
return Base::retSuccess('授权成功', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮箱 + 验证码申请试用并签发
|
||||||
|
*/
|
||||||
|
public function trial()
|
||||||
|
{
|
||||||
|
User::auth('admin');
|
||||||
|
$email = trim(Request::input('email'));
|
||||||
|
$code = trim(Request::input('code'));
|
||||||
|
if ($email === '' || $code === '') {
|
||||||
|
return Base::retError('请输入邮箱和验证码');
|
||||||
|
}
|
||||||
|
$data = OnlineLicense::trial($email, $code);
|
||||||
|
return Base::retSuccess('试用已开通', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前在线授权状态
|
||||||
|
*/
|
||||||
|
public function status()
|
||||||
|
{
|
||||||
|
User::auth('admin');
|
||||||
|
return Base::retSuccess('success', OnlineLicense::status());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进入授权页时的静默刷新:服务可达则更新授权数据,网络失败则不更新、不提示。
|
||||||
|
*/
|
||||||
|
public function refresh()
|
||||||
|
{
|
||||||
|
User::auth('admin');
|
||||||
|
OnlineLicense::refresh();
|
||||||
|
return Base::retSuccess('success', OnlineLicense::status());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出在线授权(释放座位 + 回落默认)
|
||||||
|
*/
|
||||||
|
public function logout()
|
||||||
|
{
|
||||||
|
User::auth('admin');
|
||||||
|
OnlineLicense::logout();
|
||||||
|
return Base::retSuccess('已退出在线授权');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,7 +5,6 @@ namespace App\Http\Controllers\Api;
|
|||||||
use Request;
|
use Request;
|
||||||
use Redirect;
|
use Redirect;
|
||||||
use Response;
|
use Response;
|
||||||
use Madzipper;
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use App\Module\Down;
|
use App\Module\Down;
|
||||||
use App\Module\Doo;
|
use App\Module\Doo;
|
||||||
@ -2003,7 +2002,7 @@ class ProjectController extends AbstractController
|
|||||||
Base::deleteDirAndFile($zipPath, true);
|
Base::deleteDirAndFile($zipPath, true);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Madzipper::make($zipPath)->add($xlsPath)->close();
|
Base::zipAddFiles($zipPath, $xlsPath);
|
||||||
} catch (\Throwable) {
|
} catch (\Throwable) {
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
@ -2171,7 +2170,7 @@ class ProjectController extends AbstractController
|
|||||||
Base::deleteDirAndFile($zipPath, true);
|
Base::deleteDirAndFile($zipPath, true);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Madzipper::make($zipPath)->add($xlsPath)->close();
|
Base::zipAddFiles($zipPath, $xlsPath);
|
||||||
} catch (\Throwable) {
|
} catch (\Throwable) {
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
|
|||||||
@ -9,21 +9,22 @@ use App\Module\AI;
|
|||||||
use App\Module\Down;
|
use App\Module\Down;
|
||||||
use Request;
|
use Request;
|
||||||
use Response;
|
use Response;
|
||||||
use Madzipper;
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use App\Module\Doo;
|
use App\Module\Doo;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Module\Base;
|
use App\Module\Base;
|
||||||
|
use App\Module\OnlineLicense;
|
||||||
use App\Module\Timer;
|
use App\Module\Timer;
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use LdapRecord\Container;
|
use LdapRecord\Container;
|
||||||
use App\Module\BillExport;
|
use App\Module\BillExport;
|
||||||
use Guanguans\Notify\Factory;
|
use Symfony\Component\Mailer\Mailer;
|
||||||
|
use Symfony\Component\Mailer\Transport;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
use App\Models\UserCheckinRecord;
|
use App\Models\UserCheckinRecord;
|
||||||
use App\Module\Apps;
|
use App\Module\Apps;
|
||||||
use App\Module\BillMultipleExport;
|
use App\Module\BillMultipleExport;
|
||||||
use LdapRecord\LdapRecordException;
|
use LdapRecord\LdapRecordException;
|
||||||
use Guanguans\Notify\Messages\EmailMessage;
|
|
||||||
use Swoole\Coroutine;
|
use Swoole\Coroutine;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -54,7 +55,7 @@ class SystemController extends AbstractController
|
|||||||
{
|
{
|
||||||
$type = trim(Request::input('type'));
|
$type = trim(Request::input('type'));
|
||||||
if ($type == 'save') {
|
if ($type == 'save') {
|
||||||
if (env("SYSTEM_SETTING") == 'disabled') {
|
if (config('dootask.system_setting') == 'disabled') {
|
||||||
return Base::retError('当前环境禁止修改');
|
return Base::retError('当前环境禁止修改');
|
||||||
}
|
}
|
||||||
Base::checkClientVersion('0.41.11');
|
Base::checkClientVersion('0.41.11');
|
||||||
@ -110,7 +111,7 @@ class SystemController extends AbstractController
|
|||||||
return Base::retError('自动归档时间不可大于100天!');
|
return Base::retError('自动归档时间不可大于100天!');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($all['system_alias'] == env('APP_NAME')) {
|
if ($all['system_alias'] == config('app.name')) {
|
||||||
$all['system_alias'] = '';
|
$all['system_alias'] = '';
|
||||||
}
|
}
|
||||||
if ($all['system_welcome'] == '欢迎您,{username}') {
|
if ($all['system_welcome'] == '欢迎您,{username}') {
|
||||||
@ -184,7 +185,7 @@ class SystemController extends AbstractController
|
|||||||
//
|
//
|
||||||
$type = trim(Request::input('type'));
|
$type = trim(Request::input('type'));
|
||||||
if ($type == 'save') {
|
if ($type == 'save') {
|
||||||
if (env("SYSTEM_SETTING") == 'disabled') {
|
if (config('dootask.system_setting') == 'disabled') {
|
||||||
return Base::retError('当前环境禁止修改');
|
return Base::retError('当前环境禁止修改');
|
||||||
}
|
}
|
||||||
$user->identity('admin');
|
$user->identity('admin');
|
||||||
@ -254,7 +255,7 @@ class SystemController extends AbstractController
|
|||||||
//
|
//
|
||||||
$type = trim(Request::input('type'));
|
$type = trim(Request::input('type'));
|
||||||
if ($type == 'save') {
|
if ($type == 'save') {
|
||||||
if (env("SYSTEM_SETTING") == 'disabled') {
|
if (config('dootask.system_setting') == 'disabled') {
|
||||||
return Base::retError('当前环境禁止修改');
|
return Base::retError('当前环境禁止修改');
|
||||||
}
|
}
|
||||||
$all = Request::input();
|
$all = Request::input();
|
||||||
@ -278,7 +279,7 @@ class SystemController extends AbstractController
|
|||||||
}
|
}
|
||||||
//
|
//
|
||||||
$setting['open'] = $setting['open'] ?: 'close';
|
$setting['open'] = $setting['open'] ?: 'close';
|
||||||
if (env("SYSTEM_SETTING") == 'disabled') {
|
if (config('dootask.system_setting') == 'disabled') {
|
||||||
$setting['appid'] = substr($setting['appid'], 0, 4) . str_repeat('*', strlen($setting['appid']) - 8) . substr($setting['appid'], -4);
|
$setting['appid'] = substr($setting['appid'], 0, 4) . str_repeat('*', strlen($setting['appid']) - 8) . substr($setting['appid'], -4);
|
||||||
$setting['app_certificate'] = substr($setting['app_certificate'], 0, 4) . str_repeat('*', strlen($setting['app_certificate']) - 8) . substr($setting['app_certificate'], -4);
|
$setting['app_certificate'] = substr($setting['app_certificate'], 0, 4) . str_repeat('*', strlen($setting['app_certificate']) - 8) . substr($setting['app_certificate'], -4);
|
||||||
$setting['api_key'] = substr($setting['api_key'], 0, 4) . str_repeat('*', strlen($setting['api_key']) - 8) . substr($setting['api_key'], -4);
|
$setting['api_key'] = substr($setting['api_key'], 0, 4) . str_repeat('*', strlen($setting['api_key']) - 8) . substr($setting['api_key'], -4);
|
||||||
@ -324,7 +325,7 @@ class SystemController extends AbstractController
|
|||||||
$filter = trim(Request::input('filter'));
|
$filter = trim(Request::input('filter'));
|
||||||
$setting = Base::setting('aibotSetting');
|
$setting = Base::setting('aibotSetting');
|
||||||
if ($type == 'save') {
|
if ($type == 'save') {
|
||||||
if (env("SYSTEM_SETTING") == 'disabled') {
|
if (config('dootask.system_setting') == 'disabled') {
|
||||||
return Base::retError('当前环境禁止修改');
|
return Base::retError('当前环境禁止修改');
|
||||||
}
|
}
|
||||||
Base::checkClientVersion('0.41.11');
|
Base::checkClientVersion('0.41.11');
|
||||||
@ -342,11 +343,15 @@ class SystemController extends AbstractController
|
|||||||
}, ARRAY_FILTER_USE_BOTH);
|
}, ARRAY_FILTER_USE_BOTH);
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
if (env("SYSTEM_SETTING") == 'disabled') {
|
if (config('dootask.system_setting') == 'disabled') {
|
||||||
foreach ($setting as $key => $item) {
|
foreach ($setting as $key => $item) {
|
||||||
if (empty($item)) {
|
if (empty($item)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// dooai_key 是官方网关 token,需原样返回供鉴权
|
||||||
|
if ($key === 'dooai_key') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (str_ends_with($key, '_key') || str_ends_with($key, '_secret')) {
|
if (str_ends_with($key, '_key') || str_ends_with($key, '_secret')) {
|
||||||
$setting[$key] = substr($item, 0, 4) . str_repeat('*', strlen($item) - 8) . substr($item, -4);
|
$setting[$key] = substr($item, 0, 4) . str_repeat('*', strlen($item) - 8) . substr($item, -4);
|
||||||
}
|
}
|
||||||
@ -396,7 +401,7 @@ class SystemController extends AbstractController
|
|||||||
//
|
//
|
||||||
$type = trim(Request::input('type'));
|
$type = trim(Request::input('type'));
|
||||||
if ($type == 'save') {
|
if ($type == 'save') {
|
||||||
if (env("SYSTEM_SETTING") == 'disabled') {
|
if (config('dootask.system_setting') == 'disabled') {
|
||||||
return Base::retError('当前环境禁止修改');
|
return Base::retError('当前环境禁止修改');
|
||||||
}
|
}
|
||||||
$all = Request::input();
|
$all = Request::input();
|
||||||
@ -545,7 +550,7 @@ class SystemController extends AbstractController
|
|||||||
//
|
//
|
||||||
$type = trim(Request::input('type'));
|
$type = trim(Request::input('type'));
|
||||||
if ($type == 'save') {
|
if ($type == 'save') {
|
||||||
if (env("SYSTEM_SETTING") == 'disabled') {
|
if (config('dootask.system_setting') == 'disabled') {
|
||||||
return Base::retError('当前环境禁止修改');
|
return Base::retError('当前环境禁止修改');
|
||||||
}
|
}
|
||||||
$all = Request::input();
|
$all = Request::input();
|
||||||
@ -610,7 +615,7 @@ class SystemController extends AbstractController
|
|||||||
return Base::retError($e->getMessage() ?: "验证失败:未知错误", config("ldap.connections.default"));
|
return Base::retError($e->getMessage() ?: "验证失败:未知错误", config("ldap.connections.default"));
|
||||||
}
|
}
|
||||||
} elseif ($type == 'save') {
|
} elseif ($type == 'save') {
|
||||||
if (env("SYSTEM_SETTING") == 'disabled') {
|
if (config('dootask.system_setting') == 'disabled') {
|
||||||
return Base::retError('当前环境禁止修改');
|
return Base::retError('当前环境禁止修改');
|
||||||
}
|
}
|
||||||
$all = Base::newTrim(Request::input());
|
$all = Base::newTrim(Request::input());
|
||||||
@ -662,7 +667,7 @@ class SystemController extends AbstractController
|
|||||||
//
|
//
|
||||||
$type = trim(Request::input('type'));
|
$type = trim(Request::input('type'));
|
||||||
if ($type == 'save') {
|
if ($type == 'save') {
|
||||||
if (env("SYSTEM_SETTING") == 'disabled') {
|
if (config('dootask.system_setting') == 'disabled') {
|
||||||
return Base::retError('当前环境禁止修改');
|
return Base::retError('当前环境禁止修改');
|
||||||
}
|
}
|
||||||
$all = Base::newTrim(Request::input());
|
$all = Base::newTrim(Request::input());
|
||||||
@ -695,8 +700,8 @@ class SystemController extends AbstractController
|
|||||||
*/
|
*/
|
||||||
public function demo()
|
public function demo()
|
||||||
{
|
{
|
||||||
$demo_account = env('DEMO_ACCOUNT');
|
$demo_account = config('dootask.demo_account');
|
||||||
$demo_password = env('DEMO_PASSWORD');
|
$demo_password = config('dootask.demo_password');
|
||||||
if (empty($demo_account) || empty($demo_password)) {
|
if (empty($demo_account) || empty($demo_password)) {
|
||||||
return Base::retError('No demo account');
|
return Base::retError('No demo account');
|
||||||
}
|
}
|
||||||
@ -857,6 +862,8 @@ class SystemController extends AbstractController
|
|||||||
if ($type == 'save') {
|
if ($type == 'save') {
|
||||||
$license = Request::input('license');
|
$license = Request::input('license');
|
||||||
Doo::licenseSave($license);
|
Doo::licenseSave($license);
|
||||||
|
// 离线/在线互斥:保存离线 license 即退出在线模式(尽力释放座位+清在线标志,不删除刚写入的文件)
|
||||||
|
OnlineLicense::switchToOffline();
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
$data = [
|
$data = [
|
||||||
@ -892,6 +899,11 @@ class SystemController extends AbstractController
|
|||||||
if ($data['info']['expired_at'] && strtotime($data['info']['expired_at']) <= Timer::time()) {
|
if ($data['info']['expired_at'] && strtotime($data['info']['expired_at']) <= Timer::time()) {
|
||||||
$data['error'][] = '终端License已过期';
|
$data['error'][] = '终端License已过期';
|
||||||
}
|
}
|
||||||
|
// 在线授权:把状态机提醒并入 error[](dashboard 警告条与本页错误展示自动复用),并附在线状态
|
||||||
|
foreach (OnlineLicense::stageMessages() as $msg) {
|
||||||
|
$data['error'][] = $msg;
|
||||||
|
}
|
||||||
|
$data['online'] = OnlineLicense::status();
|
||||||
//
|
//
|
||||||
if ($type === 'error') {
|
if ($type === 'error') {
|
||||||
$data = [
|
$data = [
|
||||||
@ -917,7 +929,7 @@ class SystemController extends AbstractController
|
|||||||
*/
|
*/
|
||||||
public function get__info()
|
public function get__info()
|
||||||
{
|
{
|
||||||
if (Request::input("key") !== env('APP_KEY')) {
|
if (Request::input("key") !== config('app.key')) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return Base::retSuccess('success', [
|
return Base::retSuccess('success', [
|
||||||
@ -1233,21 +1245,19 @@ class SystemController extends AbstractController
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Setting::validateAddr($all['to'], function($to) use ($all) {
|
Setting::validateAddr($all['to'], function($to) use ($all) {
|
||||||
Factory::mailer()
|
$mailer = new Mailer(Transport::fromDsn("smtp://{$all['account']}:{$all['password']}@{$all['smtp_server']}:{$all['port']}?verify_peer=0"));
|
||||||
->setDsn("smtp://{$all['account']}:{$all['password']}@{$all['smtp_server']}:{$all['port']}?verify_peer=0")
|
$mailer->send((new Email())
|
||||||
->setMessage(EmailMessage::create()
|
->from(Base::settingFind('system', 'system_alias', 'Task') . " <{$all['account']}>")
|
||||||
->from(Base::settingFind('system', 'system_alias', 'Task') . " <{$all['account']}>")
|
->to($to)
|
||||||
->to($to)
|
->subject('Mail sending test')
|
||||||
->subject('Mail sending test')
|
->html('<p>' . Doo::translate('收到此电子邮件意味着您的邮箱配置正确。') . '</p>'));
|
||||||
->html('<p>' . Doo::translate('收到此电子邮件意味着您的邮箱配置正确。') . '</p>'))
|
|
||||||
->send();
|
|
||||||
}, function () {
|
}, function () {
|
||||||
throw new \Exception("收件人地址错误或已被忽略");
|
throw new \Exception("收件人地址错误或已被忽略");
|
||||||
});
|
});
|
||||||
return Base::retSuccess('成功发送');
|
return Base::retSuccess('成功发送');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
// 一般是请求超时
|
// 一般是请求超时
|
||||||
if (str_contains($e->getMessage(), "Timed Out")) {
|
if (stripos($e->getMessage(), "timed out") !== false) {
|
||||||
return Base::retError("邮件发送超时,请检查邮箱配置是否正确");
|
return Base::retError("邮件发送超时,请检查邮箱配置是否正确");
|
||||||
} elseif ($e->getCode() === 550) {
|
} elseif ($e->getCode() === 550) {
|
||||||
return Base::retError('邮件内容被拒绝,请检查邮箱是否开启接收功能');
|
return Base::retError('邮件内容被拒绝,请检查邮箱是否开启接收功能');
|
||||||
@ -1445,7 +1455,7 @@ class SystemController extends AbstractController
|
|||||||
Base::deleteDirAndFile($zipPath, true);
|
Base::deleteDirAndFile($zipPath, true);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Madzipper::make($zipPath)->add($xlsPath)->close();
|
Base::zipAddFiles($zipPath, $xlsPath);
|
||||||
} catch (\Throwable) {
|
} catch (\Throwable) {
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
|
|||||||
@ -322,7 +322,7 @@ class UsersController extends AbstractController
|
|||||||
$expiredAtCarbon = $expiredAt ? Carbon::parse($expiredAt) : null;
|
$expiredAtCarbon = $expiredAt ? Carbon::parse($expiredAt) : null;
|
||||||
$data = [
|
$data = [
|
||||||
'expired_at' => $expiredAtCarbon?->toDateTimeString(),
|
'expired_at' => $expiredAtCarbon?->toDateTimeString(),
|
||||||
'remaining_seconds' => $expiredAtCarbon ? Carbon::now()->diffInSeconds($expiredAtCarbon, false) : null,
|
'remaining_seconds' => $expiredAtCarbon ? (int)Carbon::now()->diffInSeconds($expiredAtCarbon, false) : null,
|
||||||
'expired' => $expired,
|
'expired' => $expired,
|
||||||
'server_time' => Carbon::now()->toDateTimeString(),
|
'server_time' => Carbon::now()->toDateTimeString(),
|
||||||
];
|
];
|
||||||
@ -1635,7 +1635,7 @@ class UsersController extends AbstractController
|
|||||||
} elseif ($type === 'create') {
|
} elseif ($type === 'create') {
|
||||||
$meetingid = strtoupper(Base::generatePassword(11, 1));
|
$meetingid = strtoupper(Base::generatePassword(11, 1));
|
||||||
$name = $name ?: Doo::translate("{$user?->nickname} 发起的会议");
|
$name = $name ?: Doo::translate("{$user?->nickname} 发起的会议");
|
||||||
$channel = "DooTask:" . substr(md5($meetingid . env("APP_KEY")), 16);
|
$channel = "DooTask:" . substr(md5($meetingid . config('app.key')), 16);
|
||||||
$meeting = Meeting::createInstance([
|
$meeting = Meeting::createInstance([
|
||||||
'meetingid' => $meetingid,
|
'meetingid' => $meetingid,
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
|
|||||||
@ -26,7 +26,7 @@ use App\Tasks\UnclaimedTaskRemindTask;
|
|||||||
use App\Tasks\TodoRemindTask;
|
use App\Tasks\TodoRemindTask;
|
||||||
use App\Tasks\AiTaskLoopTask;
|
use App\Tasks\AiTaskLoopTask;
|
||||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||||
use Laravolt\Avatar\Avatar;
|
use App\Module\PatchedAvatar as Avatar;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -221,11 +221,13 @@ class IndexController extends InvokeController
|
|||||||
'radius' => 0,
|
'radius' => 0,
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
return response($avatar->create($name)->save($file))
|
$avatar->create($name)->save($file);
|
||||||
->header('Pragma', 'public')
|
return response()->file($file, [
|
||||||
->header('Cache-Control', 'max-age=1814400')
|
'Pragma' => 'public',
|
||||||
->header('Content-type', 'image/png')
|
'Cache-Control' => 'max-age=1814400',
|
||||||
->header('Expires', gmdate('D, d M Y H:i:s \G\M\T', time() + 1814400));
|
'Content-type' => 'image/png',
|
||||||
|
'Expires' => gmdate('D, d M Y H:i:s \G\M\T', time() + 1814400),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -299,7 +301,7 @@ class IndexController extends InvokeController
|
|||||||
if (preg_match("/^\d+\.\d+\.\d+$/", $publishVersion)) {
|
if (preg_match("/^\d+\.\d+\.\d+$/", $publishVersion)) {
|
||||||
// 判断密钥
|
// 判断密钥
|
||||||
$publishKey = Request::header('publish-key');
|
$publishKey = Request::header('publish-key');
|
||||||
if ($publishKey !== env('APP_KEY')) {
|
if ($publishKey !== config('app.key')) {
|
||||||
return Base::retError("key error");
|
return Base::retError("key error");
|
||||||
}
|
}
|
||||||
// 判断版本
|
// 判断版本
|
||||||
|
|||||||
@ -24,8 +24,8 @@ class InvokeController extends BaseController
|
|||||||
if ($action) {
|
if ($action) {
|
||||||
$app .= "__" . $action;
|
$app .= "__" . $action;
|
||||||
}
|
}
|
||||||
// 接口不存在
|
// 接口不存在(仅 public 方法可作为端点,protected/private 为内部方法,不暴露为路由)
|
||||||
if (!method_exists($this, $app)) {
|
if (!method_exists($this, $app) || !(new \ReflectionMethod($this, $app))->isPublic()) {
|
||||||
$msg = "404 not found (" . str_replace("__", "/", $app) . ").";
|
$msg = "404 not found (" . str_replace("__", "/", $app) . ").";
|
||||||
return Base::ajaxError($msg);
|
return Base::ajaxError($msg);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,68 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
|
||||||
|
|
||||||
class Kernel extends HttpKernel
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The application's global HTTP middleware stack.
|
|
||||||
*
|
|
||||||
* These middleware are run during every request to your application.
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $middleware = [
|
|
||||||
// \App\Http\Middleware\TrustHosts::class,
|
|
||||||
\App\Http\Middleware\TrustProxies::class,
|
|
||||||
\Fruitcake\Cors\HandleCors::class,
|
|
||||||
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
|
|
||||||
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
|
|
||||||
\App\Http\Middleware\TrimStrings::class,
|
|
||||||
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The application's route middleware groups.
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $middlewareGroups = [
|
|
||||||
'web' => [
|
|
||||||
\App\Http\Middleware\EncryptCookies::class,
|
|
||||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
|
||||||
\Illuminate\Session\Middleware\StartSession::class,
|
|
||||||
// \Illuminate\Session\Middleware\AuthenticateSession::class,
|
|
||||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
|
||||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
|
||||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
|
||||||
],
|
|
||||||
|
|
||||||
'api' => [
|
|
||||||
'throttle:api',
|
|
||||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The application's route middleware.
|
|
||||||
*
|
|
||||||
* These middleware may be assigned to groups or used individually.
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $routeMiddleware = [
|
|
||||||
'auth' => \App\Http\Middleware\Authenticate::class,
|
|
||||||
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
|
||||||
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
|
|
||||||
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
|
||||||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
|
||||||
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
|
|
||||||
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
|
|
||||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
|
||||||
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
|
|
||||||
|
|
||||||
'webapi' => \App\Http\Middleware\WebApi::class,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
|
||||||
|
|
||||||
use Illuminate\Auth\Middleware\Authenticate as Middleware;
|
|
||||||
|
|
||||||
class Authenticate extends Middleware
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Get the path the user should be redirected to when they are not authenticated.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @return string|null
|
|
||||||
*/
|
|
||||||
protected function redirectTo($request)
|
|
||||||
{
|
|
||||||
if (! $request->expectsJson()) {
|
|
||||||
return route('login');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
|
||||||
|
|
||||||
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
|
|
||||||
|
|
||||||
class EncryptCookies extends Middleware
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The names of the cookies that should not be encrypted.
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $except = [
|
|
||||||
//
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
|
|
||||||
|
|
||||||
class PreventRequestsDuringMaintenance extends Middleware
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The URIs that should be reachable while maintenance mode is enabled.
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $except = [
|
|
||||||
//
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
|
||||||
|
|
||||||
use App\Providers\RouteServiceProvider;
|
|
||||||
use Closure;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
|
|
||||||
class RedirectIfAuthenticated
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Handle an incoming request.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @param \Closure $next
|
|
||||||
* @param string|null ...$guards
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function handle(Request $request, Closure $next, ...$guards)
|
|
||||||
{
|
|
||||||
$guards = empty($guards) ? [null] : $guards;
|
|
||||||
|
|
||||||
foreach ($guards as $guard) {
|
|
||||||
if (Auth::guard($guard)->check()) {
|
|
||||||
return redirect(RouteServiceProvider::HOME);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $next($request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
|
|
||||||
|
|
||||||
class TrimStrings extends Middleware
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The names of the attributes that should not be trimmed.
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $except = [
|
|
||||||
'current_password',
|
|
||||||
'password',
|
|
||||||
'password_confirmation',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
|
||||||
|
|
||||||
use Illuminate\Http\Middleware\TrustHosts as Middleware;
|
|
||||||
|
|
||||||
class TrustHosts extends Middleware
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Get the host patterns that should be trusted.
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function hosts()
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
$this->allSubdomainsOfApplicationUrl(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
|
||||||
|
|
||||||
use Fideloper\Proxy\TrustProxies as Middleware;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class TrustProxies extends Middleware
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The trusted proxies for this application.
|
|
||||||
*
|
|
||||||
* @var array|string|null
|
|
||||||
*/
|
|
||||||
protected $proxies;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The headers that should be used to detect proxies.
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
protected $headers = Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO | Request::HEADER_X_FORWARDED_AWS_ELB;
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
|
|
||||||
|
|
||||||
class VerifyCsrfToken extends Middleware
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The URIs that should be excluded from CSRF verification.
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $except = [
|
|
||||||
// 接口部分
|
|
||||||
'api/*',
|
|
||||||
|
|
||||||
// 发布桌面端
|
|
||||||
'desktop/publish/',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@ -25,14 +25,6 @@ class WebApi
|
|||||||
RequestContext::set('start_time', microtime(true));
|
RequestContext::set('start_time', microtime(true));
|
||||||
RequestContext::set('header_language', $request->header('language'));
|
RequestContext::set('header_language', $request->header('language'));
|
||||||
|
|
||||||
// 强制 https
|
|
||||||
$APP_SCHEME = env('APP_SCHEME', 'auto');
|
|
||||||
if (in_array(strtolower($APP_SCHEME), ['https', 'on', 'ssl', '1', 'true', 'yes'], true)) {
|
|
||||||
$request->server->set('HTTPS', 'on');
|
|
||||||
$request->headers->set('X-Forwarded-Proto', 'https');
|
|
||||||
$request->setTrustedProxies([$request->getClientIp()], $request::HEADER_X_FORWARDED_PROTO);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新请求的基本URL
|
// 更新请求的基本URL
|
||||||
RequestContext::updateBaseUrl($request);
|
RequestContext::updateBaseUrl($request);
|
||||||
|
|
||||||
|
|||||||
@ -15,10 +15,8 @@ class LdapUser extends Model
|
|||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The object classes of the LDAP model.
|
* The object classes of the LDAP model.
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
*/
|
||||||
public static $objectClasses = [
|
public static array $objectClasses = [
|
||||||
'person',
|
'person',
|
||||||
'top',
|
'top',
|
||||||
];
|
];
|
||||||
|
|||||||
@ -5,6 +5,7 @@ namespace App\Models;
|
|||||||
use App\Exceptions\ApiException;
|
use App\Exceptions\ApiException;
|
||||||
use App\Module\Base;
|
use App\Module\Base;
|
||||||
use DateTimeInterface;
|
use DateTimeInterface;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
@ -31,7 +32,10 @@ class AbstractModel extends Model
|
|||||||
|
|
||||||
const ID = 'id';
|
const ID = 'id';
|
||||||
|
|
||||||
protected $dates = [
|
/**
|
||||||
|
* 全局日期字段(Laravel 10 移除 $dates 属性后改经 getCasts 合并,子模型 $casts 同名键优先)
|
||||||
|
*/
|
||||||
|
protected $defaultDatetimeCasts = [
|
||||||
'top_at',
|
'top_at',
|
||||||
'last_at',
|
'last_at',
|
||||||
|
|
||||||
@ -59,6 +63,15 @@ class AbstractModel extends Model
|
|||||||
'deleted_at',
|
'deleted_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function getCasts(): array
|
||||||
|
{
|
||||||
|
$casts = parent::getCasts();
|
||||||
|
foreach ($this->defaultDatetimeCasts as $field) {
|
||||||
|
$casts[$field] ??= 'datetime';
|
||||||
|
}
|
||||||
|
return $casts;
|
||||||
|
}
|
||||||
|
|
||||||
protected $appendattrs = [];
|
protected $appendattrs = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -189,6 +202,66 @@ class AbstractModel extends Model
|
|||||||
return $instance;
|
return $instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 覆写框架 saveOrIgnore 的底层插入逻辑。
|
||||||
|
*
|
||||||
|
* 框架默认走 insertOrIgnoreReturning(INSERT ... ON CONFLICT ... RETURNING),
|
||||||
|
* MySQL/MariaDB 的 grammar 不支持该变体,会抛
|
||||||
|
* "This database engine does not support insert or ignore with returning."。
|
||||||
|
* 这里改用 MySQL 支持的 INSERT IGNORE,并在成功插入时手动回填自增ID,
|
||||||
|
* 保持与框架一致的返回语义(冲突被忽略时返回 false)。
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||||
|
* @param array|string|null $uniqueBy
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function performInsertOrIgnore(Builder $query, array|string|null $uniqueBy)
|
||||||
|
{
|
||||||
|
// MySQL INSERT IGNORE 无法按指定列限制冲突范围,所有 unique 冲突一并吞掉。
|
||||||
|
// 若调用方传了 $uniqueBy 期望精确 scope,这里直接抛错,避免与框架语义偷偷不一致。
|
||||||
|
if ($uniqueBy !== null) {
|
||||||
|
throw new \InvalidArgumentException('saveOrIgnore $uniqueBy is not supported on MySQL driver; pass null.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->usesUniqueIds()) {
|
||||||
|
$this->setUniqueIds();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->fireModelEvent('creating') === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->usesTimestamps()) {
|
||||||
|
$this->updateTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
|
$attributes = $this->getAttributesForInsert();
|
||||||
|
|
||||||
|
if (empty($attributes)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($query->toBase()->insertOrIgnore($attributes) === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->getIncrementing()) {
|
||||||
|
$lastId = $query->getConnection()->getPdo()->lastInsertId();
|
||||||
|
// 无 auto_increment 列的表上 INSERT IGNORE 即使插入成功 lastInsertId 也返回 "0",
|
||||||
|
// 别用它去覆盖业务设置的主键。
|
||||||
|
if ($lastId > 0) {
|
||||||
|
$this->setAttribute($this->getKeyName(), $lastId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->exists = true;
|
||||||
|
$this->wasRecentlyCreated = true;
|
||||||
|
|
||||||
|
$this->fireModelEvent('created', false);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新数据校验
|
* 更新数据校验
|
||||||
* @param array $param
|
* @param array $param
|
||||||
|
|||||||
48
app/Models/AiAssistantFeedback.php
Normal file
48
app/Models/AiAssistantFeedback.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 助手回复反馈(👍/👎)
|
||||||
|
*
|
||||||
|
* @property int $id
|
||||||
|
* @property int $userid
|
||||||
|
* @property string $session_key
|
||||||
|
* @property string $session_id
|
||||||
|
* @property int $local_id
|
||||||
|
* @property string $feedback
|
||||||
|
* @property string|null $prompt
|
||||||
|
* @property string $answer_digest
|
||||||
|
* @property string|null $answer
|
||||||
|
* @property string|null $source_ids
|
||||||
|
* @property string $model
|
||||||
|
* @property \Carbon\Carbon $created_at
|
||||||
|
* @property \Carbon\Carbon $updated_at
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantFeedback cancelAppend()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantFeedback cancelHidden()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantFeedback change($array)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantFeedback getKeyValue()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantFeedback newModelQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantFeedback newQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantFeedback query()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantFeedback remove()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantFeedback saveOrIgnore()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantFeedback whereAnswer($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantFeedback whereAnswerDigest($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantFeedback whereCreatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantFeedback whereFeedback($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantFeedback whereId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantFeedback whereLocalId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantFeedback whereModel($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantFeedback wherePrompt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantFeedback whereSessionId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantFeedback whereSessionKey($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantFeedback whereSourceIds($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantFeedback whereUpdatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantFeedback whereUserid($value)
|
||||||
|
* @mixin \Eloquent
|
||||||
|
*/
|
||||||
|
class AiAssistantFeedback extends AbstractModel
|
||||||
|
{
|
||||||
|
protected $table = 'ai_assistant_feedbacks';
|
||||||
|
}
|
||||||
50
app/Models/AiAssistantSearchLog.php
Normal file
50
app/Models/AiAssistantSearchLog.php
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 助手帮助知识库检索日志
|
||||||
|
*
|
||||||
|
* @property int $id
|
||||||
|
* @property int $userid
|
||||||
|
* @property int $dialog_id
|
||||||
|
* @property string $context_key
|
||||||
|
* @property string $source
|
||||||
|
* @property string $query
|
||||||
|
* @property string $locale
|
||||||
|
* @property string|null $source_ids
|
||||||
|
* @property float $top_score
|
||||||
|
* @property int $result_count
|
||||||
|
* @property int $duration_ms
|
||||||
|
* @property int $empty
|
||||||
|
* @property \Carbon\Carbon $created_at
|
||||||
|
* @property \Carbon\Carbon $updated_at
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSearchLog cancelAppend()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSearchLog cancelHidden()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSearchLog change($array)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSearchLog getKeyValue()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSearchLog newModelQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSearchLog newQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSearchLog query()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSearchLog remove()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSearchLog saveOrIgnore()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSearchLog whereContextKey($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSearchLog whereCreatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSearchLog whereDialogId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSearchLog whereDurationMs($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSearchLog whereEmpty($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSearchLog whereId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSearchLog whereLocale($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSearchLog whereQuery($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSearchLog whereResultCount($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSearchLog whereSource($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSearchLog whereSourceIds($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSearchLog whereTopScore($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSearchLog whereUpdatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSearchLog whereUserid($value)
|
||||||
|
* @mixin \Eloquent
|
||||||
|
*/
|
||||||
|
class AiAssistantSearchLog extends AbstractModel
|
||||||
|
{
|
||||||
|
protected $table = 'ai_assistant_search_logs';
|
||||||
|
}
|
||||||
@ -15,6 +15,26 @@ namespace App\Models;
|
|||||||
* @property string|null $images
|
* @property string|null $images
|
||||||
* @property \Carbon\Carbon $created_at
|
* @property \Carbon\Carbon $created_at
|
||||||
* @property \Carbon\Carbon $updated_at
|
* @property \Carbon\Carbon $updated_at
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSession cancelAppend()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSession cancelHidden()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSession change($array)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSession getKeyValue()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSession newModelQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSession newQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSession query()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSession remove()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSession saveOrIgnore()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSession whereCreatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSession whereData($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSession whereId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSession whereImages($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSession whereSceneKey($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSession whereSessionId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSession whereSessionKey($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSession whereTitle($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSession whereUpdatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiAssistantSession whereUserid($value)
|
||||||
|
* @mixin \Eloquent
|
||||||
*/
|
*/
|
||||||
class AiAssistantSession extends AbstractModel
|
class AiAssistantSession extends AbstractModel
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,99 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Cache;
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use DB;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* App\Models\ApproveProcInstHistory
|
|
||||||
*
|
|
||||||
* @property int $id
|
|
||||||
* @property int $proc_def_id 流程定义ID
|
|
||||||
* @property string|null $proc_def_name 流程定义名
|
|
||||||
* @property string|null $title 标题
|
|
||||||
* @property int|null $department_id 用户部门ID
|
|
||||||
* @property string|null $department 用户部门
|
|
||||||
* @property string|null $company 用户公司
|
|
||||||
* @property string|null $node_id 当前节点
|
|
||||||
* @property string|null $candidate 审批人
|
|
||||||
* @property int|null $task_id 当前任务
|
|
||||||
* @property string|null $start_time 开始时间
|
|
||||||
* @property string|null $end_time 结束时间
|
|
||||||
* @property int|null $duration 持续时间
|
|
||||||
* @property string|null $start_user_id 开始用户ID
|
|
||||||
* @property string|null $start_user_name 开始用户名
|
|
||||||
* @property int|null $is_finished 是否完成
|
|
||||||
* @property string|null $var
|
|
||||||
* @property int $state 当前状态: 0待审批,1审批中,2通过,3拒绝,4撤回
|
|
||||||
* @property string|null $latest_comment
|
|
||||||
* @property string|null $global_comment
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory newModelQuery()
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory newQuery()
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory query()
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereCandidate($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereCompany($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereDepartment($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereDepartmentId($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereDuration($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereEndTime($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereGlobalComment($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereId($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereIsFinished($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereLatestComment($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereNodeId($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereProcDefId($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereProcDefName($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereStartTime($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereStartUserId($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereStartUserName($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereState($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereTaskId($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereTitle($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereVar($value)
|
|
||||||
* @mixin \Eloquent
|
|
||||||
*/
|
|
||||||
class ApproveProcInstHistory extends AbstractModel
|
|
||||||
{
|
|
||||||
protected $table = 'approve_proc_inst_history';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取用户审批状态(请假、外出)
|
|
||||||
* @param $userid
|
|
||||||
* @return mixed|null
|
|
||||||
*/
|
|
||||||
public static function getUserApprovalStatus($userid)
|
|
||||||
{
|
|
||||||
if (empty($userid)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return Cache::remember('user_is_leave_' . $userid, Carbon::now()->addMinute(), function () use ($userid) {
|
|
||||||
return self::where([
|
|
||||||
['start_user_id', '=', $userid],
|
|
||||||
[DB::raw("JSON_UNQUOTE(JSON_EXTRACT(var, '$.startTime'))"), '<=', Carbon::now()->toDateTimeString()],
|
|
||||||
[DB::raw("JSON_UNQUOTE(JSON_EXTRACT(var, '$.endTime'))"), '>=', Carbon::now()->toDateTimeString()],
|
|
||||||
['state', '=', 2]
|
|
||||||
])->where(function ($query) {
|
|
||||||
$query->where('proc_def_name', 'like', '%请假%')
|
|
||||||
->orWhere('proc_def_name', 'like', '%外出%');
|
|
||||||
})->orderByDesc('id')->value('proc_def_name');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断用户是否请假(包含:请假、外出)
|
|
||||||
* @param $userid
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public static function userIsLeave($userid)
|
|
||||||
{
|
|
||||||
return (bool)self::getUserApprovalStatus($userid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* App\Models\ApproveProcMsg
|
|
||||||
*
|
|
||||||
* @property int $id
|
|
||||||
* @property int|null $proc_inst_id 流程实例ID
|
|
||||||
* @property int|null $userid 会员ID
|
|
||||||
* @property int|null $msg_id 消息ID
|
|
||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg newModelQuery()
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg newQuery()
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg query()
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereCreatedAt($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereId($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereMsgId($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereProcInstId($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereUpdatedAt($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereUserid($value)
|
|
||||||
* @mixin \Eloquent
|
|
||||||
*/
|
|
||||||
class ApproveProcMsg extends AbstractModel
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -14,6 +14,25 @@ namespace App\Models;
|
|||||||
* @property \Carbon\Carbon|null $last_retry_at 最后重试时间
|
* @property \Carbon\Carbon|null $last_retry_at 最后重试时间
|
||||||
* @property \Carbon\Carbon $created_at
|
* @property \Carbon\Carbon $created_at
|
||||||
* @property \Carbon\Carbon $updated_at
|
* @property \Carbon\Carbon $updated_at
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ManticoreSyncFailure cancelAppend()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ManticoreSyncFailure cancelHidden()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ManticoreSyncFailure change($array)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ManticoreSyncFailure getKeyValue()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ManticoreSyncFailure newModelQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ManticoreSyncFailure newQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ManticoreSyncFailure query()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ManticoreSyncFailure remove()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ManticoreSyncFailure saveOrIgnore()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ManticoreSyncFailure whereAction($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ManticoreSyncFailure whereCreatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ManticoreSyncFailure whereDataId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ManticoreSyncFailure whereDataType($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ManticoreSyncFailure whereErrorMessage($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ManticoreSyncFailure whereId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ManticoreSyncFailure whereLastRetryAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ManticoreSyncFailure whereRetryCount($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ManticoreSyncFailure whereUpdatedAt($value)
|
||||||
|
* @mixin \Eloquent
|
||||||
*/
|
*/
|
||||||
class ManticoreSyncFailure extends AbstractModel
|
class ManticoreSyncFailure extends AbstractModel
|
||||||
{
|
{
|
||||||
@ -28,10 +47,8 @@ class ManticoreSyncFailure extends AbstractModel
|
|||||||
'last_retry_at',
|
'last_retry_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $dates = [
|
protected $casts = [
|
||||||
'last_retry_at',
|
'last_retry_at' => 'datetime',
|
||||||
'created_at',
|
|
||||||
'updated_at',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -68,6 +68,10 @@ use Request;
|
|||||||
* @method static \Illuminate\Database\Eloquent\Builder|Project whereUserid($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Project whereUserid($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Project withTrashed()
|
* @method static \Illuminate\Database\Eloquent\Builder|Project withTrashed()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Project withoutTrashed()
|
* @method static \Illuminate\Database\Eloquent\Builder|Project withoutTrashed()
|
||||||
|
* @property-read array $deputy_userids
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Project whereAiAutoAnalyze($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Project whereDepartmentOwnerView($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Project whereTaskTemplateShare($value)
|
||||||
* @mixin \Eloquent
|
* @mixin \Eloquent
|
||||||
*/
|
*/
|
||||||
class Project extends AbstractModel
|
class Project extends AbstractModel
|
||||||
|
|||||||
@ -937,7 +937,7 @@ class ProjectTask extends AbstractModel
|
|||||||
'cache' => [
|
'cache' => [
|
||||||
'task_at' => $oldStringAt,
|
'task_at' => $oldStringAt,
|
||||||
'change_at' => $newStringAt,
|
'change_at' => $newStringAt,
|
||||||
'over_sec' => $effectiveEndTime->diffInSeconds($oldAt[1]),
|
'over_sec' => (int)$effectiveEndTime->diffInSeconds($oldAt[1], true),
|
||||||
'owners' => $this->taskUser->where('owner', 1)->pluck('userid')->toArray(),
|
'owners' => $this->taskUser->where('owner', 1)->pluck('userid')->toArray(),
|
||||||
'assists' => $this->taskUser->where('owner', 0)->pluck('userid')->toArray(),
|
'assists' => $this->taskUser->where('owner', 0)->pluck('userid')->toArray(),
|
||||||
]
|
]
|
||||||
@ -1633,7 +1633,7 @@ class ProjectTask extends AbstractModel
|
|||||||
$this->addLog("{任务}超期未完成", [
|
$this->addLog("{任务}超期未完成", [
|
||||||
'cache' => [
|
'cache' => [
|
||||||
'task_at' => $this->start_at . '~' . $this->end_at,
|
'task_at' => $this->start_at . '~' . $this->end_at,
|
||||||
'over_sec' => Carbon::now()->diffInSeconds($this->end_at),
|
'over_sec' => (int)Carbon::now()->diffInSeconds($this->end_at, true),
|
||||||
'owners' => $this->taskUser->where('owner', 1)->pluck('userid')->toArray(),
|
'owners' => $this->taskUser->where('owner', 1)->pluck('userid')->toArray(),
|
||||||
'assists' => $this->taskUser->where('owner', 0)->pluck('userid')->toArray(),
|
'assists' => $this->taskUser->where('owner', 0)->pluck('userid')->toArray(),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -18,6 +18,28 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||||||
* @property \Illuminate\Support\Carbon|null $executed_at
|
* @property \Illuminate\Support\Carbon|null $executed_at
|
||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
|
* @property-read \App\Models\ProjectTask|null $task
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProjectTaskAiEvent cancelAppend()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProjectTaskAiEvent cancelHidden()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProjectTaskAiEvent change($array)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProjectTaskAiEvent getKeyValue()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProjectTaskAiEvent newModelQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProjectTaskAiEvent newQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProjectTaskAiEvent query()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProjectTaskAiEvent remove()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProjectTaskAiEvent saveOrIgnore()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProjectTaskAiEvent whereCreatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProjectTaskAiEvent whereError($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProjectTaskAiEvent whereEventType($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProjectTaskAiEvent whereExecutedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProjectTaskAiEvent whereId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProjectTaskAiEvent whereMsgId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProjectTaskAiEvent whereResult($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProjectTaskAiEvent whereRetryCount($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProjectTaskAiEvent whereStatus($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProjectTaskAiEvent whereTaskId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProjectTaskAiEvent whereUpdatedAt($value)
|
||||||
|
* @mixin \Eloquent
|
||||||
*/
|
*/
|
||||||
class ProjectTaskAiEvent extends AbstractModel
|
class ProjectTaskAiEvent extends AbstractModel
|
||||||
{
|
{
|
||||||
|
|||||||
@ -38,6 +38,8 @@ namespace App\Models;
|
|||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereTitle($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereTitle($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereUpdatedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereUpdatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereUserid($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereUserid($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProjectTaskTemplate whereLastUsedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProjectTaskTemplate whereUseCount($value)
|
||||||
* @mixin \Eloquent
|
* @mixin \Eloquent
|
||||||
*/
|
*/
|
||||||
class ProjectTaskTemplate extends AbstractModel
|
class ProjectTaskTemplate extends AbstractModel
|
||||||
|
|||||||
@ -156,7 +156,7 @@ class Report extends AbstractModel
|
|||||||
* @param User|null $user
|
* @param User|null $user
|
||||||
* @return Builder|Model|\Illuminate\Database\Query\Builder|object
|
* @return Builder|Model|\Illuminate\Database\Query\Builder|object
|
||||||
*/
|
*/
|
||||||
public static function getLastOne(User $user = null)
|
public static function getLastOne(?User $user = null)
|
||||||
{
|
{
|
||||||
$user === null && $user = User::auth();
|
$user === null && $user = User::auth();
|
||||||
$one = self::whereUserid($user->userid)->orderByDesc("created_at")->first();
|
$one = self::whereUserid($user->userid)->orderByDesc("created_at")->first();
|
||||||
|
|||||||
@ -51,12 +51,12 @@ class Setting extends AbstractModel
|
|||||||
switch ($this->name) {
|
switch ($this->name) {
|
||||||
// 系统设置
|
// 系统设置
|
||||||
case 'system':
|
case 'system':
|
||||||
$value['system_alias'] = $value['system_alias'] ?: env('APP_NAME');
|
$value['system_alias'] = ($value['system_alias'] ?? null) ?: config('app.name');
|
||||||
$value['image_compress'] = $value['image_compress'] ?: 'open';
|
$value['image_compress'] = ($value['image_compress'] ?? null) ?: 'open';
|
||||||
$value['image_quality'] = min(100, max(0, intval($value['image_quality']) ?: 90));
|
$value['image_quality'] = min(100, max(0, intval($value['image_quality'] ?? 0) ?: 90));
|
||||||
$value['image_save_local'] = $value['image_save_local'] ?: 'open';
|
$value['image_save_local'] = ($value['image_save_local'] ?? null) ?: 'open';
|
||||||
$value['task_user_limit'] = min(2000, max(1, intval($value['task_user_limit']) ?: 500));
|
$value['task_user_limit'] = min(2000, max(1, intval($value['task_user_limit'] ?? 0) ?: 500));
|
||||||
if (!is_array($value['task_default_time']) || count($value['task_default_time']) != 2 || !Timer::isTime($value['task_default_time'][0]) || !Timer::isTime($value['task_default_time'][1])) {
|
if (!is_array($value['task_default_time'] ?? null) || count($value['task_default_time']) != 2 || !Timer::isTime($value['task_default_time'][0]) || !Timer::isTime($value['task_default_time'][1])) {
|
||||||
$value['task_default_time'] = ['09:00', '18:00'];
|
$value['task_default_time'] = ['09:00', '18:00'];
|
||||||
}
|
}
|
||||||
// 项目创建权限:范围(all/departmentOwner/appoint,默认 all)+ 指定人员
|
// 项目创建权限:范围(all/departmentOwner/appoint,默认 all)+ 指定人员
|
||||||
@ -71,8 +71,8 @@ class Setting extends AbstractModel
|
|||||||
|
|
||||||
// 文件设置
|
// 文件设置
|
||||||
case 'fileSetting':
|
case 'fileSetting':
|
||||||
$value['permission_pack_type'] = $value['permission_pack_type'] ?: 'all';
|
$value['permission_pack_type'] = ($value['permission_pack_type'] ?? null) ?: 'all';
|
||||||
$value['permission_pack_userids'] = is_array($value['permission_pack_userids']) ? $value['permission_pack_userids'] : [];
|
$value['permission_pack_userids'] = is_array($value['permission_pack_userids'] ?? null) ? $value['permission_pack_userids'] : [];
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// AI 机器人设置
|
// AI 机器人设置
|
||||||
@ -81,7 +81,7 @@ class Setting extends AbstractModel
|
|||||||
$value['claude_key'] = $value['claude_token'];
|
$value['claude_key'] = $value['claude_token'];
|
||||||
}
|
}
|
||||||
$array = [];
|
$array = [];
|
||||||
$aiList = ['openai', 'claude', 'deepseek', 'gemini', 'grok', 'ollama', 'zhipu', 'qianwen', 'wenxin'];
|
$aiList = ['openai', 'claude', 'deepseek', 'gemini', 'grok', 'ollama', 'zhipu', 'qianwen', 'wenxin', 'dooai'];
|
||||||
$fieldList = ['key', 'secret', 'models', 'model', 'base_url', 'agency', 'temperature', 'system'];
|
$fieldList = ['key', 'secret', 'models', 'model', 'base_url', 'agency', 'temperature', 'system'];
|
||||||
foreach ($aiList as $aiName) {
|
foreach ($aiList as $aiName) {
|
||||||
foreach ($fieldList as $fieldName) {
|
foreach ($fieldList as $fieldName) {
|
||||||
@ -89,11 +89,13 @@ class Setting extends AbstractModel
|
|||||||
$content = !empty($value[$key]) ? trim($value[$key]) : '';
|
$content = !empty($value[$key]) ? trim($value[$key]) : '';
|
||||||
switch ($fieldName) {
|
switch ($fieldName) {
|
||||||
case 'models':
|
case 'models':
|
||||||
if ($content) {
|
// 新 JSON 数组格式原样保留;仅旧的换行格式按行清洗
|
||||||
|
if ($content && !str_starts_with($content, '[')) {
|
||||||
$content = explode("\n", $content);
|
$content = explode("\n", $content);
|
||||||
$content = array_filter($content);
|
$content = array_filter($content);
|
||||||
|
$content = implode("\n", $content);
|
||||||
}
|
}
|
||||||
$content = is_array($content) ? implode("\n", $content) : '';
|
$content = is_string($content) ? $content : '';
|
||||||
break;
|
break;
|
||||||
case 'model':
|
case 'model':
|
||||||
$models = Setting::AIBotModels2Array($array[$key . 's'], true);
|
$models = Setting::AIBotModels2Array($array[$key . 's'], true);
|
||||||
@ -219,15 +221,49 @@ class Setting extends AbstractModel
|
|||||||
*/
|
*/
|
||||||
public static function AIBotModels2Array($models, $retValue = false)
|
public static function AIBotModels2Array($models, $retValue = false)
|
||||||
{
|
{
|
||||||
$list = is_array($models) ? $models : explode("\n", $models);
|
$list = null;
|
||||||
|
if (is_array($models)) {
|
||||||
|
$list = $models;
|
||||||
|
} else {
|
||||||
|
$text = trim((string)$models);
|
||||||
|
if ($text !== '' && str_starts_with($text, '[')) {
|
||||||
|
$decoded = json_decode($text, true);
|
||||||
|
if (is_array($decoded)) {
|
||||||
|
$list = $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($list === null) {
|
||||||
|
$list = explode("\n", (string)$models);
|
||||||
|
}
|
||||||
|
}
|
||||||
$array = [];
|
$array = [];
|
||||||
foreach ($list as $item) {
|
foreach ($list as $item) {
|
||||||
$arr = Base::newTrim(explode('|', $item . '|'));
|
if (is_array($item)) {
|
||||||
if ($arr[0]) {
|
// 新 JSON 记录格式:{id,name,thinking}(兼容 {value,label})
|
||||||
|
$value = trim((string)($item['id'] ?? $item['value'] ?? ''));
|
||||||
|
if ($value === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$label = trim((string)($item['name'] ?? $item['label'] ?? ''));
|
||||||
|
$thinking = strtolower(trim((string)($item['thinking'] ?? 'off')));
|
||||||
|
if (!in_array($thinking, ['off', 'low', 'medium', 'high'], true)) {
|
||||||
|
$thinking = 'off';
|
||||||
|
}
|
||||||
$array[] = [
|
$array[] = [
|
||||||
'value' => $arr[0],
|
'value' => $value,
|
||||||
'label' => $arr[1] ?: $arr[0]
|
'label' => $label !== '' ? $label : $value,
|
||||||
|
'thinking' => $thinking,
|
||||||
];
|
];
|
||||||
|
} else {
|
||||||
|
// 兼容旧字符串格式 "id|name"
|
||||||
|
$arr = Base::newTrim(explode('|', $item . '|'));
|
||||||
|
if ($arr[0]) {
|
||||||
|
$array[] = [
|
||||||
|
'value' => $arr[0],
|
||||||
|
'label' => $arr[1] ?: $arr[0],
|
||||||
|
'thinking' => 'off',
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($retValue) {
|
if ($retValue) {
|
||||||
@ -236,6 +272,26 @@ class Setting extends AbstractModel
|
|||||||
return $array;
|
return $array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定模型的思考档位(off|low|medium|high),未配置返回 off
|
||||||
|
* @param string|array $models 模型列表设置(JSON 字符串或旧格式)
|
||||||
|
* @param string $modelName 模型 ID
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function AIBotModelThinking($models, $modelName)
|
||||||
|
{
|
||||||
|
$modelName = trim((string)$modelName);
|
||||||
|
if ($modelName === '') {
|
||||||
|
return 'off';
|
||||||
|
}
|
||||||
|
foreach (self::AIBotModels2Array($models) as $item) {
|
||||||
|
if ($item['value'] === $modelName) {
|
||||||
|
return $item['thinking'] ?? 'off';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'off';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 规范自定义微应用配置
|
* 规范自定义微应用配置
|
||||||
* @param array $list
|
* @param array $list
|
||||||
|
|||||||
@ -305,12 +305,12 @@ class User extends AbstractModel
|
|||||||
if ($onlyUserid && $onlyUserid != $this->userid) {
|
if ($onlyUserid && $onlyUserid != $this->userid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (env("PASSWORD_ADMIN") == 'disabled') {
|
if (config('dootask.password_admin') == 'disabled') {
|
||||||
if ($this->userid == 1) {
|
if ($this->userid == 1) {
|
||||||
throw new ApiException('当前环境禁止此操作');
|
throw new ApiException('当前环境禁止此操作');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (env("PASSWORD_OWNER") == 'disabled') {
|
if (config('dootask.password_owner') == 'disabled') {
|
||||||
throw new ApiException('当前环境禁止此操作');
|
throw new ApiException('当前环境禁止此操作');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -961,6 +961,8 @@ class User extends AbstractModel
|
|||||||
return url("images/avatar/default_ollama.png");
|
return url("images/avatar/default_ollama.png");
|
||||||
case 'ai-zhipu@bot.system':
|
case 'ai-zhipu@bot.system':
|
||||||
return url("images/avatar/default_zhipu.png");
|
return url("images/avatar/default_zhipu.png");
|
||||||
|
case 'ai-dooai@bot.system':
|
||||||
|
return url("images/avatar/default_dooai.png");
|
||||||
case 'bot-manager@bot.system':
|
case 'bot-manager@bot.system':
|
||||||
return url("images/avatar/default_bot.png");
|
return url("images/avatar/default_bot.png");
|
||||||
case 'meeting-alert@bot.system':
|
case 'meeting-alert@bot.system':
|
||||||
|
|||||||
@ -164,6 +164,7 @@ class UserBot extends AbstractModel
|
|||||||
'ai-zhipu' => '智谱清言',
|
'ai-zhipu' => '智谱清言',
|
||||||
'ai-qianwen' => '通义千问',
|
'ai-qianwen' => '通义千问',
|
||||||
'ai-wenxin' => '文心一言',
|
'ai-wenxin' => '文心一言',
|
||||||
|
'ai-dooai' => 'Doo AI',
|
||||||
'bot-manager' => '机器人管理',
|
'bot-manager' => '机器人管理',
|
||||||
'meeting-alert' => '会议通知',
|
'meeting-alert' => '会议通知',
|
||||||
'okr-alert' => 'OKR提醒',
|
'okr-alert' => 'OKR提醒',
|
||||||
|
|||||||
@ -33,6 +33,7 @@ use Request;
|
|||||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereOwnerUserid($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereOwnerUserid($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereParentId($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereParentId($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereUpdatedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereUpdatedAt($value)
|
||||||
|
* @property-read array $deputy_userids
|
||||||
* @mixin \Eloquent
|
* @mixin \Eloquent
|
||||||
*/
|
*/
|
||||||
class UserDepartment extends AbstractModel
|
class UserDepartment extends AbstractModel
|
||||||
|
|||||||
@ -7,8 +7,9 @@ use App\Module\Base;
|
|||||||
use App\Module\Doo;
|
use App\Module\Doo;
|
||||||
use App\Module\Timer;
|
use App\Module\Timer;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Guanguans\Notify\Factory;
|
use Symfony\Component\Mailer\Mailer;
|
||||||
use Guanguans\Notify\Messages\EmailMessage;
|
use Symfony\Component\Mailer\Transport;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App\Models\UserEmailVerification
|
* App\Models\UserEmailVerification
|
||||||
@ -97,16 +98,14 @@ class UserEmailVerification extends AbstractModel
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Factory::mailer()
|
$mailer = new Mailer(Transport::fromDsn("smtp://{$setting['account']}:{$setting['password']}@{$setting['smtp_server']}:{$setting['port']}?verify_peer=0"));
|
||||||
->setDsn("smtp://{$setting['account']}:{$setting['password']}@{$setting['smtp_server']}:{$setting['port']}?verify_peer=0")
|
$mailer->send((new Email())
|
||||||
->setMessage(EmailMessage::create()
|
->from($alias . " <{$setting['account']}>")
|
||||||
->from($alias . " <{$setting['account']}>")
|
->to($email)
|
||||||
->to($email)
|
->subject($subject)
|
||||||
->subject($subject)
|
->html($content));
|
||||||
->html($content))
|
|
||||||
->send();
|
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
if (str_contains($e->getMessage(), "Timed Out")) {
|
if (stripos($e->getMessage(), "timed out") !== false) {
|
||||||
throw new ApiException("邮件发送超时,请检查邮箱配置是否正确");
|
throw new ApiException("邮件发送超时,请检查邮箱配置是否正确");
|
||||||
} elseif ($e->getCode() === 550) {
|
} elseif ($e->getCode() === 550) {
|
||||||
throw new ApiException('邮件内容被拒绝,请检查邮箱是否开启接收功能');
|
throw new ApiException('邮件内容被拒绝,请检查邮箱是否开启接收功能');
|
||||||
|
|||||||
@ -57,8 +57,8 @@ class UserRecentItem extends AbstractModel
|
|||||||
'browsed_at',
|
'browsed_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $dates = [
|
protected $casts = [
|
||||||
'browsed_at',
|
'browsed_at' => 'datetime',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static function record(int $userid, string $targetType, int $targetId, string $sourceType = '', int $sourceId = 0): self
|
public static function record(int $userid, string $targetType, int $targetId, string $sourceType = '', int $sourceId = 0): self
|
||||||
|
|||||||
@ -40,8 +40,8 @@ class UserTaskBrowse extends AbstractModel
|
|||||||
'browsed_at',
|
'browsed_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $dates = [
|
protected $casts = [
|
||||||
'browsed_at',
|
'browsed_at' => 'datetime',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -5,8 +5,6 @@ namespace App\Models;
|
|||||||
use App\Exceptions\ApiException;
|
use App\Exceptions\ApiException;
|
||||||
use App\Module\Base;
|
use App\Module\Base;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Guanguans\Notify\Factory;
|
|
||||||
use Guanguans\Notify\Messages\EmailMessage;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App\Models\UserTransfer
|
* App\Models\UserTransfer
|
||||||
|
|||||||
@ -56,6 +56,7 @@ use Illuminate\Support\Facades\DB;
|
|||||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereUpdatedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereUpdatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog withTrashed()
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog withTrashed()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog withoutTrashed()
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog withoutTrashed()
|
||||||
|
* @property-read array $deputy_ids
|
||||||
* @mixin \Eloquent
|
* @mixin \Eloquent
|
||||||
*/
|
*/
|
||||||
class WebSocketDialog extends AbstractModel
|
class WebSocketDialog extends AbstractModel
|
||||||
|
|||||||
@ -27,6 +27,10 @@ use Carbon\Carbon;
|
|||||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo whereId($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo whereId($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo whereMsgId($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo whereMsgId($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo whereUserid($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo whereUserid($value)
|
||||||
|
* @property \Illuminate\Support\Carbon|null $remind_at 提醒时间
|
||||||
|
* @property \Illuminate\Support\Carbon|null $reminded_at 已提醒时间
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|WebSocketDialogMsgTodo whereRemindAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|WebSocketDialogMsgTodo whereRemindedAt($value)
|
||||||
* @mixin \Eloquent
|
* @mixin \Eloquent
|
||||||
*/
|
*/
|
||||||
class WebSocketDialogMsgTodo extends AbstractModel
|
class WebSocketDialogMsgTodo extends AbstractModel
|
||||||
|
|||||||
@ -43,6 +43,8 @@ namespace App\Models;
|
|||||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereTopAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereTopAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereUpdatedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereUpdatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereUserid($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereUserid($value)
|
||||||
|
* @property int $role 0=普通成员 1=群主 2=群管理员
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|WebSocketDialogUser whereRole($value)
|
||||||
* @mixin \Eloquent
|
* @mixin \Eloquent
|
||||||
*/
|
*/
|
||||||
class WebSocketDialogUser extends AbstractModel
|
class WebSocketDialogUser extends AbstractModel
|
||||||
|
|||||||
@ -21,7 +21,8 @@ class AI
|
|||||||
'ollama',
|
'ollama',
|
||||||
'zhipu',
|
'zhipu',
|
||||||
'qianwen',
|
'qianwen',
|
||||||
'wenxin'
|
'wenxin',
|
||||||
|
'dooai'
|
||||||
];
|
];
|
||||||
protected const OPENAI_DEFAULT_MODEL = 'gpt-5.1-mini';
|
protected const OPENAI_DEFAULT_MODEL = 'gpt-5.1-mini';
|
||||||
|
|
||||||
@ -140,7 +141,31 @@ class AI
|
|||||||
* @param mixed $contextInput
|
* @param mixed $contextInput
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public static function createStreamKey($modelType, $modelName, $contextInput = [])
|
/**
|
||||||
|
* 判定当前用户是否启用 ai-kb RAG(灰度判定)
|
||||||
|
*
|
||||||
|
* 规则(参考 config/ai.php):
|
||||||
|
* - 总开关 rag_enabled=false → 关闭所有(kill switch)
|
||||||
|
* - rag_canary_userids 为空 → 全员启用
|
||||||
|
* - 否则仅白名单 userid 启用
|
||||||
|
*/
|
||||||
|
public static function ragEnabledFor(int $userid): bool
|
||||||
|
{
|
||||||
|
if (!config('ai.rag_enabled', true)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$raw = trim((string) config('ai.rag_canary_userids', ''));
|
||||||
|
if ($raw === '') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
$allow = array_filter(array_map(
|
||||||
|
fn($v) => (int) trim($v),
|
||||||
|
explode(',', $raw)
|
||||||
|
), fn($v) => $v > 0);
|
||||||
|
return in_array($userid, $allow, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function createStreamKey($modelType, $modelName, $contextInput = [], $locale = 'zh', $ragEnabled = true, $contextKey = '', $fd = 0)
|
||||||
{
|
{
|
||||||
$modelType = trim((string)$modelType);
|
$modelType = trim((string)$modelType);
|
||||||
$modelName = trim((string)$modelName);
|
$modelName = trim((string)$modelName);
|
||||||
@ -221,6 +246,14 @@ class AI
|
|||||||
'model_type' => $remoteModelType,
|
'model_type' => $remoteModelType,
|
||||||
'model_name' => $modelName,
|
'model_name' => $modelName,
|
||||||
'context' => $contextJson,
|
'context' => $contextJson,
|
||||||
|
'locale' => $locale,
|
||||||
|
// ai-kb 灰度透传:1 启用 RAG(hint + search_help_docs tool),0 关闭
|
||||||
|
'rag_enabled' => $ragEnabled ? '1' : '0',
|
||||||
|
// 前端会话ID,AI 服务存为 context_key 用于检索打点关联
|
||||||
|
'context_key' => mb_substr(trim((string)$contextKey), 0, 100),
|
||||||
|
// AI 助手路径启用 doo 执行工具;fd 为用户当前 WebSocket 连接(页面操作用,0 表示无)
|
||||||
|
'doo_enabled' => '1',
|
||||||
|
'fd' => intval($fd),
|
||||||
];
|
];
|
||||||
|
|
||||||
$baseUrl = trim((string)($setting[$modelType . '_base_url'] ?? ''));
|
$baseUrl = trim((string)($setting[$modelType . '_base_url'] ?? ''));
|
||||||
|
|||||||
@ -72,7 +72,7 @@ class Apps
|
|||||||
*/
|
*/
|
||||||
public static function dispatchUserHook(User $user, string $action, string $eventType = '', array $changedFields = []): void
|
public static function dispatchUserHook(User $user, string $action, string $eventType = '', array $changedFields = []): void
|
||||||
{
|
{
|
||||||
$appKey = env('APP_KEY', '');
|
$appKey = config('app.key') ?: '';
|
||||||
if (empty($appKey)) {
|
if (empty($appKey)) {
|
||||||
info('[appstore_hook] APP_KEY is empty, skip dispatchUserHook');
|
info('[appstore_hook] APP_KEY is empty, skip dispatchUserHook');
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -848,6 +848,13 @@ class Base
|
|||||||
*/
|
*/
|
||||||
public static function getSchemeAndHost()
|
public static function getSchemeAndHost()
|
||||||
{
|
{
|
||||||
|
// 优先用当前请求的协议+主机:getScheme() 会经 TrustProxies 采信 X-Forwarded-Proto,
|
||||||
|
// 从而正确识别 https;host 取自 Host 头(不信 X-Forwarded-Host,避免 Host 注入)
|
||||||
|
$request = request();
|
||||||
|
if ($request instanceof \Illuminate\Http\Request && $request->getHttpHost()) {
|
||||||
|
return $request->getSchemeAndHttpHost();
|
||||||
|
}
|
||||||
|
// 非请求上下文(Task/命令行等)的兜底
|
||||||
$scheme = isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == '443' ? 'https://' : 'http://';
|
$scheme = isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == '443' ? 'https://' : 'http://';
|
||||||
return $scheme.($_SERVER['HTTP_HOST'] ?? '');
|
return $scheme.($_SERVER['HTTP_HOST'] ?? '');
|
||||||
}
|
}
|
||||||
@ -2582,6 +2589,23 @@ class Base
|
|||||||
return $array;
|
return $array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 zip 压缩包并添加文件,条目名取文件 basename(等价旧 Madzipper::make()->add()->close())
|
||||||
|
* @param string $zipPath 压缩包路径
|
||||||
|
* @param string|array $files 要添加的文件路径
|
||||||
|
*/
|
||||||
|
public static function zipAddFiles($zipPath, $files)
|
||||||
|
{
|
||||||
|
$zip = new \ZipArchive();
|
||||||
|
if ($zip->open($zipPath, \ZipArchive::CREATE) !== true) {
|
||||||
|
throw new \RuntimeException("Unable to open zip file: " . $zipPath);
|
||||||
|
}
|
||||||
|
foreach ((array)$files as $file) {
|
||||||
|
$zip->addFile($file, basename($file));
|
||||||
|
}
|
||||||
|
$zip->close();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取中文字符拼音首字母
|
* 获取中文字符拼音首字母
|
||||||
* @param $str
|
* @param $str
|
||||||
@ -2597,8 +2621,7 @@ class Base
|
|||||||
return '#';
|
return '#';
|
||||||
}
|
}
|
||||||
if (!preg_match("/^[a-zA-Z]$/", $first)) {
|
if (!preg_match("/^[a-zA-Z]$/", $first)) {
|
||||||
$pinyin = new Pinyin();
|
$first = Pinyin::abbr($first, true)->join('');
|
||||||
$first = $pinyin->abbr($first, '', PINYIN_NAME);
|
|
||||||
}
|
}
|
||||||
return $first ? strtoupper($first) : '#';
|
return $first ? strtoupper($first) : '#';
|
||||||
}
|
}
|
||||||
@ -2616,8 +2639,7 @@ class Base
|
|||||||
}
|
}
|
||||||
if (!preg_match("/^[a-zA-Z0-9_.]+$/", $str)) {
|
if (!preg_match("/^[a-zA-Z0-9_.]+$/", $str)) {
|
||||||
$str = Cache::rememberForever("cn2pinyin:" . md5($str . '_' . $delim), function () use ($delim, $str) {
|
$str = Cache::rememberForever("cn2pinyin:" . md5($str . '_' . $delim), function () use ($delim, $str) {
|
||||||
$pinyin = new Pinyin();
|
return Pinyin::permalink($str, $delim);
|
||||||
return $pinyin->permalink($str, $delim);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return $str;
|
return $str;
|
||||||
|
|||||||
@ -53,7 +53,7 @@ class Doo
|
|||||||
*/
|
*/
|
||||||
public static function licenseContent(): string
|
public static function licenseContent(): string
|
||||||
{
|
{
|
||||||
if (env("SYSTEM_LICENSE") == 'hidden') {
|
if (config('dootask.system_license') == 'hidden') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
$paths = [
|
$paths = [
|
||||||
|
|||||||
@ -14,6 +14,8 @@ class Ihttp
|
|||||||
}
|
}
|
||||||
if(!empty($urlset['query'])) {
|
if(!empty($urlset['query'])) {
|
||||||
$urlset['query'] = "?{$urlset['query']}";
|
$urlset['query'] = "?{$urlset['query']}";
|
||||||
|
} else {
|
||||||
|
$urlset['query'] = '';
|
||||||
}
|
}
|
||||||
if(empty($urlset['port'])) {
|
if(empty($urlset['port'])) {
|
||||||
$urlset['port'] = $urlset['scheme'] == 'https' ? '443' : '80';
|
$urlset['port'] = $urlset['scheme'] == 'https' ? '443' : '80';
|
||||||
|
|||||||
@ -29,8 +29,8 @@ class ManticoreBase
|
|||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->host = env('SEARCH_HOST', 'search');
|
$this->host = config('dootask.search_host');
|
||||||
$this->port = (int) env('SEARCH_PORT', 9306);
|
$this->port = (int) config('dootask.search_port');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
454
app/Module/OnlineLicense.php
Normal file
454
app/Module/OnlineLicense.php
Normal file
@ -0,0 +1,454 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Module;
|
||||||
|
|
||||||
|
use App\Exceptions\ApiException;
|
||||||
|
use App\Services\RequestContext;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Facades\Crypt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线授权客户端编排。
|
||||||
|
*
|
||||||
|
* 在线授权产出的仍是现有格式的离线 license blob,只是「获取方式」变成用 appstore 账号登录
|
||||||
|
* 自助签发、并由本类定时续期。doo.so 本地校验、license 文件存储全部复用。绑定状态以单例
|
||||||
|
* 形式存于 settings 表(name=onlineLicense),instance_token 用 Crypt 加密。
|
||||||
|
*
|
||||||
|
* 四级状态机(基于租约内嵌到期 lease_expired_at 与本地宽限,全程不依赖 appstore 可达):
|
||||||
|
* active 续期正常
|
||||||
|
* reminder 续期失败/租约剩余不足 warn_days(仅管理员可见提醒)
|
||||||
|
* frozen 租约已过期(doo.so 既有行为:限制新增用户)
|
||||||
|
* revoked 冻结超过 grace_days 或 appstore 明确吊销 → 回落默认 3 人版
|
||||||
|
*/
|
||||||
|
class OnlineLicense
|
||||||
|
{
|
||||||
|
const KEY = 'onlineLicense';
|
||||||
|
|
||||||
|
// ---- 配置 ----
|
||||||
|
|
||||||
|
protected static function appstoreUrl(): string
|
||||||
|
{
|
||||||
|
return rtrim((string)config('dootask.online_license_appstore_url'), '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function renewWithinDays(): int
|
||||||
|
{
|
||||||
|
return (int)config('dootask.online_license_renew_within_days', 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function warnDays(): int
|
||||||
|
{
|
||||||
|
return (int)config('dootask.online_license_warn_days', 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function graceDays(): int
|
||||||
|
{
|
||||||
|
return (int)config('dootask.online_license_grace_days', 14);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 状态读写(单例 settings)----
|
||||||
|
|
||||||
|
public static function get(): array
|
||||||
|
{
|
||||||
|
return Base::setting(self::KEY) ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function set(array $patch): array
|
||||||
|
{
|
||||||
|
$next = array_merge(self::get(), $patch);
|
||||||
|
Base::setting(self::KEY, $next);
|
||||||
|
return $next;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function enabled(): bool
|
||||||
|
{
|
||||||
|
$s = self::get();
|
||||||
|
return !empty($s['enabled']) && ($s['mode'] ?? '') === 'online';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function token(): string
|
||||||
|
{
|
||||||
|
$enc = self::get()['instance_token'] ?? '';
|
||||||
|
if (empty($enc)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Crypt::decryptString($enc);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前请求语言,透传给 appstore 用于邮件按语言渲染(中文/繁体→中文,其余→英文)。
|
||||||
|
* 非请求上下文(如定时续期)返回空串,由 appstore 回落默认语言。
|
||||||
|
*/
|
||||||
|
protected static function lang(): string
|
||||||
|
{
|
||||||
|
return (string)Base::headerOrInput('language');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function fingerprint(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'sn' => Doo::dooSN(),
|
||||||
|
'macs' => implode(',', Doo::macs()),
|
||||||
|
// 优先真实外网地址:config('app.url') 若为 localhost 由 replaceBaseUrl 替换为缓存的访问地址
|
||||||
|
'url' => RequestContext::replaceBaseUrl((string)config('app.url')),
|
||||||
|
// DooTask 应用版本(非 doo.so 库版本)
|
||||||
|
'version' => Base::getVersion(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- appstore 调用 ----
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调 appstore license 接口。返回 ['ok'=>bool, 'data'=>array, 'message'=>string]。
|
||||||
|
* $bearer 非空时带实例令牌(续期/释放)。
|
||||||
|
*/
|
||||||
|
protected static function call(string $path, array $payload, string $bearer = ''): array
|
||||||
|
{
|
||||||
|
$url = self::appstoreUrl() . '/api/v1/license/' . ltrim($path, '/');
|
||||||
|
$headers = ['Content-Type' => 'application/json'];
|
||||||
|
if ($bearer !== '') {
|
||||||
|
$headers['Authorization'] = 'Bearer ' . $bearer;
|
||||||
|
}
|
||||||
|
$resp = Ihttp::ihttp_request($url, json_encode($payload, JSON_UNESCAPED_UNICODE), $headers, 15);
|
||||||
|
if (Base::isError($resp)) {
|
||||||
|
return ['ok' => false, 'data' => [], 'message' => $resp['msg'] ?: '无法连接授权服务'];
|
||||||
|
}
|
||||||
|
$body = Base::json2array($resp['data'] ?? '');
|
||||||
|
if (($body['code'] ?? 0) !== 200) {
|
||||||
|
return ['ok' => false, 'data' => [], 'message' => $body['message'] ?: '授权服务返回错误'];
|
||||||
|
}
|
||||||
|
return ['ok' => true, 'data' => $body['data'] ?? [], 'message' => ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理签发结果:issued/renewed 则落地 license + 更新绑定状态;其它状态原样返回供上层决策。
|
||||||
|
*/
|
||||||
|
protected static function applyIssue(string $account, array $d): string
|
||||||
|
{
|
||||||
|
$status = $d['status'] ?? '';
|
||||||
|
if (in_array($status, ['issued', 'renewed'], true)) {
|
||||||
|
$blob = $d['license'] ?? '';
|
||||||
|
if ($blob === '') {
|
||||||
|
throw new ApiException('授权服务未返回 license');
|
||||||
|
}
|
||||||
|
Doo::licenseSave($blob); // 复用离线落地与 doo.so 校验
|
||||||
|
$snap = $d['snapshot'] ?? [];
|
||||||
|
$patch = [
|
||||||
|
'enabled' => true,
|
||||||
|
'mode' => 'online',
|
||||||
|
'account' => $account,
|
||||||
|
'plan' => $snap['plan'] ?? '',
|
||||||
|
'people' => $snap['people'] ?? 0,
|
||||||
|
'valid_until' => $snap['valid_until'] ?? null,
|
||||||
|
'lease_expired_at' => $snap['lease_expired_at'] ?? null,
|
||||||
|
'server_status' => $status,
|
||||||
|
'error_count' => 0,
|
||||||
|
'last_error' => '',
|
||||||
|
'frozen_since' => null,
|
||||||
|
'last_renewed_at' => Carbon::now()->toDateTimeString(),
|
||||||
|
];
|
||||||
|
if (!empty($d['instance_token'])) {
|
||||||
|
$patch['instance_token'] = Crypt::encryptString($d['instance_token']);
|
||||||
|
}
|
||||||
|
self::set($patch);
|
||||||
|
self::computeStage();
|
||||||
|
}
|
||||||
|
return $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 对外动作 ----
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送邮箱验证码(登录与试用共用),返回脱敏邮箱。
|
||||||
|
*/
|
||||||
|
public static function emailSend(string $email): string
|
||||||
|
{
|
||||||
|
$r = self::call('email/send', ['email' => $email, 'lang' => self::lang()]);
|
||||||
|
if (!$r['ok']) {
|
||||||
|
throw new ApiException($r['message']);
|
||||||
|
}
|
||||||
|
return $r['data']['email'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮箱 + 验证码登录并签发。失败抛 ApiException。
|
||||||
|
*/
|
||||||
|
public static function login(string $email, string $code): array
|
||||||
|
{
|
||||||
|
$r = self::call('login', array_merge(['email' => $email, 'code' => $code, 'lang' => self::lang()], self::fingerprint()));
|
||||||
|
if (!$r['ok']) {
|
||||||
|
throw new ApiException($r['message']);
|
||||||
|
}
|
||||||
|
$status = self::applyIssue($email, $r['data']);
|
||||||
|
if (!in_array($status, ['issued', 'renewed'], true)) {
|
||||||
|
throw new ApiException(self::statusHint($status));
|
||||||
|
}
|
||||||
|
return self::status();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮箱 + 验证码申请试用并签发。
|
||||||
|
*/
|
||||||
|
public static function trial(string $email, string $code): array
|
||||||
|
{
|
||||||
|
$payload = array_merge(['email' => $email, 'code' => $code, 'lang' => self::lang()], self::fingerprint());
|
||||||
|
$r = self::call('trial', $payload);
|
||||||
|
if (!$r['ok']) {
|
||||||
|
throw new ApiException($r['message']);
|
||||||
|
}
|
||||||
|
$status = self::applyIssue($email, $r['data']);
|
||||||
|
if (!in_array($status, ['issued', 'renewed'], true)) {
|
||||||
|
throw new ApiException(self::statusHint($status));
|
||||||
|
}
|
||||||
|
return self::status();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 续期(定时任务调用)。不抛异常:网络/服务错误只累加计数,最终由状态机本地降级。
|
||||||
|
*/
|
||||||
|
public static function renew(): void
|
||||||
|
{
|
||||||
|
if (!self::enabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$token = self::token();
|
||||||
|
if ($token === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$r = self::call('renew', self::fingerprint(), $token);
|
||||||
|
if (!$r['ok']) {
|
||||||
|
$s = self::get();
|
||||||
|
self::set([
|
||||||
|
'error_count' => (int)($s['error_count'] ?? 0) + 1,
|
||||||
|
'last_error' => $r['message'],
|
||||||
|
]);
|
||||||
|
self::computeStage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$status = $r['data']['status'] ?? '';
|
||||||
|
if (in_array($status, ['issued', 'renewed'], true)) {
|
||||||
|
self::applyIssue(self::get()['account'] ?? '', $r['data']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 服务侧明确状态(revoked/suspended/no_entitlement):不延长租约,记录后交状态机
|
||||||
|
self::set(['server_status' => $status, 'last_error' => self::statusHint($status)]);
|
||||||
|
self::computeStage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否到了该续期的时间(租约剩余不足 renew_within_days)。
|
||||||
|
*/
|
||||||
|
public static function dueForRenew(): bool
|
||||||
|
{
|
||||||
|
$lease = self::get()['lease_expired_at'] ?? null;
|
||||||
|
if (!$lease) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return Carbon::parse($lease)->lte(Carbon::now()->addDays(self::renewWithinDays()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定时续期入口:由容器内独立进程的 artisan 命令(online-license:renew)按小时调用。
|
||||||
|
* 先本地状态机推进(断网也能降级 frozen→revoked),再在租约将尽时续期。
|
||||||
|
*/
|
||||||
|
public static function cron(): void
|
||||||
|
{
|
||||||
|
if (!self::enabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::computeStage();
|
||||||
|
if (self::enabled() && self::dueForRenew()) {
|
||||||
|
self::renew();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进入授权页时的静默刷新:服务可达则按服务结果更新(成功续签 / 反映吊销冻结),
|
||||||
|
* 网络失败则什么都不做、不提示、不降级(避免一次页面刷新失败就误报)。
|
||||||
|
*/
|
||||||
|
public static function refresh(): void
|
||||||
|
{
|
||||||
|
if (!self::enabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$token = self::token();
|
||||||
|
if ($token === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$r = self::call('renew', self::fingerprint(), $token);
|
||||||
|
if (!$r['ok']) {
|
||||||
|
return; // 刷新失败:不更新、不提示
|
||||||
|
}
|
||||||
|
$status = $r['data']['status'] ?? '';
|
||||||
|
if (in_array($status, ['issued', 'renewed'], true)) {
|
||||||
|
self::applyIssue(self::get()['account'] ?? '', $r['data']);
|
||||||
|
} elseif (in_array($status, ['revoked', 'suspended', 'no_entitlement'], true)) {
|
||||||
|
// 服务侧明确结果(非网络失败):如实反映
|
||||||
|
self::set(['server_status' => $status, 'last_error' => self::statusHint($status)]);
|
||||||
|
self::computeStage();
|
||||||
|
}
|
||||||
|
} catch (\Throwable) {
|
||||||
|
// 忽略,保持现状
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出在线授权:释放座位 + 回落默认。
|
||||||
|
*/
|
||||||
|
public static function logout(): void
|
||||||
|
{
|
||||||
|
$token = self::token();
|
||||||
|
if ($token !== '') {
|
||||||
|
self::call('deactivate', [], $token);
|
||||||
|
}
|
||||||
|
self::fallbackToDefault();
|
||||||
|
Base::setting(self::KEY, ['enabled' => false, 'mode' => 'offline']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换到离线授权(互斥):保存离线 license 后调用。
|
||||||
|
* 尽力释放在线座位 + 清在线标志,但「不」删除 license 文件(刚保存的离线 license 要保留)。
|
||||||
|
*/
|
||||||
|
public static function switchToOffline(): void
|
||||||
|
{
|
||||||
|
if (!self::enabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$token = self::token();
|
||||||
|
if ($token !== '') {
|
||||||
|
self::call('deactivate', [], $token); // 尽力释放座位,失败忽略
|
||||||
|
}
|
||||||
|
Base::setting(self::KEY, ['enabled' => false, 'mode' => 'offline']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 状态机 ----
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 据租约到期 + 宽限重新计算 status,并在 revoked 时执行降级。
|
||||||
|
*/
|
||||||
|
public static function computeStage(): string
|
||||||
|
{
|
||||||
|
$s = self::get();
|
||||||
|
if (($s['mode'] ?? '') !== 'online' || empty($s['enabled'])) {
|
||||||
|
return 'offline';
|
||||||
|
}
|
||||||
|
$now = Carbon::now();
|
||||||
|
$server = $s['server_status'] ?? '';
|
||||||
|
$lease = $s['lease_expired_at'] ?? null;
|
||||||
|
|
||||||
|
if ($server === 'revoked') {
|
||||||
|
return self::transitionRevoked();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($lease && Carbon::parse($lease)->lte($now)) {
|
||||||
|
// 租约已过期 → 冻结;超过宽限 → 吊销
|
||||||
|
$frozenSince = $s['frozen_since'] ?? null;
|
||||||
|
if (!$frozenSince) {
|
||||||
|
$frozenSince = $now->toDateTimeString();
|
||||||
|
self::set(['frozen_since' => $frozenSince]);
|
||||||
|
}
|
||||||
|
if (Carbon::parse($frozenSince)->addDays(self::graceDays())->lte($now)) {
|
||||||
|
return self::transitionRevoked();
|
||||||
|
}
|
||||||
|
self::set(['status' => 'frozen']);
|
||||||
|
return 'frozen';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 租约有效
|
||||||
|
$remindByLease = $lease && Carbon::parse($lease)->lte($now->copy()->addDays(self::warnDays()));
|
||||||
|
$remindByError = (int)($s['error_count'] ?? 0) > 0 || $server === 'suspended' || $server === 'no_entitlement';
|
||||||
|
$status = ($remindByLease || $remindByError) ? 'reminder' : 'active';
|
||||||
|
self::set(['status' => $status, 'frozen_since' => null]);
|
||||||
|
return $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function transitionRevoked(): string
|
||||||
|
{
|
||||||
|
self::fallbackToDefault();
|
||||||
|
self::set(['status' => 'revoked', 'enabled' => false]);
|
||||||
|
return 'revoked';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除在线 license 文件,让 dooso 回落默认 3 人版(触发既有超员禁用)。
|
||||||
|
*/
|
||||||
|
protected static function fallbackToDefault(): void
|
||||||
|
{
|
||||||
|
foreach (['LICENSE', 'license'] as $name) {
|
||||||
|
$path = config_path($name);
|
||||||
|
if (is_file($path)) {
|
||||||
|
@unlink($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 提醒文案(注入 system/license 的 error[],复用 dashboard 警告条与 license 页)----
|
||||||
|
|
||||||
|
public static function stageMessages(): array
|
||||||
|
{
|
||||||
|
if (!self::enabled() && (self::get()['status'] ?? '') !== 'revoked') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$s = self::get();
|
||||||
|
$status = $s['status'] ?? self::computeStage();
|
||||||
|
$msgs = [];
|
||||||
|
switch ($status) {
|
||||||
|
case 'reminder':
|
||||||
|
if (($s['server_status'] ?? '') === 'suspended') {
|
||||||
|
$msgs[] = '在线授权已被冻结,请联系服务商';
|
||||||
|
} elseif ((int)($s['error_count'] ?? 0) > 0) {
|
||||||
|
$msgs[] = '在线授权续期失败,请检查网络';
|
||||||
|
} else {
|
||||||
|
$msgs[] = '在线授权即将到期,请保持联网续期';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'frozen':
|
||||||
|
$msgs[] = '在线授权已过期,新增用户受限,请尽快续期';
|
||||||
|
break;
|
||||||
|
case 'revoked':
|
||||||
|
$msgs[] = '在线授权已失效,已回落到基础版';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return $msgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function statusHint(string $status): string
|
||||||
|
{
|
||||||
|
return match ($status) {
|
||||||
|
'no_entitlement' => '该账号暂无可用授权,请先申请试用或购买',
|
||||||
|
'revoked' => '该授权已被吊销',
|
||||||
|
'suspended' => '该授权已被冻结',
|
||||||
|
'seat_taken' => '该授权已在另一台实例使用,请先在原实例释放(换机)',
|
||||||
|
'entitlement_expired' => '该授权已到期',
|
||||||
|
default => '签发失败(' . $status . ')',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对外状态(前端在线 Tab / status 接口用,不含敏感 token)。
|
||||||
|
*/
|
||||||
|
public static function status(): array
|
||||||
|
{
|
||||||
|
$s = self::get();
|
||||||
|
if (($s['mode'] ?? '') !== 'online' || empty($s['enabled'])) {
|
||||||
|
return ['mode' => 'offline'];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'mode' => 'online',
|
||||||
|
'account' => $s['account'] ?? '',
|
||||||
|
'plan' => $s['plan'] ?? '',
|
||||||
|
'people' => $s['people'] ?? 0,
|
||||||
|
'valid_until' => $s['valid_until'] ?? null,
|
||||||
|
'lease_expired_at' => $s['lease_expired_at'] ?? null,
|
||||||
|
'last_renewed_at' => $s['last_renewed_at'] ?? null,
|
||||||
|
'status' => $s['status'] ?? self::computeStage(),
|
||||||
|
'error_count' => (int)($s['error_count'] ?? 0),
|
||||||
|
'last_error' => $s['last_error'] ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
48
app/Module/PatchedAvatar.php
Normal file
48
app/Module/PatchedAvatar.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Module;
|
||||||
|
|
||||||
|
use Intervention\Image\Drivers\Gd\Driver as GdDriver;
|
||||||
|
use Intervention\Image\Drivers\Imagick\Driver as ImagickDriver;
|
||||||
|
use Intervention\Image\ImageManager;
|
||||||
|
use Intervention\Image\Typography\FontFactory;
|
||||||
|
use Laravolt\Avatar\Avatar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* laravolt/avatar 6.5.0 的 buildAvatar 给纵向对齐传 'middle',
|
||||||
|
* 而 intervention/image 4.1.3 的 Alignment 枚举仅接受 'center',会抛
|
||||||
|
* InvalidArgumentException(Invalid value for alignment)。上游修复前以子类覆写修正。
|
||||||
|
*/
|
||||||
|
class PatchedAvatar extends Avatar
|
||||||
|
{
|
||||||
|
public function buildAvatar(): static
|
||||||
|
{
|
||||||
|
$this->buildInitial();
|
||||||
|
|
||||||
|
$x = $this->width / 2;
|
||||||
|
$y = $this->height / 2;
|
||||||
|
|
||||||
|
$driver = $this->driver === 'gd' ? new GdDriver : new ImagickDriver;
|
||||||
|
$this->image = ImageManager::usingDriver($driver)->createImage($this->width, $this->height);
|
||||||
|
|
||||||
|
$this->createShape();
|
||||||
|
|
||||||
|
if (empty($this->initials)) {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->image->text(
|
||||||
|
$this->initials,
|
||||||
|
(int) $x,
|
||||||
|
(int) $y,
|
||||||
|
function (FontFactory $font) {
|
||||||
|
$font->filepath($this->font);
|
||||||
|
$font->size($this->fontSize);
|
||||||
|
$font->color($this->foreground);
|
||||||
|
$font->align('center', 'center');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -24,7 +24,8 @@ abstract class AbstractData
|
|||||||
|
|
||||||
protected function __construct()
|
protected function __construct()
|
||||||
{
|
{
|
||||||
$this->table = app('swoole')->{$this->getTableName()};
|
// 非 Swoole 运行时(artisan/测试)无 swoole 绑定,table 为 null,各方法返回默认值
|
||||||
|
$this->table = app()->bound('swoole') ? app('swoole')->{$this->getTableName()} : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getTable()
|
public function getTable()
|
||||||
@ -42,22 +43,34 @@ abstract class AbstractData
|
|||||||
|
|
||||||
public static function set($key, $value)
|
public static function set($key, $value)
|
||||||
{
|
{
|
||||||
|
if (!self::instance()->table) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return self::instance()->table->set($key, ['value' => $value]);
|
return self::instance()->table->set($key, ['value' => $value]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function get($key, $default = null)
|
public static function get($key, $default = null)
|
||||||
{
|
{
|
||||||
|
if (!self::instance()->table) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
$data = self::instance()->table->get($key);
|
$data = self::instance()->table->get($key);
|
||||||
return $data ? $data['value'] : $default;
|
return $data ? $data['value'] : $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function del($key)
|
public static function del($key)
|
||||||
{
|
{
|
||||||
|
if (!self::instance()->table) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return self::instance()->table->del($key);
|
return self::instance()->table->del($key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function exist($key)
|
public static function exist($key)
|
||||||
{
|
{
|
||||||
|
if (!self::instance()->table) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return self::instance()->table->exist($key);
|
return self::instance()->table->exist($key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,6 +83,9 @@ abstract class AbstractData
|
|||||||
|
|
||||||
public static function clear()
|
public static function clear()
|
||||||
{
|
{
|
||||||
|
if (!self::instance()->table) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
foreach (self::instance()->table as $key => $row) {
|
foreach (self::instance()->table as $key => $row) {
|
||||||
self::del($key);
|
self::del($key);
|
||||||
}
|
}
|
||||||
@ -77,6 +93,9 @@ abstract class AbstractData
|
|||||||
|
|
||||||
public static function getAll()
|
public static function getAll()
|
||||||
{
|
{
|
||||||
|
if (!self::instance()->table) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
$result = [];
|
$result = [];
|
||||||
foreach (self::instance()->table as $key => $row) {
|
foreach (self::instance()->table as $key => $row) {
|
||||||
$result[$key] = $row['value'];
|
$result[$key] = $row['value'];
|
||||||
|
|||||||
@ -17,6 +17,9 @@ class OnlineData extends AbstractData
|
|||||||
*/
|
*/
|
||||||
public static function online($userid)
|
public static function online($userid)
|
||||||
{
|
{
|
||||||
|
if (!self::instance()->getTable()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
$key = "online::" . $userid;
|
$key = "online::" . $userid;
|
||||||
$value = self::instance()->getTable()->incr($key, 'value');
|
$value = self::instance()->getTable()->incr($key, 'value');
|
||||||
if ($value === 1) {
|
if ($value === 1) {
|
||||||
@ -35,6 +38,9 @@ class OnlineData extends AbstractData
|
|||||||
*/
|
*/
|
||||||
public static function offline($userid)
|
public static function offline($userid)
|
||||||
{
|
{
|
||||||
|
if (!self::instance()->getTable()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
$key = "online::" . $userid;
|
$key = "online::" . $userid;
|
||||||
$value = self::instance()->getTable()->decr($key, 'value');
|
$value = self::instance()->getTable()->decr($key, 'value');
|
||||||
if ($value === 0) {
|
if ($value === 0) {
|
||||||
@ -57,6 +63,9 @@ class OnlineData extends AbstractData
|
|||||||
*/
|
*/
|
||||||
public static function live($userid)
|
public static function live($userid)
|
||||||
{
|
{
|
||||||
|
if (!self::instance()->getTable()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
$key = "online::" . $userid;
|
$key = "online::" . $userid;
|
||||||
return intval(self::instance()->getTable()->get($key));
|
return intval(self::instance()->getTable()->get($key));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,40 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Models\File;
|
||||||
|
use App\Models\FileUser;
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Models\ProjectTask;
|
||||||
|
use App\Models\ProjectTaskContent;
|
||||||
|
use App\Models\ProjectTaskUser;
|
||||||
|
use App\Models\ProjectTaskVisibilityUser;
|
||||||
|
use App\Models\ProjectUser;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\UserTag;
|
||||||
|
use App\Models\UserTagRecognition;
|
||||||
|
use App\Models\WebSocketDialog;
|
||||||
|
use App\Models\WebSocketDialogMsg;
|
||||||
|
use App\Models\WebSocketDialogUser;
|
||||||
|
use App\Observers\FileObserver;
|
||||||
|
use App\Observers\FileUserObserver;
|
||||||
|
use App\Observers\ProjectObserver;
|
||||||
|
use App\Observers\ProjectTaskContentObserver;
|
||||||
|
use App\Observers\ProjectTaskObserver;
|
||||||
|
use App\Observers\ProjectTaskUserObserver;
|
||||||
|
use App\Observers\ProjectTaskVisibilityUserObserver;
|
||||||
|
use App\Observers\ProjectUserObserver;
|
||||||
|
use App\Observers\UserObserver;
|
||||||
|
use App\Observers\UserTagObserver;
|
||||||
|
use App\Observers\UserTagRecognitionObserver;
|
||||||
|
use App\Observers\WebSocketDialogMsgObserver;
|
||||||
|
use App\Observers\WebSocketDialogObserver;
|
||||||
|
use App\Observers\WebSocketDialogUserObserver;
|
||||||
|
use Illuminate\Auth\Events\Registered;
|
||||||
|
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
|
||||||
|
use Illuminate\Cache\RateLimiting\Limit;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
@ -32,5 +66,48 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
\Illuminate\Database\Eloquent\Builder::macro('rawSql', function(){
|
\Illuminate\Database\Eloquent\Builder::macro('rawSql', function(){
|
||||||
return ($this->getQuery()->rawSql());
|
return ($this->getQuery()->rawSql());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$this->configureRateLimiting();
|
||||||
|
$this->registerEvents();
|
||||||
|
$this->registerObservers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* api 组限流(原 RouteServiceProvider::configureRateLimiting)
|
||||||
|
*/
|
||||||
|
protected function configureRateLimiting()
|
||||||
|
{
|
||||||
|
RateLimiter::for('api', function (Request $request) {
|
||||||
|
return Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 事件监听(原 EventServiceProvider::$listen)
|
||||||
|
*/
|
||||||
|
protected function registerEvents()
|
||||||
|
{
|
||||||
|
Event::listen(Registered::class, SendEmailVerificationNotification::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型观察者(原 EventServiceProvider::boot)
|
||||||
|
*/
|
||||||
|
protected function registerObservers()
|
||||||
|
{
|
||||||
|
File::observe(FileObserver::class);
|
||||||
|
FileUser::observe(FileUserObserver::class);
|
||||||
|
Project::observe(ProjectObserver::class);
|
||||||
|
ProjectTask::observe(ProjectTaskObserver::class);
|
||||||
|
ProjectTaskContent::observe(ProjectTaskContentObserver::class);
|
||||||
|
ProjectTaskUser::observe(ProjectTaskUserObserver::class);
|
||||||
|
ProjectTaskVisibilityUser::observe(ProjectTaskVisibilityUserObserver::class);
|
||||||
|
ProjectUser::observe(ProjectUserObserver::class);
|
||||||
|
User::observe(UserObserver::class);
|
||||||
|
UserTag::observe(UserTagObserver::class);
|
||||||
|
UserTagRecognition::observe(UserTagRecognitionObserver::class);
|
||||||
|
WebSocketDialog::observe(WebSocketDialogObserver::class);
|
||||||
|
WebSocketDialogMsg::observe(WebSocketDialogMsgObserver::class);
|
||||||
|
WebSocketDialogUser::observe(WebSocketDialogUserObserver::class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Providers;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
|
||||||
use Illuminate\Support\Facades\Gate;
|
|
||||||
|
|
||||||
class AuthServiceProvider extends ServiceProvider
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The policy mappings for the application.
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $policies = [
|
|
||||||
// 'App\Models\Model' => 'App\Policies\ModelPolicy',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register any authentication / authorization services.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function boot()
|
|
||||||
{
|
|
||||||
$this->registerPolicies();
|
|
||||||
|
|
||||||
//
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Providers;
|
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Broadcast;
|
|
||||||
use Illuminate\Support\ServiceProvider;
|
|
||||||
|
|
||||||
class BroadcastServiceProvider extends ServiceProvider
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Bootstrap any application services.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function boot()
|
|
||||||
{
|
|
||||||
Broadcast::routes();
|
|
||||||
|
|
||||||
require base_path('routes/channels.php');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Providers;
|
|
||||||
|
|
||||||
use App\Models\File;
|
|
||||||
use App\Models\FileUser;
|
|
||||||
use App\Models\Project;
|
|
||||||
use App\Models\ProjectTask;
|
|
||||||
use App\Models\ProjectTaskContent;
|
|
||||||
use App\Models\ProjectTaskUser;
|
|
||||||
use App\Models\ProjectTaskVisibilityUser;
|
|
||||||
use App\Models\ProjectUser;
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Models\UserTag;
|
|
||||||
use App\Models\UserTagRecognition;
|
|
||||||
use App\Models\WebSocketDialog;
|
|
||||||
use App\Models\WebSocketDialogMsg;
|
|
||||||
use App\Models\WebSocketDialogUser;
|
|
||||||
use App\Observers\FileObserver;
|
|
||||||
use App\Observers\FileUserObserver;
|
|
||||||
use App\Observers\ProjectObserver;
|
|
||||||
use App\Observers\ProjectTaskContentObserver;
|
|
||||||
use App\Observers\ProjectTaskObserver;
|
|
||||||
use App\Observers\ProjectTaskUserObserver;
|
|
||||||
use App\Observers\ProjectTaskVisibilityUserObserver;
|
|
||||||
use App\Observers\ProjectUserObserver;
|
|
||||||
use App\Observers\UserObserver;
|
|
||||||
use App\Observers\UserTagObserver;
|
|
||||||
use App\Observers\UserTagRecognitionObserver;
|
|
||||||
use App\Observers\WebSocketDialogMsgObserver;
|
|
||||||
use App\Observers\WebSocketDialogObserver;
|
|
||||||
use App\Observers\WebSocketDialogUserObserver;
|
|
||||||
use Illuminate\Auth\Events\Registered;
|
|
||||||
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
|
|
||||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
|
||||||
|
|
||||||
class EventServiceProvider extends ServiceProvider
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The event listener mappings for the application.
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $listen = [
|
|
||||||
Registered::class => [
|
|
||||||
SendEmailVerificationNotification::class,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register any events for your application.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function boot()
|
|
||||||
{
|
|
||||||
File::observe(FileObserver::class);
|
|
||||||
FileUser::observe(FileUserObserver::class);
|
|
||||||
Project::observe(ProjectObserver::class);
|
|
||||||
ProjectTask::observe(ProjectTaskObserver::class);
|
|
||||||
ProjectTaskContent::observe(ProjectTaskContentObserver::class);
|
|
||||||
ProjectTaskUser::observe(ProjectTaskUserObserver::class);
|
|
||||||
ProjectTaskVisibilityUser::observe(ProjectTaskVisibilityUserObserver::class);
|
|
||||||
ProjectUser::observe(ProjectUserObserver::class);
|
|
||||||
User::observe(UserObserver::class);
|
|
||||||
UserTag::observe(UserTagObserver::class);
|
|
||||||
UserTagRecognition::observe(UserTagRecognitionObserver::class);
|
|
||||||
WebSocketDialog::observe(WebSocketDialogObserver::class);
|
|
||||||
WebSocketDialogMsg::observe(WebSocketDialogMsgObserver::class);
|
|
||||||
WebSocketDialogUser::observe(WebSocketDialogUserObserver::class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Providers;
|
|
||||||
|
|
||||||
use Illuminate\Cache\RateLimiting\Limit;
|
|
||||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\RateLimiter;
|
|
||||||
use Illuminate\Support\Facades\Route;
|
|
||||||
|
|
||||||
class RouteServiceProvider extends ServiceProvider
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The path to the "home" route for your application.
|
|
||||||
*
|
|
||||||
* This is used by Laravel authentication to redirect users after login.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
public const HOME = '/home';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The controller namespace for the application.
|
|
||||||
*
|
|
||||||
* When present, controller route declarations will automatically be prefixed with this namespace.
|
|
||||||
*
|
|
||||||
* @var string|null
|
|
||||||
*/
|
|
||||||
// protected $namespace = 'App\\Http\\Controllers';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Define your route model bindings, pattern filters, etc.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function boot()
|
|
||||||
{
|
|
||||||
$this->configureRateLimiting();
|
|
||||||
|
|
||||||
$this->routes(function () {
|
|
||||||
Route::prefix('api')
|
|
||||||
->middleware('api')
|
|
||||||
->namespace($this->namespace)
|
|
||||||
->group(base_path('routes/api.php'));
|
|
||||||
|
|
||||||
Route::middleware('web')
|
|
||||||
->namespace($this->namespace)
|
|
||||||
->group(base_path('routes/web.php'));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configure the rate limiters for the application.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function configureRateLimiting()
|
|
||||||
{
|
|
||||||
RateLimiter::for('api', function (Request $request) {
|
|
||||||
return Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -136,6 +136,20 @@ class WebSocketService implements WebSocketHandlerInterface
|
|||||||
}
|
}
|
||||||
Cache::put("User::encrypt:" . $frame->fd, Base::array2json($data), Carbon::now()->addDay());
|
Cache::put("User::encrypt:" . $frame->fd, Base::array2json($data), Carbon::now()->addDay());
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// AI 助手页面操作结果回包(由 assistant/operation/dispatch 派发,前端执行后回传)
|
||||||
|
case 'operationResult':
|
||||||
|
$requestId = trim($data['requestId'] ?? '');
|
||||||
|
if ($requestId !== '') {
|
||||||
|
$row = WebSocket::whereFd($frame->fd)->first();
|
||||||
|
Cache::put("ai_op_result:{$requestId}", [
|
||||||
|
'userid' => $row?->userid ?: 0,
|
||||||
|
'success' => !empty($data['success']),
|
||||||
|
'result' => $data['result'] ?? null,
|
||||||
|
'error' => $data['error'] ?? null,
|
||||||
|
], 60);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回消息
|
// 返回消息
|
||||||
|
|||||||
@ -29,6 +29,19 @@ abstract class AbstractTask extends Task
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重写投递:非 Swoole 运行时(artisan/测试)无 swoole 绑定,无法投递异步任务,跳过(与 AbstractObserver 守卫一致)
|
||||||
|
* @param mixed $task
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function task($task)
|
||||||
|
{
|
||||||
|
if (!app()->bound('swoole')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return parent::task($task);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 开始执行任务
|
* 开始执行任务
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -6,6 +6,7 @@ use App\Models\FileContent;
|
|||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
use App\Models\ProjectTask;
|
use App\Models\ProjectTask;
|
||||||
use App\Models\Report;
|
use App\Models\Report;
|
||||||
|
use App\Models\Setting;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\UserBot;
|
use App\Models\UserBot;
|
||||||
use App\Models\UserDepartment;
|
use App\Models\UserDepartment;
|
||||||
@ -469,21 +470,29 @@ class BotReceiveMsgTask extends AbstractTask
|
|||||||
if ($msg->msg['model_name']) {
|
if ($msg->msg['model_name']) {
|
||||||
$extras['model_name'] = $msg->msg['model_name'];
|
$extras['model_name'] = $msg->msg['model_name'];
|
||||||
}
|
}
|
||||||
// 提取模型“思考”参数
|
// 优先读取模型列表中按模型配置的思考档位(off|low|medium|high)
|
||||||
$thinkPatterns = [
|
$thinkingEffort = Setting::AIBotModelThinking($setting[$type . '_models'] ?? '', $extras['model_name']);
|
||||||
"/^(.+?)(\s+|\s*[_-]\s*)(think|thinking|reasoning)\s*$/",
|
// 兼容旧约定:模型名带 (thinking)/-reasoning 等后缀时,剥离后缀并视为 medium 档
|
||||||
"/^(.+?)\s*\(\s*(think|thinking|reasoning)\s*\)\s*$/"
|
if ($thinkingEffort === 'off') {
|
||||||
];
|
$thinkPatterns = [
|
||||||
$thinkMatch = [];
|
"/^(.+?)(\s+|\s*[_-]\s*)(think|thinking|reasoning)\s*$/",
|
||||||
foreach ($thinkPatterns as $pattern) {
|
"/^(.+?)\s*\(\s*(think|thinking|reasoning)\s*\)\s*$/"
|
||||||
if (preg_match($pattern, $extras['model_name'], $thinkMatch)) {
|
];
|
||||||
break;
|
$thinkMatch = [];
|
||||||
|
foreach ($thinkPatterns as $pattern) {
|
||||||
|
if (preg_match($pattern, $extras['model_name'], $thinkMatch)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($thinkMatch && !empty($thinkMatch[1])) {
|
||||||
|
$extras['model_name'] = $thinkMatch[1];
|
||||||
|
$thinkingEffort = 'medium';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($thinkMatch && !empty($thinkMatch[1])) {
|
if ($thinkingEffort !== 'off') {
|
||||||
$extras['model_name'] = $thinkMatch[1];
|
$extras['thinking_effort'] = $thinkingEffort;
|
||||||
$extras['max_tokens'] = 20000;
|
$extras['max_tokens'] = 20000;
|
||||||
$extras['thinking'] = 4096;
|
$extras['thinking'] = 4096; // 兼容旧版插件
|
||||||
$extras['temperature'] = 1.0;
|
$extras['temperature'] = 1.0;
|
||||||
}
|
}
|
||||||
// 设定会话ID
|
// 设定会话ID
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Tasks;
|
namespace App\Tasks;
|
||||||
|
|
||||||
use App\Models\ApproveProcInstHistory;
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\UserCheckinRecord;
|
use App\Models\UserCheckinRecord;
|
||||||
use App\Models\WebSocketDialog;
|
use App\Models\WebSocketDialog;
|
||||||
@ -80,9 +79,6 @@ class CheckinRemindTask extends AbstractTask
|
|||||||
if (!UserCheckinRecord::whereUserid($user->userid)->where('created_at', '>', Carbon::now()->subDays(3))->exists()) {
|
if (!UserCheckinRecord::whereUserid($user->userid)->where('created_at', '>', Carbon::now()->subDays(3))->exists()) {
|
||||||
continue; // 3天内没有打卡
|
continue; // 3天内没有打卡
|
||||||
}
|
}
|
||||||
if (ApproveProcInstHistory::userIsLeave($user->userid)) {
|
|
||||||
continue; // 请假、外出
|
|
||||||
}
|
|
||||||
$dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid);
|
$dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid);
|
||||||
if ($dialog) {
|
if ($dialog) {
|
||||||
if ($type === 'exceed') {
|
if ($type === 'exceed') {
|
||||||
|
|||||||
@ -65,7 +65,7 @@ class DeleteTmpTask extends AbstractTask
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'file':
|
case 'file':
|
||||||
$day = intval(env("AUTO_EMPTY_FILE_RECYCLE", 365));
|
$day = intval(config('dootask.auto_empty_file_recycle'));
|
||||||
if ($day <= 0) {
|
if ($day <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -81,7 +81,7 @@ class DeleteTmpTask extends AbstractTask
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'tmp_file':
|
case 'tmp_file':
|
||||||
$day = intval(env("AUTO_EMPTY_TEMP_FILE", 30));
|
$day = intval(config('dootask.auto_empty_temp_file'));
|
||||||
if ($day <= 0) {
|
if ($day <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,8 +9,9 @@ use App\Module\Base;
|
|||||||
use App\Module\Doo;
|
use App\Module\Doo;
|
||||||
use App\Module\Timer;
|
use App\Module\Timer;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Guanguans\Notify\Factory;
|
use Symfony\Component\Mailer\Mailer;
|
||||||
use Guanguans\Notify\Messages\EmailMessage;
|
use Symfony\Component\Mailer\Transport;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 未读消息邮件通知任务
|
* 未读消息邮件通知任务
|
||||||
@ -258,20 +259,18 @@ class EmailNoticeTask extends AbstractTask
|
|||||||
private function sendEmail($user, $emailData): void
|
private function sendEmail($user, $emailData): void
|
||||||
{
|
{
|
||||||
Setting::validateAddr($user->email, function($to) use ($emailData) {
|
Setting::validateAddr($user->email, function($to) use ($emailData) {
|
||||||
Factory::mailer()
|
$mailer = new Mailer(Transport::fromDsn(sprintf(
|
||||||
->setDsn(sprintf(
|
'smtp://%s:%s@%s:%s?verify_peer=0',
|
||||||
'smtp://%s:%s@%s:%s?verify_peer=0',
|
$this->emailSetting['account'],
|
||||||
$this->emailSetting['account'],
|
$this->emailSetting['password'],
|
||||||
$this->emailSetting['password'],
|
$this->emailSetting['smtp_server'],
|
||||||
$this->emailSetting['smtp_server'],
|
$this->emailSetting['port']
|
||||||
$this->emailSetting['port']
|
)));
|
||||||
))
|
$mailer->send((new Email())
|
||||||
->setMessage(EmailMessage::create()
|
->from(sprintf('%s <%s>', Base::settingFind('system', 'system_alias', 'Task'), $this->emailSetting['account']))
|
||||||
->from(sprintf('%s <%s>', Base::settingFind('system', 'system_alias', 'Task'), $this->emailSetting['account']))
|
->to($to)
|
||||||
->to($to)
|
->subject($emailData['subject'])
|
||||||
->subject($emailData['subject'])
|
->html($emailData['content']));
|
||||||
->html($emailData['content']))
|
|
||||||
->send();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -60,7 +60,7 @@ class LoopTask extends AbstractTask
|
|||||||
}
|
}
|
||||||
// 新任务时间、周期
|
// 新任务时间、周期
|
||||||
if ($task->start_at) {
|
if ($task->start_at) {
|
||||||
$diffSecond = Carbon::parse($task->start_at)->diffInSeconds(Carbon::parse($task->end_at), true);
|
$diffSecond = (int)Carbon::parse($task->start_at)->diffInSeconds(Carbon::parse($task->end_at), true);
|
||||||
$task->start_at = Carbon::parse($task->loop_at);
|
$task->start_at = Carbon::parse($task->loop_at);
|
||||||
$task->end_at = $task->start_at->clone()->addSeconds($diffSecond);
|
$task->end_at = $task->start_at->clone()->addSeconds($diffSecond);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -116,6 +116,10 @@ class PushTask extends AbstractTask
|
|||||||
if (!Base::isTwoArray($lists)) {
|
if (!Base::isTwoArray($lists)) {
|
||||||
$lists = [$lists];
|
$lists = [$lists];
|
||||||
}
|
}
|
||||||
|
// 非 Swoole 运行时(artisan/测试)无 swoole 绑定,无法推送,直接跳过(与 AbstractObserver 守卫一致)
|
||||||
|
if (!app()->bound('swoole')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
$swoole = app('swoole');
|
$swoole = app('swoole');
|
||||||
foreach ($lists AS $item) {
|
foreach ($lists AS $item) {
|
||||||
if (!is_array($item) || empty($item)) {
|
if (!is_array($item) || empty($item)) {
|
||||||
|
|||||||
50
artisan
50
artisan
@ -1,53 +1,15 @@
|
|||||||
#!/usr/bin/env php
|
#!/usr/bin/env php
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
|
|
||||||
define('LARAVEL_START', microtime(true));
|
define('LARAVEL_START', microtime(true));
|
||||||
|
|
||||||
/*
|
// Register the Composer autoloader...
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Register The Auto Loader
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Composer provides a convenient, automatically generated class loader
|
|
||||||
| for our application. We just need to utilize it! We'll require it
|
|
||||||
| into the script here so that we do not have to worry about the
|
|
||||||
| loading of any of our classes manually. It's great to relax.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
require __DIR__.'/vendor/autoload.php';
|
require __DIR__.'/vendor/autoload.php';
|
||||||
|
|
||||||
$app = require_once __DIR__.'/bootstrap/app.php';
|
// Bootstrap Laravel and handle the command...
|
||||||
|
$status = (require_once __DIR__.'/bootstrap/app.php')
|
||||||
/*
|
->handleCommand(new ArgvInput);
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Run The Artisan Application
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| When we run the console application, the current CLI command will be
|
|
||||||
| executed in this console and the response sent back to a terminal
|
|
||||||
| or another output device for the developers. Here goes nothing!
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
|
|
||||||
|
|
||||||
$status = $kernel->handle(
|
|
||||||
$input = new Symfony\Component\Console\Input\ArgvInput,
|
|
||||||
new Symfony\Component\Console\Output\ConsoleOutput
|
|
||||||
);
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Shutdown The Application
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Once Artisan has finished running, we will fire off the shutdown events
|
|
||||||
| so that any final work may be done by the application before we shut
|
|
||||||
| down the process. This is the last thing to happen to the request.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
$kernel->terminate($input, $status);
|
|
||||||
|
|
||||||
exit($status);
|
exit($status);
|
||||||
|
|||||||
296
bin/install
Executable file
296
bin/install
Executable file
@ -0,0 +1,296 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# DooTask 一键安装 / 升级脚本
|
||||||
|
#
|
||||||
|
# 用法(在目标目录执行):
|
||||||
|
# curl -fsSL https://raw.githubusercontent.com/kuaifan/dootask/pro/bin/install | bash
|
||||||
|
#
|
||||||
|
# 脚本会根据「当前目录」自动判断该做什么,无需额外参数:
|
||||||
|
# - 空目录 : 全新安装(克隆代码到当前目录 + ./cmd install)
|
||||||
|
# - 已克隆但未安装 : 继续安装(./cmd install)
|
||||||
|
# - 已安装 : 检查更新,确认后用「线上最新 cmd」执行升级
|
||||||
|
# - 非空且不是 DooTask : 拒绝操作并提示(绝不在此克隆或重置)
|
||||||
|
#
|
||||||
|
# 升级一次到位的关键:升级时从「线上 raw」取最新 cmd 到临时文件执行,
|
||||||
|
# 既不依赖用户机器上那份可能过时的 cmd,也不写本地 .git(规避属主/权限问题),
|
||||||
|
# 真正的 git pull / 依赖 / 迁移 / 重启全部交给这份最新 cmd,避免「升两次」。
|
||||||
|
#
|
||||||
|
# 输出语言:仅当 locale 明确是中文 UTF-8 时显示中文,否则一律英文。
|
||||||
|
#
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
# ---------- 配置 ----------
|
||||||
|
BRANCH="pro" # 全新安装默认分支(升级时跟随当前分支)
|
||||||
|
REPO_GITHUB="https://github.com/kuaifan/dootask.git"
|
||||||
|
REPO_GITEE="https://gitee.com/aipaw/dootask.git"
|
||||||
|
# raw 基址:升级时取版本号与最新 cmd 用。后期可把 RAW_PRIMARY 换成官网映射的域名。
|
||||||
|
RAW_PRIMARY="https://raw.githubusercontent.com/kuaifan/dootask" # https://<base>/<branch>/<path>
|
||||||
|
RAW_FALLBACK="https://cdn.jsdelivr.net/gh/kuaifan/dootask" # https://<base>@<branch>/<path>
|
||||||
|
|
||||||
|
# ---------- 语言判定 ----------
|
||||||
|
# 默认英文;仅当 locale 明确是「中文 UTF-8」时才用中文(中文非 UTF-8 如 GBK 也用英文以免乱码)。
|
||||||
|
DT_LANG="en"
|
||||||
|
__loc="${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}"
|
||||||
|
case "$__loc" in
|
||||||
|
zh_*|zh-*|zh)
|
||||||
|
case "$__loc" in
|
||||||
|
*[Uu][Tt][Ff]*) DT_LANG="zh" ;; # 明确 UTF-8 → 中文
|
||||||
|
*.*) DT_LANG="en" ;; # 其他编码(如 .GBK)→ 英文,避免乱码
|
||||||
|
*) DT_LANG="zh" ;; # 无编码后缀(裸 zh_CN)→ 现代默认 UTF-8
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
unset __loc
|
||||||
|
|
||||||
|
# ---------- 文案 ----------
|
||||||
|
# 调用处只写中文(动态值用 (*) 占位,顺序对应后续参数,与前端 $L 风格一致)。
|
||||||
|
# 中文环境直接用原文;英文环境在此集中查表翻译,未登记的中文原样返回。
|
||||||
|
msg() {
|
||||||
|
local tpl="$1"; shift
|
||||||
|
local out="$tpl"
|
||||||
|
if [ "$DT_LANG" != "zh" ]; then
|
||||||
|
case "$tpl" in
|
||||||
|
"成功") out="OK" ;;
|
||||||
|
"警告") out="WARN" ;;
|
||||||
|
"错误") out="ERROR" ;;
|
||||||
|
"未知") out="unknown" ;;
|
||||||
|
"git 未安装,请先安装后重试")
|
||||||
|
out="git is not installed. Please install it and retry." ;;
|
||||||
|
"curl 未安装,请先安装后重试")
|
||||||
|
out="curl is not installed. Please install it and retry." ;;
|
||||||
|
"Docker 未安装,请先安装后重试")
|
||||||
|
out="Docker is not installed. Please install it and retry." ;;
|
||||||
|
"docker-compose(或 docker compose 插件)未安装,请先安装后重试")
|
||||||
|
out="docker-compose (or the docker compose plugin) is not installed. Please install it and retry." ;;
|
||||||
|
"当前目录为空,开始全新安装 DooTask ...")
|
||||||
|
out="Current directory is empty. Starting a fresh DooTask installation..." ;;
|
||||||
|
"克隆代码(GitHub)...")
|
||||||
|
out="Cloning source (GitHub)..." ;;
|
||||||
|
"GitHub 克隆失败,尝试 Gitee 镜像 ...")
|
||||||
|
out="GitHub clone failed, trying the Gitee mirror..." ;;
|
||||||
|
"代码克隆失败,请检查网络后重试")
|
||||||
|
out="Failed to clone the source. Please check your network and retry." ;;
|
||||||
|
"代码克隆完成")
|
||||||
|
out="Source cloned." ;;
|
||||||
|
"执行安装 ...")
|
||||||
|
out="Running installation..." ;;
|
||||||
|
"DooTask 安装完成")
|
||||||
|
out="DooTask installation complete." ;;
|
||||||
|
"检测到已克隆但尚未安装,执行安装 ...")
|
||||||
|
out="Repository found but not yet installed. Running installation..." ;;
|
||||||
|
"检测到已安装的 DooTask,正在检查更新 ...")
|
||||||
|
out="Existing DooTask installation detected. Checking for updates..." ;;
|
||||||
|
"无法获取远程版本信息(分支 (*)),请检查网络后重试")
|
||||||
|
out="Unable to fetch remote version info (branch (*)). Please check your network and retry." ;;
|
||||||
|
"当前已是最新版本(v(*))")
|
||||||
|
out="Already up to date (v(*))." ;;
|
||||||
|
"发现新版本:当前 v(*) → 最新 v(*)(分支 (*))")
|
||||||
|
out="New version available: current v(*) -> latest v(*) (branch (*))." ;;
|
||||||
|
"是否立即升级?")
|
||||||
|
out="Upgrade now?" ;;
|
||||||
|
"已取消升级")
|
||||||
|
out="Upgrade cancelled." ;;
|
||||||
|
"获取最新 cmd 失败,请检查网络后重试")
|
||||||
|
out="Failed to fetch the latest cmd. Please check your network and retry." ;;
|
||||||
|
"开始升级 ...")
|
||||||
|
out="Starting upgrade..." ;;
|
||||||
|
"DooTask 升级完成")
|
||||||
|
out="DooTask upgrade complete." ;;
|
||||||
|
"当前目录非空,且不是 DooTask 项目目录。")
|
||||||
|
out="Current directory is not empty and is not a DooTask project." ;;
|
||||||
|
"请在「空目录」中执行全新安装,或进入「已安装的 DooTask 目录」执行升级。")
|
||||||
|
out="Run this in an empty directory for a fresh install, or inside an existing DooTask directory to upgrade." ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
# 动态值:依次把 (*) 替换为参数
|
||||||
|
local a
|
||||||
|
for a in "$@"; do
|
||||||
|
out="${out/(\*)/$a}"
|
||||||
|
done
|
||||||
|
printf '%s' "$out"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 输出 ----------
|
||||||
|
if [ -t 1 ]; then
|
||||||
|
Red="\033[31m"; Green="\033[32m"; Yellow="\033[33m"; Blue="\033[36m"; Font="\033[0m"
|
||||||
|
else
|
||||||
|
Red=""; Green=""; Yellow=""; Blue=""; Font=""
|
||||||
|
fi
|
||||||
|
info() { echo -e "${Blue}==>${Font} $1"; }
|
||||||
|
success() { echo -e "${Green}[$(msg 成功)]${Font} $1"; }
|
||||||
|
warning() { echo -e "${Yellow}[$(msg 警告)]${Font} $1"; }
|
||||||
|
error() { echo -e "${Red}[$(msg 错误)]${Font} $1" >&2; }
|
||||||
|
die() { error "$1"; exit 1; }
|
||||||
|
|
||||||
|
# ---------- 交互输入 ----------
|
||||||
|
# curl | bash 时 stdin 被管道占用,交互一律从 /dev/tty 读,否则 read 会读到 EOF
|
||||||
|
has_tty() { [ -e /dev/tty ]; }
|
||||||
|
|
||||||
|
confirm() {
|
||||||
|
# $1=提示语,默认 Y;无终端时返回失败(不擅自执行需确认的操作)
|
||||||
|
local prompt="$1" ans
|
||||||
|
has_tty || return 1
|
||||||
|
read -r -p "$prompt [Y/n] " ans < /dev/tty
|
||||||
|
[[ -z "$ans" || "$ans" =~ ^[Yy]([Ee][Ss])?$ ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 提权执行 ----------
|
||||||
|
# install / update 需要 root;统一用 bash 执行脚本(规避 /tmp noexec),
|
||||||
|
# 交互(含 cmd 内部的 read 与 sudo 密码)接到 /dev/tty。
|
||||||
|
# git clone 不走这里,用当前用户执行,避免代码属主变成 root。
|
||||||
|
run_cmd() {
|
||||||
|
local script="$1"; shift
|
||||||
|
local stdin_src="/dev/stdin"
|
||||||
|
has_tty && stdin_src="/dev/tty"
|
||||||
|
if [ "$(id -u)" -eq 0 ]; then
|
||||||
|
bash "$script" "$@" < "$stdin_src"
|
||||||
|
else
|
||||||
|
sudo bash "$script" "$@" < "$stdin_src"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 前置检查 ----------
|
||||||
|
precheck() {
|
||||||
|
command -v git >/dev/null 2>&1 || die "$(msg 'git 未安装,请先安装后重试')"
|
||||||
|
command -v curl >/dev/null 2>&1 || die "$(msg 'curl 未安装,请先安装后重试')"
|
||||||
|
command -v docker >/dev/null 2>&1 || die "$(msg 'Docker 未安装,请先安装后重试')"
|
||||||
|
if ! docker compose version >/dev/null 2>&1 && ! docker-compose version >/dev/null 2>&1; then
|
||||||
|
die "$(msg 'docker-compose(或 docker compose 插件)未安装,请先安装后重试')"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 工具 ----------
|
||||||
|
# 从 raw 取「指定分支的文件」到 stdout:主源失败时回退 jsdelivr 镜像
|
||||||
|
fetch_raw() {
|
||||||
|
# $1=branch, $2=path
|
||||||
|
curl -fsSL "${RAW_PRIMARY}/$1/$2" 2>/dev/null \
|
||||||
|
|| curl -fsSL "${RAW_FALLBACK}@$1/$2" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# 从 stdin 读取 package.json 内容并提取版本号
|
||||||
|
read_pkg_version() {
|
||||||
|
grep -m1 '"version"' | sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/'
|
||||||
|
}
|
||||||
|
|
||||||
|
is_dootask_project() {
|
||||||
|
# 只读 .git,不写;当前用户读取一般文件不受属主影响
|
||||||
|
if [ -d .git ] && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
|
local url; url="$(git config --get remote.origin.url 2>/dev/null || true)"
|
||||||
|
[[ "$url" == *dootask* ]] && return 0
|
||||||
|
fi
|
||||||
|
[ -f cmd ] && [ -f docker-compose.yml ] && return 0
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
is_installed() { [ -f vendor/autoload.php ]; }
|
||||||
|
|
||||||
|
# 可忽略的系统垃圾文件(判断空目录时忽略;全新安装前会清除以便 git clone .)
|
||||||
|
_IGNORABLE=".DS_Store .localized .Spotlight-V100 .fseventsd .TemporaryItems .Trashes .DocumentRevisions-V100 .VolumeIcon.icns .AppleDouble .AppleDB .AppleDesktop Thumbs.db ehthumbs.db desktop.ini .directory"
|
||||||
|
|
||||||
|
# 是否「可忽略的系统文件」(含 macOS AppleDouble 的 ._xxx)
|
||||||
|
_is_ignorable() {
|
||||||
|
case " $_IGNORABLE " in *" $1 "*) return 0 ;; esac
|
||||||
|
case "$1" in ._*) return 0 ;; esac
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 当前目录是否「实质为空」:只剩可忽略的系统垃圾文件也算空
|
||||||
|
dir_empty() {
|
||||||
|
local f
|
||||||
|
while IFS= read -r f; do
|
||||||
|
_is_ignorable "${f##*/}" || return 1
|
||||||
|
done < <(find . -maxdepth 1 -mindepth 1 2>/dev/null)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 清除可忽略的系统垃圾文件(仅白名单),确保 git clone . 不被这些文件挡住
|
||||||
|
clean_ignorable() {
|
||||||
|
local f
|
||||||
|
while IFS= read -r f; do
|
||||||
|
_is_ignorable "${f##*/}" && rm -rf "$f"
|
||||||
|
done < <(find . -maxdepth 1 -mindepth 1 2>/dev/null)
|
||||||
|
}
|
||||||
|
|
||||||
|
current_branch() { git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "$BRANCH"; }
|
||||||
|
|
||||||
|
# ---------- 动作:全新安装 ----------
|
||||||
|
do_fresh_install() {
|
||||||
|
info "$(msg '当前目录为空,开始全新安装 DooTask ...')"
|
||||||
|
clean_ignorable # 清掉 .DS_Store 等系统垃圾,确保 git clone . 不被挡住
|
||||||
|
info "$(msg '克隆代码(GitHub)...')"
|
||||||
|
if ! git clone --depth=1 --branch "$BRANCH" "$REPO_GITHUB" . 2>/dev/null; then
|
||||||
|
warning "$(msg 'GitHub 克隆失败,尝试 Gitee 镜像 ...')"
|
||||||
|
rm -rf .git # 仅清理 clone 残留;若工作树仍有残留,下一步 clone 会报错而非误删
|
||||||
|
git clone --depth=1 --branch "$BRANCH" "$REPO_GITEE" . \
|
||||||
|
|| die "$(msg '代码克隆失败,请检查网络后重试')"
|
||||||
|
fi
|
||||||
|
success "$(msg '代码克隆完成')"
|
||||||
|
info "$(msg '执行安装 ...')"
|
||||||
|
run_cmd ./cmd install
|
||||||
|
success "$(msg 'DooTask 安装完成')"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 动作:续装 ----------
|
||||||
|
do_install() {
|
||||||
|
info "$(msg '检测到已克隆但尚未安装,执行安装 ...')"
|
||||||
|
run_cmd ./cmd install
|
||||||
|
success "$(msg 'DooTask 安装完成')"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 动作:升级 ----------
|
||||||
|
do_upgrade() {
|
||||||
|
info "$(msg '检测到已安装的 DooTask,正在检查更新 ...')"
|
||||||
|
local branch; branch="$(current_branch)"
|
||||||
|
|
||||||
|
local local_ver remote_ver
|
||||||
|
local_ver="$( [ -f package.json ] && read_pkg_version < package.json )"
|
||||||
|
remote_ver="$(fetch_raw "$branch" package.json | read_pkg_version)"
|
||||||
|
[ -z "$local_ver" ] && local_ver="$(msg 未知)"
|
||||||
|
|
||||||
|
# 取不到远程版本(网络/分支异常)→ 报错,避免误判
|
||||||
|
[ -z "$remote_ver" ] && die "$(msg '无法获取远程版本信息(分支 (*)),请检查网络后重试' "$branch")"
|
||||||
|
|
||||||
|
if [ "$local_ver" = "$remote_ver" ]; then
|
||||||
|
success "$(msg '当前已是最新版本(v(*))' "$local_ver")"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
info "$(msg '发现新版本:当前 v(*) → 最新 v(*)(分支 (*))' "$local_ver" "$remote_ver" "$branch")"
|
||||||
|
if ! confirm "$(msg '是否立即升级?')"; then
|
||||||
|
warning "$(msg '已取消升级')"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 从 raw 取「线上最新 cmd」到临时文件执行:不碰本地 .git、不依赖磁盘旧 cmd,
|
||||||
|
# 真正的 git pull / 装依赖 / 迁移 / 重启由这份最新 cmd 完成,一次到位。
|
||||||
|
local tmp; tmp="$(mktemp)"
|
||||||
|
if ! fetch_raw "$branch" cmd > "$tmp" || [ ! -s "$tmp" ]; then
|
||||||
|
rm -f "$tmp"; die "$(msg '获取最新 cmd 失败,请检查网络后重试')"
|
||||||
|
fi
|
||||||
|
info "$(msg '开始升级 ...')"
|
||||||
|
run_cmd "$tmp" update
|
||||||
|
rm -f "$tmp"
|
||||||
|
success "$(msg 'DooTask 升级完成')"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 主流程 ----------
|
||||||
|
main() {
|
||||||
|
precheck
|
||||||
|
if is_dootask_project; then
|
||||||
|
if is_installed; then
|
||||||
|
do_upgrade
|
||||||
|
else
|
||||||
|
do_install
|
||||||
|
fi
|
||||||
|
elif dir_empty; then
|
||||||
|
do_fresh_install
|
||||||
|
else
|
||||||
|
error "$(msg '当前目录非空,且不是 DooTask 项目目录。')"
|
||||||
|
error "$(msg '请在「空目录」中执行全新安装,或进入「已安装的 DooTask 目录」执行升级。')"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@ -1,55 +1,75 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/*
|
use App\Exceptions\ApiException;
|
||||||
|--------------------------------------------------------------------------
|
use App\Exceptions\ImagePathHandler;
|
||||||
| Create The Application
|
use App\Module\Base;
|
||||||
|--------------------------------------------------------------------------
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
|
|
use Illuminate\Foundation\Application;
|
||||||
| The first thing we will do is create a new Laravel application instance
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
| which serves as the "glue" for all the components of Laravel, and is
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
| the IoC container for the system binding all of the various parts.
|
use Illuminate\Http\Request;
|
||||||
|
|
use Illuminate\Support\Facades\Log;
|
||||||
*/
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
|
||||||
$app = new Illuminate\Foundation\Application(
|
return Application::configure(basePath: $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__))
|
||||||
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
|
->withRouting(
|
||||||
);
|
web: __DIR__.'/../routes/web.php',
|
||||||
|
api: __DIR__.'/../routes/api.php',
|
||||||
|
apiPrefix: 'api',
|
||||||
|
commands: __DIR__.'/../routes/console.php',
|
||||||
|
)
|
||||||
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
|
// PHP(Swoole)只在内网被 nginx 访问,外部无法直连,故信任内网代理。
|
||||||
|
// 只采信 X-Forwarded-Proto:nginx 已用 $the_scheme 覆盖该头(值由 nginx 控制),
|
||||||
|
// 据此让 url() 实时跟随 https;host/for 一律不信,避免 Host 注入与 IP 伪造。
|
||||||
|
$middleware->trustProxies(at: '*', headers: Request::HEADER_X_FORWARDED_PROTO);
|
||||||
|
|
||||||
/*
|
$middleware->trimStrings(except: [
|
||||||
|--------------------------------------------------------------------------
|
'current_password',
|
||||||
| Bind Important Interfaces
|
'password',
|
||||||
|--------------------------------------------------------------------------
|
'password_confirmation',
|
||||||
|
|
]);
|
||||||
| Next, we need to bind some important interfaces into the container so
|
|
||||||
| we will be able to resolve them when needed. The kernels serve the
|
|
||||||
| incoming requests to this application from both the web and CLI.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
$app->singleton(
|
$middleware->validateCsrfTokens(except: [
|
||||||
Illuminate\Contracts\Http\Kernel::class,
|
// 接口部分
|
||||||
App\Http\Kernel::class
|
'api/*',
|
||||||
);
|
|
||||||
|
|
||||||
$app->singleton(
|
// 发布桌面端
|
||||||
Illuminate\Contracts\Console\Kernel::class,
|
'desktop/publish/',
|
||||||
App\Console\Kernel::class
|
]);
|
||||||
);
|
|
||||||
|
|
||||||
$app->singleton(
|
// api 组限流(限流规则定义在 AppServiceProvider::boot)
|
||||||
Illuminate\Contracts\Debug\ExceptionHandler::class,
|
$middleware->throttleApi();
|
||||||
App\Exceptions\Handler::class
|
|
||||||
);
|
|
||||||
|
|
||||||
/*
|
$middleware->alias([
|
||||||
|--------------------------------------------------------------------------
|
'webapi' => \App\Http\Middleware\WebApi::class,
|
||||||
| Return The Application
|
]);
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This script returns the application instance. The instance is given to
|
|
||||||
| the calling script so we can separate the building of the instances
|
|
||||||
| from the actual running of the application and sending responses.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
return $app;
|
$middleware->redirectGuestsTo('/login');
|
||||||
|
$middleware->redirectUsersTo('/home');
|
||||||
|
})
|
||||||
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
|
// /uploads/**.png/crop/... 动态裁剪与缩略图(命中则返回图片,否则走默认 404)
|
||||||
|
$exceptions->render(function (NotFoundHttpException $e, Request $request) {
|
||||||
|
return ImagePathHandler::render($request);
|
||||||
|
});
|
||||||
|
|
||||||
|
$exceptions->render(function (ApiException $e) {
|
||||||
|
return response()->json(Base::retError($e->getMessage(), $e->getData(), $e->getCode()));
|
||||||
|
});
|
||||||
|
|
||||||
|
$exceptions->render(function (ModelNotFoundException $e) {
|
||||||
|
return response()->json(Base::retError('Interface error'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ApiException 按 isWriteLog 决定是否记录,且不走默认 report
|
||||||
|
$exceptions->report(function (ApiException $e) {
|
||||||
|
if ($e->isWriteLog()) {
|
||||||
|
Log::error($e->getMessage(), [
|
||||||
|
'code' => $e->getCode(),
|
||||||
|
'data' => $e->getData(),
|
||||||
|
'exception' => ' at ' . $e->getFile() . ':' . $e->getLine()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
})->stop();
|
||||||
|
})->create();
|
||||||
|
|||||||
5
bootstrap/providers.php
Normal file
5
bootstrap/providers.php
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
App\Providers\AppServiceProvider::class,
|
||||||
|
];
|
||||||
331
cmd
331
cmd
@ -9,10 +9,104 @@ YellowBG="\033[43;37m"
|
|||||||
RedBG="\033[41;37m"
|
RedBG="\033[41;37m"
|
||||||
Font="\033[0m"
|
Font="\033[0m"
|
||||||
|
|
||||||
|
# 语言判定:默认英文;仅当 locale 明确是「中文 UTF-8」时才用中文(中文非 UTF-8 如 GBK 也用英文以免乱码)。
|
||||||
|
DT_LANG="en"
|
||||||
|
__loc="${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}"
|
||||||
|
case "$__loc" in
|
||||||
|
zh_*|zh-*|zh)
|
||||||
|
case "$__loc" in
|
||||||
|
*[Uu][Tt][Ff]*) DT_LANG="zh" ;;
|
||||||
|
*.*) DT_LANG="en" ;;
|
||||||
|
*) DT_LANG="zh" ;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
unset __loc
|
||||||
|
|
||||||
|
# 文案:调用处只写中文(动态值用 (*) 占位,顺序对应后续参数)。
|
||||||
|
# 中文环境直接用原文;英文环境在此集中查表翻译,未登记的中文原样返回。
|
||||||
|
msg() {
|
||||||
|
local tpl="$1"; shift
|
||||||
|
local out="$tpl"
|
||||||
|
if [ "$DT_LANG" != "zh" ]; then
|
||||||
|
case "$tpl" in
|
||||||
|
"警告") out="WARN" ;;
|
||||||
|
"错误") out="ERROR" ;;
|
||||||
|
"地址") out="URL" ;;
|
||||||
|
"(*) 完成") out="(*) done" ;;
|
||||||
|
"(*) 失败") out="(*) failed" ;;
|
||||||
|
"备份数据库") out="Backing up database" ;;
|
||||||
|
"还原数据库") out="Restoring database" ;;
|
||||||
|
"无法创建脚本副本") out="Failed to create script copy" ;;
|
||||||
|
"没有找到 (*) 容器!") out="Container (*) not found!" ;;
|
||||||
|
"请使用 sudo 运行此脚本") out="Please run this script with sudo" ;;
|
||||||
|
"未安装 Docker!") out="Docker is not installed!" ;;
|
||||||
|
"未安装 Docker-compose!") out="Docker-compose is not installed!" ;;
|
||||||
|
"Docker-compose 版本过低,请升级至v2+!") out="Docker-compose is too old. Please upgrade to v2+!" ;;
|
||||||
|
"未安装 npm!") out="npm is not installed!" ;;
|
||||||
|
"未安装 Node.js!") out="Node.js is not installed!" ;;
|
||||||
|
"Node.js 版本过低,请升级至v20+!") out="Node.js is too old. Please upgrade to v20+!" ;;
|
||||||
|
"备份文件:(*)") out="Backup file: (*)" ;;
|
||||||
|
"没有备份文件!") out="No backup files found!" ;;
|
||||||
|
"可用备份列表:") out="Available backups:" ;;
|
||||||
|
"请输入备份文件编号还原:") out="Enter the backup number to restore: " ;;
|
||||||
|
"编号无效,请重新输入。") out="Invalid number, please try again." ;;
|
||||||
|
"HTTP服务端口不是80,是否修改并继续操作? [Y/n]") out="HTTP port is not 80. Change it and continue? [Y/n]" ;;
|
||||||
|
"HTTPS服务端口不是443,是否修改并继续操作? [Y/n]") out="HTTPS port is not 443. Change it and continue? [Y/n]" ;;
|
||||||
|
"继续操作") out="Continuing" ;;
|
||||||
|
"操作终止") out="Operation aborted" ;;
|
||||||
|
"任务已存在,无需添加。") out="Cron job already exists, skipped." ;;
|
||||||
|
"任务已添加。") out="Cron job added." ;;
|
||||||
|
"设置env参数失败!") out="Failed to set env variable!" ;;
|
||||||
|
"APP_ID((*))已被其他实例使用:(*)") out="APP_ID ((*)) is already used by another instance: (*)" ;;
|
||||||
|
"请先清空 .env 中的 APP_ID 和 APP_IPPR 再重新安装") out="Please clear APP_ID and APP_IPPR in .env, then reinstall" ;;
|
||||||
|
"端口 (*) 已被占用,请指定其他端口") out="Port (*) is already in use, please specify another port" ;;
|
||||||
|
"目录权限检测失败!请检查目录权限设置") out="Directory permission check failed! Please check directory permissions" ;;
|
||||||
|
"目录【(*)】权限不足!") out="Directory [(*)] is not writable!" ;;
|
||||||
|
"安装依赖失败") out="Failed to install dependencies" ;;
|
||||||
|
"安装依赖失败,请重试!") out="Failed to install dependencies, please retry!" ;;
|
||||||
|
"生成密钥失败") out="Failed to generate app key" ;;
|
||||||
|
"数据库迁移失败") out="Database migration failed" ;;
|
||||||
|
"安装完成") out="Installation complete" ;;
|
||||||
|
"请先执行安装命令") out="Please run the install command first" ;;
|
||||||
|
"检测到本地修改,是否强制更新?[Y/n]") out="Local changes detected. Force update? [Y/n]" ;;
|
||||||
|
"取消更新,请先处理本地修改") out="Update cancelled, please handle local changes first" ;;
|
||||||
|
"获取远程更新失败") out="Failed to fetch remote updates" ;;
|
||||||
|
"设置远程Fetch配置失败") out="Failed to set remote fetch config" ;;
|
||||||
|
"获取远程分支 (*) 失败") out="Failed to fetch remote branch (*)" ;;
|
||||||
|
"切换分支到 (*) 失败") out="Failed to switch to branch (*)" ;;
|
||||||
|
"数据库有迁移变动,执行数据库备份...") out="Database migrations changed, backing up database..." ;;
|
||||||
|
"数据库备份失败") out="Database backup failed" ;;
|
||||||
|
"数据库备份完成") out="Database backup complete" ;;
|
||||||
|
"强制更新代码失败") out="Failed to force-update code" ;;
|
||||||
|
"代码拉取失败,可能存在冲突,请使用 --force 参数") out="Failed to pull code (possible conflict), please use --force" ;;
|
||||||
|
"更新PHP依赖失败") out="Failed to update PHP dependencies" ;;
|
||||||
|
"执行数据库备份...") out="Backing up database..." ;;
|
||||||
|
"重启服务失败") out="Failed to restart services" ;;
|
||||||
|
"更新完成") out="Update complete" ;;
|
||||||
|
"警告:此操作将永久删除以下内容:") out="WARNING: This will permanently delete:" ;;
|
||||||
|
"- 数据库") out="- Database" ;;
|
||||||
|
"- 应用程序") out="- Application" ;;
|
||||||
|
"- 日志文件") out="- Log files" ;;
|
||||||
|
"确认要继续卸载吗?(y/N): ") out="Confirm uninstall? (y/N): " ;;
|
||||||
|
"开始卸载...") out="Uninstalling..." ;;
|
||||||
|
"终止卸载。") out="Uninstall aborted." ;;
|
||||||
|
"卸载完成") out="Uninstall complete" ;;
|
||||||
|
"修改成功") out="Changed successfully" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
# 动态值:依次把 (*) 替换为参数
|
||||||
|
local a
|
||||||
|
for a in "$@"; do
|
||||||
|
out="${out/(\*)/$a}"
|
||||||
|
done
|
||||||
|
printf '%s' "$out"
|
||||||
|
}
|
||||||
|
|
||||||
# 通知信息
|
# 通知信息
|
||||||
OK="${Green}[OK]${Font}"
|
OK="${Green}[OK]${Font}"
|
||||||
Warn="${Yellow}[警告]${Font}"
|
Warn="${Yellow}[$(msg 警告)]${Font}"
|
||||||
Error="${Red}[错误]${Font}"
|
Error="${Red}[$(msg 错误)]${Font}"
|
||||||
|
|
||||||
# 基本参数
|
# 基本参数
|
||||||
WORK_DIR="$(pwd)"
|
WORK_DIR="$(pwd)"
|
||||||
@ -28,7 +122,7 @@ fi
|
|||||||
# 缓存执行
|
# 缓存执行
|
||||||
if [ -z "$CACHED_EXECUTION" ] && [ "$1" == "update" ]; then
|
if [ -z "$CACHED_EXECUTION" ] && [ "$1" == "update" ]; then
|
||||||
if ! cat "$0" > ._cmd 2>/dev/null; then
|
if ! cat "$0" > ._cmd 2>/dev/null; then
|
||||||
error "无法创建脚本副本"
|
error "$(msg '无法创建脚本副本')"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
chmod +x ._cmd
|
chmod +x ._cmd
|
||||||
@ -42,10 +136,10 @@ fi
|
|||||||
# 判断是否成功
|
# 判断是否成功
|
||||||
judge() {
|
judge() {
|
||||||
if [[ 0 -eq $? ]]; then
|
if [[ 0 -eq $? ]]; then
|
||||||
success "$1 完成"
|
success "$(msg '(*) 完成' "$1")"
|
||||||
sleep 1
|
sleep 1
|
||||||
else
|
else
|
||||||
error "$1 失败"
|
error "$(msg '(*) 失败' "$1")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@ -128,7 +222,7 @@ switch_debug() {
|
|||||||
# 检查是否有sudo
|
# 检查是否有sudo
|
||||||
check_sudo() {
|
check_sudo() {
|
||||||
if [ "$EUID" -ne 0 ]; then
|
if [ "$EUID" -ne 0 ]; then
|
||||||
error "请使用 sudo 运行此脚本"
|
error "$(msg '请使用 sudo 运行此脚本')"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@ -137,21 +231,21 @@ check_sudo() {
|
|||||||
check_docker() {
|
check_docker() {
|
||||||
docker --version &> /dev/null
|
docker --version &> /dev/null
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
error "未安装 Docker!"
|
error "$(msg '未安装 Docker!')"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
docker-compose version &> /dev/null
|
docker-compose version &> /dev/null
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
docker compose version &> /dev/null
|
docker compose version &> /dev/null
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
error "未安装 Docker-compose!"
|
error "$(msg '未安装 Docker-compose!')"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
COMPOSE="docker compose"
|
COMPOSE="docker compose"
|
||||||
fi
|
fi
|
||||||
if [[ -n `$COMPOSE version | grep -E "\s+v1\."` ]]; then
|
if [[ -n `$COMPOSE version | grep -E "\s+v1\."` ]]; then
|
||||||
$COMPOSE version
|
$COMPOSE version
|
||||||
error "Docker-compose 版本过低,请升级至v2+!"
|
error "$(msg 'Docker-compose 版本过低,请升级至v2+!')"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@ -160,17 +254,17 @@ check_docker() {
|
|||||||
check_node() {
|
check_node() {
|
||||||
npm --version &> /dev/null
|
npm --version &> /dev/null
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
error "未安装 npm!"
|
error "$(msg '未安装 npm!')"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
node --version &> /dev/null
|
node --version &> /dev/null
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
error "未安装 Node.js!"
|
error "$(msg '未安装 Node.js!')"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [[ -n `node --version | grep -E "v1"` ]]; then
|
if [[ -n `node --version | grep -E "v1"` ]]; then
|
||||||
node --version
|
node --version
|
||||||
error "Node.js 版本过低,请升级至v20+!"
|
error "$(msg 'Node.js 版本过低,请升级至v20+!')"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@ -180,6 +274,19 @@ docker_name() {
|
|||||||
echo `$COMPOSE ps | awk '{print $1}' | grep "\-$1\-"`
|
echo `$COMPOSE ps | awk '{print $1}' | grep "\-$1\-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 等待 php 容器健康(最多约 90s)
|
||||||
|
wait_php_healthy() {
|
||||||
|
local name st wait=0
|
||||||
|
name="$(docker_name php)"
|
||||||
|
[ -z "$name" ] && return 0
|
||||||
|
while [ $wait -lt 90 ]; do
|
||||||
|
st="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$name" 2>/dev/null)"
|
||||||
|
{ [ "$st" = "healthy" ] || [ "$st" = "running" ]; } && break
|
||||||
|
sleep 3
|
||||||
|
wait=$((wait + 3))
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
# 编译前端
|
# 编译前端
|
||||||
web_build() {
|
web_build() {
|
||||||
local type=$1
|
local type=$1
|
||||||
@ -244,12 +351,24 @@ container_exec() {
|
|||||||
local cmd=$@
|
local cmd=$@
|
||||||
local name=$(docker_name "$container")
|
local name=$(docker_name "$container")
|
||||||
if [ -z "$name" ]; then
|
if [ -z "$name" ]; then
|
||||||
error "没有找到 ${container} 容器!"
|
error "$(msg '没有找到 (*) 容器!' "$container")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
docker exec $TTY_FLAG "$name" /bin/sh -c "$cmd"
|
docker exec $TTY_FLAG "$name" /bin/sh -c "$cmd"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 使用当前 docker-compose.yml 定义的服务镜像执行一次性容器命令
|
||||||
|
container_run() {
|
||||||
|
local container=$1
|
||||||
|
shift 1
|
||||||
|
local cmd=$@
|
||||||
|
if [ -t 0 ] && [ -t 1 ]; then
|
||||||
|
$COMPOSE run --rm --entrypoint /bin/sh "$container" -c "$cmd"
|
||||||
|
else
|
||||||
|
$COMPOSE run --rm -T --entrypoint /bin/sh "$container" -c "$cmd"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# 备份数据库、还原数据库
|
# 备份数据库、还原数据库
|
||||||
mysql_snapshot() {
|
mysql_snapshot() {
|
||||||
if [ "$1" = "backup" ]; then
|
if [ "$1" = "backup" ]; then
|
||||||
@ -260,8 +379,8 @@ mysql_snapshot() {
|
|||||||
mkdir -p ${WORK_DIR}/docker/mysql/backup
|
mkdir -p ${WORK_DIR}/docker/mysql/backup
|
||||||
filename="${WORK_DIR}/docker/mysql/backup/${database}_$(date "+%Y%m%d%H%M%S").sql.gz"
|
filename="${WORK_DIR}/docker/mysql/backup/${database}_$(date "+%Y%m%d%H%M%S").sql.gz"
|
||||||
container_exec mariadb "exec mysqldump --databases $database -u${username} -p${password}" | gzip > $filename
|
container_exec mariadb "exec mysqldump --databases $database -u${username} -p${password}" | gzip > $filename
|
||||||
judge "备份数据库"
|
judge "$(msg '备份数据库')"
|
||||||
[ -f "$filename" ] && echo "备份文件:${filename}"
|
[ -f "$filename" ] && echo "$(msg '备份文件:(*)' "$filename")"
|
||||||
elif [ "$1" = "recovery" ]; then
|
elif [ "$1" = "recovery" ]; then
|
||||||
database=$(env_get DB_DATABASE)
|
database=$(env_get DB_DATABASE)
|
||||||
username=$(env_get DB_USERNAME)
|
username=$(env_get DB_USERNAME)
|
||||||
@ -272,31 +391,31 @@ mysql_snapshot() {
|
|||||||
backup_files=("${WORK_DIR}/docker/mysql/backup/"*.sql.gz)
|
backup_files=("${WORK_DIR}/docker/mysql/backup/"*.sql.gz)
|
||||||
shopt -u nullglob
|
shopt -u nullglob
|
||||||
if [ ${#backup_files[@]} -eq 0 ]; then
|
if [ ${#backup_files[@]} -eq 0 ]; then
|
||||||
error "没有备份文件!"
|
error "$(msg '没有备份文件!')"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "可用备份列表:"
|
echo "$(msg '可用备份列表:')"
|
||||||
for idx in "${!backup_files[@]}"; do
|
for idx in "${!backup_files[@]}"; do
|
||||||
printf "%2d) %s\n" "$((idx + 1))" "$(basename "${backup_files[$idx]}")"
|
printf "%2d) %s\n" "$((idx + 1))" "$(basename "${backup_files[$idx]}")"
|
||||||
done
|
done
|
||||||
while true; do
|
while true; do
|
||||||
read -rp "请输入备份文件编号还原:" selection
|
read -rp "$(msg '请输入备份文件编号还原:')" selection
|
||||||
if [[ "$selection" =~ ^[0-9]+$ ]] && [ "$selection" -ge 1 ] && [ "$selection" -le ${#backup_files[@]} ]; then
|
if [[ "$selection" =~ ^[0-9]+$ ]] && [ "$selection" -ge 1 ] && [ "$selection" -le ${#backup_files[@]} ]; then
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
warning "编号无效,请重新输入。"
|
warning "$(msg '编号无效,请重新输入。')"
|
||||||
done
|
done
|
||||||
filename="${backup_files[$((selection - 1))]}"
|
filename="${backup_files[$((selection - 1))]}"
|
||||||
inputname="$(basename "$filename")"
|
inputname="$(basename "$filename")"
|
||||||
container_name=`docker_name mariadb`
|
container_name=`docker_name mariadb`
|
||||||
if [ -z "$container_name" ]; then
|
if [ -z "$container_name" ]; then
|
||||||
error "没有找到 mariadb 容器!"
|
error "$(msg '没有找到 (*) 容器!' mariadb)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
docker cp "$filename" "${container_name}:/"
|
docker cp "$filename" "${container_name}:/"
|
||||||
container_exec mariadb "gunzip < '/${inputname}' | mysql -u${username} -p${password} $database"
|
container_exec mariadb "gunzip < '/${inputname}' | mysql -u${username} -p${password} $database"
|
||||||
container_exec php "php artisan migrate"
|
container_exec php "php artisan migrate"
|
||||||
judge "还原数据库"
|
judge "$(msg '还原数据库')"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -327,33 +446,33 @@ remove_by_network() {
|
|||||||
https_auto() {
|
https_auto() {
|
||||||
restart_nginx="n"
|
restart_nginx="n"
|
||||||
if [[ "$(env_get APP_PORT)" != "80" ]]; then
|
if [[ "$(env_get APP_PORT)" != "80" ]]; then
|
||||||
warning "HTTP服务端口不是80,是否修改并继续操作? [Y/n]"
|
warning "$(msg 'HTTP服务端口不是80,是否修改并继续操作? [Y/n]')"
|
||||||
read -r continue_http
|
read -r continue_http
|
||||||
[[ -z ${continue_http} ]] && continue_http="Y"
|
[[ -z ${continue_http} ]] && continue_http="Y"
|
||||||
case $continue_http in
|
case $continue_http in
|
||||||
[yY][eE][sS] | [yY])
|
[yY][eE][sS] | [yY])
|
||||||
success "继续操作"
|
success "$(msg '继续操作')"
|
||||||
env_set "APP_PORT" "80"
|
env_set "APP_PORT" "80"
|
||||||
restart_nginx="y"
|
restart_nginx="y"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
error "操作终止"
|
error "$(msg '操作终止')"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
if [[ "$(env_get APP_SSL_PORT)" != "443" ]]; then
|
if [[ "$(env_get APP_SSL_PORT)" != "443" ]]; then
|
||||||
warning "HTTPS服务端口不是443,是否修改并继续操作? [Y/n]"
|
warning "$(msg 'HTTPS服务端口不是443,是否修改并继续操作? [Y/n]')"
|
||||||
read -r continue_https
|
read -r continue_https
|
||||||
[[ -z ${continue_https} ]] && continue_https="Y"
|
[[ -z ${continue_https} ]] && continue_https="Y"
|
||||||
case $continue_https in
|
case $continue_https in
|
||||||
[yY][eE][sS] | [yY])
|
[yY][eE][sS] | [yY])
|
||||||
success "继续操作"
|
success "$(msg '继续操作')"
|
||||||
env_set "APP_SSL_PORT" "443"
|
env_set "APP_SSL_PORT" "443"
|
||||||
restart_nginx="y"
|
restart_nginx="y"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
error "操作终止"
|
error "$(msg '操作终止')"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@ -368,13 +487,13 @@ https_auto() {
|
|||||||
new_job="* 6 * * * docker run --rm -v $(pwd):/work nginx:alpine sh /work/bin/https renew"
|
new_job="* 6 * * * docker run --rm -v $(pwd):/work nginx:alpine sh /work/bin/https renew"
|
||||||
current_crontab=$(crontab -l 2>/dev/null)
|
current_crontab=$(crontab -l 2>/dev/null)
|
||||||
if ! echo "$current_crontab" | grep -v "https renew"; then
|
if ! echo "$current_crontab" | grep -v "https renew"; then
|
||||||
echo "任务已存在,无需添加。"
|
echo "$(msg '任务已存在,无需添加。')"
|
||||||
else
|
else
|
||||||
crontab -l |{
|
crontab -l |{
|
||||||
cat
|
cat
|
||||||
echo "$new_job"
|
echo "$new_job"
|
||||||
} | crontab -
|
} | crontab -
|
||||||
echo "任务已添加。"
|
echo "$(msg '任务已添加。')"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -404,7 +523,7 @@ env_set() {
|
|||||||
docker run $TTY_FLAG --rm -v ${WORK_DIR}:/www nginx:alpine sh -c "sed -i "/^${key}=/c\\${key}=${val}" /www/.env"
|
docker run $TTY_FLAG --rm -v ${WORK_DIR}:/www nginx:alpine sh -c "sed -i "/^${key}=/c\\${key}=${val}" /www/.env"
|
||||||
fi
|
fi
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
error "设置env参数失败!"
|
error "$(msg '设置env参数失败!')"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@ -456,7 +575,8 @@ arg_get() {
|
|||||||
|
|
||||||
# 显示帮助信息
|
# 显示帮助信息
|
||||||
show_help() {
|
show_help() {
|
||||||
cat << 'EOF'
|
if [ "$DT_LANG" = "zh" ]; then
|
||||||
|
cat << 'EOF'
|
||||||
DooTask 管理脚本
|
DooTask 管理脚本
|
||||||
|
|
||||||
用法: ./cmd <命令> [参数]
|
用法: ./cmd <命令> [参数]
|
||||||
@ -504,6 +624,56 @@ DooTask 管理脚本
|
|||||||
./cmd mysql backup 备份数据库
|
./cmd mysql backup 备份数据库
|
||||||
./cmd artisan migrate 执行数据库迁移
|
./cmd artisan migrate 执行数据库迁移
|
||||||
EOF
|
EOF
|
||||||
|
else
|
||||||
|
cat << 'EOF'
|
||||||
|
DooTask Management Script
|
||||||
|
|
||||||
|
Usage: ./cmd <command> [options]
|
||||||
|
|
||||||
|
📦 Core:
|
||||||
|
install Install DooTask (supports --port <port> --relock)
|
||||||
|
update Update DooTask (supports --branch <branch> --force --local)
|
||||||
|
uninstall Uninstall DooTask
|
||||||
|
|
||||||
|
⚙️ Configuration:
|
||||||
|
port <port> Change service port
|
||||||
|
url <address> Change access URL
|
||||||
|
env <key> <value> Set environment variable
|
||||||
|
debug [true|false] Toggle debug mode
|
||||||
|
repassword [username] Reset database password
|
||||||
|
|
||||||
|
🚀 Build:
|
||||||
|
serve, dev Start dev mode
|
||||||
|
build, prod Production build
|
||||||
|
electron Build desktop app
|
||||||
|
|
||||||
|
🔧 Services:
|
||||||
|
up [service] Start containers
|
||||||
|
down [service] Stop containers
|
||||||
|
restart [service] Restart containers
|
||||||
|
reup Rebuild and start
|
||||||
|
|
||||||
|
💾 Database:
|
||||||
|
mysql backup Back up database
|
||||||
|
mysql recovery Restore database
|
||||||
|
|
||||||
|
🛠️ Dev tools:
|
||||||
|
artisan <command> Run Laravel Artisan command
|
||||||
|
composer <command> Run Composer command
|
||||||
|
php <command> Run PHP command
|
||||||
|
|
||||||
|
📚 Others:
|
||||||
|
doc Generate API docs
|
||||||
|
https Configure HTTPS
|
||||||
|
--help, -h Show this help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
./cmd install --port 8080 Install on port 8080
|
||||||
|
./cmd update --branch dev Switch to dev branch and update
|
||||||
|
./cmd mysql backup Back up database
|
||||||
|
./cmd artisan migrate Run database migration
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# 检测APP_ID是否与其他实例冲突
|
# 检测APP_ID是否与其他实例冲突
|
||||||
@ -512,8 +682,8 @@ check_instance() {
|
|||||||
local container_name="dootask-php-${app_id}"
|
local container_name="dootask-php-${app_id}"
|
||||||
local mount_path=$(docker inspect "$container_name" --format '{{range .Mounts}}{{if eq .Destination "/var/www"}}{{.Source}}{{end}}{{end}}' 2>/dev/null)
|
local mount_path=$(docker inspect "$container_name" --format '{{range .Mounts}}{{if eq .Destination "/var/www"}}{{.Source}}{{end}}{{end}}' 2>/dev/null)
|
||||||
if [[ -n "$mount_path" ]] && [[ "$mount_path" != "$WORK_DIR" ]]; then
|
if [[ -n "$mount_path" ]] && [[ "$mount_path" != "$WORK_DIR" ]]; then
|
||||||
error "APP_ID(${app_id})已被其他实例使用:${mount_path}"
|
error "$(msg 'APP_ID((*))已被其他实例使用:(*)' "$app_id" "$mount_path")"
|
||||||
error "请先清空 .env 中的 APP_ID 和 APP_IPPR 再重新安装"
|
error "$(msg '请先清空 .env 中的 APP_ID 和 APP_IPPR 再重新安装')"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@ -525,7 +695,7 @@ check_port() {
|
|||||||
local current_port=$2
|
local current_port=$2
|
||||||
if [[ "$port" -gt 0 ]] && [[ "$port" != "$current_port" ]]; then
|
if [[ "$port" -gt 0 ]] && [[ "$port" != "$current_port" ]]; then
|
||||||
if ! docker run --rm -p "${port}:80" --entrypoint true nginx:alpine 2>/dev/null; then
|
if ! docker run --rm -p "${port}:80" --entrypoint true nginx:alpine 2>/dev/null; then
|
||||||
error "端口 ${port} 已被占用,请指定其他端口"
|
error "$(msg '端口 (*) 已被占用,请指定其他端口' "$port")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@ -570,13 +740,13 @@ handle_install() {
|
|||||||
writable="yes"
|
writable="yes"
|
||||||
docker run --rm ${cmda} nginx:alpine sh -c "${cmdb} touch /usr/share/docker/dootask.lock" &> /dev/null
|
docker run --rm ${cmda} nginx:alpine sh -c "${cmdb} touch /usr/share/docker/dootask.lock" &> /dev/null
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
error "目录权限检测失败!请检查目录权限设置"
|
error "$(msg '目录权限检测失败!请检查目录权限设置')"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
for vol in "${volumes[@]}"; do
|
for vol in "${volumes[@]}"; do
|
||||||
if [ ! -f "${vol}/dootask.lock" ]; then
|
if [ ! -f "${vol}/dootask.lock" ]; then
|
||||||
if [ $remaining -lt 0 ]; then
|
if [ $remaining -lt 0 ]; then
|
||||||
error "目录【${vol}】权限不足!"
|
error "$(msg '目录【(*)】权限不足!' "$vol")"
|
||||||
exit 1
|
exit 1
|
||||||
else
|
else
|
||||||
writable="no"
|
writable="no"
|
||||||
@ -607,28 +777,32 @@ handle_install() {
|
|||||||
$COMPOSE up php -d
|
$COMPOSE up php -d
|
||||||
|
|
||||||
# 安装PHP依赖
|
# 安装PHP依赖
|
||||||
exec_judge "container_exec php 'composer install --optimize-autoloader'" "安装依赖失败"
|
exec_judge "container_exec php 'composer install --optimize-autoloader'" "$(msg '安装依赖失败')"
|
||||||
|
|
||||||
# 最终检查
|
# 最终检查
|
||||||
if [ ! -f "${WORK_DIR}/vendor/autoload.php" ]; then
|
if [ ! -f "${WORK_DIR}/vendor/autoload.php" ]; then
|
||||||
error "安装依赖失败,请重试!"
|
error "$(msg '安装依赖失败,请重试!')"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 生成应用密钥
|
# 生成应用密钥
|
||||||
[[ -z "$(env_get APP_KEY)" ]] && exec_judge "container_exec php 'php artisan key:generate'" "生成密钥失败"
|
[[ -z "$(env_get APP_KEY)" ]] && exec_judge "container_exec php 'php artisan key:generate'" "$(msg '生成密钥失败')"
|
||||||
|
|
||||||
# 设置生产模式
|
# 设置生产模式
|
||||||
switch_debug "false"
|
switch_debug "false"
|
||||||
|
|
||||||
# 数据库迁移
|
# 数据库迁移
|
||||||
exec_judge "container_exec php 'php artisan migrate --seed'" "数据库迁移失败"
|
exec_judge "container_exec php 'php artisan migrate --seed'" "$(msg '数据库迁移失败')"
|
||||||
|
|
||||||
# 启动所有容器
|
# 启动所有容器
|
||||||
$COMPOSE up -d --remove-orphans
|
$COMPOSE up -d --remove-orphans
|
||||||
|
|
||||||
success "安装完成"
|
# 兜底拉起 nginx(避免首启时序竞态)
|
||||||
echo -e "地址: http://${GreenBG}127.0.0.1:$(env_get APP_PORT)${Font}"
|
wait_php_healthy
|
||||||
|
[ -z "$(docker_name nginx)" ] && $COMPOSE up -d --remove-orphans
|
||||||
|
|
||||||
|
success "$(msg '安装完成')"
|
||||||
|
echo -e "$(msg '地址'): http://${GreenBG}127.0.0.1:$(env_get APP_PORT)${Font}"
|
||||||
container_exec mariadb "sh /etc/mysql/repassword.sh"
|
container_exec mariadb "sh /etc/mysql/repassword.sh"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -642,7 +816,7 @@ handle_update() {
|
|||||||
|
|
||||||
# 检查是否已经安装
|
# 检查是否已经安装
|
||||||
if [ ! -f "${WORK_DIR}/vendor/autoload.php" ]; then
|
if [ ! -f "${WORK_DIR}/vendor/autoload.php" ]; then
|
||||||
error "请先执行安装命令"
|
error "$(msg '请先执行安装命令')"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -652,10 +826,13 @@ handle_update() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z "$is_local" ]]; then
|
if [[ -z "$is_local" ]]; then
|
||||||
|
# 信任项目目录,避免 git 归属检查拦截
|
||||||
|
git config --global --get-all safe.directory 2>/dev/null | grep -qxF "${WORK_DIR}" \
|
||||||
|
|| git config --global --add safe.directory "${WORK_DIR}" 2>/dev/null
|
||||||
# 检查本地修改
|
# 检查本地修改
|
||||||
if ! git diff --quiet || ! git diff --cached --quiet; then
|
if ! git diff --quiet || ! git diff --cached --quiet; then
|
||||||
if [[ "$force_update" != "yes" ]]; then
|
if [[ "$force_update" != "yes" ]]; then
|
||||||
warning "检测到本地修改,是否强制更新?[Y/n]"
|
warning "$(msg '检测到本地修改,是否强制更新?[Y/n]')"
|
||||||
read -r confirm_force
|
read -r confirm_force
|
||||||
[[ -z ${confirm_force} ]] && confirm_force="Y"
|
[[ -z ${confirm_force} ]] && confirm_force="Y"
|
||||||
case $confirm_force in
|
case $confirm_force in
|
||||||
@ -663,7 +840,7 @@ handle_update() {
|
|||||||
force_update="yes"
|
force_update="yes"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
error "取消更新,请先处理本地修改"
|
error "$(msg '取消更新,请先处理本地修改')"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@ -671,21 +848,21 @@ handle_update() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# 远程更新模式
|
# 远程更新模式
|
||||||
exec_judge "git fetch --all" "获取远程更新失败"
|
exec_judge "git fetch --all" "$(msg '获取远程更新失败')"
|
||||||
|
|
||||||
# 确定目标分支
|
# 确定目标分支
|
||||||
if [[ -n "$target_branch" ]]; then
|
if [[ -n "$target_branch" ]]; then
|
||||||
current_branch="$target_branch"
|
current_branch="$target_branch"
|
||||||
if ! git config --get "branch.${current_branch}.remote" | grep -q "origin"; then
|
if ! git config --get "branch.${current_branch}.remote" | grep -q "origin"; then
|
||||||
exec_judge "git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'" "设置远程Fetch配置失败"
|
exec_judge "git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'" "$(msg '设置远程Fetch配置失败')"
|
||||||
fi
|
fi
|
||||||
if ! git show-ref --verify --quiet refs/heads/${current_branch}; then
|
if ! git show-ref --verify --quiet refs/heads/${current_branch}; then
|
||||||
exec_judge "git fetch origin ${current_branch}:${current_branch}" "获取远程分支 ${current_branch} 失败"
|
exec_judge "git fetch origin ${current_branch}:${current_branch}" "$(msg '获取远程分支 (*) 失败' "$current_branch")"
|
||||||
fi
|
fi
|
||||||
if [[ "$force_update" == "yes" ]]; then
|
if [[ "$force_update" == "yes" ]]; then
|
||||||
exec_judge "git checkout -f ${current_branch}" "切换分支到 ${current_branch} 失败"
|
exec_judge "git checkout -f ${current_branch}" "$(msg '切换分支到 (*) 失败' "$current_branch")"
|
||||||
else
|
else
|
||||||
exec_judge "git checkout ${current_branch}" "切换分支到 ${current_branch} 失败"
|
exec_judge "git checkout ${current_branch}" "$(msg '切换分支到 (*) 失败' "$current_branch")"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
current_branch=$(git branch | sed -n -e 's/^\* \(.*\)/\1/p')
|
current_branch=$(git branch | sed -n -e 's/^\* \(.*\)/\1/p')
|
||||||
@ -694,27 +871,27 @@ handle_update() {
|
|||||||
# 检查数据库迁移变动
|
# 检查数据库迁移变动
|
||||||
db_changes=$(git diff --name-only HEAD..origin/${current_branch} 2>/dev/null | grep -E "^database/" || true)
|
db_changes=$(git diff --name-only HEAD..origin/${current_branch} 2>/dev/null | grep -E "^database/" || true)
|
||||||
if [[ -n "$db_changes" ]]; then
|
if [[ -n "$db_changes" ]]; then
|
||||||
echo "数据库有迁移变动,执行数据库备份..."
|
echo "$(msg '数据库有迁移变动,执行数据库备份...')"
|
||||||
exec_judge "mysql_snapshot backup" "数据库备份失败" "数据库备份完成"
|
exec_judge "mysql_snapshot backup" "$(msg '数据库备份失败')" "$(msg '数据库备份完成')"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 更新代码
|
# 更新代码
|
||||||
if [[ "$force_update" == "yes" ]]; then
|
if [[ "$force_update" == "yes" ]]; then
|
||||||
exec_judge "git reset --hard origin/${current_branch}" "强制更新代码失败"
|
exec_judge "git reset --hard origin/${current_branch}" "$(msg '强制更新代码失败')"
|
||||||
else
|
else
|
||||||
exec_judge "git pull --ff-only origin ${current_branch}" "代码拉取失败,可能存在冲突,请使用 --force 参数"
|
exec_judge "git pull --ff-only origin ${current_branch}" "$(msg '代码拉取失败,可能存在冲突,请使用 --force 参数')"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 更新依赖
|
# 更新依赖
|
||||||
exec_judge "container_exec php 'composer install --optimize-autoloader'" "更新PHP依赖失败"
|
exec_judge "container_run php 'composer install --optimize-autoloader'" "$(msg '更新PHP依赖失败')"
|
||||||
else
|
else
|
||||||
# 本地更新模式
|
# 本地更新模式
|
||||||
echo "执行数据库备份..."
|
echo "$(msg '执行数据库备份...')"
|
||||||
exec_judge "mysql_snapshot backup" "数据库备份失败" "数据库备份完成"
|
exec_judge "mysql_snapshot backup" "$(msg '数据库备份失败')" "$(msg '数据库备份完成')"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 数据库迁移
|
# 数据库迁移
|
||||||
exec_judge "container_exec php 'php artisan migrate'" "数据库迁移失败"
|
exec_judge "container_run php 'php artisan migrate'" "$(msg '数据库迁移失败')"
|
||||||
|
|
||||||
# 停止服务
|
# 停止服务
|
||||||
$COMPOSE stop php nginx &> /dev/null
|
$COMPOSE stop php nginx &> /dev/null
|
||||||
@ -724,30 +901,34 @@ handle_update() {
|
|||||||
$COMPOSE up -d --remove-orphans
|
$COMPOSE up -d --remove-orphans
|
||||||
if [[ 0 -ne $? ]]; then
|
if [[ 0 -ne $? ]]; then
|
||||||
$COMPOSE down --remove-orphans
|
$COMPOSE down --remove-orphans
|
||||||
exec_judge "$COMPOSE up -d" "重启服务失败"
|
exec_judge "$COMPOSE up -d" "$(msg '重启服务失败')"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 兜底拉起 nginx(避免首启时序竞态)
|
||||||
|
wait_php_healthy
|
||||||
|
[ -z "$(docker_name nginx)" ] && $COMPOSE up -d --remove-orphans
|
||||||
|
|
||||||
env_set UPDATE_TIME "$(date +%s)"
|
env_set UPDATE_TIME "$(date +%s)"
|
||||||
success "更新完成"
|
success "$(msg '更新完成')"
|
||||||
}
|
}
|
||||||
|
|
||||||
# 卸载函数
|
# 卸载函数
|
||||||
handle_uninstall() {
|
handle_uninstall() {
|
||||||
check_sudo
|
check_sudo
|
||||||
# 确认卸载
|
# 确认卸载
|
||||||
echo -e "${RedBG}警告:此操作将永久删除以下内容:${Font}"
|
echo -e "${RedBG}$(msg '警告:此操作将永久删除以下内容:')${Font}"
|
||||||
echo "- 数据库"
|
echo "$(msg '- 数据库')"
|
||||||
echo "- 应用程序"
|
echo "$(msg '- 应用程序')"
|
||||||
echo "- 日志文件"
|
echo "$(msg '- 日志文件')"
|
||||||
echo ""
|
echo ""
|
||||||
read -rp "确认要继续卸载吗?(y/N): " confirm_uninstall
|
read -rp "$(msg '确认要继续卸载吗?(y/N): ')" confirm_uninstall
|
||||||
[[ -z ${confirm_uninstall} ]] && confirm_uninstall="N"
|
[[ -z ${confirm_uninstall} ]] && confirm_uninstall="N"
|
||||||
case $confirm_uninstall in
|
case $confirm_uninstall in
|
||||||
[yY][eE][sS] | [yY])
|
[yY][eE][sS] | [yY])
|
||||||
echo -e "${RedBG}开始卸载...${Font}"
|
echo -e "${RedBG}$(msg '开始卸载...')${Font}"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo -e "${GreenBG}终止卸载。${Font}"
|
echo -e "${GreenBG}$(msg '终止卸载。')${Font}"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@ -755,8 +936,8 @@ handle_uninstall() {
|
|||||||
# 清理网络相关容器
|
# 清理网络相关容器
|
||||||
remove_by_network
|
remove_by_network
|
||||||
|
|
||||||
# 停止并删除容器
|
# 停止并删除容器(含命名卷)
|
||||||
$COMPOSE down --remove-orphans
|
$COMPOSE down --remove-orphans --volumes
|
||||||
|
|
||||||
# 重置调试模式
|
# 重置调试模式
|
||||||
env_set APP_DEBUG "false"
|
env_set APP_DEBUG "false"
|
||||||
@ -768,7 +949,7 @@ handle_uninstall() {
|
|||||||
find "./docker/appstore/log" -name "*.log" -delete 2>/dev/null
|
find "./docker/appstore/log" -name "*.log" -delete 2>/dev/null
|
||||||
find "./storage/logs" -name "*.log" -delete 2>/dev/null
|
find "./storage/logs" -name "*.log" -delete 2>/dev/null
|
||||||
|
|
||||||
success "卸载完成"
|
success "$(msg '卸载完成')"
|
||||||
}
|
}
|
||||||
|
|
||||||
####################################################################################
|
####################################################################################
|
||||||
@ -806,14 +987,14 @@ case "$1" in
|
|||||||
check_port "$1" "$(env_get APP_PORT)"
|
check_port "$1" "$(env_get APP_PORT)"
|
||||||
env_set APP_PORT "$1"
|
env_set APP_PORT "$1"
|
||||||
$COMPOSE up -d
|
$COMPOSE up -d
|
||||||
success "修改成功"
|
success "$(msg '修改成功')"
|
||||||
echo -e "地址: http://${GreenBG}127.0.0.1:$(env_get APP_PORT)${Font}"
|
echo -e "$(msg '地址'): http://${GreenBG}127.0.0.1:$(env_get APP_PORT)${Font}"
|
||||||
;;
|
;;
|
||||||
"url")
|
"url")
|
||||||
shift 1
|
shift 1
|
||||||
env_set APP_URL "$1"
|
env_set APP_URL "$1"
|
||||||
restart_php
|
restart_php
|
||||||
success "修改成功"
|
success "$(msg '修改成功')"
|
||||||
;;
|
;;
|
||||||
"env")
|
"env")
|
||||||
shift 1
|
shift 1
|
||||||
@ -821,7 +1002,7 @@ case "$1" in
|
|||||||
env_set $1 "$2"
|
env_set $1 "$2"
|
||||||
fi
|
fi
|
||||||
restart_php
|
restart_php
|
||||||
success "修改成功"
|
success "$(msg '修改成功')"
|
||||||
;;
|
;;
|
||||||
"repassword")
|
"repassword")
|
||||||
shift 1
|
shift 1
|
||||||
@ -882,7 +1063,7 @@ case "$1" in
|
|||||||
else
|
else
|
||||||
https_auto
|
https_auto
|
||||||
fi
|
fi
|
||||||
restart_php
|
$COMPOSE up -d
|
||||||
;;
|
;;
|
||||||
"artisan")
|
"artisan")
|
||||||
shift 1
|
shift 1
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.0",
|
"php": "^8.3",
|
||||||
"ext-curl": "*",
|
"ext-curl": "*",
|
||||||
"ext-dom": "*",
|
"ext-dom": "*",
|
||||||
"ext-ffi": "*",
|
"ext-ffi": "*",
|
||||||
@ -20,40 +20,36 @@
|
|||||||
"ext-openssl": "*",
|
"ext-openssl": "*",
|
||||||
"ext-simplexml": "*",
|
"ext-simplexml": "*",
|
||||||
"ext-zip": "*",
|
"ext-zip": "*",
|
||||||
"directorytree/ldaprecord-laravel": "^2.7",
|
"directorytree/ldaprecord-laravel": "^4.0",
|
||||||
"fideloper/proxy": "^4.4.1",
|
"firebase/php-jwt": "^7.1",
|
||||||
"firebase/php-jwt": "^6.9",
|
|
||||||
"fruitcake/laravel-cors": "^2.0.4",
|
|
||||||
"guanguans/notify": "^1.21.1",
|
|
||||||
"guzzlehttp/guzzle": "^7.3.0",
|
"guzzlehttp/guzzle": "^7.3.0",
|
||||||
"hedeqiang/umeng": "^2.1",
|
"hedeqiang/umeng": "^2.1",
|
||||||
"laravel/framework": "^v8.83.27",
|
"laravel/framework": "^13.0",
|
||||||
"laravel/tinker": "^v2.6.1",
|
"laravel/tinker": "^3.0",
|
||||||
"laravolt/avatar": "^5.1",
|
"laravolt/avatar": "^6.5",
|
||||||
"league/commonmark": "^2.5",
|
"league/commonmark": "^2.5",
|
||||||
"league/html-to-markdown": "^5.1",
|
"league/html-to-markdown": "^5.1",
|
||||||
"maatwebsite/excel": "^3.1.31",
|
"maatwebsite/excel": "^3.1.69",
|
||||||
"madnest/madzipper": "^v1.1.0",
|
|
||||||
"matomo/device-detector": "^6.4",
|
"matomo/device-detector": "^6.4",
|
||||||
"mews/captcha": "^3.2.6",
|
"mews/captcha": "^3.5",
|
||||||
"orangehill/iseed": "^3.0.1",
|
"orangehill/iseed": "^3.8",
|
||||||
"overtrue/pinyin": "^4.0",
|
"overtrue/pinyin": "^5.3",
|
||||||
"phpoffice/phppresentation": "^1.1",
|
"phpoffice/phppresentation": "^1.2",
|
||||||
"phpoffice/phpword": "^1.3",
|
"phpoffice/phpword": "^1.4",
|
||||||
"predis/predis": "^1.1.7",
|
"predis/predis": "^2.3",
|
||||||
"smalot/pdfparser": "^2.11",
|
"smalot/pdfparser": "^2.11",
|
||||||
"symfony/mailer": "^6.0"
|
"symfony/console": "^7.4",
|
||||||
|
"symfony/yaml": "^7.4"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"barryvdh/laravel-ide-helper": "^v2.10.0",
|
"barryvdh/laravel-ide-helper": "^3.7",
|
||||||
"facade/ignition": "^2.10.2",
|
"fakerphp/faker": "^1.24",
|
||||||
"fakerphp/faker": "^v1.14.1",
|
"hhxsv5/laravel-s": "~3.8.0",
|
||||||
"hhxsv5/laravel-s": "^v3.7.19",
|
"kitloong/laravel-migrations-generator": "^7.4",
|
||||||
"kitloong/laravel-migrations-generator": "^4.4.2",
|
"larastan/larastan": "^3.10",
|
||||||
"laravel/sail": "^v1.8.1",
|
"mockery/mockery": "^1.6",
|
||||||
"mockery/mockery": "^1.4.3",
|
"nunomaduro/collision": "^8.6",
|
||||||
"nunomaduro/collision": "^v5.5.0",
|
"phpunit/phpunit": "^11.5"
|
||||||
"phpunit/phpunit": "^9.5.6"
|
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
@ -80,7 +76,9 @@
|
|||||||
],
|
],
|
||||||
"post-create-project-cmd": [
|
"post-create-project-cmd": [
|
||||||
"@php artisan key:generate --ansi"
|
"@php artisan key:generate --ansi"
|
||||||
]
|
],
|
||||||
|
"stan": "phpstan analyse --no-progress --memory-limit=-1",
|
||||||
|
"stan-baseline": "phpstan analyse --generate-baseline --memory-limit=-1"
|
||||||
},
|
},
|
||||||
"extra": {
|
"extra": {
|
||||||
"laravel": {
|
"laravel": {
|
||||||
@ -95,7 +93,7 @@
|
|||||||
"php-http/discovery": true
|
"php-http/discovery": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"minimum-stability": "dev",
|
"minimum-stability": "stable",
|
||||||
"prefer-stable": true,
|
"prefer-stable": true,
|
||||||
"repositories": {
|
"repositories": {
|
||||||
}
|
}
|
||||||
|
|||||||
5716
composer.lock
generated
5716
composer.lock
generated
File diff suppressed because it is too large
Load Diff
48
config/ai.php
Normal file
48
config/ai.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| DooTask AI 助手灰度配置
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| RAG(帮助知识库检索)功能上线时按以下顺序灰度:
|
||||||
|
| Stage 1 — staging:RAG_ENABLED=true 仅 staging 环境,全体可用
|
||||||
|
| Stage 2 — canary:RAG_ENABLED=true + RAG_CANARY_USERIDS="1,2,3,4,5"
|
||||||
|
| 仅白名单 user 命中 RAG
|
||||||
|
| Stage 3 — broad:清空 RAG_CANARY_USERIDS,全局启用
|
||||||
|
|
|
||||||
|
| 紧急关停(kill switch,5 分钟生效):
|
||||||
|
| 1) 改容器 env RAG_ENABLED=false
|
||||||
|
| 2) ./cmd php restart 让 swoole 重读 config
|
||||||
|
| 3) AI 容器收到 rag_enabled=0 时跳过 RAG hint 注入与 search_help_docs 工具挂载
|
||||||
|
|
|
||||||
|
| 灰度判定语义:
|
||||||
|
| rag_enabled (env total switch)
|
||||||
|
| ├─ false → 所有人都不走 RAG(kill switch)
|
||||||
|
| └─ true → 进一步看 canary:
|
||||||
|
| ├─ rag_canary_userids 为空(默认)→ 全员启用
|
||||||
|
| └─ rag_canary_userids 有值 → 仅白名单 userid 启用
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| RAG 总开关
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| true - 默认开启,按 canary 白名单进一步过滤
|
||||||
|
| false - 紧急 kill switch,所有用户都不走 RAG
|
||||||
|
*/
|
||||||
|
'rag_enabled' => filter_var(env('RAG_ENABLED', true), FILTER_VALIDATE_BOOLEAN),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| RAG canary 白名单
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 逗号分隔的 userid 列表。
|
||||||
|
| 留空表示 全员启用(Stage 3 broad rollout)。
|
||||||
|
| 有值表示 仅白名单 userid 命中 RAG(Stage 2 canary)。
|
||||||
|
*/
|
||||||
|
'rag_canary_userids' => env('RAG_CANARY_USERIDS', ''),
|
||||||
|
];
|
||||||
107
config/app.php
107
config/app.php
@ -123,111 +123,4 @@ return [
|
|||||||
|
|
||||||
'cipher' => 'AES-256-CBC',
|
'cipher' => 'AES-256-CBC',
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Autoloaded Service Providers
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| The service providers listed here will be automatically loaded on the
|
|
||||||
| request to your application. Feel free to add your own services to
|
|
||||||
| this array to grant expanded functionality to your applications.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'providers' => [
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Laravel Framework Service Providers...
|
|
||||||
*/
|
|
||||||
Illuminate\Auth\AuthServiceProvider::class,
|
|
||||||
Illuminate\Broadcasting\BroadcastServiceProvider::class,
|
|
||||||
Illuminate\Bus\BusServiceProvider::class,
|
|
||||||
Illuminate\Cache\CacheServiceProvider::class,
|
|
||||||
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
|
|
||||||
Illuminate\Cookie\CookieServiceProvider::class,
|
|
||||||
Illuminate\Database\DatabaseServiceProvider::class,
|
|
||||||
Illuminate\Encryption\EncryptionServiceProvider::class,
|
|
||||||
Illuminate\Filesystem\FilesystemServiceProvider::class,
|
|
||||||
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
|
|
||||||
Illuminate\Hashing\HashServiceProvider::class,
|
|
||||||
Illuminate\Mail\MailServiceProvider::class,
|
|
||||||
Illuminate\Notifications\NotificationServiceProvider::class,
|
|
||||||
Illuminate\Pagination\PaginationServiceProvider::class,
|
|
||||||
Illuminate\Pipeline\PipelineServiceProvider::class,
|
|
||||||
Illuminate\Queue\QueueServiceProvider::class,
|
|
||||||
Illuminate\Redis\RedisServiceProvider::class,
|
|
||||||
Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
|
|
||||||
Illuminate\Session\SessionServiceProvider::class,
|
|
||||||
Illuminate\Translation\TranslationServiceProvider::class,
|
|
||||||
Illuminate\Validation\ValidationServiceProvider::class,
|
|
||||||
Illuminate\View\ViewServiceProvider::class,
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Package Service Providers...
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Application Service Providers...
|
|
||||||
*/
|
|
||||||
App\Providers\AppServiceProvider::class,
|
|
||||||
App\Providers\AuthServiceProvider::class,
|
|
||||||
// App\Providers\BroadcastServiceProvider::class,
|
|
||||||
App\Providers\EventServiceProvider::class,
|
|
||||||
App\Providers\RouteServiceProvider::class,
|
|
||||||
|
|
||||||
],
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Class Aliases
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This array of class aliases will be registered when this application
|
|
||||||
| is started. However, feel free to register as many as you wish as
|
|
||||||
| the aliases are "lazy" loaded so they don't hinder performance.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'aliases' => [
|
|
||||||
|
|
||||||
'App' => Illuminate\Support\Facades\App::class,
|
|
||||||
'Arr' => Illuminate\Support\Arr::class,
|
|
||||||
'Artisan' => Illuminate\Support\Facades\Artisan::class,
|
|
||||||
'Auth' => Illuminate\Support\Facades\Auth::class,
|
|
||||||
'Blade' => Illuminate\Support\Facades\Blade::class,
|
|
||||||
'Broadcast' => Illuminate\Support\Facades\Broadcast::class,
|
|
||||||
'Bus' => Illuminate\Support\Facades\Bus::class,
|
|
||||||
'Cache' => Illuminate\Support\Facades\Cache::class,
|
|
||||||
'Config' => Illuminate\Support\Facades\Config::class,
|
|
||||||
'Cookie' => Illuminate\Support\Facades\Cookie::class,
|
|
||||||
'Crypt' => Illuminate\Support\Facades\Crypt::class,
|
|
||||||
'Date' => Illuminate\Support\Facades\Date::class,
|
|
||||||
'DB' => Illuminate\Support\Facades\DB::class,
|
|
||||||
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
|
|
||||||
'Event' => Illuminate\Support\Facades\Event::class,
|
|
||||||
'File' => Illuminate\Support\Facades\File::class,
|
|
||||||
'Gate' => Illuminate\Support\Facades\Gate::class,
|
|
||||||
'Hash' => Illuminate\Support\Facades\Hash::class,
|
|
||||||
'Http' => Illuminate\Support\Facades\Http::class,
|
|
||||||
'Lang' => Illuminate\Support\Facades\Lang::class,
|
|
||||||
'Log' => Illuminate\Support\Facades\Log::class,
|
|
||||||
'Mail' => Illuminate\Support\Facades\Mail::class,
|
|
||||||
'Notification' => Illuminate\Support\Facades\Notification::class,
|
|
||||||
'Password' => Illuminate\Support\Facades\Password::class,
|
|
||||||
'Queue' => Illuminate\Support\Facades\Queue::class,
|
|
||||||
'Redirect' => Illuminate\Support\Facades\Redirect::class,
|
|
||||||
// 'Redis' => Illuminate\Support\Facades\Redis::class,
|
|
||||||
'Request' => Illuminate\Support\Facades\Request::class,
|
|
||||||
'Response' => Illuminate\Support\Facades\Response::class,
|
|
||||||
'Route' => Illuminate\Support\Facades\Route::class,
|
|
||||||
'Schema' => Illuminate\Support\Facades\Schema::class,
|
|
||||||
'Session' => Illuminate\Support\Facades\Session::class,
|
|
||||||
'Storage' => Illuminate\Support\Facades\Storage::class,
|
|
||||||
'Str' => Illuminate\Support\Str::class,
|
|
||||||
'URL' => Illuminate\Support\Facades\URL::class,
|
|
||||||
'Validator' => Illuminate\Support\Facades\Validator::class,
|
|
||||||
'View' => Illuminate\Support\Facades\View::class,
|
|
||||||
|
|
||||||
],
|
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
48
config/dootask.php
Normal file
48
config/dootask.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
// 系统设置开关:设为 'disabled' 时禁止通过接口修改系统设置(SystemController)
|
||||||
|
'system_setting' => env('SYSTEM_SETTING'),
|
||||||
|
|
||||||
|
// 许可证显示开关:设为 'hidden' 时隐藏系统许可证信息(Doo::license)
|
||||||
|
'system_license' => env('SYSTEM_LICENSE'),
|
||||||
|
|
||||||
|
// 演示账号:登录页展示的演示账号(SystemController::demo)
|
||||||
|
'demo_account' => env('DEMO_ACCOUNT'),
|
||||||
|
|
||||||
|
// 演示密码:登录页展示的演示账号密码(SystemController::demo)
|
||||||
|
'demo_password' => env('DEMO_PASSWORD'),
|
||||||
|
|
||||||
|
// 管理员密码修改开关:设为 'disabled' 时禁止修改管理员密码(User 模型)
|
||||||
|
'password_admin' => env('PASSWORD_ADMIN'),
|
||||||
|
|
||||||
|
// 创始人密码修改开关:设为 'disabled' 时禁止修改创始人密码(User 模型)
|
||||||
|
'password_owner' => env('PASSWORD_OWNER'),
|
||||||
|
|
||||||
|
// Manticore 全文搜索服务主机(ManticoreBase)
|
||||||
|
'search_host' => env('SEARCH_HOST', 'search'),
|
||||||
|
|
||||||
|
// Manticore 全文搜索服务端口(ManticoreBase)
|
||||||
|
'search_port' => env('SEARCH_PORT', 9306),
|
||||||
|
|
||||||
|
// 文件回收站自动清空天数(DeleteTmpTask)
|
||||||
|
'auto_empty_file_recycle' => env('AUTO_EMPTY_FILE_RECYCLE', 365),
|
||||||
|
|
||||||
|
// 临时文件自动清理天数(DeleteTmpTask)
|
||||||
|
'auto_empty_temp_file' => env('AUTO_EMPTY_TEMP_FILE', 30),
|
||||||
|
|
||||||
|
// 在线授权:appstore 授权中心地址(OnlineLicense;默认中央,测试可指向 dev appstore)
|
||||||
|
// [调试中] 临时指向本地 dev appstore,发版前改回 'https://appstore.dootask.com'
|
||||||
|
'online_license_appstore_url' => env('ONLINE_LICENSE_APPSTORE_URL', 'https://appstore.dootask.com'),
|
||||||
|
|
||||||
|
// 在线授权:租约剩余不足该天数时触发续期(OnlineLicense)
|
||||||
|
'online_license_renew_within_days' => env('ONLINE_LICENSE_RENEW_WITHIN_DAYS', 20),
|
||||||
|
|
||||||
|
// 在线授权:租约剩余不足该天数时在提醒(OnlineLicense)
|
||||||
|
'online_license_warn_days' => env('ONLINE_LICENSE_WARN_DAYS', 7),
|
||||||
|
|
||||||
|
// 在线授权:冻结(租约过期)后到吊销的宽限天数(OnlineLicense)
|
||||||
|
'online_license_grace_days' => env('ONLINE_LICENSE_GRACE_DAYS', 14),
|
||||||
|
|
||||||
|
];
|
||||||
@ -198,6 +198,9 @@ return [
|
|||||||
'jobs' => [
|
'jobs' => [
|
||||||
// Enable LaravelScheduleJob to run `php artisan schedule:run` every 1 minute, replace Linux Crontab
|
// Enable LaravelScheduleJob to run `php artisan schedule:run` every 1 minute, replace Linux Crontab
|
||||||
// Hhxsv5\LaravelS\Illuminate\LaravelScheduleJob::class,
|
// Hhxsv5\LaravelS\Illuminate\LaravelScheduleJob::class,
|
||||||
|
|
||||||
|
// 在线授权续期改由容器内独立进程跑(supervisor [program:license] + artisan online-license:renew),
|
||||||
|
// 不再依赖 LARAVELS_TIMER;见 docker/php/license.conf
|
||||||
],
|
],
|
||||||
|
|
||||||
// Max waiting time of reloading
|
// Max waiting time of reloading
|
||||||
|
|||||||
@ -35,8 +35,9 @@ return [
|
|||||||
'port' => env('LDAP_PORT', 389),
|
'port' => env('LDAP_PORT', 389),
|
||||||
'base_dn' => env('LDAP_BASE_DN', 'dc=local,dc=com'),
|
'base_dn' => env('LDAP_BASE_DN', 'dc=local,dc=com'),
|
||||||
'timeout' => env('LDAP_TIMEOUT', 5),
|
'timeout' => env('LDAP_TIMEOUT', 5),
|
||||||
'use_ssl' => env('LDAP_SSL', false),
|
// LdapRecord v4:use_tls=ldaps(沿用旧 LDAP_SSL 变量),use_starttls=StartTLS(沿用旧 LDAP_TLS 变量)
|
||||||
'use_tls' => env('LDAP_TLS', false),
|
'use_tls' => env('LDAP_SSL', false),
|
||||||
|
'use_starttls' => env('LDAP_TLS', false),
|
||||||
],
|
],
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|||||||
@ -23,18 +23,19 @@ class UpdateOwnerAddIndexSome20231217 extends Migration
|
|||||||
$table->index('project_id');
|
$table->index('project_id');
|
||||||
$table->index(['project_id','userid']);
|
$table->index(['project_id','userid']);
|
||||||
$table->index('owner');
|
$table->index('owner');
|
||||||
$table->integer('owner')->change();
|
// Laravel 11+ 的 change() 会丢弃未声明的修饰符,须重申 nullable/default/comment
|
||||||
|
$table->integer('owner')->nullable()->default(0)->comment('是否负责人')->change();
|
||||||
});
|
});
|
||||||
Schema::table('project_tasks', function (Blueprint $table) {
|
Schema::table('project_tasks', function (Blueprint $table) {
|
||||||
$table->index('parent_id');
|
$table->index('parent_id');
|
||||||
$table->index('dialog_id');
|
$table->index('dialog_id');
|
||||||
$table->index('userid');
|
$table->index('userid');
|
||||||
$table->integer('visibility')->change();
|
$table->integer('visibility')->nullable()->default(1)->comment('任务可见性:1-项目人员 2-任务人员 3-指定成员')->change();
|
||||||
});
|
});
|
||||||
Schema::table('project_task_users', function (Blueprint $table) {
|
Schema::table('project_task_users', function (Blueprint $table) {
|
||||||
$table->index(['task_id','userid']);
|
$table->index(['task_id','userid']);
|
||||||
$table->index('owner');
|
$table->index('owner');
|
||||||
$table->integer('owner')->change();
|
$table->integer('owner')->nullable()->default(0)->comment('是否任务负责人')->change();
|
||||||
});
|
});
|
||||||
Schema::table('project_task_files', function (Blueprint $table) {
|
Schema::table('project_task_files', function (Blueprint $table) {
|
||||||
$table->index('project_id');
|
$table->index('project_id');
|
||||||
@ -63,16 +64,16 @@ class UpdateOwnerAddIndexSome20231217 extends Migration
|
|||||||
$table->index('link_id');
|
$table->index('link_id');
|
||||||
});
|
});
|
||||||
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
|
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
|
||||||
$table->integer('link')->change();
|
$table->integer('link')->nullable()->default(0)->comment('是否存在链接')->change();
|
||||||
$table->integer('modify')->change();
|
$table->integer('modify')->nullable()->default(0)->comment('是否编辑')->change();
|
||||||
$table->integer('forward_show')->change();
|
$table->integer('forward_show')->nullable()->default(1)->comment('是否显示转发的来源')->change();
|
||||||
});
|
});
|
||||||
Schema::table('web_socket_dialog_users', function (Blueprint $table) {
|
Schema::table('web_socket_dialog_users', function (Blueprint $table) {
|
||||||
$table->index('dialog_id');
|
$table->index('dialog_id');
|
||||||
$table->index('userid');
|
$table->index('userid');
|
||||||
$table->integer('mark_unread')->change();
|
$table->integer('mark_unread')->nullable()->default(0)->comment('是否标记为未读:0否,1是')->change();
|
||||||
$table->integer('silence')->change();
|
$table->integer('silence')->nullable()->default(0)->comment('是否免打扰:0否,1是')->change();
|
||||||
$table->integer('important')->change();
|
$table->integer('important')->nullable()->default(0)->comment('是否不可移出(项目、任务、部门人员)')->change();
|
||||||
});
|
});
|
||||||
Schema::table('web_socket_dialog_msg_todos', function (Blueprint $table) {
|
Schema::table('web_socket_dialog_msg_todos', function (Blueprint $table) {
|
||||||
$table->index('msg_id');
|
$table->index('msg_id');
|
||||||
@ -80,22 +81,22 @@ class UpdateOwnerAddIndexSome20231217 extends Migration
|
|||||||
});
|
});
|
||||||
Schema::table('web_socket_dialog_msg_reads', function (Blueprint $table) {
|
Schema::table('web_socket_dialog_msg_reads', function (Blueprint $table) {
|
||||||
$table->index('dialog_id');
|
$table->index('dialog_id');
|
||||||
$table->integer('mention')->change();
|
$table->integer('mention')->nullable()->default(0)->comment('是否提及(被@)')->change();
|
||||||
$table->integer('silence')->change();
|
$table->integer('silence')->nullable()->default(0)->comment('是否免打扰:0否,1是')->change();
|
||||||
$table->integer('email')->change();
|
$table->integer('email')->nullable()->default(0)->comment('是否发了邮件')->change();
|
||||||
$table->integer('after')->change();
|
$table->integer('after')->nullable()->default(0)->comment('在阅读之后才添加的记录')->change();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 文件相关
|
// 文件相关
|
||||||
Schema::table('files', function (Blueprint $table) {
|
Schema::table('files', function (Blueprint $table) {
|
||||||
$table->index('pid');
|
$table->index('pid');
|
||||||
$table->index('cid');
|
$table->index('cid');
|
||||||
$table->integer('share')->change();
|
$table->integer('share')->nullable()->default(0)->comment('是否共享')->change();
|
||||||
});
|
});
|
||||||
Schema::table('file_users', function (Blueprint $table) {
|
Schema::table('file_users', function (Blueprint $table) {
|
||||||
$table->index('file_id');
|
$table->index('file_id');
|
||||||
$table->index('userid');
|
$table->index('userid');
|
||||||
$table->integer('permission')->change();
|
$table->integer('permission')->nullable()->default(0)->comment('权限:0只读,1读写')->change();
|
||||||
});
|
});
|
||||||
Schema::table('file_links', function (Blueprint $table) {
|
Schema::table('file_links', function (Blueprint $table) {
|
||||||
$table->index('file_id');
|
$table->index('file_id');
|
||||||
|
|||||||
@ -14,7 +14,8 @@ class UpdateFilesNameLengthTo200 extends Migration
|
|||||||
public function up()
|
public function up()
|
||||||
{
|
{
|
||||||
Schema::table('files', function (Blueprint $table) {
|
Schema::table('files', function (Blueprint $table) {
|
||||||
$table->string('name', 255)->change();
|
// Laravel 11+ 的 change() 会丢弃未声明的修饰符,须重申 nullable/default/comment
|
||||||
|
$table->string('name', 255)->nullable()->default('')->comment('名称')->change();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateAiAssistantSearchLogsTable extends Migration
|
||||||
|
{
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
if (Schema::hasTable('ai_assistant_search_logs')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Schema::create('ai_assistant_search_logs', function (Blueprint $table) {
|
||||||
|
$table->bigIncrements('id');
|
||||||
|
$table->bigInteger('userid')->default(0)->comment('用户ID(token推导)');
|
||||||
|
$table->bigInteger('dialog_id')->default(0)->comment('对话ID(chat流程;invoke流程为0)');
|
||||||
|
$table->string('context_key', 191)->default('')->comment('上下文标识(chat=插件context_key;invoke=前端session_id)');
|
||||||
|
$table->string('source', 20)->default('')->comment('来源:chat|invoke');
|
||||||
|
$table->string('query', 500)->default('')->comment('检索query(截断500)');
|
||||||
|
$table->string('locale', 10)->default('')->comment('语种 zh|en');
|
||||||
|
$table->text('source_ids')->nullable()->comment('命中source id列表 JSON');
|
||||||
|
$table->decimal('top_score', 6, 4)->default(0)->comment('最高相似度 0-1');
|
||||||
|
$table->integer('result_count')->default(0)->comment('命中数量');
|
||||||
|
$table->integer('duration_ms')->default(0)->comment('检索耗时毫秒');
|
||||||
|
$table->tinyInteger('empty')->default(0)->comment('是否空结果 0|1');
|
||||||
|
$table->timestamps();
|
||||||
|
$table->index('userid', 'idx_userid');
|
||||||
|
$table->index('context_key', 'idx_context_key');
|
||||||
|
$table->index(['empty', 'created_at'], 'idx_empty_created');
|
||||||
|
$table->index('created_at', 'idx_created_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('ai_assistant_search_logs');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateAiAssistantFeedbacksTable extends Migration
|
||||||
|
{
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
if (Schema::hasTable('ai_assistant_feedbacks')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Schema::create('ai_assistant_feedbacks', function (Blueprint $table) {
|
||||||
|
$table->bigIncrements('id');
|
||||||
|
$table->bigInteger('userid')->default(0)->comment('用户ID');
|
||||||
|
$table->string('session_key', 100)->default('')->comment('场景分类key(同ai_assistant_sessions)');
|
||||||
|
$table->string('session_id', 100)->default('')->comment('前端会话ID(=检索日志context_key,松关联)');
|
||||||
|
$table->bigInteger('local_id')->default(0)->comment('前端回复条目localId');
|
||||||
|
$table->string('feedback', 10)->default('')->comment('like|dislike');
|
||||||
|
$table->text('prompt')->nullable()->comment('用户问题(截断1000)');
|
||||||
|
$table->string('answer_digest', 32)->default('')->comment('回复内容md5');
|
||||||
|
$table->text('answer')->nullable()->comment('回复摘录(去reasoning截断2000)');
|
||||||
|
$table->text('source_ids')->nullable()->comment('回复引用的kb source id列表 JSON');
|
||||||
|
$table->string('model', 100)->default('')->comment('模型名');
|
||||||
|
$table->timestamps();
|
||||||
|
$table->unique(['userid', 'session_key', 'session_id', 'local_id'], 'uk_user_entry');
|
||||||
|
$table->index(['feedback', 'created_at'], 'idx_feedback_created');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('ai_assistant_feedbacks');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
php:
|
php:
|
||||||
container_name: "dootask-php-${APP_ID}"
|
container_name: "dootask-php-${APP_ID}"
|
||||||
image: "kuaifan/php:swoole-8.0.rc21"
|
image: "kuaifan/php:swoole-8.4"
|
||||||
shm_size: 2G
|
shm_size: 2G
|
||||||
ulimits:
|
ulimits:
|
||||||
core:
|
core:
|
||||||
@ -11,6 +11,7 @@ services:
|
|||||||
- shared_data:/usr/share/dootask
|
- shared_data:/usr/share/dootask
|
||||||
- ./docker/crontab/crontab.conf:/etc/supervisor/conf.d/crontab.conf
|
- ./docker/crontab/crontab.conf:/etc/supervisor/conf.d/crontab.conf
|
||||||
- ./docker/php/php.conf:/etc/supervisor/conf.d/php.conf
|
- ./docker/php/php.conf:/etc/supervisor/conf.d/php.conf
|
||||||
|
- ./docker/php/license.conf:/etc/supervisor/conf.d/license.conf
|
||||||
- ./docker/php/php.ini:/usr/local/etc/php/php.ini
|
- ./docker/php/php.ini:/usr/local/etc/php/php.ini
|
||||||
- ./docker/logs/supervisor:/var/log/supervisor
|
- ./docker/logs/supervisor:/var/log/supervisor
|
||||||
- ./:/var/www
|
- ./:/var/www
|
||||||
@ -42,8 +43,10 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "${APP_PORT}:80"
|
- "${APP_PORT}:80"
|
||||||
- "${APP_SSL_PORT:-0}:443"
|
- "${APP_SSL_PORT:-0}:443"
|
||||||
|
environment:
|
||||||
|
APP_SCHEME: "${APP_SCHEME:-auto}"
|
||||||
volumes:
|
volumes:
|
||||||
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
|
- ./docker/nginx/default.conf:/etc/nginx/templates/default.conf.template
|
||||||
- ./:/var/www
|
- ./:/var/www
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost/health"]
|
test: ["CMD", "curl", "-f", "http://localhost/health"]
|
||||||
@ -96,7 +99,7 @@ services:
|
|||||||
appstore:
|
appstore:
|
||||||
container_name: "dootask-appstore-${APP_ID}"
|
container_name: "dootask-appstore-${APP_ID}"
|
||||||
privileged: true
|
privileged: true
|
||||||
image: "dootask/appstore:0.4.3"
|
image: "dootask/appstore:0.5.3"
|
||||||
volumes:
|
volumes:
|
||||||
- shared_data:/usr/share/dootask
|
- shared_data:/usr/share/dootask
|
||||||
- ${HOST_DOCKER_SOCK:-/var/run/docker.sock}:/var/run/docker.sock
|
- ${HOST_DOCKER_SOCK:-/var/run/docker.sock}:/var/run/docker.sock
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user