@xcanwin/manyoyo 5.8.9 → 5.8.10
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/bin/manyoyo.js +24 -66
- package/lib/plugin/playwright.js +1049 -169
- package/lib/web/server.js +2173 -209
- package/package.json +1 -1
- package/lib/plugin/playwright-bootstrap.js +0 -116
- package/lib/plugin/playwright-command-output.js +0 -95
- package/lib/plugin/playwright-container-runtime.js +0 -94
- package/lib/plugin/playwright-extension-manager.js +0 -265
- package/lib/plugin/playwright-extension-paths.js +0 -98
- package/lib/plugin/playwright-host-runtime.js +0 -114
- package/lib/plugin/playwright-scene-config.js +0 -137
- package/lib/plugin/playwright-scene-drivers.js +0 -285
- package/lib/plugin/playwright-scene-state.js +0 -80
- package/lib/web/agent-command.js +0 -153
- package/lib/web/api-route-helpers.js +0 -88
- package/lib/web/container-exec.js +0 -215
- package/lib/web/http-handlers.js +0 -163
- package/lib/web/runtime-state.js +0 -50
- package/lib/web/server-context.js +0 -71
- package/lib/web/server-lifecycle.js +0 -129
- package/lib/web/session-api-routes.js +0 -390
- package/lib/web/structured-output.js +0 -149
- package/lib/web/structured-trace.js +0 -603
- package/lib/web/system-api-routes.js +0 -114
- package/lib/web/terminal-session.js +0 -205
- package/lib/web/upgrade-handler.js +0 -94
package/lib/web/agent-command.js
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const {
|
|
4
|
-
resolveAgentProgram,
|
|
5
|
-
buildAgentResumeCommand
|
|
6
|
-
} = require('../agent-resume');
|
|
7
|
-
|
|
8
|
-
function normalizeAgentPromptCommandTemplate(value, sourceLabel = 'agentPromptCommand') {
|
|
9
|
-
if (value === undefined || value === null) {
|
|
10
|
-
return '';
|
|
11
|
-
}
|
|
12
|
-
if (typeof value !== 'string') {
|
|
13
|
-
throw new Error(`${sourceLabel} 必须是字符串`);
|
|
14
|
-
}
|
|
15
|
-
const text = value.trim();
|
|
16
|
-
if (!text) {
|
|
17
|
-
return '';
|
|
18
|
-
}
|
|
19
|
-
if (!text.includes('{prompt}')) {
|
|
20
|
-
throw new Error(`${sourceLabel} 必须包含 {prompt} 占位符`);
|
|
21
|
-
}
|
|
22
|
-
if (/^codex\s+exec(?:\s|$)/.test(text) && !text.includes('--skip-git-repo-check')) {
|
|
23
|
-
return text.replace(/^codex\s+exec\b/, 'codex exec --skip-git-repo-check');
|
|
24
|
-
}
|
|
25
|
-
return text;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function isAgentPromptCommandEnabled(value) {
|
|
29
|
-
return typeof value === 'string' && value.includes('{prompt}') && Boolean(value.trim());
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function quoteBashSingleValue(value) {
|
|
33
|
-
const text = String(value || '');
|
|
34
|
-
return `'${text.replace(/'/g, `'\"'\"'`)}'`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function renderAgentPromptCommand(template, prompt) {
|
|
38
|
-
const templateText = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
|
|
39
|
-
const safePrompt = quoteBashSingleValue(prompt);
|
|
40
|
-
return templateText.replace(/\{prompt\}/g, safePrompt);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function prependAgentFlags(commandText, matchPattern, flagSpecs) {
|
|
44
|
-
const matched = String(commandText || '').match(matchPattern);
|
|
45
|
-
if (!matched) {
|
|
46
|
-
return String(commandText || '');
|
|
47
|
-
}
|
|
48
|
-
const prefix = matched[1] || '';
|
|
49
|
-
let suffix = matched[matched.length - 1] || '';
|
|
50
|
-
for (let i = flagSpecs.length - 1; i >= 0; i -= 1) {
|
|
51
|
-
const spec = flagSpecs[i];
|
|
52
|
-
if (!spec || !spec.flag || !(spec.pattern instanceof RegExp) || spec.pattern.test(suffix)) {
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
suffix = ` ${spec.flag}${suffix}`;
|
|
56
|
-
}
|
|
57
|
-
return `${prefix}${suffix}`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function buildCodexAgentExecCommand(template, prompt) {
|
|
61
|
-
const templateText = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
|
|
62
|
-
const execMatch = templateText.match(
|
|
63
|
-
/^((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)codex\s+exec\b/
|
|
64
|
-
);
|
|
65
|
-
let codexTemplate = templateText;
|
|
66
|
-
if (execMatch) {
|
|
67
|
-
const prefix = execMatch[1] || '';
|
|
68
|
-
const suffix = templateText.slice(execMatch[0].length);
|
|
69
|
-
const hasJson = /(?:^|\s)--json(?:\s|$)/.test(suffix);
|
|
70
|
-
const injectedFlags = hasJson ? '' : ' --json';
|
|
71
|
-
codexTemplate = `${prefix}codex exec${injectedFlags}${suffix}`;
|
|
72
|
-
}
|
|
73
|
-
return codexTemplate === templateText
|
|
74
|
-
? renderAgentPromptCommand(templateText, prompt)
|
|
75
|
-
: renderAgentPromptCommand(codexTemplate, prompt);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function buildClaudeAgentExecCommand(template, prompt) {
|
|
79
|
-
const templateText = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
|
|
80
|
-
const claudeTemplate = prependAgentFlags(
|
|
81
|
-
templateText,
|
|
82
|
-
/^(((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)claude\b)(.*)$/,
|
|
83
|
-
[
|
|
84
|
-
{ flag: '--verbose', pattern: /(?:^|\s)--verbose(?:\s|$)/ },
|
|
85
|
-
{ flag: '--output-format stream-json', pattern: /(?:^|\s)--output-format(?:\s|$)/ }
|
|
86
|
-
]
|
|
87
|
-
);
|
|
88
|
-
return claudeTemplate === templateText
|
|
89
|
-
? renderAgentPromptCommand(templateText, prompt)
|
|
90
|
-
: renderAgentPromptCommand(claudeTemplate, prompt);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function buildGeminiAgentExecCommand(template, prompt) {
|
|
94
|
-
const templateText = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
|
|
95
|
-
const geminiTemplate = prependAgentFlags(
|
|
96
|
-
templateText,
|
|
97
|
-
/^(((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)gemini\b)(.*)$/,
|
|
98
|
-
[
|
|
99
|
-
{ flag: '--output-format stream-json', pattern: /(?:^|\s)--output-format(?:\s|$)/ }
|
|
100
|
-
]
|
|
101
|
-
);
|
|
102
|
-
return geminiTemplate === templateText
|
|
103
|
-
? renderAgentPromptCommand(templateText, prompt)
|
|
104
|
-
: renderAgentPromptCommand(geminiTemplate, prompt);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function buildOpenCodeAgentExecCommand(template, prompt) {
|
|
108
|
-
const templateText = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
|
|
109
|
-
const opencodeTemplate = prependAgentFlags(
|
|
110
|
-
templateText,
|
|
111
|
-
/^(((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)opencode\s+run\b)(.*)$/,
|
|
112
|
-
[
|
|
113
|
-
{ flag: '--format json', pattern: /(?:^|\s)--format(?:\s|$)/ }
|
|
114
|
-
]
|
|
115
|
-
);
|
|
116
|
-
return opencodeTemplate === templateText
|
|
117
|
-
? renderAgentPromptCommand(templateText, prompt)
|
|
118
|
-
: renderAgentPromptCommand(opencodeTemplate, prompt);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function buildWebAgentExecCommand(template, prompt, agentProgram) {
|
|
122
|
-
switch (agentProgram) {
|
|
123
|
-
case 'claude':
|
|
124
|
-
return buildClaudeAgentExecCommand(template, prompt);
|
|
125
|
-
case 'gemini':
|
|
126
|
-
return buildGeminiAgentExecCommand(template, prompt);
|
|
127
|
-
case 'codex':
|
|
128
|
-
return buildCodexAgentExecCommand(template, prompt);
|
|
129
|
-
case 'opencode':
|
|
130
|
-
return buildOpenCodeAgentExecCommand(template, prompt);
|
|
131
|
-
default:
|
|
132
|
-
break;
|
|
133
|
-
}
|
|
134
|
-
return renderAgentPromptCommand(template, prompt);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function getAgentRuntimeMeta(template) {
|
|
138
|
-
const normalizedTemplate = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
|
|
139
|
-
const agentProgram = resolveAgentProgram(normalizedTemplate);
|
|
140
|
-
const resumeCommand = buildAgentResumeCommand(agentProgram);
|
|
141
|
-
return {
|
|
142
|
-
agentProgram: agentProgram || '',
|
|
143
|
-
resumeCommand: resumeCommand || '',
|
|
144
|
-
resumeSupported: Boolean(resumeCommand)
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
module.exports = {
|
|
149
|
-
normalizeAgentPromptCommandTemplate,
|
|
150
|
-
isAgentPromptCommandEnabled,
|
|
151
|
-
buildWebAgentExecCommand,
|
|
152
|
-
getAgentRuntimeMeta
|
|
153
|
-
};
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
function createApiRouteHelpers(deps) {
|
|
4
|
-
const {
|
|
5
|
-
req,
|
|
6
|
-
res,
|
|
7
|
-
ctx,
|
|
8
|
-
state,
|
|
9
|
-
sendJson,
|
|
10
|
-
readJsonBody,
|
|
11
|
-
getValidSessionRef,
|
|
12
|
-
prepareWebAgentExecution
|
|
13
|
-
} = deps;
|
|
14
|
-
|
|
15
|
-
const withSessionRef = handler => async match => {
|
|
16
|
-
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
17
|
-
if (!sessionRef) {
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
await handler(sessionRef, match);
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const withJsonBody = handler => async (...args) => {
|
|
24
|
-
const payload = await readJsonBody(req);
|
|
25
|
-
await handler(payload, ...args);
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const withSessionJsonBody = (handler, fallbackError = '') => withSessionRef(async (sessionRef, match) => {
|
|
29
|
-
let payload = null;
|
|
30
|
-
if (fallbackError) {
|
|
31
|
-
try {
|
|
32
|
-
payload = await readJsonBody(req);
|
|
33
|
-
} catch (e) {
|
|
34
|
-
sendJson(res, 400, { error: e.message || fallbackError });
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
} else {
|
|
38
|
-
payload = await readJsonBody(req);
|
|
39
|
-
}
|
|
40
|
-
await handler(sessionRef, payload, match);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
const getRequiredBodyText = (payload, key, emptyMessage) => {
|
|
44
|
-
const value = String(payload && payload[key] || '').trim();
|
|
45
|
-
if (!value) {
|
|
46
|
-
sendJson(res, 400, { error: emptyMessage });
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
return value;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const prepareAgentRequest = async (sessionRef, prompt) => {
|
|
53
|
-
try {
|
|
54
|
-
return await prepareWebAgentExecution(ctx, state, sessionRef, prompt);
|
|
55
|
-
} catch (e) {
|
|
56
|
-
sendJson(res, 400, { error: e && e.message ? e.message : 'Agent 执行准备失败' });
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
withSessionRef,
|
|
63
|
-
withJsonBody,
|
|
64
|
-
withSessionJsonBody,
|
|
65
|
-
getRequiredBodyText,
|
|
66
|
-
prepareAgentRequest
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function runMatchedRoute(routes, method, pathname) {
|
|
71
|
-
for (const route of routes) {
|
|
72
|
-
if (route.method !== method) {
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
const matched = route.match(pathname);
|
|
76
|
-
if (!matched) {
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
await route.handler(matched);
|
|
80
|
-
return true;
|
|
81
|
-
}
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
module.exports = {
|
|
86
|
-
createApiRouteHelpers,
|
|
87
|
-
runMatchedRoute
|
|
88
|
-
};
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { spawn } = require('child_process');
|
|
4
|
-
|
|
5
|
-
function createTextBuffer(maxChars) {
|
|
6
|
-
let value = '';
|
|
7
|
-
let truncated = false;
|
|
8
|
-
return {
|
|
9
|
-
append(chunk) {
|
|
10
|
-
if (!chunk) {
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
|
-
const text = chunk.toString('utf-8');
|
|
14
|
-
if (!text) {
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
if (value.length >= maxChars) {
|
|
18
|
-
truncated = true;
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
const remain = maxChars - value.length;
|
|
22
|
-
if (text.length > remain) {
|
|
23
|
-
value += text.slice(0, remain);
|
|
24
|
-
truncated = true;
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
value += text;
|
|
28
|
-
},
|
|
29
|
-
buildOutput(suffix) {
|
|
30
|
-
return truncated ? `${value}\n...${suffix}` : value;
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function drainLines(text, carry, handleLine) {
|
|
36
|
-
let pending = carry + String(text || '');
|
|
37
|
-
let newlineIndex = pending.indexOf('\n');
|
|
38
|
-
while (newlineIndex !== -1) {
|
|
39
|
-
const line = pending.slice(0, newlineIndex).replace(/\r$/, '');
|
|
40
|
-
handleLine(line);
|
|
41
|
-
pending = pending.slice(newlineIndex + 1);
|
|
42
|
-
newlineIndex = pending.indexOf('\n');
|
|
43
|
-
}
|
|
44
|
-
return pending;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function createWebContainerExecHelpers(options = {}) {
|
|
48
|
-
const buildWebSessionKey = options.buildWebSessionKey || (() => '');
|
|
49
|
-
const defaultAgentId = options.defaultAgentId || 'default';
|
|
50
|
-
const extractAgentMessageFromStructuredOutput = options.extractAgentMessageFromStructuredOutput || (() => '');
|
|
51
|
-
const parseJsonObjectLine = options.parseJsonObjectLine || (() => null);
|
|
52
|
-
const prepareStructuredTraceEvents = options.prepareStructuredTraceEvents || (() => []);
|
|
53
|
-
const extractContentDeltaFromPayload = options.extractContentDeltaFromPayload || (() => null);
|
|
54
|
-
const structuredTraceDeps = options.structuredTraceDeps || {};
|
|
55
|
-
const clipText = options.clipText || (text => String(text || ''));
|
|
56
|
-
const stripAnsi = options.stripAnsi || (text => String(text || ''));
|
|
57
|
-
const maxRawOutputChars = Number.isInteger(options.maxRawOutputChars) ? options.maxRawOutputChars : 32 * 1024 * 1024;
|
|
58
|
-
|
|
59
|
-
function buildFinalOutput(agentProgram, stdoutBuffer, stderrBuffer) {
|
|
60
|
-
const clippedStdout = stdoutBuffer.buildOutput('[stdout-truncated]');
|
|
61
|
-
const clippedStderr = stderrBuffer.buildOutput('[stderr-truncated]');
|
|
62
|
-
const clippedRaw = `${clippedStdout}${clippedStdout && clippedStderr ? '\n' : ''}${clippedStderr}`;
|
|
63
|
-
const extractedAgentMessage = extractAgentMessageFromStructuredOutput(agentProgram, clippedStdout);
|
|
64
|
-
const cleanOutputSource = extractedAgentMessage || clippedRaw;
|
|
65
|
-
return clipText(stripAnsi(cleanOutputSource).trim() || '(无输出)');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
async execCommandInWebContainer(ctx, containerName, command, options = {}) {
|
|
70
|
-
const opts = options && typeof options === 'object' ? options : {};
|
|
71
|
-
const agentProgram = typeof opts.agentProgram === 'string' ? opts.agentProgram : '';
|
|
72
|
-
return await new Promise((resolve, reject) => {
|
|
73
|
-
const process = spawn(
|
|
74
|
-
ctx.dockerCmd,
|
|
75
|
-
['exec', containerName, '/bin/bash', '-lc', command],
|
|
76
|
-
{ stdio: ['ignore', 'pipe', 'pipe'] }
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
const stdoutBuffer = createTextBuffer(maxRawOutputChars);
|
|
80
|
-
const stderrBuffer = createTextBuffer(maxRawOutputChars);
|
|
81
|
-
|
|
82
|
-
process.stdout.on('data', chunk => stdoutBuffer.append(chunk));
|
|
83
|
-
process.stderr.on('data', chunk => stderrBuffer.append(chunk));
|
|
84
|
-
|
|
85
|
-
process.on('error', reject);
|
|
86
|
-
process.on('close', code => {
|
|
87
|
-
const exitCode = typeof code === 'number' ? code : 1;
|
|
88
|
-
resolve({
|
|
89
|
-
exitCode,
|
|
90
|
-
output: buildFinalOutput(agentProgram, stdoutBuffer, stderrBuffer)
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
},
|
|
95
|
-
async execAgentInWebContainerStream(ctx, state, sessionRefOrContainerName, command, options = {}) {
|
|
96
|
-
const opts = options && typeof options === 'object' ? options : {};
|
|
97
|
-
const sessionRef = typeof sessionRefOrContainerName === 'string'
|
|
98
|
-
? { containerName: sessionRefOrContainerName, agentId: defaultAgentId }
|
|
99
|
-
: sessionRefOrContainerName;
|
|
100
|
-
const sessionKey = buildWebSessionKey(sessionRef.containerName, sessionRef.agentId);
|
|
101
|
-
const agentProgram = typeof opts.agentProgram === 'string' ? opts.agentProgram : '';
|
|
102
|
-
const onEvent = typeof opts.onEvent === 'function' ? opts.onEvent : () => {};
|
|
103
|
-
const process = spawn(
|
|
104
|
-
ctx.dockerCmd,
|
|
105
|
-
['exec', sessionRef.containerName, '/bin/bash', '-lc', command],
|
|
106
|
-
{ stdio: ['ignore', 'pipe', 'pipe'] }
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
const runState = {
|
|
110
|
-
containerName: sessionRef.containerName,
|
|
111
|
-
sessionKey,
|
|
112
|
-
process,
|
|
113
|
-
command,
|
|
114
|
-
startedAt: new Date().toISOString(),
|
|
115
|
-
stopping: false
|
|
116
|
-
};
|
|
117
|
-
state.agentRuns.set(sessionRef.containerName, runState);
|
|
118
|
-
|
|
119
|
-
return await new Promise((resolve, reject) => {
|
|
120
|
-
const stdoutBuffer = createTextBuffer(maxRawOutputChars);
|
|
121
|
-
const stderrBuffer = createTextBuffer(maxRawOutputChars);
|
|
122
|
-
let stdoutPending = '';
|
|
123
|
-
let stderrPending = '';
|
|
124
|
-
const structuredTraceState = {
|
|
125
|
-
toolNamesById: new Map()
|
|
126
|
-
};
|
|
127
|
-
let contentDeltaAccumulator = '';
|
|
128
|
-
|
|
129
|
-
function emitStdoutTraceLine(line) {
|
|
130
|
-
const rawLine = String(line || '').trim();
|
|
131
|
-
if (!rawLine) {
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
if (agentProgram === 'claude' || agentProgram === 'gemini' || agentProgram === 'codex' || agentProgram === 'opencode') {
|
|
135
|
-
const payload = parseJsonObjectLine(rawLine);
|
|
136
|
-
if (payload) {
|
|
137
|
-
const traceEvents = prepareStructuredTraceEvents(agentProgram, payload, structuredTraceState, structuredTraceDeps);
|
|
138
|
-
traceEvents.forEach(traceEvent => {
|
|
139
|
-
if (!traceEvent || !traceEvent.text) {
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
onEvent({
|
|
143
|
-
type: 'trace',
|
|
144
|
-
stream: 'stdout',
|
|
145
|
-
text: traceEvent.text,
|
|
146
|
-
traceEvent
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
const deltaContent = extractContentDeltaFromPayload(agentProgram, payload, structuredTraceDeps);
|
|
150
|
-
if (deltaContent !== null) {
|
|
151
|
-
if (deltaContent.reset) {
|
|
152
|
-
contentDeltaAccumulator = deltaContent.text;
|
|
153
|
-
} else {
|
|
154
|
-
contentDeltaAccumulator += deltaContent.text;
|
|
155
|
-
}
|
|
156
|
-
onEvent({
|
|
157
|
-
type: 'content_delta',
|
|
158
|
-
content: contentDeltaAccumulator
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
if (agentProgram === 'codex' && (/^OpenAI Codex\b/.test(rawLine) || /^tokens used\b/i.test(rawLine))) {
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
onEvent({ type: 'trace', stream: 'stdout', text: rawLine });
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function emitStderrTraceLine(line) {
|
|
171
|
-
const rawLine = String(line || '').trim();
|
|
172
|
-
if (!rawLine) {
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
onEvent({ type: 'trace', stream: 'stderr', text: `[stderr] ${rawLine}` });
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
process.stdout.on('data', chunk => {
|
|
179
|
-
stdoutBuffer.append(chunk);
|
|
180
|
-
stdoutPending = drainLines(chunk.toString('utf-8'), stdoutPending, emitStdoutTraceLine);
|
|
181
|
-
});
|
|
182
|
-
process.stderr.on('data', chunk => {
|
|
183
|
-
stderrBuffer.append(chunk);
|
|
184
|
-
stderrPending = drainLines(chunk.toString('utf-8'), stderrPending, emitStderrTraceLine);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
process.on('error', error => {
|
|
188
|
-
state.agentRuns.delete(sessionRef.containerName);
|
|
189
|
-
reject(error);
|
|
190
|
-
});
|
|
191
|
-
process.on('close', code => {
|
|
192
|
-
state.agentRuns.delete(sessionRef.containerName);
|
|
193
|
-
if (stdoutPending) {
|
|
194
|
-
emitStdoutTraceLine(stdoutPending);
|
|
195
|
-
stdoutPending = '';
|
|
196
|
-
}
|
|
197
|
-
if (stderrPending) {
|
|
198
|
-
emitStderrTraceLine(stderrPending);
|
|
199
|
-
stderrPending = '';
|
|
200
|
-
}
|
|
201
|
-
const exitCode = typeof code === 'number' ? code : 1;
|
|
202
|
-
resolve({
|
|
203
|
-
exitCode,
|
|
204
|
-
output: buildFinalOutput(agentProgram, stdoutBuffer, stderrBuffer),
|
|
205
|
-
interrupted: exitCode !== 0 && runState.stopping === true
|
|
206
|
-
});
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
module.exports = {
|
|
214
|
-
createWebContainerExecHelpers
|
|
215
|
-
};
|
package/lib/web/http-handlers.js
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
function createWebHttpHandlers(deps) {
|
|
4
|
-
const {
|
|
5
|
-
loadTemplate,
|
|
6
|
-
sendHtml,
|
|
7
|
-
sendJson,
|
|
8
|
-
sendRedirect,
|
|
9
|
-
sendStaticAsset,
|
|
10
|
-
sendVendorAsset,
|
|
11
|
-
readJsonBody,
|
|
12
|
-
secureStringEqual,
|
|
13
|
-
createWebAuthSession,
|
|
14
|
-
clearWebAuthSession,
|
|
15
|
-
getWebAuthCookie,
|
|
16
|
-
getWebAuthClearCookie,
|
|
17
|
-
getWebAuthSession,
|
|
18
|
-
AUTH_FRONTEND_ASSETS,
|
|
19
|
-
APP_FRONTEND_ASSETS,
|
|
20
|
-
APP_VENDOR_ASSETS,
|
|
21
|
-
handleWebApi
|
|
22
|
-
} = deps;
|
|
23
|
-
|
|
24
|
-
function serveAllowedStaticAsset(req, res, pathname, pattern, allowedAssets, sendAsset) {
|
|
25
|
-
const matched = req.method === 'GET' ? pathname.match(pattern) : null;
|
|
26
|
-
if (!matched) {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
const assetName = matched[1];
|
|
30
|
-
if (!allowedAssets.has(assetName)) {
|
|
31
|
-
sendHtml(res, 404, '<h1>404 Not Found</h1>');
|
|
32
|
-
return true;
|
|
33
|
-
}
|
|
34
|
-
sendAsset(res, assetName);
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async function handleWebAuthRoutes(req, res, pathname, ctx, state) {
|
|
39
|
-
if (req.method === 'GET' && pathname === '/favicon.ico') {
|
|
40
|
-
res.writeHead(204, { 'Cache-Control': 'no-store' });
|
|
41
|
-
res.end();
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (req.method === 'GET' && pathname === '/auth/login') {
|
|
46
|
-
sendHtml(res, 200, loadTemplate('login.html'));
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (serveAllowedStaticAsset(req, res, pathname, /^\/auth\/frontend\/([A-Za-z0-9._-]+)$/, AUTH_FRONTEND_ASSETS, sendStaticAsset)) {
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (req.method === 'POST' && pathname === '/auth/login') {
|
|
55
|
-
const payload = await readJsonBody(req);
|
|
56
|
-
const username = String(payload.username || '').trim();
|
|
57
|
-
const password = String(payload.password || '');
|
|
58
|
-
|
|
59
|
-
if (!username || !password) {
|
|
60
|
-
sendJson(res, 400, { error: '用户名和密码不能为空' });
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const userOk = secureStringEqual(username, ctx.authUser);
|
|
65
|
-
const passOk = secureStringEqual(password, ctx.authPass);
|
|
66
|
-
if (!(userOk && passOk)) {
|
|
67
|
-
sendJson(res, 401, { error: '用户名或密码错误' });
|
|
68
|
-
return true;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const sessionId = createWebAuthSession(state, username);
|
|
72
|
-
sendJson(
|
|
73
|
-
res,
|
|
74
|
-
200,
|
|
75
|
-
{ ok: true, username },
|
|
76
|
-
{ 'Set-Cookie': getWebAuthCookie(sessionId) }
|
|
77
|
-
);
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (req.method === 'POST' && pathname === '/auth/logout') {
|
|
82
|
-
clearWebAuthSession(state, req);
|
|
83
|
-
sendJson(
|
|
84
|
-
res,
|
|
85
|
-
200,
|
|
86
|
-
{ ok: true },
|
|
87
|
-
{ 'Set-Cookie': getWebAuthClearCookie() }
|
|
88
|
-
);
|
|
89
|
-
return true;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function sendWebUnauthorized(res, pathname) {
|
|
96
|
-
if (pathname.startsWith('/api/') || pathname.startsWith('/auth/')) {
|
|
97
|
-
sendJson(res, 401, { error: 'UNAUTHORIZED' });
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
if (pathname === '/' || pathname === '') {
|
|
101
|
-
sendRedirect(res, 302, '/auth/login', { 'Set-Cookie': getWebAuthClearCookie() });
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
sendHtml(
|
|
105
|
-
res,
|
|
106
|
-
401,
|
|
107
|
-
loadTemplate('login.html'),
|
|
108
|
-
{ 'Set-Cookie': getWebAuthClearCookie() }
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function handleWebHttpRequest(req, res, pathname, ctx, state) {
|
|
113
|
-
if (await handleWebAuthRoutes(req, res, pathname, ctx, state)) {
|
|
114
|
-
return true;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const authSession = getWebAuthSession(state, req);
|
|
118
|
-
if (!authSession) {
|
|
119
|
-
sendWebUnauthorized(res, pathname);
|
|
120
|
-
return true;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (req.method === 'GET' && pathname === '/') {
|
|
124
|
-
sendHtml(res, 200, loadTemplate('app.html'));
|
|
125
|
-
return true;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (serveAllowedStaticAsset(req, res, pathname, /^\/app\/frontend\/([A-Za-z0-9._-]+)$/, APP_FRONTEND_ASSETS, sendStaticAsset)) {
|
|
129
|
-
return true;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (serveAllowedStaticAsset(req, res, pathname, /^\/app\/vendor\/([A-Za-z0-9._-]+)$/, APP_VENDOR_ASSETS, sendVendorAsset)) {
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (pathname === '/healthz') {
|
|
137
|
-
sendJson(res, 200, { ok: true });
|
|
138
|
-
return true;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (pathname.startsWith('/api/')) {
|
|
142
|
-
const handled = await handleWebApi(req, res, pathname, ctx, state);
|
|
143
|
-
if (!handled) {
|
|
144
|
-
sendJson(res, 404, { error: 'Not Found' });
|
|
145
|
-
}
|
|
146
|
-
return true;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
sendHtml(res, 404, '<h1>404 Not Found</h1>');
|
|
150
|
-
return true;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
serveAllowedStaticAsset,
|
|
155
|
-
handleWebAuthRoutes,
|
|
156
|
-
sendWebUnauthorized,
|
|
157
|
-
handleWebHttpRequest
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
module.exports = {
|
|
162
|
-
createWebHttpHandlers
|
|
163
|
-
};
|
package/lib/web/runtime-state.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
function createWebRuntimeStateHelpers(options = {}) {
|
|
4
|
-
const createMap = () => new Map();
|
|
5
|
-
|
|
6
|
-
return {
|
|
7
|
-
createInitialWebRuntimeState(baseState = {}) {
|
|
8
|
-
return {
|
|
9
|
-
...baseState,
|
|
10
|
-
authSessions: createMap(),
|
|
11
|
-
terminalSessions: createMap(),
|
|
12
|
-
agentRuns: createMap()
|
|
13
|
-
};
|
|
14
|
-
},
|
|
15
|
-
stopWebAgentRun(state, containerName) {
|
|
16
|
-
const runState = state.agentRuns.get(containerName);
|
|
17
|
-
if (!runState || !runState.process || runState.process.killed) {
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
runState.stopping = true;
|
|
21
|
-
try {
|
|
22
|
-
runState.process.kill('SIGTERM');
|
|
23
|
-
} catch {
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
return true;
|
|
27
|
-
},
|
|
28
|
-
cleanupWebRuntimeState(state) {
|
|
29
|
-
for (const session of state.terminalSessions.values()) {
|
|
30
|
-
const ptyProcess = session && session.ptyProcess;
|
|
31
|
-
if (ptyProcess && !ptyProcess.killed) {
|
|
32
|
-
try { ptyProcess.kill('SIGTERM'); } catch {}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
state.terminalSessions.clear();
|
|
36
|
-
|
|
37
|
-
for (const runState of state.agentRuns.values()) {
|
|
38
|
-
const child = runState && runState.process;
|
|
39
|
-
if (child && !child.killed) {
|
|
40
|
-
try { child.kill('SIGTERM'); } catch {}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
state.agentRuns.clear();
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
module.exports = {
|
|
49
|
-
createWebRuntimeStateHelpers
|
|
50
|
-
};
|