@easyai-canvas/canvas-plugin-sdk 0.1.0
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/LICENSE +5 -0
- package/README.md +22 -0
- package/bin/easyai-plugin.mjs +267 -0
- package/dist/customer-main.d.ts +1 -0
- package/dist/customer-main.js +256 -0
- package/dist/protocol/index.d.ts +295 -0
- package/dist/protocol/index.js +454 -0
- package/dist/runtime/auth.d.ts +1 -0
- package/dist/runtime/auth.js +68 -0
- package/dist/runtime/engine.d.ts +49 -0
- package/dist/runtime/engine.js +296 -0
- package/dist/runtime/host-files.d.ts +16 -0
- package/dist/runtime/host-files.js +208 -0
- package/dist/runtime/instance-lock.d.ts +1 -0
- package/dist/runtime/instance-lock.js +52 -0
- package/dist/runtime/package-runtime.d.ts +44 -0
- package/dist/runtime/package-runtime.js +361 -0
- package/dist/runtime/packaged-plugins.d.ts +14 -0
- package/dist/runtime/packaged-plugins.js +58 -0
- package/dist/runtime/plugin-config.d.ts +4 -0
- package/dist/runtime/plugin-config.js +31 -0
- package/dist/runtime/store.d.ts +48 -0
- package/dist/runtime/store.js +151 -0
- package/dist/runtime/worker.d.ts +1 -0
- package/dist/runtime/worker.js +38 -0
- package/dist/sdk/frontend.d.ts +62 -0
- package/dist/sdk/frontend.js +40 -0
- package/dist/sdk/index.d.ts +38 -0
- package/dist/sdk/index.js +10 -0
- package/dist/studio/index.html +649 -0
- package/dist/studio/remote-connector.d.ts +60 -0
- package/dist/studio/remote-connector.js +295 -0
- package/package.json +45 -0
- package/tools/build-plugin-package.mjs +99 -0
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>画布插件调试</title>
|
|
7
|
+
<link rel="stylesheet" href="/studio/fonts/studio-fonts.css">
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
--ink: #30363d;
|
|
11
|
+
--muted: #555f69;
|
|
12
|
+
--quiet: #6b7580;
|
|
13
|
+
--line: #e6e9ed;
|
|
14
|
+
--line-strong: #d6dce2;
|
|
15
|
+
--surface: #fff;
|
|
16
|
+
--page: #f8f9fb;
|
|
17
|
+
--soft: #fbfcfd;
|
|
18
|
+
--accent: #363d45;
|
|
19
|
+
--control-shadow: 0 1px 2px #26323d12, 0 3px 8px #26323d08;
|
|
20
|
+
--floating-shadow: 0 18px 48px #26323d1c, 0 3px 10px #26323d0d;
|
|
21
|
+
--success: #1f7a5b;
|
|
22
|
+
--warning: #91621f;
|
|
23
|
+
--danger: #b74742;
|
|
24
|
+
--font-ui: "Inter Variable", "Noto Sans SC Variable", "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
25
|
+
--font-code: "IBM Plex Mono", "SFMono-Regular", Menlo, Consolas, monospace;
|
|
26
|
+
font-family: var(--font-ui);
|
|
27
|
+
color: var(--ink);
|
|
28
|
+
background: var(--page);
|
|
29
|
+
}
|
|
30
|
+
* { box-sizing: border-box; }
|
|
31
|
+
body { margin: 0; }
|
|
32
|
+
button, select, input { font: inherit; }
|
|
33
|
+
button { cursor: pointer; }
|
|
34
|
+
button:disabled { cursor: wait; opacity: .52; }
|
|
35
|
+
button:focus-visible, select:focus-visible, input:focus-visible, summary:focus-visible { outline: 3px solid #9da8b3; outline-offset: 3px; }
|
|
36
|
+
#canvas { position: fixed; inset: 0; width: 100%; height: 100%; border: 0; background: var(--page); }
|
|
37
|
+
|
|
38
|
+
/* Platform connection */
|
|
39
|
+
.setup { position: fixed; z-index: 5; inset: 0; display: flex; flex-direction: column; overflow: auto; background: var(--page); }
|
|
40
|
+
.setup[hidden] { display: none; }
|
|
41
|
+
.setup-header { flex: none; height: 64px; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 0 clamp(24px, 4vw, 56px); border-bottom: 1px solid var(--line); background: var(--surface); }
|
|
42
|
+
.brand { display: flex; align-items: center; gap: 13px; min-width: 0; font-size: 13px; font-weight: 600; white-space: nowrap; }
|
|
43
|
+
.brand strong { font-size: 19px; font-weight: 700; letter-spacing: -.055em; }
|
|
44
|
+
.brand-divider { width: 1px; height: 20px; background: var(--line-strong); }
|
|
45
|
+
.local-service { display: flex; align-items: center; gap: 8px; color: var(--quiet); font-size: 12px; white-space: nowrap; }
|
|
46
|
+
.service-dot { flex: none; width: 7px; height: 7px; border-radius: 50%; background: #9da6a2; }
|
|
47
|
+
.local-service[data-state="ready"] .service-dot { background: var(--success); }
|
|
48
|
+
.local-service[data-state="error"] { color: var(--danger); }
|
|
49
|
+
.local-service[data-state="error"] .service-dot { background: var(--danger); }
|
|
50
|
+
.setup-body { flex: none; display: grid; grid-template-columns: minmax(0, 1.32fr) minmax(320px, 1fr); width: min(100% - 48px, 1000px); margin: auto; border: 1px solid var(--line-strong); border-radius: 20px; overflow: hidden; background: var(--surface); box-shadow: 0 18px 54px #26323d0e, 0 2px 8px #26323d08; }
|
|
51
|
+
.setup-content { min-width: 0; padding: 58px clamp(32px, 4vw, 58px) 64px; }
|
|
52
|
+
.setup-content h1 { margin: 0 0 12px; font-size: 30px; line-height: 1.35; font-weight: 600; }
|
|
53
|
+
.setup-intro { margin: 0; color: var(--muted); font-size: 14px; line-height: 1.7; }
|
|
54
|
+
#connect-form { max-width: 496px; margin-top: 34px; }
|
|
55
|
+
#connect-form label { display: block; margin-bottom: 10px; font-size: 13px; font-weight: 600; }
|
|
56
|
+
#connect-form input { display: block; width: 100%; height: 50px; padding: 0 16px; border: 1px solid var(--line-strong); border-radius: 12px; color: var(--ink); background: var(--surface); box-shadow: var(--control-shadow); font-size: 14px; transition: border-color .15s, box-shadow .15s; }
|
|
57
|
+
#connect-form input::placeholder { color: #89918e; }
|
|
58
|
+
#connect-form input:hover { border-color: #abb5be; }
|
|
59
|
+
#connect-form input:focus { border-color: #8996a2; outline: 0; box-shadow: 0 0 0 4px #8795a322, var(--control-shadow); }
|
|
60
|
+
.field-help { margin: 9px 0 0; color: var(--quiet); font-size: 12px; line-height: 1.6; }
|
|
61
|
+
.form-actions { margin-top: 25px; }
|
|
62
|
+
.form-actions button { display: inline-flex; align-items: center; justify-content: center; gap: 9px; min-width: 174px; min-height: 48px; border: 1px solid var(--line-strong); border-radius: 13px; padding: 0 18px; color: var(--ink); background: #fff; box-shadow: var(--control-shadow); font-size: 14px; font-weight: 600; }
|
|
63
|
+
.form-actions button:hover:not(:disabled) { border-color: #bbc4cd; background: var(--soft); box-shadow: 0 2px 7px #26323d1a; }
|
|
64
|
+
.form-actions button svg { width: 18px; height: 18px; flex: none; }
|
|
65
|
+
#setup-status { color: var(--muted); font-size: 13px; line-height: 1.55; }
|
|
66
|
+
#setup-status:not(:empty) { margin: 20px 0 0; padding: 14px 16px; border: 1px solid var(--line); border-radius: 12px; background: var(--soft); }
|
|
67
|
+
#setup-status:empty { display: none; }
|
|
68
|
+
#setup-status.setup-error { border-color: #eed4d0; color: var(--danger); background: #fff7f5; }
|
|
69
|
+
.diagnostics { min-width: 0; padding: 58px 36px 54px; border-left: 1px solid var(--line); background: var(--soft); }
|
|
70
|
+
.diagnostics-inner { width: 100%; }
|
|
71
|
+
.diagnostics-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 38px; }
|
|
72
|
+
.diagnostics-head h2 { margin: 0; font-size: 16px; font-weight: 600; }
|
|
73
|
+
.phase-code { border: 1px solid var(--line); border-radius: 8px; padding: 6px 9px; color: var(--quiet); background: #fff; box-shadow: var(--control-shadow); font: 500 10px/1.2 var(--font-code); letter-spacing: .03em; }
|
|
74
|
+
.diagnostics-overview { margin-bottom: 34px; }
|
|
75
|
+
.diagnostics-overview strong { display: block; margin-bottom: 9px; color: var(--ink); font-size: 23px; line-height: 1.35; font-weight: 600; }
|
|
76
|
+
.diagnostics-overview strong.diag-ready { color: var(--success); }
|
|
77
|
+
.diagnostics-overview strong.diag-error { color: var(--danger); }
|
|
78
|
+
.diagnostics-overview p { margin: 0; color: var(--muted); font-size: 12px; line-height: 1.65; }
|
|
79
|
+
.diagnostics-list { margin: 0; border-top: 1px solid var(--line-strong); }
|
|
80
|
+
.diagnostics-list > div { display: grid; grid-template-columns: 112px minmax(0, 1fr); gap: 14px; align-items: center; min-height: 62px; border-bottom: 1px solid var(--line); }
|
|
81
|
+
.diagnostics-list dt { color: var(--quiet); font-size: 12px; }
|
|
82
|
+
.diagnostics-list dd { margin: 0; overflow: hidden; color: var(--ink); font-size: 13px; font-weight: 600; text-align: right; text-overflow: ellipsis; white-space: nowrap; }
|
|
83
|
+
.diagnostics-list code { font: 500 12px var(--font-code); }
|
|
84
|
+
.diagnostics-list .diag-muted { color: var(--quiet); font-weight: 500; }
|
|
85
|
+
.diagnostics-list .diag-error { color: var(--danger); }
|
|
86
|
+
.diagnostics-list .diag-ready { color: var(--success); }
|
|
87
|
+
.diagnostics-footnote { margin: 18px 0 0; color: var(--quiet); font-size: 11px; }
|
|
88
|
+
|
|
89
|
+
/* Canvas controls */
|
|
90
|
+
.dock { position: fixed; z-index: 3; left: 50%; bottom: 16px; transform: translateX(-50%); display: flex; align-items: center; gap: 3px; max-width: calc(100vw - 16px); padding: 5px; border: 1px solid var(--line-strong); border-radius: 13px; background: #fff; box-shadow: var(--floating-shadow); }
|
|
91
|
+
.dock[hidden] { display: none; }
|
|
92
|
+
.dock-details, .identity, .controls { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
|
93
|
+
.identity { min-height: 36px; max-width: min(40vw, 360px); padding: 0 9px 0 3px; }
|
|
94
|
+
.identity strong { overflow: hidden; font-size: 12px; white-space: nowrap; text-overflow: ellipsis; }
|
|
95
|
+
.identity small { color: var(--quiet); font-size: 11px; white-space: nowrap; }
|
|
96
|
+
.dock button { display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-height: 36px; border: 0; border-radius: 8px; padding: 0 9px; color: var(--ink); background: transparent; font-size: 12px; font-weight: 600; white-space: nowrap; }
|
|
97
|
+
.dock button:hover:not(:disabled) { background: var(--page); }
|
|
98
|
+
.dock button svg { display: block; width: 16px; height: 16px; flex: none; }
|
|
99
|
+
.dock-grip { width: 34px; padding: 0 !important; cursor: grab; touch-action: none; color: #8b9590 !important; }
|
|
100
|
+
.dock-grip:active { cursor: grabbing; }
|
|
101
|
+
.dock-collapse { width: 34px; padding: 0 !important; }
|
|
102
|
+
.dock-expand { padding: 0 10px !important; }
|
|
103
|
+
.dock.compact .dock-details, .dock.compact .dock-collapse { display: none; }
|
|
104
|
+
.dock:not(.compact) .dock-expand { display: none; }
|
|
105
|
+
.dot { width: 7px; height: 7px; flex: none; border-radius: 50%; background: #9da6a2; }
|
|
106
|
+
.dot.ready { background: var(--success); }
|
|
107
|
+
.dot.warning { background: #b98530; }
|
|
108
|
+
.dot.error { background: var(--danger); }
|
|
109
|
+
.controls { gap: 2px; padding-left: 8px; border-left: 1px solid var(--line); }
|
|
110
|
+
.controls .primary { background: #f2f4f6; }
|
|
111
|
+
.controls .primary:hover:not(:disabled) { background: #e9edf0; }
|
|
112
|
+
.dialog-actions .primary, .empty .primary { color: var(--ink); background: #f2f4f6; }
|
|
113
|
+
.dialog-actions .primary:hover, .empty .primary:hover { color: var(--ink); background: #e9edf0; }
|
|
114
|
+
.notice { position: fixed; z-index: 2; left: 50%; bottom: 74px; transform: translateX(-50%); max-width: min(90vw, 620px); padding: 11px 15px; border: 1px solid #e9d8b8; border-radius: 12px; color: #79551d; background: #fff9ed; box-shadow: var(--control-shadow); font-size: 12px; }
|
|
115
|
+
.notice[hidden], .empty[hidden] { display: none; }
|
|
116
|
+
.workbench-dialog-open .dock, .workbench-dialog-open .notice { visibility: hidden; pointer-events: none; }
|
|
117
|
+
.empty { position: fixed; z-index: 2; inset: 0; display: grid; place-content: center; gap: 14px; padding: 24px; color: var(--ink); background: var(--page); text-align: center; }
|
|
118
|
+
.empty h1 { margin: 0; font-size: 20px; }
|
|
119
|
+
.empty p { margin: 0; color: var(--quiet); font-size: 13px; }
|
|
120
|
+
.empty button { justify-self: center; min-height: 40px; border: 0; border-radius: 10px; padding: 0 16px; font-size: 12px; font-weight: 600; }
|
|
121
|
+
|
|
122
|
+
/* Plugin management dialog */
|
|
123
|
+
dialog { width: min(820px, calc(100vw - 32px)); max-width: calc(100vw - 32px); max-height: min(720px, calc(100dvh - 32px)); margin: auto; overflow: hidden; border: 1px solid var(--line-strong); border-radius: 18px; padding: 0; color: var(--ink); background: #fff; box-shadow: var(--floating-shadow); }
|
|
124
|
+
dialog[open] { display: flex; flex-direction: column; }
|
|
125
|
+
dialog::backdrop { background: #30384266; }
|
|
126
|
+
.dialog-head { flex: none; display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding: 24px 28px 20px; border-bottom: 1px solid var(--line); }
|
|
127
|
+
.dialog-head h2 { margin: 0 0 5px; font-size: 20px; line-height: 1.35; font-weight: 600; }
|
|
128
|
+
.dialog-head p { margin: 0; color: var(--quiet); font-size: 12px; }
|
|
129
|
+
.dialog-head button { display: grid; place-items: center; width: 36px; height: 36px; flex: none; border: 0; border-radius: 9px; color: var(--quiet); background: transparent; }
|
|
130
|
+
.dialog-head button:hover { color: var(--ink); background: var(--page); }
|
|
131
|
+
.dialog-head svg { width: 18px; height: 18px; }
|
|
132
|
+
.dialog-body { flex: 1; min-height: 0; display: grid; grid-template-columns: minmax(0, 1fr) 290px; overflow: hidden; }
|
|
133
|
+
.dialog-main { min-width: 0; overflow: auto; padding: 0 28px 26px; }
|
|
134
|
+
.target-section, .session-section { padding: 24px 0; }
|
|
135
|
+
.target-section { border-bottom: 1px solid var(--line); }
|
|
136
|
+
.section-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 15px; }
|
|
137
|
+
.section-heading h3 { margin: 0; font-size: 13px; font-weight: 700; }
|
|
138
|
+
.status-badge { display: inline-flex; align-items: center; gap: 7px; padding: 6px 10px; border-radius: 999px; color: var(--warning); background: #faf1df; font-size: 11px; font-weight: 600; white-space: nowrap; }
|
|
139
|
+
.status-badge::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
140
|
+
.status-badge.ready { color: var(--success); background: #e9f4ef; }
|
|
141
|
+
.status-badge.error { color: var(--danger); background: #fff1ef; }
|
|
142
|
+
.plugin-list { display: grid; gap: 8px; max-height: 224px; overflow: auto; }
|
|
143
|
+
.plugin-list:empty::before { content: "暂无可调试的插件"; padding: 18px; border: 1px dashed var(--line-strong); border-radius: 12px; color: var(--quiet); font-size: 12px; text-align: center; }
|
|
144
|
+
.plugin-choice { position: relative; display: grid; grid-template-columns: 18px minmax(0, 1fr) auto; align-items: center; gap: 10px; min-height: 68px; border: 1px solid var(--line); border-radius: 12px; padding: 10px 12px; background: #fff; cursor: pointer; }
|
|
145
|
+
.plugin-choice:hover { border-color: var(--line-strong); background: var(--soft); }
|
|
146
|
+
.plugin-choice:has(input:checked) { border-color: #aab6c1; background: #f9fafb; }
|
|
147
|
+
.plugin-choice:focus-within { outline: 2px solid #8e9ca9; outline-offset: -3px; }
|
|
148
|
+
.plugin-choice input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
|
|
149
|
+
.plugin-choice-mark { width: 16px; height: 16px; border: 1.5px solid #a9b3bd; border-radius: 50%; background: #fff; }
|
|
150
|
+
.plugin-choice:has(input:checked) .plugin-choice-mark { border: 5px solid #46535f; }
|
|
151
|
+
.plugin-choice-copy { display: grid; gap: 3px; min-width: 0; }
|
|
152
|
+
.plugin-choice-copy strong, .plugin-choice-copy code { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
153
|
+
.plugin-choice-copy strong { color: var(--ink); font-size: 13px; font-weight: 600; }
|
|
154
|
+
.plugin-choice-copy code { color: var(--quiet); font: 400 10px/1.4 var(--font-code); }
|
|
155
|
+
.plugin-choice-meta { color: var(--quiet); font-size: 11px; white-space: nowrap; }
|
|
156
|
+
.plugin-choice-meta.disabled { color: var(--danger); }
|
|
157
|
+
.plugin-actions { display: flex; align-items: center; gap: 8px; margin-top: 14px; }
|
|
158
|
+
.plugin-actions button { display: inline-flex; align-items: center; justify-content: center; gap: 7px; min-height: 40px; border: 1px solid var(--line-strong); border-radius: 10px; padding: 0 12px; color: var(--ink); background: #fff; box-shadow: var(--control-shadow); font-size: 12px; font-weight: 600; }
|
|
159
|
+
.plugin-actions button:hover:not(:disabled) { background: var(--page); }
|
|
160
|
+
.plugin-actions #open-plugin { background: #f2f4f6; }
|
|
161
|
+
.plugin-actions svg { width: 16px; height: 16px; flex: none; }
|
|
162
|
+
.package-note { margin: 10px 0 0; color: var(--quiet); font-size: 11px; line-height: 1.5; }
|
|
163
|
+
.package-note:empty { display: none; }
|
|
164
|
+
.package-note.error { color: var(--danger); }
|
|
165
|
+
.section-context { overflow: hidden; color: var(--quiet); font-size: 11px; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
|
|
166
|
+
.session-summary { margin: 0; }
|
|
167
|
+
.session-summary > div { display: flex; align-items: center; justify-content: space-between; gap: 14px; min-height: 51px; border-top: 1px solid #e9ebea; }
|
|
168
|
+
.session-summary > div:first-child { border-top: 0; }
|
|
169
|
+
.session-summary dt { color: var(--muted); font-size: 12px; }
|
|
170
|
+
.session-summary dd { max-width: 62%; margin: 0; overflow: hidden; color: var(--ink); font-size: 12px; font-weight: 650; text-align: right; text-overflow: ellipsis; white-space: nowrap; }
|
|
171
|
+
.technical-details { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 18px; }
|
|
172
|
+
.technical-details summary { width: fit-content; color: var(--accent); font-size: 12px; font-weight: 650; cursor: pointer; }
|
|
173
|
+
.technical-details dl { display: grid; grid-template-columns: 84px minmax(0, 1fr); gap: 10px 12px; margin: 14px 0 0; padding: 14px; border: 1px solid var(--line); border-radius: 12px; background: var(--soft); font-size: 11px; }
|
|
174
|
+
.technical-details dt { color: var(--quiet); }
|
|
175
|
+
.technical-details dd { margin: 0; overflow-wrap: anywhere; font-family: var(--font-code); }
|
|
176
|
+
.activity-section { display: flex; flex-direction: column; min-width: 0; overflow: hidden; padding: 24px 18px; border-left: 1px solid var(--line); background: var(--soft); }
|
|
177
|
+
.activity-section > p { margin: -6px 0 16px; color: var(--quiet); font-size: 12px; line-height: 1.5; }
|
|
178
|
+
.activity-count { color: var(--quiet); font-size: 11px; }
|
|
179
|
+
.activity-list { flex: 1; min-height: 120px; max-height: 350px; overflow: auto; border-top: 1px solid var(--line); }
|
|
180
|
+
.activity-list:empty::before { content: "暂无活动"; display: block; padding: 25px; color: var(--quiet); font-size: 12px; text-align: center; }
|
|
181
|
+
.activity-item { display: grid; grid-template-columns: 58px minmax(0, 1fr); gap: 10px; padding: 11px 2px; color: var(--muted); font-size: 12px; line-height: 1.5; }
|
|
182
|
+
.activity-item + .activity-item { border-top: 1px solid #eceeed; }
|
|
183
|
+
.activity-time { color: #69736e; font: 400 11px/1.6 var(--font-code); }
|
|
184
|
+
.activity-message { overflow-wrap: anywhere; }
|
|
185
|
+
.dialog-actions { flex: none; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 15px 28px; border-top: 1px solid var(--line); background: #fff; }
|
|
186
|
+
.dialog-actions > div { display: flex; gap: 8px; }
|
|
187
|
+
.dialog-actions button { display: inline-flex; align-items: center; justify-content: center; gap: 7px; min-height: 42px; border: 1px solid var(--line-strong); border-radius: 11px; padding: 0 15px; color: var(--ink); background: #fff; box-shadow: var(--control-shadow); font-size: 12px; font-weight: 600; white-space: nowrap; }
|
|
188
|
+
.dialog-actions button svg { width: 16px; height: 16px; flex: none; }
|
|
189
|
+
.dialog-actions button:hover { background: var(--page); }
|
|
190
|
+
.dialog-actions #disconnect-panel { border-color: transparent; color: var(--muted); box-shadow: none; }
|
|
191
|
+
.dialog-actions .primary { border-color: #c7cdd4; }
|
|
192
|
+
|
|
193
|
+
@media (max-width: 900px) {
|
|
194
|
+
.setup-body { display: flex; flex-direction: column; width: min(620px, calc(100% - 40px)); }
|
|
195
|
+
.setup-content { padding: 42px 46px 46px; }
|
|
196
|
+
.diagnostics { padding: 32px 46px 38px; border-top: 1px solid var(--line); border-left: 0; }
|
|
197
|
+
}
|
|
198
|
+
@media (max-width: 760px) {
|
|
199
|
+
.dialog-body { grid-template-columns: 1fr; overflow: auto; }
|
|
200
|
+
.dialog-main { overflow: visible; }
|
|
201
|
+
.activity-section { min-height: 180px; border-top: 1px solid var(--line); border-left: 0; }
|
|
202
|
+
.activity-list { max-height: 180px; }
|
|
203
|
+
}
|
|
204
|
+
@media (max-width: 520px) {
|
|
205
|
+
dialog { height: min(760px, calc(100dvh - 32px)); }
|
|
206
|
+
.setup-header { height: 58px; padding: 0 18px; }
|
|
207
|
+
.brand strong { font-size: 18px; }
|
|
208
|
+
.brand { font-size: 12px; }
|
|
209
|
+
.local-service { font-size: 11px; }
|
|
210
|
+
.setup-body { width: calc(100% - 28px); margin: 22px auto 28px; border-radius: 16px; }
|
|
211
|
+
.setup-content { padding: 30px 24px 34px; }
|
|
212
|
+
.diagnostics { padding: 28px 24px 32px; }
|
|
213
|
+
.setup-content h1 { font-size: 25px; }
|
|
214
|
+
#connect-form { margin-top: 32px; }
|
|
215
|
+
.form-actions button { width: 100%; }
|
|
216
|
+
.dock { max-width: calc(100vw - 12px); bottom: 8px; }
|
|
217
|
+
.dock-details { gap: 3px; }
|
|
218
|
+
.identity { min-height: 44px; max-width: 98px; padding-inline: 7px; }
|
|
219
|
+
.identity strong { font-size: 11px; }
|
|
220
|
+
.identity small, .controls #disconnect { display: none; }
|
|
221
|
+
.controls { gap: 1px; padding-left: 5px; }
|
|
222
|
+
.controls button { width: 44px; padding-inline: 0; }
|
|
223
|
+
.controls .button-label { display: none; }
|
|
224
|
+
.dialog-head { padding: 18px 20px; }
|
|
225
|
+
.dialog-head button { width: 44px; height: 44px; margin: -6px -6px 0 0; }
|
|
226
|
+
.dialog-main { padding-inline: 20px; }
|
|
227
|
+
.target-section, .session-section { padding-block: 18px; }
|
|
228
|
+
.section-heading { margin-bottom: 12px; }
|
|
229
|
+
.session-summary > div { min-height: 42px; }
|
|
230
|
+
.technical-details { padding-top: 12px; }
|
|
231
|
+
.activity-section { padding: 18px 20px; }
|
|
232
|
+
.dialog-actions { padding: 12px 20px; }
|
|
233
|
+
.dialog-actions button { min-height: 44px; padding-inline: 9px; }
|
|
234
|
+
.dock button { min-height: 44px; padding-inline: 0; }
|
|
235
|
+
.dock-grip, .dock-collapse { width: 44px; }
|
|
236
|
+
}
|
|
237
|
+
@media (max-width: 375px) {
|
|
238
|
+
.plugin-choice { grid-template-columns: 18px minmax(0, 1fr); }
|
|
239
|
+
.plugin-choice-copy code { white-space: normal; overflow-wrap: anywhere; }
|
|
240
|
+
.plugin-choice-meta { grid-column: 2; }
|
|
241
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
242
|
+
.dialog-actions > div { margin-left: auto; }
|
|
243
|
+
.dialog-actions button { padding-inline: 8px; }
|
|
244
|
+
}
|
|
245
|
+
@media (prefers-reduced-motion: reduce) {
|
|
246
|
+
#connect-form input { transition: none; }
|
|
247
|
+
}
|
|
248
|
+
</style>
|
|
249
|
+
</head>
|
|
250
|
+
<body>
|
|
251
|
+
<iframe id="canvas" title="真实画布插件调试"></iframe>
|
|
252
|
+
<main class="setup" id="setup">
|
|
253
|
+
<header class="setup-header"><div class="brand"><strong>EasyAI</strong><span class="brand-divider"></span><span>插件开发工作台</span></div><div id="local-service" class="local-service" data-state="checking" role="status" aria-live="polite"><span class="service-dot" aria-hidden="true"></span><span id="local-service-text">正在检查本地服务</span></div></header>
|
|
254
|
+
<div class="setup-body">
|
|
255
|
+
<section class="setup-content" aria-labelledby="connection-title">
|
|
256
|
+
<h1 id="connection-title">连接画布平台</h1>
|
|
257
|
+
<p class="setup-intro">输入平台地址,在新窗口授权后进入私人调试画布。</p>
|
|
258
|
+
<form id="connect-form"><label for="platform-address">画布平台地址</label><input id="platform-address" type="url" placeholder="https://canvas.example.com" autocomplete="url" aria-describedby="address-help" required><p id="address-help" class="field-help">线上平台使用 HTTPS,本机平台可使用 HTTP。</p><div class="form-actions"><button id="connect-button" type="submit" disabled><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.5 10h11m-4-4 4 4-4 4M15.5 4h2v12h-2"/></svg><span>登录并连接</span></button></div></form>
|
|
259
|
+
<p id="setup-status" role="status" aria-live="polite"></p>
|
|
260
|
+
</section>
|
|
261
|
+
<aside class="diagnostics" aria-labelledby="diagnostics-title"><div class="diagnostics-inner">
|
|
262
|
+
<div class="diagnostics-head"><h2 id="diagnostics-title">连接诊断</h2><code id="diag-phase" class="phase-code">CHECKING</code></div>
|
|
263
|
+
<div class="diagnostics-overview"><strong id="diag-auth">等待连接</strong><p id="diag-description">正在检查本地插件服务。</p></div>
|
|
264
|
+
<dl class="diagnostics-list"><div><dt>本地服务</dt><dd id="diag-service">检查中</dd></div><div><dt>服务地址</dt><dd><code id="diag-local"></code></dd></div><div><dt>目标平台</dt><dd><code id="diag-platform" class="diag-muted">未指定</code></dd></div></dl>
|
|
265
|
+
<p class="diagnostics-footnote">状态每 2 秒更新</p>
|
|
266
|
+
</div></aside>
|
|
267
|
+
</div>
|
|
268
|
+
</main>
|
|
269
|
+
<div class="empty" id="empty"><h1>正在连接插件调试画布</h1><p id="empty-text">读取本地插件目录…</p><button id="retry" class="primary" type="button" hidden>重试</button></div>
|
|
270
|
+
<div id="dock" class="dock" role="toolbar" aria-label="插件调试工具条">
|
|
271
|
+
<button id="dock-grip" class="dock-grip" type="button" aria-label="拖动工具条,或用方向键移动" title="拖动或用方向键移动"><svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><circle cx="5" cy="4" r="1.25"/><circle cx="11" cy="4" r="1.25"/><circle cx="5" cy="8" r="1.25"/><circle cx="11" cy="8" r="1.25"/><circle cx="5" cy="12" r="1.25"/><circle cx="11" cy="12" r="1.25"/></svg></button>
|
|
272
|
+
<div class="dock-details">
|
|
273
|
+
<div class="identity" aria-live="polite"><span id="dot" class="dot"></span><strong id="plugin-name">插件调试</strong><small id="state">正在连接</small></div>
|
|
274
|
+
<div class="controls"><button id="refresh" class="primary" type="button" aria-label="刷新构建" disabled><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16.6 8.5A6.7 6.7 0 1 0 17 12M16.5 3.5v5h-5"/></svg><span class="button-label">刷新构建</span></button><button id="disconnect" type="button"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 2.5v7M5.1 5.4a7.5 7.5 0 1 0 9.8 0"/></svg><span class="button-label">断开连接</span></button><button id="manage" type="button" aria-label="管理插件调试"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.5" y="2.5" width="6" height="6" rx="1.4"/><rect x="11.5" y="11.5" width="6" height="6" rx="1.4"/><path d="M8.5 5.5h3a3 3 0 0 1 3 3v3M5.5 8.5v3a3 3 0 0 0 3 3h3"/></svg><span class="button-label">管理</span></button></div>
|
|
275
|
+
</div>
|
|
276
|
+
<button id="dock-collapse" class="dock-collapse" type="button" aria-label="收起插件调试工具条" title="收起"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m3.5 6 4.5 4.5L12.5 6"/></svg></button>
|
|
277
|
+
<button id="dock-expand" class="dock-expand" type="button" aria-label="展开插件调试工具条" title="展开插件调试工具条">插件调试</button>
|
|
278
|
+
</div>
|
|
279
|
+
<div id="notice" class="notice" role="status" hidden></div>
|
|
280
|
+
<dialog id="panel" aria-labelledby="panel-title">
|
|
281
|
+
<div class="dialog-head"><div><h2 id="panel-title">插件管理</h2><p>选择调试插件并下载可用的插件包</p></div><button id="close" type="button" aria-label="关闭管理面板"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M4.5 4.5 15.5 15.5M15.5 4.5 4.5 15.5"/></svg></button></div>
|
|
282
|
+
<div class="dialog-body"><div class="dialog-main"><section class="target-section" aria-labelledby="target-title"><div class="section-heading"><h3 id="target-title">插件</h3><span id="panel-status" class="status-badge" role="status">正在连接</span></div><div id="plugin-list" class="plugin-list" role="radiogroup" aria-labelledby="target-title"></div><div class="plugin-actions"><button id="open-plugin" type="button"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 10h11m-4-4 4 4-4 4M16 4h2v12h-2"/></svg><span>在画布中调试</span></button><button id="download-package" type="button" disabled><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 2.5v10m-3.5-3.5L10 12.5 13.5 9M3.5 14v2.5h13V14"/></svg><span id="download-label">下载插件包</span></button></div><p id="package-note" class="package-note" role="status" aria-live="polite"></p></section>
|
|
283
|
+
<section class="session-section" aria-labelledby="session-title"><div class="section-heading"><h3 id="session-title">当前会话</h3><span id="active-plugin" class="section-context"></span></div><dl class="session-summary"><div><dt>开发服务</dt><dd id="connection">—</dd></div><div><dt>当前构建</dt><dd id="build">—</dd></div><div><dt>调试画布</dt><dd id="project">—</dd></div></dl><details class="technical-details"><summary>查看连接详情</summary><dl><dt>连接 ID</dt><dd id="connection-id">—</dd><dt>构建摘要</dt><dd id="build-full">—</dd><dt>项目 ID</dt><dd id="project-id">—</dd></dl></details></section></div>
|
|
284
|
+
<aside class="activity-section" aria-labelledby="activity-title"><div class="section-heading"><h3 id="activity-title">最近活动</h3><span id="activity-count" class="activity-count">0 条</span></div><p>当前工作台会话的连接与构建记录</p><div id="log" class="activity-list" role="log" aria-label="调试活动" aria-live="polite"></div></aside></div>
|
|
285
|
+
<div class="dialog-actions"><button id="disconnect-panel" type="button">退出连接</button><div><button id="panel-cancel" type="button">关闭</button><button id="reconnect" class="primary" type="button"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16.6 8.5A6.7 6.7 0 1 0 17 12M16.5 3.5v5h-5"/></svg><span>重新连接</span></button></div></div>
|
|
286
|
+
</dialog>
|
|
287
|
+
<script>
|
|
288
|
+
(() => {
|
|
289
|
+
const protocol = 'easyai.plugin-debug/v1';
|
|
290
|
+
const $ = id => document.getElementById(id);
|
|
291
|
+
const frame = $('canvas'), panel = $('panel'), dock = $('dock');
|
|
292
|
+
let connectorStatus = null, connectAttemptError = '', localServiceAvailable = false, connectInProgress = false;
|
|
293
|
+
try { $('platform-address').value = localStorage.getItem('plugin-debug-platform-url') || ''; } catch {}
|
|
294
|
+
$('diag-local').textContent = window.location.host;
|
|
295
|
+
function renderTargetAddress(raw = $('platform-address').value) {
|
|
296
|
+
const value = raw.trim();
|
|
297
|
+
let host = '未指定';
|
|
298
|
+
if (value) { try { host = new URL(value).host || '地址待确认'; } catch { host = '地址待确认'; } }
|
|
299
|
+
$('diag-platform').textContent = host;
|
|
300
|
+
$('diag-platform').title = value;
|
|
301
|
+
$('diag-platform').className = host === '未指定' || host === '地址待确认' ? 'diag-muted' : '';
|
|
302
|
+
}
|
|
303
|
+
$('platform-address').addEventListener('input', () => renderTargetAddress());
|
|
304
|
+
renderTargetAddress();
|
|
305
|
+
function setSetupMessage(message) { if ($('setup-status').textContent !== message) $('setup-status').textContent = message; }
|
|
306
|
+
function renderDiagnosticPhase(phase) {
|
|
307
|
+
const state = !localServiceAvailable ? 'service-error' : phase === 'idle' && connectInProgress ? 'checking' : phase === 'idle' && connectAttemptError ? 'error' : phase;
|
|
308
|
+
const labels = { idle: '等待连接', checking: '检查平台地址', authorizing: '等待平台授权', connecting: '正在建立连接', connected: '已授权', disconnected: '连接中断', error: '平台连接失败', 'service-error': '服务不可用' };
|
|
309
|
+
const descriptions = { idle: '输入平台地址后开始授权。', checking: '正在验证你输入的平台地址。', authorizing: '请在新窗口完成平台登录与授权。', connecting: '正在建立插件服务与画布的连接。', connected: '授权已完成,正在打开调试画布。', disconnected: '连接已中断,请查看左侧错误信息。', error: '请查看左侧错误信息并修改地址后重试。', 'service-error': '检查开发命令,服务恢复后可继续连接。' };
|
|
310
|
+
$('diag-phase').textContent = state.toUpperCase().replace('-', '_');
|
|
311
|
+
$('diag-auth').textContent = labels[state] || '等待连接';
|
|
312
|
+
$('diag-description').textContent = descriptions[state] || descriptions.idle;
|
|
313
|
+
$('diag-auth').className = state === 'connected' ? 'diag-ready' : state === 'error' || state === 'disconnected' || state === 'service-error' ? 'diag-error' : '';
|
|
314
|
+
}
|
|
315
|
+
function setLocalServiceState(state) {
|
|
316
|
+
$('local-service').dataset.state = state;
|
|
317
|
+
const message = state === 'ready' ? '本地服务在线' : state === 'error' ? '本地服务不可用' : '正在检查本地服务';
|
|
318
|
+
if ($('local-service-text').textContent !== message) $('local-service-text').textContent = message;
|
|
319
|
+
$('diag-service').textContent = state === 'ready' ? '在线' : state === 'error' ? '不可用' : '检查中';
|
|
320
|
+
$('diag-service').className = state === 'ready' ? 'diag-ready' : state === 'error' ? 'diag-error' : 'diag-muted';
|
|
321
|
+
localServiceAvailable = state === 'ready';
|
|
322
|
+
$('connect-button').disabled = !localServiceAvailable || connectInProgress;
|
|
323
|
+
}
|
|
324
|
+
async function loadConnectionStatus() {
|
|
325
|
+
try {
|
|
326
|
+
const response = await fetch('/studio/status', { cache: 'no-store' });
|
|
327
|
+
if (!response.ok) throw new Error('本地服务状态读取失败');
|
|
328
|
+
connectorStatus = await response.json();
|
|
329
|
+
setLocalServiceState('ready');
|
|
330
|
+
const targetAddress = connectorStatus.phase === 'idle' ? $('platform-address').value : connectorStatus.platformOrigin || $('platform-address').value;
|
|
331
|
+
renderTargetAddress(targetAddress);
|
|
332
|
+
const connected = connectorStatus.phase === 'connected';
|
|
333
|
+
if (connectorStatus.phase !== 'idle') connectAttemptError = '';
|
|
334
|
+
renderDiagnosticPhase(connectorStatus.phase);
|
|
335
|
+
$('setup').hidden = connected;
|
|
336
|
+
$('dock').hidden = !connected;
|
|
337
|
+
$('setup-status').classList.toggle('setup-error', connectorStatus.phase === 'disconnected' || Boolean(connectAttemptError));
|
|
338
|
+
setSetupMessage(connectorStatus.phase === 'authorizing' ? '请在打开的页面登录并允许插件开发连接。' : connectorStatus.phase === 'connecting' ? '正在建立连接…' : connectorStatus.phase === 'disconnected' ? connectorStatus.error || '连接已断开,正在重试…' : connectInProgress ? '正在检查画布平台…' : connectAttemptError);
|
|
339
|
+
if (connected && (!catalog || catalog.connectionId !== connectorStatus.connectionId)) {
|
|
340
|
+
frame.removeAttribute('src'); session = null; await loadCatalog();
|
|
341
|
+
}
|
|
342
|
+
} catch { setLocalServiceState('error'); renderDiagnosticPhase('service-error'); setSetupMessage('本地插件服务暂时不可用,请检查开发命令。'); $('setup-status').classList.add('setup-error'); }
|
|
343
|
+
}
|
|
344
|
+
$('connect-form').addEventListener('submit', async event => {
|
|
345
|
+
event.preventDefault();
|
|
346
|
+
const address = $('platform-address').value.trim();
|
|
347
|
+
const popup = window.open('about:blank', 'plugin-debug-login', 'popup,width=560,height=720');
|
|
348
|
+
connectInProgress = true;
|
|
349
|
+
$('connect-button').disabled = true;
|
|
350
|
+
connectAttemptError = '';
|
|
351
|
+
renderTargetAddress(address);
|
|
352
|
+
renderDiagnosticPhase('checking');
|
|
353
|
+
$('setup-status').classList.remove('setup-error');
|
|
354
|
+
setSetupMessage('正在检查画布平台…');
|
|
355
|
+
try {
|
|
356
|
+
const response = await fetch('/studio/connect', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ platformUrl: address }) });
|
|
357
|
+
const result = await response.json();
|
|
358
|
+
if (!response.ok) throw new Error(result.message || result.error || '无法连接画布平台');
|
|
359
|
+
try { localStorage.setItem('plugin-debug-platform-url', address); } catch {}
|
|
360
|
+
if (popup) popup.location.href = result.authorizeUrl;
|
|
361
|
+
else window.open(result.authorizeUrl, '_blank', 'noopener');
|
|
362
|
+
setSetupMessage('请在打开的页面登录并授权。');
|
|
363
|
+
renderDiagnosticPhase('authorizing');
|
|
364
|
+
} catch (error) { popup?.close(); connectAttemptError = error.message; renderDiagnosticPhase('error'); setSetupMessage(error.message); $('setup-status').classList.add('setup-error'); }
|
|
365
|
+
finally { connectInProgress = false; $('connect-button').disabled = !localServiceAvailable; }
|
|
366
|
+
});
|
|
367
|
+
async function disconnect() {
|
|
368
|
+
if (panel.open) panel.close();
|
|
369
|
+
await fetch('/studio/connect', { method: 'DELETE' }).catch(() => undefined);
|
|
370
|
+
connectAttemptError = '';
|
|
371
|
+
frame.removeAttribute('src'); session = null; catalog = null; await loadConnectionStatus();
|
|
372
|
+
}
|
|
373
|
+
$('disconnect').addEventListener('click', disconnect);
|
|
374
|
+
$('disconnect-panel').addEventListener('click', disconnect);
|
|
375
|
+
let catalog = null, selected = null, panelSelected = null, session = null, preparedSession = null, busy = false, frameLoading = false, frameFailed = false, waiting = null, readyTimer = null, noticeTimer = null, lastBuildState = '';
|
|
376
|
+
let pluginListKey = '', packageDownloading = false, packageMessage = '', packageError = false;
|
|
377
|
+
const requestedPlugin = new URLSearchParams(window.location.search).get('plugin');
|
|
378
|
+
let canvasDialogOpen = false, manuallyCollapsed = false, expandedOverDialog = false, dockPosition = null, drag = null;
|
|
379
|
+
const lines = [];
|
|
380
|
+
const log = message => {
|
|
381
|
+
const time = new Date().toLocaleTimeString('zh-CN', { hour12: false });
|
|
382
|
+
lines.push({ time, message });
|
|
383
|
+
const item = document.createElement('div'); item.className = 'activity-item';
|
|
384
|
+
const stamp = document.createElement('time'); stamp.className = 'activity-time'; stamp.textContent = time;
|
|
385
|
+
const content = document.createElement('span'); content.className = 'activity-message'; content.textContent = message;
|
|
386
|
+
item.append(stamp, content); $('log').append(item);
|
|
387
|
+
while ($('log').children.length > 70) $('log').firstElementChild.remove();
|
|
388
|
+
$('activity-count').textContent = `${lines.length} 条`;
|
|
389
|
+
$('log').scrollTop = $('log').scrollHeight;
|
|
390
|
+
};
|
|
391
|
+
const show = (message, warning = true) => {
|
|
392
|
+
clearTimeout(noticeTimer);
|
|
393
|
+
$('notice').hidden = !message;
|
|
394
|
+
$('notice').textContent = message;
|
|
395
|
+
$('dot').className = `dot ${warning ? 'warning' : 'ready'}`;
|
|
396
|
+
if (message && !warning) noticeTimer = setTimeout(() => { $('notice').hidden = true; }, 4500);
|
|
397
|
+
};
|
|
398
|
+
function placeDock(left, top) {
|
|
399
|
+
const rect = dock.getBoundingClientRect();
|
|
400
|
+
dockPosition = {
|
|
401
|
+
left: Math.min(Math.max(8, left), Math.max(8, window.innerWidth - rect.width - 8)),
|
|
402
|
+
top: Math.min(Math.max(8, top), Math.max(8, window.innerHeight - rect.height - 8))
|
|
403
|
+
};
|
|
404
|
+
Object.assign(dock.style, { left: `${dockPosition.left}px`, top: `${dockPosition.top}px`, bottom: 'auto', transform: 'none' });
|
|
405
|
+
}
|
|
406
|
+
function rememberDock() {
|
|
407
|
+
try {
|
|
408
|
+
if (dockPosition) localStorage.setItem('plugin-debug-dock-position', JSON.stringify(dockPosition));
|
|
409
|
+
} catch { /* 拒绝存储时仍可在当前页面移动工具条。 */ }
|
|
410
|
+
}
|
|
411
|
+
function renderDock() {
|
|
412
|
+
document.body.classList.toggle('workbench-dialog-open', panel.open);
|
|
413
|
+
dock.classList.toggle('compact', manuallyCollapsed || (canvasDialogOpen && !expandedOverDialog));
|
|
414
|
+
if (dockPosition) requestAnimationFrame(() => placeDock(dockPosition.left, dockPosition.top));
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
const saved = JSON.parse(localStorage.getItem('plugin-debug-dock-position') || 'null');
|
|
418
|
+
if (Number.isFinite(saved?.left) && Number.isFinite(saved?.top)) requestAnimationFrame(() => placeDock(saved.left, saved.top));
|
|
419
|
+
} catch { /* Saved layout is optional. */ }
|
|
420
|
+
dock.addEventListener('pointerdown', event => {
|
|
421
|
+
if (event.button !== 0 || event.target.closest('button:not(#dock-grip)')) return;
|
|
422
|
+
const rect = dock.getBoundingClientRect();
|
|
423
|
+
drag = { pointerId: event.pointerId, x: event.clientX, y: event.clientY, left: rect.left, top: rect.top };
|
|
424
|
+
dock.setPointerCapture(event.pointerId);
|
|
425
|
+
});
|
|
426
|
+
dock.addEventListener('pointermove', event => {
|
|
427
|
+
if (!drag || event.pointerId !== drag.pointerId) return;
|
|
428
|
+
placeDock(drag.left + event.clientX - drag.x, drag.top + event.clientY - drag.y);
|
|
429
|
+
});
|
|
430
|
+
const finishDrag = event => {
|
|
431
|
+
if (!drag || event.pointerId !== drag.pointerId) return;
|
|
432
|
+
drag = null;
|
|
433
|
+
rememberDock();
|
|
434
|
+
};
|
|
435
|
+
dock.addEventListener('pointerup', finishDrag);
|
|
436
|
+
dock.addEventListener('pointercancel', finishDrag);
|
|
437
|
+
$('dock-grip').addEventListener('keydown', event => {
|
|
438
|
+
const directions = { ArrowLeft: [-20, 0], ArrowRight: [20, 0], ArrowUp: [0, -20], ArrowDown: [0, 20] };
|
|
439
|
+
const delta = directions[event.key];
|
|
440
|
+
if (!delta) return;
|
|
441
|
+
event.preventDefault();
|
|
442
|
+
const rect = dock.getBoundingClientRect();
|
|
443
|
+
placeDock(rect.left + delta[0], rect.top + delta[1]);
|
|
444
|
+
rememberDock();
|
|
445
|
+
});
|
|
446
|
+
window.addEventListener('resize', () => { if (dockPosition) placeDock(dockPosition.left, dockPosition.top); });
|
|
447
|
+
$('dock-collapse').addEventListener('click', () => { manuallyCollapsed = true; expandedOverDialog = false; renderDock(); });
|
|
448
|
+
$('dock-expand').addEventListener('click', () => { manuallyCollapsed = false; expandedOverDialog = canvasDialogOpen; renderDock(); });
|
|
449
|
+
const chosen = () => catalog?.plugins.find(item => item.id === selected);
|
|
450
|
+
const panelPlugin = () => catalog?.plugins.find(item => item.id === panelSelected);
|
|
451
|
+
function renderPluginList() {
|
|
452
|
+
const plugins = catalog?.plugins || [];
|
|
453
|
+
const key = JSON.stringify(plugins.map(item => [item.id, item.name, item.nodes, item.actions, item.enabled]));
|
|
454
|
+
if (key !== pluginListKey) {
|
|
455
|
+
pluginListKey = key;
|
|
456
|
+
$('plugin-list').replaceChildren();
|
|
457
|
+
for (const item of plugins) {
|
|
458
|
+
const row = document.createElement('label'); row.className = 'plugin-choice';
|
|
459
|
+
const input = document.createElement('input'); input.type = 'radio'; input.name = 'studio-plugin'; input.value = item.id;
|
|
460
|
+
const mark = document.createElement('span'); mark.className = 'plugin-choice-mark'; mark.setAttribute('aria-hidden', 'true');
|
|
461
|
+
const copy = document.createElement('span'); copy.className = 'plugin-choice-copy';
|
|
462
|
+
const name = document.createElement('strong'); name.textContent = item.name;
|
|
463
|
+
const id = document.createElement('code'); id.textContent = item.id;
|
|
464
|
+
copy.append(name, id);
|
|
465
|
+
const meta = document.createElement('span'); meta.className = `plugin-choice-meta${item.enabled ? '' : ' disabled'}`;
|
|
466
|
+
meta.textContent = item.enabled ? `${item.nodes} 个节点 / ${item.actions} 个操作` : '已停用';
|
|
467
|
+
row.append(input, mark, copy, meta); $('plugin-list').append(row);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
for (const input of $('plugin-list').querySelectorAll('input[name="studio-plugin"]')) {
|
|
471
|
+
input.checked = input.value === panelSelected;
|
|
472
|
+
input.disabled = packageDownloading;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const url = () => {
|
|
476
|
+
const address = new URL('/developer/plugin-debug', catalog.platformOrigin);
|
|
477
|
+
address.searchParams.set('pluginId', selected);
|
|
478
|
+
address.searchParams.set('connectionId', catalog.connectionId);
|
|
479
|
+
if (preparedSession?.sessionId) address.searchParams.set('sessionId', preparedSession.sessionId);
|
|
480
|
+
return address.toString();
|
|
481
|
+
};
|
|
482
|
+
function render() {
|
|
483
|
+
const plugin = chosen();
|
|
484
|
+
$('plugin-name').textContent = plugin?.name || '插件调试';
|
|
485
|
+
const building = catalog?.devBuild?.state;
|
|
486
|
+
$('state').textContent = building === 'error' ? '构建失败' : building === 'building' ? '正在构建' : frameFailed ? '画布加载失败' : busy ? (session ? '正在刷新' : '正在打开画布') : session ? (!session.active ? '会话已失效' : plugin?.buildKey === session.fingerprint ? '已连接' : '发现新构建') : '等待画布';
|
|
487
|
+
$('dot').className = `dot ${building === 'error' || frameFailed ? 'error' : building === 'building' ? 'warning' : session ? !session.active ? 'error' : plugin?.buildKey === session.fingerprint ? 'ready' : 'warning' : ''}`;
|
|
488
|
+
$('refresh').disabled = !session?.active || busy || frameFailed || Boolean(building && building !== 'ready');
|
|
489
|
+
const stateText = $('state').textContent;
|
|
490
|
+
$('panel-status').textContent = stateText;
|
|
491
|
+
$('panel-status').className = `status-badge ${$('dot').classList.contains('error') ? 'error' : $('dot').classList.contains('ready') ? 'ready' : ''}`;
|
|
492
|
+
$('connection').textContent = connectorStatus?.phase === 'connected' ? '已连接 · 本机' : '未连接';
|
|
493
|
+
$('active-plugin').textContent = plugin?.name || '';
|
|
494
|
+
$('build').textContent = building === 'error' ? '构建失败' : building === 'building' ? '构建中' : plugin?.buildKey ? plugin.buildKey.slice(0, 8) : '等待构建';
|
|
495
|
+
$('project').textContent = session?.active ? '私人调试画布' : session ? '会话已失效' : '等待打开';
|
|
496
|
+
$('connection-id').textContent = catalog?.connectionId || '—';
|
|
497
|
+
$('build-full').textContent = plugin?.buildKey || '—';
|
|
498
|
+
$('project-id').textContent = session?.projectId || '—';
|
|
499
|
+
renderPluginList();
|
|
500
|
+
const inspected = panelPlugin();
|
|
501
|
+
$('open-plugin').disabled = !inspected?.enabled || busy || packageDownloading;
|
|
502
|
+
$('download-package').disabled = !inspected?.package || packageDownloading;
|
|
503
|
+
$('download-package').setAttribute('aria-busy', String(packageDownloading));
|
|
504
|
+
$('download-label').textContent = packageDownloading ? '正在准备…' : inspected?.package?.mode === 'build' ? '打包并下载' : '下载插件包';
|
|
505
|
+
$('package-note').textContent = packageMessage || (inspected?.package?.mode === 'build' ? '从当前源码生成 ZIP,可用于平台安装。' : inspected?.package?.mode === 'download' ? '下载本地服务已加载的 ZIP 插件包。' : '当前插件暂无可下载的插件包。');
|
|
506
|
+
$('package-note').className = `package-note${packageError ? ' error' : ''}`;
|
|
507
|
+
$('empty').hidden = Boolean(plugin) && !frameLoading && !frameFailed;
|
|
508
|
+
}
|
|
509
|
+
async function connect() {
|
|
510
|
+
if (!catalog?.connectionId || !chosen() || connectorStatus?.phase !== 'connected') return;
|
|
511
|
+
clearTimeout(readyTimer);
|
|
512
|
+
session = null; preparedSession = null; busy = true; frameLoading = true; frameFailed = false; waiting = null;
|
|
513
|
+
$('retry').hidden = true;
|
|
514
|
+
$('empty-text').textContent = '正在打开调试画布…';
|
|
515
|
+
canvasDialogOpen = false; expandedOverDialog = false; renderDock(); render();
|
|
516
|
+
try {
|
|
517
|
+
const response = await fetch('/studio/open', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ pluginId: selected }) });
|
|
518
|
+
const next = await response.json();
|
|
519
|
+
if (!response.ok) throw new Error(next.message || next.error || '调试画布创建失败');
|
|
520
|
+
preparedSession = next;
|
|
521
|
+
} catch (error) { busy = false; frameLoading = false; frameFailed = true; $('empty-text').textContent = error.message; $('retry').hidden = false; show(error.message); log(error.message); render(); return; }
|
|
522
|
+
frame.src = url();
|
|
523
|
+
log(`打开真实画布:${selected}`);
|
|
524
|
+
readyTimer = setTimeout(() => {
|
|
525
|
+
if (!session) {
|
|
526
|
+
busy = false; frameLoading = false; frameFailed = true; $('retry').hidden = false;
|
|
527
|
+
$('empty-text').textContent = '调试画布加载超时,请重新打开画布。';
|
|
528
|
+
log('等待调试画布超时'); render();
|
|
529
|
+
}
|
|
530
|
+
}, 45000);
|
|
531
|
+
render();
|
|
532
|
+
}
|
|
533
|
+
async function loadCatalog() {
|
|
534
|
+
try {
|
|
535
|
+
const response = await fetch('/studio/catalog', { cache: 'no-store' });
|
|
536
|
+
if (!response.ok) throw new Error(`插件服务返回 ${response.status}`);
|
|
537
|
+
const next = await response.json();
|
|
538
|
+
if (next.protocol !== protocol || !Array.isArray(next.plugins)) throw new Error('插件目录格式无效');
|
|
539
|
+
const old = chosen();
|
|
540
|
+
catalog = next;
|
|
541
|
+
const buildState = next.devBuild?.state || '';
|
|
542
|
+
if (buildState !== lastBuildState) {
|
|
543
|
+
if (buildState === 'error') { show(`插件界面构建失败:${next.devBuild.message || '请检查开发服务日志'}`); log('插件界面构建失败'); }
|
|
544
|
+
if (buildState === 'ready' && lastBuildState === 'error') { show('插件界面已重新构建,请刷新画布。', false); log('插件界面构建恢复'); }
|
|
545
|
+
lastBuildState = buildState;
|
|
546
|
+
}
|
|
547
|
+
if (!next.plugins.some(item => item.id === selected))
|
|
548
|
+
selected = next.plugins.find(item => item.id === requestedPlugin)?.id || next.plugins[0]?.id || null;
|
|
549
|
+
if (!next.plugins.some(item => item.id === panelSelected)) panelSelected = selected;
|
|
550
|
+
if (selected && !frame.getAttribute('src') && !busy && !frameFailed) connect();
|
|
551
|
+
if (!selected) { $('empty-text').textContent = '本地服务尚未注册插件,请在源码中注册并启动开发服务。'; $('retry').hidden = false; }
|
|
552
|
+
if (session && old?.buildKey !== chosen()?.buildKey) { show('检测到新构建。完成当前任务后,点击“刷新构建”。'); log('检测到插件构建更新'); }
|
|
553
|
+
render();
|
|
554
|
+
} catch (error) {
|
|
555
|
+
$('empty-text').textContent = `暂时无法读取本地插件目录:${error.message}`;
|
|
556
|
+
$('retry').hidden = false; show('本地插件服务暂时不可用,请检查开发命令。'); log(error.message);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
window.addEventListener('message', event => {
|
|
560
|
+
if (event.origin === window.location.origin && event.data?.type === 'plugin-debug-auth-complete') { void loadConnectionStatus(); return; }
|
|
561
|
+
if (!catalog || event.origin !== catalog.platformOrigin || event.source !== frame.contentWindow) return;
|
|
562
|
+
const data = event.data;
|
|
563
|
+
if (!data || typeof data !== 'object' || data.protocol !== protocol || data.pluginId !== selected) return;
|
|
564
|
+
if (data.type === 'bootstrap.request' && preparedSession?.sessionId === data.sessionId &&
|
|
565
|
+
typeof data.codeChallenge === 'string' && /^[A-Za-z0-9_-]{43,128}$/.test(data.codeChallenge)) {
|
|
566
|
+
void (async () => {
|
|
567
|
+
try {
|
|
568
|
+
const response = await fetch('/studio/embed-ticket', { method: 'POST', headers: { 'content-type': 'application/json' },
|
|
569
|
+
body: JSON.stringify({ debugSessionId: data.sessionId, codeChallenge: data.codeChallenge }) });
|
|
570
|
+
const issued = await response.json();
|
|
571
|
+
if (!response.ok) throw new Error(issued.message || issued.error || '画布登录失败');
|
|
572
|
+
frame.contentWindow.postMessage({ protocol, type: 'bootstrap.ticket', pluginId: selected,
|
|
573
|
+
sessionId: data.sessionId, ticket: issued.ticket }, catalog.platformOrigin);
|
|
574
|
+
} catch (error) { show(error.message); log(error.message); }
|
|
575
|
+
})();
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (data.type === 'ready' && data.status?.sessionId === data.sessionId) {
|
|
579
|
+
clearTimeout(readyTimer); session = data.status; busy = false; frameLoading = false; frameFailed = false; show(''); log(`已连接私人画布 ${session.projectId}`); render(); return;
|
|
580
|
+
}
|
|
581
|
+
if (data.type === 'error' && !data.sessionId) {
|
|
582
|
+
clearTimeout(readyTimer); busy = false; frameLoading = false; frameFailed = true; $('retry').hidden = false;
|
|
583
|
+
$('empty-text').textContent = data.message || '调试画布加载失败,请重新打开画布。';
|
|
584
|
+
log($('empty-text').textContent); render(); return;
|
|
585
|
+
}
|
|
586
|
+
if (!session || data.sessionId !== session.sessionId) return;
|
|
587
|
+
if (data.type === 'canvasDialogState' && typeof data.open === 'boolean') {
|
|
588
|
+
if (data.open !== canvasDialogOpen) expandedOverDialog = false;
|
|
589
|
+
canvasDialogOpen = data.open; renderDock(); return;
|
|
590
|
+
}
|
|
591
|
+
if (data.type === 'status' && data.status?.sessionId === session.sessionId) { session = data.status; if (!session.active) show('调试会话已失效,请在管理菜单中重新连接。'); render(); return; }
|
|
592
|
+
if (data.type === 'refreshing' && data.requestId === waiting) { busy = true; log('正在保存画布并检查新构建'); render(); }
|
|
593
|
+
if (data.type === 'refreshResult' && data.requestId === waiting) {
|
|
594
|
+
session = data.status; waiting = null; busy = Boolean(data.changed); frameLoading = Boolean(data.changed); show(data.changed ? '新构建已启用,正在重新加载画布…' : '当前已经是最新构建。', false); log(data.changed ? '构建已更新,画布正在重载' : '当前构建没有变化');
|
|
595
|
+
if (data.changed) $('empty-text').textContent = '正在加载新构建…';
|
|
596
|
+
if (data.changed) readyTimer = setTimeout(() => {
|
|
597
|
+
busy = false; session = null; frameLoading = false; frameFailed = true; $('retry').hidden = false;
|
|
598
|
+
$('empty-text').textContent = '新构建已启用,但画布重新加载失败。请重新打开画布。';
|
|
599
|
+
render();
|
|
600
|
+
}, 45000);
|
|
601
|
+
render();
|
|
602
|
+
}
|
|
603
|
+
if (data.type === 'refreshError' && data.requestId === waiting) { busy = false; waiting = null; show(data.message || '刷新失败,请稍后重试'); log(data.message || '刷新失败'); render(); }
|
|
604
|
+
});
|
|
605
|
+
$('refresh').addEventListener('click', () => {
|
|
606
|
+
if (!session || busy) return;
|
|
607
|
+
waiting = crypto.randomUUID(); busy = true;
|
|
608
|
+
frame.contentWindow.postMessage({ protocol, type: 'refresh', sessionId: session.sessionId, pluginId: selected, requestId: waiting }, catalog.platformOrigin);
|
|
609
|
+
render();
|
|
610
|
+
});
|
|
611
|
+
$('manage').addEventListener('click', () => { panelSelected = selected; packageMessage = ''; packageError = false; render(); panel.showModal(); $('plugin-list').querySelector('input:checked')?.focus(); renderDock(); });
|
|
612
|
+
panel.addEventListener('close', () => { renderDock(); $('manage').focus(); });
|
|
613
|
+
$('close').addEventListener('click', () => panel.close());
|
|
614
|
+
$('panel-cancel').addEventListener('click', () => panel.close());
|
|
615
|
+
$('plugin-list').addEventListener('change', event => {
|
|
616
|
+
if (!event.target.matches('input[name="studio-plugin"]')) return;
|
|
617
|
+
panelSelected = event.target.value; packageMessage = ''; packageError = false; render();
|
|
618
|
+
});
|
|
619
|
+
$('open-plugin').addEventListener('click', () => {
|
|
620
|
+
if (!panelPlugin()?.enabled || busy) return;
|
|
621
|
+
selected = panelSelected; show(''); panel.close(); connect();
|
|
622
|
+
});
|
|
623
|
+
$('download-package').addEventListener('click', async () => {
|
|
624
|
+
const plugin = panelPlugin();
|
|
625
|
+
if (!plugin?.package || packageDownloading) return;
|
|
626
|
+
packageDownloading = true; packageMessage = ''; packageError = false; render();
|
|
627
|
+
try {
|
|
628
|
+
const response = await fetch(`/studio/packages/${encodeURIComponent(plugin.id)}/download`, { method: 'POST' });
|
|
629
|
+
if (!response.ok) {
|
|
630
|
+
const details = await response.json().catch(() => ({}));
|
|
631
|
+
throw new Error(details.error || details.message || '插件包下载失败');
|
|
632
|
+
}
|
|
633
|
+
const blob = await response.blob();
|
|
634
|
+
const filename = `${plugin.id.replace(/[^A-Za-z0-9._-]/g, '_')}.zip`;
|
|
635
|
+
const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = filename;
|
|
636
|
+
link.click(); setTimeout(() => URL.revokeObjectURL(link.href), 10_000);
|
|
637
|
+
packageMessage = `已下载 ${filename}`; log(`下载插件包:${plugin.id}`);
|
|
638
|
+
} catch (error) {
|
|
639
|
+
packageMessage = error.message || '插件包下载失败'; packageError = true; log(packageMessage);
|
|
640
|
+
} finally { packageDownloading = false; render(); }
|
|
641
|
+
});
|
|
642
|
+
$('reconnect').addEventListener('click', () => { show(''); connect(); panel.close(); });
|
|
643
|
+
$('retry').addEventListener('click', () => frameFailed ? connect() : loadCatalog());
|
|
644
|
+
void loadConnectionStatus(); setInterval(loadConnectionStatus, 2000);
|
|
645
|
+
loadCatalog(); setInterval(loadCatalog, 3000);
|
|
646
|
+
})();
|
|
647
|
+
</script>
|
|
648
|
+
</body>
|
|
649
|
+
</html>
|