hank9999 commited on
Commit
280bd89
·
1 Parent(s): 4052aea

feat(admin): 支持删除凭据功能

Browse files
admin-ui/src/api/credentials.ts CHANGED
@@ -78,3 +78,9 @@ export async function addCredential(
78
  const { data } = await api.post<AddCredentialResponse>('/credentials', req)
79
  return data
80
  }
 
 
 
 
 
 
 
78
  const { data } = await api.post<AddCredentialResponse>('/credentials', req)
79
  return data
80
  }
81
+
82
+ // 删除凭据
83
+ export async function deleteCredential(id: number): Promise<SuccessResponse> {
84
+ const { data } = await api.delete<SuccessResponse>(`/credentials/${id}`)
85
+ return data
86
+ }
admin-ui/src/components/credential-card.tsx CHANGED
@@ -1,16 +1,25 @@
1
  import { useState } from 'react'
2
  import { toast } from 'sonner'
3
- import { RefreshCw, ChevronUp, ChevronDown, Wallet } from 'lucide-react'
4
  import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
5
  import { Button } from '@/components/ui/button'
6
  import { Badge } from '@/components/ui/badge'
7
  import { Switch } from '@/components/ui/switch'
8
  import { Input } from '@/components/ui/input'
 
 
 
 
 
 
 
 
9
  import type { CredentialStatusItem } from '@/types/api'
10
  import {
11
  useSetDisabled,
12
  useSetPriority,
13
  useResetFailure,
 
14
  } from '@/hooks/use-credentials'
15
 
