Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
B
bit-pm
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
燕伟桐
bit-pm
Commits
7ad83a2b
提交
7ad83a2b
authored
4月 15, 2025
作者:
Will Chen
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Basic GitHub integration flow
上级
3ceb3e23
隐藏空白字符变更
内嵌
并排
正在显示
9 个修改的文件
包含
633 行增加
和
26 行删除
+633
-26
GitHubConnector.tsx
src/components/GitHubConnector.tsx
+197
-0
useSettings.ts
src/hooks/useSettings.ts
+25
-23
github_handlers.ts
src/ipc/handlers/github_handlers.ts
+282
-0
ipc_client.ts
src/ipc/ipc_client.ts
+68
-0
ipc_host.ts
src/ipc/ipc_host.ts
+2
-0
schemas.ts
src/lib/schemas.ts
+11
-0
settings.ts
src/main/settings.ts
+31
-1
app-details.tsx
src/pages/app-details.tsx
+5
-2
preload.ts
src/preload.ts
+12
-0
没有找到文件。
src/components/GitHubConnector.tsx
0 → 100644
浏览文件 @
7ad83a2b
import
{
useState
,
useEffect
}
from
"react"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Github
}
from
"lucide-react"
;
import
{
IpcClient
}
from
"@/ipc/ipc_client"
;
import
{
useSettings
}
from
"@/hooks/useSettings"
;
interface
GitHubConnectorProps
{
appId
:
number
|
null
;
}
export
function
GitHubConnector
({
appId
}:
GitHubConnectorProps
)
{
// --- GitHub Device Flow State ---
const
{
settings
,
refreshSettings
}
=
useSettings
();
const
[
githubUserCode
,
setGithubUserCode
]
=
useState
<
string
|
null
>
(
null
);
const
[
githubVerificationUri
,
setGithubVerificationUri
]
=
useState
<
string
|
null
>
(
null
);
const
[
githubError
,
setGithubError
]
=
useState
<
string
|
null
>
(
null
);
const
[
isConnectingToGithub
,
setIsConnectingToGithub
]
=
useState
(
false
);
const
[
githubStatusMessage
,
setGithubStatusMessage
]
=
useState
<
string
|
null
>
(
null
);
// --- ---
const
handleConnectToGithub
=
async
()
=>
{
if
(
!
appId
)
return
;
setIsConnectingToGithub
(
true
);
setGithubError
(
null
);
setGithubUserCode
(
null
);
setGithubVerificationUri
(
null
);
setGithubStatusMessage
(
"Requesting device code from GitHub..."
);
// Send IPC message to main process to start the flow
IpcClient
.
getInstance
().
startGithubDeviceFlow
(
appId
);
};
useEffect
(()
=>
{
if
(
!
appId
)
return
;
// Don't set up listeners if appId is null initially
const
cleanupFunctions
:
(()
=>
void
)[]
=
[];
// Listener for updates (user code, verification uri, status messages)
const
removeUpdateListener
=
IpcClient
.
getInstance
().
onGithubDeviceFlowUpdate
((
data
)
=>
{
console
.
log
(
"Received github:flow-update"
,
data
);
if
(
data
.
userCode
)
{
setGithubUserCode
(
data
.
userCode
);
}
if
(
data
.
verificationUri
)
{
setGithubVerificationUri
(
data
.
verificationUri
);
}
if
(
data
.
message
)
{
setGithubStatusMessage
(
data
.
message
);
}
setGithubError
(
null
);
// Clear previous errors on new update
if
(
!
data
.
userCode
&&
!
data
.
verificationUri
&&
data
.
message
)
{
// Likely just a status message, keep connecting state
setIsConnectingToGithub
(
true
);
}
if
(
data
.
userCode
&&
data
.
verificationUri
)
{
setIsConnectingToGithub
(
true
);
// Still connecting until success/error
}
});
cleanupFunctions
.
push
(
removeUpdateListener
);
// Listener for success
const
removeSuccessListener
=
IpcClient
.
getInstance
().
onGithubDeviceFlowSuccess
((
data
)
=>
{
console
.
log
(
"Received github:flow-success"
,
data
);
setGithubStatusMessage
(
"Successfully connected to GitHub!"
);
setGithubUserCode
(
null
);
// Clear user-facing info
setGithubVerificationUri
(
null
);
setGithubError
(
null
);
setIsConnectingToGithub
(
false
);
refreshSettings
();
// TODO: Maybe update parent UI to show "Connected" state or trigger next action
});
cleanupFunctions
.
push
(
removeSuccessListener
);
// Listener for errors
const
removeErrorListener
=
IpcClient
.
getInstance
().
onGithubDeviceFlowError
(
(
data
)
=>
{
console
.
log
(
"Received github:flow-error"
,
data
);
setGithubError
(
data
.
error
||
"An unknown error occurred."
);
setGithubStatusMessage
(
null
);
setGithubUserCode
(
null
);
setGithubVerificationUri
(
null
);
setIsConnectingToGithub
(
false
);
}
);
cleanupFunctions
.
push
(
removeErrorListener
);
// Cleanup function to remove all listeners when component unmounts or appId changes
return
()
=>
{
cleanupFunctions
.
forEach
((
cleanup
)
=>
cleanup
());
// Optional: Send a message to main process to cancel polling if component unmounts
// Only cancel if we were actually connecting for this specific appId
// IpcClient.getInstance().cancelGithubDeviceFlow(appId);
// Reset state when appId changes or component unmounts
setGithubUserCode
(
null
);
setGithubVerificationUri
(
null
);
setGithubError
(
null
);
setIsConnectingToGithub
(
false
);
setGithubStatusMessage
(
null
);
};
},
[
appId
]);
// Re-run effect if appId changes
if
(
settings
?.
githubSettings
.
secrets
)
{
return
(
<
div
className=
"mt-4 w-full"
>
<
p
>
Connected to GitHub!
</
p
>
</
div
>
);
}
return
(
<
div
className=
"mt-4 w-full"
>
{
" "
}
<
Button
onClick=
{
handleConnectToGithub
}
className=
"cursor-pointer w-full py-6 flex justify-center items-center gap-2 text-lg"
size=
"lg"
variant=
"outline"
disabled=
{
isConnectingToGithub
||
!
appId
}
// Also disable if appId is null
>
Connect to GitHub
<
Github
className=
"h-5 w-5"
/>
{
isConnectingToGithub
&&
(
<
svg
className=
"animate-spin h-5 w-5 ml-2"
xmlns=
"http://www.w3.org/2000/svg"
fill=
"none"
viewBox=
"0 0 24 24"
>
<
circle
className=
"opacity-25"
cx=
"12"
cy=
"12"
r=
"10"
stroke=
"currentColor"
strokeWidth=
"4"
></
circle
>
<
path
className=
"opacity-75"
fill=
"currentColor"
d=
"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></
path
>
</
svg
>
)
}
</
Button
>
{
/* GitHub Connection Status/Instructions */
}
{
(
githubUserCode
||
githubStatusMessage
||
githubError
)
&&
(
<
div
className=
"mt-6 p-4 border rounded-md bg-gray-50 dark:bg-gray-700/50 border-gray-200 dark:border-gray-600"
>
<
h4
className=
"font-medium mb-2"
>
GitHub Connection
</
h4
>
{
githubError
&&
(
<
p
className=
"text-red-600 dark:text-red-400 mb-2"
>
Error:
{
githubError
}
</
p
>
)
}
{
githubUserCode
&&
githubVerificationUri
&&
(
<
div
className=
"mb-2"
>
<
p
>
1. Go to:
<
a
href=
{
githubVerificationUri
}
// Make it a direct link
onClick=
{
(
e
)
=>
{
e
.
preventDefault
();
IpcClient
.
getInstance
().
openExternalUrl
(
githubVerificationUri
);
}
}
target=
"_blank"
rel=
"noopener noreferrer"
className=
"ml-1 text-blue-600 hover:underline dark:text-blue-400"
>
{
githubVerificationUri
}
</
a
>
</
p
>
<
p
>
2. Enter code:
<
strong
className=
"ml-1 font-mono text-lg tracking-wider bg-gray-200 dark:bg-gray-600 px-2 py-0.5 rounded"
>
{
githubUserCode
}
</
strong
>
</
p
>
</
div
>
)
}
{
githubStatusMessage
&&
(
<
p
className=
"text-sm text-gray-600 dark:text-gray-300"
>
{
githubStatusMessage
}
</
p
>
)
}
</
div
>
)
}
</
div
>
);
}
src/hooks/useSettings.ts
浏览文件 @
7ad83a2b
import
{
useState
,
useEffect
}
from
"react"
;
import
{
useState
,
useEffect
,
useCallback
}
from
"react"
;
import
{
useAtom
}
from
"jotai"
;
import
{
useAtom
}
from
"jotai"
;
import
{
userSettingsAtom
,
envVarsAtom
}
from
"@/atoms/appAtoms"
;
import
{
userSettingsAtom
,
envVarsAtom
}
from
"@/atoms/appAtoms"
;
import
{
IpcClient
}
from
"@/ipc/ipc_client"
;
import
{
IpcClient
}
from
"@/ipc/ipc_client"
;
...
@@ -18,31 +18,30 @@ export function useSettings() {
...
@@ -18,31 +18,30 @@ export function useSettings() {
const
[
envVars
,
setEnvVarsAtom
]
=
useAtom
(
envVarsAtom
);
const
[
envVars
,
setEnvVarsAtom
]
=
useAtom
(
envVarsAtom
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
useState
<
Error
|
null
>
(
null
);
const
[
error
,
setError
]
=
useState
<
Error
|
null
>
(
null
);
const
loadInitialData
=
useCallback
(
async
()
=>
{
setLoading
(
true
);
try
{
const
ipcClient
=
IpcClient
.
getInstance
();
// Fetch settings and env vars concurrently
const
[
userSettings
,
fetchedEnvVars
]
=
await
Promise
.
all
([
ipcClient
.
getUserSettings
(),
ipcClient
.
getEnvVars
(),
]);
setSettingsAtom
(
userSettings
);
setEnvVarsAtom
(
fetchedEnvVars
);
setError
(
null
);
}
catch
(
error
)
{
console
.
error
(
"Error loading initial data:"
,
error
);
setError
(
error
instanceof
Error
?
error
:
new
Error
(
String
(
error
)));
}
finally
{
setLoading
(
false
);
}
},
[
setSettingsAtom
,
setEnvVarsAtom
]);
useEffect
(()
=>
{
useEffect
(()
=>
{
const
loadInitialData
=
async
()
=>
{
setLoading
(
true
);
try
{
const
ipcClient
=
IpcClient
.
getInstance
();
// Fetch settings and env vars concurrently
const
[
userSettings
,
fetchedEnvVars
]
=
await
Promise
.
all
([
ipcClient
.
getUserSettings
(),
ipcClient
.
getEnvVars
(),
]);
setSettingsAtom
(
userSettings
);
setEnvVarsAtom
(
fetchedEnvVars
);
setError
(
null
);
}
catch
(
error
)
{
console
.
error
(
"Error loading initial data:"
,
error
);
setError
(
error
instanceof
Error
?
error
:
new
Error
(
String
(
error
)));
}
finally
{
setLoading
(
false
);
}
};
loadInitialData
();
// Only run once on mount, dependencies are stable getters/setters
// Only run once on mount, dependencies are stable getters/setters
},
[
setSettingsAtom
,
setEnvVarsAtom
]);
loadInitialData
();
},
[
loadInitialData
]);
const
updateSettings
=
async
(
newSettings
:
Partial
<
UserSettings
>
)
=>
{
const
updateSettings
=
async
(
newSettings
:
Partial
<
UserSettings
>
)
=>
{
setLoading
(
true
);
setLoading
(
true
);
...
@@ -84,5 +83,8 @@ export function useSettings() {
...
@@ -84,5 +83,8 @@ export function useSettings() {
isProviderSetup
(
provider
)
isProviderSetup
(
provider
)
);
);
},
},
refreshSettings
:
()
=>
{
loadInitialData
();
},
};
};
}
}
src/ipc/handlers/github_handlers.ts
0 → 100644
浏览文件 @
7ad83a2b
import
{
ipcMain
,
IpcMainEvent
,
BrowserWindow
,
IpcMainInvokeEvent
,
}
from
"electron"
;
import
fetch
from
"node-fetch"
;
// Use node-fetch for making HTTP requests in main process
import
{
writeSettings
}
from
"../../main/settings"
;
// --- GitHub Device Flow Constants ---
// TODO: Fetch this securely, e.g., from environment variables or a config file
const
GITHUB_CLIENT_ID
=
process
.
env
.
GITHUB_CLIENT_ID
||
"Ov23liWV2HdC0RBLecWx"
;
const
GITHUB_DEVICE_CODE_URL
=
"https://github.com/login/device/code"
;
const
GITHUB_ACCESS_TOKEN_URL
=
"https://github.com/login/oauth/access_token"
;
const
GITHUB_SCOPES
=
"repo,user"
;
// Define the scopes needed
// --- State Management (Simple in-memory, consider alternatives for robustness) ---
interface
DeviceFlowState
{
deviceCode
:
string
;
interval
:
number
;
timeoutId
:
NodeJS
.
Timeout
|
null
;
isPolling
:
boolean
;
window
:
BrowserWindow
|
null
;
// Reference to the window that initiated the flow
}
// Simple map to track ongoing flows (key could be appId or a unique flow ID if needed)
// For simplicity, let's assume only one flow can happen at a time for now.
let
currentFlowState
:
DeviceFlowState
|
null
=
null
;
// --- Helper Functions ---
// function event.sender.send(channel: string, data: any) {
// if (currentFlowState?.window && !currentFlowState.window.isDestroyed()) {
// currentFlowState.window.webContents.send(channel, data);
// }
// }
async
function
pollForAccessToken
(
event
:
IpcMainInvokeEvent
)
{
if
(
!
currentFlowState
||
!
currentFlowState
.
isPolling
)
{
console
.
log
(
"[GitHub Handler] Polling stopped or no active flow."
);
return
;
}
const
{
deviceCode
,
interval
}
=
currentFlowState
;
console
.
log
(
`[GitHub Handler] Polling for token with device code:
${
deviceCode
}
`
);
event
.
sender
.
send
(
"github:flow-update"
,
{
message
:
"Polling GitHub for authorization..."
,
});
try
{
const
response
=
await
fetch
(
GITHUB_ACCESS_TOKEN_URL
,
{
method
:
"POST"
,
headers
:
{
"Content-Type"
:
"application/json"
,
Accept
:
"application/json"
,
},
body
:
JSON
.
stringify
({
client_id
:
GITHUB_CLIENT_ID
,
device_code
:
deviceCode
,
grant_type
:
"urn:ietf:params:oauth:grant-type:device_code"
,
}),
});
const
data
=
await
response
.
json
();
if
(
response
.
ok
&&
data
.
access_token
)
{
// --- SUCCESS ---
console
.
log
(
"[GitHub Handler] Successfully obtained GitHub Access Token."
);
// TODO: Store this token securely!
event
.
sender
.
send
(
"github:flow-success"
,
{
message
:
"Successfully connected!"
,
});
writeSettings
({
githubSettings
:
{
secrets
:
{
accessToken
:
data
.
access_token
,
},
},
});
// TODO: Associate token with appId if provided
stopPolling
();
return
;
}
else
if
(
data
.
error
)
{
switch
(
data
.
error
)
{
case
"authorization_pending"
:
console
.
log
(
"[GitHub Handler] Authorization pending..."
);
event
.
sender
.
send
(
"github:flow-update"
,
{
message
:
"Waiting for user authorization..."
,
});
// Schedule next poll
currentFlowState
.
timeoutId
=
setTimeout
(
()
=>
pollForAccessToken
(
event
),
interval
*
1000
);
break
;
case
"slow_down"
:
const
newInterval
=
interval
+
5
;
console
.
log
(
`[GitHub Handler] Slow down requested. New interval:
${
newInterval
}
s`
);
currentFlowState
.
interval
=
newInterval
;
// Update interval
event
.
sender
.
send
(
"github:flow-update"
,
{
message
:
`GitHub asked to slow down. Retrying in
${
newInterval
}
s...`
,
});
currentFlowState
.
timeoutId
=
setTimeout
(
()
=>
pollForAccessToken
(
event
),
newInterval
*
1000
);
break
;
case
"expired_token"
:
console
.
error
(
"[GitHub Handler] Device code expired."
);
event
.
sender
.
send
(
"github:flow-error"
,
{
error
:
"Verification code expired. Please try again."
,
});
stopPolling
();
break
;
case
"access_denied"
:
console
.
error
(
"[GitHub Handler] Access denied by user."
);
event
.
sender
.
send
(
"github:flow-error"
,
{
error
:
"Authorization denied by user."
,
});
stopPolling
();
break
;
default
:
console
.
error
(
`[GitHub Handler] Unknown GitHub error:
${
data
.
error_description
||
data
.
error
}
`
);
event
.
sender
.
send
(
"github:flow-error"
,
{
error
:
`GitHub authorization error:
${
data
.
error_description
||
data
.
error
}
`
,
});
stopPolling
();
break
;
}
}
else
{
throw
new
Error
(
`Unknown response structure:
${
JSON
.
stringify
(
data
)}
`
);
}
}
catch
(
error
)
{
console
.
error
(
"[GitHub Handler] Error polling for GitHub access token:"
,
error
);
event
.
sender
.
send
(
"github:flow-error"
,
{
error
:
`Network or unexpected error during polling:
${
error
instanceof
Error
?
error
.
message
:
String
(
error
)
}
`
,
});
stopPolling
();
}
}
function
stopPolling
()
{
if
(
currentFlowState
)
{
if
(
currentFlowState
.
timeoutId
)
{
clearTimeout
(
currentFlowState
.
timeoutId
);
}
currentFlowState
.
isPolling
=
false
;
currentFlowState
.
timeoutId
=
null
;
// Maybe keep window reference for a bit if needed, or clear it
// currentFlowState.window = null;
console
.
log
(
"[GitHub Handler] Polling stopped."
);
}
// Setting to null signifies no active flow
// currentFlowState = null; // Decide if you want to clear immediately or allow potential restart
}
// --- IPC Handlers ---
function
handleStartGithubFlow
(
event
:
IpcMainInvokeEvent
,
args
:
{
appId
:
number
|
null
}
)
{
console
.
log
(
`[GitHub Handler] Received github:start-flow for appId:
${
args
.
appId
}
`
);
// If a flow is already in progress, maybe cancel it or send an error
if
(
currentFlowState
&&
currentFlowState
.
isPolling
)
{
console
.
warn
(
"[GitHub Handler] Another GitHub flow is already in progress."
);
event
.
sender
.
send
(
"github:flow-error"
,
{
error
:
"Another connection process is already active."
,
});
return
;
}
// Store the window that initiated the request
const
window
=
BrowserWindow
.
fromWebContents
(
event
.
sender
);
if
(
!
window
)
{
console
.
error
(
"[GitHub Handler] Could not get BrowserWindow instance."
);
return
;
}
currentFlowState
=
{
deviceCode
:
""
,
interval
:
5
,
// Default interval
timeoutId
:
null
,
isPolling
:
false
,
window
:
window
,
};
event
.
sender
.
send
(
"github:flow-update"
,
{
message
:
"Requesting device code from GitHub..."
,
});
fetch
(
GITHUB_DEVICE_CODE_URL
,
{
method
:
"POST"
,
headers
:
{
"Content-Type"
:
"application/json"
,
Accept
:
"application/json"
,
},
body
:
JSON
.
stringify
({
client_id
:
GITHUB_CLIENT_ID
,
scope
:
GITHUB_SCOPES
,
}),
})
.
then
((
res
)
=>
{
if
(
!
res
.
ok
)
{
return
res
.
json
().
then
((
errData
)
=>
{
throw
new
Error
(
`GitHub API Error:
${
errData
.
error_description
||
res
.
statusText
}
`
);
});
}
return
res
.
json
();
})
.
then
((
data
)
=>
{
console
.
log
(
"[GitHub Handler] Received device code response:"
,
data
);
if
(
!
currentFlowState
)
return
;
// Flow might have been cancelled
currentFlowState
.
deviceCode
=
data
.
device_code
;
currentFlowState
.
interval
=
data
.
interval
||
5
;
currentFlowState
.
isPolling
=
true
;
// Send user code and verification URI to renderer
event
.
sender
.
send
(
"github:flow-update"
,
{
userCode
:
data
.
user_code
,
verificationUri
:
data
.
verification_uri
,
message
:
"Please authorize in your browser."
,
});
// Start polling after the initial interval
currentFlowState
.
timeoutId
=
setTimeout
(
()
=>
pollForAccessToken
(
event
),
currentFlowState
.
interval
*
1000
);
})
.
catch
((
error
)
=>
{
console
.
error
(
"[GitHub Handler] Error initiating GitHub device flow:"
,
error
);
event
.
sender
.
send
(
"github:flow-error"
,
{
error
:
`Failed to start GitHub connection:
${
error
.
message
}
`
,
});
stopPolling
();
// Ensure polling stops on initial error
currentFlowState
=
null
;
// Clear state on initial error
});
}
// Optional: Handle cancellation from renderer
// function handleCancelGithubFlow(event: IpcMainEvent) {
// console.log('[GitHub Handler] Received github:cancel-flow');
// stopPolling();
// currentFlowState = null; // Clear state on cancel
// // Optionally send confirmation back
// event.sender.send('github:flow-cancelled', { message: 'GitHub flow cancelled.' });
// }
// --- Registration ---
export
function
registerGithubHandlers
()
{
ipcMain
.
handle
(
"github:start-flow"
,
handleStartGithubFlow
);
// ipcMain.on('github:cancel-flow', handleCancelGithubFlow); // Uncomment if you add cancellation
}
src/ipc/ipc_client.ts
浏览文件 @
7ad83a2b
...
@@ -29,6 +29,20 @@ export interface AppStreamCallbacks {
...
@@ -29,6 +29,20 @@ export interface AppStreamCallbacks {
onOutput
:
(
output
:
AppOutput
)
=>
void
;
onOutput
:
(
output
:
AppOutput
)
=>
void
;
}
}
export
interface
GitHubDeviceFlowUpdateData
{
userCode
?:
string
;
verificationUri
?:
string
;
message
?:
string
;
}
export
interface
GitHubDeviceFlowSuccessData
{
message
?:
string
;
}
export
interface
GitHubDeviceFlowErrorData
{
error
:
string
;
}
export
class
IpcClient
{
export
class
IpcClient
{
private
static
instance
:
IpcClient
;
private
static
instance
:
IpcClient
;
private
ipcRenderer
:
IpcRenderer
;
private
ipcRenderer
:
IpcRenderer
;
...
@@ -505,4 +519,58 @@ export class IpcClient {
...
@@ -505,4 +519,58 @@ export class IpcClient {
throw
error
;
throw
error
;
}
}
}
}
// --- GitHub Device Flow ---
public
startGithubDeviceFlow
(
appId
:
number
|
null
):
void
{
this
.
ipcRenderer
.
invoke
(
"github:start-flow"
,
{
appId
});
}
public
onGithubDeviceFlowUpdate
(
callback
:
(
data
:
GitHubDeviceFlowUpdateData
)
=>
void
):
()
=>
void
{
const
listener
=
(
data
:
any
)
=>
{
console
.
log
(
"github:flow-update"
,
data
);
callback
(
data
as
GitHubDeviceFlowUpdateData
);
};
this
.
ipcRenderer
.
on
(
"github:flow-update"
,
listener
);
// Return a function to remove the listener
return
()
=>
{
this
.
ipcRenderer
.
removeListener
(
"github:flow-update"
,
listener
);
};
}
public
onGithubDeviceFlowSuccess
(
callback
:
(
data
:
GitHubDeviceFlowSuccessData
)
=>
void
):
()
=>
void
{
const
listener
=
(
data
:
any
)
=>
{
console
.
log
(
"github:flow-success"
,
data
);
callback
(
data
as
GitHubDeviceFlowSuccessData
);
};
this
.
ipcRenderer
.
on
(
"github:flow-success"
,
listener
);
return
()
=>
{
this
.
ipcRenderer
.
removeListener
(
"github:flow-success"
,
listener
);
};
}
public
onGithubDeviceFlowError
(
callback
:
(
data
:
GitHubDeviceFlowErrorData
)
=>
void
):
()
=>
void
{
const
listener
=
(
data
:
any
)
=>
{
console
.
log
(
"github:flow-error"
,
data
);
callback
(
data
as
GitHubDeviceFlowErrorData
);
};
this
.
ipcRenderer
.
on
(
"github:flow-error"
,
listener
);
return
()
=>
{
this
.
ipcRenderer
.
removeListener
(
"github:flow-error"
,
listener
);
};
}
// TODO: Implement cancel method if needed
// public cancelGithubDeviceFlow(): void {
// this.ipcRenderer.sendMessage("github:cancel-flow");
// }
// --- End GitHub Device Flow ---
// Example methods for listening to events (if needed)
// public on(channel: string, func: (...args: any[]) => void): void {
}
}
src/ipc/ipc_host.ts
浏览文件 @
7ad83a2b
...
@@ -4,6 +4,7 @@ import { registerChatStreamHandlers } from "./handlers/chat_stream_handlers";
...
@@ -4,6 +4,7 @@ import { registerChatStreamHandlers } from "./handlers/chat_stream_handlers";
import
{
registerSettingsHandlers
}
from
"./handlers/settings_handlers"
;
import
{
registerSettingsHandlers
}
from
"./handlers/settings_handlers"
;
import
{
registerShellHandlers
}
from
"./handlers/shell_handler"
;
import
{
registerShellHandlers
}
from
"./handlers/shell_handler"
;
import
{
registerDependencyHandlers
}
from
"./handlers/dependency_handlers"
;
import
{
registerDependencyHandlers
}
from
"./handlers/dependency_handlers"
;
import
{
registerGithubHandlers
}
from
"./handlers/github_handlers"
;
export
function
registerIpcHandlers
()
{
export
function
registerIpcHandlers
()
{
// Register all IPC handlers by category
// Register all IPC handlers by category
...
@@ -13,4 +14,5 @@ export function registerIpcHandlers() {
...
@@ -13,4 +14,5 @@ export function registerIpcHandlers() {
registerSettingsHandlers
();
registerSettingsHandlers
();
registerShellHandlers
();
registerShellHandlers
();
registerDependencyHandlers
();
registerDependencyHandlers
();
registerGithubHandlers
();
}
}
src/lib/schemas.ts
浏览文件 @
7ad83a2b
...
@@ -64,6 +64,16 @@ export type ProviderSetting = z.infer<typeof ProviderSettingSchema>;
...
@@ -64,6 +64,16 @@ export type ProviderSetting = z.infer<typeof ProviderSettingSchema>;
export
const
RuntimeModeSchema
=
z
.
enum
([
"web-sandbox"
,
"local-node"
,
"unset"
]);
export
const
RuntimeModeSchema
=
z
.
enum
([
"web-sandbox"
,
"local-node"
,
"unset"
]);
export
type
RuntimeMode
=
z
.
infer
<
typeof
RuntimeModeSchema
>
;
export
type
RuntimeMode
=
z
.
infer
<
typeof
RuntimeModeSchema
>
;
export
const
GitHubSecretsSchema
=
z
.
object
({
accessToken
:
z
.
string
().
nullable
(),
});
export
type
GitHubSecrets
=
z
.
infer
<
typeof
GitHubSecretsSchema
>
;
export
const
GitHubSettingsSchema
=
z
.
object
({
secrets
:
GitHubSecretsSchema
.
nullable
(),
});
export
type
GitHubSettings
=
z
.
infer
<
typeof
GitHubSettingsSchema
>
;
/**
/**
* Zod schema for user settings
* Zod schema for user settings
*/
*/
...
@@ -71,6 +81,7 @@ export const UserSettingsSchema = z.object({
...
@@ -71,6 +81,7 @@ export const UserSettingsSchema = z.object({
selectedModel
:
LargeLanguageModelSchema
,
selectedModel
:
LargeLanguageModelSchema
,
providerSettings
:
z
.
record
(
z
.
string
(),
ProviderSettingSchema
),
providerSettings
:
z
.
record
(
z
.
string
(),
ProviderSettingSchema
),
runtimeMode
:
RuntimeModeSchema
,
runtimeMode
:
RuntimeModeSchema
,
githubSettings
:
GitHubSettingsSchema
,
});
});
/**
/**
...
...
src/main/settings.ts
浏览文件 @
7ad83a2b
...
@@ -2,7 +2,7 @@ import fs from "node:fs";
...
@@ -2,7 +2,7 @@ import fs from "node:fs";
import
path
from
"node:path"
;
import
path
from
"node:path"
;
import
{
getUserDataPath
}
from
"../paths/paths"
;
import
{
getUserDataPath
}
from
"../paths/paths"
;
import
{
UserSettingsSchema
,
type
UserSettings
}
from
"../lib/schemas"
;
import
{
UserSettingsSchema
,
type
UserSettings
}
from
"../lib/schemas"
;
import
{
safeStorage
}
from
"electron"
;
const
DEFAULT_SETTINGS
:
UserSettings
=
{
const
DEFAULT_SETTINGS
:
UserSettings
=
{
selectedModel
:
{
selectedModel
:
{
name
:
"auto"
,
name
:
"auto"
,
...
@@ -10,6 +10,9 @@ const DEFAULT_SETTINGS: UserSettings = {
...
@@ -10,6 +10,9 @@ const DEFAULT_SETTINGS: UserSettings = {
},
},
providerSettings
:
{},
providerSettings
:
{},
runtimeMode
:
"unset"
,
runtimeMode
:
"unset"
,
githubSettings
:
{
secrets
:
null
,
},
};
};
const
SETTINGS_FILE
=
"user-settings.json"
;
const
SETTINGS_FILE
=
"user-settings.json"
;
...
@@ -31,6 +34,13 @@ export function readSettings(): UserSettings {
...
@@ -31,6 +34,13 @@ export function readSettings(): UserSettings {
...
DEFAULT_SETTINGS
,
...
DEFAULT_SETTINGS
,
...
rawSettings
,
...
rawSettings
,
});
});
if
(
validatedSettings
.
githubSettings
?.
secrets
)
{
const
accessToken
=
validatedSettings
.
githubSettings
.
secrets
.
accessToken
;
validatedSettings
.
githubSettings
.
secrets
=
{
accessToken
:
accessToken
?
decrypt
(
accessToken
)
:
null
,
};
}
return
validatedSettings
;
return
validatedSettings
;
}
catch
(
error
)
{
}
catch
(
error
)
{
console
.
error
(
"Error reading settings:"
,
error
);
console
.
error
(
"Error reading settings:"
,
error
);
...
@@ -45,8 +55,28 @@ export function writeSettings(settings: Partial<UserSettings>): void {
...
@@ -45,8 +55,28 @@ export function writeSettings(settings: Partial<UserSettings>): void {
const
newSettings
=
{
...
currentSettings
,
...
settings
};
const
newSettings
=
{
...
currentSettings
,
...
settings
};
// Validate before writing
// Validate before writing
const
validatedSettings
=
UserSettingsSchema
.
parse
(
newSettings
);
const
validatedSettings
=
UserSettingsSchema
.
parse
(
newSettings
);
if
(
validatedSettings
.
githubSettings
?.
secrets
)
{
const
accessToken
=
validatedSettings
.
githubSettings
.
secrets
.
accessToken
;
validatedSettings
.
githubSettings
.
secrets
=
{
accessToken
:
accessToken
?
encrypt
(
accessToken
)
:
null
,
};
}
fs
.
writeFileSync
(
filePath
,
JSON
.
stringify
(
validatedSettings
,
null
,
2
));
fs
.
writeFileSync
(
filePath
,
JSON
.
stringify
(
validatedSettings
,
null
,
2
));
}
catch
(
error
)
{
}
catch
(
error
)
{
console
.
error
(
"Error writing settings:"
,
error
);
console
.
error
(
"Error writing settings:"
,
error
);
}
}
}
}
export
function
encrypt
(
data
:
string
):
string
{
if
(
safeStorage
.
isEncryptionAvailable
())
{
return
safeStorage
.
encryptString
(
data
).
toString
(
"base64"
);
}
return
data
;
}
export
function
decrypt
(
data
:
string
):
string
{
if
(
safeStorage
.
isEncryptionAvailable
())
{
return
safeStorage
.
decryptString
(
Buffer
.
from
(
data
,
"base64"
));
}
return
data
;
}
src/pages/app-details.tsx
浏览文件 @
7ad83a2b
...
@@ -3,7 +3,7 @@ import { useAtom, useAtomValue } from "jotai";
...
@@ -3,7 +3,7 @@ import { useAtom, useAtomValue } from "jotai";
import
{
appBasePathAtom
,
appsListAtom
}
from
"@/atoms/appAtoms"
;
import
{
appBasePathAtom
,
appsListAtom
}
from
"@/atoms/appAtoms"
;
import
{
IpcClient
}
from
"@/ipc/ipc_client"
;
import
{
IpcClient
}
from
"@/ipc/ipc_client"
;
import
{
useLoadApps
}
from
"@/hooks/useLoadApps"
;
import
{
useLoadApps
}
from
"@/hooks/useLoadApps"
;
import
{
useState
}
from
"react"
;
import
{
useState
,
useEffect
}
from
"react"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
import
{
ArrowLeft
,
ArrowLeft
,
...
@@ -11,6 +11,7 @@ import {
...
@@ -11,6 +11,7 @@ import {
ArrowRight
,
ArrowRight
,
MessageCircle
,
MessageCircle
,
Pencil
,
Pencil
,
Github
,
}
from
"lucide-react"
;
}
from
"lucide-react"
;
import
{
import
{
Popover
,
Popover
,
...
@@ -26,6 +27,7 @@ import {
...
@@ -26,6 +27,7 @@ import {
DialogHeader
,
DialogHeader
,
DialogTitle
,
DialogTitle
,
}
from
"@/components/ui/dialog"
;
}
from
"@/components/ui/dialog"
;
import
{
GitHubConnector
}
from
"@/components/GitHubConnector"
;
export
default
function
AppDetailsPage
()
{
export
default
function
AppDetailsPage
()
{
const
navigate
=
useNavigate
();
const
navigate
=
useNavigate
();
...
@@ -225,7 +227,7 @@ export default function AppDetailsPage() {
...
@@ -225,7 +227,7 @@ export default function AppDetailsPage() {
</
span
>
</
span
>
</
div
>
</
div
>
</
div
>
</
div
>
<
div
className=
"mt-8 flex gap-4"
>
<
div
className=
"mt-8 flex
flex-col
gap-4"
>
<
Button
<
Button
onClick=
{
()
=>
onClick=
{
()
=>
appId
&&
navigate
({
to
:
"/chat"
,
search
:
{
id
:
appId
}
})
appId
&&
navigate
({
to
:
"/chat"
,
search
:
{
id
:
appId
}
})
...
@@ -236,6 +238,7 @@ export default function AppDetailsPage() {
...
@@ -236,6 +238,7 @@ export default function AppDetailsPage() {
Open in Chat
Open in Chat
<
MessageCircle
className=
"h-5 w-5"
/>
<
MessageCircle
className=
"h-5 w-5"
/>
</
Button
>
</
Button
>
<
GitHubConnector
appId=
{
appId
}
/>
</
div
>
</
div
>
{
/* Rename Dialog */
}
{
/* Rename Dialog */
}
...
...
src/preload.ts
浏览文件 @
7ad83a2b
...
@@ -32,6 +32,7 @@ const validInvokeChannels = [
...
@@ -32,6 +32,7 @@ const validInvokeChannels = [
"open-external-url"
,
"open-external-url"
,
"reset-all"
,
"reset-all"
,
"nodejs-status"
,
"nodejs-status"
,
"github:start-flow"
,
]
as
const
;
]
as
const
;
// Add valid receive channels
// Add valid receive channels
...
@@ -40,6 +41,9 @@ const validReceiveChannels = [
...
@@ -40,6 +41,9 @@ const validReceiveChannels = [
"chat:response:end"
,
"chat:response:end"
,
"chat:response:error"
,
"chat:response:error"
,
"app:output"
,
"app:output"
,
"github:flow-update"
,
"github:flow-success"
,
"github:flow-error"
,
]
as
const
;
]
as
const
;
type
ValidInvokeChannel
=
(
typeof
validInvokeChannels
)[
number
];
type
ValidInvokeChannel
=
(
typeof
validInvokeChannels
)[
number
];
...
@@ -76,5 +80,13 @@ contextBridge.exposeInMainWorld("electron", {
...
@@ -76,5 +80,13 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer
.
removeAllListeners
(
channel
);
ipcRenderer
.
removeAllListeners
(
channel
);
}
}
},
},
removeListener
:
(
channel
:
ValidReceiveChannel
,
listener
:
(...
args
:
unknown
[])
=>
void
)
=>
{
if
(
validReceiveChannels
.
includes
(
channel
))
{
ipcRenderer
.
removeListener
(
channel
,
listener
);
}
},
},
},
});
});
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论