feat: 微信公众号技能包重命名(weixin_public_account)并增强功能

- 将 wechat_official_account 重命名为 weixin_public_account,符合中文命名规范
- 新增 DOCX 文档生成技能、聊天历史、请求 ID 中间件
- 增强工作流、热点服务、文章服务等模块功能
- 前端同步重命名组件和 API
- 新增架构文档 AR13/AR14、专员文档更新
- 补充测试用例(seed_specialists_test, db_migration_test)

Co-Authored-AI: yes
This commit is contained in:
eaiadmin
2026-09-24 21:13:19 +08:00
parent 593323a934
commit 89ae31c998
60 changed files with 7251 additions and 967 deletions
@@ -2,36 +2,117 @@ package web
import "net/http"
// AppError 业务错误(对齐 Python core/errors.py 的错误码与状态码)
// 错误分类(category)—— 表达展示策略、跳转策略、重试策略、监控聚类,使用小写 snake_case。
const (
CatValidation = "validation" // 参数不合法 / 表单缺失 / 格式错误
CatAuth = "auth" // 认证失败:未登录、会话过期、令牌无效
CatPermission = "permission" // 权限不足
CatNotFound = "not_found" // 资源不存在
CatConflict = "conflict" // 状态冲突:重复创建、并发更新
CatRateLimit = "rate_limit" // 限流:请求过快
CatDependency = "dependency" // 外部依赖错误:第三方 API、数据库、LLM
CatProviderQuota = "provider_quota" // 第三方额度不足:AI 积分耗尽
CatServer = "server" // 系统内部异常:未捕获、代码 bug
)
// 稳定业务错误码 —— 分段:1000 通用 / 2000 认证 / 3000 权限 / 6000 AI / 9000 系统。
// 同一语义稳定复用,不允许同一错误码被不同语义复用。
const (
CodeOK = 0 // 成功(response.go OK 信封使用)
CodeValidation = 1000 // 通用参数错误
CodeNotFound = 1001 // 资源不存在
CodeConflict = 1002 // 状态冲突 / 重复创建
CodeRateLimit = 1003 // 限流
CodeUnauthorized = 2000 // 未认证 / 会话过期 / 令牌无效
CodeForbidden = 3000 // 权限不足
CodeLLMNotConfigured = 6001 // LLM 未配置
CodeLLMRequestFailed = 6002 // LLM 请求失败
CodePointsExhausted = 6003 // AI 积分耗尽
CodeInternal = 9000 // 系统内部错误(未捕获异常兜底)
)
// 各分类默认的下一步操作建议(hint):message 回答"发生了什么",hint 回答"下一步怎么做"。
const (
DefaultHintValidation = "请检查输入内容、必填项和字段格式后重试。"
DefaultHintAuth = "请重新登录后再试。"
DefaultHintPermission = "请确认当前账号是否拥有相应权限,或联系管理员授权。"
DefaultHintNotFound = "请确认资源标识是否正确,或刷新列表后再试。"
DefaultHintConflict = "请刷新页面确认当前状态后,再重试操作。"
DefaultHintRateLimit = "操作过于频繁,请稍后重试。"
DefaultHintLLMNotConfig = "请先在系统配置中启用并配置 AI 模型路由后重试。"
DefaultHintLLMRequest = "请稍后重试;如持续失败,请检查 AI 服务商状态与网络链路。"
DefaultHintPointsExhaust = "请检查账户积分余额,或联系管理员补充后重试。"
DefaultHintInternal = "请稍后重试;如持续失败,请将请求编号反馈给开发排查。"
)
// AppError 统一业务错误模型:HTTP 状态码 + 稳定业务错误码 + 分类 + 用户提示。
// 对齐项目错误治理规范,为"协议语义(状态码) / 稳定编号(code) / 行为分流(category)
// / 用户可理解(message+hint) / 开发可定位(request_id+details)"四层模型。
type AppError struct {
StatusCode int
Code string
Code int
Message string
Category string
Hint string
Details any
RequestID string
}
func (e *AppError) Error() string { return e.Message }
// NewBadRequest 参数错误(保持旧签名,向后兼容,内部自动携带稳定 code/category/hint)。
func NewBadRequest(msg string) *AppError {
return &AppError{http.StatusBadRequest, "bad_request", msg}
return &AppError{http.StatusBadRequest, CodeValidation, msg, CatValidation, DefaultHintValidation, nil, ""}
}
// NewAuthError 认证失败(未登录 / 会话过期 / 令牌无效)。
func NewAuthError(msg string) *AppError {
return &AppError{http.StatusUnauthorized, "unauthorized", msg}
return &AppError{http.StatusUnauthorized, CodeUnauthorized, msg, CatAuth, DefaultHintAuth, nil, ""}
}
// NewForbiddenError 权限不足。
func NewForbiddenError(msg string) *AppError {
return &AppError{http.StatusForbidden, "forbidden", msg}
return &AppError{http.StatusForbidden, CodeForbidden, msg, CatPermission, DefaultHintPermission, nil, ""}
}
// NewNotFoundError 资源不存在。
func NewNotFoundError(msg string) *AppError {
return &AppError{http.StatusNotFound, "not_found", msg}
return &AppError{http.StatusNotFound, CodeNotFound, msg, CatNotFound, DefaultHintNotFound, nil, ""}
}
// NewConflictError 状态冲突 / 重复创建。
func NewConflictError(msg string) *AppError {
return &AppError{http.StatusConflict, "conflict", msg}
return &AppError{http.StatusConflict, CodeConflict, msg, CatConflict, DefaultHintConflict, nil, ""}
}
// NewLLMNotConfigured LLM 路由未配置。
func NewLLMNotConfigured(msg string) *AppError {
return &AppError{http.StatusNotImplemented, "llm_not_configured", msg}
return &AppError{http.StatusNotImplemented, CodeLLMNotConfigured, msg, CatDependency, DefaultHintLLMNotConfig, nil, ""}
}
// NewLLMError LLM 请求失败(外部依赖)。
func NewLLMError(msg string) *AppError {
return &AppError{http.StatusBadGateway, "llm_request_failed", msg}
return &AppError{http.StatusBadGateway, CodeLLMRequestFailed, msg, CatDependency, DefaultHintLLMRequest, nil, ""}
}
// NewPointsExhausted AI 积分耗尽。
func NewPointsExhausted(msg string) *AppError {
return &AppError{http.StatusPaymentRequired, "ai_points_exhausted", msg}
return &AppError{http.StatusPaymentRequired, CodePointsExhausted, msg, CatProviderQuota, DefaultHintPointsExhaust, nil, ""}
}
// NewServerError 系统内部错误(未捕获异常兜底)。
func NewServerError(msg string, details any) *AppError {
return &AppError{http.StatusInternalServerError, CodeInternal, msg, CatServer, DefaultHintInternal, details, ""}
}
// WithHint 覆写提示语(链式),用于需要给出更具体下一步建议的场景。
func (e *AppError) WithHint(hint string) *AppError {
e.Hint = hint
return e
}
// WithDetails 附带结构化开发上下文(链式),禁止塞大段日志/堆栈。
func (e *AppError) WithDetails(details any) *AppError {
e.Details = details
return e
}
@@ -1,17 +1,78 @@
package web
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
)
// OK 成功响应信封 {"data": ..., "error": null, "message": "success"}
func OK(c *gin.Context, data any) {
c.JSON(http.StatusOK, gin.H{"data": data, "error": nil, "message": "success"})
// CtxRequestID Gin Context 中存放 request_id 的键(由 middleware.RequestID 写入)。
const CtxRequestID = "req_id"
// RequestIDOf 从 Gin Context 读取本次请求的 request_id(无则返回空串)。
func RequestIDOf(c *gin.Context) string {
if v, ok := c.Get(CtxRequestID); ok {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
// Fail 错误响应信封 {"data": null, "error": ..., "message": ...}
// OK 成功响应信封 {"code":0,"message":"success","data":...,"error":null}
func OK(c *gin.Context, data any) {
c.JSON(http.StatusOK, gin.H{
"code": CodeOK,
"message": "success",
"data": data,
"error": nil,
})
}
// Fail 错误响应信封,统一输出四层模型:
//
// {"code": 稳定业务错误码, "message": 用户主提示, "data": null,
// "error": {"category":分类, "status":HTTP状态码, "hint":下一步建议, "request_id":追踪编号, "details":开发上下文}}
//
// 并回写 X-Request-ID 响应头,便于前后端与日志联查。
func Fail(c *gin.Context, e *AppError) {
c.JSON(e.StatusCode, gin.H{"data": nil, "error": e.Code, "message": e.Message})
requestID := RequestIDOf(c)
e.RequestID = requestID
c.Header("X-Request-ID", requestID)
errorObj := gin.H{
"category": e.Category,
"status": e.StatusCode,
"hint": e.Hint,
"request_id": requestID,
}
if e.Details != nil {
errorObj["details"] = e.Details
}
logFail(c, e)
c.JSON(e.StatusCode, gin.H{
"code": e.Code,
"message": e.Message,
"data": nil,
"error": errorObj,
})
}
// logFail 记录结构化错误日志,满足可定位要求:request_id / method / path / status / code / category。
func logFail(c *gin.Context, e *AppError) {
if e == nil {
return
}
req := c.Request
method := ""
path := ""
if req != nil {
method = req.Method
path = req.URL.Path
}
log.Printf("HTTP ERROR [%s] status=%d code=%d %s %s -> %s request_id=%s",
e.Category, e.StatusCode, e.Code, method, path, e.Message, e.RequestID)
}