@walkhi/code-relax 0.1.0-beta.50 → 0.1.0-beta.51

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 (89) hide show
  1. package/README.md +105 -105
  2. package/dist/bin/cleanup-accounts.mjs +63 -63
  3. package/dist/bin/codex-remote.mjs +403 -403
  4. package/dist/bin/publish-android-release.mjs +63 -63
  5. package/dist/bin/release-upload-server.mjs +190 -190
  6. package/dist/build-platform.json +1 -1
  7. package/dist/get-shared-service-url.ps1 +14 -14
  8. package/dist/shared/code-browser.cjs +159 -159
  9. package/dist/shared/file-changes.cjs +31 -31
  10. package/dist/shared/file-citations.cjs +117 -117
  11. package/dist/shared/file-mentions.cjs +92 -92
  12. package/dist/shared/message-delivery.cjs +115 -115
  13. package/dist/shared/p2p-file-channel.cjs +87 -87
  14. package/dist/shared/server-requests.cjs +311 -311
  15. package/dist/shared/timeline-sync.cjs +173 -173
  16. package/dist/skills/code-relax/SKILL.md +22 -22
  17. package/dist/src/bounded-process-log.mjs +63 -63
  18. package/dist/src/celsius-client.mjs +107 -107
  19. package/dist/src/dots-approval-timeline.mjs +99 -99
  20. package/dist/src/dots-avatar.mjs +42 -42
  21. package/dist/src/dots-chat-projection.mjs +50 -50
  22. package/dist/src/dots-chat-server.mjs +483 -483
  23. package/dist/src/dots-message-images.mjs +319 -319
  24. package/dist/src/dots-profile.mjs +77 -77
  25. package/dist/src/dots-room-activity.mjs +172 -172
  26. package/dist/src/dots-room-client.mjs +135 -135
  27. package/dist/src/dots-room-projection.mjs +94 -94
  28. package/dist/src/dots-schedules.mjs +66 -66
  29. package/dist/src/dots-summary-tasks.mjs +32 -32
  30. package/dist/src/dots-task-catalog.mjs +430 -430
  31. package/dist/src/file-transfer-range.mjs +22 -22
  32. package/dist/src/file-upload-session.mjs +130 -130
  33. package/dist/src/hosted-history-app-server.mjs +103 -103
  34. package/dist/src/hosted-history-cache.mjs +113 -113
  35. package/dist/src/lan-socket-server.mjs +266 -266
  36. package/dist/src/managed-app-server.mjs +373 -373
  37. package/dist/src/managed-endpoint.mjs +25 -25
  38. package/dist/src/message-upload-files.mjs +109 -109
  39. package/dist/src/onboarding.mjs +12 -12
  40. package/dist/src/platform/README.md +1 -1
  41. package/dist/src/platform/codex-protocol.mjs +46 -46
  42. package/dist/src/platform/desktop-project-store.mjs +208 -208
  43. package/dist/src/platform/linux/desktop-host.mjs +87 -87
  44. package/dist/src/platform/linux/process.mjs +53 -53
  45. package/dist/src/platform/linux/system.mjs +50 -50
  46. package/dist/src/platform/linux/user-environment.mjs +9 -9
  47. package/dist/src/platform/macos/codex-runtime.mjs +125 -125
  48. package/dist/src/platform/macos/desktop-host.mjs +86 -86
  49. package/dist/src/platform/macos/user-environment.mjs +9 -9
  50. package/dist/src/platform/native-package.mjs +58 -58
  51. package/dist/src/platform/windows/activate-desktop.ps1 +49 -49
  52. package/dist/src/platform/windows/codex-runtime.mjs +215 -215
  53. package/dist/src/platform/windows/credential-store.mjs +27 -27
  54. package/dist/src/platform/windows/credential-store.ps1 +49 -49
  55. package/dist/src/platform/windows/desktop-host.mjs +65 -65
  56. package/dist/src/platform/windows/desktop-info.ps1 +38 -38
  57. package/dist/src/platform/windows/desktop-project-registration.mjs +4 -4
  58. package/dist/src/platform/windows/desktop-shortcuts.ps1 +14 -14
  59. package/dist/src/platform/windows/dots-profile.mjs +49 -49
  60. package/dist/src/platform/windows/launch-worker.mjs +41 -41
  61. package/dist/src/platform/windows/project-locations.mjs +16 -16
  62. package/dist/src/platform/windows/service-host.mjs +35 -35
  63. package/dist/src/platform/windows/user-environment.mjs +52 -52
  64. package/dist/src/project-management.mjs +58 -58
  65. package/dist/src/runtime-discovery.mjs +47 -47
  66. package/dist/src/self-relay/account-cleanup.mjs +30 -30
  67. package/dist/src/self-relay/admin-page.mjs +80 -80
  68. package/dist/src/self-relay/admin-style.mjs +14 -14
  69. package/dist/src/self-relay/connector.mjs +439 -439
  70. package/dist/src/self-relay/demo.mjs +31 -31
  71. package/dist/src/self-relay/diagnostics.mjs +54 -54
  72. package/dist/src/self-relay/email-accounts.mjs +183 -183
  73. package/dist/src/self-relay/file-wire.mjs +13 -13
  74. package/dist/src/self-relay/p2p-probe.mjs +168 -168
  75. package/dist/src/self-relay/privacy-page.mjs +26 -26
  76. package/dist/src/self-relay/public-releases.mjs +67 -67
  77. package/dist/src/self-relay/rtc-file-bridge.mjs +41 -41
  78. package/dist/src/self-relay/usage-meter.mjs +141 -141
  79. package/dist/src/server.mjs +5 -4
  80. package/dist/src/service-doctor.mjs +148 -148
  81. package/dist/src/service-install.mjs +97 -97
  82. package/dist/src/service-lifecycle.mjs +139 -139
  83. package/dist/src/shared-app-server.mjs +666 -666
  84. package/dist/src/shared-catalog.mjs +132 -132
  85. package/dist/src/shared-thread-stream.mjs +165 -165
  86. package/dist/src/usage-reset.mjs +17 -17
  87. package/dist/src/workstation-downloads.mjs +120 -120
  88. package/package.json +4 -4
  89. package/tools/postinstall.mjs +186 -186
