copilotoffice 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -75
- package/dist/electron/main.js +2307 -138
- package/dist/electron/terminal/ipc-relay.js +210 -7
- package/dist/electron/terminal/preload.js +83 -13
- package/dist/electron/terminal/server.js +5961 -940
- package/dist/game.bundle.js +12559 -8482
- package/package.json +13 -7
package/dist/electron/main.js
CHANGED
|
@@ -23,24 +23,146 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
));
|
|
24
24
|
|
|
25
25
|
// electron/main.ts
|
|
26
|
-
var
|
|
27
|
-
var
|
|
28
|
-
var
|
|
29
|
-
var
|
|
26
|
+
var import_electron4 = require("electron");
|
|
27
|
+
var path8 = __toESM(require("path"));
|
|
28
|
+
var fs7 = __toESM(require("fs"));
|
|
29
|
+
var import_child_process4 = require("child_process");
|
|
30
30
|
|
|
31
31
|
// electron/terminal/ipc-relay.ts
|
|
32
32
|
var import_electron = require("electron");
|
|
33
|
+
var import_child_process2 = require("child_process");
|
|
34
|
+
var import_events = require("events");
|
|
35
|
+
var crypto2 = __toESM(require("crypto"));
|
|
36
|
+
var path2 = __toESM(require("path"));
|
|
37
|
+
|
|
38
|
+
// electron/terminal/pty-registry.ts
|
|
33
39
|
var import_child_process = require("child_process");
|
|
34
40
|
var crypto = __toESM(require("crypto"));
|
|
41
|
+
var fs = __toESM(require("fs"));
|
|
42
|
+
var os = __toESM(require("os"));
|
|
35
43
|
var path = __toESM(require("path"));
|
|
44
|
+
var START_TIME_GRACE_MS = 2e3;
|
|
45
|
+
var DEFAULT_REGISTRY_FILE = path.join(process.cwd(), ".data", "pty-pids.json");
|
|
46
|
+
function readPtyRegistry(file = DEFAULT_REGISTRY_FILE) {
|
|
47
|
+
try {
|
|
48
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
49
|
+
const parsed = JSON.parse(raw);
|
|
50
|
+
if (!Array.isArray(parsed)) return [];
|
|
51
|
+
return parsed.filter(
|
|
52
|
+
(r) => !!r && typeof r.pid === "number" && Number.isFinite(r.pid)
|
|
53
|
+
);
|
|
54
|
+
} catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function writePtyRegistry(records, file = DEFAULT_REGISTRY_FILE) {
|
|
59
|
+
try {
|
|
60
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
61
|
+
const tmp = `${file}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
|
|
62
|
+
fs.writeFileSync(tmp, JSON.stringify(records, null, 2));
|
|
63
|
+
fs.renameSync(tmp, file);
|
|
64
|
+
} catch {
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function isPidAlive(pid) {
|
|
68
|
+
try {
|
|
69
|
+
process.kill(pid, 0);
|
|
70
|
+
return true;
|
|
71
|
+
} catch (err) {
|
|
72
|
+
return err?.code === "EPERM";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function processStartTimeMs(pid) {
|
|
76
|
+
try {
|
|
77
|
+
if (os.platform() === "win32") {
|
|
78
|
+
const cmd = `powershell -NoProfile -NonInteractive -Command "$p = Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}'; if ($p) { [DateTimeOffset]::new($p.CreationDate).ToUnixTimeMilliseconds() }"`;
|
|
79
|
+
const out2 = (0, import_child_process.execSync)(cmd, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
80
|
+
const ms2 = parseInt(out2, 10);
|
|
81
|
+
return Number.isFinite(ms2) ? ms2 : null;
|
|
82
|
+
}
|
|
83
|
+
const out = (0, import_child_process.execSync)(`ps -o lstart= -p ${pid}`, {
|
|
84
|
+
encoding: "utf8",
|
|
85
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
86
|
+
}).trim();
|
|
87
|
+
if (!out) return null;
|
|
88
|
+
const ms = Date.parse(out);
|
|
89
|
+
return Number.isNaN(ms) ? null : ms;
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function processIdentityMatches(record, startTimeOf) {
|
|
95
|
+
const startMs = startTimeOf(record.pid);
|
|
96
|
+
if (startMs == null) return false;
|
|
97
|
+
return startMs <= record.startedAt + START_TIME_GRACE_MS;
|
|
98
|
+
}
|
|
99
|
+
function killTree(pid) {
|
|
100
|
+
try {
|
|
101
|
+
if (os.platform() === "win32") {
|
|
102
|
+
(0, import_child_process.execSync)(`taskkill /T /F /PID ${pid}`, { stdio: "ignore" });
|
|
103
|
+
} else {
|
|
104
|
+
try {
|
|
105
|
+
process.kill(-pid, "SIGKILL");
|
|
106
|
+
} catch {
|
|
107
|
+
process.kill(pid, "SIGKILL");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function reapRegisteredPtys(options = {}) {
|
|
116
|
+
const file = options.file ?? DEFAULT_REGISTRY_FILE;
|
|
117
|
+
const isAlive = options.isAlive ?? isPidAlive;
|
|
118
|
+
const kill = options.kill ?? killTree;
|
|
119
|
+
const startTimeOf = options.startTimeOf ?? processStartTimeMs;
|
|
120
|
+
const protectedPids = /* @__PURE__ */ new Set([process.pid, ...options.protectedPids ?? []]);
|
|
121
|
+
const records = readPtyRegistry(file);
|
|
122
|
+
const reaped = [];
|
|
123
|
+
const skipped = [];
|
|
124
|
+
const failed = [];
|
|
125
|
+
const survivors = [];
|
|
126
|
+
for (const record of records) {
|
|
127
|
+
const { pid } = record;
|
|
128
|
+
if (protectedPids.has(pid) || !isAlive(pid)) {
|
|
129
|
+
skipped.push(pid);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (!processIdentityMatches(record, startTimeOf)) {
|
|
133
|
+
skipped.push(pid);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (kill(pid)) {
|
|
137
|
+
reaped.push(pid);
|
|
138
|
+
} else {
|
|
139
|
+
failed.push(pid);
|
|
140
|
+
survivors.push(record);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
writePtyRegistry(survivors, file);
|
|
144
|
+
return { reaped, skipped, failed };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// electron/terminal/ipc-relay.ts
|
|
36
148
|
var TerminalRelay = class _TerminalRelay {
|
|
37
149
|
constructor(getWindow) {
|
|
150
|
+
/**
|
|
151
|
+
* Event bus for main-process consumers (e.g. the Teams service) that need
|
|
152
|
+
* server→main copilot/terminal events without going through the renderer.
|
|
153
|
+
* Emits: 'copilot-event' (agentId, event), 'copilot-turn-start' (agentId),
|
|
154
|
+
* 'copilot-turn-end' (agentId), 'copilot-tool-start' (agentId, toolName, toolId, status),
|
|
155
|
+
* 'session-meta-updated' (agentId, meta), 'terminal-exit' (agentId, exitCode).
|
|
156
|
+
*/
|
|
157
|
+
this.mainEvents = new import_events.EventEmitter();
|
|
38
158
|
this.server = null;
|
|
39
159
|
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
40
160
|
/** True while a deliberate shutdown→respawn is in progress (prevents double-spawn). */
|
|
41
161
|
this.shuttingDown = false;
|
|
42
162
|
/** Requests that arrived while the server was not connected. Flushed on ready. */
|
|
43
163
|
this.queuedRequests = [];
|
|
164
|
+
/** Latest backend-selection outcome reported by the server on 'ready'. */
|
|
165
|
+
this.backendInfo = null;
|
|
44
166
|
this.getWindow = getWindow;
|
|
45
167
|
}
|
|
46
168
|
static {
|
|
@@ -49,10 +171,10 @@ var TerminalRelay = class _TerminalRelay {
|
|
|
49
171
|
}
|
|
50
172
|
// ── Server Lifecycle ──────────────────────────────────────────
|
|
51
173
|
spawnServer(distDir) {
|
|
52
|
-
return new Promise((
|
|
53
|
-
const serverPath =
|
|
174
|
+
return new Promise((resolve2, reject) => {
|
|
175
|
+
const serverPath = path2.join(distDir, "terminal", "server.js");
|
|
54
176
|
console.log("[Relay] Forking terminal server:", serverPath);
|
|
55
|
-
this.server = (0,
|
|
177
|
+
this.server = (0, import_child_process2.fork)(serverPath, [], {
|
|
56
178
|
cwd: process.cwd(),
|
|
57
179
|
stdio: ["pipe", "inherit", "inherit", "ipc"]
|
|
58
180
|
});
|
|
@@ -60,7 +182,7 @@ var TerminalRelay = class _TerminalRelay {
|
|
|
60
182
|
reject(new Error("Terminal server did not send ready in time"));
|
|
61
183
|
}, 15e3);
|
|
62
184
|
this.server.on("message", (msg) => {
|
|
63
|
-
this.handleServerMessage(msg, readyTimeout,
|
|
185
|
+
this.handleServerMessage(msg, readyTimeout, resolve2);
|
|
64
186
|
});
|
|
65
187
|
this.server.on("exit", (code, signal) => {
|
|
66
188
|
console.error(`[Relay] Terminal server exited (code=${code}, signal=${signal})`);
|
|
@@ -76,6 +198,14 @@ var TerminalRelay = class _TerminalRelay {
|
|
|
76
198
|
}
|
|
77
199
|
const win = this.getWindow();
|
|
78
200
|
if (win && !win.isDestroyed()) {
|
|
201
|
+
try {
|
|
202
|
+
const { reaped } = reapRegisteredPtys();
|
|
203
|
+
if (reaped.length > 0) {
|
|
204
|
+
console.log(`[Relay] Reaped ${reaped.length} orphaned PTY tree(s) from crashed server:`, reaped);
|
|
205
|
+
}
|
|
206
|
+
} catch (e) {
|
|
207
|
+
console.error("[Relay] Failed to reap orphaned PTYs after crash:", e);
|
|
208
|
+
}
|
|
79
209
|
console.log("[Relay] Respawning terminal server...");
|
|
80
210
|
this.spawnServer(distDir).catch(
|
|
81
211
|
(e) => console.error("[Relay] Failed to respawn:", e)
|
|
@@ -107,10 +237,10 @@ var TerminalRelay = class _TerminalRelay {
|
|
|
107
237
|
this.pendingRequests.clear();
|
|
108
238
|
this.queuedRequests = [];
|
|
109
239
|
const oldPid = oldServer.pid;
|
|
110
|
-
return new Promise((
|
|
240
|
+
return new Promise((resolve2) => {
|
|
111
241
|
const onExit = () => {
|
|
112
242
|
clearTimeout(timeout);
|
|
113
|
-
|
|
243
|
+
resolve2();
|
|
114
244
|
};
|
|
115
245
|
oldServer.once("exit", onExit);
|
|
116
246
|
const timeout = setTimeout(() => {
|
|
@@ -120,14 +250,14 @@ var TerminalRelay = class _TerminalRelay {
|
|
|
120
250
|
process.kill(oldPid, 0);
|
|
121
251
|
console.log(`[Relay] Server PID ${oldPid} still alive after timeout \u2014 force killing`);
|
|
122
252
|
if (process.platform === "win32") {
|
|
123
|
-
(0,
|
|
253
|
+
(0, import_child_process2.execSync)(`taskkill /T /F /PID ${oldPid}`, { stdio: "ignore" });
|
|
124
254
|
} else {
|
|
125
255
|
process.kill(oldPid, "SIGKILL");
|
|
126
256
|
}
|
|
127
257
|
} catch {
|
|
128
258
|
}
|
|
129
259
|
}
|
|
130
|
-
|
|
260
|
+
resolve2();
|
|
131
261
|
}, 3e3);
|
|
132
262
|
});
|
|
133
263
|
}
|
|
@@ -140,32 +270,67 @@ var TerminalRelay = class _TerminalRelay {
|
|
|
140
270
|
request(msg) {
|
|
141
271
|
if (!this.server?.connected) {
|
|
142
272
|
console.log(`[Relay] Server not connected \u2014 queuing request ${msg.type} (${msg.requestId})`);
|
|
143
|
-
return new Promise((
|
|
144
|
-
this.queuedRequests.push({ msg, resolve });
|
|
273
|
+
return new Promise((resolve2) => {
|
|
274
|
+
this.queuedRequests.push({ msg, resolve: resolve2 });
|
|
145
275
|
});
|
|
146
276
|
}
|
|
147
|
-
return new Promise((
|
|
277
|
+
return new Promise((resolve2) => {
|
|
148
278
|
const timeout = setTimeout(() => {
|
|
149
279
|
if (this.pendingRequests.delete(msg.requestId)) {
|
|
150
280
|
console.warn(`[Relay] Request ${msg.type} (${msg.requestId}) timed out after ${_TerminalRelay.REQUEST_TIMEOUT_MS}ms`);
|
|
151
|
-
|
|
281
|
+
resolve2({ success: false, error: "Request timed out" });
|
|
152
282
|
}
|
|
153
283
|
}, _TerminalRelay.REQUEST_TIMEOUT_MS);
|
|
154
284
|
this.pendingRequests.set(msg.requestId, (result) => {
|
|
155
285
|
clearTimeout(timeout);
|
|
156
|
-
|
|
286
|
+
resolve2(result);
|
|
157
287
|
});
|
|
158
288
|
this.send(msg);
|
|
159
289
|
});
|
|
160
290
|
}
|
|
161
291
|
id() {
|
|
162
|
-
return
|
|
292
|
+
return crypto2.randomUUID();
|
|
293
|
+
}
|
|
294
|
+
// ── Main-process gateway (for the Teams service) ──────────────
|
|
295
|
+
// These let a main-process consumer talk to the terminal server directly,
|
|
296
|
+
// reusing the same request/response plumbing as the renderer IPC handlers.
|
|
297
|
+
mainGetSessionId(officeId, agentId) {
|
|
298
|
+
return this.request({ type: "get-session-id", requestId: this.id(), officeId, agentId });
|
|
299
|
+
}
|
|
300
|
+
/** True only when the agent's PTY is alive AND the CLI has signalled ready. */
|
|
301
|
+
mainIsAgentReady(officeId, agentId) {
|
|
302
|
+
return this.request({ type: "query-agent-statuses", requestId: this.id(), officeId }).then((statuses) => {
|
|
303
|
+
const s = statuses?.[agentId];
|
|
304
|
+
return !!(s && s.alive && s.ready);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
mainGetSessionMeta(officeId, agentId) {
|
|
308
|
+
return this.request({ type: "get-session-meta", requestId: this.id(), officeId, agentId });
|
|
309
|
+
}
|
|
310
|
+
mainWrite(officeId, agentId, data) {
|
|
311
|
+
return this.request({ type: "write", requestId: this.id(), officeId, agentId, data });
|
|
312
|
+
}
|
|
313
|
+
mainSubmitPrompt(officeId, agentId, prompt, label) {
|
|
314
|
+
return this.request({ type: "submit-prompt", requestId: this.id(), officeId, agentId, prompt, label });
|
|
315
|
+
}
|
|
316
|
+
/** Fire-and-forget: control whether copilot-events are mirrored to main for an agent without a viewer. */
|
|
317
|
+
mainSetAgentForwarding(officeId, agentId, enabled) {
|
|
318
|
+
this.send({ type: "set-agent-forwarding", officeId, agentId, enabled });
|
|
163
319
|
}
|
|
164
320
|
handleServerMessage(msg, readyTimeout, onReady) {
|
|
165
321
|
if (msg.type === "ready") {
|
|
166
322
|
clearTimeout(readyTimeout);
|
|
167
323
|
this.shuttingDown = false;
|
|
168
324
|
console.log("[Relay] Terminal server ready");
|
|
325
|
+
if (msg.backend) {
|
|
326
|
+
this.backendInfo = msg.backend;
|
|
327
|
+
if (msg.backend.fellBack) {
|
|
328
|
+
const win2 = this.getWindow();
|
|
329
|
+
if (win2 && !win2.isDestroyed()) {
|
|
330
|
+
win2.webContents.send("backend-fallback", this.backendInfo);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
169
334
|
if (this.queuedRequests.length > 0) {
|
|
170
335
|
console.log(`[Relay] Flushing ${this.queuedRequests.length} queued request(s)`);
|
|
171
336
|
for (const queued of this.queuedRequests) {
|
|
@@ -185,6 +350,29 @@ var TerminalRelay = class _TerminalRelay {
|
|
|
185
350
|
}
|
|
186
351
|
return;
|
|
187
352
|
}
|
|
353
|
+
switch (msg.type) {
|
|
354
|
+
case "copilot-event":
|
|
355
|
+
this.mainEvents.emit("copilot-event", msg.agentId, msg.event);
|
|
356
|
+
break;
|
|
357
|
+
case "copilot-turn-start":
|
|
358
|
+
this.mainEvents.emit("copilot-turn-start", msg.agentId);
|
|
359
|
+
break;
|
|
360
|
+
case "copilot-turn-end":
|
|
361
|
+
this.mainEvents.emit("copilot-turn-end", msg.agentId);
|
|
362
|
+
break;
|
|
363
|
+
case "copilot-user-message":
|
|
364
|
+
this.mainEvents.emit("copilot-user-message", msg.agentId, msg.text);
|
|
365
|
+
break;
|
|
366
|
+
case "copilot-tool-start":
|
|
367
|
+
this.mainEvents.emit("copilot-tool-start", msg.agentId, msg.toolName, msg.toolId, msg.status);
|
|
368
|
+
break;
|
|
369
|
+
case "session-meta-updated":
|
|
370
|
+
this.mainEvents.emit("session-meta-updated", msg.agentId, msg.meta);
|
|
371
|
+
break;
|
|
372
|
+
case "terminal-exit":
|
|
373
|
+
this.mainEvents.emit("terminal-exit", msg.agentId, msg.exitCode);
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
188
376
|
const win = this.getWindow();
|
|
189
377
|
if (!win || win.isDestroyed()) return;
|
|
190
378
|
switch (msg.type) {
|
|
@@ -195,7 +383,7 @@ var TerminalRelay = class _TerminalRelay {
|
|
|
195
383
|
win.webContents.send("terminal-exit", msg.agentId, msg.exitCode);
|
|
196
384
|
break;
|
|
197
385
|
case "copilot-event":
|
|
198
|
-
win.webContents.send("copilot-event", msg.agentId, msg.event);
|
|
386
|
+
if (!msg.mainOnly) win.webContents.send("copilot-event", msg.agentId, msg.event);
|
|
199
387
|
break;
|
|
200
388
|
case "copilot-tool-start":
|
|
201
389
|
win.webContents.send("copilot-tool-start", msg.agentId, msg.toolName, msg.toolId, msg.status);
|
|
@@ -210,7 +398,7 @@ var TerminalRelay = class _TerminalRelay {
|
|
|
210
398
|
win.webContents.send("copilot-turn-start", msg.agentId);
|
|
211
399
|
break;
|
|
212
400
|
case "copilot-user-message":
|
|
213
|
-
win.webContents.send("copilot-user-message", msg.agentId);
|
|
401
|
+
win.webContents.send("copilot-user-message", msg.agentId, msg.text);
|
|
214
402
|
break;
|
|
215
403
|
case "session-meta-updated":
|
|
216
404
|
win.webContents.send("session-meta-updated", msg.agentId, msg.meta);
|
|
@@ -218,17 +406,24 @@ var TerminalRelay = class _TerminalRelay {
|
|
|
218
406
|
case "terminal-preload-status":
|
|
219
407
|
win.webContents.send("terminal-preload-status", msg.agentId, msg.status);
|
|
220
408
|
break;
|
|
409
|
+
case "backend-online":
|
|
410
|
+
win.webContents.send("backend-online", msg.officeId, msg.backend);
|
|
411
|
+
break;
|
|
412
|
+
case "backend-session-fallback":
|
|
413
|
+
win.webContents.send("backend-session-fallback", msg.officeId, msg.agentId, msg.reason);
|
|
414
|
+
break;
|
|
221
415
|
}
|
|
222
416
|
}
|
|
223
417
|
// ── IPC Handler Registration ──────────────────────────────────
|
|
224
418
|
registerIpc() {
|
|
419
|
+
import_electron.ipcMain.handle("terminal-backend-info", () => this.backendInfo);
|
|
225
420
|
import_electron.ipcMain.handle(
|
|
226
421
|
"terminal-start",
|
|
227
422
|
(_event, officeId, agentId, workingDir, cols, rows, preseededPrompt, launchMode) => this.request({ type: "start", requestId: this.id(), officeId, agentId, workingDir, cols, rows, preseededPrompt, launchMode })
|
|
228
423
|
);
|
|
229
424
|
import_electron.ipcMain.handle(
|
|
230
425
|
"terminal-attach",
|
|
231
|
-
(_event, officeId, agentId) => this.request({ type: "attach", requestId: this.id(), officeId, agentId })
|
|
426
|
+
(_event, officeId, agentId, foreground) => this.request({ type: "attach", requestId: this.id(), officeId, agentId, foreground })
|
|
232
427
|
);
|
|
233
428
|
import_electron.ipcMain.handle("terminal-detach", (_event, officeId, agentId) => {
|
|
234
429
|
this.send({ type: "detach", officeId, agentId });
|
|
@@ -242,6 +437,14 @@ var TerminalRelay = class _TerminalRelay {
|
|
|
242
437
|
this.send({ type: "resize", officeId, agentId, cols, rows });
|
|
243
438
|
return { success: true };
|
|
244
439
|
});
|
|
440
|
+
import_electron.ipcMain.handle("set-yolo", (_event, enabled) => {
|
|
441
|
+
this.send({ type: "set-yolo", enabled: !!enabled });
|
|
442
|
+
return { success: true };
|
|
443
|
+
});
|
|
444
|
+
import_electron.ipcMain.handle("set-additional-params", (_event, params) => {
|
|
445
|
+
this.send({ type: "set-additional-params", params: String(params ?? "") });
|
|
446
|
+
return { success: true };
|
|
447
|
+
});
|
|
245
448
|
import_electron.ipcMain.handle(
|
|
246
449
|
"terminal-kill",
|
|
247
450
|
(_event, officeId, agentId) => this.request({ type: "kill", requestId: this.id(), officeId, agentId })
|
|
@@ -314,22 +517,22 @@ var TerminalRelay = class _TerminalRelay {
|
|
|
314
517
|
};
|
|
315
518
|
|
|
316
519
|
// electron/officeFileStore.ts
|
|
317
|
-
var
|
|
318
|
-
var
|
|
520
|
+
var fs2 = __toESM(require("fs"));
|
|
521
|
+
var path3 = __toESM(require("path"));
|
|
319
522
|
var DEFAULT_DATA_SUBDIR = ".data";
|
|
320
523
|
var DEFAULT_FILE_NAME = "copilot-offices.json";
|
|
321
524
|
function createOfficeFileStore(options = {}) {
|
|
322
525
|
const cwd = options.cwd ?? process.cwd();
|
|
323
526
|
const dataSubdir = options.dataSubdir ?? DEFAULT_DATA_SUBDIR;
|
|
324
527
|
const fileName = options.fileName ?? DEFAULT_FILE_NAME;
|
|
325
|
-
const dataDir =
|
|
326
|
-
const filePath =
|
|
528
|
+
const dataDir = path3.join(cwd, dataSubdir);
|
|
529
|
+
const filePath = path3.join(dataDir, fileName);
|
|
327
530
|
return {
|
|
328
531
|
filePath,
|
|
329
532
|
load() {
|
|
330
533
|
try {
|
|
331
|
-
if (!
|
|
332
|
-
const data =
|
|
534
|
+
if (!fs2.existsSync(filePath)) return { success: true, data: null };
|
|
535
|
+
const data = fs2.readFileSync(filePath, "utf8");
|
|
333
536
|
return { success: true, data };
|
|
334
537
|
} catch (e) {
|
|
335
538
|
return { success: false, data: null, error: String(e) };
|
|
@@ -337,8 +540,8 @@ function createOfficeFileStore(options = {}) {
|
|
|
337
540
|
},
|
|
338
541
|
save(data) {
|
|
339
542
|
try {
|
|
340
|
-
|
|
341
|
-
|
|
543
|
+
fs2.mkdirSync(dataDir, { recursive: true });
|
|
544
|
+
fs2.writeFileSync(filePath, data, "utf8");
|
|
342
545
|
return { success: true };
|
|
343
546
|
} catch (e) {
|
|
344
547
|
return { success: false, error: String(e) };
|
|
@@ -403,123 +606,1995 @@ function registerNonTerminalIpc(hooks) {
|
|
|
403
606
|
});
|
|
404
607
|
}
|
|
405
608
|
|
|
406
|
-
// electron/
|
|
407
|
-
var
|
|
408
|
-
var
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
609
|
+
// electron/teams/onlineAgentsStore.ts
|
|
610
|
+
var fs3 = __toESM(require("fs"));
|
|
611
|
+
var path4 = __toESM(require("path"));
|
|
612
|
+
|
|
613
|
+
// electron/teams/log.ts
|
|
614
|
+
var PREFIX = "[TeamsRemote]";
|
|
615
|
+
function tlog(...args) {
|
|
616
|
+
console.log(PREFIX, ...args);
|
|
617
|
+
}
|
|
618
|
+
function twarn(...args) {
|
|
619
|
+
console.warn(PREFIX, ...args);
|
|
620
|
+
}
|
|
621
|
+
function terror(...args) {
|
|
622
|
+
console.error(PREFIX, ...args);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// electron/teams/onlineAgentsStore.ts
|
|
626
|
+
var STORE_VERSION = 1;
|
|
627
|
+
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
628
|
+
function emptyState() {
|
|
629
|
+
return { bindings: [], knownThreads: [] };
|
|
630
|
+
}
|
|
631
|
+
function gcStale(bindings, nowMs, maxAgeMs = THIRTY_DAYS_MS) {
|
|
632
|
+
const kept = [];
|
|
633
|
+
const removed = [];
|
|
634
|
+
for (const b of bindings) {
|
|
635
|
+
if (nowMs - (b.lastConnected || 0) > maxAgeMs) removed.push(b);
|
|
636
|
+
else kept.push(b);
|
|
637
|
+
}
|
|
638
|
+
return { kept, removed };
|
|
639
|
+
}
|
|
640
|
+
var FileTeamsOnlineStore = class {
|
|
641
|
+
constructor(filePath) {
|
|
642
|
+
this.filePath = filePath;
|
|
643
|
+
}
|
|
644
|
+
static defaultPath(dataDir) {
|
|
645
|
+
return path4.join(dataDir, "teams-online-agents.json");
|
|
646
|
+
}
|
|
647
|
+
async load() {
|
|
648
|
+
try {
|
|
649
|
+
const raw = await fs3.promises.readFile(this.filePath, "utf-8");
|
|
650
|
+
const parsed = JSON.parse(raw);
|
|
651
|
+
return {
|
|
652
|
+
bindings: Array.isArray(parsed.bindings) ? parsed.bindings : [],
|
|
653
|
+
knownThreads: Array.isArray(parsed.knownThreads) ? parsed.knownThreads : []
|
|
654
|
+
};
|
|
655
|
+
} catch (e) {
|
|
656
|
+
if (e?.code === "ENOENT") return emptyState();
|
|
657
|
+
terror("Failed to load online store \u2014 starting empty:", e);
|
|
658
|
+
return emptyState();
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
async save(state) {
|
|
662
|
+
const dir = path4.dirname(this.filePath);
|
|
663
|
+
await fs3.promises.mkdir(dir, { recursive: true });
|
|
664
|
+
const payload = JSON.stringify(
|
|
665
|
+
{ version: STORE_VERSION, bindings: state.bindings, knownThreads: state.knownThreads },
|
|
666
|
+
null,
|
|
667
|
+
2
|
|
668
|
+
);
|
|
669
|
+
await fs3.promises.writeFile(this.filePath, payload, "utf-8");
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
// electron/teams/dispatchQueue.ts
|
|
674
|
+
var DispatchQueue = class {
|
|
675
|
+
constructor(process2) {
|
|
676
|
+
this.process = process2;
|
|
677
|
+
this.queues = /* @__PURE__ */ new Map();
|
|
678
|
+
this.processing = /* @__PURE__ */ new Set();
|
|
679
|
+
}
|
|
680
|
+
key(officeId, agentId) {
|
|
681
|
+
return `${officeId}:${agentId}`;
|
|
682
|
+
}
|
|
683
|
+
enqueue(item) {
|
|
684
|
+
const k = this.key(item.officeId, item.agentId);
|
|
685
|
+
const q = this.queues.get(k) ?? [];
|
|
686
|
+
q.push(item);
|
|
687
|
+
this.queues.set(k, q);
|
|
688
|
+
void this.drain(k);
|
|
689
|
+
}
|
|
690
|
+
/** Number of pending items for an agent (excludes the in-flight one). */
|
|
691
|
+
pending(officeId, agentId) {
|
|
692
|
+
return this.queues.get(this.key(officeId, agentId))?.length ?? 0;
|
|
693
|
+
}
|
|
694
|
+
/** Drop all queued work for an agent (e.g. on going offline). */
|
|
695
|
+
clear(officeId, agentId) {
|
|
696
|
+
this.queues.delete(this.key(officeId, agentId));
|
|
697
|
+
}
|
|
698
|
+
async drain(k) {
|
|
699
|
+
if (this.processing.has(k)) return;
|
|
700
|
+
this.processing.add(k);
|
|
701
|
+
try {
|
|
702
|
+
for (; ; ) {
|
|
703
|
+
const q = this.queues.get(k);
|
|
704
|
+
const item = q?.shift();
|
|
705
|
+
if (!item) break;
|
|
706
|
+
try {
|
|
707
|
+
await this.process(item);
|
|
708
|
+
} catch (e) {
|
|
709
|
+
twarn("dispatch item failed:", e.message);
|
|
424
710
|
}
|
|
425
711
|
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
712
|
+
} finally {
|
|
713
|
+
this.processing.delete(k);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
// electron/teams/channelResolver.ts
|
|
719
|
+
function resolveChannel(office, settings) {
|
|
720
|
+
const override = office?.teamsChannelUrl?.trim();
|
|
721
|
+
if (override) return override;
|
|
722
|
+
return settings?.defaultChannelUrl?.trim() || "";
|
|
723
|
+
}
|
|
724
|
+
function activeChannelSet(bindings) {
|
|
725
|
+
const set = /* @__PURE__ */ new Set();
|
|
726
|
+
for (const b of bindings) {
|
|
727
|
+
if (b.online && b.channelId) set.add(b.channelId);
|
|
728
|
+
}
|
|
729
|
+
return set;
|
|
730
|
+
}
|
|
731
|
+
function findBinding(channelId, threadRootId, bindings) {
|
|
732
|
+
return bindings.find(
|
|
733
|
+
(b) => b.online && b.channelId === channelId && b.threadRootId === threadRootId
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
function classifyThread(channelId, threadRootId, bindings, knownThreads) {
|
|
737
|
+
if (findBinding(channelId, threadRootId, bindings)) return "bound";
|
|
738
|
+
if (threadRootId && knownThreads.some((t) => t.threadRootId === threadRootId)) {
|
|
739
|
+
return "orphaned";
|
|
740
|
+
}
|
|
741
|
+
return "foreign";
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// electron/teams/messageFilter.ts
|
|
745
|
+
var STALE_MS = 5 * 60 * 1e3;
|
|
746
|
+
var INJECTION_PATTERNS = [
|
|
747
|
+
/ignore (all |your )?(previous|prior|above) instructions/i,
|
|
748
|
+
/disregard (the |your )?(system|previous) prompt/i,
|
|
749
|
+
/reveal (your )?(system )?prompt/i
|
|
750
|
+
];
|
|
751
|
+
function scanInjection(content) {
|
|
752
|
+
return INJECTION_PATTERNS.some((re) => re.test(content));
|
|
753
|
+
}
|
|
754
|
+
var BOT_DISPLAY_NAMES = /^(flow bot|power automate|microsoft power automate)$/i;
|
|
755
|
+
function isBotSender(msg) {
|
|
756
|
+
const id = (msg.senderId || "").trim();
|
|
757
|
+
if (id) return id.includes("28:");
|
|
758
|
+
return BOT_DISPLAY_NAMES.test((msg.senderName || "").trim());
|
|
759
|
+
}
|
|
760
|
+
var MessageFilter = class _MessageFilter {
|
|
761
|
+
constructor(nowFn = Date.now) {
|
|
762
|
+
this.nowFn = nowFn;
|
|
763
|
+
this.seen = /* @__PURE__ */ new Set();
|
|
764
|
+
this.seenOrder = [];
|
|
765
|
+
}
|
|
766
|
+
static {
|
|
767
|
+
this.MAX_SEEN = 5e3;
|
|
768
|
+
}
|
|
769
|
+
/** Reset dedup memory (e.g. on service restart). */
|
|
770
|
+
reset() {
|
|
771
|
+
this.seen.clear();
|
|
772
|
+
this.seenOrder = [];
|
|
773
|
+
}
|
|
774
|
+
remember(id) {
|
|
775
|
+
this.seen.add(id);
|
|
776
|
+
this.seenOrder.push(id);
|
|
777
|
+
if (this.seenOrder.length > _MessageFilter.MAX_SEEN) {
|
|
778
|
+
const old = this.seenOrder.shift();
|
|
779
|
+
if (old) this.seen.delete(old);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
evaluate(msg, bindings, knownThreads) {
|
|
783
|
+
if (msg.messageId) {
|
|
784
|
+
if (this.seen.has(msg.messageId)) return { action: "ignore", reason: "duplicate" };
|
|
785
|
+
this.remember(msg.messageId);
|
|
786
|
+
}
|
|
787
|
+
if (msg.hasMarker) return { action: "ignore", reason: "self-post" };
|
|
788
|
+
if (isBotSender(msg)) return { action: "ignore", reason: "bot-sender" };
|
|
789
|
+
if (msg.composeTime) {
|
|
790
|
+
const t = Date.parse(msg.composeTime);
|
|
791
|
+
if (Number.isFinite(t) && this.nowFn() - t > STALE_MS) {
|
|
792
|
+
return { action: "ignore", reason: "stale" };
|
|
440
793
|
}
|
|
441
794
|
}
|
|
795
|
+
const active = activeChannelSet(bindings);
|
|
796
|
+
if (!active.has(msg.channelId)) return { action: "ignore", reason: "inactive-channel" };
|
|
797
|
+
if (!msg.threadRootId) return { action: "ignore", reason: "root-message" };
|
|
798
|
+
const classification = classifyThread(msg.channelId, msg.threadRootId, bindings, knownThreads);
|
|
799
|
+
if (classification === "foreign") {
|
|
800
|
+
return { action: "ignore", classification, reason: "foreign-thread" };
|
|
801
|
+
}
|
|
802
|
+
if (classification === "orphaned") {
|
|
803
|
+
return { action: "orphaned-notice", classification, reason: "orphaned-thread" };
|
|
804
|
+
}
|
|
805
|
+
if (scanInjection(msg.content)) {
|
|
806
|
+
return { action: "ignore", classification, reason: "injection-blocked" };
|
|
807
|
+
}
|
|
808
|
+
const binding = findBinding(msg.channelId, msg.threadRootId, bindings);
|
|
809
|
+
return { action: "dispatch", classification, binding };
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
// electron/teams/handleRegistry.ts
|
|
814
|
+
function normalizeHandle(name) {
|
|
815
|
+
if (!name) return "";
|
|
816
|
+
return name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
817
|
+
}
|
|
818
|
+
function assignHandle(base, takenOnline) {
|
|
819
|
+
if (!base) {
|
|
820
|
+
throw new Error("Cannot derive a Teams handle from an empty/invalid agent name");
|
|
821
|
+
}
|
|
822
|
+
if (!takenOnline.has(base)) return base;
|
|
823
|
+
for (let i = 1; ; i++) {
|
|
824
|
+
const candidate = `${base}-${i}`;
|
|
825
|
+
if (!takenOnline.has(candidate)) return candidate;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// electron/teams/channelLink.ts
|
|
830
|
+
function parseChannelLink(url) {
|
|
831
|
+
if (!url || typeof url !== "string") return null;
|
|
832
|
+
const trimmed = url.trim();
|
|
833
|
+
if (!trimmed) return null;
|
|
834
|
+
let parsed;
|
|
835
|
+
try {
|
|
836
|
+
parsed = new URL(trimmed);
|
|
442
837
|
} catch {
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
840
|
+
const teamId = (parsed.searchParams.get("groupId") || "").trim();
|
|
841
|
+
const tenantId = (parsed.searchParams.get("tenantId") || "").trim();
|
|
842
|
+
const marker = "/l/channel/";
|
|
843
|
+
const idx = parsed.pathname.indexOf(marker);
|
|
844
|
+
let channelId = "";
|
|
845
|
+
if (idx >= 0) {
|
|
846
|
+
const rest = parsed.pathname.slice(idx + marker.length);
|
|
847
|
+
const seg = rest.split("/")[0] || "";
|
|
848
|
+
try {
|
|
849
|
+
channelId = decodeURIComponent(seg).trim();
|
|
850
|
+
} catch {
|
|
851
|
+
channelId = seg.trim();
|
|
852
|
+
}
|
|
443
853
|
}
|
|
854
|
+
if (!teamId || !channelId) return null;
|
|
855
|
+
if (!/^19:.+@thread\./.test(channelId)) return null;
|
|
856
|
+
return { teamId, channelId, tenantId };
|
|
444
857
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
var
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
858
|
+
|
|
859
|
+
// electron/teams/chunk.ts
|
|
860
|
+
var DEFAULT_MAX = 3500;
|
|
861
|
+
function chunkReply(text, max = DEFAULT_MAX) {
|
|
862
|
+
const body = text ?? "";
|
|
863
|
+
const prefixBudget = 12;
|
|
864
|
+
const limit = Math.max(1, max - prefixBudget);
|
|
865
|
+
if (body.length <= limit) {
|
|
866
|
+
return body.length === 0 ? [""] : [body];
|
|
867
|
+
}
|
|
868
|
+
const raw = [];
|
|
869
|
+
let remaining = body;
|
|
870
|
+
while (remaining.length > limit) {
|
|
871
|
+
let cut = remaining.lastIndexOf("\n", limit);
|
|
872
|
+
if (cut <= 0) cut = limit;
|
|
873
|
+
raw.push(remaining.slice(0, cut));
|
|
874
|
+
remaining = remaining.slice(cut).replace(/^\n/, "");
|
|
875
|
+
}
|
|
876
|
+
if (remaining.length > 0) raw.push(remaining);
|
|
877
|
+
const n = raw.length;
|
|
878
|
+
if (n === 1) return raw;
|
|
879
|
+
return raw.map((part, i) => `(${i + 1}/${n}) ${part}`);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// electron/teams/htmlText.ts
|
|
883
|
+
function stripHtml(html) {
|
|
884
|
+
if (!html) return "";
|
|
885
|
+
return html.replace(/<!--[\s\S]*?-->/g, "").replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<[^>]+>/g, "").replace(/ /gi, " ").replace(/&/gi, "&").replace(/</gi, "<").replace(/>/gi, ">").replace(/"/gi, '"').replace(/'/gi, "'").replace(/\n{3,}/g, "\n\n").trim();
|
|
886
|
+
}
|
|
887
|
+
function escapeHtml(text) {
|
|
888
|
+
return (text ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// electron/teams/imageMarker.ts
|
|
892
|
+
var fs4 = __toESM(require("fs"));
|
|
893
|
+
var path5 = __toESM(require("path"));
|
|
894
|
+
var IMAGE_MARKER_SOURCE = "<!--office-image:(.*?)-->";
|
|
895
|
+
var IMAGE_MARKER_RE = new RegExp(IMAGE_MARKER_SOURCE, "g");
|
|
896
|
+
function extractImageMarkers(input) {
|
|
897
|
+
const src = input ?? "";
|
|
898
|
+
const paths = [];
|
|
899
|
+
const seen = /* @__PURE__ */ new Set();
|
|
900
|
+
const matcher = new RegExp(IMAGE_MARKER_SOURCE, "g");
|
|
901
|
+
for (const m of src.matchAll(matcher)) {
|
|
902
|
+
const p = (m[1] ?? "").trim();
|
|
903
|
+
if (!p || seen.has(p)) continue;
|
|
904
|
+
seen.add(p);
|
|
905
|
+
paths.push(p);
|
|
906
|
+
}
|
|
907
|
+
const text = src.replace(new RegExp(IMAGE_MARKER_SOURCE, "g"), "").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
908
|
+
return { text, paths };
|
|
909
|
+
}
|
|
910
|
+
function sniffImageType(buf) {
|
|
911
|
+
if (buf.length >= 8 && buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71 && buf[4] === 13 && buf[5] === 10 && buf[6] === 26 && buf[7] === 10) {
|
|
912
|
+
return "image/png";
|
|
913
|
+
}
|
|
914
|
+
if (buf.length >= 3 && buf[0] === 255 && buf[1] === 216 && buf[2] === 255) {
|
|
915
|
+
return "image/jpeg";
|
|
916
|
+
}
|
|
917
|
+
if (buf.length >= 6 && buf[0] === 71 && buf[1] === 73 && buf[2] === 70 && buf[3] === 56 && (buf[4] === 55 || buf[4] === 57) && buf[5] === 97) {
|
|
918
|
+
return "image/gif";
|
|
919
|
+
}
|
|
920
|
+
if (buf.length >= 12 && buf.toString("ascii", 0, 4) === "RIFF" && buf.toString("ascii", 8, 12) === "WEBP") {
|
|
921
|
+
return "image/webp";
|
|
922
|
+
}
|
|
923
|
+
if (buf.length >= 2 && buf[0] === 66 && buf[1] === 77) {
|
|
924
|
+
return "image/bmp";
|
|
925
|
+
}
|
|
926
|
+
return null;
|
|
927
|
+
}
|
|
928
|
+
function resolveWithinBase(rawPath, baseDir) {
|
|
929
|
+
if (!baseDir) return null;
|
|
930
|
+
if (path5.isAbsolute(rawPath)) return null;
|
|
931
|
+
const root = path5.resolve(baseDir);
|
|
932
|
+
const resolved = path5.resolve(root, rawPath);
|
|
933
|
+
const rel = path5.relative(root, resolved);
|
|
934
|
+
if (rel === "" || rel.startsWith("..") || path5.isAbsolute(rel)) return null;
|
|
935
|
+
return resolved;
|
|
936
|
+
}
|
|
937
|
+
var DEFAULT_MAX_BYTES = 4 * 1024 * 1024;
|
|
938
|
+
var DEFAULT_MAX_IMAGES = 8;
|
|
939
|
+
var DEFAULT_MAX_TOTAL_BYTES = 20 * 1024 * 1024;
|
|
940
|
+
async function loadHostedImages(paths, options = {}) {
|
|
941
|
+
const baseDir = options.baseDir;
|
|
942
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
943
|
+
const maxImages = options.maxImages ?? DEFAULT_MAX_IMAGES;
|
|
944
|
+
const maxTotalBytes = options.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES;
|
|
945
|
+
const readFile = options.readFile ?? ((p) => fs4.promises.readFile(p));
|
|
946
|
+
const warn = options.warn ?? (() => {
|
|
947
|
+
});
|
|
948
|
+
const images = [];
|
|
949
|
+
let totalBytes = 0;
|
|
950
|
+
for (const rawPath of paths) {
|
|
951
|
+
if (images.length >= maxImages) {
|
|
952
|
+
warn(`office-image: image count cap (${maxImages}) reached \u2014 ignoring "${rawPath}"`);
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
const absPath = resolveWithinBase(rawPath, baseDir);
|
|
956
|
+
if (!absPath) {
|
|
957
|
+
warn(`office-image: rejected path outside agent sandbox: "${rawPath}"`);
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
try {
|
|
961
|
+
const buf = await readFile(absPath);
|
|
962
|
+
if (buf.length === 0) {
|
|
963
|
+
warn(`office-image: skipping empty file ${absPath}`);
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
if (buf.length > maxBytes) {
|
|
967
|
+
warn(`office-image: skipping ${absPath} \u2014 ${buf.length} bytes exceeds ${maxBytes} limit`);
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
const sniffed = sniffImageType(buf);
|
|
971
|
+
if (!sniffed) {
|
|
972
|
+
warn(`office-image: skipping ${absPath} \u2014 not a recognized image (magic-byte check failed)`);
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
if (totalBytes + buf.length > maxTotalBytes) {
|
|
976
|
+
warn(`office-image: aggregate byte cap (${maxTotalBytes}) reached \u2014 ignoring ${absPath}`);
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
totalBytes += buf.length;
|
|
980
|
+
images.push({
|
|
981
|
+
id: String(images.length + 1),
|
|
982
|
+
contentType: sniffed,
|
|
983
|
+
contentBytesBase64: buf.toString("base64")
|
|
457
984
|
});
|
|
985
|
+
} catch (e) {
|
|
986
|
+
warn(`office-image: could not read ${absPath}: ${e.message}`);
|
|
458
987
|
}
|
|
459
|
-
} else {
|
|
460
|
-
watcherProcess = (0, import_child_process2.spawn)("npx", ["esbuild", "src/main.ts", "--bundle", "--outfile=dist/game.bundle.js", "--platform=browser", "--format=iife", "--global-name=CopilotOffice", "--watch"], {
|
|
461
|
-
cwd: copilotOfficePath,
|
|
462
|
-
shell: true,
|
|
463
|
-
stdio: "pipe"
|
|
464
|
-
});
|
|
465
988
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
989
|
+
return images;
|
|
990
|
+
}
|
|
991
|
+
function hostedImagesHtml(images) {
|
|
992
|
+
return images.map((img) => `<img src="../hostedContents/${img.id}/$value" alt="attachment" style="max-width:100%">`).join("<br>");
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// electron/teams/ackQuips.ts
|
|
996
|
+
var ACK_QUIPS = [
|
|
997
|
+
"Cracking my knuckles\u2026",
|
|
998
|
+
"Summoning the electrons\u2026",
|
|
999
|
+
"Bribing the compiler\u2026",
|
|
1000
|
+
"Yelling at the terminal politely\u2026",
|
|
1001
|
+
"Consulting the rubber duck\u2026",
|
|
1002
|
+
"Putting down my coffee\u2026",
|
|
1003
|
+
"Rolling a d20 for initiative\u2026",
|
|
1004
|
+
"Waking up the hamsters\u2026",
|
|
1005
|
+
"Pretending I wasn't slacking\u2026",
|
|
1006
|
+
"Feeding the machine spirit\u2026"
|
|
1007
|
+
];
|
|
1008
|
+
function pickAckQuip(rng = Math.random) {
|
|
1009
|
+
if (ACK_QUIPS.length === 0) return "Working on this\u2026";
|
|
1010
|
+
return ACK_QUIPS[Math.floor(rng() * ACK_QUIPS.length)] ?? ACK_QUIPS[0];
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// electron/teams/teamsService.ts
|
|
1014
|
+
var RECONCILE_MS = 15e3;
|
|
1015
|
+
var TURN_SETTLE_MS = 2500;
|
|
1016
|
+
var TeamsService = class _TeamsService {
|
|
1017
|
+
constructor(deps) {
|
|
1018
|
+
this.deps = deps;
|
|
1019
|
+
this.bindings = [];
|
|
1020
|
+
this.knownThreads = [];
|
|
1021
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
1022
|
+
// key = agentId
|
|
1023
|
+
/**
|
|
1024
|
+
* Ambient (locally-driven) turn accumulators, keyed by agentId. Populated only
|
|
1025
|
+
* when an online agent produces output that was NOT triggered by a Teams dispatch
|
|
1026
|
+
* (i.e. someone drove the agent in the app's own terminal). Streams those turns
|
|
1027
|
+
* into the bound thread so the channel mirrors everything the online agent does,
|
|
1028
|
+
* not just replies to Teams-originated requests. Disjoint from {@link pending}:
|
|
1029
|
+
* an agent with an in-flight Teams dispatch never uses this map.
|
|
1030
|
+
*/
|
|
1031
|
+
this.ambient = /* @__PURE__ */ new Map();
|
|
1032
|
+
// key = agentId
|
|
1033
|
+
/**
|
|
1034
|
+
* Message ids of every message the app has posted (thread roots + replies).
|
|
1035
|
+
* Primary self-loop guard: an inbound message whose id is here is our own echo
|
|
1036
|
+
* and is dropped before all other processing. Deterministic — does not depend on
|
|
1037
|
+
* content markers surviving Teams' sanitizer. Capped FIFO to bound memory.
|
|
1038
|
+
*/
|
|
1039
|
+
this.postedMessageIds = /* @__PURE__ */ new Set();
|
|
1040
|
+
this.postedOrder = [];
|
|
1041
|
+
this.unsubEvent = null;
|
|
1042
|
+
this.unsubExit = null;
|
|
1043
|
+
this.reconcileTimer = null;
|
|
1044
|
+
this.started = false;
|
|
1045
|
+
this.now = deps.now ?? Date.now;
|
|
1046
|
+
this.settleMs = deps.turnSettleMs ?? TURN_SETTLE_MS;
|
|
1047
|
+
this.filter = new MessageFilter(this.now);
|
|
1048
|
+
this.queue = new DispatchQueue((item) => this.processDispatch(item));
|
|
1049
|
+
}
|
|
1050
|
+
static {
|
|
1051
|
+
this.MAX_POSTED_IDS = 2e3;
|
|
1052
|
+
}
|
|
1053
|
+
// ── Lifecycle ────────────────────────────────────────────────
|
|
1054
|
+
async start() {
|
|
1055
|
+
if (this.started) return;
|
|
1056
|
+
this.started = true;
|
|
1057
|
+
tlog("Service starting\u2026");
|
|
1058
|
+
const state = await this.deps.store.load();
|
|
1059
|
+
const { kept, removed } = gcStale(state.bindings, this.now());
|
|
1060
|
+
this.bindings = kept.map((b) => ({ ...b, online: false }));
|
|
1061
|
+
this.knownThreads = state.knownThreads;
|
|
1062
|
+
tlog(`Loaded ${this.bindings.length} persisted binding(s), ${this.knownThreads.length} known thread(s).`);
|
|
1063
|
+
if (removed.length > 0) {
|
|
1064
|
+
await this.persist();
|
|
1065
|
+
tlog(`GC removed ${removed.length} stale binding(s) (>30 days).`);
|
|
1066
|
+
this.deps.emitToast({
|
|
1067
|
+
level: "info",
|
|
1068
|
+
message: `Teams: cleaned up ${removed.length} stale agent binding(s) (>30 days).`
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
this.unsubEvent = this.deps.gateway.onAgentEvent((e) => this.onAgentEvent(e));
|
|
1072
|
+
this.unsubExit = this.deps.gateway.onSessionExit((agentId) => this.onSessionExit(agentId));
|
|
1073
|
+
await this.deps.source.start((m) => {
|
|
1074
|
+
void this.handleInbound(m);
|
|
478
1075
|
});
|
|
479
|
-
|
|
1076
|
+
tlog(`Receive transport started (health=${this.deps.source.health}).`);
|
|
1077
|
+
this.reconcileTimer = setInterval(() => void this.reconcile(), RECONCILE_MS);
|
|
1078
|
+
void this.reconcile();
|
|
480
1079
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
nodeIntegration: false,
|
|
491
|
-
backgroundThrottling: false
|
|
1080
|
+
async stop() {
|
|
1081
|
+
this.started = false;
|
|
1082
|
+
if (this.reconcileTimer) clearInterval(this.reconcileTimer);
|
|
1083
|
+
this.reconcileTimer = null;
|
|
1084
|
+
this.unsubEvent?.();
|
|
1085
|
+
this.unsubExit?.();
|
|
1086
|
+
this.unsubEvent = this.unsubExit = null;
|
|
1087
|
+
for (const rec of this.ambient.values()) {
|
|
1088
|
+
if (rec.settleTimer) clearTimeout(rec.settleTimer);
|
|
492
1089
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
1090
|
+
this.ambient.clear();
|
|
1091
|
+
for (const b of this.bindings) {
|
|
1092
|
+
if (b.online) this.deps.gateway.setForwarding(b.officeId, b.agentId, false);
|
|
1093
|
+
}
|
|
1094
|
+
for (const [agentId, rec] of this.pending) {
|
|
1095
|
+
this.queue.clear(rec.officeId, agentId);
|
|
1096
|
+
this.cancelSettle(rec);
|
|
1097
|
+
rec.resolve();
|
|
1098
|
+
this.pending.delete(agentId);
|
|
1099
|
+
}
|
|
1100
|
+
await this.deps.source.stop();
|
|
1101
|
+
tlog("Service stopped.");
|
|
498
1102
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
1103
|
+
// ── Public API (invoked from IPC) ────────────────────────────
|
|
1104
|
+
getStatuses() {
|
|
1105
|
+
return this.bindings.map((b) => this.toStatus(b));
|
|
1106
|
+
}
|
|
1107
|
+
getStatus(officeId, agentId) {
|
|
1108
|
+
const b = this.findBinding(officeId, agentId);
|
|
1109
|
+
return b ? this.toStatus(b) : null;
|
|
1110
|
+
}
|
|
1111
|
+
/** Bring an agent online: resolve channel, create thread, bind, start listening. */
|
|
1112
|
+
async register(ctx) {
|
|
1113
|
+
const { officeId, agentId } = ctx;
|
|
1114
|
+
const settings = this.deps.getSettings();
|
|
1115
|
+
if (!settings.enabled) return { success: false, error: "Teams remote is disabled in settings." };
|
|
1116
|
+
const channelUrl = resolveChannel({ teamsChannelUrl: ctx.officeChannelUrl }, settings);
|
|
1117
|
+
const coords = parseChannelLink(channelUrl);
|
|
1118
|
+
if (!coords) {
|
|
1119
|
+
return { success: false, error: "no-channel" };
|
|
510
1120
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
if (
|
|
514
|
-
|
|
515
|
-
watcherProcess = null;
|
|
1121
|
+
tlog(`Register requested: ${ctx.displayName} (${officeId}:${agentId}) \u2192 channel ${coords.channelId}`);
|
|
1122
|
+
const sessionId = await this.deps.gateway.getSessionId(officeId, agentId);
|
|
1123
|
+
if (!sessionId) {
|
|
1124
|
+
return { success: false, error: "No active session for this agent. Open its terminal first." };
|
|
516
1125
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
1126
|
+
const displayName = ctx.displayName || agentId;
|
|
1127
|
+
const workingDir = ctx.workingDir || "";
|
|
1128
|
+
const existing = this.findBinding(officeId, agentId);
|
|
1129
|
+
if (existing && existing.online) {
|
|
1130
|
+
return { success: true, handle: existing.handle, threadWebUrl: existing.threadWebUrl };
|
|
1131
|
+
}
|
|
1132
|
+
const meta = await this.deps.gateway.getSessionMeta(officeId, agentId);
|
|
1133
|
+
const sessionTitle = meta?.title?.trim() || "";
|
|
1134
|
+
const base = normalizeHandle(displayName);
|
|
1135
|
+
let handle;
|
|
1136
|
+
try {
|
|
1137
|
+
handle = assignHandle(base, this.onlineHandles());
|
|
1138
|
+
} catch {
|
|
1139
|
+
return { success: false, error: "Could not derive a valid handle from the agent name." };
|
|
1140
|
+
}
|
|
1141
|
+
const subject = sessionTitle ? `${displayName}: ${sessionTitle}` : `${displayName}: ${handle}`;
|
|
1142
|
+
const introHtml = this.buildIntro({ displayName, workingDir }, handle, sessionTitle);
|
|
1143
|
+
let thread;
|
|
1144
|
+
try {
|
|
1145
|
+
thread = await this.deps.graph.createThread({
|
|
1146
|
+
teamId: coords.teamId,
|
|
1147
|
+
channelId: coords.channelId,
|
|
1148
|
+
subject,
|
|
1149
|
+
html: introHtml
|
|
1150
|
+
});
|
|
1151
|
+
} catch (e) {
|
|
1152
|
+
return { success: false, error: `Failed to create Teams thread: ${e.message}` };
|
|
1153
|
+
}
|
|
1154
|
+
tlog(`Thread created (root=${thread.threadRootId}) for @${handle}.`);
|
|
1155
|
+
this.rememberPosted(thread.threadRootId);
|
|
1156
|
+
const binding = {
|
|
1157
|
+
agentId,
|
|
1158
|
+
officeId,
|
|
1159
|
+
sessionId,
|
|
1160
|
+
handle,
|
|
1161
|
+
displayName,
|
|
1162
|
+
workingDir,
|
|
1163
|
+
sessionTitle,
|
|
1164
|
+
teamId: coords.teamId,
|
|
1165
|
+
channelId: coords.channelId,
|
|
1166
|
+
tenantId: coords.tenantId,
|
|
1167
|
+
threadRootId: thread.threadRootId,
|
|
1168
|
+
threadWebUrl: thread.webUrl,
|
|
1169
|
+
online: true,
|
|
1170
|
+
lastConnected: this.now()
|
|
1171
|
+
};
|
|
1172
|
+
this.bindings = this.bindings.filter((b) => !(b.officeId === officeId && b.agentId === agentId));
|
|
1173
|
+
this.bindings.push(binding);
|
|
1174
|
+
this.rememberThread(thread.threadRootId);
|
|
1175
|
+
await this.persist();
|
|
1176
|
+
this.updateSourceChannels();
|
|
1177
|
+
this.deps.gateway.setForwarding(officeId, agentId, true);
|
|
1178
|
+
this.deps.emitStatus(this.toStatus(binding));
|
|
1179
|
+
tlog(`ONLINE: @${handle} (${officeId}:${agentId}). Active channels: ${activeChannelSet(this.bindings).size}.`);
|
|
1180
|
+
return { success: true, handle, threadWebUrl: thread.webUrl };
|
|
1181
|
+
}
|
|
1182
|
+
/** Take an agent offline (connection only; session untouched). */
|
|
1183
|
+
async goOffline(officeId, agentId, postNotice = true) {
|
|
1184
|
+
const b = this.findBinding(officeId, agentId);
|
|
1185
|
+
if (!b) return { success: true };
|
|
1186
|
+
tlog(`OFFLINE: @${b.handle} (${officeId}:${agentId}).`);
|
|
1187
|
+
if (postNotice && b.online && b.threadRootId) {
|
|
1188
|
+
await this.safeReply(b, "\u{1F50C} This agent has gone offline. Replies here will not be answered.");
|
|
1189
|
+
}
|
|
1190
|
+
this.queue.clear(officeId, agentId);
|
|
1191
|
+
this.deps.gateway.setForwarding(officeId, agentId, false);
|
|
1192
|
+
const amb = this.ambient.get(agentId);
|
|
1193
|
+
if (amb) {
|
|
1194
|
+
if (amb.settleTimer) clearTimeout(amb.settleTimer);
|
|
1195
|
+
this.ambient.delete(agentId);
|
|
1196
|
+
}
|
|
1197
|
+
const inFlight = this.pending.get(agentId);
|
|
1198
|
+
if (inFlight) {
|
|
1199
|
+
this.cancelSettle(inFlight);
|
|
1200
|
+
this.pending.delete(agentId);
|
|
1201
|
+
inFlight.resolve();
|
|
1202
|
+
}
|
|
1203
|
+
this.bindings = this.bindings.filter((x) => !(x.officeId === officeId && x.agentId === agentId));
|
|
1204
|
+
await this.persist();
|
|
1205
|
+
this.updateSourceChannels();
|
|
1206
|
+
this.deps.emitStatus({ agentId, officeId, online: false, handle: b.handle, health: "disconnected" });
|
|
1207
|
+
return { success: true };
|
|
1208
|
+
}
|
|
1209
|
+
// ── Inbound handling ─────────────────────────────────────────
|
|
1210
|
+
async handleInbound(msg) {
|
|
1211
|
+
if (msg.messageId && this.postedMessageIds.has(msg.messageId)) {
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
const result = this.filter.evaluate(msg, this.bindings, this.knownThreads);
|
|
1215
|
+
if (result.action === "ignore") {
|
|
1216
|
+
if (result.reason && result.reason !== "duplicate" && result.reason !== "root-message" && result.reason !== "inactive-channel") {
|
|
1217
|
+
tlog(`Ignored inbound (${result.reason}) from "${msg.senderName}".`);
|
|
1218
|
+
}
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
if (result.action === "orphaned-notice") {
|
|
1222
|
+
tlog(`Orphaned thread ${msg.threadRootId} messaged by "${msg.senderName}" \u2014 posting inactive notice.`);
|
|
1223
|
+
await this.postOrphanedNotice(msg);
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
const binding = result.binding;
|
|
1227
|
+
if (!binding) return;
|
|
1228
|
+
const content = msg.content.trim();
|
|
1229
|
+
if (content === "/stop") {
|
|
1230
|
+
tlog(`/stop received in @${binding.handle}'s thread \u2014 taking offline.`);
|
|
1231
|
+
await this.goOffline(binding.officeId, binding.agentId, true);
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
tlog(`Dispatch: "${msg.senderName}" \u2192 @${binding.handle} (queued=${this.queue.pending(binding.officeId, binding.agentId)}): ${truncate(msg.content, 80)}`);
|
|
1235
|
+
this.queue.enqueue({
|
|
1236
|
+
officeId: binding.officeId,
|
|
1237
|
+
agentId: binding.agentId,
|
|
1238
|
+
sessionId: binding.sessionId,
|
|
1239
|
+
threadRootId: binding.threadRootId,
|
|
1240
|
+
prompt: msg.content,
|
|
1241
|
+
senderName: msg.senderName
|
|
1242
|
+
});
|
|
1243
|
+
if (this.deps.getSettings().ackEnabled) {
|
|
1244
|
+
void this.safeReply(binding, `${this.agentLabel(binding)} \u231B ${escapeHtml(pickAckQuip())} <i>(message received)</i>`);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
async postOrphanedNotice(msg) {
|
|
1248
|
+
const known = this.knownThreads.find((t) => t.threadRootId === msg.threadRootId);
|
|
1249
|
+
if (!known || known.noticePosted) return;
|
|
1250
|
+
known.noticePosted = true;
|
|
1251
|
+
await this.persist();
|
|
1252
|
+
const coords = this.coordsForChannel(msg.channelId);
|
|
1253
|
+
if (!coords) return;
|
|
1254
|
+
try {
|
|
1255
|
+
const posted = await this.deps.graph.replyToThread({
|
|
1256
|
+
teamId: coords.teamId,
|
|
1257
|
+
channelId: msg.channelId,
|
|
1258
|
+
threadRootId: msg.threadRootId,
|
|
1259
|
+
html: "\u2139\uFE0F This thread is no longer active and will not receive responses."
|
|
1260
|
+
});
|
|
1261
|
+
if (posted?.messageId) this.rememberPosted(posted.messageId);
|
|
1262
|
+
} catch (e) {
|
|
1263
|
+
twarn("Failed to post orphaned-thread notice:", e.message);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
// ── Dispatch + response capture ──────────────────────────────
|
|
1267
|
+
processDispatch(item) {
|
|
1268
|
+
return new Promise((resolve2) => {
|
|
1269
|
+
const binding = this.findBinding(item.officeId, item.agentId);
|
|
1270
|
+
if (!this.started || !binding || !binding.online) {
|
|
1271
|
+
resolve2();
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
const record = {
|
|
1275
|
+
officeId: item.officeId,
|
|
1276
|
+
agentId: item.agentId,
|
|
1277
|
+
binding,
|
|
1278
|
+
chunks: [],
|
|
1279
|
+
resolve: resolve2,
|
|
1280
|
+
startedAt: this.now(),
|
|
1281
|
+
lastCheckIn: this.now(),
|
|
1282
|
+
settleTimer: null
|
|
1283
|
+
};
|
|
1284
|
+
this.pending.set(item.agentId, record);
|
|
1285
|
+
this.deps.gateway.setForwarding(item.officeId, item.agentId, true);
|
|
1286
|
+
const label = item.senderName ? `Teams \xB7 ${item.senderName}` : "Teams";
|
|
1287
|
+
this.deps.gateway.submitPrompt(item.officeId, item.agentId, item.prompt, label).catch((e) => {
|
|
1288
|
+
twarn("submitPrompt failed:", e.message);
|
|
1289
|
+
this.pending.delete(item.agentId);
|
|
1290
|
+
resolve2();
|
|
1291
|
+
});
|
|
1292
|
+
setTimeout(() => {
|
|
1293
|
+
if (this.pending.get(item.agentId) === record) {
|
|
1294
|
+
void this.finalizeDispatch(item.agentId);
|
|
1295
|
+
}
|
|
1296
|
+
}, 10 * 60 * 1e3);
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
onAgentEvent(e) {
|
|
1300
|
+
const rec = this.pending.get(e.agentId);
|
|
1301
|
+
if (!rec) {
|
|
1302
|
+
this.onAmbientEvent(e);
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
if (e.kind === "message" && e.content) {
|
|
1306
|
+
this.cancelSettle(rec);
|
|
1307
|
+
rec.chunks.push(e.content);
|
|
1308
|
+
} else if (e.kind === "turn-start") {
|
|
1309
|
+
this.cancelSettle(rec);
|
|
1310
|
+
} else if (e.kind === "user-message") {
|
|
1311
|
+
return;
|
|
1312
|
+
} else if (e.kind === "turn-end") {
|
|
1313
|
+
void this.flushTurn(rec);
|
|
1314
|
+
this.scheduleSettle(e.agentId, rec);
|
|
1315
|
+
} else if (e.kind === "tool-start") {
|
|
1316
|
+
this.cancelSettle(rec);
|
|
1317
|
+
void this.maybeCheckIn(rec, e.toolName);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
/** Clear the pending dispatch close-out timer, if armed. */
|
|
1321
|
+
cancelSettle(rec) {
|
|
1322
|
+
if (rec.settleTimer) {
|
|
1323
|
+
clearTimeout(rec.settleTimer);
|
|
1324
|
+
rec.settleTimer = null;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
/** (Re)arm the debounce that closes out the dispatch once the agent goes idle. */
|
|
1328
|
+
scheduleSettle(agentId, rec) {
|
|
1329
|
+
this.cancelSettle(rec);
|
|
1330
|
+
rec.settleTimer = setTimeout(() => {
|
|
1331
|
+
rec.settleTimer = null;
|
|
1332
|
+
void this.finalizeDispatch(agentId);
|
|
1333
|
+
}, this.settleMs);
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Post everything accumulated for the current turn as a Teams reply, then clear
|
|
1337
|
+
* the buffer. Does NOT stop forwarding or end the dispatch — a multi-turn
|
|
1338
|
+
* response keeps flushing per turn. No-op when the turn produced no text.
|
|
1339
|
+
*/
|
|
1340
|
+
async flushTurn(rec) {
|
|
1341
|
+
const text = rec.chunks.join("\n\n").trim();
|
|
1342
|
+
rec.chunks = [];
|
|
1343
|
+
if (!text) return;
|
|
1344
|
+
const elapsed = Math.round((this.now() - rec.startedAt) / 1e3);
|
|
1345
|
+
tlog(`Reply \u2192 @${rec.binding.handle} thread (${text.length} chars, ${elapsed}s): ${truncate(text, 80)}`);
|
|
1346
|
+
rec.lastReplyText = text;
|
|
1347
|
+
await this.postReply(rec.binding, text);
|
|
1348
|
+
}
|
|
1349
|
+
// ── Ambient (locally-driven) turn streaming ──────────────────
|
|
1350
|
+
/**
|
|
1351
|
+
* Handle a copilot event for an online agent that has NO in-flight Teams dispatch —
|
|
1352
|
+
* i.e. the agent was driven from the app's own terminal. Streams the resulting
|
|
1353
|
+
* request + reply into the bound thread so the channel mirrors everything the online
|
|
1354
|
+
* agent does. Silently ignores agents that are offline / not bound.
|
|
1355
|
+
*/
|
|
1356
|
+
onAmbientEvent(e) {
|
|
1357
|
+
const binding = this.bindings.find((b) => b.agentId === e.agentId && b.online);
|
|
1358
|
+
if (!binding) return;
|
|
1359
|
+
if (e.kind === "user-message") {
|
|
1360
|
+
const text = (e.content ?? "").trim();
|
|
1361
|
+
if (text) void this.postLocalRequest(binding, text);
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
if (e.kind === "message" && e.content) {
|
|
1365
|
+
const rec = this.ensureAmbient(e.agentId, binding);
|
|
1366
|
+
this.cancelAmbientSettle(rec);
|
|
1367
|
+
rec.chunks.push(e.content);
|
|
1368
|
+
} else if (e.kind === "turn-start") {
|
|
1369
|
+
const rec = this.ensureAmbient(e.agentId, binding);
|
|
1370
|
+
this.cancelAmbientSettle(rec);
|
|
1371
|
+
} else if (e.kind === "turn-end") {
|
|
1372
|
+
const rec = this.ambient.get(e.agentId);
|
|
1373
|
+
if (!rec) return;
|
|
1374
|
+
void this.flushAmbient(rec);
|
|
1375
|
+
this.scheduleAmbientSettle(e.agentId, rec);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
ensureAmbient(agentId, binding) {
|
|
1379
|
+
let rec = this.ambient.get(agentId);
|
|
1380
|
+
if (!rec) {
|
|
1381
|
+
rec = {
|
|
1382
|
+
officeId: binding.officeId,
|
|
1383
|
+
agentId,
|
|
1384
|
+
binding,
|
|
1385
|
+
chunks: [],
|
|
1386
|
+
startedAt: this.now(),
|
|
1387
|
+
settleTimer: null
|
|
1388
|
+
};
|
|
1389
|
+
this.ambient.set(agentId, rec);
|
|
1390
|
+
}
|
|
1391
|
+
return rec;
|
|
1392
|
+
}
|
|
1393
|
+
cancelAmbientSettle(rec) {
|
|
1394
|
+
if (rec.settleTimer) {
|
|
1395
|
+
clearTimeout(rec.settleTimer);
|
|
1396
|
+
rec.settleTimer = null;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
scheduleAmbientSettle(agentId, rec) {
|
|
1400
|
+
this.cancelAmbientSettle(rec);
|
|
1401
|
+
rec.settleTimer = setTimeout(() => {
|
|
1402
|
+
rec.settleTimer = null;
|
|
1403
|
+
void this.finalizeAmbient(agentId);
|
|
1404
|
+
}, this.settleMs);
|
|
1405
|
+
}
|
|
1406
|
+
/** Post the current ambient turn's accumulated text; clears the buffer. No-op when empty. */
|
|
1407
|
+
async flushAmbient(rec) {
|
|
1408
|
+
const text = rec.chunks.join("\n\n").trim();
|
|
1409
|
+
rec.chunks = [];
|
|
1410
|
+
if (!text) return;
|
|
1411
|
+
const elapsed = Math.round((this.now() - rec.startedAt) / 1e3);
|
|
1412
|
+
tlog(`Local reply \u2192 @${rec.binding.handle} thread (${text.length} chars, ${elapsed}s): ${truncate(text, 80)}`);
|
|
1413
|
+
await this.postReply(rec.binding, text);
|
|
1414
|
+
}
|
|
1415
|
+
/** Close out an ambient turn once the agent goes idle: flush residual text, drop record. */
|
|
1416
|
+
async finalizeAmbient(agentId) {
|
|
1417
|
+
const rec = this.ambient.get(agentId);
|
|
1418
|
+
if (!rec) return;
|
|
1419
|
+
this.cancelAmbientSettle(rec);
|
|
1420
|
+
this.ambient.delete(agentId);
|
|
1421
|
+
await this.flushAmbient(rec);
|
|
1422
|
+
}
|
|
1423
|
+
/** Post a locally-typed user request into the thread, tagged so it's distinct from replies. */
|
|
1424
|
+
async postLocalRequest(binding, text) {
|
|
1425
|
+
const chunks = chunkReply(text, 3500);
|
|
1426
|
+
for (const chunk of chunks) {
|
|
1427
|
+
await this.safeReply(
|
|
1428
|
+
binding,
|
|
1429
|
+
`\u{1F464} <b>Human</b> \u{1F4AC} <i>local request:</i><br>${escapeHtml(chunk).replace(/\n/g, "<br>")}`
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* Close out a dispatch once the agent has gone idle: flush any residual text,
|
|
1435
|
+
* drop the pending record, and resolve the dispatch promise so the per-agent
|
|
1436
|
+
* queue advances. Event mirroring stays on for the agent's whole online lifetime
|
|
1437
|
+
* (it's toggled in register/reconnect ↔ goOffline), so it is NOT disabled here.
|
|
1438
|
+
*/
|
|
1439
|
+
async finalizeDispatch(agentId) {
|
|
1440
|
+
const rec = this.pending.get(agentId);
|
|
1441
|
+
if (!rec) return;
|
|
1442
|
+
this.cancelSettle(rec);
|
|
1443
|
+
this.pending.delete(agentId);
|
|
1444
|
+
await this.flushTurn(rec);
|
|
1445
|
+
await this.maybeNotifyComplete(rec);
|
|
1446
|
+
rec.resolve();
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Post a single end-of-response notification via the distinct-identity {@link
|
|
1450
|
+
* TeamsServiceDeps.notifier} (relay/Dump channel) so a Power Automate flow re-posts it
|
|
1451
|
+
* under the Flow-bot identity with an @mention. Active only when a notifier is wired and
|
|
1452
|
+
* {@link TeamsServiceDeps.isNotifyActive} is true. Skips silently when the dispatch
|
|
1453
|
+
* produced no text. Never throws — a notification failure must not wedge the queue.
|
|
1454
|
+
*/
|
|
1455
|
+
async maybeNotifyComplete(rec) {
|
|
1456
|
+
const notifier = this.deps.notifier;
|
|
1457
|
+
if (!notifier || !(this.deps.isNotifyActive?.() ?? false)) return;
|
|
1458
|
+
if (!rec.lastReplyText) return;
|
|
1459
|
+
const title = (rec.binding.sessionTitle || "").trim();
|
|
1460
|
+
const where = title ? ` in \u201C${escapeHtml(title)}\u201D` : "";
|
|
1461
|
+
const html = `${this.agentLabel(rec.binding)} has finished responding${where}`;
|
|
1462
|
+
try {
|
|
1463
|
+
const posted = await notifier.replyToThread({
|
|
1464
|
+
teamId: rec.binding.teamId,
|
|
1465
|
+
channelId: rec.binding.channelId,
|
|
1466
|
+
threadRootId: rec.binding.threadRootId,
|
|
1467
|
+
html
|
|
1468
|
+
});
|
|
1469
|
+
if (posted?.messageId) this.rememberPosted(posted.messageId);
|
|
1470
|
+
} catch (e) {
|
|
1471
|
+
twarn("completion notify failed:", e.message);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
async maybeCheckIn(rec, toolName) {
|
|
1475
|
+
const settings = this.deps.getSettings();
|
|
1476
|
+
if (!settings.checkInEnabled) return;
|
|
1477
|
+
const t = this.now();
|
|
1478
|
+
if (t - rec.startedAt < settings.checkInThresholdMs) return;
|
|
1479
|
+
if (t - rec.lastCheckIn < settings.checkInThrottleMs) return;
|
|
1480
|
+
rec.lastCheckIn = t;
|
|
1481
|
+
const label = toolName ? ` (running: ${escapeHtml(toolName)})` : "";
|
|
1482
|
+
await this.safeReply(rec.binding, `${this.agentLabel(rec.binding)} \u23F3 Still working\u2026${label}`);
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* Bold agent-name prefix for every app-posted message. Since replies are posted
|
|
1486
|
+
* under the operator's own Teams identity, this makes automated agent output
|
|
1487
|
+
* visually distinct from messages the operator typed by hand.
|
|
1488
|
+
*/
|
|
1489
|
+
agentLabel(binding) {
|
|
1490
|
+
return `\u{1F916} <b>${escapeHtml(binding.displayName)}</b>`;
|
|
1491
|
+
}
|
|
1492
|
+
async postReply(binding, text) {
|
|
1493
|
+
const prefix = this.agentLabel(binding);
|
|
1494
|
+
const { text: cleaned, paths } = extractImageMarkers(text);
|
|
1495
|
+
let images = [];
|
|
1496
|
+
if (paths.length) {
|
|
1497
|
+
tlog(`office-image: @${binding.handle} reply has ${paths.length} sentinel(s): ${paths.join(", ")} (baseDir=${binding.workingDir})`);
|
|
1498
|
+
images = await loadHostedImages(paths, { baseDir: binding.workingDir, warn: (m) => twarn(m) });
|
|
1499
|
+
if (images.length) {
|
|
1500
|
+
tlog(`office-image: loaded ${images.length}/${paths.length} image(s) for @${binding.handle} \u2014 will attach inline.`);
|
|
1501
|
+
} else {
|
|
1502
|
+
twarn(`office-image: no images loaded for @${binding.handle} despite ${paths.length} sentinel(s) \u2014 all paths rejected (see warnings above).`);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
if (cleaned) {
|
|
1506
|
+
const chunks = chunkReply(cleaned, 3500);
|
|
1507
|
+
for (const chunk of chunks) {
|
|
1508
|
+
await this.safeReply(binding, `${prefix}<br>${escapeHtml(chunk).replace(/\n/g, "<br>")}`);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
if (images.length) {
|
|
1512
|
+
tlog(`office-image: posting ${images.length} inline image reply to @${binding.handle} thread.`);
|
|
1513
|
+
await this.safeReply(binding, `${prefix}<br>${hostedImagesHtml(images)}`, images);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
/** Reply to a thread, swallowing errors (logs only) so the queue keeps moving. */
|
|
1517
|
+
async safeReply(binding, html, hostedImages) {
|
|
1518
|
+
try {
|
|
1519
|
+
const posted = await this.deps.graph.replyToThread({
|
|
1520
|
+
teamId: binding.teamId,
|
|
1521
|
+
channelId: binding.channelId,
|
|
1522
|
+
threadRootId: binding.threadRootId,
|
|
1523
|
+
html,
|
|
1524
|
+
hostedImages
|
|
1525
|
+
});
|
|
1526
|
+
if (posted?.messageId) this.rememberPosted(posted.messageId);
|
|
1527
|
+
} catch (e) {
|
|
1528
|
+
twarn("replyToThread failed:", e.message);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
// ── Reconnect / teardown reconcile (FR-022/024) ──────────────
|
|
1532
|
+
/**
|
|
1533
|
+
* Run a reconcile pass on demand (e.g. right after the renderer re-attaches
|
|
1534
|
+
* terminal sessions on startup / office switch), so Teams bindings re-online
|
|
1535
|
+
* immediately instead of waiting for the next periodic tick. No-op until started.
|
|
1536
|
+
*/
|
|
1537
|
+
async reconcileNow() {
|
|
1538
|
+
if (!this.started) return;
|
|
1539
|
+
await this.reconcile();
|
|
1540
|
+
}
|
|
1541
|
+
async reconcile() {
|
|
1542
|
+
let changed = false;
|
|
1543
|
+
for (const b of [...this.bindings]) {
|
|
1544
|
+
if (!this.started) return;
|
|
1545
|
+
let current = null;
|
|
1546
|
+
try {
|
|
1547
|
+
current = await this.deps.gateway.getSessionId(b.officeId, b.agentId);
|
|
1548
|
+
} catch {
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
if (!this.started) return;
|
|
1552
|
+
if (b.online) {
|
|
1553
|
+
if (current !== b.sessionId) {
|
|
1554
|
+
tlog(`Session changed for @${b.handle} (${b.sessionId} \u2192 ${current ?? "none"}) \u2014 taking offline (FR-022).`);
|
|
1555
|
+
await this.goOffline(b.officeId, b.agentId, true);
|
|
1556
|
+
changed = true;
|
|
1557
|
+
} else {
|
|
1558
|
+
b.lastConnected = this.now();
|
|
1559
|
+
}
|
|
1560
|
+
} else {
|
|
1561
|
+
if (current && current === b.sessionId) {
|
|
1562
|
+
const ready = await this.deps.gateway.isAgentReady(b.officeId, b.agentId).catch(() => false);
|
|
1563
|
+
if (!ready) continue;
|
|
1564
|
+
if (!this.started) return;
|
|
1565
|
+
b.online = true;
|
|
1566
|
+
b.lastConnected = this.now();
|
|
1567
|
+
this.deps.gateway.setForwarding(b.officeId, b.agentId, true);
|
|
1568
|
+
this.deps.emitStatus(this.toStatus(b));
|
|
1569
|
+
tlog(`Reconnected @${b.handle} to its persisted thread (session ${b.sessionId}).`);
|
|
1570
|
+
void this.safeReply(b, `${this.agentLabel(b)} \u{1F504} Reconnected \u2014 back online and ready. Reply here to continue.`);
|
|
1571
|
+
changed = true;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
if (changed) {
|
|
1576
|
+
await this.persist();
|
|
1577
|
+
this.updateSourceChannels();
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
async onSessionExit(agentId) {
|
|
1581
|
+
const b = this.bindings.find((x) => x.agentId === agentId && x.online);
|
|
1582
|
+
if (b) {
|
|
1583
|
+
tlog(`Session exited for @${b.handle} \u2014 taking offline.`);
|
|
1584
|
+
await this.goOffline(b.officeId, b.agentId, true);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
// ── Helpers ──────────────────────────────────────────────────
|
|
1588
|
+
buildIntro(info, handle, sessionTitle) {
|
|
1589
|
+
const lines = [
|
|
1590
|
+
`<p>\u{1F7E2} <b>${escapeHtml(info.displayName)}</b> is now online via Copilot Office.</p>`,
|
|
1591
|
+
`<p>Reply in this thread to talk to the agent. Send <code>/stop</code> to take it offline.</p>`,
|
|
1592
|
+
`<ul>`,
|
|
1593
|
+
`<li><b>Handle:</b> ${escapeHtml(handle)}</li>`,
|
|
1594
|
+
`<li><b>Folder:</b> ${escapeHtml(info.workingDir)}</li>`
|
|
1595
|
+
];
|
|
1596
|
+
if (sessionTitle) lines.push(`<li><b>Session:</b> ${escapeHtml(sessionTitle)}</li>`);
|
|
1597
|
+
lines.push(`</ul>`);
|
|
1598
|
+
return lines.join("");
|
|
1599
|
+
}
|
|
1600
|
+
toStatus(b) {
|
|
1601
|
+
return {
|
|
1602
|
+
agentId: b.agentId,
|
|
1603
|
+
officeId: b.officeId,
|
|
1604
|
+
online: b.online,
|
|
1605
|
+
handle: b.handle,
|
|
1606
|
+
threadWebUrl: b.threadWebUrl,
|
|
1607
|
+
health: b.online ? this.deps.source.health : "disconnected"
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
findBinding(officeId, agentId) {
|
|
1611
|
+
return this.bindings.find((b) => b.officeId === officeId && b.agentId === agentId);
|
|
1612
|
+
}
|
|
1613
|
+
onlineHandles() {
|
|
1614
|
+
return new Set(this.bindings.filter((b) => b.online).map((b) => b.handle));
|
|
1615
|
+
}
|
|
1616
|
+
rememberThread(threadRootId) {
|
|
1617
|
+
if (!this.knownThreads.some((t) => t.threadRootId === threadRootId)) {
|
|
1618
|
+
this.knownThreads.push({ threadRootId, noticePosted: false });
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
/** Record a message id the app posted, so its Trouter echo is dropped (D9). Capped FIFO. */
|
|
1622
|
+
rememberPosted(messageId) {
|
|
1623
|
+
if (!messageId || this.postedMessageIds.has(messageId)) return;
|
|
1624
|
+
this.postedMessageIds.add(messageId);
|
|
1625
|
+
this.postedOrder.push(messageId);
|
|
1626
|
+
if (this.postedOrder.length > _TeamsService.MAX_POSTED_IDS) {
|
|
1627
|
+
const old = this.postedOrder.shift();
|
|
1628
|
+
if (old) this.postedMessageIds.delete(old);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
coordsForChannel(channelId) {
|
|
1632
|
+
const b = this.bindings.find((x) => x.channelId === channelId);
|
|
1633
|
+
if (b) return { teamId: b.teamId, tenantId: b.tenantId };
|
|
1634
|
+
return null;
|
|
1635
|
+
}
|
|
1636
|
+
/** Push the current active-channel set to a pollable source (chatsvc fallback). */
|
|
1637
|
+
updateSourceChannels() {
|
|
1638
|
+
const src = this.deps.source;
|
|
1639
|
+
if (typeof src.setChannels === "function") {
|
|
1640
|
+
src.setChannels([...activeChannelSet(this.bindings)]);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
async persist() {
|
|
1644
|
+
await this.deps.store.save({ bindings: this.bindings, knownThreads: this.knownThreads });
|
|
1645
|
+
}
|
|
1646
|
+
};
|
|
1647
|
+
function truncate(text, max) {
|
|
1648
|
+
const oneLine = (text ?? "").replace(/\s+/g, " ").trim();
|
|
1649
|
+
return oneLine.length <= max ? oneLine : `${oneLine.slice(0, max)}\u2026`;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
// electron/teams/auth.ts
|
|
1653
|
+
var import_child_process3 = require("child_process");
|
|
1654
|
+
var RESOURCE_URLS = {
|
|
1655
|
+
graph: "https://graph.microsoft.com",
|
|
1656
|
+
ic3: "https://ic3.teams.office.com"
|
|
1657
|
+
};
|
|
1658
|
+
var REFRESH_BUFFER_MS = 5 * 60 * 1e3;
|
|
1659
|
+
function decodeJwtExpMs(token) {
|
|
1660
|
+
try {
|
|
1661
|
+
const parts = token.split(".");
|
|
1662
|
+
if (parts.length < 2) return 0;
|
|
1663
|
+
const payload = JSON.parse(
|
|
1664
|
+
Buffer.from(parts[1].replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf-8")
|
|
1665
|
+
);
|
|
1666
|
+
const exp = Number(payload?.exp);
|
|
1667
|
+
return Number.isFinite(exp) ? exp * 1e3 : 0;
|
|
1668
|
+
} catch {
|
|
1669
|
+
return 0;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
var defaultAzRunner = (resourceUrl) => new Promise((resolve2, reject) => {
|
|
1673
|
+
const isWin = process.platform === "win32";
|
|
1674
|
+
const azArgs = ["account", "get-access-token", "--resource", resourceUrl, "--output", "json"];
|
|
1675
|
+
const [file, args] = isWin ? [process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", `az ${azArgs.join(" ")}`]] : ["az", azArgs];
|
|
1676
|
+
(0, import_child_process3.execFile)(
|
|
1677
|
+
file,
|
|
1678
|
+
args,
|
|
1679
|
+
{ windowsHide: true, maxBuffer: 1024 * 1024 },
|
|
1680
|
+
(err, stdout) => {
|
|
1681
|
+
if (err) {
|
|
1682
|
+
reject(new Error(`az token acquisition failed for ${resourceUrl} (${err.message})`));
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
try {
|
|
1686
|
+
const parsed = JSON.parse(stdout);
|
|
1687
|
+
const token = String(parsed?.accessToken || "");
|
|
1688
|
+
if (!token) {
|
|
1689
|
+
reject(new Error(`az returned no accessToken for ${resourceUrl}`));
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
resolve2(token);
|
|
1693
|
+
} catch {
|
|
1694
|
+
reject(new Error(`Failed to parse az token output for ${resourceUrl}`));
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
);
|
|
1698
|
+
});
|
|
1699
|
+
var AzTokenProvider = class {
|
|
1700
|
+
/**
|
|
1701
|
+
* `runner` is injectable for tests (defaults to the real `az` CLI). `persistence`
|
|
1702
|
+
* (optional) seeds the in-memory cache from an encrypted on-disk store at startup
|
|
1703
|
+
* and is updated on every successful acquisition, so a still-valid token survives
|
|
1704
|
+
* app restarts and avoids a slow `az` cold-start.
|
|
1705
|
+
*/
|
|
1706
|
+
constructor(runner = defaultAzRunner, persistence) {
|
|
1707
|
+
this.runner = runner;
|
|
1708
|
+
this.persistence = persistence;
|
|
1709
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
1710
|
+
if (persistence) {
|
|
1711
|
+
try {
|
|
1712
|
+
const loaded = persistence.load();
|
|
1713
|
+
for (const [res, ct] of Object.entries(loaded)) {
|
|
1714
|
+
if (ct && typeof ct.token === "string" && ct.token && typeof ct.expiresAt === "number") {
|
|
1715
|
+
this.cache.set(res, ct);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
} catch {
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
saveCache() {
|
|
1723
|
+
if (!this.persistence) return;
|
|
1724
|
+
try {
|
|
1725
|
+
const all = {};
|
|
1726
|
+
for (const [res, ct] of this.cache.entries()) all[res] = ct;
|
|
1727
|
+
this.persistence.save(all);
|
|
1728
|
+
} catch {
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
async getToken(resource) {
|
|
1732
|
+
const now = Date.now();
|
|
1733
|
+
const cached = this.cache.get(resource);
|
|
1734
|
+
if (cached && cached.expiresAt - now > REFRESH_BUFFER_MS) {
|
|
1735
|
+
return cached.token;
|
|
1736
|
+
}
|
|
1737
|
+
try {
|
|
1738
|
+
const token = await this.runner(RESOURCE_URLS[resource]);
|
|
1739
|
+
const expMs = decodeJwtExpMs(token);
|
|
1740
|
+
const expiresAt = expMs > 0 ? expMs : now + 30 * 60 * 1e3;
|
|
1741
|
+
this.cache.set(resource, { token, expiresAt });
|
|
1742
|
+
this.saveCache();
|
|
1743
|
+
tlog(`Acquired ${resource} token (expires ${new Date(expiresAt).toISOString()}).`);
|
|
1744
|
+
return token;
|
|
1745
|
+
} catch (e) {
|
|
1746
|
+
if (cached && cached.expiresAt > now) {
|
|
1747
|
+
twarn(`Token refresh failed for ${resource}; reusing cached token.`);
|
|
1748
|
+
return cached.token;
|
|
1749
|
+
}
|
|
1750
|
+
throw e;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
1754
|
+
|
|
1755
|
+
// electron/teams/tokenCacheStore.ts
|
|
1756
|
+
var fs5 = __toESM(require("fs"));
|
|
1757
|
+
var path6 = __toESM(require("path"));
|
|
1758
|
+
var RESOURCES = ["graph", "ic3"];
|
|
1759
|
+
function createSafeStorageTokenPersistence(filePath, safeStorage2, opts = {}) {
|
|
1760
|
+
const fsLike = opts.fsLike ?? fs5;
|
|
1761
|
+
const now = opts.now ?? Date.now;
|
|
1762
|
+
return {
|
|
1763
|
+
load() {
|
|
1764
|
+
try {
|
|
1765
|
+
if (!safeStorage2.isEncryptionAvailable()) return {};
|
|
1766
|
+
if (!fsLike.existsSync(filePath)) return {};
|
|
1767
|
+
const buf = fsLike.readFileSync(filePath);
|
|
1768
|
+
const json = safeStorage2.decryptString(buf);
|
|
1769
|
+
const parsed = JSON.parse(json);
|
|
1770
|
+
const out = {};
|
|
1771
|
+
for (const res of RESOURCES) {
|
|
1772
|
+
const ct = parsed?.[res];
|
|
1773
|
+
if (ct && typeof ct.token === "string" && ct.token && typeof ct.expiresAt === "number" && ct.expiresAt > now()) {
|
|
1774
|
+
out[res] = { token: ct.token, expiresAt: ct.expiresAt };
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
return out;
|
|
1778
|
+
} catch {
|
|
1779
|
+
try {
|
|
1780
|
+
fsLike.rmSync(filePath, { force: true });
|
|
1781
|
+
} catch {
|
|
1782
|
+
}
|
|
1783
|
+
return {};
|
|
1784
|
+
}
|
|
1785
|
+
},
|
|
1786
|
+
save(all) {
|
|
1787
|
+
try {
|
|
1788
|
+
if (!safeStorage2.isEncryptionAvailable()) return;
|
|
1789
|
+
const json = JSON.stringify(all);
|
|
1790
|
+
const enc = safeStorage2.encryptString(json);
|
|
1791
|
+
fsLike.mkdirSync(path6.dirname(filePath), { recursive: true });
|
|
1792
|
+
fsLike.writeFileSync(filePath, enc);
|
|
1793
|
+
} catch {
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
// electron/teams/marker.ts
|
|
1800
|
+
var TEAMS_MARKER = "copilotoffice-agent-post-v1";
|
|
1801
|
+
var ZW_MARKER = "\u200B\u200C\u200D\u200B\u200C\u200D";
|
|
1802
|
+
function embedMarker(html) {
|
|
1803
|
+
if (hasMarker(html)) return html;
|
|
1804
|
+
const body = html ?? "";
|
|
1805
|
+
const firstTag = body.match(/^\s*<[a-zA-Z][^>]*>/);
|
|
1806
|
+
if (firstTag) {
|
|
1807
|
+
const insertAt = firstTag.index + firstTag[0].length;
|
|
1808
|
+
return body.slice(0, insertAt) + ZW_MARKER + body.slice(insertAt);
|
|
1809
|
+
}
|
|
1810
|
+
return ZW_MARKER + body;
|
|
1811
|
+
}
|
|
1812
|
+
function hasMarker(content) {
|
|
1813
|
+
if (!content) return false;
|
|
1814
|
+
return content.includes(ZW_MARKER) || content.includes(TEAMS_MARKER);
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
// electron/teams/graphClient.ts
|
|
1818
|
+
function buildHostedContents(images) {
|
|
1819
|
+
if (!images || images.length === 0) return void 0;
|
|
1820
|
+
return images.map((img) => ({
|
|
1821
|
+
"@microsoft.graph.temporaryId": img.id,
|
|
1822
|
+
contentBytes: img.contentBytesBase64,
|
|
1823
|
+
contentType: img.contentType
|
|
1824
|
+
}));
|
|
1825
|
+
}
|
|
1826
|
+
var GRAPH_BASE = "https://graph.microsoft.com/v1.0";
|
|
1827
|
+
var GraphClient = class {
|
|
1828
|
+
constructor(tokens) {
|
|
1829
|
+
this.tokens = tokens;
|
|
1830
|
+
}
|
|
1831
|
+
async authHeaders() {
|
|
1832
|
+
const token = await this.tokens.getToken("graph");
|
|
1833
|
+
return {
|
|
1834
|
+
Authorization: `Bearer ${token}`,
|
|
1835
|
+
"Content-Type": "application/json"
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
async createThread(p) {
|
|
1839
|
+
const url = `${GRAPH_BASE}/teams/${encodeURIComponent(p.teamId)}/channels/${encodeURIComponent(
|
|
1840
|
+
p.channelId
|
|
1841
|
+
)}/messages`;
|
|
1842
|
+
const body = {
|
|
1843
|
+
subject: p.subject,
|
|
1844
|
+
body: { contentType: "html", content: embedMarker(p.html) }
|
|
1845
|
+
};
|
|
1846
|
+
const hostedContents = buildHostedContents(p.hostedImages);
|
|
1847
|
+
if (hostedContents) body.hostedContents = hostedContents;
|
|
1848
|
+
const res = await fetch(url, {
|
|
1849
|
+
method: "POST",
|
|
1850
|
+
headers: await this.authHeaders(),
|
|
1851
|
+
body: JSON.stringify(body)
|
|
1852
|
+
});
|
|
1853
|
+
if (!res.ok) {
|
|
1854
|
+
throw new Error(`Graph createThread failed: ${res.status} ${await safeText(res)}`);
|
|
1855
|
+
}
|
|
1856
|
+
const json = await res.json();
|
|
1857
|
+
if (!json.id) throw new Error("Graph createThread: response missing message id");
|
|
1858
|
+
return { threadRootId: json.id, webUrl: json.webUrl || "" };
|
|
1859
|
+
}
|
|
1860
|
+
async replyToThread(p) {
|
|
1861
|
+
const url = `${GRAPH_BASE}/teams/${encodeURIComponent(p.teamId)}/channels/${encodeURIComponent(
|
|
1862
|
+
p.channelId
|
|
1863
|
+
)}/messages/${encodeURIComponent(p.threadRootId)}/replies`;
|
|
1864
|
+
const body = { body: { contentType: "html", content: embedMarker(p.html) } };
|
|
1865
|
+
const hostedContents = buildHostedContents(p.hostedImages);
|
|
1866
|
+
if (hostedContents) body.hostedContents = hostedContents;
|
|
1867
|
+
const res = await fetch(url, {
|
|
1868
|
+
method: "POST",
|
|
1869
|
+
headers: await this.authHeaders(),
|
|
1870
|
+
body: JSON.stringify(body)
|
|
1871
|
+
});
|
|
1872
|
+
if (!res.ok) {
|
|
1873
|
+
throw new Error(`Graph replyToThread failed: ${res.status} ${await safeText(res)}`);
|
|
1874
|
+
}
|
|
1875
|
+
const json = await res.json();
|
|
1876
|
+
return { messageId: json.id || "" };
|
|
1877
|
+
}
|
|
1878
|
+
async listChannels(teamId) {
|
|
1879
|
+
const url = `${GRAPH_BASE}/teams/${encodeURIComponent(teamId)}/channels`;
|
|
1880
|
+
const res = await fetch(url, { headers: await this.authHeaders() });
|
|
1881
|
+
if (!res.ok) throw new Error(`Graph listChannels failed: ${res.status}`);
|
|
1882
|
+
const json = await res.json();
|
|
1883
|
+
return json.value || [];
|
|
1884
|
+
}
|
|
1885
|
+
/** List a team's tags (id + displayName) — used to resolve a tag name to its tagId. */
|
|
1886
|
+
async listTags(teamId) {
|
|
1887
|
+
const url = `${GRAPH_BASE}/teams/${encodeURIComponent(teamId)}/tags?$select=id,displayName`;
|
|
1888
|
+
const res = await fetch(url, { headers: await this.authHeaders() });
|
|
1889
|
+
if (!res.ok) throw new Error(`Graph listTags failed: ${res.status} ${await safeText(res)}`);
|
|
1890
|
+
const json = await res.json();
|
|
1891
|
+
return json.value || [];
|
|
1892
|
+
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Resolve a user reference to an AAD object id. A UPN (contains '@') or a GUID is
|
|
1895
|
+
* looked up / passed through directly; anything else is treated as a display name and
|
|
1896
|
+
* matched (exact, case-insensitive) against Graph. Returns '' when unresolved.
|
|
1897
|
+
*/
|
|
1898
|
+
async findUserId(query) {
|
|
1899
|
+
const q = query.trim();
|
|
1900
|
+
if (!q) return "";
|
|
1901
|
+
const isGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(q);
|
|
1902
|
+
const isUpn = q.includes("@");
|
|
1903
|
+
if (isGuid || isUpn) {
|
|
1904
|
+
const url2 = `${GRAPH_BASE}/users/${encodeURIComponent(q)}?$select=id`;
|
|
1905
|
+
const res2 = await fetch(url2, { headers: await this.authHeaders() });
|
|
1906
|
+
if (!res2.ok) return "";
|
|
1907
|
+
const json2 = await res2.json();
|
|
1908
|
+
return json2.id || "";
|
|
1909
|
+
}
|
|
1910
|
+
const filter = `displayName eq '${q.replace(/'/g, "''")}'`;
|
|
1911
|
+
const url = `${GRAPH_BASE}/users?$filter=${encodeURIComponent(filter)}&$select=id&$top=1`;
|
|
1912
|
+
const res = await fetch(url, { headers: await this.authHeaders() });
|
|
1913
|
+
if (!res.ok) return "";
|
|
1914
|
+
const json = await res.json();
|
|
1915
|
+
return json.value?.[0]?.id || "";
|
|
1916
|
+
}
|
|
1917
|
+
};
|
|
1918
|
+
async function safeText(res) {
|
|
1919
|
+
try {
|
|
1920
|
+
return (await res.text()).slice(0, 300);
|
|
1921
|
+
} catch {
|
|
1922
|
+
return "";
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
// electron/teams/trouterClient.ts
|
|
1927
|
+
var import_ws = __toESM(require("ws"));
|
|
1928
|
+
var crypto3 = __toESM(require("crypto"));
|
|
1929
|
+
var DEFAULT_GATEWAY = "go-msit.trouter.teams.microsoft.com";
|
|
1930
|
+
var V2_REGISTRAR = "https://teams.cloud.microsoft/registrar/prod/V2/registrations";
|
|
1931
|
+
var ORIGIN = "https://teams.cloud.microsoft";
|
|
1932
|
+
var HEARTBEAT_MS = 3e4;
|
|
1933
|
+
var REREGISTER_MS = 45 * 60 * 1e3;
|
|
1934
|
+
function extractThreadRootId(conversationLink) {
|
|
1935
|
+
if (!conversationLink) return "";
|
|
1936
|
+
const m = /;messageid=([^;&/?]+)/i.exec(conversationLink);
|
|
1937
|
+
return m ? m[1] : "";
|
|
1938
|
+
}
|
|
1939
|
+
function extractChannelId(conversationLink) {
|
|
1940
|
+
if (!conversationLink) return "";
|
|
1941
|
+
let convo = conversationLink;
|
|
1942
|
+
const idx = convo.indexOf("/conversations/");
|
|
1943
|
+
if (idx >= 0) convo = convo.slice(idx + "/conversations/".length);
|
|
1944
|
+
convo = convo.split(";")[0];
|
|
1945
|
+
try {
|
|
1946
|
+
convo = decodeURIComponent(convo);
|
|
1947
|
+
} catch {
|
|
1948
|
+
}
|
|
1949
|
+
return convo;
|
|
1950
|
+
}
|
|
1951
|
+
function parseFrame(raw) {
|
|
1952
|
+
if (raw.startsWith("1::")) return { type: "connect" };
|
|
1953
|
+
if (raw.startsWith("5")) {
|
|
1954
|
+
const m = /^5(?::[^:]*)?::([\s\S]*)$/.exec(raw);
|
|
1955
|
+
if (m) {
|
|
1956
|
+
try {
|
|
1957
|
+
return { type: "event", payload: JSON.parse(m[1]) };
|
|
1958
|
+
} catch {
|
|
1959
|
+
return { type: "event", payload: m[1] };
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
if (raw.startsWith("3:::")) {
|
|
1964
|
+
try {
|
|
1965
|
+
return { type: "message", payload: JSON.parse(raw.slice(4)) };
|
|
1966
|
+
} catch {
|
|
1967
|
+
return { type: "message", payload: raw.slice(4) };
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
if (raw.startsWith("6:::")) return { type: "ack", payload: raw.slice(4) };
|
|
1971
|
+
return { type: "unknown", payload: raw };
|
|
1972
|
+
}
|
|
1973
|
+
function parseEventMessage(body) {
|
|
1974
|
+
let data = body;
|
|
1975
|
+
if (typeof data === "string") {
|
|
1976
|
+
try {
|
|
1977
|
+
data = JSON.parse(data);
|
|
1978
|
+
} catch {
|
|
1979
|
+
return null;
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
const d = data;
|
|
1983
|
+
if (!d || d.type !== "EventMessage" || d.resourceType !== "NewMessage") return null;
|
|
1984
|
+
const resource = d.resource || {};
|
|
1985
|
+
const convLink = String(resource.conversationLink || "");
|
|
1986
|
+
const content = String(resource.content || "");
|
|
1987
|
+
return {
|
|
1988
|
+
messageId: String(resource.id || ""),
|
|
1989
|
+
channelId: extractChannelId(convLink),
|
|
1990
|
+
threadRootId: extractThreadRootId(convLink),
|
|
1991
|
+
senderName: String(resource.imdisplayname || ""),
|
|
1992
|
+
senderId: String(resource.from || ""),
|
|
1993
|
+
content: stripHtml(content),
|
|
1994
|
+
composeTime: String(resource.composetime || ""),
|
|
1995
|
+
hasMarker: hasMarker(content)
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
var TrouterClient = class {
|
|
1999
|
+
constructor(tokens, gateway = DEFAULT_GATEWAY) {
|
|
2000
|
+
this.tokens = tokens;
|
|
2001
|
+
this.gateway = gateway;
|
|
2002
|
+
this.health = "disconnected";
|
|
2003
|
+
this.ws = null;
|
|
2004
|
+
this.running = false;
|
|
2005
|
+
this.frameCounter = 0;
|
|
2006
|
+
this.surl = "";
|
|
2007
|
+
this.registrarUrl = "";
|
|
2008
|
+
this.registrationId = "";
|
|
2009
|
+
this.heartbeatTimer = null;
|
|
2010
|
+
this.reregisterTimer = null;
|
|
2011
|
+
this.reconnectTimer = null;
|
|
2012
|
+
this.onMessage = null;
|
|
2013
|
+
}
|
|
2014
|
+
async start(onMessage) {
|
|
2015
|
+
this.onMessage = onMessage;
|
|
2016
|
+
this.running = true;
|
|
2017
|
+
await this.connect();
|
|
2018
|
+
}
|
|
2019
|
+
async stop() {
|
|
2020
|
+
this.running = false;
|
|
2021
|
+
this.clearTimers();
|
|
2022
|
+
if (this.ws) {
|
|
2023
|
+
try {
|
|
2024
|
+
this.ws.close();
|
|
2025
|
+
} catch {
|
|
2026
|
+
}
|
|
2027
|
+
this.ws = null;
|
|
2028
|
+
}
|
|
2029
|
+
this.health = "disconnected";
|
|
2030
|
+
}
|
|
2031
|
+
clearTimers() {
|
|
2032
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
2033
|
+
if (this.reregisterTimer) clearInterval(this.reregisterTimer);
|
|
2034
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
2035
|
+
this.heartbeatTimer = this.reregisterTimer = null;
|
|
2036
|
+
this.reconnectTimer = null;
|
|
2037
|
+
}
|
|
2038
|
+
scheduleReconnect() {
|
|
2039
|
+
if (!this.running || this.reconnectTimer) return;
|
|
2040
|
+
this.reconnectTimer = setTimeout(() => {
|
|
2041
|
+
this.reconnectTimer = null;
|
|
2042
|
+
if (this.running) this.connect().catch((e) => twarn("Trouter reconnect failed:", e.message));
|
|
2043
|
+
}, 5e3);
|
|
2044
|
+
}
|
|
2045
|
+
async connect() {
|
|
2046
|
+
const token = await this.tokens.getToken("ic3");
|
|
2047
|
+
const epid = crypto3.randomUUID();
|
|
2048
|
+
this.registrationId = epid;
|
|
2049
|
+
const tc = JSON.stringify({ cv: "2026.03.01.1", ua: "TeamsCDL", hr: "", v: "1415/26020101120" });
|
|
2050
|
+
const qs = new URLSearchParams({
|
|
2051
|
+
tc,
|
|
2052
|
+
timeout: "40",
|
|
2053
|
+
epid,
|
|
2054
|
+
ccid: "",
|
|
2055
|
+
dom: "teams.cloud.microsoft",
|
|
2056
|
+
cor_id: crypto3.randomUUID(),
|
|
2057
|
+
con_num: `${Date.now()}_0`
|
|
2058
|
+
});
|
|
2059
|
+
const wsUrl = `wss://${this.gateway}/v4/c?${qs.toString()}`;
|
|
2060
|
+
const ws = new import_ws.default(wsUrl, { headers: { Origin: ORIGIN }, maxPayload: 2 ** 22 });
|
|
2061
|
+
this.ws = ws;
|
|
2062
|
+
ws.on("open", () => {
|
|
2063
|
+
});
|
|
2064
|
+
ws.on("message", (data) => {
|
|
2065
|
+
this.handleRaw(String(data), token, epid).catch(
|
|
2066
|
+
(e) => twarn("Trouter frame handler error:", e.message)
|
|
2067
|
+
);
|
|
2068
|
+
});
|
|
2069
|
+
ws.on("error", (err) => {
|
|
2070
|
+
this.health = "error";
|
|
2071
|
+
twarn("Trouter socket error:", err.message);
|
|
2072
|
+
});
|
|
2073
|
+
ws.on("close", () => {
|
|
2074
|
+
this.health = "disconnected";
|
|
2075
|
+
this.clearTimers();
|
|
2076
|
+
this.scheduleReconnect();
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
2079
|
+
send(frame) {
|
|
2080
|
+
if (this.ws && this.ws.readyState === import_ws.default.OPEN) this.ws.send(frame);
|
|
2081
|
+
}
|
|
2082
|
+
async handleRaw(raw, token, epid) {
|
|
2083
|
+
const { type, payload } = parseFrame(raw);
|
|
2084
|
+
if (type === "connect") {
|
|
2085
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
2086
|
+
const auth = JSON.stringify({
|
|
2087
|
+
name: "user.authenticate",
|
|
2088
|
+
args: [
|
|
2089
|
+
{
|
|
2090
|
+
headers: {
|
|
2091
|
+
"X-Ms-Test-User": "False",
|
|
2092
|
+
Authorization: `Bearer ${token}`,
|
|
2093
|
+
"X-MS-Migration": "True"
|
|
2094
|
+
},
|
|
2095
|
+
connectparams: {
|
|
2096
|
+
issuer: "",
|
|
2097
|
+
scae: "1",
|
|
2098
|
+
sig: "",
|
|
2099
|
+
sr: epid,
|
|
2100
|
+
sp: "",
|
|
2101
|
+
se: String((now + 600) * 1e3),
|
|
2102
|
+
st: String(now * 1e3)
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
]
|
|
2106
|
+
});
|
|
2107
|
+
this.send(`5:::${auth}`);
|
|
2108
|
+
return;
|
|
2109
|
+
}
|
|
2110
|
+
if (type === "event" && payload && typeof payload === "object") {
|
|
2111
|
+
const evt = payload;
|
|
2112
|
+
if (evt.name === "trouter.connected") {
|
|
2113
|
+
const info = evt.args?.[0] || {};
|
|
2114
|
+
this.surl = info.surl || "";
|
|
2115
|
+
this.registrarUrl = info.registrarUrl || V2_REGISTRAR;
|
|
2116
|
+
this.health = "connected";
|
|
2117
|
+
tlog("Trouter connected \u2014 registering for channel push\u2026");
|
|
2118
|
+
await this.register(token);
|
|
2119
|
+
this.startHeartbeat(token);
|
|
2120
|
+
return;
|
|
2121
|
+
}
|
|
2122
|
+
if (evt.name === "ping") {
|
|
2123
|
+
this.frameCounter += 1;
|
|
2124
|
+
this.send(`6:::${this.frameCounter}["pong"]`);
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
if (evt.name === "trouter.message_loss") {
|
|
2128
|
+
this.frameCounter += 1;
|
|
2129
|
+
const ack = JSON.stringify({ name: "trouter.processed_message_loss", args: evt.args || [{}] });
|
|
2130
|
+
this.send(`5:${this.frameCounter}+::${ack}`);
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2135
|
+
if (type === "message" && payload && typeof payload === "object") {
|
|
2136
|
+
const msg = payload;
|
|
2137
|
+
if (msg.id) {
|
|
2138
|
+
const ack = JSON.stringify({ id: msg.id, status: 200, headers: {}, body: "" });
|
|
2139
|
+
this.send(`3:::${ack}`);
|
|
2140
|
+
}
|
|
2141
|
+
const inbound = parseEventMessage(msg.body);
|
|
2142
|
+
if (!inbound || !this.onMessage) return;
|
|
2143
|
+
this.onMessage(inbound);
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
startHeartbeat(token) {
|
|
2147
|
+
this.clearTimers();
|
|
2148
|
+
this.heartbeatTimer = setInterval(() => {
|
|
2149
|
+
this.frameCounter += 1;
|
|
2150
|
+
this.send(`5:${this.frameCounter}+::{"name":"ping"}`);
|
|
2151
|
+
}, HEARTBEAT_MS);
|
|
2152
|
+
this.reregisterTimer = setInterval(() => {
|
|
2153
|
+
this.register(token).catch((e) => twarn("Trouter re-register failed:", e.message));
|
|
2154
|
+
}, REREGISTER_MS);
|
|
2155
|
+
}
|
|
2156
|
+
async register(token) {
|
|
2157
|
+
const payload = {
|
|
2158
|
+
clientDescription: {
|
|
2159
|
+
appId: "TeamsCDLWebWorker",
|
|
2160
|
+
aesKey: "",
|
|
2161
|
+
languageId: "en-US",
|
|
2162
|
+
platform: "edge",
|
|
2163
|
+
templateKey: "TeamsCDLWebWorker_2.6",
|
|
2164
|
+
platformUIVersion: "1415/26020101120"
|
|
2165
|
+
},
|
|
2166
|
+
registrationId: this.registrationId,
|
|
2167
|
+
nodeId: "",
|
|
2168
|
+
transports: { TROUTER: [{ context: "", path: this.surl, ttl: 3600 }] }
|
|
2169
|
+
};
|
|
2170
|
+
const headers = {
|
|
2171
|
+
Authorization: `Bearer ${token}`,
|
|
2172
|
+
"Content-Type": "application/json",
|
|
2173
|
+
Origin: ORIGIN
|
|
2174
|
+
};
|
|
2175
|
+
const urls = Array.from(new Set([this.registrarUrl, V2_REGISTRAR].filter(Boolean)));
|
|
2176
|
+
for (const url of urls) {
|
|
2177
|
+
try {
|
|
2178
|
+
const res = await fetch(url, { method: "POST", headers, body: JSON.stringify(payload) });
|
|
2179
|
+
if (!res.ok && res.status !== 202) {
|
|
2180
|
+
twarn(`Registrar ${url.slice(0, 60)} returned ${res.status}`);
|
|
2181
|
+
}
|
|
2182
|
+
} catch (e) {
|
|
2183
|
+
twarn(`Registrar ${url.slice(0, 60)} failed:`, e.message);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
};
|
|
2188
|
+
|
|
2189
|
+
// electron/teams/sessionGateway.ts
|
|
2190
|
+
var RelaySessionGateway = class {
|
|
2191
|
+
constructor(relay2) {
|
|
2192
|
+
this.relay = relay2;
|
|
2193
|
+
}
|
|
2194
|
+
getSessionId(officeId, agentId) {
|
|
2195
|
+
return this.relay.mainGetSessionId(officeId, agentId);
|
|
2196
|
+
}
|
|
2197
|
+
getSessionMeta(officeId, agentId) {
|
|
2198
|
+
return this.relay.mainGetSessionMeta(officeId, agentId);
|
|
2199
|
+
}
|
|
2200
|
+
isAgentReady(officeId, agentId) {
|
|
2201
|
+
return this.relay.mainIsAgentReady(officeId, agentId);
|
|
2202
|
+
}
|
|
2203
|
+
async submitPrompt(officeId, agentId, prompt, label) {
|
|
2204
|
+
const res = await this.relay.mainSubmitPrompt(officeId, agentId, prompt, label);
|
|
2205
|
+
if (!res.success) {
|
|
2206
|
+
throw new Error(res.error || `Failed to submit prompt to ${officeId}:${agentId}`);
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
setForwarding(officeId, agentId, enabled) {
|
|
2210
|
+
this.relay.mainSetAgentForwarding(officeId, agentId, enabled);
|
|
2211
|
+
}
|
|
2212
|
+
onAgentEvent(cb) {
|
|
2213
|
+
const onCopilotEvent = (...args) => {
|
|
2214
|
+
const agentId = args[0];
|
|
2215
|
+
const event = args[1];
|
|
2216
|
+
if (event?.type === "assistant.message") {
|
|
2217
|
+
const content = extractMessageContent(event);
|
|
2218
|
+
if (content) cb({ agentId, kind: "message", content });
|
|
2219
|
+
}
|
|
2220
|
+
};
|
|
2221
|
+
const onTurnStart = (...args) => cb({ agentId: args[0], kind: "turn-start" });
|
|
2222
|
+
const onTurnEnd = (...args) => cb({ agentId: args[0], kind: "turn-end" });
|
|
2223
|
+
const onToolStart = (...args) => cb({ agentId: args[0], kind: "tool-start", toolName: args[1] });
|
|
2224
|
+
const onUserMessage = (...args) => cb({ agentId: args[0], kind: "user-message", content: args[1] ?? "" });
|
|
2225
|
+
this.relay.mainEvents.on("copilot-event", onCopilotEvent);
|
|
2226
|
+
this.relay.mainEvents.on("copilot-turn-start", onTurnStart);
|
|
2227
|
+
this.relay.mainEvents.on("copilot-turn-end", onTurnEnd);
|
|
2228
|
+
this.relay.mainEvents.on("copilot-tool-start", onToolStart);
|
|
2229
|
+
this.relay.mainEvents.on("copilot-user-message", onUserMessage);
|
|
2230
|
+
return () => {
|
|
2231
|
+
this.relay.mainEvents.off("copilot-event", onCopilotEvent);
|
|
2232
|
+
this.relay.mainEvents.off("copilot-turn-start", onTurnStart);
|
|
2233
|
+
this.relay.mainEvents.off("copilot-turn-end", onTurnEnd);
|
|
2234
|
+
this.relay.mainEvents.off("copilot-tool-start", onToolStart);
|
|
2235
|
+
this.relay.mainEvents.off("copilot-user-message", onUserMessage);
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
onSessionExit(cb) {
|
|
2239
|
+
const onExit = (...args) => cb(args[0]);
|
|
2240
|
+
this.relay.mainEvents.on("terminal-exit", onExit);
|
|
2241
|
+
return () => this.relay.mainEvents.off("terminal-exit", onExit);
|
|
2242
|
+
}
|
|
2243
|
+
};
|
|
2244
|
+
function extractMessageContent(event) {
|
|
2245
|
+
const data = event?.data ?? {};
|
|
2246
|
+
const content = data.content ?? data.text ?? data.message;
|
|
2247
|
+
if (typeof content === "string") return content;
|
|
2248
|
+
if (Array.isArray(content)) {
|
|
2249
|
+
return content.map((c) => typeof c === "string" ? c : c?.text).filter((s) => typeof s === "string").join("");
|
|
2250
|
+
}
|
|
2251
|
+
return "";
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
// electron/teams/teamsSettingsStore.ts
|
|
2255
|
+
var fs6 = __toESM(require("fs"));
|
|
2256
|
+
var path7 = __toESM(require("path"));
|
|
2257
|
+
var DEFAULT_TEAMS_SETTINGS = {
|
|
2258
|
+
enabled: false,
|
|
2259
|
+
defaultChannelUrl: "",
|
|
2260
|
+
relayChannelUrl: "",
|
|
2261
|
+
relayMentionType: "none",
|
|
2262
|
+
relayMentionValue: "",
|
|
2263
|
+
notifyOnCompleteEnabled: true,
|
|
2264
|
+
ackEnabled: true,
|
|
2265
|
+
checkInEnabled: true,
|
|
2266
|
+
checkInThresholdMs: 12e4,
|
|
2267
|
+
checkInThrottleMs: 6e4
|
|
2268
|
+
};
|
|
2269
|
+
function normalizeTeamsSettings(partial) {
|
|
2270
|
+
return {
|
|
2271
|
+
enabled: partial?.enabled ?? DEFAULT_TEAMS_SETTINGS.enabled,
|
|
2272
|
+
defaultChannelUrl: partial?.defaultChannelUrl ?? DEFAULT_TEAMS_SETTINGS.defaultChannelUrl,
|
|
2273
|
+
relayChannelUrl: partial?.relayChannelUrl ?? DEFAULT_TEAMS_SETTINGS.relayChannelUrl,
|
|
2274
|
+
relayMentionType: partial?.relayMentionType ?? DEFAULT_TEAMS_SETTINGS.relayMentionType,
|
|
2275
|
+
relayMentionValue: partial?.relayMentionValue ?? DEFAULT_TEAMS_SETTINGS.relayMentionValue,
|
|
2276
|
+
notifyOnCompleteEnabled: partial?.notifyOnCompleteEnabled ?? DEFAULT_TEAMS_SETTINGS.notifyOnCompleteEnabled,
|
|
2277
|
+
ackEnabled: partial?.ackEnabled ?? DEFAULT_TEAMS_SETTINGS.ackEnabled,
|
|
2278
|
+
checkInEnabled: partial?.checkInEnabled ?? DEFAULT_TEAMS_SETTINGS.checkInEnabled,
|
|
2279
|
+
checkInThresholdMs: partial?.checkInThresholdMs ?? DEFAULT_TEAMS_SETTINGS.checkInThresholdMs,
|
|
2280
|
+
checkInThrottleMs: partial?.checkInThrottleMs ?? DEFAULT_TEAMS_SETTINGS.checkInThrottleMs
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
function createTeamsSettingsStore(cwd = process.cwd()) {
|
|
2284
|
+
const dataDir = path7.join(cwd, ".data");
|
|
2285
|
+
const filePath = path7.join(dataDir, "teams-settings.json");
|
|
2286
|
+
return {
|
|
2287
|
+
filePath,
|
|
2288
|
+
load() {
|
|
2289
|
+
try {
|
|
2290
|
+
if (!fs6.existsSync(filePath)) return { ...DEFAULT_TEAMS_SETTINGS };
|
|
2291
|
+
return normalizeTeamsSettings(JSON.parse(fs6.readFileSync(filePath, "utf8")));
|
|
2292
|
+
} catch {
|
|
2293
|
+
return { ...DEFAULT_TEAMS_SETTINGS };
|
|
2294
|
+
}
|
|
2295
|
+
},
|
|
2296
|
+
save(settings) {
|
|
2297
|
+
fs6.mkdirSync(dataDir, { recursive: true });
|
|
2298
|
+
fs6.writeFileSync(filePath, JSON.stringify(normalizeTeamsSettings(settings), null, 2), "utf8");
|
|
2299
|
+
}
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
// electron/teams/channelAllowlist.ts
|
|
2304
|
+
function allowedChannelIdSet(defaultChannelUrl, overrideUrls = []) {
|
|
2305
|
+
const set = /* @__PURE__ */ new Set();
|
|
2306
|
+
for (const url of [defaultChannelUrl, ...overrideUrls]) {
|
|
2307
|
+
const trimmed = (url ?? "").trim();
|
|
2308
|
+
if (!trimmed) continue;
|
|
2309
|
+
const coords = parseChannelLink(trimmed);
|
|
2310
|
+
if (coords?.channelId) set.add(coords.channelId);
|
|
2311
|
+
}
|
|
2312
|
+
return set;
|
|
2313
|
+
}
|
|
2314
|
+
function createAllowlistedGraphSender(inner, getAllowed) {
|
|
2315
|
+
const assertAllowed = (channelId, op) => {
|
|
2316
|
+
if (!getAllowed().has(channelId)) {
|
|
2317
|
+
throw new Error(
|
|
2318
|
+
`Teams outbound blocked: channel ${channelId} is not in the settings/overrides allowlist (${op}).`
|
|
2319
|
+
);
|
|
2320
|
+
}
|
|
2321
|
+
};
|
|
2322
|
+
const wrapped = {
|
|
2323
|
+
createThread: async (p) => {
|
|
2324
|
+
assertAllowed(p.channelId, "createThread");
|
|
2325
|
+
return inner.createThread(p);
|
|
2326
|
+
},
|
|
2327
|
+
replyToThread: async (p) => {
|
|
2328
|
+
assertAllowed(p.channelId, "replyToThread");
|
|
2329
|
+
return inner.replyToThread(p);
|
|
2330
|
+
}
|
|
2331
|
+
};
|
|
2332
|
+
if (inner.listChannels) {
|
|
2333
|
+
wrapped.listChannels = (teamId) => inner.listChannels(teamId);
|
|
2334
|
+
}
|
|
2335
|
+
return wrapped;
|
|
2336
|
+
}
|
|
2337
|
+
function officeChannelOverridesFromJson(json) {
|
|
2338
|
+
if (!json) return [];
|
|
2339
|
+
try {
|
|
2340
|
+
const record = JSON.parse(json);
|
|
2341
|
+
const offices = Array.isArray(record?.offices) ? record.offices : [];
|
|
2342
|
+
const urls = [];
|
|
2343
|
+
for (const o of offices) {
|
|
2344
|
+
const u = o?.teamsChannelUrl;
|
|
2345
|
+
if (typeof u === "string" && u.trim()) urls.push(u.trim());
|
|
2346
|
+
}
|
|
2347
|
+
return urls;
|
|
2348
|
+
} catch {
|
|
2349
|
+
return [];
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
function createCachedAllowedChannels(compute, ttlMs = 2e3, now = Date.now) {
|
|
2353
|
+
let cached = null;
|
|
2354
|
+
let computedAt = 0;
|
|
2355
|
+
return () => {
|
|
2356
|
+
const t = now();
|
|
2357
|
+
if (cached && t - computedAt < ttlMs) return cached;
|
|
2358
|
+
try {
|
|
2359
|
+
cached = compute();
|
|
2360
|
+
computedAt = t;
|
|
2361
|
+
} catch {
|
|
2362
|
+
if (!cached) cached = /* @__PURE__ */ new Set();
|
|
2363
|
+
}
|
|
2364
|
+
return cached;
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
// electron/teams/relaySender.ts
|
|
2369
|
+
var META_OPEN = "[[CO-META]]";
|
|
2370
|
+
var META_CLOSE = "[[/CO-META]]";
|
|
2371
|
+
function stripMetaMarkers(html) {
|
|
2372
|
+
let out = html;
|
|
2373
|
+
let prev;
|
|
2374
|
+
do {
|
|
2375
|
+
prev = out;
|
|
2376
|
+
out = out.split(META_OPEN).join("").split(META_CLOSE).join("");
|
|
2377
|
+
} while (out !== prev);
|
|
2378
|
+
return out;
|
|
2379
|
+
}
|
|
2380
|
+
function encodeMetaBlock(meta) {
|
|
2381
|
+
const b64 = Buffer.from(JSON.stringify(meta), "utf8").toString("base64");
|
|
2382
|
+
return `<p>${META_OPEN}${b64}${META_CLOSE}</p>`;
|
|
2383
|
+
}
|
|
2384
|
+
function createRelaySender(opts) {
|
|
2385
|
+
const resolveDump = () => {
|
|
2386
|
+
const url = opts.getDumpChannelUrl().trim();
|
|
2387
|
+
if (!url) throw new Error("Teams Dump channel URL is not configured.");
|
|
2388
|
+
const coords = parseChannelLink(url);
|
|
2389
|
+
if (!coords) throw new Error("Teams Dump channel URL could not be parsed.");
|
|
2390
|
+
return { teamId: coords.teamId, channelId: coords.channelId };
|
|
2391
|
+
};
|
|
2392
|
+
const postToDump = async (dest, title, html, hostedImages) => {
|
|
2393
|
+
if (!opts.isDestinationAllowed(dest.channelId)) {
|
|
2394
|
+
throw new Error(
|
|
2395
|
+
`Teams outbound blocked: destination channel ${dest.channelId} is not in the allowlist (relay).`
|
|
2396
|
+
);
|
|
2397
|
+
}
|
|
2398
|
+
const dump = resolveDump();
|
|
2399
|
+
let resolved = { mentionType: "none", mentionId: "" };
|
|
2400
|
+
const ref = opts.getMention();
|
|
2401
|
+
if (ref && ref.type !== "none" && ref.value.trim()) {
|
|
2402
|
+
try {
|
|
2403
|
+
resolved = await opts.resolveMention({ type: ref.type, value: ref.value.trim() }, dest.teamId);
|
|
2404
|
+
} catch (err) {
|
|
2405
|
+
tlog(`Relay mention resolution failed (${err.message}); posting without mention.`);
|
|
2406
|
+
resolved = { mentionType: "none", mentionId: "" };
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
const humanHtml = embedMarker(stripMetaMarkers(html));
|
|
2410
|
+
const meta = {
|
|
2411
|
+
v: 1,
|
|
2412
|
+
destTeamId: dest.teamId,
|
|
2413
|
+
destChannelId: dest.channelId,
|
|
2414
|
+
threadRootId: dest.threadRootId,
|
|
2415
|
+
mentionType: resolved.mentionType,
|
|
2416
|
+
mentionId: resolved.mentionId,
|
|
2417
|
+
title,
|
|
2418
|
+
html: humanHtml
|
|
2419
|
+
};
|
|
2420
|
+
const body = `${humanHtml}${encodeMetaBlock(meta)}`;
|
|
2421
|
+
await opts.primary.createThread({
|
|
2422
|
+
teamId: dump.teamId,
|
|
2423
|
+
channelId: dump.channelId,
|
|
2424
|
+
subject: "CopilotOffice",
|
|
2425
|
+
html: body,
|
|
2426
|
+
hostedImages
|
|
2427
|
+
});
|
|
2428
|
+
tlog(`Relay post to Dump channel ok (dest=${dest.channelId}, thread=${dest.threadRootId || "root"}, mention=${resolved.mentionType}).`);
|
|
2429
|
+
};
|
|
2430
|
+
return {
|
|
2431
|
+
async createThread(p) {
|
|
2432
|
+
await postToDump({ teamId: p.teamId, channelId: p.channelId, threadRootId: "" }, p.subject, p.html, p.hostedImages);
|
|
2433
|
+
return { threadRootId: "", webUrl: "" };
|
|
2434
|
+
},
|
|
2435
|
+
async replyToThread(p) {
|
|
2436
|
+
await postToDump(
|
|
2437
|
+
{ teamId: p.teamId, channelId: p.channelId, threadRootId: p.threadRootId },
|
|
2438
|
+
"",
|
|
2439
|
+
p.html,
|
|
2440
|
+
p.hostedImages
|
|
2441
|
+
);
|
|
2442
|
+
return { messageId: "" };
|
|
2443
|
+
}
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
// electron/teams/teamsIpc.ts
|
|
2448
|
+
var import_electron3 = require("electron");
|
|
2449
|
+
function registerTeamsIpc(hooks) {
|
|
2450
|
+
const { service, settingsStore } = hooks;
|
|
2451
|
+
import_electron3.ipcMain.handle("teams:status", (_e, args) => {
|
|
2452
|
+
if (args?.officeId && args?.agentId) {
|
|
2453
|
+
const status = service.getStatus(args.officeId, args.agentId);
|
|
2454
|
+
return { success: true, connected: !!status?.online, bindings: status ? [status] : [] };
|
|
2455
|
+
}
|
|
2456
|
+
const bindings = service.getStatuses();
|
|
2457
|
+
return { success: true, connected: bindings.some((b) => b.online), bindings };
|
|
2458
|
+
});
|
|
2459
|
+
import_electron3.ipcMain.handle("teams:register", (_e, ctx) => service.register(ctx));
|
|
2460
|
+
import_electron3.ipcMain.handle(
|
|
2461
|
+
"teams:stop",
|
|
2462
|
+
(_e, args) => service.goOffline(args.officeId, args.agentId, true)
|
|
2463
|
+
);
|
|
2464
|
+
import_electron3.ipcMain.handle("teams:reconcile", async () => {
|
|
2465
|
+
await service.reconcileNow();
|
|
2466
|
+
return { success: true };
|
|
2467
|
+
});
|
|
2468
|
+
import_electron3.ipcMain.handle("teams:getSettings", () => {
|
|
2469
|
+
return { success: true, settings: settingsStore.load() };
|
|
2470
|
+
});
|
|
2471
|
+
import_electron3.ipcMain.handle("teams:saveSettings", (_e, args) => {
|
|
2472
|
+
const settings = normalizeTeamsSettings(args?.settings);
|
|
2473
|
+
let parsed;
|
|
2474
|
+
if (settings.defaultChannelUrl.trim()) {
|
|
2475
|
+
const coords = parseChannelLink(settings.defaultChannelUrl);
|
|
2476
|
+
if (!coords) {
|
|
2477
|
+
return { success: false, error: "The default channel link could not be parsed." };
|
|
2478
|
+
}
|
|
2479
|
+
parsed = coords;
|
|
2480
|
+
}
|
|
2481
|
+
if (settings.relayChannelUrl.trim()) {
|
|
2482
|
+
if (!parseChannelLink(settings.relayChannelUrl)) {
|
|
2483
|
+
return { success: false, error: "The relay trigger channel link could not be parsed." };
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
settingsStore.save(settings);
|
|
2487
|
+
hooks.onSettingsChanged?.(settings);
|
|
2488
|
+
return { success: true, parsed };
|
|
2489
|
+
});
|
|
2490
|
+
}
|
|
2491
|
+
function makeStatusEmitter(getWindow) {
|
|
2492
|
+
return (status) => {
|
|
2493
|
+
const win = getWindow();
|
|
2494
|
+
if (win && !win.isDestroyed()) win.webContents.send("teams:status:changed", status);
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
function makeToastEmitter(getWindow) {
|
|
2498
|
+
return (toast) => {
|
|
2499
|
+
const win = getWindow();
|
|
2500
|
+
if (win && !win.isDestroyed()) win.webContents.send("teams:toast", toast);
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
// electron/main.ts
|
|
2505
|
+
var OPEN_DEVTOOLS_ON_START = process.env.COPILOT_OFFICE_OPEN_DEVTOOLS !== "0";
|
|
2506
|
+
var ENABLE_FILE_WATCHER = process.env.COPILOT_OFFICE_ENABLE_WATCHER !== "0";
|
|
2507
|
+
function killOrphanedProcesses() {
|
|
2508
|
+
try {
|
|
2509
|
+
const { reaped, skipped } = reapRegisteredPtys();
|
|
2510
|
+
if (reaped.length > 0) {
|
|
2511
|
+
console.log(`[Main] Reaped ${reaped.length} orphaned PTY process tree(s):`, reaped);
|
|
2512
|
+
}
|
|
2513
|
+
if (skipped.length > 0) {
|
|
2514
|
+
console.log(`[Main] Skipped ${skipped.length} already-dead PTY record(s) from registry`);
|
|
2515
|
+
}
|
|
2516
|
+
} catch {
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
var mainWindow = null;
|
|
2520
|
+
var watcherProcess = null;
|
|
2521
|
+
var relay = new TerminalRelay(() => mainWindow);
|
|
2522
|
+
var teamsService = null;
|
|
2523
|
+
var pendingHardReload = false;
|
|
2524
|
+
function startFileWatcher() {
|
|
2525
|
+
const copilotOfficePath = path8.join(process.cwd(), "CopilotOffice");
|
|
2526
|
+
if (!fs7.existsSync(path8.join(copilotOfficePath, "package.json"))) {
|
|
2527
|
+
if (fs7.existsSync(path8.join(process.cwd(), "src", "main.ts"))) {
|
|
2528
|
+
watcherProcess = (0, import_child_process4.spawn)("npx", ["esbuild", "src/main.ts", "--bundle", "--outfile=dist/game.bundle.js", "--platform=browser", "--format=iife", "--global-name=CopilotOffice", "--watch"], {
|
|
2529
|
+
cwd: process.cwd(),
|
|
2530
|
+
shell: true,
|
|
2531
|
+
stdio: "pipe"
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2534
|
+
} else {
|
|
2535
|
+
watcherProcess = (0, import_child_process4.spawn)("npx", ["esbuild", "src/main.ts", "--bundle", "--outfile=dist/game.bundle.js", "--platform=browser", "--format=iife", "--global-name=CopilotOffice", "--watch"], {
|
|
2536
|
+
cwd: copilotOfficePath,
|
|
2537
|
+
shell: true,
|
|
2538
|
+
stdio: "pipe"
|
|
2539
|
+
});
|
|
2540
|
+
}
|
|
2541
|
+
if (watcherProcess) {
|
|
2542
|
+
watcherProcess.stdout?.on("data", (data) => {
|
|
2543
|
+
const msg = data.toString();
|
|
2544
|
+
console.log("[Watcher]", msg);
|
|
2545
|
+
if (msg.includes("watching") || msg.includes("built")) {
|
|
2546
|
+
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
2547
|
+
mainWindow.webContents.send("build-complete");
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
});
|
|
2551
|
+
watcherProcess.stderr?.on("data", (data) => {
|
|
2552
|
+
console.error("[Watcher Error]", data.toString());
|
|
2553
|
+
});
|
|
2554
|
+
console.log("File watcher started");
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
function createWindow() {
|
|
2558
|
+
mainWindow = new import_electron4.BrowserWindow({
|
|
2559
|
+
width: 2560,
|
|
2560
|
+
height: 1440,
|
|
2561
|
+
title: "Copilot Office",
|
|
2562
|
+
webPreferences: {
|
|
2563
|
+
preload: path8.join(__dirname, "terminal", "preload.js"),
|
|
2564
|
+
contextIsolation: true,
|
|
2565
|
+
nodeIntegration: false,
|
|
2566
|
+
backgroundThrottling: false
|
|
2567
|
+
}
|
|
2568
|
+
});
|
|
2569
|
+
mainWindow.maximize();
|
|
2570
|
+
mainWindow.loadFile(path8.join(__dirname, "../../src/index.html"));
|
|
2571
|
+
if (OPEN_DEVTOOLS_ON_START) {
|
|
2572
|
+
mainWindow.webContents.openDevTools({ mode: "detach" });
|
|
2573
|
+
}
|
|
2574
|
+
mainWindow.webContents.on("did-start-navigation", (_event, _url, isInPlace) => {
|
|
2575
|
+
if (isInPlace && pendingHardReload) {
|
|
2576
|
+
console.log("[Main] Hard reload \u2014 restarting terminal server");
|
|
2577
|
+
pendingHardReload = false;
|
|
2578
|
+
relay.shutdown().then(
|
|
2579
|
+
() => relay.spawnServer(__dirname)
|
|
2580
|
+
).catch(
|
|
2581
|
+
(e) => console.error("[Main] Failed to respawn after reload:", e)
|
|
2582
|
+
);
|
|
2583
|
+
} else if (isInPlace) {
|
|
2584
|
+
console.log("[Main] Soft reload \u2014 keeping terminal server alive");
|
|
2585
|
+
}
|
|
2586
|
+
});
|
|
2587
|
+
mainWindow.on("closed", () => {
|
|
2588
|
+
if (watcherProcess) {
|
|
2589
|
+
watcherProcess.kill();
|
|
2590
|
+
watcherProcess = null;
|
|
2591
|
+
}
|
|
2592
|
+
mainWindow = null;
|
|
2593
|
+
});
|
|
2594
|
+
}
|
|
2595
|
+
import_electron4.app.whenReady().then(async () => {
|
|
2596
|
+
import_electron4.Menu.setApplicationMenu(null);
|
|
2597
|
+
killOrphanedProcesses();
|
|
523
2598
|
relay.registerIpc();
|
|
524
2599
|
const officeStore = createOfficeFileStore();
|
|
525
2600
|
registerNonTerminalIpc({
|
|
@@ -536,22 +2611,116 @@ import_electron3.app.whenReady().then(async () => {
|
|
|
536
2611
|
console.log("[Main] File watcher disabled");
|
|
537
2612
|
}
|
|
538
2613
|
createWindow();
|
|
539
|
-
|
|
540
|
-
if (
|
|
2614
|
+
import_electron4.app.on("activate", () => {
|
|
2615
|
+
if (import_electron4.BrowserWindow.getAllWindows().length === 0) createWindow();
|
|
541
2616
|
});
|
|
2617
|
+
try {
|
|
2618
|
+
const settingsStore = createTeamsSettingsStore(process.cwd());
|
|
2619
|
+
const tokenPersistence = createSafeStorageTokenPersistence(
|
|
2620
|
+
path8.join(process.cwd(), ".data", "teams-token.enc"),
|
|
2621
|
+
import_electron4.safeStorage
|
|
2622
|
+
);
|
|
2623
|
+
const tokens = new AzTokenProvider(void 0, tokenPersistence);
|
|
2624
|
+
const getAllowedChannels = createCachedAllowedChannels(
|
|
2625
|
+
() => allowedChannelIdSet(
|
|
2626
|
+
settingsStore.load().defaultChannelUrl,
|
|
2627
|
+
officeChannelOverridesFromJson(officeStore.load().data)
|
|
2628
|
+
)
|
|
2629
|
+
);
|
|
2630
|
+
let cachedTeamsSettings = settingsStore.load();
|
|
2631
|
+
let teamsSettingsAt = Date.now();
|
|
2632
|
+
const getTeamsSettingsCached = () => {
|
|
2633
|
+
const now = Date.now();
|
|
2634
|
+
if (now - teamsSettingsAt >= 2e3) {
|
|
2635
|
+
cachedTeamsSettings = settingsStore.load();
|
|
2636
|
+
teamsSettingsAt = now;
|
|
2637
|
+
}
|
|
2638
|
+
return cachedTeamsSettings;
|
|
2639
|
+
};
|
|
2640
|
+
const rawGraph = new GraphClient(tokens);
|
|
2641
|
+
const allowlistedGraph = createAllowlistedGraphSender(rawGraph, getAllowedChannels);
|
|
2642
|
+
const resolveMention = async (ref, destTeamId) => {
|
|
2643
|
+
try {
|
|
2644
|
+
if (ref.type === "user") {
|
|
2645
|
+
const id = await rawGraph.findUserId(ref.value);
|
|
2646
|
+
return id ? { mentionType: "user", mentionId: id } : { mentionType: "none", mentionId: "" };
|
|
2647
|
+
}
|
|
2648
|
+
if (ref.type === "tag") {
|
|
2649
|
+
const raw = ref.value.trim();
|
|
2650
|
+
return raw ? { mentionType: "tag", mentionId: raw } : { mentionType: "none", mentionId: "" };
|
|
2651
|
+
}
|
|
2652
|
+
} catch {
|
|
2653
|
+
}
|
|
2654
|
+
return { mentionType: "none", mentionId: "" };
|
|
2655
|
+
};
|
|
2656
|
+
const relaySender = createRelaySender({
|
|
2657
|
+
primary: rawGraph,
|
|
2658
|
+
getDumpChannelUrl: () => getTeamsSettingsCached().relayChannelUrl,
|
|
2659
|
+
getMention: () => ({
|
|
2660
|
+
type: getTeamsSettingsCached().relayMentionType,
|
|
2661
|
+
value: getTeamsSettingsCached().relayMentionValue
|
|
2662
|
+
}),
|
|
2663
|
+
resolveMention,
|
|
2664
|
+
// The fan-out destination must satisfy the same outbound allowlist as the direct path.
|
|
2665
|
+
isDestinationAllowed: (channelId) => getAllowedChannels().has(channelId)
|
|
2666
|
+
});
|
|
2667
|
+
const graph = allowlistedGraph;
|
|
2668
|
+
const isNotifyActive = () => {
|
|
2669
|
+
const s = getTeamsSettingsCached();
|
|
2670
|
+
return s.notifyOnCompleteEnabled && !!s.relayChannelUrl.trim();
|
|
2671
|
+
};
|
|
2672
|
+
const source = new TrouterClient(tokens);
|
|
2673
|
+
const gateway = new RelaySessionGateway(relay);
|
|
2674
|
+
const store = new FileTeamsOnlineStore(
|
|
2675
|
+
FileTeamsOnlineStore.defaultPath(path8.join(process.cwd(), ".data"))
|
|
2676
|
+
);
|
|
2677
|
+
const emitStatus = makeStatusEmitter(() => mainWindow);
|
|
2678
|
+
const emitToast = makeToastEmitter(() => mainWindow);
|
|
2679
|
+
teamsService = new TeamsService({
|
|
2680
|
+
store,
|
|
2681
|
+
tokens,
|
|
2682
|
+
graph,
|
|
2683
|
+
notifier: relaySender,
|
|
2684
|
+
isNotifyActive,
|
|
2685
|
+
source,
|
|
2686
|
+
gateway,
|
|
2687
|
+
getSettings: () => settingsStore.load(),
|
|
2688
|
+
emitStatus,
|
|
2689
|
+
emitToast
|
|
2690
|
+
});
|
|
2691
|
+
registerTeamsIpc({
|
|
2692
|
+
service: teamsService,
|
|
2693
|
+
settingsStore,
|
|
2694
|
+
getMainWindow: () => mainWindow,
|
|
2695
|
+
onSettingsChanged: (settings) => {
|
|
2696
|
+
if (settings.enabled) teamsService?.start().catch((e) => console.error("[Main] Teams start failed:", e));
|
|
2697
|
+
else teamsService?.stop().catch((e) => console.error("[Main] Teams stop failed:", e));
|
|
2698
|
+
}
|
|
2699
|
+
});
|
|
2700
|
+
if (settingsStore.load().enabled) {
|
|
2701
|
+
teamsService.start().catch((e) => console.error("[Main] Teams start failed:", e));
|
|
2702
|
+
} else {
|
|
2703
|
+
console.log("[TeamsRemote] Feature disabled \u2014 service idle (enable it in Settings \u2192 Teams Remote).");
|
|
2704
|
+
}
|
|
2705
|
+
} catch (e) {
|
|
2706
|
+
console.error("[Main] Failed to initialize Teams service:", e);
|
|
2707
|
+
}
|
|
542
2708
|
});
|
|
543
|
-
|
|
2709
|
+
import_electron4.app.on("window-all-closed", () => {
|
|
544
2710
|
if (watcherProcess) watcherProcess.kill();
|
|
545
|
-
if (process.platform !== "darwin")
|
|
2711
|
+
if (process.platform !== "darwin") import_electron4.app.quit();
|
|
546
2712
|
});
|
|
547
2713
|
var isShuttingDown = false;
|
|
548
|
-
|
|
2714
|
+
import_electron4.app.on("before-quit", (event) => {
|
|
549
2715
|
if (isShuttingDown) return;
|
|
550
2716
|
isShuttingDown = true;
|
|
551
2717
|
event.preventDefault();
|
|
552
2718
|
console.log("[Main] Awaiting relay shutdown before quit\u2026");
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
2719
|
+
const teamsStop = teamsService ? teamsService.stop().catch(() => void 0) : Promise.resolve();
|
|
2720
|
+
Promise.resolve(teamsStop).finally(() => {
|
|
2721
|
+
relay.shutdown().finally(() => {
|
|
2722
|
+
console.log("[Main] Relay shutdown complete \u2014 quitting");
|
|
2723
|
+
import_electron4.app.quit();
|
|
2724
|
+
});
|
|
556
2725
|
});
|
|
557
2726
|
});
|