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-inputwith an arcoa-select(multiple+allow-search+allow-clear); the option source is/api/channel/modelson mount. form.modelsswitched fromstringto[]string; submissionsjoin(',')to keep the backend's stored format;parseModelsFieldsplits 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.
- Replaced the comma-separated
- Channel modal: new "Model Redirect" textarea (
Channel.vue):- Adds the
model_mappingfield (maps to backendchannels.model_mapping) backed bya-textareaaccepting a JSON object; submission is gated byJSON.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.
- Adds the
- Channel modal: groups converted to a multi-select dropdown and moved under the name field (
Channel.vue):- Replaced the comma-separated
a-inputwith arcoa-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.groupsswitched to an array; submissionsjoin(',')to preserve the backend's stored format.
- Replaced the comma-separated
- Channel modal: model dropdown source switched to the
model_pricetable (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.modelsunion fallback is preserved so re-opening the edit dialog on a historical channel still shows its saved selection.
- The dropdown now reads
- New read-only endpoint
GET /api/model_price/options(controller/model_price.go):- Returns only
enabled=truemodel_namestrings, sorted ascending; rows with emptymodel_nameare 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.
- Returns only
- 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_expireto the form state; editing auto-checks it when the existingexpired_time=0. - On submit,
expired_timeis sent as0when "Never Expire" is checked or the picker is empty (matching the backend's0 = never expireconvention); 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-checkboxflex row layout is reused for the new "Never Expire" checkbox.
- The previously standalone checkbox form-item is collapsed into the right side of the remain_quota input. The shared
🐛 Bug Fixes
- "Page is not found" ReferenceError after creating a token (
Token.vue:handleSubmit):- The code mistakenly wrote
page.value = 0(the actual variable isactivePage); the ReferenceError surfaced as a stray "Page is not found" toast. - Replaced with
activePage.value = 1so a successful create lands on page 1.
- The code mistakenly wrote
- Residual-id leakage when creating pricing entries via the pricing modal (4-layer defence):
- Frontend
PricingSetting.vueclearsmpForm.id/gpForm.idon a fresh "Add" click so the POST body never carries the previously-edited id. controller.AddModelPrice/AddGroupPriceforceprice.Id = 0afterc.ShouldBindJSON, so even a buggy client cannot persist a client-supplied id.model.ModelPrice.Insert/GroupPrice.Insertdefensively zerop.Id = 0as a final guard against future callers passing an edit-state id intoInsert().- 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.
- Frontend
🔧 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-checkboxflex row (12px gap,nowrapcheckbox) is also reused by the new "Never Expire" checkbox. - Model Redirect input and hint stacked vertically (
Channel.vue): wrapped in a.form-stackflex column; new.form-hint-blockmodifier overrides.form-hint'smargin-leftin favour ofmargin-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 defaultIconCalendar, 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/GroupPriceInsert Id-zeroing fallback (model/model_price_test.go): assertsInsert()strips a non-zeroIdto avoid primary-key collisions.AddModelPrice/AddGroupPriceHTTP-layer Id-zeroing end-to-end (controller/model_price_test.go): seeds a pre-existing row atid=9999, then submits an "add" withid=9999; asserts the old row is intact and the new row gets an auto-incremented id rather than 9999.user_testTestMain migrated to include ModelPrice / GroupPrice tables (controller/user_test.go): the in-memory SQLite now auto-migratesModelPriceandGroupPriceso the pricing tests share the same harness.ListModelPriceOptionsenabled-filter + empty-name guard (controller/model_price_test.go): seeds 2 enabled + 1 disabled row (raw SQL bypasses GORM'sdefault:true;not nullcoercion on explicitfalse); asserts only enabled rows are returned and an emptymodel_namenever leaks into the payload.
⚠️ Upgrade Notes
- Zero database migration: no schema changes.
- Frontend rebuild required: all changes under
web/default-pro/requirenpm run buildso the new assets are re-embedded into the Go binary via//go:embed. - Backend binary restart required: the new
ListModelPriceOptionshandler 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_pricetable. Historical channels that have saved models absent frommodel_price(e.g.gpt-4-0314,text-davinci-003) will not see those options in the dropdown — but theform.modelsunion 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 correspondingmodel_name. - The channel modal's "Groups" field is now a dropdown sourced from
/api/group/(defaults todefault/vip/svipfromgroup_prices). Add agroup_pricesrow 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).
- The channel modal's "Model" dropdown source is switched from the compile-time catalogue to the runtime
- Build & test:
go build ./...passes.go test ./controller/ ./model/ ./middleware/is green (4 new pricing / model-option assertions).cd web/default-pro && npm run buildpasses.