Skip to content

Commit daf9692

Browse files
committed
refactor: update route status handling and improve model binding logic
1 parent 0e07398 commit daf9692

6 files changed

Lines changed: 49 additions & 57 deletions

File tree

backend/internal/infra/persistence/postgres/channel/repository.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -150,11 +150,12 @@ func (r *Repo) ListUpstreams(ctx context.Context, input repository.ListChannelUp
150150
).
151151
Joins(
152152
`LEFT JOIN (
153-
SELECT upstream_id,
154-
COUNT(*) AS models_count,
155-
COUNT(*) FILTER (WHERE status = 'active') AS active_models_count
156-
FROM llm_upstream_models
157-
GROUP BY upstream_id
153+
SELECT um.upstream_id,
154+
COUNT(r.id) AS models_count,
155+
COUNT(r.id) FILTER (WHERE r.status = 'active' AND um.status = 'active') AS active_models_count
156+
FROM llm_upstream_models um
157+
LEFT JOIN llm_model_routes r ON r.upstream_model_id = um.id
158+
GROUP BY um.upstream_id
158159
) AS stats ON stats.upstream_id = u.id`,
159160
)
160161
listQuery = applyUpstreamListFilters(listQuery, input)
@@ -692,10 +693,10 @@ func applyUpstreamModelListFilters(query *gorm.DB, input repository.ListChannelU
692693
)
693694
}
694695
switch strings.TrimSpace(input.RouteStatus) {
696+
case "bound":
697+
query = query.Where("r.id IS NOT NULL")
695698
case "active", "inactive":
696699
query = query.Where("r.id IS NOT NULL AND r.status = ?", input.RouteStatus)
697-
case "unbound":
698-
query = query.Where("r.id IS NULL")
699700
}
700701
if status := strings.TrimSpace(input.UpstreamStatus); status == "active" || status == "inactive" {
701702
query = query.Where("um.status = ?", status)

backend/internal/transport/http/channel/handler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ func (h *Handler) ResetUpstreamCircuit(c *gin.Context) {
352352
// @Param page query int false "页码"
353353
// @Param page_size query int false "每页数量"
354354
// @Param q query string false "搜索关键词"
355-
// @Param route_status query string false "路由状态:active/inactive/unbound"
355+
// @Param route_status query string false "路由状态:bound/active/inactive"
356356
// @Param upstream_status query string false "上游模型状态:active/inactive"
357357
// @Param protocol query string false "接口协议"
358358
// @Param sort query string false "排序:upstream_asc/upstream_desc/platform_asc/platform_desc/status_asc/protocol_asc"

frontend/features/admin/components/sections/upstreams/upstream-models-dialog.tsx

Lines changed: 27 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -205,19 +205,11 @@ const ModelRow = React.memo(function ModelRow({ row, isSelected, onSelect, onUpd
205205
const t = useTranslations("adminChannels");
206206
const platformModelName = row.platformModelNameDraft.trim();
207207
const hasBindingDraft = platformModelName.length > 0;
208-
const isUnboundCatalogRow = row.routeID === 0 && !hasBindingDraft;
209-
const isPendingDelete = row.routeID > 0 && row.routeStatus === "inactive" && !hasBindingDraft;
210-
const routeChecked = row.routeStatus === "active" || (!row.routeStatus && hasBindingDraft);
208+
const routeChecked = row.routeStatus === "active";
211209
const protocolValue = hasBindingDraft ? row.protocol || row.suggestedProtocol || AUTO_PROTOCOL_VALUE : AUTO_PROTOCOL_VALUE;
212210

213211
const handlePlatformModelChange = (value: string) => {
214-
const patch: Partial<Omit<RowDraft, "draftKey" | "isDirty">> = {
215-
platformModelNameDraft: value,
216-
};
217-
if (row.routeID === 0) {
218-
patch.routeStatus = value.trim() ? row.routeStatus || "active" : "";
219-
}
220-
onUpdate(row.draftKey, patch);
212+
onUpdate(row.draftKey, { platformModelNameDraft: value });
221213
};
222214

223215
return (
@@ -238,16 +230,12 @@ const ModelRow = React.memo(function ModelRow({ row, isSelected, onSelect, onUpd
238230
</div>
239231
</TableCell>
240232
<TableCell className="w-[56px] whitespace-nowrap">
241-
{isUnboundCatalogRow ? (
242-
<span className="text-xs text-muted-foreground">{t("modelsDialog.unbound")}</span>
243-
) : (
244-
<Switch
245-
size="sm"
246-
checked={routeChecked}
247-
onCheckedChange={(checked) => onUpdate(row.draftKey, { routeStatus: checked ? "active" : "inactive" })}
248-
aria-label={t("modelsDialog.routeStatusFor", { name: row.upstreamModelName })}
249-
/>
250-
)}
233+
<Switch
234+
size="sm"
235+
checked={routeChecked}
236+
onCheckedChange={(checked) => onUpdate(row.draftKey, { routeStatus: checked ? "active" : "inactive" })}
237+
aria-label={t("modelsDialog.routeStatusFor", { name: row.upstreamModelName })}
238+
/>
251239
</TableCell>
252240
<TableCell className="max-w-[220px] font-mono text-xs text-muted-foreground">
253241
<span className="block truncate" title={row.upstreamModelName}>
@@ -265,7 +253,7 @@ const ModelRow = React.memo(function ModelRow({ row, isSelected, onSelect, onUpd
265253
<TableCell className="w-[176px] whitespace-nowrap">
266254
{!hasBindingDraft ? (
267255
<span className="text-xs text-muted-foreground">
268-
{isPendingDelete ? t("modelsDialog.deleteAfterSave") : t("modelsDialog.unbound")}
256+
{t("modelsDialog.deleteAfterSave")}
269257
</span>
270258
) : (
271259
<Select
@@ -307,9 +295,9 @@ type RemoteModelsDialogProps = {
307295
onImported: () => void;
308296
};
309297

310-
function remoteModelStatusKey(item: AdminLLMRemoteModelItem): "bound" | "synced" | "unsynced" {
298+
function remoteModelStatusKey(item: AdminLLMRemoteModelItem): "bound" | "unbound" | "unsynced" {
311299
if (item.alreadyBound) return "bound";
312-
return item.alreadySynced ? "synced" : "unsynced";
300+
return item.alreadySynced ? "unbound" : "unsynced";
313301
}
314302

315303
function dedupeRemoteModels(items: AdminLLMRemoteModelItem[]): AdminLLMRemoteModelItem[] {
@@ -386,10 +374,10 @@ function RemoteModelsDialog({
386374
try {
387375
const token = await resolveAccessToken();
388376
const data = await listAdminLLMRemoteModels(token, upstream.id);
389-
const unbound = dedupeRemoteModels(data.items.filter((i) => !i.alreadyBound));
390-
setRemoteItems(unbound);
391-
setSelected(new Set(unbound.map((i) => i.upstreamModelName)));
392-
setDraftPlatformModelNames(createDraftPlatformModelNameMap(unbound));
377+
const syncableItems = dedupeRemoteModels(data.items.filter((i) => !i.alreadyBound));
378+
setRemoteItems(syncableItems);
379+
setSelected(new Set(syncableItems.map((i) => i.upstreamModelName)));
380+
setDraftPlatformModelNames(createDraftPlatformModelNameMap(syncableItems));
393381
} catch (err) {
394382
toast.error(resolveErrorMessage(err, t("modelsDialog.remoteLoadFailed")));
395383
onOpenChange(false);
@@ -738,7 +726,7 @@ type UpstreamModelsDialogProps = {
738726
onRemoteOpenHandled?: () => void;
739727
};
740728

741-
type RouteStatusFilter = "all" | "active" | "inactive" | "unbound";
729+
type RouteStatusFilter = "bound" | "active" | "inactive";
742730
type UpstreamStatusFilter = "all" | "active" | "inactive";
743731
type RouteSortValue = "upstream_asc" | "upstream_desc" | "platform_asc" | "platform_desc" | "status_asc" | "protocol_asc";
744732

@@ -762,7 +750,7 @@ const DEFAULT_ROUTE_LIST_PARAMS: RouteListParams = {
762750
page: 1,
763751
pageSize: PAGE_SIZE_DEFAULT,
764752
query: "",
765-
routeStatusFilter: "all",
753+
routeStatusFilter: "bound",
766754
upstreamStatusFilter: "all",
767755
protocolFilter: "",
768756
sortValue: "upstream_asc",
@@ -809,7 +797,7 @@ export function UpstreamModelsDialog({
809797
page: params.page,
810798
pageSize: params.pageSize,
811799
query: params.query,
812-
routeStatus: params.routeStatusFilter === "all" ? "" : params.routeStatusFilter,
800+
routeStatus: params.routeStatusFilter,
813801
upstreamStatus: params.upstreamStatusFilter === "all" ? "" : params.upstreamStatusFilter,
814802
protocol: params.protocolFilter,
815803
sort: params.sortValue,
@@ -896,7 +884,7 @@ export function UpstreamModelsDialog({
896884
const pageCount = Math.max(1, Math.ceil(total / pageSize));
897885
const hasActiveListQuery =
898886
listParams.query !== "" ||
899-
routeStatusFilter !== "all" ||
887+
routeStatusFilter !== "bound" ||
900888
upstreamStatusFilter !== "all" ||
901889
protocolFilter !== "";
902890

@@ -985,6 +973,7 @@ export function UpstreamModelsDialog({
985973
});
986974
}
987975
void loadBindings();
976+
onUpstreamUpdated({ ...upstream });
988977
} catch (err) {
989978
toast.error(resolveErrorMessage(err, t("toast.deleteFailed")));
990979
} finally {
@@ -1064,6 +1053,7 @@ export function UpstreamModelsDialog({
10641053
toast.success(t("modelsDialog.savedChanges", { savedCount }));
10651054
}
10661055
await loadBindings();
1056+
onUpstreamUpdated({ ...upstream });
10671057
} catch (err) {
10681058
toast.error(resolveErrorMessage(err, t("toast.updateFailed")));
10691059
} finally {
@@ -1101,13 +1091,12 @@ export function UpstreamModelsDialog({
11011091
{
11021092
key: "route-status",
11031093
label: t("modelsDialog.routeStatus"),
1104-
value: routeStatusFilter === "all" ? "" : routeStatusFilter,
1105-
onValueChange: (value) => updateListParams({ routeStatusFilter: (value || "all") as RouteStatusFilter }),
1094+
value: routeStatusFilter === "bound" ? "" : routeStatusFilter,
1095+
onValueChange: (value) => updateListParams({ routeStatusFilter: (value || "bound") as RouteStatusFilter }),
11061096
options: [
11071097
{ label: t("modelsDialog.allRoutes"), value: "" },
11081098
{ label: t("status.active"), value: "active" },
11091099
{ label: t("status.inactive"), value: "inactive" },
1110-
{ label: t("modelsDialog.unbound"), value: "unbound" },
11111100
],
11121101
},
11131102
{
@@ -1308,7 +1297,10 @@ export function UpstreamModelsDialog({
13081297
open={newBindingOpen}
13091298
onOpenChange={setNewBindingOpen}
13101299
upstreamId={upstream.id}
1311-
onCreated={loadBindings}
1300+
onCreated={() => {
1301+
void loadBindings();
1302+
onUpstreamUpdated({ ...upstream });
1303+
}}
13121304
/>
13131305
)}
13141306

frontend/features/admin/hooks/use-admin-upstreams.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ export function useAdminUpstreams(): UseAdminUpstreamsState {
387387
if (modelsTarget?.id === updated.id) {
388388
setModelsTarget(updated);
389389
}
390+
void load();
390391
}
391392

392393
return {

frontend/i18n/messages/en-US/admin-channels.json

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -145,18 +145,17 @@
145145
"creating": "Creating...",
146146
"saving": "Saving..."
147147
},
148-
"modelsDialog": {
148+
"modelsDialog": {
149149
"selectKind": "Select kind",
150150
"selectModel": "Select {name}",
151-
"unbound": "Unbound",
152151
"routeStatusFor": "{name} status",
153152
"platformModelName": "Platform model name",
154153
"platformModel": "Platform model",
155154
"deleteAfterSave": "Delete after saving",
156155
"autoProtocol": "Auto match",
157156
"remoteStatus": {
158157
"bound": "Bound",
159-
"synced": "Synced",
158+
"unbound": "Unbound",
160159
"unsynced": "Not synced"
161160
},
162161
"remoteLoadFailed": "Failed to fetch remote models",
@@ -183,18 +182,18 @@
183182
"loadFailed": "Load failed",
184183
"batchDeletePartialFailed": "Some bindings failed to delete",
185184
"batchDeleteDone": "Bindings deleted",
186-
"batchDeleteSummary": "Deleted {successCount}, not found {notFoundCount}, failed {failedCount}. The upstream model catalog is kept.",
185+
"batchDeleteSummary": "Deleted {successCount}, not found {notFoundCount}, failed {failedCount}. The upstream model catalog is kept and can be rebound by syncing.",
187186
"noPendingChanges": "No pending changes",
188187
"upstreamModelRequired": "Upstream model name is required",
189188
"activeRouteRequiresPlatformModel": "Active routes require a platform model name. To unbind, disable the route before saving.",
190189
"duplicateBinding": "Upstream model \"{upstreamModelName}\" is already bound to platform model \"{platformModelName}\"",
191190
"noSavableChanges": "No savable binding changes",
192191
"savedAndDeleted": "Saved {savedCount} changes and deleted {deletedCount} bindings",
193192
"deletedBindings": "Deleted {deletedCount} bindings",
194-
"deleteBindingDescription": "Only platform route bindings are deleted. The upstream model catalog remains as unbound rows.",
193+
"deleteBindingDescription": "Only platform route bindings are deleted. The upstream model catalog is kept and can be rebound by syncing.",
195194
"savedChanges": "Saved {savedCount} changes",
196195
"manageTitle": "Manage route bindings",
197-
"manageDescription": "Manage route bindings from upstream models to platform model names.",
196+
"manageDescription": "Manage configured route bindings. Rebind models through sync when needed.",
198197
"manageSearchPlaceholder": "Search upstream model, platform model, binding code, or protocol",
199198
"refreshBindings": "Refresh bindings",
200199
"allRoutes": "All",
@@ -215,7 +214,7 @@
215214
"noMatchedBindings": "No matching route bindings",
216215
"noBindings": "No route bindings. Sync models or create a binding manually.",
217216
"batchDeleteTitle": "Delete bindings",
218-
"batchDeleteDescription": "Delete {count} selected bindings from this upstream. The model catalog is kept.",
217+
"batchDeleteDescription": "Delete {count} selected bindings from this upstream. The model catalog is kept and can be rebound by syncing.",
219218
"bulkConfirmTitle": "Confirm bulk update",
220219
"bulkConfirmDescription": "Apply this bulk update to {count} selected bindings.",
221220
"bulkConfirmApply": "Apply",

frontend/i18n/messages/zh-CN/admin-channels.json

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -145,18 +145,17 @@
145145
"creating": "创建中…",
146146
"saving": "保存中…"
147147
},
148-
"modelsDialog": {
148+
"modelsDialog": {
149149
"selectKind": "选择类别",
150150
"selectModel": "选择 {name}",
151-
"unbound": "未绑定",
152151
"routeStatusFor": "{name} 状态",
153152
"platformModelName": "平台模型名",
154153
"platformModel": "平台模型",
155154
"deleteAfterSave": "保存后删除",
156155
"autoProtocol": "自动匹配",
157156
"remoteStatus": {
158157
"bound": "已绑定",
159-
"synced": "已同步",
158+
"unbound": "未绑定",
160159
"unsynced": "未同步"
161160
},
162161
"remoteLoadFailed": "获取远端模型失败",
@@ -183,18 +182,18 @@
183182
"loadFailed": "加载失败",
184183
"batchDeletePartialFailed": "批量删除绑定部分失败",
185184
"batchDeleteDone": "批量删除绑定完成",
186-
"batchDeleteSummary": "已删除绑定 {successCount} 条,未找到 {notFoundCount} 条,失败 {failedCount} 条;上游模型目录会保留。",
185+
"batchDeleteSummary": "已删除绑定 {successCount} 条,未找到 {notFoundCount} 条,失败 {failedCount} 条;上游模型目录会保留,可通过同步重新创建绑定",
187186
"noPendingChanges": "没有待保存的修改",
188187
"upstreamModelRequired": "上游真实模型名不能为空",
189188
"activeRouteRequiresPlatformModel": "启用路由必须填写平台模型名;如果要解除绑定,请先关闭路由后保存。",
190189
"duplicateBinding": "上游模型「{upstreamModelName}」已绑定平台模型「{platformModelName}」",
191190
"noSavableChanges": "没有可保存的绑定变更",
192191
"savedAndDeleted": "已保存 {savedCount} 处修改,删除 {deletedCount} 条绑定",
193192
"deletedBindings": "已删除 {deletedCount} 条绑定",
194-
"deleteBindingDescription": "仅删除平台路由关系,上游模型目录会保留为未绑定行",
193+
"deleteBindingDescription": "仅删除平台路由关系,上游模型目录会保留,可通过同步重新创建绑定",
195194
"savedChanges": "已保存 {savedCount} 处修改",
196195
"manageTitle": "管理路由绑定",
197-
"manageDescription": "管理上游模型到平台模型名的路由绑定",
196+
"manageDescription": "管理已配置的路由绑定;需要重新绑定的模型请通过同步添加",
198197
"manageSearchPlaceholder": "搜索上游模型名、平台模型名、绑定编码或协议",
199198
"refreshBindings": "刷新绑定",
200199
"allRoutes": "全部",
@@ -215,7 +214,7 @@
215214
"noMatchedBindings": "没有匹配的路由绑定",
216215
"noBindings": "暂无任何路由绑定,请同步模型或手动新增绑定",
217216
"batchDeleteTitle": "批量删除绑定",
218-
"batchDeleteDescription": "将删除当前上游下已选的 {count} 条绑定关系,但会保留模型目录。",
217+
"batchDeleteDescription": "将删除当前上游下已选的 {count} 条绑定关系,但会保留模型目录。后续可通过同步重新创建绑定。",
219218
"bulkConfirmTitle": "确认批量修改",
220219
"bulkConfirmDescription": "将对已选的 {count} 条绑定应用此批量修改。",
221220
"bulkConfirmApply": "确认应用",

0 commit comments

Comments
 (0)