chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

36
.gitignore vendored Normal file
View File

@@ -0,0 +1,36 @@
# Ignore everything by default. This repo is for qiming-related code only.
/*
!/.gitignore
!/qiming/
!/qiming-backend/
!/qiming-file-server/
!/qiming-mobile/
!/qimingclaw/
!/qimingcode/
# Dependencies
node_modules/
**/node_modules/
.pnpm-store/
# Build outputs and generated artifacts
dist/
build/
out/
target/
.umi/
.umi-production/
.cache/
.turbo/
coverage/
# Logs and local machine state
*.log
.DS_Store
Thumbs.db
# Local environment files
.env
.env.*
!.env.example

View File

@@ -0,0 +1,41 @@
# 商业授权 | Commercial License
## 授权说明 | Licensing Overview
本软件采用 **Apache 2.0 开源许可证**,您可以免费使用、修改和分发。但在以下情况下,您需要获得商业授权:
This software is licensed under the **Apache License 2.0**, which allows free use, modification, and distribution. However, a commercial license is required for the following scenarios:
## 需要商业授权的场景 | Commercial License Scenarios
### ✅ 免费使用Apache 2.0| Free Use (Apache 2.0)
- 个人学习和研究 | Personal learning and research
- 企业内部使用 | Internal enterprise use
- 开源项目 | Open source projects
- 商业项目(保留版权声明)| Commercial projects (with copyright attribution)
### 💰 需要商业授权 | Commercial License Required
- **白标产品**:移除或替换原始版权声明 | **White-label Products**: Remove or replace original copyright notices
- **OEM 合作**:将软件集成到你的产品中销售 | **OEM Partnerships**: Integrate software into your products for resale
- **品牌定制**:使用你自己的品牌分发软件 | **Custom Branding**: Distribute software under your own brand
- **无署名分发**:不注明原始来源的分发 | **Attribution-free Distribution**: Distribute without original source attribution
## 常见问题 | FAQ
### Q: 开源版和商业版有什么区别?| What's the difference between open source and commercial versions?
**A:** 功能完全相同。区别在于:| **A:** The functionality is identical. The difference is:
- 开源版必须保留版权声明 | Open source version requires copyright attribution
- 商业版可以移除版权,使用自己的品牌 | Commercial version allows copyright removal and custom branding
### Q: 我已经修改了代码,还需要授权吗?| Do I need a license if I've modified the code?
**A:** 如果你的修改是基于原始代码的衍生作品,且仍包含原始代码,移除版权声明仍需授权。| **A:** If your modifications are derivative works based on the original code and still contain original code, you still need a license to remove copyright notices.
### Q: 授权后是否包含技术支持?| Does the license include technical support?
**A:** 企业版和 OEM 版包含基础技术支持,其他级别支持需单独购买。| **A:** Enterprise and OEM editions include basic technical support. Other support levels require separate purchase.
### Q: 可以转售授权吗?| Can I resell the license?
**A:** 不可转售。每个授权仅对申请者有效。| **A:** No resale allowed. Each license is valid only for the applicant.

190
qiming-backend/LICENSE Normal file
View File

@@ -0,0 +1,190 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2025 Agent Platform Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

521
qiming-backend/README.md Normal file
View File

