@walkhi/code-relax 0.1.0-beta.10 → 0.1.0-beta.12

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.
Files changed (88) hide show
  1. package/README.md +44 -54
  2. package/dist/bin/codex-remote.mjs +34 -60
  3. package/dist/bin/migrate-relay-db.mjs +104 -0
  4. package/dist/bin/self-relay-server.mjs +17 -10
  5. package/dist/shared/app-server-events.cjs +15 -3
  6. package/dist/shared/file-mentions.cjs +35 -0
  7. package/dist/shared/server-requests.cjs +86 -58
  8. package/dist/skills/code-relax/SKILL.md +1 -1
  9. package/dist/src/desktop-launcher.mjs +6 -11
  10. package/dist/src/file-upload-session.mjs +124 -0
  11. package/dist/src/lan-socket-server.mjs +51 -8
  12. package/dist/src/managed-app-server.mjs +21 -10
  13. package/dist/src/onboarding.mjs +3 -3
  14. package/dist/src/platform/windows/codex-runtime.mjs +138 -4
  15. package/dist/src/platform/windows/desktop-host.mjs +1 -2
  16. package/dist/src/platform/windows/desktop-runtime.ps1 +10 -0
  17. package/dist/src/platform/windows/desktop-shortcuts.ps1 +6 -13
  18. package/dist/src/platform/windows/resident-start.js +8 -0
  19. package/dist/src/platform/windows/resident-startup.ps1 +6 -3
  20. package/dist/src/platform/windows/service-host.mjs +0 -9
  21. package/dist/src/self-relay/admin-page.mjs +1 -1
  22. package/dist/src/self-relay/admin-state.mjs +8 -6
  23. package/dist/src/self-relay/connector.mjs +84 -12
  24. package/dist/src/self-relay/email-accounts.mjs +125 -0
  25. package/dist/src/self-relay/lifecycle.mjs +52 -100
  26. package/dist/src/self-relay/mysql-state.mjs +104 -0
  27. package/dist/src/self-relay/p2p-probe.mjs +1 -1
  28. package/dist/src/self-relay/server.mjs +155 -38
  29. package/dist/src/server.mjs +736 -610
  30. package/dist/src/service-doctor.mjs +1 -1
  31. package/dist/src/service-install.mjs +1 -4
  32. package/dist/src/shared-app-server.mjs +274 -272
  33. package/dist/src/shared-catalog.mjs +1 -11
  34. package/dist/src/shared-recovery.mjs +5 -5
  35. package/dist/src/skill-install.mjs +0 -13
  36. package/package.json +2 -1
  37. package/dist/src/debug-ui.mjs +0 -131
  38. package/dist/src/platform/windows/desktop-shortcut-run.ps1 +0 -5
  39. package/dist/web/activity-view.js +0 -283
  40. package/dist/web/approval-ui.js +0 -188
  41. package/dist/web/capabilities.js +0 -50
  42. package/dist/web/chat-transport.js +0 -196
  43. package/dist/web/chat.css +0 -778
  44. package/dist/web/chat.js +0 -3815
  45. package/dist/web/client-platform.js +0 -11
  46. package/dist/web/community.css +0 -82
  47. package/dist/web/community.html +0 -36
  48. package/dist/web/composer-controller.js +0 -97
  49. package/dist/web/conversation-controller.js +0 -99
  50. package/dist/web/disclosure-state-controller.js +0 -95
  51. package/dist/web/draft-controller.js +0 -103
  52. package/dist/web/harmony-platform.js +0 -52
  53. package/dist/web/highlight-powershell.min.js +0 -39
  54. package/dist/web/highlight.min.js +0 -1262
  55. package/dist/web/history-cache.js +0 -240
  56. package/dist/web/history-controller.js +0 -167
  57. package/dist/web/index.html +0 -288
  58. package/dist/web/lan-session.js +0 -122
  59. package/dist/web/link-action-controller.js +0 -212
  60. package/dist/web/message-send-controller.js +0 -177
  61. package/dist/web/message-view.js +0 -98
  62. package/dist/web/p2p-data-channel.js +0 -50
  63. package/dist/web/p2p-diagnostics.js +0 -28
  64. package/dist/web/p2p-probe.js +0 -123
  65. package/dist/web/page-resume.js +0 -28
  66. package/dist/web/pending-message-store.js +0 -108
  67. package/dist/web/queue-controller.js +0 -82
  68. package/dist/web/resources.json +0 -1
  69. package/dist/web/self-relay-session.js +0 -129
  70. package/dist/web/server-requests.js +0 -67
  71. package/dist/web/station-connection-controller.js +0 -75
  72. package/dist/web/task-list-view.js +0 -289
  73. package/dist/web/thread-attention-controller.js +0 -124
  74. package/dist/web/thread-context-controller.js +0 -30
  75. package/dist/web/thread-list-controller.js +0 -61
  76. package/dist/web/thread-list-sync.js +0 -86
  77. package/dist/web/thread-title-controller.js +0 -57
  78. package/dist/web/timeline-formatters.js +0 -249
  79. package/dist/web/timeline-reducer.js +0 -77
  80. package/dist/web/timeline-renderer.js +0 -161
  81. package/dist/web/timeline-scroll-controller.js +0 -50
  82. package/dist/web/timeline-sync.js +0 -67
  83. package/dist/web/usage-controller.js +0 -258
  84. package/dist/web/vendor/README.md +0 -14
  85. package/dist/web/vendor/highlight-powershell.min.js +0 -39
  86. package/dist/web/vendor/highlight.LICENSE.txt +0 -29
  87. package/dist/web/vendor/highlight.min.js +0 -1262
  88. package/dist/web/vendor/lucide.LICENSE.txt +0 -17
