diff --git a/.gitignore b/.gitignore
index 99107eee..8c71dd10 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,6 +25,8 @@ node_modules/
# Build outputs and generated artifacts
dist/
build/
+!qiming-file-server/src/utils/build/
+!qiming-file-server/src/utils/build/**
out/
target/
.umi/
diff --git a/development-environment-startup-manual.md b/development-environment-startup-manual.md
index 3fc5f8e2..d02905b1 100644
--- a/development-environment-startup-manual.md
+++ b/development-environment-startup-manual.md
@@ -1819,3 +1819,270 @@ Path = ...\qimingclaw\node_modules\.pnpm\electron@...\electron...
```
说明已有一份 qimingclaw 开发进程在运行,不要重复启动。需要重启时,先关闭当前 Electron 窗口,再重新执行第 9.5 节启动命令。
+
+## 10. macOS 本机启动补充
+
+本节用于 macOS 本机开发,默认工作目录为:
+
+```bash
+cd /Users/qiming/qiming-work
+```
+
+服务器端 Milvus、Elasticsearch、rcoder 的启动方式仍按第 2 章执行;macOS 本机主要差异是命令写法、端口检查方式,以及 Homebrew 安装的外部命令路径。
+
+### 10.1 基础工具检查
+
+```bash
+java -version
+node -v
+pnpm -v
+rustc --version
+cargo --version
+deno --version
+uv --version
+```
+
+如果没有全局 Maven,但已安装 IntelliJ IDEA,可以使用 IDEA 内置 Maven:
+
+```bash
+MAVEN="/Applications/IntelliJ IDEA.app/Contents/plugins/maven/lib/maven3/bin/mvn"
+"$MAVEN" -version
+```
+
+确认 Homebrew:
+
+```bash
+/opt/homebrew/bin/brew --version
+```
+
+### 10.2 安装并验证 Tika 外部工具
+
+`qiming-backend` 使用 Apache Tika 解析文件时,会在启动阶段探测 `ffmpeg`、`exiftool`、`sox`、`tesseract`。这些不是 Java 依赖,需要系统命令能被后端进程找到。
+
+安装:
+
+```bash
+/opt/homebrew/bin/brew install ffmpeg exiftool sox tesseract
+```
+
+验证:
+
+```bash
+export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
+
+command -v ffmpeg
+command -v exiftool
+command -v sox
+command -v tesseract
+
+ffmpeg -version | sed -n '1p'
+exiftool -ver
+sox --version
+tesseract --version | sed -n '1p'
+```
+
+后端启动日志里如果看到下面内容,说明 Tika 已经能找到这些命令:
+
+```text
+exit value for ffmpeg: 0
+exit value for exiftool: 0
+exit value for sox: 0
+hasTesseract (path: [tesseract]): true
+```
+
+如果仍看到 `Cannot run program "ffmpeg"`、`Cannot run program "exiftool"`、`Cannot run program "sox"`、`Cannot run program "tesseract"`,优先检查启动后端的进程 PATH。IDEA 从 Dock 启动时经常拿不到 shell 里的 `/opt/homebrew/bin`,需要在 Run Configuration 的 Environment variables 里加入:
+
+```text
+PATH=/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
+```
+
+### 10.3 本机确认 MySQL 和 Redis
+
+当前根手册规划 MySQL 使用 `3308`,Redis 使用 `16379`:
+
+```bash
+nc -vz localhost 3308
+nc -vz localhost 16379
+mysql -h127.0.0.1 -P3308 -uroot -p你的MySQL密码 agent_platform -e "SELECT 1;"
+redis-cli -p 16379 -a 你的Redis密码 ping
+```
+
+正常结果:
+
+```text
+MySQL 查询返回 1
+Redis 返回 PONG
+```
+
+### 10.4 建立 Milvus SSH 隧道
+
+新开一个终端,保持不关闭:
+
+```bash
+ssh -N -L 19530:127.0.0.1:19530 root@服务器IP
+```
+
+另开终端验证:
+
+```bash
+nc -vz localhost 19530
+```
+
+### 10.5 验证服务器 Elasticsearch 和 rcoder
+
+```bash
+curl -f http://服务器IP:19200
+curl "http://服务器IP:19200/_cluster/health?pretty"
+
+nc -vz 服务器IP 18087
+nc -vz 服务器IP 18088
+```
+
+### 10.6 启动 qiming-backend
+
+如果使用 IDEA 启动,推荐在 `PlatformApiApplication` 的 Run Configuration 里设置 Environment variables:
+
+```text
+PATH=/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
+SPRING_PROFILES_ACTIVE=dev
+SERVER_PORT=18081
+DB_HOST=localhost
+DB_NAME=agent_platform
+DB_USERNAME=root
+DB_PASSWORD=你的MySQL密码
+REDIS_HOST=localhost
+REDIS_PORT=16379
+REDIS_PASSWORD=你的Redis密码
+REDIS_DB=1
+MILVUS_URI=http://服务器IP:19530/
+ES_URL=http://服务器IP:19200
+BUILD_SERVER_URL=http://localhost:16000/api
+CODE_EXECUTE_URL=http://localhost:18020/api/run_code_with_log
+MCP_PROXY_URL=http://localhost:18020
+AI_AGENT_URL=http://服务器IP:18087
+DOCKER_PROXY_ENABLE=true
+DOCKER_PROXY_URL=http://服务器IP:18088
+OUTER_PORT=11076
+```
+
+如果用终端启动:
+
+```bash
+cd /Users/qiming/qiming-work/qiming-backend
+
+export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
+export SPRING_PROFILES_ACTIVE="dev"
+export SERVER_PORT="18081"
+
+export DB_HOST="localhost"
+export DB_NAME="agent_platform"
+export DB_USERNAME="root"
+export DB_PASSWORD="你的MySQL密码"
+
+export REDIS_HOST="localhost"
+export REDIS_PORT="16379"
+export REDIS_PASSWORD="你的Redis密码"
+export REDIS_DB="1"
+
+export MILVUS_URI="http://服务器IP:19530/"
+export ES_URL="http://服务器IP:19200"
+export ES_USERNAME=""
+export ES_PASSWORD=""
+export ES_API_KEY=""
+
+export BUILD_SERVER_URL="http://localhost:16000/api"
+export CODE_EXECUTE_URL="http://localhost:18020/api/run_code_with_log"
+export MCP_PROXY_URL="http://localhost:18020"
+export AI_AGENT_URL="http://服务器IP:18087"
+export DOCKER_PROXY_ENABLE="true"
+export DOCKER_PROXY_URL="http://服务器IP:18088"
+export OUTER_PORT="11076"
+
+MAVEN="/Applications/IntelliJ IDEA.app/Contents/plugins/maven/lib/maven3/bin/mvn"
+"$MAVEN" package -Dmaven.test.skip=true -pl app-platform-bootstrap/app-platform-web-bootstrap -am -Pdev
+java -jar app-platform-bootstrap/app-platform-web-bootstrap/target/app-platform-web-bootstrap-0.0.1-SNAPSHOT.jar
+```
+
+验证:
+
+```bash
+nc -vz localhost 18081
+curl -I http://localhost:18081/doc.html
+```
+
+### 10.7 启动 qiming 前端
+
+```bash
+cd /Users/qiming/qiming-work/qiming
+pnpm install
+pnpm dev
+```
+
+`qiming/.env` 已固定 `PORT=18000`,直接执行 `pnpm dev` 即可。
+
+验证:
+
+```bash
+nc -vz localhost 18000
+open http://localhost:18000
+```
+
+### 10.8 启动 qiming-file-server
+
+```bash
+cd /Users/qiming/qiming-work/qiming-file-server
+npm install
+npm run dev
+```
+
+验证:
+
+```bash
+curl -f http://localhost:16000/health
+```
+
+### 10.9 启动 mcp-proxy
+
+```bash
+cd /Users/qiming/qiming-work/qiming-mcp-proxy
+cargo install --path ./mcp-proxy --force
+
+export MCP_PROXY_PORT="18020"
+mcp-proxy
+```
+
+验证:
+
+```bash
+curl -f http://localhost:18020/health
+```
+
+### 10.10 验证 run_code_rmcp
+
+```bash
+cd /Users/qiming/qiming-work/qiming-run_code_rmcp
+
+run_code_rmcp --show-logs js -c "function handler(){ console.log('js ok'); return 'hello qiming'; }"
+run_code_rmcp --show-logs python -c "def handler(args=None): print('python ok'); return 'hello python'"
+```
+
+正常应看到:
+
+```text
+Result: "hello qiming"
+Result: "hello python"
+```
+
+### 10.11 启动完成后统一检查
+
+```bash
+nc -vz localhost 18000
+nc -vz localhost 18081
+nc -vz localhost 16000
+nc -vz localhost 18020
+nc -vz localhost 19530
+
+curl -I http://localhost:18081/doc.html
+curl -f http://localhost:16000/health
+curl -f http://localhost:18020/health
+```
diff --git a/qiming-backend/app-platform-bootstrap/app-platform-web-bootstrap/src/main/resources/application-dev.yml b/qiming-backend/app-platform-bootstrap/app-platform-web-bootstrap/src/main/resources/application-dev.yml
index e38468a4..ae065d1b 100644
--- a/qiming-backend/app-platform-bootstrap/app-platform-web-bootstrap/src/main/resources/application-dev.yml
+++ b/qiming-backend/app-platform-bootstrap/app-platform-web-bootstrap/src/main/resources/application-dev.yml
@@ -27,10 +27,10 @@ spring:
datasource:
# 主数据源 (MySQL)
master:
- url: jdbc:mysql://${DB_HOST:localhost}:3308/${DB_NAME:agent_platform}?serverTimezone=Asia/Shanghai&characterEncoding=utf-8&allowMultiQueries=true&socketTimeout=300000&useSSL=false
+ url: jdbc:mysql://${DB_HOST:localhost}:3306/${DB_NAME:agent_platform}?serverTimezone=Asia/Shanghai&characterEncoding=utf-8&allowMultiQueries=true&socketTimeout=300000&useSSL=false
driver-class-name: com.mysql.cj.jdbc.Driver
username: ${DB_USERNAME:root}
- password: ${DB_PASSWORD:root}
+ password: ${DB_PASSWORD:Byyagz@121}
# 以下为 Druid 连接池配置 (如果使用)
druid:
initial-size: 5
@@ -48,10 +48,10 @@ spring:
filters: stat # 启用 stat filter 用于监控
# Doris 数据源
doris:
- url: jdbc:mysql://${DORIS_HOST:localhost}:3308/${DORIS_DB_NAME:agent_platform}?serverTimezone=Asia/Shanghai&characterEncoding=utf-8&allowMultiQueries=true&socketTimeout=300000&useSSL=false
+ url: jdbc:mysql://${DORIS_HOST:localhost}:3306/${DORIS_DB_NAME:agent_platform}?serverTimezone=Asia/Shanghai&characterEncoding=utf-8&allowMultiQueries=true&socketTimeout=300000&useSSL=false
driver-class-name: com.mysql.cj.jdbc.Driver
username: ${DORIS_USERNAME:root}
- password: ${DORIS_PASSWORD:root}
+ password: ${DORIS_PASSWORD:Byyagz@121}
# Doris 表配置
table:
replication-num: 1 # 副本数,生产环境通常为3,开发环境可以为1
@@ -68,7 +68,7 @@ spring:
data:
redis:
host: ${REDIS_HOST:localhost}
- port: ${REDIS_PORT:16379}
+ port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:Byyagz@121}
database: ${REDIS_DB:1}
lettuce:
diff --git a/qiming-backend/app-platform-bootstrap/app-platform-web-bootstrap/src/main/resources/logback-custom.xml b/qiming-backend/app-platform-bootstrap/app-platform-web-bootstrap/src/main/resources/logback-custom.xml
index d90c2f57..1e7cad78 100644
--- a/qiming-backend/app-platform-bootstrap/app-platform-web-bootstrap/src/main/resources/logback-custom.xml
+++ b/qiming-backend/app-platform-bootstrap/app-platform-web-bootstrap/src/main/resources/logback-custom.xml
@@ -48,7 +48,6 @@
-
@@ -68,4 +67,4 @@
-
\ No newline at end of file
+
diff --git a/qiming-file-server/src/utils/build/buildProjectUtils.js b/qiming-file-server/src/utils/build/buildProjectUtils.js
new file mode 100644
index 00000000..ba974af9
--- /dev/null
+++ b/qiming-file-server/src/utils/build/buildProjectUtils.js
@@ -0,0 +1,276 @@
+import { exec } from "child_process";
+import path from "path";
+import fs from "fs";
+import { log, logBuild } from "../log/logUtils.js";
+import BuildErrorParser from "../error/buildErrorParser.js";
+import config from "../../appConfig/index.js";
+import {
+ BusinessError,
+ SystemError,
+ FileError,
+ ResourceError,
+} from "../error/errorHandler.js";
+import { installDependencies } from "../buildDependency/dependencyManager.js";
+import {
+ extractIsolationContext,
+ resolveProjectPath,
+} from "../common/projectPathUtils.js";
+
+const distTargetDir = config.DIST_TARGET_DIR;
+
+// 构建并发控制(无队列)
+const buildingProjects = new Set(); // 正在构建中的项目
+let currentBuilds = 0; // 当前并行构建数
+
+// 将dist目录拷贝到指定目录
+async function copyBuildOutputToTarget({
+ req,
+ projectPath,
+ projectId,
+ outStream,
+}) {
+ try {
+ const sourceDir = path.join(projectPath, "dist");
+ const targetBase = distTargetDir;
+ const targetDir = path.join(targetBase, projectId, "dist");
+
+ if (!fs.existsSync(sourceDir)) {
+ const msg = `dist directory not found: ${sourceDir}`;
+ log(projectId, "WARN", msg, { projectId });
+ outStream && outStream.write(`${msg}\n`);
+ return;
+ }
+
+ // 确保目标父目录存在
+ if (!fs.existsSync(targetBase)) {
+ fs.mkdirSync(targetBase, { recursive: true });
+ }
+
+ // 先清空旧目录
+ if (fs.existsSync(targetDir)) {
+ await fs.promises.rm(targetDir, { recursive: true, force: true });
+ }
+
+ // 复制 dist -> 目标目录
+ if (fs.promises.cp) {
+ await fs.promises.cp(sourceDir, targetDir, { recursive: true });
+ } else {
+ // Node 版本不支持 fs.promises.cp 时的降级方案
+ const copyRecursiveSync = (src, dest) => {
+ const stat = fs.statSync(src);
+ if (stat.isDirectory()) {
+ if (!fs.existsSync(dest)) {
+ fs.mkdirSync(dest, { recursive: true });
+ }
+ for (const entry of fs.readdirSync(src)) {
+ copyRecursiveSync(path.join(src, entry), path.join(dest, entry));
+ }
+ } else {
+ fs.copyFileSync(src, dest);
+ }
+ };
+ copyRecursiveSync(sourceDir, targetDir);
+ }
+
+ const okMsg = `dist directory copied to: ${targetDir}`;
+ log(projectId, "INFO", okMsg, { projectId });
+ outStream && outStream.write(`${okMsg}\n`);
+ } catch (err) {
+ const errMsg = `Failed to copy dist directory: ${err.message}`;
+ log(projectId, "ERROR", errMsg, { projectId });
+ outStream && outStream.write(`${errMsg}\n`);
+ throw err;
+ }
+}
+
+//执行build脚本
+function runBuildScript(projectId, projectPath, scriptName, extraArgs = []) {
+ return new Promise((resolve, reject) => {
+ const argsPart =
+ Array.isArray(extraArgs) && extraArgs.length > 0
+ ? " -- " + extraArgs.map((s) => String(s)).join(" ")
+ : "";
+ const command = `cd ${projectPath} && pnpm run ${scriptName}${argsPart}`;
+ logBuild(projectId, "INFO", "Execute command", { command });
+ // 同步输出到普通日志也打印一次,便于从统一日志流检索
+ try {
+ log(projectId, "INFO", "Execute build script", { command, cwd: projectPath });
+ } catch (_) {}
+
+ exec(
+ command,
+ {
+ env: process.env, // 继承父进程的环境变量,包括 pnpm 配置
+ maxBuffer: 10 * 1024 * 1024, // 10MB 缓冲区
+ },
+ (error, stdout, stderr) => {
+ if (error) {
+ logBuild(projectId, "ERROR", "Execution error", {
+ error: error.message,
+ stderr,
+ });
+
+ // 使用错误解析器提供用户友好的错误信息
+ const errorParser = new BuildErrorParser();
+ const errorMessage = stderr || error.message;
+ const userFriendlyMessage = errorParser.parseBuildError(
+ errorMessage,
+ projectId
+ );
+
+ // 创建包含用户友好信息的构建错误
+ const buildError = new SystemError(userFriendlyMessage, {
+ originalError: error.message,
+ command: command,
+ });
+
+ return reject(buildError);
+ }
+ logBuild(projectId, "INFO", "Script execution completed", { stdout });
+ resolve(stdout);
+ });
+ });
+}
+
+/**
+ * 构建项目
+ * @param {Object} req 请求对象
+ * @param {string} projectId 项目ID
+ * @returns {Promise