Skip to content

v0.0.20 ​

✨ New Features ​

  • Channel modal: searchable multi-select for the model field (web/default-pro/src/views/channel/Channel.vue):
    • Replaced the comma-separated a-input with an arco a-select (multiple + allow-search + allow-clear); the option source is /api/channel/models on mount.
    • form.models switched from string to []string; submissions join(',') to keep the backend's stored format; parseModelsField splits the backend string back into an array on edit.
    • Models already present on the form are unioned into the option set so legacy "wild" models stay visible when reopening the dialog.
  • Channel modal: new "Model Redirect" textarea (Channel.vue):
    • Adds the model_mapping field (maps to backend channels.model_mapping) backed by a-textarea accepting a JSON object; submission is gated by JSON.parse + typeof === 'object' validation.
    • The backend column has always existed (model/channel.go:38); the front-end simply never exposed it. Now aligned with the reference web/default EditChannel form.
  • Channel modal: groups converted to a multi-select dropdown and moved under the name field (Channel.vue):
    • Replaced the comma-separated a-input with arco a-select (multiple + allow-search + allow-clear) sourced from /api/group/ (controller/group.go).
    • The form-item moved from under "Models" to under "Name", matching the reference web/default order.
    • form.groups switched to an array; submissions join(',') to preserve the backend's stored format.
  • Channel modal: model dropdown source switched to the model_price table (Channel.vue:fetchAvailableModels):
    • The dropdown now reads GET /api/model_price/options (admin-accessible), aligning with the billing source of truth.
    • Operators can add a model to the dropdown without code changes — just add a row in Pricing with the desired model_name.
    • The form.models union fallback is preserved so re-opening the edit dialog on a historical channel still shows its saved selection.
  • New read-only endpoint GET /api/model_price/options (controller/model_price.go):
    • Returns only enabled=true model_name strings, sorted ascending; rows with empty model_name are silently skipped to avoid dirty data in the dropdown.
    • AdminAuth-accessible; isolated from the RootAuth-protected write endpoints. The old GET /api/model_price/ now carries a // Deprecated: comment and stays for root admins to view the full pricing payload.
  • Token modal: "Never Expire" checkbox next to the expiry picker (Token.vue):
    • Adds a "永不过期" checkbox to the right of the expiry picker; checking it disables the picker.
    • Adds form.never_expire to the form state; editing auto-checks it when the existing expired_time=0.
    • On submit, expired_time is sent as 0 when "Never Expire" is checked or the picker is empty (matching the backend's 0 = never expire convention); the helper text under the field toggles with the checked state.
  • Token modal: "Unlimited Quota" checkbox folded inline with the quota input (Token.vue):
    • The previously standalone checkbox form-item is collapsed into the right side of the remain_quota input. The shared .input-with-checkbox flex row layout is reused for the new "Never Expire" checkbox.

🐛 Bug Fixes ​

  • "Page is not found" ReferenceError after creating a token (Token.vue:handleSubmit):
    • The code mistakenly wrote page.value = 0 (the actual variable is activePage); the ReferenceError surfaced as a stray "Page is not found" toast.
    • Replaced with activePage.value = 1 so a successful create lands on page 1.
  • Residual-id leakage when creating pricing entries via the pricing modal (4-layer defence):
    • Frontend PricingSetting.vue clears mpForm.id / gpForm.id on a fresh "Add" click so the POST body never carries the previously-edited id.
    • controller.AddModelPrice / AddGroupPrice force price.Id = 0 after c.ShouldBindJSON, so even a buggy client cannot persist a client-supplied id.
    • model.ModelPrice.Insert / GroupPrice.Insert defensively zero p.Id = 0 as a final guard against future callers passing an edit-state id into Insert().
    • Any single layer being skipped is harmless on its own; we keep all three because a failure here is a primary-key collision level data incident.

🔧 Refactor ​

  • "Unlimited Quota" checkbox moved inline with the quota input (Token.vue): the standalone <a-form-item> is folded into the input row; the shared .input-with-checkbox flex row (12px gap, nowrap checkbox) is also reused by the new "Never Expire" checkbox.
  • Model Redirect input and hint stacked vertically (Channel.vue): wrapped in a .form-stack flex column; new .form-hint-block modifier overrides .form-hint's margin-left in favour of margin-top, leaving other inline hints (key / fallback / fallback_priority) untouched.

🎨 Style ​

  • Date-picker suffix icon removed (Token.vue): passing <template #suffix-icon></template> overrides arco's default IconCalendar, aligning the picker width with the <a-input-number> on the remain_quota row.
  • Model Redirect hint uses pre-wrap + break-all (Channel.vue): long JSON sample text wraps naturally instead of being truncated to a single line.

🧪 Tests ​

  • ModelPrice / GroupPrice Insert Id-zeroing fallback (model/model_price_test.go): asserts Insert() strips a non-zero Id to avoid primary-key collisions.
  • AddModelPrice / AddGroupPrice HTTP-layer Id-zeroing end-to-end (controller/model_price_test.go): seeds a pre-existing row at id=9999, then submits an "add" with id=9999; asserts the old row is intact and the new row gets an auto-incremented id rather than 9999.
  • user_test TestMain migrated to include ModelPrice / GroupPrice tables (controller/user_test.go): the in-memory SQLite now auto-migrates ModelPrice and GroupPrice so the pricing tests share the same harness.
  • ListModelPriceOptions enabled-filter + empty-name guard (controller/model_price_test.go): seeds 2 enabled + 1 disabled row (raw SQL bypasses GORM's default:true;not null coercion on explicit false); asserts only enabled rows are returned and an empty model_name never leaks into the payload.

⚠️ Upgrade Notes ​

  • Zero database migration: no schema changes.
  • Frontend rebuild required: all changes under web/default-pro/ require npm run build so the new assets are re-embedded into the Go binary via //go:embed.
  • Backend binary restart required: the new ListModelPriceOptions handler and its route only become available after the new binary is running.
  • Behavior changes:
    • The channel modal's "Model" dropdown source is switched from the compile-time catalogue to the runtime model_price table. Historical channels that have saved models absent from model_price (e.g. gpt-4-0314, text-davinci-003) will not see those options in the dropdown — but the form.models union fallback keeps the saved selection visible when re-opening the edit dialog, so the save path remains intact. To bring a historical model into the dropdown, add a row in Pricing with the corresponding model_name.
    • The channel modal's "Groups" field is now a dropdown sourced from /api/group/ (defaults to default / vip / svip from group_prices). Add a group_prices row to expose additional groups.
    • The token modal's "Never Expire" and "Unlimited Quota" checkboxes default to unchecked; editing an existing token auto-checks them when the backend value is 0 / -1 (never_expire = !exp, unlimited_quota = !!record.unlimited_quota).
  • Build & test:
    • go build ./... passes.
    • go test ./controller/ ./model/ ./middleware/ is green (4 new pricing / model-option assertions).
    • cd web/default-pro && npm run build passes.