@@ -1,141 +1,141 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { recordDailyUsage } from './usage-daily.mjs';
3
-
4
- const epoch = value => new Date(`${value.replace(' ', 'T')}Z`).getTime();
5
-
6
- // Count confirmed intervals only; a lost connection never creates estimated time.
7
- export function usageMeter(pool) {
8
- async function action(connection, computerId, clientId, kind, key) {
9
- if (!connection.userId || !['message', 'turn'].includes(kind) || typeof key !== 'string' || !key || key.length > 128) return;
10
- const column = kind === 'message' ? 'message_count' : 'turn_count';
11
- const db = await pool.getConnection();
12
- try {
13
- await db.beginTransaction();
14
- const [users] = await db.execute("SELECT id FROM users WHERE id=? AND status='active' FOR UPDATE", [connection.userId]);
15
- if (!users.length) { await db.commit(); return false; }
16
- const [result] = await db.execute(`INSERT IGNORE INTO relay_usage_actions
17
- (computer_id,kind,action_key,user_id,client_id,occurred_at)
18
- VALUES (?,?,?,?,?,UTC_TIMESTAMP(3))`,
19
- [computerId, kind, key, connection.userId, clientId]);
20
- if (result.affectedRows === 1) {
21
- await db.execute(`INSERT INTO relay_usage_totals (user_id,${column},updated_at)
22
- VALUES (?,1,UTC_TIMESTAMP(3)) ON DUPLICATE KEY UPDATE ${column}=${column}+1,
23
- updated_at=VALUES(updated_at)`, [connection.userId]);
24
- await db.execute(`INSERT INTO relay_usage_computer_totals (computer_id,${column},updated_at)
25
- VALUES (?,1,UTC_TIMESTAMP(3)) ON DUPLICATE KEY UPDATE ${column}=${column}+1,
26
- updated_at=VALUES(updated_at)`, [computerId]);
27
- }
28
- await db.commit();
29
- return result.affectedRows === 1;
30
- } catch (error) { await db.rollback(); throw error; }
31
- finally { db.release(); }
32
- }
33
- async function turnOrigin(computerId, key) {
34
- const [rows] = await pool.execute(`SELECT a.user_id,a.client_id FROM relay_usage_actions a JOIN users u ON u.id=a.user_id
35
- WHERE a.computer_id=? AND a.kind='turn' AND a.action_key=? AND u.status='active'`, [computerId, key]);
36
- return rows[0] ? { userId: rows[0].user_id, clientId: rows[0].client_id } : null;
37
- }
38
- async function pulse(connection, active, computerId, clientId, externalKey = null) {
39
- if (!connection.userId) return;
40
- if (!active && !connection.usageSessionId) return;
41
- if (active && !connection.usageSessionId) {
42
- const id = randomUUID();
43
- const [inserted] = await pool.execute(`INSERT INTO relay_usage_sessions
44
- (id,kind,external_key,user_id,computer_id,client_id,started_at,confirmed_until,charged_seconds)
45
- SELECT ?,'task',?,?,?,?,UTC_TIMESTAMP(3),UTC_TIMESTAMP(3),0 FROM users WHERE id=? AND status='active'`, [id, externalKey, connection.userId, computerId, clientId, connection.userId]);
46
- if (!inserted.affectedRows) return;
47
- connection.usageSessionId = id;
48
- return;
49
- }
50
- const id = connection.usageSessionId;
51
- const db = await pool.getConnection();
52
- try {
53
- await db.beginTransaction();
54
- const [users] = await db.execute("SELECT id FROM users WHERE id=? AND status='active' FOR UPDATE", [connection.userId]);
55
- if (!users.length) { await db.commit(); connection.usageSessionId = undefined; return; }
56
- const [rows] = await db.execute('SELECT user_id,computer_id,confirmed_until,ended_at FROM relay_usage_sessions WHERE id=? FOR UPDATE', [id]);
57
- const row = rows[0];
58
- if (!row || row.ended_at) throw new Error('Invalid relay usage session');
59
- const [clock] = await db.query('SELECT UTC_TIMESTAMP(3) AS now');
60
- const now = clock[0].now;
61
- const nowMs = epoch(now);
62
- const chargeStart = Math.max(epoch(row.confirmed_until), nowMs - 20000);
63
- const seconds = Math.max(0, nowMs - chargeStart) / 1000;
64
- const [plans] = await db.execute('SELECT tier,valid_until FROM relay_usage_entitlements WHERE user_id=?', [row.user_id]);
65
- const subscribed = plans[0]?.tier === 'subscriber' && plans[0].valid_until && epoch(plans[0].valid_until) > epoch(now);
66
- const freeSeconds = subscribed ? 0 : seconds;
67
- await db.execute(`UPDATE relay_usage_sessions SET confirmed_until=?,ended_at=?,charged_seconds=charged_seconds+?,
68
- free_charged_seconds=free_charged_seconds+? WHERE id=?`, [now, active ? null : now, seconds, freeSeconds, id]);
69
- if (seconds > 0) {
70
- await recordDailyUsage(db, row.user_id, row.computer_id, !subscribed,
71
- chargeStart, nowMs, now);
72
- await db.execute(`INSERT INTO relay_usage_totals (user_id,task_seconds,updated_at)
73
- VALUES (?,?,?) ON DUPLICATE KEY UPDATE task_seconds=task_seconds+VALUES(task_seconds),
74
- updated_at=VALUES(updated_at)`, [row.user_id, seconds, now]);
75
- await db.execute(`INSERT INTO relay_usage_computer_totals (computer_id,task_seconds,free_task_seconds,updated_at)
76
- VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE task_seconds=task_seconds+VALUES(task_seconds),
77
- free_task_seconds=free_task_seconds+VALUES(free_task_seconds),updated_at=VALUES(updated_at)`,
78
- [row.computer_id, seconds, freeSeconds, now]);
79
- }
80
- await db.commit();
81
- if (!active) connection.usageSessionId = undefined;
82
- } catch (error) { await db.rollback(); throw error; }
83
- finally { db.release(); }
84
- }
85
-
86
- async function lost(connection) {
87
- const id = connection.usageSessionId;
88
- connection.usageSessionId = undefined;
89
- if (!id) return;
90
- await pool.execute(`UPDATE relay_usage_sessions SET ended_at=confirmed_until
91
- WHERE id=? AND ended_at IS NULL`, [id]);
92
- }
93
-
94
- return { action, turnOrigin, pulse, lost };
95
- }
96
-
97
- export async function usageReport(pool, page, view = 'user') {
98
- if (view === 'computer') {
99
- const offset = (page - 1) * 30;
100
- const [counts] = await pool.query(`SELECT COUNT(*) AS total FROM relay_usage_computer_days
101
- WHERE usage_date=DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) AND task_seconds>0`);
102
- const [rows] = await pool.execute(`SELECT c.id,
103
- d.task_seconds AS today_task_seconds,
104
- d.free_task_seconds AS today_free_task_seconds,
105
- COALESCE(a.message_count,0) AS message_count,
106
- COALESCE(a.turn_count,0) AS turn_count,
107
- COALESCE(GREATEST(s.last_used_at,a.last_used_at),s.last_used_at,a.last_used_at) AS last_used_at
108
- FROM relay_usage_computer_days d JOIN computers c ON c.id=d.computer_id
109
- LEFT JOIN (SELECT computer_id,SUM(kind='message') AS message_count,
110
- SUM(kind='turn') AS turn_count,MAX(occurred_at) AS last_used_at FROM relay_usage_actions
111
- WHERE occurred_at >= DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) - INTERVAL 8 HOUR
112
- AND occurred_at < DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) + INTERVAL 16 HOUR
113
- GROUP BY computer_id) a ON a.computer_id=c.id
114
- LEFT JOIN (SELECT computer_id,MAX(confirmed_until) AS last_used_at
115
- FROM relay_usage_sessions GROUP BY computer_id) s ON s.computer_id=c.id
116
- WHERE d.usage_date=DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) AND d.task_seconds>0
117
- ORDER BY d.task_seconds DESC,c.id LIMIT ? OFFSET ?`, [30, offset]);
118
- return { page, pageSize: 30, total: counts[0].total, rows };
119
- }
120
- const offset = (page - 1) * 30;
121
- const [counts] = await pool.query(`SELECT COUNT(*) AS total FROM relay_usage_user_days d
122
- JOIN user_login_identities i ON i.user_id=d.user_id AND i.kind='email'
123
- WHERE d.usage_date=DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) AND d.task_seconds>0`);
124
- const [rows] = await pool.execute(`SELECT u.id,i.normalized_value AS email,
125
- d.task_seconds AS today_task_seconds,
126
- COALESCE(a.message_count,0) AS message_count,
127
- COALESCE(a.turn_count,0) AS turn_count,
128
- COALESCE(GREATEST(s.last_used_at,a.last_used_at),s.last_used_at,a.last_used_at) AS last_used_at
129
- FROM relay_usage_user_days d JOIN users u ON u.id=d.user_id
130
- JOIN user_login_identities i ON i.user_id=u.id AND i.kind='email'
131
- LEFT JOIN (SELECT user_id,SUM(kind='message') AS message_count,
132
- SUM(kind='turn') AS turn_count,MAX(occurred_at) AS last_used_at FROM relay_usage_actions
133
- WHERE occurred_at >= DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) - INTERVAL 8 HOUR
134
- AND occurred_at < DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) + INTERVAL 16 HOUR
135
- GROUP BY user_id) a ON a.user_id=u.id
136
- LEFT JOIN (SELECT user_id,MAX(confirmed_until) AS last_used_at
137
- FROM relay_usage_sessions GROUP BY user_id) s ON s.user_id=u.id
138
- WHERE d.usage_date=DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) AND d.task_seconds>0
139
- ORDER BY d.task_seconds DESC,u.id LIMIT ? OFFSET ?`, [30, offset]);
140
- return { page, pageSize: 30, total: counts[0].total, rows };
141
- }
1
+ import { randomUUID } from 'node:crypto';
2
+ import { recordDailyUsage } from './usage-daily.mjs';
3
+
4
+ const epoch = value => new Date(`${value.replace(' ', 'T')}Z`).getTime();
5
+
6
+ // Count confirmed intervals only; a lost connection never creates estimated time.
7
+ export function usageMeter(pool) {
8
+ async function action(connection, computerId, clientId, kind, key) {
9
+ if (!connection.userId || !['message', 'turn'].includes(kind) || typeof key !== 'string' || !key || key.length > 128) return;
10
+ const column = kind === 'message' ? 'message_count' : 'turn_count';
11
+ const db = await pool.getConnection();
12
+ try {
13
+ await db.beginTransaction();
14
+ const [users] = await db.execute("SELECT id FROM users WHERE id=? AND status='active' FOR UPDATE", [connection.userId]);
15
+ if (!users.length) { await db.commit(); return false; }
16
+ const [result] = await db.execute(`INSERT IGNORE INTO relay_usage_actions
17
+ (computer_id,kind,action_key,user_id,client_id,occurred_at)
18
+ VALUES (?,?,?,?,?,UTC_TIMESTAMP(3))`,
19
+ [computerId, kind, key, connection.userId, clientId]);
20
+ if (result.affectedRows === 1) {
21
+ await db.execute(`INSERT INTO relay_usage_totals (user_id,${column},updated_at)
22
+ VALUES (?,1,UTC_TIMESTAMP(3)) ON DUPLICATE KEY UPDATE ${column}=${column}+1,
23
+ updated_at=VALUES(updated_at)`, [connection.userId]);
24
+ await db.execute(`INSERT INTO relay_usage_computer_totals (computer_id,${column},updated_at)
25
+ VALUES (?,1,UTC_TIMESTAMP(3)) ON DUPLICATE KEY UPDATE ${column}=${column}+1,
26
+ updated_at=VALUES(updated_at)`, [computerId]);
27
+ }
28
+ await db.commit();
29
+ return result.affectedRows === 1;
30
+ } catch (error) { await db.rollback(); throw error; }
31
+ finally { db.release(); }
32
+ }
33
+ async function turnOrigin(computerId, key) {
34
+ const [rows] = await pool.execute(`SELECT a.user_id,a.client_id FROM relay_usage_actions a JOIN users u ON u.id=a.user_id
35
+ WHERE a.computer_id=? AND a.kind='turn' AND a.action_key=? AND u.status='active'`, [computerId, key]);
36
+ return rows[0] ? { userId: rows[0].user_id, clientId: rows[0].client_id } : null;
37
+ }
38
+ async function pulse(connection, active, computerId, clientId, externalKey = null) {
39
+ if (!connection.userId) return;
40
+ if (!active && !connection.usageSessionId) return;
41
+ if (active && !connection.usageSessionId) {
42
+ const id = randomUUID();
43
+ const [inserted] = await pool.execute(`INSERT INTO relay_usage_sessions
44
+ (id,kind,external_key,user_id,computer_id,client_id,started_at,confirmed_until,charged_seconds)
45
+ SELECT ?,'task',?,?,?,?,UTC_TIMESTAMP(3),UTC_TIMESTAMP(3),0 FROM users WHERE id=? AND status='active'`, [id, externalKey, connection.userId, computerId, clientId, connection.userId]);
46
+ if (!inserted.affectedRows) return;
47
+ connection.usageSessionId = id;
48
+ return;
49
+ }
50
+ const id = connection.usageSessionId;
51
+ const db = await pool.getConnection();
52
+ try {
53
+ await db.beginTransaction();
54
+ const [users] = await db.execute("SELECT id FROM users WHERE id=? AND status='active' FOR UPDATE", [connection.userId]);
55
+ if (!users.length) { await db.commit(); connection.usageSessionId = undefined; return; }
56
+ const [rows] = await db.execute('SELECT user_id,computer_id,confirmed_until,ended_at FROM relay_usage_sessions WHERE id=? FOR UPDATE', [id]);
57
+ const row = rows[0];
58
+ if (!row || row.ended_at) throw new Error('Invalid relay usage session');
59
+ const [clock] = await db.query('SELECT UTC_TIMESTAMP(3) AS now');
60
+ const now = clock[0].now;
61
+ const nowMs = epoch(now);
62
+ const chargeStart = Math.max(epoch(row.confirmed_until), nowMs - 20000);
63
+ const seconds = Math.max(0, nowMs - chargeStart) / 1000;
64
+ const [plans] = await db.execute('SELECT tier,valid_until FROM relay_usage_entitlements WHERE user_id=?', [row.user_id]);
65
+ const subscribed = plans[0]?.tier === 'subscriber' && plans[0].valid_until && epoch(plans[0].valid_until) > epoch(now);
66
+ const freeSeconds = subscribed ? 0 : seconds;
67
+ await db.execute(`UPDATE relay_usage_sessions SET confirmed_until=?,ended_at=?,charged_seconds=charged_seconds+?,
68
+ free_charged_seconds=free_charged_seconds+? WHERE id=?`, [now, active ? null : now, seconds, freeSeconds, id]);
69
+ if (seconds > 0) {
70
+ await recordDailyUsage(db, row.user_id, row.computer_id, !subscribed,
71
+ chargeStart, nowMs, now);
72
+ await db.execute(`INSERT INTO relay_usage_totals (user_id,task_seconds,updated_at)
73
+ VALUES (?,?,?) ON DUPLICATE KEY UPDATE task_seconds=task_seconds+VALUES(task_seconds),
74
+ updated_at=VALUES(updated_at)`, [row.user_id, seconds, now]);
75
+ await db.execute(`INSERT INTO relay_usage_computer_totals (computer_id,task_seconds,free_task_seconds,updated_at)
76
+ VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE task_seconds=task_seconds+VALUES(task_seconds),
77
+ free_task_seconds=free_task_seconds+VALUES(free_task_seconds),updated_at=VALUES(updated_at)`,
78
+ [row.computer_id, seconds, freeSeconds, now]);
79
+ }
80
+ await db.commit();
81
+ if (!active) connection.usageSessionId = undefined;
82
+ } catch (error) { await db.rollback(); throw error; }
83
+ finally { db.release(); }
84
+ }
85
+
86
+ async function lost(connection) {
87
+ const id = connection.usageSessionId;
88
+ connection.usageSessionId = undefined;
89
+ if (!id) return;
90
+ await pool.execute(`UPDATE relay_usage_sessions SET ended_at=confirmed_until
91
+ WHERE id=? AND ended_at IS NULL`, [id]);
92
+ }
93
+
94
+ return { action, turnOrigin, pulse, lost };
95
+ }
96
+
97
+ export async function usageReport(pool, page, view = 'user') {
98
+ if (view === 'computer') {
99
+ const offset = (page - 1) * 30;
100
+ const [counts] = await pool.query(`SELECT COUNT(*) AS total FROM relay_usage_computer_days
101
+ WHERE usage_date=DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) AND task_seconds>0`);
102
+ const [rows] = await pool.execute(`SELECT c.id,
103
+ d.task_seconds AS today_task_seconds,
104
+ d.free_task_seconds AS today_free_task_seconds,
105
+ COALESCE(a.message_count,0) AS message_count,
106
+ COALESCE(a.turn_count,0) AS turn_count,
107
+ COALESCE(GREATEST(s.last_used_at,a.last_used_at),s.last_used_at,a.last_used_at) AS last_used_at
108
+ FROM relay_usage_computer_days d JOIN computers c ON c.id=d.computer_id
109
+ LEFT JOIN (SELECT computer_id,SUM(kind='message') AS message_count,
110
+ SUM(kind='turn') AS turn_count,MAX(occurred_at) AS last_used_at FROM relay_usage_actions
111
+ WHERE occurred_at >= DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) - INTERVAL 8 HOUR
112
+ AND occurred_at < DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) + INTERVAL 16 HOUR
113
+ GROUP BY computer_id) a ON a.computer_id=c.id
114
+ LEFT JOIN (SELECT computer_id,MAX(confirmed_until) AS last_used_at
115
+ FROM relay_usage_sessions GROUP BY computer_id) s ON s.computer_id=c.id
116
+ WHERE d.usage_date=DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) AND d.task_seconds>0
117
+ ORDER BY d.task_seconds DESC,c.id LIMIT ? OFFSET ?`, [30, offset]);
118
+ return { page, pageSize: 30, total: counts[0].total, rows };
119
+ }
120
+ const offset = (page - 1) * 30;
121
+ const [counts] = await pool.query(`SELECT COUNT(*) AS total FROM relay_usage_user_days d
122
+ JOIN user_login_identities i ON i.user_id=d.user_id AND i.kind='email'
123
+ WHERE d.usage_date=DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) AND d.task_seconds>0`);
124
+ const [rows] = await pool.execute(`SELECT u.id,i.normalized_value AS email,
125
+ d.task_seconds AS today_task_seconds,
126
+ COALESCE(a.message_count,0) AS message_count,
127
+ COALESCE(a.turn_count,0) AS turn_count,
128
+ COALESCE(GREATEST(s.last_used_at,a.last_used_at),s.last_used_at,a.last_used_at) AS last_used_at
129
+ FROM relay_usage_user_days d JOIN users u ON u.id=d.user_id
130
+ JOIN user_login_identities i ON i.user_id=u.id AND i.kind='email'
131
+ LEFT JOIN (SELECT user_id,SUM(kind='message') AS message_count,
132
+ SUM(kind='turn') AS turn_count,MAX(occurred_at) AS last_used_at FROM relay_usage_actions
133
+ WHERE occurred_at >= DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) - INTERVAL 8 HOUR
134
+ AND occurred_at < DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) + INTERVAL 16 HOUR
135
+ GROUP BY user_id) a ON a.user_id=u.id
136
+ LEFT JOIN (SELECT user_id,MAX(confirmed_until) AS last_used_at
137
+ FROM relay_usage_sessions GROUP BY user_id) s ON s.user_id=u.id
138
+ WHERE d.usage_date=DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) AND d.task_seconds>0
139
+ ORDER BY d.task_seconds DESC,u.id LIMIT ? OFFSET ?`, [30, offset]);
140
+ return { page, pageSize: 30, total: counts[0].total, rows };
141
+ }
@@ -92,6 +92,7 @@ function readRuntimeVersion() {
92
92
 
93
93
  const options = parseOptions(process.argv.slice(2));
94
94
  const runtimeVersion = readRuntimeVersion();
95
+ const serviceInstanceId = crypto.randomUUID();
95
96
 
96
97
  const webRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'web');
