tagword-codeagent 1.1.6__py3-none-any.whl

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 (173) hide show
  1. codeagent/__init__.py +10 -0
  2. codeagent/__main__.py +13 -0
  3. codeagent/cli/__init__.py +13 -0
  4. codeagent/cli/entrypoint.py +161 -0
  5. codeagent/cli/main.py +979 -0
  6. codeagent/core/__init__.py +2 -0
  7. codeagent/core/attachments.py +342 -0
  8. codeagent/core/audio_models.py +50 -0
  9. codeagent/core/bootstrap.py +178 -0
  10. codeagent/core/default_presets.py +159 -0
  11. codeagent/core/env.py +106 -0
  12. codeagent/core/folder_picker.py +151 -0
  13. codeagent/core/image_understanding.py +90 -0
  14. codeagent/core/models.py +6 -0
  15. codeagent/core/paths.py +238 -0
  16. codeagent/core/pricing.py +107 -0
  17. codeagent/core/process_ports.py +258 -0
  18. codeagent/core/seed_bridge.py +21 -0
  19. codeagent/core/settings.py +98 -0
  20. codeagent/core/speech_synth.py +330 -0
  21. codeagent/core/team_config.py +247 -0
  22. codeagent/core/team_manager.py +245 -0
  23. codeagent/core/token_counter.py +41 -0
  24. codeagent/core/tts_voices.py +103 -0
  25. codeagent/core/usage_billing.py +72 -0
  26. codeagent/core/vision_models.py +93 -0
  27. codeagent/memory/diary.py +144 -0
  28. codeagent/persona_defaults/README.md +12 -0
  29. codeagent/persona_defaults/agent.md +352 -0
  30. codeagent/persona_defaults/identity.md +29 -0
  31. codeagent/persona_defaults/memory.md +4 -0
  32. codeagent/persona_defaults/skills.md +325 -0
  33. codeagent/persona_defaults/soul.md +38 -0
  34. codeagent/persona_defaults/tools.md +41 -0
  35. codeagent/persona_defaults/user.md +18 -0
  36. codeagent/runtime/__init__.py +11 -0
  37. codeagent/runtime/agent_context.py +6 -0
  38. codeagent/runtime/agent_runtime.py +21 -0
  39. codeagent/runtime/compact_prompt.py +9 -0
  40. codeagent/runtime/compact_state.py +40 -0
  41. codeagent/runtime/llm_worker.py +132 -0
  42. codeagent/runtime/orchestrator.py +161 -0
  43. codeagent/runtime/prompt_enrichment.py +324 -0
  44. codeagent/runtime/session_manager.py +30 -0
  45. codeagent/runtime/task_split.py +38 -0
  46. codeagent/runtime/turn_loop.py +13 -0
  47. codeagent/runtime/worker.py +48 -0
  48. codeagent/server/__init__.py +505 -0
  49. codeagent/server/app_factory.py +1292 -0
  50. codeagent/server/attachment_api.py +261 -0
  51. codeagent/server/hub.py +175 -0
  52. codeagent/server/icon.png +0 -0
  53. codeagent/server/self_healing.py +143 -0
  54. codeagent/server/team_engine.py +243 -0
  55. codeagent/server/tts_api.py +51 -0
  56. codeagent/server/webui_api_app.py +3180 -0
  57. codeagent/skills/__init__.py +2 -0
  58. codeagent/skills/select.py +144 -0
  59. codeagent/tools/__init__.py +6 -0
  60. codeagent/tools/agent_tools.py +10 -0
  61. codeagent/web/__init__.py +1 -0
  62. codeagent/web/__pycache__/__init__.cpython-313.pyc +0 -0
  63. codeagent/web/__pycache__/app.cpython-313.pyc +0 -0
  64. codeagent/web/__pycache__/auth_impl.cpython-313.pyc +0 -0
  65. codeagent/web/auth_impl.py +326 -0
  66. codeagent/web/static/00-marked.min.js +6 -0
  67. codeagent/web/static/00-mermaid.min.js +2314 -0
  68. codeagent/web/static/00-purify.min.js +3 -0
  69. codeagent/web/static/00-storage.js +83 -0
  70. codeagent/web/static/00-utils.js +51 -0
  71. codeagent/web/static/01-theme.css +96 -0
  72. codeagent/web/static/01a-scroll.js +86 -0
  73. codeagent/web/static/01b-think.js +62 -0
  74. codeagent/web/static/01c-session-identity.js +81 -0
  75. codeagent/web/static/01c-session-thinking.js +61 -0
  76. codeagent/web/static/01c-session-tree-menus.js +349 -0
  77. codeagent/web/static/01c-session-tree.js +531 -0
  78. codeagent/web/static/01c-session-utilities.js +83 -0
  79. codeagent/web/static/01c-vision-model.js +132 -0
  80. codeagent/web/static/01d-image-gen-model.js +84 -0
  81. codeagent/web/static/01e-audio-model.js +97 -0
  82. codeagent/web/static/01f-compose-ui.js +88 -0
  83. codeagent/web/static/01g-tts-settings.js +128 -0
  84. codeagent/web/static/01h-music-model.js +85 -0
  85. codeagent/web/static/01i-model-stack.js +291 -0
  86. codeagent/web/static/01j-video-gen-model.js +85 -0
  87. codeagent/web/static/01k-model-stack-bootstrap.js +66 -0
  88. codeagent/web/static/01k-model-stack-state.js +209 -0
  89. codeagent/web/static/01q-inflight.js +195 -0
  90. codeagent/web/static/02-reset.css +22 -0
  91. codeagent/web/static/02-session-ui.js +201 -0
  92. codeagent/web/static/03-layout-p1.css +2 -0
  93. codeagent/web/static/03-layout-p2.css +2 -0
  94. codeagent/web/static/03a-links-stream.js +88 -0
  95. codeagent/web/static/03b-stream-text.js +119 -0
  96. codeagent/web/static/03c-render-bubble.js +520 -0
  97. codeagent/web/static/03d-agent-bubble.js +126 -0
  98. codeagent/web/static/03e-tool-trace.js +182 -0
  99. codeagent/web/static/03f-timeline.js +54 -0
  100. codeagent/web/static/03g-bubble-tts.js +292 -0
  101. codeagent/web/static/04-components-p1.css +1 -0
  102. codeagent/web/static/04-components-p2b.css +1 -0
  103. codeagent/web/static/04-components-p3.css +2 -0
  104. codeagent/web/static/04-components-p4.css +1 -0
  105. codeagent/web/static/04-components-p5.css +1 -0
  106. codeagent/web/static/04-components-p6.css +58 -0
  107. codeagent/web/static/04-components-p7.css +4 -0
  108. codeagent/web/static/04-components-p8.css +1 -0
  109. codeagent/web/static/04-components-p9.css +1 -0
  110. codeagent/web/static/04-ws-connect.js +351 -0
  111. codeagent/web/static/04-ws-helpers.js +116 -0
  112. codeagent/web/static/05-chat-p1.css +1058 -0
  113. codeagent/web/static/05-chat-p2.css +1 -0
  114. codeagent/web/static/05-chat-p3.css +295 -0
  115. codeagent/web/static/05a-session-history.js +176 -0
  116. codeagent/web/static/05b-session-list.js +114 -0
  117. codeagent/web/static/05c-dialog.js +123 -0
  118. codeagent/web/static/05d-project-create.js +172 -0
  119. codeagent/web/static/05e-project-remote.js +143 -0
  120. codeagent/web/static/06-chat.js +552 -0
  121. codeagent/web/static/06-utilities.css +181 -0
  122. codeagent/web/static/06b-attachments.js +304 -0
  123. codeagent/web/static/06c-camera.js +488 -0
  124. codeagent/web/static/07-activity-bar.css +127 -0
  125. codeagent/web/static/09-git-p1.css +1 -0
  126. codeagent/web/static/09-git-p2.css +1 -0
  127. codeagent/web/static/09a-init-bind.js +110 -0
  128. codeagent/web/static/09b-panel-toggle.js +80 -0
  129. codeagent/web/static/09c-api-refresh.js +100 -0
  130. codeagent/web/static/09d-status-log.js +80 -0
  131. codeagent/web/static/09e-commit.js +80 -0
  132. codeagent/web/static/09f-remote.js +93 -0
  133. codeagent/web/static/11a-cron-parse.js +117 -0
  134. codeagent/web/static/11b-cron-panel.js +100 -0
  135. codeagent/web/static/11c-cron-card.js +133 -0
  136. codeagent/web/static/11d-cron-form.js +105 -0
  137. codeagent/web/static/11e-env-config.js +168 -0
  138. codeagent/web/static/11f-env-llm-a-state.js +136 -0
  139. codeagent/web/static/11f-env-llm-b-form.js +409 -0
  140. codeagent/web/static/11f-env-llm-c-board.js +322 -0
  141. codeagent/web/static/11g-env-paths-git.js +973 -0
  142. codeagent/web/static/11h-env-mcp-a-state.js +161 -0
  143. codeagent/web/static/11h-env-mcp-b-form.js +229 -0
  144. codeagent/web/static/11h-env-mcp-c-board.js +565 -0
  145. codeagent/web/static/12-plugins.js +127 -0
  146. codeagent/web/static/13a-skills.js +156 -0
  147. codeagent/web/static/13a-todo-utils.js +140 -0
  148. codeagent/web/static/13b-todo-fetch.js +106 -0
  149. codeagent/web/static/13b-tools.js +127 -0
  150. codeagent/web/static/13c-todo-crud.js +92 -0
  151. codeagent/web/static/14-activity-bar.js +177 -0
  152. codeagent/web/static/15-plans.css +194 -0
  153. codeagent/web/static/15-plans.js +219 -0
  154. codeagent/web/static/16-mobile.css +612 -0
  155. codeagent/web/static/16-mobile.js +178 -0
  156. codeagent/web/static/17-preset-ux.css +287 -0
  157. codeagent/web/static/18-agent-mgr.css +616 -0
  158. codeagent/web/static/18-agent-mgr.js +204 -0
  159. codeagent/web/static/24-health-dashboard.js +68 -0
  160. codeagent/web/static/99-brand-polish.css +152 -0
  161. codeagent/web/static/body.html +824 -0
  162. codeagent/web/static/setup/setup_form.html +171 -0
  163. codeagent/web/static/setup/setup_head.html +100 -0
  164. codeagent/web/static/setup/setup_tail.html +264 -0
  165. codeagent/web/static/setup/setup_test.html +38 -0
  166. codeagent/web/web_login.html +265 -0
  167. codeagent/web/webui.html +35 -0
  168. tagword_codeagent-1.1.6.dist-info/METADATA +273 -0
  169. tagword_codeagent-1.1.6.dist-info/RECORD +173 -0
  170. tagword_codeagent-1.1.6.dist-info/WHEEL +5 -0
  171. tagword_codeagent-1.1.6.dist-info/entry_points.txt +3 -0
  172. tagword_codeagent-1.1.6.dist-info/licenses/LICENSE +21 -0
  173. tagword_codeagent-1.1.6.dist-info/top_level.txt +1 -0
