@allbluecn/web-app 0.23.7 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/components/ai/global-harness-sidebar/__tests__/global-ai-prompts.test.ts +0 -2
- package/src/components/harness/ai-chat-history/components/conversation-item.tsx +6 -0
- package/src/components/harness/ai-chat-history/types.ts +4 -0
- package/src/components/harness/chat-ui.tsx +20 -0
- package/src/components/harness/composer/harness-composer.tsx +43 -10
- package/src/components/harness/composer/harness-engine-control.tsx +129 -0
- package/src/components/harness/harness-workbench-page.tsx +129 -17
- package/src/components/layout/sidebar-03/app-sidebar.tsx +2 -9
- package/src/config/sidebar-routes.tsx +0 -13
- package/src/lib/changelog/sdk-versions.ts +1 -1
- package/src/lib/hotkeys/__tests__/registry.test.ts +4 -11
- package/src/lib/hotkeys/registry.ts +0 -8
- package/src/paraglide/README.md +1 -1
- package/src/paraglide/messages/_index.d.ts +12 -2
- package/src/paraglide/messages/_index.js +12 -2
- package/src/paraglide/messages/{agent_desktop_title.d.ts → harness_engine_cloud.d.ts} +5 -5
- package/src/paraglide/messages/harness_engine_cloud.js +29 -0
- package/src/paraglide/messages/harness_engine_label.d.ts +16 -0
- package/src/paraglide/messages/harness_engine_label.js +29 -0
- package/src/paraglide/messages/harness_engine_local.d.ts +16 -0
- package/src/paraglide/messages/harness_engine_local.js +29 -0
- package/src/paraglide/messages/harness_engine_localbadge1.d.ts +17 -0
- package/src/paraglide/messages/harness_engine_localbadge1.js +30 -0
- package/src/paraglide/messages/harness_engine_localhint1.d.ts +17 -0
- package/src/paraglide/messages/harness_engine_localhint1.js +30 -0
- package/src/paraglide/messages/harness_engine_localnoattachments2.d.ts +17 -0
- package/src/paraglide/messages/harness_engine_localnoattachments2.js +30 -0
- package/src/paraglide/messages/harness_engine_switchlocked1.d.ts +17 -0
- package/src/paraglide/messages/harness_engine_switchlocked1.js +30 -0
- package/src/paraglide/messages/harness_localworkspace_choose1.d.ts +17 -0
- package/src/paraglide/messages/harness_localworkspace_choose1.js +30 -0
- package/src/paraglide/messages/harness_localworkspace_dialogdescription2.d.ts +17 -0
- package/src/paraglide/messages/harness_localworkspace_dialogdescription2.js +30 -0
- package/src/paraglide/messages/harness_localworkspace_dialogtitle2.d.ts +17 -0
- package/src/paraglide/messages/harness_localworkspace_dialogtitle2.js +30 -0
- package/src/paraglide/messages/harness_localworkspace_manage1.d.ts +17 -0
- package/src/paraglide/messages/harness_localworkspace_manage1.js +30 -0
- package/src/paraglide/messages/harness_localworkspace_required1.d.ts +17 -0
- package/src/paraglide/messages/harness_localworkspace_required1.js +30 -0
- package/src/routeTree.gen.ts +0 -23
- package/src/routes/_nav/harness/$threadId.lazy.tsx +19 -2
- package/src/routes/_nav/harness/$threadId.tsx +8 -2
- package/src/server/__tests__/chat-conversations.test.ts +13 -3
- package/src/server/__tests__/desktop-bootstrap.test.ts +0 -1
- package/src/server/ai-gateway/__tests__/local-agent-adapter.test.ts +110 -0
- package/src/server/ai-gateway/__tests__/unified-router-local-agent.test.ts +63 -0
- package/src/server/ai-gateway/chat-handler.ts +68 -2
- package/src/server/ai-gateway/chat-stream-builder.ts +52 -0
- package/src/server/ai-gateway/local-agent-adapter.ts +232 -0
- package/src/server/ai-gateway/unified-router.ts +49 -6
- package/src/server/serverFns/chat/conversations.ts +122 -1
- package/src/components/desktop-agent/__tests__/agent-chat.test.tsx +0 -194
- package/src/components/desktop-agent/agent-chat.tsx +0 -222
- package/src/lib/desktop-agent/__tests__/connection.test.ts +0 -310
- package/src/lib/desktop-agent/__tests__/errors.test.ts +0 -43
- package/src/lib/desktop-agent/connection.ts +0 -93
- package/src/lib/desktop-agent/errors.ts +0 -21
- package/src/paraglide/messages/agent_desktop_title.js +0 -29
- package/src/paraglide/messages/common_hotkey_desc_agentabort1.d.ts +0 -17
- package/src/paraglide/messages/common_hotkey_desc_agentabort1.js +0 -30
- package/src/routes/-tests/agent-gate.test.ts +0 -45
- package/src/routes/_nav/agent/route.lazy.tsx +0 -63
- package/src/routes/_nav/agent/route.tsx +0 -21
- /package/src/components/{desktop-agent → harness/local-agent}/__tests__/agent-tool-card.test.tsx +0 -0
- /package/src/components/{desktop-agent → harness/local-agent}/__tests__/agent-workspace-card.test.tsx +0 -0
- /package/src/components/{desktop-agent → harness/local-agent}/agent-tool-card.tsx +0 -0
- /package/src/components/{desktop-agent → harness/local-agent}/agent-workspace-card.tsx +0 -0
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
-
// AllBlue - 理想之海
|
|
3
|
-
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
-
|
|
5
|
-
// desktop-agent 连接(桌面本地优先 P3-B5):调用时懒取 agent 端点 → 真实
|
|
6
|
-
// fetchServerSentEvents 直连 http://127.0.0.1:{port}/prompt。fetch 打桩、SSE 解析
|
|
7
|
-
// 走真实 @tanstack/ai-client 实现——请求线格式即与 Pro 端点的跨仓契约:URL /
|
|
8
|
-
// X-Desktop-Token / X-Run-Id 头 / forwardedProps.cwd(端点 extractCwd 取值位)。
|
|
9
|
-
// 两种壳形态各覆盖:新壳经 agentCredentials 按需取凭据(appInfo.agentToken 恒 null,
|
|
10
|
-
// 不参与连接),旧壳回落 appInfo 的 agentPort/agentToken。
|
|
11
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
12
|
-
import type { UIMessage } from '@tanstack/ai-client'
|
|
13
|
-
|
|
14
|
-
type AppInfo = {
|
|
15
|
-
version: string
|
|
16
|
-
clientId: string
|
|
17
|
-
platform: string
|
|
18
|
-
backendPort: number | null
|
|
19
|
-
agentPort?: number | null
|
|
20
|
-
agentToken?: string | null
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
type AgentCredentials = { port: number; token: string }
|
|
24
|
-
|
|
25
|
-
type Bridge = {
|
|
26
|
-
appInfo: () => Promise<AppInfo>
|
|
27
|
-
/** 新壳按需取凭据通道;旧壳无此方法(undefined) */
|
|
28
|
-
agentCredentials?: () => Promise<AgentCredentials | null>
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const bridgeRef: { current: Bridge | null } = { current: null }
|
|
32
|
-
|
|
33
|
-
vi.mock('@/lib/desktop-bridge-types', () => ({
|
|
34
|
-
getDesktopBridge: () => bridgeRef.current,
|
|
35
|
-
}))
|
|
36
|
-
|
|
37
|
-
import {
|
|
38
|
-
DesktopAgentUnavailableError,
|
|
39
|
-
createDesktopAgentConnection,
|
|
40
|
-
resolveDesktopAgentEndpoint,
|
|
41
|
-
} from '../connection'
|
|
42
|
-
|
|
43
|
-
const BASE_INFO: AppInfo = { version: '1.0.0', clientId: 'c1', platform: 'darwin', backendPort: 51000 }
|
|
44
|
-
|
|
45
|
-
function bridgeWith(info: Partial<AppInfo>) {
|
|
46
|
-
return { appInfo: vi.fn(async () => ({ ...BASE_INFO, ...info })) }
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** 新壳桥:appInfo 里 agentPort 仍广播(门控用)、agentToken 恒 null;凭据走按需 IPC */
|
|
50
|
-
function newShellBridge(credentials: AgentCredentials | null, info: Partial<AppInfo> = {}) {
|
|
51
|
-
return {
|
|
52
|
-
appInfo: vi.fn(async () => ({ ...BASE_INFO, agentPort: null, agentToken: null, ...info })),
|
|
53
|
-
agentCredentials: vi.fn(async () => credentials),
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const encoder = new TextEncoder()
|
|
58
|
-
function sse(event: Record<string, unknown>): Uint8Array {
|
|
59
|
-
return encoder.encode(`data: ${JSON.stringify(event)}\n\n`)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** 打桩 fetch:返回给定 AG-UI 事件序列的 SSE 响应 */
|
|
63
|
-
function stubSseFetch(events: Array<Record<string, unknown>>) {
|
|
64
|
-
const fetchMock = vi.fn(async (_url: string | URL | RequestInfo, _init?: RequestInit) => {
|
|
65
|
-
const stream = new ReadableStream<Uint8Array>({
|
|
66
|
-
start(c) {
|
|
67
|
-
for (const e of events) c.enqueue(sse(e))
|
|
68
|
-
c.close()
|
|
69
|
-
},
|
|
70
|
-
})
|
|
71
|
-
return new Response(stream, { status: 200, headers: { 'content-type': 'text/event-stream' } })
|
|
72
|
-
})
|
|
73
|
-
vi.stubGlobal('fetch', fetchMock)
|
|
74
|
-
return fetchMock
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const RUN_EVENTS = [
|
|
78
|
-
{ type: 'RUN_STARTED', threadId: 't-1', runId: 'r-1' },
|
|
79
|
-
{ type: 'TEXT_MESSAGE_START', messageId: 'msg-1', role: 'assistant' },
|
|
80
|
-
{ type: 'TEXT_MESSAGE_CONTENT', messageId: 'msg-1', delta: '目录里有 a.txt' },
|
|
81
|
-
{ type: 'TEXT_MESSAGE_END', messageId: 'msg-1' },
|
|
82
|
-
{ type: 'RUN_FINISHED', threadId: 't-1', runId: 'r-1' },
|
|
83
|
-
]
|
|
84
|
-
|
|
85
|
-
const USER_MESSAGE: UIMessage = { id: 'u1', role: 'user', parts: [{ type: 'text', content: '列出目录' }] }
|
|
86
|
-
|
|
87
|
-
async function drain(iterable: AsyncIterable<unknown>): Promise<unknown[]> {
|
|
88
|
-
const out: unknown[] = []
|
|
89
|
-
for await (const chunk of iterable) out.push(chunk)
|
|
90
|
-
return out
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
beforeEach(() => {
|
|
94
|
-
bridgeRef.current = null
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
afterEach(() => {
|
|
98
|
-
vi.unstubAllGlobals()
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
describe('resolveDesktopAgentEndpoint(appInfo 懒取)', () => {
|
|
102
|
-
it('无桥(云 Web / OSS)→ null', async () => {
|
|
103
|
-
await expect(resolveDesktopAgentEndpoint()).resolves.toBeNull()
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
it('agent 未装配(agentPort/agentToken 为 null 或旧壳 undefined)→ null', async () => {
|
|
107
|
-
bridgeRef.current = bridgeWith({ agentPort: null, agentToken: null })
|
|
108
|
-
await expect(resolveDesktopAgentEndpoint()).resolves.toBeNull()
|
|
109
|
-
bridgeRef.current = bridgeWith({})
|
|
110
|
-
await expect(resolveDesktopAgentEndpoint()).resolves.toBeNull()
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
it('端口或令牌非法(越界 / 非整数 / 空令牌)→ null,不拼出坏 URL', async () => {
|
|
114
|
-
for (const agentPort of [0, 65536, 1.5, -1]) {
|
|
115
|
-
bridgeRef.current = bridgeWith({ agentPort, agentToken: 'tok' })
|
|
116
|
-
await expect(resolveDesktopAgentEndpoint()).resolves.toBeNull()
|
|
117
|
-
}
|
|
118
|
-
bridgeRef.current = bridgeWith({ agentPort: 4567, agentToken: '' })
|
|
119
|
-
await expect(resolveDesktopAgentEndpoint()).resolves.toBeNull()
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
it('就绪 → 127.0.0.1 端点 URL + 令牌', async () => {
|
|
123
|
-
bridgeRef.current = bridgeWith({ agentPort: 4567, agentToken: 'tok-abc' })
|
|
124
|
-
await expect(resolveDesktopAgentEndpoint()).resolves.toEqual({
|
|
125
|
-
url: 'http://127.0.0.1:4567/prompt',
|
|
126
|
-
token: 'tok-abc',
|
|
127
|
-
})
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
it('appInfo 抛错(IPC 断开 / main 未就绪)→ null,不流出原生错误原文', async () => {
|
|
131
|
-
bridgeRef.current = {
|
|
132
|
-
appInfo: vi.fn(async () => {
|
|
133
|
-
throw new Error('ipc closed')
|
|
134
|
-
}),
|
|
135
|
-
}
|
|
136
|
-
await expect(resolveDesktopAgentEndpoint()).resolves.toBeNull()
|
|
137
|
-
})
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
describe('resolveDesktopAgentEndpoint(新壳 agentCredentials 优先 / 旧壳回落 appInfo)', () => {
|
|
141
|
-
it('新壳:用 agentCredentials 的 {port, token} 构造端点,不读 appInfo 的令牌', async () => {
|
|
142
|
-
// appInfo 故意给一个不同的端口 + 陈旧令牌:端点必须只来自 agentCredentials
|
|
143
|
-
const bridge = newShellBridge({ port: 2468, token: 'tok-cred' }, { agentPort: 4567, agentToken: 'ok' })
|
|
144
|
-
bridgeRef.current = bridge
|
|
145
|
-
await expect(resolveDesktopAgentEndpoint()).resolves.toEqual({
|
|
146
|
-
url: 'http://127.0.0.1:2468/prompt',
|
|
147
|
-
token: 'tok-cred',
|
|
148
|
-
})
|
|
149
|
-
expect(bridge.agentCredentials).toHaveBeenCalledTimes(1)
|
|
150
|
-
expect(bridge.appInfo).not.toHaveBeenCalled()
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
it('新壳:agentCredentials 返回 null(未装配)或抛错(IPC 断开)→ null,不回落 appInfo', async () => {
|
|
154
|
-
const bridge = newShellBridge(null)
|
|
155
|
-
bridgeRef.current = bridge
|
|
156
|
-
await expect(resolveDesktopAgentEndpoint()).resolves.toBeNull()
|
|
157
|
-
expect(bridge.appInfo).not.toHaveBeenCalled()
|
|
158
|
-
|
|
159
|
-
bridgeRef.current = {
|
|
160
|
-
appInfo: vi.fn(async () => ({ ...BASE_INFO, agentPort: 4567, agentToken: null })),
|
|
161
|
-
agentCredentials: vi.fn(async () => {
|
|
162
|
-
throw new Error('ipc closed')
|
|
163
|
-
}),
|
|
164
|
-
}
|
|
165
|
-
await expect(resolveDesktopAgentEndpoint()).resolves.toBeNull()
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
it('新壳:agentCredentials 产出的端口/令牌非法 → null(与 appInfo 路径同口径)', async () => {
|
|
169
|
-
bridgeRef.current = newShellBridge({ port: 0, token: 'tok' })
|
|
170
|
-
await expect(resolveDesktopAgentEndpoint()).resolves.toBeNull()
|
|
171
|
-
bridgeRef.current = newShellBridge({ port: 4567, token: '' })
|
|
172
|
-
await expect(resolveDesktopAgentEndpoint()).resolves.toBeNull()
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
it('旧壳(无 agentCredentials)→ 回落 appInfo 的 agentPort + agentToken', async () => {
|
|
176
|
-
bridgeRef.current = bridgeWith({ agentPort: 4567, agentToken: 'tok-abc' })
|
|
177
|
-
await expect(resolveDesktopAgentEndpoint()).resolves.toEqual({
|
|
178
|
-
url: 'http://127.0.0.1:4567/prompt',
|
|
179
|
-
token: 'tok-abc',
|
|
180
|
-
})
|
|
181
|
-
})
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
describe('createDesktopAgentConnection(fetchServerSentEvents 包装)', () => {
|
|
185
|
-
it('端点不可用 → 抛 DesktopAgentUnavailableError,且不发起 fetch', async () => {
|
|
186
|
-
const fetchMock = stubSseFetch(RUN_EVENTS)
|
|
187
|
-
bridgeRef.current = bridgeWith({ agentPort: null, agentToken: null })
|
|
188
|
-
const connection = createDesktopAgentConnection()
|
|
189
|
-
await expect(
|
|
190
|
-
drain(connection.connect([USER_MESSAGE], undefined, undefined, { threadId: 't-1', runId: 'r-1' })),
|
|
191
|
-
).rejects.toBeInstanceOf(DesktopAgentUnavailableError)
|
|
192
|
-
expect(fetchMock).not.toHaveBeenCalled()
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
it('appInfo 抛错 → 同样归「服务未就绪」(DesktopAgentUnavailableError),不发 fetch、不透出 IPC 原文', async () => {
|
|
196
|
-
const fetchMock = stubSseFetch(RUN_EVENTS)
|
|
197
|
-
bridgeRef.current = {
|
|
198
|
-
appInfo: vi.fn(async () => {
|
|
199
|
-
throw new Error('ipc closed')
|
|
200
|
-
}),
|
|
201
|
-
}
|
|
202
|
-
const error = await drain(
|
|
203
|
-
createDesktopAgentConnection().connect([USER_MESSAGE], undefined, undefined, { threadId: 't', runId: 'r' }),
|
|
204
|
-
).catch((cause: unknown) => cause)
|
|
205
|
-
expect(error).toBeInstanceOf(DesktopAgentUnavailableError)
|
|
206
|
-
expect(fetchMock).not.toHaveBeenCalled()
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
it('请求线格式:URL / X-Desktop-Token / X-Run-Id / forwardedProps.cwd 与 Pro 端点契约一致', async () => {
|
|
210
|
-
const fetchMock = stubSseFetch(RUN_EVENTS)
|
|
211
|
-
bridgeRef.current = bridgeWith({ agentPort: 4567, agentToken: 'tok-abc' })
|
|
212
|
-
const connection = createDesktopAgentConnection()
|
|
213
|
-
await drain(
|
|
214
|
-
connection.connect([USER_MESSAGE], undefined, undefined, {
|
|
215
|
-
threadId: 't-1',
|
|
216
|
-
runId: 'r-1',
|
|
217
|
-
forwardedProps: { cwd: '/Users/x/proj' },
|
|
218
|
-
}),
|
|
219
|
-
)
|
|
220
|
-
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
221
|
-
const [url, init] = fetchMock.mock.calls[0]!
|
|
222
|
-
expect(url).toBe('http://127.0.0.1:4567/prompt')
|
|
223
|
-
expect(init?.method).toBe('POST')
|
|
224
|
-
const headers = init?.headers as Record<string, string>
|
|
225
|
-
expect(headers['X-Desktop-Token']).toBe('tok-abc')
|
|
226
|
-
expect(headers['X-Run-Id']).toBe('r-1')
|
|
227
|
-
expect(headers['Content-Type']).toBe('application/json')
|
|
228
|
-
const body = JSON.parse(String(init?.body)) as Record<string, unknown>
|
|
229
|
-
// 端点 extractCwd:顶层无 cwd,取 forwardedProps.cwd(fetchServerSentEvents 真实形态)
|
|
230
|
-
expect(body.cwd).toBeUndefined()
|
|
231
|
-
expect(body.forwardedProps).toEqual({ cwd: '/Users/x/proj' })
|
|
232
|
-
expect(body.threadId).toBe('t-1')
|
|
233
|
-
expect(body.runId).toBe('r-1')
|
|
234
|
-
// 端点 extractPrompt:messages 末条 user 消息文本(纯文本 content 为 string)
|
|
235
|
-
expect(body.messages).toEqual([expect.objectContaining({ role: 'user', content: '列出目录' })])
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
it('新壳:URL / X-Desktop-Token 取自 agentCredentials(appInfo 的 agentToken 恒 null 不参与)', async () => {
|
|
239
|
-
const fetchMock = stubSseFetch(RUN_EVENTS)
|
|
240
|
-
const bridge = newShellBridge({ port: 2468, token: 'tok-cred' }, { agentPort: 4567, agentToken: null })
|
|
241
|
-
bridgeRef.current = bridge
|
|
242
|
-
await drain(
|
|
243
|
-
createDesktopAgentConnection().connect([USER_MESSAGE], undefined, undefined, { threadId: 't-1', runId: 'r-1' }),
|
|
244
|
-
)
|
|
245
|
-
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
246
|
-
const [url, init] = fetchMock.mock.calls[0]!
|
|
247
|
-
expect(url).toBe('http://127.0.0.1:2468/prompt')
|
|
248
|
-
expect((init?.headers as Record<string, string>)['X-Desktop-Token']).toBe('tok-cred')
|
|
249
|
-
// 每次 connect 都重新按需取一次凭据(backend 重启换端口/令牌后下一问即用新值)
|
|
250
|
-
expect(bridge.agentCredentials).toHaveBeenCalledTimes(1)
|
|
251
|
-
expect(bridge.appInfo).not.toHaveBeenCalled()
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
it('新壳未装配(agentCredentials → null)→ 抛 DesktopAgentUnavailableError,不发 fetch', async () => {
|
|
255
|
-
const fetchMock = stubSseFetch(RUN_EVENTS)
|
|
256
|
-
bridgeRef.current = newShellBridge(null)
|
|
257
|
-
await expect(
|
|
258
|
-
drain(createDesktopAgentConnection().connect([USER_MESSAGE], undefined, undefined, { threadId: 't', runId: 'r' })),
|
|
259
|
-
).rejects.toBeInstanceOf(DesktopAgentUnavailableError)
|
|
260
|
-
expect(fetchMock).not.toHaveBeenCalled()
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
it('SSE 帧原样产出为 AG-UI chunk(RUN_STARTED 起、RUN_FINISHED 止)', async () => {
|
|
264
|
-
stubSseFetch(RUN_EVENTS)
|
|
265
|
-
bridgeRef.current = bridgeWith({ agentPort: 4567, agentToken: 'tok-abc' })
|
|
266
|
-
const chunks = (await drain(
|
|
267
|
-
createDesktopAgentConnection().connect([USER_MESSAGE], undefined, undefined, {
|
|
268
|
-
threadId: 't-1',
|
|
269
|
-
runId: 'r-1',
|
|
270
|
-
}),
|
|
271
|
-
)) as Array<{ type: string }>
|
|
272
|
-
expect(chunks.map((c) => c.type)).toEqual([
|
|
273
|
-
'RUN_STARTED',
|
|
274
|
-
'TEXT_MESSAGE_START',
|
|
275
|
-
'TEXT_MESSAGE_CONTENT',
|
|
276
|
-
'TEXT_MESSAGE_END',
|
|
277
|
-
'RUN_FINISHED',
|
|
278
|
-
])
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
it('旧壳:每次 connect 都重取 appInfo(backend 重启换端口/令牌后下一问即用新值)', async () => {
|
|
282
|
-
const fetchMock = stubSseFetch(RUN_EVENTS)
|
|
283
|
-
const appInfo = vi
|
|
284
|
-
.fn<() => Promise<AppInfo>>()
|
|
285
|
-
.mockResolvedValueOnce({ ...BASE_INFO, agentPort: 4567, agentToken: 'tok-old' })
|
|
286
|
-
.mockResolvedValueOnce({ ...BASE_INFO, agentPort: 5678, agentToken: 'tok-new' })
|
|
287
|
-
bridgeRef.current = { appInfo }
|
|
288
|
-
const connection = createDesktopAgentConnection()
|
|
289
|
-
const ctx = { threadId: 't-1', runId: 'r-1' }
|
|
290
|
-
await drain(connection.connect([USER_MESSAGE], undefined, undefined, ctx))
|
|
291
|
-
await drain(connection.connect([USER_MESSAGE], undefined, undefined, ctx))
|
|
292
|
-
expect(appInfo).toHaveBeenCalledTimes(2)
|
|
293
|
-
expect(fetchMock.mock.calls.map((c) => c[0])).toEqual([
|
|
294
|
-
'http://127.0.0.1:4567/prompt',
|
|
295
|
-
'http://127.0.0.1:5678/prompt',
|
|
296
|
-
])
|
|
297
|
-
expect((fetchMock.mock.calls[1]![1]?.headers as Record<string, string>)['X-Desktop-Token']).toBe('tok-new')
|
|
298
|
-
})
|
|
299
|
-
|
|
300
|
-
it('端点非 2xx(如 401 令牌失效)→ 错误携带 status 透传给 useChat error 态', async () => {
|
|
301
|
-
vi.stubGlobal(
|
|
302
|
-
'fetch',
|
|
303
|
-
vi.fn(async () => new Response('{"error":"bad token"}', { status: 401, statusText: 'Unauthorized' })),
|
|
304
|
-
)
|
|
305
|
-
bridgeRef.current = bridgeWith({ agentPort: 4567, agentToken: 'tok-abc' })
|
|
306
|
-
await expect(
|
|
307
|
-
drain(createDesktopAgentConnection().connect([USER_MESSAGE], undefined, undefined, { threadId: 't', runId: 'r' })),
|
|
308
|
-
).rejects.toMatchObject({ status: 401 })
|
|
309
|
-
})
|
|
310
|
-
})
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
-
// AllBlue - 理想之海
|
|
3
|
-
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
-
|
|
5
|
-
// desktop-agent 错误文案映射(桌面本地优先 P3-B5):useChat error 态 → 可读 i18n 文案。
|
|
6
|
-
// 错误来源三类:连接包装(DesktopAgentUnavailableError)、端点非 2xx(官方
|
|
7
|
-
// StreamHttpError 携带 status:401 令牌失效 / 403 目录未授权或 origin 被拒)、
|
|
8
|
-
// 网络层(StreamReadError:端口不通 / CORS 预检被拒),其余(RUN_ERROR 等)透出原文。
|
|
9
|
-
import { describe, expect, it } from 'vitest'
|
|
10
|
-
import { m } from '@/paraglide/messages.js'
|
|
11
|
-
import { DesktopAgentUnavailableError } from '../connection'
|
|
12
|
-
import { describeDesktopAgentError } from '../errors'
|
|
13
|
-
|
|
14
|
-
function withStatus(status: number): Error {
|
|
15
|
-
return Object.assign(new Error(`HTTP error! status: ${status}`), { name: 'StreamHttpError', status })
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe('describeDesktopAgentError', () => {
|
|
19
|
-
it('端点不可用 → 未就绪提示(实例与同名错误两种形态)', () => {
|
|
20
|
-
expect(describeDesktopAgentError(new DesktopAgentUnavailableError())).toBe(m.agent_desktop_errorUnavailable())
|
|
21
|
-
const renamed = Object.assign(new Error('x'), { name: 'DesktopAgentUnavailableError' })
|
|
22
|
-
expect(describeDesktopAgentError(renamed)).toBe(m.agent_desktop_errorUnavailable())
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
it('401 → 令牌失效提示;403 → 未授权/来源被拒提示', () => {
|
|
26
|
-
expect(describeDesktopAgentError(withStatus(401))).toBe(m.agent_desktop_errorUnauthorized())
|
|
27
|
-
expect(describeDesktopAgentError(withStatus(403))).toBe(m.agent_desktop_errorForbidden())
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('网络层失败(StreamReadError:端口不通 / CORS)→ 无法连接提示', () => {
|
|
31
|
-
const err = Object.assign(new Error('Failed to fetch'), { name: 'StreamReadError' })
|
|
32
|
-
expect(describeDesktopAgentError(err)).toBe(m.agent_desktop_errorConnection())
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('其余(RUN_ERROR、400、500 等)→ 运行失败 + 原文', () => {
|
|
36
|
-
expect(describeDesktopAgentError(new Error('模型配置缺失'))).toBe(
|
|
37
|
-
m.agent_desktop_errorGeneric({ message: '模型配置缺失' }),
|
|
38
|
-
)
|
|
39
|
-
expect(describeDesktopAgentError(withStatus(500))).toBe(
|
|
40
|
-
m.agent_desktop_errorGeneric({ message: 'HTTP error! status: 500' }),
|
|
41
|
-
)
|
|
42
|
-
})
|
|
43
|
-
})
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
-
// AllBlue - 理想之海
|
|
3
|
-
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
-
|
|
5
|
-
// 本地文件助手的 agent 连接(桌面本地优先 P3-B5):useChat 的 connection 适配。
|
|
6
|
-
//
|
|
7
|
-
// 端点 = Pro desktop 域 AgentService(B4):backend utilityProcess 内 127.0.0.1
|
|
8
|
-
// 随机端口 `POST /prompt`(AG-UI SSE),须带 `X-Desktop-Token`。端口与令牌经桌面桥
|
|
9
|
-
// 取得,两种壳形态:
|
|
10
|
-
// - 新壳:`desktopBridge.agentCredentials()` 按需 IPC 返回 { port, token }(令牌不再
|
|
11
|
-
// 随 appInfo 常驻广播——常驻信息面会让任意 renderer JS(含 XSS)随时拿到令牌去读
|
|
12
|
-
// 已授权目录;appInfo.agentToken 在新壳恒为 null 占位)。
|
|
13
|
-
// - 旧壳(无 agentCredentials):回落 `desktopBridge.appInfo()` 的 agentPort/agentToken。
|
|
14
|
-
// backend 重启即换新值(main 侧 backend exit 清空),所以**每次请求时懒取**,不在组件
|
|
15
|
-
// 顶层静态拼 URL。
|
|
16
|
-
//
|
|
17
|
-
// 为什么包一层而不是直接 fetchServerSentEvents(url, options):其 url 参数只接受
|
|
18
|
-
// 同步 getter,且在 connect() 内先于(可异步的)options 求值——URL 无法来自同一次
|
|
19
|
-
// 调用里 await 的桥调用。包装只做「await 桥 → 构造 fetchServerSentEvents →
|
|
20
|
-
// 原样委托 connect」,SSE 解析/AG-UI 协议/abort 全部是官方实现,无自定义协议面。
|
|
21
|
-
//
|
|
22
|
-
// 请求线格式(与 Pro 端点的跨仓契约,见 agent-service.ts extractCwd/extractPrompt):
|
|
23
|
-
// cwd 由 useChat `forwardedProps: { cwd }` 进 RunAgentInput.forwardedProps;端点
|
|
24
|
-
// 取 messages 末条 user 消息为 prompt;X-Run-Id 头由官方客户端自动附带(端点预检已放行)。
|
|
25
|
-
import { fetchServerSentEvents } from '@tanstack/ai-client'
|
|
26
|
-
import type { ConnectConnectionAdapter } from '@tanstack/ai-client'
|
|
27
|
-
import { getDesktopBridge } from '@/lib/desktop-bridge-types'
|
|
28
|
-
|
|
29
|
-
export interface DesktopAgentEndpoint {
|
|
30
|
-
/** `http://127.0.0.1:{agentPort}/prompt` */
|
|
31
|
-
url: string
|
|
32
|
-
/** X-Desktop-Token 访问令牌(AgentService 每次启动随机生成) */
|
|
33
|
-
token: string
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** agent 端点不可用:无桥(云 Web / OSS)、旧壳(appInfo 无 agent 字段)、新壳未装配
|
|
37
|
-
* (agentCredentials 返回 null)、或 DESKTOP_AGENT 门控未开 / bundle 装配失败 */
|
|
38
|
-
export class DesktopAgentUnavailableError extends Error {
|
|
39
|
-
constructor() {
|
|
40
|
-
super('desktop agent endpoint unavailable')
|
|
41
|
-
this.name = 'DesktopAgentUnavailableError'
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** 端口合法性(1..65535 整数):非法即「不可用」,不拼出坏 URL */
|
|
46
|
-
function isValidAgentPort(port: unknown): port is number {
|
|
47
|
-
return typeof port === 'number' && Number.isInteger(port) && port >= 1 && port <= 65535
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** 由端口 + 令牌构造端点;任一非法返回 null(不吞半截凭据)。令牌空串按不可用处理
|
|
51
|
-
* ——新壳 appInfo.agentToken 恒 null,回落路径若拿到空串说明壳未装配 */
|
|
52
|
-
function buildEndpoint(port: unknown, token: unknown): DesktopAgentEndpoint | null {
|
|
53
|
-
if (!isValidAgentPort(port)) return null
|
|
54
|
-
if (typeof token !== 'string' || token === '') return null
|
|
55
|
-
return { url: `http://127.0.0.1:${port}/prompt`, token }
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** 调用时读取当前 agent 端点;不可用返回 null(端口/令牌非法同样视为不可用,不拼坏 URL)。
|
|
59
|
-
* 取端点优先 agentCredentials(新壳按需取凭证),旧壳回落 appInfo 的 agentPort/agentToken。 */
|
|
60
|
-
export async function resolveDesktopAgentEndpoint(): Promise<DesktopAgentEndpoint | null> {
|
|
61
|
-
const bridge = getDesktopBridge()
|
|
62
|
-
if (!bridge) return null
|
|
63
|
-
// 新壳独占使用 agentCredentials:其存在即表示令牌不再经 appInfo 广播,回落拿到
|
|
64
|
-
// 的只会是恒 null 占位,无收益。IPC 断开/main 未就绪时抛错与 appInfo 同口径归
|
|
65
|
-
// 「服务未就绪」(不可用),不把原生错误原文透出成「运行失败:<原文>」
|
|
66
|
-
if (typeof bridge.agentCredentials === 'function') {
|
|
67
|
-
const credentials = await bridge.agentCredentials().catch(() => null)
|
|
68
|
-
if (!credentials) return null
|
|
69
|
-
return buildEndpoint(credentials.port, credentials.token)
|
|
70
|
-
}
|
|
71
|
-
// 旧壳:端口/令牌同随 appInfo 广播
|
|
72
|
-
const info = await bridge.appInfo().catch(() => null)
|
|
73
|
-
if (!info) return null
|
|
74
|
-
return buildEndpoint(info.agentPort, info.agentToken)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* useChat 的 connection:每次 connect 懒取端点后委托官方 fetchServerSentEvents。
|
|
79
|
-
* `resolveEndpoint` 可注入(测试 / 未来多端点),缺省读 desktopBridge.appInfo。
|
|
80
|
-
*/
|
|
81
|
-
export function createDesktopAgentConnection(
|
|
82
|
-
resolveEndpoint: () => Promise<DesktopAgentEndpoint | null> = resolveDesktopAgentEndpoint,
|
|
83
|
-
): ConnectConnectionAdapter {
|
|
84
|
-
return {
|
|
85
|
-
async *connect(messages, data, abortSignal, runContext) {
|
|
86
|
-
const endpoint = await resolveEndpoint()
|
|
87
|
-
if (!endpoint) throw new DesktopAgentUnavailableError()
|
|
88
|
-
yield* fetchServerSentEvents(endpoint.url, {
|
|
89
|
-
headers: { 'X-Desktop-Token': endpoint.token },
|
|
90
|
-
}).connect(messages, data, abortSignal, runContext)
|
|
91
|
-
},
|
|
92
|
-
}
|
|
93
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
-
// AllBlue - 理想之海
|
|
3
|
-
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
-
|
|
5
|
-
// 本地文件助手错误文案映射(桌面本地优先 P3-B5):把 useChat 的 error 态翻译成
|
|
6
|
-
// 用户可读的 i18n 文案。三类可归因错误:
|
|
7
|
-
// - 端点不可用(DesktopAgentUnavailableError):无桥 / 旧壳 / agent 未装配
|
|
8
|
-
// - 端点非 2xx(@tanstack/ai-client StreamHttpError,带 status):401 令牌失效
|
|
9
|
-
// (backend 重启换了令牌,下一问懒取即恢复)、403 目录未授权或 origin 被拒
|
|
10
|
-
// - 网络层(StreamReadError):端口不通、CORS 预检被拒(浏览器只给 TypeError)
|
|
11
|
-
// 其余(RUN_ERROR 的 agent 失败原因、400/5xx 等)原文透出,便于排障。
|
|
12
|
-
import { m } from '@/paraglide/messages.js'
|
|
13
|
-
|
|
14
|
-
export function describeDesktopAgentError(error: Error): string {
|
|
15
|
-
if (error.name === 'DesktopAgentUnavailableError') return m.agent_desktop_errorUnavailable()
|
|
16
|
-
const status = (error as { status?: unknown }).status
|
|
17
|
-
if (status === 401) return m.agent_desktop_errorUnauthorized()
|
|
18
|
-
if (status === 403) return m.agent_desktop_errorForbidden()
|
|
19
|
-
if (error.name === 'StreamReadError') return m.agent_desktop_errorConnection()
|
|
20
|
-
return m.agent_desktop_errorGeneric({ message: error.message || String(error) })
|
|
21
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/* eslint-disable */
|
|
2
|
-
import { getLocale, experimentalStaticLocale } from '../runtime.js';
|
|
3
|
-
|
|
4
|
-
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
|
|
5
|
-
|
|
6
|
-
/** @typedef {{}} Agent_Desktop_TitleInputs */
|
|
7
|
-
|
|
8
|
-
const zh_agent_desktop_title = /** @type {(inputs: Agent_Desktop_TitleInputs) => LocalizedString} */ () => {
|
|
9
|
-
return /** @type {LocalizedString} */ (`本地文件助手`)
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
const en_agent_desktop_title = /** @type {(inputs: Agent_Desktop_TitleInputs) => LocalizedString} */ () => {
|
|
13
|
-
return /** @type {LocalizedString} */ (`Local File Assistant`)
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* | output |
|
|
18
|
-
* | --- |
|
|
19
|
-
* | "Local File Assistant" |
|
|
20
|
-
*
|
|
21
|
-
* @param {Agent_Desktop_TitleInputs} inputs
|
|
22
|
-
* @param {{ locale?: "zh" | "en" }} options
|
|
23
|
-
* @returns {LocalizedString}
|
|
24
|
-
*/
|
|
25
|
-
export const agent_desktop_title = /** @type {((inputs?: Agent_Desktop_TitleInputs, options?: { locale?: "zh" | "en" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Agent_Desktop_TitleInputs, { locale?: "zh" | "en" }, {}>} */ ((inputs = {}, options = {}) => {
|
|
26
|
-
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
|
|
27
|
-
if (locale === "en") return en_agent_desktop_title(inputs)
|
|
28
|
-
return zh_agent_desktop_title(inputs)
|
|
29
|
-
});
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export { common_hotkey_desc_agentabort1 as "common_hotkey_desc_agentAbort" };
|
|
2
|
-
export type LocalizedString = import("../runtime.js").LocalizedString;
|
|
3
|
-
export type Common_Hotkey_Desc_Agentabort1Inputs = {};
|
|
4
|
-
/**
|
|
5
|
-
* | output |
|
|
6
|
-
* | --- |
|
|
7
|
-
* | "Stop the local file assistant" |
|
|
8
|
-
*
|
|
9
|
-
* @param {Common_Hotkey_Desc_Agentabort1Inputs} inputs
|
|
10
|
-
* @param {{ locale?: "zh" | "en" }} options
|
|
11
|
-
* @returns {LocalizedString}
|
|
12
|
-
*/
|
|
13
|
-
declare const common_hotkey_desc_agentabort1: ((inputs?: Common_Hotkey_Desc_Agentabort1Inputs, options?: {
|
|
14
|
-
locale?: "zh" | "en";
|
|
15
|
-
}) => LocalizedString) & import("../runtime.js").MessageMetadata<Common_Hotkey_Desc_Agentabort1Inputs, {
|
|
16
|
-
locale?: "zh" | "en";
|
|
17
|
-
}, {}>;
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/* eslint-disable */
|
|
2
|
-
import { getLocale, experimentalStaticLocale } from '../runtime.js';
|
|
3
|
-
|
|
4
|
-
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
|
|
5
|
-
|
|
6
|
-
/** @typedef {{}} Common_Hotkey_Desc_Agentabort1Inputs */
|
|
7
|
-
|
|
8
|
-
const zh_common_hotkey_desc_agentabort1 = /** @type {(inputs: Common_Hotkey_Desc_Agentabort1Inputs) => LocalizedString} */ () => {
|
|
9
|
-
return /** @type {LocalizedString} */ (`中止本地文件助手运行`)
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
const en_common_hotkey_desc_agentabort1 = /** @type {(inputs: Common_Hotkey_Desc_Agentabort1Inputs) => LocalizedString} */ () => {
|
|
13
|
-
return /** @type {LocalizedString} */ (`Stop the local file assistant`)
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* | output |
|
|
18
|
-
* | --- |
|
|
19
|
-
* | "Stop the local file assistant" |
|
|
20
|
-
*
|
|
21
|
-
* @param {Common_Hotkey_Desc_Agentabort1Inputs} inputs
|
|
22
|
-
* @param {{ locale?: "zh" | "en" }} options
|
|
23
|
-
* @returns {LocalizedString}
|
|
24
|
-
*/
|
|
25
|
-
const common_hotkey_desc_agentabort1 = /** @type {((inputs?: Common_Hotkey_Desc_Agentabort1Inputs, options?: { locale?: "zh" | "en" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Common_Hotkey_Desc_Agentabort1Inputs, { locale?: "zh" | "en" }, {}>} */ ((inputs = {}, options = {}) => {
|
|
26
|
-
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
|
|
27
|
-
if (locale === "en") return en_common_hotkey_desc_agentabort1(inputs)
|
|
28
|
-
return zh_common_hotkey_desc_agentabort1(inputs)
|
|
29
|
-
});
|
|
30
|
-
export { common_hotkey_desc_agentabort1 as "common_hotkey_desc_agentAbort" }
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
-
// AllBlue - 理想之海
|
|
3
|
-
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
-
|
|
5
|
-
// 本地文件助手页(/_nav/agent)路由门控(桌面本地优先 P3-B5):依赖桌面壳
|
|
6
|
-
// AgentService,云 Web / OSS 构建无此页——beforeLoad 以构建期常量
|
|
7
|
-
// isDesktopEdition(VITE_DEPLOY_TARGET)判定,非桌面构建直接 throw notFound
|
|
8
|
-
// (SSR 即 404,无占位闪烁)。运行时的桥探测(useDesktopAgentPresence)另由
|
|
9
|
-
// lib/desktop-agent/__tests__/use-desktop-agent-presence.test.tsx 覆盖。
|
|
10
|
-
// 断言方式同 -edition-routing.test.tsx:直接调 Route.options.beforeLoad。
|
|
11
|
-
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
12
|
-
|
|
13
|
-
async function importAgentRoute(desktop: boolean) {
|
|
14
|
-
vi.resetModules()
|
|
15
|
-
vi.doMock('@/lib/edition-desktop', () => ({ isDesktopEdition: () => desktop }))
|
|
16
|
-
return import('../_nav/agent/route')
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** beforeLoad 抛出的 notFound 对象(@tanstack/router-core:{ isNotFound: true }) */
|
|
20
|
-
function catchNotFound(fn: () => unknown): { isNotFound?: boolean } | null {
|
|
21
|
-
try {
|
|
22
|
-
fn()
|
|
23
|
-
return null
|
|
24
|
-
} catch (error) {
|
|
25
|
-
return error as { isNotFound?: boolean }
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
afterEach(() => {
|
|
30
|
-
vi.doUnmock('@/lib/edition-desktop')
|
|
31
|
-
vi.resetModules()
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
describe('/_nav/agent beforeLoad 门控', () => {
|
|
35
|
-
it('非桌面构建(云 Web / OSS):throw notFound(页面不存在,SSR 即 404)', async () => {
|
|
36
|
-
const { Route } = await importAgentRoute(false)
|
|
37
|
-
const result = catchNotFound(() => Route.options.beforeLoad!({} as never))
|
|
38
|
-
expect(result?.isNotFound).toBe(true)
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('桌面构建:放行(运行时无桥由 lazy 组件的 useDesktopAgentPresence 落到 NotFound)', async () => {
|
|
42
|
-
const { Route } = await importAgentRoute(true)
|
|
43
|
-
expect(Route.options.beforeLoad!({} as never)).toBeUndefined()
|
|
44
|
-
})
|
|
45
|
-
})
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
-
// AllBlue - 理想之海
|
|
3
|
-
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
-
|
|
5
|
-
// 本地文件助手页(桌面本地优先 P3-B5):授权目录卡 + agent 对话区。门控说明见
|
|
6
|
-
// 同目录 route.tsx;本文件承担运行时桥探测(mounted 模式:SSR / hydration 首帧
|
|
7
|
-
// 渲染空白、不读 window,挂载后 Electron 恢复页面,无桥即 NotFound)。
|
|
8
|
-
//
|
|
9
|
-
// 双层判定:presence(含 agentPort 装配,见 lib/desktop-agent/availability)决定
|
|
10
|
-
// 页面是否可达;isAgentWorkspaceSupported(B1 三方法)决定目录授权是否可用——
|
|
11
|
-
// 旧壳缺三方法时整页只留升级提示,不渲染对话区(否则对话区「请先添加授权目录」
|
|
12
|
-
// 与卡片「请升级客户端」互相打脸)。
|
|
13
|
-
import { useState } from 'react'
|
|
14
|
-
import { createLazyFileRoute } from '@tanstack/react-router'
|
|
15
|
-
import { useQuery } from '@tanstack/react-query'
|
|
16
|
-
import { AgentChat } from '@/components/desktop-agent/agent-chat'
|
|
17
|
-
import { AgentWorkspaceCard } from '@/components/desktop-agent/agent-workspace-card'
|
|
18
|
-
import { isAgentWorkspaceSupported } from '@/lib/desktop-agent/availability'
|
|
19
|
-
import { useDesktopAgentPresence } from '@/lib/desktop-agent/use-desktop-agent-presence'
|
|
20
|
-
import { desktopAgentWorkspacesQueryOptions } from '@/lib/queries/desktop-agent'
|
|
21
|
-
import { m } from '@/paraglide/messages.js'
|
|
22
|
-
import { NavNotFound } from '../-components/nav-not-found'
|
|
23
|
-
|
|
24
|
-
export const Route = createLazyFileRoute('/_nav/agent')({
|
|
25
|
-
component: DesktopAgentRoute,
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
function DesktopAgentRoute() {
|
|
29
|
-
const presence = useDesktopAgentPresence()
|
|
30
|
-
if (presence === 'pending') return null
|
|
31
|
-
if (presence === 'absent') return <NavNotFound />
|
|
32
|
-
return <DesktopAgentPage />
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function DesktopAgentPage() {
|
|
36
|
-
const supportsWorkspaces = isAgentWorkspaceSupported()
|
|
37
|
-
// enabled 与卡片同口径:不支持目录授权时不查清单,避免无效 IPC
|
|
38
|
-
const { data: workspaces = [] } = useQuery({
|
|
39
|
-
...desktopAgentWorkspacesQueryOptions(),
|
|
40
|
-
enabled: supportsWorkspaces,
|
|
41
|
-
})
|
|
42
|
-
const [picked, setPicked] = useState<string | null>(null)
|
|
43
|
-
const [running, setRunning] = useState(false)
|
|
44
|
-
// 未选择 / 选中项已被撤销 → 回落清单首项(清单为空则 null,对话区禁用输入)
|
|
45
|
-
const cwd = picked !== null && workspaces.includes(picked) ? picked : (workspaces[0] ?? null)
|
|
46
|
-
|
|
47
|
-
return (
|
|
48
|
-
<div className="flex h-full min-h-0 flex-col gap-4 overflow-hidden p-6">
|
|
49
|
-
<header className="space-y-1">
|
|
50
|
-
<h1 className="text-lg font-semibold">{m.agent_desktop_title()}</h1>
|
|
51
|
-
<p className="text-sm text-muted-foreground">{m.agent_desktop_pageDescription()}</p>
|
|
52
|
-
</header>
|
|
53
|
-
<div className="flex min-h-0 flex-1 flex-col gap-4 lg:flex-row">
|
|
54
|
-
<div className="shrink-0 lg:w-80 lg:overflow-y-auto">
|
|
55
|
-
<AgentWorkspaceCard selected={cwd} onSelect={setPicked} disabled={running} />
|
|
56
|
-
</div>
|
|
57
|
-
{/* 旧壳(无目录授权 IPC)只留卡片里的升级提示;否则每个目录一份会话
|
|
58
|
-
(换目录即新会话,运行中由 disabled 锁定切换) */}
|
|
59
|
-
{supportsWorkspaces && <AgentChat key={cwd ?? 'none'} cwd={cwd} onRunningChange={setRunning} />}
|
|
60
|
-
</div>
|
|
61
|
-
</div>
|
|
62
|
-
)
|
|
63
|
-
}
|