ai_co_discuss 0.3.31 → 0.3.33

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.
@@ -1699,6 +1699,8 @@ def execution_action(connection, room_id, execution_id, token, payload):
1699
1699
  connection.execute("""INSERT INTO execution_capabilities (execution_id,room_id,stoppable,progress) VALUES (?,?,?,?)
1700
1700
  ON CONFLICT(execution_id) DO UPDATE SET stoppable=excluded.stoppable,progress=excluded.progress""",
1701
1701
  (execution_id, room_id, int(payload["stoppable"]), int(payload["progress"])))
1702
+ elif action == "retry":
1703
+ retry_failed(connection, room_id, row)
1702
1704
  elif row["status"] not in ACTIVE:
1703
1705
  raise FileExistsError("执行已结束")
1704
1706
  elif action == "renew":
@@ -1745,11 +1747,35 @@ def execution_action(connection, room_id, execution_id, token, payload):
1745
1747
  # Only for proven non-delivery: the requests return to the queue unchanged.
1746
1748
  finish_execution(connection, execution_id, "cancelled", str(payload.get("reason") or "未送达"), "pending")
1747
1749
  else:
1748
- raise ValueError("action 须为 renew、fail、release、report、segments、retry_wait、retry_start、retry_now 或 retry_stop")
1750
+ raise ValueError("action 须为 renew、fail、release、report、segments、retry_wait、retry_start、retry_now、retry_stop 或 retry")
1749
1751
  detail = execution_detail(connection, connection.execute("SELECT * FROM member_executions WHERE id=?", (execution_id,)).fetchone())
1750
1752
  return {**detail, "duplicate": duplicate} if action == "stopped" else detail
1751
1753
 
1752
1754
 
1755
+ def retry_failed(connection, room_id, execution):
1756
+ """Owner retry of a failed execution: its requests go back to the same member's queue and the runner delivers them anew.
1757
+
1758
+ The failed execution stays as history but no longer carries the requests, so a second retry is refused. A host batch
1759
+ that failed as a whole reopens and its chain resumes, so the host still gets the answer back.
1760
+ """
1761
+ if execution["status"] != "failed":
1762
+ raise FileExistsError("只有失败的执行可以重试")
1763
+ requests = connection.execute("""SELECT id,batch_id,chain_id FROM member_requests r WHERE execution_id=? AND status='failed'
1764
+ AND NOT EXISTS(SELECT 1 FROM session_requests s WHERE s.member_request_id=r.id)""", (execution["id"],)).fetchall()
1765
+ if not requests:
1766
+ raise FileExistsError("这次执行没有可重试的请求:已重试过,或是讨论发言(由主持人重新安排)")
1767
+ if not connection.execute("SELECT 1 FROM room_members WHERE id=? AND status='active'", (execution["member_id"],)).fetchone():
1768
+ raise FileExistsError("该成员已离开或暂离,无法重试")
1769
+ connection.execute(f"UPDATE member_requests SET status='pending',execution_id=NULL WHERE id IN ({','.join('?' * len(requests))})",
1770
+ [request["id"] for request in requests])
1771
+ for request in requests:
1772
+ batch_id, chain_id = request["batch_id"], request["chain_id"]
1773
+ if batch_id and not connection.execute(
1774
+ "SELECT 1 FROM member_requests WHERE room_id=? AND batch_id=? AND status='completed'", (room_id, batch_id)).fetchone():
1775
+ connection.execute("UPDATE free_batches SET settled=0 WHERE room_id=? AND batch_id=?", (room_id, batch_id))
1776
+ connection.execute("DELETE FROM free_chains WHERE room_id=? AND chain_id=? AND reason='all_failed'", (room_id, chain_id))
1777
+
1778
+
1753
1779
  def owned_member(connection, room_id, member_id, token):
1754
1780
  """Read one member after proving the owner's credential."""
1755
1781
  require_owner(connection, room_id, token)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai_co_discuss",
