ai_co_discuss 0.2.41 → 0.2.43
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/.agents/skills/discuss-host/SKILL.md +1 -0
- package/backend/admin.py +186 -0
- package/backend/schema.sql +670 -655
- package/backend/server.py +57 -1
- package/docs/architecture.md +4 -1
- package/docs/deployment.md +4 -0
- package/docs/runner.md +1 -1
- package/docs/workflow.md +1 -1
- package/package.json +1 -1
- package/runner/discussion_runner.mjs +5 -2
- package/runner/scheduler.mjs +50 -26
- package/web/app.js +171 -9
- package/web/modern.css +1 -1
- package/web/modern.js +1441 -1127
package/web/app.js
CHANGED
|
@@ -17,12 +17,22 @@ const DIRECTORY_DRAG_OFFSET = 12;
|
|
|
17
17
|
const THEME_STORAGE_KEY = "ai-codiscuss-theme";
|
|
18
18
|
const DIRECTORY_SORT_KEY = "ai-codiscuss-directory-sort";
|
|
19
19
|
const LAUNCH_PENDING_PREFIX = "discuss-launch:";
|
|
20
|
+
const ADMIN_SESSION_KEY = "discuss-admin-session";
|
|
21
|
+
const ADMIN_USERNAME_MAX = 30;
|
|
22
|
+
const ADMIN_PASSWORD_MIN = 8;
|
|
23
|
+
const ADMIN_PASSWORD_MAX = 128;
|
|
24
|
+
const ADMIN_AVATAR_MAX_CHARS = 60000;
|
|
25
|
+
const ADMIN_AVATAR_SIZE = 96;
|
|
26
|
+
const ADMIN_AVATAR_QUALITY = 0.8;
|
|
20
27
|
const MEMBER_ACCESS_PREFIX = "discuss-members:";
|
|
21
28
|
const MEMBER_PENDING_PREFIX = "discuss-member-pending:";
|
|
22
29
|
const LOCAL_CONNECTION_KEY = "discuss-local-connection";
|
|
23
30
|
const LOCAL_CONNECTION_TIMEOUT_MS = 15000;
|
|
24
31
|
const MODEL_CATALOG_TIMEOUT_MS = 35000;
|
|
25
32
|
let local_connection = null;
|
|
33
|
+
const DEFAULT_USER_LABEL = "User";
|
|
34
|
+
const DEFAULT_USER_ICON = "/icons/user.svg";
|
|
35
|
+
const MANAGEMENT_LOGIN_HINT = "需要登录管理员账号或使用创建该房间的浏览器";
|
|
26
36
|
const THEMES = ["channel", "channel-light"];
|
|
27
37
|
const SOURCE_LABELS = { demo: "示例", user: "本人发言", proxy: "用户代发", cli: "CLI 发言", api: "API 发言", url: "历史 URL 发言", host_proxy: "主持人代发" };
|
|
28
38
|
const PHASE_LABELS = { preparing: "准备中", initial: "独立观点", discussing: "深入讨论", reviewing: "结论复核", paused: "已暂停", concluded: "已收束" };
|
|
@@ -60,6 +70,7 @@ state.directory_notice = "";
|
|
|
60
70
|
state.directory_pending = new Map();
|
|
61
71
|
state.directory_version = 0;
|
|
62
72
|
state.navigation_version = 0;
|
|
73
|
+
state.admin = { loaded: false, local: false, registered: false, setup_available: false, logged_in: false, profile: null, identity_version: 0 };
|
|
63
74
|
state.launch = { available: false, remote: false, connect_command: "", profiles: {}, workspace: {}, scheduler: {}, create: true, error: "", notice: "", recoveries: [] };
|
|
64
75
|
const PLATFORM_CLIENTS = { Codex: [{ value: "codex_cli", label: "Codex CLI" }, { value: "codex", label: "Codex App" }],
|
|
65
76
|
Claude: [{ value: "claude", label: "Claude Code CLI" }],
|
|
@@ -125,12 +136,31 @@ function show_toast(message, is_error = false) {
|
|
|
125
136
|
state.toast_timer = setTimeout(() => { element("toast").hidden = true; }, TOAST_DURATION_MS);
|
|
126
137
|
}
|
|
127
138
|
|
|
139
|
+
/** The administrator session token kept by this browser, or an empty string. */
|
|
140
|
+
function admin_token() {
|
|
141
|
+
if (state.admin.local) return "";
|
|
142
|
+
try { return localStorage.getItem(ADMIN_SESSION_KEY) || ""; } catch { return ""; }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Store or clear the administrator session token; storage failures only lose persistence. */
|
|
146
|
+
function store_admin_token(token) {
|
|
147
|
+
try { token ? localStorage.setItem(ADMIN_SESSION_KEY, token) : localStorage.removeItem(ADMIN_SESSION_KEY); } catch { /* Keep the session in memory only. */ }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The administrator header for writes that bypass request_json, or no header without a session. */
|
|
151
|
+
function admin_headers() {
|
|
152
|
+
const session = admin_token();
|
|
153
|
+
return session ? { "X-Discussion-Admin": session } : {};
|
|
154
|
+
}
|
|
155
|
+
|
|
128
156
|
/** Request JSON from the local or remotely hosted discussion service. */
|
|
129
157
|
async function request_json(path, payload, access_token) {
|
|
130
158
|
const options = { cache: "no-store" };
|
|
131
159
|
const target_room = path.match(/^\/api\/rooms\/([^/?]+)/)?.[1];
|
|
132
160
|
const token = access_token || (target_room ? localStorage.getItem(`discuss-access:${target_room}`) : null);
|
|
161
|
+
const session = admin_token();
|
|
133
162
|
options.headers = token ? { "X-Discussion-Token": token } : {};
|
|
163
|
+
if (session) options.headers["X-Discussion-Admin"] = session;
|
|
134
164
|
if (payload !== undefined) {
|
|
135
165
|
options.method = "POST";
|
|
136
166
|
options.headers["Content-Type"] = "application/json";
|
|
@@ -138,10 +168,127 @@ async function request_json(path, payload, access_token) {
|
|
|
138
168
|
}
|
|
139
169
|
const response = await fetch(path, options);
|
|
140
170
|
const result = await response.json();
|
|
141
|
-
if (!response.ok) throw Object.assign(new Error(result.error || "请求失败,请重试"), { preflight: response.status >= 400 && response.status < 500 });
|
|
171
|
+
if (!response.ok) throw Object.assign(new Error(result.error || "请求失败,请重试"), { preflight: response.status >= 400 && response.status < 500, status: response.status });
|
|
142
172
|
return result;
|
|
143
173
|
}
|
|
144
174
|
|
|
175
|
+
/** The owner's public name and avatar replace the default "User" identity for every visitor. */
|
|
176
|
+
function apply_admin_identity(profile) {
|
|
177
|
+
Object.assign(PLATFORM_IDENTITIES["用户"], { label: profile?.username || DEFAULT_USER_LABEL, icon: profile?.avatar || DEFAULT_USER_ICON });
|
|
178
|
+
state.admin.identity_version++;
|
|
179
|
+
if (!element("participants")) return;
|
|
180
|
+
for (const id of ["author-select", "topic-host-select", "new-room-host"]) {
|
|
181
|
+
const option = Array.from(element(id)?.options || []).find(item => item.value === "用户");
|
|
182
|
+
if (option) option.textContent = display_name("用户");
|
|
183
|
+
}
|
|
184
|
+
render_participants();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Record the server's administrator state and refresh every identity-dependent view without a reload. */
|
|
188
|
+
function apply_admin_state(info) {
|
|
189
|
+
const local = info.local === true;
|
|
190
|
+
if (local || !info.logged_in) store_admin_token("");
|
|
191
|
+
Object.assign(state.admin, { loaded: true, local, registered: Boolean(info.registered), setup_available: Boolean(info.setup_available),
|
|
192
|
+
logged_in: Boolean(info.logged_in), profile: info.profile ? { username: info.profile.username, avatar: info.profile.avatar || "" } : null });
|
|
193
|
+
apply_admin_identity(state.admin.profile);
|
|
194
|
+
publish_view();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Read the public administrator state; a failure leaves the visitor view unchanged. */
|
|
198
|
+
async function load_admin_state() {
|
|
199
|
+
try { apply_admin_state(await request_json("/api/admin")); }
|
|
200
|
+
catch { /* The directory request reports connection problems. */ }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Return the first problem with an administrator form, matching the server's limits. */
|
|
204
|
+
function validate_admin_form({ username, password, confirm, check_password = true }) {
|
|
205
|
+
const name_length = [...String(username ?? "").trim()].length;
|
|
206
|
+
if (username !== undefined && (name_length < 1 || name_length > ADMIN_USERNAME_MAX)) return `用户名需要 1–${ADMIN_USERNAME_MAX} 个字符`;
|
|
207
|
+
if (!check_password) return "";
|
|
208
|
+
const length = [...String(password ?? "")].length;
|
|
209
|
+
if (length < ADMIN_PASSWORD_MIN || length > ADMIN_PASSWORD_MAX) return `密码需要 ${ADMIN_PASSWORD_MIN}–${ADMIN_PASSWORD_MAX} 个字符`;
|
|
210
|
+
if (confirm !== undefined && confirm !== password) return "两次输入的密码不一致";
|
|
211
|
+
return "";
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Keep the Chinese server message and add a retry hint when a locked response lacks the delay. */
|
|
215
|
+
function admin_error(error) {
|
|
216
|
+
if (error.status === 429 && !/\d/.test(error.message)) error.message += ",请稍后再试";
|
|
217
|
+
return error;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Store the new session and show the owner's identity. */
|
|
221
|
+
function accept_admin_session({ token, profile }) {
|
|
222
|
+
store_admin_token(token);
|
|
223
|
+
apply_admin_state({ registered: true, setup_available: false, logged_in: true, profile });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Create the single administrator account and sign in with it. */
|
|
227
|
+
async function register_admin({ username, password, confirm, setup_key }) {
|
|
228
|
+
const problem = validate_admin_form({ username, password, confirm }) || (String(setup_key ?? "").trim() ? "" : "请填写安装令牌");
|
|
229
|
+
if (problem) throw new Error(problem);
|
|
230
|
+
try { accept_admin_session(await request_json("/api/admin/register", { username: username.trim(), password, setup_key: setup_key.trim() })); }
|
|
231
|
+
catch (error) { throw admin_error(error); }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Sign in and keep the session in this browser. */
|
|
235
|
+
async function login_admin({ username, password }) {
|
|
236
|
+
if (!String(username ?? "").trim() || !password) throw new Error("请填写用户名和密码");
|
|
237
|
+
try { accept_admin_session(await request_json("/api/admin/login", { username: username.trim(), password })); }
|
|
238
|
+
catch (error) { throw admin_error(error); }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Leave this browser; the local session is dropped even if the server cannot be reached. */
|
|
242
|
+
async function logout_admin() {
|
|
243
|
+
try { await request_json("/api/admin/logout", {}); }
|
|
244
|
+
catch { /* The token is removed below either way. */ }
|
|
245
|
+
store_admin_token("");
|
|
246
|
+
state.admin.logged_in = false;
|
|
247
|
+
publish_view();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Save the display name and/or avatar; an empty avatar restores the default. */
|
|
251
|
+
async function update_admin_profile(patch) {
|
|
252
|
+
if (patch.username !== undefined) {
|
|
253
|
+
const problem = validate_admin_form({ username: patch.username, check_password: false });
|
|
254
|
+
if (problem) throw new Error(problem);
|
|
255
|
+
patch = { ...patch, username: patch.username.trim() };
|
|
256
|
+
}
|
|
257
|
+
const result = await request_json("/api/admin/profile", patch);
|
|
258
|
+
apply_admin_state({ local: state.admin.local, registered: true, setup_available: false, logged_in: true, profile: result.profile });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Shrink the chosen image into a small square data URL that fits the server limit. */
|
|
262
|
+
async function admin_avatar_from_file(file) {
|
|
263
|
+
if (!file?.type?.startsWith("image/")) throw new Error("请选择图片文件");
|
|
264
|
+
const url = URL.createObjectURL(file);
|
|
265
|
+
try {
|
|
266
|
+
const image = await new Promise((resolve, reject) => {
|
|
267
|
+
const loaded = new Image();
|
|
268
|
+
loaded.onload = () => resolve(loaded);
|
|
269
|
+
loaded.onerror = () => reject(new Error("无法读取这张图片"));
|
|
270
|
+
loaded.src = url;
|
|
271
|
+
});
|
|
272
|
+
const side = Math.min(image.naturalWidth, image.naturalHeight);
|
|
273
|
+
if (!side) throw new Error("无法读取这张图片");
|
|
274
|
+
const canvas = document.createElement("canvas");
|
|
275
|
+
canvas.width = canvas.height = ADMIN_AVATAR_SIZE;
|
|
276
|
+
const context = canvas.getContext("2d");
|
|
277
|
+
context.fillStyle = "#ffffff";
|
|
278
|
+
context.fillRect(0, 0, ADMIN_AVATAR_SIZE, ADMIN_AVATAR_SIZE);
|
|
279
|
+
context.drawImage(image, (image.naturalWidth - side) / 2, (image.naturalHeight - side) / 2, side, side, 0, 0, ADMIN_AVATAR_SIZE, ADMIN_AVATAR_SIZE);
|
|
280
|
+
const data_url = canvas.toDataURL("image/jpeg", ADMIN_AVATAR_QUALITY);
|
|
281
|
+
if (data_url.length > ADMIN_AVATAR_MAX_CHARS) throw new Error("图片过大,请换一张");
|
|
282
|
+
return data_url;
|
|
283
|
+
} finally { URL.revokeObjectURL(url); }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Explain why a management write failed when this browser has neither credential. */
|
|
287
|
+
function management_error_message(room_id, error) {
|
|
288
|
+
if (error.status === 403 && !state.admin.logged_in && !localStorage.getItem(`discuss-access:${room_id}`)) return MANAGEMENT_LOGIN_HINT;
|
|
289
|
+
return error.message;
|
|
290
|
+
}
|
|
291
|
+
|
|
145
292
|
/** Show connection state without claiming that any AI is online. */
|
|
146
293
|
function set_connection(connected) {
|
|
147
294
|
state.connected = connected;
|
|
@@ -179,18 +326,23 @@ function display_name(name) { return IDENTITIES[name]?.label || name; }
|
|
|
179
326
|
|
|
180
327
|
/** Render an identity avatar from a local SVG. */
|
|
181
328
|
function identity_avatar(identity) {
|
|
182
|
-
return identity.icon ? `<img src="${identity.icon}" alt="">` : identity.initials;
|
|
329
|
+
return identity.icon ? `<img src="${escape_html(identity.icon)}" alt="">` : identity.initials;
|
|
183
330
|
}
|
|
184
331
|
|
|
185
332
|
/** Render identity controls and distinguish selectable identities from connections. */
|
|
186
333
|
function render_identities() {
|
|
187
|
-
const options = Object.keys(IDENTITIES).map((name) => `<option value="${name}">${display_name(name)}</option>`).join("");
|
|
334
|
+
const options = Object.keys(IDENTITIES).map((name) => `<option value="${name}">${escape_html(display_name(name))}</option>`).join("");
|
|
188
335
|
element("author-select").innerHTML = options;
|
|
189
336
|
element("topic-host-select").innerHTML = options;
|
|
190
337
|
element("new-room-host").innerHTML = options;
|
|
191
338
|
render_expert_inputs("new-expert-modes", default_experts());
|
|
339
|
+
render_participants();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** List the selectable identities in the classic sidebar. */
|
|
343
|
+
function render_participants() {
|
|
192
344
|
element("participants").innerHTML = Object.entries(IDENTITIES).map(([name, identity]) =>
|
|
193
|
-
`<div class="participant ${identity.style}"><span class="avatar" aria-hidden="true">${identity_avatar(identity)}</span><span class="participant-name">${display_name(name)}</span><span class="participant-role">${identity.description}</span></div>`).join("");
|
|
345
|
+
`<div class="participant ${identity.style}"><span class="avatar" aria-hidden="true">${identity_avatar(identity)}</span><span class="participant-name">${escape_html(display_name(name))}</span><span class="participant-role">${identity.description}</span></div>`).join("");
|
|
194
346
|
}
|
|
195
347
|
|
|
196
348
|
/** Render rooms while retaining the selected room. */
|
|
@@ -331,6 +483,7 @@ async function delete_channel(id) {
|
|
|
331
483
|
if (!room) return;
|
|
332
484
|
const previous = state.rooms.map(item => item.id);
|
|
333
485
|
return update_directory({ key: `delete:${id}`, label: `「${room.title}」删除`,
|
|
486
|
+
error_message: error => `「${room.title}」删除失败,已恢复原状态:${management_error_message(id, error)}`,
|
|
334
487
|
apply: () => {
|
|
335
488
|
state.rooms = state.rooms.filter(item => item.id !== id);
|
|
336
489
|
if (state.room?.id === id) run_action(() => select_room(visible_rooms()[0]?.id));
|
|
@@ -370,7 +523,7 @@ async function archive_channel(id, archived) {
|
|
|
370
523
|
const saved = await request_json(`/api/rooms/${id}/${archived ? "archive" : "restore"}`, { actor: "用户" });
|
|
371
524
|
display_channel_archive(id, { archived: Boolean(saved.archived), phase: saved.phase }, change);
|
|
372
525
|
} catch (error) {
|
|
373
|
-
state.directory_error = `「${room.title}」${archived ? "存档" : "恢复"}失败,已恢复原状态:${error
|
|
526
|
+
state.directory_error = `「${room.title}」${archived ? "存档" : "恢复"}失败,已恢复原状态:${management_error_message(id, error)}`;
|
|
374
527
|
display_channel_archive(id, previous, change);
|
|
375
528
|
throw error;
|
|
376
529
|
} finally {
|
|
@@ -1077,7 +1230,7 @@ async function upload_message_attachments(room_id, actor, drafts) {
|
|
|
1077
1230
|
const params = new URLSearchParams({ file_name: item.file.name, actor, submission_id: item.upload_submission });
|
|
1078
1231
|
const token = localStorage.getItem(`discuss-access:${room_id}`);
|
|
1079
1232
|
const response = await fetch(`/api/rooms/${room_id}/attachments?${params}`, { method: "POST",
|
|
1080
|
-
headers: { "Content-Type": "application/octet-stream", ...(token ? { "X-Discussion-Token": token } : {}) }, body: item.file });
|
|
1233
|
+
headers: { "Content-Type": "application/octet-stream", ...(token ? { "X-Discussion-Token": token } : {}), ...admin_headers() }, body: item.file });
|
|
1081
1234
|
const result = await response.json();
|
|
1082
1235
|
if (!response.ok) throw new Error(result.error || "附件上传失败");
|
|
1083
1236
|
item.uploaded = result.attachment.id;
|
|
@@ -1310,6 +1463,12 @@ function save_room_credentials(room, token, members = []) {
|
|
|
1310
1463
|
localStorage.setItem(MEMBER_ACCESS_PREFIX + room.id, JSON.stringify(saved));
|
|
1311
1464
|
}
|
|
1312
1465
|
|
|
1466
|
+
/** Public administrator state for the account UI; the session token never leaves the controller. */
|
|
1467
|
+
function admin_snapshot() {
|
|
1468
|
+
const { loaded, local, registered, setup_available, logged_in, profile, identity_version } = state.admin;
|
|
1469
|
+
return { loaded, local, registered, setup_available, logged_in, identity_version, profile: profile && { ...profile } };
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1313
1472
|
/** Build only non-secret status metadata for the currently selected room. */
|
|
1314
1473
|
function access_snapshot() {
|
|
1315
1474
|
const room = state.room;
|
|
@@ -2900,6 +3059,7 @@ async function initialize() {
|
|
|
2900
3059
|
if (button?.dataset.launchDiscard) discard_launch(button.dataset.launchDiscard);
|
|
2901
3060
|
if (button?.dataset.launchRoom) run_action(() => select_room(button.dataset.launchRoom));
|
|
2902
3061
|
});
|
|
3062
|
+
load_admin_state();
|
|
2903
3063
|
try {
|
|
2904
3064
|
const result = await request_json("/api/rooms");
|
|
2905
3065
|
state.rooms = result.rooms;
|
|
@@ -2911,7 +3071,7 @@ async function initialize() {
|
|
|
2911
3071
|
start_live_updates();
|
|
2912
3072
|
document.addEventListener("visibilitychange", () => {
|
|
2913
3073
|
if (document.hidden) stop_live_updates();
|
|
2914
|
-
else start_live_updates();
|
|
3074
|
+
else { start_live_updates(); load_admin_state(); }
|
|
2915
3075
|
});
|
|
2916
3076
|
window.addEventListener("pagehide", stop_live_updates);
|
|
2917
3077
|
window.addEventListener("pageshow", start_live_updates);
|
|
@@ -2936,7 +3096,7 @@ function view_snapshot() {
|
|
|
2936
3096
|
composer_error: state.composer_error, upload_status: state.upload_status, sending_message: state.sending_message,
|
|
2937
3097
|
mention_only: state.mention_only, reply_to: state.reply_to, turn_reply_to: state.turn_reply_to,
|
|
2938
3098
|
free_notice: state.free_notice, free_error: state.free_error, free_proxy: state.free_proxy,
|
|
2939
|
-
create_parent: state.create_parent, access: access_snapshot(),
|
|
3099
|
+
create_parent: state.create_parent, access: access_snapshot(), admin: admin_snapshot(),
|
|
2940
3100
|
tab: state.tab, snapshot_id: state.snapshot_id, context_dirty: state.context_dirty,
|
|
2941
3101
|
theme: document.documentElement.dataset.theme, fields, experts, launch: structuredClone(state.launch), named_followup: state.named_followup };
|
|
2942
3102
|
}
|
|
@@ -3052,7 +3212,7 @@ async function upload_imported_files(room_id, token, mapping, files) {
|
|
|
3052
3212
|
for (const [old_id, new_id] of Object.entries(mapping)) {
|
|
3053
3213
|
const data = files.get(old_id);
|
|
3054
3214
|
const response = data && await fetch(`/api/rooms/${encodeURIComponent(room_id)}/attachments/${encodeURIComponent(new_id)}/import`,
|
|
3055
|
-
{ method: "POST", headers: { "X-Discussion-Token": token }, body: data });
|
|
3215
|
+
{ method: "POST", headers: { "X-Discussion-Token": token, ...admin_headers() }, body: data });
|
|
3056
3216
|
if (!response?.ok) missing++;
|
|
3057
3217
|
}
|
|
3058
3218
|
return missing;
|
|
@@ -3137,6 +3297,8 @@ window.discuss_view = { snapshot: view_snapshot, write_field: write_view_field,
|
|
|
3137
3297
|
start_drag: start_directory_drag,
|
|
3138
3298
|
attachment_markup, attachment_size, attachment_file_icon, drop_attachments, paste_attachments,
|
|
3139
3299
|
write_expert: write_view_expert, add_instance: add_view_instance, platform_of, execute: execute_view_action, theme: change_view_theme,
|
|
3300
|
+
admin: { register: register_admin, login: login_admin, logout: logout_admin,
|
|
3301
|
+
update_profile: update_admin_profile, avatar_from_file: admin_avatar_from_file, validate: validate_admin_form },
|
|
3140
3302
|
request: request_json, identities: IDENTITIES, display_name, sources: SOURCE_LABELS, phases: PHASE_LABELS,
|
|
3141
3303
|
runtime_label, execution_label, limits_label, participant_model, participant_access, can_retry_execution,
|
|
3142
3304
|
turns: TURN_LABELS, turn_label, turn_guidance, floor_status, progress_round, progress_speaker, brief_meta, max_message_length: MAX_MESSAGE_LENGTH, provenance: message_provenance, format_time };
|