ai_co_discuss 0.3.49 → 0.3.51
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/README.md +1 -1
- package/docs/en/workflow.md +1 -1
- package/docs/runner.md +3 -1
- package/docs/workflow.md +1 -1
- package/package.json +1 -1
- package/runner/discussion_launch.mjs +58 -10
- package/runner/tts_providers.mjs +9 -5
- package/web/app.js +91 -45
- package/web/modern.css +1 -1
- package/web/modern.js +1759 -1722
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ Most multi-agent frameworks spin up fresh agents inside their own runtime. AI Co
|
|
|
30
30
|
- **Structured discussions** — set a goal and round / time limits; the host schedules speakers and follow-ups, then the room returns to free collaboration with history and conclusions kept.
|
|
31
31
|
- **Goal mode** — give the host an objective with `/goal`; whenever the room goes idle the host is woken to carry on, until it confirms the goal is complete, gets blocked, a budget runs out or you end it.
|
|
32
32
|
- **Smart Scheduling** — optionally let a decision model handle routine routing instead of waking the host.
|
|
33
|
-
- **Workflows** — save a way of working as a host playbook (`SKILL.md`) plus a set of agent cards
|
|
33
|
+
- **Workflows** — save a way of working as a host playbook (`SKILL.md`) plus a set of agent cards and share it as a folder. Browse and edit workflows when creating a room; start one later by asking the host in the message box or selecting it with `/` and confirming its members. It brings the method and the people, not a guaranteed procedure. See [runner docs](docs/runner.md#工作流可选).
|
|
34
34
|
- **Mix any participant** — local CLIs and desktop apps, model APIs and manually connected sessions in one room.
|
|
35
35
|
- **Shared workspace** — point a room at a project directory, grant each local agent read-only or writable access, and browse files with text, Markdown and static HTML previews from the room.
|
|
36
36
|
- **Live roster** — invite, pause, recall or remove members at any time, including resuming an existing agent session.
|
package/docs/en/workflow.md
CHANGED
|
@@ -48,7 +48,7 @@ A Goal keeps the host pursuing one objective: when the room goes idle, the local
|
|
|
48
48
|
|
|
49
49
|
- **Start**: send `/goal <objective>` and the host starts it as written; with a bare `/goal` the host draws the objective from the recent conversation and declines when it cannot. You can also click **Start Goal** (开启 Goal) in the member panel, or choose **Start Goal** under **After creating** (创建后) when creating a room. Typing `/` at the start of the input box lists Goal and Discussion in a **Modes** (模式) group; while one is running, it lists its pause, resume and end commands.
|
|
50
50
|
- **Progress**: after the host's dispatched work ends and the room stays idle for 30 seconds (not counting when you just spoke, a member is still working or messages are queued), the host is woken once, shown as "Goal 续跑 #N". If the host then hands out no work (it only answers or asks you), it is not woken again until you reply.
|
|
51
|
-
- **Budget**: unlimited by default.
|
|
51
|
+
- **Budget**: unlimited by default. Starting or editing a Goal only requires an objective; expand **Advanced settings** (高级设置) to set an optional wake-up count or time limit. At the limit the host wraps up once and the Goal ends. Three failed wake-ups in a row stop it.
|
|
52
52
|
- **End**: the host declares it complete after checking the actual results, or blocked when the same obstacle persists for three wake-ups. You can `/goal pause`, `/goal resume`, `/goal clear` or `/goal edit <new objective>` at any time, or use the panel buttons. **Stop all** also pauses the Goal. An end card in the conversation offers **Continue Goal** with the same objective.
|
|
53
53
|
|
|
54
54
|
A Goal grants no extra permissions: the host stays within members' project access and the chain-round limit, and does not invite members, commit, push or deploy on its own.
|
package/docs/runner.md
CHANGED
|
@@ -37,6 +37,8 @@ codiscuss setup
|
|
|
37
37
|
|
|
38
38
|
运行 `codiscuss local open`,点击“新建房间”,添加成员并指定主持人。启动失败时刷新页面,可沿用原请求继续,不会重复建房。
|
|
39
39
|
|
|
40
|
+
话题名称、项目目录和本次成员每次重新填写;智能调度、协作方式、讨论与 Goal 上限、连续派活轮数等选项自动记住上一次选择,取消建房也保留。偏好保存在运行服务的电脑上,其他已配对设备使用同一份;讨论与 Goal 的目标仍需每次填写。工作流只记住浏览选中项,不会因此启动。
|
|
41
|
+
|
|
40
42
|
### 浏览项目文件
|
|
41
43
|
|
|
42
44
|
房间设置了项目目录后,左栏“房间”下方的“文件”页可浏览电脑上的文件夹与文件,查看大小和修改时间。点击文件打开悬浮预览,支持文本、Markdown 渲染与 HTML 静态渲染,并可切换源码。未设置项目目录时入口灰显。
|
|
@@ -117,7 +119,7 @@ API 成员可以担任专家或主持人。房间指定了项目目录时,它
|
|
|
117
119
|
|
|
118
120
|
## 工作流(可选)
|
|
119
121
|
|
|
120
|
-
工作流把一种多人协作的做法存下来,供别的房间复用:一份给主持人读的做法说明(SKILL.md),加一组 Agent
|
|
122
|
+
工作流把一种多人协作的做法存下来,供别的房间复用:一份给主持人读的做法说明(SKILL.md),加一组 Agent 卡片(每张就是一个专家预设)。建房或普通邀请时在“工作流”一行选中它,只供查看和编辑,不添加成员,也不随建房启动。建房后,在输入框明确要求主持人启动(权限见下文),或打 `/` 选择工作流、确认成员后启用;主持人此后按说明组织工作。它只带来做法和成员,不保证工序,具体怎么做仍由主持人判断,你随时可以 @ 任何成员。
|
|
121
123
|
|
|
122
124
|
- **保存**:在“工作流”一行点“添加”,选“新建”,或导入别人分享的 zip 或文件夹(手机、平板上同样可用)。导入的内容先在编辑页预览,点保存才写入。选中一个工作流后,下方显示它的描述,可“查看说明”(做法说明与成员卡片)或“编辑”,删除在编辑页底部。编辑页可以“从当前房间成员导入”卡片,提示词保持原文。想让主持人起草,打开下面的“允许主持人自建工作流”。
|
|
123
125
|
- **让主持人自己建**:建房时(第 3 节“协作方式”)或在“成员与设置 → 协作设置”勾选“允许主持人自建工作流”后,直接对主持人说要什么分工(或“把这个房间的做法存成工作流”),它会自己保存、在本房间启用,并邀请工作流里的成员,接着就按流程派活,不需要你导入或添加成员。之后让它改流程,改动从成员下一次执行起生效。主持人带来的成员项目权限最高为可写;只有本地软件担任的主持人能这样做,API 主持人不行。
|
package/docs/workflow.md
CHANGED
|
@@ -58,7 +58,7 @@ Goal 让主持人持续推进一个目标:房间空闲时,本机服务会再
|
|
|
58
58
|
|
|
59
59
|
- **开启**:在输入框发送 `/goal 目标`,主持人照原文开启;只发 `/goal` 时,主持人从最近的对话提炼目标,提炼不出就不开启并说明缺什么。也可以在成员栏点“开启 Goal”,或建房时在“创建后”选“开启 Goal”,直接填写目标。输入框开头打 `/` 时,菜单最上面一组“模式”列出 Goal 与讨论;进行中时改为列出暂停、继续、结束等命令。需要一位由本机唤醒的 AI 主持人;Goal 与讨论不能同时进行。
|
|
60
60
|
- **推进**:主持人派出的活全部结束、房间空闲 30 秒后(你刚发言、还有成员在工作或有排队消息时不算空闲),本机服务唤醒主持人一次,消息流里显示“Goal 续跑 #N”。主持人这次没派活(只回复你或向你提问)时,不再自动唤醒,直到你回复。
|
|
61
|
-
-
|
|
61
|
+
- **预算**:默认不限。开启或编辑 Goal 时只需填写目标;需要控制预算时,展开“高级设置”设续跑次数或时长上限。到限后主持人收尾一次,Goal 结束。连续 3 次续跑执行失败时自动停止。
|
|
62
62
|
- **结束**:主持人对照实际结果核验后宣告完成,或同一阻碍连续 3 次无法解决时宣告卡住;你可以随时 `/goal pause`、`/goal resume`、`/goal clear`、`/goal edit 新目标`,或用成员栏的按钮。“全部停止”也会暂停 Goal。结束后消息流留一张结束卡,可“继续 Goal”沿用原目标重新开启。
|
|
63
63
|
|
|
64
64
|
Goal 不扩大权限:主持人仍受成员项目权限和连续派活轮数限制,不会自行邀请成员、提交、推送或部署。
|
package/package.json
CHANGED
|
@@ -23,7 +23,7 @@ import { stt_part } from './stt.mjs';
|
|
|
23
23
|
import { access_levels, BUILTIN_EXPERT_PRESETS, MAX_EXPERT_PROMPT, PROJECT_ACCESS } from './expert_prompts.mjs';
|
|
24
24
|
import { acquire_lock, api, DiscussionStore, service_drafts, validate_config } from './discussion_runner.mjs';
|
|
25
25
|
import { FreeRoomRunner, free_task_files } from './free_runner.mjs';
|
|
26
|
-
import { bind_room_workflow, delete_workflow, get_workflow, list_workflows, load_room_workflow, parse_workflow_draft, preview_workflow, save_workflow } from './workflows.mjs';
|
|
26
|
+
import { bind_room_workflow, delete_workflow, get_workflow, list_workflows, load_room_workflow, parse_workflow_draft, preview_workflow, save_workflow, WORKFLOW_ID_PATTERN } from './workflows.mjs';
|
|
27
27
|
import { assert_shared_home, CONNECTION_FIELDS, configuration_snapshot, read_registry, registered_identity, register_room, service_home, service_pid, start_service } from './discussion_service.mjs';
|
|
28
28
|
|
|
29
29
|
// A platform is a type; `Claude-review` is another Claude instance with its own session, model and effort. An ID is
|
|
@@ -336,18 +336,66 @@ export async function room_files(home, raw) {
|
|
|
336
336
|
return { room_id: input.room_id, ...read_project_files(config.cwd, input.path, input.offset, input.query) };
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
-
|
|
339
|
+
const CREATE_PREFERENCE_RANGES = { max_rounds: [0, 100], max_minutes: [0, 1440], goal_max_continues: [0, 100], goal_max_minutes: [0, 1440], chain_rounds: [1, 20] };
|
|
340
|
+
const CREATE_PREFERENCE_FLAGS = ['named_followup', 'host_workflows', 'advanced_open'];
|
|
341
|
+
const MAX_PREFERENCE_MODEL_LENGTH = 200;
|
|
342
|
+
const SETTINGS_LOCK_WAIT_MS = 5000;
|
|
343
|
+
|
|
344
|
+
/** Validate only reusable form choices; room text, members, paths and credentials are never preferences. */
|
|
345
|
+
function validate_create_preferences(input) {
|
|
346
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('建房偏好须为对象');
|
|
347
|
+
for (const [field, value] of Object.entries(input)) {
|
|
348
|
+
let valid = false;
|
|
349
|
+
if (CREATE_PREFERENCE_FLAGS.includes(field)) valid = typeof value === 'boolean';
|
|
350
|
+
else if (Object.hasOwn(CREATE_PREFERENCE_RANGES, field)) {
|
|
351
|
+
const [minimum, maximum] = CREATE_PREFERENCE_RANGES[field];
|
|
352
|
+
valid = Number.isInteger(value) && value >= minimum && value <= maximum;
|
|
353
|
+
} else if (field === 'drive') valid = ['free', 'discussion', 'goal'].includes(value);
|
|
354
|
+
else if (field === 'workflow_id') valid = typeof value === 'string' && (!value || WORKFLOW_ID_PATTERN.test(value));
|
|
355
|
+
else if (field === 'scheduler') valid = valid_scheduler_preference(value);
|
|
356
|
+
if (!valid) throw new Error(`建房偏好字段无效:${field}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** A scheduler preference carries selections, never a key or even a key reference. */
|
|
361
|
+
function valid_scheduler_preference(value) {
|
|
362
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
363
|
+
const fields = {
|
|
364
|
+
enabled: item => typeof item === 'boolean',
|
|
365
|
+
provider: item => typeof item === 'string' && (!item || Object.hasOwn(SCHEDULER_PROVIDERS, item)),
|
|
366
|
+
model: item => typeof item === 'string' && item.length <= MAX_PREFERENCE_MODEL_LENGTH,
|
|
367
|
+
mode: item => typeof item === 'string' && (!item || Object.hasOwn(SCHEDULER_MODES, item)),
|
|
368
|
+
};
|
|
369
|
+
return Object.entries(value).every(([field, item]) => Object.hasOwn(fields, field) && fields[field](item));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Update software defaults or form preferences independently, preserving the other section. */
|
|
340
373
|
export function save_launch_settings(home, input) {
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
374
|
+
const patch = {};
|
|
375
|
+
if (Object.hasOwn(input, 'participants')) {
|
|
376
|
+
const participants = {};
|
|
377
|
+
for (const [name, profile] of Object.entries(input.participants || {})) {
|
|
378
|
+
if (platform_of(name) !== name) throw new Error(`本机默认配置按平台保存;${name} 的模型与强度只用于本场讨论`);
|
|
379
|
+
validate_profile(name, profile);
|
|
380
|
+
participants[name] = profile;
|
|
381
|
+
}
|
|
382
|
+
patch.participants = participants;
|
|
383
|
+
}
|
|
384
|
+
if (Object.hasOwn(input, 'create_preferences')) {
|
|
385
|
+
validate_create_preferences(input.create_preferences);
|
|
386
|
+
patch.create_preferences = input.create_preferences;
|
|
346
387
|
}
|
|
347
|
-
const settings = { participants };
|
|
348
388
|
mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
349
|
-
|
|
350
|
-
|
|
389
|
+
const lock = new DatabaseSync(join(home, 'launch-settings-lock.sqlite3'));
|
|
390
|
+
try {
|
|
391
|
+
lock.exec(`PRAGMA busy_timeout=${SETTINGS_LOCK_WAIT_MS}; BEGIN IMMEDIATE`);
|
|
392
|
+
const previous = launch_settings(home);
|
|
393
|
+
const settings = { ...previous, ...patch };
|
|
394
|
+
if (patch.create_preferences) settings.create_preferences = { ...previous.create_preferences, ...patch.create_preferences };
|
|
395
|
+
atomic_json(join(home, 'launch-settings.json'), settings);
|
|
396
|
+
lock.exec('COMMIT');
|
|
397
|
+
return settings;
|
|
398
|
+
} finally { lock.close(); }
|
|
351
399
|
}
|
|
352
400
|
|
|
353
401
|
const PROJECT_NAME_LIMIT = 80;
|
package/runner/tts_providers.mjs
CHANGED
|
@@ -17,6 +17,8 @@ const VOLC_MESSAGES = {
|
|
|
17
17
|
};
|
|
18
18
|
const ALIYUN_SOCKET = 'wss://dashscope.aliyuncs.com/api-ws/v1/inference';
|
|
19
19
|
const ALIYUN_MODEL = 'cosyvoice-v3-flash';
|
|
20
|
+
// Its default answered 160 kbps (measured 2026-11-04); 64 kbps (8 KB/s, as Volcengine) keeps a 300-character answer under the 1 MiB relay limit.
|
|
21
|
+
const ALIYUN_BIT_RATE = 64;
|
|
20
22
|
// Aliyun and Azure answer 24 kHz mono MP3, so trailing silence is appended as silent frames of that format.
|
|
21
23
|
const SAMPLE_RATE = 24000;
|
|
22
24
|
// One silent MPEG-2 Layer III frame (24 kHz, 32 kbps, mono): a header and zeroed side info, 576 samples = 24 ms.
|
|
@@ -64,13 +66,13 @@ const voices = (list) => list.map(([id, label]) => ({ id, label }));
|
|
|
64
66
|
*/
|
|
65
67
|
export const TTS_PROVIDERS = [
|
|
66
68
|
{ id: 'volcengine', label: '火山引擎', key_base_url: 'https://openspeech.bytedance.com', console_url: 'https://console.volcengine.com/speech/new',
|
|
67
|
-
note: '
|
|
69
|
+
note: '约 3 元/万字,语音输入共用此 Key', voices: voices(VOLC_VOICES),
|
|
68
70
|
default_speaker: 'zh_female_vv_uranus_bigtts', context: true, region: false, voice_input: true },
|
|
69
71
|
{ id: 'aliyun', label: '阿里云百炼', key_base_url: 'https://dashscope.aliyuncs.com', console_url: 'https://bailian.console.aliyun.com/?tab=model#/api-key',
|
|
70
|
-
note: '
|
|
72
|
+
note: '约 2 元/万字(汉字计 2 字符)', voices: voices(ALIYUN_VOICES),
|
|
71
73
|
default_speaker: 'longxiaochun_v3', context: false, region: false, voice_input: false },
|
|
72
74
|
{ id: 'azure', label: 'Azure', key_base_url: 'https://tts.speech.microsoft.com', console_url: 'https://portal.azure.com/#create/Microsoft.CognitiveServicesSpeechServices',
|
|
73
|
-
note: '
|
|
75
|
+
note: '每月免费约 25 万字,超出约 2 元/万字', voices: voices(AZURE_VOICES),
|
|
74
76
|
default_speaker: 'zh-CN-XiaoxiaoNeural', context: false, region: true, voice_input: false },
|
|
75
77
|
];
|
|
76
78
|
|
|
@@ -147,14 +149,16 @@ function aliyun_speak({ key, text, speaker, rate, silence }, open) {
|
|
|
147
149
|
const finish = (error, value) => {
|
|
148
150
|
if (settled) return;
|
|
149
151
|
settled = true; clearTimeout(timer);
|
|
150
|
-
|
|
152
|
+
// A close without a status code is never answered by the service, which then holds the connection open.
|
|
153
|
+
try { socket.close(1000); } catch { /* Already closed. */ }
|
|
151
154
|
if (error) reject(error); else resolve(value);
|
|
152
155
|
};
|
|
153
156
|
const timer = setTimeout(() => finish(new Error('语音合成请求超时,请检查网络后重试')), TTS_TIMEOUT_MS);
|
|
154
157
|
const send = (action, payload) => socket.send(JSON.stringify({ header: { action, task_id, streaming: 'duplex' }, payload }));
|
|
155
158
|
socket.binaryType = 'arraybuffer';
|
|
156
159
|
socket.onopen = () => send('run-task', { task_group: 'audio', task: 'tts', function: 'SpeechSynthesizer', model: ALIYUN_MODEL, input: {},
|
|
157
|
-
parameters: { text_type: 'PlainText', voice: speaker, format: 'mp3', sample_rate: SAMPLE_RATE, rate: 1 + rate / 100,
|
|
160
|
+
parameters: { text_type: 'PlainText', voice: speaker, format: 'mp3', sample_rate: SAMPLE_RATE, bit_rate: ALIYUN_BIT_RATE, rate: 1 + rate / 100,
|
|
161
|
+
word_timestamp_enabled: true } });
|
|
158
162
|
socket.onmessage = ({ data }) => {
|
|
159
163
|
if (typeof data !== 'string') { chunks.push(Buffer.from(data)); return; }
|
|
160
164
|
const { header = {}, payload = {} } = JSON.parse(data);
|
package/web/app.js
CHANGED
|
@@ -126,10 +126,9 @@ state.navigation_version = 0;
|
|
|
126
126
|
state.admin = { loaded: false, local: false, registered: false, setup_available: false, logged_in: false, profile: null, identity_version: 0 };
|
|
127
127
|
// needs: "" | "pairing" | "renew"; pairing_code arrives from a scanned link (#pair=CODE); prompt waits for the user's tap.
|
|
128
128
|
state.device = { id: "", needs: "", pairing_code: "", prompt: null };
|
|
129
|
-
state.launch = { available: false, remote: false, relay: null, connect_command: "", profiles: {}, projects: [], presets: [], builtin_presets: [], hidden_builtins: [], workflows: [], description_templates: {}, workspace: {}, scheduler: {}, error: "", recoveries: [] };
|
|
130
|
-
// The workflow chosen in the new-room form: { id, label, members: { card: instance } }; cards removed from the form drop out at submission.
|
|
131
|
-
state.create_workflow = null;
|
|
129
|
+
state.launch = { available: false, remote: false, relay: null, connect_command: "", profiles: {}, projects: [], presets: [], builtin_presets: [], hidden_builtins: [], workflows: [], description_templates: {}, workspace: {}, scheduler: {}, create_preferences: {}, error: "", recoveries: [] };
|
|
132
130
|
state.create_host_workflows = false;
|
|
131
|
+
let create_preferences_pending = {}, create_preferences_saving = null, create_preferences_version = 0;
|
|
133
132
|
const PLATFORM_CLIENTS = { Codex: [{ value: "codex_cli", label: "Codex CLI" }, { value: "codex", label: "Codex App" }],
|
|
134
133
|
Claude: [{ value: "claude", label: "Claude Code CLI" }],
|
|
135
134
|
Antigravity: [{ value: "antigravity_cli", label: "Antigravity CLI" }, { value: "antigravity", label: "Antigravity App" }] };
|
|
@@ -1137,23 +1136,83 @@ function mentioned(text, name) {
|
|
|
1137
1136
|
return new RegExp(`(^|[\\s((,,。;;::])@${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w-])`, "u").test(prose);
|
|
1138
1137
|
}
|
|
1139
1138
|
|
|
1140
|
-
/**
|
|
1139
|
+
/** Clear this room's text, project and roster, then restore the user's last reusable choices. */
|
|
1141
1140
|
function reset_create_form() {
|
|
1142
1141
|
element("room-form").reset();
|
|
1143
1142
|
render_expert_inputs("new-expert-modes", default_experts());
|
|
1144
1143
|
render_host_options("new-room-host", [], "");
|
|
1145
1144
|
state.launch.workspace = {};
|
|
1146
|
-
state.launch.scheduler = default_scheduler();
|
|
1147
|
-
state.named_followup = false;
|
|
1148
|
-
state.create_discussion = false;
|
|
1149
1145
|
state.create_goal = null;
|
|
1150
|
-
|
|
1151
|
-
state.create_workflow = null;
|
|
1152
|
-
state.create_host_workflows = false;
|
|
1146
|
+
apply_create_preferences();
|
|
1153
1147
|
state.launch.profiles = JSON.parse(JSON.stringify(state.launch.saved_profiles || {}));
|
|
1154
1148
|
Object.values(state.launch.profiles).forEach(fill_api_key);
|
|
1155
1149
|
render_launch_settings();
|
|
1156
1150
|
publish_view();
|
|
1151
|
+
return refresh_create_preferences();
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/** Restore choices without touching the new room's title, project, members, host or goal text. */
|
|
1155
|
+
function apply_create_preferences() {
|
|
1156
|
+
const preferences = state.launch.create_preferences || {};
|
|
1157
|
+
const scheduler = preferences.scheduler;
|
|
1158
|
+
const base_url = state.launch.scheduler_providers?.[scheduler?.provider]?.base_url;
|
|
1159
|
+
state.launch.scheduler = scheduler ? { ...scheduler, api_key_ref: state.launch.api_keys?.[base_url] || "" } : default_scheduler();
|
|
1160
|
+
state.named_followup = preferences.named_followup ?? false;
|
|
1161
|
+
state.create_discussion = preferences.drive === "discussion";
|
|
1162
|
+
state.create_goal = preferences.drive === "goal" ? { objective: state.create_goal?.objective || "",
|
|
1163
|
+
max_continues: preferences.goal_max_continues ?? 0, max_minutes: preferences.goal_max_minutes ?? 0 } : null;
|
|
1164
|
+
state.create_chain_rounds = preferences.chain_rounds ?? DEFAULT_CHAIN_ROUNDS;
|
|
1165
|
+
state.create_host_workflows = preferences.host_workflows ?? false;
|
|
1166
|
+
for (const field of ["max_rounds", "max_minutes"]) {
|
|
1167
|
+
const control = element(`new-room-limit-${field}`);
|
|
1168
|
+
if (control && preferences[field] !== undefined) control.value = String(preferences[field]);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/** Remember only changed preference fields; cancellation of the form does not undo these choices. */
|
|
1173
|
+
function write_create_preferences(patch) {
|
|
1174
|
+
const current = state.launch.create_preferences || {};
|
|
1175
|
+
const changed = Object.fromEntries(Object.entries(patch).filter(([field, value]) => JSON.stringify(current[field]) !== JSON.stringify(value)));
|
|
1176
|
+
if (!Object.keys(changed).length) return;
|
|
1177
|
+
state.launch.create_preferences = { ...current, ...changed };
|
|
1178
|
+
create_preferences_pending = { ...create_preferences_pending, ...changed };
|
|
1179
|
+
create_preferences_version++;
|
|
1180
|
+
flush_create_preferences();
|
|
1181
|
+
publish_view();
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/** Serialize preference patches so a slower earlier save cannot overwrite the latest operation. */
|
|
1185
|
+
function flush_create_preferences() {
|
|
1186
|
+
if (create_preferences_saving) return create_preferences_saving;
|
|
1187
|
+
if (!state.launch.available || !Object.keys(create_preferences_pending).length) return Promise.resolve();
|
|
1188
|
+
const patch = create_preferences_pending;
|
|
1189
|
+
create_preferences_pending = {};
|
|
1190
|
+
create_preferences_saving = local_launch_request("settings", { create_preferences: patch }).catch(error => {
|
|
1191
|
+
create_preferences_pending = { ...patch, ...create_preferences_pending };
|
|
1192
|
+
notify("error", `建房偏好未保存:${error.message}`);
|
|
1193
|
+
return false;
|
|
1194
|
+
}).then(result => {
|
|
1195
|
+
create_preferences_saving = null;
|
|
1196
|
+
if (result !== false) return flush_create_preferences();
|
|
1197
|
+
});
|
|
1198
|
+
return create_preferences_saving;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/** Reopen with the paired computer's latest choices, keeping edits made while the read was in flight. */
|
|
1202
|
+
async function refresh_create_preferences() {
|
|
1203
|
+
if (!state.launch.available) return;
|
|
1204
|
+
const version = create_preferences_version;
|
|
1205
|
+
await flush_create_preferences();
|
|
1206
|
+
try {
|
|
1207
|
+
const settings = await local_launch_request("settings");
|
|
1208
|
+
if (version !== create_preferences_version) return;
|
|
1209
|
+
for (const field of ["api_keys", "scheduler_providers", "scheduler_default_mode", "workflows"]) {
|
|
1210
|
+
if (settings[field] !== undefined) state.launch[field] = settings[field];
|
|
1211
|
+
}
|
|
1212
|
+
state.launch.create_preferences = { ...settings.create_preferences, ...create_preferences_pending };
|
|
1213
|
+
apply_create_preferences();
|
|
1214
|
+
publish_view();
|
|
1215
|
+
} catch (error) { notify("error", `建房偏好未读取:${error.message}`); }
|
|
1157
1216
|
}
|
|
1158
1217
|
|
|
1159
1218
|
/** Persist a member mutation before transmission and reuse its credential after an uncertain response. */
|
|
@@ -2666,6 +2725,7 @@ async function load_launch_settings() {
|
|
|
2666
2725
|
const settings = await local_launch_request("settings");
|
|
2667
2726
|
// Keep the saved machine defaults so every new form can start from them again.
|
|
2668
2727
|
state.launch.saved_profiles = JSON.parse(JSON.stringify(settings.participants || {}));
|
|
2728
|
+
state.launch.create_preferences = { ...settings.create_preferences, ...create_preferences_pending };
|
|
2669
2729
|
Object.assign(state.launch, { available: true, profiles: settings.participants, projects: settings.projects || [], api_providers: settings.api_providers || {}, api_keys: settings.api_keys || {},
|
|
2670
2730
|
presets: settings.presets || [], builtin_presets: settings.builtin_presets || [], hidden_builtins: settings.hidden_builtins || [], workflows: settings.workflows || [], description_templates: settings.description_templates || {},
|
|
2671
2731
|
scheduler_providers: settings.scheduler_providers || {}, scheduler_modes: settings.scheduler_modes || {}, scheduler_default_mode: settings.scheduler_default_mode || "", error: "",
|
|
@@ -2673,8 +2733,8 @@ async function load_launch_settings() {
|
|
|
2673
2733
|
Object.values(state.launch.profiles).forEach(fill_api_key);
|
|
2674
2734
|
if (local_connection && !relay_machine) pair_local_machine();
|
|
2675
2735
|
move_browser_description_templates();
|
|
2676
|
-
|
|
2677
|
-
|
|
2736
|
+
apply_create_preferences();
|
|
2737
|
+
flush_create_preferences();
|
|
2678
2738
|
load_room_models();
|
|
2679
2739
|
} catch (error) {
|
|
2680
2740
|
// An unpaired or expired device keeps its way to the computer: pairing and renewal go through it.
|
|
@@ -2978,20 +3038,13 @@ function launch_options() {
|
|
|
2978
3038
|
function creation_launch_options() {
|
|
2979
3039
|
const { enabled, provider, model, api_key_ref, mode } = state.launch.scheduler;
|
|
2980
3040
|
return { ...launch_options(), ...(enabled && provider && model && api_key_ref ? { scheduler: { provider, model, api_key_ref, ...(mode ? { mode } : {}) } } : {}),
|
|
2981
|
-
...
|
|
2982
|
-
}
|
|
2983
|
-
|
|
2984
|
-
/** The new room's workflow, naming only the cards still in the form as woken members. */
|
|
2985
|
-
function creation_workflow() {
|
|
2986
|
-
if (!state.create_workflow) return {};
|
|
2987
|
-
const chosen = read_expert_inputs("new-expert-modes");
|
|
2988
|
-
const members = Object.fromEntries(Object.entries(state.create_workflow.members).filter(([, name]) => chosen[name] === "cli"));
|
|
2989
|
-
return Object.keys(members).length ? { workflow: { id: state.create_workflow.id, members } } : {};
|
|
3041
|
+
...(state.create_host_workflows ? { host_workflows: true } : {}) };
|
|
2990
3042
|
}
|
|
2991
3043
|
|
|
2992
3044
|
/** Whether the new room's host may save and enable workflows by itself, inviting their cards. */
|
|
2993
3045
|
function write_create_host_workflows(value) {
|
|
2994
3046
|
state.create_host_workflows = Boolean(value);
|
|
3047
|
+
write_create_preferences({ host_workflows: state.create_host_workflows });
|
|
2995
3048
|
publish_view();
|
|
2996
3049
|
}
|
|
2997
3050
|
|
|
@@ -3018,25 +3071,6 @@ function workflow_preset({ card, requested_access, needs_platform, ...preset })
|
|
|
3018
3071
|
return preset;
|
|
3019
3072
|
}
|
|
3020
3073
|
|
|
3021
|
-
/**
|
|
3022
|
-
* Fill the new-room form with a workflow's cards, each a preset-made instance, and remember which card each plays.
|
|
3023
|
-
* Choosing another (or the same one again after editing) replaces the previous cards; clearing the choice keeps them as ordinary members.
|
|
3024
|
-
*/
|
|
3025
|
-
async function choose_create_workflow(id) {
|
|
3026
|
-
const workflow = id ? await local_launch_request("workflows", { action: "get", id }) : null;
|
|
3027
|
-
if (!workflow) { state.create_workflow = null; return publish_view(); }
|
|
3028
|
-
for (const name of Object.values(state.create_workflow?.members || {})) write_view_expert("new-expert-modes", name, "");
|
|
3029
|
-
const members = {};
|
|
3030
|
-
for (const agent of workflow.agents) members[agent.card] = add_preset_instance("new-expert-modes", workflow_preset(agent), "cli");
|
|
3031
|
-
state.create_workflow = { id, label: workflow.label || id, members };
|
|
3032
|
-
publish_view();
|
|
3033
|
-
}
|
|
3034
|
-
|
|
3035
|
-
/** The workflow a card in the new-room form or the invite dialog came from, for its read-only prompt. */
|
|
3036
|
-
function create_workflow_label(name) {
|
|
3037
|
-
return state.create_workflow && Object.values(state.create_workflow.members).includes(name) ? state.create_workflow.label : "";
|
|
3038
|
-
}
|
|
3039
|
-
|
|
3040
3074
|
/** Bind a workflow to this room after its cards were invited (or brought back); the host reads it from the next task. */
|
|
3041
3075
|
async function enable_room_workflow({ id, members }) {
|
|
3042
3076
|
const room_id = state.room.id;
|
|
@@ -3062,6 +3096,7 @@ async function move_workflow_members(action) {
|
|
|
3062
3096
|
/** Choose whether the new discussion's host writes named follow-ups for each expert between rounds. */
|
|
3063
3097
|
function write_named_followup(value) {
|
|
3064
3098
|
state.named_followup = Boolean(value);
|
|
3099
|
+
write_create_preferences({ named_followup: state.named_followup });
|
|
3065
3100
|
publish_view();
|
|
3066
3101
|
}
|
|
3067
3102
|
|
|
@@ -3072,6 +3107,7 @@ state.create_chain_rounds = DEFAULT_CHAIN_ROUNDS;
|
|
|
3072
3107
|
/** Change the new room's round limit; it is saved right after the room is created. */
|
|
3073
3108
|
function write_create_chain_rounds(value) {
|
|
3074
3109
|
state.create_chain_rounds = value;
|
|
3110
|
+
write_create_preferences({ chain_rounds: value });
|
|
3075
3111
|
publish_view();
|
|
3076
3112
|
}
|
|
3077
3113
|
|
|
@@ -3079,13 +3115,18 @@ function write_create_chain_rounds(value) {
|
|
|
3079
3115
|
function write_create_discussion(value) {
|
|
3080
3116
|
state.create_discussion = Boolean(value);
|
|
3081
3117
|
if (state.create_discussion) state.create_goal = null;
|
|
3118
|
+
write_create_preferences({ drive: state.create_discussion ? "discussion" : state.create_goal ? "goal" : "free" });
|
|
3082
3119
|
publish_view();
|
|
3083
3120
|
}
|
|
3084
3121
|
|
|
3085
3122
|
/** Turn the new room's goal on (with a patch of its fields) or off; a goal and a discussion exclude each other. */
|
|
3086
3123
|
function write_create_goal(patch) {
|
|
3087
|
-
|
|
3124
|
+
const preferences = state.launch.create_preferences || {};
|
|
3125
|
+
state.create_goal = patch === null ? null : { objective: "", max_continues: preferences.goal_max_continues ?? 0,
|
|
3126
|
+
max_minutes: preferences.goal_max_minutes ?? 0, ...(state.create_goal || {}), ...patch };
|
|
3088
3127
|
if (state.create_goal) state.create_discussion = false;
|
|
3128
|
+
write_create_preferences({ drive: state.create_goal ? "goal" : state.create_discussion ? "discussion" : "free",
|
|
3129
|
+
...(state.create_goal ? { goal_max_continues: state.create_goal.max_continues, goal_max_minutes: state.create_goal.max_minutes } : {}) });
|
|
3089
3130
|
publish_view();
|
|
3090
3131
|
}
|
|
3091
3132
|
|
|
@@ -3108,6 +3149,8 @@ function write_scheduler(field, value) {
|
|
|
3108
3149
|
scheduler.model = preset?.models[0] || "";
|
|
3109
3150
|
scheduler.api_key_ref = preset ? state.launch.api_keys?.[preset.base_url] || "" : "";
|
|
3110
3151
|
}
|
|
3152
|
+
const { enabled = false, provider = "", model = "", mode = "" } = scheduler;
|
|
3153
|
+
write_create_preferences({ scheduler: { enabled, provider, model, mode } });
|
|
3111
3154
|
publish_view();
|
|
3112
3155
|
}
|
|
3113
3156
|
|
|
@@ -4333,7 +4376,7 @@ function view_snapshot() {
|
|
|
4333
4376
|
device: { id: state.device.id, needs: state.device.needs, pairing_code: state.device.pairing_code,
|
|
4334
4377
|
prompt: state.device.prompt ? { title: state.device.prompt.title, description: state.device.prompt.description, confirm_label: state.device.prompt.confirm_label } : null },
|
|
4335
4378
|
tab: state.tab, snapshot_id: state.snapshot_id, context_dirty: state.context_dirty,
|
|
4336
|
-
theme: document.documentElement.dataset.theme, fields, experts, launch: structuredClone(state.launch), named_followup: state.named_followup, create_discussion: state.create_discussion, create_goal: state.create_goal, create_chain_rounds: state.create_chain_rounds,
|
|
4379
|
+
theme: document.documentElement.dataset.theme, fields, experts, launch: structuredClone(state.launch), named_followup: state.named_followup, create_discussion: state.create_discussion, create_goal: state.create_goal, create_chain_rounds: state.create_chain_rounds, create_host_workflows: state.create_host_workflows };
|
|
4337
4380
|
}
|
|
4338
4381
|
|
|
4339
4382
|
/** Update a shared draft using the same input events as the original controls. */
|
|
@@ -4348,6 +4391,9 @@ function write_view_field(id, value) {
|
|
|
4348
4391
|
control.value = value ?? "";
|
|
4349
4392
|
control.dispatchEvent(new Event("input", { bubbles: true }));
|
|
4350
4393
|
control.dispatchEvent(new Event("change", { bubbles: true }));
|
|
4394
|
+
if (["new-room-limit-max_rounds", "new-room-limit-max_minutes"].includes(id)) {
|
|
4395
|
+
write_create_preferences({ [id.slice("new-room-limit-".length)]: Number(control.value) });
|
|
4396
|
+
}
|
|
4351
4397
|
publish_view();
|
|
4352
4398
|
}
|
|
4353
4399
|
|
|
@@ -4546,7 +4592,7 @@ async function execute_view_action(name, value) {
|
|
|
4546
4592
|
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,
|
|
4547
4593
|
save_launch_defaults, resume_launch, discard_launch, retry_launch_connection, check_launch_connection: load_launch_settings,
|
|
4548
4594
|
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,
|
|
4549
|
-
|
|
4595
|
+
enable_room_workflow, move_workflow_members, set_room_host_workflows,
|
|
4550
4596
|
adopt_recommendation, rate_decision,
|
|
4551
4597
|
queue_send: id => queue_request({ action: "send", id }), queue_delete: id => queue_request({ action: "delete", id }),
|
|
4552
4598
|
queue_reorder: ids => queue_request({ action: "reorder", ids }), queue_resume: () => queue_request({ action: "resume" }),
|
|
@@ -4568,13 +4614,13 @@ function change_view_theme(theme) {
|
|
|
4568
4614
|
try { localStorage.setItem(THEME_STORAGE_KEY, document.documentElement.dataset.theme); } catch { /* Keep the session preference. */ }
|
|
4569
4615
|
}
|
|
4570
4616
|
|
|
4571
|
-
window.discuss_view = { snapshot: view_snapshot, write_field: write_view_field, notify, load_decision_input,
|
|
4617
|
+
window.discuss_view = { snapshot: view_snapshot, write_field: write_view_field, write_create_preferences, notify, load_decision_input,
|
|
4572
4618
|
write_launch: write_launch_field, default_launch_entry, description_template, save_description_template, description_draft, write_description_draft, describe_member, set_chain_rounds, continue_chain, expert_presets, apply_expert_preset, unique_instance, member_label, default_name, label_draft, write_label_draft, preset_identity, save_expert_preset, delete_expert_preset, write_room_connection, write_room_adapter, launch_clients: LAUNCH_CLIENTS,
|
|
4573
4619
|
launch_model_state, load_launch_models, write_launch_model, save_launch_api_key, write_scheduler, save_scheduler_api_key, check_scheduler, write_named_followup, write_create_discussion, write_create_goal, write_create_chain_rounds, check_launch_api, launch_summary, launch_can_set_access, pick_launch_directory, browse_local_directory, list_remote_directories, save_launch_projects, restore_launch_profile, reset_create_form, empty_discussion_message,
|
|
4574
4620
|
start_drag: start_directory_drag, reconnect: reconnect_live_updates, stop_label, local_request: local_launch_request,
|
|
4575
4621
|
attachment_markup, attachment_size, attachment_file_icon, drop_attachments, paste_attachments,
|
|
4576
4622
|
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,
|
|
4577
|
-
reload_workflows, workflow_preset,
|
|
4623
|
+
reload_workflows, workflow_preset, write_create_host_workflows,
|
|
4578
4624
|
admin: { register: register_admin, login: login_admin, logout: logout_admin,
|
|
4579
4625
|
update_profile: update_admin_profile, avatar_from_file: admin_avatar_from_file, validate: validate_admin_form,
|
|
4580
4626
|
sessions: admin_sessions, revoke: revoke_admin_sessions, change_password: change_admin_password, login_with_passkey,
|