@auto-ai/agent 2.1.172 → 2.1.174

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.
Files changed (31) hide show
  1. package/dist/safe-a/404/index.html +1 -1
  2. package/dist/safe-a/404.html +1 -1
  3. package/dist/safe-a/index.html +2 -2
  4. package/dist/safe-a/index.txt +1 -1
  5. package/dist/safe-a/manage/about/index.html +2 -2
  6. package/dist/safe-a/manage/about/index.txt +1 -1
  7. package/dist/safe-a/manage/env/index.html +2 -2
  8. package/dist/safe-a/manage/env/index.txt +1 -1
  9. package/dist/safe-a/manage/general/index.html +2 -2
  10. package/dist/safe-a/manage/general/index.txt +1 -1
  11. package/dist/safe-a/manage/index.html +2 -2
  12. package/dist/safe-a/manage/index.txt +1 -1
  13. package/dist/safe-a/manage/mcp/index.html +2 -2
  14. package/dist/safe-a/manage/mcp/index.txt +1 -1
  15. package/dist/safe-a/manage/permissions/index.html +2 -2
  16. package/dist/safe-a/manage/permissions/index.txt +1 -1
  17. package/dist/safe-a/manage/skills/index.html +2 -2
  18. package/dist/safe-a/manage/skills/index.txt +1 -1
  19. package/dist/safe-a/manage/task/index.html +2 -2
  20. package/dist/safe-a/manage/task/index.txt +1 -1
  21. package/dist/safe-a/manage/teams/index.html +2 -2
  22. package/dist/safe-a/manage/teams/index.txt +1 -1
  23. package/dist/safe-a/manage/tools/index.html +2 -2
  24. package/dist/safe-a/manage/tools/index.txt +1 -1
  25. package/dist/ws-test/ws-test.css +259 -68
  26. package/dist/ws-test/ws-test.html +90 -137
  27. package/dist/ws-test/ws-test.js +555 -519
  28. package/package.json +6 -6
  29. /package/dist/safe-a/_next/static/{-ohtVxolUnzQcSsncJVgK → dqDFW64gE_H1NTMvUHYb-}/_buildManifest.js +0 -0
  30. /package/dist/safe-a/_next/static/{-ohtVxolUnzQcSsncJVgK → dqDFW64gE_H1NTMvUHYb-}/_clientMiddlewareManifest.json +0 -0
  31. /package/dist/safe-a/_next/static/{-ohtVxolUnzQcSsncJVgK → dqDFW64gE_H1NTMvUHYb-}/_ssgManifest.js +0 -0
@@ -50,28 +50,27 @@
50
50
  const btnSkillMdCancel = $('btnSkillMdCancel')
51
51
  const btnSkillMdSave = $('btnSkillMdSave')
52
52
  let skillMdEditingName = ''
53
- const agentEnvModal = $('agentEnvModal')
54
- const agentEnvModalCard = $('agentEnvModalCard')
55
- const agentEnvModalBackdrop = $('agentEnvModalBackdrop')
56
- const agentEnvSessionTabStrip = $('agentEnvSessionTabStrip')
53
+ const agentSettingsView = $('agentSettingsView')
57
54
  const agentEnvSessionTabList = $('agentEnvSessionTabList')
58
- const agentEnvPanelEnv = $('agentEnvPanelEnv')
55
+ const agentEnvPanelUnified = $('agentEnvPanelUnified')
59
56
  const agentEnvPanelAgentMd = $('agentEnvPanelAgentMd')
60
- const agentEnvPanelSettings = $('agentEnvPanelSettings')
61
57
  const agentEnvAgentMdEditor = $('agentEnvAgentMdEditor')
62
- const agentEnvAgentTypeInput = $('agentEnvAgentTypeInput')
63
- const agentEnvWhenToUseInput = $('agentEnvWhenToUseInput')
64
- const agentEnvTeamEmpty = $('agentEnvTeamEmpty')
65
- const agentEnvTeamChips = $('agentEnvTeamChips')
66
- const agentEnvTeamPick = $('agentEnvTeamPick')
67
- const btnAgentEnvAddTeamMember = $('btnAgentEnvAddTeamMember')
68
- const agentEnvToolsInput = $('agentEnvToolsInput')
69
- const agentEnvModelInput = $('agentEnvModelInput')
70
- const agentEnvEffortInput = $('agentEnvEffortInput')
71
- const agentEnvBackgroundInput = $('agentEnvBackgroundInput')
72
- const agentEnvIsolationInput = $('agentEnvIsolationInput')
58
+ const llmProvidersSettingsBlock = $('llmProvidersSettingsBlock')
59
+ const llmFormId = $('llmFormId')
60
+ const llmFormName = $('llmFormName')
61
+ const llmFormProvider = $('llmFormProvider')
62
+ const llmFormBaseUrl = $('llmFormBaseUrl')
63
+ const llmFormApiKey = $('llmFormApiKey')
64
+ const llmFormAuthToken = $('llmFormAuthToken')
65
+ const llmFormModelId = $('llmFormModelId')
66
+ const llmFormModelLabel = $('llmFormModelLabel')
67
+ const llmProvidersEmptyHint = $('llmProvidersEmptyHint')
68
+ const llmProvidersList = $('llmProvidersList')
69
+ const btnLlmProvidersAdd = $('btnLlmProvidersAdd')
70
+ const LLM_MASKED_CREDENTIAL = '***'
71
+ /** 设置页 LLM 服务商草稿,保存时写入 agent.json.llmProviders */
72
+ let llmProvidersDraft = []
73
73
  const agentEnvList = $('agentEnvList')
74
- const btnAgentEnvModalClose = $('btnAgentEnvModalClose')
75
74
  const btnAgentEnvCancel = $('btnAgentEnvCancel')
76
75
  const btnAgentEnvSave = $('btnAgentEnvSave')
77
76
  const toolFilesModal = $('toolFilesModal')
@@ -318,14 +317,14 @@
318
317
  isolation: '',
319
318
  whentouse: '',
320
319
  env: {},
320
+ llmProviders: [],
321
321
  }
322
322
  // session 级 overrides:只影响当前 sessionId 子进程;不写 agent.json
323
- let sessionOverrideState = { model: '', env: {} }
323
+ let sessionOverrideState = { providerId: '', model: '', env: {} }
324
324
  // 未分配 sessionId 时暂存,收到 {type:'session', sessionId} 后补写
325
325
  let pendingSessionOverrides = null
326
- const AGENT_ENV_GROUP_ORDER = ['llm', 'git', 'geelib', 'tutui', 'system']
326
+ const AGENT_ENV_GROUP_ORDER = ['git', 'geelib', 'tutui', 'system']
327
327
  const AGENT_ENV_GROUP_LABELS = {
328
- llm: 'llm设置',
329
328
  git: 'git设置',
330
329
  geelib: 'geelib设置',
331
330
  tutui: 'tutui设置',
@@ -333,7 +332,7 @@
333
332
  }
334
333
  let agentEnvSchema = []
335
334
  let agentEnvFieldValues = {}
336
- let agentEnvActiveTab = 'env'
335
+ let agentEnvActiveTab = 'settings'
337
336
  let agentEnvAgentMdLoadOk = false
338
337
  let pickerKind = 'tools'
339
338
  let pickerSelected = new Set()
@@ -343,6 +342,7 @@
343
342
  let remoteMcpServers = []
344
343
  const WS_PERMISSION_MODE_KEY = 'WS_PERMISSION_MODE'
345
344
  const LLM_MODEL_KEY = 'LLM_MODEL'
345
+ const LLM_PROVIDER_ID_KEY = 'LLM_PROVIDER_ID'
346
346
  const WS_ALLOW_TOOL_ASK_KEY = 'WS_ALLOW_TOOL_ASK'
347
347
  const TUITUI_CHANNEL_SESSION_KEY = 'TUITUI_CHANNEL_SESSION'
348
348
  const TUITUI_CHANNEL_SESSION_ALLOWED = new Set(['0', '1'])
@@ -364,10 +364,13 @@
364
364
 
365
365
  /** 项目根 .env 中的默认值(agent.json 未配置时用于新 session 展示) */
366
366
  let projectEnvDefaults = {
367
- LLM_MODEL: '',
368
367
  WS_PERMISSION_MODE: '',
369
368
  WS_ALLOW_TOOL_ASK: '',
370
369
  }
370
+
371
+ /** LLM providers merged 默认 provider/model(由刷新函数计算) */
372
+ let llmDefaultProviderId = ''
373
+ let llmDefaultModelId = ''
371
374
  /** 固定表单字段在项目 .env 的默认值(用于环境变量面板回显) */
372
375
  let projectEnvFormDefaults = {}
373
376
 
@@ -456,7 +459,7 @@
456
459
  /**
457
460
  * 业务逻辑:
458
461
  * 1) 环境变量固定表单必须由后端 schema 驱动,避免前后端字段漂移;
459
- * 2) 仅接收 5 个固定分组字段,其他分组直接忽略;
462
+ * 2) 仅接收 git/geelib/tutui/system 分组字段,其他分组直接忽略;
460
463
  * 3) 字段顺序保持后端下发顺序,确保展示和保存稳定可预期。
461
464
  */
462
465
  function normalizeAgentEnvSchema(rawSchema) {
@@ -482,22 +485,49 @@
482
485
  return out
483
486
  }
484
487
 
