Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2239ea9f58 | ||
|
|
4b34932468 | ||
|
|
f4b623deb6 | ||
|
|
84f225f3f3 | ||
|
|
fbd1c829a1 | ||
|
|
82d2ca6360 | ||
|
|
717e520556 | ||
|
|
c8ddb511cf | ||
|
|
caf728de8d |
67
.github/workflows/publish.yml
vendored
67
.github/workflows/publish.yml
vendored
@@ -131,8 +131,6 @@ jobs:
|
||||
include:
|
||||
- platform: "macos-latest"
|
||||
build_type: "mac"
|
||||
- platform: "ubuntu-latest"
|
||||
build_type: "android"
|
||||
- platform: "windows-latest"
|
||||
build_type: "windows"
|
||||
|
||||
@@ -147,69 +145,8 @@ jobs:
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
# Android 构建步骤
|
||||
- name: (Android) Build Js
|
||||
if: matrix.build_type == 'android'
|
||||
uses: nick-fields/retry@v2
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: |
|
||||
git submodule init
|
||||
git submodule update --remote "resources/mobile"
|
||||
./cmd appbuild publish
|
||||
|
||||
- name: (Android) Setup JDK 11
|
||||
if: matrix.build_type == 'android'
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: "zulu"
|
||||
java-version: "11"
|
||||
|
||||
- name: (Android) Build App
|
||||
if: matrix.build_type == 'android'
|
||||
uses: nick-fields/retry@v2
|
||||
with:
|
||||
timeout_minutes: 20
|
||||
max_attempts: 5
|
||||
command: |
|
||||
cd resources/mobile/platforms/android/eeuiApp
|
||||
chmod +x ./gradlew
|
||||
./gradlew assembleRelease --quiet
|
||||
|
||||
- name: (Android) Upload File
|
||||
if: matrix.build_type == 'android'
|
||||
env:
|
||||
UPLOAD_TOKEN: ${{ secrets.UPLOAD_TOKEN }}
|
||||
UPLOAD_URL: ${{ secrets.UPLOAD_URL }}
|
||||
run: |
|
||||
node ./electron/build.js android-upload
|
||||
|
||||
- name: (Android) Upload Release
|
||||
if: matrix.build_type == 'android'
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const globby = require('globby');
|
||||
|
||||
// 查找 APK 文件
|
||||
const files = await globby('resources/mobile/platforms/android/eeuiApp/app/build/outputs/apk/release/*.apk');
|
||||
|
||||
for (const file of files) {
|
||||
const data = await fs.promises.readFile(file);
|
||||
|
||||
await github.rest.repos.uploadReleaseAsset({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: process.env.RELEASE_ID,
|
||||
name: path.basename(file),
|
||||
data: data
|
||||
});
|
||||
}
|
||||
# 移动端构建已迁移到 dootask-app 仓库(EAS Build),见
|
||||
# dootask-app/.github/workflows/build.yml
|
||||
|
||||
# Mac 构建步骤
|
||||
- name: (Mac) Build Client
|
||||
|
||||
45
.github/workflows/sync-gitee.yml
vendored
Normal file
45
.github/workflows/sync-gitee.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: "Sync to Gitee"
|
||||
|
||||
# Required GitHub Secrets:
|
||||
#
|
||||
# GITEE_SSH_PRIVATE_KEY - SSH private key with push access to gitee.com/aipaw/dootask
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Publish"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: Push to Gitee
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup SSH key
|
||||
env:
|
||||
GITEE_SSH_PRIVATE_KEY: ${{ secrets.GITEE_SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "$GITEE_SSH_PRIVATE_KEY" > ~/.ssh/gitee_key
|
||||
chmod 600 ~/.ssh/gitee_key
|
||||
cat >> ~/.ssh/config << EOF
|
||||
Host gitee.com
|
||||
HostName gitee.com
|
||||
IdentityFile ~/.ssh/gitee_key
|
||||
StrictHostKeyChecking no
|
||||
EOF
|
||||
|
||||
- name: Push to Gitee
|
||||
run: |
|
||||
git remote add gitee git@gitee.com:aipaw/dootask.git
|
||||
git push gitee pro
|
||||
|
||||
- name: Clean up
|
||||
if: always()
|
||||
run: rm -rf ~/.ssh/gitee_key
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,6 +1,3 @@
|
||||
[submodule "resources/drawio"]
|
||||
path = resources/drawio
|
||||
url = https://github.com/jgraph/drawio.git
|
||||
[submodule "resources/mobile"]
|
||||
path = resources/mobile
|
||||
url = https://github.com/kuaifan/dootask-app.git
|
||||
|
||||
@@ -18,14 +18,25 @@ npm run build # 编译前端
|
||||
说明:
|
||||
|
||||
- 执行 `npm run build` 作用是生成网页端;
|
||||
- 客户端 (Windows、Mac、Android) 会通过 GitHub Actions 自动生成并发布;所以,如果要自动发布只需要提交git并推送即可;
|
||||
- 如果想手动生成客户端执行 `./cmd electron` 根据提示选择操作。
|
||||
- 桌面客户端(Windows、Mac)会通过 GitHub Actions 自动生成并发布;所以,如果要自动发布只需要提交 git 并推送即可;
|
||||
- 如果想手动生成桌面客户端执行 `./cmd electron` 根据提示选择操作。
|
||||
|
||||
## 编译移动端 App
|
||||
|
||||
## 编译 App
|
||||
移动端(iOS / Android)已迁移到独立仓库 [kuaifan/dootask-app](https://github.com/kuaifan/dootask-app)
|
||||
(Expo + EAS Build)。构建流程:
|
||||
|
||||
```shell
|
||||
./cmd appbuild publish # 编译生成App需要的资源
|
||||
# 1. 本仓库:构建前端资源
|
||||
./cmd appbuild
|
||||
|
||||
# 2. 拷贝到 dootask-app 仓库
|
||||
cp -r public/* ~/workspaces/dootask-app/assets/web/
|
||||
|
||||
# 3. dootask-app 仓库:EAS Build 本地或 CI 触发
|
||||
cd ~/workspaces/dootask-app
|
||||
npx eas build --platform android --profile preview
|
||||
# 或在 dootask-app 的 GitHub Actions 里手动触发 "EAS Build" workflow
|
||||
```
|
||||
|
||||
编译完后进入 `resources/mobile` EEUI框架目录内打包 Android 或 iOS 应用(Android 以实现 GitHub Actions 自动发布)
|
||||
详见 dootask-app 仓库的 README。
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\AiAssistantSession;
|
||||
use App\Models\User;
|
||||
use App\Module\AI;
|
||||
use App\Module\Apps;
|
||||
@@ -155,4 +156,152 @@ class AssistantController extends AbstractController
|
||||
}
|
||||
return $dotProduct / $denominator;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话列表
|
||||
*/
|
||||
public function session__list()
|
||||
{
|
||||
$user = User::auth();
|
||||
$sessionKey = trim(Request::input('session_key', 'default'));
|
||||
|
||||
$sessions = AiAssistantSession::where('userid', $user->userid)
|
||||
->where('session_key', $sessionKey)
|
||||
->orderByDesc('updated_at')
|
||||
->get();
|
||||
|
||||
$list = [];
|
||||
foreach ($sessions as $session) {
|
||||
$data = Base::json2array($session->data);
|
||||
$images = Base::json2array($session->images);
|
||||
foreach ($images as $imageId => $path) {
|
||||
$images[$imageId] = Base::fillUrl($path);
|
||||
}
|
||||
$list[] = [
|
||||
'id' => $session->session_id,
|
||||
'title' => $session->title,
|
||||
'responses' => $data,
|
||||
'images' => $images,
|
||||
'sceneKey' => $session->scene_key,
|
||||
'createdAt' => $session->created_at ? $session->created_at->getTimestampMs() : 0,
|
||||
'updatedAt' => $session->updated_at ? $session->updated_at->getTimestampMs() : 0,
|
||||
];
|
||||
}
|
||||
|
||||
return Base::retSuccess('success', $list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存会话
|
||||
*/
|
||||
public function session__save()
|
||||
{
|
||||
$user = User::auth();
|
||||
$sessionKey = trim(Request::input('session_key', 'default'));
|
||||
$sessionId = trim(Request::input('session_id', ''));
|
||||
$sceneKey = trim(Request::input('scene_key', ''));
|
||||
$title = trim(Request::input('title', ''));
|
||||
$data = Request::input('data', []);
|
||||
$newImages = Request::input('new_images', []);
|
||||
|
||||
if (empty($sessionId)) {
|
||||
return Base::retError('session_id 不能为空');
|
||||
}
|
||||
|
||||
$newImageUrls = [];
|
||||
if (is_array($newImages)) {
|
||||
$path = 'uploads/assistant/' . date('Ym') . '/' . $user->userid . '/';
|
||||
foreach ($newImages as $img) {
|
||||
$imageId = $img['imageId'] ?? '';
|
||||
$dataUrl = $img['dataUrl'] ?? '';
|
||||
if (empty($imageId) || empty($dataUrl)) {
|
||||
continue;
|
||||
}
|
||||
$result = Base::image64save([
|
||||
'image64' => $dataUrl,
|
||||
'path' => $path,
|
||||
'autoThumb' => false,
|
||||
]);
|
||||
if (Base::isSuccess($result)) {
|
||||
$newImageUrls[$imageId] = $result['data']['path'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$session = AiAssistantSession::where('userid', $user->userid)
|
||||
->where('session_key', $sessionKey)
|
||||
->where('session_id', $sessionId)
|
||||
->first();
|
||||
|
||||
$imageMap = $newImageUrls;
|
||||
if ($session) {
|
||||
$existingImages = Base::json2array($session->images);
|
||||
$imageMap = array_merge($existingImages, $newImageUrls);
|
||||
}
|
||||
|
||||
$session = AiAssistantSession::createInstance([
|
||||
'userid' => $user->userid,
|
||||
'session_key' => $sessionKey,
|
||||
'session_id' => $sessionId,
|
||||
'scene_key' => $sceneKey,
|
||||
'title' => mb_substr($title, 0, 255),
|
||||
'data' => Base::array2json(is_array($data) ? $data : []),
|
||||
'images' => Base::array2json($imageMap),
|
||||
], $session?->id);
|
||||
$session->save();
|
||||
|
||||
// 仅返回本次新增的图片URL
|
||||
$urls = [];
|
||||
foreach ($newImageUrls as $imageId => $path) {
|
||||
$urls[$imageId] = Base::fillUrl($path);
|
||||
}
|
||||
|
||||
return Base::retSuccess('success', [
|
||||
'image_urls' => $urls,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
*/
|
||||
public function session__delete()
|
||||
{
|
||||
$user = User::auth();
|
||||
$sessionKey = trim(Request::input('session_key', 'default'));
|
||||
$sessionId = trim(Request::input('session_id', ''));
|
||||
$clearAll = Request::input('clear_all', false);
|
||||
|
||||
$query = AiAssistantSession::where('userid', $user->userid)
|
||||
->where('session_key', $sessionKey);
|
||||
|
||||
if ($clearAll) {
|
||||
$sessions = $query->get();
|
||||
foreach ($sessions as $session) {
|
||||
$this->deleteSessionImages($session);
|
||||
}
|
||||
$query->delete();
|
||||
} else {
|
||||
if (empty($sessionId)) {
|
||||
return Base::retError('session_id 不能为空');
|
||||
}
|
||||
$session = $query->where('session_id', $sessionId)->first();
|
||||
if ($session) {
|
||||
$this->deleteSessionImages($session);
|
||||
$session->delete();
|
||||
}
|
||||
}
|
||||
|
||||
return Base::retSuccess('success');
|
||||
}
|
||||
|
||||
private function deleteSessionImages(AiAssistantSession $session)
|
||||
{
|
||||
$images = Base::json2array($session->images);
|
||||
foreach ($images as $path) {
|
||||
$fullPath = public_path($path);
|
||||
if (file_exists($fullPath)) {
|
||||
@unlink($fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1557,7 +1557,8 @@ class SystemController extends AbstractController
|
||||
{
|
||||
$userAgent = strtolower(Request::server('HTTP_USER_AGENT'));
|
||||
$isMain = str_contains($userAgent, 'maintaskwindow');
|
||||
$isApp = str_contains($userAgent, 'kuaifan_eeui');
|
||||
$isApp = str_contains($userAgent, 'kuaifan_eeui')
|
||||
|| str_contains($userAgent, 'dootask_expo');
|
||||
$version = Base::getVersion();
|
||||
$array = [];
|
||||
|
||||
|
||||
@@ -457,12 +457,16 @@ class IndexController extends InvokeController
|
||||
'button' => Doo::translate('点击下载'),
|
||||
]);
|
||||
}
|
||||
// 浏览器类型
|
||||
// 浏览器类型(兼容旧 EEUI 与新 Expo 壳)
|
||||
$browser = 'none';
|
||||
if (str_contains($userAgent, 'chrome') || str_contains($userAgent, 'android_kuaifan_eeui')) {
|
||||
$browser = str_contains($userAgent, 'android_kuaifan_eeui') ? 'android-mobile' : 'chrome-desktop';
|
||||
} elseif (str_contains($userAgent, 'safari') || str_contains($userAgent, 'ios_kuaifan_eeui')) {
|
||||
$browser = str_contains($userAgent, 'ios_kuaifan_eeui') ? 'safari-mobile' : 'safari-desktop';
|
||||
$isAndroidApp = str_contains($userAgent, 'android_kuaifan_eeui')
|
||||
|| str_contains($userAgent, 'android_dootask_expo');
|
||||
$isIosApp = str_contains($userAgent, 'ios_kuaifan_eeui')
|
||||
|| str_contains($userAgent, 'ios_dootask_expo');
|
||||
if (str_contains($userAgent, 'chrome') || $isAndroidApp) {
|
||||
$browser = $isAndroidApp ? 'android-mobile' : 'chrome-desktop';
|
||||
} elseif (str_contains($userAgent, 'safari') || $isIosApp) {
|
||||
$browser = $isIosApp ? 'safari-mobile' : 'safari-desktop';
|
||||
}
|
||||
// electron 直接在线预览查看
|
||||
if (str_contains($userAgent, 'electron') || str_contains($browser, 'desktop')) {
|
||||
|
||||
@@ -19,8 +19,6 @@ class LdapUser extends Model
|
||||
* @var array
|
||||
*/
|
||||
public static $objectClasses = [
|
||||
'inetOrgPerson',
|
||||
'organizationalPerson',
|
||||
'person',
|
||||
'top',
|
||||
];
|
||||
@@ -208,7 +206,9 @@ class LdapUser extends Model
|
||||
}
|
||||
$user = User::whereEmail($email)->first();
|
||||
if (empty($user)) {
|
||||
$user = User::reg($email, $password);
|
||||
// LDAP 用户通过 LDAP 认证,本地密码用随机值以满足密码策略
|
||||
$localPassword = Base::generatePassword(16) . 'Aa1!';
|
||||
$user = User::reg($email, $localPassword);
|
||||
} elseif (!$user->isLdap()) {
|
||||
info("[LDAP] merged with existing local account: userid={$user->userid}, email={$email}");
|
||||
}
|
||||
|
||||
22
app/Models/AiAssistantSession.php
Normal file
22
app/Models/AiAssistantSession.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
/**
|
||||
* AI 助手会话
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $userid
|
||||
* @property string $session_key
|
||||
* @property string $session_id
|
||||
* @property string $scene_key
|
||||
* @property string $title
|
||||
* @property string|null $data
|
||||
* @property string|null $images
|
||||
* @property \Carbon\Carbon $created_at
|
||||
* @property \Carbon\Carbon $updated_at
|
||||
*/
|
||||
class AiAssistantSession extends AbstractModel
|
||||
{
|
||||
protected $table = 'ai_assistant_sessions';
|
||||
}
|
||||
@@ -129,8 +129,8 @@ class UserDevice extends AbstractModel
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match("/android_kuaifan_eeui/i", $ua)) {
|
||||
// Android 客户端
|
||||
if (preg_match("/android_(kuaifan_eeui|dootask_expo)/i", $ua, $m)) {
|
||||
// Android 客户端(兼容旧 EEUI 与新 Expo 壳)
|
||||
$result['app_type'] = 'Android';
|
||||
if ($dd->getBrandName() && $dd->getModel()) {
|
||||
// 厂商+型号
|
||||
@@ -145,9 +145,9 @@ class UserDevice extends AbstractModel
|
||||
// 平板
|
||||
$result['app_name'] = 'Phablet';
|
||||
}
|
||||
$result['app_version'] = self::getAfterVersion($ua, 'kuaifan_eeui/');
|
||||
} elseif (preg_match("/ios_kuaifan_eeui/i", $ua)) {
|
||||
// iOS 客户端
|
||||
$result['app_version'] = self::getAfterVersion($ua, $m[1] . '/');
|
||||
} elseif (preg_match("/ios_(kuaifan_eeui|dootask_expo)/i", $ua, $m)) {
|
||||
// iOS 客户端(兼容旧 EEUI 与新 Expo 壳)
|
||||
$result['app_type'] = 'iOS';
|
||||
if (preg_match("/(macintosh|ipad)/i", $ua)) {
|
||||
// iPad
|
||||
@@ -156,7 +156,7 @@ class UserDevice extends AbstractModel
|
||||
// iPhone
|
||||
$result['app_name'] = 'iPhone';
|
||||
}
|
||||
$result['app_version'] = self::getAfterVersion($ua, 'kuaifan_eeui/');
|
||||
$result['app_version'] = self::getAfterVersion($ua, $m[1] . '/');
|
||||
} elseif (preg_match("/dootask/i", $ua)) {
|
||||
// DooTask 客户端
|
||||
$result['app_type'] = $osInfo['name'];
|
||||
|
||||
@@ -1841,13 +1841,14 @@ class Base
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是App移动端
|
||||
* 是否是App移动端(兼容旧 EEUI 壳与新 Expo 壳)
|
||||
* @return bool
|
||||
*/
|
||||
public static function isEEUIApp()
|
||||
{
|
||||
$userAgent = strtolower(Request::server('HTTP_USER_AGENT'));
|
||||
return str_contains($userAgent, 'kuaifan_eeui');
|
||||
return str_contains($userAgent, 'kuaifan_eeui')
|
||||
|| str_contains($userAgent, 'dootask_expo');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
18
cmd
18
cmd
@@ -833,23 +833,19 @@ case "$1" in
|
||||
;;
|
||||
"appbuild"|"buildapp")
|
||||
shift 1
|
||||
# 移动端已迁移到独立仓库 dootask-app(Expo + EAS Build),但前端资源的
|
||||
# post-processing(生成 config.js、把 manifest 里的 css/js 注入 index.html、
|
||||
# 拷贝 language/)仍然走 electron/build.js 的 startBuild({id:'app'}) 分支。
|
||||
# 产物在 electron/public/;实际移动端打包在 dootask-app 仓库执行。
|
||||
electron_operate app "$@"
|
||||
echo ""
|
||||
echo "前端资源已构建至 electron/public/"
|
||||
echo "同步到 dootask-app:cp -r electron/public/* ~/wwwroot/dootask-app/assets/web/"
|
||||
;;
|
||||
"electron")
|
||||
shift 1
|
||||
electron_operate "$@"
|
||||
;;
|
||||
"eeui")
|
||||
shift 1
|
||||
cli="$@"
|
||||
por=""
|
||||
if [[ "$cli" == "build" ]]; then
|
||||
cli="build --simple"
|
||||
elif [[ "$cli" == "dev" ]]; then
|
||||
por="-p 8880:8880"
|
||||
fi
|
||||
docker run $TTY_FLAG --rm -v ${WORK_DIR}/resources/mobile:/work -w /work ${por} kuaifan/eeui-cli:0.0.1 eeui ${cli}
|
||||
;;
|
||||
"npm")
|
||||
shift 1
|
||||
npm "$@"
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateAiAssistantSessionsTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
if (Schema::hasTable('ai_assistant_sessions')) {
|
||||
return;
|
||||
}
|
||||
Schema::create('ai_assistant_sessions', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('userid')->default(0)->comment('用户ID');
|
||||
$table->string('session_key', 100)->default('')->comment('场景分类key');
|
||||
$table->string('session_id', 100)->default('')->comment('前端生成的会话ID');
|
||||
$table->string('scene_key', 200)->default('')->comment('具体场景标识');
|
||||
$table->string('title', 255)->default('')->comment('会话标题');
|
||||
$table->longText('data')->nullable()->comment('responses JSON');
|
||||
$table->longText('images')->nullable()->comment('图片映射 {imageId: relativePath}');
|
||||
$table->timestamps();
|
||||
$table->index('userid', 'idx_userid');
|
||||
$table->unique(['userid', 'session_key', 'session_id'], 'uk_user_session');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('ai_assistant_sessions');
|
||||
}
|
||||
}
|
||||
70
electron/build.js
vendored
70
electron/build.js
vendored
@@ -566,42 +566,9 @@ async function startBuild(data) {
|
||||
indexString = indexString.replace("<!--script-->", `<script type="module" src="./${manifestContent['resources/assets/js/app.js']['file']}"></script>`);
|
||||
fs.writeFileSync(indexFile, indexString, 'utf8');
|
||||
//
|
||||
if (data.id === 'app') {
|
||||
const eeuiDir = path.resolve(__dirname, "../resources/mobile");
|
||||
const eeuiRun = `docker run --rm -v ${eeuiDir}:/work -w /work kuaifan/eeui-cli:0.0.1`
|
||||
const publicDir = path.resolve(__dirname, "../resources/mobile/src/public");
|
||||
fse.removeSync(publicDir)
|
||||
fse.copySync(electronDir, publicDir)
|
||||
if (argv[3] === "publish") {
|
||||
// Android config
|
||||
const gradleFile = path.resolve(eeuiDir, "platforms/android/eeuiApp/local.properties")
|
||||
let gradleResult = fs.existsSync(gradleFile) ? fs.readFileSync(gradleFile, 'utf8') : "";
|
||||
gradleResult = gradleResult.replace(/(versionCode|versionName)\s*=\s*(.+?)(\n|$)/g, '')
|
||||
gradleResult += `versionCode = ${config.codeVerson}\nversionName = ${config.version}\n`
|
||||
fs.writeFileSync(gradleFile, gradleResult, 'utf8')
|
||||
// iOS config
|
||||
const xcconfigFile = path.resolve(eeuiDir, "platforms/ios/eeuiApp/Config/Version.xcconfig")
|
||||
let xcconfigResult = fs.existsSync(xcconfigFile) ? fs.readFileSync(xcconfigFile, 'utf8') : "";
|
||||
xcconfigResult = xcconfigResult.replace(/(VERSION_CODE|VERSION_NAME)\s*=\s*(.+?)(\n|$)/g, '')
|
||||
xcconfigResult += `VERSION_CODE = ${config.codeVerson}\nVERSION_NAME = ${config.version}\n`
|
||||
fs.writeFileSync(xcconfigFile, xcconfigResult, 'utf8')
|
||||
}
|
||||
if (['build', 'publish'].includes(argv[3])) {
|
||||
if (!fs.existsSync(path.resolve(eeuiDir, "node_modules"))) {
|
||||
child_process.execSync(`${eeuiRun} npm install`, {stdio: "inherit", cwd: "resources/mobile"});
|
||||
}
|
||||
child_process.execSync(`${eeuiRun} eeui build --simple`, {stdio: "inherit", cwd: "resources/mobile"});
|
||||
} else {
|
||||
[
|
||||
path.resolve(publicDir, "../../platforms/ios/eeuiApp/bundlejs/eeui/public"),
|
||||
path.resolve(publicDir, "../../platforms/android/eeuiApp/app/src/main/assets/eeui/public"),
|
||||
].some(dir => {
|
||||
fse.removeSync(dir)
|
||||
fse.copySync(electronDir, dir)
|
||||
})
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 注:移动端(data.id === 'app')的 EEUI 打包逻辑已随着迁移到 dootask-app 仓库
|
||||
// 而移除。前端资源现在通过 ./cmd appbuild(= web_build prod)构建到 public/,
|
||||
// 实际的 iOS/Android 打包在 dootask-app 仓库用 EAS Build 执行。
|
||||
const output = `dist/${data.id.replace(/\./g, '-')}/${platform}`
|
||||
// package.json Backup
|
||||
fse.copySync(packageFile, packageBakFile)
|
||||
@@ -698,12 +665,7 @@ if (["dev"].includes(argv[2])) {
|
||||
child_process.spawn("npx", ["vite", "--", "fromcmd", "electronDev"], {stdio: "inherit"});
|
||||
child_process.spawn("npm", ["run", "start-quiet"], {stdio: "inherit", cwd: "electron"});
|
||||
} else if (["app"].includes(argv[2])) {
|
||||
// 编译前端页面给 App
|
||||
let mobileSrcDir = path.resolve(__dirname, "../resources/mobile");
|
||||
if (!fs.existsSync(mobileSrcDir)) {
|
||||
console.error("resources/mobile 未找到");
|
||||
process.exit()
|
||||
}
|
||||
// 编译前端页面给移动端 App(dootask-app 仓库消费 electron/public/ 目录)
|
||||
startBuild({
|
||||
name: 'App',
|
||||
id: 'app',
|
||||
@@ -717,30 +679,6 @@ if (["dev"].includes(argv[2])) {
|
||||
notarize: false,
|
||||
}
|
||||
})
|
||||
} else if (["android-upload"].includes(argv[2])) {
|
||||
// 上传安卓文件(GitHub Actions)
|
||||
(async () => {
|
||||
const publisher = createPublisher()
|
||||
if (!publisher) {
|
||||
console.error("缺少 UPLOAD_TOKEN 或 UPLOAD_URL 环境变量")
|
||||
process.exit(1)
|
||||
}
|
||||
const releaseDir = path.resolve(__dirname, "../resources/mobile/platforms/android/eeuiApp/app/build/outputs/apk/release");
|
||||
if (!fs.existsSync(releaseDir)) {
|
||||
console.error("发布文件未找到")
|
||||
process.exit(1)
|
||||
}
|
||||
const files = fs.readdirSync(releaseDir)
|
||||
for (const filename of files) {
|
||||
const localFile = path.join(releaseDir, filename)
|
||||
if (/\.apk$/.test(filename) && fs.existsSync(localFile) && fs.statSync(localFile).isFile()) {
|
||||
await publisher.uploadPackage(localFile, { platform: 'android' })
|
||||
}
|
||||
}
|
||||
})().catch(err => {
|
||||
console.error(err.message || err)
|
||||
process.exit(1)
|
||||
})
|
||||
} else if (["release"].includes(argv[2])) {
|
||||
// 通知官网发布完成(GitHub Actions)
|
||||
(async () => {
|
||||
|
||||
7
resources/assets/js/app.js
vendored
7
resources/assets/js/app.js
vendored
@@ -1,5 +1,5 @@
|
||||
const isElectron = !!(window && window.process && window.process.type && window.electron);
|
||||
const isEEUIApp = window && window.navigator && /eeui/i.test(window.navigator.userAgent);
|
||||
const isEEUIApp = window && window.navigator && /eeui|dootask_expo/i.test(window.navigator.userAgent);
|
||||
const isSoftware = isElectron || isEEUIApp;
|
||||
|
||||
document.getElementById("app")?.setAttribute("data-preload", "false");
|
||||
@@ -325,14 +325,15 @@ const $preload = async () => {
|
||||
document.getElementById("app")?.setAttribute("data-preload", "true")
|
||||
|
||||
if ($A.isEEUIApp) {
|
||||
// 同时等待旧 EEUI 的 requireModuleJs 与新 Expo 壳注入的 __EXPO_BRIDGE_READY__
|
||||
const requireTime = new Date().getTime();
|
||||
while (typeof requireModuleJs !== "function") {
|
||||
while (typeof requireModuleJs !== "function" && !window.__EXPO_BRIDGE_READY__) {
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
if (new Date().getTime() - requireTime > 15 * 1000) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (typeof requireModuleJs !== "function") {
|
||||
if (typeof requireModuleJs !== "function" && !window.__EXPO_BRIDGE_READY__) {
|
||||
const errorTip = $A.L("加载失败,请重启软件")
|
||||
const errorView = document.querySelector(".app-view-loading")
|
||||
if (errorView) {
|
||||
|
||||
@@ -322,6 +322,7 @@ export default {
|
||||
maxImages: 5, // 最大图片数量
|
||||
imageCacheKeyPrefix: 'aiAssistant.images', // 图片缓存 key 前缀
|
||||
imageCache: {}, // 内存中的图片缓存 {imageId: dataUrl}
|
||||
serverImageMap: {}, // 服务端返回的 {imageId: url} 映射
|
||||
isDragging: false, // 是否正在拖放图片
|
||||
dragCounter: 0, // 拖放计数器(处理嵌套元素)
|
||||
|
||||
@@ -335,6 +336,9 @@ export default {
|
||||
this.refreshWelcomePromptsDebounced = debounce(() => {
|
||||
this.displayWelcomePrompts = getWelcomePrompts(this.$store, this.$route?.params || {});
|
||||
}, 100);
|
||||
this.saveSessionStoreDebounced = debounce(() => {
|
||||
this.saveSessionStore();
|
||||
}, 2000);
|
||||
},
|
||||
mounted() {
|
||||
emitter.on('openAIAssistant', this.onOpenAIAssistant);
|
||||
@@ -1274,29 +1278,28 @@ export default {
|
||||
|
||||
// ==================== 会话管理方法 ====================
|
||||
|
||||
/**
|
||||
* 获取指定场景的缓存 key
|
||||
*/
|
||||
getSessionCacheKey(sessionKey) {
|
||||
return `${this.sessionCacheKeyPrefix}_${sessionKey || 'default'}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载指定场景的会话数据
|
||||
*/
|
||||
async loadSessionStore(sessionKey) {
|
||||
const cacheKey = this.getSessionCacheKey(sessionKey);
|
||||
try {
|
||||
const stored = await $A.IDBString(cacheKey);
|
||||
if (stored) {
|
||||
this.sessionStore = JSON.parse(stored);
|
||||
if (!Array.isArray(this.sessionStore)) {
|
||||
this.sessionStore = [];
|
||||
}
|
||||
const {data} = await this.$store.dispatch("call", {
|
||||
url: 'assistant/session/list',
|
||||
data: {session_key: sessionKey},
|
||||
});
|
||||
if (Array.isArray(data)) {
|
||||
this.sessionStore = data;
|
||||
// 缓存服务端返回的图片URL映射
|
||||
data.forEach(session => {
|
||||
if (session.images) {
|
||||
Object.assign(this.serverImageMap, session.images);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.sessionStore = [];
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[AIAssistant] 加载会话失败:', e);
|
||||
this.sessionStore = [];
|
||||
}
|
||||
this.sessionStoreLoaded = true;
|
||||
@@ -1305,12 +1308,42 @@ export default {
|
||||
/**
|
||||
* 持久化当前场景的会话数据
|
||||
*/
|
||||
saveSessionStore() {
|
||||
const cacheKey = this.getSessionCacheKey(this.currentSessionKey);
|
||||
async saveSessionStore() {
|
||||
if (!this.currentSessionId) return;
|
||||
const session = this.sessionStore.find(s => s.id === this.currentSessionId);
|
||||
if (!session) return;
|
||||
|
||||
// 收集本次需要上传的新图片(在 imageCache 中有 base64 但 serverImageMap 中没有的)
|
||||
const newImages = [];
|
||||
const imageIds = this.extractImageIdsFromSession(session);
|
||||
for (const imageId of imageIds) {
|
||||
if (!this.serverImageMap[imageId] && this.imageCache[imageId]) {
|
||||
newImages.push({
|
||||
imageId,
|
||||
dataUrl: this.imageCache[imageId],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$A.IDBSave(cacheKey, JSON.stringify(this.sessionStore));
|
||||
const {data} = await this.$store.dispatch("call", {
|
||||
url: 'assistant/session/save',
|
||||
method: 'post',
|
||||
data: {
|
||||
session_key: this.currentSessionKey,
|
||||
session_id: session.id,
|
||||
scene_key: session.sceneKey || '',
|
||||
title: session.title || '',
|
||||
data: session.responses || [],
|
||||
new_images: newImages,
|
||||
},
|
||||
});
|
||||
// 更新服务端图片映射
|
||||
if (data?.image_urls) {
|
||||
Object.assign(this.serverImageMap, data.image_urls);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[AIAssistant] Failed to save session store:', e);
|
||||
console.warn('[AIAssistant] 保存会话失败:', e);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1448,7 +1481,7 @@ export default {
|
||||
this.sessionStore.splice(this.maxSessionsPerKey);
|
||||
}
|
||||
|
||||
this.saveSessionStore();
|
||||
this.saveSessionStoreDebounced();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1492,11 +1525,16 @@ export default {
|
||||
const index = this.sessionStore.findIndex(s => s.id === sessionId);
|
||||
if (index > -1) {
|
||||
const session = this.sessionStore[index];
|
||||
// 清理会话相关的图片缓存
|
||||
this.clearSessionImageCache(session);
|
||||
this.sessionStore.splice(index, 1);
|
||||
this.saveSessionStore();
|
||||
// 如果删除的是当前会话,创建新会话
|
||||
this.$store.dispatch("call", {
|
||||
url: 'assistant/session/delete',
|
||||
method: 'post',
|
||||
data: {
|
||||
session_key: this.currentSessionKey,
|
||||
session_id: sessionId,
|
||||
},
|
||||
}).catch(e => console.warn('[AIAssistant] 删除会话失败:', e));
|
||||
if (this.currentSessionId === sessionId) {
|
||||
this.createNewSession(false);
|
||||
}
|
||||
@@ -1510,13 +1548,18 @@ export default {
|
||||
$A.modalConfirm({
|
||||
title: this.$L('清空历史会话'),
|
||||
content: this.$L('确定要清空当前场景的所有历史会话吗?'),
|
||||
onOk: async () => {
|
||||
// 清理所有会话的图片缓存
|
||||
for (const session of this.sessionStore) {
|
||||
await this.clearSessionImageCache(session);
|
||||
}
|
||||
onOk: () => {
|
||||
this.serverImageMap = {};
|
||||
this.imageCache = {};
|
||||
this.sessionStore = [];
|
||||
this.saveSessionStore();
|
||||
this.$store.dispatch("call", {
|
||||
url: 'assistant/session/delete',
|
||||
method: 'post',
|
||||
data: {
|
||||
session_key: this.currentSessionKey,
|
||||
clear_all: true,
|
||||
},
|
||||
}).catch(e => console.warn('[AIAssistant] 清空会话失败:', e));
|
||||
this.createNewSession(false);
|
||||
}
|
||||
});
|
||||
@@ -1884,9 +1927,8 @@ export default {
|
||||
* @returns {Promise<string>} - Base64 data URL (image/jpeg)
|
||||
*/
|
||||
async compressImageForAI(file) {
|
||||
// File 转 dataUrl 后压缩到 1024px,强制质量压缩
|
||||
const dataUrl = await this.fileToDataUrl(file);
|
||||
return this.resizeDataUrl(dataUrl, 1024, true);
|
||||
return this.resizeDataUrl(dataUrl, 1568, true);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1929,27 +1971,11 @@ export default {
|
||||
return `${timestamp}_${random}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取图片缓存 key
|
||||
*/
|
||||
getImageCacheKey(imageId) {
|
||||
return `${this.imageCacheKeyPrefix}_${imageId}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* 保存图片到独立缓存
|
||||
*/
|
||||
async saveImageToCache(imageId, dataUrl) {
|
||||
const cacheKey = this.getImageCacheKey(imageId);
|
||||
try {
|
||||
// 压缩到 512px 再保存(历史图片不需要高清)
|
||||
const compressedUrl = await this.resizeDataUrl(dataUrl, 512);
|
||||
await $A.IDBSave(cacheKey, compressedUrl);
|
||||
// 同时保存到内存缓存
|
||||
this.imageCache[imageId] = compressedUrl;
|
||||
} catch (e) {
|
||||
console.warn('[AIAssistant] 图片缓存保存失败:', e);
|
||||
}
|
||||
saveImageToCache(imageId, dataUrl) {
|
||||
this.imageCache[imageId] = dataUrl;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -2000,21 +2026,13 @@ export default {
|
||||
* 从缓存获取图片
|
||||
*/
|
||||
async getImageFromCache(imageId) {
|
||||
// 先检查内存缓存
|
||||
// 1. 先检查内存缓存
|
||||
if (this.imageCache[imageId]) {
|
||||
return this.imageCache[imageId];
|
||||
}
|
||||
// 从 IndexedDB 获取
|
||||
const cacheKey = this.getImageCacheKey(imageId);
|
||||
try {
|
||||
const dataUrl = await $A.IDBString(cacheKey);
|
||||
if (dataUrl) {
|
||||
// 保存到内存缓存
|
||||
this.imageCache[imageId] = dataUrl;
|
||||
return dataUrl;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[AIAssistant] 图片缓存读取失败:', e);
|
||||
// 2. 检查服务端URL映射
|
||||
if (this.serverImageMap[imageId]) {
|
||||
return this.serverImageMap[imageId];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
@@ -2022,14 +2040,9 @@ export default {
|
||||
/**
|
||||
* 删除单个图片缓存
|
||||
*/
|
||||
async deleteImageCache(imageId) {
|
||||
const cacheKey = this.getImageCacheKey(imageId);
|
||||
try {
|
||||
await $A.IDBDel(cacheKey);
|
||||
delete this.imageCache[imageId];
|
||||
} catch (e) {
|
||||
console.warn('[AIAssistant] 图片缓存删除失败:', e);
|
||||
}
|
||||
deleteImageCache(imageId) {
|
||||
delete this.imageCache[imageId];
|
||||
delete this.serverImageMap[imageId];
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -2052,10 +2065,10 @@ export default {
|
||||
/**
|
||||
* 清理会话相关的图片缓存
|
||||
*/
|
||||
async clearSessionImageCache(session) {
|
||||
clearSessionImageCache(session) {
|
||||
const imageIds = this.extractImageIdsFromSession(session);
|
||||
for (const imageId of imageIds) {
|
||||
await this.deleteImageCache(imageId);
|
||||
this.deleteImageCache(imageId);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
33
resources/assets/js/functions/eeui.js
vendored
33
resources/assets/js/functions/eeui.js
vendored
@@ -35,14 +35,14 @@ import {languageName} from "../language";
|
||||
})
|
||||
},
|
||||
|
||||
// 获取eeui版本号
|
||||
// 获取eeui版本号(Expo 壳下改为读取启动时注入的 __EXPO_INIT_DATA__,兼容旧 EEUI)
|
||||
eeuiAppVersion() {
|
||||
return $A.eeuiModule()?.getVersion();
|
||||
return window.__EXPO_INIT_DATA__?.version ?? $A.eeuiModule()?.getVersion();
|
||||
},
|
||||
|
||||
// 获取本地软件版本号
|
||||
eeuiAppLocalVersion() {
|
||||
return $A.eeuiModule()?.getLocalVersion();
|
||||
return window.__EXPO_INIT_DATA__?.version ?? $A.eeuiModule()?.getLocalVersion();
|
||||
},
|
||||
|
||||
// Alert
|
||||
@@ -61,8 +61,12 @@ import {languageName} from "../language";
|
||||
return $A.eeuiModule()?.rewriteUrl(val);
|
||||
},
|
||||
|
||||
// 获取页面信息
|
||||
// 获取页面信息(Expo 壳在 injectedJS 启动时写入 __EXPO_INIT_DATA__.pageInfo)
|
||||
eeuiAppGetPageInfo(pageName) {
|
||||
const cached = window.__EXPO_INIT_DATA__?.pageInfo;
|
||||
if (cached) {
|
||||
return pageName ? { ...cached, pageName } : cached;
|
||||
}
|
||||
return $A.eeuiModule()?.getPageInfo(pageName || "");
|
||||
},
|
||||
|
||||
@@ -152,13 +156,16 @@ import {languageName} from "../language";
|
||||
$A.eeuiModule()?.checkUpdate();
|
||||
},
|
||||
|
||||
// 获取主题名称 light|dark
|
||||
// 获取主题名称 light|dark(Expo 壳:启动时注入 + 系统变更推送更新 __EXPO_INIT_DATA__.themeName)
|
||||
eeuiAppGetThemeName() {
|
||||
return $A.eeuiModule()?.getThemeName();
|
||||
return window.__EXPO_INIT_DATA__?.themeName ?? $A.eeuiModule()?.getThemeName();
|
||||
},
|
||||
|
||||
// 判断软键盘是否可见
|
||||
// 判断软键盘是否可见(Expo 壳:keyboardDidShow/Hide 会同步更新 __EXPO_INIT_DATA__.keyboardVisible)
|
||||
eeuiAppKeyboardStatus() {
|
||||
if (window.__EXPO_INIT_DATA__ && typeof window.__EXPO_INIT_DATA__.keyboardVisible === "boolean") {
|
||||
return window.__EXPO_INIT_DATA__.keyboardVisible;
|
||||
}
|
||||
return $A.eeuiModule()?.keyboardStatus();
|
||||
},
|
||||
|
||||
@@ -167,8 +174,12 @@ import {languageName} from "../language";
|
||||
$A.eeuiModule()?.setVariate(key, value);
|
||||
},
|
||||
|
||||
// 获取全局变量
|
||||
// 获取全局变量(Expo 壳:setVariate 时 RN 会 broadcast 到所有 WebView 的 __EXPO_VARIATES__)
|
||||
eeuiAppGetVariate(key, defaultVal = "") {
|
||||
const cache = window.__EXPO_VARIATES__;
|
||||
if (cache && Object.prototype.hasOwnProperty.call(cache, key)) {
|
||||
return cache[key];
|
||||
}
|
||||
return $A.eeuiModule()?.getVariate(key, defaultVal);
|
||||
},
|
||||
|
||||
@@ -177,8 +188,12 @@ import {languageName} from "../language";
|
||||
$A.eeuiModule()?.setCachesString(key, value, expired);
|
||||
},
|
||||
|
||||
// 获取缓存数据
|
||||
// 获取缓存数据(Expo 壳:若 __EXPO_CACHES__ 已 hydrate 就同步读取,否则回落到原生桥)
|
||||
eeuiAppGetCachesString(key, defaultVal = "") {
|
||||
const cache = window.__EXPO_CACHES__;
|
||||
if (cache && Object.prototype.hasOwnProperty.call(cache, key)) {
|
||||
return cache[key];
|
||||
}
|
||||
return $A.eeuiModule()?.getCachesString(key, defaultVal);
|
||||
},
|
||||
|
||||
|
||||
Submodule resources/mobile deleted from 4e8ee678b3
@@ -78,7 +78,7 @@
|
||||
document.body.classList.add("dark");
|
||||
}
|
||||
//
|
||||
const isEEUIApp = window && window.navigator && /eeui/i.test(window.navigator.userAgent);
|
||||
const isEEUIApp = window && window.navigator && /eeui|dootask_expo/i.test(window.navigator.userAgent);
|
||||
if (isEEUIApp) {
|
||||
document.querySelector(".link").addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
Reference in New Issue
Block a user