ai_co_discuss 0.3.7 → 0.3.9
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/backend/free_room.py +78 -20
- package/backend/schema.sql +8 -0
- package/backend/server.py +28 -5
- package/package.json +1 -1
- package/runner/discuss.py +4 -0
- package/runner/discussion_service.mjs +15 -5
- package/runner/free_runner.mjs +2 -1
- package/web/app.js +148 -173
- package/web/modern.css +1 -1
- package/web/modern.js +4389 -4587
package/web/app.js
CHANGED
|
@@ -53,33 +53,31 @@ const PLATFORM_IDENTITIES = {
|
|
|
53
53
|
Grok: { initials: "GK", style: "grok", description: "AI 身份" },
|
|
54
54
|
};
|
|
55
55
|
const IDENTITIES = by_platform(PLATFORM_IDENTITIES, (identity, name) => ({ ...identity, label: name, description: "AI 实例" }));
|
|
56
|
-
const state = { rooms: [], folders: [], room: null, page: 1, pages: 1, query: "", mention_only: false,
|
|
56
|
+
const state = { rooms: [], folders: [], room: null, page: 1, pages: 1, query: "", mention_only: false,
|
|
57
57
|
free_proxy: "", create_parent: null, named_followup: false, create_discussion: false,
|
|
58
58
|
archive_view: false, directory_sort: "manual", management: null, dragged_room: null, folder_moves: new Map(), folder_version: 0,
|
|
59
59
|
reply_to: null, turn_reply_to: null, tab: "topic", refresh_version: 0, context_dirty: false, topic_dirty: false,
|
|
60
60
|
snapshot_id: 0, pending_submission: null, pending_turn: null, pending_room: null, toast_timer: null,
|
|
61
|
-
creation_mode: "brainstorm", creation_drafts: {}
|
|
61
|
+
creation_mode: "brainstorm", creation_drafts: {} };
|
|
62
62
|
const live_updates = { source: null, connected: false, enabled: false, timer: null, poll_timer: null,
|
|
63
63
|
running: false, dirty: false, full: false, revisions: "", refreshed_at: 0 };
|
|
64
64
|
state.messages = [];
|
|
65
65
|
state.message_history = null;
|
|
66
66
|
state.connected = false;
|
|
67
|
+
state.reconnecting = false;
|
|
67
68
|
state.attachment_drafts = [];
|
|
68
|
-
state.composer_error = "";
|
|
69
69
|
state.upload_status = "";
|
|
70
70
|
state.sending_message = false;
|
|
71
71
|
state.attachment_preview = null;
|
|
72
72
|
state.archive_changes = new Map();
|
|
73
73
|
state.archive_version = 0;
|
|
74
|
-
state.directory_error = "";
|
|
75
|
-
state.directory_notice = "";
|
|
76
74
|
// The open import's progress for the import dialog: { done, rooms: [{ title, status, members, error }], errors }.
|
|
77
75
|
state.import_progress = null;
|
|
78
76
|
state.directory_pending = new Map();
|
|
79
77
|
state.directory_version = 0;
|
|
80
78
|
state.navigation_version = 0;
|
|
81
79
|
state.admin = { loaded: false, local: false, registered: false, setup_available: false, logged_in: false, profile: null, identity_version: 0 };
|
|
82
|
-
state.launch = { available: false, remote: false, relay: null, connect_command: "", profiles: {}, projects: [], presets: [], builtin_presets: [], description_templates: {}, workspace: {}, scheduler: {}, create: true, error: "",
|
|
80
|
+
state.launch = { available: false, remote: false, relay: null, connect_command: "", profiles: {}, projects: [], presets: [], builtin_presets: [], description_templates: {}, workspace: {}, scheduler: {}, create: true, error: "", recoveries: [] };
|
|
83
81
|
const PLATFORM_CLIENTS = { Codex: [{ value: "codex_cli", label: "Codex CLI" }, { value: "codex", label: "Codex App" }],
|
|
84
82
|
Claude: [{ value: "claude", label: "Claude Code CLI" }],
|
|
85
83
|
Gemini: [{ value: "antigravity_cli", label: "Antigravity CLI" }, { value: "antigravity", label: "Antigravity App" }] };
|
|
@@ -136,7 +134,8 @@ function format_time(value) {
|
|
|
136
134
|
|
|
137
135
|
/** Display operation feedback only in the classic renderer. */
|
|
138
136
|
function show_toast(message, is_error = false) {
|
|
139
|
-
|
|
137
|
+
// The channel themes stay quiet on success; failures go to the shared toaster.
|
|
138
|
+
if (document.documentElement.dataset.theme.startsWith("channel")) { if (is_error) notify("error", message); return; }
|
|
140
139
|
clearTimeout(state.toast_timer);
|
|
141
140
|
element("toast").textContent = message;
|
|
142
141
|
element("toast").classList.toggle("error", is_error);
|
|
@@ -144,6 +143,17 @@ function show_toast(message, is_error = false) {
|
|
|
144
143
|
state.toast_timer = setTimeout(() => { element("toast").hidden = true; }, TOAST_DURATION_MS);
|
|
145
144
|
}
|
|
146
145
|
|
|
146
|
+
/** Show a transient notice in the channel's toaster; level is "error", "warning", or "info". */
|
|
147
|
+
function notify(level, message) {
|
|
148
|
+
document.dispatchEvent(new CustomEvent("discuss:notice", { detail: { level, message } }));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Show a rethrown error in its own wording, so the action dispatcher does not show it again. */
|
|
152
|
+
function notify_failure(error, message) {
|
|
153
|
+
notify("error", message);
|
|
154
|
+
error.notified = true;
|
|
155
|
+
}
|
|
156
|
+
|
|
147
157
|
/** The administrator session token kept by this browser, or an empty string. */
|
|
148
158
|
function admin_token() {
|
|
149
159
|
if (state.admin.local) return "";
|
|
@@ -313,6 +323,7 @@ function management_error_message(room_id, error) {
|
|
|
313
323
|
/** Show connection state without claiming that any AI is online. */
|
|
314
324
|
function set_connection(connected) {
|
|
315
325
|
state.connected = connected;
|
|
326
|
+
state.reconnecting = false;
|
|
316
327
|
element("connection-dot").className = `status-dot ${connected ? "connected" : "failed"}`;
|
|
317
328
|
element("connection-status").textContent = connected ? "讨论网站已连接" : "网站连接中断,可手动刷新";
|
|
318
329
|
publish_view();
|
|
@@ -435,7 +446,6 @@ async function update_directory(change) {
|
|
|
435
446
|
if (state.directory_pending.has(change.key)) return;
|
|
436
447
|
state.directory_pending.set(change.key, change);
|
|
437
448
|
state.directory_version++;
|
|
438
|
-
state.directory_error = "";
|
|
439
449
|
change.apply();
|
|
440
450
|
render_rooms();
|
|
441
451
|
publish_view();
|
|
@@ -445,7 +455,7 @@ async function update_directory(change) {
|
|
|
445
455
|
return saved;
|
|
446
456
|
} catch (error) {
|
|
447
457
|
change.rollback();
|
|
448
|
-
|
|
458
|
+
notify_failure(error, change.error_message?.(error) || `${change.label}失败,已恢复原状态:${error.message}`);
|
|
449
459
|
throw error;
|
|
450
460
|
} finally {
|
|
451
461
|
state.directory_pending.delete(change.key);
|
|
@@ -538,13 +548,12 @@ async function archive_channel(id, archived) {
|
|
|
538
548
|
const change = { archived, phase: archived && room.phase && room.phase !== "concluded" ? "paused" : room.phase };
|
|
539
549
|
state.archive_changes.set(id, change);
|
|
540
550
|
state.archive_version++;
|
|
541
|
-
state.directory_error = "";
|
|
542
551
|
display_channel_archive(id, change);
|
|
543
552
|
try {
|
|
544
553
|
const saved = await request_json(`/api/rooms/${id}/${archived ? "archive" : "restore"}`, { actor: "用户" });
|
|
545
554
|
display_channel_archive(id, { archived: Boolean(saved.archived), phase: saved.phase }, change);
|
|
546
555
|
} catch (error) {
|
|
547
|
-
|
|
556
|
+
notify_failure(error, `「${room.title}」${archived ? "存档" : "恢复"}失败,已恢复原状态:${management_error_message(id, error)}`);
|
|
548
557
|
display_channel_archive(id, previous, change);
|
|
549
558
|
throw error;
|
|
550
559
|
} finally {
|
|
@@ -580,7 +589,6 @@ async function assign_channel_folder(id, folder_id) {
|
|
|
580
589
|
const previous_folder = room.folder_id ?? null;
|
|
581
590
|
if (state.folders.some(folder => folder.id === folder_id && folder.pending)) return;
|
|
582
591
|
if (folder_id && state.directory_pending.get("folder-order")?.removed_folder === folder_id) return;
|
|
583
|
-
state.directory_error = "";
|
|
584
592
|
state.folder_moves.set(id, folder_id);
|
|
585
593
|
state.folder_version++;
|
|
586
594
|
display_channel_folder(id, folder_id);
|
|
@@ -589,7 +597,7 @@ async function assign_channel_folder(id, folder_id) {
|
|
|
589
597
|
display_channel_folder(id, saved.folder_id ?? null);
|
|
590
598
|
show_toast("话题归类已更新");
|
|
591
599
|
} catch (error) {
|
|
592
|
-
|
|
600
|
+
notify_failure(error, `「${room.title}」归类失败,已恢复原状态:${error.message}`);
|
|
593
601
|
display_channel_folder(id, previous_folder);
|
|
594
602
|
throw error;
|
|
595
603
|
} finally {
|
|
@@ -886,7 +894,6 @@ async function refresh_room({ room: read_room = true } = {}) {
|
|
|
886
894
|
|
|
887
895
|
/** Select a room and reset room-specific draft and filter state. */
|
|
888
896
|
async function select_room(room_id) {
|
|
889
|
-
state.access_error = ""; state.access_notice = "";
|
|
890
897
|
state.navigation_version++;
|
|
891
898
|
state.refresh_version++;
|
|
892
899
|
state.room = state.rooms.find((room) => room.id === room_id);
|
|
@@ -898,9 +905,7 @@ async function select_room(room_id) {
|
|
|
898
905
|
state.pending_turn = null;
|
|
899
906
|
state.pending_submission = null;
|
|
900
907
|
clear_attachment_drafts();
|
|
901
|
-
state.composer_error = "";
|
|
902
908
|
state.launch.repair = null;
|
|
903
|
-
state.launch.repair_error = '';
|
|
904
909
|
state.launch.room_models = null;
|
|
905
910
|
state.messages = [];
|
|
906
911
|
state.message_history = null;
|
|
@@ -1025,7 +1030,7 @@ function set_proxy(name) { state.free_proxy = state.free_proxy === name ? "" : n
|
|
|
1025
1030
|
async function copy_online_notice(name) {
|
|
1026
1031
|
const ids = (state.room.free?.requests || []).filter(item => item.member === name && item.status === "pending").map(item => `#${item.message_id}`);
|
|
1027
1032
|
await copy_text(`请阅读 ${location.origin}/r/${state.room.id}?participant=${name} ,回应 ${ids.join("、") || "最新消息"}。单条原文:${location.origin}/r/${state.room.id}/messages/编号。请在你自己的窗口回答,不要操作网页或调用写入接口。`);
|
|
1028
|
-
|
|
1033
|
+
notify("info", `已复制给 ${name} 的阅读通知;复制不代表已送达。拿到回答后,在输入框选择“代发 ${name}”粘贴原文。`);
|
|
1029
1034
|
}
|
|
1030
1035
|
|
|
1031
1036
|
/** Start every new form from defaults: room use, delivery, text, roster, host, limits and software overrides. */
|
|
@@ -1043,7 +1048,6 @@ function reset_create_form() {
|
|
|
1043
1048
|
state.create_chain_rounds = DEFAULT_CHAIN_ROUNDS;
|
|
1044
1049
|
state.launch.profiles = JSON.parse(JSON.stringify(state.launch.saved_profiles || {}));
|
|
1045
1050
|
Object.values(state.launch.profiles).forEach(fill_api_key);
|
|
1046
|
-
state.launch.notice = "";
|
|
1047
1051
|
render_launch_settings();
|
|
1048
1052
|
publish_view();
|
|
1049
1053
|
}
|
|
@@ -1071,7 +1075,7 @@ async function return_conclusion() {
|
|
|
1071
1075
|
const summary = room.host_brief?.text || room.state_text || "(主持人尚未整理结论,请查看原文)";
|
|
1072
1076
|
await request_json(`/api/rooms/${parent.id}/messages`, { author: "用户", text: `头脑风暴「${room.title}」已收束。\n\n${summary}\n\n原文:${location.origin}/r/${room.id}`,
|
|
1073
1077
|
kind: "chat", submitted_by: "用户", submission_id: new_submission_id(), to: [] });
|
|
1074
|
-
|
|
1078
|
+
notify("info", `结论已发回「${parent.title}」`);
|
|
1075
1079
|
await select_room(parent.id);
|
|
1076
1080
|
}
|
|
1077
1081
|
|
|
@@ -1237,7 +1241,7 @@ async function send_descriptions(room_id, token, descriptions) {
|
|
|
1237
1241
|
const filled = Object.fromEntries(Object.entries(descriptions).filter(([, text]) => text?.trim()));
|
|
1238
1242
|
if (!room_id || !token || !Object.keys(filled).length) return;
|
|
1239
1243
|
try { await request_json(`/api/rooms/${room_id}/descriptions`, { descriptions: filled }, token); }
|
|
1240
|
-
catch (error) {
|
|
1244
|
+
catch (error) { notify("warning", `成员描述未保存:${error.message},可在发言顺序的成员菜单里重新填写。`); }
|
|
1241
1245
|
}
|
|
1242
1246
|
|
|
1243
1247
|
/** Save display labels by member name with the room credential; a failure leaves the members under their names. */
|
|
@@ -1245,50 +1249,47 @@ async function send_labels(room_id, token, labels) {
|
|
|
1245
1249
|
const filled = Object.fromEntries(Object.entries(labels).filter(([, text]) => text?.trim()));
|
|
1246
1250
|
if (!room_id || !token || !Object.keys(filled).length) return;
|
|
1247
1251
|
try { await request_json(`/api/rooms/${room_id}/labels`, { labels: filled }, token); }
|
|
1248
|
-
catch (error) {
|
|
1252
|
+
catch (error) { notify("warning", `成员显示名未保存:${error.message},房间里按成员名显示。`); }
|
|
1249
1253
|
}
|
|
1250
1254
|
|
|
1251
1255
|
/** Save the new room's round limit with the room credential; a failure leaves the room on the default. */
|
|
1252
1256
|
async function send_chain_rounds(room_id, token, max_rounds) {
|
|
1253
1257
|
if (!room_id || !token || max_rounds === DEFAULT_CHAIN_ROUNDS) return;
|
|
1254
1258
|
try { await request_json(`/api/rooms/${room_id}/budget`, { max_rounds }, token); }
|
|
1255
|
-
catch (error) {
|
|
1259
|
+
catch (error) { notify("warning", `推进轮数未保存:${error.message},房间沿用默认 ${DEFAULT_CHAIN_ROUNDS} 轮,可在房间页修改。`); }
|
|
1256
1260
|
}
|
|
1257
1261
|
|
|
1258
1262
|
/** Edit one member's description inside the room; the next decision by the scheduler or host uses it. */
|
|
1259
1263
|
async function describe_member(name, text) {
|
|
1260
1264
|
const room_id = state.room.id;
|
|
1261
|
-
state.free_error = "";
|
|
1262
1265
|
try {
|
|
1263
1266
|
const token = localStorage.getItem(`discuss-access:${room_id}`);
|
|
1264
1267
|
if (!token) throw new Error("此浏览器缺少房间控制凭据,请使用创建房间的浏览器");
|
|
1265
1268
|
await request_json(`/api/rooms/${room_id}/descriptions`, { descriptions: { [name]: text } }, token);
|
|
1266
1269
|
if (state.room?.id === room_id) await refresh_room();
|
|
1267
|
-
} catch (error) {
|
|
1270
|
+
} catch (error) { notify("error", error.message); throw error; }
|
|
1268
1271
|
}
|
|
1269
1272
|
|
|
1270
1273
|
/** Change how many rounds the host may run from one owner message; chains still running follow the new value. */
|
|
1271
1274
|
async function set_chain_rounds(max_rounds) {
|
|
1272
1275
|
const room_id = state.room.id;
|
|
1273
|
-
state.free_error = "";
|
|
1274
1276
|
try {
|
|
1275
1277
|
const token = localStorage.getItem(`discuss-access:${room_id}`);
|
|
1276
1278
|
if (!token) throw new Error("此浏览器缺少房间控制凭据,请使用创建房间的浏览器");
|
|
1277
1279
|
await request_json(`/api/rooms/${room_id}/budget`, { max_rounds }, token);
|
|
1278
1280
|
if (state.room?.id === room_id) await refresh_room();
|
|
1279
|
-
} catch (error) {
|
|
1281
|
+
} catch (error) { notify("error", error.message); throw error; }
|
|
1280
1282
|
}
|
|
1281
1283
|
|
|
1282
1284
|
/** Let a chain that stopped at its round limit run on for more rounds; the dispatch it held back goes out at once. */
|
|
1283
1285
|
async function continue_chain(chain_id, rounds) {
|
|
1284
1286
|
const room_id = state.room.id;
|
|
1285
|
-
state.free_error = "";
|
|
1286
1287
|
try {
|
|
1287
1288
|
const token = localStorage.getItem(`discuss-access:${room_id}`);
|
|
1288
1289
|
if (!token) throw new Error("此浏览器缺少房间控制凭据,请使用创建房间的浏览器");
|
|
1289
1290
|
await request_json(`/api/rooms/${room_id}/chains/${chain_id}`, { action: "continue", rounds }, token);
|
|
1290
1291
|
if (state.room?.id === room_id) await refresh_room();
|
|
1291
|
-
} catch (error) {
|
|
1292
|
+
} catch (error) { notify("error", error.message); throw error; }
|
|
1292
1293
|
}
|
|
1293
1294
|
|
|
1294
1295
|
/** Allow operations a woken member's software refused; the local service passes the rules from its next run. */
|
|
@@ -1319,33 +1320,30 @@ async function set_member_adapter({ name, adapter }) {
|
|
|
1319
1320
|
/** Owner changes use the website; only automatic members need the local service to reconnect. */
|
|
1320
1321
|
async function change_member({ member_id, name, action, role, latest_id }) {
|
|
1321
1322
|
const room_id = state.room.id;
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
}
|
|
1337
|
-
if (state.room?.id === room_id) await refresh_room();
|
|
1338
|
-
} catch (error) { if (state.room?.id === room_id) state.free_error = error.message; }
|
|
1323
|
+
if (action === "rejoin") {
|
|
1324
|
+
const member = (state.room.free?.members || state.room.members).find(item => item.id === member_id);
|
|
1325
|
+
const token = localStorage.getItem(`discuss-access:${room_id}`);
|
|
1326
|
+
const record = prepare_member_request(room_id, `rejoin:${member_id}`, { name, access: member.access });
|
|
1327
|
+
const result = member.access === "runner"
|
|
1328
|
+
? await local_launch_request("room-member", { room_id, token, action: "rejoin", name, member_token: record.member.token })
|
|
1329
|
+
: await request_json(`/api/rooms/${room_id}/members/${member_id}`, { action: "active", token: record.member.token });
|
|
1330
|
+
finish_member_request(room_id, token, record, result);
|
|
1331
|
+
if (state.room?.id === room_id) notify("info", member.access === "manual" ? `${name} 已请回,凭证已更新;请用新的启动指令让它重新注册。` : `${name} 已请回,续接原会话。`);
|
|
1332
|
+
} else {
|
|
1333
|
+
await request_json(`/api/rooms/${room_id}/members/${member_id}${action === "interrupt" ? "/interrupt" : ""}`,
|
|
1334
|
+
action === "interrupt" ? {} : { action, ...(role ? { role } : {}), ...(latest_id !== undefined ? { latest_id } : {}) });
|
|
1335
|
+
}
|
|
1336
|
+
if (state.room?.id === room_id) await refresh_room();
|
|
1339
1337
|
}
|
|
1340
1338
|
|
|
1341
1339
|
/** Reorder who has not spoken yet against the revision the board showed; a changed order is refused and the board reloads. */
|
|
1342
1340
|
async function reorder_speakers({ revision, order }) {
|
|
1343
1341
|
const room_id = state.room.id;
|
|
1344
|
-
state.order_error = null;
|
|
1345
1342
|
try { await request_json(`/api/rooms/${room_id}/speaking-order`, { revision, order }); }
|
|
1346
1343
|
catch (error) {
|
|
1347
1344
|
// A conflict means someone else's change landed first; the board reloads below, so ask for the move again (same text as the board's own notice).
|
|
1348
|
-
|
|
1345
|
+
if (error.status === 409) notify("warning", "发言顺序刚有变化,已更新为最新顺序,请重新调整");
|
|
1346
|
+
else notify("error", error.message);
|
|
1349
1347
|
}
|
|
1350
1348
|
if (state.room?.id === room_id) await refresh_room();
|
|
1351
1349
|
}
|
|
@@ -1353,41 +1351,39 @@ async function reorder_speakers({ revision, order }) {
|
|
|
1353
1351
|
/** Invite an independent member, retaining its credential before either delivery path runs. */
|
|
1354
1352
|
async function invite_member({ name, access, project_access = "read", session_id = "" }) {
|
|
1355
1353
|
const room_id = state.room.id;
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
:
|
|
1385
|
-
:
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
}
|
|
1390
|
-
} catch (error) { if (state.room?.id === room_id) state.free_error = error.message; }
|
|
1354
|
+
const token = localStorage.getItem(`discuss-access:${room_id}`);
|
|
1355
|
+
if (!token) throw new Error("此浏览器缺少房间控制凭据,请使用创建房间的浏览器");
|
|
1356
|
+
const taken = new Set((state.room.free?.members || state.room.members || []).map(member => member.name));
|
|
1357
|
+
const instance = unique_instance(name, taken);
|
|
1358
|
+
const web_member = access === "online";
|
|
1359
|
+
const record = prepare_member_request(room_id, `add:${name}:${access}`, { name: instance, access: web_member ? "manual" : access,
|
|
1360
|
+
...(web_member ? { manual_source: "web" } : {}), adapter: "", session_mode: "new" }, { project_access, session_id: session_id.trim() });
|
|
1361
|
+
let result;
|
|
1362
|
+
if (access === "runner") {
|
|
1363
|
+
const draft = launch_options();
|
|
1364
|
+
const participants = { ...draft.participants, [record.member.name]: { ...draft.participants?.[name],
|
|
1365
|
+
...draft.participants?.[record.member.name], project_access: record.options?.project_access || "read",
|
|
1366
|
+
// An existing conversation joins with its own context; the runner resumes it instead of starting a new one.
|
|
1367
|
+
...(record.options?.session_id ? { session_id: record.options.session_id } : {}) } };
|
|
1368
|
+
if (participants[record.member.name].adapter === "api") participants[record.member.name].project_access = "read";
|
|
1369
|
+
result = await local_launch_request("room-member", { room_id, token, action: "add", name: record.member.name, access,
|
|
1370
|
+
member_token: record.member.token, participants, cwd: draft.cwd });
|
|
1371
|
+
} else result = await request_json(`/api/rooms/${room_id}/members`, record.member);
|
|
1372
|
+
finish_member_request(room_id, token, record, result);
|
|
1373
|
+
await send_descriptions(room_id, token, { [record.member.name]: description_draft(name) });
|
|
1374
|
+
await send_labels(room_id, token, { [record.member.name]: label_drafts[name] });
|
|
1375
|
+
delete description_drafts[name];
|
|
1376
|
+
delete label_drafts[name];
|
|
1377
|
+
if (state.room?.id === room_id) {
|
|
1378
|
+
const notice = web_member ? `${record.member.name} 已加入,阅读通知与真实回答由你或授权主持人转交。`
|
|
1379
|
+
// A manual member's start step lives on the board row, the composer bar and the invite dialog; no extra notice.
|
|
1380
|
+
: access === "manual" ? ""
|
|
1381
|
+
: record.options?.session_id ? `${record.member.name} 已接入原会话;它工作时请不要在原窗口继续操作。`
|
|
1382
|
+
: state.room.mode === "free" ? `${record.member.name} 已加入,之后点名它即会由本机服务唤醒。`
|
|
1383
|
+
: `${record.member.name} 已加入讨论,主持人会重新安排发言。`;
|
|
1384
|
+
if (notice) notify("info", notice);
|
|
1385
|
+
await refresh_room();
|
|
1386
|
+
}
|
|
1391
1387
|
}
|
|
1392
1388
|
|
|
1393
1389
|
/** Clear only the composer that produced the saved message. */
|
|
@@ -1415,10 +1411,8 @@ async function send_message(event) {
|
|
|
1415
1411
|
event.preventDefault();
|
|
1416
1412
|
if (!state.room || state.sending_message) return;
|
|
1417
1413
|
const room_id = state.room.id, drafts = state.attachment_drafts;
|
|
1418
|
-
|
|
1419
|
-
let
|
|
1420
|
-
try { payload = message_payload(); }
|
|
1421
|
-
catch (error) { state.composer_error = error.message; render_attachment_drafts(); publish_view(); throw error; }
|
|
1414
|
+
const payload = message_payload();
|
|
1415
|
+
let saved = false;
|
|
1422
1416
|
if (!payload.text && !drafts.length) return;
|
|
1423
1417
|
const signature = JSON.stringify({ room: room_id, ...payload, files: drafts.map(file => file.id) });
|
|
1424
1418
|
if (state.pending_submission?.signature !== signature) state.pending_submission = { signature, id: new_submission_id() };
|
|
@@ -1433,9 +1427,9 @@ async function send_message(event) {
|
|
|
1433
1427
|
saved = true;
|
|
1434
1428
|
await finish_message(result, room_id, drafts);
|
|
1435
1429
|
} catch (error) {
|
|
1436
|
-
if (state.room?.id === room_id)
|
|
1430
|
+
if (state.room?.id === room_id) notify_failure(error, saved
|
|
1437
1431
|
? `留言已保存,但页面刷新失败:${error.message}`
|
|
1438
|
-
: `${error.message}
|
|
1432
|
+
: `${error.message};草稿已保留,可重试发送`);
|
|
1439
1433
|
throw error;
|
|
1440
1434
|
} finally {
|
|
1441
1435
|
state.sending_message = false;
|
|
@@ -1466,9 +1460,8 @@ function attachment_file_icon(file_name) {
|
|
|
1466
1460
|
function add_attachments(files) {
|
|
1467
1461
|
if (!state.room || state.sending_message) return;
|
|
1468
1462
|
const selected = Array.from(files);
|
|
1469
|
-
state.
|
|
1470
|
-
if (
|
|
1471
|
-
else if (selected.some(file => file.size > MAX_ATTACHMENT_BYTES)) state.composer_error = "单个附件最多 20 MiB";
|
|
1463
|
+
if (state.attachment_drafts.length + selected.length > MAX_MESSAGE_ATTACHMENTS) notify("warning", "每条消息最多 10 个附件");
|
|
1464
|
+
else if (selected.some(file => file.size > MAX_ATTACHMENT_BYTES)) notify("warning", "单个附件最多 20 MiB");
|
|
1472
1465
|
else state.attachment_drafts.push(...selected.map(file => ({ id: new_submission_id(), file,
|
|
1473
1466
|
preview_url: /^image\/(png|jpeg|gif|webp)$/.test(file.type) ? URL.createObjectURL(file) : "", uploaded: null, actor: null })));
|
|
1474
1467
|
render_attachment_drafts();
|
|
@@ -1488,19 +1481,18 @@ function remove_attachment(id) {
|
|
|
1488
1481
|
const file = state.attachment_drafts.find(item => item.id === id);
|
|
1489
1482
|
if (file?.preview_url) URL.revokeObjectURL(file.preview_url);
|
|
1490
1483
|
state.attachment_drafts = state.attachment_drafts.filter(item => item.id !== id);
|
|
1491
|
-
state.composer_error = "";
|
|
1492
1484
|
render_attachment_drafts();
|
|
1493
1485
|
publish_view();
|
|
1494
1486
|
}
|
|
1495
1487
|
|
|
1496
|
-
/** Display shared file drafts and
|
|
1488
|
+
/** Display shared file drafts and upload progress in the classic theme. */
|
|
1497
1489
|
function render_attachment_drafts() {
|
|
1498
1490
|
const container = element("attachment-drafts");
|
|
1499
1491
|
if (!container) return;
|
|
1500
1492
|
container.innerHTML = state.attachment_drafts.map(item => `<div class="attachment-draft">${item.preview_url ? `<img src="${item.preview_url}" alt="">` : ""}
|
|
1501
1493
|
<span>${escape_html(item.file.name)}<small>${attachment_size(item.file.size)}</small></span>
|
|
1502
1494
|
<button type="button" data-remove-attachment="${item.id}" aria-label="移除 ${escape_html(item.file.name)}" ${state.sending_message ? "disabled" : ""}>×</button></div>`).join("");
|
|
1503
|
-
element("attachment-status").textContent = state.
|
|
1495
|
+
element("attachment-status").textContent = state.upload_status;
|
|
1504
1496
|
element("choose-attachments").disabled = state.sending_message;
|
|
1505
1497
|
}
|
|
1506
1498
|
|
|
@@ -1780,7 +1772,7 @@ function access_snapshot() {
|
|
|
1780
1772
|
name: member.name, access: is_web_member(member) ? "online" : member.access, available: member.status === "active" && !is_web_member(member)
|
|
1781
1773
|
&& Boolean(saved[member.name]?.token) && is_web_member(saved[member.name]) === is_web_member(member)
|
|
1782
1774
|
&& (!saved[member.name].id || saved[member.name].id === member.id),
|
|
1783
|
-
}))
|
|
1775
|
+
})) };
|
|
1784
1776
|
}
|
|
1785
1777
|
|
|
1786
1778
|
/** Generate a separate random credential for each declared participant instance; `cli` members are woken locally. */
|
|
@@ -1835,27 +1827,20 @@ async function submit_room(event, folder_id = null) {
|
|
|
1835
1827
|
const signature = signed(payload, launch_draft);
|
|
1836
1828
|
const navigation = state.navigation_version;
|
|
1837
1829
|
if (!launching) element("room-dialog").close();
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
const experts = Object.fromEntries(Object.entries(payload.experts || {}).map(([name, mode]) => [name, mode === "manual" ? "cli" : mode]));
|
|
1846
|
-
const input = { ...(launch_draft || {}), room: { ...payload, ...(payload.experts ? { experts } : {}), members: pending.members, submission_id: pending.id }, token };
|
|
1847
|
-
room = await update_directory({ key: "create-room", label: `「${payload.title}」创建`,
|
|
1830
|
+
const pending = prepare_room_request(signature);
|
|
1831
|
+
const token = pending.token;
|
|
1832
|
+
pending.members ||= creation_members(payload.experts);
|
|
1833
|
+
// The website tracks capabilities (cli/online); a manual expert is a local reader and writer whose delivery lives in its member record.
|
|
1834
|
+
const experts = Object.fromEntries(Object.entries(payload.experts || {}).map(([name, mode]) => [name, mode === "manual" ? "cli" : mode]));
|
|
1835
|
+
const input = { ...(launch_draft || {}), room: { ...payload, ...(payload.experts ? { experts } : {}), members: pending.members, submission_id: pending.id }, token };
|
|
1836
|
+
const room = await update_directory({ key: "create-room", label: `「${payload.title}」创建`,
|
|
1848
1837
|
error_message: launching ? error => `「${payload.title}」${error.message}` : null,
|
|
1849
1838
|
apply: () => { state.creating_room = payload; }, rollback: () => { state.creating_room = null; },
|
|
1850
1839
|
save: async () => (await submit_local_launch(input, payload.title, !launching, signature)).room,
|
|
1851
1840
|
commit: saved => {
|
|
1852
1841
|
state.creating_room = null;
|
|
1853
1842
|
if (!state.rooms.some(existing => existing.id === saved.id)) state.rooms.push(saved);
|
|
1854
|
-
} });
|
|
1855
|
-
catch (error) {
|
|
1856
|
-
if (launching) { state.launch.error = error.message; render_launch_settings(); publish_view(); }
|
|
1857
|
-
throw error;
|
|
1858
|
-
}
|
|
1843
|
+
} });
|
|
1859
1844
|
state.pending_room = null;
|
|
1860
1845
|
state.create_parent = null;
|
|
1861
1846
|
if (discussion) await open_discussion_after_creation(room.id, discussion);
|
|
@@ -1878,7 +1863,7 @@ async function open_discussion_after_creation(room_id, { goal, named_followup, l
|
|
|
1878
1863
|
await request_json(`${path}/discussion`, { action: "start", revision: current.revision,
|
|
1879
1864
|
...(goal ? { goal } : {}), limits, named_followup });
|
|
1880
1865
|
} catch (error) {
|
|
1881
|
-
|
|
1866
|
+
notify("warning", `房间已创建,但开启讨论失败:${error.message};可在右侧成员栏重试`);
|
|
1882
1867
|
}
|
|
1883
1868
|
}
|
|
1884
1869
|
|
|
@@ -2001,7 +1986,6 @@ async function resume_launch(id) {
|
|
|
2001
1986
|
const record = stored_launches().find(item => item.id === id);
|
|
2002
1987
|
if (!record || state.directory_pending.has(`launch-${id}`)) return;
|
|
2003
1988
|
state.directory_pending.set(`launch-${id}`, true);
|
|
2004
|
-
state.launch.error = "";
|
|
2005
1989
|
publish_view();
|
|
2006
1990
|
try {
|
|
2007
1991
|
const result = await submit_local_launch(record.input, record.title, record.direct, record.signature);
|
|
@@ -2014,11 +1998,10 @@ async function resume_launch(id) {
|
|
|
2014
1998
|
element("room-dialog").close();
|
|
2015
1999
|
}
|
|
2016
2000
|
if (!record.direct) clear_launch_overrides(record.input);
|
|
2017
|
-
|
|
2018
|
-
state.directory_error = "";
|
|
2001
|
+
notify("info", result.status === "running" ? "已核对原启动,实际执行请查看进度回执" : "已核对原房间状态;不会自动恢复已暂停或结束的讨论");
|
|
2019
2002
|
if (state.pending_room?.id === id) state.pending_room = null;
|
|
2020
2003
|
await select_room(result.room.id);
|
|
2021
|
-
} catch (error) {
|
|
2004
|
+
} catch (error) { notify("error", error.message); }
|
|
2022
2005
|
finally { state.directory_pending.delete(`launch-${id}`); render_launch_settings(); publish_view(); }
|
|
2023
2006
|
}
|
|
2024
2007
|
|
|
@@ -2026,7 +2009,6 @@ async function resume_launch(id) {
|
|
|
2026
2009
|
function discard_launch(id) {
|
|
2027
2010
|
localStorage.removeItem(LAUNCH_PENDING_PREFIX + id);
|
|
2028
2011
|
if (state.pending_room?.id === id) state.pending_room = null;
|
|
2029
|
-
state.launch.error = "";
|
|
2030
2012
|
refresh_launch_recoveries();
|
|
2031
2013
|
}
|
|
2032
2014
|
|
|
@@ -2327,7 +2309,6 @@ function write_launch_field(name, field, value) {
|
|
|
2327
2309
|
if (field === "base_url") delete state.launch.profiles[name].api_key_ref;
|
|
2328
2310
|
fill_api_key(state.launch.profiles[name]);
|
|
2329
2311
|
}
|
|
2330
|
-
state.launch.notice = "";
|
|
2331
2312
|
if (field === "adapter") render_launch_settings();
|
|
2332
2313
|
else render_launch_summaries();
|
|
2333
2314
|
publish_view();
|
|
@@ -2499,8 +2480,7 @@ async function pick_launch_directory() {
|
|
|
2499
2480
|
try {
|
|
2500
2481
|
const path = await browse_local_directory();
|
|
2501
2482
|
if (path) write_launch_field("workspace", "cwd", path);
|
|
2502
|
-
|
|
2503
|
-
} catch (error) { state.launch.error = error.message; }
|
|
2483
|
+
} catch (error) { notify("error", error.message); }
|
|
2504
2484
|
publish_view();
|
|
2505
2485
|
}
|
|
2506
2486
|
|
|
@@ -2521,9 +2501,8 @@ async function save_launch_defaults() {
|
|
|
2521
2501
|
try {
|
|
2522
2502
|
await local_launch_request("settings", { participants });
|
|
2523
2503
|
state.launch.saved_profiles = { ...state.launch.saved_profiles, ...JSON.parse(JSON.stringify(participants)) };
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
} catch (error) { state.launch.error = error.message; }
|
|
2504
|
+
notify("info", "已保存本机软件配置,后续讨论可复用");
|
|
2505
|
+
} catch (error) { notify("error", error.message); }
|
|
2527
2506
|
render_launch_settings();
|
|
2528
2507
|
publish_view();
|
|
2529
2508
|
}
|
|
@@ -2538,10 +2517,9 @@ async function launch_current_room() {
|
|
|
2538
2517
|
const draft = launch_options();
|
|
2539
2518
|
const result = await submit_local_launch({ ...draft, room_id, token }, state.room.title || "已有讨论");
|
|
2540
2519
|
clear_launch_overrides(draft);
|
|
2541
|
-
|
|
2542
|
-
state.launch.error = "";
|
|
2520
|
+
notify("info", result.status === "running" ? "启动已提交,实际执行请查看进度回执" : "本次启动已有记录,请查看房间状态");
|
|
2543
2521
|
if (state.room?.id === room_id) await refresh_room();
|
|
2544
|
-
} catch (error) {
|
|
2522
|
+
} catch (error) { notify("error", error.message); }
|
|
2545
2523
|
render_launch_settings();
|
|
2546
2524
|
publish_view();
|
|
2547
2525
|
}
|
|
@@ -2549,13 +2527,12 @@ async function launch_current_room() {
|
|
|
2549
2527
|
/** Load the selected paused room's original connection paths after proving browser ownership. */
|
|
2550
2528
|
async function load_room_configuration() {
|
|
2551
2529
|
const room_id = state.room.id;
|
|
2552
|
-
state.launch.repair_error = '';
|
|
2553
2530
|
try {
|
|
2554
2531
|
const token = localStorage.getItem(`discuss-access:${room_id}`);
|
|
2555
2532
|
if (!token) throw new Error('此浏览器缺少房间凭据,请使用原浏览器或原接入配置');
|
|
2556
2533
|
const result = await local_launch_request('room-config', { room_id, token });
|
|
2557
|
-
if (state.room?.id === room_id) state.launch.repair =
|
|
2558
|
-
} catch (error) { if (state.room?.id === room_id)
|
|
2534
|
+
if (state.room?.id === room_id) state.launch.repair = result;
|
|
2535
|
+
} catch (error) { if (state.room?.id === room_id) notify("error", error.message); }
|
|
2559
2536
|
render_room_configuration();
|
|
2560
2537
|
publish_view();
|
|
2561
2538
|
}
|
|
@@ -2566,7 +2543,6 @@ function write_room_connection(name, value) {
|
|
|
2566
2543
|
if (repair?.room_id !== state.room?.id || !repair.participants[name]) return;
|
|
2567
2544
|
const profile = repair.participants[name];
|
|
2568
2545
|
profile[profile.adapter === 'antigravity' ? 'ipc_directory' : 'executable'] = value;
|
|
2569
|
-
repair.notice = '';
|
|
2570
2546
|
publish_view();
|
|
2571
2547
|
}
|
|
2572
2548
|
|
|
@@ -2577,7 +2553,6 @@ function write_room_adapter(name, value) {
|
|
|
2577
2553
|
if (repair?.room_id !== state.room?.id || !['codex', 'codex_cli'].includes(profile?.adapter)
|
|
2578
2554
|
|| !['codex', 'codex_cli'].includes(value)) return;
|
|
2579
2555
|
profile.adapter = value;
|
|
2580
|
-
repair.notice = '';
|
|
2581
2556
|
publish_view();
|
|
2582
2557
|
}
|
|
2583
2558
|
|
|
@@ -2589,18 +2564,18 @@ async function save_room_configuration() {
|
|
|
2589
2564
|
const field = profile.adapter === 'antigravity' ? 'ipc_directory' : 'executable';
|
|
2590
2565
|
return [name, { [field]: profile[field], ...(['codex', 'codex_cli'].includes(profile.adapter) ? { adapter: profile.adapter } : {}) }];
|
|
2591
2566
|
}));
|
|
2592
|
-
state.launch.repair_error = '';
|
|
2593
2567
|
try {
|
|
2594
2568
|
const token = localStorage.getItem(`discuss-access:${repair.room_id}`);
|
|
2595
2569
|
if (!token) throw new Error('此浏览器缺少房间凭据,请重新核对');
|
|
2596
2570
|
const result = await local_launch_request('room-config', { room_id: repair.room_id, token,
|
|
2597
2571
|
version: repair.version, runtime_revision: repair.runtime_revision, participants });
|
|
2598
2572
|
if (state.room?.id === repair.room_id) {
|
|
2599
|
-
state.launch.repair =
|
|
2600
|
-
|
|
2573
|
+
state.launch.repair = result;
|
|
2574
|
+
notify('info', result.applied ? '本场接入设置已保存,服务已采用;可在进度看板重启并核对实际发言。'
|
|
2575
|
+
: '本场接入设置已保存;服务尚未确认采用,请先核对本机服务再重启。');
|
|
2601
2576
|
await refresh_room();
|
|
2602
2577
|
}
|
|
2603
|
-
} catch (error) { if (state.room?.id === repair.room_id)
|
|
2578
|
+
} catch (error) { if (state.room?.id === repair.room_id) notify("error", error.message); }
|
|
2604
2579
|
render_room_configuration();
|
|
2605
2580
|
publish_view();
|
|
2606
2581
|
}
|
|
@@ -2623,7 +2598,6 @@ function render_room_configuration() {
|
|
|
2623
2598
|
fields.dataset.roomId = state.room?.id || '';
|
|
2624
2599
|
}
|
|
2625
2600
|
element('local-room-configuration-save').hidden = !repair;
|
|
2626
|
-
element('local-room-configuration-notice').textContent = state.launch.repair_error || repair?.notice || '';
|
|
2627
2601
|
}
|
|
2628
2602
|
|
|
2629
2603
|
/** Render the same local setup controls in the classic theme. */
|
|
@@ -2661,7 +2635,7 @@ function render_launch_settings() {
|
|
|
2661
2635
|
+ `<label>本次工作目录(可选)<input data-launch-name="workspace" data-launch-field="cwd" value="${escape_html(state.launch.workspace.cwd || "")}"></label><label>本次 AI 项目(可选)<input data-launch-name="workspace" data-launch-field="discussion_project" value="${escape_html(state.launch.workspace.discussion_project || "")}"></label>`;
|
|
2662
2636
|
}
|
|
2663
2637
|
for (const id of ["local-launch-notice", "new-local-launch-notice"]) {
|
|
2664
|
-
if (element(id)) element(id).textContent = [state.launch.error
|
|
2638
|
+
if (element(id)) element(id).textContent = [state.launch.error,
|
|
2665
2639
|
!state.launch.available && state.launch.connect_command ? `在这台电脑运行 ${state.launch.connect_command},使用命令打开的网页完成连接。` : ""].filter(Boolean).join(" ");
|
|
2666
2640
|
}
|
|
2667
2641
|
for (const id of ["local-launch-save", "new-local-launch-save"]) if (element(id)) element(id).disabled = !state.launch.available;
|
|
@@ -2834,8 +2808,7 @@ function download_member_access(name) {
|
|
|
2834
2808
|
const member = access.members.find(item => item.name === name);
|
|
2835
2809
|
if (!access.can_manage || !member?.available || member.access === "online") throw new Error("当前浏览器没有此成员可下载的私有配置");
|
|
2836
2810
|
download_private_json({ site: location.origin, room: room.id, token: member_credentials(room.id)[name].token }, `member-${room.id}-${name}.json`);
|
|
2837
|
-
|
|
2838
|
-
flash_access_notice(`已下载 ${name} 的私有配置`);
|
|
2811
|
+
notify("info", `已下载 ${name} 的私有配置`);
|
|
2839
2812
|
}
|
|
2840
2813
|
|
|
2841
2814
|
/** Copy the same room-scoped credential as one line of text for an agent that cannot take a file. */
|
|
@@ -2844,19 +2817,7 @@ async function copy_member_access_text(name) {
|
|
|
2844
2817
|
const member = access.members.find(item => item.name === name);
|
|
2845
2818
|
if (!access.can_manage || !member?.available || member.access === "online") throw new Error("当前浏览器没有此成员可复制的凭证");
|
|
2846
2819
|
await copy_text(JSON.stringify({ site: location.origin, room: room.id, token: member_credentials(room.id)[name].token }));
|
|
2847
|
-
|
|
2848
|
-
}
|
|
2849
|
-
|
|
2850
|
-
/** Confirm a copy or download briefly in the member rail; the note clears itself. */
|
|
2851
|
-
function flash_access_notice(message) {
|
|
2852
|
-
state.access_error = "";
|
|
2853
|
-
state.access_notice = message;
|
|
2854
|
-
clearTimeout(state.access_notice_timer);
|
|
2855
|
-
state.access_notice_timer = setTimeout(() => {
|
|
2856
|
-
if (state.access_notice !== message) return;
|
|
2857
|
-
state.access_notice = "";
|
|
2858
|
-
publish_view();
|
|
2859
|
-
}, TOAST_DURATION_MS);
|
|
2820
|
+
notify("info", `已复制 ${name} 的凭证文本(含 token)`);
|
|
2860
2821
|
}
|
|
2861
2822
|
|
|
2862
2823
|
/** Copy a member-specific entry instruction; only the launch variant carries the credential, never conversation context. */
|
|
@@ -2879,7 +2840,7 @@ async function copy_member_join(name, with_credential = false) {
|
|
|
2879
2840
|
${instruction}
|
|
2880
2841
|
房间入口:${address}
|
|
2881
2842
|
先核对最新阶段、任务和原文编号,再按房间协议参与。${credential}`);
|
|
2882
|
-
if (state.room?.id === room.id)
|
|
2843
|
+
if (state.room?.id === room.id) notify("info", with_credential ? `已复制 ${name} 的启动指令(含 token)` : `已复制 ${name} 的参与说明`);
|
|
2883
2844
|
}
|
|
2884
2845
|
|
|
2885
2846
|
/** How a manual member learns it is its turn: free rooms are pulled by the member, discussions only on the host's assignment. */
|
|
@@ -3119,16 +3080,16 @@ async function fork_room(message_id) {
|
|
|
3119
3080
|
const registered = new Set((local?.members || []).filter(member => member.status !== "failed").map(member => member.name));
|
|
3120
3081
|
for (const member of (state.room.free?.members || []).filter(item => item.status === "away")) {
|
|
3121
3082
|
if (member.access === "runner" && !registered.has(member.name)) continue;
|
|
3122
|
-
await change_member({ member_id: member.id, name: member.name, action: "rejoin" });
|
|
3123
|
-
|
|
3083
|
+
try { await change_member({ member_id: member.id, name: member.name, action: "rejoin" }); }
|
|
3084
|
+
catch (error) { problems.push(`${member.name} 未能请回:${error.message}`); }
|
|
3124
3085
|
}
|
|
3125
3086
|
const fresh = (local?.members || []).filter(member => member.status === "fresh").map(member => member.name);
|
|
3126
3087
|
const failed = (local?.members || []).filter(member => member.status === "failed").map(member => `${member.name}(${member.error})`);
|
|
3127
3088
|
if (failed.length) problems.push(`未分叉、保持离开:${failed.join("、")}`);
|
|
3128
3089
|
const done = `已从 #${message_id} 分支出新房间,原房间不变`;
|
|
3129
|
-
//
|
|
3130
|
-
|
|
3131
|
-
|
|
3090
|
+
// A partial fork says both the result and the problems in one warning instead of a separate notice.
|
|
3091
|
+
if (problems.length) notify("warning", `${done},但${problems.join(";")}`);
|
|
3092
|
+
else notify("info", `${done}${fresh.length ? `;${fresh.join("、")} 将以新会话开始` : ""}。`);
|
|
3132
3093
|
publish_view();
|
|
3133
3094
|
}
|
|
3134
3095
|
|
|
@@ -3614,7 +3575,11 @@ async function flush_live_refresh() {
|
|
|
3614
3575
|
const full = live_updates.full;
|
|
3615
3576
|
live_updates.full = false;
|
|
3616
3577
|
live_updates.running = true;
|
|
3617
|
-
try { await refresh_room({ room: full }); } catch
|
|
3578
|
+
try { await refresh_room({ room: full }); } catch (error) {
|
|
3579
|
+
console.warn("refresh failed", error);
|
|
3580
|
+
if (state.reconnecting) show_toast(`仍未连接:${error.message}`, true);
|
|
3581
|
+
set_connection(false);
|
|
3582
|
+
}
|
|
3618
3583
|
finally {
|
|
3619
3584
|
live_updates.running = false;
|
|
3620
3585
|
if (live_updates.dirty) queue_live_refresh();
|
|
@@ -3628,7 +3593,7 @@ function arm_update_poll() {
|
|
|
3628
3593
|
live_updates.poll_timer = setTimeout(() => {
|
|
3629
3594
|
poll_live_updates();
|
|
3630
3595
|
arm_update_poll();
|
|
3631
|
-
}, live_updates.connected ? REFRESH_INTERVAL_MS : FALLBACK_REFRESH_INTERVAL_MS);
|
|
3596
|
+
}, live_updates.connected && state.connected ? REFRESH_INTERVAL_MS : FALLBACK_REFRESH_INTERVAL_MS);
|
|
3632
3597
|
}
|
|
3633
3598
|
|
|
3634
3599
|
/**
|
|
@@ -3637,9 +3602,10 @@ function arm_update_poll() {
|
|
|
3637
3602
|
* A site without revisions is read in full as before.
|
|
3638
3603
|
*/
|
|
3639
3604
|
async function poll_live_updates() {
|
|
3605
|
+
if (!live_updates.source && typeof EventSource !== "undefined") return reconnect_live_updates();
|
|
3640
3606
|
const room_id = state.room?.id || "";
|
|
3641
3607
|
const working = (state.room?.free?.members || []).some((member) => WORKING_STATUSES.includes(member.execution?.status));
|
|
3642
|
-
if (working || Date.now() - live_updates.refreshed_at >= SAFETY_REFRESH_MS) return queue_live_refresh();
|
|
3608
|
+
if (!state.connected || working || Date.now() - live_updates.refreshed_at >= SAFETY_REFRESH_MS) return queue_live_refresh();
|
|
3643
3609
|
let revisions;
|
|
3644
3610
|
try { revisions = await request_json(`/api/revisions?rooms=${encodeURIComponent(room_id)}`); }
|
|
3645
3611
|
catch { return queue_live_refresh(); }
|
|
@@ -3669,6 +3635,8 @@ function start_live_updates() {
|
|
|
3669
3635
|
if (live_updates.source !== source) return;
|
|
3670
3636
|
const was_connected = live_updates.connected;
|
|
3671
3637
|
live_updates.connected = false;
|
|
3638
|
+
// A closed stream is never retried by the browser; the next poll opens a new one.
|
|
3639
|
+
if (source.readyState === EventSource.CLOSED) live_updates.source = null;
|
|
3672
3640
|
if (was_connected) arm_update_poll();
|
|
3673
3641
|
});
|
|
3674
3642
|
}
|
|
@@ -3676,6 +3644,17 @@ function start_live_updates() {
|
|
|
3676
3644
|
queue_live_refresh();
|
|
3677
3645
|
}
|
|
3678
3646
|
|
|
3647
|
+
/** Reopen the update stream and read the site now; used by the footer retry and when the stream was closed. */
|
|
3648
|
+
async function reconnect_live_updates() {
|
|
3649
|
+
if (document.hidden) return;
|
|
3650
|
+
state.reconnecting = true;
|
|
3651
|
+
publish_view();
|
|
3652
|
+
live_updates.source?.close();
|
|
3653
|
+
live_updates.source = null;
|
|
3654
|
+
live_updates.connected = false;
|
|
3655
|
+
start_live_updates();
|
|
3656
|
+
}
|
|
3657
|
+
|
|
3679
3658
|
/** Release streams and scheduled reads while hidden or leaving the page. */
|
|
3680
3659
|
function stop_live_updates() {
|
|
3681
3660
|
live_updates.enabled = false;
|
|
@@ -3749,13 +3728,13 @@ function view_snapshot() {
|
|
|
3749
3728
|
const experts = Object.fromEntries(["new-expert-modes", "topic-expert-modes"].map((id) =>
|
|
3750
3729
|
[id, element(id) ? read_expert_inputs(id) : {}]));
|
|
3751
3730
|
return { rooms: ordered_rooms(), folders: state.folders, folder_groups: ordered_folders(), room: state.room, archive_view: state.archive_view, directory_sort: state.directory_sort, messages: state.messages, total: state.message_total,
|
|
3752
|
-
archive_pending: [...state.archive_changes.keys()],
|
|
3731
|
+
archive_pending: [...state.archive_changes.keys()], import_progress: structuredClone(state.import_progress),
|
|
3753
3732
|
directory_pending: [...state.directory_pending.keys()], folder_pending: [...state.folder_moves.keys()], creating_room: state.creating_room,
|
|
3754
|
-
connected: state.connected, backup_pending: Boolean(state.backup_pending), page: state.page, pages: state.pages, query: state.query,
|
|
3733
|
+
connected: state.connected, reconnecting: state.reconnecting, backup_pending: Boolean(state.backup_pending), page: state.page, pages: state.pages, query: state.query,
|
|
3755
3734
|
attachment_drafts: state.attachment_drafts.map(item => ({ id: item.id, file_name: item.file.name, size: item.file.size, preview_url: item.preview_url })),
|
|
3756
|
-
|
|
3735
|
+
upload_status: state.upload_status, sending_message: state.sending_message,
|
|
3757
3736
|
mention_only: state.mention_only, reply_to: state.reply_to, turn_reply_to: state.turn_reply_to,
|
|
3758
|
-
|
|
3737
|
+
free_proxy: state.free_proxy,
|
|
3759
3738
|
create_parent: state.create_parent, access: access_snapshot(), admin: admin_snapshot(),
|
|
3760
3739
|
tab: state.tab, snapshot_id: state.snapshot_id, context_dirty: state.context_dirty,
|
|
3761
3740
|
theme: document.documentElement.dataset.theme, fields, experts, launch: structuredClone(state.launch), named_followup: state.named_followup, create_discussion: state.create_discussion, create_chain_rounds: state.create_chain_rounds };
|
|
@@ -3851,7 +3830,6 @@ async function backup_room(room_id = state.room?.id) {
|
|
|
3851
3830
|
async function backup_rooms({ ids, name }) {
|
|
3852
3831
|
if (!ids.length || state.backup_pending) return;
|
|
3853
3832
|
state.backup_pending = true;
|
|
3854
|
-
state.directory_notice = state.directory_error = "";
|
|
3855
3833
|
publish_view();
|
|
3856
3834
|
try {
|
|
3857
3835
|
const snapshots = [];
|
|
@@ -3861,9 +3839,9 @@ async function backup_rooms({ ids, name }) {
|
|
|
3861
3839
|
snapshots.push({ ...snapshot, source_url: `${location.origin}/r/${encodeURIComponent(id)}` });
|
|
3862
3840
|
}
|
|
3863
3841
|
await window.discuss_backup.download_rooms_backup(snapshots, name);
|
|
3864
|
-
|
|
3842
|
+
notify("info", `已生成 ${snapshots.length} 个频道的备份,请查看浏览器下载。`);
|
|
3865
3843
|
} catch (error) {
|
|
3866
|
-
|
|
3844
|
+
notify("error", `导出失败,未生成备份:${error.message}`);
|
|
3867
3845
|
} finally {
|
|
3868
3846
|
state.backup_pending = false;
|
|
3869
3847
|
publish_view();
|
|
@@ -3975,14 +3953,11 @@ async function execute_view_action(name, value) {
|
|
|
3975
3953
|
prepare_brainstorm, return_conclusion, adopt_recommendation, rate_decision,
|
|
3976
3954
|
busy: () => { throw new Error("上一个操作仍在处理中,请稍候;长时间无响应请刷新页面"); },
|
|
3977
3955
|
copy_reading: () => copy_text(`${location.origin}/r/${state.room.id}`), copy: copy_text };
|
|
3978
|
-
if (!["refresh", "busy"].includes(name)) state.free_error = "";
|
|
3979
3956
|
try {
|
|
3980
3957
|
await actions[name](value);
|
|
3981
3958
|
return true;
|
|
3982
3959
|
} catch (error) {
|
|
3983
|
-
if (
|
|
3984
|
-
else if (!state.free_error) state.free_error = error.message;
|
|
3985
|
-
show_toast(error.message, true);
|
|
3960
|
+
if (!error.notified) notify("error", error.message);
|
|
3986
3961
|
return false;
|
|
3987
3962
|
} finally { publish_view(); }
|
|
3988
3963
|
}
|
|
@@ -3993,10 +3968,10 @@ function change_view_theme(theme) {
|
|
|
3993
3968
|
try { localStorage.setItem(THEME_STORAGE_KEY, document.documentElement.dataset.theme); } catch { /* Keep the session preference. */ }
|
|
3994
3969
|
}
|
|
3995
3970
|
|
|
3996
|
-
window.discuss_view = { snapshot: view_snapshot, write_field: write_view_field,
|
|
3971
|
+
window.discuss_view = { snapshot: view_snapshot, write_field: write_view_field, notify,
|
|
3997
3972
|
write_launch: write_launch_field, default_launch_entry, description_template, save_description_template, description_draft, write_description_draft, describe_member, set_chain_rounds, continue_chain, expert_presets, apply_expert_preset, unique_instance, member_label, preset_identity, save_expert_preset, delete_expert_preset, write_room_connection, write_room_adapter, launch_clients: LAUNCH_CLIENTS,
|
|
3998
3973
|
launch_model_state, load_launch_models, write_launch_model, save_launch_api_key, write_scheduler, save_scheduler_api_key, check_scheduler, write_named_followup, write_create_discussion, write_create_chain_rounds, check_launch_api, launch_summary, launch_can_set_access, pick_launch_directory, browse_local_directory, save_launch_projects, restore_launch_profile, reset_create_form, empty_discussion_message,
|
|
3999
|
-
start_drag: start_directory_drag,
|
|
3974
|
+
start_drag: start_directory_drag, reconnect: reconnect_live_updates,
|
|
4000
3975
|
attachment_markup, attachment_size, attachment_file_icon, drop_attachments, paste_attachments,
|
|
4001
3976
|
write_expert: write_view_expert, add_instance: add_view_instance, add_preset: add_preset_instance, platform_of, execute: execute_view_action, theme: change_view_theme,
|
|
4002
3977
|
admin: { register: register_admin, login: login_admin, logout: logout_admin,
|