ai_co_discuss 0.3.57 → 0.3.59
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/workflow-creator/SKILL.md +1 -1
- package/package.json +1 -1
- package/runner/discussion_launch.mjs +29 -6
- package/web/app.js +57 -17
- package/web/modern.css +1 -1
- package/web/modern.js +3146 -3141
|
@@ -75,7 +75,7 @@ project_access: write
|
|
|
75
75
|
|
|
76
76
|
- **工作流 ID**(文件夹名 = SKILL.md 的 `name`):小写英文和数字,可用单个连字符连接(不在首尾),1—40 位。`label` 最多 32 字。
|
|
77
77
|
- **`description`** 最多 500 字:写清做什么、什么时候用。主持人只凭它判断一条请求该不该按这个工作流做,写含糊了,该用时会不用、不该用时会套用。
|
|
78
|
-
- **卡片**:最多 30 张。`name` 是卡片 ID,等于文件名,只用小写英文、数字和下划线,最多 16 位。`platform` 是平台 ID:`Claude`、`Codex`、`Antigravity`、`DeepSeek`、`Qwen`、`Grok`(网页显示的 Gemini 写 `Antigravity
|
|
78
|
+
- **卡片**:最多 30 张。`name` 是卡片 ID,等于文件名,只用小写英文、数字和下划线,最多 16 位。`platform` 是平台 ID:`Claude`、`Codex`、`Antigravity`、`DeepSeek`、`Qwen`、`Grok`(网页显示的 Gemini 写 `Antigravity`),须是本机已配置的软件,或已在本机保存 API Key 的模型 API(没有本地软件的平台按官方 API 加入)。成员名由程序拼成 `平台-卡片ID`。
|
|
79
79
|
- **卡片可选字段**:`label`、`description`、`project_access`(read / write / auto / full)、`model`、`effort`。不用的字段直接省略;模型和强度省略即用本机默认值。接入方式、可执行文件、密钥不写进卡片,由各自电脑的配置决定。
|
|
80
80
|
- 头信息只支持单行 `key: value`;不支持多行值、列表和嵌套。
|
|
81
81
|
- 卡片正文留空时使用该软件的默认提示。
|
package/package.json
CHANGED
|
@@ -1400,8 +1400,7 @@ export function workflows_command(home, input = {}) {
|
|
|
1400
1400
|
if (action === 'save') return save_workflow(home, input.workflow, validate_preset);
|
|
1401
1401
|
if (action === 'delete') return delete_workflow(home, input.id);
|
|
1402
1402
|
if (action === 'preview') {
|
|
1403
|
-
|
|
1404
|
-
return preview_workflow(input, validate_preset, name => Boolean(configured[platform_of(name)]));
|
|
1403
|
+
return preview_workflow(input, validate_preset, name => card_platform_ready(home, name));
|
|
1405
1404
|
}
|
|
1406
1405
|
if (action === 'room-cards') return room_cards(home, input);
|
|
1407
1406
|
throw new Error('action 须为 list、get、save、delete、preview 或 room-cards');
|
|
@@ -1494,6 +1493,24 @@ async function host_room(home, access_file) {
|
|
|
1494
1493
|
return { path, config, roster };
|
|
1495
1494
|
}
|
|
1496
1495
|
|
|
1496
|
+
/**
|
|
1497
|
+
* The API settings a program-started card on a platform without local software starts from, as the invite form fills
|
|
1498
|
+
* them: the platform's official API with the key last saved here for that root. Null when the platform has saved
|
|
1499
|
+
* software settings or local software, or no key is saved.
|
|
1500
|
+
*/
|
|
1501
|
+
function card_api_profile(home, platform) {
|
|
1502
|
+
if (launch_settings(home).participants[platform] || platform_adapters(platform).some(id => adapter_profile(id).kind !== 'api')) return null;
|
|
1503
|
+
const provider = API_PROVIDERS[platform];
|
|
1504
|
+
const api_key_ref = provider && saved_api_keys(home)[provider.base_url];
|
|
1505
|
+
return api_key_ref ? { adapter: 'api', base_url: provider.base_url, api_protocol: provider.api_protocol, model: provider.models[0], api_key_ref } : null;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
/** Whether a card's platform can start here: saved software settings, or an API with a saved key. */
|
|
1509
|
+
function card_platform_ready(home, name) {
|
|
1510
|
+
const platform = platform_of(name || '');
|
|
1511
|
+
return Boolean(platform && (launch_settings(home).participants[platform] || card_api_profile(home, platform)));
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1497
1514
|
/**
|
|
1498
1515
|
* Give cards of the room's workflow a member: a present one stays, one that left is brought back, and a card with
|
|
1499
1516
|
* none (or whose member was removed) is invited as a program-started member with the card's settings, label and
|
|
@@ -1508,7 +1525,11 @@ async function join_workflow_cards(home, config, roster, workflow, members, card
|
|
|
1508
1525
|
if (current?.status === 'away') { await room_member(home, { ...owner, action: 'rejoin', name: current.name }); continue; }
|
|
1509
1526
|
const taken = new Set([...roster.map(member => member.name), ...Object.values(next)]);
|
|
1510
1527
|
const name = taken.has(agent.name) ? free_instance(agent.name, taken) : agent.name;
|
|
1511
|
-
const
|
|
1528
|
+
const api = card_api_profile(home, platform_of(agent.name));
|
|
1529
|
+
const card = Object.fromEntries(['project_access', 'model', 'effort'].filter(field => agent[field] && !(api && field === 'effort')).map(field => [field, agent[field]]));
|
|
1530
|
+
// An API member has no command line, so a card asking for more than writing gets the most it allows.
|
|
1531
|
+
if (api && card.project_access && !access_levels('api').includes(card.project_access)) card.project_access = 'write';
|
|
1532
|
+
const profile = { ...api, ...card };
|
|
1512
1533
|
const joined = await room_member(home, { ...owner, action: 'add', name, participants: { [name]: profile } });
|
|
1513
1534
|
next[agent.card] = joined.member;
|
|
1514
1535
|
if (agent.label) labels[joined.member] = agent.label;
|
|
@@ -1591,9 +1612,11 @@ export async function adopt_workflow(home, { access_file, draft_file, workflow_i
|
|
|
1591
1612
|
if (bound !== draft.id && list_workflows(home).some(item => item.id === draft.id)) throw new Error(`工作流库已有 ${draft.id},主持人不能覆盖其他工作流;要启用它请用 --id ${draft.id},新建请换一个 ID`);
|
|
1592
1613
|
workflow = draft;
|
|
1593
1614
|
}
|
|
1594
|
-
const
|
|
1595
|
-
|
|
1596
|
-
|
|
1615
|
+
const missing = workflow.agents.filter(agent => !card_platform_ready(home, agent.name)).map(agent => agent.name || agent.card);
|
|
1616
|
+
if (missing.length) {
|
|
1617
|
+
const ready = [...new Set([...Object.keys(launch_settings(home).participants), ...Object.keys(API_PROVIDERS).filter(platform => card_api_profile(home, platform))])];
|
|
1618
|
+
throw new Error(`本机没有配置这些卡片的软件或 API Key:${missing.join('、')};卡片的 platform 只能用本机已配置的平台 ID:${ready.join('、')}`);
|
|
1619
|
+
}
|
|
1597
1620
|
workflow = workflow_id ? { ...workflow, agents: host_cards(workflow.agents) }
|
|
1598
1621
|
: save_workflow(home, { ...workflow, agents: host_cards(workflow.agents) }, validate_preset);
|
|
1599
1622
|
const owner = { site: config.site, room_id: config.room, token: config.token };
|
package/web/app.js
CHANGED
|
@@ -48,6 +48,12 @@ const RELAY_COMMAND_POLL_MS = 250;
|
|
|
48
48
|
const RELAY_SETTINGS_WAIT_MS = 20000;
|
|
49
49
|
// The computer allows each operation 60 seconds, as the local website does.
|
|
50
50
|
const RELAY_COMMAND_WAIT_MS = 75000;
|
|
51
|
+
// The website answers a relayed request within its wait (9 seconds unless asked for more); a request still unanswered
|
|
52
|
+
// this much later is stuck in the network and is given up, so a hung connection cannot hold back reconnecting.
|
|
53
|
+
const RELAY_DEFAULT_WAIT_MS = 9000;
|
|
54
|
+
const MACHINE_REQUEST_MARGIN_MS = 15000;
|
|
55
|
+
// How long a pairing request waits for the computer over the relay, in milliseconds as the relay counts.
|
|
56
|
+
const PAIR_WAIT_MS = 15000;
|
|
51
57
|
const RELAY_CIPHER_INFO = "codiscuss-relay-v1";
|
|
52
58
|
// The rooms kept on the paired computer are read and written through its relay connection; only room routes go there.
|
|
53
59
|
const RELAY_PROTOCOL = "codiscuss-relay";
|
|
@@ -231,6 +237,7 @@ async function request_json(path, payload, access_token) {
|
|
|
231
237
|
options.headers["Content-Type"] = "application/json";
|
|
232
238
|
options.body = JSON.stringify(payload);
|
|
233
239
|
}
|
|
240
|
+
if (/^\/api\/(machines|admin)/.test(path)) options.signal = AbortSignal.timeout((Number.isInteger(payload?.wait) ? payload.wait : RELAY_DEFAULT_WAIT_MS) + MACHINE_REQUEST_MARGIN_MS);
|
|
234
241
|
const response = await fetch(path, options);
|
|
235
242
|
const result = await response.json();
|
|
236
243
|
if (!response.ok) throw Object.assign(new Error(result.error || "请求失败,请重试"), { preflight: response.status >= 400 && response.status < 500, status: response.status });
|
|
@@ -2303,7 +2310,7 @@ async function pair_exchange(machine, payload) {
|
|
|
2303
2310
|
const id = crypto.randomUUID();
|
|
2304
2311
|
const { key, envelope } = await seal_relay_request(machine, id, { action: "device-pair", payload });
|
|
2305
2312
|
const path = `/api/machines/${encodeURIComponent(machine.id)}/pair`;
|
|
2306
|
-
const response = await fetch(path, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id, envelope, wait:
|
|
2313
|
+
const response = await fetch(path, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id, envelope, wait: PAIR_WAIT_MS }) });
|
|
2307
2314
|
const result = await response.json();
|
|
2308
2315
|
if (!response.ok) throw new Error(result.error || "配对请求被网站拒绝");
|
|
2309
2316
|
if (result.status === "done") return open_relay_response(key, id, result.response);
|
|
@@ -2435,7 +2442,12 @@ async function relay_exchange(action, payload, wait) {
|
|
|
2435
2442
|
const target = computer_rooms_only() ? { target: "computer" } : {};
|
|
2436
2443
|
const { key, envelope } = await seal_relay_request(machine, id, { action, payload, ...target });
|
|
2437
2444
|
const result = await request_json(`/api/machines/${encodeURIComponent(machine.id)}/relay`, { id, envelope, ...(wait ? { wait } : {}) })
|
|
2438
|
-
.catch(error => {
|
|
2445
|
+
.catch(error => {
|
|
2446
|
+
// A request given up in the network may still have reached the computer, so its outcome is unconfirmed.
|
|
2447
|
+
if (error.name === "TimeoutError") throw Object.assign(new Error(`结果待确认:电脑「${machine.name}」未在限时内答复,请稍后查看房间,确认是否已生效后再重试`),
|
|
2448
|
+
{ unconfirmed: true, launch: { stage: "unconfirmed" } });
|
|
2449
|
+
throw error.status === 401 ? Object.assign(error, { need: "pairing" }) : error;
|
|
2450
|
+
});
|
|
2439
2451
|
if (result.status === "done") return open_relay_response(key, id, result.response);
|
|
2440
2452
|
if (result.status === "offline") throw Object.assign(new Error(`电脑「${machine.name}」不在线,请确认它开机并运行 codiscuss`), { offline: true });
|
|
2441
2453
|
throw Object.assign(new Error(`结果待确认:电脑「${machine.name}」未在限时内答复,请稍后查看房间,确认是否已生效后再重试`),
|
|
@@ -2619,15 +2631,29 @@ async function show_computer_rooms(remote) {
|
|
|
2619
2631
|
await select_room(undefined);
|
|
2620
2632
|
}
|
|
2621
2633
|
|
|
2622
|
-
|
|
2623
|
-
|
|
2634
|
+
let computer_retry = null, computer_attempt = 0;
|
|
2635
|
+
/**
|
|
2636
|
+
* Once signed in, find the paired computer and show its rooms. This is the only way the computer's rooms appear; until
|
|
2637
|
+
* they do, it tries again by itself, backing off while the computer stays away. `reload: false` reuses settings just read.
|
|
2638
|
+
*/
|
|
2639
|
+
async function connect_computer_rooms({ reload = true } = {}) {
|
|
2624
2640
|
if (!state.admin.logged_in || computer_connecting) return;
|
|
2625
2641
|
computer_connecting = true;
|
|
2642
|
+
clearTimeout(computer_retry);
|
|
2643
|
+
computer_retry = null;
|
|
2626
2644
|
try {
|
|
2627
|
-
await load_launch_settings();
|
|
2645
|
+
if (reload) await load_launch_settings();
|
|
2628
2646
|
if (relay_machine && !state.remote_rooms) await show_computer_rooms(true);
|
|
2629
2647
|
} catch (error) { console.warn("Connecting to the computer failed:", error.message); }
|
|
2630
|
-
finally {
|
|
2648
|
+
finally {
|
|
2649
|
+
computer_connecting = false;
|
|
2650
|
+
if (state.remote_rooms) computer_attempt = 0;
|
|
2651
|
+
else if (state.admin.logged_in && !document.hidden) {
|
|
2652
|
+
const delay = RECONNECT_DELAYS_MS[Math.min(computer_attempt, RECONNECT_DELAYS_MS.length - 1)];
|
|
2653
|
+
computer_attempt += 1;
|
|
2654
|
+
computer_retry = setTimeout(() => { computer_retry = null; connect_computer_rooms(); }, delay);
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2631
2657
|
}
|
|
2632
2658
|
|
|
2633
2659
|
/** The relay's change notices for the computer's rooms, shaped like the website's update stream. */
|
|
@@ -2796,7 +2822,9 @@ async function load_launch_settings() {
|
|
|
2796
2822
|
state.launch.relay = await load_relay_machine();
|
|
2797
2823
|
// An online computer that publishes a command key does everything the local service would; an older one only creates rooms.
|
|
2798
2824
|
// The previous one stays in use until this answer arrives, so requests made meanwhile still reach the computer.
|
|
2799
|
-
|
|
2825
|
+
// A website without rooms of its own asks the relay itself, which knows at once whether the computer is connected;
|
|
2826
|
+
// the website's own record of the computer's last heartbeat can lag behind when that heartbeat is slow to arrive.
|
|
2827
|
+
relay_machine = state.launch.relay?.public_key && (state.launch.relay.online || computer_rooms_only()) ? state.launch.relay : null;
|
|
2800
2828
|
if (!relay_machine) return;
|
|
2801
2829
|
state.launch.remote = true;
|
|
2802
2830
|
}
|
|
@@ -2815,7 +2843,10 @@ async function load_launch_settings() {
|
|
|
2815
2843
|
load_room_models();
|
|
2816
2844
|
} catch (error) {
|
|
2817
2845
|
// An unpaired or expired device keeps its way to the computer: pairing and renewal go through it.
|
|
2818
|
-
|
|
2846
|
+
// A website without rooms of its own reaches the computer only through the relay, so one failed request keeps it.
|
|
2847
|
+
relay_machine = computer_rooms_only() || error.need === "pairing" || error.need === "renew" ? relay_machine : null;
|
|
2848
|
+
// The relay answering that the computer is not connected is shown as the computer being offline.
|
|
2849
|
+
if (computer_rooms_only() && !state.remote_rooms) state.remote_online = error.offline ? false : null;
|
|
2819
2850
|
state.launch.remote = Boolean(local_connection);
|
|
2820
2851
|
Object.assign(state.launch, { available: false, room_models: null, error: error.message });
|
|
2821
2852
|
// Without a reachable local service, launches can still go through the paired computer.
|
|
@@ -2841,7 +2872,10 @@ async function load_relay_machine() {
|
|
|
2841
2872
|
return null;
|
|
2842
2873
|
}
|
|
2843
2874
|
return machine;
|
|
2844
|
-
} catch {
|
|
2875
|
+
} catch {
|
|
2876
|
+
// A failed read says nothing about the pairing, so the computer known so far stays in use.
|
|
2877
|
+
return state.launch.relay ?? null;
|
|
2878
|
+
}
|
|
2845
2879
|
}
|
|
2846
2880
|
|
|
2847
2881
|
/** Hand a room creation to the paired computer through the website and wait for it to finish. */
|
|
@@ -4449,8 +4483,10 @@ async function initialize() {
|
|
|
4449
4483
|
if (name && field === "model") write_launch_model(name, event.target.value);
|
|
4450
4484
|
else if (name) write_launch_field(name, field, event.target.type === "checkbox" ? event.target.checked : event.target.value);
|
|
4451
4485
|
});
|
|
4452
|
-
// Whether this website keeps rooms of its own decides how settings and rooms are read.
|
|
4453
|
-
await load_admin_state();
|
|
4486
|
+
// Whether this website keeps rooms of its own decides how settings and rooms are read, so it is asked until it answers.
|
|
4487
|
+
for (let attempt = 0; await load_admin_state(), !state.admin.loaded; attempt++) {
|
|
4488
|
+
await new Promise(resolve => setTimeout(resolve, RECONNECT_DELAYS_MS[Math.min(attempt, RECONNECT_DELAYS_MS.length - 1)]));
|
|
4489
|
+
}
|
|
4454
4490
|
const launch_settings = load_launch_settings();
|
|
4455
4491
|
document.addEventListener("click", download_relay_file, true);
|
|
4456
4492
|
new MutationObserver(() => { if (state.remote_rooms) show_relay_images(); }).observe(document.body, { childList: true, subtree: true });
|
|
@@ -4466,14 +4502,18 @@ async function initialize() {
|
|
|
4466
4502
|
});
|
|
4467
4503
|
if (computer_rooms_only()) {
|
|
4468
4504
|
await launch_settings;
|
|
4469
|
-
|
|
4505
|
+
// The computer's rooms appear only through connecting, which keeps trying by itself, so nothing here can hide them.
|
|
4506
|
+
await connect_computer_rooms({ reload: false });
|
|
4470
4507
|
}
|
|
4471
4508
|
try {
|
|
4472
|
-
|
|
4473
|
-
state.
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4509
|
+
// Connecting has already opened the computer's rooms when it reached them.
|
|
4510
|
+
if (!computer_rooms_only() || !state.remote_rooms) {
|
|
4511
|
+
const result = computer_rooms_only() ? { rooms: [] } : await request_json("/api/rooms");
|
|
4512
|
+
state.rooms = result.rooms;
|
|
4513
|
+
state.last_room = result.last_room || "";
|
|
4514
|
+
set_folder_directory(result);
|
|
4515
|
+
await select_room(opening_room());
|
|
4516
|
+
}
|
|
4477
4517
|
set_connection(true);
|
|
4478
4518
|
} catch (error) { set_connection(false); show_toast(error.message, true); }
|
|
4479
4519
|
start_live_updates();
|