ai_co_discuss 0.3.58 → 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 +41 -17
- package/web/modern.css +1 -1
- package/web/modern.js +3123 -3123
|
@@ -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
|
@@ -52,6 +52,8 @@ const RELAY_COMMAND_WAIT_MS = 75000;
|
|
|
52
52
|
// this much later is stuck in the network and is given up, so a hung connection cannot hold back reconnecting.
|
|
53
53
|
const RELAY_DEFAULT_WAIT_MS = 9000;
|
|
54
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;
|
|
55
57
|
const RELAY_CIPHER_INFO = "codiscuss-relay-v1";
|
|
56
58
|
// The rooms kept on the paired computer are read and written through its relay connection; only room routes go there.
|
|
57
59
|
const RELAY_PROTOCOL = "codiscuss-relay";
|
|
@@ -235,7 +237,7 @@ async function request_json(path, payload, access_token) {
|
|
|
235
237
|
options.headers["Content-Type"] = "application/json";
|
|
236
238
|
options.body = JSON.stringify(payload);
|
|
237
239
|
}
|
|
238
|
-
if (
|
|
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);
|
|
239
241
|
const response = await fetch(path, options);
|
|
240
242
|
const result = await response.json();
|
|
241
243
|
if (!response.ok) throw Object.assign(new Error(result.error || "请求失败,请重试"), { preflight: response.status >= 400 && response.status < 500, status: response.status });
|
|
@@ -2308,7 +2310,7 @@ async function pair_exchange(machine, payload) {
|
|
|
2308
2310
|
const id = crypto.randomUUID();
|
|
2309
2311
|
const { key, envelope } = await seal_relay_request(machine, id, { action: "device-pair", payload });
|
|
2310
2312
|
const path = `/api/machines/${encodeURIComponent(machine.id)}/pair`;
|
|
2311
|
-
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 }) });
|
|
2312
2314
|
const result = await response.json();
|
|
2313
2315
|
if (!response.ok) throw new Error(result.error || "配对请求被网站拒绝");
|
|
2314
2316
|
if (result.status === "done") return open_relay_response(key, id, result.response);
|
|
@@ -2629,15 +2631,29 @@ async function show_computer_rooms(remote) {
|
|
|
2629
2631
|
await select_room(undefined);
|
|
2630
2632
|
}
|
|
2631
2633
|
|
|
2632
|
-
|
|
2633
|
-
|
|
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 } = {}) {
|
|
2634
2640
|
if (!state.admin.logged_in || computer_connecting) return;
|
|
2635
2641
|
computer_connecting = true;
|
|
2642
|
+
clearTimeout(computer_retry);
|
|
2643
|
+
computer_retry = null;
|
|
2636
2644
|
try {
|
|
2637
|
-
await load_launch_settings();
|
|
2645
|
+
if (reload) await load_launch_settings();
|
|
2638
2646
|
if (relay_machine && !state.remote_rooms) await show_computer_rooms(true);
|
|
2639
2647
|
} catch (error) { console.warn("Connecting to the computer failed:", error.message); }
|
|
2640
|
-
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
|
+
}
|
|
2641
2657
|
}
|
|
2642
2658
|
|
|
2643
2659
|
/** The relay's change notices for the computer's rooms, shaped like the website's update stream. */
|
|
@@ -2806,7 +2822,9 @@ async function load_launch_settings() {
|
|
|
2806
2822
|
state.launch.relay = await load_relay_machine();
|
|
2807
2823
|
// An online computer that publishes a command key does everything the local service would; an older one only creates rooms.
|
|
2808
2824
|
// The previous one stays in use until this answer arrives, so requests made meanwhile still reach the computer.
|
|
2809
|
-
|
|
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;
|
|
2810
2828
|
if (!relay_machine) return;
|
|
2811
2829
|
state.launch.remote = true;
|
|
2812
2830
|
}
|
|
@@ -2827,6 +2845,8 @@ async function load_launch_settings() {
|
|
|
2827
2845
|
// An unpaired or expired device keeps its way to the computer: pairing and renewal go through it.
|
|
2828
2846
|
// A website without rooms of its own reaches the computer only through the relay, so one failed request keeps it.
|
|
2829
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;
|
|
2830
2850
|
state.launch.remote = Boolean(local_connection);
|
|
2831
2851
|
Object.assign(state.launch, { available: false, room_models: null, error: error.message });
|
|
2832
2852
|
// Without a reachable local service, launches can still go through the paired computer.
|
|
@@ -4463,8 +4483,10 @@ async function initialize() {
|
|
|
4463
4483
|
if (name && field === "model") write_launch_model(name, event.target.value);
|
|
4464
4484
|
else if (name) write_launch_field(name, field, event.target.type === "checkbox" ? event.target.checked : event.target.value);
|
|
4465
4485
|
});
|
|
4466
|
-
// Whether this website keeps rooms of its own decides how settings and rooms are read.
|
|
4467
|
-
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
|
+
}
|
|
4468
4490
|
const launch_settings = load_launch_settings();
|
|
4469
4491
|
document.addEventListener("click", download_relay_file, true);
|
|
4470
4492
|
new MutationObserver(() => { if (state.remote_rooms) show_relay_images(); }).observe(document.body, { childList: true, subtree: true });
|
|
@@ -4480,16 +4502,18 @@ async function initialize() {
|
|
|
4480
4502
|
});
|
|
4481
4503
|
if (computer_rooms_only()) {
|
|
4482
4504
|
await launch_settings;
|
|
4483
|
-
//
|
|
4484
|
-
|
|
4485
|
-
state.remote_rooms ||= reachable;
|
|
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 });
|
|
4486
4507
|
}
|
|
4487
4508
|
try {
|
|
4488
|
-
|
|
4489
|
-
state.
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
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
|
+
}
|
|
4493
4517
|
set_connection(true);
|
|
4494
4518
|
} catch (error) { set_connection(false); show_toast(error.message, true); }
|
|
4495
4519
|
start_live_updates();
|