chore: 工作台产品化进行中的改动

把工作区里其余在制品一并入库,主要是工作台产品化的推进:

  后端:新增 capability_definition / project / my_app_center / office_skill
        接口与 action_definition / skill_definition / project / user_app_center
        模型,config 加路由健康上报。
  前端:新增 frontend/src/skills(Office 技能与 workbuddy 复刻)、
        项目管理、应用中心、能力目录页,以及配套 api / store / config;
        聊天侧新增 SpecialistChip / SpecialistPanel / SkillStrip / AppChatRail
        等组件。
  清理:移除旧 views/tools 下的单页工具(已并入工作台)、_frozen 冻结组件、
        cmd/inspect_oa_debug 调试入口,以及两份调试笔记。
  其它:文档与启动脚本同步。

(这批改动与上一提交的 SY23 工作并行进行,此前已在同一工作区内交织。)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
eaiadmin
2026-09-17 21:32:35 +08:00
co-authored by Claude Code
parent 8b136d4a10
commit 16d63de4e1
180 changed files with 22283 additions and 13850 deletions
+143 -127
View File
@@ -1,9 +1,9 @@
# AR01 — 后端架构设计
> **版本:V1.1 | 框架:FastAPI + SQLAlchemy + MySQL 8.0**
> **参考:pj006-zhilianyuan2 的 BE01_backend_arch + main.py 装配模式**
> **版本:V2.0 | 框架:Go + Gin + GORM + SQLite(modernc)**
> **参考:eai_agentplatform/backend-go/internal/api/ + internal/model/ + internal/store/**
>
> **⚠️ 本文档为 V1.1 设计期历史快照,不再反映当前实现。** 后端已重写为 **Go + Gin + GORM + MySQL 8.0 + FAISS**,以 `docs/changelog.md`(V1.2)、`docs/db_schema.md`、`docs/deploy.md` 为准;下文 FastAPI/Python 结构与 `backend/` 路径仅作设计参考。
> **当前实现**:Go 单二进制 + systemd + Clonezilla 整盘克隆,`backend-go/internal/` 为标准源码布局。
---
@@ -11,163 +11,179 @@
```
┌─────────────────────────────────────────────┐
│ API 路由层 (routes) │
│ API 路由层 (api/) │
│ auth / company_train / product / course │
│ exam / media / ai_chat / system │
├─────────────────────────────────────────────┤
│ Pydantic 模型层 (schemas) │
│ 请求/响应模型,统一响应格式 Envelope │
│ Gin Context + 请求/响应结构体 │
│ web.OK / web.Fail 统一响应 │
├─────────────────────────────────────────────┤
│ 服务层 (services) │
│ exam_service / media_service / ai_service │
│ 业务逻辑 (api/ 内函数) │
│ exam.go / media.go / ai_chat.go │
├─────────────────────────────────────────────┤
│ SQLAlchemy ORM 模型层 (models) │
│ GORM 模型层 (model/) │
│ User / Product / Course / MediaFile / ... │
├─────────────────────────────────────────────┤
│ 核心层 (core) │
│ config / security / deps │
│ 数据库层 (store/) │
│ db.go: SQLite Init + AutoMigrate │
├─────────────────────────────────────────────┤
│ MySQL 8.0 + data/media │
│ SQLite + data/media │
└─────────────────────────────────────────────┘
```
## 2. 目录结构
```
backend/
├── app/
│ ├── main.py # FastAPI 应用装配 + CORS + 异常处理
│ ├── api/ # 路由层
│ │ ├── auth.py # /api/auth/* — 登录/注册/me
│ │ ├── company_train.py # /api/company-train/*
│ │ ├── product.py # /api/products/*
│ │ ├── sales_train.py # /api/courses/*
│ │ ├── exam.py # /api/exam/* — 题库/组卷/考试/记录
│ │ ├── media.py # /api/media/* — 上传/预览/审批
│ │ ├── ai_chat.py # /api/ai-chat/* — PathCoach SSE
│ │ └── system.py # /api/system/* — 用户/成绩/配置
│ ├── models/ # SQLAlchemy ORM 模型
│ │ ├── user.py
│ │ ├── product.py
│ │ ├── course.py
│ │ ├── media_file.py
│ │ ├── knowledge_chunk.py
│ │ ├── question.py
│ │ ├── exam_paper.py
│ │ └── exam_record.py
│ ├── schemas/ # Pydantic 请求/响应模型
│ ├── services/ # 业务逻辑层
│ │ ├── media_service.py # 上传/转换/提取
│ │ ├── ai_service.py # LLM 调用 + 知识检索
│ │ └── exam_service.py # 题库/组卷/判分/记录
│ ├── core/ # 核心基础设施
│ │ ├── config.py # .env + 系统参数读取
│ │ ├── security.py # JWT 签发/校验 + bcrypt
│ │ └── deps.py # FastAPI Depends(get_db / get_current_user)
│ └── utils/ # 工具函数
├── data/media/ # 文件存储(git忽略)
│ ├── upload/
│ └── _preview_cache/
├── requirements.txt
└── .env
backend-go/
├── cmd/
│ └── server.go # 入口:gin.Engine + systemd 裸进程
├── internal/
│ ├── api/ # 路由层(Gin handlers)
│ │ ├── auth.go # /api/auth/* — 登录/注册/me
│ │ ├── company_train.go # /api/company-train/*
│ │ ├── product.go # /api/products/*
│ │ ├── exam.go # /api/exam/* — 题库/组卷/考试/记录
│ │ ├── media.go # /api/media/* — 上传/预览/审批
│ │ ├── ai_chat.go # /api/ai-chat/* — PathCoach SSE
│ │ ├── system.go # /api/system/* — 用户/成绩/配置
│ │ ├── router.go # RegisterRoutes 总路由
│ │ └── (其余为功能模块)
│ ├── model/ # GORM 模型
│ │ ├── user.go
│ │ ├── product.go
│ │ ├── course.go
│ │ ├── media_file.go
│ │ ├── knowledge_chunk.go
│ │ ├── question.go
│ │ ├── exam_paper.go
│ │ └── exam_record.go
│ ├── store/ # 数据库层
│ │ └── db.go # SQLite Init + AutoMigrate
│ ├── ai/ # AI 客户端与检索
│ │ ├── llm.go # OpenAI 兼容 LLM 调用
│ │ └── retrieve.go # 知识检索
│ ├── config/ # 配置加载
│ └── middleware/ # 中间件(JWT 认证等)
├── knowledge_source/ # 知识源 Markdown(运行时)
├── data/ # 运行时数据(db+media,git 忽略)
└── deploy/ # systemd 单元 + DELIVERY.md
```
## 3. 应用装配模式(main.py)
## 3. 应用装配模式(RegisterRoutes)
参考 zhilianyuan2 的模式,每个模块的 router 独立注册:
入口 `cmd/server.go` 创建 `gin.Engine`,调用 `api.RegisterRoutes` 注册全部路由:
```python
from fastapi import FastAPI
from app.api import auth, company_train, product, sales_train
from app.api import exam, media, ai_chat, system
```go
// backend-go/cmd/server.go
package main
app = FastAPI(title="eai_agentplatform_app", version="1.1.0")
import (
"github.com/gin-gonic/gin"
"eai_agentplatform/backend/internal/api"
"eai_agentplatform/backend/internal/config"
)
# 异常处理器
@app.exception_handler(AppError)
def handle_app_error(request, exc):
return JSONResponse(status_code=exc.status_code, content={...})
func main() {
cfg := config.Load()
r := gin.Default()
# 路由注册
app.include_router(auth.router)
app.include_router(company_train.router)
app.include_router(product.router)
app.include_router(sales_train.router)
app.include_router(exam.router)
app.include_router(media.router)
app.include_router(ai_chat.router)
app.include_router(system.router)
// 公开静态文件(已审批素材)
r.Static("/media", cfg.KBDataDir+"/approved")
// 注册全部路由
api.RegisterRoutes(r, cfg)
r.Run(":8080")
}
```
## 4. 依赖注入模式
路由注册(`api/router.go`):
参考 zhilianyuan2 的 `auth/dependencies.py`:
```go
// api/router.go
func RegisterRoutes(r *gin.Engine, cfg *config.Config) {
// 健康检查
r.GET("/api/health", HealthCheck)
```python
# core/deps.py
async def get_current_user(
credentials: HTTPAuthorizationCredentials | None = Depends(HTTPBearer(auto_error=False)),
db: Session = Depends(get_db),
) -> User:
"""解析 JWT → 校验用户状态 → 返回 User"""
if credentials is None:
raise AuthError("缺少 Authorization Bearer 令牌")
payload = decode_access_token(credentials.credentials, settings)
user = db.query(User).filter(User.username == payload["sub"]).first()
if user is None or user.status != "active":
raise AuthError("用户不存在或已禁用")
return user
// 公开 + 员工
r.GET("/api/company-train", middleware.Auth(cfg), GetCompanyTrain)
r.GET("/api/specialists", middleware.Auth(cfg), ListSpecialists)
def require_admin(user: User = Depends(get_current_user)) -> User:
"""管理员角色守卫"""
if user.role != "admin":
raise ForbiddenError("需要管理员权限")
return user
// 员工端考试
r.GET("/api/exam/list", middleware.Auth(cfg), ExamList)
r.GET("/api/exam/cover", middleware.Auth(cfg), ExamCover)
r.POST("/api/exam/start", middleware.Auth(cfg), ExamStart)
r.POST("/api/exam/submit", middleware.Auth(cfg), ExamSubmit)
// 管理员
admin := r.Group("/api").Use(middleware.Auth(cfg), middleware.RequireAdmin())
{
admin.POST("/products", CreateProduct)
admin.POST("/courses", CreateCourse)
admin.POST("/media/audit/:mediaId", AuditMedia)
admin.POST("/knowledge/scan", KnowledgeScan)
admin.GET("/system/users", ListUsers)
admin.GET("/system/config", GetConfig)
admin.GET("/system/exam-records", ListExamRecords)
}
// AI 对话
r.POST("/api/ai-chat/message", middleware.Auth(cfg), ChatMessage)
r.GET("/api/ai-chat/quick-actions", middleware.Auth(cfg), QuickActions)
}
```
## 4. 中间件模式
```go
// internal/middleware/auth.go
func Auth(cfg *config.Config) gin.HandlerFunc {
return func(c *gin.Context) {
claims, err := auth.ParseToken(
c.GetHeader("Authorization"), cfg.JWTSecret,
)
if err != nil {
c.JSON(401, gin.H{"error": "unauthorized"})
c.Abort()
return
}
c.Set("user", claims)
c.Next()
}
}
func RequireAdmin() gin.HandlerFunc {
return func(c *gin.Context) {
user := c.MustGet("user").(jwt.MapClaims)
if role, ok := user["role"].(string); !ok || role != "admin" {
c.JSON(403, gin.H{"error": "forbidden"})
c.Abort()
return
}
c.Next()
}
}
```
## 5. API 路由前缀
| 路由前缀 | 模块 | 说明 |
|---------|------|------|
| `/api/auth/*` | auth | 登录/注册/当前用户 |
| `/api/company-train/*` | company_train | 公司介绍内容 |
| `/api/products/*` | product | 产品 CRUD + 导入 |
| `/api/courses/*` | sales_train | 课程 CRUD + 绑定产品 |
| `/api/exam/*` | exam | 题库/组卷/考试/记录 |
| `/api/media/*` | media | 上传/预览/审批/状态 |
| `/api/ai-chat/*` | ai_chat | PathCoach 流式对话 |
| `/api/system/*` | system | 用户/成绩/配置 |
| `/api/auth/*` | auth.go | 登录/注册/当前用户 |
| `/api/company-train/*` | company_train.go | 公司介绍内容 |
| `/api/products/*` | product.go | 产品 CRUD + 导入 |
| `/api/courses/*` | courses.go | 课程 CRUD + 绑定产品 |
| `/api/exam/*` | exam.go | 题库/组卷/考试/记录 |
| `/api/media/*` | media.go | 上传/预览/审批/状态 |
| `/api/ai-chat/*` | ai_chat.go | PathCoach 流式对话 |
| `/api/system/*` | system.go | 用户/成绩/配置 |
| `/api/health` | — | 健康检查 |
## 6. 异步任务模式
## 6. 单二进制交付
文档转换管线(审批通过后异步执行):
```bash
CGO_ENABLED=0 go build -o bin/eai_agentplatform-server ./cmd/server
# 输出:statically linked,无运行时依赖
```
```python
# services/media_service.py
import threading
def _async_convert_and_extract(media_file_id: int):
"""审批通过后异步执行:文档转 PDF → 文本提取 → 切片入库"""
with Session() as db:
media = db.query(MediaFile).get(media_file_id)
# 1. 调用 LibreOffice 转 PDF
pdf_path = libreoffice_convert(media.stored_path)
# 2. PyMuPDF 提取文本
text = pymupdf_extract(pdf_path)
# 3. 按段落切片写入 knowledge_chunk
chunks = split_into_chunks(text)
for i, chunk in enumerate(chunks):
db.add(KnowledgeChunk(media_file_id=media.id, ...))
media.extracted = True
db.commit()
def approve_media(media_file_id: int, auditor_id: int):
"""审批通过 → 触发异步转换"""
media.status = "approved"
media.audit_by = auditor_id
media.audit_at = datetime.utcnow()
db.commit()
# 启动异步任务
threading.Thread(target=_async_convert_and_extract, args=(media_file_id,)).start()
```
部署:`systemd` 直接拉起单二进制,`data/` 目录下 SQLite 数据库自动建表。
+8 -1
View File
@@ -2,6 +2,13 @@
> **版本:V1.1 | 左导航 + 中间工作区 + 右 AI 侧栏 | Vue3 + Vite + Element Plus**
> **参考:pj006-zhilianyuan2 frontend-orgadmin 三栏布局模式**
>
> **2026-09-16 口径同步说明:**
> 本文档记录的是 V1 三栏前端架构基线。
> 当前正式产品导航与对象化工作台口径,请以 `docs/01_System_Overall/SY22_Role_Skill_App_Unified_Task_Architecture.md` 为准:
> `新建任务 / 项目 / 专员·技能·APP·连接器 / 长程APP / 知识库 / 后台管理 / 我的`
> 另外,当前前端命名已明确拆分为:
> `pageRoute` = 普通页面导航,`objectEntryRoute` = 业务对象入口,`ai_route_*` = AI 模型路由。
---
@@ -172,4 +179,4 @@ export const useAiChatStore = defineStore('aiChat', {
clearMessages() { this.messages = [] },
}
})
```
```
+7 -17
View File
@@ -1,9 +1,7 @@
# AR03 — 数据库架构设计
> **版本:V1.1 | 引擎:MySQL 8.0 | ORM:SQLAlchemy(现为 GORM)**
> **版本:V2.0 | 引擎:SQLite(modernc 纯Go驱动)**
> **完整建表 SQL 请见 docs/db_schema.md**
>
> **⚠️ 本文档为 V1.1 设计期历史快照。** 当前实现已切换为 **MySQL 8.0 + FAISS 向量检索**(ORM 由 SQLAlchemy 改为 GORM),并新增岗位/积分/证书/部门/消息等表,以 `docs/db_schema.md` 与 `docs/changelog.md`(V1.4–V1.7)为准。
---
@@ -41,7 +39,6 @@ exam_paper ──< exam_record (paper_id)
| `course` | `category`, `status`, `related_product_id` | BTREE | 同上 |
| `media_file` | `status`, `submitter_id`, `(bind_type, bind_id)`, `extracted` | BTREE | 审批列表、绑定查询、提取状态 |
| `knowledge_chunk` | `media_file_id` | BTREE | 关联查询 |
| `knowledge_chunk` | `content` | **FULLTEXT** | AI 知识检索(MySQL 全文索引) |
| `question` | `domain`, `course_id`, `status` | BTREE | 知识域筛选、课程筛选 |
| `exam_paper` | `type`, `status` | BTREE | 考试类型筛选 |
| `exam_record` | `user_id`, `paper_id`, `passed`, `submitted_at` | BTREE | 用户查记录、管理员查全部 |
@@ -49,30 +46,23 @@ exam_paper ──< exam_record (paper_id)
## 4. AI 知识检索说明
V1.1 使用 **MySQL 全文索引**(FULLTEXT);现已升级为 **MySQL 全文索引(关键词)+ FAISS 向量检索(语义)双路混合召回**:
Go 内 brute-force 余弦向量检索(Ollama bge-m3 embedding)+ 关键词兜底:
```sql
-- knowledge_chunk 表已建全文索引(关键词召回)
FULLTEXT INDEX ft_kc_content (content)
-- 检索查询
SELECT * FROM knowledge_chunk
WHERE MATCH(content) AGAINST(:keywords IN NATURAL LANGUAGE MODE)
LIMIT 10
-- SQLite 原生,无全文索引,向量检索在 Go 层实现
-- knowledge_chunk.content 字段存储文本,向量计算由 Go net/http 调用 Ollama
```
**检索流程(当前):**
1. 用户提问 → 关键词 + embedding 向量
2. MySQL FULLTEXT 关键词召回 + FAISS 语义向量召回,双路融合排序
1. 用户提问 → embedding 向量(bge-m3)
2. Go 遍历 knowledge_chunk.content 计算余弦距离(数据量小无需 ANN)
3. 匹配段落作为上下文注入 LLM Prompt
4. LLM 基于上下文生成回答
> **演进说明:** V1.1 曾为避免 embedding 依赖而仅用全文索引;平台升级后引入 FAISS 补足语义召回,见 `docs/db_schema.md`。
## 5. 文件存储策略
- **数据库只存元数据**,不存文件二进制
- 物理文件存储在 `backend/data/media/`
- 物理文件存储在 `data/media/`
- 目录结构:
```
data/media/
+56 -62
View File
@@ -1,9 +1,9 @@
# AR04 — 部署架构设计
> **版本:V1.1 | 部署模式:纯本地离线**
> **完整部署步骤请见 docs/deploy.md**
>
> **⚠️ 本文档为 V1.1 设计期历史快照(Docker + FastAPI 拓扑),不再反映当前实现。** 当前部署为 **Go 单二进制 + MySQL 8.0 + FAISS + systemd + Clonezilla 整盘克隆**,无 Docker、无 Python 运行时,以 `docs/deploy.md` 为准。
> **版本:V1.3 | 部署模式:纯本地离线 · Go 单二进制 + SQLite · systemd**
> **当前实现**:**Go 单二进制 + SQLite(eai_agentplatform.db) + 内网 Ollama(LLM/embedding) + systemd**,无 Docker、无 MySQL、无 FAISS、无 Python 运行时。
> **知识链路**:检索为 Go 原生实施(embed_gen 路由嵌入候选块 + 余弦召回,对齐 D07/D13);分类/入库已彻底迁移到 Go,Python 的 `knowledge_service` 已删除。
> **完整部署步骤请见 docs/deploy.md。**
---
@@ -27,19 +27,19 @@
┌──────────────┘ └──────────────┐
│ │
┌─────▼──────┐ ┌────────▼────────┐
│ FastAPI │ │ Vue 静态打包 │
│ :8000 │ │ nginx html/ │
│ │ └─────────────────┘
│ · JWT │
│ · 业务 │
│ eai_agentplatform-server │ Vue 静态打包 │
│ :8080 │ │ nginx html/ │
│ 单二进制 │ └─────────────────┘
│ · Gin + │
│ · SQLite │
└──┬──┬──┬──┘
│ │ │
┌───────────┘ │ └──────────────┐
│ │ │
┌──▼─────┐ ┌────▼───────┐ ┌──────▼──────────┐
│ MySQL │ │ data/media │ │ LibreOffice │
│ 8.0 │ │ 文件存储 │ │ 预览容器 │
│ :3306 │ │ │ │ :8100 │
│ SQLite │ │ data/kb_data│ │ LibreOffice │
│ 单文件 │ │ 文件存储 │ │ + pdftotext │
│ eai_platform.db │ │ │ (裸进程) │
└────────┘ └────────────┘ └─────────────────┘
│
│ (文档转换)
@@ -54,13 +54,15 @@
## 2. 服务清单
| 服务 | 端口 | 基础镜像/依赖 | 说明 |
|------|------|-------------|------|
| Nginx | 80/443 | nginx:alpine | HTTP 反代 + 前端静态资源 |
| FastAPI | 8000 | python:3.10 | 后端 API(uvicorn 启动) |
| MySQL | 3306 | mysql:8.0 | 数据库 |
| LibreOffice | 8100 | 自定义 Docker 镜像 | 文档转 PDF 预览 |
| LLM 服务 | 11434 | ollama/vllm | 内网 AI 推理 |
| 服务 | 端口/路径 | 运行方式 | 说明 |
|------|-----------|---------|------|
| Nginx | 80/443 | 系统包 | HTTP 反代 + 前端静态资源托管 |
| eai_agentplatform-server | 8080 | Go 单二进制(CGO_ENABLED=0,静态链接) | Gin + GORM 后端 API(内嵌 SQLite) |
| SQLite | data/eai_agentplatform.db | 内嵌 | 单文件关系数据(glebarez/sqlite 纯 Go 驱动,无需 CGO) |
| LibreOffice + pdftotext | 裸进程 | exec 调用 | 文档转 PDF/文本预览(非容器,不监听端口) |
| LLM 服务 | 11434 | 外置 | 内网 OpenAI 兼容接口(Ollama / vLLM / 网关) |
**核心特征**:无 Docker、无 Python 运行时、无 MySQL、无 FAISS。后端为 Go 单二进制,数据为 SQLite 单文件;知识检索为 Go 原生向量/关键词召回(align D07/D13)。
## 3. Nginx 关键配置
@@ -72,60 +74,51 @@ location / {
# API 反代 + SSE
location /api/ {
proxy_pass http://127.0.0.1:8000;
proxy_pass http://127.0.0.1:8080;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 300s;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# 媒体文件预览(仅内部)
location /media/ {
alias /opt/eai_agentplatform/backend/data/media/;
internal;
# 媒体文件预览(Go 二进制直出)
location /api/media/ {
proxy_pass http://127.0.0.1:8080;
}
client_max_body_size 2048M;
```
## 4. docker-compose 服务拓扑
## 4. systemd 管理
```yaml
version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_DATABASE: eai_agentplatform
MYSQL_USER: eai_agentplatform
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
```ini
[Unit]
Description=eai_agentplatform Server
After=network.target
backend:
build: ./backend
environment:
DATABASE_URL: mysql+pymysql://eai_agentplatform:${DB_PASSWORD}@mysql:3306/eai_agentplatform
JWT_SECRET: ${JWT_SECRET}
LLM_BASE_URL: http://llm-server:11434/v1
volumes:
- ./data/media:/app/data/media
depends_on:
- mysql
[Service]
Type=simple
User=eai_agentplatform
Group=eai_agentplatform
WorkingDirectory=/opt/eai_agentplatform
ExecStart=/opt/eai_agentplatform/eai_agentplatform-server
Restart=on-failure
RestartSec=5
EnvironmentFile=/opt/eai_agentplatform/.env
libreoffice:
image: libreoffice-preview:latest
volumes:
- ./data/media:/data/media
# 安全加固
ExecStartPre=/opt/eai_agentplatform/eai_agentplatform-server migrate
ProtectSystem=strict
ProtectHome=true
NoNewPrivileges=true
PrivateTmp=true
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./frontend/dist:/usr/share/nginx/html
- ./nginx.conf:/etc/nginx/conf.d/default.conf
depends_on:
- backend
[Install]
WantedBy=multi-user.target
```
## 5. 安全边界
@@ -135,6 +128,7 @@ services:
| 网络 | 仅监听内网,不暴露公网端口 |
| 认证 | JWT token 校验 + bcrypt 密码 |
| 鉴权 | 后端 API role 校验(非前端) |
| 文件 | 白名单扩展名 + UUID 命名 + 只读预览 |
| 数据库 | 独立用户 + 最小权限 |
| LLM | 仅内网地址,严禁公网 API |
| 文件 | 白名单扩展名 + UUID 命名 + proxy_pass 内部预览 |
| 数据库 | SQLite 单文件(系统级文件权限,systemd ProtectSystem=strict) |
| 备份 | 内建定期 VACUUM INTO,默认 24h 间隔,保留 7 份 |
| LLM | 仅内网地址,严禁公网 API |