ai_co_discuss 0.3.42 → 0.3.44
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/workflow-draft.md +1 -1
- package/docs/runner.md +3 -2
- package/docs/workflow.md +1 -1
- package/package.json +1 -1
- package/runner/discussion_launch.mjs +10 -5
- package/runner/discussion_service.mjs +45 -7
- package/runner/tts.mjs +19 -8
- package/web/app.js +17 -9
- package/web/modern.css +1 -1
- package/web/modern.js +2437 -2185
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
本机服务会校验并保存到工作流库,在本房间启用,邀请缺少的卡片成员(离开的请回),返回 `members`(卡片 ID → 成员名)。成功后本次执行里就可以按成员名点名派活,不用等用户。之后要改流程,用同一 ID 改草稿再运行一次,成员下一次执行就读到新内容。只能更新本房间绑定的工作流,不能覆盖库里别的工作流;房间已启用另一个工作流时由用户先停用。卡片的项目权限最高到 write,写 auto / full 会被降为 write。命令失败时按报错修正,不要绕过它直接写本机服务目录。
|
|
12
|
-
- 没有该字段:你只起草,在回答里发出草稿,由用户在网页“工作流库 →
|
|
12
|
+
- 没有该字段:你只起草,在回答里发出草稿,由用户在网页“工作流库 → 粘贴导入”保存;不要自己写入本机服务目录。需要的话告诉用户可以在“成员与设置 → 协作设置”里打开“允许主持人自建工作流”。
|
|
13
13
|
|
|
14
14
|
## 内容
|
|
15
15
|
|
package/docs/runner.md
CHANGED
|
@@ -99,15 +99,16 @@ API 成员可以担任专家或主持人。房间指定了项目目录时,它
|
|
|
99
99
|
工作流把一种多人协作的做法存下来,供别的房间复用:一份给主持人读的做法说明(SKILL.md),加一组 Agent 卡片(每张就是一个专家预设)。建房或邀请时在“工作流”一行选中它,确认卡片后作为成员加入;主持人此后按说明组织工作。它只带来做法和成员,不保证工序,具体怎么做仍由主持人判断,你随时可以 @ 任何成员。
|
|
100
100
|
|
|
101
101
|
- **保存**:点“工作流库”新建或编辑;也可以请房间主持人“把这个房间的做法存成工作流”,复制它起草的消息,在工作流库“粘贴导入”。编辑页可以“从当前房间成员导入”卡片,提示词保持原文。
|
|
102
|
-
-
|
|
102
|
+
- **让主持人自己建**:建房时(第 3 节“协作方式”)或在“成员与设置 → 协作设置”勾选“允许主持人自建工作流”后,直接对主持人说要什么分工(或“把这个房间的做法存成工作流”),它会自己保存、在本房间启用,并邀请工作流里的成员,接着就按流程派活,不需要你导入或添加成员。之后让它改流程,改动从成员下一次执行起生效。主持人带来的成员项目权限最高为可写;只有本地软件担任的主持人能这样做,API 主持人不行。
|
|
103
103
|
- **分享**:工作流存在本机服务目录的 `workflows` 文件夹,一个工作流一个文件夹;编辑页可复制路径,复制整个文件夹即可分享。导入时会列出每张卡片的提示词和项目权限;要求写文件的卡片默认按只读导入,逐张勾选后才放开。
|
|
104
104
|
- **修改与停用**:修改工作流会作用于所有启用它的房间,成员下一次执行就按新内容;工作流卡片的提示词弹窗提供“去工作流库编辑”。右栏显示工作流状态并可停用:它带来的成员离开(主持人保留),正在进行的工作随之停止;重新启用先打开邀请面板,确认后请回原成员并加入你保留的新增卡片。
|
|
105
105
|
|
|
106
106
|
## 朗读与语音输入(可选)
|
|
107
107
|
|
|
108
|
-
在火山引擎控制台开通「语音合成2.0」(朗读)和「录音文件识别2.0」(语音输入),复制 API Key,填进网页“设置”的“语音”。朗读可设一个通用音色,并按 AI 平台(Claude、Codex、Gemini
|
|
108
|
+
在火山引擎控制台开通「语音合成2.0」(朗读)和「录音文件识别2.0」(语音输入),复制 API Key,填进网页“设置”的“语音”。朗读可设一个通用音色,并按 AI 平台(Claude、Codex、Gemini 等)分别配音色,朗读项目文件也可单独配一个音色,没配的平台、你自己的消息、程序消息和文件用通用音色;还可选语速、改语气提示(默认是“像给朋友讲故事一样”的自然语气)。密钥只保存在本机,费用按你的账号计费。
|
|
109
109
|
|
|
110
110
|
- 朗读:每条消息的工具栏有“朗读”(手机上在“…”菜单里),在消息里选中文字后点“听当前”从选中处开始;读完这条会接着读后面的消息直到最新,点输入框右下角的播报按钮开启语音播报:之后的新回复会自动朗读回答摘要(没写摘要的短回答读全文,长回答读第一段),主持人的发言会报“主持人”;讨论中还会提示新一轮的发言顺序和轮到谁,执行失败、操作待你允许和讨论结束也会播一句。同时到达的回复依次播报,积压较多时只播最新 3 条。锁屏或切到其他 App 时继续播放,锁屏界面和耳机按键可以暂停、继续、跳到下一条;读过的段落会缓存在电脑上,重听不再计费。正在读的句子会高亮,点一下高亮的句子暂停,再点继续;代码块跳过。朗读时消息区底部有“跟随”和“退出”:页面默认不随朗读滚动,点“跟随”后才把正在读的句子保持在视野内,手动滚动一下就取消跟随。
|
|
111
|
+
- 朗读文件:在“文件”里打开 Markdown 文件,预览模式下标题栏有“朗读”,从正文开头读到文件末尾(文件头的 `---` 元数据不读);选中文字后可从选中处开始。暂停、跟随、退出与消息朗读相同;切到源码、换文件或关闭预览即停止。
|
|
111
112
|
- 语音输入:点输入框右下角的麦克风开始说话,再点一次转成文字插入输入框,单次最长 5 分钟。浏览器会先请求麦克风权限。
|
|
112
113
|
|
|
113
114
|
两者都由本机服务调用火山引擎,手机等其他设备经已配对电脑使用,电脑关机时不可用。
|
package/docs/workflow.md
CHANGED
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
在右侧成员栏可以:
|
|
69
69
|
|
|
70
70
|
- **邀请**成员。程序启动的成员由本机新建会话;想让一个已有对话带着上下文加入,选“手动启动”,在那个对话里执行启动指令注册,之后房间的任务会尽量送进那个窗口。讨论中新邀请的专家排在本轮队尾。
|
|
71
|
-
- **专家预设**:建房与邀请时,“添加”行下方另起一行“预设”,点一下就加入一位已设好描述、专属提示词、项目权限(以及可选的模型与强度)的成员。内置 `Codex-review`(只读,只提问题不改代码)与 `Gemini-docs`(经 agy
|
|
71
|
+
- **专家预设**:建房与邀请时,“添加”行下方另起一行“预设”,点一下就加入一位已设好描述、专属提示词、项目权限(以及可选的模型与强度)的成员。内置 `Codex-review`(只读,只提问题不改代码)与 `Gemini-docs`(经 agy,可写,只改文档);翻开任意卡片点背面的书签图标可把它存为你自己的预设,同名时替换内置预设。点“预设”行末尾的“管理”后,每个预设(含内置)出现删除按钮,删除的内置预设不再显示。预设存在本机服务所在的电脑上,需要连接本机服务。
|
|
72
72
|
- **暂离、请回或移除**成员,讨论不会因此暂停。
|
|
73
73
|
- **更换主持人**:点成员卡片上的皇冠。讨论中更换会暂停讨论。
|
|
74
74
|
- **调整发言顺序**:拖动右栏“发言顺序”中待发言的成员。
|
package/package.json
CHANGED
|
@@ -93,7 +93,7 @@ tts 用朗读设置把 text(不超过 300 字)合成为 MP3,返回 base64
|
|
|
93
93
|
stt 按序接收录音片段(upload_id、index、base64 PCM 16 kHz 单声道、final);最后一段时识别整段录音并返回 text。
|
|
94
94
|
pick-directory 在本机弹出系统目录选择窗口,返回所选路径;取消时不返回路径。
|
|
95
95
|
projects 读取本机已配置的项目(名称与绝对路径);提供 JSON(projects 列表)时整体替换保存。
|
|
96
|
-
presets
|
|
96
|
+
presets 读取本机的专家预设(含未删除的内置预设)与各平台默认描述;提供 JSON(presets 列表、description_templates 和/或 hidden_builtins 已删除的内置预设名)时替换对应部分。
|
|
97
97
|
machine-link 返回本机连接指定 site 的电脑编号、名称与凭据,供已登录管理员的网页完成配对;不存在时创建。
|
|
98
98
|
admin-account 把在指定 site 注册的管理员用户名与密码保存到服务目录的 admin-accounts.json;须提供 site、username、password。
|
|
99
99
|
room-member 为房间邀请、请回一位成员;须提供 site、room_id、token、action(add/rejoin)、name。
|
|
@@ -393,11 +393,13 @@ function english_preset_ids(presets) {
|
|
|
393
393
|
});
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
-
/** The user's expert presets and per-platform default descriptions saved on this computer, plus the built-in presets. */
|
|
396
|
+
/** The user's expert presets and per-platform default descriptions saved on this computer, plus the built-in presets the user has not deleted. */
|
|
397
397
|
export function expert_presets(home) {
|
|
398
398
|
const path = join(home, 'expert-presets.json');
|
|
399
399
|
const saved = existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : {};
|
|
400
|
-
|
|
400
|
+
const hidden_builtins = saved.hidden_builtins || [];
|
|
401
|
+
return { presets: english_preset_ids(saved.presets || []), description_templates: saved.description_templates || {}, hidden_builtins,
|
|
402
|
+
builtin_presets: BUILTIN_EXPERT_PRESETS.filter(preset => !hidden_builtins.includes(preset.name)) };
|
|
401
403
|
}
|
|
402
404
|
|
|
403
405
|
/** Check one expert preset; a workflow's agent card is a preset and obeys the same rule. */
|
|
@@ -412,11 +414,14 @@ export function validate_preset(preset) {
|
|
|
412
414
|
if ([preset.model, preset.effort].some(value => (value || '').length > MAX_PRESET_FIELD)) throw new Error(`${preset.name} 的模型或强度过长`);
|
|
413
415
|
}
|
|
414
416
|
|
|
415
|
-
/** Replace the user's presets
|
|
417
|
+
/** Replace the user's presets, default descriptions or deleted built-ins (whichever the input carries); every field is checked first. */
|
|
416
418
|
export function save_expert_presets(home, input) {
|
|
417
419
|
const current = expert_presets(home);
|
|
418
420
|
const presets = input.presets === undefined ? current.presets : input.presets;
|
|
419
421
|
const templates = input.description_templates === undefined ? current.description_templates : input.description_templates;
|
|
422
|
+
const hidden_builtins = input.hidden_builtins === undefined ? current.hidden_builtins : input.hidden_builtins;
|
|
423
|
+
const builtin_names = BUILTIN_EXPERT_PRESETS.map(preset => preset.name);
|
|
424
|
+
if (!Array.isArray(hidden_builtins) || hidden_builtins.some(name => !builtin_names.includes(name))) throw new Error(`hidden_builtins 只能列出内置预设:${builtin_names.join('、')}`);
|
|
420
425
|
if (!Array.isArray(presets) || presets.length > MAX_EXPERT_PRESETS) throw new Error(`presets 必须为列表,最多 ${MAX_EXPERT_PRESETS} 个`);
|
|
421
426
|
const names = new Set();
|
|
422
427
|
for (const preset of presets) {
|
|
@@ -429,7 +434,7 @@ export function save_expert_presets(home, input) {
|
|
|
429
434
|
throw new Error(`description_templates 须为 {平台名: 描述},每段 1–${MAX_PRESET_DESCRIPTION} 字`);
|
|
430
435
|
}
|
|
431
436
|
mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
432
|
-
atomic_json(join(home, 'expert-presets.json'), { presets, description_templates: templates });
|
|
437
|
+
atomic_json(join(home, 'expert-presets.json'), { presets, description_templates: templates, hidden_builtins });
|
|
433
438
|
return expert_presets(home);
|
|
434
439
|
}
|
|
435
440
|
|
|
@@ -37,6 +37,8 @@ const SWEEP_MS = 6 * 60 * 60 * 1000;
|
|
|
37
37
|
const LOG_LIMIT_BYTES = 5 * 1024 * 1024;
|
|
38
38
|
const LOG_KEEP_BYTES = 1024 * 1024;
|
|
39
39
|
const STALE_LAUNCH_MS = 7 * 24 * 60 * 60 * 1000;
|
|
40
|
+
// One execution's task, credential and software log, e.g. `free-12.task.json`, `claude-free-12.log`; `event-` is the older mode.
|
|
41
|
+
const EXECUTION_FILE = /^(?:[a-z_]+-)?(?:free|event)-(\d+)\.(?:task\.json|access\.json|log)$/;
|
|
40
42
|
const SERVICE_LOGS = ['service.log', join('local', 'website.log'), join('local', 'supervisor.log')];
|
|
41
43
|
const SCRIPT = fileURLToPath(import.meta.url);
|
|
42
44
|
const REPO_ROOT = dirname(dirname(SCRIPT));
|
|
@@ -567,6 +569,35 @@ function trim_log(path) {
|
|
|
567
569
|
return true;
|
|
568
570
|
}
|
|
569
571
|
|
|
572
|
+
/** The execution each member's delivery record still names; its files may be in use however old they are. */
|
|
573
|
+
function current_executions(directory) {
|
|
574
|
+
const path = join(directory, 'runtime.sqlite3');
|
|
575
|
+
if (!existsSync(path)) return new Set();
|
|
576
|
+
const db = new DatabaseSync(path, { readOnly: true });
|
|
577
|
+
try {
|
|
578
|
+
if (!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='free_deliveries'").get()) return new Set();
|
|
579
|
+
return new Set(db.prepare('SELECT execution_id FROM free_deliveries').all().map(row => row.execution_id));
|
|
580
|
+
} catch { return null; } finally { db.close(); }
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** Remove a room's per-execution files once they are a week old and no member's delivery still names the execution. */
|
|
584
|
+
function sweep_execution_files(directory, now) {
|
|
585
|
+
if (!existsSync(directory)) return 0;
|
|
586
|
+
const current = current_executions(directory);
|
|
587
|
+
// An unreadable delivery table gives no proof which executions ended, so nothing is removed.
|
|
588
|
+
if (!current) return 0;
|
|
589
|
+
let removed = 0;
|
|
590
|
+
for (const name of readdirSync(directory)) {
|
|
591
|
+
const id = EXECUTION_FILE.exec(name)?.[1];
|
|
592
|
+
if (!id || current.has(Number(id))) continue;
|
|
593
|
+
const path = join(directory, name);
|
|
594
|
+
if (now - statSync(path).mtimeMs < STALE_LAUNCH_MS) continue;
|
|
595
|
+
rmSync(path, { force: true });
|
|
596
|
+
removed += 1;
|
|
597
|
+
}
|
|
598
|
+
return removed;
|
|
599
|
+
}
|
|
600
|
+
|
|
570
601
|
/** The newest change time of a launch folder's own records, which a pending launch still updates. */
|
|
571
602
|
function launch_touched(folder) {
|
|
572
603
|
return Math.max(...[folder, join(folder, 'launch.json'), join(folder, 'config.json')].filter(existsSync).map(path => statSync(path).mtimeMs));
|
|
@@ -574,23 +605,30 @@ function launch_touched(folder) {
|
|
|
574
605
|
|
|
575
606
|
/**
|
|
576
607
|
* Remove what no registered room uses: launch folders left by rooms deleted while the service was off, or launches
|
|
577
|
-
* never confirmed, once they are a week old and no process holds them; answer drafts whose folder is gone;
|
|
578
|
-
* oversized part of the service's logs. Registered rooms, settings and the local website's database are never touched.
|
|
608
|
+
* never confirmed, once they are a week old and no process holds them; answer drafts whose folder is gone; registered
|
|
609
|
+
* rooms' task, credential and log files of executions that ended a week ago; and the oversized part of the service's logs. Registered rooms, settings and the local website's database are never touched.
|
|
579
610
|
*/
|
|
580
611
|
export function sweep_service_home(home, now = Date.now()) {
|
|
581
612
|
const registered = read_registry(home).map(path => dirname(resolve(path)));
|
|
582
|
-
const launches = join(home, 'launches'), removed = { launches: [], drafts: [], logs: [] };
|
|
613
|
+
const launches = join(home, 'launches'), removed = { launches: [], drafts: [], logs: [], execution_files: 0 };
|
|
583
614
|
for (const name of existsSync(launches) ? readdirSync(launches) : []) {
|
|
584
615
|
const folder = join(launches, name);
|
|
585
616
|
if (registered.includes(folder) || service_pid(folder) || service_pid(join(folder, 'runtime')) || now - launch_touched(folder) < STALE_LAUNCH_MS) continue;
|
|
586
617
|
try { remove_room_files(home, join(folder, 'config.json')); removed.launches.push(name); } catch { /* Still held; the next sweep retries. */ }
|
|
587
618
|
}
|
|
588
619
|
// A draft folder is keyed by its room's data directory; one matching no remaining folder or registered room is orphaned.
|
|
589
|
-
const live = new Set();
|
|
620
|
+
const live = new Set(), runtimes = new Set();
|
|
590
621
|
for (const path of read_registry(home)) {
|
|
591
622
|
const folder = dirname(resolve(path));
|
|
592
623
|
for (const directory of [folder, join(folder, 'runtime')]) live.add(service_drafts(home, directory));
|
|
593
|
-
try {
|
|
624
|
+
try {
|
|
625
|
+
const { directory } = validate_config(JSON.parse(readFileSync(path, 'utf8')), folder);
|
|
626
|
+
live.add(service_drafts(home, directory));
|
|
627
|
+
runtimes.add(resolve(directory));
|
|
628
|
+
} catch { /* Covered by the folder keys; an unreadable configuration keeps its execution files. */ }
|
|
629
|
+
}
|
|
630
|
+
for (const directory of runtimes) {
|
|
631
|
+
try { removed.execution_files += sweep_execution_files(directory, now); } catch { /* Still held; the next sweep retries. */ }
|
|
594
632
|
}
|
|
595
633
|
for (const name of existsSync(launches) ? readdirSync(launches) : []) {
|
|
596
634
|
for (const directory of [join(launches, name), join(launches, name, 'runtime')]) live.add(service_drafts(home, directory));
|
|
@@ -669,8 +707,8 @@ export async function run_service(home, output = () => {}) {
|
|
|
669
707
|
swept_at = Date.now();
|
|
670
708
|
try {
|
|
671
709
|
const removed = sweep_service_home(home);
|
|
672
|
-
if (removed.launches.length || removed.drafts.length || removed.logs.length) {
|
|
673
|
-
log_line(`清理本机文件:启动目录 ${removed.launches.length} 个,草稿 ${removed.drafts.length} 个,日志 ${removed.logs.join('、') || '无'}`);
|
|
710
|
+
if (removed.launches.length || removed.drafts.length || removed.logs.length || removed.execution_files) {
|
|
711
|
+
log_line(`清理本机文件:启动目录 ${removed.launches.length} 个,草稿 ${removed.drafts.length} 个,执行文件 ${removed.execution_files} 个,日志 ${removed.logs.join('、') || '无'}`);
|
|
674
712
|
}
|
|
675
713
|
} catch (error) { log_line(`清理本机文件失败:${error.message}`); }
|
|
676
714
|
}
|
package/runner/tts.mjs
CHANGED
|
@@ -20,8 +20,9 @@ const SPEAKER_PATTERN = /^[\w.-]{1,128}$/;
|
|
|
20
20
|
const MAX_SILENCE_MS = 3000;
|
|
21
21
|
// Synthesized paragraphs are kept on this computer, so hearing the same text again is instant and not billed again.
|
|
22
22
|
const CACHE_DIR = 'tts-cache';
|
|
23
|
-
|
|
24
|
-
const
|
|
23
|
+
// The cache is bounded by total size; past it, the least recently heard entries go until it is back under the target.
|
|
24
|
+
const CACHE_LIMIT_BYTES = 50 * 1024 * 1024;
|
|
25
|
+
const CACHE_KEEP_BYTES = 40 * 1024 * 1024;
|
|
25
26
|
const MAX_MEMBER_VOICES = 50;
|
|
26
27
|
// A default tone instruction (context_texts) so reading sounds told rather than recited; users may change or clear it.
|
|
27
28
|
export const TTS_DEFAULT_CONTEXT = '请用自然放松的语气朗读,像给朋友讲故事一样,有轻重缓急,句子之间自然停顿。';
|
|
@@ -63,7 +64,7 @@ const STATUS_MESSAGES = {
|
|
|
63
64
|
export function tts_settings(home) {
|
|
64
65
|
const path = join(home, SETTINGS_FILE);
|
|
65
66
|
const saved = existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : {};
|
|
66
|
-
return { ...TTS_DEFAULTS, member_voices: {}, ...saved, voices: TTS_VOICES, key_base_url: TTS_BASE_URL };
|
|
67
|
+
return { ...TTS_DEFAULTS, member_voices: {}, file_voice: '', ...saved, voices: TTS_VOICES, key_base_url: TTS_BASE_URL };
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
/** Validate and save the read-aloud settings the page edits. */
|
|
@@ -83,7 +84,10 @@ export function save_tts_settings(home, input) {
|
|
|
83
84
|
if (String(name).length > 40 || !SPEAKER_PATTERN.test(voice)) throw new Error('成员音色无效,请重新选择');
|
|
84
85
|
member_voices[name] = voice;
|
|
85
86
|
}
|
|
86
|
-
|
|
87
|
+
// The voice for reading project files; empty uses the default voice.
|
|
88
|
+
const file_voice = String(input.file_voice ?? '').trim();
|
|
89
|
+
if (file_voice && !SPEAKER_PATTERN.test(file_voice)) throw new Error('文件音色无效,请重新选择');
|
|
90
|
+
const settings = { speaker, speech_rate, context, member_voices, file_voice, ...(api_key_ref ? { api_key_ref } : {}) };
|
|
87
91
|
mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
88
92
|
atomic_json(join(home, SETTINGS_FILE), settings);
|
|
89
93
|
return tts_settings(home);
|
|
@@ -125,10 +129,17 @@ function cache_read(path) {
|
|
|
125
129
|
function cache_write(directory, path, value) {
|
|
126
130
|
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
127
131
|
atomic_json(path, value);
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
+
const entries = readdirSync(directory).filter(name => name.endsWith('.json')).map(name => {
|
|
133
|
+
const { size, mtimeMs } = statSync(join(directory, name));
|
|
134
|
+
return { name, size, mtimeMs };
|
|
135
|
+
});
|
|
136
|
+
let total = entries.reduce((sum, entry) => sum + entry.size, 0);
|
|
137
|
+
if (total <= CACHE_LIMIT_BYTES) return;
|
|
138
|
+
for (const entry of entries.sort((a, b) => a.mtimeMs - b.mtimeMs)) {
|
|
139
|
+
if (total <= CACHE_KEEP_BYTES) break;
|
|
140
|
+
rmSync(join(directory, entry.name), { force: true });
|
|
141
|
+
total -= entry.size;
|
|
142
|
+
}
|
|
132
143
|
}
|
|
133
144
|
|
|
134
145
|
/**
|
package/web/app.js
CHANGED
|
@@ -124,7 +124,7 @@ state.directory_pending = new Map();
|
|
|
124
124
|
state.directory_version = 0;
|
|
125
125
|
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
|
-
state.launch = { available: false, remote: false, relay: null, connect_command: "", profiles: {}, projects: [], presets: [], builtin_presets: [], workflows: [], description_templates: {}, workspace: {}, scheduler: {}, error: "", recoveries: [] };
|
|
127
|
+
state.launch = { available: false, remote: false, relay: null, connect_command: "", profiles: {}, projects: [], presets: [], builtin_presets: [], hidden_builtins: [], workflows: [], description_templates: {}, workspace: {}, scheduler: {}, error: "", recoveries: [] };
|
|
128
128
|
// The workflow chosen in the new-room form: { id, label, members: { card: instance } }; cards removed from the form drop out at submission.
|
|
129
129
|
state.create_workflow = null;
|
|
130
130
|
state.create_host_workflows = false;
|
|
@@ -1215,10 +1215,11 @@ async function move_browser_description_templates() {
|
|
|
1215
1215
|
} catch (error) { console.warn("Moving description templates failed:", error.message); }
|
|
1216
1216
|
}
|
|
1217
1217
|
|
|
1218
|
-
/** Save the user's presets and/or
|
|
1218
|
+
/** Save the user's presets, default descriptions and/or deleted built-ins in the local service and keep its answer. */
|
|
1219
1219
|
async function save_presets(input) {
|
|
1220
1220
|
const result = await local_launch_request("presets", input);
|
|
1221
|
-
Object.assign(state.launch, { presets: result.presets, description_templates: result.description_templates
|
|
1221
|
+
Object.assign(state.launch, { presets: result.presets, description_templates: result.description_templates,
|
|
1222
|
+
builtin_presets: result.builtin_presets || [], hidden_builtins: result.hidden_builtins || [] });
|
|
1222
1223
|
publish_view();
|
|
1223
1224
|
}
|
|
1224
1225
|
|
|
@@ -1304,11 +1305,14 @@ async function save_expert_preset(name, label) {
|
|
|
1304
1305
|
} catch (error) { show_toast(`预设未保存:${error.message}`, true); }
|
|
1305
1306
|
}
|
|
1306
1307
|
|
|
1307
|
-
/** Remove the
|
|
1308
|
+
/** Remove the preset of this name from the add row: the user's own one, and a built-in one of the same name with it. */
|
|
1308
1309
|
async function delete_expert_preset(name) {
|
|
1310
|
+
const builtin = state.launch.builtin_presets.some(item => item.name === name);
|
|
1311
|
+
const shown = expert_presets().find(item => item.name === name)?.label || name;
|
|
1309
1312
|
try {
|
|
1310
|
-
await save_presets({ presets: state.launch.presets.filter(item => item.name !== name)
|
|
1311
|
-
|
|
1313
|
+
await save_presets({ presets: state.launch.presets.filter(item => item.name !== name),
|
|
1314
|
+
...builtin ? { hidden_builtins: [...state.launch.hidden_builtins, name] } : {} });
|
|
1315
|
+
show_toast(`已删除预设「${shown}」`);
|
|
1312
1316
|
} catch (error) { show_toast(`预设未删除:${error.message}`, true); }
|
|
1313
1317
|
}
|
|
1314
1318
|
|
|
@@ -1570,7 +1574,11 @@ async function send_message(event) {
|
|
|
1570
1574
|
element("send-message").disabled = true;
|
|
1571
1575
|
state.sending_message = true;
|
|
1572
1576
|
element("message-input").disabled = true;
|
|
1573
|
-
|
|
1577
|
+
const preview = { author: payload.author, text: payload.text, files: drafts.map(file => file.file.name) };
|
|
1578
|
+
// A message the site will likely queue skips the in-transcript card, which would flash before moving above the composer.
|
|
1579
|
+
const queue = state.room.free?.queued_messages;
|
|
1580
|
+
const likely_queued = payload.queue && (queue?.busy || queue?.items?.length);
|
|
1581
|
+
state.sending_preview = likely_queued ? null : preview;
|
|
1574
1582
|
let sent;
|
|
1575
1583
|
state.sending_done = new Promise(resolve => { sent = resolve; });
|
|
1576
1584
|
publish_view();
|
|
@@ -1585,7 +1593,7 @@ async function send_message(event) {
|
|
|
1585
1593
|
// An unsaved message stays as a failed card in place, the one signal of this failure; the draft is kept for the retry.
|
|
1586
1594
|
if (saved) notify_failure(error, `留言已保存,但页面刷新失败:${error.message}`);
|
|
1587
1595
|
else {
|
|
1588
|
-
state.sending_preview = { ...
|
|
1596
|
+
state.sending_preview = { ...preview, failed: error.message };
|
|
1589
1597
|
error.notified = true;
|
|
1590
1598
|
}
|
|
1591
1599
|
throw error;
|
|
@@ -2412,7 +2420,7 @@ async function load_launch_settings() {
|
|
|
2412
2420
|
// Keep the saved machine defaults so every new form can start from them again.
|
|
2413
2421
|
state.launch.saved_profiles = JSON.parse(JSON.stringify(settings.participants || {}));
|
|
2414
2422
|
Object.assign(state.launch, { available: true, profiles: settings.participants, projects: settings.projects || [], api_providers: settings.api_providers || {}, api_keys: settings.api_keys || {},
|
|
2415
|
-
presets: settings.presets || [], builtin_presets: settings.builtin_presets || [], workflows: settings.workflows || [], description_templates: settings.description_templates || {},
|
|
2423
|
+
presets: settings.presets || [], builtin_presets: settings.builtin_presets || [], hidden_builtins: settings.hidden_builtins || [], workflows: settings.workflows || [], description_templates: settings.description_templates || {},
|
|
2416
2424
|
scheduler_providers: settings.scheduler_providers || {}, scheduler_modes: settings.scheduler_modes || {}, scheduler_default_mode: settings.scheduler_default_mode || "", error: "",
|
|
2417
2425
|
notice: relay_machine ? `经电脑「${relay_machine.name}」执行本机操作` : local_connection ? "已连接本机服务,讨论保存在当前网站" : "" });
|
|
2418
2426
|
Object.values(state.launch.profiles).forEach(fill_api_key);
|