feat(saas): industry agent template assignment system
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

Phase 1-8 of industry-agent-delivery plan:

- DB migration: accounts.assigned_template_id (ON DELETE SET NULL)
- SaaS API: 4 new endpoints (assign/get/unassign/create-agent)
- Service layer: assign_template_to_account, get_assigned_template, unassign_template, create_agent_from_template)
- Types: AssignTemplateRequest, AgentConfigFromTemplate (capabilities merged into tools)
- Frontend SaaS Client: assignTemplate, getAssignedTemplate, unassignTemplate, createAgentFromTemplate
- saasStore: assignedTemplate state + login auto-fetch + actions
- saas-relay-client: fix unused import and saasUrl reference error
- connectionStore: fix relayModel undefined error
- capabilities default to glm-4-flash

- Route registration: new template assignment routes

Cospec and handlers consolidated

Build: cargo check --workspace PASS, tsc --noEmit Pass
This commit is contained in:
iven
2026-04-03 13:31:58 +08:00
parent 5b1b747810
commit ea00c32c08
10 changed files with 618 additions and 16 deletions

View File

@@ -472,6 +472,19 @@ export const useConnectionStore = create<ConnectionStore>((set, get) => {
// agent management local via KernelClient.
// baseUrl = saasUrl + /api/v1/relay → kernel appends /chat/completions
// apiKey = SaaS JWT token → sent as Authorization: Bearer <jwt>
// Fetch available models from SaaS relay (shared by both branches)
let relayModels: Array<{ id: string }>;
try {
relayModels = await saasClient.listModels();
} catch {
throw new Error('无法获取可用模型列表,请确认管理后台已配置 Provider 和模型');
}
if (relayModels.length === 0) {
throw new Error('SaaS 平台没有可用模型,请先在管理后台配置 Provider 和模型');
}
if (isTauriRuntime()) {
if (!session.token) {
throw new Error('SaaS 中转模式需要认证令牌,请重新登录 SaaS 平台');
@@ -479,20 +492,8 @@ export const useConnectionStore = create<ConnectionStore>((set, get) => {
const kernelClient = getKernelClient();
// Fetch available models from SaaS relay
let models: Array<{ id: string }>;
try {
models = await saasClient.listModels();
} catch {
throw new Error('无法获取可用模型列表,请确认管理后台已配置 Provider 和模型');
}
if (models.length === 0) {
throw new Error('SaaS 平台没有可用模型,请先在管理后台配置 Provider 和模型');
}
// Use first available model (TODO: let user choose preferred model)
const relayModel = models[0];
const relayModel = relayModels[0];
kernelClient.setConfig({
provider: 'custom',
@@ -525,9 +526,21 @@ export const useConnectionStore = create<ConnectionStore>((set, get) => {
baseUrl: `${session.saasUrl}/api/v1/relay`,
});
} else {
// Non-Tauri (browser) — simple connected state without kernel
set({ connectionState: 'connected', gatewayVersion: 'saas-relay' });
log.debug('Connected to SaaS relay (browser mode)');
// Non-Tauri (browser) — use SaaS relay gateway client for agent listing + chat
const { createSaaSRelayGatewayClient } = await import('../lib/saas-relay-client');
const relayModelId = relayModels[0].id;
const relayClient = createSaaSRelayGatewayClient(session.saasUrl, relayModelId);
set({
connectionState: 'connected',
gatewayVersion: 'saas-relay',
client: relayClient as unknown as GatewayClient,
});
const { initializeStores } = await import('./index');
initializeStores();
log.debug('Connected to SaaS relay (browser mode)', { relayModel: relayModelId });
}
return;
}

View File

@@ -27,6 +27,7 @@ import {
type TotpSetupResponse,
type SyncConfigRequest,
type AgentTemplateAvailable,
type AgentTemplateFull,
} from '../lib/saas-client';
import { createLogger } from '../lib/logger';
import {
@@ -73,6 +74,8 @@ export interface SaaSStateSlice {
saasReachable: boolean;
/** Agent templates available for onboarding */
availableTemplates: AgentTemplateAvailable[];
/** Currently assigned template (null if not yet assigned or assignment removed) */
assignedTemplate: AgentTemplateFull | null;
/** Consecutive heartbeat/health-check failures */
_consecutiveFailures: number;
_heartbeatTimer?: ReturnType<typeof setInterval>;
@@ -90,6 +93,12 @@ export interface SaaSActionsSlice {
pushConfigToSaaS: () => Promise<void>;
registerCurrentDevice: () => Promise<void>;
fetchAvailableTemplates: () => Promise<void>;
/** Assign a template to the current account */
assignTemplate: (templateId: string) => Promise<void>;
/** Fetch the currently assigned template (auto-called after login) */
fetchAssignedTemplate: () => Promise<void>;
/** Unassign the current template */
unassignTemplate: () => Promise<void>;
clearError: () => void;
restoreSession: () => void;
setupTotp: () => Promise<TotpSetupResponse>;
@@ -145,6 +154,7 @@ export const useSaaSStore = create<SaaSStore>((set, get) => {
totpSetupData: null,
saasReachable: true,
availableTemplates: [],
assignedTemplate: null,
_consecutiveFailures: 0,
// === Actions ===
@@ -199,6 +209,11 @@ export const useSaaSStore = create<SaaSStore>((set, get) => {
log.warn('Failed to fetch templates after login:', err);
});
// Fetch assigned template in background (non-blocking)
get().fetchAssignedTemplate().catch((err: unknown) => {
log.warn('Failed to fetch assigned template after login:', err);
});
// Fetch available models in background (non-blocking)
get().fetchAvailableModels().catch((err: unknown) => {
log.warn('Failed to fetch models after login:', err);
@@ -628,6 +643,35 @@ export const useSaaSStore = create<SaaSStore>((set, get) => {
}
},
assignTemplate: async (templateId: string) => {
try {
const template = await saasClient.assignTemplate(templateId);
set({ assignedTemplate: template });
} catch (err) {
log.warn('Failed to assign template:', err);
// Don't throw — let wizard continue with fallback flow
}
},
fetchAssignedTemplate: async () => {
try {
const template = await saasClient.getAssignedTemplate();
set({ assignedTemplate: template });
} catch {
// Not critical — null is fine
set({ assignedTemplate: null });
}
},
unassignTemplate: async () => {
try {
await saasClient.unassignTemplate();
set({ assignedTemplate: null });
} catch (err) {
log.warn('Failed to unassign template:', err);
}
},
clearError: () => {
set({ error: null });
},