@shawnstack/quickforge 1.3.4 → 1.3.6
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/README.md +10 -10
- package/package.json +1 -1
- package/server/agent-manager.mjs +2 -2
- package/server/ai-http-logger.mjs +208 -0
- package/server/conversation-compaction.mjs +3 -2
- package/server/index.mjs +2 -0
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# 速构 QuickForge
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<img alt="Version" src="https://img.shields.io/badge/version-1.3.
|
|
4
|
+
<img alt="Version" src="https://img.shields.io/badge/version-1.3.6-blue" />
|
|
5
5
|
<img alt="License" src="https://img.shields.io/badge/license-MIT-green" />
|
|
6
6
|
<img alt="Node" src="https://img.shields.io/badge/node-%3E%3D20-brightgreen" />
|
|
7
7
|
<img alt="React" src="https://img.shields.io/badge/react-19-61DAFB?logo=react" />
|
|
@@ -65,7 +65,7 @@ QuickForge 的工具能力很直接,因此也需要谨慎使用:
|
|
|
65
65
|
#### 从 npm 安装
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
|
-
npm install -g @shawnstack/quickforge@1.3.
|
|
68
|
+
npm install -g @shawnstack/quickforge@1.3.6
|
|
69
69
|
qf
|
|
70
70
|
|
|
71
71
|
# CLI 工具
|
|
@@ -79,17 +79,17 @@ qf update
|
|
|
79
79
|
当前版本的离线包:
|
|
80
80
|
|
|
81
81
|
```text
|
|
82
|
-
package-offline/shawnstack-quickforge-1.3.
|
|
82
|
+
package-offline/shawnstack-quickforge-1.3.6.tgz
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
在安装了 Node.js 20+ 和 npm 的机器上执行:
|
|
86
86
|
|
|
87
87
|
```bash
|
|
88
|
-
npm install -g ./package-offline/shawnstack-quickforge-1.3.
|
|
88
|
+
npm install -g ./package-offline/shawnstack-quickforge-1.3.6.tgz
|
|
89
89
|
qf
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
该包由 `v1.3.
|
|
92
|
+
该包由 `v1.3.6` 标签生成,包含离线安装所需的运行时依赖。
|
|
93
93
|
|
|
94
94
|
### 本地开发
|
|
95
95
|
|
|
@@ -228,7 +228,7 @@ QuickForge intentionally exposes powerful local capabilities, so the boundaries
|
|
|
228
228
|
#### npm
|
|
229
229
|
|
|
230
230
|
```bash
|
|
231
|
-
npm install -g @shawnstack/quickforge@1.3.
|
|
231
|
+
npm install -g @shawnstack/quickforge@1.3.6
|
|
232
232
|
qf
|
|
233
233
|
|
|
234
234
|
# CLI utilities
|
|
@@ -239,20 +239,20 @@ qf update
|
|
|
239
239
|
|
|
240
240
|
#### Offline tarball
|
|
241
241
|
|
|
242
|
-
The offline release package for `v1.3.
|
|
242
|
+
The offline release package for `v1.3.6` is:
|
|
243
243
|
|
|
244
244
|
```text
|
|
245
|
-
package-offline/shawnstack-quickforge-1.3.
|
|
245
|
+
package-offline/shawnstack-quickforge-1.3.6.tgz
|
|
246
246
|
```
|
|
247
247
|
|
|
248
248
|
Install it on a machine with Node.js 20+ and npm:
|
|
249
249
|
|
|
250
250
|
```bash
|
|
251
|
-
npm install -g ./package-offline/shawnstack-quickforge-1.3.
|
|
251
|
+
npm install -g ./package-offline/shawnstack-quickforge-1.3.6.tgz
|
|
252
252
|
qf
|
|
253
253
|
```
|
|
254
254
|
|
|
255
|
-
The package was generated from tag `v1.3.
|
|
255
|
+
The package was generated from tag `v1.3.6` and includes bundled runtime dependencies for offline installation.
|
|
256
256
|
|
|
257
257
|
### Local development
|
|
258
258
|
|
package/package.json
CHANGED
package/server/agent-manager.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events'
|
|
2
2
|
import { randomUUID } from 'node:crypto'
|
|
3
3
|
import { Agent } from '@mariozechner/pi-agent-core'
|
|
4
|
-
import {
|
|
4
|
+
import { streamSimpleWithAiHttpLogging } from './ai-http-logger.mjs'
|
|
5
5
|
import { toolHandlers, loadSkillToolContext } from './tools/index.mjs'
|
|
6
6
|
import { createSkillTools, workspaceTools } from './tools/definitions.mjs'
|
|
7
7
|
import { callMcpTool, createMcpToolDefinitions, isMcpToolName } from './mcp/registry.mjs'
|
|
@@ -699,7 +699,7 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
699
699
|
messages,
|
|
700
700
|
tools,
|
|
701
701
|
},
|
|
702
|
-
streamFn:
|
|
702
|
+
streamFn: streamSimpleWithAiHttpLogging,
|
|
703
703
|
getApiKey,
|
|
704
704
|
sessionId,
|
|
705
705
|
convertToLlm: serverConvertToLlm,
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
4
|
+
import { randomUUID } from 'node:crypto'
|
|
5
|
+
import { streamSimple } from '@mariozechner/pi-ai'
|
|
6
|
+
import { logsDir } from './storage.mjs'
|
|
7
|
+
|
|
8
|
+
const PATCH_MARKER = Symbol.for('quickforge.aiHttpLogger.fetchPatched')
|
|
9
|
+
const ORIGINAL_FETCH = Symbol.for('quickforge.aiHttpLogger.originalFetch')
|
|
10
|
+
const enabledValues = new Set(['1', 'true', 'yes', 'on', 'full', 'raw'])
|
|
11
|
+
const aiHttpLogEnabled = enabledValues.has(String(process.env.QUICKFORGE_AI_HTTP_LOG || '').toLowerCase())
|
|
12
|
+
const aiHttpContext = new AsyncLocalStorage()
|
|
13
|
+
|
|
14
|
+
function currentLogFile() {
|
|
15
|
+
const date = new Date().toISOString().slice(0, 10)
|
|
16
|
+
return path.join(logsDir, `ai-http-${date}.jsonl`)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function writeAiHttpRecord(record) {
|
|
20
|
+
if (!aiHttpLogEnabled) return
|
|
21
|
+
try {
|
|
22
|
+
fs.mkdirSync(logsDir, { recursive: true })
|
|
23
|
+
fs.appendFile(currentLogFile(), `${JSON.stringify({ ts: new Date().toISOString(), ...record })}\n`, () => {})
|
|
24
|
+
} catch {
|
|
25
|
+
// Keep AI calls working even when diagnostic logging fails.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function headersToRecord(headers) {
|
|
30
|
+
const result = {}
|
|
31
|
+
if (!headers) return result
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const iterable = typeof headers.entries === 'function' ? headers.entries() : Object.entries(headers)
|
|
35
|
+
for (const [key, value] of iterable) {
|
|
36
|
+
result[String(key)] = Array.isArray(value) ? value.join(', ') : String(value)
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
// ignore malformed headers
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return result
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isRequest(value) {
|
|
46
|
+
return typeof Request !== 'undefined' && value instanceof Request
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function requestUrl(input) {
|
|
50
|
+
if (isRequest(input)) return input.url
|
|
51
|
+
if (input instanceof URL) return input.href
|
|
52
|
+
return String(input)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function requestMethod(input, init) {
|
|
56
|
+
return String(init?.method || (isRequest(input) ? input.method : 'GET')).toUpperCase()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function requestHeaders(input, init) {
|
|
60
|
+
return {
|
|
61
|
+
...(isRequest(input) ? headersToRecord(input.headers) : {}),
|
|
62
|
+
...headersToRecord(init?.headers),
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function bodyToText(body) {
|
|
67
|
+
if (body === undefined || body === null) return null
|
|
68
|
+
if (typeof body === 'string') return body
|
|
69
|
+
if (body instanceof URLSearchParams) return body.toString()
|
|
70
|
+
if (typeof Blob !== 'undefined' && body instanceof Blob) return body.text()
|
|
71
|
+
if (body instanceof ArrayBuffer) return Buffer.from(body).toString('utf8')
|
|
72
|
+
if (ArrayBuffer.isView(body)) return Buffer.from(body.buffer, body.byteOffset, body.byteLength).toString('utf8')
|
|
73
|
+
if (typeof FormData !== 'undefined' && body instanceof FormData) {
|
|
74
|
+
const entries = []
|
|
75
|
+
for (const [key, value] of body.entries()) {
|
|
76
|
+
entries.push([
|
|
77
|
+
key,
|
|
78
|
+
typeof value === 'string'
|
|
79
|
+
? value
|
|
80
|
+
: { name: value.name, type: value.type, size: value.size },
|
|
81
|
+
])
|
|
82
|
+
}
|
|
83
|
+
return JSON.stringify(entries)
|
|
84
|
+
}
|
|
85
|
+
if (typeof ReadableStream !== 'undefined' && body instanceof ReadableStream) {
|
|
86
|
+
return '[ReadableStream body not captured to avoid consuming the request stream]'
|
|
87
|
+
}
|
|
88
|
+
if (typeof body === 'object') {
|
|
89
|
+
try {
|
|
90
|
+
return JSON.stringify(body)
|
|
91
|
+
} catch {
|
|
92
|
+
return String(body)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return String(body)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function readRequestBody(input, init) {
|
|
99
|
+
if (init && Object.hasOwn(init, 'body')) return bodyToText(init.body)
|
|
100
|
+
if (!isRequest(input)) return null
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
return await input.clone().text()
|
|
104
|
+
} catch (error) {
|
|
105
|
+
return `[request body capture failed: ${error instanceof Error ? error.message : String(error)}]`
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function logResponseBody(response, baseRecord) {
|
|
110
|
+
try {
|
|
111
|
+
const body = await response.clone().text()
|
|
112
|
+
writeAiHttpRecord({
|
|
113
|
+
...baseRecord,
|
|
114
|
+
type: 'ai_http_response',
|
|
115
|
+
response: {
|
|
116
|
+
status: response.status,
|
|
117
|
+
statusText: response.statusText,
|
|
118
|
+
headers: headersToRecord(response.headers),
|
|
119
|
+
body,
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
} catch (error) {
|
|
123
|
+
writeAiHttpRecord({
|
|
124
|
+
...baseRecord,
|
|
125
|
+
type: 'ai_http_response_capture_error',
|
|
126
|
+
response: {
|
|
127
|
+
status: response.status,
|
|
128
|
+
statusText: response.statusText,
|
|
129
|
+
headers: headersToRecord(response.headers),
|
|
130
|
+
},
|
|
131
|
+
error: error instanceof Error ? error.message : String(error),
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function loggedFetch(originalFetch, input, init) {
|
|
137
|
+
const context = aiHttpContext.getStore()
|
|
138
|
+
if (!context) return originalFetch(input, init)
|
|
139
|
+
|
|
140
|
+
const httpRequestId = randomUUID()
|
|
141
|
+
const startedAt = Date.now()
|
|
142
|
+
const method = requestMethod(input, init)
|
|
143
|
+
const url = requestUrl(input)
|
|
144
|
+
const baseRecord = {
|
|
145
|
+
traceId: context.traceId,
|
|
146
|
+
httpRequestId,
|
|
147
|
+
sessionId: context.sessionId,
|
|
148
|
+
purpose: context.purpose,
|
|
149
|
+
provider: context.provider,
|
|
150
|
+
api: context.api,
|
|
151
|
+
model: context.model,
|
|
152
|
+
method,
|
|
153
|
+
url,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
writeAiHttpRecord({
|
|
157
|
+
...baseRecord,
|
|
158
|
+
type: 'ai_http_request',
|
|
159
|
+
request: {
|
|
160
|
+
method,
|
|
161
|
+
url,
|
|
162
|
+
headers: requestHeaders(input, init),
|
|
163
|
+
body: await readRequestBody(input, init),
|
|
164
|
+
},
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const response = await originalFetch(input, init)
|
|
169
|
+
const durationMs = Date.now() - startedAt
|
|
170
|
+
void logResponseBody(response, { ...baseRecord, durationMs })
|
|
171
|
+
return response
|
|
172
|
+
} catch (error) {
|
|
173
|
+
writeAiHttpRecord({
|
|
174
|
+
...baseRecord,
|
|
175
|
+
type: 'ai_http_error',
|
|
176
|
+
durationMs: Date.now() - startedAt,
|
|
177
|
+
error: error instanceof Error ? error.stack || error.message : String(error),
|
|
178
|
+
})
|
|
179
|
+
throw error
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function installAiHttpLogger() {
|
|
184
|
+
if (!aiHttpLogEnabled || typeof globalThis.fetch !== 'function') return
|
|
185
|
+
if (globalThis[PATCH_MARKER]) return
|
|
186
|
+
|
|
187
|
+
const originalFetch = globalThis.fetch.bind(globalThis)
|
|
188
|
+
globalThis[ORIGINAL_FETCH] = originalFetch
|
|
189
|
+
globalThis.fetch = (input, init) => loggedFetch(originalFetch, input, init)
|
|
190
|
+
globalThis[PATCH_MARKER] = true
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function streamSimpleWithAiHttpLogging(model, context, options = {}) {
|
|
194
|
+
if (!aiHttpLogEnabled) return streamSimple(model, context, options)
|
|
195
|
+
|
|
196
|
+
const traceContext = {
|
|
197
|
+
traceId: randomUUID(),
|
|
198
|
+
sessionId: options?.sessionId,
|
|
199
|
+
purpose: options?.metadata?.quickforgePurpose || 'chat',
|
|
200
|
+
provider: model?.provider,
|
|
201
|
+
api: model?.api,
|
|
202
|
+
model: model?.id,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return aiHttpContext.run(traceContext, () => streamSimple(model, context, options))
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
installAiHttpLogger()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import {
|
|
3
|
+
import { streamSimpleWithAiHttpLogging } from './ai-http-logger.mjs'
|
|
4
4
|
import { cacheDir } from './storage.mjs'
|
|
5
5
|
|
|
6
6
|
export const DEFAULT_COMPACT_KEEP_TURNS = 0
|
|
@@ -253,7 +253,7 @@ export async function compactConversation({ messages, model, thinkingLevel, getA
|
|
|
253
253
|
const modelMaxTokens = Number(model.maxTokens) || 4096
|
|
254
254
|
const maxTokens = Math.max(512, Math.min(modelMaxTokens, 4096))
|
|
255
255
|
const apiKey = getApiKey ? await getApiKey(model.provider) : undefined
|
|
256
|
-
const stream =
|
|
256
|
+
const stream = streamSimpleWithAiHttpLogging(
|
|
257
257
|
model,
|
|
258
258
|
{
|
|
259
259
|
systemPrompt: COMPACT_SYSTEM_PROMPT,
|
|
@@ -266,6 +266,7 @@ export async function compactConversation({ messages, model, thinkingLevel, getA
|
|
|
266
266
|
temperature: 0.2,
|
|
267
267
|
reasoning: thinkingLevel === 'off' ? undefined : 'low',
|
|
268
268
|
maxRetryDelayMs: 60000,
|
|
269
|
+
metadata: { quickforgePurpose: 'compact' },
|
|
269
270
|
},
|
|
270
271
|
)
|
|
271
272
|
|
package/server/index.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import { handleLanAccessApi, renderLanUnlockPage } from './routes/lan-access.mjs
|
|
|
25
25
|
import { handleMcpApi } from './routes/mcp.mjs'
|
|
26
26
|
import { serveStatic } from './routes/static.mjs'
|
|
27
27
|
import { logger, flushLogger } from './utils/logger.mjs'
|
|
28
|
+
import { installAiHttpLogger } from './ai-http-logger.mjs'
|
|
28
29
|
import { isLoopbackAddress, getLanUrls } from './utils/network.mjs'
|
|
29
30
|
import { parseCookies } from './share-store.mjs'
|
|
30
31
|
import { lanAccessCookieName, verifyLanAccessToken } from './lan-access-store.mjs'
|
|
@@ -50,6 +51,7 @@ const vitePort = Number(process.env.QUICKFORGE_VITE_PORT || 5176)
|
|
|
50
51
|
let restartInProgress = false
|
|
51
52
|
|
|
52
53
|
setDefaultWorkspaceRoot(process.env.QUICKFORGE_WORKSPACE_DIR || projectRoot)
|
|
54
|
+
installAiHttpLogger()
|
|
53
55
|
|
|
54
56
|
function getRestartSupport() {
|
|
55
57
|
return { supported: true, reason: null }
|