dsh-miloco-bridge 0.1.0 → 0.1.2

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
  // ---------------------------------------------------------------- 部署 / 连接
@@ -381,11 +755,20 @@ window.__ModuleLoader__.load({
381
755
  h('div', { className: 'muted' },
382
756
  backend.error || `连不上 ${connection.baseUrl || '(未配置地址)'}。`
383
757
  + '插件本身可以独立安装;部署或连接之后才会出现设备与感知能力。'),
758
+ h('div', { className: 'muted' },
759
+ '一键部署会下载官方 release 安装器(支持 GitHub 镜像),不会在本机编译源码。'),
384
760
  h('div', { className: 'row' },
385
761
  h('button', { type: 'button', className: 'primary', disabled: busy, onClick: deploy },
386
762
  busy ? '部署中…' : '一键部署 Miloco'),
387
- h('button', { type: 'button', disabled: busy, onClick: () => setConnectOpen(!connectOpen) },
388
- connectOpen ? '收起' : '连接已有的 Miloco')),
763
+ h('button', {
764
+ type: 'button',
765
+ disabled: busy,
766
+ onClick: () => {
767
+ const opening = !connectOpen
768
+ setConnectOpen(opening)
769
+ if (opening) setUninstallOpen(false)
770
+ },
771
+ }, '连接已有的 Miloco')),
389
772
  connectForm,
390
773
  deployLog ? h('pre', null, deployLog) : null))
391
774
  }
