Blizzard
5b871bc363
feat(voice): 火山协议自检工具 voicecheck(本地端到端验协议,免麦克风免全栈)
...
不用部署、不用麦克风:本机直连火山公网端点,端到端验证手搓的 ASR/TTS 帧协议。
TTS 合成一句→收音频存 out_tts.wav→降采样 24k→16k 喂 ASR→打印转写。
转写≈原句即证明两套协议都被真火山接受。凭证只走环境变量(VOLC_API_KEY 等),不进 git。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-22 09:33:55 +08:00
Blizzard
6fe0a58f1b
feat(voice): 火山双向流 TTS 客户端 + 下行接线(Phase 1 嘴巴)
...
回答 token 流 → 攒句器 → 火山双向 TTS → 音频帧回推客户端,端到端连续朗读打通。
- tts_frame.go: V3 事件族帧编解码(事件号+会话ID+gzip),与 ASR 简帧不同族;
从官方参考实现核实 generate_header/parse_response 字节布局;3 解析单测
- tts.go: seed-tts-2.0 双向流客户端 StartTTS(握手ConnectionStarted/SessionStarted)
/Speak(逐句TaskRequest)/Finish/Audio()/Close,PCM 24k;新版 API Key 鉴权
- voice_tts.go: speak() 先订阅token流再建TTS(core NATS无持久,握手期攒句入pending
就绪补吐,不丢开头);音频泵首帧ServerSpeaking、收尾ServerTTSEnd;打断stopTTS
- voice.go: barge_in→stopTTS;会话结束连带停TTS;final转写→go speak(taskID)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-22 09:25:49 +08:00
Blizzard
302e1ebaff
feat(voice): 上行接线——最终转写→组DSL→提交任务(Phase 1 打通)
...
ASR 最终转写触发一次任务提交,复用 HTTP SubmitTask 那条共用关卡
(preflightCore/launchCore),语音只是"嘴替键盘",编排/工具/计费一行不新造。
- task_handler.go: preflight/launch 抽出无 gin 内核 preflightCore/launchCore
(preflightBlock 承载拦截态),gin 版做薄封装;语音会话无 gin.Context 也走同一关卡
- voice_task.go: buildVoiceGraph(转写→input→agent 单图) + submitVoiceTask(校验/关卡/落库发射)
- voice.go: 会话升级时抓租户/会话;结果 goroutine 见 Final→onFinalTranscript 提交、
回 ServerTask{task_id};去重连发的重复 final;画布图一次性消费
- voice_task_test.go: 组图合法性 + 带转写 + input→agent 连边
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-22 09:16:29 +08:00
Blizzard
4ee4a91a51
feat(voice): 火山 ASR 流式识别客户端 + 会话接线(Phase 1 耳朵)
...
voice/asr.go: 连 wss://openspeech.bytedance.com/api/v3/sauc/bigmodel,新版 API Key 鉴权
(Authorization: Bearer + X-Api-Resource-Id + Connect-Id),发初始配置帧(bigmodel/zh/ITN/
标点/VAD),PushAudio 流式喂 PCM、Finish 收尾;读 goroutine 解析响应(result 支持数组/对象/
字符串,type=final 为最终)推入 Results 通道。
handler/voice.go 接线:onAudio→PushAudio、start→重开识别、end→Finish;起 goroutine 把
转写 send(transcript) 实时回推客户端。加 writeMu 串行化写(读循环与 ASR 结果 goroutine
都写同一 WS,gorilla 禁并发写)。连接结束 stopASR 收尾。
带单测(配置JSON字段/result三形态解析)。真识别需部署联调(要真连火山);API Key 是否
还需 X-Api-App-Key 联调若 401 再补。上行接线(final→SubmitTask)与 TTS 是下一步。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-21 17:27:32 +08:00
Blizzard
b526b21dee
feat(voice): 火山语音 WS 二进制帧编解码(ASR/TTS 共用核心)
...
从官方参考实现核实的 V3 协议帧格式,港到 Go:byte0=0x11、byte1=(msgType<<4)|flags、
byte2=序列化<<4|压缩、byte3=0x00、大端 uint32 payload 长度、payload;服务端响应含 4B
序列号故 payload 从第 12 字节起。encodeFrame/jsonFrame/audioFrame/parseServerFrame +
msgType(0x01 config/0x02 音频/0x09 结果/0x0F 错误)与 flags(0x02 最终)常量。
3 个单测钉死头布局/音频最终帧/响应解析(确定性,不依赖网络)。
ASR 初始配置 JSON、WS 连接与流式识别(接 onAudio)是下一步网络层实现。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-21 17:22:09 +08:00
Blizzard
15f5a85612
feat(voice): WebSocket 端点 + 客户端↔网关协议(Phase 1 地基)
...
加 gorilla/websocket(无 genproto 冲突)。voice/protocol.go 定死单条 WS 的消息协议:
二进制帧=音频(上行麦克风/下行TTS),文本帧=JSON 控制/事件(ClientMsg:start/end/barge_in/
bye;ServerMsg:ready/transcript/task/speaking/tts_end/error);音频 PCM 16k 单声道。
handler/voice.go: GET /api/v1/voice/stream 升级 WS,鉴权走 AuthFromHeaderOrQuery(?token=,
WS 带不了 Bearer),会话外壳 + 协议读循环(音频帧/控制消息分派)已通,火山 ASR/TTS 客户端
在下一步挂 onAudio/onControl 的 TODO 点接入。build+vet+test 绿。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-21 17:17:58 +08:00
Blizzard
a28ae49b6a
refactor(voice): 语音配置改用新版 API Key 鉴权(弃旧版 appid+token)
...
用户提醒:火山新版走 API Key 鉴权,不用旧版 appid+access_token。新版 WS 握手只带两个
header——Authorization(Bearer <APIKey>) + X-Api-Resource-Id。
配置从 {appid, access_token, 2×resource-id, voice} 收敛为 {api_key, 2×resource-id,
voice}:APIKey 走 secrets AES 加密入库;ASREnabled/TTSEnabled 改为只看 api_key+对应
resource-id。admin 配置页两个字段并一个 API Key 字段,清单同步改为新版口径。build+tsc 绿。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-21 17:06:22 +08:00
Blizzard
618184689e
feat(voice): 语音配置控制面——火山豆包 appid/token/resource-id/音色(参数落地处)
...
JARVIS 不依赖火山账号能先做的第二块:配置层,做成'你参数一填就落库'的形态。
- voice.Config: AppID/AccessToken/ASRResourceID/TTSResourceID/TTSVoiceType,AccessToken
走 secrets AES 加密入库(镜像微信配置);ASREnabled/TTSEnabled 可各自独立判断。
- admin GET/PUT /admin/voice(RequireAdmin),token 空串=沿用已存、明文回显。
- admin「语音设置」页(运维组):五个字段 + ASR/TTS 就绪徽标 + '去哪拿参数'清单。
端点/音频格式(PCM 16k 单声道)由代码固定不入配置。WS 会话 + 火山 ASR/TTS 客户端等
用户回传 resource-id 后按官方 demo 协议做。build+tsc 绿。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-21 17:00:35 +08:00
Blizzard
644b635d73
feat(voice): 攒句器——语音下行 TTS 的 token 攒句(JARVIS 地基第一块)
...
语音交互(VOICE_DESIGN.md)不依赖火山账号的第一块地基:SentenceBuffer 把 LLM 逐 token
输出攒成'适合喂 TTS 的句子片段'。逐字喂 TTS 太碎(单字合成不自然、首包延迟高);句末标点
(。!?.!?;;换行)即成一句,从句标点(,,::)且攒够 12 rune 也吐(让长回答尽早出声)。
跨 Push 续半句,Flush 收尾吐无标点结尾。纯逻辑无外部依赖、4 个单测(跨 Push/短从句不断/
长从句先吐/Flush 收尾)。
WS 会话 + 火山 ASR/TTS 客户端 + 控制面配置等到真 API 参数到手再按真形状做,不写猜测桩。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-21 16:30:50 +08:00
Blizzard
f16f63284f
feat(cluster): gateway 后台定时器加 leader 锁——多副本安全(B5)
...
此前两个 gateway 定时器(订阅推进/掉单补偿)每实例各扫一遍:多副本下重复查库,
且对微信查单调用量随副本线性放大(微信有频控,先被限流的是自己)。
store.TryRunExclusive:用 PG advisory try-lock 做集群级单实例执行(leader 选举)——每轮
tick 非阻塞抢锁,抢到才跑、跑完释放;抢不到说明别的实例是 leader、本轮跳过。自愈:锁随
持有连接释放,leader 挂了下一轮别的实例自然抢到接管,无需显式故障转移。订阅/补偿用不同
锁键(可由不同实例分别 lead)。非 PG(sqlite 测试)/无 DB → 退回本地直接跑(单实例安全)。
两个定时器的 tick 各包一层 TryRunExclusive。至此 gateway 可安全多副本(SSE 走共享 Redis
流无需粘性、状态全外置、JWT 无状态,剩数据层 HA 属 C 层 ops)。带 fallback 单测。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-21 16:21:31 +08:00
Blizzard
9b153871eb
feat(monitor): NATS 集群 Raft 副本健康 + 基建 ping 延迟(监测组完善)
...
此前 /status 把 NATS 当一盏二元灯(连不上就 fatal 故恒真),看不出 3 节点集群里
哪个节点掉了、JetStream 持久流的 Raft 副本是否还齐(计费/状态/评测流不丢的关键)。
DB/Redis/MinIO 也只二元 ping、无延迟。
- bus.ClusterStatus:连的节点名 + 集群发现节点数(nc.Servers) + RTT(nc.RTT) + 6 条关键
持久流(tasks/status/usage/eval/ingest/approvals)的 Raft 副本健康(leader + healthy/total,
单节点部署记 1/1;某节点掉队 → healthy<total 记降级)。
- /status 新增 nats 集群对象 + NATS 灯改为'连接且无副本降级才绿'、detail 显示'N 节点·连 X·
流副本齐全/降级'、latency=RTT;PG/Redis/MinIO 加 ping 往返耗时。
- admin StatusPage:新增 NATS 集群面板(节点/RTT/连接 + 各流 leader/副本健康/消息数表),
基建行显示延迟。
sundynix-shared/gateway build+vet+test 绿;admin tsc 绿。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-21 16:15:22 +08:00
Blizzard
a42f30239f
fix(payment): 微信 API 调用加超时兜底(B3 出网韧性)
...
微信是外部第三方、最可能慢/挂,而 wechatpay-go SDK 默认 http.Client 无 Timeout:
一次卡住的 Prepay/QueryOrder 会拖住请求 goroutine;尤其掉单补偿定时器用的是
context.Background()(无超时)→ 微信一挂那轮 tick 无限期卡死。
双保险:客户端级 HTTP 超时(WithHTTPClient Timeout=15s,belt) + CreatePay/QueryOrder
每次调用 ctx 超时(suspenders,兜住 SDK 忽略或背景 ctx 的情况)。
范围克制:只硬化微信这个真外部依赖。PG/Redis(连接池+降级)、NATS(无限重连)、mcp-go RAG
客户端(embed 30s/chat 60s/rerank 20s 已有超时)本就有韧性,不额外套熔断(那对本规模是过度设计)。
build+vet+test 绿。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-21 16:01:09 +08:00
Blizzard
ebbeed90e9
feat(auth): 登录账户级失败锁定 + 密码强度 8-72 位(B4)
...
补上 A5 只做了 IP 限流、没做账户锁定的缺口(审计 B4)。
- 账户级失败锁定:连续登录失败达 5 次即临时锁定 15min(Redis 计数+TTL),锁定期直接拒绝
不再校验密码,成功登录即清零。与 A5 的 IP 限流互补:IP 限流挡'一个 IP 猛打',账户锁定
挡'分布式慢速猜一个号'。权衡:per-account 可被人为锁死受害者(lockout DoS),故窗口设短
自动解锁+叠加 IP 限流;Redis 降级则不锁定(尽力而为,IP 限流仍在)。
- 密码强度:注册从'至少 6 位'提到 8-72 位。上限 72 字节——bcrypt 超 72 字节静默截断,
不拦会让超长密码实际只用前 72 字节。桌面端注册提示同步改 8-72 位。
存量用户不受影响(只校验注册/改密时的新密码)。build+vet+test 绿;desktop tsc 绿。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-21 15:56:29 +08:00
Blizzard
da9c76f073
feat(prod): 实时就绪探针——gateway /readyz 真 ping + dispatcher/mcp-go 加 HTTP 探针(B2)
...
此前 gateway /readyz 用的是 Enabled() 启动期降级标志(反映不了运行中 PG 掉线,LB 会继续
往已不可用实例导流);dispatcher/mcp-go 干脆没有 HTTP 探针(只有 NATS ServeHealth,k8s/LB
够不着、只能靠 gateway 经 NATS 代探)。
- gateway /readyz 改用 db.Ping 实时探活。只把 **DB 当硬依赖门**:Redis 掉线仍可服务(限流有
A5 进程内 fail-safe 兜底、SSE 回落 live NATS),一 blip 就把全部实例踢出轮转反而制造整站
故障,故 Redis 只上报不 gate。NATS 启动即连(fatal)不单列。
- 新 sundynix-shared/health 包:Serve/Handler 提供 /healthz(恒 200 liveness) + /readyz(由
ready() 决定 readiness)。dispatcher(:8091, DISPATCHER_HEALTH_ADDR)与 mcp-go(:8092,
MCP_GO_HEALTH_ADDR)各起一个,readiness = NATS 连接可用(bus.IsConnected)。接入各自优雅停机。
- bus 加 IsConnected()(nc.IsConnected 实时);dispatcher Subscriber 透传。
四模块 build+vet+test 绿;health 带 Handler 单测。探针端口仅内部用(对外仍只暴露 gateway)。
compose/k8s 的 healthcheck 编排属 C 层 ops,另做。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-21 15:50:31 +08:00
Blizzard
4500335da7
refactor(store): 迁移路径务实硬化——advisory lock + 版本表 + 破坏性迁移移出启动路径(B1)
...
此前每次启动裸跑 AutoMigrate + 一串手写索引/回填,且启动路径上有两处 DROP TABLE
CASCADE 的 legacy 迁移;多实例并发启动无锁 → 并发 ALTER/建索引竞争,一方报错即掉降级。
务实硬化(不引外部工具,保留 gorm 结构体为源):
- PG advisory lock:整段迁移在 pg_advisory_lock 内串行,多实例同时启动只有一个进锁跑,
其余阻塞等待。取锁 60s 超时兜底(取不到带告警继续,AutoMigrate/索引多幂等)。
- 破坏性 legacy 迁移移出默认路径:migrateLegacyIntIDs/migrateDocLinkToID(DROP TABLE
CASCADE)默认不跑,仅 ALLOW_LEGACY_SCHEMA_MIGRATION=1 时执行;检测到旧 schema 但未开
只告警不动手。现网早已是雪花 id 本就不触发,但从此不再是启动就可能 DROP。
- 版本化 runner:AutoMigrate 之外的步骤(3 个部分唯一索引 + NULL 余额回填)登记为
schemaSteps,各跑一次并记入 sundynix_schema_migration 表,下次跳过;某步失败即停、
不记录、下次重试。将来 AutoMigrate 做不了的破坏性/数据迁移在末尾追加新 id 即可。
pgsql.go 的迁移块收敛为一句 runMigrations(db)。带 runner 单测(跑一次/跳过/追加/失败即停)。
现网 DB 已有的索引/回填重跑无害(IF NOT EXISTS / WHERE IS NULL),跑后记录。build+vet+test 绿。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-21 15:26:58 +08:00
Blizzard
ac38d5e663
fix(prod): 后端生产级 A 类硬伤全清(7 项:授权/崩溃点/全表扫/限流/鉴权/限额)
...
部署前生产级审计(可靠性/数据层/安全三路)后,清掉 7 处代码级硬伤:
A1 后台定时器 goroutine 无 panic recover → 单个 DB panic 崩整个 gateway。加 safeGo/
safeCall,包住订阅/掉单补偿/微信推送/探针 goroutine,单轮 tick 再兜一层。
A2 提示词控制面(建/激活/停用,热广播全服务)只 RequireAuth → 任意登录用户改全局提示词。
三写端点+列表挂 RequireAdmin。
A3 HITL 审批端点无角色门 → viewer 可放行烧钱执行。加 RequireTenantRole(member)。
A4 审计/护栏列表 limit 无校验,limit=-1 让 gorm 取消 LIMIT 全表扫。加 clampLimit/
clampOffset,AdminTasks/AdminSpaces 补上界。
A5 限流 Redis 一挂就完全放行(fail-open)。加进程内固定窗口兜底(fail-safe) + 登录/注册
按 IP 专用严限流(10/min)。
A6 公开 by-id 端点(stream/exec/report导出/kb导入流)无鉴权无租户过滤。加
AuthFromHeaderOrQuery(从 ?token= 取 JWT) + task/report 按 owner 归属校验;桌面端
5 处 EventSource/下载 URL 经 tokenQuery 附 JWT。
A7 文件上传无大小上限(整文件进内存 OOM 面) → 50MB 闸(KB_MAX_UPLOAD_BYTES)+ LimitReader;
http.Server 加 ReadHeaderTimeout/ReadTimeout/MaxHeaderBytes(不设 WriteTimeout 保 SSE)。
带单测:clampLimit/safeCall/procLimiter/AuthFromHeaderOrQuery/TaskOwner。
build+vet+全量 test 绿;desktop tsc 绿。B(迁移工具/实时探针/出网韧性/登录锁定/leader选举)
与 C(TLS/PG HA/K8s/备份自动化/可观测)分期后做,参照 production_readiness.md。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-21 15:04:47 +08:00
Blizzard
ce7cca657e
fix(backend): 补齐三处漏洞——暂停租户拦截/金额不符落审计终态/邀请码列表滤失效
...
P0 暂停租户是空开关:admin 能设 suspended,但 preflight 只查预算+余额、不看
租户 status → 暂停后照样能提交烧积分。加 TenantSuspended 校验(活跃租户 + 分叉时
的计费租户都拦),403 拒绝。
P1 金额不符只刷日志:回调/查单判了不符却没落审计、订单永远卡 pending 被补偿定时器
每轮重扫刷屏。加 disputed 终态 + MarkOrderDisputed(CAS 只挂一次) + 审计(首次写一次);
disputed 不在 pending 扫描内,停止无限重扫。admin /orders?status=disputed 可查。
P1 邀请码列表混入失效码:ListInvites 只按 status=active 过滤,过期/满员的码仍显示为
有效、误导邀请人。有效列表加 expires_at>now 且 used<max 过滤(RedeemInvite 本就会拒,
这里修的是展示一致性)。
三处均带 store 单测(TenantSuspended/MarkOrderDisputed CAS/ListInvites 过滤)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-21 14:20:34 +08:00
Blizzard
bd829cfecb
feat(invite): 租户成员二维码邀请(可复用团队码,扫码关注即入组)
...
owner/admin 在桌面端生成一张微信带参二维码发给团队,成员用微信扫码关注
即自动加入租户,并收到「✅ 已加入团队【X】」被动回复。与登录二维码同一微信机制,
scene 加 inv_ 前缀分流;扫码入组走被动回复,不需要 access_token、不碰 IP 白名单。
- store: TenantInvite(可复用码=有效期+人数上限+可撤销三道闸);RedeemInvite 幂等入组、
同一人重复扫不重复消耗名额、复活已移除者;带单测钉死过期/撤销/满员/幂等。
- handler: 建码/列表/撤销端点(RequireTenantRole admin);WxMPEvent inv_ 分支。
- 顺手修潜在生产 bug:微信用户此前都建成空邮箱,User.Email 整列唯一索引下第二个微信
用户就撞唯一约束建号失败(登录"全通"只因当前仅一个微信用户)。改为按 openid 合成占位
邮箱 wx-<openid>@wx.local,绕开冲突且不动索引。邀请功能会批量建微信用户,非修不可。
- desktop: 顶栏「邀请成员」入口(租户 admin/owner 可见) + 二维码弹窗(生成/大图展示/撤销)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-21 11:47:21 +08:00
Blizzard
68608c1592
feat(wechat): 支付成功后微信客服消息推回执(积分包/订阅购买)
...
用户扫码付款后(在 48h 互动窗口内),主动推一条客服消息回执:
到账积分/开通套餐 + 当前余额。只在 MarkOrderPaid changed=true 首次到账时推,
异步+超时隔离,失败只记日志、绝不影响入账。非微信用户自动跳过。
不做「周期刷新提醒」:客服消息受 48h 窗口限制,定时刷新那刻用户多半已超窗、
必然失败,那类隔天提醒须用模板消息(暂缓)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-21 11:23:41 +08:00
Blizzard
83269e067a
feat(wechat): 关注公众号后自动回复欢迎语(被动回复,可后台配置)
...
用户关注服务号(扫登录码后关注 或 直接搜索关注)时,在回调 HTTP 响应里
回一条文本消息(微信「被动回复」)。被动回复不需要 access_token、不受 IP 白名单
限制,永远能发。欢迎语在管理端「登录设置」可配,留空用默认。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-21 11:17:41 +08:00
Blizzard
0f2afdaac1
feat(admin): 微信用户观测页 + 唯一昵称 + 密钥明文回显(单管理员后台)
...
按需求四项:
1. 微信新用户昵称改「微信用户_XXXXX」:后缀从 openid 的 sha1 派生(去混淆字母表),
openid 唯一 → 后缀实际不重复,且同 openid 每次一致(重登不换名)。用户可自行改名。
2. 后台加「平台 → 微信用户」列表:昵称 / openid(点击复制)/ 积分余额 / 加入时间。
每人仍独立租户与积分(各买各的,确认过不共享积分池),此页只做统一观测。
3. 登录设置的 AppSecret 明文回显(不再只显示"已保存")。
4. 支付配置的 APIv3 密钥明文回显。
—— 用户明确后台单人使用、RequireAdmin 已拦,接受这一安全降级;密文仍加密入库。
修一个 gorm Scan 坑:WechatUserRow 的 WechatOpenID/BalanceMicro 没加 column tag,
gorm 把 WechatOpenID 断成列名 wechat_open_id,与 SQL alias wechat_openid 对不上 →
openid 静默返回空。教训:Scan 到自定义结构 + SQL 用 alias 时,字段一律显式加 column tag。
本地验证:真库造两个微信用户,列表接口正确返回 openid(修 tag 前是空);
登录设置页 AppSecret 已是可见文本框;nickname 单测覆盖唯一/稳定/去混淆。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-21 10:36:27 +08:00
Blizzard
a1c68ec6b2
feat(auth): 微信 access_token 走中控服务器(腾讯云静态 IP 换 token)
...
隐患:gateway 换 access_token 时微信看到的是本地宽带出网 IP(106.58.232.43,
动态会变),一变白名单就失效、二维码建不出来(40164)。
按微信官方推荐的「中控服务器」架构解决:腾讯云静态 IP 统一换 token,gateway 拉取
使用。微信 IP 白名单只限制换 token 这一步,拿 token 建二维码不查 IP —— 所以
换 token 挪到中控、建二维码仍在 gateway 本地,白名单只填腾讯云 IP,永不失效。
- wechat.PullToken:从中控 HTTPS 端点拉 token(Bearer 密钥鉴权),坏响应/403/空
token 一律报错不当成功;中控没给 expires_in 时按 7200 兜底。
- handler.accessToken:配了 WECHAT_TOKEN_URL 就只从中控拉、绝不自己 FetchAccessToken
(微信要求单点刷新,多点各自换会互相顶掉 token);不配维持直连,零副作用可回退。
- compose:gateway 加 WECHAT_TOKEN_URL / WECHAT_TOKEN_SECRET(从宿主机 .env 注入)。
- deploy/wechat-token-zhongkong.md:腾讯云侧 cron 脚本 + nginx 配置 + 切换验证步骤。
本地验证(假中控 httptest + 真 gateway):建票时 Redis 缓存的是中控给的 token
(FAKE_TOKEN_FROM_ZHONGKONG),gateway 未直连微信换 token;随后拿该 token 调
qrcode/create(假 token 报 40001 属预期)——证明「中控换 token → 本地建二维码」链路成立。
4 组 PullToken 单测覆盖正常/403/坏JSON/兜底。
真机验证(部署后):把本地 IP 从白名单删掉、只留腾讯云 IP,扫码仍能登录即坐实
qrcode 不受 IP 限制;若建二维码报 40164 则退回 tinyproxy 正向代理备选。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-21 09:53:27 +08:00
Blizzard
883540bd7e
feat(auth): 微信扫码登录改为「带参二维码 + 关注/扫码事件」(登录即涨粉)
...
上一版做成了网页授权(OAuth 允许页),方向错了。改成用户要的流程:
扫码 → 弹公众号关注页 → 关注即登录,服务号顺带涨粉。
流程:PC 建票 → 后端用 access_token 调「带参数二维码」接口(scene=ticket) →
展示微信二维码图 → 用户扫码关注 → 微信推 subscribe/SCAN 事件到 /wx/mp/callback →
按 openid 找/建用户 → ticket 置 authorized → PC 轮询拿 JWT。
明文模式(消息加解密):回调只验签名 sha1(sort(token,ts,nonce)),不做 AES。
关键实现点:
- access_token 缓存进 Redis(跨实例共享,避免重复拉取互相失效)+ 进程内锁双检;
- 事件同时处理 subscribe(未关注,EventKey 带 qrscene_ 前缀)与 SCAN(已关注,不带);
- 事件回调必须验签——否则任何人 POST 一个 openid 就能登录别人;
- 回调无论如何回 "success",否则微信重试并给用户弹"公众号故障";
- User.wechat_openid 用部分唯一索引(WHERE <> ''),避开存量空串互撞。
配置(appid/secret/token)后台可改、secret AES 加密入库。管理端「运维 → 登录设置」
列出还需在公众平台做的事(服务器 URL / Token 一致 / 明文模式 / IP 白名单)。
本地验证(真流程,非 mock):验签回 echostr 与微信算法一致;模拟 subscribe 事件
→ 建号 + 置票 → PC 轮询拿到 token+user → 库里确有该 openid 用户。真微信推真事件
留待部署后扫码。前端 web 登录页加「微信扫码/邮箱」双 tab,默认微信。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-21 08:58:31 +08:00
Blizzard
07955ddf07
feat(auth): 微信扫码登录后端 —— 网页授权 + ticket 轮询
...
服务号「植趣 ZeeQ」已认证,走网页授权(snsapi_base,只拿 openid、用户无感),
不接管消息推送,副作用最小。
流程:PC 建 ticket → 二维码指向 /wx/mp?t= → 用户微信扫码 → 302 到微信授权页 →
回调 /api/v1/wx/mp/callback 用 code 换 openid → 找/建用户 → ticket 置 authorized →
PC 轮询 /wx/mp/poll 拿到 authorized → 签发 JWT。ticket 一次性消费防重放。
- 配置(appid/secret/base_url)后台可改,secret AES 加密入库,与微信支付同一套 secrets;
- ticket 存 Redis(短 TTL),无 Redis 时回退进程内内存(本地单实例可用,生产必须有 Redis);
- User 加 wechat_openid。**部分唯一索引**(WHERE openid <> '')而非普通唯一:
存量邮箱用户该列是空串,普通唯一索引会让多个空串互撞、AutoMigrate 直接失败
—— 与之前 NULL 余额同类的坑,这次提前避开。
单测覆盖:授权 URL 拼接(含 #wechat_redirect 锚点必须在末尾)、secret 加密往返、
建号/查号、空 openid 不误命中存量用户。微信 API 调用依赖公网回调,本地测不了,
留待部署后真机扫码。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-21 08:44:00 +08:00
Blizzard
a8a497b4ce
feat(gateway): 支持微信域名校验文件(为网页授权登录铺路)
...
微信公众平台配置「JS接口安全域名 / 网页授权域名」时会下发 MP_verify_xxx.txt,
要求能从域名根目录直接访问。文件放宿主机 /home/workspace/wechat-verify,
只读挂进容器(不进镜像、不进 git —— 它随时可能重发,且属站点凭证类文件)。
未设 WECHAT_VERIFY_DIR 时这段完全不生效。
**第一版写成了独立路由 `/:mpfile`,直接把官网打挂**:它匹配所有单段路径,
于是 /pricing、/download 全变 404(实测确认)。改为并进 NoRoute 的 SPA 兜底里,
在"已排除 /api/ 与内嵌静态文件"之后、回退 index.html 之前处理。
文件名白名单:必须 MP_verify_ 前缀 + .txt 后缀、不含路径分隔符与 ..,
否则落 SPA 兜底 —— 避免把挂载目录变成任意文件下载口子。
回归验证:/ /pricing /download /admin 均 200,校验文件取到正确内容,
/passwd.txt 与路径穿越都只拿到 index.html。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-20 17:20:02 +08:00
Blizzard
b1fea23a0c
feat(site): 官网定价页 —— 套餐由后台配置驱动,公开可见
...
官网加「定价」菜单与页面。数据来自 GET /api/v1/pricing(**不挂鉴权**):
运营改价、上下架、调发放节奏都在管理端完成,官网跟着变,不用发版;
未登录就能看到价格,这是转化的前提,也是官网存在的意义。
接口只吐在售项与展示字段,成本、权重、租户信息一律不出去。
结账仍在 Web 面完成(那里有登录态、租户上下文与支付轮询),官网只负责
「看价 → 去买」。同一套支付流程不在两处各实现一遍——这也是本次订阅开发
一直遵循的那条线:积分包与订阅共用一个支付弹窗,桌面端不重复购买入口。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-20 16:59:58 +08:00
Blizzard
f8f7359723
feat(admin): 订阅管理页 + 修「余额列为 NULL 导致充值永不到账」
...
管理端「支付 → 订阅」:套餐配置 + 全平台订阅观测。配置时直接算出「一个周期
发几次、合计多少积分」,时长不能被间隔整除时橙字提示到期前会有空档 —— 让人在
配的时候就看见后果,而不是上线后才发现只发了一次。
顺带修了个真 bug,是拿真库验订阅时撞出来的(本地 42 个租户里 11 个中招):
credit_balance_micro 是后加的列,早于它创建的租户行值为 NULL。而入账语句是
「余额 + N」—— SQL 里 NULL + N 仍是 NULL,于是这些租户**充值永远不到账**:
分录照写、余额不动、不报错。这条路径是充值/兑换码/退款/扣费/订阅发放共用的,
不是订阅引入的问题。
三处修:
- 5 处余额增减一律改 coalesce(credit_balance_micro, 0),新写入自愈;
- 启动迁移回填存量 NULL(按账本求和,让「余额 = SUM(ledger)」重新成立);
- 模型只加 default:0,**刻意不加 not null** —— 存量库有 NULL 行,AutoMigrate
尝试 SET NOT NULL 会直接失败,而且它在回填之前跑,等于把部署搞挂。
回归测试先证明能失败(去掉 coalesce → 余额 0)再确认修复。第一版测试因为我给
模型加了 not null 而无法造出 NULL,恰好暴露了上面那个部署风险。
真库验证:回填后 42 个租户 0 个 NULL;那个"有分录但余额 NULL"的租户余额
2981 = 账本合计 2981.34。管理端页面显示真实订阅(已发放 3 次 = 首笔 + 补发 2 笔)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-20 16:28:32 +08:00
Blizzard
80d4aaf4ba
feat(billing): 订阅制后端 —— 手动购买 + 周期发放积分 + 到期失效
...
规格(按需求):用户扫码买一个订阅周期,有效期内每 N 天发一次积分,到期即失效,
不自动续费。N 与每次发放额度都在套餐里配,后台可改。
为什么不做自动续费:微信 Native 扫码支付没有代扣能力,真自动续费要走「委托代扣」
——另一套产品与资质。与其假装有,不如把"到期即失效"这个语义做扎实。
两个决定,都写进了代码注释:
- **发放语义是累加而非重置**。每次刷新写一条 grant 分录、余额累加。重置型
(月度配额清零)会让「余额 = SUM(ledger)」这条对账不变量变复杂,且有误清
用户自费积分的风险。
- **订阅开通放在 store.MarkOrderPaid 内**,而不是各调用方。回调与掉单补偿两条
路都经过它,放这一处才没人能漏掉;按 orderID 幂等,重复调用无害。
复用而非另造:订阅单与积分包单走同一条支付链路(下单/回调/查单/掉单补偿),
只是 kind=sub 且 credits_micro=0——积分不在付款时给,由订阅按周期发。
定时器每 10 分钟扫一轮,语义与幂等都在 store.TickSubscription 里,与手动触发
共用,不会两处漂移。停机期间欠下的发放会一次性补齐。
8 组测试。其中一条当场抓到真 bug:开通时原本无条件发一笔,同一订单重复开通
(回调重推/查单赛跑)会因序号自增绕过幂等索引,白送积分。改为开通也走与定时器
同一套排期判断——排期天然幂等。教训:幂等键要锚在业务时间轴上,不能靠自增序号。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-20 16:11:34 +08:00
Blizzard
466959d3c3
fix: 静默吞错横扫 —— 数据落库/配置广播/审计失败不再无声
...
把检索链路那次的做法推到同类位置。判据是"丢了这个 error 的代价",只改代价
高的,不动刻意的 fire-and-forget。
改(丢了就是数据永久丢失或行为不可解释):
- task_handler: 任务**输出**与**执行轨迹**的收尾落库。这是唯一的持久副本
(Redis 流 10min TTL),失败则永远无法复盘,界面上只显示"这次运行没有
轨迹"。轨迹的 json.Marshal 失败分支同样是静默跳过,一并补上。
- admin.broadcastActive: 模型配置热更新广播。失败 = dispatcher/mcp-go 仍用
旧配置,症状是"控制台改了模型却不生效",而操作者这边一切正常——正是今天
排查半天的那类问题。
- middleware/audit: 审计留痕写入。不阻断主流程是对的(业务已完成),但静默
失败意味着敏感操作没有记录,且没人知道记录缺了,这是合规缺口。
不改(确认过是合理的):
- dispatcher 的 PublishToken/PublishExec:往前端推流,丢一帧是 UI 瑕疵,
且按 token 打日志会刷屏;
- 计量回写 PublishUsage:本来就检查 error 并打日志,钱的路径是干净的。
另修一处误导文案:任务下钻的轨迹空态原本把原因说死为"早于该功能上线",
现在"落库失败"也是已知原因,文案改为并列并指向日志里的 [task] 告警。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-20 15:49:13 +08:00
Blizzard
7c8c723245
fix(admin): 诊断解析放错了函数 + 补 kb_search 形状测试
...
上一个提交里,诊断解析被塞进了用户面的 KbSearch,而真正传 diag 的
AdminKbSearch 还在用老代码 —— 于是试验台永远拿不到 routes。写法上是
字符串替换只改了文件里第一处匹配,而这两个 handler 的收尾代码一模一样。
用户面已还原(它不传 diag,拿到的本就是裸数组,多一层解析是错的)。
补 3 组 mcp 层测试钉死 kb_search 的两种返回形状:
- 不带 diag(生产调用:dispatcher / Agent 工具链)→ 裸 JSON 数组,契约不变;
- 带 diag → {hits, routes} 对象;
- not ready 时不被总闸短路,仍逐路诊断。
这层是"改可观测性顺手改坏生产"的高风险位置:diag 分支若无条件生效,所有
调用方的解析都会静默失败。
真环境验证(本地 gateway + mcp-go + 真 PG/Milvus/bleve):
vector empty 该知识库在 Milvus 中没有向量(未入库或集合被重建过)
fulltext ok 15 命中
graph disabled Neo4j 未连接或未配置
以前这三行都只是"0 条"。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-20 15:24:49 +08:00
Blizzard
b26fe21408
fix(rag): 检索三路不再静默吞错 —— 逐路诊断 + 一路挂不拖垮全部
...
排查"向量路为什么是空的"花了半小时,因为空就是空,没有任何线索。这次把
整条检索链上的静默降级一次清掉。
真 bug(不只是可观测性):
- kb_search 与 Search() 都拿 rag.Ready() 当总闸,而 Ready() 只代表"向量路
可用"(embedding + Milvus)。全文(bleve)与图谱(Neo4j)根本不依赖它们,却
被一并毙掉 → "模型配置没下发"表现为"整个知识库什么都搜不到",还不报错。
改为逐路判定,任一路可用就仍有召回。
不再吞错:
- milvus.search 原先把 error 转成 nil,nil —— 检索失败与无召回彻底无法区分;
- bleve.search / graph.search 出错直接回 nil,连日志都没有;
- searchPaths 丢掉 embedding 的 error。
三处改为如实返回,错误统一打日志。
逐路诊断(RouteDiag):每路上报 ok/empty/disabled/error + 耗时 + 原因,经
kb_search 的 diag 参数(仅试验台传,生产调用返回值不变)→ gateway → 检索
试验台。界面上现在能直接看出"这一路没配置/报错了/确实没匹配",不必翻日志。
内存兜底索引也会在 note 里点明"重启即清零"。
测试:3 组,覆盖"无 embedding 时全文仍可召回"、三种空的区分、内存索引提示。
把总闸加回去验证过第一条确实会红——测试能抓到这个回归,不是摆设。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-20 15:10:03 +08:00
Blizzard
e4ec74893f
feat(admin): 任务观测支持下钻 —— 轨迹/输出/评测/DSL
...
全平台任务页此前只能看列表,点进去什么都没有。现在点一行开抽屉,四个页签:
执行轨迹、最终输出、评测明细、提交时的 DSL。不含审批操作——审批是客户端
用户的行为(桌面端 ApprovalBar),管理端只做观测。
不复用用户面的 /tasks/:id/replay:Task/Eval 都在租户插件作用域内,用请求 ctx
查别的租户的任务不会报错,而是静默返回空输出/空轨迹,UI 上表现为"这任务没
产出",比报错难查得多。新增 admin 端点走 WithoutTenant。
数据取自 sundynix_task 收尾落库的 output/trace 列,不依赖 Redis 流(10min TTL)。
所以这是复盘视图,运行中的任务轨迹为空——UI 里明确写出来,免得被当成轨迹丢了。
修的两处与测试环境失真有关(写测试时暴露的):
- 测试库没配 NamingStrategy,与 OpenPostgres 不一致:多数模型有显式
TableName() 碰巧对得上,但 Task 这类没有的会退化成 "tasks",导致写裸
SQL 的查询在测试里查无此表。现已对齐 sundynix_ 前缀 + 单数表名。
- graph 的 ::text 换成标准 cast(... as text):前者是 Postgres 专有,
换掉后这条查询才能被内存库覆盖。
新增 5 组测试,其中一组专门先证明"租户过滤在测试环境里确实开着"——否则
"跨租户能读到"的断言可能只是因为插件没装,属于假过。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-20 14:07:02 +08:00
Blizzard
84463394d4
fix(admin): 审计筛选下沉 SQL + 工具数不再写死 + 模型删除加确认
...
清单里那批小毛病,逐条复核后修(「审计详情列缺失」那条已不成立,早补上了)。
1. 审计筛选只在当前页生效 —— 影响最大的一条。分页是服务端的,筛选却在
前端对已取回的 50 条做,于是搜一个用户 ID 显示"无结果"时,后面几页
可能还有几百条。审计的用途就是查证,"搜不到"会被读成"没发生过"。
改为 action/path/q 三个条件全部落到 SQL,前端只管发条件(防抖 300ms)。
2. 服务状态页把 mcp-go/mcp-py 的工具数写死成 23/4 —— 增删工具后一直骗人,
且服务离线时照样显示,看不出工具其实一个都没注册上。改取实际上报值。
3. 模型删除一点即删,无任何确认。补二次确认,并对"正在使用中"的模型
单独说明后果(删掉会立刻打断线上对话/向量能力)。
审计筛选补了 4 组回归测试,两条是踩出来的坑:
- q 的 OR 组必须带括号:gorm 以 AND 拼接各 Where,裸 OR 会让 action
条件被绕过(测试里用"同 IP 不同方法"两行钉死这个语义);
- LIKE 必须显式写 ESCAPE '\':Postgres 默认拿反斜杠当转义符,SQLite
不写就没有转义符——原来的写法在单测里静默失效,搜 "100%" 命中 0 条。
顺带把 ILIKE 换成 LOWER()+LIKE,这段才能被内存库覆盖。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-20 13:51:59 +08:00
Blizzard
bfd0d74c34
feat(status): 全文索引降级上报到控制台 —— 让静默降级看得见
...
上一个提交修的是"卷没挂"这个实例,这个提交修的是"它坏了几周没人发现"这个
类别问题:全文路退内存兜底后,混合检索只表现为召回变差,不报错、无告警。
- mcp-go: bleveStore 记录 persistent;health 工具多报一个 fulltext_disk。
仍是 map[string]bool,gateway 现有反序列化不受影响(加非 bool 字段会让
整个 health 解析失败,连带 Milvus/Neo4j 健康灯一起黑)。
- gateway: 基建灯新增「全文索引」一项,退内存时给出后果与排查方向,
而不只是一盏灰灯。
- admin: 基建列表此前只渲染 name/up,detail 字段一直没露出——而降级原因
恰恰只存在于 detail 里。补上渲染(离线态用琥珀色),并给「全文索引」
补 INFRA_META(它不是独立服务,没有端口,落在容器卷上)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-20 13:44:48 +08:00
Blizzard
70b0419387
fix(payment): 支付轮询链路收口——查单节流 + 前端轮询重写
...
后端:
- 主动查单加最小间隔(5s)。此前 BillingOrderStatus 见 pending 就直连微信查单,
而前端 2.5s 轮一次 —— 单笔订单在 30min TTL 内可打出约 720 次渠道调用,
微信侧有频控,并发用户一多先被限流的是我们自己。回调才是入账主路径,
查单只是兜底,节流最坏只让在场用户多等 5s。
- 标记随订单落终态清除,并在补偿定时器每轮 prune 掉超 TTL 的残留
(用户扫码前就关弹窗的订单不会再被轮询,其标记无人回收)。
- 下单响应补 expires_at:二维码有效期由服务端 orderTTL 说了算,
前端硬编码一份迟早漂移。
前端(sundynix-web):
- orderStatus 不再丢掉 warn。服务端在「已付但金额与订单不符」时不入账、
挂起人工核对,订单一直停在 pending —— 丢掉 warn 用户就会一直等一个
永远不会来的结果。现在弹窗显式告警并给出订单号。
- setInterval → 递归 setTimeout:请求慢于间隔时 setInterval 会把请求摞起来,
对「每次可能触发渠道查单」的端点尤其糟。
- 标签页切走时暂停轮询(用户扫完码要切去微信 App),切回前台立刻补查一次。
- 连续失败指数退避至 15s 封顶,断网时不再定频猛打。
- 二维码倒计时 + 失效遮罩,过期不再让用户扫一个必然失败的码。
- 明说「关掉也不会丢钱,到账会自动补上」——掉单补偿定时器本就兜底,
但此前 UI 没讲,用户只能守着弹窗。
- 关闭弹窗一律刷新余额:用户可能在关闭前一刻付款、状态刚落地。
测试:新增 handler 包首个测试,钉住节流与标记回收行为(-race 通过)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-20 12:46:40 +08:00
Blizzard
79b110afd0
feat(admin): 数据源页转 RAG 运维台 + 支付/模型菜单重组 + 概览升级为仪表盘
...
后端:
- 新增 POST /admin/kb/search:管理端跨租户检索,支持 mode 指定单路
(vector/fulltext/graph/hybrid),不走 scopedKB(否则会被强制锁到调用者
自己的 space,跨租户排障就没法做了)
- KB 清单补 space_id(检索键是 <space_id>/<name>,缺它前端拼不出 key)
前端:
- 数据源&RAG 页补「检索试验台」:同一 query 并排跑生产链路 + 四路诊断,
召回不准时能直接定位是向量/分词/图谱哪一环挂了
- 支付拆成「配置 / 订单与对账」两个子页,挂到运维 > 支付 下;
导航支持二级菜单(NavParent 命中子路由自动展开)
- SettingsPage → ModelConfigPage「模型配置」,模型参数与计费规则合一
- 概览 → 仪表盘:并入计费与用量(UsagePage → UsageSection),
去掉系统健康拓扑(与服务状态页重复,同一份 /admin/status 数据)
- 全局隐藏滚动条(保留滚动)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-20 11:17:54 +08:00
Blizzard
0d2929931f
feat(admin): upgrade admin console UI widgets, add plan & status management, and redesign system status dashboard
2026-07-18 22:18:12 +08:00
Blizzard
373167b705
feat(admin): 补全平台任务观测 + 空间管理 + 基建加 MinIO/实时探针
...
对照已实现系统功能补 admin 缺失模块(feat/site):
- 全平台任务/运行观测:GET /admin/tasks(跨租户查 sundynix_task,join 租户名/提交人邮箱/评测,
按状态/租户筛 + 状态计数;含 HITL 待审批=筛 waiting)+ TasksPage(状态卡+筛+表)。
- 空间(Space)管理:GET /admin/spaces(跨租户列 Space + 成员数子查询 + kind/归档态)+ SpacesPage。
- 基建观测:infra 加 MinIO(126 对象存储);postgres/redis/minio 从启动标志升级为实时 ping
(blob.Ping/Postgres.Ping/Redis.Ping),能反映中途掉线。StatusPage 加 minio 标签、6 个依赖。
- 模型健康/熔断:确认 DashboardPage 已有「运行时链路态」渲染 m.health,无需重做。
验证:admin tsc + vitest 41 过、gateway build/vet/test 过;两新页浏览器实测渲染+优雅错误处理;
两新端点起临时 gateway 打真 PG 实测——tasks(counts+3路join)、spaces(成员数子查询)均返真数据。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-18 17:06:25 +08:00
Blizzard
09ac03b758
perf(docker): 依赖下载全部换国内源(内网构建提速)
...
deploy-132 / deploy (push) Failing after 5m35s
go mod download 走默认 proxy.golang.org 内网极慢。四个 Dockerfile 统一换国内镜像:
- Go: GOPROXY=https://goproxy.cn,direct (三个 Go 服务)
- alpine: apk 源换 mirrors.aliyun.com(ca-certificates/tzdata)
- pip: mcp-py 换清华 pypi 源
- npm: gateway admin 已在用 npmmirror(此前已有)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-18 16:03:45 +08:00
Blizzard
2d14245086
fix(docker): 运行镜像 gcr.io/distroless → alpine(内网 runner 连不上 gcr.io)
...
deploy-132 / deploy (push) Has been cancelled
128 的 runner 能拉 docker.io 但连不上 gcr.io(dial 443 i/o timeout),distroless 拉不到。
三个 Go 服务运行 stage 换 alpine:3.20 + ca-certificates(TLS 连 LLM/MinIO)+ tzdata +
非 root 用户。二进制 CGO_ENABLED=0 全静态,alpine 上直接跑;本地已验证构建+启动正常。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-18 15:54:35 +08:00
Blizzard
28a1543249
feat(gateway): 内嵌 admin 控制台(go:embed),去掉单独 nginx 容器
...
复刻 sundynix-site 的 webfs 模式:gateway 一个容器同时 serve 控制台 UI + API + 供
桌面端直连。admin 用 HashRouter + API base 走相对 /api/v1(构建传 VITE_GATEWAY=''),
源码零改动即可同源 serve。
- internal/webui:go:embed all:admin_dist + Dist();占位 index.html 让不构建前端也能编译。
目录命名 admin_dist(避开 .gitignore dist/ 与 .dockerignore **/dist 通配)。
- router.go NoRoute:非 /api/ 路径走内嵌静态,命中 /assets/* 直吐、否则回退 index.html;
embed 失败仅 log 降级不影响 API。/metrics /healthz /readyz /api/v1 已注册,永不进 NoRoute。
- Dockerfile 加 admin node 构建 stage,go build 前 COPY dist 覆盖 embed 占位。
- live 验证:/ 返回 admin(200)、/assets/*.js(200)、/api/v1/admin/overview(401 走 API)、/healthz(200)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-18 14:45:24 +08:00
Blizzard
3a4e1d53a5
refactor(payment): 渠道抽象成 Channel 接口 —— 接新渠道=加 adapter 不动骨架
...
PAYMENT_DESIGN §3 承诺的 internal/payment/channel.go 适配器接口此前不存在,微信硬编码在
manager/handler 里。补齐抽象:
- channel.go:Channel 接口(Name/CreatePay/QueryOrder/VerifyCallback)+ 统一 PayIntent/PayResult
+ 渠道名常量。入参用基本类型不吃 *store.PaymentOrder,payment 包不反依赖 store。
- Wechat 实现 Channel(编译期断言 var _ Channel);QueryResult 归一为 PayResult;CreatePay 返回 PayIntent。
- Manager 从「持一个 *Wechat」改为渠道注册表:Get(name)/Available()/Status(name)/ReloadWechat;
按渠道名持有已装配实例,热重载不变。
- 回调路由收敛 /billing/callback/wechat → /billing/callback/:channel 按名路由(旧 notify URL 仍匹配);
查单/掉单补偿据 order.Channel 路由,不再写死微信。下单支持可选 channel(缺省 wechat)。
- 支付宝/Stripe 现在真·只差一个 adapter+注册。唯一未泛化:回调 ack 应答格式(现微信态,注释标明)。
- payment 包首个测试:Manager 注册表 4 用例(空/注册摘除/空配置/配置不全)。build/vet/test/lint 全绿。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-18 13:56:24 +08:00
Blizzard
1beef44625
ci: 加 -race + golangci-lint(new-issues) + govulncheck/gitleaks 安全扫描
...
补齐审计指出的「CI 缺 lint/安全扫描/-race」低成本质量门:
- go job:go test → go test -race(4 模块本地已验证 race-clean,ubuntu 自带 gcc)。
- lint job:golangci-lint(standard 集)仅 PR 跑 + only-new-issues —— 新代码必须干净,
存量 ~42 处(多为未检 Close/死代码)单独消化,不拿存量红门。加 .golangci.yml(测试放过 errcheck)。
- security job:govulncheck advisory(stdlib/nats CVE 靠 toolchain/依赖升级,只做可见性不阻断)
+ gitleaks 扫提交防密钥泄漏。
- 顺手清掉本会话新代码的 lint:blob GetBytes 的 defer Close、退款测试未检 CreateOrder。
注:gitleaks-action 个人/公开仓库免费;组织仓库需 GITLEAKS_LICENSE。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-18 13:44:38 +08:00
Blizzard
5e5f6e9610
feat(billing): 人工退款入口 —— paid 单冲销积分+置 refunded(幂等)
...
PAYMENT_DESIGN §5 承诺却只留了 OrderRefunded 常量、无入口。补齐全链路:
- store.RefundOrder:订单 CAS(paid→refunded) 为主闸(重复退 changed=false 幂等),
同事务记 adjust 负分录(ref=订单号)+ 回退物化余额。照抄 MarkOrderPaid 双闸范式;
新增 idx_ledger_refund_ref 部分唯一索引(kind='adjust' AND ref<>'')做账本级兜底,
与 grant 索引对称、不与 admin 手工校正(ref 空)冲突。
- 积分若已消费,回退后余额可为负(人工退款预期,账本仍自洽,后续消费被硬拦截)。
- handler AdminRefundOrder + POST /admin/orders/:id/refund(admin 组已挂 Audit 留痕);
真渠道钱款原路退回需 admin 另在商户后台操作,本地仅冲销积分与订单态(不接自动退款 API)。
- admin 订单流加「退款」按钮(仅 paid 单可见,二次确认+填原因)。
- 测试:RefundOrder 冲销+幂等、只退 paid 两个不变量测试(sqlite 真 DB,余额=账本之和)。
gateway build/vet/test 全绿,admin tsc 干净。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-18 13:35:31 +08:00
Blizzard
ec9431bfa4
fix(report): 报告源/产物落 MinIO,去掉 gateway↔mcp-go 本地盘耦合
...
多机/容器化第一时间断裂的隐患(ARCHITECTURE_REVIEW §7 #1 ):此前报告
源(reportStore)与 .docx 产物(reportExport/reportRender)都写 mcp-go
本地 SUNDYNIX_REPORTS_DIR,gateway 再按同一路径 c.File 回流——mcp-go
与 gateway 不共享磁盘即断。
- blob 包从 gateway/internal 提到 sundynix-shared/blob,gateway 与 mcp-go
共用同一 MinIO;新增 PutBytes/GetBytes 走二进制(.docx)。
- mcp-go 注入 blob:report 源/产物优先 Put 到对象存储(键 reports/<id>.{json,docx}),
导出结果返回 minio://<key>;MinIO 未就绪回退本地盘(单机降级,getSource 兼容旧本地报告)。
- gateway ExportReport 识别 minio:// → GetBytes 流式下载,否则 c.File 本地降级。
- 测试:mcp 本地回落往返单测 + shared/blob 真 MinIO 二进制往返(BLOB_TEST_ENDPOINT 门控)。
四模块 build 绿,受影响测试全过。
- 附带:go mod tidy 引入 genproto 拆包冲突,四模块统一钉单体 genproto 新版解决
(注:勿 go work sync,会剪掉 indirect require 回落旧版重现冲突)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-18 13:29:31 +08:00
Blizzard
e29bc9a91e
feat(admin): 「数据源 & RAG」页做实 —— admin 三 mock 页清零 (P1)
...
审计 P1「admin 三页纯 mock」最后一页。此前 DatasourcesPage 的 GraphRAG 拓扑图
写死节点、「向量/全文/图谱权重滑块」纯 mock——而且权重概念本身虚构:mcp-go 的
RRF 融合是各路等权的倒排互惠融合(rrfK=60 平滑常数),根本没有"每路占几成"。
- store/datasource_query.go:AllDatasources(全平台 KB + 各库文档数/总字数,
按 (space_id,name) 关联 doc,WithoutTenant);GET /admin/datasources(含
SystemCounts 平台计数)。
- DatasourcesPage 重写:保留真实 Embedding 模型配置(ModelManager) + 诚实的
混合检索管线说明(三路 Milvus/Bleve/Neo4j + RRF 等权融合 k=60,非可调权重) +
平台计数卡片 + 真知识库清单表。删假滑块+假拓扑(~200行 mock)。
诚实边界:RRF 无每路权重,不摆假滑块;检索参数在 mcp-go 代码中。
live:/admin/datasources 返 34用户/25库/53文档,清单带真实文档数字数;
浏览器渲染全对。tsc+41 vitest 全绿。**admin 三 mock 页(Evals/Guardrails/
Datasources)全部做实。**
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-18 12:26:16 +08:00
Blizzard
d04d830c37
feat(admin): 「自动评测」页做实 —— 接真评测数据,去 mock (P1)
...
审计 P1「admin 三页纯 mock」之一。此前 EvalsPage 是写死的质量趋势+编造的
错题本+虚构纠偏轨迹。现接 sundynix_eval 真数据(评测经 JetStream eval 流持久
落库,刚升级)。
- store/eval_query.go:EvalTrend(按天 avg 综合分/忠实度+低分计数)、EvalSummaryFor
(ok/warn/poor/corrected 计数+均值)、PoorEvals(错题本,level in poor/warn +
评语+纠偏标记+租户名)。全 WithoutTenant 平台口径;忠实度均值只算 sources>0
(无来源的忠实度恒0会压低失真)。
- GET /admin/evals?days=(RequireAdmin);admin api.ts + EvalsPage 重写:
总览卡片(综合分/合格率/低分占比/纠偏采纳率)+质量&忠实度趋势(纯SVG折线+低分
背景条)+错题本(点行展开评语)。
- 诚实边界:纠偏「前后全文对照」后端未持久化,只存了 Reason/Corrected/各维度分,
故错题本展示评语+「已纠偏」标记,不再编造 before/after。
live:/admin/evals 返 57 次评测 avg=0.88、错题本10条、11天趋势;浏览器渲染
真数据(趋势线07-15真实下探)。go+tsc+41 vitest 全绿。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-18 12:03:54 +08:00
Blizzard
940330cdb7
feat(eval): 评测结果回写升 JetStream 持久 —— 消灭最后一处 core NATS 回写 (P1)
...
完成度审计 P1 + 记忆 nats-durability 的既定规矩「计费/需落库的回写一律
JetStream+幂等,别fire-and-forget」。此前 eval 是全仓最后一处 core NATS pub-sub
回写:网关离线/慢消费者时评测结果直接丢——而质量趋势/门控都依赖它。
照抄已升级的 status/usage 范式(同为 dispatcher→gateway→PG 回写):
- contract 加 StreamEval/ConsumerEval;bus 加 EnsureEvalStream + ConsumeEval
(durable consumer,AckExplicit,落库失败 Nak 重投自愈),PublishEval 改
js.Publish 同步等 ack。删 core NATS 的 SubscribeEval。
- gateway/dispatcher 两个 wrapper 在 connect 时 EnsureEvalStream;gateway main
的评测订阅从 SubscribeEval(fire-and-forget)换 ConsumeEval(handler 返 error→
Nak),接入优雅停机 drain。
- 幂等前提已满足:SaveEval 按 task_id upsert,at-least-once 重投只覆盖不重复。
验证:e2e 去重测试(TestGatewayQueueDedup)升级到 ConsumeEval,50 条两副本合计
处理50次零重复;live 真端到端:提交任务→跑完→评测经新 eval 流落 PG(level=ok)
一次成功;两服务启动 eval 流 ensure 无报错。go build/vet/test 全绿。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-18 11:52:40 +08:00
Blizzard
3d15cdb493
fix(gateway): 任务落库失败不再吞 —— 关键写失败上浮 5xx (P0-2)
...
DEPTH_ROADMAP:225 未完项 + 完成度审计 P0-2:launch() 里 SaveTask 是 best-effort,
DB 写失败只 log 却照样 PublishTask + 返 202。结果任务发出去在后端跑了,却不进
运行历史、复盘不了、报告类的用户切页面回来彻底找不回——用户以为成功、实际没落库。
- SaveTask 对「DB 降级(nil)」返 nil、对「DB 活着写失败」返真 error,天然可区分:
前者静默跳过(开发态本就无库),后者上浮为提交失败。落库在 Publish 之前,
失败时还没发布,中止干净、不产生"看不见的执行"。
- 两个调用点(SubmitTask/GenerateReport)已把 launch 错误映射 5xx,无需再改。
范围克制:审计列的其它 best-effort 点保持不动——dispatcher 的异步回写
(UpdateTaskStatus/SaveTaskOutput/SaveTaskTrace)、审计日志、用量累计计数,
都不是"用户等着响应"的路径,没有请求可返 5xx,best-effort 是对的。
唯独 launch 这处是"用户以为提交成功实际没有",才该阻断。
live 验证:改名 task 表模拟 DB 写失败 → 提交返 502+明确文案(不再假 202);
表改回 → 立刻恢复 202。happy path 202+落库不变。go build/vet/test 全绿。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-18 11:45:08 +08:00
Blizzard
bff6e5c7fd
test(gateway): 补上「钱路径」与租户隔离的回归测试 —— P0 最高优先级缺口
...
完成度审计(见记忆 completion-audit)最尖的一条:涉及钱的路径此前 0 测试,
支付一上线 bug=真实错账;租户数据层隔离也只测了角色门禁没测数据层真隔离。
把本会话 live 手验过的断言固化成回归测试。
测试基建:纯 Go sqlite(glebarez,无 CGO)内存库,迁同款模型+建部分唯一索引+
挂租户作用域回调,复用生产 store 方法测真逻辑。CI ubuntu 无 Postgres 也能跑
(此前 store 测试全是纯逻辑,DB 事务逻辑从没进过关卡)。
钱路径不变量(6):
- GrantCredits 记分录+增余额,余额恒等于 SUM(ledger)
- 兑换码核销一次性(CAS)+原子入账,重复核销余额纹丝不动
- 账本(kind,ref)部分唯一索引:重复 grant 被兜底拦下、usage 不受约束
- MarkOrderPaid CAS 幂等:重复回调 changed=false 不重复入账
- SaveUsageEvent task_id 幂等:同任务重投不重复扣费
- ReconcileOrders 抓出 order_without_ledger(钱到了积分没给)
租户隔离(3):创建自动填 tenant_id、查询按 ctx 租户过滤、跨租户改/删命不中、
WithoutTenant 系统视角全可见。
go build/vet + 全 gateway 测试全绿;sqlite 仅测试引用,不进生产二进制。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-07-18 11:39:36 +08:00