feihong-code 0.2.2 → 0.5.1

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 (152) hide show
  1. package/.github/FUNDING.yml +4 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +37 -37
  3. package/.github/ISSUE_TEMPLATE/config.yml +14 -14
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +28 -28
  5. package/.github/PULL_REQUEST_TEMPLATE.md +46 -46
  6. package/.github/SECURITY.md +67 -67
  7. package/.github/dependabot.yml +16 -0
  8. package/.github/workflows/ci.yml +1 -1
  9. package/AGENT-GUIDE.md +382 -237
  10. package/CHANGELOG.md +405 -73
  11. package/CODE_OF_CONDUCT.md +56 -56
  12. package/CONTRIBUTING.md +68 -68
  13. package/LICENSE +22 -22
  14. package/README.md +31 -25
  15. package/README.self-evolve.md +274 -0
  16. package/dist/agent/code-review.js +2 -0
  17. package/dist/agent/code-review.js.map +1 -1
  18. package/dist/agent/code-writer.js +70 -17
  19. package/dist/agent/code-writer.js.map +1 -1
  20. package/dist/agent/context-compactor.js +13 -4
  21. package/dist/agent/context-compactor.js.map +1 -1
  22. package/dist/agent/experience.js +262 -62
  23. package/dist/agent/experience.js.map +1 -1
  24. package/dist/agent/orchestrator.js +148 -68
  25. package/dist/agent/orchestrator.js.map +1 -1
  26. package/dist/agent/quality-gate.js +10 -4
  27. package/dist/agent/quality-gate.js.map +1 -1
  28. package/dist/agent/repo-context.js +181 -0
  29. package/dist/agent/repo-context.js.map +1 -0
  30. package/dist/agent/repo-reader.js +92 -99
  31. package/dist/agent/repo-reader.js.map +1 -1
  32. package/dist/agent/self-heal.js +38 -48
  33. package/dist/agent/self-heal.js.map +1 -1
  34. package/dist/agent/self-improver.js +65 -61
  35. package/dist/agent/self-improver.js.map +1 -1
  36. package/dist/agent/subagent-summary.js +31 -0
  37. package/dist/agent/subagent-summary.js.map +1 -0
  38. package/dist/agent/subagent.js +52 -2
  39. package/dist/agent/subagent.js.map +1 -1
  40. package/dist/agent/symbol-index.js +160 -0
  41. package/dist/agent/symbol-index.js.map +1 -0
  42. package/dist/agent/team.js +193 -0
  43. package/dist/agent/team.js.map +1 -0
  44. package/dist/cli/commands.js +124 -143
  45. package/dist/cli/commands.js.map +1 -1
  46. package/dist/cli/index.js +113 -106
  47. package/dist/cli/index.js.map +1 -1
  48. package/dist/cli/repl.js +82 -7
  49. package/dist/cli/repl.js.map +1 -1
  50. package/dist/cli/run.js +601 -95
  51. package/dist/cli/run.js.map +1 -1
  52. package/dist/cli/tui.js +145 -0
  53. package/dist/cli/tui.js.map +1 -0
  54. package/dist/cli/version.js +1 -1
  55. package/dist/enterprise/audit.js +75 -12
  56. package/dist/enterprise/audit.js.map +1 -1
  57. package/dist/enterprise/index.js +14 -11
  58. package/dist/enterprise/index.js.map +1 -1
  59. package/dist/enterprise/policy.js +22 -10
  60. package/dist/enterprise/policy.js.map +1 -1
  61. package/dist/harness/executor.js +127 -0
  62. package/dist/harness/executor.js.map +1 -0
  63. package/dist/harness/harness.js +87 -0
  64. package/dist/harness/harness.js.map +1 -0
  65. package/dist/harness/index.js +31 -0
  66. package/dist/harness/index.js.map +1 -0
  67. package/dist/harness/loader.js +138 -0
  68. package/dist/harness/loader.js.map +1 -0
  69. package/dist/harness/reporter.js +34 -0
  70. package/dist/harness/reporter.js.map +1 -0
  71. package/dist/harness/types.js +10 -0
  72. package/dist/harness/types.js.map +1 -0
  73. package/dist/harness/verifier.js +48 -0
  74. package/dist/harness/verifier.js.map +1 -0
  75. package/dist/hello.js +14 -0
  76. package/dist/hello.js.map +1 -0
  77. package/dist/memory/auto-summarize.js +208 -0
  78. package/dist/memory/auto-summarize.js.map +1 -0
  79. package/dist/memory/index.js +228 -0
  80. package/dist/memory/index.js.map +1 -0
  81. package/dist/models/model-router.js +23 -10
  82. package/dist/models/model-router.js.map +1 -1
  83. package/dist/plugins/plugin-loader.js +179 -0
  84. package/dist/plugins/plugin-loader.js.map +1 -0
  85. package/dist/runtime/event-log.js.map +1 -1
  86. package/dist/runtime/hooks.js +80 -0
  87. package/dist/runtime/hooks.js.map +1 -0
  88. package/dist/self-evolve/hook.js +60 -0
  89. package/dist/self-evolve/hook.js.map +1 -0
  90. package/dist/shared/config.js +35 -3
  91. package/dist/shared/config.js.map +1 -1
  92. package/dist/shared/i18n.js +535 -0
  93. package/dist/shared/i18n.js.map +1 -0
  94. package/dist/skills/grill.js +2 -1
  95. package/dist/skills/grill.js.map +1 -1
  96. package/dist/skills/self-heal.js +73 -0
  97. package/dist/skills/self-heal.js.map +1 -0
  98. package/dist/skills/skill-loader.js +131 -0
  99. package/dist/skills/skill-loader.js.map +1 -0
  100. package/dist/skills/skill-market.js +195 -0
  101. package/dist/skills/skill-market.js.map +1 -0
  102. package/dist/tools/analysis/code-analyzer.js +45 -18
  103. package/dist/tools/analysis/code-analyzer.js.map +1 -1
  104. package/dist/tools/index.js +5 -0
  105. package/dist/tools/index.js.map +1 -1
  106. package/dist/tools/mcp/index.js +84 -0
  107. package/dist/tools/mcp/index.js.map +1 -0
  108. package/dist/tools/mcp/mcp-client.js +172 -0
  109. package/dist/tools/mcp/mcp-client.js.map +1 -0
  110. package/dist/tools/sandbox.js +126 -0
  111. package/dist/tools/sandbox.js.map +1 -0
  112. package/dist/tools/shell/exec.js +25 -0
  113. package/dist/tools/shell/exec.js.map +1 -1
  114. package/dist/tools/shell/run-shell.tool.js +4 -1
  115. package/dist/tools/shell/run-shell.tool.js.map +1 -1
  116. package/dist/tools/skills/load-skill.tool.js +36 -0
  117. package/dist/tools/skills/load-skill.tool.js.map +1 -0
  118. package/dist/tools/tool.interface.js.map +1 -1
  119. package/dist/tools/tool.registry.js +37 -1
  120. package/dist/tools/tool.registry.js.map +1 -1
  121. package/dist/tools/web/web.tool.js +139 -0
  122. package/dist/tools/web/web.tool.js.map +1 -0
  123. package/dist/web/auth.js +42 -3
  124. package/dist/web/auth.js.map +1 -1
  125. package/dist/web/channels.js +171 -0
  126. package/dist/web/channels.js.map +1 -0
  127. package/dist/web/public/index.html +3253 -39
  128. package/dist/web/public/index.html.tmp +3117 -0
  129. package/dist/web/public/index_new.html +3196 -0
  130. package/dist/web/server.js +790 -11
  131. package/dist/web/server.js.map +1 -1
  132. package/dist/web/task-queue.js +352 -0
  133. package/dist/web/task-queue.js.map +1 -0
  134. package/dist/web/web-config.js +143 -0
  135. package/dist/web/web-config.js.map +1 -0
  136. package/docs/Deployment_Guide_EN.md +288 -0
  137. package/docs/Technical_Manual_EN.md +216 -0
  138. package/docs/User_Manual_EN.md +314 -0
  139. package/docs/self-evolve-implementation.md +165 -0
  140. package/docs/self-evolve.md +166 -0
  141. package/docs//344/272/247/345/223/201/345/274/200/345/217/221/346/226/207/346/241/243.md +614 -614
  142. package/docs//344/274/201/344/270/232/351/203/250/347/275/262/344/270/216/345/220/210/350/247/204.md +267 -267
  143. package/docs//344/275/277/347/224/250/350/257/264/346/230/216/344/271/246.md +318 -358
  144. package/docs//345/270/270/350/247/201/351/227/256/351/242/230/344/270/216/346/225/205/351/232/234/346/216/222/346/237/245.md +130 -130
  145. package/docs//346/212/200/346/234/257/350/257/264/346/230/216/344/271/246.md +216 -303
  146. package/docs//346/236/266/346/236/204/344/270/216API.md +238 -238
  147. package/docs//347/224/250/346/210/267/346/211/213/345/206/214.md +196 -196
  148. package/docs//351/203/250/347/275/262/346/214/207/345/215/227.md +165 -165
  149. package/docs//351/203/250/347/275/262/350/257/264/346/230/216/344/271/246.md +288 -0
  150. package/docs//351/205/215/347/275/256/345/217/202/350/200/203.md +127 -127
  151. package/package.json +114 -109
  152. package/tool-schema.json +127 -127
