ai_co_discuss 0.3.30 → 0.3.32
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 +28 -2
- package/docs/workflow.md +1 -1
- package/package.json +1 -1
- package/web/app.js +46 -6
- package/web/modern.css +1 -1
- package/web/modern.js +1219 -1187
package/backend/free_room.py
CHANGED
|
@@ -521,7 +521,7 @@ def describe_members(connection, room_id, token, payload):
|
|
|
521
521
|
|
|
522
522
|
|
|
523
523
|
def member_labels(connection, room_id):
|
|
524
|
-
"""The owner's display labels of members by name;
|
|
524
|
+
"""The owner's display labels of members by name; screens show them and the page turns a user's @label into the member name, never credentials."""
|
|
525
525
|
return {row["name"]: row["label"] for row in connection.execute(
|
|
526
526
|
"SELECT name,label FROM member_labels WHERE room_id=? ORDER BY name", (room_id,))}
|
|
527
527
|
|
|
@@ -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 或
|
|
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/docs/workflow.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
## 自由协作房间
|
|
17
17
|
|
|
18
|
-
- **点名**:`@成员`
|
|
18
|
+
- **点名**:`@成员` 后,它在自己的原会话中处理并回复,多位可并行。用界面上显示的昵称点名即可(如 `@Gemini`),输入 `@` 后从列表选择最方便。
|
|
19
19
|
- **未点名的消息**:交给主持人分派;只有一位成员时直接交给它。
|
|
20
20
|
- **主持人派活**:主持人点名一位成员时,该成员直接回复你;主持人需要接着安排(如做完再交评审)时会要求它做完交回,点名多位时都完成后主持人再汇总。你的一条消息最多让主持人推进 3 轮(主持人派活、成员回答、交回汇总为一轮),建房时在“推进方式”里或建房后在房间页的“协作设置”里调整(1–20 轮);到上限会停下等你,右栏点“继续”即接着推进。
|
|
21
21
|
- **全体回答**:`@全体成员` 请所有成员各自回答,`@全体依次` 按发言顺序逐个回答。
|
package/package.json
CHANGED
package/web/app.js
CHANGED
|
@@ -947,7 +947,7 @@ async function refresh_room({ room: read_room = true } = {}) {
|
|
|
947
947
|
}
|
|
948
948
|
if (state.room && !state.rooms.some((item) => item.id === state.room.id)) return select_room(visible_rooms()[0]?.id);
|
|
949
949
|
if (!state.room || !read_room) {
|
|
950
|
-
if (!state.room && visible_rooms().length) return select_room(
|
|
950
|
+
if (!state.room && visible_rooms().length) return select_room(opening_room());
|
|
951
951
|
render_rooms();
|
|
952
952
|
set_connection(true);
|
|
953
953
|
return;
|
|
@@ -964,6 +964,17 @@ async function refresh_room({ room: read_room = true } = {}) {
|
|
|
964
964
|
}
|
|
965
965
|
|
|
966
966
|
/** Select a room and reset room-specific draft and filter state. */
|
|
967
|
+
// The room last opened in this browser, reopened on the next visit instead of the first in the list.
|
|
968
|
+
const LAST_ROOM_KEY = "ai-codiscuss-last-room";
|
|
969
|
+
|
|
970
|
+
/** The room to open when none is chosen: the address's room, else the one last opened here, else the first listed. */
|
|
971
|
+
function opening_room() {
|
|
972
|
+
let last = null;
|
|
973
|
+
try { last = localStorage.getItem(LAST_ROOM_KEY); } catch { /* Storage may be unavailable. */ }
|
|
974
|
+
const wanted = [new URLSearchParams(location.search).get("room"), last].find((id) => id && state.rooms.some((room) => room.id === id));
|
|
975
|
+
return wanted || visible_rooms()[0]?.id;
|
|
976
|
+
}
|
|
977
|
+
|
|
967
978
|
async function select_room(room_id) {
|
|
968
979
|
state.navigation_version++;
|
|
969
980
|
state.refresh_version++;
|
|
@@ -991,6 +1002,7 @@ async function select_room(room_id) {
|
|
|
991
1002
|
cancel_reply();
|
|
992
1003
|
select_tab("discussion", { refresh: false });
|
|
993
1004
|
history.replaceState(null, "", state.room ? `/?room=${encodeURIComponent(room_id)}` : "/");
|
|
1005
|
+
if (state.room) try { localStorage.setItem(LAST_ROOM_KEY, room_id); } catch { /* Keep working without remembering. */ }
|
|
994
1006
|
if (state.room) state.archive_view = Boolean(state.room.archived);
|
|
995
1007
|
else {
|
|
996
1008
|
element("host-brief-panel").hidden = true;
|
|
@@ -1036,10 +1048,13 @@ function message_payload() {
|
|
|
1036
1048
|
const members = state.room.free?.members || [];
|
|
1037
1049
|
const payload = { author: "用户", text, reply_to: state.reply_to, kind: "chat", submitted_by: "用户" };
|
|
1038
1050
|
const active = members.filter(member => member.status === "active" && (member.access !== "manual" || member.registered_at)).map(member => member.name);
|
|
1051
|
+
// Members are named by the nickname shown on screen (or their name); the request carries the member name.
|
|
1052
|
+
const named_here = mentioned_members(text, active);
|
|
1053
|
+
const mentioned_member = name => named_here.has(name);
|
|
1039
1054
|
const session = state.room.session;
|
|
1040
1055
|
const session_live = session && ["active", "paused"].includes(session.status);
|
|
1041
1056
|
if (session_live) {
|
|
1042
|
-
const direct = active.filter(
|
|
1057
|
+
const direct = active.filter(mentioned_member);
|
|
1043
1058
|
if (direct.length) payload.to = direct;
|
|
1044
1059
|
return payload;
|
|
1045
1060
|
}
|
|
@@ -1047,7 +1062,7 @@ function message_payload() {
|
|
|
1047
1062
|
// @全体依次 or @all-seq has them answer one after another, the host first); without any, the server gives the message to the only member or the host.
|
|
1048
1063
|
const sequential = mentioned(text, "全体依次") || mentioned(text, "all-seq");
|
|
1049
1064
|
const everyone = !sequential && (mentioned(text, "全体成员") || mentioned(text, "all"));
|
|
1050
|
-
const named = sequential || everyone ? active : active.filter(
|
|
1065
|
+
const named = sequential || everyone ? active : active.filter(mentioned_member);
|
|
1051
1066
|
if (named.length) payload.to = named;
|
|
1052
1067
|
if (sequential && named.length) payload.sequential = true;
|
|
1053
1068
|
if (everyone && named.length) payload.order = "auto";
|
|
@@ -1056,6 +1071,32 @@ function message_payload() {
|
|
|
1056
1071
|
return payload;
|
|
1057
1072
|
}
|
|
1058
1073
|
|
|
1074
|
+
/**
|
|
1075
|
+
* How a member may be @named: by the nickname shown on screen, and always by its member name. A nickname shared with
|
|
1076
|
+
* another member (or equal to another member's name) is ambiguous and is not used, so the member name stays the only way.
|
|
1077
|
+
*/
|
|
1078
|
+
function mention_handles(name, names) {
|
|
1079
|
+
const nickname = display_name(name);
|
|
1080
|
+
const taken = names.filter(other => other !== name).flatMap(other => [other, display_name(other)]);
|
|
1081
|
+
return nickname && nickname !== name && !taken.includes(nickname) ? [nickname, name] : [name];
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* The members a text @names, longest handle first: once "@Claude评审" names one member, the shorter "@Claude" inside it
|
|
1086
|
+
* does not also name another (Chinese nicknames have no space after them).
|
|
1087
|
+
*/
|
|
1088
|
+
function mentioned_members(text, names) {
|
|
1089
|
+
let prose = text.replace(/```[\s\S]*?```/g, "").replace(/`[^`\n]*`/g, "").replace(/^>.*$/gm, "");
|
|
1090
|
+
const handles = names.flatMap(name => mention_handles(name, names).map(handle => ({ name, handle })))
|
|
1091
|
+
.sort((left, right) => right.handle.length - left.handle.length);
|
|
1092
|
+
const found = new Set();
|
|
1093
|
+
for (const { name, handle } of handles) {
|
|
1094
|
+
const pattern = new RegExp(`(^|[\\s((,,。;;::])@${handle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w-])`, "gu");
|
|
1095
|
+
prose = prose.replace(pattern, (match, lead) => { found.add(name); return lead + " ".repeat(match.length - lead.length); });
|
|
1096
|
+
}
|
|
1097
|
+
return found;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1059
1100
|
/** Whether plain text (not code or quotes) names a member with @, so a typed mention selects it. */
|
|
1060
1101
|
function mentioned(text, name) {
|
|
1061
1102
|
const prose = text.replace(/```[\s\S]*?```/g, "").replace(/`[^`\n]*`/g, "").replace(/^>.*$/gm, "");
|
|
@@ -3833,8 +3874,7 @@ async function initialize() {
|
|
|
3833
3874
|
const result = computer_rooms_only() && !state.remote_rooms ? { rooms: [] } : await request_json("/api/rooms");
|
|
3834
3875
|
state.rooms = result.rooms;
|
|
3835
3876
|
set_folder_directory(result);
|
|
3836
|
-
|
|
3837
|
-
await select_room(state.rooms.some((room) => room.id === requested) ? requested : visible_rooms()[0]?.id);
|
|
3877
|
+
await select_room(opening_room());
|
|
3838
3878
|
set_connection(true);
|
|
3839
3879
|
} catch (error) { set_connection(false); show_toast(error.message, true); }
|
|
3840
3880
|
start_live_updates();
|
|
@@ -4111,7 +4151,7 @@ window.discuss_view = { snapshot: view_snapshot, write_field: write_view_field,
|
|
|
4111
4151
|
write_expert: write_view_expert, add_instance: add_view_instance, add_preset: add_preset_instance, platform_of, execute: execute_view_action, theme: change_view_theme,
|
|
4112
4152
|
admin: { register: register_admin, login: login_admin, logout: logout_admin,
|
|
4113
4153
|
update_profile: update_admin_profile, avatar_from_file: admin_avatar_from_file, validate: validate_admin_form },
|
|
4114
|
-
request: request_json, rewind_plan, identities: IDENTITIES, display_name, sources: SOURCE_LABELS,
|
|
4154
|
+
request: request_json, rewind_plan, identities: IDENTITIES, display_name, mention_handles, mentioned_members, sources: SOURCE_LABELS,
|
|
4115
4155
|
set_member_access, set_member_model, allow_denials, set_member_adapter, reload_room_models, limits_label, stop_reason_label, participant_model, participant_files, participant_session_hint, participant_access,
|
|
4116
4156
|
turn_label, turn_guidance, round_groups, limit_rounds, progress_speaker, brief_meta, max_message_length: MAX_MESSAGE_LENGTH, provenance: message_provenance, format_time };
|
|
4117
4157
|
|