@@ -0,0 +1,521 @@
# Qiming Backend
[![Java](https://img.shields.io/badge/Java-17-orange.svg)](https://openjdk.java.net/)
[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.3.8-brightgreen.svg)](https://spring.io/projects/spring-boot)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
**Enterprise-grade AI Agent Development and Operation Platform** - Providing a complete solution for agent creation and distribution, knowledge base management, model proxy, memory system, and plugin ecosystem.
English | [简体中文](README.zh-CN.md)
---
## Table of Contents
- [Core Features](#core-features)
- [Architecture](#architecture)
- [Quick Start](#quick-start)
- [Configuration](#configuration) ⚠️ **Important**
- [Module Description](#module-description)
- [Development Guide](#development-guide)
- [Contributing](#contributing)
- [License](#license)
- [Commercial License](#commercial-license)
---
## Core Features
### Agent Engine
- **Visual Orchestration**: Drag-and-drop workflow design, zero-code agent creation
- **Code-based Development**: Support for Java/Python plugin development, flexible extension
- **Multi-model Support**: OpenAI, Claude, Ollama, Qwen, and other mainstream models
- **Streaming Output**: SSE real-time response, smooth user experience
- **Multi-turn Conversations**: Complete session context management
### Knowledge Base Management
- **Document Parsing**: PDF, Word, Excel, Markdown, and other formats
- **Vector Storage**: Milvus vector database, efficient similarity search
- **Intelligent Chunking**: Automatic document segmentation and overlap processing
- **Hybrid Retrieval**: Vector search + Full-text search + QA search
### Memory System
- **Long-term Memory**: Persistent user data and preferences
- **Short-term Memory**: Session context management
- **Classification System**: 12 main categories, 50+ sub-categories
- **Sensitive Information Filtering**: Automatic desensitization
### Model Proxy (In Development)
- **Unified Interface**: Shield differences between different model APIs
- **Intelligent Routing**: Select the optimal model based on scenarios
- **Quota Management**: Token billing and rate limiting
- **Failover**: Automatic fault tolerance switching
### Plugin Ecosystem
- **Plugin Market**: Publish and discover plugins
- **MCP Protocol**: Model Context Protocol support
- **Page Applications**: No-code page builder
---
## Architecture
### Tech Stack
| Category | Technology | Version |
|----------|-----------|---------|
| Language | Java | 17 |
| Framework | Spring Boot | 3.3.8 |
| Database | MySQL | 8.0+ |
| Vector Database | Milvus | 2.5.4 |
| Cache | Redis | 7.0+ |
| ORM | MyBatis Plus | 3.5+ |
| API Documentation | Knife4j | 4.0+ |
| Container | Docker | - |
| Orchestration | Kubernetes | - |
### Architecture Diagram
```
┌────────────────────────────────────────────────────────────────────────────┐
│ Frontend Layer │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────────────────┐ │
│ │ PC Web │ │ H5 │ │ Mini App │ │ IM (Feishu/DingTalk/ │ │
│ │ │ │ │ │ │ │ WeCom/Slack) │ │
│ └──────────┘ └──────────┘ └──────────┘ └─────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────────────┐
│ Access Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────────┐ │
│ │ REST API │ │ Long │ │ WebSocket │ │
│ │ │ │ Connection │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────────────┐
│ Application Layer │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Component Library │ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ │
│ │ │ Model│ │Know- │ │ Data │ │Plugin│ │ Work-│ │ MCP │ │ Skill│ │ │
│ │ │ │ │ ledge│ │ Table│ │ │ │ flow │ │ │ │ │ │ │
│ │ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Management Portal │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ User │ │ Audit │ │ Public │ │ Content │ │ Task │ │ │
│ │ │Management│ │Management│ │ Model │ │Management│ │Management│ │ │
│ │ ├──────────┤ ├──────────┤ ├──────────┤ ├──────────┤ ├──────────┤ │ │
│ │ │ Log │ │ Menu │ │ System │ │ │ │ │ │ │
│ │ │ Query │ │Permission│ │ Config │ │ │ │ │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Product Applications │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │ │
│ │ │ Web App │ │ Q&A │ │ General Agent │ │ │
│ │ │ │ │ Agent │ │ │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────────────┐
│ Infrastructure Layer │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Lower-level Components │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────────┐ │ │
│ │ │ Cloud │ │ qimingclaw │ │ General Agent Engine │ │ │
│ │ │ Sandbox │ │ PC Client │ │ │ │ │
│ │ │ │ │ (mac/win/ │ │ ┌───────────────────────┐ │ │
│ │ │ │ │ docker) │ │ │ MCP Integration │ │ │
│ │ │ │ │ │ │ ├───────────────────────┤ │ │
│ │ │ │ │ │ │ │ File Management │ │ │
│ │ │ │ │ │ │ │ Skill Management │ │ │
│ │ │ │ │ │ │ │ ACP Adapter Layer │ │ │
│ │ │ │ │ │ │ │ ┌─────────────────┐ │ │ │
│ │ │ │ │ │ │ │ │ Supported Agent:│ │ │ │
│ │ │ │ │ │ │ │ │ claudecode │ │ │ │
│ │ │ │ │ │ │ │ │ opencode │ │ │ │
│ │ │ │ │ │ │ │ │ codex │ │ │ │
│ │ │ │ │ │ │ │ │ openclaw │ │ │ │
│ │ │ │ │ │ │ │ │ kimicli │ │ │ │
│ │ │ │ │ │ │ │ └─────────────────┘ │ │ │
│ │ │ │ │ │ │ ├───────────────────────┤ │ │
│ │ │ │ │ │ │ │ Browser │ │ │
│ │ │ │ │ │ │ │ Automation │ │ │
│ │ │ │ │ │ │ │ GUI Automation │ │ │
│ │ │ │ │ │ │ │ Network Channel │ │ │
│ │ │ │ │ │ │ │ Runtime Integration │ │ │
│ │ │ │ │ │ │ └───────────────────────┘ │ │
│ │ │ │ │ │ └─────────────────────────────┘ │ │
│ │ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Core Infrastructure │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────┐ │ │
│ │ │ Database │ │ Cache │ │ Vector │ │ Search │ │ Model │ │ │
│ │ │ MySQL │ │ Redis │ │ Milvus │ │ Elastic │ │ Proxy│ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────┘
```
### Project Repository Overview
The Qiming AI Agent Platform consists of multiple interconnected repositories:
#### **Frontend & Mobile**
| Repository | Description | URL |
|-----------|-------------|-----|
| **qiming** | Frontend Web | [https://github.com/qiming-ai/qiming](https://github.com/qiming-ai/qiming) |
| **qiming-mobile** | Mobile Application | [https://github.com/qiming-ai/qiming-mobile](https://github.com/qiming-ai/qiming-mobile) |
| **noVNC** | Web-based VNC Client | [https://github.com/qiming-ai/noVNC](https://github.com/qiming-ai/noVNC) |
#### **Backend & Application Layer**
| Repository | Description | URL |
|-----------|-------------|-----|
| **qiming-backend** | Application Layer (Backend) - **This Repository** | [https://github.com/qiming-ai/qiming-backend](https://github.com/qiming-ai/qiming-backend) |
#### **Agent Engine & Clients**
| Repository | Description | URL |
|-----------|-------------|-----|
| **qimingclaw** | Agent PC Client (mac/win/docker) | [https://github.com/qiming-ai/qimingclaw](https://github.com/qiming-ai/qimingclaw) |
| **qimingcode** | Qiming Agent Engine (based on open-source opencode) | [https://github.com/qiming-ai/qimingcode](https://github.com/qiming-ai/qimingcode) |
| **claude-code-acp-ts** | Claude Code ACP based on Zed | [https://github.com/qiming-ai/claude-code-acp-ts](https://github.com/qiming-ai/claude-code-acp-ts) |
#### **Infrastructure & Services**
| Repository | Description | URL |
|-----------|-------------|-----|
| **rcoder** | Sandbox & Container Scheduling (includes General Agent Engine) | [https://github.com/qiming-ai/rcoder](https://github.com/qiming-ai/rcoder) |
| **mcp-proxy** | MCP Service (used by qimingclaw and sandbox) | [https://github.com/qiming-ai/mcp-proxy](https://github.com/qiming-ai/mcp-proxy) |
| **qiming-file-server** | File Service (used by sandbox and qimingclaw, includes skill sync) | [https://github.com/qiming-ai/qiming-file-server](https://github.com/qiming-ai/qiming-file-server) |
#### **Web Application Development**
| Repository | Description | URL |
|-----------|-------------|-----|
| **xagi-frontend-templates** | Web Application Development Templates | [https://github.com/qiming-ai/xagi-frontend-templates](https://github.com/qiming-ai/xagi-frontend-templates) |
| **vite-plugin-design-mode** | Visual Editor Vite Plugin | [https://github.com/qiming-ai/vite-plugin-design-mode](https://github.com/qiming-ai/vite-plugin-design-mode) |
| **dev-inject** | Web Application Smart Script Injection | [https://github.com/qiming-ai/dev-inject](https://github.com/qiming-ai/dev-inject) |
#### **Plugin & Script Execution**
| Repository | Description | URL |
|-----------|-------------|-----|
| **run_code_rmcp** | Plugin Script Execution (TypeScript/JavaScript/Python) | [https://github.com/qiming-ai/run_code_rmcp](https://github.com/qiming-ai/run_code_rmcp) |
#### **Network & Utilities**
| Repository | Description | URL |
|-----------|-------------|-----|
| **lanproxy-go-client** | Network Tunnel Client (used by qimingclaw) | [https://github.com/ffay/lanproxy-go-client](https://github.com/ffay/lanproxy-go-client) |
---
### Design Patterns
- **DDD (Domain-Driven Design)**: Clear layer separation
- **CQRS**: Command Query Responsibility Segregation
- **Strategy Pattern**: Model selection, plugin invocation
- **Observer Pattern**: Message broadcasting
- **Repository Pattern**: Data access abstraction
---
## Quick Start
### Prerequisites
| Component | Minimum | Recommended |
|-----------|---------|-------------|
| Java | JDK 17 | OpenJDK 17 |
| MySQL | 8.0+ | 8.0+ |
| Redis | 6.0+ | 7.0+ |
| Milvus | 2.5+ | 2.5+ |
| Elasticsearch | 9.2.1 | 9.2.1 |
| Memory | 4GB | 8GB+ |
| CPU | 2 cores | 4 cores+ |
### 1. Clone the Repository
```bash
git clone https://github.com/qiming-ai/qiming-backend.git
cd qiming-backend
```
### 2. Start Basic Services
- **Milvus Vector Database**: https://github.com/milvus-io/milvus
- **MySQL**: Install and configure MySQL, create database `agent_platform`, import `sql/init.sql`
- **Elasticsearch**: Install and configure Elasticsearch 9.2.1, configure Chinese word segmentation plugin for Chinese environment
- **mcp-proxy**: https://github.com/qiming-ai/mcp-proxy
- **Web development scheduling and agent sandbox service**: https://github.com/qiming-ai/rcoder
> For development and testing, you can use the official deployment package to run all basic services directly.
### 3. Add Configuration File
Copy `application-${env}.sample.yml` to `application-${env}.yml`:
### 4. Start the Application
**Option 1: Using IDE (IntelliJ IDEA, etc.)**
- Open `app-platform-bootstrap/app-platform-web-bootstrap/src/main/java/com/xspaceagi/PlatformApiApplication.java`
- Click the run button to start
**Option 2: Using Maven Commands**
```bash
# Build the project
mvn clean package -DskipTests
# Start the application
cd app-platform-bootstrap/app-platform-web-bootstrap
java -jar target/app-platform-web-bootstrap-*.jar
# Or run directly with Maven
mvn spring-boot:run -pl app-platform-bootstrap/app-platform-web-bootstrap -Pdev
```
**Option 3: Using IDEA Maven Panel**
- Open the Maven panel
- Find the `app-platform-web-bootstrap` module
- Right-click and select `Run` -> `spring-boot:run`
### 5. Access the Application
After starting, access the application (default port 8081):
- **API Documentation**: http://localhost:8081/doc.html
- **Health Check**: http://localhost:8081/health
---
## Module Description
### Project Structure
```
agent-platform/
├── app-platform-bootstrap/ # Bootstrap module
│ └── app-platform-web-bootstrap/ # Web application entry point
├── app-platform-foundation/ # Foundation module
│ ├── system-spec/ # System specification
│ ├── system-sdk/ # System SDK
│ └── system-domain/ # System domain models
├── app-platform-modules/ # Business modules
│ ├── app-platform-agent/ # Agent core module
│ ├── app-platform-knowledge/ # Knowledge base module
│ ├── app-platform-memory/ # Memory module
│ ├── app-platform-model-proxy/ # Model proxy module
│ ├── app-platform-mcp/ # MCP protocol module
│ ├── app-platform-compose/ # Custom table module
│ ├── app-platform-custom-page/ # Custom page module
│ ├── app-platform-eco-market/ # Ecosystem market module
│ ├── app-platform-sandbox/ # Sandbox module
│ ├── app-platform-log/ # Logging module
│ └── platform-system/ # Platform system module
├── fast-boot-dependencies/ # Dependency management
└── specs/ # Specification documents
```
### Core Modules
| Module | Description |
|--------|-------------|
| **Agent** | Agent creation, session management, message processing |
| **Knowledge** | Knowledge base management, document upload, vector search |
| **Memory** | Long-term memory storage and retrieval |
| **Model Proxy** | Unified model proxy interface |
| **MCP** | Model Context Protocol implementation |
| **Eco Market** | Plugin market and template management |
| **Compose** | Custom table functionality |
| **Custom Page** | No-code page builder |
---
## Configuration
### Configuration File Structure
```
app-platform-web-bootstrap/src/main/resources/
├── application.yml # Main configuration file (common settings)
├── application-dev.yml # Development environment configuration (local use, not committed)
├── application-test.yml # Test environment configuration (optional)
└── application-prod.yml # Production environment configuration (optional)
```
### Complete Environment Variable List
#### Database Configuration
| Variable | Description | Required | Default |
|----------|-------------|----------|---------|
| `DB_HOST` | MySQL database host | ✅ | localhost |
| `DB_PORT` | MySQL port | ✅ | 3306 |
| `DB_NAME` | Database name | ✅ | agent_platform |
| `DB_USERNAME` | Database username | ✅ | root |
| `DB_PASSWORD` | Database password | ✅ | - |
#### Custom Table Component Database (Doris not tested, supports MySQL)
| Variable | Description | Required | Default |
|----------|-------------|----------|---------|
| `DORIS_HOST` | Doris host | ✅ | localhost |
| `DORIS_DB_NAME` | Doris database name | ✅ | agent_custom_table |
| `DORIS_USERNAME` | Doris username | ✅ | root |
| `DORIS_PASSWORD` | Doris password | ✅ | - |
#### Redis Configuration
| Variable | Description | Required | Default |
|----------|-------------|----------|---------|
| `REDIS_HOST` | Redis host | ✅ | localhost |
| `REDIS_PORT` | Redis port | ✅ | 6379 |
| `REDIS_PASSWORD` | Redis password | ✅ | - |
| `REDIS_DB` | Redis database number | ✅ | 1 |
#### Milvus Vector Database
| Variable | Description | Required | Default |
|----------|-------------|----------|---------|
| `MILVUS_URI` | Milvus connection URI | ✅ | http://localhost:19530 |
| `MILVUS_USER` | Milvus username | ✅ | root |
| `MILVUS_PASSWORD` | Milvus password | ✅ | - |
#### Elasticsearch (Optional)
| Variable | Description | Required | Default |
|----------|-------------|----------|---------|
| `ES_URL` | Elasticsearch URL | ✅ | http://localhost:9200 |
| `ES_USERNAME` | Elasticsearch username | ✅ | elastic |
| `ES_PASSWORD` | Elasticsearch password | ✅ | - |
| `ES_API_KEY` | Elasticsearch API Key | ✅ | - |
#### Security Configuration
| Variable | Description | Required | Default |
|----------|-------------|----------|---------|
| `JWT_SECRET_KEY` | JWT signing key | ✅ | - (at least 32 characters) |
#### Tencent Cloud COS (Optional)
| Variable | Description | Required | Default |
|----------|-------------|----------|---------|
| `COS_SECRET_ID` | Tencent Cloud COS secret ID | ✅ | - |
| `COS_SECRET_KEY` | Tencent Cloud COS secret key | ✅ | - |
| `COS_BASE_URL` | COS access domain | ✅ | https://... |
#### File Storage
| Variable | Description | Required | Default |
|----------|-------------|----------|---------|
| `FILE_UPLOAD_FOLDER` | File upload directory | ✅ | /tmp/uploads |
| `FILE_BASE_URL` | File access URL | ✅ | https://yourdomain/api/file |
| `STORAGE_TYPE` | Storage type, currently supports Tencent Cloud COS and local file storage | ✅ | file |
#### External Services (Optional)
| Variable | Description | Required | Default |
|----------|-------------|----------|---------|
| `CODE_EXECUTE_URL` | Code execution service | ✅ | http://localhost:8020/... |
| `LOG_SERVICE_URL` | Log service URL, can be omitted, deprecated | ✅ | http://localhost:8097 |
| `MCP_PROXY_URL` | MCP proxy URL | ✅ | http://localhost:8020 |
#### Page Application Development Services (Optional)
| Variable | Description | Required | Default |
|----------|-------------|----------|---------|
| `DEV_SERVER_HOST` | Development server host | ✅ | http://localhost |
| `PROD_SERVER_HOST` | Production server host | ✅ | http://localhost:8099 |
| `BUILD_SERVER_URL` | Build server URL | ✅ | http://localhost:60000/api |
| `AI_AGENT_URL` | AI agent URL | ✅ | http://localhost:8086 |
| `DOCKER_PROXY_URL` | Docker proxy URL | ✅ | http://localhost:8088 |
#### Internal Network Tunnel Proxy Configuration (Optional)
| Variable | Description | Required | Default |
|----------|-------------|----------|---------|
| `SERVICE_HOST` | Internal service address | ❌ | 127.0.0.1 |
| `BIND_HOST` | Bind address | ❌ | 0.0.0.0 |
| `REVERSE_PORTS` | Port range | ❌ | 30000-40000 |
| `OUTER_HOST` | Client connection IP address | ❌ | - |
| `OUTER_PORT` | Client connection port | ❌ | 6443 |
#### Model Proxy (Optional)
| Variable | Description | Required | Default |
|----------|-------------|----------|---------|
| `MODEL_API_BASE_URL` | Model API URL, corresponding to accessible MODEL_PROXY_PORT | ❌ | - |
| `MODEL_PROXY_ENABLE` | Whether to enable proxy | ❌ | true |
| `MODEL_PROXY_PORT` | Proxy port | ❌ | 18086 |
| `MODEL_PROXY_SAVE_LOG` | Whether to save logs, depends on Elasticsearch | ❌ | true |
---
## Development Guide
### Code Standards
The project follows DDD layered architecture:
```
UI Layer → Adapter Layer → Application Layer → Domain Layer → Infrastructure Layer
```
---
## Contributing
Contributions of code, documentation, and bug reports are welcome!
1. Fork this repository
2. Create a feature branch (`git checkout -b feature/AmazingFeature`)
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
### Contributors
Thanks to all developers who have contributed to this project!
---
## License
This project is licensed under the **Apache License 2.0**.
See [LICENSE](LICENSE) file for details.
---
## Commercial License
### Open Source Use (Free)
This software can be used, modified, and distributed for free, but you must:
- ✅ Retain original copyright notices
- ✅ Include the Apache 2.0 license text
- ✅ State that your product is based on this software
### Commercial License (Paid)
If you need:
- 🏢 **Remove copyright notices** (white-label products)
- 🔄 **OEM partnership** (integrate into your products for resale)
- 🎨 **Custom branding** (use your own brand)
- 🚀 **Priority support** (enterprise-level technical support)
You can apply for a commercial license to obtain additional rights.
For details, see: [COMMERCIAL_LICENSE.md](COMMERCIAL_LICENSE.md)

View File

@@ -0,0 +1,515 @@
# Qiming Backend
[![Java](https://img.shields.io/badge/Java-17-orange.svg)](https://openjdk.java.net/)
[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.3.8-brightgreen.svg)](https://spring.io/projects/spring-boot)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
**企业级 AI 智能体开发与运行平台** - 提供完整的智能体创建分发、知识库管理、模型代理、记忆系统和插件生态解决方案。
[English](README.md) | 简体中文
---
## 目录
- [核心特性](#核心特性)
- [技术架构](#技术架构)
- [快速开始](#快速开始)
- [配置说明](#配置说明) ⚠️ **重要**
- [模块说明](#模块说明)
- [开发指南](#开发指南)
- [贡献指南](#贡献指南)
- [开源协议](#开源协议)
- [商业授权](#商业授权)
---
## 核心特性
### 智能体引擎
- **可视化编排**: 拖拽式工作流设计,零代码创建智能体
- **代码化开发**: 支持 Java/Python 插件开发,灵活扩展
- **多模型支持**: OpenAI、Claude、Ollama、通义千问等主流模型
- **流式输出**: 支持 SSE 实时响应,流畅的用户体验
- **多轮对话**: 完整的会话上下文管理
### 知识库管理
- **文档解析**: PDF、Word、Excel、Markdown 等多种格式
- **向量化存储**: Milvus 向量数据库,高效相似度检索
- **智能分块**: 自动文档切分与重叠处理
- **混合检索**: 向量检索 + 全文检索 + QA 检索
### 记忆系统
- **长期记忆**: 持久化用户数据与偏好
- **短期记忆**: 会话上下文管理
- **分类体系**: 12 种主分类、50+ 种子分类
- **敏感信息过滤**: 自动脱敏处理
### 模型代理(完善中)
- **统一接口**: 屏蔽不同模型 API 差异
- **智能路由**: 根据场景选择最优模型
- **配额管理**: Token 计费和限流控制
- **容错切换**: 自动故障转移
### 插件生态
- **插件市场**: 发布和发现插件
- **MCP 协议**: Model Context Protocol 支持
- **页面应用**: 无代码页面构建
---
## 技术架构
### 技术栈
| 技术类别 | 技术选型 | 版本 |
|---------|---------|------|
| 开发语言 | Java | 17 |
| 框架 | Spring Boot | 3.3.8 |
| 数据库 | MySQL | 8.0+ |
| 向量数据库 | Milvus | 2.5.4 |
| 缓存 | Redis | 7.0+ |
| ORM | MyBatis Plus | 3.5+ |
| API 文档 | Knife4j | 4.0+ |
| 容器化 | Docker | - |
| 编排 | Kubernetes | - |
### 架构图
```
┌────────────────────────────────────────────────────────────────────────────┐
│ 前端层 (Frontend Layer) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────────────────┐ │
│ │ PC端 │ │ H5 │ │ 小程序 │ │ IM (飞书/钉钉/企微) │ │
│ └──────────┘ └──────────┘ └──────────┘ └─────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────────────┐
│ 接入层 (Access Layer) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────────┐ │
│ │ REST API │ │ 长连接 │ │ WebSocket │ │
│ └──────────────┘ └──────────────┘ └──────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────────────┐
│ 应用层 (Application Layer) │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 组件库 (Component Library) │ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ │
│ │ │ 模型 │ │知识库 │ │数据表 │ │ 插件 │ │工作流 │ │ MCP │ │ 技能 │ │ │
│ │ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 管理端 (Management Portal) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────┐ │ │
│ │ │ 用户管理 │ │ 审核管理 │ │公共模型 │ │ 内容管理 │ │任务管理│ │ │
│ │ ├──────────┤ ├──────────┤ ├──────────┤ ├──────────┤ ├──────┤ │ │
│ │ │ 日志查询 │ │ 菜单权限 │ │ 系统配置 │ │ │ │ │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 产品应用 (Product Applications) │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │ │
│ │ │ 网页应用 │ │ 问答型智能体 │ │ 通用型智能体 │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────────────┐
│ 基础设施层 (Infrastructure Layer) │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 下层组件 (Lower-level Components) │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────────┐ │ │
│ │ │ 云端沙箱 │ │ qimingclaw │ │ 通用智能体引擎 │ │ │
│ │ │ │ │ 个人电脑 │ │ │ │
│ │ │ │ │ 客户端 │ │ ┌───────────────────────┐ │ │
│ │ │ │ │ (mac/win/ │ │ │ MCP 集成 │ │ │
│ │ │ │ │ docker) │ │ ├───────────────────────┤ │ │
│ │ │ │ │ │ │ │ 文件管理 │ │ │
│ │ │ │ │ │ │ │ SKILL 管理 │ │ │
│ │ │ │ │ │ │ │ ACP 适配层 │ │ │
│ │ │ │ │ │ │ │ ┌─────────────────┐ │ │ │
│ │ │ │ │ │ │ │ │ 支持 Agent: │ │ │ │
│ │ │ │ │ │ │ │ │ claudecode │ │ │ │
│ │ │ │ │ │ │ │ │ opencode │ │ │ │
│ │ │ │ │ │ │ │ │ codex │ │ │ │
│ │ │ │ │ │ │ │ │ openclaw │ │ │ │
│ │ │ │ │ │ │ │ │ kimicli │ │ │ │
│ │ │ │ │ │ │ │ └─────────────────┘ │ │ │
│ │ │ │ │ │ │ ├───────────────────────┤ │ │
│ │ │ │ │ │ │ │ 浏览器操作 │ │ │
│ │ │ │ │ │ │ │ GUI 操作 │ │ │
│ │ │ │ │ │ │ │ 网络通道 │ │ │
│ │ │ │ │ │ │ │ 基础运行环境集成 │ │ │
│ │ │ │ │ │ │ └───────────────────────┘ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 核心基础设施 (Core Infrastructure) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────┐ │ │
│ │ │ Database │ │ Cache │ │ Vector │ │ Search │ │ Model │ │ │
│ │ │ MySQL │ │ Redis │ │ Milvus │ │ Elastic │ │ Proxy│ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────┘
```
### 项目仓库纵览
Qiming AI 智能体平台由多个相互关联的仓库组成:
#### **前端与移动端**
| 仓库名称 | 描述 | 地址 |
|---------|------|------|
| **qiming** | 前端 Web | [https://github.com/qiming-ai/qiming](https://github.com/qiming-ai/qiming) |
| **qiming-mobile** | 移动端应用 | [https://github.com/qiming-ai/qiming-mobile](https://github.com/qiming-ai/qiming-mobile) |
| **noVNC** | 基于网页的 VNC 客户端 | [https://github.com/qiming-ai/noVNC](https://github.com/qiming-ai/noVNC) |
#### **后端与应用层**
| 仓库名称 | 描述 | 地址 |
|---------|------|------|
| **qiming-backend** | 应用层(后端)- **当前仓库** | [https://github.com/qiming-ai/qiming-backend](https://github.com/qiming-ai/qiming-backend) |
#### **智能体引擎与客户端**
| 仓库名称 | 描述 | 地址 |
|---------|------|------|
| **qimingclaw** | 智能体电脑客户端mac/win/docker| [https://github.com/qiming-ai/qimingclaw](https://github.com/qiming-ai/qimingclaw) |
| **qimingcode** | 启明智能体引擎(基于开源 opencode| [https://github.com/qiming-ai/qimingcode](https://github.com/qiming-ai/qimingcode) |
| **claude-code-acp-ts** | 基于 Zed 的 Claude Code ACP | [https://github.com/qiming-ai/claude-code-acp-ts](https://github.com/qiming-ai/claude-code-acp-ts) |
#### **基础设施与服务**
| 仓库名称 | 描述 | 地址 |
|---------|------|------|
| **rcoder** | 沙箱与容器调度(含通用智能体引擎)| [https://github.com/qiming-ai/rcoder](https://github.com/qiming-ai/rcoder) |
| **mcp-proxy** | MCP 服务qimingclaw 和沙箱均使用)| [https://github.com/qiming-ai/mcp-proxy](https://github.com/qiming-ai/mcp-proxy) |
| **qiming-file-server** | 文件服务(沙箱和 qimingclaw 使用,含 skill 同步)| [https://github.com/qiming-ai/qiming-file-server](https://github.com/qiming-ai/qiming-file-server) |
#### **网页应用开发**
| 仓库名称 | 描述 | 地址 |
|---------|------|------|
| **xagi-frontend-templates** | 网页应用开发模版 | [https://github.com/qiming-ai/xagi-frontend-templates](https://github.com/qiming-ai/xagi-frontend-templates) |
| **vite-plugin-design-mode** | 可视化编辑 Vite 插件 | [https://github.com/qiming-ai/vite-plugin-design-mode](https://github.com/qiming-ai/vite-plugin-design-mode) |
| **dev-inject** | 网页应用智能脚本注入 | [https://github.com/qiming-ai/dev-inject](https://github.com/qiming-ai/dev-inject) |
#### **插件与脚本执行**
| 仓库名称 | 描述 | 地址 |
|---------|------|------|
| **run_code_rmcp** | 插件脚本执行TS/JS/Python| [https://github.com/qiming-ai/run_code_rmcp](https://github.com/qiming-ai/run_code_rmcp) |
#### **网络与工具**
| 仓库名称 | 描述 | 地址 |
|---------|------|------|
| **lanproxy-go-client** | 网络穿透客户端qimingclaw 使用)| [https://github.com/ffay/lanproxy-go-client](https://github.com/ffay/lanproxy-go-client) |
---
### 设计模式
- **DDD 领域驱动设计**: 清晰的层次划分
- **CQRS**: 命令查询职责分离
- **策略模式**: 模型选择、插件调用
- **观察者模式**: 消息推送
- **仓储模式**: 数据访问抽象
---
## 快速开始
### 环境要求
| 组件 | 最低要求 | 推荐配置 |
|--------|--------|------------|
| Java | JDK 17 | OpenJDK 17 |
| MySQL | 8.0+ | 8.0+ |
| Redis | 6.0+ | 7.0+ |
| Milvus | 2.5+ | 2.5+ |
| Elacticsearch | 9.2.1 | 9.2.1 |
| 内存 | 4GB | 8GB+ |
| CPU | 2 核 | 4 核+ |
### 1. 克隆项目
```bash
git clone https://github.com/qiming-ai/qiming-backend.git
cd qiming-backend
```
### 2. 启动基础服务
- Milvus 向量数据库 https://github.com/milvus-io/milvus
- 安装配置mysql创建数据库 `agent_platform`,导入 sql/init.sql
- 安装配置 Elasticsearch 9.2.1,中文环境需要配置中文分词插件
- mcp-proxy项目部署 https://github.com/qiming-ai/mcp-proxy
- 网页开发调度以及智能体沙箱服务 https://github.com/qiming-ai/rcoder
> 开发测试时可以将官方提供的部署安装包跑起来后直接使用各项基础服务
### 3. 增加配置文件
`application-${env}.sample.yml` 文件复制为 `application-${env}.yml` 文件:
### 4. 启动应用
**方式 1: 使用 IDEA 等 IDE 启动**
- 打开 `app-platform-bootstrap/app-platform-web-bootstrap/src/main/java/com/xspaceagi/PlatformApiApplication.java`
- 点击运行按钮启动
**方式 2: 使用 Maven 命令**
```bash
# 编译项目
mvn clean package -DskipTests
# 启动应用
cd app-platform-bootstrap/app-platform-web-bootstrap
java -jar target/app-platform-web-bootstrap-*.jar
# 或使用 Maven 直接运行
mvn spring-boot:run -pl app-platform-bootstrap/app-platform-web-bootstrap -Pdev
```
**方式 3: 使用 IDEA Maven 面板**
- 打开 Maven 面板
- 找到 `app-platform-web-bootstrap` 模块
- 右键选择 `Run` -> `spring-boot:run`
### 5. 访问应用
应用启动后,访问(默认端口 8081
- **API 文档**: http://localhost:8081/doc.html
- **健康检查**: http://localhost:8081/health
---
## 模块说明
### 项目结构
```
agent-platform/
├── app-platform-bootstrap/ # 启动模块
│ └── app-platform-web-bootstrap/ # Web 应用启动入口
├── app-platform-foundation/ # 基础模块
│ ├── system-spec/ # 系统规范定义
│ ├── system-sdk/ # 系统 SDK
│ └── system-domain/ # 系统领域模型
├── app-platform-modules/ # 业务模块
│ ├── app-platform-agent/ # 智能体核心模块
│ ├── app-platform-knowledge/ # 知识库模块
│ ├── app-platform-memory/ # 记忆模块
│ ├── app-platform-model-proxy/ # 模型代理模块
│ ├── app-platform-mcp/ # MCP 协议模块
│ ├── app-platform-compose/ # 自定义表格模块
│ ├── app-platform-custom-page/ # 自定义页面模块
│ ├── app-platform-eco-market/ # 生态市场模块
│ ├── app-platform-sandbox/ # 沙箱模块
│ ├── app-platform-log/ # 日志模块
│ └── platform-system/ # 平台系统模块
├── fast-boot-dependencies/ # 依赖管理
└── specs/ # 规格文档
```
### 核心模块
| 模块 | 说明 |
|-----|---------------------------|
| **Agent** | 智能体创建、会话管理、消息处理 |
| **Knowledge** | 知识库管理、文档上传、向量检索 |
| **Memory** | 长期记忆存储与检索 |
| **Model Proxy** | 模型代理调用接口 |
| **MCP** | Model Context Protocol 实现 |
| **Eco Market** | 插件市场与模板管理 |
| **Compose** | 自定义表格功能 |
| **Custom Page** | 无代码页面构建 |
---
## 配置说明
### 配置文件结构
```
app-platform-web-bootstrap/src/main/resources/
├── application.yml # 主配置文件(公共配置)
├── application-dev.yml # 开发环境配置(本地使用,不提交)
├── application-test.yml # 测试环境配置(可选)
└── application-prod.yml # 生产环境配置(可选)
```
### 环境变量完整列表
#### 数据库配置
| 变量名 | 说明 | 必填 | 默认值 |
|-------|------|-----|--------|
| `DB_HOST` | MySQL 数据库地址 | ✅ | localhost |
| `DB_PORT` | MySQL 端口 | ✅ | 3306 |
| `DB_NAME` | 数据库名称 | ✅ | agent_platform |
| `DB_USERNAME` | 数据库用户名 | ✅ | root |
| `DB_PASSWORD` | 数据库密码 | ✅ | - |
#### 数据表组件依赖的数据库Doris库未测试支持mysql
| 变量名 | 说明 | 必填 | 默认值 |
|-------|------|--|--------|
| `DORIS_HOST` | Doris 地址 | ✅ | localhost |
| `DORIS_DB_NAME` | Doris 数据库名 | ✅ | agent_custom_table |
| `DORIS_USERNAME` | Doris 用户名 | ✅ | root |
| `DORIS_PASSWORD` | Doris 密码 | ✅ | - |
#### Redis 配置
| 变量名 | 说明 | 必填 | 默认值 |
|-------|------|----|--------|
| `REDIS_HOST` | Redis 地址 | ✅ | localhost |
| `REDIS_PORT` | Redis 端口 | ✅ | 6379 |
| `REDIS_PASSWORD` | Redis 密码 | ✅ | - |
| `REDIS_DB` | Redis 数据库编号 | ✅ | 1 |
#### Milvus 向量数据库
| 变量名 | 说明 | 必填 | 默认值 |
|-------|------|-----|--------|
| `MILVUS_URI` | Milvus 连接地址 | ✅ | http://localhost:19530 |
| `MILVUS_USER` | Milvus 用户名 | ✅ | root |
| `MILVUS_PASSWORD` | Milvus 密码 | ✅ | - |
#### Elasticsearch可选
| 变量名 | 说明 | 必填 | 默认值 |
|-------|------|--|--------|
| `ES_URL` | Elasticsearch 地址 | ✅ | http://localhost:9200 |
| `ES_USERNAME` | Elasticsearch 用户名 | ✅ | elastic |
| `ES_PASSWORD` | Elasticsearch 密码 | ✅ | - |
| `ES_API_KEY` | Elasticsearch API Key | ✅ | - |
#### 安全配置
| 变量名 | 说明 | 必填 | 默认值 |
|-------|------|------|--------|
| `JWT_SECRET_KEY` | JWT 签名密钥 | ✅ | - (至少32位) |
#### 腾讯云 COS可选
| 变量名 | 说明 | 必填 | 默认值 |
|-------|------|---|--------|
| `COS_SECRET_ID` | 腾讯云 COS 密钥 ID | ✅ | - |
| `COS_SECRET_KEY` | 腾讯云 COS 密钥 Key | ✅ | - |
| `COS_BASE_URL` | COS 访问域名 | ✅ | https://... |
#### 文件存储
| 变量名 | 说明 | 必填 | 默认值 |
|----------------------|--------------------------|---|-----------------------------|
| `FILE_UPLOAD_FOLDER` | 文件上传目录 | ✅ | /tmp/uploads |
| `FILE_BASE_URL` | 文件访问 URL | ✅ | https://yourdomain/api/file |
| `STORAGE_TYPE` | 存储类型暂时支持腾讯云cos和本地存储file | ✅ | file |
#### 外部服务(可选)
| 变量名 | 说明 | 必填 | 默认值 |
|-------|------------------|---|--------|
| `CODE_EXECUTE_URL` | 代码执行服务 | ✅ | http://localhost:8020/... |
| `LOG_SERVICE_URL` | 日志服务地址,可以不配置,已废弃 | ✅ | http://localhost:8097 |
| `MCP_PROXY_URL` | MCP 代理地址 | ✅ | http://localhost:8020 |
#### 页面应用开发服务(可选)
| 变量名 | 说明 | 必填 | 默认值 |
|-------|------|--|--------|
| `DEV_SERVER_HOST` | 开发服务器地址 | ✅ | http://localhost |
| `PROD_SERVER_HOST` | 生产服务器地址 | ✅ | http://localhost:8099 |
| `BUILD_SERVER_URL` | 构建服务地址 | ✅ | http://localhost:60000/api |
| `AI_AGENT_URL` | AI 智能体地址 | ✅ | http://localhost:8086 |
| `DOCKER_PROXY_URL` | Docker 代理地址 | ✅ | http://localhost:8088 |
#### 内网穿透代理配置(可选)
| 变量名 | 说明 | 必填 | 默认值 |
|-------|-----------|------|-------------|
| `SERVICE_HOST` | 内部服务地址 | ❌ | 127.0.0.1 |
| `BIND_HOST` | 绑定地址 | ❌ | 0.0.0.0 |
| `REVERSE_PORTS` | 端口范围 | ❌ | 30000-40000 |
| `OUTER_HOST` | 客户端连接IP地址 | ❌ | - |
| `OUTER_PORT` | 客户端连接端口 | ❌ | 6443 |
#### 模型代理(可选)
| 变量名 | 说明 | 必填 | 默认值 |
|-------|-----------------------------------|------|--------|
| `MODEL_API_BASE_URL` | 模型 API 地址对应可访问MODEL_PROXY_PORT端口 | ❌ | - |
| `MODEL_PROXY_ENABLE` | 是否启用代理 | ❌ | true |
| `MODEL_PROXY_PORT` | 代理端口 | ❌ | 18086 |
| `MODEL_PROXY_SAVE_LOG` | 是否保存日志依赖elasticsearch | ❌ | true |
---
## 开发指南
### 代码规范
项目遵循 DDD 分层架构:
```
UI 层 → Adapter 层 → Application 层 → Domain 层 → Infrastructure 层
```
---
## 贡献指南
欢迎贡献代码、文档、Bug 报告!
1. Fork 本仓库
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 开启 Pull Request
### 贡献者
感谢所有为这个项目做出贡献的开发者!
---
## 开源协议
本项目采用 **Apache License 2.0** 开源协议。
详见 [LICENSE](LICENSE) 文件。
---
## 商业授权
### 开源使用(免费)
本软件可以免费使用、修改和分发,但需要:
- ✅ 保留原始版权声明
- ✅ 包含 Apache 2.0 许可证文本
- ✅ 说明你的产品基于本软件
### 商业授权(付费)
如果你需要:
- 🏢 **移除版权声明**(白标产品)
- 🔄 **OEM 合作**(集成到你的产品中销售)
- 🎨 **自定义品牌**(使用你自己的品牌)
- 🚀 **优先支持**(企业级技术支持)
可以申请商业授权,获取额外权利。
详细信息请查看:[COMMERCIAL_LICENSE.md](COMMERCIAL_LICENSE.md)

View File

@@ -0,0 +1,142 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-bootstrap</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-web-bootstrap</artifactId>
<dependencies>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-mcp-ui</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-agent-core-ui</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-im-web</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-knowledge-core-ui</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-knowledge-api</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-compose-api</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-compose-ui</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-log-web</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-log-api</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-eco-market-client-web</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-eco-market-api</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-custom-page-ui</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-sandbox-ui</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-memory-api</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-model-proxy-application</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-file-web</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>system-api</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>system-web</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-credit-web</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-subscription-web</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-pricing-web</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-pay-web</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-bill-web</artifactId>
</dependency>
</dependencies>
<profiles>
<profile>
<id>dev</id>
<properties>
<!-- 环境标识,需要与配置文件的名称相对应 -->
<activatedProperties>dev</activatedProperties>
</properties>
<activation>
<!-- 默认环境 -->
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<profile>
<id>test</id>
<properties>
<activatedProperties>test</activatedProperties>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<activatedProperties>prod</activatedProperties>
</properties>
</profile>
</profiles>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,19 @@
package com.xspaceagi;
import com.xspaceagi.system.domain.log.EnableLogPrint;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableScheduling;
@EnableScheduling
@EnableCaching
@EnableLogPrint
@SpringBootApplication(scanBasePackages = {"com.xspaceagi", "io.springfox"})
public class PlatformApiApplication {
public static void main(String[] args) {
SpringApplication.run(PlatformApiApplication.class, args);
}
}

View File

@@ -0,0 +1,20 @@
//package com.xspaceagi.config;
//
//import org.springframework.context.annotation.Bean;
//import org.springframework.context.annotation.Configuration;
//
//
//@Configuration
//@EnableSwagger
//public class SwaggerConfig {
//
// @Bean
// public Docket createRestApiOpRecord() {
// return new Docket(DocumentationType.SWAGGER_2).groupName("接口文档").useDefaultResponseMessages(false).apiInfo(apiInfo()).select()
// .apis(RequestHandlerSelectors.basePackage("com.xspaceagi.agent.web.ui")).paths(PathSelectors.any()).build();
// }
//
// private ApiInfo apiInfo() {
// return new ApiInfoBuilder().title("应用发布页接口文档v1").description("接口文档").version("v1").build();
// }
//}

View File

@@ -0,0 +1,46 @@
package com.xspaceagi.config;
import java.time.LocalDateTime;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.fasterxml.jackson.core.StreamReadConstraints;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.xspaceagi.log.spec.CustomLocalDateTimeSerializer;
import com.xspaceagi.log.spec.FlexibleDateTimeDeserializer;
/**
* UTC 时间序列化配置
*/
@Configuration
public class UTCDateTimeConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
return builder -> {
// 使用 SimpleModule 统一注册序列化器和反序列化器
SimpleModule module = new SimpleModule();
// 注册自定义序列化器
module.addSerializer(LocalDateTime.class, new CustomLocalDateTimeSerializer());
// 注册自定义反序列化器
module.addDeserializer(LocalDateTime.class, new FlexibleDateTimeDeserializer());
builder.modules(module);
// 配置 StreamReadConstraints 以支持更大的字符串长度
// 设置最大字符串长度为 100MB (100 * 1024 * 1024 字符),用于支持大文件内容
builder.postConfigurer(objectMapper -> {
StreamReadConstraints constraints = StreamReadConstraints.builder()
.maxStringLength(100 * 1024 * 1024)
.build();
objectMapper.getFactory().setStreamReadConstraints(constraints);
});
};
}
}

View File

@@ -0,0 +1,34 @@
package com.xspaceagi.config;
import io.milvus.v2.client.ConnectConfig;
import io.milvus.v2.client.MilvusClientV2;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
@Slf4j
@Configuration
public class VectorDBConfig {
@Value("${milvus.uri}")
private String milvusUri;
@Lazy
@Bean
public MilvusClientV2 milvusClient() {
log.info("milvus uri:{}", milvusUri);
ConnectConfig config = ConnectConfig.builder()
.uri(milvusUri)
.build();
MilvusClientV2 client = new MilvusClientV2(config);
log.info("milvus client init success, uri:{}", milvusUri);
return client;
}
}

View File

@@ -0,0 +1,101 @@
package com.xspaceagi.config;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.util.Assert;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.baomidou.mybatisplus.annotation.IEnum;
import com.xspaceagi.interceptor.ApiKeyInterceptor;
import com.xspaceagi.interceptor.AuthInterceptor;
import com.xspaceagi.interceptor.HttpInterceptor;
import com.xspaceagi.interceptor.TIdInterceptor;
import jakarta.annotation.Resource;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private HttpInterceptor httpInterceptor;
@Autowired
private AuthInterceptor jwtInterceptor;
/**
* tid统一标识处理
*/
@Resource
private TIdInterceptor tidInterceptor;
@Resource
private ApiKeyInterceptor apiKeyInterceptor;
@Value("${access.control.allow-origin}")
private String accessControlAllowOrigin;
@Override
public void addInterceptors(InterceptorRegistry interceptorRegistry) {
// 请求参数记录日志拦截器
interceptorRegistry.addInterceptor(tidInterceptor).order(1).addPathPatterns("/api/**");
interceptorRegistry.addInterceptor(httpInterceptor).order(21).addPathPatterns("/api/**");
interceptorRegistry.addInterceptor(jwtInterceptor).order(22).addPathPatterns("/api/**");
interceptorRegistry.addInterceptor(apiKeyInterceptor).order(23).addPathPatterns("/api/**");
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 允许所有请求路径跨域访问
.allowCredentials(true) // 是否携带Cookie默认false
.allowedHeaders("Origin", "X-Requested-With", "Content-Type", "Accept", "Authorization",
"Cache-Control", "Fragment") // 允许的请求头类型
.maxAge(3600) // 预检请求的缓存时间(单位:秒)
.allowedMethods("HEAD", "GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许的请求方法类型
.allowedOriginPatterns(accessControlAllowOrigin); // 允许哪些域名进行跨域访问
}
@Bean
public HttpMessageConverter<String> responseBodyConverter() {
Charset defaultCharset = StandardCharsets.UTF_8;
return new StringHttpMessageConverter(defaultCharset);
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(responseBodyConverter());
}
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverterFactory(new ConverterFactory<String, IEnum>() {
@Override
public <T extends IEnum> Converter<String, T> getConverter(Class<T> targetType) {
return source -> {
Assert.isTrue(targetType.isEnum(), () -> "the class what implements IEnum must be the enum type");
return Arrays.stream(targetType.getEnumConstants())
.filter(t -> String.valueOf(t.getValue()).equals(source))
.findFirst().orElse(null);
};
}
});
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
}
}

View File

@@ -0,0 +1,18 @@
package com.xspaceagi.ctl;
import com.xspaceagi.system.spec.dto.ReqResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/")
@Slf4j
public class HealthController {
@RequestMapping(path = "/health")
public ReqResult<Void> health() {
return ReqResult.success();
}
}

View File

@@ -0,0 +1,110 @@
package com.xspaceagi.filter;
import com.xspaceagi.sandbox.SandboxApiRewriteProperties;
import com.xspaceagi.sandbox.SandboxRequestAttributes;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import java.io.IOException;
import java.util.List;
/**
* Sandbox 兼容 API URL 重写过滤器(全局生效)
*/
@Component
@Order(1)
@Slf4j
public class SandboxApiRewriteFilter implements Filter {
private static final String SANDBOX_PREFIX = "/api/v1/4sandbox";
/**
* 允许重写的路径模式(相对于 /api/v1/4sandbox 之后的部分匹配)
* 支持通配符:*、**AntPathMatcher
*/
private final SandboxApiRewriteProperties rewriteProperties;
private final AntPathMatcher pathMatcher = new AntPathMatcher();
public SandboxApiRewriteFilter(SandboxApiRewriteProperties rewriteProperties) {
this.rewriteProperties = rewriteProperties;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
if (requestURI == null || !requestURI.startsWith(SANDBOX_PREFIX)) {
chain.doFilter(request, response);
return;
}
// 标记请求来源,供 controller 做沙箱/前端差异处理
httpRequest.setAttribute(SandboxRequestAttributes.REQUEST_SOURCE, SandboxRequestAttributes.SOURCE_SANDBOX);
String suffix = requestURI.substring(SANDBOX_PREFIX.length());
String pathAfterPrefix = suffix.startsWith("/") ? suffix.substring(1) : suffix;
if (!isAllowRewrite(pathAfterPrefix)) {
chain.doFilter(request, response);
return;
}
// 保留原始 URI/api/v1/4sandbox/...),给鉴权拦截器做白名单/AK 判断
httpRequest.setAttribute(SandboxRequestAttributes.ORIGINAL_REQUEST_URI, requestURI);
String rewrittenURI = "/api" + suffix; // suffix 以 '/' 开头更安全
log.debug("Rewrite 4Sandbox API URL: {} -> {}", requestURI, rewrittenURI);
HttpServletRequest wrappedRequest = new RewriteHttpServletRequestWrapper(httpRequest, rewrittenURI);
chain.doFilter(wrappedRequest, response);
}
private boolean isAllowRewrite(String pathAfterPrefix) {
List<String> patterns = rewriteProperties.getAllowPath();
if (patterns == null || patterns.isEmpty()) {
return false;
}
for (String rawPattern : patterns) {
String pattern = rawPattern == null ? "" : rawPattern.trim();
if (pattern.startsWith("/")) {
pattern = pattern.substring(1);
}
if (pattern.isEmpty()) {
continue;
}
if (pathMatcher.match(pattern, pathAfterPrefix)) {
return true;
}
}
return false;
}
private static class RewriteHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final String rewrittenURI;
public RewriteHttpServletRequestWrapper(HttpServletRequest request, String rewrittenURI) {
super(request);
this.rewrittenURI = rewrittenURI;
}
@Override
public String getRequestURI() {
return rewrittenURI;
}
@Override
public String getServletPath() {
// Spring MVC 路由匹配使用 servletPath/requestURI 等信息;统一返回改写后的路径。
return rewrittenURI;
}
}
}

View File

@@ -0,0 +1,106 @@
package com.xspaceagi.interceptor;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* AES加解密工具类
* 支持多种加密模式和填充方式
*
* @author Qiming
*/
public class AESCrypto {
// 加密算法
private static final String ALGORITHM = "AES";
// 默认使用AES-256-CBC模式
private static final String DEFAULT_TRANSFORMATION = "AES/CBC/PKCS5Padding";
/**
* AES加密支持自定义模式
*
* @param plainText 明文
* @param key 密钥
* @param iv 初始化向量ECB模式可为null
* @param transformation 转换模式AES/CBC/PKCS5Padding
* @return Base64编码的密文
*/
public static String encrypt(String plainText, String key, String iv, String transformation) throws Exception {
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM);
Cipher cipher = Cipher.getInstance(transformation);
// 判断是否需要IV
if (transformation.contains("ECB")) {
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
} else {
IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
}
byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encrypted);
}
/**
* AES解密支持自定义模式
*
* @param cipherText Base64编码的密文
* @param key 密钥
* @param iv 初始化向量ECB模式可为null
* @param transformation 转换模式
* @return 解密后的明文
*/
public static String decrypt(String cipherText, String key, String iv, String transformation) throws Exception {
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM);
Cipher cipher = Cipher.getInstance(transformation);
// 判断是否需要IV
if (transformation.contains("ECB")) {
cipher.init(Cipher.DECRYPT_MODE, secretKey);
} else {
IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec);
}
byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(cipherText));
return new String(decrypted, StandardCharsets.UTF_8);
}
/**
* AES解密使用Hex密钥
*
* @param cipherText Base64编码的密文
* @param keyHex 十六进制密钥
* @param ivHex 十六进制IV
* @return 解密后的明文
*/
public static String decryptWithHexKey(String cipherText, String keyHex, String ivHex) throws Exception {
byte[] keyBytes = hexStringToByteArray(keyHex);
byte[] ivBytes = hexStringToByteArray(ivHex);
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, ALGORITHM);
Cipher cipher = Cipher.getInstance(DEFAULT_TRANSFORMATION);
IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec);
byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(cipherText));
return new String(decrypted, StandardCharsets.UTF_8);
}
/**
* 十六进制字符串转字节数组
*/
private static byte[] hexStringToByteArray(String hex) {
int len = hex.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i + 1), 16));
}
return data;
}
}

View File

@@ -0,0 +1,180 @@
package com.xspaceagi.interceptor;
import com.xspaceagi.agent.core.adapter.application.AgentApplicationService;
import com.xspaceagi.agent.core.adapter.dto.AgentDetailDto;
import com.xspaceagi.sandbox.SandboxRequestAttributes;
import com.xspaceagi.system.application.dto.TenantConfigDto;
import com.xspaceagi.system.application.dto.UserDto;
import com.xspaceagi.system.application.service.UserApiKeyApplicationService;
import com.xspaceagi.system.application.service.UserApplicationService;
import com.xspaceagi.system.infra.dao.entity.OpenApiDefinition;
import com.xspaceagi.system.infra.dao.entity.User;
import com.xspaceagi.system.sdk.service.UserAccessKeyApiService;
import com.xspaceagi.system.sdk.service.dto.UserAccessKeyDto;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
import com.xspaceagi.system.spec.enums.YesOrNoEnum;
import com.xspaceagi.system.spec.exception.BizException;
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import java.util.List;
@Component
public class ApiKeyInterceptor implements HandlerInterceptor {
@Resource
private UserApplicationService userApplicationService;
@Resource
private AgentApplicationService agentApplicationService;
@Resource
private UserAccessKeyApiService userAccessKeyApiService;
@Resource
private UserApiKeyApplicationService userApiKeyApplicationService;
private final AntPathMatcher pathMatcher = new AntPathMatcher();
/**
* 请求处理完之后
*/
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object obj, Exception exc) throws Exception {
RequestContext.remove();
}
/**
* 请求处理完成
*/
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object obj, ModelAndView model) throws Exception {
}
/**
* 请求处理之前
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String originalRequestUri = getOriginalRequestUri(request);
String authorization = request.getHeader("Authorization");
if (authorization != null) {
authorization = authorization.replaceFirst("Basic", "").replaceFirst("Bearer", "").trim();
}
//是否为ak校验
if (authorization != null && (authorization.startsWith("ak-") || authorization.startsWith("ck-"))) {
UserAccessKeyDto userAccessKeyDto = userAccessKeyApiService.queryAccessKey(authorization);
if (userAccessKeyDto == null || (userAccessKeyDto.getStatus() != null && userAccessKeyDto.getStatus() != 1)) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.apiKeyInvalid);
}
if (userAccessKeyDto.getExpire() != null && userAccessKeyDto.getExpire().getTime() < System.currentTimeMillis()) {
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.apiKeyExpired);
}
if (!completeAuthContext(request, userAccessKeyDto)) {
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.apiKeyAccessDenied);
}
return true;
}
if (originalRequestUri.startsWith("/api/v1/")) {
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.apiKeyMissing);
}
return true;
}
private boolean completeAuthContext(HttpServletRequest request, UserAccessKeyDto userAccessKeyDto) {
String originalRequestUri = getOriginalRequestUri(request);
UserDto userDto = userApplicationService.queryById(userAccessKeyDto.getUserId());
if (userDto == null || userDto.getStatus() == User.Status.Disabled || userDto.getStatus() == User.Status.Deleted) {
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.apiKeyUserDisabled);
}
RequestContext.get().setUserId(userDto.getId());
RequestContext.get().setUser(userDto);
RequestContext.get().setUserContext(UserDto.convertToUserContext(userDto));
RequestContext.get().setLogin(true);
RequestContext.get().setUserAccessKey(userAccessKeyDto);
if (userAccessKeyDto.getTargetType() == UserAccessKeyDto.AKTargetType.Agent) {
if (originalRequestUri.startsWith("/api/v1/") || originalRequestUri.startsWith("/api/file/")) {
TenantConfigDto tenantConfigDto = (TenantConfigDto) RequestContext.get().getTenantConfig();
if (tenantConfigDto.getAllowAgentApi() != null && tenantConfigDto.getAllowAgentApi().equals(YesOrNoEnum.N.getKey()) && userDto.getRole() != User.Role.Admin) {
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.apiKeyAgentApiDisabled);
}
AgentDetailDto agentDetailDto = agentApplicationService.queryAgentDetail(Long.parseLong(userAccessKeyDto.getTargetId()), false);
RequestContext.get().setAkTarget(agentDetailDto);
return true;
}
}
if (userAccessKeyDto.getTargetType() == UserAccessKeyDto.AKTargetType.OpenApi) {
String path = request.getRequestURI();//path参考格式 /api/v1/user/add、/api/v1/user/1011OpenApiDefinition的配置可能为/api/v1/user/:id做匹配时要适配 :xx 为实际值)
List<OpenApiDefinition> openApiDefinitions = userApiKeyApplicationService.queryOpenApiDefinitions(userDto.getId());
OpenApiDefinition openApiDefinition = matchOpenApiDefinition(path, openApiDefinitions);// 从openApiDefinitions中过滤得到
if (openApiDefinition == null) {
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.apiKeyOpenApiNotFound);
}
request.setAttribute("currentAPI", openApiDefinition);
request.setAttribute("userAccessKey", userAccessKeyDto);
//普通用户不能使用管理员接口
if (openApiDefinition.getRole() == User.Role.Admin && userDto.getRole() != User.Role.Admin) {
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied);
}
List<UserAccessKeyDto.ApiConfig> apiConfigs = userAccessKeyDto.getConfig().getApiConfigs();
// apiConfigs如果为空则有全部接口权限
if (CollectionUtils.isNotEmpty(apiConfigs) && apiConfigs.stream().noneMatch(apiConfig -> openApiDefinition.getKey().equals(apiConfig.getKey()))) {
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied);
}
userApiKeyApplicationService.requestLimitCheck(userDto.getId(), openApiDefinition);
return true;
}
// 允许沙箱访问的接口范围
if (userAccessKeyDto.getTargetType() == UserAccessKeyDto.AKTargetType.Sandbox && originalRequestUri.startsWith("/api/v1/4sandbox")) {
return true;
}
return userAccessKeyDto.getTargetType() == UserAccessKeyDto.AKTargetType.Tenant;
}
public OpenApiDefinition matchOpenApiDefinition(String path, List<OpenApiDefinition> definitions) {
if (definitions == null || definitions.isEmpty()) {
return null;
}
for (OpenApiDefinition def : definitions) {
// 当前节点匹配
if (def.getPath() != null && def.getKey().equals("api.v1.chat.file") && pathMatcher.matchStart(convertPathPattern(def.getPath()), path)) {
return def;
}
if (def.getPath() != null && pathMatcher.match(convertPathPattern(def.getPath()), path)) {
return def;
}
// 递归匹配子节点
OpenApiDefinition matched = matchOpenApiDefinition(path, def.getApiList());
if (matched != null) {
return matched;
}
}
return null;
}
private static String convertPathPattern(String path) {
if (path == null || !path.contains(":")) {
return path;
}
return path.replaceAll(":([a-zA-Z_][a-zA-Z0-9_]*)", "{$1}");
}
private String getOriginalRequestUri(HttpServletRequest request) {
Object val = request.getAttribute(SandboxRequestAttributes.ORIGINAL_REQUEST_URI);
if (val instanceof String s && !s.isEmpty()) {
return s;
}
return request.getRequestURI();
}
}

View File

@@ -0,0 +1,125 @@
package com.xspaceagi.interceptor;
import com.xspaceagi.pay.spec.exception.PayGatewayBizException;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
import com.xspaceagi.system.spec.exception.*;
import jakarta.validation.UnexpectedTypeException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.resource.NoResourceFoundException;
@Slf4j
@ControllerAdvice
public class AppExceptionHandler {
/**
* 处理异常消息,如果是业务异常将通过json的格式返回否则根据模式来处理异常的显示.
*/
@ExceptionHandler
@ResponseBody
public ReqResult<?> processException(Exception ex) {
ReqResult<?> responseData;
if (ex instanceof PayGatewayBizException payGatewayBizException) {
responseData = ReqResult.error(
payGatewayBizException.getCode(),
payGatewayBizException.getDisplayCode(),
payGatewayBizException.getMessage());
} else if (ex instanceof BizException bizException) {
String message = resolveBizExceptionMessage(bizException);
responseData = ReqResult.error(bizException.getCode(), message);
} else {
if (ex instanceof SpacePermissionException) {
log.warn("Exception[SpacePermissionException] ", ex);
responseData = ReqResult.error(ErrorCodeEnum.PERMISSION_DENIED.getCode(),
ErrorCodeEnum.PERMISSION_DENIED.getMsg());
} else if (ex instanceof ResourcePermissionException) {
responseData = ReqResult.error(ErrorCodeEnum.RESOURCE_PERMISSION_DENIED.getCode(),
ErrorCodeEnum.RESOURCE_PERMISSION_DENIED.getMsg());
} else if (ex instanceof HttpRequestMethodNotSupportedException) {
log.warn("Exception[HttpRequestMethodNotSupportedException] ", ex);
responseData = ReqResult.error(ErrorCodeEnum.METHOD_NOT_ALLOWED.getCode(), ex.getMessage());
} else if (ex instanceof MissingServletRequestParameterException) {
log.warn("Exception[MissingServletRequestParameterException] ", ex);
responseData = ReqResult.error(ErrorCodeEnum.INVALID_PARAM.getCode(), ex.getMessage());
} else if (ex instanceof HttpMessageNotReadableException) {
log.warn("Exception[HttpMessageNotReadableException] ", ex);
responseData = ReqResult.error(ErrorCodeEnum.INVALID_PARAM.getCode(), ex.getMessage());
} else if (ex instanceof MethodArgumentNotValidException methodArgumentNotValidException) {
log.warn("Exception[MethodArgumentNotValidException] ", ex);
String msg = methodArgumentNotValidException.getBindingResult().getFieldError().getDefaultMessage();
responseData = ReqResult.error(ErrorCodeEnum.INVALID_PARAM.getCode(), msg);
} else if (ex instanceof UnexpectedTypeException) {
log.warn("Exception[UnexpectedTypeException] ", ex);
responseData = ReqResult.error(ErrorCodeEnum.INVALID_PARAM.getCode(), ex.getMessage());
} else if (ex instanceof HttpMediaTypeNotSupportedException) {
log.warn("Exception[HttpMediaTypeNotSupportedException] ", ex);
responseData = ReqResult.error(ErrorCodeEnum.INVALID_PARAM.getCode(), ex.getMessage());
} else if (ex instanceof KnowledgeException knowledgeException) {
log.warn("Exception[KnowledgeException] ", ex);
responseData = ReqResult.error(knowledgeException.getCode(), knowledgeException.getDisplayCode(),
ex.getMessage());
} else if (ex instanceof SystemManagerException systemManagerException) {
log.warn("Exception[SystemManagerException] ", ex);
responseData = ReqResult.error(systemManagerException.getCode(),
systemManagerException.getDisplayCode(), ex.getMessage());
} else if (ex instanceof ComposeException composeException) {
responseData = ReqResult.error(composeException.getCode(), composeException.getDisplayCode(),
composeException.getMessage());
} else if (ex instanceof EcoMarketException ecoMarketException) {
responseData = ReqResult.error(ecoMarketException.getCode(), ecoMarketException.getDisplayCode(),
ecoMarketException.getMessage());
} else if (ex instanceof IllegalArgumentException) {
responseData = ReqResult.error(ErrorCodeEnum.INVALID_PARAM.getCode(), ex.getMessage());
} else if (ex instanceof NoResourceFoundException) {
responseData = ReqResult.error(ErrorCodeEnum.API_NOT_FOUND.getCode(), ex.getMessage());
} else {
log.error("System error ", ex);
responseData = ReqResult.error(ErrorCodeEnum.SYS_ERROR.getCode(), "系统开小差啦,请稍后重试");
}
}
return responseData;
}
/**
* BizException按 {@link RequestContext#getLang()} 选择文案;{@code zh-CN}(忽略大小写)用中文模板,否则用英文模板;
* 使用 {@link BizException#getMessageFormatArgs()} 与 {@link BizExceptionCodeEnum} 模板重新 {@link String#format}。
* 无 {@code msgName}、无上下文、无 {@code lang} 或枚举名无法解析时回退 {@link Throwable#getMessage()}。
*/
private static String resolveBizExceptionMessage(BizException ex) {
String fallback = ex.getMessage();
String msgName = ex.getMsgName();
if (msgName == null || msgName.isEmpty()) {
return fallback;
}
RequestContext<?> ctx = RequestContext.get();
if (ctx == null) {
return fallback;
}
String lang = ctx.getLang();
if (lang == null || lang.isBlank()) {
return fallback;
}
try {
BizExceptionCodeEnum codeEnum = BizExceptionCodeEnum.valueOf(msgName);
boolean zhCn = "zh-CN".equalsIgnoreCase(lang.trim());
String tpl = zhCn ? codeEnum.getMessageZh() : codeEnum.getMessageEn();
Object[] args = ex.getMessageFormatArgs();
if (args == null || args.length == 0) {
return tpl;
}
return String.format(tpl, args);
} catch (IllegalArgumentException e) {
return fallback;
}
}
}

View File

@@ -0,0 +1,361 @@
package com.xspaceagi.interceptor;
import com.xspaceagi.custompage.domain.model.CustomPageConfigModel;
import com.xspaceagi.custompage.infra.service.ProxyConfigService;
import com.xspaceagi.sandbox.SandboxRequestAttributes;
import com.xspaceagi.system.application.dto.I18nLangDto;
import com.xspaceagi.system.application.dto.TenantConfigDto;
import com.xspaceagi.system.application.dto.UserDto;
import com.xspaceagi.system.application.service.*;
import com.xspaceagi.system.infra.dao.entity.User;
import com.xspaceagi.system.infra.dao.entity.UserReq;
import com.xspaceagi.system.spec.auth.UserAuthProperties;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
import com.xspaceagi.system.spec.enums.HttpStatusEnum;
import com.xspaceagi.system.spec.enums.I18nSideEnum;
import com.xspaceagi.system.spec.exception.BizException;
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import com.xspaceagi.system.spec.utils.IPUtil;
import com.xspaceagi.system.spec.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import jakarta.annotation.Resource;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Locale;
@Component
public class AuthInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(AuthInterceptor.class);
@Value("${jwt.secretKey}")
private String jwtSecretKey;
@Resource
private UserAuthProperties userAuthProperties;
@Resource
private AuthService authService;
// 页面开发代理,查询域名映射关系
@Resource
private ProxyConfigService proxyConfigService;
@Resource
private TenantConfigApplicationService tenantConfigApplicationService;
@Resource
private UserRequestApplicationService userRequestApplicationService;
@Resource
private I18nApplicationService i18nApplicationService;
@Resource
private I18nLangApplicationService i18nLangApplicationService;
@Value("${server.port:8080}")
private Integer port;
@Value("${license:}")
private String license;
/**
* 请求处理完之后
*/
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object obj, Exception exc) throws Exception {
RequestContext.remove();
}
/**
* 请求处理完成
*/
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object obj, ModelAndView model) throws Exception {
if (RequestContext.get() != null) {
userRequestApplicationService.addUserRequest(
UserReq.builder()
.tenantId(RequestContext.get().getTenantId())
.userId(RequestContext.get().getUserId() == null ? -1L : RequestContext.get().getUserId())
.build()
);
}
}
/**
* 请求处理之前
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
request.setCharacterEncoding("UTF-8");
if (request.getMethod().equals("OPTIONS")) {
return true;
}
//license check
checkLicense();
// 优先使用 Sandbox Filter 记录的“原始 URI”
String originalRequestUri = getOriginalRequestUri(request);
String domainName = request.getHeader("Host");
if (domainName == null) {
return false;
}
String referer = request.getHeader("Referer");
Long tenantId = tenantConfigApplicationService.queryTenantIdByDomainName(domainName.split(":")[0]);
// 获取页面应用绑定域名场景下的租户ID
if (tenantId == null && request.getRequestURI().contains("/api/page/")) {
CustomPageConfigModel configModel = proxyConfigService.queryCustomPageConfigByDomain(domainName.split(":")[0]);
if (configModel != null) {
tenantId = configModel.getTenantId();
//构造referer页面的工作流插件调用基于referer中的页面项目id和关联的智能体ID进行关系确认
referer = "https://" + domainName + "/page/" + configModel.getId() + "-" + configModel.getDevAgentId() + "/prod";
request.setAttribute("referer", referer);//后面使用
}
}
if (tenantId == null) {
// 对于无需鉴权的路径(如飞书/钉钉 webhook使用默认租户不要求登录
if (isExcludedPath(originalRequestUri)) {
tenantId = 1L;
} else if (domainName.contains("qiming.com")) {
throw BizException.of(ErrorCodeEnum.UNAUTHORIZED_REDIRECT, BizExceptionCodeEnum.bootstrapAuthRedirectUrl, "https://qiming.com");
}
String headerTenantId = request.getHeader("x-tenant-id");
if (StringUtils.isNotBlank(headerTenantId)) {
try {
tenantId = Long.parseLong(headerTenantId);
} catch (NumberFormatException e) {
// ignore
}
}
if (tenantId == null) {
tenantId = 1L;
}
}
RequestContext<UserDto> requestContext = RequestContext.<UserDto>builder().tenantId(tenantId).build();
RequestContext.set(requestContext);
TenantConfigDto tenantConfig = tenantConfigApplicationService.getTenantConfig(requestContext.getTenantId());
String fragment = request.getHeader("Fragment");
if (StringUtils.isNotBlank(referer) && StringUtils.isNotBlank(fragment)) {
referer = referer + "#" + fragment;
}
if (referer != null && !log.isDebugEnabled() && !referer.contains("https://servicewechat.com")) {
try {
URL url = new URL(referer);
String baseUrl;
if (url.getHost().equals("localhost") || url.getHost().equals("127.0.0.1")) {
baseUrl = url.getProtocol() + "://" + url.getHost() + ":" + port;
} else {
baseUrl = url.getProtocol() + "://" + url.getHost() + (url.getPort() == 443 || url.getPort() == 80 || url.getPort() == -1 ? "" : ":" + url.getPort());
}
tenantConfig.setSiteConfigUrl(tenantConfig.getSiteUrl());
tenantConfig.setSiteUrl(baseUrl);
} catch (MalformedURLException e) {
// ignore
log.error("referer url error: {}", e.getMessage());
}
}
tenantConfig.setTenantId(requestContext.getTenantId());
requestContext.setTenantConfig(tenantConfig);
//获取终端语言
requestContext.setClientLang("en-us");
Locale locale = request.getLocale();
requestContext.setClientLang(locale.toLanguageTag().toLowerCase());
String token = null;
//优先使用cookie中的token
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals("ticket")) {
token = cookie.getValue();
}
if (cookie.getName().equals("lang")) {
requestContext.setClientCookieLang(cookie.getValue());
}
}
}
String authorization = request.getHeader("Authorization");
if (token == null && authorization != null && authorization.length() > 35) {//避免前端传递不符合token的字符串
token = authorization.replaceFirst("Basic", "").replaceFirst("Bearer", "").trim();
}
if (StringUtils.isNotBlank(token)) {
try {
log.debug("jwt token: {}", token);
UserDto userDto = authService.getLoginUserInfo(token);
if (userDto != null && userDto.getTenantId().equals(tenantId)) {
if (userDto.getStatus() == User.Status.Disabled || userDto.getStatus() == User.Status.Deleted) {
authService.expireUserAllToken(userDto.getId());
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.systemAccountDisabled);
}
var userContent = UserDto.convertToUserContext(userDto);
requestContext.setUserContext(userContent);
requestContext.setUser(userDto);
requestContext.setUserId(userDto.getId());
requestContext.setTenantId(userDto.getTenantId());
requestContext.setClientIp(IPUtil.getIpAddr(request));
requestContext.setLogin(true);
requestContext.setToken(token);
requestContext.setLangMap(userDto.getLangMap());
requestContext.setLang(userDto.getLang());
log.debug("JWT token OK, userId: {} ", userDto.getId());
if (originalRequestUri.startsWith("/api/system/")) {
//判断是不是管理员
if (userDto.getRole() != User.Role.Admin) {
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied);
}
}
String header = request.getHeader("X-Client-Type");
if (header != null || token.startsWith("ticket")) {
authService.renewToken(token);
} else {
int expire = (int) (tenantConfig.getAuthExpire() == null ? 86400 : tenantConfig.getAuthExpire() * 60);
Claims claims = JwtUtils.parseJwt(token, jwtSecretKey);
if (claims.getExpiration().getTime() < System.currentTimeMillis() + expire * 1000L * 0.8) {
//refresh token
String newToken = authService.refreshToken(token);
Cookie cookie = new Cookie("ticket", newToken);
cookie.setMaxAge(tenantConfig.getAuthExpire() == null ? 86400 * 30 : tenantConfig.getAuthExpire().intValue() * 60);
cookie.setPath("/");
response.addCookie(cookie);
requestContext.setToken(newToken);
}
}
return true;
}
log.debug("jwt token expired: {}", token);
} catch (Exception e) {
if (e instanceof BizException) {
throw e;
}
log.warn("JWT token error {}", e.getMessage());
}
}
log.debug("jwt token is null, check uri: {}", originalRequestUri);
try {
checkAndThrowAuthException(originalRequestUri);
} catch (Exception e) {
if (StringUtils.isNotBlank(referer)) {
throw BizException.of(ErrorCodeEnum.UNAUTHORIZED_REDIRECT, BizExceptionCodeEnum.bootstrapAuthRedirectUrl,
"/login?redirect=" + URLEncoder.encode(referer, StandardCharsets.UTF_8));
}
throw e;
}
if (requestContext.getLangMap() == null) {
String lang = requestContext.getClientCookieLang();
if (lang == null) {
I18nLangDto aDefault = i18nLangApplicationService.getDefault(tenantId);
if (aDefault != null) {
lang = aDefault.getLang();
} else {
lang = requestContext.getClientLang();
}
}
requestContext.setLang(lang);
requestContext.setLangMap(i18nApplicationService.querySystemLangMap(tenantId, I18nSideEnum.Backend.getSide(), lang));
}
return true;
}
private String getOriginalRequestUri(HttpServletRequest request) {
Object val = request.getAttribute(SandboxRequestAttributes.ORIGINAL_REQUEST_URI);
if (val instanceof String s && !s.isEmpty()) {
return s;
}
return request.getRequestURI();
}
private void checkLicense() {
if (StringUtils.isNotBlank(license)) {
String expireDate;
try {
String hexKey = "a0189de032115b6b7031a8fb5194ed68b3c716d3e09c21592fe4187b4586048b"; // 256位
String hexIV = "0ac97ac7d1081ebae42bfd987c5739eb";
expireDate = AESCrypto.decryptWithHexKey(license, hexKey, hexIV);
} catch (Exception e) {
log.error("license error: {}", e.getMessage());
throw BizException.of(ErrorCodeEnum.LICENSE_CONTENT_INVALID, BizExceptionCodeEnum.bootstrapLicenseContentInvalid);
}
if (parseToTimestamp(expireDate) * 1000L < System.currentTimeMillis()) {
throw BizException.of(ErrorCodeEnum.UNAUTHORIZED_REDIRECT, BizExceptionCodeEnum.bootstrapAuthRedirectUrl, "/license-expired");
}
}
}
public static long parseToTimestamp(String expireDate) {
if (expireDate == null || expireDate.isEmpty()) return -1;
try {
// 标准化分隔符
String normalized = expireDate
.replace('/', '-')
.replace('.', '-');
// 处理 8 位纯数字
if (normalized.matches("\\d{8}")) {
return LocalDate.parse(normalized, DateTimeFormatter.BASIC_ISO_DATE)
.atStartOfDay(ZoneId.systemDefault()).toEpochSecond();
}
// 处理带 T 和时区的 ISO 格式
if (normalized.contains("T")) {
if (normalized.endsWith("Z")) {
return Instant.parse(normalized).getEpochSecond();
}
return OffsetDateTime.parse(normalized, DateTimeFormatter.ISO_OFFSET_DATE_TIME)
.toInstant().getEpochSecond();
}
// 处理日期 + 时间(空格分隔)
if (normalized.contains(" ")) {
return LocalDateTime.parse(normalized,
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
.atZone(ZoneId.systemDefault()).toEpochSecond();
}
// 仅日期
return LocalDate.parse(normalized, DateTimeFormatter.ISO_LOCAL_DATE)
.atStartOfDay(ZoneId.systemDefault()).toEpochSecond();
} catch (Exception e) {
return -1;
}
}
private boolean isExcludedPath(String uri) {
List<String> excludePath = userAuthProperties.getExcludePath();
for (String ignoreLoginUri : excludePath) {
if (uri.startsWith(ignoreLoginUri)) {
return true;
}
}
return false;
}
private void checkAndThrowAuthException(String uri) {
if (isExcludedPath(uri)) {
return;
}
throw BizException.of(HttpStatusEnum.UNAUTHORIZED, ErrorCodeEnum.UNAUTHORIZED,
BizExceptionCodeEnum.systemUnauthorizedOrSessionExpired);
}
}

View File

@@ -0,0 +1,208 @@
package com.xspaceagi.interceptor;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONWriter;
import com.xspaceagi.log.sdk.service.ILogRpcService;
import com.xspaceagi.log.sdk.vo.LogDocument;
import com.xspaceagi.system.application.dto.UserDto;
import com.xspaceagi.system.infra.dao.entity.OpenApiDefinition;
import com.xspaceagi.system.sdk.service.dto.UserAccessKeyDto;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* 请求参数记录
*
* @apiNote http前端后请求参数记录
*/
@Slf4j
@Component
@ControllerAdvice
public class HttpInterceptor implements HandlerInterceptor, ResponseBodyAdvice, RequestBodyAdvice {
@Value("${access.control.allow-origin}")
private String accessControlAllowOrigin;
@Autowired
private jakarta.servlet.http.HttpServletRequest request;
@Resource
private ILogRpcService iLogRpcService;
@Override
public boolean preHandle(jakarta.servlet.http.HttpServletRequest request,
jakarta.servlet.http.HttpServletResponse response, Object handler) {
String origin = request.getHeader("Origin");
// 如果配置是 "*" 或者 origin 包含配置的值,则允许跨域
boolean allowOrigin = StringUtils.isNotBlank(origin) &&
("*".equals(accessControlAllowOrigin) || origin.contains(accessControlAllowOrigin));
if (allowOrigin) {
response.setHeader("Vary", "Origin");
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Methods", "HEAD,GET,POST,PUT,DELETE,OPTIONS");
response.setHeader("Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept, Authorization, Cache-Control, Fragment");
response.setHeader("Access-Control-Allow-Credentials", "true");
}
if (request.getMethod().equals("OPTIONS")) {
return true;
}
String uri = request.getRequestURI();
if (!(handler instanceof HandlerMethod)) {
return true;
}
long reqBeginTime = Instant.now().toEpochMilli();
// 请求开始时间
request.setAttribute("reqBeginTime", reqBeginTime);
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
request.setAttribute("methodName", method.getDeclaringClass().getName() + "." + method.getName());
log.info("Request URI {}, X-Client-Type {}", uri, request.getHeader("X-Client-Type"));
return true;
}
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (!(body instanceof ReqResult httpResult)) {
return body;
}
ServletServerHttpRequest servletServerHttpRequest = (ServletServerHttpRequest) request;
jakarta.servlet.http.HttpServletRequest httpServletRequest = servletServerHttpRequest.getServletRequest();
Map<String, String[]> params = httpServletRequest.getParameterMap();
Map<String, Object> req = new HashMap<>();
req.put("parameter", params);
Object reqBody = this.request.getAttribute("requestBody");
if (null != reqBody) {
req.put("body", reqBody);
}
RequestContext<Object> objectRequestContext = RequestContext.get();
if (null != objectRequestContext && objectRequestContext.getUser() != null && objectRequestContext.getUser() instanceof UserDto userDto) {
Map<String, Object> user = new HashMap<>();
user.put("tenantId", userDto.getTenantId());
user.put("userId", userDto.getId());
user.put("userName", userDto.getUserName() == null ? userDto.getNickName() : userDto.getUserName());
req.put("user", user);
}
long reqBeginTime = (long) httpServletRequest.getAttribute("reqBeginTime");
String methodName = (String) httpServletRequest.getAttribute("methodName");
OpenApiDefinition openApiDefinition = (OpenApiDefinition) httpServletRequest.getAttribute("currentAPI");
UserAccessKeyDto userAccessKey = (UserAccessKeyDto) httpServletRequest.getAttribute("userAccessKey");
try {
if (log.isInfoEnabled() || openApiDefinition != null) {
// 启用 LargeObject 特性,支持序列化包含大文件内容的请求和响应体
String requestBodyStr = JSON.toJSONString(req, JSONWriter.Feature.LargeObject);
String responseBody = JSON.toJSONString(body, JSONWriter.Feature.LargeObject);
if (openApiDefinition != null) {
requestBodyStr = httpServletRequest.getMethod() + " " + httpServletRequest.getRequestURI() + "\n" + requestBodyStr;
}
buildAndSendLogDocument(openApiDefinition, userAccessKey, requestBodyStr, responseBody, reqBeginTime, httpResult);
log.info("HTTP API call log\nService {}\nRequest {}\nResponse {}\nElapsed {}ms", methodName, requestBodyStr.length() > 1024 * 8 ? requestBodyStr.substring(0, 1024 * 8) : requestBodyStr, responseBody.length() > 1024 * 8 ? responseBody.substring(0, 1024 * 8) : responseBody
, System.currentTimeMillis() - reqBeginTime);
}
httpResult.setTid(MDC.get("tid"));
return httpResult;
} catch (Exception e) {
log.error("Failed to record HTTP access log", e);
// 使用 LargeObject 特性序列化请求参数,避免大文件内容导致序列化失败
String paramsStr = JSON.toJSONString(params, JSONWriter.Feature.LargeObject);
log.error("HTTP API call log\nService {}\nRequest {}\nError {}\nElapsed {}ms", methodName, paramsStr.length() > 1024 * 8 ? paramsStr.substring(0, 1024 * 8) : paramsStr,
e.getMessage(), System.currentTimeMillis() - reqBeginTime);
throw e;
}
}
private void buildAndSendLogDocument(OpenApiDefinition openApiDefinition, UserAccessKeyDto userAccessKey, String requestBodyStr, String responseBody, Long reqBeginTime, ReqResult httpResult) {
if (openApiDefinition == null) {
return;
}
UserDto user = (UserDto) RequestContext.get().getUser();
LogDocument logDocument = LogDocument.builder()
.id(UUID.randomUUID().toString().replace("-", ""))
.tenantId(RequestContext.get().getTenantId())
.spaceId(-1L)
.userId(RequestContext.get().getUserId())
.userName(user.getUserName())
.targetType("ApiKey")
.targetName(userAccessKey.getName())
.targetId(userAccessKey.getId().toString())
.input(requestBodyStr)
.output(responseBody)
.requestStartTime(reqBeginTime)
.requestEndTime(System.currentTimeMillis())
.resultCode(httpResult.getCode())
.resultMsg(httpResult.getMessage())
.conversationId(openApiDefinition.getPath())
.requestId(MDC.get("tid"))
.createTime(System.currentTimeMillis())
.from("ApiKey")
.build();
iLogRpcService.bulkIndex(List.of(logDocument));
}
@Override
public boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
return inputMessage;
}
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
request.setAttribute("requestBody", body);
return body;
}
@Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}

View File

@@ -0,0 +1,44 @@
package com.xspaceagi.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import java.lang.reflect.Method;
import java.time.Instant;
import java.util.concurrent.ThreadLocalRandom;
/**
* tid ,获取拦截器
*
* @author soddy
*/
@Slf4j
@Component
public class TIdInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
Integer reqId = ThreadLocalRandom.current().nextInt(100, 999999);
MDC.put("tid", reqId + "" + Instant.now().toEpochMilli());
// 请求开始时间
request.setAttribute("reqBeginTime", Instant.now().toEpochMilli());
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
request.setAttribute("methodName", method.getDeclaringClass().getName() + "." + method.getName());
} else {
request.setAttribute("methodName", "静态资源未识别的方法");
}
return true;
}
}

View File

@@ -0,0 +1,24 @@
package com.xspaceagi.sandbox;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* Sandbox API URL 重写配置(全局生效)
*/
@Component
@ConfigurationProperties(prefix = "sandbox.api.rewrite")
@Getter
@Setter
public class SandboxApiRewriteProperties {
/**
* 允许重写的路径模式(相对于 /api/v1/4sandbox 之后的部分匹配)。
*/
private List<String> allowPath = new ArrayList<>();
}

View File

@@ -0,0 +1,233 @@
spring:
mvc:
pathmatch:
matching-strategy: ANT_PATH_MATCHER
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
# 使用 dynamic-datasource-spring-boot-starter 配置
datasource:
# 动态SQL生成器配置
sql-generator:
# 指定使用的数据库类型mysql 或 doris
type: mysql
dynamic:
# 设置默认的数据源或者数据源组, 默认值即为 master
primary: master
# 设置严格模式,默认 false. 严格模式下未匹配到数据源直接报错, 非严格模式下则使用默认数据源 primary 配置的数据源
strict: false
# p6spy 开关, 默认 false
p6spy: false
# seata 开关, 默认 false
seata: false
# 数据源配置信息
datasource:
# 主数据源 (MySQL)
master:
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:your_mysql_password}
# 以下为 Druid 连接池配置 (如果使用)
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
filters: stat # 启用 stat filter 用于监控
# Doris 数据源
doris:
url: jdbc:mysql://${DORIS_HOST:localhost}:3306/${DORIS_DB_NAME:agent_custom_table}?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:your_doris_password}
# Doris 表配置
table:
replication-num: 1 # 副本数生产环境通常为3开发环境可以为1
bucket-num: 10 # 分桶数
duplicate-key: id # 重复键字段名,默认使用第一个字段
distributed-key: id # 分布键字段名,默认使用第一个字段
# 可选: 如果 Doris 也用 Druid可以添加 druid 配置
druid:
initial-size: 2
min-idle: 2
max-active: 10
max-wait: 60000
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:your_redis_password}
database: ${REDIS_DB:1}
lettuce:
pool:
max-idle: 16
max-active: 32
min-idle: 8
main:
allow-circular-references: true
log:
level: DEBUG
server:
port: 8081
jwt:
secretKey: ${JWT_SECRET_KEY:your_jwt_secret_key_minimum_32_characters}
file:
uploadFolder: ${FILE_UPLOAD_FOLDER:/tmp/uploads}
baseUrl: ${FILE_BASE_URL:https://your-file-domain.com/files/}
# Storage configuration
storage:
type: ${STORAGE_TYPE:local} # Storage type: local, cos(Tencent Cloud), oss(Alibaba Cloud), s3(S3 protocol/MinIO)
local:
upload:
folder: ${LOCAL_UPLOAD_FOLDER:/tmp/uploads}
# Tencent Cloud COS configuration (takes effect when storage.type=cos)
cos:
secret-id: ${COS_SECRET_ID:}
secret-key: ${COS_SECRET_KEY:}
region: ${COS_REGION:ap-chengdu}
bucket-name: ${COS_BUCKET_NAME:}
endpoint: ${COS_ENDPOINT:}
# Legacy configuration below
secretId: ${COS_SECRET_ID:}
secretKey: ${COS_SECRET_KEY:}
baseUrl: ${COS_BASE_URL:}
# Alibaba Cloud OSS configuration (takes effect when storage.type=oss)
oss:
endpoint: ${OSS_ENDPOINT:}
access-key-id: ${OSS_ACCESS_KEY_ID:}
access-key-secret: ${OSS_ACCESS_KEY_SECRET:}
region-name: ${OSS_REGION_NAME:cn-chengdu}
bucket-name: ${OSS_BUCKET_NAME:}
# S3 protocol configuration (takes effect when storage.type=s3, supports MinIO etc.)
s3:
endpoint: ${S3_ENDPOINT:}
access-key: ${S3_ACCESS_KEY:}
secret-key: ${S3_SECRET_KEY:}
bucket-name: ${S3_BUCKET_NAME:}
region: ${S3_REGION:us-east-1}
vector-store:
engine: milvus
milvus:
uri: ${MILVUS_URI:http://localhost:19530}
user: ${MILVUS_USER:root}
password: ${MILVUS_PASSWORD:your_milvus_password}
code:
execute:
url: ${CODE_EXECUTE_URL:http://localhost:8020/api/run_code_with_log}
# 日志模块配置
log-module:
log:
rust-service:
# Rust 日志服务的基础 URL
base-url: ${LOG_SERVICE_URL:http://localhost:8097}
# 连接超时时间(秒)
connect-timeout-seconds: 10
# 写入超时时间(秒)
write-timeout-seconds: 30
# 读取超时时间(秒)
read-timeout-seconds: 30
# 是否启用连接池
enable-connection-pool: true
# 最大空闲连接数
max-idle-connections: 5
# 连接保持存活时间(分钟)
keep-alive-duration-minutes: 5
search:
elasticsearch:
url: ${ES_URL:http://localhost:9200}
username: ${ES_USERNAME:elastic}
password: ${ES_PASSWORD:your_elasticsearch_password}
api_key: ${ES_API_KEY:}
# 生态市场模块配置
eco-market:
# 服务端配置
server:
# 远程server端的 ip端口配置
base-url: ${ECO_MARKET_SERVER_URL:https://test-agent-market-api.xspaceagi.com}
client:
# 客户端配置
retry:
#自动注册客户端到 server端, 重试间隔时间,单位:秒;
interval: 300
mcp:
proxy-base-url: ${MCP_PROXY_URL:http://localhost:8020}
knife4j:
# 开启增强配置
enable: true
# 标识是否生产环境true-生产环境关闭文档false-显示文档
production: false
custom-page:
http-proxy:
host: ${CUSTOM_PAGE_PROXY_HOST:0.0.0.0}
port: ${CUSTOM_PAGE_PROXY_PORT:18082}
dev-server-host: ${DEV_SERVER_HOST:http://localhost}
prod-server-host: ${PROD_SERVER_HOST:http://localhost:8099}
build-server:
base-url: ${BUILD_SERVER_URL:http://localhost:60000/api}
ai-agent:
base-url: ${AI_AGENT_URL:http://localhost:8086}
docker-proxy:
enable: ${DOCKER_PROXY_ENABLE:true}
base-url: ${DOCKER_PROXY_URL:http://localhost:8088}
computer:
proxy:
host: ${COMPUTER_PROXY_HOST:0.0.0.0}
port: ${COMPUTER_PROXY_PORT:18085}
supportCustomDomain: true
# 内外穿透相关服务配置
reverse:
server:
# 内部服务器配置
inner:
# 内部服务可访问地址,如果分布式部署,可以在每台主机上通过环境变量制定 SERVICE_HOST
service-host: ${SERVICE_HOST:127.0.0.1}
# 绑定主机地址,如果分布式部署,可以在每台主机上通过环境变量制定 BIND_HOST
bind-host: ${BIND_HOST:0.0.0.0}
# 内部服务器端口范围
ports: ${REVERSE_PORTS:30000-40000}
# 外部服务器配置
outer:
# 外部用于与客户端保持连接的服务器主机地址,多个用英文逗号隔开,例如 192.168.1.12,192.168.1.13,留空时使用平台服务器地址
host: ${OUTER_HOST:}
# 外部服务器端口
port: ${OUTER_PORT:10076}
model-api-proxy:
base-api-url: ${MODEL_API_BASE_URL:}
enable-model-proxy: ${MODEL_PROXY_ENABLE:true}
port: ${MODEL_PROXY_PORT:18086}
#记录日志
save-log: ${MODEL_PROXY_SAVE_LOG:true}

View File

@@ -0,0 +1,232 @@
spring:
mvc:
pathmatch:
matching-strategy: ANT_PATH_MATCHER
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
# 使用 dynamic-datasource-spring-boot-starter 配置
datasource:
# 动态SQL生成器配置
sql-generator:
# 指定使用的数据库类型mysql 或 doris
type: mysql
dynamic:
# 设置默认的数据源或者数据源组, 默认值即为 master
primary: master
# 设置严格模式,默认 false. 严格模式下未匹配到数据源直接报错, 非严格模式下则使用默认数据源 primary 配置的数据源
strict: false
# p6spy 开关, 默认 false
p6spy: false
# seata 开关, 默认 false
seata: false
# 数据源配置信息
datasource:
# 主数据源 (MySQL)
master:
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:your_mysql_password}
# 以下为 Druid 连接池配置 (如果使用)
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
filters: stat # 启用 stat filter 用于监控
# Doris 数据源
doris:
url: jdbc:mysql://${DORIS_HOST:localhost}:3306/${DORIS_DB_NAME:agent_custom_table}?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:your_doris_password}
# Doris 表配置
table:
replication-num: 1 # 副本数生产环境通常为3开发环境可以为1
bucket-num: 10 # 分桶数
duplicate-key: id # 重复键字段名,默认使用第一个字段
distributed-key: id # 分布键字段名,默认使用第一个字段
# 可选: 如果 Doris 也用 Druid可以添加 druid 配置
druid:
initial-size: 2
min-idle: 2
max-active: 10
max-wait: 60000
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:your_redis_password}
database: ${REDIS_DB:1}
lettuce:
pool:
max-idle: 16
max-active: 32
min-idle: 8
main:
allow-circular-references: true
log:
level: DEBUG
server:
port: 8081
jwt:
secretKey: ${JWT_SECRET_KEY:your_jwt_secret_key_minimum_32_characters}
file:
uploadFolder: ${FILE_UPLOAD_FOLDER:/tmp/uploads}
baseUrl: ${FILE_BASE_URL:https://your-file-domain.com/files/}
# Storage configuration
storage:
type: ${STORAGE_TYPE:local} # Storage type: local, cos(Tencent Cloud), oss(Alibaba Cloud), s3(S3 protocol/MinIO)
local:
upload:
folder: ${LOCAL_UPLOAD_FOLDER:/tmp/uploads}
# Tencent Cloud COS configuration (takes effect when storage.type=cos)
cos:
secret-id: ${COS_SECRET_ID:}
secret-key: ${COS_SECRET_KEY:}
region: ${COS_REGION:ap-chengdu}
bucket-name: ${COS_BUCKET_NAME:}
endpoint: ${COS_ENDPOINT:}
# Legacy configuration below
secretId: ${COS_SECRET_ID:}
secretKey: ${COS_SECRET_KEY:}
baseUrl: ${COS_BASE_URL:}
# Alibaba Cloud OSS configuration (takes effect when storage.type=oss)
oss:
endpoint: ${OSS_ENDPOINT:}
access-key-id: ${OSS_ACCESS_KEY_ID:}
access-key-secret: ${OSS_ACCESS_KEY_SECRET:}
region-name: ${OSS_REGION_NAME:cn-chengdu}
bucket-name: ${OSS_BUCKET_NAME:}
# S3 protocol configuration (takes effect when storage.type=s3, supports MinIO etc.)
s3:
endpoint: ${S3_ENDPOINT:}
access-key: ${S3_ACCESS_KEY:}
secret-key: ${S3_SECRET_KEY:}
bucket-name: ${S3_BUCKET_NAME:}
region: ${S3_REGION:us-east-1}
vector-store:
engine: milvus
milvus:
uri: ${MILVUS_URI:http://localhost:19530}
user: ${MILVUS_USER:root}
password: ${MILVUS_PASSWORD:your_milvus_password}
code:
execute:
url: ${CODE_EXECUTE_URL:http://localhost:8020/api/run_code_with_log}
# 日志模块配置
log-module:
log:
rust-service:
# Rust 日志服务的基础 URL
base-url: ${LOG_SERVICE_URL:http://localhost:8097}
# 连接超时时间(秒)
connect-timeout-seconds: 10
# 写入超时时间(秒)
write-timeout-seconds: 30
# 读取超时时间(秒)
read-timeout-seconds: 30
# 是否启用连接池
enable-connection-pool: true
# 最大空闲连接数
max-idle-connections: 5
# 连接保持存活时间(分钟)
keep-alive-duration-minutes: 5
search:
elasticsearch:
url: ${ES_URL:http://localhost:9200}
username: ${ES_USERNAME:elastic}
password: ${ES_PASSWORD:your_elasticsearch_password}
api_key: ${ES_API_KEY:}
# 生态市场模块配置
eco-market:
# 服务端配置
server:
# 远程server端的 ip端口配置
base-url: ${ECO_MARKET_SERVER_URL:https://agent-market-api.xspaceagi.com}
client:
# 客户端配置
retry:
#自动注册客户端到 server端, 重试间隔时间,单位:秒;
interval: 300
mcp:
proxy-base-url: ${MCP_PROXY_URL:http://localhost:8020}
knife4j:
# 开启增强配置
enable: true
# 标识是否生产环境true-生产环境关闭文档false-显示文档
production: false
custom-page:
http-proxy:
host: ${CUSTOM_PAGE_PROXY_HOST:0.0.0.0}
port: ${CUSTOM_PAGE_PROXY_PORT:18082}
dev-server-host: ${DEV_SERVER_HOST:http://localhost}
prod-server-host: ${PROD_SERVER_HOST:http://localhost:8099}
build-server:
base-url: ${BUILD_SERVER_URL:http://localhost:60000/api}
ai-agent:
base-url: ${AI_AGENT_URL:http://localhost:8086}
docker-proxy:
enable: ${DOCKER_PROXY_ENABLE:true}
base-url: ${DOCKER_PROXY_URL:http://localhost:8088}
computer:
proxy:
host: ${COMPUTER_PROXY_HOST:0.0.0.0}
port: ${COMPUTER_PROXY_PORT:18085}
supportCustomDomain: true
# 内外穿透相关服务配置
reverse:
server:
# 内部服务器配置
inner:
# 内部服务可访问地址,如果分布式部署,可以在每台主机上通过环境变量制定 SERVICE_HOST
service-host: ${SERVICE_HOST:127.0.0.1}
# 绑定主机地址,如果分布式部署,可以在每台主机上通过环境变量制定 BIND_HOST
bind-host: ${BIND_HOST:0.0.0.0}
# 内部服务器端口范围
ports: ${REVERSE_PORTS:30000-40000}
# 外部服务器配置
outer:
# 外部用于与客户端保持连接的服务器主机地址,多个用英文逗号隔开,例如 192.168.1.12,192.168.1.13,留空时使用平台服务器地址
host: ${OUTER_HOST:}
# 外部服务器端口
port: ${OUTER_PORT:10076}
model-api-proxy:
base-api-url: ${MODEL_API_BASE_URL:}
enable-model-proxy: ${MODEL_PROXY_ENABLE:true}
port: ${MODEL_PROXY_PORT:18086}
#记录日志
save-log: ${MODEL_PROXY_SAVE_LOG:true}

View File

@@ -0,0 +1,232 @@
spring:
mvc:
pathmatch:
matching-strategy: ANT_PATH_MATCHER
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
# 使用 dynamic-datasource-spring-boot-starter 配置
datasource:
# 动态SQL生成器配置
sql-generator:
# 指定使用的数据库类型mysql 或 doris
type: mysql
dynamic:
# 设置默认的数据源或者数据源组, 默认值即为 master
primary: master
# 设置严格模式,默认 false. 严格模式下未匹配到数据源直接报错, 非严格模式下则使用默认数据源 primary 配置的数据源
strict: false
# p6spy 开关, 默认 false
p6spy: false
# seata 开关, 默认 false
seata: false
# 数据源配置信息
datasource:
# 主数据源 (MySQL)
master:
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:your_mysql_password}
# 以下为 Druid 连接池配置 (如果使用)
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
filters: stat # 启用 stat filter 用于监控
# Doris 数据源
doris:
url: jdbc:mysql://${DORIS_HOST:localhost}:3306/${DORIS_DB_NAME:agent_custom_table}?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:your_doris_password}
# Doris 表配置
table:
replication-num: 1 # 副本数生产环境通常为3开发环境可以为1
bucket-num: 10 # 分桶数
duplicate-key: id # 重复键字段名,默认使用第一个字段
distributed-key: id # 分布键字段名,默认使用第一个字段
# 可选: 如果 Doris 也用 Druid可以添加 druid 配置
druid:
initial-size: 2
min-idle: 2
max-active: 10
max-wait: 60000
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:your_redis_password}
database: ${REDIS_DB:1}
lettuce:
pool:
max-idle: 16
max-active: 32
min-idle: 8
main:
allow-circular-references: true
log:
level: DEBUG
server:
port: 8081
jwt:
secretKey: ${JWT_SECRET_KEY:your_jwt_secret_key_minimum_32_characters}
file:
uploadFolder: ${FILE_UPLOAD_FOLDER:/tmp/uploads}
baseUrl: ${FILE_BASE_URL:https://your-file-domain.com/files/}
# Storage configuration
storage:
type: ${STORAGE_TYPE:local} # Storage type: local, cos(Tencent Cloud), oss(Alibaba Cloud), s3(S3 protocol/MinIO)
local:
upload:
folder: ${LOCAL_UPLOAD_FOLDER:/tmp/uploads}
# Tencent Cloud COS configuration (takes effect when storage.type=cos)
cos:
secret-id: ${COS_SECRET_ID:}
secret-key: ${COS_SECRET_KEY:}
region: ${COS_REGION:ap-chengdu}
bucket-name: ${COS_BUCKET_NAME:}
endpoint: ${COS_ENDPOINT:}
# Legacy configuration below
secretId: ${COS_SECRET_ID:}
secretKey: ${COS_SECRET_KEY:}
baseUrl: ${COS_BASE_URL:}
# Alibaba Cloud OSS configuration (takes effect when storage.type=oss)
oss:
endpoint: ${OSS_ENDPOINT:}
access-key-id: ${OSS_ACCESS_KEY_ID:}
access-key-secret: ${OSS_ACCESS_KEY_SECRET:}
region-name: ${OSS_REGION_NAME:cn-chengdu}
bucket-name: ${OSS_BUCKET_NAME:}
# S3 protocol configuration (takes effect when storage.type=s3, supports MinIO etc.)
s3:
endpoint: ${S3_ENDPOINT:}
access-key: ${S3_ACCESS_KEY:}
secret-key: ${S3_SECRET_KEY:}
bucket-name: ${S3_BUCKET_NAME:}
region: ${S3_REGION:us-east-1}
vector-store:
engine: milvus
milvus:
uri: ${MILVUS_URI:http://localhost:19530}
user: ${MILVUS_USER:root}
password: ${MILVUS_PASSWORD:your_milvus_password}
code:
execute:
url: ${CODE_EXECUTE_URL:http://localhost:8020/api/run_code_with_log}
# 日志模块配置
log-module:
log:
rust-service:
# Rust 日志服务的基础 URL
base-url: ${LOG_SERVICE_URL:http://localhost:8097}
# 连接超时时间(秒)
connect-timeout-seconds: 10
# 写入超时时间(秒)
write-timeout-seconds: 30
# 读取超时时间(秒)
read-timeout-seconds: 30
# 是否启用连接池
enable-connection-pool: true
# 最大空闲连接数
max-idle-connections: 5
# 连接保持存活时间(分钟)
keep-alive-duration-minutes: 5
search:
elasticsearch:
url: ${ES_URL:http://localhost:9200}
username: ${ES_USERNAME:elastic}
password: ${ES_PASSWORD:your_elasticsearch_password}
api_key: ${ES_API_KEY:}
# 生态市场模块配置
eco-market:
# 服务端配置
server:
# 远程server端的 ip端口配置
base-url: ${ECO_MARKET_SERVER_URL:https://test-agent-market-api.xspaceagi.com}
client:
# 客户端配置
retry:
#自动注册客户端到 server端, 重试间隔时间,单位:秒;
interval: 300
mcp:
proxy-base-url: ${MCP_PROXY_URL:http://localhost:8020}
knife4j:
# 开启增强配置
enable: true
# 标识是否生产环境true-生产环境关闭文档false-显示文档
production: false
custom-page:
http-proxy:
host: ${CUSTOM_PAGE_PROXY_HOST:0.0.0.0}
port: ${CUSTOM_PAGE_PROXY_PORT:18082}
dev-server-host: ${DEV_SERVER_HOST:http://localhost}
prod-server-host: ${PROD_SERVER_HOST:http://localhost:8099}
build-server:
base-url: ${BUILD_SERVER_URL:http://localhost:60000/api}
ai-agent:
base-url: ${AI_AGENT_URL:http://localhost:8086}
docker-proxy:
enable: ${DOCKER_PROXY_ENABLE:true}
base-url: ${DOCKER_PROXY_URL:http://localhost:8088}
computer:
proxy:
host: ${COMPUTER_PROXY_HOST:0.0.0.0}
port: ${COMPUTER_PROXY_PORT:18085}
supportCustomDomain: true
# 内外穿透相关服务配置
reverse:
server:
# 内部服务器配置
inner:
# 内部服务可访问地址,如果分布式部署,可以在每台主机上通过环境变量制定 SERVICE_HOST
service-host: ${SERVICE_HOST:127.0.0.1}
# 绑定主机地址,如果分布式部署,可以在每台主机上通过环境变量制定 BIND_HOST
bind-host: ${BIND_HOST:0.0.0.0}
# 内部服务器端口范围
ports: ${REVERSE_PORTS:30000-40000}
# 外部服务器配置
outer:
# 外部用于与客户端保持连接的服务器主机地址,多个用英文逗号隔开,例如 192.168.1.12,192.168.1.13,留空时使用平台服务器地址
host: ${OUTER_HOST:}
# 外部服务器端口
port: ${OUTER_PORT:10076}
model-api-proxy:
base-api-url: ${MODEL_API_BASE_URL:}
enable-model-proxy: ${MODEL_PROXY_ENABLE:true}
port: ${MODEL_PROXY_PORT:18086}
#记录日志
save-log: ${MODEL_PROXY_SAVE_LOG:true}

View File

@@ -0,0 +1,134 @@
spring:
application:
name: app-platform
profiles:
active: @activatedProperties@
# 默认为false。如果开启的话在事务真正物理提交doCommit失败后会进行回滚。
transaction:
rollback-on-commit-failure: true
# [可选]配置,自行判断是否使用Spring Cache注解(@Cacheable 等)选项,如果不启用,在启动类上注释: @EnableCaching
cache:
type: CAFFEINE
#应用程序启动创建缓存的名称,必须将所有注释为@Cacheable缓存name或value罗列在这里
cache-names:
- queryOrgTree
- getUserInfo
- tableTotalCache
caffeine:
spec: "maximumSize=2000,expireAfterAccess=30s"
# 排除DruidDataSourceAutoConfigure,使用dynamic-datasource-spring-boot3-starter
# autoconfigure:
# exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
server:
compression:
enabled: true
min-response-size: 2KB
mime-types: application/json,application/xml,text/html,text/xml,text/plain,text/css,text/javascript,application/javascript
tomcat:
# 设置POST请求大小限制0表示不限制
max-http-form-post-size: 0
# 设置URI编码为UTF-8支持中文等非ASCII字符
uri-encoding: UTF-8
logging:
config: classpath:logback-custom.xml
#允许跨域的根域名
access:
control:
allow-origin: "*"
#无需检测登录的接口,前缀匹配
auth:
excludePath:
- /api/user/login
- /health
- /ready
- /favicon.ico
- /doc.html
- /webjars/**
- /v3/**
- /api/user/login
- /api/user/passwordLogin
- /api/user/codeLogin
- /api/user/mp/codeLogin
- /api/user/code/send
- /api/user/email/check
- /api/user/password/reset
- /api/tenant/config
- /api/system/eco/market/server
- /api/temp/chat/conversation
- /api/temp/chat/completions
- /api/logo/
- /api/file
- /api/f/
- /api/mcp/sse
- /api/mcp/message
- /api/v1/
- /api/ticket/redirect
- /api/audio
- /api/page/workflow
- /api/page/plugin
- /api/published/category
- /api/published/agent/list
- /api/computer/static
- /api/agent/conversation/share/detail
- /api/sys/v
- /api/sandbox/config/reg
- /api/sandbox/config/health/
- /api/sandbox/config/redirect/
# IM三方回调免登录
- /api/im/feishu
- /api/im/dingtalk
- /api/im/wework
- /api/i18n/
# DataSource Config
mybatis-plus:
mapper-locations: classpath*:/mapper/*.xml
tenant:
ignoreTenantTables:
- tenant
- pf_retry_data
- system_config
- card
- knowledge_task
- knowledge_task_history
- schedule_task
- eco_market_server_secret
- eco_market_server_config
- eco_market_server_publish_config
- eco_market_client_secret
# 扫描此包下pojo类生成短别名,在xml中可以使用类名,不用输全路径类名
type-aliases-package: com.xspaceagi.**.infra.dao.entity
# 此包下的类会由SqlSessionFactoryBean注册成TypeHandler
type-handlers-package:
global-config:
# 数据库配置
db-config:
# 默认值是ID_WORKER(框架生成的分布式唯一ID,Long型),其他选项AUTO(数据库自增),INPUT(自定义),UUID(框架生成uuid),ID_WORKER_STR(框架生成的分布式唯一ID,String型)
id-type: auto
# 字段策略涉及insert,update及wrapper内部的entity属性生成的where条件;有4个选项IGNORED(不判断),NOT_NULL(非null即可),NOT_EMPTY(非null且非空串),DEFAULT(非null即可)
# 比如User对象name是""空串,调用insert接口,IGNORED会插入空串,NOT_NULL会插入空串,NOT_EMPTY不会插入name字段
where-strategy: NOT_EMPTY
# qiming-cli、saas、manual
installation-source: saas
app:
version: 1.0.8.3
stt:
api:
base-url: https://stt-api.qiming.com
sandbox:
api:
rewrite:
allow-path:
- "skill/**"
- "publish/apply"
- "im/wechat/push-message"

View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
<springProperty name="LOG_LEVEL" scope="context" source="log.level"/>
<springProperty name="DEV_MODE" scope="context" source="spring.profiles.active"/>
<!-- 通过 logging.file.path 配置日志输出路径,默认输出到相对路径 logs/ -->
<springProperty name="logPath" scope="context" source="logging.file.path" defaultValue="logs"/>
<property name="outPattern"
value="[%-5p] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] [%c] [%X{tid}] [%X{prefix}] - %m%n"/>
<property name="maxHistory" value="7"/>
<property name="charSet" value="UTF-8"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${outPattern}</pattern>
</encoder>
</appender>
<appender name="APP_LOG_FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${logPath}/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${logPath}/app.log.%d{yyyy-MM-dd}
</FileNamePattern>
<maxHistory>${maxHistory}</maxHistory>
</rollingPolicy>
<encoder>
<Pattern>${outPattern}</Pattern>
<charset>${charSet}</charset>
</encoder>
</appender>
<!-- 如果是可以容忍重启时丢失的应用日志,可以使用异步日志输出,大幅提升日志性能 -->
<appender name="ASYNC_APP_LOG_FILE" class="ch.qos.logback.classic.AsyncAppender">
<!-- 建议不丢失日志设置为0 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列的长度该值会影响性能默认值为256 -->
<queueSize>512</queueSize>
<appender-ref ref="APP_LOG_FILE"/>
</appender>
<logger name="org.springframework" level="ERROR"/>
<logger name="org.apache.zookeeper" level="ERROR"/>
<logger name="com.baomidou" level="ERROR"/>
<logger name="org.mybatis.spring" level="ERROR"/>
<logger name="com.xspaceagi.system.infra.dao.mapper" level="ERROR"/>
<logger name="com.xspaceagi.agent.core.infra.dao.mapper" level="ERROR"/>
<logger name="io.lettuce.core" level="ERROR"/>
<logger name="org.redisson" level="ERROR"/>
<logger name="com.alibaba" level="ERROR"/>
<logger name="com.xspaceagi.system.domain.service.impl.ScheduleTaskDomainServiceImpl" level="ERROR"/>
<!-- 缺省应用日志输出 -->
<root level="${LOG_LEVEL}">
<if condition='"${DEV_MODE}".equals("dev")'>
<then>
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_APP_LOG_FILE"/>
</then>
</if>
<if condition='"${DEV_MODE}".equals("prod")'>
<then>
<appender-ref ref="ASYNC_APP_LOG_FILE"/>
</then>
</if>
<if condition='"${DEV_MODE}".equals("test")'>
<then>
<appender-ref ref="ASYNC_APP_LOG_FILE"/>
</then>
</if>
</root>
</configuration>

View File

@@ -0,0 +1,67 @@
package com.xspaceagi.compose.api.service;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.alibaba.fastjson2.JSON;
import com.xspaceagi.PlatformApiApplication;
import com.xspaceagi.compose.sdk.DorisDataPage;
import com.xspaceagi.compose.sdk.request.DorisTableDefineRequest;
import com.xspaceagi.compose.sdk.request.QueryDorisTableDefinePageRequest;
import com.xspaceagi.compose.sdk.service.IComposeDbTableRpcService;
import com.xspaceagi.compose.sdk.vo.define.TableDefineVo;
import com.xspaceagi.system.spec.common.RequestContext;
import lombok.extern.slf4j.Slf4j;
@SpringBootTest(classes = PlatformApiApplication.class)
@Slf4j
public class ComposeDbTableRpcServiceImplQueryTableDefinitionTest {
@Autowired
private IComposeDbTableRpcService composeDbTableRpcService;
@Test
void testQueryTableDefinition() {
// 设置租户上下文,防止 MyBatis Plus 多租户插件报错
try {
RequestContext.setThreadTenantId(1L);
// RequestContext.get().setUserId(6L);
DorisTableDefineRequest request = new DorisTableDefineRequest();
request.setTableId(28L); // 请根据实际测试表ID设置
TableDefineVo definitionVo = composeDbTableRpcService.queryTableDefinition(request);
log.info("definitionVo:{}", JSON.toJSONString(definitionVo));
assertNotNull(definitionVo);
// 可根据实际业务补充更多断言
} finally {
RequestContext.remove();
}
}
@Test
void testQueryTableDefineBySpaceId() {
try {
RequestContext.setThreadTenantId(1L);
QueryDorisTableDefinePageRequest request = QueryDorisTableDefinePageRequest.builder()
.spaceId(37L) // 请根据实际测试空间ID设置
.pageNo(1)
.pageSize(10)
.build();
DorisDataPage<TableDefineVo> pageResult = composeDbTableRpcService.queryTableDefineBySpaceId(request);
log.info("pageResult:{}", JSON.toJSONString(pageResult));
assertNotNull(pageResult, "分页结果不应为null");
assertNotNull(pageResult.getRecords(), "记录列表不应为null");
// 可根据实际业务补充更多断言,比如校验返回数量、内容等
} finally {
RequestContext.remove();
}
}
}

View File

@@ -0,0 +1,322 @@
package com.xspaceagi.compose.api.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import com.alibaba.fastjson2.JSON;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.xspaceagi.PlatformApiApplication;
import com.xspaceagi.compose.sdk.request.DorisTableDataRequest;
import com.xspaceagi.compose.sdk.request.DorisTableDefineRequest;
import com.xspaceagi.compose.sdk.response.DorisTableDataResponse;
import com.xspaceagi.compose.sdk.vo.define.CreateTableDefineVo;
import com.xspaceagi.system.spec.common.RequestContext;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@SpringBootTest(classes = PlatformApiApplication.class)
public class ComposeDbTableRpcServiceImplTest {
@Autowired
private ComposeDbTableRpcServiceImpl composeDbTableRpcService;
@Test
void testQueryTableData_update() {
// 设置租户上下文,防止 MyBatis Plus 多租户插件报错
try {
RequestContext.setThreadTenantId(1L);
// 按照示例构造请求参数
DorisTableDataRequest request = new DorisTableDataRequest();
request.setTableId(28L);
request.setSql(
"UPDATE custom_table_table_name SET agent_name={{agent_name}} WHERE agent_id={{agent_id}} limit 100");
java.util.Map<String, Object> args = new java.util.HashMap<>();
args.put("agent_id", "agent_001");
args.put("agent_name", "智能体测试");
request.setArgs(args);
java.util.Map<String, Object> extArgs = new java.util.HashMap<>();
extArgs.put("uid", "6");
request.setExtArgs(extArgs);
DorisTableDataResponse response = composeDbTableRpcService.queryTableData(request);
log.info("update response: {}", JSON.toJSONString(response));
assertNotNull(response);
// 可根据实际数据断言返回内容
} finally {
RequestContext.remove();
}
}
@Test
void testQueryTableData_WithNullTableId() {
// 设置租户上下文,防止 MyBatis Plus 多租户插件报错
try {
RequestContext.setThreadTenantId(1L);
DorisTableDataRequest request = new DorisTableDataRequest();
// 设置空的tableId
request.setTableId(null);
// 验证异常抛出
assertThrows(RuntimeException.class, () -> {
composeDbTableRpcService.queryTableData(request);
});
} finally {
RequestContext.remove();
}
}
@Test
void testInsertCustomTable28() {
// 设置租户上下文,防止 MyBatis Plus 多租户插件报错
try {
RequestContext.setThreadTenantId(1L);
DorisTableDataRequest insertRequest = new DorisTableDataRequest();
insertRequest.setTableId(28L);
insertRequest.setSql(
"INSERT INTO custom_table_table_name ( agent_id, agent_name, nick_name, uid, user_name, created) VALUES ( 'agent_002', '测试智能体2', '测试昵称2', 'user_12346', 'user2', NOW())");
// insert 一般不需要 args/extArgs直接写死
DorisTableDataResponse response = composeDbTableRpcService.queryTableData(insertRequest);
log.info("insert response: {}", JSON.toJSONString(response));
assertNotNull(response);
// 可根据 insert 返回内容断言
} finally {
RequestContext.remove();
}
}
@Test
void testSelectCustomTable28() {
// 设置租户上下文,防止 MyBatis Plus 多租户插件报错
try {
RequestContext.setThreadTenantId(1L);
DorisTableDataRequest selectRequest = new DorisTableDataRequest();
selectRequest.setTableId(28L);
selectRequest.setSql("SELECT * FROM custom_table_table_name WHERE uid='user_12346'");
DorisTableDataResponse response = composeDbTableRpcService.queryTableData(selectRequest);
log.info("insert response: {}", JSON.toJSONString(response));
assertNotNull(response);
// 可断言返回数据包含 user_12346
} finally {
RequestContext.remove();
}
}
@Test
void testSelectLikeCustomTable28() {
// 设置租户上下文,防止 MyBatis Plus 多租户插件报错
try {
RequestContext.setThreadTenantId(1L);
DorisTableDataRequest selectRequest = new DorisTableDataRequest();
selectRequest.setTableId(28L);
Map<String, Object> args = new HashMap<>();
args.put("team_name", "user");
selectRequest.setArgs(args);
selectRequest.setSql("SELECT * FROM custom_table WHERE user_name LIKE '%${{team_name}}%'");
DorisTableDataResponse response = composeDbTableRpcService.queryTableData(selectRequest);
log.info("insert response: {}", JSON.toJSONString(response));
assertNotNull(response);
// 可断言返回数据包含 user_12346
} finally {
RequestContext.remove();
}
}
@Test
void testSelectCustomTable28_withLimit() {
// 设置租户上下文,防止 MyBatis Plus 多租户插件报错
try {
RequestContext.setThreadTenantId(1L);
DorisTableDataRequest selectRequest = new DorisTableDataRequest();
selectRequest.setTableId(28L);
selectRequest.setSql("SELECT * FROM custom_table_table_name WHERE uid='user_12346' limit 100");
DorisTableDataResponse response = composeDbTableRpcService.queryTableData(selectRequest);
log.info("insert response: {}", JSON.toJSONString(response));
assertNotNull(response);
// 可断言返回数据包含 user_12346
} finally {
RequestContext.remove();
}
}
@Test
void testDeleteCustomTable28() {
// 设置租户上下文,防止 MyBatis Plus 多租户插件报错
try {
RequestContext.setThreadTenantId(1L);
DorisTableDataRequest deleteRequest = new DorisTableDataRequest();
deleteRequest.setTableId(28L);
deleteRequest.setSql("DELETE FROM custom_table_table_name WHERE id=1002");
DorisTableDataResponse response = composeDbTableRpcService.queryTableData(deleteRequest);
assertNotNull(response);
// 可根据 delete 返回内容断言
} finally {
RequestContext.remove();
}
}
@Test
void testSelectCustomTable28WithJoin() {
// 设置租户上下文,防止 MyBatis Plus 多租户插件报错
try {
RequestContext.setThreadTenantId(1L);
DorisTableDataRequest joinRequest = new DorisTableDataRequest();
joinRequest.setTableId(28L); // 这里可用主表ID
joinRequest.setSql(
"SELECT a.*, b.nick_name AS b_nick_name FROM custom_table_table_name a LEFT JOIN custom_table_table_name b ON a.uid = b.uid WHERE a.uid='user_12346'");
DorisTableDataResponse response = composeDbTableRpcService.queryTableData(joinRequest);
log.info("SELECT response: {}", JSON.toJSONString(response));
assertNotNull(response);
// 可断言返回数据包含 user_12346 及 b_nick_name 字段
} finally {
RequestContext.remove();
}
}
@Test
void testSelectCustomTable28WithJoin_ListParams() {
// 设置租户上下文,防止 MyBatis Plus 多租户插件报错
try {
RequestContext.setThreadTenantId(1L);
DorisTableDataRequest joinRequest = new DorisTableDataRequest();
joinRequest.setTableId(28L); // 这里可用主表ID
var uidList = Arrays.asList("user_12346", "user_12347");
Map<String, Object> args = new HashMap<>();
args.put("uidList", uidList);
joinRequest.setArgs(args);
// 使用{{uidList}} 占位符, 替换sql中的参数,后面使用uidList 列表需要解析成英文逗号分割
joinRequest.setSql(
"SELECT a.*, b.nick_name AS b_nick_name FROM custom_table_table_name a LEFT JOIN custom_table_table_name b ON a.uid = b.uid WHERE a.uid in ({{uidList}})");
DorisTableDataResponse response = composeDbTableRpcService.queryTableData(joinRequest);
log.info("SELECT response: {}", JSON.toJSONString(response));
assertNotNull(response);
assertNotNull(response.getData());
// 可断言返回数据包含 user_12346 及 b_nick_name 字段
} finally {
RequestContext.remove();
}
}
@Test
void testBatchInsertCustomTable28() {
// 设置租户上下文,防止 MyBatis Plus 多租户插件报错
try {
RequestContext.setThreadTenantId(1L);
DorisTableDataRequest batchInsertRequest = new DorisTableDataRequest();
batchInsertRequest.setTableId(28L);
batchInsertRequest.setSql(
"INSERT INTO custom_table_table_name ( agent_id, agent_name, nick_name, uid, user_name, created) VALUES "
+
"( 'agent_003', '测试智能体3', '测试昵称3', 'user_12347', 'user3', NOW()), " +
"( 'agent_004', '测试智能体4', '测试昵称4', 'user_12348', 'user4', NOW()), " +
"( 'agent_005', '测试智能体5', '测试昵称5', 'user_12349', 'user5', NOW())");
DorisTableDataResponse response = composeDbTableRpcService.queryTableData(batchInsertRequest);
log.info("batch insert response: {}", JSON.toJSONString(response));
assertNotNull(response);
// 验证批量插入的行数应该为3
assertEquals(3L, response.getRowNum());
} finally {
RequestContext.remove();
}
}
@Test
void testBatchInsertWithSemicolonCustomTable28() {
// 设置租户上下文,防止 MyBatis Plus 多租户插件报错
try {
RequestContext.setThreadTenantId(1L);
DorisTableDataRequest batchInsertRequest = new DorisTableDataRequest();
batchInsertRequest.setTableId(28L);
batchInsertRequest.setSql(
"INSERT INTO custom_table_table_name ( agent_id, agent_name, nick_name, uid, user_name, created) VALUES ( 'agent_006', '测试智能体6', '测试昵称6', 'user_12350', 'user6', NOW());"
+
"INSERT INTO custom_table_table_name ( agent_id, agent_name, nick_name, uid, user_name, created) VALUES ( 'agent_007', '测试智能体7', '测试昵称7', 'user_12351', 'user7', NOW());"
+
"INSERT INTO custom_table_table_name ( agent_id, agent_name, nick_name, uid, user_name, created) VALUES ( 'agent_008', '测试智能体8', '测试昵称8', 'user_12352', 'user8', NOW());");
DorisTableDataResponse response = composeDbTableRpcService.queryTableData(batchInsertRequest);
log.info("batch insert with semicolon response: {}", JSON.toJSONString(response));
assertNotNull(response);
// 验证批量插入的行数,使用分号分割时每条语句都会执行成功
assertNotNull(response.getRowNum());
} finally {
RequestContext.remove();
}
}
@Test
void testBatchUpdateWithSemicolonCustomTable28() {
// 设置租户上下文,防止 MyBatis Plus 多租户插件报错
try {
RequestContext.setThreadTenantId(1L);
DorisTableDataRequest batchUpdateRequest = new DorisTableDataRequest();
batchUpdateRequest.setTableId(28L);
batchUpdateRequest.setSql(
"UPDATE custom_table_table_name SET agent_name='更新智能体3', nick_name='更新昵称3' WHERE agent_id='agent_003';"
+
"UPDATE custom_table_table_name SET agent_name='更新智能体4', nick_name='更新昵称4' WHERE agent_id='agent_004';"
+
"UPDATE custom_table_table_name SET agent_name='更新智能体5', nick_name='更新昵称5' WHERE agent_id='agent_005';");
DorisTableDataResponse response = composeDbTableRpcService.queryTableData(batchUpdateRequest);
log.info("batch update with semicolon response: {}", JSON.toJSONString(response));
assertNotNull(response);
// 验证批量更新成功
assertNotNull(response.getRowNum());
// 验证数据是否更新成功
DorisTableDataRequest selectRequest = new DorisTableDataRequest();
selectRequest.setTableId(28L);
selectRequest.setSql(
"SELECT * FROM custom_table_table_name WHERE agent_id IN ('agent_003', 'agent_004', 'agent_005')");
DorisTableDataResponse selectResponse = composeDbTableRpcService.queryTableData(selectRequest);
log.info("select after batch update response: {}", JSON.toJSONString(selectResponse));
assertNotNull(selectResponse);
assertNotNull(selectResponse.getData());
} finally {
RequestContext.remove();
}
}
@Test
public void testCreateTable_Success() {
try {
RequestContext.setThreadTenantId(1L);
// 第一步:先获取一个已存在表的定义,作为参考模板
DorisTableDefineRequest queryRequest = new DorisTableDefineRequest();
queryRequest.setTableId(113L); // 请根据实际情况修改
CreateTableDefineVo templateTable = composeDbTableRpcService.queryCreateTableInfo(queryRequest);
log.info("Loaded template table definition: {}", JSON.toJSONString(templateTable));
// 第三步:执行创建表操作
var tableId = composeDbTableRpcService.createTable(templateTable);
log.info("Table created, tableId={}",tableId);
// 可以再次查询验证表是否创建成功
// 这里省略验证步骤
} catch (Exception e) {
log.error("Create table failed", e);
assert false : "表创建应该成功,但抛出了异常: " + e.getMessage();
} finally {
RequestContext.remove();
}
}
}

View File

@@ -0,0 +1,103 @@
package com.xspaceagi.compose.api.service;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.xspaceagi.log.api.service.LogPlatformRpcServiceImpl;
import com.xspaceagi.log.sdk.reponse.AgentLogModelResponse;
import com.xspaceagi.log.sdk.request.AgentLogEntryRequest;
import com.xspaceagi.log.sdk.request.AgentLogSearchParamsRequest;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@SpringBootTest
public class LogPlatformRpcServiceImplTest {
@Autowired
private LogPlatformRpcServiceImpl logPlatformRpcService;
@Test
public void testAddLog_Success() {
// Arrange
AgentLogEntryRequest request = new AgentLogEntryRequest();
// 填充request的测试数据根据实际定义
request.setRequestId("req_001333");
request.setMessageId("msg_001");
request.setConversationId("session_001");
request.setUserUid("user_123");
request.setTenantId("1");
request.setSpaceId("space_001");
request.setUserInput("请帮我分析一下这个数据");
request.setOutput("根据您提供的数据,我分析出以下结论...");
request.setRequestStartTime(LocalDateTime.parse("2024-01-15T10:30:00Z", DateTimeFormatter.ISO_DATE_TIME));
request.setRequestEndTime(LocalDateTime.parse("2024-01-15T10:30:05Z", DateTimeFormatter.ISO_DATE_TIME));
request.setStatus("success");
request.setUserId(1L);
request.setUserName("张三");
// Act
boolean result = logPlatformRpcService.addLog(request);
// Assert
assert (result);
}
@Test
public void testSearchAgentLogs_Success() {
// Arrange - 准备搜索参数
AgentLogSearchParamsRequest searchParams = new AgentLogSearchParamsRequest();
// 设置时间范围 - 使用RFC 3339格式ISO_DATE_TIME
LocalDateTime startTime = LocalDateTime.parse("2024-01-01T00:00:00Z", DateTimeFormatter.ISO_DATE_TIME);
LocalDateTime endTime = LocalDateTime.parse("2024-12-31T23:59:59Z", DateTimeFormatter.ISO_DATE_TIME);
searchParams.setStartTime(startTime);
searchParams.setEndTime(endTime);
searchParams.setRequestId("req_001333");
// 设置其他搜索条件
searchParams.setTenantId("1"); // 当前租户
searchParams.setUserInput(Arrays.asList("分析", "数据")); // 搜索包含这些关键词的用户输入
// 设置当前页和页大小
Long current = 1L;
Long pageSize = 10L;
log.info("searchParams result", JSON.toJSONString(searchParams));
// Act - 执行搜索
IPage<AgentLogModelResponse> result = logPlatformRpcService.searchAgentLogs(searchParams, current, pageSize);
// Assert - 验证结果
log.info("Total search hits: {}", result.getTotal());
log.info("Current page row count: {}", result.getRecords().size());
log.info("result", JSON.toJSONString(result));
// check 结果记录, RequestId("req_001333") 必须都是这个值的
for (AgentLogModelResponse log : result.getRecords()) {
assert (log.getRequestId().equals("req_001333"));
}
// 如果有搜索结果,验证第一条记录的基本信息
if (!result.getRecords().isEmpty()) {
AgentLogModelResponse firstLog = result.getRecords().get(0);
log.info("First row requestId: {}", firstLog.getRequestId());
log.info("First row user input: {}", firstLog.getUserInput());
log.info("First row createdAt: {}", firstLog.getCreatedAt());
// 验证返回的记录包含必要的字段
assert (firstLog.getRequestId() != null);
assert (firstLog.getUserInput() != null);
}
}
}

View File

@@ -0,0 +1,59 @@
package com.xspaceagi.ecoMarket;
import com.xspaceagi.eco.market.sdk.reponse.ClientSecretResponse;
import com.xspaceagi.eco.market.sdk.request.ClientSecretRequest;
import com.xspaceagi.eco.market.sdk.service.IEcoMarketRpcService;
import com.xspaceagi.system.spec.common.RequestContext;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/**
* EcoMarketRpcService 集成测试类
*/
@Slf4j
@SpringBootTest
public class EcoMarketRpcServiceTest {
@Resource
private IEcoMarketRpcService ecoMarketRpcService;
/**
* 测试查询客户端密钥功能 - 成功场景
*/
@Test
public void testQueryClientSecret_Success() {
try {
RequestContext.setThreadTenantId(1L);
// Arrange
ClientSecretRequest request = new ClientSecretRequest();
request.setTenantId(1L);
// Act
ClientSecretResponse response = ecoMarketRpcService.queryClientSecret(request);
log.info("Client secret query result, response={}", response);
// Assert
assertNotNull(response, "响应不应为空");
if (response != null) {
assertNotNull(response.getTenantId(), "租户ID不应为空");
assertEquals(1L, response.getTenantId(), "租户ID应该匹配");
log.info("Client secret: id={}, name={}, clientId={}, tenantId={}",
response.getId(), response.getName(), response.getClientId(), response.getTenantId());
}
} catch (Exception e) {
log.error("Exception while testing client secret query", e);
throw e;
} finally {
RequestContext.remove();
}
}
}

View File

@@ -0,0 +1,200 @@
package com.xspaceagi.knowledge;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import com.alibaba.fastjson2.JSON;
import com.xspaceagi.knowledge.api.KnowledgeConfigRpcService;
import com.xspaceagi.knowledge.api.KnowledgeQaSearchRpcService;
import com.xspaceagi.knowledge.sdk.request.KnowledgeCreateRequestVo;
import com.xspaceagi.knowledge.sdk.request.KnowledgeDocumentRequestVo;
import com.xspaceagi.knowledge.sdk.request.KnowledgeQaRequestVo;
import com.xspaceagi.knowledge.sdk.response.KnowledgeConfigVo;
import com.xspaceagi.knowledge.sdk.response.KnowledgeDocumentResponseVo;
import com.xspaceagi.knowledge.sdk.response.KnowledgeQaResponseVo;
import com.xspaceagi.knowledge.sdk.sevice.IKnowledgeDocumentSearchRpcService;
import com.xspaceagi.system.spec.common.RequestContext;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@SpringBootTest
public class KnowledegRpcServiceImplTest {
@Resource
private KnowledgeConfigRpcService knowledgeConfigRpcService;
@Resource
private IKnowledgeDocumentSearchRpcService knowledgeDocumentSearchRpcService;
@Resource
private KnowledgeQaSearchRpcService knowledgeQaSearchRpcService;
@Test
public void testCreateKnowledgeConfig() {
try {
RequestContext.setThreadTenantId(1L);
// Arrange
KnowledgeCreateRequestVo requestVo = KnowledgeCreateRequestVo.builder()
.spaceId(1L)
.name("Test Knowledge")
.description("This is a test knowledge")
.dataType(1)
.icon("https://example.com/icon.png")
.userId(1L)
.build();
// Act
Long result = knowledgeConfigRpcService.createKnowledgeConfig(requestVo);
log.info("Create database result, result={}", result);
// Assert
assertNotNull(result);
} finally {
RequestContext.remove();
}
}
@Test
public void testQueryKnowledgeConfigById() {
try {
RequestContext.setThreadTenantId(1L);
KnowledgeConfigVo knowledgeConfigVo = knowledgeConfigRpcService.queryKnowledgeConfigById(153L);
log.info("Knowledge config query result, knowledgeConfigVo={}", JSON.toJSONString(knowledgeConfigVo));
assertNotNull(knowledgeConfigVo);
} finally {
RequestContext.remove();
}
}
/**
* 测试知识库文档搜索功能
*/
@Test
public void testDocumentSearch() {
try {
RequestContext.setThreadTenantId(1L);
// Arrange
KnowledgeDocumentRequestVo requestVo = new KnowledgeDocumentRequestVo();
requestVo.setKbId(113L); // 使用上面查询的知识库ID
// Act
KnowledgeDocumentResponseVo responseVo = knowledgeDocumentSearchRpcService.documentSearch(requestVo);
log.info("Knowledge doc search result, responseVo={}", JSON.toJSONString(responseVo));
// Assert
assertNotNull(responseVo);
assertNotNull(responseVo.getDocumentVoList());
log.info("Document count: {}", responseVo.getDocumentVoList().size());
} finally {
RequestContext.remove();
}
}
/**
* 测试知识库文档搜索功能 - 测试空结果
*/
@Test
public void testDocumentSearchWithEmptyResult() {
try {
RequestContext.setThreadTenantId(1L);
// Arrange - 使用一个不存在的知识库ID
KnowledgeDocumentRequestVo requestVo = new KnowledgeDocumentRequestVo();
requestVo.setKbId(999999L);
// Act
KnowledgeDocumentResponseVo responseVo = knowledgeDocumentSearchRpcService.documentSearch(requestVo);
log.info("Knowledge doc search (empty), responseVo={}", JSON.toJSONString(responseVo));
// Assert
assertNotNull(responseVo);
assertNotNull(responseVo.getDocumentVoList());
log.info("Document count: {}", responseVo.getDocumentVoList().size());
} finally {
RequestContext.remove();
}
}
/**
* 测试知识库问答搜索功能
* 测试参数kbId=41, question="浮点数的定义是什么", 期望docId=24
*/
@Test
public void testQaSearch() {
try {
RequestContext.setThreadTenantId(1L);
// Arrange - 创建问答搜索请求
KnowledgeQaRequestVo requestVo = new KnowledgeQaRequestVo();
requestVo.setKbId(413L); // 知识库ID
requestVo.setQuestion("Antigravity"); // 问题
requestVo.setTopK(5); // 返回前5个结果
requestVo.setIgnoreDocStatus(true); // 不忽略文档状态
requestVo.setIgnoreTenantId(false); // 不忽略租户ID
// Act - 执行问答搜索
KnowledgeQaResponseVo responseVo = knowledgeQaSearchRpcService.search(requestVo);
log.info("Knowledge Q&A search result, responseVo={}", JSON.toJSONString(responseVo));
// Assert - 验证结果
assertNotNull(responseVo);
assertNotNull(responseVo.getQaVoList());
log.info("Q&A result count: {}", responseVo.getQaVoList().size());
// 打印详细结果特别是验证docId字段
if (responseVo.getQaVoList() != null && !responseVo.getQaVoList().isEmpty()) {
responseVo.getQaVoList().forEach(qaVo -> {
log.info("Q&A detail - qaId: {}, kbId: {}, docId: {}, question: {}, answer: {}, score: {}",
qaVo.getQaId(), qaVo.getKbId(), qaVo.getDocId(),
qaVo.getQuestion(), qaVo.getAnswer(), qaVo.getScore());
});
}
} finally {
RequestContext.remove();
}
}
/**
* 测试知识库问答搜索功能 - 测试空结果
*/
@Test
public void testQaSearchWithEmptyResult() {
try {
RequestContext.setThreadTenantId(1L);
// Arrange - 使用不存在的问题
KnowledgeQaRequestVo requestVo = new KnowledgeQaRequestVo();
requestVo.setKbId(41L); // 知识库ID
requestVo.setQuestion("一个不存在的问题 xyzabc123"); // 不存在的问题
requestVo.setTopK(5); // 返回前5个结果
requestVo.setIgnoreDocStatus(false); // 不忽略文档状态
requestVo.setIgnoreTenantId(false); // 不忽略租户ID
// Act - 执行问答搜索
KnowledgeQaResponseVo responseVo = knowledgeQaSearchRpcService.search(requestVo);
log.info("Knowledge Q&A search (empty), responseVo={}", JSON.toJSONString(responseVo));
// Assert - 验证结果
assertNotNull(responseVo);
assertNotNull(responseVo.getQaVoList());
log.info("Q&A result count: {}", responseVo.getQaVoList().size());
} finally {
RequestContext.remove();
}
}
}

View File

@@ -0,0 +1,266 @@
package com.xspaceagi.knowledge;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.xspaceagi.system.spec.common.RequestContext;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import com.xspaceagi.knowledge.sdk.request.KnowledgeFullTextSearchRequestVo;
import com.xspaceagi.knowledge.sdk.response.KnowledgeFullTextSearchResponseVo;
import com.xspaceagi.knowledge.sdk.sevice.IKnowledgeFullTextSearchRpcService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
/**
* 知识库全文检索 RPC 服务测试
*
* @author system
* @date 2025-03-31
*/
@Slf4j
@SpringBootTest
public class KnowledgeFullTextSearchRpcServiceTest {
@Resource
private IKnowledgeFullTextSearchRpcService knowledgeFullTextSearchRpcService;
/**
* 测试知识库全文检索功能 - 单知识库检索
*/
@Test
public void testFullTextSearch() {
try {
RequestContext.setThreadTenantId(1L);
// Arrange
KnowledgeFullTextSearchRequestVo requestVo = new KnowledgeFullTextSearchRequestVo();
requestVo.setKbIds(java.util.Arrays.asList(411L));
requestVo.setTenantId(1L);
requestVo.setQueryText("智能体");
requestVo.setTopK(10);
log.info("Start full-text search test: kbIds={}, queryText={}, topK={}",
requestVo.getKbIds(), requestVo.getQueryText(), requestVo.getTopK());
// Act
KnowledgeFullTextSearchResponseVo responseVo = knowledgeFullTextSearchRpcService.search(requestVo);
// Assert
assertNotNull(responseVo, "返回结果不能为空");
assertNotNull(responseVo.getResults(), "结果列表不能为空");
assertTrue(responseVo.getCostTimeMs() >= 0, "耗时应该大于等于0");
log.info("\n\n========================================");
log.info("=== Full-text search test result ===");
log.info("========================================");
log.info("Retrieval result count: {}", responseVo.getResults().size());
log.info("Elapsed: {}ms", responseVo.getCostTimeMs());
log.info("========================================\n");
// 打印前3个结果详情
if (!responseVo.getResults().isEmpty()) {
log.info("\n=== Top 3 results ===");
int index = 1;
for (var result : responseVo.getResults().stream().limit(3).toList()) {
String textPreview = result.getRawText() != null && result.getRawText().length() > 100
? result.getRawText().substring(0, 100) + "..."
: result.getRawText();
log.info("\n[Result #{}]", index++);
log.info(" rawSegmentId: {}", result.getRawSegmentId());
log.info(" docId: {}", result.getDocId());
log.info(" kbId: {}", result.getKbId());
log.info(" BM25 score: {}", result.getScore());
log.info(" sortIndex: {}", result.getSortIndex());
log.info(" documentName: {}", result.getDocumentName());
log.info(" Text: {}", textPreview);
}
log.info("\n========================================\n");
} else {
log.warn("\n⚠ No matching results found.\n");
}
} finally {
RequestContext.remove();
}
}
/**
* 测试全文检索 - 多知识库检索
*/
@Test
public void testFullTextSearchMultipleKbs() {
try {
RequestContext.setThreadTenantId(1L);
// Arrange - 测试多个知识库
KnowledgeFullTextSearchRequestVo requestVo = new KnowledgeFullTextSearchRequestVo();
requestVo.setKbIds(java.util.Arrays.asList(200L, 113L)); // 多个知识库
requestVo.setTenantId(1L);
requestVo.setQueryText("Python");
requestVo.setTopK(5);
log.info("Start multi-KB full-text test: kbIds={}, queryText={}, topK={}",
requestVo.getKbIds(), requestVo.getQueryText(), requestVo.getTopK());
// Act
KnowledgeFullTextSearchResponseVo responseVo = knowledgeFullTextSearchRpcService.search(requestVo);
// Assert
assertNotNull(responseVo);
assertNotNull(responseVo.getResults());
log.info("\n\n========================================");
log.info("=== Multi-KB search test result ===");
log.info("========================================");
log.info("Retrieval result count: {}", responseVo.getResults().size());
log.info("Elapsed: {}ms", responseVo.getCostTimeMs());
// 统计各知识库的结果数量
if (!responseVo.getResults().isEmpty()) {
java.util.Map<Long, Long> kbCountMap = responseVo.getResults().stream()
.collect(java.util.stream.Collectors.groupingBy(
result -> result.getKbId(),
java.util.stream.Collectors.counting()
));
log.info("\n=== Per-KB result distribution ===");
kbCountMap.forEach((kbId, count) ->
log.info("Result count for kbId {}: {}", kbId, count));
log.info("========================================\n");
} else {
log.warn("\n⚠ No matching results found.\n");
}
} finally {
RequestContext.remove();
}
}
/**
* 测试全文检索 - 指定文档ID检索
*/
@Test
public void testFullTextSearchWithDocIds() {
try {
RequestContext.setThreadTenantId(1L);
// Arrange - 测试指定文档ID检索
KnowledgeFullTextSearchRequestVo requestVo = new KnowledgeFullTextSearchRequestVo();
requestVo.setKbIds(java.util.Arrays.asList(200L));
requestVo.setTenantId(1L);
requestVo.setQueryText("Python");
requestVo.setTopK(10);
requestVo.setDocIds(java.util.Arrays.asList(1L, 2L, 3L)); // 指定文档ID
log.info("Start specified-doc search test: kbIds={}, docIds={}, queryText={}",
requestVo.getKbIds(), requestVo.getDocIds(), requestVo.getQueryText());
// Act
KnowledgeFullTextSearchResponseVo responseVo = knowledgeFullTextSearchRpcService.search(requestVo);
// Assert
assertNotNull(responseVo);
assertNotNull(responseVo.getResults());
log.info("\n\n========================================");
log.info("=== Specified-doc search test result ===");
log.info("========================================");
log.info("Retrieval result count: {}", responseVo.getResults().size());
log.info("Elapsed: {}ms", responseVo.getCostTimeMs());
// 验证所有结果都来自指定的文档
if (!responseVo.getResults().isEmpty()) {
boolean allFromSpecifiedDocs = responseVo.getResults().stream()
.allMatch(result -> requestVo.getDocIds().contains(result.getDocId()));
log.info("\nAll results from specified docs: {}", allFromSpecifiedDocs);
log.info("========================================\n");
} else {
log.warn("\n⚠ No matching results found.\n");
}
} finally {
RequestContext.remove();
}
}
/**
* 测试全文检索 - 空结果场景
*/
@Test
public void testFullTextSearchWithNoResults() {
try {
RequestContext.setThreadTenantId(1L);
// Arrange - 使用不存在的知识库ID
KnowledgeFullTextSearchRequestVo requestVo = new KnowledgeFullTextSearchRequestVo();
requestVo.setKbIds(java.util.Arrays.asList(999999L));
requestVo.setTenantId(1L);
requestVo.setQueryText("不存在的内容XYZABC123");
requestVo.setTopK(10);
log.info("Start empty-result test: kbIds={}, queryText={}",
requestVo.getKbIds(), requestVo.getQueryText());
// Act
KnowledgeFullTextSearchResponseVo responseVo = knowledgeFullTextSearchRpcService.search(requestVo);
// Assert
assertNotNull(responseVo);
assertNotNull(responseVo.getResults());
log.info("\n\n========================================");
log.info("=== Empty result test ===");
log.info("========================================");
log.info("Retrieval result count: {}", responseVo.getResults().size());
log.info("Elapsed: {}ms", responseVo.getCostTimeMs());
log.info("========================================\n");
} finally {
RequestContext.remove();
}
}
/**
* 测试全文检索 - 不同TopK值
*/
@Test
public void testFullTextSearchWithDifferentTopK() {
try {
RequestContext.setThreadTenantId(1L);
int[] topKValues = {1, 5, 10, 20};
for (int topK : topKValues) {
// Arrange
KnowledgeFullTextSearchRequestVo requestVo = new KnowledgeFullTextSearchRequestVo();
requestVo.setKbIds(java.util.Arrays.asList(200L));
requestVo.setTenantId(1L);
requestVo.setQueryText("Python");
requestVo.setTopK(topK);
log.info("Test TopK={}: kbIds={}, queryText={}",
topK, requestVo.getKbIds(), requestVo.getQueryText());
// Act
KnowledgeFullTextSearchResponseVo responseVo = knowledgeFullTextSearchRpcService.search(requestVo);
// Assert
assertNotNull(responseVo);
assertNotNull(responseVo.getResults());
assertTrue(responseVo.getResults().size() <= topK,
"返回结果数量不应超过TopK值");
log.info("TopK={}: returned {} rows, {}ms",
topK, responseVo.getResults().size(), responseVo.getCostTimeMs());
}
} finally {
RequestContext.remove();
}
}
}

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-bootstrap</artifactId>
<packaging>pom</packaging>
<modules>
<module>app-platform-web-bootstrap</module>
</modules>
</project>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-foundation</artifactId>
<packaging>pom</packaging>
</project>

View File

@@ -0,0 +1,38 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-agent</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>app-platform-agent-core-adapter</artifactId>
<name>app-platform-agent-core-adapter</name>
<dependencies>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-agent-core-spec</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-client-chat</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-mcp-sdk</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-custom-page-sdk</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>system-application</artifactId>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,241 @@
package com.xspaceagi.agent.core.adapter.application;
import com.xspaceagi.agent.core.adapter.dto.AgentDetailDto;
import com.xspaceagi.agent.core.adapter.dto.CardDto;
import com.xspaceagi.agent.core.adapter.dto.UserAgentDto;
import com.xspaceagi.agent.core.adapter.dto.config.AgentComponentConfigDto;
import com.xspaceagi.agent.core.adapter.dto.config.AgentConfigDto;
import com.xspaceagi.agent.core.adapter.dto.config.Arg;
import com.xspaceagi.agent.core.adapter.dto.config.ModelConfigDto;
import java.util.List;
public interface AgentApplicationService {
/**
* 新增智能体
*
* @param agent
*/
Long add(AgentConfigDto agent);
/**
* 更新智能体基础配置AgentConfig上有的字段
*/
void update(AgentConfigDto agentConfigDto);
/**
* 删除智能体,含配置以及发布信息、统计信息、用户收藏点赞信息
*
* @param agentId
*/
void delete(Long agentId);
/**
* 复制智能体
*
* @param userId
* @param agentId
* @return
*/
Long copyAgent(Long userId, Long agentId);
/**
* 复制智能体
*
* @param userId
* @param agentConfigDto
* @param targetSpaceId
* @return
*/
Long copyAgent(Long userId, AgentConfigDto agentConfigDto, Long targetSpaceId);
Long importAgent(Long userId, AgentConfigDto agentConfigDto, Long targetSpaceId);
/**
* 移动智能体到其他空间
*/
void transfer(Long userId, Long agentId, Long targetSpaceId);
/**
* 删除空间下的所有智能体
*
* @param spaceId
*/
void deleteBySpaceId(Long spaceId);
/**
* 获取空间下的智能体列表
*/
List<AgentConfigDto> queryListBySpaceId(Long spaceId);
/**
* 根据ID获取智能体列表
*
* @param agentIds
* @return
*/
List<AgentConfigDto> queryListByIds(List<Long> agentIds);
/**
* 根据ID获取智能体配置
*
* @param agentId
* @return
*/
AgentConfigDto queryById(Long agentId);
AgentConfigDto queryByUid(String agentUid);
AgentConfigDto queryAgentByIdWithStatics(Long agentId);
/**
* 添加组件配置
*/
void addComponentConfig(AgentComponentConfigDto agentComponentConfigDto);
/**
* 更新组件配置
*
* @param agentComponentConfigDto
*/
void updateComponentConfig(AgentComponentConfigDto agentComponentConfigDto);
/**
* 删除组件配置
*
* @param id 组件配置ID
*/
void deleteComponentConfig(Long id);
/**
* 获取组件配置列表
*/
List<AgentComponentConfigDto> queryComponentConfigList(Long agentId);
/**
* 获取组件配置
*
* @param id
* @return
*/
AgentComponentConfigDto queryComponentConfig(Long id);
List<Arg> getAgentNoneSystemVariables(Long agentId, Long spaceId);
/**
* 获取完整的智能体配置,可选发布状态;执行使用
*/
AgentConfigDto queryPublishedConfigForExecute(Long agentId);
AgentConfigDto queryPublishedConfig(Long agentId, boolean execute);
/**
* 获取完整的智能体配置;测试使用
*/
AgentConfigDto queryConfigForTestExecute(Long agentId);
AgentDetailDto queryAgentDetail(Long agentId, boolean isPublished);
/**
* 获取卡片列表
*/
List<CardDto> queryCardList();
/**
* 收藏
*/
void collect(Long userId, Long agentId);
/**
* 取消收藏
*/
void unCollect(Long userId, Long agentId);
void devCollect(Long userId, Long agentId);
void unDevCollect(Long userId, Long agentId);
/**
* 点赞
*/
void like(Long userId, Long agentId);
/**
* 取消点赞
*/
void unLike(Long userId, Long agentId);
/**
* 添加或更新最近使用的智能体
*
* @param userId
* @param agentId
*/
void addOrUpdateRecentUsed(Long userId, Long agentId);
void addOrUpdateRecentUsed(Long userId, Long agentId, Long conversationId);
/**
* 获取用户最近编辑的智能体列表
*/
List<UserAgentDto> queryRecentEditList(Long userId, Integer size);
/**
* 获取用户最近使用过的智能体列表
*/
List<UserAgentDto> queryRecentUseList(Long userId, String kw, Integer size, Integer pageIndex);
/**
* 获取用户最近使用过的智能体列表
*/
List<UserAgentDto> queryRecentUseList(Long userId, Integer size);
UserAgentDto queryUserAgentRecentUse(Long userId, Long agentId);
/**
* 获取用户收藏的智能体列表
*/
List<UserAgentDto> queryCollectionList(Long userId, Integer page, Integer size);
/**
* 开发收藏
*
* @param userId
* @param page
* @param size
* @return
*/
List<UserAgentDto> queryDevCollectionList(Long userId, Integer page, Integer size);
/**
* 为通用智能体构建MCP用于代理执行插件工作流知识库数据表
*/
void buildProxyMcp(AgentConfigDto agentConfigDto, boolean isDev);
void updateAccessControlStatus(Long agentId, Integer status);
/**
* 统计用户创建的智能体数量
*
* @param userId
* @return
*/
Long countUserCreatedAgent(Long userId);
/**
* 统计用户创建的网页应用数量
*/
Long countUserCreatedPageApp(Long userId);
/**
* 获取用户可选择的模型列表
*
* @param userId 用户ID
* @param agentId 智能体ID
* @return 模型列表
*/
List<ModelConfigDto> queryUserCanSelectModelListForAgent(Long userId, Long agentId);
}

View File

@@ -0,0 +1,47 @@
package com.xspaceagi.agent.core.adapter.application;
import com.xspaceagi.agent.core.adapter.dto.AgentTempChatDto;
import java.util.List;
public interface AgentTempChatApplicationService {
/**
* 创建临时链接
*
* @param agentId
* @return
*/
AgentTempChatDto createTempChat(Long agentId);
/**
* 删除临时链接
*
* @param agentId
* @param id
*/
void deleteTempChat(Long agentId, Long id);
/**
* 修改临时链接
*
* @param agentTempChatDto
*/
void updateTempChat(AgentTempChatDto agentTempChatDto);
/**
* 查询临时链接
*
* @param agentId
* @return
*/
List<AgentTempChatDto> queryTempChatList(Long agentId);
/**
* 根据chatKey查询临时链接
*
* @param chatKey
* @return
*/
AgentTempChatDto queryTempChatByChatKey(String chatKey);
}

View File

@@ -0,0 +1,22 @@
package com.xspaceagi.agent.core.adapter.application;
import com.xspaceagi.agent.core.adapter.dto.AddSkillsToWorkspaceDto;
import com.xspaceagi.agent.core.adapter.dto.CreateWorkspaceDto;
public interface AgentWorkspaceApplicationService {
/**
* 创建/重置工作空间不会重复创建只重置skill/subagent不会清除用户文件
* 如果传了技能id列表会同时推送技能文件
* 如果传了subagent列表会同时推送subagent文件
*/
void createWorkspace(CreateWorkspaceDto createWorkspaceDto);
/**
* 动态添加技能到工作空间
* 支持传多个技能 id动态增加的技能在重置工作空间时不会被清除
*/
void addSkillsToWorkspace(AddSkillsToWorkspaceDto addSkillsToWorkspaceDto);
}

View File

@@ -0,0 +1,40 @@
package com.xspaceagi.agent.core.adapter.application;
import com.xspaceagi.agent.core.adapter.dto.PublishedDto;
import com.xspaceagi.agent.core.adapter.repository.entity.Published;
import java.util.List;
public interface CollectApplicationService {
/**
* 收藏
*/
void collect(Long userId, Published.TargetType targetType, Long targetId);
/**
* 取消收藏
*/
void unCollect(Long userId, Published.TargetType targetType, Long targetId);
/**
* 查询收藏列表
* @param userId
* @param targetType
* @param spaceId
* @return
*/
List<PublishedDto> queryCollectList(Long userId, Published.TargetType targetType, Long spaceId);
/**
* 查询收藏列表
*/
List<PublishedDto> queryCollectList(Long userId, Published.TargetType targetType, List<Long> spaceIds);
/**
* 查询收藏列表
*/
List<PublishedDto> queryCollectListWithoutConfig(Long userId, Published.TargetType targetType, List<Long> spaceIds);
}

View File

@@ -0,0 +1,10 @@
package com.xspaceagi.agent.core.adapter.application;
import java.util.List;
import com.xspaceagi.agent.core.adapter.dto.ComponentDto;
public interface ComponentApplicationService {
List<ComponentDto> getComponentListBySpaceId(Long spaceId);
}

View File

@@ -0,0 +1,29 @@
package com.xspaceagi.agent.core.adapter.application;
import com.xspaceagi.agent.core.adapter.dto.ComputerPodResultDto;
/**
* 容器应用服务
*/
public interface ComputerPodApplicationService {
/**
* 启动容器
*/
ComputerPodResultDto ensurePod(Long cId);
/**
* 容器保活
*/
ComputerPodResultDto keepalive(Long cId);
/**
* 重启容器(销毁后重建)
*/
ComputerPodResultDto restart(Long cId);
/**
* 查询容器 VNC 服务状态
*/
ComputerPodResultDto getVncStatus(Long cId);
}

View File

@@ -0,0 +1,14 @@
package com.xspaceagi.agent.core.adapter.application;
import com.xspaceagi.agent.core.adapter.dto.ConfigHistoryDto;
import com.xspaceagi.agent.core.adapter.repository.entity.Published;
import java.util.List;
public interface ConfigHistoryApplicationService {
List<ConfigHistoryDto> queryConfigHistoryList(Published.TargetType targetType, Long targetId);
ConfigHistoryDto queryConfigHistory(Long id);
}

View File

@@ -0,0 +1,140 @@
package com.xspaceagi.agent.core.adapter.application;
import com.xspaceagi.agent.core.adapter.dto.*;
import com.xspaceagi.agent.core.adapter.repository.entity.Conversation;
import org.springframework.ai.chat.messages.Message;
import reactor.core.publisher.Flux;
import java.util.Collection;
import java.util.List;
import java.util.Map;
public interface ConversationApplicationService {
/**
* 创建会话,返回会话
*/
ConversationDto createConversation(Long userId, Long agentId, boolean devMode);
ConversationDto createConversation(Long userId, Long agentId, boolean devMode, Map<String, Object> variables);
ConversationDto createConversation(Long userId, Long agentId, boolean devMode, boolean tempChat, Map<String, Object> variables);
ConversationDto createConversation(Long userId, Long agentId, boolean devMode, boolean tempChat);
void createConversationForPageApp(Long userId, Long agentId);
ConversationDto createConversationForTaskCenter(Long tenantId, Long userId, Long agentId);
/**
* 创建任务会话,返回会话
*
* @param userId
* @return
*/
ConversationDto createTaskConversation(Long userId, TaskConversationAddOrUpdateDto taskConversationAddOrUpdateDto);
/**
* 取消任务会话
*
* @param conversationId
*/
void cancelTaskConversation(Long userId, Long conversationId);
/**
* 更新任务会话
*/
void updateTaskConversation(Long userId, TaskConversationAddOrUpdateDto taskConversationAddOrUpdateDto);
void updateConversationSandboxServerId(Long cid, String sandboxServerId);
void updateConversationStatus(Long cid, Conversation.ConversationTaskStatus status);
/**
* 删除会话
*
* @param userId
* @param id
*/
void deleteConversation(Long userId, Long id);
/**
* 更新会话
*/
void updateConversationTopic(Long userId, ConversationUpdateDto conversationUpdateDto);
/**
* 查询会话
*
* @param userId
* @param id
* @return
*/
ConversationDto getConversation(Long userId, Long id);
ConversationDto getConversationByCid(Long cid);
ConversationDto getConversationByUid(Long userId, String uid);
ConversationDto getConversationByUid(String uid);
/**
* 查询会话列表
*
* @param userId
* @return
*/
List<ConversationDto> queryConversationList(Long userId, Long agentId);
List<ConversationDto> queryConversationList(Long userId, Long agentId, Long lastId, Integer limit, String topic);
/**
* 根据sandboxServerId查询会话列表
*
* @return
*/
List<ConversationDto> queryConversationListBySandboxServerId(Long sandboxServerId);
/**
* 查询任务会话列表
*
* @param userId
* @return
*/
List<ConversationDto> queryTaskConversationList(Long userId, Long agentId, Conversation.ConversationTaskStatus taskStatus);
/**
* 查询会话消息
*/
List<ChatMessageDto> queryConversationMessageList(Long userId, Long conversationId, Long index, int size);
/**
* 总结会话
*/
void summaryConversation(Long conversationId);
/**
* 放进会话总结队列
*
* @param conversationId 会话id
*/
void pushToSummaryQueue(Long conversationId);
String queryMemory(Long tenantId, Long userId, Long agentId, String inputMessage, String context, boolean justKeywordSearch, boolean filterSensitive);
Flux<AgentOutputDto> chat(TryReqDto tryReqDto, Map<String, String> headersFromRequest, boolean isTempChat);
Flux<AgentOutputDto> chat(TryReqDto tryReqDto, Map<String, String> headersFromRequest, boolean isTempChat, Boolean devMode);
List<AgentUserDto> queryAgentUserList(Long agentId, Long cursorUserId);
Long nextConversationId(Long agentId, String sandboxServerId);
Collection<? extends Message> getRoundMessages(String conversationId, int i);
void addRoundMessage(String conversationId, Message message);
}

View File

@@ -0,0 +1,49 @@
package com.xspaceagi.agent.core.adapter.application;
import com.xspaceagi.agent.core.adapter.dto.ComputerFileInfo;
import com.xspaceagi.system.spec.common.UserContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import java.util.List;
import java.util.Map;
/**
* 通用文件应用服务
*/
public interface IComputerFileApplicationService {
/**
* 查询文件列表
*/
Map<String, Object> getFileList(Long userId, Long cId, String proxyPath, UserContext userContext);
/**
* 更新文件
*/
Map<String, Object> filesUpdate(Long userId, Long cId, List<ComputerFileInfo> files, UserContext userContext);
/**
* 上传文件
*/
Map<String, Object> uploadFile(Long userId, Long cId, String filePath, MultipartFile file, UserContext userContext);
/**
* 批量上传文件
*/
Map<String, Object> uploadFiles(Long userId, Long cId, List<String> filePaths, List<MultipartFile> files, UserContext userContext);
/**
* 获取静态文件
*/
ResponseEntity<StreamingResponseBody> getStaticFile(Long cId, HttpServletRequest request);
/**
* 下载全部文件zip
*/
ResponseEntity<StreamingResponseBody> downloadAllFiles(Long userId, Long cId, UserContext userContext);
}

View File

@@ -0,0 +1,145 @@
package com.xspaceagi.agent.core.adapter.application;
import com.xspaceagi.agent.core.adapter.dto.CodeCheckResultDto;
import com.xspaceagi.agent.core.adapter.dto.ModelQueryDto;
import com.xspaceagi.agent.core.adapter.dto.config.ModelConfigDto;
import org.springframework.core.ParameterizedTypeReference;
import java.util.List;
public interface ModelApplicationService {
/**
* 添加或更新模型配置
*
* @param modelDto
*/
void addOrUpdate(ModelConfigDto modelDto);
/**
* 更新模型访问控制状态
*
* @param id
* @param status
*/
void updateAccessControlStatus(Long id, Integer status);
/**
* 删除模型配置
*
* @param modelId
*/
void delete(Long modelId);
/**
* 查询空间下的模型列表
*/
List<ModelConfigDto> queryModelConfigLisBySpaceId(Long spaceId);
/**
* 查询可使用的模型列表
*
* @return
*/
List<ModelConfigDto> queryModelConfigList(ModelQueryDto modelQueryDto);
/**
* 获取商家全局模型
*
* @return
*/
List<ModelConfigDto> queryTenantModelConfigList(Integer accessControlStatus);
/**
* 根据ID查询模型配置
*
* @param modelId
* @return
*/
ModelConfigDto queryModelConfigById(Long modelId);
List<ModelConfigDto> queryModelConfigListByIds(List<Long> modelIds);
ModelConfigDto queryDefaultModelConfig();
ModelConfigDto queryDefaultModelConfig(Long tenantId);
/**
* 检查模型使用权限
*
* @param modelId
*/
void checkModelUsePermission(Long modelId);
/**
* 检查模型管理权限
*
* @param modelId
*/
void checkModelManagePermission(Long modelId);
/**
* 通过提示词给到模型执行返回结果根据实际定义的Bean
*
* @param sysPrompt 系统提示词
* @param userPrompt 用户输入
* @param type 类型
* @return
*/
<T> T call(String sysPrompt, String userPrompt, ParameterizedTypeReference<T> type);
/**
* 通过提示词给到模型执行返回结果根据实际定义的Bean
*
* @param modelId 模型ID
* @param sysPrompt 系统提示词
* @param userPrompt 用户输入
* @param type 类型
* @return
*/
<T> T call(Long modelId, String sysPrompt, String userPrompt, ParameterizedTypeReference<T> type);
/**
* 通过提示词给到模型执行返回结果为定义的Bean
*
* @param userPrompt 用户输入
* @param type 类型
* @return
*/
<T> T call(String userPrompt, ParameterizedTypeReference<T> type);
/**
* 通过提示词给到模型执行,返回结果为文本
*
* @param userPrompt 用户输入
* @return
*/
String call(String userPrompt);
/**
* 向量化
*
* @param texts
* @return
*/
List<float[]> embeddings(List<String> texts, Long modelId);
ModelConfigDto getDefaultEmbedModel();
void checkUserModelPermission(Long userId, Long modelId);
CodeCheckResultDto codeSaleCheck(String code);
/**
* 测试模型连通性
*
* @param modelConfig
* @param testPrompt
* @return
*/
String testModelConnectivity(ModelConfigDto modelConfig, String testPrompt);
List<ModelConfigDto> getMySystemModels(Long userId, String tab);
}

View File

@@ -0,0 +1,95 @@
package com.xspaceagi.agent.core.adapter.application;
import com.xspaceagi.agent.core.adapter.dto.*;
import com.xspaceagi.agent.core.adapter.dto.config.Arg;
import com.xspaceagi.agent.core.adapter.dto.config.plugin.PluginDto;
import java.util.List;
public interface PluginApplicationService {
/**
* 新增插件
*/
Long add(PluginAddDto pluginAddDto);
/**
* 更新插件
*/
void update(PluginUpdateDto pluginUpdateDto);
List<Arg> analysisPluginOutput(AnalysisHttpPluginOutputDto analysisHttpPluginOutputDto);
/**
* 删除插件
*/
void delete(Long pluginId);
/**
* 复制插件
*
* @return
*/
Long copyPlugin(Long userId, Long pluginId);
Long copyPlugin(Long userId, PluginDto pluginDto, Long targetSpaceId);
/**
* 删除空间下的所有插件
*
* @param spaceId
*/
void deleteBySpaceId(Long spaceId);
/**
* 获取空间下的插件列表
*/
List<PluginDto> queryListBySpaceId(Long spaceId);
/**
* 根据ID获取插件列表
*
* @return
*/
List<PluginDto> queryListByIds(List<Long> pluginIds);
/**
* 根据ID获取插件配置
*
* @return
*/
PluginDto queryById(Long pluginId);
/**
* 获取发布状态的插件配置
*
* @param pluginId
* @return
*/
PluginDto queryPublishedPluginConfig(Long pluginId, Long spaceId);
/**
* 测试插件执行
*
* @return
*/
PluginExecuteResultDto execute(PluginExecuteRequestDto pluginExecuteRequestDto, PluginDto pluginDto);
/**
* 检查空间插件权限
*
* @param spaceId
* @param pluginId
*/
void checkSpacePluginPermission(Long spaceId, Long pluginId);
/**
* 检查插件配置完整性
*
* @param pluginDto
* @return
*/
List<String> validatePluginConfig(PluginDto pluginDto);
}

View File

@@ -0,0 +1,111 @@
package com.xspaceagi.agent.core.adapter.application;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.xspaceagi.agent.core.adapter.dto.*;
import com.xspaceagi.agent.core.adapter.repository.entity.Published;
import com.xspaceagi.system.spec.dto.PageQueryVo;
import com.xspaceagi.system.spec.page.SuperPage;
import java.util.List;
public interface PublishApplicationService {
/**
* 获取已发布分页结果
*/
SuperPage<PublishedDto> queryPublishedList(PublishedQueryDto publishedQueryDto);
SuperPage<PublishedDto> queryPublishedListForAt(PublishedQueryDto publishedQueryDto);
List<PublishedDto> queryPublishedList(Published.TargetType targetType, List<Long> targetIds);
List<PublishedDto> queryPublishedListWithoutConfig(Published.TargetType targetType, List<Long> targetIds, String kw);
List<PublishedDto> queryPublishedList(Published.TargetType targetType, List<Long> targetIds, String kw);
IPage<PublishedDto> queryPublishedListForManage(PublishedQueryDto publishedQueryDto);
/**
* 获取已发布详情
*/
PublishedDto queryPublished(Published.TargetType targetType, Long targetId);
PublishedDto queryPublished(Published.TargetType targetType, Long targetId, boolean loadConfig);
PublishedDto queryPublishedWithSpaceId(Published.TargetType targetType, Long targetId, Long spaceId);
/**
* 发布申请
*/
Long publishApply(PublishApplyDto publishApplyDto);
/**
* 通过发布
*/
void publish(Long applyId);
void publish(Published.TargetType targetType, Long targetId, Published.PublishScope scope, List<PublishApplyDto> publishApplies);
/**
* 复制发布信息到指定空间
*/
Long copyPublish(Long userId, Published.TargetType targetType, Long targetId, Long originalSpaceId, Long targetSpaceId);
/**
* 拒绝发布
*/
void rejectPublish(PublishRejectDto publishRejectDto);
/**
* 下架
*/
void offShelf(OffShelfDto offShelfDto);
/**
* 查询发布申请列表
*
* @param pageQueryVo
* @return
*/
IPage<PublishApplyDto> queryPublishApplyList(PageQueryVo<PublishApplyQueryDto> pageQueryVo);
/**
* 根据ID查询发布申请
*
* @param applyId
* @return
*/
PublishApplyDto queryPublishApplyById(Long applyId);
/**
* 根据publishId查询数据
*/
PublishedDto queryPublishedById(Long publishId);
/**
* 增加统计数量
*
* @param targetType
* @param targetId
* @param key
* @param inc
*/
void incStatisticsCount(Published.TargetType targetType, Long targetId, String key, Long inc);
/**
* 删除发布申请
*/
void deletePublishedApply(Published.TargetType type, Long targetId);
StatisticsDto queryStatistics(Published.TargetType type, Long targetId);
void deleteBySpaceId(Long spaceId);
PublishedPermissionDto hasPermission(Published.TargetType targetType, Long targetId);
void updateAccessControlStatus(Published.TargetType targetType, Long targetId, Integer status);
void updatePublishName(Published.TargetType targetType, Long targetId, String name);
}

View File

@@ -0,0 +1,100 @@
package com.xspaceagi.agent.core.adapter.application;
import com.xspaceagi.agent.core.adapter.dto.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.List;
public interface SkillApplicationService {
/**
* 添加技能
*/
Long add(SkillConfigDto skillConfigDto);
/**
* 更新技能
*/
void update(SkillConfigDto skillConfigDto, boolean isReplaceFiles);
/**
* 删除技能
*/
void delete(Long skillId);
/**
* 根据ID查询技能
*/
SkillConfigDto queryById(Long skillId, boolean loadFiles);
/**
* 根据ID查询技能
*/
SkillConfigDto queryById(Long skillId);
/**
* 查询已发布的技能配置
*/
SkillConfigDto queryPublishedSkillConfig(Long skillId, Long spaceId, boolean loadFiles);
/**
* 解析已发布技能配置(兼容新旧格式)
*/
SkillConfigDto parsePublishedSkillConfig(String config, Object ext);
/**
* 查询用户相关的已发布的技能,过滤掉没有权限的技能
*/
List<SkillConfigDto> queryUserRelatedPublishedSkillConfigs(Long userId, List<Long> skillIds);
/**
* 查询技能列表
*/
List<SkillConfigDto> queryList(SkillQueryDto queryDto);
/**
* 导出技能
*/
SkillExportResultDto exportSkill(Long skillId);
/**
* 导出技能
*/
SkillExportResultDto exportSkill(SkillConfigDto skillConfigDto);
/**
* 导入技能
*/
Long importSkill(MultipartFile file, SkillConfigDto existSkill, Long targetSpaceId, SkillExtDto ext);
/**
* 复制技能
*/
Long copySkill(SkillConfigDto skillConfigDto, Long targetSpaceId);
/**
* 查询技能模板
*/
SkillConfigDto getSkillTemplate(InputStream inputStream);
/**
* 校验空间技能权限
*/
void checkSpaceSkillPermission(Long spaceId, Long skillId);
/**
* 处理上传的文件并上传到文件服务,返回文件索引
*/
SkillFileDto processUploadFile(MultipartFile file, String filePath, Long skillId);
/**
* 记录最近使用的技能
*/
void saveRecentlyUsedSkills(List<Long> skillIds);
/**
* 查询最近使用的技能
*/
List<PublishedDto> queryRecentlyUsedSkills(String kw, Integer size);
}

View File

@@ -0,0 +1,12 @@
package com.xspaceagi.agent.core.adapter.application;
import com.xspaceagi.agent.core.adapter.dto.ExportTemplateDto;
import com.xspaceagi.agent.core.adapter.repository.entity.Published;
import com.xspaceagi.system.application.dto.UserDto;
public interface TemplateExportOrImportService {
ExportTemplateDto queryTemplateConfig(Published.TargetType targetType, Long targetId);
Long importTemplateConfig(UserDto user, Long spaceId, Published.TargetType targetType, String templateConfig);
}

View File

@@ -0,0 +1,165 @@
package com.xspaceagi.agent.core.adapter.application;
import com.alibaba.fastjson2.JSONObject;
import com.xspaceagi.agent.core.adapter.dto.*;
import com.xspaceagi.agent.core.adapter.dto.config.workflow.WorkflowConfigDto;
import com.xspaceagi.agent.core.adapter.dto.config.workflow.WorkflowNodeDto;
import reactor.core.publisher.Flux;
import java.util.List;
public interface WorkflowApplicationService {
/**
* 新增工作流
*/
Long add(WorkflowConfigDto workflowConfig);
/**
* 整体保存工作流
*/
void save(JSONObject workflowConfigJson, WorkflowConfigDto oldWorkflowConfigDto);
/**
* 更新工作流
*/
void update(WorkflowConfigDto workflowConfig);
/**
* 删除工作流
*/
void delete(Long workflowId);
/**
* 复制工作流
*
* @return
*/
Long copyWorkflow(Long userId, Long workflowId);
/**
* 复制工作流
*
* @return
*/
Long copyWorkflow(Long userId, WorkflowConfigDto workflowConfigDto, Long targetSpaceId);
void restoreWorkflow(WorkflowConfigDto workflowConfigDto);
void restoreWorkflow(String historyConfig);
/**
* 移动工作流到其他空间
*/
void transfer(Long workflowId, Long targetSpaceId);
/**
* 删除空间下的所有工作流
*
* @param spaceId
*/
void deleteBySpaceId(Long spaceId);
/**
* 获取空间下的工作流列表
*/
List<WorkflowConfigDto> queryListBySpaceId(Long spaceId);
/**
* 根据ID获取工作流列表
*
* @return
*/
List<WorkflowConfigDto> queryListByIds(List<Long> workflowIds);
/**
* 根据ID获取工作流配置
*
* @return
*/
WorkflowConfigDto queryById(Long workflowId);
WorkflowConfigDto queryByIdWithoutNodes(Long workflowId);
//查询已发布的工作流配置
WorkflowConfigDto queryPublishedWorkflowConfig(Long workflowId, Long spaceId, boolean forExecute);
WorkflowConfigDto queryPublishedWorkflowConfig(Long workflowId, Long spaceId);
/**
* 添加节点
*/
Long addWorkflowNode(WorkflowNodeAddDto workflowNodeAddDto);
/**
* 更新节点配置
*/
<T> void updateWorkflowNodeConfig(WorkflowNodeUpdateDto<T> workflowNodeDto);
/**
* 删除节点
*/
void deleteWorkflowNode(Long id);
/**
* 获取节点列表
*/
List<WorkflowNodeDto> queryWorkflowNodeList(Long workflowId);
/**
* 获取节点列表
*/
List<WorkflowNodeDto> queryWorkflowNodeListForTestExecute(Long workflowId);
/**
* 获取上级节点列表
*/
PreviousDto queryPreviousNodes(Long nodeId);
/**
* 获取节点配置
*
* @param id
* @return
*/
WorkflowNodeDto queryWorkflowNode(Long id);
Long copyWorkflowNode(Long id);
void updateNextIds(Long nodeId, List<Long> nextIds);
/**
* 检查用户工作流权限
*
* @param spaceId
* @param workflowId
*/
void checkSpaceWorkflowPermission(Long spaceId, Long workflowId);
/**
* 梳理出工作流节点层级关系,输出给前端配置使用
*
* @param workflowNodeDtos
* @return
*/
List<WorkflowNodeDto> organizeNodeHierarchicalRelationship(List<WorkflowNodeDto> workflowNodeDtos);
/**
* 校验工作流节点配置是否正确,用于发布和试运行之前
*
* @param workflowId
* @return
*/
List<WorkflowNodeCheckDto> validWorkflow(Long workflowId);
WorkflowNodeCheckDto validWorkflowNode(WorkflowNodeDto workflowNodeDto);
void updateLoopInnerNodes(Long loopNodeId, List<JSONObject> innerNodes);
Flux<WorkflowExecutingDto> executeWorkflow(WorkflowExecuteRequestDto workflowExecuteRequestDto, WorkflowConfigDto workflowConfigDto);
Long workflowEditVersion(Long workflowId, boolean inc);
}

View File

@@ -0,0 +1,9 @@
package com.xspaceagi.agent.core.adapter.constant;
public final class SkillFileFormatConstants {
private SkillFileFormatConstants() {
}
public static final String SKILL_FILES_V2 = "skill_files_v2";
}

View File

@@ -0,0 +1,31 @@
package com.xspaceagi.agent.core.adapter.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "动态增加技能到工作空间请求DTO")
public class AddSkillsToWorkspaceDto implements Serializable {
@Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long userId;
@NotNull
@JsonProperty("cId")
@Schema(description = "会话ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long cId;
@Schema(description = "技能ID列表", requiredMode = Schema.RequiredMode.REQUIRED)
private List<SkillConfigDto> skillConfigs;
}

View File

@@ -0,0 +1,34 @@
package com.xspaceagi.agent.core.adapter.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AgentAddDto implements Serializable {
@Schema(description = "空间ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "Space ID is required")
private Long spaceId; // 空间ID
@Schema(description = "Agent名称", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "Agent name is required")
private String name; // Agent名称
@Schema(description = "Agent描述")
private String description; // Agent描述
@Schema(description = "图标地址")
private String icon; // 图标地址
@Schema(description = "类型ChatBot 对话智能体TaskAgent 任务型智能体", requiredMode = Schema.RequiredMode.REQUIRED)
private String type;
}

View File

@@ -0,0 +1,31 @@
package com.xspaceagi.agent.core.adapter.dto;
import com.xspaceagi.agent.core.adapter.repository.entity.AgentComponentConfig;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AgentComponentConfigAddDto {
@Schema(description = "关联的AgentID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "agentId is required")
private Long agentId; // AgentID
@Schema(description = "组件类型", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "type is required")
private AgentComponentConfig.Type type;
@Schema(description = "关联的组件ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "targetId is required")
private Long targetId; // 关联的组件ID
@Schema(description = "工具名称选择MCP时有用")
private String toolName;
}

View File

@@ -0,0 +1,37 @@
package com.xspaceagi.agent.core.adapter.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AgentComponentConfigUpdateDto<T> {
@Schema(description = "组件配置ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long id; // 组件配置ID
@Schema(description = "组件名称")
private String name;
@Schema(description = "组件图标")
private String icon;
@Schema(description = "组件描述")
private String description;
@Schema(description = "目标组件ID")
private Long targetId;
@Schema(description = "绑定组件配置,不同组件配置不一样")
private T bindConfig; //绑定组件配置
private Integer exceptionOut; // 异常是否抛出,中断主要流程
private String fallbackMsg; // 异常时兜底内容
}

View File

@@ -0,0 +1,77 @@
package com.xspaceagi.agent.core.adapter.dto;
import com.xspaceagi.agent.core.adapter.repository.entity.AgentConfig;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AgentConfigUpdateDto implements Serializable {
@Schema(description = "智能体ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "id is required")
private Long id; // 智能体ID
@Schema(description = "Agent名称")
private String name; // Agent名称
@Schema(description = "Agent描述")
private String description; // Agent描述
@Schema(description = "图标地址")
private String icon; // 图标地址
@Schema(description = "系统提示词")
private String systemPrompt; // 系统提示词
@Schema(description = "用户消息提示词")
private String userPrompt; // 用户消息提示词,{{AGENT_USER_MSG}}引用用户消息
@Schema(description = "是否开启问题建议")
private AgentConfig.OpenStatus openSuggest; // 是否开启问题建议
@Schema(description = "问题建议提示词")
private String suggestPrompt; // 用户问题建议
@Schema(description = "首次打开聊天框自动回复消息")
private String openingChatMsg; // 首次打开聊天框自动回复消息
@Schema(description = "首次打开引导问题(弃用)")
private List<String> openingGuidQuestions; // 开场引导问题
@Schema(description = "引导问题")
private List<GuidQuestionDto> guidQuestionDtos;
@Schema(description = "是否开启长期记忆")
private AgentConfig.OpenStatus openLongMemory; // 是否开启长期记忆
@Schema(description = "是否开启定时任务")
private AgentConfig.OpenStatus openScheduledTask;
@Schema(description = "是否隐藏远程桌面1 隐藏0 不隐藏")
private Integer hideDesktop;
@Schema(description = "是否允许用户在对话框中选择其他模型, 1 允许,其他不允许")
private Integer allowOtherModel;
@Schema(description = "是否允许用户在对话框中@技能, 1 允许,其他不允许")
private Integer allowAtSkill;
@Schema(description = "是否允许用户在对话框中选择自己的电脑, 1 允许,其他不允许")
private Integer allowPrivateSandbox;
@Schema(description = "是否默认展开扩展页面区域, 1 展开0 不展开")
private Integer expandPageArea;
@Schema(description = "是否隐藏聊天区域1 隐藏0 不隐藏")
private Integer hideChatArea;
}

View File

@@ -0,0 +1,133 @@
package com.xspaceagi.agent.core.adapter.dto;
import com.xspaceagi.agent.core.adapter.dto.config.AgentConfigDto;
import com.xspaceagi.agent.core.adapter.dto.config.Arg;
import com.xspaceagi.agent.core.adapter.dto.config.bind.EventBindConfigDto;
import com.xspaceagi.agent.core.adapter.repository.entity.AgentConfig;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
@Data
public class AgentDetailDto implements Serializable {
private Long spaceId;
private Long creatorId;
@Schema(description = "智能体ID")
private Long agentId;
@Schema(description = "agent唯一标识")
private String uid; // agent唯一标识
@Schema(description = "智能体类型ChatBot 对话智能体PageApp 网页应用智能体")
private String type;
@Schema(description = "智能体名称")
private String name;
@Schema(description = "智能体介绍信息")
private String description;
@Schema(description = "图标")
private String icon;
@Schema(description = "发布备注信息")
private String remark;
@Schema(description = "智能体发布时间")
private Date publishDate;
@Schema(description = "统计信息")
private StatisticsDto statistics;
@Schema(description = "发布者信息")
private PublishUserDto publishUser;
@Schema(description = "当前登录用户是否收藏")
private boolean isCollect;
@Schema(description = "智能体分类名称")
private String category;
@Schema(description = "是否开启问题建议")
private AgentConfig.OpenStatus openSuggest;
@Schema(description = "开场白文案")
private String openingChatMsg;
@Schema(description = "引导问题(弃用)")
private List<String> openingGuidQuestions;
@Schema(description = "引导问题")
private List<GuidQuestionDto> guidQuestionDtos;
@Schema(description = "是否开启定时任务")
private AgentConfig.OpenStatus openScheduledTask;
@Schema(description = "用户手动填写的变量参数")
private List<Arg> variables;
@Schema(description = "分享链接")
private String shareLink;
@Schema(description = "可手动选择的组件列表")
private List<AgentManualComponentDto> manualComponents;
@Schema(description = "是否允许复制, 1 允许")
private Integer allowCopy;
@Schema(description = "会话ID")
private Long conversationId;
@Schema(description = "沙盒ID")
private Long sandboxId;
@Schema(description = "是否拥有权限")
private boolean hasPermission;
@Schema(description = "是否默认展开扩展页面区域, 1 展开0 不展开")
private Integer expandPageArea;
@Schema(description = "是否隐藏聊天区域1 隐藏0 不隐藏")
private Integer hideChatArea;
@Schema(description = "是否隐藏远程桌面, 1 隐藏0 不隐藏")
private Integer hideDesktop;
@Schema(description = "是否允许用户在对话框中选择其他模型, 1 允许,其他不允许")
private Integer allowOtherModel;
@Schema(description = "是否允许用户在对话框中@技能, 1 允许,其他不允许")
private Integer allowAtSkill;
@Schema(description = "是否允许用户在对话框中选择自己的电脑, 1 允许,其他不允许")
private Integer allowPrivateSandbox;
@Schema(description = "扩展页面首页")
private String pageHomeIndex;
@Schema(description = "扩展页面菜单")
private List<AgentConfigDto.CustomPageMenu> customPageMenus;
@Schema(description = "事件绑定配置")
private EventBindConfigDto eventBindConfig;
@Schema(description = "是否需要付费")
private boolean paymentRequired;
@Schema(description = "可试用次数0=不支持试用")
private Integer trialCount;
@Schema(description = "已调用的试用次数")
private Integer calledTrialCount;
@Schema(description = "是否已订阅,对智能体和技能有效")
private boolean subscribed;
@Schema(description = "超出调用限制提示")
private boolean overCallLimit;
}

View File

@@ -0,0 +1,35 @@
package com.xspaceagi.agent.core.adapter.dto;
import com.xspaceagi.agent.core.adapter.repository.entity.AgentComponentConfig;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AgentManualComponentDto {
@Schema(description = "组件ID")
private Long id; // 组件配置ID
@Schema(description = "组件名称")
private String name;
@Schema(description = "组件图标")
private String icon;
@Schema(description = "组件描述")
private String description;
@Schema(description = "组件类型")
private AgentComponentConfig.Type type; // 组件类型可选值Plugin, Workflow, Trigger, Knowledge, Variable, Database
@Schema(description = "是否默认选中0-否1-是")
private Integer defaultSelected;
}

View File

@@ -0,0 +1,36 @@
package com.xspaceagi.agent.core.adapter.dto;
import lombok.Data;
import java.io.Serializable;
@Data
public class AgentOutputDto implements Serializable {
private String requestId;
private EventTypeEnum eventType;
private String error;
private Object data;
private boolean completed;
public enum EventTypeEnum {
// 执行过程
PROCESSING,
// 过程消息输出
PROCESSING_MESSAGE,
// 输出消息
MESSAGE,
// 最终统计等消息
FINAL_RESULT,
// 心跳
HEART_BEAT,
// 异常信息
ERROR
}
}

View File

@@ -0,0 +1,39 @@
package com.xspaceagi.agent.core.adapter.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AgentTempChatDto {
@Schema(description = "链接ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long id;
private Long userId;
@Schema(description = "智能体ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long agentId;
@Schema(description = "链接Key", hidden = true)
private String chatKey;
@Schema(description = "链接地址")
private String chatUrl;
@Schema(description = "二维码地址")
private String qrCodeUrl;
@Schema(description = "是否需要登录")
private Integer requireLogin;
@Schema(description = "链接过期时间")
private Date expire;
}

View File

@@ -0,0 +1,17 @@
package com.xspaceagi.agent.core.adapter.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class AgentUserDto {
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "用户昵称")
private String nickName;
@Schema(description = "用户头像")
private String avatar;
}

View File

@@ -0,0 +1,18 @@
package com.xspaceagi.agent.core.adapter.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.Map;
@Data
public class AnalysisHttpPluginOutputDto implements Serializable {
@Schema(description = "插件ID")
@NotNull(message = "Plugin ID is required")
private Long pluginId;
private Map<String, Object> params;
}

View File

@@ -0,0 +1,20 @@
package com.xspaceagi.agent.core.adapter.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
public class AttachmentDto implements Serializable {
private String fileKey;
@Schema(description = "文件URL", requiredMode = Schema.RequiredMode.REQUIRED)
private String fileUrl;
private String fileName;
@Schema(description = "文件类型", requiredMode = Schema.RequiredMode.REQUIRED)
private String mimeType;
}

View File

@@ -0,0 +1,16 @@
package com.xspaceagi.agent.core.adapter.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
public class CardArgsBindConfigDto {
@Schema(description = "卡片参数名称key")
private String key;
@Schema(description = "卡片参数引用信息,例如插件的出参 data.xxx")
private String bindValue;
}

View File

@@ -0,0 +1,21 @@
package com.xspaceagi.agent.core.adapter.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class CardBindDto implements Serializable {
@Schema(description = "卡片ID")
private Long cardId;
@Schema(description = "卡片唯一标识")
private String cardKey;
@Schema(description = "卡片参数绑定信息")
private List<CardArgsBindConfigDto> cardArgsBindConfigs;
}

View File

@@ -0,0 +1,34 @@
package com.xspaceagi.agent.core.adapter.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
public class CardDto {
@Schema(description = "卡片ID")
private Long id; // 卡片ID
@Schema(description = "卡片唯一标识")
private String cardKey; // 卡片唯一标识,与前端组件做关联
@Schema(description = "卡片名称")
private String name; // 卡片名称
@Schema(description = "卡片示例图片地址")
private String imageUrl; // 卡片示例图片地址
@Schema(description = "卡片参数")
private List<CardArgsDto> argList; // 卡片参数
@Data
public static class CardArgsDto {
@Schema(description = "卡片参数名称key")
private String key;
@Schema(description = "卡片参数引用绑定提示信息")
private String placeholder;
}
}

View File

@@ -0,0 +1,86 @@
package com.xspaceagi.agent.core.adapter.dto;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import com.xspaceagi.agent.core.spec.enums.AgentCategoryEnum;
import com.xspaceagi.agent.core.spec.enums.PageAppCategoryEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
import java.util.stream.Collectors;
@Data
public class CategoryDto implements Serializable {
@Schema(description = "类别名称")
@JsonPropertyDescription("类别名称")
private String name;
@Schema(description = "类别描述")
@JsonPropertyDescription("类别描述")
private String description;
public static List<CategoryDto> convertAgentCategoryList() {
AgentCategoryEnum[] agentCategoryEnums = AgentCategoryEnum.values();
//agentCategoryEnums转List<AgentCategoryDto>
return List.of(agentCategoryEnums).stream().map(agentCategoryEnum -> {
CategoryDto agentCategoryDto = new CategoryDto();
agentCategoryDto.setName(agentCategoryEnum.getName());
agentCategoryDto.setDescription(agentCategoryEnum.getDesc());
return agentCategoryDto;
}).collect(Collectors.toList());
}
public static List<CategoryDto> convertPageAppCategoryList() {
PageAppCategoryEnum[] agentCategoryEnums = PageAppCategoryEnum.values();
//agentCategoryEnums转List<AgentCategoryDto>
return List.of(agentCategoryEnums).stream().map(agentCategoryEnum -> {
CategoryDto agentCategoryDto = new CategoryDto();
agentCategoryDto.setName(agentCategoryEnum.getName());
agentCategoryDto.setDescription(agentCategoryEnum.getDesc());
return agentCategoryDto;
}).collect(Collectors.toList());
}
public static List<CategoryDto> convertPluginCategoryList() {
PluginCategoryEnum[] pluginCategoryEnums = PluginCategoryEnum.values();
return List.of(pluginCategoryEnums).stream().map(pluginCategoryEnum -> {
CategoryDto categoryDto = new CategoryDto();
categoryDto.setName(pluginCategoryEnum.getName());
categoryDto.setDescription(pluginCategoryEnum.getDesc());
return categoryDto;
}).collect(Collectors.toList());
}
public enum PluginCategoryEnum {
//智能硬件
Hardware("Hardware", "智能硬件"),
NewsReading("NewsReading", "新闻阅读"),
Life("Life", "便利生活"),
Image("Image", "图像"),
Utility("Utility", "实用工具"),
WebSearch("WebSearch", "网页搜索"),
ScienceAndEducation("ScienceAndEducation", "科学与教育"),
Social("Social", "社交"),
GameAndEntertainment("GameAndEntertainment", "游戏与娱乐"),
FinanceAndCommerce("FinanceAndCommerce", "金融与商业"),
Video("Video", "音视频"),
Other("Other", "其他");
private String name;
private String desc;
PluginCategoryEnum(String name, String desc) {
this.name = name;
this.desc = desc;
}
public String getName() {
return name;
}
public String getDesc() {
return desc;
}
}
}

View File

@@ -0,0 +1,89 @@
package com.xspaceagi.agent.core.adapter.dto;
import com.xspaceagi.agent.core.spec.enums.MessageTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.MessageType;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ChatMessageDto implements Message {
private Long index;
private Long tenantId;
@Schema(description = "消息发送方类型, User、Agent")
private SenderType senderType;
@Schema(description = "消息发送方ID")
private String senderId;
@Schema(description = "关联用户ID")
private Long userId;
@Schema(description = "关联的agentID")
private Long agentId;
@Schema(description = "消息ID")
private String id;
@Schema(description = "assistant 模型回复user 用户消息")
private Role role;
private MessageTypeEnum type;
@Schema(description = "消息内容")
private String text;
@Schema(description = "消息时间")
private Date time;
@Schema(description = "消息附件")
private List<AttachmentDto> attachments;
@Schema(description = "思考内容")
private String think;
@Schema(description = "引用消息内容")
private String quotedText;
private boolean finished;
private String finishReason;
@Schema(description = "执行过程输出数据")
private List<Object> componentExecutedList;
@Override
public Map<String, Object> getMetadata() {
return null;
}
@Override
public MessageType getMessageType() {
return MessageType.valueOf(role.name());
}
public enum Role {
USER,
ASSISTANT,
SYSTEM,
FUNCTION,
}
public enum SenderType {
USER,
AGENT
}
}

View File

@@ -0,0 +1,20 @@
package com.xspaceagi.agent.core.adapter.dto;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class CodeCheckResultDto {
@JsonPropertyDescription("是否通过检测只要没有违反Goals中列出的范围就通过并返回true")
private Boolean pass;
@JsonPropertyDescription("通过或不通过的原因")
private String reason;
}

View File

@@ -0,0 +1,57 @@
package com.xspaceagi.agent.core.adapter.dto;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
import com.xspaceagi.agent.core.adapter.repository.entity.Published;
import com.xspaceagi.agent.core.spec.enums.ComponentTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class ComponentDto implements Serializable {
@Schema(description = "组件ID")
private Long id;
@Schema(description = "空间ID")
private Long spaceId;
@Schema(description = "组件类型")
private ComponentTypeEnum type;
@Schema(description = "组件名称")
private String name;
@Schema(description = "组件描述")
private String description;
@Schema(description = "图标地址")
private String icon;
@Schema(description = "发布状态,工作流、插件有效")
private Published.PublishStatus publishStatus;
@Schema(description = "最后编辑时间")
private Date modified;
@Schema(description = "创建时间")
private Date created;
private Long creatorId;
@Schema(description = "创建者信息")
private CreatorDto creator;
@Schema(description = "扩展字段")
private Object ext;
@Schema(description = "权限列表")
private List<String> permissions;
@Schema(description = "是否启用")
private Integer enabled;
}

View File

@@ -0,0 +1,30 @@
package com.xspaceagi.agent.core.adapter.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class ComputerFileInfo {
@Schema(description = "文件名称")
private String name;
@Schema(description = "是否为二进制文件")
private boolean binary;
@Schema(description = "文件大小是否超过限制")
private boolean sizeExceeded;
@Schema(description = "文件内容")
private String contents;
@Schema(description = "重命名前的文件名")
private String renameFrom;
//create | delete | rename | modify
@Schema(description = "操作类型")
private String operation;
@Schema(description = "是否目录")
private Boolean isDir = false;
}

View File

@@ -0,0 +1,37 @@
package com.xspaceagi.agent.core.adapter.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
/**
* 容器操作结果 DTO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ComputerPodResultDto {
/**
* 状态码0000 表示成功
*/
private String code;
/**
* 消息
*/
private String message;
/**
* 返回数据(包含 container_info 等信息)
*/
private Map<String, Object> data;
/**
* 跟踪唯一标识
*/
private String tid;
}

View File

@@ -0,0 +1,54 @@
package com.xspaceagi.agent.core.adapter.dto;
import com.baomidou.mybatisplus.annotation.TableName;
import com.xspaceagi.agent.core.adapter.repository.entity.ConfigHistory;
import com.xspaceagi.agent.core.adapter.repository.entity.Published;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@TableName("agent_config_history")
public class ConfigHistoryDto {
private Long id;
private Published.TargetType targetType;
private Long targetId;
@Schema(description = "操作类型,Add 新增, Edit 编辑, Publish 发布")
private ConfigHistory.Type type;
@Schema(description = "当时的配置信息")
private Object config;
@Schema(description = "操作描述")
private String description;
@Schema(description = "操作人")
private OpUser opUser;
private Date modified;
@Schema(description = "创建时间")
private Date created;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class OpUser {
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "用户名")
private String userName;
@Schema(description = "昵称")
private String nickName;
@Schema(description = "头像")
private String avatar;
}
}

View File

@@ -0,0 +1,77 @@
package com.xspaceagi.agent.core.adapter.dto;
import com.xspaceagi.agent.core.adapter.repository.entity.Conversation;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ConversationDto {
@Schema(description = "会话ID")
private Long id;
private Long tenantId;
private Long userId;
@Schema(description = "会话UUID")
private String uid;
@Schema(description = "智能体ID")
private Long agentId;
@Schema(description = "会话主题")
private String topic;
@Schema(description = "会话摘要,当开启长期记忆时,会对每次会话进行总结")
private String summary;
@Schema(description = "用户填写的会话变量内容")
private Map<String, Object> variables;
private Date modified;
private Date created;
@Schema(description = "Agent信息已发布过的agent才有此信息")
private AgentDetailDto agent;
@Schema(description = "会话消息列表,会话列表查询时不会返回该字段值")
private List<ChatMessageDto> messageList;
private Conversation.ConversationType type;
private String taskId;
@Schema(description = "任务状态,只针对 EXECUTING执行中 做展示")
private Conversation.ConversationTaskStatus taskStatus;
private String taskCron;
private String taskCronDesc;
private Integer devMode;
private Integer topicUpdated;
private String sandboxServerId;
private String sandboxSessionId;
@Schema(description = "已分享的URI地址比对上了则不需要认证")
private List<String> sharedUris;
public Integer getDevMode() {
return devMode == null ? 0 : devMode;
}
}

View File

@@ -0,0 +1,26 @@
package com.xspaceagi.agent.core.adapter.dto;
import com.xspaceagi.agent.core.adapter.repository.entity.Conversation;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
public class ConversationQueryDto implements Serializable {
@Schema(description = "智能体ID可选")
private Long agentId;
@Schema(description = "会话ID可选")
private Long lastId;
@Schema(description = "返回会话数量,可选")
private Integer limit;
@Schema(description = "定时会话状态,可选")
private Conversation.ConversationTaskStatus taskStatus;
@Schema(description = "会话主题,可选")
private String topic;
}

View File

@@ -0,0 +1,21 @@
package com.xspaceagi.agent.core.adapter.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serializable;
@Data
public class ConversationUpdateDto implements Serializable {
@Schema(description = "会话ID")
private Long id;
@Schema(description = "用户第一条消息")
@NotNull(message = "User first message is required")
private String firstMessage;
@Schema(description = "会话主题可以不传firstMessage与topic二选一")
private String topic;
}

View File

@@ -0,0 +1,34 @@
package com.xspaceagi.agent.core.adapter.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "创建工作空间请求DTO")
public class CreateWorkspaceDto implements Serializable {
@Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long userId;
@NotNull
@JsonProperty("cId")
@Schema(description = "会话ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long cId;
@Schema(description = "技能ID列表")
private List<Long> skillIds;
@Schema(description = "子代理列表")
private List<SubagentDto> subagents;
}

View File

@@ -0,0 +1,26 @@
package com.xspaceagi.agent.core.adapter.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class CreatorDto {
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "用户名")
private String userName;
@Schema(description = "昵称")
private String nickName;
@Schema(description = "头像")
private String avatar;
}

View File

@@ -0,0 +1,12 @@
package com.xspaceagi.agent.core.adapter.dto;
import lombok.Data;
@Data
public class ExportTemplateDto {
private String type;
private String name;
private Object templateConfig;
private Long spaceId;
}

View File

@@ -0,0 +1,48 @@
package com.xspaceagi.agent.core.adapter.dto;
import com.xspaceagi.agent.core.adapter.dto.config.Arg;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
public class GuidQuestionDto {
@Schema(description = "问题类型")
private GuidQuestionType type;
@Schema(description = "问题信息")
private String info;
@Schema(description = "图标")
private String icon;
@Schema(description = "链接地址type类型为Link时有效")
private String url;
@Schema(description = "页面IDtype类型为Page时有效")
private Long pageId;
@Schema(description = "页面基础路径type类型为Page时有效")
private String basePath;
@Schema(description = "页面路径type类型为Page时有效")
private String pageUri;
@Schema(description = "页面地址配置更新时不需要传递type类型为Page时有效完整的页面地址前端需要使用 BASE_URL+pageUrl")
private String pageUrl;
@Schema(description = "参数配置")
private List<Arg> args;
@Schema(description = "参数值配置更新时不需要传递用户点击跳转时直接使用type类型为Page时有效")
private Map<String, Object> params;
public enum GuidQuestionType {
Question,
Page,
Link
}
}

View File

@@ -0,0 +1,13 @@
package com.xspaceagi.agent.core.adapter.dto;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import lombok.Data;
import java.util.List;
@Data
public class HostExtractDto {
@JsonPropertyDescription("域名列表")
private List<String> domains;
}

View File

@@ -0,0 +1,15 @@
package com.xspaceagi.agent.core.adapter.dto;
import com.xspaceagi.agent.core.adapter.repository.entity.Published;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class ImportTemplateResultDto {
@Schema(description = "导入类型")
private Published.TargetType targetType;
@Schema(description = "导入生成ID")
private Long targetId;
}

View File

@@ -0,0 +1,21 @@
package com.xspaceagi.agent.core.adapter.dto;
import com.xspaceagi.agent.core.adapter.dto.config.bind.KnowledgeBaseBindConfigDto;
import com.xspaceagi.agent.core.spec.enums.SearchStrategyEnum;
import lombok.Data;
import java.io.Serializable;
@Data
public class KnowledgeSearchConfigDto implements Serializable {
private SearchStrategyEnum searchStrategy;
private Integer maxRecallCount;
private Double matchingDegree;
private KnowledgeBaseBindConfigDto.NoneRecallReplyTypeEnum noneRecallReplyType;
private String noneRecallReply;
}

View File

@@ -0,0 +1,15 @@
package com.xspaceagi.agent.core.adapter.dto;
import lombok.Data;
import java.io.Serializable;
@Data
public class MemoryDto implements Serializable {
private String topic;
private String content;
private String date;
}

View File

@@ -0,0 +1,79 @@
package com.xspaceagi.agent.core.adapter.dto;
import com.xspaceagi.agent.core.adapter.dto.config.ModelConfigDto;
import com.xspaceagi.agent.core.adapter.repository.entity.ModelConfig;
import com.xspaceagi.agent.core.spec.enums.ModelApiProtocolEnum;
import com.xspaceagi.agent.core.spec.enums.ModelCapabilityEnum;
import com.xspaceagi.agent.core.spec.enums.ModelFunctionCallEnum;
import com.xspaceagi.agent.core.spec.enums.ModelTypeEnum;
import com.xspaceagi.agent.core.spec.enums.UsageScenarioEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class ModelConfigAddDto implements Serializable {
@Schema(description = "模型ID可选不传递为新增传递了为更新")
private Long id;
@Schema(description = "空间ID可选在空间中添加模型组件时传递该参数")
private Long spaceId;
@Schema(description = "模型名称", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "Model name is required")
private String name; // 模型名称
@Schema(description = "模型描述")
private String description; // 模型描述
@Schema(description = "模型标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "gpt-3.5-turbo")
@NotNull(message = "Model key is required")
private String model; // 模型标识
@Schema(description = "模型类型可选值Completions, Chat, Edits, Images, Embeddings, Audio, Other", requiredMode = Schema.RequiredMode.REQUIRED)
private ModelTypeEnum type; // 模型类型可选值Completions, Chat, Edits, Images, Embeddings, Audio, Other
@Schema(description = "提供商ID")
private String pid;
@Schema(description = "模型能力类型列表可选值Text-文本生成、Image-图像理解、Audio-语音识别、Video-视频理解、TextEmbedding-文本向量、MultiEmbedding-多模态向量、Reasoning-深度思考")
private List<ModelCapabilityEnum> types;
@Schema(description = "最大输出token数")
private Integer maxTokens; // token上限
@Schema(description = "上下文最大输出token数")
private Integer maxContextTokens; // 上下文token上限
@Schema(description = "模型接口协议可选值OpenAI, Ollama", requiredMode = Schema.RequiredMode.REQUIRED)
private ModelApiProtocolEnum apiProtocol; // 模型接口协议可选值OpenAI, Ollama
@Schema(description = "API列表", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "API list is required")
private List<ModelConfigDto.ApiInfo> apiInfoList; // API列表 [{"url":"","key":"","weight":1}]
@Schema(description = "接口调用策略可选值RoundRobin, WeightedRoundRobin, LeastConnections, WeightedLeastConnections, Random, ResponseTime", requiredMode = Schema.RequiredMode.REQUIRED)
private ModelConfig.ModelStrategyEnum strategy; // 接口调用策略可选值RoundRobin, WeightedRoundRobin, LeastConnections, WeightedLeastConnections, Random, ResponseTime
@Schema(description = "向量维度")
private Integer dimension; // 向量维度
@Schema(description = "函数调用支持可选范围CallSupported 支持普通函数调用; StreamCallSupported 支持流式函数调用Unsupported 不支持函数调用")
private ModelFunctionCallEnum functionCall;
@Schema(description = "是否为推理模型0 否1 是")
private Integer isReasonModel;
@Schema(description = "是否启用,1-启用0-禁用")
private Integer enabled;
@Schema(description = "访问控制可选值0-不管控1-管控")
private Integer accessControl;
@Schema(description = "可使用的业务场景")
private List<UsageScenarioEnum> usageScenarios;
}

View File

@@ -0,0 +1,40 @@
package com.xspaceagi.agent.core.adapter.dto;
import com.xspaceagi.agent.core.spec.enums.ModelApiProtocolEnum;
import com.xspaceagi.agent.core.spec.enums.ModelTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class ModelDto implements Serializable {
@Schema(description = "模型ID")
private Long modelId;
@Schema(description = "模型标识")
private String model;
@Schema(description = "模型接口地址")
private String host;
@Schema(description = "模型APIKEY数组")
private List<String> apiKeys;
@Schema(description = "模型备注(名称)")
private String remark;
@Schema(description = "最大支持token数")
private Integer maxTokens;
@Schema(description = "模型类型")
private ModelTypeEnum type;
@Schema(description = "模型调用接口协议")
private ModelApiProtocolEnum apiProtocol;
@Schema(description = "向量维度,模型类型为 Embeddings 时有效")
private Integer dimension;
}

View File

@@ -0,0 +1,26 @@
package com.xspaceagi.agent.core.adapter.dto;
import com.xspaceagi.agent.core.adapter.repository.entity.ModelConfig;
import com.xspaceagi.agent.core.spec.enums.ModelApiProtocolEnum;
import com.xspaceagi.agent.core.spec.enums.ModelTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class ModelQueryDto {
@Schema(description = "模型类型")
private ModelTypeEnum modelType;
@Schema(description = "接口协议类型")
private ModelApiProtocolEnum apiProtocol;
@Schema(description = "模型范围,不传则返回所有有权限的模型")
private ModelConfig.ModelScopeEnum scope;
@Schema(description = "空间ID可选传递后会返回当前空间管理的模型")
private Long spaceId;
@Schema(description = "是否启用")
private Integer enabled;
}

View File

@@ -0,0 +1,16 @@
package com.xspaceagi.agent.core.adapter.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
public class OffShelfDto implements Serializable {
@Schema(description = "发布ID", hidden = true)
private Long publishId;
@Schema(description = "下架原因")
private String reason;
}

View File

@@ -0,0 +1,36 @@
package com.xspaceagi.agent.core.adapter.dto;
import com.xspaceagi.agent.core.spec.enums.CodeLanguageEnum;
import com.xspaceagi.agent.core.spec.enums.PluginTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serializable;
@Data
public class PluginAddDto implements Serializable {
@Schema(description = "空间ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "Space ID is required")
private Long spaceId;
@Schema(description = "创建人ID")
private Long creatorId;
@Schema(description = "插件名称", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "Plugin name is required")
private String name;
@Schema(description = "插件描述")
private String description;
@Schema(description = "插件图标")
private String icon;
@Schema(description = "插件类型")
private PluginTypeEnum type;
@Schema(description = "插件代码语言")
private CodeLanguageEnum codeLang;
}

View File

@@ -0,0 +1,24 @@
package com.xspaceagi.agent.core.adapter.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.Map;
@Data
public class PluginExecuteRequestDto implements Serializable {
@Schema(description = "请求ID")
private String requestId;
private Long agentId;
@Schema(description = "插件ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long pluginId;
@Schema(description = "插件参数")
private Map<String, Object> params;
private boolean test;
}

View File

@@ -0,0 +1,35 @@
package com.xspaceagi.agent.core.adapter.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PluginExecuteResultDto implements Serializable {
@Schema(description = "执行结果状态")
private boolean success;
@Schema(description = "执行结果")
private Object result;
@Schema(description = "执行日志")
private List<String> logs;
@Schema(description = "执行错误信息")
private String error;
@Schema(description = "请求ID")
private String requestId;
@Schema(description = "请求耗时")
private Long costTime;
}

View File

@@ -0,0 +1,27 @@
package com.xspaceagi.agent.core.adapter.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serializable;
@Data
public class PluginUpdateDto<T> implements Serializable {
@Schema(description = "插件ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "Plugin ID is required")
private Long id;
@Schema(description = "插件名称")
private String name;
@Schema(description = "插件描述")
private String description;
@Schema(description = "插件图标")
private String icon;
@Schema(description = "插件配置")
private T config;
}

View File

@@ -0,0 +1,28 @@
package com.xspaceagi.agent.core.adapter.dto;
import com.xspaceagi.agent.core.adapter.dto.config.Arg;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PreviousDto implements Serializable {
@Schema(description = "所有上级节点列表")
private List<PreviousNodeDto> previousNodes;
@Schema(description = "循环节点内部节点列表")
private List<PreviousNodeDto> innerPreviousNodes;
@Schema(description = "所有上级节点的输出参数Map")
private Map<String, Arg> argMap;
}

View File

@@ -0,0 +1,40 @@
package com.xspaceagi.agent.core.adapter.dto;
import com.xspaceagi.agent.core.adapter.dto.config.Arg;
import com.xspaceagi.agent.core.adapter.repository.entity.WorkflowNodeConfig;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class PreviousNodeDto implements Serializable {
@Schema(description = "节点ID")
private Long id;
@Schema(description = "节点名称")
private String name;
@Schema(description = "节点图标地址")
private String icon;
@Schema(description = "节点类型")
private WorkflowNodeConfig.NodeType type;
@Schema(description = "节点输出参数")
private List<Arg> outputArgs;
private Long loopNodeId;
private Integer sort;
public boolean equals(Object obj) {
if (obj instanceof PreviousNodeDto) {
PreviousNodeDto node = (PreviousNodeDto) obj;
return this.id.equals(node.id);
}
return false;
}
}

View File

@@ -0,0 +1,73 @@
package com.xspaceagi.agent.core.adapter.dto;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
import com.xspaceagi.agent.core.adapter.repository.entity.Published;
import com.xspaceagi.agent.core.spec.enums.PluginTypeEnum;
import com.xspaceagi.system.application.dto.UserDto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class PublishApplyDto implements Serializable {
private Long id;
@Schema(description = "所属空间ID")
private Long spaceId;
@Schema(description = "目标对象类型")
private Published.TargetType targetType;
@Schema(description = "子类型")
private Published.TargetSubType targetSubType;
@Schema(description = "目标对象ID智能体ID、插件ID、工作流ID")
private Long targetId;
@Schema(description = "名称")
private String name;
@Schema(description = "描述")
private String description;
@Schema(description = "图标")
private String icon;
@Schema(description = "扩展字段")
private Object ext;
//该字段已没有实际作用,后续弃用
@Schema(description = "发布渠道")
private List<Published.PublishChannel> channels;
private Published.PublishScope scope;
@Schema(description = "发布状态")
private Published.PublishStatus publishStatus;
@Schema(description = "目标配置", hidden = true)
private Object targetConfig;
@Schema(description = "发布记录")
private String remark;
@Schema(description = "发布修改时间")
private Date modified;
@Schema(description = "发布创建时间")
private Date created;
@Schema(description = "发布申请用户")
private UserDto applyUser;
private PluginTypeEnum pluginType;
private Integer allowCopy;
private Integer onlyTemplate;
private String category;
}

View File

@@ -0,0 +1,25 @@
package com.xspaceagi.agent.core.adapter.dto;
import com.xspaceagi.agent.core.adapter.repository.entity.Published;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serializable;
@Data
public class PublishApplyQueryDto implements Serializable {
@Schema(description = "发布类型")
@NotNull(message = "targetType is required; use Agent for agents and apps, or omit")
private Published.TargetType targetType;
@Schema(description = "发布子类型类型PageApp 和 Agent")
private Published.TargetSubType targetSubType;
@Schema(description = "发布状态")
private Published.PublishStatus publishStatus;
@Schema(description = "关键字搜索")
private String kw;
}

View File

@@ -0,0 +1,43 @@
package com.xspaceagi.agent.core.adapter.dto;
import com.xspaceagi.agent.core.adapter.repository.entity.Published;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class PublishApplySubmitDto implements Serializable {
@Schema(description = "发布目标类型目前可发布的类型有Agent,Plugin,Workflow")
private Published.TargetType targetType;
@Schema(description = "发布目标ID例如智能体ID工作流ID插件ID")
private Long targetId;
@Schema(description = "发布记录")
private String remark;
@Schema(description = "发布分类")
private String category;
@Schema(description = "发布项")
private List<PublishItem> items;
@Data
public static class PublishItem {
@Schema(description = "发布范围可选范围Space 空间,Tenant 系统")
private Published.PublishScope scope;
@Schema(description = "发布空间ID")
private Long spaceId;
@Schema(description = "是否允许复制,0不允许1允许")
private Integer allowCopy;
@Schema(description = "仅展示模板, 0 否1 是")
private Integer onlyTemplate;
}
}

View File

@@ -0,0 +1,14 @@
package com.xspaceagi.agent.core.adapter.dto;
import com.xspaceagi.agent.core.adapter.repository.entity.Published;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
public class PublishChannelDto implements Serializable {
@Schema(description = "发布渠道")
private Published.PublishChannel channel;
}

View File

@@ -0,0 +1,16 @@
package com.xspaceagi.agent.core.adapter.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
public class PublishRejectDto implements Serializable {
@Schema(description = "发布申请ID", hidden = true)
private Long applyId;
@Schema(description = "拒绝原因")
private String reason;
}

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