feat: 实现公告板功能
- 后端:从 GitHub Issues API 获取公告数据 - 支持筛选指定 assignees 的评论 - 支持排除指定 issue 的评论 - 从评论第一行提取标题(移除 Markdown 格式) - 支持获取 reactions 数据(+1、heart、laugh 等) - 实现 10 分钟缓存机制 - 强制刷新时,如果被限流则保留缓存数据 - 添加 GitHub API 健康检查 - 前端:公告列表和详情页面 - 响应式设计(移动端和桌面端) - 移动端使用侧边抽屉显示列表 - 支持 Markdown 渲染 - 支持内容折叠(超过 30 行) - 显示 reactions(使用 emoji) - 添加刷新按钮(支持强制刷新) - 公告菜单项放在第一位 - 默认页面改为公告页 - 其他: - 更新 favicon 为 SVG 格式 - 添加 GitHub API 限流方案文档 - 添加 GitHub Token 配置文档
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
package com.wrbug.polymarketbot.api
|
||||
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
|
||||
/**
|
||||
* GitHub API 接口
|
||||
*/
|
||||
interface GitHubApi {
|
||||
/**
|
||||
* 获取 Issue 信息
|
||||
* @param owner 仓库所有者
|
||||
* @param repo 仓库名
|
||||
* @param issueNumber Issue 编号
|
||||
* @return Issue 信息响应
|
||||
*/
|
||||
@GET("repos/{owner}/{repo}/issues/{issue_number}")
|
||||
suspend fun getIssue(
|
||||
@Path("owner") owner: String,
|
||||
@Path("repo") repo: String,
|
||||
@Path("issue_number") issueNumber: Int
|
||||
): Response<GitHubIssueResponse>
|
||||
|
||||
/**
|
||||
* 获取 Issue 评论列表
|
||||
* @param owner 仓库所有者
|
||||
* @param repo 仓库名
|
||||
* @param issueNumber Issue 编号
|
||||
* @return 评论列表响应
|
||||
*/
|
||||
@GET("repos/{owner}/{repo}/issues/{issue_number}/comments")
|
||||
suspend fun getIssueComments(
|
||||
@Path("owner") owner: String,
|
||||
@Path("repo") repo: String,
|
||||
@Path("issue_number") issueNumber: Int
|
||||
): Response<List<GitHubCommentResponse>>
|
||||
}
|
||||
|
||||
/**
|
||||
* GitHub Issue 响应
|
||||
*/
|
||||
data class GitHubIssueResponse(
|
||||
val id: Long,
|
||||
val number: Int,
|
||||
val assignees: List<GitHubUser>
|
||||
)
|
||||
|
||||
/**
|
||||
* GitHub 评论响应
|
||||
*/
|
||||
data class GitHubCommentResponse(
|
||||
val id: Long,
|
||||
val body: String,
|
||||
val user: GitHubUser,
|
||||
val created_at: String,
|
||||
val updated_at: String,
|
||||
val issue_url: String? = null, // Issue URL,格式:https://api.github.com/repos/owner/repo/issues/3703128976
|
||||
val reactions: GitHubReactions? = null // Reactions 数据
|
||||
)
|
||||
|
||||
/**
|
||||
* GitHub Reactions 数据
|
||||
*/
|
||||
data class GitHubReactions(
|
||||
val url: String,
|
||||
val total_count: Int,
|
||||
val `+1`: Int = 0, // +1 数量
|
||||
val `-1`: Int = 0, // -1 数量
|
||||
val laugh: Int = 0, // 😄 数量
|
||||
val confused: Int = 0, // 😕 数量
|
||||
val heart: Int = 0, // ❤️ 数量
|
||||
val hooray: Int = 0, // 🎉 数量
|
||||
val eyes: Int = 0, // 👀 数量
|
||||
val rocket: Int = 0 // 🚀 数量
|
||||
)
|
||||
|
||||
/**
|
||||
* GitHub 用户信息
|
||||
*/
|
||||
data class GitHubUser(
|
||||
val login: String,
|
||||
val id: Long,
|
||||
val avatar_url: String? = null
|
||||
)
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.wrbug.polymarketbot.controller
|
||||
|
||||
import com.wrbug.polymarketbot.dto.*
|
||||
import com.wrbug.polymarketbot.enums.ErrorCode
|
||||
import com.wrbug.polymarketbot.service.AnnouncementService
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.context.MessageSource
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
/**
|
||||
* 公告控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/announcements")
|
||||
class AnnouncementController(
|
||||
private val announcementService: AnnouncementService,
|
||||
private val messageSource: MessageSource
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(AnnouncementController::class.java)
|
||||
|
||||
/**
|
||||
* 获取公告列表(最近10条)
|
||||
*/
|
||||
@PostMapping("/list")
|
||||
fun getAnnouncementList(@RequestBody request: AnnouncementListRequest): ResponseEntity<ApiResponse<AnnouncementListResponse>> {
|
||||
return try {
|
||||
val result = runBlocking { announcementService.getAnnouncementList(request.forceRefresh) }
|
||||
result.fold(
|
||||
onSuccess = { response ->
|
||||
ResponseEntity.ok(ApiResponse.success(response))
|
||||
},
|
||||
onFailure = { e ->
|
||||
logger.error("获取公告列表失败: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("获取公告列表异常: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取公告详情
|
||||
*/
|
||||
@PostMapping("/detail")
|
||||
fun getAnnouncementDetail(@RequestBody request: AnnouncementDetailRequest): ResponseEntity<ApiResponse<AnnouncementDto>> {
|
||||
return try {
|
||||
val result = runBlocking { announcementService.getAnnouncementDetail(request.id, request.forceRefresh) }
|
||||
result.fold(
|
||||
onSuccess = { announcement ->
|
||||
ResponseEntity.ok(ApiResponse.success(announcement))
|
||||
},
|
||||
onFailure = { e ->
|
||||
logger.error("获取公告详情失败: ${e.message}", e)
|
||||
when (e) {
|
||||
is IllegalArgumentException -> ResponseEntity.ok(
|
||||
ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource)
|
||||
)
|
||||
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("获取公告详情异常: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.wrbug.polymarketbot.dto
|
||||
|
||||
/**
|
||||
* 公告列表请求
|
||||
*/
|
||||
data class AnnouncementListRequest(
|
||||
val forceRefresh: Boolean = false // 是否强制刷新缓存
|
||||
)
|
||||
|
||||
/**
|
||||
* 公告详情请求
|
||||
*/
|
||||
data class AnnouncementDetailRequest(
|
||||
val id: Long? = null, // 评论ID,如果为空则返回最新一条
|
||||
val forceRefresh: Boolean = false // 是否强制刷新缓存
|
||||
)
|
||||
|
||||
/**
|
||||
* Reactions 信息
|
||||
*/
|
||||
data class ReactionsDto(
|
||||
val plusOne: Int = 0, // 👍 +1 数量
|
||||
val minusOne: Int = 0, // 👎 -1 数量
|
||||
val laugh: Int = 0, // 😄 数量
|
||||
val confused: Int = 0, // 😕 数量
|
||||
val heart: Int = 0, // ❤️ 数量
|
||||
val hooray: Int = 0, // 🎉 数量
|
||||
val eyes: Int = 0, // 👀 数量
|
||||
val rocket: Int = 0, // 🚀 数量
|
||||
val total: Int = 0 // 总数量
|
||||
)
|
||||
|
||||
/**
|
||||
* 公告信息响应
|
||||
*/
|
||||
data class AnnouncementDto(
|
||||
val id: Long, // GitHub 评论 ID
|
||||
val title: String, // 标题(从评论第一行提取,已移除 Markdown 格式)
|
||||
val body: String, // Markdown 内容(完整内容)
|
||||
val author: String, // 作者用户名
|
||||
val authorAvatarUrl: String?, // 作者头像 URL
|
||||
val createdAt: Long, // 创建时间(时间戳,毫秒)
|
||||
val updatedAt: Long, // 更新时间(时间戳,毫秒)
|
||||
val reactions: ReactionsDto? = null // Reactions 数据
|
||||
)
|
||||
|
||||
/**
|
||||
* 公告列表响应
|
||||
*/
|
||||
data class AnnouncementListResponse(
|
||||
val list: List<AnnouncementDto>,
|
||||
val hasMore: Boolean, // 是否还有更多(总数 > 10)
|
||||
val total: Int // 总数
|
||||
)
|
||||
|
||||
@@ -0,0 +1,401 @@
|
||||
package com.wrbug.polymarketbot.service
|
||||
|
||||
import com.wrbug.polymarketbot.api.GitHubApi
|
||||
import com.wrbug.polymarketbot.dto.AnnouncementDto
|
||||
import com.wrbug.polymarketbot.dto.AnnouncementListResponse
|
||||
import com.wrbug.polymarketbot.util.RetrofitFactory
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Service
|
||||
import java.time.Instant
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
/**
|
||||
* 公告服务
|
||||
* 从 GitHub Issues API 获取公告信息
|
||||
*/
|
||||
@Service
|
||||
class AnnouncementService(
|
||||
private val retrofitFactory: RetrofitFactory,
|
||||
@Value("\${github.repo.owner:WrBug}")
|
||||
private val repoOwner: String,
|
||||
@Value("\${github.repo.name:PolyHermes}")
|
||||
private val repoName: String,
|
||||
@Value("\${github.announcement.issue.number:1}")
|
||||
private val issueNumber: Int
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(AnnouncementService::class.java)
|
||||
|
||||
// GitHub API 客户端(懒加载)
|
||||
private val githubApi: GitHubApi by lazy {
|
||||
retrofitFactory.createGitHubApi()
|
||||
}
|
||||
|
||||
// 需要排除的 Issue ID(从 issue_url 中提取)
|
||||
private val excludedIssueId = "3703128976"
|
||||
|
||||
// 缓存数据(1分钟有效期)
|
||||
private data class CachedData<T>(
|
||||
val data: T,
|
||||
val timestamp: Long
|
||||
)
|
||||
|
||||
private var cachedList: CachedData<AnnouncementListResponse>? = null
|
||||
private var cachedAssignees: CachedData<List<String>>? = null
|
||||
private var cachedComments: CachedData<List<com.wrbug.polymarketbot.api.GitHubCommentResponse>>? = null
|
||||
|
||||
// 缓存有效期:10分钟(毫秒)
|
||||
private val cacheExpiryTime = 10 * 60 * 1000L
|
||||
|
||||
/**
|
||||
* 检查缓存是否有效
|
||||
*/
|
||||
private fun <T> isCacheValid(cached: CachedData<T>?): Boolean {
|
||||
if (cached == null) return false
|
||||
val now = System.currentTimeMillis()
|
||||
return (now - cached.timestamp) < cacheExpiryTime
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否被限流
|
||||
*/
|
||||
private fun isRateLimited(response: retrofit2.Response<*>): Boolean {
|
||||
// HTTP 403 通常表示限流
|
||||
if (response.code() == 403) {
|
||||
return true
|
||||
}
|
||||
// 检查响应头中的限流信息
|
||||
val remaining = response.headers()["X-RateLimit-Remaining"]
|
||||
return remaining == "0"
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Issue 的 assignees 列表(通过 API 获取,带缓存)
|
||||
* @return Pair<assignees列表, 是否使用了缓存>
|
||||
*/
|
||||
private suspend fun getAssignees(forceRefresh: Boolean = false): Pair<List<String>, Boolean> {
|
||||
// 检查缓存
|
||||
if (!forceRefresh && isCacheValid(cachedAssignees)) {
|
||||
logger.debug("使用缓存的 assignees")
|
||||
return Pair(cachedAssignees!!.data, true)
|
||||
}
|
||||
|
||||
return try {
|
||||
val response = githubApi.getIssue(
|
||||
owner = repoOwner,
|
||||
repo = repoName,
|
||||
issueNumber = issueNumber
|
||||
)
|
||||
|
||||
// 如果被限流,使用缓存数据,不更新缓存
|
||||
if (isRateLimited(response)) {
|
||||
logger.warn("GitHub API 被限流,使用缓存的 assignees(不更新缓存)")
|
||||
if (cachedAssignees != null) {
|
||||
return Pair(cachedAssignees!!.data, true) // 返回缓存数据,标记为使用了缓存
|
||||
}
|
||||
// 如果没有缓存,使用默认值
|
||||
return Pair(listOf("WrBug"), false)
|
||||
}
|
||||
|
||||
val assignees = if (response.isSuccessful && response.body() != null) {
|
||||
response.body()!!.assignees.map { it.login }
|
||||
} else {
|
||||
logger.warn("获取 Issue assignees 失败,使用默认值: code=${response.code()}")
|
||||
listOf("WrBug") // 默认值
|
||||
}
|
||||
|
||||
// 更新缓存
|
||||
cachedAssignees = CachedData(assignees, System.currentTimeMillis())
|
||||
Pair(assignees, false) // 返回新数据,标记为未使用缓存
|
||||
} catch (e: Exception) {
|
||||
logger.error("获取 Issue assignees 异常: ${e.message}", e)
|
||||
// 如果缓存存在,使用缓存
|
||||
if (cachedAssignees != null) {
|
||||
logger.warn("使用缓存的 assignees(API 调用失败)")
|
||||
return Pair(cachedAssignees!!.data, true) // 返回缓存数据,标记为使用了缓存
|
||||
}
|
||||
Pair(listOf("WrBug"), false) // 默认值
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Issue 评论列表(带缓存)
|
||||
* @return Pair<评论列表, 是否使用了缓存>
|
||||
*/
|
||||
private suspend fun getIssueComments(forceRefresh: Boolean = false): Pair<List<com.wrbug.polymarketbot.api.GitHubCommentResponse>, Boolean> {
|
||||
// 检查缓存
|
||||
if (!forceRefresh && isCacheValid(cachedComments)) {
|
||||
logger.debug("使用缓存的评论列表")
|
||||
return Pair(cachedComments!!.data, true)
|
||||
}
|
||||
|
||||
val response = githubApi.getIssueComments(
|
||||
owner = repoOwner,
|
||||
repo = repoName,
|
||||
issueNumber = issueNumber
|
||||
)
|
||||
|
||||
// 如果被限流,使用缓存数据,不更新缓存
|
||||
if (isRateLimited(response)) {
|
||||
logger.warn("GitHub API 被限流,使用缓存的评论列表(不更新缓存)")
|
||||
if (cachedComments != null) {
|
||||
return Pair(cachedComments!!.data, true) // 返回缓存数据,标记为使用了缓存
|
||||
}
|
||||
// 如果没有缓存,抛出异常
|
||||
throw Exception("获取公告列表失败: GitHub API 被限流,且无缓存数据")
|
||||
}
|
||||
|
||||
if (!response.isSuccessful || response.body() == null) {
|
||||
logger.error("获取 GitHub Issue 评论失败: code=${response.code()}, message=${response.message()}")
|
||||
// 如果缓存存在,使用缓存
|
||||
if (cachedComments != null) {
|
||||
logger.warn("使用缓存的评论列表(API 调用失败)")
|
||||
return Pair(cachedComments!!.data, true) // 返回缓存数据,标记为使用了缓存
|
||||
}
|
||||
throw Exception("获取公告列表失败: HTTP ${response.code()}")
|
||||
}
|
||||
|
||||
val comments = response.body()!!
|
||||
|
||||
// 更新缓存
|
||||
cachedComments = CachedData(comments, System.currentTimeMillis())
|
||||
return Pair(comments, false) // 返回新数据,标记为未使用缓存
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取公告列表(最近10条)
|
||||
* @param forceRefresh 是否强制刷新缓存
|
||||
*/
|
||||
suspend fun getAnnouncementList(forceRefresh: Boolean = false): Result<AnnouncementListResponse> {
|
||||
// 检查缓存
|
||||
if (!forceRefresh && isCacheValid(cachedList)) {
|
||||
logger.debug("使用缓存的公告列表")
|
||||
return Result.success(cachedList!!.data)
|
||||
}
|
||||
|
||||
return try {
|
||||
// 强制刷新时,先尝试获取新数据
|
||||
val (assigneeList, assigneesFromCache) = getAssignees(forceRefresh)
|
||||
val (comments, commentsFromCache) = getIssueComments(forceRefresh)
|
||||
|
||||
// 如果强制刷新时使用了缓存(被限流),直接返回缓存数据,不更新缓存
|
||||
if (forceRefresh && (assigneesFromCache || commentsFromCache)) {
|
||||
logger.warn("强制刷新时被限流,返回缓存的公告列表(不更新缓存)")
|
||||
if (cachedList != null) {
|
||||
return Result.success(cachedList!!.data)
|
||||
}
|
||||
}
|
||||
|
||||
// 筛选条件:
|
||||
// 1. assignees 发布的评论
|
||||
// 2. 排除 issueNumber 为 3703128976 的评论(从 issue_url 中提取)
|
||||
val filteredComments = comments
|
||||
.filter { comment ->
|
||||
// 检查是否为 assignee
|
||||
val isAssignee = assigneeList.contains(comment.user.login)
|
||||
|
||||
// 检查是否应该排除(从 issue_url 中提取 issue ID)
|
||||
val shouldExclude = comment.issue_url?.let { issueUrl ->
|
||||
// issue_url 格式:https://api.github.com/repos/owner/repo/issues/3703128976
|
||||
// 提取最后的数字
|
||||
val issueId = issueUrl.split("/").lastOrNull()
|
||||
issueId == excludedIssueId
|
||||
} ?: false
|
||||
|
||||
isAssignee && !shouldExclude
|
||||
}
|
||||
.sortedByDescending { comment ->
|
||||
parseGitHubTime(comment.created_at)
|
||||
}
|
||||
|
||||
val total = filteredComments.size
|
||||
val hasMore = total > 10
|
||||
|
||||
// 取前10条
|
||||
val latest10 = filteredComments.take(10).map { comment ->
|
||||
toAnnouncementDto(comment)
|
||||
}
|
||||
|
||||
val result = AnnouncementListResponse(
|
||||
list = latest10,
|
||||
hasMore = hasMore,
|
||||
total = total
|
||||
)
|
||||
|
||||
// 只有在数据正常返回时才更新缓存(不是从缓存获取的)
|
||||
if (!assigneesFromCache && !commentsFromCache) {
|
||||
cachedList = CachedData(result, System.currentTimeMillis())
|
||||
}
|
||||
|
||||
Result.success(result)
|
||||
} catch (e: Exception) {
|
||||
logger.error("获取公告列表异常: ${e.message}", e)
|
||||
// 如果缓存存在,返回缓存
|
||||
if (cachedList != null) {
|
||||
logger.warn("使用缓存的公告列表(API 调用失败)")
|
||||
return Result.success(cachedList!!.data)
|
||||
}
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取公告详情
|
||||
* @param id 评论ID,如果为 null 则返回最新一条
|
||||
* @param forceRefresh 是否强制刷新缓存
|
||||
*/
|
||||
suspend fun getAnnouncementDetail(id: Long?, forceRefresh: Boolean = false): Result<AnnouncementDto> {
|
||||
return try {
|
||||
// 获取 assignees
|
||||
val (assigneeList, assigneesFromCache) = getAssignees(forceRefresh)
|
||||
|
||||
// 获取评论列表
|
||||
val (comments, commentsFromCache) = getIssueComments(forceRefresh)
|
||||
|
||||
// 筛选条件:
|
||||
// 1. assignees 发布的评论
|
||||
// 2. 排除 issueNumber 为 3703128976 的评论(从 issue_url 中提取)
|
||||
val filteredComments = comments
|
||||
.filter { comment ->
|
||||
// 检查是否为 assignee
|
||||
val isAssignee = assigneeList.contains(comment.user.login)
|
||||
|
||||
// 检查是否应该排除(从 issue_url 中提取 issue ID)
|
||||
val shouldExclude = comment.issue_url?.let { issueUrl ->
|
||||
// issue_url 格式:https://api.github.com/repos/owner/repo/issues/3703128976
|
||||
// 提取最后的数字
|
||||
val issueId = issueUrl.split("/").lastOrNull()
|
||||
issueId == excludedIssueId
|
||||
} ?: false
|
||||
|
||||
isAssignee && !shouldExclude
|
||||
}
|
||||
.sortedByDescending { comment ->
|
||||
parseGitHubTime(comment.created_at)
|
||||
}
|
||||
|
||||
val targetComment = if (id != null) {
|
||||
filteredComments.find { it.id == id }
|
||||
} else {
|
||||
filteredComments.firstOrNull()
|
||||
}
|
||||
|
||||
if (targetComment == null) {
|
||||
return Result.failure(IllegalArgumentException("公告不存在"))
|
||||
}
|
||||
|
||||
Result.success(toAnnouncementDto(targetComment))
|
||||
} catch (e: Exception) {
|
||||
logger.error("获取公告详情异常: ${e.message}", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 GitHub 评论转换为 AnnouncementDto
|
||||
*/
|
||||
private fun toAnnouncementDto(comment: com.wrbug.polymarketbot.api.GitHubCommentResponse): AnnouncementDto {
|
||||
// 提取标题(第一行,移除 Markdown 格式)
|
||||
val title = extractTitle(comment.body)
|
||||
|
||||
// 转换 reactions 数据
|
||||
val reactions = comment.reactions?.let { r ->
|
||||
com.wrbug.polymarketbot.dto.ReactionsDto(
|
||||
plusOne = r.`+1`,
|
||||
minusOne = r.`-1`,
|
||||
laugh = r.laugh,
|
||||
confused = r.confused,
|
||||
heart = r.heart,
|
||||
hooray = r.hooray,
|
||||
eyes = r.eyes,
|
||||
rocket = r.rocket,
|
||||
total = r.total_count
|
||||
)
|
||||
}
|
||||
|
||||
return AnnouncementDto(
|
||||
id = comment.id,
|
||||
title = title,
|
||||
body = comment.body,
|
||||
author = comment.user.login,
|
||||
authorAvatarUrl = comment.user.avatar_url,
|
||||
createdAt = parseGitHubTime(comment.created_at),
|
||||
updatedAt = parseGitHubTime(comment.updated_at),
|
||||
reactions = reactions
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从评论内容中提取标题(第一行,移除 Markdown 格式)
|
||||
* 支持的 Markdown 格式:
|
||||
* - # 标题
|
||||
* - ## 标题
|
||||
* - ### 标题
|
||||
* - **粗体**
|
||||
* - *斜体*
|
||||
* - `代码`
|
||||
* - [链接](url)
|
||||
*/
|
||||
private fun extractTitle(body: String): String {
|
||||
if (body.isBlank()) {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 获取第一行
|
||||
val firstLine = body.lines().firstOrNull()?.trim() ?: ""
|
||||
if (firstLine.isBlank()) {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 移除 Markdown 格式
|
||||
var title = firstLine
|
||||
|
||||
// 移除标题标记(# ## ### 等)
|
||||
title = title.replace(Regex("^#{1,6}\\s+"), "")
|
||||
|
||||
// 移除粗体标记(**text** 或 __text__)
|
||||
title = title.replace(Regex("\\*\\*([^*]+)\\*\\*"), "$1")
|
||||
title = title.replace(Regex("__([^_]+)__"), "$1")
|
||||
|
||||
// 移除斜体标记(*text* 或 _text_)
|
||||
title = title.replace(Regex("(?<!\\*)\\*([^*]+)\\*(?!\\*)"), "$1")
|
||||
title = title.replace(Regex("(?<!_)_([^_]+)_(?!_)"), "$1")
|
||||
|
||||
// 移除代码标记(`code`)
|
||||
title = title.replace(Regex("`([^`]+)`"), "$1")
|
||||
|
||||
// 移除链接标记([text](url))
|
||||
title = title.replace(Regex("\\[([^\\]]+)\\]\\([^\\)]+\\)"), "$1")
|
||||
|
||||
// 移除图片标记()
|
||||
title = title.replace(Regex("!\\[([^\\]]*)\\]\\([^\\)]+\\)"), "$1")
|
||||
|
||||
// 移除删除线标记(~~text~~)
|
||||
title = title.replace(Regex("~~([^~]+)~~"), "$1")
|
||||
|
||||
// 移除引用标记(> text)
|
||||
title = title.replace(Regex("^>\\s+"), "")
|
||||
|
||||
// 移除列表标记(- * + 1. 等)
|
||||
title = title.replace(Regex("^[-*+]\\s+"), "")
|
||||
title = title.replace(Regex("^\\d+\\.\\s+"), "")
|
||||
|
||||
return title.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 GitHub 时间格式(ISO 8601)为时间戳(毫秒)
|
||||
* GitHub API 返回的时间格式:2025-12-07T14:30:00Z
|
||||
*/
|
||||
private fun parseGitHubTime(timeStr: String): Long {
|
||||
return try {
|
||||
Instant.parse(timeStr).toEpochMilli()
|
||||
} catch (e: Exception) {
|
||||
logger.warn("解析 GitHub 时间失败: $timeStr", e)
|
||||
System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,8 @@ class ApiHealthCheckService(
|
||||
async { checkGammaApi() },
|
||||
async { checkPolygonRpc() },
|
||||
async { checkPolymarketWebSocket() },
|
||||
async { checkBuilderRelayerApi() }
|
||||
async { checkBuilderRelayerApi() },
|
||||
async { checkGitHubApi() }
|
||||
)
|
||||
|
||||
jobs.awaitAll().forEach { result ->
|
||||
@@ -453,5 +454,14 @@ class ApiHealthCheckService(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 GitHub API
|
||||
*/
|
||||
private suspend fun checkGitHubApi(): ApiHealthCheckDto = withContext(Dispatchers.IO) {
|
||||
val url = "https://api.github.com/"
|
||||
// 直接使用 GitHub API 根端点检查可用性
|
||||
checkApi("GitHub API", url)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.wrbug.polymarketbot.api.BuilderRelayerApi
|
||||
import com.wrbug.polymarketbot.api.EthereumRpcApi
|
||||
import com.wrbug.polymarketbot.api.GitHubApi
|
||||
import com.wrbug.polymarketbot.api.PolymarketClobApi
|
||||
import com.wrbug.polymarketbot.api.PolymarketDataApi
|
||||
import com.wrbug.polymarketbot.api.PolymarketGammaApi
|
||||
@@ -201,6 +202,41 @@ class RetrofitFactory(
|
||||
.build()
|
||||
.create(BuilderRelayerApi::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 GitHub API 客户端
|
||||
* GitHub API 是公开 API,不需要认证(但建议使用 token 提高速率限制)
|
||||
* 添加 Accept 头以获取 reactions 数据
|
||||
* @return GitHubApi 客户端
|
||||
*/
|
||||
fun createGitHubApi(): GitHubApi {
|
||||
val baseUrl = "https://api.github.com"
|
||||
|
||||
// 添加拦截器,设置 Accept 头以获取 reactions 数据
|
||||
val githubInterceptor = object : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request().newBuilder()
|
||||
.header("Accept", "application/vnd.github+json")
|
||||
.build()
|
||||
return chain.proceed(request)
|
||||
}
|
||||
}
|
||||
|
||||
val okHttpClient = createClient()
|
||||
.addInterceptor(githubInterceptor)
|
||||
.build()
|
||||
|
||||
val gson = GsonBuilder()
|
||||
.setLenient()
|
||||
.create()
|
||||
|
||||
return Retrofit.Builder()
|
||||
.baseUrl("$baseUrl/")
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.build()
|
||||
.create(GitHubApi::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -86,3 +86,8 @@ rate-limit.reset-password.max-attempts=3
|
||||
# 时间窗口(秒)
|
||||
rate-limit.reset-password.window-seconds=60
|
||||
|
||||
# GitHub 配置(用于公告功能)
|
||||
github.repo.owner=WrBug
|
||||
github.repo.name=PolyHermes
|
||||
github.announcement.issue.number=1
|
||||
|
||||
|
||||
Reference in New Issue
Block a user