Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
B
bit-pm
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
燕伟桐
bit-pm
Commits
ea9301c7
Unverified
提交
ea9301c7
authored
5月 12, 2025
作者:
Will Chen
提交者:
GitHub
5月 12, 2025
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Support delete custom model (#136)
上级
e1150749
全部展开
显示空白字符变更
内嵌
并排
正在显示
8 个修改的文件
包含
390 行增加
和
4 行删除
+390
-4
package-lock.json
package-lock.json
+0
-0
package.json
package.json
+2
-1
ModelsSection.tsx
src/components/settings/ModelsSection.tsx
+89
-2
alert-dialog.tsx
src/components/ui/alert-dialog.tsx
+155
-0
useDeleteCustomModel.ts
src/hooks/useDeleteCustomModel.ts
+49
-0
language_model_handlers.ts
src/ipc/handlers/language_model_handlers.ts
+76
-1
ipc_client.ts
src/ipc/ipc_client.ts
+17
-0
preload.ts
src/preload.ts
+2
-0
没有找到文件。
package-lock.json
浏览文件 @
ea9301c7
差异被折叠。
点击展开。
package.json
浏览文件 @
ea9301c7
...
@@ -81,13 +81,14 @@
...
@@ -81,13 +81,14 @@
"
@monaco-editor/react
"
:
"^4.7.0-rc.0"
,
"
@monaco-editor/react
"
:
"^4.7.0-rc.0"
,
"
@openrouter/ai-sdk-provider
"
:
"^0.4.5"
,
"
@openrouter/ai-sdk-provider
"
:
"^0.4.5"
,
"
@radix-ui/react-accordion
"
:
"^1.2.4"
,
"
@radix-ui/react-accordion
"
:
"^1.2.4"
,
"
@radix-ui/react-alert-dialog
"
:
"^1.1.13"
,
"
@radix-ui/react-dialog
"
:
"^1.1.7"
,
"
@radix-ui/react-dialog
"
:
"^1.1.7"
,
"
@radix-ui/react-dropdown-menu
"
:
"^2.1.7"
,
"
@radix-ui/react-dropdown-menu
"
:
"^2.1.7"
,
"
@radix-ui/react-label
"
:
"^2.1.4"
,
"
@radix-ui/react-label
"
:
"^2.1.4"
,
"
@radix-ui/react-popover
"
:
"^1.1.7"
,
"
@radix-ui/react-popover
"
:
"^1.1.7"
,
"
@radix-ui/react-select
"
:
"^2.2.2"
,
"
@radix-ui/react-select
"
:
"^2.2.2"
,
"
@radix-ui/react-separator
"
:
"^1.1.2"
,
"
@radix-ui/react-separator
"
:
"^1.1.2"
,
"
@radix-ui/react-slot
"
:
"^1.
1
.2"
,
"
@radix-ui/react-slot
"
:
"^1.
2
.2"
,
"
@radix-ui/react-switch
"
:
"^1.2.0"
,
"
@radix-ui/react-switch
"
:
"^1.2.0"
,
"
@radix-ui/react-toggle
"
:
"^1.1.3"
,
"
@radix-ui/react-toggle
"
:
"^1.1.3"
,
"
@radix-ui/react-toggle-group
"
:
"^1.1.3"
,
"
@radix-ui/react-toggle-group
"
:
"^1.1.3"
,
...
...
src/components/settings/ModelsSection.tsx
浏览文件 @
ea9301c7
import
{
useState
}
from
"react"
;
import
{
useState
}
from
"react"
;
import
{
AlertTriangle
,
PlusIcon
}
from
"lucide-react"
;
import
{
AlertTriangle
,
PlusIcon
,
TrashIcon
}
from
"lucide-react"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Skeleton
}
from
"@/components/ui/skeleton"
;
import
{
Skeleton
}
from
"@/components/ui/skeleton"
;
import
{
Alert
,
AlertDescription
,
AlertTitle
}
from
"@/components/ui/alert"
;
import
{
Alert
,
AlertDescription
,
AlertTitle
}
from
"@/components/ui/alert"
;
import
{
CreateCustomModelDialog
}
from
"@/components/CreateCustomModelDialog"
;
import
{
CreateCustomModelDialog
}
from
"@/components/CreateCustomModelDialog"
;
import
{
useLanguageModelsForProvider
}
from
"@/hooks/useLanguageModelsForProvider"
;
// Use the hook directly here
import
{
useLanguageModelsForProvider
}
from
"@/hooks/useLanguageModelsForProvider"
;
// Use the hook directly here
import
{
useDeleteCustomModel
}
from
"@/hooks/useDeleteCustomModel"
;
// Import the new hook
import
{
AlertDialog
,
AlertDialogAction
,
AlertDialogCancel
,
AlertDialogContent
,
AlertDialogDescription
,
AlertDialogFooter
,
AlertDialogHeader
,
AlertDialogTitle
,
}
from
"@/components/ui/alert-dialog"
;
interface
ModelsSectionProps
{
interface
ModelsSectionProps
{
providerId
:
string
;
providerId
:
string
;
...
@@ -12,6 +23,9 @@ interface ModelsSectionProps {
...
@@ -12,6 +23,9 @@ interface ModelsSectionProps {
export
function
ModelsSection
({
providerId
}:
ModelsSectionProps
)
{
export
function
ModelsSection
({
providerId
}:
ModelsSectionProps
)
{
const
[
isCustomModelDialogOpen
,
setIsCustomModelDialogOpen
]
=
useState
(
false
);
const
[
isCustomModelDialogOpen
,
setIsCustomModelDialogOpen
]
=
useState
(
false
);
const
[
isConfirmDeleteDialogOpen
,
setIsConfirmDeleteDialogOpen
]
=
useState
(
false
);
const
[
modelToDelete
,
setModelToDelete
]
=
useState
<
string
|
null
>
(
null
);
// Fetch custom models within this component now
// Fetch custom models within this component now
const
{
const
{
...
@@ -21,6 +35,30 @@ export function ModelsSection({ providerId }: ModelsSectionProps) {
...
@@ -21,6 +35,30 @@ export function ModelsSection({ providerId }: ModelsSectionProps) {
refetch
:
refetchModels
,
refetch
:
refetchModels
,
}
=
useLanguageModelsForProvider
(
providerId
);
}
=
useLanguageModelsForProvider
(
providerId
);
const
{
mutate
:
deleteModel
,
isPending
:
isDeleting
}
=
useDeleteCustomModel
({
onSuccess
:
()
=>
{
refetchModels
();
// Refetch models list after successful deletion
// Optionally show a success toast here
},
onError
:
(
error
:
Error
)
=>
{
// Optionally show an error toast here
console
.
error
(
"Failed to delete model:"
,
error
);
},
});
const
handleDeleteClick
=
(
modelApiName
:
string
)
=>
{
setModelToDelete
(
modelApiName
);
setIsConfirmDeleteDialogOpen
(
true
);
};
const
handleConfirmDelete
=
()
=>
{
if
(
modelToDelete
)
{
deleteModel
({
providerId
,
modelApiName
:
modelToDelete
});
setModelToDelete
(
null
);
}
setIsConfirmDeleteDialogOpen
(
false
);
};
return
(
return
(
<
div
className=
"mt-8 border-t pt-6"
>
<
div
className=
"mt-8 border-t pt-6"
>
<
h2
className=
"text-2xl font-semibold mb-4"
>
Models
</
h2
>
<
h2
className=
"text-2xl font-semibold mb-4"
>
Models
</
h2
>
...
@@ -53,7 +91,17 @@ export function ModelsSection({ providerId }: ModelsSectionProps) {
...
@@ -53,7 +91,17 @@ export function ModelsSection({ providerId }: ModelsSectionProps) {
<
h4
className=
"text-lg font-semibold text-gray-800 dark:text-gray-100"
>
<
h4
className=
"text-lg font-semibold text-gray-800 dark:text-gray-100"
>
{
model
.
displayName
}
{
model
.
displayName
}
</
h4
>
</
h4
>
{
/* Optional: Add an edit/delete button here later */
}
{
model
.
type
===
"custom"
&&
(
<
Button
variant=
"ghost"
size=
"icon"
onClick=
{
()
=>
handleDeleteClick
(
model
.
apiName
)
}
disabled=
{
isDeleting
}
className=
"text-red-500 hover:text-red-700 hover:bg-red-100 dark:hover:bg-red-900/50 h-8 w-8"
>
<
TrashIcon
className=
"h-4 w-4"
/>
</
Button
>
)
}
</
div
>
</
div
>
<
p
className=
"text-sm text-gray-500 dark:text-gray-400 italic"
>
<
p
className=
"text-sm text-gray-500 dark:text-gray-400 italic"
>
{
model
.
apiName
}
{
model
.
apiName
}
...
@@ -75,12 +123,18 @@ export function ModelsSection({ providerId }: ModelsSectionProps) {
...
@@ -75,12 +123,18 @@ export function ModelsSection({ providerId }: ModelsSectionProps) {
</
span
>
</
span
>
)
}
)
}
</
div
>
</
div
>
<
div
className=
"flex flex-wrap gap-x-2"
>
<
span
className=
"mt-2 inline-block bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded-full dark:bg-blue-900 dark:text-blue-300"
>
{
model
.
type
===
"cloud"
?
"Built-in"
:
"Custom"
}
</
span
>
{
model
.
tag
&&
(
{
model
.
tag
&&
(
<
span
className=
"mt-2 inline-block bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded-full dark:bg-blue-900 dark:text-blue-300"
>
<
span
className=
"mt-2 inline-block bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded-full dark:bg-blue-900 dark:text-blue-300"
>
{
model
.
tag
}
{
model
.
tag
}
</
span
>
</
span
>
)
}
)
}
</
div
>
</
div
>
</
div
>
))
}
))
}
</
div
>
</
div
>
)
}
)
}
...
@@ -109,6 +163,39 @@ export function ModelsSection({ providerId }: ModelsSectionProps) {
...
@@ -109,6 +163,39 @@ export function ModelsSection({ providerId }: ModelsSectionProps) {
}
}
}
}
providerId=
{
providerId
}
providerId=
{
providerId
}
/>
/>
<
AlertDialog
open=
{
isConfirmDeleteDialogOpen
}
onOpenChange=
{
setIsConfirmDeleteDialogOpen
}
>
<
AlertDialogContent
>
<
AlertDialogHeader
>
<
AlertDialogTitle
>
Are you sure you want to delete this model?
</
AlertDialogTitle
>
<
AlertDialogDescription
>
This action cannot be undone. This will permanently delete the
custom model "
{
modelToDelete
?
models
?.
find
((
m
)
=>
m
.
apiName
===
modelToDelete
)
?.
displayName
||
modelToDelete
:
""
}
" (API Name:
{
modelToDelete
}
).
</
AlertDialogDescription
>
</
AlertDialogHeader
>
<
AlertDialogFooter
>
<
AlertDialogCancel
onClick=
{
()
=>
setModelToDelete
(
null
)
}
>
Cancel
</
AlertDialogCancel
>
<
AlertDialogAction
onClick=
{
handleConfirmDelete
}
className=
"bg-red-600 hover:bg-red-700"
>
{
isDeleting
?
"Deleting..."
:
"Yes, delete it"
}
</
AlertDialogAction
>
</
AlertDialogFooter
>
</
AlertDialogContent
>
</
AlertDialog
>
</
div
>
</
div
>
);
);
}
}
src/components/ui/alert-dialog.tsx
0 → 100644
浏览文件 @
ea9301c7
import
*
as
React
from
"react"
;
import
*
as
AlertDialogPrimitive
from
"@radix-ui/react-alert-dialog"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
buttonVariants
}
from
"@/components/ui/button"
;
function
AlertDialog
({
...
props
}:
React
.
ComponentProps
<
typeof
AlertDialogPrimitive
.
Root
>
)
{
return
<
AlertDialogPrimitive
.
Root
data
-
slot=
"alert-dialog"
{
...
props
}
/>;
}
function
AlertDialogTrigger
({
...
props
}:
React
.
ComponentProps
<
typeof
AlertDialogPrimitive
.
Trigger
>
)
{
return
(
<
AlertDialogPrimitive
.
Trigger
data
-
slot=
"alert-dialog-trigger"
{
...
props
}
/>
);
}
function
AlertDialogPortal
({
...
props
}:
React
.
ComponentProps
<
typeof
AlertDialogPrimitive
.
Portal
>
)
{
return
(
<
AlertDialogPrimitive
.
Portal
data
-
slot=
"alert-dialog-portal"
{
...
props
}
/>
);
}
function
AlertDialogOverlay
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
AlertDialogPrimitive
.
Overlay
>
)
{
return
(
<
AlertDialogPrimitive
.
Overlay
data
-
slot=
"alert-dialog-overlay"
className=
{
cn
(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50"
,
className
,
)
}
{
...
props
}
/>
);
}
function
AlertDialogContent
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
AlertDialogPrimitive
.
Content
>
)
{
return
(
<
AlertDialogPortal
>
<
AlertDialogOverlay
/>
<
AlertDialogPrimitive
.
Content
data
-
slot=
"alert-dialog-content"
className=
{
cn
(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg"
,
className
,
)
}
{
...
props
}
/>
</
AlertDialogPortal
>
);
}
function
AlertDialogHeader
({
className
,
...
props
}:
React
.
ComponentProps
<
"div"
>
)
{
return
(
<
div
data
-
slot=
"alert-dialog-header"
className=
{
cn
(
"flex flex-col gap-2 text-center sm:text-left"
,
className
)
}
{
...
props
}
/>
);
}
function
AlertDialogFooter
({
className
,
...
props
}:
React
.
ComponentProps
<
"div"
>
)
{
return
(
<
div
data
-
slot=
"alert-dialog-footer"
className=
{
cn
(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end"
,
className
,
)
}
{
...
props
}
/>
);
}
function
AlertDialogTitle
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
AlertDialogPrimitive
.
Title
>
)
{
return
(
<
AlertDialogPrimitive
.
Title
data
-
slot=
"alert-dialog-title"
className=
{
cn
(
"text-lg font-semibold"
,
className
)
}
{
...
props
}
/>
);
}
function
AlertDialogDescription
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
AlertDialogPrimitive
.
Description
>
)
{
return
(
<
AlertDialogPrimitive
.
Description
data
-
slot=
"alert-dialog-description"
className=
{
cn
(
"text-muted-foreground text-sm"
,
className
)
}
{
...
props
}
/>
);
}
function
AlertDialogAction
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
AlertDialogPrimitive
.
Action
>
)
{
return
(
<
AlertDialogPrimitive
.
Action
className=
{
cn
(
buttonVariants
(),
className
)
}
{
...
props
}
/>
);
}
function
AlertDialogCancel
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
AlertDialogPrimitive
.
Cancel
>
)
{
return
(
<
AlertDialogPrimitive
.
Cancel
className=
{
cn
(
buttonVariants
({
variant
:
"outline"
}),
className
)
}
{
...
props
}
/>
);
}
export
{
AlertDialog
,
AlertDialogPortal
,
AlertDialogOverlay
,
AlertDialogTrigger
,
AlertDialogContent
,
AlertDialogHeader
,
AlertDialogFooter
,
AlertDialogTitle
,
AlertDialogDescription
,
AlertDialogAction
,
AlertDialogCancel
,
};
src/hooks/useDeleteCustomModel.ts
0 → 100644
浏览文件 @
ea9301c7
import
{
useMutation
,
useQueryClient
}
from
"@tanstack/react-query"
;
import
{
IpcClient
}
from
"@/ipc/ipc_client"
;
interface
DeleteCustomModelParams
{
providerId
:
string
;
modelApiName
:
string
;
}
export
function
useDeleteCustomModel
({
onSuccess
,
onError
,
}:
{
onSuccess
?:
()
=>
void
;
onError
?:
(
error
:
Error
)
=>
void
;
})
{
const
queryClient
=
useQueryClient
();
const
mutation
=
useMutation
<
void
,
Error
,
DeleteCustomModelParams
>
({
mutationFn
:
async
(
params
:
DeleteCustomModelParams
)
=>
{
if
(
!
params
.
providerId
||
!
params
.
modelApiName
)
{
throw
new
Error
(
"Provider ID and Model API Name are required for deletion."
,
);
}
const
ipcClient
=
IpcClient
.
getInstance
();
// This method will be added to IpcClient next
await
ipcClient
.
deleteCustomModel
(
params
);
},
onSuccess
:
(
data
,
params
:
DeleteCustomModelParams
)
=>
{
// Invalidate queries related to language models for the specific provider
queryClient
.
invalidateQueries
({
queryKey
:
[
"language-models"
,
params
.
providerId
],
});
// Invalidate general model list if needed
queryClient
.
invalidateQueries
({
queryKey
:
[
"languageModels"
]
});
onSuccess
?.();
},
onError
:
(
error
:
Error
)
=>
{
console
.
error
(
"Error deleting custom model:"
,
error
);
onError
?.(
error
);
},
meta
:
{
// Optional: for global error handling like toasts
showErrorToast
:
true
,
},
});
return
mutation
;
}
src/ipc/handlers/language_model_handlers.ts
浏览文件 @
ea9301c7
...
@@ -12,10 +12,11 @@ import {
...
@@ -12,10 +12,11 @@ import {
}
from
"../shared/language_model_helpers"
;
}
from
"../shared/language_model_helpers"
;
import
{
db
}
from
"@/db"
;
import
{
db
}
from
"@/db"
;
import
{
import
{
language_models
,
language_model_providers
as
languageModelProvidersSchema
,
language_model_providers
as
languageModelProvidersSchema
,
language_models
as
languageModelsSchema
,
language_models
as
languageModelsSchema
,
}
from
"@/db/schema"
;
}
from
"@/db/schema"
;
import
{
eq
}
from
"drizzle-orm"
;
import
{
and
,
eq
}
from
"drizzle-orm"
;
import
{
IpcMainInvokeEvent
}
from
"electron"
;
import
{
IpcMainInvokeEvent
}
from
"electron"
;
const
logger
=
log
.
scope
(
"language_model_handlers"
);
const
logger
=
log
.
scope
(
"language_model_handlers"
);
...
@@ -129,6 +130,80 @@ export function registerLanguageModelHandlers() {
...
@@ -129,6 +130,80 @@ export function registerLanguageModelHandlers() {
},
},
);
);
handle
(
"delete-custom-language-model"
,
async
(
event
:
IpcMainInvokeEvent
,
params
:
{
modelId
:
string
},
):
Promise
<
void
>
=>
{
const
{
modelId
:
apiName
}
=
params
;
// Validation
if
(
!
apiName
)
{
throw
new
Error
(
"Model API name (modelId) is required"
);
}
logger
.
info
(
`Handling delete-custom-language-model for apiName:
${
apiName
}
`
,
);
const
existingModel
=
await
db
.
select
()
.
from
(
languageModelsSchema
)
.
where
(
eq
(
languageModelsSchema
.
apiName
,
apiName
))
.
get
();
if
(
!
existingModel
)
{
throw
new
Error
(
`A model with API name (modelId) "
${
apiName
}
" was not found`
,
);
}
await
db
.
delete
(
languageModelsSchema
)
.
where
(
eq
(
languageModelsSchema
.
apiName
,
apiName
));
},
);
handle
(
"delete-custom-model"
,
async
(
_event
:
IpcMainInvokeEvent
,
params
:
{
providerId
:
string
;
modelApiName
:
string
},
):
Promise
<
void
>
=>
{
const
{
providerId
,
modelApiName
}
=
params
;
logger
.
info
(
`Handling delete-custom-model for
${
providerId
}
/
${
modelApiName
}
`
,
);
if
(
!
providerId
||
!
modelApiName
)
{
throw
new
Error
(
"Provider ID and Model API Name are required."
);
}
logger
.
info
(
`Attempting to delete custom model
${
modelApiName
}
for provider
${
providerId
}
`
,
);
const
result
=
db
.
delete
(
language_models
)
.
where
(
and
(
eq
(
language_models
.
provider_id
,
providerId
),
eq
(
language_models
.
apiName
,
modelApiName
),
),
)
.
run
();
if
(
result
.
changes
===
0
)
{
logger
.
warn
(
`No custom model found matching providerId=
${
providerId
}
and apiName=
${
modelApiName
}
for deletion.`
,
);
}
else
{
logger
.
info
(
`Successfully deleted
${
result
.
changes
}
custom model(s) with apiName=
${
modelApiName
}
for provider=
${
providerId
}
`
,
);
}
},
);
handle
(
handle
(
"get-language-models"
,
"get-language-models"
,
async
(
async
(
...
...
src/ipc/ipc_client.ts
浏览文件 @
ea9301c7
...
@@ -58,6 +58,11 @@ export interface DeepLinkData {
...
@@ -58,6 +58,11 @@ export interface DeepLinkData {
url
?:
string
;
url
?:
string
;
}
}
interface
DeleteCustomModelParams
{
providerId
:
string
;
modelApiName
:
string
;
}
export
class
IpcClient
{
export
class
IpcClient
{
private
static
instance
:
IpcClient
;
private
static
instance
:
IpcClient
;
private
ipcRenderer
:
IpcRenderer
;
private
ipcRenderer
:
IpcRenderer
;
...
@@ -761,5 +766,17 @@ export class IpcClient {
...
@@ -761,5 +766,17 @@ export class IpcClient {
await
this
.
ipcRenderer
.
invoke
(
"create-custom-language-model"
,
params
);
await
this
.
ipcRenderer
.
invoke
(
"create-custom-language-model"
,
params
);
}
}
public
async
deleteCustomLanguageModel
(
modelId
:
string
):
Promise
<
void
>
{
return
this
.
ipcRenderer
.
invoke
(
"delete-custom-language-model"
,
modelId
);
}
async
deleteCustomModel
(
params
:
DeleteCustomModelParams
):
Promise
<
void
>
{
return
this
.
ipcRenderer
.
invoke
(
"delete-custom-model"
,
params
);
}
// --- End window control methods ---
// --- End window control methods ---
// --- Language Model Operations ---
// --- App Operations ---
}
}
src/preload.ts
浏览文件 @
ea9301c7
...
@@ -9,6 +9,8 @@ const validInvokeChannels = [
...
@@ -9,6 +9,8 @@ const validInvokeChannels = [
"create-custom-language-model"
,
"create-custom-language-model"
,
"get-language-model-providers"
,
"get-language-model-providers"
,
"create-custom-language-model-provider"
,
"create-custom-language-model-provider"
,
"delete-custom-language-model"
,
"delete-custom-model"
,
"chat:add-dep"
,
"chat:add-dep"
,
"chat:message"
,
"chat:message"
,
"chat:cancel"
,
"chat:cancel"
,
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论