Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Q
qjclaw-dmg
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
AI-甘富林
qjclaw-dmg
Commits
127ab878
Commit
127ab878
authored
Apr 03, 2026
by
AI-甘富林
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 启动完成后改为首页绑定员工密钥
parent
dd42a21c
Changes
4
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
43 additions
and
67 deletions
+43
-67
ipc.ts
apps/desktop/src/main/ipc.ts
+2
-0
secrets.ts
apps/desktop/src/main/services/secrets.ts
+7
-0
App.tsx
apps/ui/src/App.tsx
+33
-67
index.ts
packages/shared-types/src/index.ts
+1
-0
No files found.
apps/desktop/src/main/ipc.ts
View file @
127ab878
...
@@ -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
,
...
...
apps/desktop/src/main/services/secrets.ts
View file @
127ab878
...
@@ -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"
;
...
...
apps/ui/src/App.tsx
View file @
127ab878
...
@@ -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 (
setup
Required) {
if (
binding
Required) {
return undefined;
return undefined;
}
}
return viewMode === "chat" ? HOME_CHAT_PROJECT_ID : activeProject?.id;
return viewMode === "chat" ? HOME_CHAT_PROJECT_ID : activeProject?.id;
}, [activeProject?.id,
setup
Required, viewMode]);
}, [activeProject?.id,
binding
Required, viewMode]);
const resolvedActiveSessionId = useMemo(() => resolvePreferredSessionId(sessions, activeSessionId), [activeSessionId, sessions]);
const resolvedActiveSessionId = useMemo(() => resolvePreferredSessionId(sessions, activeSessionId), [activeSessionId, sessions]);
const isBound = !
setup
Required;
const isBound = !
binding
Required;
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 ||
setup
Required || !sessionScopeProjectId) {
if (!isBound ||
binding
Required || !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, setupRequire
d, workspace]);
}, [activeSessionId,
bindingRequired, desktopApi.chat, isBound, sessionScopeProjectI
d, 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}
...
...
packages/shared-types/src/index.ts
View file @
127ab878
...
@@ -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
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment