sessix-server 0.4.8 → 0.5.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/index.js +1273 -867
- package/dist/server.js +1273 -867
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -302,7 +302,7 @@ function t(key, params) {
|
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
// src/server.ts
|
|
305
|
-
var
|
|
305
|
+
var import_uuid8 = require("uuid");
|
|
306
306
|
var import_promises7 = require("fs/promises");
|
|
307
307
|
var import_node_os9 = require("os");
|
|
308
308
|
var import_node_path9 = require("path");
|
|
@@ -311,7 +311,7 @@ var import_node_util3 = require("util");
|
|
|
311
311
|
|
|
312
312
|
// src/providers/ProcessProvider.ts
|
|
313
313
|
var import_child_process = require("child_process");
|
|
314
|
-
var
|
|
314
|
+
var import_readline2 = require("readline");
|
|
315
315
|
var import_events = require("events");
|
|
316
316
|
var import_node_os2 = require("os");
|
|
317
317
|
var import_uuid = require("uuid");
|
|
@@ -362,12 +362,60 @@ function isNormalExit(code, signal) {
|
|
|
362
362
|
}
|
|
363
363
|
|
|
364
364
|
// src/utils/claudePath.ts
|
|
365
|
+
function resolveStable(candidate) {
|
|
366
|
+
if (!candidate) return null;
|
|
367
|
+
try {
|
|
368
|
+
const real = (0, import_node_fs.realpathSync)(candidate.trim());
|
|
369
|
+
(0, import_node_fs.accessSync)(real, import_node_fs.constants.X_OK);
|
|
370
|
+
return real;
|
|
371
|
+
} catch {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function resolveViaLoginShell() {
|
|
376
|
+
if (isWindows) return null;
|
|
377
|
+
const shell = process.env.SHELL || "/bin/zsh";
|
|
378
|
+
try {
|
|
379
|
+
const out = (0, import_node_child_process2.execSync)(`${shell} -ilc 'command -v claude' 2>/dev/null`, {
|
|
380
|
+
encoding: "utf-8",
|
|
381
|
+
timeout: 8e3
|
|
382
|
+
}).trim().split("\n").filter(Boolean).pop();
|
|
383
|
+
return resolveStable(out);
|
|
384
|
+
} catch {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function resolveViaFnm() {
|
|
389
|
+
if (isWindows) return null;
|
|
390
|
+
const base = (0, import_node_path.join)((0, import_node_os.homedir)(), ".fnm", "node-versions");
|
|
391
|
+
try {
|
|
392
|
+
const versions = (0, import_node_fs.readdirSync)(base).filter((v) => /^v?\d+\./.test(v)).sort(
|
|
393
|
+
(a, b) => b.localeCompare(a, void 0, { numeric: true, sensitivity: "base" })
|
|
394
|
+
);
|
|
395
|
+
for (const v of versions) {
|
|
396
|
+
const p = resolveStable((0, import_node_path.join)(base, v, "installation", "bin", "claude"));
|
|
397
|
+
if (p) return p;
|
|
398
|
+
}
|
|
399
|
+
} catch {
|
|
400
|
+
}
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
var cached = null;
|
|
365
404
|
function findClaudePath() {
|
|
405
|
+
if (cached) return cached;
|
|
406
|
+
const override = resolveStable(process.env.SESSIX_CLAUDE_PATH);
|
|
407
|
+
if (override) return cached = log(override, "env:SESSIX_CLAUDE_PATH");
|
|
366
408
|
try {
|
|
367
409
|
const cmd = isWindows ? "where claude" : "which claude";
|
|
368
|
-
|
|
410
|
+
const which = (0, import_node_child_process2.execSync)(cmd, { encoding: "utf-8" }).trim().split("\n")[0];
|
|
411
|
+
const stable = resolveStable(which);
|
|
412
|
+
if (stable) return cached = log(stable, "which");
|
|
369
413
|
} catch {
|
|
370
414
|
}
|
|
415
|
+
const fnm = resolveViaFnm();
|
|
416
|
+
if (fnm) return cached = log(fnm, "fnm-scan");
|
|
417
|
+
const viaShell = resolveViaLoginShell();
|
|
418
|
+
if (viaShell) return cached = log(viaShell, "login-shell");
|
|
371
419
|
const candidates = isWindows ? [
|
|
372
420
|
(0, import_node_path.join)(process.env.LOCALAPPDATA ?? "", "Programs", "claude", "claude.exe"),
|
|
373
421
|
(0, import_node_path.join)((0, import_node_os.homedir)(), "AppData", "Local", "Programs", "claude", "claude.exe"),
|
|
@@ -378,153 +426,521 @@ function findClaudePath() {
|
|
|
378
426
|
"/opt/homebrew/bin/claude"
|
|
379
427
|
];
|
|
380
428
|
for (const candidate of candidates) {
|
|
429
|
+
const stable = resolveStable(candidate);
|
|
430
|
+
if (stable) return cached = log(stable, "candidate");
|
|
431
|
+
}
|
|
432
|
+
console.warn(
|
|
433
|
+
"[claudePath] \u672A\u80FD\u5B9A\u4F4D claude\uFF0C\u515C\u5E95\u4F7F\u7528\u88F8 'claude'\uFF08PATH \u4E0D\u542B claude \u65F6\u4F1A\u5931\u8D25\uFF09"
|
|
434
|
+
);
|
|
435
|
+
return cached = "claude";
|
|
436
|
+
}
|
|
437
|
+
function log(path2, via) {
|
|
438
|
+
console.log(`[claudePath] \u89E3\u6790\u5230 claude: ${path2} (via ${via})`);
|
|
439
|
+
return path2;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// src/session/ProjectReader.ts
|
|
443
|
+
var import_promises = require("fs/promises");
|
|
444
|
+
var import_readline = require("readline");
|
|
445
|
+
var import_path = require("path");
|
|
446
|
+
var import_os = require("os");
|
|
447
|
+
var CLAUDE_PROJECTS_DIR = (0, import_path.join)((0, import_os.homedir)(), ".claude", "projects");
|
|
448
|
+
function getSessionFilePath(projectPath, sessionId) {
|
|
449
|
+
return (0, import_path.join)(CLAUDE_PROJECTS_DIR, encodeDirName(projectPath), `${sessionId}.jsonl`);
|
|
450
|
+
}
|
|
451
|
+
async function getSessionModel(projectPath, sessionId) {
|
|
452
|
+
const filePath = getSessionFilePath(projectPath, sessionId);
|
|
453
|
+
const raw = await (0, import_promises.readFile)(filePath, "utf-8").catch((err) => {
|
|
454
|
+
if (err.code === "ENOENT") return null;
|
|
455
|
+
throw err;
|
|
456
|
+
});
|
|
457
|
+
if (raw === null) return void 0;
|
|
458
|
+
const lines = raw.split("\n");
|
|
459
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
460
|
+
const line = lines[i].trim();
|
|
461
|
+
if (!line) continue;
|
|
381
462
|
try {
|
|
382
|
-
|
|
383
|
-
|
|
463
|
+
const obj = JSON.parse(line);
|
|
464
|
+
if (obj.type !== "assistant" || !obj.message) continue;
|
|
465
|
+
const model = obj.message.model;
|
|
466
|
+
if (typeof model === "string" && model && model !== "unknown") {
|
|
467
|
+
return model;
|
|
468
|
+
}
|
|
384
469
|
} catch {
|
|
385
470
|
}
|
|
386
471
|
}
|
|
387
|
-
return
|
|
472
|
+
return void 0;
|
|
388
473
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
activeSessions = /* @__PURE__ */ new Map();
|
|
395
|
-
/** 事件发射器,用于分发 Claude 事件流 */
|
|
396
|
-
emitter = new import_events.EventEmitter();
|
|
397
|
-
/** 已发射的 AskUserQuestion toolUseId 集合,按会话隔离(避免 partial message 重复触发) */
|
|
398
|
-
emittedQuestionToolUseIds = /* @__PURE__ */ new Map();
|
|
399
|
-
/**
|
|
400
|
-
* 启动新会话或恢复已有会话
|
|
401
|
-
*
|
|
402
|
-
* 会 spawn 一个 `claude` CLI 进程,设置工作目录和环境变量,
|
|
403
|
-
* 并开始监听 stdout 的 NDJSON 输出。
|
|
404
|
-
*/
|
|
405
|
-
async startSession(opts) {
|
|
406
|
-
const { projectPath, message, sessionId: existingSessionId, model, permissionMode, effort, images, fallbackModel, maxBudgetUsd } = opts;
|
|
407
|
-
const sessionId = existingSessionId ?? (0, import_uuid.v4)();
|
|
408
|
-
if (this.activeSessions.has(sessionId)) {
|
|
409
|
-
await this.killSession(sessionId);
|
|
474
|
+
async function getProjects() {
|
|
475
|
+
try {
|
|
476
|
+
const dirExists = await directoryExists(CLAUDE_PROJECTS_DIR);
|
|
477
|
+
if (!dirExists) {
|
|
478
|
+
return { ok: true, value: [] };
|
|
410
479
|
}
|
|
411
|
-
const
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
480
|
+
const entries = await (0, import_promises.readdir)(CLAUDE_PROJECTS_DIR, { withFileTypes: true });
|
|
481
|
+
const projects = [];
|
|
482
|
+
for (const entry of entries) {
|
|
483
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const encodedPath = entry.name;
|
|
487
|
+
const decodedPath = decodeDirName(encodedPath);
|
|
488
|
+
const name = decodedPath.split("/").filter(Boolean).pop() ?? encodedPath;
|
|
489
|
+
const projectDir = (0, import_path.join)(CLAUDE_PROJECTS_DIR, encodedPath);
|
|
490
|
+
const { count: sessionCount, latestMtime } = await countJsonlFilesWithMtime(projectDir);
|
|
491
|
+
projects.push({
|
|
492
|
+
id: encodedPath,
|
|
493
|
+
path: decodedPath,
|
|
494
|
+
name,
|
|
495
|
+
sessionCount,
|
|
496
|
+
lastActiveAt: latestMtime
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
projects.sort((a, b) => a.name.localeCompare(b.name));
|
|
500
|
+
return { ok: true, value: projects };
|
|
501
|
+
} catch (err) {
|
|
502
|
+
return {
|
|
503
|
+
ok: false,
|
|
504
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
420
505
|
};
|
|
421
|
-
const resume = opts.resume ?? !!existingSessionId;
|
|
422
|
-
const proc = this.spawnClaudeProcess(sessionId, projectPath, resume, model, permissionMode, effort, fallbackModel, maxBudgetUsd);
|
|
423
|
-
this.writeUserMessage(proc, message, sessionId, images);
|
|
424
|
-
session.pid = proc.pid;
|
|
425
|
-
this.activeSessions.set(sessionId, { session, process: proc, model, permissionMode, effort, fallbackModel, maxBudgetUsd });
|
|
426
|
-
proc.on("error", (err) => {
|
|
427
|
-
console.error(`[ProcessProvider] Session ${sessionId} process error:`, err.message);
|
|
428
|
-
this.activeSessions.delete(sessionId);
|
|
429
|
-
const syntheticResult = {
|
|
430
|
-
type: "result",
|
|
431
|
-
subtype: "error",
|
|
432
|
-
result: `Process spawn failed: ${err.message}`,
|
|
433
|
-
session_id: sessionId,
|
|
434
|
-
duration_ms: 0,
|
|
435
|
-
is_error: true,
|
|
436
|
-
num_turns: 0
|
|
437
|
-
};
|
|
438
|
-
this.emitter.emit(this.getEventName(sessionId), syntheticResult);
|
|
439
|
-
});
|
|
440
|
-
this.attachStdoutListener(sessionId, proc);
|
|
441
|
-
this.attachStderrListener(sessionId, proc);
|
|
442
|
-
this.attachExitListener(sessionId, proc);
|
|
443
|
-
return session;
|
|
444
506
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
return;
|
|
507
|
+
}
|
|
508
|
+
async function getHistoricalSessions(projectPath) {
|
|
509
|
+
try {
|
|
510
|
+
const encodedPath = encodeDirName(projectPath);
|
|
511
|
+
const projectDir = (0, import_path.join)(CLAUDE_PROJECTS_DIR, encodedPath);
|
|
512
|
+
const dirExists = await directoryExists(projectDir);
|
|
513
|
+
if (!dirExists) {
|
|
514
|
+
return { ok: true, value: [] };
|
|
454
515
|
}
|
|
455
|
-
|
|
516
|
+
const entries = await (0, import_promises.readdir)(projectDir, { withFileTypes: true });
|
|
517
|
+
const jsonlFiles = entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl"));
|
|
518
|
+
const mtimeMap = /* @__PURE__ */ new Map();
|
|
519
|
+
await Promise.all(
|
|
520
|
+
jsonlFiles.map(async (entry) => {
|
|
521
|
+
const sessionId = entry.name.slice(0, -6);
|
|
522
|
+
const filePath = (0, import_path.join)(projectDir, entry.name);
|
|
523
|
+
try {
|
|
524
|
+
const contentTs = await extractLastTimestamp(filePath);
|
|
525
|
+
if (contentTs) {
|
|
526
|
+
mtimeMap.set(sessionId, contentTs);
|
|
527
|
+
} else {
|
|
528
|
+
const fileStat = await (0, import_promises.stat)(filePath);
|
|
529
|
+
mtimeMap.set(sessionId, fileStat.mtimeMs);
|
|
530
|
+
}
|
|
531
|
+
} catch {
|
|
532
|
+
mtimeMap.set(sessionId, 0);
|
|
533
|
+
}
|
|
534
|
+
})
|
|
535
|
+
);
|
|
536
|
+
const uuidDirs = entries.filter(
|
|
537
|
+
(e) => e.isDirectory() && UUID_RE.test(e.name) && !mtimeMap.has(e.name)
|
|
538
|
+
);
|
|
539
|
+
for (const entry of uuidDirs) {
|
|
456
540
|
try {
|
|
457
|
-
|
|
541
|
+
const fileStat = await (0, import_promises.stat)((0, import_path.join)(projectDir, entry.name));
|
|
542
|
+
mtimeMap.set(entry.name, fileStat.mtimeMs);
|
|
458
543
|
} catch {
|
|
544
|
+
mtimeMap.set(entry.name, 0);
|
|
459
545
|
}
|
|
460
|
-
await killProcessCrossPlatform(entry.process);
|
|
461
|
-
}
|
|
462
|
-
this.emittedQuestionToolUseIds.delete(sessionId);
|
|
463
|
-
this.activeSessions.delete(sessionId);
|
|
464
|
-
}
|
|
465
|
-
/**
|
|
466
|
-
* 向已有会话发送新消息
|
|
467
|
-
*
|
|
468
|
-
* 快速路径:进程存活时直接写 stdin(毫秒级响应)。
|
|
469
|
-
* 慢速路径:进程已退出时 respawn 并 --resume。
|
|
470
|
-
*/
|
|
471
|
-
async sendMessage(sessionId, message, permissionMode, images) {
|
|
472
|
-
const entry = this.activeSessions.get(sessionId);
|
|
473
|
-
if (!entry) {
|
|
474
|
-
throw new Error(`Session ${sessionId} not found or already ended`);
|
|
475
546
|
}
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
547
|
+
const indexPath = (0, import_path.join)(projectDir, "sessions-index.json");
|
|
548
|
+
const sessionMap = /* @__PURE__ */ new Map();
|
|
549
|
+
try {
|
|
550
|
+
const indexContent = await (0, import_promises.readFile)(indexPath, "utf-8");
|
|
551
|
+
const indexData = JSON.parse(indexContent);
|
|
552
|
+
if (indexData.version === 1 && Array.isArray(indexData.entries)) {
|
|
553
|
+
for (const entry of indexData.entries) {
|
|
554
|
+
const mtime = mtimeMap.get(entry.sessionId) ?? entry.fileMtime ?? (entry.modified ? new Date(entry.modified).getTime() : 0);
|
|
555
|
+
sessionMap.set(entry.sessionId, {
|
|
556
|
+
sessionId: entry.sessionId,
|
|
557
|
+
lastModified: mtime,
|
|
558
|
+
summary: entry.summary,
|
|
559
|
+
firstPrompt: entry.firstPrompt,
|
|
560
|
+
messageCount: entry.messageCount
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
await Promise.all(
|
|
564
|
+
Array.from(sessionMap.values()).filter((s) => (s.messageCount ?? 0) > 0 && !s.summary && !s.firstPrompt).map(async (s) => {
|
|
565
|
+
const filePath = (0, import_path.join)(projectDir, `${s.sessionId}.jsonl`);
|
|
566
|
+
const firstPrompt = await extractFirstPrompt(filePath).catch(() => void 0);
|
|
567
|
+
if (firstPrompt) s.firstPrompt = firstPrompt;
|
|
568
|
+
})
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
} catch {
|
|
482
572
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
if (
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
573
|
+
const uuidDirSet = new Set(uuidDirs.map((e) => e.name));
|
|
574
|
+
for (const [sessionId, mtime] of mtimeMap) {
|
|
575
|
+
if (!sessionMap.has(sessionId)) {
|
|
576
|
+
if (uuidDirSet.has(sessionId)) {
|
|
577
|
+
sessionMap.set(sessionId, { sessionId, lastModified: mtime, messageCount: -1 });
|
|
578
|
+
} else {
|
|
579
|
+
const filePath = (0, import_path.join)(projectDir, `${sessionId}.jsonl`);
|
|
580
|
+
const firstPrompt = await extractFirstPrompt(filePath).catch(() => void 0);
|
|
581
|
+
sessionMap.set(sessionId, { sessionId, lastModified: mtime, firstPrompt });
|
|
489
582
|
}
|
|
490
|
-
killProcessCrossPlatform(entry.process);
|
|
491
583
|
}
|
|
492
|
-
} else {
|
|
493
|
-
console.log(`[ProcessProvider] Session ${sessionId}: process exited, respawning`);
|
|
494
584
|
}
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
entry.session.lastActiveAt = Date.now();
|
|
501
|
-
entry.session.pid = proc.pid;
|
|
502
|
-
entry.process = proc;
|
|
503
|
-
entry.permissionMode = newMode;
|
|
504
|
-
entry.pendingQuestion = savedPendingQuestion;
|
|
505
|
-
proc.on("error", (err) => {
|
|
506
|
-
console.error(`[ProcessProvider] Session ${sessionId} sendMessage process error:`, err.message);
|
|
507
|
-
this.activeSessions.delete(sessionId);
|
|
508
|
-
const syntheticResult = {
|
|
509
|
-
type: "result",
|
|
510
|
-
subtype: "error",
|
|
511
|
-
result: `Failed to send message: ${err.message}`,
|
|
512
|
-
session_id: sessionId,
|
|
513
|
-
duration_ms: 0,
|
|
514
|
-
is_error: true,
|
|
515
|
-
num_turns: 0
|
|
516
|
-
};
|
|
517
|
-
this.emitter.emit(this.getEventName(sessionId), syntheticResult);
|
|
585
|
+
const sessions = Array.from(sessionMap.values()).filter((s) => {
|
|
586
|
+
if (s.messageCount === 0) return false;
|
|
587
|
+
if (s.messageCount === -1) return true;
|
|
588
|
+
if (s.firstPrompt === void 0 && s.messageCount === void 0) return false;
|
|
589
|
+
return true;
|
|
518
590
|
});
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
591
|
+
sessions.sort((a, b) => b.lastModified - a.lastModified);
|
|
592
|
+
return { ok: true, value: sessions };
|
|
593
|
+
} catch (err) {
|
|
594
|
+
return {
|
|
595
|
+
ok: false,
|
|
596
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
597
|
+
};
|
|
522
598
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
599
|
+
}
|
|
600
|
+
async function getSessionHistory(projectPath, sessionId) {
|
|
601
|
+
try {
|
|
602
|
+
const encodedPath = encodeDirName(projectPath);
|
|
603
|
+
const filePath = (0, import_path.join)(CLAUDE_PROJECTS_DIR, encodedPath, `${sessionId}.jsonl`);
|
|
604
|
+
const raw = await (0, import_promises.readFile)(filePath, "utf-8").catch((err) => {
|
|
605
|
+
if (err.code === "ENOENT") return null;
|
|
606
|
+
throw err;
|
|
607
|
+
});
|
|
608
|
+
if (raw === null) return { ok: true, value: [] };
|
|
609
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
610
|
+
const events = [];
|
|
611
|
+
for (const line of lines) {
|
|
612
|
+
try {
|
|
613
|
+
const obj = JSON.parse(line);
|
|
614
|
+
const type = obj.type;
|
|
615
|
+
if (type === "user" && obj.message) {
|
|
616
|
+
const msgContent = obj.message.content;
|
|
617
|
+
if (typeof msgContent === "string") {
|
|
618
|
+
if (msgContent.includes("<local-command") || msgContent.includes("<command-name>")) continue;
|
|
619
|
+
} else if (Array.isArray(msgContent)) {
|
|
620
|
+
const hasText = msgContent.some(
|
|
621
|
+
(b) => b.type === "text" && !b.text?.includes("<local-command") && !b.text?.includes("<command-name>")
|
|
622
|
+
);
|
|
623
|
+
if (!hasText) continue;
|
|
624
|
+
}
|
|
625
|
+
const normalizedContent = typeof msgContent === "string" ? [{ type: "text", text: msgContent }] : Array.isArray(msgContent) ? msgContent.filter((b) => b.type === "text" && typeof b.text === "string") : [];
|
|
626
|
+
if (normalizedContent.length === 0) continue;
|
|
627
|
+
events.push({
|
|
628
|
+
type: "user",
|
|
629
|
+
message: {
|
|
630
|
+
...obj.message,
|
|
631
|
+
content: normalizedContent
|
|
632
|
+
},
|
|
633
|
+
session_id: sessionId
|
|
634
|
+
});
|
|
635
|
+
} else if (type === "assistant" && obj.message) {
|
|
636
|
+
const content = (obj.message.content ?? []).filter(
|
|
637
|
+
(b) => b.type === "text" || b.type === "tool_use" || b.type === "thinking"
|
|
638
|
+
);
|
|
639
|
+
if (content.length === 0) continue;
|
|
640
|
+
events.push({
|
|
641
|
+
type: "assistant",
|
|
642
|
+
message: {
|
|
643
|
+
id: obj.message.id ?? obj.uuid ?? `hist-${events.length}`,
|
|
644
|
+
model: obj.message.model ?? "unknown",
|
|
645
|
+
role: "assistant",
|
|
646
|
+
content,
|
|
647
|
+
stop_reason: obj.message.stop_reason,
|
|
648
|
+
usage: obj.message.usage
|
|
649
|
+
},
|
|
650
|
+
session_id: sessionId
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
} catch {
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (events.length > 0) {
|
|
657
|
+
let totalInputTokens = 0;
|
|
658
|
+
let totalOutputTokens = 0;
|
|
659
|
+
for (const ev of events) {
|
|
660
|
+
if (ev.type === "assistant" && ev.message.usage) {
|
|
661
|
+
totalInputTokens += ev.message.usage.input_tokens ?? 0;
|
|
662
|
+
totalOutputTokens += ev.message.usage.output_tokens ?? 0;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (totalInputTokens > 0 || totalOutputTokens > 0) {
|
|
666
|
+
events.push({
|
|
667
|
+
type: "result",
|
|
668
|
+
subtype: "success",
|
|
669
|
+
is_error: false,
|
|
670
|
+
duration_ms: 0,
|
|
671
|
+
num_turns: events.filter((e) => e.type === "user").length,
|
|
672
|
+
result: "",
|
|
673
|
+
session_id: sessionId,
|
|
674
|
+
usage: { input_tokens: totalInputTokens, output_tokens: totalOutputTokens }
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return { ok: true, value: events };
|
|
679
|
+
} catch (err) {
|
|
680
|
+
return {
|
|
681
|
+
ok: false,
|
|
682
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
async function extractLastTimestamp(filePath) {
|
|
687
|
+
let fileHandle;
|
|
688
|
+
try {
|
|
689
|
+
fileHandle = await (0, import_promises.open)(filePath, "r");
|
|
690
|
+
const fileStat = await fileHandle.stat();
|
|
691
|
+
const readSize = Math.min(fileStat.size, 8192);
|
|
692
|
+
const buffer = Buffer.alloc(readSize);
|
|
693
|
+
await fileHandle.read(buffer, 0, readSize, fileStat.size - readSize);
|
|
694
|
+
const tail = buffer.toString("utf-8");
|
|
695
|
+
const lines = tail.split("\n").filter((l) => l.trim());
|
|
696
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
697
|
+
try {
|
|
698
|
+
const obj = JSON.parse(lines[i]);
|
|
699
|
+
if (obj.timestamp) {
|
|
700
|
+
const ts = new Date(obj.timestamp).getTime();
|
|
701
|
+
if (!isNaN(ts)) return ts;
|
|
702
|
+
}
|
|
703
|
+
} catch {
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
} catch {
|
|
707
|
+
} finally {
|
|
708
|
+
await fileHandle?.close();
|
|
709
|
+
}
|
|
710
|
+
return void 0;
|
|
711
|
+
}
|
|
712
|
+
async function extractFirstPrompt(filePath) {
|
|
713
|
+
let fileHandle;
|
|
714
|
+
try {
|
|
715
|
+
fileHandle = await (0, import_promises.open)(filePath, "r");
|
|
716
|
+
const rl = (0, import_readline.createInterface)({
|
|
717
|
+
input: fileHandle.createReadStream({ encoding: "utf-8" }),
|
|
718
|
+
crlfDelay: Infinity
|
|
719
|
+
});
|
|
720
|
+
let lineCount = 0;
|
|
721
|
+
for await (const line of rl) {
|
|
722
|
+
if (++lineCount > 20) break;
|
|
723
|
+
if (!line.trim()) continue;
|
|
724
|
+
try {
|
|
725
|
+
const obj = JSON.parse(line);
|
|
726
|
+
if (obj.type === "user" && obj.message) {
|
|
727
|
+
const msgContent = obj.message.content;
|
|
728
|
+
let text = "";
|
|
729
|
+
if (typeof msgContent === "string") {
|
|
730
|
+
text = msgContent;
|
|
731
|
+
} else if (Array.isArray(msgContent)) {
|
|
732
|
+
const textBlock = msgContent.find((b) => b.type === "text" && typeof b.text === "string");
|
|
733
|
+
text = textBlock?.text ?? "";
|
|
734
|
+
}
|
|
735
|
+
if (text && !text.includes("<local-command") && !text.includes("<command-name>")) {
|
|
736
|
+
text = text.replace(/<ide_opened_file>[\s\S]*?<\/ide_opened_file>/gi, "");
|
|
737
|
+
text = text.replace(/<[^>]+>/g, "").trim();
|
|
738
|
+
rl.close();
|
|
739
|
+
return text.length > 80 ? text.slice(0, 80) + "..." : text;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
} catch {
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
} catch {
|
|
746
|
+
} finally {
|
|
747
|
+
await fileHandle?.close();
|
|
748
|
+
}
|
|
749
|
+
return void 0;
|
|
750
|
+
}
|
|
751
|
+
function decodeDirName(dirName) {
|
|
752
|
+
const placeholder = "\0";
|
|
753
|
+
const escaped = dirName.replace(/--/g, placeholder);
|
|
754
|
+
const decoded = escaped.replace(/-/g, "/");
|
|
755
|
+
return decoded.replace(new RegExp(placeholder, "g"), "-");
|
|
756
|
+
}
|
|
757
|
+
function encodeDirName(path2) {
|
|
758
|
+
const escaped = path2.replace(/-/g, "--");
|
|
759
|
+
return escaped.replace(/\//g, "-");
|
|
760
|
+
}
|
|
761
|
+
async function directoryExists(dirPath) {
|
|
762
|
+
try {
|
|
763
|
+
const s = await (0, import_promises.stat)(dirPath);
|
|
764
|
+
return s.isDirectory();
|
|
765
|
+
} catch {
|
|
766
|
+
return false;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
770
|
+
async function countJsonlFilesWithMtime(dirPath) {
|
|
771
|
+
try {
|
|
772
|
+
const entries = await (0, import_promises.readdir)(dirPath, { withFileTypes: true });
|
|
773
|
+
const jsonlNames = new Set(
|
|
774
|
+
entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => e.name.slice(0, -6))
|
|
775
|
+
);
|
|
776
|
+
const uuidDirs = entries.filter(
|
|
777
|
+
(e) => e.isDirectory() && UUID_RE.test(e.name) && !jsonlNames.has(e.name)
|
|
778
|
+
);
|
|
779
|
+
let latestMtime = 0;
|
|
780
|
+
const jsonlEntries = entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl"));
|
|
781
|
+
await Promise.all([
|
|
782
|
+
...jsonlEntries.map(async (entry) => {
|
|
783
|
+
try {
|
|
784
|
+
const contentTs = await extractLastTimestamp((0, import_path.join)(dirPath, entry.name));
|
|
785
|
+
const ts = contentTs ?? (await (0, import_promises.stat)((0, import_path.join)(dirPath, entry.name))).mtimeMs;
|
|
786
|
+
if (ts > latestMtime) latestMtime = ts;
|
|
787
|
+
} catch {
|
|
788
|
+
}
|
|
789
|
+
}),
|
|
790
|
+
...uuidDirs.map(async (entry) => {
|
|
791
|
+
try {
|
|
792
|
+
const fileStat = await (0, import_promises.stat)((0, import_path.join)(dirPath, entry.name));
|
|
793
|
+
if (fileStat.mtimeMs > latestMtime) latestMtime = fileStat.mtimeMs;
|
|
794
|
+
} catch {
|
|
795
|
+
}
|
|
796
|
+
})
|
|
797
|
+
]);
|
|
798
|
+
return { count: jsonlNames.size + uuidDirs.length, latestMtime };
|
|
799
|
+
} catch {
|
|
800
|
+
return { count: 0, latestMtime: 0 };
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// src/providers/ProcessProvider.ts
|
|
805
|
+
var CLAUDE_PATH = findClaudePath();
|
|
806
|
+
var ProcessProvider = class {
|
|
807
|
+
/** 活跃会话映射表:sessionId -> { session, process } */
|
|
808
|
+
activeSessions = /* @__PURE__ */ new Map();
|
|
809
|
+
/** 事件发射器,用于分发 Claude 事件流 */
|
|
810
|
+
emitter = new import_events.EventEmitter();
|
|
811
|
+
/**
|
|
812
|
+
* 启动新会话或恢复已有会话
|
|
813
|
+
*
|
|
814
|
+
* 会 spawn 一个 `claude` CLI 进程,设置工作目录和环境变量,
|
|
815
|
+
* 并开始监听 stdout 的 NDJSON 输出。
|
|
816
|
+
*/
|
|
817
|
+
async startSession(opts) {
|
|
818
|
+
const { projectPath, message, sessionId: existingSessionId, model, permissionMode, effort, images, fallbackModel, maxBudgetUsd } = opts;
|
|
819
|
+
const sessionId = existingSessionId ?? (0, import_uuid.v4)();
|
|
820
|
+
if (this.activeSessions.has(sessionId)) {
|
|
821
|
+
await this.killSession(sessionId);
|
|
822
|
+
}
|
|
823
|
+
const projectId = projectPath.split("/").filter(Boolean).pop() ?? "unknown";
|
|
824
|
+
const session = {
|
|
825
|
+
id: sessionId,
|
|
826
|
+
projectId,
|
|
827
|
+
projectPath,
|
|
828
|
+
status: "running",
|
|
829
|
+
createdAt: Date.now(),
|
|
830
|
+
lastActiveAt: Date.now(),
|
|
831
|
+
summary: message.slice(0, 80)
|
|
832
|
+
};
|
|
833
|
+
const resume = opts.resume ?? !!existingSessionId;
|
|
834
|
+
let effectiveModel = model;
|
|
835
|
+
if (resume && !effectiveModel) {
|
|
836
|
+
effectiveModel = await getSessionModel(projectPath, sessionId).catch(() => void 0);
|
|
837
|
+
if (effectiveModel) {
|
|
838
|
+
console.log(`[ProcessProvider] Session ${sessionId}: resume restored original model "${effectiveModel}"`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
const proc = this.spawnClaudeProcess(sessionId, projectPath, resume, effectiveModel, permissionMode, effort, fallbackModel, maxBudgetUsd);
|
|
842
|
+
this.writeUserMessage(proc, message, sessionId, images);
|
|
843
|
+
session.pid = proc.pid;
|
|
844
|
+
this.activeSessions.set(sessionId, { session, process: proc, model: effectiveModel, permissionMode, effort, fallbackModel, maxBudgetUsd });
|
|
845
|
+
proc.on("error", (err) => {
|
|
846
|
+
console.error(`[ProcessProvider] Session ${sessionId} process error:`, err.message);
|
|
847
|
+
this.activeSessions.delete(sessionId);
|
|
848
|
+
const syntheticResult = {
|
|
849
|
+
type: "result",
|
|
850
|
+
subtype: "error",
|
|
851
|
+
result: `Process spawn failed: ${err.message}`,
|
|
852
|
+
session_id: sessionId,
|
|
853
|
+
duration_ms: 0,
|
|
854
|
+
is_error: true,
|
|
855
|
+
num_turns: 0
|
|
856
|
+
};
|
|
857
|
+
this.emitter.emit(this.getEventName(sessionId), syntheticResult);
|
|
858
|
+
});
|
|
859
|
+
this.attachStdoutListener(sessionId, proc);
|
|
860
|
+
this.attachStderrListener(sessionId, proc);
|
|
861
|
+
this.attachExitListener(sessionId, proc);
|
|
862
|
+
return session;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* 终止指定会话
|
|
866
|
+
*
|
|
867
|
+
* kill 进程并从活跃映射中移除。
|
|
868
|
+
*/
|
|
869
|
+
async killSession(sessionId) {
|
|
870
|
+
const entry = this.activeSessions.get(sessionId);
|
|
871
|
+
if (!entry) {
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
if (entry.process.exitCode === null && entry.process.signalCode === null) {
|
|
875
|
+
try {
|
|
876
|
+
entry.process.stdin?.end();
|
|
877
|
+
} catch {
|
|
878
|
+
}
|
|
879
|
+
await killProcessCrossPlatform(entry.process);
|
|
880
|
+
}
|
|
881
|
+
this.activeSessions.delete(sessionId);
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* 向已有会话发送新消息
|
|
885
|
+
*
|
|
886
|
+
* 快速路径:进程存活时直接写 stdin(毫秒级响应)。
|
|
887
|
+
* 慢速路径:进程已退出时 respawn 并 --resume。
|
|
888
|
+
*/
|
|
889
|
+
async sendMessage(sessionId, message, permissionMode, images) {
|
|
890
|
+
const entry = this.activeSessions.get(sessionId);
|
|
891
|
+
if (!entry) {
|
|
892
|
+
throw new Error(`Session ${sessionId} not found or already ended`);
|
|
893
|
+
}
|
|
894
|
+
const modeChanged = permissionMode != null && permissionMode !== (entry.permissionMode ?? "default");
|
|
895
|
+
if (!modeChanged && entry.process.exitCode === null && entry.process.signalCode === null && !entry.process.stdin?.destroyed) {
|
|
896
|
+
entry.session.status = "running";
|
|
897
|
+
entry.session.lastActiveAt = Date.now();
|
|
898
|
+
this.writeUserMessage(entry.process, message, sessionId, images);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
if (modeChanged) {
|
|
902
|
+
console.log(`[ProcessProvider] Session ${sessionId}: permission mode change ${entry.permissionMode ?? "default"} \u2192 ${permissionMode}, respawn`);
|
|
903
|
+
if (entry.process.exitCode === null && entry.process.signalCode === null) {
|
|
904
|
+
try {
|
|
905
|
+
entry.process.stdin?.end();
|
|
906
|
+
} catch {
|
|
907
|
+
}
|
|
908
|
+
killProcessCrossPlatform(entry.process);
|
|
909
|
+
}
|
|
910
|
+
} else {
|
|
911
|
+
console.log(`[ProcessProvider] Session ${sessionId}: process exited, respawning`);
|
|
912
|
+
}
|
|
913
|
+
const newMode = permissionMode ?? entry.permissionMode;
|
|
914
|
+
const proc = this.spawnClaudeProcess(sessionId, entry.session.projectPath, true, entry.model, newMode, entry.effort, entry.fallbackModel, entry.maxBudgetUsd);
|
|
915
|
+
this.writeUserMessage(proc, message, sessionId, images);
|
|
916
|
+
entry.session.status = "running";
|
|
917
|
+
entry.session.lastActiveAt = Date.now();
|
|
918
|
+
entry.session.pid = proc.pid;
|
|
919
|
+
entry.process = proc;
|
|
920
|
+
entry.permissionMode = newMode;
|
|
921
|
+
proc.on("error", (err) => {
|
|
922
|
+
console.error(`[ProcessProvider] Session ${sessionId} sendMessage process error:`, err.message);
|
|
923
|
+
this.activeSessions.delete(sessionId);
|
|
924
|
+
const syntheticResult = {
|
|
925
|
+
type: "result",
|
|
926
|
+
subtype: "error",
|
|
927
|
+
result: `Failed to send message: ${err.message}`,
|
|
928
|
+
session_id: sessionId,
|
|
929
|
+
duration_ms: 0,
|
|
930
|
+
is_error: true,
|
|
931
|
+
num_turns: 0
|
|
932
|
+
};
|
|
933
|
+
this.emitter.emit(this.getEventName(sessionId), syntheticResult);
|
|
934
|
+
});
|
|
935
|
+
this.attachStdoutListener(sessionId, proc);
|
|
936
|
+
this.attachStderrListener(sessionId, proc);
|
|
937
|
+
this.attachExitListener(sessionId, proc);
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* 订阅指定会话的 Claude 事件流
|
|
941
|
+
*
|
|
942
|
+
* @returns 取消订阅函数
|
|
943
|
+
*/
|
|
528
944
|
onEvent(sessionId, callback) {
|
|
529
945
|
const eventName = this.getEventName(sessionId);
|
|
530
946
|
this.emitter.on(eventName, callback);
|
|
@@ -738,7 +1154,7 @@ var ProcessProvider = class {
|
|
|
738
1154
|
console.warn(`[ProcessProvider] Session ${sessionId}: stdout unavailable`);
|
|
739
1155
|
return;
|
|
740
1156
|
}
|
|
741
|
-
const rl = (0,
|
|
1157
|
+
const rl = (0, import_readline2.createInterface)({
|
|
742
1158
|
input: proc.stdout,
|
|
743
1159
|
crlfDelay: Infinity
|
|
744
1160
|
});
|
|
@@ -752,56 +1168,6 @@ var ProcessProvider = class {
|
|
|
752
1168
|
const result = this.parseLine(trimmed);
|
|
753
1169
|
if (result.ok) {
|
|
754
1170
|
const event = result.value;
|
|
755
|
-
if (event.type === "assistant") {
|
|
756
|
-
for (const block of event.message.content) {
|
|
757
|
-
if (block.type === "tool_use") {
|
|
758
|
-
const isQuestion = block.name === "AskUserQuestion" || block.name === "AskFollowupQuestion";
|
|
759
|
-
if (!isQuestion) continue;
|
|
760
|
-
const input = block.input;
|
|
761
|
-
let question = "";
|
|
762
|
-
let options;
|
|
763
|
-
let questions;
|
|
764
|
-
if (typeof input.question === "string") {
|
|
765
|
-
question = input.question;
|
|
766
|
-
options = Array.isArray(input.options) ? input.options : void 0;
|
|
767
|
-
} else if (Array.isArray(input.questions) && input.questions.length > 0) {
|
|
768
|
-
questions = input.questions.map((q) => {
|
|
769
|
-
const item = {
|
|
770
|
-
question: typeof q.question === "string" ? q.question : "",
|
|
771
|
-
header: typeof q.header === "string" ? q.header : void 0,
|
|
772
|
-
multiSelect: typeof q.multiSelect === "boolean" ? q.multiSelect : void 0
|
|
773
|
-
};
|
|
774
|
-
if (Array.isArray(q.options)) {
|
|
775
|
-
item.options = q.options.map((o) => ({
|
|
776
|
-
label: typeof o.label === "string" ? o.label : String(o),
|
|
777
|
-
description: typeof o.description === "string" ? o.description : void 0
|
|
778
|
-
}));
|
|
779
|
-
}
|
|
780
|
-
return item;
|
|
781
|
-
});
|
|
782
|
-
const first = questions[0];
|
|
783
|
-
question = first.question;
|
|
784
|
-
options = first.options?.map((o) => o.label);
|
|
785
|
-
}
|
|
786
|
-
if (!question) continue;
|
|
787
|
-
const prevKey = `${block.id}:${question}:${JSON.stringify(options ?? [])}`;
|
|
788
|
-
let sessionSet = this.emittedQuestionToolUseIds.get(sessionId);
|
|
789
|
-
if (!sessionSet) {
|
|
790
|
-
sessionSet = /* @__PURE__ */ new Set();
|
|
791
|
-
this.emittedQuestionToolUseIds.set(sessionId, sessionSet);
|
|
792
|
-
}
|
|
793
|
-
if (sessionSet.has(prevKey)) continue;
|
|
794
|
-
sessionSet.add(prevKey);
|
|
795
|
-
console.log(`[ProcessProvider] Session ${sessionId}: detected ${block.name} (toolUseId=${block.id})`);
|
|
796
|
-
this.emitter.emit(this.getQuestionEventName(sessionId), {
|
|
797
|
-
toolUseId: block.id,
|
|
798
|
-
question,
|
|
799
|
-
options,
|
|
800
|
-
questions
|
|
801
|
-
});
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
1171
|
this.updateSessionStatus(sessionId, event);
|
|
806
1172
|
this.emitter.emit(this.getEventName(sessionId), event);
|
|
807
1173
|
} else {
|
|
@@ -932,70 +1298,21 @@ ${context}`;
|
|
|
932
1298
|
proc.once("error", reject);
|
|
933
1299
|
});
|
|
934
1300
|
}
|
|
935
|
-
/**
|
|
936
|
-
* 向正在等待中的 AskUserQuestion 提供答案
|
|
937
|
-
*
|
|
938
|
-
* 将答案写入 Claude 进程的 stdin(作为 tool_result),
|
|
939
|
-
* Claude 收到后继续执行。
|
|
940
|
-
*/
|
|
941
|
-
async answerQuestion(sessionId, toolUseId, answer) {
|
|
942
|
-
const entry = this.activeSessions.get(sessionId);
|
|
943
|
-
if (!entry) {
|
|
944
|
-
throw new Error(`Session ${sessionId} not found`);
|
|
945
|
-
}
|
|
946
|
-
if (!entry.process.stdin || entry.process.stdin.destroyed) {
|
|
947
|
-
throw new Error(`Session ${sessionId} stdin unavailable`);
|
|
948
|
-
}
|
|
949
|
-
const toolResult = JSON.stringify({
|
|
950
|
-
type: "user",
|
|
951
|
-
session_id: "",
|
|
952
|
-
message: {
|
|
953
|
-
role: "user",
|
|
954
|
-
content: [{ type: "tool_result", tool_use_id: toolUseId, content: answer }]
|
|
955
|
-
},
|
|
956
|
-
parent_tool_use_id: toolUseId
|
|
957
|
-
});
|
|
958
|
-
await new Promise((resolve, reject) => {
|
|
959
|
-
entry.process.stdin.write(toolResult + "\n", (err) => {
|
|
960
|
-
if (err) reject(err);
|
|
961
|
-
else resolve();
|
|
962
|
-
});
|
|
963
|
-
});
|
|
964
|
-
console.log(`[ProcessProvider] Session ${sessionId}: AskUserQuestion answered (toolUseId=${toolUseId})`);
|
|
965
|
-
}
|
|
966
|
-
/**
|
|
967
|
-
* 订阅指定会话的 AskUserQuestion 事件
|
|
968
|
-
*
|
|
969
|
-
* @returns 取消订阅函数
|
|
970
|
-
*/
|
|
971
|
-
onQuestion(sessionId, callback) {
|
|
972
|
-
const eventName = this.getQuestionEventName(sessionId);
|
|
973
|
-
this.emitter.on(eventName, callback);
|
|
974
|
-
return () => {
|
|
975
|
-
this.emitter.off(eventName, callback);
|
|
976
|
-
};
|
|
977
|
-
}
|
|
978
1301
|
/**
|
|
979
1302
|
* 生成事件名称
|
|
980
1303
|
*/
|
|
981
1304
|
getEventName(sessionId) {
|
|
982
1305
|
return `claude:${sessionId}`;
|
|
983
1306
|
}
|
|
984
|
-
/**
|
|
985
|
-
* 生成 AskUserQuestion 内部事件名称
|
|
986
|
-
*/
|
|
987
|
-
getQuestionEventName(sessionId) {
|
|
988
|
-
return `question:${sessionId}`;
|
|
989
|
-
}
|
|
990
1307
|
};
|
|
991
1308
|
|
|
992
1309
|
// src/providers/CodexProvider.ts
|
|
993
1310
|
var import_child_process2 = require("child_process");
|
|
994
|
-
var
|
|
1311
|
+
var import_readline3 = require("readline");
|
|
995
1312
|
var import_events2 = require("events");
|
|
996
1313
|
var import_fs = require("fs");
|
|
997
|
-
var
|
|
998
|
-
var
|
|
1314
|
+
var import_path2 = require("path");
|
|
1315
|
+
var import_os2 = require("os");
|
|
999
1316
|
var import_uuid2 = require("uuid");
|
|
1000
1317
|
|
|
1001
1318
|
// src/utils/codexPath.ts
|
|
@@ -1074,9 +1391,9 @@ function isCodexAvailable() {
|
|
|
1074
1391
|
}
|
|
1075
1392
|
|
|
1076
1393
|
// src/providers/CodexProvider.ts
|
|
1077
|
-
var SESSIX_DIR = (0,
|
|
1078
|
-
var CODEX_SESSIONS_FILE = (0,
|
|
1079
|
-
var CODEX_EVENTS_DIR = (0,
|
|
1394
|
+
var SESSIX_DIR = (0, import_path2.join)((0, import_os2.homedir)(), ".sessix");
|
|
1395
|
+
var CODEX_SESSIONS_FILE = (0, import_path2.join)(SESSIX_DIR, "codex-sessions.json");
|
|
1396
|
+
var CODEX_EVENTS_DIR = (0, import_path2.join)(SESSIX_DIR, "codex-events");
|
|
1080
1397
|
var SESSION_EXPIRY_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
1081
1398
|
var CodexProvider = class {
|
|
1082
1399
|
activeSessions = /* @__PURE__ */ new Map();
|
|
@@ -1223,12 +1540,6 @@ var CodexProvider = class {
|
|
|
1223
1540
|
async generateSuggestion(_context) {
|
|
1224
1541
|
return "";
|
|
1225
1542
|
}
|
|
1226
|
-
async answerQuestion(_sessionId, _toolUseId, _answer) {
|
|
1227
|
-
}
|
|
1228
|
-
onQuestion(_sessionId, _callback) {
|
|
1229
|
-
return () => {
|
|
1230
|
-
};
|
|
1231
|
-
}
|
|
1232
1543
|
// ============================================
|
|
1233
1544
|
// 私有方法
|
|
1234
1545
|
// ============================================
|
|
@@ -1280,7 +1591,7 @@ var CodexProvider = class {
|
|
|
1280
1591
|
*/
|
|
1281
1592
|
attachStdoutListener(sessionId, proc) {
|
|
1282
1593
|
if (!proc.stdout) return;
|
|
1283
|
-
const rl = (0,
|
|
1594
|
+
const rl = (0, import_readline3.createInterface)({ input: proc.stdout, crlfDelay: Infinity });
|
|
1284
1595
|
const entry = this.activeSessions.get(sessionId);
|
|
1285
1596
|
if (entry) entry.rl = rl;
|
|
1286
1597
|
rl.on("line", (line) => {
|
|
@@ -1562,9 +1873,9 @@ var CodexProvider = class {
|
|
|
1562
1873
|
* 优先从内存读,miss 时从磁盘加载
|
|
1563
1874
|
*/
|
|
1564
1875
|
getSessionHistory(sessionId) {
|
|
1565
|
-
const
|
|
1566
|
-
if (
|
|
1567
|
-
const filePath = (0,
|
|
1876
|
+
const cached2 = this.sessionEvents.get(sessionId);
|
|
1877
|
+
if (cached2 && cached2.length > 0) return cached2;
|
|
1878
|
+
const filePath = (0, import_path2.join)(CODEX_EVENTS_DIR, `${sessionId}.json`);
|
|
1568
1879
|
try {
|
|
1569
1880
|
if (!(0, import_fs.existsSync)(filePath)) return [];
|
|
1570
1881
|
const data = JSON.parse((0, import_fs.readFileSync)(filePath, "utf-8"));
|
|
@@ -1585,7 +1896,7 @@ var CodexProvider = class {
|
|
|
1585
1896
|
(0, import_fs.mkdirSync)(CODEX_EVENTS_DIR, { recursive: true });
|
|
1586
1897
|
}
|
|
1587
1898
|
(0, import_fs.writeFileSync)(
|
|
1588
|
-
(0,
|
|
1899
|
+
(0, import_path2.join)(CODEX_EVENTS_DIR, `${sessionId}.json`),
|
|
1589
1900
|
JSON.stringify(events),
|
|
1590
1901
|
"utf-8"
|
|
1591
1902
|
);
|
|
@@ -1610,7 +1921,7 @@ var CodexProvider = class {
|
|
|
1610
1921
|
if (now - m.lastActiveAt > SESSION_EXPIRY_MS) {
|
|
1611
1922
|
expiredCount++;
|
|
1612
1923
|
try {
|
|
1613
|
-
const eventsFile = (0,
|
|
1924
|
+
const eventsFile = (0, import_path2.join)(CODEX_EVENTS_DIR, `${sessionId}.json`);
|
|
1614
1925
|
if ((0, import_fs.existsSync)(eventsFile)) (0, import_fs.unlinkSync)(eventsFile);
|
|
1615
1926
|
} catch {
|
|
1616
1927
|
}
|
|
@@ -1723,7 +2034,6 @@ var ProviderFactory = class {
|
|
|
1723
2034
|
};
|
|
1724
2035
|
|
|
1725
2036
|
// src/session/SessionManager.ts
|
|
1726
|
-
var import_uuid3 = require("uuid");
|
|
1727
2037
|
var BUFFER_MAX = 5e3;
|
|
1728
2038
|
var SessionManager = class {
|
|
1729
2039
|
provider;
|
|
@@ -1902,6 +2212,21 @@ var SessionManager = class {
|
|
|
1902
2212
|
this.updateSessionStatus(sessionId, "running");
|
|
1903
2213
|
}
|
|
1904
2214
|
}
|
|
2215
|
+
/**
|
|
2216
|
+
* 幂等清理单个待回答问题(由 ApprovalProxy onQuestionResolved 触发:
|
|
2217
|
+
* 答案到达 / 325s 超时 / 会话 kill / 服务关闭)。
|
|
2218
|
+
* 已不存在则静默返回(不打 warn,与 handleQuestionResponse 区分)。
|
|
2219
|
+
*/
|
|
2220
|
+
clearPendingQuestion(requestId) {
|
|
2221
|
+
const pending = this.pendingQuestions.get(requestId);
|
|
2222
|
+
if (!pending) return;
|
|
2223
|
+
const { sessionId } = pending;
|
|
2224
|
+
this.pendingQuestions.delete(requestId);
|
|
2225
|
+
pending.resolve("");
|
|
2226
|
+
if (!this.hasPendingQuestionsForSession(sessionId)) {
|
|
2227
|
+
this.updateSessionStatus(sessionId, "running");
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
1905
2230
|
/**
|
|
1906
2231
|
* 获取指定会话的所有待回答问题(用于重连时恢复)
|
|
1907
2232
|
*/
|
|
@@ -2043,6 +2368,9 @@ var SessionManager = class {
|
|
|
2043
2368
|
clearTimeout(pending.timer);
|
|
2044
2369
|
}
|
|
2045
2370
|
this.pendingAssistantEvents.clear();
|
|
2371
|
+
for (const pending of this.pendingQuestions.values()) {
|
|
2372
|
+
pending.resolve("");
|
|
2373
|
+
}
|
|
2046
2374
|
this.pendingQuestions.clear();
|
|
2047
2375
|
this.lastBroadcastStatus.clear();
|
|
2048
2376
|
this.eventCallbacks.length = 0;
|
|
@@ -2052,22 +2380,15 @@ var SessionManager = class {
|
|
|
2052
2380
|
// 内部方法
|
|
2053
2381
|
// ============================================
|
|
2054
2382
|
/**
|
|
2055
|
-
*
|
|
2383
|
+
* 订阅指定会话的事件流(AskUserQuestion 已改由 ApprovalProxy hook 驱动)
|
|
2056
2384
|
*/
|
|
2057
2385
|
subscribeToSession(sessionId) {
|
|
2058
2386
|
const provider = this.getProviderForSession(sessionId);
|
|
2059
2387
|
const unsubscribeEvent = provider.onEvent(sessionId, (event) => {
|
|
2060
2388
|
this.handleClaudeEvent(sessionId, event);
|
|
2061
2389
|
});
|
|
2062
|
-
const unsubscribeQuestion = provider.onQuestion(
|
|
2063
|
-
sessionId,
|
|
2064
|
-
({ toolUseId, question, options, questions }) => {
|
|
2065
|
-
this.handleAskUserQuestion(sessionId, toolUseId, question, options, questions);
|
|
2066
|
-
}
|
|
2067
|
-
);
|
|
2068
2390
|
this.unsubscribeMap.set(sessionId, () => {
|
|
2069
2391
|
unsubscribeEvent();
|
|
2070
|
-
unsubscribeQuestion();
|
|
2071
2392
|
});
|
|
2072
2393
|
}
|
|
2073
2394
|
/**
|
|
@@ -2216,55 +2537,34 @@ var SessionManager = class {
|
|
|
2216
2537
|
return runningStartedAt ? { ...base, runningStartedAt } : base;
|
|
2217
2538
|
}
|
|
2218
2539
|
/**
|
|
2219
|
-
*
|
|
2540
|
+
* 由 ApprovalProxy 在 PreToolUse hook 拦截到 AskUserQuestion 时调用。
|
|
2541
|
+
* 登记 pendingQuestion、广播 question_request、置 waiting_question,
|
|
2542
|
+
* 返回的 Promise 在 handleQuestionResponse 时 resolve。
|
|
2220
2543
|
*/
|
|
2221
|
-
|
|
2222
|
-
const existingEntry = Array.from(this.pendingQuestions.entries()).find(
|
|
2223
|
-
([, v]) => v.toolUseId === toolUseId
|
|
2224
|
-
);
|
|
2225
|
-
if (existingEntry) {
|
|
2226
|
-
const [existingRequestId, existingPending] = existingEntry;
|
|
2227
|
-
existingPending.question = question;
|
|
2228
|
-
existingPending.options = options;
|
|
2229
|
-
existingPending.questions = questions;
|
|
2230
|
-
existingPending.createdAt = Date.now();
|
|
2231
|
-
const updatedRequest = {
|
|
2232
|
-
id: existingRequestId,
|
|
2233
|
-
sessionId,
|
|
2234
|
-
toolUseId,
|
|
2235
|
-
question,
|
|
2236
|
-
options,
|
|
2237
|
-
questions,
|
|
2238
|
-
createdAt: existingPending.createdAt
|
|
2239
|
-
};
|
|
2240
|
-
this.emit({ type: "question_request", request: updatedRequest });
|
|
2241
|
-
console.log(`[SessionManager] Session ${sessionId}: AskUserQuestion updated (requestId=${existingRequestId})`);
|
|
2242
|
-
return;
|
|
2243
|
-
}
|
|
2244
|
-
const requestId = (0, import_uuid3.v4)();
|
|
2544
|
+
askQuestion(sessionId, toolUseId, questions, requestId) {
|
|
2245
2545
|
const request = {
|
|
2246
2546
|
id: requestId,
|
|
2247
2547
|
sessionId,
|
|
2248
2548
|
toolUseId,
|
|
2249
|
-
question,
|
|
2250
|
-
options,
|
|
2549
|
+
question: questions[0]?.question ?? "",
|
|
2550
|
+
options: questions[0]?.options?.map((o) => o.label),
|
|
2251
2551
|
questions,
|
|
2252
2552
|
createdAt: Date.now()
|
|
2253
2553
|
};
|
|
2254
2554
|
this.updateSessionStatus(sessionId, "waiting_question");
|
|
2255
2555
|
this.emit({ type: "question_request", request });
|
|
2256
|
-
const answerPromise = new Promise((resolve) => {
|
|
2257
|
-
this.pendingQuestions.set(requestId, { sessionId, toolUseId, question, options, questions, createdAt: request.createdAt, resolve });
|
|
2258
|
-
});
|
|
2259
|
-
answerPromise.then(async (answer) => {
|
|
2260
|
-
try {
|
|
2261
|
-
const provider = this.getProviderForSession(sessionId);
|
|
2262
|
-
await provider.answerQuestion(sessionId, toolUseId, answer);
|
|
2263
|
-
} catch (err) {
|
|
2264
|
-
console.error(`[SessionManager] answerQuestion failed (${sessionId}):`, err);
|
|
2265
|
-
}
|
|
2266
|
-
}).catch((err) => console.error("[SessionManager] answerPromise rejected:", err));
|
|
2267
2556
|
console.log(`[SessionManager] Session ${sessionId}: AskUserQuestion pushed (requestId=${requestId})`);
|
|
2557
|
+
return new Promise((resolve) => {
|
|
2558
|
+
this.pendingQuestions.set(requestId, {
|
|
2559
|
+
sessionId,
|
|
2560
|
+
toolUseId,
|
|
2561
|
+
question: request.question,
|
|
2562
|
+
options: request.options,
|
|
2563
|
+
questions,
|
|
2564
|
+
createdAt: request.createdAt,
|
|
2565
|
+
resolve
|
|
2566
|
+
});
|
|
2567
|
+
});
|
|
2268
2568
|
}
|
|
2269
2569
|
/**
|
|
2270
2570
|
* 清除指定会话的所有待回答问题
|
|
@@ -2277,6 +2577,7 @@ var SessionManager = class {
|
|
|
2277
2577
|
}
|
|
2278
2578
|
}
|
|
2279
2579
|
for (const requestId of toRemove) {
|
|
2580
|
+
this.pendingQuestions.get(requestId)?.resolve("");
|
|
2280
2581
|
this.pendingQuestions.delete(requestId);
|
|
2281
2582
|
}
|
|
2282
2583
|
}
|
|
@@ -2296,7 +2597,7 @@ var SessionManager = class {
|
|
|
2296
2597
|
|
|
2297
2598
|
// src/session/SessionFileWatcher.ts
|
|
2298
2599
|
var import_chokidar = __toESM(require("chokidar"));
|
|
2299
|
-
var
|
|
2600
|
+
var import_promises2 = require("fs/promises");
|
|
2300
2601
|
var import_node_readline = require("readline");
|
|
2301
2602
|
var SessionFileWatcher = class {
|
|
2302
2603
|
watchers = /* @__PURE__ */ new Map();
|
|
@@ -2375,7 +2676,7 @@ var SessionFileWatcher = class {
|
|
|
2375
2676
|
let fileHandle;
|
|
2376
2677
|
let rl;
|
|
2377
2678
|
try {
|
|
2378
|
-
fileHandle = await (0,
|
|
2679
|
+
fileHandle = await (0, import_promises2.open)(entry.filePath, "r");
|
|
2379
2680
|
const fileStat = await fileHandle.stat();
|
|
2380
2681
|
const newSize = fileStat.size;
|
|
2381
2682
|
if (newSize <= entry.byteOffset) return;
|
|
@@ -2685,7 +2986,7 @@ var import_node_http = __toESM(require("http"));
|
|
|
2685
2986
|
var import_node_fs3 = __toESM(require("fs"));
|
|
2686
2987
|
var import_node_path3 = __toESM(require("path"));
|
|
2687
2988
|
var import_node_os4 = __toESM(require("os"));
|
|
2688
|
-
var
|
|
2989
|
+
var import_uuid3 = require("uuid");
|
|
2689
2990
|
var ApprovalProxy = class _ApprovalProxy {
|
|
2690
2991
|
server;
|
|
2691
2992
|
token;
|
|
@@ -2693,10 +2994,16 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2693
2994
|
settingsPath = import_node_path3.default.join(import_node_os4.default.homedir(), ".claude", "settings.json");
|
|
2694
2995
|
/** 待处理的审批请求:requestId -> { resolve, timer, request } */
|
|
2695
2996
|
pendingApprovals = /* @__PURE__ */ new Map();
|
|
2997
|
+
/** 待回答的 AskUserQuestion:requestId -> { resolve, timer, request } */
|
|
2998
|
+
pendingQuestions = /* @__PURE__ */ new Map();
|
|
2999
|
+
/** 由外部注入:把问题推给手机并等待答案(返回用户答案文本) */
|
|
3000
|
+
questionHandler = null;
|
|
2696
3001
|
/** 审批请求回调(通知外部推送到手机) */
|
|
2697
3002
|
approvalRequestCallbacks = [];
|
|
2698
3003
|
/** 审批 resolve 回调(任何来源的 resolve 都会触发,用于 WS 广播清理 */
|
|
2699
3004
|
approvalResolvedCallbacks = [];
|
|
3005
|
+
/** 问题 resolve 回调(任何来源的 resolve 都会触发,用于 SessionManager 清理) */
|
|
3006
|
+
questionResolvedCallbacks = [];
|
|
2700
3007
|
/** Hook 通知回调(PreCompact / PermissionDenied / 等非阻塞 hook) */
|
|
2701
3008
|
notifyCallbacks = [];
|
|
2702
3009
|
/** YOLO 模式状态:sessionId -> enabled */
|
|
@@ -2760,6 +3067,30 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2760
3067
|
}
|
|
2761
3068
|
}
|
|
2762
3069
|
}
|
|
3070
|
+
/**
|
|
3071
|
+
* 注册问题 resolve 回调
|
|
3072
|
+
*
|
|
3073
|
+
* 任何来源的 resolve 都会触发:
|
|
3074
|
+
* - resolveQuestion(手机端答案到达)
|
|
3075
|
+
* - 325s 超时自动空答案
|
|
3076
|
+
* - clearPendingQuestionsForSession(会话被 kill)
|
|
3077
|
+
* - close()(服务关闭)
|
|
3078
|
+
*
|
|
3079
|
+
* 用于通知 SessionManager 清理 pendingQuestions,避免会话卡在 waiting_question。
|
|
3080
|
+
*/
|
|
3081
|
+
onQuestionResolved(callback) {
|
|
3082
|
+
this.questionResolvedCallbacks.push(callback);
|
|
3083
|
+
}
|
|
3084
|
+
/** 通知所有问题 resolve 回调(内部调用) */
|
|
3085
|
+
notifyQuestionResolved(requestId) {
|
|
3086
|
+
for (const cb of this.questionResolvedCallbacks) {
|
|
3087
|
+
try {
|
|
3088
|
+
cb(requestId);
|
|
3089
|
+
} catch (err) {
|
|
3090
|
+
console.error("[ApprovalProxy] question resolved callback error:", err);
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
2763
3094
|
/**
|
|
2764
3095
|
* 注册非阻塞 hook 通知回调(如 PreCompact、PermissionDenied)
|
|
2765
3096
|
*
|
|
@@ -2814,6 +3145,42 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2814
3145
|
this.notifyApprovalResolved(requestId, decision);
|
|
2815
3146
|
return true;
|
|
2816
3147
|
}
|
|
3148
|
+
/** 注入问题处理器(server.ts 接到 SessionManager.askQuestion) */
|
|
3149
|
+
setQuestionHandler(handler) {
|
|
3150
|
+
this.questionHandler = handler;
|
|
3151
|
+
}
|
|
3152
|
+
/** 解析一个待回答问题(手机端答案到达时由 server.ts 调用) */
|
|
3153
|
+
resolveQuestion(requestId, answer) {
|
|
3154
|
+
const pending = this.pendingQuestions.get(requestId);
|
|
3155
|
+
if (!pending) {
|
|
3156
|
+
console.warn(`[ApprovalProxy] Question request not found: ${requestId}`);
|
|
3157
|
+
return false;
|
|
3158
|
+
}
|
|
3159
|
+
clearTimeout(pending.timer);
|
|
3160
|
+
pending.resolve(answer);
|
|
3161
|
+
this.pendingQuestions.delete(requestId);
|
|
3162
|
+
console.log(`[ApprovalProxy] Question answered: ${requestId}`);
|
|
3163
|
+
this.notifyQuestionResolved(requestId);
|
|
3164
|
+
return true;
|
|
3165
|
+
}
|
|
3166
|
+
/** 清理会话的待回答问题(会话被 kill 时,给空答案让 hook 不再阻塞) */
|
|
3167
|
+
clearPendingQuestionsForSession(sessionId) {
|
|
3168
|
+
const toRemove = [];
|
|
3169
|
+
for (const [requestId, pending] of this.pendingQuestions) {
|
|
3170
|
+
if (pending.request.sessionId === sessionId) {
|
|
3171
|
+
toRemove.push(requestId);
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
for (const requestId of toRemove) {
|
|
3175
|
+
const pending = this.pendingQuestions.get(requestId);
|
|
3176
|
+
if (!pending) continue;
|
|
3177
|
+
clearTimeout(pending.timer);
|
|
3178
|
+
pending.resolve("");
|
|
3179
|
+
this.pendingQuestions.delete(requestId);
|
|
3180
|
+
console.log(`[ApprovalProxy] Session ${sessionId} killed, cleared pending question ${requestId}`);
|
|
3181
|
+
this.notifyQuestionResolved(requestId);
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
2817
3184
|
/** 获取当前待处理的审批数量 */
|
|
2818
3185
|
getPendingCount() {
|
|
2819
3186
|
return this.pendingApprovals.size;
|
|
@@ -2935,6 +3302,13 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2935
3302
|
pending.resolve({ decision: "deny", reason: t("approval.serverClosed") });
|
|
2936
3303
|
}
|
|
2937
3304
|
this.pendingApprovals.clear();
|
|
3305
|
+
const pendingQuestionEntries = Array.from(this.pendingQuestions.entries());
|
|
3306
|
+
for (const [requestId, pending] of pendingQuestionEntries) {
|
|
3307
|
+
clearTimeout(pending.timer);
|
|
3308
|
+
pending.resolve("");
|
|
3309
|
+
this.notifyQuestionResolved(requestId);
|
|
3310
|
+
}
|
|
3311
|
+
this.pendingQuestions.clear();
|
|
2938
3312
|
this.server.close((err) => {
|
|
2939
3313
|
if (err) {
|
|
2940
3314
|
reject(err);
|
|
@@ -2990,7 +3364,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2990
3364
|
try {
|
|
2991
3365
|
const body = await this.parseJsonBody(req);
|
|
2992
3366
|
const payload = body.payload ?? body;
|
|
2993
|
-
const requestId = (0,
|
|
3367
|
+
const requestId = (0, import_uuid3.v4)();
|
|
2994
3368
|
const projectPath = String(body.projectPath ?? "unknown");
|
|
2995
3369
|
const toolName = String(payload.tool_name ?? body.tool_name ?? "unknown");
|
|
2996
3370
|
const toolInput = payload.tool_input ?? body.tool_input ?? {};
|
|
@@ -3004,6 +3378,51 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
3004
3378
|
createdAt: Date.now()
|
|
3005
3379
|
};
|
|
3006
3380
|
console.log(`[ApprovalProxy] ${t("approval.received")}: ${requestId} (${approvalRequest.toolName})`);
|
|
3381
|
+
if ((approvalRequest.toolName === "AskUserQuestion" || approvalRequest.toolName === "AskFollowupQuestion") && this.questionHandler) {
|
|
3382
|
+
const questions = parseQuestionsFromInput(toolInput);
|
|
3383
|
+
if (questions.length === 0) {
|
|
3384
|
+
this.sendJson(res, 200, { decision: "allow" });
|
|
3385
|
+
return;
|
|
3386
|
+
}
|
|
3387
|
+
const toolUseId = String(
|
|
3388
|
+
payload.tool_use_id ?? body.tool_use_id ?? ""
|
|
3389
|
+
);
|
|
3390
|
+
const qRequest = {
|
|
3391
|
+
id: requestId,
|
|
3392
|
+
sessionId: approvalRequest.sessionId,
|
|
3393
|
+
toolUseId,
|
|
3394
|
+
question: questions[0].question,
|
|
3395
|
+
options: questions[0].options?.map((o) => o.label),
|
|
3396
|
+
questions,
|
|
3397
|
+
createdAt: Date.now()
|
|
3398
|
+
};
|
|
3399
|
+
const answer = await new Promise((resolve) => {
|
|
3400
|
+
const timer = setTimeout(() => {
|
|
3401
|
+
this.pendingQuestions.delete(requestId);
|
|
3402
|
+
console.log(`[ApprovalProxy] Question timeout: ${requestId}`);
|
|
3403
|
+
resolve("");
|
|
3404
|
+
this.notifyQuestionResolved(requestId);
|
|
3405
|
+
}, 325e3);
|
|
3406
|
+
this.pendingQuestions.set(requestId, { resolve, timer, request: qRequest });
|
|
3407
|
+
this.questionHandler(qRequest.sessionId, toolUseId, questions, requestId).then((ans) => {
|
|
3408
|
+
if (ans && this.pendingQuestions.has(requestId)) this.resolveQuestion(requestId, ans);
|
|
3409
|
+
}).catch((err) => console.error("[ApprovalProxy] questionHandler error:", err));
|
|
3410
|
+
});
|
|
3411
|
+
if (!answer) {
|
|
3412
|
+
this.sendJson(res, 200, {
|
|
3413
|
+
decision: "deny",
|
|
3414
|
+
reason: "\u7528\u6237\u672A\u5728\u9650\u5B9A\u65F6\u95F4\u5185\u56DE\u7B54\u8BE5\u95EE\u9898\u3002\u8BF7\u57FA\u4E8E\u73B0\u6709\u4FE1\u606F\u81EA\u884C\u51B3\u7B56\u540E\u7EE7\u7EED\uFF0C\u4E0D\u8981\u91CD\u590D\u8C03\u7528 AskUserQuestion\u3002",
|
|
3415
|
+
systemMessage: "\u7528\u6237\u672A\u56DE\u7B54 AskUserQuestion\uFF1B\u8BF7\u52FF\u91CD\u8BD5\u8BE5\u5DE5\u5177\uFF0C\u81EA\u884C\u51B3\u7B56\u7EE7\u7EED\u3002"
|
|
3416
|
+
});
|
|
3417
|
+
return;
|
|
3418
|
+
}
|
|
3419
|
+
this.sendJson(res, 200, {
|
|
3420
|
+
decision: "deny",
|
|
3421
|
+
reason: formatQuestionAnswer(questions, answer),
|
|
3422
|
+
systemMessage: "\u7528\u6237\u5DF2\u901A\u8FC7 Sessix \u79FB\u52A8\u7AEF\u56DE\u7B54\u4E86 AskUserQuestion\uFF0C\u56DE\u7B54\u5728\u5DE5\u5177\u53CD\u9988\u4E2D\u7ED9\u51FA\u3002\u8BF7\u5C06\u5176\u89C6\u4E3A\u8BE5\u5DE5\u5177\u7684\u6709\u6548\u8FD4\u56DE\u76F4\u63A5\u7EE7\u7EED\uFF0C\u4E0D\u8981\u518D\u6B21\u8C03\u7528 AskUserQuestion/AskFollowupQuestion\u3002"
|
|
3423
|
+
});
|
|
3424
|
+
return;
|
|
3425
|
+
}
|
|
3007
3426
|
if (this.isToolAlwaysAllowed(approvalRequest.toolName, projectPath !== "unknown" ? projectPath : void 0)) {
|
|
3008
3427
|
console.log(`[ApprovalProxy] ${t("approval.alwaysAllowPassThrough", { tool: approvalRequest.toolName })}`);
|
|
3009
3428
|
this.sendJson(res, 200, { decision: "allow" });
|
|
@@ -3193,6 +3612,49 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
3193
3612
|
res.end(body);
|
|
3194
3613
|
}
|
|
3195
3614
|
};
|
|
3615
|
+
function parseQuestionsFromInput(input) {
|
|
3616
|
+
if (Array.isArray(input.questions) && input.questions.length > 0) {
|
|
3617
|
+
return input.questions.map((q) => {
|
|
3618
|
+
const item = {
|
|
3619
|
+
question: typeof q.question === "string" ? q.question : "",
|
|
3620
|
+
header: typeof q.header === "string" ? q.header : void 0,
|
|
3621
|
+
multiSelect: typeof q.multiSelect === "boolean" ? q.multiSelect : void 0
|
|
3622
|
+
};
|
|
3623
|
+
if (Array.isArray(q.options)) {
|
|
3624
|
+
item.options = q.options.map((o) => ({
|
|
3625
|
+
label: typeof o.label === "string" ? o.label : "",
|
|
3626
|
+
description: typeof o.description === "string" ? o.description : void 0
|
|
3627
|
+
}));
|
|
3628
|
+
}
|
|
3629
|
+
return item;
|
|
3630
|
+
});
|
|
3631
|
+
}
|
|
3632
|
+
if (typeof input.question === "string") {
|
|
3633
|
+
const opts = Array.isArray(input.options) ? input.options.map((o) => ({ label: String(o) })) : void 0;
|
|
3634
|
+
return [{ question: input.question, options: opts }];
|
|
3635
|
+
}
|
|
3636
|
+
return [];
|
|
3637
|
+
}
|
|
3638
|
+
function formatQuestionAnswer(questions, raw) {
|
|
3639
|
+
let pairs = [];
|
|
3640
|
+
try {
|
|
3641
|
+
const parsed = JSON.parse(raw);
|
|
3642
|
+
if (parsed && typeof parsed === "object" && parsed.answers && typeof parsed.answers === "object") {
|
|
3643
|
+
const answers = parsed.answers;
|
|
3644
|
+
pairs = questions.length > 0 ? questions.map((item) => ({ q: item.question, a: String(answers[item.question] ?? "") })) : Object.entries(answers).map(([q, a]) => ({ q, a: String(a) }));
|
|
3645
|
+
}
|
|
3646
|
+
} catch {
|
|
3647
|
+
}
|
|
3648
|
+
if (pairs.length === 0) {
|
|
3649
|
+
pairs = [{ q: questions[0]?.question ?? "\uFF08\u672A\u77E5\u95EE\u9898\uFF09", a: raw }];
|
|
3650
|
+
}
|
|
3651
|
+
const body = pairs.map((p, i) => `${i + 1}. \u95EE\u9898\uFF1A${p.q}
|
|
3652
|
+
\u7528\u6237\u56DE\u7B54\uFF1A${p.a}`).join("\n");
|
|
3653
|
+
return `\u3010\u7528\u6237\u5DF2\u901A\u8FC7 Sessix \u56DE\u7B54\u3011
|
|
3654
|
+
${body}
|
|
3655
|
+
|
|
3656
|
+
\u8BF7\u5C06\u4EE5\u4E0A\u56DE\u7B54\u89C6\u4E3A AskUserQuestion \u7684\u6709\u6548\u8FD4\u56DE\uFF0C\u76F4\u63A5\u636E\u6B64\u7EE7\u7EED\u5B8C\u6210\u4EFB\u52A1\uFF0C\u4E0D\u8981\u518D\u6B21\u8C03\u7528 AskUserQuestion/AskFollowupQuestion\u3002`;
|
|
3657
|
+
}
|
|
3196
3658
|
|
|
3197
3659
|
// src/mdns/MdnsService.ts
|
|
3198
3660
|
var import_node_child_process5 = require("child_process");
|
|
@@ -3358,7 +3820,7 @@ function getLanAddresses(networkInterfacesFn) {
|
|
|
3358
3820
|
}
|
|
3359
3821
|
|
|
3360
3822
|
// src/hooks/HookInstaller.ts
|
|
3361
|
-
var
|
|
3823
|
+
var import_promises3 = require("fs/promises");
|
|
3362
3824
|
var import_node_path4 = require("path");
|
|
3363
3825
|
var import_node_os6 = require("os");
|
|
3364
3826
|
var SESSIX_HOOKS_DIR = (0, import_node_path4.join)((0, import_node_os6.homedir)(), ".sessix", "hooks");
|
|
@@ -3378,7 +3840,7 @@ var LEGACY_HOOK_COMMANDS = [
|
|
|
3378
3840
|
"~/.sessix/hooks/permission-accept.sh"
|
|
3379
3841
|
];
|
|
3380
3842
|
var HOOK_SCRIPT_TEMPLATE = `#!/usr/bin/env node
|
|
3381
|
-
// Sessix Approval Hook
|
|
3843
|
+
// Sessix Approval Hook v2 (systemMessage passthrough)
|
|
3382
3844
|
// \u4EC5\u5728 Sessix \u7BA1\u7406\u7684\u4F1A\u8BDD\u4E2D\u6FC0\u6D3B
|
|
3383
3845
|
|
|
3384
3846
|
const sessionId = process.env.SESSIX_SESSION_ID
|
|
@@ -3409,6 +3871,9 @@ process.stdin.on('end', async () => {
|
|
|
3409
3871
|
if (decision === 'deny' && data.reason) {
|
|
3410
3872
|
output.hookSpecificOutput.permissionDecisionReason = String(data.reason)
|
|
3411
3873
|
}
|
|
3874
|
+
if (data.systemMessage) {
|
|
3875
|
+
output.systemMessage = String(data.systemMessage)
|
|
3876
|
+
}
|
|
3412
3877
|
process.stdout.write(JSON.stringify(output))
|
|
3413
3878
|
process.exit(0)
|
|
3414
3879
|
} catch {
|
|
@@ -3548,17 +4013,17 @@ var HookInstaller = class {
|
|
|
3548
4013
|
* 4. 更新 Claude Code settings.json 添加 hook 配置
|
|
3549
4014
|
*/
|
|
3550
4015
|
async install() {
|
|
3551
|
-
await (0,
|
|
3552
|
-
await (0,
|
|
3553
|
-
await (0,
|
|
3554
|
-
await (0,
|
|
3555
|
-
await (0,
|
|
3556
|
-
await (0,
|
|
3557
|
-
await (0,
|
|
3558
|
-
await (0,
|
|
3559
|
-
await (0,
|
|
3560
|
-
await (0,
|
|
3561
|
-
await (0,
|
|
4016
|
+
await (0, import_promises3.mkdir)(SESSIX_HOOKS_DIR, { recursive: true });
|
|
4017
|
+
await (0, import_promises3.writeFile)(HOOK_SCRIPT_PATH, HOOK_SCRIPT_TEMPLATE, "utf-8");
|
|
4018
|
+
await (0, import_promises3.writeFile)(PERMISSION_ACCEPT_PATH, PERMISSION_ACCEPT_TEMPLATE, "utf-8");
|
|
4019
|
+
await (0, import_promises3.writeFile)(COMPACT_HOOK_PATH, COMPACT_HOOK_TEMPLATE, "utf-8");
|
|
4020
|
+
await (0, import_promises3.writeFile)(POST_COMPACT_HOOK_PATH, POST_COMPACT_HOOK_TEMPLATE, "utf-8");
|
|
4021
|
+
await (0, import_promises3.writeFile)(PERMISSION_DENIED_HOOK_PATH, PERMISSION_DENIED_HOOK_TEMPLATE, "utf-8");
|
|
4022
|
+
await (0, import_promises3.chmod)(HOOK_SCRIPT_PATH, 493);
|
|
4023
|
+
await (0, import_promises3.chmod)(PERMISSION_ACCEPT_PATH, 493);
|
|
4024
|
+
await (0, import_promises3.chmod)(COMPACT_HOOK_PATH, 493);
|
|
4025
|
+
await (0, import_promises3.chmod)(POST_COMPACT_HOOK_PATH, 493);
|
|
4026
|
+
await (0, import_promises3.chmod)(PERMISSION_DENIED_HOOK_PATH, 493);
|
|
3562
4027
|
await this.addHookToSettings();
|
|
3563
4028
|
console.log("[HookInstaller] Hook installation complete");
|
|
3564
4029
|
}
|
|
@@ -3588,30 +4053,30 @@ var HookInstaller = class {
|
|
|
3588
4053
|
let postCompactScriptExists = false;
|
|
3589
4054
|
let permissionDeniedScriptExists = false;
|
|
3590
4055
|
try {
|
|
3591
|
-
approvalScriptContent = await (0,
|
|
4056
|
+
approvalScriptContent = await (0, import_promises3.readFile)(HOOK_SCRIPT_PATH, "utf-8");
|
|
3592
4057
|
} catch {
|
|
3593
4058
|
}
|
|
3594
4059
|
try {
|
|
3595
|
-
await (0,
|
|
4060
|
+
await (0, import_promises3.access)(PERMISSION_ACCEPT_PATH);
|
|
3596
4061
|
permissionScriptExists = true;
|
|
3597
4062
|
} catch {
|
|
3598
4063
|
}
|
|
3599
4064
|
try {
|
|
3600
|
-
await (0,
|
|
4065
|
+
await (0, import_promises3.access)(COMPACT_HOOK_PATH);
|
|
3601
4066
|
compactScriptExists = true;
|
|
3602
4067
|
} catch {
|
|
3603
4068
|
}
|
|
3604
4069
|
try {
|
|
3605
|
-
await (0,
|
|
4070
|
+
await (0, import_promises3.access)(POST_COMPACT_HOOK_PATH);
|
|
3606
4071
|
postCompactScriptExists = true;
|
|
3607
4072
|
} catch {
|
|
3608
4073
|
}
|
|
3609
4074
|
try {
|
|
3610
|
-
await (0,
|
|
4075
|
+
await (0, import_promises3.access)(PERMISSION_DENIED_HOOK_PATH);
|
|
3611
4076
|
permissionDeniedScriptExists = true;
|
|
3612
4077
|
} catch {
|
|
3613
4078
|
}
|
|
3614
|
-
const isLatestVersion = approvalScriptContent.includes("permissionDecision");
|
|
4079
|
+
const isLatestVersion = approvalScriptContent.includes("permissionDecision") && approvalScriptContent.includes("Sessix Approval Hook v2");
|
|
3615
4080
|
const settings = await this.readClaudeSettings();
|
|
3616
4081
|
const configExists = this.hasHookConfig(settings);
|
|
3617
4082
|
return isLatestVersion && permissionScriptExists && compactScriptExists && postCompactScriptExists && permissionDeniedScriptExists && configExists;
|
|
@@ -3719,7 +4184,7 @@ var HookInstaller = class {
|
|
|
3719
4184
|
*/
|
|
3720
4185
|
async readClaudeSettings() {
|
|
3721
4186
|
try {
|
|
3722
|
-
const content = await (0,
|
|
4187
|
+
const content = await (0, import_promises3.readFile)(CLAUDE_SETTINGS_PATH, "utf-8");
|
|
3723
4188
|
return JSON.parse(content);
|
|
3724
4189
|
} catch {
|
|
3725
4190
|
return {};
|
|
@@ -3729,8 +4194,8 @@ var HookInstaller = class {
|
|
|
3729
4194
|
* 写入 Claude Code settings.json
|
|
3730
4195
|
*/
|
|
3731
4196
|
async writeClaudeSettings(settings) {
|
|
3732
|
-
await (0,
|
|
3733
|
-
await (0,
|
|
4197
|
+
await (0, import_promises3.mkdir)((0, import_node_path4.join)((0, import_node_os6.homedir)(), ".claude"), { recursive: true });
|
|
4198
|
+
await (0, import_promises3.writeFile)(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
3734
4199
|
}
|
|
3735
4200
|
/**
|
|
3736
4201
|
* 检查 settings 中是否已包含所有 Sessix hook 配置
|
|
@@ -3771,7 +4236,7 @@ var HookInstaller = class {
|
|
|
3771
4236
|
var import_node_path5 = require("path");
|
|
3772
4237
|
var RECENT_ACTIVITY_MAX = 6;
|
|
3773
4238
|
var ACTIVITY_PUSH_THROTTLE_MS = 4e3;
|
|
3774
|
-
var NotificationService = class {
|
|
4239
|
+
var NotificationService = class _NotificationService {
|
|
3775
4240
|
constructor(sessionManager, expoChannel = null) {
|
|
3776
4241
|
this.sessionManager = sessionManager;
|
|
3777
4242
|
this.expoChannel = expoChannel;
|
|
@@ -3813,6 +4278,14 @@ var NotificationService = class {
|
|
|
3813
4278
|
* token 注册时启动,flushActivityEnd / removeActivityPushToken 时停止。
|
|
3814
4279
|
*/
|
|
3815
4280
|
laHeartbeatTimers = /* @__PURE__ */ new Map();
|
|
4281
|
+
/**
|
|
4282
|
+
* 上次推送的内容指纹(status + recentActivity + approvalId)。
|
|
4283
|
+
* 只在内容实际变化时发 priority-10 推送;未变化时低频刷新(30s),
|
|
4284
|
+
* 节省 APNs push budget,避免 iOS 节流导致 LA 停滞。
|
|
4285
|
+
*/
|
|
4286
|
+
lastPushedFingerprint = /* @__PURE__ */ new Map();
|
|
4287
|
+
/** 内容未变化时低频刷新间隔(仅刷新 stats/timer,不含内容变化) */
|
|
4288
|
+
static STATS_REFRESH_INTERVAL_MS = 3e4;
|
|
3816
4289
|
/** 添加通知渠道(id 唯一,可用于后续动态开关) */
|
|
3817
4290
|
addChannel(id, channel, enabled = true) {
|
|
3818
4291
|
this.channelMap.set(id, { channel, enabled });
|
|
@@ -3830,9 +4303,21 @@ var NotificationService = class {
|
|
|
3830
4303
|
removePushToken(token) {
|
|
3831
4304
|
this.expoChannel?.removeToken(token);
|
|
3832
4305
|
}
|
|
4306
|
+
/** 注册原生 APNs device token(直连 APNs,优先于 Expo 推送服务) */
|
|
4307
|
+
addNativePushToken(token, ws) {
|
|
4308
|
+
this.activityPushChannel?.addAlertToken(token, ws);
|
|
4309
|
+
if (this.activityPushChannel?.hasAlertTokens()) {
|
|
4310
|
+
console.log("[NotificationService] \u2705 \u76F4\u8FDE APNs alert token \u5DF2\u6CE8\u518C\uFF0CExpo \u63A8\u9001\u6E20\u9053\u964D\u7EA7\u5907\u7528");
|
|
4311
|
+
}
|
|
4312
|
+
}
|
|
4313
|
+
/** 移除原生 APNs device token */
|
|
4314
|
+
removeNativePushToken(token) {
|
|
4315
|
+
this.activityPushChannel?.removeAlertToken(token);
|
|
4316
|
+
}
|
|
3833
4317
|
/** 更新通知音效偏好 */
|
|
3834
4318
|
setSoundPreferences(prefs) {
|
|
3835
4319
|
this.expoChannel?.setSoundPreferences(prefs);
|
|
4320
|
+
this.activityPushChannel?.setAlertSoundPreferences(prefs);
|
|
3836
4321
|
}
|
|
3837
4322
|
/** 设置 ActivityKit Push 渠道(可选,需要 APNs 认证配置) */
|
|
3838
4323
|
setActivityPushChannel(channel) {
|
|
@@ -3857,6 +4342,7 @@ var NotificationService = class {
|
|
|
3857
4342
|
this.recentActivityState.delete(sessionId);
|
|
3858
4343
|
this.lastActivityPushAt.delete(sessionId);
|
|
3859
4344
|
this.activityCounters.delete(sessionId);
|
|
4345
|
+
this.lastPushedFingerprint.delete(sessionId);
|
|
3860
4346
|
}
|
|
3861
4347
|
/** 注入"会话 → 待审批列表"提供器(server.ts 启动时调用) */
|
|
3862
4348
|
setPendingApprovalsProvider(fn) {
|
|
@@ -4007,6 +4493,7 @@ var NotificationService = class {
|
|
|
4007
4493
|
this.lastActivityPushAt.clear();
|
|
4008
4494
|
this.pendingPriority.clear();
|
|
4009
4495
|
this.activityCounters.clear();
|
|
4496
|
+
this.lastPushedFingerprint.clear();
|
|
4010
4497
|
}
|
|
4011
4498
|
// ============================================
|
|
4012
4499
|
// 内部方法
|
|
@@ -4058,12 +4545,19 @@ var NotificationService = class {
|
|
|
4058
4545
|
}
|
|
4059
4546
|
}
|
|
4060
4547
|
notify(payload) {
|
|
4061
|
-
|
|
4548
|
+
const hasDirectApns = this.activityPushChannel?.hasAlertTokens() === true;
|
|
4549
|
+
for (const [id, { channel, enabled }] of this.channelMap.entries()) {
|
|
4062
4550
|
if (!enabled) continue;
|
|
4551
|
+
if (id === "expo" && hasDirectApns) continue;
|
|
4063
4552
|
channel.send(payload).catch((err) => {
|
|
4064
4553
|
console.error("[NotificationService] Notification send failed:", err);
|
|
4065
4554
|
});
|
|
4066
4555
|
}
|
|
4556
|
+
if (hasDirectApns && this.activityPushChannel) {
|
|
4557
|
+
this.activityPushChannel.send(payload).catch((err) => {
|
|
4558
|
+
console.error("[NotificationService] Direct APNs push failed, no fallback:", err);
|
|
4559
|
+
});
|
|
4560
|
+
}
|
|
4067
4561
|
}
|
|
4068
4562
|
/** 从 assistant 事件中提取最新文本消息 */
|
|
4069
4563
|
trackAssistantText(sessionId, event) {
|
|
@@ -4100,6 +4594,8 @@ var NotificationService = class {
|
|
|
4100
4594
|
while (state2.history.length > RECENT_ACTIVITY_MAX) state2.history.shift();
|
|
4101
4595
|
state2.currentEntries = [];
|
|
4102
4596
|
state2.currentMessageId = null;
|
|
4597
|
+
state2.accumulatedText = "";
|
|
4598
|
+
state2.countedToolIds = /* @__PURE__ */ new Set();
|
|
4103
4599
|
}
|
|
4104
4600
|
return;
|
|
4105
4601
|
}
|
|
@@ -4108,7 +4604,7 @@ var NotificationService = class {
|
|
|
4108
4604
|
if (!Array.isArray(msg.content)) return;
|
|
4109
4605
|
let state = this.recentActivityState.get(sessionId);
|
|
4110
4606
|
if (!state) {
|
|
4111
|
-
state = { history: [], currentMessageId: null, currentEntries: [] };
|
|
4607
|
+
state = { history: [], currentMessageId: null, currentEntries: [], accumulatedText: "", countedToolIds: /* @__PURE__ */ new Set() };
|
|
4112
4608
|
this.recentActivityState.set(sessionId, state);
|
|
4113
4609
|
}
|
|
4114
4610
|
if (state.currentMessageId !== msg.id) {
|
|
@@ -4118,16 +4614,25 @@ var NotificationService = class {
|
|
|
4118
4614
|
}
|
|
4119
4615
|
state.currentEntries = [];
|
|
4120
4616
|
state.currentMessageId = msg.id;
|
|
4617
|
+
state.accumulatedText = "";
|
|
4618
|
+
state.countedToolIds = /* @__PURE__ */ new Set();
|
|
4619
|
+
}
|
|
4620
|
+
for (const block of msg.content) {
|
|
4621
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
4622
|
+
state.accumulatedText += block.text;
|
|
4623
|
+
}
|
|
4121
4624
|
}
|
|
4122
4625
|
const next = [];
|
|
4626
|
+
const accText = this.summarizeText(state.accumulatedText);
|
|
4627
|
+
if (accText.length >= 4) next.push(accText);
|
|
4123
4628
|
for (const block of msg.content) {
|
|
4124
|
-
if (block.type === "
|
|
4125
|
-
const line = this.summarizeText(block.text);
|
|
4126
|
-
if (line.length >= 4) next.push(line);
|
|
4127
|
-
} else if (block.type === "tool_use") {
|
|
4629
|
+
if (block.type === "tool_use") {
|
|
4128
4630
|
const line = this.summarizeToolCall(block.name, block.input ?? {});
|
|
4129
4631
|
if (line) next.push(line);
|
|
4130
|
-
|
|
4632
|
+
if (!state.countedToolIds.has(block.id)) {
|
|
4633
|
+
state.countedToolIds.add(block.id);
|
|
4634
|
+
this.incrementCounter(sessionId, block.name);
|
|
4635
|
+
}
|
|
4131
4636
|
}
|
|
4132
4637
|
}
|
|
4133
4638
|
state.currentEntries = next;
|
|
@@ -4243,7 +4748,7 @@ var NotificationService = class {
|
|
|
4243
4748
|
return;
|
|
4244
4749
|
}
|
|
4245
4750
|
if (session.status === "running" || session.status === "waiting_approval" || session.status === "waiting_question") {
|
|
4246
|
-
this.scheduleActivityPush(sessionId);
|
|
4751
|
+
this.scheduleActivityPush(sessionId, true);
|
|
4247
4752
|
}
|
|
4248
4753
|
}, ACTIVITY_PUSH_THROTTLE_MS);
|
|
4249
4754
|
this.laHeartbeatTimers.set(sessionId, timer);
|
|
@@ -4280,6 +4785,7 @@ var NotificationService = class {
|
|
|
4280
4785
|
* 无 token → 普通 Expo push。清理所有相关状态。
|
|
4281
4786
|
*/
|
|
4282
4787
|
flushActivityEnd(sessionId, reason) {
|
|
4788
|
+
console.log(`[NotificationService] \u{1F514} flushActivityEnd(${reason}) session=${sessionId.slice(0, 8)}\u2026 expoAvailable=${this.expoChannel?.isAvailable() ?? false}`);
|
|
4283
4789
|
const sessionTitle = this.getSessionTitle(sessionId);
|
|
4284
4790
|
const latestMsg = this.latestAssistantText.get(sessionId);
|
|
4285
4791
|
const isError = reason === "error";
|
|
@@ -4317,14 +4823,36 @@ var NotificationService = class {
|
|
|
4317
4823
|
this.recentActivityState.delete(sessionId);
|
|
4318
4824
|
this.lastActivityPushAt.delete(sessionId);
|
|
4319
4825
|
this.activityCounters.delete(sessionId);
|
|
4826
|
+
this.lastPushedFingerprint.delete(sessionId);
|
|
4320
4827
|
console.log(`[NotificationService] \u{1F3C1} LA end (${reason}) session=${sessionId.slice(0, 8)}\u2026`);
|
|
4321
4828
|
}
|
|
4829
|
+
/**
|
|
4830
|
+
* 计算内容指纹:status + recentActivity + latestApproval。
|
|
4831
|
+
* 用于判断 LA 内容是否实际变化,避免重复推送消耗 APNs budget。
|
|
4832
|
+
*/
|
|
4833
|
+
computeContentFingerprint(sessionId) {
|
|
4834
|
+
const activity = this.getRecentActivity(sessionId);
|
|
4835
|
+
const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
|
|
4836
|
+
const approvals = this.pendingApprovalsProvider?.(sessionId) ?? [];
|
|
4837
|
+
const latestApproval = approvals[approvals.length - 1];
|
|
4838
|
+
return `${session?.status ?? ""}|${activity.join(" ")}|${latestApproval?.id ?? ""}`;
|
|
4839
|
+
}
|
|
4322
4840
|
/** 真正发送一次 LA content push(无 alert) */
|
|
4323
4841
|
flushActivityPush(sessionId) {
|
|
4324
4842
|
const channel = this.activityPushChannel;
|
|
4325
4843
|
if (!channel?.hasToken(sessionId)) return;
|
|
4326
4844
|
const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
|
|
4327
4845
|
if (!session) return;
|
|
4846
|
+
const fingerprint = this.computeContentFingerprint(sessionId);
|
|
4847
|
+
const lastFingerprint = this.lastPushedFingerprint.get(sessionId);
|
|
4848
|
+
const contentChanged = fingerprint !== lastFingerprint;
|
|
4849
|
+
if (!contentChanged) {
|
|
4850
|
+
const lastPush = this.lastActivityPushAt.get(sessionId) ?? 0;
|
|
4851
|
+
if (Date.now() - lastPush < _NotificationService.STATS_REFRESH_INTERVAL_MS) {
|
|
4852
|
+
return;
|
|
4853
|
+
}
|
|
4854
|
+
}
|
|
4855
|
+
this.lastPushedFingerprint.set(sessionId, fingerprint);
|
|
4328
4856
|
const recentActivity = this.getRecentActivity(sessionId);
|
|
4329
4857
|
const latestMessage = recentActivity[recentActivity.length - 1] ?? this.latestAssistantText.get(sessionId) ?? "";
|
|
4330
4858
|
const sessionTitle = this.getSessionTitle(sessionId);
|
|
@@ -4353,13 +4881,14 @@ var NotificationService = class {
|
|
|
4353
4881
|
};
|
|
4354
4882
|
}
|
|
4355
4883
|
contentState.stats = this.buildStatsPayload(session);
|
|
4356
|
-
const
|
|
4884
|
+
const explicitPriority = this.pendingPriority.get(sessionId);
|
|
4885
|
+
const priority = explicitPriority ?? (contentChanged ? "10" : "5");
|
|
4357
4886
|
this.pendingPriority.delete(sessionId);
|
|
4358
4887
|
this.lastActivityPushAt.set(sessionId, Date.now());
|
|
4359
4888
|
const lineCount = recentActivity.length;
|
|
4360
4889
|
channel.updateActivity(sessionId, contentState, { priority }).then((ok) => {
|
|
4361
4890
|
if (ok) {
|
|
4362
|
-
console.log(`[NotificationService] \u{1F4E1} LA push \u2713 session=${sessionId.slice(0, 8)}\u2026 status=${status} p=${priority} lines=${lineCount}`);
|
|
4891
|
+
console.log(`[NotificationService] \u{1F4E1} LA push \u2713 session=${sessionId.slice(0, 8)}\u2026 status=${status} p=${priority} changed=${contentChanged} lines=${lineCount}`);
|
|
4363
4892
|
}
|
|
4364
4893
|
}).catch((err) => {
|
|
4365
4894
|
console.warn(`[NotificationService] \u{1F4E1} LA push \u2717 session=${sessionId.slice(0, 8)}\u2026:`, err instanceof Error ? err.message : err);
|
|
@@ -4488,6 +5017,8 @@ var DesktopNotificationChannel = class {
|
|
|
4488
5017
|
|
|
4489
5018
|
// src/notification/ExpoNotificationChannel.ts
|
|
4490
5019
|
var EXPO_PUSH_API = "https://exp.host/--/api/v2/push/send";
|
|
5020
|
+
var EXPO_RECEIPT_API = "https://exp.host/--/api/v2/push/getReceipts";
|
|
5021
|
+
var RECEIPT_CHECK_DELAY_MS = 1e4;
|
|
4491
5022
|
var ExpoNotificationChannel = class {
|
|
4492
5023
|
tokens = /* @__PURE__ */ new Set();
|
|
4493
5024
|
/** push token → WebSocket 连接映射,用于前台抑制 */
|
|
@@ -4516,7 +5047,13 @@ var ExpoNotificationChannel = class {
|
|
|
4516
5047
|
console.log(`[ExpoNotificationChannel] ${t("notification.soundPrefsUpdated")}`);
|
|
4517
5048
|
}
|
|
4518
5049
|
async send(payload) {
|
|
4519
|
-
if (this.tokens.size === 0)
|
|
5050
|
+
if (this.tokens.size === 0) {
|
|
5051
|
+
const isCompletion = payload.data?.type === "task_complete" || payload.data?.type === "task_error";
|
|
5052
|
+
if (isCompletion) {
|
|
5053
|
+
console.warn("[ExpoNotificationChannel] \u26A0\uFE0F \u65E0 Expo push token\uFF0C\u5B8C\u6210\u901A\u77E5\u65E0\u6CD5\u63A8\u9001\u3002\u8BF7\u786E\u8BA4\u624B\u673A\u7AEF register_push_token \u5DF2\u53D1\u9001\uFF08\u91CD\u542F App \u53EF\u5F3A\u5236\u91CD\u65B0\u6CE8\u518C\uFF09");
|
|
5054
|
+
}
|
|
5055
|
+
return;
|
|
5056
|
+
}
|
|
4520
5057
|
const isCompletionNotif = payload.data?.type === "task_complete" || payload.data?.type === "task_error";
|
|
4521
5058
|
const targetTokens = isCompletionNotif ? Array.from(this.tokens) : Array.from(this.tokens).filter((token) => {
|
|
4522
5059
|
const ws = this.tokenWsMap.get(token);
|
|
@@ -4529,6 +5066,7 @@ var ExpoNotificationChannel = class {
|
|
|
4529
5066
|
if (prefs) {
|
|
4530
5067
|
const notifType = payload.data?.type ?? "";
|
|
4531
5068
|
if (notifType === "approval_request" && prefs.approval) sound = prefs.approval;
|
|
5069
|
+
else if (notifType === "question_request" && prefs.approval) sound = prefs.approval;
|
|
4532
5070
|
else if (notifType === "task_complete" && prefs.taskComplete) sound = prefs.taskComplete;
|
|
4533
5071
|
else if (notifType === "task_error" && prefs.taskError) sound = prefs.taskError;
|
|
4534
5072
|
}
|
|
@@ -4559,16 +5097,76 @@ var ExpoNotificationChannel = class {
|
|
|
4559
5097
|
console.warn(`[ExpoNotificationChannel] ${t("notification.pushApiFormatError")}`, JSON.stringify(body));
|
|
4560
5098
|
return;
|
|
4561
5099
|
}
|
|
4562
|
-
|
|
5100
|
+
const receiptIdToToken = /* @__PURE__ */ new Map();
|
|
5101
|
+
for (let i = 0; i < body.data.length; i++) {
|
|
5102
|
+
const ticket = body.data[i];
|
|
4563
5103
|
if (ticket.status === "error") {
|
|
4564
|
-
|
|
5104
|
+
const errorCode = ticket.details?.error ?? "unknown";
|
|
5105
|
+
console.error(`[ExpoNotificationChannel] ${t("notification.pushFailed")} ${ticket.message} (${errorCode})`);
|
|
5106
|
+
if (errorCode === "DeviceNotRegistered" && targetTokens[i]) {
|
|
5107
|
+
const staleToken = targetTokens[i];
|
|
5108
|
+
this.tokens.delete(staleToken);
|
|
5109
|
+
this.tokenWsMap.delete(staleToken);
|
|
5110
|
+
this.soundPreferences.delete(staleToken);
|
|
5111
|
+
console.warn(`[ExpoNotificationChannel] \u26A0\uFE0F \u5DF2\u79FB\u9664\u5931\u6548 token\uFF08DeviceNotRegistered\uFF09\u3002\u82E5\u901A\u77E5\u672A\u6062\u590D\uFF0C\u8BF7\u91CD\u542F App \u91CD\u65B0\u6CE8\u518C push token\u3002`);
|
|
5112
|
+
}
|
|
5113
|
+
} else if (ticket.status === "ok" && typeof ticket.id === "string" && targetTokens[i]) {
|
|
5114
|
+
receiptIdToToken.set(ticket.id, targetTokens[i]);
|
|
4565
5115
|
}
|
|
4566
5116
|
}
|
|
5117
|
+
this.scheduleReceiptCheck(receiptIdToToken);
|
|
4567
5118
|
}
|
|
4568
5119
|
} catch (err) {
|
|
4569
5120
|
console.warn(`[ExpoNotificationChannel] ${t("notification.sendFailed")}`, err);
|
|
4570
5121
|
}
|
|
4571
5122
|
}
|
|
5123
|
+
/**
|
|
5124
|
+
* Expo push 二阶段:延迟查 receipt,暴露 ticket 阶段看不到的 APNs 投递失败。
|
|
5125
|
+
*
|
|
5126
|
+
* 关键诊断点:InvalidCredentials / MismatchSenderId 表示 Expo 项目的 APNs
|
|
5127
|
+
* 凭证配置问题(不是用户机器问题)——这正是"只有开发者能收到推送"的根因,
|
|
5128
|
+
* 且 ticket 全为 ok、不查 receipt 永远静默。
|
|
5129
|
+
*/
|
|
5130
|
+
scheduleReceiptCheck(receiptIdToToken) {
|
|
5131
|
+
if (receiptIdToToken.size === 0) return;
|
|
5132
|
+
const timer = setTimeout(async () => {
|
|
5133
|
+
try {
|
|
5134
|
+
const ids = Array.from(receiptIdToToken.keys());
|
|
5135
|
+
const res = await fetch(EXPO_RECEIPT_API, {
|
|
5136
|
+
method: "POST",
|
|
5137
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
5138
|
+
body: JSON.stringify({ ids })
|
|
5139
|
+
});
|
|
5140
|
+
const body = await res.json();
|
|
5141
|
+
if (!res.ok || !body?.data || typeof body.data !== "object") {
|
|
5142
|
+
console.warn("[ExpoNotificationChannel] \u26A0\uFE0F push receipt \u67E5\u8BE2\u5931\u8D25", res.status, JSON.stringify(body));
|
|
5143
|
+
return;
|
|
5144
|
+
}
|
|
5145
|
+
const receipts = body.data;
|
|
5146
|
+
for (const [receiptId, receipt] of Object.entries(receipts)) {
|
|
5147
|
+
if (receipt?.status !== "error") continue;
|
|
5148
|
+
const errorCode = receipt.details?.error ?? "unknown";
|
|
5149
|
+
const token = receiptIdToToken.get(receiptId);
|
|
5150
|
+
console.error(
|
|
5151
|
+
`[ExpoNotificationChannel] \u274C APNs \u6295\u9012\u5931\u8D25 receipt=${receiptId} error=${errorCode}` + (receipt.message ? ` \u2014 ${receipt.message}` : "")
|
|
5152
|
+
);
|
|
5153
|
+
if (errorCode === "DeviceNotRegistered" && token) {
|
|
5154
|
+
this.tokens.delete(token);
|
|
5155
|
+
this.tokenWsMap.delete(token);
|
|
5156
|
+
this.soundPreferences.delete(token);
|
|
5157
|
+
console.warn("[ExpoNotificationChannel] \u26A0\uFE0F \u5DF2\u79FB\u9664\u5931\u6548 token\uFF08receipt DeviceNotRegistered\uFF09\u3002\u91CD\u542F App \u53EF\u91CD\u65B0\u6CE8\u518C\u3002");
|
|
5158
|
+
} else if (errorCode === "InvalidCredentials" || errorCode === "MismatchSenderId") {
|
|
5159
|
+
console.error(
|
|
5160
|
+
"[ExpoNotificationChannel] \u26A0\uFE0F \u8FD9\u662F Expo \u9879\u76EE\u7684 APNs \u51ED\u8BC1\u914D\u7F6E\u95EE\u9898\uFF08\u4E0D\u662F\u7528\u6237\u673A\u5668\u95EE\u9898\uFF09\uFF1A\n Expo \u63A5\u53D7\u4E86\u63A8\u9001\u4F46 APNs \u62D2\u6295\uFF0C\u901A\u5E38\u56E0\u4E3A\u8BE5 Expo \u9879\u76EE\u672A\u914D\u7F6E\u53EF\u6295\u9012\u5230\u3010\u751F\u4EA7\u73AF\u5883\u3011\u7684 APNs Auth Key\u3002\n \u6392\u67E5\uFF1Acd packages/mobile && npx eas credentials -p ios \u2014\u2014 \u786E\u8BA4\u5B58\u5728\u8986\u76D6\u751F\u4EA7\u7684 APNs Push Key(.p8)\u3002"
|
|
5161
|
+
);
|
|
5162
|
+
}
|
|
5163
|
+
}
|
|
5164
|
+
} catch (err) {
|
|
5165
|
+
console.warn("[ExpoNotificationChannel] \u26A0\uFE0F push receipt \u67E5\u8BE2\u5F02\u5E38:", err);
|
|
5166
|
+
}
|
|
5167
|
+
}, RECEIPT_CHECK_DELAY_MS);
|
|
5168
|
+
timer.unref?.();
|
|
5169
|
+
}
|
|
4572
5170
|
};
|
|
4573
5171
|
|
|
4574
5172
|
// src/notification/ActivityPushChannel.ts
|
|
@@ -4582,6 +5180,14 @@ var APNS_HOSTS = {
|
|
|
4582
5180
|
var ActivityPushChannel = class {
|
|
4583
5181
|
/** sessionId -> activityPushToken */
|
|
4584
5182
|
tokens = /* @__PURE__ */ new Map();
|
|
5183
|
+
/** 原生 device token 集合(用于普通 alert push,绕过 Expo 推送服务) */
|
|
5184
|
+
alertTokens = /* @__PURE__ */ new Set();
|
|
5185
|
+
/** alert token -> WebSocket 映射(用于前台在线过滤) */
|
|
5186
|
+
alertTokenWsMap = /* @__PURE__ */ new Map();
|
|
5187
|
+
/** alert token 已确认的 APNs 环境(独立于 LA token 的探测结果) */
|
|
5188
|
+
alertTokenEnv = /* @__PURE__ */ new Map();
|
|
5189
|
+
/** per-alert-token 通知音效偏好 */
|
|
5190
|
+
alertSoundPreferences = /* @__PURE__ */ new Map();
|
|
4585
5191
|
/**
|
|
4586
5192
|
* 每个 token 已确认工作的 APNs 环境。
|
|
4587
5193
|
* Debug build (aps-environment=development) 的 token 仅在 sandbox 端有效;
|
|
@@ -4600,6 +5206,7 @@ var ActivityPushChannel = class {
|
|
|
4600
5206
|
teamId;
|
|
4601
5207
|
keyId;
|
|
4602
5208
|
authKey;
|
|
5209
|
+
bundleId;
|
|
4603
5210
|
/** 缓存的 JWT token + 过期时间 */
|
|
4604
5211
|
cachedJwt = null;
|
|
4605
5212
|
/** 每个环境一条 HTTP/2 长连接 */
|
|
@@ -4608,6 +5215,7 @@ var ActivityPushChannel = class {
|
|
|
4608
5215
|
this.teamId = config.teamId;
|
|
4609
5216
|
this.keyId = config.keyId;
|
|
4610
5217
|
this.authKey = fs2.readFileSync(config.authKeyPath, "utf-8");
|
|
5218
|
+
this.bundleId = config.bundleId ?? "com.kachun.sessix";
|
|
4611
5219
|
this.probeOrder = config.sandbox === false ? ["production", "sandbox"] : ["sandbox", "production"];
|
|
4612
5220
|
console.log(`[ActivityPushChannel] Initialized (probe order: ${this.probeOrder.join(" \u2192 ")})`);
|
|
4613
5221
|
}
|
|
@@ -4652,557 +5260,335 @@ var ActivityPushChannel = class {
|
|
|
4652
5260
|
/** 发送 content-state 更新到指定会话的 Live Activity(纯内容刷新,不响通知)。返回 true 表示实际发出 */
|
|
4653
5261
|
async updateActivity(sessionId, contentState, opts) {
|
|
4654
5262
|
const token = this.tokens.get(sessionId);
|
|
4655
|
-
if (!token) return false;
|
|
4656
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
4657
|
-
const priority = opts?.priority ?? "5";
|
|
4658
|
-
const payload = {
|
|
4659
|
-
aps: {
|
|
4660
|
-
timestamp: now,
|
|
4661
|
-
event: "update",
|
|
4662
|
-
"content-state": contentState,
|
|
4663
|
-
"stale-date": now + 600
|
|
4664
|
-
}
|
|
4665
|
-
};
|
|
4666
|
-
try {
|
|
4667
|
-
await this.sendToAPNs(token, payload, {
|
|
4668
|
-
priority,
|
|
4669
|
-
collapseId: `state-${sessionId.slice(0, 54)}`
|
|
4670
|
-
});
|
|
4671
|
-
return true;
|
|
4672
|
-
} catch (err) {
|
|
4673
|
-
console.warn(`[ActivityPushChannel] Update failed session=${sessionId}:`, err);
|
|
4674
|
-
return false;
|
|
4675
|
-
}
|
|
4676
|
-
}
|
|
4677
|
-
/** 发送带通知的 content-state 更新(审批请求时使用),返回是否发送成功 */
|
|
4678
|
-
async updateActivityWithAlert(sessionId, contentState, alert) {
|
|
4679
|
-
const token = this.tokens.get(sessionId);
|
|
4680
|
-
if (!token) return false;
|
|
4681
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
4682
|
-
const payload = {
|
|
4683
|
-
aps: {
|
|
4684
|
-
timestamp: now,
|
|
4685
|
-
event: "update",
|
|
4686
|
-
"content-state": contentState,
|
|
4687
|
-
"stale-date": now + 600,
|
|
4688
|
-
alert,
|
|
4689
|
-
sound: "default"
|
|
4690
|
-
}
|
|
4691
|
-
};
|
|
4692
|
-
try {
|
|
4693
|
-
await this.sendToAPNs(token, payload, { priority: "10" });
|
|
4694
|
-
return true;
|
|
4695
|
-
} catch (err) {
|
|
4696
|
-
console.warn(`[ActivityPushChannel] Alert update failed session=${sessionId}:`, err);
|
|
4697
|
-
return false;
|
|
4698
|
-
}
|
|
4699
|
-
}
|
|
4700
|
-
/**
|
|
4701
|
-
* 结束指定会话的 Live Activity。
|
|
4702
|
-
* 可选 alert:APNs event=end 时同时推送横幅通知 + 声音,用于在会话完成时提醒用户。
|
|
4703
|
-
*/
|
|
4704
|
-
async endActivity(sessionId, contentState, opts) {
|
|
4705
|
-
const token = this.tokens.get(sessionId);
|
|
4706
|
-
if (!token) return;
|
|
4707
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
4708
|
-
const aps = {
|
|
4709
|
-
timestamp: now,
|
|
4710
|
-
event: "end",
|
|
4711
|
-
"content-state": contentState
|
|
4712
|
-
};
|
|
4713
|
-
if (opts?.alert) {
|
|
4714
|
-
aps.alert = opts.alert;
|
|
4715
|
-
aps.sound = "default";
|
|
4716
|
-
}
|
|
4717
|
-
const payload = { aps };
|
|
4718
|
-
try {
|
|
4719
|
-
await this.sendToAPNs(token, payload, { priority: "10" });
|
|
4720
|
-
} catch (err) {
|
|
4721
|
-
this.tokens.delete(sessionId);
|
|
4722
|
-
throw err;
|
|
4723
|
-
}
|
|
4724
|
-
this.tokens.delete(sessionId);
|
|
4725
|
-
}
|
|
4726
|
-
/** 检查是否有指定会话的 token */
|
|
4727
|
-
hasToken(sessionId) {
|
|
4728
|
-
return this.tokens.has(sessionId);
|
|
4729
|
-
}
|
|
4730
|
-
/**
|
|
4731
|
-
* 发送 APNs,自动处理环境探测。
|
|
4732
|
-
* 对每个 token:先用已确认的环境(如有);否则按 probeOrder 顺序探测,
|
|
4733
|
-
* 收到 BadDeviceToken / BadEnvironmentKeyInToken 自动切到另一个环境,
|
|
4734
|
-
* 并把成功的环境绑定到该 token。
|
|
4735
|
-
*/
|
|
4736
|
-
async sendToAPNs(deviceToken, payload, opts = {}) {
|
|
4737
|
-
if (this.deadTokens.has(deviceToken)) {
|
|
4738
|
-
throw new Error(`token permanently dead (Activity ended): ${deviceToken.slice(0, 16)}\u2026`);
|
|
4739
|
-
}
|
|
4740
|
-
const known = this.tokenEnv.get(deviceToken);
|
|
4741
|
-
if (known) {
|
|
4742
|
-
return this.sendToAPNsOnce(deviceToken, payload, opts, known);
|
|
4743
|
-
}
|
|
4744
|
-
const short = deviceToken.slice(0, 16);
|
|
4745
|
-
console.log(`[ActivityPushChannel] \u{1F50D} probe start token=${short}\u2026 order=[${this.probeOrder.join(",")}]`);
|
|
4746
|
-
let lastErr = null;
|
|
4747
|
-
for (const env of this.probeOrder) {
|
|
4748
|
-
try {
|
|
4749
|
-
console.log(`[ActivityPushChannel] \u{1F50D} probe try ${env} token=${short}\u2026`);
|
|
4750
|
-
await this.sendToAPNsOnce(deviceToken, payload, opts, env);
|
|
4751
|
-
this.tokenEnv.set(deviceToken, env);
|
|
4752
|
-
console.log(`[ActivityPushChannel] \u2705 probe bound to ${env} (token=${short}\u2026)`);
|
|
4753
|
-
return;
|
|
4754
|
-
} catch (err) {
|
|
4755
|
-
lastErr = err;
|
|
4756
|
-
const reason = err instanceof ApnsError ? JSON.parse(err.responseBody || "{}")?.reason ?? err.statusCode : String(err);
|
|
4757
|
-
console.log(`[ActivityPushChannel] \u{1F50D} probe ${env} failed: ${reason}`);
|
|
4758
|
-
if (!isBadDeviceTokenError(err)) {
|
|
4759
|
-
throw err;
|
|
4760
|
-
}
|
|
4761
|
-
}
|
|
4762
|
-
}
|
|
4763
|
-
this.deadTokens.add(deviceToken);
|
|
4764
|
-
for (const [sid, tok] of this.tokens) {
|
|
4765
|
-
if (tok === deviceToken) {
|
|
4766
|
-
this.tokens.delete(sid);
|
|
4767
|
-
console.warn(`[ActivityPushChannel] \u2620\uFE0F dead token, session removed: session=${sid.slice(0, 8)}\u2026 token=${short}\u2026 (Activity ended/iOS reinstalled)`);
|
|
4768
|
-
break;
|
|
4769
|
-
}
|
|
4770
|
-
}
|
|
4771
|
-
throw lastErr ?? new Error("APNs send failed: all environments rejected token");
|
|
4772
|
-
}
|
|
4773
|
-
/** 单次 APNs HTTP/2 请求(内部 helper,不做探测) */
|
|
4774
|
-
async sendToAPNsOnce(deviceToken, payload, opts, env) {
|
|
4775
|
-
const topic = "com.kachun.sessix.push-type.liveactivity";
|
|
4776
|
-
const jwt = this.getJWT();
|
|
4777
|
-
const payloadStr = JSON.stringify(payload);
|
|
4778
|
-
const priority = opts.priority ?? "10";
|
|
4779
|
-
return new Promise((resolve, reject) => {
|
|
4780
|
-
let client;
|
|
4781
|
-
try {
|
|
4782
|
-
client = this.getHttp2Client(env);
|
|
4783
|
-
} catch (err) {
|
|
4784
|
-
return reject(err);
|
|
4785
|
-
}
|
|
4786
|
-
const headers = {
|
|
4787
|
-
":method": "POST",
|
|
4788
|
-
":path": `/3/device/${deviceToken}`,
|
|
4789
|
-
"authorization": `bearer ${jwt}`,
|
|
4790
|
-
"apns-topic": topic,
|
|
4791
|
-
"apns-push-type": "liveactivity",
|
|
4792
|
-
"apns-priority": priority,
|
|
4793
|
-
"apns-expiration": String(Math.floor(Date.now() / 1e3) + 300),
|
|
4794
|
-
"content-type": "application/json",
|
|
4795
|
-
"content-length": Buffer.byteLength(payloadStr)
|
|
4796
|
-
};
|
|
4797
|
-
if (opts.collapseId) {
|
|
4798
|
-
headers["apns-collapse-id"] = opts.collapseId;
|
|
4799
|
-
}
|
|
4800
|
-
const req = client.request(headers);
|
|
4801
|
-
let statusCode = 0;
|
|
4802
|
-
let responseData = "";
|
|
4803
|
-
req.on("response", (headers2) => {
|
|
4804
|
-
statusCode = Number(headers2[":status"] ?? 0);
|
|
4805
|
-
});
|
|
4806
|
-
req.on("data", (chunk) => {
|
|
4807
|
-
responseData += chunk;
|
|
4808
|
-
});
|
|
4809
|
-
req.on("end", () => {
|
|
4810
|
-
if (statusCode === 200) {
|
|
4811
|
-
resolve();
|
|
4812
|
-
} else {
|
|
4813
|
-
if (statusCode === 0) {
|
|
4814
|
-
const c = this.http2Clients[env];
|
|
4815
|
-
c?.destroy();
|
|
4816
|
-
delete this.http2Clients[env];
|
|
4817
|
-
}
|
|
4818
|
-
reject(new ApnsError(statusCode, responseData));
|
|
4819
|
-
}
|
|
4820
|
-
});
|
|
4821
|
-
req.on("error", (err) => {
|
|
4822
|
-
reject(err);
|
|
4823
|
-
});
|
|
4824
|
-
req.write(payloadStr);
|
|
4825
|
-
req.end();
|
|
4826
|
-
});
|
|
4827
|
-
}
|
|
4828
|
-
/** 生成或获取缓存的 APNs JWT token */
|
|
4829
|
-
getJWT() {
|
|
4830
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
4831
|
-
if (this.cachedJwt && this.cachedJwt.expiresAt > now) {
|
|
4832
|
-
return this.cachedJwt.token;
|
|
4833
|
-
}
|
|
4834
|
-
const header = Buffer.from(JSON.stringify({
|
|
4835
|
-
alg: "ES256",
|
|
4836
|
-
kid: this.keyId
|
|
4837
|
-
})).toString("base64url");
|
|
4838
|
-
const claims = Buffer.from(JSON.stringify({
|
|
4839
|
-
iss: this.teamId,
|
|
4840
|
-
iat: now
|
|
4841
|
-
})).toString("base64url");
|
|
4842
|
-
const signingInput = `${header}.${claims}`;
|
|
4843
|
-
const sign = crypto.createSign("SHA256");
|
|
4844
|
-
sign.update(signingInput);
|
|
4845
|
-
const signature = sign.sign({ key: this.authKey, dsaEncoding: "ieee-p1363" }, "base64url");
|
|
4846
|
-
const token = `${signingInput}.${signature}`;
|
|
4847
|
-
this.cachedJwt = { token, expiresAt: now + 3e3 };
|
|
4848
|
-
return token;
|
|
4849
|
-
}
|
|
4850
|
-
};
|
|
4851
|
-
var ApnsError = class extends Error {
|
|
4852
|
-
constructor(statusCode, responseBody) {
|
|
4853
|
-
super(`APNs returned ${statusCode}: ${responseBody}`);
|
|
4854
|
-
this.statusCode = statusCode;
|
|
4855
|
-
this.responseBody = responseBody;
|
|
4856
|
-
this.name = "ApnsError";
|
|
4857
|
-
}
|
|
4858
|
-
};
|
|
4859
|
-
function isBadDeviceTokenError(err) {
|
|
4860
|
-
if (!(err instanceof ApnsError)) return false;
|
|
4861
|
-
if (err.statusCode !== 400 && err.statusCode !== 403 && err.statusCode !== 410) return false;
|
|
4862
|
-
try {
|
|
4863
|
-
const parsed = JSON.parse(err.responseBody);
|
|
4864
|
-
return parsed.reason === "BadDeviceToken" || parsed.reason === "BadEnvironmentKeyInToken" || parsed.reason === "Unregistered";
|
|
4865
|
-
} catch {
|
|
4866
|
-
return false;
|
|
4867
|
-
}
|
|
4868
|
-
}
|
|
4869
|
-
|
|
4870
|
-
// src/session/ProjectReader.ts
|
|
4871
|
-
var import_promises3 = require("fs/promises");
|
|
4872
|
-
var import_readline3 = require("readline");
|
|
4873
|
-
var import_path2 = require("path");
|
|
4874
|
-
var import_os2 = require("os");
|
|
4875
|
-
var CLAUDE_PROJECTS_DIR = (0, import_path2.join)((0, import_os2.homedir)(), ".claude", "projects");
|
|
4876
|
-
function getSessionFilePath(projectPath, sessionId) {
|
|
4877
|
-
return (0, import_path2.join)(CLAUDE_PROJECTS_DIR, encodeDirName(projectPath), `${sessionId}.jsonl`);
|
|
4878
|
-
}
|
|
4879
|
-
async function getProjects() {
|
|
4880
|
-
try {
|
|
4881
|
-
const dirExists = await directoryExists(CLAUDE_PROJECTS_DIR);
|
|
4882
|
-
if (!dirExists) {
|
|
4883
|
-
return { ok: true, value: [] };
|
|
4884
|
-
}
|
|
4885
|
-
const entries = await (0, import_promises3.readdir)(CLAUDE_PROJECTS_DIR, { withFileTypes: true });
|
|
4886
|
-
const projects = [];
|
|
4887
|
-
for (const entry of entries) {
|
|
4888
|
-
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
4889
|
-
continue;
|
|
5263
|
+
if (!token) return false;
|
|
5264
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
5265
|
+
const priority = opts?.priority ?? "5";
|
|
5266
|
+
const payload = {
|
|
5267
|
+
aps: {
|
|
5268
|
+
timestamp: now,
|
|
5269
|
+
event: "update",
|
|
5270
|
+
"content-state": contentState,
|
|
5271
|
+
"stale-date": now + 600
|
|
4890
5272
|
}
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
projects.push({
|
|
4897
|
-
id: encodedPath,
|
|
4898
|
-
path: decodedPath,
|
|
4899
|
-
name,
|
|
4900
|
-
sessionCount,
|
|
4901
|
-
lastActiveAt: latestMtime
|
|
5273
|
+
};
|
|
5274
|
+
try {
|
|
5275
|
+
await this.sendToAPNs(token, payload, {
|
|
5276
|
+
priority,
|
|
5277
|
+
collapseId: `state-${sessionId.slice(0, 54)}`
|
|
4902
5278
|
});
|
|
5279
|
+
return true;
|
|
5280
|
+
} catch (err) {
|
|
5281
|
+
console.warn(`[ActivityPushChannel] Update failed session=${sessionId}:`, err);
|
|
5282
|
+
return false;
|
|
4903
5283
|
}
|
|
4904
|
-
projects.sort((a, b) => a.name.localeCompare(b.name));
|
|
4905
|
-
return { ok: true, value: projects };
|
|
4906
|
-
} catch (err) {
|
|
4907
|
-
return {
|
|
4908
|
-
ok: false,
|
|
4909
|
-
error: err instanceof Error ? err : new Error(String(err))
|
|
4910
|
-
};
|
|
4911
5284
|
}
|
|
4912
|
-
|
|
4913
|
-
async
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
const
|
|
4917
|
-
const
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
jsonlFiles.map(async (entry) => {
|
|
4926
|
-
const sessionId = entry.name.slice(0, -6);
|
|
4927
|
-
const filePath = (0, import_path2.join)(projectDir, entry.name);
|
|
4928
|
-
try {
|
|
4929
|
-
const contentTs = await extractLastTimestamp(filePath);
|
|
4930
|
-
if (contentTs) {
|
|
4931
|
-
mtimeMap.set(sessionId, contentTs);
|
|
4932
|
-
} else {
|
|
4933
|
-
const fileStat = await (0, import_promises3.stat)(filePath);
|
|
4934
|
-
mtimeMap.set(sessionId, fileStat.mtimeMs);
|
|
4935
|
-
}
|
|
4936
|
-
} catch {
|
|
4937
|
-
mtimeMap.set(sessionId, 0);
|
|
4938
|
-
}
|
|
4939
|
-
})
|
|
4940
|
-
);
|
|
4941
|
-
const uuidDirs = entries.filter(
|
|
4942
|
-
(e) => e.isDirectory() && UUID_RE.test(e.name) && !mtimeMap.has(e.name)
|
|
4943
|
-
);
|
|
4944
|
-
for (const entry of uuidDirs) {
|
|
4945
|
-
try {
|
|
4946
|
-
const fileStat = await (0, import_promises3.stat)((0, import_path2.join)(projectDir, entry.name));
|
|
4947
|
-
mtimeMap.set(entry.name, fileStat.mtimeMs);
|
|
4948
|
-
} catch {
|
|
4949
|
-
mtimeMap.set(entry.name, 0);
|
|
5285
|
+
/** 发送带通知的 content-state 更新(审批请求时使用),返回是否发送成功 */
|
|
5286
|
+
async updateActivityWithAlert(sessionId, contentState, alert) {
|
|
5287
|
+
const token = this.tokens.get(sessionId);
|
|
5288
|
+
if (!token) return false;
|
|
5289
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
5290
|
+
const payload = {
|
|
5291
|
+
aps: {
|
|
5292
|
+
timestamp: now,
|
|
5293
|
+
event: "update",
|
|
5294
|
+
"content-state": contentState,
|
|
5295
|
+
"stale-date": now + 600,
|
|
5296
|
+
alert,
|
|
5297
|
+
sound: "default"
|
|
4950
5298
|
}
|
|
5299
|
+
};
|
|
5300
|
+
try {
|
|
5301
|
+
await this.sendToAPNs(token, payload, { priority: "10" });
|
|
5302
|
+
return true;
|
|
5303
|
+
} catch (err) {
|
|
5304
|
+
console.warn(`[ActivityPushChannel] Alert update failed session=${sessionId}:`, err);
|
|
5305
|
+
return false;
|
|
4951
5306
|
}
|
|
4952
|
-
|
|
4953
|
-
|
|
5307
|
+
}
|
|
5308
|
+
/**
|
|
5309
|
+
* 结束指定会话的 Live Activity。
|
|
5310
|
+
* 可选 alert:APNs event=end 时同时推送横幅通知 + 声音,用于在会话完成时提醒用户。
|
|
5311
|
+
*/
|
|
5312
|
+
async endActivity(sessionId, contentState, opts) {
|
|
5313
|
+
const token = this.tokens.get(sessionId);
|
|
5314
|
+
if (!token) return;
|
|
5315
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
5316
|
+
const aps = {
|
|
5317
|
+
timestamp: now,
|
|
5318
|
+
event: "end",
|
|
5319
|
+
"content-state": contentState
|
|
5320
|
+
};
|
|
5321
|
+
if (opts?.alert) {
|
|
5322
|
+
aps.alert = opts.alert;
|
|
5323
|
+
aps.sound = "default";
|
|
5324
|
+
}
|
|
5325
|
+
const payload = { aps };
|
|
4954
5326
|
try {
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
const mtime = mtimeMap.get(entry.sessionId) ?? entry.fileMtime ?? (entry.modified ? new Date(entry.modified).getTime() : 0);
|
|
4960
|
-
sessionMap.set(entry.sessionId, {
|
|
4961
|
-
sessionId: entry.sessionId,
|
|
4962
|
-
lastModified: mtime,
|
|
4963
|
-
summary: entry.summary,
|
|
4964
|
-
firstPrompt: entry.firstPrompt,
|
|
4965
|
-
messageCount: entry.messageCount
|
|
4966
|
-
});
|
|
4967
|
-
}
|
|
4968
|
-
await Promise.all(
|
|
4969
|
-
Array.from(sessionMap.values()).filter((s) => (s.messageCount ?? 0) > 0 && !s.summary && !s.firstPrompt).map(async (s) => {
|
|
4970
|
-
const filePath = (0, import_path2.join)(projectDir, `${s.sessionId}.jsonl`);
|
|
4971
|
-
const firstPrompt = await extractFirstPrompt(filePath).catch(() => void 0);
|
|
4972
|
-
if (firstPrompt) s.firstPrompt = firstPrompt;
|
|
4973
|
-
})
|
|
4974
|
-
);
|
|
4975
|
-
}
|
|
4976
|
-
} catch {
|
|
5327
|
+
await this.sendToAPNs(token, payload, { priority: "10" });
|
|
5328
|
+
} catch (err) {
|
|
5329
|
+
this.tokens.delete(sessionId);
|
|
5330
|
+
throw err;
|
|
4977
5331
|
}
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
5332
|
+
this.tokens.delete(sessionId);
|
|
5333
|
+
}
|
|
5334
|
+
/** 检查是否有指定会话的 token */
|
|
5335
|
+
hasToken(sessionId) {
|
|
5336
|
+
return this.tokens.has(sessionId);
|
|
5337
|
+
}
|
|
5338
|
+
// ============================================
|
|
5339
|
+
// 普通 Alert Push(直连 APNs,绕过 Expo 推送服务)
|
|
5340
|
+
// ============================================
|
|
5341
|
+
/** 注册原生 APNs device token,用于发送普通 alert 通知 */
|
|
5342
|
+
addAlertToken(token, ws) {
|
|
5343
|
+
this.alertTokens.add(token);
|
|
5344
|
+
if (ws) this.alertTokenWsMap.set(token, ws);
|
|
5345
|
+
console.log(`[ActivityPushChannel] Alert token registered (${this.alertTokens.size} device(s))`);
|
|
5346
|
+
}
|
|
5347
|
+
/** 移除原生 APNs device token */
|
|
5348
|
+
removeAlertToken(token) {
|
|
5349
|
+
this.alertTokens.delete(token);
|
|
5350
|
+
this.alertTokenWsMap.delete(token);
|
|
5351
|
+
this.alertTokenEnv.delete(token);
|
|
5352
|
+
this.alertSoundPreferences.delete(token);
|
|
5353
|
+
}
|
|
5354
|
+
/** 是否有可用的 alert token */
|
|
5355
|
+
hasAlertTokens() {
|
|
5356
|
+
return this.alertTokens.size > 0;
|
|
5357
|
+
}
|
|
5358
|
+
/** 更新 alert token 音效偏好 */
|
|
5359
|
+
setAlertSoundPreferences(prefs) {
|
|
5360
|
+
for (const token of this.alertTokens) {
|
|
5361
|
+
this.alertSoundPreferences.set(token, prefs);
|
|
4989
5362
|
}
|
|
4990
|
-
const sessions = Array.from(sessionMap.values()).filter((s) => {
|
|
4991
|
-
if (s.messageCount === 0) return false;
|
|
4992
|
-
if (s.messageCount === -1) return true;
|
|
4993
|
-
if (s.firstPrompt === void 0 && s.messageCount === void 0) return false;
|
|
4994
|
-
return true;
|
|
4995
|
-
});
|
|
4996
|
-
sessions.sort((a, b) => b.lastModified - a.lastModified);
|
|
4997
|
-
return { ok: true, value: sessions };
|
|
4998
|
-
} catch (err) {
|
|
4999
|
-
return {
|
|
5000
|
-
ok: false,
|
|
5001
|
-
error: err instanceof Error ? err : new Error(String(err))
|
|
5002
|
-
};
|
|
5003
5363
|
}
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5364
|
+
/**
|
|
5365
|
+
* 发送普通 alert 推送通知(直连 APNs,sandbox/production 自动探测)。
|
|
5366
|
+
* 实现 NotificationChannel.send 接口,可注册到 NotificationService。
|
|
5367
|
+
*/
|
|
5368
|
+
async send(payload) {
|
|
5369
|
+
if (this.alertTokens.size === 0) return;
|
|
5370
|
+
const isCompletionNotif = payload.data?.type === "task_complete" || payload.data?.type === "task_error";
|
|
5371
|
+
const targetTokens = isCompletionNotif ? Array.from(this.alertTokens) : Array.from(this.alertTokens).filter((token) => {
|
|
5372
|
+
const ws = this.alertTokenWsMap.get(token);
|
|
5373
|
+
return !ws || ws.readyState !== ws.OPEN;
|
|
5012
5374
|
});
|
|
5013
|
-
if (
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
const
|
|
5020
|
-
if (
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
if (msgContent.includes("<local-command") || msgContent.includes("<command-name>")) continue;
|
|
5024
|
-
} else if (Array.isArray(msgContent)) {
|
|
5025
|
-
const hasText = msgContent.some(
|
|
5026
|
-
(b) => b.type === "text" && !b.text?.includes("<local-command") && !b.text?.includes("<command-name>")
|
|
5027
|
-
);
|
|
5028
|
-
if (!hasText) continue;
|
|
5029
|
-
}
|
|
5030
|
-
const normalizedContent = typeof msgContent === "string" ? [{ type: "text", text: msgContent }] : Array.isArray(msgContent) ? msgContent.filter((b) => b.type === "text" && typeof b.text === "string") : [];
|
|
5031
|
-
if (normalizedContent.length === 0) continue;
|
|
5032
|
-
events.push({
|
|
5033
|
-
type: "user",
|
|
5034
|
-
message: {
|
|
5035
|
-
...obj.message,
|
|
5036
|
-
content: normalizedContent
|
|
5037
|
-
},
|
|
5038
|
-
session_id: sessionId
|
|
5039
|
-
});
|
|
5040
|
-
} else if (type === "assistant" && obj.message) {
|
|
5041
|
-
const content = (obj.message.content ?? []).filter(
|
|
5042
|
-
(b) => b.type === "text" || b.type === "tool_use" || b.type === "thinking"
|
|
5043
|
-
);
|
|
5044
|
-
if (content.length === 0) continue;
|
|
5045
|
-
events.push({
|
|
5046
|
-
type: "assistant",
|
|
5047
|
-
message: {
|
|
5048
|
-
id: obj.message.id ?? obj.uuid ?? `hist-${events.length}`,
|
|
5049
|
-
model: obj.message.model ?? "unknown",
|
|
5050
|
-
role: "assistant",
|
|
5051
|
-
content,
|
|
5052
|
-
stop_reason: obj.message.stop_reason,
|
|
5053
|
-
usage: obj.message.usage
|
|
5054
|
-
},
|
|
5055
|
-
session_id: sessionId
|
|
5056
|
-
});
|
|
5057
|
-
}
|
|
5058
|
-
} catch {
|
|
5375
|
+
if (targetTokens.length === 0) return;
|
|
5376
|
+
console.log(`[ActivityPushChannel] Alert push \u2192 ${targetTokens.length}/${this.alertTokens.size} device(s)${isCompletionNotif ? " (forced)" : ""}`);
|
|
5377
|
+
await Promise.all(targetTokens.map(async (deviceToken) => {
|
|
5378
|
+
let sound = payload.sound ?? "default";
|
|
5379
|
+
const prefs = this.alertSoundPreferences.get(deviceToken);
|
|
5380
|
+
if (prefs) {
|
|
5381
|
+
const notifType = payload.data?.type ?? "";
|
|
5382
|
+
if (notifType === "approval_request" && prefs.approval) sound = prefs.approval;
|
|
5383
|
+
else if (notifType === "task_complete" && prefs.taskComplete) sound = prefs.taskComplete;
|
|
5384
|
+
else if (notifType === "task_error" && prefs.taskError) sound = prefs.taskError;
|
|
5059
5385
|
}
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5386
|
+
const apnsPayload = {
|
|
5387
|
+
aps: {
|
|
5388
|
+
alert: { title: payload.title, ...payload.subtitle ? { subtitle: payload.subtitle } : {}, body: payload.body },
|
|
5389
|
+
...payload.badge !== void 0 ? { badge: payload.badge } : {},
|
|
5390
|
+
...sound && sound !== "none" ? { sound } : {},
|
|
5391
|
+
...payload.categoryId ? { category: payload.categoryId } : {}
|
|
5392
|
+
},
|
|
5393
|
+
...payload.data ?? {}
|
|
5394
|
+
};
|
|
5395
|
+
try {
|
|
5396
|
+
await this.sendAlertToAPNs(deviceToken, apnsPayload);
|
|
5397
|
+
} catch (err) {
|
|
5398
|
+
console.warn(`[ActivityPushChannel] Alert push failed for token ${deviceToken.slice(0, 16)}\u2026:`, err instanceof Error ? err.message : err);
|
|
5069
5399
|
}
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5400
|
+
}));
|
|
5401
|
+
}
|
|
5402
|
+
/** isAvailable 实现(NotificationChannel 接口) */
|
|
5403
|
+
isAvailable() {
|
|
5404
|
+
return this.alertTokens.size > 0;
|
|
5405
|
+
}
|
|
5406
|
+
/**
|
|
5407
|
+
* 发送 alert 通知到指定 device token,自动探测 sandbox/production。
|
|
5408
|
+
* 使用独立的 alertTokenEnv 映射(与 LA token 环境探测隔离)。
|
|
5409
|
+
*/
|
|
5410
|
+
async sendAlertToAPNs(deviceToken, payload) {
|
|
5411
|
+
const known = this.alertTokenEnv.get(deviceToken);
|
|
5412
|
+
if (known) {
|
|
5413
|
+
return this.sendToAPNsOnce(deviceToken, payload, { priority: "10", pushType: "alert", topic: this.bundleId }, known);
|
|
5414
|
+
}
|
|
5415
|
+
const short = deviceToken.slice(0, 16);
|
|
5416
|
+
console.log(`[ActivityPushChannel] \u{1F50D} alert probe start token=${short}\u2026 order=[${this.probeOrder.join(",")}]`);
|
|
5417
|
+
let lastErr = null;
|
|
5418
|
+
for (const env of this.probeOrder) {
|
|
5419
|
+
try {
|
|
5420
|
+
console.log(`[ActivityPushChannel] \u{1F50D} alert probe try ${env} token=${short}\u2026`);
|
|
5421
|
+
await this.sendToAPNsOnce(deviceToken, payload, { priority: "10", pushType: "alert", topic: this.bundleId }, env);
|
|
5422
|
+
this.alertTokenEnv.set(deviceToken, env);
|
|
5423
|
+
console.log(`[ActivityPushChannel] \u2705 alert probe bound to ${env} (token=${short}\u2026)`);
|
|
5424
|
+
return;
|
|
5425
|
+
} catch (err) {
|
|
5426
|
+
lastErr = err;
|
|
5427
|
+
const reason = err instanceof ApnsError ? JSON.parse(err.responseBody || "{}")?.reason ?? err.statusCode : String(err);
|
|
5428
|
+
console.log(`[ActivityPushChannel] \u{1F50D} alert probe ${env} failed: ${reason}`);
|
|
5429
|
+
if (isProviderTokenError(err)) throw err;
|
|
5430
|
+
if (!isBadDeviceTokenError(err)) throw err;
|
|
5081
5431
|
}
|
|
5082
5432
|
}
|
|
5083
|
-
|
|
5084
|
-
} catch (err) {
|
|
5085
|
-
return {
|
|
5086
|
-
ok: false,
|
|
5087
|
-
error: err instanceof Error ? err : new Error(String(err))
|
|
5088
|
-
};
|
|
5433
|
+
throw lastErr ?? new Error("APNs alert send failed: all environments rejected token");
|
|
5089
5434
|
}
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
const
|
|
5101
|
-
|
|
5435
|
+
/**
|
|
5436
|
+
* 发送 APNs,自动处理环境探测。
|
|
5437
|
+
* 对每个 token:先用已确认的环境(如有);否则按 probeOrder 顺序探测,
|
|
5438
|
+
* 收到 BadDeviceToken / BadEnvironmentKeyInToken 自动切到另一个环境,
|
|
5439
|
+
* 并把成功的环境绑定到该 token。
|
|
5440
|
+
*/
|
|
5441
|
+
async sendToAPNs(deviceToken, payload, opts = {}) {
|
|
5442
|
+
if (this.deadTokens.has(deviceToken)) {
|
|
5443
|
+
throw new Error(`token permanently dead (Activity ended): ${deviceToken.slice(0, 16)}\u2026`);
|
|
5444
|
+
}
|
|
5445
|
+
const known = this.tokenEnv.get(deviceToken);
|
|
5446
|
+
if (known) {
|
|
5447
|
+
return this.sendToAPNsOnce(deviceToken, payload, opts, known);
|
|
5448
|
+
}
|
|
5449
|
+
const short = deviceToken.slice(0, 16);
|
|
5450
|
+
console.log(`[ActivityPushChannel] \u{1F50D} probe start token=${short}\u2026 order=[${this.probeOrder.join(",")}]`);
|
|
5451
|
+
let lastErr = null;
|
|
5452
|
+
for (const env of this.probeOrder) {
|
|
5102
5453
|
try {
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5454
|
+
console.log(`[ActivityPushChannel] \u{1F50D} probe try ${env} token=${short}\u2026`);
|
|
5455
|
+
await this.sendToAPNsOnce(deviceToken, payload, opts, env);
|
|
5456
|
+
this.tokenEnv.set(deviceToken, env);
|
|
5457
|
+
console.log(`[ActivityPushChannel] \u2705 probe bound to ${env} (token=${short}\u2026)`);
|
|
5458
|
+
return;
|
|
5459
|
+
} catch (err) {
|
|
5460
|
+
lastErr = err;
|
|
5461
|
+
const reason = err instanceof ApnsError ? JSON.parse(err.responseBody || "{}")?.reason ?? err.statusCode : String(err);
|
|
5462
|
+
console.log(`[ActivityPushChannel] \u{1F50D} probe ${env} failed: ${reason}`);
|
|
5463
|
+
if (isProviderTokenError(err)) {
|
|
5464
|
+
console.error(
|
|
5465
|
+
`[ActivityPushChannel] \u274C APNs Auth Key \u5DF2\u5931\u6548\uFF08${reason}\uFF09\u3002\u8BF7\u5230 Apple Developer \u4E0B\u8F7D\u65B0\u7684 .p8 \u6587\u4EF6\uFF0C\u5E76\u66F4\u65B0 ~/.sessix/apns.json \u4E2D\u7684 keyId \u548C authKeyPath\uFF0C\u7136\u540E\u91CD\u542F\u670D\u52A1\u7AEF\u3002`
|
|
5466
|
+
);
|
|
5467
|
+
throw err;
|
|
5468
|
+
}
|
|
5469
|
+
if (!isBadDeviceTokenError(err)) {
|
|
5470
|
+
throw err;
|
|
5107
5471
|
}
|
|
5108
|
-
} catch {
|
|
5109
5472
|
}
|
|
5110
5473
|
}
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5474
|
+
this.deadTokens.add(deviceToken);
|
|
5475
|
+
for (const [sid, tok] of this.tokens) {
|
|
5476
|
+
if (tok === deviceToken) {
|
|
5477
|
+
this.tokens.delete(sid);
|
|
5478
|
+
console.warn(`[ActivityPushChannel] \u2620\uFE0F dead token, session removed: session=${sid.slice(0, 8)}\u2026 token=${short}\u2026 (Activity ended/iOS reinstalled)`);
|
|
5479
|
+
break;
|
|
5480
|
+
}
|
|
5481
|
+
}
|
|
5482
|
+
throw lastErr ?? new Error("APNs send failed: all environments rejected token");
|
|
5114
5483
|
}
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
const
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
});
|
|
5125
|
-
let lineCount = 0;
|
|
5126
|
-
for await (const line of rl) {
|
|
5127
|
-
if (++lineCount > 20) break;
|
|
5128
|
-
if (!line.trim()) continue;
|
|
5484
|
+
/** 单次 APNs HTTP/2 请求(内部 helper,不做探测) */
|
|
5485
|
+
async sendToAPNsOnce(deviceToken, payload, opts, env) {
|
|
5486
|
+
const topic = opts.topic ?? `${this.bundleId}.push-type.liveactivity`;
|
|
5487
|
+
const pushType = opts.pushType ?? "liveactivity";
|
|
5488
|
+
const jwt = this.getJWT();
|
|
5489
|
+
const payloadStr = JSON.stringify(payload);
|
|
5490
|
+
const priority = opts.priority ?? "10";
|
|
5491
|
+
return new Promise((resolve, reject) => {
|
|
5492
|
+
let client;
|
|
5129
5493
|
try {
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5494
|
+
client = this.getHttp2Client(env);
|
|
5495
|
+
} catch (err) {
|
|
5496
|
+
return reject(err);
|
|
5497
|
+
}
|
|
5498
|
+
const headers = {
|
|
5499
|
+
":method": "POST",
|
|
5500
|
+
":path": `/3/device/${deviceToken}`,
|
|
5501
|
+
"authorization": `bearer ${jwt}`,
|
|
5502
|
+
"apns-topic": topic,
|
|
5503
|
+
"apns-push-type": pushType,
|
|
5504
|
+
"apns-priority": priority,
|
|
5505
|
+
"apns-expiration": String(Math.floor(Date.now() / 1e3) + 300),
|
|
5506
|
+
"content-type": "application/json",
|
|
5507
|
+
"content-length": Buffer.byteLength(payloadStr)
|
|
5508
|
+
};
|
|
5509
|
+
if (opts.collapseId) {
|
|
5510
|
+
headers["apns-collapse-id"] = opts.collapseId;
|
|
5511
|
+
}
|
|
5512
|
+
const req = client.request(headers);
|
|
5513
|
+
req.setTimeout(1e4, () => {
|
|
5514
|
+
req.close(http2.constants.NGHTTP2_CANCEL);
|
|
5515
|
+
});
|
|
5516
|
+
let statusCode = 0;
|
|
5517
|
+
let responseData = "";
|
|
5518
|
+
req.on("response", (headers2) => {
|
|
5519
|
+
statusCode = Number(headers2[":status"] ?? 0);
|
|
5520
|
+
});
|
|
5521
|
+
req.on("data", (chunk) => {
|
|
5522
|
+
responseData += chunk;
|
|
5523
|
+
});
|
|
5524
|
+
req.on("end", () => {
|
|
5525
|
+
if (statusCode === 200) {
|
|
5526
|
+
resolve();
|
|
5527
|
+
} else {
|
|
5528
|
+
if (statusCode === 0) {
|
|
5529
|
+
const c = this.http2Clients[env];
|
|
5530
|
+
c?.destroy();
|
|
5531
|
+
delete this.http2Clients[env];
|
|
5145
5532
|
}
|
|
5533
|
+
reject(new ApnsError(statusCode, responseData));
|
|
5146
5534
|
}
|
|
5147
|
-
}
|
|
5148
|
-
|
|
5535
|
+
});
|
|
5536
|
+
req.on("error", (err) => {
|
|
5537
|
+
reject(err);
|
|
5538
|
+
});
|
|
5539
|
+
req.write(payloadStr);
|
|
5540
|
+
req.end();
|
|
5541
|
+
});
|
|
5542
|
+
}
|
|
5543
|
+
/** 生成或获取缓存的 APNs JWT token */
|
|
5544
|
+
getJWT() {
|
|
5545
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
5546
|
+
if (this.cachedJwt && this.cachedJwt.expiresAt > now) {
|
|
5547
|
+
return this.cachedJwt.token;
|
|
5149
5548
|
}
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5549
|
+
const header = Buffer.from(JSON.stringify({
|
|
5550
|
+
alg: "ES256",
|
|
5551
|
+
kid: this.keyId
|
|
5552
|
+
})).toString("base64url");
|
|
5553
|
+
const claims = Buffer.from(JSON.stringify({
|
|
5554
|
+
iss: this.teamId,
|
|
5555
|
+
iat: now
|
|
5556
|
+
})).toString("base64url");
|
|
5557
|
+
const signingInput = `${header}.${claims}`;
|
|
5558
|
+
const sign = crypto.createSign("SHA256");
|
|
5559
|
+
sign.update(signingInput);
|
|
5560
|
+
const signature = sign.sign({ key: this.authKey, dsaEncoding: "ieee-p1363" }, "base64url");
|
|
5561
|
+
const token = `${signingInput}.${signature}`;
|
|
5562
|
+
this.cachedJwt = { token, expiresAt: now + 3e3 };
|
|
5563
|
+
return token;
|
|
5153
5564
|
}
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
}
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
async function directoryExists(dirPath) {
|
|
5565
|
+
};
|
|
5566
|
+
var ApnsError = class extends Error {
|
|
5567
|
+
constructor(statusCode, responseBody) {
|
|
5568
|
+
super(`APNs returned ${statusCode}: ${responseBody}`);
|
|
5569
|
+
this.statusCode = statusCode;
|
|
5570
|
+
this.responseBody = responseBody;
|
|
5571
|
+
this.name = "ApnsError";
|
|
5572
|
+
}
|
|
5573
|
+
};
|
|
5574
|
+
function isProviderTokenError(err) {
|
|
5575
|
+
if (!(err instanceof ApnsError)) return false;
|
|
5576
|
+
if (err.statusCode !== 403) return false;
|
|
5167
5577
|
try {
|
|
5168
|
-
const
|
|
5169
|
-
return
|
|
5578
|
+
const parsed = JSON.parse(err.responseBody);
|
|
5579
|
+
return parsed.reason === "InvalidProviderToken" || parsed.reason === "ExpiredProviderToken";
|
|
5170
5580
|
} catch {
|
|
5171
5581
|
return false;
|
|
5172
5582
|
}
|
|
5173
5583
|
}
|
|
5174
|
-
|
|
5175
|
-
|
|
5584
|
+
function isBadDeviceTokenError(err) {
|
|
5585
|
+
if (!(err instanceof ApnsError)) return false;
|
|
5586
|
+
if (err.statusCode !== 400 && err.statusCode !== 403 && err.statusCode !== 410) return false;
|
|
5176
5587
|
try {
|
|
5177
|
-
const
|
|
5178
|
-
|
|
5179
|
-
entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => e.name.slice(0, -6))
|
|
5180
|
-
);
|
|
5181
|
-
const uuidDirs = entries.filter(
|
|
5182
|
-
(e) => e.isDirectory() && UUID_RE.test(e.name) && !jsonlNames.has(e.name)
|
|
5183
|
-
);
|
|
5184
|
-
let latestMtime = 0;
|
|
5185
|
-
const jsonlEntries = entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl"));
|
|
5186
|
-
await Promise.all([
|
|
5187
|
-
...jsonlEntries.map(async (entry) => {
|
|
5188
|
-
try {
|
|
5189
|
-
const contentTs = await extractLastTimestamp((0, import_path2.join)(dirPath, entry.name));
|
|
5190
|
-
const ts = contentTs ?? (await (0, import_promises3.stat)((0, import_path2.join)(dirPath, entry.name))).mtimeMs;
|
|
5191
|
-
if (ts > latestMtime) latestMtime = ts;
|
|
5192
|
-
} catch {
|
|
5193
|
-
}
|
|
5194
|
-
}),
|
|
5195
|
-
...uuidDirs.map(async (entry) => {
|
|
5196
|
-
try {
|
|
5197
|
-
const fileStat = await (0, import_promises3.stat)((0, import_path2.join)(dirPath, entry.name));
|
|
5198
|
-
if (fileStat.mtimeMs > latestMtime) latestMtime = fileStat.mtimeMs;
|
|
5199
|
-
} catch {
|
|
5200
|
-
}
|
|
5201
|
-
})
|
|
5202
|
-
]);
|
|
5203
|
-
return { count: jsonlNames.size + uuidDirs.length, latestMtime };
|
|
5588
|
+
const parsed = JSON.parse(err.responseBody);
|
|
5589
|
+
return parsed.reason === "BadDeviceToken" || parsed.reason === "BadEnvironmentKeyInToken" || parsed.reason === "Unregistered";
|
|
5204
5590
|
} catch {
|
|
5205
|
-
return
|
|
5591
|
+
return false;
|
|
5206
5592
|
}
|
|
5207
5593
|
}
|
|
5208
5594
|
|
|
@@ -5328,7 +5714,10 @@ var AuthManager = class extends import_events3.EventEmitter {
|
|
|
5328
5714
|
email: parsed.email,
|
|
5329
5715
|
authMethod: parsed.authMethod
|
|
5330
5716
|
};
|
|
5331
|
-
} catch {
|
|
5717
|
+
} catch (err) {
|
|
5718
|
+
console.warn(
|
|
5719
|
+
`[AuthManager] checkAuth \u5931\u8D25 (claudePath=${CLAUDE_PATH2}): ${err instanceof Error ? err.message : String(err)}`
|
|
5720
|
+
);
|
|
5332
5721
|
return { loggedIn: false };
|
|
5333
5722
|
}
|
|
5334
5723
|
}
|
|
@@ -5424,7 +5813,7 @@ var AuthManager = class extends import_events3.EventEmitter {
|
|
|
5424
5813
|
|
|
5425
5814
|
// src/terminal/TerminalExecutor.ts
|
|
5426
5815
|
var import_node_child_process8 = require("child_process");
|
|
5427
|
-
var
|
|
5816
|
+
var import_uuid4 = require("uuid");
|
|
5428
5817
|
var EXEC_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
5429
5818
|
var TerminalExecutor = class {
|
|
5430
5819
|
processes = /* @__PURE__ */ new Map();
|
|
@@ -5446,7 +5835,7 @@ var TerminalExecutor = class {
|
|
|
5446
5835
|
}
|
|
5447
5836
|
}
|
|
5448
5837
|
exec(sessionId, command, cwd) {
|
|
5449
|
-
const execId = (0,
|
|
5838
|
+
const execId = (0, import_uuid4.v4)();
|
|
5450
5839
|
const shell = isWindows ? "powershell" : process.env.SHELL || "/bin/zsh";
|
|
5451
5840
|
const args = isWindows ? ["-Command", command] : ["-l", "-c", command];
|
|
5452
5841
|
const proc = (0, import_node_child_process8.spawn)(shell, args, {
|
|
@@ -5523,7 +5912,7 @@ var import_node_util = require("util");
|
|
|
5523
5912
|
var import_promises4 = require("fs/promises");
|
|
5524
5913
|
var import_node_path6 = require("path");
|
|
5525
5914
|
var import_node_os7 = require("os");
|
|
5526
|
-
var
|
|
5915
|
+
var import_uuid5 = require("uuid");
|
|
5527
5916
|
var execAsync = (0, import_node_util.promisify)(import_node_child_process9.exec);
|
|
5528
5917
|
var BUILD_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
5529
5918
|
var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
@@ -5711,7 +6100,7 @@ ${e.stderr ?? ""}`);
|
|
|
5711
6100
|
return null;
|
|
5712
6101
|
}
|
|
5713
6102
|
if (override) await this.saveConfig(projectPath, override);
|
|
5714
|
-
const buildId = (0,
|
|
6103
|
+
const buildId = (0, import_uuid5.v4)();
|
|
5715
6104
|
const args = buildArgs(config);
|
|
5716
6105
|
const proc = (0, import_node_child_process9.spawn)("xcodebuild", args, {
|
|
5717
6106
|
cwd: projectPath,
|
|
@@ -5775,7 +6164,7 @@ ${e.stderr ?? ""}`);
|
|
|
5775
6164
|
this.emitInstallError(sessionId, "", "\u672A\u627E\u5230\u6784\u5EFA\u914D\u7F6E\uFF0C\u8BF7\u5148\u6784\u5EFA\u4E00\u6B21\n");
|
|
5776
6165
|
return null;
|
|
5777
6166
|
}
|
|
5778
|
-
const installId = (0,
|
|
6167
|
+
const installId = (0, import_uuid5.v4)();
|
|
5779
6168
|
let appPath;
|
|
5780
6169
|
try {
|
|
5781
6170
|
appPath = await this.getAppPath(projectPath, config);
|
|
@@ -6346,7 +6735,7 @@ function sourceWeight(s) {
|
|
|
6346
6735
|
// src/git/GitExecutor.ts
|
|
6347
6736
|
var import_node_child_process10 = require("child_process");
|
|
6348
6737
|
var import_node_util2 = require("util");
|
|
6349
|
-
var
|
|
6738
|
+
var import_uuid6 = require("uuid");
|
|
6350
6739
|
var execAsync2 = (0, import_node_util2.promisify)(import_node_child_process10.exec);
|
|
6351
6740
|
var STATUS_TIMEOUT_MS = 15e3;
|
|
6352
6741
|
var COMMIT_TIMEOUT_MS = 6e4;
|
|
@@ -6466,7 +6855,7 @@ var GitExecutor = class {
|
|
|
6466
6855
|
* - 若未提供 files:默认 git add -A(提交所有变更)
|
|
6467
6856
|
*/
|
|
6468
6857
|
async commit(sessionId, projectPath, message, files, alsoPush) {
|
|
6469
|
-
const opId = (0,
|
|
6858
|
+
const opId = (0, import_uuid6.v4)();
|
|
6470
6859
|
this.runSequence(sessionId, opId, "commit", projectPath, [
|
|
6471
6860
|
files && files.length > 0 ? ["git", "add", "--", ...files] : ["git", "add", "-A"],
|
|
6472
6861
|
["git", "commit", "-m", message]
|
|
@@ -6482,7 +6871,7 @@ var GitExecutor = class {
|
|
|
6482
6871
|
return opId;
|
|
6483
6872
|
}
|
|
6484
6873
|
async push(sessionId, projectPath) {
|
|
6485
|
-
const opId = (0,
|
|
6874
|
+
const opId = (0, import_uuid6.v4)();
|
|
6486
6875
|
this.runSequence(sessionId, opId, "push", projectPath, [
|
|
6487
6876
|
["git", "push"]
|
|
6488
6877
|
], PUSH_TIMEOUT_MS).catch((err) => {
|
|
@@ -6560,7 +6949,7 @@ var GitExecutor = class {
|
|
|
6560
6949
|
var import_promises6 = require("fs/promises");
|
|
6561
6950
|
var import_node_os8 = require("os");
|
|
6562
6951
|
var import_node_path8 = require("path");
|
|
6563
|
-
var
|
|
6952
|
+
var import_uuid7 = require("uuid");
|
|
6564
6953
|
var MAX_TIMEOUT_MS = 2147483647;
|
|
6565
6954
|
var ScheduledSessionManager = class {
|
|
6566
6955
|
tasks = /* @__PURE__ */ new Map();
|
|
@@ -6598,7 +6987,7 @@ var ScheduledSessionManager = class {
|
|
|
6598
6987
|
/** 注册一个定时任务(payload 由调用方校验) */
|
|
6599
6988
|
schedule(scheduledAt, payload) {
|
|
6600
6989
|
const task = {
|
|
6601
|
-
id: (0,
|
|
6990
|
+
id: (0, import_uuid7.v4)(),
|
|
6602
6991
|
scheduledAt,
|
|
6603
6992
|
createdAt: Date.now(),
|
|
6604
6993
|
payload
|
|
@@ -6703,6 +7092,7 @@ function isValidTask(value) {
|
|
|
6703
7092
|
var import_node_child_process11 = require("child_process");
|
|
6704
7093
|
var DEFAULT_MODELS = [
|
|
6705
7094
|
{ value: "opus", label: "Opus 4.7", sublabel: "Most capable for ambitious work" },
|
|
7095
|
+
{ value: "claude-opus-4-6", label: "Opus 4.6", sublabel: "Previous generation flagship" },
|
|
6706
7096
|
{ value: "sonnet", label: "Sonnet 4.6", sublabel: "Most efficient for everyday tasks" },
|
|
6707
7097
|
{ value: "haiku", label: "Haiku 4.5", sublabel: "Fastest for quick answers" }
|
|
6708
7098
|
];
|
|
@@ -6837,7 +7227,7 @@ async function start(opts = {}) {
|
|
|
6837
7227
|
try {
|
|
6838
7228
|
token = (await (0, import_promises7.readFile)(tokenFile, "utf8")).trim();
|
|
6839
7229
|
} catch {
|
|
6840
|
-
token = (0,
|
|
7230
|
+
token = (0, import_uuid8.v4)();
|
|
6841
7231
|
await (0, import_promises7.mkdir)(configDir, { recursive: true });
|
|
6842
7232
|
await (0, import_promises7.writeFile)(tokenFile, token, "utf8");
|
|
6843
7233
|
}
|
|
@@ -7029,6 +7419,7 @@ async function start(opts = {}) {
|
|
|
7029
7419
|
case "kill_session": {
|
|
7030
7420
|
wsBridge.broadcast({ type: "status_change", sessionId: event.sessionId, status: "idle" });
|
|
7031
7421
|
approvalProxy.clearPendingForSession(event.sessionId);
|
|
7422
|
+
approvalProxy.clearPendingQuestionsForSession(event.sessionId);
|
|
7032
7423
|
await sessionManager.killSession(event.sessionId);
|
|
7033
7424
|
wsBridge.broadcast({
|
|
7034
7425
|
type: "session_list",
|
|
@@ -7047,6 +7438,7 @@ async function start(opts = {}) {
|
|
|
7047
7438
|
}
|
|
7048
7439
|
case "answer_question": {
|
|
7049
7440
|
sessionManager.handleQuestionResponse(event.requestId, event.answer);
|
|
7441
|
+
approvalProxy.resolveQuestion(event.requestId, event.answer);
|
|
7050
7442
|
break;
|
|
7051
7443
|
}
|
|
7052
7444
|
case "subscribe": {
|
|
@@ -7231,6 +7623,14 @@ async function start(opts = {}) {
|
|
|
7231
7623
|
notificationService.removePushToken(event.token);
|
|
7232
7624
|
break;
|
|
7233
7625
|
}
|
|
7626
|
+
case "register_native_push_token": {
|
|
7627
|
+
notificationService.addNativePushToken(event.token, ws);
|
|
7628
|
+
break;
|
|
7629
|
+
}
|
|
7630
|
+
case "unregister_native_push_token": {
|
|
7631
|
+
notificationService.removeNativePushToken(event.token);
|
|
7632
|
+
break;
|
|
7633
|
+
}
|
|
7234
7634
|
case "update_notification_sounds": {
|
|
7235
7635
|
notificationService.setSoundPreferences(event.preferences);
|
|
7236
7636
|
break;
|
|
@@ -7468,6 +7868,9 @@ async function start(opts = {}) {
|
|
|
7468
7868
|
decision: decision.decision
|
|
7469
7869
|
});
|
|
7470
7870
|
});
|
|
7871
|
+
approvalProxy.onQuestionResolved((requestId) => {
|
|
7872
|
+
sessionManager.clearPendingQuestion(requestId);
|
|
7873
|
+
});
|
|
7471
7874
|
approvalProxy.onApprovalRequest((request) => {
|
|
7472
7875
|
wsBridge.broadcast({ type: "approval_request", request });
|
|
7473
7876
|
setTimeout(() => {
|
|
@@ -7484,6 +7887,9 @@ async function start(opts = {}) {
|
|
|
7484
7887
|
notificationService.notifyApproval(request, pendingCount);
|
|
7485
7888
|
}, 6e4);
|
|
7486
7889
|
});
|
|
7890
|
+
approvalProxy.setQuestionHandler(
|
|
7891
|
+
(sessionId, toolUseId, questions, requestId) => sessionManager.askQuestion(sessionId, toolUseId, questions, requestId)
|
|
7892
|
+
);
|
|
7487
7893
|
sessionManager.onEvent((event) => {
|
|
7488
7894
|
if (event.type !== "question_request") return;
|
|
7489
7895
|
const { request } = event;
|
|
@@ -7626,7 +8032,7 @@ async function start(opts = {}) {
|
|
|
7626
8032
|
openPairing: (duration) => pairingManager.open(duration),
|
|
7627
8033
|
closePairing: () => pairingManager.close(),
|
|
7628
8034
|
regenerateToken: async () => {
|
|
7629
|
-
const newToken = (0,
|
|
8035
|
+
const newToken = (0, import_uuid8.v4)();
|
|
7630
8036
|
await (0, import_promises7.mkdir)(configDir, { recursive: true });
|
|
7631
8037
|
await (0, import_promises7.writeFile)(tokenFile, newToken, "utf8");
|
|
7632
8038
|
instance.token = newToken;
|