Move Composio, logo.dev and Antigravity keys into Settings
The three third-party keys were hardcoded or read from the environment. Operators now enter them in Settings > Integrations; they are stored encrypted in the app-wide integration_keys table (migration 058). An agent holding connector grants fails loudly when no Composio key is saved. The goal-review test no longer depends on absolute dates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1ea040c0f0
commit
56b0594f91
36 files changed
+430
-100
No files matched your search
@@ -26,6 +26,19 @@ export const zh: Record<string, string> = {
|
||||
"Add workspace": "添加工作区",
|
||||
"Advance": "推进",
|
||||
"Advanced": "高级",
|
||||
"Integrations": "集成密钥",
|
||||
"Keys are stored on this device, encrypted by the operating system, and shared by every company.": "密钥保存在本机,由操作系统加密,所有公司共用。",
|
||||
"Composio API key": "Composio API 密钥",
|
||||
"Powers the Connectors page and agent connector tools. Create one at platform.composio.dev.": "用于连接器页面和 Agent 的连接器工具。在 platform.composio.dev 创建。",
|
||||
"logo.dev publishable key": "logo.dev 公开密钥",
|
||||
"Shows brand logos for connectors. Create one at logo.dev.": "为连接器显示品牌图标。在 logo.dev 创建。",
|
||||
"Google Antigravity OAuth client secret": "Google Antigravity OAuth 客户端密钥",
|
||||
"Needed to sign in to and refresh the Google Antigravity provider.": "登录和续期 Google Antigravity 时需要。",
|
||||
"Saved": "已保存",
|
||||
"Not set": "未设置",
|
||||
"Replace key": "替换密钥",
|
||||
"Key": "密钥",
|
||||
"Remove": "移除",
|
||||
"Agent": "智能体",
|
||||
"Agent could not be reached": "无法连接到智能体",
|
||||
"Agent runs": "智能体运行",
|
||||
|
||||
@@ -2,11 +2,12 @@ import { useMemo, useState } from "react";
|
||||
import { ProvidersPanel } from "./settings/ProvidersPanel";
|
||||
import { WorkspacePanel } from "./settings/WorkspacePanel";
|
||||
import { AdvancedPanel } from "./settings/AdvancedPanel";
|
||||
import { IntegrationKeysPanel } from "./settings/IntegrationKeysPanel";
|
||||
import { t } from "../i18n/i18n";
|
||||
import type { PageProps } from "./types";
|
||||
import { activeOrgId, requireActiveEmployee, requireActiveOrganization } from "./types";
|
||||
|
||||
type SettingsTab = "general" | "providers" | "advanced";
|
||||
type SettingsTab = "general" | "providers" | "integrations" | "advanced";
|
||||
|
||||
interface NavSpec {
|
||||
id: SettingsTab;
|
||||
@@ -16,6 +17,7 @@ interface NavSpec {
|
||||
const TABS: NavSpec[] = [
|
||||
{ id: "general", label: t("General") },
|
||||
{ id: "providers", label: t("Providers") },
|
||||
{ id: "integrations", label: t("Integrations") },
|
||||
{ id: "advanced", label: t("Advanced") },
|
||||
];
|
||||
|
||||
@@ -56,6 +58,7 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
|
||||
actions={actions}
|
||||
/>
|
||||
)}
|
||||
{tab === "integrations" && <IntegrationKeysPanel keys={snapshot.integrationKeys} actions={actions} />}
|
||||
{tab === "advanced" && (
|
||||
<AdvancedPanel
|
||||
orgId={orgId}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useState, type FormEvent } from "react";
|
||||
import type { IntegrationKeyName, IntegrationKeyStatus } from "../../../../shared/commands";
|
||||
import { Button, Field, Pill } from "../../shared/ui";
|
||||
import { t } from "../../i18n/i18n";
|
||||
import type { SnapshotActions } from "../../app/useSnapshot";
|
||||
|
||||
const KEY_COPY: Record<IntegrationKeyName, { label: string; hint: string; placeholder: string }> = {
|
||||
composio: {
|
||||
label: t("Composio API key"),
|
||||
hint: t("Powers the Connectors page and agent connector tools. Create one at platform.composio.dev."),
|
||||
placeholder: "ak_…",
|
||||
},
|
||||
logo_dev: {
|
||||
label: t("logo.dev publishable key"),
|
||||
hint: t("Shows brand logos for connectors. Create one at logo.dev."),
|
||||
placeholder: "pk_…",
|
||||
},
|
||||
google_antigravity_client_secret: {
|
||||
label: t("Google Antigravity OAuth client secret"),
|
||||
hint: t("Needed to sign in to and refresh the Google Antigravity provider."),
|
||||
placeholder: "GOCSPX-…",
|
||||
},
|
||||
};
|
||||
|
||||
export function IntegrationKeysPanel({ keys, actions }: { keys: IntegrationKeyStatus[]; actions: SnapshotActions }) {
|
||||
return (
|
||||
<div className="page">
|
||||
<h1 className="page-h1">{t("Integrations")}</h1>
|
||||
<p style={{ margin: "8px 0 24px", color: "var(--text-2)" }}>
|
||||
{t("Keys are stored on this device, encrypted by the operating system, and shared by every company.")}
|
||||
</p>
|
||||
{keys.map((status) => <KeyRow key={status.name} status={status} actions={actions} />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function KeyRow({ status, actions }: { status: IntegrationKeyStatus; actions: SnapshotActions }) {
|
||||
const copy = KEY_COPY[status.name];
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const perform = async (operation: () => Promise<unknown>) => {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
await operation();
|
||||
} catch (failure) {
|
||||
setError(failure instanceof Error ? failure.message : String(failure));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const save = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const form = event.currentTarget;
|
||||
const value = String(new FormData(form).get("value") ?? "");
|
||||
void perform(async () => {
|
||||
await actions.run(() => window.agentCompany.setIntegrationKey({ name: status.name, value }));
|
||||
form.reset();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={save} style={{ marginBottom: 24 }}>
|
||||
<div className="row" style={{ gap: 8, marginBottom: 6 }}>
|
||||
<span className="section-label">{copy.label}</span>
|
||||
{status.configured ? <Pill tone="green" dot>{t("Saved")}</Pill> : <Pill>{t("Not set")}</Pill>}
|
||||
</div>
|
||||
<p style={{ margin: "0 0 8px", color: "var(--text-2)" }}>{copy.hint}</p>
|
||||
<div className="row" style={{ gap: 8, alignItems: "flex-end" }}>
|
||||
<div style={{ flex: 1, maxWidth: 420 }}>
|
||||
<Field label={status.configured ? t("Replace key") : t("Key")}>
|
||||
<input className="input" name="value" type="password" autoComplete="off" placeholder={copy.placeholder} required />
|
||||
</Field>
|
||||
</div>
|
||||
<Button type="submit" tone="primary" disabled={busy}>{t("Save")}</Button>
|
||||
{status.configured && (
|
||||
<Button
|
||||
disabled={busy}
|
||||
onClick={() => void perform(() => actions.run(() => window.agentCompany.clearIntegrationKey({ name: status.name })))}
|
||||
>
|
||||
{t("Remove")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{error && <div className="form-error" role="alert">{error}</div>}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user