dsh-miloco-bridge 0.1.0 → 0.1.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.
package/lib/client.js CHANGED
@@ -40,55 +40,202 @@ window.__ModuleLoader__.load({
40
40
  const style = document.createElement('style')
41
41
  style.id = STYLE_ID
42
42
  style.textContent = `
43
- .dsh-miloco-bridge-settings { font-size: 13px; line-height: 1.6; max-width: 780px; }
44
- .dsh-miloco-bridge-settings h3 { font-size: 14px; margin: 0 0 8px; font-weight: 600; }
45
- .dsh-miloco-bridge-settings .card { border: 1px solid rgba(128,128,128,.32); border-radius: 8px; padding: 12px 14px; margin-bottom: 14px; }
46
- .dsh-miloco-bridge-settings .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 8px 14px; margin-top: 6px; }
47
- .dsh-miloco-bridge-settings .stat { display: flex; flex-direction: column; }
48
- .dsh-miloco-bridge-settings .stat .k { opacity: .66; font-size: 12px; }
49
- .dsh-miloco-bridge-settings .stat .v { font-size: 16px; font-weight: 600; }
43
+ .dsh-miloco-bridge-settings {
44
+ --mc-border: var(--dsw-alias-border-l2, rgba(128,128,128,.24));
45
+ --mc-border-soft: var(--dsw-alias-border-l1, rgba(128,128,128,.14));
46
+ --mc-bg: var(--dsw-alias-bg-l1, rgba(128,128,128,.035));
47
+ --mc-bg-soft: var(--dsw-alias-bg-l2, rgba(128,128,128,.07));
48
+ --mc-text: var(--dsw-alias-label-primary, inherit);
49
+ --mc-text-2: var(--dsw-alias-label-secondary, rgba(128,128,128,.95));
50
+ --mc-text-3: var(--dsw-alias-label-tertiary, rgba(128,128,128,.72));
51
+ --mc-accent: var(--dsw-alias-brand-primary, #4a7edc);
52
+ --mc-success: var(--dsw-alias-state-success-primary, #3c9a5a);
53
+ --mc-error: var(--dsw-alias-state-error-primary, #c85a50);
54
+ font-size: 13px; line-height: 1.65; max-width: 820px; color: var(--mc-text);
55
+ }
56
+ .dsh-miloco-bridge-settings h3 { font-size: 14px; margin: 0 0 10px; font-weight: 600; letter-spacing: .01em; }
57
+ .dsh-miloco-bridge-settings .cardHead { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 2px; }
58
+ .dsh-miloco-bridge-settings .cardHead h3 { margin: 0; }
59
+ .dsh-miloco-bridge-settings .iconBtn {
60
+ flex: none; display: inline-flex; align-items: center; justify-content: center;
61
+ width: 26px; height: 26px; padding: 0; border: 0; border-radius: 6px;
62
+ background: transparent; color: var(--mc-text-3);
63
+ }
64
+ .dsh-miloco-bridge-settings .iconBtn:hover:not(:disabled) {
65
+ background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); border-color: transparent;
66
+ }
67
+ .dsh-miloco-bridge-settings .iconBtn.spinning svg { animation: dsh-miloco-spin .9s linear infinite; }
68
+ @keyframes dsh-miloco-spin { to { transform: rotate(360deg); } }
69
+ .dsh-miloco-bridge-settings .card {
70
+ border: 1px solid var(--mc-border); border-radius: 10px; padding: 15px 16px;
71
+ margin-bottom: 12px; background: transparent;
72
+ }
73
+ .dsh-miloco-bridge-settings .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 8px; margin-top: 10px; }
74
+ .dsh-miloco-bridge-settings .stat {
75
+ display: flex; flex-direction: column; padding: 9px 11px; border-radius: 8px;
76
+ background: transparent; border: 1px solid var(--mc-border-soft);
77
+ }
78
+ .dsh-miloco-bridge-settings .stat .k { color: var(--mc-text-3); font-size: 11px; }
79
+ .dsh-miloco-bridge-settings .stat .v { font-size: 17px; font-weight: 600; line-height: 1.35; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
50
80
  .dsh-miloco-bridge-settings .row { display: flex; align-items: center; gap: 8px; margin: 8px 0; flex-wrap: wrap; }
51
- .dsh-miloco-bridge-settings .row > label { min-width: 96px; opacity: .8; }
52
- .dsh-miloco-bridge-settings select, .dsh-miloco-bridge-settings input {
53
- flex: 1 1 220px; min-width: 160px; padding: 5px 8px; border-radius: 6px;
54
- border: 1px solid rgba(128,128,128,.36); background: transparent; color: inherit; font: inherit;
81
+ .dsh-miloco-bridge-settings .row > label { min-width: 96px; color: var(--mc-text-2); }
82
+ .dsh-miloco-bridge-settings .field { display: flex; flex-direction: column; gap: 4px; margin: 10px 0; }
83
+ .dsh-miloco-bridge-settings .formGrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 4px 14px; margin: 10px 0; }
84
+ .dsh-miloco-bridge-settings .formGrid .field { margin: 0; }
85
+ .dsh-miloco-bridge-settings .field > label { color: var(--mc-text-2); font-size: 12px; }
86
+ .dsh-miloco-bridge-settings .fieldRow { display: flex; align-items: center; gap: 8px; }
87
+ .dsh-miloco-bridge-settings .fieldRow input:not([type="checkbox"]):not([type="radio"]) { flex: 1 1 auto; width: auto; min-width: 0; }
88
+ .dsh-miloco-bridge-settings .row > select,
89
+ .dsh-miloco-bridge-settings .row > input:not([type="checkbox"]):not([type="radio"]) { flex: 1 1 220px; width: auto; }
90
+ .dsh-miloco-bridge-settings .statusRow {
91
+ display: grid; grid-template-columns: 96px minmax(0, 1fr) auto; align-items: center;
92
+ gap: 10px; padding: 7px 0; border-bottom: 1px solid var(--mc-border-soft);
93
+ }
94
+ .dsh-miloco-bridge-settings .statusRow:last-of-type { border-bottom: 0; }
95
+ .dsh-miloco-bridge-settings .statusLabel { color: var(--mc-text-2); }
96
+ .dsh-miloco-bridge-settings .statusValue { min-width: 0; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
97
+ .dsh-miloco-bridge-settings .statusCheck { justify-self: end; color: var(--mc-success); font-weight: 700; font-size: 14px; line-height: 1; }
98
+ .dsh-miloco-bridge-settings .statusEnd { justify-self: end; }
99
+ .dsh-miloco-bridge-settings .statusValue > a, .dsh-miloco-bridge-settings .statusValue > code {
100
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%;
101
+ }
102
+ .dsh-miloco-bridge-settings select,
103
+ .dsh-miloco-bridge-settings input:not([type="checkbox"]):not([type="radio"]) {
104
+ flex: 0 0 auto; width: 100%; height: 34px; box-sizing: border-box;
105
+ padding: 6px 9px; border-radius: 7px; border: 1px solid var(--mc-border);
106
+ background: var(--dsw-alias-bg-base, transparent); color: inherit; font: inherit; outline: none;
107
+ }
108
+ .dsh-miloco-bridge-settings select { cursor: pointer; }
109
+ .dsh-miloco-bridge-settings input[type="checkbox"] {
110
+ flex: 0 0 auto; width: auto; min-width: 0; height: auto; margin: 2px 0 0;
111
+ accent-color: var(--mc-accent);
112
+ }
113
+ .dsh-miloco-bridge-settings select:focus, .dsh-miloco-bridge-settings input:focus {
114
+ border-color: var(--mc-accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--mc-accent) 18%, transparent);
55
115
  }
56
116
  .dsh-miloco-bridge-settings button {
57
- padding: 5px 14px; border-radius: 6px; border: 1px solid rgba(128,128,128,.36);
58
- background: transparent; color: inherit; font: inherit; cursor: pointer;
117
+ padding: 6px 12px; border-radius: 7px; border: 1px solid var(--mc-border);
118
+ background: var(--mc-bg-soft); color: inherit; font: inherit; cursor: pointer;
119
+ transition: background .15s ease, border-color .15s ease, opacity .15s ease;
59
120
  }
60
- .dsh-miloco-bridge-settings button.primary { border-color: rgba(70,130,220,.7); color: rgba(70,130,220,1); }
61
- .dsh-miloco-bridge-settings button:disabled { opacity: .5; cursor: default; }
121
+ .dsh-miloco-bridge-settings button:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.14)); border-color: var(--mc-text-3); }
122
+ .dsh-miloco-bridge-settings button.primary { background: var(--mc-accent); border-color: transparent; color: #fff; }
123
+ .dsh-miloco-bridge-settings button.primary:hover:not(:disabled) { opacity: .9; }
124
+ .dsh-miloco-bridge-settings button.dangerBtn { color: var(--mc-error); border-color: var(--mc-error); background: transparent; }
125
+ .dsh-miloco-bridge-settings button:disabled { opacity: .45; cursor: default; }
62
126
  .dsh-miloco-bridge-settings .pill {
63
- display: inline-block; padding: 1px 8px; border-radius: 999px; font-size: 12px;
64
- border: 1px solid rgba(128,128,128,.36);
127
+ display: inline-flex; align-items: center; padding: 1px 8px; border-radius: 999px;
128
+ font-size: 11.5px; line-height: 1.6; border: 1px solid var(--mc-border); color: var(--mc-text-2);
129
+ white-space: nowrap;
130
+ }
131
+ .dsh-miloco-bridge-settings .pill.ok { border-color: var(--mc-success); color: var(--mc-success); }
132
+ .dsh-miloco-bridge-settings .pill.bad { border-color: var(--mc-error); color: var(--mc-error); }
133
+ .dsh-miloco-bridge-settings .pill.warn { border-color: #b8860b; color: #b8860b; }
134
+ .dsh-miloco-bridge-settings .muted { color: var(--mc-text-3); font-size: 12px; }
135
+ .dsh-miloco-bridge-settings .modeList { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 8px; margin: 12px 0 8px; }
136
+ .dsh-miloco-bridge-settings .modeItem {
137
+ display: flex; align-items: flex-start; gap: 9px; margin: 0; padding: 10px 12px;
138
+ border: 1px solid var(--mc-border); border-radius: 9px; background: transparent;
139
+ cursor: pointer; transition: border-color .15s ease, background .15s ease;
65
140
  }
66
- .dsh-miloco-bridge-settings .pill.ok { border-color: rgba(60,160,90,.6); color: rgba(60,160,90,1); }
67
- .dsh-miloco-bridge-settings .pill.bad { border-color: rgba(200,90,80,.6); color: rgba(200,90,80,1); }
68
- .dsh-miloco-bridge-settings .pill.warn { border-color: rgba(210,150,50,.6); color: rgba(210,150,50,1); }
69
- .dsh-miloco-bridge-settings .muted { opacity: .68; font-size: 12px; }
70
- .dsh-miloco-bridge-settings .modeList { display: flex; flex-direction: column; gap: 4px; margin: 10px 0 6px; }
71
- .dsh-miloco-bridge-settings .modeItem { display: flex; align-items: flex-start; gap: 8px; margin: 0; padding: 6px 8px; border-radius: 6px; }
72
- .dsh-miloco-bridge-settings .modeItem:hover { background: rgba(128,128,128,.08); }
141
+ .dsh-miloco-bridge-settings .modeItem:hover { border-color: var(--mc-text-3); }
142
+ .dsh-miloco-bridge-settings .modeItem.selected { border-color: var(--mc-accent); background: var(--mc-bg-soft); }
73
143
  .dsh-miloco-bridge-settings .modeItem input[type="radio"] {
74
144
  flex: 0 0 auto; min-width: 0; width: auto; margin: 2px 0 0; padding: 0;
75
- border: 0; background: transparent; accent-color: rgba(70,130,220,1);
145
+ border: 0; background: transparent; accent-color: var(--mc-accent);
76
146
  }
77
- .dsh-miloco-bridge-settings .modeText { display: flex; flex: 1 1 auto; flex-direction: column; gap: 2px; min-width: 0; text-align: left; }
147
+ .dsh-miloco-bridge-settings .modeText { display: flex; flex: 1 1 auto; flex-direction: column; gap: 3px; min-width: 0; text-align: left; }
148
+ .dsh-miloco-bridge-settings .modeText strong { font-size: 13px; }
78
149
  .dsh-miloco-bridge-settings .modeText .muted { margin: 0; }
79
- .dsh-miloco-bridge-settings a { color: rgba(70,130,220,1); text-decoration: none; }
150
+ .dsh-miloco-bridge-settings a { color: var(--mc-accent); text-decoration: none; }
80
151
  .dsh-miloco-bridge-settings a:hover { text-decoration: underline; }
81
- .dsh-miloco-bridge-settings .danger { border: 1px solid rgba(200,90,80,.5); border-radius: 8px; padding: 10px 12px; margin-top: 10px; }
82
- .dsh-miloco-bridge-settings .danger h4 { margin: 0 0 6px; font-size: 13px; color: rgba(200,90,80,1); }
152
+ .dsh-miloco-bridge-settings .danger { border: 1px solid var(--mc-error); border-radius: 8px; padding: 10px 12px; margin-top: 10px; background: transparent; }
153
+ .dsh-miloco-bridge-settings .danger h4 { margin: 0 0 6px; font-size: 13px; color: var(--mc-error); }
83
154
  .dsh-miloco-bridge-settings .danger label { display: flex; gap: 6px; align-items: flex-start; margin: 6px 0; font-size: 12px; }
84
- .dsh-miloco-bridge-settings .guide { margin: 8px 0 0; padding: 10px 12px; border-radius: 6px; border: 1px solid rgba(128,128,128,.28); background: rgba(128,128,128,.06); }
155
+ .dsh-miloco-bridge-settings .guide { margin: 8px 0 0; padding: 10px 12px; border-radius: 7px; border: 1px solid var(--mc-border-soft); background: transparent; }
85
156
  .dsh-miloco-bridge-settings .guide h4 { margin: 8px 0 4px; font-size: 13px; }
86
157
  .dsh-miloco-bridge-settings .guide ul { margin: 4px 0; padding-left: 18px; }
87
158
  .dsh-miloco-bridge-settings .guide li { margin: 2px 0; }
159
+ .dsh-miloco-bridge-settings .versionSub {
160
+ display: flex; flex-wrap: wrap; align-items: center; flex-basis: 100%;
161
+ min-width: 0; color: var(--mc-text-3); font-size: 12px; line-height: 1.6;
162
+ }
163
+ .dsh-miloco-bridge-settings .versionSub > * + *::before {
164
+ content: '·'; margin: 0 6px; color: var(--mc-text-3);
165
+ }
166
+ .dsh-miloco-bridge-settings .versionSub .versionLink {
167
+ border: 0; background: transparent; padding: 0; border-radius: 0;
168
+ color: var(--mc-accent); font: inherit; line-height: inherit; cursor: pointer;
169
+ }
170
+ .dsh-miloco-bridge-settings .versionSub .versionLink:hover:not(:disabled) {
171
+ background: transparent; text-decoration: underline;
172
+ }
173
+ .dsh-miloco-bridge-settings .versionDirty { color: #b8860b; }
174
+ .dsh-miloco-bridge-settings .versionNote { margin-top: 6px; }
175
+ .dsh-miloco-bridge-settings .versionManualPanel { margin-top: 8px; }
176
+ .dsh-miloco-bridge-settings .versionCommand {
177
+ margin: 0; padding: 8px 10px; border-radius: 6px; max-height: 120px; overflow: auto;
178
+ border: 1px solid var(--mc-border-soft); background: var(--dsw-alias-bg-base, transparent);
179
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
180
+ font-size: 11.5px; line-height: 1.5; white-space: pre-wrap; word-break: break-word;
181
+ }
88
182
  .dsh-miloco-bridge-settings code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
183
+ .dsh-miloco-preset-helpbar { display: inline-flex; align-items: center; gap: 2px; margin-right: auto; }
184
+ .dsh-miloco-preset-helpbtn {
185
+ border: 0; background: transparent; color: var(--dsw-alias-label-secondary, rgba(128,128,128,.95));
186
+ font: inherit; font-size: 12px; line-height: 1.5; padding: 2px 6px; border-radius: 5px; cursor: pointer;
187
+ }
188
+ .dsh-miloco-preset-helpbtn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
189
+ .dsh-miloco-guide-backdrop {
190
+ position: fixed; inset: 0; z-index: 10000; display: flex; align-items: center; justify-content: center;
191
+ padding: 24px; background: rgba(0,0,0,.24);
192
+ }
193
+ .dsh-miloco-guide-modal {
194
+ display: flex; flex-direction: column; width: min(640px, 100%); height: min(600px, 100%);
195
+ max-height: 100%; overflow: hidden; padding: 0; border-radius: 16px; border: 0;
196
+ background: var(--dsw-alias-bg-base, #fff); color: var(--dsw-alias-label-primary, #111);
197
+ box-shadow: 0 18px 56px rgba(0,0,0,.22);
198
+ }
199
+ .dsh-miloco-guide-header { flex: none; padding: 24px 24px 20px 28px; }
200
+ .dsh-miloco-guide-title-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
201
+ .dsh-miloco-guide-title { margin: 0; font-size: 20px; font-weight: 600; line-height: 28px; }
202
+ .dsh-miloco-guide-intro { margin: 8px 0 0; color: var(--dsw-alias-label-secondary, rgba(128,128,128,.95)); font-size: 14px; line-height: 22px; }
203
+ .dsh-miloco-guide-close {
204
+ flex: none; width: 32px; height: 32px; border: 0; border-radius: 8px; padding: 0;
205
+ background: transparent; color: var(--dsw-alias-label-tertiary, rgba(128,128,128,.8));
206
+ font: inherit; font-size: 22px; line-height: 1; cursor: pointer;
207
+ }
208
+ .dsh-miloco-guide-close:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
209
+ .dsh-miloco-guide-tabs {
210
+ flex: none; display: grid; grid-template-columns: 1fr 1fr; gap: 4px;
211
+ margin: 0 28px 22px; padding: 4px; border-radius: 10px;
212
+ background: var(--dsw-alias-bg-l2, #f5f5f5);
213
+ }
214
+ .dsh-miloco-guide-tab {
215
+ border: 1px solid transparent; border-radius: 8px; padding: 7px 10px;
216
+ background: transparent; color: var(--dsw-alias-label-secondary, rgba(128,128,128,.95));
217
+ font: inherit; font-size: 14px; line-height: 20px; cursor: pointer;
218
+ }
219
+ .dsh-miloco-guide-tab.selected {
220
+ background: var(--dsw-alias-bg-base, #fff);
221
+ border-color: var(--dsw-alias-border-l2, rgba(128,128,128,.24));
222
+ color: var(--dsw-alias-label-primary, #111);
223
+ box-shadow: 0 1px 2px rgba(0,0,0,.04);
224
+ }
225
+ .dsh-miloco-guide-panel {
226
+ flex: 1; min-height: 0; overflow-y: auto; padding: 0 24px 24px 28px;
227
+ color: var(--dsw-alias-label-primary, #111); font-size: 16px; overflow-wrap: anywhere;
228
+ }
229
+ .dsh-miloco-guide-panel h3 { margin: 28px 0 10px; font-size: 16px; font-weight: 600; line-height: 24px; }
230
+ .dsh-miloco-guide-panel h3:first-child { margin-top: 0; }
231
+ .dsh-miloco-guide-panel p { margin: 0 0 16px; font-size: 16px; line-height: 1.625; }
232
+ .dsh-miloco-guide-panel ul { margin: 0 0 16px; padding-left: 22px; }
233
+ .dsh-miloco-guide-panel li { margin: 6px 0; font-size: 16px; line-height: 1.6; }
234
+ .dsh-miloco-guide-panel blockquote { margin: 10px 0 14px; padding: 12px 16px; border: 0; border-radius: 10px; background: var(--dsw-alias-bg-module-platform, var(--dsw-alias-bg-l2, #f5f5f5)); }
235
+ .dsh-miloco-guide-panel blockquote p { margin: 0; }
89
236
  .dsh-miloco-bridge-settings pre {
90
237
  margin: 8px 0 0; padding: 8px 10px; border-radius: 6px; max-height: 260px; overflow: auto;
91
- border: 1px solid rgba(128,128,128,.28); font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
238
+ border: 1px solid var(--mc-border-soft); font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
92
239
  font-size: 11.5px; line-height: 1.5; white-space: pre-wrap;
93
240
  }
94
241
  `
@@ -169,6 +316,157 @@ window.__ModuleLoader__.load({
169
316
  return options
170
317
  }
171
318
 
319
+ /**
320
+ * DSH 0.2.1 only ships guide dialogs for built-in presets. Add equivalent
321
+ * explanation/usage buttons to the custom Miloco preset card so the same
322
+ * help is reachable from 「设置 → Agent 预设」. This is a defensive DOM
323
+ * enhancement: if DSH changes the card structure, it silently does nothing.
324
+ */
325
+ const PRESET_GUIDE = {
326
+ title: 'Miloco 家庭助手',
327
+ intro: {
328
+ explanation: '只读和管家两种模式决定这个预设能做什么。',
329
+ usage: '新建任务时选择「Miloco 家庭助手」,说明要完成什么、涉及哪个房间或家人。',
330
+ },
331
+ content: {
332
+ explanation: [
333
+ '<h3>保留哪些能力</h3>',
334
+ '<p>保留 DSH 通用 Agent 的对话与工具调用;米家设备、摄像头、感知、规则/任务与家庭档案',
335
+ '是它独有的能力,且只在这个预设内出现。</p>',
336
+ '<h3>什么时候选</h3>',
337
+ '<p>想要联动家居设备时:只需要了解家里情况时选只读;需要操作设备或维护家庭信息时选管家。',
338
+ '涉及门锁、燃气、摄像头开关等动作时,技能层会要求二次确认。</p>',
339
+ '<p>模式说明与示例见「设置 → 插件 → Miloco」。</p>',
340
+ ].join(''),
341
+ usage: [
342
+ '<h3>查看家里情况</h3>',
343
+ '<blockquote><p>客厅现在有人吗?刚才门口发生了什么?</p></blockquote>',
344
+ '<p>用摄像头画面和感知事件回答,不控制设备。</p>',
345
+ '<h3>操作设备或执行场景</h3>',
346
+ '<blockquote><p>把客厅灯打开;执行回家模式。</p></blockquote>',
347
+ '<p>需要管家模式;控制前会先确认设备与参数。</p>',
348
+ '<h3>维护家庭信息</h3>',
349
+ '<blockquote><p>记一下孩子每天喝几杯水;奶奶对什么过敏?</p></blockquote>',
350
+ '<p>用于家庭档案、成员与宠物登记,以及任务创建。</p>',
351
+ ].join(''),
352
+ },
353
+ }
354
+
355
+ /**
356
+ * Open a read-only help dialog matching DSH's native preset guide layout.
357
+ * @param {'explanation'|'usage'} kind - Initial guide page.
358
+ */
359
+ function openPresetGuide(kind) {
360
+ if (typeof document === 'undefined' || !document.body) return
361
+ document.querySelector('.dsh-miloco-guide-backdrop')?.remove()
362
+ const backdrop = document.createElement('div')
363
+ backdrop.className = 'dsh-miloco-guide-backdrop'
364
+ const modal = document.createElement('div')
365
+ modal.className = 'dsh-miloco-guide-modal'
366
+ modal.setAttribute('role', 'dialog')
367
+ modal.setAttribute('aria-modal', 'true')
368
+
369
+ const header = document.createElement('div')
370
+ header.className = 'dsh-miloco-guide-header'
371
+ const titleRow = document.createElement('div')
372
+ titleRow.className = 'dsh-miloco-guide-title-row'
373
+ const title = document.createElement('h2')
374
+ title.className = 'dsh-miloco-guide-title'
375
+ title.textContent = PRESET_GUIDE.title
376
+ const close = document.createElement('button')
377
+ close.type = 'button'
378
+ close.className = 'dsh-miloco-guide-close'
379
+ close.textContent = '×'
380
+ close.setAttribute('aria-label', '关闭')
381
+ const intro = document.createElement('p')
382
+ intro.className = 'dsh-miloco-guide-intro'
383
+ titleRow.append(title, close)
384
+ header.append(titleRow, intro)
385
+
386
+ const tabs = document.createElement('div')
387
+ tabs.className = 'dsh-miloco-guide-tabs'
388
+ const tabExplanation = document.createElement('button')
389
+ tabExplanation.type = 'button'
390
+ tabExplanation.className = 'dsh-miloco-guide-tab'
391
+ tabExplanation.textContent = '模式说明'
392
+ const tabUsage = document.createElement('button')
393
+ tabUsage.type = 'button'
394
+ tabUsage.className = 'dsh-miloco-guide-tab'
395
+ tabUsage.textContent = '如何使用'
396
+ tabs.append(tabExplanation, tabUsage)
397
+
398
+ const panel = document.createElement('div')
399
+ panel.className = 'dsh-miloco-guide-panel'
400
+
401
+ const setPage = next => {
402
+ const active = next === 'usage' ? 'usage' : 'explanation'
403
+ panel.innerHTML = PRESET_GUIDE.content[active]
404
+ panel.dataset.guidePage = active
405
+ intro.textContent = PRESET_GUIDE.intro[active]
406
+ tabExplanation.classList.toggle('selected', active === 'explanation')
407
+ tabUsage.classList.toggle('selected', active === 'usage')
408
+ }
409
+ tabExplanation.addEventListener('click', () => setPage('explanation'))
410
+ tabUsage.addEventListener('click', () => setPage('usage'))
411
+ setPage(kind)
412
+
413
+ modal.append(header, tabs, panel)
414
+ backdrop.append(modal)
415
+
416
+ const dismiss = () => {
417
+ backdrop.remove()
418
+ document.removeEventListener('keydown', onKey)
419
+ }
420
+ const onKey = event => {
421
+ if (event.key === 'Escape') dismiss()
422
+ }
423
+ close.addEventListener('click', dismiss)
424
+ backdrop.addEventListener('click', event => {
425
+ if (event.target === backdrop) dismiss()
426
+ })
427
+ document.addEventListener('keydown', onKey)
428
+ document.body.append(backdrop)
429
+ close.focus()
430
+ }
431
+
432
+ /**
433
+ * Add help buttons to the custom Miloco preset card when it appears.
434
+ * @param {any} ctx - Browser plugin context.
435
+ */
436
+ function installPresetGuide(ctx) {
437
+ if (typeof document === 'undefined' || !document.body) return
438
+ const enhance = () => {
439
+ const card = document.querySelector('[data-agent-preset-id="miloco"]')
440
+ if (!card || card.dataset.dshMilocoGuide === '1') return
441
+ const foot = card.querySelector('[class*="cardFoot"]')
442
+ if (!foot || foot.querySelector('.dsh-miloco-preset-helpbar')) return
443
+ const bar = document.createElement('span')
444
+ bar.className = 'dsh-miloco-preset-helpbar'
445
+ const makeButton = (label, kind) => {
446
+ const button = document.createElement('button')
447
+ button.type = 'button'
448
+ button.className = 'dsh-miloco-preset-helpbtn'
449
+ button.textContent = label
450
+ button.setAttribute('aria-label', `${label}: Miloco 家庭助手`)
451
+ button.addEventListener('click', event => {
452
+ event.preventDefault()
453
+ event.stopPropagation()
454
+ openPresetGuide(kind)
455
+ })
456
+ return button
457
+ }
458
+ bar.append(makeButton('模式说明', 'explanation'), makeButton('如何使用', 'usage'))
459
+ foot.insertBefore(bar, foot.firstChild)
460
+ card.dataset.dshMilocoGuide = '1'
461
+ }
462
+ const observer = new MutationObserver(enhance)
463
+ observer.observe(document.body, { childList: true, subtree: true })
464
+ enhance()
465
+ try {
466
+ ctx.effect?.(() => () => observer.disconnect(), 'dsh-miloco: preset guide')
467
+ } catch { /* 客户端页面卸载时忽略 */ }
468
+ }
469
+
172
470
  function MilocoSettingsTab() {
173
471
  const [status, setStatus] = React.useState(null)
174
472
  const [error, setError] = React.useState(null)
@@ -191,6 +489,14 @@ window.__ModuleLoader__.load({
191
489
  const [confirmText, setConfirmText] = React.useState('')
192
490
  const [uninstallBusy, setUninstallBusy] = React.useState(false)
193
491
  const [uninstallResult, setUninstallResult] = React.useState(null)
492
+ const [versionInfo, setVersionInfo] = React.useState(null)
493
+ const [versionBusy, setVersionBusy] = React.useState(false)
494
+ const [versionError, setVersionError] = React.useState(null)
495
+ const [versionCommandOpen, setVersionCommandOpen] = React.useState(false)
496
+ const [upgradeStatus, setUpgradeStatus] = React.useState(null)
497
+ const [upgradeConfirmOpen, setUpgradeConfirmOpen] = React.useState(false)
498
+ const [upgradeBusy, setUpgradeBusy] = React.useState(false)
499
+ const [upgradePolling, setUpgradePolling] = React.useState(false)
194
500
 
195
501
  const reload = React.useCallback(() => {
196
502
  setError(null)
@@ -209,7 +515,74 @@ window.__ModuleLoader__.load({
209
515
  .catch((cause) => setError(cause instanceof Error ? cause.message : String(cause)))
210
516
  }, [])
211
517
 
212
- React.useEffect(() => { reload() }, [reload])
518
+ const loadVersion = React.useCallback((refresh = false) => {
519
+ setVersionBusy(true)
520
+ setVersionError(null)
521
+ call(`/version${refresh ? '?refresh=1' : ''}`)
522
+ .then((payload) => {
523
+ setVersionInfo(payload)
524
+ setUpgradeStatus(payload.upgradeStatus ?? null)
525
+ if (payload.error) setVersionError(payload.error)
526
+ })
527
+ .catch((cause) => setVersionError(cause instanceof Error ? cause.message : String(cause)))
528
+ .finally(() => setVersionBusy(false))
529
+ }, [])
530
+
531
+ const pollUpgrade = React.useCallback(() => {
532
+ setUpgradePolling(true)
533
+ const started = Date.now()
534
+ const tick = () => {
535
+ call('/upgrade/status')
536
+ .then((payload) => {
537
+ const status = payload.status
538
+ setUpgradeStatus(status)
539
+ if (status && ['done', 'failed', 'idle'].includes(status.phase)) {
540
+ setUpgradePolling(false)
541
+ loadVersion(true)
542
+ return
543
+ }
544
+ if (Date.now() - started > 5 * 60_000) {
545
+ setUpgradePolling(false)
546
+ loadVersion(true)
547
+ return
548
+ }
549
+ window.setTimeout(tick, 3000)
550
+ })
551
+ .catch(() => {
552
+ // The upgrade restarts the backend; treat this as the restart phase.
553
+ setUpgradeStatus({ phase: 'restarting' })
554
+ if (Date.now() - started > 5 * 60_000) {
555
+ setUpgradePolling(false)
556
+ loadVersion(true)
557
+ return
558
+ }
559
+ window.setTimeout(tick, 3000)
560
+ })
561
+ }
562
+ tick()
563
+ }, [loadVersion])
564
+
565
+ const runUpgrade = React.useCallback(() => {
566
+ setUpgradeBusy(true)
567
+ setVersionError(null)
568
+ call('/upgrade', {
569
+ method: 'POST',
570
+ headers: { 'content-type': 'application/json' },
571
+ body: JSON.stringify({}),
572
+ })
573
+ .then(() => {
574
+ setUpgradeConfirmOpen(false)
575
+ setUpgradeStatus({ phase: 'starting' })
576
+ pollUpgrade()
577
+ })
578
+ .catch((cause) => setVersionError(cause instanceof Error ? cause.message : String(cause)))
579
+ .finally(() => setUpgradeBusy(false))
580
+ }, [pollUpgrade])
581
+
582
+ React.useEffect(() => {
583
+ reload()
584
+ loadVersion(false)
585
+ }, [reload, loadVersion])
213
586
 
214
587
  const refreshSessions = React.useCallback(() => {
215
588
  call('/sessions')
@@ -371,7 +744,8 @@ window.__ModuleLoader__.load({
371
744
  onChange: (event) => setConnectPushToken(event.target.value),
372
745
  })),
373
746
  h('div', { className: 'row' },
374
- h('button', { type: 'button', disabled: busy || !connectUrl, onClick: connect }, '连接')))
747
+ h('button', { type: 'button', onClick: () => setConnectOpen(false) }, '取消'),
748
+ h('button', { type: 'button', className: 'primary', disabled: busy || !connectUrl, onClick: connect }, '连接')))
375
749
  : null
376
750
 
377
751
  // ---------------------------------------------------------------- 部署 / 连接
@@ -384,8 +758,15 @@ window.__ModuleLoader__.load({
384
758
  h('div', { className: 'row' },
385
759
  h('button', { type: 'button', className: 'primary', disabled: busy, onClick: deploy },
386
760
  busy ? '部署中…' : '一键部署 Miloco'),
387
- h('button', { type: 'button', disabled: busy, onClick: () => setConnectOpen(!connectOpen) },
388
- connectOpen ? '收起' : '连接已有的 Miloco')),
761
+ h('button', {
762
+ type: 'button',
763
+ disabled: busy,
764
+ onClick: () => {
765
+ const opening = !connectOpen
766
+ setConnectOpen(opening)
767
+ if (opening) setUninstallOpen(false)
768
+ },
769
+ }, '连接已有的 Miloco')),
389
770
  connectForm,
390
771
  deployLog ? h('pre', null, deployLog) : null))
391
772
  }
@@ -397,7 +778,7 @@ window.__ModuleLoader__.load({
397
778
  h('div', { className: 'grid' },
398
779
  h('div', { className: 'stat' },
399
780
  h('span', { className: 'k' }, '米家账号'),
400
- h('span', { className: 'v' }, overview.account?.nickname || (overview.accountBound ? '已绑定' : '未绑定'))),
781
+ h('span', { className: 'v', title: overview.account?.nickname || '' }, overview.account?.nickname || (overview.accountBound ? '已绑定' : '未绑定'))),
401
782
  h('div', { className: 'stat' },
402
783
  h('span', { className: 'k' }, '设备'),
403
784
  h('span', { className: 'v' }, overview.deviceCount ?? '—')),
@@ -430,7 +811,7 @@ window.__ModuleLoader__.load({
430
811
  h('div', { className: 'muted' }, '模式决定这个预设手里有哪些工具:'),
431
812
  h('div', { className: 'modeList' }, modes.map(item => h('label', {
432
813
  key: item.id,
433
- className: 'modeItem',
814
+ className: `modeItem${mode === item.id ? ' selected' : ''}`,
434
815
  },
435
816
  h('input', {
436
817
  type: 'radio', name: 'dsh-miloco-bridge-mode', checked: mode === item.id,
@@ -478,60 +859,62 @@ window.__ModuleLoader__.load({
478
859
  : ''
479
860
  cards.push(h('div', { className: 'card', key: 'target' },
480
861
  h('h3', null, '推送目标'),
481
- h('div', { className: 'row' },
862
+ h('div', { className: 'field' },
482
863
  h('label', null, '对话会话'),
483
- h('input', {
484
- type: 'text',
485
- list: SESSION_LIST_ID,
486
- value: sessionId,
487
- placeholder: lastSession
488
- ? `留空 = 上一次触发 miloco 的会话(${lastSession})`
489
- : '留空 = 上一次触发 miloco 的会话(还没有)',
490
- onChange: (event) => setSessionId(event.target.value),
491
- }),
492
- h('datalist', { id: SESSION_LIST_ID }, sessionOptions(status)),
493
- h('button', { type: 'button', disabled: busy, onClick: refreshSessions }, '刷新会话')),
864
+ h('div', { className: 'fieldRow' },
865
+ h('input', {
866
+ type: 'text',
867
+ list: SESSION_LIST_ID,
868
+ value: sessionId,
869
+ placeholder: lastSession
870
+ ? `留空 = 上一次触发 miloco 的会话(${lastSession})`
871
+ : '留空 = 上一次触发 miloco 的会话(还没有)',
872
+ onChange: (event) => setSessionId(event.target.value),
873
+ }),
874
+ h('datalist', { id: SESSION_LIST_ID }, sessionOptions(status)),
875
+ h('button', { type: 'button', disabled: busy, onClick: refreshSessions }, '刷新会话'))),
494
876
  h('div', { className: 'muted' },
495
877
  '留空 = 投到「上一次触发 miloco 的会话」,也就是最近一次调用过米家工具的对话。'
496
878
  + '也可以从下拉里选任意对话,或直接填会话 id。'),
497
- h('div', { className: 'row' },
498
- h('label', null, '模型'),
499
- h('select', {
500
- value: modelValue,
501
- onChange: (event) => {
502
- setModelValue(event.target.value)
503
- setReasoningEffort('')
504
- },
505
- }, modelOptions(status))),
506
- selectedEfforts.length > 0
507
- ? h('div', { className: 'row' },
508
- h('label', null, '推理强度'),
879
+ h('div', { className: 'formGrid' },
880
+ h('div', { className: 'field' },
881
+ h('label', null, '模型'),
509
882
  h('select', {
510
- value: effortValue,
511
- onChange: (event) => setReasoningEffort(event.target.value),
883
+ value: modelValue,
884
+ onChange: (event) => {
885
+ setModelValue(event.target.value)
886
+ setReasoningEffort('')
887
+ },
888
+ }, modelOptions(status))),
889
+ selectedEfforts.length > 0
890
+ ? h('div', { className: 'field' },
891
+ h('label', null, '推理强度'),
892
+ h('select', {
893
+ value: effortValue,
894
+ onChange: (event) => setReasoningEffort(event.target.value),
895
+ }, [
896
+ h('option', { key: '__model_default__', value: '' }, '跟随模型默认'),
897
+ ...selectedEfforts.map(item => h('option', {
898
+ key: item.id,
899
+ value: item.id,
900
+ }, item.name || item.id)),
901
+ ]))
902
+ : null,
903
+ h('div', { className: 'field' },
904
+ h('label', null, '感知等级'),
905
+ h('select', {
906
+ value: perceptionLevel,
907
+ onChange: (event) => setPerceptionLevel(event.target.value),
512
908
  }, [
513
- h('option', { key: '__model_default__', value: '' }, '跟随模型默认'),
514
- ...selectedEfforts.map(item => h('option', {
515
- key: item.id,
516
- value: item.id,
517
- }, item.name || item.id)),
518
- ]))
519
- : null,
520
- h('div', { className: 'row' },
521
- h('label', null, '感知等级'),
522
- h('select', {
523
- value: perceptionLevel,
524
- onChange: (event) => setPerceptionLevel(event.target.value),
525
- }, [
526
- h('option', { key: 'all', value: 'all' }, '全部事件'),
527
- h('option', { key: 'low', value: 'low' }, '低及以上'),
528
- h('option', { key: 'medium', value: 'medium' }, '中及以上'),
529
- h('option', { key: 'high', value: 'high' }, '仅高优先级'),
530
- ])),
909
+ h('option', { key: 'all', value: 'all' }, '全部事件'),
910
+ h('option', { key: 'low', value: 'low' }, '低及以上'),
911
+ h('option', { key: 'medium', value: 'medium' }, '中及以上'),
912
+ h('option', { key: 'high', value: 'high' }, '仅高优先级'),
913
+ ]))),
531
914
  h('div', { className: 'muted' },
532
915
  '低于所选等级的感知建议不会投递到 DSH;规则、任务、onboarding 与普通对话事件不受影响。'),
533
916
  h('div', { className: 'row' },
534
- h('button', { type: 'button', disabled: busy, onClick: save }, busy ? '保存中…' : '保存'),
917
+ h('button', { type: 'button', className: 'primary', disabled: busy, onClick: save }, busy ? '保存中…' : '保存'),
535
918
  saved ? h('span', { className: 'pill ok' }, saved) : null,
536
919
  error ? h('span', { className: 'pill bad' }, error) : null),
537
920
  h('div', { className: 'muted' },
@@ -539,47 +922,212 @@ window.__ModuleLoader__.load({
539
922
 
540
923
  // ---------------------------------------------------------------- 运行状态
541
924
  const deployState = status.deploy ?? {}
925
+ const versionData = versionInfo?.version ?? null
926
+ const upgradeInfo = versionInfo?.upgrade ?? null
927
+ const currentVersion = upgradeInfo?.current || versionData?.version || ''
928
+ const latestVersion = upgradeInfo?.latest || ''
929
+ const deployKind = upgradeInfo?.deploy_kind || ''
930
+ const hasUpdate = Boolean(upgradeInfo?.has_update)
931
+ const isDevDeployment = deployKind === 'dev'
932
+ const releaseUrl = upgradeInfo?.release_url || ''
933
+ const sourceRoot = versionInfo?.sourceRoot || deployState.sourceDir || ''
934
+ const gitDirty = Boolean(versionData?.git?.dirty)
935
+ const commitShort = versionData?.git?.commit_short || ''
936
+ const upgradePhase = upgradeStatus?.phase || ''
937
+ const upgradePhaseLabels = {
938
+ idle: '空闲',
939
+ starting: '准备中',
940
+ downloading: '下载中',
941
+ installing: '安装中',
942
+ restarting: '服务重启中',
943
+ done: '已完成',
944
+ failed: '失败',
945
+ }
946
+ const devUpdateCommand = sourceRoot
947
+ ? `cd ${sourceRoot} && git pull --ff-only && bash scripts/install.sh --dev --skip-openclaw && systemctl --user restart miloco.service`
948
+ : ''
949
+ const versionPill = versionInfo?.reachable === false
950
+ ? ['bad', '不可用']
951
+ : !versionInfo
952
+ ? ['', versionBusy ? '读取中' : '未知']
953
+ : isDevDeployment
954
+ ? ['', '源码部署']
955
+ : hasUpdate
956
+ ? ['warn', '有新版本']
957
+ : ['ok', '已是最新']
958
+
542
959
  cards.push(h('div', { className: 'card', key: 'runtime' },
543
- h('h3', null, '运行状态'),
544
- h('div', { className: 'row' },
545
- h('label', null, '后端'),
546
- connection.baseUrl
547
- ? h('a', {
548
- href: connection.baseUrl,
549
- target: '_blank',
550
- rel: 'noopener noreferrer',
551
- title: '在新标签页打开 Miloco 面板',
552
- }, connection.baseUrl)
553
- : h('code', null, '—'),
554
- h('span', { className: `pill ${reachable ? 'ok' : 'bad'}` }, reachable ? '已连通' : '连不上'),
555
- h('span', { className: 'muted' }, `来源:${connection.source ?? '—'}`)),
556
- h('div', { className: 'row' },
557
- h('label', null, '推送入口'),
558
- h('code', null, push.path || '—'),
559
- h('span', { className: `pill ${push.tokenSet ? 'ok' : 'bad'}` },
560
- push.tokenSet ? '令牌已配置' : '令牌未配置')),
561
- h('div', { className: 'row' },
562
- h('label', null, 'Miloco adapter'),
563
- h('span', { className: `pill ${adapter.active ? 'ok' : (adapter.installed ? 'warn' : '')}` },
564
- adapter.active ? '已启用' : (adapter.installed ? '已安装未启用' : '未安装')),
565
- h('span', { className: 'muted' },
566
- adapter.installed
567
- ? `agent.platform=${adapter.agentPlatform || '未设置'}`
568
- : '安装 Miloco 后可一键写入 adapter 与令牌')),
569
- h('div', { className: 'row' },
570
- h('label', null, '配置位置'),
571
- h('span', { className: 'muted' },
572
- `插件默认:${status.configLocation?.bundle ?? ''};`
573
- + `profile 覆盖:${status.configLocation?.profile ?? ''}`)),
960
+ h('div', { className: 'cardHead' },
961
+ h('h3', null, '运行状态'),
962
+ h('button', {
963
+ type: 'button',
964
+ className: `iconBtn${busy || versionBusy ? ' spinning' : ''}`,
965
+ title: '刷新运行状态与版本信息',
966
+ 'aria-label': '刷新运行状态与版本信息',
967
+ disabled: busy,
968
+ onClick: () => {
969
+ reload()
970
+ loadVersion(true)
971
+ },
972
+ }, h('svg', {
973
+ viewBox: '0 0 24 24',
974
+ width: 16,
975
+ height: 16,
976
+ fill: 'none',
977
+ stroke: 'currentColor',
978
+ strokeWidth: 1.8,
979
+ strokeLinecap: 'round',
980
+ strokeLinejoin: 'round',
981
+ 'aria-hidden': 'true',
982
+ },
983
+ h('path', { d: 'M20 11a8.1 8.1 0 0 0-15.5-2m-.5-4v4h4' }),
984
+ h('path', { d: 'M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4' })))),
985
+ h('div', { className: 'statusRow' },
986
+ h('span', { className: 'statusLabel' }, '后端'),
987
+ h('div', { className: 'statusValue' },
988
+ connection.baseUrl
989
+ ? h('a', {
990
+ href: connection.baseUrl,
991
+ target: '_blank',
992
+ rel: 'noopener noreferrer',
993
+ title: '在新标签页打开 Miloco 面板',
994
+ }, connection.baseUrl)
995
+ : h('code', null, '—'),
996
+ h('span', { className: 'muted' }, `来源:${connection.source ?? '—'}`)),
997
+ reachable
998
+ ? h('span', { className: 'statusCheck', title: '已连通', 'aria-label': '已连通' }, '✓')
999
+ : h('span', { className: 'statusEnd pill bad' }, '连不上')),
1000
+ h('div', { className: 'statusRow' },
1001
+ h('span', { className: 'statusLabel' }, '推送入口'),
1002
+ h('div', { className: 'statusValue' },
1003
+ h('code', null, push.path || '—')),
1004
+ push.tokenSet
1005
+ ? h('span', { className: 'statusCheck', title: '令牌已配置', 'aria-label': '令牌已配置' }, '✓')
1006
+ : h('span', { className: 'statusEnd pill bad' }, '未配置')),
1007
+ h('div', { className: 'statusRow' },
1008
+ h('span', { className: 'statusLabel' }, 'Miloco adapter'),
1009
+ h('div', { className: 'statusValue' },
1010
+ h('span', { className: 'muted' },
1011
+ adapter.installed
1012
+ ? `agent.platform=${adapter.agentPlatform || '未设置'}`
1013
+ : '安装 Miloco 后可一键写入 adapter 与令牌')),
1014
+ adapter.active
1015
+ ? h('span', { className: 'statusCheck', title: '已启用', 'aria-label': '已启用' }, '✓')
1016
+ : h('span', { className: `statusEnd pill ${adapter.installed ? 'warn' : 'bad'}` },
1017
+ adapter.installed ? '未启用' : '未安装')),
1018
+ h('div', { className: 'statusRow' },
1019
+ h('span', { className: 'statusLabel' }, 'Miloco 版本'),
1020
+ h('div', { className: 'statusValue' },
1021
+ h('code', {
1022
+ title: currentVersion || '未知',
1023
+ }, currentVersion ? currentVersion.split('+')[0] : '—'),
1024
+ commitShort ? h('span', { className: 'muted' }, commitShort) : null,
1025
+ h('span', { className: `pill ${versionPill[0]}` }, versionPill[1]),
1026
+ h('div', { className: 'versionSub' },
1027
+ latestVersion
1028
+ ? h('span', null, `最新正式版 ${latestVersion}`)
1029
+ : null,
1030
+ releaseUrl
1031
+ ? h('a', {
1032
+ href: releaseUrl,
1033
+ target: '_blank',
1034
+ rel: 'noopener noreferrer',
1035
+ title: '在 GitHub 查看发布说明',
1036
+ }, '发布页')
1037
+ : null,
1038
+ isDevDeployment && devUpdateCommand
1039
+ ? h('button', {
1040
+ type: 'button',
1041
+ className: 'versionLink',
1042
+ title: '查看源码部署的更新命令',
1043
+ onClick: () => setVersionCommandOpen(!versionCommandOpen),
1044
+ }, versionCommandOpen ? '收起命令' : '手动更新')
1045
+ : null,
1046
+ h('button', {
1047
+ type: 'button',
1048
+ className: 'versionLink',
1049
+ title: '重新读取 Miloco 版本与更新状态',
1050
+ disabled: versionBusy,
1051
+ onClick: () => loadVersion(true),
1052
+ }, versionBusy ? '检查中…' : '检查更新'),
1053
+ isDevDeployment && gitDirty
1054
+ ? h('span', {
1055
+ className: 'versionDirty',
1056
+ title: '源码工作区有本地改动,更新前请先处理或提交',
1057
+ }, '工作区有改动')
1058
+ : null,
1059
+ versionError
1060
+ ? h('span', { className: 'versionDirty', title: versionError }, '版本读取失败')
1061
+ : null,
1062
+ upgradePhase && !['idle', 'done', 'failed'].includes(upgradePhase)
1063
+ ? h('span', null, `升级中:${upgradePhaseLabels[upgradePhase] || upgradePhase}`)
1064
+ : null)),
1065
+ hasUpdate && !isDevDeployment
1066
+ ? h('div', { className: 'statusEnd' },
1067
+ h('button', {
1068
+ type: 'button',
1069
+ className: 'primary',
1070
+ disabled: upgradeBusy,
1071
+ onClick: () => setUpgradeConfirmOpen(!upgradeConfirmOpen),
1072
+ }, upgradeConfirmOpen ? '收起' : '更新'))
1073
+ : null),
1074
+ isDevDeployment && devUpdateCommand && versionCommandOpen
1075
+ ? h('div', { className: 'versionManualPanel' },
1076
+ h('code', { className: 'versionCommand' }, devUpdateCommand),
1077
+ h('div', { className: 'row' },
1078
+ h('button', {
1079
+ type: 'button',
1080
+ onClick: () => {
1081
+ try { void navigator.clipboard?.writeText(devUpdateCommand) } catch { /* ignore */ }
1082
+ },
1083
+ }, '复制命令')))
1084
+ : null,
1085
+ upgradePhase === 'failed'
1086
+ ? h('div', { className: 'muted versionNote' }, '上次升级失败,可查看 Miloco 的 upgrade.log 后重试。')
1087
+ : null,
1088
+ upgradeConfirmOpen
1089
+ ? h('div', { className: 'danger' },
1090
+ h('h4', null, '更新 Miloco'),
1091
+ h('div', { className: 'muted' },
1092
+ '升级会短暂中断感知与自动化,完成后 Miloco 会自动重启。'),
1093
+ h('div', { className: 'row' },
1094
+ h('button', { type: 'button', onClick: () => setUpgradeConfirmOpen(false) }, '取消'),
1095
+ h('button', {
1096
+ type: 'button',
1097
+ className: 'primary',
1098
+ disabled: upgradeBusy,
1099
+ onClick: runUpgrade,
1100
+ }, upgradeBusy ? '启动中…' : '确认更新')))
1101
+ : null,
1102
+ upgradePolling
1103
+ ? h('div', { className: 'muted versionNote' }, '正在等待升级完成…')
1104
+ : null,
1105
+ h('div', { className: 'statusRow' },
1106
+ h('span', { className: 'statusLabel' }, '配置位置'),
1107
+ h('div', { className: 'statusValue' },
1108
+ h('span', { className: 'muted' },
1109
+ `插件默认:${status.configLocation?.bundle ?? ''};`
1110
+ + `profile 覆盖:${status.configLocation?.profile ?? ''}`))),
574
1111
  h('div', { className: 'row' },
575
- h('button', { type: 'button', disabled: busy, onClick: reload }, '刷新'),
576
- h('button', { type: 'button', disabled: busy, onClick: () => setConnectOpen(!connectOpen) },
577
- connectOpen ? '收起连接设置' : '修改连接'),
578
1112
  h('button', {
579
1113
  type: 'button',
1114
+ disabled: busy,
1115
+ onClick: () => {
1116
+ const opening = !connectOpen
1117
+ setConnectOpen(opening)
1118
+ if (opening) setUninstallOpen(false)
1119
+ },
1120
+ }, '修改连接'),
1121
+ h('button', {
1122
+ type: 'button',
1123
+ className: 'dangerBtn',
580
1124
  disabled: uninstallBusy,
581
- onClick: () => setUninstallOpen(!uninstallOpen),
582
- }, uninstallOpen ? '收起卸载' : '卸载 Miloco')),
1125
+ onClick: () => {
1126
+ const opening = !uninstallOpen
1127
+ setUninstallOpen(opening)
1128
+ if (opening) setConnectOpen(false)
1129
+ },
1130
+ }, '卸载 Miloco')),
583
1131
  reachable ? connectForm : null,
584
1132
  uninstallResult ? h('div', { className: 'muted' }, uninstallResult) : null,
585
1133
  uninstallOpen
@@ -608,7 +1156,11 @@ window.__ModuleLoader__.load({
608
1156
  }),
609
1157
  h('button', {
610
1158
  type: 'button',
611
- className: 'primary',
1159
+ onClick: () => { setUninstallOpen(false); setConfirmText('') },
1160
+ }, '取消'),
1161
+ h('button', {
1162
+ type: 'button',
1163
+ className: 'dangerBtn',
612
1164
  disabled: uninstallBusy || confirmText !== 'UNINSTALL',
613
1165
  onClick: uninstall,
614
1166
  }, uninstallBusy ? '卸载中…' : '确认卸载')))
@@ -623,6 +1175,7 @@ window.__ModuleLoader__.load({
623
1175
 
624
1176
  function apply(ctx) {
625
1177
  ensureStyles()
1178
+ installPresetGuide(ctx)
626
1179
  ctx.slots.inject('settings.plugins.tab', () => ctx.slots.register({
627
1180
  name: 'settings.plugins.tab',
628
1181
  id: TAB_ID,