@@ -0,0 +1,3196 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>飞虹 Code 控制台 / fhcode Console</title>
7
+ <style>
8
+ :root {
9
+ --bg: #f5f7fa;
10
+ --card: #ffffff;
11
+ --ink: #1f2933;
12
+ --ink-2: #475569;
13
+ --muted: #6b7785;
14
+ --brand: #2563eb;
15
+ --brand-soft: #eff4ff;
16
+ --ok: #16a34a;
17
+ --err: #dc2626;
18
+ --warn: #d97706;
19
+ --border: #e3e8ef;
20
+ --line: #e3e8ef;
21
+ --sidebar-width: 56px;
22
+ --topbar-height: 48px;
23
+ }
24
+ .dark {
25
+ --bg: #0f172a;
26
+ --card: #1e293b;
27
+ --ink: #f1f5f9;
28
+ --ink-2: #cbd5e1;
29
+ --muted: #94a3b8;
30
+ --brand: #3b82f6;
31
+ --brand-soft: #1e3a8a;
32
+ --border: #334155;
33
+ --line: #334155;
34
+ }
35
+ * { box-sizing: border-box; }
36
+ body {
37
+ margin: 0;
38
+ font-family: -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", Roboto, Helvetica, Arial, sans-serif;
39
+ background: var(--bg);
40
+ color: var(--ink);
41
+ height: 100vh;
42
+ overflow: hidden;
43
+ }
44
+
45
+ /* ========== 登录遮罩 ========== */
46
+ #loginOverlay {
47
+ position: fixed; inset: 0; background: var(--bg); z-index: 50;
48
+ display: flex; align-items: center; justify-content: center;
49
+ }
50
+ #loginOverlay .login-card {
51
+ background: var(--card); border: 1px solid var(--border); border-radius: 16px;
52
+ padding: 32px; width: 360px; box-shadow: 0 10px 30px rgba(31,41,51,0.08);
53
+ }
54
+ #loginOverlay h2 { margin: 0 0 4px; font-size: 20px; }
55
+ #loginOverlay p { margin: 0 0 20px; color: var(--muted); font-size: 13px; }
56
+ #loginOverlay input {
57
+ width: 100%; border: 1px solid var(--border); border-radius: 10px;
58
+ padding: 10px 12px; font-size: 14px; margin-bottom: 12px; font-family: inherit;
59
+ background: var(--card); color: var(--ink);
60
+ }
61
+ #loginOverlay button {
62
+ width: 100%; border: none; background: var(--brand); color: #fff;
63
+ border-radius: 10px; padding: 10px; font-size: 14px; cursor: pointer;
64
+ }
65
+ #loginOverlay .login-status { font-size: 13px; margin-top: 10px; min-height: 18px; }
66
+ #loginOverlay .login-status.err { color: var(--err); }
67
+
68
+ /* ========== 主布局 ========== */
69
+ .app-layout {
70
+ display: flex;
71
+ flex-direction: column;
72
+ height: 100vh;
73
+ display: none;
74
+ }
75
+ .app-layout.show { display: flex; }
76
+
77
+ /* ========== 顶部导航栏 ========== */
78
+ .topbar {
79
+ height: var(--topbar-height);
80
+ background: var(--card);
81
+ border-bottom: 1px solid var(--border);
82
+ display: flex;
83
+ align-items: center;
84
+ padding: 0 16px;
85
+ gap: 12px;
86
+ flex-shrink: 0;
87
+ }
88
+ .topbar-brand {
89
+ font-weight: 700;
90
+ font-size: 15px;
91
+ color: var(--brand);
92
+ display: flex;
93
+ align-items: center;
94
+ gap: 8px;
95
+ }
96
+ .topbar-brand .logo {
97
+ width: 28px;
98
+ height: 28px;
99
+ background: var(--brand);
100
+ color: #fff;
101
+ border-radius: 8px;
102
+ display: flex;
103
+ align-items: center;
104
+ justify-content: center;
105
+ font-size: 14px;
106
+ }
107
+ .topbar-nav {
108
+ display: flex;
109
+ gap: 4px;
110
+ margin-left: 24px;
111
+ }
112
+ .topbar-nav-item {
113
+ padding: 6px 12px;
114
+ border-radius: 8px;
115
+ font-size: 13px;
116
+ color: var(--ink-2);
117
+ cursor: pointer;
118
+ display: flex;
119
+ align-items: center;
120
+ gap: 6px;
121
+ transition: all 0.15s;
122
+ }
123
+ .topbar-nav-item:hover {
124
+ background: var(--bg);
125
+ color: var(--ink);
126
+ }
127
+ .topbar-nav-item.active {
128
+ background: var(--brand-soft);
129
+ color: var(--brand);
130
+ font-weight: 600;
131
+ }
132
+ .topbar-spacer { flex: 1; }
133
+ .topbar-user {
134
+ display: flex;
135
+ align-items: center;
136
+ gap: 10px;
137
+ padding: 6px 10px;
138
+ border-radius: 10px;
139
+ cursor: pointer;
140
+ transition: background 0.15s;
141
+ }
142
+ .topbar-user:hover { background: var(--bg); }
143
+ .topbar-avatar {
144
+ width: 32px;
145
+ height: 32px;
146
+ background: var(--brand-soft);
147
+ border-radius: 50%;
148
+ display: flex;
149
+ align-items: center;
150
+ justify-content: center;
151
+ font-size: 16px;
152
+ }
153
+ .topbar-phone { font-size: 13px; font-weight: 500; color: var(--ink); }
154
+
155
+ /* ========== 主体内容区 ========== */
156
+ .main-area {
157
+ flex: 1;
158
+ display: flex;
159
+ overflow: hidden;
160
+ }
161
+
162
+ /* ========== 左侧侧边栏 ========== */
163
+ .sidebar {
164
+ width: 200px;
165
+ background: var(--card);
166
+ border-right: 1px solid var(--border);
167
+ display: flex;
168
+ flex-direction: column;
169
+ padding: 12px 8px;
170
+ gap: 2px;
171
+ flex-shrink: 0;
172
+ overflow-y: auto;
173
+ }
174
+ .sidebar-section {
175
+ margin-bottom: 8px;
176
+ padding-bottom: 8px;
177
+ border-bottom: 1px solid var(--border);
178
+ }
179
+ .sidebar-section-title {
180
+ font-size: 11px;
181
+ color: var(--muted);
182
+ text-transform: uppercase;
183
+ letter-spacing: 0.5px;
184
+ padding: 4px 8px;
185
+ margin-bottom: 4px;
186
+ }
187
+ .sidebar-icon {
188
+ width: 100%;
189
+ height: 36px;
190
+ border-radius: 8px;
191
+ display: flex;
192
+ align-items: center;
193
+ gap: 10px;
194
+ padding: 0 8px;
195
+ cursor: pointer;
196
+ color: var(--ink-2);
197
+ transition: all 0.15s;
198
+ position: relative;
199
+ font-size: 13px;
200
+ }
201
+ .sidebar-icon .icon {
202
+ font-size: 16px;
203
+ width: 20px;
204
+ text-align: center;
205
+ flex-shrink: 0;
206
+ }
207
+ .sidebar-icon .label {
208
+ flex: 1;
209
+ white-space: nowrap;
210
+ overflow: hidden;
211
+ text-overflow: ellipsis;
212
+ }
213
+ .sidebar-icon .badge {
214
+ font-size: 10px;
215
+ padding: 2px 6px;
216
+ border-radius: 10px;
217
+ background: var(--brand-soft);
218
+ color: var(--brand);
219
+ font-weight: 600;
220
+ flex-shrink: 0;
221
+ }
222
+ .sidebar-icon:hover {
223
+ background: var(--bg);
224
+ color: var(--ink);
225
+ }
226
+ .sidebar-icon.active {
227
+ background: var(--brand-soft);
228
+ color: var(--brand);
229
+ font-weight: 600;
230
+ }
231
+ .sidebar-icon.active::before {
232
+ content: '';
233
+ position: absolute;
234
+ left: 0;
235
+ top: 50%;
236
+ transform: translateY(-50%);
237
+ width: 3px;
238
+ height: 20px;
239
+ background: var(--brand);
240
+ border-radius: 0 2px 2px 0;
241
+ }
242
+ .sidebar-spacer { flex: 1; }
243
+ .sidebar-pin {
244
+ font-size: 10px;
245
+ color: var(--warn);
246
+ flex-shrink: 0;
247
+ }
248
+ .task-item {
249
+ display: flex;
250
+ align-items: center;
251
+ gap: 8px;
252
+ padding: 8px;
253
+ border-radius: 6px;
254
+ cursor: pointer;
255
+ font-size: 12px;
256
+ transition: background 0.15s;
257
+ }
258
+ .task-item:hover { background: var(--bg); }
259
+ .task-item.active { background: var(--brand-soft); }
260
+ .task-item .task-icon { font-size: 14px; }
261
+ .task-item .task-title {
262
+ flex: 1;
263
+ white-space: nowrap;
264
+ overflow: hidden;
265
+ text-overflow: ellipsis;
266
+ color: var(--ink-2);
267
+ }
268
+ .task-item.active .task-title { color: var(--brand); }
269
+ .task-item .task-actions {
270
+ display: none;
271
+ gap: 4px;
272
+ font-size: 11px;
273
+ }
274
+ .task-item:hover .task-actions { display: flex; }
275
+ .task-item .task-actions button {
276
+ padding: 2px 4px;
277
+ border: none;
278
+ background: transparent;
279
+ cursor: pointer;
280
+ border-radius: 4px;
281
+ font-size: 10px;
282
+ }
283
+ .task-item .task-actions button:hover { background: var(--border); }
284
+ .task-item .task-actions .delete-btn:hover { background: #fee2e2; color: var(--err); }
285
+ .task-pinned { color: var(--warn); }
286
+
287
+ /* ========== 中间对话区 ========== */
288
+ .chat-area {
289
+ flex: 1;
290
+ display: flex;
291
+ flex-direction: column;
292
+ min-width: 0;
293
+ background: var(--bg);
294
+ }
295
+
296
+ /* ========== 任务上下文头部 ========== */
297
+ .task-context-header {
298
+ display: flex;
299
+ align-items: center;
300
+ gap: 12px;
301
+ padding: 12px 16px;
302
+ background: var(--card);
303
+ border-bottom: 1px solid var(--border);
304
+ flex-shrink: 0;
305
+ }
306
+ .tch-left { flex: 1; min-width: 0; }
307
+ .tch-title { font-size: 15px; font-weight: 700; color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
308
+ .tch-sub { font-size: 12px; color: var(--muted); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
309
+ .tch-right { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
310
+ .tch-time { font-size: 12px; color: var(--muted); }
311
+
312
+ /* ========== 思维链路线程 ========== */
313
+ .messages { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; padding: 16px; padding-right: 4px; }
314
+ .step { max-width: 92%; font-size: 13.5px; line-height: 1.6; word-break: break-word; }
315
+ .step.created {
316
+ align-self: stretch; background: var(--bg); border: 1px dashed var(--border);
317
+ border-radius: 10px; padding: 8px 12px; color: var(--muted); font-size: 12.5px;
318
+ }
319
+ .step.reasoning {
320
+ align-self: flex-start; background: var(--card); border: 1px solid var(--border);
321
+ border-left: 3px solid var(--brand); border-radius: 10px; padding: 10px 14px;
322
+ white-space: pre-wrap; color: var(--ink-2);
323
+ }
324
+ .step.reasoning .reasoning-head { font-size: 11px; color: var(--brand); font-weight: 600; margin-bottom: 4px; display: flex; align-items: center; gap: 6px; }
325
+ .step.toolcall {
326
+ align-self: flex-start; background: #fffbeb; border: 1px solid #fde68a;
327
+ border-radius: 10px; padding: 8px 12px; color: #92400e; font-size: 12.5px;
328
+ }
329
+ .dark .step.toolcall { background: #3a2f12; color: #fcd34d; border-color: #5b4a1f; }
330
+ .step.toolcall .tc-name { font-weight: 600; }
331
+ .step.toolcall .tc-args {
332
+ color: var(--muted); font-size: 12px; margin-top: 2px; white-space: pre-wrap; word-break: break-word;
333
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
334
+ }
335
+ .step.result {
336
+ align-self: flex-start; background: var(--card); border: 1px solid var(--border);
337
+ border-radius: 10px; padding: 8px 12px; color: var(--ink-2); font-size: 12.5px; max-width: 92%;
338
+ }
339
+ .step.result .res-head { font-weight: 600; display: flex; align-items: center; gap: 6px; }
340
+ .step.result.ok .res-head { color: var(--ok); }
341
+ .step.result.fail .res-head { color: var(--err); }
342
+ .step.result .res-out {
343
+ margin-top: 4px; white-space: pre-wrap; word-break: break-word; max-height: 160px; overflow-y: auto;
344
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 12px; color: var(--muted);
345
+ }
346
+ .step.note { align-self: stretch; background: var(--bg); border-radius: 8px; padding: 6px 12px; color: var(--muted); font-size: 12px; }
347
+ /* 对话流与思维链路分隔标题 */
348
+ .conv-divider {
349
+ align-self: stretch; text-align: center; font-size: 11px; color: var(--muted);
350
+ border-top: 1px solid var(--line); padding-top: 8px; margin-top: 4px;
351
+ }
352
+ .step.final {
353
+ align-self: flex-end; background: var(--ok); color: #fff; border-radius: 12px;
354
+ padding: 12px 16px; white-space: pre-wrap; word-break: break-word; max-width: 92%;
355
+ position: relative;
356
+ }
357
+ .step.final .final-head { font-size: 11px; opacity: 0.85; margin-bottom: 4px; font-weight: 600; }
358
+ .step.final .final-actions {
359
+ position: absolute; top: 8px; right: 8px; display: flex; gap: 4px;
360
+ opacity: 0; transition: opacity 0.2s;
361
+ }
362
+ .step.final:hover .final-actions { opacity: 1; }
363
+ .step.final .final-actions button {
364
+ background: rgba(255,255,255,0.2); border: none; color: #fff;
365
+ border-radius: 6px; padding: 3px 8px; font-size: 11px; cursor: pointer;
366
+ transition: background 0.15s;
367
+ }
368
+ .step.final .final-actions button:hover { background: rgba(255,255,255,0.35); }
369
+ .step.error {
370
+ align-self: flex-end; background: var(--err); color: #fff; border-radius: 12px;
371
+ padding: 12px 16px; white-space: pre-wrap; word-break: break-word; max-width: 92%;
372
+ }
373
+
374
+ /* 对话消息区 */
375
+ .conv {
376
+ flex: 1;
377
+ display: flex;
378
+ flex-direction: column;
379
+ padding: 16px;
380
+ gap: 12px;
381
+ overflow: hidden;
382
+ }
383
+ .messages {
384
+ flex: 1;
385
+ overflow-y: auto;
386
+ display: flex;
387
+ flex-direction: column;
388
+ gap: 10px;
389
+ padding-right: 4px;
390
+ }
391
+ .msg {
392
+ padding: 10px 14px;
393
+ border-radius: 12px;
394
+ font-size: 13.5px;
395
+ line-height: 1.6;
396
+ max-width: 85%;
397
+ white-space: pre-wrap;
398
+ word-break: break-word;
399
+ }
400
+ .msg.user {
401
+ background: var(--brand-soft);
402
+ align-self: flex-end;
403
+ }
404
+ .msg.assistant {
405
+ background: var(--card);
406
+ border: 1px solid var(--border);
407
+ align-self: flex-start;
408
+ }
409
+ .dark .msg.assistant { background: #1e293b; }
410
+ .msg.sys {
411
+ background: #f1f5f9;
412
+ align-self: flex-start;
413
+ color: var(--ink-2);
414
+ }
415
+ .dark .msg.sys { background: #334155; }
416
+ .msg .artifact { color: var(--brand); text-decoration: underline; cursor: pointer; }
417
+ .msg .artifact:hover { opacity: 0.8; }
418
+
419
+ /* 输入栏 */
420
+ .input-area {
421
+ padding: 0 16px 16px;
422
+ flex-shrink: 0;
423
+ }
424
+ .input-bar {
425
+ border: 1px solid var(--border);
426
+ border-radius: 14px;
427
+ padding: 12px;
428
+ background: var(--card);
429
+ box-shadow: 0 4px 12px rgba(0,0,0,0.04);
430
+ }
431
+ .input-bar textarea {
432
+ width: 100%;
433
+ border: none;
434
+ margin: 0;
435
+ min-height: 60px;
436
+ resize: none;
437
+ background: transparent;
438
+ color: var(--ink);
439
+ font-size: 14px;
440
+ font-family: inherit;
441
+ }
442
+ .input-bar textarea:focus { outline: none; }
443
+ .input-toolbar {
444
+ display: flex;
445
+ align-items: center;
446
+ gap: 8px;
447
+ margin-top: 10px;
448
+ flex-wrap: wrap;
449
+ }
450
+ .input-toolbar .spacer { flex: 1; }
451
+ .pill {
452
+ border: 1px solid var(--border);
453
+ border-radius: 8px;
454
+ padding: 5px 10px;
455
+ font-size: 12.5px;
456
+ background: var(--card);
457
+ color: var(--ink-2);
458
+ cursor: pointer;
459
+ user-select: none;
460
+ }
461
+ .pill:hover { border-color: var(--brand); }
462
+ .voice-badge { display: inline-flex; align-items: center; gap: 6px; color: var(--err); font-size: 12px; }
463
+
464
+ /* 工作区条 */
465
+ .workspace-bar {
466
+ display: flex;
467
+ align-items: center;
468
+ gap: 8px;
469
+ padding: 8px 0;
470
+ font-size: 13px;
471
+ color: var(--muted);
472
+ }
473
+ .wb-select {
474
+ display: flex;
475
+ align-items: center;
476
+ gap: 6px;
477
+ cursor: pointer;
478
+ padding: 5px 10px;
479
+ border-radius: 8px;
480
+ background: var(--bg);
481
+ border: 1px solid var(--border);
482
+ font-size: 13px;
483
+ color: var(--ink);
484
+ max-width: 320px;
485
+ transition: border-color 0.15s;
486
+ }
487
+ .wb-select:hover { border-color: var(--brand); }
488
+ .wb-icon { font-size: 15px; }
489
+ .wb-label {
490
+ flex: 1;
491
+ white-space: nowrap;
492
+ overflow: hidden;
493
+ text-overflow: ellipsis;
494
+ }
495
+ .wb-arrow { font-size: 10px; color: var(--muted); }
496
+ .wb-perm {
497
+ display: flex;
498
+ align-items: center;
499
+ gap: 5px;
500
+ cursor: pointer;
501
+ padding: 5px 10px;
502
+ border-radius: 8px;
503
+ background: var(--bg);
504
+ border: 1px solid var(--border);
505
+ font-size: 13px;
506
+ color: var(--ink);
507
+ transition: border-color 0.15s;
508
+ }
509
+ .wb-perm:hover { border-color: var(--brand); }
510
+
511
+ /* 通话栏 */
512
+ .call-bar {
513
+ display: flex;
514
+ align-items: center;
515
+ gap: 10px;
516
+ padding: 10px 0;
517
+ border-top: 1px solid var(--border);
518
+ margin-top: 8px;
519
+ }
520
+ .call-hint { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }
521
+ .call-btn {
522
+ border: 1px solid var(--border);
523
+ background: var(--brand-soft);
524
+ color: var(--brand);
525
+ border-radius: 8px;
526
+ padding: 7px 12px;
527
+ cursor: pointer;
528
+ font-size: 13px;
529
+ font-weight: 600;
530
+ }
531
+ .call-btn:hover { filter: brightness(0.97); }
532
+ .call-btn.hangup { background: #fee2e2; color: var(--err); border-color: #fecaca; }
533
+ .call-status { font-size: 12px; color: var(--muted); margin-left: auto; }
534
+
535
+ /* 底部工具栏 */
536
+ .chat-footbar {
537
+ display: flex;
538
+ align-items: center;
539
+ justify-content: space-between;
540
+ padding: 8px 16px;
541
+ background: var(--card);
542
+ border-top: 1px solid var(--border);
543
+ font-size: 13px;
544
+ }
545
+ .model-label { font-size: 13px; color: var(--muted); }
546
+ .model-select {
547
+ padding: 6px 10px;
548
+ border: 1px solid var(--border);
549
+ border-radius: 8px;
550
+ background: var(--card);
551
+ color: var(--ink);
552
+ font-size: 13px;
553
+ }
554
+ .ghost-btn {
555
+ border: 1px solid var(--border);
556
+ background: var(--card);
557
+ color: var(--ink-2);
558
+ border-radius: 8px;
559
+ padding: 6px 9px;
560
+ cursor: pointer;
561
+ font-size: 14px;
562
+ }
563
+ .ghost-btn:hover { background: var(--brand-soft); }
564
+
565
+ /* 新建任务按钮(侧边栏) */
566
+ .new-task-btn {
567
+ border: 1px solid var(--border);
568
+ background: var(--brand);
569
+ color: #fff;
570
+ border-radius: 8px;
571
+ padding: 3px 8px;
572
+ cursor: pointer;
573
+ font-size: 11px;
574
+ font-weight: 600;
575
+ line-height: 1.6;
576
+ }
577
+ .new-task-btn:hover { opacity: 0.88; }
578
+
579
+ /* 新建对话任务弹窗 */
580
+ .nt-form { display: flex; flex-direction: column; gap: 12px; margin-top: 4px; }
581
+ .nt-label { font-size: 12.5px; font-weight: 600; color: var(--ink-2); display: block; margin-bottom: 4px; }
582
+ .nt-form textarea, .nt-form select {
583
+ width: 100%; box-sizing: border-box; padding: 8px 10px; font-size: 13px;
584
+ border: 1px solid var(--border); border-radius: 10px; background: var(--bg);
585
+ color: var(--ink); resize: vertical; font-family: inherit;
586
+ }
587
+ .nt-form select { cursor: pointer; }
588
+ .nt-row { display: flex; gap: 12px; }
589
+ .nt-row > div { flex: 1; }
590
+ .nt-hint { font-size: 12px; color: var(--muted); }
591
+
592
+ /* ========== 右侧面板 ========== */
593
+ .right-panel {
594
+ width: 320px;
595
+ background: var(--card);
596
+ border-left: 1px solid var(--border);
597
+ display: flex;
598
+ flex-direction: column;
599
+ flex-shrink: 0;
600
+ overflow: hidden;
601
+ }
602
+ .right-panel.collapsed { width: 0; border: none; }
603
+ .right-tabs {
604
+ display: flex;
605
+ gap: 4px;
606
+ padding: 12px 12px 0;
607
+ border-bottom: 1px solid var(--line);
608
+ }
609
+ .right-tab {
610
+ padding: 8px 14px;
611
+ font-size: 13px;
612
+ color: var(--muted);
613
+ cursor: pointer;
614
+ border-bottom: 2px solid transparent;
615
+ }
616
+ .right-tab.active { color: var(--brand); border-bottom-color: var(--brand); font-weight: 600; }
617
+ .right-body { flex: 1; overflow-y: auto; padding: 12px; }
618
+ .preview-panel { display: none; }
619
+ .preview-panel.active { display: block; }
620
+ .preview-title { font-size: 13px; font-weight: 600; margin-bottom: 8px; color: var(--ink); }
621
+ .preview-content {
622
+ background: var(--bg);
623
+ border: 1px solid var(--border);
624
+ border-radius: 10px;
625
+ padding: 12px;
626
+ font-size: 12.5px;
627
+ overflow-x: auto;
628
+ }
629
+ .preview-content pre { margin: 0; white-space: pre-wrap; word-break: break-word; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
630
+
631
+ /* 任务详情 */
632
+ .detail-ops {
633
+ display: grid;
634
+ grid-template-columns: 1fr 1fr;
635
+ gap: 8px;
636
+ margin-bottom: 14px;
637
+ }
638
+ .detail-ops .secondary {
639
+ border: 1px solid var(--border);
640
+ background: var(--brand-soft);
641
+ color: var(--brand);
642
+ border-radius: 8px;
643
+ padding: 7px 11px;
644
+ cursor: pointer;
645
+ font-size: 12.5px;
646
+ font-weight: 600;
647
+ }
648
+ .detail-ops .secondary:hover { filter: brightness(0.97); }
649
+ .artifact-list { display: flex; flex-direction: column; gap: 4px; }
650
+ .artifact-file {
651
+ padding: 6px 8px;
652
+ border: 1px solid var(--border);
653
+ border-radius: 6px;
654
+ font-size: 12.5px;
655
+ color: var(--ink-2);
656
+ cursor: pointer;
657
+ background: var(--bg);
658
+ }
659
+ .artifact-file:hover { background: var(--brand-soft); color: var(--brand); }
660
+
661
+ /* ========== 功能页面 ========== */
662
+ .page-view {
663
+ flex: 1;
664
+ display: none;
665
+ flex-direction: column;
666
+ padding: 24px;
667
+ overflow-y: auto;
668
+ }
669
+ .page-view.active { display: flex; }
670
+ .page-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
671
+ .page-head h2 { margin: 0; font-size: 20px; }
672
+ .page-head .sub { color: var(--muted); font-size: 13px; margin-top: 4px; }
673
+ .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; }
674
+ .tile {
675
+ border: 1px solid var(--border);
676
+ border-radius: 12px;
677
+ padding: 16px;
678
+ cursor: pointer;
679
+ background: var(--card);
680
+ transition: all 0.15s;
681
+ }
682
+ .tile:hover { border-color: var(--brand); box-shadow: 0 6px 18px rgba(37,99,235,0.12); }
683
+ .tile .icon { font-size: 24px; }
684
+ .tile .title { font-weight: 600; margin: 8px 0 4px; font-size: 14px; }
685
+ .tile .desc { color: var(--muted); font-size: 12px; line-height: 1.5; }
686
+ .tile .ops { display: flex; gap: 6px; margin-top: 12px; }
687
+ .tile .ops button { padding: 6px 12px; font-size: 12px; flex: 1; }
688
+
689
+ /* ========== 通用样式 ========== */
690
+ .card {
691
+ background: var(--card);
692
+ border: 1px solid var(--border);
693
+ border-radius: 16px;
694
+ padding: 20px;
695
+ box-shadow: 0 8px 24px rgba(31,41,51,0.06);
696
+ }
697
+ label { font-size: 13px; color: var(--muted); display: block; margin-bottom: 6px; }
698
+ input[type="text"], textarea, select {
699
+ width: 100%;
700
+ border: 1px solid var(--border);
701
+ border-radius: 10px;
702
+ padding: 10px 12px;
703
+ font-size: 14px;
704
+ font-family: inherit;
705
+ margin-bottom: 12px;
706
+ background: var(--card);
707
+ color: var(--ink);
708
+ }
709
+ textarea { min-height: 80px; resize: vertical; }
710
+ button {
711
+ border: none;
712
+ background: var(--brand);
713
+ color: #fff;
714
+ border-radius: 10px;
715
+ padding: 9px 16px;
716
+ font-size: 14px;
717
+ cursor: pointer;
718
+ }
719
+ button:hover { filter: brightness(1.05); }
720
+ button.secondary { background: #fff; color: var(--ink); border: 1px solid var(--border); }
721
+ button.ghost { background: transparent; color: var(--ink-2); border: 1px solid var(--border); }
722
+ button.danger { background: var(--err); }
723
+ button:disabled { opacity: 0.5; cursor: not-allowed; }
724
+ .row { display: flex; gap: 8px; }
725
+ .row > * { flex: 1; }
726
+ .muted { color: var(--muted); font-size: 12.5px; }
727
+ .empty { color: var(--muted); font-size: 13px; text-align: center; padding: 18px; }
728
+ .badge { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11px; font-weight: 600; }
729
+ .badge.queued { background: #eef2f7; color: var(--muted); }
730
+ .badge.running { background: var(--brand-soft); color: var(--brand); }
731
+ .badge.done { background: #f0fdf4; color: var(--ok); }
732
+ .badge.failed { background: #fef2f2; color: var(--err); }
733
+
734
+ /* ========== 模态框 ========== */
735
+ .mask {
736
+ position: fixed;
737
+ inset: 0;
738
+ background: rgba(15,23,42,0.45);
739
+ z-index: 60;
740
+ display: none;
741
+ align-items: center;
742
+ justify-content: center;
743
+ }
744
+ .mask.show { display: flex; }
745
+ .modal {
746
+ background: var(--card);
747
+ border-radius: 16px;
748
+ padding: 24px;
749
+ width: 520px;
750
+ max-width: 92vw;
751
+ max-height: 88vh;
752
+ overflow-y: auto;
753
+ box-shadow: 0 20px 60px rgba(0,0,0,0.25);
754
+ }
755
+ .modal h3 { margin: 0 0 6px; font-size: 18px; }
756
+ .modal .hint { color: var(--muted); font-size: 12.5px; margin: 0 0 16px; }
757
+ .modal .footer { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
758
+ .close-x { float: right; cursor: pointer; color: var(--muted); font-size: 18px; }
759
+
760
+ /* ========== 下拉菜单 ========== */
761
+ .dropdown { position: relative; }
762
+ .dropdown-menu {
763
+ position: absolute;
764
+ bottom: 100%;
765
+ left: 0;
766
+ margin-bottom: 6px;
767
+ background: var(--card);
768
+ border: 1px solid var(--border);
769
+ border-radius: 12px;
770
+ box-shadow: 0 10px 30px rgba(0,0,0,0.12);
771
+ min-width: 180px;
772
+ display: none;
773
+ z-index: 55;
774
+ padding: 6px 0;
775
+ }
776
+ .dropdown-menu.show { display: block; }
777
+ .dropdown-item { padding: 9px 14px; font-size: 13px; color: var(--ink-2); cursor: pointer; display: flex; align-items: center; gap: 8px; }
778
+ .dropdown-item:hover { background: var(--bg); }
779
+ .dropdown-divider { height: 1px; background: var(--line); margin: 6px 0; }
780
+
781
+ /* ========== 权限窗口 ========== */
782
+ .perm-section { margin-bottom: 14px; }
783
+ .perm-title { font-size: 13px; font-weight: 600; margin-bottom: 8px; }
784
+ .perm-row { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; font-size: 13px; }
785
+ .perm-row input[type="checkbox"] { width: auto; margin: 0; }
786
+ .perm-row label { margin: 0; color: var(--ink-2); }
787
+
788
+ /* ========== 智能体选择 ========== */
789
+ .agent-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
790
+ .agent-card { border: 1px solid var(--border); border-radius: 12px; padding: 14px; text-align: center; cursor: pointer; }
791
+ .agent-card:hover, .agent-card.selected { border-color: var(--brand); background: var(--brand-soft); }
792
+ .agent-card .icon { font-size: 24px; }
793
+ .agent-card .title { font-size: 13px; font-weight: 600; margin-top: 6px; }
794
+
795
+ /* ========== 大模型管理 ========== */
796
+ .model-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 14px; max-height: 200px; overflow-y: auto; }
797
+ .model-item {
798
+ border: 1px solid var(--border);
799
+ border-radius: 10px;
800
+ padding: 10px 12px;
801
+ display: flex;
802
+ align-items: center;
803
+ gap: 10px;
804
+ background: var(--bg);
805
+ }
806
+ .model-item .mi-name { font-weight: 600; color: var(--ink); font-size: 13px; }
807
+ .model-item .mi-base { font-size: 11px; color: var(--muted); word-break: break-all; }
808
+ .model-item .mi-default { font-size: 11px; color: var(--ok); border: 1px solid #bbf7d0; background: #f0fdf4; border-radius: 6px; padding: 1px 6px; }
809
+ .model-item .mi-actions { margin-left: auto; display: flex; gap: 6px; }
810
+ .model-item .mi-actions button {
811
+ border: 1px solid var(--border);
812
+ background: var(--card);
813
+ color: var(--ink-2);
814
+ border-radius: 6px;
815
+ padding: 4px 8px;
816
+ cursor: pointer;
817
+ font-size: 12px;
818
+ }
819
+ .model-item .mi-actions button:hover { background: var(--brand-soft); }
820
+ .model-item .mi-actions button.danger { color: var(--err); }
821
+ .model-form { border-top: 1px solid var(--line); padding-top: 14px; }
822
+ .form-row { display: flex; gap: 10px; }
823
+ .form-row .form-field { flex: 1; }
824
+ .form-field { margin-bottom: 10px; }
825
+ .form-field label { font-size: 12px; color: var(--muted); display: block; margin-bottom: 5px; }
826
+ .form-field input {
827
+ width: 100%;
828
+ padding: 8px 10px;
829
+ border: 1px solid var(--border);
830
+ border-radius: 8px;
831
+ background: var(--card);
832
+ color: var(--ink);
833
+ font-size: 13px;
834
+ box-sizing: border-box;
835
+ margin: 0;
836
+ }
837
+ .form-actions { display: flex; align-items: center; gap: 10px; margin-top: 4px; }
838
+ .model-form-msg { font-size: 12px; color: var(--ok); }
839
+ .model-form-msg.err { color: var(--err); }
840
+ .primary { background: var(--brand); color: #fff; border: none; border-radius: 8px; padding: 8px 16px; cursor: pointer; font-size: 13px; font-weight: 600; }
841
+ .ghost { background: var(--card); color: var(--ink-2); border: 1px solid var(--border); border-radius: 8px; padding: 8px 16px; cursor: pointer; font-size: 13px; }
842
+ .primary.sm, .ghost.sm { padding: 5px 12px; font-size: 12px; }
843
+
844
+ /* ========== 设置面板 ========== */
845
+ .settings-layout { display: flex; gap: 0; min-height: 360px; }
846
+ .settings-nav { flex: 0 0 130px; border-right: 1px solid var(--line); padding-right: 8px; }
847
+ .settings-cat { padding: 9px 10px; border-radius: 8px; cursor: pointer; font-size: 13px; color: var(--ink-2); }
848
+ .settings-cat:hover { background: var(--bg); }
849
+ .settings-cat.active { background: var(--brand-soft); color: var(--brand); font-weight: 600; }
850
+ .settings-body { flex: 1; padding: 4px 4px 4px 16px; overflow-y: auto; }
851
+ .settings-item {
852
+ display: flex;
853
+ align-items: center;
854
+ gap: 12px;
855
+ padding: 12px;
856
+ border: 1px solid var(--border);
857
+ border-radius: 10px;
858
+ margin-bottom: 10px;
859
+ cursor: pointer;
860
+ background: var(--card);
861
+ }
862
+ .settings-item:hover { background: var(--brand-soft); }
863
+ .settings-item .si-icon { font-size: 18px; }
864
+ .settings-item .si-main { flex: 1; }
865
+ .settings-item .si-title { font-weight: 600; font-size: 13.5px; color: var(--ink); }
866
+ .settings-item .si-desc { font-size: 12px; color: var(--muted); }
867
+ .settings-item .si-arrow { color: var(--muted); }
868
+
869
+ /* ========== 底部上拉菜单 ========== */
870
+ .bottom-sheet-mask {
871
+ position: fixed;
872
+ inset: 0;
873
+ background: rgba(15,23,42,0.35);
874
+ z-index: 70;
875
+ display: none;
876
+ flex-direction: column;
877
+ justify-content: flex-end;
878
+ }
879
+ .bottom-sheet-mask.show { display: flex; }
880
+ .bottom-sheet {
881
+ background: var(--card);
882
+ border-radius: 20px 20px 0 0;
883
+ max-height: 80vh;
884
+ overflow-y: auto;
885
+ padding: 16px 0;
886
+ box-shadow: 0 -10px 40px rgba(0,0,0,0.15);
887
+ transform: translateY(100%);
888
+ transition: transform 0.25s ease;
889
+ }
890
+ .bottom-sheet-mask.show .bottom-sheet { transform: translateY(0); }
891
+ .sheet-header { padding: 8px 20px 16px; display: flex; align-items: center; gap: 12px; border-bottom: 1px solid var(--line); margin-bottom: 6px; }
892
+ .sheet-item { padding: 12px 20px; display: flex; align-items: center; gap: 12px; cursor: pointer; font-size: 14px; color: var(--ink-2); }
893
+ .sheet-item:hover { background: var(--bg); }
894
+ .sheet-item .icon { width: 22px; text-align: center; }
895
+ .sheet-item .right { margin-left: auto; font-size: 12px; color: var(--muted); }
896
+
897
+ /* ========== 工作区 ========== */
898
+ .workspace-box { border: 1px solid var(--border); border-radius: 10px; padding: 10px; background: var(--bg); }
899
+ .workspace-cwd { font-size: 12px; color: var(--ink-2); word-break: break-all; margin-bottom: 8px; }
900
+ .workspace-input { display: flex; gap: 6px; }
901
+ .workspace-input input { flex: 1; margin: 0; font-size: 12px; padding: 6px 8px; }
902
+ .workspace-input button { padding: 6px 10px; font-size: 12px; }
903
+ .file-tree { margin-top: 8px; display: flex; flex-direction: column; gap: 2px; max-height: 220px; overflow-y: auto; }
904
+ .file-item { padding: 5px 8px; border-radius: 6px; cursor: pointer; font-size: 12px; display: flex; align-items: center; gap: 6px; color: var(--ink-2); }
905
+ .file-item:hover { background: var(--brand-soft); }
906
+ .file-item.dir { font-weight: 500; }
907
+ .file-item.drive-switcher { flex-direction: column; align-items: flex-start; padding: 8px; background: var(--bg); border: 1px dashed var(--border); }
908
+ .drive-option { padding: 3px 10px; margin-top: 4px; border-radius: 4px; font-size: 11px; cursor: pointer; color: var(--ink-2); }
909
+ .drive-option:hover { background: var(--brand-soft); color: var(--brand); }
910
+
911
+ /* ========== 文件芯片 ========== */
912
+ .file-chip { display: inline-flex; align-items: center; gap: 6px; background: var(--brand-soft); color: var(--brand); padding: 4px 8px; border-radius: 6px; font-size: 12px; cursor: pointer; }
913
+ .file-chip:hover { opacity: 0.85; }
914
+
915
+ /* ========== 截图遮罩 ========== */
916
+ .crop-overlay { position: fixed; inset: 0; z-index: 9999; background: rgba(0,0,0,0.35); cursor: crosshair; }
917
+ .crop-tip { position: fixed; top: 16px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.7); color: #fff; padding: 6px 14px; border-radius: 20px; font-size: 13px; pointer-events: none; }
918
+ .crop-selection { position: fixed; border: 2px dashed #fff; background: rgba(255,255,255,0.12); box-shadow: 0 0 0 9999px rgba(0,0,0,0.25); pointer-events: none; }
919
+
920
+ /* ========== Toast ========== */
921
+ .toast {
922
+ position: fixed;
923
+ bottom: 24px;
924
+ left: 50%;
925
+ transform: translateX(-50%);
926
+ background: #1f2933;
927
+ color: #fff;
928
+ padding: 10px 18px;
929
+ border-radius: 10px;
930
+ font-size: 13px;
931
+ z-index: 80;
932
+ opacity: 0;
933
+ transition: 0.25s;
934
+ pointer-events: none;
935
+ }
936
+ .toast.show { opacity: 1; }
937
+
938
+ /* ========== 隐藏文件输入 ========== */
939
+ input[type="file"] { display: none; }
940
+
941
+ /* ========== 记忆系统 ========== */
942
+ .stat-card { text-align: center; padding: 16px; }
943
+ .stat-label { font-size: 12px; color: var(--muted); margin-bottom: 8px; }
944
+ .stat-value { font-size: 24px; font-weight: 700; color: var(--brand); }
945
+ .stat-date { font-size: 13px; color: var(--ink-2); font-weight: 400; }
946
+ .memory-content { max-height: 60vh; overflow-y: auto; white-space: pre-wrap; font-family: "SF Mono", Monaco, monospace; font-size: 12.5px; line-height: 1.7; }
947
+ .memory-content h1, .memory-content h2, .memory-content h3 { color: var(--brand); margin-top: 0; }
948
+ .memory-content pre { background: var(--bg); padding: 12px; border-radius: 8px; overflow-x: auto; }
949
+ .memory-content code { background: var(--bg); padding: 2px 6px; border-radius: 4px; font-size: 12px; }
950
+ </style>
951
+ </head>
952
+ <body>
953
+ <div id="loginOverlay">
954
+ <div class="login-card">
955
+ <h2>飞虹 Code 控制台</h2>
956
+ <p>请输入手机号码直接登录</p>
957
+ <label for="loginPhone">手机号码</label>
958
+ <input type="text" id="loginPhone" autocomplete="off" placeholder="如 13800138000" />
959
+ <button id="loginBtn">手机号登录</button>
960
+ <div class="login-status" id="loginStatus"></div>
961
+ </div>
962
+ </div>
963
+
964
+ <div class="app-layout" id="appLayout">
965
+ <!-- 顶部导航栏 -->
966
+ <header class="topbar">
967
+ <div class="topbar-brand">
968
+ <div class="logo">🦞</div>
969
+ <span>飞虹 Code</span>
970
+ </div>
971
+ <nav class="topbar-nav">
972
+ <div class="topbar-nav-item active" data-nav="chat">💬 对话</div>
973
+ <div class="topbar-nav-item" data-nav="automations">⚡ 自动化</div>
974
+ <div class="topbar-nav-item" data-nav="templates">📚 模板库</div>
975
+ <div class="topbar-nav-item" data-nav="market">🧩 插件</div>
976
+ <div class="topbar-nav-item" data-nav="office">📎 办公</div>
977
+ <div class="topbar-nav-item" data-nav="memory">🧠 记忆</div>
978
+ </nav>
979
+ <div class="topbar-spacer"></div>
980
+ <div class="topbar-user" id="topbarUser">
981
+ <div class="topbar-avatar">🦐</div>
982
+ <span class="topbar-phone" id="topbarPhone">未登录</span>
983
+ </div>
984
+ </header>
985
+
986
+ <!-- 主体区 -->
987
+ <div class="main-area">
988
+ <!-- 左侧图标导航 -->
989
+ <aside class="sidebar">
990
+ <!-- 导航菜单 -->
991
+ <div class="sidebar-section">
992
+ <div class="sidebar-section-title">导航</div>
993
+ <div class="sidebar-icon active" data-nav="chat" title="对话任务">
994
+ <span class="icon">💬</span>
995
+ <span class="label">对话任务</span>
996
+ </div>
997
+ <div class="sidebar-icon" data-nav="automations" title="自动化">
998
+ <span class="icon">⚡</span>
999
+ <span class="label">自动化</span>
1000
+ </div>
1001
+ <div class="sidebar-icon" data-nav="templates" title="模板库">
1002
+ <span class="icon">📚</span>
1003
+ <span class="label">模板库</span>
1004
+ </div>
1005
+ <div class="sidebar-icon" data-nav="market" title="插件市场">
1006
+ <span class="icon">🧩</span>
1007
+ <span class="label">插件市场</span>
1008
+ </div>
1009
+ <div class="sidebar-icon" data-nav="office" title="办公助理">
1010
+ <span class="icon">📎</span>
1011
+ <span class="label">办公助理</span>
1012
+ </div>
1013
+ <div class="sidebar-icon" data-nav="memory" title="记忆系统">
1014
+ <span class="icon">🧠</span>
1015
+ <span class="label">记忆系统</span>
1016
+ </div>
1017
+ </div>
1018
+
1019
+ <!-- 任务列表 -->
1020
+ <div class="sidebar-section" id="taskListSection" style="display:none;">
1021
+ <div class="sidebar-section-title" style="display:flex;align-items:center;justify-content:space-between;">
1022
+ <span>任务列表 <span class="badge" id="taskCountBadge" style="margin-left:4px;">0</span></span>
1023
+ <button class="new-task-btn" id="newTaskBtn" title="新建对话任务">+ 新建任务</button>
1024
+ </div>
1025
+ <div id="taskListContainer" style="max-height:300px;overflow-y:auto;">
1026
+ <div class="empty" style="font-size:11px;padding:8px;">暂无任务</div>
1027
+ </div>
1028
+ </div>
1029
+
1030
+ <div class="sidebar-spacer"></div>
1031
+
1032
+ <!-- 用户和设置 -->
1033
+ <div class="sidebar-section">
1034
+ <div class="sidebar-icon" id="sidebarUser" title="用户菜单">
1035
+ <span class="icon">👤</span>
1036
+ <span class="label">用户</span>
1037
+ </div>
1038
+ <div class="sidebar-icon" id="sidebarSettings" title="设置">
1039
+ <span class="icon">⚙️</span>
1040
+ <span class="label">设置</span>
1041
+ </div>
1042
+ </div>
1043
+ </aside>
1044
+
1045
+ <!-- 中间内容区 -->
1046
+ <main class="chat-area" id="chatArea">
1047
+ <!-- 任务上下文头部 -->
1048
+ <div class="task-context-header" id="taskContextHeader">
1049
+ <div class="tch-left">
1050
+ <div class="tch-title" id="tchTitle">未选择任务</div>
1051
+ <div class="tch-sub" id="tchSub">从左侧任务列表选择,或在下方输入指令发起新任务</div>
1052
+ </div>
1053
+ <div class="tch-right">
1054
+ <button class="ghost-btn" id="tchNewTask" title="新建对话任务">+ 新建任务</button>
1055
+ <span class="badge" id="tchStatus">—</span>
1056
+ <span class="tch-time" id="tchTime"></span>
1057
+ <button class="ghost-btn" id="tchRefresh" title="刷新当前任务">⟳</button>
1058
+ </div>
1059
+ </div>
1060
+
1061
+ <!-- 对话视图 -->
1062
+ <div class="conv view active" data-view="chat">
1063
+ <div class="messages" id="messages">
1064
+ <div class="msg sys">点击「+ 新建任务」开启一个新任务,或直接在下方向当前任务发送消息——所有对话都会归属同一个任务运行。</div>
1065
+ </div>
1066
+
1067
+ <div class="input-area">
1068
+ <div class="input-bar">
1069
+ <textarea id="goalInput" placeholder="输入指令,消息将归属当前任务持续对话;点上方「+ 新建任务」可开启新任务(创建 → 推理 → 工具验证 → 闭环全程追踪)"></textarea>
1070
+ <div class="input-toolbar">
1071
+ <div class="dropdown" id="convMenuWrap">
1072
+ <span class="pill" id="convMenuBtn" title="更多操作">+ 菜单</span>
1073
+ <div class="dropdown-menu" id="convMenu">
1074
+ <div class="dropdown-item" data-action="screenshot">📷 截图</div>
1075
+ <div class="dropdown-item" data-action="upload-file">📄 上传文件</div>
1076
+ <div class="dropdown-item" data-action="upload-image">🖼 上传图片</div>
1077
+ <div class="dropdown-item" data-action="direct-mode">🖥 直接操作电脑</div>
1078
+ <div class="dropdown-item" data-action="voice">🎤 语音输入</div>
1079
+ <div class="dropdown-divider"></div>
1080
+ <div class="dropdown-item" data-action="agent-type">🤖 选择智能体类型</div>
1081
+ </div>
1082
+ </div>
1083
+ <button class="pill" id="screenshotBtn" title="截图(框选屏幕区域发给 AI)">📷 截图</button>
1084
+ <span class="pill" id="skillPill" title="当前智能体">🤖 通用助手</span>
1085
+ <span class="pill" id="directModePill" style="display:none;" title="直接操作电脑模式已开启">🖥 直接操作电脑</span>
1086
+ <span id="voiceBadge" class="voice-badge" style="display:none;">● 正在听…</span>
1087
+ <span class="spacer"></span>
1088
+ <button id="sendBtn">▶ 发送</button>
1089
+ </div>
1090
+ </div>
1091
+ <div class="workspace-bar">
1092
+ <button class="wb-select" id="workspacePickBtn" title="选择工作区文件夹">
1093
+ <span class="wb-icon">📂</span>
1094
+ <span class="wb-label" id="workspaceCwdBottom">加载中…</span>
1095
+ <span class="wb-arrow">▾</span>
1096
+ </button>
1097
+ <div class="wb-perm" id="workspacePermBadge" title="点击配置权限">
1098
+ <span>🛡️</span>
1099
+ <span id="workspacePermLabel">默认权限</span>
1100
+ </div>
1101
+ </div>
1102
+ <div class="call-bar">
1103
+ <span class="call-hint">通话</span>
1104
+ <button id="voiceCallBtn" class="call-btn">📞 语音</button>
1105
+ <button id="videoCallBtn" class="call-btn">🎥 视频</button>
1106
+ <button id="hangupCallBtn" class="call-btn hangup" style="display:none;">⛔ 挂断</button>
1107
+ <span id="callStatus" class="call-status"></span>
1108
+ </div>
1109
+ </div>
1110
+ </div>
1111
+
1112
+ <!-- 自动化页面 -->
1113
+ <div class="page-view" data-view="automations">
1114
+ <div class="page-head">
1115
+ <div><h2>⚡ 自动化</h2><div class="sub">把常用目标存成指令,一键发起任务</div></div>
1116
+ <button id="newAutoBtn">+ 新建指令</button>
1117
+ </div>
1118
+ <div class="grid" id="autoGrid"><div class="empty">还没有快捷指令</div></div>
1119
+ </div>
1120
+
1121
+ <!-- 模板库页面 -->
1122
+ <div class="page-view" data-view="templates">
1123
+ <div class="page-head">
1124
+ <div><h2>📚 模板库</h2><div class="sub">点击模板即可填充到输入框</div></div>
1125
+ <button id="newTplBtn">+ 保存当前为模板</button>
1126
+ </div>
1127
+ <div class="section-title">内置模板</div>
1128
+ <div class="grid" id="builtinGrid"></div>
1129
+ <div class="section-title" style="margin-top:20px;">我的模板</div>
1130
+ <div class="grid" id="userTplGrid"><div class="empty">暂无自定义模板</div></div>
1131
+ </div>
1132
+
1133
+ <!-- 插件市场页面 -->
1134
+ <div class="page-view" data-view="market">
1135
+ <div class="page-head">
1136
+ <div><h2>🧩 插件市场</h2><div class="sub">聚合 ClawHub 与 Agent-Foundry 技能生态</div></div>
1137
+ <div class="row" style="flex:0 0 auto; gap:8px;">
1138
+ <input type="text" id="marketSearch" placeholder="搜索…" style="margin:0;width:160px;" />
1139
+ <select id="marketSource" style="margin:0;width:130px;">
1140
+ <option value="all">全部来源</option>
1141
+ <option value="clawhub">ClawHub</option>
1142
+ <option value="agent-foundry">Agent-Foundry</option>
1143
+ </select>
1144
+ <button id="marketRefresh">刷新</button>
1145
+ </div>
1146
+ </div>
1147
+ <div class="grid" id="marketGrid"><div class="empty">加载中…</div></div>
1148
+ <div class="section-title" style="margin-top:20px;">已安装</div>
1149
+ <div class="grid" id="installedGrid"><div class="empty">暂无已安装技能</div></div>
1150
+ </div>
1151
+
1152
+ <!-- 办公助理页面 -->
1153
+ <div class="page-view" data-view="office">
1154
+ <div class="page-head">
1155
+ <div><h2>📎 办公助理</h2><div class="sub">文档处理快捷入口,点击即生成提示词</div></div>
1156
+ </div>
1157
+ <div class="grid" id="officeGrid"><div class="empty">加载中…</div></div>
1158
+ </div>
1159
+
1160
+ <!-- 记忆系统页面 -->
1161
+ <div class="page-view" data-view="memory">
1162
+ <div class="page-head">
1163
+ <div><h2>🧠 记忆系统</h2><div class="sub">短期记忆每日自动整理,沉淀长期知识</div></div>
1164
+ <div class="row">
1165
+ <button id="memRefreshBtn" class="ghost">🔄 刷新</button>
1166
+ <button id="memSummarizeBtn">✨ 立即总结</button>
1167
+ </div>
1168
+ </div>
1169
+
1170
+ <!-- 统计卡片 -->
1171
+ <div class="memory-stats" id="memoryStats" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin-bottom:20px;">
1172
+ <div class="card stat-card">
1173
+ <div class="stat-label">短期记忆天数</div>
1174
+ <div class="stat-value" id="statShortDays">-</div>
1175
+ </div>
1176
+ <div class="card stat-card">
1177
+ <div class="stat-label">长期记忆条目</div>
1178
+ <div class="stat-value" id="statLongNotes">-</div>
1179
+ </div>
1180
+ <div class="card stat-card">
1181
+ <div class="stat-label">上次总结</div>
1182
+ <div class="stat-value stat-date" id="statLastSum">-</div>
1183
+ </div>
1184
+ <div class="card stat-card">
1185
+ <div class="stat-label">下次自动总结</div>
1186
+ <div class="stat-value stat-date" id="statNextSum">-</div>
1187
+ </div>
1188
+ </div>
1189
+
1190
+ <!-- Tab 切换 -->
1191
+ <div class="memory-tabs" style="display:flex;gap:8px;margin-bottom:16px;border-bottom:1px solid var(--border);padding-bottom:8px;">
1192
+ <button class="mem-tab active" data-memtab="short" style="padding:6px 14px;border-radius:8px;border:1px solid var(--border);background:var(--card);cursor:pointer;font-size:13px;">📅 短期记忆</button>
1193
+ <button class="mem-tab" data-memtab="long" style="padding:6px 14px;border-radius:8px;border:1px solid var(--border);background:var(--card);cursor:pointer;font-size:13px;">💾 长期记忆</button>
1194
+ <button class="mem-tab" data-memtab="history" style="padding:6px 14px;border-radius:8px;border:1px solid var(--border);background:var(--card);cursor:pointer;font-size:13px;">📜 总结历史</button>
1195
+ </div>
1196
+
1197
+ <!-- 短期记忆面板 -->
1198
+ <div class="mem-panel active" id="memShortPanel">
1199
+ <div class="row" style="margin-bottom:16px;align-items:center;">
1200
+ <input type="date" id="memDateInput" style="margin:0;width:160px;" />
1201
+ <button id="memLoadDay" style="margin:0;">查看</button>
1202
+ <span class="muted" id="memDateLabel" style="margin-left:8px;"></span>
1203
+ </div>
1204
+ <div class="card memory-content" id="memShortContent">
1205
+ <div class="empty">选择日期查看短期记忆</div>
1206
+ </div>
1207
+ </div>
1208
+
1209
+ <!-- 长期记忆面板 -->
1210
+ <div class="mem-panel" id="memLongPanel" style="display:none;">
1211
+ <div class="card memory-content" id="memLongContent">
1212
+ <div class="empty">加载中…</div>
1213
+ </div>
1214
+ </div>
1215
+
1216
+ <!-- 总结历史面板 -->
1217
+ <div class="mem-panel" id="memHistoryPanel" style="display:none;">
1218
+ <div class="card memory-content" id="memHistoryContent">
1219
+ <div class="empty">暂无总结记录</div>
1220
+ </div>
1221
+ </div>
1222
+ </div>
1223
+
1224
+ <!-- 底部工具栏 -->
1225
+ <div class="chat-footbar" id="chatFootbar">
1226
+ <div><strong>对话任务</strong><span class="muted"> · 提交自然语言需求,云端静默执行并回传结果</span></div>
1227
+ <div class="row" style="flex:0 0 auto; gap:8px; align-items:center;">
1228
+ <label class="model-label" for="modelSelect">大模型</label>
1229
+ <select id="modelSelect" class="model-select" title="切换当前使用的大模型">
1230
+ <option value="">默认(系统路由)</option>
1231
+ </select>
1232
+ <button id="modelSettingsShortcut" class="ghost-btn" title="大模型设置">🧠</button>
1233
+ </div>
1234
+ </div>
1235
+ </main>
1236
+
1237
+ <!-- 右侧面板 -->
1238
+ <aside class="right-panel" id="rightPanel">
1239
+ <div class="right-tabs">
1240
+ <div class="right-tab active" data-tab="detail">📦 任务详情</div>
1241
+ <div class="right-tab" data-tab="preview">👁 产物预览</div>
1242
+ </div>
1243
+ <div class="right-body">
1244
+ <div class="preview-panel active" id="detailPanel">
1245
+ <div id="taskDetail"><div class="muted">从顶部选择一个任务查看详情。</div></div>
1246
+ </div>
1247
+ <div class="preview-panel" id="previewPanel">
1248
+ <div class="preview-title" id="previewTitle">预览</div>
1249
+ <div class="preview-content" id="previewContent"><div class="muted">点击任务或对话中的文件/代码进行预览。</div></div>
1250
+ </div>
1251
+ </div>
1252
+ </aside>
1253
+ </div>
1254
+ </div>
1255
+
1256
+ <!-- 模态框区域 -->
1257
+ <div class="mask" id="autoModal">
1258
+ <div class="modal">
1259
+ <span class="close-x" data-close="autoModal">✕</span>
1260
+ <h3>新建快捷指令</h3>
1261
+ <p class="hint">保存后可在「自动化」页一键发起任务。</p>
1262
+ <label for="autoName">指令名称</label>
1263
+ <input type="text" id="autoName" placeholder="如:每日构建检查" />
1264
+ <label for="autoGoal">目标(提交给模型的需求)</label>
1265
+ <textarea id="autoGoal" placeholder="运行 npm test 并汇总失败用例…"></textarea>
1266
+ <div class="footer">
1267
+ <button class="ghost" data-close="autoModal">取消</button>
1268
+ <button id="autoSaveBtn">保存</button>
1269
+ </div>
1270
+ </div>
1271
+ </div>
1272
+
1273
+ <div class="mask" id="tplModal">
1274
+ <div class="modal">
1275
+ <span class="close-x" data-close="tplModal">✕</span>
1276
+ <h3>保存为模板</h3>
1277
+ <label for="tplTitle">模板标题</label>
1278
+ <input type="text" id="tplTitle" placeholder="如:SQL 优化" />
1279
+ <label for="tplCategory">分类</label>
1280
+ <input type="text" id="tplCategory" placeholder="自定义" />
1281
+ <label for="tplGoal">目标内容</label>
1282
+ <textarea id="tplGoal" placeholder="粘贴要保存的需求文本…"></textarea>
1283
+ <div class="footer">
1284
+ <button class="ghost" data-close="tplModal">取消</button>
1285
+ <button id="tplSaveBtn">保存</button>
1286
+ </div>
1287
+ </div>
1288
+ </div>
1289
+
1290
+ <div class="mask" id="permModal">
1291
+ <div class="modal" style="width:480px;">
1292
+ <span class="close-x" data-close="permModal">✕</span>
1293
+ <h3>🔒 权限处理窗口</h3>
1294
+ <p class="hint">配置工作期间 AI 可读范围及可操作权限。</p>
1295
+ <div class="perm-section">
1296
+ <div class="perm-title">可读范围</div>
1297
+ <div class="perm-row">
1298
+ <input type="radio" name="readScope" id="scopeWorkspace" value="workspace" checked />
1299
+ <label for="scopeWorkspace">仅当前工作区</label>
1300
+ </div>
1301
+ <div class="perm-row">
1302
+ <input type="radio" name="readScope" id="scopeSpecified" value="specified" />
1303
+ <label for="scopeSpecified">指定目录</label>
1304
+ </div>
1305
+ <div class="perm-row">
1306
+ <input type="text" id="permReadPath" placeholder="输入允许的目录路径" style="margin:0;" />
1307
+ </div>
1308
+ <div class="perm-row">
1309
+ <input type="radio" name="readScope" id="scopeAll" value="all" />
1310
+ <label for="scopeAll">全部文件</label>
1311
+ </div>
1312
+ </div>
1313
+ <div class="perm-section">
1314
+ <div class="perm-title">操作权限</div>
1315
+ <div class="perm-row">
1316
+ <input type="checkbox" id="permRead" checked />
1317
+ <label for="permRead">读取文件</label>
1318
+ </div>
1319
+ <div class="perm-row">
1320
+ <input type="checkbox" id="permWrite" />
1321
+ <label for="permWrite">写入文件</label>
1322
+ </div>
1323
+ <div class="perm-row">
1324
+ <input type="checkbox" id="permShell" />
1325
+ <label for="permShell">执行命令</label>
1326
+ </div>
1327
+ <div class="perm-row">
1328
+ <input type="checkbox" id="permNetwork" checked />
1329
+ <label for="permNetwork">访问网络</label>
1330
+ </div>
1331
+ <div class="perm-row">
1332
+ <input type="checkbox" id="permBrowser" />
1333
+ <label for="permBrowser">浏览器控制</label>
1334
+ </div>
1335
+ </div>
1336
+ <div class="footer">
1337
+ <button class="ghost" data-close="permModal">取消</button>
1338
+ <button id="permSaveBtn">保存</button>
1339
+ </div>
1340
+ </div>
1341
+ </div>
1342
+
1343
+ <div class="mask" id="agentModal">
1344
+ <div class="modal" style="width:420px;">
1345
+ <span class="close-x" data-close="agentModal">✕</span>
1346
+ <h3>🤖 选择智能体类型</h3>
1347
+ <p class="hint">不同智能体会自动调整提示词与执行策略。</p>
1348
+ <div class="agent-grid" id="agentGrid"></div>
1349
+ <div class="footer">
1350
+ <button class="ghost" data-close="agentModal">取消</button>
1351
+ </div>
1352
+ </div>
1353
+ </div>
1354
+
1355
+ <div class="crop-overlay" id="cropOverlay" style="display:none;">
1356
+ <div class="crop-tip" id="cropTip">按住鼠标拖拽选择截图区域 · 松开确认 · ESC 取消</div>
1357
+ <div class="crop-selection" id="cropSelection"></div>
1358
+ <video id="cropVideo" style="display:none;"></video>
1359
+ </div>
1360
+
1361
+ <div class="mask" id="folderPickerModal">
1362
+ <div class="modal" style="width:520px;">
1363
+ <span class="close-x" data-close="folderPickerModal">✕</span>
1364
+ <h3>📂 选择工作区文件夹</h3>
1365
+ <div class="fp-path" id="fpPath" style="font-size:12px;color:var(--ink-2);word-break:break-all;padding:8px;background:var(--bg);border-radius:8px;">—</div>
1366
+ <div class="file-tree" id="fpTree" style="max-height:280px;overflow:auto;margin:10px 0;"></div>
1367
+ <div class="footer">
1368
+ <button class="ghost" id="fpCancel" data-close="folderPickerModal">取消</button>
1369
+ <button class="primary" id="fpConfirm">选择此文件夹</button>
1370
+ </div>
1371
+ </div>
1372
+ </div>
1373
+
1374
+ <div class="mask" id="modelModal">
1375
+ <div class="modal" style="width:520px;">
1376
+ <span class="close-x" data-close="modelModal">✕</span>
1377
+ <h3>🧠 大模型设置</h3>
1378
+ <p class="hint">配置自定义大模型,可保存多个并在对话中随时切换。勾选「默认」的项将作为对话默认模型。</p>
1379
+ <div class="model-list" id="modelList"></div>
1380
+ <div class="model-form">
1381
+ <div class="form-row">
1382
+ <div class="form-field">
1383
+ <label>模型名称 *</label>
1384
+ <input type="text" id="modelName" placeholder="如 GPT-4o / 通义千问 / 自建模型" />
1385
+ </div>
1386
+ <div class="form-field">
1387
+ <label>API 地址</label>
1388
+ <input type="text" id="modelApiBase" placeholder="https://api.openai.com/v1" />
1389
+ </div>
1390
+ </div>
1391
+ <div class="form-field">
1392
+ <label>密钥(API Key)</label>
1393
+ <input type="password" id="modelApiKey" placeholder="sk-..." autocomplete="off" />
1394
+ </div>
1395
+ <div class="form-field">
1396
+ <label>推理过程运行内容(中间环节)</label>
1397
+ <textarea id="modelReasoning" rows="3" placeholder="配置该模型在中间推理环节的运行内容,例如:先列出需求拆解,再给出实现方案,最后输出代码。"></textarea>
1398
+ </div>
1399
+ <div class="form-actions">
1400
+ <button id="modelSaveBtn" class="primary">保存配置</button>
1401
+ <button id="modelResetBtn" class="ghost">清空表单</button>
1402
+ <span id="modelFormMsg" class="model-form-msg"></span>
1403
+ </div>
1404
+ </div>
1405
+ <div class="footer">
1406
+ <button class="ghost" data-close="modelModal">关闭</button>
1407
+ </div>
1408
+ </div>
1409
+ </div>
1410
+
1411
+ <div class="mask" id="newTaskModal">
1412
+ <div class="modal" style="width:560px;">
1413
+ <span class="close-x" data-close="newTaskModal">✕</span>
1414
+ <h3>+ 新建对话任务</h3>
1415
+ <p class="hint">创建一个全新任务,中部对话栏的所有消息都将归属该任务运行(支持多轮连续对话)。</p>
1416
+ <div class="nt-form">
1417
+ <label class="nt-label">任务描述 <span style="color:var(--err)">*</span></label>
1418
+ <textarea id="ntGoal" rows="5" placeholder="例如:请读取当前工作区 package.json,告诉我其中的 name 与 version 字段"></textarea>
1419
+ <div class="nt-row">
1420
+ <div>
1421
+ <label class="nt-label">智能体类型</label>
1422
+ <select id="ntAgentType"></select>
1423
+ </div>
1424
+ <div>
1425
+ <label class="nt-label">工作区</label>
1426
+ <select id="ntWorkspaceDir"></select>
1427
+ </div>
1428
+ </div>
1429
+ <div class="nt-hint">工作区默认使用底部当前所选目录;若选择「默认」,则沿用服务器工作区。</div>
1430
+ </div>
1431
+ <div class="footer">
1432
+ <button class="ghost" data-close="newTaskModal">取消</button>
1433
+ <button class="primary" id="ntCreateBtn">创建任务</button>
1434
+ </div>
1435
+ </div>
1436
+ </div>
1437
+
1438
+ <div class="mask" id="settingsModal">
1439
+ <div class="modal" style="width:560px;">
1440
+ <span class="close-x" data-close="settingsModal">✕</span>
1441
+ <h3>⚙️ 设置</h3>
1442
+ <div class="settings-layout">
1443
+ <div class="settings-nav">
1444
+ <div class="settings-cat active" data-cat="general">通用</div>
1445
+ <div class="settings-cat" data-cat="internal">栏目配置</div>
1446
+ </div>
1447
+ <div class="settings-body" id="settingsBody">
1448
+ <div class="settings-group" data-group="general">
1449
+ <div class="settings-item" data-action="perm">
1450
+ <span class="si-icon">🔒</span>
1451
+ <div class="si-main"><div class="si-title">权限处理</div><div class="si-desc">配置工作期间可读范围与操作权限</div></div>
1452
+ <span class="si-arrow">›</span>
1453
+ </div>
1454
+ <div class="settings-item" data-action="theme">
1455
+ <span class="si-icon">🎨</span>
1456
+ <div class="si-main"><div class="si-title">外观</div><div class="si-desc">切换浅色 / 深色主题</div></div>
1457
+ <span class="si-arrow">›</span>
1458
+ </div>
1459
+ </div>
1460
+ <div class="settings-group" data-group="internal" style="display:none;">
1461
+ <div class="settings-item" data-action="model-custom" id="modelCustomItem">
1462
+ <span class="si-icon">🧠</span>
1463
+ <div class="si-main">
1464
+ <div class="si-title">大模型自定义设置</div>
1465
+ <div class="si-desc">自由添加并配置自定义大模型,提供大模型选择列表,可设为默认</div>
1466
+ </div>
1467
+ <span class="si-arrow">›</span>
1468
+ </div>
1469
+ <div class="model-manage" id="modelManage" style="display:none;">
1470
+ <div class="mm-list" id="modelListInline"></div>
1471
+ <div class="mm-add">
1472
+ <div class="mm-hint">添加新大模型(名称、API 地址、密钥、推理内容):</div>
1473
+ <div class="model-form-inline">
1474
+ <input type="text" id="imName" placeholder="模型名称 *" />
1475
+ <input type="text" id="imBase" placeholder="API 地址" />
1476
+ <input type="password" id="imKey" placeholder="密钥 API Key" autocomplete="off" />
1477
+ <textarea id="imReasoning" rows="2" placeholder="推理过程运行内容(中间环节,可选)"></textarea>
1478
+ <div class="mm-actions">
1479
+ <button id="imSave" class="primary sm">保存配置</button>
1480
+ <button id="imReset" class="ghost sm">清空</button>
1481
+ <span id="imMsg" class="model-form-msg"></span>
1482
+ </div>
1483
+ </div>
1484
+ </div>
1485
+ <div class="mm-foot">
1486
+ <button class="ghost sm" id="modelManageClose">收起</button>
1487
+ </div>
1488
+ </div>
1489
+ </div>
1490
+ </div>
1491
+ </div>
1492
+ <div class="footer">
1493
+ <button class="ghost" data-close="settingsModal">关闭</button>
1494
+ </div>
1495
+ </div>
1496
+ </div>
1497
+
1498
+ <div class="bottom-sheet-mask" id="userMenuMask">
1499
+ <div class="bottom-sheet" id="userMenu">
1500
+ <div class="sheet-header">
1501
+ <div class="topbar-avatar" style="width:40px;height:40px;border-radius:50%;background:var(--brand-soft);display:flex;align-items:center;justify-content:center;font-size:20px;">🦐</div>
1502
+ <div>
1503
+ <div class="topbar-phone" id="sheetPhone">未登录</div>
1504
+ <div class="muted">免费版</div>
1505
+ </div>
1506
+ </div>
1507
+ <div class="sheet-item" data-action="version"><span class="icon">🎖</span>体验版<span class="right">当前</span></div>
1508
+ <div class="sheet-item" data-action="buddy"><span class="icon">⛽</span>Buddy 加油站</div>
1509
+ <div class="sheet-item" data-action="invite"><span class="icon">👥</span>去邀约<span class="right">最高得会员</span></div>
1510
+ <div class="sheet-item" data-action="points"><span class="icon">💎</span>积分余额</div>
1511
+ <div class="sheet-item" data-action="growth"><span class="icon">🌱</span>成长计划<span class="right">连登有奖</span></div>
1512
+ <div class="sheet-item" data-action="settings"><span class="icon">⚙️</span>设置</div>
1513
+ <div class="sheet-item" data-action="theme"><span class="icon">🎨</span>外观</div>
1514
+ <div class="sheet-item" data-action="help"><span class="icon">❓</span>帮助与反馈</div>
1515
+ <div class="sheet-item" data-action="update"><span class="icon">🔄</span>检查更新</div>
1516
+ <div class="sheet-item" data-action="logout"><span class="icon">↩️</span>退出登录</div>
1517
+ </div>
1518
+ </div>
1519
+
1520
+ <input type="file" id="fileInput" />
1521
+ <input type="file" id="imageInput" accept="image/*" />
1522
+ <div class="toast" id="toast"></div>
1523
+
1524
+ <script>
1525
+ const AGENT_TYPES = {
1526
+ general: { label: '通用助手', icon: '🤖' },
1527
+ 'fix-code': { label: '修复代码', icon: '🐛' },
1528
+ 'write-code': { label: '编写代码', icon: '✍️' },
1529
+ 'exec-command': { label: '执行命令', icon: '⚡' },
1530
+ };
1531
+
1532
+ const state = {
1533
+ token: '',
1534
+ phone: '',
1535
+ tasks: [],
1536
+ currentTaskId: null,
1537
+ submittedTaskIds: new Set(),
1538
+ completedTaskIds: new Set(),
1539
+ pinnedTasks: new Set(), // 置顶任务ID集合
1540
+ market: [],
1541
+ installed: new Set(),
1542
+ agentType: 'general',
1543
+ directMode: false,
1544
+ permissions: {
1545
+ readScope: 'workspace',
1546
+ readPath: '',
1547
+ allowRead: true,
1548
+ allowWrite: false,
1549
+ allowShell: false,
1550
+ allowNetwork: true,
1551
+ allowBrowser: false,
1552
+ },
1553
+ workspaceDir: '',
1554
+ rightTab: 'detail',
1555
+ models: [],
1556
+ defaultModelId: null,
1557
+ modelId: '',
1558
+ };
1559
+
1560
+ (function initSession() {
1561
+ const urlToken = new URLSearchParams(location.search).get('token');
1562
+ if (urlToken) localStorage.setItem('fhcode.token', urlToken);
1563
+ const savedModel = localStorage.getItem('fhcode.model');
1564
+ if (savedModel !== null) state.modelId = savedModel;
1565
+ state.token = localStorage.getItem('fhcode.token') || '';
1566
+ state.phone = localStorage.getItem('fhcode.phone') || '';
1567
+ try {
1568
+ const p = JSON.parse(localStorage.getItem('fhcode.permissions') || '{}');
1569
+ if (p) Object.assign(state.permissions, p);
1570
+ } catch {}
1571
+ })();
1572
+
1573
+ function authHeaders() {
1574
+ return state.token ? { 'Authorization': 'Bearer ' + state.token, 'Content-Type': 'application/json' } : { 'Content-Type': 'application/json' };
1575
+ }
1576
+ async function api(path, method = 'GET', body) {
1577
+ const res = await fetch(path, { method, headers: authHeaders(), body: body ? JSON.stringify(body) : undefined });
1578
+ if (!res.ok) {
1579
+ const d = await res.json().catch(() => ({}));
1580
+ // 401 时自动清除无效 token,跳转到登录
1581
+ if (res.status === 401 && d.error === 'unauthorized') {
1582
+ console.warn('[api] token 已失效,清除并跳转登录');
1583
+ localStorage.removeItem('fhcode.token');
1584
+ state.token = '';
1585
+ document.getElementById('loginOverlay').style.display = 'flex';
1586
+ throw new Error('登录已过期,请重新登录');
1587
+ }
1588
+ throw new Error(d.error || ('HTTP ' + res.status));
1589
+ }
1590
+ return res.json();
1591
+ }
1592
+ function escapeHtml(s) { return String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])); }
1593
+ function fmtTime(iso) { try { return new Date(iso).toLocaleString(); } catch { return iso || ''; } }
1594
+ function toast(msg) {
1595
+ const el = document.getElementById('toast');
1596
+ el.textContent = msg; el.classList.add('show');
1597
+ clearTimeout(el._t); el._t = setTimeout(() => el.classList.remove('show'), 2200);
1598
+ }
1599
+ function openModal(id) { document.getElementById(id).classList.add('show'); }
1600
+ function closeModal(id) { document.getElementById(id).classList.remove('show'); }
1601
+ // 消息气泡通用操作:复制 / 编辑 / 分享 / 创建文档
1602
+ function bubbleActions(e, content) {
1603
+ e.stopPropagation();
1604
+ const menu = document.getElementById('bubbleMenu');
1605
+ if (menu) menu.remove();
1606
+ const m = document.createElement('div');
1607
+ m.id = 'bubbleMenu';
1608
+ m.style.cssText = 'position:fixed;z-index:10001;background:var(--card);border:1px solid var(--border);border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,0.18);padding:4px 0;min-width:140px;font-size:13px;';
1609
+ const items = [
1610
+ { label: '📋 复制', action: 'copy' },
1611
+ { label: '✏️ 编辑', action: 'edit' },
1612
+ { label: '🔗 分享链接', action: 'share' },
1613
+ { label: '📄 创建文档', action: 'doc' },
1614
+ ];
1615
+ items.forEach((it) => {
1616
+ const el = document.createElement('div');
1617
+ el.className = 'dropdown-item';
1618
+ el.textContent = it.label;
1619
+ el.style.cssText = 'padding:7px 16px;cursor:pointer;';
1620
+ el.addEventListener('mouseenter', () => { el.style.background = 'var(--brand-soft)'; });
1621
+ el.addEventListener('mouseleave', () => { el.style.background = ''; });
1622
+ el.addEventListener('click', () => {
1623
+ m.remove();
1624
+ handleBubbleAction(it.action, content);
1625
+ });
1626
+ m.appendChild(el);
1627
+ });
1628
+ document.body.appendChild(m);
1629
+ const rect = e.target.getBoundingClientRect();
1630
+ m.style.right = (window.innerWidth - rect.right + 4) + 'px';
1631
+ m.style.top = rect.top + 'px';
1632
+ }
1633
+ async function handleBubbleAction(action, content) {
1634
+ if (action === 'copy') {
1635
+ try { await navigator.clipboard.writeText(content); toast('已复制到剪贴板'); }
1636
+ catch { document.execCommand('copy'); toast('已复制'); }
1637
+ } else if (action === 'edit') {
1638
+ const edited = prompt('编辑内容:', content);
1639
+ if (edited != null && edited.trim()) {
1640
+ toast('已更新(本地预览)');
1641
+ }
1642
+ } else if (action === 'share') {
1643
+ const payload = btoa(unescape(encodeURIComponent(content.slice(0, 2000))));
1644
+ const url = location.origin + location.pathname + '#share/' + payload;
1645
+ try { await navigator.clipboard.writeText(url); toast('分享链接已复制'); }
1646
+ catch { prompt('复制以下链接分享给他人:', url); }
1647
+ } else if (action === 'doc') {
1648
+ const title = prompt('文档标题(默认: fhcode-回复):', 'fhcode-回复-' + Date.now());
1649
+ if (!title) return;
1650
+ const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
1651
+ const a = document.createElement('a');
1652
+ a.href = URL.createObjectURL(blob);
1653
+ a.download = title.replace(/[^\u4e00-\u9fa5a-zA-Z0-9_\-]/g, '_') + '.md';
1654
+ a.click();
1655
+ URL.revokeObjectURL(a.href);
1656
+ toast('文档已下载');
1657
+ }
1658
+ }
1659
+
1660
+ /* ========== 登录 ========== */
1661
+ async function doLogin() {
1662
+ const input = document.getElementById('loginPhone');
1663
+ const status = document.getElementById('loginStatus');
1664
+ const btn = document.getElementById('loginBtn');
1665
+ const phone = (input.value || '').trim();
1666
+ if (!/^\d{6,20}$/.test(phone)) { status.textContent = '请输入有效的手机号码'; status.classList.add('err'); return; }
1667
+ btn.disabled = true;
1668
+ status.textContent = '登录中…';
1669
+ status.classList.remove('err');
1670
+ try {
1671
+ const d = await api('/api/auth/login', 'POST', { phone });
1672
+ if (!d || !d.token) throw new Error('服务端未返回令牌');
1673
+ state.token = d.token;
1674
+ state.phone = d.phone || phone;
1675
+ localStorage.setItem('fhcode.token', state.token);
1676
+ localStorage.setItem('fhcode.phone', state.phone);
1677
+ status.classList.remove('err');
1678
+ document.getElementById('loginOverlay').style.display = 'none';
1679
+ document.getElementById('appLayout').classList.add('show');
1680
+ await afterLogin();
1681
+ } catch (e) {
1682
+ status.textContent = '登录失败:' + e.message;
1683
+ status.classList.add('err');
1684
+ } finally {
1685
+ btn.disabled = false;
1686
+ }
1687
+ }
1688
+ document.getElementById('loginBtn').addEventListener('click', doLogin);
1689
+ document.getElementById('loginPhone').addEventListener('keydown', (e) => { if (e.key === 'Enter') doLogin(); });
1690
+
1691
+ async function afterLogin() {
1692
+ updateUserBar();
1693
+ await Promise.allSettled([loadTasks(), loadAutomations(), loadTemplates(), loadMarket(), loadOffice(), loadWorkspace(), loadModels(), loadMemoryStats()]);
1694
+ startRefresh();
1695
+ }
1696
+
1697
+ function updateUserBar() {
1698
+ document.getElementById('topbarPhone').textContent = state.phone || '未登录';
1699
+ document.getElementById('sheetPhone').textContent = state.phone || '未登录';
1700
+ const a = AGENT_TYPES[state.agentType];
1701
+ document.getElementById('skillPill').textContent = (a?.icon || '🤖') + ' ' + (a?.label || '通用助手');
1702
+ }
1703
+
1704
+ /* ========== 导航切换 ========== */
1705
+ function switchView(nav) {
1706
+ document.querySelectorAll('.topbar-nav-item').forEach((n) => n.classList.toggle('active', n.getAttribute('data-nav') === nav));
1707
+ document.querySelectorAll('.sidebar-icon').forEach((n) => n.classList.toggle('active', n.getAttribute('data-nav') === nav));
1708
+ document.querySelectorAll('.view, .page-view').forEach((v) => v.classList.toggle('active', v.getAttribute('data-view') === nav));
1709
+ // 切换时显示/隐藏底部工具栏(任务上下文头部始终可见)
1710
+ const isChat = nav === 'chat';
1711
+ document.getElementById('chatFootbar').style.display = isChat ? 'flex' : 'none';
1712
+ }
1713
+
1714
+ document.querySelectorAll('.topbar-nav-item').forEach((el) => {
1715
+ el.addEventListener('click', () => switchView(el.getAttribute('data-nav')));
1716
+ });
1717
+ document.querySelectorAll('.sidebar-icon[data-nav]').forEach((el) => {
1718
+ el.addEventListener('click', () => switchView(el.getAttribute('data-nav')));
1719
+ });
1720
+
1721
+ /* ========== 用户菜单 ========== */
1722
+ const userMenuMask = document.getElementById('userMenuMask');
1723
+ document.getElementById('topbarUser').addEventListener('click', () => userMenuMask.classList.add('show'));
1724
+ document.getElementById('sidebarUser').addEventListener('click', () => userMenuMask.classList.add('show'));
1725
+ userMenuMask.addEventListener('click', (e) => { if (e.target === userMenuMask) userMenuMask.classList.remove('show'); });
1726
+ document.querySelectorAll('#userMenu .sheet-item').forEach((el) => {
1727
+ el.addEventListener('click', () => {
1728
+ const action = el.getAttribute('data-action');
1729
+ userMenuMask.classList.remove('show');
1730
+ switch (action) {
1731
+ case 'settings': openModal('settingsModal'); break;
1732
+ case 'models': openModels(); break;
1733
+ case 'theme': toggleTheme(); break;
1734
+ case 'help': openBrowser('https://github.com/wch887292/feihong-code/issues'); break;
1735
+ case 'buddy': openBrowser('https://www.klai.top'); break;
1736
+ case 'invite': toast('邀请链接已复制(演示)'); break;
1737
+ case 'points': toast('积分余额:0(演示)'); break;
1738
+ case 'growth': toast('连续登录 1 天(演示)'); break;
1739
+ case 'version': toast('当前为体验版'); break;
1740
+ case 'update': checkUpdate(); break;
1741
+ case 'logout':
1742
+ localStorage.removeItem('fhcode.token');
1743
+ localStorage.removeItem('fhcode.phone');
1744
+ location.reload();
1745
+ break;
1746
+ }
1747
+ });
1748
+ });
1749
+
1750
+ document.getElementById('sidebarSettings').addEventListener('click', () => {
1751
+ userMenuMask.classList.remove('show');
1752
+ openModal('settingsModal');
1753
+ });
1754
+
1755
+ function openBrowser(url) {
1756
+ api('/api/open/browser', 'POST', { url }).then(() => toast('已在系统浏览器打开')).catch((e) => toast('打开失败:' + e.message));
1757
+ }
1758
+ async function checkUpdate() {
1759
+ try {
1760
+ const d = await api('/api/health');
1761
+ toast('当前版本 ' + d.version + ',已是最新(演示)');
1762
+ } catch (e) { toast('检查失败:' + e.message); }
1763
+ }
1764
+ function toggleTheme() {
1765
+ document.documentElement.classList.toggle('dark');
1766
+ toast('已切换外观');
1767
+ }
1768
+
1769
+ /* ========== 任务列表与详情 ========== */
1770
+ function statusBadge(s) { return '<span class="badge ' + s + '">' + s + '</span>'; }
1771
+ async function loadTasks() {
1772
+ if (!state.token) return;
1773
+ try {
1774
+ const d = await api('/api/tasks');
1775
+ state.tasks = d.tasks || [];
1776
+ renderSidebarTaskList();
1777
+ // 首次进入自动选中最新任务(列表已按创建时间倒序)
1778
+ if (!state.currentTaskId && state.tasks.length) {
1779
+ state.currentTaskId = state.tasks[0].id;
1780
+ renderSidebarTaskList();
1781
+ }
1782
+ if (state.currentTaskId) {
1783
+ await refreshCurrentThread();
1784
+ } else {
1785
+ renderTaskThread(null);
1786
+ }
1787
+ }
1788
+ catch (e) { console.warn('加载任务失败', e); }
1789
+ }
1790
+ async function selectTask(id) {
1791
+ state.currentTaskId = id;
1792
+ renderSidebarTaskList();
1793
+ renderTaskDetail(id);
1794
+ await refreshCurrentThread();
1795
+ switchRightTab('detail');
1796
+ }
1797
+ function renderSidebarTaskList() {
1798
+ const container = document.getElementById('taskListContainer');
1799
+ const section = document.getElementById('taskListSection');
1800
+ const badge = document.getElementById('taskCountBadge');
1801
+ if (!container || !section) return;
1802
+
1803
+ // 置顶任务排在前面,其余按时间倒序
1804
+ const sortedTasks = [...state.tasks].sort((a, b) => {
1805
+ if (state.pinnedTasks.has(a.id) && !state.pinnedTasks.has(b.id)) return -1;
1806
+ if (!state.pinnedTasks.has(a.id) && state.pinnedTasks.has(b.id)) return 1;
1807
+ return new Date(b.createdAt) - new Date(a.createdAt);
1808
+ });
1809
+
1810
+ const activeTasks = sortedTasks.filter(t => t.status === 'queued' || t.status === 'running' || t.status === 'done' || t.status === 'failed');
1811
+
1812
+ if (activeTasks.length === 0) {
1813
+ section.style.display = 'none';
1814
+ return;
1815
+ }
1816
+
1817
+ section.style.display = 'block';
1818
+ badge.textContent = activeTasks.length;
1819
+
1820
+ let html = '';
1821
+ activeTasks.slice(0, 15).forEach(t => {
1822
+ const icon = t.status === 'running' ? '⏳' : t.status === 'queued' ? '📥' : t.error ? '❌' : '✅';
1823
+ const title = t.goal.slice(0, 18) + (t.goal.length > 18 ? '…' : '');
1824
+ const isPinned = state.pinnedTasks.has(t.id);
1825
+ const activeClass = state.currentTaskId === t.id ? 'active' : '';
1826
+ html += `<div class="task-item ${activeClass}" data-task-id="${t.id}" data-pinned="${isPinned}">
1827
+ <span class="task-icon">${icon}</span>
1828
+ <span class="task-title">${title}</span>
1829
+ ${isPinned ? '<span class="task-pinned">📌</span>' : ''}
1830
+ <div class="task-actions">
1831
+ <button class="pin-btn" data-id="${t.id}" data-pinned="${isPinned}" title="${isPinned ? '取消置顶' : '置顶'}">${isPinned ? '📌' : '📍'}</button>
1832
+ ${t.status === 'done' || t.status === 'failed' ? '<button class="delete-btn" data-id="' + t.id + '" title="删除任务">🗑️</button>' : ''}
1833
+ </div>
1834
+ </div>`;
1835
+ });
1836
+
1837
+ container.innerHTML = html;
1838
+
1839
+ // 绑定点击事件
1840
+ container.querySelectorAll('.task-item[data-task-id]').forEach(el => {
1841
+ el.addEventListener('click', (e) => {
1842
+ // 如果是点击pin按钮,不切换任务
1843
+ if (e.target.classList.contains('pin-btn')) return;
1844
+ const id = el.getAttribute('data-task-id');
1845
+ selectTask(id);
1846
+ });
1847
+
1848
+ // 右键菜单 - 置顶/取消置顶
1849
+ el.addEventListener('contextmenu', (e) => {
1850
+ e.preventDefault();
1851
+ const id = el.getAttribute('data-task-id');
1852
+ togglePinTask(id);
1853
+ });
1854
+ });
1855
+
1856
+ // 绑定pin按钮
1857
+ container.querySelectorAll('.pin-btn').forEach(btn => {
1858
+ btn.addEventListener('click', (e) => {
1859
+ e.stopPropagation();
1860
+ const id = btn.getAttribute('data-id');
1861
+ togglePinTask(id);
1862
+ });
1863
+ });
1864
+
1865
+ // 绑定删除按钮
1866
+ container.querySelectorAll('.delete-btn').forEach(btn => {
1867
+ btn.addEventListener('click', (e) => {
1868
+ e.stopPropagation();
1869
+ const id = btn.getAttribute('data-id');
1870
+ deleteTask(id);
1871
+ });
1872
+ });
1873
+ }
1874
+
1875
+ // 切换置顶状态
1876
+ function togglePinTask(taskId) {
1877
+ if (state.pinnedTasks.has(taskId)) {
1878
+ state.pinnedTasks.delete(taskId);
1879
+ toast('已取消置顶');
1880
+ } else {
1881
+ state.pinnedTasks.add(taskId);
1882
+ toast('已置顶');
1883
+ }
1884
+ renderSidebarTaskList();
1885
+ }
1886
+
1887
+ // 删除任务
1888
+ async function deleteTask(taskId) {
1889
+ if (!confirm('确定要删除这个任务吗?')) return;
1890
+ try {
1891
+ await api('/api/tasks/' + taskId, 'DELETE');
1892
+ toast('任务已删除');
1893
+ renderSidebarTaskList();
1894
+ } catch (e) {
1895
+ toast('删除失败:' + e.message);
1896
+ }
1897
+ }
1898
+ function renderTaskDetail(id) {
1899
+ const t = state.tasks.find((x) => x.id === id);
1900
+ const box = document.getElementById('taskDetail');
1901
+ if (!t) { box.innerHTML = '<div class="muted">任务不存在。</div>'; return; }
1902
+ const r = t.result;
1903
+ let ops = '';
1904
+ if (t.workspaceDir) {
1905
+ ops = '<div class="detail-ops">' +
1906
+ '<button class="secondary" data-op="browser">🌐 打开浏览器</button>' +
1907
+ '<button class="secondary" data-op="folder" data-path="' + escapeHtml(t.workspaceDir) + '">📂 打开资源管理器</button>' +
1908
+ '<button class="secondary" data-op="artifacts" data-path="' + escapeHtml(t.workspaceDir) + '">📑 查看产物文件</button>' +
1909
+ '</div>';
1910
+ }
1911
+ const rows = [
1912
+ ops,
1913
+ '<div><b>目标</b><br>' + escapeHtml(t.goal) + '</div>',
1914
+ '<div><b>状态</b> ' + statusBadge(t.status) + '</div>',
1915
+ t.agentType ? '<div><b>智能体</b> ' + escapeHtml(AGENT_TYPES[t.agentType]?.label || t.agentType) + '</div>' : '',
1916
+ r ? '<div><b>迭代</b> ' + r.iterations + ' · <b>成本</b> $' + r.costUsd.toFixed(6) + '</div>' : '',
1917
+ r && r.logFile ? '<div><b>日志</b> <code>' + escapeHtml(r.logFile) + '</code></div>' : '',
1918
+ r && r.finalAnswer ? '<div><b>结果</b><br><pre style="white-space:pre-wrap;word-break:break-word;">' + escapeHtml(r.finalAnswer) + '</pre></div>' : '',
1919
+ t.error ? '<div style="color:var(--err)"><b>错误</b><br>' + escapeHtml(t.error) + '</div>' : '',
1920
+ ].filter(Boolean).join('<hr style="border:none;border-top:1px solid var(--line);margin:10px 0;">');
1921
+ box.innerHTML = rows || '<div class="muted">无详情</div>';
1922
+ box.querySelectorAll('[data-op]').forEach((b) => b.addEventListener('click', () => handleDetailOp(t, b.getAttribute('data-op'), b.getAttribute('data-path'))));
1923
+ }
1924
+ async function handleDetailOp(task, op, path) {
1925
+ if (op === 'folder') {
1926
+ try { await api('/api/open/folder', 'POST', { path: path || task.workspaceDir }); toast('已打开资源管理器'); }
1927
+ catch (e) { toast('打开失败:' + e.message); }
1928
+ } else if (op === 'browser') {
1929
+ const url = extractUrl(task.result?.finalAnswer) || 'https://www.klai.top';
1930
+ openBrowser(url);
1931
+ } else if (op === 'artifacts') {
1932
+ await showArtifacts(path || task.workspaceDir);
1933
+ } else if (op === 'preview' || op === 'code') {
1934
+ const text = task.result?.finalAnswer || '';
1935
+ showPreview('任务产物', linkifyArtifacts(text), 'text');
1936
+ switchRightTab('preview');
1937
+ }
1938
+ }
1939
+ /* ========== 任务思维链路渲染 ========== */
1940
+ // 拉取当前选中任务的完整详情(含思维链路步骤)并渲染
1941
+ async function refreshCurrentThread() {
1942
+ if (!state.currentTaskId) { renderTaskThread(null); return; }
1943
+ try {
1944
+ const d = await api('/api/tasks/' + state.currentTaskId);
1945
+ const t = d.task;
1946
+ const idx = state.tasks.findIndex((x) => x.id === t.id);
1947
+ if (idx >= 0) state.tasks[idx] = t; else state.tasks.push(t);
1948
+ renderTaskThread(t);
1949
+ } catch (e) { console.warn('刷新当前任务线程失败', e); renderTaskThread(null); }
1950
+ }
1951
+
1952
+ function renderTaskHeader(task) {
1953
+ const titleEl = document.getElementById('tchTitle');
1954
+ const statusEl = document.getElementById('tchStatus');
1955
+ const timeEl = document.getElementById('tchTime');
1956
+ const subEl = document.getElementById('tchSub');
1957
+ if (!task) {
1958
+ titleEl.textContent = '未选择任务';
1959
+ statusEl.className = 'badge'; statusEl.textContent = '—';
1960
+ timeEl.textContent = '';
1961
+ subEl.textContent = '从左侧任务列表选择,或在下方输入指令发起新任务';
1962
+ return;
1963
+ }
1964
+ titleEl.textContent = task.goal.length > 60 ? task.goal.slice(0, 60) + '…' : task.goal;
1965
+ statusEl.className = 'badge ' + task.status;
1966
+ statusEl.textContent = task.status;
1967
+ timeEl.textContent = '创建于 ' + fmtTime(task.createdAt);
1968
+ subEl.textContent = '思维链路全程追踪 · 共 ' + (task.steps ? task.steps.length : 0) + ' 个步骤';
1969
+ }
1970
+
1971
+ // 将单个任务的思维链路渲染为对话流:创建时间 → 逐步推理 → 工具验证 → 最终回复
1972
+ function renderTaskThread(task) {
1973
+ const box = document.getElementById('messages');
1974
+ const nearBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 80;
1975
+ renderTaskHeader(task);
1976
+ if (!task) {
1977
+ box.innerHTML = '<div class="msg sys">从左侧任务列表选择任务,或在下方输入指令发起新任务。中部仅展示当前选中任务的完整思维链路(创建时间、逐步推理、工具验证、最终回复)。</div>';
1978
+ return;
1979
+ }
1980
+ const steps = task.steps || [];
1981
+ let html = '';
1982
+ // 对话流:该任务下所有多轮对话气泡(user 提问 / assistant 回复)
1983
+ const conv = Array.isArray(task.conversation) ? task.conversation : [];
1984
+ const convVisible = conv.filter((m) => m && (m.role === 'user' || m.role === 'assistant') && (m.content || '').trim());
1985
+ if (convVisible.length) {
1986
+ html += '<div class="conv-divider">🗨 对话流</div>';
1987
+ for (const m of convVisible) {
1988
+ if (m.role === 'user') html += '<div class="msg user">' + linkifyArtifacts(m.content) + '</div>';
1989
+ else html += '<div class="msg assistant">' + linkifyArtifacts(m.content) + '</div>';
1990
+ }
1991
+ }
1992
+ html += '<div class="conv-divider">🧠 思维链路</div>';
1993
+ html += '<div class="step created">📌 <b>任务创建</b> · ' + escapeHtml(fmtTime(task.createdAt)) + ' · 状态 <b>' + escapeHtml(task.status) + '</b></div>';
1994
+ for (const step of steps) {
1995
+ const d = step.data || {};
1996
+ if (step.type === 'model.response') {
1997
+ const content = (d.content || '').toString();
1998
+ const toolCalls = Array.isArray(d.toolCalls) ? d.toolCalls : [];
1999
+ let inner = '<div class="reasoning-head">🧠 模型推理 · ' + escapeHtml(d.model || '') + '</div>';
2000
+ if (content) inner += escapeHtml(content);
2001
+ if (toolCalls.length) inner += '<div class="muted" style="margin-top:6px;">↳ 计划调用工具:' + toolCalls.map((t) => '<code>' + escapeHtml(t) + '</code>').join('、') + '</div>';
2002
+ html += '<div class="step reasoning">' + inner + '</div>';
2003
+ } else if (step.type === 'tool.call') {
2004
+ const args = d.args ? JSON.stringify(d.args, null, 2) : '';
2005
+ html += '<div class="step toolcall"><div class="tc-name">🔧 调用 ' + escapeHtml(d.name || '') + '</div>' + (args ? '<div class="tc-args">' + escapeHtml(args) + '</div>' : '') + '</div>';
2006
+ } else if (step.type === 'tool.result') {
2007
+ const ok = !!d.ok;
2008
+ const out = (d.output || '').toString();
2009
+ html += '<div class="step result ' + (ok ? 'ok' : 'fail') + '"><div class="res-head">' + (ok ? '✅' : '❌') + ' ' + escapeHtml(d.name || '') + ' ' + (ok ? '验证通过' : '验证失败') + '</div>' + (out ? '<div class="res-out">' + escapeHtml(out) + '</div>' : '') + '</div>';
2010
+ } else if (step.type === 'self-heal') {
2011
+ html += '<div class="step note">🩹 进入自愈:检测到 <b>' + escapeHtml(d.category || '') + '</b> 类错误,已注入反思重试(第 ' + (d.iteration != null ? d.iteration : '?') + ' 轮)</div>';
2012
+ } else if (step.type === 'context.compact') {
2013
+ html += '<div class="step note">📦 上下文压缩:从 ' + (d.originalLength != null ? d.originalLength : '?') + ' 条压缩至 ' + (d.compressedLength != null ? d.compressedLength : '?') + ' 条,保留首条目标与近期对话</div>';
2014
+ }
2015
+ }
2016
+ // 终态:最终回复 / 失败
2017
+ if (task.status === 'done') {
2018
+ if (task.result && task.result.finalAnswer) {
2019
+ const answerId = 'final-' + task.runId;
2020
+ html += '<div class="step final" id="' + answerId + '">'
2021
+ + '<div class="final-head">📬 最终回复</div>'
2022
+ + '<div style="margin-top:8px;text-align:right;">'
2023
+ + '<button onclick="bubbleActions(event, ' + JS.stringify(task.result.finalAnswer).replace(/'/g, "\\'") + ')" style="background:rgba(255,255,255,0.2);border:none;color:#fff;border-radius:6px;padding:3px 8px;font-size:11px;cursor:pointer;">⚙️ 操作</button>'
2024
+ + '</div>'
2025
+ + '<div style="margin-top:4px;">' + escapeHtml(task.result.finalAnswer) + '</div>'
2026
+ + '</div>';
2027
+ }
2028
+ } else if (task.status === 'failed') {
2029
+ if (task.result && task.result.finalAnswer) html += '<div class="step final"><div class="final-head">📬 部分回复</div>' + escapeHtml(task.result.finalAnswer) + '</div>';
2030
+ html += '<div class="step error"><div class="final-head">⛔ 任务失败</div>' + escapeHtml(task.error || '未知错误') + '</div>';
2031
+ } else if (task.status === 'running') {
2032
+ html += '<div class="step note">⏳ 任务进行中,思维链路实时刷新…</div>';
2033
+ } else if (task.status === 'queued') {
2034
+ html += '<div class="step note">📥 任务已入队,等待执行…</div>';
2035
+ }
2036
+ box.innerHTML = html;
2037
+ if (nearBottom) box.scrollTop = box.scrollHeight;
2038
+ }
2039
+ async function showArtifacts(dir) {
2040
+ try {
2041
+ const d = await api('/api/workspace/list?path=' + encodeURIComponent(dir));
2042
+ const files = (d.entries || []).filter((e) => e.type !== 'dir');
2043
+ const box = document.getElementById('taskDetail');
2044
+ const listHtml = files.length
2045
+ ? files.map((f) => '<div class="artifact-file" data-path="' + escapeHtml(f.path) + '">📄 ' + escapeHtml(f.name) + '</div>').join('')
2046
+ : '<div class="muted">该目录暂无文件产物。</div>';
2047
+ const hint = '<div class="muted" style="margin-bottom:8px;">产物文件(来自:' + escapeHtml(dir) + '),点击在右侧预览:</div>';
2048
+ const cur = box.innerHTML;
2049
+ box.innerHTML = cur + '<hr style="border:none;border-top:1px solid var(--line);margin:10px 0;">' + hint + '<div class="artifact-list">' + listHtml + '</div>';
2050
+ box.querySelectorAll('.artifact-file').forEach((el) => el.addEventListener('click', () => {
2051
+ previewFile(el.getAttribute('data-path'));
2052
+ toast('已打开:' + el.getAttribute('data-path'));
2053
+ }));
2054
+ switchRightTab('detail');
2055
+ } catch (e) { toast('读取产物失败:' + e.message); }
2056
+ }
2057
+
2058
+ /* ========== 右侧 Tab ========== */
2059
+ document.querySelectorAll('.right-tab').forEach((tab) => {
2060
+ tab.addEventListener('click', () => switchRightTab(tab.getAttribute('data-tab')));
2061
+ });
2062
+ function switchRightTab(name) {
2063
+ state.rightTab = name;
2064
+ document.querySelectorAll('.right-tab').forEach((t) => t.classList.toggle('active', t.getAttribute('data-tab') === name));
2065
+ document.getElementById('detailPanel').classList.toggle('active', name === 'detail');
2066
+ document.getElementById('previewPanel').classList.toggle('active', name === 'preview');
2067
+ }
2068
+ function showPreview(title, content, mode) {
2069
+ document.getElementById('previewTitle').textContent = title;
2070
+ const box = document.getElementById('previewContent');
2071
+ if (mode === 'code' || mode === 'file') {
2072
+ box.innerHTML = '<pre>' + escapeHtml(content) + '</pre>';
2073
+ } else {
2074
+ box.innerHTML = '<div style="white-space:pre-wrap;word-break:break-word;">' + content + '</div>';
2075
+ }
2076
+ }
2077
+
2078
+ /* ========== 对话与发送 ========== */
2079
+ async function sendTask() {
2080
+ const input = document.getElementById('goalInput');
2081
+ let goal = input.value.trim();
2082
+ if (!goal) return;
2083
+ if (!state.token) { toast('请先登录'); return; }
2084
+ if (state.directMode) goal = '[直接操作电脑] ' + goal;
2085
+ const btn = document.getElementById('sendBtn');
2086
+ btn.disabled = true;
2087
+ try {
2088
+ // 已有当前任务 → 多轮续接:消息归属同一任务生命周期
2089
+ if (state.currentTaskId) {
2090
+ const cur = state.tasks.find((t) => t.id === state.currentTaskId);
2091
+ if (cur && (cur.status === 'queued' || cur.status === 'running')) {
2092
+ toast('当前任务执行中,请等待完成后再继续对话');
2093
+ return;
2094
+ }
2095
+ const d = await api('/api/tasks/' + state.currentTaskId + '/messages', 'POST', { message: goal });
2096
+ input.value = '';
2097
+ renderSidebarTaskList();
2098
+ renderTaskDetail(d.task.id);
2099
+ renderTaskThread(d.task);
2100
+ await loadTasks();
2101
+ toast('已发送,继续当前任务对话');
2102
+ return;
2103
+ }
2104
+ const d = await api('/api/tasks', 'POST', {
2105
+ goal,
2106
+ agentType: state.agentType,
2107
+ permissions: state.permissions,
2108
+ workspaceDir: state.workspaceDir || undefined,
2109
+ modelId: state.modelId || undefined,
2110
+ });
2111
+ input.value = '';
2112
+ // 新任务即成为当前任务,中部围绕其单一生命周期可视化追踪
2113
+ state.currentTaskId = d.task.id;
2114
+ const taskListSection = document.getElementById('taskListSection');
2115
+ if (taskListSection) taskListSection.style.display = 'block';
2116
+ renderSidebarTaskList();
2117
+ renderTaskDetail(d.task.id);
2118
+ renderTaskThread({ id: d.task.id, goal, status: 'queued', createdAt: new Date().toISOString(), steps: [], conversation: [{ role: 'user', content: goal }] });
2119
+ await loadTasks();
2120
+ toast('任务已提交,开始追踪思维链路');
2121
+ } catch (e) { toast('提交失败:' + e.message); }
2122
+ finally { btn.disabled = false; }
2123
+ }
2124
+ function appendMsg(role, text, extras = {}) {
2125
+ const box = document.getElementById('messages');
2126
+ const el = document.createElement('div');
2127
+ el.className = 'msg ' + role;
2128
+ if (extras.html) el.innerHTML = extras.html;
2129
+ else if (extras.file) {
2130
+ el.innerHTML = '<span class="file-chip" data-path="' + escapeHtml(extras.file) + '" data-type="' + escapeHtml(extras.mime || 'file') + '">📎 ' + escapeHtml(extras.name) + '</span>';
2131
+ } else {
2132
+ el.innerHTML = linkifyArtifacts(text);
2133
+ }
2134
+ box.appendChild(el);
2135
+ box.scrollTop = box.scrollHeight;
2136
+ bindArtifacts(el);
2137
+ }
2138
+ document.getElementById('sendBtn').addEventListener('click', sendTask);
2139
+ document.getElementById('goalInput').addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) sendTask(); });
2140
+ document.getElementById('tchRefresh').addEventListener('click', () => { refreshCurrentThread(); toast('已刷新当前任务'); });
2141
+ document.getElementById('screenshotBtn').addEventListener('click', takeScreenshot);
2142
+
2143
+ /* ========== 输入框右键菜单 ========== */
2144
+ const goalInput = document.getElementById('goalInput');
2145
+ goalInput.addEventListener('contextmenu', (e) => {
2146
+ e.preventDefault();
2147
+ // 移除旧菜单
2148
+ const old = document.getElementById('textCtxMenu');
2149
+ if (old) old.remove();
2150
+ // 创建右键菜单
2151
+ const menu = document.createElement('div');
2152
+ menu.id = 'textCtxMenu';
2153
+ menu.style.cssText = 'position:fixed;z-index:10000;background:var(--card);border:1px solid var(--border);border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:4px 0;min-width:160px;';
2154
+ const items = [
2155
+ { label: '📋 复制', action: 'copy' },
2156
+ { label: '✂️ 剪切', action: 'cut' },
2157
+ { label: '📌 粘贴', action: 'paste' },
2158
+ { label: '⬛ 全选', action: 'selectall' },
2159
+ ];
2160
+ items.forEach((it) => {
2161
+ const el = document.createElement('div');
2162
+ el.className = 'dropdown-item';
2163
+ el.textContent = it.label;
2164
+ el.style.cssText = 'padding:6px 16px;cursor:pointer;font-size:13px;';
2165
+ el.addEventListener('mouseenter', () => { el.style.background = 'var(--brand-soft)'; });
2166
+ el.addEventListener('mouseleave', () => { el.style.background = ''; });
2167
+ el.addEventListener('click', () => {
2168
+ menu.remove();
2169
+ if (it.action === 'copy') document.execCommand('copy');
2170
+ else if (it.action === 'cut') document.execCommand('cut');
2171
+ else if (it.action === 'paste') navigator.clipboard.readText().then(t => { document.execCommand('insertText', false, t); });
2172
+ else if (it.action === 'selectall') { goalInput.select(); }
2173
+ });
2174
+ menu.appendChild(el);
2175
+ });
2176
+ document.body.appendChild(menu);
2177
+ menu.style.left = e.clientX + 'px';
2178
+ menu.style.top = e.clientY + 'px';
2179
+ });
2180
+ // 点击其他地方关闭右键菜单
2181
+ document.addEventListener('click', (e) => {
2182
+ const menu = document.getElementById('textCtxMenu');
2183
+ if (menu && !menu.contains(e.target)) menu.remove();
2184
+ });
2185
+
2186
+ /* ========== 新建对话任务 ========== */
2187
+ async function openNewTaskModal() {
2188
+ // 智能体类型下拉
2189
+ const agentSel = document.getElementById('ntAgentType');
2190
+ agentSel.innerHTML = Object.entries(AGENT_TYPES)
2191
+ .map(([k, v]) => '<option value="' + k + '"' + (state.agentType === k ? ' selected' : '') + '>' + v.icon + ' ' + v.label + '</option>')
2192
+ .join('');
2193
+ // 工作区下拉:默认(沿用当前底部所选)+ 磁盘根目录
2194
+ const wsSel = document.getElementById('ntWorkspaceDir');
2195
+ const cur = state.workspaceDir || '';
2196
+ wsSel.innerHTML = '<option value="">默认(' + (cur || '服务器工作区') + ')</option>';
2197
+ try {
2198
+ const d = await api('/api/drives');
2199
+ (d.drives || []).forEach((dr) => {
2200
+ if (dr !== cur) wsSel.innerHTML += '<option value="' + escapeHtml(dr) + '">' + escapeHtml(dr) + '</option>';
2201
+ });
2202
+ } catch {}
2203
+ document.getElementById('ntGoal').value = '';
2204
+ openModal('newTaskModal');
2205
+ setTimeout(() => document.getElementById('ntGoal').focus(), 60);
2206
+ }
2207
+ async function createNewTask() {
2208
+ const goal = document.getElementById('ntGoal').value.trim();
2209
+ if (!goal) { toast('请填写任务描述'); return; }
2210
+ const btn = document.getElementById('ntCreateBtn');
2211
+ btn.disabled = true;
2212
+ try {
2213
+ const wsVal = document.getElementById('ntWorkspaceDir').value;
2214
+ const payload = {
2215
+ goal,
2216
+ agentType: document.getElementById('ntAgentType').value || state.agentType,
2217
+ permissions: state.permissions,
2218
+ modelId: state.modelId || undefined,
2219
+ };
2220
+ // 工作区:仅在明确选择了目录时覆盖
2221
+ const d = await api('/api/tasks', 'POST', wsVal ? Object.assign(payload, { workspaceDir: wsVal }) : payload);
2222
+ document.getElementById('goalInput').value = '';
2223
+ state.currentTaskId = d.task.id;
2224
+ const taskListSection = document.getElementById('taskListSection');
2225
+ if (taskListSection) taskListSection.style.display = 'block';
2226
+ renderSidebarTaskList();
2227
+ renderTaskDetail(d.task.id);
2228
+ renderTaskThread({ id: d.task.id, goal, status: 'queued', createdAt: new Date().toISOString(), steps: [], conversation: [{ role: 'user', content: goal }] });
2229
+ switchView('chat');
2230
+ closeModal('newTaskModal');
2231
+ await loadTasks();
2232
+ toast('新任务已创建,所有对话将归属该任务');
2233
+ } catch (e) { toast('创建失败:' + e.message); }
2234
+ finally { btn.disabled = false; }
2235
+ }
2236
+ document.getElementById('newTaskBtn')?.addEventListener('click', openNewTaskModal);
2237
+ document.getElementById('tchNewTask')?.addEventListener('click', openNewTaskModal);
2238
+ document.getElementById('ntCreateBtn')?.addEventListener('click', createNewTask);
2239
+ document.getElementById('ntGoal')?.addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) createNewTask(); });
2240
+
2241
+ /* ========== 产物链接化 ========== */
2242
+ function linkifyArtifacts(text) {
2243
+ let html = escapeHtml(text);
2244
+ html = html.replace(/(https?:\/\/[^\s]+)/g, '<span class="artifact" data-kind="url" data-path="$1">$1</span>');
2245
+ html = html.replace(/([A-Za-z]:\\[\\\w\-.\s]+)/g, '<span class="artifact" data-kind="file" data-path="$1">$1</span>');
2246
+ html = html.replace(/(\/[\w\-.\/\s]+)/g, '<span class="artifact" data-kind="file" data-path="$1">$1</span>');
2247
+ return html;
2248
+ }
2249
+ function bindArtifacts(container) {
2250
+ container.querySelectorAll('.artifact').forEach((el) => {
2251
+ el.addEventListener('click', () => {
2252
+ const kind = el.getAttribute('data-kind');
2253
+ const path = el.getAttribute('data-path');
2254
+ if (kind === 'url') openBrowser(path);
2255
+ else previewFile(path);
2256
+ });
2257
+ });
2258
+ container.querySelectorAll('.file-chip').forEach((el) => {
2259
+ el.addEventListener('click', () => {
2260
+ const path = el.getAttribute('data-path');
2261
+ const mime = el.getAttribute('data-type') || '';
2262
+ if (mime.startsWith('image/')) previewImage(path);
2263
+ else previewFile(path);
2264
+ });
2265
+ });
2266
+ }
2267
+ async function previewFile(path) {
2268
+ try {
2269
+ const d = await api('/api/files/read', 'POST', { path });
2270
+ showPreview('文件: ' + path, d.content, 'file');
2271
+ switchRightTab('preview');
2272
+ } catch (e) { toast('预览失败:' + e.message); }
2273
+ }
2274
+ function previewImage(path) {
2275
+ document.getElementById('previewTitle').textContent = '图片: ' + path;
2276
+ const box = document.getElementById('previewContent');
2277
+ const img = document.createElement('img');
2278
+ img.src = 'file://' + path;
2279
+ img.style.maxWidth = '100%';
2280
+ img.style.borderRadius = '8px';
2281
+ img.addEventListener('error', () => {
2282
+ box.innerHTML = '<div class="muted">无法直接显示本地图片,请用系统相册打开</div>';
2283
+ });
2284
+ box.innerHTML = '';
2285
+ box.appendChild(img);
2286
+ switchRightTab('preview');
2287
+ }
2288
+ function extractUrl(text) {
2289
+ if (!text) return '';
2290
+ const m = text.match(/(https?:\/\/[^\s]+)/);
2291
+ return m ? m[1] : '';
2292
+ }
2293
+
2294
+ /* ========== 中间菜单 ========== */
2295
+ const convMenuWrap = document.getElementById('convMenuWrap');
2296
+ const convMenu = document.getElementById('convMenu');
2297
+ document.getElementById('convMenuBtn').addEventListener('click', (e) => {
2298
+ e.stopPropagation();
2299
+ convMenu.classList.toggle('show');
2300
+ });
2301
+ document.addEventListener('click', (e) => { if (!convMenuWrap.contains(e.target)) convMenu.classList.remove('show'); });
2302
+ convMenu.querySelectorAll('.dropdown-item').forEach((el) => {
2303
+ el.addEventListener('click', () => {
2304
+ convMenu.classList.remove('show');
2305
+ const action = el.getAttribute('data-action');
2306
+ if (action === 'screenshot') takeScreenshot();
2307
+ else if (action === 'upload-file') document.getElementById('fileInput').click();
2308
+ else if (action === 'upload-image') document.getElementById('imageInput').click();
2309
+ else if (action === 'direct-mode') toggleDirectMode();
2310
+ else if (action === 'voice') startVoice();
2311
+ else if (action === 'agent-type') openAgentSelector();
2312
+ });
2313
+ });
2314
+
2315
+ function toggleDirectMode() {
2316
+ state.directMode = !state.directMode;
2317
+ document.getElementById('directModePill').style.display = state.directMode ? 'inline-flex' : 'none';
2318
+ toast(state.directMode ? '已开启「直接操作电脑」模式' : '已关闭「直接操作电脑」模式');
2319
+ }
2320
+
2321
+ async function takeScreenshot() {
2322
+ try {
2323
+ const overlay = document.getElementById('cropOverlay');
2324
+ const selection = document.getElementById('cropSelection');
2325
+ const video = document.getElementById('cropVideo');
2326
+ const stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
2327
+ video.srcObject = stream;
2328
+ await video.play();
2329
+ overlay.style.display = 'block';
2330
+ selection.style.display = 'none';
2331
+ let startX = 0, startY = 0, drawing = false;
2332
+ const onDown = (e) => {
2333
+ drawing = true;
2334
+ startX = e.clientX; startY = e.clientY;
2335
+ selection.style.display = 'block';
2336
+ selection.style.left = startX + 'px';
2337
+ selection.style.top = startY + 'px';
2338
+ selection.style.width = '0px';
2339
+ selection.style.height = '0px';
2340
+ };
2341
+ const onMove = (e) => {
2342
+ if (!drawing) return;
2343
+ const x = Math.min(e.clientX, startX), y = Math.min(e.clientY, startY);
2344
+ const w = Math.abs(e.clientX - startX), h = Math.abs(e.clientY - startY);
2345
+ selection.style.left = x + 'px';
2346
+ selection.style.top = y + 'px';
2347
+ selection.style.width = w + 'px';
2348
+ selection.style.height = h + 'px';
2349
+ };
2350
+ const finish = async (e) => {
2351
+ if (!drawing) return;
2352
+ drawing = false;
2353
+ window.removeEventListener('mousedown', onDown);
2354
+ window.removeEventListener('mousemove', onMove);
2355
+ window.removeEventListener('mouseup', finish);
2356
+ const x = Math.min(e.clientX, startX), y = Math.min(e.clientY, startY);
2357
+ const w = Math.abs(e.clientX - startX), h = Math.abs(e.clientY - startY);
2358
+ overlay.style.display = 'none';
2359
+ if (w < 8 || h < 8) { stream.getTracks().forEach((t) => t.stop()); return; }
2360
+ try {
2361
+ const srcW = video.videoWidth, srcH = video.videoHeight;
2362
+ const scaleX = srcW / window.innerWidth, scaleY = srcH / window.innerHeight;
2363
+ const sx = Math.round(x * scaleX), sy = Math.round(y * scaleY);
2364
+ const sw = Math.round(w * scaleX), sh = Math.round(h * scaleY);
2365
+ const canvas = document.createElement('canvas');
2366
+ canvas.width = sw; canvas.height = sh;
2367
+ canvas.getContext('2d').drawImage(video, sx, sy, sw, sh, 0, 0, sw, sh);
2368
+ stream.getTracks().forEach((t) => t.stop());
2369
+ const base64 = canvas.toDataURL('image/png').split(',')[1];
2370
+ const d = await api('/api/upload', 'POST', { name: 'screenshot.png', mime: 'image/png', dataBase64: base64 });
2371
+ appendMsg('user', '', { file: d.path, name: 'screenshot.png', mime: 'image/png' });
2372
+ toast('截图已上传');
2373
+ } catch (err) { toast('截图失败:' + err.message); }
2374
+ };
2375
+ const onKey = (e) => {
2376
+ if (e.key === 'Escape') {
2377
+ overlay.style.display = 'none';
2378
+ stream.getTracks().forEach((t) => t.stop());
2379
+ window.removeEventListener('mousedown', onDown);
2380
+ window.removeEventListener('mousemove', onMove);
2381
+ window.removeEventListener('mouseup', finish);
2382
+ window.removeEventListener('keydown', onKey);
2383
+ }
2384
+ };
2385
+ window.addEventListener('mousedown', onDown);
2386
+ window.addEventListener('mousemove', onMove);
2387
+ window.addEventListener('mouseup', finish);
2388
+ window.addEventListener('keydown', onKey);
2389
+ } catch (e) { toast('截图失败:' + e.message); }
2390
+ }
2391
+
2392
+ function handleUpload(input, mimePrefix) {
2393
+ const file = input.files[0];
2394
+ if (!file) return;
2395
+ const reader = new FileReader();
2396
+ reader.onload = async () => {
2397
+ try {
2398
+ const base64 = String(reader.result).split(',')[1];
2399
+ const d = await api('/api/upload', 'POST', { name: file.name, mime: file.type || mimePrefix, dataBase64: base64 });
2400
+ appendMsg('user', '', { file: d.path, name: file.name, mime: file.type || mimePrefix });
2401
+ toast('已上传:' + file.name);
2402
+ } catch (e) { toast('上传失败:' + e.message); }
2403
+ };
2404
+ reader.readAsDataURL(file);
2405
+ input.value = '';
2406
+ }
2407
+ document.getElementById('fileInput').addEventListener('change', function() { handleUpload(this, 'application/octet-stream'); });
2408
+ document.getElementById('imageInput').addEventListener('change', function() { handleUpload(this, 'image/png'); });
2409
+
2410
+ /* ========== 大模型配置 ========== */
2411
+ async function loadModels() {
2412
+ try {
2413
+ const d = await api('/api/models');
2414
+ state.models = d.models || [];
2415
+ state.defaultModelId = d.defaultId || null;
2416
+ const manual = localStorage.getItem('fhcode.model');
2417
+ if (manual !== null && manual !== '') {
2418
+ state.modelId = manual;
2419
+ } else {
2420
+ state.modelId = state.defaultModelId || '';
2421
+ }
2422
+ renderModelSelect();
2423
+ } catch (e) { console.warn('加载大模型失败', e); }
2424
+ }
2425
+ function renderModelSelect() {
2426
+ const sel = document.getElementById('modelSelect');
2427
+ const opts = ['<option value="">默认(系统路由)</option>'];
2428
+ state.models.forEach((m) => {
2429
+ opts.push('<option value="' + escapeHtml(m.id) + '">' + escapeHtml(m.name) + (m.default ? ' ★' : '') + '</option>');
2430
+ });
2431
+ sel.innerHTML = opts.join('');
2432
+ sel.value = state.modelId || '';
2433
+ }
2434
+ function openModels() {
2435
+ renderModelList();
2436
+ resetModelForm();
2437
+ openModal('modelModal');
2438
+ }
2439
+ function renderModelList() {
2440
+ const list = document.getElementById('modelList');
2441
+ if (!state.models.length) {
2442
+ list.innerHTML = '<div class="empty">还没有大模型配置,请在下方添加。</div>';
2443
+ return;
2444
+ }
2445
+ list.innerHTML = state.models.map((m) => (
2446
+ '<div class="model-item" data-id="' + escapeHtml(m.id) + '">' +
2447
+ '<div><div class="mi-name">' + escapeHtml(m.name) + '</div>' +
2448
+ '<div class="mi-base">' + escapeHtml(m.apiBase || '(未填写 API 地址)') + (m.reasoning ? ' · 已配推理内容' : '') + '</div></div>' +
2449
+ (m.default ? '<span class="mi-default">默认</span>' : '') +
2450
+ '<div class="mi-actions">' +
2451
+ (m.default ? '' : '<button data-act="default">设为默认</button>') +
2452
+ '<button data-act="edit">编辑</button>' +
2453
+ '<button class="danger" data-act="del">删除</button>' +
2454
+ '</div>' +
2455
+ '</div>'
2456
+ )).join('');
2457
+ list.querySelectorAll('.model-item').forEach((el) => {
2458
+ const id = el.getAttribute('data-id');
2459
+ el.querySelectorAll('button').forEach((b) => {
2460
+ b.addEventListener('click', (e) => {
2461
+ e.stopPropagation();
2462
+ const act = b.getAttribute('data-act');
2463
+ if (act === 'default') setDefaultModel(id);
2464
+ else if (act === 'edit') editModel(id);
2465
+ else if (act === 'del') deleteModel(id);
2466
+ });
2467
+ });
2468
+ });
2469
+ }
2470
+ function resetModelForm() {
2471
+ document.getElementById('modelName').value = '';
2472
+ document.getElementById('modelApiBase').value = '';
2473
+ document.getElementById('modelApiKey').value = '';
2474
+ document.getElementById('modelReasoning').value = '';
2475
+ document.getElementById('modelFormMsg').textContent = '';
2476
+ document.getElementById('modelFormMsg').classList.remove('err');
2477
+ document.getElementById('modelSaveBtn').dataset.editing = '';
2478
+ }
2479
+ function editModel(id) {
2480
+ const m = state.models.find((x) => x.id === id);
2481
+ if (!m) return;
2482
+ document.getElementById('modelName').value = m.name;
2483
+ document.getElementById('modelApiBase').value = m.apiBase || '';
2484
+ document.getElementById('modelApiKey').value = m.apiKey || '';
2485
+ document.getElementById('modelReasoning').value = m.reasoning || '';
2486
+ document.getElementById('modelFormMsg').textContent = '正在编辑:' + m.name;
2487
+ document.getElementById('modelFormMsg').classList.remove('err');
2488
+ document.getElementById('modelSaveBtn').dataset.editing = id;
2489
+ }
2490
+ async function saveModel() {
2491
+ const name = document.getElementById('modelName').value.trim();
2492
+ const apiBase = document.getElementById('modelApiBase').value.trim();
2493
+ const apiKey = document.getElementById('modelApiKey').value;
2494
+ const reasoning = document.getElementById('modelReasoning').value;
2495
+ const msg = document.getElementById('modelFormMsg');
2496
+ if (!name) { msg.textContent = '请填写模型名称'; msg.classList.add('err'); return; }
2497
+ const editing = document.getElementById('modelSaveBtn').dataset.editing;
2498
+ try {
2499
+ const body = { name, apiBase, apiKey, reasoning };
2500
+ if (editing) body.id = editing;
2501
+ const d = await api('/api/models', 'POST', body);
2502
+ msg.textContent = '已保存:' + d.model.name;
2503
+ msg.classList.remove('err');
2504
+ await loadModels();
2505
+ renderModelList();
2506
+ resetModelForm();
2507
+ } catch (e) { msg.textContent = '保存失败:' + e.message; msg.classList.add('err'); }
2508
+ }
2509
+ async function deleteModel(id) {
2510
+ try {
2511
+ await api('/api/models/' + encodeURIComponent(id), 'DELETE');
2512
+ toast('已删除配置');
2513
+ await loadModels();
2514
+ renderModelList();
2515
+ } catch (e) { toast('删除失败:' + e.message); }
2516
+ }
2517
+ async function setDefaultModel(id) {
2518
+ try {
2519
+ const d = await api('/api/models/' + encodeURIComponent(id) + '/default', 'POST');
2520
+ state.defaultModelId = d.defaultId;
2521
+ state.modelId = d.defaultId;
2522
+ localStorage.setItem('fhcode.model', '');
2523
+ renderModelSelect();
2524
+ renderModelList();
2525
+ renderModelListInline();
2526
+ toast('已设为默认大模型');
2527
+ } catch (e) { toast('设置失败:' + e.message); }
2528
+ }
2529
+ document.getElementById('modelSaveBtn').addEventListener('click', saveModel);
2530
+ document.getElementById('modelResetBtn').addEventListener('click', resetModelForm);
2531
+ document.getElementById('modelSettingsShortcut').addEventListener('click', openModels);
2532
+ document.getElementById('modelSelect').addEventListener('change', (e) => {
2533
+ state.modelId = e.target.value || '';
2534
+ localStorage.setItem('fhcode.model', state.modelId);
2535
+ const m = state.models.find((x) => x.id === state.modelId);
2536
+ toast(m ? ('已切换:' + m.name) : '已切换:系统默认路由');
2537
+ });
2538
+
2539
+ /* ========== 设置面板 ========== */
2540
+ function openSettings() { openModal('settingsModal'); }
2541
+ document.querySelectorAll('#settingsModal .settings-cat').forEach((c) => {
2542
+ c.addEventListener('click', () => {
2543
+ const cat = c.getAttribute('data-cat');
2544
+ document.querySelectorAll('#settingsModal .settings-cat').forEach((x) => x.classList.toggle('active', x === c));
2545
+ document.querySelectorAll('#settingsModal .settings-group').forEach((g) => {
2546
+ g.style.display = g.getAttribute('data-group') === cat ? '' : 'none';
2547
+ });
2548
+ });
2549
+ });
2550
+ document.querySelectorAll('#settingsModal .settings-item').forEach((it) => {
2551
+ it.addEventListener('click', () => {
2552
+ const act = it.getAttribute('data-action');
2553
+ if (act === 'perm') { closeModal('settingsModal'); openPermissions(); }
2554
+ else if (act === 'theme') { closeModal('settingsModal'); toggleTheme(); }
2555
+ else if (act === 'model-custom') { toggleModelManage(); }
2556
+ });
2557
+ });
2558
+ function toggleModelManage() {
2559
+ const box = document.getElementById('modelManage');
2560
+ const open = box.style.display === 'none' || !box.style.display;
2561
+ box.style.display = open ? '' : 'none';
2562
+ if (open) { renderModelListInline(); resetInlineForm(); }
2563
+ }
2564
+ function renderModelListInline() {
2565
+ const list = document.getElementById('modelListInline');
2566
+ if (!state.models.length) {
2567
+ list.innerHTML = '<div class="empty">还没有大模型配置,请在下方添加。</div>';
2568
+ return;
2569
+ }
2570
+ list.innerHTML = state.models.map((m) => (
2571
+ '<div class="mm-model" data-id="' + escapeHtml(m.id) + '">' +
2572
+ '<span class="mm-name">' + escapeHtml(m.name) + '</span>' +
2573
+ '<span class="mm-base">' + escapeHtml(m.apiBase || '(未填写 API 地址)') + (m.reasoning ? ' · 已配推理内容' : '') + '</span>' +
2574
+ (m.default ? '<span class="mm-tag">默认</span>' : '') +
2575
+ (m.default ? '' : '<button data-act="default">设为默认</button>') +
2576
+ '<button data-act="edit">编辑</button>' +
2577
+ '<button class="danger" data-act="del">删除</button>' +
2578
+ '</div>'
2579
+ )).join('');
2580
+ list.querySelectorAll('.mm-model').forEach((el) => {
2581
+ const id = el.getAttribute('data-id');
2582
+ el.querySelectorAll('button').forEach((b) => {
2583
+ b.addEventListener('click', (e) => {
2584
+ e.stopPropagation();
2585
+ const act = b.getAttribute('data-act');
2586
+ if (act === 'default') setDefaultModel(id);
2587
+ else if (act === 'edit') fillInlineForm(id);
2588
+ else if (act === 'del') deleteModel(id);
2589
+ });
2590
+ });
2591
+ });
2592
+ }
2593
+ function resetInlineForm() {
2594
+ document.getElementById('imName').value = '';
2595
+ document.getElementById('imBase').value = '';
2596
+ document.getElementById('imKey').value = '';
2597
+ document.getElementById('imReasoning').value = '';
2598
+ document.getElementById('imMsg').textContent = '';
2599
+ document.getElementById('imMsg').classList.remove('err');
2600
+ document.getElementById('imSave').dataset.editing = '';
2601
+ }
2602
+ function fillInlineForm(id) {
2603
+ const m = state.models.find((x) => x.id === id);
2604
+ if (!m) return;
2605
+ document.getElementById('imName').value = m.name;
2606
+ document.getElementById('imBase').value = m.apiBase || '';
2607
+ document.getElementById('imKey').value = m.apiKey || '';
2608
+ document.getElementById('imReasoning').value = m.reasoning || '';
2609
+ document.getElementById('imMsg').textContent = '正在编辑:' + m.name;
2610
+ document.getElementById('imMsg').classList.remove('err');
2611
+ document.getElementById('imSave').dataset.editing = id;
2612
+ }
2613
+ async function saveInlineModel() {
2614
+ const name = document.getElementById('imName').value.trim();
2615
+ const apiBase = document.getElementById('imBase').value.trim();
2616
+ const apiKey = document.getElementById('imKey').value;
2617
+ const reasoning = document.getElementById('imReasoning').value;
2618
+ const msg = document.getElementById('imMsg');
2619
+ if (!name) { msg.textContent = '请填写模型名称'; msg.classList.add('err'); return; }
2620
+ const editing = document.getElementById('imSave').dataset.editing;
2621
+ try {
2622
+ const body = { name, apiBase, apiKey, reasoning };
2623
+ if (editing) body.id = editing;
2624
+ const d = await api('/api/models', 'POST', body);
2625
+ msg.textContent = '已保存:' + d.model.name;
2626
+ msg.classList.remove('err');
2627
+ await loadModels();
2628
+ renderModelListInline();
2629
+ resetInlineForm();
2630
+ } catch (e) { msg.textContent = '保存失败:' + e.message; msg.classList.add('err'); }
2631
+ }
2632
+ document.getElementById('imSave').addEventListener('click', saveInlineModel);
2633
+ document.getElementById('imReset').addEventListener('click', resetInlineForm);
2634
+ document.getElementById('modelManageClose').addEventListener('click', () => { document.getElementById('modelManage').style.display = 'none'; });
2635
+
2636
+ /* ========== 通话控件 ========== */
2637
+ let callStream = null;
2638
+ async function startCall(kind) {
2639
+ const status = document.getElementById('callStatus');
2640
+ try {
2641
+ const constraints = kind === 'video' ? { audio: true, video: true } : { audio: true };
2642
+ callStream = await navigator.mediaDevices.getUserMedia(constraints);
2643
+ status.textContent = (kind === 'video' ? '视频通话中' : '语音通话中') + '…';
2644
+ document.getElementById('hangupCallBtn').style.display = 'inline-block';
2645
+ toast('已发起' + (kind === 'video' ? '视频' : '语音') + '通话(演示)');
2646
+ } catch (e) {
2647
+ status.textContent = '';
2648
+ toast('无法发起通话:' + e.message);
2649
+ }
2650
+ }
2651
+ function hangupCall() {
2652
+ if (callStream) { callStream.getTracks().forEach((t) => t.stop()); callStream = null; }
2653
+ document.getElementById('callStatus').textContent = '';
2654
+ document.getElementById('hangupCallBtn').style.display = 'none';
2655
+ toast('已挂断');
2656
+ }
2657
+ document.getElementById('voiceCallBtn').addEventListener('click', () => startCall('voice'));
2658
+ document.getElementById('videoCallBtn').addEventListener('click', () => startCall('video'));
2659
+ document.getElementById('hangupCallBtn').addEventListener('click', hangupCall);
2660
+
2661
+ function startVoice() {
2662
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
2663
+ if (!SpeechRecognition) { toast('当前浏览器不支持语音输入'); return; }
2664
+ const rec = new SpeechRecognition();
2665
+ rec.lang = 'zh-CN';
2666
+ rec.continuous = false;
2667
+ rec.interimResults = false;
2668
+ const badge = document.getElementById('voiceBadge');
2669
+ badge.style.display = 'inline-flex';
2670
+ rec.onresult = (e) => {
2671
+ const text = e.results[0][0].transcript;
2672
+ const input = document.getElementById('goalInput');
2673
+ input.value = (input.value ? input.value + ' ' : '') + text;
2674
+ };
2675
+ rec.onerror = (e) => { toast('语音识别失败:' + e.error); badge.style.display = 'none'; };
2676
+ rec.onend = () => { badge.style.display = 'none'; };
2677
+ rec.start();
2678
+ }
2679
+
2680
+ /* ========== 智能体选择 ========== */
2681
+ function openAgentSelector() {
2682
+ const grid = document.getElementById('agentGrid');
2683
+ grid.innerHTML = Object.entries(AGENT_TYPES).map(([k, v]) =>
2684
+ '<div class="agent-card ' + (state.agentType === k ? 'selected' : '') + '" data-type="' + k + '"><div class="icon">' + v.icon + '</div><div class="title">' + v.label + '</div></div>'
2685
+ ).join('');
2686
+ grid.querySelectorAll('.agent-card').forEach((c) => c.addEventListener('click', () => {
2687
+ state.agentType = c.getAttribute('data-type');
2688
+ updateUserBar();
2689
+ closeModal('agentModal');
2690
+ toast('已切换:' + AGENT_TYPES[state.agentType].label);
2691
+ }));
2692
+ openModal('agentModal');
2693
+ }
2694
+ document.getElementById('skillPill').addEventListener('click', openAgentSelector);
2695
+
2696
+ /* ========== 权限窗口 ========== */
2697
+ function openPermissions() {
2698
+ const p = state.permissions;
2699
+ document.querySelector('input[name="readScope"][value="' + p.readScope + '"]').checked = true;
2700
+ document.getElementById('permReadPath').value = p.readPath || '';
2701
+ document.getElementById('permRead').checked = p.allowRead;
2702
+ document.getElementById('permWrite').checked = p.allowWrite;
2703
+ document.getElementById('permShell').checked = p.allowShell;
2704
+ document.getElementById('permNetwork').checked = p.allowNetwork;
2705
+ document.getElementById('permBrowser').checked = p.allowBrowser;
2706
+ openModal('permModal');
2707
+ }
2708
+ document.getElementById('permSaveBtn').addEventListener('click', () => {
2709
+ const scope = document.querySelector('input[name="readScope"]:checked')?.value || 'workspace';
2710
+ state.permissions = {
2711
+ readScope: scope,
2712
+ readPath: document.getElementById('permReadPath').value.trim(),
2713
+ allowRead: document.getElementById('permRead').checked,
2714
+ allowWrite: document.getElementById('permWrite').checked,
2715
+ allowShell: document.getElementById('permShell').checked,
2716
+ allowNetwork: document.getElementById('permNetwork').checked,
2717
+ allowBrowser: document.getElementById('permBrowser').checked,
2718
+ };
2719
+ localStorage.setItem('fhcode.permissions', JSON.stringify(state.permissions));
2720
+ closeModal('permModal');
2721
+ toast('权限配置已保存');
2722
+ });
2723
+
2724
+ /* ========== 工作区 ========== */
2725
+ async function loadWorkspace() {
2726
+ if (!state.token) {
2727
+ console.warn('[workspace] 未登录,跳过加载');
2728
+ return;
2729
+ }
2730
+ try {
2731
+ const d = await api('/api/workspace');
2732
+ state.workspaceDir = d.cwd;
2733
+ // 持久化到 localStorage
2734
+ localStorage.setItem('fhcode.workspaceDir', d.cwd);
2735
+ renderWorkspaceBar();
2736
+ console.log('[workspace] 加载成功:', d.cwd);
2737
+ } catch (e) {
2738
+ console.error('[workspace] 加载失败:', e.message);
2739
+ toast('工作区加载失败,请检查登录状态');
2740
+ }
2741
+ }
2742
+ async function loadWorkspaceTree(dir) {
2743
+ // 已废弃:左侧工作区树已移除,保留此函数仅供兼容
2744
+ return;
2745
+ }
2746
+ // 工作区跳转功能已移除(旧布局左侧工作区树)
2747
+
2748
+ function renderWorkspaceBar() {
2749
+ const el = document.getElementById('workspaceCwdBottom');
2750
+ if (el) el.textContent = state.workspaceDir || '(未设置)';
2751
+ }
2752
+ document.getElementById('workspacePermBadge')?.addEventListener('click', () => openPermissions());
2753
+ document.getElementById('workspacePickBtn')?.addEventListener('click', () => {
2754
+ openFolderPicker();
2755
+ });
2756
+
2757
+ let fpCurrent = '';
2758
+ function getDirectoryName(path) {
2759
+ console.log('[getDirectoryName] input:', path);
2760
+ // 处理 Windows 驱动器根目录: C:, D:, H: 等
2761
+ if (!path || path === '' || path === '/') return '/';
2762
+ // 检查是否是纯驱动器字母(如 H:)
2763
+ if (/^[A-Za-z]:$/.test(path)) { const r = path + '\\'; console.log('[getDirectoryName] result:', r); return r; }
2764
+ // 检查是否是驱动器根目录(如 H:\)
2765
+ if (/^[A-Za-z]:\\$/.test(path)) { console.log('[getDirectoryName] result:', path); return path; }
2766
+ const parts = path.split(/[/\\]/).filter(Boolean);
2767
+ console.log('[getDirectoryName] parts:', parts);
2768
+ if (parts.length <= 1) {
2769
+ const driveMatch = parts[0].match(/^([A-Za-z]):$/);
2770
+ if (driveMatch) { const r = driveMatch[1] + ':' + '\\'; console.log('[getDirectoryName] result:', r); return r; }
2771
+ return parts[0] || '/';
2772
+ }
2773
+ const parentParts = parts.slice(0, -1);
2774
+ const firstPart = parentParts[0];
2775
+ if (/^[A-Za-z]:$/.test(firstPart)) {
2776
+ // Windows 驱动器根目录特殊处理
2777
+ const r = firstPart + '\\' + parentParts.slice(1).join('\\');
2778
+ console.log('[getDirectoryName] result:', r, '(parentParts:', parentParts + ')');
2779
+ return r;
2780
+ }
2781
+ const r = parentParts.join('\\');
2782
+ console.log('[getDirectoryName] result:', r);
2783
+ return r;
2784
+ }
2785
+ async function openFolderPicker() {
2786
+ console.log('[folderPicker] 打开文件夹选择器');
2787
+ console.log('[folderPicker] 当前 state.workspaceDir:', state.workspaceDir);
2788
+ console.log('[folderPicker] 当前 state.token:', state.token ? '已登录' : '未登录');
2789
+
2790
+ // 如果 workspaceDir 为空,尝试从 localStorage 恢复或询问用户
2791
+ let initialDir = state.workspaceDir;
2792
+ if (!initialDir) {
2793
+ // 尝试从 localStorage 恢复
2794
+ const savedCwd = localStorage.getItem('fhcode.workspaceDir');
2795
+ if (savedCwd) {
2796
+ initialDir = savedCwd;
2797
+ console.log('[folderPicker] 从 localStorage 恢复工作区:', initialDir);
2798
+ } else {
2799
+ // 使用默认路径
2800
+ initialDir = '.';
2801
+ console.log('[folderPicker] 使用默认路径:', initialDir);
2802
+ }
2803
+ }
2804
+
2805
+ fpCurrent = initialDir || '.';
2806
+ const fpPathEl = document.getElementById('fpPath');
2807
+ if (fpPathEl) fpPathEl.textContent = fpCurrent || '(根目录)';
2808
+
2809
+ try {
2810
+ console.log('[folderPicker] 加载目录列表:', fpCurrent);
2811
+ await loadFolderPicker(fpCurrent);
2812
+ console.log('[folderPicker] 目录加载成功');
2813
+ } catch (e) {
2814
+ console.error('[folderPicker] 加载失败:', e);
2815
+ toast('读取目录失败:' + e.message);
2816
+ return;
2817
+ }
2818
+ openModal('folderPickerModal');
2819
+ console.log('[folderPicker] 模态框已打开');
2820
+ }
2821
+ async function loadFolderPicker(dir) {
2822
+ console.log('[folderPicker] 加载:', dir);
2823
+ const d = await api('/api/workspace/list?path=' + encodeURIComponent(dir));
2824
+ fpCurrent = d.cwd;
2825
+ const fpPathEl = document.getElementById('fpPath');
2826
+ if (fpPathEl) fpPathEl.textContent = fpCurrent;
2827
+ const tree = document.getElementById('fpTree');
2828
+ if (!tree) {
2829
+ console.error('[folderPicker] fpTree 元素不存在');
2830
+ return;
2831
+ }
2832
+ const parentPath = getDirectoryName(d.cwd);
2833
+ // 如果在驱动器根目录,不显示"..",改为显示驱动器列表
2834
+ const isDriveRoot = /^[A-Za-z]:\\$/.test(d.cwd);
2835
+ let upButtonHtml = '';
2836
+ if (!isDriveRoot && parentPath && parentPath !== d.cwd) {
2837
+ upButtonHtml = '<div class="file-item" data-path="' + escapeHtml(parentPath) + '" data-type="up">⬆ ..</div>';
2838
+ } else if (isDriveRoot) {
2839
+ // 动态获取可用驱动器列表
2840
+ const drives = await getDrivesList();
2841
+ upButtonHtml = '<div class="file-item drive-switcher">' +
2842
+ '<span>🖥 切换驱动器</span>' +
2843
+ drives.map(d => '<div class="drive-option" data-drive="' + d + '">' + d + '</div>').join('') +
2844
+ '</div>';
2845
+ }
2846
+ tree.innerHTML = upButtonHtml +
2847
+ d.entries.map((e) => '<div class="file-item ' + (e.type === 'dir' ? 'dir' : '') + '" data-path="' + escapeHtml(e.path) + '" data-type="' + e.type + '">' + (e.type === 'dir' ? '📁' : '📄') + ' ' + escapeHtml(e.name) + '</div>').join('');
2848
+ console.log('[folderPicker] 目录渲染完成, 条目数:', d.entries.length);
2849
+ }
2850
+ // 驱动器列表缓存
2851
+ let _drivesCache = null;
2852
+ async function getDrivesList() {
2853
+ if (_drivesCache) return _drivesCache;
2854
+ try {
2855
+ const d = await api('/api/drives');
2856
+ _drivesCache = d.drives || [];
2857
+ return _drivesCache;
2858
+ } catch {
2859
+ return ['C:\\', 'D:\\', 'E:\\', 'F:\\', 'H:\\'];
2860
+ }
2861
+ }
2862
+
2863
+ document.getElementById('fpConfirm')?.addEventListener('click', async () => {
2864
+ console.log('[folderPicker] 点击确认, 当前选择:', fpCurrent);
2865
+ try {
2866
+ console.log('[folderPicker] 提交切换工作区...');
2867
+ const result = await api('/api/workspace', 'POST', { cwd: fpCurrent });
2868
+ console.log('[folderPicker] API 返回:', result);
2869
+ state.workspaceDir = fpCurrent;
2870
+ // 持久化到 localStorage
2871
+ localStorage.setItem('fhcode.workspaceDir', fpCurrent);
2872
+ renderWorkspaceBar();
2873
+ closeModal('folderPickerModal');
2874
+ toast('已切换工作区:' + fpCurrent);
2875
+ } catch (e) {
2876
+ console.error('[folderPicker] 切换失败:', e);
2877
+ toast('设置失败:' + e.message);
2878
+ }
2879
+ });
2880
+
2881
+ /* ========== 自动化、模板、市场、办公助理 ========== */
2882
+ async function loadAutomations() {
2883
+ try { const d = await api('/api/automations'); renderAutoGrid(d.automations || []); }
2884
+ catch (e) { console.warn('加载自动化失败', e); }
2885
+ }
2886
+ function renderAutoGrid(list) {
2887
+ const grid = document.getElementById('autoGrid');
2888
+ if (!list.length) { grid.innerHTML = '<div class="empty">还没有快捷指令</div>'; return; }
2889
+ grid.innerHTML = list.map((a) => '<div class="tile" data-id="' + a.id + '"><div class="icon">⚡</div><div class="title">' + escapeHtml(a.name) + '</div><div class="desc">' + escapeHtml(a.goal).slice(0, 60) + (a.goal.length > 60 ? '…' : '') + '<br>已运行 ' + a.runCount + ' 次</div><div class="ops"><button class="run" data-id="' + a.id + '">▶ 运行</button><button class="ghost del" data-id="' + a.id + '">删除</button></div></div>').join('');
2890
+ grid.querySelectorAll('button.run').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); runAuto(b.getAttribute('data-id')); }));
2891
+ grid.querySelectorAll('button.del').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); delAuto(b.getAttribute('data-id')); }));
2892
+ }
2893
+ async function runAuto(id) {
2894
+ try { const d = await api('/api/automations/' + id + '/run', 'POST'); toast('已发起任务,运行次数 ' + d.runCount); await loadTasks(); }
2895
+ catch (e) { toast('运行失败:' + e.message); }
2896
+ }
2897
+ async function delAuto(id) {
2898
+ try { await api('/api/automations/' + id, 'DELETE'); await loadAutomations(); toast('已删除'); }
2899
+ catch (e) { toast('删除失败:' + e.message); }
2900
+ }
2901
+ document.getElementById('newAutoBtn')?.addEventListener('click', () => openModal('autoModal'));
2902
+ document.getElementById('autoSaveBtn')?.addEventListener('click', async () => {
2903
+ const name = document.getElementById('autoName').value.trim();
2904
+ const goal = document.getElementById('autoGoal').value.trim();
2905
+ if (!name || !goal) { toast('请填写名称与目标'); return; }
2906
+ try { await api('/api/automations', 'POST', { name, goal }); closeModal('autoModal');
2907
+ const n = document.getElementById('autoName');
2908
+ const g = document.getElementById('autoGoal');
2909
+ if (n) n.value = ''; if (g) g.value = '';
2910
+ await loadAutomations(); toast('已保存'); }
2911
+ catch (e) { toast('保存失败:' + e.message); }
2912
+ });
2913
+
2914
+ async function loadTemplates() {
2915
+ try { const d = await api('/api/templates'); renderBuiltin(d.builtin || []); renderUserTpl(d.user || []); }
2916
+ catch (e) { console.warn('加载模板失败', e); }
2917
+ }
2918
+ function tplCard(t, deletable) {
2919
+ return '<div class="tile" data-goal="' + encodeURIComponent(t.goal) + '"><div class="icon">' + (t.icon || '📄') + '</div><div class="title">' + escapeHtml(t.title) + '</div><div class="desc">' + escapeHtml(t.category || '') + ' · ' + escapeHtml(t.goal).slice(0, 40) + '…</div><div class="ops"><button class="use" data-goal="' + encodeURIComponent(t.goal) + '">填入输入框</button>' + (deletable ? '<button class="ghost del" data-id="' + t.id + '">删除</button>' : '') + '</div></div>';
2920
+ }
2921
+ function renderBuiltin(list) {
2922
+ const grid = document.getElementById('builtinGrid');
2923
+ if (!list.length) { grid.innerHTML = '<div class="empty">无内置模板</div>'; return; }
2924
+ grid.innerHTML = list.map((t) => tplCard(t, false)).join('');
2925
+ bindTplCards(grid);
2926
+ }
2927
+ function renderUserTpl(list) {
2928
+ const grid = document.getElementById('userTplGrid');
2929
+ if (!list.length) { grid.innerHTML = '<div class="empty">暂无自定义模板</div>'; return; }
2930
+ grid.innerHTML = list.map((t) => tplCard(t, true)).join('');
2931
+ bindTplCards(grid);
2932
+ }
2933
+ function bindTplCards(grid) {
2934
+ grid.querySelectorAll('button.use').forEach((b) => b.addEventListener('click', (e) => {
2935
+ e.stopPropagation();
2936
+ const inp = document.getElementById('goalInput');
2937
+ if (inp) inp.value = decodeURIComponent(b.getAttribute('data-goal'));
2938
+ switchView('chat');
2939
+ if (inp) inp.focus();
2940
+ toast('已填入输入框,去发送吧');
2941
+ }));
2942
+ grid.querySelectorAll('button.del').forEach((b) => b.addEventListener('click', async (e) => {
2943
+ e.stopPropagation();
2944
+ try { await api('/api/templates/' + b.getAttribute('data-id'), 'DELETE'); await loadTemplates(); toast('已删除'); }
2945
+ catch (err) { toast('删除失败:' + err.message); }
2946
+ }));
2947
+ }
2948
+ document.getElementById('newTplBtn')?.addEventListener('click', () => {
2949
+ const goal = document.getElementById('goalInput')?.value?.trim() || '';
2950
+ const inp = document.getElementById('tplGoal');
2951
+ if (inp) inp.value = goal;
2952
+ openModal('tplModal');
2953
+ });
2954
+ document.getElementById('tplSaveBtn')?.addEventListener('click', async () => {
2955
+ const title = document.getElementById('tplTitle').value.trim();
2956
+ const goal = document.getElementById('tplGoal').value.trim();
2957
+ const category = document.getElementById('tplCategory').value.trim();
2958
+ if (!title || !goal) { toast('请填写标题与内容'); return; }
2959
+ try { await api('/api/templates', 'POST', { title, goal, category }); closeModal('tplModal');
2960
+ const t = document.getElementById('tplTitle');
2961
+ const g = document.getElementById('tplGoal');
2962
+ const c = document.getElementById('tplCategory');
2963
+ if (t) t.value = ''; if (g) g.value = ''; if (c) c.value = '';
2964
+ await loadTemplates(); toast('已保存模板'); }
2965
+ catch (e) { toast('保存失败:' + e.message); }
2966
+ });
2967
+
2968
+ async function loadMarket() {
2969
+ const q = document.getElementById('marketSearch').value.trim();
2970
+ const source = document.getElementById('marketSource').value;
2971
+ const grid = document.getElementById('marketGrid');
2972
+ grid.innerHTML = '<div class="empty">加载中…</div>';
2973
+ try {
2974
+ const params = new URLSearchParams({ source });
2975
+ if (q) params.set('q', q);
2976
+ const d = await api('/api/skills/market?' + params.toString());
2977
+ state.market = d.skills || [];
2978
+ renderMarketGrid();
2979
+ await loadInstalled();
2980
+ } catch (e) { grid.innerHTML = '<div class="empty" style="color:var(--err)">加载失败:' + escapeHtml(e.message) + '</div>'; }
2981
+ }
2982
+ function renderMarketGrid() {
2983
+ const grid = document.getElementById('marketGrid');
2984
+ if (!state.market.length) { grid.innerHTML = '<div class="empty">没有找到技能</div>'; return; }
2985
+ grid.innerHTML = state.market.map((s) => '<div class="tile"><div class="icon">' + (s.source === 'clawhub' ? '🧩' : '🛠') + '</div><div class="title">' + escapeHtml(s.name) + '</div><div class="desc">' + escapeHtml(s.description).slice(0, 70) + '…<br><span class="muted">' + (s.source === 'clawhub' ? 'ClawHub' : 'Agent-Foundry') + ' · 下载 ' + (s.downloads || 0) + '</span></div><div class="ops"><button class="install" data-id="' + encodeURIComponent(s.id) + '" data-name="' + encodeURIComponent(s.name) + '" data-source="' + s.source + '">安装</button></div></div>').join('');
2986
+ grid.querySelectorAll('button.install').forEach((b) => b.addEventListener('click', (e) => {
2987
+ e.stopPropagation();
2988
+ installSkill(b.getAttribute('data-id'), b.getAttribute('data-name'), b.getAttribute('data-source'));
2989
+ }));
2990
+ }
2991
+ async function installSkill(idEnc, nameEnc, source) {
2992
+ const id = decodeURIComponent(idEnc);
2993
+ const name = decodeURIComponent(nameEnc);
2994
+ try { await api('/api/skills/install', 'POST', { id, name, source }); toast('已安装:' + name); await loadInstalled(); }
2995
+ catch (e) { toast('安装失败:' + e.message); }
2996
+ }
2997
+ async function loadInstalled() {
2998
+ try {
2999
+ const d = await api('/api/skills/installed');
3000
+ state.installed = new Set((d.skills || []).map((s) => s.id));
3001
+ const grid = document.getElementById('installedGrid');
3002
+ if (!d.skills || !d.skills.length) { grid.innerHTML = '<div class="empty">暂无已安装技能</div>'; return; }
3003
+ grid.innerHTML = d.skills.map((s) => '<div class="tile"><div class="icon">✅</div><div class="title">' + escapeHtml(s.name) + '</div><div class="desc">' + escapeHtml(s.description || '').slice(0, 60) + '…</div></div>').join('');
3004
+ } catch (e) { console.warn('加载已安装失败', e); }
3005
+ }
3006
+ document.getElementById('marketRefresh')?.addEventListener('click', loadMarket);
3007
+ let marketTimer;
3008
+ document.getElementById('marketSearch')?.addEventListener('input', () => { clearTimeout(marketTimer); marketTimer = setTimeout(loadMarket, 400); });
3009
+ document.getElementById('marketSource')?.addEventListener('change', loadMarket);
3010
+
3011
+ async function loadOffice() {
3012
+ try {
3013
+ const d = await api('/api/office/capabilities');
3014
+ const grid = document.getElementById('officeGrid');
3015
+ grid.innerHTML = (d.capabilities || []).map((c) => '<div class="tile" data-prompt="' + encodeURIComponent(c.prompt) + '"><div class="icon">' + c.icon + '</div><div class="title">' + escapeHtml(c.title) + '</div><div class="desc">' + escapeHtml(c.desc) + '</div><div class="ops"><button class="use" data-prompt="' + encodeURIComponent(c.prompt) + '">生成提示词</button></div></div>').join('');
3016
+ grid.querySelectorAll('button.use').forEach((b) => b.addEventListener('click', (e) => {
3017
+ e.stopPropagation();
3018
+ const inp = document.getElementById('goalInput');
3019
+ if (inp) inp.value = decodeURIComponent(b.getAttribute('data-prompt'));
3020
+ switchView('chat');
3021
+ if (inp) inp.focus();
3022
+ toast('已填入提示词');
3023
+ }));
3024
+ } catch (e) { document.getElementById('officeGrid').innerHTML = '<div class="empty" style="color:var(--err)">加载失败:' + escapeHtml(e.message) + '</div>'; }
3025
+ }
3026
+
3027
+ document.querySelectorAll('[data-close]').forEach((el) => el.addEventListener('click', () => closeModal(el.getAttribute('data-close'))));
3028
+ document.querySelectorAll('.mask').forEach((m) => m.addEventListener('click', (e) => { if (e.target === m) m.classList.remove('show'); }));
3029
+ // 文件夹选择器点击事件(只绑定一次,避免累积)
3030
+ document.getElementById('fpTree')?.addEventListener('click', async (e) => {
3031
+ const driveOption = e.target.closest('.drive-option');
3032
+ if (driveOption) {
3033
+ e.stopPropagation();
3034
+ const drive = driveOption.getAttribute('data-drive');
3035
+ console.log('[folderPicker] 选择驱动器:', drive);
3036
+ try { await loadFolderPicker(drive); } catch (err) { toast('读取失败:' + err.message); }
3037
+ return;
3038
+ }
3039
+ const item = e.target.closest('.file-item');
3040
+ if (!item) return;
3041
+ const type = item.getAttribute('data-type');
3042
+ const path = item.getAttribute('data-path');
3043
+ console.log('[folderPicker] 点击:', type, path);
3044
+ if (type === 'dir' || type === 'up') {
3045
+ try { await loadFolderPicker(path); } catch (e) { toast('读取失败:' + e.message); }
3046
+ } else if (type) {
3047
+ previewFile(path);
3048
+ }
3049
+ });
3050
+ function startRefresh() { setInterval(loadTasks, 5000); }
3051
+
3052
+ /* ========== 记忆系统 ========== */
3053
+ async function loadMemoryStats() {
3054
+ try {
3055
+ const d = await api('/api/memory/stats');
3056
+ document.getElementById('statShortDays').textContent = d.shortTermFiles + ' 天';
3057
+ document.getElementById('statLongNotes').textContent = d.longTermNotes + ' 条';
3058
+ document.getElementById('statLastSum').textContent = d.lastSummarize || '暂无';
3059
+ // 计算下次自动总结时间(今晚24:00)
3060
+ const now = new Date();
3061
+ const next = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 0);
3062
+ document.getElementById('statNextSum').textContent = next.toLocaleString('zh-CN', { hour12: false });
3063
+ } catch (e) { console.warn('加载记忆统计失败', e); }
3064
+ }
3065
+
3066
+ async function loadMemoryContent(dateStr) {
3067
+ try {
3068
+ const d = await api('/api/memory/short?date=' + dateStr);
3069
+ const content = document.getElementById('memShortContent');
3070
+ if (!d || !d.content) {
3071
+ content.innerHTML = '<div class="empty">当天暂无记录</div>';
3072
+ return;
3073
+ }
3074
+ content.innerHTML = formatMarkdown(d.content);
3075
+ document.getElementById('memDateLabel').textContent = '📅 ' + dateStr;
3076
+ } catch (e) { toast('加载失败:' + e.message); }
3077
+ }
3078
+
3079
+ async function loadLongTermMemory() {
3080
+ try {
3081
+ const d = await api('/api/memory/long');
3082
+ const content = document.getElementById('memLongContent');
3083
+ if (!d || !d.content) {
3084
+ content.innerHTML = '<div class="empty">长期记忆为空</div>';
3085
+ return;
3086
+ }
3087
+ content.innerHTML = formatMarkdown(d.content);
3088
+ } catch (e) { toast('加载失败:' + e.message); }
3089
+ }
3090
+
3091
+ async function loadSummaryHistory() {
3092
+ try {
3093
+ const d = await api('/api/memory/history?limit=20');
3094
+ const content = document.getElementById('memHistoryContent');
3095
+ const list = d.history || [];
3096
+ if (!list.length) {
3097
+ content.innerHTML = '<div class="empty">暂无总结历史</div>';
3098
+ return;
3099
+ }
3100
+ content.innerHTML = list.map(h =>
3101
+ '<div style="padding:8px 0;border-bottom:1px solid var(--border);font-size:13px;">' +
3102
+ '<strong>' + h.date + '</strong> → ' + h.newEntries + ' 条新增, ' + h.updatedEntries + ' 条更新<br>' +
3103
+ '<span class="muted">' + h.timestamp + '</span>' +
3104
+ '</div>'
3105
+ ).join('');
3106
+ } catch (e) { toast('加载失败:' + e.message); }
3107
+ }
3108
+
3109
+ function formatMarkdown(text) {
3110
+ // 简单 Markdown 转换
3111
+ return text
3112
+ .replace(/&/g, '&amp;')
3113
+ .replace(/</g, '&lt;')
3114
+ .replace(/>/g, '&gt;')
3115
+ .replace(/^### (.*$)/gm, '<h3>$1</h3>')
3116
+ .replace(/^## (.*$)/gm, '<h2>$1</h2>')
3117
+ .replace(/^# (.*$)/gm, '<h1>$1</h1>')
3118
+ .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
3119
+ .replace(/\*(.*?)\*/g, '<em>$1</em>')
3120
+ .replace(/`(.*?)`/g, '<code>$1</code>')
3121
+ .replace(/\n/g, '<br>');
3122
+ }
3123
+
3124
+ function initMemoryUI() {
3125
+ // 设置默认日期为今天
3126
+ const today = new Date().toISOString().split('T')[0];
3127
+ document.getElementById('memDateInput').value = today;
3128
+ loadMemoryContent(today);
3129
+
3130
+ // Tab 切换
3131
+ document.querySelectorAll('.mem-tab').forEach(tab => {
3132
+ tab.addEventListener('click', () => {
3133
+ document.querySelectorAll('.mem-tab').forEach(t => t.classList.remove('active'));
3134
+ document.querySelectorAll('.mem-panel').forEach(p => p.classList.toggle('active', false));
3135
+ tab.classList.add('active');
3136
+ const panelId = tab.getAttribute('data-memtab') + 'Panel';
3137
+ document.getElementById(panelId)?.classList.add('active');
3138
+ });
3139
+ });
3140
+
3141
+ // 查看按钮
3142
+ document.getElementById('memLoadDay')?.addEventListener('click', () => {
3143
+ const dateStr = document.getElementById('memDateInput').value;
3144
+ if (dateStr) loadMemoryContent(dateStr);
3145
+ });
3146
+
3147
+ // 刷新按钮
3148
+ document.getElementById('memRefreshBtn')?.addEventListener('click', () => {
3149
+ loadMemoryStats();
3150
+ const dateStr = document.getElementById('memDateInput').value;
3151
+ if (dateStr) loadMemoryContent(dateStr);
3152
+ loadLongTermMemory();
3153
+ });
3154
+
3155
+ // 立即总结按钮
3156
+ document.getElementById('memSummarizeBtn')?.addEventListener('click', async () => {
3157
+ const btn = document.getElementById('memSummarizeBtn');
3158
+ btn.disabled = true;
3159
+ btn.textContent = '⏳ 总结中...';
3160
+ try {
3161
+ const dateStr = document.getElementById('memDateInput').value || today;
3162
+ const d = await api('/api/memory/summarize', 'POST', { date: dateStr });
3163
+ toast('✅ ' + (d.message || '总结完成'));
3164
+ loadMemoryStats();
3165
+ loadSummaryHistory();
3166
+ } catch (e) {
3167
+ toast('总结失败:' + e.message);
3168
+ } finally {
3169
+ btn.disabled = false;
3170
+ btn.textContent = '✨ 立即总结';
3171
+ }
3172
+ });
3173
+
3174
+ // 初始加载
3175
+ loadMemoryStats();
3176
+ loadLongTermMemory();
3177
+ loadSummaryHistory();
3178
+ }
3179
+
3180
+ // 当切换到记忆页面时初始化
3181
+ const origSwitchView = switchView;
3182
+ window.switchView = function(nav) {
3183
+ origSwitchView(nav);
3184
+ if (nav === 'memory') {
3185
+ setTimeout(initMemoryUI, 100);
3186
+ }
3187
+ };
3188
+
3189
+ if (state.token && state.phone) {
3190
+ document.getElementById('loginOverlay').style.display = 'none';
3191
+ document.getElementById('appLayout').classList.add('show');
3192
+ afterLogin();
3193
+ }
3194
+ </script>
3195
+ </body>
3196
+ </html>