@xcanwin/manyoyo 8.5.1 → 8.6.2

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,19 +13,19 @@ 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, resolveContainerId } = require('../container-exec');
16
+ const { buildExecArgs, resolveContainerId, readMergedEnv } = require('../container-exec');
17
17
  const { createNetworkManager, NETWORK_NAME } = require('../container-network');
18
- const { normalizePolicy } = require('../network-policy');
18
+ const { normalizePolicy, isUnrestricted: isUnrestrictedPolicy } = require('../network-policy');
19
19
  const { withEnvEndpoints } = require('../network-endpoints');
20
20
  const { egressPaths, isSidecarName } = require('../egress-sidecar');
21
21
  const { read: readDenied } = require('../egress-denied');
22
22
  const { isBrowserBackground } = require('../browser-background');
23
23
  const { createDeniedNotifier } = require('./denied-notify');
24
- const { domainAllowed } = require('../network-policy');
24
+ const { domainRuleAllows } = require('../network-policy');
25
25
  const { createPortForwarder } = require('../port-forward');
26
26
  const { createContainerManageRoutes, pendingRisks } = require('./container-manage');
27
27
  const { resolveManageOptions } = require('../container-manage-options');
28
- const { defaultPolicy } = require('../network-policy');
28
+ const { defaultPolicy, usesProxy } = require('../network-policy');
29
29
  const { normalizeMirrors } = require('../runtime-normalizers');
30
30
  const { estimateContainerCapacity } = require('../capacity');
31
31
  const { extractAgentMessageFromCodexJsonl } = require('../codex-output');
