@luoyi-dsh/instagram-mcp 1.0.0 → 1.0.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/client.js ADDED
@@ -0,0 +1,870 @@
1
+ /**
2
+ * client.js —— DSH Web 瀏覽器端設定介面 (Instagram FastMCP 核心服務面板)
3
+ * ============================================================================
4
+ * 設計規範遵循:
5
+ * - .agents/skills/design-taste-frontend
6
+ * - references/app-screens.md (Forms & Settings, KPIs, Quiet Surfaces)
7
+ * ============================================================================
8
+ */
9
+
10
+ window.__ModuleLoader__.load({
11
+ id: '@luoyi-dsh/instagram-mcp',
12
+ factory: (require) => {
13
+ const React = require('react')
14
+ const h = React.createElement
15
+ const { useState, useEffect } = React
16
+
17
+ const API = '/api/instagram/mcp'
18
+
19
+ const asJson = async (res) => {
20
+ if (res.ok) return res.json()
21
+ const text = await res.text().catch(() => '')
22
+ throw new Error(`HTTP ${res.status}: ${text || res.statusText}`)
23
+ }
24
+
25
+ const jget = async (path) => asJson(await fetch(API + path, { credentials: 'same-origin' }))
26
+ const jpost = async (path, body) =>
27
+ asJson(
28
+ await fetch(API + path, {
29
+ method: 'POST',
30
+ credentials: 'same-origin',
31
+ headers: { 'content-type': 'application/json' },
32
+ body: JSON.stringify(body || {}),
33
+ })
34
+ )
35
+
36
+ // 精緻向量 SVG 圖標 (嚴禁 Emoji,遵循 anti-slop 規範)
37
+ const Icon = {
38
+ Server: ({ size = 18 }) =>
39
+ h(
40
+ 'svg',
41
+ { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' },
42
+ h('rect', { width: '20', height: '8', x: '2', y: '2', rx: '2', ry: '2' }),
43
+ h('rect', { width: '20', height: '8', x: '2', y: '14', rx: '2', ry: '2' }),
44
+ h('line', { x1: '6', x2: '6.01', y1: '6', y2: '6' }),
45
+ h('line', { x1: '6', x2: '6.01', y1: '18', y2: '18' })
46
+ ),
47
+ Refresh: ({ size = 14, spinning = false }) =>
48
+ h(
49
+ 'svg',
50
+ {
51
+ width: size,
52
+ height: size,
53
+ viewBox: '0 0 24 24',
54
+ fill: 'none',
55
+ stroke: 'currentColor',
56
+ strokeWidth: 2,
57
+ strokeLinecap: 'round',
58
+ strokeLinejoin: 'round',
59
+ style: spinning ? { animation: 'mcp-spin 1s linear infinite' } : {},
60
+ },
61
+ h('path', { d: 'M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8' }),
62
+ h('path', { d: 'M21 3v5h-5' }),
63
+ h('path', { d: 'M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16' }),
64
+ h('path', { d: 'M8 16H3v5' })
65
+ ),
66
+ Shield: ({ size = 15 }) =>
67
+ h(
68
+ 'svg',
69
+ { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' },
70
+ h('path', { d: 'M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z' })
71
+ ),
72
+ Key: ({ size = 15 }) =>
73
+ h(
74
+ 'svg',
75
+ { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' },
76
+ h('circle', { cx: '7.5', cy: '15.5', r: '5.5' }),
77
+ h('path', { d: 'm21 2-9.6 9.6' }),
78
+ h('path', { d: 'm15.5 7.5 3 3L22 7l-3-3' })
79
+ ),
80
+ Terminal: ({ size = 15 }) =>
81
+ h(
82
+ 'svg',
83
+ { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' },
84
+ h('polyline', { points: '4 17 10 11 4 5' }),
85
+ h('line', { x1: '12', x2: '20', y1: '19', y2: '19' })
86
+ ),
87
+ Check: ({ size = 14 }) =>
88
+ h(
89
+ 'svg',
90
+ { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2.5, strokeLinecap: 'round', strokeLinejoin: 'round' },
91
+ h('polyline', { points: '20 6 9 17 4 12' })
92
+ ),
93
+ Eye: ({ size = 14 }) =>
94
+ h(
95
+ 'svg',
96
+ { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' },
97
+ h('path', { d: 'M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z' }),
98
+ h('circle', { cx: '12', cy: '12', r: '3' })
99
+ ),
100
+ EyeOff: ({ size = 14 }) =>
101
+ h(
102
+ 'svg',
103
+ { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' },
104
+ h('path', { d: 'M9.88 9.88a3 3 0 1 0 4.24 4.24' }),
105
+ h('path', { d: 'M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68' }),
106
+ h('path', { d: 'M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61' }),
107
+ h('line', { x1: '2', x2: '22', y1: '2', y2: '22' })
108
+ ),
109
+ }
110
+
111
+ const CSS = `
112
+ @keyframes mcp-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
113
+ .mcp-container {
114
+ display: flex;
115
+ flex-direction: column;
116
+ gap: 20px;
117
+ max-width: 860px;
118
+ padding: 8px 12px 60px;
119
+ color: var(--dsw-alias-label-primary, #e2e8f0);
120
+ font-family: inherit;
121
+ box-sizing: border-box;
122
+ }
123
+ .mcp-header {
124
+ display: flex;
125
+ align-items: center;
126
+ justify-content: space-between;
127
+ border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.08));
128
+ padding-bottom: 16px;
129
+ gap: 12px;
130
+ }
131
+ .mcp-header-left {
132
+ display: flex;
133
+ align-items: center;
134
+ gap: 12px;
135
+ }
136
+ .mcp-avatar {
137
+ width: 40px;
138
+ height: 40px;
139
+ border-radius: 8px;
140
+ background: rgba(225, 48, 108, 0.12);
141
+ color: #e1306c;
142
+ display: flex;
143
+ align-items: center;
144
+ justify-content: center;
145
+ border: 1px solid rgba(225, 48, 108, 0.25);
146
+ flex-shrink: 0;
147
+ }
148
+ .mcp-title {
149
+ font-size: 16px;
150
+ font-weight: 600;
151
+ color: #ffffff;
152
+ margin: 0;
153
+ }
154
+ .mcp-subtitle {
155
+ font-size: 12px;
156
+ color: var(--dsw-alias-label-secondary, #94a3b8);
157
+ margin: 2px 0 0;
158
+ }
159
+ .mcp-status-pill {
160
+ display: inline-flex;
161
+ align-items: center;
162
+ gap: 6px;
163
+ padding: 3px 10px;
164
+ border-radius: 9999px;
165
+ font-size: 11px;
166
+ font-weight: 500;
167
+ white-space: nowrap;
168
+ }
169
+ .mcp-status-pill.running {
170
+ background: rgba(16, 185, 129, 0.12);
171
+ color: #34d399;
172
+ border: 1px solid rgba(16, 185, 129, 0.25);
173
+ }
174
+ .mcp-status-pill.stopped {
175
+ background: rgba(148, 163, 184, 0.12);
176
+ color: #94a3b8;
177
+ border: 1px solid rgba(148, 163, 184, 0.25);
178
+ }
179
+ .mcp-status-dot {
180
+ width: 6px;
181
+ height: 6px;
182
+ border-radius: 50%;
183
+ }
184
+ .mcp-status-pill.running .mcp-status-dot { background: #10b981; }
185
+ .mcp-status-pill.stopped .mcp-status-dot { background: #64748b; }
186
+
187
+ .mcp-tabs {
188
+ display: flex;
189
+ gap: 6px;
190
+ border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.08));
191
+ padding-bottom: 8px;
192
+ }
193
+ .mcp-tab-btn {
194
+ display: inline-flex;
195
+ align-items: center;
196
+ gap: 6px;
197
+ padding: 6px 14px;
198
+ border-radius: 6px;
199
+ border: 1px solid transparent;
200
+ background: transparent;
201
+ color: var(--dsw-alias-label-secondary, #94a3b8);
202
+ font-size: 12px;
203
+ font-weight: 500;
204
+ cursor: pointer;
205
+ transition: all 0.15s ease;
206
+ }
207
+ .mcp-tab-btn:hover {
208
+ background: rgba(255, 255, 255, 0.04);
209
+ color: #ffffff;
210
+ }
211
+ .mcp-tab-btn.active {
212
+ background: rgba(255, 255, 255, 0.08);
213
+ border-color: var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.12));
214
+ color: #ffffff;
215
+ }
216
+
217
+ .mcp-card {
218
+ background: rgba(255, 255, 255, 0.02);
219
+ border: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.08));
220
+ border-radius: 8px;
221
+ padding: 16px;
222
+ display: flex;
223
+ flex-direction: column;
224
+ gap: 16px;
225
+ }
226
+ .mcp-card-title {
227
+ font-size: 13px;
228
+ font-weight: 600;
229
+ color: #ffffff;
230
+ display: flex;
231
+ align-items: center;
232
+ gap: 8px;
233
+ }
234
+
235
+ .mcp-field-row {
236
+ display: grid;
237
+ grid-template-columns: 200px 1fr;
238
+ align-items: center;
239
+ gap: 16px;
240
+ }
241
+ @media (max-width: 680px) {
242
+ .mcp-field-row {
243
+ grid-template-columns: 1fr;
244
+ gap: 6px;
245
+ }
246
+ }
247
+ .mcp-field-label {
248
+ font-size: 12px;
249
+ font-weight: 500;
250
+ color: var(--dsw-alias-label-secondary, #94a3b8);
251
+ }
252
+ .mcp-field-desc {
253
+ font-size: 11px;
254
+ color: var(--dsw-alias-label-tertiary, #64748b);
255
+ margin-top: 2px;
256
+ }
257
+ .mcp-input {
258
+ width: 100%;
259
+ box-sizing: border-box;
260
+ height: 32px;
261
+ padding: 0 10px;
262
+ border-radius: 6px;
263
+ font-size: 12px;
264
+ border: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.1));
265
+ background: rgba(0, 0, 0, 0.25);
266
+ color: #f1f5f9;
267
+ font-family: inherit;
268
+ transition: border-color 0.15s, box-shadow 0.15s;
269
+ }
270
+ .mcp-input:focus {
271
+ outline: none;
272
+ border-color: #e1306c;
273
+ box-shadow: 0 0 0 1px #e1306c;
274
+ }
275
+ .mcp-input-wrap {
276
+ position: relative;
277
+ display: flex;
278
+ align-items: center;
279
+ }
280
+ .mcp-input-wrap .mcp-input {
281
+ padding-right: 32px;
282
+ }
283
+ .mcp-input-toggle {
284
+ position: absolute;
285
+ right: 6px;
286
+ background: none;
287
+ border: none;
288
+ color: #94a3b8;
289
+ cursor: pointer;
290
+ padding: 4px;
291
+ display: flex;
292
+ align-items: center;
293
+ justify-content: center;
294
+ }
295
+ .mcp-input-toggle:hover { color: #ffffff; }
296
+
297
+ .mcp-switch-box {
298
+ display: flex;
299
+ align-items: center;
300
+ justify-content: space-between;
301
+ padding: 10px 14px;
302
+ background: rgba(255, 255, 255, 0.02);
303
+ border: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.06));
304
+ border-radius: 6px;
305
+ }
306
+ .mcp-toggle-btn {
307
+ position: relative;
308
+ width: 36px;
309
+ height: 20px;
310
+ border-radius: 10px;
311
+ background: rgba(255, 255, 255, 0.15);
312
+ cursor: pointer;
313
+ border: 0;
314
+ padding: 0;
315
+ transition: background 0.2s ease;
316
+ flex-shrink: 0;
317
+ }
318
+ .mcp-toggle-btn.on { background: #10b981; }
319
+ .mcp-toggle-thumb {
320
+ position: absolute;
321
+ top: 2px;
322
+ left: 2px;
323
+ width: 16px;
324
+ height: 16px;
325
+ border-radius: 50%;
326
+ background: #ffffff;
327
+ transition: left 0.2s ease;
328
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
329
+ }
330
+ .mcp-toggle-btn.on .mcp-toggle-thumb { left: 18px; }
331
+
332
+ .mcp-btn {
333
+ display: inline-flex;
334
+ align-items: center;
335
+ justify-content: center;
336
+ gap: 6px;
337
+ height: 32px;
338
+ padding: 0 12px;
339
+ border-radius: 6px;
340
+ font-size: 12px;
341
+ font-weight: 500;
342
+ cursor: pointer;
343
+ border: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.12));
344
+ background: rgba(255, 255, 255, 0.05);
345
+ color: #f1f5f9;
346
+ transition: all 0.15s ease;
347
+ }
348
+ .mcp-btn:hover:not(:disabled) {
349
+ background: rgba(255, 255, 255, 0.1);
350
+ border-color: rgba(255, 255, 255, 0.2);
351
+ }
352
+ .mcp-btn-icon {
353
+ width: 32px;
354
+ padding: 0;
355
+ flex-shrink: 0;
356
+ }
357
+ .mcp-btn:disabled {
358
+ opacity: 0.5;
359
+ cursor: not-allowed;
360
+ }
361
+ .mcp-btn-primary {
362
+ background: #e1306c;
363
+ border-color: #e1306c;
364
+ color: #ffffff;
365
+ }
366
+ .mcp-btn-primary:hover:not(:disabled) {
367
+ background: #d0205c;
368
+ border-color: #d0205c;
369
+ }
370
+
371
+ .mcp-action-bar {
372
+ display: flex;
373
+ align-items: center;
374
+ justify-content: space-between;
375
+ padding: 12px 16px;
376
+ background: rgba(0, 0, 0, 0.4);
377
+ border: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.08));
378
+ border-radius: 8px;
379
+ margin-top: 8px;
380
+ }
381
+ .mcp-action-status {
382
+ font-size: 12px;
383
+ display: flex;
384
+ align-items: center;
385
+ gap: 6px;
386
+ }
387
+ .mcp-action-status.saved { color: #34d399; }
388
+ .mcp-action-status.dirty { color: #fbbf24; }
389
+ .mcp-action-status.error { color: #f87171; }
390
+ `
391
+
392
+ function McpPanel() {
393
+ const [tab, setTab] = useState('status')
394
+ const [config, setConfig] = useState(null)
395
+ const [status, setStatus] = useState(null)
396
+ const [loading, setLoading] = useState(true)
397
+ const [saving, setSaving] = useState(false)
398
+ const [restarting, setRestarting] = useState(false)
399
+ const [isDirty, setIsDirty] = useState(false)
400
+ const [saveNotice, setSaveNotice] = useState(null)
401
+ const [showPassword, setShowPassword] = useState(false)
402
+
403
+ const loadData = async () => {
404
+ setLoading(true)
405
+ try {
406
+ const [cfgRes, statusRes] = await Promise.all([
407
+ jget('/config'),
408
+ jget('/status').catch(() => ({ ok: false })),
409
+ ])
410
+ if (cfgRes?.ok) {
411
+ setConfig(cfgRes.config)
412
+ setIsDirty(false)
413
+ }
414
+ if (statusRes?.ok) {
415
+ setStatus(statusRes)
416
+ }
417
+ } catch (e) {
418
+ console.error('[IG-MCP] 載入資料失敗:', e)
419
+ } finally {
420
+ setLoading(false)
421
+ }
422
+ }
423
+
424
+ useEffect(() => {
425
+ loadData()
426
+ }, [])
427
+
428
+ const updateConfig = (key, value) => {
429
+ setConfig((prev) => {
430
+ const next = { ...prev, [key]: value }
431
+ setIsDirty(true)
432
+ return next
433
+ })
434
+ }
435
+
436
+ const handleSave = async () => {
437
+ if (!config) return
438
+ setSaving(true)
439
+ setSaveNotice(null)
440
+ try {
441
+ const res = await jpost('/config', config)
442
+ if (res?.ok) {
443
+ setConfig(res.config)
444
+ setIsDirty(false)
445
+ setSaveNotice({ type: 'saved', text: 'FastMCP 配置已成功儲存!' })
446
+ setTimeout(() => setSaveNotice(null), 3000)
447
+ }
448
+ } catch (e) {
449
+ setSaveNotice({ type: 'error', text: `儲存失敗: ${e.message}` })
450
+ } finally {
451
+ setSaving(false)
452
+ }
453
+ }
454
+
455
+ const handleRestart = async () => {
456
+ setRestarting(true)
457
+ try {
458
+ const res = await jpost('/restart', {})
459
+ if (res?.ok) {
460
+ setSaveNotice({ type: 'saved', text: 'FastMCP 服務重啟完成!' })
461
+ await loadData()
462
+ setTimeout(() => setSaveNotice(null), 3000)
463
+ }
464
+ } catch (e) {
465
+ setSaveNotice({ type: 'error', text: `重啟失敗: ${e.message}` })
466
+ } finally {
467
+ setRestarting(false)
468
+ }
469
+ }
470
+
471
+ const handleTestLogin = async () => {
472
+ setRestarting(true)
473
+ try {
474
+ const res = await jpost('/login', {
475
+ username: config?.username,
476
+ password: config?.password,
477
+ twoFactorCode: config?.twoFactorCode,
478
+ proxyUrl: config?.proxyUrl,
479
+ })
480
+ if (res?.ok) {
481
+ setSaveNotice({ type: 'saved', text: `驗證成功: ${res.loginStatus?.message || '已就緒'}` })
482
+ } else {
483
+ setSaveNotice({ type: 'error', text: `驗證失敗: ${res.loginStatus?.message || '未知錯誤'}` })
484
+ }
485
+ await loadData()
486
+ } catch (e) {
487
+ setSaveNotice({ type: 'error', text: `連線異常: ${e.message}` })
488
+ } finally {
489
+ setRestarting(false)
490
+ }
491
+ }
492
+
493
+ if (loading || !config) {
494
+ return h('div', { className: 'mcp-container' }, '載入 FastMCP 設定中...')
495
+ }
496
+
497
+ const isRunning = Boolean(status?.mcpRunning || status?.mcpReady)
498
+
499
+ return h(
500
+ 'div',
501
+ { className: 'mcp-container' },
502
+ // 頂部導航
503
+ h(
504
+ 'div',
505
+ { className: 'mcp-header' },
506
+ h(
507
+ 'div',
508
+ { className: 'mcp-header-left' },
509
+ h('div', { className: 'mcp-avatar' }, h(Icon.Server)),
510
+ h(
511
+ 'div',
512
+ null,
513
+ h('h2', { className: 'mcp-title' }, 'Instagram FastMCP 核心服務'),
514
+ h('p', { className: 'mcp-subtitle' }, 'Python 子進程生命週期維持、Session 快取與 MCP 標準工具介面')
515
+ )
516
+ ),
517
+ h(
518
+ 'div',
519
+ { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
520
+ h(
521
+ 'span',
522
+ { className: `mcp-status-pill ${isRunning ? 'running' : 'stopped'}` },
523
+ h('span', { className: 'mcp-status-dot' }),
524
+ isRunning ? 'FastMCP 運行中' : '子進程未連線'
525
+ ),
526
+ h(
527
+ 'button',
528
+ {
529
+ className: 'mcp-btn mcp-btn-icon',
530
+ onClick: handleRestart,
531
+ disabled: restarting,
532
+ title: restarting ? '正在重啟...' : '重啟服務',
533
+ 'aria-label': restarting ? '正在重啟...' : '重啟服務',
534
+ },
535
+ h(Icon.Refresh, { spinning: restarting })
536
+ )
537
+ )
538
+ ),
539
+
540
+ // 頁籤導覽
541
+ h(
542
+ 'div',
543
+ { className: 'mcp-tabs' },
544
+ h(
545
+ 'button',
546
+ {
547
+ className: `mcp-tab-btn ${tab === 'status' ? 'active' : ''}`,
548
+ onClick: () => setTab('status'),
549
+ },
550
+ h(Icon.Terminal),
551
+ '服務狀態與環境'
552
+ ),
553
+ h(
554
+ 'button',
555
+ {
556
+ className: `mcp-tab-btn ${tab === 'auth' ? 'active' : ''}`,
557
+ onClick: () => setTab('auth'),
558
+ },
559
+ h(Icon.Key),
560
+ '帳號與憑證'
561
+ ),
562
+ h(
563
+ 'button',
564
+ {
565
+ className: `mcp-tab-btn ${tab === 'env' ? 'active' : ''}`,
566
+ onClick: () => setTab('env'),
567
+ },
568
+ h(Icon.Shield),
569
+ '進階參數配置'
570
+ )
571
+ ),
572
+
573
+ // TAB 1: 服務狀態
574
+ tab === 'status' &&
575
+ h(
576
+ 'div',
577
+ { className: 'mcp-card' },
578
+ h('div', { className: 'mcp-card-title' }, h(Icon.Server), 'FastMCP 執行環境指標'),
579
+ h(
580
+ 'div',
581
+ { className: 'mcp-field-row' },
582
+ h('div', { className: 'mcp-field-label' }, '登入與會話狀態'),
583
+ h(
584
+ 'div',
585
+ { style: { fontSize: '13px', color: status?.loginStatus?.ok ? '#34d399' : '#fbbf24' } },
586
+ status?.loginStatus?.message || (isRunning ? '已啟動但尚未驗證帳號' : '未連線')
587
+ )
588
+ ),
589
+ h(
590
+ 'div',
591
+ { className: 'mcp-field-row' },
592
+ h('div', { className: 'mcp-field-label' }, 'Session 快取檔案'),
593
+ h(
594
+ 'div',
595
+ { style: { fontSize: '12px', fontFamily: 'monospace' } },
596
+ status?.sessionExists ? '✅ 已存在有效 Session 快取' : '⚠️ 尚未偵測到 Session 快取檔案'
597
+ )
598
+ ),
599
+ h(
600
+ 'div',
601
+ { className: 'mcp-field-row' },
602
+ h('div', { className: 'mcp-field-label' }, 'Python 虛擬環境'),
603
+ h('div', { style: { fontSize: '12px', fontFamily: 'monospace', color: '#94a3b8' } }, status?.pythonPath || '自動偵測系統環境')
604
+ ),
605
+ h(
606
+ 'div',
607
+ { style: { display: 'flex', gap: '8px', paddingTop: '8px' } },
608
+ h(
609
+ 'button',
610
+ {
611
+ className: 'mcp-btn mcp-btn-primary',
612
+ onClick: handleTestLogin,
613
+ disabled: restarting,
614
+ },
615
+ '測試驗證登入'
616
+ ),
617
+ h(
618
+ 'button',
619
+ {
620
+ className: 'mcp-btn',
621
+ onClick: loadData,
622
+ },
623
+ '重新整理狀態'
624
+ )
625
+ )
626
+ ),
627
+
628
+ // TAB 2: 帳號與憑證
629
+ tab === 'auth' &&
630
+ h(
631
+ 'div',
632
+ { className: 'mcp-card' },
633
+ h('div', { className: 'mcp-card-title' }, h(Icon.Key), 'Instagram 認證憑證'),
634
+ h(
635
+ 'div',
636
+ { className: 'mcp-field-row' },
637
+ h('div', null, h('div', { className: 'mcp-field-label' }, 'Instagram 帳號'), h('div', { className: 'mcp-field-desc' }, '用戶名稱(不含 @)')),
638
+ h('input', {
639
+ className: 'mcp-input',
640
+ value: config.username || '',
641
+ placeholder: '例如: your_username',
642
+ onChange: (e) => updateConfig('username', e.target.value),
643
+ })
644
+ ),
645
+ h(
646
+ 'div',
647
+ { className: 'mcp-field-row' },
648
+ h('div', null, h('div', { className: 'mcp-field-label' }, '密碼'), h('div', { className: 'mcp-field-desc' }, '已有 Session 時可留空')),
649
+ h(
650
+ 'div',
651
+ { className: 'mcp-input-wrap' },
652
+ h('input', {
653
+ className: 'mcp-input',
654
+ type: showPassword ? 'text' : 'password',
655
+ value: config.password || '',
656
+ placeholder: 'Instagram 帳號密碼',
657
+ onChange: (e) => updateConfig('password', e.target.value),
658
+ }),
659
+ h(
660
+ 'button',
661
+ {
662
+ className: 'mcp-input-toggle',
663
+ type: 'button',
664
+ onClick: () => setShowPassword(!showPassword),
665
+ },
666
+ showPassword ? h(Icon.EyeOff) : h(Icon.Eye)
667
+ )
668
+ )
669
+ ),
670
+ h(
671
+ 'div',
672
+ { className: 'mcp-field-row' },
673
+ h('div', null, h('div', { className: 'mcp-field-label' }, '2FA 雙步驟代碼'), h('div', { className: 'mcp-field-desc' }, '遇到安全驗證挑戰時填寫')),
674
+ h('input', {
675
+ className: 'mcp-input',
676
+ value: config.twoFactorCode || '',
677
+ placeholder: '6 位數雙重驗證代碼',
678
+ onChange: (e) => updateConfig('twoFactorCode', e.target.value),
679
+ })
680
+ ),
681
+ h(
682
+ 'div',
683
+ { className: 'mcp-field-row' },
684
+ h('div', null, h('div', { className: 'mcp-field-label' }, 'Proxy 代理 URL'), h('div', { className: 'mcp-field-desc' }, '防止風控 (http:// 或 socks5://)')),
685
+ h('input', {
686
+ className: 'mcp-input',
687
+ value: config.proxyUrl || '',
688
+ placeholder: '例如: http://127.0.0.1:7890',
689
+ onChange: (e) => updateConfig('proxyUrl', e.target.value),
690
+ })
691
+ )
692
+ ),
693
+
694
+ // TAB 3: 進階參數
695
+ tab === 'env' &&
696
+ h(
697
+ 'div',
698
+ { className: 'mcp-card' },
699
+ h('div', { className: 'mcp-card-title' }, h(Icon.Shield), '環境與生命週期參數'),
700
+ h(
701
+ 'div',
702
+ { className: 'mcp-field-row' },
703
+ h('div', null, h('div', { className: 'mcp-field-label' }, '自訂 Python 執行檔路徑'), h('div', { className: 'mcp-field-desc' }, '留空則自動使用內建虛擬環境')),
704
+ h('input', {
705
+ className: 'mcp-input',
706
+ value: config.pythonPath || '',
707
+ placeholder: '例如: C:\\Python311\\python.exe',
708
+ onChange: (e) => updateConfig('pythonPath', e.target.value),
709
+ })
710
+ ),
711
+ h(
712
+ 'div',
713
+ { className: 'mcp-field-row' },
714
+ h('div', null, h('div', { className: 'mcp-field-label' }, 'Session 快取相對路徑'), h('div', { className: 'mcp-field-desc' }, '預設為 data/ig_session.json')),
715
+ h('input', {
716
+ className: 'mcp-input',
717
+ value: config.sessionPath || '',
718
+ placeholder: 'data/ig_session.json',
719
+ onChange: (e) => updateConfig('sessionPath', e.target.value),
720
+ })
721
+ ),
722
+ h(
723
+ 'div',
724
+ { className: 'mcp-switch-box' },
725
+ h(
726
+ 'div',
727
+ null,
728
+ h('div', { style: { fontSize: '13px', fontWeight: 500, color: '#ffffff' } }, '自動啟動 FastMCP 子進程'),
729
+ h('div', { style: { fontSize: '11px', color: '#64748b' } }, 'DSH 啟動時自動初始化 Python 環境並常駐運行')
730
+ ),
731
+ h(
732
+ 'button',
733
+ {
734
+ className: `mcp-toggle-btn ${config.autoStart !== false ? 'on' : ''}`,
735
+ onClick: () => updateConfig('autoStart', config.autoStart === false),
736
+ },
737
+ h('span', { className: 'mcp-toggle-thumb' })
738
+ )
739
+ )
740
+ ),
741
+
742
+ // 底部動作列
743
+ h(
744
+ 'div',
745
+ { className: 'mcp-action-bar' },
746
+ h(
747
+ 'div',
748
+ { className: `mcp-action-status ${saveNotice ? saveNotice.type : isDirty ? 'dirty' : ''}` },
749
+ saveNotice
750
+ ? (saveNotice.type === 'saved' ? h(Icon.Check) : null)
751
+ : isDirty
752
+ ? '※ 有尚未儲存的參數變更'
753
+ : '',
754
+ saveNotice ? saveNotice.text : ''
755
+ ),
756
+ h(
757
+ 'div',
758
+ { style: { display: 'flex', gap: '8px' } },
759
+ h(
760
+ 'button',
761
+ {
762
+ className: 'mcp-btn',
763
+ onClick: loadData,
764
+ disabled: saving,
765
+ },
766
+ '放棄變更'
767
+ ),
768
+ h(
769
+ 'button',
770
+ {
771
+ className: 'mcp-btn mcp-btn-primary',
772
+ onClick: handleSave,
773
+ disabled: saving,
774
+ },
775
+ saving ? h(Icon.Refresh, { spinning: true }) : h(Icon.Check),
776
+ saving ? '儲存中...' : '儲存變更'
777
+ )
778
+ )
779
+ )
780
+ )
781
+ }
782
+
783
+ return {
784
+ inject: ['slots'],
785
+ apply(ctx) {
786
+ // 注入專屬 CSS
787
+ try {
788
+ if (!document.querySelector('style[data-plugin-css="dsh-instagram-mcp"]')) {
789
+ const style = document.createElement('style')
790
+ style.setAttribute('data-plugin-css', 'dsh-instagram-mcp')
791
+ style.textContent = CSS
792
+ document.head.appendChild(style)
793
+ if (typeof ctx?.effect === 'function') {
794
+ ctx.effect(() => () => style.remove(), 'dsh-instagram-mcp: style')
795
+ }
796
+ }
797
+ } catch (e) {}
798
+
799
+ const slots = (typeof ctx.get === 'function' ? ctx.get('slots') : undefined) || ctx.slots
800
+ if (!slots) return
801
+
802
+ // 1. 左側一級專屬導航槽位 (settings.section)
803
+ const registerSection = () => {
804
+ try {
805
+ slots.register(
806
+ {
807
+ name: 'settings.section',
808
+ id: 'instagram-mcp',
809
+ order: 34,
810
+ label: 'Instagram MCP',
811
+ },
812
+ () => h(McpPanel)
813
+ )
814
+ } catch (e) {
815
+ console.error('[@luoyi-dsh/instagram-mcp] 註冊 settings.section 異常:', e)
816
+ }
817
+ }
818
+ if (typeof slots.inject === 'function') {
819
+ slots.inject('settings.section', registerSection)
820
+ } else if (typeof slots.register === 'function') {
821
+ registerSection()
822
+ }
823
+
824
+ // 2. 外掛程式頁面頂部頁籤 (settings.plugins.tab)
825
+ const registerTab = () => {
826
+ try {
827
+ slots.register(
828
+ {
829
+ name: 'settings.plugins.tab',
830
+ id: 'instagram-mcp',
831
+ order: 34,
832
+ label: 'Instagram MCP',
833
+ },
834
+ () => h(McpPanel)
835
+ )
836
+ } catch (e) {
837
+ console.error('[@luoyi-dsh/instagram-mcp] 註冊 settings.plugins.tab 異常:', e)
838
+ }
839
+ }
840
+ if (typeof slots.inject === 'function') {
841
+ slots.inject('settings.plugins.tab', registerTab)
842
+ } else if (typeof slots.register === 'function') {
843
+ registerTab()
844
+ }
845
+
846
+ // 3. 外掛程式卡片列表槽位 (plugins.item)
847
+ const registerItem = () => {
848
+ try {
849
+ slots.register(
850
+ {
851
+ name: 'plugins.item',
852
+ id: 'instagram-mcp',
853
+ order: 34,
854
+ label: 'Instagram FastMCP 核心服務',
855
+ },
856
+ () => h(McpPanel)
857
+ )
858
+ } catch (e) {
859
+ console.error('[@luoyi-dsh/instagram-mcp] 註冊 plugins.item 異常:', e)
860
+ }
861
+ }
862
+ if (typeof slots.inject === 'function') {
863
+ slots.inject('plugins.item', registerItem)
864
+ } else if (typeof slots.register === 'function') {
865
+ registerItem()
866
+ }
867
+ },
868
+ }
869
+ },
870
+ })