@modelzen/feishu-codex-bridge 0.2.0 → 0.2.1
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.js +166 -10
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1961,8 +1961,8 @@ function card(elements, opts = {}) {
|
|
|
1961
1961
|
if (opts.streaming) {
|
|
1962
1962
|
config.streaming_mode = true;
|
|
1963
1963
|
config.streaming_config = {
|
|
1964
|
-
print_frequency_ms: { default:
|
|
1965
|
-
print_step: { default:
|
|
1964
|
+
print_frequency_ms: { default: 25 },
|
|
1965
|
+
print_step: { default: 6 },
|
|
1966
1966
|
print_strategy: "fast"
|
|
1967
1967
|
};
|
|
1968
1968
|
}
|
|
@@ -1984,6 +1984,9 @@ function card(elements, opts = {}) {
|
|
|
1984
1984
|
function md(content) {
|
|
1985
1985
|
return { tag: "markdown", content };
|
|
1986
1986
|
}
|
|
1987
|
+
function mdStream(content, elementId) {
|
|
1988
|
+
return { tag: "markdown", element_id: elementId, content };
|
|
1989
|
+
}
|
|
1987
1990
|
function image(imgKey, alt = "") {
|
|
1988
1991
|
return {
|
|
1989
1992
|
tag: "img",
|
|
@@ -2533,6 +2536,7 @@ function escapeInline2(s) {
|
|
|
2533
2536
|
var RC = {
|
|
2534
2537
|
stop: "run.stop"
|
|
2535
2538
|
};
|
|
2539
|
+
var ANSWER_EID = "answer";
|
|
2536
2540
|
var REASONING_MAX = 1500;
|
|
2537
2541
|
var COLLAPSE_TOOL_THRESHOLD = 3;
|
|
2538
2542
|
var PROCESS_BODY_BUDGET = 22e3;
|
|
@@ -2546,14 +2550,19 @@ function renderRunning(state, rc) {
|
|
|
2546
2550
|
const elements = [];
|
|
2547
2551
|
const reasoning = reasoningContent(state);
|
|
2548
2552
|
if (reasoning) elements.push(reasoningPanel(reasoning, state.reasoningActive));
|
|
2549
|
-
const
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2553
|
+
const showTools = rc.showTools !== false;
|
|
2554
|
+
const tools = [];
|
|
2555
|
+
const textParts = [];
|
|
2556
|
+
for (const b of state.blocks) {
|
|
2557
|
+
if (b.kind === "tool") {
|
|
2558
|
+
if (showTools) tools.push(b.tool);
|
|
2559
|
+
} else if (b.content.trim()) {
|
|
2560
|
+
textParts.push(b.content);
|
|
2555
2561
|
}
|
|
2556
2562
|
}
|
|
2563
|
+
if (tools.length > 0) elements.push(...renderToolGroup(tools, false));
|
|
2564
|
+
const answer = textParts.join("\n\n");
|
|
2565
|
+
if (answer) elements.push(mdStream(answer, ANSWER_EID));
|
|
2557
2566
|
if (state.footer) elements.push(footerStatus(state.footer));
|
|
2558
2567
|
if (rc.cardKey) elements.push(actions([button("\u23F9 \u7EC8\u6B62", { a: RC.stop, m: rc.cardKey }, "danger")]));
|
|
2559
2568
|
return elements;
|
|
@@ -2700,16 +2709,105 @@ function truncate4(s, n) {
|
|
|
2700
2709
|
}
|
|
2701
2710
|
|
|
2702
2711
|
// src/card/run-card-stream.ts
|
|
2703
|
-
var STREAM_THROTTLE_MS =
|
|
2712
|
+
var STREAM_THROTTLE_MS = 150;
|
|
2704
2713
|
var RunCardStream = class {
|
|
2705
2714
|
cardId = "";
|
|
2706
2715
|
_messageId = "";
|
|
2707
2716
|
seq = 0;
|
|
2708
2717
|
lastPush = 0;
|
|
2709
2718
|
lastContent = "";
|
|
2719
|
+
// Per-turn push counters — surfaced via stats() for the stream.timing log.
|
|
2720
|
+
pushCount = 0;
|
|
2721
|
+
cardPushes = 0;
|
|
2722
|
+
// whole-card card.update (structure)
|
|
2723
|
+
elPushes = 0;
|
|
2724
|
+
// element cardElement.content (answer typewriter)
|
|
2725
|
+
totalRttMs = 0;
|
|
2726
|
+
maxRttMs = 0;
|
|
2727
|
+
// Coalesced streaming. The consume loop records the latest {card, answerEid} in
|
|
2728
|
+
// `pending`; a single pump() drains it — NON-BLOCKING, so consuming codex events
|
|
2729
|
+
// never stalls on a round-trip (awaiting each push serially drained the backlog
|
|
2730
|
+
// at one event per ~RTT → "已回完、飞书还在慢慢打字"). The pump pushes only the most
|
|
2731
|
+
// recent snapshot per round-trip: when only the answer text grew it streams that
|
|
2732
|
+
// via cardElement.content (typewriter), otherwise it whole-card updates.
|
|
2733
|
+
pending = null;
|
|
2734
|
+
pumpChannel = null;
|
|
2735
|
+
pumpPromise = null;
|
|
2736
|
+
// Baselines for the pump's route decision (structure unchanged + answer grew?).
|
|
2737
|
+
lastStructureSig = "";
|
|
2738
|
+
lastAnswerText = "";
|
|
2710
2739
|
get messageId() {
|
|
2711
2740
|
return this._messageId;
|
|
2712
2741
|
}
|
|
2742
|
+
/** Push counts (whole-card vs element) + round-trip stats for this card. */
|
|
2743
|
+
stats() {
|
|
2744
|
+
return {
|
|
2745
|
+
pushCount: this.pushCount,
|
|
2746
|
+
cardPushes: this.cardPushes,
|
|
2747
|
+
elPushes: this.elPushes,
|
|
2748
|
+
totalRttMs: this.totalRttMs,
|
|
2749
|
+
maxRttMs: this.maxRttMs
|
|
2750
|
+
};
|
|
2751
|
+
}
|
|
2752
|
+
/**
|
|
2753
|
+
* Record the latest card and ensure the pump is running. Returns immediately;
|
|
2754
|
+
* calls that arrive while a push is in flight collapse into a single push of
|
|
2755
|
+
* the most recent card once that round-trip completes. Use this from the
|
|
2756
|
+
* event-consume loop instead of awaiting {@link streamCard} per event.
|
|
2757
|
+
*/
|
|
2758
|
+
streamCoalesced(channel, fullCard, answerEid) {
|
|
2759
|
+
this.pending = { card: fullCard, answerEid };
|
|
2760
|
+
this.pumpChannel = channel;
|
|
2761
|
+
if (!this.pumpPromise) this.pumpPromise = this.pump();
|
|
2762
|
+
}
|
|
2763
|
+
/** Await any in-flight coalesced push so the final streaming frame lands (and
|
|
2764
|
+
* `seq` stays ordered) before the terminal update. Call after the loop ends. */
|
|
2765
|
+
async drain() {
|
|
2766
|
+
if (this.pumpPromise) await this.pumpPromise;
|
|
2767
|
+
}
|
|
2768
|
+
async pump() {
|
|
2769
|
+
try {
|
|
2770
|
+
while (this.pending && this.pumpChannel) {
|
|
2771
|
+
const { card: card2, answerEid } = this.pending;
|
|
2772
|
+
this.pending = null;
|
|
2773
|
+
const t0 = Date.now();
|
|
2774
|
+
const answer = answerEid ? answerContent(card2, answerEid) : null;
|
|
2775
|
+
const sig = structureSig(card2, answerEid);
|
|
2776
|
+
if (answerEid && answer !== null && sig === this.lastStructureSig && answer !== this.lastAnswerText && answer.startsWith(this.lastAnswerText)) {
|
|
2777
|
+
await this.streamElement(this.pumpChannel, answerEid, answer);
|
|
2778
|
+
this.lastAnswerText = answer;
|
|
2779
|
+
} else {
|
|
2780
|
+
await this.streamCard(this.pumpChannel, card2, true);
|
|
2781
|
+
this.lastStructureSig = sig;
|
|
2782
|
+
this.lastAnswerText = answer ?? "";
|
|
2783
|
+
}
|
|
2784
|
+
const gap = STREAM_THROTTLE_MS - (Date.now() - t0);
|
|
2785
|
+
if (this.pending && gap > 0) await new Promise((r) => setTimeout(r, gap));
|
|
2786
|
+
}
|
|
2787
|
+
} finally {
|
|
2788
|
+
this.pumpPromise = null;
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
/** Element-level streaming push (cardkit.v1.cardElement.content): the answer
|
|
2792
|
+
* element's accumulated full text. Feishu diffs the prefix and types the delta
|
|
2793
|
+
* per the card's streaming_config. Needs streaming_mode on the card. */
|
|
2794
|
+
async streamElement(channel, elementId, content) {
|
|
2795
|
+
if (!this.cardId) return;
|
|
2796
|
+
const t0 = Date.now();
|
|
2797
|
+
try {
|
|
2798
|
+
await channel.rawClient.cardkit.v1.cardElement.content({
|
|
2799
|
+
path: { card_id: this.cardId, element_id: elementId },
|
|
2800
|
+
data: { content, sequence: ++this.seq, uuid: `e_${this.cardId}_${this.seq}` }
|
|
2801
|
+
});
|
|
2802
|
+
const rtt = Date.now() - t0;
|
|
2803
|
+
this.pushCount++;
|
|
2804
|
+
this.elPushes++;
|
|
2805
|
+
this.totalRttMs += rtt;
|
|
2806
|
+
if (rtt > this.maxRttMs) this.maxRttMs = rtt;
|
|
2807
|
+
} catch (err) {
|
|
2808
|
+
log.fail("card", err, { phase: "run-stream-el", cardId: this.cardId, seq: this.seq });
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2713
2811
|
/** Create the entity from the initial (running) card and send a message
|
|
2714
2812
|
* referencing it by card_id. Returns the carrier message id. */
|
|
2715
2813
|
async create(channel, chatId, initialCard, opts) {
|
|
@@ -2751,11 +2849,17 @@ var RunCardStream = class {
|
|
|
2751
2849
|
if (!force && now - this.lastPush < STREAM_THROTTLE_MS) return;
|
|
2752
2850
|
this.lastPush = now;
|
|
2753
2851
|
this.lastContent = data;
|
|
2852
|
+
const t0 = Date.now();
|
|
2754
2853
|
try {
|
|
2755
2854
|
await channel.rawClient.cardkit.v1.card.update({
|
|
2756
2855
|
path: { card_id: this.cardId },
|
|
2757
2856
|
data: { card: { type: "card_json", data }, sequence: ++this.seq, uuid: `s_${this.cardId}_${this.seq}` }
|
|
2758
2857
|
});
|
|
2858
|
+
const rtt = Date.now() - t0;
|
|
2859
|
+
this.pushCount++;
|
|
2860
|
+
this.cardPushes++;
|
|
2861
|
+
this.totalRttMs += rtt;
|
|
2862
|
+
if (rtt > this.maxRttMs) this.maxRttMs = rtt;
|
|
2759
2863
|
} catch (err) {
|
|
2760
2864
|
log.fail("card", err, { phase: "run-stream", cardId: this.cardId, seq: this.seq });
|
|
2761
2865
|
}
|
|
@@ -2785,6 +2889,21 @@ var RunCardStream = class {
|
|
|
2785
2889
|
}
|
|
2786
2890
|
}
|
|
2787
2891
|
};
|
|
2892
|
+
function answerContent(card2, eid) {
|
|
2893
|
+
const els = card2.body?.elements;
|
|
2894
|
+
if (!Array.isArray(els)) return null;
|
|
2895
|
+
for (const el of els) {
|
|
2896
|
+
if (el && el.element_id === eid) return typeof el.content === "string" ? el.content : "";
|
|
2897
|
+
}
|
|
2898
|
+
return null;
|
|
2899
|
+
}
|
|
2900
|
+
function structureSig(card2, eid) {
|
|
2901
|
+
const body = card2.body;
|
|
2902
|
+
const els = body?.elements;
|
|
2903
|
+
if (!eid || !Array.isArray(els)) return JSON.stringify(card2);
|
|
2904
|
+
const blanked = els.map((el) => el && el.element_id === eid ? { ...el, content: "" } : el);
|
|
2905
|
+
return JSON.stringify({ ...card2, body: { ...body, elements: blanked } });
|
|
2906
|
+
}
|
|
2788
2907
|
|
|
2789
2908
|
// src/card/outbound-images.ts
|
|
2790
2909
|
import { readFile as readFile5, stat as stat2 } from "fs/promises";
|
|
@@ -5104,11 +5223,29 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
|
|
|
5104
5223
|
},
|
|
5105
5224
|
stopSignal
|
|
5106
5225
|
);
|
|
5226
|
+
const tStart = Date.now();
|
|
5227
|
+
let firstEvAt = 0;
|
|
5228
|
+
let firstTextAt = 0;
|
|
5229
|
+
let lastEvAt = tStart;
|
|
5230
|
+
let evCount = 0;
|
|
5231
|
+
let textChars = 0;
|
|
5107
5232
|
for await (const ev of guarded) {
|
|
5233
|
+
const tEv = Date.now();
|
|
5234
|
+
if (!firstEvAt) firstEvAt = tEv;
|
|
5235
|
+
const et = ev.type;
|
|
5236
|
+
if (et === "text_delta") {
|
|
5237
|
+
if (!firstTextAt) firstTextAt = tEv;
|
|
5238
|
+
const d = ev.delta;
|
|
5239
|
+
if (typeof d === "string") textChars += d.length;
|
|
5240
|
+
}
|
|
5241
|
+
lastEvAt = tEv;
|
|
5242
|
+
evCount++;
|
|
5108
5243
|
render.apply(ev);
|
|
5109
5244
|
rc.rs = render.snapshot();
|
|
5110
|
-
|
|
5245
|
+
stream2.streamCoalesced(channel, buildRunCard(rc), ANSWER_EID);
|
|
5111
5246
|
}
|
|
5247
|
+
const doneAt = Date.now();
|
|
5248
|
+
await stream2.drain();
|
|
5112
5249
|
state.interrupt = void 0;
|
|
5113
5250
|
const killed = interrupted || timedOut;
|
|
5114
5251
|
if (timedOut) render.timeout(Math.max(1, Math.round(idleMs / 6e4)));
|
|
@@ -5129,6 +5266,25 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
|
|
|
5129
5266
|
rc.images = await uploadOutboundImages(channel, imgSources, opts.cwd ?? fallbackCwd);
|
|
5130
5267
|
}
|
|
5131
5268
|
await stream2.updateCard(channel, buildRunCard(rc));
|
|
5269
|
+
{
|
|
5270
|
+
const terminalAt = Date.now();
|
|
5271
|
+
const st = stream2.stats();
|
|
5272
|
+
log.info("stream", "timing", {
|
|
5273
|
+
firstEv: firstEvAt ? firstEvAt - tStart : -1,
|
|
5274
|
+
firstText: firstTextAt ? firstTextAt - tStart : -1,
|
|
5275
|
+
lastEv: lastEvAt - tStart,
|
|
5276
|
+
done: doneAt - tStart,
|
|
5277
|
+
terminal: terminalAt - tStart,
|
|
5278
|
+
doneToTerminal: terminalAt - doneAt,
|
|
5279
|
+
events: evCount,
|
|
5280
|
+
textChars,
|
|
5281
|
+
pushes: st.pushCount,
|
|
5282
|
+
cardPushes: st.cardPushes,
|
|
5283
|
+
elPushes: st.elPushes,
|
|
5284
|
+
rttAvg: st.pushCount ? Math.round(st.totalRttMs / st.pushCount) : 0,
|
|
5285
|
+
rttMax: st.maxRttMs
|
|
5286
|
+
});
|
|
5287
|
+
}
|
|
5132
5288
|
runsByCard.delete(cardMsgId);
|
|
5133
5289
|
promoteCard(finalMsgId, rc);
|
|
5134
5290
|
for (const fence of fences) {
|