@walkhi/code-relax 0.1.0-beta.50 → 0.1.0-beta.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.
Files changed (101) 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/bin/set-relay-admin-password.mjs +11 -0
  7. package/dist/build-platform.json +1 -1
  8. package/dist/get-shared-service-url.ps1 +14 -14
  9. package/dist/shared/code-browser.cjs +159 -159
  10. package/dist/shared/file-changes.cjs +31 -31
  11. package/dist/shared/file-citations.cjs +117 -117
  12. package/dist/shared/file-mentions.cjs +92 -92
  13. package/dist/shared/message-delivery.cjs +115 -115
  14. package/dist/shared/p2p-file-channel.cjs +87 -87
  15. package/dist/shared/release-version.cjs +31 -0
  16. package/dist/shared/server-requests.cjs +311 -311
  17. package/dist/shared/timeline-sync.cjs +173 -173
  18. package/dist/skills/code-relax/SKILL.md +22 -22
  19. package/dist/src/bounded-process-log.mjs +63 -63
  20. package/dist/src/celsius-client.mjs +107 -107
  21. package/dist/src/dots-approval-timeline.mjs +99 -99
  22. package/dist/src/dots-avatar.mjs +42 -42
  23. package/dist/src/dots-chat-projection.mjs +50 -50
  24. package/dist/src/dots-chat-server.mjs +483 -483
  25. package/dist/src/dots-message-images.mjs +319 -319
  26. package/dist/src/dots-profile.mjs +77 -77
  27. package/dist/src/dots-room-activity.mjs +172 -172
  28. package/dist/src/dots-room-client.mjs +137 -135
  29. package/dist/src/dots-room-projection.mjs +94 -94
  30. package/dist/src/dots-schedules.mjs +66 -66
  31. package/dist/src/dots-summary-tasks.mjs +32 -32
  32. package/dist/src/dots-task-catalog.mjs +430 -430
  33. package/dist/src/file-transfer-range.mjs +22 -22
  34. package/dist/src/file-upload-session.mjs +146 -130
  35. package/dist/src/hosted-history-app-server.mjs +103 -103
  36. package/dist/src/hosted-history-cache.mjs +113 -113
  37. package/dist/src/lan-socket-server.mjs +338 -266
  38. package/dist/src/managed-app-server.mjs +373 -373
  39. package/dist/src/managed-endpoint.mjs +25 -25
  40. package/dist/src/message-upload-files.mjs +109 -109
  41. package/dist/src/onboarding.mjs +12 -12
  42. package/dist/src/platform/README.md +1 -1
  43. package/dist/src/platform/codex-protocol.mjs +46 -46
  44. package/dist/src/platform/desktop-project-store.mjs +219 -208
  45. package/dist/src/platform/linux/desktop-host.mjs +87 -87
  46. package/dist/src/platform/linux/process.mjs +53 -53
  47. package/dist/src/platform/linux/system.mjs +50 -50
  48. package/dist/src/platform/linux/user-environment.mjs +9 -9
  49. package/dist/src/platform/macos/codex-runtime.mjs +125 -125
  50. package/dist/src/platform/macos/desktop-host.mjs +86 -86
  51. package/dist/src/platform/macos/user-environment.mjs +9 -9
  52. package/dist/src/platform/native-package.mjs +58 -58
  53. package/dist/src/platform/windows/activate-desktop.ps1 +49 -49
  54. package/dist/src/platform/windows/codex-runtime.mjs +215 -215
  55. package/dist/src/platform/windows/credential-store.mjs +27 -27
  56. package/dist/src/platform/windows/credential-store.ps1 +49 -49
  57. package/dist/src/platform/windows/desktop-host.mjs +65 -65
  58. package/dist/src/platform/windows/desktop-info.ps1 +38 -38
  59. package/dist/src/platform/windows/desktop-project-registration.mjs +4 -4
  60. package/dist/src/platform/windows/desktop-shortcuts.ps1 +14 -14
  61. package/dist/src/platform/windows/dots-profile.mjs +49 -49
  62. package/dist/src/platform/windows/launch-worker.mjs +41 -41
  63. package/dist/src/platform/windows/project-locations.mjs +16 -16
  64. package/dist/src/platform/windows/service-host.mjs +35 -35
  65. package/dist/src/platform/windows/user-environment.mjs +52 -52
  66. package/dist/src/project-management.mjs +58 -58
  67. package/dist/src/runtime-discovery.mjs +47 -47
  68. package/dist/src/self-relay/account-cleanup.mjs +30 -30
  69. package/dist/src/self-relay/admin-auth.mjs +59 -0
  70. package/dist/src/self-relay/admin-login-page.mjs +7 -0
  71. package/dist/src/self-relay/admin-page.mjs +87 -80
  72. package/dist/src/self-relay/admin-server.mjs +49 -18
  73. package/dist/src/self-relay/admin-style.mjs +58 -14
  74. package/dist/src/self-relay/admin-workstations-page.mjs +95 -0
  75. package/dist/src/self-relay/admin-workstations.mjs +143 -0
  76. package/dist/src/self-relay/connection-reporting.mjs +38 -0
  77. package/dist/src/self-relay/connector.mjs +444 -439
  78. package/dist/src/self-relay/demo.mjs +31 -31
  79. package/dist/src/self-relay/diagnostics.mjs +54 -54
  80. package/dist/src/self-relay/email-accounts.mjs +183 -183
  81. package/dist/src/self-relay/file-wire.mjs +13 -13
  82. package/dist/src/self-relay/lifecycle.mjs +11 -0
  83. package/dist/src/self-relay/p2p-probe.mjs +168 -168
  84. package/dist/src/self-relay/phone-connections.mjs +55 -0
  85. package/dist/src/self-relay/privacy-page.mjs +26 -26
  86. package/dist/src/self-relay/public-releases.mjs +67 -67
  87. package/dist/src/self-relay/rtc-file-bridge.mjs +46 -41
  88. package/dist/src/self-relay/server.mjs +63 -3
  89. package/dist/src/self-relay/usage-meter.mjs +141 -141
  90. package/dist/src/server.mjs +23 -9
  91. package/dist/src/service-doctor.mjs +148 -148
  92. package/dist/src/service-install.mjs +97 -97
  93. package/dist/src/service-lifecycle.mjs +139 -139
  94. package/dist/src/service-update.mjs +6 -3
  95. package/dist/src/shared-app-server.mjs +683 -666
  96. package/dist/src/shared-catalog.mjs +132 -132
  97. package/dist/src/shared-thread-stream.mjs +165 -165
  98. package/dist/src/usage-reset.mjs +17 -17
  99. package/dist/src/workstation-downloads.mjs +120 -120
  100. package/package.json +4 -4
  101. package/tools/postinstall.mjs +186 -186
@@ -1,107 +1,107 @@
1
- import { EventEmitter } from 'node:events';
2
- import WebSocket from 'ws';
3
-
4
- // Shared Celsius framing and acknowledged topic lifecycle; callers own identity and HTTP.
5
- export class CelsiusClient extends EventEmitter {
6
- constructor({ getConnection, topics = [] }) {
7
- super(); this.getConnection = getConnection; this.initialTopics = topics;
8
- this.generation = 0; this.topics = new Map(); this.commands = new Map();
9
- this.nextId = 0; this.topicLoads = new Map();
10
- }
11
- async connect() {
12
- if (this.ready) return;
13
- if (this.connecting) return this.connecting;
14
- const generation = this.generation;
15
- const work = (async () => {
16
- const value = await this.getConnection();
17
- if (generation !== this.generation) throw new Error('消息连接已变化。');
18
- const url = new URL(value.websocket_url);
19
- if (url.protocol !== 'wss:' || url.username || url.password) throw new Error('消息推送地址无效。');
20
- await new Promise((resolve, reject) => {
21
- const socket = new WebSocket(url, { handshakeTimeout: 15000, maxPayload: 8 * 1024 * 1024 });
22
- this.socket = socket;
23
- const timer = setTimeout(() => { reject(new Error('消息推送订阅超时。')); socket.terminate(); }, 30000);
24
- const current = () => this.socket === socket && generation === this.generation;
25
- socket.on('error', () => {}); // Do not expose the signed URL in library errors.
26
- socket.on('open', async () => {
27
- try {
28
- await this.command({ type: 'connect', presence: { type: 'presence', state: 'background' } });
29
- for (const topic of this.initialTopics) await this.subscribe(topic);
30
- if (!current()) throw new Error('消息连接已变化。');
31
- this.ready = true; clearTimeout(timer); resolve(); this.emit('connected');
32
- } catch (error) { clearTimeout(timer); reject(error); socket.terminate(); }
33
- });
34
- socket.on('message', data => {
35
- if (!current()) return;
36
- let frames; try { frames = JSON.parse(data.toString()); } catch { return; }
37
- if (!Array.isArray(frames)) return;
38
- for (const frame of frames) {
39
- if (frame.type === 'message') this.receive(frame);
40
- else if (frame.id != null) this.commands.get(frame.id)?.resolve(frame.reply);
41
- }
42
- });
43
- socket.on('close', code => {
44
- clearTimeout(timer); reject(new Error('消息推送连接已断开。'));
45
- if (!current()) return;
46
- this.ready = false; this.socket = null;
47
- for (const entry of this.commands.values()) entry.reject(new Error('消息推送连接已断开。'));
48
- this.commands.clear(); this.topics.clear(); this.topicLoads.clear();
49
- this.emit('disconnected', { source: 'celsius', code });
50
- });
51
- });
52
- })();
53
- this.connecting = work;
54
- try { await work; } finally { if (this.connecting === work) this.connecting = null; }
55
- }
56
-
57
- command(command) {
58
- if (this.socket?.readyState !== WebSocket.OPEN) return Promise.reject(new Error('消息推送尚未连接。'));
59
- const id = ++this.nextId;
60
- const socket = this.socket;
61
- return new Promise((resolve, reject) => {
62
- const finish = (error, value) => {
63
- if (!this.commands.has(id)) return;
64
- clearTimeout(timer); this.commands.delete(id);
65
- if (error) reject(error); else resolve(value);
66
- };
67
- const timer = setTimeout(() => { finish(new Error('消息推送请求超时。')); socket.terminate(); }, 30000);
68
- this.commands.set(id, { resolve: value => finish(null, value), reject: error => finish(error) });
69
- socket.send(JSON.stringify([{ id, command }]), error => { if (error) finish(new Error('消息推送连接已断开。')); });
70
- });
71
- }
72
-
73
- async subscribe(name, { offset, requireRecovery = false } = {}) {
74
- if (this.topics.has(name)) return;
75
- if (this.topicLoads.has(name)) return this.topicLoads.get(name);
76
- const generation = this.generation, socket = this.socket;
77
- // Every transport reconnection invalidates chat snapshots and reads a new
78
- // latest window. An old offset never certifies the new snapshot by itself.
79
- const work = this.command({ type: 'subscribe', topic_id: name, ...(offset == null ? {} : { offset }) }).then(reply => {
80
- if (socket !== this.socket || generation !== this.generation) throw new Error('消息连接已变化。');
81
- if (reply?.type !== 'subscribe' || reply.topic_id !== name) throw new Error('消息推送订阅未确认。');
82
- if (requireRecovery && !reply.recovered) throw new Error('ChatGPT 输出历史未完整接续,请核对原消息。');
83
- this.topics.set(name, reply.last_offset || null);
84
- if (reply.recovered) for (const message of reply.catchups || []) this.receive(message);
85
- });
86
- this.topicLoads.set(name, work);
87
- try { await work; } finally { if (this.topicLoads.get(name) === work) this.topicLoads.delete(name); }
88
- }
89
-
90
- async unsubscribe(name) {
91
- if (!this.topics.delete(name) || this.socket?.readyState !== WebSocket.OPEN) return;
92
- await this.command({ type: 'unsubscribe', topic_id: name });
93
- }
94
-
95
- receive(frame) {
96
- if (frame.offset) this.topics.set(frame.topic_id, frame.offset);
97
- this.emit('message', frame.topic_id, frame.payload);
98
- }
99
-
100
-
101
- close() {
102
- this.generation++; this.ready = false; this.connecting = null;
103
- const socket = this.socket; this.socket = null; socket?.terminate();
104
- for (const entry of this.commands.values()) entry.reject(new Error('消息推送连接已结束。'));
105
- this.commands.clear(); this.topics.clear(); this.topicLoads.clear();
106
- }
107
- }
1
+ import { EventEmitter } from 'node:events';
2
+ import WebSocket from 'ws';
3
+
4
+ // Shared Celsius framing and acknowledged topic lifecycle; callers own identity and HTTP.
5
+ export class CelsiusClient extends EventEmitter {
6
+ constructor({ getConnection, topics = [] }) {
7
+ super(); this.getConnection = getConnection; this.initialTopics = topics;
8
+ this.generation = 0; this.topics = new Map(); this.commands = new Map();
9
+ this.nextId = 0; this.topicLoads = new Map();
10
+ }
11
+ async connect() {
12
+ if (this.ready) return;
13
+ if (this.connecting) return this.connecting;
14
+ const generation = this.generation;
15
+ const work = (async () => {
16
+ const value = await this.getConnection();
17
+ if (generation !== this.generation) throw new Error('消息连接已变化。');
18
+ const url = new URL(value.websocket_url);
19
+ if (url.protocol !== 'wss:' || url.username || url.password) throw new Error('消息推送地址无效。');
20
+ await new Promise((resolve, reject) => {
21
+ const socket = new WebSocket(url, { handshakeTimeout: 15000, maxPayload: 8 * 1024 * 1024 });
22
+ this.socket = socket;
23
+ const timer = setTimeout(() => { reject(new Error('消息推送订阅超时。')); socket.terminate(); }, 30000);
24
+ const current = () => this.socket === socket && generation === this.generation;
25
+ socket.on('error', () => {}); // Do not expose the signed URL in library errors.
26
+ socket.on('open', async () => {
27
+ try {
28
+ await this.command({ type: 'connect', presence: { type: 'presence', state: 'background' } });
29
+ for (const topic of this.initialTopics) await this.subscribe(topic);
30
+ if (!current()) throw new Error('消息连接已变化。');
31
+ this.ready = true; clearTimeout(timer); resolve(); this.emit('connected');
32
+ } catch (error) { clearTimeout(timer); reject(error); socket.terminate(); }
33
+ });
34
+ socket.on('message', data => {
35
+ if (!current()) return;
36
+ let frames; try { frames = JSON.parse(data.toString()); } catch { return; }
37
+ if (!Array.isArray(frames)) return;
38
+ for (const frame of frames) {
39
+ if (frame.type === 'message') this.receive(frame);
40
+ else if (frame.id != null) this.commands.get(frame.id)?.resolve(frame.reply);
41
+ }
42
+ });
43
+ socket.on('close', code => {
44
+ clearTimeout(timer); reject(new Error('消息推送连接已断开。'));
45
+ if (!current()) return;
46
+ this.ready = false; this.socket = null;
47
+ for (const entry of this.commands.values()) entry.reject(new Error('消息推送连接已断开。'));
48
+ this.commands.clear(); this.topics.clear(); this.topicLoads.clear();
49
+ this.emit('disconnected', { source: 'celsius', code });
50
+ });
51
+ });
52
+ })();
53
+ this.connecting = work;
54
+ try { await work; } finally { if (this.connecting === work) this.connecting = null; }
55
+ }
56
+
57
+ command(command) {
58
+ if (this.socket?.readyState !== WebSocket.OPEN) return Promise.reject(new Error('消息推送尚未连接。'));
59
+ const id = ++this.nextId;
60
+ const socket = this.socket;
61
+ return new Promise((resolve, reject) => {
62
+ const finish = (error, value) => {
63
+ if (!this.commands.has(id)) return;
64
+ clearTimeout(timer); this.commands.delete(id);
65
+ if (error) reject(error); else resolve(value);
66
+ };
67
+ const timer = setTimeout(() => { finish(new Error('消息推送请求超时。')); socket.terminate(); }, 30000);
68
+ this.commands.set(id, { resolve: value => finish(null, value), reject: error => finish(error) });
69
+ socket.send(JSON.stringify([{ id, command }]), error => { if (error) finish(new Error('消息推送连接已断开。')); });
70
+ });
71
+ }
72
+
73
+ async subscribe(name, { offset, requireRecovery = false } = {}) {
74
+ if (this.topics.has(name)) return;
75
+ if (this.topicLoads.has(name)) return this.topicLoads.get(name);
76
+ const generation = this.generation, socket = this.socket;
77
+ // Every transport reconnection invalidates chat snapshots and reads a new
78
+ // latest window. An old offset never certifies the new snapshot by itself.
79
+ const work = this.command({ type: 'subscribe', topic_id: name, ...(offset == null ? {} : { offset }) }).then(reply => {
80
+ if (socket !== this.socket || generation !== this.generation) throw new Error('消息连接已变化。');
81
+ if (reply?.type !== 'subscribe' || reply.topic_id !== name) throw new Error('消息推送订阅未确认。');
82
+ if (requireRecovery && !reply.recovered) throw new Error('ChatGPT 输出历史未完整接续,请核对原消息。');
83
+ this.topics.set(name, reply.last_offset || null);
84
+ if (reply.recovered) for (const message of reply.catchups || []) this.receive(message);
85
+ });
86
+ this.topicLoads.set(name, work);
87
+ try { await work; } finally { if (this.topicLoads.get(name) === work) this.topicLoads.delete(name); }
88
+ }
89
+
90
+ async unsubscribe(name) {
91
+ if (!this.topics.delete(name) || this.socket?.readyState !== WebSocket.OPEN) return;
92
+ await this.command({ type: 'unsubscribe', topic_id: name });
93
+ }
94
+
95
+ receive(frame) {
96
+ if (frame.offset) this.topics.set(frame.topic_id, frame.offset);
97
+ this.emit('message', frame.topic_id, frame.payload);
98
+ }
99
+
100
+
101
+ close() {
102
+ this.generation++; this.ready = false; this.connecting = null;
103
+ const socket = this.socket; this.socket = null; socket?.terminate();
104
+ for (const entry of this.commands.values()) entry.reject(new Error('消息推送连接已结束。'));
105
+ this.commands.clear(); this.topics.clear(); this.topicLoads.clear();
106
+ }
107
+ }
@@ -1,99 +1,99 @@
1
- const itemTime = item => item.dotsReceipt?.sentAt ? Date.parse(item.dotsReceipt.sentAt)
2
- : item.startedAtMs ?? item.receivedAtMs;
3
- const terminal = status => ['resolved', 'expired'].includes(status);
4
-
5
- // Approval records have their own official room source. Keep them with the
6
- // account-scoped hosted cache, then place them in the chat's observed positions.
7
- export class DotsApprovalTimeline {
8
- constructor(server) { this.server = server; this.states = new Map(); }
9
-
10
- async state(threadId) {
11
- const scope = this.server.historyScope(threadId);
12
- if (!scope) throw new Error('读取点点审批历史需要账号身份。');
13
- let state = this.states.get(scope);
14
- if (!state) {
15
- state = { scope, threadId, epoch: this.server.epoch, records: new Map() };
16
- state.ready = (async () => {
17
- const saved = await this.server.historyCache?.get(scope, 'approval-records');
18
- for (const record of saved?.data || []) {
19
- // A display record never reactivates an old pending request.
20
- state.records.set(this.key(record), terminal(record.status) ? record : { ...record, status: 'expired' });
21
- }
22
- })();
23
- this.states.set(scope, state);
24
- }
25
- await state.ready;
26
- this.check(threadId, state);
27
- return state;
28
- }
29
-
30
- check(threadId, state) {
31
- if (state.scope !== this.server.historyScope(threadId) || state.epoch !== this.server.epoch) throw new Error('点点审批历史的账号连接已变化。');
32
- }
33
-
34
- key(item) { return JSON.stringify([typeof item.requestId, item.requestId]); }
35
-
36
- merge(state, incoming) {
37
- let changed = false;
38
- for (const item of incoming) {
39
- const key = this.key(item), previous = state.records.get(key);
40
- const merged = { ...previous, ...item, id: previous?.id || item.id,
41
- receivedAtMs: previous?.receivedAtMs ?? item.receivedAtMs,
42
- ...(previous?.result && item.result == null ? { result: previous.result } : {}),
43
- ...(previous?.afterItemId ? { afterItemId: previous.afterItemId } : {}) };
44
- if (JSON.stringify(previous) !== JSON.stringify(merged)) { state.records.set(key, merged); changed = true; }
45
- }
46
- return changed;
47
- }
48
-
49
- save(state) {
50
- return this.server.historyCache?.put(state.scope, 'approval-records', { data: [...state.records.values()] });
51
- }
52
-
53
- targets(sourceThreadId) {
54
- return [...this.states.values()].filter(state => state.scope === this.server.historyScope(state.threadId)
55
- && [...state.records.values()].some(record => record.params.threadId === sourceThreadId)).map(state => state.threadId);
56
- }
57
-
58
- async observe(threadId, records, turns) {
59
- const state = await this.state(threadId);
60
- const incoming = records.map(record => {
61
- const turn = turns?.find(turn => turn.id === record.turnId);
62
- const afterItemId = turn?.items.filter(item => item.type !== 'approval').at(-1)?.id;
63
- return { ...record, ...(afterItemId ? { afterItemId } : {}) };
64
- });
65
- if (this.merge(state, incoming)) await this.save(state);
66
- this.check(threadId, state);
67
- }
68
-
69
- overlay(threadId, turns, range = {}) {
70
- const state = this.states.get(this.server.historyScope(threadId));
71
- if (!state) return turns;
72
- const ids = new Set([...state.records.values()].map(item => item.id));
73
- const result = turns.map(turn => ({ ...turn, items: turn.items.filter(item => item.type !== 'approval' || !ids.has(item.id)) }));
74
- for (const approval of [...state.records.values()].sort((a, b) => a.receivedAtMs - b.receivedAtMs)) {
75
- if (Number.isFinite(range.fromMs) && approval.receivedAtMs < range.fromMs
76
- || Number.isFinite(range.toMs) && approval.receivedAtMs > range.toMs) continue;
77
- let turn = result.find(turn => turn.id === approval.roomMessageId || turn.id === approval.turnId);
78
- if (!turn) {
79
- if (!Number.isFinite(range.fromMs)) continue;
80
- const positions = result.flatMap(turn => turn.items.map(item => ({ turn, item, time: itemTime(item) })))
81
- .filter(position => Number.isFinite(position.time));
82
- turn = positions.filter(position => position.time <= approval.receivedAtMs).at(-1)?.turn || positions[0]?.turn;
83
- }
84
- if (!turn) continue;
85
- const after = turn.items.findIndex(item => item.id === approval.afterItemId);
86
- if (after >= 0) {
87
- let index = after + 1;
88
- while (turn.items[index]?.type === 'approval' && turn.items[index].receivedAtMs <= approval.receivedAtMs) index++;
89
- turn.items.splice(index, 0, approval);
90
- } else {
91
- const next = turn.items.findIndex(item => Number.isFinite(itemTime(item)) && itemTime(item) > approval.receivedAtMs);
92
- turn.items.splice(next < 0 ? turn.items.length : next, 0, approval);
93
- }
94
- }
95
- return result;
96
- }
97
-
98
- clear() { this.states.clear(); }
99
- }
1
+ const itemTime = item => item.dotsReceipt?.sentAt ? Date.parse(item.dotsReceipt.sentAt)
2
+ : item.startedAtMs ?? item.receivedAtMs;
3
+ const terminal = status => ['resolved', 'expired'].includes(status);
4
+
5
+ // Approval records have their own official room source. Keep them with the
6
+ // account-scoped hosted cache, then place them in the chat's observed positions.
7
+ export class DotsApprovalTimeline {
8
+ constructor(server) { this.server = server; this.states = new Map(); }
9
+
10
+ async state(threadId) {
11
+ const scope = this.server.historyScope(threadId);
12
+ if (!scope) throw new Error('读取点点审批历史需要账号身份。');
13
+ let state = this.states.get(scope);
14
+ if (!state) {
15
+ state = { scope, threadId, epoch: this.server.epoch, records: new Map() };
16
+ state.ready = (async () => {
17
+ const saved = await this.server.historyCache?.get(scope, 'approval-records');
18
+ for (const record of saved?.data || []) {
19
+ // A display record never reactivates an old pending request.
20
+ state.records.set(this.key(record), terminal(record.status) ? record : { ...record, status: 'expired' });
21
+ }
22
+ })();
23
+ this.states.set(scope, state);
24
+ }
25
+ await state.ready;
26
+ this.check(threadId, state);
27
+ return state;
28
+ }
29
+
30
+ check(threadId, state) {
31
+ if (state.scope !== this.server.historyScope(threadId) || state.epoch !== this.server.epoch) throw new Error('点点审批历史的账号连接已变化。');
32
+ }
33
+
34
+ key(item) { return JSON.stringify([typeof item.requestId, item.requestId]); }
35
+
36
+ merge(state, incoming) {
37
+ let changed = false;
38
+ for (const item of incoming) {
39
+ const key = this.key(item), previous = state.records.get(key);
40
+ const merged = { ...previous, ...item, id: previous?.id || item.id,
41
+ receivedAtMs: previous?.receivedAtMs ?? item.receivedAtMs,
42
+ ...(previous?.result && item.result == null ? { result: previous.result } : {}),
43
+ ...(previous?.afterItemId ? { afterItemId: previous.afterItemId } : {}) };
44
+ if (JSON.stringify(previous) !== JSON.stringify(merged)) { state.records.set(key, merged); changed = true; }
45
+ }
46
+ return changed;
47
+ }
48
+
49
+ save(state) {
50
+ return this.server.historyCache?.put(state.scope, 'approval-records', { data: [...state.records.values()] });
51
+ }
52
+
53
+ targets(sourceThreadId) {
54
+ return [...this.states.values()].filter(state => state.scope === this.server.historyScope(state.threadId)
55
+ && [...state.records.values()].some(record => record.params.threadId === sourceThreadId)).map(state => state.threadId);
56
+ }
57
+
58
+ async observe(threadId, records, turns) {
59
+ const state = await this.state(threadId);
60
+ const incoming = records.map(record => {
61
+ const turn = turns?.find(turn => turn.id === record.turnId);
62
+ const afterItemId = turn?.items.filter(item => item.type !== 'approval').at(-1)?.id;
63
+ return { ...record, ...(afterItemId ? { afterItemId } : {}) };
64
+ });
65
+ if (this.merge(state, incoming)) await this.save(state);
66
+ this.check(threadId, state);
67
+ }
68
+
69
+ overlay(threadId, turns, range = {}) {
70
+ const state = this.states.get(this.server.historyScope(threadId));
71
+ if (!state) return turns;
72
+ const ids = new Set([...state.records.values()].map(item => item.id));
73
+ const result = turns.map(turn => ({ ...turn, items: turn.items.filter(item => item.type !== 'approval' || !ids.has(item.id)) }));
74
+ for (const approval of [...state.records.values()].sort((a, b) => a.receivedAtMs - b.receivedAtMs)) {
75
+ if (Number.isFinite(range.fromMs) && approval.receivedAtMs < range.fromMs
76
+ || Number.isFinite(range.toMs) && approval.receivedAtMs > range.toMs) continue;
77
+ let turn = result.find(turn => turn.id === approval.roomMessageId || turn.id === approval.turnId);
78
+ if (!turn) {
79
+ if (!Number.isFinite(range.fromMs)) continue;
80
+ const positions = result.flatMap(turn => turn.items.map(item => ({ turn, item, time: itemTime(item) })))
81
+ .filter(position => Number.isFinite(position.time));
82
+ turn = positions.filter(position => position.time <= approval.receivedAtMs).at(-1)?.turn || positions[0]?.turn;
83
+ }
84
+ if (!turn) continue;
85
+ const after = turn.items.findIndex(item => item.id === approval.afterItemId);
86
+ if (after >= 0) {
87
+ let index = after + 1;
88
+ while (turn.items[index]?.type === 'approval' && turn.items[index].receivedAtMs <= approval.receivedAtMs) index++;
89
+ turn.items.splice(index, 0, approval);
90
+ } else {
91
+ const next = turn.items.findIndex(item => Number.isFinite(itemTime(item)) && itemTime(item) > approval.receivedAtMs);
92
+ turn.items.splice(next < 0 ? turn.items.length : next, 0, approval);
93
+ }
94
+ }
95
+ return result;
96
+ }
97
+
98
+ clear() { this.states.clear(); }
99
+ }
@@ -1,42 +1,42 @@
1
- import sharp from 'sharp';
2
-
3
- // Resolve Desktop's saved snapshot on the workstation. Signed URLs never go to the phone.
4
- export function createDotsAvatarReader(localServer) {
5
- const cache = new Map();
6
- return async (profile, identity) => {
7
- if (!profile.avatarFileId) return '';
8
- const key = JSON.stringify([identity.accountId, identity.userId, profile.avatarFileId]);
9
- if (!cache.has(key)) {
10
- const work = (async () => {
11
- const auth = await localServer.request('getAuthStatus', { includeToken: true, refreshToken: false });
12
- const principal = JSON.parse(Buffer.from(auth.authToken.split('.')[1], 'base64url'))['https://api.openai.com/auth'];
13
- if (principal?.chatgpt_account_id !== identity.accountId || principal.chatgpt_user_id !== identity.userId) {
14
- throw new Error('头像账号已变化,请刷新对话。');
15
- }
16
- const response = await fetch(`https://chatgpt.com/backend-api/files/download/${encodeURIComponent(profile.avatarFileId)}?download_intent=false&inline=true`, {
17
- headers: { Authorization: `Bearer ${auth.authToken}`, 'ChatGPT-Account-Id': identity.accountId },
18
- redirect: 'error', signal: AbortSignal.timeout(15000),
19
- });
20
- if (!response.ok) { await response.body?.cancel(); throw new Error(`点点头像资料读取失败(${response.status})`); }
21
- const { download_url: downloadUrl } = await response.json();
22
- const url = new URL(downloadUrl);
23
- const authenticated = url.origin === 'https://chatgpt.com' && url.pathname === '/backend-api/estuary/content';
24
- if (url.protocol !== 'https:' || !(authenticated || url.hostname.endsWith('.oaiusercontent.com'))) throw new Error('点点头像下载地址无效。');
25
- const image = await fetch(url, { redirect: 'error', signal: AbortSignal.timeout(15000),
26
- ...(authenticated ? { headers: { Authorization: `Bearer ${auth.authToken}`, 'ChatGPT-Account-Id': identity.accountId } } : {}) });
27
- if (!image.ok) { await image.body?.cancel(); throw new Error(`点点头像下载失败(${image.status})`); }
28
- const chunks = []; let size = 0;
29
- for await (const chunk of image.body) {
30
- size += chunk.length;
31
- if (size > 4 * 1024 * 1024) throw new Error('点点头像超过 4 MiB。');
32
- chunks.push(chunk);
33
- }
34
- const thumbnail = await sharp(Buffer.concat(chunks)).resize(96, 96, { fit: 'inside', withoutEnlargement: true }).webp().toBuffer();
35
- return `data:image/webp;base64,${thumbnail.toString('base64')}`;
36
- })();
37
- cache.set(key, work);
38
- work.catch(() => { if (cache.get(key) === work) cache.delete(key); });
39
- }
40
- return cache.get(key);
41
- };
42
- }
1
+ import sharp from 'sharp';
2
+
3
+ // Resolve Desktop's saved snapshot on the workstation. Signed URLs never go to the phone.
4
+ export function createDotsAvatarReader(localServer) {
5
+ const cache = new Map();
6
+ return async (profile, identity) => {
7
+ if (!profile.avatarFileId) return '';
8
+ const key = JSON.stringify([identity.accountId, identity.userId, profile.avatarFileId]);
9
+ if (!cache.has(key)) {
10
+ const work = (async () => {
11
+ const auth = await localServer.request('getAuthStatus', { includeToken: true, refreshToken: false });
12
+ const principal = JSON.parse(Buffer.from(auth.authToken.split('.')[1], 'base64url'))['https://api.openai.com/auth'];
13
+ if (principal?.chatgpt_account_id !== identity.accountId || principal.chatgpt_user_id !== identity.userId) {
14
+ throw new Error('头像账号已变化,请刷新对话。');
15
+ }
16
+ const response = await fetch(`https://chatgpt.com/backend-api/files/download/${encodeURIComponent(profile.avatarFileId)}?download_intent=false&inline=true`, {
17
+ headers: { Authorization: `Bearer ${auth.authToken}`, 'ChatGPT-Account-Id': identity.accountId },
18
+ redirect: 'error', signal: AbortSignal.timeout(15000),
19
+ });
20
+ if (!response.ok) { await response.body?.cancel(); throw new Error(`点点头像资料读取失败(${response.status})`); }
21
+ const { download_url: downloadUrl } = await response.json();
22
+ const url = new URL(downloadUrl);
23
+ const authenticated = url.origin === 'https://chatgpt.com' && url.pathname === '/backend-api/estuary/content';
24
+ if (url.protocol !== 'https:' || !(authenticated || url.hostname.endsWith('.oaiusercontent.com'))) throw new Error('点点头像下载地址无效。');
25
+ const image = await fetch(url, { redirect: 'error', signal: AbortSignal.timeout(15000),
26
+ ...(authenticated ? { headers: { Authorization: `Bearer ${auth.authToken}`, 'ChatGPT-Account-Id': identity.accountId } } : {}) });
27
+ if (!image.ok) { await image.body?.cancel(); throw new Error(`点点头像下载失败(${image.status})`); }
28
+ const chunks = []; let size = 0;
29
+ for await (const chunk of image.body) {
30
+ size += chunk.length;
31
+ if (size > 4 * 1024 * 1024) throw new Error('点点头像超过 4 MiB。');
32
+ chunks.push(chunk);
33
+ }
34
+ const thumbnail = await sharp(Buffer.concat(chunks)).resize(96, 96, { fit: 'inside', withoutEnlargement: true }).webp().toBuffer();
35
+ return `data:image/webp;base64,${thumbnail.toString('base64')}`;
36
+ })();
37
+ cache.set(key, work);
38
+ work.catch(() => { if (cache.get(key) === work) cache.delete(key); });
39
+ }
40
+ return cache.get(key);
41
+ };
42
+ }
@@ -1,50 +1,50 @@
1
- import { createHash } from 'node:crypto';
2
- import { dotsImageReference } from './dots-message-images.mjs';
3
-
4
- const nonempty = value => typeof value === 'string' && value.trim().length > 0;
5
-
6
- export function dotsChatRevision(turns) {
7
- const latest = dotsChatTailRevision(turns);
8
- const message = dotsChatMessageRevision(turns);
9
- // A message can keep streaming while a later agent action is already visible.
10
- return !latest || latest === message ? latest
11
- : createHash('sha256').update(JSON.stringify([latest, message])).digest('hex');
12
- }
13
-
14
- // A bounded disk-head probe compares the same last visible item on both sides.
15
- // The full transcript revision also tracks an earlier streaming message.
16
- export function dotsChatTailRevision(turns) {
17
- return chatItemRevision(turns.at(-1)?.items.at(-1));
18
- }
19
-
20
- export function dotsChatMessageRevision(turns) {
21
- for (let turnIndex = turns.length - 1; turnIndex >= 0; turnIndex--) {
22
- const items = turns[turnIndex].items;
23
- for (let itemIndex = items.length - 1; itemIndex >= 0; itemIndex--) {
24
- const item = items[itemIndex];
25
- if (item.type === 'userMessage' || item.type === 'agentMessage') return chatItemRevision(item);
26
- }
27
- }
28
- // A recent window containing only agent actions cannot acknowledge a chat message.
29
- return undefined;
30
- }
31
-
32
- function chatItemRevision(item) {
33
- if (!item) return '';
34
- const content = item.type === 'userMessage' ? item.content.map(part => {
35
- const reference = dotsImageReference(part);
36
- if (!reference?.url) return part;
37
- // Renewing an official storage signature does not create a new message.
38
- const url = new URL(reference.url);
39
- return { ...part, url: url.origin + url.pathname };
40
- }) : item.type === 'subAgentActivity' ? [item.kind, item.agentThreadId, item.agentPath]
41
- : [item.text || '', item.attachments || []];
42
- // Identity and displayed content survive reconnects; internal work does not affect this marker.
43
- return createHash('sha256').update(JSON.stringify([item.id, item.type, content])).digest('hex');
44
- }
45
- // Execution events remain separate from room user/agent messages.
46
- export function projectDotsChatItem(item) {
47
- if (item?.type !== 'subAgentActivity' || !nonempty(item.id) || !nonempty(item.agentThreadId)
48
- || !['started', 'interacted', 'completed', 'interrupted'].includes(item.kind)) return null;
49
- return { type: item.type, id: item.id, kind: item.kind, agentThreadId: item.agentThreadId, agentPath: item.agentPath || '' };
50
- }
1
+ import { createHash } from 'node:crypto';
2
+ import { dotsImageReference } from './dots-message-images.mjs';
3
+
4
+ const nonempty = value => typeof value === 'string' && value.trim().length > 0;
5
+
6
+ export function dotsChatRevision(turns) {
7
+ const latest = dotsChatTailRevision(turns);
8
+ const message = dotsChatMessageRevision(turns);
9
+ // A message can keep streaming while a later agent action is already visible.
10
+ return !latest || latest === message ? latest
11
+ : createHash('sha256').update(JSON.stringify([latest, message])).digest('hex');
12
+ }
13
+
14
+ // A bounded disk-head probe compares the same last visible item on both sides.
15
+ // The full transcript revision also tracks an earlier streaming message.
16
+ export function dotsChatTailRevision(turns) {
17
+ return chatItemRevision(turns.at(-1)?.items.at(-1));
18
+ }
19
+
20
+ export function dotsChatMessageRevision(turns) {
21
+ for (let turnIndex = turns.length - 1; turnIndex >= 0; turnIndex--) {
22
+ const items = turns[turnIndex].items;
23
+ for (let itemIndex = items.length - 1; itemIndex >= 0; itemIndex--) {
24
+ const item = items[itemIndex];
25
+ if (item.type === 'userMessage' || item.type === 'agentMessage') return chatItemRevision(item);
26
+ }
27
+ }
28
+ // A recent window containing only agent actions cannot acknowledge a chat message.
29
+ return undefined;
30
+ }
31
+
32
+ function chatItemRevision(item) {
33
+ if (!item) return '';
34
+ const content = item.type === 'userMessage' ? item.content.map(part => {
35
+ const reference = dotsImageReference(part);
36
+ if (!reference?.url) return part;
37
+ // Renewing an official storage signature does not create a new message.
38
+ const url = new URL(reference.url);
39
+ return { ...part, url: url.origin + url.pathname };
40
+ }) : item.type === 'subAgentActivity' ? [item.kind, item.agentThreadId, item.agentPath]
41
+ : [item.text || '', item.attachments || []];
42
+ // Identity and displayed content survive reconnects; internal work does not affect this marker.
43
+ return createHash('sha256').update(JSON.stringify([item.id, item.type, content])).digest('hex');
44
+ }
45
+ // Execution events remain separate from room user/agent messages.
46
+ export function projectDotsChatItem(item) {
47
+ if (item?.type !== 'subAgentActivity' || !nonempty(item.id) || !nonempty(item.agentThreadId)
48
+ || !['started', 'interacted', 'completed', 'interrupted'].includes(item.kind)) return null;
49
+ return { type: item.type, id: item.id, kind: item.kind, agentThreadId: item.agentThreadId, agentPath: item.agentPath || '' };
50
+ }