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/server.js
CHANGED
|
@@ -307,7 +307,7 @@ function t(key, params) {
|
|
|
307
307
|
}
|
|
308
308
|
|
|
309
309
|
// src/server.ts
|
|
310
|
-
var
|
|
310
|
+
var import_uuid8 = require("uuid");
|
|
311
311
|
var import_promises7 = require("fs/promises");
|
|
312
312
|
var import_node_os9 = require("os");
|
|
313
313
|
var import_node_path9 = require("path");
|
|
@@ -316,7 +316,7 @@ var import_node_util3 = require("util");
|
|
|
316
316
|
|
|
317
317
|
// src/providers/ProcessProvider.ts
|
|
318
318
|
var import_child_process = require("child_process");
|
|
319
|
-
var
|
|
319
|
+
var import_readline2 = require("readline");
|
|
320
320
|
var import_events = require("events");
|
|
321
321
|
var import_node_os2 = require("os");
|
|
322
322
|
var import_uuid = require("uuid");
|
|
@@ -367,12 +367,60 @@ function isNormalExit(code, signal) {
|
|
|
367
367
|
}
|
|
368
368
|
|
|
369
369
|
// src/utils/claudePath.ts
|
|
370
|
+
function resolveStable(candidate) {
|
|
371
|
+
if (!candidate) return null;
|
|
372
|
+
try {
|
|
373
|
+
const real = (0, import_node_fs.realpathSync)(candidate.trim());
|
|
374
|
+
(0, import_node_fs.accessSync)(real, import_node_fs.constants.X_OK);
|
|
375
|
+
return real;
|
|
376
|
+
} catch {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function resolveViaLoginShell() {
|
|
381
|
+
if (isWindows) return null;
|
|
382
|
+
const shell = process.env.SHELL || "/bin/zsh";
|
|
383
|
+
try {
|
|
384
|
+
const out = (0, import_node_child_process2.execSync)(`${shell} -ilc 'command -v claude' 2>/dev/null`, {
|
|
385
|
+
encoding: "utf-8",
|
|
386
|
+
timeout: 8e3
|
|
387
|
+
}).trim().split("\n").filter(Boolean).pop();
|
|
388
|
+
return resolveStable(out);
|
|
389
|
+
} catch {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
function resolveViaFnm() {
|
|
394
|
+
if (isWindows) return null;
|
|
395
|
+
const base = (0, import_node_path.join)((0, import_node_os.homedir)(), ".fnm", "node-versions");
|
|
396
|
+
try {
|
|
397
|
+
const versions = (0, import_node_fs.readdirSync)(base).filter((v) => /^v?\d+\./.test(v)).sort(
|
|
398
|
+
(a, b) => b.localeCompare(a, void 0, { numeric: true, sensitivity: "base" })
|
|
399
|
+
);
|
|
400
|
+
for (const v of versions) {
|
|
401
|
+
const p = resolveStable((0, import_node_path.join)(base, v, "installation", "bin", "claude"));
|
|
402
|
+
if (p) return p;
|
|
403
|
+
}
|
|
404
|
+
} catch {
|
|
405
|
+
}
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
var cached = null;
|
|
370
409
|
function findClaudePath() {
|
|
410
|
+
if (cached) return cached;
|
|
411
|
+
const override = resolveStable(process.env.SESSIX_CLAUDE_PATH);
|
|
412
|
+
if (override) return cached = log(override, "env:SESSIX_CLAUDE_PATH");
|
|
371
413
|
try {
|
|
372
414
|
const cmd = isWindows ? "where claude" : "which claude";
|
|
373
|
-
|
|
415
|
+
const which = (0, import_node_child_process2.execSync)(cmd, { encoding: "utf-8" }).trim().split("\n")[0];
|
|
416
|
+
const stable = resolveStable(which);
|
|
417
|
+
if (stable) return cached = log(stable, "which");
|
|
374
418
|
} catch {
|
|
375
419
|
}
|
|
420
|
+
const fnm = resolveViaFnm();
|
|
421
|
+
if (fnm) return cached = log(fnm, "fnm-scan");
|
|
422
|
+
const viaShell = resolveViaLoginShell();
|
|
423
|
+
if (viaShell) return cached = log(viaShell, "login-shell");
|
|
376
424
|
const candidates = isWindows ? [
|
|
377
425
|
(0, import_node_path.join)(process.env.LOCALAPPDATA ?? "", "Programs", "claude", "claude.exe"),
|
|
378
426
|
(0, import_node_path.join)((0, import_node_os.homedir)(), "AppData", "Local", "Programs", "claude", "claude.exe"),
|
|
@@ -383,153 +431,521 @@ function findClaudePath() {
|
|
|
383
431
|
"/opt/homebrew/bin/claude"
|
|
384
432
|
];
|
|
385
433
|
for (const candidate of candidates) {
|
|
434
|
+
const stable = resolveStable(candidate);
|
|
435
|
+
if (stable) return cached = log(stable, "candidate");
|
|
436
|
+
}
|
|
437
|
+
console.warn(
|
|
438
|
+
"[claudePath] \u672A\u80FD\u5B9A\u4F4D claude\uFF0C\u515C\u5E95\u4F7F\u7528\u88F8 'claude'\uFF08PATH \u4E0D\u542B claude \u65F6\u4F1A\u5931\u8D25\uFF09"
|
|
439
|
+
);
|
|
440
|
+
return cached = "claude";
|
|
441
|
+
}
|
|
442
|
+
function log(path2, via) {
|
|
443
|
+
console.log(`[claudePath] \u89E3\u6790\u5230 claude: ${path2} (via ${via})`);
|
|
444
|
+
return path2;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/session/ProjectReader.ts
|
|
448
|
+
var import_promises = require("fs/promises");
|
|
449
|
+
var import_readline = require("readline");
|
|
450
|
+
var import_path = require("path");
|
|
451
|
+
var import_os = require("os");
|
|
452
|
+
var CLAUDE_PROJECTS_DIR = (0, import_path.join)((0, import_os.homedir)(), ".claude", "projects");
|
|
453
|
+
function getSessionFilePath(projectPath, sessionId) {
|
|
454
|
+
return (0, import_path.join)(CLAUDE_PROJECTS_DIR, encodeDirName(projectPath), `${sessionId}.jsonl`);
|
|
455
|
+
}
|
|
456
|
+
async function getSessionModel(projectPath, sessionId) {
|
|
457
|
+
const filePath = getSessionFilePath(projectPath, sessionId);
|
|
458
|
+
const raw = await (0, import_promises.readFile)(filePath, "utf-8").catch((err) => {
|
|
459
|
+
if (err.code === "ENOENT") return null;
|
|
460
|
+
throw err;
|
|
461
|
+
});
|
|
462
|
+
if (raw === null) return void 0;
|
|
463
|
+
const lines = raw.split("\n");
|
|
464
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
465
|
+
const line = lines[i].trim();
|
|
466
|
+
if (!line) continue;
|
|
386
467
|
try {
|
|
387
|
-
|
|
388
|
-
|
|
468
|
+
const obj = JSON.parse(line);
|
|
469
|
+
if (obj.type !== "assistant" || !obj.message) continue;
|
|
470
|
+
const model = obj.message.model;
|
|
471
|
+
if (typeof model === "string" && model && model !== "unknown") {
|
|
472
|
+
return model;
|
|
473
|
+
}
|
|
389
474
|
} catch {
|
|
390
475
|
}
|
|
391
476
|
}
|
|
392
|
-
return
|
|
477
|
+
return void 0;
|
|
393
478
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
activeSessions = /* @__PURE__ */ new Map();
|
|
400
|
-
/** 事件发射器,用于分发 Claude 事件流 */
|
|
401
|
-
emitter = new import_events.EventEmitter();
|
|
402
|
-
/** 已发射的 AskUserQuestion toolUseId 集合,按会话隔离(避免 partial message 重复触发) */
|
|
403
|
-
emittedQuestionToolUseIds = /* @__PURE__ */ new Map();
|
|
404
|
-
/**
|
|
405
|
-
* 启动新会话或恢复已有会话
|
|
406
|
-
*
|
|
407
|
-
* 会 spawn 一个 `claude` CLI 进程,设置工作目录和环境变量,
|
|
408
|
-
* 并开始监听 stdout 的 NDJSON 输出。
|
|
409
|
-
*/
|
|
410
|
-
async startSession(opts) {
|
|
411
|
-
const { projectPath, message, sessionId: existingSessionId, model, permissionMode, effort, images, fallbackModel, maxBudgetUsd } = opts;
|
|
412
|
-
const sessionId = existingSessionId ?? (0, import_uuid.v4)();
|
|
413
|
-
if (this.activeSessions.has(sessionId)) {
|
|
414
|
-
await this.killSession(sessionId);
|
|
479
|
+
async function getProjects() {
|
|
480
|
+
try {
|
|
481
|
+
const dirExists = await directoryExists(CLAUDE_PROJECTS_DIR);
|
|
482
|
+
if (!dirExists) {
|
|
483
|
+
return { ok: true, value: [] };
|
|
415
484
|
}
|
|
416
|
-
const
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
485
|
+
const entries = await (0, import_promises.readdir)(CLAUDE_PROJECTS_DIR, { withFileTypes: true });
|
|
486
|
+
const projects = [];
|
|
487
|
+
for (const entry of entries) {
|
|
488
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
const encodedPath = entry.name;
|
|
492
|
+
const decodedPath = decodeDirName(encodedPath);
|
|
493
|
+
const name = decodedPath.split("/").filter(Boolean).pop() ?? encodedPath;
|
|
494
|
+
const projectDir = (0, import_path.join)(CLAUDE_PROJECTS_DIR, encodedPath);
|
|
495
|
+
const { count: sessionCount, latestMtime } = await countJsonlFilesWithMtime(projectDir);
|
|
496
|
+
projects.push({
|
|
497
|
+
id: encodedPath,
|
|
498
|
+
path: decodedPath,
|
|
499
|
+
name,
|
|
500
|
+
sessionCount,
|
|
501
|
+
lastActiveAt: latestMtime
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
projects.sort((a, b) => a.name.localeCompare(b.name));
|
|
505
|
+
return { ok: true, value: projects };
|
|
506
|
+
} catch (err) {
|
|
507
|
+
return {
|
|
508
|
+
ok: false,
|
|
509
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
425
510
|
};
|
|
426
|
-
const resume = opts.resume ?? !!existingSessionId;
|
|
427
|
-
const proc = this.spawnClaudeProcess(sessionId, projectPath, resume, model, permissionMode, effort, fallbackModel, maxBudgetUsd);
|
|
428
|
-
this.writeUserMessage(proc, message, sessionId, images);
|
|
429
|
-
session.pid = proc.pid;
|
|
430
|
-
this.activeSessions.set(sessionId, { session, process: proc, model, permissionMode, effort, fallbackModel, maxBudgetUsd });
|
|
431
|
-
proc.on("error", (err) => {
|
|
432
|
-
console.error(`[ProcessProvider] Session ${sessionId} process error:`, err.message);
|
|
433
|
-
this.activeSessions.delete(sessionId);
|
|
434
|
-
const syntheticResult = {
|
|
435
|
-
type: "result",
|
|
436
|
-
subtype: "error",
|
|
437
|
-
result: `Process spawn failed: ${err.message}`,
|
|
438
|
-
session_id: sessionId,
|
|
439
|
-
duration_ms: 0,
|
|
440
|
-
is_error: true,
|
|
441
|
-
num_turns: 0
|
|
442
|
-
};
|
|
443
|
-
this.emitter.emit(this.getEventName(sessionId), syntheticResult);
|
|
444
|
-
});
|
|
445
|
-
this.attachStdoutListener(sessionId, proc);
|
|
446
|
-
this.attachStderrListener(sessionId, proc);
|
|
447
|
-
this.attachExitListener(sessionId, proc);
|
|
448
|
-
return session;
|
|
449
511
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
return;
|
|
512
|
+
}
|
|
513
|
+
async function getHistoricalSessions(projectPath) {
|
|
514
|
+
try {
|
|
515
|
+
const encodedPath = encodeDirName(projectPath);
|
|
516
|
+
const projectDir = (0, import_path.join)(CLAUDE_PROJECTS_DIR, encodedPath);
|
|
517
|
+
const dirExists = await directoryExists(projectDir);
|
|
518
|
+
if (!dirExists) {
|
|
519
|
+
return { ok: true, value: [] };
|
|
459
520
|
}
|
|
460
|
-
|
|
521
|
+
const entries = await (0, import_promises.readdir)(projectDir, { withFileTypes: true });
|
|
522
|
+
const jsonlFiles = entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl"));
|
|
523
|
+
const mtimeMap = /* @__PURE__ */ new Map();
|
|
524
|
+
await Promise.all(
|
|
525
|
+
jsonlFiles.map(async (entry) => {
|
|
526
|
+
const sessionId = entry.name.slice(0, -6);
|
|
527
|
+
const filePath = (0, import_path.join)(projectDir, entry.name);
|
|
528
|
+
try {
|
|
529
|
+
const contentTs = await extractLastTimestamp(filePath);
|
|
530
|
+
if (contentTs) {
|
|
531
|
+
mtimeMap.set(sessionId, contentTs);
|
|
532
|
+
} else {
|
|
533
|
+
const fileStat = await (0, import_promises.stat)(filePath);
|
|
534
|
+
mtimeMap.set(sessionId, fileStat.mtimeMs);
|
|
535
|
+
}
|
|
536
|
+
} catch {
|
|
537
|
+
mtimeMap.set(sessionId, 0);
|
|
538
|
+
}
|
|
539
|
+
})
|
|
540
|
+
);
|
|
541
|
+
const uuidDirs = entries.filter(
|
|
542
|
+
(e) => e.isDirectory() && UUID_RE.test(e.name) && !mtimeMap.has(e.name)
|
|
543
|
+
);
|
|
544
|
+
for (const entry of uuidDirs) {
|
|
461
545
|
try {
|
|
462
|
-
|
|
546
|
+
const fileStat = await (0, import_promises.stat)((0, import_path.join)(projectDir, entry.name));
|
|
547
|
+
mtimeMap.set(entry.name, fileStat.mtimeMs);
|
|
463
548
|
} catch {
|
|
549
|
+
mtimeMap.set(entry.name, 0);
|
|
464
550
|
}
|
|
465
|
-
await killProcessCrossPlatform(entry.process);
|
|
466
|
-
}
|
|
467
|
-
this.emittedQuestionToolUseIds.delete(sessionId);
|
|
468
|
-
this.activeSessions.delete(sessionId);
|
|
469
|
-
}
|
|
470
|
-
/**
|
|
471
|
-
* 向已有会话发送新消息
|
|
472
|
-
*
|
|
473
|
-
* 快速路径:进程存活时直接写 stdin(毫秒级响应)。
|
|
474
|
-
* 慢速路径:进程已退出时 respawn 并 --resume。
|
|
475
|
-
*/
|
|
476
|
-
async sendMessage(sessionId, message, permissionMode, images) {
|
|
477
|
-
const entry = this.activeSessions.get(sessionId);
|
|
478
|
-
if (!entry) {
|
|
479
|
-
throw new Error(`Session ${sessionId} not found or already ended`);
|
|
480
551
|
}
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
552
|
+
const indexPath = (0, import_path.join)(projectDir, "sessions-index.json");
|
|
553
|
+
const sessionMap = /* @__PURE__ */ new Map();
|
|
554
|
+
try {
|
|
555
|
+
const indexContent = await (0, import_promises.readFile)(indexPath, "utf-8");
|
|
556
|
+
const indexData = JSON.parse(indexContent);
|
|
557
|
+
if (indexData.version === 1 && Array.isArray(indexData.entries)) {
|
|
558
|
+
for (const entry of indexData.entries) {
|
|
559
|
+
const mtime = mtimeMap.get(entry.sessionId) ?? entry.fileMtime ?? (entry.modified ? new Date(entry.modified).getTime() : 0);
|
|
560
|
+
sessionMap.set(entry.sessionId, {
|
|
561
|
+
sessionId: entry.sessionId,
|
|
562
|
+
lastModified: mtime,
|
|
563
|
+
summary: entry.summary,
|
|
564
|
+
firstPrompt: entry.firstPrompt,
|
|
565
|
+
messageCount: entry.messageCount
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
await Promise.all(
|
|
569
|
+
Array.from(sessionMap.values()).filter((s) => (s.messageCount ?? 0) > 0 && !s.summary && !s.firstPrompt).map(async (s) => {
|
|
570
|
+
const filePath = (0, import_path.join)(projectDir, `${s.sessionId}.jsonl`);
|
|
571
|
+
const firstPrompt = await extractFirstPrompt(filePath).catch(() => void 0);
|
|
572
|
+
if (firstPrompt) s.firstPrompt = firstPrompt;
|
|
573
|
+
})
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
} catch {
|
|
487
577
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
if (
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
}
|
|
578
|
+
const uuidDirSet = new Set(uuidDirs.map((e) => e.name));
|
|
579
|
+
for (const [sessionId, mtime] of mtimeMap) {
|
|
580
|
+
if (!sessionMap.has(sessionId)) {
|
|
581
|
+
if (uuidDirSet.has(sessionId)) {
|
|
582
|
+
sessionMap.set(sessionId, { sessionId, lastModified: mtime, messageCount: -1 });
|
|
583
|
+
} else {
|
|
584
|
+
const filePath = (0, import_path.join)(projectDir, `${sessionId}.jsonl`);
|
|
585
|
+
const firstPrompt = await extractFirstPrompt(filePath).catch(() => void 0);
|
|
586
|
+
sessionMap.set(sessionId, { sessionId, lastModified: mtime, firstPrompt });
|
|
494
587
|
}
|
|
495
|
-
killProcessCrossPlatform(entry.process);
|
|
496
588
|
}
|
|
497
|
-
} else {
|
|
498
|
-
console.log(`[ProcessProvider] Session ${sessionId}: process exited, respawning`);
|
|
499
589
|
}
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
entry.session.lastActiveAt = Date.now();
|
|
506
|
-
entry.session.pid = proc.pid;
|
|
507
|
-
entry.process = proc;
|
|
508
|
-
entry.permissionMode = newMode;
|
|
509
|
-
entry.pendingQuestion = savedPendingQuestion;
|
|
510
|
-
proc.on("error", (err) => {
|
|
511
|
-
console.error(`[ProcessProvider] Session ${sessionId} sendMessage process error:`, err.message);
|
|
512
|
-
this.activeSessions.delete(sessionId);
|
|
513
|
-
const syntheticResult = {
|
|
514
|
-
type: "result",
|
|
515
|
-
subtype: "error",
|
|
516
|
-
result: `Failed to send message: ${err.message}`,
|
|
517
|
-
session_id: sessionId,
|
|
518
|
-
duration_ms: 0,
|
|
519
|
-
is_error: true,
|
|
520
|
-
num_turns: 0
|
|
521
|
-
};
|
|
522
|
-
this.emitter.emit(this.getEventName(sessionId), syntheticResult);
|
|
590
|
+
const sessions = Array.from(sessionMap.values()).filter((s) => {
|
|
591
|
+
if (s.messageCount === 0) return false;
|
|
592
|
+
if (s.messageCount === -1) return true;
|
|
593
|
+
if (s.firstPrompt === void 0 && s.messageCount === void 0) return false;
|
|
594
|
+
return true;
|
|
523
595
|
});
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
596
|
+
sessions.sort((a, b) => b.lastModified - a.lastModified);
|
|
597
|
+
return { ok: true, value: sessions };
|
|
598
|
+
} catch (err) {
|
|
599
|
+
return {
|
|
600
|
+
ok: false,
|
|
601
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
602
|
+
};
|
|
527
603
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
604
|
+
}
|
|
605
|
+
async function getSessionHistory(projectPath, sessionId) {
|
|
606
|
+
try {
|
|
607
|
+
const encodedPath = encodeDirName(projectPath);
|
|
608
|
+
const filePath = (0, import_path.join)(CLAUDE_PROJECTS_DIR, encodedPath, `${sessionId}.jsonl`);
|
|
609
|
+
const raw = await (0, import_promises.readFile)(filePath, "utf-8").catch((err) => {
|
|
610
|
+
if (err.code === "ENOENT") return null;
|
|
611
|
+
throw err;
|
|
612
|
+
});
|
|
613
|
+
if (raw === null) return { ok: true, value: [] };
|
|
614
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
615
|
+
const events = [];
|
|
616
|
+
for (const line of lines) {
|
|
617
|
+
try {
|
|
618
|
+
const obj = JSON.parse(line);
|
|
619
|
+
const type = obj.type;
|
|
620
|
+
if (type === "user" && obj.message) {
|
|
621
|
+
const msgContent = obj.message.content;
|
|
622
|
+
if (typeof msgContent === "string") {
|
|
623
|
+
if (msgContent.includes("<local-command") || msgContent.includes("<command-name>")) continue;
|
|
624
|
+
} else if (Array.isArray(msgContent)) {
|
|
625
|
+
const hasText = msgContent.some(
|
|
626
|
+
(b) => b.type === "text" && !b.text?.includes("<local-command") && !b.text?.includes("<command-name>")
|
|
627
|
+
);
|
|
628
|
+
if (!hasText) continue;
|
|
629
|
+
}
|
|
630
|
+
const normalizedContent = typeof msgContent === "string" ? [{ type: "text", text: msgContent }] : Array.isArray(msgContent) ? msgContent.filter((b) => b.type === "text" && typeof b.text === "string") : [];
|
|
631
|
+
if (normalizedContent.length === 0) continue;
|
|
632
|
+
events.push({
|
|
633
|
+
type: "user",
|
|
634
|
+
message: {
|
|
635
|
+
...obj.message,
|
|
636
|
+
content: normalizedContent
|
|
637
|
+
},
|
|
638
|
+
session_id: sessionId
|
|
639
|
+
});
|
|
640
|
+
} else if (type === "assistant" && obj.message) {
|
|
641
|
+
const content = (obj.message.content ?? []).filter(
|
|
642
|
+
(b) => b.type === "text" || b.type === "tool_use" || b.type === "thinking"
|
|
643
|
+
);
|
|
644
|
+
if (content.length === 0) continue;
|
|
645
|
+
events.push({
|
|
646
|
+
type: "assistant",
|
|
647
|
+
message: {
|
|
648
|
+
id: obj.message.id ?? obj.uuid ?? `hist-${events.length}`,
|
|
649
|
+
model: obj.message.model ?? "unknown",
|
|
650
|
+
role: "assistant",
|
|
651
|
+
content,
|
|
652
|
+
stop_reason: obj.message.stop_reason,
|
|
653
|
+
usage: obj.message.usage
|
|
654
|
+
},
|
|
655
|
+
session_id: sessionId
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
} catch {
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (events.length > 0) {
|
|
662
|
+
let totalInputTokens = 0;
|
|
663
|
+
let totalOutputTokens = 0;
|
|
664
|
+
for (const ev of events) {
|
|
665
|
+
if (ev.type === "assistant" && ev.message.usage) {
|
|
666
|
+
totalInputTokens += ev.message.usage.input_tokens ?? 0;
|
|
667
|
+
totalOutputTokens += ev.message.usage.output_tokens ?? 0;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (totalInputTokens > 0 || totalOutputTokens > 0) {
|
|
671
|
+
events.push({
|
|
672
|
+
type: "result",
|
|
673
|
+
subtype: "success",
|
|
674
|
+
is_error: false,
|
|
675
|
+
duration_ms: 0,
|
|
676
|
+
num_turns: events.filter((e) => e.type === "user").length,
|
|
677
|
+
result: "",
|
|
678
|
+
session_id: sessionId,
|
|
679
|
+
usage: { input_tokens: totalInputTokens, output_tokens: totalOutputTokens }
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return { ok: true, value: events };
|
|
684
|
+
} catch (err) {
|
|
685
|
+
return {
|
|
686
|
+
ok: false,
|
|
687
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
async function extractLastTimestamp(filePath) {
|
|
692
|
+
let fileHandle;
|
|
693
|
+
try {
|
|
694
|
+
fileHandle = await (0, import_promises.open)(filePath, "r");
|
|
695
|
+
const fileStat = await fileHandle.stat();
|
|
696
|
+
const readSize = Math.min(fileStat.size, 8192);
|
|
697
|
+
const buffer = Buffer.alloc(readSize);
|
|
698
|
+
await fileHandle.read(buffer, 0, readSize, fileStat.size - readSize);
|
|
699
|
+
const tail = buffer.toString("utf-8");
|
|
700
|
+
const lines = tail.split("\n").filter((l) => l.trim());
|
|
701
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
702
|
+
try {
|
|
703
|
+
const obj = JSON.parse(lines[i]);
|
|
704
|
+
if (obj.timestamp) {
|
|
705
|
+
const ts = new Date(obj.timestamp).getTime();
|
|
706
|
+
if (!isNaN(ts)) return ts;
|
|
707
|
+
}
|
|
708
|
+
} catch {
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
} catch {
|
|
712
|
+
} finally {
|
|
713
|
+
await fileHandle?.close();
|
|
714
|
+
}
|
|
715
|
+
return void 0;
|
|
716
|
+
}
|
|
717
|
+
async function extractFirstPrompt(filePath) {
|
|
718
|
+
let fileHandle;
|
|
719
|
+
try {
|
|
720
|
+
fileHandle = await (0, import_promises.open)(filePath, "r");
|
|
721
|
+
const rl = (0, import_readline.createInterface)({
|
|
722
|
+
input: fileHandle.createReadStream({ encoding: "utf-8" }),
|
|
723
|
+
crlfDelay: Infinity
|
|
724
|
+
});
|
|
725
|
+
let lineCount = 0;
|
|
726
|
+
for await (const line of rl) {
|
|
727
|
+
if (++lineCount > 20) break;
|
|
728
|
+
if (!line.trim()) continue;
|
|
729
|
+
try {
|
|
730
|
+
const obj = JSON.parse(line);
|
|
731
|
+
if (obj.type === "user" && obj.message) {
|
|
732
|
+
const msgContent = obj.message.content;
|
|
733
|
+
let text = "";
|
|
734
|
+
if (typeof msgContent === "string") {
|
|
735
|
+
text = msgContent;
|
|
736
|
+
} else if (Array.isArray(msgContent)) {
|
|
737
|
+
const textBlock = msgContent.find((b) => b.type === "text" && typeof b.text === "string");
|
|
738
|
+
text = textBlock?.text ?? "";
|
|
739
|
+
}
|
|
740
|
+
if (text && !text.includes("<local-command") && !text.includes("<command-name>")) {
|
|
741
|
+
text = text.replace(/<ide_opened_file>[\s\S]*?<\/ide_opened_file>/gi, "");
|
|
742
|
+
text = text.replace(/<[^>]+>/g, "").trim();
|
|
743
|
+
rl.close();
|
|
744
|
+
return text.length > 80 ? text.slice(0, 80) + "..." : text;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
} catch {
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
} catch {
|
|
751
|
+
} finally {
|
|
752
|
+
await fileHandle?.close();
|
|
753
|
+
}
|
|
754
|
+
return void 0;
|
|
755
|
+
}
|
|
756
|
+
function decodeDirName(dirName) {
|
|
757
|
+
const placeholder = "\0";
|
|
758
|
+
const escaped = dirName.replace(/--/g, placeholder);
|
|
759
|
+
const decoded = escaped.replace(/-/g, "/");
|
|
760
|
+
return decoded.replace(new RegExp(placeholder, "g"), "-");
|
|
761
|
+
}
|
|
762
|
+
function encodeDirName(path2) {
|
|
763
|
+
const escaped = path2.replace(/-/g, "--");
|
|
764
|
+
return escaped.replace(/\//g, "-");
|
|
765
|
+
}
|
|
766
|
+
async function directoryExists(dirPath) {
|
|
767
|
+
try {
|
|
768
|
+
const s = await (0, import_promises.stat)(dirPath);
|
|
769
|
+
return s.isDirectory();
|
|
770
|
+
} catch {
|
|
771
|
+
return false;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
775
|
+
async function countJsonlFilesWithMtime(dirPath) {
|
|
776
|
+
try {
|
|
777
|
+
const entries = await (0, import_promises.readdir)(dirPath, { withFileTypes: true });
|
|
778
|
+
const jsonlNames = new Set(
|
|
779
|
+
entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => e.name.slice(0, -6))
|
|
780
|
+
);
|
|
781
|
+
const uuidDirs = entries.filter(
|
|
782
|
+
(e) => e.isDirectory() && UUID_RE.test(e.name) && !jsonlNames.has(e.name)
|
|
783
|
+
);
|
|
784
|
+
let latestMtime = 0;
|
|
785
|
+
const jsonlEntries = entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl"));
|
|
786
|
+
await Promise.all([
|
|
787
|
+
...jsonlEntries.map(async (entry) => {
|
|
788
|
+
try {
|
|
789
|
+
const contentTs = await extractLastTimestamp((0, import_path.join)(dirPath, entry.name));
|
|
790
|
+
const ts = contentTs ?? (await (0, import_promises.stat)((0, import_path.join)(dirPath, entry.name))).mtimeMs;
|
|
791
|
+
if (ts > latestMtime) latestMtime = ts;
|
|
792
|
+
} catch {
|
|
793
|
+
}
|
|
794
|
+
}),
|
|
795
|
+
...uuidDirs.map(async (entry) => {
|
|
796
|
+
try {
|
|
797
|
+
const fileStat = await (0, import_promises.stat)((0, import_path.join)(dirPath, entry.name));
|
|
798
|
+
if (fileStat.mtimeMs > latestMtime) latestMtime = fileStat.mtimeMs;
|
|
799
|
+
} catch {
|
|
800
|
+
}
|
|
801
|
+
})
|
|
802
|
+
]);
|
|
803
|
+
return { count: jsonlNames.size + uuidDirs.length, latestMtime };
|
|
804
|
+
} catch {
|
|
805
|
+
return { count: 0, latestMtime: 0 };
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// src/providers/ProcessProvider.ts
|
|
810
|
+
var CLAUDE_PATH = findClaudePath();
|
|
811
|
+
var ProcessProvider = class {
|
|
812
|
+
/** 活跃会话映射表:sessionId -> { session, process } */
|
|
813
|
+
activeSessions = /* @__PURE__ */ new Map();
|
|
814
|
+
/** 事件发射器,用于分发 Claude 事件流 */
|
|
815
|
+
emitter = new import_events.EventEmitter();
|
|
816
|
+
/**
|
|
817
|
+
* 启动新会话或恢复已有会话
|
|
818
|
+
*
|
|
819
|
+
* 会 spawn 一个 `claude` CLI 进程,设置工作目录和环境变量,
|
|
820
|
+
* 并开始监听 stdout 的 NDJSON 输出。
|
|
821
|
+
*/
|
|
822
|
+
async startSession(opts) {
|
|
823
|
+
const { projectPath, message, sessionId: existingSessionId, model, permissionMode, effort, images, fallbackModel, maxBudgetUsd } = opts;
|
|
824
|
+
const sessionId = existingSessionId ?? (0, import_uuid.v4)();
|
|
825
|
+
if (this.activeSessions.has(sessionId)) {
|
|
826
|
+
await this.killSession(sessionId);
|
|
827
|
+
}
|
|
828
|
+
const projectId = projectPath.split("/").filter(Boolean).pop() ?? "unknown";
|
|
829
|
+
const session = {
|
|
830
|
+
id: sessionId,
|
|
831
|
+
projectId,
|
|
832
|
+
projectPath,
|
|
833
|
+
status: "running",
|
|
834
|
+
createdAt: Date.now(),
|
|
835
|
+
lastActiveAt: Date.now(),
|
|
836
|
+
summary: message.slice(0, 80)
|
|
837
|
+
};
|
|
838
|
+
const resume = opts.resume ?? !!existingSessionId;
|
|
839
|
+
let effectiveModel = model;
|
|
840
|
+
if (resume && !effectiveModel) {
|
|
841
|
+
effectiveModel = await getSessionModel(projectPath, sessionId).catch(() => void 0);
|
|
842
|
+
if (effectiveModel) {
|
|
843
|
+
console.log(`[ProcessProvider] Session ${sessionId}: resume restored original model "${effectiveModel}"`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
const proc = this.spawnClaudeProcess(sessionId, projectPath, resume, effectiveModel, permissionMode, effort, fallbackModel, maxBudgetUsd);
|
|
847
|
+
this.writeUserMessage(proc, message, sessionId, images);
|
|
848
|
+
session.pid = proc.pid;
|
|
849
|
+
this.activeSessions.set(sessionId, { session, process: proc, model: effectiveModel, permissionMode, effort, fallbackModel, maxBudgetUsd });
|
|
850
|
+
proc.on("error", (err) => {
|
|
851
|
+
console.error(`[ProcessProvider] Session ${sessionId} process error:`, err.message);
|
|
852
|
+
this.activeSessions.delete(sessionId);
|
|
853
|
+
const syntheticResult = {
|
|
854
|
+
type: "result",
|
|
855
|
+
subtype: "error",
|
|
856
|
+
result: `Process spawn failed: ${err.message}`,
|
|
857
|
+
session_id: sessionId,
|
|
858
|
+
duration_ms: 0,
|
|
859
|
+
is_error: true,
|
|
860
|
+
num_turns: 0
|
|
861
|
+
};
|
|
862
|
+
this.emitter.emit(this.getEventName(sessionId), syntheticResult);
|
|
863
|
+
});
|
|
864
|
+
this.attachStdoutListener(sessionId, proc);
|
|
865
|
+
this.attachStderrListener(sessionId, proc);
|
|
866
|
+
this.attachExitListener(sessionId, proc);
|
|
867
|
+
return session;
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* 终止指定会话
|
|
871
|
+
*
|
|
872
|
+
* kill 进程并从活跃映射中移除。
|
|
873
|
+
*/
|
|
874
|
+
async killSession(sessionId) {
|
|
875
|
+
const entry = this.activeSessions.get(sessionId);
|
|
876
|
+
if (!entry) {
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
if (entry.process.exitCode === null && entry.process.signalCode === null) {
|
|
880
|
+
try {
|
|
881
|
+
entry.process.stdin?.end();
|
|
882
|
+
} catch {
|
|
883
|
+
}
|
|
884
|
+
await killProcessCrossPlatform(entry.process);
|
|
885
|
+
}
|
|
886
|
+
this.activeSessions.delete(sessionId);
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* 向已有会话发送新消息
|
|
890
|
+
*
|
|
891
|
+
* 快速路径:进程存活时直接写 stdin(毫秒级响应)。
|
|
892
|
+
* 慢速路径:进程已退出时 respawn 并 --resume。
|
|
893
|
+
*/
|
|
894
|
+
async sendMessage(sessionId, message, permissionMode, images) {
|
|
895
|
+
const entry = this.activeSessions.get(sessionId);
|
|
896
|
+
if (!entry) {
|
|
897
|
+
throw new Error(`Session ${sessionId} not found or already ended`);
|
|
898
|
+
}
|
|
899
|
+
const modeChanged = permissionMode != null && permissionMode !== (entry.permissionMode ?? "default");
|
|
900
|
+
if (!modeChanged && entry.process.exitCode === null && entry.process.signalCode === null && !entry.process.stdin?.destroyed) {
|
|
901
|
+
entry.session.status = "running";
|
|
902
|
+
entry.session.lastActiveAt = Date.now();
|
|
903
|
+
this.writeUserMessage(entry.process, message, sessionId, images);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
if (modeChanged) {
|
|
907
|
+
console.log(`[ProcessProvider] Session ${sessionId}: permission mode change ${entry.permissionMode ?? "default"} \u2192 ${permissionMode}, respawn`);
|
|
908
|
+
if (entry.process.exitCode === null && entry.process.signalCode === null) {
|
|
909
|
+
try {
|
|
910
|
+
entry.process.stdin?.end();
|
|
911
|
+
} catch {
|
|
912
|
+
}
|
|
913
|
+
killProcessCrossPlatform(entry.process);
|
|
914
|
+
}
|
|
915
|
+
} else {
|
|
916
|
+
console.log(`[ProcessProvider] Session ${sessionId}: process exited, respawning`);
|
|
917
|
+
}
|
|
918
|
+
const newMode = permissionMode ?? entry.permissionMode;
|
|
919
|
+
const proc = this.spawnClaudeProcess(sessionId, entry.session.projectPath, true, entry.model, newMode, entry.effort, entry.fallbackModel, entry.maxBudgetUsd);
|
|
920
|
+
this.writeUserMessage(proc, message, sessionId, images);
|
|
921
|
+
entry.session.status = "running";
|
|
922
|
+
entry.session.lastActiveAt = Date.now();
|
|
923
|
+
entry.session.pid = proc.pid;
|
|
924
|
+
entry.process = proc;
|
|
925
|
+
entry.permissionMode = newMode;
|
|
926
|
+
proc.on("error", (err) => {
|
|
927
|
+
console.error(`[ProcessProvider] Session ${sessionId} sendMessage process error:`, err.message);
|
|
928
|
+
this.activeSessions.delete(sessionId);
|
|
929
|
+
const syntheticResult = {
|
|
930
|
+
type: "result",
|
|
931
|
+
subtype: "error",
|
|
932
|
+
result: `Failed to send message: ${err.message}`,
|
|
933
|
+
session_id: sessionId,
|
|
934
|
+
duration_ms: 0,
|
|
935
|
+
is_error: true,
|
|
936
|
+
num_turns: 0
|
|
937
|
+
};
|
|
938
|
+
this.emitter.emit(this.getEventName(sessionId), syntheticResult);
|
|
939
|
+
});
|
|
940
|
+
this.attachStdoutListener(sessionId, proc);
|
|
941
|
+
this.attachStderrListener(sessionId, proc);
|
|
942
|
+
this.attachExitListener(sessionId, proc);
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* 订阅指定会话的 Claude 事件流
|
|
946
|
+
*
|
|
947
|
+
* @returns 取消订阅函数
|
|
948
|
+
*/
|
|
533
949
|
onEvent(sessionId, callback) {
|
|
534
950
|
const eventName = this.getEventName(sessionId);
|
|
535
951
|
this.emitter.on(eventName, callback);
|
|
@@ -743,7 +1159,7 @@ var ProcessProvider = class {
|
|
|
743
1159
|
console.warn(`[ProcessProvider] Session ${sessionId}: stdout unavailable`);
|
|
744
1160
|
return;
|
|
745
1161
|
}
|
|
746
|
-
const rl = (0,
|
|
1162
|
+
const rl = (0, import_readline2.createInterface)({
|
|
747
1163
|
input: proc.stdout,
|
|
748
1164
|
crlfDelay: Infinity
|
|
749
1165
|
});
|
|
@@ -757,56 +1173,6 @@ var ProcessProvider = class {
|
|
|
757
1173
|
const result = this.parseLine(trimmed);
|
|
758
1174
|
if (result.ok) {
|
|
759
1175
|
const event = result.value;
|
|
760
|
-
if (event.type === "assistant") {
|
|
761
|
-
for (const block of event.message.content) {
|
|
762
|
-
if (block.type === "tool_use") {
|
|
763
|
-
const isQuestion = block.name === "AskUserQuestion" || block.name === "AskFollowupQuestion";
|
|
764
|
-
if (!isQuestion) continue;
|
|
765
|
-
const input = block.input;
|
|
766
|
-
let question = "";
|
|
767
|
-
let options;
|
|
768
|
-
let questions;
|
|
769
|
-
if (typeof input.question === "string") {
|
|
770
|
-
question = input.question;
|
|
771
|
-
options = Array.isArray(input.options) ? input.options : void 0;
|
|
772
|
-
} else if (Array.isArray(input.questions) && input.questions.length > 0) {
|
|
773
|
-
questions = input.questions.map((q) => {
|
|
774
|
-
const item = {
|
|
775
|
-
question: typeof q.question === "string" ? q.question : "",
|
|
776
|
-
header: typeof q.header === "string" ? q.header : void 0,
|
|
777
|
-
multiSelect: typeof q.multiSelect === "boolean" ? q.multiSelect : void 0
|
|
778
|
-
};
|
|
779
|
-
if (Array.isArray(q.options)) {
|
|
780
|
-
item.options = q.options.map((o) => ({
|
|
781
|
-
label: typeof o.label === "string" ? o.label : String(o),
|
|
782
|
-
description: typeof o.description === "string" ? o.description : void 0
|
|
783
|
-
}));
|
|
784
|
-
}
|
|
785
|
-
return item;
|
|
786
|
-
});
|
|
787
|
-
const first = questions[0];
|
|
788
|
-
question = first.question;
|
|
789
|
-
options = first.options?.map((o) => o.label);
|
|
790
|
-
}
|
|
791
|
-
if (!question) continue;
|
|
792
|
-
const prevKey = `${block.id}:${question}:${JSON.stringify(options ?? [])}`;
|
|
793
|
-
let sessionSet = this.emittedQuestionToolUseIds.get(sessionId);
|
|
794
|
-
if (!sessionSet) {
|
|
795
|
-
sessionSet = /* @__PURE__ */ new Set();
|
|
796
|
-
this.emittedQuestionToolUseIds.set(sessionId, sessionSet);
|
|
797
|
-
}
|
|
798
|
-
if (sessionSet.has(prevKey)) continue;
|
|
799
|
-
sessionSet.add(prevKey);
|
|
800
|
-
console.log(`[ProcessProvider] Session ${sessionId}: detected ${block.name} (toolUseId=${block.id})`);
|
|
801
|
-
this.emitter.emit(this.getQuestionEventName(sessionId), {
|
|
802
|
-
toolUseId: block.id,
|
|
803
|
-
question,
|
|
804
|
-
options,
|
|
805
|
-
questions
|
|
806
|
-
});
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
1176
|
this.updateSessionStatus(sessionId, event);
|
|
811
1177
|
this.emitter.emit(this.getEventName(sessionId), event);
|
|
812
1178
|
} else {
|
|
@@ -937,70 +1303,21 @@ ${context}`;
|
|
|
937
1303
|
proc.once("error", reject);
|
|
938
1304
|
});
|
|
939
1305
|
}
|
|
940
|
-
/**
|
|
941
|
-
* 向正在等待中的 AskUserQuestion 提供答案
|
|
942
|
-
*
|
|
943
|
-
* 将答案写入 Claude 进程的 stdin(作为 tool_result),
|
|
944
|
-
* Claude 收到后继续执行。
|
|
945
|
-
*/
|
|
946
|
-
async answerQuestion(sessionId, toolUseId, answer) {
|
|
947
|
-
const entry = this.activeSessions.get(sessionId);
|
|
948
|
-
if (!entry) {
|
|
949
|
-
throw new Error(`Session ${sessionId} not found`);
|
|
950
|
-
}
|
|
951
|
-
if (!entry.process.stdin || entry.process.stdin.destroyed) {
|
|
952
|
-
throw new Error(`Session ${sessionId} stdin unavailable`);
|
|
953
|
-
}
|
|
954
|
-
const toolResult = JSON.stringify({
|
|
955
|
-
type: "user",
|
|
956
|
-
session_id: "",
|
|
957
|
-
message: {
|
|
958
|
-
role: "user",
|
|
959
|
-
content: [{ type: "tool_result", tool_use_id: toolUseId, content: answer }]
|
|
960
|
-
},
|
|
961
|
-
parent_tool_use_id: toolUseId
|
|
962
|
-
});
|
|
963
|
-
await new Promise((resolve, reject) => {
|
|
964
|
-
entry.process.stdin.write(toolResult + "\n", (err) => {
|
|
965
|
-
if (err) reject(err);
|
|
966
|
-
else resolve();
|
|
967
|
-
});
|
|
968
|
-
});
|
|
969
|
-
console.log(`[ProcessProvider] Session ${sessionId}: AskUserQuestion answered (toolUseId=${toolUseId})`);
|
|
970
|
-
}
|
|
971
|
-
/**
|
|
972
|
-
* 订阅指定会话的 AskUserQuestion 事件
|
|
973
|
-
*
|
|
974
|
-
* @returns 取消订阅函数
|
|
975
|
-
*/
|
|
976
|
-
onQuestion(sessionId, callback) {
|
|
977
|
-
const eventName = this.getQuestionEventName(sessionId);
|
|
978
|
-
this.emitter.on(eventName, callback);
|
|
979
|
-
return () => {
|
|
980
|
-
this.emitter.off(eventName, callback);
|
|
981
|
-
};
|
|
982
|
-
}
|
|
983
1306
|
/**
|
|
984
1307
|
* 生成事件名称
|
|
985
1308
|
*/
|
|
986
1309
|
getEventName(sessionId) {
|
|
987
1310
|
return `claude:${sessionId}`;
|
|
988
1311
|
}
|
|
989
|
-
/**
|
|
990
|
-
* 生成 AskUserQuestion 内部事件名称
|
|
991
|
-
*/
|
|
992
|
-
getQuestionEventName(sessionId) {
|
|
993
|
-
return `question:${sessionId}`;
|
|
994
|
-
}
|
|
995
1312
|
};
|
|
996
1313
|
|
|
997
1314
|
// src/providers/CodexProvider.ts
|
|
998
1315
|
var import_child_process2 = require("child_process");
|
|
999
|
-
var
|
|
1316
|
+
var import_readline3 = require("readline");
|
|
1000
1317
|
var import_events2 = require("events");
|
|
1001
1318
|
var import_fs = require("fs");
|
|
1002
|
-
var
|
|
1003
|
-
var
|
|
1319
|
+
var import_path2 = require("path");
|
|
1320
|
+
var import_os2 = require("os");
|
|
1004
1321
|
var import_uuid2 = require("uuid");
|
|
1005
1322
|
|
|
1006
1323
|
// src/utils/codexPath.ts
|
|
@@ -1079,9 +1396,9 @@ function isCodexAvailable() {
|
|
|
1079
1396
|
}
|
|
1080
1397
|
|
|
1081
1398
|
// src/providers/CodexProvider.ts
|
|
1082
|
-
var SESSIX_DIR = (0,
|
|
1083
|
-
var CODEX_SESSIONS_FILE = (0,
|
|
1084
|
-
var CODEX_EVENTS_DIR = (0,
|
|
1399
|
+
var SESSIX_DIR = (0, import_path2.join)((0, import_os2.homedir)(), ".sessix");
|
|
1400
|
+
var CODEX_SESSIONS_FILE = (0, import_path2.join)(SESSIX_DIR, "codex-sessions.json");
|
|
1401
|
+
var CODEX_EVENTS_DIR = (0, import_path2.join)(SESSIX_DIR, "codex-events");
|
|
1085
1402
|
var SESSION_EXPIRY_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
1086
1403
|
var CodexProvider = class {
|
|
1087
1404
|
activeSessions = /* @__PURE__ */ new Map();
|
|
@@ -1228,12 +1545,6 @@ var CodexProvider = class {
|
|
|
1228
1545
|
async generateSuggestion(_context) {
|
|
1229
1546
|
return "";
|
|
1230
1547
|
}
|
|
1231
|
-
async answerQuestion(_sessionId, _toolUseId, _answer) {
|
|
1232
|
-
}
|
|
1233
|
-
onQuestion(_sessionId, _callback) {
|
|
1234
|
-
return () => {
|
|
1235
|
-
};
|
|
1236
|
-
}
|
|
1237
1548
|
// ============================================
|
|
1238
1549
|
// 私有方法
|
|
1239
1550
|
// ============================================
|
|
@@ -1285,7 +1596,7 @@ var CodexProvider = class {
|
|
|
1285
1596
|
*/
|
|
1286
1597
|
attachStdoutListener(sessionId, proc) {
|
|
1287
1598
|
if (!proc.stdout) return;
|
|
1288
|
-
const rl = (0,
|
|
1599
|
+
const rl = (0, import_readline3.createInterface)({ input: proc.stdout, crlfDelay: Infinity });
|
|
1289
1600
|
const entry = this.activeSessions.get(sessionId);
|
|
1290
1601
|
if (entry) entry.rl = rl;
|
|
1291
1602
|
rl.on("line", (line) => {
|
|
@@ -1567,9 +1878,9 @@ var CodexProvider = class {
|
|
|
1567
1878
|
* 优先从内存读,miss 时从磁盘加载
|
|
1568
1879
|
*/
|
|
1569
1880
|
getSessionHistory(sessionId) {
|
|
1570
|
-
const
|
|
1571
|
-
if (
|
|
1572
|
-
const filePath = (0,
|
|
1881
|
+
const cached2 = this.sessionEvents.get(sessionId);
|
|
1882
|
+
if (cached2 && cached2.length > 0) return cached2;
|
|
1883
|
+
const filePath = (0, import_path2.join)(CODEX_EVENTS_DIR, `${sessionId}.json`);
|
|
1573
1884
|
try {
|
|
1574
1885
|
if (!(0, import_fs.existsSync)(filePath)) return [];
|
|
1575
1886
|
const data = JSON.parse((0, import_fs.readFileSync)(filePath, "utf-8"));
|
|
@@ -1590,7 +1901,7 @@ var CodexProvider = class {
|
|
|
1590
1901
|
(0, import_fs.mkdirSync)(CODEX_EVENTS_DIR, { recursive: true });
|
|
1591
1902
|
}
|
|
1592
1903
|
(0, import_fs.writeFileSync)(
|
|
1593
|
-
(0,
|
|
1904
|
+
(0, import_path2.join)(CODEX_EVENTS_DIR, `${sessionId}.json`),
|
|
1594
1905
|
JSON.stringify(events),
|
|
1595
1906
|
"utf-8"
|
|
1596
1907
|
);
|
|
@@ -1615,7 +1926,7 @@ var CodexProvider = class {
|
|
|
1615
1926
|
if (now - m.lastActiveAt > SESSION_EXPIRY_MS) {
|
|
1616
1927
|
expiredCount++;
|
|
1617
1928
|
try {
|
|
1618
|
-
const eventsFile = (0,
|
|
1929
|
+
const eventsFile = (0, import_path2.join)(CODEX_EVENTS_DIR, `${sessionId}.json`);
|
|
1619
1930
|
if ((0, import_fs.existsSync)(eventsFile)) (0, import_fs.unlinkSync)(eventsFile);
|
|
1620
1931
|
} catch {
|
|
1621
1932
|
}
|
|
@@ -1728,7 +2039,6 @@ var ProviderFactory = class {
|
|
|
1728
2039
|
};
|
|
1729
2040
|
|
|
1730
2041
|
// src/session/SessionManager.ts
|
|
1731
|
-
var import_uuid3 = require("uuid");
|
|
1732
2042
|
var BUFFER_MAX = 5e3;
|
|
1733
2043
|
var SessionManager = class {
|
|
1734
2044
|
provider;
|
|
@@ -1907,6 +2217,21 @@ var SessionManager = class {
|
|
|
1907
2217
|
this.updateSessionStatus(sessionId, "running");
|
|
1908
2218
|
}
|
|
1909
2219
|
}
|
|
2220
|
+
/**
|
|
2221
|
+
* 幂等清理单个待回答问题(由 ApprovalProxy onQuestionResolved 触发:
|
|
2222
|
+
* 答案到达 / 325s 超时 / 会话 kill / 服务关闭)。
|
|
2223
|
+
* 已不存在则静默返回(不打 warn,与 handleQuestionResponse 区分)。
|
|
2224
|
+
*/
|
|
2225
|
+
clearPendingQuestion(requestId) {
|
|
2226
|
+
const pending = this.pendingQuestions.get(requestId);
|
|
2227
|
+
if (!pending) return;
|
|
2228
|
+
const { sessionId } = pending;
|
|
2229
|
+
this.pendingQuestions.delete(requestId);
|
|
2230
|
+
pending.resolve("");
|
|
2231
|
+
if (!this.hasPendingQuestionsForSession(sessionId)) {
|
|
2232
|
+
this.updateSessionStatus(sessionId, "running");
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
1910
2235
|
/**
|
|
1911
2236
|
* 获取指定会话的所有待回答问题(用于重连时恢复)
|
|
1912
2237
|
*/
|
|
@@ -2048,6 +2373,9 @@ var SessionManager = class {
|
|
|
2048
2373
|
clearTimeout(pending.timer);
|
|
2049
2374
|
}
|
|
2050
2375
|
this.pendingAssistantEvents.clear();
|
|
2376
|
+
for (const pending of this.pendingQuestions.values()) {
|
|
2377
|
+
pending.resolve("");
|
|
2378
|
+
}
|
|
2051
2379
|
this.pendingQuestions.clear();
|
|
2052
2380
|
this.lastBroadcastStatus.clear();
|
|
2053
2381
|
this.eventCallbacks.length = 0;
|
|
@@ -2057,22 +2385,15 @@ var SessionManager = class {
|
|
|
2057
2385
|
// 内部方法
|
|
2058
2386
|
// ============================================
|
|
2059
2387
|
/**
|
|
2060
|
-
*
|
|
2388
|
+
* 订阅指定会话的事件流(AskUserQuestion 已改由 ApprovalProxy hook 驱动)
|
|
2061
2389
|
*/
|
|
2062
2390
|
subscribeToSession(sessionId) {
|
|
2063
2391
|
const provider = this.getProviderForSession(sessionId);
|
|
2064
2392
|
const unsubscribeEvent = provider.onEvent(sessionId, (event) => {
|
|
2065
2393
|
this.handleClaudeEvent(sessionId, event);
|
|
2066
2394
|
});
|
|
2067
|
-
const unsubscribeQuestion = provider.onQuestion(
|
|
2068
|
-
sessionId,
|
|
2069
|
-
({ toolUseId, question, options, questions }) => {
|
|
2070
|
-
this.handleAskUserQuestion(sessionId, toolUseId, question, options, questions);
|
|
2071
|
-
}
|
|
2072
|
-
);
|
|
2073
2395
|
this.unsubscribeMap.set(sessionId, () => {
|
|
2074
2396
|
unsubscribeEvent();
|
|
2075
|
-
unsubscribeQuestion();
|
|
2076
2397
|
});
|
|
2077
2398
|
}
|
|
2078
2399
|
/**
|
|
@@ -2221,55 +2542,34 @@ var SessionManager = class {
|
|
|
2221
2542
|
return runningStartedAt ? { ...base, runningStartedAt } : base;
|
|
2222
2543
|
}
|
|
2223
2544
|
/**
|
|
2224
|
-
*
|
|
2545
|
+
* 由 ApprovalProxy 在 PreToolUse hook 拦截到 AskUserQuestion 时调用。
|
|
2546
|
+
* 登记 pendingQuestion、广播 question_request、置 waiting_question,
|
|
2547
|
+
* 返回的 Promise 在 handleQuestionResponse 时 resolve。
|
|
2225
2548
|
*/
|
|
2226
|
-
|
|
2227
|
-
const existingEntry = Array.from(this.pendingQuestions.entries()).find(
|
|
2228
|
-
([, v]) => v.toolUseId === toolUseId
|
|
2229
|
-
);
|
|
2230
|
-
if (existingEntry) {
|
|
2231
|
-
const [existingRequestId, existingPending] = existingEntry;
|
|
2232
|
-
existingPending.question = question;
|
|
2233
|
-
existingPending.options = options;
|
|
2234
|
-
existingPending.questions = questions;
|
|
2235
|
-
existingPending.createdAt = Date.now();
|
|
2236
|
-
const updatedRequest = {
|
|
2237
|
-
id: existingRequestId,
|
|
2238
|
-
sessionId,
|
|
2239
|
-
toolUseId,
|
|
2240
|
-
question,
|
|
2241
|
-
options,
|
|
2242
|
-
questions,
|
|
2243
|
-
createdAt: existingPending.createdAt
|
|
2244
|
-
};
|
|
2245
|
-
this.emit({ type: "question_request", request: updatedRequest });
|
|
2246
|
-
console.log(`[SessionManager] Session ${sessionId}: AskUserQuestion updated (requestId=${existingRequestId})`);
|
|
2247
|
-
return;
|
|
2248
|
-
}
|
|
2249
|
-
const requestId = (0, import_uuid3.v4)();
|
|
2549
|
+
askQuestion(sessionId, toolUseId, questions, requestId) {
|
|
2250
2550
|
const request = {
|
|
2251
2551
|
id: requestId,
|
|
2252
2552
|
sessionId,
|
|
2253
2553
|
toolUseId,
|
|
2254
|
-
question,
|
|
2255
|
-
options,
|
|
2554
|
+
question: questions[0]?.question ?? "",
|
|
2555
|
+
options: questions[0]?.options?.map((o) => o.label),
|
|
2256
2556
|
questions,
|
|
2257
2557
|
createdAt: Date.now()
|
|
2258
2558
|
};
|
|
2259
2559
|
this.updateSessionStatus(sessionId, "waiting_question");
|
|
2260
2560
|
this.emit({ type: "question_request", request });
|
|
2261
|
-
const answerPromise = new Promise((resolve) => {
|
|
2262
|
-
this.pendingQuestions.set(requestId, { sessionId, toolUseId, question, options, questions, createdAt: request.createdAt, resolve });
|
|
2263
|
-
});
|
|
2264
|
-
answerPromise.then(async (answer) => {
|
|
2265
|
-
try {
|
|
2266
|
-
const provider = this.getProviderForSession(sessionId);
|
|
2267
|
-
await provider.answerQuestion(sessionId, toolUseId, answer);
|
|
2268
|
-
} catch (err) {
|
|
2269
|
-
console.error(`[SessionManager] answerQuestion failed (${sessionId}):`, err);
|
|
2270
|
-
}
|
|
2271
|
-
}).catch((err) => console.error("[SessionManager] answerPromise rejected:", err));
|
|
2272
2561
|
console.log(`[SessionManager] Session ${sessionId}: AskUserQuestion pushed (requestId=${requestId})`);
|
|
2562
|
+
return new Promise((resolve) => {
|
|
2563
|
+
this.pendingQuestions.set(requestId, {
|
|
2564
|
+
sessionId,
|
|
2565
|
+
toolUseId,
|
|
2566
|
+
question: request.question,
|
|
2567
|
+
options: request.options,
|
|
2568
|
+
questions,
|
|
2569
|
+
createdAt: request.createdAt,
|
|
2570
|
+
resolve
|
|
2571
|
+
});
|
|
2572
|
+
});
|
|
2273
2573
|
}
|
|
2274
2574
|
/**
|
|
2275
2575
|
* 清除指定会话的所有待回答问题
|
|
@@ -2282,6 +2582,7 @@ var SessionManager = class {
|
|
|
2282
2582
|
}
|
|
2283
2583
|
}
|
|
2284
2584
|
for (const requestId of toRemove) {
|
|
2585
|
+
this.pendingQuestions.get(requestId)?.resolve("");
|
|
2285
2586
|
this.pendingQuestions.delete(requestId);
|
|
2286
2587
|
}
|
|
2287
2588
|
}
|
|
@@ -2301,7 +2602,7 @@ var SessionManager = class {
|
|
|
2301
2602
|
|
|
2302
2603
|
// src/session/SessionFileWatcher.ts
|
|
2303
2604
|
var import_chokidar = __toESM(require("chokidar"));
|
|
2304
|
-
var
|
|
2605
|
+
var import_promises2 = require("fs/promises");
|
|
2305
2606
|
var import_node_readline = require("readline");
|
|
2306
2607
|
var SessionFileWatcher = class {
|
|
2307
2608
|
watchers = /* @__PURE__ */ new Map();
|
|
@@ -2380,7 +2681,7 @@ var SessionFileWatcher = class {
|
|
|
2380
2681
|
let fileHandle;
|
|
2381
2682
|
let rl;
|
|
2382
2683
|
try {
|
|
2383
|
-
fileHandle = await (0,
|
|
2684
|
+
fileHandle = await (0, import_promises2.open)(entry.filePath, "r");
|
|
2384
2685
|
const fileStat = await fileHandle.stat();
|
|
2385
2686
|
const newSize = fileStat.size;
|
|
2386
2687
|
if (newSize <= entry.byteOffset) return;
|
|
@@ -2690,7 +2991,7 @@ var import_node_http = __toESM(require("http"));
|
|
|
2690
2991
|
var import_node_fs3 = __toESM(require("fs"));
|
|
2691
2992
|
var import_node_path3 = __toESM(require("path"));
|
|
2692
2993
|
var import_node_os4 = __toESM(require("os"));
|
|
2693
|
-
var
|
|
2994
|
+
var import_uuid3 = require("uuid");
|
|
2694
2995
|
var ApprovalProxy = class _ApprovalProxy {
|
|
2695
2996
|
server;
|
|
2696
2997
|
token;
|
|
@@ -2698,10 +2999,16 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2698
2999
|
settingsPath = import_node_path3.default.join(import_node_os4.default.homedir(), ".claude", "settings.json");
|
|
2699
3000
|
/** 待处理的审批请求:requestId -> { resolve, timer, request } */
|
|
2700
3001
|
pendingApprovals = /* @__PURE__ */ new Map();
|
|
3002
|
+
/** 待回答的 AskUserQuestion:requestId -> { resolve, timer, request } */
|
|
3003
|
+
pendingQuestions = /* @__PURE__ */ new Map();
|
|
3004
|
+
/** 由外部注入:把问题推给手机并等待答案(返回用户答案文本) */
|
|
3005
|
+
questionHandler = null;
|
|
2701
3006
|
/** 审批请求回调(通知外部推送到手机) */
|
|
2702
3007
|
approvalRequestCallbacks = [];
|
|
2703
3008
|
/** 审批 resolve 回调(任何来源的 resolve 都会触发,用于 WS 广播清理 */
|
|
2704
3009
|
approvalResolvedCallbacks = [];
|
|
3010
|
+
/** 问题 resolve 回调(任何来源的 resolve 都会触发,用于 SessionManager 清理) */
|
|
3011
|
+
questionResolvedCallbacks = [];
|
|
2705
3012
|
/** Hook 通知回调(PreCompact / PermissionDenied / 等非阻塞 hook) */
|
|
2706
3013
|
notifyCallbacks = [];
|
|
2707
3014
|
/** YOLO 模式状态:sessionId -> enabled */
|
|
@@ -2765,6 +3072,30 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2765
3072
|
}
|
|
2766
3073
|
}
|
|
2767
3074
|
}
|
|
3075
|
+
/**
|
|
3076
|
+
* 注册问题 resolve 回调
|
|
3077
|
+
*
|
|
3078
|
+
* 任何来源的 resolve 都会触发:
|
|
3079
|
+
* - resolveQuestion(手机端答案到达)
|
|
3080
|
+
* - 325s 超时自动空答案
|
|
3081
|
+
* - clearPendingQuestionsForSession(会话被 kill)
|
|
3082
|
+
* - close()(服务关闭)
|
|
3083
|
+
*
|
|
3084
|
+
* 用于通知 SessionManager 清理 pendingQuestions,避免会话卡在 waiting_question。
|
|
3085
|
+
*/
|
|
3086
|
+
onQuestionResolved(callback) {
|
|
3087
|
+
this.questionResolvedCallbacks.push(callback);
|
|
3088
|
+
}
|
|
3089
|
+
/** 通知所有问题 resolve 回调(内部调用) */
|
|
3090
|
+
notifyQuestionResolved(requestId) {
|
|
3091
|
+
for (const cb of this.questionResolvedCallbacks) {
|
|
3092
|
+
try {
|
|
3093
|
+
cb(requestId);
|
|
3094
|
+
} catch (err) {
|
|
3095
|
+
console.error("[ApprovalProxy] question resolved callback error:", err);
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
2768
3099
|
/**
|
|
2769
3100
|
* 注册非阻塞 hook 通知回调(如 PreCompact、PermissionDenied)
|
|
2770
3101
|
*
|
|
@@ -2819,6 +3150,42 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2819
3150
|
this.notifyApprovalResolved(requestId, decision);
|
|
2820
3151
|
return true;
|
|
2821
3152
|
}
|
|
3153
|
+
/** 注入问题处理器(server.ts 接到 SessionManager.askQuestion) */
|
|
3154
|
+
setQuestionHandler(handler) {
|
|
3155
|
+
this.questionHandler = handler;
|
|
3156
|
+
}
|
|
3157
|
+
/** 解析一个待回答问题(手机端答案到达时由 server.ts 调用) */
|
|
3158
|
+
resolveQuestion(requestId, answer) {
|
|
3159
|
+
const pending = this.pendingQuestions.get(requestId);
|
|
3160
|
+
if (!pending) {
|
|
3161
|
+
console.warn(`[ApprovalProxy] Question request not found: ${requestId}`);
|
|
3162
|
+
return false;
|
|
3163
|
+
}
|
|
3164
|
+
clearTimeout(pending.timer);
|
|
3165
|
+
pending.resolve(answer);
|
|
3166
|
+
this.pendingQuestions.delete(requestId);
|
|
3167
|
+
console.log(`[ApprovalProxy] Question answered: ${requestId}`);
|
|
3168
|
+
this.notifyQuestionResolved(requestId);
|
|
3169
|
+
return true;
|
|
3170
|
+
}
|
|
3171
|
+
/** 清理会话的待回答问题(会话被 kill 时,给空答案让 hook 不再阻塞) */
|
|
3172
|
+
clearPendingQuestionsForSession(sessionId) {
|
|
3173
|
+
const toRemove = [];
|
|
3174
|
+
for (const [requestId, pending] of this.pendingQuestions) {
|
|
3175
|
+
if (pending.request.sessionId === sessionId) {
|
|
3176
|
+
toRemove.push(requestId);
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
for (const requestId of toRemove) {
|
|
3180
|
+
const pending = this.pendingQuestions.get(requestId);
|
|
3181
|
+
if (!pending) continue;
|
|
3182
|
+
clearTimeout(pending.timer);
|
|
3183
|
+
pending.resolve("");
|
|
3184
|
+
this.pendingQuestions.delete(requestId);
|
|
3185
|
+
console.log(`[ApprovalProxy] Session ${sessionId} killed, cleared pending question ${requestId}`);
|
|
3186
|
+
this.notifyQuestionResolved(requestId);
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
2822
3189
|
/** 获取当前待处理的审批数量 */
|
|
2823
3190
|
getPendingCount() {
|
|
2824
3191
|
return this.pendingApprovals.size;
|
|
@@ -2940,6 +3307,13 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2940
3307
|
pending.resolve({ decision: "deny", reason: t("approval.serverClosed") });
|
|
2941
3308
|
}
|
|
2942
3309
|
this.pendingApprovals.clear();
|
|
3310
|
+
const pendingQuestionEntries = Array.from(this.pendingQuestions.entries());
|
|
3311
|
+
for (const [requestId, pending] of pendingQuestionEntries) {
|
|
3312
|
+
clearTimeout(pending.timer);
|
|
3313
|
+
pending.resolve("");
|
|
3314
|
+
this.notifyQuestionResolved(requestId);
|
|
3315
|
+
}
|
|
3316
|
+
this.pendingQuestions.clear();
|
|
2943
3317
|
this.server.close((err) => {
|
|
2944
3318
|
if (err) {
|
|
2945
3319
|
reject(err);
|
|
@@ -2995,7 +3369,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2995
3369
|
try {
|
|
2996
3370
|
const body = await this.parseJsonBody(req);
|
|
2997
3371
|
const payload = body.payload ?? body;
|
|
2998
|
-
const requestId = (0,
|
|
3372
|
+
const requestId = (0, import_uuid3.v4)();
|
|
2999
3373
|
const projectPath = String(body.projectPath ?? "unknown");
|
|
3000
3374
|
const toolName = String(payload.tool_name ?? body.tool_name ?? "unknown");
|
|
3001
3375
|
const toolInput = payload.tool_input ?? body.tool_input ?? {};
|
|
@@ -3009,6 +3383,51 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
3009
3383
|
createdAt: Date.now()
|
|
3010
3384
|
};
|
|
3011
3385
|
console.log(`[ApprovalProxy] ${t("approval.received")}: ${requestId} (${approvalRequest.toolName})`);
|
|
3386
|
+
if ((approvalRequest.toolName === "AskUserQuestion" || approvalRequest.toolName === "AskFollowupQuestion") && this.questionHandler) {
|
|
3387
|
+
const questions = parseQuestionsFromInput(toolInput);
|
|
3388
|
+
if (questions.length === 0) {
|
|
3389
|
+
this.sendJson(res, 200, { decision: "allow" });
|
|
3390
|
+
return;
|
|
3391
|
+
}
|
|
3392
|
+
const toolUseId = String(
|
|
3393
|
+
payload.tool_use_id ?? body.tool_use_id ?? ""
|
|
3394
|
+
);
|
|
3395
|
+
const qRequest = {
|
|
3396
|
+
id: requestId,
|
|
3397
|
+
sessionId: approvalRequest.sessionId,
|
|
3398
|
+
toolUseId,
|
|
3399
|
+
question: questions[0].question,
|
|
3400
|
+
options: questions[0].options?.map((o) => o.label),
|
|
3401
|
+
questions,
|
|
3402
|
+
createdAt: Date.now()
|
|
3403
|
+
};
|
|
3404
|
+
const answer = await new Promise((resolve) => {
|
|
3405
|
+
const timer = setTimeout(() => {
|
|
3406
|
+
this.pendingQuestions.delete(requestId);
|
|
3407
|
+
console.log(`[ApprovalProxy] Question timeout: ${requestId}`);
|
|
3408
|
+
resolve("");
|
|
3409
|
+
this.notifyQuestionResolved(requestId);
|
|
3410
|
+
}, 325e3);
|
|
3411
|
+
this.pendingQuestions.set(requestId, { resolve, timer, request: qRequest });
|
|
3412
|
+
this.questionHandler(qRequest.sessionId, toolUseId, questions, requestId).then((ans) => {
|
|
3413
|
+
if (ans && this.pendingQuestions.has(requestId)) this.resolveQuestion(requestId, ans);
|
|
3414
|
+
}).catch((err) => console.error("[ApprovalProxy] questionHandler error:", err));
|
|
3415
|
+
});
|
|
3416
|
+
if (!answer) {
|
|
3417
|
+
this.sendJson(res, 200, {
|
|
3418
|
+
decision: "deny",
|
|
3419
|
+
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",
|
|
3420
|
+
systemMessage: "\u7528\u6237\u672A\u56DE\u7B54 AskUserQuestion\uFF1B\u8BF7\u52FF\u91CD\u8BD5\u8BE5\u5DE5\u5177\uFF0C\u81EA\u884C\u51B3\u7B56\u7EE7\u7EED\u3002"
|
|
3421
|
+
});
|
|
3422
|
+
return;
|
|
3423
|
+
}
|
|
3424
|
+
this.sendJson(res, 200, {
|
|
3425
|
+
decision: "deny",
|
|
3426
|
+
reason: formatQuestionAnswer(questions, answer),
|
|
3427
|
+
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"
|
|
3428
|
+
});
|
|
3429
|
+
return;
|
|
3430
|
+
}
|
|
3012
3431
|
if (this.isToolAlwaysAllowed(approvalRequest.toolName, projectPath !== "unknown" ? projectPath : void 0)) {
|
|
3013
3432
|
console.log(`[ApprovalProxy] ${t("approval.alwaysAllowPassThrough", { tool: approvalRequest.toolName })}`);
|
|
3014
3433
|
this.sendJson(res, 200, { decision: "allow" });
|
|
@@ -3198,6 +3617,49 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
3198
3617
|
res.end(body);
|
|
3199
3618
|
}
|
|
3200
3619
|
};
|
|
3620
|
+
function parseQuestionsFromInput(input) {
|
|
3621
|
+
if (Array.isArray(input.questions) && input.questions.length > 0) {
|
|
3622
|
+
return input.questions.map((q) => {
|
|
3623
|
+
const item = {
|
|
3624
|
+
question: typeof q.question === "string" ? q.question : "",
|
|
3625
|
+
header: typeof q.header === "string" ? q.header : void 0,
|
|
3626
|
+
multiSelect: typeof q.multiSelect === "boolean" ? q.multiSelect : void 0
|
|
3627
|
+
};
|
|
3628
|
+
if (Array.isArray(q.options)) {
|
|
3629
|
+
item.options = q.options.map((o) => ({
|
|
3630
|
+
label: typeof o.label === "string" ? o.label : "",
|
|
3631
|
+
description: typeof o.description === "string" ? o.description : void 0
|
|
3632
|
+
}));
|
|
3633
|
+
}
|
|
3634
|
+
return item;
|
|
3635
|
+
});
|
|
3636
|
+
}
|
|
3637
|
+
if (typeof input.question === "string") {
|
|
3638
|
+
const opts = Array.isArray(input.options) ? input.options.map((o) => ({ label: String(o) })) : void 0;
|
|
3639
|
+
return [{ question: input.question, options: opts }];
|
|
3640
|
+
}
|
|
3641
|
+
return [];
|
|
3642
|
+
}
|
|
3643
|
+
function formatQuestionAnswer(questions, raw) {
|
|
3644
|
+
let pairs = [];
|
|
3645
|
+
try {
|
|
3646
|
+
const parsed = JSON.parse(raw);
|
|
3647
|
+
if (parsed && typeof parsed === "object" && parsed.answers && typeof parsed.answers === "object") {
|
|
3648
|
+
const answers = parsed.answers;
|
|
3649
|
+
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) }));
|
|
3650
|
+
}
|
|
3651
|
+
} catch {
|
|
3652
|
+
}
|
|
3653
|
+
if (pairs.length === 0) {
|
|
3654
|
+
pairs = [{ q: questions[0]?.question ?? "\uFF08\u672A\u77E5\u95EE\u9898\uFF09", a: raw }];
|
|
3655
|
+
}
|
|
3656
|
+
const body = pairs.map((p, i) => `${i + 1}. \u95EE\u9898\uFF1A${p.q}
|
|
3657
|
+
\u7528\u6237\u56DE\u7B54\uFF1A${p.a}`).join("\n");
|
|
3658
|
+
return `\u3010\u7528\u6237\u5DF2\u901A\u8FC7 Sessix \u56DE\u7B54\u3011
|
|
3659
|
+
${body}
|
|
3660
|
+
|
|
3661
|
+
\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`;
|
|
3662
|
+
}
|
|
3201
3663
|
|
|
3202
3664
|
// src/mdns/MdnsService.ts
|
|
3203
3665
|
var import_node_child_process5 = require("child_process");
|
|
@@ -3363,7 +3825,7 @@ function getLanAddresses(networkInterfacesFn) {
|
|
|
3363
3825
|
}
|
|
3364
3826
|
|
|
3365
3827
|
// src/hooks/HookInstaller.ts
|
|
3366
|
-
var
|
|
3828
|
+
var import_promises3 = require("fs/promises");
|
|
3367
3829
|
var import_node_path4 = require("path");
|
|
3368
3830
|
var import_node_os6 = require("os");
|
|
3369
3831
|
var SESSIX_HOOKS_DIR = (0, import_node_path4.join)((0, import_node_os6.homedir)(), ".sessix", "hooks");
|
|
@@ -3383,7 +3845,7 @@ var LEGACY_HOOK_COMMANDS = [
|
|
|
3383
3845
|
"~/.sessix/hooks/permission-accept.sh"
|
|
3384
3846
|
];
|
|
3385
3847
|
var HOOK_SCRIPT_TEMPLATE = `#!/usr/bin/env node
|
|
3386
|
-
// Sessix Approval Hook
|
|
3848
|
+
// Sessix Approval Hook v2 (systemMessage passthrough)
|
|
3387
3849
|
// \u4EC5\u5728 Sessix \u7BA1\u7406\u7684\u4F1A\u8BDD\u4E2D\u6FC0\u6D3B
|
|
3388
3850
|
|
|
3389
3851
|
const sessionId = process.env.SESSIX_SESSION_ID
|
|
@@ -3414,6 +3876,9 @@ process.stdin.on('end', async () => {
|
|
|
3414
3876
|
if (decision === 'deny' && data.reason) {
|
|
3415
3877
|
output.hookSpecificOutput.permissionDecisionReason = String(data.reason)
|
|
3416
3878
|
}
|
|
3879
|
+
if (data.systemMessage) {
|
|
3880
|
+
output.systemMessage = String(data.systemMessage)
|
|
3881
|
+
}
|
|
3417
3882
|
process.stdout.write(JSON.stringify(output))
|
|
3418
3883
|
process.exit(0)
|
|
3419
3884
|
} catch {
|
|
@@ -3553,17 +4018,17 @@ var HookInstaller = class {
|
|
|
3553
4018
|
* 4. 更新 Claude Code settings.json 添加 hook 配置
|
|
3554
4019
|
*/
|
|
3555
4020
|
async install() {
|
|
3556
|
-
await (0,
|
|
3557
|
-
await (0,
|
|
3558
|
-
await (0,
|
|
3559
|
-
await (0,
|
|
3560
|
-
await (0,
|
|
3561
|
-
await (0,
|
|
3562
|
-
await (0,
|
|
3563
|
-
await (0,
|
|
3564
|
-
await (0,
|
|
3565
|
-
await (0,
|
|
3566
|
-
await (0,
|
|
4021
|
+
await (0, import_promises3.mkdir)(SESSIX_HOOKS_DIR, { recursive: true });
|
|
4022
|
+
await (0, import_promises3.writeFile)(HOOK_SCRIPT_PATH, HOOK_SCRIPT_TEMPLATE, "utf-8");
|
|
4023
|
+
await (0, import_promises3.writeFile)(PERMISSION_ACCEPT_PATH, PERMISSION_ACCEPT_TEMPLATE, "utf-8");
|
|
4024
|
+
await (0, import_promises3.writeFile)(COMPACT_HOOK_PATH, COMPACT_HOOK_TEMPLATE, "utf-8");
|
|
4025
|
+
await (0, import_promises3.writeFile)(POST_COMPACT_HOOK_PATH, POST_COMPACT_HOOK_TEMPLATE, "utf-8");
|
|
4026
|
+
await (0, import_promises3.writeFile)(PERMISSION_DENIED_HOOK_PATH, PERMISSION_DENIED_HOOK_TEMPLATE, "utf-8");
|
|
4027
|
+
await (0, import_promises3.chmod)(HOOK_SCRIPT_PATH, 493);
|
|
4028
|
+
await (0, import_promises3.chmod)(PERMISSION_ACCEPT_PATH, 493);
|
|
4029
|
+
await (0, import_promises3.chmod)(COMPACT_HOOK_PATH, 493);
|
|
4030
|
+
await (0, import_promises3.chmod)(POST_COMPACT_HOOK_PATH, 493);
|
|
4031
|
+
await (0, import_promises3.chmod)(PERMISSION_DENIED_HOOK_PATH, 493);
|
|
3567
4032
|
await this.addHookToSettings();
|
|
3568
4033
|
console.log("[HookInstaller] Hook installation complete");
|
|
3569
4034
|
}
|
|
@@ -3593,30 +4058,30 @@ var HookInstaller = class {
|
|
|
3593
4058
|
let postCompactScriptExists = false;
|
|
3594
4059
|
let permissionDeniedScriptExists = false;
|
|
3595
4060
|
try {
|
|
3596
|
-
approvalScriptContent = await (0,
|
|
4061
|
+
approvalScriptContent = await (0, import_promises3.readFile)(HOOK_SCRIPT_PATH, "utf-8");
|
|
3597
4062
|
} catch {
|
|
3598
4063
|
}
|
|
3599
4064
|
try {
|
|
3600
|
-
await (0,
|
|
4065
|
+
await (0, import_promises3.access)(PERMISSION_ACCEPT_PATH);
|
|
3601
4066
|
permissionScriptExists = true;
|
|
3602
4067
|
} catch {
|
|
3603
4068
|
}
|
|
3604
4069
|
try {
|
|
3605
|
-
await (0,
|
|
4070
|
+
await (0, import_promises3.access)(COMPACT_HOOK_PATH);
|
|
3606
4071
|
compactScriptExists = true;
|
|
3607
4072
|
} catch {
|
|
3608
4073
|
}
|
|
3609
4074
|
try {
|
|
3610
|
-
await (0,
|
|
4075
|
+
await (0, import_promises3.access)(POST_COMPACT_HOOK_PATH);
|
|
3611
4076
|
postCompactScriptExists = true;
|
|
3612
4077
|
} catch {
|
|
3613
4078
|
}
|
|
3614
4079
|
try {
|
|
3615
|
-
await (0,
|
|
4080
|
+
await (0, import_promises3.access)(PERMISSION_DENIED_HOOK_PATH);
|
|
3616
4081
|
permissionDeniedScriptExists = true;
|
|
3617
4082
|
} catch {
|
|
3618
4083
|
}
|
|
3619
|
-
const isLatestVersion = approvalScriptContent.includes("permissionDecision");
|
|
4084
|
+
const isLatestVersion = approvalScriptContent.includes("permissionDecision") && approvalScriptContent.includes("Sessix Approval Hook v2");
|
|
3620
4085
|
const settings = await this.readClaudeSettings();
|
|
3621
4086
|
const configExists = this.hasHookConfig(settings);
|
|
3622
4087
|
return isLatestVersion && permissionScriptExists && compactScriptExists && postCompactScriptExists && permissionDeniedScriptExists && configExists;
|
|
@@ -3724,7 +4189,7 @@ var HookInstaller = class {
|
|
|
3724
4189
|
*/
|
|
3725
4190
|
async readClaudeSettings() {
|
|
3726
4191
|
try {
|
|
3727
|
-
const content = await (0,
|
|
4192
|
+
const content = await (0, import_promises3.readFile)(CLAUDE_SETTINGS_PATH, "utf-8");
|
|
3728
4193
|
return JSON.parse(content);
|
|
3729
4194
|
} catch {
|
|
3730
4195
|
return {};
|
|
@@ -3734,8 +4199,8 @@ var HookInstaller = class {
|
|
|
3734
4199
|
* 写入 Claude Code settings.json
|
|
3735
4200
|
*/
|
|
3736
4201
|
async writeClaudeSettings(settings) {
|
|
3737
|
-
await (0,
|
|
3738
|
-
await (0,
|
|
4202
|
+
await (0, import_promises3.mkdir)((0, import_node_path4.join)((0, import_node_os6.homedir)(), ".claude"), { recursive: true });
|
|
4203
|
+
await (0, import_promises3.writeFile)(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
3739
4204
|
}
|
|
3740
4205
|
/**
|
|
3741
4206
|
* 检查 settings 中是否已包含所有 Sessix hook 配置
|
|
@@ -3776,7 +4241,7 @@ var HookInstaller = class {
|
|
|
3776
4241
|
var import_node_path5 = require("path");
|
|
3777
4242
|
var RECENT_ACTIVITY_MAX = 6;
|
|
3778
4243
|
var ACTIVITY_PUSH_THROTTLE_MS = 4e3;
|
|
3779
|
-
var NotificationService = class {
|
|
4244
|
+
var NotificationService = class _NotificationService {
|
|
3780
4245
|
constructor(sessionManager, expoChannel = null) {
|
|
3781
4246
|
this.sessionManager = sessionManager;
|
|
3782
4247
|
this.expoChannel = expoChannel;
|
|
@@ -3818,6 +4283,14 @@ var NotificationService = class {
|
|
|
3818
4283
|
* token 注册时启动,flushActivityEnd / removeActivityPushToken 时停止。
|
|
3819
4284
|
*/
|
|
3820
4285
|
laHeartbeatTimers = /* @__PURE__ */ new Map();
|
|
4286
|
+
/**
|
|
4287
|
+
* 上次推送的内容指纹(status + recentActivity + approvalId)。
|
|
4288
|
+
* 只在内容实际变化时发 priority-10 推送;未变化时低频刷新(30s),
|
|
4289
|
+
* 节省 APNs push budget,避免 iOS 节流导致 LA 停滞。
|
|
4290
|
+
*/
|
|
4291
|
+
lastPushedFingerprint = /* @__PURE__ */ new Map();
|
|
4292
|
+
/** 内容未变化时低频刷新间隔(仅刷新 stats/timer,不含内容变化) */
|
|
4293
|
+
static STATS_REFRESH_INTERVAL_MS = 3e4;
|
|
3821
4294
|
/** 添加通知渠道(id 唯一,可用于后续动态开关) */
|
|
3822
4295
|
addChannel(id, channel, enabled = true) {
|
|
3823
4296
|
this.channelMap.set(id, { channel, enabled });
|
|
@@ -3835,9 +4308,21 @@ var NotificationService = class {
|
|
|
3835
4308
|
removePushToken(token) {
|
|
3836
4309
|
this.expoChannel?.removeToken(token);
|
|
3837
4310
|
}
|
|
4311
|
+
/** 注册原生 APNs device token(直连 APNs,优先于 Expo 推送服务) */
|
|
4312
|
+
addNativePushToken(token, ws) {
|
|
4313
|
+
this.activityPushChannel?.addAlertToken(token, ws);
|
|
4314
|
+
if (this.activityPushChannel?.hasAlertTokens()) {
|
|
4315
|
+
console.log("[NotificationService] \u2705 \u76F4\u8FDE APNs alert token \u5DF2\u6CE8\u518C\uFF0CExpo \u63A8\u9001\u6E20\u9053\u964D\u7EA7\u5907\u7528");
|
|
4316
|
+
}
|
|
4317
|
+
}
|
|
4318
|
+
/** 移除原生 APNs device token */
|
|
4319
|
+
removeNativePushToken(token) {
|
|
4320
|
+
this.activityPushChannel?.removeAlertToken(token);
|
|
4321
|
+
}
|
|
3838
4322
|
/** 更新通知音效偏好 */
|
|
3839
4323
|
setSoundPreferences(prefs) {
|
|
3840
4324
|
this.expoChannel?.setSoundPreferences(prefs);
|
|
4325
|
+
this.activityPushChannel?.setAlertSoundPreferences(prefs);
|
|
3841
4326
|
}
|
|
3842
4327
|
/** 设置 ActivityKit Push 渠道(可选,需要 APNs 认证配置) */
|
|
3843
4328
|
setActivityPushChannel(channel) {
|
|
@@ -3862,6 +4347,7 @@ var NotificationService = class {
|
|
|
3862
4347
|
this.recentActivityState.delete(sessionId);
|
|
3863
4348
|
this.lastActivityPushAt.delete(sessionId);
|
|
3864
4349
|
this.activityCounters.delete(sessionId);
|
|
4350
|
+
this.lastPushedFingerprint.delete(sessionId);
|
|
3865
4351
|
}
|
|
3866
4352
|
/** 注入"会话 → 待审批列表"提供器(server.ts 启动时调用) */
|
|
3867
4353
|
setPendingApprovalsProvider(fn) {
|
|
@@ -4012,6 +4498,7 @@ var NotificationService = class {
|
|
|
4012
4498
|
this.lastActivityPushAt.clear();
|
|
4013
4499
|
this.pendingPriority.clear();
|
|
4014
4500
|
this.activityCounters.clear();
|
|
4501
|
+
this.lastPushedFingerprint.clear();
|
|
4015
4502
|
}
|
|
4016
4503
|
// ============================================
|
|
4017
4504
|
// 内部方法
|
|
@@ -4063,12 +4550,19 @@ var NotificationService = class {
|
|
|
4063
4550
|
}
|
|
4064
4551
|
}
|
|
4065
4552
|
notify(payload) {
|
|
4066
|
-
|
|
4553
|
+
const hasDirectApns = this.activityPushChannel?.hasAlertTokens() === true;
|
|
4554
|
+
for (const [id, { channel, enabled }] of this.channelMap.entries()) {
|
|
4067
4555
|
if (!enabled) continue;
|
|
4556
|
+
if (id === "expo" && hasDirectApns) continue;
|
|
4068
4557
|
channel.send(payload).catch((err) => {
|
|
4069
4558
|
console.error("[NotificationService] Notification send failed:", err);
|
|
4070
4559
|
});
|
|
4071
4560
|
}
|
|
4561
|
+
if (hasDirectApns && this.activityPushChannel) {
|
|
4562
|
+
this.activityPushChannel.send(payload).catch((err) => {
|
|
4563
|
+
console.error("[NotificationService] Direct APNs push failed, no fallback:", err);
|
|
4564
|
+
});
|
|
4565
|
+
}
|
|
4072
4566
|
}
|
|
4073
4567
|
/** 从 assistant 事件中提取最新文本消息 */
|
|
4074
4568
|
trackAssistantText(sessionId, event) {
|
|
@@ -4105,6 +4599,8 @@ var NotificationService = class {
|
|
|
4105
4599
|
while (state2.history.length > RECENT_ACTIVITY_MAX) state2.history.shift();
|
|
4106
4600
|
state2.currentEntries = [];
|
|
4107
4601
|
state2.currentMessageId = null;
|
|
4602
|
+
state2.accumulatedText = "";
|
|
4603
|
+
state2.countedToolIds = /* @__PURE__ */ new Set();
|
|
4108
4604
|
}
|
|
4109
4605
|
return;
|
|
4110
4606
|
}
|
|
@@ -4113,7 +4609,7 @@ var NotificationService = class {
|
|
|
4113
4609
|
if (!Array.isArray(msg.content)) return;
|
|
4114
4610
|
let state = this.recentActivityState.get(sessionId);
|
|
4115
4611
|
if (!state) {
|
|
4116
|
-
state = { history: [], currentMessageId: null, currentEntries: [] };
|
|
4612
|
+
state = { history: [], currentMessageId: null, currentEntries: [], accumulatedText: "", countedToolIds: /* @__PURE__ */ new Set() };
|
|
4117
4613
|
this.recentActivityState.set(sessionId, state);
|
|
4118
4614
|
}
|
|
4119
4615
|
if (state.currentMessageId !== msg.id) {
|
|
@@ -4123,16 +4619,25 @@ var NotificationService = class {
|
|
|
4123
4619
|
}
|
|
4124
4620
|
state.currentEntries = [];
|
|
4125
4621
|
state.currentMessageId = msg.id;
|
|
4622
|
+
state.accumulatedText = "";
|
|
4623
|
+
state.countedToolIds = /* @__PURE__ */ new Set();
|
|
4624
|
+
}
|
|
4625
|
+
for (const block of msg.content) {
|
|
4626
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
4627
|
+
state.accumulatedText += block.text;
|
|
4628
|
+
}
|
|
4126
4629
|
}
|
|
4127
4630
|
const next = [];
|
|
4631
|
+
const accText = this.summarizeText(state.accumulatedText);
|
|
4632
|
+
if (accText.length >= 4) next.push(accText);
|
|
4128
4633
|
for (const block of msg.content) {
|
|
4129
|
-
if (block.type === "
|
|
4130
|
-
const line = this.summarizeText(block.text);
|
|
4131
|
-
if (line.length >= 4) next.push(line);
|
|
4132
|
-
} else if (block.type === "tool_use") {
|
|
4634
|
+
if (block.type === "tool_use") {
|
|
4133
4635
|
const line = this.summarizeToolCall(block.name, block.input ?? {});
|
|
4134
4636
|
if (line) next.push(line);
|
|
4135
|
-
|
|
4637
|
+
if (!state.countedToolIds.has(block.id)) {
|
|
4638
|
+
state.countedToolIds.add(block.id);
|
|
4639
|
+
this.incrementCounter(sessionId, block.name);
|
|
4640
|
+
}
|
|
4136
4641
|
}
|
|
4137
4642
|
}
|
|
4138
4643
|
state.currentEntries = next;
|
|
@@ -4248,7 +4753,7 @@ var NotificationService = class {
|
|
|
4248
4753
|
return;
|
|
4249
4754
|
}
|
|
4250
4755
|
if (session.status === "running" || session.status === "waiting_approval" || session.status === "waiting_question") {
|
|
4251
|
-
this.scheduleActivityPush(sessionId);
|
|
4756
|
+
this.scheduleActivityPush(sessionId, true);
|
|
4252
4757
|
}
|
|
4253
4758
|
}, ACTIVITY_PUSH_THROTTLE_MS);
|
|
4254
4759
|
this.laHeartbeatTimers.set(sessionId, timer);
|
|
@@ -4285,6 +4790,7 @@ var NotificationService = class {
|
|
|
4285
4790
|
* 无 token → 普通 Expo push。清理所有相关状态。
|
|
4286
4791
|
*/
|
|
4287
4792
|
flushActivityEnd(sessionId, reason) {
|
|
4793
|
+
console.log(`[NotificationService] \u{1F514} flushActivityEnd(${reason}) session=${sessionId.slice(0, 8)}\u2026 expoAvailable=${this.expoChannel?.isAvailable() ?? false}`);
|
|
4288
4794
|
const sessionTitle = this.getSessionTitle(sessionId);
|
|
4289
4795
|
const latestMsg = this.latestAssistantText.get(sessionId);
|
|
4290
4796
|
const isError = reason === "error";
|
|
@@ -4322,14 +4828,36 @@ var NotificationService = class {
|
|
|
4322
4828
|
this.recentActivityState.delete(sessionId);
|
|
4323
4829
|
this.lastActivityPushAt.delete(sessionId);
|
|
4324
4830
|
this.activityCounters.delete(sessionId);
|
|
4831
|
+
this.lastPushedFingerprint.delete(sessionId);
|
|
4325
4832
|
console.log(`[NotificationService] \u{1F3C1} LA end (${reason}) session=${sessionId.slice(0, 8)}\u2026`);
|
|
4326
4833
|
}
|
|
4834
|
+
/**
|
|
4835
|
+
* 计算内容指纹:status + recentActivity + latestApproval。
|
|
4836
|
+
* 用于判断 LA 内容是否实际变化,避免重复推送消耗 APNs budget。
|
|
4837
|
+
*/
|
|
4838
|
+
computeContentFingerprint(sessionId) {
|
|
4839
|
+
const activity = this.getRecentActivity(sessionId);
|
|
4840
|
+
const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
|
|
4841
|
+
const approvals = this.pendingApprovalsProvider?.(sessionId) ?? [];
|
|
4842
|
+
const latestApproval = approvals[approvals.length - 1];
|
|
4843
|
+
return `${session?.status ?? ""}|${activity.join(" ")}|${latestApproval?.id ?? ""}`;
|
|
4844
|
+
}
|
|
4327
4845
|
/** 真正发送一次 LA content push(无 alert) */
|
|
4328
4846
|
flushActivityPush(sessionId) {
|
|
4329
4847
|
const channel = this.activityPushChannel;
|
|
4330
4848
|
if (!channel?.hasToken(sessionId)) return;
|
|
4331
4849
|
const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
|
|
4332
4850
|
if (!session) return;
|
|
4851
|
+
const fingerprint = this.computeContentFingerprint(sessionId);
|
|
4852
|
+
const lastFingerprint = this.lastPushedFingerprint.get(sessionId);
|
|
4853
|
+
const contentChanged = fingerprint !== lastFingerprint;
|
|
4854
|
+
if (!contentChanged) {
|
|
4855
|
+
const lastPush = this.lastActivityPushAt.get(sessionId) ?? 0;
|
|
4856
|
+
if (Date.now() - lastPush < _NotificationService.STATS_REFRESH_INTERVAL_MS) {
|
|
4857
|
+
return;
|
|
4858
|
+
}
|
|
4859
|
+
}
|
|
4860
|
+
this.lastPushedFingerprint.set(sessionId, fingerprint);
|
|
4333
4861
|
const recentActivity = this.getRecentActivity(sessionId);
|
|
4334
4862
|
const latestMessage = recentActivity[recentActivity.length - 1] ?? this.latestAssistantText.get(sessionId) ?? "";
|
|
4335
4863
|
const sessionTitle = this.getSessionTitle(sessionId);
|
|
@@ -4358,13 +4886,14 @@ var NotificationService = class {
|
|
|
4358
4886
|
};
|
|
4359
4887
|
}
|
|
4360
4888
|
contentState.stats = this.buildStatsPayload(session);
|
|
4361
|
-
const
|
|
4889
|
+
const explicitPriority = this.pendingPriority.get(sessionId);
|
|
4890
|
+
const priority = explicitPriority ?? (contentChanged ? "10" : "5");
|
|
4362
4891
|
this.pendingPriority.delete(sessionId);
|
|
4363
4892
|
this.lastActivityPushAt.set(sessionId, Date.now());
|
|
4364
4893
|
const lineCount = recentActivity.length;
|
|
4365
4894
|
channel.updateActivity(sessionId, contentState, { priority }).then((ok) => {
|
|
4366
4895
|
if (ok) {
|
|
4367
|
-
console.log(`[NotificationService] \u{1F4E1} LA push \u2713 session=${sessionId.slice(0, 8)}\u2026 status=${status} p=${priority} lines=${lineCount}`);
|
|
4896
|
+
console.log(`[NotificationService] \u{1F4E1} LA push \u2713 session=${sessionId.slice(0, 8)}\u2026 status=${status} p=${priority} changed=${contentChanged} lines=${lineCount}`);
|
|
4368
4897
|
}
|
|
4369
4898
|
}).catch((err) => {
|
|
4370
4899
|
console.warn(`[NotificationService] \u{1F4E1} LA push \u2717 session=${sessionId.slice(0, 8)}\u2026:`, err instanceof Error ? err.message : err);
|
|
@@ -4493,6 +5022,8 @@ var DesktopNotificationChannel = class {
|
|
|
4493
5022
|
|
|
4494
5023
|
// src/notification/ExpoNotificationChannel.ts
|
|
4495
5024
|
var EXPO_PUSH_API = "https://exp.host/--/api/v2/push/send";
|
|
5025
|
+
var EXPO_RECEIPT_API = "https://exp.host/--/api/v2/push/getReceipts";
|
|
5026
|
+
var RECEIPT_CHECK_DELAY_MS = 1e4;
|
|
4496
5027
|
var ExpoNotificationChannel = class {
|
|
4497
5028
|
tokens = /* @__PURE__ */ new Set();
|
|
4498
5029
|
/** push token → WebSocket 连接映射,用于前台抑制 */
|
|
@@ -4521,7 +5052,13 @@ var ExpoNotificationChannel = class {
|
|
|
4521
5052
|
console.log(`[ExpoNotificationChannel] ${t("notification.soundPrefsUpdated")}`);
|
|
4522
5053
|
}
|
|
4523
5054
|
async send(payload) {
|
|
4524
|
-
if (this.tokens.size === 0)
|
|
5055
|
+
if (this.tokens.size === 0) {
|
|
5056
|
+
const isCompletion = payload.data?.type === "task_complete" || payload.data?.type === "task_error";
|
|
5057
|
+
if (isCompletion) {
|
|
5058
|
+
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");
|
|
5059
|
+
}
|
|
5060
|
+
return;
|
|
5061
|
+
}
|
|
4525
5062
|
const isCompletionNotif = payload.data?.type === "task_complete" || payload.data?.type === "task_error";
|
|
4526
5063
|
const targetTokens = isCompletionNotif ? Array.from(this.tokens) : Array.from(this.tokens).filter((token) => {
|
|
4527
5064
|
const ws = this.tokenWsMap.get(token);
|
|
@@ -4534,6 +5071,7 @@ var ExpoNotificationChannel = class {
|
|
|
4534
5071
|
if (prefs) {
|
|
4535
5072
|
const notifType = payload.data?.type ?? "";
|
|
4536
5073
|
if (notifType === "approval_request" && prefs.approval) sound = prefs.approval;
|
|
5074
|
+
else if (notifType === "question_request" && prefs.approval) sound = prefs.approval;
|
|
4537
5075
|
else if (notifType === "task_complete" && prefs.taskComplete) sound = prefs.taskComplete;
|
|
4538
5076
|
else if (notifType === "task_error" && prefs.taskError) sound = prefs.taskError;
|
|
4539
5077
|
}
|
|
@@ -4564,16 +5102,76 @@ var ExpoNotificationChannel = class {
|
|
|
4564
5102
|
console.warn(`[ExpoNotificationChannel] ${t("notification.pushApiFormatError")}`, JSON.stringify(body));
|
|
4565
5103
|
return;
|
|
4566
5104
|
}
|
|
4567
|
-
|
|
5105
|
+
const receiptIdToToken = /* @__PURE__ */ new Map();
|
|
5106
|
+
for (let i = 0; i < body.data.length; i++) {
|
|
5107
|
+
const ticket = body.data[i];
|
|
4568
5108
|
if (ticket.status === "error") {
|
|
4569
|
-
|
|
5109
|
+
const errorCode = ticket.details?.error ?? "unknown";
|
|
5110
|
+
console.error(`[ExpoNotificationChannel] ${t("notification.pushFailed")} ${ticket.message} (${errorCode})`);
|
|
5111
|
+
if (errorCode === "DeviceNotRegistered" && targetTokens[i]) {
|
|
5112
|
+
const staleToken = targetTokens[i];
|
|
5113
|
+
this.tokens.delete(staleToken);
|
|
5114
|
+
this.tokenWsMap.delete(staleToken);
|
|
5115
|
+
this.soundPreferences.delete(staleToken);
|
|
5116
|
+
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`);
|
|
5117
|
+
}
|
|
5118
|
+
} else if (ticket.status === "ok" && typeof ticket.id === "string" && targetTokens[i]) {
|
|
5119
|
+
receiptIdToToken.set(ticket.id, targetTokens[i]);
|
|
4570
5120
|
}
|
|
4571
5121
|
}
|
|
5122
|
+
this.scheduleReceiptCheck(receiptIdToToken);
|
|
4572
5123
|
}
|
|
4573
5124
|
} catch (err) {
|
|
4574
5125
|
console.warn(`[ExpoNotificationChannel] ${t("notification.sendFailed")}`, err);
|
|
4575
5126
|
}
|
|
4576
5127
|
}
|
|
5128
|
+
/**
|
|
5129
|
+
* Expo push 二阶段:延迟查 receipt,暴露 ticket 阶段看不到的 APNs 投递失败。
|
|
5130
|
+
*
|
|
5131
|
+
* 关键诊断点:InvalidCredentials / MismatchSenderId 表示 Expo 项目的 APNs
|
|
5132
|
+
* 凭证配置问题(不是用户机器问题)——这正是"只有开发者能收到推送"的根因,
|
|
5133
|
+
* 且 ticket 全为 ok、不查 receipt 永远静默。
|
|
5134
|
+
*/
|
|
5135
|
+
scheduleReceiptCheck(receiptIdToToken) {
|
|
5136
|
+
if (receiptIdToToken.size === 0) return;
|
|
5137
|
+
const timer = setTimeout(async () => {
|
|
5138
|
+
try {
|
|
5139
|
+
const ids = Array.from(receiptIdToToken.keys());
|
|
5140
|
+
const res = await fetch(EXPO_RECEIPT_API, {
|
|
5141
|
+
method: "POST",
|
|
5142
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
5143
|
+
body: JSON.stringify({ ids })
|
|
5144
|
+
});
|
|
5145
|
+
const body = await res.json();
|
|
5146
|
+
if (!res.ok || !body?.data || typeof body.data !== "object") {
|
|
5147
|
+
console.warn("[ExpoNotificationChannel] \u26A0\uFE0F push receipt \u67E5\u8BE2\u5931\u8D25", res.status, JSON.stringify(body));
|
|
5148
|
+
return;
|
|
5149
|
+
}
|
|
5150
|
+
const receipts = body.data;
|
|
5151
|
+
for (const [receiptId, receipt] of Object.entries(receipts)) {
|
|
5152
|
+
if (receipt?.status !== "error") continue;
|
|
5153
|
+
const errorCode = receipt.details?.error ?? "unknown";
|
|
5154
|
+
const token = receiptIdToToken.get(receiptId);
|
|
5155
|
+
console.error(
|
|
5156
|
+
`[ExpoNotificationChannel] \u274C APNs \u6295\u9012\u5931\u8D25 receipt=${receiptId} error=${errorCode}` + (receipt.message ? ` \u2014 ${receipt.message}` : "")
|
|
5157
|
+
);
|
|
5158
|
+
if (errorCode === "DeviceNotRegistered" && token) {
|
|
5159
|
+
this.tokens.delete(token);
|
|
5160
|
+
this.tokenWsMap.delete(token);
|
|
5161
|
+
this.soundPreferences.delete(token);
|
|
5162
|
+
console.warn("[ExpoNotificationChannel] \u26A0\uFE0F \u5DF2\u79FB\u9664\u5931\u6548 token\uFF08receipt DeviceNotRegistered\uFF09\u3002\u91CD\u542F App \u53EF\u91CD\u65B0\u6CE8\u518C\u3002");
|
|
5163
|
+
} else if (errorCode === "InvalidCredentials" || errorCode === "MismatchSenderId") {
|
|
5164
|
+
console.error(
|
|
5165
|
+
"[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"
|
|
5166
|
+
);
|
|
5167
|
+
}
|
|
5168
|
+
}
|
|
5169
|
+
} catch (err) {
|
|
5170
|
+
console.warn("[ExpoNotificationChannel] \u26A0\uFE0F push receipt \u67E5\u8BE2\u5F02\u5E38:", err);
|
|
5171
|
+
}
|
|
5172
|
+
}, RECEIPT_CHECK_DELAY_MS);
|
|
5173
|
+
timer.unref?.();
|
|
5174
|
+
}
|
|
4577
5175
|
};
|
|
4578
5176
|
|
|
4579
5177
|
// src/notification/ActivityPushChannel.ts
|
|
@@ -4587,6 +5185,14 @@ var APNS_HOSTS = {
|
|
|
4587
5185
|
var ActivityPushChannel = class {
|
|
4588
5186
|
/** sessionId -> activityPushToken */
|
|
4589
5187
|
tokens = /* @__PURE__ */ new Map();
|
|
5188
|
+
/** 原生 device token 集合(用于普通 alert push,绕过 Expo 推送服务) */
|
|
5189
|
+
alertTokens = /* @__PURE__ */ new Set();
|
|
5190
|
+
/** alert token -> WebSocket 映射(用于前台在线过滤) */
|
|
5191
|
+
alertTokenWsMap = /* @__PURE__ */ new Map();
|
|
5192
|
+
/** alert token 已确认的 APNs 环境(独立于 LA token 的探测结果) */
|
|
5193
|
+
alertTokenEnv = /* @__PURE__ */ new Map();
|
|
5194
|
+
/** per-alert-token 通知音效偏好 */
|
|
5195
|
+
alertSoundPreferences = /* @__PURE__ */ new Map();
|
|
4590
5196
|
/**
|
|
4591
5197
|
* 每个 token 已确认工作的 APNs 环境。
|
|
4592
5198
|
* Debug build (aps-environment=development) 的 token 仅在 sandbox 端有效;
|
|
@@ -4605,6 +5211,7 @@ var ActivityPushChannel = class {
|
|
|
4605
5211
|
teamId;
|
|
4606
5212
|
keyId;
|
|
4607
5213
|
authKey;
|
|
5214
|
+
bundleId;
|
|
4608
5215
|
/** 缓存的 JWT token + 过期时间 */
|
|
4609
5216
|
cachedJwt = null;
|
|
4610
5217
|
/** 每个环境一条 HTTP/2 长连接 */
|
|
@@ -4613,6 +5220,7 @@ var ActivityPushChannel = class {
|
|
|
4613
5220
|
this.teamId = config.teamId;
|
|
4614
5221
|
this.keyId = config.keyId;
|
|
4615
5222
|
this.authKey = fs2.readFileSync(config.authKeyPath, "utf-8");
|
|
5223
|
+
this.bundleId = config.bundleId ?? "com.kachun.sessix";
|
|
4616
5224
|
this.probeOrder = config.sandbox === false ? ["production", "sandbox"] : ["sandbox", "production"];
|
|
4617
5225
|
console.log(`[ActivityPushChannel] Initialized (probe order: ${this.probeOrder.join(" \u2192 ")})`);
|
|
4618
5226
|
}
|
|
@@ -4657,557 +5265,335 @@ var ActivityPushChannel = class {
|
|
|
4657
5265
|
/** 发送 content-state 更新到指定会话的 Live Activity(纯内容刷新,不响通知)。返回 true 表示实际发出 */
|
|
4658
5266
|
async updateActivity(sessionId, contentState, opts) {
|
|
4659
5267
|
const token = this.tokens.get(sessionId);
|
|
4660
|
-
if (!token) return false;
|
|
4661
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
4662
|
-
const priority = opts?.priority ?? "5";
|
|
4663
|
-
const payload = {
|
|
4664
|
-
aps: {
|
|
4665
|
-
timestamp: now,
|
|
4666
|
-
event: "update",
|
|
4667
|
-
"content-state": contentState,
|
|
4668
|
-
"stale-date": now + 600
|
|
4669
|
-
}
|
|
4670
|
-
};
|
|
4671
|
-
try {
|
|
4672
|
-
await this.sendToAPNs(token, payload, {
|
|
4673
|
-
priority,
|
|
4674
|
-
collapseId: `state-${sessionId.slice(0, 54)}`
|
|
4675
|
-
});
|
|
4676
|
-
return true;
|
|
4677
|
-
} catch (err) {
|
|
4678
|
-
console.warn(`[ActivityPushChannel] Update failed session=${sessionId}:`, err);
|
|
4679
|
-
return false;
|
|
4680
|
-
}
|
|
4681
|
-
}
|
|
4682
|
-
/** 发送带通知的 content-state 更新(审批请求时使用),返回是否发送成功 */
|
|
4683
|
-
async updateActivityWithAlert(sessionId, contentState, alert) {
|
|
4684
|
-
const token = this.tokens.get(sessionId);
|
|
4685
|
-
if (!token) return false;
|
|
4686
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
4687
|
-
const payload = {
|
|
4688
|
-
aps: {
|
|
4689
|
-
timestamp: now,
|
|
4690
|
-
event: "update",
|
|
4691
|
-
"content-state": contentState,
|
|
4692
|
-
"stale-date": now + 600,
|
|
4693
|
-
alert,
|
|
4694
|
-
sound: "default"
|
|
4695
|
-
}
|
|
4696
|
-
};
|
|
4697
|
-
try {
|
|
4698
|
-
await this.sendToAPNs(token, payload, { priority: "10" });
|
|
4699
|
-
return true;
|
|
4700
|
-
} catch (err) {
|
|
4701
|
-
console.warn(`[ActivityPushChannel] Alert update failed session=${sessionId}:`, err);
|
|
4702
|
-
return false;
|
|
4703
|
-
}
|
|
4704
|
-
}
|
|
4705
|
-
/**
|
|
4706
|
-
* 结束指定会话的 Live Activity。
|
|
4707
|
-
* 可选 alert:APNs event=end 时同时推送横幅通知 + 声音,用于在会话完成时提醒用户。
|
|
4708
|
-
*/
|
|
4709
|
-
async endActivity(sessionId, contentState, opts) {
|
|
4710
|
-
const token = this.tokens.get(sessionId);
|
|
4711
|
-
if (!token) return;
|
|
4712
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
4713
|
-
const aps = {
|
|
4714
|
-
timestamp: now,
|
|
4715
|
-
event: "end",
|
|
4716
|
-
"content-state": contentState
|
|
4717
|
-
};
|
|
4718
|
-
if (opts?.alert) {
|
|
4719
|
-
aps.alert = opts.alert;
|
|
4720
|
-
aps.sound = "default";
|
|
4721
|
-
}
|
|
4722
|
-
const payload = { aps };
|
|
4723
|
-
try {
|
|
4724
|
-
await this.sendToAPNs(token, payload, { priority: "10" });
|
|
4725
|
-
} catch (err) {
|
|
4726
|
-
this.tokens.delete(sessionId);
|
|
4727
|
-
throw err;
|
|
4728
|
-
}
|
|
4729
|
-
this.tokens.delete(sessionId);
|
|
4730
|
-
}
|
|
4731
|
-
/** 检查是否有指定会话的 token */
|
|
4732
|
-
hasToken(sessionId) {
|
|
4733
|
-
return this.tokens.has(sessionId);
|
|
4734
|
-
}
|
|
4735
|
-
/**
|
|
4736
|
-
* 发送 APNs,自动处理环境探测。
|
|
4737
|
-
* 对每个 token:先用已确认的环境(如有);否则按 probeOrder 顺序探测,
|
|
4738
|
-
* 收到 BadDeviceToken / BadEnvironmentKeyInToken 自动切到另一个环境,
|
|
4739
|
-
* 并把成功的环境绑定到该 token。
|
|
4740
|
-
*/
|
|
4741
|
-
async sendToAPNs(deviceToken, payload, opts = {}) {
|
|
4742
|
-
if (this.deadTokens.has(deviceToken)) {
|
|
4743
|
-
throw new Error(`token permanently dead (Activity ended): ${deviceToken.slice(0, 16)}\u2026`);
|
|
4744
|
-
}
|
|
4745
|
-
const known = this.tokenEnv.get(deviceToken);
|
|
4746
|
-
if (known) {
|
|
4747
|
-
return this.sendToAPNsOnce(deviceToken, payload, opts, known);
|
|
4748
|
-
}
|
|
4749
|
-
const short = deviceToken.slice(0, 16);
|
|
4750
|
-
console.log(`[ActivityPushChannel] \u{1F50D} probe start token=${short}\u2026 order=[${this.probeOrder.join(",")}]`);
|
|
4751
|
-
let lastErr = null;
|
|
4752
|
-
for (const env of this.probeOrder) {
|
|
4753
|
-
try {
|
|
4754
|
-
console.log(`[ActivityPushChannel] \u{1F50D} probe try ${env} token=${short}\u2026`);
|
|
4755
|
-
await this.sendToAPNsOnce(deviceToken, payload, opts, env);
|
|
4756
|
-
this.tokenEnv.set(deviceToken, env);
|
|
4757
|
-
console.log(`[ActivityPushChannel] \u2705 probe bound to ${env} (token=${short}\u2026)`);
|
|
4758
|
-
return;
|
|
4759
|
-
} catch (err) {
|
|
4760
|
-
lastErr = err;
|
|
4761
|
-
const reason = err instanceof ApnsError ? JSON.parse(err.responseBody || "{}")?.reason ?? err.statusCode : String(err);
|
|
4762
|
-
console.log(`[ActivityPushChannel] \u{1F50D} probe ${env} failed: ${reason}`);
|
|
4763
|
-
if (!isBadDeviceTokenError(err)) {
|
|
4764
|
-
throw err;
|
|
4765
|
-
}
|
|
4766
|
-
}
|
|
4767
|
-
}
|
|
4768
|
-
this.deadTokens.add(deviceToken);
|
|
4769
|
-
for (const [sid, tok] of this.tokens) {
|
|
4770
|
-
if (tok === deviceToken) {
|
|
4771
|
-
this.tokens.delete(sid);
|
|
4772
|
-
console.warn(`[ActivityPushChannel] \u2620\uFE0F dead token, session removed: session=${sid.slice(0, 8)}\u2026 token=${short}\u2026 (Activity ended/iOS reinstalled)`);
|
|
4773
|
-
break;
|
|
4774
|
-
}
|
|
4775
|
-
}
|
|
4776
|
-
throw lastErr ?? new Error("APNs send failed: all environments rejected token");
|
|
4777
|
-
}
|
|
4778
|
-
/** 单次 APNs HTTP/2 请求(内部 helper,不做探测) */
|
|
4779
|
-
async sendToAPNsOnce(deviceToken, payload, opts, env) {
|
|
4780
|
-
const topic = "com.kachun.sessix.push-type.liveactivity";
|
|
4781
|
-
const jwt = this.getJWT();
|
|
4782
|
-
const payloadStr = JSON.stringify(payload);
|
|
4783
|
-
const priority = opts.priority ?? "10";
|
|
4784
|
-
return new Promise((resolve, reject) => {
|
|
4785
|
-
let client;
|
|
4786
|
-
try {
|
|
4787
|
-
client = this.getHttp2Client(env);
|
|
4788
|
-
} catch (err) {
|
|
4789
|
-
return reject(err);
|
|
4790
|
-
}
|
|
4791
|
-
const headers = {
|
|
4792
|
-
":method": "POST",
|
|
4793
|
-
":path": `/3/device/${deviceToken}`,
|
|
4794
|
-
"authorization": `bearer ${jwt}`,
|
|
4795
|
-
"apns-topic": topic,
|
|
4796
|
-
"apns-push-type": "liveactivity",
|
|
4797
|
-
"apns-priority": priority,
|
|
4798
|
-
"apns-expiration": String(Math.floor(Date.now() / 1e3) + 300),
|
|
4799
|
-
"content-type": "application/json",
|
|
4800
|
-
"content-length": Buffer.byteLength(payloadStr)
|
|
4801
|
-
};
|
|
4802
|
-
if (opts.collapseId) {
|
|
4803
|
-
headers["apns-collapse-id"] = opts.collapseId;
|
|
4804
|
-
}
|
|
4805
|
-
const req = client.request(headers);
|
|
4806
|
-
let statusCode = 0;
|
|
4807
|
-
let responseData = "";
|
|
4808
|
-
req.on("response", (headers2) => {
|
|
4809
|
-
statusCode = Number(headers2[":status"] ?? 0);
|
|
4810
|
-
});
|
|
4811
|
-
req.on("data", (chunk) => {
|
|
4812
|
-
responseData += chunk;
|
|
4813
|
-
});
|
|
4814
|
-
req.on("end", () => {
|
|
4815
|
-
if (statusCode === 200) {
|
|
4816
|
-
resolve();
|
|
4817
|
-
} else {
|
|
4818
|
-
if (statusCode === 0) {
|
|
4819
|
-
const c = this.http2Clients[env];
|
|
4820
|
-
c?.destroy();
|
|
4821
|
-
delete this.http2Clients[env];
|
|
4822
|
-
}
|
|
4823
|
-
reject(new ApnsError(statusCode, responseData));
|
|
4824
|
-
}
|
|
4825
|
-
});
|
|
4826
|
-
req.on("error", (err) => {
|
|
4827
|
-
reject(err);
|
|
4828
|
-
});
|
|
4829
|
-
req.write(payloadStr);
|
|
4830
|
-
req.end();
|
|
4831
|
-
});
|
|
4832
|
-
}
|
|
4833
|
-
/** 生成或获取缓存的 APNs JWT token */
|
|
4834
|
-
getJWT() {
|
|
4835
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
4836
|
-
if (this.cachedJwt && this.cachedJwt.expiresAt > now) {
|
|
4837
|
-
return this.cachedJwt.token;
|
|
4838
|
-
}
|
|
4839
|
-
const header = Buffer.from(JSON.stringify({
|
|
4840
|
-
alg: "ES256",
|
|
4841
|
-
kid: this.keyId
|
|
4842
|
-
})).toString("base64url");
|
|
4843
|
-
const claims = Buffer.from(JSON.stringify({
|
|
4844
|
-
iss: this.teamId,
|
|
4845
|
-
iat: now
|
|
4846
|
-
})).toString("base64url");
|
|
4847
|
-
const signingInput = `${header}.${claims}`;
|
|
4848
|
-
const sign = crypto.createSign("SHA256");
|
|
4849
|
-
sign.update(signingInput);
|
|
4850
|
-
const signature = sign.sign({ key: this.authKey, dsaEncoding: "ieee-p1363" }, "base64url");
|
|
4851
|
-
const token = `${signingInput}.${signature}`;
|
|
4852
|
-
this.cachedJwt = { token, expiresAt: now + 3e3 };
|
|
4853
|
-
return token;
|
|
4854
|
-
}
|
|
4855
|
-
};
|
|
4856
|
-
var ApnsError = class extends Error {
|
|
4857
|
-
constructor(statusCode, responseBody) {
|
|
4858
|
-
super(`APNs returned ${statusCode}: ${responseBody}`);
|
|
4859
|
-
this.statusCode = statusCode;
|
|
4860
|
-
this.responseBody = responseBody;
|
|
4861
|
-
this.name = "ApnsError";
|
|
4862
|
-
}
|
|
4863
|
-
};
|
|
4864
|
-
function isBadDeviceTokenError(err) {
|
|
4865
|
-
if (!(err instanceof ApnsError)) return false;
|
|
4866
|
-
if (err.statusCode !== 400 && err.statusCode !== 403 && err.statusCode !== 410) return false;
|
|
4867
|
-
try {
|
|
4868
|
-
const parsed = JSON.parse(err.responseBody);
|
|
4869
|
-
return parsed.reason === "BadDeviceToken" || parsed.reason === "BadEnvironmentKeyInToken" || parsed.reason === "Unregistered";
|
|
4870
|
-
} catch {
|
|
4871
|
-
return false;
|
|
4872
|
-
}
|
|
4873
|
-
}
|
|
4874
|
-
|
|
4875
|
-
// src/session/ProjectReader.ts
|
|
4876
|
-
var import_promises3 = require("fs/promises");
|
|
4877
|
-
var import_readline3 = require("readline");
|
|
4878
|
-
var import_path2 = require("path");
|
|
4879
|
-
var import_os2 = require("os");
|
|
4880
|
-
var CLAUDE_PROJECTS_DIR = (0, import_path2.join)((0, import_os2.homedir)(), ".claude", "projects");
|
|
4881
|
-
function getSessionFilePath(projectPath, sessionId) {
|
|
4882
|
-
return (0, import_path2.join)(CLAUDE_PROJECTS_DIR, encodeDirName(projectPath), `${sessionId}.jsonl`);
|
|
4883
|
-
}
|
|
4884
|
-
async function getProjects() {
|
|
4885
|
-
try {
|
|
4886
|
-
const dirExists = await directoryExists(CLAUDE_PROJECTS_DIR);
|
|
4887
|
-
if (!dirExists) {
|
|
4888
|
-
return { ok: true, value: [] };
|
|
4889
|
-
}
|
|
4890
|
-
const entries = await (0, import_promises3.readdir)(CLAUDE_PROJECTS_DIR, { withFileTypes: true });
|
|
4891
|
-
const projects = [];
|
|
4892
|
-
for (const entry of entries) {
|
|
4893
|
-
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
4894
|
-
continue;
|
|
5268
|
+
if (!token) return false;
|
|
5269
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
5270
|
+
const priority = opts?.priority ?? "5";
|
|
5271
|
+
const payload = {
|
|
5272
|
+
aps: {
|
|
5273
|
+
timestamp: now,
|
|
5274
|
+
event: "update",
|
|
5275
|
+
"content-state": contentState,
|
|
5276
|
+
"stale-date": now + 600
|
|
4895
5277
|
}
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
projects.push({
|
|
4902
|
-
id: encodedPath,
|
|
4903
|
-
path: decodedPath,
|
|
4904
|
-
name,
|
|
4905
|
-
sessionCount,
|
|
4906
|
-
lastActiveAt: latestMtime
|
|
5278
|
+
};
|
|
5279
|
+
try {
|
|
5280
|
+
await this.sendToAPNs(token, payload, {
|
|
5281
|
+
priority,
|
|
5282
|
+
collapseId: `state-${sessionId.slice(0, 54)}`
|
|
4907
5283
|
});
|
|
5284
|
+
return true;
|
|
5285
|
+
} catch (err) {
|
|
5286
|
+
console.warn(`[ActivityPushChannel] Update failed session=${sessionId}:`, err);
|
|
5287
|
+
return false;
|
|
4908
5288
|
}
|
|
4909
|
-
projects.sort((a, b) => a.name.localeCompare(b.name));
|
|
4910
|
-
return { ok: true, value: projects };
|
|
4911
|
-
} catch (err) {
|
|
4912
|
-
return {
|
|
4913
|
-
ok: false,
|
|
4914
|
-
error: err instanceof Error ? err : new Error(String(err))
|
|
4915
|
-
};
|
|
4916
5289
|
}
|
|
4917
|
-
|
|
4918
|
-
async
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
const
|
|
4922
|
-
const
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
jsonlFiles.map(async (entry) => {
|
|
4931
|
-
const sessionId = entry.name.slice(0, -6);
|
|
4932
|
-
const filePath = (0, import_path2.join)(projectDir, entry.name);
|
|
4933
|
-
try {
|
|
4934
|
-
const contentTs = await extractLastTimestamp(filePath);
|
|
4935
|
-
if (contentTs) {
|
|
4936
|
-
mtimeMap.set(sessionId, contentTs);
|
|
4937
|
-
} else {
|
|
4938
|
-
const fileStat = await (0, import_promises3.stat)(filePath);
|
|
4939
|
-
mtimeMap.set(sessionId, fileStat.mtimeMs);
|
|
4940
|
-
}
|
|
4941
|
-
} catch {
|
|
4942
|
-
mtimeMap.set(sessionId, 0);
|
|
4943
|
-
}
|
|
4944
|
-
})
|
|
4945
|
-
);
|
|
4946
|
-
const uuidDirs = entries.filter(
|
|
4947
|
-
(e) => e.isDirectory() && UUID_RE.test(e.name) && !mtimeMap.has(e.name)
|
|
4948
|
-
);
|
|
4949
|
-
for (const entry of uuidDirs) {
|
|
4950
|
-
try {
|
|
4951
|
-
const fileStat = await (0, import_promises3.stat)((0, import_path2.join)(projectDir, entry.name));
|
|
4952
|
-
mtimeMap.set(entry.name, fileStat.mtimeMs);
|
|
4953
|
-
} catch {
|
|
4954
|
-
mtimeMap.set(entry.name, 0);
|
|
5290
|
+
/** 发送带通知的 content-state 更新(审批请求时使用),返回是否发送成功 */
|
|
5291
|
+
async updateActivityWithAlert(sessionId, contentState, alert) {
|
|
5292
|
+
const token = this.tokens.get(sessionId);
|
|
5293
|
+
if (!token) return false;
|
|
5294
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
5295
|
+
const payload = {
|
|
5296
|
+
aps: {
|
|
5297
|
+
timestamp: now,
|
|
5298
|
+
event: "update",
|
|
5299
|
+
"content-state": contentState,
|
|
5300
|
+
"stale-date": now + 600,
|
|
5301
|
+
alert,
|
|
5302
|
+
sound: "default"
|
|
4955
5303
|
}
|
|
5304
|
+
};
|
|
5305
|
+
try {
|
|
5306
|
+
await this.sendToAPNs(token, payload, { priority: "10" });
|
|
5307
|
+
return true;
|
|
5308
|
+
} catch (err) {
|
|
5309
|
+
console.warn(`[ActivityPushChannel] Alert update failed session=${sessionId}:`, err);
|
|
5310
|
+
return false;
|
|
4956
5311
|
}
|
|
4957
|
-
|
|
4958
|
-
|
|
5312
|
+
}
|
|
5313
|
+
/**
|
|
5314
|
+
* 结束指定会话的 Live Activity。
|
|
5315
|
+
* 可选 alert:APNs event=end 时同时推送横幅通知 + 声音,用于在会话完成时提醒用户。
|
|
5316
|
+
*/
|
|
5317
|
+
async endActivity(sessionId, contentState, opts) {
|
|
5318
|
+
const token = this.tokens.get(sessionId);
|
|
5319
|
+
if (!token) return;
|
|
5320
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
5321
|
+
const aps = {
|
|
5322
|
+
timestamp: now,
|
|
5323
|
+
event: "end",
|
|
5324
|
+
"content-state": contentState
|
|
5325
|
+
};
|
|
5326
|
+
if (opts?.alert) {
|
|
5327
|
+
aps.alert = opts.alert;
|
|
5328
|
+
aps.sound = "default";
|
|
5329
|
+
}
|
|
5330
|
+
const payload = { aps };
|
|
4959
5331
|
try {
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
const mtime = mtimeMap.get(entry.sessionId) ?? entry.fileMtime ?? (entry.modified ? new Date(entry.modified).getTime() : 0);
|
|
4965
|
-
sessionMap.set(entry.sessionId, {
|
|
4966
|
-
sessionId: entry.sessionId,
|
|
4967
|
-
lastModified: mtime,
|
|
4968
|
-
summary: entry.summary,
|
|
4969
|
-
firstPrompt: entry.firstPrompt,
|
|
4970
|
-
messageCount: entry.messageCount
|
|
4971
|
-
});
|
|
4972
|
-
}
|
|
4973
|
-
await Promise.all(
|
|
4974
|
-
Array.from(sessionMap.values()).filter((s) => (s.messageCount ?? 0) > 0 && !s.summary && !s.firstPrompt).map(async (s) => {
|
|
4975
|
-
const filePath = (0, import_path2.join)(projectDir, `${s.sessionId}.jsonl`);
|
|
4976
|
-
const firstPrompt = await extractFirstPrompt(filePath).catch(() => void 0);
|
|
4977
|
-
if (firstPrompt) s.firstPrompt = firstPrompt;
|
|
4978
|
-
})
|
|
4979
|
-
);
|
|
4980
|
-
}
|
|
4981
|
-
} catch {
|
|
5332
|
+
await this.sendToAPNs(token, payload, { priority: "10" });
|
|
5333
|
+
} catch (err) {
|
|
5334
|
+
this.tokens.delete(sessionId);
|
|
5335
|
+
throw err;
|
|
4982
5336
|
}
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
5337
|
+
this.tokens.delete(sessionId);
|
|
5338
|
+
}
|
|
5339
|
+
/** 检查是否有指定会话的 token */
|
|
5340
|
+
hasToken(sessionId) {
|
|
5341
|
+
return this.tokens.has(sessionId);
|
|
5342
|
+
}
|
|
5343
|
+
// ============================================
|
|
5344
|
+
// 普通 Alert Push(直连 APNs,绕过 Expo 推送服务)
|
|
5345
|
+
// ============================================
|
|
5346
|
+
/** 注册原生 APNs device token,用于发送普通 alert 通知 */
|
|
5347
|
+
addAlertToken(token, ws) {
|
|
5348
|
+
this.alertTokens.add(token);
|
|
5349
|
+
if (ws) this.alertTokenWsMap.set(token, ws);
|
|
5350
|
+
console.log(`[ActivityPushChannel] Alert token registered (${this.alertTokens.size} device(s))`);
|
|
5351
|
+
}
|
|
5352
|
+
/** 移除原生 APNs device token */
|
|
5353
|
+
removeAlertToken(token) {
|
|
5354
|
+
this.alertTokens.delete(token);
|
|
5355
|
+
this.alertTokenWsMap.delete(token);
|
|
5356
|
+
this.alertTokenEnv.delete(token);
|
|
5357
|
+
this.alertSoundPreferences.delete(token);
|
|
5358
|
+
}
|
|
5359
|
+
/** 是否有可用的 alert token */
|
|
5360
|
+
hasAlertTokens() {
|
|
5361
|
+
return this.alertTokens.size > 0;
|
|
5362
|
+
}
|
|
5363
|
+
/** 更新 alert token 音效偏好 */
|
|
5364
|
+
setAlertSoundPreferences(prefs) {
|
|
5365
|
+
for (const token of this.alertTokens) {
|
|
5366
|
+
this.alertSoundPreferences.set(token, prefs);
|
|
4994
5367
|
}
|
|
4995
|
-
const sessions = Array.from(sessionMap.values()).filter((s) => {
|
|
4996
|
-
if (s.messageCount === 0) return false;
|
|
4997
|
-
if (s.messageCount === -1) return true;
|
|
4998
|
-
if (s.firstPrompt === void 0 && s.messageCount === void 0) return false;
|
|
4999
|
-
return true;
|
|
5000
|
-
});
|
|
5001
|
-
sessions.sort((a, b) => b.lastModified - a.lastModified);
|
|
5002
|
-
return { ok: true, value: sessions };
|
|
5003
|
-
} catch (err) {
|
|
5004
|
-
return {
|
|
5005
|
-
ok: false,
|
|
5006
|
-
error: err instanceof Error ? err : new Error(String(err))
|
|
5007
|
-
};
|
|
5008
5368
|
}
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5369
|
+
/**
|
|
5370
|
+
* 发送普通 alert 推送通知(直连 APNs,sandbox/production 自动探测)。
|
|
5371
|
+
* 实现 NotificationChannel.send 接口,可注册到 NotificationService。
|
|
5372
|
+
*/
|
|
5373
|
+
async send(payload) {
|
|
5374
|
+
if (this.alertTokens.size === 0) return;
|
|
5375
|
+
const isCompletionNotif = payload.data?.type === "task_complete" || payload.data?.type === "task_error";
|
|
5376
|
+
const targetTokens = isCompletionNotif ? Array.from(this.alertTokens) : Array.from(this.alertTokens).filter((token) => {
|
|
5377
|
+
const ws = this.alertTokenWsMap.get(token);
|
|
5378
|
+
return !ws || ws.readyState !== ws.OPEN;
|
|
5017
5379
|
});
|
|
5018
|
-
if (
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
const
|
|
5025
|
-
if (
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
if (msgContent.includes("<local-command") || msgContent.includes("<command-name>")) continue;
|
|
5029
|
-
} else if (Array.isArray(msgContent)) {
|
|
5030
|
-
const hasText = msgContent.some(
|
|
5031
|
-
(b) => b.type === "text" && !b.text?.includes("<local-command") && !b.text?.includes("<command-name>")
|
|
5032
|
-
);
|
|
5033
|
-
if (!hasText) continue;
|
|
5034
|
-
}
|
|
5035
|
-
const normalizedContent = typeof msgContent === "string" ? [{ type: "text", text: msgContent }] : Array.isArray(msgContent) ? msgContent.filter((b) => b.type === "text" && typeof b.text === "string") : [];
|
|
5036
|
-
if (normalizedContent.length === 0) continue;
|
|
5037
|
-
events.push({
|
|
5038
|
-
type: "user",
|
|
5039
|
-
message: {
|
|
5040
|
-
...obj.message,
|
|
5041
|
-
content: normalizedContent
|
|
5042
|
-
},
|
|
5043
|
-
session_id: sessionId
|
|
5044
|
-
});
|
|
5045
|
-
} else if (type === "assistant" && obj.message) {
|
|
5046
|
-
const content = (obj.message.content ?? []).filter(
|
|
5047
|
-
(b) => b.type === "text" || b.type === "tool_use" || b.type === "thinking"
|
|
5048
|
-
);
|
|
5049
|
-
if (content.length === 0) continue;
|
|
5050
|
-
events.push({
|
|
5051
|
-
type: "assistant",
|
|
5052
|
-
message: {
|
|
5053
|
-
id: obj.message.id ?? obj.uuid ?? `hist-${events.length}`,
|
|
5054
|
-
model: obj.message.model ?? "unknown",
|
|
5055
|
-
role: "assistant",
|
|
5056
|
-
content,
|
|
5057
|
-
stop_reason: obj.message.stop_reason,
|
|
5058
|
-
usage: obj.message.usage
|
|
5059
|
-
},
|
|
5060
|
-
session_id: sessionId
|
|
5061
|
-
});
|
|
5062
|
-
}
|
|
5063
|
-
} catch {
|
|
5380
|
+
if (targetTokens.length === 0) return;
|
|
5381
|
+
console.log(`[ActivityPushChannel] Alert push \u2192 ${targetTokens.length}/${this.alertTokens.size} device(s)${isCompletionNotif ? " (forced)" : ""}`);
|
|
5382
|
+
await Promise.all(targetTokens.map(async (deviceToken) => {
|
|
5383
|
+
let sound = payload.sound ?? "default";
|
|
5384
|
+
const prefs = this.alertSoundPreferences.get(deviceToken);
|
|
5385
|
+
if (prefs) {
|
|
5386
|
+
const notifType = payload.data?.type ?? "";
|
|
5387
|
+
if (notifType === "approval_request" && prefs.approval) sound = prefs.approval;
|
|
5388
|
+
else if (notifType === "task_complete" && prefs.taskComplete) sound = prefs.taskComplete;
|
|
5389
|
+
else if (notifType === "task_error" && prefs.taskError) sound = prefs.taskError;
|
|
5064
5390
|
}
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5391
|
+
const apnsPayload = {
|
|
5392
|
+
aps: {
|
|
5393
|
+
alert: { title: payload.title, ...payload.subtitle ? { subtitle: payload.subtitle } : {}, body: payload.body },
|
|
5394
|
+
...payload.badge !== void 0 ? { badge: payload.badge } : {},
|
|
5395
|
+
...sound && sound !== "none" ? { sound } : {},
|
|
5396
|
+
...payload.categoryId ? { category: payload.categoryId } : {}
|
|
5397
|
+
},
|
|
5398
|
+
...payload.data ?? {}
|
|
5399
|
+
};
|
|
5400
|
+
try {
|
|
5401
|
+
await this.sendAlertToAPNs(deviceToken, apnsPayload);
|
|
5402
|
+
} catch (err) {
|
|
5403
|
+
console.warn(`[ActivityPushChannel] Alert push failed for token ${deviceToken.slice(0, 16)}\u2026:`, err instanceof Error ? err.message : err);
|
|
5074
5404
|
}
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5405
|
+
}));
|
|
5406
|
+
}
|
|
5407
|
+
/** isAvailable 实现(NotificationChannel 接口) */
|
|
5408
|
+
isAvailable() {
|
|
5409
|
+
return this.alertTokens.size > 0;
|
|
5410
|
+
}
|
|
5411
|
+
/**
|
|
5412
|
+
* 发送 alert 通知到指定 device token,自动探测 sandbox/production。
|
|
5413
|
+
* 使用独立的 alertTokenEnv 映射(与 LA token 环境探测隔离)。
|
|
5414
|
+
*/
|
|
5415
|
+
async sendAlertToAPNs(deviceToken, payload) {
|
|
5416
|
+
const known = this.alertTokenEnv.get(deviceToken);
|
|
5417
|
+
if (known) {
|
|
5418
|
+
return this.sendToAPNsOnce(deviceToken, payload, { priority: "10", pushType: "alert", topic: this.bundleId }, known);
|
|
5419
|
+
}
|
|
5420
|
+
const short = deviceToken.slice(0, 16);
|
|
5421
|
+
console.log(`[ActivityPushChannel] \u{1F50D} alert probe start token=${short}\u2026 order=[${this.probeOrder.join(",")}]`);
|
|
5422
|
+
let lastErr = null;
|
|
5423
|
+
for (const env of this.probeOrder) {
|
|
5424
|
+
try {
|
|
5425
|
+
console.log(`[ActivityPushChannel] \u{1F50D} alert probe try ${env} token=${short}\u2026`);
|
|
5426
|
+
await this.sendToAPNsOnce(deviceToken, payload, { priority: "10", pushType: "alert", topic: this.bundleId }, env);
|
|
5427
|
+
this.alertTokenEnv.set(deviceToken, env);
|
|
5428
|
+
console.log(`[ActivityPushChannel] \u2705 alert probe bound to ${env} (token=${short}\u2026)`);
|
|
5429
|
+
return;
|
|
5430
|
+
} catch (err) {
|
|
5431
|
+
lastErr = err;
|
|
5432
|
+
const reason = err instanceof ApnsError ? JSON.parse(err.responseBody || "{}")?.reason ?? err.statusCode : String(err);
|
|
5433
|
+
console.log(`[ActivityPushChannel] \u{1F50D} alert probe ${env} failed: ${reason}`);
|
|
5434
|
+
if (isProviderTokenError(err)) throw err;
|
|
5435
|
+
if (!isBadDeviceTokenError(err)) throw err;
|
|
5086
5436
|
}
|
|
5087
5437
|
}
|
|
5088
|
-
|
|
5089
|
-
} catch (err) {
|
|
5090
|
-
return {
|
|
5091
|
-
ok: false,
|
|
5092
|
-
error: err instanceof Error ? err : new Error(String(err))
|
|
5093
|
-
};
|
|
5438
|
+
throw lastErr ?? new Error("APNs alert send failed: all environments rejected token");
|
|
5094
5439
|
}
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
const
|
|
5106
|
-
|
|
5440
|
+
/**
|
|
5441
|
+
* 发送 APNs,自动处理环境探测。
|
|
5442
|
+
* 对每个 token:先用已确认的环境(如有);否则按 probeOrder 顺序探测,
|
|
5443
|
+
* 收到 BadDeviceToken / BadEnvironmentKeyInToken 自动切到另一个环境,
|
|
5444
|
+
* 并把成功的环境绑定到该 token。
|
|
5445
|
+
*/
|
|
5446
|
+
async sendToAPNs(deviceToken, payload, opts = {}) {
|
|
5447
|
+
if (this.deadTokens.has(deviceToken)) {
|
|
5448
|
+
throw new Error(`token permanently dead (Activity ended): ${deviceToken.slice(0, 16)}\u2026`);
|
|
5449
|
+
}
|
|
5450
|
+
const known = this.tokenEnv.get(deviceToken);
|
|
5451
|
+
if (known) {
|
|
5452
|
+
return this.sendToAPNsOnce(deviceToken, payload, opts, known);
|
|
5453
|
+
}
|
|
5454
|
+
const short = deviceToken.slice(0, 16);
|
|
5455
|
+
console.log(`[ActivityPushChannel] \u{1F50D} probe start token=${short}\u2026 order=[${this.probeOrder.join(",")}]`);
|
|
5456
|
+
let lastErr = null;
|
|
5457
|
+
for (const env of this.probeOrder) {
|
|
5107
5458
|
try {
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5459
|
+
console.log(`[ActivityPushChannel] \u{1F50D} probe try ${env} token=${short}\u2026`);
|
|
5460
|
+
await this.sendToAPNsOnce(deviceToken, payload, opts, env);
|
|
5461
|
+
this.tokenEnv.set(deviceToken, env);
|
|
5462
|
+
console.log(`[ActivityPushChannel] \u2705 probe bound to ${env} (token=${short}\u2026)`);
|
|
5463
|
+
return;
|
|
5464
|
+
} catch (err) {
|
|
5465
|
+
lastErr = err;
|
|
5466
|
+
const reason = err instanceof ApnsError ? JSON.parse(err.responseBody || "{}")?.reason ?? err.statusCode : String(err);
|
|
5467
|
+
console.log(`[ActivityPushChannel] \u{1F50D} probe ${env} failed: ${reason}`);
|
|
5468
|
+
if (isProviderTokenError(err)) {
|
|
5469
|
+
console.error(
|
|
5470
|
+
`[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`
|
|
5471
|
+
);
|
|
5472
|
+
throw err;
|
|
5473
|
+
}
|
|
5474
|
+
if (!isBadDeviceTokenError(err)) {
|
|
5475
|
+
throw err;
|
|
5112
5476
|
}
|
|
5113
|
-
} catch {
|
|
5114
5477
|
}
|
|
5115
5478
|
}
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5479
|
+
this.deadTokens.add(deviceToken);
|
|
5480
|
+
for (const [sid, tok] of this.tokens) {
|
|
5481
|
+
if (tok === deviceToken) {
|
|
5482
|
+
this.tokens.delete(sid);
|
|
5483
|
+
console.warn(`[ActivityPushChannel] \u2620\uFE0F dead token, session removed: session=${sid.slice(0, 8)}\u2026 token=${short}\u2026 (Activity ended/iOS reinstalled)`);
|
|
5484
|
+
break;
|
|
5485
|
+
}
|
|
5486
|
+
}
|
|
5487
|
+
throw lastErr ?? new Error("APNs send failed: all environments rejected token");
|
|
5119
5488
|
}
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
const
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
});
|
|
5130
|
-
let lineCount = 0;
|
|
5131
|
-
for await (const line of rl) {
|
|
5132
|
-
if (++lineCount > 20) break;
|
|
5133
|
-
if (!line.trim()) continue;
|
|
5489
|
+
/** 单次 APNs HTTP/2 请求(内部 helper,不做探测) */
|
|
5490
|
+
async sendToAPNsOnce(deviceToken, payload, opts, env) {
|
|
5491
|
+
const topic = opts.topic ?? `${this.bundleId}.push-type.liveactivity`;
|
|
5492
|
+
const pushType = opts.pushType ?? "liveactivity";
|
|
5493
|
+
const jwt = this.getJWT();
|
|
5494
|
+
const payloadStr = JSON.stringify(payload);
|
|
5495
|
+
const priority = opts.priority ?? "10";
|
|
5496
|
+
return new Promise((resolve, reject) => {
|
|
5497
|
+
let client;
|
|
5134
5498
|
try {
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5499
|
+
client = this.getHttp2Client(env);
|
|
5500
|
+
} catch (err) {
|
|
5501
|
+
return reject(err);
|
|
5502
|
+
}
|
|
5503
|
+
const headers = {
|
|
5504
|
+
":method": "POST",
|
|
5505
|
+
":path": `/3/device/${deviceToken}`,
|
|
5506
|
+
"authorization": `bearer ${jwt}`,
|
|
5507
|
+
"apns-topic": topic,
|
|
5508
|
+
"apns-push-type": pushType,
|
|
5509
|
+
"apns-priority": priority,
|
|
5510
|
+
"apns-expiration": String(Math.floor(Date.now() / 1e3) + 300),
|
|
5511
|
+
"content-type": "application/json",
|
|
5512
|
+
"content-length": Buffer.byteLength(payloadStr)
|
|
5513
|
+
};
|
|
5514
|
+
if (opts.collapseId) {
|
|
5515
|
+
headers["apns-collapse-id"] = opts.collapseId;
|
|
5516
|
+
}
|
|
5517
|
+
const req = client.request(headers);
|
|
5518
|
+
req.setTimeout(1e4, () => {
|
|
5519
|
+
req.close(http2.constants.NGHTTP2_CANCEL);
|
|
5520
|
+
});
|
|
5521
|
+
let statusCode = 0;
|
|
5522
|
+
let responseData = "";
|
|
5523
|
+
req.on("response", (headers2) => {
|
|
5524
|
+
statusCode = Number(headers2[":status"] ?? 0);
|
|
5525
|
+
});
|
|
5526
|
+
req.on("data", (chunk) => {
|
|
5527
|
+
responseData += chunk;
|
|
5528
|
+
});
|
|
5529
|
+
req.on("end", () => {
|
|
5530
|
+
if (statusCode === 200) {
|
|
5531
|
+
resolve();
|
|
5532
|
+
} else {
|
|
5533
|
+
if (statusCode === 0) {
|
|
5534
|
+
const c = this.http2Clients[env];
|
|
5535
|
+
c?.destroy();
|
|
5536
|
+
delete this.http2Clients[env];
|
|
5150
5537
|
}
|
|
5538
|
+
reject(new ApnsError(statusCode, responseData));
|
|
5151
5539
|
}
|
|
5152
|
-
}
|
|
5153
|
-
|
|
5540
|
+
});
|
|
5541
|
+
req.on("error", (err) => {
|
|
5542
|
+
reject(err);
|
|
5543
|
+
});
|
|
5544
|
+
req.write(payloadStr);
|
|
5545
|
+
req.end();
|
|
5546
|
+
});
|
|
5547
|
+
}
|
|
5548
|
+
/** 生成或获取缓存的 APNs JWT token */
|
|
5549
|
+
getJWT() {
|
|
5550
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
5551
|
+
if (this.cachedJwt && this.cachedJwt.expiresAt > now) {
|
|
5552
|
+
return this.cachedJwt.token;
|
|
5154
5553
|
}
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5554
|
+
const header = Buffer.from(JSON.stringify({
|
|
5555
|
+
alg: "ES256",
|
|
5556
|
+
kid: this.keyId
|
|
5557
|
+
})).toString("base64url");
|
|
5558
|
+
const claims = Buffer.from(JSON.stringify({
|
|
5559
|
+
iss: this.teamId,
|
|
5560
|
+
iat: now
|
|
5561
|
+
})).toString("base64url");
|
|
5562
|
+
const signingInput = `${header}.${claims}`;
|
|
5563
|
+
const sign = crypto.createSign("SHA256");
|
|
5564
|
+
sign.update(signingInput);
|
|
5565
|
+
const signature = sign.sign({ key: this.authKey, dsaEncoding: "ieee-p1363" }, "base64url");
|
|
5566
|
+
const token = `${signingInput}.${signature}`;
|
|
5567
|
+
this.cachedJwt = { token, expiresAt: now + 3e3 };
|
|
5568
|
+
return token;
|
|
5158
5569
|
}
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
}
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
async function directoryExists(dirPath) {
|
|
5570
|
+
};
|
|
5571
|
+
var ApnsError = class extends Error {
|
|
5572
|
+
constructor(statusCode, responseBody) {
|
|
5573
|
+
super(`APNs returned ${statusCode}: ${responseBody}`);
|
|
5574
|
+
this.statusCode = statusCode;
|
|
5575
|
+
this.responseBody = responseBody;
|
|
5576
|
+
this.name = "ApnsError";
|
|
5577
|
+
}
|
|
5578
|
+
};
|
|
5579
|
+
function isProviderTokenError(err) {
|
|
5580
|
+
if (!(err instanceof ApnsError)) return false;
|
|
5581
|
+
if (err.statusCode !== 403) return false;
|
|
5172
5582
|
try {
|
|
5173
|
-
const
|
|
5174
|
-
return
|
|
5583
|
+
const parsed = JSON.parse(err.responseBody);
|
|
5584
|
+
return parsed.reason === "InvalidProviderToken" || parsed.reason === "ExpiredProviderToken";
|
|
5175
5585
|
} catch {
|
|
5176
5586
|
return false;
|
|
5177
5587
|
}
|
|
5178
5588
|
}
|
|
5179
|
-
|
|
5180
|
-
|
|
5589
|
+
function isBadDeviceTokenError(err) {
|
|
5590
|
+
if (!(err instanceof ApnsError)) return false;
|
|
5591
|
+
if (err.statusCode !== 400 && err.statusCode !== 403 && err.statusCode !== 410) return false;
|
|
5181
5592
|
try {
|
|
5182
|
-
const
|
|
5183
|
-
|
|
5184
|
-
entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => e.name.slice(0, -6))
|
|
5185
|
-
);
|
|
5186
|
-
const uuidDirs = entries.filter(
|
|
5187
|
-
(e) => e.isDirectory() && UUID_RE.test(e.name) && !jsonlNames.has(e.name)
|
|
5188
|
-
);
|
|
5189
|
-
let latestMtime = 0;
|
|
5190
|
-
const jsonlEntries = entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl"));
|
|
5191
|
-
await Promise.all([
|
|
5192
|
-
...jsonlEntries.map(async (entry) => {
|
|
5193
|
-
try {
|
|
5194
|
-
const contentTs = await extractLastTimestamp((0, import_path2.join)(dirPath, entry.name));
|
|
5195
|
-
const ts = contentTs ?? (await (0, import_promises3.stat)((0, import_path2.join)(dirPath, entry.name))).mtimeMs;
|
|
5196
|
-
if (ts > latestMtime) latestMtime = ts;
|
|
5197
|
-
} catch {
|
|
5198
|
-
}
|
|
5199
|
-
}),
|
|
5200
|
-
...uuidDirs.map(async (entry) => {
|
|
5201
|
-
try {
|
|
5202
|
-
const fileStat = await (0, import_promises3.stat)((0, import_path2.join)(dirPath, entry.name));
|
|
5203
|
-
if (fileStat.mtimeMs > latestMtime) latestMtime = fileStat.mtimeMs;
|
|
5204
|
-
} catch {
|
|
5205
|
-
}
|
|
5206
|
-
})
|
|
5207
|
-
]);
|
|
5208
|
-
return { count: jsonlNames.size + uuidDirs.length, latestMtime };
|
|
5593
|
+
const parsed = JSON.parse(err.responseBody);
|
|
5594
|
+
return parsed.reason === "BadDeviceToken" || parsed.reason === "BadEnvironmentKeyInToken" || parsed.reason === "Unregistered";
|
|
5209
5595
|
} catch {
|
|
5210
|
-
return
|
|
5596
|
+
return false;
|
|
5211
5597
|
}
|
|
5212
5598
|
}
|
|
5213
5599
|
|
|
@@ -5333,7 +5719,10 @@ var AuthManager = class extends import_events3.EventEmitter {
|
|
|
5333
5719
|
email: parsed.email,
|
|
5334
5720
|
authMethod: parsed.authMethod
|
|
5335
5721
|
};
|
|
5336
|
-
} catch {
|
|
5722
|
+
} catch (err) {
|
|
5723
|
+
console.warn(
|
|
5724
|
+
`[AuthManager] checkAuth \u5931\u8D25 (claudePath=${CLAUDE_PATH2}): ${err instanceof Error ? err.message : String(err)}`
|
|
5725
|
+
);
|
|
5337
5726
|
return { loggedIn: false };
|
|
5338
5727
|
}
|
|
5339
5728
|
}
|
|
@@ -5429,7 +5818,7 @@ var AuthManager = class extends import_events3.EventEmitter {
|
|
|
5429
5818
|
|
|
5430
5819
|
// src/terminal/TerminalExecutor.ts
|
|
5431
5820
|
var import_node_child_process8 = require("child_process");
|
|
5432
|
-
var
|
|
5821
|
+
var import_uuid4 = require("uuid");
|
|
5433
5822
|
var EXEC_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
5434
5823
|
var TerminalExecutor = class {
|
|
5435
5824
|
processes = /* @__PURE__ */ new Map();
|
|
@@ -5451,7 +5840,7 @@ var TerminalExecutor = class {
|
|
|
5451
5840
|
}
|
|
5452
5841
|
}
|
|
5453
5842
|
exec(sessionId, command, cwd) {
|
|
5454
|
-
const execId = (0,
|
|
5843
|
+
const execId = (0, import_uuid4.v4)();
|
|
5455
5844
|
const shell = isWindows ? "powershell" : process.env.SHELL || "/bin/zsh";
|
|
5456
5845
|
const args = isWindows ? ["-Command", command] : ["-l", "-c", command];
|
|
5457
5846
|
const proc = (0, import_node_child_process8.spawn)(shell, args, {
|
|
@@ -5528,7 +5917,7 @@ var import_node_util = require("util");
|
|
|
5528
5917
|
var import_promises4 = require("fs/promises");
|
|
5529
5918
|
var import_node_path6 = require("path");
|
|
5530
5919
|
var import_node_os7 = require("os");
|
|
5531
|
-
var
|
|
5920
|
+
var import_uuid5 = require("uuid");
|
|
5532
5921
|
var execAsync = (0, import_node_util.promisify)(import_node_child_process9.exec);
|
|
5533
5922
|
var BUILD_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
5534
5923
|
var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
@@ -5716,7 +6105,7 @@ ${e.stderr ?? ""}`);
|
|
|
5716
6105
|
return null;
|
|
5717
6106
|
}
|
|
5718
6107
|
if (override) await this.saveConfig(projectPath, override);
|
|
5719
|
-
const buildId = (0,
|
|
6108
|
+
const buildId = (0, import_uuid5.v4)();
|
|
5720
6109
|
const args = buildArgs(config);
|
|
5721
6110
|
const proc = (0, import_node_child_process9.spawn)("xcodebuild", args, {
|
|
5722
6111
|
cwd: projectPath,
|
|
@@ -5780,7 +6169,7 @@ ${e.stderr ?? ""}`);
|
|
|
5780
6169
|
this.emitInstallError(sessionId, "", "\u672A\u627E\u5230\u6784\u5EFA\u914D\u7F6E\uFF0C\u8BF7\u5148\u6784\u5EFA\u4E00\u6B21\n");
|
|
5781
6170
|
return null;
|
|
5782
6171
|
}
|
|
5783
|
-
const installId = (0,
|
|
6172
|
+
const installId = (0, import_uuid5.v4)();
|
|
5784
6173
|
let appPath;
|
|
5785
6174
|
try {
|
|
5786
6175
|
appPath = await this.getAppPath(projectPath, config);
|
|
@@ -6351,7 +6740,7 @@ function sourceWeight(s) {
|
|
|
6351
6740
|
// src/git/GitExecutor.ts
|
|
6352
6741
|
var import_node_child_process10 = require("child_process");
|
|
6353
6742
|
var import_node_util2 = require("util");
|
|
6354
|
-
var
|
|
6743
|
+
var import_uuid6 = require("uuid");
|
|
6355
6744
|
var execAsync2 = (0, import_node_util2.promisify)(import_node_child_process10.exec);
|
|
6356
6745
|
var STATUS_TIMEOUT_MS = 15e3;
|
|
6357
6746
|
var COMMIT_TIMEOUT_MS = 6e4;
|
|
@@ -6471,7 +6860,7 @@ var GitExecutor = class {
|
|
|
6471
6860
|
* - 若未提供 files:默认 git add -A(提交所有变更)
|
|
6472
6861
|
*/
|
|
6473
6862
|
async commit(sessionId, projectPath, message, files, alsoPush) {
|
|
6474
|
-
const opId = (0,
|
|
6863
|
+
const opId = (0, import_uuid6.v4)();
|
|
6475
6864
|
this.runSequence(sessionId, opId, "commit", projectPath, [
|
|
6476
6865
|
files && files.length > 0 ? ["git", "add", "--", ...files] : ["git", "add", "-A"],
|
|
6477
6866
|
["git", "commit", "-m", message]
|
|
@@ -6487,7 +6876,7 @@ var GitExecutor = class {
|
|
|
6487
6876
|
return opId;
|
|
6488
6877
|
}
|
|
6489
6878
|
async push(sessionId, projectPath) {
|
|
6490
|
-
const opId = (0,
|
|
6879
|
+
const opId = (0, import_uuid6.v4)();
|
|
6491
6880
|
this.runSequence(sessionId, opId, "push", projectPath, [
|
|
6492
6881
|
["git", "push"]
|
|
6493
6882
|
], PUSH_TIMEOUT_MS).catch((err) => {
|
|
@@ -6565,7 +6954,7 @@ var GitExecutor = class {
|
|
|
6565
6954
|
var import_promises6 = require("fs/promises");
|
|
6566
6955
|
var import_node_os8 = require("os");
|
|
6567
6956
|
var import_node_path8 = require("path");
|
|
6568
|
-
var
|
|
6957
|
+
var import_uuid7 = require("uuid");
|
|
6569
6958
|
var MAX_TIMEOUT_MS = 2147483647;
|
|
6570
6959
|
var ScheduledSessionManager = class {
|
|
6571
6960
|
tasks = /* @__PURE__ */ new Map();
|
|
@@ -6603,7 +6992,7 @@ var ScheduledSessionManager = class {
|
|
|
6603
6992
|
/** 注册一个定时任务(payload 由调用方校验) */
|
|
6604
6993
|
schedule(scheduledAt, payload) {
|
|
6605
6994
|
const task = {
|
|
6606
|
-
id: (0,
|
|
6995
|
+
id: (0, import_uuid7.v4)(),
|
|
6607
6996
|
scheduledAt,
|
|
6608
6997
|
createdAt: Date.now(),
|
|
6609
6998
|
payload
|
|
@@ -6708,6 +7097,7 @@ function isValidTask(value) {
|
|
|
6708
7097
|
var import_node_child_process11 = require("child_process");
|
|
6709
7098
|
var DEFAULT_MODELS = [
|
|
6710
7099
|
{ value: "opus", label: "Opus 4.7", sublabel: "Most capable for ambitious work" },
|
|
7100
|
+
{ value: "claude-opus-4-6", label: "Opus 4.6", sublabel: "Previous generation flagship" },
|
|
6711
7101
|
{ value: "sonnet", label: "Sonnet 4.6", sublabel: "Most efficient for everyday tasks" },
|
|
6712
7102
|
{ value: "haiku", label: "Haiku 4.5", sublabel: "Fastest for quick answers" }
|
|
6713
7103
|
];
|
|
@@ -6842,7 +7232,7 @@ async function start(opts = {}) {
|
|
|
6842
7232
|
try {
|
|
6843
7233
|
token = (await (0, import_promises7.readFile)(tokenFile, "utf8")).trim();
|
|
6844
7234
|
} catch {
|
|
6845
|
-
token = (0,
|
|
7235
|
+
token = (0, import_uuid8.v4)();
|
|
6846
7236
|
await (0, import_promises7.mkdir)(configDir, { recursive: true });
|
|
6847
7237
|
await (0, import_promises7.writeFile)(tokenFile, token, "utf8");
|
|
6848
7238
|
}
|
|
@@ -7034,6 +7424,7 @@ async function start(opts = {}) {
|
|
|
7034
7424
|
case "kill_session": {
|
|
7035
7425
|
wsBridge.broadcast({ type: "status_change", sessionId: event.sessionId, status: "idle" });
|
|
7036
7426
|
approvalProxy.clearPendingForSession(event.sessionId);
|
|
7427
|
+
approvalProxy.clearPendingQuestionsForSession(event.sessionId);
|
|
7037
7428
|
await sessionManager.killSession(event.sessionId);
|
|
7038
7429
|
wsBridge.broadcast({
|
|
7039
7430
|
type: "session_list",
|
|
@@ -7052,6 +7443,7 @@ async function start(opts = {}) {
|
|
|
7052
7443
|
}
|
|
7053
7444
|
case "answer_question": {
|
|
7054
7445
|
sessionManager.handleQuestionResponse(event.requestId, event.answer);
|
|
7446
|
+
approvalProxy.resolveQuestion(event.requestId, event.answer);
|
|
7055
7447
|
break;
|
|
7056
7448
|
}
|
|
7057
7449
|
case "subscribe": {
|
|
@@ -7236,6 +7628,14 @@ async function start(opts = {}) {
|
|
|
7236
7628
|
notificationService.removePushToken(event.token);
|
|
7237
7629
|
break;
|
|
7238
7630
|
}
|
|
7631
|
+
case "register_native_push_token": {
|
|
7632
|
+
notificationService.addNativePushToken(event.token, ws);
|
|
7633
|
+
break;
|
|
7634
|
+
}
|
|
7635
|
+
case "unregister_native_push_token": {
|
|
7636
|
+
notificationService.removeNativePushToken(event.token);
|
|
7637
|
+
break;
|
|
7638
|
+
}
|
|
7239
7639
|
case "update_notification_sounds": {
|
|
7240
7640
|
notificationService.setSoundPreferences(event.preferences);
|
|
7241
7641
|
break;
|
|
@@ -7473,6 +7873,9 @@ async function start(opts = {}) {
|
|
|
7473
7873
|
decision: decision.decision
|
|
7474
7874
|
});
|
|
7475
7875
|
});
|
|
7876
|
+
approvalProxy.onQuestionResolved((requestId) => {
|
|
7877
|
+
sessionManager.clearPendingQuestion(requestId);
|
|
7878
|
+
});
|
|
7476
7879
|
approvalProxy.onApprovalRequest((request) => {
|
|
7477
7880
|
wsBridge.broadcast({ type: "approval_request", request });
|
|
7478
7881
|
setTimeout(() => {
|
|
@@ -7489,6 +7892,9 @@ async function start(opts = {}) {
|
|
|
7489
7892
|
notificationService.notifyApproval(request, pendingCount);
|
|
7490
7893
|
}, 6e4);
|
|
7491
7894
|
});
|
|
7895
|
+
approvalProxy.setQuestionHandler(
|
|
7896
|
+
(sessionId, toolUseId, questions, requestId) => sessionManager.askQuestion(sessionId, toolUseId, questions, requestId)
|
|
7897
|
+
);
|
|
7492
7898
|
sessionManager.onEvent((event) => {
|
|
7493
7899
|
if (event.type !== "question_request") return;
|
|
7494
7900
|
const { request } = event;
|
|
@@ -7631,7 +8037,7 @@ async function start(opts = {}) {
|
|
|
7631
8037
|
openPairing: (duration) => pairingManager.open(duration),
|
|
7632
8038
|
closePairing: () => pairingManager.close(),
|
|
7633
8039
|
regenerateToken: async () => {
|
|
7634
|
-
const newToken = (0,
|
|
8040
|
+
const newToken = (0, import_uuid8.v4)();
|
|
7635
8041
|
await (0, import_promises7.mkdir)(configDir, { recursive: true });
|
|
7636
8042
|
await (0, import_promises7.writeFile)(tokenFile, newToken, "utf8");
|
|
7637
8043
|
instance.token = newToken;
|