@@ -397,7 +780,7 @@ window.__ModuleLoader__.load({
397
780
  h('div', { className: 'grid' },
398
781
  h('div', { className: 'stat' },
399
782
  h('span', { className: 'k' }, '米家账号'),
400
- h('span', { className: 'v' }, overview.account?.nickname || (overview.accountBound ? '已绑定' : '未绑定'))),
783
+ h('span', { className: 'v', title: overview.account?.nickname || '' }, overview.account?.nickname || (overview.accountBound ? '已绑定' : '未绑定'))),
401
784
  h('div', { className: 'stat' },
402
785
  h('span', { className: 'k' }, '设备'),
403
786
  h('span', { className: 'v' }, overview.deviceCount ?? '—')),
@@ -430,7 +813,7 @@ window.__ModuleLoader__.load({
430
813
  h('div', { className: 'muted' }, '模式决定这个预设手里有哪些工具:'),
431
814
  h('div', { className: 'modeList' }, modes.map(item => h('label', {
432
815
  key: item.id,
433
- className: 'modeItem',
816
+ className: `modeItem${mode === item.id ? ' selected' : ''}`,
434
817
  },
435
818
  h('input', {
436
819
  type: 'radio', name: 'dsh-miloco-bridge-mode', checked: mode === item.id,
@@ -478,60 +861,62 @@ window.__ModuleLoader__.load({
478
861
  : ''
479
862
  cards.push(h('div', { className: 'card', key: 'target' },
480
863
  h('h3', null, '推送目标'),
481
- h('div', { className: 'row' },
864
+ h('div', { className: 'field' },
482
865
  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 }, '刷新会话')),
866
+ h('div', { className: 'fieldRow' },
867
+ h('input', {
868
+ type: 'text',
869
+ list: SESSION_LIST_ID,
870
+ value: sessionId,
871
+ placeholder: lastSession
872
+ ? `留空 = 上一次触发 miloco 的会话(${lastSession})`
873
+ : '留空 = 上一次触发 miloco 的会话(还没有)',
874
+ onChange: (event) => setSessionId(event.target.value),
875
+ }),
876
+ h('datalist', { id: SESSION_LIST_ID }, sessionOptions(status)),
877
+ h('button', { type: 'button', disabled: busy, onClick: refreshSessions }, '刷新会话'))),
494
878
  h('div', { className: 'muted' },
495
879
  '留空 = 投到「上一次触发 miloco 的会话」,也就是最近一次调用过米家工具的对话。'
496
880
  + '也可以从下拉里选任意对话,或直接填会话 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, '推理强度'),
881
+ h('div', { className: 'formGrid' },
882
+ h('div', { className: 'field' },
883
+ h('label', null, '模型'),
884
+ h('select', {
885
+ value: modelValue,
886
+ onChange: (event) => {
887
+ setModelValue(event.target.value)
888
+ setReasoningEffort('')
889
+ },
890
+ }, modelOptions(status))),
891
+ selectedEfforts.length > 0
892
+ ? h('div', { className: 'field' },
893
+ h('label', null, '推理强度'),
894
+ h('select', {
895
+ value: effortValue,
896
+ onChange: (event) => setReasoningEffort(event.target.value),
897
+ }, [
898
+ h('option', { key: '__model_default__', value: '' }, '跟随模型默认'),
899
+ ...selectedEfforts.map(item => h('option', {
900
+ key: item.id,
901
+ value: item.id,
902
+ }, item.name || item.id)),
903
+ ]))
904
+ : null,
905
+ h('div', { className: 'field' },
906
+ h('label', null, '感知等级'),
509
907
  h('select', {
510
- value: effortValue,
511
- onChange: (event) => setReasoningEffort(event.target.value),
908
+ value: perceptionLevel,
909
+ onChange: (event) => setPerceptionLevel(event.target.value),
512
910
  }, [
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
- ])),
911
+ h('option', { key: 'all', value: 'all' }, '全部事件'),
912
+ h('option', { key: 'low', value: 'low' }, '低及以上'),
913
+ h('option', { key: 'medium', value: 'medium' }, '中及以上'),
914
+ h('option', { key: 'high', value: 'high' }, '仅高优先级'),
915
+ ]))),
531
916
  h('div', { className: 'muted' },
532
917
  '低于所选等级的感知建议不会投递到 DSH;规则、任务、onboarding 与普通对话事件不受影响。'),
533
918
  h('div', { className: 'row' },
534
- h('button', { type: 'button', disabled: busy, onClick: save }, busy ? '保存中…' : '保存'),
919
+ h('button', { type: 'button', className: 'primary', disabled: busy, onClick: save }, busy ? '保存中…' : '保存'),
535
920
  saved ? h('span', { className: 'pill ok' }, saved) : null,
536
921
  error ? h('span', { className: 'pill bad' }, error) : null),
537
922
  h('div', { className: 'muted' },
@@ -539,47 +924,231 @@ window.__ModuleLoader__.load({
539
924
 
540
925
  // ---------------------------------------------------------------- 运行状态
541
926
  const deployState = status.deploy ?? {}
927
+ const versionData = versionInfo?.version ?? null
928
+ const upgradeInfo = versionInfo?.upgrade ?? null
929
+ const currentVersion = upgradeInfo?.current || versionData?.version || ''
930
+ const latestVersion = upgradeInfo?.latest || ''
931
+ const deployKind = upgradeInfo?.deploy_kind
932
+ || (currentVersion && /\.dev\d*|\+/.test(currentVersion) ? 'dev' : '')
933
+ const hasUpdate = Boolean(upgradeInfo?.has_update)
934
+ const isDevDeployment = deployKind === 'dev'
935
+ const upgradeSource = upgradeInfo?.source || 'backend'
936
+ const mirrorCheck = upgradeSource === 'mirror'
937
+ const upgradeUnavailable = !upgradeInfo || upgradeInfo.reachable === false
938
+ const releaseUrl = upgradeInfo?.release_url || ''
939
+ const sourceRoot = versionInfo?.sourceRoot || deployState.sourceDir || ''
940
+ const gitDirty = Boolean(versionData?.git?.dirty)
941
+ const commitShort = versionData?.git?.commit_short || ''
942
+ const upgradePhase = upgradeStatus?.phase || ''
943
+ const upgradeLogPath = upgradeStatus?.logPath || ''
944
+ const upgradePhaseLabels = {
945
+ idle: '空闲',
946
+ starting: '准备中',
947
+ downloading: '下载中',
948
+ installing: '安装中',
949
+ restarting: '服务重启中',
950
+ done: '已完成',
951
+ failed: '失败',
952
+ }
953
+ const devUpdateCommand = sourceRoot
954
+ ? `cd ${sourceRoot} && git pull --ff-only && bash scripts/install.sh --dev --skip-openclaw && systemctl --user restart miloco.service`
955
+ : ''
956
+ const versionPill = versionInfo?.reachable === false
957
+ ? ['bad', '不可用']
958
+ : !versionInfo
959
+ ? ['', versionBusy ? '读取中' : '未知']
960
+ : isDevDeployment
961
+ ? ['', '源码部署']
962
+ : upgradeUnavailable
963
+ ? ['warn', '检查失败']
964
+ : hasUpdate
965
+ ? ['warn', '有新版本']
966
+ : ['ok', '已是最新']
967
+
542
968
  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 ?? ''}`)),
969
+ h('div', { className: 'cardHead' },
970
+ h('h3', null, '运行状态'),
971
+ h('button', {
972
+ type: 'button',
973
+ className: `iconBtn${busy || versionBusy ? ' spinning' : ''}`,
974
+ title: '刷新运行状态与版本信息',
975
+ 'aria-label': '刷新运行状态与版本信息',
976
+ disabled: busy,
977
+ onClick: () => {
978
+ reload()
979
+ loadVersion(true)
980
+ },
981
+ }, h('svg', {
982
+ viewBox: '0 0 24 24',
983
+ width: 16,
984
+ height: 16,
985
+ fill: 'none',
986
+ stroke: 'currentColor',
987
+ strokeWidth: 1.8,
988
+ strokeLinecap: 'round',
989
+ strokeLinejoin: 'round',
990
+ 'aria-hidden': 'true',
991
+ },
992
+ h('path', { d: 'M20 11a8.1 8.1 0 0 0-15.5-2m-.5-4v4h4' }),
993
+ h('path', { d: 'M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4' })))),
994
+ h('div', { className: 'statusRow' },
995
+ h('span', { className: 'statusLabel' }, '后端'),
996
+ h('div', { className: 'statusValue' },
997
+ connection.baseUrl
998
+ ? h('a', {
999
+ href: connection.baseUrl,
1000
+ target: '_blank',
1001
+ rel: 'noopener noreferrer',
1002
+ title: '在新标签页打开 Miloco 面板',
1003
+ }, connection.baseUrl)
1004
+ : h('code', null, '—'),
1005
+ h('span', { className: 'muted' }, `来源:${connection.source ?? '—'}`)),
1006
+ reachable
1007
+ ? h('span', { className: 'statusCheck', title: '已连通', 'aria-label': '已连通' }, '✓')
1008
+ : h('span', { className: 'statusEnd pill bad' }, '连不上')),
1009
+ h('div', { className: 'statusRow' },
1010
+ h('span', { className: 'statusLabel' }, '推送入口'),
1011
+ h('div', { className: 'statusValue' },
1012
+ h('code', null, push.path || '—')),
1013
+ push.tokenSet
1014
+ ? h('span', { className: 'statusCheck', title: '令牌已配置', 'aria-label': '令牌已配置' }, '✓')
1015
+ : h('span', { className: 'statusEnd pill bad' }, '未配置')),
1016
+ h('div', { className: 'statusRow' },
1017
+ h('span', { className: 'statusLabel' }, 'Miloco adapter'),
1018
+ h('div', { className: 'statusValue' },
1019
+ h('span', { className: 'muted' },
1020
+ adapter.installed
1021
+ ? `agent.platform=${adapter.agentPlatform || '未设置'}`
1022
+ : '安装 Miloco 后可一键写入 adapter 与令牌')),
1023
+ adapter.active
1024
+ ? h('span', { className: 'statusCheck', title: '已启用', 'aria-label': '已启用' }, '✓')
1025
+ : h('span', { className: `statusEnd pill ${adapter.installed ? 'warn' : 'bad'}` },
1026
+ adapter.installed ? '未启用' : '未安装')),
1027
+ h('div', { className: 'statusRow' },
1028
+ h('span', { className: 'statusLabel' }, 'Miloco 版本'),
1029
+ h('div', { className: 'statusValue' },
1030
+ h('code', {
1031
+ title: currentVersion || '未知',
1032
+ }, currentVersion ? currentVersion.split('+')[0] : '—'),
1033
+ commitShort ? h('span', { className: 'muted' }, commitShort) : null,
1034
+ h('span', { className: `pill ${versionPill[0]}` }, versionPill[1]),
1035
+ h('div', { className: 'versionSub' },
1036
+ latestVersion
1037
+ ? h('span', null, `最新正式版 ${latestVersion}`)
1038
+ : null,
1039
+ mirrorCheck
1040
+ ? h('span', {
1041
+ className: 'muted',
1042
+ title: 'GitHub 直连不可用,已通过镜像检查',
1043
+ }, '镜像')
1044
+ : null,
1045
+ releaseUrl
1046
+ ? h('a', {
1047
+ href: releaseUrl,
1048
+ target: '_blank',
1049
+ rel: 'noopener noreferrer',
1050
+ title: '查看发布说明',
1051
+ }, '发布页')
1052
+ : null,
1053
+ isDevDeployment && devUpdateCommand
1054
+ ? h('button', {
1055
+ type: 'button',
1056
+ className: 'versionLink',
1057
+ title: '查看源码部署的更新命令',
1058
+ onClick: () => setVersionCommandOpen(!versionCommandOpen),
1059
+ }, versionCommandOpen ? '收起命令' : '手动更新')
1060
+ : null,
1061
+ h('button', {
1062
+ type: 'button',
1063
+ className: 'versionLink',
1064
+ title: '重新读取 Miloco 版本与更新状态',
1065
+ disabled: versionBusy,
1066
+ onClick: () => loadVersion(true),
1067
+ }, versionBusy ? '检查中…' : '检查更新'),
1068
+ upgradeUnavailable
1069
+ ? h('span', {
1070
+ className: 'versionDirty',
1071
+ title: upgradeInfo?.error || 'GitHub 与镜像均不可达',
1072
+ }, '更新检查不可用')
1073
+ : null,
1074
+ isDevDeployment && gitDirty
1075
+ ? h('span', {
1076
+ className: 'versionDirty',
1077
+ title: '源码工作区有本地改动,更新前请先处理或提交',
1078
+ }, '工作区有改动')
1079
+ : null,
1080
+ versionError
1081
+ ? h('span', { className: 'versionDirty', title: versionError }, '版本读取失败')
1082
+ : null,
1083
+ upgradePhase && !['idle', 'done', 'failed'].includes(upgradePhase)
1084
+ ? h('span', null, `升级中:${upgradePhaseLabels[upgradePhase] || upgradePhase}`)
1085
+ : null)),
1086
+ hasUpdate && !isDevDeployment
1087
+ ? h('div', { className: 'statusEnd' },
1088
+ h('button', {
1089
+ type: 'button',
1090
+ className: 'primary',
1091
+ disabled: upgradeBusy,
1092
+ onClick: () => setUpgradeConfirmOpen(!upgradeConfirmOpen),
1093
+ }, upgradeConfirmOpen ? '收起' : '更新'))
1094
+ : null),
1095
+ isDevDeployment && devUpdateCommand && versionCommandOpen
1096
+ ? h('div', { className: 'versionManualPanel' },
1097
+ h('code', { className: 'versionCommand' }, devUpdateCommand),
1098
+ h('div', { className: 'row' },
1099
+ h('button', {
1100
+ type: 'button',
1101
+ onClick: () => {
1102
+ try { void navigator.clipboard?.writeText(devUpdateCommand) } catch { /* ignore */ }
1103
+ },
1104
+ }, '复制命令')))
1105
+ : null,
1106
+ upgradePhase === 'failed'
1107
+ ? h('div', { className: 'muted versionNote' }, `上次升级失败${upgradeLogPath ? `,可查看 ${upgradeLogPath}` : ',可查看 Miloco 的 upgrade.log'} 后重试。`)
1108
+ : null,
1109
+ upgradeConfirmOpen
1110
+ ? h('div', { className: 'danger' },
1111
+ h('h4', null, '更新 Miloco'),
1112
+ h('div', { className: 'muted' },
1113
+ '升级会短暂中断感知与自动化,完成后 Miloco 会自动重启。'),
1114
+ h('div', { className: 'row' },
1115
+ h('button', { type: 'button', onClick: () => setUpgradeConfirmOpen(false) }, '取消'),
1116
+ h('button', {
1117
+ type: 'button',
1118
+ className: 'primary',
1119
+ disabled: upgradeBusy,
1120
+ onClick: runUpgrade,
1121
+ }, upgradeBusy ? '启动中…' : '确认更新')))
1122
+ : null,
1123
+ upgradePolling
1124
+ ? h('div', { className: 'muted versionNote' }, '正在等待升级完成…')
1125
+ : null,
1126
+ h('div', { className: 'statusRow' },
1127
+ h('span', { className: 'statusLabel' }, '配置位置'),
1128
+ h('div', { className: 'statusValue' },
1129
+ h('span', { className: 'muted' },
1130
+ `插件默认:${status.configLocation?.bundle ?? ''};`
1131
+ + `profile 覆盖:${status.configLocation?.profile ?? ''}`))),
574
1132
  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
1133
  h('button', {
579
1134
  type: 'button',
1135
+ disabled: busy,
1136
+ onClick: () => {
1137
+ const opening = !connectOpen
1138
+ setConnectOpen(opening)
1139
+ if (opening) setUninstallOpen(false)
1140
+ },
1141
+ }, '修改连接'),
1142
+ h('button', {
1143
+ type: 'button',
1144
+ className: 'dangerBtn',
580
1145
  disabled: uninstallBusy,
581
- onClick: () => setUninstallOpen(!uninstallOpen),
582
- }, uninstallOpen ? '收起卸载' : '卸载 Miloco')),
1146
+ onClick: () => {
1147
+ const opening = !uninstallOpen
1148
+ setUninstallOpen(opening)
1149
+ if (opening) setConnectOpen(false)
1150
+ },
1151
+ }, '卸载 Miloco')),
583
1152
  reachable ? connectForm : null,
584
1153
  uninstallResult ? h('div', { className: 'muted' }, uninstallResult) : null,
585
1154
  uninstallOpen
@@ -608,7 +1177,11 @@ window.__ModuleLoader__.load({
608
1177
  }),
609
1178
  h('button', {
610
1179
  type: 'button',
611
- className: 'primary',
1180
+ onClick: () => { setUninstallOpen(false); setConfirmText('') },
1181
+ }, '取消'),
1182
+ h('button', {
1183
+ type: 'button',
1184
+ className: 'dangerBtn',
612
1185
  disabled: uninstallBusy || confirmText !== 'UNINSTALL',
613
1186
  onClick: uninstall,
614
1187
  }, uninstallBusy ? '卸载中…' : '确认卸载')))
@@ -623,6 +1196,7 @@ window.__ModuleLoader__.load({
623
1196
 
624
1197
  function apply(ctx) {
625
1198
  ensureStyles()
1199
+ installPresetGuide(ctx)
626
1200
  ctx.slots.inject('settings.plugins.tab', () => ctx.slots.register({
627
1201
  name: 'settings.plugins.tab',
628
1202
  id: TAB_ID,