16
  interface CredentialCardProps {
@@ -21,10 +30,12 @@ interface CredentialCardProps {
21
  export function CredentialCard({ credential, onViewBalance }: CredentialCardProps) {
22
  const [editingPriority, setEditingPriority] = useState(false)
23
  const [priorityValue, setPriorityValue] = useState(String(credential.priority))
 
24
 
25
  const setDisabled = useSetDisabled()
26
  const setPriority = useSetPriority()
27
  const resetFailure = useResetFailure()
 
28
 
29
  const handleToggleDisabled = () => {
30
  setDisabled.mutate(
@@ -71,6 +82,18 @@ export function CredentialCard({ credential, onViewBalance }: CredentialCardProp
71
  })
72
  }
73
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  const formatExpiry = (expiresAt: string | null) => {
75
  if (!expiresAt) return '未知'
76
  const date = new Date(expiresAt)
@@ -85,151 +108,191 @@ export function CredentialCard({ credential, onViewBalance }: CredentialCardProp
85
  }
86
 
87
  return (
88
- <Card className={credential.isCurrent ? 'ring-2 ring-primary' : ''}>
89
- <CardHeader className="pb-2">
90
- <div className="flex items-center justify-between">
91
- <CardTitle className="text-lg flex items-center gap-2">
92
- 凭据 #{credential.id}
93
- {credential.isCurrent && (
94
- <Badge variant="success">当前</Badge>
95
- )}
96
- {credential.disabled && (
97
- <Badge variant="destructive">已禁用</Badge>
98
- )}
99
- </CardTitle>
100
- <div className="flex items-center gap-2">
101
- <span className="text-sm text-muted-foreground">启用</span>
102
- <Switch
103
- checked={!credential.disabled}
104
- onCheckedChange={handleToggleDisabled}
105
- disabled={setDisabled.isPending}
106
- />
 
 
107
  </div>
108
- </div>
109
- </CardHeader>
110
- <CardContent className="space-y-4">
111
- {/* 信息网格 */}
112
- <div className="grid grid-cols-2 gap-4 text-sm">
113
- <div>
114
- <span className="text-muted-foreground">优先级:</span>
115
- {editingPriority ? (
116
- <div className="inline-flex items-center gap-1 ml-1">
117
- <Input
118
- type="number"
119
- value={priorityValue}
120
- onChange={(e) => setPriorityValue(e.target.value)}
121
- className="w-16 h-7 text-sm"
122
- min="0"
123
- />
124
- <Button
125
- size="sm"
126
- variant="ghost"
127
- className="h-7 w-7 p-0"
128
- onClick={handlePriorityChange}
129
- disabled={setPriority.isPending}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  >
131
-
132
- </Button>
133
- <Button
134
- size="sm"
135
- variant="ghost"
136
- className="h-7 w-7 p-0"
137
- onClick={() => {
138
- setEditingPriority(false)
139
- setPriorityValue(String(credential.priority))
140
- }}
141
- >
142
-
143
- </Button>
144
- </div>
145
- ) : (
146
- <span
147
- className="font-medium cursor-pointer hover:underline ml-1"
148
- onClick={() => setEditingPriority(true)}
149
- >
150
- {credential.priority}
151
- <span className="text-xs text-muted-foreground ml-1">(点击编辑)</span>
152
  </span>
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  )}
154
  </div>
155
- <div>
156
- <span className="text-muted-foreground">失败次数:</span>
157
- <span className={credential.failureCount > 0 ? 'text-red-500 font-medium' : ''}>
158
- {credential.failureCount}
159
- </span>
160
- </div>
161
- <div>
162
- <span className="text-muted-foreground">认证方式:</span>
163
- <span className="font-medium">{credential.authMethod || '未知'}</span>
164
- </div>
165
- <div>
166
- <span className="text-muted-foreground">Token 有效期:</span>
167
- <span className="font-medium">{formatExpiry(credential.expiresAt)}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  </div>
169
- {credential.hasProfileArn && (
170
- <div className="col-span-2">
171
- <Badge variant="secondary">有 Profile ARN</Badge>
172
- </div>
173
- )}
174
- </div>
175
 
176
- {/* 操作按钮 */}
177
- <div className="flex flex-wrap gap-2 pt-2 border-t">
178
- <Button
179
- size="sm"
180
- variant="outline"
181
- onClick={handleReset}
182
- disabled={resetFailure.isPending || credential.failureCount === 0}
183
- >
184
- <RefreshCw className="h-4 w-4 mr-1" />
185
- 重置失败
186
- </Button>
187
- <Button
188
- size="sm"
189
- variant="outline"
190
- onClick={() => {
191
- const newPriority = Math.max(0, credential.priority - 1)
192
- setPriority.mutate(
193
- { id: credential.id, priority: newPriority },
194
- {
195
- onSuccess: (res) => toast.success(res.message),
196
- onError: (err) => toast.error('操作失败: ' + (err as Error).message),
197
- }
198
- )
199
- }}
200
- disabled={setPriority.isPending || credential.priority === 0}
201
- >
202
- <ChevronUp className="h-4 w-4 mr-1" />
203
- 提高优先级
204
- </Button>
205
- <Button
206
- size="sm"
207
- variant="outline"
208
- onClick={() => {
209
- const newPriority = credential.priority + 1
210
- setPriority.mutate(
211
- { id: credential.id, priority: newPriority },
212
- {
213
- onSuccess: (res) => toast.success(res.message),
214
- onError: (err) => toast.error('操作失败: ' + (err as Error).message),
215
- }
216
- )
217
- }}
218
- disabled={setPriority.isPending}
219
- >
220
- <ChevronDown className="h-4 w-4 mr-1" />
221
- 降低优先级
222
- </Button>
223
- <Button
224
- size="sm"
225
- variant="default"
226
- onClick={() => onViewBalance(credential.id)}
227
- >
228
- <Wallet className="h-4 w-4 mr-1" />
229
- 查看余额
230
- </Button>
231
- </div>
232
- </CardContent>
233
- </Card>
234
  )
235
  }
 
1
  import { useState } from 'react'
2
  import { toast } from 'sonner'
3
+ import { RefreshCw, ChevronUp, ChevronDown, Wallet, Trash2 } from 'lucide-react'
4
  import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
5
  import { Button } from '@/components/ui/button'
6
  import { Badge } from '@/components/ui/badge'
7
  import { Switch } from '@/components/ui/switch'
8
  import { Input } from '@/components/ui/input'
9
+ import {
10
+ Dialog,
11
+ DialogContent,
12
+ DialogDescription,
13
+ DialogFooter,
14
+ DialogHeader,
15
+ DialogTitle,
16
+ } from '@/components/ui/dialog'
17
  import type { CredentialStatusItem } from '@/types/api'
18
  import {
19
  useSetDisabled,
20
  useSetPriority,
21
  useResetFailure,
22
+ useDeleteCredential,
23
  } from '@/hooks/use-credentials'
24
 
25
  interface CredentialCardProps {
 
30
  export function CredentialCard({ credential, onViewBalance }: CredentialCardProps) {
31
  const [editingPriority, setEditingPriority] = useState(false)
32
  const [priorityValue, setPriorityValue] = useState(String(credential.priority))
33
+ const [showDeleteDialog, setShowDeleteDialog] = useState(false)
34
 
35
  const setDisabled = useSetDisabled()
36
  const setPriority = useSetPriority()
37
  const resetFailure = useResetFailure()
38
+ const deleteCredential = useDeleteCredential()
39
 
40
  const handleToggleDisabled = () => {
41
  setDisabled.mutate(
 
82
  })
83
  }
84
 
85
+ const handleDelete = () => {
86
+ deleteCredential.mutate(credential.id, {
87
+ onSuccess: (res) => {
88
+ toast.success(res.message)
89
+ setShowDeleteDialog(false)
90
+ },
91
+ onError: (err) => {
92
+ toast.error('删除失败: ' + (err as Error).message)
93
+ },
94
+ })
95
+ }
96
+
97
  const formatExpiry = (expiresAt: string | null) => {
98
  if (!expiresAt) return '未知'
99
  const date = new Date(expiresAt)
 
108
  }
109
 
110
  return (
111
+ <>
112
+ <Card className={credential.isCurrent ? 'ring-2 ring-primary' : ''}>
113
+ <CardHeader className="pb-2">
114
+ <div className="flex items-center justify-between">
115
+ <CardTitle className="text-lg flex items-center gap-2">
116
+ 凭据 #{credential.id}
117
+ {credential.isCurrent && (
118
+ <Badge variant="success">当前</Badge>
119
+ )}
120
+ {credential.disabled && (
121
+ <Badge variant="destructive">已禁用</Badge>
122
+ )}
123
+ </CardTitle>
124
+ <div className="flex items-center gap-2">
125
+ <span className="text-sm text-muted-foreground">启用</span>
126
+ <Switch
127
+ checked={!credential.disabled}
128
+ onCheckedChange={handleToggleDisabled}
129
+ disabled={setDisabled.isPending}
130
+ />
131
+ </div>
132
  </div>
133
+ </CardHeader>
134
+ <CardContent className="space-y-4">
135
+ {/* 信息网格 */}
136
+ <div className="grid grid-cols-2 gap-4 text-sm">
137
+ <div>
138
+ <span className="text-muted-foreground">优先级:</span>
139
+ {editingPriority ? (
140
+ <div className="inline-flex items-center gap-1 ml-1">
141
+ <Input
142
+ type="number"
143
+ value={priorityValue}
144
+ onChange={(e) => setPriorityValue(e.target.value)}
145
+ className="w-16 h-7 text-sm"
146
+ min="0"
147
+ />
148
+ <Button
149
+ size="sm"
150
+ variant="ghost"
151
+ className="h-7 w-7 p-0"
152
+ onClick={handlePriorityChange}
153
+ disabled={setPriority.isPending}
154
+ >
155
+
156
+ </Button>
157
+ <Button
158
+ size="sm"
159
+ variant="ghost"
160
+ className="h-7 w-7 p-0"
161
+ onClick={() => {
162
+ setEditingPriority(false)
163
+ setPriorityValue(String(credential.priority))
164
+ }}
165
+ >
166
+
167
+ </Button>
168
+ </div>
169
+ ) : (
170
+ <span
171
+ className="font-medium cursor-pointer hover:underline ml-1"
172
+ onClick={() => setEditingPriority(true)}
173
  >
174
+ {credential.priority}
175
+ <span className="text-xs text-muted-foreground ml-1">(点击编辑)</span>
176
+ </span>
177
+ )}
178
+ </div>
179
+ <div>
180
+ <span className="text-muted-foreground">失败次数:</span>
181
+ <span className={credential.failureCount > 0 ? 'text-red-500 font-medium' : ''}>
182
+ {credential.failureCount}
 
 
 
 
 
 
 
 
 
 
 
 
183
  </span>
184
+ </div>
185
+ <div>
186
+ <span className="text-muted-foreground">认证方式:</span>
187
+ <span className="font-medium">{credential.authMethod || '未知'}</span>
188
+ </div>
189
+ <div>
190
+ <span className="text-muted-foreground">Token 有效期:</span>
191
+ <span className="font-medium">{formatExpiry(credential.expiresAt)}</span>
192
+ </div>
193
+ {credential.hasProfileArn && (
194
+ <div className="col-span-2">
195
+ <Badge variant="secondary">有 Profile ARN</Badge>
196
+ </div>
197
  )}
198
  </div>
199
+
200
+ {/* 操作按钮 */}
201
+ <div className="flex flex-wrap gap-2 pt-2 border-t">
202
+ <Button
203
+ size="sm"
204
+ variant="outline"
205
+ onClick={handleReset}
206
+ disabled={resetFailure.isPending || credential.failureCount === 0}
207
+ >
208
+ <RefreshCw className="h-4 w-4 mr-1" />
209
+ 重置失败
210
+ </Button>
211
+ <Button
212
+ size="sm"
213
+ variant="outline"
214
+ onClick={() => {
215
+ const newPriority = Math.max(0, credential.priority - 1)
216
+ setPriority.mutate(
217
+ { id: credential.id, priority: newPriority },
218
+ {
219
+ onSuccess: (res) => toast.success(res.message),
220
+ onError: (err) => toast.error('操作失败: ' + (err as Error).message),
221
+ }
222
+ )
223
+ }}
224
+ disabled={setPriority.isPending || credential.priority === 0}
225
+ >
226
+ <ChevronUp className="h-4 w-4 mr-1" />
227
+ 提高优先级
228
+ </Button>
229
+ <Button
230
+ size="sm"
231
+ variant="outline"
232
+ onClick={() => {
233
+ const newPriority = credential.priority + 1
234
+ setPriority.mutate(
235
+ { id: credential.id, priority: newPriority },
236
+ {
237
+ onSuccess: (res) => toast.success(res.message),
238
+ onError: (err) => toast.error('操作失败: ' + (err as Error).message),
239
+ }
240
+ )
241
+ }}
242
+ disabled={setPriority.isPending}
243
+ >
244
+ <ChevronDown className="h-4 w-4 mr-1" />
245
+ 降低优先级
246
+ </Button>
247
+ <Button
248
+ size="sm"
249
+ variant="default"
250
+ onClick={() => onViewBalance(credential.id)}
251
+ >
252
+ <Wallet className="h-4 w-4 mr-1" />
253
+ 查看余额
254
+ </Button>
255
+ <Button
256
+ size="sm"
257
+ variant="destructive"
258
+ onClick={() => setShowDeleteDialog(true)}
259
+ disabled={!credential.disabled}
260
+ title={!credential.disabled ? '需要先禁用凭据才能删除' : undefined}
261
+ >
262
+ <Trash2 className="h-4 w-4 mr-1" />
263
+ 删除
264
+ </Button>
265
  </div>
266
+ </CardContent>
267
+ </Card>
 
 
 
 
268
 
269
+ {/* 删除确认对话框 */}
270
+ <Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
271
+ <DialogContent>
272
+ <DialogHeader>
273
+ <DialogTitle>确认删除凭据</DialogTitle>
274
+ <DialogDescription>
275
+ 您确定要删除凭据 #{credential.id} 吗?此操作无法撤销。
276
+ </DialogDescription>
277
+ </DialogHeader>
278
+ <DialogFooter>
279
+ <Button
280
+ variant="outline"
281
+ onClick={() => setShowDeleteDialog(false)}
282
+ disabled={deleteCredential.isPending}
283
+ >
284
+ 取消
285
+ </Button>
286
+ <Button
287
+ variant="destructive"
288
+ onClick={handleDelete}
289
+ disabled={deleteCredential.isPending}
290
+ >
291
+ 确认删除
292
+ </Button>
293
+ </DialogFooter>
294
+ </DialogContent>
295
+ </Dialog>
296
+ </>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  )
298
  }
admin-ui/src/hooks/use-credentials.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
  resetCredentialFailure,
7
  getCredentialBalance,
8
  addCredential,
 
9
  } from '@/api/credentials'
10
  import type { AddCredentialRequest } from '@/types/api'
11
 
@@ -73,3 +74,14 @@ export function useAddCredential() {
73
  },
74
  })
