Commit 127ab878 authored by AI-甘富林's avatar AI-甘富林

feat: 启动完成后改为首页绑定员工密钥

parent dd42a21c
......@@ -521,6 +521,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
} = await loadActiveProjectWorkspaceState(projectStore);
const bundleSyncStatus = projectBundleService.getSyncStatus();
const bundleSyncFailed = bundleSyncStatus.state === "error";
const shellReady = !bundleSyncFailed;
const chatSummary = projects.length > 0
? baseChatSummary
: bundleSyncFailed
......@@ -544,6 +545,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
};
return {
shellReady,
apiKeyConfigured: config.apiKeyConfigured,
bindingRequired: !config.apiKeyConfigured,
setupRequired: !config.apiKeyConfigured,
......
......@@ -18,6 +18,7 @@ type SecretName = "apiKey" | "gatewayToken" | "deviceToken" | "authToken";
type KeytarModule = typeof import("keytar");
const KEYTAR_SERVICE = "QianjiangClaw";
const FORCED_SECRET_BACKEND = (process.env.QJCLAW_SECRET_BACKEND ?? "").trim().toLowerCase();
const KEYTAR_ACCOUNT_MAP: Record<SecretName, string> = {
apiKey: "provider-api-key",
gatewayToken: "gateway-token",
......@@ -108,6 +109,12 @@ export class SecretManager {
async load(): Promise<void> {
await this.fallbackStore.load();
if (FORCED_SECRET_BACKEND === "file-fallback") {
this.backend = "file-fallback(forced)";
this.store = this.fallbackStore;
return;
}
const keytar = await this.tryLoadKeytar();
if (!keytar) {
this.backend = "file-fallback";
......
......@@ -369,6 +369,7 @@ function getMockSessions(projectId?: string): WorkspaceSummary["sessions"] {
const mockDesktopApi = {
workspace: {
getSummary: async () => ({
shellReady: true,
apiKeyConfigured: true,
bindingRequired: false,
setupRequired: false,
......@@ -750,8 +751,9 @@ export default function App() {
const effectiveSkills = useMemo(() => (readySkills.length ? [DEFAULT_SKILL, ...readySkills] : [DEFAULT_SKILL]), [readySkills]);
const selectedSkill = useMemo(() => effectiveSkills.find((skill) => skill.id === selectedSkillId) ?? effectiveSkills[0] ?? DEFAULT_SKILL, [effectiveSkills, selectedSkillId]);
const setupMode = workspace?.setupMode ?? config?.setupMode ?? "employee-key";
const setupRequired = workspace?.setupRequired ?? !Boolean(workspace?.apiKeyConfigured ?? config?.apiKeyConfigured);
const chatLaunchState: ChatLaunchState = workspace?.chatLaunchState ?? (!setupRequired ? "starting" : "unbound");
const shellReady = workspace?.shellReady ?? false;
const bindingRequired = workspace?.bindingRequired ?? !Boolean(workspace?.apiKeyConfigured ?? config?.apiKeyConfigured);
const chatLaunchState: ChatLaunchState = workspace?.chatLaunchState ?? (!bindingRequired ? "starting" : "unbound");
const chatStatusMessage = workspace?.chatStatusMessage ?? (chatLaunchState === "starting" ? ui.startingHint : chatLaunchState === "error" ? ui.chatNotReadyError : "");
const startupMessage = workspace?.startupMessage ?? ((refreshing && !workspace) ? ui.startupBooting : chatStatusMessage);
const startupPhase = workspace?.startupPhase ?? ((refreshing && !workspace) ? "syncing-config" : "idle");
......@@ -773,17 +775,17 @@ export default function App() {
isActive: project.id === activeProject?.id
})), [activeProject?.id, expertPageProjects]);
const sessionScopeProjectId = useMemo(() => {
if (setupRequired) {
if (bindingRequired) {
return undefined;
}
return viewMode === "chat" ? HOME_CHAT_PROJECT_ID : activeProject?.id;
}, [activeProject?.id, setupRequired, viewMode]);
}, [activeProject?.id, bindingRequired, viewMode]);
const resolvedActiveSessionId = useMemo(() => resolvePreferredSessionId(sessions, activeSessionId), [activeSessionId, sessions]);
const isBound = !setupRequired;
const isBound = !bindingRequired;
const hasConversationProject = viewMode === "chat"
? visibleProjects.length > 0
: Boolean(workspace?.projectReady && activeProject?.id);
const showStartupOverlay = viewMode !== "settings" && ((refreshing && !workspace) || setupRequired || (isBound && chatLaunchState !== "ready"));
const showStartupOverlay = viewMode !== "settings" && ((refreshing && !workspace) || !shellReady || (isBound && chatLaunchState !== "ready"));
const sending = sendPhase !== "idle";
const canSend = isBound && hasConversationProject && prompt.trim().length > 0 && !sending && !saving;
const sendButtonLabel = sendPhase === "preparing"
......@@ -794,7 +796,6 @@ export default function App() {
? ui.bindFirst
: ui.send;
const isDirectProviderSetup = setupModeDraft === "direct-provider";
const setupActionDisabled = saving || !apiKeyDraft.trim() || (isDirectProviderSetup && (!baseUrlDraft.trim() || !defaultModelDraft.trim()));
const showBindEntry = !isBound && !showStartupOverlay;
const showSettingsStatusHint = viewMode === "settings" && isBound && chatLaunchState !== "ready" && Boolean(startupMessage);
const isConversationView = viewMode === "chat" || viewMode === "experts";
......@@ -896,7 +897,7 @@ export default function App() {
let cancelled = false;
async function syncScopedSessions() {
if (!isBound || setupRequired || !sessionScopeProjectId) {
if (!isBound || bindingRequired || !sessionScopeProjectId) {
if (!cancelled) {
setSessions([]);
setMessages([]);
......@@ -930,7 +931,7 @@ export default function App() {
return () => {
cancelled = true;
};
}, [activeSessionId, desktopApi.chat, isBound, sessionScopeProjectId, setupRequired, workspace]);
}, [activeSessionId, bindingRequired, desktopApi.chat, isBound, sessionScopeProjectId, workspace]);
useEffect(() => {
if (viewMode === "settings" || !showStartupOverlay || !isBound || chatLaunchState !== "starting") {
......@@ -1907,12 +1908,18 @@ export default function App() {
<div className="bind-entry">
<div className="bind-entry-copy">
<strong>{ui.bindTitle}</strong>
<p>{ui.bindDesc}</p>
<p>{setupMode === "employee-key" ? "客户端已准备就绪,输入员工密钥后即可同步聊天配置。" : ui.bindDesc}</p>
</div>
{setupMode === "employee-key" ? (
<div className="bind-row">
<input type="password" value={apiKeyDraft} placeholder={ui.apiKeyPlaceholder} onChange={(event) => setApiKeyDraft(event.target.value)} />
<button disabled={saving || apiKeyDraft.trim().length === 0} onClick={() => void saveConfig(apiKeyDraft)}>{saving ? ui.binding : ui.bindNow}</button>
</div>
) : (
<div className="button-row">
<button type="button" className="secondary" onClick={() => setViewMode("settings")}>{ui.openSettings}</button>
</div>
)}
</div>
);
const activeEmptyState = viewMode === "experts" ? (
......@@ -2212,46 +2219,6 @@ export default function App() {
<p className="startup-overlay-subtitle">{startupCurtainCopy.brandSubtitle}</p>
<p className="startup-overlay-tagline">{startupCurtainCopy.brandTagline}</p>
</div>
{setupRequired ? (
<div className="startup-setup-shell">
<div className="startup-setup-tabs">
<button type="button" className={"startup-setup-tab" + (setupModeDraft === "employee-key" ? " active" : "")} onClick={() => setSetupModeDraft("employee-key")}>Employee Key</button>
<button type="button" className={"startup-setup-tab" + (setupModeDraft === "direct-provider" ? " active" : "")} onClick={() => setSetupModeDraft("direct-provider")}>Direct Provider</button>
</div>
<div className="startup-setup-form">
{setupModeDraft === "direct-provider" ? (
<>
<label>
<span className="field-label">Provider</span>
<select value={providerDraft} onChange={(event) => setProviderDraft(event.target.value)}>
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
<option value="openai-compatible">OpenAI Compatible</option>
</select>
</label>
<label>
<span className="field-label">Base URL</span>
<input value={baseUrlDraft} placeholder="https://api.openai.com/v1" onChange={(event) => setBaseUrlDraft(event.target.value)} />
</label>
<label>
<span className="field-label">Default Model</span>
<input value={defaultModelDraft} placeholder="gpt-5.4-mini" onChange={(event) => setDefaultModelDraft(event.target.value)} />
</label>
</>
) : (
<p className="startup-setup-note">Enter an employee key and Claw will sync runtime configuration automatically.</p>
)}
<label>
<span className="field-label">{setupModeDraft === "direct-provider" ? "API Key" : ui.apiKey}</span>
<input type="password" value={apiKeyDraft} placeholder={setupModeDraft === "direct-provider" ? "Enter API Key" : ui.apiKeyPlaceholder} onChange={(event) => setApiKeyDraft(event.target.value)} />
</label>
</div>
<div className="button-row startup-overlay-actions">
<button type="button" disabled={setupActionDisabled} onClick={() => void saveConfig(apiKeyDraft, setupModeDraft)}>{saving ? ui.preparing : "Bind Now"}</button>
<button type="button" className="secondary" onClick={() => setViewMode("settings")}>{ui.openSettings}</button>
</div>
</div>
) : (
<>
<div className="startup-overlay-progress" aria-hidden="true">
<span style={{ width: String(Math.round(startupProgress * 100)) + "%" }} />
......@@ -2267,7 +2234,6 @@ export default function App() {
</div>
) : null}
</>
)}
</div>
</div>
) : null}
......
......@@ -259,6 +259,7 @@ export interface PluginSummary {
}
export interface WorkspaceSummary {
shellReady: boolean;
apiKeyConfigured: boolean;
bindingRequired: boolean;
setupRequired: boolean;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment