@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,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
+ }