@xcanwin/manyoyo 8.4.0 → 8.5.0
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 +2 -0
- package/lib/browser-background.js +23 -0
- package/lib/container-env.sh +34 -0
- package/lib/container-id.js +11 -0
- package/lib/container-init.sh +6 -25
- package/lib/container-network.js +101 -12
- package/lib/container-run.js +3 -0
- package/lib/container-state.js +5 -26
- package/lib/egress-denied.js +128 -0
- package/lib/egress-proxy.js +78 -61
- package/lib/egress-sidecar-main.js +112 -0
- package/lib/egress-sidecar.js +276 -0
- package/lib/json5-text-edit.js +5 -5
- package/lib/network-policy.js +5 -1
- package/lib/port-forward.js +5 -3
- package/lib/web/AGENTS.md +1 -1
- package/lib/web/container-manage.js +56 -0
- package/lib/web/denied-notify.js +52 -0
- package/lib/web/index.html +44 -44
- package/lib/web/server.js +100 -110
- package/package.json +1 -1
package/lib/web/server.js
CHANGED
|
@@ -13,11 +13,15 @@ const { buildContainerRunArgs, hasNetworkArg } = require('../container-run');
|
|
|
13
13
|
const { buildContainerIntegration, mergeIntegration } = require('../plugin');
|
|
14
14
|
const { applyAptMirror } = require('../mirrors');
|
|
15
15
|
const containerState = require('../container-state');
|
|
16
|
-
const { buildExecArgs } = require('../container-exec');
|
|
16
|
+
const { buildExecArgs, resolveContainerId } = require('../container-exec');
|
|
17
17
|
const { createNetworkManager, NETWORK_NAME } = require('../container-network');
|
|
18
18
|
const { normalizePolicy } = require('../network-policy');
|
|
19
19
|
const { withEnvEndpoints } = require('../network-endpoints');
|
|
20
|
-
const {
|
|
20
|
+
const { egressPaths, isSidecarName } = require('../egress-sidecar');
|
|
21
|
+
const { read: readDenied } = require('../egress-denied');
|
|
22
|
+
const { isBrowserBackground } = require('../browser-background');
|
|
23
|
+
const { createDeniedNotifier } = require('./denied-notify');
|
|
24
|
+
const { domainAllowed } = require('../network-policy');
|
|
21
25
|
const { createPortForwarder } = require('../port-forward');
|
|
22
26
|
const { createContainerManageRoutes, pendingRisks } = require('./container-manage');
|
|
23
27
|
const { resolveManageOptions } = require('../container-manage-options');
|
|
@@ -3156,7 +3160,7 @@ function listWebManyoyoContainers(ctx) {
|
|
|
3156
3160
|
const candidates = [];
|
|
3157
3161
|
output.trim().split('\n').forEach(line => {
|
|
3158
3162
|
const [name, status, image] = line.split('\t');
|
|
3159
|
-
if (!ctx.isValidContainerName(name)) {
|
|
3163
|
+
if (!ctx.isValidContainerName(name) || isSidecarName(name)) {
|
|
3160
3164
|
return;
|
|
3161
3165
|
}
|
|
3162
3166
|
const imageName = image || '';
|
|
@@ -3324,55 +3328,23 @@ async function cleanupStaleSetupTestContainers(ctx) {
|
|
|
3324
3328
|
}
|
|
3325
3329
|
}
|
|
3326
3330
|
|
|
3327
|
-
//
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
function probeEgressProxy(port) {
|
|
3331
|
-
return new Promise(resolve => {
|
|
3332
|
-
const req = http.request({ host: '127.0.0.1', port, method: 'GET', path: 'http://manyoyo.invalid/', timeout: 3000 }, res => {
|
|
3333
|
-
res.resume();
|
|
3334
|
-
resolve(res.statusCode === 407 && String(res.headers['proxy-authenticate'] || '').includes(EGRESS_REALM));
|
|
3335
|
-
});
|
|
3336
|
-
req.on('error', () => resolve(false));
|
|
3337
|
-
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
3338
|
-
req.end();
|
|
3339
|
-
});
|
|
3340
|
-
}
|
|
3341
|
-
|
|
3342
|
-
// 过滤代理(域名白名单的强制点):有 allowlist 容器时才启动;同机另一个 serve 已经提供就复用它
|
|
3343
|
-
function ensureWebEgressProxy(ctx, state) {
|
|
3344
|
-
if (!state.egressProxyPromise) {
|
|
3331
|
+
// Agent 一轮对话里,容器被过滤代理拦下的新域名(浏览器后台请求不算):只在 allowlist 容器上轮询
|
|
3332
|
+
function createWebDeniedNotifier(ctx, sessionRef, emit) {
|
|
3333
|
+
try {
|
|
3345
3334
|
const homeDir = ctx.homeDir || os.homedir();
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
const
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
const proxy = createEgressProxy({
|
|
3356
|
-
upstream: process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy || '',
|
|
3357
|
-
getToken: id => cached(`t:${id}`, () => containerState.readEgressToken(homeDir, id)),
|
|
3358
|
-
getPolicy: id => cached(`p:${id}`, () => {
|
|
3359
|
-
try {
|
|
3360
|
-
const policy = normalizePolicy(containerState.readNetworkRaw(homeDir, id) || undefined);
|
|
3361
|
-
return { preset: policy.preset, domains: policy.egress.domains, rules: policy.egress.rules };
|
|
3362
|
-
} catch (e) {
|
|
3363
|
-
return null;
|
|
3364
|
-
}
|
|
3365
|
-
})
|
|
3335
|
+
const id = resolveContainerId(ctx.dockerExecArgs, sessionRef.containerName);
|
|
3336
|
+
if (!id || !containerState.stateExists(homeDir, id)) return null;
|
|
3337
|
+
const deniedDir = egressPaths(homeDir).denied;
|
|
3338
|
+
const policyOf = () => normalizePolicy(containerState.readNetworkRaw(homeDir, id) || undefined);
|
|
3339
|
+
if (policyOf().preset !== 'allowlist') return null;
|
|
3340
|
+
return createDeniedNotifier({
|
|
3341
|
+
read: () => readDenied(deniedDir, id),
|
|
3342
|
+
isIgnored: host => isBrowserBackground(host) || domainAllowed(policyOf().egress.domains, host),
|
|
3343
|
+
emit
|
|
3366
3344
|
});
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
.catch(async error => {
|
|
3370
|
-
if (error.code === 'EADDRINUSE' && await probeEgressProxy(EGRESS_PROXY_PORT)) return { port: EGRESS_PROXY_PORT };
|
|
3371
|
-
state.egressProxyPromise = null;
|
|
3372
|
-
throw new Error(error.code === 'EADDRINUSE' ? `过滤代理端口 ${EGRESS_PROXY_PORT} 被其他程序占用` : error.message);
|
|
3373
|
-
});
|
|
3345
|
+
} catch (e) {
|
|
3346
|
+
return null;
|
|
3374
3347
|
}
|
|
3375
|
-
return state.egressProxyPromise;
|
|
3376
3348
|
}
|
|
3377
3349
|
|
|
3378
3350
|
function getWebPortForwarder(ctx, state) {
|
|
@@ -3396,7 +3368,14 @@ async function startContainerServices(ctx, state) {
|
|
|
3396
3368
|
const manager = getWebNetworkManager(ctx, state);
|
|
3397
3369
|
const homeDir = ctx.homeDir || os.homedir();
|
|
3398
3370
|
sweepStaleExecEnvFiles(homeDir);
|
|
3399
|
-
|
|
3371
|
+
const reconcile = () => reconcileContainerServices(ctx, state, manager);
|
|
3372
|
+
state.networkWatch = manager.watchStarts({ onReconnect: reconcile });
|
|
3373
|
+
await reconcile();
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
// 拉起 autostartOnServe 的容器并恢复端口暴露:serve 启动时做一次,运行时重启后(事件流重连)再做一次,幂等
|
|
3377
|
+
async function reconcileContainerServices(ctx, state, manager) {
|
|
3378
|
+
const homeDir = ctx.homeDir || os.homedir();
|
|
3400
3379
|
const forwarder = getWebPortForwarder(ctx, state);
|
|
3401
3380
|
// 以运行时里真实存在的容器为准(按 id 对应名字);状态目录里有、容器却没有的是孤儿,既不拉起也不转发
|
|
3402
3381
|
state.containerNames = new Map((await manager.listManaged()).map(item => [item.id, item.name]));
|
|
@@ -3501,7 +3480,6 @@ function getWebNetworkManager(ctx, state) {
|
|
|
3501
3480
|
homeDir: ctx.homeDir || os.homedir(),
|
|
3502
3481
|
imageRef: () => `${ctx.imageName}:${ctx.imageVersion}`,
|
|
3503
3482
|
getMirrorUrls: () => Object.values(readWebMirrors(state)).filter(Boolean),
|
|
3504
|
-
getFilterProxy: () => ensureWebEgressProxy(ctx, state),
|
|
3505
3483
|
warn: message => ctx.logger.warn(message)
|
|
3506
3484
|
});
|
|
3507
3485
|
}
|
|
@@ -4806,6 +4784,9 @@ function spawnWebTerminalProcess(ctx, containerName, cols, rows) {
|
|
|
4806
4784
|
'if [ -f ~/.bashrc ]; then',
|
|
4807
4785
|
' . ~/.bashrc',
|
|
4808
4786
|
'fi',
|
|
4787
|
+
'if [ -f /run/manyoyo-sys/env.sh ]; then',
|
|
4788
|
+
' . /run/manyoyo-sys/env.sh',
|
|
4789
|
+
'fi',
|
|
4809
4790
|
'if [ -n "${MANYOYO_TERM_COLS:-}" ] && [ -n "${MANYOYO_TERM_ROWS:-}" ]; then',
|
|
4810
4791
|
' COLUMNS="$MANYOYO_TERM_COLS"',
|
|
4811
4792
|
' LINES="$MANYOYO_TERM_ROWS"',
|
|
@@ -6294,69 +6275,79 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
6294
6275
|
});
|
|
6295
6276
|
|
|
6296
6277
|
let lastPartialPersistAt = 0;
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6278
|
+
const handleStreamEvent = event => {
|
|
6279
|
+
// token 级增量:只走网线,不进事件日志,历史落盘按
|
|
6280
|
+
// AGENT_STREAM_PARTIAL_PERSIST_INTERVAL_MS 节流
|
|
6281
|
+
const isPartialDelta = event && event.type === 'content_delta' && event.partial === true;
|
|
6282
|
+
if (event && event.type === 'trace' && event.text) {
|
|
6283
|
+
traceLines.push(String(event.text));
|
|
6284
|
+
if (event.traceEvent && typeof event.traceEvent === 'object') {
|
|
6285
|
+
traceEvents.push(event.traceEvent);
|
|
6286
|
+
}
|
|
6287
|
+
patchWebSessionMessage(state.webHistoryDir, sessionRef, traceMessage && traceMessage.id, {
|
|
6288
|
+
content: traceLines.join('\n'),
|
|
6289
|
+
traceEvents: traceEvents.slice(),
|
|
6290
|
+
pending: true
|
|
6291
|
+
});
|
|
6292
|
+
}
|
|
6293
|
+
if (event && event.type === 'content_delta' && typeof event.content === 'string') {
|
|
6294
|
+
const now = Date.now();
|
|
6295
|
+
const shouldPersist = !isPartialDelta
|
|
6296
|
+
|| now - lastPartialPersistAt >= AGENT_STREAM_PARTIAL_PERSIST_INTERVAL_MS;
|
|
6297
|
+
if (!streamingReplyMessageId) {
|
|
6298
|
+
const streamingReplyMessage = appendWebSessionMessage(
|
|
6299
|
+
state.webHistoryDir,
|
|
6300
|
+
sessionRef,
|
|
6301
|
+
'assistant',
|
|
6302
|
+
event.content,
|
|
6303
|
+
{
|
|
6304
|
+
mode: 'agent',
|
|
6305
|
+
streamingReply: true,
|
|
6314
6306
|
pending: true
|
|
6315
|
-
});
|
|
6316
|
-
}
|
|
6317
|
-
if (event && event.type === 'content_delta' && typeof event.content === 'string') {
|
|
6318
|
-
const now = Date.now();
|
|
6319
|
-
const shouldPersist = !isPartialDelta
|
|
6320
|
-
|| now - lastPartialPersistAt >= AGENT_STREAM_PARTIAL_PERSIST_INTERVAL_MS;
|
|
6321
|
-
if (!streamingReplyMessageId) {
|
|
6322
|
-
const streamingReplyMessage = appendWebSessionMessage(
|
|
6323
|
-
state.webHistoryDir,
|
|
6324
|
-
sessionRef,
|
|
6325
|
-
'assistant',
|
|
6326
|
-
event.content,
|
|
6327
|
-
{
|
|
6328
|
-
mode: 'agent',
|
|
6329
|
-
streamingReply: true,
|
|
6330
|
-
pending: true
|
|
6331
|
-
}
|
|
6332
|
-
);
|
|
6333
|
-
streamingReplyMessageId = streamingReplyMessage && streamingReplyMessage.id
|
|
6334
|
-
? streamingReplyMessage.id
|
|
6335
|
-
: '';
|
|
6336
|
-
lastPartialPersistAt = now;
|
|
6337
|
-
} else if (shouldPersist) {
|
|
6338
|
-
patchWebSessionMessage(state.webHistoryDir, sessionRef, streamingReplyMessageId, {
|
|
6339
|
-
content: event.content,
|
|
6340
|
-
pending: true
|
|
6341
|
-
});
|
|
6342
|
-
lastPartialPersistAt = now;
|
|
6343
6307
|
}
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
}
|
|
6354
|
-
emitStreamEvent(event, resolveWebStreamControlEventType(event), {
|
|
6355
|
-
transportType: event && event.type ? event.type : '',
|
|
6356
|
-
text: event && (event.text || event.content) ? String(event.text || event.content) : ''
|
|
6308
|
+
);
|
|
6309
|
+
streamingReplyMessageId = streamingReplyMessage && streamingReplyMessage.id
|
|
6310
|
+
? streamingReplyMessage.id
|
|
6311
|
+
: '';
|
|
6312
|
+
lastPartialPersistAt = now;
|
|
6313
|
+
} else if (shouldPersist) {
|
|
6314
|
+
patchWebSessionMessage(state.webHistoryDir, sessionRef, streamingReplyMessageId, {
|
|
6315
|
+
content: event.content,
|
|
6316
|
+
pending: true
|
|
6357
6317
|
});
|
|
6318
|
+
lastPartialPersistAt = now;
|
|
6358
6319
|
}
|
|
6320
|
+
}
|
|
6321
|
+
if (isPartialDelta) {
|
|
6322
|
+
lastStreamWriteAt = Date.now();
|
|
6323
|
+
// 只发增量片段:前端在 content_chunk 上做追加,
|
|
6324
|
+
// reset 表示换了一条 assistant 消息、从空白重新开始
|
|
6325
|
+
sendNdjson(res, event.chunkReset === true
|
|
6326
|
+
? { type: 'content_chunk', text: '', reset: true }
|
|
6327
|
+
: { type: 'content_chunk', text: event.chunk });
|
|
6328
|
+
return;
|
|
6329
|
+
}
|
|
6330
|
+
emitStreamEvent(event, resolveWebStreamControlEventType(event), {
|
|
6331
|
+
transportType: event && event.type ? event.type : '',
|
|
6332
|
+
text: event && (event.text || event.content) ? String(event.text || event.content) : ''
|
|
6359
6333
|
});
|
|
6334
|
+
|
|
6335
|
+
};
|
|
6336
|
+
// 域名白名单下被过滤代理拦下的新域名:对话进行中立即提示一次
|
|
6337
|
+
const deniedNotifier = createWebDeniedNotifier(ctx, sessionRef, text => handleStreamEvent({ type: 'trace', stream: 'stdout', text }));
|
|
6338
|
+
try {
|
|
6339
|
+
if (deniedNotifier) deniedNotifier.start();
|
|
6340
|
+
let result;
|
|
6341
|
+
try {
|
|
6342
|
+
result = await execAgentInWebContainerStream(ctx, state, sessionRef, command, {
|
|
6343
|
+
agentProgram: agentMeta.agentProgram,
|
|
6344
|
+
// 复用路由入口占好的那把锁,而不是另起一份运行登记
|
|
6345
|
+
runState,
|
|
6346
|
+
onEvent: handleStreamEvent
|
|
6347
|
+
});
|
|
6348
|
+
} finally {
|
|
6349
|
+
if (deniedNotifier) deniedNotifier.stop();
|
|
6350
|
+
}
|
|
6360
6351
|
ctx.logger.info('agent turn finished', {
|
|
6361
6352
|
session: turnSessionKey,
|
|
6362
6353
|
durationMs: Date.now() - turnStartedAt,
|
|
@@ -6854,7 +6845,6 @@ async function startWebServer(options) {
|
|
|
6854
6845
|
state.networkWatch = null;
|
|
6855
6846
|
}
|
|
6856
6847
|
if (state.portForwarder) state.portForwarder.closeAll();
|
|
6857
|
-
if (state.egressProxy) state.egressProxy.stop();
|
|
6858
6848
|
for (const session of state.terminalSessions.values()) {
|
|
6859
6849
|
const ptyProcess = session && session.ptyProcess;
|
|
6860
6850
|
if (ptyProcess && !ptyProcess.killed) {
|