feat: add configurable client tools and photo input

This commit is contained in:
Xin Wang
2026-07-30 19:06:03 +08:00
parent 510a277b5a
commit 913435785e
24 changed files with 1802 additions and 139 deletions

View File

@@ -20,6 +20,7 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import {
Select,
SelectContent,
@@ -31,6 +32,8 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
import {
mcpServersApi,
toolsApi,
type ToolExecutionMode,
type McpServer,
type McpServerUpsert,
type McpToolDefinition,
@@ -364,6 +367,14 @@ export function McpServerDialog({
tool={tool}
expanded={expandedToolIds.has(tool.id)}
onToggle={() => toggleTool(tool.id)}
onUpdated={(updated) => {
setServerTools((current) =>
current.map((item) =>
item.id === updated.id ? updated : item,
),
);
void onChanged();
}}
/>
))}
</div>
@@ -390,12 +401,22 @@ function McpToolRow({
tool,
expanded,
onToggle,
onUpdated,
}: {
tool: Tool;
expanded: boolean;
onToggle: () => void;
onUpdated: (tool: Tool) => void;
}) {
const definition = tool.definition as McpToolDefinition;
const [allowInterruptions, setAllowInterruptions] = useState(
definition.config.allowInterruptions ?? true,
);
const [executionMode, setExecutionMode] = useState<ToolExecutionMode>(
definition.config.executionMode ?? "immediate",
);
const [savingPolicy, setSavingPolicy] = useState(false);
const [policyError, setPolicyError] = useState<string | null>(null);
const schema = definition.config.inputSchema ?? {};
const properties = useMemo(
() => Object.entries((schema.properties as Record<string, unknown> | undefined) ?? {}),
@@ -403,6 +424,37 @@ function McpToolRow({
);
const required = new Set(Array.isArray(schema.required) ? schema.required.map(String) : []);
async function savePolicy() {
if (savingPolicy) return;
setSavingPolicy(true);
setPolicyError(null);
try {
const updated = await toolsApi.update(tool.id, {
name: tool.name,
functionName: tool.functionName,
description: tool.description,
definition: {
...definition,
config: {
...definition.config,
allowInterruptions,
executionMode,
},
},
secrets: tool.secrets,
status: tool.status,
mcpServerId: tool.mcpServerId,
});
onUpdated(updated);
} catch (error) {
setPolicyError(
error instanceof Error ? error.message : "保存工具运行策略失败",
);
} finally {
setSavingPolicy(false);
}
}
return (
<div>
<button
@@ -425,6 +477,47 @@ function McpToolRow({
{tool.description && (
<p className="mb-4 text-sm leading-6 text-muted-foreground">{tool.description}</p>
)}
<div className="mb-4 grid gap-4 rounded-lg border border-hairline bg-card p-4 sm:grid-cols-2">
<Field label="执行模式">
<Select
value={executionMode}
onValueChange={(value: ToolExecutionMode) =>
setExecutionMode(value)
}
>
<SelectTrigger className="w-full"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="immediate"> · </SelectItem>
<SelectItem value="async"> · </SelectItem>
</SelectContent>
</Select>
</Field>
<div className="flex items-center justify-between gap-4 rounded-lg border border-hairline px-3 py-2">
<div>
<div className="text-sm font-medium text-foreground"></div>
<div className="mt-0.5 text-xs text-muted-foreground"></div>
</div>
<Switch
checked={allowInterruptions}
onCheckedChange={setAllowInterruptions}
/>
</div>
<div className="flex items-center gap-3 sm:col-span-2">
<Button
type="button"
size="sm"
variant="outline"
disabled={savingPolicy}
onClick={() => void savePolicy()}
>
{savingPolicy && <Loader2 size={14} className="animate-spin" />}
</Button>
{policyError && (
<span className="text-xs text-destructive">{policyError}</span>
)}
</div>
</div>
{properties.length === 0 ? (
<div className="text-sm text-muted-foreground"></div>
) : (