@@ -1,17 +1,14 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { execFile } from 'node:child_process';
4
- import { setTimeout as delay } from 'node:timers/promises';
5
- import { startBackground, processAlive } from '../background-process.mjs';
6
4
  import { promisify } from 'node:util';
7
5
  import { randomBytes, randomUUID } from 'node:crypto';
8
- import { fileURLToPath } from 'node:url';
9
6
  import { hostname } from 'node:os';
10
7
  import QRCode from 'qrcode';
11
8
  import { credentialStore } from '../platform/windows/credential-store.mjs';
12
9
  import { readRecord, writeRecord } from '../service-lifecycle.mjs';
13
10
  import { serviceHealth } from '../service-health.mjs';
14
- import { openControl, controlRequest } from '../service-control.mjs';
11
+ import { controlRequest } from '../service-control.mjs';
15
12
  import { connectBridgeRelay } from './connector.mjs';
16
13
  import { validProbeIceServers } from './p2p-probe.mjs';
17
14
  import { relayAddress, validSecret } from './wire.mjs';
@@ -19,8 +16,6 @@ import { saveLanAuthorizations } from './lan-authorizations.mjs';
19
16
  import { lanNetworks } from '../platform/windows/network-info.mjs';
20
17
 
21
18
  const exec = promisify(execFile);
22
- const script = fileURLToPath(import.meta.url);
23
- const recordPath = root => path.join(root, 'self-relay-session.json');
24
19
  const officialConfig = { url: 'wss://api.uulife.site/relay' };
25
20
 
