@walkhi/code-relax 0.1.0-beta.7 → 0.1.0-beta.9
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 +12 -13
- package/dist/bin/codex-remote.mjs +43 -18
- package/dist/bin/self-relay-demo.mjs +1 -1
- package/dist/shared/app-server-events.cjs +5 -3
- package/dist/src/desktop-launcher.mjs +7 -10
- package/dist/src/managed-app-server.mjs +3 -0
- package/dist/src/message-images.mjs +0 -18
- package/dist/src/onboarding.mjs +3 -4
- package/dist/src/platform/README.md +1 -1
- package/dist/src/platform/windows/desktop-shortcut-run.ps1 +1 -1
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +1 -1
- package/dist/src/platform/windows/resident-startup.ps1 +58 -18
- package/dist/src/platform/windows/service-host.mjs +4 -7
- package/dist/src/platform/windows/stop-desktop.ps1 +7 -4
- package/dist/src/self-relay/admin-page.mjs +54 -0
- package/dist/src/self-relay/admin-state.mjs +25 -12
- package/dist/src/self-relay/demo.mjs +2 -3
- package/dist/src/self-relay/lifecycle.mjs +6 -6
- package/dist/src/self-relay/server.mjs +7 -11
- package/dist/src/server.mjs +163 -1034
- package/dist/src/service-doctor.mjs +10 -5
- package/dist/src/service-lifecycle.mjs +3 -8
- package/dist/src/shared-app-server.mjs +13 -11
- package/dist/src/shared-recovery.mjs +5 -5
- package/dist/src/shared-thread-stream.mjs +1 -1
- package/dist/web/capabilities.js +1 -8
- package/dist/web/chat-transport.js +6 -12
- package/dist/web/chat.css +13 -14
- package/dist/web/chat.js +23 -41
- package/dist/web/community.css +15 -10
- package/dist/web/community.html +4 -5
- package/dist/web/composer-controller.js +0 -4
- package/dist/web/index.html +7 -39
- package/dist/web/resources.json +1 -1
- package/dist/web/task-list-view.js +1 -8
- package/dist/web/timeline-reducer.js +0 -4
- package/package.json +16 -18
- package/tools/postinstall.mjs +4 -1
- package/dist/src/app-server-client.mjs +0 -468
- package/dist/src/app-server-tasks.mjs +0 -358
- package/dist/src/platform/windows/desktop-monitor.mjs +0 -34
- package/dist/src/platform/windows/desktop-tools.mjs +0 -48
- package/dist/src/thread-catalog.mjs +0 -47
- package/dist/tools/find-desktop-pipe.mjs +0 -10
- package/dist/web/community-view.js +0 -20
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
|
|
5
5
|
export function relayAdminState(file, initialMaxRelaySessions) {
|
|
6
|
-
let value = { version:
|
|
6
|
+
let value = { version: 2, maxRelaySessions: initialMaxRelaySessions, announcement: null, announcements: [] };
|
|
7
7
|
if (file && fs.existsSync(file)) value = validate(JSON.parse(fs.readFileSync(file, 'utf8')));
|
|
8
8
|
|
|
9
9
|
function save(next) {
|
|
@@ -32,25 +32,38 @@ export function relayAdminState(file, initialMaxRelaySessions) {
|
|
|
32
32
|
read: () => structuredClone(value),
|
|
33
33
|
setMaxRelaySessions(maxRelaySessions) { return save({ ...value, maxRelaySessions }); },
|
|
34
34
|
publish(title, message) {
|
|
35
|
-
|
|
36
|
-
publishedAt: new Date().toISOString() }
|
|
35
|
+
const announcement = { id: randomUUID(), title: clean(title, 80), message: clean(message, 2000),
|
|
36
|
+
publishedAt: new Date().toISOString() };
|
|
37
|
+
return save({ ...value, announcement, announcements: [announcement, ...value.announcements] });
|
|
37
38
|
},
|
|
38
39
|
withdraw() { return save({ ...value, announcement: null }); },
|
|
40
|
+
deleteAnnouncement(id) {
|
|
41
|
+
if (typeof id !== 'string' || !value.announcements.some(item => item.id === id)) throw new Error('Announcement not found');
|
|
42
|
+
if (value.announcement?.id === id) throw new Error('Withdraw the current announcement before deleting it');
|
|
43
|
+
return save({ ...value, announcements: value.announcements.filter(item => item.id !== id) });
|
|
44
|
+
},
|
|
39
45
|
};
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
function validate(source) {
|
|
43
|
-
if (!source || source.version
|
|
49
|
+
if (!source || ![1, 2].includes(source.version) || !Number.isSafeInteger(source.maxRelaySessions) ||
|
|
44
50
|
source.maxRelaySessions < 1 || source.maxRelaySessions > 256) throw new Error('Invalid relay admin state');
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
const announcement = source.announcement == null ? null : validateAnnouncement(source.announcement);
|
|
52
|
+
const announcements = source.version === 1 ? (announcement ? [announcement] : []) : source.announcements;
|
|
53
|
+
if (!Array.isArray(announcements)) throw new Error('Invalid relay announcement history');
|
|
54
|
+
const validated = announcements.map(validateAnnouncement);
|
|
55
|
+
if (new Set(validated.map(item => item.id)).size !== validated.length ||
|
|
56
|
+
(announcement && !validated.some(item => item.id === announcement.id &&
|
|
57
|
+
item.title === announcement.title && item.message === announcement.message && item.publishedAt === announcement.publishedAt))) {
|
|
58
|
+
throw new Error('Invalid relay announcement history');
|
|
52
59
|
}
|
|
53
|
-
return { version:
|
|
60
|
+
return { version: 2, maxRelaySessions: source.maxRelaySessions, announcement, announcements: validated };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function validateAnnouncement(item) {
|
|
64
|
+
if (!item || !/^[\w-]{16,128}$/.test(item.id) || typeof item.publishedAt !== 'string' ||
|
|
65
|
+
!Number.isFinite(Date.parse(item.publishedAt))) throw new Error('Invalid relay announcement');
|
|
66
|
+
return { id: item.id, title: clean(item.title, 80), message: clean(item.message, 2000), publishedAt: item.publishedAt };
|
|
54
67
|
}
|
|
55
68
|
|
|
56
69
|
function clean(value, limit) {
|
|
@@ -7,7 +7,7 @@ import { connectRelayClient } from './client.mjs';
|
|
|
7
7
|
import { secret } from './wire.mjs';
|
|
8
8
|
|
|
9
9
|
export async function startRelayDemo({ mode = 'lan-shared', pairingTtlMs, dispatch, remoteRelay, hostName = 'TEST-PC', lanCandidates = [], relayOptions = {} } = {}) {
|
|
10
|
-
if (
|
|
10
|
+
if (mode !== 'lan-shared') throw new Error('Invalid demo mode');
|
|
11
11
|
const token = secret(), calls = [];
|
|
12
12
|
const bridge = http.createServer((_request, response) => { response.writeHead(404); response.end(); });
|
|
13
13
|
const lan = attachLanSocket(bridge, { token, dispatch: async (request, reply) => {
|
|
@@ -17,8 +17,7 @@ export async function startRelayDemo({ mode = 'lan-shared', pairingTtlMs, dispat
|
|
|
17
17
|
if (request.url.split('?')[0] === '/api/threads/demo/events') {
|
|
18
18
|
reply.startEvents(); reply.event('update', { thread: { id: 'demo' }, simulated: true }); return;
|
|
19
19
|
}
|
|
20
|
-
if (request.url === '/api/demo/stop') return reply.json(
|
|
21
|
-
mode === 'lan-shared' ? { stopped: true, simulated: true } : { error: '基础控制不支持停止任务' });
|
|
20
|
+
if (request.url === '/api/demo/stop') return reply.json(200, { stopped: true, simulated: true });
|
|
22
21
|
reply.json(404, { error: '模拟 Bridge 不执行真实任务' });
|
|
23
22
|
} });
|
|
24
23
|
await new Promise(resolve => bridge.listen(0, '127.0.0.1', resolve));
|
|
@@ -79,7 +79,7 @@ export async function selfRelay(command, stateRoot, requiredMode = '') {
|
|
|
79
79
|
if (current) {
|
|
80
80
|
if (requiredMode) {
|
|
81
81
|
const bridge = await serviceHealth(readRecord(path.join(stateRoot, 'bridge-session.json')));
|
|
82
|
-
if (bridge?.transportMode !== requiredMode) throw new Error('
|
|
82
|
+
if (bridge?.transportMode !== requiredMode) throw new Error('Codex 内核未就绪;请先执行 shared-start。');
|
|
83
83
|
}
|
|
84
84
|
if (command === 'relay-revoke') return controlRequest(previous, 'POST', 'revoke');
|
|
85
85
|
if (command === 'relay-pair' || (current.relayConnected && !current.paired && !current.qrPath)) return controlRequest(previous, 'POST', 'pair');
|
|
@@ -115,7 +115,7 @@ async function worker(stateRoot, instanceId, refresh = false, requiredMode = '')
|
|
|
115
115
|
const session = () => readRecord(path.join(stateRoot, 'bridge-session.json'));
|
|
116
116
|
let health = await serviceHealth(session());
|
|
117
117
|
if (health?.status !== 'ok') throw new Error('Bridge 尚未运行,请先执行 relax start --json。');
|
|
118
|
-
if (requiredMode && health.transportMode !== requiredMode) throw new Error('
|
|
118
|
+
if (requiredMode && health.transportMode !== requiredMode) throw new Error('Codex 内核未就绪,请先执行 relax shared-start --json。');
|
|
119
119
|
const legacyPath = path.join(stateRoot, 'self-relay-device.json');
|
|
120
120
|
const legacy = readRecord(legacyPath);
|
|
121
121
|
const stored = await credentialStore('load');
|
|
@@ -145,8 +145,8 @@ async function worker(stateRoot, instanceId, refresh = false, requiredMode = '')
|
|
|
145
145
|
const hostKey = identity.registered ? undefined : await registrationKey(config);
|
|
146
146
|
let bridgeError = '';
|
|
147
147
|
async function refreshHealth() {
|
|
148
|
-
try { health = await serviceHealth(session()) || { transportMode: health.transportMode, status: 'offline'
|
|
149
|
-
catch (error) { bridgeError = error.message; health = { transportMode: health.transportMode, status: 'offline'
|
|
148
|
+
try { health = await serviceHealth(session()) || { transportMode: health.transportMode, status: 'offline' }; bridgeError = ''; }
|
|
149
|
+
catch (error) { bridgeError = error.message; health = { transportMode: health.transportMode, status: 'offline' }; }
|
|
150
150
|
}
|
|
151
151
|
const secret = randomBytes(32).toString('base64url');
|
|
152
152
|
let connector, control, qrPath, expiresAt = 0, paired = false, clientCount = 0, attachedCount = 0;
|
|
@@ -168,10 +168,10 @@ async function worker(stateRoot, instanceId, refresh = false, requiredMode = '')
|
|
|
168
168
|
: '尚未完成设备授权;调用 relax pair --json 生成配对二维码。';
|
|
169
169
|
return { running: !closing, relayConnected: online, mode: 'self-relay', transportMode: health.transportMode,
|
|
170
170
|
relayUrl: config.url, deviceId: identity.id, paired, clientCount, authorization: online ? (paired ? 'authorized' : 'unpaired') : 'unverified', phoneConnected: attachedCount > 0,
|
|
171
|
-
bridgeReady: health.status === 'ok' &&
|
|
171
|
+
bridgeReady: health.status === 'ok' && health.execution?.ready === true && health.sharedAppServerConnected === true, bridgeError,
|
|
172
172
|
expiresAt: expiresAt ? new Date(expiresAt).toISOString() : null,
|
|
173
173
|
...(available ? { qrPath } : {}), mobileAccess: 'unverified', lastError,
|
|
174
|
-
displayText: `Code Relax 中继 · ${health.transportMode === 'lan-shared' ? '
|
|
174
|
+
displayText: `Code Relax 中继 · ${health.transportMode === 'lan-shared' ? 'Codex 内核已就绪' : 'Codex 内核未就绪'}\n${next}\n停止连接器保留授权:relax relay-stop --json。撤销全部移动设备:relax relay-revoke --json。` };
|
|
175
175
|
};
|
|
176
176
|
async function shutdown() {
|
|
177
177
|
if (closing) return;
|
|
@@ -6,6 +6,7 @@ import { secret, digest, matches, validSecret, send, decode } from './wire.mjs';
|
|
|
6
6
|
import { readDebugAllowlist, debugAllowed } from './debug-allowlist.mjs';
|
|
7
7
|
import { validProbeSignal } from './p2p-probe.mjs';
|
|
8
8
|
import { relayAdminState } from './admin-state.mjs';
|
|
9
|
+
import { adminPage } from './admin-page.mjs';
|
|
9
10
|
|
|
10
11
|
// Always loopback; a TLS reverse proxy supplies the private-trial Internet endpoint.
|
|
11
12
|
export async function startSelfRelay({ port = 0, hostKey = secret(), pairingTtlMs = 120000,
|
|
@@ -91,7 +92,8 @@ export async function startSelfRelay({ port = 0, hostKey = secret(), pairingTtlM
|
|
|
91
92
|
adminReply(response, 200, { maxRelaySessions: state.maxRelaySessions, activeRelaySessions,
|
|
92
93
|
activeExemptRelaySessions: [...rooms.values()].reduce((sum, room) => sum + [...room.connections.values()].filter(connection => connection.relayExempt).length, 0),
|
|
93
94
|
websocketConnections: wss.clients.size, onlineComputers: [...rooms.values()].filter(room => room.host).length,
|
|
94
|
-
activePhones: [...rooms.values()].reduce((sum, room) => sum + room.connections.size, 0),
|
|
95
|
+
activePhones: [...rooms.values()].reduce((sum, room) => sum + room.connections.size, 0),
|
|
96
|
+
announcement: state.announcement, announcements: state.announcements }); return;
|
|
95
97
|
}
|
|
96
98
|
if (!['PUT', 'DELETE'].includes(request.method || '') || request.headers['content-type'] !== 'application/json' ||
|
|
97
99
|
request.headers['x-code-relax-admin'] !== '1') { adminReply(response, 405, { error: 'Unsupported admin request' }); return; }
|
|
@@ -106,6 +108,10 @@ export async function startSelfRelay({ port = 0, hostKey = secret(), pairingTtlM
|
|
|
106
108
|
} else if (target.pathname === '/admin/api/announcement' && request.method === 'DELETE') {
|
|
107
109
|
const state = adminState.withdraw(); broadcastAnnouncement(state.announcement);
|
|
108
110
|
log({ event: 'admin-announcement-withdrawn' });
|
|
111
|
+
} else if (target.pathname.startsWith('/admin/api/announcements/') && request.method === 'DELETE') {
|
|
112
|
+
const id = target.pathname.slice('/admin/api/announcements/'.length);
|
|
113
|
+
adminState.deleteAnnouncement(id);
|
|
114
|
+
log({ event: 'admin-announcement-deleted', announcementId: id });
|
|
109
115
|
} else { adminReply(response, 404, { error: 'Admin route not found' }); return; }
|
|
110
116
|
adminReply(response, 200, { ok: true });
|
|
111
117
|
} catch (error) { adminReply(response, 400, { error: error.message }); }
|
|
@@ -456,13 +462,3 @@ function readBody(request, limit) {
|
|
|
456
462
|
request.on('end', () => resolve(value)); request.on('error', reject);
|
|
457
463
|
});
|
|
458
464
|
}
|
|
459
|
-
|
|
460
|
-
function adminPage() {
|
|
461
|
-
return `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Code Relax 管理</title><style>
|
|
462
|
-
body{margin:0;background:#0d1210;color:#eef4f0;font:15px system-ui,sans-serif}main{max-width:760px;margin:40px auto;padding:0 20px}section{background:#151d19;border:1px solid #2a3931;border-radius:14px;padding:18px;margin:16px 0}h1{font-size:24px}h2{font-size:17px}label{display:block;margin:12px 0 6px;color:#aab7af}input,textarea{box-sizing:border-box;width:100%;padding:11px;border:1px solid #43564b;border-radius:9px;background:#0d1210;color:#eef4f0}textarea{min-height:120px;resize:vertical}button{margin:12px 8px 0 0;padding:10px 16px;border:0;border-radius:9px;background:#2c7653;color:white}button.secondary{background:#39443e}pre{white-space:pre-wrap;color:#b9c8bf}.row{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.metric{padding:12px;background:#0d1210;border-radius:9px}.metric b{display:block;font-size:22px}</style><main><h1>Code Relax 管理</h1><section><h2>运行状态</h2><div id="metrics" class="row"></div><button onclick="load()">刷新</button></section><section><h2>服务器转发容量</h2><label>最大并发 WSS 转发会话</label><input id="limit" type="number" min="1" max="256"><button onclick="saveLimit()">保存</button></section><section><h2>系统公告</h2><label>标题</label><input id="title" maxlength="80"><label>正文</label><textarea id="message" maxlength="2000"></textarea><button onclick="publish()">发布</button><button class="secondary" onclick="withdraw()">撤下</button><pre id="current"></pre></section><pre id="result"></pre><script>
|
|
463
|
-
const request=async(path,options={})=>{const response=await fetch(path,options);const body=await response.json();if(!response.ok)throw new Error(body.error||('HTTP '+response.status));return body};
|
|
464
|
-
async function load(){try{const s=await request('/admin/api/status');limit.value=s.maxRelaySessions;metrics.innerHTML='<div class="metric"><b>'+s.activeRelaySessions+'</b>受限转发 / '+s.maxRelaySessions+'</div><div class="metric"><b>'+s.activeExemptRelaySessions+'</b>白名单转发</div><div class="metric"><b>'+s.websocketConnections+'</b>WebSocket</div><div class="metric"><b>'+s.onlineComputers+'</b>在线工作站</div><div class="metric"><b>'+s.activePhones+'</b>活动手机</div>';current.textContent=s.announcement?s.announcement.title+'\\n'+s.announcement.message+'\\n'+s.announcement.publishedAt:'当前无公告';}catch(e){result.textContent=e.message}}
|
|
465
|
-
const write=(method,path,body)=>request(path,{method,headers:{'Content-Type':'application/json','X-Code-Relax-Admin':'1'},body:JSON.stringify(body)}).then(()=>{result.textContent='已保存';return load()}).catch(e=>result.textContent=e.message);
|
|
466
|
-
function saveLimit(){return write('PUT','/admin/api/settings',{maxRelaySessions:Number(limit.value)})}function publish(){return write('PUT','/admin/api/announcement',{title:title.value,message:message.value})}function withdraw(){return write('DELETE','/admin/api/announcement',{})}load();
|
|
467
|
-
</script></main></html>`;
|
|
468
|
-
}
|