488
+ /** 读取设置页 whentouse 输入框(由 renderAgentEnvList 动态挂载在「系统设置」分组) */
489
+ function queryAgentEnvWhenToUseInput() {
490
+ return $('agentEnvWhenToUseInput')
491
+ }
492
+
493
+ /**
494
+ * 在「系统设置」分组渲染 agent.json.whentouse 输入框。
495
+ */
496
+ function appendAgentEnvWhentouseRow(section, whentouseValue) {
497
+ const row = document.createElement('div')
498
+ row.className = 'agent-env-fixed-row agent-env-fixed-row--whentouse'
499
+ const label = document.createElement('label')
500
+ label.className = 'agent-env-fixed-label'
501
+ label.setAttribute('for', 'agentEnvWhenToUseInput')
502
+ label.textContent = '使用时机(whentouse)'
503
+ const textarea = document.createElement('textarea')
504
+ textarea.id = 'agentEnvWhenToUseInput'
505
+ textarea.className = 'agent-md-editor settings-textarea--whentouse agent-env-kv-input'
506
+ textarea.rows = 3
507
+ textarea.spellcheck = false
508
+ textarea.placeholder = '输入该 agent 的使用时机描述'
509
+ textarea.setAttribute('aria-label', 'agent.json whentouse')
510
+ textarea.value = typeof whentouseValue === 'string' ? whentouseValue : ''
511
+ row.appendChild(label)
512
+ row.appendChild(textarea)
513
+ section.appendChild(row)
514
+ }
515
+
485
516
  /**
486
517
  * 业务逻辑:
487
518
  * 1) 固定表单只允许 schema 内字段,所有输入值写入内存 map;
488
519
  * 2) 保存前由 collectAndValidateAgentEnvDraft 统一做 trim 与空值过滤;
489
- * 3) 渲染时按分组插入标题,保证 UI 与需求分区一致。
520
+ * 3) 渲染时按分组插入标题;「系统设置」仅含 whentouse,不含全局 .env 变量。
490
521
  */
491
522
  function renderAgentEnvList() {
492
523
  if (!agentEnvList) return
524
+ const priorWhenInput = queryAgentEnvWhenToUseInput()
525
+ const priorWhentouse = priorWhenInput
526
+ ? String(priorWhenInput.value || '')
527
+ : typeof agentConfigState.whentouse === 'string'
528
+ ? agentConfigState.whentouse
529
+ : ''
493
530
  agentEnvList.innerHTML = ''
494
- if (!agentEnvSchema.length) {
495
- const empty = document.createElement('div')
496
- empty.className = 'tool-files-empty'
497
- empty.textContent = '未加载到环境变量表单配置'
498
- agentEnvList.appendChild(empty)
499
- return
500
- }
501
531
  const schemaByGroup = {}
502
532
  for (let i = 0; i < AGENT_ENV_GROUP_ORDER.length; i++) {
503
533
  schemaByGroup[AGENT_ENV_GROUP_ORDER[i]] = []
@@ -507,9 +537,22 @@
507
537
  schemaByGroup[field.group].push(field)
508
538
  }
509
539
  const frag = document.createDocumentFragment()
540
+ let renderedAny = false
510
541
  for (let i = 0; i < AGENT_ENV_GROUP_ORDER.length; i++) {
511
542
  const group = AGENT_ENV_GROUP_ORDER[i]
512
543
  const fields = schemaByGroup[group]
544
+ if (group === 'system') {
545
+ const section = document.createElement('section')
546
+ section.className = 'agent-env-fixed-section'
547
+ const title = document.createElement('h3')
548
+ title.className = 'agent-env-fixed-section-title'
549
+ title.textContent = AGENT_ENV_GROUP_LABELS.system
550
+ section.appendChild(title)
551
+ appendAgentEnvWhentouseRow(section, priorWhentouse)
552
+ frag.appendChild(section)
553
+ renderedAny = true
554
+ continue
555
+ }
513
556
  if (!fields || !fields.length) continue
514
557
  const section = document.createElement('section')
515
558
  section.className = 'agent-env-fixed-section'
@@ -555,36 +598,6 @@
555
598
  agentEnvFieldValues[field.key] = select.value
556
599
  })
557
600
  input = select
558
- } else if (field.key === 'LLM_PROVIDER') {
559
- /**
560
- * 业务逻辑:模型类型是受控枚举,使用下拉框约束可选值,
561
- * 避免手输导致 provider 非法。
562
- */
563
- const select = document.createElement('select')
564
- select.id = inputId
565
- select.className = 'agent-env-kv-input'
566
- const providerOptions = [
567
- { value: '', label: '请选择' },
568
- { value: 'anthropic', label: 'anthropic' },
569
- { value: 'openai', label: 'openai' },
570
- ]
571
- for (let k = 0; k < providerOptions.length; k++) {
572
- const option = document.createElement('option')
573
- option.value = providerOptions[k].value
574
- option.textContent = providerOptions[k].label
575
- select.appendChild(option)
576
- }
577
- const currentValue =
578
- typeof agentEnvFieldValues[field.key] === 'string'
579
- ? agentEnvFieldValues[field.key]
580
- : ''
581
- select.value = currentValue === 'anthropic' || currentValue === 'openai'
582
- ? currentValue
583
- : ''
584
- select.addEventListener('change', function () {
585
- agentEnvFieldValues[field.key] = select.value
586
- })
587
- input = select
588
601
  } else {
589
602
  const textInput = document.createElement('input')
590
603
  textInput.id = inputId
@@ -605,18 +618,29 @@
605
618
  section.appendChild(row)
606
619
  }
607
620
  frag.appendChild(section)
621
+ renderedAny = true
622
+ }
623
+ if (!renderedAny) {
624
+ const empty = document.createElement('div')
625
+ empty.className = 'tool-files-empty'
626
+ empty.textContent = '未加载到环境变量表单配置'
627
+ agentEnvList.appendChild(empty)
628
+ return
608
629
  }
609
630
  agentEnvList.appendChild(frag)
610
631
  }
611
632
 
