Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions local_agent/src/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,13 @@ export function connect(url: string, token: string): void {
currentUrl = url;
currentToken = token;
shouldReconnect = true;
resetSessionApprovalState();
doConnect();
}

export function disconnect(): void {
shouldReconnect = false;
resetSessionApprovalState();
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
Expand Down Expand Up @@ -157,10 +159,23 @@ const pendingConfirms: Map<string, {

// 本次会话是否已启用「始终允许」模式(连接断开后重置)
let sessionAlwaysApprove = false;
// 记录启用「始终允许」的聊天会话 ID(仅对应会话生效)
const alwaysApproveChatSessionIds = new Set<string>();

// 本次会话缓存的 sudo 密码(连接断开后清空,绝不写盘)
let cachedSudoPassword: string | null = null;

function resetSessionApprovalState(): void {
sessionAlwaysApprove = false;
alwaysApproveChatSessionIds.clear();
cachedSudoPassword = null;
}

function getChatSessionId(request: ToolRequest): string | null {
const sessionId = request.args?.__chat_session_id;
return typeof sessionId === "string" && sessionId.length > 0 ? sessionId : null;
}

/** 处理服务端发来的确认响应 */
export function handleConfirmResponse(id: string, approved: boolean, always?: boolean, password?: string): void {
const pending = pendingConfirms.get(id);
Expand Down Expand Up @@ -207,11 +222,13 @@ function needsSudo(command: string): boolean {
async function handleRequest(request: ToolRequest): Promise<void> {
const command = typeof request.args.command === "string" ? request.args.command : "";
const isSudoCommand = request.tool === "run_command" && needsSudo(command);
const chatSessionId = getChatSessionId(request);
const isSessionApproved = !!chatSessionId && alwaysApproveChatSessionIds.has(chatSessionId);
const isApproved = autoApprove || sessionAlwaysApprove || isSessionApproved;

// Check if dangerous — skip confirmation if autoApprove or sessionAlwaysApprove is on
// Check if dangerous — skip confirmation when current request is already approved.
if (
!autoApprove &&
!sessionAlwaysApprove &&
!isApproved &&
request.tool === "run_command" &&
isDangerous(command)
) {
Expand All @@ -222,7 +239,12 @@ async function handleRequest(request: ToolRequest): Promise<void> {
const result = await requestWebConfirmation(request, requirePassword);
if (result.action === "always") {
// 「始终允许」:本次会话后续所有命令都跳过确认
sessionAlwaysApprove = true;
if (chatSessionId) {
alwaysApproveChatSessionIds.add(chatSessionId);
} else {
// 兼容未携带 __chat_session_id 的旧请求:仍在当前连接会话内放行。
sessionAlwaysApprove = true;
}
if (result.password) {
cachedSudoPassword = result.password;
}
Expand Down
3 changes: 3 additions & 0 deletions root_agent/tools/local_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,11 @@ async def local_list_files(
async def _call(tool_context: ToolContext, tool: str, args: dict, device: str = "") -> str:
"""实际调用 local_agent 的内部方法。"""
user_id = tool_context.state.get("__user_id")
chat_session_id = tool_context.state.get("__chat_session_id")
if not user_id:
return "错误:无法确定用户身份"
if isinstance(chat_session_id, str) and chat_session_id:
args = {**args, "__chat_session_id": chat_session_id}

from server.routers.local_agent import (
_connections,
Expand Down