Skip to content

feat: Starlark 脚本引擎支持第三方中转站用量采集#942

Open
alfadb wants to merge 3 commits intoWei-Shaw:mainfrom
alfadb:feat/starlark-usage-engine
Open

feat: Starlark 脚本引擎支持第三方中转站用量采集#942
alfadb wants to merge 3 commits intoWei-Shaw:mainfrom
alfadb:feat/starlark-usage-engine

Conversation

@alfadb
Copy link
Contributor

@alfadb alfadb commented Mar 11, 2026

Summary

  • 新增 Starlark 脚本引擎,支持为第三方中转站(非官方上游)编写自定义用量采集脚本
  • 管理后台新增「用量脚本」页面,支持脚本的 CRUD 管理
  • 账号用量查询自动匹配 base_url 对应的脚本并执行采集
  • 前端 AccountUsageCell 组件支持展示 script_windows 用量数据

Changes

Backend

  • internal/service/script_engine.go — Starlark 脚本执行引擎,支持 HTTP 请求、JSON 解析、时间函数等内置模块
  • internal/service/account_usage_service.go — 在用量查询流程中集成脚本引擎,按 base_url host 匹配脚本
  • internal/handler/admin/usage_script_handler.go — 管理 API:列表/创建/更新/删除用量脚本
  • ent/schema/usage_script.go — 数据库 schema:host_pattern、account_type、script 等字段
  • migrations/072_add_usage_scripts.sql — 数据库迁移

Frontend

  • views/admin/UsageScriptsView.vue — 用量脚本管理页面
  • components/account/AccountUsageCell.vue — 支持 script_windows 展示
  • api/admin/usageScripts.ts — API 客户端

Test plan

  • 创建用量脚本并验证 CRUD 操作
  • 配置带 base_url 的账号,验证脚本自动匹配和执行
  • 验证脚本执行超时和错误处理
  • 验证前端 script_windows 用量展示

🤖 Generated with Claude Code

alfadb added 3 commits March 11, 2026 21:23
…nitoring

- Add usage_scripts DB table (base_url_host + account_type as key)
- Implement ScriptEngine: Starlark interpreter with http_get/http_post/json_parse builtins
- Integrate with AccountUsageService: auto-match scripts by account base_url
- Add scheduling integration: utilization >= 1.0 triggers temp_unschedulable
- Add Admin CRUD API for usage scripts management
- Wire injection for ScriptEngine and UsageScriptRepository
- Add Admin CRUD page for usage scripts management (UsageScriptsView.vue)
- Add API client for usage scripts (api/admin/usageScripts.ts)
- Add ScriptUsageWindow type to AccountUsageInfo
- Display script_windows in AccountUsageCell for accounts with custom base_url
- Add route /admin/usage-scripts and sidebar navigation entry
- Add i18n translations (en/zh) for usage scripts
Copilot AI review requested due to automatic review settings March 11, 2026 13:39
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a Starlark-based “usage collection script” system to support usage monitoring for third-party relay upstreams (non-official upstreams), including admin CRUD APIs/UI and usage display integration.

Changes:

  • Backend: introduce a Starlark script engine + usage-script persistence (Ent schema + migration) + admin CRUD routes/handler, and integrate script execution into account usage lookup.
  • Frontend: add an admin “Usage Scripts” management page, navigation entry, API client, and usage UI support for script-based usage windows.
  • Types/i18n: add script_windows typing and translations for the new admin page.

Reviewed changes

