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,973 @@
1
+ // ---------------- Config paths & allowlist ----------------
2
+ // escapeHtml/escAttr 已统一在 00-utils.js(顶层声明)。
3
+
4
+ var GIT_PROVIDER_HTTPS_HOST = {
5
+ github: 'https://github.com',
6
+ gitlab: 'https://gitlab.com',
7
+ gitee: 'https://gitee.com',
8
+ bitbucket: 'https://bitbucket.org',
9
+ };
10
+
11
+ var GIT_PROVIDER_SSH_HOST = {
12
+ github: 'git@github.com',
13
+ gitlab: 'git@gitlab.com',
14
+ gitee: 'git@gitee.com',
15
+ bitbucket: 'git@bitbucket.org',
16
+ };
17
+
18
+ function _gitProviderFromSelect(id) {
19
+ var el = document.getElementById(id);
20
+ return el ? String(el.value || 'github').trim() : 'github';
21
+ }
22
+
23
+ function _syncCredUrlFromProvider() {
24
+ var prov = _gitProviderFromSelect('selConfigCredProvider');
25
+ var inp = document.getElementById('inpConfigCredUrl');
26
+ if (!inp) return;
27
+ if (prov === 'custom') {
28
+ inp.readOnly = false;
29
+ if (!inp.value) inp.value = 'https://';
30
+ } else {
31
+ inp.readOnly = true;
32
+ inp.value = GIT_PROVIDER_HTTPS_HOST[prov] || GIT_PROVIDER_HTTPS_HOST.github;
33
+ }
34
+ }
35
+
36
+ async function loadConfigPaths() {
37
+ const display = document.getElementById('configPathsDisplay');
38
+ if (!display) return;
39
+ try {
40
+ const r = await fetch('/api/ui/config/paths');
41
+ if (!r.ok) return;
42
+ const j = await r.json();
43
+ display.innerHTML =
44
+ '<div class="form-row" style="margin-bottom:var(--sp-1);max-width:100%;">'
45
+ + '<label class="form-label" style="width:auto;min-width:100px;">根目录</label>'
46
+ + '<code style="font-size:12px;word-break:break-all;line-height:28px;">' + escHtml(j.codeagent_root_path || '') + '</code>'
47
+ + '</div>'
48
+ + '<div class="form-row" style="margin-bottom:var(--sp-1);max-width:100%;">'
49
+ + '<label class="form-label" style="width:auto;min-width:100px;">技能目录</label>'
50
+ + '<code style="font-size:12px;word-break:break-all;line-height:28px;">' + escHtml(j.skills_path || '') + '</code>'
51
+ + '</div>'
52
+ + '<div class="form-row" style="max-width:100%;">'
53
+ + '<label class="form-label" style="width:auto;min-width:100px;">人设目录</label>'
54
+ + '<code style="font-size:12px;word-break:break-all;line-height:28px;">' + escHtml(j.persona_path || '') + '</code>'
55
+ + '</div>';
56
+ } catch (_) {}
57
+ await loadAllowlistConfig();
58
+ }
59
+
60
+ async function loadAllowlistConfig() {
61
+ const el = document.getElementById('allowlistConfig');
62
+ if (!el) return;
63
+ try {
64
+ const r = await fetch('/api/ui/allowlist');
65
+ if (!r.ok) return;
66
+ const j = await r.json();
67
+ const mode = j.mode || 'all';
68
+ const paths = j.paths || [];
69
+ el.innerHTML =
70
+ '<div class="form-row"><label class="form-label" style="width:auto;min-width:100px;">权限模式</label>'
71
+ + '<select id="selAllowMode" class="md-select" style="max-width:200px;">'
72
+ + '<option value="all"' + (mode === 'all' ? ' selected' : '') + '>允许所有目录</option>'
73
+ + '<option value="whitelist"' + (mode === 'whitelist' ? ' selected' : '') + '>仅允许白名单</option>'
74
+ + '</select>'
75
+ + '<span class="form-hint">限制 Agent 文件读写操作的范围</span>'
76
+ + '</div>'
77
+ + '<div id="allowPathsEditor"' + (mode === 'whitelist' ? '' : ' style="display:none;"') + '>'
78
+ + ' <label class="form-label">白名单路径(每行一个)</label>'
79
+ + ' <textarea id="allowPathsText" class="env-edit" style="height:100px;" spellcheck="false">' + escHtml(paths.join('\n')) + '</textarea>'
80
+ + '</div>'
81
+ + '<div class="row-actions" style="margin-top:var(--sp-2);">'
82
+ + '<button type="button" class="btn btn--primary btn--sm" id="btnAllowSave">保存</button>'
83
+ + '</div>'
84
+ + '<div id="allowStatus" class="status-line"></div>';
85
+
86
+ document.getElementById('selAllowMode').addEventListener('change', function() {
87
+ const editor = document.getElementById('allowPathsEditor');
88
+ if (editor) editor.style.display = this.value === 'whitelist' ? '' : 'none';
89
+ });
90
+ document.getElementById('btnAllowSave').addEventListener('click', async function() {
91
+ const s = document.getElementById('allowStatus');
92
+ if (!s) return;
93
+ s.textContent = '保存中…';
94
+ s.classList.remove('is-err');
95
+ try {
96
+ const newMode = document.getElementById('selAllowMode').value;
97
+ const newPaths = document.getElementById('allowPathsText') ? document.getElementById('allowPathsText').value.split('\n').map(function(x) { return x.trim(); }).filter(Boolean) : [];
98
+ const dr = await fetch('/api/ui/allowlist', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mode: newMode, paths: newPaths }) });
99
+ if (!dr.ok) throw new Error((await dr.json()).detail || dr.statusText);
100
+ s.textContent = '已保存。';
101
+ } catch (e) { s.classList.add('is-err'); s.textContent = String(e); }
102
+ });
103
+ } catch (_) {}
104
+ }
105
+
106
+
107
+ async function loadGitRemoteConfig() {
108
+ const section = document.getElementById('fieldsetGitRemote');
109
+ if (!section) return;
110
+
111
+ // ---- 默认远程配置 ----
112
+ await _loadGitDefaults();
113
+
114
+ // ---- SSH ----
115
+ _bindGitSshEvents();
116
+
117
+ // ---- HTTPS 凭据 ----
118
+ _bindGitCredEvents();
119
+
120
+ // ---- Git 代理 ----
121
+ _loadGitProxy();
122
+ _bindGitProxyEvents();
123
+ }
124
+
125
+ // =============================================
126
+ // Git 代理(带启用开关)
127
+ // =============================================
128
+ async function _loadGitProxy() {
129
+ var chk = document.getElementById('chkGitProxyEnabled');
130
+ var httpInp = document.getElementById('inpGitHttpProxy');
131
+ var httpsInp = document.getElementById('inpGitHttpsProxy');
132
+ if (!chk || !httpInp || !httpsInp) return;
133
+ try {
134
+ var r = await fetch('/api/ui/git/proxy-config', {
135
+ method: 'POST',
136
+ headers: { 'Content-Type': 'application/json' },
137
+ body: JSON.stringify({ method: 'get' })
138
+ });
139
+ var j = await r.json();
140
+ if (j.ok) {
141
+ chk.checked = !!j.enabled;
142
+ httpInp.value = j.http || '';
143
+ httpsInp.value = j.https || '';
144
+ _toggleGitProxyForm(j.enabled);
145
+ }
146
+ } catch(_) {}
147
+ }
148
+
149
+ function _bindGitProxyEvents() {
150
+ var saveBtn = document.getElementById('btnGitProxySave');
151
+ if (!saveBtn || saveBtn.dataset.bound) return;
152
+ saveBtn.dataset.bound = '1';
153
+ saveBtn.addEventListener('click', _saveGitProxy);
154
+
155
+ var clHttp = document.getElementById('btnGitHttpProxyClear');
156
+ var clHttps = document.getElementById('btnGitHttpsProxyClear');
157
+ if (clHttp) clHttp.addEventListener('click', function() {
158
+ _clearGitProxy('http');
159
+ });
160
+ if (clHttps) clHttps.addEventListener('click', function() {
161
+ _clearGitProxy('https');
162
+ });
163
+
164
+ var chk = document.getElementById('chkGitProxyEnabled');
165
+ if (chk) {
166
+ chk.addEventListener('change', function() {
167
+ _toggleGitProxyForm(this.checked);
168
+ });
169
+ }
170
+ }
171
+
172
+ function _toggleGitProxyForm(show) {
173
+ var form = document.getElementById('gitProxyForm');
174
+ var inputs = form ? form.querySelectorAll('input[type="text"], button.btn--ghost') : [];
175
+ if (form) form.style.opacity = show ? '1' : '0.5';
176
+ inputs.forEach(function(el) {
177
+ el.disabled = !show;
178
+ });
179
+ }
180
+
181
+ async function _saveGitProxy() {
182
+ var chk = document.getElementById('chkGitProxyEnabled');
183
+ var httpInp = document.getElementById('inpGitHttpProxy');
184
+ var httpsInp = document.getElementById('inpGitHttpsProxy');
185
+ var status = document.getElementById('gitProxyStatus');
186
+ if (!chk || !httpInp || !httpsInp || !status) return;
187
+ status.textContent = '保存中...';
188
+
189
+ var enabled = chk.checked;
190
+ var httpVal = httpInp.value.trim();
191
+ var httpsVal = httpsInp.value.trim();
192
+
193
+ try {
194
+ var r = await fetch('/api/ui/git/proxy-config', {
195
+ method: 'POST',
196
+ headers: { 'Content-Type': 'application/json' },
197
+ body: JSON.stringify({
198
+ method: 'set',
199
+ enabled: enabled,
200
+ http: httpVal,
201
+ https: httpsVal
202
+ })
203
+ });
204
+ var j = await r.json();
205
+ if (j.ok) {
206
+ _toggleGitProxyForm(enabled);
207
+ status.textContent = '✅ 代理配置已保存';
208
+ } else {
209
+ status.textContent = '❌ ' + (j.detail || '保存失败');
210
+ }
211
+ setTimeout(function() { status.textContent = ''; }, 3000);
212
+ } catch(e) {
213
+ status.textContent = '❌ ' + String(e);
214
+ }
215
+ }
216
+
217
+ async function _clearGitProxy(scheme) {
218
+ var inp = document.getElementById(scheme === 'http' ? 'inpGitHttpProxy' : 'inpGitHttpsProxy');
219
+ var status = document.getElementById('gitProxyStatus');
220
+ if (inp) inp.value = '';
221
+ try {
222
+ await fetch('/api/ui/git/proxy', {
223
+ method: 'POST',
224
+ headers: { 'Content-Type': 'application/json' },
225
+ body: JSON.stringify({ method: 'unset', scheme: scheme })
226
+ });
227
+ if (status) { status.textContent = '✅ 已清除 git ' + scheme + ' 代理'; setTimeout(function() { status.textContent = ''; }, 2000); }
228
+ } catch(e) {
229
+ if (status) status.textContent = '❌ ' + String(e);
230
+ }
231
+ }
232
+
233
+ /* ---------- 默认远程配置(多方案) ---------- */
234
+ /* 数据格式:
235
+ { enabled: true,
236
+ presets: [{ name, provider, owner, host, protocol, autoPush }],
237
+ defaultPreset: "方案名称" }
238
+ */
239
+
240
+ // 当前内存中的全量数据
241
+ var _gitDefaultsData = { enabled: false, presets: [], defaultPreset: '' };
242
+
243
+ async function _loadGitDefaults() {
244
+ const chk = document.getElementById('chkGitDefaultEnabled');
245
+ if (!chk) return;
246
+
247
+ try {
248
+ const r = await fetch('/api/ui/git/defaults');
249
+ if (!r.ok) return;
250
+ const j = await r.json();
251
+ var def = j.defaults || {};
252
+ // 兼容旧格式:单套方案自动转多方案
253
+ if (!def.presets && def.provider) {
254
+ def.presets = [{
255
+ name: '默认',
256
+ provider: def.provider,
257
+ owner: def.owner || '',
258
+ host: def.host || '',
259
+ protocol: def.protocol || 'ssh',
260
+ autoPush: !!def.autoPush,
261
+ }];
262
+ def.defaultPreset = '默认';
263
+ }
264
+ _gitDefaultsData = def;
265
+ chk.checked = !!def.enabled;
266
+ _toggleGitDefaultsForm(def.enabled);
267
+ // 同步 selGitDefaultProvider(旧占位,供 SSH 读取)
268
+ _syncGitDefaultProvider();
269
+ _renderPresetList();
270
+
271
+ // 同步 HTTTPS 凭据平台与 SSH 状态
272
+ var firstPreset = (def.presets && def.presets.length) ? def.presets[0] : null;
273
+ if (firstPreset) {
274
+ var selProv = document.getElementById('selConfigCredProvider');
275
+ if (selProv) selProv.value = firstPreset.provider;
276
+ _syncCredUrlFromProvider();
277
+ var sshBody = document.getElementById('gitConfigSshBody');
278
+ if (sshBody && sshBody.style.display === 'block') _refreshGitSshStatus();
279
+ }
280
+ } catch (_) {}
281
+
282
+ // 绑定事件(只绑一次)
283
+ if (window._gitDefaultsBound) return;
284
+ window._gitDefaultsBound = true;
285
+
286
+ chk.addEventListener('change', function() {
287
+ _gitDefaultsData.enabled = this.checked;
288
+ _toggleGitDefaultsForm(this.checked);
289
+ if (!this.checked) _commitGitDefaults();
290
+ });
291
+
292
+ document.getElementById('selPresetProvider')?.addEventListener('change', function() {
293
+ _togglePresetHost(this.value);
294
+ });
295
+
296
+ document.getElementById('btnPresetAdd')?.addEventListener('click', function() {
297
+ _openPresetEditor(null);
298
+ });
299
+
300
+ document.getElementById('btnPresetSave')?.addEventListener('click', _savePreset);
301
+ document.getElementById('btnPresetCancel')?.addEventListener('click', _closePresetEditor);
302
+ document.getElementById('btnPatSave')?.addEventListener('click', _savePat);
303
+ document.getElementById('btnPatCancel')?.addEventListener('click', _closePatForm);
304
+ }
305
+
306
+ function _toggleGitDefaultsForm(show) {
307
+ var form = document.getElementById('gitDefaultsForm');
308
+ if (form) form.style.display = show ? 'block' : 'none';
309
+ }
310
+
311
+ function _togglePresetHost(provider) {
312
+ var row = document.getElementById('rowPresetHost');
313
+ if (row) row.style.display = provider === 'custom' ? '' : 'none';
314
+ }
315
+
316
+ // ---- 方案列表渲染 ----
317
+ function _renderPresetList() {
318
+ var container = document.getElementById('gitPresetList');
319
+ if (!container) return;
320
+ var presets = _gitDefaultsData.presets || [];
321
+ var defaultName = _gitDefaultsData.defaultPreset || '';
322
+ if (!presets.length) {
323
+ container.innerHTML = '<div style="font-size:11px;color:var(--text-muted);padding:4px 0;">暂无方案,点击下方「+ 添加方案」</div>';
324
+ return;
325
+ }
326
+ var html = '';
327
+ for (var i = 0; i < presets.length; i++) {
328
+ var p = presets[i];
329
+ var isDefault = (p.name === defaultName);
330
+ var providerLabel = p.provider === 'custom' ? (p.host || '自定义') : p.provider;
331
+ html += '<div style="padding:8px;margin-bottom:6px;'
332
+ + 'border:1px solid ' + (isDefault ? 'var(--accent)' : 'var(--border)') + ';border-radius:6px;'
333
+ + 'background:' + (isDefault ? 'rgba(59,130,246,0.06)' : 'transparent') + ';">'
334
+
335
+ // 第一行:星标 + 名称 + 信息 + 编辑/删除
336
+ + '<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">'
337
+ + '<span style="cursor:pointer;font-size:12px;color:' + (isDefault ? 'var(--accent)' : 'var(--text-muted)') + ';" title="设为默认" onclick="_setDefaultPreset(\'' + _escJs(p.name) + '\')">'
338
+ + (isDefault ? '⭐' : '☆') + '</span>'
339
+ + '<span style="font-size:12px;font-weight:500;">' + _escHtml(p.name) + '</span>'
340
+ + '<span style="font-size:10px;color:var(--text-muted);">' + _escHtml(p.owner || '') + ' @ ' + _escHtml(providerLabel) + '</span>'
341
+ + '<span style="font-size:10px;color:var(--text-muted);">(' + (p.protocol || 'ssh') + ')</span>'
342
+ + '<div style="margin-left:auto;display:flex;gap:2px;">'
343
+ + '<button class="btn btn--ghost btn--xs" onclick="_openPresetEditor(' + i + ')" title="编辑">✎</button>'
344
+ + '<button class="btn btn--ghost btn--xs" style="color:#e74c3c;" onclick="_deletePreset(' + i + ')" title="删除">✕</button>'
345
+ + '</div>'
346
+ + '</div>'
347
+
348
+ // 第二行:SSH + PAT 状态 + 操作按钮
349
+ + '<div style="display:flex;align-items:center;gap:12px;font-size:11px;margin-left:18px;">'
350
+ + '<span>🔑 SSH <span id="presetSshStatus_' + i + '">…</span></span>'
351
+ + '<span>🔐 PAT <span id="presetPatStatus_' + i + '">…</span></span>'
352
+ + '<div style="margin-left:auto;display:flex;gap:4px;">'
353
+ + '<button class="btn btn--ghost btn--xs" id="presetSshBtn_' + i + '" onclick="_testPresetSsh(' + i + ')">测试 SSH</button>'
354
+ + '<button class="btn btn--ghost btn--xs" onclick="_openPatForm(' + i + ')">配置 PAT</button>'
355
+ + '</div>'
356
+ + '</div>'
357
+
358
+ + '</div>';
359
+ }
360
+ container.innerHTML = html;
361
+ _syncGitDefaultProvider();
362
+ // 异步检查每套方案的 SSH 和 PAT 状态
363
+ setTimeout(function() { _refreshPresetStatuses(); }, 100);
364
+ }
365
+
366
+ // ---- 方案编辑 ----
367
+ var _editingPresetIndex = -1; // -1 = 新增
368
+
369
+ function _openPresetEditor(index) {
370
+ _editingPresetIndex = (index === null || index === undefined) ? -1 : index;
371
+ var editor = document.getElementById('gitPresetEditor');
372
+ var title = document.getElementById('gitPresetEditorTitle');
373
+ if (!editor || !title) return;
374
+
375
+ // 隐藏列表中的添加按钮
376
+ document.getElementById('btnPresetAdd').style.display = 'none';
377
+
378
+ if (_editingPresetIndex >= 0) {
379
+ // 编辑已有方案
380
+ title.textContent = '编辑方案';
381
+ var p = (_gitDefaultsData.presets || [])[_editingPresetIndex];
382
+ if (!p) { _closePresetEditor(); return; }
383
+ document.getElementById('inpPresetName').value = p.name || '';
384
+ document.getElementById('selPresetProvider').value = p.provider || 'github';
385
+ document.getElementById('inpPresetHost').value = p.host || '';
386
+ document.getElementById('inpPresetOwner').value = p.owner || '';
387
+ var protoEl = document.querySelector('input[name="presetProtocol"][value="' + (p.protocol || 'ssh') + '"]');
388
+ if (protoEl) protoEl.checked = true;
389
+ document.getElementById('chkPresetAutoPush').checked = !!p.autoPush;
390
+ } else {
391
+ title.textContent = '添加方案';
392
+ document.getElementById('inpPresetName').value = '';
393
+ document.getElementById('selPresetProvider').value = 'github';
394
+ document.getElementById('inpPresetHost').value = '';
395
+ document.getElementById('inpPresetOwner').value = '';
396
+ var sshEl = document.querySelector('input[name="presetProtocol"][value="ssh"]');
397
+ if (sshEl) sshEl.checked = true;
398
+ document.getElementById('chkPresetAutoPush').checked = false;
399
+ }
400
+ _togglePresetHost(document.getElementById('selPresetProvider').value);
401
+ editor.style.display = 'block';
402
+ document.getElementById('inpPresetName').focus();
403
+ }
404
+
405
+ function _closePresetEditor() {
406
+ var editor = document.getElementById('gitPresetEditor');
407
+ if (editor) editor.style.display = 'none';
408
+ _editingPresetIndex = -1;
409
+ document.getElementById('btnPresetAdd').style.display = '';
410
+ document.getElementById('presetFormStatus').textContent = '';
411
+ }
412
+
413
+ function _savePreset() {
414
+ var status = document.getElementById('presetFormStatus');
415
+ if (!status) return;
416
+
417
+ var name = document.getElementById('inpPresetName').value.trim();
418
+ if (!name) { status.textContent = '❌ 方案名称不能为空'; return; }
419
+
420
+ var provider = document.getElementById('selPresetProvider').value;
421
+ var host = document.getElementById('inpPresetHost').value.trim();
422
+ var owner = document.getElementById('inpPresetOwner').value.trim();
423
+
424
+ if (provider === 'custom' && !host) { status.textContent = '❌ 自定义方案需填写主机地址'; return; }
425
+ if (!owner) { status.textContent = '❌ 所有者不能为空'; return; }
426
+
427
+ var preset = {
428
+ name: name,
429
+ provider: provider,
430
+ owner: owner,
431
+ host: host,
432
+ protocol: document.querySelector('input[name="presetProtocol"]:checked')?.value || 'ssh',
433
+ autoPush: document.getElementById('chkPresetAutoPush').checked,
434
+ };
435
+
436
+ var presets = _gitDefaultsData.presets || [];
437
+ // 重名检查(排除自己)
438
+ for (var i = 0; i < presets.length; i++) {
439
+ if (presets[i].name === name && i !== _editingPresetIndex) {
440
+ status.textContent = '❌ 方案名称「' + _escHtml(name) + '」已存在';
441
+ return;
442
+ }
443
+ }
444
+
445
+ status.textContent = '保存中…';
446
+
447
+ if (_editingPresetIndex >= 0) {
448
+ presets[_editingPresetIndex] = preset;
449
+ } else {
450
+ presets.push(preset);
451
+ // 第一个方案自动设为默认
452
+ if (!_gitDefaultsData.defaultPreset) _gitDefaultsData.defaultPreset = name;
453
+ }
454
+ _gitDefaultsData.presets = presets;
455
+
456
+ _closePresetEditor();
457
+ _renderPresetList();
458
+ _commitGitDefaults();
459
+ status.textContent = '';
460
+ }
461
+
462
+ function _deletePreset(index) {
463
+ if (!confirm('确定删除方案「' + _escHtml((_gitDefaultsData.presets || [])[index]?.name || '') + '」吗?')) return;
464
+ var presets = _gitDefaultsData.presets || [];
465
+ var deletedName = presets[index]?.name;
466
+ presets.splice(index, 1);
467
+ _gitDefaultsData.presets = presets;
468
+ // 如果删的是默认方案,选第一个
469
+ if (_gitDefaultsData.defaultPreset === deletedName) {
470
+ _gitDefaultsData.defaultPreset = presets.length ? presets[0].name : '';
471
+ }
472
+ _renderPresetList();
473
+ _commitGitDefaults();
474
+ }
475
+
476
+ function _setDefaultPreset(name) {
477
+ _gitDefaultsData.defaultPreset = name;
478
+ _renderPresetList();
479
+ _syncGitDefaultProvider();
480
+ _commitGitDefaults();
481
+ }
482
+
483
+ // ---- 保存到后端 ----
484
+ function _commitGitDefaults() {
485
+ var status = document.getElementById('gitDefaultsStatus');
486
+ var body = {
487
+ enabled: _gitDefaultsData.enabled,
488
+ presets: _gitDefaultsData.presets || [],
489
+ defaultPreset: _gitDefaultsData.defaultPreset || '',
490
+ };
491
+ fetch('/api/ui/git/defaults', {
492
+ method: 'PUT',
493
+ headers: { 'Content-Type': 'application/json' },
494
+ body: JSON.stringify(body),
495
+ }).then(function(r) {
496
+ if (r.ok && status) {
497
+ status.textContent = '✅ 已保存';
498
+ setTimeout(function() { status.textContent = ''; }, 2000);
499
+ }
500
+ }).catch(function() {});
501
+ }
502
+
503
+ // ---- 同步 selGitDefaultProvider(旧占位,供 SSH 代码读取) ----
504
+ function _syncGitDefaultProvider() {
505
+ var el = document.getElementById('selGitDefaultProvider');
506
+ if (!el) return;
507
+ var defaultName = _gitDefaultsData.defaultPreset || '';
508
+ var presets = _gitDefaultsData.presets || [];
509
+ var p = null;
510
+ for (var i = 0; i < presets.length; i++) {
511
+ if (presets[i].name === defaultName) { p = presets[i]; break; }
512
+ }
513
+ if (!p && presets.length) p = presets[0];
514
+ el.value = p ? p.provider : 'github';
515
+ }
516
+
517
+ // ---- 工具函数 ----
518
+ function _escHtml(s) {
519
+ if (typeof s !== 'string') return String(s || '');
520
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
521
+ }
522
+ function _escJs(s) {
523
+ if (typeof s !== 'string') return String(s || '');
524
+ return s.replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/"/g,'\\"');
525
+ }
526
+
527
+ // ---- 每方案的 SSH/PAT 状态检查与操作 ----
528
+ function _getPresetSshHost(preset) {
529
+ if (preset.provider === 'custom') return preset.host || '';
530
+ return GIT_PROVIDER_SSH_HOST[preset.provider] || 'github.com';
531
+ }
532
+ function _getPresetHttpsUrl(preset) {
533
+ if (preset.provider === 'custom') return 'https://' + (preset.host || '');
534
+ return GIT_PROVIDER_HTTPS_HOST[preset.provider] || 'https://github.com';
535
+ }
536
+
537
+ async function _checkPresetSsh(index) {
538
+ var el = document.getElementById('presetSshStatus_' + index);
539
+ if (!el) return;
540
+ var presets = _gitDefaultsData.presets || [];
541
+ var p = presets[index];
542
+ if (!p) { el.innerHTML = '—'; return; }
543
+ el.innerHTML = '检查…';
544
+ var payload = { command: 'ssh', args: 'status', provider: p.provider };
545
+ if (p.provider === 'custom' && p.host) payload.host = p.host;
546
+ try {
547
+ var r = await fetch('/api/ui/git', {
548
+ method: 'POST',
549
+ headers: { 'Content-Type': 'application/json' },
550
+ body: JSON.stringify(payload),
551
+ });
552
+ var j = await r.json();
553
+ el.innerHTML = (j && j.connected) ? '✅' : '❌';
554
+ } catch (_) { el.innerHTML = '⚠️'; }
555
+ }
556
+
557
+ async function _checkPresetPat(index) {
558
+ var el = document.getElementById('presetPatStatus_' + index);
559
+ if (!el) return;
560
+ var presets = _gitDefaultsData.presets || [];
561
+ var p = presets[index];
562
+ if (!p) { el.innerHTML = '—'; return; }
563
+ var hostUrl = _getPresetHttpsUrl(p);
564
+ el.innerHTML = '检查…';
565
+ try {
566
+ // 用 git credential fill 检查该 host 是否有已存凭据
567
+ var r = await fetch('/api/ui/git', {
568
+ method: 'POST',
569
+ headers: { 'Content-Type': 'application/json' },
570
+ body: JSON.stringify({
571
+ command: 'credential', action: 'show',
572
+ }),
573
+ });
574
+ var j = await r.json();
575
+ var text = (j && j.result) || '';
576
+ // 如果 show 结果包含了这个 host,认作已配置
577
+ var urlPart = hostUrl.replace(/^https?:\/\//, '');
578
+ el.innerHTML = text.indexOf(urlPart) !== -1 ? '✅' : '❌';
579
+ } catch (_) { el.innerHTML = '⚠️'; }
580
+ }
581
+
582
+ async function _testPresetSsh(index) {
583
+ var btn = document.getElementById('presetSshBtn_' + index);
584
+ if (btn) btn.textContent = '测试中…';
585
+ await _checkPresetSsh(index);
586
+ if (btn) btn.textContent = '测试 SSH';
587
+ }
588
+
589
+ var _patEditingIndex = -1;
590
+
591
+ function _openPatForm(index) {
592
+ _patEditingIndex = index;
593
+ var presets = _gitDefaultsData.presets || [];
594
+ var p = presets[index];
595
+ if (!p) return;
596
+
597
+ document.getElementById('patFormHostLabel').textContent = _getPresetHttpsUrl(p);
598
+ document.getElementById('inpPatUser').value = p.owner || '';
599
+ document.getElementById('inpPatToken').value = '';
600
+ document.getElementById('patFormStatus').textContent = '';
601
+ document.getElementById('gitPatForm').style.display = 'block';
602
+ document.getElementById('inpPatToken').focus();
603
+ }
604
+
605
+ function _closePatForm() {
606
+ document.getElementById('gitPatForm').style.display = 'none';
607
+ _patEditingIndex = -1;
608
+ }
609
+
610
+ async function _savePat() {
611
+ var status = document.getElementById('patFormStatus');
612
+ if (!status) return;
613
+ var presets = _gitDefaultsData.presets || [];
614
+ var p = presets[_patEditingIndex];
615
+ if (!p) { status.textContent = '❌ 方案不存在'; return; }
616
+
617
+ var user = document.getElementById('inpPatUser').value.trim();
618
+ var token = document.getElementById('inpPatToken').value.trim();
619
+ if (!token) { status.textContent = '❌ Token 不能为空'; return; }
620
+
621
+ status.textContent = '保存中…';
622
+ try {
623
+ var hostUrl = _getPresetHttpsUrl(p);
624
+ var r = await fetch('/api/ui/git', {
625
+ method: 'POST',
626
+ headers: { 'Content-Type': 'application/json' },
627
+ body: JSON.stringify({
628
+ command: 'credential', action: 'store',
629
+ url: hostUrl,
630
+ username: user,
631
+ token: token,
632
+ }),
633
+ });
634
+ if (!r.ok) {
635
+ var errData = await r.json();
636
+ throw new Error(errData.detail || errData.error || '保存失败');
637
+ }
638
+ status.textContent = '✅ 已保存';
639
+ _checkPresetPat(_patEditingIndex);
640
+ setTimeout(function() { _closePatForm(); }, 1000);
641
+ } catch (e) {
642
+ status.textContent = '❌ ' + String(e);
643
+ }
644
+ }
645
+
646
+ // ---- 更新 preset 卡片里的 SSH/PAT 状态 ----
647
+ function _refreshPresetStatuses() {
648
+ var presets = _gitDefaultsData.presets || [];
649
+ for (var i = 0; i < presets.length; i++) {
650
+ _checkPresetSsh(i);
651
+ _checkPresetPat(i);
652
+ }
653
+ }
654
+
655
+ /* ---------- SSH 密钥管理 ---------- */
656
+ function _bindGitSshEvents() {
657
+ if (window._gitSshBound) return;
658
+ window._gitSshBound = true;
659
+
660
+ var header = document.getElementById('gitConfigSshHeader');
661
+ if (!header) return;
662
+
663
+ header.addEventListener('click', function() {
664
+ var body = document.getElementById('gitConfigSshBody');
665
+ var icon = document.getElementById('gitConfigSshIcon');
666
+ if (!body || !icon) return;
667
+ var opening = body.style.display !== 'block';
668
+ body.style.display = opening ? 'block' : 'none';
669
+ icon.textContent = opening ? '▼' : '▶';
670
+ if (opening) _refreshGitSshStatus();
671
+ });
672
+
673
+ var g = document.getElementById('btnConfigSshGen');
674
+ var c = document.getElementById('btnConfigSshCat');
675
+ var t = document.getElementById('btnConfigSshTest');
676
+ if (g) g.addEventListener('click', function() { _gitSshAction('generate'); });
677
+ if (c) c.addEventListener('click', function() { _gitSshAction('cat'); });
678
+ if (t) t.addEventListener('click', function() { _gitSshAction('test'); });
679
+ }
680
+
681
+ async function _refreshGitSshStatus() {
682
+ var el = document.getElementById('gitConfigSshStatus');
683
+ if (!el) return;
684
+ el.textContent = '检查中...';
685
+ try {
686
+ var r = await fetch('/api/ui/git', {
687
+ method: 'POST',
688
+ headers: { 'Content-Type': 'application/json' },
689
+ body: JSON.stringify({
690
+ command: 'ssh',
691
+ args: 'status',
692
+ provider: _gitProviderFromSelect('selGitDefaultProvider'),
693
+ }),
694
+ });
695
+ var j = await r.json();
696
+ el.innerHTML = _renderGitSshStatus(j);
697
+ _applyGitSshActionState(j);
698
+ } catch (e) { el.textContent = '❌ ' + String(e); }
699
+ }
700
+
701
+ function _renderGitSshStatus(j) {
702
+ if (!j || typeof j !== 'object') return escHtml(String(j && j.result || '未知'));
703
+ var parts = [];
704
+ var connected = !!j.connected;
705
+ var recommendation = String(j.recommendation || '');
706
+ var title = connected
707
+ ? '✅ SSH 已可用'
708
+ : (recommendation === 'needs_platform_or_config'
709
+ ? '⚠️ 已检测到密钥,但当前平台未连通'
710
+ : (recommendation === 'generate_suggested' ? '❌ 未检测到可用 SSH 密钥' : '❔ 暂时无法判断 SSH 状态'));
711
+ var host = escHtml(j.ssh_host || '');
712
+ var auth = escHtml(j.auth_message || '');
713
+ var bannerBg = connected
714
+ ? 'rgba(34,197,94,0.12)'
715
+ : (recommendation === 'generate_suggested' ? 'rgba(239,68,68,0.12)' : 'rgba(234,179,8,0.12)');
716
+ var bannerIcon = connected ? '✅' : (recommendation === 'generate_suggested' ? '❌' : '⚠️');
717
+ parts.push(
718
+ '<div style="padding:6px 8px;border-radius:4px;background:' + bannerBg + ';margin-bottom:6px;">'
719
+ + '<div><strong>' + title + '</strong></div>'
720
+ + '<div style="margin-top:3px;">' + bannerIcon + ' <strong>' + host + '</strong>'
721
+ + (auth ? ' — ' + auth : '')
722
+ + '</div>'
723
+ + '</div>'
724
+ );
725
+ var ids = j.identities || [];
726
+ if (ids.length) {
727
+ parts.push('<div style="margin:4px 0;"><strong>实际会尝试的密钥</strong> <span style="color:var(--text-muted);">(ssh -G)</span></div>');
728
+ parts.push('<ul style="margin:0;padding-left:16px;">');
729
+ ids.forEach(function(row) {
730
+ var mark = row.private_exists ? '✓' : '✗';
731
+ var line = mark + ' <code style="font-size:10px;">' + escHtml(row.private || '') + '</code>';
732
+ if (row.public_exists) {
733
+ line += '<br><span style="color:var(--text-muted);padding-left:14px;">公钥: '
734
+ + escHtml(row.public || '') + '</span>';
735
+ }
736
+ parts.push('<li style="margin-bottom:4px;">' + line + '</li>');
737
+ });
738
+ parts.push('</ul>');
739
+ } else {
740
+ parts.push('<div style="color:var(--text-muted);">未解析到 identityfile(可能仅依赖 ssh-agent / 系统钥匙串)</div>');
741
+ }
742
+ var agent = j.agent_keys || [];
743
+ if (agent.length) {
744
+ parts.push('<div style="margin-top:6px;"><strong>ssh-agent</strong></div><ul style="margin:0;padding-left:16px;">');
745
+ agent.slice(0, 5).forEach(function(ln) {
746
+ parts.push('<li><code style="font-size:10px;">' + escHtml(ln) + '</code></li>');
747
+ });
748
+ parts.push('</ul>');
749
+ }
750
+ var rec = j.recommendation_text || j.result || '';
751
+ if (rec) {
752
+ var recStyle = connected
753
+ ? 'color:var(--text-muted);'
754
+ : (j.recommendation === 'generate_suggested' ? 'color:var(--text);' : 'color:#b45309;');
755
+ parts.push('<div style="margin-top:8px;font-size:11px;' + recStyle + '">' + escHtml(rec) + '</div>');
756
+ }
757
+ return parts.join('');
758
+ }
759
+
760
+ function _setBtnStyle(btn, isPrimary) {
761
+ if (!btn) return;
762
+ btn.classList.remove('btn--primary', 'btn--ghost');
763
+ btn.classList.add(isPrimary ? 'btn--primary' : 'btn--ghost');
764
+ }
765
+
766
+ function _applyGitSshActionState(j) {
767
+ var btnGen = document.getElementById('btnConfigSshGen');
768
+ var btnCat = document.getElementById('btnConfigSshCat');
769
+ var btnTest = document.getElementById('btnConfigSshTest');
770
+ if (!btnGen || !btnCat || !btnTest) return;
771
+
772
+ btnGen.style.display = '';
773
+ btnCat.style.display = '';
774
+ btnTest.style.display = '';
775
+ btnGen.textContent = '生成密钥';
776
+ btnCat.textContent = '查看公钥';
777
+ btnTest.textContent = '测试连接';
778
+
779
+ var connected = !!(j && j.connected);
780
+ var recommendation = String((j && j.recommendation) || '');
781
+ if (connected) {
782
+ _setBtnStyle(btnCat, true);
783
+ _setBtnStyle(btnTest, false);
784
+ _setBtnStyle(btnGen, false);
785
+ btnCat.textContent = '查看并复制公钥';
786
+ btnTest.textContent = '重新检测';
787
+ btnGen.textContent = '仍要生成新密钥';
788
+ return;
789
+ }
790
+ if (recommendation === 'needs_platform_or_config') {
791
+ _setBtnStyle(btnCat, true);
792
+ _setBtnStyle(btnTest, false);
793
+ _setBtnStyle(btnGen, false);
794
+ btnCat.textContent = '查看并复制公钥';
795
+ btnTest.textContent = '重新检测';
796
+ btnGen.style.display = 'none';
797
+ return;
798
+ }
799
+ if (recommendation === 'generate_suggested') {
800
+ _setBtnStyle(btnGen, true);
801
+ _setBtnStyle(btnTest, false);
802
+ _setBtnStyle(btnCat, false);
803
+ btnGen.textContent = '生成 ed25519 密钥';
804
+ btnTest.textContent = '重新检测';
805
+ btnCat.style.display = 'none';
806
+ return;
807
+ }
808
+ _setBtnStyle(btnTest, true);
809
+ _setBtnStyle(btnCat, false);
810
+ _setBtnStyle(btnGen, false);
811
+ btnTest.textContent = '重新检测';
812
+ }
813
+
814
+ async function _gitSshAction(action, opts) {
815
+ opts = opts || {};
816
+ var output = document.getElementById('gitConfigSshOutput');
817
+ if (!output) return;
818
+ var provider = _gitProviderFromSelect('selGitDefaultProvider');
819
+ if (action === 'generate' && !opts.force) {
820
+ if (!confirm('生成新的 SSH 密钥(ed25519)?已有 ~/.ssh/id_ed25519 不会被覆盖。')) return;
821
+ }
822
+ output.textContent = '⏳ 执行中...';
823
+ try {
824
+ var payload = { command: 'ssh', args: action, provider: provider };
825
+ if (action === 'generate' && opts.force) payload.force = true;
826
+ var r = await fetch('/api/ui/git', {
827
+ method: 'POST',
828
+ headers: { 'Content-Type': 'application/json' },
829
+ body: JSON.stringify(payload),
830
+ });
831
+ var j = await r.json();
832
+ if (action === 'generate' && j.needs_confirm && !opts.force) {
833
+ var msg = j.result || '当前 SSH 已可用,仍要生成新密钥吗?';
834
+ if (confirm(msg)) {
835
+ return _gitSshAction('generate', { force: true });
836
+ }
837
+ output.textContent = '已取消';
838
+ return;
839
+ }
840
+ output.textContent = j.result || j.error || '完成';
841
+ if (action === 'generate' || action === 'test') _refreshGitSshStatus();
842
+ if (action === 'generate' && !j.error) {
843
+ await _gitSshAction('cat', { fromGenerate: true });
844
+ }
845
+ if (action === 'cat' && j.result) {
846
+ var pubMatch = j.result.match(/(ssh-\S+)/);
847
+ if (pubMatch) {
848
+ try {
849
+ await navigator.clipboard.writeText(pubMatch[1]);
850
+ output.textContent += opts.fromGenerate ? '\n📋 已自动复制新公钥。' : '\n📋 公钥已复制!';
851
+ } catch (_) {}
852
+ }
853
+ }
854
+ } catch (e) { output.textContent = '❌ ' + String(e); }
855
+ }
856
+
857
+ /* ---------- HTTPS 凭据管理 ---------- */
858
+ function _bindGitCredEvents() {
859
+ if (window._gitCredBound) return;
860
+ window._gitCredBound = true;
861
+
862
+ var header = document.getElementById('gitConfigCredHeader');
863
+ if (!header) return;
864
+
865
+ header.addEventListener('click', function() {
866
+ var body = document.getElementById('gitConfigCredBody');
867
+ var icon = document.getElementById('gitConfigCredIcon');
868
+ if (!body || !icon) return;
869
+ var opening = body.style.display !== 'block';
870
+ body.style.display = opening ? 'block' : 'none';
871
+ icon.textContent = opening ? '▼' : '▶';
872
+ if (opening) _refreshGitCredStatus();
873
+ });
874
+
875
+ var bAdd = document.getElementById('btnConfigCredAdd');
876
+ var bCan = document.getElementById('btnConfigCredCancel');
877
+ var bSav = document.getElementById('btnConfigCredSave');
878
+ var bSho = document.getElementById('btnConfigCredShow');
879
+ var bClr = document.getElementById('btnConfigCredClear');
880
+ if (bAdd) bAdd.addEventListener('click', function() {
881
+ var f = document.getElementById('gitConfigCredForm');
882
+ var defProv = _gitProviderFromSelect('selGitDefaultProvider');
883
+ var credProv = document.getElementById('selConfigCredProvider');
884
+ if (credProv) credProv.value = defProv;
885
+ _syncCredUrlFromProvider();
886
+ if (f) f.style.display = 'block';
887
+ });
888
+ var credProvSel = document.getElementById('selConfigCredProvider');
889
+ if (credProvSel) credProvSel.addEventListener('change', _syncCredUrlFromProvider);
890
+ if (bCan) bCan.addEventListener('click', function() {
891
+ var f = document.getElementById('gitConfigCredForm');
892
+ if (f) f.style.display = 'none';
893
+ });
894
+ if (bSav) bSav.addEventListener('click', _saveGitCred);
895
+ if (bSho) bSho.addEventListener('click', _showGitCred);
896
+ if (bClr) bClr.addEventListener('click', _clearGitCred);
897
+ }
898
+
899
+ async function _refreshGitCredStatus() {
900
+ var el = document.getElementById('gitConfigCredStatus');
901
+ if (!el) return;
902
+ el.textContent = '检查中...';
903
+ try {
904
+ var r = await fetch('/api/ui/git', {
905
+ method: 'POST',
906
+ headers: { 'Content-Type': 'application/json' },
907
+ body: JSON.stringify({ command: 'credential', action: 'show' }),
908
+ });
909
+ var j = await r.json();
910
+ if (j.error && !j.result) {
911
+ el.textContent = '❌ ' + j.error;
912
+ return;
913
+ }
914
+ el.innerHTML = (j.result || '未知').replace(/\n/g, '<br>');
915
+ } catch (e) { el.textContent = '❌ ' + String(e); }
916
+ }
917
+
918
+ async function _saveGitCred() {
919
+ var url = document.getElementById('inpConfigCredUrl').value.trim();
920
+ var user = document.getElementById('inpConfigCredUser').value.trim();
921
+ var token = document.getElementById('inpConfigCredToken').value.trim();
922
+ var provider = _gitProviderFromSelect('selConfigCredProvider');
923
+ if (!url || !user || !token) { alert('请填写平台、用户名和 Token'); return; }
924
+ var status = document.getElementById('gitConfigCredStatus');
925
+ if (!status) return;
926
+ status.textContent = '保存中...';
927
+ try {
928
+ var r = await fetch('/api/ui/git', {
929
+ method: 'POST',
930
+ headers: { 'Content-Type': 'application/json' },
931
+ body: JSON.stringify({
932
+ command: 'credential',
933
+ action: 'store',
934
+ url: url,
935
+ provider: provider,
936
+ username: user,
937
+ token: token,
938
+ }),
939
+ });
940
+ var j = await r.json();
941
+ if (j.error && !j.result) {
942
+ status.textContent = '❌ ' + j.error;
943
+ return;
944
+ }
945
+ status.textContent = j.result || '✅ 已保存';
946
+ document.getElementById('gitConfigCredForm').style.display = 'none';
947
+ document.getElementById('inpConfigCredUser').value = '';
948
+ document.getElementById('inpConfigCredToken').value = '';
949
+ setTimeout(function() { status.textContent = ''; }, 3000);
950
+ } catch (e) { status.textContent = '❌ ' + String(e); }
951
+ }
952
+
953
+ async function _showGitCred() {
954
+ await _refreshGitCredStatus();
955
+ }
956
+
957
+ async function _clearGitCred() {
958
+ if (!confirm('确定清除已配置的 Git HTTPS 凭据吗?')) return;
959
+ var status = document.getElementById('gitConfigCredStatus');
960
+ if (!status) return;
961
+ status.textContent = '清除中...';
962
+ try {
963
+ var r = await fetch('/api/ui/git', {
964
+ method: 'POST',
965
+ headers: { 'Content-Type': 'application/json' },
966
+ body: JSON.stringify({ command: 'credential', action: 'clear' }),
967
+ });
968
+ var j = await r.json();
969
+ status.textContent = j.result || j.error || '✅ 已清除';
970
+ setTimeout(function() { status.textContent = ''; }, 3000);
971
+ } catch (e) { status.textContent = '❌ ' + String(e); }
972
+ }
973
+