75
  }
 
 
 
 
 
 
 
 
 
 
 
 
6
  resetCredentialFailure,
7
  getCredentialBalance,
8
  addCredential,
9
+ deleteCredential,
10
  } from '@/api/credentials'
11
  import type { AddCredentialRequest } from '@/types/api'
12
 
 
74
  },
75
  })
76
  }
77
+
78
+ // 删除凭据
79
+ export function useDeleteCredential() {
80
+ const queryClient = useQueryClient()
81
+ return useMutation({
82
+ mutationFn: (id: number) => deleteCredential(id),
83
+ onSuccess: () => {
84
+ queryClient.invalidateQueries({ queryKey: ['credentials'] })
85
+ },
86
+ })
87
+ }
src/admin/handlers.rs CHANGED
@@ -90,3 +90,15 @@ pub async fn add_credential(
90
  Err(e) => (e.status_code(), Json(e.into_response())).into_response(),
91
  }
92
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  Err(e) => (e.status_code(), Json(e.into_response())).into_response(),
91
  }
92
  }
93
+
94
+ /// DELETE /api/admin/credentials/:id
95
+ /// 删除凭据
96
+ pub async fn delete_credential(
97
+ State(state): State<AdminState>,
98
+ Path(id): Path<u64>,
99
+ ) -> impl IntoResponse {
100
+ match state.service.delete_credential(id) {
101
+ Ok(_) => Json(SuccessResponse::new(format!("凭据 #{} 已删除", id))).into_response(),
102
+ Err(e) => (e.status_code(), Json(e.into_response())).into_response(),
103
+ }
104
+ }
src/admin/router.rs CHANGED
@@ -2,13 +2,13 @@
2
 