Copilot reviewed 39 out of 40 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
frontend/src/views/admin/UsageScriptsView.vue New admin CRUD page for managing usage scripts.
frontend/src/types/index.ts Adds script_windows and ScriptUsageWindow type for usage display.
frontend/src/router/index.ts Adds /admin/usage-scripts admin route.
frontend/src/i18n/locales/zh.ts Adds nav + page translations for usage scripts (ZH).
frontend/src/i18n/locales/en.ts Adds nav + page translations for usage scripts (EN).
frontend/src/components/layout/AppSidebar.vue Adds sidebar nav item for usage scripts.
frontend/src/components/account/AccountUsageCell.vue Adds rendering for script-based usage windows.
frontend/src/api/admin/usageScripts.ts New admin API client for usage scripts.
backend/migrations/072_add_usage_scripts.sql Adds usage_scripts table + partial unique index.
backend/internal/service/wire.go Registers ScriptEngine provider in service DI.
backend/internal/service/usage_script.go Adds service-layer UsageScript model.
backend/internal/service/script_usage_types.go Adds script usage result/window DTO types.
backend/internal/service/script_engine.go Implements Starlark execution engine + builtins.
backend/internal/service/account_usage_service.go Integrates script lookup/execution into usage flow + adds repo interface.
backend/internal/server/routes/admin.go Registers admin routes for usage script CRUD.
backend/internal/repository/wire.go Registers UsageScriptRepository provider.
backend/internal/repository/usage_script_repo.go Implements UsageScriptRepository via Ent.
backend/internal/handler/wire.go Wires UsageScriptHandler into admin handlers set.
backend/internal/handler/handler.go Adds UsageScript to AdminHandlers.
backend/internal/handler/admin/usage_script_handler.go Implements admin CRUD HTTP handlers for usage scripts.
backend/go.mod Adds go.starlark.net dependency.
backend/go.sum Updates module sums for new dependency.
backend/ent/usagescript_update.go Ent generated code for UsageScript updates.
backend/ent/usagescript_query.go Ent generated code for UsageScript queries.
backend/ent/usagescript_delete.go Ent generated code for UsageScript deletes.
backend/ent/usagescript_create.go Ent generated code for UsageScript creates/upserts.
backend/ent/usagescript/where.go Ent generated predicates for UsageScript fields.
backend/ent/usagescript/usagescript.go Ent generated metadata for UsageScript schema.
backend/ent/usagescript.go Ent generated model struct for UsageScript.
backend/ent/tx.go Adds UsageScript client to Ent transactions.
backend/ent/schema/usage_script.go Defines Ent schema for UsageScript entity.
backend/ent/runtime/runtime.go Registers runtime defaults/validators/hooks for UsageScript.
backend/ent/predicate/predicate.go Adds predicate type for UsageScript.
backend/ent/mutation.go Adds Ent mutation plumbing for UsageScript.
backend/ent/migrate/schema.go Adds UsageScripts table schema to Ent migration metadata.
backend/ent/intercept/intercept.go Adds intercept scaffolding for UsageScript queries.
backend/ent/hook/hook.go Adds hook scaffolding for UsageScript mutations.
backend/ent/ent.go Updates Ent column validation map to include UsageScript.
backend/ent/client.go Adds UsageScript client and wiring to Ent client.
backend/cmd/server/wire_gen.go Updates generated DI wiring to include ScriptEngine + repo + handler.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +63 to +69
func (r *usageScriptRepository) Update(ctx context.Context, id int64, script *service.UsageScript) (*service.UsageScript, error) {
m, err := r.client.UsageScript.UpdateOneID(id).
SetBaseURLHost(script.BaseURLHost).
SetAccountType(script.AccountType).
SetScript(script.Script).
SetEnabled(script.Enabled).
Save(ctx)
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update uses UpdateOneID(id) without filtering out soft-deleted rows. In other repos (e.g. APIKey) updates typically add Where(DeletedAtIsNil()) to avoid mutating deleted records. Consider adding Where(usagescript.DeletedAtIsNil()) here (and translating NotFound appropriately) so soft-deleted scripts can't be updated by ID.

Copilot uses AI. Check for mistakes.
Comment on lines +183 to +189
body, err := io.ReadAll(io.LimitReader(resp.Body, scriptMaxResponseBody))
if err != nil {
return nil, fmt.Errorf("http_get: read body: %w", err)
}

return starlark.Tuple{starlark.MakeInt(resp.StatusCode), starlark.String(body), responseHeadersToStarlark(resp)}, nil
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

body is a []byte from io.ReadAll, but it's being converted directly to starlark.String(body), which won't compile (cannot convert []byte to string). Convert the response body to a Go string first (or return starlark.Bytes).

Copilot uses AI. Check for mistakes.
Comment on lines +220 to +226
body, err := io.ReadAll(io.LimitReader(resp.Body, scriptMaxResponseBody))
if err != nil {
return nil, fmt.Errorf("http_post: read body: %w", err)
}

return starlark.Tuple{starlark.MakeInt(resp.StatusCode), starlark.String(body), responseHeadersToStarlark(resp)}, nil
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as http_get: body is []byte but returned as starlark.String(body), which will not compile. Convert to string(body) (or return bytes) before wrapping it as a Starlark value.

Copilot uses AI. Check for mistakes.
Comment on lines +90 to +99
script := &service.UsageScript{
BaseURLHost: req.BaseURLHost,
AccountType: req.AccountType,
Script: req.Script,
}
if req.Enabled != nil {
script.Enabled = *req.Enabled
} else {
script.Enabled = true
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update currently treats missing fields as empty values and also forces Enabled=true when enabled is omitted. This makes partial updates unsafe (e.g., a client sending only {enabled:false} would blank out base_url_host/account_type/script, and sending an update without enabled will unintentionally re-enable the script). Either require all fields in the update request (add binding:"required") or implement patch semantics by loading the existing row and only updating fields that are explicitly set (use pointer fields / separate PATCH endpoint).

Suggested change
script := &service.UsageScript{
BaseURLHost: req.BaseURLHost,
AccountType: req.AccountType,
Script: req.Script,
}
if req.Enabled != nil {
script.Enabled = *req.Enabled
} else {
script.Enabled = true
}
// Require all fields to be present for an update to avoid unsafe partial updates.
if req.BaseURLHost == "" || req.AccountType == "" || req.Script == "" || req.Enabled == nil {
response.BadRequest(c, "base_url_host, account_type, script and enabled are required")
return
}
script := &service.UsageScript{
BaseURLHost: req.BaseURLHost,
AccountType: req.AccountType,
Script: req.Script,
Enabled: *req.Enabled,
}

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +54
export async function toggleEnabled(id: number, enabled: boolean): Promise<UsageScript> {
return update(id, { enabled } as UpdateUsageScriptRequest)
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toggleEnabled force-casts a partial payload ({ enabled }) to UpdateUsageScriptRequest, but update() expects the full object. Given the backend update overwrites fields, calling this will likely wipe base_url_host/account_type/script. Prefer a dedicated backend endpoint for toggling, or change UpdateUsageScriptRequest to support partial updates safely and remove the unsafe cast.

Copilot uses AI. Check for mistakes.

func (UsageScript) Indexes() []ent.Index {
return []ent.Index{
index.Fields("base_url_host", "account_type").Unique(),
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This entity uses SoftDeleteMixin, but the schema defines a plain unique index on (base_url_host, account_type). With soft deletes, a non-partial unique constraint prevents recreating a script with the same keys after deletion. Other schemas document using a partial unique index via SQL migrations (e.g. ent/schema/group.go). Consider removing .Unique() here and enforcing uniqueness via a partial unique index WHERE deleted_at IS NULL (as the SQL migration already does), plus keep a deleted_at index for query performance if needed.

Suggested change
index.Fields("base_url_host", "account_type").Unique(),
index.Fields("base_url_host", "account_type"),

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants