ai_co_discuss 0.2.69 → 0.2.71
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/.agents/skills/discuss-host/SKILL.md +5 -1
- package/.agents/skills/discuss-participant/references/free.md +1 -1
- package/backend/free_room.py +214 -24
- package/backend/schema.sql +27 -1
- package/backend/server.py +63 -6
- package/docs/architecture.md +4 -1
- package/docs/cli.md +197 -196
- package/docs/runner.md +2 -1
- package/docs/workflow.md +155 -153
- package/package.json +1 -1
- package/runner/discuss.py +1001 -991
- package/runner/discussion_adapters.mjs +11 -4
- package/runner/discussion_runner.mjs +1 -1
- package/runner/free_runner.mjs +40 -4
- package/runner/scheduler.mjs +48 -24
- package/web/app.js +20 -6
- package/web/modern.css +1 -1
- package/web/modern.js +5145 -4799
|
@@ -253,6 +253,15 @@ export function kill_process_tree(child, plain_kill, platform = process.platform
|
|
|
253
253
|
return plain_kill();
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
/** Make a delivered process's kill end its whole tree on Windows, so a stopped turn leaves no tool commands running. */
|
|
257
|
+
function tree_killable(child) {
|
|
258
|
+
if (typeof child.kill === 'function') {
|
|
259
|
+
const plain_kill = child.kill.bind(child);
|
|
260
|
+
child.kill = () => kill_process_tree(child, plain_kill);
|
|
261
|
+
}
|
|
262
|
+
return child;
|
|
263
|
+
}
|
|
264
|
+
|
|
256
265
|
async function claude_delivery(config, binding, task, spawn_process = spawn) {
|
|
257
266
|
const session_id = binding.session_id || randomUUID();
|
|
258
267
|
try { bind_access(task.access_path, session_id); }
|
|
@@ -266,10 +275,7 @@ async function claude_delivery(config, binding, task, spawn_process = spawn) {
|
|
|
266
275
|
env: { ...standalone_claude_env(), DISCUSS_ACCESS_FILE: task.access_path },
|
|
267
276
|
});
|
|
268
277
|
} catch (error) { throw not_sent(error); }
|
|
269
|
-
|
|
270
|
-
const plain_kill = child.kill.bind(child);
|
|
271
|
-
child.kill = () => kill_process_tree(child, plain_kill);
|
|
272
|
-
}
|
|
278
|
+
tree_killable(child);
|
|
273
279
|
let spawned = false;
|
|
274
280
|
const log_path = resolve(task.directory, `claude-${task.id}.log`);
|
|
275
281
|
let output = '';
|
|
@@ -497,6 +503,7 @@ async function cli_delivery(config, binding, task, spawn_process, user_home) {
|
|
|
497
503
|
env: { ...process.env, DISCUSS_ACCESS_FILE: task.access_path },
|
|
498
504
|
});
|
|
499
505
|
} catch (error) { throw not_sent(error); }
|
|
506
|
+
tree_killable(child);
|
|
500
507
|
const state = monitor_cli(child, config, binding, task);
|
|
501
508
|
task.on_process(child);
|
|
502
509
|
await new Promise((resolve_spawn, reject) => {
|
|
@@ -462,7 +462,7 @@ export class DiscussionRunner {
|
|
|
462
462
|
async record_decision(plan, applied) {
|
|
463
463
|
try {
|
|
464
464
|
await api(this.config, '/scheduler-decisions', { kind: plan.kind, label: plan.label, confidence: plan.confidence ?? null,
|
|
465
|
-
applied, provider: this.config.scheduler.provider, after_message_id: plan.after_message_id || 0, detail: plan.detail });
|
|
465
|
+
applied, provider: this.config.scheduler.provider, after_message_id: plan.after_message_id || 0, detail: plan.detail, input: plan.input });
|
|
466
466
|
} catch (error) { this.log(`智能调度决策记录未保存:${error.message}`); }
|
|
467
467
|
}
|
|
468
468
|
/** Enter once per delivery; a failure is recorded so reconcile revokes the execution. */
|
package/runner/free_runner.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { appendFileSync, mkdirSync, readFileSync, statSync } from 'node:fs';
|
|
|
5
5
|
import { basename, join, resolve } from 'node:path';
|
|
6
6
|
import { atomic_json, deliver_task, sidecar_receipt, TASK_SUBCOMMANDS } from './discussion_adapters.mjs';
|
|
7
7
|
import { api } from './discussion_runner.mjs';
|
|
8
|
-
import { ROUTE_CONFIDENCE, ROUTE_CONTEXT_MESSAGES, ROUTE_PROMPT_VERSION, ROUTE_WIDE_CONFIDENCE, route_free_message } from './scheduler.mjs';
|
|
8
|
+
import { ROUTE_CONFIDENCE, ROUTE_CONTEXT_MESSAGES, ROUTE_PROMPT_VERSION, ROUTE_WIDE_CONFIDENCE, decide_group_order, route_free_message } from './scheduler.mjs';
|
|
9
9
|
|
|
10
10
|
const PACKAGE_VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
11
11
|
const RENEW_MS = 30_000;
|
|
@@ -121,6 +121,8 @@ export class FreeRoomRunner {
|
|
|
121
121
|
this.log = log;
|
|
122
122
|
this.fetch = fetch_impl;
|
|
123
123
|
this.routed = new Set();
|
|
124
|
+
// Group requests (@全体成员) being arranged; a failed arrangement leaves the set so the next tick retries.
|
|
125
|
+
this.grouped = new Set();
|
|
124
126
|
this.decisions = new Set();
|
|
125
127
|
// Routed requests whose decision is still pending: their host waits, so one pick replaces the host's start.
|
|
126
128
|
this.routing = new Set();
|
|
@@ -164,8 +166,9 @@ export class FreeRoomRunner {
|
|
|
164
166
|
async tick() {
|
|
165
167
|
const inbox = await api(this.config, '/inbox');
|
|
166
168
|
const room = await api(this.config, '');
|
|
167
|
-
// Smart scheduling runs beside delivery: the host
|
|
169
|
+
// Smart scheduling runs beside delivery: the host waits for its answer (or failure) before starting, see `routing`.
|
|
168
170
|
if (this.config.scheduler && !inbox.archived) this.recommend_unnamed(inbox, room);
|
|
171
|
+
if (!inbox.archived) this.arrange_groups(inbox, room);
|
|
169
172
|
this.room_title = room.title;
|
|
170
173
|
const active = new Map(inbox.executions.map(item => [item.id, item]));
|
|
171
174
|
const present = new Set(inbox.members.map(member => member.name));
|
|
@@ -177,7 +180,7 @@ export class FreeRoomRunner {
|
|
|
177
180
|
for (const member of inbox.members) {
|
|
178
181
|
const row = this.row(member.name);
|
|
179
182
|
if (row && LIVE.includes(row.status)) await this.reconcile(member, row, active, inbox.execution_minutes);
|
|
180
|
-
else if (!inbox.archived && member.pending.
|
|
183
|
+
else if (!inbox.archived && member.pending.some(request => request.purpose !== 'group') && !this.members.get(member.name)?.size
|
|
181
184
|
&& !member.pending.some(request => this.routing.has(request.id))) await this.start(member, row);
|
|
182
185
|
}
|
|
183
186
|
}
|
|
@@ -202,6 +205,39 @@ export class FreeRoomRunner {
|
|
|
202
205
|
decision.finally(() => this.decisions.delete(decision));
|
|
203
206
|
}
|
|
204
207
|
}
|
|
208
|
+
/** For each message naming everyone, have them answer at once, or one after another when smart scheduling says so. */
|
|
209
|
+
arrange_groups(inbox, room) {
|
|
210
|
+
const everyone = [...inbox.members, ...(inbox.observers || [])];
|
|
211
|
+
for (const request of everyone.flatMap(member => member.pending).filter(item => item.purpose === 'group' && !this.grouped.has(item.id))) {
|
|
212
|
+
this.grouped.add(request.id);
|
|
213
|
+
const arranged = (this.config.scheduler ? this.mark_routing(request.message_id, true).then(() => this.order_group(request, room, everyone))
|
|
214
|
+
.finally(() => this.mark_routing(request.message_id, false)) : this.apply_group(request.message_id, { action: 'all' }))
|
|
215
|
+
.catch(error => { this.grouped.delete(request.id); this.log(`全体成员 #${request.message_id} 未能安排:${error.message}`); });
|
|
216
|
+
this.decisions.add(arranged);
|
|
217
|
+
arranged.finally(() => this.decisions.delete(arranged));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/** Ask the scheduler how everyone answers; a failed call still has them answer at once. */
|
|
221
|
+
async order_group(request, room, everyone) {
|
|
222
|
+
const page = await api(this.config, `/messages?after=${Math.max(0, request.message_id - ROUTE_CONTEXT_MESSAGES - 1)}`);
|
|
223
|
+
const message = page.messages.find(item => item.id === request.message_id);
|
|
224
|
+
const members = (room.speaking_order?.items || []).map(item => item.name).filter(name => everyone.some(member => member.name === name));
|
|
225
|
+
const decision = await decide_group_order(this.config.api_home, this.config.scheduler,
|
|
226
|
+
{ title: room.title, goal: room.goal, context: page.messages, message, members: members.length ? members : everyone.map(member => member.name) }, this.fetch)
|
|
227
|
+
.catch(error => { this.log(`智能调度 #${request.message_id} 未判断全体回答方式:${error.message}`); return { action: 'all' }; });
|
|
228
|
+
await this.apply_group(request.message_id, decision);
|
|
229
|
+
if (decision.detail) {
|
|
230
|
+
const label = decision.action === 'sequential' ? '请全体按发言顺序依次回答' : '请全体同时回答';
|
|
231
|
+
await this.record_decision(request.message_id, decision, label, true);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/** Replace the host's group request with everyone's requests; a message already arranged elsewhere is left as is. */
|
|
235
|
+
async apply_group(message_id, decision) {
|
|
236
|
+
this.log(`全体成员 #${message_id}:${decision.action === 'sequential' ? '依次回答' : '同时回答'}`);
|
|
237
|
+
await api(this.config, `/recommendations/${message_id}`, { action: decision.action }).catch(error => {
|
|
238
|
+
if (!/HTTP (404|409)/.test(error.message)) throw error;
|
|
239
|
+
});
|
|
240
|
+
}
|
|
205
241
|
/** Tell the website a message is being analysed so it can show the card; a failed mark never blocks routing. */
|
|
206
242
|
async mark_routing(message_id, active) {
|
|
207
243
|
try { await api(this.config, '/scheduler-routing', { message_id, active }); }
|
|
@@ -248,7 +284,7 @@ export class FreeRoomRunner {
|
|
|
248
284
|
async record_decision(message_id, decision, label, applied) {
|
|
249
285
|
try {
|
|
250
286
|
await api(this.config, '/scheduler-decisions', { kind: 'interjection', label, confidence: decision.confidence ?? null,
|
|
251
|
-
applied, provider: this.config.scheduler.provider, after_message_id: message_id, detail: decision.detail,
|
|
287
|
+
applied, provider: this.config.scheduler.provider, after_message_id: message_id, detail: decision.detail, input: decision.input,
|
|
252
288
|
context: { model: this.config.scheduler.model, threshold: ROUTE_CONFIDENCE, wide_threshold: ROUTE_WIDE_CONFIDENCE,
|
|
253
289
|
prompt: ROUTE_PROMPT_VERSION, package: PACKAGE_VERSION } });
|
|
254
290
|
} catch (error) { this.log(`智能调度 #${message_id} 决策记录未保存:${error.message}`); }
|
package/runner/scheduler.mjs
CHANGED
|
@@ -12,7 +12,7 @@ export const ROUTE_CONFIDENCE = 0.7;
|
|
|
12
12
|
/** Waking everyone or no one costs more when wrong (N runs, or an ignored message), so it needs more certainty. */
|
|
13
13
|
export const ROUTE_WIDE_CONFIDENCE = 0.85;
|
|
14
14
|
/** Bump when the routing question or its option descriptions change, so ratings can be compared across wordings. */
|
|
15
|
-
export const ROUTE_PROMPT_VERSION =
|
|
15
|
+
export const ROUTE_PROMPT_VERSION = 4;
|
|
16
16
|
const HOST_CHOICE = '__host__';
|
|
17
17
|
const NOTE_CHOICE = '__note__';
|
|
18
18
|
const ALL_CHOICE = '__all__';
|
|
@@ -95,6 +95,11 @@ function detail_of(questions, answers) {
|
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/** The shown detail plus the exact input (state and full questions), kept so a rated-bad decision can be reviewed later. */
|
|
99
|
+
function judged_of(state, questions, answers) {
|
|
100
|
+
return { detail: detail_of(questions, answers), input: { state, questions } };
|
|
101
|
+
}
|
|
102
|
+
|
|
98
103
|
/** Say which option the provider leaned to when it was not sure enough, so the transcript shows how close it was. */
|
|
99
104
|
function unsure_label(lean) {
|
|
100
105
|
return lean ? `倾向${lean},把握不足,交给主持人` : '拿不准,交给主持人';
|
|
@@ -108,8 +113,8 @@ function excerpt(message) {
|
|
|
108
113
|
}
|
|
109
114
|
|
|
110
115
|
/** The room's purpose in the user's words, so a decision can tell drifting from progress. */
|
|
111
|
-
function topic_lines(title, goal, user_request, { brief, focus, arrangement, progress } = {}) {
|
|
112
|
-
return [`房间:${title || ''}`, goal && `目标:${goal}`, user_request && `用户原文:${String(user_request).slice(0, MAX_MESSAGE_CHARS)}`,
|
|
116
|
+
function topic_lines(title, goal, user_request, { roster, brief, focus, arrangement, progress } = {}) {
|
|
117
|
+
return [`房间:${title || ''}`, roster && `房间成员(名字不区分大小写):${roster}`, goal && `目标:${goal}`, user_request && `用户原文:${String(user_request).slice(0, MAX_MESSAGE_CHARS)}`,
|
|
113
118
|
focus && `当前焦点:${focus}`, brief && `主持简报(含分工与规则):${String(brief).slice(0, MAX_BRIEF_CHARS)}`,
|
|
114
119
|
arrangement && `主持人最近的安排:${arrangement}`, progress].filter(Boolean).join('\n');
|
|
115
120
|
}
|
|
@@ -136,21 +141,39 @@ export function certainty(answer) {
|
|
|
136
141
|
*/
|
|
137
142
|
export async function route_free_message(home, scheduler, { title, goal, context, message, host, members }, fetch_impl) {
|
|
138
143
|
const choices = [...members.map(member => ({ value: member.name, description: member.description })),
|
|
139
|
-
{ value: HOST_CHOICE, description: `主持人 ${host.description}
|
|
140
|
-
{ value: ALL_CHOICE, description: '
|
|
144
|
+
{ value: HOST_CHOICE, description: `主持人 ${host.description}:没有指向任何人(如单独一句“hi”“你好”这类问候、闲聊,由主持人代表房间回应),或需要分工、排先后、互相回应` },
|
|
145
|
+
{ value: ALL_CHOICE, description: '全体成员(含主持人)各自回应一次,彼此不需要配合;须是用户明确要求大家各自回应,如“大家/都出来打个招呼”“各自介绍一下”“各说一句看法”' },
|
|
141
146
|
{ value: NONE_CHOICE, description: '无需回应:只是确认、致谢或旁注,不需要任何人回答' }];
|
|
142
147
|
const questions = [{ type: 'choice', name: 'handler', choices,
|
|
143
|
-
instructions: '这条用户消息没有用 @
|
|
144
|
-
const
|
|
145
|
-
const
|
|
148
|
+
instructions: '这条用户消息没有用 @ 点名成员。判断它的回应对象,按下面的顺序:1)正文叫了某位成员的名字,或在向其提问(包括“X在吗”“X还在吗”“X你怎么看”),选该成员,即使上一条是别人的回答;2)正文没有指向任何人时,看它是否在延续某位成员的回答(追问、要求修改、引用,或“回复”指向其消息),或在补充、纠正一条正由某位成员处理的消息,是则选该成员,是主持人就选主持人;3)明确要求所有成员各自回应一次(用了“大家”“都”“各自”等)选全体;4)单独的问候或闲聊(如“hi”)既没有指向某位成员,也没有要求大家一起回应,选主持人;5)只是确认、致谢或旁注选无需回应;6)对象不明确,或需要分工、排先后、互相回应时选主持人。' }];
|
|
149
|
+
const roster = [`${host.name}(主持人)`, ...members.map(member => member.name)].join('、');
|
|
150
|
+
const state = scheduler_state(title, context, message, goal, '', { roster }, ROUTE_CONTEXT_MESSAGES);
|
|
151
|
+
const answers = await decide(home, scheduler, state, questions, fetch_impl);
|
|
152
|
+
const judged = judged_of(state, questions, answers);
|
|
146
153
|
const choice = answers.handler?.choice ?? null;
|
|
147
154
|
const confidence = certainty(answers.handler);
|
|
148
155
|
const wide = choice === ALL_CHOICE || choice === NONE_CHOICE;
|
|
149
156
|
const sure = confidence !== null && confidence >= (wide ? ROUTE_WIDE_CONFIDENCE : ROUTE_CONFIDENCE);
|
|
150
|
-
if (sure && choice === ALL_CHOICE) return { action: 'all', choice, confidence,
|
|
151
|
-
if (sure && choice === NONE_CHOICE) return { action: 'none', choice, confidence,
|
|
152
|
-
if (sure && members.some(item => item.name === choice)) return { action: 'member', member: choice, choice, confidence,
|
|
153
|
-
return { action: 'host', choice, confidence,
|
|
157
|
+
if (sure && choice === ALL_CHOICE) return { action: 'all', choice, confidence, ...judged };
|
|
158
|
+
if (sure && choice === NONE_CHOICE) return { action: 'none', choice, confidence, ...judged };
|
|
159
|
+
if (sure && members.some(item => item.name === choice)) return { action: 'member', member: choice, choice, confidence, ...judged };
|
|
160
|
+
return { action: 'host', choice, confidence, ...judged };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The user named everyone (@全体成员): decide whether they answer at once (parallel) or one after another (sequential).
|
|
165
|
+
* Anything but a confident sequential stays parallel, the cheaper and earlier behaviour of naming everyone.
|
|
166
|
+
*/
|
|
167
|
+
export async function decide_group_order(home, scheduler, { title, goal, context, message, members }, fetch_impl) {
|
|
168
|
+
const questions = [{ type: 'choice', name: 'order', choices: [
|
|
169
|
+
{ value: 'parallel', description: '同时回答:各自独立回应即可,不需要看到别人的回答,如打招呼、各自介绍、各说一句看法、各自投票' },
|
|
170
|
+
{ value: 'sequential', description: `依次回答:后发言的需要看到前面的回答,如接龙、轮流补充或反驳、逐个评审上一位的结果、按顺序分工接力;按房间发言顺序(${members.join('、')})逐个回答` }],
|
|
171
|
+
instructions: '用户用 @全体成员 请房间里所有成员回应这条消息。判断大家应同时各自回答,还是需要按顺序依次回答(后一位看到前一位的回答再接着说)。' }];
|
|
172
|
+
const state = scheduler_state(title, context, message, goal, '', {}, ROUTE_CONTEXT_MESSAGES);
|
|
173
|
+
const answers = await decide(home, scheduler, state, questions, fetch_impl);
|
|
174
|
+
const confidence = certainty(answers.order);
|
|
175
|
+
const sequential = answers.order?.choice === 'sequential' && confidence !== null && confidence >= ROUTE_CONFIDENCE;
|
|
176
|
+
return { action: sequential ? 'sequential' : 'all', choice: answers.order?.choice ?? null, confidence, ...judged_of(state, questions, answers) };
|
|
154
177
|
}
|
|
155
178
|
|
|
156
179
|
/**
|
|
@@ -162,15 +185,16 @@ export async function decide_interjection(home, scheduler, { title, goal, user_r
|
|
|
162
185
|
{ value: HOST_CHOICE, description: `由主持人 ${host} 处理:留言要改变讨论目标、规则、分工、安排或结束讨论,或同时涉及多位专家` },
|
|
163
186
|
{ value: NOTE_CHOICE, description: '仅补充信息:留言只是旁注、信息或鼓励,不要求任何人回应,按当前安排继续' }];
|
|
164
187
|
const questions = [{ type: 'choice', name: 'handling', instructions: '讨论进行中用户发来这条留言。对照当前焦点、主持简报和安排,判断它最适合怎样处理。', choices }];
|
|
165
|
-
const
|
|
166
|
-
const
|
|
188
|
+
const state = scheduler_state(title, context, message, goal, user_request, extra);
|
|
189
|
+
const answers = await decide(home, scheduler, state, questions, fetch_impl);
|
|
190
|
+
const judged = judged_of(state, questions, answers);
|
|
167
191
|
const choice = answers.handling?.choice;
|
|
168
192
|
const confidence = certainty(answers.handling);
|
|
169
193
|
const sure = confidence !== null && confidence >= ROUTE_CONFIDENCE;
|
|
170
|
-
if (sure && choice === NOTE_CHOICE) return { action: 'note', confidence,
|
|
171
|
-
if (sure && experts.includes(choice)) return { action: 'ask', expert: choice, confidence,
|
|
194
|
+
if (sure && choice === NOTE_CHOICE) return { action: 'note', confidence, ...judged, label: '仅补充信息,按原计划继续' };
|
|
195
|
+
if (sure && experts.includes(choice)) return { action: 'ask', expert: choice, confidence, ...judged, label: `先请 ${choice} 回应这条留言` };
|
|
172
196
|
const lean = choice === NOTE_CHOICE ? '按原计划继续' : experts.includes(choice) ? `请 ${choice} 回应` : choice === HOST_CHOICE ? '由主持人处理' : '';
|
|
173
|
-
return { action: 'host', confidence,
|
|
197
|
+
return { action: 'host', confidence, ...judged, label: choice === HOST_CHOICE && sure ? '这条留言需要主持人处理' : unsure_label(lean) };
|
|
174
198
|
}
|
|
175
199
|
|
|
176
200
|
/**
|
|
@@ -189,17 +213,17 @@ export async function decide_next_speaker(home, scheduler, { title, goal, user_r
|
|
|
189
213
|
{ type: 'predicate', name: 'obligation',
|
|
190
214
|
instructions: `是否存在明确依据要求 ${candidates.join('、')} 之一接着发言:简报分工或流程规定轮到其人,或最近发言点名追问了其人?仅仅是“本轮还没发言”不算依据。` }];
|
|
191
215
|
const answers = await decide(home, scheduler, state, questions, fetch_impl);
|
|
192
|
-
const
|
|
216
|
+
const judged = judged_of(state, questions, answers);
|
|
193
217
|
const choice = answers.speaker?.choice;
|
|
194
218
|
const confidence = certainty(answers.speaker);
|
|
195
219
|
const obligation = answers.obligation?.probability;
|
|
196
220
|
if (candidates.includes(choice) && confidence !== null && confidence >= ROUTE_CONFIDENCE && obligation >= ROUTE_CONFIDENCE) {
|
|
197
|
-
return { action: 'next', expert: choice, confidence,
|
|
221
|
+
return { action: 'next', expert: choice, confidence, ...judged, label: `本轮继续,请 ${choice} 接着发言` };
|
|
198
222
|
}
|
|
199
223
|
if (typeof obligation === 'number' && obligation < ROUTE_CONFIDENCE) {
|
|
200
|
-
return { action: 'host', confidence,
|
|
224
|
+
return { action: 'host', confidence, ...judged, label: `有明确依据的概率 ${obligation.toFixed(2)},未达 ${ROUTE_CONFIDENCE.toFixed(2)},交给主持人安排` };
|
|
201
225
|
}
|
|
202
|
-
return { action: 'host', confidence,
|
|
226
|
+
return { action: 'host', confidence, ...judged, label: choice === HOST_CHOICE && confidence >= ROUTE_CONFIDENCE ? '建议由主持人调整安排,交给主持人' : unsure_label(candidates.includes(choice) ? `请 ${choice} 发言` : choice === HOST_CHOICE ? '交给主持人' : '') };
|
|
203
227
|
}
|
|
204
228
|
|
|
205
229
|
/**
|
|
@@ -216,13 +240,13 @@ export async function decide_round(home, scheduler, { title, goal, user_request,
|
|
|
216
240
|
if (candidates.length > 1) questions.push({ type: 'choice', name: 'first', instructions: '下一轮应先请谁发言?按简报分工或接龙顺序判断;没有规定时选被其他专家点名追问、最需要回应的人。',
|
|
217
241
|
choices: candidates.map(name => ({ value: name, description: `先请 ${name}` })) });
|
|
218
242
|
const answers = await decide(home, scheduler, state, questions, fetch_impl);
|
|
219
|
-
const
|
|
243
|
+
const judged = judged_of(state, questions, answers);
|
|
220
244
|
const choice = answers.next?.choice;
|
|
221
245
|
const confidence = certainty(answers.next);
|
|
222
246
|
if (choice !== 'continue' || confidence === null || confidence < ROUTE_CONFIDENCE || !candidates.length) {
|
|
223
247
|
const sure = confidence !== null && confidence >= ROUTE_CONFIDENCE;
|
|
224
|
-
return { action: 'host', confidence,
|
|
248
|
+
return { action: 'host', confidence, ...judged, label: choice === HOST_CHOICE && sure ? '建议收束或调整方向,交给主持人' : unsure_label(choice === 'continue' ? '继续下一轮' : choice === HOST_CHOICE ? '收束或调整方向' : '') };
|
|
225
249
|
}
|
|
226
250
|
const first = candidates.includes(answers.first?.choice) ? answers.first.choice : candidates[0];
|
|
227
|
-
return { action: 'continue', first, confidence,
|
|
251
|
+
return { action: 'continue', first, confidence, ...judged, label: `继续下一轮,先请 ${first} 发言` };
|
|
228
252
|
}
|
package/web/app.js
CHANGED
|
@@ -47,7 +47,7 @@ const PLATFORM_IDENTITIES = {
|
|
|
47
47
|
Grok: { initials: "GK", style: "grok", description: "AI 身份" },
|
|
48
48
|
};
|
|
49
49
|
const IDENTITIES = by_platform(PLATFORM_IDENTITIES, (identity, name) => ({ ...identity, label: name, description: "AI 实例" }));
|
|
50
|
-
const state = { rooms: [], folders: [], room: null, page: 1, pages: 1, query: "", mention_only: false, free_notice: "", free_error: "",
|
|
50
|
+
const state = { rooms: [], folders: [], room: null, page: 1, pages: 1, query: "", mention_only: false, free_notice: "", free_error: "", order_error: null,
|
|
51
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,
|
|
@@ -939,12 +939,14 @@ function message_payload() {
|
|
|
939
939
|
request_ids: state.room.free.requests.filter(item => item.member === online.name && item.status === "pending").map(item => item.id) };
|
|
940
940
|
const payload = { author: "用户", text, reply_to: state.reply_to, kind: "chat", submitted_by: "用户" };
|
|
941
941
|
const active = (state.room.free?.members || []).filter(member => member.status === "active" && (member.access !== "manual" || member.registered_at)).map(member => member.name);
|
|
942
|
-
// @mentions outside code name members (@全体成员 or @all names them all
|
|
943
|
-
// without any, the server gives the message to the only member or the host.
|
|
942
|
+
// @mentions outside code name members (@全体成员 or @all names them all, answering at once or in turn as smart scheduling decides;
|
|
943
|
+
// @全体依次 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.
|
|
944
944
|
const sequential = mentioned(text, "全体依次") || mentioned(text, "all-seq");
|
|
945
|
-
const
|
|
945
|
+
const everyone = !sequential && (mentioned(text, "全体成员") || mentioned(text, "all"));
|
|
946
|
+
const named = sequential || everyone ? active : active.filter(name => mentioned(text, name));
|
|
946
947
|
if (named.length) payload.to = named;
|
|
947
948
|
if (sequential && named.length) payload.sequential = true;
|
|
949
|
+
if (everyone && named.length) payload.order = "auto";
|
|
948
950
|
return payload;
|
|
949
951
|
}
|
|
950
952
|
const mode = element("message-kind").value;
|
|
@@ -1067,6 +1069,18 @@ async function change_member({ member_id, name, action, role, latest_id }) {
|
|
|
1067
1069
|
} catch (error) { if (state.room?.id === room_id) state.free_error = error.message; }
|
|
1068
1070
|
}
|
|
1069
1071
|
|
|
1072
|
+
/** Reorder who has not spoken yet against the revision the board showed; a changed order is refused and the board reloads. */
|
|
1073
|
+
async function reorder_speakers({ revision, order }) {
|
|
1074
|
+
const room_id = state.room.id;
|
|
1075
|
+
state.order_error = null;
|
|
1076
|
+
try { await request_json(`/api/rooms/${room_id}/speaking-order`, { revision, order }); }
|
|
1077
|
+
catch (error) {
|
|
1078
|
+
// 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).
|
|
1079
|
+
state.order_error = { room_id, message: error.status === 409 ? "发言顺序刚有变化,已更新为最新顺序,请重新调整" : error.message };
|
|
1080
|
+
}
|
|
1081
|
+
if (state.room?.id === room_id) await refresh_room();
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1070
1084
|
/** Invite an independent member, retaining its credential before either delivery path runs. */
|
|
1071
1085
|
async function invite_member({ name, access, project_access = "read", session_id = "" }) {
|
|
1072
1086
|
const room_id = state.room.id;
|
|
@@ -3158,7 +3172,7 @@ function view_snapshot() {
|
|
|
3158
3172
|
attachment_drafts: state.attachment_drafts.map(item => ({ id: item.id, file_name: item.file.name, size: item.file.size, preview_url: item.preview_url })),
|
|
3159
3173
|
composer_error: state.composer_error, upload_status: state.upload_status, sending_message: state.sending_message,
|
|
3160
3174
|
mention_only: state.mention_only, reply_to: state.reply_to, turn_reply_to: state.turn_reply_to,
|
|
3161
|
-
free_notice: state.free_notice, free_error: state.free_error, free_proxy: state.free_proxy,
|
|
3175
|
+
free_notice: state.free_notice, free_error: state.free_error, free_proxy: state.free_proxy, order_error: state.order_error,
|
|
3162
3176
|
create_parent: state.create_parent, access: access_snapshot(), admin: admin_snapshot(),
|
|
3163
3177
|
tab: state.tab, snapshot_id: state.snapshot_id, context_dirty: state.context_dirty,
|
|
3164
3178
|
theme: document.documentElement.dataset.theme, fields, experts, launch: structuredClone(state.launch), named_followup: state.named_followup, create_discussion: state.create_discussion };
|
|
@@ -3331,7 +3345,7 @@ async function execute_view_action(name, value) {
|
|
|
3331
3345
|
copy_online: () => copy_context(), copy_cli: () => copy_context("cli"), copy_background,
|
|
3332
3346
|
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, reopen_discussion,
|
|
3333
3347
|
save_launch_defaults, launch_current_room, resume_launch, discard_launch, retry_launch_connection, check_launch_connection: load_launch_settings,
|
|
3334
|
-
load_room_configuration, save_room_configuration, interrupt_member, change_member, invite_member, set_proxy, copy_online_notice,
|
|
3348
|
+
load_room_configuration, save_room_configuration, interrupt_member, change_member, reorder_speakers, invite_member, set_proxy, copy_online_notice,
|
|
3335
3349
|
prepare_brainstorm, return_conclusion, convert_suggestion, adopt_recommendation, rate_decision,
|
|
3336
3350
|
busy: () => { throw new Error("上一个操作仍在处理中,请稍候;长时间无响应请刷新页面"); },
|
|
3337
3351
|
copy_reading: () => copy_text(`${location.origin}/r/${state.room.id}`), copy: copy_text };
|