codex-to-im 1.0.59 → 1.0.61
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/cli.mjs +211 -74
- package/dist/daemon.mjs +1685 -707
- package/dist/ui-server.mjs +857 -463
- package/package.json +9 -8
- package/scripts/patch-codex-sdk-windows-hide.js +14 -3
package/dist/daemon.mjs
CHANGED
|
@@ -321,11 +321,50 @@ var init_sse_utils = __esm({
|
|
|
321
321
|
// src/codex-provider.ts
|
|
322
322
|
var codex_provider_exports = {};
|
|
323
323
|
__export(codex_provider_exports, {
|
|
324
|
-
CodexProvider: () => CodexProvider
|
|
324
|
+
CodexProvider: () => CodexProvider,
|
|
325
|
+
mapCodexUsage: () => mapCodexUsage
|
|
325
326
|
});
|
|
326
|
-
import
|
|
327
|
+
import fs17 from "node:fs";
|
|
327
328
|
import os4 from "node:os";
|
|
328
|
-
import
|
|
329
|
+
import path18 from "node:path";
|
|
330
|
+
function isRecord(value) {
|
|
331
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
332
|
+
}
|
|
333
|
+
function normalizeTokenCount(value) {
|
|
334
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
335
|
+
}
|
|
336
|
+
function mapCodexUsage(usage) {
|
|
337
|
+
if (!isRecord(usage)) return void 0;
|
|
338
|
+
const cacheRead = usage.cached_input_tokens ?? usage.cache_read_input_tokens;
|
|
339
|
+
const cacheCreation = usage.cache_write_input_tokens ?? usage.cache_creation_input_tokens;
|
|
340
|
+
return {
|
|
341
|
+
input_tokens: normalizeTokenCount(usage.input_tokens),
|
|
342
|
+
output_tokens: normalizeTokenCount(usage.output_tokens),
|
|
343
|
+
cache_read_input_tokens: normalizeTokenCount(cacheRead),
|
|
344
|
+
reasoning_output_tokens: normalizeTokenCount(usage.reasoning_output_tokens),
|
|
345
|
+
...cacheCreation === void 0 ? {} : { cache_creation_input_tokens: normalizeTokenCount(cacheCreation) }
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function isMissingCodexPackageError(error) {
|
|
349
|
+
if (!isRecord(error)) return false;
|
|
350
|
+
const code2 = typeof error.code === "string" ? error.code : "";
|
|
351
|
+
const message = typeof error.message === "string" ? error.message : "";
|
|
352
|
+
return (code2 === "ERR_MODULE_NOT_FOUND" || code2 === "MODULE_NOT_FOUND") && /@openai[\\/]codex(?:-sdk)?/.test(message);
|
|
353
|
+
}
|
|
354
|
+
function assertCodexClient(value) {
|
|
355
|
+
if (!isRecord(value) || typeof value.startThread !== "function" || typeof value.resumeThread !== "function") {
|
|
356
|
+
throw new Error(
|
|
357
|
+
"[CodexProvider] Incompatible @openai/codex-sdk: Codex client does not expose startThread() and resumeThread()."
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function assertCodexThread(value) {
|
|
362
|
+
if (!isRecord(value) || typeof value.runStreamed !== "function") {
|
|
363
|
+
throw new Error(
|
|
364
|
+
"[CodexProvider] Incompatible @openai/codex-sdk: thread does not expose runStreamed()."
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
329
368
|
function toApprovalPolicy(permissionMode) {
|
|
330
369
|
switch (permissionMode) {
|
|
331
370
|
case "never":
|
|
@@ -441,20 +480,37 @@ var init_codex_provider = __esm({
|
|
|
441
480
|
if (this.sdk && this.codex) {
|
|
442
481
|
return { sdk: this.sdk, codex: this.codex };
|
|
443
482
|
}
|
|
483
|
+
let sdk;
|
|
444
484
|
try {
|
|
445
|
-
|
|
446
|
-
} catch {
|
|
485
|
+
sdk = await Function('return import("@openai/codex-sdk")')();
|
|
486
|
+
} catch (error) {
|
|
487
|
+
if (isMissingCodexPackageError(error)) {
|
|
488
|
+
throw new Error(
|
|
489
|
+
"[CodexProvider] @openai/codex-sdk is missing from this codex-to-im installation. Reinstall codex-to-im or run npm install in the project root.",
|
|
490
|
+
{ cause: error }
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
494
|
+
throw new Error(
|
|
495
|
+
`[CodexProvider] Failed to load @openai/codex-sdk: ${detail}`,
|
|
496
|
+
{ cause: error }
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
if (!sdk || typeof sdk.Codex !== "function") {
|
|
447
500
|
throw new Error(
|
|
448
|
-
"[CodexProvider] @openai/codex-sdk
|
|
501
|
+
"[CodexProvider] Incompatible @openai/codex-sdk: Codex export is unavailable."
|
|
449
502
|
);
|
|
450
503
|
}
|
|
504
|
+
this.sdk = sdk;
|
|
451
505
|
const apiKey = process.env.CTI_CODEX_API_KEY || process.env.CODEX_API_KEY || process.env.OPENAI_API_KEY || void 0;
|
|
452
506
|
const baseUrl = process.env.CTI_CODEX_BASE_URL || void 0;
|
|
453
|
-
const CodexClass =
|
|
454
|
-
|
|
507
|
+
const CodexClass = sdk.Codex;
|
|
508
|
+
const codex = new CodexClass({
|
|
455
509
|
...apiKey ? { apiKey } : {},
|
|
456
510
|
...baseUrl ? { baseUrl } : {}
|
|
457
511
|
});
|
|
512
|
+
assertCodexClient(codex);
|
|
513
|
+
this.codex = codex;
|
|
458
514
|
return { sdk: this.sdk, codex: this.codex };
|
|
459
515
|
}
|
|
460
516
|
streamChat(params) {
|
|
@@ -487,13 +543,13 @@ var init_codex_provider = __esm({
|
|
|
487
543
|
{ type: "text", text: params.prompt }
|
|
488
544
|
];
|
|
489
545
|
for (const file of imageFiles) {
|
|
490
|
-
if (file.filePath &&
|
|
546
|
+
if (file.filePath && fs17.existsSync(file.filePath)) {
|
|
491
547
|
parts.push({ type: "local_image", path: file.filePath });
|
|
492
548
|
continue;
|
|
493
549
|
}
|
|
494
550
|
const ext = MIME_EXT[file.type] || ".png";
|
|
495
|
-
const tmpPath =
|
|
496
|
-
|
|
551
|
+
const tmpPath = path18.join(os4.tmpdir(), `cti-img-${Date.now()}-${Math.random().toString(36).slice(2)}${ext}`);
|
|
552
|
+
fs17.writeFileSync(tmpPath, Buffer.from(file.data, "base64"));
|
|
497
553
|
tempFiles.push(tmpPath);
|
|
498
554
|
parts.push({ type: "local_image", path: tmpPath });
|
|
499
555
|
}
|
|
@@ -514,6 +570,7 @@ var init_codex_provider = __esm({
|
|
|
514
570
|
} else {
|
|
515
571
|
thread = codex.startThread(threadOptions);
|
|
516
572
|
}
|
|
573
|
+
assertCodexThread(thread);
|
|
517
574
|
let sawAnyEvent = false;
|
|
518
575
|
let sawTerminalEvent = false;
|
|
519
576
|
let sawCompletedAssistantContent = false;
|
|
@@ -581,15 +638,9 @@ var init_codex_provider = __esm({
|
|
|
581
638
|
break;
|
|
582
639
|
}
|
|
583
640
|
case "turn.completed": {
|
|
584
|
-
const usage = event.usage;
|
|
585
641
|
const threadId = self.threadIds.get(params.sessionId);
|
|
586
642
|
controller.enqueue(sseEvent("result", {
|
|
587
|
-
usage: usage
|
|
588
|
-
input_tokens: usage.input_tokens ?? 0,
|
|
589
|
-
output_tokens: usage.output_tokens ?? 0,
|
|
590
|
-
cache_read_input_tokens: usage.cached_input_tokens ?? 0,
|
|
591
|
-
reasoning_output_tokens: usage.reasoning_output_tokens ?? 0
|
|
592
|
-
} : void 0,
|
|
643
|
+
usage: mapCodexUsage(event.usage),
|
|
593
644
|
...threadId ? { session_id: threadId } : {}
|
|
594
645
|
}));
|
|
595
646
|
sawTerminalEvent = true;
|
|
@@ -610,10 +661,9 @@ var init_codex_provider = __esm({
|
|
|
610
661
|
break;
|
|
611
662
|
}
|
|
612
663
|
default: {
|
|
613
|
-
const exhaustiveEvent = event;
|
|
614
664
|
console.warn(
|
|
615
665
|
"[codex-provider] Unhandled thread event:",
|
|
616
|
-
stringifyUnknown(
|
|
666
|
+
stringifyUnknown(event)
|
|
617
667
|
);
|
|
618
668
|
break;
|
|
619
669
|
}
|
|
@@ -665,7 +715,7 @@ var init_codex_provider = __esm({
|
|
|
665
715
|
} finally {
|
|
666
716
|
for (const tmp of tempFiles) {
|
|
667
717
|
try {
|
|
668
|
-
|
|
718
|
+
fs17.unlinkSync(tmp);
|
|
669
719
|
} catch {
|
|
670
720
|
}
|
|
671
721
|
}
|
|
@@ -782,10 +832,9 @@ var init_codex_provider = __esm({
|
|
|
782
832
|
break;
|
|
783
833
|
}
|
|
784
834
|
default: {
|
|
785
|
-
const exhaustiveItem = item;
|
|
786
835
|
console.warn(
|
|
787
836
|
"[codex-provider] Unhandled thread item:",
|
|
788
|
-
stringifyUnknown(
|
|
837
|
+
stringifyUnknown(item)
|
|
789
838
|
);
|
|
790
839
|
break;
|
|
791
840
|
}
|
|
@@ -796,9 +845,9 @@ var init_codex_provider = __esm({
|
|
|
796
845
|
});
|
|
797
846
|
|
|
798
847
|
// src/main.ts
|
|
799
|
-
import
|
|
800
|
-
import
|
|
801
|
-
import
|
|
848
|
+
import fs18 from "node:fs";
|
|
849
|
+
import path19 from "node:path";
|
|
850
|
+
import crypto11 from "node:crypto";
|
|
802
851
|
|
|
803
852
|
// src/lib/bridge/context.ts
|
|
804
853
|
var CONTEXT_KEY = "__bridge_context__";
|
|
@@ -815,23 +864,175 @@ function getBridgeContext() {
|
|
|
815
864
|
return ctx;
|
|
816
865
|
}
|
|
817
866
|
|
|
867
|
+
// src/lib/bridge/channel-adapter.ts
|
|
868
|
+
function buildInboundDedupKey(channelType, messageId) {
|
|
869
|
+
return `inbound:${channelType}:${messageId}`;
|
|
870
|
+
}
|
|
871
|
+
var BaseChannelAdapter = class {
|
|
872
|
+
inboundQueue = [];
|
|
873
|
+
inboundWaiters = [];
|
|
874
|
+
/** Human-readable instance alias, when applicable. */
|
|
875
|
+
alias;
|
|
876
|
+
/**
|
|
877
|
+
* Answer a callback query or interactive-card action when supported.
|
|
878
|
+
* Not all platforms support this — default implementation is a no-op.
|
|
879
|
+
*/
|
|
880
|
+
async answerCallback(_callbackQueryId, _text) {
|
|
881
|
+
}
|
|
882
|
+
consumeInboundMessage(isRunning) {
|
|
883
|
+
const queued = this.inboundQueue.shift();
|
|
884
|
+
if (queued) return Promise.resolve(queued);
|
|
885
|
+
if (!isRunning) return Promise.resolve(null);
|
|
886
|
+
return new Promise((resolve2) => {
|
|
887
|
+
this.inboundWaiters.push(resolve2);
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
enqueueInboundMessage(message) {
|
|
891
|
+
const waiter = this.inboundWaiters.shift();
|
|
892
|
+
if (waiter) {
|
|
893
|
+
waiter(message);
|
|
894
|
+
} else {
|
|
895
|
+
this.inboundQueue.push(message);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
clearInboundQueue() {
|
|
899
|
+
this.inboundQueue = [];
|
|
900
|
+
}
|
|
901
|
+
rejectPendingInboundConsumers() {
|
|
902
|
+
for (const waiter of this.inboundWaiters) {
|
|
903
|
+
waiter(null);
|
|
904
|
+
}
|
|
905
|
+
this.inboundWaiters = [];
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
var adapterFactories = /* @__PURE__ */ new Map();
|
|
909
|
+
function registerAdapterFactory(provider, factory) {
|
|
910
|
+
adapterFactories.set(provider, factory);
|
|
911
|
+
}
|
|
912
|
+
function createAdapter(instance) {
|
|
913
|
+
const factory = adapterFactories.get(instance.provider);
|
|
914
|
+
return factory ? factory(instance) : null;
|
|
915
|
+
}
|
|
916
|
+
|
|
818
917
|
// src/lib/bridge/bridge-manager.ts
|
|
819
918
|
import { inspect } from "node:util";
|
|
820
919
|
|
|
821
920
|
// src/lib/bridge/adapters/feishu-adapter.ts
|
|
822
|
-
import
|
|
823
|
-
import
|
|
921
|
+
import crypto3 from "crypto";
|
|
922
|
+
import fs3 from "node:fs";
|
|
824
923
|
import path2 from "node:path";
|
|
825
924
|
import * as lark from "@larksuiteoapi/node-sdk";
|
|
826
925
|
|
|
827
926
|
// src/config.ts
|
|
828
927
|
init_runtime_options();
|
|
829
|
-
import
|
|
928
|
+
import fs2 from "node:fs";
|
|
929
|
+
import crypto2 from "node:crypto";
|
|
830
930
|
import os from "node:os";
|
|
831
931
|
import path from "node:path";
|
|
932
|
+
|
|
933
|
+
// src/file-lock.ts
|
|
934
|
+
import crypto from "node:crypto";
|
|
935
|
+
import fs from "node:fs";
|
|
936
|
+
var DEFAULT_LOCK_TIMEOUT_MS = 5e3;
|
|
937
|
+
var DEFAULT_STALE_LOCK_MS = 3e4;
|
|
938
|
+
var LOCK_RETRY_INTERVAL_MS = 25;
|
|
939
|
+
var WAIT_ARRAY = new Int32Array(new SharedArrayBuffer(4));
|
|
940
|
+
var ACTIVE_LOCKS = /* @__PURE__ */ new Map();
|
|
941
|
+
function sleepSync(ms) {
|
|
942
|
+
Atomics.wait(WAIT_ARRAY, 0, 0, Math.max(1, ms));
|
|
943
|
+
}
|
|
944
|
+
function isProcessAlive(pid) {
|
|
945
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
946
|
+
try {
|
|
947
|
+
process.kill(pid, 0);
|
|
948
|
+
return true;
|
|
949
|
+
} catch (error) {
|
|
950
|
+
return error.code === "EPERM";
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
function removeStaleLock(lockPath, staleAfterMs) {
|
|
954
|
+
try {
|
|
955
|
+
const stat = fs.statSync(lockPath);
|
|
956
|
+
if (Date.now() - stat.mtimeMs < staleAfterMs) return false;
|
|
957
|
+
let ownerPid = 0;
|
|
958
|
+
try {
|
|
959
|
+
const parsed = JSON.parse(fs.readFileSync(lockPath, "utf8"));
|
|
960
|
+
ownerPid = typeof parsed.pid === "number" ? parsed.pid : 0;
|
|
961
|
+
} catch {
|
|
962
|
+
}
|
|
963
|
+
if (ownerPid && isProcessAlive(ownerPid)) return false;
|
|
964
|
+
fs.rmSync(lockPath, { force: true });
|
|
965
|
+
return true;
|
|
966
|
+
} catch (error) {
|
|
967
|
+
return error.code === "ENOENT";
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
function withFileLock(targetPath, operation, options) {
|
|
971
|
+
const lockPath = `${targetPath}.lock`;
|
|
972
|
+
const activeDepth = ACTIVE_LOCKS.get(lockPath) || 0;
|
|
973
|
+
if (activeDepth > 0) {
|
|
974
|
+
ACTIVE_LOCKS.set(lockPath, activeDepth + 1);
|
|
975
|
+
try {
|
|
976
|
+
return operation();
|
|
977
|
+
} finally {
|
|
978
|
+
const nextDepth = (ACTIVE_LOCKS.get(lockPath) || 1) - 1;
|
|
979
|
+
if (nextDepth > 0) ACTIVE_LOCKS.set(lockPath, nextDepth);
|
|
980
|
+
else ACTIVE_LOCKS.delete(lockPath);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
const timeoutMs = Math.max(0, options?.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS);
|
|
984
|
+
const staleAfterMs = Math.max(timeoutMs, options?.staleAfterMs ?? DEFAULT_STALE_LOCK_MS);
|
|
985
|
+
const deadline = Date.now() + timeoutMs;
|
|
986
|
+
const record = {
|
|
987
|
+
token: crypto.randomUUID(),
|
|
988
|
+
pid: process.pid,
|
|
989
|
+
createdAt: Date.now()
|
|
990
|
+
};
|
|
991
|
+
let handle = null;
|
|
992
|
+
while (handle === null) {
|
|
993
|
+
try {
|
|
994
|
+
handle = fs.openSync(lockPath, "wx", 384);
|
|
995
|
+
fs.writeFileSync(handle, JSON.stringify(record), "utf8");
|
|
996
|
+
} catch (error) {
|
|
997
|
+
const code2 = error.code;
|
|
998
|
+
if (code2 !== "EEXIST") throw error;
|
|
999
|
+
if (removeStaleLock(lockPath, staleAfterMs)) continue;
|
|
1000
|
+
if (Date.now() >= deadline) {
|
|
1001
|
+
throw new Error(`Timed out waiting for file lock: ${lockPath}`);
|
|
1002
|
+
}
|
|
1003
|
+
sleepSync(Math.min(LOCK_RETRY_INTERVAL_MS, Math.max(1, deadline - Date.now())));
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
try {
|
|
1007
|
+
ACTIVE_LOCKS.set(lockPath, 1);
|
|
1008
|
+
return operation();
|
|
1009
|
+
} finally {
|
|
1010
|
+
ACTIVE_LOCKS.delete(lockPath);
|
|
1011
|
+
try {
|
|
1012
|
+
fs.closeSync(handle);
|
|
1013
|
+
} catch {
|
|
1014
|
+
}
|
|
1015
|
+
try {
|
|
1016
|
+
const current = JSON.parse(fs.readFileSync(lockPath, "utf8"));
|
|
1017
|
+
if (current.token === record.token) fs.rmSync(lockPath, { force: true });
|
|
1018
|
+
} catch {
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
function withFileLocks(targetPaths, operation) {
|
|
1023
|
+
const paths = Array.from(new Set(targetPaths)).sort();
|
|
1024
|
+
const acquire = (index) => {
|
|
1025
|
+
if (index >= paths.length) return operation();
|
|
1026
|
+
return withFileLock(paths[index], () => acquire(index + 1));
|
|
1027
|
+
};
|
|
1028
|
+
return acquire(0);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// src/config.ts
|
|
832
1032
|
function isSupportedChannelProvider(value) {
|
|
833
1033
|
return value === "feishu" || value === "weixin";
|
|
834
1034
|
}
|
|
1035
|
+
var RAW_CHANNELS = Symbol("rawConfigV2Channels");
|
|
835
1036
|
function toFeishuConfig(channel) {
|
|
836
1037
|
return channel?.provider === "feishu" ? channel.config : void 0;
|
|
837
1038
|
}
|
|
@@ -871,7 +1072,7 @@ function parseEnvFile(content) {
|
|
|
871
1072
|
}
|
|
872
1073
|
function loadRawConfigEnv() {
|
|
873
1074
|
try {
|
|
874
|
-
return parseEnvFile(
|
|
1075
|
+
return parseEnvFile(fs2.readFileSync(CONFIG_PATH, "utf-8"));
|
|
875
1076
|
} catch {
|
|
876
1077
|
return /* @__PURE__ */ new Map();
|
|
877
1078
|
}
|
|
@@ -901,26 +1102,66 @@ function nowIso() {
|
|
|
901
1102
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
902
1103
|
}
|
|
903
1104
|
function ensureConfigDir() {
|
|
904
|
-
|
|
1105
|
+
fs2.mkdirSync(CTI_HOME, { recursive: true });
|
|
905
1106
|
}
|
|
906
1107
|
function readConfigV2File() {
|
|
1108
|
+
if (!fs2.existsSync(CONFIG_V2_PATH)) return null;
|
|
1109
|
+
let content;
|
|
1110
|
+
try {
|
|
1111
|
+
content = fs2.readFileSync(CONFIG_V2_PATH, "utf-8");
|
|
1112
|
+
} catch (error) {
|
|
1113
|
+
throw new Error(`\u65E0\u6CD5\u8BFB\u53D6\u914D\u7F6E\u6587\u4EF6 ${CONFIG_V2_PATH}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1114
|
+
}
|
|
1115
|
+
let raw;
|
|
1116
|
+
try {
|
|
1117
|
+
raw = JSON.parse(content);
|
|
1118
|
+
} catch (error) {
|
|
1119
|
+
throw new Error(`\u914D\u7F6E\u6587\u4EF6 ${CONFIG_V2_PATH} \u4E0D\u662F\u6709\u6548 JSON\uFF0C\u5DF2\u62D2\u7EDD\u8986\u76D6: ${error instanceof Error ? error.message : String(error)}`);
|
|
1120
|
+
}
|
|
1121
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1122
|
+
throw new Error(`\u914D\u7F6E\u6587\u4EF6 ${CONFIG_V2_PATH} \u7684\u6839\u8282\u70B9\u5FC5\u987B\u662F\u5BF9\u8C61\uFF0C\u5DF2\u62D2\u7EDD\u8986\u76D6\u3002`);
|
|
1123
|
+
}
|
|
1124
|
+
if (raw.schemaVersion !== 2) {
|
|
1125
|
+
throw new Error(`\u4E0D\u652F\u6301\u914D\u7F6E schemaVersion=${String(raw.schemaVersion)}\uFF0C\u5DF2\u62D2\u7EDD\u7528\u5F53\u524D\u7248\u672C\u8986\u76D6\u3002`);
|
|
1126
|
+
}
|
|
1127
|
+
if (!raw.runtime || typeof raw.runtime !== "object" || Array.isArray(raw.runtime)) {
|
|
1128
|
+
throw new Error(`\u914D\u7F6E\u6587\u4EF6 ${CONFIG_V2_PATH} \u7F3A\u5C11\u6709\u6548\u7684 runtime \u5BF9\u8C61\uFF0C\u5DF2\u62D2\u7EDD\u8986\u76D6\u3002`);
|
|
1129
|
+
}
|
|
1130
|
+
if (!Array.isArray(raw.channels)) {
|
|
1131
|
+
throw new Error(`\u914D\u7F6E\u6587\u4EF6 ${CONFIG_V2_PATH} \u7F3A\u5C11 channels \u6570\u7EC4\uFF0C\u5DF2\u62D2\u7EDD\u8986\u76D6\u3002`);
|
|
1132
|
+
}
|
|
1133
|
+
const rawChannels = raw.channels;
|
|
1134
|
+
const parsed = {
|
|
1135
|
+
...raw,
|
|
1136
|
+
schemaVersion: 2,
|
|
1137
|
+
runtime: {
|
|
1138
|
+
...raw.runtime,
|
|
1139
|
+
provider: normalizeRuntimeProvider(raw.runtime.provider)
|
|
1140
|
+
},
|
|
1141
|
+
channels: normalizeChannelInstances(rawChannels)
|
|
1142
|
+
};
|
|
1143
|
+
Object.defineProperty(parsed, RAW_CHANNELS, { value: rawChannels, enumerable: false });
|
|
1144
|
+
return parsed;
|
|
1145
|
+
}
|
|
1146
|
+
function atomicWriteFile(filePath, content) {
|
|
1147
|
+
ensureConfigDir();
|
|
1148
|
+
const tmpPath = `${filePath}.${process.pid}.${crypto2.randomUUID()}.tmp`;
|
|
907
1149
|
try {
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1150
|
+
fs2.writeFileSync(tmpPath, content, { mode: 384 });
|
|
1151
|
+
fs2.renameSync(tmpPath, filePath);
|
|
1152
|
+
} finally {
|
|
1153
|
+
try {
|
|
1154
|
+
fs2.rmSync(tmpPath, { force: true });
|
|
1155
|
+
} catch {
|
|
913
1156
|
}
|
|
914
|
-
return null;
|
|
915
|
-
} catch {
|
|
916
|
-
return null;
|
|
917
1157
|
}
|
|
918
1158
|
}
|
|
919
1159
|
function writeConfigV2File(config2) {
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1160
|
+
const serialized = {
|
|
1161
|
+
...config2,
|
|
1162
|
+
channels: config2[RAW_CHANNELS] || config2.channels
|
|
1163
|
+
};
|
|
1164
|
+
atomicWriteFile(CONFIG_V2_PATH, JSON.stringify(serialized, null, 2));
|
|
924
1165
|
}
|
|
925
1166
|
function defaultAliasForProvider(provider) {
|
|
926
1167
|
return provider === "feishu" ? "\u98DE\u4E66" : "\u5FAE\u4FE1";
|
|
@@ -941,6 +1182,7 @@ function normalizeChannelInstances(value) {
|
|
|
941
1182
|
const config2 = record.config && typeof record.config === "object" ? record.config : {};
|
|
942
1183
|
const timestamp = nowIso();
|
|
943
1184
|
return [{
|
|
1185
|
+
...record,
|
|
944
1186
|
id: normalizeChannelId(
|
|
945
1187
|
typeof record.id === "string" && record.id.trim() ? record.id : buildDefaultChannelId(provider)
|
|
946
1188
|
),
|
|
@@ -1044,29 +1286,33 @@ function expandConfig(v2) {
|
|
|
1044
1286
|
function loadConfig() {
|
|
1045
1287
|
const current = readConfigV2File();
|
|
1046
1288
|
if (current) return expandConfig(current);
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1289
|
+
return withFileLock(CONFIG_V2_PATH, () => {
|
|
1290
|
+
const concurrentlyCreated = readConfigV2File();
|
|
1291
|
+
if (concurrentlyCreated) return expandConfig(concurrentlyCreated);
|
|
1292
|
+
const legacyEnv = loadRawConfigEnv();
|
|
1293
|
+
if (legacyEnv.size > 0) {
|
|
1294
|
+
const migrated = migrateLegacyEnvToV2(legacyEnv);
|
|
1295
|
+
writeConfigV2File(migrated);
|
|
1296
|
+
return expandConfig(migrated);
|
|
1297
|
+
}
|
|
1298
|
+
const empty = {
|
|
1299
|
+
schemaVersion: 2,
|
|
1300
|
+
runtime: {
|
|
1301
|
+
provider: "codex",
|
|
1302
|
+
defaultWorkspaceRoot: DEFAULT_WORKSPACE_ROOT,
|
|
1303
|
+
defaultMode: "code",
|
|
1304
|
+
historyMessageLimit: 8,
|
|
1305
|
+
streamStatusIdleStartSeconds: DEFAULT_STREAM_STATUS_IDLE_START_SECONDS,
|
|
1306
|
+
streamStatusCheckIntervalSeconds: DEFAULT_STREAM_STATUS_CHECK_INTERVAL_SECONDS,
|
|
1307
|
+
codexSkipGitRepoCheck: true,
|
|
1308
|
+
codexSandboxMode: "workspace-write",
|
|
1309
|
+
codexReasoningEffort: "medium",
|
|
1310
|
+
uiAllowLan: false
|
|
1311
|
+
},
|
|
1312
|
+
channels: []
|
|
1313
|
+
};
|
|
1314
|
+
return expandConfig(empty);
|
|
1315
|
+
});
|
|
1070
1316
|
}
|
|
1071
1317
|
function listChannelInstances(config2) {
|
|
1072
1318
|
return [...config2?.channels || loadConfig().channels || []];
|
|
@@ -1162,53 +1408,6 @@ function configToSettings(config2) {
|
|
|
1162
1408
|
return m;
|
|
1163
1409
|
}
|
|
1164
1410
|
|
|
1165
|
-
// src/lib/bridge/channel-adapter.ts
|
|
1166
|
-
var BaseChannelAdapter = class {
|
|
1167
|
-
inboundQueue = [];
|
|
1168
|
-
inboundWaiters = [];
|
|
1169
|
-
/** Human-readable instance alias, when applicable. */
|
|
1170
|
-
alias;
|
|
1171
|
-
/**
|
|
1172
|
-
* Answer a callback query or interactive-card action when supported.
|
|
1173
|
-
* Not all platforms support this — default implementation is a no-op.
|
|
1174
|
-
*/
|
|
1175
|
-
async answerCallback(_callbackQueryId, _text) {
|
|
1176
|
-
}
|
|
1177
|
-
consumeInboundMessage(isRunning) {
|
|
1178
|
-
const queued = this.inboundQueue.shift();
|
|
1179
|
-
if (queued) return Promise.resolve(queued);
|
|
1180
|
-
if (!isRunning) return Promise.resolve(null);
|
|
1181
|
-
return new Promise((resolve2) => {
|
|
1182
|
-
this.inboundWaiters.push(resolve2);
|
|
1183
|
-
});
|
|
1184
|
-
}
|
|
1185
|
-
enqueueInboundMessage(message) {
|
|
1186
|
-
const waiter = this.inboundWaiters.shift();
|
|
1187
|
-
if (waiter) {
|
|
1188
|
-
waiter(message);
|
|
1189
|
-
} else {
|
|
1190
|
-
this.inboundQueue.push(message);
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
clearInboundQueue() {
|
|
1194
|
-
this.inboundQueue = [];
|
|
1195
|
-
}
|
|
1196
|
-
rejectPendingInboundConsumers() {
|
|
1197
|
-
for (const waiter of this.inboundWaiters) {
|
|
1198
|
-
waiter(null);
|
|
1199
|
-
}
|
|
1200
|
-
this.inboundWaiters = [];
|
|
1201
|
-
}
|
|
1202
|
-
};
|
|
1203
|
-
var adapterFactories = /* @__PURE__ */ new Map();
|
|
1204
|
-
function registerAdapterFactory(provider, factory) {
|
|
1205
|
-
adapterFactories.set(provider, factory);
|
|
1206
|
-
}
|
|
1207
|
-
function createAdapter(instance) {
|
|
1208
|
-
const factory = adapterFactories.get(instance.provider);
|
|
1209
|
-
return factory ? factory(instance) : null;
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
1411
|
// src/lib/bridge/markdown/feishu.ts
|
|
1213
1412
|
function hasComplexMarkdown(text2) {
|
|
1214
1413
|
if (/```[\s\S]*?```/.test(text2)) return true;
|
|
@@ -1431,18 +1630,37 @@ function buildPermissionButtonCard(text2, permissionRequestId, chatId) {
|
|
|
1431
1630
|
// src/lib/bridge/adapters/feishu-adapter.ts
|
|
1432
1631
|
var DEDUP_MAX = 1e3;
|
|
1433
1632
|
var MAX_FILE_SIZE = 20 * 1024 * 1024;
|
|
1633
|
+
var ATTACHMENT_REQUEST_TIMEOUT_MS = 6e4;
|
|
1434
1634
|
var COMPLETED_EMOJI = "DONE";
|
|
1435
1635
|
var ERROR_EMOJI = "ERROR";
|
|
1436
1636
|
var CARD_TERMINAL_REACTION_DELAY_MS = 2e3;
|
|
1437
1637
|
var CARD_THROTTLE_MS = 1e3;
|
|
1438
1638
|
var CARD_REQUEST_TIMEOUT_MS = 15e3;
|
|
1439
1639
|
var CARD_FINALIZE_FLUSH_WAIT_EXTRA_MS = 1e3;
|
|
1640
|
+
var CARD_FINALIZE_RETRY_DELAYS_MS = [1e3, 5e3, 15e3, 3e4];
|
|
1641
|
+
var CARD_STOP_FINALIZE_TIMEOUT_MS = 2e3;
|
|
1440
1642
|
var CARD_FULL_REFRESH_INTERVAL_MS = 5 * 6e4;
|
|
1441
1643
|
var FINAL_CARD_FULL_TEXT_MAX_CHARS = 12e3;
|
|
1442
1644
|
var FINAL_CARD_PREVIEW_CHARS = 4e3;
|
|
1443
1645
|
var INITIAL_STREAMING_STATUS = "\u5904\u7406\u4E2D";
|
|
1444
1646
|
var EMPTY_STREAMING_TASKS = "";
|
|
1445
1647
|
var EMPTY_STREAMING_TOOLS = "";
|
|
1648
|
+
function validateFeishuAttachmentPath(filePath, maxSize = MAX_FILE_SIZE) {
|
|
1649
|
+
let stat;
|
|
1650
|
+
try {
|
|
1651
|
+
stat = fs3.statSync(filePath);
|
|
1652
|
+
} catch {
|
|
1653
|
+
return `Attachment not found: ${filePath}`;
|
|
1654
|
+
}
|
|
1655
|
+
if (!stat.isFile()) return `Attachment is not a regular file: ${filePath}`;
|
|
1656
|
+
if (stat.size > maxSize) {
|
|
1657
|
+
return `Attachment is too large: ${stat.size} bytes (max ${maxSize} bytes)`;
|
|
1658
|
+
}
|
|
1659
|
+
return null;
|
|
1660
|
+
}
|
|
1661
|
+
function normalizeAttachmentFileName(value) {
|
|
1662
|
+
return value.replace(/[\r\n"]/g, "_") || "attachment.bin";
|
|
1663
|
+
}
|
|
1446
1664
|
function shouldDeliverFinalTextSeparately(text2) {
|
|
1447
1665
|
return text2.trim().length > FINAL_CARD_FULL_TEXT_MAX_CHARS;
|
|
1448
1666
|
}
|
|
@@ -1523,12 +1741,18 @@ var FeishuAdapter = class extends BaseChannelAdapter {
|
|
|
1523
1741
|
activeCards = /* @__PURE__ */ new Map();
|
|
1524
1742
|
/** In-flight card creation promises per stream key — prevents duplicate creation. */
|
|
1525
1743
|
cardCreatePromises = /* @__PURE__ */ new Map();
|
|
1744
|
+
/** Terminal card updates retained until CardKit confirms finalization. */
|
|
1745
|
+
pendingCardFinalizations = /* @__PURE__ */ new Map();
|
|
1746
|
+
cardFinalizePromises = /* @__PURE__ */ new Map();
|
|
1526
1747
|
/** Cached tenant token for upload APIs. */
|
|
1527
1748
|
tenantTokenCache = null;
|
|
1528
1749
|
cardRequestTimeoutMs = CARD_REQUEST_TIMEOUT_MS;
|
|
1529
1750
|
cardFinalizeFlushWaitExtraMs = CARD_FINALIZE_FLUSH_WAIT_EXTRA_MS;
|
|
1530
1751
|
cardFullRefreshIntervalMs = CARD_FULL_REFRESH_INTERVAL_MS;
|
|
1531
1752
|
cardTerminalReactionDelayMs = CARD_TERMINAL_REACTION_DELAY_MS;
|
|
1753
|
+
cardFinalizeRetryDelaysMs = CARD_FINALIZE_RETRY_DELAYS_MS;
|
|
1754
|
+
cardStopFinalizeTimeoutMs = CARD_STOP_FINALIZE_TIMEOUT_MS;
|
|
1755
|
+
stopping = false;
|
|
1532
1756
|
constructor(instance) {
|
|
1533
1757
|
super();
|
|
1534
1758
|
this.channelType = instance?.id || "feishu";
|
|
@@ -1556,6 +1780,7 @@ var FeishuAdapter = class extends BaseChannelAdapter {
|
|
|
1556
1780
|
// ── Lifecycle ───────────────────────────────────────────────
|
|
1557
1781
|
async start() {
|
|
1558
1782
|
if (this.running) return;
|
|
1783
|
+
this.stopping = false;
|
|
1559
1784
|
const configError = this.validateConfig();
|
|
1560
1785
|
if (configError) {
|
|
1561
1786
|
console.warn("[feishu-adapter] Cannot start:", configError);
|
|
@@ -1609,6 +1834,29 @@ var FeishuAdapter = class extends BaseChannelAdapter {
|
|
|
1609
1834
|
async stop() {
|
|
1610
1835
|
if (!this.running) return;
|
|
1611
1836
|
this.running = false;
|
|
1837
|
+
this.stopping = true;
|
|
1838
|
+
const finalizations = Array.from(this.activeCards.entries()).map(([cardKey, state]) => {
|
|
1839
|
+
const pending = this.pendingCardFinalizations.get(cardKey);
|
|
1840
|
+
return this.finalizeCard(
|
|
1841
|
+
state.chatId,
|
|
1842
|
+
pending?.status || "interrupted",
|
|
1843
|
+
pending?.responseText || "Bridge \u670D\u52A1\u6B63\u5728\u505C\u6B62\uFF0C\u5F53\u524D\u4EFB\u52A1\u5DF2\u4E2D\u65AD\u3002",
|
|
1844
|
+
cardKey
|
|
1845
|
+
);
|
|
1846
|
+
});
|
|
1847
|
+
if (finalizations.length > 0) {
|
|
1848
|
+
let stopTimeout = null;
|
|
1849
|
+
try {
|
|
1850
|
+
await Promise.race([
|
|
1851
|
+
Promise.allSettled(finalizations),
|
|
1852
|
+
new Promise((resolve2) => {
|
|
1853
|
+
stopTimeout = setTimeout(resolve2, Math.max(0, this.cardStopFinalizeTimeoutMs));
|
|
1854
|
+
})
|
|
1855
|
+
]);
|
|
1856
|
+
} finally {
|
|
1857
|
+
if (stopTimeout) clearTimeout(stopTimeout);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1612
1860
|
if (this.wsClient) {
|
|
1613
1861
|
try {
|
|
1614
1862
|
this.wsClient.close({ force: true });
|
|
@@ -1622,8 +1870,13 @@ var FeishuAdapter = class extends BaseChannelAdapter {
|
|
|
1622
1870
|
for (const [, state] of this.activeCards) {
|
|
1623
1871
|
if (state.throttleTimer) clearTimeout(state.throttleTimer);
|
|
1624
1872
|
}
|
|
1873
|
+
for (const request of this.pendingCardFinalizations.values()) {
|
|
1874
|
+
if (request.retryTimer) clearTimeout(request.retryTimer);
|
|
1875
|
+
}
|
|
1625
1876
|
this.activeCards.clear();
|
|
1626
1877
|
this.cardCreatePromises.clear();
|
|
1878
|
+
this.pendingCardFinalizations.clear();
|
|
1879
|
+
this.cardFinalizePromises.clear();
|
|
1627
1880
|
this.seenMessageIds.clear();
|
|
1628
1881
|
this.lastIncomingMessageId.clear();
|
|
1629
1882
|
console.log("[feishu-adapter] Stopped");
|
|
@@ -1652,6 +1905,8 @@ var FeishuAdapter = class extends BaseChannelAdapter {
|
|
|
1652
1905
|
* Called by bridge-manager via onMessageEnd().
|
|
1653
1906
|
*/
|
|
1654
1907
|
onMessageEnd(chatId, streamKey) {
|
|
1908
|
+
const cardKey = this.resolveStreamKey(chatId, streamKey);
|
|
1909
|
+
if (this.pendingCardFinalizations.has(cardKey)) return;
|
|
1655
1910
|
this.cleanupCard(chatId, streamKey);
|
|
1656
1911
|
}
|
|
1657
1912
|
// ── Card Action Handler ─────────────────────────────────────
|
|
@@ -1955,13 +2210,18 @@ var FeishuAdapter = class extends BaseChannelAdapter {
|
|
|
1955
2210
|
const remainingMs = deadline - Date.now();
|
|
1956
2211
|
if (remainingMs <= 0) return false;
|
|
1957
2212
|
const timedOut = Symbol("flush-timeout");
|
|
2213
|
+
let flushTimeout = null;
|
|
1958
2214
|
try {
|
|
1959
2215
|
const result = await Promise.race([
|
|
1960
2216
|
inFlight.then(() => null),
|
|
1961
|
-
new Promise((resolve2) =>
|
|
2217
|
+
new Promise((resolve2) => {
|
|
2218
|
+
flushTimeout = setTimeout(() => resolve2(timedOut), remainingMs);
|
|
2219
|
+
})
|
|
1962
2220
|
]);
|
|
1963
2221
|
if (result === timedOut) return false;
|
|
1964
2222
|
} catch {
|
|
2223
|
+
} finally {
|
|
2224
|
+
if (flushTimeout) clearTimeout(flushTimeout);
|
|
1965
2225
|
}
|
|
1966
2226
|
continue;
|
|
1967
2227
|
}
|
|
@@ -1986,10 +2246,60 @@ var FeishuAdapter = class extends BaseChannelAdapter {
|
|
|
1986
2246
|
} catch {
|
|
1987
2247
|
}
|
|
1988
2248
|
}
|
|
2249
|
+
const existingRequest = this.pendingCardFinalizations.get(cardKey);
|
|
2250
|
+
if (existingRequest?.retryTimer) {
|
|
2251
|
+
clearTimeout(existingRequest.retryTimer);
|
|
2252
|
+
}
|
|
2253
|
+
const request = existingRequest || {
|
|
2254
|
+
chatId,
|
|
2255
|
+
status,
|
|
2256
|
+
responseText,
|
|
2257
|
+
streamKey,
|
|
2258
|
+
retryIndex: 0,
|
|
2259
|
+
retryTimer: null
|
|
2260
|
+
};
|
|
2261
|
+
request.chatId = chatId;
|
|
2262
|
+
request.status = status;
|
|
2263
|
+
request.responseText = responseText;
|
|
2264
|
+
request.streamKey = streamKey;
|
|
2265
|
+
request.retryTimer = null;
|
|
2266
|
+
this.pendingCardFinalizations.set(cardKey, request);
|
|
2267
|
+
const result = await this.runCardFinalizationAttempt(cardKey);
|
|
2268
|
+
if (!result.finalized) {
|
|
2269
|
+
if (result.retryable) {
|
|
2270
|
+
this.scheduleCardFinalizationRetry(cardKey);
|
|
2271
|
+
} else {
|
|
2272
|
+
this.clearPendingCardFinalization(cardKey);
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
return result.finalized && result.textCovered;
|
|
2276
|
+
}
|
|
2277
|
+
async runCardFinalizationAttempt(cardKey) {
|
|
2278
|
+
const existing = this.cardFinalizePromises.get(cardKey);
|
|
2279
|
+
if (existing) return existing;
|
|
2280
|
+
const attempt = this.performCardFinalizationAttempt(cardKey);
|
|
2281
|
+
this.cardFinalizePromises.set(cardKey, attempt);
|
|
2282
|
+
try {
|
|
2283
|
+
return await attempt;
|
|
2284
|
+
} finally {
|
|
2285
|
+
if (this.cardFinalizePromises.get(cardKey) === attempt) {
|
|
2286
|
+
this.cardFinalizePromises.delete(cardKey);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
async performCardFinalizationAttempt(cardKey) {
|
|
2291
|
+
const request = this.pendingCardFinalizations.get(cardKey);
|
|
1989
2292
|
const state = this.activeCards.get(cardKey);
|
|
1990
|
-
if (!
|
|
2293
|
+
if (!request || !state) {
|
|
2294
|
+
return { finalized: false, textCovered: false, retryable: false };
|
|
2295
|
+
}
|
|
2296
|
+
if (!this.restClient) {
|
|
2297
|
+
return { finalized: false, textCovered: false, retryable: !this.stopping };
|
|
2298
|
+
}
|
|
1991
2299
|
const cardkit = this.restClient.cardkit?.v1;
|
|
1992
|
-
if (!cardkit?.card?.settings || !cardkit?.card?.update)
|
|
2300
|
+
if (!cardkit?.card?.settings || !cardkit?.card?.update) {
|
|
2301
|
+
return { finalized: false, textCovered: false, retryable: false };
|
|
2302
|
+
}
|
|
1993
2303
|
if (state.throttleTimer) {
|
|
1994
2304
|
clearTimeout(state.throttleTimer);
|
|
1995
2305
|
state.throttleTimer = null;
|
|
@@ -2016,21 +2326,21 @@ var FeishuAdapter = class extends BaseChannelAdapter {
|
|
|
2016
2326
|
};
|
|
2017
2327
|
const elapsedMs = Date.now() - state.startTime;
|
|
2018
2328
|
const footer = {
|
|
2019
|
-
status: statusLabels[status] || status,
|
|
2329
|
+
status: statusLabels[request.status] || request.status,
|
|
2020
2330
|
elapsed: formatElapsed(elapsedMs)
|
|
2021
2331
|
};
|
|
2022
2332
|
const existingText = state.pendingText || "";
|
|
2023
2333
|
const trimmedExisting = existingText.trim();
|
|
2024
|
-
const trimmedResponse = responseText.trim();
|
|
2334
|
+
const trimmedResponse = request.responseText.trim();
|
|
2025
2335
|
let finalText = trimmedResponse || trimmedExisting;
|
|
2026
|
-
if (status === "interrupted" && trimmedExisting && trimmedResponse && trimmedResponse !== trimmedExisting && !trimmedExisting.includes(trimmedResponse)) {
|
|
2336
|
+
if (request.status === "interrupted" && trimmedExisting && trimmedResponse && trimmedResponse !== trimmedExisting && !trimmedExisting.includes(trimmedResponse)) {
|
|
2027
2337
|
finalText = `${trimmedExisting}
|
|
2028
2338
|
|
|
2029
2339
|
${trimmedResponse}`;
|
|
2030
2340
|
}
|
|
2031
2341
|
const deliverTextSeparately = shouldDeliverFinalTextSeparately(finalText);
|
|
2032
2342
|
const cardText = deliverTextSeparately ? buildFinalCardTextPreview(finalText) : finalText;
|
|
2033
|
-
const finalCardJson = buildFinalCardJson(cardText, state.taskItems, state.toolCalls, footer, status);
|
|
2343
|
+
const finalCardJson = buildFinalCardJson(cardText, state.taskItems, state.toolCalls, footer, request.status);
|
|
2034
2344
|
state.sequence++;
|
|
2035
2345
|
await this.withFeishuRequestTimeout(cardKey, "card.update", () => cardkit.card.update({
|
|
2036
2346
|
path: { card_id: state.cardId },
|
|
@@ -2040,20 +2350,52 @@ ${trimmedResponse}`;
|
|
|
2040
2350
|
}
|
|
2041
2351
|
}));
|
|
2042
2352
|
this.activeCards.delete(cardKey);
|
|
2043
|
-
|
|
2044
|
-
|
|
2353
|
+
this.clearPendingCardFinalization(cardKey);
|
|
2354
|
+
console.log(`[feishu-adapter] Card finalized: streamKey=${cardKey}, cardId=${state.cardId}, status=${request.status}, elapsed=${formatElapsed(elapsedMs)}`);
|
|
2355
|
+
const terminalReactionEmoji = request.status === "completed" ? COMPLETED_EMOJI : request.status === "error" ? ERROR_EMOJI : null;
|
|
2045
2356
|
if (terminalReactionEmoji && this.hasTerminalReactionApi()) {
|
|
2046
2357
|
await this.waitBeforeTerminalReaction();
|
|
2047
2358
|
await this.addTerminalReaction(cardKey, state.messageId, terminalReactionEmoji);
|
|
2048
2359
|
}
|
|
2049
|
-
return !deliverTextSeparately;
|
|
2360
|
+
return { finalized: true, textCovered: !deliverTextSeparately, retryable: false };
|
|
2050
2361
|
} catch (err) {
|
|
2051
2362
|
console.warn("[feishu-adapter] Card finalize failed:", err instanceof Error ? err.message : err);
|
|
2052
|
-
return false;
|
|
2053
|
-
} finally {
|
|
2054
|
-
this.activeCards.delete(cardKey);
|
|
2363
|
+
return { finalized: false, textCovered: false, retryable: true };
|
|
2055
2364
|
}
|
|
2056
2365
|
}
|
|
2366
|
+
scheduleCardFinalizationRetry(cardKey) {
|
|
2367
|
+
const request = this.pendingCardFinalizations.get(cardKey);
|
|
2368
|
+
if (!request || request.retryTimer || this.stopping || !this.restClient) return;
|
|
2369
|
+
if (request.retryIndex >= this.cardFinalizeRetryDelaysMs.length) {
|
|
2370
|
+
console.warn(`[feishu-adapter] Card finalize retries exhausted: streamKey=${cardKey}`);
|
|
2371
|
+
this.cleanupCard(request.chatId, request.streamKey || cardKey);
|
|
2372
|
+
return;
|
|
2373
|
+
}
|
|
2374
|
+
const delayMs = Math.max(0, this.cardFinalizeRetryDelaysMs[request.retryIndex] || 0);
|
|
2375
|
+
request.retryIndex += 1;
|
|
2376
|
+
request.retryTimer = setTimeout(() => {
|
|
2377
|
+
const current = this.pendingCardFinalizations.get(cardKey);
|
|
2378
|
+
if (!current) return;
|
|
2379
|
+
current.retryTimer = null;
|
|
2380
|
+
void this.runCardFinalizationAttempt(cardKey).then((result) => {
|
|
2381
|
+
if (result.finalized) return;
|
|
2382
|
+
if (result.retryable) {
|
|
2383
|
+
this.scheduleCardFinalizationRetry(cardKey);
|
|
2384
|
+
} else {
|
|
2385
|
+
this.cleanupCard(current.chatId, current.streamKey || cardKey);
|
|
2386
|
+
}
|
|
2387
|
+
}).catch((error) => {
|
|
2388
|
+
console.warn("[feishu-adapter] Card finalize retry failed:", error instanceof Error ? error.message : error);
|
|
2389
|
+
this.scheduleCardFinalizationRetry(cardKey);
|
|
2390
|
+
});
|
|
2391
|
+
}, delayMs);
|
|
2392
|
+
request.retryTimer.unref?.();
|
|
2393
|
+
}
|
|
2394
|
+
clearPendingCardFinalization(cardKey) {
|
|
2395
|
+
const request = this.pendingCardFinalizations.get(cardKey);
|
|
2396
|
+
if (request?.retryTimer) clearTimeout(request.retryTimer);
|
|
2397
|
+
this.pendingCardFinalizations.delete(cardKey);
|
|
2398
|
+
}
|
|
2057
2399
|
hasTerminalReactionApi() {
|
|
2058
2400
|
const messageReaction = this.restClient?.im?.messageReaction;
|
|
2059
2401
|
return typeof messageReaction?.create === "function";
|
|
@@ -2084,6 +2426,7 @@ ${trimmedResponse}`;
|
|
|
2084
2426
|
cleanupCard(chatId, streamKey) {
|
|
2085
2427
|
const cardKey = this.resolveStreamKey(chatId, streamKey);
|
|
2086
2428
|
this.cardCreatePromises.delete(cardKey);
|
|
2429
|
+
this.clearPendingCardFinalization(cardKey);
|
|
2087
2430
|
const state = this.activeCards.get(cardKey);
|
|
2088
2431
|
if (!state) return;
|
|
2089
2432
|
if (state.throttleTimer) {
|
|
@@ -2322,7 +2665,8 @@ ${trimmedResponse}`;
|
|
|
2322
2665
|
body: JSON.stringify({
|
|
2323
2666
|
app_id: appId,
|
|
2324
2667
|
app_secret: appSecret
|
|
2325
|
-
})
|
|
2668
|
+
}),
|
|
2669
|
+
signal: AbortSignal.timeout(CARD_REQUEST_TIMEOUT_MS)
|
|
2326
2670
|
});
|
|
2327
2671
|
const data = await response.json();
|
|
2328
2672
|
if (!response.ok || data.code !== 0 || !data.tenant_access_token) {
|
|
@@ -2347,9 +2691,8 @@ ${trimmedResponse}`;
|
|
|
2347
2691
|
return { ok: true, messageId: lastMessageId };
|
|
2348
2692
|
}
|
|
2349
2693
|
async sendAttachment(chatId, attachment, replyToMessageId) {
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
}
|
|
2694
|
+
const validationError = validateFeishuAttachmentPath(attachment.path);
|
|
2695
|
+
if (validationError) return { ok: false, error: validationError };
|
|
2353
2696
|
try {
|
|
2354
2697
|
if (attachment.kind === "image") {
|
|
2355
2698
|
const imageKey = await this.uploadImage(attachment);
|
|
@@ -2373,14 +2716,17 @@ ${trimmedResponse}`;
|
|
|
2373
2716
|
}
|
|
2374
2717
|
async uploadImage(attachment) {
|
|
2375
2718
|
const token = await this.getTenantAccessToken();
|
|
2376
|
-
const fileName =
|
|
2719
|
+
const fileName = normalizeAttachmentFileName(
|
|
2720
|
+
attachment.name || path2.basename(attachment.path) || "image.png"
|
|
2721
|
+
);
|
|
2377
2722
|
const form = new FormData();
|
|
2378
2723
|
form.set("image_type", "message");
|
|
2379
|
-
form.set("image", new Blob([
|
|
2724
|
+
form.set("image", new Blob([await fs3.promises.readFile(attachment.path)]), fileName);
|
|
2380
2725
|
const response = await fetch(`${this.getOpenApiBaseUrl()}/open-apis/im/v1/images`, {
|
|
2381
2726
|
method: "POST",
|
|
2382
2727
|
headers: { Authorization: `Bearer ${token}` },
|
|
2383
|
-
body: form
|
|
2728
|
+
body: form,
|
|
2729
|
+
signal: AbortSignal.timeout(ATTACHMENT_REQUEST_TIMEOUT_MS)
|
|
2384
2730
|
});
|
|
2385
2731
|
const data = await response.json();
|
|
2386
2732
|
if (!response.ok || data.code !== 0 || !data.data?.image_key) {
|
|
@@ -2390,15 +2736,18 @@ ${trimmedResponse}`;
|
|
|
2390
2736
|
}
|
|
2391
2737
|
async uploadFile(attachment) {
|
|
2392
2738
|
const token = await this.getTenantAccessToken();
|
|
2393
|
-
const fileName =
|
|
2739
|
+
const fileName = normalizeAttachmentFileName(
|
|
2740
|
+
attachment.name || path2.basename(attachment.path) || "attachment.bin"
|
|
2741
|
+
);
|
|
2394
2742
|
const form = new FormData();
|
|
2395
2743
|
form.set("file_type", "stream");
|
|
2396
2744
|
form.set("file_name", fileName);
|
|
2397
|
-
form.set("file", new Blob([
|
|
2745
|
+
form.set("file", new Blob([await fs3.promises.readFile(attachment.path)]), fileName);
|
|
2398
2746
|
const response = await fetch(`${this.getOpenApiBaseUrl()}/open-apis/im/v1/files`, {
|
|
2399
2747
|
method: "POST",
|
|
2400
2748
|
headers: { Authorization: `Bearer ${token}` },
|
|
2401
|
-
body: form
|
|
2749
|
+
body: form,
|
|
2750
|
+
signal: AbortSignal.timeout(ATTACHMENT_REQUEST_TIMEOUT_MS)
|
|
2402
2751
|
});
|
|
2403
2752
|
const data = await response.json();
|
|
2404
2753
|
if (!response.ok || data.code !== 0 || !data.data?.file_key) {
|
|
@@ -2937,20 +3286,20 @@ ${trimmedResponse}`;
|
|
|
2937
3286
|
buffer = Buffer.concat(chunks);
|
|
2938
3287
|
} catch (streamErr) {
|
|
2939
3288
|
console.warn("[feishu-adapter] Stream read failed, falling back to writeFile:", streamErr instanceof Error ? streamErr.message : streamErr);
|
|
2940
|
-
const
|
|
3289
|
+
const fs19 = await import("fs");
|
|
2941
3290
|
const os5 = await import("os");
|
|
2942
|
-
const
|
|
2943
|
-
const tmpPath =
|
|
3291
|
+
const path20 = await import("path");
|
|
3292
|
+
const tmpPath = path20.join(os5.tmpdir(), `feishu-dl-${crypto3.randomUUID()}`);
|
|
2944
3293
|
try {
|
|
2945
3294
|
await res.writeFile(tmpPath);
|
|
2946
|
-
buffer =
|
|
3295
|
+
buffer = fs19.readFileSync(tmpPath);
|
|
2947
3296
|
if (buffer.length > MAX_FILE_SIZE) {
|
|
2948
3297
|
console.warn(`[feishu-adapter] Resource too large (>${MAX_FILE_SIZE} bytes), key: ${fileKey}`);
|
|
2949
3298
|
return null;
|
|
2950
3299
|
}
|
|
2951
3300
|
} finally {
|
|
2952
3301
|
try {
|
|
2953
|
-
|
|
3302
|
+
fs19.unlinkSync(tmpPath);
|
|
2954
3303
|
} catch {
|
|
2955
3304
|
}
|
|
2956
3305
|
}
|
|
@@ -2960,7 +3309,7 @@ ${trimmedResponse}`;
|
|
|
2960
3309
|
return null;
|
|
2961
3310
|
}
|
|
2962
3311
|
const base64 = buffer.toString("base64");
|
|
2963
|
-
const id =
|
|
3312
|
+
const id = crypto3.randomUUID();
|
|
2964
3313
|
const mimeType = MIME_BY_TYPE[resourceType] || "application/octet-stream";
|
|
2965
3314
|
const ext = resourceType === "image" ? "png" : resourceType === "audio" ? "ogg" : resourceType === "video" ? "mp4" : "bin";
|
|
2966
3315
|
console.log(`[feishu-adapter] Resource downloaded: ${buffer.length} bytes, key=${fileKey}`);
|
|
@@ -2996,25 +3345,34 @@ ${trimmedResponse}`;
|
|
|
2996
3345
|
registerAdapterFactory("feishu", (instance) => new FeishuAdapter(instance));
|
|
2997
3346
|
|
|
2998
3347
|
// src/weixin-store.ts
|
|
2999
|
-
import
|
|
3348
|
+
import fs4 from "node:fs";
|
|
3349
|
+
import crypto4 from "node:crypto";
|
|
3000
3350
|
import path3 from "node:path";
|
|
3001
3351
|
var DATA_DIR = path3.join(CTI_HOME, "data");
|
|
3002
3352
|
var ACCOUNTS_PATH = path3.join(DATA_DIR, "weixin-accounts.json");
|
|
3003
3353
|
var CONTEXT_TOKENS_PATH = path3.join(DATA_DIR, "weixin-context-tokens.json");
|
|
3004
3354
|
function ensureDir(dir) {
|
|
3005
|
-
|
|
3355
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
3006
3356
|
}
|
|
3007
3357
|
function atomicWrite(filePath, data) {
|
|
3008
|
-
const tmpPath = `${filePath}.tmp`;
|
|
3009
|
-
|
|
3010
|
-
|
|
3358
|
+
const tmpPath = `${filePath}.${process.pid}.${crypto4.randomUUID()}.tmp`;
|
|
3359
|
+
try {
|
|
3360
|
+
fs4.writeFileSync(tmpPath, data, "utf-8");
|
|
3361
|
+
fs4.renameSync(tmpPath, filePath);
|
|
3362
|
+
} finally {
|
|
3363
|
+
try {
|
|
3364
|
+
fs4.rmSync(tmpPath, { force: true });
|
|
3365
|
+
} catch {
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3011
3368
|
}
|
|
3012
3369
|
function readJson(filePath, fallback) {
|
|
3013
3370
|
try {
|
|
3014
|
-
const raw =
|
|
3371
|
+
const raw = fs4.readFileSync(filePath, "utf-8");
|
|
3015
3372
|
return JSON.parse(raw);
|
|
3016
|
-
} catch {
|
|
3017
|
-
return fallback;
|
|
3373
|
+
} catch (error) {
|
|
3374
|
+
if (error.code === "ENOENT") return fallback;
|
|
3375
|
+
throw new Error(`\u65E0\u6CD5\u8BFB\u53D6\u5FAE\u4FE1\u6570\u636E\u6587\u4EF6 ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3018
3376
|
}
|
|
3019
3377
|
}
|
|
3020
3378
|
function getAccountRecency(account) {
|
|
@@ -3069,13 +3427,15 @@ function getWeixinContextToken(accountId, peerUserId) {
|
|
|
3069
3427
|
return tokens[contextKey(accountId, peerUserId)];
|
|
3070
3428
|
}
|
|
3071
3429
|
function upsertWeixinContextToken(accountId, peerUserId, contextToken) {
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3430
|
+
withFileLock(CONTEXT_TOKENS_PATH, () => {
|
|
3431
|
+
const tokens = readContextTokens();
|
|
3432
|
+
tokens[contextKey(accountId, peerUserId)] = contextToken;
|
|
3433
|
+
writeContextTokens(tokens);
|
|
3434
|
+
});
|
|
3075
3435
|
}
|
|
3076
3436
|
|
|
3077
3437
|
// src/adapters/weixin/weixin-api.ts
|
|
3078
|
-
import
|
|
3438
|
+
import crypto5 from "node:crypto";
|
|
3079
3439
|
|
|
3080
3440
|
// src/adapters/weixin/weixin-types.ts
|
|
3081
3441
|
var MessageItemType = {
|
|
@@ -3099,7 +3459,7 @@ var LONG_POLL_TIMEOUT_MS = 35e3;
|
|
|
3099
3459
|
var API_TIMEOUT_MS = 15e3;
|
|
3100
3460
|
var CONFIG_TIMEOUT_MS = 1e4;
|
|
3101
3461
|
function generateWechatUin() {
|
|
3102
|
-
return
|
|
3462
|
+
return crypto5.randomBytes(4).toString("base64");
|
|
3103
3463
|
}
|
|
3104
3464
|
function normalizeBaseUrl(baseUrl) {
|
|
3105
3465
|
return (baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
@@ -3158,7 +3518,7 @@ async function getUpdates(creds, getUpdatesBuf, timeoutMs = LONG_POLL_TIMEOUT_MS
|
|
|
3158
3518
|
}
|
|
3159
3519
|
}
|
|
3160
3520
|
function generateClientId() {
|
|
3161
|
-
return `cti-weixin-${Date.now()}-${
|
|
3521
|
+
return `cti-weixin-${Date.now()}-${crypto5.randomBytes(4).toString("hex")}`;
|
|
3162
3522
|
}
|
|
3163
3523
|
async function sendMessage(creds, toUserId, items, contextToken) {
|
|
3164
3524
|
const clientId = generateClientId();
|
|
@@ -3235,10 +3595,35 @@ function decodeWeixinChatId(chatId) {
|
|
|
3235
3595
|
}
|
|
3236
3596
|
|
|
3237
3597
|
// src/adapters/weixin/weixin-media.ts
|
|
3238
|
-
import
|
|
3598
|
+
import crypto6 from "node:crypto";
|
|
3239
3599
|
var MAX_MEDIA_SIZE = 100 * 1024 * 1024;
|
|
3600
|
+
async function readResponseBodyWithLimit(response, maxSize, label) {
|
|
3601
|
+
const contentLength = Number(response.headers.get("content-length"));
|
|
3602
|
+
if (Number.isFinite(contentLength) && contentLength > maxSize) {
|
|
3603
|
+
throw new Error(`Media too large: ${contentLength} bytes`);
|
|
3604
|
+
}
|
|
3605
|
+
if (!response.body) return Buffer.alloc(0);
|
|
3606
|
+
const reader = response.body.getReader();
|
|
3607
|
+
const chunks = [];
|
|
3608
|
+
let totalSize = 0;
|
|
3609
|
+
try {
|
|
3610
|
+
while (true) {
|
|
3611
|
+
const { done, value } = await reader.read();
|
|
3612
|
+
if (done) break;
|
|
3613
|
+
totalSize += value.byteLength;
|
|
3614
|
+
if (totalSize > maxSize) {
|
|
3615
|
+
await reader.cancel(`Media too large while downloading ${label}`);
|
|
3616
|
+
throw new Error(`Media too large: more than ${maxSize} bytes`);
|
|
3617
|
+
}
|
|
3618
|
+
chunks.push(Buffer.from(value));
|
|
3619
|
+
}
|
|
3620
|
+
} finally {
|
|
3621
|
+
reader.releaseLock();
|
|
3622
|
+
}
|
|
3623
|
+
return Buffer.concat(chunks, totalSize);
|
|
3624
|
+
}
|
|
3240
3625
|
function decryptMedia(data, key) {
|
|
3241
|
-
const decipher =
|
|
3626
|
+
const decipher = crypto6.createDecipheriv("aes-128-ecb", key, null);
|
|
3242
3627
|
return Buffer.concat([decipher.update(data), decipher.final()]);
|
|
3243
3628
|
}
|
|
3244
3629
|
function parseBase64EncodedAesKey(aesKeyBase64) {
|
|
@@ -3293,13 +3678,10 @@ async function downloadAndDecryptMedia(cdnUrl, aesKey, label) {
|
|
|
3293
3678
|
if (!res.ok) {
|
|
3294
3679
|
throw new Error(`CDN download failed for ${label}: ${res.status}`);
|
|
3295
3680
|
}
|
|
3296
|
-
const encrypted =
|
|
3681
|
+
const encrypted = await readResponseBodyWithLimit(res, MAX_MEDIA_SIZE, label);
|
|
3297
3682
|
if (encrypted.length === 0) {
|
|
3298
3683
|
throw new Error(`Downloaded ${label} is empty`);
|
|
3299
3684
|
}
|
|
3300
|
-
if (encrypted.length > MAX_MEDIA_SIZE) {
|
|
3301
|
-
throw new Error(`Media too large: ${encrypted.length} bytes`);
|
|
3302
|
-
}
|
|
3303
3685
|
return decryptMedia(encrypted, aesKey);
|
|
3304
3686
|
}
|
|
3305
3687
|
async function downloadMediaFromItem(item, cdnBaseUrl) {
|
|
@@ -3348,7 +3730,7 @@ async function downloadMediaFromItem(item, cdnBaseUrl) {
|
|
|
3348
3730
|
}
|
|
3349
3731
|
const data = await downloadAndDecryptMedia(buildCdnDownloadUrl(encryptParam, cdnBaseUrl), aesKey, filename);
|
|
3350
3732
|
return {
|
|
3351
|
-
id:
|
|
3733
|
+
id: crypto6.randomUUID(),
|
|
3352
3734
|
name: filename,
|
|
3353
3735
|
type: mimeType,
|
|
3354
3736
|
size: data.length,
|
|
@@ -7080,6 +7462,16 @@ function escape2(state, silent) {
|
|
|
7080
7462
|
state.pos = pos;
|
|
7081
7463
|
return true;
|
|
7082
7464
|
}
|
|
7465
|
+
if (ch1 === 32) {
|
|
7466
|
+
if (!silent) {
|
|
7467
|
+
const token = state.push("text_special", "", 0);
|
|
7468
|
+
token.content = "\\";
|
|
7469
|
+
token.markup = "\\";
|
|
7470
|
+
token.info = "escape";
|
|
7471
|
+
}
|
|
7472
|
+
state.pos = pos;
|
|
7473
|
+
return true;
|
|
7474
|
+
}
|
|
7083
7475
|
let escapedStr = state.src[pos];
|
|
7084
7476
|
if (ch1 >= 55296 && ch1 <= 56319 && pos + 1 < max) {
|
|
7085
7477
|
const ch2 = state.src.charCodeAt(pos + 1);
|
|
@@ -7895,34 +8287,34 @@ function re_default(opts) {
|
|
|
7895
8287
|
re.src_ZPCc = [re.src_Z, re.src_P, re.src_Cc].join("|");
|
|
7896
8288
|
re.src_ZCc = [re.src_Z, re.src_Cc].join("|");
|
|
7897
8289
|
const text_separators = "[><\uFF5C]";
|
|
7898
|
-
re.src_pseudo_letter =
|
|
8290
|
+
re.src_pseudo_letter = `(?:(?!${text_separators}|${re.src_ZPCc})${re.src_Any})`;
|
|
7899
8291
|
re.src_ip4 = "(?:(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)";
|
|
7900
|
-
re.src_auth =
|
|
8292
|
+
re.src_auth = `(?:(?:(?!${re.src_ZCc}|[@/\\[\\]()]).){1,50}@)?`;
|
|
7901
8293
|
re.src_port = "(?::(?:6(?:[0-4]\\d{3}|5(?:[0-4]\\d{2}|5(?:[0-2]\\d|3[0-5])))|[1-5]?\\d{1,4}))?";
|
|
7902
|
-
re.src_host_terminator =
|
|
7903
|
-
re.src_path =
|
|
7904
|
-
|
|
7905
|
-
re.src_email_name = '[\\-;:&=\\+\\$,\\.a-zA-Z0-9_][\\-;:&=\\+\\$,\\"\\.a-zA-Z0-9_]
|
|
8294
|
+
re.src_host_terminator = `(?=$|${text_separators}|${re.src_ZPCc})(?!${opts["---"] ? "-(?!--)|" : "-|"}_|:\\d|\\.-|\\.(?!$|${re.src_ZPCc}))`;
|
|
8295
|
+
re.src_path = `(?:[/?#](?:(?!${re.src_ZCc}|${text_separators}|[()[\\]{}.,"'?!\\-;]).|\\[(?:(?!${re.src_ZCc}|\\]).)*\\]|\\((?:(?!${re.src_ZCc}|[)]).)*\\)|\\{(?:(?!${re.src_ZCc}|[}]).)*\\}|\\"(?:(?!${re.src_ZCc}|["]).)+\\"|\\'(?:(?!${re.src_ZCc}|[']).)+\\'|\\'(?=${re.src_pseudo_letter}|[-])|\\.{2,}[a-zA-Z0-9%/&]|\\.(?!${re.src_ZCc}|[.]|$)|` + (opts["---"] ? "\\-(?!--(?:[^-]|$))(?:-*)|" : "\\-+|") + // allow `,,,` in paths
|
|
8296
|
+
`,(?!${re.src_ZCc}|$)|;(?!${re.src_ZCc}|$)|\\!+(?!${re.src_ZCc}|[!]|$)|\\?(?!${re.src_ZCc}|[?]|$))+|\\/)?`;
|
|
8297
|
+
re.src_email_name = '[\\-;:&=\\+\\$,\\.a-zA-Z0-9_][\\-;:&=\\+\\$,\\"\\.a-zA-Z0-9_]{0,63}';
|
|
7906
8298
|
re.src_xn = "xn--[a-z0-9\\-]{1,59}";
|
|
7907
8299
|
re.src_domain_root = // Allow letters & digits (http://test1)
|
|
7908
|
-
"(?:" + re.src_xn +
|
|
7909
|
-
re.src_domain = "(?:" + re.src_xn +
|
|
7910
|
-
re.src_host =
|
|
7911
|
-
re.tpl_host_fuzzy = "(?:" + re.src_ip4 +
|
|
7912
|
-
re.tpl_host_no_ip_fuzzy =
|
|
8300
|
+
"(?:" + re.src_xn + `|${re.src_pseudo_letter}{1,63})`;
|
|
8301
|
+
re.src_domain = "(?:" + re.src_xn + `|(?:${re.src_pseudo_letter})|(?:${re.src_pseudo_letter}(?:-|${re.src_pseudo_letter}){0,61}${re.src_pseudo_letter}))`;
|
|
8302
|
+
re.src_host = `(?:(?:(?:(?:${re.src_domain})\\.)*${re.src_domain}))`;
|
|
8303
|
+
re.tpl_host_fuzzy = "(?:" + re.src_ip4 + `|(?:(?:(?:${re.src_domain})\\.)+(?:%TLDS%)))`;
|
|
8304
|
+
re.tpl_host_no_ip_fuzzy = `(?:(?:(?:${re.src_domain})\\.)+(?:%TLDS%))`;
|
|
7913
8305
|
re.src_host_strict = re.src_host + re.src_host_terminator;
|
|
7914
8306
|
re.tpl_host_fuzzy_strict = re.tpl_host_fuzzy + re.src_host_terminator;
|
|
7915
8307
|
re.src_host_port_strict = re.src_host + re.src_port + re.src_host_terminator;
|
|
7916
8308
|
re.tpl_host_port_fuzzy_strict = re.tpl_host_fuzzy + re.src_port + re.src_host_terminator;
|
|
7917
8309
|
re.tpl_host_port_no_ip_fuzzy_strict = re.tpl_host_no_ip_fuzzy + re.src_port + re.src_host_terminator;
|
|
7918
|
-
re.tpl_host_fuzzy_test =
|
|
7919
|
-
re.tpl_email_fuzzy =
|
|
8310
|
+
re.tpl_host_fuzzy_test = `localhost|www\\.|\\.\\d{1,3}\\.|(?:\\.(?:%TLDS%)(?:${re.src_ZPCc}|>|$))`;
|
|
8311
|
+
re.tpl_email_fuzzy = `(^|${text_separators}|"|\\(|${re.src_ZCc})(${re.src_email_name}@${re.tpl_host_fuzzy_strict})`;
|
|
7920
8312
|
re.tpl_link_fuzzy = // Fuzzy link can't be prepended with .:/\- and non punctuation.
|
|
7921
8313
|
// but can start with > (markdown blockquote)
|
|
7922
|
-
|
|
8314
|
+
`(^|(?![.:/\\-_@])(?:[$+<=>^\`|\uFF5C]|${re.src_ZPCc}))((?![$+<=>^\`|\uFF5C])${re.tpl_host_port_fuzzy_strict}${re.src_path})`;
|
|
7923
8315
|
re.tpl_link_no_ip_fuzzy = // Fuzzy link can't be prepended with .:/\- and non punctuation.
|
|
7924
8316
|
// but can start with > (markdown blockquote)
|
|
7925
|
-
|
|
8317
|
+
`(^|(?![.:/\\-_@])(?:[$+<=>^\`|\uFF5C]|${re.src_ZPCc}))((?![$+<=>^\`|\uFF5C])${re.tpl_host_port_no_ip_fuzzy_strict}${re.src_path})`;
|
|
7926
8318
|
return re;
|
|
7927
8319
|
}
|
|
7928
8320
|
|
|
@@ -7973,7 +8365,7 @@ var defaultSchemas = {
|
|
|
7973
8365
|
const tail = text2.slice(pos);
|
|
7974
8366
|
if (!self.re.http) {
|
|
7975
8367
|
self.re.http = new RegExp(
|
|
7976
|
-
|
|
8368
|
+
`^\\/\\/${self.re.src_auth}${self.re.src_host_port_strict}${self.re.src_path}`,
|
|
7977
8369
|
"i"
|
|
7978
8370
|
);
|
|
7979
8371
|
}
|
|
@@ -7992,7 +8384,7 @@ var defaultSchemas = {
|
|
|
7992
8384
|
self.re.no_http = new RegExp(
|
|
7993
8385
|
"^" + self.re.src_auth + // Don't allow single-level domains, because of false positives like '//test'
|
|
7994
8386
|
// with code comments
|
|
7995
|
-
|
|
8387
|
+
`(?:localhost|(?:(?:${self.re.src_domain})\\.)+${self.re.src_domain_root})` + self.re.src_port + self.re.src_host_terminator + self.re.src_path,
|
|
7996
8388
|
"i"
|
|
7997
8389
|
);
|
|
7998
8390
|
}
|
|
@@ -8013,7 +8405,7 @@ var defaultSchemas = {
|
|
|
8013
8405
|
const tail = text2.slice(pos);
|
|
8014
8406
|
if (!self.re.mailto) {
|
|
8015
8407
|
self.re.mailto = new RegExp(
|
|
8016
|
-
|
|
8408
|
+
`^${self.re.src_email_name}@${self.re.src_host_strict}`,
|
|
8017
8409
|
"i"
|
|
8018
8410
|
);
|
|
8019
8411
|
}
|
|
@@ -8062,7 +8454,7 @@ function compile(self) {
|
|
|
8062
8454
|
const aliases = [];
|
|
8063
8455
|
self.__compiled__ = {};
|
|
8064
8456
|
function schemaError(name, val) {
|
|
8065
|
-
throw new Error(
|
|
8457
|
+
throw new Error(`(LinkifyIt) Invalid schema "${name}": ${val}`);
|
|
8066
8458
|
}
|
|
8067
8459
|
Object.keys(self.__schemas__).forEach(function(name) {
|
|
8068
8460
|
const val = self.__schemas__[name];
|
|
@@ -8105,11 +8497,11 @@ function compile(self) {
|
|
|
8105
8497
|
const slist = Object.keys(self.__compiled__).filter(function(name) {
|
|
8106
8498
|
return name.length > 0 && self.__compiled__[name];
|
|
8107
8499
|
}).map(escapeRE2).join("|");
|
|
8108
|
-
self.re.schema_test = RegExp(
|
|
8109
|
-
self.re.schema_search = RegExp(
|
|
8110
|
-
self.re.schema_at_start = RegExp(
|
|
8500
|
+
self.re.schema_test = RegExp(`(^|(?!_)(?:[><\uFF5C]|${re.src_ZPCc}))(${slist})`, "i");
|
|
8501
|
+
self.re.schema_search = RegExp(`(^|(?!_)(?:[><\uFF5C]|${re.src_ZPCc}))(${slist})`, "ig");
|
|
8502
|
+
self.re.schema_at_start = RegExp(`^${self.re.schema_search.source}`, "i");
|
|
8111
8503
|
self.re.pretest = RegExp(
|
|
8112
|
-
|
|
8504
|
+
`(${self.re.schema_test.source})|(${self.re.host_fuzzy_test.source})|@`,
|
|
8113
8505
|
"i"
|
|
8114
8506
|
);
|
|
8115
8507
|
}
|
|
@@ -8303,10 +8695,10 @@ LinkifyIt.prototype.tlds = function tlds(list2, keepOld) {
|
|
|
8303
8695
|
};
|
|
8304
8696
|
LinkifyIt.prototype.normalize = function normalize2(match2) {
|
|
8305
8697
|
if (!match2.schema) {
|
|
8306
|
-
match2.url =
|
|
8698
|
+
match2.url = `http://${match2.url}`;
|
|
8307
8699
|
}
|
|
8308
8700
|
if (match2.schema === "mailto:" && !/^mailto:/i.test(match2.url)) {
|
|
8309
|
-
match2.url =
|
|
8701
|
+
match2.url = `mailto:${match2.url}`;
|
|
8310
8702
|
}
|
|
8311
8703
|
};
|
|
8312
8704
|
LinkifyIt.prototype.onCompile = function onCompile() {
|
|
@@ -9352,6 +9744,9 @@ var WeixinAdapter = class extends BaseChannelAdapter {
|
|
|
9352
9744
|
consecutiveFailures = /* @__PURE__ */ new Map();
|
|
9353
9745
|
typingTickets = /* @__PURE__ */ new Map();
|
|
9354
9746
|
pendingCursors = /* @__PURE__ */ new Map();
|
|
9747
|
+
pollCursors = /* @__PURE__ */ new Map();
|
|
9748
|
+
cursorGenerations = /* @__PURE__ */ new Map();
|
|
9749
|
+
cursorRecoveryGates = /* @__PURE__ */ new Map();
|
|
9355
9750
|
nextBatchId = 1;
|
|
9356
9751
|
constructor(instance) {
|
|
9357
9752
|
super();
|
|
@@ -9392,6 +9787,10 @@ var WeixinAdapter = class extends BaseChannelAdapter {
|
|
|
9392
9787
|
this.stopAccountWorker(accountId);
|
|
9393
9788
|
}
|
|
9394
9789
|
this.pendingCursors.clear();
|
|
9790
|
+
this.pollCursors.clear();
|
|
9791
|
+
this.cursorGenerations.clear();
|
|
9792
|
+
for (const gate of this.cursorRecoveryGates.values()) gate.resolve();
|
|
9793
|
+
this.cursorRecoveryGates.clear();
|
|
9395
9794
|
this.clearInboundQueue();
|
|
9396
9795
|
this.rejectPendingInboundConsumers();
|
|
9397
9796
|
console.log("[weixin-adapter] Stopped");
|
|
@@ -9451,11 +9850,11 @@ var WeixinAdapter = class extends BaseChannelAdapter {
|
|
|
9451
9850
|
isAuthorized(_userId, _chatId) {
|
|
9452
9851
|
return true;
|
|
9453
9852
|
}
|
|
9454
|
-
acknowledgeUpdate(updateId) {
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
this.
|
|
9853
|
+
acknowledgeUpdate(updateId, messageId) {
|
|
9854
|
+
this.settlePendingCursorMessage(updateId, messageId, false);
|
|
9855
|
+
}
|
|
9856
|
+
rejectUpdate(updateId, messageId) {
|
|
9857
|
+
this.settlePendingCursorMessage(updateId, messageId, true);
|
|
9459
9858
|
}
|
|
9460
9859
|
onMessageStart(chatId) {
|
|
9461
9860
|
this.sendTypingIndicator(chatId, TypingStatus.TYPING).catch(() => {
|
|
@@ -9507,6 +9906,7 @@ var WeixinAdapter = class extends BaseChannelAdapter {
|
|
|
9507
9906
|
this.pollAborts.set(accountId, controller);
|
|
9508
9907
|
this.seenMessageIds.set(accountId, /* @__PURE__ */ new Set());
|
|
9509
9908
|
this.consecutiveFailures.set(accountId, 0);
|
|
9909
|
+
this.cursorGenerations.set(accountId, 0);
|
|
9510
9910
|
void this.runPollLoop(accountId, creds, controller.signal);
|
|
9511
9911
|
}
|
|
9512
9912
|
stopAccountWorker(accountId) {
|
|
@@ -9515,6 +9915,14 @@ var WeixinAdapter = class extends BaseChannelAdapter {
|
|
|
9515
9915
|
this.workerSignatures.delete(accountId);
|
|
9516
9916
|
this.seenMessageIds.delete(accountId);
|
|
9517
9917
|
this.consecutiveFailures.delete(accountId);
|
|
9918
|
+
this.pollCursors.delete(accountId);
|
|
9919
|
+
this.cursorGenerations.delete(accountId);
|
|
9920
|
+
for (const [batchId, batch] of this.pendingCursors) {
|
|
9921
|
+
if (batch.accountId === accountId) this.pendingCursors.delete(batchId);
|
|
9922
|
+
}
|
|
9923
|
+
const recoveryGate = this.cursorRecoveryGates.get(accountId);
|
|
9924
|
+
recoveryGate?.resolve();
|
|
9925
|
+
this.cursorRecoveryGates.delete(accountId);
|
|
9518
9926
|
for (const key of Array.from(this.typingTickets.keys())) {
|
|
9519
9927
|
if (key.startsWith(`${accountId}:`)) {
|
|
9520
9928
|
this.typingTickets.delete(key);
|
|
@@ -9529,11 +9937,20 @@ var WeixinAdapter = class extends BaseChannelAdapter {
|
|
|
9529
9937
|
continue;
|
|
9530
9938
|
}
|
|
9531
9939
|
try {
|
|
9940
|
+
await this.waitForCursorRecovery(accountId, signal);
|
|
9941
|
+
if (signal.aborted) break;
|
|
9532
9942
|
const { store } = getBridgeContext();
|
|
9533
9943
|
const offsetKey = `weixin:${accountId}`;
|
|
9534
9944
|
const rawOffset = store.getChannelOffset(offsetKey);
|
|
9535
|
-
const
|
|
9945
|
+
const persistedCursor = rawOffset === "0" ? "" : rawOffset;
|
|
9946
|
+
const cursor = this.pollCursors.get(accountId) ?? persistedCursor;
|
|
9947
|
+
this.pollCursors.set(accountId, cursor);
|
|
9948
|
+
const cursorGeneration = this.cursorGenerations.get(accountId) ?? 0;
|
|
9536
9949
|
const response = await getUpdates(creds, cursor);
|
|
9950
|
+
if (signal.aborted || !this._running) break;
|
|
9951
|
+
if ((this.cursorGenerations.get(accountId) ?? 0) !== cursorGeneration || this.cursorRecoveryGates.has(accountId)) {
|
|
9952
|
+
continue;
|
|
9953
|
+
}
|
|
9537
9954
|
if (response.errcode === ERRCODE_SESSION_EXPIRED) {
|
|
9538
9955
|
setPaused(accountId, "Session expired (errcode -14)");
|
|
9539
9956
|
console.warn(`[weixin-adapter] Account ${accountId} session expired, pausing`);
|
|
@@ -9542,34 +9959,33 @@ var WeixinAdapter = class extends BaseChannelAdapter {
|
|
|
9542
9959
|
if (response.errcode && response.errcode !== 0) {
|
|
9543
9960
|
throw new Error(`API error: ${response.errcode} ${response.errmsg || ""}`.trim());
|
|
9544
9961
|
}
|
|
9545
|
-
|
|
9546
|
-
|
|
9547
|
-
if (
|
|
9548
|
-
batchId = this.
|
|
9549
|
-
|
|
9550
|
-
|
|
9551
|
-
|
|
9552
|
-
|
|
9553
|
-
|
|
9554
|
-
|
|
9555
|
-
|
|
9556
|
-
|
|
9962
|
+
const messages = response.msgs ?? [];
|
|
9963
|
+
const nextCursor = response.get_updates_buf;
|
|
9964
|
+
if (nextCursor) {
|
|
9965
|
+
const batchId = this.createPendingCursorBatch(accountId, offsetKey, nextCursor);
|
|
9966
|
+
try {
|
|
9967
|
+
for (const message of messages) {
|
|
9968
|
+
await this.processMessage(accountId, message, batchId);
|
|
9969
|
+
}
|
|
9970
|
+
const batch = this.pendingCursors.get(batchId);
|
|
9971
|
+
if (batch) batch.sealed = true;
|
|
9972
|
+
this.pollCursors.set(accountId, nextCursor);
|
|
9973
|
+
this.maybeCommitPendingCursors(accountId);
|
|
9974
|
+
} catch (error) {
|
|
9975
|
+
const batch = this.pendingCursors.get(batchId);
|
|
9976
|
+
if (batch) {
|
|
9977
|
+
batch.failed = true;
|
|
9978
|
+
batch.sealed = true;
|
|
9979
|
+
this.ensureCursorRecoveryGate(accountId);
|
|
9980
|
+
}
|
|
9981
|
+
this.maybeCommitPendingCursors(accountId);
|
|
9982
|
+
throw error;
|
|
9557
9983
|
}
|
|
9558
|
-
|
|
9559
|
-
|
|
9560
|
-
for (const message of response.msgs) {
|
|
9984
|
+
} else {
|
|
9985
|
+
for (const message of messages) {
|
|
9561
9986
|
await this.processMessage(accountId, message);
|
|
9562
9987
|
}
|
|
9563
9988
|
}
|
|
9564
|
-
if (batchId !== void 0 && response.get_updates_buf) {
|
|
9565
|
-
const batch = this.pendingCursors.get(batchId);
|
|
9566
|
-
if (batchCompleted && batch) {
|
|
9567
|
-
batch.sealed = true;
|
|
9568
|
-
this.maybeCommitPendingCursor(batchId);
|
|
9569
|
-
} else if (!batchCompleted) {
|
|
9570
|
-
this.pendingCursors.delete(batchId);
|
|
9571
|
-
}
|
|
9572
|
-
}
|
|
9573
9989
|
this.consecutiveFailures.set(accountId, 0);
|
|
9574
9990
|
} catch (err) {
|
|
9575
9991
|
if (signal.aborted) break;
|
|
@@ -9587,12 +10003,20 @@ var WeixinAdapter = class extends BaseChannelAdapter {
|
|
|
9587
10003
|
}
|
|
9588
10004
|
async processMessage(accountId, message, batchId) {
|
|
9589
10005
|
if (!message.from_user_id) return;
|
|
9590
|
-
const
|
|
10006
|
+
const fallbackSequence = message.seq ?? message.create_time ?? "unknown";
|
|
10007
|
+
const inboundMessageId = message.message_id || `weixin_${accountId}_${message.from_user_id}_${fallbackSequence}`;
|
|
10008
|
+
const messageKey = inboundMessageId;
|
|
10009
|
+
if (getBridgeContext().store.checkDedup(buildInboundDedupKey(this.channelType, inboundMessageId))) {
|
|
10010
|
+
return;
|
|
10011
|
+
}
|
|
9591
10012
|
const seenIds = this.seenMessageIds.get(accountId);
|
|
9592
10013
|
if (seenIds?.has(messageKey)) {
|
|
9593
10014
|
return;
|
|
9594
10015
|
}
|
|
9595
10016
|
seenIds?.add(messageKey);
|
|
10017
|
+
if (batchId !== void 0) {
|
|
10018
|
+
this.pendingCursors.get(batchId)?.observedMessageIds.add(messageKey);
|
|
10019
|
+
}
|
|
9596
10020
|
if (seenIds && seenIds.size > DEDUP_MAX2) {
|
|
9597
10021
|
const overflow = Array.from(seenIds).slice(0, seenIds.size - DEDUP_MAX2);
|
|
9598
10022
|
for (const staleKey of overflow) {
|
|
@@ -9656,7 +10080,7 @@ ${failureNote}` : attachments.length > 0 ? failureNote : text2;
|
|
|
9656
10080
|
}
|
|
9657
10081
|
const chatId = encodeWeixinChatId(accountId, message.from_user_id);
|
|
9658
10082
|
const inbound = {
|
|
9659
|
-
messageId:
|
|
10083
|
+
messageId: inboundMessageId,
|
|
9660
10084
|
address: {
|
|
9661
10085
|
channelType: this.channelType,
|
|
9662
10086
|
channelProvider: this.provider,
|
|
@@ -9686,7 +10110,10 @@ ${failureNote}` : attachments.length > 0 ? failureNote : text2;
|
|
|
9686
10110
|
}
|
|
9687
10111
|
if (batchId !== void 0) {
|
|
9688
10112
|
const batch = this.pendingCursors.get(batchId);
|
|
9689
|
-
if (batch
|
|
10113
|
+
if (batch && !batch.messageIds.has(inbound.messageId)) {
|
|
10114
|
+
batch.messageIds.add(inbound.messageId);
|
|
10115
|
+
batch.remaining++;
|
|
10116
|
+
}
|
|
9690
10117
|
}
|
|
9691
10118
|
this.enqueueInboundMessage(inbound);
|
|
9692
10119
|
const summary = attachments.length > 0 ? `[${attachments.length} attachment(s)] ${inbound.text.slice(0, 150)}` : missingVoiceTranscriptCount > 0 && !inbound.text ? "[voice transcription unavailable]" : failedCount > 0 && !inbound.text ? `[${failedCount} attachment(s) failed]` : inbound.text.slice(0, 200);
|
|
@@ -9748,13 +10175,103 @@ ${failureNote}` : attachments.length > 0 ? failureNote : text2;
|
|
|
9748
10175
|
);
|
|
9749
10176
|
});
|
|
9750
10177
|
}
|
|
9751
|
-
|
|
10178
|
+
createPendingCursorBatch(accountId, offsetKey, cursor) {
|
|
10179
|
+
const batchId = this.nextBatchId++;
|
|
10180
|
+
this.pendingCursors.set(batchId, {
|
|
10181
|
+
accountId,
|
|
10182
|
+
offsetKey,
|
|
10183
|
+
cursor,
|
|
10184
|
+
remaining: 0,
|
|
10185
|
+
sealed: false,
|
|
10186
|
+
failed: false,
|
|
10187
|
+
messageIds: /* @__PURE__ */ new Set(),
|
|
10188
|
+
observedMessageIds: /* @__PURE__ */ new Set(),
|
|
10189
|
+
settledMessageIds: /* @__PURE__ */ new Set()
|
|
10190
|
+
});
|
|
10191
|
+
return batchId;
|
|
10192
|
+
}
|
|
10193
|
+
settlePendingCursorMessage(updateId, messageId, failed) {
|
|
9752
10194
|
const batch = this.pendingCursors.get(updateId);
|
|
9753
|
-
if (!batch
|
|
10195
|
+
if (!batch) return;
|
|
10196
|
+
const targetMessageId = messageId && batch.messageIds.has(messageId) ? messageId : Array.from(batch.messageIds).find((id) => !batch.settledMessageIds.has(id));
|
|
10197
|
+
if (!targetMessageId || batch.settledMessageIds.has(targetMessageId)) return;
|
|
10198
|
+
batch.settledMessageIds.add(targetMessageId);
|
|
10199
|
+
batch.remaining = Math.max(0, batch.remaining - 1);
|
|
10200
|
+
if (failed) {
|
|
10201
|
+
batch.failed = true;
|
|
10202
|
+
this.ensureCursorRecoveryGate(batch.accountId);
|
|
10203
|
+
}
|
|
10204
|
+
this.maybeCommitPendingCursors(batch.accountId);
|
|
10205
|
+
}
|
|
10206
|
+
maybeCommitPendingCursors(accountId) {
|
|
10207
|
+
const accountBatches = () => Array.from(this.pendingCursors.entries()).filter(([, batch]) => batch.accountId === accountId).sort(([left], [right]) => left - right);
|
|
10208
|
+
let batches = accountBatches();
|
|
10209
|
+
while (batches.length > 0) {
|
|
10210
|
+
const [batchId, batch] = batches[0];
|
|
10211
|
+
if (!batch.sealed || batch.remaining > 0 || batch.failed) break;
|
|
10212
|
+
try {
|
|
10213
|
+
getBridgeContext().store.setChannelOffset(batch.offsetKey, batch.cursor);
|
|
10214
|
+
} catch (error) {
|
|
10215
|
+
batch.failed = true;
|
|
10216
|
+
this.ensureCursorRecoveryGate(accountId);
|
|
10217
|
+
console.error(
|
|
10218
|
+
`[weixin-adapter] Failed to commit cursor for ${accountId}:`,
|
|
10219
|
+
error instanceof Error ? error.message : error
|
|
10220
|
+
);
|
|
10221
|
+
break;
|
|
10222
|
+
}
|
|
10223
|
+
this.pendingCursors.delete(batchId);
|
|
10224
|
+
batches = accountBatches();
|
|
10225
|
+
}
|
|
10226
|
+
batches = accountBatches();
|
|
10227
|
+
if (!batches.some(([, batch]) => batch.failed)) return;
|
|
10228
|
+
this.ensureCursorRecoveryGate(accountId);
|
|
10229
|
+
if (!batches.every(([, batch]) => batch.sealed && batch.remaining === 0)) return;
|
|
10230
|
+
const offsetKey = batches[0][1].offsetKey;
|
|
10231
|
+
let persistedCursor;
|
|
10232
|
+
try {
|
|
10233
|
+
const rawOffset = getBridgeContext().store.getChannelOffset(offsetKey);
|
|
10234
|
+
persistedCursor = rawOffset === "0" ? "" : rawOffset;
|
|
10235
|
+
} catch (error) {
|
|
10236
|
+
console.error(
|
|
10237
|
+
`[weixin-adapter] Failed to reload cursor for ${accountId}:`,
|
|
10238
|
+
error instanceof Error ? error.message : error
|
|
10239
|
+
);
|
|
9754
10240
|
return;
|
|
9755
10241
|
}
|
|
9756
|
-
|
|
9757
|
-
|
|
10242
|
+
const seenIds = this.seenMessageIds.get(accountId);
|
|
10243
|
+
for (const [batchId, batch] of batches) {
|
|
10244
|
+
for (const messageKey of batch.observedMessageIds) seenIds?.delete(messageKey);
|
|
10245
|
+
this.pendingCursors.delete(batchId);
|
|
10246
|
+
}
|
|
10247
|
+
this.pollCursors.set(accountId, persistedCursor);
|
|
10248
|
+
this.cursorGenerations.set(accountId, (this.cursorGenerations.get(accountId) ?? 0) + 1);
|
|
10249
|
+
const gate = this.cursorRecoveryGates.get(accountId);
|
|
10250
|
+
this.cursorRecoveryGates.delete(accountId);
|
|
10251
|
+
gate?.resolve();
|
|
10252
|
+
}
|
|
10253
|
+
ensureCursorRecoveryGate(accountId) {
|
|
10254
|
+
const existing = this.cursorRecoveryGates.get(accountId);
|
|
10255
|
+
if (existing) return existing;
|
|
10256
|
+
let resolve2;
|
|
10257
|
+
const promise = new Promise((done) => {
|
|
10258
|
+
resolve2 = done;
|
|
10259
|
+
});
|
|
10260
|
+
const gate = { promise, resolve: resolve2 };
|
|
10261
|
+
this.cursorRecoveryGates.set(accountId, gate);
|
|
10262
|
+
return gate;
|
|
10263
|
+
}
|
|
10264
|
+
waitForCursorRecovery(accountId, signal) {
|
|
10265
|
+
const gate = this.cursorRecoveryGates.get(accountId);
|
|
10266
|
+
if (!gate || signal.aborted) return Promise.resolve();
|
|
10267
|
+
return new Promise((resolve2) => {
|
|
10268
|
+
const finish = () => {
|
|
10269
|
+
signal.removeEventListener("abort", finish);
|
|
10270
|
+
resolve2();
|
|
10271
|
+
};
|
|
10272
|
+
signal.addEventListener("abort", finish, { once: true });
|
|
10273
|
+
gate.promise.then(finish, finish);
|
|
10274
|
+
});
|
|
9758
10275
|
}
|
|
9759
10276
|
filterConfiguredAccounts(accounts) {
|
|
9760
10277
|
if (!this.configuredAccountId) return accounts;
|
|
@@ -9810,10 +10327,10 @@ function recordBindingChange(store, input) {
|
|
|
9810
10327
|
}
|
|
9811
10328
|
|
|
9812
10329
|
// src/desktop-sessions.ts
|
|
9813
|
-
import
|
|
10330
|
+
import fs5 from "node:fs";
|
|
9814
10331
|
import os2 from "node:os";
|
|
9815
10332
|
import path4 from "node:path";
|
|
9816
|
-
import
|
|
10333
|
+
import crypto7 from "node:crypto";
|
|
9817
10334
|
import { DatabaseSync } from "node:sqlite";
|
|
9818
10335
|
var ACTIVE_WINDOW_MS = 15 * 60 * 1e3;
|
|
9819
10336
|
var MAX_SESSION_META_BYTES = 4 * 1024 * 1024;
|
|
@@ -9838,13 +10355,13 @@ function getDesktopStateDbPath() {
|
|
|
9838
10355
|
const codexHome = getCodexHome();
|
|
9839
10356
|
let entries;
|
|
9840
10357
|
try {
|
|
9841
|
-
entries =
|
|
10358
|
+
entries = fs5.readdirSync(codexHome, { withFileTypes: true });
|
|
9842
10359
|
} catch {
|
|
9843
10360
|
return null;
|
|
9844
10361
|
}
|
|
9845
10362
|
const candidates = entries.filter((entry) => entry.isFile() && /^state_\d+\.sqlite$/i.test(entry.name)).map((entry) => path4.join(codexHome, entry.name)).sort((left, right) => {
|
|
9846
10363
|
try {
|
|
9847
|
-
return
|
|
10364
|
+
return fs5.statSync(right).mtimeMs - fs5.statSync(left).mtimeMs;
|
|
9848
10365
|
} catch {
|
|
9849
10366
|
return 0;
|
|
9850
10367
|
}
|
|
@@ -9869,10 +10386,10 @@ function isInternalSkillWorkspace(cwd) {
|
|
|
9869
10386
|
}
|
|
9870
10387
|
function loadSavedWorkspaceRoots() {
|
|
9871
10388
|
const statePath = getCodexGlobalStatePath();
|
|
9872
|
-
if (!
|
|
10389
|
+
if (!fs5.existsSync(statePath)) return null;
|
|
9873
10390
|
let parsed;
|
|
9874
10391
|
try {
|
|
9875
|
-
parsed = JSON.parse(
|
|
10392
|
+
parsed = JSON.parse(fs5.readFileSync(statePath, "utf-8"));
|
|
9876
10393
|
} catch {
|
|
9877
10394
|
return null;
|
|
9878
10395
|
}
|
|
@@ -9887,10 +10404,10 @@ function isWithinSavedWorkspaceRoots(cwd, roots) {
|
|
|
9887
10404
|
}
|
|
9888
10405
|
function loadArchivedThreadIds() {
|
|
9889
10406
|
const archivedRoot = getArchivedSessionsRoot();
|
|
9890
|
-
if (!
|
|
10407
|
+
if (!fs5.existsSync(archivedRoot)) return /* @__PURE__ */ new Set();
|
|
9891
10408
|
let entries;
|
|
9892
10409
|
try {
|
|
9893
|
-
entries =
|
|
10410
|
+
entries = fs5.readdirSync(archivedRoot, { withFileTypes: true });
|
|
9894
10411
|
} catch {
|
|
9895
10412
|
return /* @__PURE__ */ new Set();
|
|
9896
10413
|
}
|
|
@@ -9903,13 +10420,13 @@ function loadArchivedThreadIds() {
|
|
|
9903
10420
|
return ids;
|
|
9904
10421
|
}
|
|
9905
10422
|
function readFirstLine(filePath, maxBytes = MAX_SESSION_META_BYTES) {
|
|
9906
|
-
const fd =
|
|
10423
|
+
const fd = fs5.openSync(filePath, "r");
|
|
9907
10424
|
try {
|
|
9908
10425
|
const chunks = [];
|
|
9909
10426
|
let bytesReadTotal = 0;
|
|
9910
10427
|
const buffer = Buffer.alloc(4096);
|
|
9911
10428
|
while (bytesReadTotal < maxBytes) {
|
|
9912
|
-
const bytesRead =
|
|
10429
|
+
const bytesRead = fs5.readSync(fd, buffer, 0, buffer.length, bytesReadTotal);
|
|
9913
10430
|
if (bytesRead <= 0) break;
|
|
9914
10431
|
const slice = Buffer.from(buffer.subarray(0, bytesRead));
|
|
9915
10432
|
chunks.push(slice);
|
|
@@ -9922,31 +10439,31 @@ function readFirstLine(filePath, maxBytes = MAX_SESSION_META_BYTES) {
|
|
|
9922
10439
|
}
|
|
9923
10440
|
return Buffer.concat(chunks).toString("utf-8").split(/\r?\n/, 1)[0] || "";
|
|
9924
10441
|
} finally {
|
|
9925
|
-
|
|
10442
|
+
fs5.closeSync(fd);
|
|
9926
10443
|
}
|
|
9927
10444
|
}
|
|
9928
10445
|
function readFilePrefix(filePath, maxBytes = MAX_SESSION_TITLE_SCAN_BYTES) {
|
|
9929
|
-
const fd =
|
|
10446
|
+
const fd = fs5.openSync(filePath, "r");
|
|
9930
10447
|
try {
|
|
9931
10448
|
const buffer = Buffer.alloc(Math.min(maxBytes, 64 * 1024));
|
|
9932
10449
|
const chunks = [];
|
|
9933
10450
|
let offset = 0;
|
|
9934
10451
|
while (offset < maxBytes) {
|
|
9935
10452
|
const bytesToRead = Math.min(buffer.length, maxBytes - offset);
|
|
9936
|
-
const bytesRead =
|
|
10453
|
+
const bytesRead = fs5.readSync(fd, buffer, 0, bytesToRead, offset);
|
|
9937
10454
|
if (bytesRead <= 0) break;
|
|
9938
10455
|
chunks.push(Buffer.from(buffer.subarray(0, bytesRead)));
|
|
9939
10456
|
offset += bytesRead;
|
|
9940
10457
|
}
|
|
9941
10458
|
return Buffer.concat(chunks).toString("utf-8");
|
|
9942
10459
|
} finally {
|
|
9943
|
-
|
|
10460
|
+
fs5.closeSync(fd);
|
|
9944
10461
|
}
|
|
9945
10462
|
}
|
|
9946
10463
|
function walkSessionFiles(dirPath, target) {
|
|
9947
10464
|
let entries;
|
|
9948
10465
|
try {
|
|
9949
|
-
entries =
|
|
10466
|
+
entries = fs5.readdirSync(dirPath, { withFileTypes: true });
|
|
9950
10467
|
} catch {
|
|
9951
10468
|
return;
|
|
9952
10469
|
}
|
|
@@ -9969,10 +10486,10 @@ function isDesktopLike(meta) {
|
|
|
9969
10486
|
}
|
|
9970
10487
|
function loadThreadIndexEntries(archivedThreadIds) {
|
|
9971
10488
|
const indexPath = getSessionIndexPath();
|
|
9972
|
-
if (!
|
|
10489
|
+
if (!fs5.existsSync(indexPath)) return /* @__PURE__ */ new Map();
|
|
9973
10490
|
let content = "";
|
|
9974
10491
|
try {
|
|
9975
|
-
content =
|
|
10492
|
+
content = fs5.readFileSync(indexPath, "utf-8");
|
|
9976
10493
|
} catch {
|
|
9977
10494
|
return /* @__PURE__ */ new Map();
|
|
9978
10495
|
}
|
|
@@ -10012,7 +10529,7 @@ function parseUpdatedAtValue(value) {
|
|
|
10012
10529
|
}
|
|
10013
10530
|
function loadVisibleDesktopThreads(limit) {
|
|
10014
10531
|
const dbPath = getDesktopStateDbPath();
|
|
10015
|
-
if (!dbPath || !
|
|
10532
|
+
if (!dbPath || !fs5.existsSync(dbPath)) return null;
|
|
10016
10533
|
let db = null;
|
|
10017
10534
|
try {
|
|
10018
10535
|
db = new DatabaseSync(dbPath, { readOnly: true });
|
|
@@ -10079,7 +10596,7 @@ function parseDesktopSession(filePath, threadIndexEntries, archivedThreadIds) {
|
|
|
10079
10596
|
}
|
|
10080
10597
|
let stat;
|
|
10081
10598
|
try {
|
|
10082
|
-
stat =
|
|
10599
|
+
stat = fs5.statSync(filePath);
|
|
10083
10600
|
} catch {
|
|
10084
10601
|
return null;
|
|
10085
10602
|
}
|
|
@@ -10268,25 +10785,34 @@ function formatDesktopToolName(namespaceValue, nameValue) {
|
|
|
10268
10785
|
return namespace.endsWith("__") || namespace.endsWith("/") || namespace.endsWith(".") ? `${namespace}${name}` : `${namespace}__${name}`;
|
|
10269
10786
|
}
|
|
10270
10787
|
function createDesktopEventSignature(rawLine) {
|
|
10271
|
-
return
|
|
10788
|
+
return crypto7.createHash("sha1").update(rawLine).digest("hex");
|
|
10272
10789
|
}
|
|
10273
|
-
function
|
|
10790
|
+
function readCompleteUtf8LineRange(filePath, startOffset, endOffset) {
|
|
10274
10791
|
const safeStart = Math.max(0, startOffset);
|
|
10275
10792
|
const safeEnd = Math.max(safeStart, endOffset);
|
|
10276
10793
|
const bytesToRead = safeEnd - safeStart;
|
|
10277
|
-
if (bytesToRead <= 0) return "";
|
|
10278
|
-
const fd =
|
|
10794
|
+
if (bytesToRead <= 0) return { content: "", nextOffset: safeStart };
|
|
10795
|
+
const fd = fs5.openSync(filePath, "r");
|
|
10279
10796
|
try {
|
|
10280
10797
|
const buffer = Buffer.alloc(bytesToRead);
|
|
10281
10798
|
let totalRead = 0;
|
|
10282
10799
|
while (totalRead < bytesToRead) {
|
|
10283
|
-
const bytesRead =
|
|
10800
|
+
const bytesRead = fs5.readSync(fd, buffer, totalRead, bytesToRead - totalRead, safeStart + totalRead);
|
|
10284
10801
|
if (bytesRead <= 0) break;
|
|
10285
10802
|
totalRead += bytesRead;
|
|
10286
10803
|
}
|
|
10287
|
-
|
|
10804
|
+
const readBuffer = buffer.subarray(0, totalRead);
|
|
10805
|
+
const lastNewlineIndex = readBuffer.lastIndexOf(10);
|
|
10806
|
+
if (lastNewlineIndex < 0) {
|
|
10807
|
+
return { content: "", nextOffset: safeStart };
|
|
10808
|
+
}
|
|
10809
|
+
const consumedBytes = lastNewlineIndex + 1;
|
|
10810
|
+
return {
|
|
10811
|
+
content: readBuffer.subarray(0, consumedBytes).toString("utf-8"),
|
|
10812
|
+
nextOffset: safeStart + consumedBytes
|
|
10813
|
+
};
|
|
10288
10814
|
} finally {
|
|
10289
|
-
|
|
10815
|
+
fs5.closeSync(fd);
|
|
10290
10816
|
}
|
|
10291
10817
|
}
|
|
10292
10818
|
function isSessionEventLine(line) {
|
|
@@ -10301,6 +10827,7 @@ function isTurnContextLine(line) {
|
|
|
10301
10827
|
var IGNORED_EVENT_MSG_TYPES = /* @__PURE__ */ new Set([
|
|
10302
10828
|
"context_compacted",
|
|
10303
10829
|
"thread_settings_applied",
|
|
10830
|
+
"thread_goal_updated",
|
|
10304
10831
|
"thread_name_updated",
|
|
10305
10832
|
"thread_rolled_back",
|
|
10306
10833
|
"token_count"
|
|
@@ -10309,6 +10836,12 @@ var IGNORED_RESPONSE_ITEM_TYPES = /* @__PURE__ */ new Set([
|
|
|
10309
10836
|
"image_generation_call",
|
|
10310
10837
|
"web_search_call"
|
|
10311
10838
|
]);
|
|
10839
|
+
var IGNORED_TOP_LEVEL_TYPES = /* @__PURE__ */ new Set([
|
|
10840
|
+
"compacted",
|
|
10841
|
+
"session_meta",
|
|
10842
|
+
"turn_context",
|
|
10843
|
+
"world_state"
|
|
10844
|
+
]);
|
|
10312
10845
|
var TERMINAL_COMPLETION_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
10313
10846
|
"task_complete",
|
|
10314
10847
|
"turn.completed",
|
|
@@ -10334,6 +10867,10 @@ function extractTerminalCompletionText(payload) {
|
|
|
10334
10867
|
}
|
|
10335
10868
|
return "";
|
|
10336
10869
|
}
|
|
10870
|
+
function isSyntheticDesktopUserContext(text2) {
|
|
10871
|
+
const normalized = text2.trim();
|
|
10872
|
+
return normalized.startsWith("<environment_context>") && normalized.endsWith("</environment_context>");
|
|
10873
|
+
}
|
|
10337
10874
|
function isIgnoredMirrorLineKind(line) {
|
|
10338
10875
|
if (isSessionEventLine(line)) {
|
|
10339
10876
|
const payloadType = typeof line.payload?.type === "string" ? line.payload.type.trim() : "";
|
|
@@ -10343,7 +10880,8 @@ function isIgnoredMirrorLineKind(line) {
|
|
|
10343
10880
|
const payloadType = typeof line.payload?.type === "string" ? line.payload.type.trim() : "";
|
|
10344
10881
|
return IGNORED_RESPONSE_ITEM_TYPES.has(payloadType);
|
|
10345
10882
|
}
|
|
10346
|
-
|
|
10883
|
+
const topLevelType = typeof line.type === "string" ? line.type.trim() : "";
|
|
10884
|
+
return IGNORED_TOP_LEVEL_TYPES.has(topLevelType);
|
|
10347
10885
|
}
|
|
10348
10886
|
function describeUnhandledMirrorLineKind(line) {
|
|
10349
10887
|
if (isIgnoredMirrorLineKind(line)) return null;
|
|
@@ -10355,11 +10893,12 @@ function describeUnhandledMirrorLineKind(line) {
|
|
|
10355
10893
|
const payloadType = typeof line.payload?.type === "string" ? line.payload.type.trim() : "";
|
|
10356
10894
|
return `response_item:${payloadType || "<unknown>"}`;
|
|
10357
10895
|
}
|
|
10358
|
-
|
|
10896
|
+
const topLevelType = typeof line.type === "string" ? line.type.trim() : "";
|
|
10897
|
+
return `top_level:${topLevelType || "<unknown>"}`;
|
|
10359
10898
|
}
|
|
10360
10899
|
function listDesktopSessions(limit) {
|
|
10361
10900
|
const root = getCodexSessionsRoot();
|
|
10362
|
-
if (!
|
|
10901
|
+
if (!fs5.existsSync(root)) return [];
|
|
10363
10902
|
const archivedThreadIds = loadArchivedThreadIds();
|
|
10364
10903
|
const threadIndexEntries = loadThreadIndexEntries(archivedThreadIds);
|
|
10365
10904
|
const savedWorkspaceRoots = loadSavedWorkspaceRoots();
|
|
@@ -10397,9 +10936,17 @@ function listDesktopSessions(limit) {
|
|
|
10397
10936
|
}
|
|
10398
10937
|
return sessions.sort((a, b) => b.lastEventAt.localeCompare(a.lastEventAt)).slice(0, typeof limit === "number" && Number.isFinite(limit) && limit > 0 ? Math.max(1, Math.floor(limit)) : void 0);
|
|
10399
10938
|
}
|
|
10939
|
+
var DESKTOP_SESSION_LOOKUP_CACHE_TTL_MS = 1e3;
|
|
10940
|
+
var desktopSessionLookupCache = null;
|
|
10400
10941
|
function getDesktopSessionByThreadId(threadId) {
|
|
10401
|
-
const
|
|
10402
|
-
|
|
10942
|
+
const timestamp = Date.now();
|
|
10943
|
+
if (!desktopSessionLookupCache || desktopSessionLookupCache.expiresAt <= timestamp) {
|
|
10944
|
+
desktopSessionLookupCache = {
|
|
10945
|
+
expiresAt: timestamp + DESKTOP_SESSION_LOOKUP_CACHE_TTL_MS,
|
|
10946
|
+
sessions: new Map(listDesktopSessions().map((session) => [session.threadId, session]))
|
|
10947
|
+
};
|
|
10948
|
+
}
|
|
10949
|
+
return desktopSessionLookupCache.sessions.get(threadId) || null;
|
|
10403
10950
|
}
|
|
10404
10951
|
function extractDesktopMessageText(line) {
|
|
10405
10952
|
const parts = line.payload?.content?.map((item) => item && typeof item.text === "string" ? item.text : "").filter(Boolean) || [];
|
|
@@ -10636,7 +11183,7 @@ function pushDesktopMirrorEventRecord(records, parsed, rawLine, activeTurnId) {
|
|
|
10636
11183
|
}
|
|
10637
11184
|
if (parsed.payload?.type === "user_message") {
|
|
10638
11185
|
const text2 = extractNormalizedStructuredText(parsed.payload.message);
|
|
10639
|
-
if (!text2) return true;
|
|
11186
|
+
if (!text2 || isSyntheticDesktopUserContext(text2)) return true;
|
|
10640
11187
|
records.push({
|
|
10641
11188
|
signature,
|
|
10642
11189
|
type: "message",
|
|
@@ -10691,6 +11238,26 @@ function pushDesktopMirrorResponseRecord(records, parsed, rawLine, activeTurnId,
|
|
|
10691
11238
|
});
|
|
10692
11239
|
return true;
|
|
10693
11240
|
}
|
|
11241
|
+
if (parsed.payload?.type === "message" && parsed.payload.role === "user") {
|
|
11242
|
+
const text2 = extractDesktopMessageText(parsed);
|
|
11243
|
+
if (!text2 || isSyntheticDesktopUserContext(text2)) return true;
|
|
11244
|
+
const previous = records.at(-1);
|
|
11245
|
+
if (previous?.type === "message" && previous.role === "user" && previous.content === text2 && previous.turnId === (activeTurnId || void 0)) {
|
|
11246
|
+
return true;
|
|
11247
|
+
}
|
|
11248
|
+
records.push({
|
|
11249
|
+
signature,
|
|
11250
|
+
type: "message",
|
|
11251
|
+
role: "user",
|
|
11252
|
+
content: text2,
|
|
11253
|
+
timestamp,
|
|
11254
|
+
...activeTurnId ? { turnId: activeTurnId } : {}
|
|
11255
|
+
});
|
|
11256
|
+
return true;
|
|
11257
|
+
}
|
|
11258
|
+
if (parsed.payload?.type === "message") {
|
|
11259
|
+
return true;
|
|
11260
|
+
}
|
|
10694
11261
|
if (parsed.payload?.type === "tool_search_call") {
|
|
10695
11262
|
const toolId = extractNormalizedFreeText(parsed.payload.call_id) || signature;
|
|
10696
11263
|
records.push({
|
|
@@ -10920,16 +11487,16 @@ ${event.content}` : event.content
|
|
|
10920
11487
|
function readDesktopSessionEventStreamByFilePath(filePath) {
|
|
10921
11488
|
let content = "";
|
|
10922
11489
|
try {
|
|
10923
|
-
content =
|
|
11490
|
+
content = fs5.readFileSync(filePath, "utf-8");
|
|
10924
11491
|
} catch {
|
|
10925
11492
|
return [];
|
|
10926
11493
|
}
|
|
10927
11494
|
return parseDesktopSessionEventText(content, "", true).events;
|
|
10928
11495
|
}
|
|
10929
11496
|
function readDesktopSessionMirrorRecordDeltaByFilePath(filePath, startOffset, endOffset, trailingText = "", currentTurnId = null, currentSpecialCallIds = []) {
|
|
10930
|
-
let
|
|
11497
|
+
let range;
|
|
10931
11498
|
try {
|
|
10932
|
-
|
|
11499
|
+
range = readCompleteUtf8LineRange(filePath, startOffset, endOffset);
|
|
10933
11500
|
} catch {
|
|
10934
11501
|
return {
|
|
10935
11502
|
records: [],
|
|
@@ -10940,10 +11507,16 @@ function readDesktopSessionMirrorRecordDeltaByFilePath(filePath, startOffset, en
|
|
|
10940
11507
|
unknownKinds: []
|
|
10941
11508
|
};
|
|
10942
11509
|
}
|
|
10943
|
-
const parsed = parseDesktopMirrorRecordText(
|
|
11510
|
+
const parsed = parseDesktopMirrorRecordText(
|
|
11511
|
+
range.content,
|
|
11512
|
+
trailingText,
|
|
11513
|
+
false,
|
|
11514
|
+
currentTurnId,
|
|
11515
|
+
currentSpecialCallIds
|
|
11516
|
+
);
|
|
10944
11517
|
return {
|
|
10945
11518
|
records: parsed.records,
|
|
10946
|
-
nextOffset:
|
|
11519
|
+
nextOffset: range.nextOffset,
|
|
10947
11520
|
trailingText: parsed.trailingText,
|
|
10948
11521
|
nextTurnId: parsed.nextTurnId,
|
|
10949
11522
|
nextSpecialCallIds: parsed.nextSpecialCallIds,
|
|
@@ -11103,14 +11676,14 @@ function bindStoreToSdkSession(store, channelType, chatId, sdkSessionId, opts) {
|
|
|
11103
11676
|
}
|
|
11104
11677
|
|
|
11105
11678
|
// src/internal-sessions.ts
|
|
11106
|
-
import
|
|
11679
|
+
import fs6 from "node:fs";
|
|
11107
11680
|
import path6 from "node:path";
|
|
11108
11681
|
var DRAFT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
11109
11682
|
var MAX_HIDDEN_DRAFT_SESSIONS = 64;
|
|
11110
11683
|
var INTERNAL_SESSION_ROOT = path6.join(CTI_HOME, "runtime", "internal-sessions");
|
|
11111
11684
|
var DRAFT_SESSION_PREFIX = "Draft";
|
|
11112
11685
|
function ensureDirectory(dirPath) {
|
|
11113
|
-
|
|
11686
|
+
fs6.mkdirSync(dirPath, { recursive: true });
|
|
11114
11687
|
}
|
|
11115
11688
|
function sanitizePathSlug(raw) {
|
|
11116
11689
|
return raw.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "scratch";
|
|
@@ -12601,6 +13174,10 @@ function filterSuppressedMirrorRecords(store, sessionId, records, config2, nowMs
|
|
|
12601
13174
|
break;
|
|
12602
13175
|
}
|
|
12603
13176
|
if (record.type === "message" && record.role === "user") {
|
|
13177
|
+
if (isSyntheticDesktopUserContext(record.content || "")) {
|
|
13178
|
+
handled = true;
|
|
13179
|
+
break;
|
|
13180
|
+
}
|
|
12604
13181
|
if (suppression.promptText && normalizedContent === suppression.promptText) {
|
|
12605
13182
|
suppression.awaitingPromptMatch = false;
|
|
12606
13183
|
suppression.droppingTurn = true;
|
|
@@ -12874,15 +13451,32 @@ function createAdapterRuntime(getState2, deps) {
|
|
|
12874
13451
|
const msg = await adapter.consumeOne();
|
|
12875
13452
|
if (!msg) continue;
|
|
12876
13453
|
if (msg.callbackData || msg.text.trim().startsWith("/") || deps.isNumericPermissionShortcut(adapter.provider, msg.text.trim(), msg.address.chatId)) {
|
|
12877
|
-
|
|
13454
|
+
try {
|
|
13455
|
+
await deps.handleMessage(adapter, msg);
|
|
13456
|
+
} catch (error) {
|
|
13457
|
+
if (msg.updateId != null) {
|
|
13458
|
+
adapter.rejectUpdate?.(msg.updateId, msg.messageId);
|
|
13459
|
+
}
|
|
13460
|
+
throw error;
|
|
13461
|
+
}
|
|
12878
13462
|
} else {
|
|
12879
|
-
|
|
12880
|
-
|
|
12881
|
-
|
|
12882
|
-
|
|
12883
|
-
|
|
12884
|
-
|
|
12885
|
-
|
|
13463
|
+
try {
|
|
13464
|
+
const sessionId = deps.resolveSessionIdForMessage(msg);
|
|
13465
|
+
deps.processWithSessionLock(
|
|
13466
|
+
sessionId,
|
|
13467
|
+
() => deps.handleMessage(adapter, msg)
|
|
13468
|
+
).catch((err) => {
|
|
13469
|
+
if (msg.updateId != null) {
|
|
13470
|
+
adapter.rejectUpdate?.(msg.updateId, msg.messageId);
|
|
13471
|
+
}
|
|
13472
|
+
console.error(`[bridge-manager] Session ${sessionId.slice(0, 8)} error:`, err);
|
|
13473
|
+
});
|
|
13474
|
+
} catch (error) {
|
|
13475
|
+
if (msg.updateId != null) {
|
|
13476
|
+
adapter.rejectUpdate?.(msg.updateId, msg.messageId);
|
|
13477
|
+
}
|
|
13478
|
+
throw error;
|
|
13479
|
+
}
|
|
12886
13480
|
}
|
|
12887
13481
|
} catch (err) {
|
|
12888
13482
|
if (abort.signal.aborted) break;
|
|
@@ -12925,11 +13519,11 @@ function createAdapterRuntime(getState2, deps) {
|
|
|
12925
13519
|
}
|
|
12926
13520
|
|
|
12927
13521
|
// src/lib/bridge/bridge-session-support.ts
|
|
12928
|
-
import
|
|
13522
|
+
import fs8 from "node:fs";
|
|
12929
13523
|
import path10 from "node:path";
|
|
12930
13524
|
|
|
12931
13525
|
// src/codex-models.ts
|
|
12932
|
-
import
|
|
13526
|
+
import fs7 from "node:fs";
|
|
12933
13527
|
import os3 from "node:os";
|
|
12934
13528
|
import path9 from "node:path";
|
|
12935
13529
|
var DEFAULT_CODEX_CONFIG_PATH = path9.join(os3.homedir(), ".codex", "config.toml");
|
|
@@ -12982,7 +13576,7 @@ var KNOWN_CODEX_MODEL_FALLBACKS = [
|
|
|
12982
13576
|
];
|
|
12983
13577
|
function readConfiguredCodexModel(configPath = DEFAULT_CODEX_CONFIG_PATH) {
|
|
12984
13578
|
try {
|
|
12985
|
-
const raw =
|
|
13579
|
+
const raw = fs7.readFileSync(configPath, "utf-8");
|
|
12986
13580
|
let inSection = false;
|
|
12987
13581
|
for (const line of raw.split(/\r?\n/)) {
|
|
12988
13582
|
const trimmed = line.trim();
|
|
@@ -13026,7 +13620,7 @@ function parseSupportedReasoningLevels(value) {
|
|
|
13026
13620
|
}
|
|
13027
13621
|
function listCachedCodexModels(cachePath = DEFAULT_CODEX_MODELS_CACHE_PATH) {
|
|
13028
13622
|
try {
|
|
13029
|
-
const raw =
|
|
13623
|
+
const raw = fs7.readFileSync(cachePath, "utf-8");
|
|
13030
13624
|
const parsed = JSON.parse(raw);
|
|
13031
13625
|
if (!Array.isArray(parsed.models)) return [];
|
|
13032
13626
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -13212,7 +13806,7 @@ function resolveNewSessionWorkingDirectory(rawArgs, binding, session) {
|
|
|
13212
13806
|
return { ok: true, workDir: validated };
|
|
13213
13807
|
}
|
|
13214
13808
|
function ensureWorkingDirectoryExists(workDir) {
|
|
13215
|
-
|
|
13809
|
+
fs8.mkdirSync(workDir, { recursive: true });
|
|
13216
13810
|
}
|
|
13217
13811
|
function resetDraftSession2(address) {
|
|
13218
13812
|
const { store } = getBridgeContext();
|
|
@@ -13382,14 +13976,20 @@ async function deliverResponse(adapter, address, responseText, sessionId, replyT
|
|
|
13382
13976
|
replyToMessageId
|
|
13383
13977
|
}, { sessionId });
|
|
13384
13978
|
if (!attachmentResult.ok) {
|
|
13385
|
-
|
|
13979
|
+
const attachmentError = attachmentResult.error || "\u672A\u77E5\u9519\u8BEF";
|
|
13980
|
+
const noticeResult = await deliverTextResponse(
|
|
13386
13981
|
adapter,
|
|
13387
13982
|
address,
|
|
13388
13983
|
`\u9644\u4EF6\u53D1\u9001\u5931\u8D25\uFF1A${attachment.path}
|
|
13389
|
-
${
|
|
13984
|
+
${attachmentError}`,
|
|
13390
13985
|
sessionId,
|
|
13391
13986
|
replyToMessageId
|
|
13392
13987
|
);
|
|
13988
|
+
return {
|
|
13989
|
+
ok: false,
|
|
13990
|
+
messageId: noticeResult.messageId,
|
|
13991
|
+
error: noticeResult.ok ? `\u9644\u4EF6\u53D1\u9001\u5931\u8D25\uFF1A${attachment.path}: ${attachmentError}` : `\u9644\u4EF6\u53D1\u9001\u5931\u8D25\uFF1A${attachment.path}: ${attachmentError}; \u9519\u8BEF\u63D0\u793A\u4E5F\u53D1\u9001\u5931\u8D25\uFF1A${noticeResult.error || "\u672A\u77E5\u9519\u8BEF"}`
|
|
13992
|
+
};
|
|
13393
13993
|
}
|
|
13394
13994
|
lastResult = attachmentResult;
|
|
13395
13995
|
}
|
|
@@ -14115,9 +14715,9 @@ ${truncateHistoryContent(formatStoredMessageContent(message.content))}`;
|
|
|
14115
14715
|
import path13 from "node:path";
|
|
14116
14716
|
|
|
14117
14717
|
// src/lib/bridge/conversation-engine.ts
|
|
14118
|
-
import
|
|
14718
|
+
import fs9 from "fs";
|
|
14119
14719
|
import path12 from "path";
|
|
14120
|
-
import
|
|
14720
|
+
import crypto8 from "crypto";
|
|
14121
14721
|
|
|
14122
14722
|
// src/lib/bridge/turns/final-response-artifacts.ts
|
|
14123
14723
|
function attachmentKey(attachment) {
|
|
@@ -14195,6 +14795,74 @@ async function consumeSseEvents(stream, onEvent) {
|
|
|
14195
14795
|
}
|
|
14196
14796
|
|
|
14197
14797
|
// src/lib/bridge/conversation-engine.ts
|
|
14798
|
+
var MAX_INBOUND_ATTACHMENT_BYTES = 100 * 1024 * 1024;
|
|
14799
|
+
var MAX_INBOUND_ATTACHMENTS_TOTAL_BYTES = 120 * 1024 * 1024;
|
|
14800
|
+
function validateInboundAttachmentSizes(files, maxFileBytes = MAX_INBOUND_ATTACHMENT_BYTES, maxTotalBytes = MAX_INBOUND_ATTACHMENTS_TOTAL_BYTES) {
|
|
14801
|
+
if (!files || files.length === 0) return null;
|
|
14802
|
+
let totalBytes = 0;
|
|
14803
|
+
for (const file of files) {
|
|
14804
|
+
const encodedSize = typeof file.data === "string" ? Buffer.byteLength(file.data, "base64") : 0;
|
|
14805
|
+
const fileSize = Math.max(0, Number(file.size) || 0, encodedSize);
|
|
14806
|
+
if (fileSize > maxFileBytes) {
|
|
14807
|
+
return `Attachment "${file.name}" is too large (${fileSize} bytes; max ${maxFileBytes} bytes).`;
|
|
14808
|
+
}
|
|
14809
|
+
totalBytes += fileSize;
|
|
14810
|
+
if (totalBytes > maxTotalBytes) {
|
|
14811
|
+
return `Attachments are too large in total (${totalBytes} bytes; max ${maxTotalBytes} bytes).`;
|
|
14812
|
+
}
|
|
14813
|
+
}
|
|
14814
|
+
return null;
|
|
14815
|
+
}
|
|
14816
|
+
var DEFAULT_DESKTOP_BUSY_RETRY_DELAYS_MS = [
|
|
14817
|
+
5e3,
|
|
14818
|
+
1e4,
|
|
14819
|
+
15e3,
|
|
14820
|
+
3e4,
|
|
14821
|
+
3e4
|
|
14822
|
+
];
|
|
14823
|
+
function isSessionBusyErrorMessage(message) {
|
|
14824
|
+
return (message || "").toLowerCase().includes("session is busy processing another request");
|
|
14825
|
+
}
|
|
14826
|
+
function isDesktopBackedSessionForBusyRetry(session) {
|
|
14827
|
+
if (session?.thread_origin !== "desktop") return false;
|
|
14828
|
+
return Boolean(session.desktop_thread_id || session.sdk_session_id || session.codex_thread_id);
|
|
14829
|
+
}
|
|
14830
|
+
function getDesktopBusyRetryDelaysMs() {
|
|
14831
|
+
const configured = process.env.CTI_DESKTOP_BUSY_RETRY_DELAYS_MS;
|
|
14832
|
+
if (!configured?.trim()) return DEFAULT_DESKTOP_BUSY_RETRY_DELAYS_MS;
|
|
14833
|
+
const delays = configured.split(",").map((part) => parseInt(part.trim(), 10)).filter((value) => Number.isFinite(value) && value >= 0);
|
|
14834
|
+
return delays.length > 0 ? delays : DEFAULT_DESKTOP_BUSY_RETRY_DELAYS_MS;
|
|
14835
|
+
}
|
|
14836
|
+
function isRetryableDesktopBusyResult(result) {
|
|
14837
|
+
return result.hasError && (result.errorCode === "session_busy" || isSessionBusyErrorMessage(result.errorMessage)) && !result.responseText.trim() && result.outboundAttachments.length === 0 && result.permissionRequests.length === 0;
|
|
14838
|
+
}
|
|
14839
|
+
function formatDesktopBusyRetryNote(attempt, maxAttempts, delayMs) {
|
|
14840
|
+
const delaySeconds = Math.ceil(delayMs / 1e3);
|
|
14841
|
+
const delayText = delaySeconds > 0 ? `${delaySeconds} \u79D2\u540E` : "\u9A6C\u4E0A";
|
|
14842
|
+
return `Codex Desktop thread \u4ECD\u5728\u5904\u7406\u4E0A\u4E00\u8F6E\u8BF7\u6C42\uFF0C${delayText}\u91CD\u8BD5\uFF08${attempt}/${maxAttempts}\uFF09\u3002`;
|
|
14843
|
+
}
|
|
14844
|
+
function buildDesktopBusyExhaustedMessage(originalMessage) {
|
|
14845
|
+
const detail = originalMessage?.trim();
|
|
14846
|
+
return detail ? `Codex Desktop thread \u4ECD\u5728\u5904\u7406\u4E0A\u4E00\u8F6E\u8BF7\u6C42\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002\u539F\u59CB\u9519\u8BEF\uFF1A${detail}` : "Codex Desktop thread \u4ECD\u5728\u5904\u7406\u4E0A\u4E00\u8F6E\u8BF7\u6C42\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
|
|
14847
|
+
}
|
|
14848
|
+
function waitForRetryDelay(delayMs, signal) {
|
|
14849
|
+
if (signal.aborted) return Promise.resolve(false);
|
|
14850
|
+
if (delayMs <= 0) return Promise.resolve(true);
|
|
14851
|
+
return new Promise((resolve2) => {
|
|
14852
|
+
let timer = null;
|
|
14853
|
+
const finish = (completed) => {
|
|
14854
|
+
if (timer) {
|
|
14855
|
+
clearTimeout(timer);
|
|
14856
|
+
timer = null;
|
|
14857
|
+
}
|
|
14858
|
+
signal.removeEventListener("abort", onAbort);
|
|
14859
|
+
resolve2(completed);
|
|
14860
|
+
};
|
|
14861
|
+
const onAbort = () => finish(false);
|
|
14862
|
+
timer = setTimeout(() => finish(true), delayMs);
|
|
14863
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
14864
|
+
});
|
|
14865
|
+
}
|
|
14198
14866
|
function resolveReasoningEffort(store, session) {
|
|
14199
14867
|
return normalizeReasoningEffort(
|
|
14200
14868
|
session?.reasoning_effort || store.getSetting("bridge_codex_reasoning_effort")
|
|
@@ -14237,7 +14905,19 @@ ${attachmentSupplement}` : attachmentSupplement;
|
|
|
14237
14905
|
async function processMessage(binding, text2, onPermissionRequest, abortSignal, files, onPartialText, onToolEvent, onTaskEvent, onStatusNote, onPromptPrepared) {
|
|
14238
14906
|
const { store, llm } = getBridgeContext();
|
|
14239
14907
|
const sessionId = binding.codepilotSessionId;
|
|
14240
|
-
const
|
|
14908
|
+
const attachmentValidationError = validateInboundAttachmentSizes(files);
|
|
14909
|
+
if (attachmentValidationError) {
|
|
14910
|
+
return {
|
|
14911
|
+
responseText: "",
|
|
14912
|
+
outboundAttachments: [],
|
|
14913
|
+
tokenUsage: null,
|
|
14914
|
+
hasError: true,
|
|
14915
|
+
errorMessage: attachmentValidationError,
|
|
14916
|
+
permissionRequests: [],
|
|
14917
|
+
sdkSessionId: null
|
|
14918
|
+
};
|
|
14919
|
+
}
|
|
14920
|
+
const lockId = crypto8.randomBytes(8).toString("hex");
|
|
14241
14921
|
const lockAcquired = store.acquireSessionLock(sessionId, lockId, `bridge-${binding.channelType}`, 600);
|
|
14242
14922
|
if (!lockAcquired) {
|
|
14243
14923
|
return {
|
|
@@ -14246,6 +14926,7 @@ async function processMessage(binding, text2, onPermissionRequest, abortSignal,
|
|
|
14246
14926
|
tokenUsage: null,
|
|
14247
14927
|
hasError: true,
|
|
14248
14928
|
errorMessage: "Session is busy processing another request",
|
|
14929
|
+
errorCode: "session_busy",
|
|
14249
14930
|
permissionRequests: [],
|
|
14250
14931
|
sdkSessionId: null
|
|
14251
14932
|
};
|
|
@@ -14267,16 +14948,21 @@ async function processMessage(binding, text2, onPermissionRequest, abortSignal,
|
|
|
14267
14948
|
let persistedFileMeta = [];
|
|
14268
14949
|
if (files && files.length > 0) {
|
|
14269
14950
|
if (workDir) {
|
|
14951
|
+
const createdFilePaths = [];
|
|
14270
14952
|
try {
|
|
14271
14953
|
const uploadDir = path12.join(workDir, ".codepilot-uploads");
|
|
14272
|
-
if (!
|
|
14273
|
-
|
|
14954
|
+
if (!fs9.existsSync(uploadDir)) {
|
|
14955
|
+
fs9.mkdirSync(uploadDir, { recursive: true });
|
|
14274
14956
|
}
|
|
14275
14957
|
const fileMeta = files.map((f) => {
|
|
14276
14958
|
const safeName = path12.basename(f.name).replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
14277
|
-
const filePath = path12.join(uploadDir, `${
|
|
14959
|
+
const filePath = path12.join(uploadDir, `${crypto8.randomUUID()}-${safeName || "attachment.bin"}`);
|
|
14278
14960
|
const buffer = Buffer.from(f.data, "base64");
|
|
14279
|
-
|
|
14961
|
+
if (buffer.length > MAX_INBOUND_ATTACHMENT_BYTES) {
|
|
14962
|
+
throw new Error(`Attachment "${f.name}" exceeds the maximum size after decoding`);
|
|
14963
|
+
}
|
|
14964
|
+
fs9.writeFileSync(filePath, buffer);
|
|
14965
|
+
createdFilePaths.push(filePath);
|
|
14280
14966
|
return { id: f.id, name: f.name, type: f.type, size: buffer.length, filePath };
|
|
14281
14967
|
});
|
|
14282
14968
|
persistedFileMeta = fileMeta;
|
|
@@ -14286,6 +14972,12 @@ async function processMessage(binding, text2, onPermissionRequest, abortSignal,
|
|
|
14286
14972
|
});
|
|
14287
14973
|
savedContent = `<!--files:${JSON.stringify(fileMeta)}-->${text2}`;
|
|
14288
14974
|
} catch (err) {
|
|
14975
|
+
for (const filePath of createdFilePaths) {
|
|
14976
|
+
try {
|
|
14977
|
+
fs9.rmSync(filePath, { force: true });
|
|
14978
|
+
} catch {
|
|
14979
|
+
}
|
|
14980
|
+
}
|
|
14289
14981
|
console.warn("[conversation-engine] Failed to persist file attachments:", err instanceof Error ? err.message : err);
|
|
14290
14982
|
savedContent = `[${files.length} attachment(s) attached] ${text2}`;
|
|
14291
14983
|
}
|
|
@@ -14331,37 +15023,65 @@ async function processMessage(binding, text2, onPermissionRequest, abortSignal,
|
|
|
14331
15023
|
abortSignal.addEventListener("abort", () => abortController.abort(), { once: true });
|
|
14332
15024
|
}
|
|
14333
15025
|
}
|
|
14334
|
-
const
|
|
14335
|
-
|
|
14336
|
-
|
|
14337
|
-
|
|
14338
|
-
|
|
14339
|
-
|
|
14340
|
-
|
|
14341
|
-
|
|
14342
|
-
|
|
14343
|
-
|
|
14344
|
-
|
|
14345
|
-
|
|
14346
|
-
|
|
14347
|
-
|
|
14348
|
-
|
|
14349
|
-
|
|
14350
|
-
|
|
14351
|
-
|
|
14352
|
-
|
|
15026
|
+
const desktopBusyRetryDelaysMs = isDesktopBackedSessionForBusyRetry(session) ? getDesktopBusyRetryDelaysMs() : [];
|
|
15027
|
+
let desktopBusyRetryCount = 0;
|
|
15028
|
+
while (true) {
|
|
15029
|
+
const stream = llm.streamChat({
|
|
15030
|
+
prompt: promptText,
|
|
15031
|
+
sessionId,
|
|
15032
|
+
sdkSessionId: binding.sdkSessionId || void 0,
|
|
15033
|
+
model: effectiveModel,
|
|
15034
|
+
forceModel: !binding.sdkSessionId && Boolean(effectiveModel),
|
|
15035
|
+
sandboxMode,
|
|
15036
|
+
modelReasoningEffort,
|
|
15037
|
+
systemPrompt: session?.system_prompt || void 0,
|
|
15038
|
+
workingDirectory: workDir || void 0,
|
|
15039
|
+
abortController,
|
|
15040
|
+
permissionMode,
|
|
15041
|
+
provider: resolvedProvider,
|
|
15042
|
+
conversationHistory: historyMsgs,
|
|
15043
|
+
files: llmFiles,
|
|
15044
|
+
onRuntimeStatusChange: (status) => {
|
|
15045
|
+
try {
|
|
15046
|
+
store.setSessionRuntimeStatus(sessionId, status);
|
|
15047
|
+
} catch {
|
|
15048
|
+
}
|
|
14353
15049
|
}
|
|
15050
|
+
});
|
|
15051
|
+
const result = await consumeStream(
|
|
15052
|
+
stream,
|
|
15053
|
+
sessionId,
|
|
15054
|
+
onPermissionRequest,
|
|
15055
|
+
onPartialText,
|
|
15056
|
+
onToolEvent,
|
|
15057
|
+
onTaskEvent,
|
|
15058
|
+
onStatusNote
|
|
15059
|
+
);
|
|
15060
|
+
if (!isRetryableDesktopBusyResult(result) || desktopBusyRetryDelaysMs.length === 0) {
|
|
15061
|
+
return result;
|
|
14354
15062
|
}
|
|
14355
|
-
|
|
14356
|
-
|
|
14357
|
-
|
|
14358
|
-
|
|
14359
|
-
|
|
14360
|
-
|
|
14361
|
-
|
|
14362
|
-
|
|
14363
|
-
onStatusNote
|
|
14364
|
-
|
|
15063
|
+
if (desktopBusyRetryCount >= desktopBusyRetryDelaysMs.length) {
|
|
15064
|
+
return {
|
|
15065
|
+
...result,
|
|
15066
|
+
errorMessage: buildDesktopBusyExhaustedMessage(result.errorMessage)
|
|
15067
|
+
};
|
|
15068
|
+
}
|
|
15069
|
+
const delayMs = desktopBusyRetryDelaysMs[desktopBusyRetryCount];
|
|
15070
|
+
desktopBusyRetryCount += 1;
|
|
15071
|
+
onStatusNote?.(formatDesktopBusyRetryNote(
|
|
15072
|
+
desktopBusyRetryCount,
|
|
15073
|
+
desktopBusyRetryDelaysMs.length,
|
|
15074
|
+
delayMs
|
|
15075
|
+
));
|
|
15076
|
+
const shouldRetry2 = await waitForRetryDelay(delayMs, abortController.signal);
|
|
15077
|
+
if (!shouldRetry2) {
|
|
15078
|
+
return {
|
|
15079
|
+
...result,
|
|
15080
|
+
errorMessage: "Task stopped by user",
|
|
15081
|
+
errorCode: void 0
|
|
15082
|
+
};
|
|
15083
|
+
}
|
|
15084
|
+
}
|
|
14365
15085
|
} finally {
|
|
14366
15086
|
clearInterval(renewalInterval);
|
|
14367
15087
|
store.releaseSessionLock(sessionId, lockId);
|
|
@@ -14376,10 +15096,36 @@ async function consumeStream(stream, sessionId, onPermissionRequest, onPartialTe
|
|
|
14376
15096
|
let tokenUsage = null;
|
|
14377
15097
|
let hasError = false;
|
|
14378
15098
|
let errorMessage = "";
|
|
15099
|
+
let errorCode;
|
|
14379
15100
|
const seenToolResultIds = /* @__PURE__ */ new Set();
|
|
14380
15101
|
const permissionRequests = [];
|
|
14381
15102
|
let capturedSdkSessionId = null;
|
|
14382
|
-
const
|
|
15103
|
+
const finalizeConsumedContent = () => {
|
|
15104
|
+
if (currentText.trim()) {
|
|
15105
|
+
contentBlocks.push({ type: "text", text: currentText });
|
|
15106
|
+
currentText = "";
|
|
15107
|
+
}
|
|
15108
|
+
const outboundAttachments = [];
|
|
15109
|
+
for (const block2 of contentBlocks) {
|
|
15110
|
+
if (block2.type !== "text") continue;
|
|
15111
|
+
const parsed = collectFinalResponseArtifacts(block2.text);
|
|
15112
|
+
block2.text = parsed.text;
|
|
15113
|
+
outboundAttachments.push(...parsed.attachments);
|
|
15114
|
+
}
|
|
15115
|
+
if (contentBlocks.length > 0) {
|
|
15116
|
+
const hasToolBlocks = contentBlocks.some(
|
|
15117
|
+
(block2) => block2.type === "tool_use" || block2.type === "tool_result"
|
|
15118
|
+
);
|
|
15119
|
+
const content = hasToolBlocks ? JSON.stringify(contentBlocks) : contentBlocks.filter((block2) => block2.type === "text").map((block2) => block2.text).join("\n\n").trim();
|
|
15120
|
+
if (content) {
|
|
15121
|
+
store.addMessage(sessionId, "assistant", content, tokenUsage ? JSON.stringify(tokenUsage) : null);
|
|
15122
|
+
}
|
|
15123
|
+
}
|
|
15124
|
+
return {
|
|
15125
|
+
responseText: contentBlocks.filter((block2) => block2.type === "text").map((block2) => block2.text).join("").trim(),
|
|
15126
|
+
outboundAttachments: dedupeOutboundAttachments(outboundAttachments)
|
|
15127
|
+
};
|
|
15128
|
+
};
|
|
14383
15129
|
try {
|
|
14384
15130
|
await consumeSseEvents(stream, async (event) => {
|
|
14385
15131
|
switch (event.type) {
|
|
@@ -14507,6 +15253,9 @@ async function consumeStream(stream, sessionId, onPermissionRequest, onPartialTe
|
|
|
14507
15253
|
case "error":
|
|
14508
15254
|
hasError = true;
|
|
14509
15255
|
errorMessage = event.data || "Unknown error";
|
|
15256
|
+
if (isSessionBusyErrorMessage(errorMessage)) {
|
|
15257
|
+
errorCode = "session_busy";
|
|
15258
|
+
}
|
|
14510
15259
|
break;
|
|
14511
15260
|
case "result": {
|
|
14512
15261
|
try {
|
|
@@ -14523,54 +15272,28 @@ async function consumeStream(stream, sessionId, onPermissionRequest, onPartialTe
|
|
|
14523
15272
|
}
|
|
14524
15273
|
}
|
|
14525
15274
|
});
|
|
14526
|
-
|
|
14527
|
-
contentBlocks.push({ type: "text", text: currentText });
|
|
14528
|
-
}
|
|
14529
|
-
if (contentBlocks.length > 0) {
|
|
14530
|
-
for (const block2 of contentBlocks) {
|
|
14531
|
-
if (block2.type !== "text") continue;
|
|
14532
|
-
const parsed = collectFinalResponseArtifacts(block2.text);
|
|
14533
|
-
block2.text = parsed.text;
|
|
14534
|
-
outboundAttachments.push(...parsed.attachments);
|
|
14535
|
-
}
|
|
14536
|
-
const hasToolBlocks = contentBlocks.some(
|
|
14537
|
-
(b) => b.type === "tool_use" || b.type === "tool_result"
|
|
14538
|
-
);
|
|
14539
|
-
const content = hasToolBlocks ? JSON.stringify(contentBlocks) : contentBlocks.filter((b) => b.type === "text").map((b) => b.text).join("\n\n").trim();
|
|
14540
|
-
if (content) {
|
|
14541
|
-
store.addMessage(sessionId, "assistant", content, tokenUsage ? JSON.stringify(tokenUsage) : null);
|
|
14542
|
-
}
|
|
14543
|
-
}
|
|
14544
|
-
const responseText = contentBlocks.filter((b) => b.type === "text").map((b) => b.text).join("").trim();
|
|
15275
|
+
const finalizedContent = finalizeConsumedContent();
|
|
14545
15276
|
return {
|
|
14546
|
-
responseText,
|
|
14547
|
-
outboundAttachments:
|
|
14548
|
-
tokenUsage,
|
|
14549
|
-
hasError,
|
|
14550
|
-
errorMessage,
|
|
14551
|
-
|
|
14552
|
-
|
|
14553
|
-
|
|
14554
|
-
|
|
14555
|
-
|
|
14556
|
-
|
|
14557
|
-
}
|
|
14558
|
-
if (contentBlocks.length > 0) {
|
|
14559
|
-
const hasToolBlocks = contentBlocks.some(
|
|
14560
|
-
(b) => b.type === "tool_use" || b.type === "tool_result"
|
|
14561
|
-
);
|
|
14562
|
-
const content = hasToolBlocks ? JSON.stringify(contentBlocks) : contentBlocks.filter((b) => b.type === "text").map((b) => b.text).join("\n\n").trim();
|
|
14563
|
-
if (content) {
|
|
14564
|
-
store.addMessage(sessionId, "assistant", content);
|
|
14565
|
-
}
|
|
14566
|
-
}
|
|
15277
|
+
responseText: finalizedContent.responseText,
|
|
15278
|
+
outboundAttachments: finalizedContent.outboundAttachments,
|
|
15279
|
+
tokenUsage,
|
|
15280
|
+
hasError,
|
|
15281
|
+
errorMessage,
|
|
15282
|
+
errorCode,
|
|
15283
|
+
permissionRequests,
|
|
15284
|
+
sdkSessionId: capturedSdkSessionId
|
|
15285
|
+
};
|
|
15286
|
+
} catch (e) {
|
|
15287
|
+
const finalizedContent = finalizeConsumedContent();
|
|
14567
15288
|
const isAbort = e instanceof DOMException && e.name === "AbortError" || e instanceof Error && e.name === "AbortError";
|
|
15289
|
+
const fallbackErrorMessage = isAbort ? "Task stopped by user" : e instanceof Error ? e.message : "Stream consumption error";
|
|
14568
15290
|
return {
|
|
14569
|
-
responseText:
|
|
14570
|
-
outboundAttachments:
|
|
15291
|
+
responseText: finalizedContent.responseText,
|
|
15292
|
+
outboundAttachments: finalizedContent.outboundAttachments,
|
|
14571
15293
|
tokenUsage,
|
|
14572
15294
|
hasError: true,
|
|
14573
|
-
errorMessage:
|
|
15295
|
+
errorMessage: fallbackErrorMessage,
|
|
15296
|
+
errorCode: isSessionBusyErrorMessage(fallbackErrorMessage) ? "session_busy" : void 0,
|
|
14574
15297
|
permissionRequests,
|
|
14575
15298
|
sdkSessionId: capturedSdkSessionId
|
|
14576
15299
|
};
|
|
@@ -15206,8 +15929,28 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
|
|
|
15206
15929
|
}
|
|
15207
15930
|
let finalOutcome = "failed";
|
|
15208
15931
|
let finalOutcomeDetail;
|
|
15932
|
+
let finalDeliveryError;
|
|
15209
15933
|
let shouldRecordHealthEnd = true;
|
|
15210
15934
|
let forceStopStarted = false;
|
|
15935
|
+
const deliverFinalPayload = async (response, options) => {
|
|
15936
|
+
try {
|
|
15937
|
+
const result = await deliverFinalResponse({
|
|
15938
|
+
adapter,
|
|
15939
|
+
address: msg.address,
|
|
15940
|
+
sessionId: binding.codepilotSessionId,
|
|
15941
|
+
replyToMessageId: msg.messageId,
|
|
15942
|
+
deliverResponse: deps.deliverResponse
|
|
15943
|
+
}, response, options);
|
|
15944
|
+
if (result.ok) return true;
|
|
15945
|
+
finalDeliveryError = `\u6700\u7EC8\u56DE\u590D\u6295\u9012\u5931\u8D25\uFF1A${result.error || "\u672A\u77E5\u9519\u8BEF"}`;
|
|
15946
|
+
} catch (error) {
|
|
15947
|
+
finalDeliveryError = `\u6700\u7EC8\u56DE\u590D\u6295\u9012\u5F02\u5E38\uFF1A${error instanceof Error ? error.message : String(error)}`;
|
|
15948
|
+
}
|
|
15949
|
+
console.error(`[interactive-message-runner] ${finalDeliveryError}`);
|
|
15950
|
+
finalOutcome = "failed";
|
|
15951
|
+
finalOutcomeDetail = finalDeliveryError;
|
|
15952
|
+
return false;
|
|
15953
|
+
};
|
|
15211
15954
|
taskState.forceStop = async (detail = "\u4EFB\u52A1\u5DF2\u6536\u5230\u505C\u6B62\u8BF7\u6C42\u3002") => {
|
|
15212
15955
|
if (forceStopStarted) return true;
|
|
15213
15956
|
forceStopStarted = true;
|
|
@@ -15295,13 +16038,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
|
|
|
15295
16038
|
});
|
|
15296
16039
|
const cardFinalized2 = await finalizeStreamUiOnce(streamEndStatus, terminalResponse2.text);
|
|
15297
16040
|
if (hasFinalResponsePayload(terminalResponse2)) {
|
|
15298
|
-
await
|
|
15299
|
-
adapter,
|
|
15300
|
-
address: msg.address,
|
|
15301
|
-
sessionId: binding.codepilotSessionId,
|
|
15302
|
-
replyToMessageId: msg.messageId,
|
|
15303
|
-
deliverResponse: deps.deliverResponse
|
|
15304
|
-
}, terminalResponse2, { skipText: cardFinalized2 });
|
|
16041
|
+
await deliverFinalPayload(terminalResponse2, { skipText: cardFinalized2 });
|
|
15305
16042
|
}
|
|
15306
16043
|
return;
|
|
15307
16044
|
}
|
|
@@ -15334,30 +16071,11 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
|
|
|
15334
16071
|
);
|
|
15335
16072
|
}
|
|
15336
16073
|
if (staleResponse) {
|
|
15337
|
-
await
|
|
15338
|
-
adapter,
|
|
15339
|
-
address: msg.address,
|
|
15340
|
-
sessionId: binding.codepilotSessionId,
|
|
15341
|
-
replyToMessageId: msg.messageId,
|
|
15342
|
-
deliverResponse: deps.deliverResponse
|
|
15343
|
-
}, staleResponse, { skipText: cardFinalized });
|
|
16074
|
+
await deliverFinalPayload(staleResponse, { skipText: cardFinalized });
|
|
15344
16075
|
} else if (hasFinalResponsePayload(effectiveResponse)) {
|
|
15345
|
-
await
|
|
15346
|
-
adapter,
|
|
15347
|
-
address: msg.address,
|
|
15348
|
-
sessionId: binding.codepilotSessionId,
|
|
15349
|
-
replyToMessageId: msg.messageId,
|
|
15350
|
-
deliverResponse: deps.deliverResponse
|
|
15351
|
-
}, effectiveResponse, { skipText: cardFinalized });
|
|
16076
|
+
await deliverFinalPayload(effectiveResponse, { skipText: cardFinalized });
|
|
15352
16077
|
} else if (result.hasError && !taskAbort.signal.aborted) {
|
|
15353
|
-
await
|
|
15354
|
-
{
|
|
15355
|
-
adapter,
|
|
15356
|
-
address: msg.address,
|
|
15357
|
-
sessionId: binding.codepilotSessionId,
|
|
15358
|
-
replyToMessageId: msg.messageId,
|
|
15359
|
-
deliverResponse: deps.deliverResponse
|
|
15360
|
-
},
|
|
16078
|
+
await deliverFinalPayload(
|
|
15361
16079
|
assembleSdkFinalResponse({
|
|
15362
16080
|
text: `**Error:** ${result.errorMessage}`,
|
|
15363
16081
|
hasError: true,
|
|
@@ -15369,8 +16087,10 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
|
|
|
15369
16087
|
deps.persistSdkSessionUpdate(binding.codepilotSessionId, result.sdkSessionId, result.hasError);
|
|
15370
16088
|
} catch {
|
|
15371
16089
|
}
|
|
15372
|
-
|
|
15373
|
-
|
|
16090
|
+
if (!finalDeliveryError) {
|
|
16091
|
+
finalOutcome = terminalAfterProcess?.outcome || (result.hasError ? "failed" : "completed");
|
|
16092
|
+
finalOutcomeDetail = terminalAfterProcess?.detail || (result.hasError ? result.errorMessage?.trim() || void 0 : void 0);
|
|
16093
|
+
}
|
|
15374
16094
|
} finally {
|
|
15375
16095
|
await finalizeStreamUiOnce(
|
|
15376
16096
|
taskAbort.signal.aborted ? "interrupted" : finalOutcome === "completed" ? "completed" : "error",
|
|
@@ -15567,7 +16287,7 @@ function createInteractiveRuntime(getState2, deps) {
|
|
|
15567
16287
|
}
|
|
15568
16288
|
|
|
15569
16289
|
// src/lib/bridge/mirror-runtime.ts
|
|
15570
|
-
import
|
|
16290
|
+
import fs11 from "node:fs";
|
|
15571
16291
|
|
|
15572
16292
|
// src/lib/bridge/mirror-subscription-state.ts
|
|
15573
16293
|
function resetMirrorReadState(subscription) {
|
|
@@ -15588,7 +16308,7 @@ function createMirrorSubscription(input) {
|
|
|
15588
16308
|
chatId: input.chatId,
|
|
15589
16309
|
threadId: input.threadId,
|
|
15590
16310
|
filePath: input.filePath,
|
|
15591
|
-
cursor: { initialized: false, lastEventCount: 0 },
|
|
16311
|
+
cursor: input.lastDeliveredAt ? { initialized: true, lastEventTimestamp: input.lastDeliveredAt, lastEventCount: 0 } : { initialized: false, lastEventCount: 0 },
|
|
15592
16312
|
dirty: true,
|
|
15593
16313
|
status: input.filePath ? "watching" : "stale",
|
|
15594
16314
|
watcher: null,
|
|
@@ -15612,7 +16332,7 @@ function createMirrorSubscription(input) {
|
|
|
15612
16332
|
};
|
|
15613
16333
|
}
|
|
15614
16334
|
function resetMirrorSubscriptionForThreadChange(subscription, lastDeliveredAt) {
|
|
15615
|
-
subscription.cursor = { initialized: false, lastEventCount: 0 };
|
|
16335
|
+
subscription.cursor = lastDeliveredAt ? { initialized: true, lastEventTimestamp: lastDeliveredAt, lastEventCount: 0 } : { initialized: false, lastEventCount: 0 };
|
|
15616
16336
|
subscription.lastDeliveredAt = lastDeliveredAt;
|
|
15617
16337
|
subscription.dirty = true;
|
|
15618
16338
|
subscription.pendingTurn = null;
|
|
@@ -15669,7 +16389,7 @@ function recordMirrorSubscriptionFailure(subscription, suspendThreshold, suspend
|
|
|
15669
16389
|
}
|
|
15670
16390
|
|
|
15671
16391
|
// src/lib/bridge/mirror-reconcile-core.ts
|
|
15672
|
-
import
|
|
16392
|
+
import fs10 from "node:fs";
|
|
15673
16393
|
|
|
15674
16394
|
// src/desktop-session-mirror.ts
|
|
15675
16395
|
function makeCursor(records) {
|
|
@@ -15760,7 +16480,7 @@ function reconcileDesktopMirrorCursor(cursor, records) {
|
|
|
15760
16480
|
if (cursor.lastEventCount === 0) {
|
|
15761
16481
|
return {
|
|
15762
16482
|
nextCursor,
|
|
15763
|
-
deliverableRecords: records,
|
|
16483
|
+
deliverableRecords: cursor.lastEventTimestamp ? collectEventsAfterTimestamp(records, cursor.lastEventTimestamp) : records,
|
|
15764
16484
|
reset: false
|
|
15765
16485
|
};
|
|
15766
16486
|
}
|
|
@@ -15782,7 +16502,7 @@ function reconcileDesktopMirrorCursor(cursor, records) {
|
|
|
15782
16502
|
// src/lib/bridge/mirror-reconcile-core.ts
|
|
15783
16503
|
function statMirrorFile(filePath) {
|
|
15784
16504
|
try {
|
|
15785
|
-
const stat =
|
|
16505
|
+
const stat = fs10.statSync(filePath);
|
|
15786
16506
|
if (!stat.isFile()) return null;
|
|
15787
16507
|
return {
|
|
15788
16508
|
size: stat.size,
|
|
@@ -15810,12 +16530,43 @@ function markMirrorSnapshotMissing(subscription) {
|
|
|
15810
16530
|
resetMirrorReadState(subscription);
|
|
15811
16531
|
}
|
|
15812
16532
|
function isMirrorSnapshotUnchanged(subscription, snapshot) {
|
|
15813
|
-
return !subscription.dirty && subscription.fileIdentity === snapshot.identity && subscription.fileSize === snapshot.size && subscription.fileMtimeMs === snapshot.mtimeMs;
|
|
16533
|
+
return !subscription.dirty && subscription.fileIdentity === snapshot.identity && subscription.fileOffset === snapshot.size && !subscription.trailingText && subscription.fileSize === snapshot.size && subscription.fileMtimeMs === snapshot.mtimeMs;
|
|
16534
|
+
}
|
|
16535
|
+
function findLastCompleteLineOffset(filePath, fileSize) {
|
|
16536
|
+
if (fileSize <= 0) return 0;
|
|
16537
|
+
const fd = fs10.openSync(filePath, "r");
|
|
16538
|
+
try {
|
|
16539
|
+
const chunkSize = 64 * 1024;
|
|
16540
|
+
let end = fileSize;
|
|
16541
|
+
while (end > 0) {
|
|
16542
|
+
const start2 = Math.max(0, end - chunkSize);
|
|
16543
|
+
const buffer = Buffer.alloc(end - start2);
|
|
16544
|
+
const bytesRead = fs10.readSync(fd, buffer, 0, buffer.length, start2);
|
|
16545
|
+
const newlineIndex = buffer.subarray(0, bytesRead).lastIndexOf(10);
|
|
16546
|
+
if (newlineIndex >= 0) return start2 + newlineIndex + 1;
|
|
16547
|
+
end = start2;
|
|
16548
|
+
}
|
|
16549
|
+
return 0;
|
|
16550
|
+
} finally {
|
|
16551
|
+
fs10.closeSync(fd);
|
|
16552
|
+
}
|
|
15814
16553
|
}
|
|
15815
16554
|
function readMirrorDeliverableRecords(subscription, snapshot) {
|
|
15816
16555
|
let deliverableRecords = [];
|
|
15817
16556
|
let unknownKinds = [];
|
|
15818
16557
|
const requiresFullRecover = !subscription.cursor.initialized || subscription.fileOffset === 0 || subscription.fileIdentity !== null && subscription.fileIdentity !== snapshot.identity || subscription.fileSize !== null && snapshot.size < subscription.fileOffset || subscription.fileSize !== null && snapshot.size === subscription.fileOffset && subscription.fileMtimeMs !== null && snapshot.mtimeMs !== subscription.fileMtimeMs;
|
|
16558
|
+
if (requiresFullRecover && !subscription.cursor.initialized && !subscription.lastDeliveredAt) {
|
|
16559
|
+
subscription.cursor = { initialized: true, lastEventCount: 0 };
|
|
16560
|
+
subscription.trailingText = "";
|
|
16561
|
+
subscription.fileOffset = findLastCompleteLineOffset(subscription.filePath, snapshot.size);
|
|
16562
|
+
subscription.activeMirrorTurnId = null;
|
|
16563
|
+
subscription.activeSpecialCallIds.clear();
|
|
16564
|
+
subscription.fileSize = snapshot.size;
|
|
16565
|
+
subscription.fileMtimeMs = snapshot.mtimeMs;
|
|
16566
|
+
subscription.fileIdentity = snapshot.identity;
|
|
16567
|
+
subscription.dirty = false;
|
|
16568
|
+
return { records: [], unknownKinds: [] };
|
|
16569
|
+
}
|
|
15819
16570
|
if (requiresFullRecover) {
|
|
15820
16571
|
const previousCursor = subscription.cursor;
|
|
15821
16572
|
const fullDelta = readDesktopSessionMirrorRecordDeltaByFilePath(
|
|
@@ -15829,8 +16580,8 @@ function readMirrorDeliverableRecords(subscription, snapshot) {
|
|
|
15829
16580
|
const delta = reconcileDesktopMirrorCursor(subscription.cursor, fullDelta.records);
|
|
15830
16581
|
subscription.cursor = delta.nextCursor;
|
|
15831
16582
|
deliverableRecords = filterDuplicateAssistantEvents(previousCursor, delta.deliverableRecords);
|
|
15832
|
-
subscription.trailingText =
|
|
15833
|
-
subscription.fileOffset =
|
|
16583
|
+
subscription.trailingText = fullDelta.trailingText;
|
|
16584
|
+
subscription.fileOffset = fullDelta.nextOffset;
|
|
15834
16585
|
subscription.activeMirrorTurnId = fullDelta.nextTurnId;
|
|
15835
16586
|
subscription.activeSpecialCallIds = new Set(fullDelta.nextSpecialCallIds);
|
|
15836
16587
|
unknownKinds = fullDelta.unknownKinds;
|
|
@@ -15939,6 +16690,44 @@ async function runMirrorReconcileBatch(deps) {
|
|
|
15939
16690
|
|
|
15940
16691
|
// src/lib/bridge/mirror-runtime.ts
|
|
15941
16692
|
function createMirrorRuntime(getState2, options, deps) {
|
|
16693
|
+
function isAtOrBefore(timestamp, boundary) {
|
|
16694
|
+
if (!timestamp) return true;
|
|
16695
|
+
const timestampMs = Date.parse(timestamp);
|
|
16696
|
+
const boundaryMs = Date.parse(boundary);
|
|
16697
|
+
if (Number.isFinite(timestampMs) && Number.isFinite(boundaryMs)) {
|
|
16698
|
+
return timestampMs <= boundaryMs;
|
|
16699
|
+
}
|
|
16700
|
+
return timestamp <= boundary;
|
|
16701
|
+
}
|
|
16702
|
+
function discardClaimedMirrorTurn(subscription, routeResult) {
|
|
16703
|
+
if (!routeResult.terminalClaimed) return;
|
|
16704
|
+
const claimedTurnId = routeResult.claimedTurnId;
|
|
16705
|
+
if (claimedTurnId) {
|
|
16706
|
+
const claimedStreamKey = buildMirrorStreamKey(subscription.sessionId, claimedTurnId, "");
|
|
16707
|
+
subscription.bufferedRecords = subscription.bufferedRecords.filter(
|
|
16708
|
+
(record) => record.turnId !== claimedTurnId
|
|
16709
|
+
);
|
|
16710
|
+
subscription.pendingDeliveries = subscription.pendingDeliveries.filter(
|
|
16711
|
+
(turn) => turn.streamKey !== claimedStreamKey
|
|
16712
|
+
);
|
|
16713
|
+
if (subscription.pendingTurn?.turnId === claimedTurnId) {
|
|
16714
|
+
deps.stopMirrorStreaming(subscription, "interrupted");
|
|
16715
|
+
subscription.pendingTurn = null;
|
|
16716
|
+
}
|
|
16717
|
+
}
|
|
16718
|
+
const claimedAt = routeResult.claimedAt;
|
|
16719
|
+
if (!claimedAt) return;
|
|
16720
|
+
const retainedAtOrBeforeClaim = [
|
|
16721
|
+
...subscription.bufferedRecords.map((record) => record.timestamp),
|
|
16722
|
+
...subscription.pendingDeliveries.map((turn) => turn.timestamp),
|
|
16723
|
+
...routeResult.unclaimed.map((record) => record.timestamp),
|
|
16724
|
+
...subscription.pendingTurn ? [subscription.pendingTurn.lastActivityAt] : []
|
|
16725
|
+
].some((timestamp) => isAtOrBefore(timestamp, claimedAt));
|
|
16726
|
+
if (retainedAtOrBeforeClaim) return;
|
|
16727
|
+
if (!subscription.lastDeliveredAt || !isAtOrBefore(claimedAt, subscription.lastDeliveredAt)) {
|
|
16728
|
+
subscription.lastDeliveredAt = claimedAt;
|
|
16729
|
+
}
|
|
16730
|
+
}
|
|
15942
16731
|
function closeMirrorWatcher(subscription) {
|
|
15943
16732
|
if (subscription.watcher) {
|
|
15944
16733
|
try {
|
|
@@ -15970,7 +16759,7 @@ function createMirrorRuntime(getState2, options, deps) {
|
|
|
15970
16759
|
}
|
|
15971
16760
|
closeMirrorWatcher(subscription);
|
|
15972
16761
|
try {
|
|
15973
|
-
subscription.watcher =
|
|
16762
|
+
subscription.watcher = fs11.watch(filePath, () => {
|
|
15974
16763
|
subscription.dirty = true;
|
|
15975
16764
|
scheduleMirrorWake();
|
|
15976
16765
|
});
|
|
@@ -16126,7 +16915,14 @@ function createMirrorRuntime(getState2, options, deps) {
|
|
|
16126
16915
|
`[bridge-manager] Unhandled desktop mirror event for thread ${subscription.threadId}: ${kind}`
|
|
16127
16916
|
);
|
|
16128
16917
|
}
|
|
16129
|
-
const routeResult = deliverableRecords.length > 0 && deps.routeDesktopRecords ? await deps.routeDesktopRecords(subscription.sessionId, subscription.threadId, deliverableRecords) : {
|
|
16918
|
+
const routeResult = deliverableRecords.length > 0 && deps.routeDesktopRecords ? await deps.routeDesktopRecords(subscription.sessionId, subscription.threadId, deliverableRecords) : {
|
|
16919
|
+
claimed: [],
|
|
16920
|
+
unclaimed: deliverableRecords,
|
|
16921
|
+
terminalClaimed: false,
|
|
16922
|
+
claimedTurnId: null,
|
|
16923
|
+
claimedAt: null
|
|
16924
|
+
};
|
|
16925
|
+
discardClaimedMirrorTurn(subscription, routeResult);
|
|
16130
16926
|
const mirrorRecords = routeResult.unclaimed;
|
|
16131
16927
|
if (mirrorRecords.length > 0) {
|
|
16132
16928
|
deps.observeSessionHealthRecords(subscription.sessionId, subscription.threadId, mirrorRecords);
|
|
@@ -16371,7 +17167,9 @@ function createMirrorFeedbackController(deps) {
|
|
|
16371
17167
|
}
|
|
16372
17168
|
async function deliverMirrorTurn(subscription, turn) {
|
|
16373
17169
|
const adapter = deps.getAdapter(subscription.channelType);
|
|
16374
|
-
if (!adapter || !adapter.isRunning())
|
|
17170
|
+
if (!adapter || !adapter.isRunning()) {
|
|
17171
|
+
throw new Error(`mirror adapter unavailable: ${subscription.channelType}`);
|
|
17172
|
+
}
|
|
16375
17173
|
const title = deps.getThreadTitle(subscription.threadId)?.trim() || "\u684C\u9762\u7EBF\u7A0B";
|
|
16376
17174
|
const responseParseMode = getFeedbackParseMode(subscription.channelType);
|
|
16377
17175
|
const markdown = responseParseMode === "Markdown";
|
|
@@ -17190,7 +17988,9 @@ async function routeDesktopRecords(sessionId, desktopThreadId, records, coordina
|
|
|
17190
17988
|
return {
|
|
17191
17989
|
claimed: [],
|
|
17192
17990
|
unclaimed: records,
|
|
17193
|
-
terminalClaimed: false
|
|
17991
|
+
terminalClaimed: false,
|
|
17992
|
+
claimedTurnId: null,
|
|
17993
|
+
claimedAt: null
|
|
17194
17994
|
};
|
|
17195
17995
|
}
|
|
17196
17996
|
const claim = await coordinator.claimDesktopTerminal(
|
|
@@ -17200,7 +18000,9 @@ async function routeDesktopRecords(sessionId, desktopThreadId, records, coordina
|
|
|
17200
18000
|
return {
|
|
17201
18001
|
claimed: [],
|
|
17202
18002
|
unclaimed: records,
|
|
17203
|
-
terminalClaimed: false
|
|
18003
|
+
terminalClaimed: false,
|
|
18004
|
+
claimedTurnId: null,
|
|
18005
|
+
claimedAt: null
|
|
17204
18006
|
};
|
|
17205
18007
|
}
|
|
17206
18008
|
const claimedTurnId = terminalRecord.turnId;
|
|
@@ -17209,7 +18011,9 @@ async function routeDesktopRecords(sessionId, desktopThreadId, records, coordina
|
|
|
17209
18011
|
return {
|
|
17210
18012
|
claimed,
|
|
17211
18013
|
unclaimed: records.filter((record) => !claimedSet.has(record.signature)),
|
|
17212
|
-
terminalClaimed: true
|
|
18014
|
+
terminalClaimed: true,
|
|
18015
|
+
claimedTurnId: claimedTurnId || null,
|
|
18016
|
+
claimedAt: terminalRecord.timestamp || null
|
|
17213
18017
|
};
|
|
17214
18018
|
}
|
|
17215
18019
|
|
|
@@ -17660,10 +18464,26 @@ async function handleMessage(adapter, msg) {
|
|
|
17660
18464
|
const meta = adapterState.adapterMeta.get(adapter.channelType) || { lastMessageAt: null, lastError: null, configFingerprint: "" };
|
|
17661
18465
|
meta.lastMessageAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
17662
18466
|
adapterState.adapterMeta.set(adapter.channelType, meta);
|
|
18467
|
+
let updateSettled = false;
|
|
18468
|
+
const reject = () => {
|
|
18469
|
+
if (updateSettled) return;
|
|
18470
|
+
updateSettled = true;
|
|
18471
|
+
if (msg.updateId != null) {
|
|
18472
|
+
adapter.rejectUpdate?.(msg.updateId, msg.messageId);
|
|
18473
|
+
}
|
|
18474
|
+
};
|
|
17663
18475
|
const ack = () => {
|
|
17664
|
-
if (
|
|
17665
|
-
|
|
18476
|
+
if (updateSettled) return;
|
|
18477
|
+
if (msg.updateId != null) {
|
|
18478
|
+
try {
|
|
18479
|
+
store.insertDedup(buildInboundDedupKey(adapter.channelType, msg.messageId));
|
|
18480
|
+
} catch (error) {
|
|
18481
|
+
reject();
|
|
18482
|
+
throw error;
|
|
18483
|
+
}
|
|
18484
|
+
adapter.acknowledgeUpdate?.(msg.updateId, msg.messageId);
|
|
17666
18485
|
}
|
|
18486
|
+
updateSettled = true;
|
|
17667
18487
|
};
|
|
17668
18488
|
if (msg.callbackData) {
|
|
17669
18489
|
const handled = handlePermissionCallback(msg.callbackData, msg.address.chatId, msg.callbackMessageId);
|
|
@@ -17761,34 +18581,31 @@ async function handleMessage(adapter, msg) {
|
|
|
17761
18581
|
ack();
|
|
17762
18582
|
return;
|
|
17763
18583
|
}
|
|
17764
|
-
|
|
17765
|
-
|
|
17766
|
-
|
|
17767
|
-
|
|
17768
|
-
|
|
17769
|
-
|
|
17770
|
-
|
|
17771
|
-
|
|
17772
|
-
|
|
17773
|
-
|
|
17774
|
-
|
|
17775
|
-
|
|
17776
|
-
|
|
17777
|
-
|
|
17778
|
-
|
|
17779
|
-
|
|
17780
|
-
|
|
17781
|
-
|
|
17782
|
-
|
|
17783
|
-
|
|
17784
|
-
|
|
17785
|
-
|
|
17786
|
-
|
|
17787
|
-
|
|
17788
|
-
|
|
17789
|
-
} finally {
|
|
17790
|
-
ack();
|
|
17791
|
-
}
|
|
18584
|
+
await runInteractiveMessage(adapter, msg, text2, hasAttachments ? msg.attachments : void 0, {
|
|
18585
|
+
registerInteractiveTask: (task) => INTERACTIVE_RUNTIME.registerInteractiveTask(task),
|
|
18586
|
+
registerBridgeTurn: (turn) => TURN_COORDINATOR.registerInteractiveTurn(turn),
|
|
18587
|
+
resetMirrorSessionForInteractiveRun,
|
|
18588
|
+
isCurrentInteractiveTask: (sessionId, taskId) => INTERACTIVE_RUNTIME.isCurrentInteractiveTask(sessionId, taskId),
|
|
18589
|
+
touchInteractiveTask: (sessionId, taskId) => INTERACTIVE_RUNTIME.touchInteractiveTask(sessionId, taskId),
|
|
18590
|
+
recordInteractiveHealthStart: (sessionId, detail) => SESSION_HEALTH_RUNTIME.recordInteractiveStart(sessionId, detail),
|
|
18591
|
+
recordInteractiveHealthProgress: (sessionId, type, detail) => SESSION_HEALTH_RUNTIME.recordInteractiveProgress(sessionId, type, detail),
|
|
18592
|
+
recordInteractiveHealthTool: (sessionId, toolId, toolName, status) => {
|
|
18593
|
+
SESSION_HEALTH_RUNTIME.recordToolState(sessionId, toolId, toolName, status);
|
|
18594
|
+
},
|
|
18595
|
+
recordInteractiveStreamUiSnapshot: (sessionId, snapshot) => {
|
|
18596
|
+
SESSION_HEALTH_RUNTIME.recordStructuredStreamUi(sessionId, snapshot);
|
|
18597
|
+
},
|
|
18598
|
+
recordInteractiveHealthEnd: (sessionId, outcome, detail) => SESSION_HEALTH_RUNTIME.recordInteractiveEnd(sessionId, outcome, detail),
|
|
18599
|
+
beginMirrorSuppression: beginMirrorSuppression2,
|
|
18600
|
+
abortMirrorSuppression: abortMirrorSuppression2,
|
|
18601
|
+
settleMirrorSuppression: settleMirrorSuppression2,
|
|
18602
|
+
releaseInteractiveTask: (sessionId, taskId) => INTERACTIVE_RUNTIME.releaseInteractiveTask(sessionId, taskId),
|
|
18603
|
+
releaseBridgeTurn: (sessionId, taskId) => TURN_COORDINATOR.releaseSessionTurn(sessionId, taskId),
|
|
18604
|
+
deliverResponse,
|
|
18605
|
+
persistSdkSessionUpdate,
|
|
18606
|
+
desktopTerminalFinalizationTimeoutMs: DESKTOP_TERMINAL_FINALIZATION_TIMEOUT_MS
|
|
18607
|
+
});
|
|
18608
|
+
ack();
|
|
17792
18609
|
}
|
|
17793
18610
|
async function handleCommand(adapter, msg, text2) {
|
|
17794
18611
|
await handleBridgeCommand(adapter, msg, text2, {
|
|
@@ -17826,32 +18643,87 @@ function persistSdkSessionUpdate(sessionId, sdkSessionId, hasError) {
|
|
|
17826
18643
|
}
|
|
17827
18644
|
|
|
17828
18645
|
// src/store.ts
|
|
17829
|
-
import
|
|
18646
|
+
import fs13 from "node:fs";
|
|
18647
|
+
import path15 from "node:path";
|
|
18648
|
+
import crypto10 from "node:crypto";
|
|
18649
|
+
|
|
18650
|
+
// src/atomic-file.ts
|
|
18651
|
+
import crypto9 from "node:crypto";
|
|
18652
|
+
import fs12 from "node:fs";
|
|
17830
18653
|
import path14 from "node:path";
|
|
17831
|
-
|
|
17832
|
-
var
|
|
17833
|
-
|
|
18654
|
+
var RETRYABLE_RENAME_CODES = /* @__PURE__ */ new Set(["EACCES", "EBUSY", "EPERM"]);
|
|
18655
|
+
var WAIT_ARRAY2 = new Int32Array(new SharedArrayBuffer(4));
|
|
18656
|
+
function defaultSleepSync(delayMs) {
|
|
18657
|
+
Atomics.wait(WAIT_ARRAY2, 0, 0, Math.max(1, delayMs));
|
|
18658
|
+
}
|
|
18659
|
+
function isRetryableRenameError(error) {
|
|
18660
|
+
return RETRYABLE_RENAME_CODES.has(error?.code || "");
|
|
18661
|
+
}
|
|
18662
|
+
function renameFileWithRetrySync(source, destination, options = {}) {
|
|
18663
|
+
const maxAttempts = Math.max(1, options.maxAttempts ?? 8);
|
|
18664
|
+
const baseDelayMs = Math.max(1, options.baseDelayMs ?? 15);
|
|
18665
|
+
const renameSync = options.renameSync ?? fs12.renameSync;
|
|
18666
|
+
const sleepSync2 = options.sleepSync ?? defaultSleepSync;
|
|
18667
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
18668
|
+
try {
|
|
18669
|
+
renameSync(source, destination);
|
|
18670
|
+
return;
|
|
18671
|
+
} catch (error) {
|
|
18672
|
+
if (!isRetryableRenameError(error) || attempt === maxAttempts - 1) {
|
|
18673
|
+
throw error;
|
|
18674
|
+
}
|
|
18675
|
+
sleepSync2(Math.min(250, baseDelayMs * 2 ** attempt));
|
|
18676
|
+
}
|
|
18677
|
+
}
|
|
18678
|
+
}
|
|
18679
|
+
function atomicWriteFileSync(filePath, data) {
|
|
18680
|
+
fs12.mkdirSync(path14.dirname(filePath), { recursive: true });
|
|
18681
|
+
const tmpPath = `${filePath}.${process.pid}.${crypto9.randomUUID()}.tmp`;
|
|
18682
|
+
try {
|
|
18683
|
+
fs12.writeFileSync(tmpPath, data, "utf-8");
|
|
18684
|
+
renameFileWithRetrySync(tmpPath, filePath);
|
|
18685
|
+
} finally {
|
|
18686
|
+
try {
|
|
18687
|
+
fs12.rmSync(tmpPath, { force: true });
|
|
18688
|
+
} catch {
|
|
18689
|
+
}
|
|
18690
|
+
}
|
|
18691
|
+
}
|
|
18692
|
+
|
|
18693
|
+
// src/store.ts
|
|
18694
|
+
var DATA_DIR2 = path15.join(CTI_HOME, "data");
|
|
18695
|
+
var MESSAGES_DIR = path15.join(DATA_DIR2, "messages");
|
|
18696
|
+
var SESSIONS_PATH = path15.join(DATA_DIR2, "sessions.json");
|
|
18697
|
+
var BINDINGS_PATH = path15.join(DATA_DIR2, "bindings.json");
|
|
18698
|
+
var PERMISSIONS_PATH = path15.join(DATA_DIR2, "permissions.json");
|
|
18699
|
+
var OFFSETS_PATH = path15.join(DATA_DIR2, "offsets.json");
|
|
18700
|
+
var DEDUP_PATH = path15.join(DATA_DIR2, "dedup.json");
|
|
18701
|
+
var AUDIT_PATH = path15.join(DATA_DIR2, "audit.json");
|
|
18702
|
+
var DEFAULT_DEDUP_TTL_MS = 5 * 60 * 1e3;
|
|
18703
|
+
var INBOUND_DEDUP_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
17834
18704
|
function ensureDir2(dir) {
|
|
17835
|
-
|
|
18705
|
+
fs13.mkdirSync(dir, { recursive: true });
|
|
17836
18706
|
}
|
|
17837
18707
|
function atomicWrite2(filePath, data) {
|
|
17838
|
-
|
|
17839
|
-
|
|
17840
|
-
|
|
18708
|
+
atomicWriteFileSync(filePath, data);
|
|
18709
|
+
}
|
|
18710
|
+
function dedupTtlMs(key) {
|
|
18711
|
+
return key.startsWith("inbound:") ? INBOUND_DEDUP_TTL_MS : DEFAULT_DEDUP_TTL_MS;
|
|
17841
18712
|
}
|
|
17842
18713
|
function readJson2(filePath, fallback) {
|
|
17843
18714
|
try {
|
|
17844
|
-
const raw =
|
|
18715
|
+
const raw = fs13.readFileSync(filePath, "utf-8");
|
|
17845
18716
|
return JSON.parse(raw);
|
|
17846
|
-
} catch {
|
|
17847
|
-
return fallback;
|
|
18717
|
+
} catch (error) {
|
|
18718
|
+
if (error.code === "ENOENT") return fallback;
|
|
18719
|
+
throw new Error(`\u65E0\u6CD5\u8BFB\u53D6 JSON \u6570\u636E\u6587\u4EF6 ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
17848
18720
|
}
|
|
17849
18721
|
}
|
|
17850
18722
|
function writeJson(filePath, data) {
|
|
17851
18723
|
atomicWrite2(filePath, JSON.stringify(data, null, 2));
|
|
17852
18724
|
}
|
|
17853
18725
|
function uuid() {
|
|
17854
|
-
return
|
|
18726
|
+
return crypto10.randomUUID();
|
|
17855
18727
|
}
|
|
17856
18728
|
function now() {
|
|
17857
18729
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -17910,38 +18782,38 @@ var JsonFileStore = class {
|
|
|
17910
18782
|
this.reloadSessions();
|
|
17911
18783
|
this.reloadBindings();
|
|
17912
18784
|
const perms = readJson2(
|
|
17913
|
-
|
|
18785
|
+
PERMISSIONS_PATH,
|
|
17914
18786
|
{}
|
|
17915
18787
|
);
|
|
17916
18788
|
for (const [id, p] of Object.entries(perms)) {
|
|
17917
18789
|
this.permissionLinks.set(id, p);
|
|
17918
18790
|
}
|
|
17919
18791
|
const offsets = readJson2(
|
|
17920
|
-
|
|
18792
|
+
OFFSETS_PATH,
|
|
17921
18793
|
{}
|
|
17922
18794
|
);
|
|
17923
18795
|
for (const [k, v] of Object.entries(offsets)) {
|
|
17924
18796
|
this.offsets.set(k, v);
|
|
17925
18797
|
}
|
|
17926
18798
|
const dedup = readJson2(
|
|
17927
|
-
|
|
18799
|
+
DEDUP_PATH,
|
|
17928
18800
|
{}
|
|
17929
18801
|
);
|
|
17930
18802
|
for (const [k, v] of Object.entries(dedup)) {
|
|
17931
18803
|
this.dedupKeys.set(k, v);
|
|
17932
18804
|
}
|
|
17933
|
-
this.auditLog = readJson2(
|
|
18805
|
+
this.auditLog = readJson2(AUDIT_PATH, []);
|
|
17934
18806
|
}
|
|
17935
18807
|
reloadSessions() {
|
|
17936
18808
|
const sessions = readJson2(
|
|
17937
|
-
|
|
18809
|
+
SESSIONS_PATH,
|
|
17938
18810
|
{}
|
|
17939
18811
|
);
|
|
17940
18812
|
this.sessions = new Map(Object.entries(sessions));
|
|
17941
18813
|
}
|
|
17942
18814
|
reloadBindings() {
|
|
17943
18815
|
const bindings = readJson2(
|
|
17944
|
-
|
|
18816
|
+
BINDINGS_PATH,
|
|
17945
18817
|
{}
|
|
17946
18818
|
);
|
|
17947
18819
|
const normalized = /* @__PURE__ */ new Map();
|
|
@@ -17955,57 +18827,104 @@ var JsonFileStore = class {
|
|
|
17955
18827
|
}
|
|
17956
18828
|
this.bindings = normalized;
|
|
17957
18829
|
if (changed) {
|
|
17958
|
-
|
|
18830
|
+
withFileLock(BINDINGS_PATH, () => {
|
|
18831
|
+
const latest = readJson2(BINDINGS_PATH, {});
|
|
18832
|
+
this.bindings = new Map(Object.values(latest).map((binding) => {
|
|
18833
|
+
const upgraded = upgradeLegacyBinding(binding);
|
|
18834
|
+
return [`${upgraded.channelType}:${upgraded.chatId}`, upgraded];
|
|
18835
|
+
}));
|
|
18836
|
+
this.persistBindings();
|
|
18837
|
+
});
|
|
17959
18838
|
}
|
|
17960
18839
|
}
|
|
18840
|
+
reloadPermissions() {
|
|
18841
|
+
this.permissionLinks = new Map(Object.entries(
|
|
18842
|
+
readJson2(PERMISSIONS_PATH, {})
|
|
18843
|
+
));
|
|
18844
|
+
}
|
|
18845
|
+
reloadOffsets() {
|
|
18846
|
+
this.offsets = new Map(Object.entries(readJson2(OFFSETS_PATH, {})));
|
|
18847
|
+
}
|
|
18848
|
+
reloadDedup() {
|
|
18849
|
+
this.dedupKeys = new Map(Object.entries(readJson2(DEDUP_PATH, {})));
|
|
18850
|
+
}
|
|
18851
|
+
reloadAudit() {
|
|
18852
|
+
this.auditLog = readJson2(AUDIT_PATH, []);
|
|
18853
|
+
}
|
|
17961
18854
|
persistSessions() {
|
|
17962
18855
|
writeJson(
|
|
17963
|
-
|
|
18856
|
+
SESSIONS_PATH,
|
|
17964
18857
|
Object.fromEntries(this.sessions)
|
|
17965
18858
|
);
|
|
17966
18859
|
}
|
|
17967
18860
|
persistBindings() {
|
|
17968
18861
|
writeJson(
|
|
17969
|
-
|
|
18862
|
+
BINDINGS_PATH,
|
|
17970
18863
|
Object.fromEntries(this.bindings)
|
|
17971
18864
|
);
|
|
17972
18865
|
}
|
|
17973
18866
|
persistPermissions() {
|
|
17974
18867
|
writeJson(
|
|
17975
|
-
|
|
18868
|
+
PERMISSIONS_PATH,
|
|
17976
18869
|
Object.fromEntries(this.permissionLinks)
|
|
17977
18870
|
);
|
|
17978
18871
|
}
|
|
17979
18872
|
persistOffsets() {
|
|
17980
18873
|
writeJson(
|
|
17981
|
-
|
|
18874
|
+
OFFSETS_PATH,
|
|
17982
18875
|
Object.fromEntries(this.offsets)
|
|
17983
18876
|
);
|
|
17984
18877
|
}
|
|
17985
18878
|
persistDedup() {
|
|
17986
18879
|
writeJson(
|
|
17987
|
-
|
|
18880
|
+
DEDUP_PATH,
|
|
17988
18881
|
Object.fromEntries(this.dedupKeys)
|
|
17989
18882
|
);
|
|
17990
18883
|
}
|
|
17991
18884
|
persistAudit() {
|
|
17992
|
-
writeJson(
|
|
18885
|
+
writeJson(AUDIT_PATH, this.auditLog);
|
|
17993
18886
|
}
|
|
17994
18887
|
persistMessages(sessionId) {
|
|
17995
18888
|
const msgs = this.messages.get(sessionId) || [];
|
|
17996
|
-
writeJson(
|
|
18889
|
+
writeJson(path15.join(MESSAGES_DIR, `${sessionId}.json`), msgs);
|
|
17997
18890
|
}
|
|
17998
18891
|
loadMessages(sessionId) {
|
|
17999
18892
|
if (this.messages.has(sessionId)) {
|
|
18000
18893
|
return this.messages.get(sessionId);
|
|
18001
18894
|
}
|
|
18002
18895
|
const msgs = readJson2(
|
|
18003
|
-
|
|
18896
|
+
path15.join(MESSAGES_DIR, `${sessionId}.json`),
|
|
18004
18897
|
[]
|
|
18005
18898
|
);
|
|
18006
18899
|
this.messages.set(sessionId, msgs);
|
|
18007
18900
|
return msgs;
|
|
18008
18901
|
}
|
|
18902
|
+
mutateBindings(mutation) {
|
|
18903
|
+
return withFileLock(BINDINGS_PATH, () => {
|
|
18904
|
+
this.reloadBindings();
|
|
18905
|
+
const result = mutation();
|
|
18906
|
+
this.persistBindings();
|
|
18907
|
+
return result;
|
|
18908
|
+
});
|
|
18909
|
+
}
|
|
18910
|
+
mutateSessions(mutation) {
|
|
18911
|
+
return withFileLock(SESSIONS_PATH, () => {
|
|
18912
|
+
this.reloadSessions();
|
|
18913
|
+
const result = mutation();
|
|
18914
|
+
this.persistSessions();
|
|
18915
|
+
return result;
|
|
18916
|
+
});
|
|
18917
|
+
}
|
|
18918
|
+
mutateMessages(sessionId, mutation) {
|
|
18919
|
+
const messagePath = path15.join(MESSAGES_DIR, `${sessionId}.json`);
|
|
18920
|
+
return withFileLock(messagePath, () => {
|
|
18921
|
+
const messages = readJson2(messagePath, []);
|
|
18922
|
+
this.messages.set(sessionId, messages);
|
|
18923
|
+
const result = mutation(messages);
|
|
18924
|
+
this.persistMessages(sessionId);
|
|
18925
|
+
return result;
|
|
18926
|
+
});
|
|
18927
|
+
}
|
|
18009
18928
|
// ── Settings ──
|
|
18010
18929
|
refreshSettings() {
|
|
18011
18930
|
if (!this.dynamicSettings) return;
|
|
@@ -18028,66 +18947,65 @@ var JsonFileStore = class {
|
|
|
18028
18947
|
return this.bindings.get(`${channelType}:${chatId}`) ?? null;
|
|
18029
18948
|
}
|
|
18030
18949
|
upsertChannelBinding(data) {
|
|
18031
|
-
this.
|
|
18032
|
-
|
|
18033
|
-
|
|
18034
|
-
|
|
18035
|
-
|
|
18036
|
-
|
|
18950
|
+
return this.mutateBindings(() => {
|
|
18951
|
+
const key = `${data.channelType}:${data.chatId}`;
|
|
18952
|
+
const existing = this.bindings.get(key);
|
|
18953
|
+
if (existing) {
|
|
18954
|
+
const updated = {
|
|
18955
|
+
...existing,
|
|
18956
|
+
codepilotSessionId: data.codepilotSessionId,
|
|
18957
|
+
sdkSessionId: data.sdkSessionId ?? existing.sdkSessionId,
|
|
18958
|
+
channelProvider: data.channelProvider ?? existing.channelProvider,
|
|
18959
|
+
channelAlias: data.channelAlias ?? existing.channelAlias,
|
|
18960
|
+
chatUserId: data.chatUserId ?? existing.chatUserId,
|
|
18961
|
+
chatDisplayName: data.chatDisplayName ?? existing.chatDisplayName,
|
|
18962
|
+
workingDirectory: data.workingDirectory,
|
|
18963
|
+
model: data.model,
|
|
18964
|
+
mode: data.mode ?? existing.mode,
|
|
18965
|
+
updatedAt: now()
|
|
18966
|
+
};
|
|
18967
|
+
this.bindings.set(key, updated);
|
|
18968
|
+
return updated;
|
|
18969
|
+
}
|
|
18970
|
+
const binding = {
|
|
18971
|
+
id: uuid(),
|
|
18972
|
+
channelType: data.channelType,
|
|
18973
|
+
channelProvider: data.channelProvider,
|
|
18974
|
+
channelAlias: data.channelAlias,
|
|
18975
|
+
chatId: data.chatId,
|
|
18976
|
+
chatUserId: data.chatUserId,
|
|
18977
|
+
chatDisplayName: data.chatDisplayName,
|
|
18037
18978
|
codepilotSessionId: data.codepilotSessionId,
|
|
18038
|
-
sdkSessionId: data.sdkSessionId ??
|
|
18039
|
-
channelProvider: data.channelProvider ?? existing.channelProvider,
|
|
18040
|
-
channelAlias: data.channelAlias ?? existing.channelAlias,
|
|
18041
|
-
chatUserId: data.chatUserId ?? existing.chatUserId,
|
|
18042
|
-
chatDisplayName: data.chatDisplayName ?? existing.chatDisplayName,
|
|
18979
|
+
sdkSessionId: data.sdkSessionId ?? "",
|
|
18043
18980
|
workingDirectory: data.workingDirectory,
|
|
18044
18981
|
model: data.model,
|
|
18045
|
-
mode: data.mode
|
|
18982
|
+
mode: data.mode || this.getSetting("bridge_default_mode") || "code",
|
|
18983
|
+
active: true,
|
|
18984
|
+
createdAt: now(),
|
|
18046
18985
|
updatedAt: now()
|
|
18047
18986
|
};
|
|
18048
|
-
this.bindings.set(key,
|
|
18049
|
-
|
|
18050
|
-
|
|
18051
|
-
}
|
|
18052
|
-
const binding = {
|
|
18053
|
-
id: uuid(),
|
|
18054
|
-
channelType: data.channelType,
|
|
18055
|
-
channelProvider: data.channelProvider,
|
|
18056
|
-
channelAlias: data.channelAlias,
|
|
18057
|
-
chatId: data.chatId,
|
|
18058
|
-
chatUserId: data.chatUserId,
|
|
18059
|
-
chatDisplayName: data.chatDisplayName,
|
|
18060
|
-
codepilotSessionId: data.codepilotSessionId,
|
|
18061
|
-
sdkSessionId: data.sdkSessionId ?? "",
|
|
18062
|
-
workingDirectory: data.workingDirectory,
|
|
18063
|
-
model: data.model,
|
|
18064
|
-
mode: data.mode || this.getSetting("bridge_default_mode") || "code",
|
|
18065
|
-
active: true,
|
|
18066
|
-
createdAt: now(),
|
|
18067
|
-
updatedAt: now()
|
|
18068
|
-
};
|
|
18069
|
-
this.bindings.set(key, binding);
|
|
18070
|
-
this.persistBindings();
|
|
18071
|
-
return binding;
|
|
18987
|
+
this.bindings.set(key, binding);
|
|
18988
|
+
return binding;
|
|
18989
|
+
});
|
|
18072
18990
|
}
|
|
18073
18991
|
deleteChannelBinding(id) {
|
|
18074
|
-
this.
|
|
18075
|
-
|
|
18076
|
-
|
|
18077
|
-
|
|
18078
|
-
|
|
18079
|
-
|
|
18080
|
-
}
|
|
18992
|
+
this.mutateBindings(() => {
|
|
18993
|
+
for (const [key, binding] of this.bindings) {
|
|
18994
|
+
if (binding.id !== id) continue;
|
|
18995
|
+
this.bindings.delete(key);
|
|
18996
|
+
return;
|
|
18997
|
+
}
|
|
18998
|
+
});
|
|
18081
18999
|
}
|
|
18082
19000
|
updateChannelBinding(id, updates) {
|
|
18083
|
-
this.
|
|
18084
|
-
|
|
18085
|
-
|
|
18086
|
-
|
|
18087
|
-
|
|
18088
|
-
|
|
19001
|
+
this.mutateBindings(() => {
|
|
19002
|
+
for (const [key, b] of this.bindings) {
|
|
19003
|
+
if (b.id === id) {
|
|
19004
|
+
this.bindings.set(key, { ...b, ...updates, updatedAt: now() });
|
|
19005
|
+
break;
|
|
19006
|
+
}
|
|
18089
19007
|
}
|
|
18090
|
-
}
|
|
19008
|
+
});
|
|
18091
19009
|
}
|
|
18092
19010
|
listChannelBindings(channelType) {
|
|
18093
19011
|
this.reloadBindings();
|
|
@@ -18114,74 +19032,78 @@ var JsonFileStore = class {
|
|
|
18114
19032
|
return null;
|
|
18115
19033
|
}
|
|
18116
19034
|
createSession(name, model, systemPrompt, cwd, mode, options) {
|
|
18117
|
-
this.
|
|
18118
|
-
|
|
18119
|
-
|
|
18120
|
-
|
|
18121
|
-
|
|
18122
|
-
|
|
18123
|
-
|
|
18124
|
-
|
|
18125
|
-
|
|
18126
|
-
|
|
18127
|
-
|
|
18128
|
-
|
|
18129
|
-
|
|
18130
|
-
|
|
18131
|
-
|
|
18132
|
-
|
|
18133
|
-
|
|
18134
|
-
|
|
18135
|
-
|
|
18136
|
-
|
|
19035
|
+
return this.mutateSessions(() => {
|
|
19036
|
+
const timestamp = now();
|
|
19037
|
+
const session = {
|
|
19038
|
+
id: uuid(),
|
|
19039
|
+
name,
|
|
19040
|
+
working_directory: cwd || process.cwd(),
|
|
19041
|
+
model,
|
|
19042
|
+
preferred_mode: mode,
|
|
19043
|
+
system_prompt: systemPrompt,
|
|
19044
|
+
reasoning_effort: options?.reasoningEffort,
|
|
19045
|
+
session_type: options?.sessionType || "normal",
|
|
19046
|
+
hidden: options?.hidden === true,
|
|
19047
|
+
parent_session_id: options?.parentSessionId,
|
|
19048
|
+
expires_at: options?.expiresAt,
|
|
19049
|
+
created_at: timestamp,
|
|
19050
|
+
updated_at: timestamp
|
|
19051
|
+
};
|
|
19052
|
+
this.sessions.set(session.id, session);
|
|
19053
|
+
return session;
|
|
19054
|
+
});
|
|
18137
19055
|
}
|
|
18138
19056
|
updateSessionProviderId(sessionId, providerId) {
|
|
18139
|
-
this.
|
|
18140
|
-
|
|
18141
|
-
|
|
19057
|
+
this.mutateSessions(() => {
|
|
19058
|
+
const s = this.sessions.get(sessionId);
|
|
19059
|
+
if (!s) return;
|
|
18142
19060
|
s.provider_id = providerId;
|
|
18143
19061
|
s.updated_at = now();
|
|
18144
|
-
|
|
18145
|
-
}
|
|
19062
|
+
});
|
|
18146
19063
|
}
|
|
18147
19064
|
updateSession(sessionId, updates, options) {
|
|
18148
|
-
this.
|
|
18149
|
-
|
|
18150
|
-
|
|
18151
|
-
|
|
18152
|
-
|
|
18153
|
-
|
|
18154
|
-
|
|
18155
|
-
|
|
18156
|
-
|
|
18157
|
-
|
|
18158
|
-
|
|
19065
|
+
this.mutateSessions(() => {
|
|
19066
|
+
const session = this.sessions.get(sessionId);
|
|
19067
|
+
if (!session) return;
|
|
19068
|
+
const next = {
|
|
19069
|
+
...session,
|
|
19070
|
+
...updates,
|
|
19071
|
+
id: session.id,
|
|
19072
|
+
updated_at: options?.touch === false ? session.updated_at : now()
|
|
19073
|
+
};
|
|
19074
|
+
this.sessions.set(sessionId, next);
|
|
19075
|
+
});
|
|
18159
19076
|
}
|
|
18160
19077
|
deleteSession(sessionId) {
|
|
18161
|
-
|
|
18162
|
-
|
|
18163
|
-
|
|
18164
|
-
|
|
18165
|
-
|
|
18166
|
-
|
|
19078
|
+
const messagePath = path15.join(MESSAGES_DIR, `${sessionId}.json`);
|
|
19079
|
+
withFileLocks([SESSIONS_PATH, BINDINGS_PATH, messagePath], () => {
|
|
19080
|
+
this.reloadSessions();
|
|
19081
|
+
this.reloadBindings();
|
|
19082
|
+
this.sessions.delete(sessionId);
|
|
19083
|
+
for (const [key, binding] of this.bindings) {
|
|
19084
|
+
if (binding.codepilotSessionId === sessionId) {
|
|
19085
|
+
this.bindings.delete(key);
|
|
19086
|
+
}
|
|
18167
19087
|
}
|
|
18168
|
-
|
|
18169
|
-
|
|
18170
|
-
|
|
18171
|
-
|
|
18172
|
-
|
|
18173
|
-
|
|
18174
|
-
|
|
18175
|
-
|
|
19088
|
+
this.messages.delete(sessionId);
|
|
19089
|
+
try {
|
|
19090
|
+
fs13.rmSync(messagePath, { force: true });
|
|
19091
|
+
} catch {
|
|
19092
|
+
}
|
|
19093
|
+
this.persistSessions();
|
|
19094
|
+
this.persistBindings();
|
|
19095
|
+
});
|
|
18176
19096
|
}
|
|
18177
19097
|
// ── Messages ──
|
|
18178
19098
|
addMessage(sessionId, role, content, _usage) {
|
|
18179
|
-
|
|
18180
|
-
|
|
18181
|
-
|
|
19099
|
+
this.mutateMessages(sessionId, (messages) => {
|
|
19100
|
+
messages.push({ role, content });
|
|
19101
|
+
});
|
|
18182
19102
|
}
|
|
18183
19103
|
getMessages(sessionId, opts) {
|
|
18184
|
-
const
|
|
19104
|
+
const messagePath = path15.join(MESSAGES_DIR, `${sessionId}.json`);
|
|
19105
|
+
const msgs = readJson2(messagePath, []);
|
|
19106
|
+
this.messages.set(sessionId, msgs);
|
|
18185
19107
|
if (opts?.limit && opts.limit > 0) {
|
|
18186
19108
|
return { messages: msgs.slice(-opts.limit) };
|
|
18187
19109
|
}
|
|
@@ -18213,61 +19135,62 @@ var JsonFileStore = class {
|
|
|
18213
19135
|
}
|
|
18214
19136
|
}
|
|
18215
19137
|
setSessionRuntimeStatus(_sessionId, _status) {
|
|
18216
|
-
this.
|
|
18217
|
-
|
|
18218
|
-
|
|
18219
|
-
|
|
18220
|
-
|
|
18221
|
-
|
|
18222
|
-
|
|
18223
|
-
|
|
18224
|
-
|
|
18225
|
-
|
|
18226
|
-
|
|
18227
|
-
|
|
18228
|
-
|
|
18229
|
-
|
|
18230
|
-
|
|
18231
|
-
|
|
18232
|
-
|
|
18233
|
-
|
|
18234
|
-
|
|
18235
|
-
|
|
19138
|
+
this.mutateSessions(() => {
|
|
19139
|
+
const session = this.sessions.get(_sessionId);
|
|
19140
|
+
if (!session) return;
|
|
19141
|
+
const queuedCount = session.queued_count && session.queued_count > 0 ? session.queued_count : 0;
|
|
19142
|
+
let runtimeStatus;
|
|
19143
|
+
if (_status === "running") {
|
|
19144
|
+
runtimeStatus = queuedCount > 0 ? "queued" : "running";
|
|
19145
|
+
} else if (_status === "idle") {
|
|
19146
|
+
runtimeStatus = queuedCount > 0 ? "queued" : "idle";
|
|
19147
|
+
} else {
|
|
19148
|
+
runtimeStatus = session.runtime_status;
|
|
19149
|
+
}
|
|
19150
|
+
const next = {
|
|
19151
|
+
...session,
|
|
19152
|
+
runtime_status: runtimeStatus,
|
|
19153
|
+
last_runtime_update_at: now(),
|
|
19154
|
+
updated_at: now()
|
|
19155
|
+
};
|
|
19156
|
+
this.sessions.set(_sessionId, next);
|
|
19157
|
+
});
|
|
18236
19158
|
}
|
|
18237
19159
|
// ── SDK Session ──
|
|
18238
19160
|
updateSdkSessionId(sessionId, sdkSessionId) {
|
|
18239
|
-
|
|
18240
|
-
|
|
18241
|
-
|
|
18242
|
-
|
|
18243
|
-
s
|
|
18244
|
-
|
|
18245
|
-
|
|
18246
|
-
|
|
18247
|
-
|
|
18248
|
-
|
|
18249
|
-
|
|
18250
|
-
|
|
19161
|
+
withFileLocks([SESSIONS_PATH, BINDINGS_PATH], () => {
|
|
19162
|
+
this.reloadSessions();
|
|
19163
|
+
this.reloadBindings();
|
|
19164
|
+
const s = this.sessions.get(sessionId);
|
|
19165
|
+
if (s) {
|
|
19166
|
+
s.sdk_session_id = sdkSessionId;
|
|
19167
|
+
if (sdkSessionId) {
|
|
19168
|
+
s.codex_thread_id = sdkSessionId;
|
|
19169
|
+
s.thread_origin = s.thread_origin || "bridge";
|
|
19170
|
+
} else {
|
|
19171
|
+
delete s.codex_thread_id;
|
|
19172
|
+
if (s.thread_origin !== "desktop") {
|
|
19173
|
+
delete s.thread_origin;
|
|
19174
|
+
}
|
|
18251
19175
|
}
|
|
19176
|
+
s.updated_at = now();
|
|
18252
19177
|
}
|
|
18253
|
-
|
|
18254
|
-
|
|
18255
|
-
|
|
18256
|
-
|
|
18257
|
-
if (b.codepilotSessionId === sessionId) {
|
|
18258
|
-
this.bindings.set(key, { ...b, sdkSessionId, updatedAt: now() });
|
|
19178
|
+
for (const [key, b] of this.bindings) {
|
|
19179
|
+
if (b.codepilotSessionId === sessionId) {
|
|
19180
|
+
this.bindings.set(key, { ...b, sdkSessionId, updatedAt: now() });
|
|
19181
|
+
}
|
|
18259
19182
|
}
|
|
18260
|
-
|
|
18261
|
-
|
|
19183
|
+
this.persistSessions();
|
|
19184
|
+
this.persistBindings();
|
|
19185
|
+
});
|
|
18262
19186
|
}
|
|
18263
19187
|
updateSessionModel(sessionId, model) {
|
|
18264
|
-
this.
|
|
18265
|
-
|
|
18266
|
-
|
|
19188
|
+
this.mutateSessions(() => {
|
|
19189
|
+
const s = this.sessions.get(sessionId);
|
|
19190
|
+
if (!s) return;
|
|
18267
19191
|
s.model = model;
|
|
18268
19192
|
s.updated_at = now();
|
|
18269
|
-
|
|
18270
|
-
}
|
|
19193
|
+
});
|
|
18271
19194
|
}
|
|
18272
19195
|
syncSdkTasks(_sessionId, _todos) {
|
|
18273
19196
|
}
|
|
@@ -18280,66 +19203,84 @@ var JsonFileStore = class {
|
|
|
18280
19203
|
}
|
|
18281
19204
|
// ── Audit & Dedup ──
|
|
18282
19205
|
insertAuditLog(entry) {
|
|
18283
|
-
|
|
18284
|
-
|
|
18285
|
-
|
|
18286
|
-
|
|
19206
|
+
withFileLock(AUDIT_PATH, () => {
|
|
19207
|
+
this.reloadAudit();
|
|
19208
|
+
this.auditLog.push({
|
|
19209
|
+
...entry,
|
|
19210
|
+
id: uuid(),
|
|
19211
|
+
createdAt: now()
|
|
19212
|
+
});
|
|
19213
|
+
if (this.auditLog.length > 1e3) {
|
|
19214
|
+
this.auditLog = this.auditLog.slice(-1e3);
|
|
19215
|
+
}
|
|
19216
|
+
this.persistAudit();
|
|
18287
19217
|
});
|
|
18288
|
-
if (this.auditLog.length > 1e3) {
|
|
18289
|
-
this.auditLog = this.auditLog.slice(-1e3);
|
|
18290
|
-
}
|
|
18291
|
-
this.persistAudit();
|
|
18292
19218
|
}
|
|
18293
19219
|
checkDedup(key) {
|
|
19220
|
+
this.reloadDedup();
|
|
18294
19221
|
const ts = this.dedupKeys.get(key);
|
|
18295
19222
|
if (ts === void 0) return false;
|
|
18296
|
-
if (Date.now() - ts >
|
|
19223
|
+
if (Date.now() - ts > dedupTtlMs(key)) {
|
|
18297
19224
|
this.dedupKeys.delete(key);
|
|
18298
19225
|
return false;
|
|
18299
19226
|
}
|
|
18300
19227
|
return true;
|
|
18301
19228
|
}
|
|
18302
19229
|
insertDedup(key) {
|
|
18303
|
-
|
|
18304
|
-
|
|
19230
|
+
withFileLock(DEDUP_PATH, () => {
|
|
19231
|
+
this.reloadDedup();
|
|
19232
|
+
this.dedupKeys.set(key, Date.now());
|
|
19233
|
+
this.persistDedup();
|
|
19234
|
+
});
|
|
18305
19235
|
}
|
|
18306
19236
|
cleanupExpiredDedup() {
|
|
18307
|
-
|
|
18308
|
-
|
|
18309
|
-
|
|
18310
|
-
|
|
18311
|
-
|
|
18312
|
-
|
|
19237
|
+
withFileLock(DEDUP_PATH, () => {
|
|
19238
|
+
this.reloadDedup();
|
|
19239
|
+
const timestamp = Date.now();
|
|
19240
|
+
let changed = false;
|
|
19241
|
+
for (const [key, ts] of this.dedupKeys) {
|
|
19242
|
+
if (timestamp - ts > dedupTtlMs(key)) {
|
|
19243
|
+
this.dedupKeys.delete(key);
|
|
19244
|
+
changed = true;
|
|
19245
|
+
}
|
|
18313
19246
|
}
|
|
18314
|
-
|
|
18315
|
-
|
|
19247
|
+
if (changed) this.persistDedup();
|
|
19248
|
+
});
|
|
18316
19249
|
}
|
|
18317
19250
|
insertOutboundRef(_ref) {
|
|
18318
19251
|
}
|
|
18319
19252
|
// ── Permission Links ──
|
|
18320
19253
|
insertPermissionLink(link2) {
|
|
18321
|
-
|
|
18322
|
-
|
|
18323
|
-
|
|
18324
|
-
|
|
18325
|
-
|
|
18326
|
-
|
|
18327
|
-
|
|
18328
|
-
|
|
18329
|
-
|
|
18330
|
-
|
|
19254
|
+
withFileLock(PERMISSIONS_PATH, () => {
|
|
19255
|
+
this.reloadPermissions();
|
|
19256
|
+
const record = {
|
|
19257
|
+
permissionRequestId: link2.permissionRequestId,
|
|
19258
|
+
chatId: link2.chatId,
|
|
19259
|
+
messageId: link2.messageId,
|
|
19260
|
+
sessionId: link2.sessionId,
|
|
19261
|
+
resolved: false,
|
|
19262
|
+
suggestions: link2.suggestions
|
|
19263
|
+
};
|
|
19264
|
+
this.permissionLinks.set(link2.permissionRequestId, record);
|
|
19265
|
+
this.persistPermissions();
|
|
19266
|
+
});
|
|
18331
19267
|
}
|
|
18332
19268
|
getPermissionLink(permissionRequestId) {
|
|
19269
|
+
this.reloadPermissions();
|
|
18333
19270
|
return this.permissionLinks.get(permissionRequestId) ?? null;
|
|
18334
19271
|
}
|
|
18335
19272
|
markPermissionLinkResolved(permissionRequestId) {
|
|
18336
|
-
|
|
18337
|
-
|
|
18338
|
-
|
|
18339
|
-
|
|
18340
|
-
|
|
19273
|
+
return withFileLock(PERMISSIONS_PATH, () => {
|
|
19274
|
+
this.reloadPermissions();
|
|
19275
|
+
const link2 = this.permissionLinks.get(permissionRequestId);
|
|
19276
|
+
if (!link2 || link2.resolved) return false;
|
|
19277
|
+
link2.resolved = true;
|
|
19278
|
+
this.persistPermissions();
|
|
19279
|
+
return true;
|
|
19280
|
+
});
|
|
18341
19281
|
}
|
|
18342
19282
|
listPendingPermissionLinksByChat(chatId) {
|
|
19283
|
+
this.reloadPermissions();
|
|
18343
19284
|
const result = [];
|
|
18344
19285
|
for (const link2 of this.permissionLinks.values()) {
|
|
18345
19286
|
if (link2.chatId === chatId && !link2.resolved) {
|
|
@@ -18350,11 +19291,15 @@ var JsonFileStore = class {
|
|
|
18350
19291
|
}
|
|
18351
19292
|
// ── Channel Offsets ──
|
|
18352
19293
|
getChannelOffset(key) {
|
|
19294
|
+
this.reloadOffsets();
|
|
18353
19295
|
return this.offsets.get(key) ?? "0";
|
|
18354
19296
|
}
|
|
18355
19297
|
setChannelOffset(key, offset) {
|
|
18356
|
-
|
|
18357
|
-
|
|
19298
|
+
withFileLock(OFFSETS_PATH, () => {
|
|
19299
|
+
this.reloadOffsets();
|
|
19300
|
+
this.offsets.set(key, offset);
|
|
19301
|
+
this.persistOffsets();
|
|
19302
|
+
});
|
|
18358
19303
|
}
|
|
18359
19304
|
};
|
|
18360
19305
|
|
|
@@ -18397,8 +19342,8 @@ var PendingPermissions = class {
|
|
|
18397
19342
|
};
|
|
18398
19343
|
|
|
18399
19344
|
// src/logger.ts
|
|
18400
|
-
import
|
|
18401
|
-
import
|
|
19345
|
+
import fs14 from "node:fs";
|
|
19346
|
+
import path16 from "node:path";
|
|
18402
19347
|
import { inspect as inspect2 } from "node:util";
|
|
18403
19348
|
var MASK_PATTERNS = [
|
|
18404
19349
|
/(?:token|secret|password|api_key)["']?\s*[:=]\s*["']?([^\s"',]+)/gi,
|
|
@@ -18416,8 +19361,8 @@ function maskSecrets(text2) {
|
|
|
18416
19361
|
}
|
|
18417
19362
|
return result;
|
|
18418
19363
|
}
|
|
18419
|
-
var LOG_DIR =
|
|
18420
|
-
var LOG_PATH =
|
|
19364
|
+
var LOG_DIR = path16.join(CTI_HOME, "logs");
|
|
19365
|
+
var LOG_PATH = path16.join(LOG_DIR, "bridge.log");
|
|
18421
19366
|
var MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
18422
19367
|
var MAX_ROTATED = 3;
|
|
18423
19368
|
var logStream = null;
|
|
@@ -18438,11 +19383,11 @@ function formatLogArg(value) {
|
|
|
18438
19383
|
return String(value);
|
|
18439
19384
|
}
|
|
18440
19385
|
function openLogStream() {
|
|
18441
|
-
return
|
|
19386
|
+
return fs14.createWriteStream(LOG_PATH, { flags: "a" });
|
|
18442
19387
|
}
|
|
18443
19388
|
function rotateIfNeeded() {
|
|
18444
19389
|
try {
|
|
18445
|
-
const stat =
|
|
19390
|
+
const stat = fs14.statSync(LOG_PATH);
|
|
18446
19391
|
if (stat.size < MAX_LOG_SIZE) return;
|
|
18447
19392
|
} catch {
|
|
18448
19393
|
return;
|
|
@@ -18452,17 +19397,17 @@ function rotateIfNeeded() {
|
|
|
18452
19397
|
logStream = null;
|
|
18453
19398
|
}
|
|
18454
19399
|
const path32 = `${LOG_PATH}.${MAX_ROTATED}`;
|
|
18455
|
-
if (
|
|
19400
|
+
if (fs14.existsSync(path32)) fs14.unlinkSync(path32);
|
|
18456
19401
|
for (let i = MAX_ROTATED - 1; i >= 1; i--) {
|
|
18457
19402
|
const src = `${LOG_PATH}.${i}`;
|
|
18458
19403
|
const dst = `${LOG_PATH}.${i + 1}`;
|
|
18459
|
-
if (
|
|
19404
|
+
if (fs14.existsSync(src)) fs14.renameSync(src, dst);
|
|
18460
19405
|
}
|
|
18461
|
-
|
|
19406
|
+
fs14.renameSync(LOG_PATH, `${LOG_PATH}.1`);
|
|
18462
19407
|
logStream = openLogStream();
|
|
18463
19408
|
}
|
|
18464
19409
|
function setupLogger() {
|
|
18465
|
-
|
|
19410
|
+
fs14.mkdirSync(LOG_DIR, { recursive: true });
|
|
18466
19411
|
logStream = openLogStream();
|
|
18467
19412
|
const write = (level, args) => {
|
|
18468
19413
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -18478,18 +19423,18 @@ function setupLogger() {
|
|
|
18478
19423
|
}
|
|
18479
19424
|
|
|
18480
19425
|
// src/bridge-instance-lock.ts
|
|
18481
|
-
import
|
|
18482
|
-
import
|
|
18483
|
-
var runtimeDir =
|
|
18484
|
-
var bridgeInstanceLockFile =
|
|
19426
|
+
import fs15 from "node:fs";
|
|
19427
|
+
import path17 from "node:path";
|
|
19428
|
+
var runtimeDir = path17.join(CTI_HOME, "runtime");
|
|
19429
|
+
var bridgeInstanceLockFile = path17.join(runtimeDir, "bridge.instance.lock");
|
|
18485
19430
|
function readJsonFile(filePath, fallback) {
|
|
18486
19431
|
try {
|
|
18487
|
-
return JSON.parse(
|
|
19432
|
+
return JSON.parse(fs15.readFileSync(filePath, "utf-8"));
|
|
18488
19433
|
} catch {
|
|
18489
19434
|
return fallback;
|
|
18490
19435
|
}
|
|
18491
19436
|
}
|
|
18492
|
-
function
|
|
19437
|
+
function isProcessAlive2(pid) {
|
|
18493
19438
|
if (!pid) return false;
|
|
18494
19439
|
try {
|
|
18495
19440
|
process.kill(pid, 0);
|
|
@@ -18509,15 +19454,15 @@ function tryAcquireBridgeInstanceLock(options = {}) {
|
|
|
18509
19454
|
const filePath = options.filePath ?? bridgeInstanceLockFile;
|
|
18510
19455
|
const ownerPid = options.ownerPid ?? process.pid;
|
|
18511
19456
|
const nowMs = options.nowMs ?? Date.now();
|
|
18512
|
-
const isAlive = options.isAlive ??
|
|
19457
|
+
const isAlive = options.isAlive ?? isProcessAlive2;
|
|
18513
19458
|
const payload = {
|
|
18514
19459
|
pid: ownerPid,
|
|
18515
19460
|
createdAt: new Date(nowMs).toISOString()
|
|
18516
19461
|
};
|
|
18517
19462
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
18518
19463
|
try {
|
|
18519
|
-
|
|
18520
|
-
|
|
19464
|
+
fs15.mkdirSync(path17.dirname(filePath), { recursive: true });
|
|
19465
|
+
fs15.writeFileSync(filePath, JSON.stringify(payload, null, 2), { encoding: "utf-8", flag: "wx" });
|
|
18521
19466
|
return { acquired: true };
|
|
18522
19467
|
} catch (error) {
|
|
18523
19468
|
const code2 = error.code;
|
|
@@ -18527,7 +19472,7 @@ function tryAcquireBridgeInstanceLock(options = {}) {
|
|
|
18527
19472
|
return { acquired: false, holderPid: existing2.pid };
|
|
18528
19473
|
}
|
|
18529
19474
|
try {
|
|
18530
|
-
|
|
19475
|
+
fs15.unlinkSync(filePath);
|
|
18531
19476
|
} catch {
|
|
18532
19477
|
}
|
|
18533
19478
|
}
|
|
@@ -18542,37 +19487,46 @@ function releaseBridgeInstanceLock(filePath = bridgeInstanceLockFile, ownerPid =
|
|
|
18542
19487
|
const existing = readBridgeInstanceLock(filePath);
|
|
18543
19488
|
if (!existing) {
|
|
18544
19489
|
try {
|
|
18545
|
-
|
|
19490
|
+
fs15.unlinkSync(filePath);
|
|
18546
19491
|
} catch {
|
|
18547
19492
|
}
|
|
18548
19493
|
return;
|
|
18549
19494
|
}
|
|
18550
19495
|
if (existing.pid !== ownerPid) return;
|
|
18551
19496
|
try {
|
|
18552
|
-
|
|
19497
|
+
fs15.unlinkSync(filePath);
|
|
18553
19498
|
} catch {
|
|
18554
19499
|
}
|
|
18555
19500
|
}
|
|
18556
19501
|
|
|
18557
|
-
// src/
|
|
18558
|
-
|
|
18559
|
-
|
|
18560
|
-
var PID_FILE = path18.join(RUNTIME_DIR, "bridge.pid");
|
|
18561
|
-
async function resolveProvider() {
|
|
18562
|
-
const { CodexProvider: CodexProvider2 } = await Promise.resolve().then(() => (init_codex_provider(), codex_provider_exports));
|
|
18563
|
-
return new CodexProvider2();
|
|
18564
|
-
}
|
|
18565
|
-
function writeStatus(info) {
|
|
18566
|
-
fs15.mkdirSync(RUNTIME_DIR, { recursive: true });
|
|
19502
|
+
// src/runtime-status.ts
|
|
19503
|
+
import fs16 from "node:fs";
|
|
19504
|
+
function writeRuntimeStatus(filePath, info, options = {}) {
|
|
18567
19505
|
let existing = {};
|
|
18568
19506
|
try {
|
|
18569
|
-
existing = JSON.parse(
|
|
19507
|
+
existing = JSON.parse(fs16.readFileSync(filePath, "utf-8"));
|
|
18570
19508
|
} catch {
|
|
18571
19509
|
}
|
|
19510
|
+
const existingRunId = typeof existing.runId === "string" ? existing.runId : "";
|
|
19511
|
+
if (options.expectedRunId && existingRunId && existingRunId !== options.expectedRunId && options.allowRunIdTakeover !== true) {
|
|
19512
|
+
return false;
|
|
19513
|
+
}
|
|
18572
19514
|
const merged = { ...existing, ...info };
|
|
18573
|
-
|
|
18574
|
-
|
|
18575
|
-
|
|
19515
|
+
atomicWriteFileSync(filePath, JSON.stringify(merged, null, 2));
|
|
19516
|
+
return true;
|
|
19517
|
+
}
|
|
19518
|
+
|
|
19519
|
+
// src/main.ts
|
|
19520
|
+
var RUNTIME_DIR = path19.join(CTI_HOME, "runtime");
|
|
19521
|
+
var STATUS_FILE = path19.join(RUNTIME_DIR, "status.json");
|
|
19522
|
+
var PID_FILE = path19.join(RUNTIME_DIR, "bridge.pid");
|
|
19523
|
+
var runId = crypto11.randomUUID();
|
|
19524
|
+
async function resolveProvider() {
|
|
19525
|
+
const { CodexProvider: CodexProvider2 } = await Promise.resolve().then(() => (init_codex_provider(), codex_provider_exports));
|
|
19526
|
+
return new CodexProvider2();
|
|
19527
|
+
}
|
|
19528
|
+
function writeStatus(info, options = {}) {
|
|
19529
|
+
return writeRuntimeStatus(STATUS_FILE, info, options);
|
|
18576
19530
|
}
|
|
18577
19531
|
function getRunningChannels() {
|
|
18578
19532
|
return getStatus().adapters.map((adapter) => adapter.channelType).sort();
|
|
@@ -18584,10 +19538,6 @@ async function main() {
|
|
|
18584
19538
|
const lockState = tryAcquireBridgeInstanceLock();
|
|
18585
19539
|
if (!lockState.acquired) {
|
|
18586
19540
|
const holderPid = lockState.holderPid;
|
|
18587
|
-
writeStatus({
|
|
18588
|
-
running: true,
|
|
18589
|
-
...Number.isFinite(holderPid) && holderPid ? { pid: holderPid } : {}
|
|
18590
|
-
});
|
|
18591
19541
|
console.log(
|
|
18592
19542
|
`[codex-to-im] Another bridge daemon is already running${holderPid ? ` (PID: ${holderPid})` : ""}. Exiting duplicate launcher.`
|
|
18593
19543
|
);
|
|
@@ -18599,9 +19549,15 @@ async function main() {
|
|
|
18599
19549
|
releaseBridgeInstanceLock(void 0, process.pid);
|
|
18600
19550
|
instanceLockHeld = false;
|
|
18601
19551
|
};
|
|
19552
|
+
writeStatus({
|
|
19553
|
+
running: false,
|
|
19554
|
+
pid: process.pid,
|
|
19555
|
+
runId,
|
|
19556
|
+
channels: [],
|
|
19557
|
+
adapters: []
|
|
19558
|
+
}, { expectedRunId: runId, allowRunIdTakeover: true });
|
|
18602
19559
|
const config2 = loadConfig();
|
|
18603
19560
|
setupLogger();
|
|
18604
|
-
const runId = crypto7.randomUUID();
|
|
18605
19561
|
console.log(`[codex-to-im] Starting bridge (run_id: ${runId})`);
|
|
18606
19562
|
const settings = configToSettings(config2);
|
|
18607
19563
|
const store = new JsonFileStore(settings, { dynamicSettings: true });
|
|
@@ -18617,8 +19573,8 @@ async function main() {
|
|
|
18617
19573
|
permissions: gateway,
|
|
18618
19574
|
lifecycle: {
|
|
18619
19575
|
onBridgeStart: () => {
|
|
18620
|
-
|
|
18621
|
-
|
|
19576
|
+
fs18.mkdirSync(RUNTIME_DIR, { recursive: true });
|
|
19577
|
+
fs18.writeFileSync(PID_FILE, String(process.pid), "utf-8");
|
|
18622
19578
|
const channels = getRunningChannels();
|
|
18623
19579
|
writeStatus({
|
|
18624
19580
|
running: true,
|
|
@@ -18627,7 +19583,7 @@ async function main() {
|
|
|
18627
19583
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
18628
19584
|
channels,
|
|
18629
19585
|
adapters: getAdapterStatuses()
|
|
18630
|
-
});
|
|
19586
|
+
}, { expectedRunId: runId });
|
|
18631
19587
|
console.log(`[codex-to-im] Bridge started (PID: ${process.pid}, channels: ${channels.join(", ")})`);
|
|
18632
19588
|
},
|
|
18633
19589
|
onBridgeAdaptersChanged: (channels) => {
|
|
@@ -18637,41 +19593,60 @@ async function main() {
|
|
|
18637
19593
|
runId,
|
|
18638
19594
|
channels,
|
|
18639
19595
|
adapters: getAdapterStatuses()
|
|
18640
|
-
});
|
|
19596
|
+
}, { expectedRunId: runId });
|
|
18641
19597
|
console.log(`[codex-to-im] Active channels updated: ${channels.join(", ") || "none"}`);
|
|
18642
19598
|
},
|
|
18643
19599
|
onBridgeStop: () => {
|
|
18644
19600
|
releaseInstanceLock();
|
|
18645
|
-
writeStatus({ running: false, channels: [], adapters: [] });
|
|
19601
|
+
writeStatus({ running: false, channels: [], adapters: [] }, { expectedRunId: runId });
|
|
18646
19602
|
console.log("[codex-to-im] Bridge stopped");
|
|
18647
19603
|
}
|
|
18648
19604
|
}
|
|
18649
19605
|
});
|
|
18650
19606
|
await start();
|
|
18651
19607
|
let shuttingDown = false;
|
|
18652
|
-
const shutdown = async (
|
|
19608
|
+
const shutdown = async (reason, exitCode = 0) => {
|
|
18653
19609
|
if (shuttingDown) return;
|
|
18654
19610
|
shuttingDown = true;
|
|
18655
|
-
const reason = signal ? `signal: ${signal}` : "shutdown requested";
|
|
18656
19611
|
console.log(`[codex-to-im] Shutting down (${reason})...`);
|
|
18657
19612
|
pendingPerms.denyAll();
|
|
18658
|
-
|
|
18659
|
-
|
|
18660
|
-
|
|
18661
|
-
|
|
19613
|
+
try {
|
|
19614
|
+
await stop();
|
|
19615
|
+
} catch (error) {
|
|
19616
|
+
console.error("[codex-to-im] Graceful bridge stop failed:", error instanceof Error ? error.stack || error.message : error);
|
|
19617
|
+
} finally {
|
|
19618
|
+
releaseInstanceLock();
|
|
19619
|
+
try {
|
|
19620
|
+
writeStatus({ running: false, lastExitReason: reason }, { expectedRunId: runId });
|
|
19621
|
+
} catch (error) {
|
|
19622
|
+
console.error("[codex-to-im] Failed to write final runtime status:", error instanceof Error ? error.message : error);
|
|
19623
|
+
}
|
|
19624
|
+
process.exit(exitCode);
|
|
19625
|
+
}
|
|
18662
19626
|
};
|
|
18663
|
-
process.on("SIGTERM", () =>
|
|
18664
|
-
|
|
18665
|
-
|
|
19627
|
+
process.on("SIGTERM", () => {
|
|
19628
|
+
void shutdown("signal: SIGTERM");
|
|
19629
|
+
});
|
|
19630
|
+
process.on("SIGINT", () => {
|
|
19631
|
+
void shutdown("signal: SIGINT");
|
|
19632
|
+
});
|
|
19633
|
+
process.on("SIGHUP", () => {
|
|
19634
|
+
void shutdown("signal: SIGHUP");
|
|
19635
|
+
});
|
|
18666
19636
|
process.on("unhandledRejection", (reason) => {
|
|
18667
19637
|
console.error("[codex-to-im] unhandledRejection:", reason instanceof Error ? reason.stack || reason.message : reason);
|
|
18668
|
-
writeStatus({
|
|
19638
|
+
writeStatus({
|
|
19639
|
+
running: true,
|
|
19640
|
+
pid: process.pid,
|
|
19641
|
+
runId,
|
|
19642
|
+
channels: getRunningChannels(),
|
|
19643
|
+
adapters: getAdapterStatuses(),
|
|
19644
|
+
lastExitReason: `unhandledRejection: ${reason instanceof Error ? reason.message : String(reason)}`
|
|
19645
|
+
}, { expectedRunId: runId });
|
|
18669
19646
|
});
|
|
18670
19647
|
process.on("uncaughtException", (err) => {
|
|
18671
19648
|
console.error("[codex-to-im] uncaughtException:", err.stack || err.message);
|
|
18672
|
-
|
|
18673
|
-
writeStatus({ running: false, lastExitReason: `uncaughtException: ${err.message}` });
|
|
18674
|
-
process.exit(1);
|
|
19649
|
+
void shutdown(`uncaughtException: ${err.message}`, 1);
|
|
18675
19650
|
});
|
|
18676
19651
|
process.on("beforeExit", (code2) => {
|
|
18677
19652
|
console.log(`[codex-to-im] beforeExit (code: ${code2})`);
|
|
@@ -18687,7 +19662,10 @@ main().catch((err) => {
|
|
|
18687
19662
|
console.error("[codex-to-im] Fatal error:", err instanceof Error ? err.stack || err.message : err);
|
|
18688
19663
|
releaseBridgeInstanceLock(void 0, process.pid);
|
|
18689
19664
|
try {
|
|
18690
|
-
writeStatus(
|
|
19665
|
+
writeStatus(
|
|
19666
|
+
{ running: false, lastExitReason: `fatal: ${err instanceof Error ? err.message : String(err)}` },
|
|
19667
|
+
{ expectedRunId: runId }
|
|
19668
|
+
);
|
|
18691
19669
|
} catch {
|
|
18692
19670
|
}
|
|
18693
19671
|
process.exit(1);
|