cc-viewer 1.6.345 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -2
- package/cli.js +116 -37
- package/dist/assets/App-DTkacfRW.js +2 -0
- package/dist/assets/App-dYPa5-df.css +1 -0
- package/dist/assets/{MdxEditorPanel-C8ITEOzT.js → MdxEditorPanel-C5Q6uUyH.js} +1 -1
- package/dist/assets/Mobile-MoBp9c8k.js +1 -0
- package/dist/assets/{_baseUniq-CG81mgN_.js → _baseUniq-D9_E5ZDp.js} +1 -1
- package/dist/assets/{arc-C161J4f2.js → arc-D4iFrlhb.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-BmbDMdn9.js → architectureDiagram-Q4EWVU46-f1neo7hg.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-DXpMvdxd.js → blockDiagram-DXYQGD6D-DWi9e9tG.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-8Mypg0IU.js → c4Diagram-AHTNJAMY-DAdj49A2.js} +1 -1
- package/dist/assets/{channel-B2khdmvc.js → channel-B3VthT0m.js} +1 -1
- package/dist/assets/{chunk-4BX2VUAB-I61r1g6z.js → chunk-4BX2VUAB-BrPcAHV3.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-VFPlRE5E.js → chunk-4TB4RGXK-Bvopc1V9.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-DvCamg_6.js → chunk-55IACEB6-Juw-7Z3i.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-Dj4Q8RwR.js → chunk-EDXVE4YY-DVYP9tpB.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-_bgQuuJh.js → chunk-FMBD7UC4-VQ7VOXl3.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-3to38LyN.js → chunk-OYMX7WX6-F8Rj4ZUZ.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-Zl26wKKz.js → chunk-QZHKN3VN-Dn7K3yi9.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-RLYPrlh0.js → chunk-YZCP3GAM-UjO-KTsn.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-DmSozxoO.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-DmSozxoO.js +1 -0
- package/dist/assets/clone-BQtvy9Ae.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-CsYOjJ0V.js → cose-bilkent-S5V4N54A-BDhNj8rq.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-C3YXXaFU.js → dagre-KV5264BT-BzdcDuyt.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-DUCOg1ld.js → diagram-5BDNPKRD-DP_SX65N.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-BtlLl_zX.js → diagram-G4DWMVQ6-B-pBFw8Q.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-1Qp9xfOk.js → diagram-MMDJMWI5-6XSfN1eN.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-Cj7VMp3L.js → diagram-TYMM5635-DJvGxvpE.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-CVZVn541.js → erDiagram-SMLLAGMA-CpqdpGYj.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-CiU9Fkkv.js → flowDiagram-DWJPFMVM-C9m7wjwI.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-aRaG97ID.js → ganttDiagram-T4ZO3ILL-CU0XOfY3.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-Cr2LJlvX.js → gitGraphDiagram-UUTBAWPF-ipyGULnP.js} +1 -1
- package/dist/assets/{graph-B59lDyMn.js → graph-BR2KmES9.js} +1 -1
- package/dist/assets/{index-C1ptnCpq.js → index--wgRW_OB.js} +1 -1
- package/dist/assets/index-Ac8vnV3v.js +2 -0
- package/dist/assets/{index-CRAWA8PC.js → index-C-be29ey.js} +1 -1
- package/dist/assets/{index-CSM1Kw8l.js → index-C16fiVhv.js} +1 -1
- package/dist/assets/{index-Ddk-kg22.js → index-D5YruOKD.js} +1 -1
- package/dist/assets/{index-BnhNIjbj.js → index-D5tSeMU7.js} +1 -1
- package/dist/assets/{index-vFEQq-NB.js → index-DuZQ6FqR.js} +1 -1
- package/dist/assets/{index-CO2hZ1s4.js → index-OWzSX2T8.js} +1 -1
- package/dist/assets/{infoDiagram-42DDH7IO-CnO22bw8.js → infoDiagram-42DDH7IO-BQWQjfFX.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-ByNzpScH.js → ishikawaDiagram-UXIWVN3A-Df-KH_Hz.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-yaVaCWPq.js → journeyDiagram-VCZTEJTY-CMbB_4G9.js} +1 -1
- package/dist/assets/{jszip.min-BdVFP3OW.js → jszip.min-B6k2t9Ch.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-RIPR_RQ3.js → kanban-definition-6JOO6SKY-CImdlKN3.js} +1 -1
- package/dist/assets/{layout-0rzel0v_.js → layout-DQl4aw09.js} +1 -1
- package/dist/assets/{linear-sktN0tB6.js → linear-CLD3aPlK.js} +1 -1
- package/dist/assets/mermaid.core-CN35dvKJ.js +7 -0
- package/dist/assets/{min-rspGOkz_.js → min-_D86henJ.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-BsVzZzTz.js → mindmap-definition-QFDTVHPH-2JVcu1nk.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-CsoWsw4f.js → pieDiagram-DEJITSTG-Dik-m5u7.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-CTMKuCAI.js → quadrantDiagram-34T5L4WZ-Bm5l40Jq.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-C67256te.js → requirementDiagram-MS252O5E-Ck9w5rTj.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-X8Y7KnW0.js → sankeyDiagram-XADWPNL6-BbGVCFEf.js} +1 -1
- package/dist/assets/seqResourceLoaders-Cd_TAXTb.js +2 -0
- package/dist/assets/{seqResourceLoaders-B3TSSS6T.css → seqResourceLoaders-CxuD1CYl.css} +1 -1
- package/dist/assets/{sequenceDiagram-FGHM5R23-Cils0TAp.js → sequenceDiagram-FGHM5R23-vjvEdksk.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-DFnohvpG.js → stateDiagram-FHFEXIEX-DTqMoiB2.js} +1 -1
- package/dist/assets/{stateDiagram-v2-QKLJ7IA2-BTX_4o81.js → stateDiagram-v2-QKLJ7IA2-CNbAwnhu.js} +1 -1
- package/dist/assets/{timeline-definition-GMOUNBTQ-DrIYFrzZ.js → timeline-definition-GMOUNBTQ-Cw8O-52d.js} +1 -1
- package/dist/assets/{vendor-antd-D5HmSDSg.js → vendor-antd-DI7JL-mE.js} +1 -1
- package/dist/assets/{vendor-codemirror-C5yuj7ze.js → vendor-codemirror-B9c49dtM.js} +1 -1
- package/dist/assets/{vendor-mdxeditor-BcEooTvb.js → vendor-mdxeditor-B6cpBtIE.js} +2 -2
- package/dist/assets/{vendor-qrcode-d9LCn0-9.js → vendor-qrcode-Dn90-s-u.js} +1 -1
- package/dist/assets/{vendor-virtuoso-K9m4MN6a.js → vendor-virtuoso-CpdSKD_6.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-lRqBYTr9.js → vennDiagram-DHZGUBPP-DCuqymb7.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-e8YfxpOe.js → wardley-RL74JXVD-BJ20wJUU.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-Bxrg774h.js → wardleyDiagram-NUSXRM2D-Vz6US3qt.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-DWhZqhq8.js → xychartDiagram-5P7HB3ND-CzkLKrmi.js} +1 -1
- package/dist/index.html +4 -4
- package/findcc.js +79 -7
- package/package.json +2 -1
- package/server/i18n.js +21 -141
- package/server/interceptor.js +211 -403
- package/server/lib/adapters/dingtalk-adapter.js +16 -2
- package/server/lib/adapters/discord-adapter.js +28 -6
- package/server/lib/adapters/feishu-adapter.js +50 -4
- package/server/lib/adapters/wecom-adapter.js +24 -0
- package/server/lib/async-write-queue.js +12 -1
- package/server/lib/create_system_prompt.js +13 -7
- package/server/lib/delta-reconstructor.js +3 -3
- package/server/lib/error-report.js +45 -0
- package/server/lib/im-bridge-core.js +71 -4
- package/server/lib/im-lock.js +16 -2
- package/server/lib/im-log-watcher.js +68 -8
- package/server/lib/im-process-manager.js +7 -1
- package/server/lib/interceptor-core.js +19 -174
- package/server/lib/log-management.js +214 -247
- package/server/lib/log-stream.js +59 -108
- package/server/lib/log-watcher.js +34 -272
- package/server/lib/log-zip.js +148 -0
- package/server/lib/model-system-prompts.js +10 -6
- package/server/lib/session-boundary.js +266 -0
- package/server/lib/session-pin-store.js +10 -19
- package/server/lib/spawn-model-resolver.js +109 -0
- package/server/lib/stats-worker.js +138 -267
- package/server/lib/system-prompt-files.js +7 -3
- package/server/lib/system-prompt-render.js +79 -0
- package/server/lib/user-prompt-extract.js +250 -0
- package/server/lib/v2/adapter.js +1059 -0
- package/server/lib/v2/blob-store.js +44 -0
- package/server/lib/v2/conversation-store.js +217 -0
- package/server/lib/v2/convert-manager.js +79 -0
- package/server/lib/v2/convert-worker.js +42 -0
- package/server/lib/v2/convert.js +409 -0
- package/server/lib/v2/identity.js +204 -0
- package/server/lib/v2/journal.js +62 -0
- package/server/lib/v2/layout.js +167 -0
- package/server/lib/v2/live-feed.js +451 -0
- package/server/lib/v2/migrate-prompt.js +54 -0
- package/server/lib/v2/replay.js +191 -0
- package/server/lib/v2/session-select.js +222 -0
- package/server/lib/v2/singleflight.js +104 -0
- package/server/lib/v2/v2-writer.js +513 -0
- package/server/lib/v2/verify.js +233 -0
- package/server/pty-manager.js +115 -36
- package/server/routes/dingtalk.js +9 -3
- package/server/routes/events.js +30 -130
- package/server/routes/files-fs.js +4 -2
- package/server/routes/im.js +67 -20
- package/server/routes/logs.js +194 -152
- package/server/routes/preferences.js +0 -3
- package/server/routes/project-meta.js +2 -3
- package/server/routes/session-pin.js +7 -8
- package/server/routes/workspaces.js +44 -9
- package/server/server.js +59 -66
- package/server/workspace-registry.js +24 -4
- package/dist/assets/App-BjHJ9KJ6.css +0 -1
- package/dist/assets/App-CCmR0aki.js +0 -2
- package/dist/assets/Mobile-DCHg97f1.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-BXnyUgsj.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-BXnyUgsj.js +0 -1
- package/dist/assets/clone-md-Mc1-T.js +0 -1
- package/dist/assets/index-CfPXhStV.js +0 -2
- package/dist/assets/mermaid.core-C2Parj1q.js +0 -7
- package/dist/assets/seqResourceLoaders-CAYxnMSH.js +0 -2
- package/server/lib/instance-registry.js +0 -57
- package/server/lib/jsonl-archive.js +0 -180
- package/server/lib/teammate-detect.js +0 -32
- package/server/lib/zip-safety.js +0 -68
|
@@ -179,21 +179,35 @@ const dingtalkAdapter = {
|
|
|
179
179
|
|
|
180
180
|
async connect(cfg, hooks) {
|
|
181
181
|
let client;
|
|
182
|
+
// keepAlive is required for silent-drop detection: without it the SDK never pings, so a
|
|
183
|
+
// network loss with no FIN leaves the socket "open" and `client.connected` true forever.
|
|
184
|
+
// Known SDK quirk: dingtalk-stream arms a new heartbeat interval on every socket 'open'
|
|
185
|
+
// without clearing the previous one, so each reconnect leaks one 8s interval (bounded by
|
|
186
|
+
// reconnect count; disconnect() clears the last). Accepted — detection matters more.
|
|
187
|
+
const opts = { clientId: cfg.appKey, clientSecret: cfg.appSecret, keepAlive: true };
|
|
182
188
|
if (clientFactory) {
|
|
183
|
-
client = clientFactory(
|
|
189
|
+
client = clientFactory(opts);
|
|
184
190
|
if (typeof client.registerCallbackListener === 'function') {
|
|
185
191
|
client.registerCallbackListener('__test__', (res) => hooks.onInbound(normalizeInbound(res), res));
|
|
186
192
|
}
|
|
187
193
|
} else {
|
|
188
194
|
const mod = await import('dingtalk-stream');
|
|
189
195
|
const { DWClient, TOPIC_ROBOT } = mod;
|
|
190
|
-
client = new DWClient(
|
|
196
|
+
client = new DWClient(opts);
|
|
191
197
|
client.registerCallbackListener(TOPIC_ROBOT, (res) => hooks.onInbound(normalizeInbound(res), res));
|
|
192
198
|
}
|
|
193
199
|
await client.connect?.();
|
|
194
200
|
return client;
|
|
195
201
|
},
|
|
196
202
|
|
|
203
|
+
// dingtalk-stream emits no client-level lifecycle events (open/close/error live on an internal
|
|
204
|
+
// ws that is replaced on every reconnect), so the core polls this instead. `userDisconnect` is
|
|
205
|
+
// TS-private but a plain runtime field; it flips true only on manual disconnect().
|
|
206
|
+
connectionProbe(client) {
|
|
207
|
+
if (!client || client.userDisconnect) return 'disconnected';
|
|
208
|
+
return client.connected ? 'connected' : 'reconnecting';
|
|
209
|
+
},
|
|
210
|
+
|
|
197
211
|
async disconnect(client) {
|
|
198
212
|
try { await client?.disconnect?.(); } catch { /* best-effort */ }
|
|
199
213
|
},
|
|
@@ -80,10 +80,21 @@ const discordAdapter = {
|
|
|
80
80
|
});
|
|
81
81
|
ctx.store.client = client;
|
|
82
82
|
// Persistent error listener for the life of the client: a Node EventEmitter that emits 'error'
|
|
83
|
-
// with NO listener throws and crashes the process.
|
|
84
|
-
// consumed
|
|
85
|
-
//
|
|
86
|
-
|
|
83
|
+
// with NO listener throws and crashes the process. NOTE: the connect-window once('error') below
|
|
84
|
+
// is NOT consumed by a successful ClientReady resolve — it survives and must therefore be a
|
|
85
|
+
// settled-guarded no-op post-connect (see below). Record errors as lastError here (state flips
|
|
86
|
+
// come from the shard lifecycle events below; the gateway auto-reconnects).
|
|
87
|
+
client.on('error', (e) => hooks.onConnectionChange?.(null, e));
|
|
88
|
+
// Shard lifecycle → core tri-state. Single unsharded gateway = one shard, so no per-shard map.
|
|
89
|
+
// Events?.X ?? 'literal' keeps test fakes (whose Events map lacks shard constants) working.
|
|
90
|
+
client.on(Events?.ShardReconnecting ?? 'shardReconnecting', () => hooks.onConnectionChange?.('reconnecting'));
|
|
91
|
+
client.on(Events?.ShardResume ?? 'shardResume', () => hooks.onConnectionChange?.('connected'));
|
|
92
|
+
client.on(Events?.ShardReady ?? 'shardReady', () => hooks.onConnectionChange?.('connected'));
|
|
93
|
+
client.on(Events?.ShardDisconnect ?? 'shardDisconnect', (ev) =>
|
|
94
|
+
hooks.onConnectionChange?.('disconnected', new Error(`gateway closed (code ${ev?.code ?? 'unknown'})`)));
|
|
95
|
+
client.on(Events?.Invalidated ?? 'invalidated', () =>
|
|
96
|
+
hooks.onConnectionChange?.('disconnected', new Error('session invalidated')));
|
|
97
|
+
client.on(Events?.ShardError ?? 'shardError', (e) => hooks.onConnectionChange?.(null, e));
|
|
87
98
|
client.on(Events.MessageCreate, (message) => {
|
|
88
99
|
// Loop-guard: ignore the bot's own messages (Discord redelivers our send as a new event) and
|
|
89
100
|
// all other bots. author.bot covers self; the id check is belt-and-suspenders.
|
|
@@ -98,8 +109,19 @@ const discordAdapter = {
|
|
|
98
109
|
const timer = setTimeout(() => { try { client.destroy(); } catch { /* best-effort */ } finish(reject, new Error('connect timeout')); }, CONNECT_PROBE_MS);
|
|
99
110
|
if (typeof timer.unref === 'function') timer.unref();
|
|
100
111
|
client.once(Events.ClientReady, () => finish(resolve, client));
|
|
101
|
-
|
|
102
|
-
|
|
112
|
+
// Guard the destroy behind `settled`: this once-listener is NOT consumed by ClientReady, so
|
|
113
|
+
// an unguarded destroy would kill the LIVE client on the first post-connect 'error' event
|
|
114
|
+
// (freezing the bridge with no reconnect — the exact stale-status bug this adapter fixes).
|
|
115
|
+
client.once('error', (e) => {
|
|
116
|
+
if (settled) return; // post-connect errors are handled by the persistent listener above
|
|
117
|
+
try { client.destroy(); } catch { /* best-effort */ }
|
|
118
|
+
finish(reject, new Error(String(e?.message || e)));
|
|
119
|
+
});
|
|
120
|
+
client.login(cfg.botToken).catch((e) => {
|
|
121
|
+
if (settled) return;
|
|
122
|
+
try { client.destroy(); } catch { /* best-effort */ }
|
|
123
|
+
finish(reject, new Error(String(e?.message || e)));
|
|
124
|
+
});
|
|
103
125
|
});
|
|
104
126
|
},
|
|
105
127
|
|
|
@@ -18,6 +18,10 @@ import { registerAdapter } from '../im-bridge-core.js';
|
|
|
18
18
|
let sdkFactory = null;
|
|
19
19
|
export function __setClientFactory(fn) { sdkFactory = fn; }
|
|
20
20
|
|
|
21
|
+
// Internal connect guard (< core CONNECT_TIMEOUT_MS): bounds the onReady wait on hook-capable SDK
|
|
22
|
+
// builds so a misconfigured app fails with lastError instead of leaking a background retry loop.
|
|
23
|
+
const CONNECT_PROBE_MS = 12_000;
|
|
24
|
+
|
|
21
25
|
const TOKEN_PATH = '/open-apis/auth/v3/tenant_access_token/internal';
|
|
22
26
|
function tokenHost(region) {
|
|
23
27
|
return region === 'lark' ? 'https://open.larksuite.com' : 'https://open.feishu.cn';
|
|
@@ -90,17 +94,59 @@ const feishuAdapter = {
|
|
|
90
94
|
const base = { appId: cfg.appId, appSecret: cfg.appSecret, domain };
|
|
91
95
|
// Outbound client (auto-manages the tenant_access_token cache).
|
|
92
96
|
ctx.store.sendClient = new Lark.Client(base);
|
|
93
|
-
|
|
94
|
-
|
|
97
|
+
let settled = false;
|
|
98
|
+
let resolveReady, rejectReady;
|
|
99
|
+
const ready = new Promise((resolve, reject) => { resolveReady = resolve; rejectReady = reject; });
|
|
100
|
+
ready.catch(() => {}); // detached guard: a pre-await rejection must not raise unhandledRejection
|
|
101
|
+
// Inbound long-connection event client. wsConfig keys per SDK build: 1.66.0 reads ONLY the
|
|
102
|
+
// lowercase `pingTimeout` (seconds) — it arms the pong/liveness watchdog that terminates a
|
|
103
|
+
// zombie socket after a SILENT network drop (NAT idle, sleep, no FIN) and triggers reconnect;
|
|
104
|
+
// without it such drops are never detected. The capitalized PingInterval/PingTimeout keys are
|
|
105
|
+
// kept for older builds that read them (1.66.0 ignores unknown keys, so both are safe).
|
|
95
106
|
const ws = new Lark.WSClient({
|
|
96
107
|
...base,
|
|
97
108
|
loggerLevel: Lark.LoggerLevel ? Lark.LoggerLevel.warn : undefined, // warn: avoid noisy info-level stdout
|
|
98
|
-
wsConfig: { PingInterval: 30, PingTimeout: 5 },
|
|
109
|
+
wsConfig: { PingInterval: 30, PingTimeout: 5, pingTimeout: 5 },
|
|
110
|
+
// Lifecycle hooks ship with newer SDK builds (present in the installed 1.66.0); an older
|
|
111
|
+
// build silently ignores unknown ctor options, so wiring them is always safe.
|
|
112
|
+
onReady: () => {
|
|
113
|
+
if (settled) { hooks.onConnectionChange?.('connected'); return; }
|
|
114
|
+
settled = true; resolveReady();
|
|
115
|
+
},
|
|
116
|
+
onReconnecting: () => hooks.onConnectionChange?.('reconnecting'),
|
|
117
|
+
onReconnected: () => hooks.onConnectionChange?.('connected'),
|
|
118
|
+
onError: (err) => {
|
|
119
|
+
if (settled) { hooks.onConnectionChange?.('disconnected', err); return; }
|
|
120
|
+
settled = true; rejectReady(err instanceof Error ? err : new Error(String(err?.message || err)));
|
|
121
|
+
},
|
|
99
122
|
});
|
|
100
123
|
const dispatcher = new Lark.EventDispatcher({}).register({
|
|
101
124
|
'im.message.receive_v1': async (data) => hooks.onInbound(normalizeInbound(data), null),
|
|
102
125
|
});
|
|
103
|
-
|
|
126
|
+
if (typeof ws.getConnectionStatus === 'function') {
|
|
127
|
+
// Hook-capable build (feature-detected): start() resolves before the handshake completes,
|
|
128
|
+
// so gate on onReady/onError; bound with an internal timeout below the core's connect race.
|
|
129
|
+
const timer = setTimeout(() => {
|
|
130
|
+
if (settled) return;
|
|
131
|
+
settled = true;
|
|
132
|
+
rejectReady(new Error('connect timeout'));
|
|
133
|
+
}, CONNECT_PROBE_MS);
|
|
134
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
135
|
+
try {
|
|
136
|
+
await ws.start({ eventDispatcher: dispatcher });
|
|
137
|
+
await ready;
|
|
138
|
+
} catch (e) {
|
|
139
|
+
// Stop the SDK's background retry loop so a failed connect doesn't leak.
|
|
140
|
+
try { await ws.close?.({}); } catch { /* best-effort */ }
|
|
141
|
+
throw e;
|
|
142
|
+
} finally {
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
// Older SDK (and legacy test fakes): original behavior — start() is trusted, no
|
|
147
|
+
// lifecycle hooks fire, connection state stays start/stop-driven.
|
|
148
|
+
await ws.start({ eventDispatcher: dispatcher });
|
|
149
|
+
}
|
|
104
150
|
return ws;
|
|
105
151
|
},
|
|
106
152
|
|
|
@@ -75,6 +75,30 @@ const wecomAdapter = {
|
|
|
75
75
|
const client = new WSClient({ botId: cfg.botId, secret: cfg.secret });
|
|
76
76
|
ctx.store.client = client; // single client for inbound + outbound (sendOne reads it)
|
|
77
77
|
client.on('message.text', (frame) => hooks.onInbound(normalizeInbound(frame), null));
|
|
78
|
+
// Persistent lifecycle listeners → core tri-state. Registered up front (pre-connect emits are
|
|
79
|
+
// ignored by the core while not running); removed by disconnect()'s removeAllListeners.
|
|
80
|
+
// NOTE: one connection per bot — running testConnection against the SAME bot kicks the live
|
|
81
|
+
// bridge via disconnected_event, which the SDK treats as TERMINAL (isManualClose, no
|
|
82
|
+
// reconnect). The status will correctly show Disconnected until the bridge is restarted;
|
|
83
|
+
// auto-restart-after-test is a deliberate non-goal for now (backlog).
|
|
84
|
+
let kicked = false;
|
|
85
|
+
client.on('event.disconnected_event', () => {
|
|
86
|
+
// Server kicked this connection (a new one took over): the SDK sets isManualClose and will
|
|
87
|
+
// NOT reconnect, so this is terminal; the 'disconnected' emit that follows must not
|
|
88
|
+
// downgrade it to reconnecting.
|
|
89
|
+
kicked = true;
|
|
90
|
+
hooks.onConnectionChange?.('disconnected', new Error('kicked: new connection established'));
|
|
91
|
+
});
|
|
92
|
+
client.on('disconnected', (reason) => {
|
|
93
|
+
if (!kicked) hooks.onConnectionChange?.('reconnecting', reason ? new Error(String(reason)) : null);
|
|
94
|
+
});
|
|
95
|
+
client.on('reconnecting', () => hooks.onConnectionChange?.('reconnecting'));
|
|
96
|
+
client.on('authenticated', () => { kicked = false; hooks.onConnectionChange?.('connected'); });
|
|
97
|
+
client.on('error', (e) => {
|
|
98
|
+
const terminal = e?.name === 'WSReconnectExhaustedError' || e?.name === 'WSAuthFailureError'
|
|
99
|
+
|| e?.code === 'WS_RECONNECT_EXHAUSTED';
|
|
100
|
+
hooks.onConnectionChange?.(terminal ? 'disconnected' : null, e);
|
|
101
|
+
});
|
|
78
102
|
// connect() is synchronous; resolve once the WS handshake authenticates so the core's
|
|
79
103
|
// `connected` flag and CONNECT_TIMEOUT_MS race are meaningful (bad creds → lastError, not a
|
|
80
104
|
// false "connected"). Reject (and tear down) on error or an internal timeout.
|
|
@@ -36,8 +36,19 @@ export class AsyncWriteQueue {
|
|
|
36
36
|
get pendingBytes() { return this._pendingBytes; }
|
|
37
37
|
|
|
38
38
|
append(data, onDone) {
|
|
39
|
+
return this.appendTo(this._getPath(), data, onDone);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Explicit-path variant (wire-v2 S2): same queue, same ordering/drain
|
|
44
|
+
* machinery, caller supplies the target file. Within one drain batch, path
|
|
45
|
+
* groups are written in first-enqueue order, so a caller enqueuing
|
|
46
|
+
* conversation lines before the journal line keeps that relative order on
|
|
47
|
+
* disk (WIRE_FORMAT_V2.md §1.3 write-order protocol). Empty/falsy path stays
|
|
48
|
+
* a silent no-op — workspace mode and leaderless teammates rely on it.
|
|
49
|
+
*/
|
|
50
|
+
appendTo(path, data, onDone) {
|
|
39
51
|
if (this._closed) return;
|
|
40
|
-
const path = this._getPath();
|
|
41
52
|
if (!path) {
|
|
42
53
|
if (onDone) try { onDone(); } catch {}
|
|
43
54
|
return;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// node server/lib/create_system_prompt.js deepseek-v4-pro # a named preset
|
|
8
8
|
// node server/lib/create_system_prompt.js --list # list presets
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { spawnSync } from 'node:child_process'
|
|
11
11
|
import { readFileSync, statSync } from 'node:fs'
|
|
12
12
|
import os from 'node:os'
|
|
13
13
|
import { delimiter, join, sep } from 'node:path'
|
|
@@ -54,13 +54,16 @@ function envString(name) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
function commandOutput(command, args, cwd) {
|
|
57
|
-
return stringOrEmpty(() =>
|
|
58
|
-
|
|
57
|
+
return stringOrEmpty(() => {
|
|
58
|
+
const result = spawnSync(command, args, {
|
|
59
59
|
cwd,
|
|
60
60
|
encoding: 'utf8',
|
|
61
61
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
timeout: 15000, // 15s hard cap: hanging git (NFS / huge repo / broken index) must not block spawn
|
|
63
|
+
})
|
|
64
|
+
if (result.error || result.status !== 0) return ''
|
|
65
|
+
return result.stdout.trim()
|
|
66
|
+
})
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
function firstNonEmpty(...values) {
|
|
@@ -130,8 +133,11 @@ function resolveMemory(home, cwd) {
|
|
|
130
133
|
return { dir, index, enabled: enabled ? 'true' : 'false' }
|
|
131
134
|
}
|
|
132
135
|
|
|
133
|
-
export function createSystemPromptVariables(overrides = {}) {
|
|
134
|
-
|
|
136
|
+
export function createSystemPromptVariables(overrides = {}, opts = {}) {
|
|
137
|
+
// opts.cwd: resolve cwd-dependent variables (environment.cwd, git.*, memory.dir) against a
|
|
138
|
+
// caller-supplied directory instead of process.cwd() — the spawn-time renderer passes the
|
|
139
|
+
// workspace being launched, which is not necessarily where the ccv server itself runs.
|
|
140
|
+
const cwd = (typeof opts.cwd === 'string' && opts.cwd) ? opts.cwd : stringOrEmpty(() => process.cwd())
|
|
135
141
|
const now = new Date()
|
|
136
142
|
const timeZone = stringOrEmpty(
|
|
137
143
|
() => Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
@@ -88,7 +88,7 @@ function _seqGuardCheck(entry, st) {
|
|
|
88
88
|
/**
|
|
89
89
|
* 标记乱序条目并尽量就地补偿:accumulated 已包含更新的真值时,用其前缀
|
|
90
90
|
* (= 截至该条目声称长度的最新内容)回填 body.messages,避免裸 delta 切片
|
|
91
|
-
*
|
|
91
|
+
* 残留在请求详情面板中。
|
|
92
92
|
* 返回 true 表示已补偿(messages 为一致全量),false 表示需后续 checkpoint 补偿。
|
|
93
93
|
*/
|
|
94
94
|
function _markStaleEntry(entry, accumulated) {
|
|
@@ -142,7 +142,7 @@ function _integrityCheck(entry, accumulated, st) {
|
|
|
142
142
|
* 2. 真值前缀 —— stale 就地补偿、超长 slice 修复(_staleReorder 置位,merge 跳过);
|
|
143
143
|
* 3. 裸 delta 切片待补偿 —— stale 就地补偿失败 / poisoned 冻结(_staleReorder 或
|
|
144
144
|
* _reconstructBroken 置位):由第二遍 checkpoint 补偿回填,无第二遍的路径靠
|
|
145
|
-
* isMergeBlockedEntry 阻断 merge
|
|
145
|
+
* isMergeBlockedEntry 阻断 merge 兜底。
|
|
146
146
|
*
|
|
147
147
|
* @param {object} entry - 单条日志条目(原地修改)
|
|
148
148
|
* @param {{accumulated: Array, intState: {baselineSeen: boolean, poisoned: boolean},
|
|
@@ -239,7 +239,7 @@ export function reconstructEntries(entries) {
|
|
|
239
239
|
|
|
240
240
|
/**
|
|
241
241
|
* 用候选全量条目(checkpoint/旧格式,排除 teammate)补偿一条断裂条目。
|
|
242
|
-
* 内容已是真值前缀,清除标记让批量 merge
|
|
242
|
+
* 内容已是真值前缀,清除标记让批量 merge 正常消费。
|
|
243
243
|
* @returns {boolean} true = 已补偿
|
|
244
244
|
*/
|
|
245
245
|
function _tryRepairFromCandidate(brokenEntry, expectedCount, candidate) {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// CLIENT-SAFE: no node deps. Imported by src/ — do not add fs/process/node: imports.
|
|
2
|
+
/**
|
|
3
|
+
* Error-reporting convention for swallowed catch blocks.
|
|
4
|
+
*
|
|
5
|
+
* Canonical home since wire-v2 S2 (moved verbatim from src/utils/errorReport.js,
|
|
6
|
+
* which remains as a re-export shell): server/lib/v2/* writers follow the same
|
|
7
|
+
* convention, and src/ is not shipped in the npm package.
|
|
8
|
+
*
|
|
9
|
+
* The rule (see CLAUDE.md): a `catch` whose failure has diagnostic value —
|
|
10
|
+
* anything where a throw means lost data or broken UI (SSE/WS event parsing,
|
|
11
|
+
* entry ingest, state updates, fetch response handling) — must call
|
|
12
|
+
* `reportSwallowed(tag, err)` instead of swallowing silently. Purely-benign
|
|
13
|
+
* best-effort guards (localStorage get/set, tabBridge IPC, revokeObjectURL,
|
|
14
|
+
* listener-cleanup `off()`) stay bare `catch {}`.
|
|
15
|
+
*
|
|
16
|
+
* Tag grammar: `<channel>.<event-name-verbatim>`, e.g. `sse.load_chunk`,
|
|
17
|
+
* `sse.stream-progress`, `ws.terminal-msg`. Keep the event name exactly as it
|
|
18
|
+
* appears in the code/protocol so a grep for the tag round-trips to the site.
|
|
19
|
+
* All reports share the greppable `[ccv:` console prefix.
|
|
20
|
+
*
|
|
21
|
+
* Dedup: identical failures usually repeat (e.g. every SSE event of a changed
|
|
22
|
+
* payload shape fails the same way), so each tag logs at most
|
|
23
|
+
* MAX_REPORTS_PER_TAG occurrences per session, then one suppression notice,
|
|
24
|
+
* then goes silent. Counter-based on purpose — no Date/timers, so this module
|
|
25
|
+
* is statically loadable and deterministic under `node --test`.
|
|
26
|
+
*/
|
|
27
|
+
export const MAX_REPORTS_PER_TAG = 5;
|
|
28
|
+
|
|
29
|
+
const _counts = new Map();
|
|
30
|
+
|
|
31
|
+
export function reportSwallowed(tag, err, extra) {
|
|
32
|
+
const n = (_counts.get(tag) || 0) + 1;
|
|
33
|
+
_counts.set(tag, n);
|
|
34
|
+
if (n > MAX_REPORTS_PER_TAG + 1) return;
|
|
35
|
+
if (n === MAX_REPORTS_PER_TAG + 1) {
|
|
36
|
+
console.warn(`[ccv:${tag}] further occurrences suppressed`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (extra !== undefined) console.warn(`[ccv:${tag}]`, err, extra);
|
|
40
|
+
else console.warn(`[ccv:${tag}]`, err);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function _resetForTest() {
|
|
44
|
+
_counts.clear();
|
|
45
|
+
}
|
|
@@ -43,6 +43,10 @@ const STREAM_MIN_DELTA = 20; // 累计增长达 20 字才推一帧
|
|
|
43
43
|
const STREAM_MAX_FRAMES = 25; // 每回合中途流式帧硬上限;超限停推,finalize 仍落全文
|
|
44
44
|
const STREAM_MAX_CHARS = 20_000; // 卡片内容字符上限,超出截断
|
|
45
45
|
|
|
46
|
+
// Poll interval for adapters that expose connectionProbe() instead of lifecycle events
|
|
47
|
+
// (dingtalk-stream replaces its internal ws on every reconnect, so no listener survives).
|
|
48
|
+
let CONN_POLL_MS = 5_000;
|
|
49
|
+
|
|
46
50
|
// ─── registry + core-global single-flight ───
|
|
47
51
|
const instances = new Map(); // platformId → instance
|
|
48
52
|
let activeInjection = null; // { platformId, since, target } — the one in-flight turn
|
|
@@ -61,6 +65,9 @@ function newInstance(adapter) {
|
|
|
61
65
|
client: null,
|
|
62
66
|
running: false,
|
|
63
67
|
connected: false,
|
|
68
|
+
connectionState: 'disconnected', // 'connected' | 'reconnecting' | 'disconnected'; invariant: connected === (connectionState === 'connected')
|
|
69
|
+
startGen: 0, // generation counter; stale adapter listeners from a previous start are ignored
|
|
70
|
+
connPollTimer: null, // fallback poll timer for adapters exposing connectionProbe()
|
|
64
71
|
lastError: null,
|
|
65
72
|
bridgeDeps: null,
|
|
66
73
|
boundConversation: null,
|
|
@@ -88,9 +95,15 @@ function newInstance(adapter) {
|
|
|
88
95
|
* @property {Object} [capabilities] Informational flags ({inboundAck, sdkManagesToken}); not read by the core.
|
|
89
96
|
* @property {(cfg:Object)=>boolean} hasCreds True when cfg carries enough creds to connect.
|
|
90
97
|
* @property {(cfg:Object)=>Object} [statusFields] Extra non-secret status fields for the admin API.
|
|
91
|
-
* @property {(cfg:Object, hooks:{onInbound:(normalized:Object, ackCtx:*)=>void}, ctx:{fetch,store})=>Promise<*>} connect
|
|
98
|
+
* @property {(cfg:Object, hooks:{onInbound:(normalized:Object, ackCtx:*)=>void, onConnectionChange?:(state:?string, err?:*)=>void}, ctx:{fetch,store})=>Promise<*>} connect
|
|
92
99
|
* Open the long connection; return the live client. Call hooks.onInbound(normalized, ackCtx)
|
|
93
100
|
* per inbound message, where normalized = {text, conversationId, senderId, msgId, target}.
|
|
101
|
+
* Optionally call hooks.onConnectionChange?.(state, err) on transport lifecycle changes,
|
|
102
|
+
* where state ∈ 'connected'|'reconnecting'|'disconnected', or null to record err only.
|
|
103
|
+
* Always invoke it with optional chaining — callers (tests) may pass onInbound alone.
|
|
104
|
+
* @property {(client:*)=>('connected'|'reconnecting'|'disconnected')} [connectionProbe]
|
|
105
|
+
* For SDKs without usable lifecycle events: derive the live connection state from the
|
|
106
|
+
* client's fields. When present, the core polls it every CONN_POLL_MS while running.
|
|
94
107
|
* @property {(client:*, ctx:{fetch,store})=>Promise<void>} [disconnect] Tear down the client (best-effort).
|
|
95
108
|
* @property {(ackCtx:*, client:*)=>void} [ack] Ack an inbound msg (platforms that redeliver if not acked).
|
|
96
109
|
* @property {(cfg:Object, target:Object, content:string, ctx:{fetch,store})=>Promise<void>} sendOne Send one chunk.
|
|
@@ -320,6 +333,26 @@ function armActiveInjection(inst, target, since) {
|
|
|
320
333
|
}
|
|
321
334
|
}
|
|
322
335
|
|
|
336
|
+
/**
|
|
337
|
+
* Transition the tri-state connection status. `gen` binds the caller to one startBridge lifetime,
|
|
338
|
+
* so a stale adapter listener firing after stopBridge/reloadBridge can never flip the state.
|
|
339
|
+
* state === null records the error only; identical consecutive states are no-ops (collapses
|
|
340
|
+
* SDK error/reconnect storms into single audited transitions).
|
|
341
|
+
*/
|
|
342
|
+
function setConnectionState(inst, gen, state, err) {
|
|
343
|
+
if (gen !== inst.startGen || !inst.running) return;
|
|
344
|
+
const errStr = err != null ? String(err?.message || err) : null;
|
|
345
|
+
if (errStr) inst.lastError = errStr;
|
|
346
|
+
if (state == null || state === inst.connectionState) return;
|
|
347
|
+
inst.connectionState = state;
|
|
348
|
+
inst.connected = state === 'connected';
|
|
349
|
+
// Recovery clears the retained disconnect cause — the UI ranks lastError above connected, so
|
|
350
|
+
// keeping it would leave the status stuck on "Error" after every transient drop. Deduped
|
|
351
|
+
// repeats and null-state calls don't clear, preserving send-failure diagnostics while healthy.
|
|
352
|
+
if (state === 'connected' && !errStr) inst.lastError = null;
|
|
353
|
+
audit(inst, 'connection', { state, ...(errStr ? { error: errStr } : {}) });
|
|
354
|
+
}
|
|
355
|
+
|
|
323
356
|
// ─── small helpers ───
|
|
324
357
|
function audit(inst, event, data) {
|
|
325
358
|
try {
|
|
@@ -711,21 +744,45 @@ export async function startBridge(id, deps) {
|
|
|
711
744
|
if (!inst.bridgeDeps || typeof inst.bridgeDeps.getConfig !== 'function') { audit(inst, 'start-skipped', { reason: 'no-deps' }); return; }
|
|
712
745
|
const cfg = inst.bridgeDeps.getConfig();
|
|
713
746
|
if (!cfg || !cfg.enabled || !inst.adapter.hasCreds(cfg)) return; // off / incomplete → no-op
|
|
747
|
+
// Bump AFTER the early-return guards: startBridge on an already-running instance is a legal
|
|
748
|
+
// no-op, and bumping there would orphan the live adapter's listeners (state frozen forever).
|
|
749
|
+
const gen = ++inst.startGen;
|
|
714
750
|
try {
|
|
715
|
-
const hooks = {
|
|
751
|
+
const hooks = {
|
|
752
|
+
onInbound: (normalized, ackCtx) => handleInbound(inst, normalized, ackCtx),
|
|
753
|
+
onConnectionChange: (state, err) => setConnectionState(inst, gen, state, err),
|
|
754
|
+
};
|
|
716
755
|
// Bound the connect so a hung adapter (e.g. Feishu WSClient.start() on a misconfigured app)
|
|
717
756
|
// becomes a lastError instead of blocking the whole startup chain.
|
|
718
|
-
|
|
757
|
+
const client = await Promise.race([
|
|
719
758
|
inst.adapter.connect(cfg, hooks, ctxFor(inst)),
|
|
720
759
|
new Promise((_, reject) => { const tm = setTimeout(() => reject(new Error('connect timeout')), CONNECT_TIMEOUT_MS); if (typeof tm.unref === 'function') tm.unref(); }),
|
|
721
760
|
]);
|
|
761
|
+
if (gen !== inst.startGen) {
|
|
762
|
+
// stopBridge / reload ran while the connect was in flight: this start lost the race. Don't
|
|
763
|
+
// resurrect the instance — tear down the late-arriving client so it doesn't leak.
|
|
764
|
+
try { await inst.adapter.disconnect?.(client, ctxFor(inst)); } catch { /* best-effort */ }
|
|
765
|
+
audit(inst, 'start-superseded', {});
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
inst.client = client;
|
|
722
769
|
inst.running = true;
|
|
723
770
|
inst.connected = true;
|
|
771
|
+
inst.connectionState = 'connected';
|
|
724
772
|
inst.lastError = null;
|
|
773
|
+
if (typeof inst.adapter.connectionProbe === 'function') {
|
|
774
|
+
inst.connPollTimer = setInterval(() => {
|
|
775
|
+
try { setConnectionState(inst, gen, inst.adapter.connectionProbe(inst.client), null); }
|
|
776
|
+
catch { /* best-effort: a probe throw must not kill the interval */ }
|
|
777
|
+
}, CONN_POLL_MS);
|
|
778
|
+
if (typeof inst.connPollTimer.unref === 'function') inst.connPollTimer.unref();
|
|
779
|
+
}
|
|
725
780
|
audit(inst, 'start', inst.adapter.statusFields ? inst.adapter.statusFields(cfg) : {});
|
|
726
781
|
} catch (e) {
|
|
782
|
+
if (gen !== inst.startGen) return; // superseded by stop/reload — don't overwrite their state
|
|
727
783
|
inst.lastError = String(e?.message || e);
|
|
728
784
|
inst.connected = false;
|
|
785
|
+
inst.connectionState = 'disconnected';
|
|
729
786
|
audit(inst, 'start-error', { error: inst.lastError });
|
|
730
787
|
}
|
|
731
788
|
}
|
|
@@ -733,6 +790,9 @@ export async function startBridge(id, deps) {
|
|
|
733
790
|
export async function stopBridge(id) {
|
|
734
791
|
const inst = instances.get(id);
|
|
735
792
|
if (!inst) return;
|
|
793
|
+
// Invalidate connection hooks up front so anything the adapter teardown fires can't flip state.
|
|
794
|
+
inst.startGen++;
|
|
795
|
+
if (inst.connPollTimer) { clearInterval(inst.connPollTimer); inst.connPollTimer = null; }
|
|
736
796
|
if (inst.ackCardPromise && activeInjection?.platformId === id) {
|
|
737
797
|
await finalizeAckCard(inst, activeInjection.target, tr(inst, 'noSession'), 'error');
|
|
738
798
|
} else {
|
|
@@ -743,6 +803,7 @@ export async function stopBridge(id) {
|
|
|
743
803
|
inst.client = null;
|
|
744
804
|
inst.running = false;
|
|
745
805
|
inst.connected = false;
|
|
806
|
+
inst.connectionState = 'disconnected';
|
|
746
807
|
inst.boundConversation = null;
|
|
747
808
|
if (activeInjection && activeInjection.platformId === id) clearActiveInjection();
|
|
748
809
|
inst.queue.length = 0;
|
|
@@ -760,10 +821,11 @@ export function isBridgeRunning(id) {
|
|
|
760
821
|
|
|
761
822
|
export function getBridgeStatus(id) {
|
|
762
823
|
const inst = instances.get(id);
|
|
763
|
-
if (!inst) return { running: false, connected: false, lastError: null, boundConversationId: null };
|
|
824
|
+
if (!inst) return { running: false, connected: false, connectionState: 'disconnected', lastError: null, boundConversationId: null };
|
|
764
825
|
const base = {
|
|
765
826
|
running: inst.running,
|
|
766
827
|
connected: inst.connected,
|
|
828
|
+
connectionState: inst.connectionState,
|
|
767
829
|
lastError: inst.lastError,
|
|
768
830
|
boundConversationId: inst.boundConversation?.conversationId || null,
|
|
769
831
|
};
|
|
@@ -799,6 +861,8 @@ export async function stopAll() {
|
|
|
799
861
|
// ─── test seams ───
|
|
800
862
|
export function __setStreamTickMsForTests(ms) { STREAM_TICK_MS = (ms == null ? 300 : ms); }
|
|
801
863
|
|
|
864
|
+
export function __setConnPollMsForTests(ms) { CONN_POLL_MS = (ms == null ? 5_000 : ms); }
|
|
865
|
+
|
|
802
866
|
export function __setMaxQueueForTests(id, n) {
|
|
803
867
|
const inst = instances.get(id);
|
|
804
868
|
if (inst) inst.maxQueueOverride = n;
|
|
@@ -809,6 +873,9 @@ export function __resetForTests(id) {
|
|
|
809
873
|
const inst = instances.get(id);
|
|
810
874
|
if (!inst) return;
|
|
811
875
|
inst.client = null; inst.running = false; inst.connected = false; inst.lastError = null;
|
|
876
|
+
inst.connectionState = 'disconnected';
|
|
877
|
+
inst.startGen++; // orphan any listeners still bound to a previous start
|
|
878
|
+
if (inst.connPollTimer) { clearInterval(inst.connPollTimer); inst.connPollTimer = null; }
|
|
812
879
|
inst.bridgeDeps = null; inst.boundConversation = null; inst.lastRepliedTurnTs = null;
|
|
813
880
|
inst.maxQueueOverride = null;
|
|
814
881
|
inst.seenMsgIds.length = 0; inst.queue.length = 0; inst.sendTimes.length = 0;
|
package/server/lib/im-lock.js
CHANGED
|
@@ -135,7 +135,15 @@ export function defaultProbe(id, port, { timeoutMs = 400 } = {}) {
|
|
|
135
135
|
const j = JSON.parse(body);
|
|
136
136
|
// 身份:能对「该 id」返回 IM status 形状的 loopback 服务即视为我们的 worker
|
|
137
137
|
if (j && (j.connection || typeof j.enabled === 'boolean')) {
|
|
138
|
-
finish({
|
|
138
|
+
finish({
|
|
139
|
+
ok: true,
|
|
140
|
+
connected: !!(j.connection && j.connection.connected),
|
|
141
|
+
// Old workers (pre-tri-state builds) omit connectionState → derive from connected.
|
|
142
|
+
connectionState: j.connection?.connectionState
|
|
143
|
+
?? (j.connection?.connected ? 'connected' : 'disconnected'),
|
|
144
|
+
lastError: j.connection?.lastError ?? null,
|
|
145
|
+
pid: j.pid,
|
|
146
|
+
});
|
|
139
147
|
} else finish(null);
|
|
140
148
|
} catch { finish(null); }
|
|
141
149
|
});
|
|
@@ -178,7 +186,13 @@ export async function getImLiveness(id, opts = {}) {
|
|
|
178
186
|
|
|
179
187
|
const res = await probe(id, lock.port);
|
|
180
188
|
if (res && res.ok && (res.pid == null || res.pid === lock.pid)) {
|
|
181
|
-
return {
|
|
189
|
+
return {
|
|
190
|
+
state: 'ready',
|
|
191
|
+
lock,
|
|
192
|
+
connected: !!res.connected,
|
|
193
|
+
connectionState: res.connectionState ?? (res.connected ? 'connected' : 'disconnected'),
|
|
194
|
+
lastError: res.lastError ?? null,
|
|
195
|
+
};
|
|
182
196
|
}
|
|
183
197
|
return { state: pidAlive(lock.pid) ? 'hung' : 'dead', lock };
|
|
184
198
|
}
|