M4 收口:神识用户面与计量闭环——BYOK/配额三段式/usage 落库/Anthropic+Gemini 适配器/神识阁
- 神识 Go:store 双实现(PG 自动建表+60s 就绪重试 / 内存回退)承载 usage_logs·byok_keys·quota_balances;BYOK 密钥库(AES-GCM 加密、API 脱敏、撤回即失效、主密钥未配置 fail-closed);配额三段式(预扣 max_tokens 钳位 1M→结算实际 token→全败幂等退款,HJ-2004);计量归属(IR user_id→usage_logs,/me/usage 用户维度聚合+配额总览,/admin/usage/sample 对账抽样);Anthropic/Gemini 出站适配器(SSE 解析、system 提升、用量映射);stub 端点补名修复计量空 endpoint - IR 契约:user_id/byok_key_id 双端同步(Go types.go + Py types.py) - 灵台:runs.owner_id(迁移 005,server_default 兼容存量)经 kernel 全链透传为 IR user_id;shenshi_client 扩展用户面方法(X-Hunjing-User-Id 代理,Echo 桩带内存实现);/me/byok-keys 登记·列表·撤回与 /me/usage 端点(4 用例) - 前端神识阁(/models 重建):偏好(生成参数表单)/自带密钥(登记加密保存、脱敏列表、撤回二次确认)/用量(聚合表+配额进度条)三区;me API 库+mock+5 用例;侧栏恢复入口 - 配置与部署:.env.example 增 SHENSHI_DATABASE_URL/SHENSHI_BYOK_MASTER_KEY/SHENSHI_USER_BUDGET_TOKENS;compose 神识容器接自有库(init-shenshi.sql 预建 hunjing_shenshi,与灵台不共库共表) - 证据:Go 34 用例(BYOK 独占/配额拒绝/结算退款/归属聚合)、后端 532、前端 210、BFF 4 全绿;跨进程 E2E 四场景(灵台 HttpShenshiClient→真 Go 神识:登记脱敏/BYOK 独占不可达整体失败/用量归属聚合/撤回即失效)通过;rewrite-plan M4 标记完成、能力矩阵神识行升已验证
This commit is contained in:
1 parent
462d570751
commit
144415247e
51 files changed
+3533
-90
No files matched your search
@@ -0,0 +1,163 @@
|
||||
// PG 实现:与内存实现同语义的直写 SQL。
|
||||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
type pgStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func (p *pgStore) InsertUsage(l UsageLog) error {
|
||||
_, err := p.db.Exec(
|
||||
`INSERT INTO usage_logs (id, run_id, character_id, user_id, model, slot, endpoint,
|
||||
status, error_code, first_token_ms, total_ms, prompt_tokens, completion_tokens, at)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,
|
||||
l.ID, l.RunID, l.CharacterID, l.UserID, l.Model, l.Slot, l.Endpoint,
|
||||
l.Status, l.ErrorCode, l.FirstTokenMS, l.TotalMS, l.PromptTokens, l.CompletionTokens, l.At,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *pgStore) AggregateUsage(userID string) ([]UsageAgg, error) {
|
||||
q := `SELECT user_id, model, endpoint, COUNT(*),
|
||||
COUNT(*) FILTER (WHERE status = 'ok'),
|
||||
COALESCE(SUM(prompt_tokens) FILTER (WHERE status = 'ok'), 0),
|
||||
COALESCE(SUM(completion_tokens) FILTER (WHERE status = 'ok'), 0)
|
||||
FROM usage_logs`
|
||||
args := []any{}
|
||||
if userID != "" {
|
||||
q += ` WHERE user_id = $1`
|
||||
args = append(args, userID)
|
||||
}
|
||||
q += ` GROUP BY user_id, model, endpoint ORDER BY user_id, model`
|
||||
rows, err := p.db.Query(q, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
var out []UsageAgg
|
||||
for rows.Next() {
|
||||
var r UsageAgg
|
||||
if err := rows.Scan(&r.UserID, &r.Model, &r.Endpoint, &r.Requests, &r.OK, &r.PromptTokens, &r.CompletionTokens); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (p *pgStore) SampleUsage(limit int) ([]UsageLog, error) {
|
||||
rows, err := p.db.Query(
|
||||
`SELECT id, run_id, character_id, user_id, model, slot, endpoint, status, error_code,
|
||||
first_token_ms, total_ms, prompt_tokens, completion_tokens, at
|
||||
FROM usage_logs ORDER BY at DESC LIMIT $1`, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
var out []UsageLog
|
||||
for rows.Next() {
|
||||
var l UsageLog
|
||||
if err := rows.Scan(&l.ID, &l.RunID, &l.CharacterID, &l.UserID, &l.Model, &l.Slot, &l.Endpoint,
|
||||
&l.Status, &l.ErrorCode, &l.FirstTokenMS, &l.TotalMS, &l.PromptTokens, &l.CompletionTokens, &l.At); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, l)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (p *pgStore) UpsertByok(k ByokKey) error {
|
||||
_, err := p.db.Exec(
|
||||
`INSERT INTO byok_keys (id, user_id, name, type, base_url, api_key, model, created_at)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
|
||||
ON CONFLICT (id) DO UPDATE SET name=$3, type=$4, base_url=$5, api_key=$6, model=$7`,
|
||||
k.ID, k.UserID, k.Name, k.Type, k.BaseURL, k.APIKey, k.Model, k.CreatedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *pgStore) ListByok(userID string) ([]ByokKey, error) {
|
||||
return p.byokQuery(`SELECT id, user_id, name, type, base_url, api_key, model, created_at, last_used, revoked_at
|
||||
FROM byok_keys WHERE user_id = $1 AND revoked_at IS NULL ORDER BY created_at`, userID)
|
||||
}
|
||||
|
||||
func (p *pgStore) GetByok(userID, id string) (*ByokKey, error) {
|
||||
keys, err := p.byokQuery(`SELECT id, user_id, name, type, base_url, api_key, model, created_at, last_used, revoked_at
|
||||
FROM byok_keys WHERE user_id = $1 AND id = $2 AND revoked_at IS NULL`, userID, id)
|
||||
if err != nil || len(keys) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
return &keys[0], nil
|
||||
}
|
||||
|
||||
func (p *pgStore) byokQuery(q string, args ...any) ([]ByokKey, error) {
|
||||
rows, err := p.db.Query(q, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
var out []ByokKey
|
||||
for rows.Next() {
|
||||
var k ByokKey
|
||||
if err := rows.Scan(&k.ID, &k.UserID, &k.Name, &k.Type, &k.BaseURL, &k.APIKey, &k.Model, &k.CreatedAt, &k.LastUsed, &k.RevokedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, k)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (p *pgStore) RevokeByok(userID, id string) error {
|
||||
res, err := p.db.Exec(`UPDATE byok_keys SET revoked_at = $1 WHERE user_id = $2 AND id = $3 AND revoked_at IS NULL`,
|
||||
time.Now().UTC(), userID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n, _ := res.RowsAffected(); n == 0 {
|
||||
return ErrByokNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *pgStore) TouchByok(id string, at time.Time) error {
|
||||
_, err := p.db.Exec(`UPDATE byok_keys SET last_used = $1 WHERE id = $2`, at, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *pgStore) Reserve(userID string, amount int64) error {
|
||||
if amount <= 0 {
|
||||
return nil
|
||||
}
|
||||
// UPSERT + 原子累加;配额上限裁决在 quota 包(集中单一函数)
|
||||
_, err := p.db.Exec(`INSERT INTO quota_balances (user_id, reserved, used) VALUES ($1, $2, 0)
|
||||
ON CONFLICT (user_id) DO UPDATE SET reserved = quota_balances.reserved + $2`, userID, amount)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *pgStore) Settle(userID string, reserved, actual int64) error {
|
||||
// 饱和截断在 SQL 层以 GREATEST 保证(防负)
|
||||
_, err := p.db.Exec(`UPDATE quota_balances SET
|
||||
reserved = GREATEST(reserved - $2, 0), used = used + $3 WHERE user_id = $1`,
|
||||
userID, reserved, actual)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *pgStore) Refund(userID string, reserved int64) error {
|
||||
_, err := p.db.Exec(`UPDATE quota_balances SET reserved = GREATEST(reserved - $2, 0) WHERE user_id = $1`,
|
||||
userID, reserved)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *pgStore) Balance(userID string) (QuotaBalance, error) {
|
||||
var b QuotaBalance
|
||||
b.UserID = userID
|
||||
err := p.db.QueryRow(`SELECT reserved, used FROM quota_balances WHERE user_id = $1`, userID).
|
||||
Scan(&b.Reserved, &b.Used)
|
||||
if err == sql.ErrNoRows {
|
||||
return b, nil
|
||||
}
|
||||
return b, err
|
||||
}
|
||||
Reference in new issue
Block a user