612
- /** 新 session 默认模型:agent.json.env → agent.json.model → 项目 .env */
613
- function readDefaultLlmModel() {
614
- const fromAgentEnv = readAgentEnvString(LLM_MODEL_KEY)
615
- if (fromAgentEnv) return fromAgentEnv
616
- const fromAgentModel =
617
- typeof agentConfigState.model === 'string' ? agentConfigState.model.trim() : ''
618
- if (fromAgentModel) return fromAgentModel
619
- return projectEnvDefaults.LLM_MODEL || ''
633
+ /**
634
+ * session 的默认 provider + model:
635
+ * 由后端 /api/llm-providers 返回的 merged.defaultProviderId 与 provider.defaultModel 推导。
636
+ *
637
+ * 业务语义:若默认值尚未准备好,直接抛错暴露配置/加载时序问题,
638
+ * 不做静默兜底(避免错误模型运行)。
639
+ */
640
+ function readDefaultLlmProviderAndModel() {
641
+ const providerId = String(llmDefaultProviderId || '').trim()
642
+ const model = String(llmDefaultModelId || '').trim()
643
+ return { providerId, model }
620
644
  }
621
645
 
622
646
  /**
@@ -678,8 +702,10 @@
678
702
  * 3) 将结果投影为 sessionOverrideState,保证 UI 与后端 PATCH 入参一致。
679
703
  */
680
704
  function buildDefaultSessionOverrideState() {
705
+ const { providerId, model } = readDefaultLlmProviderAndModel()
681
706
  return {
682
- model: readDefaultLlmModel(),
707
+ providerId,
708
+ model,
683
709
  env: {
684
710
  [WS_PERMISSION_MODE_KEY]: readDefaultWsPermissionMode(),
685
711
  [WS_ALLOW_TOOL_ASK_KEY]: readDefaultWsAllowToolAsk(),
@@ -2503,11 +2529,21 @@
2503
2529
  treeCache: new Map(),
2504
2530
  }
2505
2531
 
2532
+ /** Agent 设置主视图是否激活(与聊天区、文件管理互斥) */
2533
+ const agentSettingsState = {
2534
+ viewActive: false,
2535
+ }
2536
+
2506
2537
  /** 当前是否在文件管理视图 */
2507
2538
  function isRepoManageViewActive() {
2508
2539
  return repoManageState.viewActive
2509
2540
  }
2510
2541
 
2542
+ /** 当前是否在 Agent 设置主视图 */
2543
+ function isAgentSettingsViewActive() {
2544
+ return agentSettingsState.viewActive
2545
+ }
2546
+
2511
2547
  /** 判断是否为可在页面内展示的文本类文件 */
2512
2548
  function isRepoTextFile(fileName) {
2513
2549
  const base = String(fileName || '').split('/').pop() || ''
@@ -2648,6 +2684,9 @@
2648
2684
 
2649
2685
  /** 显示/隐藏文件浏览主区域、聊天区;顶栏在文件视图下展示「文件」而非会话标题 */
2650
2686
  function setRepoManageVisible(visible) {
2687
+ if (visible) {
2688
+ closeAgentSettingsView()
2689
+ }
2651
2690
  repoManageState.viewActive = visible
2652
2691
  if (repoManageView) repoManageView.hidden = !visible
2653
2692
  if (chatWorkspace) chatWorkspace.hidden = visible
@@ -2670,6 +2709,41 @@
2670
2709
  if (btnManageRepo) btnManageRepo.classList.remove('is-active')
2671
2710
  }
2672
2711
 
2712
+ /**
2713
+ * 切换 Agent 设置主视图与聊天区可见性。
2714
+ * 业务逻辑:设置页与文件管理、聊天区互斥,顶栏展示「设置」与当前 agent 名。
2715
+ */
2716
+ function setAgentSettingsVisible(visible) {
2717
+ if (visible) {
2718
+ repoManageState.viewActive = false
2719
+ if (repoManageView) repoManageView.hidden = true
2720
+ if (btnManageRepo) btnManageRepo.classList.remove('is-active')
2721
+ }
2722
+ agentSettingsState.viewActive = visible
2723
+ if (agentSettingsView) agentSettingsView.hidden = !visible
2724
+ if (chatWorkspace) chatWorkspace.hidden = visible
2725
+ if (chatComposer) chatComposer.hidden = visible
2726
+ if (mainHeaderEl) mainHeaderEl.hidden = false
2727
+ if (visible) {
2728
+ if (mainTitleEl) mainTitleEl.textContent = '设置'
2729
+ if (mainSubEl) {
2730
+ mainSubEl.textContent = currentAgentParam()
2731
+ mainSubEl.hidden = false
2732
+ }
2733
+ if (btnEditAgentEnv) btnEditAgentEnv.classList.add('is-active')
2734
+ } else {
2735
+ if (btnEditAgentEnv) btnEditAgentEnv.classList.remove('is-active')
2736
+ updateMainHeader()
2737
+ }
2738
+ }
2739
+
2740
+ /** 关闭设置主视图,回到聊天 */
2741
+ function closeAgentSettingsView() {
2742
+ if (!agentSettingsState.viewActive) return
2743
+ setAgentSettingsVisible(false)
2744
+ if (agentEnvSessionTabList) agentEnvSessionTabList.innerHTML = ''
2745
+ }
2746
+
2673
2747
  /** 设置 entries/history 面板加载或错误态 */
2674
2748
  function setRepoPanelState(el, message, isError) {
2675
2749
  if (!el) return
@@ -4469,6 +4543,14 @@
4469
4543
  }
4470
4544
  return
4471
4545
  }
4546
+ if (isAgentSettingsViewActive()) {
4547
+ mainTitleEl.textContent = '设置'
4548
+ if (mainSubEl) {
4549
+ mainSubEl.textContent = currentAgentParam()
4550
+ mainSubEl.hidden = false
4551
+ }
4552
+ return
4553
+ }
4472
4554
  if (typeof titleText === 'string' && titleText.trim()) {
4473
4555
  mainTitleEl.textContent = titleText.trim()
4474
4556
  return
@@ -4600,181 +4682,6 @@
4600
4682
  return u.toString()
4601
4683
  }
4602
4684
 
4603
- function buildAgentTeamMembersPostUrl(leaderId) {
4604
- return new URL(
4605
- '/api/agents/' + encodeURIComponent(String(leaderId || '').trim()) + '/team-members',
4606
- httpOriginForApi() + '/',
4607
- ).toString()
4608
- }
4609
-
4610
- function buildAgentTeamMemberDeleteUrl(leaderId, agentType) {
4611
- return new URL(
4612
- '/api/agents/' +
4613
- encodeURIComponent(String(leaderId || '').trim()) +
4614
- '/team-members/' +
4615
- encodeURIComponent(String(agentType || '').trim()),
4616
- httpOriginForApi() + '/',
4617
- ).toString()
4618
- }
4619
-
4620
- function buildAgentsListUrlForOffice() {
4621
- return new URL('/api/agents', httpOriginForApi() + '/').toString()
4622
- }
4623
-
4624
- /** 设置弹窗:可绑定队员仅来自 agent-runtime(GET /api/agents) */
4625
- async function fetchTeamMemberCatalogForSettings(leaderId) {
4626
- const leader = String(leaderId || '').trim()
4627
- const r = await fetch(buildAgentsListUrlForOffice())
4628
- const body = await r.json().catch(function () {
4629
- return {}
4630
- })
4631
- if (!r.ok) {
4632
- throw new Error(body.message || body.error || r.statusText)
4633
- }
4634
- const agents = Array.isArray(body.agents) ? body.agents : []
4635
- const out = []
4636
- for (let i = 0; i < agents.length; i++) {
4637
- const row = agents[i]
4638
- const id = String(row.id || '').trim()
4639
- if (!id || id === leader) continue
4640
- out.push({
4641
- agentType: id,
4642
- whenToUse: typeof row.whentouse === 'string' ? row.whentouse : '',
4643
- })
4644
- }
4645
- out.sort(function (a, b) {
4646
- return a.agentType.localeCompare(b.agentType)
4647
- })
4648
- return out
4649
- }
4650
-
4651
- /** 设置弹窗:渲染已绑定队员与添加下拉 */
4652
- async function renderAgentEnvTeamMembers() {
4653
- if (!agentEnvTeamChips) return
4654
- const leaderId = currentAgentParam()
4655
- const members = Array.isArray(agentConfigState.agentTeamMembers)
4656
- ? agentConfigState.agentTeamMembers
4657
- : []
4658
- if (agentEnvTeamEmpty) {
4659
- agentEnvTeamEmpty.hidden = members.length > 0
4660
- }
4661
- agentEnvTeamChips.innerHTML = ''
4662
- const boundSet = new Set()
4663
- for (let i = 0; i < members.length; i++) {
4664
- const m = members[i]
4665
- const agentType = String(m.agentType || '').trim()
4666
- if (!agentType) continue
4667
- boundSet.add(agentType)
4668
- const chip = document.createElement('span')
4669
- chip.className = 'agent-env-team-chip'
4670
- chip.title =
4671
- typeof m.whentouse === 'string' && m.whentouse.trim()
4672
- ? m.whentouse.trim()
4673
- : agentType
4674
- const label = document.createElement('span')
4675
- label.textContent = agentType
4676
- const rm = document.createElement('button')
4677
- rm.type = 'button'
4678
- rm.setAttribute('aria-label', '移除 ' + agentType)
4679
- rm.textContent = '×'
4680
- rm.addEventListener('click', function () {
4681
- void removeAgentEnvTeamMember(agentType)
4682
- })
4683
- chip.appendChild(label)
4684
- chip.appendChild(rm)
4685
- agentEnvTeamChips.appendChild(chip)
4686
- }
4687
- if (agentEnvTeamPick) {
4688
- agentEnvTeamPick.innerHTML = '<option value="">选择要添加的 Agent…</option>'
4689
- let catalog = []
4690
- try {
4691
- catalog = await fetchTeamMemberCatalogForSettings(leaderId)
4692
- } catch {
4693
- catalog = []
4694
- }
4695
- for (let j = 0; j < catalog.length; j++) {
4696
- const t = catalog[j]
4697
- const id = String(t.agentType || '').trim()
4698
- if (!id || id === leaderId || boundSet.has(id)) continue
4699
- const opt = document.createElement('option')
4700
- opt.value = id
4701
- const when =
4702
- typeof t.whenToUse === 'string' && t.whenToUse.trim() ? t.whenToUse.trim() : ''
4703
- opt.textContent = when ? id + ' — ' + when.slice(0, 36) : id
4704
- agentEnvTeamPick.appendChild(opt)
4705
- }
4706
- }
4707
- }
4708
-
4709
- function applyAgentTeamMembersFromApi(agentRow) {
4710
- if (!agentRow || typeof agentRow !== 'object') return
4711
- if (Array.isArray(agentRow.agentTeamMembers)) {
4712
- agentConfigState.agentTeamMembers = agentRow.agentTeamMembers.map(function (m) {
4713
- return {
4714
- agentType: String(m.agentType || ''),
4715
- whentouse: typeof m.whentouse === 'string' ? m.whentouse : '',
4716
- }
4717
- })
4718
- }
4719
- if (Array.isArray(agentRow.agentTeams)) {
4720
- agentConfigState.agentTeams = agentRow.agentTeams.map(String)
4721
- } else if (Array.isArray(agentRow.agentTeamMembers)) {
4722
- agentConfigState.agentTeams = agentRow.agentTeamMembers
4723
- .map(function (m) {
4724
- return String(m.agentType || '')
4725
- })
4726
- .filter(Boolean)
4727
- }
4728
- renderBindChips()
4729
- }
4730
-
4731
- async function addAgentEnvTeamMember() {
4732
- const leaderId = currentAgentParam()
4733
- if (!agentEnvTeamPick) return
4734
- const agentType = agentEnvTeamPick.value.trim()
4735
- if (!agentType) {
4736
- window.alert('请先选择要添加的队员')
4737
- return
4738
- }
4739
- const r = await fetch(buildAgentTeamMembersPostUrl(leaderId), {
4740
- method: 'POST',
4741
- headers: { 'content-type': 'application/json; charset=utf-8' },
4742
- body: JSON.stringify({ agentType: agentType }),
4743
- })
4744
- const body = await r.json().catch(function () {
4745
- return {}
4746
- })
4747
- if (!r.ok) {
4748
- window.alert('添加队员失败:' + (body.message || body.error || r.statusText))
4749
- return
4750
- }
4751
- applyAgentTeamMembersFromApi(body.agent)
4752
- agentEnvTeamPick.value = ''
4753
- await renderAgentEnvTeamMembers()
4754
- void persistAgentConfigAndRestartSession({ silent: true })
4755
- setStatus('已添加队员 ' + agentType, serverReady ? 'ready' : '')
4756
- }
4757
-
4758
- async function removeAgentEnvTeamMember(agentType) {
4759
- const leaderId = currentAgentParam()
4760
- if (!window.confirm('确定移除队员「' + agentType + '」?')) {
4761
- return
4762
- }
4763
- const r = await fetch(buildAgentTeamMemberDeleteUrl(leaderId, agentType), {
4764
- method: 'DELETE',
4765
- })
4766
- const body = await r.json().catch(function () {
4767
- return {}
4768
- })
4769
- if (!r.ok) {
4770
- window.alert('移除队员失败:' + (body.message || body.error || r.statusText))
4771
- return
4772
- }
4773
- applyAgentTeamMembersFromApi(body.agent)
4774
- await renderAgentEnvTeamMembers()
4775
- void persistAgentConfigAndRestartSession({ silent: true })
4776
- setStatus('已移除队员 ' + agentType, serverReady ? 'ready' : '')
4777
- }
4778
4685
  async function loadSessions(options) {
4779
4686
  sessionListMetaEl.textContent = '加载中…'
4780
4687
  let data
@@ -4825,6 +4732,7 @@
4825
4732
 
4826
4733
  function switchToSession(id) {
4827
4734
  closeRepoManageView()
4735
+ closeAgentSettingsView()
4828
4736
  const nextSid = typeof id === 'string' ? id : ''
4829
4737
  sessionIdInput.value = nextSid
4830
4738
  currentSessionContextPercent = null
@@ -5644,12 +5552,16 @@
5644
5552
 
5645
5553
  function buildModelsApiUrl() {
5646
5554
  const id = currentAgentParam()
5647
- const u = new URL('/api/models', httpOriginForApi() + '/')
5555
+ const u = new URL('/api/llm-providers', httpOriginForApi() + '/')
5648
5556
  u.searchParams.set('agent', id)
5649
5557
  return u.toString()
5650
5558
  }
5651
5559
 
5652
- /** GET /api/models 返回的动态列表(仅展示接口数据 + 浏览器最近使用) */
5560
+ /**
5561
+ * 会话模型候选:来自 /api/llm-providers.merged 的本地配置(不走云端探测)。
5562
+ *
5563
+ * 注意:同一个 model.id 在不同 provider 下可能重复,因此候选需要 providerId 一起参与。
5564
+ */
5653
5565
  let apiModelRows = []
5654
5566
  let lastModelsFetchTs = 0
5655
5567
 
@@ -5668,102 +5580,79 @@
5668
5580
  if (!r.ok) {
5669
5581
  throw new Error(b.message || b.error || r.statusText)
5670
5582
  }
5671
- const rows = Array.isArray(b.models) ? b.models : []
5672
- apiModelRows = rows
5673
- .filter(function (x) {
5674
- return x && typeof x.id === 'string' && x.id.trim()
5675
- })
5676
- .map(function (x) {
5677
- return {
5678
- id: String(x.id).trim(),
5679
- label:
5680
- typeof x.label === 'string' && x.label.trim()
5681
- ? String(x.label).trim()
5682
- : undefined,
5683
- }
5684
- })
5685
- logLine('models-api', { source: b.source, count: apiModelRows.length })
5686
- } catch (e) {
5687
- apiModelRows = []
5688
- logLine('models-api', 'load failed: ' + e)
5689
- }
5690
- }
5583
+ const providers = Array.isArray(b.providers) ? b.providers : []
5691
5584
 
5692
- function labelForModelId(modelId) {
5693
- for (let i = 0; i < apiModelRows.length; i++) {
5694
- if (apiModelRows[i].id === modelId) return apiModelRows[i].label
5695
- }
5696
- return undefined
5697
- }
5698
-
5699
- const RECENT_MODELS_KEY = 'ws-test-composer-recent-models'
5700
-
5701
- function readRecentModels() {
5702
- try {
5703
- const raw = localStorage.getItem(RECENT_MODELS_KEY)
5704
- const arr = raw ? JSON.parse(raw) : []
5705
- return Array.isArray(arr)
5706
- ? arr.filter(function (x) {
5707
- return typeof x === 'string' && x.trim()
5708
- }).slice(0, 12)
5709
- : []
5710
- } catch {
5711
- return []
5712
- }
5713
- }
5714
-
5715
- function pushRecentModel(m) {
5716
- if (!m || !String(m).trim()) return
5717
- const v = String(m).trim()
5718
- let arr = readRecentModels().filter(function (x) {
5719
- return x !== v
5720
- })
5721
- arr.unshift(v)
5722
- arr = arr.slice(0, 12)
5723
- try {
5724
- localStorage.setItem(RECENT_MODELS_KEY, JSON.stringify(arr))
5725
- } catch {
5726
- /* ignore */
5727
- }
5728
- }
5585
+ apiModelRows = []
5586
+ for (let i = 0; i < providers.length; i++) {
5587
+ const p = providers[i]
5588
+ const providerId = typeof p.id === 'string' ? p.id.trim() : ''
5589
+ const providerName = typeof p.name === 'string' ? p.name.trim() : ''
5590
+ const models = Array.isArray(p.models) ? p.models : []
5591
+ for (let j = 0; j < models.length; j++) {
5592
+ const m = models[j]
5593
+ const modelId = typeof m.id === 'string' ? m.id.trim() : ''
5594
+ if (!modelId) continue
5595
+ const label =
5596
+ typeof m.label === 'string' && m.label.trim() ? m.label.trim() : undefined
5597
+ const display = providerName
5598
+ ? providerName + ' · ' + (label ? label : modelId)
5599
+ : (label ? label : modelId)
5600
+ apiModelRows.push({
5601
+ providerId,
5602
+ providerName,
5603
+ id: modelId,
5604
+ label,
5605
+ display,
5606
+ })
5607
+ }
5608
+ }
5729
5609
 
5730
- function mergeModelCandidates() {
5731
- if (!apiModelRows.length) {
5732
- return []
5733
- }
5734
- const seen = new Set()
5735
- const out = []
5736
- function add(x) {
5737
- if (!x || !String(x).trim()) return
5738
- const s = String(x).trim()
5739
- if (seen.has(s)) return
5740
- seen.add(s)
5741
- out.push(s)
5742
- }
5743
- const recent = readRecentModels()
5744
- for (let i = 0; i < recent.length; i++) {
5745
- add(recent[i])
5746
- }
5747
- for (let a = 0; a < apiModelRows.length; a++) {
5748
- add(apiModelRows[a].id)
5610
+ // 计算 merged 默认 provider/model(给新 session 使用)
5611
+ const defaultProviderIdRaw =
5612
+ typeof b.defaultProviderId === 'string' ? b.defaultProviderId.trim() : ''
5613
+ const resolvedDefaultProviderId =
5614
+ defaultProviderIdRaw ||
5615
+ (apiModelRows.length ? apiModelRows[0].providerId : '')
5616
+ llmDefaultProviderId = resolvedDefaultProviderId
5617
+ const defaultProvider = providers.find(p => String(p.id) === resolvedDefaultProviderId) || providers[0]
5618
+ const defaultModelRaw =
5619
+ defaultProvider && typeof defaultProvider.defaultModel === 'string'
5620
+ ? defaultProvider.defaultModel.trim()
5621
+ : ''
5622
+ const defaultModelId =
5623
+ defaultModelRaw ||
5624
+ (defaultProvider && Array.isArray(defaultProvider.models) && defaultProvider.models[0]
5625
+ ? String(defaultProvider.models[0].id).trim()
5626
+ : '')
5627
+ llmDefaultModelId = defaultModelId
5628
+
5629
+ logLine('llm-providers-api', { count: apiModelRows.length, defaultProviderId: llmDefaultProviderId, defaultModelId: llmDefaultModelId })
5630
+ } catch (e) {
5631
+ apiModelRows = []
5632
+ llmDefaultProviderId = ''
5633
+ llmDefaultModelId = ''
5634
+ logLine('llm-providers-api', 'load failed: ' + e)
5749
5635
  }
5750
- return out
5751
5636
  }
5752
5637
 
5753
5638
  function filterModelCandidates(query) {
5754
- const all = mergeModelCandidates()
5755
5639
  const q = String(query || '')
5756
5640
  .trim()
5757
5641
  .toLowerCase()
5642
+ const all = Array.isArray(apiModelRows) ? apiModelRows : []
5758
5643
  if (!q) return all.slice(0, 50)
5759
5644
  const hit = []
5760
5645
  for (let i = 0; i < all.length; i++) {
5761
- const id = all[i]
5762
- const lab = labelForModelId(id)
5763
- const idHit = id.toLowerCase().indexOf(q) >= 0
5764
- const labHit = lab && lab.toLowerCase().indexOf(q) >= 0
5765
- if (idHit || labHit) hit.push(id)
5766
- if (hit.length >= 50) break
5646
+ const x = all[i]
5647
+ const idHit = x.id.toLowerCase().indexOf(q) >= 0
5648
+ const labelHit = x.label && x.label.toLowerCase().indexOf(q) >= 0
5649
+ const providerHit =
5650
+ x.providerName && x.providerName.toLowerCase().indexOf(q) >= 0
5651
+ const providerIdHit = x.providerId && x.providerId.toLowerCase().indexOf(q) >= 0
5652
+ if (idHit || labelHit || providerHit || providerIdHit) {
5653
+ hit.push(x)
5654
+ if (hit.length >= 50) break
5655
+ }
5767
5656
  }
5768
5657
  return hit
5769
5658
  }
@@ -5796,17 +5685,23 @@
5796
5685
  return
5797
5686
  }
5798
5687
  for (let i = 0; i < items.length; i++) {
5799
- const id = items[i]
5800
- const lab = labelForModelId(id)
5688
+ const entry = items[i]
5689
+ const id = entry && typeof entry.id === 'string' ? entry.id : ''
5801
5690
  const li = document.createElement('li')
5802
5691
  li.setAttribute('role', 'option')
5803
5692
  li.setAttribute('data-model', id)
5804
- li.textContent = lab ? lab + ' · ' + id : id
5693
+ if (entry && typeof entry.providerId === 'string') {
5694
+ li.setAttribute('data-provider-id', entry.providerId)
5695
+ }
5696
+ li.textContent = entry && entry.display ? entry.display : id
5805
5697
  if (i === composerModelHighlight) li.classList.add('is-active')
5806
5698
  li.addEventListener('mousedown', function (ev) {
5807
5699
  ev.preventDefault()
5808
5700
  composerModelSuppressBlurCommit = true
5809
5701
  if (composerModelInput) composerModelInput.value = id
5702
+ if (entry && typeof entry.providerId === 'string') {
5703
+ sessionOverrideState.providerId = entry.providerId
5704
+ }
5810
5705
  syncComposerModelTitle()
5811
5706
  hideComposerModelDropdown()
5812
5707
  void commitComposerModelFromInput()
@@ -5854,8 +5749,10 @@
5854
5749
  let idx = composerModelHighlight
5855
5750
  if (idx < 0 || idx >= opts.length) idx = 0
5856
5751
  const id = opts[idx].getAttribute('data-model')
5752
+ const pid = opts[idx].getAttribute('data-provider-id') || ''
5857
5753
  if (id) {
5858
5754
  composerModelInput.value = id
5755
+ if (pid) sessionOverrideState.providerId = pid
5859
5756
  syncComposerModelTitle()
5860
5757
  hideComposerModelDropdown()
5861
5758
  void commitComposerModelFromInput()
@@ -5865,25 +5762,83 @@
5865
5762
  async function commitComposerModelFromInput() {
5866
5763
  if (!composerModelInput) return
5867
5764
  const v = composerModelInput.value.trim()
5868
- if (v === (sessionOverrideState.model || '')) return
5869
- sessionOverrideState.model = v
5870
- if (v) pushRecentModel(v)
5765
+ const defaults = buildDefaultSessionOverrideState()
5766
+
5767
+ // 留空语义:清除 session 级 overrides,让 worker 依据项目/agent 默认配置自动选择 provider/model。
5768
+ if (!v) {
5769
+ const shouldUpdate =
5770
+ (sessionOverrideState.model || '') !== '' ||
5771
+ (sessionOverrideState.providerId || '') !== ''
5772
+ if (!shouldUpdate) return
5773
+ sessionOverrideState.model = ''
5774
+ sessionOverrideState.providerId = ''
5775
+ await persistSessionOverridesAndRestart({ silent: true })
5776
+ return
5777
+ }
5778
+
5779
+ const modelId = v
5780
+
5781
+ const matches = Array.isArray(apiModelRows)
5782
+ ? apiModelRows.filter(function (x) {
5783
+ return x && x.id === modelId
5784
+ })
5785
+ : []
5786
+
5787
+ let providerId = sessionOverrideState.providerId || ''
5788
+ if (matches.length === 1) {
5789
+ providerId = matches[0].providerId
5790
+ } else if (matches.length > 1) {
5791
+ // 相同 model.id 可能出现在多个 provider 下:
5792
+ // 若当前 provider 已匹配其中之一则沿用,否则阻止保存要求用户明确选择。
5793
+ const found = providerId
5794
+ ? matches.some(function (x) {
5795
+ return x.providerId === providerId
5796
+ })
5797
+ : false
5798
+ if (!found) {
5799
+ window.alert('该模型 ID 在多个服务商中重复,请从下拉列表选择完整项(服务商 · 模型)。')
5800
+ return
5801
+ }
5802
+ } else {
5803
+ // 没匹配到 model.id:不允许用不受控的 provider/model 组合运行。
5804
+ window.alert('模型 ID 未配置:' + modelId)
5805
+ return
5806
+ }
5807
+
5808
+ const shouldUpdate =
5809
+ modelId !== (sessionOverrideState.model || '') || providerId !== (sessionOverrideState.providerId || '')
5810
+ if (!shouldUpdate) return
5811
+
5812
+ sessionOverrideState.model = modelId
5813
+ sessionOverrideState.providerId = providerId
5871
5814
  await persistSessionOverridesAndRestart({ silent: true })
5872
5815
  }
5873
5816
 
5874
5817
  function syncComposerModelTitle() {
5875
5818
  if (!composerModelInput) return
5876
5819
  const v = composerModelInput.value.trim()
5877
- composerModelInput.title = v
5878
- ? 'session 级模型覆盖(完整 ID,悬停可复制核对):' + v
5879
- : '留空=跟随 agent.json 默认;模型 ID 须与网关控制台一致(区分大小写)'
5820
+ if (!v) {
5821
+ composerModelInput.title = '留空=跟随默认服务商与模型'
5822
+ return
5823
+ }
5824
+ const entry = Array.isArray(apiModelRows)
5825
+ ? apiModelRows.find(function (x) {
5826
+ return x && x.id === v && (!sessionOverrideState.providerId || x.providerId === sessionOverrideState.providerId)
5827
+ })
5828
+ : null
5829
+ composerModelInput.title = entry
5830
+ ? 'session 级模型覆盖(服务商 · 模型):' + entry.display
5831
+ : 'session 级模型覆盖(模型 ID,需与配置一致):' + v
5880
5832
  }
5881
5833
 
5882
5834
  function syncModelStateToEnv() {
5883
5835
  const env = getSessionOverrideEnv()
5884
- const model = typeof sessionOverrideState.model === 'string'
5885
- ? sessionOverrideState.model.trim()
5886
- : ''
5836
+ const model = typeof sessionOverrideState.model === 'string' ? sessionOverrideState.model.trim() : ''
5837
+ const providerId =
5838
+ typeof sessionOverrideState.providerId === 'string'
5839
+ ? sessionOverrideState.providerId.trim()
5840
+ : ''
5841
+ env[LLM_PROVIDER_ID_KEY] = providerId
5887
5842
  env[LLM_MODEL_KEY] = model
5888
5843
  }
5889
5844
 
@@ -6044,6 +5999,7 @@
6044
5999
  function resetSessionOverrideStateToDefault() {
6045
6000
  const defaults = buildDefaultSessionOverrideState()
6046
6001
  sessionOverrideState = {
6002
+ providerId: defaults.providerId,
6047
6003
  model: defaults.model,
6048
6004
  env: Object.assign({}, defaults.env),
6049
6005
  }
@@ -6075,6 +6031,10 @@
6075
6031
  )
6076
6032
  : {}
6077
6033
  sessionOverrideState = {
6034
+ providerId:
6035
+ typeof ovEnv[LLM_PROVIDER_ID_KEY] === 'string'
6036
+ ? String(ovEnv[LLM_PROVIDER_ID_KEY]).trim()
6037
+ : defaults.providerId,
6078
6038
  model:
6079
6039
  typeof ovEnv[LLM_MODEL_KEY] === 'string'
6080
6040
  ? String(ovEnv[LLM_MODEL_KEY]).trim()
@@ -6102,6 +6062,10 @@
6102
6062
  const env = getSessionOverrideEnv()
6103
6063
  const patchEnv = {}
6104
6064
  const defaults = buildDefaultSessionOverrideState()
6065
+ patchEnv[LLM_PROVIDER_ID_KEY] =
6066
+ typeof sessionOverrideState.providerId === 'string'
6067
+ ? sessionOverrideState.providerId.trim()
6068
+ : defaults.providerId
6105
6069
  patchEnv[LLM_MODEL_KEY] =
6106
6070
  typeof sessionOverrideState.model === 'string'
6107
6071
  ? sessionOverrideState.model.trim()
@@ -6317,6 +6281,7 @@
6317
6281
  isolation: '',
6318
6282
  whentouse: '',
6319
6283
  env: {},
6284
+ llmProviders: [],
6320
6285
  }
6321
6286
  renderBindChips()
6322
6287
  syncModelInputFromState()
@@ -6353,6 +6318,9 @@
6353
6318
  : '',
6354
6319
  whentouse: typeof c.whentouse === 'string' ? c.whentouse : '',
6355
6320
  env: c.env && typeof c.env === 'object' ? Object.fromEntries(Object.entries(c.env).map(function (kv) { return [String(kv[0]), String(kv[1])] })) : {},
6321
+ llmProviders: Array.isArray(c.llmProviders)
6322
+ ? cloneLlmProvidersDraft(c.llmProviders)
6323
+ : [],
6356
6324
  }
6357
6325
  } else {
6358
6326
  agentConfigState = {
@@ -6368,6 +6336,7 @@
6368
6336
  isolation: '',
6369
6337
  whentouse: '',
6370
6338
  env: {},
6339
+ llmProviders: [],
6371
6340
  }
6372
6341
  }
6373
6342
  if (body.agent && Array.isArray(body.agent.agentTeamMembers)) {
@@ -7623,6 +7592,7 @@
7623
7592
  ev.preventDefault()
7624
7593
  ev.stopPropagation()
7625
7594
  closeRepoManageView()
7595
+ closeAgentSettingsView()
7626
7596
  const items = sidebarNavEl.querySelectorAll('.sidebar-nav-row')
7627
7597
  for (let i = 0; i < items.length; i++) {
7628
7598
  items[i].classList.toggle('is-active', items[i] === btn)
@@ -7644,6 +7614,7 @@
7644
7614
  ev.preventDefault()
7645
7615
  ev.stopPropagation()
7646
7616
  closeRepoManageView()
7617
+ closeAgentSettingsView()
7647
7618
  const items = sidebarNavEl.querySelectorAll('.sidebar-nav-row')
7648
7619
  for (let i = 0; i < items.length; i++) {
7649
7620
  items[i].classList.toggle('is-active', items[i] === btn)
@@ -7872,6 +7843,7 @@
7872
7843
  ev.preventDefault()
7873
7844
  ev.stopPropagation()
7874
7845
  closeRepoManageView()
7846
+ closeAgentSettingsView()
7875
7847
  if (sidebarNavEl) {
7876
7848
  const items = sidebarNavEl.querySelectorAll('.sidebar-nav-row')
7877
7849
  for (let i = 0; i < items.length; i++) {
@@ -7960,40 +7932,40 @@
7960
7932
  }
7961
7933
  }
7962
7934
 
7963
- function closeAgentEnvModal() {
7964
- if (!agentEnvModal) return
7965
- agentEnvModal.classList.remove('is-open')
7966
- agentEnvModal.setAttribute('aria-hidden', 'true')
7967
- document.body.style.overflow = ''
7968
- if (agentEnvModalCard) {
7969
- agentEnvModalCard.removeAttribute('aria-label')
7970
- agentEnvModalCard.setAttribute('aria-labelledby', 'agentEnvModalTitle')
7971
- }
7972
- if (agentEnvSessionTabStrip) agentEnvSessionTabStrip.setAttribute('hidden', '')
7973
- if (agentEnvSessionTabList) agentEnvSessionTabList.innerHTML = ''
7974
- }
7975
-
7976
7935
  function syncAgentEnvTabPanels() {
7977
- if (!agentEnvPanelEnv || !agentEnvPanelAgentMd || !agentEnvPanelSettings) return
7978
- agentEnvPanelEnv.hidden = agentEnvActiveTab !== 'env'
7936
+ if (!agentEnvPanelUnified || !agentEnvPanelAgentMd) return
7937
+ agentEnvPanelUnified.hidden = agentEnvActiveTab !== 'settings'
7979
7938
  agentEnvPanelAgentMd.hidden = agentEnvActiveTab !== 'agentMd'
7980
- agentEnvPanelSettings.hidden = agentEnvActiveTab !== 'settings'
7981
- }
7982
- function renderAgentEnvHeaderTabs() {
7983
- renderHeaderTabsIn(
7984
- agentEnvSessionTabList,
7985
- [
7986
- { id: 'env', label: '环境变量' },
7987
- { id: 'agentMd', label: '编辑 agent.md' },
7988
- { id: 'settings', label: '设置' },
7989
- ],
7990
- agentEnvActiveTab,
7991
- function (id) {
7992
- agentEnvActiveTab = id
7993
- renderAgentEnvHeaderTabs()
7939
+ }
7940
+
7941
+ /** 渲染左侧配置导航(样式对齐文件管理目录树) */
7942
+ function renderAgentSettingsNav() {
7943
+ if (!agentEnvSessionTabList) return
7944
+ agentEnvSessionTabList.innerHTML = ''
7945
+ const tabs = [
7946
+ { id: 'settings', label: '设置' },
7947
+ { id: 'agentMd', label: '编辑 agent.md' },
7948
+ ]
7949
+ for (let i = 0; i < tabs.length; i++) {
7950
+ const tab = tabs[i]
7951
+ const btn = document.createElement('button')
7952
+ btn.type = 'button'
7953
+ btn.className =
7954
+ 'agent-settings-nav-item' + (tab.id === agentEnvActiveTab ? ' is-selected' : '')
7955
+ btn.setAttribute('role', 'tab')
7956
+ btn.setAttribute('aria-selected', tab.id === agentEnvActiveTab ? 'true' : 'false')
7957
+ btn.textContent = tab.label
7958
+ btn.addEventListener('click', function () {
7959
+ if (agentEnvActiveTab === tab.id) return
7960
+ agentEnvActiveTab = tab.id
7961
+ renderAgentSettingsNav()
7994
7962
  syncAgentEnvTabPanels()
7995
- },
7996
- )
7963
+ if (tab.id === 'agentMd') {
7964
+ void loadAgentEnvAgentMdFromApi()
7965
+ }
7966
+ })
7967
+ agentEnvSessionTabList.appendChild(btn)
7968
+ }
7997
7969
  }
7998
7970
 
7999
7971
  function loadAgentEnvAgentMdFromApi() {
@@ -8044,27 +8016,21 @@
8044
8016
  })
8045
8017
  }
8046
8018
 
8047
- function openAgentEnvModal(initialTab) {
8048
- if (!agentEnvModal) return
8049
- if (initialTab === 'env' || initialTab === 'agentMd' || initialTab === 'settings') {
8019
+ function openAgentSettingsView(initialTab) {
8020
+ if (!agentSettingsView) return
8021
+ closeRepoManageView()
8022
+ if (initialTab === 'agentMd' || initialTab === 'settings') {
8050
8023
  agentEnvActiveTab = initialTab
8051
8024
  } else {
8052
- agentEnvActiveTab = 'env'
8025
+ agentEnvActiveTab = 'settings'
8053
8026
  }
8054
8027
  const envObj =
8055
8028
  agentConfigState.env && typeof agentConfigState.env === 'object'
8056
8029
  ? agentConfigState.env
8057
8030
  : {}
8058
8031
  agentEnvFieldValues = buildAgentEnvFieldValues(envObj)
8059
- if (agentEnvModalCard) {
8060
- agentEnvModalCard.setAttribute('aria-label', '环境变量、编辑 agent.md 与设置')
8061
- agentEnvModalCard.removeAttribute('aria-labelledby')
8062
- }
8063
- if (agentEnvSessionTabStrip) {
8064
- agentEnvSessionTabStrip.setAttribute('aria-label', '环境变量、编辑 agent.md、设置')
8065
- agentEnvSessionTabStrip.removeAttribute('hidden')
8066
- }
8067
- /** 每次打开设置弹窗都拉最新 schema,避免后端新增字段后页面仍用旧缓存。 */
8032
+ setAgentSettingsVisible(true)
8033
+ /** 每次进入设置页都拉最新 schema,避免后端新增字段后页面仍用旧缓存。 */
8068
8034
  void loadProjectEnvDefaults().then(function () {
8069
8035
  const latestEnvObj =
8070
8036
  agentConfigState.env && typeof agentConfigState.env === 'object'
@@ -8073,104 +8039,188 @@
8073
8039
  agentEnvFieldValues = buildAgentEnvFieldValues(latestEnvObj)
8074
8040
  renderAgentEnvList()
8075
8041
  })
8076
- renderAgentEnvHeaderTabs()
8042
+ renderAgentSettingsNav()
8077
8043
  syncAgentEnvTabPanels()
8078
- agentEnvModal.classList.add('is-open')
8079
- agentEnvModal.setAttribute('aria-hidden', 'false')
8080
- document.body.style.overflow = 'hidden'
8044
+ initLlmProvidersSettingsUi()
8081
8045
  renderAgentEnvList()
8082
- if (agentEnvWhenToUseInput) {
8083
- agentEnvWhenToUseInput.value = typeof agentConfigState.whentouse === 'string'
8046
+ const whenInput = queryAgentEnvWhenToUseInput()
8047
+ if (whenInput) {
8048
+ whenInput.value = typeof agentConfigState.whentouse === 'string'
8084
8049
  ? agentConfigState.whentouse
8085
8050
  : ''
8086
8051
  }
8087
- if (agentEnvAgentTypeInput) {
8088
- agentEnvAgentTypeInput.value = currentAgentParam()
8089
- }
8090
- if (agentEnvToolsInput) {
8091
- agentEnvToolsInput.value = Array.isArray(agentConfigState.tools)
8092
- ? agentConfigState.tools.join('\n')
8093
- : ''
8094
- }
8095
- if (agentEnvModelInput) {
8096
- agentEnvModelInput.value = typeof agentConfigState.model === 'string'
8097
- ? agentConfigState.model
8098
- : ''
8099
- }
8100
- syncAgentEffortSelectValue(agentConfigState.effort)
8101
- if (agentEnvBackgroundInput) {
8102
- agentEnvBackgroundInput.value = agentConfigState.background === true ? 'true' : 'false'
8103
- }
8104
- if (agentEnvIsolationInput) {
8105
- const nextIsolation =
8106
- agentConfigState.isolation === 'worktree' || agentConfigState.isolation === 'remote'
8107
- ? agentConfigState.isolation
8108
- : ''
8109
- agentEnvIsolationInput.value = nextIsolation
8110
- }
8111
- void renderAgentEnvTeamMembers()
8112
8052
  void loadAgentEnvAgentMdFromApi()
8113
8053
  }
8114
8054
 
8115
8055
  /**
8116
- * effort 下拉框值转换为后端可接受的类型。
8117
- * 业务逻辑:
8118
- * 1) 空值表示不设置 effort,统一写回 null;
8119
- * 2) low/medium/high/max 直接透传;
8120
- * 3) 兼容历史整数值(旧版本允许手输整数),继续转成 number 写回;
8121
- * 4) 其他值视为非法,阻止保存并提示。
8056
+ * 深拷贝 agent.json.llmProviders 到设置页草稿。
8122
8057
  */
8123
- function parseAgentEffortFromInput(raw) {
8124
- const v = String(raw || '').trim()
8125
- if (!v) {
8126
- return { ok: true, value: null }
8058
+ function cloneLlmProvidersDraft(raw) {
8059
+ const list = Array.isArray(raw) ? raw : []
8060
+ return list.map(function (p) {
8061
+ const models = Array.isArray(p.models) ? p.models : []
8062
+ return {
8063
+ id: typeof p.id === 'string' ? p.id.trim() : '',
8064
+ name: typeof p.name === 'string' ? p.name.trim() : '',
8065
+ baseUrl: typeof p.baseUrl === 'string' ? p.baseUrl.trim() : '',
8066
+ apiKey: typeof p.apiKey === 'string' ? p.apiKey : '',
8067
+ authToken: typeof p.authToken === 'string' ? p.authToken : '',
8068
+ provider: p.provider === 'anthropic' ? 'anthropic' : 'openai',
8069
+ models: models.map(function (m) {
8070
+ return {
8071
+ id: typeof m.id === 'string' ? m.id.trim() : '',
8072
+ label: typeof m.label === 'string' ? m.label.trim() : '',
8073
+ }
8074
+ }),
8075
+ defaultModel: typeof p.defaultModel === 'string' ? p.defaultModel.trim() : '',
8076
+ }
8077
+ })
8078
+ }
8079
+
8080
+ function clearLlmProviderForm() {
8081
+ if (llmFormId) llmFormId.value = ''
8082
+ if (llmFormName) llmFormName.value = ''
8083
+ if (llmFormProvider) llmFormProvider.value = 'openai'
8084
+ if (llmFormBaseUrl) llmFormBaseUrl.value = ''
8085
+ if (llmFormApiKey) llmFormApiKey.value = ''
8086
+ if (llmFormAuthToken) llmFormAuthToken.value = ''
8087
+ if (llmFormModelId) llmFormModelId.value = ''
8088
+ if (llmFormModelLabel) llmFormModelLabel.value = ''
8089
+ }
8090
+
8091
+ /**
8092
+ * 从添加表单读取一条服务商配置;校验失败返回错误文案。
8093
+ */
8094
+ function readLlmProviderFromForm() {
8095
+ const id = llmFormId ? String(llmFormId.value || '').trim() : ''
8096
+ const name = llmFormName ? String(llmFormName.value || '').trim() : ''
8097
+ const provider =
8098
+ llmFormProvider && llmFormProvider.value === 'anthropic' ? 'anthropic' : 'openai'
8099
+ const baseUrl = llmFormBaseUrl ? String(llmFormBaseUrl.value || '').trim() : ''
8100
+ const apiKey = llmFormApiKey ? String(llmFormApiKey.value || '').trim() : ''
8101
+ const authToken = llmFormAuthToken ? String(llmFormAuthToken.value || '').trim() : ''
8102
+ const modelId = llmFormModelId ? String(llmFormModelId.value || '').trim() : ''
8103
+ const modelLabel = llmFormModelLabel ? String(llmFormModelLabel.value || '').trim() : ''
8104
+ if (!id) return { ok: false, message: '请填写服务商 ID' }
8105
+ if (!/^[A-Za-z0-9_-]+$/.test(id)) {
8106
+ return { ok: false, message: '服务商 id 仅允许字母数字下划线中划线' }
8107
+ }
8108
+ if (llmProvidersDraft.some(function (p) {
8109
+ return p.id === id
8110
+ })) {
8111
+ return { ok: false, message: '服务商 id 已存在: ' + id }
8112
+ }
8113
+ if (!name) return { ok: false, message: '请填写展示名称' }
8114
+ if (!baseUrl) return { ok: false, message: '请填写 Base URL' }
8115
+ if (!apiKey && !authToken) {
8116
+ return { ok: false, message: 'API Key 与 Auth Token 至少填写一项' }
8117
+ }
8118
+ if (!modelId) return { ok: false, message: '请填写模型 ID' }
8119
+ return {
8120
+ ok: true,
8121
+ entry: {
8122
+ id: id,
8123
+ name: name,
8124
+ baseUrl: baseUrl,
8125
+ apiKey: apiKey,
8126
+ authToken: authToken,
8127
+ provider: provider,
8128
+ models: [{ id: modelId, label: modelLabel }],
8129
+ defaultModel: modelId,
8130
+ },
8127
8131
  }
8128
- const lower = v.toLowerCase()
8129
- if (lower === 'low' || lower === 'medium' || lower === 'high' || lower === 'max') {
8130
- return { ok: true, value: lower }
8132
+ }
8133
+
8134
+ function renderLlmProvidersList() {
8135
+ if (!llmProvidersList) return
8136
+ llmProvidersList.innerHTML = ''
8137
+ if (llmProvidersEmptyHint) {
8138
+ llmProvidersEmptyHint.hidden = llmProvidersDraft.length > 0
8131
8139
  }
8132
- if (/^-?\d+$/.test(v)) {
8133
- return { ok: true, value: Number(v) }
8140
+ for (let i = 0; i < llmProvidersDraft.length; i++) {
8141
+ const p = llmProvidersDraft[i]
8142
+ const li = document.createElement('li')
8143
+ li.className = 'llm-provider-list-item'
8144
+
8145
+ const nameWrap = document.createElement('span')
8146
+ nameWrap.className = 'llm-provider-list-name'
8147
+ nameWrap.textContent = p.name || p.id
8148
+ if (i === 0) {
8149
+ const badge = document.createElement('span')
8150
+ badge.className = 'llm-provider-list-default'
8151
+ badge.textContent = '默认'
8152
+ nameWrap.appendChild(badge)
8153
+ }
8154
+
8155
+ const removeBtn = document.createElement('button')
8156
+ removeBtn.type = 'button'
8157
+ removeBtn.className = 'llm-provider-list-remove'
8158
+ removeBtn.textContent = '删除'
8159
+ removeBtn.addEventListener('click', function () {
8160
+ llmProvidersDraft.splice(i, 1)
8161
+ renderLlmProvidersList()
8162
+ })
8163
+
8164
+ li.appendChild(nameWrap)
8165
+ li.appendChild(removeBtn)
8166
+ llmProvidersList.appendChild(li)
8134
8167
  }
8135
- return { ok: false, message: 'effort 仅支持 low/medium/high/max 或整数' }
8168
+ }
8169
+
8170
+ function initLlmProvidersSettingsUi() {
8171
+ llmProvidersDraft = cloneLlmProvidersDraft(agentConfigState.llmProviders)
8172
+ clearLlmProviderForm()
8173
+ renderLlmProvidersList()
8136
8174
  }
8137
8175
 
8138
8176
  /**
8139
- * 同步 effort 下拉框可选项并回填当前值。
8140
- * 业务逻辑:
8141
- * 1) 常规场景只展示固定档位(low/medium/high/max);
8142
- * 2) 若 agent.json 中已有历史整数值,为避免用户打开弹窗后被意外清空,
8143
- * 动态追加一个“当前值”选项用于承载该值;
8144
- * 3) 每次打开弹窗先清理旧的动态选项,防止重复追加。
8177
+ * 保存前校验 llmProviders 草稿;首项为默认厂商,允许空数组。
8145
8178
  */
8146
- function syncAgentEffortSelectValue(rawEffort) {
8147
- if (!agentEnvEffortInput) return
8148
- const dynamicOptions = agentEnvEffortInput.querySelectorAll('option[data-dynamic-effort="true"]')
8149
- dynamicOptions.forEach(function (opt) {
8150
- if (opt && opt.parentNode) {
8151
- opt.parentNode.removeChild(opt)
8179
+ function validateLlmProvidersDraftForSave(providers) {
8180
+ const list = Array.isArray(providers) ? providers : []
8181
+ if (!list.length) return ''
8182
+ const seenIds = new Set()
8183
+ for (let i = 0; i < list.length; i++) {
8184
+ const p = list[i]
8185
+ const id = String(p.id || '').trim()
8186
+ if (!id) return '第 ' + (i + 1) + ' 个服务商缺少 id'
8187
+ if (!/^[A-Za-z0-9_-]+$/.test(id)) {
8188
+ return '服务商 id "' + id + '" 仅允许字母数字下划线中划线'
8189
+ }
8190
+ if (seenIds.has(id)) return '服务商 id 重复: ' + id
8191
+ seenIds.add(id)
8192
+ if (!String(p.name || '').trim()) return '服务商 "' + id + '" 缺少展示名称'
8193
+ if (!String(p.baseUrl || '').trim()) return '服务商 "' + id + '" 缺少 baseUrl'
8194
+ if (p.provider !== 'anthropic' && p.provider !== 'openai') {
8195
+ return '服务商 "' + id + '" 协议类型非法'
8196
+ }
8197
+ const apiKeyOk =
8198
+ p.apiKey === LLM_MASKED_CREDENTIAL || String(p.apiKey || '').trim().length > 0
8199
+ const authTokenOk =
8200
+ p.authToken === LLM_MASKED_CREDENTIAL ||
8201
+ String(p.authToken || '').trim().length > 0
8202
+ if (!apiKeyOk && !authTokenOk) {
8203
+ return '服务商 "' + id + '" 需配置 API Key 或 Auth Token 至少一项'
8204
+ }
8205
+ if (!Array.isArray(p.models) || p.models.length === 0) {
8206
+ return '服务商 "' + id + '" 至少配置 1 个模型'
8207
+ }
8208
+ for (let j = 0; j < p.models.length; j++) {
8209
+ if (!String(p.models[j].id || '').trim()) {
8210
+ return '服务商 "' + id + '" 第 ' + (j + 1) + ' 个模型缺少 id'
8211
+ }
8152
8212
  }
8153
- })
8154
- if (rawEffort === null || rawEffort === undefined || String(rawEffort).trim() === '') {
8155
- agentEnvEffortInput.value = 'low'
8156
- return
8157
- }
8158
- const raw = String(rawEffort).trim()
8159
- const lower = raw.toLowerCase()
8160
- if (lower === 'low' || lower === 'medium' || lower === 'high' || lower === 'max') {
8161
- agentEnvEffortInput.value = lower
8162
- return
8163
- }
8164
- if (/^-?\d+$/.test(raw)) {
8165
- const opt = document.createElement('option')
8166
- opt.value = raw
8167
- opt.textContent = '当前值:' + raw + '(历史整数)'
8168
- opt.setAttribute('data-dynamic-effort', 'true')
8169
- agentEnvEffortInput.appendChild(opt)
8170
- agentEnvEffortInput.value = raw
8171
- return
8213
+ const defaultModel = String(p.defaultModel || '').trim() || p.models[0].id
8214
+ if (
8215
+ !p.models.some(function (m) {
8216
+ return m.id === defaultModel
8217
+ })
8218
+ ) {
8219
+ return '服务商 "' + id + '" 默认模型不在模型列表中'
8220
+ }
8221
+ p.defaultModel = defaultModel
8172
8222
  }
8173
- agentEnvEffortInput.value = 'low'
8223
+ return ''
8174
8224
  }
8175
8225
 
8176
8226
  function openAgentMdModal() {
@@ -8244,30 +8294,11 @@
8244
8294
  btnAgentEnvSave.disabled = true
8245
8295
  }
8246
8296
  try {
8247
- const whentouseValue = agentEnvWhenToUseInput
8248
- ? String(agentEnvWhenToUseInput.value || '')
8249
- : ''
8250
- const nextModel = agentEnvModelInput
8251
- ? String(agentEnvModelInput.value || '').trim()
8252
- : ''
8253
- const effortParsed = parseAgentEffortFromInput(
8254
- agentEnvEffortInput ? agentEnvEffortInput.value : '',
8255
- )
8256
- if (!effortParsed.ok) {
8257
- window.alert(effortParsed.message)
8258
- return
8259
- }
8260
- const nextBackground =
8261
- agentEnvBackgroundInput && agentEnvBackgroundInput.value === 'true'
8262
- const nextIsolationRaw = agentEnvIsolationInput
8263
- ? String(agentEnvIsolationInput.value || '').trim()
8264
- : ''
8265
- if (
8266
- nextIsolationRaw !== '' &&
8267
- nextIsolationRaw !== 'worktree' &&
8268
- nextIsolationRaw !== 'remote'
8269
- ) {
8270
- window.alert('isolation 仅支持 worktree/remote 或留空')
8297
+ const whenInput = queryAgentEnvWhenToUseInput()
8298
+ const whentouseValue = whenInput ? String(whenInput.value || '') : ''
8299
+ const llmErr = validateLlmProvidersDraftForSave(llmProvidersDraft)
8300
+ if (llmErr) {
8301
+ window.alert('LLM:' + llmErr)
8271
8302
  return
8272
8303
  }
8273
8304
  const patchBody = {
@@ -8277,12 +8308,17 @@
8277
8308
  mcpServers: agentConfigState.mcpServers.slice(),
8278
8309
  skills: agentConfigState.skills.slice(),
8279
8310
  agentTeams: agentConfigState.agentTeams.slice(),
8280
- model: nextModel,
8281
- effort: effortParsed.value,
8282
- background: nextBackground,
8283
- isolation: nextIsolationRaw,
8311
+ model: typeof agentConfigState.model === 'string' ? agentConfigState.model : '',
8312
+ effort: agentConfigState.effort,
8313
+ background: agentConfigState.background === true,
8314
+ isolation:
8315
+ agentConfigState.isolation === 'worktree' ||
8316
+ agentConfigState.isolation === 'remote'
8317
+ ? agentConfigState.isolation
8318
+ : '',
8284
8319
  whentouse: whentouseValue,
8285
8320
  env: checked.env,
8321
+ llmProviders: cloneLlmProvidersDraft(llmProvidersDraft),
8286
8322
  },
8287
8323
  }
8288
8324
  const includeAgentMd =
@@ -8299,14 +8335,14 @@
8299
8335
  return {}
8300
8336
  })
8301
8337
  if (!r.ok) {
8302
- window.alert('保存失败:' + (body.message || body.error || r.statusText))
8338
+ window.alert(
8339
+ 'Agent 配置保存失败:' + (body.message || body.error || r.statusText),
8340
+ )
8303
8341
  return
8304
8342
  }
8343
+ void refreshModelCatalogFromApi(true)
8305
8344
  agentConfigState.env = Object.assign({}, checked.env)
8306
- agentConfigState.model = nextModel
8307
- agentConfigState.effort = effortParsed.value
8308
- agentConfigState.background = nextBackground
8309
- agentConfigState.isolation = nextIsolationRaw
8345
+ agentConfigState.llmProviders = cloneLlmProvidersDraft(llmProvidersDraft)
8310
8346
  agentConfigState.whentouse = whentouseValue
8311
8347
  syncPermissionModeSelectFromState()
8312
8348
  if (includeAgentMd) {
@@ -8318,15 +8354,15 @@
8318
8354
  window.alert('已保存到磁盘,但同步提示词到当前连接失败: ' + e)
8319
8355
  }
8320
8356
  }
8321
- restartCurrentSessionSubprocessWithReason('save-agent-env-prompt')
8357
+ restartCurrentSessionSubprocessWithReason('save-agent-settings-prompt')
8322
8358
  } else {
8323
- restartCurrentSessionSubprocessWithReason('save-agent-env')
8359
+ restartCurrentSessionSubprocessWithReason('save-agent-settings')
8324
8360
  }
8325
- closeAgentEnvModal()
8361
+ closeAgentSettingsView()
8326
8362
  setStatus('已保存', serverReady ? 'ready' : '')
8327
8363
  window.alert('已保存到磁盘,并已重连 / 尝试同步当前 session。')
8328
8364
  } catch (e) {
8329
- window.alert('保存失败: ' + e)
8365
+ window.alert(String(e && e.message ? e.message : e))
8330
8366
  } finally {
8331
8367
  if (btnAgentEnvSave) {
8332
8368
  btnAgentEnvSave.disabled = false
@@ -8372,7 +8408,7 @@
8372
8408
  }
8373
8409
  if (btnEditAgentEnv) {
8374
8410
  btnEditAgentEnv.addEventListener('click', function () {
8375
- openAgentEnvModal('env')
8411
+ openAgentSettingsView('settings')
8376
8412
  })
8377
8413
  }
8378
8414
  if (btnSkillPackageManager) {
@@ -8621,23 +8657,24 @@
8621
8657
  openToolFilesModal()
8622
8658
  })
8623
8659
  }
8624
- if (btnAgentEnvModalClose) {
8625
- btnAgentEnvModalClose.addEventListener('click', closeAgentEnvModal)
8626
- }
8627
8660
  if (btnAgentEnvCancel) {
8628
- btnAgentEnvCancel.addEventListener('click', closeAgentEnvModal)
8629
- }
8630
- if (agentEnvModalBackdrop) {
8631
- agentEnvModalBackdrop.addEventListener('click', closeAgentEnvModal)
8661
+ btnAgentEnvCancel.addEventListener('click', closeAgentSettingsView)
8632
8662
  }
8633
8663
  if (btnAgentEnvSave) {
8634
8664
  btnAgentEnvSave.addEventListener('click', function () {
8635
8665
  void saveAgentEnvAndReconnect()
8636
8666
  })
8637
8667
  }
8638
- if (btnAgentEnvAddTeamMember) {
8639
- btnAgentEnvAddTeamMember.addEventListener('click', function () {
8640
- void addAgentEnvTeamMember()
8668
+ if (btnLlmProvidersAdd) {
8669
+ btnLlmProvidersAdd.addEventListener('click', function () {
8670
+ const parsed = readLlmProviderFromForm()
8671
+ if (!parsed.ok) {
8672
+ window.alert(parsed.message)
8673
+ return
8674
+ }
8675
+ llmProvidersDraft.push(parsed.entry)
8676
+ clearLlmProviderForm()
8677
+ renderLlmProvidersList()
8641
8678
  })
8642
8679
  }
8643
8680
  if (btnAgentMdModalClose) {
@@ -8705,10 +8742,6 @@
8705
8742
  closeSkillMdModal()
8706
8743
  return
8707
8744
  }
8708
- if (agentEnvModal && agentEnvModal.classList.contains('is-open')) {
8709
- closeAgentEnvModal()
8710
- return
8711
- }
8712
8745
  if (scheduleRunsModal && scheduleRunsModal.classList.contains('is-open')) {
8713
8746
  closeScheduleRunsModal()
8714
8747
  return
@@ -8758,6 +8791,9 @@
8758
8791
  void bootstrapSessionSelection()
8759
8792
  void refreshModelCatalogFromApi(true)
8760
8793
  void loadAgentConfig()
8794
+ if (isAgentSettingsViewActive()) {
8795
+ openAgentSettingsView(agentEnvActiveTab)
8796
+ }
8761
8797
  }
8762
8798
  if (agentIdInput) {
8763
8799
  agentIdInput.addEventListener('change', onAgentChanged)