@shawnstack/quickforge 1.4.1 → 1.5.1
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 +12 -12
- package/bin/quickforge.mjs +9 -0
- package/dist/assets/AgentProfilesPage-BIwd5Nzg.js +1 -0
- package/dist/assets/ChatPanelHost-De-DMjx5.js +242 -0
- package/dist/assets/PluginsPage-kRzB5k8J.js +1 -0
- package/dist/assets/ScheduledTasksPage-ZnjohaPS.js +2 -0
- package/dist/assets/SharedConversationPage-EQdZgWCM.js +1 -0
- package/dist/assets/TerminalDock-P2pJH_tx.js +2 -0
- package/dist/assets/WorkspaceInspector-CkLAqYQ6.js +3 -0
- package/dist/assets/WorkspaceReaderDialog-BwzZ8Tgv.js +1 -0
- package/dist/assets/diff-line-counts-CeZC7b0z.js +10 -0
- package/dist/assets/icons-DJqt-rnw.js +1 -0
- package/dist/assets/index-CcGy4TXo.js +1354 -0
- package/dist/assets/index-DuTUuAMk.css +3 -0
- package/dist/assets/{monaco-evITXh-m.js → monaco-CNEfYIy1.js} +1 -1
- package/dist/assets/{react-vendor-Mthyt1p4.js → react-vendor-CZCcjpSR.js} +1 -1
- package/dist/favicon.svg +16 -1
- package/dist/index.html +5 -5
- package/dist/manifest.webmanifest +30 -30
- package/package.json +3 -2
- package/server/acp/server.mjs +921 -0
- package/server/agent-manager.mjs +200 -34
- package/server/agent-profile-files.mjs +179 -0
- package/server/agent-profiles.mjs +59 -5
- package/server/auto-compaction.mjs +82 -39
- package/server/channels/process-channel.mjs +278 -0
- package/server/channels/providers/wechat.mjs +271 -0
- package/server/channels/registry.mjs +58 -0
- package/server/custom-commands.mjs +13 -1
- package/server/frontmatter.mjs +167 -0
- package/server/index.mjs +52 -3
- package/server/project-config.mjs +43 -6
- package/server/routes/agent-profiles.mjs +6 -2
- package/server/routes/agent.mjs +12 -1
- package/server/routes/channels.mjs +145 -0
- package/server/routes/models.mjs +68 -0
- package/server/routes/project.mjs +2 -2
- package/server/routes/scheduled-tasks.mjs +6 -5
- package/server/routes/storage.mjs +4 -2
- package/server/routes/system.mjs +27 -0
- package/server/routes/tools.mjs +17 -6
- package/server/routes/workspace.mjs +142 -20
- package/server/session-utils.mjs +10 -2
- package/server/storage.mjs +29 -2
- package/server/system-prompt.mjs +1 -0
- package/server/tools/definitions.mjs +18 -0
- package/server/tools/index.mjs +86 -0
- package/server/utils/package-update.mjs +156 -0
- package/server/utils/workspace.mjs +1 -1
- package/dist/assets/AgentProfilesPage-CNK5PxA3.js +0 -1
- package/dist/assets/ChatPanelHost-FqPQwwMO.js +0 -217
- package/dist/assets/PluginsPage-BCu1Ept0.js +0 -1
- package/dist/assets/ScheduledTasksPage-Bx04rjui.js +0 -2
- package/dist/assets/SharedConversationPage-55vX9sqe.js +0 -1
- package/dist/assets/TerminalDock-DLN_pLkJ.js +0 -2
- package/dist/assets/WorkspaceInspector-DoemHHnY.js +0 -3
- package/dist/assets/WorkspaceReaderDialog-C6xUHBCw.js +0 -6
- package/dist/assets/icons-BWtivFsx.js +0 -1
- package/dist/assets/index-CxOHP41X.css +0 -3
- package/dist/assets/index-Dcf73EL8.js +0 -895
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { EventEmitter } from 'node:events'
|
|
3
|
+
import { setTimeout as delay } from 'node:timers/promises'
|
|
4
|
+
|
|
5
|
+
const DEFAULT_LOG_LIMIT = 300
|
|
6
|
+
const STOP_TIMEOUT_MS = 5000
|
|
7
|
+
|
|
8
|
+
function nowIso() {
|
|
9
|
+
return new Date().toISOString()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normalizeChunk(chunk) {
|
|
13
|
+
return Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function stripAnsi(value) {
|
|
17
|
+
return String(value).replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, '')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function appendLineBuffer(buffer, text, onLine) {
|
|
21
|
+
const combined = buffer + text
|
|
22
|
+
const lines = combined.split(/\r?\n/)
|
|
23
|
+
const nextBuffer = lines.pop() || ''
|
|
24
|
+
for (const line of lines) onLine(line)
|
|
25
|
+
return nextBuffer
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function runDetachedCommand(command, args) {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
const child = spawn(command, args, { windowsHide: true, stdio: 'ignore' })
|
|
31
|
+
child.once('error', () => resolve(false))
|
|
32
|
+
child.once('exit', (code) => resolve(code === 0))
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function terminateProcess(child, signal = 'SIGTERM') {
|
|
37
|
+
if (!child?.pid) return false
|
|
38
|
+
if (process.platform === 'win32') {
|
|
39
|
+
return runDetachedCommand('taskkill.exe', ['/PID', String(child.pid), '/T', signal === 'SIGKILL' ? '/F' : undefined].filter(Boolean))
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
return process.kill(-child.pid, signal)
|
|
43
|
+
} catch {
|
|
44
|
+
try {
|
|
45
|
+
return child.kill(signal)
|
|
46
|
+
} catch {
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class ProcessChannelProvider extends EventEmitter {
|
|
53
|
+
constructor(definition, options = {}) {
|
|
54
|
+
super()
|
|
55
|
+
this.definition = definition
|
|
56
|
+
this.logLimit = options.logLimit || DEFAULT_LOG_LIMIT
|
|
57
|
+
this.status = 'stopped'
|
|
58
|
+
this.process = null
|
|
59
|
+
this.pid = null
|
|
60
|
+
this.startedAt = null
|
|
61
|
+
this.stoppedAt = null
|
|
62
|
+
this.exitCode = null
|
|
63
|
+
this.exitSignal = null
|
|
64
|
+
this.error = null
|
|
65
|
+
this.logs = []
|
|
66
|
+
this.qrCodeUrl = null
|
|
67
|
+
this.qrCodeText = ''
|
|
68
|
+
this.activeAction = null
|
|
69
|
+
this.stopRequested = false
|
|
70
|
+
this.stdoutBuffer = ''
|
|
71
|
+
this.stderrBuffer = ''
|
|
72
|
+
this.startPromise = null
|
|
73
|
+
this.stopPromise = null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
snapshot() {
|
|
77
|
+
return {
|
|
78
|
+
id: this.definition.id,
|
|
79
|
+
name: this.definition.name,
|
|
80
|
+
description: this.definition.description,
|
|
81
|
+
kind: this.definition.kind || 'process',
|
|
82
|
+
provider: this.definition.provider,
|
|
83
|
+
icon: this.definition.icon,
|
|
84
|
+
commandLabel: this.definition.commandLabel,
|
|
85
|
+
supportsWorkspaceSelection: this.definition.supportsWorkspaceSelection === true,
|
|
86
|
+
status: this.status,
|
|
87
|
+
pid: this.pid,
|
|
88
|
+
startedAt: this.startedAt,
|
|
89
|
+
stoppedAt: this.stoppedAt,
|
|
90
|
+
exitCode: this.exitCode,
|
|
91
|
+
exitSignal: this.exitSignal,
|
|
92
|
+
error: this.error,
|
|
93
|
+
logs: this.logs,
|
|
94
|
+
qrCodeUrl: this.qrCodeUrl,
|
|
95
|
+
qrCodeText: this.qrCodeText,
|
|
96
|
+
actions: this.definition.actions || [],
|
|
97
|
+
requirements: this.definition.requirements || [],
|
|
98
|
+
activeAction: this.activeAction,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
emitEvent(type, payload = {}) {
|
|
103
|
+
const event = {
|
|
104
|
+
type,
|
|
105
|
+
channelId: this.definition.id,
|
|
106
|
+
timestamp: nowIso(),
|
|
107
|
+
...payload,
|
|
108
|
+
}
|
|
109
|
+
this.emit('event', event)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
setStatus(status, extra = {}) {
|
|
113
|
+
this.status = status
|
|
114
|
+
if (Object.prototype.hasOwnProperty.call(extra, 'error')) {
|
|
115
|
+
this.error = extra.error
|
|
116
|
+
}
|
|
117
|
+
this.emitEvent('status', { status, snapshot: this.snapshot() })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
addLog(stream, text) {
|
|
121
|
+
if (!text) return
|
|
122
|
+
const entry = {
|
|
123
|
+
id: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
|
|
124
|
+
time: nowIso(),
|
|
125
|
+
stream,
|
|
126
|
+
text,
|
|
127
|
+
}
|
|
128
|
+
this.logs.push(entry)
|
|
129
|
+
if (this.logs.length > this.logLimit) {
|
|
130
|
+
this.logs.splice(0, this.logs.length - this.logLimit)
|
|
131
|
+
}
|
|
132
|
+
this.emitEvent('log', { log: entry })
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
addProcessText(stream, text) {
|
|
136
|
+
const cleanText = stripAnsi(text).replace(/\r/g, '\n')
|
|
137
|
+
if (!cleanText) return
|
|
138
|
+
this.addLog(stream, cleanText)
|
|
139
|
+
this.inspectOutput(cleanText, stream)
|
|
140
|
+
|
|
141
|
+
const lineHandler = (line) => this.inspectOutputLine(line, stream)
|
|
142
|
+
if (stream === 'stderr') {
|
|
143
|
+
this.stderrBuffer = appendLineBuffer(this.stderrBuffer, cleanText, lineHandler)
|
|
144
|
+
} else {
|
|
145
|
+
this.stdoutBuffer = appendLineBuffer(this.stdoutBuffer, cleanText, lineHandler)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
inspectOutput(_text, _stream) {
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
inspectOutputLine(_line, _stream) {
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
buildStartCommand() {
|
|
156
|
+
throw new Error('buildStartCommand() is not implemented')
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async beforeStart(_options = {}) {}
|
|
160
|
+
|
|
161
|
+
async start(options = {}) {
|
|
162
|
+
if (this.process) return this.snapshot()
|
|
163
|
+
if (this.startPromise) return this.startPromise
|
|
164
|
+
if (this.status === 'stopping') return this.snapshot()
|
|
165
|
+
|
|
166
|
+
this.startPromise = this.#startProcess(options)
|
|
167
|
+
try {
|
|
168
|
+
return await this.startPromise
|
|
169
|
+
} finally {
|
|
170
|
+
this.startPromise = null
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async #startProcess(options = {}) {
|
|
175
|
+
this.stopRequested = false
|
|
176
|
+
this.exitCode = null
|
|
177
|
+
this.exitSignal = null
|
|
178
|
+
this.error = null
|
|
179
|
+
this.qrCodeUrl = null
|
|
180
|
+
this.qrCodeText = ''
|
|
181
|
+
this.stdoutBuffer = ''
|
|
182
|
+
this.stderrBuffer = ''
|
|
183
|
+
this.startedAt = nowIso()
|
|
184
|
+
this.stoppedAt = null
|
|
185
|
+
this.setStatus('starting')
|
|
186
|
+
|
|
187
|
+
await this.beforeStart(options)
|
|
188
|
+
if (this.process) return this.snapshot()
|
|
189
|
+
|
|
190
|
+
const commandSpec = this.buildStartCommand(options)
|
|
191
|
+
|
|
192
|
+
const child = spawn(commandSpec.command, commandSpec.args || [], {
|
|
193
|
+
cwd: commandSpec.cwd,
|
|
194
|
+
env: commandSpec.env || process.env,
|
|
195
|
+
windowsHide: true,
|
|
196
|
+
detached: process.platform !== 'win32',
|
|
197
|
+
shell: commandSpec.shell === true,
|
|
198
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
this.process = child
|
|
202
|
+
this.pid = child.pid || null
|
|
203
|
+
this.emitEvent('status', { status: this.status, snapshot: this.snapshot() })
|
|
204
|
+
|
|
205
|
+
child.stdout?.on('data', (chunk) => this.addProcessText('stdout', normalizeChunk(chunk)))
|
|
206
|
+
child.stderr?.on('data', (chunk) => this.addProcessText('stderr', normalizeChunk(chunk)))
|
|
207
|
+
|
|
208
|
+
child.once('error', (error) => {
|
|
209
|
+
this.error = error.message || String(error)
|
|
210
|
+
this.addLog('stderr', this.error)
|
|
211
|
+
this.process = null
|
|
212
|
+
this.pid = null
|
|
213
|
+
this.stoppedAt = nowIso()
|
|
214
|
+
this.setStatus('error', { error: this.error })
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
child.once('exit', (code, signal) => {
|
|
218
|
+
this.process = null
|
|
219
|
+
this.pid = null
|
|
220
|
+
this.exitCode = code
|
|
221
|
+
this.exitSignal = signal
|
|
222
|
+
this.stoppedAt = nowIso()
|
|
223
|
+
if (this.stopRequested || code === 0) {
|
|
224
|
+
this.setStatus('stopped')
|
|
225
|
+
} else {
|
|
226
|
+
const reason = `Channel process exited with code ${code ?? 'null'}${signal ? ` signal ${signal}` : ''}`
|
|
227
|
+
this.error = reason
|
|
228
|
+
this.setStatus('error', { error: reason })
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
return this.snapshot()
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async stop() {
|
|
236
|
+
if (this.stopPromise) return this.stopPromise
|
|
237
|
+
this.stopPromise = this.#stopProcess()
|
|
238
|
+
try {
|
|
239
|
+
return await this.stopPromise
|
|
240
|
+
} finally {
|
|
241
|
+
this.stopPromise = null
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async #stopProcess() {
|
|
246
|
+
if (!this.process) {
|
|
247
|
+
this.setStatus('stopped')
|
|
248
|
+
return this.snapshot()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this.stopRequested = true
|
|
252
|
+
this.setStatus('stopping')
|
|
253
|
+
const child = this.process
|
|
254
|
+
await terminateProcess(child, 'SIGTERM')
|
|
255
|
+
|
|
256
|
+
const exited = await Promise.race([
|
|
257
|
+
new Promise((resolve) => child.once('exit', () => resolve(true))),
|
|
258
|
+
delay(STOP_TIMEOUT_MS).then(() => false),
|
|
259
|
+
])
|
|
260
|
+
|
|
261
|
+
if (!exited && this.process === child) {
|
|
262
|
+
await terminateProcess(child, 'SIGKILL')
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return this.snapshot()
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async restart(options = {}) {
|
|
269
|
+
await this.stop()
|
|
270
|
+
return this.start(options)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async runAction(_action, _options = {}) {
|
|
274
|
+
const error = new Error('Unsupported channel action')
|
|
275
|
+
error.statusCode = 404
|
|
276
|
+
throw error
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { promises as fs } from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { ProcessChannelProvider } from '../process-channel.mjs'
|
|
5
|
+
import { getDefaultWorkspaceRoot, readProjectConfig } from '../../project-config.mjs'
|
|
6
|
+
|
|
7
|
+
const WEIXIN_ACP_PACKAGE = 'weixin-acp'
|
|
8
|
+
const MIN_NODE_MAJOR = 22
|
|
9
|
+
const ACTION_TIMEOUT_MS = 120_000
|
|
10
|
+
const DEFAULT_WORKSPACE_ID = 'default'
|
|
11
|
+
|
|
12
|
+
function npxCommand() {
|
|
13
|
+
return process.platform === 'win32' ? 'npx.cmd' : 'npx'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function shouldUseShellForNpx() {
|
|
17
|
+
return process.platform === 'win32'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function nodeMajor() {
|
|
21
|
+
return Number(process.versions.node.split('.')[0] || 0)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function qfAcpCommand(projectRoot) {
|
|
25
|
+
const nodeCommand = process.platform === 'win32' ? 'node' : process.execPath
|
|
26
|
+
return [nodeCommand, path.join(projectRoot, 'bin', 'quickforge.mjs'), 'acp']
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function workspaceSummary(id, name, workspacePath, kind) {
|
|
30
|
+
return {
|
|
31
|
+
id,
|
|
32
|
+
name,
|
|
33
|
+
path: workspacePath,
|
|
34
|
+
kind,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function assertDirectoryExists(workspacePath) {
|
|
39
|
+
try {
|
|
40
|
+
const stat = await fs.stat(workspacePath)
|
|
41
|
+
if (stat.isDirectory()) return
|
|
42
|
+
} catch {
|
|
43
|
+
// Report a clear channel action error below.
|
|
44
|
+
}
|
|
45
|
+
const error = new Error(`Workspace is not a directory: ${workspacePath}`)
|
|
46
|
+
error.statusCode = 400
|
|
47
|
+
throw error
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function resolveLaunchWorkspace(options = {}) {
|
|
51
|
+
const requestedId = typeof options.projectId === 'string' ? options.projectId : DEFAULT_WORKSPACE_ID
|
|
52
|
+
if (!requestedId || requestedId === DEFAULT_WORKSPACE_ID) {
|
|
53
|
+
const workspacePath = getDefaultWorkspaceRoot()
|
|
54
|
+
await assertDirectoryExists(workspacePath)
|
|
55
|
+
return workspaceSummary(DEFAULT_WORKSPACE_ID, 'workspace', workspacePath, 'default')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const config = await readProjectConfig()
|
|
59
|
+
const project = config.projects.find((item) => item.id === requestedId)
|
|
60
|
+
if (!project) {
|
|
61
|
+
const error = new Error(`Unknown project: ${requestedId}`)
|
|
62
|
+
error.statusCode = 404
|
|
63
|
+
throw error
|
|
64
|
+
}
|
|
65
|
+
await assertDirectoryExists(project.path)
|
|
66
|
+
return workspaceSummary(project.id, project.name, project.path, 'project')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function extractUrl(text) {
|
|
70
|
+
const matches = String(text).match(/https?:\/\/[^\s\u001b]+/g)
|
|
71
|
+
if (!matches?.length) return null
|
|
72
|
+
return matches[matches.length - 1].replace(/[),,。]+$/u, '')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function looksLikeTerminalQr(text) {
|
|
76
|
+
return /[█▀▄]{6,}/u.test(text) || /[▄▀]{6,}/u.test(text)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function runCommand(command, args, options = {}) {
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
const child = spawn(command, args, {
|
|
82
|
+
cwd: options.cwd,
|
|
83
|
+
env: options.env || process.env,
|
|
84
|
+
windowsHide: true,
|
|
85
|
+
shell: options.shell === true,
|
|
86
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
87
|
+
})
|
|
88
|
+
let stdout = ''
|
|
89
|
+
let stderr = ''
|
|
90
|
+
let settled = false
|
|
91
|
+
const timer = setTimeout(() => {
|
|
92
|
+
if (settled) return
|
|
93
|
+
child.kill('SIGTERM')
|
|
94
|
+
settled = true
|
|
95
|
+
const error = new Error(`Command timed out after ${options.timeoutMs || ACTION_TIMEOUT_MS}ms`)
|
|
96
|
+
error.stdout = stdout
|
|
97
|
+
error.stderr = stderr
|
|
98
|
+
reject(error)
|
|
99
|
+
}, options.timeoutMs || ACTION_TIMEOUT_MS)
|
|
100
|
+
|
|
101
|
+
child.stdout?.on('data', (chunk) => {
|
|
102
|
+
const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk)
|
|
103
|
+
stdout += text
|
|
104
|
+
options.onStdout?.(text)
|
|
105
|
+
})
|
|
106
|
+
child.stderr?.on('data', (chunk) => {
|
|
107
|
+
const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk)
|
|
108
|
+
stderr += text
|
|
109
|
+
options.onStderr?.(text)
|
|
110
|
+
})
|
|
111
|
+
child.once('error', (error) => {
|
|
112
|
+
if (settled) return
|
|
113
|
+
settled = true
|
|
114
|
+
clearTimeout(timer)
|
|
115
|
+
reject(error)
|
|
116
|
+
})
|
|
117
|
+
child.once('exit', (code, signal) => {
|
|
118
|
+
if (settled) return
|
|
119
|
+
settled = true
|
|
120
|
+
clearTimeout(timer)
|
|
121
|
+
if (code === 0) {
|
|
122
|
+
resolve({ stdout, stderr, code, signal })
|
|
123
|
+
} else {
|
|
124
|
+
const error = new Error(`Command exited with code ${code ?? 'null'}${signal ? ` signal ${signal}` : ''}`)
|
|
125
|
+
error.stdout = stdout
|
|
126
|
+
error.stderr = stderr
|
|
127
|
+
error.code = code
|
|
128
|
+
error.signal = signal
|
|
129
|
+
reject(error)
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export class WechatChannelProvider extends ProcessChannelProvider {
|
|
136
|
+
constructor({ projectRoot }) {
|
|
137
|
+
super({
|
|
138
|
+
id: 'wechat',
|
|
139
|
+
name: '微信',
|
|
140
|
+
description: '通过 weixin-acp 将微信消息接入 QuickForge ACP Agent。',
|
|
141
|
+
kind: 'process',
|
|
142
|
+
provider: 'weixin-acp',
|
|
143
|
+
icon: 'wechat',
|
|
144
|
+
commandLabel: 'npx weixin-acp start -- qf acp',
|
|
145
|
+
supportsWorkspaceSelection: true,
|
|
146
|
+
actions: [
|
|
147
|
+
{ id: 'logout', label: '退出登录', destructive: true },
|
|
148
|
+
{ id: 'relogin', label: '重新登录' },
|
|
149
|
+
],
|
|
150
|
+
requirements: [`Node.js >= ${MIN_NODE_MAJOR}`, 'npm/npx 可用', '首次启动需要微信扫码登录'],
|
|
151
|
+
})
|
|
152
|
+
this.projectRoot = projectRoot
|
|
153
|
+
this.launchWorkspace = null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
snapshot() {
|
|
157
|
+
return {
|
|
158
|
+
...super.snapshot(),
|
|
159
|
+
launchWorkspace: this.launchWorkspace,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async beforeStart(options = {}) {
|
|
164
|
+
if (nodeMajor() < MIN_NODE_MAJOR) {
|
|
165
|
+
const message = `微信渠道需要 Node.js >= ${MIN_NODE_MAJOR},当前版本是 ${process.versions.node}。请升级 Node 后重试。`
|
|
166
|
+
this.error = message
|
|
167
|
+
this.setStatus('error', { error: message })
|
|
168
|
+
const error = new Error(message)
|
|
169
|
+
error.statusCode = 409
|
|
170
|
+
throw error
|
|
171
|
+
}
|
|
172
|
+
this.launchWorkspace = await resolveLaunchWorkspace(options)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
buildStartCommand() {
|
|
176
|
+
const [acpCommand, ...acpArgs] = qfAcpCommand(this.projectRoot)
|
|
177
|
+
return {
|
|
178
|
+
command: npxCommand(),
|
|
179
|
+
args: ['-y', WEIXIN_ACP_PACKAGE, 'start', '--', acpCommand, ...acpArgs],
|
|
180
|
+
cwd: this.launchWorkspace?.path || getDefaultWorkspaceRoot() || this.projectRoot,
|
|
181
|
+
env: process.env,
|
|
182
|
+
shell: shouldUseShellForNpx(),
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
inspectOutput(text) {
|
|
187
|
+
const url = extractUrl(text)
|
|
188
|
+
if (url) {
|
|
189
|
+
this.qrCodeUrl = url
|
|
190
|
+
if (this.status === 'starting') this.setStatus('waiting_scan')
|
|
191
|
+
this.emitEvent('qrcode', { qrCodeUrl: this.qrCodeUrl, qrCodeText: this.qrCodeText, snapshot: this.snapshot() })
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (looksLikeTerminalQr(text)) {
|
|
195
|
+
const nextText = `${this.qrCodeText}\n${text}`.trim()
|
|
196
|
+
this.qrCodeText = nextText.length > 8000 ? nextText.slice(-8000) : nextText
|
|
197
|
+
if (this.status === 'starting') this.setStatus('waiting_scan')
|
|
198
|
+
this.emitEvent('qrcode', { qrCodeUrl: this.qrCodeUrl, qrCodeText: this.qrCodeText, snapshot: this.snapshot() })
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (/扫码|二维码|等待扫码|scan/i.test(text) && this.status === 'starting') {
|
|
202
|
+
this.setStatus('waiting_scan')
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (/与微信连接成功|启动 bot|\[weixin\] 启动 bot|connected|login success/i.test(text)) {
|
|
206
|
+
this.qrCodeText = ''
|
|
207
|
+
this.setStatus('running')
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
inspectOutputLine(line) {
|
|
212
|
+
if (/与微信连接成功|启动 bot|\[weixin\] 启动 bot|connected|login success/i.test(line)) {
|
|
213
|
+
this.qrCodeText = ''
|
|
214
|
+
this.setStatus('running')
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async runAction(action, options = {}) {
|
|
219
|
+
if (action === 'logout') return this.logout()
|
|
220
|
+
if (action === 'relogin') return this.relogin(options)
|
|
221
|
+
return super.runAction(action, options)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async logout({ preserveActiveAction = false } = {}) {
|
|
225
|
+
if (this.activeAction && !preserveActiveAction) return this.snapshot()
|
|
226
|
+
if (!preserveActiveAction) this.activeAction = 'logout'
|
|
227
|
+
this.emitEvent('status', { status: this.status, snapshot: this.snapshot() })
|
|
228
|
+
try {
|
|
229
|
+
if (this.process) await this.stop()
|
|
230
|
+
this.addLog('system', 'Running: npx weixin-acp logout')
|
|
231
|
+
await runCommand(npxCommand(), ['-y', WEIXIN_ACP_PACKAGE, 'logout'], {
|
|
232
|
+
cwd: this.projectRoot,
|
|
233
|
+
env: process.env,
|
|
234
|
+
shell: shouldUseShellForNpx(),
|
|
235
|
+
onStdout: (text) => this.addProcessText('stdout', text),
|
|
236
|
+
onStderr: (text) => this.addProcessText('stderr', text),
|
|
237
|
+
})
|
|
238
|
+
this.qrCodeUrl = null
|
|
239
|
+
this.qrCodeText = ''
|
|
240
|
+
this.error = null
|
|
241
|
+
this.setStatus('stopped')
|
|
242
|
+
return this.snapshot()
|
|
243
|
+
} catch (error) {
|
|
244
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
245
|
+
this.error = message
|
|
246
|
+
this.addLog('stderr', message)
|
|
247
|
+
this.setStatus('error', { error: message })
|
|
248
|
+
throw error
|
|
249
|
+
} finally {
|
|
250
|
+
if (!preserveActiveAction) this.activeAction = null
|
|
251
|
+
this.emitEvent('status', { status: this.status, snapshot: this.snapshot() })
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async relogin(options = {}) {
|
|
256
|
+
if (this.activeAction) return this.snapshot()
|
|
257
|
+
this.activeAction = 'relogin'
|
|
258
|
+
this.emitEvent('status', { status: this.status, snapshot: this.snapshot() })
|
|
259
|
+
try {
|
|
260
|
+
await this.logout({ preserveActiveAction: true })
|
|
261
|
+
return await this.start({ projectId: options.projectId || this.launchWorkspace?.id || DEFAULT_WORKSPACE_ID })
|
|
262
|
+
} finally {
|
|
263
|
+
this.activeAction = null
|
|
264
|
+
this.emitEvent('status', { status: this.status, snapshot: this.snapshot() })
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function createWechatChannelProvider(options) {
|
|
270
|
+
return new WechatChannelProvider(options)
|
|
271
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events'
|
|
2
|
+
import { createWechatChannelProvider } from './providers/wechat.mjs'
|
|
3
|
+
|
|
4
|
+
export const channelEvents = new EventEmitter()
|
|
5
|
+
|
|
6
|
+
const providers = new Map()
|
|
7
|
+
let initialized = false
|
|
8
|
+
|
|
9
|
+
function attachProvider(provider) {
|
|
10
|
+
providers.set(provider.definition.id, provider)
|
|
11
|
+
provider.on('event', (event) => {
|
|
12
|
+
channelEvents.emit('channel_event', event)
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function initializeChannels(options = {}) {
|
|
17
|
+
if (initialized) return
|
|
18
|
+
initialized = true
|
|
19
|
+
attachProvider(createWechatChannelProvider({ projectRoot: options.projectRoot }))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function requireProvider(id) {
|
|
23
|
+
const provider = providers.get(id)
|
|
24
|
+
if (!provider) {
|
|
25
|
+
const error = new Error(`Unknown channel: ${id}`)
|
|
26
|
+
error.statusCode = 404
|
|
27
|
+
throw error
|
|
28
|
+
}
|
|
29
|
+
return provider
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function listChannels() {
|
|
33
|
+
return Array.from(providers.values()).map((provider) => provider.snapshot())
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getChannelStatus(id) {
|
|
37
|
+
return requireProvider(id).snapshot()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function startChannel(id, options = {}) {
|
|
41
|
+
return requireProvider(id).start(options)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function stopChannel(id) {
|
|
45
|
+
return requireProvider(id).stop()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function restartChannel(id, options = {}) {
|
|
49
|
+
return requireProvider(id).restart(options)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function runChannelAction(id, action, options = {}) {
|
|
53
|
+
return requireProvider(id).runAction(action, options)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function shutdownChannels() {
|
|
57
|
+
await Promise.allSettled(Array.from(providers.values()).map((provider) => provider.stop()))
|
|
58
|
+
}
|
|
@@ -26,9 +26,14 @@ const builtinCommandCatalog = [
|
|
|
26
26
|
argumentHint: '[scope]',
|
|
27
27
|
permissionNote: 'no edits',
|
|
28
28
|
},
|
|
29
|
+
{
|
|
30
|
+
name: 'summary',
|
|
31
|
+
description: 'Create a new chat with this conversation summarized to reduce context usage.',
|
|
32
|
+
argumentHint: '',
|
|
33
|
+
},
|
|
29
34
|
{
|
|
30
35
|
name: 'compact',
|
|
31
|
-
description: '
|
|
36
|
+
description: 'Compact this conversation context in place using the same rolling summary as auto-compaction.',
|
|
32
37
|
argumentHint: '',
|
|
33
38
|
},
|
|
34
39
|
{
|
|
@@ -373,6 +378,9 @@ export function parseInternalCommandInvocation(message) {
|
|
|
373
378
|
const compactMatch = text.match(/^\/compact(?:\s+([\s\S]*))?$/i)
|
|
374
379
|
if (compactMatch) return { type: 'compact', args: (compactMatch[1] || '').trim() }
|
|
375
380
|
|
|
381
|
+
const summaryMatch = text.match(/^\/summary(?:\s+([\s\S]*))?$/i)
|
|
382
|
+
if (summaryMatch) return { type: 'summary', args: (summaryMatch[1] || '').trim() }
|
|
383
|
+
|
|
376
384
|
const createMatch = text.match(/^\/command\s+new\s+([A-Za-z0-9][A-Za-z0-9-]*)\s*$/i)
|
|
377
385
|
if (createMatch) {
|
|
378
386
|
const name = normalizeCommandName(createMatch[1])
|
|
@@ -389,6 +397,10 @@ export async function handleInternalCommand(invocation, workspaceRoot, commandDi
|
|
|
389
397
|
return { compact: true, args: invocation.args || '' }
|
|
390
398
|
}
|
|
391
399
|
|
|
400
|
+
if (invocation.type === 'summary') {
|
|
401
|
+
return { summary: true, args: invocation.args || '' }
|
|
402
|
+
}
|
|
403
|
+
|
|
392
404
|
if (invocation.type === 'plan') {
|
|
393
405
|
if (!invocation.args) return 'Usage: /plan <task>'
|
|
394
406
|
return { plan: true, args: invocation.args }
|