ai_co_discuss 0.2.47 → 0.2.49

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/web/app.js CHANGED
@@ -48,7 +48,7 @@ const PLATFORM_IDENTITIES = {
48
48
  };
49
49
  const IDENTITIES = by_platform(PLATFORM_IDENTITIES, (identity, name) => ({ ...identity, label: name, description: "AI 实例" }));
50
50
  const state = { rooms: [], folders: [], room: null, page: 1, pages: 1, query: "", mention_only: false, free_notice: "", free_error: "",
51
- free_proxy: "", create_parent: null, named_followup: false,
51
+ free_proxy: "", create_parent: null, named_followup: false, create_discussion: false,
52
52
  archive_view: false, directory_sort: "manual", management: null, dragged_room: null, folder_moves: new Map(), folder_version: 0,
53
53
  reply_to: null, turn_reply_to: null, tab: "topic", refresh_version: 0, context_dirty: false, topic_dirty: false,
54
54
  snapshot_id: 0, pending_submission: null, pending_turn: null, pending_room: null, toast_timer: null,
@@ -690,7 +690,7 @@ function render_room() {
690
690
  if (!state.room) return;
691
691
  const room = state.room;
692
692
  element("room-title").textContent = room.title;
693
- element("room-subtitle").textContent = `${room.archived ? '已存档 · ' : ''}${room.message_count} 条发言 · 发言权:${floor_status(room)}`;
693
+ element("room-subtitle").textContent = `${room.archived ? '已存档 · ' : ''}${room.message_count} 条发言`;
694
694
  element("message-count").textContent = room.message_count;
695
695
  element("room-goal").textContent = room.goal || "尚未填写议题目标。";
696
696
  element("latest-id").textContent = `#${room.latest_id}`;
@@ -704,16 +704,10 @@ function render_room() {
704
704
  }
705
705
  render_access();
706
706
  render_topic();
707
- render_receipts();
708
707
  render_host_brief();
709
708
  update_composer_role();
710
709
  }
711
710
 
712
- /** Describe the actual holder or nominated recipient for both themes. */
713
- function floor_status(room) {
714
- return room.floor?.holder ? `${display_name(room.floor.holder)} 持有` : `待 ${display_name(room.floor?.next_speaker || room.host)} 领取`;
715
- }
716
-
717
711
  /** A round completes only after all configured experts have contributed. */
718
712
  function progress_round(room) {
719
713
  const groups = round_groups(room);
@@ -771,16 +765,6 @@ function render_host_brief() {
771
765
  element("host-brief-meta").textContent = brief_meta(room);
772
766
  }
773
767
 
774
- /** Show bounded receipt references without interpreting free-form reports as status. */
775
- function render_receipts() {
776
- const receipts = state.room.latest_receipts;
777
- const links = receipts?.map((receipt) => `<button type="button" class="text-button" data-receipt-detail="${receipt.id}">${escape_html(display_name(receipt.author))} #${receipt.id} · ${escape_html(new Date(receipt.created_at).toLocaleString("zh-CN"))}${receipt.id > state.room.state_until ? " · 晚于摘要" : ""}</button>`).join("") || "尚无运行回执。";
778
- for (const id of ["topic-receipts", "state-receipts"]) {
779
- element(id).hidden = !Array.isArray(receipts);
780
- element(`${id}-links`).innerHTML = links;
781
- }
782
- }
783
-
784
768
  /** Describe roles separately from authors and the actual submitter. */
785
769
  function message_provenance(message) {
786
770
  const role = message.kind === "receipt" ? "运行回执" : message.kind === "moderation" ? "主持发言" : (message.author === "用户" ? "用户发言" : "专家观点");
@@ -961,10 +945,15 @@ function message_payload() {
961
945
  return payload;
962
946
  }
963
947
  const mode = element("message-kind").value;
964
- const author = ["moderation", "receipt"].includes(mode) ? "用户" : element("author-select").value;
948
+ const author = mode === "moderation" ? "用户" : element("author-select").value;
965
949
  if (author !== "用户" && state.room.experts?.[author] !== "online") throw new Error("本地 AI 须自行提交回答;网页仅代发在线成员的真实原文");
966
950
  const payload = { author, text, reply_to: state.reply_to, turn_id: state.turn_reply_to,
967
- kind: ["moderation", "receipt"].includes(mode) ? mode : "opinion", submitted_by: "用户" };
951
+ kind: mode === "moderation" ? mode : "opinion", submitted_by: "用户" };
952
+ // @mentions of active members outside code are direct questions; the website queues them beside the discussion.
953
+ if (payload.kind === "opinion" && payload.author === "用户") {
954
+ const named = (state.room.members || []).filter(member => member.status === "active" && member.name !== "用户" && mentioned(text, member.name)).map(member => member.name);
955
+ if (named.length) payload.to = named;
956
+ }
968
957
  if (payload.kind === "opinion" && payload.author !== "用户") {
969
958
  payload.summary = element("message-summary").value.trim();
970
959
  payload.summary_by = element("message-summary-source").value === "submitter" ? payload.submitted_by : payload.author;
@@ -1002,6 +991,7 @@ function reset_create_form() {
1002
991
  state.launch.workspace = {};
1003
992
  state.launch.scheduler = {};
1004
993
  state.named_followup = false;
994
+ state.create_discussion = false;
1005
995
  state.launch.profiles = JSON.parse(JSON.stringify(state.launch.saved_profiles || {}));
1006
996
  Object.values(state.launch.profiles).forEach(fill_api_key);
1007
997
  state.launch.notice = "";
@@ -1427,9 +1417,9 @@ function render_access() {
1427
1417
 
1428
1418
  /** Read numeric limits from the shared classic/modern form controls. */
1429
1419
  function read_limits(prefix) {
1430
- return Object.fromEntries(["max_rounds", "max_minutes", "execution_minutes", "on_limit", "summary_minutes"].map(key => {
1420
+ return Object.fromEntries(["max_rounds", "max_minutes", "execution_minutes"].map(key => {
1431
1421
  const value = element(`${prefix}-limit-${key}${prefix === "topic" ? "-input" : ""}`).value;
1432
- return [key, key === "on_limit" ? value : Number(value)];
1422
+ return [key, Number(value)];
1433
1423
  }));
1434
1424
  }
1435
1425
 
@@ -1447,9 +1437,8 @@ function limits_label(limits = {}) {
1447
1437
  const rounds = limits.round_cap || limits.max_rounds ? limit_rounds(limits).text : `${limits.completed_rounds || 0} 轮 · 未设轮次上限`;
1448
1438
  const duration = limits.max_minutes ? `总时长 ${limits.max_minutes} 分钟` : "未设总时长";
1449
1439
  const progress = finished ? "讨论已收束" : !limits.started_at ? "尚未计时" : `已用 ${Math.floor((limits.elapsed_seconds || 0) / 60)} 分钟${limits.remaining_seconds == null ? "" : ` · 剩余 ${Math.ceil(limits.remaining_seconds / 60)} 分钟`}`;
1450
- const status = finished ? "" : limits.closing ? "正在收束" : limits.reached ? "已达上限,停止新增安排" : "";
1451
- const summary = limits.max_minutes || limits.deadline ? `总时长内预留 ${limits.summary_minutes} 分钟` : `收束最多 ${limits.summary_minutes} 分钟`;
1452
- const action = limits.on_limit === "summarize" ? `主持人总结 · ${summary}` : "到限暂停";
1440
+ const status = finished ? "" : limits.reached ? "已达上限,停止新增安排" : "";
1441
+ const action = "到限暂停";
1453
1442
  const execution = limits.execution_minutes ? `${limits.execution_minutes} 分钟` : "不限时长";
1454
1443
  return `${rounds};${duration};${progress}。单次自动执行 ${execution}。${action}${status ? `;${status}` : ""}。`;
1455
1444
  }
@@ -1534,11 +1523,17 @@ async function submit_room(event, folder_id = null) {
1534
1523
  const payload = room_creation_payload(folder_id);
1535
1524
  const free = payload.mode === "free";
1536
1525
  // A discussion can still be created without the local service and started later from the room;
1537
- // a free room needs the service only for members it wakes.
1538
- const launching = free ? Object.values(payload.experts).includes("cli") : state.launch.create && state.launch.available;
1526
+ // a free room needs the service only for members it wakes or for smart scheduling.
1527
+ const launching = free ? Object.values(payload.experts).includes("cli") || (state.launch.available && Boolean(creation_launch_options().scheduler)) : state.launch.create && state.launch.available;
1539
1528
  if (launching && !state.launch.available) throw new Error("本机唤醒的成员需要连接本机服务;也可改为手动接入或网页代发");
1540
1529
  const launch_draft = launching ? creation_launch_options() : null;
1541
- const signature = JSON.stringify({ payload, launch_draft });
1530
+ // A discussion is only a way to push the room forward: the room is created as a free room, then opened with these limits.
1531
+ const discussion = free && state.create_discussion ? { goal: payload.goal.trim(), named_followup: state.named_followup,
1532
+ limits: read_limits("new-room") } : null;
1533
+ if (discussion && !(launching && payload.host && payload.experts[payload.host] === "cli")) throw new Error("开启讨论需要一位由本机唤醒的 AI 主持人;请先添加并设为主持人");
1534
+ // The key joins the signature only when a discussion is requested, so recovered requests keep matching their stored signature.
1535
+ const signed = (current_payload, current_draft) => JSON.stringify(discussion ? { payload: current_payload, launch_draft: current_draft, discussion } : { payload: current_payload, launch_draft: current_draft });
1536
+ const signature = signed(payload, launch_draft);
1542
1537
  const navigation = state.navigation_version;
1543
1538
  if (!launching) element("room-dialog").close();
1544
1539
  state.launch.error = "";
@@ -1564,7 +1559,8 @@ async function submit_room(event, folder_id = null) {
1564
1559
  }
1565
1560
  state.pending_room = null;
1566
1561
  state.create_parent = null;
1567
- if (JSON.stringify({ payload: room_creation_payload(folder_id), launch_draft: launching ? creation_launch_options() : null }) === signature) {
1562
+ if (discussion) await open_discussion_after_creation(room.id, discussion);
1563
+ if (signed(room_creation_payload(folder_id), launching ? creation_launch_options() : null) === signature) {
1568
1564
  element("room-form").reset();
1569
1565
  reset_creation_mode();
1570
1566
  render_expert_inputs("new-expert-modes", default_experts());
@@ -1575,6 +1571,18 @@ async function submit_room(event, folder_id = null) {
1575
1571
  if (state.navigation_version === navigation) { state.archive_view = false; await select_room(room.id); }
1576
1572
  }
1577
1573
 
1574
+ /** Open and start the discussion of a room just created; a failure leaves the room as an ordinary free room. */
1575
+ async function open_discussion_after_creation(room_id, { goal, named_followup, limits }) {
1576
+ const path = `/api/rooms/${room_id}`;
1577
+ try {
1578
+ await request_json(`${path}/discussion`, { action: "start", ...(goal ? { goal } : {}), limits, named_followup });
1579
+ const runtime = await request_json(`${path}/runtime`);
1580
+ await request_json(`${path}/runtime`, { action: "start", mode: "automatic", revision: runtime.revision, deadline: 0 });
1581
+ } catch (error) {
1582
+ show_toast(`房间已创建,但开启讨论失败:${error.message};可在右侧成员栏重试`);
1583
+ }
1584
+ }
1585
+
1578
1586
  /** Read a site-bound connection without exposing its credential to the shared view snapshot. */
1579
1587
  function restore_local_connection() {
1580
1588
  const fragment = new URLSearchParams(location.hash?.slice(1) || "");
@@ -1939,6 +1947,12 @@ function write_named_followup(value) {
1939
1947
  publish_view();
1940
1948
  }
1941
1949
 
1950
+ /** Choose whether a new room opens a discussion right away; the room itself is always the same collaboration room. */
1951
+ function write_create_discussion(value) {
1952
+ state.create_discussion = Boolean(value);
1953
+ publish_view();
1954
+ }
1955
+
1942
1956
  /** Change the smart scheduling draft; a new provider starts from its preset model and any key saved for its root. */
1943
1957
  function write_scheduler(field, value) {
1944
1958
  const scheduler = state.launch.scheduler;
@@ -2333,8 +2347,6 @@ async function skip_turn() {
2333
2347
  const revision = runtime.revision;
2334
2348
  let saved = false;
2335
2349
  try {
2336
- const floor = room.floor;
2337
- if (floor.holder !== turn.expert) await request_json(`${path}/floor`, { actor: "用户", speaker: turn.expert, action: "take", revision: floor.revision });
2338
2350
  await request_json(`${path}/messages`, { author: turn.expert, submitted_by: "用户", kind: "opinion", turn_id: turn.id,
2339
2351
  text: "(本次发言已被用户跳过)", submission_id: new_submission_id() });
2340
2352
  saved = true;
@@ -2364,14 +2376,6 @@ async function extend_rounds(rounds) {
2364
2376
  show_toast(`已追加 ${rounds} 轮并重启讨论`);
2365
2377
  }
2366
2378
 
2367
- /** Ask the host for a summary of the paused discussion; it ends in the concluded state. */
2368
- async function request_summary() {
2369
- await request_json(`/api/rooms/${state.room.id}/limits`, { actor: "用户", action: "summarize" });
2370
- await resume_after_limit_decision();
2371
- await refresh_room();
2372
- show_toast("已请主持人总结,总结后讨论收束");
2373
- }
2374
-
2375
2379
  /** Reopen a concluded discussion with a fresh round and time budget; history stays. */
2376
2380
  async function reopen_discussion({ max_rounds, max_minutes }) {
2377
2381
  await request_json(`/api/rooms/${state.room.id}/limits`, { actor: "用户", action: "reopen", max_rounds, max_minutes });
@@ -2380,13 +2384,6 @@ async function reopen_discussion({ max_rounds, max_minutes }) {
2380
2384
  show_toast("讨论已重启,按新的轮次与时长上限继续");
2381
2385
  }
2382
2386
 
2383
- /** End early with a host summary limited to the configured closing reserve. */
2384
- async function force_close() {
2385
- await request_json(`/api/rooms/${state.room.id}/limits`, { actor: "用户", force: true });
2386
- await refresh_room();
2387
- show_toast("已取消当前安排,等待主持人总结收尾");
2388
- }
2389
-
2390
2387
  /** End early without a summary, keeping every original message. */
2391
2388
  async function conclude_discussion() {
2392
2389
  const turn = state.room.current_turn;
@@ -2398,6 +2395,24 @@ async function conclude_discussion() {
2398
2395
  show_toast("讨论已直接收束,历史记录保留");
2399
2396
  }
2400
2397
 
2398
+ /** Turn this free room into a discussion within the given limits and start the run; the local service takes it over at its next check. */
2399
+ async function start_room_discussion({ goal, max_rounds, max_minutes }) {
2400
+ const room_id = state.room.id, path = `/api/rooms/${room_id}`;
2401
+ if (!localStorage.getItem(`discuss-access:${room_id}`)) throw new Error("此浏览器缺少房间控制凭据,请使用创建房间的浏览器");
2402
+ await request_json(`${path}/discussion`, { action: "start", ...(goal ? { goal } : {}), limits: { max_rounds, max_minutes } });
2403
+ const runtime = await request_json(`${path}/runtime`);
2404
+ await request_json(`${path}/runtime`, { action: "start", mode: "automatic", revision: runtime.revision, deadline: 0 });
2405
+ await refresh_room();
2406
+ show_toast("讨论已开启,由主持人推进;需本机接入程序保持运行");
2407
+ }
2408
+
2409
+ /** Conclude the discussion and go on as a free room; every message and the conclusion stay. */
2410
+ async function end_room_discussion() {
2411
+ await request_json(`/api/rooms/${state.room.id}/discussion`, { action: "end" });
2412
+ await refresh_room();
2413
+ show_toast("讨论已结束,房间回到自由协作,历史记录保留");
2414
+ }
2415
+
2401
2416
  /** Revoke active execution before the local adapters are stopped. */
2402
2417
  async function pause_runtime() {
2403
2418
  await request_json(`/api/rooms/${state.room.id}/runtime`, { action: "pause", revision: state.room.runtime.revision });
@@ -2540,7 +2555,7 @@ async function adopt_recommendation({ message_id, mode }) {
2540
2555
 
2541
2556
  /** A completed creation starts the next form with fresh mode defaults. */
2542
2557
  function reset_creation_mode() {
2543
- state.creation_mode = "brainstorm";
2558
+ state.creation_mode = "free";
2544
2559
  state.creation_drafts = {};
2545
2560
  }
2546
2561
 
@@ -2574,9 +2589,9 @@ function render_topic() {
2574
2589
  for (const field of ["title", "goal", "focus", "user_request", "background", "boundaries"]) element(`topic-${field}-input`).value = room[field] || "";
2575
2590
  render_host_options("topic-host-select", Object.keys(room.experts).filter(name => room.experts[name] === "cli"), room.host);
2576
2591
  element("topic-phase-select").value = room.phase;
2577
- for (const key of ["max_rounds", "max_minutes", "execution_minutes", "on_limit", "summary_minutes"]) {
2592
+ for (const key of ["max_rounds", "max_minutes", "execution_minutes"]) {
2578
2593
  const input = element(`topic-limit-${key}-input`);
2579
- input.value = room.limits?.[key] || (key === "on_limit" ? "pause" : "");
2594
+ input.value = room.limits?.[key] || "";
2580
2595
  input.disabled = !["preparing", "paused"].includes(room.phase);
2581
2596
  }
2582
2597
  render_expert_inputs("topic-expert-modes", room.experts);
@@ -2589,11 +2604,6 @@ function render_topic() {
2589
2604
 
2590
2605
  /** Display the actual assignment state without implying an external delivery. */
2591
2606
  function render_current_turn() {
2592
- const floor = state.room.floor;
2593
- element("floor-status").textContent = floor_status(state.room);
2594
- element("take-floor").disabled = !floor || Boolean(floor.holder);
2595
- element("take-floor").textContent = `为 ${display_name(floor?.next_speaker || state.room.host)} 领取`;
2596
- element("return-floor").disabled = !floor?.holder;
2597
2607
  const turn = state.room.current_turn;
2598
2608
  const active = turn && ["pending", "notified"].includes(turn.status);
2599
2609
  const inactive = ["paused", "concluded"].includes(state.room.phase);
@@ -2607,20 +2617,10 @@ function render_current_turn() {
2607
2617
  element("cancel-turn").disabled = !active;
2608
2618
  element("view-turn-answer").hidden = !turn?.answer_id;
2609
2619
  const limited = state.room.limits?.reached;
2610
- const unavailable = active || inactive || limited || !Object.keys(state.room.experts).length || floor?.holder !== state.room.host;
2620
+ const unavailable = active || inactive || limited || !Object.keys(state.room.experts).length;
2611
2621
  element("schedule-turn").disabled = Boolean(unavailable);
2612
2622
  element("schedule-note").textContent = inactive ? "话题已暂停或收束;恢复阶段后才能继续安排。" :
2613
- (active ? "先完成或取消当前安排,再安排下一位。" : limited ? "已达到讨论上限,请检查到限处理;需要继续时先暂停并调整参数。" : floor?.holder !== state.room.host ? "主持人领取发言权后可安排下一位。" : "创建后本地专家可自行检查;在线专家需由主持人或用户联系。");
2614
- }
2615
-
2616
- /** Perform one explicit, versioned floor operation on behalf of its speaker. */
2617
- async function change_floor(action) {
2618
- const floor = state.room.floor;
2619
- const speaker = action === "take" ? floor.next_speaker : floor.holder;
2620
- try {
2621
- await request_json(`/api/rooms/${state.room.id}/floor`, { actor: "用户", speaker, action, revision: floor.revision });
2622
- show_toast(action === "take" ? `${display_name(speaker)} 已领取发言权` : "发言权已归还");
2623
- } finally { await refresh_room(); }
2623
+ (active ? "先完成或取消当前安排,再安排下一位。" : limited ? "已达到讨论上限,请检查到限处理;需要继续时先暂停并调整参数。" : "创建后本地专家可自行检查;在线专家需由主持人或用户联系。");
2624
2624
  }
2625
2625
 
2626
2626
  /** Distinguish local work discovery from contacting an online expert. */
@@ -2719,12 +2719,12 @@ function cancel_turn_reply() {
2719
2719
  /** Keep moderator statements distinct from opinions in the same identity. */
2720
2720
  function update_composer_role() {
2721
2721
  const mode = element("message-kind").value;
2722
- element("author-select").disabled = ["moderation", "receipt"].includes(mode);
2723
- if (["moderation", "receipt"].includes(mode)) element("author-select").value = "用户";
2722
+ element("author-select").disabled = mode === "moderation";
2723
+ if (mode === "moderation") element("author-select").value = "用户";
2724
2724
  const needs_summary = ["opinion", "forward"].includes(mode) && element("author-select").value !== "用户";
2725
2725
  element("message-summary-fields").hidden = !needs_summary;
2726
2726
  element("message-summary").required = needs_summary;
2727
- element("author-note").textContent = mode === "receipt" ? "运行回执,由用户录入" : mode === "moderation" ? "主持发言" :
2727
+ element("author-note").textContent = mode === "moderation" ? "主持发言" :
2728
2728
  (mode === "forward" ? "由用户代发" : (element("author-select").value === "用户" ? "发言" : "的观点代发"));
2729
2729
  }
2730
2730
 
@@ -2738,7 +2738,6 @@ function bind_host_controls() {
2738
2738
  "local-room-configuration-load": load_room_configuration, "local-room-configuration-save": save_room_configuration,
2739
2739
  "local-room-configuration-restart": restart_discussion,
2740
2740
  "runtime-pause": pause_runtime, "check-discussion-limits": check_limits,
2741
- "take-floor": () => change_floor("take"), "return-floor": () => change_floor("return"),
2742
2741
  "copy-turn": copy_turn, "confirm-notified": () => change_turn_status("notified"), "cancel-turn": () => change_turn_status("cancelled"),
2743
2742
  "forward-turn": prepare_turn_answer, "cancel-turn-answer": cancel_turn_reply,
2744
2743
  "view-turn-answer": () => show_message(state.room.current_turn.answer_id),
@@ -2758,10 +2757,6 @@ async function run_action(action) {
2758
2757
 
2759
2758
  /** Delegate room and message actions to their actual visible controls. */
2760
2759
  function bind_delegated_actions() {
2761
- element("room-content").addEventListener("click", (event) => {
2762
- const receipt = event.target.closest("[data-receipt-detail]");
2763
- if (receipt) run_action(() => show_message(receipt.dataset.receiptDetail));
2764
- });
2765
2760
  element("room-list").addEventListener("click", (event) => {
2766
2761
  const management = event.target.closest("[data-manage-room]");
2767
2762
  if (management) {
@@ -3148,7 +3143,7 @@ function view_snapshot() {
3148
3143
  free_notice: state.free_notice, free_error: state.free_error, free_proxy: state.free_proxy,
3149
3144
  create_parent: state.create_parent, access: access_snapshot(), admin: admin_snapshot(),
3150
3145
  tab: state.tab, snapshot_id: state.snapshot_id, context_dirty: state.context_dirty,
3151
- theme: document.documentElement.dataset.theme, fields, experts, launch: structuredClone(state.launch), named_followup: state.named_followup };
3146
+ theme: document.documentElement.dataset.theme, fields, experts, launch: structuredClone(state.launch), named_followup: state.named_followup, create_discussion: state.create_discussion };
3152
3147
  }
3153
3148
 
3154
3149
  /** Update a shared draft using the same input events as the original controls. */
@@ -3315,9 +3310,8 @@ async function execute_view_action(name, value) {
3315
3310
  schedule: () => schedule_turn(event), filter: filter_view, reply: set_view_reply,
3316
3311
  cancel_reply, cancel_answer: cancel_turn_reply, prepare_answer: prepare_turn_answer,
3317
3312
  copy_turn, notify: () => change_turn_status("notified"), cancel_turn: () => change_turn_status("cancelled"), skip_turn,
3318
- take_floor: () => change_floor("take"), return_floor: () => change_floor("return"),
3319
3313
  copy_online: () => copy_context(), copy_cli: () => copy_context("cli"), copy_background,
3320
- choose_runtime, export_runtime, download_member_access, copy_member_access_text, copy_member_join, copy_member_launch: name => copy_member_join(name, true), pause_runtime, check_limits, pause_discussion, restart_discussion, retry_execution, force_close, conclude_discussion, extend_rounds, request_summary, reopen_discussion,
3314
+ choose_runtime, export_runtime, download_member_access, copy_member_access_text, copy_member_join, copy_member_launch: name => copy_member_join(name, true), pause_runtime, check_limits, pause_discussion, restart_discussion, retry_execution, conclude_discussion, start_room_discussion, end_room_discussion, extend_rounds, reopen_discussion,
3321
3315
  save_launch_defaults, launch_current_room, resume_launch, discard_launch, retry_launch_connection, check_launch_connection: load_launch_settings,
3322
3316
  load_room_configuration, save_room_configuration, interrupt_member, change_member, invite_member, set_proxy, copy_online_notice,
3323
3317
  prepare_brainstorm, return_conclusion, convert_suggestion, adopt_recommendation,
@@ -3343,7 +3337,7 @@ function change_view_theme(theme) {
3343
3337
 
3344
3338
  window.discuss_view = { snapshot: view_snapshot, write_field: write_view_field,
3345
3339
  write_launch: write_launch_field, write_room_connection, write_room_adapter, launch_clients: LAUNCH_CLIENTS,
3346
- launch_model_state, load_launch_models, write_launch_model, save_launch_api_key, write_scheduler, save_scheduler_api_key, check_scheduler, write_named_followup, check_launch_api, launch_summary, launch_can_set_access, launch_can_bind_session, pick_launch_directory, restore_launch_profile, reset_create_form, empty_discussion_message,
3340
+ 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, check_launch_api, launch_summary, launch_can_set_access, launch_can_bind_session, pick_launch_directory, restore_launch_profile, reset_create_form, empty_discussion_message,
3347
3341
  start_drag: start_directory_drag,
3348
3342
  attachment_markup, attachment_size, attachment_file_icon, drop_attachments, paste_attachments,
3349
3343
  write_expert: write_view_expert, add_instance: add_view_instance, platform_of, execute: execute_view_action, theme: change_view_theme,
@@ -3351,7 +3345,7 @@ window.discuss_view = { snapshot: view_snapshot, write_field: write_view_field,
3351
3345
  update_profile: update_admin_profile, avatar_from_file: admin_avatar_from_file, validate: validate_admin_form },
3352
3346
  request: request_json, identities: IDENTITIES, display_name, sources: SOURCE_LABELS, phases: PHASE_LABELS,
3353
3347
  runtime_label, execution_label, limits_label, participant_model, participant_access, can_retry_execution,
3354
- turns: TURN_LABELS, turn_label, turn_guidance, floor_status, progress_round, round_groups, limit_rounds, progress_speaker, brief_meta, max_message_length: MAX_MESSAGE_LENGTH, provenance: message_provenance, format_time };
3348
+ turns: TURN_LABELS, turn_label, turn_guidance, progress_round, round_groups, limit_rounds, progress_speaker, brief_meta, max_message_length: MAX_MESSAGE_LENGTH, provenance: message_provenance, format_time };
3355
3349
 
3356
3350
  restore_theme();
3357
3351
  restore_directory_sort();
package/web/backup.js CHANGED
@@ -15,7 +15,7 @@ var discuss_backup=(function(e){Object.defineProperty(e,Symbol.toStringTag,{valu
15
15
  在任一共议网站的房间目录选“导入房间”并选择本 ZIP,可一次导入全部房间;已有同一房间时跳过。
16
16
  `;function $(e){let t=e??``,n=Math.max(2,...(t.match(/`+/g)||[]).map(e=>e.length)),r="`".repeat(n+1);return`${r}text\n${t}\n${r}`}function Me(e,t,n,r){let i=t?.submitted_by||([`cli`,`api`,`url`,`user`].includes(e.source)?e.author:`用户`),a=[`<a id="message-${e.id}"></a>`,`### #${e.id} · ${e.author}`,`时间:${e.created_at};来源:${e.source};职责:${t?.kind||`opinion`};代发/提交者:${i}`];e.reply_to&&a.push(`回复:[#${e.reply_to}](#message-${e.reply_to})`),t?.turn_id&&a.push(`对应安排:[#${t.turn_id}](#turn-${t.turn_id})`),n?.text&&a.push(`简述(作者:${n.author}):`,$(n.text)),a.push(`原文:`,$(e.text));for(let e of r){let t=Pe(e).split(`/`).map(e=>encodeURIComponent(e).replace(/[()]/g,e=>`%${e.charCodeAt(0).toString(16)}`)).join(`/`);a.push(`附件:${$(e.file_name)}\n\n[下载原文件](${t}) · ${e.size} 字节 · SHA-256: ${e.sha256}`)}return a.join(`
17
17
 
18
- `)}function Ne(e){let t=e.tables,n=t.rooms[0],r=t.topic_settings[0],i=t.room_user_requests?.[0],a=!i&&r?.background.match(/^用户原文[::][ \t]*(?:\r?\n)?([\s\S]*?)(?:\r?\n){2,}((?:项目背景与入口|背景)[::][\s\S]*)$/),o=i?.text??(a?a[1]:``),s=a?a[2]:r?.background,c=t.room_briefs[0],l=new Map(t.message_details.map(e=>[e.message_id,e])),u=new Map(t.message_summaries.map(e=>[e.message_id,e])),d=[`# 讨论记录`,$(n.title),`房间:${n.id};创建:${n.created_at};导出:${e.exported_at}`,`来源:${e.source_url||`未记录`}`,`消息:${t.messages.length} 条;发言安排:${t.discussion_turns.length} 项`,`目录状态:${t.room_directory[0]?.archived?`已存档`:`使用中`};文件夹:${t.topic_folders[0]?.name||`未分组`}`,`## 目标`,$(n.goal)];r&&d.push(`主持人:${r.host};阶段:${r.phase}`,`参与配置:`,$(r.experts),`## 用户原文`,$(o),`## 背景`,$(s),`## 讨论边界`,$(r.boundaries),`## 当前焦点`,$(r.focus)),c&&d.push(`## 主持简报`,`覆盖至 #${c.until};更新:${c.updated_at}`,$(c.text)),t.discussion_limits?.[0]&&d.push(`## 讨论上限`,$(JSON.stringify(t.discussion_limits[0],null,2))),d.push(`## 共识与分歧`,`覆盖至 #${n.state_until}`,$(n.state_text),`## 发言权`,$(JSON.stringify(t.room_floor[0]||{},null,2)),`## 全部消息`);for(let e of t.messages)d.push(Me(e,l.get(e.id),u.get(e.id),(t.attachments||[]).filter(t=>t.message_id===e.id)));t.messages.length||d.push(`暂无消息。`),d.push(`## 全部发言安排`);for(let e of t.discussion_turns)d.push(`<a id="turn-${e.id}"></a>`,`### 安排 #${e.id} · ${e.expert}`,`状态:${e.status};创建:${e.created_at};通知:${e.notified_at||`未记录`}`,$(e.question)),e.answer_id&&d.push(`回答:[#${e.answer_id}](#message-${e.answer_id})`);return t.discussion_turns.length||d.push(`暂无发言安排。`),d.join(`
18
+ `)}function Ne(e){let t=e.tables,n=t.rooms[0],r=t.topic_settings[0],i=t.room_user_requests?.[0],a=!i&&r?.background.match(/^用户原文[::][ \t]*(?:\r?\n)?([\s\S]*?)(?:\r?\n){2,}((?:项目背景与入口|背景)[::][\s\S]*)$/),o=i?.text??(a?a[1]:``),s=a?a[2]:r?.background,c=t.room_briefs[0],l=new Map(t.message_details.map(e=>[e.message_id,e])),u=new Map(t.message_summaries.map(e=>[e.message_id,e])),d=[`# 讨论记录`,$(n.title),`房间:${n.id};创建:${n.created_at};导出:${e.exported_at}`,`来源:${e.source_url||`未记录`}`,`消息:${t.messages.length} 条;发言安排:${t.discussion_turns.length} 项`,`目录状态:${t.room_directory[0]?.archived?`已存档`:`使用中`};文件夹:${t.topic_folders[0]?.name||`未分组`}`,`## 目标`,$(n.goal)];r&&d.push(`主持人:${r.host};阶段:${r.phase}`,`参与配置:`,$(r.experts),`## 用户原文`,$(o),`## 背景`,$(s),`## 讨论边界`,$(r.boundaries),`## 当前焦点`,$(r.focus)),c&&d.push(`## 主持简报`,`覆盖至 #${c.until};更新:${c.updated_at}`,$(c.text)),t.discussion_limits?.[0]&&d.push(`## 讨论上限`,$(JSON.stringify(t.discussion_limits[0],null,2))),d.push(`## 共识与分歧`,`覆盖至 #${n.state_until}`,$(n.state_text),`## 全部消息`);for(let e of t.messages)d.push(Me(e,l.get(e.id),u.get(e.id),(t.attachments||[]).filter(t=>t.message_id===e.id)));t.messages.length||d.push(`暂无消息。`),d.push(`## 全部发言安排`);for(let e of t.discussion_turns)d.push(`<a id="turn-${e.id}"></a>`,`### 安排 #${e.id} · ${e.expert}`,`状态:${e.status};创建:${e.created_at};通知:${e.notified_at||`未记录`}`,$(e.question)),e.answer_id&&d.push(`回答:[#${e.answer_id}](#message-${e.answer_id})`);return t.discussion_turns.length||d.push(`暂无发言安排。`),d.join(`
19
19
 
20
20
  `)+`
21
21
  `}function Pe(e){let t=e.file_name.replace(/[<>:"/\\|?*\u0000-\u001f]/g,`_`).replace(/[. ]+$/,``)||`附件`;return`附件/${e.id}_${t}`}async function Fe(e){let t=await fetch(`/api/rooms/${encodeURIComponent(e.room_id)}/attachments/${encodeURIComponent(e.id)}/content?download=1`);if(!t.ok)throw Error(`附件下载失败:${e.file_name},未生成备份`);let n=new Uint8Array(await t.arrayBuffer());if(n.length!==e.size)throw Error(`附件不完整:${e.file_name},未生成备份`);if(globalThis.crypto?.subtle&&[...new Uint8Array(await crypto.subtle.digest(`SHA-256`,n))].map(e=>e.toString(16).padStart(2,`0`)).join(``)!==e.sha256)throw Error(`附件校验失败:${e.file_name},未生成备份`);return n}async function Ie(e,t,n,r){e[`${n}完整数据.json`]=_e(JSON.stringify(t,null,2)+`
package/web/index.html CHANGED
@@ -49,16 +49,14 @@
49
49
  <section class="panel topic-panel" data-panel="topic">
50
50
  <div class="section-intro"><div><span class="eyebrow">话题概览 / TOPIC</span><h2 id="topic-phase">准备中</h2><p id="topic-host"></p></div><button class="button secondary" id="copy-host">复制主持人入口</button></div>
51
51
  <div class="topic-overview"><h3>本次目标</h3><p id="topic-goal"></p><h3>当前焦点</h3><p id="topic-focus"></p></div>
52
- <div class="turn-card"><div class="state-card-header"><h3>发言权</h3><span id="floor-status" class="muted-chip"></span></div><div class="turn-content"><div class="topic-actions"><button class="button secondary" id="take-floor">领取发言权</button><button class="button secondary" id="return-floor">归还发言权</button></div><p class="local-note">发言或安排保存后自动归还;用户本人可随时留言。</p></div></div>
53
52
  <div class="turn-card"><div class="state-card-header"><h3>当前发言安排</h3><span id="turn-status" class="muted-chip"></span></div><div class="turn-content"><p id="turn-expert"></p><p id="turn-question"></p><div class="topic-actions"><button class="button secondary" id="copy-turn">复制发言通知</button><button class="button secondary" id="confirm-notified">确认已通知</button><button class="button primary" id="forward-turn">录入专家回答</button><button class="text-button" id="view-turn-answer" hidden>查看回答原文</button><button class="text-button" id="cancel-turn">取消安排</button></div><p class="local-note" id="turn-note"></p></div></div>
54
- <section id="topic-receipts" class="topic-reading" hidden><h3>最新运行回执</h3><p>每位参与者最近一条;实际运行结果请查看原文。晚于摘要的回执可能尚未整理。</p><div id="topic-receipts-links" class="topic-actions"></div></section>
55
- <div class="topic-reading"><h3>选择需要查看的内容</h3><p>不必从头读取全部讨论;按问题选择材料,再追溯原文。</p><div class="topic-actions"><button class="button secondary" id="topic-context">共识与分歧</button><button class="button secondary" id="topic-history">检索与完整记录</button><a class="text-button" id="turn-history" target="_blank" rel="noopener">发言安排历史 &gt;&gt;</a></div></div>
53
+ <div class="topic-reading"><h3>选择需要查看的内容</h3><p>不必从头读取全部讨论;按问题选择材料,再追溯原文。</p><div class="topic-actions"><button class="button secondary" id="topic-context">共识与分歧</button><button class="button secondary" id="topic-history">检索与完整记录</button><a class="text-button" id="turn-history" target="_blank" rel="noopener">发言安排历史 &gt;&gt;</a></div></div>
56
54
  <details class="topic-details"><summary>用户原文、背景与讨论边界</summary><div class="topic-detail-content"><h3>用户原文</h3><p id="topic-user_request"></p><h3>背景</h3><p id="topic-background"></p><h3>边界</h3><p id="topic-boundaries"></p></div></details>
57
55
  <details class="topic-details"><summary>参与专家与读写方式</summary><div id="topic-experts" class="topic-detail-content"></div></details>
58
56
  <details class="topic-details" id="host-management"><summary>主持管理 · 修改话题与安排发言</summary><div class="topic-detail-content">
59
57
  <p class="local-note" id="host-operator"></p>
60
58
  <form id="turn-form" class="topic-form"><h3>安排下一位专家</h3><label>发言专家<select id="turn-expert-select"></select></label><label>需要回应的问题<textarea id="turn-question-input" maxlength="1000" rows="3" required placeholder="只写本轮需要回答的问题,不粘贴上下文。"></textarea></label><button class="button primary" id="schedule-turn" type="submit">创建发言安排</button><p id="schedule-note"></p></form>
61
- <section class="info-card"><h3>讨论上限与进度</h3><p id="discussion-limits-status"></p><button id="check-discussion-limits" class="button secondary" type="button">检查并执行到限处理</button></section><form id="topic-form" class="topic-form"><h3>话题资料与进度</h3><div class="topic-form-grid"><label>话题名称<input id="topic-title-input" maxlength="80" required></label><label>主持人<select id="topic-host-select"></select></label><label>阶段<select id="topic-phase-select"><option value="preparing">准备中</option><option value="initial">独立观点</option><option value="discussing">深入讨论</option><option value="reviewing">结论复核</option><option value="paused">已暂停</option><option value="concluded">已收束</option></select></label><label>当前焦点<input id="topic-focus-input" maxlength="1000"></label></div><label>议题目标<textarea id="topic-goal-input" maxlength="1000" rows="2"></textarea></label><label>用户原文<textarea id="topic-user_request-input" maxlength="4000" rows="3"></textarea></label><label>背景<textarea id="topic-background-input" maxlength="4000" rows="3"></textarea></label><label>讨论边界<textarea id="topic-boundaries-input" maxlength="4000" rows="3"></textarea></label><fieldset><legend>讨论上限</legend><div class="topic-form-grid"><label>讨论轮次上限<input id="topic-limit-max_rounds-input" type="number" min="0" max="100" step="1" value="3" placeholder="留空不限"></label><label>讨论总时长(分钟)<input id="topic-limit-max_minutes-input" type="number" min="0" max="1440" step="1" value="60" placeholder="留空不限"></label><label>单次 AI 执行上限(分钟,仅自动)<input id="topic-limit-execution_minutes-input" type="number" min="0" max="60" step="1" value="0" placeholder="留空不限"></label><label>收束预留(分钟,仅总结时)<input id="topic-limit-summary_minutes-input" type="number" min="1" max="60" step="1" value="5" placeholder="留空不限"></label><label>达到上限后<select id="topic-limit-on_limit-input"><option value="pause">暂停保留现场</option><option value="summarize">由主持人总结</option></select></label></div><p class="local-note">轮次、总时长、单次执行留空或 0 表示不限;首次进入讨论阶段或创建安排后开始计时,暂停期间不计时。总结预留计入总时长。一轮指每位参与专家各完成一次观点。</p></fieldset><fieldset><legend>参与专家</legend><div id="topic-expert-modes" class="expert-mode-grid"></div></fieldset><p class="local-note">AI 主持人须配置为本地 CLI,可同时作为专家。主持发言与个人观点分别记录。</p><button class="button primary" type="submit">保存话题设置</button></form>
59
+ <section class="info-card"><h3>讨论上限与进度</h3><p id="discussion-limits-status"></p><button id="check-discussion-limits" class="button secondary" type="button">检查并执行到限处理</button></section><form id="topic-form" class="topic-form"><h3>话题资料与进度</h3><div class="topic-form-grid"><label>话题名称<input id="topic-title-input" maxlength="80" required></label><label>主持人<select id="topic-host-select"></select></label><label>阶段<select id="topic-phase-select"><option value="preparing">准备中</option><option value="initial">独立观点</option><option value="discussing">深入讨论</option><option value="reviewing">结论复核</option><option value="paused">已暂停</option><option value="concluded">已收束</option></select></label><label>当前焦点<input id="topic-focus-input" maxlength="1000"></label></div><label>议题目标<textarea id="topic-goal-input" maxlength="1000" rows="2"></textarea></label><label>用户原文<textarea id="topic-user_request-input" maxlength="4000" rows="3"></textarea></label><label>背景<textarea id="topic-background-input" maxlength="4000" rows="3"></textarea></label><label>讨论边界<textarea id="topic-boundaries-input" maxlength="4000" rows="3"></textarea></label><fieldset><legend>讨论上限</legend><div class="topic-form-grid"><label>讨论轮次上限<input id="topic-limit-max_rounds-input" type="number" min="0" max="100" step="1" value="3" placeholder="留空不限"></label><label>讨论总时长(分钟)<input id="topic-limit-max_minutes-input" type="number" min="0" max="1440" step="1" value="60" placeholder="留空不限"></label><label>单次 AI 执行上限(分钟,仅自动)<input id="topic-limit-execution_minutes-input" type="number" min="0" max="60" step="1" value="0" placeholder="留空不限"></label></div><p class="local-note">轮次、总时长、单次执行留空或 0 表示不限;首次进入讨论阶段或创建安排后开始计时,暂停期间不计时;到限后暂停,由用户决定追加轮次或结束。一轮指每位参与专家各完成一次观点。</p></fieldset><fieldset><legend>参与专家</legend><div id="topic-expert-modes" class="expert-mode-grid"></div></fieldset><p class="local-note">AI 主持人须配置为本地 CLI,可同时作为专家。主持发言与个人观点分别记录。</p><button class="button primary" type="submit">保存话题设置</button></form>
62
60
  </div></details>
63
61
  </section>
64
62
  <section class="discussion-layout panel" data-panel="discussion" hidden>
@@ -71,7 +69,7 @@
71
69
  <div class="system-notice"><b>[ 参与方式 ]</b> 本地 CLI AI 可读可写;在线 AI 只读,观点由用户代发。用「回复」保留引用。</div>
72
70
  <div id="messages" class="messages" aria-live="polite" aria-label="聊天记录"></div>
73
71
  <form id="message-form" class="composer">
74
- <div class="composer-toolbar"><div class="author-picker"><label for="author-select">发言身份:</label><select id="author-select" aria-label="发言者"></select><span id="author-note">发言</span></div><select id="message-kind" aria-label="发言职责"><option value="opinion">用户发言 / 代发观点</option><option value="moderation">主持发言</option><option value="receipt">运行回执</option><option value="forward">主持人代发专家观点</option></select></div>
72
+ <div class="composer-toolbar"><div class="author-picker"><label for="author-select">发言身份:</label><select id="author-select" aria-label="发言者"></select><span id="author-note">发言</span></div><select id="message-kind" aria-label="发言职责"><option value="opinion">用户发言 / 代发观点</option><option value="moderation">主持发言</option><option value="forward">主持人代发专家观点</option></select></div>
75
73
  <div id="turn-answer-preview" class="reply-preview" hidden><span id="turn-answer-label"></span><button type="button" id="cancel-turn-answer" aria-label="取消关联发言安排">×</button></div>
76
74
  <div id="reply-preview" class="reply-preview" hidden><span id="reply-label"></span><button type="button" id="cancel-reply" aria-label="取消回复">×</button></div>
77
75
  <div id="message-summary-fields" class="message-summary-fields" hidden><label for="message-summary">回答摘要</label><textarea id="message-summary" rows="2" aria-label="回答摘要" placeholder="先说结论,保留关键理由、条件和不确定性。完整内容写在正文中。"></textarea><label>摘要提供者<select id="message-summary-source" aria-label="摘要提供者"><option value="author">原回答作者</option><option value="submitter">代发者整理</option></select></label></div>
@@ -87,7 +85,7 @@
87
85
  <div class="section-intro"><div><span class="eyebrow">房间记事本 / SHARED CONTEXT</span><h2>当前共识与分歧</h2><p>由用户或主持人整理,用 #编号 引用原始发言。</p></div><button class="button secondary" id="copy-context">复制给在线 AI</button></div>
88
86
  <div class="state-grid">
89
87
  <form id="state-form" class="state-card"><div class="state-card-header"><h3>主持简报</h3><span class="muted-chip">返回房间时优先展示</span></div><textarea id="brief-input" rows="4" aria-label="主持简报正文" placeholder="简述当前结论、共识、歧义,以及下一步准备追问或调整什么、为什么。"></textarea><div class="state-card-header"><h3>详细共识与进度</h3><span id="state-version" class="muted-chip"></span></div><textarea id="state-input" maxlength="4000" rows="10" aria-label="当前讨论状态" placeholder="已达成的共识…&#10;尚未解决的分歧…&#10;需要补充的证据…&#10;&#10;使用 #消息编号 引用原始依据。"></textarea><div class="state-footer"><small id="state-update-note"></small><button class="button primary" type="submit">保存状态</button></div></form>
90
- <div class="state-explanation"><h3>记事本说明</h3><p>这里保存主持人整理的结果,不会覆盖原始聊天记录。</p><section id="state-receipts" class="topic-reading" hidden><h3>最新运行回执</h3><p>以下回执独立于摘要更新;晚于摘要的回执可能尚未整理,实际运行结果请查看原文。</p><div id="state-receipts-links" class="topic-actions"></div></section><hr><h4>给参与者带上这些</h4><ul><li>议题目标与边界</li><li>当前共识、分歧和记录截止位置</li><li>与自己有关的 @ 消息</li><li>查阅完整历史的地址</li></ul><p class="soft-note">无法联网的 AI 可以接收复制的材料,再由用户协助检索和代发。</p></div>
88
+ <div class="state-explanation"><h3>记事本说明</h3><p>这里保存主持人整理的结果,不会覆盖原始聊天记录。</p><hr><h4>给参与者带上这些</h4><ul><li>议题目标与边界</li><li>当前共识、分歧和记录截止位置</li><li>与自己有关的 @ 消息</li><li>查阅完整历史的地址</li></ul><p class="soft-note">无法联网的 AI 可以接收复制的材料,再由用户协助检索和代发。</p></div>
91
89
  </div>
92
90
  </section>
93
91
  <section class="panel access-panel" data-panel="access" hidden>
@@ -111,7 +109,7 @@
111
109
  </div>
112
110
  <footer class="workspace-footer"><span>共议聊天室 <span class="footer-dot">·</span> 记录每一个不同的观点</span><span id="demo-note">示例发言为预置内容,未连接真实 AI</span></footer>
113
111
  </div>
114
- <dialog id="room-dialog"><form id="room-form"><div class="dialog-heading"><h2>新建话题</h2><button type="button" class="dialog-close" id="close-room-dialog" aria-label="关闭">×</button></div><div class="dialog-content"><label>话题名称<input id="new-room-title" maxlength="80" placeholder="例如:产品第一版应该保留哪些能力?" required></label><label>议题目标<textarea id="new-room-goal" maxlength="1000" rows="3" placeholder="本次讨论要解决什么问题?"></textarea></label><label>主持人<select id="new-room-host"></select></label><label>房间用法<select id="new-room-kind"><option value="brainstorm">讨论</option><option value="free">自由协作</option></select></label><fieldset><legend>讨论上限</legend><div class="topic-form-grid"><label>讨论轮次上限<input id="new-room-limit-max_rounds" type="number" min="0" max="100" step="1" value="3" placeholder="留空不限"></label><label>讨论总时长(分钟)<input id="new-room-limit-max_minutes" type="number" min="0" max="1440" step="1" value="0" placeholder="留空不限"></label><label>单次 AI 执行上限(分钟,仅自动)<input id="new-room-limit-execution_minutes" type="number" min="0" max="60" step="1" value="0" placeholder="留空不限"></label><label>收束预留(分钟,仅总结时)<input id="new-room-limit-summary_minutes" type="number" min="1" max="60" step="1" value="5" placeholder="留空不限"></label><label>达到上限后<select id="new-room-limit-on_limit"><option value="pause">暂停保留现场</option><option value="summarize">由主持人总结</option></select></label></div><p class="local-note">轮次、总时长、单次执行留空或 0 表示不限;首次进入讨论阶段或创建安排后开始计时,暂停期间不计时。总结预留计入总时长。一轮指每位参与专家各完成一次观点。</p></fieldset><details class="topic-details"><summary>用户原文、背景、边界与参与专家</summary><div class="topic-detail-content"><label>用户原文<textarea id="new-room-user_request" maxlength="4000" rows="3"></textarea></label><label>背景<textarea id="new-room-background" maxlength="4000" rows="3"></textarea></label><label>讨论边界<textarea id="new-room-boundaries" maxlength="4000" rows="3"></textarea></label><div id="new-expert-modes" class="expert-mode-grid"></div></div></details><p class="local-note">AI 主持人需选择本地 CLI 方式,也可同时作为专家参与。</p><fieldset><legend>网页启动入口</legend><label><input type="checkbox" data-launch-name="create" data-launch-field="">创建后立即启动 AI</label><div id="new-local-launch-settings" hidden></div><button type="button" id="new-local-launch-save" class="button secondary">保存本机软件配置</button><p id="new-local-launch-notice" role="status"></p></fieldset><button class="button primary full-width" type="submit">创建话题</button></div></form></dialog>
112
+ <dialog id="room-dialog"><form id="room-form"><div class="dialog-heading"><h2>新建话题</h2><button type="button" class="dialog-close" id="close-room-dialog" aria-label="关闭">×</button></div><div class="dialog-content"><label>话题名称<input id="new-room-title" maxlength="80" placeholder="例如:产品第一版应该保留哪些能力?" required></label><label>议题目标<textarea id="new-room-goal" maxlength="1000" rows="3" placeholder="本次讨论要解决什么问题?"></textarea></label><label>主持人<select id="new-room-host"></select></label><label>房间用法<select id="new-room-kind"><option value="free">自由协作</option><option value="brainstorm">讨论</option></select></label><fieldset><legend>讨论上限</legend><div class="topic-form-grid"><label>讨论轮次上限<input id="new-room-limit-max_rounds" type="number" min="0" max="100" step="1" value="3" placeholder="留空不限"></label><label>讨论总时长(分钟)<input id="new-room-limit-max_minutes" type="number" min="0" max="1440" step="1" value="0" placeholder="留空不限"></label><label>单次 AI 执行上限(分钟,仅自动)<input id="new-room-limit-execution_minutes" type="number" min="0" max="60" step="1" value="0" placeholder="留空不限"></label></div><p class="local-note">轮次、总时长、单次执行留空或 0 表示不限;首次进入讨论阶段或创建安排后开始计时,暂停期间不计时;到限后暂停,由用户决定追加轮次或结束。一轮指每位参与专家各完成一次观点。</p></fieldset><details class="topic-details"><summary>用户原文、背景、边界与参与专家</summary><div class="topic-detail-content"><label>用户原文<textarea id="new-room-user_request" maxlength="4000" rows="3"></textarea></label><label>背景<textarea id="new-room-background" maxlength="4000" rows="3"></textarea></label><label>讨论边界<textarea id="new-room-boundaries" maxlength="4000" rows="3"></textarea></label><div id="new-expert-modes" class="expert-mode-grid"></div></div></details><p class="local-note">AI 主持人需选择本地 CLI 方式,也可同时作为专家参与。</p><fieldset><legend>网页启动入口</legend><label><input type="checkbox" data-launch-name="create" data-launch-field="">创建后立即启动 AI</label><div id="new-local-launch-settings" hidden></div><button type="button" id="new-local-launch-save" class="button secondary">保存本机软件配置</button><p id="new-local-launch-notice" role="status"></p></fieldset><button class="button primary full-width" type="submit">创建话题</button></div></form></dialog>
115
113
  <input id="attachment-input" type="file" multiple hidden aria-label="选择附件">
116
114
  <dialog id="attachment-dialog" class="attachment-dialog" aria-labelledby="attachment-title">
117
115
  <div class="attachment-preview-heading"><h2 id="attachment-title">附件预览</h2><a id="attachment-download" download>下载原文件</a><button id="close-attachment" type="button" aria-label="关闭附件预览">×</button></div>