3
  use axum::{
4
  Router, middleware,
5
- routing::{get, post},
6
  };
7
 
8
  use super::{
9
  handlers::{
10
- add_credential, get_all_credentials, get_credential_balance, reset_failure_count,
11
- set_credential_disabled, set_credential_priority,
12
  },
13
  middleware::{AdminState, admin_auth_middleware},
14
  };
@@ -18,6 +18,7 @@ use super::{
18
  /// # 端点
19
  /// - `GET /credentials` - 获取所有凭据状态
20
  /// - `POST /credentials` - 添加新凭据
 
21
  /// - `POST /credentials/:id/disabled` - 设置凭据禁用状态
22
  /// - `POST /credentials/:id/priority` - 设置凭据优先级
23
  /// - `POST /credentials/:id/reset` - 重置失败计数
@@ -33,6 +34,7 @@ pub fn create_admin_router(state: AdminState) -> Router {
33
  "/credentials",
34
  get(get_all_credentials).post(add_credential),
35
  )
 
36
  .route("/credentials/{id}/disabled", post(set_credential_disabled))
37
  .route("/credentials/{id}/priority", post(set_credential_priority))
38
  .route("/credentials/{id}/reset", post(reset_failure_count))
 
2
 
3
  use axum::{
4
  Router, middleware,
5
+ routing::{delete, get, post},
6
  };
7
 
8
  use super::{
9
  handlers::{
10
+ add_credential, delete_credential, get_all_credentials, get_credential_balance,
11
+ reset_failure_count, set_credential_disabled, set_credential_priority,
12
  },
13
  middleware::{AdminState, admin_auth_middleware},
14
  };
 
18
  /// # 端点
19
  /// - `GET /credentials` - 获取所有凭据状态
20
  /// - `POST /credentials` - 添加新凭据
21
+ /// - `DELETE /credentials/:id` - 删除凭据
22
  /// - `POST /credentials/:id/disabled` - 设置凭据禁用状态
23
  /// - `POST /credentials/:id/priority` - 设置凭据优先级
24
  /// - `POST /credentials/:id/reset` - 重置失败计数
 
34
  "/credentials",
35
  get(get_all_credentials).post(add_credential),
36
  )
