ai_co_discuss 0.2.72 → 0.2.74
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 +140 -185
- package/README.zh-CN.md +135 -0
- package/backend/remote_launch.py +175 -0
- package/backend/schema.sql +25 -0
- package/backend/server.py +35 -1
- package/docs/architecture.md +2 -0
- package/docs/runner.md +15 -3
- package/package.json +2 -2
- package/runner/discussion_launch.mjs +54 -5
- package/runner/discussion_local.mjs +3 -2
- package/runner/discussion_service.mjs +8 -0
- package/runner/remote_launch.mjs +101 -0
- package/web/app.js +68 -13
- package/web/modern.css +1 -1
- package/web/modern.js +2402 -2070
package/web/app.js
CHANGED
|
@@ -28,6 +28,9 @@ const MEMBER_ACCESS_PREFIX = "discuss-members:";
|
|
|
28
28
|
const MEMBER_PENDING_PREFIX = "discuss-member-pending:";
|
|
29
29
|
const LOCAL_CONNECTION_KEY = "discuss-local-connection";
|
|
30
30
|
const LOCAL_CONNECTION_TIMEOUT_MS = 15000;
|
|
31
|
+
// A relayed launch waits this long for the paired computer before the request is kept as a pending item.
|
|
32
|
+
const RELAY_WAIT_MS = 45000;
|
|
33
|
+
const RELAY_POLL_MS = 1500;
|
|
31
34
|
const MODEL_CATALOG_TIMEOUT_MS = 35000;
|
|
32
35
|
let local_connection = null;
|
|
33
36
|
const DEFAULT_USER_LABEL = "User";
|
|
@@ -71,7 +74,7 @@ state.directory_pending = new Map();
|
|
|
71
74
|
state.directory_version = 0;
|
|
72
75
|
state.navigation_version = 0;
|
|
73
76
|
state.admin = { loaded: false, local: false, registered: false, setup_available: false, logged_in: false, profile: null, identity_version: 0 };
|
|
74
|
-
state.launch = { available: false, remote: false, connect_command: "", profiles: {}, workspace: {}, scheduler: {}, create: true, error: "", notice: "", recoveries: [] };
|
|
77
|
+
state.launch = { available: false, remote: false, relay: null, connect_command: "", profiles: {}, projects: [], workspace: {}, scheduler: {}, create: true, error: "", notice: "", recoveries: [] };
|
|
75
78
|
const PLATFORM_CLIENTS = { Codex: [{ value: "codex_cli", label: "Codex CLI" }, { value: "codex", label: "Codex App" }],
|
|
76
79
|
Claude: [{ value: "claude", label: "Claude Code CLI" }],
|
|
77
80
|
Gemini: [{ value: "antigravity_cli", label: "Antigravity CLI" }, { value: "antigravity", label: "Antigravity App" }] };
|
|
@@ -221,6 +224,8 @@ function admin_error(error) {
|
|
|
221
224
|
function accept_admin_session({ token, profile }) {
|
|
222
225
|
store_admin_token(token);
|
|
223
226
|
apply_admin_state({ registered: true, setup_available: false, logged_in: true, profile });
|
|
227
|
+
// Pairing and the paired computer both need the session, so read them again now.
|
|
228
|
+
load_launch_settings();
|
|
224
229
|
}
|
|
225
230
|
|
|
226
231
|
/** Create the single administrator account and sign in with it. */
|
|
@@ -244,6 +249,7 @@ async function logout_admin() {
|
|
|
244
249
|
catch { /* The token is removed below either way. */ }
|
|
245
250
|
store_admin_token("");
|
|
246
251
|
state.admin.logged_in = false;
|
|
252
|
+
state.launch.relay = null;
|
|
247
253
|
publish_view();
|
|
248
254
|
}
|
|
249
255
|
|
|
@@ -1540,8 +1546,9 @@ async function submit_room(event, folder_id = null) {
|
|
|
1540
1546
|
const free = payload.mode === "free";
|
|
1541
1547
|
// A discussion can still be created without the local service and started later from the room;
|
|
1542
1548
|
// a free room needs the service only for members it wakes or for smart scheduling.
|
|
1543
|
-
const
|
|
1544
|
-
|
|
1549
|
+
const launchable = state.launch.available || Boolean(state.launch.relay);
|
|
1550
|
+
const launching = free ? Object.values(payload.experts).includes("cli") || (launchable && Boolean(creation_launch_options().scheduler || state.launch.workspace.cwd || state.launch.workspace.local_project)) : state.launch.create && state.launch.available;
|
|
1551
|
+
if (launching && !launchable) throw new Error("本机唤醒的成员需要连接本机服务;也可改为手动接入或网页代发");
|
|
1545
1552
|
const launch_draft = launching ? creation_launch_options() : null;
|
|
1546
1553
|
// A discussion is only a way to push the room forward: the room is created as a free room, then opened with these limits.
|
|
1547
1554
|
const discussion = free && state.create_discussion ? { goal: payload.goal.trim(), named_followup: state.named_followup,
|
|
@@ -1675,7 +1682,8 @@ async function submit_local_launch(input, title, direct = false, signature) {
|
|
|
1675
1682
|
const id = input.room?.submission_id || `room-${input.room_id}`;
|
|
1676
1683
|
const key = LAUNCH_PENDING_PREFIX + id;
|
|
1677
1684
|
const previous = localStorage.getItem(key);
|
|
1678
|
-
const
|
|
1685
|
+
const relay = !direct && !state.launch.available ? state.launch.relay?.id : undefined;
|
|
1686
|
+
const record = previous ? JSON.parse(previous) : { id, title, input, room_id: input.room_id, direct, signature, ...(relay ? { machine_id: relay } : {}) };
|
|
1679
1687
|
if (previous && JSON.stringify(record.input) !== JSON.stringify(input)) throw new Error("此房间有未完成的原启动请求,请先继续该请求,不要替换配置");
|
|
1680
1688
|
record.attempt_id = crypto.randomUUID();
|
|
1681
1689
|
try { localStorage.setItem(key, JSON.stringify(record)); }
|
|
@@ -1683,7 +1691,7 @@ async function submit_local_launch(input, title, direct = false, signature) {
|
|
|
1683
1691
|
refresh_launch_recoveries();
|
|
1684
1692
|
try {
|
|
1685
1693
|
const result = record.direct ? { room: await request_json("/api/rooms", record.input.room, record.input.token), status: "created" }
|
|
1686
|
-
: await local_launch_request("launch", record.input);
|
|
1694
|
+
: record.machine_id ? await relay_launch_request(record.input, record.machine_id) : await local_launch_request("launch", record.input);
|
|
1687
1695
|
save_room_credentials(result.room, record.input.token, record.input.room?.members || []);
|
|
1688
1696
|
localStorage.removeItem(key);
|
|
1689
1697
|
refresh_launch_recoveries();
|
|
@@ -1752,21 +1760,54 @@ async function load_launch_settings() {
|
|
|
1752
1760
|
local_connection = restore_local_connection();
|
|
1753
1761
|
state.launch.remote = Boolean(local_connection);
|
|
1754
1762
|
if (local_connection) state.launch.connect_command = `codiscuss local connect "${location.origin}"`;
|
|
1755
|
-
if (!local_connection && !local) return;
|
|
1763
|
+
if (!local_connection && !local) { state.launch.relay = await load_relay_machine(); return; }
|
|
1756
1764
|
const settings = await local_launch_request("settings");
|
|
1757
1765
|
// Keep the saved machine defaults so every new form can start from them again.
|
|
1758
1766
|
state.launch.saved_profiles = JSON.parse(JSON.stringify(settings.participants || {}));
|
|
1759
|
-
Object.assign(state.launch, { available: true, profiles: settings.participants, api_providers: settings.api_providers || {}, api_keys: settings.api_keys || {},
|
|
1767
|
+
Object.assign(state.launch, { available: true, profiles: settings.participants, projects: settings.projects || [], api_providers: settings.api_providers || {}, api_keys: settings.api_keys || {},
|
|
1760
1768
|
scheduler_providers: settings.scheduler_providers || {}, error: "",
|
|
1761
1769
|
notice: local_connection ? "已连接本机服务,讨论保存在当前网站" : "" });
|
|
1762
1770
|
Object.values(state.launch.profiles).forEach(fill_api_key);
|
|
1771
|
+
if (local_connection) pair_local_machine();
|
|
1763
1772
|
// Settings arrive after the form was reset; only an untouched draft gets the default.
|
|
1764
1773
|
if (!Object.keys(state.launch.scheduler).length) state.launch.scheduler = default_scheduler();
|
|
1765
1774
|
load_room_models();
|
|
1766
|
-
} catch (error) {
|
|
1775
|
+
} catch (error) {
|
|
1776
|
+
Object.assign(state.launch, { available: false, room_models: null, error: error.message });
|
|
1777
|
+
// Without a reachable local service, launches can still go through the paired computer.
|
|
1778
|
+
state.launch.relay = await load_relay_machine();
|
|
1779
|
+
if (state.launch.relay) state.launch.error = "";
|
|
1780
|
+
}
|
|
1767
1781
|
finally { refresh_launch_recoveries(); render_launch_settings(); publish_view(); }
|
|
1768
1782
|
}
|
|
1769
1783
|
|
|
1784
|
+
/** The computer paired with this site, most recently seen first; only the administrator can read or use it. */
|
|
1785
|
+
async function load_relay_machine() {
|
|
1786
|
+
if (!admin_token()) return null;
|
|
1787
|
+
try { return (await request_json("/api/machines")).machines[0] || null; } catch { return null; }
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
/** Pair the connected computer so devices without the local service can launch on it; needs the administrator session. */
|
|
1791
|
+
async function pair_local_machine() {
|
|
1792
|
+
if (!admin_token()) return;
|
|
1793
|
+
try { await request_json("/api/machines", await local_launch_request("machine-link", {})); }
|
|
1794
|
+
catch (error) { console.warn("Pairing this computer failed:", error.message); }
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
/** Hand a room creation to the paired computer through the website and wait for it to finish. */
|
|
1798
|
+
async function relay_launch_request(input, machine_id) {
|
|
1799
|
+
const id = input.room.submission_id;
|
|
1800
|
+
let launch = await request_json("/api/remote-launches", { id, machine_id, request: input });
|
|
1801
|
+
for (let waited = 0; ["pending", "claimed"].includes(launch.status) && waited < RELAY_WAIT_MS; waited += RELAY_POLL_MS) {
|
|
1802
|
+
await new Promise(resolve => setTimeout(resolve, RELAY_POLL_MS));
|
|
1803
|
+
launch = await request_json(`/api/remote-launches/${encodeURIComponent(id)}`);
|
|
1804
|
+
}
|
|
1805
|
+
if (launch.status === "failed") throw Object.assign(new Error(launch.error), { launch: { stage: launch.result.stage, room_id: launch.result.room_id } });
|
|
1806
|
+
const name = state.launch.relay?.name || "电脑";
|
|
1807
|
+
if (launch.status !== "done") throw Object.assign(new Error(`${name}${launch.status === "pending" ? "尚未接手" : "仍在启动"},请保持它开机并运行 codiscuss,稍后在待办中继续`), { launch: { stage: "unconfirmed" } });
|
|
1808
|
+
return { room: await request_json(`/api/rooms/${launch.result.room_id}`, undefined, input.token), status: launch.result.status };
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1770
1811
|
/** Re-check the local connection and explain when nothing has been connected yet. */
|
|
1771
1812
|
async function retry_launch_connection() {
|
|
1772
1813
|
await load_launch_settings();
|
|
@@ -1948,7 +1989,9 @@ function restore_launch_profile(name, profile) {
|
|
|
1948
1989
|
|
|
1949
1990
|
/** Capture local overrides before asynchronous work or navigation can change the draft. */
|
|
1950
1991
|
function launch_options() {
|
|
1951
|
-
const
|
|
1992
|
+
const { local_project, ...workspace } = state.launch.workspace;
|
|
1993
|
+
// The paired computer resolves a project id to its own folder; a browser with the local service sends the folder itself.
|
|
1994
|
+
const draft = JSON.parse(JSON.stringify({ participants: state.launch.profiles, ...workspace, ...(!state.launch.available && local_project ? { local_project } : {}), entry: "web" }));
|
|
1952
1995
|
for (const profile of Object.values(draft.participants)) if (profile.adapter === "api") profile.project_access = "read";
|
|
1953
1996
|
return draft;
|
|
1954
1997
|
}
|
|
@@ -2077,23 +2120,35 @@ function clear_launch_overrides(draft) {
|
|
|
2077
2120
|
if (state.launch.profiles[name]?.[field] === profile[field]) delete state.launch.profiles[name]?.[field];
|
|
2078
2121
|
}
|
|
2079
2122
|
}
|
|
2080
|
-
for (const field of ["cwd", "discussion_project", "project_access"]) {
|
|
2123
|
+
for (const field of ["cwd", "local_project", "discussion_project", "project_access"]) {
|
|
2081
2124
|
if (state.launch.workspace[field] === draft[field]) delete state.launch.workspace[field];
|
|
2082
2125
|
}
|
|
2083
2126
|
render_launch_settings();
|
|
2084
2127
|
publish_view();
|
|
2085
2128
|
}
|
|
2086
2129
|
|
|
2130
|
+
/** Browse for a folder on the computer running the local service; resolves to "" when cancelled. */
|
|
2131
|
+
async function browse_local_directory() {
|
|
2132
|
+
return (await local_launch_request("pick-directory", {})).path || "";
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2087
2135
|
/** Let the user browse for the project folder on the computer running the local service. */
|
|
2088
2136
|
async function pick_launch_directory() {
|
|
2089
2137
|
try {
|
|
2090
|
-
const
|
|
2091
|
-
if (
|
|
2138
|
+
const path = await browse_local_directory();
|
|
2139
|
+
if (path) write_launch_field("workspace", "cwd", path);
|
|
2092
2140
|
state.launch.error = "";
|
|
2093
2141
|
} catch (error) { state.launch.error = error.message; }
|
|
2094
2142
|
publish_view();
|
|
2095
2143
|
}
|
|
2096
2144
|
|
|
2145
|
+
/** Replace the named project directories kept on the computer running the local service. */
|
|
2146
|
+
async function save_launch_projects(projects) {
|
|
2147
|
+
const result = await local_launch_request("projects", { projects });
|
|
2148
|
+
state.launch.projects = result.projects;
|
|
2149
|
+
publish_view();
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2097
2152
|
/** Save machine defaults without per-discussion session IDs or project write access. */
|
|
2098
2153
|
async function save_launch_defaults() {
|
|
2099
2154
|
// Defaults are per platform; an extra instance's model and effort apply to its own discussion only.
|
|
@@ -3367,7 +3422,7 @@ function change_view_theme(theme) {
|
|
|
3367
3422
|
|
|
3368
3423
|
window.discuss_view = { snapshot: view_snapshot, write_field: write_view_field,
|
|
3369
3424
|
write_launch: write_launch_field, write_room_connection, write_room_adapter, launch_clients: LAUNCH_CLIENTS,
|
|
3370
|
-
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, check_launch_api, launch_summary, launch_can_set_access, launch_can_bind_session, pick_launch_directory, restore_launch_profile, reset_create_form, empty_discussion_message,
|
|
3425
|
+
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, check_launch_api, launch_summary, launch_can_set_access, launch_can_bind_session, pick_launch_directory, browse_local_directory, save_launch_projects, restore_launch_profile, reset_create_form, empty_discussion_message,
|
|
3371
3426
|
start_drag: start_directory_drag,
|
|
3372
3427
|
attachment_markup, attachment_size, attachment_file_icon, drop_attachments, paste_attachments,
|
|
3373
3428
|
write_expert: write_view_expert, add_instance: add_view_instance, platform_of, execute: execute_view_action, theme: change_view_theme,
|