26
21
  async function loadConfig(stateRoot) {
@@ -58,68 +53,23 @@ async function registrationKey(config) {
58
53
  if (!validSecret(hostKey)) throw new Error('中继注册凭据格式无效。');
59
54
  return hostKey;
60
55
  }
61
- export async function selfRelay(command, stateRoot, requiredMode = '') {
62
- const previous = readRecord(recordPath(stateRoot));
63
- let current;
64
- if (previous) {
65
- try { current = await controlRequest(previous); }
66
- catch (error) { if (error.code !== 'ECONNREFUSED') throw error; }
67
- }
68
- if (command === 'relay-status' && current) return controlRequest(previous, 'POST', 'inspect');
69
- if (command === 'relay-status') return current || { running: false, displayText: '私有中继连接器未运行。' };
70
- if (command === 'relay-stop') {
71
- if (current) await controlRequest(previous, 'POST');
72
- const deadline = Date.now() + 10000;
73
- while (current && processAlive(previous.pid)) {
74
- if (Date.now() >= deadline) throw new Error('私有中继未在 10 秒内退出,未继续恢复服务。');
75
- await delay(100);
76
- }
77
- return { stopped: true, displayText: '私有中继连接器已停止;Bridge 和 Codex 保持运行。' };
78
- }
79
- if (current) {
80
- if (requiredMode) {
81
- const bridge = await serviceHealth(readRecord(path.join(stateRoot, 'bridge-session.json')));
82
- if (bridge?.transportMode !== requiredMode) throw new Error('Codex 内核未就绪;请先执行 shared-start。');
83
- }
84
- if (command === 'relay-revoke') return controlRequest(previous, 'POST', 'revoke');
85
- if (command === 'relay-pair' || (current.relayConnected && !current.paired && !current.qrPath)) return controlRequest(previous, 'POST', 'pair');
86
- if (command === 'relay-start' && current.relayConnected) return controlRequest(previous, 'POST', 'refresh-lan');
87
- return current;
88
- }
89
- if (command === 'relay-revoke') throw new Error('请先启动连接器,再撤销手机授权。');
90
- fs.mkdirSync(stateRoot, { recursive: true });
91
- const instanceId = randomUUID();
92
- const log = path.join(stateRoot, 'self-relay.log');
93
- const readyPath = path.join(stateRoot, `relay-ready-${instanceId}.json`);
94
- const pid = await startBackground(process.execPath, [script, '--worker', stateRoot, instanceId,
95
- command === 'relay-pair' ? 'pair' : '', requiredMode], process.env, [log, log]);
96
- try {
97
- const deadline = Date.now() + 25000;
98
- while (true) {
99
- const result = readRecord(readyPath);
100
- if (result) {
101
- if (result.pid !== pid || result.instanceId !== instanceId) throw new Error('私有中继启动实例不匹配。');
102
- if (result.error) throw new Error(result.error);
103
- return result.status;
104
- }
105
- if (!processAlive(pid)) throw new Error('私有中继启动失败,请查看 self-relay.log。');
106
- if (Date.now() >= deadline) throw new Error('私有中继启动超时。');
107
- await delay(100);
108
- }
109
- } catch (error) { if (processAlive(pid)) process.kill(pid); throw error; }
110
- finally { fs.rmSync(readyPath, { force: true }); }
111
- }
112
-
113
- async function worker(stateRoot, instanceId, refresh = false, requiredMode = '') {
56
+ export async function selfRelay(command, stateRoot) {
57
+ const session = readRecord(path.join(stateRoot, 'bridge-session.json'));
58
+ if (!session) {
59
+ if (command === 'relay-status') return { running: false, displayText: 'Bridge 未运行。' };
60
+ if (command === 'relay-stop') return { stopped: true, displayText: '中继连接已停止。' };
61
+ throw new Error('Bridge 尚未运行,请先执行 relax start。');
62
+ }
63
+ return controlRequest(session, 'POST', command);
64
+ }
65
+
66
+ async function createRelayRuntime(stateRoot) {
114
67
  const config = await loadConfig(stateRoot);
115
68
  const session = () => readRecord(path.join(stateRoot, 'bridge-session.json'));
116
69
  let health = await serviceHealth(session());
117
70
  if (health?.status !== 'ok') throw new Error('Bridge 尚未运行,请先执行 relax start --json。');
118
- if (requiredMode && health.transportMode !== requiredMode) throw new Error('Codex 内核未就绪,请先执行 relax shared-start --json。');
119
- const legacyPath = path.join(stateRoot, 'self-relay-device.json');
120
- const legacy = readRecord(legacyPath);
121
- const stored = await credentialStore('load');
122
- let identities = stored.record || legacy || { version: 1, servers: {} };
71
+ const stored = await credentialStore('load');
72
+ const identities = stored.record || { version: 1, servers: {} };
123
73
  const validate = value => {
124
74
  if (value?.version !== 1 || !value.servers || typeof value.servers !== 'object' || Array.isArray(value.servers)) throw new Error('本机设备身份损坏,未自动覆盖');
125
75
  for (const [url, item] of Object.entries(value.servers)) {
@@ -128,15 +78,6 @@ async function worker(stateRoot, instanceId, refresh = false, requiredMode = '')
128
78
  }
129
79
  };
130
80
  validate(identities);
131
- if (legacy) {
132
- validate(legacy);
133
- if (stored.record && JSON.stringify(stored.record) !== JSON.stringify(legacy)) throw new Error('新旧设备身份冲突,未覆盖任何凭据');
134
- if (!stored.record) {
135
- const verified = await credentialStore('save', identities);
136
- if (JSON.stringify(verified.record) !== JSON.stringify(identities)) throw new Error('凭据迁移校验失败');
137
- }
138
- fs.rmSync(legacyPath);
139
- }
140
81
  let identity = identities.servers[config.url];
141
82
  if (!identity) {
142
83
  identity = { id: randomUUID(), secret: randomBytes(32).toString('base64url'), registered: false };
@@ -148,8 +89,7 @@ async function worker(stateRoot, instanceId, refresh = false, requiredMode = '')
148
89
  try { health = await serviceHealth(session()) || { transportMode: health.transportMode, status: 'offline' }; bridgeError = ''; }
149
90
  catch (error) { bridgeError = error.message; health = { transportMode: health.transportMode, status: 'offline' }; }
150
91
  }
151
- const secret = randomBytes(32).toString('base64url');
152
- let connector, control, qrPath, expiresAt = 0, paired = false, clientCount = 0, attachedCount = 0;
92
+ let connector, qrPath, expiresAt = 0, paired = false, clientCount = 0, attachedCount = 0;
153
93
  let closing = false, online = false, connecting = false, retryTimer, attempts = 0, lastError = '';
154
94
  const audit = (event, details = {}) => console.log(JSON.stringify({ time: new Date().toISOString(), event, ...details }));
155
95
  async function currentLanCandidates() {
@@ -163,7 +103,7 @@ async function worker(stateRoot, instanceId, refresh = false, requiredMode = '')
163
103
  const status = () => {
164
104
  const available = online && !paired && qrPath && Date.now() < expiresAt;
165
105
  const next = !online ? (lastError || '电脑连接器正在恢复中继连接,已保存授权。')
166
- : paired ? '设备已授权,手机可直接重连;网络断开不需要重新扫码。'
106
+ : paired ? '设备已授权,移动端可直接重连;网络断开不需要重新扫码。'
167
107
  : available ? '用要添加的移动设备在有效期内扫描统一配对二维码。连接器在后台运行。'
168
108
  : '尚未完成设备授权;调用 relax pair --json 生成配对二维码。';
169
109
  return { running: !closing, relayConnected: online, mode: 'self-relay', transportMode: health.transportMode,
@@ -177,8 +117,6 @@ async function worker(stateRoot, instanceId, refresh = false, requiredMode = '')
177
117
  if (closing) return;
178
118
  closing = true; clearTimeout(retryTimer);
179
119
  connector?.events.removeAllListeners(); connector?.close();
180
- if (readRecord(recordPath(stateRoot))?.instanceId === instanceId) fs.rmSync(recordPath(stateRoot), { force: true });
181
- await control?.close();
182
120
  }
183
121
  async function saveInvitation(message) {
184
122
  if (!message.code) return;
@@ -261,25 +199,39 @@ async function worker(stateRoot, instanceId, refresh = false, requiredMode = '')
261
199
  audit('lan-candidates-updated', { count: candidates.length });
262
200
  return { ...status(), lanCandidateCount: candidates.length };
263
201
  }
264
- try {
265
- const freshStatus = async () => {
266
- if (online) {
267
- const remote = await connector.command('status'); paired = remote.paired;
268
- await refreshHealth();
269
- }
270
- return status();
271
- };
272
- control = await openControl({ instanceId, secret, shutdown, status, actions: { pair, revoke, inspect: freshStatus, 'refresh-lan': refreshLan } });
273
- writeRecord(recordPath(stateRoot), { pid: process.pid, instanceId, controlSecret: secret, controlPort: control.port });
274
- await connect();
275
- const result = refresh && online ? await pair() : status();
276
- writeRecord(path.join(stateRoot, `relay-ready-${instanceId}.json`), { instanceId, pid: process.pid, status: { ...result, pid: process.pid } });
277
- } catch (error) { await shutdown(); throw error; }
278
- }
279
-
280
- if (process.argv[1] && path.resolve(process.argv[1]) === script && process.argv[2] === '--worker') {
281
- worker(process.argv[3], process.argv[4], process.argv[5] === 'pair', process.argv[6]).catch(error => {
282
- writeRecord(path.join(process.argv[3], `relay-ready-${process.argv[4]}.json`), { instanceId: process.argv[4], pid: process.pid, error: error.message });
283
- console.error(error.message); process.exitCode = 1;
284
- });
285
- }
202
+ const inspect = async () => {
203
+ if (online) {
204
+ const remote = await connector.command('status'); paired = remote.paired;
205
+ await refreshHealth();
206
+ }
207
+ return status();
208
+ };
209
+ try { await connect(); return { status, inspect, pair, revoke, refreshLan, shutdown }; }
210
+ catch (error) { await shutdown(); throw error; }
211
+ }
212
+
213
+ export function createRelayManager(stateRoot) {
214
+ let current;
215
+ let operation = Promise.resolve();
216
+ const run = command => {
217
+ const next = operation.then(async () => {
218
+ if (command === 'relay-status') return current ? current.inspect() : { running: false, displayText: '中继连接未运行。' };
219
+ if (command === 'relay-stop') {
220
+ await current?.shutdown(); current = undefined;
221
+ return { stopped: true, displayText: '中继连接已停止;Bridge 和 Codex 保持运行。' };
222
+ }
223
+ if (!current) {
224
+ if (command === 'relay-revoke') throw new Error('请先启动中继连接,再撤销移动端授权。');
225
+ current = await createRelayRuntime(stateRoot);
226
+ return command === 'relay-pair' && current.status().relayConnected ? current.pair() : current.status();
227
+ }
228
+ if (command === 'relay-revoke') return current.revoke();
229
+ if (command === 'relay-pair' || (current.status().relayConnected && !current.status().paired && !current.status().qrPath)) return current.pair();
230
+ if (command === 'relay-start' && current.status().relayConnected) return current.refreshLan();
231
+ return current.status();
232
+ });
233
+ operation = next.catch(() => {});
234
+ return next;
235
+ };
236
+ return { run, close: () => run('relay-stop'), status: () => current?.status() || { running: false } };
237
+ }
@@ -0,0 +1,104 @@
1
+ import fs from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { fileURLToPath } from 'node:url';
4
+ import mysql from 'mysql2/promise';
5
+ import { deviceStore } from './device-store.mjs';
6
+ import { relayAdminState } from './admin-state.mjs';
7
+
8
+ const sqlTime = value => value.replace('T', ' ').replace(/Z$/, '');
9
+ const isoTime = value => value.replace(' ', 'T') + 'Z';
10
+
11
+ export async function openRelayMysql(configFile, initialMaxRelaySessions) {
12
+ const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
13
+ if (!config.database || !config.user || !config.password || !(config.socketPath || config.host)) {
14
+ throw new Error('Incomplete relay MySQL configuration');
15
+ }
16
+ const pool = mysql.createPool({ ...config, waitForConnections: true, connectionLimit: 4,
17
+ namedPlaceholders: false, dateStrings: true, timezone: 'Z', multipleStatements: false });
18
+ try {
19
+ const [versions] = await pool.query('SELECT version, sha256 FROM schema_migrations');
20
+ const knownVersions = ['001_relay_core', '002_email_accounts'];
21
+ if (!versions.some(row => row.version === knownVersions[0]) || versions.length > knownVersions.length) throw new Error('Unexpected relay database schema');
22
+ for (const row of versions) {
23
+ if (!knownVersions.includes(row.version)) throw new Error('Unexpected relay database schema');
24
+ const source = fs.readFileSync(fileURLToPath(new URL(`../../database/${row.version}.sql`, import.meta.url)));
25
+ if (row.sha256 !== createHash('sha256').update(source).digest('hex')) throw new Error(`Unexpected relay database schema: ${row.version}`);
26
+ }
27
+ const [computers] = await pool.query('SELECT id, host_hash, status, revision, created_at, updated_at FROM computers');
28
+ const [clients] = await pool.query('SELECT computer_id, client_id, credential_hash, created_at, updated_at FROM mobile_authorizations ORDER BY created_at');
29
+ const [official] = await pool.query('SELECT computer_id, environment_id, name, hostname, updated_at FROM official_computers');
30
+ const records = new Map(computers.map(row => [row.id, { id: row.id, hostHash: row.host_hash, status: row.status,
31
+ revision: Number(row.revision), createdAt: isoTime(row.created_at), updatedAt: isoTime(row.updated_at), clients: [] }]));
32
+ for (const row of clients) {
33
+ const record = records.get(row.computer_id);
34
+ if (!record) throw new Error('Orphan mobile authorization');
35
+ record.clients.push({ id: row.client_id, hash: row.credential_hash,
36
+ createdAt: isoTime(row.created_at), updatedAt: isoTime(row.updated_at) });
37
+ }
38
+ for (const row of official) {
39
+ const record = records.get(row.computer_id);
40
+ if (!record) throw new Error('Orphan official computer');
41
+ record.officialComputer = { environmentId: row.environment_id, name: row.name,
42
+ hostname: row.hostname, updatedAt: isoTime(row.updated_at) };
43
+ }
44
+ const validator = deviceStore();
45
+ for (const record of records.values()) validator.save(record);
46
+
47
+ const [settings] = await pool.query('SELECT max_relay_sessions FROM relay_settings WHERE id = 1');
48
+ if (settings.length !== 1) throw new Error('Missing relay settings');
49
+ const [rows] = await pool.query('SELECT id, title, message, status, created_at, updated_at, published_at FROM announcements ORDER BY created_at DESC');
50
+ const announcements = rows.map(row => ({ id: row.id, title: row.title, message: row.message, status: row.status,
51
+ createdAt: isoTime(row.created_at), updatedAt: isoTime(row.updated_at),
52
+ publishedAt: row.published_at ? isoTime(row.published_at) : null }));
53
+ const active = announcements.find(item => item.status === 'active');
54
+ const initial = { version: 3, maxRelaySessions: settings[0].max_relay_sessions, announcements,
55
+ announcement: active ? { id: active.id, title: active.title, message: active.message, publishedAt: active.publishedAt } : null };
56
+ const admin = relayAdminState(undefined, initialMaxRelaySessions, { initial, persist: async next => {
57
+ const connection = await pool.getConnection();
58
+ try {
59
+ await connection.beginTransaction();
60
+ await connection.execute('UPDATE relay_settings SET max_relay_sessions = ?, updated_at = UTC_TIMESTAMP(3) WHERE id = 1', [next.maxRelaySessions]);
61
+ await connection.query('DELETE FROM announcements');
62
+ for (const item of next.announcements) await connection.execute(
63
+ 'INSERT INTO announcements (id,title,message,status,created_at,updated_at,published_at) VALUES (?,?,?,?,?,?,?)',
64
+ [item.id, item.title, item.message, item.status, sqlTime(item.createdAt), sqlTime(item.updatedAt),
65
+ item.publishedAt ? sqlTime(item.publishedAt) : null]);
66
+ await connection.commit();
67
+ } catch (error) { await connection.rollback(); throw error; }
68
+ finally { connection.release(); }
69
+ } });
70
+ return {
71
+ pool,
72
+ schemaVersions: versions.map(row => row.version),
73
+ devices: {
74
+ entries: () => [...records.values()],
75
+ async save(record) {
76
+ validator.save(record);
77
+ const connection = await pool.getConnection();
78
+ try {
79
+ await connection.beginTransaction();
80
+ await connection.execute(`INSERT INTO computers (id,host_hash,status,revision,created_at,updated_at)
81
+ VALUES (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE host_hash=VALUES(host_hash),status=VALUES(status),
82
+ revision=VALUES(revision),updated_at=VALUES(updated_at)`,
83
+ [record.id, record.hostHash, record.status, record.revision, sqlTime(record.createdAt), sqlTime(record.updatedAt)]);
84
+ await connection.execute('DELETE FROM mobile_authorizations WHERE computer_id = ?', [record.id]);
85
+ for (const client of record.clients) await connection.execute(
86
+ 'INSERT INTO mobile_authorizations (computer_id,client_id,credential_hash,created_at,updated_at) VALUES (?,?,?,?,?)',
87
+ [record.id, client.id, client.hash, sqlTime(client.createdAt), sqlTime(client.updatedAt)]);
88
+ if (record.officialComputer) {
89
+ const item = record.officialComputer;
90
+ await connection.execute(`INSERT INTO official_computers (computer_id,environment_id,name,hostname,updated_at)
91
+ VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE environment_id=VALUES(environment_id),name=VALUES(name),
92
+ hostname=VALUES(hostname),updated_at=VALUES(updated_at)`,
93
+ [record.id, item.environmentId, item.name, item.hostname, sqlTime(item.updatedAt)]);
94
+ }
95
+ await connection.commit(); records.set(record.id, record);
96
+ } catch (error) { await connection.rollback(); throw error; }
97
+ finally { connection.release(); }
98
+ },
99
+ },
100
+ admin,
101
+ close: () => pool.end(),
102
+ };
103
+ } catch (error) { await pool.end(); throw error; }
104
+ }
@@ -110,7 +110,7 @@ export function validProbeSignal(value) {
110
110
  if (!value || value.version !== 1 || !/^[\w-]{16,128}$/.test(value.probeId || '')) return false;
111
111
  if (['start', 'close'].includes(value.kind)) return true;
112
112
  if (['offer', 'answer'].includes(value.kind)) return typeof value.sdp === 'string' && value.sdp.length <= 65536 &&
113
- (value.direct === undefined || value.direct === 1) &&
113
+ (value.direct === undefined || value.direct === 0 || value.direct === 1) &&
114
114
  (value.iceServers === undefined || validProbeIceServers(value.iceServers));
115
115
  if (value.kind === 'candidate') return typeof value.candidate === 'string' && value.candidate.length <= 2048 &&
116
116
  typeof value.mid === 'string' && value.mid.length <= 64;