ai_co_discuss 0.3.82 → 0.3.83
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/references/discussion.md +123 -113
- package/.agents/skills/discuss-host/references/free.md +27 -1
- package/.agents/skills/workflow-creator/SKILL.md +1 -1
- package/backend/free_room.py +137 -6
- package/backend/schema.sql +9 -0
- package/backend/server.py +11 -2
- package/docs/runner.md +3 -2
- package/package.json +1 -1
- package/runner/discuss.py +1 -1
- package/runner/discussion_launch.mjs +184 -11
- package/runner/expert_prompts.mjs +2 -2
- package/web/app.js +11 -3
- package/web/modern.css +1 -1
- package/web/modern.js +28 -28
package/docs/runner.md
CHANGED
|
@@ -113,8 +113,9 @@ API 成员可以担任专家或主持人。房间指定了项目目录时,它
|
|
|
113
113
|
|
|
114
114
|
工作流把一种多人协作的做法存下来,供别的房间复用:一份给主持人读的做法说明(SKILL.md),加一组 Agent 卡片(每张就是一个专家预设)。建房或普通邀请时在“工作流”一行选中它,只供查看和编辑,不添加成员,也不随建房启动。建房后,在输入框要求主持人启动(打 `/` 选工作流可以直接写好这句请求,权限见下文);主持人此后按说明组织工作。它只带来做法和成员,不保证工序,具体怎么做仍由主持人判断,你随时可以 @ 任何成员。
|
|
115
115
|
|
|
116
|
-
- **保存**:在“工作流”一行点“添加”,选“新建”,或导入别人分享的 zip
|
|
117
|
-
- **让主持人自己建**:建房时(第 3 节“协作方式”)或在“成员与设置 →
|
|
116
|
+
- **保存**:在“工作流”一行点“添加”,选“新建”,或导入别人分享的 zip 或文件夹(手机、平板上同样可用)。导入的内容先在编辑页预览,点保存才写入。选中一个工作流后,下方显示它的描述,可“查看说明”(做法说明与成员卡片)或“编辑”,删除在编辑页底部。编辑页可以“从当前房间成员导入”卡片,提示词保持原文。想让主持人起草,打开下面的“主持人完整权限”。
|
|
117
|
+
- **让主持人自己建**:建房时(第 3 节“协作方式”)或在“成员与设置 → 协作设置”勾选“主持人完整权限”后,直接对主持人说要什么分工(或“把这个房间的做法存成工作流”),它会自己保存、在本房间启用,并邀请工作流里的成员,接着就按流程派活,不需要你导入或添加成员。之后让它改流程,改动从成员下一次执行起生效。打开这个开关后,也可以在输入框输入 `/` 选一个已保存的工作流,输入框里会写好“启用工作流「名称」(ID),”,补上要做的事发给主持人,它就启用这个工作流并带来成员(没打开时该项灰显)。主持人带来的成员项目权限最高为可写;只有本地软件担任的主持人能这样做,API 主持人不行。
|
|
118
|
+
- **主持人完整权限的其他控制**:同一个开关打开后,主持人会自己处理房间里的问题、持续推进:成员跑偏时插话、卡住时打断或重试、模型不可用时换模型、无法恢复时换人;你也可以直接说“停下写手”“全部停下”“下一个让批评家先说”“请一位 Codex 进来只读评审”“把记录员请出去”“开个 Goal 把剩下几集写完”“暂停讨论”。邀请只用本机已配置的软件,项目权限最高可写;它自己开的 Goal 用你的默认预算(不限时为 8 次续跑、90 分钟)。成员连续 10 分钟没有新输出时,系统会提醒主持人检查;主持人派出长任务时还可以给自己设定时检查(每 5–120 分钟一次,房间空闲后自动结束)。它也能撤回自己派出的活,正在循环的接力会立即停下。以下仍由你决定:对主持人自己操作或更换主持人、批准超出上限的轮数、高于可写的权限或放行工具、改项目目录、撤回你的消息、私聊。API 主持人没有命令行,不能这样做。
|
|
118
119
|
- **分享**:工作流存在本机服务目录的 `workflows` 文件夹,一个工作流一个文件夹;编辑页可复制路径,把整个文件夹(或打成 zip)发给对方即可分享。导入时会列出每张卡片的提示词和项目权限;要求写文件的卡片默认按只读导入,逐张勾选后才放开。
|
|
119
120
|
- **修改与结束**:修改工作流会作用于所有启用它的房间,成员下一次执行就按新内容;新增的卡片不会自动进入房间,主持人用到时会自己补上,也可以在右栏工作流一行点刷新图标立即补齐(删掉的卡片,其成员留作普通成员;与库一致时不显示该图标);工作流卡片的提示词只读,在“工作流”一行点“编辑”修改。工作流进行中时,主持人只在请求和它的用途相符(或你说按工作流做)时才用,其他请求照常处理。用完在“成员与设置 → 协作设置”的工作流一行点请离成员图标(或逐个让成员离开):工作流带来的成员离开(主持人保留),正在进行的工作随之停止,工作流结束,右栏这一块消失;成员会话保留,下次在输入框打 `/` 选它就把原成员请回来,接着原来的上下文。换工作流时直接选另一个,原来的成员留作普通成员。
|
|
120
121
|
|
package/package.json
CHANGED
package/runner/discuss.py
CHANGED
|
@@ -234,7 +234,7 @@ def room_commands(subcommands, registry):
|
|
|
234
234
|
elif action in ("view", "brief"):
|
|
235
235
|
parser.add_argument("--request", type=positive_int, help="手动成员:按自己的有效请求限制可读原文")
|
|
236
236
|
events = add_leaf(subcommands, registry, "room events",
|
|
237
|
-
"
|
|
237
|
+
"房主或当前主持人读取房间事件记录(主持人看不到私聊与消息正文):用户与程序的每次写入(含被拒的)及其引起的状态变化,按发生顺序,用于排查问题;默认最新 500 条", True)
|
|
238
238
|
paging = events.add_mutually_exclusive_group()
|
|
239
239
|
paging.add_argument("--after", type=non_negative_int, help="只读此事件编号之后的记录")
|
|
240
240
|
paging.add_argument("--before", type=non_negative_int, help="读此事件编号之前的一页;按返回的 more 继续向前")
|
|
@@ -84,6 +84,7 @@ const HELP = `用法:codiscuss launch settings [--file FILE]
|
|
|
84
84
|
codiscuss launch machine-link --file FILE
|
|
85
85
|
codiscuss launch workflow-adopt (--draft FILE | --id WORKFLOW) [--access-file FILE]
|
|
86
86
|
codiscuss launch workflow-join [--card ID[,ID]] [--access-file FILE]
|
|
87
|
+
codiscuss launch host-control ACTION [选项] [--access-file FILE]
|
|
87
88
|
codiscuss launch workflow-check (--dir DIR | --draft FILE)
|
|
88
89
|
codiscuss launch workflow-save (--dir DIR | --draft FILE)
|
|
89
90
|
|
|
@@ -115,8 +116,21 @@ room-member-adapter 把自由房间里空闲的 Codex 成员在 App(codex)
|
|
|
115
116
|
room-profiles 导出房间的本机成员软件配置与实际会话 ID(不含凭据),供备份附带;须提供 site、room_id、token。
|
|
116
117
|
room-restore 把导入房间的本机成员配置登记到本机并核对原会话存在,Claude Code 成员保持手动;须提供 site、room_id、token、local_members。
|
|
117
118
|
room-fork 把从原房间分支出的新房间登记到本机,各本机成员从原会话分叉、不含分支点之后的执行,原会话不变;须提供 site、room_id、token、source_room_id、source_token、executions({成员名: [排除的执行编号]})。
|
|
118
|
-
workflow-adopt 主持人在本房间启用工作流:--draft 把自己的草稿(一段,分段头 1/1)保存进工作流库后启用,--id
|
|
119
|
+
workflow-adopt 主持人在本房间启用工作流:--draft 把自己的草稿(一段,分段头 1/1)保存进工作流库后启用,--id 启用工作流库里已有的工作流(不改动它);缺的卡片成员由本机服务邀请,项目权限最高可写;须房主已为本房间打开“主持人完整权限”。凭据为本次任务的 access_file(或 DISCUSS_ACCESS_FILE)。
|
|
119
120
|
workflow-join 主持人为本房间在用工作流里还没有成员的卡片(启用后才新增的)邀请成员,不填 --card 即全部;无需额外授权,离开或移除的成员由用户请回。凭据同上。
|
|
121
|
+
host-control 主持人代房主控制房间,须房主已为本房间打开“主持人完整权限”,凭据同上;成员用 --member(成员 ID 或显示名),不能是主持人自己:
|
|
122
|
+
诊断:details --execution N 读取该执行的本机过程日志(服务端详情与事件记录用 execution / room events 读取);
|
|
123
|
+
恢复:stop --member 打断;stop-all 停止全部执行、请求与接力;steer --member --text 话 [--submission-id ID] 向它进行中的执行插话;
|
|
124
|
+
retry --execution N 重试失败的执行;model --member [--model M] [--effort E] 改模型与强度(空值恢复默认);
|
|
125
|
+
adapter --member --adapter ID 在同一软件的 App 与 CLI 间切换;access --member --access read|write 调整项目权限;
|
|
126
|
+
成员:invite --name 平台 [--access read|write] [--model M] [--effort E] 用本机已配置的软件邀请,项目权限最高可写;
|
|
127
|
+
away / remove / back --member 暂离、移除、请回;label --member [--label 显示名] [--description 描述];
|
|
128
|
+
撤回:withdraw --message N 撤回自己的一次派活:接力或循环停下,未开始的请求取消,进行中的停止,不再交回;
|
|
129
|
+
兜底:checkin --minutes N 房间有进行中的工作时每 N 分钟(5–120)给你发一次定时检查,房间空闲、停止全部或换主持人后自动结束;checkin --off 关闭;
|
|
130
|
+
推进:order 读取发言顺序,order --order 甲,乙 --revision REV 调整尚未发言的部分;
|
|
131
|
+
discussion 读取讨论,discussion --do start --goal 目标 --revision REV 开启(上限用房间默认值),--do pause|resume|end --revision REV;
|
|
132
|
+
goal 读取 Goal,goal --do start --goal 目标 --revision REV 开启(预算用房主默认值,默认不限时为 8 次续跑、90 分钟),--do pause|resume|end --revision REV。
|
|
133
|
+
不开放:批准超出上限的轮数、项目权限高于可写或放行工具、改项目目录、撤回回退用户消息、私聊、对主持人自己操作。
|
|
120
134
|
workflow-check 按导入规则校验工作流文件夹或单段草稿,返回预览,不保存;workflow-save 校验后保存进工作流库(同 ID 覆盖),卡片按声明的项目权限保存。写法见 workflow-creator 技能。
|
|
121
135
|
JSON 指定 site、entry(conversation/web)、room(建房内容,含 submission_id)
|
|
122
136
|
或 room_id(已有房间)、participants(本次软件覆盖配置)。
|
|
@@ -605,9 +619,9 @@ function validate_profile(name, profile, binding = false) {
|
|
|
605
619
|
}
|
|
606
620
|
|
|
607
621
|
/** Send one request without following redirects that could disclose the room credential. */
|
|
608
|
-
async function site_request(site, path, token, body) {
|
|
622
|
+
async function site_request(site, path, token, body, headers = {}) {
|
|
609
623
|
const response = await fetch(`${site}${path}`, { method: body === undefined ? 'GET' : 'POST',
|
|
610
|
-
headers: { 'Content-Type': 'application/json', 'X-Discussion-Token': token },
|
|
624
|
+
headers: { 'Content-Type': 'application/json', 'X-Discussion-Token': token, ...headers },
|
|
611
625
|
body: body === undefined ? undefined : JSON.stringify(body), redirect: 'error', signal: AbortSignal.timeout(10000) });
|
|
612
626
|
const result = await response.json();
|
|
613
627
|
if (!response.ok) throw new Error(`HTTP ${response.status}: ${result.error || '请求失败'}`);
|
|
@@ -810,7 +824,7 @@ function manual_access(path, input, name, token) {
|
|
|
810
824
|
* The local configuration is written before the website call, so a retry reuses the same credential.
|
|
811
825
|
* A manual member answers with its own credential and is never woken.
|
|
812
826
|
*/
|
|
813
|
-
export async function room_member(home, raw, locked = false) {
|
|
827
|
+
export async function room_member(home, raw, locked = false, headers = {}) {
|
|
814
828
|
const { input, path } = await member_room_context(home, raw);
|
|
815
829
|
const { action, name } = input;
|
|
816
830
|
const access = input.access || 'runner';
|
|
@@ -826,12 +840,12 @@ export async function room_member(home, raw, locked = false) {
|
|
|
826
840
|
if (action === 'add' && input.new_instance) {
|
|
827
841
|
// A name once bound keeps its session locally, so a new instance never reuses a removed member's name.
|
|
828
842
|
const taken = new Set([...roster.map(member => member.name), ...Object.keys(config.participants), ...Object.keys(config.members || {})]);
|
|
829
|
-
return room_member(home, { ...raw, name: next_instance(platform_of(name), taken), new_instance: false }, true);
|
|
843
|
+
return room_member(home, { ...raw, name: next_instance(platform_of(name), taken), new_instance: false }, true, headers);
|
|
830
844
|
}
|
|
831
845
|
const current = roster.find(member => member.name === name);
|
|
832
846
|
if (input.member_token !== undefined && (typeof input.member_token !== 'string' || input.member_token.length < 32 || input.member_token.length > 256)) throw new Error('member_token 须为 32–256 字符');
|
|
833
847
|
if (current?.status === 'active' && input.member_token && config.members?.[name]?.token === input.member_token) {
|
|
834
|
-
const view = await site_request(input.site, `/api/rooms/${input.room_id}/members/${current.id}`, input.token, { action: 'active', token: input.member_token });
|
|
848
|
+
const view = await site_request(input.site, `/api/rooms/${input.room_id}/members/${current.id}`, input.token, { action: 'active', token: input.member_token }, headers);
|
|
835
849
|
return { room_id: input.room_id, member: name, access: current.access, free: view, access_file: null };
|
|
836
850
|
}
|
|
837
851
|
if (action === 'add' && !current && config.participants[name] && !locked) {
|
|
@@ -842,7 +856,7 @@ export async function room_member(home, raw, locked = false) {
|
|
|
842
856
|
&& !roster.some(member => member.name === key));
|
|
843
857
|
const renamed = retried || free_instance(name, taken);
|
|
844
858
|
const participants = raw.participants?.[name] ? { participants: { ...raw.participants, [renamed]: raw.participants[name] } } : {};
|
|
845
|
-
return room_member(home, { ...raw, name: renamed, ...participants }, true);
|
|
859
|
+
return room_member(home, { ...raw, name: renamed, ...participants }, true, headers);
|
|
846
860
|
}
|
|
847
861
|
if (action === 'rejoin' && current?.status !== 'away') throw new Error(`${name} 不是离开状态,无需请回`);
|
|
848
862
|
if (action === 'add' && current) throw new Error(`${name} 已是本房间成员;离开的成员请用请回`);
|
|
@@ -866,7 +880,7 @@ export async function room_member(home, raw, locked = false) {
|
|
|
866
880
|
? { name, adapter: manual ? '' : profile.adapter, token, access, session_mode: profile?.session_id ? 'borrowed' : 'new' }
|
|
867
881
|
: { action: 'active', token };
|
|
868
882
|
const view = await site_request(input.site, action === 'add' ? `/api/rooms/${input.room_id}/members` : `/api/rooms/${input.room_id}/members/${current.id}`,
|
|
869
|
-
input.token, body);
|
|
883
|
+
input.token, body, headers);
|
|
870
884
|
return { room_id: input.room_id, member: name, access: manual ? 'manual' : 'runner', free: view, access_file };
|
|
871
885
|
} finally { release(); }
|
|
872
886
|
}
|
|
@@ -1623,7 +1637,7 @@ async function host_room(home, access_file) {
|
|
|
1623
1637
|
const room = await api(config, '');
|
|
1624
1638
|
const roster = room.free?.members || [];
|
|
1625
1639
|
if (!host || !roster.some(member => member.name === host && member.role === 'host' && member.status === 'active')) throw new Error('只有本房间的主持人能运行此命令');
|
|
1626
|
-
return { path, config, roster };
|
|
1640
|
+
return { path, config, roster, labels: room.labels || {}, host };
|
|
1627
1641
|
}
|
|
1628
1642
|
|
|
1629
1643
|
/**
|
|
@@ -1722,6 +1736,154 @@ export async function join_workflow(home, { access_file, cards }) {
|
|
|
1722
1736
|
return sync_workflow_cards(home, path, config, roster, cards?.length ? cards : known.filter(card => !mapped.includes(card)));
|
|
1723
1737
|
}
|
|
1724
1738
|
|
|
1739
|
+
// When the owner left the room's goal budget at no limit, a goal the host opens itself still stops by these.
|
|
1740
|
+
const HOST_GOAL_BUDGET = { max_continues: 8, max_minutes: 90 };
|
|
1741
|
+
const LIVE_EXECUTION = ['dispatched', 'started'];
|
|
1742
|
+
|
|
1743
|
+
/** The active-or-away member a host names by member ID or display name; never the host itself. */
|
|
1744
|
+
function host_target({ roster, labels, host }, wanted) {
|
|
1745
|
+
if (!wanted) throw new Error('须用 --member 指定成员(成员 ID 或显示名)');
|
|
1746
|
+
const member = roster.find(item => item.status !== 'removed' && (item.name === wanted || labels[item.name] === wanted));
|
|
1747
|
+
if (!member) throw new Error(`本房间没有成员 ${wanted}`);
|
|
1748
|
+
if (member.name === host) throw new Error('主持人不能对自己执行此操作;更换或移除主持人由用户决定');
|
|
1749
|
+
return member;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
/** The profile a host-invited member starts from: this computer's saved settings, project access at most write. */
|
|
1753
|
+
function host_invite_profile(home, name, { access, model, effort }) {
|
|
1754
|
+
if (access && !HOST_CARD_ACCESS.includes(access)) throw new Error('主持人邀请的成员项目权限只能是 read 或 write');
|
|
1755
|
+
if (!card_platform_ready(home, name)) {
|
|
1756
|
+
const ready = [...new Set([...Object.keys(launch_settings(home).participants), ...Object.keys(API_PROVIDERS).filter(platform => card_api_profile(home, platform))])];
|
|
1757
|
+
throw new Error(`本机没有配置 ${name} 的软件或 API Key;只能邀请本机已配置的平台:${ready.join('、')}`);
|
|
1758
|
+
}
|
|
1759
|
+
const api = card_api_profile(home, platform_of(name));
|
|
1760
|
+
const saved = launch_settings(home).participants[platform_of(name)]?.project_access;
|
|
1761
|
+
const project_access = access || (HOST_CARD_ACCESS.includes(saved) ? saved : 'write');
|
|
1762
|
+
return { ...api, project_access, ...(model ? { model } : {}), ...(effort && !api ? { effort } : {}) };
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
/** The positive execution number a host passes with --execution. */
|
|
1766
|
+
function execution_number(value) {
|
|
1767
|
+
const id = Number(value);
|
|
1768
|
+
if (!Number.isSafeInteger(id) || id <= 0) throw new Error('须用 --execution 指定执行编号');
|
|
1769
|
+
return id;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
/** Read the discussion, or start (room default limits), pause, resume or end it with the revision just read. */
|
|
1773
|
+
function host_discussion(ctx, { do: step, goal, revision, submission_id }) {
|
|
1774
|
+
if (!step) return ctx.call('/discussion');
|
|
1775
|
+
if (step === 'start') {
|
|
1776
|
+
if (!goal) throw new Error('开启讨论须用 --goal 写明讨论目标');
|
|
1777
|
+
return ctx.call('/discussion', { action: 'start', goal, revision, submission_id: submission_id || randomUUID() });
|
|
1778
|
+
}
|
|
1779
|
+
if (!['pause', 'resume', 'end'].includes(step)) throw new Error('--do 须为 start、pause、resume 或 end');
|
|
1780
|
+
return ctx.call('/discussion', { action: step, revision });
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
/** Read the goal, or open it under the owner's default budget (a bounded one when that is unlimited), pause, resume or end it. */
|
|
1784
|
+
function host_goal(ctx, { do: step, goal, revision }) {
|
|
1785
|
+
if (!step) return ctx.call('/goal');
|
|
1786
|
+
if (step === 'start') {
|
|
1787
|
+
if (!goal) throw new Error('开启 Goal 须用 --goal 写明目标');
|
|
1788
|
+
const preferences = launch_settings(ctx.home).create_preferences || {};
|
|
1789
|
+
const budget = {
|
|
1790
|
+
max_continues: preferences.goal_max_continues || HOST_GOAL_BUDGET.max_continues,
|
|
1791
|
+
max_minutes: preferences.goal_max_minutes || HOST_GOAL_BUDGET.max_minutes,
|
|
1792
|
+
};
|
|
1793
|
+
return ctx.call('/goal', { action: 'start', objective: goal, revision, ...budget });
|
|
1794
|
+
}
|
|
1795
|
+
if (!['pause', 'resume', 'end'].includes(step)) throw new Error('--do 须为 start、pause、resume 或 end');
|
|
1796
|
+
return ctx.call('/goal', { action: step === 'end' ? 'clear' : step, revision });
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
/** Steer a member's running execution with an interjection; a retry passes the same --submission-id. */
|
|
1800
|
+
function host_steer(ctx, { member: wanted, text, submission_id }) {
|
|
1801
|
+
const member = host_target(ctx, wanted);
|
|
1802
|
+
if (!text) throw new Error('须用 --text 写明要对它说的话');
|
|
1803
|
+
if (!LIVE_EXECUTION.includes(member.execution?.status)) throw new Error(`${wanted} 当前没有进行中的执行;要它发言请在回答里用 --to 点名`);
|
|
1804
|
+
return ctx.call(`/executions/${member.execution.id}`, { action: 'interject', text, submission_id: submission_id || randomUUID() });
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
/** Set a member's display name and, optionally, its description. */
|
|
1808
|
+
async function host_label(ctx, { member: wanted, label, description }) {
|
|
1809
|
+
const member = host_target(ctx, wanted);
|
|
1810
|
+
if (!label && !description) throw new Error('须用 --label 或 --description 指定显示名或描述');
|
|
1811
|
+
if (label) await ctx.call('/labels', { labels: { [member.name]: label } });
|
|
1812
|
+
if (description) await ctx.call('/descriptions', { descriptions: { [member.name]: description } });
|
|
1813
|
+
return { member: member.name, label: label || null, description: description || null };
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
/** Invite a member from this computer's configured software, project access at most write. */
|
|
1817
|
+
async function host_invite(ctx, options) {
|
|
1818
|
+
if (!options.name) throw new Error('须用 --name 指定要邀请的平台(如 Claude、Codex)');
|
|
1819
|
+
const taken = new Set(ctx.roster.map(member => member.name));
|
|
1820
|
+
const name = taken.has(options.name) ? free_instance(options.name, taken) : options.name;
|
|
1821
|
+
const profile = host_invite_profile(ctx.home, name, options);
|
|
1822
|
+
const joined = await room_member(ctx.home, { ...ctx.owner, action: 'add', name, participants: { [name]: profile } }, false, ctx.headers);
|
|
1823
|
+
return { member: joined.member, project_access: profile.project_access };
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
/** Read the speaking order, or change its not-yet-spoken part with the revision just read; display names are accepted. */
|
|
1827
|
+
function host_order(ctx, { order, revision }) {
|
|
1828
|
+
if (!order) return ctx.call('/speaking-order');
|
|
1829
|
+
if (!revision) throw new Error('调整发言顺序前先不带 --order 读取当前顺序,再用 --revision 带上其 revision');
|
|
1830
|
+
const names = order.split(',').map(name => name.trim()).filter(Boolean)
|
|
1831
|
+
.map(name => ctx.roster.find(member => member.status === 'active' && ctx.labels[member.name] === name && member.name !== name)?.name || name);
|
|
1832
|
+
return ctx.call('/speaking-order', { revision, order: names });
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
const HOST_CONTROLS = {
|
|
1836
|
+
stop: (ctx, o) => ctx.call(`/members/${host_target(ctx, o.member).id}/interrupt`, {}),
|
|
1837
|
+
'stop-all': ctx => ctx.call('/stop', {}),
|
|
1838
|
+
steer: host_steer,
|
|
1839
|
+
retry: (ctx, o) => ctx.call(`/executions/${execution_number(o.execution)}`, { action: 'retry' }),
|
|
1840
|
+
details: (ctx, o) => execution_details_command(ctx.home, { ...ctx.owner, execution_id: execution_number(o.execution) }),
|
|
1841
|
+
order: host_order,
|
|
1842
|
+
invite: host_invite,
|
|
1843
|
+
away: (ctx, o) => ctx.call(`/members/${host_target(ctx, o.member).id}`, { action: 'away' }),
|
|
1844
|
+
remove: (ctx, o) => ctx.call(`/members/${host_target(ctx, o.member).id}`, { action: 'remove' }),
|
|
1845
|
+
back: (ctx, o) => room_member(ctx.home, { ...ctx.owner, action: 'rejoin', name: host_target(ctx, o.member).name }, false, ctx.headers),
|
|
1846
|
+
model: (ctx, o) => room_member_model(ctx.home, { ...ctx.owner, name: host_target(ctx, o.member).name, model: o.model || '', effort: o.effort || '' }),
|
|
1847
|
+
access: (ctx, o) => {
|
|
1848
|
+
if (!HOST_CARD_ACCESS.includes(o.access)) throw new Error('主持人只能把成员的项目权限设为 read 或 write');
|
|
1849
|
+
return room_member_access(ctx.home, { ...ctx.owner, name: host_target(ctx, o.member).name, project_access: o.access });
|
|
1850
|
+
},
|
|
1851
|
+
adapter: (ctx, o) => room_member_adapter(ctx.home, { ...ctx.owner, name: host_target(ctx, o.member).name, adapter: o.adapter }),
|
|
1852
|
+
label: host_label,
|
|
1853
|
+
discussion: host_discussion,
|
|
1854
|
+
goal: host_goal,
|
|
1855
|
+
withdraw: (ctx, o) => {
|
|
1856
|
+
const id = Number(o.message);
|
|
1857
|
+
if (!Number.isSafeInteger(id) || id <= 0) throw new Error('须用 --message 指定要撤回的派活消息编号');
|
|
1858
|
+
return ctx.call(`/dispatches/${id}/withdraw`, {});
|
|
1859
|
+
},
|
|
1860
|
+
checkin: (ctx, o) => {
|
|
1861
|
+
if (o.off) return ctx.call('/host-checkin', { off: true });
|
|
1862
|
+
const minutes = Number(o.minutes);
|
|
1863
|
+
if (!Number.isSafeInteger(minutes)) throw new Error('须用 --minutes 指定间隔分钟数(5–120),或 --off 关闭');
|
|
1864
|
+
return ctx.call('/host-checkin', { minutes });
|
|
1865
|
+
},
|
|
1866
|
+
};
|
|
1867
|
+
|
|
1868
|
+
/**
|
|
1869
|
+
* The host runs a room control the owner otherwise does in the web page, when the owner granted this room full host
|
|
1870
|
+
* control (`host_workflows`), so it can keep work moving and handle what goes wrong by itself: stop, steer or retry a
|
|
1871
|
+
* member, read an execution's local log, change the speaking order, invite (configured software, at most write), send
|
|
1872
|
+
* away, remove or bring back members, change a member's model, effort, client or access (read/write only), label it,
|
|
1873
|
+
* and read, start, pause, resume or end the discussion or goal. The site checks every write as for the owner (revisions
|
|
1874
|
+
* included). What stays the owner's: acting on the host itself, approving rounds beyond the limit, access above write
|
|
1875
|
+
* or tool approvals, project directories, the owner's messages and private chats.
|
|
1876
|
+
*/
|
|
1877
|
+
export async function host_control(home, action, options) {
|
|
1878
|
+
if (!Object.hasOwn(HOST_CONTROLS, action || '')) throw new Error(`操作须为 ${Object.keys(HOST_CONTROLS).join('、')} 之一`);
|
|
1879
|
+
const { config, roster, labels, host } = await host_room(home, options.access_file);
|
|
1880
|
+
if (!config.host_workflows) throw new Error('房主没有为本房间打开“主持人完整权限”;请让用户在电脑上“成员与设置 → 协作设置”打开后再试');
|
|
1881
|
+
const headers = { 'User-Agent': `AI-CoDiscuss-host:${host}` };
|
|
1882
|
+
const owner = { site: config.site, room_id: config.room, token: config.token };
|
|
1883
|
+
const call = (path, body) => site_request(config.site, `/api/rooms/${config.room}${path}`, config.token, body, headers);
|
|
1884
|
+
return HOST_CONTROLS[action]({ home, config, roster, labels, host, headers, owner, call }, options);
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1725
1887
|
/** Cards as the host may bring them: any access above writing is lowered to it. */
|
|
1726
1888
|
const host_cards = agents => agents.map(agent => agent.project_access && !HOST_CARD_ACCESS.includes(agent.project_access)
|
|
1727
1889
|
? { ...agent, project_access: 'write' } : agent);
|
|
@@ -1736,7 +1898,7 @@ const host_cards = agents => agents.map(agent => agent.project_access && !HOST_C
|
|
|
1736
1898
|
export async function adopt_workflow(home, { access_file, draft_file, workflow_id }) {
|
|
1737
1899
|
if (!draft_file === !workflow_id) throw new Error('须提供 --draft 草稿文件或 --id 工作流库中的工作流 ID 之一;凭据用 --access-file 或 DISCUSS_ACCESS_FILE');
|
|
1738
1900
|
const { config, roster } = await host_room(home, access_file);
|
|
1739
|
-
if (!config.host_workflows) throw new Error('
|
|
1901
|
+
if (!config.host_workflows) throw new Error('房主没有为本房间打开“主持人完整权限”;请让用户在电脑上“成员与设置 → 协作设置”打开后再试');
|
|
1740
1902
|
const bound = config.workflow?.id;
|
|
1741
1903
|
let workflow;
|
|
1742
1904
|
if (workflow_id) workflow = get_workflow(home, workflow_id);
|
|
@@ -1795,6 +1957,10 @@ export async function launch_main(args = process.argv.slice(2), input_text) {
|
|
|
1795
1957
|
file: { type: 'string' }, 'dry-run': { type: 'boolean' }, browser: { type: 'boolean' }, remote: { type: 'boolean' }, device: { type: 'string' },
|
|
1796
1958
|
'configuration-locked': { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
|
|
1797
1959
|
draft: { type: 'string' }, 'access-file': { type: 'string' }, dir: { type: 'string' }, card: { type: 'string' }, id: { type: 'string' },
|
|
1960
|
+
member: { type: 'string' }, order: { type: 'string' }, revision: { type: 'string' }, name: { type: 'string' }, access: { type: 'string' },
|
|
1961
|
+
model: { type: 'string' }, effort: { type: 'string' }, do: { type: 'string' }, execution: { type: 'string' }, text: { type: 'string' },
|
|
1962
|
+
goal: { type: 'string' }, label: { type: 'string' }, description: { type: 'string' }, adapter: { type: 'string' }, 'submission-id': { type: 'string' },
|
|
1963
|
+
message: { type: 'string' }, minutes: { type: 'string' }, off: { type: 'boolean' },
|
|
1798
1964
|
} });
|
|
1799
1965
|
if (values.help || !positionals.length) return process.stdout.write(HELP);
|
|
1800
1966
|
if (positionals[0] === 'workflow-adopt') {
|
|
@@ -1802,6 +1968,13 @@ export async function launch_main(args = process.argv.slice(2), input_text) {
|
|
|
1802
1968
|
const result = await adopt_workflow(home, { draft_file: values.draft, workflow_id: values.id, access_file: values['access-file'] || process.env.DISCUSS_ACCESS_FILE });
|
|
1803
1969
|
return process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
1804
1970
|
}
|
|
1971
|
+
if (positionals[0] === 'host-control') {
|
|
1972
|
+
const { member, order, revision, name, access, model, effort, execution, text, goal, label, description, adapter } = values;
|
|
1973
|
+
const result = await host_control(service_home(), positionals[1], { member, order, revision, name, access, model, effort, execution, text, goal,
|
|
1974
|
+
label, description, adapter, message: values.message, minutes: values.minutes, off: values.off, do: values.do, submission_id: values['submission-id'], access_file: values['access-file'] || process.env.DISCUSS_ACCESS_FILE });
|
|
1975
|
+
return process.stdout.write(`${JSON.stringify(result)}
|
|
1976
|
+
`);
|
|
1977
|
+
}
|
|
1805
1978
|
if (positionals[0] === 'workflow-join') {
|
|
1806
1979
|
const cards = (values.card || '').split(',').map(card => card.trim()).filter(Boolean);
|
|
1807
1980
|
const result = await join_workflow(service_home(), { cards, access_file: values['access-file'] || process.env.DISCUSS_ACCESS_FILE });
|
|
@@ -1818,7 +1991,7 @@ export async function launch_main(args = process.argv.slice(2), input_text) {
|
|
|
1818
1991
|
// Another device's request may only use what this computer granted; the flag is set by the relay, never by the request.
|
|
1819
1992
|
if (values.remote) {
|
|
1820
1993
|
guard_remote_request(positionals[0], input, { projects: launch_projects(home).projects, participants: launch_settings(home).participants, home });
|
|
1821
|
-
if (positionals[0] === 'room-workflow' && input?.action === 'host') throw new Error('
|
|
1994
|
+
if (positionals[0] === 'room-workflow' && input?.action === 'host') throw new Error('“主持人完整权限”只能在电脑上开启');
|
|
1822
1995
|
// Another device repeats the saved program locations it cannot see; keep them rather than dropping them.
|
|
1823
1996
|
if (positionals[0] === 'settings' && input?.participants) input.participants = keep_saved_locations(home, input.participants);
|
|
1824
1997
|
// Tool approvals saved here stay this computer's decision; a room created from another device does not take them.
|
|
@@ -74,9 +74,9 @@ export function access_label(level) { return level === 'own' ? '自行决定' :
|
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
76
|
* Subcommands an automatic task may run; service, run, api, setup and the rest of launch stay unapproved.
|
|
77
|
-
* `launch workflow-adopt` and `launch workflow-join` check themselves that the caller is the room's host (adopt also
|
|
77
|
+
* `launch workflow-adopt` and `launch workflow-join` and `launch host-control` check themselves that the caller is the room's host (adopt and host-control also need the owner's grant).
|
|
78
78
|
*/
|
|
79
|
-
export const TASK_SUBCOMMANDS = ['room', 'message', 'turn', 'state', 'goal', 'mode', 'review', 'sequence', 'execution', 'health', 'schema', 'launch workflow-adopt', 'launch workflow-join'];
|
|
79
|
+
export const TASK_SUBCOMMANDS = ['room', 'message', 'turn', 'state', 'goal', 'mode', 'review', 'sequence', 'execution', 'health', 'schema', 'launch workflow-adopt', 'launch workflow-join', 'launch host-control'];
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
82
|
* Whether a member may run commands beyond the codiscuss subcommands: every level but 只读, and 可写 only with a project
|
package/web/app.js
CHANGED
|
@@ -3891,6 +3891,13 @@ function stop_summary(executions) {
|
|
|
3891
3891
|
}
|
|
3892
3892
|
|
|
3893
3893
|
/** Stop the whole free room: running work and everything still owed end; a new message starts again. */
|
|
3894
|
+
/** The owner turns off the host's timed check-in shown on the free board. */
|
|
3895
|
+
async function stop_host_checkin() {
|
|
3896
|
+
const room_id = state.room.id;
|
|
3897
|
+
await request_json(`/api/rooms/${room_id}/host-checkin`, { off: true });
|
|
3898
|
+
if (state.room?.id === room_id) await refresh_room();
|
|
3899
|
+
}
|
|
3900
|
+
|
|
3894
3901
|
async function stop_room() {
|
|
3895
3902
|
const room_id = state.room.id;
|
|
3896
3903
|
const live = (state.room.free?.executions || []).filter(item => ["dispatched", "started"].includes(item.status));
|
|
@@ -4100,8 +4107,9 @@ async function copy_fork_files(source_id, room_id, token, mapping) {
|
|
|
4100
4107
|
*/
|
|
4101
4108
|
async function fork_room(message_id) {
|
|
4102
4109
|
const source = state.room;
|
|
4103
|
-
|
|
4104
|
-
|
|
4110
|
+
if (!has_room_control(source.id)) throw new Error("此浏览器缺少房间控制凭据,请使用创建房间的浏览器或登录管理员");
|
|
4111
|
+
// Without the room credential the local service or the relay acts with this browser's administrator authority.
|
|
4112
|
+
const source_token = localStorage.getItem(`discuss-access:${source.id}`) || "";
|
|
4105
4113
|
const snapshot = await request_json(`/api/rooms/${encodeURIComponent(source.id)}/backup`);
|
|
4106
4114
|
const room_id = crypto.randomUUID().replaceAll("-", "").slice(0, 12);
|
|
4107
4115
|
const { body, executions } = fork_snapshot(snapshot, message_id, room_id);
|
|
@@ -4954,7 +4962,7 @@ async function execute_view_action(name, value) {
|
|
|
4954
4962
|
copy_cli: () => copy_context(), copy_background,
|
|
4955
4963
|
download_member_access, copy_member_access_text, copy_member_join, copy_member_launch: name => copy_member_join(name, true), pause_discussion, restart_discussion, start_room_discussion, end_room_discussion, start_room_goal, update_room_goal, goal_control,
|
|
4956
4964
|
save_launch_defaults, resume_launch, discard_launch, retry_launch_connection, check_launch_connection: load_launch_settings,
|
|
4957
|
-
load_room_configuration, save_room_configuration, interrupt_member, retry_delivery, retract_message, retract_latest, stop_room, rewind_and_send, fork_room, change_member, reorder_speakers, invite_member,
|
|
4965
|
+
load_room_configuration, save_room_configuration, interrupt_member, retry_delivery, retract_message, retract_latest, stop_room, stop_host_checkin, rewind_and_send, fork_room, change_member, reorder_speakers, invite_member,
|
|
4958
4966
|
enable_room_workflow, move_workflow_members, sync_room_workflow, set_room_host_workflows,
|
|
4959
4967
|
adopt_recommendation, rate_decision,
|
|
4960
4968
|
queue_send: id => queue_request({ action: "send", id }), queue_delete: delete_queued,
|