bothouse-dsh 0.1.45 → 0.1.47
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/README.en.md +9 -3
- package/README.md +9 -3
- package/client/client.js +89 -3
- package/collab/desk/index.html +1542 -0
- package/collab/desk.mjs +225 -0
- package/collab/dsh-member.mjs +29 -4
- package/collab/gate-context.mjs +13 -19
- package/collab/handoff-rules.json +175 -0
- package/collab/hub.mjs +26 -1
- package/collab/page.html +619 -70
- package/collab/panel.mjs +160 -0
- package/collab/personas.json +150 -0
- package/collab/plugin.mjs +44 -5
- package/collab/roster.mjs +33 -0
- package/collab/team-exit-rules.json +81 -0
- package/collab/team-mode-rules.md +33 -0
- package/collab/team-status.mjs +180 -0
- package/collab/wiki-galaxy.mjs +126 -0
- package/package.json +6 -2
package/collab/page.html
CHANGED
|
@@ -2,60 +2,511 @@
|
|
|
2
2
|
<html lang="zh-CN">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
6
|
+
<meta name="color-scheme" content="dark light">
|
|
6
7
|
<title>CoNest 频道</title>
|
|
7
8
|
<style>
|
|
8
|
-
|
|
9
|
+
/* 设计令牌:颜色 / 字号 / 间距 / 圆角集中在这里,亮暗两套共用同一组名字。
|
|
10
|
+
对比度按 WCAG AA 取(正文与次要文字 ≥ 4.5:1,大字与描边 ≥ 3:1)。 */
|
|
11
|
+
:root {
|
|
12
|
+
color-scheme: dark;
|
|
13
|
+
--bg: #313338;
|
|
14
|
+
--surface: #2b2d31;
|
|
15
|
+
--surface-2: #35373c;
|
|
16
|
+
--rail: #1e1f22;
|
|
17
|
+
--text: #e3e5e8;
|
|
18
|
+
--text-strong: #f2f3f5;
|
|
19
|
+
--muted: #a9b0ba;
|
|
20
|
+
--faint: #9ea5af;
|
|
21
|
+
--line: #3f4147;
|
|
22
|
+
--hover: #35373c;
|
|
23
|
+
--hover-soft: rgba(127, 127, 127, .07);
|
|
24
|
+
--active: #404249;
|
|
25
|
+
--input: #383a40;
|
|
26
|
+
--accent: #5865f2;
|
|
27
|
+
--accent-strong: #4752c4;
|
|
28
|
+
--on-accent: #ffffff;
|
|
29
|
+
--ok: #23a55a;
|
|
30
|
+
--warn: #f0b232;
|
|
31
|
+
--danger: #f23f43;
|
|
32
|
+
--scrim: rgba(0, 0, 0, .55);
|
|
33
|
+
--shadow: 0 12px 32px rgba(0, 0, 0, .35);
|
|
34
|
+
|
|
35
|
+
/* 字号 */
|
|
36
|
+
--fs-xs: 12px;
|
|
37
|
+
--fs-sm: 13px;
|
|
38
|
+
--fs-md: 15px;
|
|
39
|
+
--fs-lg: 17px;
|
|
40
|
+
--fs-xl: 20px;
|
|
41
|
+
|
|
42
|
+
/* 间距 */
|
|
43
|
+
--sp-1: 4px;
|
|
44
|
+
--sp-2: 8px;
|
|
45
|
+
--sp-3: 12px;
|
|
46
|
+
--sp-4: 16px;
|
|
47
|
+
--sp-5: 20px;
|
|
48
|
+
--sp-6: 24px;
|
|
49
|
+
--sp-8: 32px;
|
|
50
|
+
|
|
51
|
+
/* 圆角 */
|
|
52
|
+
--r-sm: 6px;
|
|
53
|
+
--r-md: 10px;
|
|
54
|
+
--r-lg: 14px;
|
|
55
|
+
--r-full: 999px;
|
|
56
|
+
|
|
57
|
+
/* 布局 */
|
|
58
|
+
--rail-w: 240px;
|
|
59
|
+
--members-w: 240px;
|
|
60
|
+
--topbar-h: 52px;
|
|
61
|
+
--pad-x: 16px;
|
|
62
|
+
--avatar: 40px;
|
|
63
|
+
--avatar-sm: 32px;
|
|
64
|
+
|
|
65
|
+
/* 动效 */
|
|
66
|
+
--dur: 160ms;
|
|
67
|
+
--ease: cubic-bezier(.2, .8, .2, 1);
|
|
68
|
+
}
|
|
69
|
+
|
|
9
70
|
@media (prefers-color-scheme: light) {
|
|
10
|
-
:root {
|
|
71
|
+
:root {
|
|
72
|
+
color-scheme: light;
|
|
73
|
+
--bg: #ffffff;
|
|
74
|
+
--surface: #f6f7f9;
|
|
75
|
+
--surface-2: #eceef1;
|
|
76
|
+
--rail: #e6e8eb;
|
|
77
|
+
--text: #2b2d31;
|
|
78
|
+
--text-strong: #111214;
|
|
79
|
+
--muted: #545861;
|
|
80
|
+
--faint: #656a73;
|
|
81
|
+
--line: #dfe1e5;
|
|
82
|
+
--hover: #e9ebee;
|
|
83
|
+
--hover-soft: rgba(17, 18, 20, .035);
|
|
84
|
+
--active: #dfe2e7;
|
|
85
|
+
--input: #eceef1;
|
|
86
|
+
--accent: #4a55d6;
|
|
87
|
+
--accent-strong: #3d48c0;
|
|
88
|
+
--on-accent: #ffffff;
|
|
89
|
+
--ok: #1a7f43;
|
|
90
|
+
--warn: #7a5200;
|
|
91
|
+
--danger: #c22b2f;
|
|
92
|
+
--scrim: rgba(16, 18, 22, .42);
|
|
93
|
+
--shadow: 0 12px 32px rgba(20, 22, 28, .16);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@media (prefers-reduced-motion: reduce) {
|
|
98
|
+
:root { --dur: 0ms; }
|
|
11
99
|
}
|
|
100
|
+
|
|
12
101
|
* { box-sizing: border-box; }
|
|
13
102
|
html, body { height: 100%; margin: 0; }
|
|
14
|
-
body {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
103
|
+
body {
|
|
104
|
+
height: 100dvh;
|
|
105
|
+
overflow: hidden;
|
|
106
|
+
display: grid;
|
|
107
|
+
grid-template-columns: var(--rail-w) minmax(0, 1fr) var(--members-w);
|
|
108
|
+
background: var(--bg);
|
|
109
|
+
color: var(--text);
|
|
110
|
+
font: var(--fs-md)/1.45 system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
111
|
+
-webkit-font-smoothing: antialiased;
|
|
112
|
+
text-rendering: optimizeLegibility;
|
|
113
|
+
}
|
|
114
|
+
button { font: inherit; color: inherit; }
|
|
115
|
+
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
116
|
+
|
|
117
|
+
/* ---------- 侧栏 ---------- */
|
|
118
|
+
aside {
|
|
119
|
+
min-height: 0;
|
|
120
|
+
overflow-y: auto;
|
|
121
|
+
overscroll-behavior: contain;
|
|
122
|
+
background: var(--surface);
|
|
123
|
+
display: flex;
|
|
124
|
+
flex-direction: column;
|
|
125
|
+
}
|
|
126
|
+
#roomsPane { border-right: 1px solid var(--rail); }
|
|
127
|
+
#members { border-left: 1px solid var(--rail); }
|
|
128
|
+
.pane-head {
|
|
129
|
+
position: sticky;
|
|
130
|
+
top: 0;
|
|
131
|
+
z-index: 2;
|
|
132
|
+
flex: none;
|
|
133
|
+
display: flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
gap: var(--sp-2);
|
|
136
|
+
height: var(--topbar-h);
|
|
137
|
+
padding: 0 var(--pad-x);
|
|
138
|
+
padding-left: max(var(--pad-x), env(safe-area-inset-left));
|
|
139
|
+
padding-right: max(var(--pad-x), env(safe-area-inset-right));
|
|
140
|
+
font-weight: 700;
|
|
141
|
+
font-size: var(--fs-lg);
|
|
142
|
+
letter-spacing: .01em;
|
|
143
|
+
color: var(--text-strong);
|
|
144
|
+
background: var(--surface);
|
|
145
|
+
border-bottom: 1px solid var(--rail);
|
|
146
|
+
}
|
|
147
|
+
.pane-head .brand {
|
|
148
|
+
width: 22px; height: 22px;
|
|
149
|
+
border-radius: var(--r-sm);
|
|
150
|
+
background: var(--accent);
|
|
151
|
+
color: var(--on-accent);
|
|
152
|
+
font-size: 12px; font-weight: 700;
|
|
153
|
+
display: grid; place-items: center;
|
|
154
|
+
flex: none;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.rooms { padding: var(--sp-2) var(--sp-2) var(--sp-4); display: flex; flex-direction: column; gap: 2px; }
|
|
158
|
+
.rooms button {
|
|
159
|
+
all: unset;
|
|
160
|
+
display: flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
gap: var(--sp-2);
|
|
163
|
+
min-height: 34px;
|
|
164
|
+
padding: 6px var(--sp-2);
|
|
165
|
+
border-radius: var(--r-sm);
|
|
166
|
+
color: var(--muted);
|
|
167
|
+
font-size: var(--fs-md);
|
|
168
|
+
cursor: pointer;
|
|
169
|
+
white-space: nowrap;
|
|
170
|
+
overflow: hidden;
|
|
171
|
+
text-overflow: ellipsis;
|
|
172
|
+
transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
|
|
173
|
+
}
|
|
174
|
+
.rooms button .hash { color: var(--faint); font-weight: 600; flex: none; }
|
|
18
175
|
.rooms button:hover { background: var(--hover); color: var(--text); }
|
|
19
|
-
.rooms button[aria-current="true"] { background: var(--
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
176
|
+
.rooms button[aria-current="true"] { background: var(--active); color: var(--text-strong); font-weight: 600; }
|
|
177
|
+
.rooms button[aria-current="true"] .hash { color: var(--text); }
|
|
178
|
+
|
|
179
|
+
/* ---------- 主区 ---------- */
|
|
180
|
+
main { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
|
|
181
|
+
.topbar {
|
|
182
|
+
flex: none;
|
|
183
|
+
display: flex;
|
|
184
|
+
align-items: center;
|
|
185
|
+
gap: var(--sp-2);
|
|
186
|
+
height: var(--topbar-h);
|
|
187
|
+
padding: 0 var(--pad-x);
|
|
188
|
+
padding-left: max(var(--pad-x), env(safe-area-inset-left));
|
|
189
|
+
padding-right: max(var(--pad-x), env(safe-area-inset-right));
|
|
190
|
+
border-bottom: 1px solid var(--line);
|
|
191
|
+
background: var(--bg);
|
|
192
|
+
position: relative;
|
|
193
|
+
z-index: 3;
|
|
194
|
+
}
|
|
195
|
+
.topbar .title {
|
|
196
|
+
margin: 0;
|
|
197
|
+
font-weight: 650;
|
|
198
|
+
font-size: var(--fs-xl);
|
|
199
|
+
line-height: 28px;
|
|
200
|
+
color: var(--text-strong);
|
|
201
|
+
white-space: nowrap;
|
|
202
|
+
overflow: hidden;
|
|
203
|
+
text-overflow: ellipsis;
|
|
204
|
+
}
|
|
205
|
+
.spacer { flex: 1; }
|
|
206
|
+
.icon-btn {
|
|
207
|
+
display: none;
|
|
208
|
+
place-items: center;
|
|
209
|
+
width: 36px; height: 36px;
|
|
210
|
+
flex: none;
|
|
211
|
+
padding: 0;
|
|
212
|
+
border: 1px solid var(--line);
|
|
213
|
+
border-radius: var(--r-sm);
|
|
214
|
+
background: var(--surface);
|
|
215
|
+
color: var(--text);
|
|
216
|
+
cursor: pointer;
|
|
217
|
+
}
|
|
218
|
+
.icon-btn:hover { background: var(--hover); }
|
|
219
|
+
.icon-btn svg { width: 18px; height: 18px; }
|
|
220
|
+
.mode {
|
|
221
|
+
border: 1px solid var(--line);
|
|
222
|
+
border-radius: var(--r-sm);
|
|
223
|
+
padding: 5px var(--sp-2);
|
|
224
|
+
background: var(--input);
|
|
225
|
+
color: var(--text);
|
|
226
|
+
font: var(--fs-sm)/1.2 system-ui, -apple-system, "PingFang SC", sans-serif;
|
|
227
|
+
max-width: 190px;
|
|
228
|
+
}
|
|
229
|
+
.mode:hover { background: var(--hover); }
|
|
230
|
+
|
|
231
|
+
#log { flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: var(--sp-4) 0 var(--sp-6); scrollbar-gutter: stable; }
|
|
232
|
+
|
|
233
|
+
.msg {
|
|
234
|
+
position: relative;
|
|
235
|
+
display: grid;
|
|
236
|
+
grid-template-columns: var(--avatar) minmax(0, 1fr);
|
|
237
|
+
gap: 0 var(--sp-3);
|
|
238
|
+
padding: var(--sp-1) var(--pad-x);
|
|
239
|
+
padding-left: max(var(--pad-x), env(safe-area-inset-left));
|
|
240
|
+
padding-right: max(var(--pad-x), env(safe-area-inset-right));
|
|
241
|
+
border-radius: var(--r-sm);
|
|
242
|
+
transition: background var(--dur) var(--ease);
|
|
243
|
+
}
|
|
244
|
+
.msg:hover { background: var(--hover-soft); }
|
|
245
|
+
.msg.cont { padding-top: 2px; }
|
|
246
|
+
.msg.cont .avatar,
|
|
247
|
+
.msg.cont .head { display: none; }
|
|
248
|
+
.msg .stamp {
|
|
249
|
+
display: none;
|
|
250
|
+
position: absolute;
|
|
251
|
+
left: max(var(--pad-x), env(safe-area-inset-left));
|
|
252
|
+
top: 2px;
|
|
253
|
+
width: var(--avatar);
|
|
254
|
+
text-align: center;
|
|
255
|
+
font-size: var(--fs-xs);
|
|
256
|
+
line-height: 1.4;
|
|
257
|
+
color: var(--faint);
|
|
258
|
+
font-variant-numeric: tabular-nums;
|
|
259
|
+
}
|
|
260
|
+
.msg.cont:hover .stamp { display: block; }
|
|
261
|
+
|
|
262
|
+
.avatar {
|
|
263
|
+
width: var(--avatar);
|
|
264
|
+
height: var(--avatar);
|
|
265
|
+
border-radius: var(--r-full);
|
|
266
|
+
display: grid;
|
|
267
|
+
place-items: center;
|
|
268
|
+
font-weight: 700;
|
|
269
|
+
font-size: var(--fs-md);
|
|
270
|
+
color: #fff;
|
|
271
|
+
position: relative;
|
|
272
|
+
flex: none;
|
|
273
|
+
user-select: none;
|
|
274
|
+
}
|
|
275
|
+
.avatar img { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: var(--r-full); object-fit: cover; }
|
|
276
|
+
|
|
277
|
+
.head { display: flex; align-items: baseline; gap: var(--sp-2); min-width: 0; }
|
|
278
|
+
.who { font-weight: 600; font-size: var(--fs-md); color: var(--text-strong); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
279
|
+
.when { color: var(--faint); font-size: var(--fs-xs); white-space: nowrap; flex: none; font-variant-numeric: tabular-nums; }
|
|
280
|
+
.body { white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere; color: var(--text); }
|
|
281
|
+
.tag {
|
|
282
|
+
font-size: 11px;
|
|
283
|
+
line-height: 1.6;
|
|
284
|
+
background: var(--accent);
|
|
285
|
+
color: var(--on-accent);
|
|
286
|
+
border-radius: 4px;
|
|
287
|
+
padding: 0 5px;
|
|
288
|
+
vertical-align: 1px;
|
|
289
|
+
flex: none;
|
|
290
|
+
}
|
|
30
291
|
.system .body { color: var(--muted); font-style: italic; }
|
|
31
|
-
.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
.card
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
292
|
+
.system .who { color: var(--muted); font-weight: 500; }
|
|
293
|
+
|
|
294
|
+
/* 任务卡:左边框 + 状态胶囊,五种状态各有颜色(暗/亮两套都单独取过对比度) */
|
|
295
|
+
.card {
|
|
296
|
+
--card-accent: var(--accent);
|
|
297
|
+
--card-tint: rgba(88, 101, 242, .16);
|
|
298
|
+
--card-text: #b7bdff;
|
|
299
|
+
border-left: 3px solid var(--card-accent);
|
|
300
|
+
background: var(--surface);
|
|
301
|
+
padding: var(--sp-2) var(--sp-3);
|
|
302
|
+
border-radius: var(--r-sm);
|
|
303
|
+
margin-top: var(--sp-1);
|
|
304
|
+
max-width: 640px;
|
|
305
|
+
}
|
|
306
|
+
.card[data-state="running"] { --card-accent: var(--warn); --card-tint: rgba(240, 178, 50, .16); --card-text: #f6d27a; }
|
|
307
|
+
.card[data-state="completed"] { --card-accent: var(--ok); --card-tint: rgba(35, 165, 90, .16); --card-text: #7ee2a8; }
|
|
308
|
+
.card[data-state="failed"] { --card-accent: var(--danger); --card-tint: rgba(242, 63, 67, .16); --card-text: #ff9a9c; }
|
|
309
|
+
.card[data-state="cancelled"] { --card-accent: var(--muted); --card-tint: rgba(127, 127, 127, .14); --card-text: var(--muted); }
|
|
310
|
+
@media (prefers-color-scheme: light) {
|
|
311
|
+
.card { --card-text: #3b46b8; --card-tint: rgba(74, 85, 214, .12); }
|
|
312
|
+
.card[data-state="running"] { --card-text: #6d4a00; --card-tint: rgba(240, 178, 50, .18); }
|
|
313
|
+
.card[data-state="completed"] { --card-text: #146c3a; --card-tint: rgba(26, 127, 67, .12); }
|
|
314
|
+
.card[data-state="failed"] { --card-text: #a52327; --card-tint: rgba(194, 43, 47, .12); }
|
|
315
|
+
.card[data-state="cancelled"] { --card-text: #545861; --card-tint: rgba(84, 88, 97, .12); }
|
|
316
|
+
}
|
|
317
|
+
.card-head { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
|
|
318
|
+
.card .title { font-weight: 600; font-size: var(--fs-sm); color: var(--text-strong); }
|
|
319
|
+
.card .chip {
|
|
320
|
+
font-size: 11px;
|
|
321
|
+
line-height: 1.6;
|
|
322
|
+
padding: 0 6px;
|
|
323
|
+
border-radius: var(--r-full);
|
|
324
|
+
color: var(--card-text);
|
|
325
|
+
background: var(--card-tint);
|
|
326
|
+
flex: none;
|
|
327
|
+
}
|
|
328
|
+
.card .body { font-size: var(--fs-sm); color: var(--text); margin-top: 2px; }
|
|
329
|
+
.card .actions { display: flex; gap: var(--sp-2); margin-top: var(--sp-2); }
|
|
330
|
+
.card .actions button {
|
|
331
|
+
padding: 4px 12px;
|
|
332
|
+
border-radius: var(--r-sm);
|
|
333
|
+
border: 1px solid var(--line);
|
|
334
|
+
background: var(--input);
|
|
335
|
+
color: var(--text);
|
|
336
|
+
cursor: pointer;
|
|
337
|
+
font-size: var(--fs-sm);
|
|
338
|
+
transition: background var(--dur) var(--ease);
|
|
339
|
+
}
|
|
340
|
+
.card .actions button:hover { background: var(--hover); }
|
|
341
|
+
.card .actions button:disabled { opacity: .5; cursor: default; }
|
|
342
|
+
|
|
343
|
+
/* 空态 / 加载态 / 出错态 */
|
|
344
|
+
.state {
|
|
345
|
+
margin: var(--sp-8) auto;
|
|
346
|
+
max-width: 420px;
|
|
347
|
+
padding: var(--sp-6);
|
|
348
|
+
text-align: center;
|
|
349
|
+
color: var(--muted);
|
|
350
|
+
font-size: var(--fs-sm);
|
|
351
|
+
}
|
|
352
|
+
.state .state-title { color: var(--text); font-size: var(--fs-md); font-weight: 600; margin-bottom: var(--sp-1); }
|
|
353
|
+
.state .state-detail { color: var(--muted); word-break: break-word; }
|
|
354
|
+
.state .state-hint { color: var(--faint); margin-top: var(--sp-2); }
|
|
355
|
+
.state .btn {
|
|
356
|
+
margin-top: var(--sp-4);
|
|
357
|
+
padding: 6px 16px;
|
|
358
|
+
border-radius: var(--r-sm);
|
|
359
|
+
border: 1px solid var(--line);
|
|
360
|
+
background: var(--input);
|
|
361
|
+
color: var(--text);
|
|
362
|
+
cursor: pointer;
|
|
363
|
+
}
|
|
364
|
+
.state .btn:hover { background: var(--hover); }
|
|
365
|
+
.spinner {
|
|
366
|
+
width: 22px; height: 22px;
|
|
367
|
+
margin: 0 auto var(--sp-3);
|
|
368
|
+
border-radius: var(--r-full);
|
|
369
|
+
border: 2px solid var(--line);
|
|
370
|
+
border-top-color: var(--accent);
|
|
371
|
+
animation: spin .8s linear infinite;
|
|
372
|
+
}
|
|
373
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
374
|
+
@media (prefers-reduced-motion: reduce) {
|
|
375
|
+
.spinner { animation: none; border-top-color: var(--accent); }
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/* ---------- 成员 ---------- */
|
|
379
|
+
.members { padding-bottom: max(var(--sp-4), env(safe-area-inset-bottom)); }
|
|
380
|
+
.members h3 {
|
|
381
|
+
margin: var(--sp-4) var(--pad-x) var(--sp-1);
|
|
382
|
+
font-size: var(--fs-xs);
|
|
383
|
+
font-weight: 600;
|
|
384
|
+
color: var(--faint);
|
|
385
|
+
text-transform: uppercase;
|
|
386
|
+
letter-spacing: .06em;
|
|
387
|
+
}
|
|
388
|
+
.member { display: flex; align-items: center; gap: var(--sp-3); padding: 5px var(--pad-x); border-radius: var(--r-sm); margin: 0 var(--sp-2); }
|
|
389
|
+
.member:hover { background: var(--hover-soft); }
|
|
390
|
+
.member .avatar { width: var(--avatar-sm); height: var(--avatar-sm); font-size: var(--fs-sm); }
|
|
391
|
+
.member-info { min-width: 0; }
|
|
392
|
+
.member-name { display: flex; align-items: center; gap: 6px; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
393
|
+
.dot {
|
|
394
|
+
position: absolute;
|
|
395
|
+
right: -2px; bottom: -2px;
|
|
396
|
+
width: 12px; height: 12px;
|
|
397
|
+
border-radius: var(--r-full);
|
|
398
|
+
border: 3px solid var(--surface);
|
|
399
|
+
background: var(--ok);
|
|
400
|
+
}
|
|
401
|
+
.offline { opacity: .5; }
|
|
402
|
+
.offline .dot { background: var(--muted); }
|
|
403
|
+
.status { font-size: var(--fs-xs); color: var(--muted); }
|
|
404
|
+
|
|
405
|
+
/* ---------- 输入区 ---------- */
|
|
406
|
+
.composer { flex: none; padding: var(--sp-2) var(--pad-x) var(--sp-4); padding-bottom: max(var(--sp-4), env(safe-area-inset-bottom)); }
|
|
407
|
+
#thinking { min-height: 20px; padding: 0 var(--pad-x) var(--sp-1); color: var(--muted); font-size: var(--fs-sm); }
|
|
408
|
+
textarea {
|
|
409
|
+
display: block;
|
|
410
|
+
width: 100%;
|
|
411
|
+
resize: none;
|
|
412
|
+
border: 1px solid transparent;
|
|
413
|
+
border-radius: var(--r-md);
|
|
414
|
+
padding: 11px var(--sp-3);
|
|
415
|
+
background: var(--input);
|
|
416
|
+
color: var(--text);
|
|
417
|
+
font: var(--fs-md)/1.45 system-ui, -apple-system, "PingFang SC", sans-serif;
|
|
418
|
+
max-height: 40vh;
|
|
419
|
+
min-height: 44px;
|
|
420
|
+
transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
|
|
421
|
+
}
|
|
422
|
+
textarea::placeholder { color: var(--faint); }
|
|
423
|
+
textarea:hover { border-color: var(--line); }
|
|
424
|
+
textarea:focus { outline: none; border-color: var(--accent); background: var(--bg); }
|
|
425
|
+
textarea:disabled { opacity: .6; cursor: default; }
|
|
426
|
+
|
|
427
|
+
/* ---------- 窄屏:两侧收成抽屉(阈值 768px 来自 U2 交接)---------- */
|
|
428
|
+
.scrim {
|
|
429
|
+
position: fixed;
|
|
430
|
+
inset: 0;
|
|
431
|
+
z-index: 20;
|
|
432
|
+
background: var(--scrim);
|
|
433
|
+
opacity: 0;
|
|
434
|
+
pointer-events: none;
|
|
435
|
+
transition: opacity var(--dur) var(--ease);
|
|
436
|
+
}
|
|
437
|
+
.scrim.show { opacity: 1; pointer-events: auto; }
|
|
438
|
+
|
|
439
|
+
@media (max-width: 768px) {
|
|
440
|
+
body { grid-template-columns: minmax(0, 1fr); }
|
|
441
|
+
#roomsPane, #members {
|
|
442
|
+
position: fixed;
|
|
443
|
+
top: 0;
|
|
444
|
+
bottom: 0;
|
|
445
|
+
z-index: 30;
|
|
446
|
+
width: min(320px, 86vw);
|
|
447
|
+
border: 0;
|
|
448
|
+
box-shadow: var(--shadow);
|
|
449
|
+
will-change: transform;
|
|
450
|
+
transition: transform var(--dur) var(--ease);
|
|
451
|
+
padding-top: env(safe-area-inset-top);
|
|
452
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
453
|
+
}
|
|
454
|
+
#roomsPane { left: 0; transform: translateX(-100%); }
|
|
455
|
+
#members { right: 0; transform: translateX(100%); }
|
|
456
|
+
body[data-drawer="rooms"] #roomsPane,
|
|
457
|
+
body[data-drawer="members"] #members { transform: translateX(0); }
|
|
458
|
+
/* 44px:验收达标线「手机点按目标 ≥44px」 */
|
|
459
|
+
.icon-btn { display: inline-grid; width: 44px; height: 44px; }
|
|
460
|
+
.mode { max-width: 140px; min-height: 44px; }
|
|
461
|
+
.rooms button { min-height: 44px; }
|
|
462
|
+
.card .actions button { min-height: 44px; }
|
|
463
|
+
.scrim { -webkit-tap-highlight-color: transparent; }
|
|
464
|
+
.pane-head { padding-left: max(var(--pad-x), env(safe-area-inset-left)); }
|
|
465
|
+
.msg { padding-left: max(var(--sp-3), env(safe-area-inset-left)); padding-right: max(var(--sp-3), env(safe-area-inset-right)); }
|
|
466
|
+
.msg .stamp { left: max(var(--sp-3), env(safe-area-inset-left)); }
|
|
467
|
+
.topbar { padding-left: max(var(--sp-3), env(safe-area-inset-left)); padding-right: max(var(--sp-3), env(safe-area-inset-right)); }
|
|
468
|
+
.composer { padding-left: max(var(--sp-3), env(safe-area-inset-left)); padding-right: max(var(--sp-3), env(safe-area-inset-right)); }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
@media (max-height: 520px) {
|
|
472
|
+
.composer { padding-bottom: var(--sp-2); }
|
|
473
|
+
#log { padding-top: var(--sp-2); }
|
|
474
|
+
}
|
|
47
475
|
</style>
|
|
48
476
|
</head>
|
|
49
477
|
<body>
|
|
50
|
-
<aside
|
|
478
|
+
<aside id="roomsPane">
|
|
479
|
+
<div class="pane-head"><span class="brand" aria-hidden="true">C</span>CoNest</div>
|
|
480
|
+
<nav class="rooms" id="rooms" aria-label="频道"></nav>
|
|
481
|
+
</aside>
|
|
482
|
+
|
|
51
483
|
<main>
|
|
52
|
-
<div class="
|
|
53
|
-
|
|
484
|
+
<div class="topbar">
|
|
485
|
+
<button class="icon-btn" id="openRooms" type="button" aria-label="打开频道列表" aria-controls="roomsPane" aria-expanded="false">
|
|
486
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
|
|
487
|
+
</button>
|
|
488
|
+
<h1 class="title" id="title">#</h1>
|
|
489
|
+
<span class="spacer"></span>
|
|
490
|
+
<select id="mode" class="mode" aria-label="派活方式">
|
|
491
|
+
<option value="mixed">混合</option>
|
|
492
|
+
<option value="solo">单模型多进程</option>
|
|
493
|
+
<option value="team">完全团队</option>
|
|
494
|
+
</select>
|
|
495
|
+
<button class="icon-btn" id="openMembers" type="button" aria-label="打开成员列表" aria-controls="members" aria-expanded="false">
|
|
496
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87M17 3.13a4 4 0 0 1 0 7.75"/></svg>
|
|
497
|
+
</button>
|
|
498
|
+
</div>
|
|
499
|
+
<div id="log" role="log" aria-live="polite" aria-busy="true"></div>
|
|
54
500
|
<div id="thinking"></div>
|
|
55
|
-
<form id="form"><textarea id="input" rows="1" placeholder="发消息,@成员 让它干活(Enter 发送,Shift+Enter 换行)"></textarea></form>
|
|
501
|
+
<form id="form" class="composer"><textarea id="input" rows="1" placeholder="发消息,@成员 让它干活(Enter 发送,Shift+Enter 换行)" disabled></textarea></form>
|
|
56
502
|
</main>
|
|
57
|
-
|
|
503
|
+
|
|
504
|
+
<aside id="members" class="members" aria-label="成员"></aside>
|
|
505
|
+
<div class="scrim" id="scrim" aria-hidden="true"></div>
|
|
506
|
+
|
|
58
507
|
<script>
|
|
508
|
+
// 经面板 /dsh/ 转发时页面挂在前缀下:接口和链接都按当前地址带上前缀,直连时前缀为空。
|
|
509
|
+
const ROOT = (location.pathname.match(/^(.*?)\/collab(?:\/|$)/) || ['', ''])[1];
|
|
59
510
|
const state = { me: 'owner', rooms: [], members: [], thinking: {}, room: null, synced: 0, mode: 'mixed' };
|
|
60
511
|
const $ = id => document.getElementById(id);
|
|
61
512
|
// crypto.randomUUID only exists in secure contexts (https/localhost); plain-http LAN/Tailscale pages fall back to getRandomValues.
|
|
@@ -66,11 +517,19 @@ const uuid = () => {
|
|
|
66
517
|
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
67
518
|
};
|
|
68
519
|
const el = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };
|
|
69
|
-
|
|
520
|
+
// 身份色只 3 种(design-spec-channels 第 0 节第 3 条:同屏角色色 ≤3);来源:该规范,不用彩虹哈希
|
|
521
|
+
// taste-skill-ignore purple:靛蓝是 design-spec-channels.md 定死的唯一强调色(用户 ClawChat 标杆,不照抄其橙),
|
|
522
|
+
// 按 SKILL.md「已有品牌」条款保留品牌色;taste-skill 明说禁 indigo 是 owner 口味而非研究结论。
|
|
523
|
+
const palette = ['#4F46E5', '#B45309', '#0F766E']; // taste-skill-ignore 靛蓝为 design-spec 唯一强调色(用户标杆),按 SKILL.md 已有品牌条款保留
|
|
70
524
|
const color = id => palette[[...id].reduce((h, c) => (h * 31 + c.charCodeAt(0)) >>> 0, 7) % palette.length];
|
|
71
525
|
const kindLabel = { dsh: 'DSH', bot: 'Agent', human: '' };
|
|
526
|
+
const cardStateLabel = { pending: '待确认', running: '进行中', completed: '已完成', failed: '失败', cancelled: '已取消' };
|
|
527
|
+
// 5 分钟:同一人连续发言合并头像与昵称的窗口,沿用 Discord 的习惯(交接要求「按同一人连续发言分组」)
|
|
528
|
+
const GROUP_WINDOW_MS = 5 * 60 * 1000;
|
|
529
|
+
// 768px 是 U2 交接里定的窄屏阈值
|
|
530
|
+
const narrow = window.matchMedia('(max-width: 768px)');
|
|
72
531
|
|
|
73
|
-
const avatarSrc = v => !v ? '' : /^https?:/.test(v) ? v : '/collab/avatars/' + encodeURIComponent(v) + '.png';
|
|
532
|
+
const avatarSrc = v => !v ? '' : /^https?:/.test(v) ? v : ROOT + '/collab/avatars/' + encodeURIComponent(v) + '.png';
|
|
74
533
|
function avatar(id, name) {
|
|
75
534
|
const m = state.members.find(m => m.id === id);
|
|
76
535
|
const a = el('div', 'avatar', (name || id).slice(0, 1)); a.style.background = color(id);
|
|
@@ -79,11 +538,42 @@ function avatar(id, name) {
|
|
|
79
538
|
return a;
|
|
80
539
|
}
|
|
81
540
|
|
|
541
|
+
const pad2 = n => String(n).padStart(2, '0');
|
|
542
|
+
function fmtTime(ts) {
|
|
543
|
+
const d = new Date(ts * 1000);
|
|
544
|
+
const hm = `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
|
|
545
|
+
return d.toDateString() === new Date().toDateString() ? hm : `${d.getMonth() + 1}月${d.getDate()}日 ${hm}`;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/* ---------- 窄屏抽屉 ---------- */
|
|
549
|
+
let drawerTrigger = null;
|
|
550
|
+
function setDrawer(which, trigger) {
|
|
551
|
+
if (which && !document.body.dataset.drawer) drawerTrigger = trigger || document.activeElement;
|
|
552
|
+
if (!which) {
|
|
553
|
+
delete document.body.dataset.drawer;
|
|
554
|
+
} else {
|
|
555
|
+
document.body.dataset.drawer = which;
|
|
556
|
+
}
|
|
557
|
+
$('scrim').classList.toggle('show', !!which);
|
|
558
|
+
$('openRooms').setAttribute('aria-expanded', String(which === 'rooms'));
|
|
559
|
+
$('openMembers').setAttribute('aria-expanded', String(which === 'members'));
|
|
560
|
+
const pane = which === 'rooms' ? $('roomsPane') : which === 'members' ? $('members') : null;
|
|
561
|
+
if (pane) pane.querySelector('button, select')?.focus({ preventScroll: true });
|
|
562
|
+
else if (drawerTrigger) { drawerTrigger.focus({ preventScroll: true }); drawerTrigger = null; }
|
|
563
|
+
}
|
|
564
|
+
const closeDrawer = () => setDrawer(null);
|
|
565
|
+
$('openRooms').addEventListener('click', () => setDrawer('rooms', $('openRooms')));
|
|
566
|
+
$('openMembers').addEventListener('click', () => setDrawer('members', $('openMembers')));
|
|
567
|
+
$('scrim').addEventListener('click', closeDrawer);
|
|
568
|
+
document.addEventListener('keydown', e => { if (e.key === 'Escape' && document.body.dataset.drawer) closeDrawer(); });
|
|
569
|
+
narrow.addEventListener('change', closeDrawer);
|
|
570
|
+
|
|
82
571
|
function renderRooms() {
|
|
83
572
|
$('rooms').replaceChildren(...state.rooms.map(r => {
|
|
84
|
-
const b = el('button', '', '
|
|
573
|
+
const b = el('button', '', '');
|
|
574
|
+
b.append(el('span', 'hash', '#'), el('span', 'room-name', r.name));
|
|
85
575
|
b.setAttribute('aria-current', String(r.id === state.room));
|
|
86
|
-
b.onclick = () => openRoom(r.id);
|
|
576
|
+
b.onclick = () => { openRoom(r.id); closeDrawer(); };
|
|
87
577
|
return b;
|
|
88
578
|
}));
|
|
89
579
|
const r = state.rooms.find(r => r.id === state.room);
|
|
@@ -97,15 +587,15 @@ function renderMembers() {
|
|
|
97
587
|
const channel = room?.parent_id ?? room?.id;
|
|
98
588
|
const visible = state.members.filter(m => !m.rooms?.length || m.rooms.includes(channel));
|
|
99
589
|
const groups = [['Agent', visible.filter(m => m.kind !== 'human')], ['真人', visible.filter(m => m.kind === 'human')]];
|
|
100
|
-
const nodes = [];
|
|
590
|
+
const nodes = [el('div', 'pane-head', '成员')];
|
|
101
591
|
for (const [title, list] of groups) {
|
|
102
592
|
if (!list.length) continue;
|
|
103
593
|
nodes.push(el('h3', '', `${title} — ${list.length}`));
|
|
104
594
|
for (const m of list) {
|
|
105
595
|
const row = el('div', 'member' + (m.online ? '' : ' offline'));
|
|
106
596
|
const a = avatar(m.id, m.name); a.append(el('span', 'dot'));
|
|
107
|
-
const info = el('div');
|
|
108
|
-
const name = el('div', '', m.name);
|
|
597
|
+
const info = el('div', 'member-info');
|
|
598
|
+
const name = el('div', 'member-name', m.name);
|
|
109
599
|
if (kindLabel[m.kind]) name.append(el('span', 'tag', kindLabel[m.kind]));
|
|
110
600
|
info.append(name, el('div', 'status', busy.has(m.id) ? '正在处理…' : (m.online ? '在线' : '离线')));
|
|
111
601
|
row.append(a, info);
|
|
@@ -118,25 +608,35 @@ function renderMembers() {
|
|
|
118
608
|
}
|
|
119
609
|
|
|
120
610
|
function messageNode(m) {
|
|
121
|
-
const
|
|
611
|
+
const system = m.author_kind === 'system';
|
|
612
|
+
const row = el('div', 'msg' + (system ? ' system' : ''));
|
|
122
613
|
row.dataset.id = m.id;
|
|
614
|
+
row.dataset.author = m.author;
|
|
615
|
+
row.dataset.ts = m.created_at;
|
|
616
|
+
row.dataset.system = system ? '1' : '0';
|
|
123
617
|
const member = state.members.find(x => x.id === m.author);
|
|
124
618
|
row.append(avatar(m.author, m.name));
|
|
125
619
|
const col = el('div');
|
|
126
|
-
const head = el('div', '
|
|
127
|
-
|
|
128
|
-
|
|
620
|
+
const head = el('div', 'head');
|
|
621
|
+
const who = el('span', 'who', m.name);
|
|
622
|
+
if (member && kindLabel[member.kind]) who.append(el('span', 'tag', kindLabel[member.kind]));
|
|
623
|
+
head.append(who, el('span', 'when', fmtTime(m.created_at)));
|
|
129
624
|
col.append(head);
|
|
130
625
|
if (m.card) {
|
|
131
626
|
let meta = {}; try { meta = JSON.parse(m.card); } catch {}
|
|
132
|
-
const card = el('div', 'card
|
|
133
|
-
card.
|
|
627
|
+
const card = el('div', 'card');
|
|
628
|
+
card.dataset.state = meta.state || '';
|
|
629
|
+
const titleRow = el('div', 'card-head');
|
|
630
|
+
titleRow.append(el('span', 'title', meta.title || ''));
|
|
631
|
+
const label = cardStateLabel[meta.state];
|
|
632
|
+
if (label) titleRow.append(el('span', 'chip', label));
|
|
633
|
+
card.append(titleRow, el('div', 'body', m.text));
|
|
134
634
|
// Pending permission card: one button per action, posting the decision (same route as a text reply).
|
|
135
635
|
if (meta.kind === 'permission' && meta.state === 'pending' && Array.isArray(meta.actions)) {
|
|
136
636
|
const bar = el('div', 'actions');
|
|
137
637
|
for (const a of meta.actions) {
|
|
138
638
|
const b = el('button', '', a.label);
|
|
139
|
-
b.onclick = () => { bar.querySelectorAll('button').forEach(x => { x.disabled = true; }); api(
|
|
639
|
+
b.onclick = () => { bar.querySelectorAll('button').forEach(x => { x.disabled = true; }); api(`${ROOT}/collab/api/messages/${m.id}/permission`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ decision: a.decision }) }); };
|
|
140
640
|
bar.append(b);
|
|
141
641
|
}
|
|
142
642
|
card.append(bar);
|
|
@@ -147,6 +647,26 @@ function messageNode(m) {
|
|
|
147
647
|
return row;
|
|
148
648
|
}
|
|
149
649
|
|
|
650
|
+
// 同一人连续发言:后续消息不再重复头像和昵称,悬停时在左侧显示时间。
|
|
651
|
+
function regroup() {
|
|
652
|
+
let prev = null;
|
|
653
|
+
for (const node of $('log').children) {
|
|
654
|
+
if (!node.classList || !node.classList.contains('msg')) { prev = null; continue; }
|
|
655
|
+
const ts = Number(node.dataset.ts);
|
|
656
|
+
const system = node.dataset.system === '1';
|
|
657
|
+
const cont = !!prev && !system && !prev.system && prev.author === node.dataset.author
|
|
658
|
+
&& new Date(ts).toDateString() === new Date(prev.ts).toDateString()
|
|
659
|
+
&& ts - prev.ts > 0 && ts - prev.ts < GROUP_WINDOW_MS;
|
|
660
|
+
node.classList.toggle('cont', cont);
|
|
661
|
+
if (cont && !node.querySelector('.stamp')) {
|
|
662
|
+
const stamp = el('span', 'stamp', fmtTime(ts));
|
|
663
|
+
stamp.setAttribute('aria-hidden', 'true');
|
|
664
|
+
node.append(stamp);
|
|
665
|
+
}
|
|
666
|
+
prev = { author: node.dataset.author, ts, system };
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
150
670
|
function upsert(m) {
|
|
151
671
|
if (m.room_id !== state.room) return;
|
|
152
672
|
const log = $('log');
|
|
@@ -154,9 +674,10 @@ function upsert(m) {
|
|
|
154
674
|
const existing = log.querySelector(`[data-id="${m.id}"]`);
|
|
155
675
|
if (existing) existing.replaceWith(messageNode(m));
|
|
156
676
|
else {
|
|
157
|
-
const after = [...log.children].find(n => Number(n.dataset.id) > m.id);
|
|
677
|
+
const after = [...log.children].find(n => n.dataset.id != null && Number(n.dataset.id) > m.id);
|
|
158
678
|
log.insertBefore(messageNode(m), after || null);
|
|
159
679
|
}
|
|
680
|
+
regroup();
|
|
160
681
|
if (stick) log.scrollTop = log.scrollHeight;
|
|
161
682
|
}
|
|
162
683
|
|
|
@@ -166,22 +687,47 @@ async function api(path, init) {
|
|
|
166
687
|
return r.json();
|
|
167
688
|
}
|
|
168
689
|
|
|
690
|
+
function showState(kind, title, detail, retry) {
|
|
691
|
+
const box = el('div', 'state ' + kind);
|
|
692
|
+
if (kind === 'loading') box.append(el('div', 'spinner'));
|
|
693
|
+
box.append(el('div', 'state-title', title));
|
|
694
|
+
if (detail) box.append(el('div', 'state-detail', detail));
|
|
695
|
+
if (kind === 'empty') box.append(el('div', 'state-hint', '在下面输入框发第一条消息吧'));
|
|
696
|
+
if (retry) { const b = el('button', 'btn', '重试'); b.type = 'button'; b.onclick = retry; box.append(b); }
|
|
697
|
+
return box;
|
|
698
|
+
}
|
|
699
|
+
|
|
169
700
|
async function openRoom(id) {
|
|
170
701
|
state.room = id; state.synced = 0;
|
|
171
|
-
$('log').replaceChildren();
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
702
|
+
$('log').replaceChildren(showState('loading', '正在加载消息…'));
|
|
703
|
+
$('log').setAttribute('aria-busy', 'true');
|
|
704
|
+
$('input').disabled = true;
|
|
705
|
+
closeDrawer();
|
|
706
|
+
try {
|
|
707
|
+
const setting = await api(`${ROOT}/collab/api/rooms/${id}/mode`);
|
|
708
|
+
state.mode = setting.dispatch_mode;
|
|
709
|
+
renderRooms(); renderMembers();
|
|
710
|
+
const { messages } = await api(`${ROOT}/collab/api/rooms/${id}/messages?limit=100`);
|
|
711
|
+
if (state.room !== id) return;
|
|
712
|
+
const log = $('log');
|
|
713
|
+
log.querySelector('.state')?.remove();
|
|
714
|
+
messages.forEach(upsert);
|
|
715
|
+
state.synced = messages.length ? messages[messages.length - 1].id : 0;
|
|
716
|
+
if (!log.querySelector('.msg')) log.append(showState('empty', '还没有消息', ''));
|
|
717
|
+
$('log').setAttribute('aria-busy', 'false');
|
|
718
|
+
$('input').disabled = false;
|
|
719
|
+
$('log').scrollTop = $('log').scrollHeight;
|
|
720
|
+
} catch (err) {
|
|
721
|
+
if (state.room !== id) return;
|
|
722
|
+
$('log').replaceChildren(showState('error', '加载失败', err.message, () => openRoom(id)));
|
|
723
|
+
$('log').setAttribute('aria-busy', 'false');
|
|
724
|
+
}
|
|
179
725
|
}
|
|
180
726
|
|
|
181
727
|
$('mode').addEventListener('change', async (e) => {
|
|
182
728
|
if (state.room == null) return;
|
|
183
729
|
try {
|
|
184
|
-
const result = await api(
|
|
730
|
+
const result = await api(`${ROOT}/collab/api/rooms/${state.room}/mode`, {
|
|
185
731
|
method: 'PATCH', headers: { 'content-type': 'application/json' },
|
|
186
732
|
body: JSON.stringify({ dispatch_mode: e.target.value }),
|
|
187
733
|
});
|
|
@@ -195,7 +741,7 @@ async function catchUp() {
|
|
|
195
741
|
const room = state.room;
|
|
196
742
|
if (room == null) return;
|
|
197
743
|
for (;;) {
|
|
198
|
-
const { messages } = await api(
|
|
744
|
+
const { messages } = await api(`${ROOT}/collab/api/rooms/${room}/messages?after=${state.synced}&limit=200`);
|
|
199
745
|
if (state.room !== room) return;
|
|
200
746
|
messages.forEach(upsert);
|
|
201
747
|
if (messages.length) state.synced = messages[messages.length - 1].id;
|
|
@@ -204,12 +750,12 @@ async function catchUp() {
|
|
|
204
750
|
}
|
|
205
751
|
|
|
206
752
|
function connect() {
|
|
207
|
-
const es = new EventSource('/collab/api/events');
|
|
753
|
+
const es = new EventSource(ROOT + '/collab/api/events');
|
|
208
754
|
es.onopen = () => catchUp().catch(console.warn);
|
|
209
755
|
es.onmessage = (e) => {
|
|
210
756
|
const ev = JSON.parse(e.data);
|
|
211
757
|
if (ev.type === 'message') upsert(ev.message);
|
|
212
|
-
else if (ev.type === 'deleted') $('log').querySelector(`[data-id="${ev.id}"]`)?.remove();
|
|
758
|
+
else if (ev.type === 'deleted') { $('log').querySelector(`[data-id="${ev.id}"]`)?.remove(); regroup(); }
|
|
213
759
|
else if (ev.type === 'members') { state.members = ev.members; renderMembers(); }
|
|
214
760
|
else if (ev.type === 'thinking') { state.thinking[ev.room_id] = ev.members; renderMembers(); }
|
|
215
761
|
};
|
|
@@ -225,7 +771,7 @@ $('form').addEventListener('submit', async (e) => {
|
|
|
225
771
|
if (!text || state.room == null) return;
|
|
226
772
|
$('input').value = ''; $('input').style.height = 'auto';
|
|
227
773
|
try {
|
|
228
|
-
const { message } = await api(
|
|
774
|
+
const { message } = await api(`${ROOT}/collab/api/rooms/${state.room}/messages`, {
|
|
229
775
|
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
230
776
|
body: JSON.stringify({ text, client_key: uuid() }),
|
|
231
777
|
});
|
|
@@ -234,11 +780,14 @@ $('form').addEventListener('submit', async (e) => {
|
|
|
234
780
|
});
|
|
235
781
|
|
|
236
782
|
(async () => {
|
|
237
|
-
const s = await api('/collab/api/state');
|
|
783
|
+
const s = await api(ROOT + '/collab/api/state');
|
|
238
784
|
Object.assign(state, { me: s.me, rooms: s.rooms, members: s.members, thinking: s.thinking });
|
|
239
785
|
connect();
|
|
240
786
|
await openRoom(s.rooms[0].id);
|
|
241
|
-
})().catch(err => {
|
|
787
|
+
})().catch(err => {
|
|
788
|
+
$('log').replaceChildren(showState('error', '加载失败', err.message, () => location.reload()));
|
|
789
|
+
$('log').setAttribute('aria-busy', 'false');
|
|
790
|
+
});
|
|
242
791
|
</script>
|
|
243
792
|
</body>
|
|
244
|
-
</html>
|
|
793
|
+
</html>
|