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