cc-viewer 1.6.324 → 1.6.326
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/assets/{App-DxhvxOv5.js → App-DgnBYI0A.js} +1 -1
- package/dist/assets/{MdxEditorPanel-Cvce-OZ1.js → MdxEditorPanel-DvtYTYBR.js} +1 -1
- package/dist/assets/{Mobile-eQkx6wDh.js → Mobile-OJJ-w638.js} +1 -1
- package/dist/assets/index-CqCAViSO.js +2 -0
- package/dist/assets/seqResourceLoaders-C2KXQgrD.js +2 -0
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/server/interceptor.js +49 -11
- package/server/lib/adapters/dingtalk-adapter.js +141 -38
- package/server/lib/adapters/feishu-adapter.js +88 -4
- package/server/lib/adapters/wecom-adapter.js +56 -1
- package/server/lib/im-bridge-core.js +134 -0
- package/server/lib/im-config.js +13 -3
- package/server/routes/dingtalk.js +2 -0
- package/server/server.js +5 -1
- package/dist/assets/index-Cw_igOCh.js +0 -2
- package/dist/assets/seqResourceLoaders-sKqNZgOI.js +0 -2
|
@@ -35,6 +35,13 @@ const IDLE_POLL_INTERVAL_MS = 5_000; // check every 5s if streaming stopped
|
|
|
35
35
|
const IDLE_POLL_THRESHOLD = 3; // 3 consecutive idle ticks (15s) → synthetic turn_end
|
|
36
36
|
const CONNECT_TIMEOUT_MS = 15_000; // bound adapter.connect() so a hung start can't block others
|
|
37
37
|
const STOP_WORDS = new Set(['/stop', 'stop', '停止', 'esc', '/esc']);
|
|
38
|
+
// ─── 逐字流式(钉钉 AI 卡片)推送节流:每帧计费,故节流 + 帧上限 ───
|
|
39
|
+
// 推送轮询间隔。300ms:数秒级回复约展示 10 帧打字机效果;过大→短/秒回回复在首 tick 前就 finalize、
|
|
40
|
+
// 看不到流式;过小→帧数与计费上升,且建卡本身有网络往返、再低收益有限。测试经 __setStreamTickMsForTests 调低。
|
|
41
|
+
let STREAM_TICK_MS = 300;
|
|
42
|
+
const STREAM_MIN_DELTA = 20; // 累计增长达 20 字才推一帧(对齐钉钉官方节流,省调用)
|
|
43
|
+
const STREAM_MAX_FRAMES = 25; // 每回合中途流式帧硬上限;超限停推,finalize 仍落全文
|
|
44
|
+
const STREAM_MAX_CHARS = 20_000; // 卡片内容字符上限,超出截断
|
|
38
45
|
|
|
39
46
|
// ─── registry + core-global single-flight ───
|
|
40
47
|
const instances = new Map(); // platformId → instance
|
|
@@ -64,6 +71,7 @@ function newInstance(adapter) {
|
|
|
64
71
|
sendTimes: [],
|
|
65
72
|
store: {}, // adapter scratch (token cache, send client)
|
|
66
73
|
ackCardPromise: null, // Promise<handle|null> for the in-flight ack card
|
|
74
|
+
streamTimer: null, // 逐字流式推送定时器(AI 卡片);绑当前注入 slot 生命周期
|
|
67
75
|
};
|
|
68
76
|
}
|
|
69
77
|
|
|
@@ -87,6 +95,17 @@ function newInstance(adapter) {
|
|
|
87
95
|
* @property {(ackCtx:*, client:*)=>void} [ack] Ack an inbound msg (platforms that redeliver if not acked).
|
|
88
96
|
* @property {(cfg:Object, target:Object, content:string, ctx:{fetch,store})=>Promise<void>} sendOne Send one chunk.
|
|
89
97
|
* @property {(cfg:Object, ctx:{fetch,store})=>Promise<{ok:boolean, detail?:string}>} testConnection Validate creds, no socket.
|
|
98
|
+
*
|
|
99
|
+
* ── Optional AI-card streaming (only platforms that support an updatable card implement these) ──
|
|
100
|
+
* @property {(cfg:Object)=>boolean} [streamEnabled] Whether per-character streaming is on for this cfg.
|
|
101
|
+
* When absent the core falls back to `!!cfg.aiCardTemplateId` (DingTalk's template-id switch).
|
|
102
|
+
* @property {(cfg:Object, target:Object, statusText:string, ctx:Object)=>Promise<?Object>} [sendAckCard]
|
|
103
|
+
* Send the instant ack as an (ideally streamable) card; return a handle ({streaming:true,…})
|
|
104
|
+
* to enable streaming, a non-streaming handle, or null to fall back to a plain-text ack.
|
|
105
|
+
* @property {(cfg:Object, target:Object, handle:Object, fullText:string, ctx:Object)=>Promise<boolean>} [streamCardText]
|
|
106
|
+
* Push the cumulative reply text into the card (best-effort; drives the typewriter effect).
|
|
107
|
+
* @property {(cfg:Object, target:Object, handle:Object, fullText:string, status:string, ctx:Object)=>Promise<boolean>} [updateAckCard]
|
|
108
|
+
* Finalize the card with the authoritative reply text + terminal status (done/interrupted/error).
|
|
90
109
|
*/
|
|
91
110
|
|
|
92
111
|
/** Register a platform adapter (called at adapter-module import). Idempotent per id. */
|
|
@@ -168,11 +187,69 @@ async function finalizeAckCard(inst, target, text, status) {
|
|
|
168
187
|
|
|
169
188
|
// ─── activeInjection lifecycle ───
|
|
170
189
|
function clearActiveInjection() {
|
|
190
|
+
// 先停掉持有 slot 的实例的逐字流式定时器(slot 释放的每条路径都经此:turn-end / /stop / 超时 /
|
|
191
|
+
// inject-fail / stopBridge),杜绝定时器泄漏与 finalize 后的乱序推送。
|
|
192
|
+
if (activeInjection) {
|
|
193
|
+
const owner = instances.get(activeInjection.platformId);
|
|
194
|
+
if (owner && owner.streamTimer) { clearInterval(owner.streamTimer); owner.streamTimer = null; }
|
|
195
|
+
}
|
|
171
196
|
activeInjection = null;
|
|
172
197
|
if (activeInjectionTimer) { clearTimeout(activeInjectionTimer); activeInjectionTimer = null; }
|
|
173
198
|
if (idlePollTimer) { clearInterval(idlePollTimer); idlePollTimer = null; }
|
|
174
199
|
idlePollCount = 0;
|
|
175
200
|
}
|
|
201
|
+
|
|
202
|
+
// slot 仍属本次注入?streamTick 入口与每次 await 后都要重验——slot 可能在 await 期间被 finalize/释放,
|
|
203
|
+
// 重验保证 finalize 永远是最后一次 PUT、定时器也不在易主后继续推。
|
|
204
|
+
function isSlotOwned(inst, since) {
|
|
205
|
+
return !!activeInjection && activeInjection.platformId === inst.adapter.id && activeInjection.since === since;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* 逐字流式推送一帧(钉钉 AI 卡片)。常驻 setInterval 驱动,贯穿本注入 slot 生命周期——**不**随
|
|
210
|
+
* isStreaming() 抖动停(带工具的回合中间 streamingState 每个 API 调用会 false↔true,停了就断流)。
|
|
211
|
+
* 全程 best-effort:任何失败都不影响最终 finalize(notifyTurnEnd 用 transcript 权威全文落定)。
|
|
212
|
+
*/
|
|
213
|
+
async function streamTick(inst, since) {
|
|
214
|
+
// slot 已释放/易主 → 自停(定时器自清)
|
|
215
|
+
if (!isSlotOwned(inst, since)) {
|
|
216
|
+
if (inst.streamTimer) { clearInterval(inst.streamTimer); inst.streamTimer = null; }
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (inst._streamInFlight) return; // 单 in-flight:避免乱序 PUT(钉钉靠 guid 排序)
|
|
220
|
+
if (inst._streamHandle === null) return; // 已判定不可流式(无句柄 / 非 AI 卡片)
|
|
221
|
+
const d = inst.bridgeDeps;
|
|
222
|
+
if (!d || typeof d.getLiveText !== 'function') return;
|
|
223
|
+
// 解析一次 ack 卡片句柄并缓存:null/非流式 → 永久关停本轮推送,不再每 tick 重复 await。
|
|
224
|
+
if (inst._streamHandle === undefined) {
|
|
225
|
+
inst._streamHandle = (await inst.ackCardPromise?.catch(() => null)) ?? null;
|
|
226
|
+
// 落审计便于诊断:streaming=false 说明 AI 卡片建卡失败/回退 legacy(aiErr 给出钉钉返回的原因);
|
|
227
|
+
// liveLen=0 说明文本源没采到(多半 mainAgent 判定或采集开关问题)。
|
|
228
|
+
audit(inst, 'stream-handle', { streaming: !!inst._streamHandle?.streaming, liveLen: (d.getLiveText?.() || '').length, aiErr: inst.store?.lastAiCardError || undefined });
|
|
229
|
+
if (!inst._streamHandle?.streaming) { inst._streamHandle = null; return; }
|
|
230
|
+
}
|
|
231
|
+
if (inst._streamFrames >= STREAM_MAX_FRAMES) return; // 计费上限:停中途推送,finalize 仍落全文
|
|
232
|
+
let text = d.getLiveText();
|
|
233
|
+
if (typeof text !== 'string') return;
|
|
234
|
+
if (text.length > STREAM_MAX_CHARS) text = text.slice(0, STREAM_MAX_CHARS);
|
|
235
|
+
if (text.length - (inst._streamPushedLen || 0) < STREAM_MIN_DELTA) return; // 增长不足,省调用
|
|
236
|
+
inst._streamInFlight = true;
|
|
237
|
+
const target = activeInjection.target;
|
|
238
|
+
const cfg = d.getConfig();
|
|
239
|
+
const ok = await inst.adapter.streamCardText(cfg, target, inst._streamHandle, text, ctxFor(inst)).catch(() => false);
|
|
240
|
+
inst._streamInFlight = false;
|
|
241
|
+
// await 后重新校验 slot:期间可能已 finalize/释放 → 本帧作废,不计数(保证 finalize 是最后一次 PUT)。
|
|
242
|
+
if (!isSlotOwned(inst, since)) return;
|
|
243
|
+
// 落审计:ok=false 说明 /card/streaming 被拒(多半 Card.Streaming.Write 权限缺失);ok=true 却看不到
|
|
244
|
+
// 流式则多半是 AI 卡片模板里流式变量名不叫 content(见 streamFrame 的 key)。
|
|
245
|
+
audit(inst, 'stream-push', { len: text.length, ok });
|
|
246
|
+
if (ok) {
|
|
247
|
+
inst._streamPushedLen = text.length;
|
|
248
|
+
inst._streamFrames = (inst._streamFrames || 0) + 1;
|
|
249
|
+
if (inst._streamFrames === STREAM_MAX_FRAMES) audit(inst, 'stream-frame-cap', { conversationId: target?.conversationId, frames: inst._streamFrames });
|
|
250
|
+
idlePollCount = 0; // 有流式活动 → 重置空闲计数,避免 idlePoll 误判触发合成 turn_end
|
|
251
|
+
}
|
|
252
|
+
}
|
|
176
253
|
function armActiveInjection(inst, target, since) {
|
|
177
254
|
activeInjection = { platformId: inst.adapter.id, since, target, transcriptPath: null };
|
|
178
255
|
if (activeInjectionTimer) clearTimeout(activeInjectionTimer);
|
|
@@ -205,6 +282,42 @@ function armActiveInjection(inst, target, since) {
|
|
|
205
282
|
}
|
|
206
283
|
}, IDLE_POLL_INTERVAL_MS);
|
|
207
284
|
if (typeof idlePollTimer.unref === 'function') idlePollTimer.unref();
|
|
285
|
+
|
|
286
|
+
// 逐字流式(钉钉 AI 卡片):仅当配了 aiCardTemplateId、开了 ack 卡片、且平台具备流式能力(worker
|
|
287
|
+
// 注入了 getLiveText + 适配器实现 streamCardText)时启动常驻推送定时器。注入轮次开始即重置文本源。
|
|
288
|
+
const cfg = inst.bridgeDeps?.getConfig?.();
|
|
289
|
+
// 流式总开关:钉钉用专属 aiCardTemplateId(非空即开);其它平台(飞书/微信)无模板 id,改由适配器
|
|
290
|
+
// 自报 streamEnabled(cfg)(通常 !!cfg.aiCard)。适配器未定义 → 回落旧逻辑,钉钉零回归。
|
|
291
|
+
const streamEnabled = !!cfg && (typeof inst.adapter.streamEnabled === 'function'
|
|
292
|
+
? !!inst.adapter.streamEnabled(cfg)
|
|
293
|
+
: !!cfg.aiCardTemplateId);
|
|
294
|
+
if (streamEnabled) {
|
|
295
|
+
const canStream = cfg.ackCard !== false
|
|
296
|
+
&& typeof inst.bridgeDeps.getLiveText === 'function'
|
|
297
|
+
&& typeof inst.adapter.streamCardText === 'function'
|
|
298
|
+
// 同时要求 updateAckCard:能逐字推却无法 finalize 会让卡片永远停在流式中途、停在「生成中」。
|
|
299
|
+
&& typeof inst.adapter.updateAckCard === 'function';
|
|
300
|
+
if (canStream) {
|
|
301
|
+
inst.bridgeDeps.resetLiveText?.();
|
|
302
|
+
if (inst.store) inst.store.lastAiCardError = null; // 清上一轮的诊断残留
|
|
303
|
+
inst._streamPushedLen = 0;
|
|
304
|
+
inst._streamFrames = 0;
|
|
305
|
+
inst._streamHandle = undefined; // 首 tick 解析一次后缓存(null = 不可流式)
|
|
306
|
+
inst._streamInFlight = false;
|
|
307
|
+
if (inst.streamTimer) clearInterval(inst.streamTimer);
|
|
308
|
+
inst.streamTimer = setInterval(() => { void streamTick(inst, since); }, STREAM_TICK_MS);
|
|
309
|
+
if (typeof inst.streamTimer.unref === 'function') inst.streamTimer.unref();
|
|
310
|
+
audit(inst, 'stream-armed', { conversationId: target?.conversationId });
|
|
311
|
+
} else {
|
|
312
|
+
// 配了 aiCardTemplateId 却没起流式:把原因落审计,便于诊断「没有流式输出」。
|
|
313
|
+
audit(inst, 'stream-skip', {
|
|
314
|
+
reason: cfg.ackCard === false ? 'ackCardOff'
|
|
315
|
+
: typeof inst.bridgeDeps?.getLiveText !== 'function' ? 'noGetLiveText(非 worker?)'
|
|
316
|
+
: typeof inst.adapter.streamCardText !== 'function' ? 'noStreamCardText'
|
|
317
|
+
: 'noUpdateAckCard',
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
208
321
|
}
|
|
209
322
|
|
|
210
323
|
// ─── small helpers ───
|
|
@@ -543,6 +656,22 @@ export async function notifyTurnEnd(sessionId, ts, transcriptPath) {
|
|
|
543
656
|
const handle = await ackP?.catch(() => null);
|
|
544
657
|
if (handle && typeof inst.adapter.updateAckCard === 'function') {
|
|
545
658
|
const cfg = inst.bridgeDeps.getConfig();
|
|
659
|
+
if (handle.streaming) {
|
|
660
|
+
// AI 卡片:整条回复就在卡片内(流式期间已逐字呈现)。finalize 用 transcript 权威全文一次性
|
|
661
|
+
// 落定 + 状态标签(执行完成),**不分块、不另发消息**——避免把已显示的全文截回首块。
|
|
662
|
+
let full = text;
|
|
663
|
+
if (full.length > STREAM_MAX_CHARS) full = full.slice(0, STREAM_MAX_CHARS) + '\n\n' + tr(inst, 'truncated');
|
|
664
|
+
try {
|
|
665
|
+
const updated = await inst.adapter.updateAckCard(cfg, target, handle, full, 'done', ctxFor(inst));
|
|
666
|
+
if (!updated) await sendReply(inst, target, text);
|
|
667
|
+
audit(inst, 'out', { conversationId: target.conversationId, streaming: true, cardUpdated: !!updated });
|
|
668
|
+
} catch (e) {
|
|
669
|
+
inst.lastError = String(e?.message || e);
|
|
670
|
+
audit(inst, 'card-update-error', { error: inst.lastError });
|
|
671
|
+
try { await sendReply(inst, target, text); } catch { /* already logged in sendReply */ }
|
|
672
|
+
}
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
546
675
|
let chunks = chunkText(text, cfg.maxChunkChars);
|
|
547
676
|
if (chunks.length > MAX_CHUNKS_PER_TURN) {
|
|
548
677
|
chunks = chunks.slice(0, MAX_CHUNKS_PER_TURN);
|
|
@@ -610,6 +739,7 @@ export async function stopBridge(id) {
|
|
|
610
739
|
inst.ackCardPromise = null;
|
|
611
740
|
}
|
|
612
741
|
try { await inst.adapter.disconnect?.(inst.client, ctxFor(inst)); } catch { /* best-effort */ }
|
|
742
|
+
if (inst.streamTimer) { clearInterval(inst.streamTimer); inst.streamTimer = null; } // 防御:流式定时器不泄漏
|
|
613
743
|
inst.client = null;
|
|
614
744
|
inst.running = false;
|
|
615
745
|
inst.connected = false;
|
|
@@ -667,6 +797,8 @@ export async function stopAll() {
|
|
|
667
797
|
}
|
|
668
798
|
|
|
669
799
|
// ─── test seams ───
|
|
800
|
+
export function __setStreamTickMsForTests(ms) { STREAM_TICK_MS = (ms == null ? 300 : ms); }
|
|
801
|
+
|
|
670
802
|
export function __setMaxQueueForTests(id, n) {
|
|
671
803
|
const inst = instances.get(id);
|
|
672
804
|
if (inst) inst.maxQueueOverride = n;
|
|
@@ -681,6 +813,8 @@ export function __resetForTests(id) {
|
|
|
681
813
|
inst.maxQueueOverride = null;
|
|
682
814
|
inst.seenMsgIds.length = 0; inst.queue.length = 0; inst.sendTimes.length = 0;
|
|
683
815
|
inst.store = {};
|
|
816
|
+
inst.ackCardPromise = null;
|
|
817
|
+
if (inst.streamTimer) { clearInterval(inst.streamTimer); inst.streamTimer = null; }
|
|
684
818
|
if (activeInjection && activeInjection.platformId === id) clearActiveInjection();
|
|
685
819
|
}
|
|
686
820
|
|
package/server/lib/im-config.js
CHANGED
|
@@ -34,7 +34,7 @@ const DESCRIPTORS = {
|
|
|
34
34
|
allowListField: 'allowStaffIds',
|
|
35
35
|
defaults: {
|
|
36
36
|
enabled: false, appKey: '', appSecret: '', allowStaffIds: [],
|
|
37
|
-
maxChunkChars: 3800, blockOnSkipPermissions: false, ackCard: true, cardTemplateId: '',
|
|
37
|
+
maxChunkChars: 3800, blockOnSkipPermissions: false, ackCard: true, cardTemplateId: '', aiCardTemplateId: '', aiCardStreamKey: '',
|
|
38
38
|
},
|
|
39
39
|
fields: [
|
|
40
40
|
{ key: 'enabled', type: 'bool' },
|
|
@@ -45,6 +45,11 @@ const DESCRIPTORS = {
|
|
|
45
45
|
{ key: 'blockOnSkipPermissions', type: 'bool' },
|
|
46
46
|
{ key: 'ackCard', type: 'bool', default: true },
|
|
47
47
|
{ key: 'cardTemplateId', type: 'string' },
|
|
48
|
+
// AI 卡片场景模板 id(声明流式变量 + flowStatus 状态变量)。非空即启用「逐字流式 +
|
|
49
|
+
// flowStatus 状态标签」AI 卡片;留空回退到 cardTemplateId 普通卡片或纯文本。
|
|
50
|
+
{ key: 'aiCardTemplateId', type: 'string' },
|
|
51
|
+
// AI 卡片模板里那个「流式 Markdown 变量」的名字。留空按钉钉/官方惯例用 'content';模板若用别名在此填。
|
|
52
|
+
{ key: 'aiCardStreamKey', type: 'string' },
|
|
48
53
|
],
|
|
49
54
|
},
|
|
50
55
|
feishu: {
|
|
@@ -52,7 +57,7 @@ const DESCRIPTORS = {
|
|
|
52
57
|
allowListField: 'allowUserIds',
|
|
53
58
|
defaults: {
|
|
54
59
|
enabled: false, appId: '', appSecret: '', region: 'feishu', allowUserIds: [],
|
|
55
|
-
maxChunkChars: 3800, blockOnSkipPermissions: false, ackCard: true,
|
|
60
|
+
maxChunkChars: 3800, blockOnSkipPermissions: false, ackCard: true, aiCard: false,
|
|
56
61
|
},
|
|
57
62
|
fields: [
|
|
58
63
|
{ key: 'enabled', type: 'bool' },
|
|
@@ -63,6 +68,9 @@ const DESCRIPTORS = {
|
|
|
63
68
|
{ key: 'maxChunkChars', type: 'chunk' },
|
|
64
69
|
{ key: 'blockOnSkipPermissions', type: 'bool' },
|
|
65
70
|
{ key: 'ackCard', type: 'bool', default: true },
|
|
71
|
+
// 开启即用 CardKit v1 流式卡片逐字回复(需应用具备 cardkit:card:write scope);关闭/缺 scope
|
|
72
|
+
// 回退到「占位卡片 + 整段替换」。
|
|
73
|
+
{ key: 'aiCard', type: 'bool', default: false },
|
|
66
74
|
],
|
|
67
75
|
},
|
|
68
76
|
wecom: {
|
|
@@ -70,7 +78,7 @@ const DESCRIPTORS = {
|
|
|
70
78
|
allowListField: 'allowUserIds',
|
|
71
79
|
defaults: {
|
|
72
80
|
enabled: false, botId: '', secret: '', allowUserIds: [],
|
|
73
|
-
maxChunkChars: 3800, blockOnSkipPermissions: false, ackCard: true,
|
|
81
|
+
maxChunkChars: 3800, blockOnSkipPermissions: false, ackCard: true, aiCard: false,
|
|
74
82
|
},
|
|
75
83
|
fields: [
|
|
76
84
|
{ key: 'enabled', type: 'bool' },
|
|
@@ -80,6 +88,8 @@ const DESCRIPTORS = {
|
|
|
80
88
|
{ key: 'maxChunkChars', type: 'chunk' },
|
|
81
89
|
{ key: 'blockOnSkipPermissions', type: 'bool' },
|
|
82
90
|
{ key: 'ackCard', type: 'bool', default: true },
|
|
91
|
+
// 开启即用智能机器人长连接 stream 消息逐字回复;关闭回退到「整段 proactive 文本」。
|
|
92
|
+
{ key: 'aiCard', type: 'bool', default: false },
|
|
83
93
|
],
|
|
84
94
|
},
|
|
85
95
|
discord: {
|
|
@@ -82,6 +82,8 @@ function dingtalkConfigPost(req, res, parsedUrl, isLocal, deps) {
|
|
|
82
82
|
blockOnSkipPermissions: incoming.blockOnSkipPermissions,
|
|
83
83
|
ackCard: incoming.ackCard,
|
|
84
84
|
cardTemplateId: incoming.cardTemplateId,
|
|
85
|
+
aiCardTemplateId: incoming.aiCardTemplateId,
|
|
86
|
+
aiCardStreamKey: incoming.aiCardStreamKey,
|
|
85
87
|
});
|
|
86
88
|
// 驱动进程管理器(替代旧的在进程 reloadBridge):启用→重启 worker,停用→停 worker。
|
|
87
89
|
try {
|
package/server/server.js
CHANGED
|
@@ -71,7 +71,7 @@ function execWithStdin(cmd, args, input, options) {
|
|
|
71
71
|
child.stdin.end();
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
|
-
import { LOG_FILE, _initPromise, _resumeState, _projectName, _logDir, streamingState, resetStreamingState, PROFILE_PATH, setLivePort } from './interceptor.js';
|
|
74
|
+
import { LOG_FILE, _initPromise, _resumeState, _projectName, _logDir, streamingState, resetStreamingState, PROFILE_PATH, setLivePort, getImLiveText, resetImLiveText } from './interceptor.js';
|
|
75
75
|
import { recordInstance, listInstances } from './lib/instance-registry.js';
|
|
76
76
|
import { LOG_DIR, setLogDir, getClaudeConfigDir } from '../findcc.js';
|
|
77
77
|
import { t, getLang, setLang } from './i18n.js';
|
|
@@ -1035,6 +1035,10 @@ export async function startViewer() {
|
|
|
1035
1035
|
getPtySkipPermissions: pmb.getPtySkipPermissions,
|
|
1036
1036
|
isStreaming: () => streamingState.active,
|
|
1037
1037
|
getConfig: () => loadConfig(id),
|
|
1038
|
+
// 钉钉 AI 卡片逐字流式的文本源(仅 worker 注入):对话过程中读主 agent 累计文本,
|
|
1039
|
+
// 注入轮次开始时重置。其它平台/主 ccv 无此 dep,core 判空即跳过流式。
|
|
1040
|
+
getLiveText: () => getImLiveText(),
|
|
1041
|
+
resetLiveText: () => resetImLiveText(),
|
|
1038
1042
|
});
|
|
1039
1043
|
} catch (e) {
|
|
1040
1044
|
console.error(`[CC Viewer] IM worker startBridge(${id}) failed:`, e?.message || e);
|