@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
|
@@ -75,7 +75,7 @@ function builtinProfileFromSubagent(definition, override = null) {
|
|
|
75
75
|
allowedTools: [...definition.allowedTools],
|
|
76
76
|
capabilityPolicy: definition.capabilityPolicy || inferCapabilityPolicy(definition.allowedTools),
|
|
77
77
|
model: modelReferenceSnapshot(override?.model || definition.model),
|
|
78
|
-
thinkingLevel: 'inherit',
|
|
78
|
+
thinkingLevel: normalizeAgentProfileThinkingLevel(override?.thinkingLevel || 'inherit'),
|
|
79
79
|
lifecycle: 'builtin',
|
|
80
80
|
managed: true,
|
|
81
81
|
maxRuntimeMs: definition.maxRuntimeMs || DEFAULT_MAX_RUNTIME_MS,
|
|
@@ -308,17 +308,36 @@ export async function updateCustomAgentProfile(id, patch) {
|
|
|
308
308
|
return { ...next, filePath: file, relativePath: `~/.quickforge/agents/${next.name}.md` }
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
-
export async function
|
|
311
|
+
export async function updateBuiltinAgentOverrides(id, patch = {}) {
|
|
312
312
|
const current = await getAgentProfile(id)
|
|
313
313
|
if (!current) throw requestError('Agent not found', 404)
|
|
314
|
-
if (!current.builtin) throw requestError('Only built-in agents support
|
|
315
|
-
const model = validateModelReference(normalizeModelReference(modelInput))
|
|
314
|
+
if (!current.builtin) throw requestError('Only built-in agents support overrides', 400)
|
|
316
315
|
const overrides = await readStore(BUILTIN_OVERRIDES_STORE).catch(() => ({}))
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
316
|
+
const base = overrides && typeof overrides === 'object' ? overrides : {}
|
|
317
|
+
const next = { ...base }
|
|
318
|
+
const entry = { ...(base[current.name] && typeof base[current.name] === 'object' ? base[current.name] : {}) }
|
|
319
|
+
|
|
320
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'model')) {
|
|
321
|
+
const model = validateModelReference(normalizeModelReference(patch.model))
|
|
322
|
+
if (model.mode === 'fixed') entry.model = model
|
|
323
|
+
else delete entry.model
|
|
324
|
+
}
|
|
325
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'thinkingLevel')) {
|
|
326
|
+
const thinkingLevel = normalizeAgentProfileThinkingLevel(patch.thinkingLevel)
|
|
327
|
+
if (thinkingLevel === 'inherit') delete entry.thinkingLevel
|
|
328
|
+
else entry.thinkingLevel = thinkingLevel
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (Object.keys(entry).length === 0) delete next[current.name]
|
|
332
|
+
else next[current.name] = entry
|
|
320
333
|
await writeStore(BUILTIN_OVERRIDES_STORE, next)
|
|
321
|
-
|
|
334
|
+
|
|
335
|
+
const definition = subagentDefinitions.find((item) => item.name === current.name)
|
|
336
|
+
return builtinProfileFromSubagent(definition, next[current.name] || null)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export async function updateBuiltinAgentModelOverride(id, modelInput) {
|
|
340
|
+
return updateBuiltinAgentOverrides(id, { model: modelInput })
|
|
322
341
|
}
|
|
323
342
|
|
|
324
343
|
export async function deleteCustomAgentProfile(id) {
|
|
@@ -18,6 +18,14 @@ export const DEFAULT_AUTO_COMPACT_SETTINGS = {
|
|
|
18
18
|
requireConfirmation: true,
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* 保留尾部(最近回合)的字符预算上限。当会话以工具消息为主、user 回合
|
|
23
|
+
* 稀疏时,按 keepRecentTurns 从尾部回溯会跨越大量工具消息,导致压缩
|
|
24
|
+
* 范围过小甚至为空(tailStart 与 compactedUpToIndex 重合)。达到该预算
|
|
25
|
+
* 时按当前用户轮次起点截断,将更早的完整工具链历史并入压缩范围。
|
|
26
|
+
*/
|
|
27
|
+
export const DEFAULT_MAX_TAIL_CHARS = 40000
|
|
28
|
+
|
|
21
29
|
const AUTO_COMPACT_MIN_INTERVAL_MS = 30_000
|
|
22
30
|
const AUTO_COMPACT_REJECTION_SUPPRESS_MS = 10 * 60_000
|
|
23
31
|
|
|
@@ -64,11 +72,13 @@ function contentToText(content) {
|
|
|
64
72
|
}).filter(Boolean).join('\n')
|
|
65
73
|
}
|
|
66
74
|
|
|
75
|
+
function messageChars(message) {
|
|
76
|
+
if (!message || typeof message !== 'object') return 0
|
|
77
|
+
return [message.role || '', contentToText(message.content), safeJson(message.attachments)].join('\n').length
|
|
78
|
+
}
|
|
79
|
+
|
|
67
80
|
function estimateMessagesChars(messages) {
|
|
68
|
-
return (Array.isArray(messages) ? messages : []).reduce((total, message) =>
|
|
69
|
-
if (!message || typeof message !== 'object') return total
|
|
70
|
-
return total + [message.role || '', contentToText(message.content), safeJson(message.attachments)].join('\n').length
|
|
71
|
-
}, 0)
|
|
81
|
+
return (Array.isArray(messages) ? messages : []).reduce((total, message) => total + messageChars(message), 0)
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
function isUserMessage(message) {
|
|
@@ -76,13 +86,26 @@ function isUserMessage(message) {
|
|
|
76
86
|
&& !isCompactSummaryMessage(message)
|
|
77
87
|
}
|
|
78
88
|
|
|
79
|
-
function
|
|
89
|
+
function isToolResultMessage(message) {
|
|
90
|
+
return message?.role === 'toolResult'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function tailStartForRecentTurns(messages, keepRecentTurns, maxTailChars = DEFAULT_MAX_TAIL_CHARS) {
|
|
80
94
|
const source = Array.isArray(messages) ? messages : []
|
|
81
95
|
let seenUserTurns = 0
|
|
96
|
+
let tailChars = 0
|
|
82
97
|
for (let index = source.length - 1; index >= 0; index--) {
|
|
83
|
-
if (
|
|
84
|
-
|
|
98
|
+
if (isUserMessage(source[index])) seenUserTurns += 1
|
|
99
|
+
tailChars += messageChars(source[index])
|
|
85
100
|
if (seenUserTurns >= keepRecentTurns) return index
|
|
101
|
+
// 保留的尾部超过预算:仍按完整用户轮次切分,避免把当前运行中的
|
|
102
|
+
// assistant/toolResult 链从中间截断,导致压缩线落在一轮对话内部。
|
|
103
|
+
if (tailChars >= maxTailChars) {
|
|
104
|
+
for (let turnStart = index; turnStart >= 0; turnStart--) {
|
|
105
|
+
if (isUserMessage(source[turnStart])) return turnStart
|
|
106
|
+
}
|
|
107
|
+
return 0
|
|
108
|
+
}
|
|
86
109
|
}
|
|
87
110
|
return 0
|
|
88
111
|
}
|
|
@@ -145,7 +168,51 @@ export function buildAutoCompactLoopMessages(session, messages) {
|
|
|
145
168
|
if (!summaryMessage) return messages
|
|
146
169
|
const source = Array.isArray(messages) ? messages : []
|
|
147
170
|
const compactedUpToIndex = Math.min(source.length, Math.max(0, Number(session.contextCompaction?.compactedUpToIndex) || 0))
|
|
148
|
-
|
|
171
|
+
// 防御旧持久化数据:compactedUpToIndex 可能指向 toolResult,跳过开头的孤儿,
|
|
172
|
+
// 避免压缩后的消息序列以 tool 消息开头(LLM API 会拒绝孤立 tool 消息)
|
|
173
|
+
let tailStart = compactedUpToIndex
|
|
174
|
+
while (tailStart < source.length && isToolResultMessage(source[tailStart])) tailStart += 1
|
|
175
|
+
return [summaryMessage, ...source.slice(tailStart)]
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function messageTimestampMs(message) {
|
|
179
|
+
const value = message?.timestamp
|
|
180
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
181
|
+
if (typeof value === 'string') {
|
|
182
|
+
const parsed = Date.parse(value)
|
|
183
|
+
if (Number.isFinite(parsed)) return parsed
|
|
184
|
+
}
|
|
185
|
+
return 0
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function minimumProviderUsageIndex(session, messages) {
|
|
189
|
+
if (!session?.contextCompaction?.summaryMessage) return 0
|
|
190
|
+
const source = Array.isArray(messages) ? messages : []
|
|
191
|
+
const compactedUpToIndex = Math.min(source.length, Math.max(0, Number(session.contextCompaction.compactedUpToIndex) || 0))
|
|
192
|
+
let tailStart = compactedUpToIndex
|
|
193
|
+
while (tailStart < source.length && isToolResultMessage(source[tailStart])) tailStart += 1
|
|
194
|
+
|
|
195
|
+
const compactedAt = Date.parse(session.contextCompaction.compactedAt || '')
|
|
196
|
+
if (Number.isFinite(compactedAt)) {
|
|
197
|
+
const firstNewMessageIndex = source.findIndex((message) => messageTimestampMs(message) > compactedAt)
|
|
198
|
+
if (firstNewMessageIndex >= 0) return Math.max(1, firstNewMessageIndex - tailStart + 1)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const sourceMessageCount = Number(session.contextCompaction.sourceMessageCount)
|
|
202
|
+
if (Number.isFinite(sourceMessageCount) && sourceMessageCount > 0 && source.length >= sourceMessageCount) {
|
|
203
|
+
return Math.max(1, sourceMessageCount - tailStart + 1)
|
|
204
|
+
}
|
|
205
|
+
return source.length - tailStart + 1
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function hasNewMessagesSinceCompaction(session, messages) {
|
|
209
|
+
if (!session?.contextCompaction?.summaryMessage) return true
|
|
210
|
+
const source = Array.isArray(messages) ? messages : []
|
|
211
|
+
const sourceMessageCount = Number(session.contextCompaction.sourceMessageCount)
|
|
212
|
+
if (Number.isFinite(sourceMessageCount) && sourceMessageCount > 0 && source.length > sourceMessageCount) return true
|
|
213
|
+
|
|
214
|
+
const compactedAt = Date.parse(session.contextCompaction.compactedAt || '')
|
|
215
|
+
return Number.isFinite(compactedAt) && source.some((message) => messageTimestampMs(message) > compactedAt)
|
|
149
216
|
}
|
|
150
217
|
|
|
151
218
|
export async function compactSessionInPlace({
|
|
@@ -281,6 +348,7 @@ export function estimateSessionContextUsage(session, messages = session?.agent?.
|
|
|
281
348
|
messages: loopMessages,
|
|
282
349
|
tools: session.agent.state.tools,
|
|
283
350
|
model: session.model,
|
|
351
|
+
minimumProviderUsageIndex: minimumProviderUsageIndex(session, sourceMessages),
|
|
284
352
|
})
|
|
285
353
|
value.isCompacted = loopMessages !== sourceMessages
|
|
286
354
|
value.originalMessageCount = sourceMessages.length
|
|
@@ -305,6 +373,7 @@ export async function maybeAutoCompactSession({ session, messages, signal, emitS
|
|
|
305
373
|
messages: loopMessages,
|
|
306
374
|
tools: session.agent.state.tools,
|
|
307
375
|
model: session.model,
|
|
376
|
+
minimumProviderUsageIndex: minimumProviderUsageIndex(session, messages),
|
|
308
377
|
})
|
|
309
378
|
if (!usage.contextWindow) return { compacted: false, usage, reason: 'missing_context_window' }
|
|
310
379
|
if (!shouldCompactContextByPercent(usage, settings.thresholdPercent)) return { compacted: false, usage, reason: 'below_threshold' }
|
|
@@ -314,7 +383,7 @@ export async function maybeAutoCompactSession({ session, messages, signal, emitS
|
|
|
314
383
|
if (session.lastAutoCompactAt && now - session.lastAutoCompactAt < AUTO_COMPACT_MIN_INTERVAL_MS) {
|
|
315
384
|
return { compacted: false, usage, reason: 'recently_compacted' }
|
|
316
385
|
}
|
|
317
|
-
if (session
|
|
386
|
+
if (!hasNewMessagesSinceCompaction(session, messages)) {
|
|
318
387
|
return { compacted: false, usage, reason: 'not_enough_new_messages' }
|
|
319
388
|
}
|
|
320
389
|
|
package/server/cloud/client.mjs
CHANGED
|
@@ -60,19 +60,16 @@ export class CloudClient {
|
|
|
60
60
|
|
|
61
61
|
health(signal) { return this.request('healthz', { signal }) }
|
|
62
62
|
ready(signal) { return this.request('readyz', { signal }) }
|
|
63
|
-
registerGuest(input, idempotencyKey, signal) {
|
|
64
|
-
return this.request('v1/guest/installations', {
|
|
65
|
-
method: 'POST', body: input, headers: { 'Idempotency-Key': idempotencyKey }, signal,
|
|
66
|
-
})
|
|
67
|
-
}
|
|
68
63
|
refresh(refreshToken, signal) {
|
|
69
64
|
return this.request('oauth/token', {
|
|
70
65
|
method: 'POST', body: { grantType: 'refresh_token', refreshToken }, signal,
|
|
71
66
|
})
|
|
72
67
|
}
|
|
73
|
-
authorizeDevice(
|
|
68
|
+
authorizeDevice({ installationId, clientId = 'quickforge-desktop', publicKey, installationName, platform, clientVersion, signal } = {}) {
|
|
74
69
|
return this.request('oauth/device_authorization', {
|
|
75
|
-
method: 'POST',
|
|
70
|
+
method: 'POST',
|
|
71
|
+
body: { installationId, clientId, publicKey, installationName, platform, clientVersion },
|
|
72
|
+
signal,
|
|
76
73
|
})
|
|
77
74
|
}
|
|
78
75
|
exchangeDeviceCode(deviceCode, clientId = 'quickforge-desktop', signal) {
|
package/server/cloud/config.mjs
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
import net from 'node:net'
|
|
2
|
-
|
|
3
1
|
const DEFAULT_TIMEOUT_MS = 10_000
|
|
4
2
|
|
|
5
|
-
function
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (ip === 4) return host.startsWith('127.')
|
|
10
|
-
if (ip === 6) return host === '::1' || host === '0:0:0:0:0:0:0:1'
|
|
11
|
-
return false
|
|
3
|
+
function invalidUrl(message) {
|
|
4
|
+
const error = new Error(message)
|
|
5
|
+
error.statusCode = 400
|
|
6
|
+
return error
|
|
12
7
|
}
|
|
13
8
|
|
|
14
9
|
export function parseCloudBaseUrl(value, _options = {}) {
|
|
@@ -19,18 +14,16 @@ export function parseCloudBaseUrl(value, _options = {}) {
|
|
|
19
14
|
try {
|
|
20
15
|
url = new URL(raw)
|
|
21
16
|
} catch {
|
|
22
|
-
throw
|
|
17
|
+
throw invalidUrl('QuickForge Cloud URL must be a valid URL.')
|
|
23
18
|
}
|
|
24
19
|
|
|
25
20
|
if (url.username || url.password || url.search || url.hash) {
|
|
26
|
-
throw
|
|
21
|
+
throw invalidUrl('QuickForge Cloud URL must not contain credentials, query parameters, or fragments.')
|
|
27
22
|
}
|
|
28
|
-
if (url.protocol !== 'https:') {
|
|
29
|
-
|
|
30
|
-
throw new Error('QUICKFORGE_CLOUD_URL must use HTTPS. HTTP is allowed only for loopback addresses.')
|
|
31
|
-
}
|
|
23
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
24
|
+
throw invalidUrl('QuickForge Cloud URL must use http or https.')
|
|
32
25
|
}
|
|
33
|
-
if (url.pathname.includes('..')) throw
|
|
26
|
+
if (url.pathname.includes('..')) throw invalidUrl('QuickForge Cloud URL contains an unsafe path.')
|
|
34
27
|
url.pathname = `${url.pathname.replace(/\/+$/, '')}/`
|
|
35
28
|
return url
|
|
36
29
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { generateKeyPair } from 'node:crypto'
|
|
1
|
+
import { generateKeyPair, randomUUID } from 'node:crypto'
|
|
2
2
|
import { promises as fs } from 'node:fs'
|
|
3
3
|
import os from 'node:os'
|
|
4
4
|
import path from 'node:path'
|
|
@@ -145,8 +145,15 @@ export function createCloudCredentialStore({
|
|
|
145
145
|
|
|
146
146
|
async function ensureInstallation() {
|
|
147
147
|
const current = await read()
|
|
148
|
-
if (current.publicKey && current.privateKeyPkcs8) return current
|
|
149
|
-
return update(async (record) =>
|
|
148
|
+
if (current.publicKey && current.privateKeyPkcs8 && current.installationId) return current
|
|
149
|
+
return update(async (record) => {
|
|
150
|
+
const keyPair = (record.publicKey && record.privateKeyPkcs8) ? {} : await generateInstallationKeyPair()
|
|
151
|
+
return {
|
|
152
|
+
...record,
|
|
153
|
+
...keyPair,
|
|
154
|
+
installationId: record.installationId || randomUUID(),
|
|
155
|
+
}
|
|
156
|
+
})
|
|
150
157
|
}
|
|
151
158
|
|
|
152
159
|
async function rotateInstallation() {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { CloudApiError } from './client.mjs'
|
|
1
|
+
import { CloudApiError } from './client.mjs'
|
|
3
2
|
|
|
4
3
|
const SESSION_INVALID_CODES = new Set(['refresh_token_reused', 'installation_revoked', 'invalid_refresh_token'])
|
|
5
4
|
const DEVICE_PENDING_CODES = new Set(['authorization_pending', 'pending'])
|
|
@@ -11,7 +10,6 @@ const DEVICE_NETWORK_CAUSE_CODES = new Set(['ECONNREFUSED', 'ECONNRESET', 'ENETD
|
|
|
11
10
|
const DEVICE_CLIENT_ID = 'quickforge-desktop'
|
|
12
11
|
const MODEL_CACHE_TTL_MS = 60_000
|
|
13
12
|
const MODEL_CACHE_HARD_TTL_MS = 5 * 60_000
|
|
14
|
-
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
15
13
|
|
|
16
14
|
function tokenExpiry(expiresIn) {
|
|
17
15
|
const seconds = Number(expiresIn)
|
|
@@ -38,15 +36,6 @@ function isDeviceFlowNetworkError(error) {
|
|
|
38
36
|
return error instanceof TypeError || DEVICE_NETWORK_CAUSE_CODES.has(causeCode)
|
|
39
37
|
}
|
|
40
38
|
|
|
41
|
-
function registrationHash(record, serviceUrl) {
|
|
42
|
-
return createHash('sha256').update(JSON.stringify({
|
|
43
|
-
installationName: record.installationName,
|
|
44
|
-
platform: record.platform,
|
|
45
|
-
clientVersion: record.clientVersion,
|
|
46
|
-
publicKey: record.publicKey,
|
|
47
|
-
serviceUrl,
|
|
48
|
-
})).digest('base64url')
|
|
49
|
-
}
|
|
50
39
|
|
|
51
40
|
export class CloudIdentityManager {
|
|
52
41
|
constructor({ client, store, serviceUrl } = {}) {
|
|
@@ -92,45 +81,6 @@ export class CloudIdentityManager {
|
|
|
92
81
|
}
|
|
93
82
|
}
|
|
94
83
|
|
|
95
|
-
async startGuest({ signal } = {}) {
|
|
96
|
-
let record = await this.store.read()
|
|
97
|
-
this.assertSessionService(record)
|
|
98
|
-
if (record.rotateInstallationBeforeRegistration) {
|
|
99
|
-
record = await this.store.rotateInstallation()
|
|
100
|
-
} else {
|
|
101
|
-
record = await this.store.ensureInstallation()
|
|
102
|
-
}
|
|
103
|
-
const requestHash = registrationHash(record, this.serviceUrl)
|
|
104
|
-
let idempotencyKey = record.pendingRegistrationKey
|
|
105
|
-
if (!UUID_PATTERN.test(idempotencyKey || '') || record.pendingRegistrationHash !== requestHash) {
|
|
106
|
-
idempotencyKey = randomUUID()
|
|
107
|
-
await this.store.update((current) => ({
|
|
108
|
-
...current,
|
|
109
|
-
pendingRegistrationKey: idempotencyKey,
|
|
110
|
-
pendingRegistrationHash: requestHash,
|
|
111
|
-
}))
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const tokens = await this.client.registerGuest({
|
|
115
|
-
installationName: record.installationName,
|
|
116
|
-
platform: record.platform,
|
|
117
|
-
clientVersion: record.clientVersion,
|
|
118
|
-
publicKey: record.publicKey,
|
|
119
|
-
}, idempotencyKey, signal)
|
|
120
|
-
|
|
121
|
-
this.setAccess(tokens)
|
|
122
|
-
await this.store.update((current) => ({
|
|
123
|
-
...current,
|
|
124
|
-
mode: tokens.identityMode || 'guest',
|
|
125
|
-
installationId: tokens.installationId,
|
|
126
|
-
refreshToken: tokens.refreshToken,
|
|
127
|
-
sessionCloudUrl: this.serviceUrl,
|
|
128
|
-
pendingRegistrationKey: undefined,
|
|
129
|
-
pendingRegistrationHash: undefined,
|
|
130
|
-
pendingDeviceFlow: undefined,
|
|
131
|
-
}))
|
|
132
|
-
return this.getStatusWithRemoteSummary(signal)
|
|
133
|
-
}
|
|
134
84
|
|
|
135
85
|
async startDeviceFlow({ signal } = {}) {
|
|
136
86
|
let record = await this.store.read()
|
|
@@ -138,16 +88,23 @@ export class CloudIdentityManager {
|
|
|
138
88
|
if (record.mode === 'account' && record.refreshToken) {
|
|
139
89
|
throw new CloudApiError('QuickForge Cloud is already connected to an account.', { status: 409, code: 'cloud_account_already_connected' })
|
|
140
90
|
}
|
|
141
|
-
if (record.
|
|
142
|
-
await this.
|
|
143
|
-
record = await this.store.read()
|
|
91
|
+
if (record.rotateInstallationBeforeRegistration) {
|
|
92
|
+
record = await this.store.rotateInstallation()
|
|
144
93
|
}
|
|
145
|
-
|
|
146
|
-
|
|
94
|
+
record = await this.store.ensureInstallation()
|
|
95
|
+
if (!record.installationId || !record.publicKey) {
|
|
96
|
+
throw new CloudApiError('QuickForge Cloud installation identity is incomplete.', { status: 409, code: 'cloud_installation_missing' })
|
|
147
97
|
}
|
|
148
98
|
this.assertSessionService(record)
|
|
149
|
-
const
|
|
150
|
-
|
|
99
|
+
const authorization = await this.client.authorizeDevice({
|
|
100
|
+
installationId: record.installationId,
|
|
101
|
+
clientId: DEVICE_CLIENT_ID,
|
|
102
|
+
publicKey: record.publicKey,
|
|
103
|
+
installationName: record.installationName,
|
|
104
|
+
platform: record.platform,
|
|
105
|
+
clientVersion: record.clientVersion,
|
|
106
|
+
signal,
|
|
107
|
+
})
|
|
151
108
|
const expiresIn = Math.max(1, Number(authorization?.expiresIn) || 600)
|
|
152
109
|
const interval = Math.max(1, Number(authorization?.interval) || 5)
|
|
153
110
|
const pendingDeviceFlow = {
|
|
@@ -270,7 +227,7 @@ export class CloudIdentityManager {
|
|
|
270
227
|
this.setAccess(tokens)
|
|
271
228
|
await this.store.update((current) => ({
|
|
272
229
|
...current,
|
|
273
|
-
mode: tokens.identityMode || current.mode || '
|
|
230
|
+
mode: tokens.identityMode || current.mode || 'account',
|
|
274
231
|
installationId: tokens.installationId || current.installationId,
|
|
275
232
|
refreshToken: tokens.refreshToken,
|
|
276
233
|
sessionCloudUrl: this.serviceUrl,
|
|
@@ -338,7 +295,7 @@ export class CloudIdentityManager {
|
|
|
338
295
|
if (this.modelsPromise) return this.modelsPromise
|
|
339
296
|
this.modelsPromise = this.withAccessToken((token) => this.client.models(token, signal), { signal })
|
|
340
297
|
.then((response) => {
|
|
341
|
-
this.modelsCache = Array.isArray(response?.items) ? response.items : []
|
|
298
|
+
this.modelsCache = Array.isArray(response?.data) ? response.data : (Array.isArray(response?.items) ? response.items : [])
|
|
342
299
|
this.modelsCachedAt = Date.now()
|
|
343
300
|
return this.modelsCache
|
|
344
301
|
})
|
package/server/cloud/models.mjs
CHANGED
|
@@ -16,7 +16,7 @@ export function toPublicCloudModel(model) {
|
|
|
16
16
|
reasoning: Boolean(capabilities.reasoning),
|
|
17
17
|
input: capabilities.vision ? ['text', 'image'] : ['text'],
|
|
18
18
|
contextWindow: Number(model.contextWindow || 128_000),
|
|
19
|
-
maxTokens: Number(model.maxTokens ||
|
|
19
|
+
maxTokens: Number(model.maxTokens || 32_768),
|
|
20
20
|
quickforgeModelSource: CLOUD_SOURCE,
|
|
21
21
|
quickforgeCatalogId: id,
|
|
22
22
|
quickforgeCapabilities: {
|