@shawnstack/quickforge 1.7.5 → 1.7.7
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/dist/assets/AgentProfilesPage-BfA_sQ3A.js +1 -0
- package/dist/assets/ChatPanelHost-Dul1ahKj.js +307 -0
- package/dist/assets/CloudAccountSettingsPage-qZh0lTUN.js +1 -0
- package/dist/assets/PluginsPage-CPE5tbbf.js +1 -0
- package/dist/assets/ScheduledTasksPage-PTxL_PPg.js +2 -0
- package/dist/assets/SettingsWorkspacePage-BE2T4d3P.js +1696 -0
- package/dist/assets/ShareLinksSettingsPage-Bl8fP62X.js +1 -0
- package/dist/assets/SharedConversationPage-BK9XJnEe.js +1 -0
- package/dist/assets/TerminalDock-CL7HU9DI.js +2 -0
- package/dist/assets/{WorkspaceInspector-D9kdnLDd.js → WorkspaceInspector-MNBLIKMn.js} +5 -5
- package/dist/assets/{dist-DLKC_odd.js → dist-yDHbi5ux.js} +1 -1
- package/dist/assets/{esm-Df4edDpj.js → esm-sdsK6gaB.js} +2 -2
- package/dist/assets/{icons-CXPP1fFT.js → icons-2D18YbSI.js} +1 -1
- package/dist/assets/index-BTPzyq7d.js +65 -0
- package/dist/assets/index-BiCTgUlY.css +3 -0
- package/dist/assets/{mcp-servers-dialog-BUSPvaqI.js → mcp-servers-dialog-GY7sdfIr.js} +2 -2
- package/dist/assets/{logger-8REuLPGK.js → model-display-label-C6-0eDxF.js} +1 -1
- package/dist/assets/{monaco-D6OhXgjg.js → monaco-DvXErxp2.js} +1 -1
- package/dist/assets/{react-vendor-1KC5PyRx.js → react-vendor-3UaA-C9W.js} +1 -1
- package/dist/assets/{skills-dialog-BThnIUBs.js → skills-dialog-C1fJ1Jl7.js} +3 -3
- package/dist/assets/{web-DoV8MSMn.js → web-B5qgZJRv.js} +1 -1
- package/dist/index.html +8 -8
- package/dist/sw.js +17 -0
- package/package.json +2 -1
- package/runtime-assets/agent/darwin-arm64/qf-agent +0 -0
- package/runtime-assets/agent/darwin-x64/qf-agent +0 -0
- package/runtime-assets/agent/linux-arm64/qf-agent +0 -0
- package/runtime-assets/agent/linux-x64/qf-agent +0 -0
- package/runtime-assets/agent/win32-x64/qf-agent.exe +0 -0
- package/server/access-policy.mjs +11 -0
- package/server/agent-manager.mjs +349 -48
- package/server/agent-profiles.mjs +27 -8
- package/server/auto-compaction.mjs +78 -9
- package/server/cloud/client.mjs +4 -7
- package/server/cloud/config.mjs +9 -16
- package/server/cloud/credential-store.mjs +10 -3
- package/server/cloud/identity.mjs +17 -60
- package/server/cloud/models.mjs +1 -1
- package/server/cloud/qf-agent-process.mjs +571 -0
- package/server/cloud/runtime.mjs +11 -5
- package/server/cloud/service-config.mjs +8 -4
- package/server/context-usage.mjs +11 -4
- package/server/index.mjs +96 -37
- package/server/mcp/registry.mjs +8 -7
- package/server/message-converters.mjs +40 -19
- package/server/model-catalog.mjs +11 -4
- package/server/opencode-acp-agent.mjs +1151 -0
- package/server/public-api.mjs +11 -5
- package/server/routes/agent-profiles.mjs +6 -3
- package/server/routes/agent.mjs +43 -3
- package/server/routes/backup.mjs +5 -4
- package/server/routes/cloud.mjs +54 -35
- package/server/routes/shares.mjs +7 -8
- package/server/routes/storage.mjs +9 -24
- package/server/routes/system.mjs +7 -4
- package/server/utils/process-tree.mjs +78 -0
- package/dist/assets/AgentProfilesPage-XNjuqJs7.js +0 -1
- package/dist/assets/ChatPanelHost-CuF-PZpc.js +0 -292
- package/dist/assets/CloudAccountSettingsPage-GJSoi6sO.js +0 -1
- package/dist/assets/PluginsPage-BQgp0qN-.js +0 -1
- package/dist/assets/ScheduledTasksPage-DdekATzq.js +0 -2
- package/dist/assets/SettingsWorkspacePage-Cb54Ic91.js +0 -1639
- package/dist/assets/ShareLinksSettingsPage-C75WJpaF.js +0 -1
- package/dist/assets/SharedConversationPage-CjMtSQTW.js +0 -1
- package/dist/assets/TerminalDock-D4zJBJc4.js +0 -2
- package/dist/assets/index-_ZhK6bvp.js +0 -65
- package/dist/assets/index-iVV2bP_f.css +0 -3
package/server/index.mjs
CHANGED
|
@@ -35,6 +35,8 @@ import { handleTerminalApi, handleTerminalUpgrade } from './routes/terminal.mjs'
|
|
|
35
35
|
import { handleChannelsApi } from './routes/channels.mjs'
|
|
36
36
|
import { handleModelsApi } from './routes/models.mjs'
|
|
37
37
|
import { handleCloudApi } from './routes/cloud.mjs'
|
|
38
|
+
import { readCloudServiceConfig } from './cloud/service-config.mjs'
|
|
39
|
+
import { startQfAgent, stopQfAgent, getQfAgentStatus } from './cloud/qf-agent-process.mjs'
|
|
38
40
|
import { serveStatic } from './routes/static.mjs'
|
|
39
41
|
import { logger, flushLogger } from './utils/logger.mjs'
|
|
40
42
|
import { getPackageInfo, checkForUpdates, checkDesktopRelease } from './utils/package-update.mjs'
|
|
@@ -47,6 +49,7 @@ import { initializeChannels, shutdownChannels } from './channels/registry.mjs'
|
|
|
47
49
|
import { shutdownMcpConnections } from './mcp/registry.mjs'
|
|
48
50
|
import { shutdownTerminalSessions } from './terminal/terminal-manager.mjs'
|
|
49
51
|
import { createNodeProcessEnv } from './utils/process-env.mjs'
|
|
52
|
+
import { isAuthenticatedAppClient } from './access-policy.mjs'
|
|
50
53
|
|
|
51
54
|
const __filename = fileURLToPath(import.meta.url)
|
|
52
55
|
const __dirname = path.dirname(__filename)
|
|
@@ -67,6 +70,10 @@ const port = Number(process.env.QUICKFORGE_PORT || (isDev ? 32176 : 5176))
|
|
|
67
70
|
const vitePort = Number(process.env.QUICKFORGE_VITE_PORT || 5176)
|
|
68
71
|
let restartInProgress = false
|
|
69
72
|
let updateInProgress = false
|
|
73
|
+
let shutdownPromise = null
|
|
74
|
+
let shutdownStarted = false
|
|
75
|
+
let qfAgentStartPromise = null
|
|
76
|
+
let boundServerPort = null
|
|
70
77
|
|
|
71
78
|
setDefaultWorkspaceRoot(process.env.QUICKFORGE_WORKSPACE_DIR || path.join(dataDir, 'workspace'))
|
|
72
79
|
|
|
@@ -74,11 +81,12 @@ function getRestartSupport() {
|
|
|
74
81
|
return { supported: true, reason: null }
|
|
75
82
|
}
|
|
76
83
|
|
|
77
|
-
async function getSystemStatus({ isLocalRequest = true } = {}) {
|
|
84
|
+
async function getSystemStatus({ isLocalRequest = true, remoteAuthorized = false } = {}) {
|
|
78
85
|
const config = await readProjectConfig()
|
|
79
86
|
const restartSupport = getRestartSupport()
|
|
80
87
|
const packageInfo = await getPackageInfo(projectRoot)
|
|
81
|
-
const
|
|
88
|
+
const restartAllowed = isAuthenticatedAppClient({ isLocalRequest, remoteAuthorized })
|
|
89
|
+
const restartSupported = restartAllowed && restartSupport.supported
|
|
82
90
|
return {
|
|
83
91
|
ok: true,
|
|
84
92
|
mode: isDev ? 'development' : 'production',
|
|
@@ -94,7 +102,7 @@ async function getSystemStatus({ isLocalRequest = true } = {}) {
|
|
|
94
102
|
terminal: isLocalRequest,
|
|
95
103
|
},
|
|
96
104
|
restartSupported,
|
|
97
|
-
restartUnsupportedReason:
|
|
105
|
+
restartUnsupportedReason: restartAllowed ? restartSupport.reason : 'Restart requires an authenticated client',
|
|
98
106
|
dataDir,
|
|
99
107
|
configDir,
|
|
100
108
|
storageDir,
|
|
@@ -151,11 +159,9 @@ function closeHttpServer() {
|
|
|
151
159
|
})
|
|
152
160
|
}
|
|
153
161
|
|
|
154
|
-
async function
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
logger.info(`Restart supervisor started (PID ${supervisorPid}).`)
|
|
158
|
-
|
|
162
|
+
async function shutdownRuntime() {
|
|
163
|
+
shutdownStarted = true
|
|
164
|
+
await stopQfAgent()
|
|
159
165
|
stopScheduledTaskRunner()
|
|
160
166
|
stopAutoArchiveRunner()
|
|
161
167
|
stopVite()
|
|
@@ -164,6 +170,19 @@ async function performRestart() {
|
|
|
164
170
|
await shutdownChannels()
|
|
165
171
|
shutdownTerminalSessions()
|
|
166
172
|
await closeHttpServer()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function stopQuickForgeServer() {
|
|
176
|
+
if (!shutdownPromise) shutdownPromise = shutdownRuntime()
|
|
177
|
+
return shutdownPromise
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function performRestart() {
|
|
181
|
+
logger.info('Restart requested from settings UI.')
|
|
182
|
+
const supervisorPid = await spawnRestartSupervisor()
|
|
183
|
+
logger.info(`Restart supervisor started (PID ${supervisorPid}).`)
|
|
184
|
+
|
|
185
|
+
await stopQuickForgeServer()
|
|
167
186
|
process.exit(0)
|
|
168
187
|
}
|
|
169
188
|
|
|
@@ -230,14 +249,7 @@ function spawnUpdateSupervisor(update) {
|
|
|
230
249
|
|
|
231
250
|
async function shutdownForUpdate() {
|
|
232
251
|
logger.info('Shutting down QuickForge for external updater.')
|
|
233
|
-
|
|
234
|
-
stopAutoArchiveRunner()
|
|
235
|
-
stopVite()
|
|
236
|
-
await shutdownAgentManager()
|
|
237
|
-
await shutdownMcpConnections()
|
|
238
|
-
await shutdownChannels()
|
|
239
|
-
shutdownTerminalSessions()
|
|
240
|
-
await closeHttpServer()
|
|
252
|
+
await stopQuickForgeServer()
|
|
241
253
|
flushLogger()
|
|
242
254
|
process.exit(0)
|
|
243
255
|
}
|
|
@@ -284,6 +296,22 @@ async function updateQuickForge() {
|
|
|
284
296
|
}
|
|
285
297
|
}
|
|
286
298
|
|
|
299
|
+
async function applyCloudServiceConfig(cloudConfig, { urlChanged = false } = {}) {
|
|
300
|
+
if (shutdownStarted) return null
|
|
301
|
+
if (!cloudConfig.enabled || !cloudConfig.valid) {
|
|
302
|
+
if (!cloudConfig.valid) logger.warn(`QuickForge remote agent was not started: ${cloudConfig.configurationError || 'invalid Cloud configuration'}`)
|
|
303
|
+
return stopQfAgent({ disabled: true })
|
|
304
|
+
}
|
|
305
|
+
if (!boundServerPort) return null
|
|
306
|
+
const agentStatus = getQfAgentStatus()
|
|
307
|
+
if (urlChanged || agentStatus.status === 'disabled' || agentStatus.enabled === false) await stopQfAgent()
|
|
308
|
+
return startQfAgent({
|
|
309
|
+
serverUrl: `http://127.0.0.1:${boundServerPort}/`,
|
|
310
|
+
ownerPid: process.pid,
|
|
311
|
+
cloudUrl: cloudConfig.cloudUrl,
|
|
312
|
+
})
|
|
313
|
+
}
|
|
314
|
+
|
|
287
315
|
// --- Route dispatching ---
|
|
288
316
|
async function handleApi(req, res, url, requestContext = {}) {
|
|
289
317
|
const pathname = url.pathname
|
|
@@ -348,6 +376,7 @@ async function handleApi(req, res, url, requestContext = {}) {
|
|
|
348
376
|
isLocalRequest: requestContext.isLocalRequest === true,
|
|
349
377
|
remoteAddress: requestContext.remoteAddress,
|
|
350
378
|
remoteAuthorized: requestContext.remoteAuthorized === true,
|
|
379
|
+
onCloudServiceConfigChanged: applyCloudServiceConfig,
|
|
351
380
|
})
|
|
352
381
|
return
|
|
353
382
|
}
|
|
@@ -449,6 +478,7 @@ async function handleApi(req, res, url, requestContext = {}) {
|
|
|
449
478
|
checkDesktopRelease: () => checkDesktopRelease(projectRoot),
|
|
450
479
|
updateQuickForge,
|
|
451
480
|
isLocalRequest: requestContext.isLocalRequest === true,
|
|
481
|
+
remoteAuthorized: requestContext.remoteAuthorized === true,
|
|
452
482
|
getTerminalShellSetting: readTerminalShellSetting,
|
|
453
483
|
updateTerminalShellSetting,
|
|
454
484
|
getTerminalShellConfig: readTerminalShellConfig,
|
|
@@ -544,6 +574,24 @@ function isAllowedHostHeader(value) {
|
|
|
544
574
|
return allowedHosts.has(parsed.hostname) && hostPort === expectedPort
|
|
545
575
|
}
|
|
546
576
|
|
|
577
|
+
const trustedTunnelSocket = Symbol('trustedTunnelSocket')
|
|
578
|
+
|
|
579
|
+
function isTunnelClientRequest(req) {
|
|
580
|
+
if (
|
|
581
|
+
isLoopbackAddress(req.socket.remoteAddress)
|
|
582
|
+
&& req.headers['x-quickforge-tunnel'] === '1'
|
|
583
|
+
&& req.headers.host === '127.0.0.1:18080'
|
|
584
|
+
) {
|
|
585
|
+
req.socket[trustedTunnelSocket] = true
|
|
586
|
+
}
|
|
587
|
+
return req.socket[trustedTunnelSocket] === true
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function isAllowedRequestHost(req, isTunnelClient) {
|
|
591
|
+
return isAllowedHostHeader(req.headers.host)
|
|
592
|
+
|| (isTunnelClient && req.headers.host === '127.0.0.1:18080')
|
|
593
|
+
}
|
|
594
|
+
|
|
547
595
|
function isLanAccessBootstrapPath(pathname) {
|
|
548
596
|
return pathname === '/api/health'
|
|
549
597
|
|| pathname === '/api/lan-access/status'
|
|
@@ -583,7 +631,11 @@ const server = createServer(async (req, res) => {
|
|
|
583
631
|
reqLogger.info(`${req.method} ${req.url} ${res.statusCode}`, { method: req.method, url: req.url, status: res.statusCode, durationMs })
|
|
584
632
|
})
|
|
585
633
|
|
|
586
|
-
|
|
634
|
+
const remoteAddress = req.socket.remoteAddress
|
|
635
|
+
// 云远程访问(RemoteTunnel):agent 与 qf 同机时流量经 127.0.0.1 回环进入并携带
|
|
636
|
+
// X-QuickForge-Tunnel: 1。仅该可信隧道允许手机本地入口 127.0.0.1:18080 作为 Host。
|
|
637
|
+
const isTunnelClient = isTunnelClientRequest(req)
|
|
638
|
+
if (!isAllowedRequestHost(req, isTunnelClient)) {
|
|
587
639
|
res.writeHead(403, { 'content-type': 'application/json; charset=utf-8' })
|
|
588
640
|
res.end(JSON.stringify({ error: 'Forbidden host' }))
|
|
589
641
|
return
|
|
@@ -605,11 +657,11 @@ const server = createServer(async (req, res) => {
|
|
|
605
657
|
}
|
|
606
658
|
try {
|
|
607
659
|
const url = new URL(req.url || '/', `http://${req.headers.host || `${host}:${port}`}`)
|
|
608
|
-
|
|
609
|
-
const isRemoteRequest = !isLoopbackAddress(remoteAddress)
|
|
610
|
-
const remoteAuthorized = isRemoteRequest ? await isAuthorizedRemoteRequest(req) : true
|
|
660
|
+
// 隧道请求视为“已认证远程客户端”:认证通过,但本地能力按远程请求裁剪。
|
|
661
|
+
const isRemoteRequest = isTunnelClient || !isLoopbackAddress(remoteAddress)
|
|
662
|
+
const remoteAuthorized = isRemoteRequest ? (isTunnelClient ? true : await isAuthorizedRemoteRequest(req)) : true
|
|
611
663
|
|
|
612
|
-
if (isRemoteRequest &&
|
|
664
|
+
if (isRemoteRequest && !remoteAuthorized && !isLanAccessBootstrapPath(url.pathname) && !isSharePath(url.pathname) && !isStaticAssetPath(url.pathname)) {
|
|
613
665
|
if (url.pathname.startsWith('/api/')) {
|
|
614
666
|
sendLanAuthRequired(res)
|
|
615
667
|
} else {
|
|
@@ -621,7 +673,6 @@ const server = createServer(async (req, res) => {
|
|
|
621
673
|
if (
|
|
622
674
|
url.pathname.startsWith('/api/') &&
|
|
623
675
|
isRemoteRequest &&
|
|
624
|
-
shareLanEnabled &&
|
|
625
676
|
!remoteAuthorized &&
|
|
626
677
|
!(url.pathname.startsWith('/api/shared/') || url.pathname === '/api/health' || url.pathname.startsWith('/api/lan-access/'))
|
|
627
678
|
) {
|
|
@@ -635,6 +686,7 @@ const server = createServer(async (req, res) => {
|
|
|
635
686
|
isLocalRequest: !isRemoteRequest,
|
|
636
687
|
remoteAddress,
|
|
637
688
|
remoteAuthorized,
|
|
689
|
+
tunnelClient: isTunnelClient,
|
|
638
690
|
})
|
|
639
691
|
return
|
|
640
692
|
}
|
|
@@ -649,7 +701,7 @@ const server = createServer(async (req, res) => {
|
|
|
649
701
|
return
|
|
650
702
|
}
|
|
651
703
|
|
|
652
|
-
if (isRemoteRequest &&
|
|
704
|
+
if (isRemoteRequest && !remoteAuthorized) {
|
|
653
705
|
res.writeHead(403, { 'content-type': 'application/json; charset=utf-8' })
|
|
654
706
|
res.end(JSON.stringify({ error: 'Remote access is limited to shared conversation links.' }))
|
|
655
707
|
return
|
|
@@ -677,7 +729,8 @@ function writeAndDestroySocket(socket, statusLine) {
|
|
|
677
729
|
}
|
|
678
730
|
|
|
679
731
|
server.on('upgrade', (req, socket, head) => {
|
|
680
|
-
|
|
732
|
+
const isTunnelClient = isTunnelClientRequest(req)
|
|
733
|
+
if (!isAllowedRequestHost(req, isTunnelClient)) {
|
|
681
734
|
writeAndDestroySocket(socket, 'HTTP/1.1 403 Forbidden')
|
|
682
735
|
return
|
|
683
736
|
}
|
|
@@ -686,7 +739,8 @@ server.on('upgrade', (req, socket, head) => {
|
|
|
686
739
|
const url = new URL(req.url || '/', `http://${req.headers.host || `${host}:${port}`}`)
|
|
687
740
|
if (url.pathname.startsWith('/api/terminal/sessions/')) {
|
|
688
741
|
handleTerminalUpgrade(req, socket, head, url, {
|
|
689
|
-
|
|
742
|
+
// 隧道流量同样是远程客户端:终端 WebSocket 保持禁止。
|
|
743
|
+
isLocalRequest: isLoopbackAddress(req.socket.remoteAddress) && !isTunnelClient,
|
|
690
744
|
})
|
|
691
745
|
return
|
|
692
746
|
}
|
|
@@ -725,35 +779,40 @@ server.on('error', (error) => {
|
|
|
725
779
|
})
|
|
726
780
|
|
|
727
781
|
server.listen(port, host, () => {
|
|
728
|
-
|
|
782
|
+
const address = server.address()
|
|
783
|
+
const boundPort = typeof address === 'object' && address ? address.port : port
|
|
784
|
+
boundServerPort = boundPort
|
|
785
|
+
logger.info(`QuickForge local API: http://${host}:${boundPort}`)
|
|
729
786
|
if (shareLanEnabled) {
|
|
730
|
-
const lanUrls = getLanUrls(
|
|
731
|
-
logger.info(`QuickForge LAN sharing is enabled. Share pages are available at: ${lanUrls.length ? lanUrls.join(', ') : `http://<your-lan-ip>:${
|
|
787
|
+
const lanUrls = getLanUrls(boundPort)
|
|
788
|
+
logger.info(`QuickForge LAN sharing is enabled. Share pages are available at: ${lanUrls.length ? lanUrls.join(', ') : `http://<your-lan-ip>:${boundPort}`}`)
|
|
732
789
|
logger.info('Remote non-share API routes are restricted while QUICKFORGE_SHARE_LAN=1.')
|
|
733
790
|
}
|
|
734
791
|
logger.info(`QuickForge data dir: ${dataDir}`)
|
|
735
792
|
logger.info(`QuickForge project: ${getWorkspaceRoot()}`)
|
|
736
793
|
|
|
794
|
+
const pending = readCloudServiceConfig({ strict: false }).then((cloudConfig) => applyCloudServiceConfig(cloudConfig)).catch((error) => {
|
|
795
|
+
if (!shutdownStarted) logger.warn(`QuickForge remote agent failed to start: ${error?.message || error}`)
|
|
796
|
+
return null
|
|
797
|
+
}).finally(() => {
|
|
798
|
+
if (qfAgentStartPromise === pending) qfAgentStartPromise = null
|
|
799
|
+
})
|
|
800
|
+
qfAgentStartPromise = pending
|
|
801
|
+
|
|
737
802
|
if (isDev) {
|
|
738
803
|
startVite()
|
|
739
804
|
setTimeout(() => openBrowser(`http://localhost:${vitePort}`), 1000)
|
|
740
805
|
} else if (shareLanEnabled) {
|
|
741
|
-
openBrowser(`http://localhost:${
|
|
806
|
+
openBrowser(`http://localhost:${boundPort}`)
|
|
742
807
|
} else {
|
|
743
|
-
openBrowser(`http://localhost:${
|
|
808
|
+
openBrowser(`http://localhost:${boundPort}`)
|
|
744
809
|
}
|
|
745
810
|
})
|
|
746
811
|
|
|
747
812
|
// Graceful shutdown
|
|
748
813
|
async function gracefulShutdown(signal) {
|
|
749
814
|
logger.info(`Received ${signal}, shutting down gracefully...`)
|
|
750
|
-
|
|
751
|
-
stopAutoArchiveRunner()
|
|
752
|
-
stopVite()
|
|
753
|
-
await shutdownAgentManager()
|
|
754
|
-
await shutdownMcpConnections()
|
|
755
|
-
await shutdownChannels()
|
|
756
|
-
shutdownTerminalSessions()
|
|
815
|
+
await stopQuickForgeServer()
|
|
757
816
|
flushLogger()
|
|
758
817
|
process.exit(0)
|
|
759
818
|
}
|
package/server/mcp/registry.mjs
CHANGED
|
@@ -215,22 +215,21 @@ async function refreshConnections() {
|
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
|
|
218
|
+
const refreshed = await Promise.all([...enabled.values()].map(async (config) => {
|
|
219
219
|
const existing = connections.get(config.name)
|
|
220
220
|
if (existing) {
|
|
221
221
|
if (existing.status === 'error' && Date.now() - (existing.lastAttemptAt || 0) >= RETRY_ERROR_AFTER_MS) {
|
|
222
222
|
connections.delete(config.name)
|
|
223
223
|
await closeConnection(existing)
|
|
224
224
|
} else {
|
|
225
|
-
|
|
225
|
+
return [config.name, existing]
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
228
|
try {
|
|
229
|
-
|
|
230
|
-
connections.set(config.name, connection)
|
|
229
|
+
return [config.name, await connectServer(config)]
|
|
231
230
|
} catch (error) {
|
|
232
231
|
logger.error(`Failed to connect MCP server ${config.name}:`, error)
|
|
233
|
-
|
|
232
|
+
return [config.name, {
|
|
234
233
|
config,
|
|
235
234
|
client: null,
|
|
236
235
|
transport: null,
|
|
@@ -240,10 +239,12 @@ async function refreshConnections() {
|
|
|
240
239
|
connectedAt: null,
|
|
241
240
|
stderr: '',
|
|
242
241
|
lastAttemptAt: Date.now(),
|
|
243
|
-
}
|
|
242
|
+
}]
|
|
244
243
|
}
|
|
245
|
-
}
|
|
244
|
+
}))
|
|
246
245
|
|
|
246
|
+
connections.clear()
|
|
247
|
+
for (const [name, connection] of refreshed) connections.set(name, connection)
|
|
247
248
|
return connections
|
|
248
249
|
}
|
|
249
250
|
|
|
@@ -21,30 +21,51 @@ export function omitDetailsForLlm(message) {
|
|
|
21
21
|
* Handles "user-with-attachments" → "user" with multi-modal content blocks.
|
|
22
22
|
* Without this the default pi-agent-core convertToLlm silently drops
|
|
23
23
|
* user-with-attachments messages, so the LLM never sees attachments.
|
|
24
|
+
*
|
|
25
|
+
* Also guards against orphaned toolResult messages (a tool result without a
|
|
26
|
+
* preceding assistant tool_call): OpenAI-compatible APIs reject such sequences
|
|
27
|
+
* with 400 "Messages with role 'tool' must be a response to a preceding
|
|
28
|
+
* message with 'tool_calls'". Assistant messages that pi-ai would skip
|
|
29
|
+
* (stopReason error/aborted) are not counted as valid tool_call ancestors,
|
|
30
|
+
* matching the upstream skip logic so their results are dropped too.
|
|
24
31
|
*/
|
|
25
32
|
export function serverConvertToLlm(messages) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
const pendingToolCallIds = new Set()
|
|
34
|
+
const converted = []
|
|
35
|
+
for (const m of messages) {
|
|
36
|
+
if (!m || typeof m !== 'object') continue
|
|
37
|
+
if (m.role === 'artifact') continue
|
|
38
|
+
if (m.role === 'user-with-attachments') {
|
|
39
|
+
const textContent = typeof m.content === 'string'
|
|
40
|
+
? [{ type: 'text', text: m.content }]
|
|
41
|
+
: [...m.content]
|
|
42
|
+
if (Array.isArray(m.attachments)) {
|
|
43
|
+
for (const att of m.attachments) {
|
|
44
|
+
if (att.type === 'image' && att.content) {
|
|
45
|
+
textContent.push({ type: 'image', data: att.content, mimeType: att.mimeType })
|
|
46
|
+
} else if (att.type === 'document' && att.extractedText) {
|
|
47
|
+
textContent.push({ type: 'text', text: `\n\n[Document: ${att.fileName}]\n${att.extractedText}` })
|
|
40
48
|
}
|
|
41
49
|
}
|
|
42
|
-
return omitDetailsForLlm({ ...m, role: 'user', content: textContent })
|
|
43
50
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
.
|
|
51
|
+
converted.push(omitDetailsForLlm({ ...m, role: 'user', content: textContent }))
|
|
52
|
+
} else if (m.role === 'user') {
|
|
53
|
+
converted.push(omitDetailsForLlm(m))
|
|
54
|
+
} else if (m.role === 'assistant') {
|
|
55
|
+
if (m.stopReason !== 'error' && m.stopReason !== 'aborted' && Array.isArray(m.content)) {
|
|
56
|
+
for (const block of m.content) {
|
|
57
|
+
if (block?.type === 'toolCall' && block.id) pendingToolCallIds.add(block.id)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
converted.push(omitDetailsForLlm(m))
|
|
61
|
+
} else if (m.role === 'toolResult') {
|
|
62
|
+
// 孤儿 toolResult(无前导 assistant tool_calls)直接丢弃,避免上游 400
|
|
63
|
+
if (!m.toolCallId || !pendingToolCallIds.has(m.toolCallId)) continue
|
|
64
|
+
pendingToolCallIds.delete(m.toolCallId)
|
|
65
|
+
converted.push(omitDetailsForLlm(m))
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return converted
|
|
48
69
|
}
|
|
49
70
|
|
|
50
71
|
/**
|
package/server/model-catalog.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { isAuthenticatedAppClient } from './access-policy.mjs'
|
|
1
2
|
import { getCloudRuntime } from './cloud/runtime.mjs'
|
|
2
3
|
import { isManagedCloudModel } from './cloud/models.mjs'
|
|
3
4
|
import { readStore } from './storage.mjs'
|
|
4
|
-
import { isTailscaleAddress } from './utils/network.mjs'
|
|
5
5
|
|
|
6
6
|
export const MODEL_REFERENCE_VERSION = 1
|
|
7
7
|
|
|
@@ -57,10 +57,10 @@ function configuredEntries(providers) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
export function cloudAllowedForContext(context = {}) {
|
|
60
|
+
if (context.source === 'shared') return context.allowCloud === true
|
|
60
61
|
if (context.allowCloud === false) return false
|
|
61
62
|
if (context.allowCloud === true || context.source === 'acp' || context.source === 'scheduled') return true
|
|
62
|
-
|
|
63
|
-
return context.remoteAuthorized === true && isTailscaleAddress(context.remoteAddress)
|
|
63
|
+
return isAuthenticatedAppClient(context)
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
export function modelReferenceFromSnapshot(model, providers = []) {
|
|
@@ -176,7 +176,14 @@ async function resolveCloud(ref, context) {
|
|
|
176
176
|
throw requestError('QuickForge Cloud is not available from this client.', 403, 'cloud_access_denied')
|
|
177
177
|
}
|
|
178
178
|
const runtime = await getCloudRuntime()
|
|
179
|
-
if (!runtime?.enabled)
|
|
179
|
+
if (!runtime?.enabled) {
|
|
180
|
+
const disabled = runtime?.config?.baseUrl && runtime?.config?.enabled !== true
|
|
181
|
+
throw requestError(
|
|
182
|
+
disabled ? 'QuickForge Cloud is disabled.' : 'QuickForge Cloud is not configured.',
|
|
183
|
+
503,
|
|
184
|
+
disabled ? 'cloud_disabled' : 'cloud_not_configured',
|
|
185
|
+
)
|
|
186
|
+
}
|
|
180
187
|
return (await runtime.models.resolve({
|
|
181
188
|
provider: 'quickforge-cloud',
|
|
182
189
|
quickforgeModelSource: 'cloud',
|