Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion frontend/server/cronjobs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
Stored,
TosCronjobRepository,
)
from .routes import mount_routes
from .routes import mount_routes, mount_storage_unavailable_routes
from .schemas import (
CreateCronjobRequest,
Cronjob,
Expand Down Expand Up @@ -56,4 +56,5 @@
"TosCronjobRepository",
"UpdateCronjobRequest",
"mount_routes",
"mount_storage_unavailable_routes",
]
46 changes: 43 additions & 3 deletions frontend/server/cronjobs/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@
IdentityResolver = Callable[[Request], CronjobIdentity]
RuntimeAuthorizer = Callable[[Request, str, str], Any]

CRONJOB_STORAGE_NOT_CONFIGURED_MESSAGE = (
"定时任务不可用:Studio 未挂载 TOS 持久化存储。"
"请配置 VEADK_STUDIO_TOS_BUCKET 和 VEADK_STUDIO_TOS_REGION,"
"确认 Studio 有权访问该 Bucket,然后重新启动 Studio。"
)
CRONJOB_STORAGE_UNREACHABLE_MESSAGE = (
"无法连接定时任务使用的 TOS 持久化存储。"
"请联系管理员检查 TOS Bucket、Region、Endpoint、访问凭据和网络连通性。"
)


def mount_routes(
app: Any,
Expand Down Expand Up @@ -203,6 +213,29 @@ async def cancel_cronjob_run(
)


def mount_storage_unavailable_routes(app: Any) -> None:
"""Keep the cronjob API contract JSON-shaped when TOS is not mounted."""

async def unavailable() -> None:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=CRONJOB_STORAGE_NOT_CONFIGURED_MESSAGE,
)

app.add_api_route(
"/web/cronjobs",
unavailable,
methods=["GET", "POST", "PATCH", "DELETE"],
include_in_schema=False,
)
app.add_api_route(
"/web/cronjobs/{path:path}",
unavailable,
methods=["GET", "POST", "PATCH", "DELETE"],
include_in_schema=False,
)


async def _invoke(call: Callable[[], Any], *, list_response: bool = False) -> Any:
try:
result = await call()
Expand All @@ -224,9 +257,16 @@ def _raise_api_error(error: Exception) -> None:
if isinstance(error, CronjobRunQueueUnavailable):
raise HTTPException(status_code=503, detail=str(error)) from error
raise HTTPException(
status_code=502,
detail="定时任务服务暂时不可用,请稍后重试。",
status_code=503,
detail=CRONJOB_STORAGE_UNREACHABLE_MESSAGE,
) from error


