1776654103
This commit is contained in:
@@ -0,0 +1 @@
|
||||
DDD
|
||||
@@ -0,0 +1,77 @@
|
||||
MCP 协议的实现,可以有2种方式,
|
||||
|
||||
1. stdio,把 jar 提供出去,别人本地配置引入即可使用,但这样的方式不太适合做统一网关服务。
|
||||
2. sse
|
||||
|
||||
项目采用 [DDD](DDD.md) 架构,首先聚焦 domain 层。
|
||||
|
||||
### 会话管理服务
|
||||
|
||||
```java
|
||||
private final ScheduledExecutorService cleanupScheduler = Executors.newSingleThreadScheduledExecutor();
|
||||
```
|
||||
|
||||
其中 private 说明这个变量这能在类内部访问,声明为 final 则此变量只能赋值一次。`newSingleThreadScheduledExecutor` 代表只有一个工作线程。
|
||||
|
||||
> 一个带有定时功能的单线程定时队列。
|
||||
|
||||
```java
|
||||
private final Map<String, SessionConfigVO> activeSessions = new ConcurrentHashMap<>();
|
||||
```
|
||||
|
||||
考虑点在于这个会话是否会被多个线程操作处理。
|
||||
|
||||
```java
|
||||
public SessionManagementService() {
|
||||
cleanupScheduler.scheduleAtFixedRate(this::cleanupExpiredSessions, 5, 5, TimeUnit.MINUTES);
|
||||
log.info("会话管理服务已启动,会话超时时间: {} 分钟", SESSION_TIMEOUT_MINUTES);
|
||||
}
|
||||
```
|
||||
|
||||
可以在构造中执行一些方法。
|
||||
|
||||
### 实时通讯
|
||||
|
||||
> 对立面轮训
|
||||
|
||||
SSE(Server sent Events)
|
||||
|
||||
基于普通的 HTTP 长连接,Content-type: text/event-stream
|
||||
|
||||
消息格式,每条消息以 \n\n 结尾,
|
||||
|
||||