@@ -0,0 +1,100 @@
1
+ const toggle = document.getElementById('cronGlobalToggle');
2
+ const list = document.getElementById('cronJobList');
3
+ if (!toggle) return;
4
+ toggle.classList.remove('is-err');
5
+ try {
6
+ const r = await fetch('/api/ui/flags?agent_id=' + encodeURIComponent(agentId));
7
+ if (!r.ok) throw new Error(String(r.status));
8
+ const f = await r.json();
9
+ const c = f.cron;
10
+ if (!c || c.error) {
11
+ return;
12
+ }
13
+
14
+ // Global enabled state
15
+ cronGlobalEnabled = c.config_enabled && !c.env_disabled;
16
+ toggle.classList.toggle('on', cronGlobalEnabled);
17
+
18
+ // Render job cards
19
+ list.innerHTML = '';
20
+ const jobs = c.jobs_config || [];
21
+ if (jobs.length === 0) {
22
+ list.innerHTML = '<div class="cron-empty">暂无定时任务,点击"+ 新增"添加。</div>';
23
+ } else {
24
+ jobs.forEach(function(j) { list.appendChild(buildCronJobCard(j)); });
25
+ }
26
+ } catch (e) {
27
+ }
28
+ }
29
+
30
+ // ---- Global toggle ----
31
+ document.getElementById('cronGlobalToggle') && document.getElementById('cronGlobalToggle').addEventListener('click', async function() {
32
+ const toggle = this;
33
+ const newVal = !cronGlobalEnabled;
34
+ toggle.classList.add('is-err');
35
+ try {
36
+ const r = await fetch('/api/ui/cron/toggle', {
37
+ method: 'POST',
38
+ headers: { 'Content-Type': 'application/json' },
39
+ body: JSON.stringify({ enabled: newVal })
40
+ });
41
+ if (!r.ok) throw new Error((await r.json()).detail || r.statusText);
42
+ cronGlobalEnabled = newVal;
43
+ toggle.classList.toggle('on', newVal);
44
+ await loadCronPanel();
45
+ } catch (e) {
46
+ toggle.classList.add('is-err');
47
+ const st = document.getElementById('cronUiStatus');
48
+ if (st) { st.classList.add('is-err'); st.textContent = String(e); }
49
+ }
50
+ toggle.classList.remove('is-err');
51
+ });
52
+
53
+ // ---- Build a cron job card ----
54
+ function buildCronJobCard(j) {
55
+ var wrap = document.createElement('div'); wrap.className = 'cron-card-wrap';
56
+ var header = document.createElement('div'); header.className = 'cron-card';
57
+ var info = document.createElement('div'); info.className = 'cron-card__info';
58
+ var nameRow = document.createElement('div'); nameRow.className = 'cron-card__name';
59
+ var dot = document.createElement('span'); dot.className = 'cron-card__status-dot' + (j.enabled ? ' on' : ' off');
60
+ nameRow.appendChild(dot);
61
+ nameRow.appendChild(document.createTextNode(j.title || j.id || '未命名任务'));
62
+ var badge = document.createElement('span');
63
+ badge.className = 'cron-card__badge' + (j.enabled ? '' : ' cron-card__badge-off');
64
+ badge.textContent = j.enabled ? '启用' : '停用';
65
+ nameRow.appendChild(badge);
66
+ info.appendChild(nameRow);
67
+ var detail = document.createElement('div'); detail.className = 'cron-card__detail';
68
+ detail.textContent = describeCron(j.cron) + ' — agent: ' + (j.agent_id || 'default');
69
+ if (j.timezone) detail.textContent += ' (TZ: ' + j.timezone + ')';
70
+ info.appendChild(detail);
71
+ var actions = document.createElement('div'); actions.className = 'cron-card__actions';
72
+ var btnToggle = document.createElement('button');
73
+ btnToggle.type = 'button'; btnToggle.className = 'btn btn--ghost btn--xs';
74
+ btnToggle.textContent = j.enabled ? '停用' : '启用';
75
+ btnToggle.addEventListener('click', async function(ev) {
76
+ ev.stopPropagation();
77
+ try {
78
+ var newJob = JSON.parse(JSON.stringify(j));
79
+ newJob.enabled = !j.enabled;
80
+ var r = await fetch('/api/ui/cron/job', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newJob) });
81
+ if (!r.ok) throw new Error((await r.json()).detail || r.statusText);
82
+ await loadCronPanel();
83
+ } catch (e) { showCronError(String(e)); }
84
+ });
85
+ actions.appendChild(btnToggle);
86
+ var btnDel = document.createElement('button');
87
+ btnDel.type = 'button'; btnDel.className = 'btn btn--ghost btn--xs cron-del'; btnDel.textContent = '删除';
88
+ btnDel.addEventListener('click', async function(ev) {
89
+ ev.stopPropagation();
90
+ if (!confirm('确定删除定时任务「' + (j.title || j.id || '') + '」?')) return;
91
+ try {
92
+ var r = await fetch('/api/ui/cron/job/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ job_id: j.id }) });
93
+ if (!r.ok) throw new Error((await r.json()).detail || r.statusText);
94
+ await loadCronPanel();
95
+ } catch (e) { showCronError(String(e)); }
96
+ });
97
+ actions.appendChild(btnDel);
98
+ header.appendChild(info); header.appendChild(actions);
99
+ var editWrap = document.createElement('div'); editWrap.className = 'cron-edit-wrap'; editWrap.style.display = 'none';
100
+ var editStatus = document.createElement('div'); editStatus.className = 'status-line';
@@ -0,0 +1,133 @@
1
+ editWrap.innerHTML = buildCronEditFormHTML(j);
2
+ editWrap.appendChild(editStatus);
3
+ var toggleEdit = function(show) { editWrap.style.display = show ? 'block' : 'none'; editStatus.textContent = ''; editStatus.classList.remove('is-err'); };
4
+ // Click the card header to toggle edit
5
+ header.style.cursor = 'pointer';
6
+ header.addEventListener('click', function(e) {
7
+ if (e.target.closest('.btn') || e.target.closest('.cron-card__actions')) return;
8
+ toggleEdit(editWrap.style.display !== 'block');
9
+ });
10
+
11
+ // Store original job id so we can detect rename and delete old entry
12
+ var origJobId = j.id;
13
+
14
+ // Wire up mode switch + day pills
15
+ wireCronFormEvents(editWrap);
16
+
17
+ editWrap.querySelector('.cron-save-btn').addEventListener('click', async function() {
18
+ var freq = readFreqFromForm(editWrap);
19
+ var name = editWrap.querySelector('.cron-fld-name').value.trim();
20
+ var agent = editWrap.querySelector('.cron-fld-agent').value.trim();
21
+ var session = editWrap.querySelector('.cron-fld-session').value.trim();
22
+ var prompt = editWrap.querySelector('.cron-fld-prompt').value.trim();
23
+ var tz = editWrap.querySelector('.cron-fld-tz').value.trim();
24
+ var maxRounds = parseInt(editWrap.querySelector('.cron-fld-rounds').value) || 12;
25
+ var maxContinuations = parseInt(editWrap.querySelector('.cron-fld-continuations').value) || 0;
26
+ var enabled = editWrap.querySelector('.cron-fld-enabled').checked;
27
+ var projectId = editWrap.querySelector('.cron-fld-project').value.trim();
28
+ if (!name) { editStatus.textContent = '任务名称不能为空'; editStatus.classList.add('is-err'); return; }
29
+ if (!prompt) { editStatus.textContent = 'Prompt 不能为空'; editStatus.classList.add('is-err'); return; }
30
+ editStatus.textContent = '保存中…'; editStatus.classList.remove('is-err');
31
+ try {
32
+ var displayName = name.trim();
33
+ if (!origJobId) { editStatus.textContent = '内部错误:缺少任务 id'; editStatus.classList.add('is-err'); return; }
34
+ var kv = {
35
+ id: origJobId,
36
+ title: displayName,
37
+ enabled: enabled,
38
+ cron: freqToCron(freq),
39
+ agent_id: agent || 'default',
40
+ session_id: session || ('cron-' + origJobId),
41
+ prompt: prompt,
42
+ max_tool_rounds: maxRounds,
43
+ max_continuations: maxContinuations,
44
+ };
45
+ if (tz) kv.timezone = tz;
46
+ if (projectId) kv.project_id = projectId;
47
+ var r = await fetch('/api/ui/cron/job', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(kv) });
48
+ var jr = await r.json().catch(function() { return {}; });
49
+ if (!r.ok) throw new Error(jr.detail || r.statusText);
50
+ editStatus.textContent = '已保存。';
51
+ toggleEdit(false);
52
+ await loadCronPanel();
53
+ } catch (e) { editStatus.classList.add('is-err'); editStatus.textContent = String(e); }
54
+ });
55
+ if (editWrap.querySelector('.cron-cancel-btn')) {
56
+ editWrap.querySelector('.cron-cancel-btn').addEventListener('click', function() { toggleEdit(false); });
57
+ }
58
+ wrap.appendChild(header); wrap.appendChild(editWrap);
59
+ return wrap;
60
+ }
61
+
62
+ /** HTML for the edit form — natural language only, no raw CRON fields */
63
+ function buildCronEditFormHTML(j) {
64
+ var f = parseCronToFreq(j.cron);
65
+ return '<div class="cron-form-grid">'
66
+ + '<div class="cron-form-full" style="display:flex;gap:var(--sp-4);align-items:start;">'
67
+ + ' <div style="flex:1;"><label class="form-label">任务名称</label><input class="cron-fld-name" type="text" value="' + escAttr(j.title || j.id || '') + '" placeholder="例如:检查消息"/></div>'
68
+ + ' <div style="width:80px;"><label class="form-label">启用</label><div style="margin-top:6px"><input class="cron-fld-enabled" type="checkbox" ' + (j.enabled !== false ? 'checked' : '') + '/></div></div>'
69
+ + '</div>'
70
+ + '<div class="cron-form-full"><label class="form-label">执行频率</label>'
71
+ + ' <select class="cron-fld-mode md-select" style="max-width:100%;">'
72
+ + CRON_FREQ_MODES.map(function(m) { return '<option value="' + m.id + '"' + (m.id === f.mode ? ' selected' : '') + '>' + m.label + '</option>'; }).join('')
73
+ + ' </select>'
74
+ + '</div>'
75
+ /* interval minutes */
76
+ + '<div class="cron-freq-block cron-freq-minutes' + (f.mode === 'minutes' ? '' : ' cron-freq-hidden') + '">'
77
+ + ' <label class="form-label">间隔分钟数</label>'
78
+ + ' <select class="cron-fld-interval md-select" style="max-width:100%;">'
79
+ + [5,10,15,20,30,45].map(function(v) { return '<option value="' + v + '"' + ((parseInt(f.interval) || 30) === v ? ' selected' : '') + '>每 ' + v + ' 分钟</option>'; }).join('')
80
+ + ' </select>'
81
+ + '</div>'
82
+ /* interval hours */
83
+ + '<div class="cron-freq-block cron-freq-hours' + (f.mode === 'hours' ? '' : ' cron-freq-hidden') + '">'
84
+ + ' <label class="form-label">间隔小时数</label>'
85
+ + ' <select class="cron-fld-interval md-select" style="max-width:100%;">'
86
+ + [1,2,3,4,6,8,12].map(function(v) { return '<option value="' + v + '"' + ((parseInt(f.interval) || 1) === v ? ' selected' : '') + '>每 ' + v + ' 小时</option>'; }).join('')
87
+ + ' </select>'
88
+ + ' <div style="margin-top:var(--sp-2);"><label class="form-label">在每小时的第几分钟</label>'
89
+ + ' <select class="cron-fld-minute md-select" style="max-width:100%;">'
90
+ + [0,5,10,15,20,25,30,35,40,45,50,55].map(function(v) { return '<option value="' + v + '"' + (parseInt(f.minute || '0') === v ? ' selected' : '') + '>' + v + ' 分</option>'; }).join('')
91
+ + ' </select></div>'
92
+ + '</div>'
93
+ /** daily/weekly/monthly: time picker */
94
+ + '<div class="cron-freq-block cron-freq-daily cron-freq-weekly cron-freq-monthly' + (f.mode === 'daily' || f.mode === 'weekly' || f.mode === 'monthly' ? '' : ' cron-freq-hidden') + '">'
95
+ + ' <label class="form-label">执行时间</label>'
96
+ + ' <div style="display:flex;gap:var(--sp-2);align-items:center;">'
97
+ + ' <select class="cron-fld-hour md-select" style="width:80px;">' + hoursOptions(f.hour) + '</select><span>:</span>'
98
+ + ' <select class="cron-fld-minute md-select" style="width:80px;">' + minutesOptions(f.minute) + '</select>'
99
+ + ' </div>'
100
+ + '</div>'
101
+ /* weekly day picker */
102
+ + '<div class="cron-freq-block cron-freq-weekly' + (f.mode === 'weekly' ? '' : ' cron-freq-hidden') + '">'
103
+ + ' <label class="form-label">选择星期</label>'
104
+ + ' <div style="display:flex;flex-wrap:wrap;gap:var(--sp-1);">'
105
+ + DAY_NAMES.map(function(dn, i) { return '<label class="cron-day-pill' + (f.dayOfWeek === String(i) ? ' cron-day-pill--on' : '') + '" data-day="' + i + '">' + dn + '</label>'; }).join('')
106
+ + ' </div>'
107
+ + ' <input class="cron-fld-dow" type="hidden" value="' + escAttr(f.dayOfWeek) + '"/>'
108
+ + '</div>'
109
+ /* monthly day picker */
110
+ + '<div class="cron-freq-block cron-freq-monthly' + (f.mode === 'monthly' ? '' : ' cron-freq-hidden') + '">'
111
+ + ' <label class="form-label">每月几号</label>'
112
+ + ' <select class="cron-fld-dom md-select" style="max-width:100%;">'
113
+ + [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28].map(function(v) { return '<option value="' + v + '"' + (parseInt(f.dayOfMonth || '1') === v ? ' selected' : '') + '>' + v + ' 日</option>'; }).join('')
114
+ + ' </select>'
115
+ + '</div>'
116
+ + '<div><label class="form-label">Agent</label><input class="cron-fld-agent" type="text" value="' + escAttr(j.agent_id || 'default') + '" placeholder="default"/></div>'
117
+ + '<div><label class="form-label">项目 ID(可选)</label><input class="cron-fld-project" type="text" value="' + escAttr(j.project_id || '') + '" placeholder="留空则无项目归属"/></div>'
118
+ + '<div><label class="form-label">Session ID(可选)</label><input class="cron-fld-session" type="text" value="' + escAttr(j.session_id || '') + '" placeholder="自动生成"/></div>'
119
+ + '<div class="cron-form-full"><label class="form-label">Prompt(发送给 Agent 的消息)</label>'
120
+ + '<textarea class="cron-fld-prompt" placeholder="【定时任务】输入 prompt…">' + escAttr(j.prompt || '') + '</textarea></div>'
121
+ + '<div><label class="form-label">时区(可选,默认 UTC)</label><input class="cron-fld-tz" type="text" value="' + escAttr(j.timezone || '') + '" placeholder="Asia/Shanghai"/></div>'
122
+ + '<div><label class="form-label">最大工具调用轮数</label><input class="cron-fld-rounds" type="number" min="1" max="32" value="' + (j.max_tool_rounds || 12) + '"/></div>'
123
+ + '<div><label class="form-label">最大续接段数</label><input class="cron-fld-continuations" type="number" min="0" max="20" value="' + (j.max_continuations || 0) + '"/><div class="cron-form-desc">轮数用完自动续接,0=不续接</div></div>'
124
+ + '<div class="cron-form-full row-actions" style="margin-top:0;">'
125
+ + ' <button type="button" class="btn btn--primary btn--sm cron-save-btn">保存</button>'
126
+ + ' <button type="button" class="btn btn--subtle btn--sm cron-cancel-btn">取消</button>'
127
+ + '</div>'
128
+ + '</div>';
129
+ }
130
+
131
+ /** Read frequency settings from an edit form into a descriptor */
132
+ function readFreqFromForm(el) {
133
+ var mode = el.querySelector('.cron-fld-mode').value;
@@ -0,0 +1,105 @@
1
+ var interval = parseInt(el.querySelector('.cron-fld-interval').value) || 30;
2
+ var hour = el.querySelector('.cron-fld-hour') ? el.querySelector('.cron-fld-hour').value : '*';
3
+ var minute = el.querySelector('.cron-fld-minute') ? el.querySelector('.cron-fld-minute').value : '0';
4
+ var dow = el.querySelector('.cron-fld-dow') ? el.querySelector('.cron-fld-dow').value : '*';
5
+ var dom = el.querySelector('.cron-fld-dom') ? el.querySelector('.cron-fld-dom').value : '*';
6
+ return { mode: mode, interval: interval, hour: hour, minute: minute, dayOfWeek: dow, dayOfMonth: dom };
7
+ }
8
+
9
+ /** Wire up mode-switching and day-picker interactions */
10
+ function wireCronFormEvents(el) {
11
+ var modeSelect = el.querySelector('.cron-fld-mode');
12
+ if (!modeSelect) return;
13
+ modeSelect.addEventListener('change', function() {
14
+ var mode = this.value;
15
+ el.querySelectorAll('.cron-freq-block').forEach(function(b) { b.classList.add('cron-freq-hidden'); });
16
+ el.querySelectorAll('.cron-freq-' + mode).forEach(function(b) { b.classList.remove('cron-freq-hidden'); });
17
+ });
18
+ el.querySelectorAll('.cron-day-pill').forEach(function(pill) {
19
+ pill.addEventListener('click', function() {
20
+ el.querySelectorAll('.cron-day-pill').forEach(function(p) { p.classList.remove('cron-day-pill--on'); });
21
+ this.classList.add('cron-day-pill--on');
22
+ var dowInput = el.querySelector('.cron-fld-dow');
23
+ if (dowInput) dowInput.value = this.getAttribute('data-day') || '1';
24
+ });
25
+ });
26
+ }
27
+
28
+ function hoursOptions(current) {
29
+ var opts = '';
30
+ for (var i = 0; i < 24; i++) {
31
+ var sel = (parseInt(current) === i || (current === '*' && i === 8)) ? ' selected' : '';
32
+ opts += '<option value="' + i + '"' + sel + '>' + (i < 10 ? '0' : '') + i + ':00</option>';
33
+ }
34
+ return opts;
35
+ }
36
+
37
+ function minutesOptions(current) {
38
+ var opts = '';
39
+ var vals = [0,5,10,15,20,25,30,35,40,45,50,55];
40
+ for (var i = 0; i < vals.length; i++) {
41
+ var v = vals[i];
42
+ var sel = (parseInt(current) === v || (current === '*' && v === 0)) ? ' selected' : '';
43
+ opts += '<option value="' + v + '"' + sel + '>' + (v < 10 ? '0' : '') + v + '</option>';
44
+ }
45
+ return opts;
46
+ }
47
+
48
+ // ---- Show new cron job form ----
49
+ function showNewCronJobForm() {
50
+ var list = document.getElementById('cronJobList');
51
+ if (!list) return;
52
+ var existing = list.querySelector('.cron-card-wrap.is-new');
53
+ if (existing) { existing.remove(); }
54
+ var wrap = document.createElement('div'); wrap.className = 'cron-card-wrap is-new';
55
+ var editWrap = document.createElement('div'); editWrap.className = 'cron-edit-wrap'; editWrap.style.display = 'block';
56
+ var editStatus = document.createElement('div'); editStatus.className = 'status-line';
57
+ var emptyJob = { id: '', enabled: true, cron: '0 8 * * *', agent_id: 'default', session_id: '', prompt: '', timezone: '', max_tool_rounds: 12, max_continuations: 0 };
58
+ editWrap.innerHTML = buildCronEditFormHTML(emptyJob);
59
+ editWrap.appendChild(editStatus);
60
+ editWrap.querySelector('.cron-cancel-btn').addEventListener('click', function() { wrap.remove(); });
61
+ wireCronFormEvents(editWrap);
62
+ editWrap.querySelector('.cron-save-btn').addEventListener('click', async function() {
63
+ var freq = readFreqFromForm(editWrap);
64
+ var name = editWrap.querySelector('.cron-fld-name').value.trim();
65
+ var agent = editWrap.querySelector('.cron-fld-agent').value.trim();
66
+ var session = editWrap.querySelector('.cron-fld-session').value.trim();
67
+ var prompt = editWrap.querySelector('.cron-fld-prompt').value.trim();
68
+ var tz = editWrap.querySelector('.cron-fld-tz').value.trim();
69
+ var maxRounds = parseInt(editWrap.querySelector('.cron-fld-rounds').value) || 12;
70
+ var maxContinuations = parseInt(editWrap.querySelector('.cron-fld-continuations').value) || 0;
71
+ var enabled = editWrap.querySelector('.cron-fld-enabled').checked;
72
+ var projectId = editWrap.querySelector('.cron-fld-project').value.trim();
73
+ if (!name) { editStatus.textContent = '任务名称不能为空'; editStatus.classList.add('is-err'); return; }
74
+ if (!prompt) { editStatus.textContent = 'Prompt 不能为空'; editStatus.classList.add('is-err'); return; }
75
+ editStatus.textContent = '保存中…'; editStatus.classList.remove('is-err');
76
+ try {
77
+ var title = name.trim();
78
+ var jobId = cronSafeJobId(title);
79
+ var kv = { id: jobId, title: title, enabled: enabled, cron: freqToCron(freq), agent_id: agent || 'default', session_id: session || ('cron-' + jobId), prompt: prompt, max_tool_rounds: maxRounds, max_continuations: maxContinuations };
80
+ if (tz) kv.timezone = tz;
81
+ if (projectId) kv.project_id = projectId;
82
+ var r = await fetch('/api/ui/cron/job', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(kv) });
83
+ var jr = await r.json().catch(function() { return {}; });
84
+ if (!r.ok) throw new Error(jr.detail || jr.statusText);
85
+ wrap.remove();
86
+ await loadCronPanel();
87
+ } catch (e) { editStatus.classList.add('is-err'); editStatus.textContent = String(e); }
88
+ });
89
+ wrap.appendChild(editWrap);
90
+ if (list.firstChild) { list.insertBefore(wrap, list.firstChild); } else { list.appendChild(wrap); }
91
+ }
92
+
93
+ function showCronError(msg) {
94
+ const st = document.getElementById('cronUiStatus');
95
+ if (st) { st.classList.add('is-err'); st.textContent = msg; }
96
+ }
97
+
98
+ // ---- Event bindings ----
99
+ document.getElementById('btnCronRefresh') && document.getElementById('btnCronRefresh').addEventListener('click', function() { loadCronPanel(); });
100
+ document.getElementById('btnCronJobAdd') && document.getElementById('btnCronJobAdd').addEventListener('click', showNewCronJobForm);
101
+
102
+ // ---------------- Chat session env config (structured UI) ----------------
103
+
104
+ /** Cache of LLM presets for the summarizer select dropdown */
105
+ let _summarizerPresetsCache = null;
@@ -0,0 +1,168 @@
1
+ let _summarizerDefaultId = '';
2
+
3
+ /** Read env value: prefer CODEAGENT_* (product), fall back to SEED_* if present. */
4
+ function _envVal(j, codeagentKey, seedKey, fallback) {
5
+ if (j[codeagentKey] !== undefined && j[codeagentKey] !== null && String(j[codeagentKey]) !== '') return j[codeagentKey];
6
+ if (seedKey && j[seedKey] !== undefined && j[seedKey] !== null && String(j[seedKey]) !== '') return j[seedKey];
7
+ return fallback;
8
+ }
9
+
10
+ /** Populate the summarizer model <select> with LLM presets */
11
+ async function populateSummarizerSelect() {
12
+ const sel = document.getElementById('selCompactSummarizer');
13
+ if (!sel) return;
14
+ try {
15
+ const r = await fetch('/api/ui/llm/presets');
16
+ if (!r.ok) return;
17
+ const j = await r.json();
18
+ _summarizerPresetsCache = j.presets || [];
19
+ _summarizerDefaultId = j.default_id || '';
20
+ sel.innerHTML = '';
21
+ (_summarizerPresetsCache).forEach(function(p) {
22
+ const opt = document.createElement('option');
23
+ opt.value = p.id;
24
+ opt.textContent = p.model || p.id;
25
+ sel.appendChild(opt);
26
+ });
27
+ } catch (_) {}
28
+ }
29
+
30
+ /** Try to match a preset by base_url+model; return its id or empty string */
31
+ function _matchSummarizerPreset(baseUrl, model) {
32
+ if (!baseUrl || !model) return '';
33
+ const b = baseUrl.trim().replace(/\/+$/, '');
34
+ const m = model.trim();
35
+ if (!b || !m) return '';
36
+ for (var i = 0; i < (_summarizerPresetsCache || []).length; i++) {
37
+ var p = _summarizerPresetsCache[i];
38
+ if ((p.base_url || '').replace(/\/+$/, '') === b && (p.model || '').trim() === m) {
39
+ return p.id;
40
+ }
41
+ }
42
+ return '';
43
+ }
44
+
45
+ async function loadChatEnvConfig() {
46
+ const status = document.getElementById('chatEnvStatus');
47
+ if (!status) return;
48
+ status.textContent = '加载中…';
49
+ try {
50
+ await populateSummarizerSelect();
51
+ const r = await fetch('/api/ui/env/chat');
52
+ if (!r.ok) throw new Error(r.statusText);
53
+ const j = await r.json();
54
+ const chkAuto = document.getElementById('chkAutoContinue');
55
+ if (chkAuto) chkAuto.checked = _envVal(j, 'CODEAGENT_CHAT_AUTO_CONTINUE_ON_LIMIT', 'SEED_CHAT_AUTO_CONTINUE_ON_LIMIT', '0') === '1';
56
+ const inpSegments = document.getElementById('inpAutoContinueMax');
57
+ if (inpSegments) {
58
+ inpSegments.value = _envVal(j, 'CODEAGENT_CHAT_AUTO_CONTINUE_MAX_SEGMENTS', 'SEED_CHAT_AUTO_CONTINUE_MAX_SEGMENTS', '4');
59
+ rowAutoContinueToggle();
60
+ }
61
+ const inpRounds = document.getElementById('inpMaxToolRounds');
62
+ if (inpRounds) inpRounds.value = _envVal(j, 'CODEAGENT_CHAT_MAX_TOOL_ROUNDS_DEFAULT', 'SEED_CHAT_MAX_TOOL_ROUNDS_DEFAULT', '16');
63
+ const inpMaxOut = document.getElementById('inpMaxOutputTokens');
64
+ if (inpMaxOut) inpMaxOut.value = _envVal(j, 'CODEAGENT_CHAT_MAX_TOKENS', 'SEED_LLM_MAX_TOKENS', '8192');
65
+ const chkCompact = document.getElementById('chkContextCompact');
66
+ if (chkCompact) chkCompact.checked = _envVal(j, 'CODEAGENT_CONTEXT_COMPACT', 'SEED_CONTEXT_COMPACT', '') === '1';
67
+ const inpMinBytes = document.getElementById('inpCompactMinBytes');
68
+ if (inpMinBytes) inpMinBytes.value = _envVal(j, 'CODEAGENT_CONTEXT_COMPACT_MIN_TOKENS', 'SEED_CONTEXT_COMPACT_MIN_TOKENS', '30000');
69
+ const sel = document.getElementById('selCompactSummarizer');
70
+ if (sel) {
71
+ const matched = _matchSummarizerPreset(
72
+ _envVal(j, 'CODEAGENT_CONTEXT_COMPACT_SUMMARIZER_BASEURL', 'SEED_CONTEXT_COMPACT_SUMMARIZER_BASEURL', ''),
73
+ _envVal(j, 'CODEAGENT_CONTEXT_COMPACT_SUMMARIZER_MODEL', 'SEED_CONTEXT_COMPACT_SUMMARIZER_MODEL', '')
74
+ );
75
+ sel.value = matched || _summarizerDefaultId || '';
76
+ }
77
+ const inpSumMax = document.getElementById('inpCompactSummarizerMaxTokens');
78
+ if (inpSumMax) inpSumMax.value = _envVal(j, 'CODEAGENT_CONTEXT_COMPACT_SUMMARIZER_MAX_TOKENS', 'SEED_CONTEXT_COMPACT_SUMMARIZER_MAX_TOKENS', '4096');
79
+ toggleCompactSubRows();
80
+ if (inpMinBytes && typeof setTokenContextMax === 'function') {
81
+ var loadedMin = parseInt(inpMinBytes.value, 10) || 0;
82
+ if (loadedMin > 0) setTokenContextMax(loadedMin);
83
+ }
84
+ status.textContent = '';
85
+ status.classList.remove('is-err');
86
+ } catch (e) { status.classList.add('is-err'); status.textContent = '加载失败:' + String(e); }
87
+ }
88
+
89
+ function rowAutoContinueToggle() {
90
+ const chk = document.getElementById('chkAutoContinue');
91
+ const row = document.getElementById('rowToolRounds');
92
+ if (row) { row.style.display = (chk && chk.checked) ? '' : 'none'; }
93
+ }
94
+
95
+ function toggleCompactSubRows() {
96
+ const chk = document.getElementById('chkContextCompact');
97
+ const rows = document.getElementById('compactSubRows');
98
+ if (rows) { rows.style.display = (chk && chk.checked) ? '' : 'none'; }
99
+ }
100
+
101
+ document.addEventListener('change', function(ev) {
102
+ if (ev.target.id === 'chkAutoContinue') rowAutoContinueToggle();
103
+ if (ev.target.id === 'chkContextCompact') toggleCompactSubRows();
104
+ });
105
+
106
+ (function () {
107
+ var btn = document.getElementById('btnChatEnvSave');
108
+ if (!btn) return;
109
+ btn.addEventListener('click', async function () {
110
+ var status = document.getElementById('chatEnvStatus');
111
+ if (!status) return;
112
+ status.textContent = '保存中…';
113
+ status.classList.remove('is-err');
114
+ try {
115
+ var sumBaseUrl = '';
116
+ var sumModel = '';
117
+ var sel = document.getElementById('selCompactSummarizer');
118
+ if (sel && sel.value) {
119
+ for (var i = 0; i < (_summarizerPresetsCache || []).length; i++) {
120
+ if (_summarizerPresetsCache[i].id === sel.value) {
121
+ sumBaseUrl = _summarizerPresetsCache[i].base_url || '';
122
+ sumModel = _summarizerPresetsCache[i].model || '';
123
+ break;
124
+ }
125
+ }
126
+ }
127
+ var chkAuto = document.getElementById('chkAutoContinue');
128
+ var inpSeg = document.getElementById('inpAutoContinueMax');
129
+ var inpRounds = document.getElementById('inpMaxToolRounds');
130
+ var inpMaxOut = document.getElementById('inpMaxOutputTokens');
131
+ var chkCompact = document.getElementById('chkContextCompact');
132
+ var inpMinB = document.getElementById('inpCompactMinBytes');
133
+ var inpSumMax = document.getElementById('inpCompactSummarizerMaxTokens');
134
+ var body = {
135
+ CODEAGENT_CHAT_AUTO_CONTINUE_ON_LIMIT: chkAuto && chkAuto.checked ? '1' : '0',
136
+ CODEAGENT_CHAT_AUTO_CONTINUE_MAX_SEGMENTS: String(parseInt(inpSeg && inpSeg.value, 10) || 0),
137
+ CODEAGENT_CHAT_MAX_TOOL_ROUNDS_DEFAULT: String(parseInt(inpRounds && inpRounds.value, 10) || 16),
138
+ CODEAGENT_CHAT_MAX_TOKENS: String(parseInt(inpMaxOut && inpMaxOut.value, 10) || 8192),
139
+ CODEAGENT_CONTEXT_COMPACT: chkCompact && chkCompact.checked ? '1' : '',
140
+ CODEAGENT_CONTEXT_COMPACT_MIN_TOKENS: String(parseInt(inpMinB && inpMinB.value, 10) || 30000),
141
+ CODEAGENT_CONTEXT_COMPACT_SUMMARIZER_BASEURL: sumBaseUrl,
142
+ CODEAGENT_CONTEXT_COMPACT_SUMMARIZER_MODEL: sumModel,
143
+ CODEAGENT_CONTEXT_COMPACT_SUMMARIZER_MAX_TOKENS: String(parseInt(inpSumMax && inpSumMax.value, 10) || 4096),
144
+ };
145
+ var r = await fetch('/api/ui/env/chat', {
146
+ method: 'POST',
147
+ headers: { 'Content-Type': 'application/json' },
148
+ body: JSON.stringify(body),
149
+ });
150
+ var j = await r.json().catch(function () { return {}; });
151
+ if (!r.ok) throw new Error(j.detail || r.statusText);
152
+ status.textContent = j.hint || '已保存';
153
+ status.classList.remove('is-err');
154
+ // 同步指示器分母为 compact 触发阈值
155
+ var minTok = parseInt(inpMinB && inpMinB.value, 10) || 0;
156
+ if (minTok > 0 && typeof setTokenContextMax === 'function') setTokenContextMax(minTok);
157
+ fetch('/api/ui/compact-config', {
158
+ method: 'POST',
159
+ headers: { 'Content-Type': 'application/json' },
160
+ body: JSON.stringify({ compact_min_tokens: minTok }),
161
+ credentials: 'same-origin',
162
+ }).catch(function () {});
163
+ } catch (e) {
164
+ status.classList.add('is-err');
165
+ status.textContent = '保存失败:' + String(e);
166
+ }
167
+ });
168
+ })();
@@ -0,0 +1,136 @@
1
+ /* ================================================================
2
+ * 11f-env-llm-a-state.js
3
+ * LLM Presets 状态层:
4
+ * - 常量(use_type 标签、self-hosted provider、board 空态提示)
5
+ * - 模块级状态(llmDefaultId / llmProviderCatalog / window._llmPresetsCache)
6
+ * - provider 目录加载(ensureProviderCatalog / getProviderEntry / listProviderModelsGrouped)
7
+ * - use_type 推断与 capability 提示
8
+ *
9
+ * 不依赖 11f-env-llm-b/c 任何文件;下游 form/board 依赖本文件的全部
10
+ * exports。所有顶层声明按 var 域或 window.*,靠文件名字母序加载。
11
+ * ================================================================ */
12
+
13
+ let llmDefaultId = '';
14
+ let llmProviderCatalog = [];
15
+ window._llmPresetsCache = [];
16
+
17
+ var PRESET_USE_TYPE_LABELS = { chat: '对话', image: '生图', vision: '识图', audio: '音频', speech: '朗读', music: '音乐', video_gen: '视频生成' };
18
+ var PRESET_USE_TYPE_ORDER = ['chat', 'vision', 'image', 'audio', 'music', 'video_gen', 'speech'];
19
+ var SELF_HOSTED_PROVIDERS = ['ollama', 'openai_compatible', 'sglang', 'custom'];
20
+
21
+ var PRESET_BOARD_EMPTY_HINTS = {
22
+ chat: '添加主对话模型(如 DeepSeek、本地 Qwen)',
23
+ vision: '添加识图模型(如 GPT-4o、本地 LLaVA)用于图片附件',
24
+ image: '添加生图模型,供 image_generate 工具调用',
25
+ audio: '添加音频转写模型(如 Whisper)',
26
+ music: '添加 MiniMax music-2.6 等音乐生成模型',
27
+ video_gen: '添加 Agnes agnes-video-v2.0 等视频生成模型',
28
+ speech: '朗读 preset 供参考;气泡 TTS 优先用 MCP 按量 Key',
29
+ };
30
+
31
+ function providerLabel(presetOrId) {
32
+ if (presetOrId && typeof presetOrId === 'object') {
33
+ if (presetOrId.provider_label) return presetOrId.provider_label;
34
+ return providerLabel(presetOrId.provider_stored || presetOrId.provider || '');
35
+ }
36
+ const pid = (presetOrId || '').trim();
37
+ if (!pid) return '自动推断';
38
+ const p = llmProviderCatalog.find(function(x) { return x.id === pid; });
39
+ return p ? (p.label || p.id) : pid;
40
+ }
41
+
42
+ async function ensureProviderCatalog() {
43
+ if (llmProviderCatalog.length) return;
44
+ try {
45
+ const r = await fetch('/api/ui/llm/providers');
46
+ if (!r.ok) return;
47
+ const j = await r.json();
48
+ llmProviderCatalog = j.providers || [];
49
+ } catch (_) {}
50
+ }
51
+
52
+ function inferPresetUseType(p) {
53
+ if (!p) return 'chat';
54
+ if (p.use_type) return p.use_type;
55
+ if (p.supports_image_gen) return 'image';
56
+ if (p.supports_audio) return 'audio';
57
+ if (p.supports_speech) return 'speech';
58
+ if (p.supports_music) return 'music';
59
+ if (p.supports_video_gen) return 'video_gen';
60
+ if (p.supports_vision) return 'vision';
61
+ return 'chat';
62
+ }
63
+
64
+ function getProviderEntry(providerId) {
65
+ return llmProviderCatalog.find(function(x) { return x.id === providerId; }) || null;
66
+ }
67
+
68
+ function providerNeedsManualConnection(providerId) {
69
+ return SELF_HOSTED_PROVIDERS.indexOf(providerId) >= 0;
70
+ }
71
+
72
+ function listProviderModelsGrouped(providerId, useType) {
73
+ const entry = getProviderEntry(providerId);
74
+ if (!entry || !entry.models || typeof entry.models !== 'object') return [];
75
+ const ut = (useType || 'chat').trim();
76
+ const groups = entry.models[ut] || entry.models.chat || entry.models.default || [];
77
+ if (!Array.isArray(groups)) return [];
78
+ const rows = [];
79
+ groups.forEach(function(m) {
80
+ if (typeof m === 'string') {
81
+ rows.push({ id: m, label: m, useType: ut });
82
+ } else if (m && m.id) {
83
+ rows.push({ id: String(m.id), label: m.label || m.id, useType: m.use_type || ut });
84
+ }
85
+ });
86
+ return rows;
87
+ }
88
+
89
+ function inferUseTypeForProviderModel(providerId, modelId) {
90
+ const groups = ['chat', 'vision', 'image', 'audio', 'music', 'video_gen', 'speech'];
91
+ for (let i = 0; i < groups.length; i++) {
92
+ const rows = listProviderModelsGrouped(providerId, groups[i]);
93
+ if (rows.some(function(r) { return r.id === modelId; })) return groups[i];
94
+ }
95
+ return 'chat';
96
+ }
97
+
98
+ function presetUseTypeLabel(p) {
99
+ if (!p) return '对话';
100
+ const ut = p.use_type_label || PRESET_USE_TYPE_LABELS[inferPresetUseType(p)] || inferPresetUseType(p);
101
+ return ut || '对话';
102
+ }
103
+
104
+ function presetCapabilityHint(providerId, useType) {
105
+ const pid = (providerId || '').trim();
106
+ const ut = (useType || 'chat').trim();
107
+ if (pid === 'minimax') {
108
+ if (ut === 'music') return '将用于 music_generate 工具;与聊天/生图/朗读共用 MiniMax API Key。';
109
+ if (ut === 'speech') return '朗读模型列表供 TTS 参考;气泡朗读优先使用 MCP 或按量 Key。';
110
+ if (ut === 'image') return '将用于 image_generate 工具;与聊天/音乐/朗读共用 MiniMax API Key。';
111
+ if (ut === 'chat') return '主对话模型;同一 Key 也可用于 TTS(若无单独 MCP Key)。';
112
+ }
113
+ if (ut === 'image') return '将用于 image_generate 工具。';
114
+ if (ut === 'vision') return '将用于 vision_analyze / 发送图片附件。';
115
+ if (ut === 'audio') return '将用于 audio_transcribe / 发送音频附件。';
116
+ if (ut === 'music') return '将用于 music_generate 工具(当前仅 MiniMax)。';
117
+ if (ut === 'video_gen') return '将用于 video_generate 工具(Agnes agnes-video-v2.0)。';
118
+ if (ut === 'speech') return '供朗读模型参考;气泡 TTS 使用 MCP / 按量 Key。';
119
+ return '主 Agent 对话与工具路由的默认 LLM。';
120
+ }
121
+
122
+ function resolvePresetProviderId(p) {
123
+ if (!p) return 'deepseek';
124
+ const stored = (p.provider_stored || p.provider || '').trim();
125
+ if (stored && stored !== 'custom') return stored;
126
+ const effective = (p.provider_effective || '').trim();
127
+ if (effective && effective !== 'openai_compatible') return effective;
128
+ const url = (p.base_url || '').toLowerCase();
129
+ if (url.indexOf('api.deepseek.com') >= 0) return 'deepseek';
130
+ if (url.indexOf('minimaxi.com') >= 0 || url.indexOf('minimax.io') >= 0) return 'minimax';
131
+ if (url.indexOf('agnes-ai.com') >= 0) return 'agnes';
132
+ if (url.indexOf('volces.com') >= 0 || url.indexOf('volcengine') >= 0) return 'volcengine';
133
+ if (url.indexOf('api.openai.com') >= 0) return 'openai';
134
+ if (url.indexOf('127.0.0.1') >= 0 || url.indexOf('localhost') >= 0) return 'ollama';
135
+ return stored || effective || 'deepseek';
136
+ }