ai_co_discuss 0.2.38 → 0.2.40
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/schema.sql +5 -0
- package/backend/server.py +13 -0
- package/docs/architecture.md +1 -1
- package/docs/runner.md +4 -4
- package/docs/workflow.md +1 -1
- package/package.json +1 -1
- package/runner/discussion_runner.mjs +1 -1
- package/runner/scheduler.mjs +29 -13
- package/web/app.js +3 -2
- package/web/modern.css +1 -1
- package/web/modern.js +2062 -1963
package/backend/schema.sql
CHANGED
|
@@ -607,6 +607,11 @@ CREATE TABLE IF NOT EXISTS scheduler_decisions (
|
|
|
607
607
|
created_at TEXT NOT NULL
|
|
608
608
|
);
|
|
609
609
|
CREATE INDEX IF NOT EXISTS scheduler_decisions_room ON scheduler_decisions(room_id,id);
|
|
610
|
+
-- What the decision model was asked and chose, shown folded under the decision; not part of backups.
|
|
611
|
+
CREATE TABLE IF NOT EXISTS scheduler_decision_details (
|
|
612
|
+
decision_id INTEGER PRIMARY KEY REFERENCES scheduler_decisions(id) ON DELETE CASCADE,
|
|
613
|
+
detail TEXT NOT NULL
|
|
614
|
+
);
|
|
610
615
|
-- Smart scheduling's suggestion that another member answer an unnamed message. The host keeps the message;
|
|
611
616
|
-- the user may adopt before expires_at (waiting until the host's request ends for mode 'after'), otherwise it lapses.
|
|
612
617
|
CREATE TABLE IF NOT EXISTS member_recommendations (
|
package/backend/server.py
CHANGED
|
@@ -74,6 +74,7 @@ MAX_QUESTION_LENGTH = 1000
|
|
|
74
74
|
MAX_PLAN_TURNS = 10
|
|
75
75
|
RECENT_DECISIONS = 100
|
|
76
76
|
MAX_DECISION_LABEL = 200
|
|
77
|
+
MAX_DECISION_DETAIL = 4000
|
|
77
78
|
SCHEDULER_PROVIDERS = ("jev", "decisions")
|
|
78
79
|
ROOM_BACKUP_QUERIES = {
|
|
79
80
|
"rooms": "SELECT * FROM rooms WHERE id=?",
|
|
@@ -220,6 +221,10 @@ def require_room(connection, room_id):
|
|
|
220
221
|
room["named_followup"] = bool(option[0]) if option else True
|
|
221
222
|
room["scheduler_decisions"] = [dict(row) for row in connection.execute(
|
|
222
223
|
"SELECT * FROM scheduler_decisions WHERE room_id=? ORDER BY id DESC LIMIT ?", (room_id, RECENT_DECISIONS))][::-1]
|
|
224
|
+
details = {row[0]: json.loads(row[1]) for row in connection.execute(
|
|
225
|
+
"SELECT decision_id,detail FROM scheduler_decision_details WHERE decision_id IN (SELECT id FROM scheduler_decisions WHERE room_id=?)", (room_id,))}
|
|
226
|
+
for decision in room["scheduler_decisions"]:
|
|
227
|
+
decision["detail"] = details.get(decision["id"])
|
|
223
228
|
room["host_arrangements"] = host_arrangements(connection, room_id, room["scheduler_decisions"])
|
|
224
229
|
parent = connection.execute("""SELECT rooms.id,rooms.title FROM room_links JOIN rooms ON rooms.id=room_links.parent_id
|
|
225
230
|
WHERE room_links.room_id=?""", (room_id,)).fetchone()
|
|
@@ -571,6 +576,8 @@ def delete_room_records(connection, room_id):
|
|
|
571
576
|
"""Remove a room's dependent records in the caller's transaction."""
|
|
572
577
|
for table in ("member_recommendations", "member_requests", "member_executions", "member_sessions", "member_registrations", "room_members", "room_modes", "free_batches", "free_chains", "room_links",
|
|
573
578
|
"discussion_options", "scheduler_decisions"):
|
|
579
|
+
if table == "scheduler_decisions":
|
|
580
|
+
connection.execute("DELETE FROM scheduler_decision_details WHERE decision_id IN (SELECT id FROM scheduler_decisions WHERE room_id=?)", (room_id,))
|
|
574
581
|
connection.execute(f"DELETE FROM {table} WHERE room_id=?", (room_id,))
|
|
575
582
|
connection.execute("DELETE FROM message_timings WHERE message_id IN (SELECT id FROM messages WHERE room_id=?)", (room_id,))
|
|
576
583
|
connection.execute("DELETE FROM message_summaries WHERE message_id IN (SELECT id FROM messages WHERE room_id=?)", (room_id,))
|
|
@@ -1033,6 +1040,12 @@ def record_scheduler_decision(connection, room_id, token, payload):
|
|
|
1033
1040
|
raise ValueError("须提供供应商 provider、是否执行 applied 与非负的 after_message_id")
|
|
1034
1041
|
cursor = connection.execute("""INSERT INTO scheduler_decisions (room_id,after_message_id,kind,label,confidence,applied,provider,created_at)
|
|
1035
1042
|
VALUES (?,?,?,?,?,?,?,?)""", (room_id, after, kind, label.strip(), confidence, int(payload["applied"]), payload["provider"], utc_now()))
|
|
1043
|
+
detail = payload.get("detail")
|
|
1044
|
+
if detail is not None:
|
|
1045
|
+
text = json.dumps(detail, ensure_ascii=False)
|
|
1046
|
+
if not isinstance(detail, list) or len(detail) > 4 or len(text) > MAX_DECISION_DETAIL:
|
|
1047
|
+
raise ValueError(f"detail 须为最多 4 项、不超过 {MAX_DECISION_DETAIL} 字的列表")
|
|
1048
|
+
connection.execute("INSERT INTO scheduler_decision_details (decision_id,detail) VALUES (?,?)", (cursor.lastrowid, text))
|
|
1036
1049
|
return dict(connection.execute("SELECT * FROM scheduler_decisions WHERE id=?", (cursor.lastrowid,)).fetchone())
|
|
1037
1050
|
|
|
1038
1051
|
|
package/docs/architecture.md
CHANGED
|
@@ -220,7 +220,7 @@ Python 通过进程内条件变量唤醒订阅者,不占用数据库连接,
|
|
|
220
220
|
## 讨论选项与智能调度记录
|
|
221
221
|
|
|
222
222
|
- `discussion_options`:建房时的讨论选项,目前只有 `named_followup`(0/1,建房 `named_followup` 布尔值,默认 false,计入建房幂等比对)。没有记录的旧房间视为 true;房间视图返回 `named_followup`。
|
|
223
|
-
- `scheduler_decisions`:讨论中每次智能调度判断的可见记录(`kind` 为 `interjection` / `round`,`label`、`confidence`、`applied`、`provider`、`after_message_id`),由接入程序用运行控制凭据 `POST scheduler-decisions` 写入,不是消息,不影响轮次、发言权与暂扣;房间视图 `scheduler_decisions` 返回最近 100
|
|
223
|
+
- `scheduler_decisions`:讨论中每次智能调度判断的可见记录(`kind` 为 `interjection` / `round`,`label`、`confidence`、`applied`、`provider`、`after_message_id`),由接入程序用运行控制凭据 `POST scheduler-decisions` 写入,不是消息,不影响轮次、发言权与暂扣;房间视图 `scheduler_decisions` 返回最近 100 条;每条可带 `detail`(独立表 `scheduler_decision_details`,由 `POST scheduler-decisions` 的 `detail` 写入:每个问题的题目说明、选项,以及决策模型选了哪项和各选项概率,最多 4 项、4000 字,不含房间消息摘录,不随备份导出),网页默认折叠,点开才显示“问 / 答”。房间视图另附 `host_arrangements`:由 `discussion_turns` 按同一批创建时间分组得到的主持人安排(不含邀请追加的开场安排,也不含已由智能调度写入决策的那一批),`after_message_id` 取该批之前最新的回答,网页在该回答后显示与智能调度卡片同样式的“主持人安排:A → B → C”一行,点开才逐条显示专家与主持人写的任务说明(`turns`);没有可跟随的回答(讨论开场)时不显示。不新增表。
|
|
224
224
|
- 两表都是独立表,D1 重新应用 schema 即可;备份、导入(`after_message_id` 随消息重编号)与删除一并处理。智能调度代主持人执行的写入不新增权限,沿用该主持事件的执行凭据。
|
|
225
225
|
|
|
226
226
|
## 自动运行协议
|
package/docs/runner.md
CHANGED
|
@@ -326,14 +326,14 @@ Codex 要求 CLI 0.163.0 或更新版本,使用 permission profiles 和本地
|
|
|
326
326
|
|
|
327
327
|
`runner/scheduler.mjs` 用中立的题目(`choice` 带选项,`predicate` 为是非题)组装两家的请求,并把答案统一为 `{choice, confidence, probabilities}` 或 `{probability}`。上下文是房间标题、目标与用户原文、最近 10 条消息(每条截断到 600 字)和待处理的那条用户消息。把握程度取所选项的概率(缺失时才用 `confidence`;Jev 的两项选择 `confidence` 明显低于该概率,实测概率 0.82 时只有 0.64),阈值 0.8。请求 30 秒超时,错误只报 HTTP 状态,不含响应正文或密钥。
|
|
328
328
|
|
|
329
|
-
- **自由房间(推荐,用户决定)**:未点名的消息照常交给主持人,判断在后台进行,不阻塞本次检查和任何投递。每条 `route` 请求只判断一次,选项为各位本机唤醒的非主持成员(描述为软件与项目读写权限)和“交回主持人”;只有选中成员且 `confidence ≥ 0.
|
|
330
|
-
- **讨论房间(有把握即执行)**:投递主持事件前先只读判断,置信度 ≥ 0.
|
|
329
|
+
- **自由房间(推荐,用户决定)**:未点名的消息照常交给主持人,判断在后台进行,不阻塞本次检查和任何投递。每条 `route` 请求只判断一次,选项为各位本机唤醒的非主持成员(描述为软件与项目读写权限)和“交回主持人”;只有选中成员且 `confidence ≥ 0.7` 时,才用 `POST recommendations` 记录推荐。网页在消息旁显示“智能调度建议让 B 回答”与 60 秒倒计时,用户可选“打断主持人,改由它回答”或“主持人答完后再回答”(主持人已答完时直接请 B 回答),过期后提示消失。判断失败、超时或置信度不足时不显示推荐,行为与未开启相同。
|
|
330
|
+
- **讨论房间(有把握即执行)**:投递主持事件前先只读判断,置信度 ≥ 0.7 才执行,否则照常唤醒主持人:
|
|
331
331
|
- **插话**:`held_since` 后恰有一条用户留言时判断。仅补充信息且有排队安排 → `turns/continue`;在问某位非主持专家 → 新计划先请该专家回应这条留言(问题引用原文 300 字),原排队安排按原顺序跟在后面(最多 10 项);改变方向、多条留言或话题修改 → 主持人。仅补充但计划已结束时,按下一条处理。
|
|
332
332
|
- **轮次衔接**:仅 `named_followup` 为 false、无排队、上一轮已完成时判断。继续 → 先请模型选出的专家(不是上一轮最后发言者),其余按房间专家顺序,统一使用 `peer_round_question`;第 1 轮后自动把阶段从 `initial` 改为 `discussing`。可收束、跑偏、重复或卡住 → 主持人。
|
|
333
333
|
- **执行方式**:有把握时 runner 领取该主持事件,以 `smart-scheduler` 会话进入,用本次执行凭据以主持人身份领取发言权、改阶段、创建或继续安排;网站的发言权、上限与凭据校验照常生效。到限、阶段不在进行中或已有进行中安排时不判断。执行中任一写入失败,把同一事件交回主持人软件并标记已进入(任务说明与 API 主持都跳过 `execution enter`)。判断失败或置信度不足时,主持任务附 `scheduler_suggestions` 供参考。
|
|
334
334
|
- **成员变化**:新邀请的专家由网站在入场事务中加入现有队列的补齐位置(本轮未发言的专家之后、会再次发言的专家之前,没有重复时在队尾),当前执行与其余安排的相对顺序不变,runner 到它的安排生效时才交付;手动专家仍由原会话提交。暂离、移除、请回等需要重排的变化会作废排队安排(留下最近一次实际安排之后的 `skipped`)。本轮未完成且无排队时,runner 不调用模型,按原顺序补回仍在场专家的安排,再请本轮尚未发言的专家开场,不唤醒主持人。
|
|
335
|
-
- 每次判断(含交回主持人)用控制凭据写入 `POST scheduler-decisions`,`after_message_id` 取最近一条已回答安排的回答,对话中显示为该发言卡片与下一张卡片之间的独立调度卡片(“智能调度:……”)。交回主持人时写明原因:建议收束、拿不准,或“未判断:已到讨论上限 / 同时有多条留言 / 话题设置有修改 / 点名追问已开启 / 本轮未发言的专家都是手动接入”;主持人的开场任务不记录。本轮未完成且没有后续安排时(如点“跳过”后),先补回因成员变动作废的计划,否则由决策模型在本轮未发言、可自动唤醒的专家中选下一位(置信度 ≥ 0.
|
|
336
|
-
- 每次判断与结果写入 `service.log`;阈值 0.
|
|
335
|
+
- 每次判断(含交回主持人)用控制凭据写入 `POST scheduler-decisions`,`after_message_id` 取最近一条已回答安排的回答,对话中显示为该发言卡片与下一张卡片之间的独立调度卡片(“智能调度:……”)。交回主持人时写明原因:建议收束、拿不准,或“未判断:已到讨论上限 / 同时有多条留言 / 话题设置有修改 / 点名追问已开启 / 本轮未发言的专家都是手动接入”;主持人的开场任务不记录。本轮未完成且没有后续安排时(如点“跳过”后),先补回因成员变动作废的计划,否则由决策模型在本轮未发言、可自动唤醒的专家中选下一位(置信度 ≥ 0.7),拿不准或建议调整方向才交回主持人。无论开新一轮还是本轮继续,每次只安排一位,答完队列为空再判断下一位;队列里已有安排时直接接力,不判断。刚被用户“跳过”的专家本轮不再被选(除非没有别人);从未发言的专家(如新邀请)使用开场提示词;手动接入的专家不在候选内,也不会被留言分流选中。
|
|
336
|
+
- 每次判断与结果写入 `service.log`;阈值 0.7(所有判断统一)未经实测校准,两家的置信度不可互相套用。面板提示:开启后房间标题与最近消息会发给所选供应商。
|
|
337
337
|
- 2026-10-23 用真实 Jev 密钥核对:`/v1/systemone` 的请求与答案格式与本模块一致;4 条自由房间分派全部合理(直接派出的置信度 0.91),3 条讨论建议中 2 条符合预期,单次 0.5–1.1 秒(首次连接约 5 秒)。该密钥的 `GET /v1/models` 只列出 `jev-latest` 与 `jev-preview`,没有可锁定的版本,默认仍用 `jev-latest`;Jev 升级可能改变判断结果。OpenAI Decisions 仅按文档实现,未用真实密钥调用。
|
|
338
338
|
|
|
339
339
|
普通成员的回应建议不会出现在待投递队列。房主转换后沿用同一请求和执行路径:任务文件的 `request_messages` 指向原成员回答,`chain_id` 属于这次用户确认;重复转换不会再次投递,手动与在线目标仍不进入自动投递。
|
package/docs/workflow.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
讨论运行中可在进度看板邀请专家(经本机服务配置后加入;Codex CLI / App、Claude 可填写已有会话 ID,续接这个带有自己上下文的原会话,标注为借用会话),或从成员菜单让专家暂离、请回或移除。暂离会取消它当前与排队中的安排,保留位置与会话,不计入之后的轮次;请回换发新凭证、续接原会话。邀请保留已有安排,新专家排在本轮的补齐位置;暂离、请回或移除不暂停讨论,但排队中的安排作废,由主持人重新安排;主持人本身不能暂离或移除,更换主持人仍会暂停;正在发言(有进行中安排)的专家须先暂停讨论才能暂离或移除。暂停后可对本轮已发言的专家选择“作废本轮发言”:彻底删除其最近一条观点,该专家重新计为本轮未发言(不新增轮次),恢复后由主持人重新安排。故障只暂停并保留原因,不静默换软件、身份或创建第二个会话。
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
邀请弹窗与新建房间使用同一种角色卡片(同宽、正反面等高):可一次选多位 AI(每个平台一张卡片,已在场的平台以新实例加入),在卡片正面切换“程序启动 / 手动启动”,点“邀请”后先加入程序启动的成员、再加入手动启动的成员(各自保持选择顺序),避免尚未注册的手动成员堵住自动成员;遇到第一个失败即停止并保留未处理的卡片。手动成员加入后弹窗列出它们的复制按钮和实时注册状态。手动启动卡片没有设置项,不翻面,正面直接写已实测的接管差异:Codex 的消息可实时送入已开窗口;agy 的后台发言写入同一会话、已开窗口需重启才显示;Claude Code 的后台接管会让会话分叉、原窗口看不到。已有本轮发言安排时,新邀请的自动和手动专家都加入队列的补齐位置:排在本轮未发言的专家之后、会再次发言的专家之前,没有重复时在队尾;保留当前发言和原有相对顺序、问题;重复邀请请求不重复排队。暂停或尚无安排时,由主持人在继续推进时将新专家安排在本轮尚未发言的原专家之后;进入收束后不再追加安排。邀请不会启动或恢复讨论,也不会清除待处理的用户留言。
|
|
14
14
|
|
|
15
15
|
邀请手动专家后,弹窗提供“复制启动指令”;复制成功后自动关闭,复制失败则保留弹窗并显示错误,方便重试。关闭后仍可在参与者列表中再次复制,不另设“稍后再复制”按钮。复制只交付指令,不改变发言顺序。轮到手动专家时,输入框上方也提供该按钮,复制成功后显示勾号与“已复制”,2 秒后恢复。
|
|
16
16
|
|
package/package.json
CHANGED
|
@@ -444,7 +444,7 @@ export class DiscussionRunner {
|
|
|
444
444
|
async record_decision(plan, applied) {
|
|
445
445
|
try {
|
|
446
446
|
await api(this.config, '/scheduler-decisions', { kind: plan.kind, label: plan.label, confidence: plan.confidence ?? null,
|
|
447
|
-
applied, provider: this.config.scheduler.provider, after_message_id: plan.after_message_id || 0 });
|
|
447
|
+
applied, provider: this.config.scheduler.provider, after_message_id: plan.after_message_id || 0, detail: plan.detail });
|
|
448
448
|
} catch (error) { this.log(`智能调度决策记录未保存:${error.message}`); }
|
|
449
449
|
}
|
|
450
450
|
/** Enter once per delivery; a failure is recorded so reconcile revokes the execution. */
|
package/runner/scheduler.mjs
CHANGED
|
@@ -7,8 +7,8 @@ export const SCHEDULER_PROVIDERS = {
|
|
|
7
7
|
decisions: { label: 'OpenAI Decisions', base_url: 'https://api.openai.com/v1', path: '/decisions', models: ['gpt-6-luna'] },
|
|
8
8
|
};
|
|
9
9
|
const REQUEST_TIMEOUT_MS = 30_000;
|
|
10
|
-
/** Below this confidence a
|
|
11
|
-
export const ROUTE_CONFIDENCE = 0.
|
|
10
|
+
/** Below this confidence a decision stays with the host, exactly as without scheduling. */
|
|
11
|
+
export const ROUTE_CONFIDENCE = 0.7;
|
|
12
12
|
const HOST_CHOICE = '__host__';
|
|
13
13
|
const NOTE_CHOICE = '__note__';
|
|
14
14
|
const CONTEXT_MESSAGES = 10;
|
|
@@ -67,6 +67,17 @@ export async function scheduler_check(home, input, fetch_impl = fetch) {
|
|
|
67
67
|
return { ok: true };
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/** What was asked (the instruction and options only, never the room excerpts) and what the provider chose, for the folded record. */
|
|
71
|
+
function detail_of(questions, answers) {
|
|
72
|
+
return questions.map(question => ({ question: question.instructions, choices: question.choices.map(choice => choice.value),
|
|
73
|
+
choice: answers[question.name]?.choice ?? null, probabilities: answers[question.name]?.probabilities ?? {} }));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Say which option the provider leaned to when it was not sure enough, so the transcript shows how close it was. */
|
|
77
|
+
function unsure_label(lean) {
|
|
78
|
+
return lean ? `倾向${lean},把握不足,交给主持人` : '拿不准,交给主持人';
|
|
79
|
+
}
|
|
80
|
+
|
|
70
81
|
/** Shorten a message so recent context stays small; the decision never needs whole answers. */
|
|
71
82
|
function excerpt(message) {
|
|
72
83
|
const text = String(message.text || '').replace(/\s+/g, ' ').trim();
|
|
@@ -116,14 +127,16 @@ export async function decide_interjection(home, scheduler, { title, goal, user_r
|
|
|
116
127
|
const choices = [...experts.map(name => ({ value: name, description: `请 ${name} 回应这条留言` })),
|
|
117
128
|
{ value: HOST_CHOICE, description: `由主持人 ${host} 处理:调整目标、方向、安排,或需要主持人决定` },
|
|
118
129
|
{ value: NOTE_CHOICE, description: '仅补充信息:不需要专人回应,按原计划继续' }];
|
|
119
|
-
const
|
|
120
|
-
|
|
130
|
+
const questions = [{ type: 'choice', name: 'handling', instructions: '讨论进行中用户发来这条留言。结合讨论目标判断它最适合怎样处理。', choices }];
|
|
131
|
+
const answers = await decide(home, scheduler, scheduler_state(title, context, message, goal, user_request), questions, fetch_impl);
|
|
132
|
+
const detail = detail_of(questions, answers);
|
|
121
133
|
const choice = answers.handling?.choice;
|
|
122
134
|
const confidence = certainty(answers.handling);
|
|
123
135
|
const sure = confidence !== null && confidence >= ROUTE_CONFIDENCE;
|
|
124
|
-
if (sure && choice === NOTE_CHOICE) return { action: 'note', confidence, label: '仅补充信息,按原计划继续' };
|
|
125
|
-
if (sure && experts.includes(choice)) return { action: 'ask', expert: choice, confidence, label: `先请 ${choice} 回应这条留言` };
|
|
126
|
-
|
|
136
|
+
if (sure && choice === NOTE_CHOICE) return { action: 'note', confidence, detail, label: '仅补充信息,按原计划继续' };
|
|
137
|
+
if (sure && experts.includes(choice)) return { action: 'ask', expert: choice, confidence, detail, label: `先请 ${choice} 回应这条留言` };
|
|
138
|
+
const lean = choice === NOTE_CHOICE ? '按原计划继续' : experts.includes(choice) ? `请 ${choice} 回应` : choice === HOST_CHOICE ? '由主持人处理' : '';
|
|
139
|
+
return { action: 'host', confidence, detail, label: choice === HOST_CHOICE && sure ? '这条留言需要主持人处理' : unsure_label(lean) };
|
|
127
140
|
}
|
|
128
141
|
|
|
129
142
|
/**
|
|
@@ -135,14 +148,16 @@ export async function decide_next_speaker(home, scheduler, { title, goal, user_r
|
|
|
135
148
|
const state = [topic_lines(title, goal, user_request), recent.length ? `本轮及最近发言(旧→新):\n${recent.join('\n')}` : '还没有发言。'].join('\n\n');
|
|
136
149
|
const choices = [...candidates.map(name => ({ value: name, description: `请 ${name} 接着发言` })),
|
|
137
150
|
{ value: HOST_CHOICE, description: `交给主持人 ${host}:讨论偏离目标、卡住,或需要主持人调整安排` }];
|
|
138
|
-
const
|
|
139
|
-
instructions: '本轮还有专家没发言,安排被取消或用完。结合已有发言判断下一位应请谁发言:优先选被点名追问、最能回应当前焦点的人。', choices }]
|
|
151
|
+
const questions = [{ type: 'choice', name: 'speaker',
|
|
152
|
+
instructions: '本轮还有专家没发言,安排被取消或用完。结合已有发言判断下一位应请谁发言:优先选被点名追问、最能回应当前焦点的人。', choices }];
|
|
153
|
+
const answers = await decide(home, scheduler, state, questions, fetch_impl);
|
|
154
|
+
const detail = detail_of(questions, answers);
|
|
140
155
|
const choice = answers.speaker?.choice;
|
|
141
156
|
const confidence = certainty(answers.speaker);
|
|
142
157
|
if (candidates.includes(choice) && confidence !== null && confidence >= ROUTE_CONFIDENCE) {
|
|
143
|
-
return { action: 'next', expert: choice, confidence, label: `本轮继续,请 ${choice} 接着发言` };
|
|
158
|
+
return { action: 'next', expert: choice, confidence, detail, label: `本轮继续,请 ${choice} 接着发言` };
|
|
144
159
|
}
|
|
145
|
-
return { action: 'host', confidence, label: '
|
|
160
|
+
return { action: 'host', confidence, detail, label: choice === HOST_CHOICE && confidence >= ROUTE_CONFIDENCE ? '建议由主持人调整安排,交给主持人' : unsure_label(candidates.includes(choice) ? `请 ${choice} 发言` : choice === HOST_CHOICE ? '交给主持人' : '') };
|
|
146
161
|
}
|
|
147
162
|
|
|
148
163
|
/**
|
|
@@ -159,12 +174,13 @@ export async function decide_round(home, scheduler, { title, goal, user_request,
|
|
|
159
174
|
if (candidates.length > 1) questions.push({ type: 'choice', name: 'first', instructions: '下一轮应先请谁发言?优先选被其他专家点名追问、最需要回应的人。',
|
|
160
175
|
choices: candidates.map(name => ({ value: name, description: `先请 ${name}` })) });
|
|
161
176
|
const answers = await decide(home, scheduler, state, questions, fetch_impl);
|
|
177
|
+
const detail = detail_of(questions, answers);
|
|
162
178
|
const choice = answers.next?.choice;
|
|
163
179
|
const confidence = certainty(answers.next);
|
|
164
180
|
if (choice !== 'continue' || confidence === null || confidence < ROUTE_CONFIDENCE || !candidates.length) {
|
|
165
181
|
const sure = confidence !== null && confidence >= ROUTE_CONFIDENCE;
|
|
166
|
-
return { action: 'host', confidence, label: choice === HOST_CHOICE && sure ? '建议收束或调整方向,交给主持人' : '
|
|
182
|
+
return { action: 'host', confidence, detail, label: choice === HOST_CHOICE && sure ? '建议收束或调整方向,交给主持人' : unsure_label(choice === 'continue' ? '继续下一轮' : choice === HOST_CHOICE ? '收束或调整方向' : '') };
|
|
167
183
|
}
|
|
168
184
|
const first = candidates.includes(answers.first?.choice) ? answers.first.choice : candidates[0];
|
|
169
|
-
return { action: 'continue', first, confidence, label: `继续下一轮,先请 ${first} 发言` };
|
|
185
|
+
return { action: 'continue', first, confidence, detail, label: `继续下一轮,先请 ${first} 发言` };
|
|
170
186
|
}
|
package/web/app.js
CHANGED
|
@@ -905,7 +905,7 @@ async function change_member({ member_id, name, action, role }) {
|
|
|
905
905
|
? await local_launch_request("room-member", { room_id, token, action: "rejoin", name, member_token: record.member.token })
|
|
906
906
|
: await request_json(`/api/rooms/${room_id}/members/${member_id}`, { action: "active", token: record.member.token });
|
|
907
907
|
finish_member_request(room_id, token, record, result);
|
|
908
|
-
if (state.room?.id === room_id) state.free_notice = member.access === "manual" ? `${name}
|
|
908
|
+
if (state.room?.id === room_id) state.free_notice = member.access === "manual" ? `${name} 已请回,凭证已更新;请用新的启动指令让它重新注册。` : `${name} 已请回,续接原会话。`;
|
|
909
909
|
} else {
|
|
910
910
|
await request_json(`/api/rooms/${room_id}/members/${member_id}${action === "interrupt" ? "/interrupt" : ""}`,
|
|
911
911
|
action === "interrupt" ? {} : { action, ...(role ? { role } : {}) });
|
|
@@ -939,7 +939,8 @@ async function invite_member({ name, access, project_access = "read", session_id
|
|
|
939
939
|
finish_member_request(room_id, token, record, result);
|
|
940
940
|
if (state.room?.id === room_id) {
|
|
941
941
|
state.free_notice = access === "online" ? `${record.member.name} 已加入,阅读通知与真实回答由你或授权主持人转交。`
|
|
942
|
-
|
|
942
|
+
// A manual member's start step lives on the board row, the composer bar and the invite dialog; no extra notice.
|
|
943
|
+
: access === "manual" ? ""
|
|
943
944
|
: record.options?.session_id ? `${record.member.name} 已接入原会话;它工作时请不要在原窗口继续操作。`
|
|
944
945
|
: state.room.mode === "free" ? `${record.member.name} 已加入,之后点名它即会由本机服务唤醒。`
|
|
945
946
|
: `${record.member.name} 已加入讨论,主持人会重新安排发言。`;
|