97
98
  const projectRoot = os.homedir();
@@ -266,8 +267,8 @@ async function handleRequest(request, response) {
266
267
  const body = await readJsonBody(request);
267
268
  if (body.confirmed !== true) throw new RequestError(400, '请确认启动桌面端。');
268
269
  const result = await startDesktop({ stateRoot, host: desktopHost, restart: url.pathname === '/api/desktop/restart' });
269
- await runtimeDiscovery.sample();
270
- sendJson(response, 202, result); return;
270
+ const observed = await runtimeDiscovery.sample();
271
+ sendJson(response, 202, { ...result, desktop: observed.desktop, checkedAt: observed.checkedAt }); return;
271
272
  }
272
273
  if (request.method === 'POST' && url.pathname === '/api/shared-recovery') {
273
274
  const body = await readJsonBody(request);
@@ -283,7 +284,7 @@ async function handleRequest(request, response) {
283
284
  if (request.method === 'POST' && url.pathname === '/api/service/restart') {
284
285
  const body = await readJsonBody(request);
285
286
  if (body.confirmed !== true) throw new RequestError(400, '请确认重启工作站服务后再继续。');
286
- sendJson(response, 202, await requestServiceRestart(stateRoot)); return;
287
+ sendJson(response, 202, { ...await requestServiceRestart(stateRoot), serviceInstanceId }); return;
287
288
  }
288
289
  if (request.method === 'POST' && url.pathname === '/api/managed-app-server/restart') {
289
290
  const body = await readJsonBody(request);
@@ -322,7 +323,7 @@ async function handleRequest(request, response) {
322
323
  // must not trigger PowerShell, process-identity, or app-server checks.
323
324
  const executionReady = executionHealth.state.ready && sharedServer?.ready === true;
324
325
  sendJson(response, 200, {
325
- status: 'ok', resident: true, version: runtimeVersion,
326
+ status: 'ok', resident: true, version: runtimeVersion, serviceInstanceId,
326
327
  keepAwake: powerManager.snapshot(),
327
328
  serviceUpdate: serviceUpdateStatus(stateRoot),
328
329
  execution: { ...executionHealth.state, ready: executionReady, canWrite: executionReady,