3
- "version": "0.3.31",
3
+ "version": "0.3.33",
4
4
  "bin": {
5
5
  "codiscuss": "runner/codiscuss.mjs"
6
6
  },
package/web/app.js CHANGED
@@ -361,9 +361,10 @@ async function admin_avatar_from_file(file) {
361
361
  } finally { URL.revokeObjectURL(url); }
362
362
  }
363
363
 
364
- /** Whether this browser may write as the room owner: its own control credential, or a logged-in administrator session. */
364
+ /** Whether this browser may write as the room owner: its own control credential, a logged-in administrator session, or the
365
+ * loopback-only local site, whose server writes as the owner for every visitor (`admin_as_owner`). */
365
366
  function has_room_control(room_id) {
366
- return Boolean(localStorage.getItem(`discuss-access:${room_id}`) || (state.admin.logged_in && admin_token()));
367
+ return Boolean(localStorage.getItem(`discuss-access:${room_id}`) || state.admin.local || (state.admin.logged_in && admin_token()));
367
368
  }
368
369
 
369
370
  /** Explain why a management write failed when this browser has neither credential. */
@@ -947,7 +948,7 @@ async function refresh_room({ room: read_room = true } = {}) {
947
948
  }
948
949
  if (state.room && !state.rooms.some((item) => item.id === state.room.id)) return select_room(visible_rooms()[0]?.id);
949
950
  if (!state.room || !read_room) {
950
- if (!state.room && visible_rooms().length) return select_room(visible_rooms()[0].id);
951
+ if (!state.room && visible_rooms().length) return select_room(opening_room());
951
952
  render_rooms();
952
953
  set_connection(true);
953
954
  return;
@@ -964,6 +965,17 @@ async function refresh_room({ room: read_room = true } = {}) {
964
965
  }
965
966
 
966
967
  /** Select a room and reset room-specific draft and filter state. */
968
+ // The room last opened in this browser, reopened on the next visit instead of the first in the list.
969
+ const LAST_ROOM_KEY = "ai-codiscuss-last-room";
970
+
971
+ /** The room to open when none is chosen: the address's room, else the one last opened here, else the first listed. */
972
+ function opening_room() {
973
+ let last = null;
974
+ try { last = localStorage.getItem(LAST_ROOM_KEY); } catch { /* Storage may be unavailable. */ }
975
+ const wanted = [new URLSearchParams(location.search).get("room"), last].find((id) => id && state.rooms.some((room) => room.id === id));
976
+ return wanted || visible_rooms()[0]?.id;
977
+ }
978
+
967
979
  async function select_room(room_id) {
968
980
  state.navigation_version++;
969
981
  state.refresh_version++;
@@ -991,6 +1003,7 @@ async function select_room(room_id) {
991
1003
  cancel_reply();
992
1004
  select_tab("discussion", { refresh: false });
993
1005
  history.replaceState(null, "", state.room ? `/?room=${encodeURIComponent(room_id)}` : "/");
1006
+ if (state.room) try { localStorage.setItem(LAST_ROOM_KEY, room_id); } catch { /* Keep working without remembering. */ }
994
1007
  if (state.room) state.archive_view = Boolean(state.room.archived);
995
1008
  else {
996
1009
  element("host-brief-panel").hidden = true;
@@ -3862,8 +3875,7 @@ async function initialize() {
3862
3875
  const result = computer_rooms_only() && !state.remote_rooms ? { rooms: [] } : await request_json("/api/rooms");
3863
3876
  state.rooms = result.rooms;
3864
3877
  set_folder_directory(result);
3865
- const requested = new URLSearchParams(location.search).get("room");
3866
- await select_room(state.rooms.some((room) => room.id === requested) ? requested : visible_rooms()[0]?.id);
3878
+ await select_room(opening_room());
3867
3879
  set_connection(true);
3868
3880
  } catch (error) { set_connection(false); show_toast(error.message, true); }
3869
3881
  start_live_updates();