@xcanwin/manyoyo 8.6.0 → 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,7 +13,7 @@ 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
18
  const { normalizePolicy, isUnrestricted: isUnrestrictedPolicy } = require('../network-policy');
19
19
  const { withEnvEndpoints } = require('../network-endpoints');
@@ -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({
@@ -5950,7 +6005,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5950
6005
  containerInfo.defaultCommand
5951
6006
  );
5952
6007
  const agentProgram = resolveAgentProgram(effectiveTemplate);
5953
- const models = await fetchAgentModelCatalog(ctx, sessionRef.containerName, agentProgram);
6008
+ const models = await fetchAgentModelCatalogCached(ctx, sessionRef.containerName, agentProgram);
5954
6009
  sendJson(res, 200, {
5955
6010
  name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId),
5956
6011
  agentProgram,
@@ -5958,6 +6013,34 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5958
6013
  });
5959
6014
  }
5960
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
+ },
5961
6044
  {
5962
6045
  method: 'POST',
5963
6046
  match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/model$/),
@@ -6872,5 +6955,6 @@ async function startWebServer(options) {
6872
6955
  }
6873
6956
 
6874
6957
  module.exports = {
6875
- startWebServer
6958
+ startWebServer,
6959
+ buildEnvModelOptions
6876
6960
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "8.6.0",
3
+ "version": "8.6.2",
4
4
  "imageVersion": "2.2.0-common",
5
5
  "playwrightCliVersion": "0.1.19",
6
6
  "patchrightCoreVersion": "1.63.0",