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

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

parent dd42a21c
...@@ -521,6 +521,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -521,6 +521,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
} = await loadActiveProjectWorkspaceState(projectStore); } = await loadActiveProjectWorkspaceState(projectStore);
const bundleSyncStatus = projectBundleService.getSyncStatus(); const bundleSyncStatus = projectBundleService.getSyncStatus();
const bundleSyncFailed = bundleSyncStatus.state === "error"; const bundleSyncFailed = bundleSyncStatus.state === "error";
const shellReady = !bundleSyncFailed;
const chatSummary = projects.length > 0 const chatSummary = projects.length > 0
? baseChatSummary ? baseChatSummary
: bundleSyncFailed : bundleSyncFailed
...@@ -544,6 +545,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -544,6 +545,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
}; };
return { return {
shellReady,
apiKeyConfigured: config.apiKeyConfigured, apiKeyConfigured: config.apiKeyConfigured,
bindingRequired: !config.apiKeyConfigured, bindingRequired: !config.apiKeyConfigured,
setupRequired: !config.apiKeyConfigured, setupRequired: !config.apiKeyConfigured,
......
...@@ -18,6 +18,7 @@ type SecretName = "apiKey" | "gatewayToken" | "deviceToken" | "authToken"; ...@@ -18,6 +18,7 @@ type SecretName = "apiKey" | "gatewayToken" | "deviceToken" | "authToken";
type KeytarModule = typeof import("keytar"); type KeytarModule = typeof import("keytar");
const KEYTAR_SERVICE = "QianjiangClaw"; const KEYTAR_SERVICE = "QianjiangClaw";
const FORCED_SECRET_BACKEND = (process.env.QJCLAW_SECRET_BACKEND ?? "").trim().toLowerCase();
const KEYTAR_ACCOUNT_MAP: Record<SecretName, string> = { const KEYTAR_ACCOUNT_MAP: Record<SecretName, string> = {
apiKey: "provider-api-key", apiKey: "provider-api-key",
gatewayToken: "gateway-token", gatewayToken: "gateway-token",
...@@ -108,6 +109,12 @@ export class SecretManager { ...@@ -108,6 +109,12 @@ export class SecretManager {
async load(): Promise<void> { async load(): Promise<void> {
await this.fallbackStore.load(); 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(); const keytar = await this.tryLoadKeytar();
if (!keytar) { if (!keytar) {
this.backend = "file-fallback"; this.backend = "file-fallback";
......
...@@ -369,6 +369,7 @@ function getMockSessions(projectId?: string): WorkspaceSummary["sessions"] { ...@@ -369,6 +369,7 @@ function getMockSessions(projectId?: string): WorkspaceSummary["sessions"] {
const mockDesktopApi = { const mockDesktopApi = {
workspace: { workspace: {
getSummary: async () => ({ getSummary: async () => ({
shellReady: true,
apiKeyConfigured: true, apiKeyConfigured: true,
bindingRequired: false, bindingRequired: false,
setupRequired: false, setupRequired: false,
...@@ -750,8 +751,9 @@ export default function App() { ...@@ -750,8 +751,9 @@ export default function App() {
const effectiveSkills = useMemo(() => (readySkills.length ? [DEFAULT_SKILL, ...readySkills] : [DEFAULT_SKILL]), [readySkills]); 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 selectedSkill = useMemo(() => effectiveSkills.find((skill) => skill.id === selectedSkillId) ?? effectiveSkills[0] ?? DEFAULT_SKILL, [effectiveSkills, selectedSkillId]);
const setupMode = workspace?.setupMode ?? config?.setupMode ?? "employee-key"; const setupMode = workspace?.setupMode ?? config?.setupMode ?? "employee-key";
const setupRequired = workspace?.setupRequired ?? !Boolean(workspace?.apiKeyConfigured ?? config?.apiKeyConfigured); const shellReady = workspace?.shellReady ?? false;
const chatLaunchState: ChatLaunchState = workspace?.chatLaunchState ?? (!setupRequired ? "starting" : "unbound"); 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 chatStatusMessage = workspace?.chatStatusMessage ?? (chatLaunchState === "starting" ? ui.startingHint : chatLaunchState === "error" ? ui.chatNotReadyError : "");
const startupMessage = workspace?.startupMessage ?? ((refreshing && !workspace) ? ui.startupBooting : chatStatusMessage); const startupMessage = workspace?.startupMessage ?? ((refreshing && !workspace) ? ui.startupBooting : chatStatusMessage);
const startupPhase = workspace?.startupPhase ?? ((refreshing && !workspace) ? "syncing-config" : "idle"); const startupPhase = workspace?.startupPhase ?? ((refreshing && !workspace) ? "syncing-config" : "idle");
...@@ -773,17 +775,17 @@ export default function App() { ...@@ -773,17 +775,17 @@ export default function App() {
isActive: project.id === activeProject?.id isActive: project.id === activeProject?.id
})), [activeProject?.id, expertPageProjects]); })), [activeProject?.id, expertPageProjects]);
const sessionScopeProjectId = useMemo(() => { const sessionScopeProjectId = useMemo(() => {
if (setupRequired) { if (bindingRequired) {
return undefined; return undefined;
} }
return viewMode === "chat" ? HOME_CHAT_PROJECT_ID : activeProject?.id; 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 resolvedActiveSessionId = useMemo(() => resolvePreferredSessionId(sessions, activeSessionId), [activeSessionId, sessions]);
const isBound = !setupRequired; const isBound = !bindingRequired;
const hasConversationProject = viewMode === "chat" const hasConversationProject = viewMode === "chat"
? visibleProjects.length > 0 ? visibleProjects.length > 0
: Boolean(workspace?.projectReady && activeProject?.id); : 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 sending = sendPhase !== "idle";
const canSend = isBound && hasConversationProject && prompt.trim().length > 0 && !sending && !saving; const canSend = isBound && hasConversationProject && prompt.trim().length > 0 && !sending && !saving;
const sendButtonLabel = sendPhase === "preparing" const sendButtonLabel = sendPhase === "preparing"
...@@ -794,7 +796,6 @@ export default function App() { ...@@ -794,7 +796,6 @@ export default function App() {
? ui.bindFirst ? ui.bindFirst
: ui.send; : ui.send;
const isDirectProviderSetup = setupModeDraft === "direct-provider"; const isDirectProviderSetup = setupModeDraft === "direct-provider";
const setupActionDisabled = saving || !apiKeyDraft.trim() || (isDirectProviderSetup && (!baseUrlDraft.trim() || !defaultModelDraft.trim()));
const showBindEntry = !isBound && !showStartupOverlay; const showBindEntry = !isBound && !showStartupOverlay;
const showSettingsStatusHint = viewMode === "settings" && isBound && chatLaunchState !== "ready" && Boolean(startupMessage); const showSettingsStatusHint = viewMode === "settings" && isBound && chatLaunchState !== "ready" && Boolean(startupMessage);
const isConversationView = viewMode === "chat" || viewMode === "experts"; const isConversationView = viewMode === "chat" || viewMode === "experts";
...@@ -896,7 +897,7 @@ export default function App() { ...@@ -896,7 +897,7 @@ export default function App() {
let cancelled = false; let cancelled = false;
async function syncScopedSessions() { async function syncScopedSessions() {
if (!isBound || setupRequired || !sessionScopeProjectId) { if (!isBound || bindingRequired || !sessionScopeProjectId) {
if (!cancelled) { if (!cancelled) {
setSessions([]); setSessions([]);
setMessages([]); setMessages([]);
...@@ -930,7 +931,7 @@ export default function App() { ...@@ -930,7 +931,7 @@ export default function App() {
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [activeSessionId, desktopApi.chat, isBound, sessionScopeProjectId, setupRequired, workspace]); }, [activeSessionId, bindingRequired, desktopApi.chat, isBound, sessionScopeProjectId, workspace]);
useEffect(() => { useEffect(() => {
if (viewMode === "settings" || !showStartupOverlay || !isBound || chatLaunchState !== "starting") { if (viewMode === "settings" || !showStartupOverlay || !isBound || chatLaunchState !== "starting") {
...@@ -1907,12 +1908,18 @@ export default function App() { ...@@ -1907,12 +1908,18 @@ export default function App() {
<div className="bind-entry"> <div className="bind-entry">
<div className="bind-entry-copy"> <div className="bind-entry-copy">
<strong>{ui.bindTitle}</strong> <strong>{ui.bindTitle}</strong>
<p>{ui.bindDesc}</p> <p>{setupMode === "employee-key" ? "客户端已准备就绪,输入员工密钥后即可同步聊天配置。" : ui.bindDesc}</p>
</div> </div>
{setupMode === "employee-key" ? (
<div className="bind-row"> <div className="bind-row">
<input type="password" value={apiKeyDraft} placeholder={ui.apiKeyPlaceholder} onChange={(event) => setApiKeyDraft(event.target.value)} /> <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> <button disabled={saving || apiKeyDraft.trim().length === 0} onClick={() => void saveConfig(apiKeyDraft)}>{saving ? ui.binding : ui.bindNow}</button>
</div> </div>
) : (
<div className="button-row">
<button type="button" className="secondary" onClick={() => setViewMode("settings")}>{ui.openSettings}</button>
</div>
)}
</div> </div>
); );
const activeEmptyState = viewMode === "experts" ? ( const activeEmptyState = viewMode === "experts" ? (
...@@ -2212,46 +2219,6 @@ export default function App() { ...@@ -2212,46 +2219,6 @@ export default function App() {
<p className="startup-overlay-subtitle">{startupCurtainCopy.brandSubtitle}</p> <p className="startup-overlay-subtitle">{startupCurtainCopy.brandSubtitle}</p>
<p className="startup-overlay-tagline">{startupCurtainCopy.brandTagline}</p> <p className="startup-overlay-tagline">{startupCurtainCopy.brandTagline}</p>
</div> </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"> <div className="startup-overlay-progress" aria-hidden="true">
<span style={{ width: String(Math.round(startupProgress * 100)) + "%" }} /> <span style={{ width: String(Math.round(startupProgress * 100)) + "%" }} />
...@@ -2267,7 +2234,6 @@ export default function App() { ...@@ -2267,7 +2234,6 @@ export default function App() {
</div> </div>
) : null} ) : null}
</> </>
)}
</div> </div>
</div> </div>
) : null} ) : null}
......
...@@ -259,6 +259,7 @@ export interface PluginSummary { ...@@ -259,6 +259,7 @@ export interface PluginSummary {
} }
export interface WorkspaceSummary { export interface WorkspaceSummary {
shellReady: boolean;
apiKeyConfigured: boolean; apiKeyConfigured: boolean;
bindingRequired: boolean; bindingRequired: boolean;
setupRequired: 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