37
+ .route("/credentials/{id}", delete(delete_credential))
38
  .route("/credentials/{id}/disabled", post(set_credential_disabled))
39
  .route("/credentials/{id}/priority", post(set_credential_priority))
40
  .route("/credentials/{id}/reset", post(reset_failure_count))
src/admin/service.rs CHANGED
@@ -144,6 +144,13 @@ impl AdminService {
144
  })
145
  }
146
 
 
 
 
 
 
 
 
147
  /// 分类简单操作错误(set_disabled, set_priority, reset_and_enable)
148
  fn classify_error(&self, e: anyhow::Error, id: u64) -> AdminServiceError {
149
  let msg = e.to_string();
@@ -210,4 +217,16 @@ impl AdminService {
210
  AdminServiceError::InternalError(msg)
211
  }
212
  }
 
 
 
 
 
 
 
 
 
 
 
 
213
  }
 
144
  })
145
  }
146
 
147
+ /// 删除凭据
148
+ pub fn delete_credential(&self, id: u64) -> Result<(), AdminServiceError> {
149
+ self.token_manager
150
+ .delete_credential(id)
151
+ .map_err(|e| self.classify_delete_error(e, id))
152
+ }
153
+
154
  /// 分类简单操作错误(set_disabled, set_priority, reset_and_enable)