__all__ = ["IdentityResolver", "RuntimeAuthorizer", "mount_routes"]
__all__ = [
"CRONJOB_STORAGE_NOT_CONFIGURED_MESSAGE",
"CRONJOB_STORAGE_UNREACHABLE_MESSAGE",
"IdentityResolver",
"RuntimeAuthorizer",
"mount_routes",
"mount_storage_unavailable_routes",
]
20 changes: 20 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,9 @@ interface NewChatCapabilitiesState {
harnessEnabled?: boolean;
builtinTools?: string[];
temporaryEnabled?: boolean;
temporaryUnavailableReason?: string;
deepseekHarnessEnabled?: boolean;
deepseekHarnessUnavailableReason?: string;
sandboxEndpointExportEnabled?: boolean;
skillCustomizationEnabled?: boolean;
}
Expand Down Expand Up @@ -315,9 +317,17 @@ async function probeNewChatCapabilities(
builtinTools: harnessResult.status === "fulfilled" ? harnessResult.value : [],
temporaryEnabled:
sandboxResult.status === "fulfilled" && sandboxResult.value.enabled,
temporaryUnavailableReason:
sandboxResult.status === "fulfilled"
? sandboxResult.value.reason
: "无法读取 Codex Sandbox 配置,请检查 Studio 服务。",
deepseekHarnessEnabled:
deepseekHarnessResult.status === "fulfilled" &&
deepseekHarnessResult.value.enabled,
deepseekHarnessUnavailableReason:
deepseekHarnessResult.status === "fulfilled"
? deepseekHarnessResult.value.reason
: "无法读取 DeepSeek Harness Sandbox 配置,请检查 Studio 服务。",
sandboxEndpointExportEnabled:
sandboxResult.status === "fulfilled" &&
sandboxResult.value.endpointExportEnabled === true,
Expand Down Expand Up @@ -6316,10 +6326,20 @@ export default function App() {
onSkillActionChange={setNewChatSkillAction}
onSkillTargetChange={setNewChatSkillTarget}
temporaryEnabled={newChatCapabilitiesReady && newChatCapabilities.temporaryEnabled}
temporaryUnavailableReason={
newChatCapabilitiesReady
? newChatCapabilities.temporaryUnavailableReason
: "正在检查 Codex Sandbox 配置"
}
deepseekHarnessEnabled={
newChatCapabilitiesReady &&
newChatCapabilities.deepseekHarnessEnabled
}
deepseekHarnessUnavailableReason={
newChatCapabilitiesReady
? newChatCapabilities.deepseekHarnessUnavailableReason
: "正在检查 DeepSeek Harness Sandbox 配置"
}
harnessEnabled={newChatCapabilitiesReady && newChatCapabilities.harnessEnabled}
builtinTools={
newChatCapabilitiesReady ? newChatCapabilities.builtinTools : []
Expand Down
43 changes: 34 additions & 9 deletions frontend/src/adk/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3237,10 +3237,35 @@ function cronJobPath(jobId = ""): string {
return `/web/cronjobs${jobId ? `/${encodeURIComponent(jobId)}` : ""}`;
}

async function cronJobErrorMessage(
response: Response,
fallback: string,
): Promise<string> {
const context = `${fallback}(HTTP ${response.status})`;
const text = await response.text().catch(() => "");
if (!text) return context;
try {
const payload = JSON.parse(text) as { detail?: unknown; error?: unknown };
const detail = payload.detail ?? payload.error;
if (typeof detail === "string" && detail.trim()) return detail.trim();
if (
detail &&
typeof detail === "object" &&
"message" in detail &&
typeof (detail as { message?: unknown }).message === "string"
) {
return (detail as { message: string }).message.trim() || context;
}
} catch {
// The Studio fallback route may be served by an older deployment.
}
return context;
}

export async function listCronJobs(signal?: AbortSignal): Promise<CronJob[]> {
const response = await apiFetch(cronJobPath(), { signal });
if (!response.ok) {
throw new Error(await httpErrorMessage(response, "加载定时任务失败"));
throw new Error(await cronJobErrorMessage(response, "加载定时任务失败"));
}
const data = (await response.json()) as CronJobListResponse | CronJob[];
return Array.isArray(data) ? data : data.items ?? [];
Expand All @@ -3252,7 +3277,7 @@ export async function getCronJob(
): Promise<CronJob> {
const response = await apiFetch(cronJobPath(jobId), { signal });
if (!response.ok) {
throw new Error(await httpErrorMessage(response, "加载定时任务详情失败"));
throw new Error(await cronJobErrorMessage(response, "加载定时任务详情失败"));
}
return (await response.json()) as CronJob;
}
Expand All @@ -3264,7 +3289,7 @@ export async function createCronJob(input: CronJobInput): Promise<CronJob> {
body: JSON.stringify(input),
});
if (!response.ok) {
throw new Error(await httpErrorMessage(response, "创建定时任务失败"));
throw new Error(await cronJobErrorMessage(response, "创建定时任务失败"));
}
return (await response.json()) as CronJob;
}
Expand All @@ -3279,7 +3304,7 @@ export async function updateCronJob(
body: JSON.stringify(input),
});
if (!response.ok) {
throw new Error(await httpErrorMessage(response, "更新定时任务失败"));
throw new Error(await cronJobErrorMessage(response, "更新定时任务失败"));
}
return (await response.json()) as CronJob;
}
Expand All @@ -3294,7 +3319,7 @@ export async function setCronJobEnabled(
});
if (!response.ok) {
throw new Error(
await httpErrorMessage(response, enabled ? "启用定时任务失败" : "暂停定时任务失败"),
await cronJobErrorMessage(response, enabled ? "启用定时任务失败" : "暂停定时任务失败"),
);
}
return (await response.json()) as CronJob;
Expand All @@ -3303,7 +3328,7 @@ export async function setCronJobEnabled(
export async function runCronJobNow(jobId: string): Promise<CronJobRun> {
const response = await apiFetch(`${cronJobPath(jobId)}/run`, { method: "POST" });
if (!response.ok) {
throw new Error(await httpErrorMessage(response, "立即执行定时任务失败"));
throw new Error(await cronJobErrorMessage(response, "立即执行定时任务失败"));
}
return (await response.json()) as CronJobRun;
}
Expand All @@ -3314,7 +3339,7 @@ export async function listCronJobRuns(
): Promise<CronJobRun[]> {
const response = await apiFetch(`${cronJobPath(jobId)}/runs`, { signal });
if (!response.ok) {
throw new Error(await httpErrorMessage(response, "加载执行历史失败"));
throw new Error(await cronJobErrorMessage(response, "加载执行历史失败"));
}
const data = (await response.json()) as CronJobRunListResponse | CronJobRun[];
return Array.isArray(data) ? data : data.items ?? [];
Expand All @@ -3329,15 +3354,15 @@ export async function cancelCronJobRun(
{ method: "POST" },
);
if (!response.ok) {
throw new Error(await httpErrorMessage(response, "终止执行失败"));
throw new Error(await cronJobErrorMessage(response, "终止执行失败"));
}
return (await response.json()) as CronJobRun;
}

export async function deleteCronJob(jobId: string): Promise<void> {
const response = await apiFetch(cronJobPath(jobId), { method: "DELETE" });
if (!response.ok) {
throw new Error(await httpErrorMessage(response, "删除定时任务失败"));
throw new Error(await cronJobErrorMessage(response, "删除定时任务失败"));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,12 @@
.website-integration-section-heading h2 { margin: 0; font-size: 14px; font-weight: 650; line-height: 1.4; }
.website-integration-section-heading > small { color: hsl(var(--muted-foreground)); font-size: 11px; }

.website-integration-form { display: grid; grid-template-columns: minmax(200px, 1.2fr) minmax(220px, 1fr) auto; align-items: end; gap: 8px 12px; }
.website-integration-form > label { color: hsl(var(--muted-foreground)); font-size: 12px; font-weight: 600; line-height: 1.4; }
.website-integration-form > label:first-child { grid-column: 1; }
.website-integration-form > label:nth-of-type(2) { grid-column: 2; }
.website-integration-form > [id="website-runtime"] { grid-column: 1; grid-row: 2; }
.website-integration-form > input { grid-column: 2; grid-row: 2; }
.website-integration-form > button { grid-column: 3; grid-row: 2; min-width: 104px; }
.website-integration-form { display: grid; grid-template-columns: minmax(240px, 1.2fr) minmax(240px, 1fr) auto; align-items: end; gap: 12px; }
.website-integration-field { min-width: 0; display: flex; flex-direction: column; gap: 8px; }
.website-integration-field > label { color: hsl(var(--muted-foreground)); font-size: 12px; font-weight: 600; line-height: 1.4; }
.website-integration-field > input { width: 100%; }
.website-integration-form-action { display: flex; align-items: flex-end; }
.website-integration-form-action > button { min-width: 104px; }
.website-integration-error { margin-top: 12px; padding: 9px 11px; border: 1px solid hsl(var(--destructive) / 0.22); border-radius: 8px; background: hsl(var(--destructive) / 0.05); color: hsl(var(--destructive)); font-size: 12px; line-height: 1.5; }

.website-integration-loading { min-height: 96px; display: grid; place-items: center; color: hsl(var(--muted-foreground)); font-size: 12.5px; }
Expand All @@ -61,13 +60,7 @@
@media (max-width: 820px) {
.website-integration-page { padding: 20px 18px 0; }
.website-integration-form { grid-template-columns: minmax(0, 1fr); }
.website-integration-form > label,
.website-integration-form > label:first-child,
.website-integration-form > label:nth-of-type(2),
.website-integration-form > [id="website-runtime"],
.website-integration-form > input,
.website-integration-form > button { grid-column: 1; grid-row: auto; }
.website-integration-form > label:nth-of-type(2) { margin-top: 4px; }
.website-integration-form-action > button { width: 100%; }
.website-integration-row { grid-template-columns: minmax(0, 1fr) auto; }
.website-integration-token, .website-integration-created { display: none; }
.website-integration-embed { grid-template-columns: minmax(0, 1fr); }
Expand Down
73 changes: 40 additions & 33 deletions frontend/src/automations/website-integration/WebsiteIntegration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,39 +192,46 @@ export function WebsiteIntegration({ onBack }: WebsiteIntegrationProps) {
</div>
</div>
<form className="website-integration-form" onSubmit={addIntegration}>
<label htmlFor="website-runtime">AgentKit Runtime</label>
<Select
id="website-runtime"
options={runtimeOptions}
value={selectedRuntime}
onChange={(option) => setSelectedRuntime(option.value)}
placeholder={loading ? "正在加载 Runtime" : "选择 Runtime"}
loading={loading}
disabled={loading || submitting || runtimeOptions.length === 0}
size="lg"
pill={false}
align="start"
/>
<label htmlFor="website-domain">网站域名</label>
<Input
id="website-domain"
value={domain}
onChange={(event) => setDomain(event.target.value)}
placeholder="例如 xxxx.com 或 localhost:5173"
autoComplete="off"
disabled={submitting}
size="lg"
/>
<Button
type="submit"
color="primary"
size="lg"
pill={false}
loading={submitting}
disabled={!domain.trim() || !selectedRuntime || loading}
>
{submitting ? "正在生成" : "生成 Token"}
</Button>
<div className="website-integration-field">
<label htmlFor="website-runtime">AgentKit Runtime</label>
<Select
id="website-runtime"
options={runtimeOptions}
value={selectedRuntime}
onChange={(option) => setSelectedRuntime(option.value)}
placeholder={loading ? "正在加载 Runtime" : "选择 Runtime"}
loading={loading}
disabled={loading || submitting || runtimeOptions.length === 0}
size="lg"
pill={false}
block
align="start"
/>
</div>
<div className="website-integration-field">
<label htmlFor="website-domain">网站域名</label>
<Input
id="website-domain"
value={domain}
onChange={(event) => setDomain(event.target.value)}
placeholder="例如 xxxx.com 或 localhost:5173"
autoComplete="off"
disabled={submitting}
size="lg"
/>
</div>
<div className="website-integration-form-action">
<Button
type="submit"
color="primary"
size="lg"
pill={false}
loading={submitting}
disabled={!domain.trim() || !selectedRuntime || loading}
>
{submitting ? "正在生成" : "生成 Token"}
</Button>
</div>
</form>
{error ? <div className="website-integration-error" role="alert">{error}</div> : null}
</section>
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/ui/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,9 @@ export interface ComposerProps {
onModeChange?: (value: NewChatMode) => void;
onTaskChange?: (value: NewChatTask | null) => void;
temporaryEnabled?: boolean;
temporaryUnavailableReason?: string;
deepseekHarnessEnabled?: boolean;
deepseekHarnessUnavailableReason?: string;
harnessEnabled?: boolean;
builtinTools?: readonly string[];
showAgentPicker?: boolean;
Expand Down Expand Up @@ -213,7 +215,9 @@ export function Composer({
onModeChange,
onTaskChange,
temporaryEnabled,
temporaryUnavailableReason,
deepseekHarnessEnabled,
deepseekHarnessUnavailableReason,
harnessEnabled = false,
builtinTools = [],
showAgentPicker = false,
Expand Down Expand Up @@ -755,7 +759,9 @@ export function Composer({
onChange={onModeChange}
disabled={busy}
temporaryEnabled={temporaryEnabled}
temporaryUnavailableReason={temporaryUnavailableReason}
deepseekHarnessEnabled={deepseekHarnessEnabled}
deepseekHarnessUnavailableReason={deepseekHarnessUnavailableReason}
/>
) : null}

Expand Down
4 changes: 3 additions & 1 deletion frontend/src/ui/new-chat-modes/NewChatAgentPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,9 @@ export function NewChatAgentPicker({
aria-haspopup="menu"
aria-expanded={activeType === type.id}
className={`new-chat-agent-picker__type${keyboardNavigating && keyboardPanel === "types" && activeTypeIndex === index ? " is-keyboard-active" : ""}`}
onMouseEnter={() => activateType(index)}
onMouseEnter={() => {
if (window.innerWidth > 640) activateType(index);
}}
onClick={() => {
activateType(index);
setKeyboardPanel("runtimes");
Expand Down
Loading
Loading