@@ -829,7 +829,7 @@ function buildCodexAgentExecCommand(template, prompt, options = {}) {
829
829
  const hasJson = /(?:^|\s)--json(?:\s|$)/.test(suffix);
830
830
  const injectedFlags = hasJson ? '' : ' --json';
831
831
  const hasModel = /(?:^|\s)(?:-m|--model)(?:\s|$)/.test(suffix);
832
- const modelFlag = model && !hasModel ? ` --model ${model}` : '';
832
+ const modelFlag = model && !hasModel ? ` --model ${quoteBashSingleValue(model)}` : '';
833
833
  if (sessionId) {
834
834
  const promptIndex = suffix.indexOf('{prompt}');
835
835
  const resumeSuffix = `${suffix.slice(0, promptIndex)}${quoteBashSingleValue(sessionId)} ${suffix.slice(promptIndex)}`;
@@ -877,7 +877,7 @@ function buildClaudeAgentExecCommand(template, prompt, options = {}) {
877
877
  }
878
878
  const model = options && typeof options.model === 'string' ? options.model.trim() : '';
879
879
  if (model) {
880
- flagSpecs.push({ flag: `--model ${model}`, pattern: /(?:^|\s)--model(?:\s|$)/ });
880
+ flagSpecs.push({ flag: `--model ${quoteBashSingleValue(model)}`, pattern: /(?:^|\s)--model(?:\s|$)/ });
881
881
  }
882
882
  const claudeTemplate = prependAgentFlags(
883
883
  templateText,
@@ -896,7 +896,7 @@ function buildGeminiAgentExecCommand(template, prompt, options = {}) {
896
896
  ];
897
897
  const model = options && typeof options.model === 'string' ? options.model.trim() : '';
898
898
  if (model) {
899
- flagSpecs.push({ flag: `--model ${model}`, pattern: /(?:^|\s)(?:-m|--model)(?:\s|$)/ });
899
+ flagSpecs.push({ flag: `--model ${quoteBashSingleValue(model)}`, pattern: /(?:^|\s)(?:-m|--model)(?:\s|$)/ });
900
900
  }
901
901
  const geminiTemplate = prependAgentFlags(
902
902
  templateText,
@@ -922,7 +922,7 @@ function buildOpenCodeAgentExecCommand(template, prompt, options = {}) {
922
922
  }
923
923
  const model = options && typeof options.model === 'string' ? options.model.trim() : '';
924
924
  if (model) {
925
- flagSpecs.push({ flag: `--model ${model}`, pattern: /(?:^|\s)(?:-m|--model)(?:\s|$)/ });
925
+ flagSpecs.push({ flag: `--model ${quoteBashSingleValue(model)}`, pattern: /(?:^|\s)(?:-m|--model)(?:\s|$)/ });
926
926
  }
927
927
  const opencodeTemplate = prependAgentFlags(
928
928
  templateText,
@@ -1319,6 +1319,61 @@ function extractOpenCodeModelsFromOutput(text) {
1319
1319
  .map(value => ({ value, label: value, description: '' }));
1320
1320
  }
1321
1321
 
1322
+ // 模型目录要在容器里起一个 Agent 进程才能问到,很慢:按“容器 + Agent 程序”缓存,
1323
+ // 并合并同一时刻的重复请求。缓存挂在 ctx 上(随 serve 实例走),不放模块级,避免测试之间串味
1324
+ const MODEL_CATALOG_TTL_MS = 10 * 60 * 1000;
1325
+ const modelCatalogCaches = new WeakMap();
1326
+
1327
+ async function fetchAgentModelCatalogCached(ctx, containerName, agentProgram) {
1328
+ let cache = modelCatalogCaches.get(ctx);
1329
+ if (!cache) {
1330
+ cache = new Map();
1331
+ modelCatalogCaches.set(ctx, cache);
1332
+ }
1333
+ const key = `${containerName}\u0000${agentProgram}`;
1334
+ const hit = cache.get(key);
1335
+ if (hit && (hit.pending || Date.now() - hit.at < MODEL_CATALOG_TTL_MS)) {
1336
+ return hit.pending || hit.models;
1337
+ }
1338
+ const pending = fetchAgentModelCatalog(ctx, containerName, agentProgram).then(models => {
1339
+ // 探测失败(超时 / 容器没起来)返回空数组,不缓存,下次还能重试
1340
+ if (models.length) cache.set(key, { at: Date.now(), models });
1341
+ else cache.delete(key);
1342
+ return models;
1343
+ }, error => {
1344
+ cache.delete(key);
1345
+ throw error;
1346
+ });
1347
+ cache.set(key, { pending });
1348
+ return pending;
1349
+ }
1350
+
1351
+ // 以会话实际配置的 env 生成模型选项。defaultModel 是“跟随默认”实际用的模型;
1352
+ // Claude 另有按档位别名(opus / sonnet / haiku …)一行一个的选项:value 是别名(--model opus 由 CLI 按 env 解析),
1353
+ // label 是该档位对应的真实模型名。不同档位指向同一个模型也各占一行,不合并。
1354
+ // 只读 *_MODEL 这类非敏感键,容器可写的 env 只用来展示,不参与任何安全判断
1355
+ const CLAUDE_TIER_ORDER = ['opus', 'fable', 'sonnet', 'haiku'];
1356
+
1357
+ function buildEnvModelOptions(agentProgram, env) {
1358
+ const source = env && typeof env === 'object' ? env : {};
1359
+ const defaultKey = { claude: 'ANTHROPIC_MODEL', codex: 'OPENAI_MODEL', opencode: 'OPENAI_MODEL', gemini: 'GEMINI_MODEL' }[agentProgram];
1360
+ if (!defaultKey) return { defaultModel: '', options: [] };
1361
+ const defaultModel = normalizeStoredAgentModelValue(source[defaultKey]);
1362
+ if (agentProgram !== 'claude') return { defaultModel, options: [] };
1363
+ const tiers = [];
1364
+ Object.keys(source).forEach(key => {
1365
+ const matched = key.match(/^ANTHROPIC_DEFAULT_([A-Z0-9]+)_MODEL$/);
1366
+ const model = matched ? normalizeStoredAgentModelValue(source[key]) : '';
1367
+ if (model) tiers.push({ alias: matched[1].toLowerCase(), model });
1368
+ });
1369
+ const rank = alias => (CLAUDE_TIER_ORDER.includes(alias) ? CLAUDE_TIER_ORDER.indexOf(alias) : CLAUDE_TIER_ORDER.length);
1370
+ tiers.sort((x, y) => rank(x.alias) - rank(y.alias) || x.alias.localeCompare(y.alias));
1371
+ return {
1372
+ defaultModel,
1373
+ options: tiers.map(tier => ({ value: tier.alias, label: tier.model, description: tier.alias }))
1374
+ };
1375
+ }
1376
+
1322
1377
  async function fetchAgentModelCatalog(ctx, containerName, agentProgram) {
1323
1378
  if (agentProgram === 'claude') {
1324
1379
  const requestLine = JSON.stringify({
@@ -2826,10 +2881,10 @@ function buildCreateRuntime(ctx, state, payload) {
2826
2881
  parseServerListen: () => ({ host: '', port: 0 })
2827
2882
  });
2828
2883
 
2829
- // 自启动与网络:请求 > runs.<name> > 全局配置;请求里放开限制(open / 非本机绑定)要带 confirmRisk
2884
+ // 自启动与网络:请求 > runs.<name> > 全局配置;请求里放开限制(自定义 / 很宽的允许规则 / 非本机绑定)要带 confirmRisk
2830
2885
  const manage = resolveManageOptions({ requestOptions, runConfig, globalConfig: config });
2831
2886
  if (manage.fromRequest && requestOptions.confirmRisk !== true && pendingRisks(defaultPolicy(), manage.network).length) {
2832
- throw new Error('该网络设置会放开限制(open 预设或绑定非本机地址),需要确认(confirmRisk)');
2887
+ throw new Error('该网络设置会放开限制(自定义模式、很宽的允许规则或绑定非本机地址),需要确认(confirmRisk)');
2833
2888
  }
2834
2889
 
2835
2890
  const containerName = resolvedBase.containerName;
@@ -2952,7 +3007,7 @@ function buildCreateRuntime(ctx, state, payload) {
2952
3007
  agentPromptCommand,
2953
3008
  defaultCommand: buildDefaultCommand(shellPrefix, shell, shellSuffix) || '/bin/bash',
2954
3009
  // 仅供服务端内部复用(如"创建相同配置容器"/"复制容器"),绝不能透传给任何对外响应字段,
2955
- // 因为 env/volumes/ports 可能包含密钥原始值(对外响应向来只暴露 envCount 等计数)。
3010
+ // 因为 env/volumes/ports 可能包含密钥原始值(对外响应不暴露它们)。
2956
3011
  runtimeSnapshot: {
2957
3012
  hostPath,
2958
3013
  containerPath,
@@ -2987,11 +3042,8 @@ function buildCreateRuntime(ctx, state, payload) {
2987
3042
  agentProgram: agentProgram || '',
2988
3043
  resumeSupported,
2989
3044
  yolo: yolo || '',
2990
- envCount: Math.floor(containerEnvs.length / 2),
2991
3045
  autostartEnabled: Boolean(manage.autostart.trim()),
2992
- networkPreset: manage.network.preset,
2993
- volumeCount: Math.floor(containerVolumes.length / 2),
2994
- portCount: Math.floor(containerPorts.length / 2)
3046
+ networkPreset: manage.network.preset
2995
3047
  }
2996
3048
  };
2997
3049
  }
@@ -3336,10 +3388,10 @@ function createWebDeniedNotifier(ctx, sessionRef, emit) {
3336
3388
  if (!id || !containerState.stateExists(homeDir, id)) return null;
3337
3389
  const deniedDir = egressPaths(homeDir).denied;
3338
3390
  const policyOf = () => normalizePolicy(containerState.readNetworkRaw(homeDir, id) || undefined);
3339
- if (policyOf().preset !== 'allowlist') return null;
3391
+ if (!usesProxy(policyOf())) return null;
3340
3392
  return createDeniedNotifier({
3341
3393
  read: () => readDenied(deniedDir, id),
3342
- isIgnored: host => isBrowserBackground(host) || domainAllowed(policyOf().egress.domains, host),
3394
+ isIgnored: host => isBrowserBackground(host) || domainRuleAllows(policyOf(), host),
3343
3395
  emit
3344
3396
  });
3345
3397
  } catch (e) {
@@ -3503,8 +3555,8 @@ async function ensureWebContainer(ctx, state, containerInput, messageSessionRef
3503
3555
  const userEnvLines = containerState.userEnvLines(runtime.containerEnvs);
3504
3556
  const playwright = await buildWebPlaywrightIntegration(ctx, state, runtime);
3505
3557
  const containerEnvs = containerState.stripEnvKeys(playwright.containerEnvs, new Set(userEnvLines.map(containerState.envLineKey)));
3506
- const networkPolicy = await withEnvEndpoints(normalizePolicy(runtime.network), [...userEnvLines, ...containerState.readEnvFilePaths(runtime.envFiles || []).flatMap(file => file.entries.map(entry => `${entry.key}=${entry.value}`))]);
3507
3558
  const networkManager = getWebNetworkManager(ctx, state);
3559
+ const networkPolicy = await withEnvEndpoints(await networkManager.resolveRefs(normalizePolicy(runtime.network)), [...userEnvLines, ...containerState.readEnvFilePaths(runtime.envFiles || []).flatMap(file => file.entries.map(entry => `${entry.key}=${entry.value}`))]);
3508
3560
  if (!hasNetworkArg([...(runtime.contModeArgs || []), ...(playwright.containerExtraArgs || [])])) {
3509
3561
  await networkManager.ensureBridgeNetwork();
3510
3562
  }
@@ -3514,7 +3566,7 @@ async function ensureWebContainer(ctx, state, containerInput, messageSessionRef
3514
3566
  envFiles: runtime.envFiles,
3515
3567
  autostart: runtime.autostart,
3516
3568
  network: networkPolicy,
3517
- netRequired: networkPolicy.preset !== 'open',
3569
+ netRequired: !isUnrestrictedPolicy(networkPolicy),
3518
3570
  meta: { name: runtime.containerName }
3519
3571
  });
3520
3572
  const args = buildContainerRunArgs({
@@ -4619,9 +4671,6 @@ function buildSessionFallbackApplied(ctx, state, name, history, summary) {
4619
4671
  agentProgram: effectiveAgentProgram,
4620
4672
  resumeSupported: effectiveResumeSupported,
4621
4673
  yolo: defaults.yolo || '',
4622
- envCount: Object.keys(defaults.env || {}).length,
4623
- volumeCount: Array.isArray(defaults.volumes) ? defaults.volumes.length : 0,
4624
- portCount: Array.isArray(defaults.ports) ? defaults.ports.length : 0,
4625
4674
  status: summary.status || 'history'
4626
4675
  };
4627
4676
  }
@@ -5956,7 +6005,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5956
6005
  containerInfo.defaultCommand
5957
6006
  );
5958
6007
  const agentProgram = resolveAgentProgram(effectiveTemplate);
5959
- const models = await fetchAgentModelCatalog(ctx, sessionRef.containerName, agentProgram);
6008
+ const models = await fetchAgentModelCatalogCached(ctx, sessionRef.containerName, agentProgram);
5960
6009
  sendJson(res, 200, {
5961
6010
  name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId),
5962
6011
  agentProgram,
@@ -5964,6 +6013,34 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5964
6013
  });
5965
6014
  }
5966
6015
  },
6016
+ {
6017
+ // 毫秒级:只读宿主机上的 env 文件,不进容器;慢的 CLI 目录探测走上面的 /models
6018
+ method: 'GET',
6019
+ match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/model-options$/),
6020
+ handler: async match => {
6021
+ const sessionRef = getValidSessionRef(ctx, res, match[1]);
6022
+ if (!sessionRef) {
6023
+ return;
6024
+ }
6025
+ const containerMap = listWebManyoyoContainers(ctx);
6026
+ const containerInfo = containerMap[sessionRef.containerName] || {};
6027
+ const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
6028
+ const agentProgram = resolveAgentProgram(resolveEffectiveAgentPromptCommandForSession(
6029
+ history,
6030
+ sessionRef.agentId,
6031
+ containerInfo.defaultCommand
6032
+ ));
6033
+ let env = {};
6034
+ try {
6035
+ env = readMergedEnv({ homeDir: ctx.homeDir || os.homedir(), dockerExecArgs: ctx.dockerExecArgs }, sessionRef.containerName);
6036
+ } catch (e) { /* 读不到 env 就只给空选项 */ }
6037
+ sendJson(res, 200, {
6038
+ name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId),
6039
+ agentProgram,
6040
+ ...buildEnvModelOptions(agentProgram, env)
6041
+ });
6042
+ }
6043
+ },
5967
6044
  {
5968
6045
  method: 'POST',
5969
6046
  match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/model$/),
@@ -6333,7 +6410,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
6333
6410
  });
6334
6411
 
6335
6412
  };
6336
- // 域名白名单下被过滤代理拦下的新域名:对话进行中立即提示一次
6413
+ // 有域名规则时被过滤代理拦下的新域名:对话进行中立即提示一次
6337
6414
  const deniedNotifier = createWebDeniedNotifier(ctx, sessionRef, text => handleStreamEvent({ type: 'trace', stream: 'stdout', text }));
6338
6415
  try {
6339
6416
  if (deniedNotifier) deniedNotifier.start();
@@ -6878,5 +6955,6 @@ async function startWebServer(options) {
6878
6955
  }
6879
6956
 
6880
6957
  module.exports = {
6881
- startWebServer
6958
+ startWebServer,
6959
+ buildEnvModelOptions
6882
6960
  };
@@ -30,14 +30,17 @@
30
30
  "ports": ["8888:8888"],
31
31
 
32
32
  // 网络策略(新建容器时写入,Web「容器」页随时改,不需要重启容器)。不写 = 默认收紧:允许公网,
33
- // 禁止宿主机其他端口、局域网 / 私有网段、云元数据与其他容器。
34
- // preset:restricted(默认)/ allowlist(域名白名单,需要 serve 运行)/ open(不加规则)
33
+ // 禁止宿主机、局域网 / 私有网段、云元数据与其他容器。
34
+ // preset:restricted(默认)/ allowlist(只放行你写的允许规则)/ custom(没匹配的就允许)
35
+ // outbound / inbound:规则从上往下,第一条命中的生效。action:allow / deny;target(入站为 source):
36
+ // IP / 网段 / 域名(仅出站)/ @containers @container:<名称> @host @private @public @metadata @any;ports 空 = 全部;proto:all / tcp / udp
35
37
  "network": {
38
+ "version": 2,
36
39
  "preset": "restricted",
37
- // 允许容器访问的宿主机端口(宿主机上监听 0.0.0.0 的服务)
38
- "host": [],
39
- // 直连放行的 IP 规则,例如局域网里的模型网关:{ "cidr": "192.168.1.50", "ports": "8000", "proto": "tcp" }
40
- "egress": { "domains": [], "rules": [] },
40
+ // 例:允许容器访问宿主机的 Ollama:{ "action": "allow", "target": "@host", "ports": "11434", "proto": "tcp" }
41
+ "outbound": [],
42
+ // 例:允许容器 B 访问我的 7000 端口:{ "action": "allow", "source": "@container:B", "ports": "7000", "proto": "tcp" }
43
+ "inbound": [],
41
44
  // 端口暴露(需要 serve 运行):监听地址默认 127.0.0.1;绑 0.0.0.0 等于局域网 / 公网可见
42
45
  "expose": []
43
46
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "8.5.1",
3
+ "version": "8.6.2",
4
4
  "imageVersion": "2.2.0-common",
5
5
  "playwrightCliVersion": "0.1.19",
6
6
  "patchrightCoreVersion": "1.63.0",