155
  fn classify_error(&self, e: anyhow::Error, id: u64) -> AdminServiceError {
156
  let msg = e.to_string();
 
217
  AdminServiceError::InternalError(msg)
218
  }
219
  }
220
+
221
+ /// 分类删除凭据错误
222
+ fn classify_delete_error(&self, e: anyhow::Error, id: u64) -> AdminServiceError {
223
+ let msg = e.to_string();
224
+ if msg.contains("不存在") {
225
+ AdminServiceError::NotFound { id }
226
+ } else if msg.contains("只能删除已禁用的凭据") {
227
+ AdminServiceError::InvalidCredential(msg)
228
+ } else {
229
+ AdminServiceError::InternalError(msg)
230
+ }
231
+ }
232
  }
src/kiro/token_manager.rs CHANGED
@@ -1088,6 +1088,69 @@ impl MultiTokenManager {
1088
  tracing::info!("成功添加凭据 #{}", new_id);
1089
  Ok(new_id)
1090
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1091
  }
1092
 
1093
  #[cfg(test)]
 
1088
  tracing::info!("成功添加凭据 #{}", new_id);
1089
  Ok(new_id)
1090
  }
1091
+
1092
+ /// 删除凭据(Admin API)
1093
+ ///
1094
+ /// # 前置条件
1095
+ /// - 凭据必须已禁用(disabled = true)
1096
+ ///
1097
+ /// # 行为
1098
+ /// 1. 验证凭据存在
1099
+ /// 2. 验证凭据已禁用
1100
+ /// 3. 从 entries 移除
1101
+ /// 4. 如果删除的是当前凭据,切换到优先级最高的可用凭据
1102
+ /// 5. 如果删除后没有凭据,将 current_id 重置为 0
1103
+ /// 6. 持久化到文件
1104
+ ///
1105
+ /// # 返回
1106
+ /// - `Ok(())` - 删除成功
1107
+ /// - `Err(_)` - 凭据不存在、未禁用或持久化失败
1108
+ pub fn delete_credential(&self, id: u64) -> anyhow::Result<()> {
1109
+ let was_current = {
1110
+ let mut entries = self.entries.lock();
1111
+
1112
+ // 查找凭据
1113
+ let entry = entries
1114
+ .iter()
1115
+ .find(|e| e.id == id)
1116
+ .ok_or_else(|| anyhow::anyhow!("凭据不存在: {}", id))?;
1117
+
1118
+ // 检查是否已禁用
1119
+ if !entry.disabled {
1120
+ anyhow::bail!("只能删除已禁用的凭据(请先禁用凭据 #{})", id);
1121
+ }
1122
+
1123
+ // 记录是否是当前凭据
1124
+ let current_id = *self.current_id.lock();
1125
+ let was_current = current_id == id;
1126
+
1127
+ // 删除凭据
1128
+ entries.retain(|e| e.id != id);
1129
+
1130
+ was_current
1131
+ };
1132
+
1133
+ // 如果删除的是当前凭据,切换到优先级最高的可用凭据
1134
+ if was_current {
1135
+ self.select_highest_priority();
1136
+ }
1137
+
1138
+ // 如果删除后没有任何凭据,将 current_id 重置为 0(与初始化行为保持一致)
1139
+ {
1140
+ let entries = self.entries.lock();
1141
+ if entries.is_empty() {
1142
+ let mut current_id = self.current_id.lock();
1143
+ *current_id = 0;
1144
+ tracing::info!("所有凭据已删除,current_id 已重置为 0");
1145
+ }
1146
+ }
1147
+
1148
+ // 持久化更改
1149
+ self.persist_credentials()?;
1150
+
1151
+ tracing::info!("已删除凭据 #{}", id);
1152
+ Ok(())
1153
+ }
1154
  }
1155
 
1156
  #[cfg(test)]