ai_co_discuss 0.3.8 → 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 +123 -168
- package/web/modern.css +1 -1
- package/web/modern.js +4352 -4569
package/web/app.js
CHANGED
|
@@ -53,12 +53,12 @@ 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 = [];
|
|
@@ -66,21 +66,18 @@ state.message_history = null;
|
|
|
66
66
|
state.connected = false;
|
|
67
67
|
state.reconnecting = false;
|
|
68
68
|
state.attachment_drafts = [];
|
|
69
|
-
state.composer_error = "";
|
|
70
69
|
state.upload_status = "";
|
|
71
70
|
state.sending_message = false;
|
|
72
71
|
state.attachment_preview = null;
|
|
73
72
|
state.archive_changes = new Map();
|
|
74
73
|
state.archive_version = 0;
|
|
75
|
-
state.directory_error = "";
|
|
76
|
-
state.directory_notice = "";
|
|
77
74
|
// The open import's progress for the import dialog: { done, rooms: [{ title, status, members, error }], errors }.
|
|
78
75
|
state.import_progress = null;
|
|
79
76
|
state.directory_pending = new Map();
|
|
80
77
|
state.directory_version = 0;
|
|
81
78
|
state.navigation_version = 0;
|
|
82
79
|
state.admin = { loaded: false, local: false, registered: false, setup_available: false, logged_in: false, profile: null, identity_version: 0 };
|
|
83
|
-
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: [] };
|
|
84
81
|
const PLATFORM_CLIENTS = { Codex: [{ value: "codex_cli", label: "Codex CLI" }, { value: "codex", label: "Codex App" }],
|
|
85
82
|
Claude: [{ value: "claude", label: "Claude Code CLI" }],
|
|
86
83
|
Gemini: [{ value: "antigravity_cli", label: "Antigravity CLI" }, { value: "antigravity", label: "Antigravity App" }] };
|
|
@@ -137,7 +134,8 @@ function format_time(value) {
|
|
|
137
134
|
|
|
138
135
|
/** Display operation feedback only in the classic renderer. */
|
|
139
136
|
function show_toast(message, is_error = false) {
|
|
140
|
-
|
|
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; }
|
|
141
139
|
clearTimeout(state.toast_timer);
|
|
142
140
|
element("toast").textContent = message;
|
|
143
141
|
element("toast").classList.toggle("error", is_error);
|
|
@@ -145,6 +143,17 @@ function show_toast(message, is_error = false) {
|
|
|
145
143
|
state.toast_timer = setTimeout(() => { element("toast").hidden = true; }, TOAST_DURATION_MS);
|
|
146
144
|
}
|
|
147
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
|
+
|
|
148
157
|
/** The administrator session token kept by this browser, or an empty string. */
|
|
149
158
|
function admin_token() {
|
|
150
159
|
if (state.admin.local) return "";
|
|
@@ -437,7 +446,6 @@ async function update_directory(change) {
|
|
|
437
446
|
if (state.directory_pending.has(change.key)) return;
|
|
438
447
|
state.directory_pending.set(change.key, change);
|
|
439
448
|
state.directory_version++;
|
|
440
|
-
state.directory_error = "";
|
|
441
449
|
change.apply();
|
|
442
450
|
render_rooms();
|
|
443
451
|
publish_view();
|
|
@@ -447,7 +455,7 @@ async function update_directory(change) {
|
|
|
447
455
|
return saved;
|
|
448
456
|
} catch (error) {
|
|
449
457
|
change.rollback();
|
|
450
|
-
|
|
458
|
+
notify_failure(error, change.error_message?.(error) || `${change.label}失败,已恢复原状态:${error.message}`);
|
|
451
459
|
throw error;
|
|
452
460
|
} finally {
|
|
453
461
|
state.directory_pending.delete(change.key);
|
|
@@ -540,13 +548,12 @@ async function archive_channel(id, archived) {
|
|
|
540
548
|
const change = { archived, phase: archived && room.phase && room.phase !== "concluded" ? "paused" : room.phase };
|
|
541
549
|
state.archive_changes.set(id, change);
|
|
542
550
|
state.archive_version++;
|
|
543
|
-
state.directory_error = "";
|
|
544
551
|
display_channel_archive(id, change);
|
|
545
552
|
try {
|
|
546
553
|
const saved = await request_json(`/api/rooms/${id}/${archived ? "archive" : "restore"}`, { actor: "用户" });
|
|
547
554
|
display_channel_archive(id, { archived: Boolean(saved.archived), phase: saved.phase }, change);
|
|
548
555
|
} catch (error) {
|
|
549
|
-
|
|
556
|
+
notify_failure(error, `「${room.title}」${archived ? "存档" : "恢复"}失败,已恢复原状态:${management_error_message(id, error)}`);
|
|
550
557
|
display_channel_archive(id, previous, change);
|
|
551
558
|
throw error;
|
|
552
559
|
} finally {
|
|
@@ -582,7 +589,6 @@ async function assign_channel_folder(id, folder_id) {
|
|
|
582
589
|
const previous_folder = room.folder_id ?? null;
|
|
583
590
|
if (state.folders.some(folder => folder.id === folder_id && folder.pending)) return;
|
|
584
591
|
if (folder_id && state.directory_pending.get("folder-order")?.removed_folder === folder_id) return;
|
|
585
|
-
state.directory_error = "";
|
|
586
592
|
state.folder_moves.set(id, folder_id);
|
|
587
593
|
state.folder_version++;
|
|
588
594
|
display_channel_folder(id, folder_id);
|
|
@@ -591,7 +597,7 @@ async function assign_channel_folder(id, folder_id) {
|
|
|
591
597
|
display_channel_folder(id, saved.folder_id ?? null);
|
|
592
598
|
show_toast("话题归类已更新");
|
|
593
599
|
} catch (error) {
|
|
594
|
-
|
|
600
|
+
notify_failure(error, `「${room.title}」归类失败,已恢复原状态:${error.message}`);
|
|
595
601
|
display_channel_folder(id, previous_folder);
|
|
596
602
|
throw error;
|
|
597
603
|
} finally {
|
|
@@ -888,7 +894,6 @@ async function refresh_room({ room: read_room = true } = {}) {
|
|
|
888
894
|
|
|
889
895
|
/** Select a room and reset room-specific draft and filter state. */
|
|
890
896
|
async function select_room(room_id) {
|
|
891
|
-
state.access_error = ""; state.access_notice = "";
|
|
892
897
|
state.navigation_version++;
|
|
893
898
|
state.refresh_version++;
|
|
894
899
|
state.room = state.rooms.find((room) => room.id === room_id);
|
|
@@ -900,9 +905,7 @@ async function select_room(room_id) {
|
|
|
900
905
|
state.pending_turn = null;
|
|
901
906
|
state.pending_submission = null;
|
|
902
907
|
clear_attachment_drafts();
|
|
903
|
-
state.composer_error = "";
|
|
904
908
|
state.launch.repair = null;
|
|
905
|
-
state.launch.repair_error = '';
|
|
906
909
|
state.launch.room_models = null;
|
|
907
910
|
state.messages = [];
|
|
908
911
|
state.message_history = null;
|
|
@@ -1027,7 +1030,7 @@ function set_proxy(name) { state.free_proxy = state.free_proxy === name ? "" : n
|
|
|
1027
1030
|
async function copy_online_notice(name) {
|
|
1028
1031
|
const ids = (state.room.free?.requests || []).filter(item => item.member === name && item.status === "pending").map(item => `#${item.message_id}`);
|
|
1029
1032
|
await copy_text(`请阅读 ${location.origin}/r/${state.room.id}?participant=${name} ,回应 ${ids.join("、") || "最新消息"}。单条原文:${location.origin}/r/${state.room.id}/messages/编号。请在你自己的窗口回答,不要操作网页或调用写入接口。`);
|
|
1030
|
-
|
|
1033
|
+
notify("info", `已复制给 ${name} 的阅读通知;复制不代表已送达。拿到回答后,在输入框选择“代发 ${name}”粘贴原文。`);
|
|
1031
1034
|
}
|
|
1032
1035
|
|
|
1033
1036
|
/** Start every new form from defaults: room use, delivery, text, roster, host, limits and software overrides. */
|
|
@@ -1045,7 +1048,6 @@ function reset_create_form() {
|
|
|
1045
1048
|
state.create_chain_rounds = DEFAULT_CHAIN_ROUNDS;
|
|
1046
1049
|
state.launch.profiles = JSON.parse(JSON.stringify(state.launch.saved_profiles || {}));
|
|
1047
1050
|
Object.values(state.launch.profiles).forEach(fill_api_key);
|
|
1048
|
-
state.launch.notice = "";
|
|
1049
1051
|
render_launch_settings();
|
|
1050
1052
|
publish_view();
|
|
1051
1053
|
}
|
|
@@ -1073,7 +1075,7 @@ async function return_conclusion() {
|
|
|
1073
1075
|
const summary = room.host_brief?.text || room.state_text || "(主持人尚未整理结论,请查看原文)";
|
|
1074
1076
|
await request_json(`/api/rooms/${parent.id}/messages`, { author: "用户", text: `头脑风暴「${room.title}」已收束。\n\n${summary}\n\n原文:${location.origin}/r/${room.id}`,
|
|
1075
1077
|
kind: "chat", submitted_by: "用户", submission_id: new_submission_id(), to: [] });
|
|
1076
|
-
|
|
1078
|
+
notify("info", `结论已发回「${parent.title}」`);
|
|
1077
1079
|
await select_room(parent.id);
|
|
1078
1080
|
}
|
|
1079
1081
|
|
|
@@ -1239,7 +1241,7 @@ async function send_descriptions(room_id, token, descriptions) {
|
|
|
1239
1241
|
const filled = Object.fromEntries(Object.entries(descriptions).filter(([, text]) => text?.trim()));
|
|
1240
1242
|
if (!room_id || !token || !Object.keys(filled).length) return;
|
|
1241
1243
|
try { await request_json(`/api/rooms/${room_id}/descriptions`, { descriptions: filled }, token); }
|
|
1242
|
-
catch (error) {
|
|
1244
|
+
catch (error) { notify("warning", `成员描述未保存:${error.message},可在发言顺序的成员菜单里重新填写。`); }
|
|
1243
1245
|
}
|
|
1244
1246
|
|
|
1245
1247
|
/** Save display labels by member name with the room credential; a failure leaves the members under their names. */
|
|
@@ -1247,50 +1249,47 @@ async function send_labels(room_id, token, labels) {
|
|
|
1247
1249
|
const filled = Object.fromEntries(Object.entries(labels).filter(([, text]) => text?.trim()));
|
|
1248
1250
|
if (!room_id || !token || !Object.keys(filled).length) return;
|
|
1249
1251
|
try { await request_json(`/api/rooms/${room_id}/labels`, { labels: filled }, token); }
|
|
1250
|
-
catch (error) {
|
|
1252
|
+
catch (error) { notify("warning", `成员显示名未保存:${error.message},房间里按成员名显示。`); }
|
|
1251
1253
|
}
|
|
1252
1254
|
|
|
1253
1255
|
/** Save the new room's round limit with the room credential; a failure leaves the room on the default. */
|
|
1254
1256
|
async function send_chain_rounds(room_id, token, max_rounds) {
|
|
1255
1257
|
if (!room_id || !token || max_rounds === DEFAULT_CHAIN_ROUNDS) return;
|
|
1256
1258
|
try { await request_json(`/api/rooms/${room_id}/budget`, { max_rounds }, token); }
|
|
1257
|
-
catch (error) {
|
|
1259
|
+
catch (error) { notify("warning", `推进轮数未保存:${error.message},房间沿用默认 ${DEFAULT_CHAIN_ROUNDS} 轮,可在房间页修改。`); }
|
|
1258
1260
|
}
|
|
1259
1261
|
|
|
1260
1262
|
/** Edit one member's description inside the room; the next decision by the scheduler or host uses it. */
|
|
1261
1263
|
async function describe_member(name, text) {
|
|
1262
1264
|
const room_id = state.room.id;
|
|
1263
|
-
state.free_error = "";
|
|
1264
1265
|
try {
|
|
1265
1266
|
const token = localStorage.getItem(`discuss-access:${room_id}`);
|
|
1266
1267
|
if (!token) throw new Error("此浏览器缺少房间控制凭据,请使用创建房间的浏览器");
|
|
1267
1268
|
await request_json(`/api/rooms/${room_id}/descriptions`, { descriptions: { [name]: text } }, token);
|
|
1268
1269
|
if (state.room?.id === room_id) await refresh_room();
|
|
1269
|
-
} catch (error) {
|
|
1270
|
+
} catch (error) { notify("error", error.message); throw error; }
|
|
1270
1271
|
}
|
|
1271
1272
|
|
|
1272
1273
|
/** Change how many rounds the host may run from one owner message; chains still running follow the new value. */
|
|
1273
1274
|
async function set_chain_rounds(max_rounds) {
|
|
1274
1275
|
const room_id = state.room.id;
|
|
1275
|
-
state.free_error = "";
|
|
1276
1276
|
try {
|
|
1277
1277
|
const token = localStorage.getItem(`discuss-access:${room_id}`);
|
|
1278
1278
|
if (!token) throw new Error("此浏览器缺少房间控制凭据,请使用创建房间的浏览器");
|
|
1279
1279
|
await request_json(`/api/rooms/${room_id}/budget`, { max_rounds }, token);
|
|
1280
1280
|
if (state.room?.id === room_id) await refresh_room();
|
|
1281
|
-
} catch (error) {
|
|
1281
|
+
} catch (error) { notify("error", error.message); throw error; }
|
|
1282
1282
|
}
|
|
1283
1283
|
|
|
1284
1284
|
/** Let a chain that stopped at its round limit run on for more rounds; the dispatch it held back goes out at once. */
|
|
1285
1285
|
async function continue_chain(chain_id, rounds) {
|
|
1286
1286
|
const room_id = state.room.id;
|
|
1287
|
-
state.free_error = "";
|
|
1288
1287
|
try {
|
|
1289
1288
|
const token = localStorage.getItem(`discuss-access:${room_id}`);
|
|
1290
1289
|
if (!token) throw new Error("此浏览器缺少房间控制凭据,请使用创建房间的浏览器");
|
|
1291
1290
|
await request_json(`/api/rooms/${room_id}/chains/${chain_id}`, { action: "continue", rounds }, token);
|
|
1292
1291
|
if (state.room?.id === room_id) await refresh_room();
|
|
1293
|
-
} catch (error) {
|
|
1292
|
+
} catch (error) { notify("error", error.message); throw error; }
|
|
1294
1293
|
}
|
|
1295
1294
|
|
|
1296
1295
|
/** Allow operations a woken member's software refused; the local service passes the rules from its next run. */
|
|
@@ -1321,33 +1320,30 @@ async function set_member_adapter({ name, adapter }) {
|
|
|
1321
1320
|
/** Owner changes use the website; only automatic members need the local service to reconnect. */
|
|
1322
1321
|
async function change_member({ member_id, name, action, role, latest_id }) {
|
|
1323
1322
|
const room_id = state.room.id;
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
}
|
|
1339
|
-
if (state.room?.id === room_id) await refresh_room();
|
|
1340
|
-
} 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();
|
|
1341
1337
|
}
|
|
1342
1338
|
|
|
1343
1339
|
/** Reorder who has not spoken yet against the revision the board showed; a changed order is refused and the board reloads. */
|
|
1344
1340
|
async function reorder_speakers({ revision, order }) {
|
|
1345
1341
|
const room_id = state.room.id;
|
|
1346
|
-
state.order_error = null;
|
|
1347
1342
|
try { await request_json(`/api/rooms/${room_id}/speaking-order`, { revision, order }); }
|
|
1348
1343
|
catch (error) {
|
|
1349
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).
|
|
1350
|
-
|
|
1345
|
+
if (error.status === 409) notify("warning", "发言顺序刚有变化,已更新为最新顺序,请重新调整");
|
|
1346
|
+
else notify("error", error.message);
|
|
1351
1347
|
}
|
|
1352
1348
|
if (state.room?.id === room_id) await refresh_room();
|
|
1353
1349
|
}
|
|
@@ -1355,41 +1351,39 @@ async function reorder_speakers({ revision, order }) {
|
|
|
1355
1351
|
/** Invite an independent member, retaining its credential before either delivery path runs. */
|
|
1356
1352
|
async function invite_member({ name, access, project_access = "read", session_id = "" }) {
|
|
1357
1353
|
const room_id = state.room.id;
|
|
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
|
-
|
|
1391
|
-
}
|
|
1392
|
-
} 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
|
+
}
|
|
1393
1387
|
}
|
|
1394
1388
|
|
|
1395
1389
|
/** Clear only the composer that produced the saved message. */
|
|
@@ -1417,10 +1411,8 @@ async function send_message(event) {
|
|
|
1417
1411
|
event.preventDefault();
|
|
1418
1412
|
if (!state.room || state.sending_message) return;
|
|
1419
1413
|
const room_id = state.room.id, drafts = state.attachment_drafts;
|
|
1420
|
-
|
|
1421
|
-
let
|
|
1422
|
-
try { payload = message_payload(); }
|
|
1423
|
-
catch (error) { state.composer_error = error.message; render_attachment_drafts(); publish_view(); throw error; }
|
|
1414
|
+
const payload = message_payload();
|
|
1415
|
+
let saved = false;
|
|
1424
1416
|
if (!payload.text && !drafts.length) return;
|
|
1425
1417
|
const signature = JSON.stringify({ room: room_id, ...payload, files: drafts.map(file => file.id) });
|
|
1426
1418
|
if (state.pending_submission?.signature !== signature) state.pending_submission = { signature, id: new_submission_id() };
|
|
@@ -1435,9 +1427,9 @@ async function send_message(event) {
|
|
|
1435
1427
|
saved = true;
|
|
1436
1428
|
await finish_message(result, room_id, drafts);
|
|
1437
1429
|
} catch (error) {
|
|
1438
|
-
if (state.room?.id === room_id)
|
|
1430
|
+
if (state.room?.id === room_id) notify_failure(error, saved
|
|
1439
1431
|
? `留言已保存,但页面刷新失败:${error.message}`
|
|
1440
|
-
: `${error.message}
|
|
1432
|
+
: `${error.message};草稿已保留,可重试发送`);
|
|
1441
1433
|
throw error;
|
|
1442
1434
|
} finally {
|
|
1443
1435
|
state.sending_message = false;
|
|
@@ -1468,9 +1460,8 @@ function attachment_file_icon(file_name) {
|
|
|
1468
1460
|
function add_attachments(files) {
|
|
1469
1461
|
if (!state.room || state.sending_message) return;
|
|
1470
1462
|
const selected = Array.from(files);
|
|
1471
|
-
state.
|
|
1472
|
-
if (
|
|
1473
|
-
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");
|
|
1474
1465
|
else state.attachment_drafts.push(...selected.map(file => ({ id: new_submission_id(), file,
|
|
1475
1466
|
preview_url: /^image\/(png|jpeg|gif|webp)$/.test(file.type) ? URL.createObjectURL(file) : "", uploaded: null, actor: null })));
|
|
1476
1467
|
render_attachment_drafts();
|
|
@@ -1490,19 +1481,18 @@ function remove_attachment(id) {
|
|
|
1490
1481
|
const file = state.attachment_drafts.find(item => item.id === id);
|
|
1491
1482
|
if (file?.preview_url) URL.revokeObjectURL(file.preview_url);
|
|
1492
1483
|
state.attachment_drafts = state.attachment_drafts.filter(item => item.id !== id);
|
|
1493
|
-
state.composer_error = "";
|
|
1494
1484
|
render_attachment_drafts();
|
|
1495
1485
|
publish_view();
|
|
1496
1486
|
}
|
|
1497
1487
|
|
|
1498
|
-
/** Display shared file drafts and
|
|
1488
|
+
/** Display shared file drafts and upload progress in the classic theme. */
|
|
1499
1489
|
function render_attachment_drafts() {
|
|
1500
1490
|
const container = element("attachment-drafts");
|
|
1501
1491
|
if (!container) return;
|
|
1502
1492
|
container.innerHTML = state.attachment_drafts.map(item => `<div class="attachment-draft">${item.preview_url ? `<img src="${item.preview_url}" alt="">` : ""}
|
|
1503
1493
|
<span>${escape_html(item.file.name)}<small>${attachment_size(item.file.size)}</small></span>
|
|
1504
1494
|
<button type="button" data-remove-attachment="${item.id}" aria-label="移除 ${escape_html(item.file.name)}" ${state.sending_message ? "disabled" : ""}>×</button></div>`).join("");
|
|
1505
|
-
element("attachment-status").textContent = state.
|
|
1495
|
+
element("attachment-status").textContent = state.upload_status;
|
|
1506
1496
|
element("choose-attachments").disabled = state.sending_message;
|
|
1507
1497
|
}
|
|
1508
1498
|
|
|
@@ -1782,7 +1772,7 @@ function access_snapshot() {
|
|
|
1782
1772
|
name: member.name, access: is_web_member(member) ? "online" : member.access, available: member.status === "active" && !is_web_member(member)
|
|
1783
1773
|
&& Boolean(saved[member.name]?.token) && is_web_member(saved[member.name]) === is_web_member(member)
|
|
1784
1774
|
&& (!saved[member.name].id || saved[member.name].id === member.id),
|
|
1785
|
-
}))
|
|
1775
|
+
})) };
|
|
1786
1776
|
}
|
|
1787
1777
|
|
|
1788
1778
|
/** Generate a separate random credential for each declared participant instance; `cli` members are woken locally. */
|
|
@@ -1837,27 +1827,20 @@ async function submit_room(event, folder_id = null) {
|
|
|
1837
1827
|
const signature = signed(payload, launch_draft);
|
|
1838
1828
|
const navigation = state.navigation_version;
|
|
1839
1829
|
if (!launching) element("room-dialog").close();
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
const experts = Object.fromEntries(Object.entries(payload.experts || {}).map(([name, mode]) => [name, mode === "manual" ? "cli" : mode]));
|
|
1848
|
-
const input = { ...(launch_draft || {}), room: { ...payload, ...(payload.experts ? { experts } : {}), members: pending.members, submission_id: pending.id }, token };
|
|
1849
|
-
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}」创建`,
|
|
1850
1837
|
error_message: launching ? error => `「${payload.title}」${error.message}` : null,
|
|
1851
1838
|
apply: () => { state.creating_room = payload; }, rollback: () => { state.creating_room = null; },
|
|
1852
1839
|
save: async () => (await submit_local_launch(input, payload.title, !launching, signature)).room,
|
|
1853
1840
|
commit: saved => {
|
|
1854
1841
|
state.creating_room = null;
|
|
1855
1842
|
if (!state.rooms.some(existing => existing.id === saved.id)) state.rooms.push(saved);
|
|
1856
|
-
} });
|
|
1857
|
-
catch (error) {
|
|
1858
|
-
if (launching) { state.launch.error = error.message; render_launch_settings(); publish_view(); }
|
|
1859
|
-
throw error;
|
|
1860
|
-
}
|
|
1843
|
+
} });
|
|
1861
1844
|
state.pending_room = null;
|
|
1862
1845
|
state.create_parent = null;
|
|
1863
1846
|
if (discussion) await open_discussion_after_creation(room.id, discussion);
|
|
@@ -1880,7 +1863,7 @@ async function open_discussion_after_creation(room_id, { goal, named_followup, l
|
|
|
1880
1863
|
await request_json(`${path}/discussion`, { action: "start", revision: current.revision,
|
|
1881
1864
|
...(goal ? { goal } : {}), limits, named_followup });
|
|
1882
1865
|
} catch (error) {
|
|
1883
|
-
|
|
1866
|
+
notify("warning", `房间已创建,但开启讨论失败:${error.message};可在右侧成员栏重试`);
|
|
1884
1867
|
}
|
|
1885
1868
|
}
|
|
1886
1869
|
|
|
@@ -2003,7 +1986,6 @@ async function resume_launch(id) {
|
|
|
2003
1986
|
const record = stored_launches().find(item => item.id === id);
|
|
2004
1987
|
if (!record || state.directory_pending.has(`launch-${id}`)) return;
|
|
2005
1988
|
state.directory_pending.set(`launch-${id}`, true);
|
|
2006
|
-
state.launch.error = "";
|
|
2007
1989
|
publish_view();
|
|
2008
1990
|
try {
|
|
2009
1991
|
const result = await submit_local_launch(record.input, record.title, record.direct, record.signature);
|
|
@@ -2016,11 +1998,10 @@ async function resume_launch(id) {
|
|
|
2016
1998
|
element("room-dialog").close();
|
|
2017
1999
|
}
|
|
2018
2000
|
if (!record.direct) clear_launch_overrides(record.input);
|
|
2019
|
-
|
|
2020
|
-
state.directory_error = "";
|
|
2001
|
+
notify("info", result.status === "running" ? "已核对原启动,实际执行请查看进度回执" : "已核对原房间状态;不会自动恢复已暂停或结束的讨论");
|
|
2021
2002
|
if (state.pending_room?.id === id) state.pending_room = null;
|
|
2022
2003
|
await select_room(result.room.id);
|
|
2023
|
-
} catch (error) {
|
|
2004
|
+
} catch (error) { notify("error", error.message); }
|
|
2024
2005
|
finally { state.directory_pending.delete(`launch-${id}`); render_launch_settings(); publish_view(); }
|
|
2025
2006
|
}
|
|
2026
2007
|
|
|
@@ -2028,7 +2009,6 @@ async function resume_launch(id) {
|
|
|
2028
2009
|
function discard_launch(id) {
|
|
2029
2010
|
localStorage.removeItem(LAUNCH_PENDING_PREFIX + id);
|
|
2030
2011
|
if (state.pending_room?.id === id) state.pending_room = null;
|
|
2031
|
-
state.launch.error = "";
|
|
2032
2012
|
refresh_launch_recoveries();
|
|
2033
2013
|
}
|
|
2034
2014
|
|
|
@@ -2329,7 +2309,6 @@ function write_launch_field(name, field, value) {
|
|
|
2329
2309
|
if (field === "base_url") delete state.launch.profiles[name].api_key_ref;
|
|
2330
2310
|
fill_api_key(state.launch.profiles[name]);
|
|
2331
2311
|
}
|
|
2332
|
-
state.launch.notice = "";
|
|
2333
2312
|
if (field === "adapter") render_launch_settings();
|
|
2334
2313
|
else render_launch_summaries();
|
|
2335
2314
|
publish_view();
|
|
@@ -2501,8 +2480,7 @@ async function pick_launch_directory() {
|
|
|
2501
2480
|
try {
|
|
2502
2481
|
const path = await browse_local_directory();
|
|
2503
2482
|
if (path) write_launch_field("workspace", "cwd", path);
|
|
2504
|
-
|
|
2505
|
-
} catch (error) { state.launch.error = error.message; }
|
|
2483
|
+
} catch (error) { notify("error", error.message); }
|
|
2506
2484
|
publish_view();
|
|
2507
2485
|
}
|
|
2508
2486
|
|
|
@@ -2523,9 +2501,8 @@ async function save_launch_defaults() {
|
|
|
2523
2501
|
try {
|
|
2524
2502
|
await local_launch_request("settings", { participants });
|
|
2525
2503
|
state.launch.saved_profiles = { ...state.launch.saved_profiles, ...JSON.parse(JSON.stringify(participants)) };
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
} catch (error) { state.launch.error = error.message; }
|
|
2504
|
+
notify("info", "已保存本机软件配置,后续讨论可复用");
|
|
2505
|
+
} catch (error) { notify("error", error.message); }
|
|
2529
2506
|
render_launch_settings();
|
|
2530
2507
|
publish_view();
|
|
2531
2508
|
}
|
|
@@ -2540,10 +2517,9 @@ async function launch_current_room() {
|
|
|
2540
2517
|
const draft = launch_options();
|
|
2541
2518
|
const result = await submit_local_launch({ ...draft, room_id, token }, state.room.title || "已有讨论");
|
|
2542
2519
|
clear_launch_overrides(draft);
|
|
2543
|
-
|
|
2544
|
-
state.launch.error = "";
|
|
2520
|
+
notify("info", result.status === "running" ? "启动已提交,实际执行请查看进度回执" : "本次启动已有记录,请查看房间状态");
|
|
2545
2521
|
if (state.room?.id === room_id) await refresh_room();
|
|
2546
|
-
} catch (error) {
|
|
2522
|
+
} catch (error) { notify("error", error.message); }
|
|
2547
2523
|
render_launch_settings();
|
|
2548
2524
|
publish_view();
|
|
2549
2525
|
}
|
|
@@ -2551,13 +2527,12 @@ async function launch_current_room() {
|
|
|
2551
2527
|
/** Load the selected paused room's original connection paths after proving browser ownership. */
|
|
2552
2528
|
async function load_room_configuration() {
|
|
2553
2529
|
const room_id = state.room.id;
|
|
2554
|
-
state.launch.repair_error = '';
|
|
2555
2530
|
try {
|
|
2556
2531
|
const token = localStorage.getItem(`discuss-access:${room_id}`);
|
|
2557
2532
|
if (!token) throw new Error('此浏览器缺少房间凭据,请使用原浏览器或原接入配置');
|
|
2558
2533
|
const result = await local_launch_request('room-config', { room_id, token });
|
|
2559
|
-
if (state.room?.id === room_id) state.launch.repair =
|
|
2560
|
-
} 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); }
|
|
2561
2536
|
render_room_configuration();
|
|
2562
2537
|
publish_view();
|
|
2563
2538
|
}
|
|
@@ -2568,7 +2543,6 @@ function write_room_connection(name, value) {
|
|
|
2568
2543
|
if (repair?.room_id !== state.room?.id || !repair.participants[name]) return;
|
|
2569
2544
|
const profile = repair.participants[name];
|
|
2570
2545
|
profile[profile.adapter === 'antigravity' ? 'ipc_directory' : 'executable'] = value;
|
|
2571
|
-
repair.notice = '';
|
|
2572
2546
|
publish_view();
|
|
2573
2547
|
}
|
|
2574
2548
|
|
|
@@ -2579,7 +2553,6 @@ function write_room_adapter(name, value) {
|
|
|
2579
2553
|
if (repair?.room_id !== state.room?.id || !['codex', 'codex_cli'].includes(profile?.adapter)
|
|
2580
2554
|
|| !['codex', 'codex_cli'].includes(value)) return;
|
|
2581
2555
|
profile.adapter = value;
|
|
2582
|
-
repair.notice = '';
|
|
2583
2556
|
publish_view();
|
|
2584
2557
|
}
|
|
2585
2558
|
|
|
@@ -2591,18 +2564,18 @@ async function save_room_configuration() {
|
|
|
2591
2564
|
const field = profile.adapter === 'antigravity' ? 'ipc_directory' : 'executable';
|
|
2592
2565
|
return [name, { [field]: profile[field], ...(['codex', 'codex_cli'].includes(profile.adapter) ? { adapter: profile.adapter } : {}) }];
|
|
2593
2566
|
}));
|
|
2594
|
-
state.launch.repair_error = '';
|
|
2595
2567
|
try {
|
|
2596
2568
|
const token = localStorage.getItem(`discuss-access:${repair.room_id}`);
|
|
2597
2569
|
if (!token) throw new Error('此浏览器缺少房间凭据,请重新核对');
|
|
2598
2570
|
const result = await local_launch_request('room-config', { room_id: repair.room_id, token,
|
|
2599
2571
|
version: repair.version, runtime_revision: repair.runtime_revision, participants });
|
|
2600
2572
|
if (state.room?.id === repair.room_id) {
|
|
2601
|
-
state.launch.repair =
|
|
2602
|
-
|
|
2573
|
+
state.launch.repair = result;
|
|
2574
|
+
notify('info', result.applied ? '本场接入设置已保存,服务已采用;可在进度看板重启并核对实际发言。'
|
|
2575
|
+
: '本场接入设置已保存;服务尚未确认采用,请先核对本机服务再重启。');
|
|
2603
2576
|
await refresh_room();
|
|
2604
2577
|
}
|
|
2605
|
-
} catch (error) { if (state.room?.id === repair.room_id)
|
|
2578
|
+
} catch (error) { if (state.room?.id === repair.room_id) notify("error", error.message); }
|
|
2606
2579
|
render_room_configuration();
|
|
2607
2580
|
publish_view();
|
|
2608
2581
|
}
|
|
@@ -2625,7 +2598,6 @@ function render_room_configuration() {
|
|
|
2625
2598
|
fields.dataset.roomId = state.room?.id || '';
|
|
2626
2599
|
}
|
|
2627
2600
|
element('local-room-configuration-save').hidden = !repair;
|
|
2628
|
-
element('local-room-configuration-notice').textContent = state.launch.repair_error || repair?.notice || '';
|
|
2629
2601
|
}
|
|
2630
2602
|
|
|
2631
2603
|
/** Render the same local setup controls in the classic theme. */
|
|
@@ -2663,7 +2635,7 @@ function render_launch_settings() {
|
|
|
2663
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>`;
|
|
2664
2636
|
}
|
|
2665
2637
|
for (const id of ["local-launch-notice", "new-local-launch-notice"]) {
|
|
2666
|
-
if (element(id)) element(id).textContent = [state.launch.error
|
|
2638
|
+
if (element(id)) element(id).textContent = [state.launch.error,
|
|
2667
2639
|
!state.launch.available && state.launch.connect_command ? `在这台电脑运行 ${state.launch.connect_command},使用命令打开的网页完成连接。` : ""].filter(Boolean).join(" ");
|
|
2668
2640
|
}
|
|
2669
2641
|
for (const id of ["local-launch-save", "new-local-launch-save"]) if (element(id)) element(id).disabled = !state.launch.available;
|
|
@@ -2836,8 +2808,7 @@ function download_member_access(name) {
|
|
|
2836
2808
|
const member = access.members.find(item => item.name === name);
|
|
2837
2809
|
if (!access.can_manage || !member?.available || member.access === "online") throw new Error("当前浏览器没有此成员可下载的私有配置");
|
|
2838
2810
|
download_private_json({ site: location.origin, room: room.id, token: member_credentials(room.id)[name].token }, `member-${room.id}-${name}.json`);
|
|
2839
|
-
|
|
2840
|
-
flash_access_notice(`已下载 ${name} 的私有配置`);
|
|
2811
|
+
notify("info", `已下载 ${name} 的私有配置`);
|
|
2841
2812
|
}
|
|
2842
2813
|
|
|
2843
2814
|
/** Copy the same room-scoped credential as one line of text for an agent that cannot take a file. */
|
|
@@ -2846,19 +2817,7 @@ async function copy_member_access_text(name) {
|
|
|
2846
2817
|
const member = access.members.find(item => item.name === name);
|
|
2847
2818
|
if (!access.can_manage || !member?.available || member.access === "online") throw new Error("当前浏览器没有此成员可复制的凭证");
|
|
2848
2819
|
await copy_text(JSON.stringify({ site: location.origin, room: room.id, token: member_credentials(room.id)[name].token }));
|
|
2849
|
-
|
|
2850
|
-
}
|
|
2851
|
-
|
|
2852
|
-
/** Confirm a copy or download briefly in the member rail; the note clears itself. */
|
|
2853
|
-
function flash_access_notice(message) {
|
|
2854
|
-
state.access_error = "";
|
|
2855
|
-
state.access_notice = message;
|
|
2856
|
-
clearTimeout(state.access_notice_timer);
|
|
2857
|
-
state.access_notice_timer = setTimeout(() => {
|
|
2858
|
-
if (state.access_notice !== message) return;
|
|
2859
|
-
state.access_notice = "";
|
|
2860
|
-
publish_view();
|
|
2861
|
-
}, TOAST_DURATION_MS);
|
|
2820
|
+
notify("info", `已复制 ${name} 的凭证文本(含 token)`);
|
|
2862
2821
|
}
|
|
2863
2822
|
|
|
2864
2823
|
/** Copy a member-specific entry instruction; only the launch variant carries the credential, never conversation context. */
|
|
@@ -2881,7 +2840,7 @@ async function copy_member_join(name, with_credential = false) {
|
|
|
2881
2840
|
${instruction}
|
|
2882
2841
|
房间入口:${address}
|
|
2883
2842
|
先核对最新阶段、任务和原文编号,再按房间协议参与。${credential}`);
|
|
2884
|
-
if (state.room?.id === room.id)
|
|
2843
|
+
if (state.room?.id === room.id) notify("info", with_credential ? `已复制 ${name} 的启动指令(含 token)` : `已复制 ${name} 的参与说明`);
|
|
2885
2844
|
}
|
|
2886
2845
|
|
|
2887
2846
|
/** How a manual member learns it is its turn: free rooms are pulled by the member, discussions only on the host's assignment. */
|
|
@@ -3121,16 +3080,16 @@ async function fork_room(message_id) {
|
|
|
3121
3080
|
const registered = new Set((local?.members || []).filter(member => member.status !== "failed").map(member => member.name));
|
|
3122
3081
|
for (const member of (state.room.free?.members || []).filter(item => item.status === "away")) {
|
|
3123
3082
|
if (member.access === "runner" && !registered.has(member.name)) continue;
|
|
3124
|
-
await change_member({ member_id: member.id, name: member.name, action: "rejoin" });
|
|
3125
|
-
|
|
3083
|
+
try { await change_member({ member_id: member.id, name: member.name, action: "rejoin" }); }
|
|
3084
|
+
catch (error) { problems.push(`${member.name} 未能请回:${error.message}`); }
|
|
3126
3085
|
}
|
|
3127
3086
|
const fresh = (local?.members || []).filter(member => member.status === "fresh").map(member => member.name);
|
|
3128
3087
|
const failed = (local?.members || []).filter(member => member.status === "failed").map(member => `${member.name}(${member.error})`);
|
|
3129
3088
|
if (failed.length) problems.push(`未分叉、保持离开:${failed.join("、")}`);
|
|
3130
3089
|
const done = `已从 #${message_id} 分支出新房间,原房间不变`;
|
|
3131
|
-
//
|
|
3132
|
-
|
|
3133
|
-
|
|
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("、")} 将以新会话开始` : ""}。`);
|
|
3134
3093
|
publish_view();
|
|
3135
3094
|
}
|
|
3136
3095
|
|
|
@@ -3769,13 +3728,13 @@ function view_snapshot() {
|
|
|
3769
3728
|
const experts = Object.fromEntries(["new-expert-modes", "topic-expert-modes"].map((id) =>
|
|
3770
3729
|
[id, element(id) ? read_expert_inputs(id) : {}]));
|
|
3771
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,
|
|
3772
|
-
archive_pending: [...state.archive_changes.keys()],
|
|
3731
|
+
archive_pending: [...state.archive_changes.keys()], import_progress: structuredClone(state.import_progress),
|
|
3773
3732
|
directory_pending: [...state.directory_pending.keys()], folder_pending: [...state.folder_moves.keys()], creating_room: state.creating_room,
|
|
3774
3733
|
connected: state.connected, reconnecting: state.reconnecting, backup_pending: Boolean(state.backup_pending), page: state.page, pages: state.pages, query: state.query,
|
|
3775
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 })),
|
|
3776
|
-
|
|
3735
|
+
upload_status: state.upload_status, sending_message: state.sending_message,
|
|
3777
3736
|
mention_only: state.mention_only, reply_to: state.reply_to, turn_reply_to: state.turn_reply_to,
|
|
3778
|
-
|
|
3737
|
+
free_proxy: state.free_proxy,
|
|
3779
3738
|
create_parent: state.create_parent, access: access_snapshot(), admin: admin_snapshot(),
|
|
3780
3739
|
tab: state.tab, snapshot_id: state.snapshot_id, context_dirty: state.context_dirty,
|
|
3781
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 };
|
|
@@ -3871,7 +3830,6 @@ async function backup_room(room_id = state.room?.id) {
|
|
|
3871
3830
|
async function backup_rooms({ ids, name }) {
|
|
3872
3831
|
if (!ids.length || state.backup_pending) return;
|
|
3873
3832
|
state.backup_pending = true;
|
|
3874
|
-
state.directory_notice = state.directory_error = "";
|
|
3875
3833
|
publish_view();
|
|
3876
3834
|
try {
|
|
3877
3835
|
const snapshots = [];
|
|
@@ -3881,9 +3839,9 @@ async function backup_rooms({ ids, name }) {
|
|
|
3881
3839
|
snapshots.push({ ...snapshot, source_url: `${location.origin}/r/${encodeURIComponent(id)}` });
|
|
3882
3840
|
}
|
|
3883
3841
|
await window.discuss_backup.download_rooms_backup(snapshots, name);
|
|
3884
|
-
|
|
3842
|
+
notify("info", `已生成 ${snapshots.length} 个频道的备份,请查看浏览器下载。`);
|
|
3885
3843
|
} catch (error) {
|
|
3886
|
-
|
|
3844
|
+
notify("error", `导出失败,未生成备份:${error.message}`);
|
|
3887
3845
|
} finally {
|
|
3888
3846
|
state.backup_pending = false;
|
|
3889
3847
|
publish_view();
|
|
@@ -3995,14 +3953,11 @@ async function execute_view_action(name, value) {
|
|
|
3995
3953
|
prepare_brainstorm, return_conclusion, adopt_recommendation, rate_decision,
|
|
3996
3954
|
busy: () => { throw new Error("上一个操作仍在处理中,请稍候;长时间无响应请刷新页面"); },
|
|
3997
3955
|
copy_reading: () => copy_text(`${location.origin}/r/${state.room.id}`), copy: copy_text };
|
|
3998
|
-
if (!["refresh", "busy"].includes(name)) state.free_error = "";
|
|
3999
3956
|
try {
|
|
4000
3957
|
await actions[name](value);
|
|
4001
3958
|
return true;
|
|
4002
3959
|
} catch (error) {
|
|
4003
|
-
if (
|
|
4004
|
-
else if (!state.free_error) state.free_error = error.message;
|
|
4005
|
-
show_toast(error.message, true);
|
|
3960
|
+
if (!error.notified) notify("error", error.message);
|
|
4006
3961
|
return false;
|
|
4007
3962
|
} finally { publish_view(); }
|
|
4008
3963
|
}
|
|
@@ -4013,7 +3968,7 @@ function change_view_theme(theme) {
|
|
|
4013
3968
|
try { localStorage.setItem(THEME_STORAGE_KEY, document.documentElement.dataset.theme); } catch { /* Keep the session preference. */ }
|
|
4014
3969
|
}
|
|
4015
3970
|
|
|
4016
|
-
window.discuss_view = { snapshot: view_snapshot, write_field: write_view_field,
|
|
3971
|
+
window.discuss_view = { snapshot: view_snapshot, write_field: write_view_field, notify,
|
|
4017
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,
|
|
4018
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,
|
|
4019
3974
|
start_drag: start_directory_drag, reconnect: reconnect_live_updates,
|