@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/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 { createEgressProxy, REALM: EGRESS_REALM } = require('../egress-proxy');
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
- // 过滤代理固定端口:容器里的代理地址写在各自的 managed.env 里,serve 重启后端口不能变
3328
- const EGRESS_PROXY_PORT = 8936;
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
- // 每个 CONNECT 都读策略 / token:加 2 秒缓存,保存策略后最多 2 秒生效
3347
- const cache = new Map();
3348
- const cached = (key, load) => {
3349
- const hit = cache.get(key);
3350
- if (hit && Date.now() - hit.at < 2000) return hit.value;
3351
- const value = load();
3352
- cache.set(key, { at: Date.now(), value });
3353
- return value;
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
- state.egressProxyPromise = proxy.start({ host: '0.0.0.0', port: EGRESS_PROXY_PORT })
3368
- .then(port => { state.egressProxy = proxy; return { port }; })
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
- state.networkWatch = manager.watchStarts();
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
- try {
6298
- const result = await execAgentInWebContainerStream(ctx, state, sessionRef, command, {
6299
- agentProgram: agentMeta.agentProgram,
6300
- // 复用路由入口占好的那把锁,而不是另起一份运行登记
6301
- runState,
6302
- onEvent: event => {
6303
- // token 级增量:只走网线,不进事件日志,历史落盘按
6304
- // AGENT_STREAM_PARTIAL_PERSIST_INTERVAL_MS 节流
6305
- const isPartialDelta = event && event.type === 'content_delta' && event.partial === true;
6306
- if (event && event.type === 'trace' && event.text) {
6307
- traceLines.push(String(event.text));
6308
- if (event.traceEvent && typeof event.traceEvent === 'object') {
6309
- traceEvents.push(event.traceEvent);
6310
- }
6311
- patchWebSessionMessage(state.webHistoryDir, sessionRef, traceMessage && traceMessage.id, {
6312
- content: traceLines.join('\n'),
6313
- traceEvents: traceEvents.slice(),
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
- if (isPartialDelta) {
6346
- lastStreamWriteAt = Date.now();
6347
- // 只发增量片段:前端在 content_chunk 上做追加,
6348
- // reset 表示换了一条 assistant 消息、从空白重新开始
6349
- sendNdjson(res, event.chunkReset === true
6350
- ? { type: 'content_chunk', text: '', reset: true }
6351
- : { type: 'content_chunk', text: event.chunk });
6352
- return;
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "8.4.0",
3
+ "version": "8.5.0",
4
4
  "imageVersion": "2.2.0-common",
5
5
  "playwrightCliVersion": "0.1.19",
6
6
  "patchrightCoreVersion": "1.63.0",