|
||||
|
||||
```
|
||||
SSE(协议)
|
||||
↑
|
||||
需要一个好的服务器框架来实现它
|
||||
↓
|
||||
Spring WebFlux(响应式 Web 框架) ← 最适合实现 SSE 的框架
|
||||
↑
|
||||
因为它是响应式(非阻塞),非常适合长连接
|
||||
↓
|
||||
在 WebFlux 里,用 Sinks + Flux<ServerSentEvent> 来产生和推送 SSE 事件
|
||||
```
|
||||
|
||||
```
|
||||
客户端发起 SSE 请求
|
||||
↓
|
||||
Netty EventLoop 接收请求(线程A)
|
||||
↓
|
||||
Controller 返回 sink.asFlux() → 注册“有新事件时推送”的回调
|
||||
↓
|
||||
线程A 立刻释放,去处理其他请求
|
||||
↓
|
||||
...(连接保持打开,线程A 忙别的)
|
||||
|
||||
你的服务层调用 sink.tryEmitNext(新消息)
|
||||
↓
|
||||
Reactor 通知 Netty:“这个连接有数据要写”
|
||||
↓
|
||||
EventLoop 线程(可能是线程B)被唤醒
|
||||
↓
|
||||
执行回调:把消息转成 "data: xxx\n\n" 格式 → 写入 Socket
|
||||
↓
|
||||
写完后,线程B 立刻释放,继续干别的
|
||||
```
|
||||
@@ -1,121 +0,0 @@
|
||||
```
|
||||
# ========== 1. 请求数统计 ==========
|
||||
curl -X POST http://localhost:7530/api/mcp-tools \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "get_request_count",
|
||||
"description": "查询请求数统计数据,返回各时间点的请求数列表",
|
||||
"backendUrl": "http://dev.p.cdn.10086.cn:8080/mcp/requestCount",
|
||||
"backendMethod": "GET",
|
||||
"backendTimeout": 10000,
|
||||
"forwardHeaders": ["Authorization", "X-User-Id"],
|
||||
"responseDescription": "返回 JSON,errCode=0 为成功,data 为数据列表,示例:{\"errCode\":0,\"data\":[{\"name\":\"2026-03-27 10:00\",\"value\":12345}],\"unit\":\"\"}",
|
||||
"parameters": [
|
||||
{"name": "cpId", "type": "string", "description": "企业ID(空默认全部)", "required": false, "sortOrder": 0},
|
||||
{"name": "domainNames", "type": "string", "description": "域名列表,逗号分隔(空默认全部)", "required": false, "sortOrder": 1},
|
||||
{"name": "product", "type": "string", "description": "产品类型(空默认全部,下载加速:0,视音频点播加速:1,网页加速:2,直播加速:5,超低时延直播:7,全站四层加速:8,全站七层加速:9)", "required": false, "sortOrder": 2},
|
||||
{"name": "granular", "type": "string", "description": "时间粒度(5分钟:0,1小时:1,1天:2)", "required": false, "sortOrder": 3},
|
||||
{"name": "providers", "type": "string", "description": "加速厂商列表,逗号分隔(空默认全部,华为平面:1,中兴平面:2,HCDN:3,自研1.0:4,自研RTMP:5,自研2.0:8,国际平面:9,合作服务商1:10,合作服务商2:11)", "required": false, "sortOrder": 4},
|
||||
{"name": "affectAreas", "type": "string", "description": "加速区域列表,逗号分隔(空默认全部,例如安徽、北京、福建)", "required": false, "sortOrder": 5},
|
||||
{"name": "isps", "type": "string", "description": "运营商列表,逗号分隔(空默认全部,移动:1,电信:2,联通:3,多线:4,国际:5,其他:0)", "required": false, "sortOrder": 6},
|
||||
{"name": "startTime", "type": "string", "description": "开始时间,格式 yyyy-MM-dd HH:mm", "required": false, "sortOrder": 7},
|
||||
{"name": "endTime", "type": "string", "description": "结束时间,格式 yyyy-MM-dd HH:mm", "required": false, "sortOrder": 8}
|
||||
]
|
||||
}'
|
||||
|
||||
# ========== 2. 命中请求数统计 ==========
|
||||
curl -X POST http://localhost:7530/api/mcp-tools \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "get_hit_req_count",
|
||||
"description": "查询命中请求数统计数据,返回各时间点的命中请求数列表",
|
||||
"backendUrl": "http://dev.p.cdn.10086.cn:8080/mcp/hitReqCount",
|
||||
"backendMethod": "GET",
|
||||
"backendTimeout": 10000,
|
||||
"forwardHeaders": ["Authorization", "X-User-Id"],
|
||||
"responseDescription": "返回 JSON,errCode=0 为成功,data 为数据列表,示例:{\"errCode\":0,\"data\":[{\"name\":\"2026-03-27 10:00\",\"value\":10000}],\"unit\":\"\"}",
|
||||
"parameters": [
|
||||
{"name": "cpId", "type": "string", "description": "企业ID(空默认全部)", "required": false, "sortOrder": 0},
|
||||
{"name": "domainNames", "type": "string", "description": "域名列表,逗号分隔(空默认全部)", "required": false, "sortOrder": 1},
|
||||
{"name": "product", "type": "string", "description": "产品类型(空默认全部)", "required": false, "sortOrder": 2},
|
||||
{"name": "granular", "type": "string", "description": "时间粒度(5分钟:0,1小时:1,1天:2)", "required": false, "sortOrder": 3},
|
||||
{"name": "providers", "type": "string", "description": "加速厂商列表,逗号分隔(空默认全部)", "required": false, "sortOrder": 4},
|
||||
{"name": "affectAreas", "type": "string", "description": "加速区域列表,逗号分隔(空默认全部)", "required": false, "sortOrder": 5},
|
||||
{"name": "isps", "type": "string", "description": "运营商列表,逗号分隔(空默认全部)", "required": false, "sortOrder": 6},
|
||||
{"name": "startTime", "type": "string", "description": "开始时间,格式 yyyy-MM-dd HH:mm", "required": false, "sortOrder": 7},
|
||||
{"name": "endTime", "type": "string", "description": "结束时间,格式 yyyy-MM-dd HH:mm", "required": false, "sortOrder": 8}
|
||||
]
|
||||
}'
|
||||
|
||||
# ========== 3. 命中率计算(一次性接口) ==========
|
||||
curl -X POST http://localhost:7530/api/mcp-tools \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "get_hit_ratio",
|
||||
"description": "计算命中率,命中率 = 命中请求数 / 总请求数,返回各时间点的命中率百分比(2位小数)",
|
||||
"backendUrl": "http://dev.p.cdn.10086.cn:8080/mcp/hitRatio",
|
||||
"backendMethod": "GET",
|
||||
"backendTimeout": 10000,
|
||||
"forwardHeaders": ["Authorization", "X-User-Id"],
|
||||
"responseDescription": "返回 JSON,errCode=0 为成功,data 为数据列表,unit 为 %,示例:{\"errCode\":0,\"data\":[{\"name\":\"2026-03-27 10:00\",\"value\":85.23}],\"unit\":\"%\"}",
|
||||
"parameters": [
|
||||
{"name": "cpId", "type": "string", "description": "企业ID(空默认全部)", "required": false, "sortOrder": 0},
|
||||
{"name": "domainNames", "type": "string", "description": "域名列表,逗号分隔(空默认全部)", "required": false, "sortOrder": 1},
|
||||
{"name": "product", "type": "string", "description": "产品类型(空默认全部)", "required": false, "sortOrder": 2},
|
||||
{"name": "granular", "type": "string", "description": "时间粒度(5分钟:0,1小时:1,1天:2)", "required": false, "sortOrder": 3},
|
||||
{"name": "providers", "type": "string", "description": "加速厂商列表,逗号分隔(空默认全部)", "required": false, "sortOrder": 4},
|
||||
{"name": "affectAreas", "type": "string", "description": "加速区域列表,逗号分隔(空默认全部)", "required": false, "sortOrder": 5},
|
||||
{"name": "isps", "type": "string", "description": "运营商列表,逗号分隔(空默认全部)", "required": false, "sortOrder": 6},
|
||||
{"name": "startTime", "type": "string", "description": "开始时间,格式 yyyy-MM-dd HH:mm", "required": false, "sortOrder": 7},
|
||||
{"name": "endTime", "type": "string", "description": "结束时间,格式 yyyy-MM-dd HH:mm", "required": false, "sortOrder": 8}
|
||||
]
|
||||
}'
|
||||
|
||||
# ========== 4. 命中率工具(分步计算用:先调requestCount,再调hitReqCount,最后调此工具计算) ==========
|
||||
curl -X POST http://localhost:7530/api/mcp-tools \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "calculate_hit_ratio",
|
||||
"description": "根据请求数和命中请求数计算命中率,返回百分比(2位小数)。适用于先调用 get_request_count 和 get_hit_req_count 获取原始数据后,再调用此工具计算命中率",
|
||||
"backendUrl": "http://dev.p.cdn.10086.cn:8080/mcp/hitRatio/calculate",
|
||||
"backendMethod": "GET",
|
||||
"backendTimeout": 5000,
|
||||
"forwardHeaders": ["Authorization", "X-User-Id"],
|
||||
"responseDescription": "返回 JSON,errCode=0 为成功,示例:{\"errCode\":0,\"data\":[{\"name\":\"hitRatio\",\"value\":85.23}],\"unit\":\"%\"}",
|
||||
"parameters": [
|
||||
{"name": "requestCount", "type": "integer", "description": "总请求数", "required": true, "sortOrder": 0},
|
||||
{"name": "hitReqCount", "type": "integer", "description": "命中请求数", "required": true, "sortOrder": 1}
|
||||
]
|
||||
}'
|
||||
|
||||
# ========== 5. 获取企业列表 ==========
|
||||
curl -X POST http://localhost:7530/api/mcp-tools \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "get_enterprise_list",
|
||||
"description": "获取当前用户所属的企业列表,返回 enterpriseId 和 enterpriseName,供统计接口选择企业参数用",
|
||||
"backendUrl": "http://dev.p.cdn.10086.cn:8080/mcp/enterprises",
|
||||
"backendMethod": "GET",
|
||||
"backendTimeout": 5000,
|
||||
"forwardHeaders": ["Authorization", "X-User-Id"],
|
||||
"responseDescription": "返回 JSON 列表,每个元素含 enterpriseId 和 enterpriseName,示例:[{\"enterpriseId\":\"abc123\",\"enterpriseName\":\"测试企业\"}]",
|
||||
"parameters": []
|
||||
}'
|
||||
|
||||
# ========== 6. 获取域名列表 ==========
|
||||
curl -X POST http://localhost:7530/api/mcp-tools \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "get_domain_list",
|
||||
"description": "根据企业ID获取该企业下已生效的域名列表,供统计接口选择域名参数用",
|
||||
"backendUrl": "http://dev.p.cdn.10086.cn:8080/mcp/domains",
|
||||
"backendMethod": "GET",
|
||||
"backendTimeout": 5000,
|
||||
"forwardHeaders": ["Authorization", "X-User-Id"],
|
||||
"responseDescription": "返回 JSON 列表,每个元素为域名字符串,示例:[\"www.example.com\",\"img.example.com\"]",
|
||||
"parameters": [
|
||||
{"name": "tenantId", "type": "string", "description": "企业ID(tenantId,即 enterpriseId)", "required": true, "sortOrder": 0}
|
||||
]
|
||||
}'
|
||||
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||

|
||||
@@ -1,3 +0,0 @@
|
||||
```
|
||||
{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% set ns = namespace(is_first=false, is_tool=false, is_output_first=true, system_prompt='') %}{%- for message in messages %}{%- if message['role'] == 'system' %}{% set ns.system_prompt = message['content'] %}{%- endif %}{%- endfor %}{{bos_token}}{{ns.system_prompt}}{%- for message in messages %}{%- if message['role'] == 'user' %}{%- set ns.is_tool = false -%}{{'<|User|>' + message['content']}}{%- endif %}{%- if message['role'] == 'assistant' and message['content'] is none %}{%- set ns.is_tool = false -%}{%- for tool in message['tool_calls']%}{%- if not ns.is_first %}{{'<|Assistant|><|tool▁calls▁begin|><|tool▁call▁begin|>' + tool['type'] + '<|tool▁sep|>' + tool['function']['name'] + '\\n' + '```json' + '\\n' + tool['function']['arguments'] + '\\n' + '```' + '<|tool▁call▁end|>'}}{%- set ns.is_first = true -%}{%- else %}{{'\\n' + '<|tool▁call▁begin|>' + tool['type'] + '<|tool▁sep|>' + tool['function']['name'] + '\\n' + '```json' + '\\n' + tool['function']['arguments'] + '\\n' + '```' + '<|tool▁call▁end|>'}}{{'<|tool▁calls▁end|><|end▁of▁sentence|>'}}{%- endif %}{%- endfor %}{%- endif %}{%- if message['role'] == 'assistant' and message['content'] is not none %}{%- if ns.is_tool %}{{'<|tool▁outputs▁end|>' + message['content'] + '<|end▁of▁sentence|>'}}{%- set ns.is_tool = false -%}{%- else %}{% set content = message['content'] %}{% if '</think>' in content %}{% set content = message['content'].replace('</think>', '').split('<think>')[-1] %}{% endif %}{{'<|Assistant|>' + content + '<|end▁of▁sentence|>'}}{%- endif %}{%- endif %}{%- if message['role'] == 'tool' %}{%- set ns.is_tool = true -%}{%- if ns.is_output_first %}{{'<|tool▁outputs▁begin|><|tool▁output▁begin|>' + message['content'] + '<|tool▁output▁end|>'}}{%- set ns.is_output_first = false %}{%- else %}{{'\\n<|tool▁output▁begin|>' + message['content'] + '<|tool▁output▁end|>'}}{%- endif %}{%- endif %}{%- endfor -%}{% if ns.is_tool %}{{'<|tool▁outputs▁end|>'}}{% endif %}{% if add_generation_prompt and not ns.is_tool %}{{'<|Assistant|>'}}{% endif %}
|
||||
```
|
||||
@@ -0,0 +1,37 @@
|
||||
```java
|
||||
package cn.bugstack.ai.infrastructure.gateway;
|
||||
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.ResponseBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.HeaderMap;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.QueryMap;
|
||||
import retrofit2.http.Url;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 资料:<a href="https://bugstack.cn/md/road-map/http.html">HTTP 框架案例</a>
|
||||
*/
|
||||
public interface GenericHttpGateway {
|
||||
|
||||
@POST
|
||||
Call<ResponseBody> post(
|
||||
@Url String url,
|
||||
@HeaderMap Map<String, Object> headers,
|
||||
@Body RequestBody body
|
||||
);
|
||||
|
||||
@GET
|
||||
Call<ResponseBody> get(
|
||||
@Url String url,
|
||||
@HeaderMap Map<String, Object> headers,
|
||||
@QueryMap Map<String, Object> queryParams
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
@@ -1,44 +0,0 @@
|
||||
|
||||
```
|
||||
http://dev.p.cdn.10086.cn:8080
|
||||
|
||||
/statistics/getHitRatio
|
||||
|
||||
?cpId=*
|
||||
&product=*
|
||||
&affectAreas=
|
||||
&domainNames=
|
||||
&provider=*
|
||||
&startTime=2026-03-27%2010:25
|
||||
&endTime=2026-03-27%2010:25
|
||||
&productId=
|
||||
&isps=
|
||||
&granular=0
|
||||
&_=1774580169469
|
||||
```
|
||||
|
||||
com.cmcc.cdn.platform.selfservice.controller.StatisticsController#getHitRatio
|
||||
|
||||
给我梳理下这个接口的功能,输出一个 markdown 文档到项目的根目录。
|
||||
|
||||
主要是用到哪些参数,有哪些校验,有没有参数转换和默认值。掉了哪些三方接口,调了接口后做了哪些事情。尽量清晰。注意细节。
|
||||
|
||||
---
|
||||
|
||||
com.cmcc.cdn.platform.selfservice.controller.StatisticsController#getHitRatio
|
||||
|
||||
getHitRatio接口分析.md 是我 getHitRatio 命中率接口的梳理,是在跑的业务。我现在给你安排一个艰巨的任务。
|
||||
|
||||
因为我们有一个 MCP 注册中心。我要接入进去。接入我们只需要提供标准的 http 接口就行了。所以按照这个接口做 3 个新接口出来:
|
||||
|
||||
1. 请求数
|
||||
2. 命中请求数
|
||||
3. 计算命中率(命中率 = 命中请求数 / 总请求数)的接口。
|
||||
|
||||
controller 在 ibs-portal\cdn-web\src\main\java\com\cmcc\cdn\platform 下建一个 mcp 文件夹写。
|
||||
|
||||
service 在 ibs-portal\cdn-service\src\main\java\com\cmcc\cdn\platform 下建一个 mcp 文件夹写。
|
||||
|
||||
因为是新接口代码一定要标准易读清晰,不要用原来的垃圾逻辑。
|
||||
|
||||
再帮我额外提供一个接口。可以实现比如 AI 获取 请求数 然后获取 命中请求数 。然后可以计算他们两个得出命中率的工具接口。方便我测试是调用一次计算命中率比较好,还是调用 请求数 然后调用 命中请求数 然后调用计算工具计算合适。
|
||||
Reference in New Issue
Block a user