@modelzen/feishu-codex-bridge 0.3.5 → 0.3.6
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 +231 -15
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5606,6 +5606,199 @@ ${lines}
|
|
|
5606
5606
|
]`;
|
|
5607
5607
|
}
|
|
5608
5608
|
|
|
5609
|
+
// src/bot/context-weave.ts
|
|
5610
|
+
var QUOTE_MAX = 800;
|
|
5611
|
+
var LINE_MAX = 280;
|
|
5612
|
+
var THREAD_WEAVE_MAX = 20;
|
|
5613
|
+
var THREAD_PAGE_SIZE = 50;
|
|
5614
|
+
async function fetchQuotedMessage(channel, messageId) {
|
|
5615
|
+
try {
|
|
5616
|
+
const res = await channel.rawClient.im.v1.message.get({ path: { message_id: messageId } });
|
|
5617
|
+
const items = res.data?.items ?? [];
|
|
5618
|
+
const item = items[0];
|
|
5619
|
+
if (!item || item.deleted) return void 0;
|
|
5620
|
+
const cm = toContextMessage(item);
|
|
5621
|
+
return cm.text.trim() ? cm : void 0;
|
|
5622
|
+
} catch (err) {
|
|
5623
|
+
log.warn("intake", "quote-fetch-failed", { messageId, err: String(err) });
|
|
5624
|
+
return void 0;
|
|
5625
|
+
}
|
|
5626
|
+
}
|
|
5627
|
+
async function fetchThreadContext(channel, threadId, opts = {}) {
|
|
5628
|
+
const limit = opts.limit ?? THREAD_WEAVE_MAX;
|
|
5629
|
+
const since = opts.sinceTime ?? 0;
|
|
5630
|
+
try {
|
|
5631
|
+
const res = await channel.rawClient.im.v1.message.list({
|
|
5632
|
+
params: {
|
|
5633
|
+
container_id_type: "thread",
|
|
5634
|
+
container_id: threadId,
|
|
5635
|
+
sort_type: "ByCreateTimeDesc",
|
|
5636
|
+
page_size: THREAD_PAGE_SIZE
|
|
5637
|
+
}
|
|
5638
|
+
});
|
|
5639
|
+
const items = res.data?.items ?? [];
|
|
5640
|
+
const picked = items.filter((it) => !it.deleted).map(toContextMessage).filter(
|
|
5641
|
+
(m) => m.fromUser && // drop the bot's own replies, other apps, system notices
|
|
5642
|
+
m.messageId !== opts.excludeMessageId && // drop the triggering @ message
|
|
5643
|
+
(since === 0 || m.createTime > since) && // delta only for existing sessions
|
|
5644
|
+
m.text.trim().length > 0
|
|
5645
|
+
);
|
|
5646
|
+
picked.sort((a, b) => a.createTime - b.createTime);
|
|
5647
|
+
const out = picked.slice(-limit);
|
|
5648
|
+
if (picked.length > out.length) {
|
|
5649
|
+
log.info("intake", "thread-context-truncated", { threadId, kept: out.length, total: picked.length });
|
|
5650
|
+
}
|
|
5651
|
+
return out;
|
|
5652
|
+
} catch (err) {
|
|
5653
|
+
log.warn("intake", "thread-context-failed", { threadId, err: String(err) });
|
|
5654
|
+
return [];
|
|
5655
|
+
}
|
|
5656
|
+
}
|
|
5657
|
+
function toContextMessage(item) {
|
|
5658
|
+
const id = item.sender?.id ?? "";
|
|
5659
|
+
const name = item.sender?.sender_name || (id ? `\u7528\u6237${id.slice(-4)}` : "\u67D0\u4EBA");
|
|
5660
|
+
return {
|
|
5661
|
+
messageId: item.message_id ?? "",
|
|
5662
|
+
senderName: name,
|
|
5663
|
+
text: extractMessageText(item.msg_type, item.body?.content, item.mentions),
|
|
5664
|
+
fromUser: item.sender?.sender_type === "user",
|
|
5665
|
+
createTime: Number(item.create_time) || 0
|
|
5666
|
+
};
|
|
5667
|
+
}
|
|
5668
|
+
function extractMessageText(msgType, content, mentions) {
|
|
5669
|
+
if (!content) return placeholderFor(msgType);
|
|
5670
|
+
let parsed;
|
|
5671
|
+
try {
|
|
5672
|
+
parsed = JSON.parse(content);
|
|
5673
|
+
} catch {
|
|
5674
|
+
return placeholderFor(msgType);
|
|
5675
|
+
}
|
|
5676
|
+
switch (msgType) {
|
|
5677
|
+
case "text":
|
|
5678
|
+
return replaceMentions(parsed?.text ?? "", mentions);
|
|
5679
|
+
case "post":
|
|
5680
|
+
return replaceMentions(extractPostText(parsed), mentions);
|
|
5681
|
+
case "image":
|
|
5682
|
+
return "[\u56FE\u7247]";
|
|
5683
|
+
case "audio":
|
|
5684
|
+
return "[\u8BED\u97F3]";
|
|
5685
|
+
case "media":
|
|
5686
|
+
return "[\u89C6\u9891]";
|
|
5687
|
+
case "file": {
|
|
5688
|
+
const name = parsed?.file_name;
|
|
5689
|
+
return name ? `[\u6587\u4EF6\uFF1A${name}]` : "[\u6587\u4EF6]";
|
|
5690
|
+
}
|
|
5691
|
+
case "sticker":
|
|
5692
|
+
return "[\u8868\u60C5]";
|
|
5693
|
+
case "interactive":
|
|
5694
|
+
return "[\u5361\u7247\u6D88\u606F]";
|
|
5695
|
+
case "share_chat":
|
|
5696
|
+
return "[\u5206\u4EAB\u7FA4\u540D\u7247]";
|
|
5697
|
+
case "share_user":
|
|
5698
|
+
return "[\u5206\u4EAB\u4E2A\u4EBA\u540D\u7247]";
|
|
5699
|
+
case "merge_forward":
|
|
5700
|
+
case "forward":
|
|
5701
|
+
return "[\u5408\u5E76\u8F6C\u53D1\u6D88\u606F]";
|
|
5702
|
+
default:
|
|
5703
|
+
return placeholderFor(msgType);
|
|
5704
|
+
}
|
|
5705
|
+
}
|
|
5706
|
+
function placeholderFor(msgType) {
|
|
5707
|
+
return msgType ? `[${msgType} \u6D88\u606F]` : "[\u6D88\u606F]";
|
|
5708
|
+
}
|
|
5709
|
+
function extractPostText(parsed) {
|
|
5710
|
+
if (!parsed || typeof parsed !== "object") return "";
|
|
5711
|
+
const obj = parsed;
|
|
5712
|
+
let title = obj.title;
|
|
5713
|
+
let blocks = obj.content;
|
|
5714
|
+
if (!Array.isArray(blocks)) {
|
|
5715
|
+
for (const v of Object.values(obj)) {
|
|
5716
|
+
if (v && typeof v === "object" && Array.isArray(v.content)) {
|
|
5717
|
+
title = v.title;
|
|
5718
|
+
blocks = v.content;
|
|
5719
|
+
break;
|
|
5720
|
+
}
|
|
5721
|
+
}
|
|
5722
|
+
}
|
|
5723
|
+
const parts = [];
|
|
5724
|
+
if (typeof title === "string" && title.trim()) parts.push(title.trim());
|
|
5725
|
+
if (Array.isArray(blocks)) {
|
|
5726
|
+
for (const line of blocks) {
|
|
5727
|
+
if (!Array.isArray(line)) continue;
|
|
5728
|
+
const lineText = line.map(nodeToText).join("");
|
|
5729
|
+
if (lineText) parts.push(lineText);
|
|
5730
|
+
}
|
|
5731
|
+
}
|
|
5732
|
+
return parts.join("\n");
|
|
5733
|
+
}
|
|
5734
|
+
function nodeToText(node) {
|
|
5735
|
+
if (!node || typeof node !== "object") return "";
|
|
5736
|
+
const n = node;
|
|
5737
|
+
switch (n.tag) {
|
|
5738
|
+
case "text":
|
|
5739
|
+
return typeof n.text === "string" ? n.text : "";
|
|
5740
|
+
case "a":
|
|
5741
|
+
return typeof n.text === "string" ? n.text : typeof n.href === "string" ? n.href : "";
|
|
5742
|
+
case "at": {
|
|
5743
|
+
const name = typeof n.user_name === "string" ? n.user_name : "";
|
|
5744
|
+
return name ? `@${name}` : "@\u67D0\u4EBA";
|
|
5745
|
+
}
|
|
5746
|
+
case "img":
|
|
5747
|
+
return "[\u56FE\u7247]";
|
|
5748
|
+
case "media":
|
|
5749
|
+
return "[\u89C6\u9891]";
|
|
5750
|
+
case "emotion":
|
|
5751
|
+
return "[\u8868\u60C5]";
|
|
5752
|
+
default:
|
|
5753
|
+
return typeof n.text === "string" ? n.text : "";
|
|
5754
|
+
}
|
|
5755
|
+
}
|
|
5756
|
+
function replaceMentions(text, mentions) {
|
|
5757
|
+
if (!text || !mentions?.length) return text;
|
|
5758
|
+
let out = text;
|
|
5759
|
+
for (const m of mentions) {
|
|
5760
|
+
if (!m.key) continue;
|
|
5761
|
+
out = out.split(m.key).join(m.name ? `@${m.name}` : "@\u67D0\u4EBA");
|
|
5762
|
+
}
|
|
5763
|
+
return out;
|
|
5764
|
+
}
|
|
5765
|
+
function sanitizeContext(s, maxLen, oneLine2) {
|
|
5766
|
+
if (!s) return "";
|
|
5767
|
+
let out = s.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "").replace(/\r\n?/g, "\n");
|
|
5768
|
+
out = oneLine2 ? out.replace(/\s+/g, " ") : out.replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n");
|
|
5769
|
+
out = out.trim();
|
|
5770
|
+
return out.length > maxLen ? `${out.slice(0, maxLen)}\u2026` : out;
|
|
5771
|
+
}
|
|
5772
|
+
function weaveQuote(text, quoted) {
|
|
5773
|
+
if (!quoted) return text;
|
|
5774
|
+
const who = sanitizeContext(quoted.senderName, 40, true) || "\u67D0\u4EBA";
|
|
5775
|
+
const body = sanitizeContext(quoted.text, QUOTE_MAX, true);
|
|
5776
|
+
if (!body) return text;
|
|
5777
|
+
const block = `[\u7528\u6237\u5F15\u7528\u4E86\u4E00\u6761\u6D88\u606F\uFF08\u6765\u81EA ${who}\uFF09\uFF1A
|
|
5778
|
+
${body}
|
|
5779
|
+
]`;
|
|
5780
|
+
const base = text.trim();
|
|
5781
|
+
return base ? `${block}
|
|
5782
|
+
|
|
5783
|
+
${base}` : block;
|
|
5784
|
+
}
|
|
5785
|
+
function weaveThreadHistory(text, msgs) {
|
|
5786
|
+
if (msgs.length === 0) return text;
|
|
5787
|
+
const lines = msgs.map((m) => {
|
|
5788
|
+
const who = sanitizeContext(m.senderName, 40, true) || "\u67D0\u4EBA";
|
|
5789
|
+
const body = sanitizeContext(m.text, LINE_MAX, true);
|
|
5790
|
+
return body ? `${who}\uFF1A${body}` : "";
|
|
5791
|
+
}).filter((l) => l.length > 0);
|
|
5792
|
+
if (lines.length === 0) return text;
|
|
5793
|
+
const block = `[\u8BDD\u9898\u4E2D\u5728\u6B64\u4E4B\u524D\u5DF2\u6709\u7684\u6D88\u606F\uFF08\u6309\u65F6\u95F4\u5148\u540E\u6392\u5217\uFF0C\u4F9B\u4F60\u7406\u89E3\u4E0A\u4E0B\u6587\uFF09\uFF1A
|
|
5794
|
+
${lines.join("\n")}
|
|
5795
|
+
]`;
|
|
5796
|
+
const base = text.trim();
|
|
5797
|
+
return base ? `${block}
|
|
5798
|
+
|
|
5799
|
+
${base}` : block;
|
|
5800
|
+
}
|
|
5801
|
+
|
|
5609
5802
|
// src/bot/comments.ts
|
|
5610
5803
|
var SUPPORTED_FILE_TYPES = /* @__PURE__ */ new Set(["doc", "docx", "sheet", "file"]);
|
|
5611
5804
|
var REPLY_MAX_CHARS = 2e3;
|
|
@@ -6091,20 +6284,26 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
|
|
|
6091
6284
|
const perm = turnPerm(project, senderId);
|
|
6092
6285
|
return { sessionKey: perm.roleSuffix ? `${baseKey}#${perm.roleSuffix}` : baseKey, ...perm };
|
|
6093
6286
|
}
|
|
6094
|
-
async function
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6287
|
+
async function ingestContext(msg, text) {
|
|
6288
|
+
let body = text;
|
|
6289
|
+
if (messageHasFiles(msg)) {
|
|
6290
|
+
const files = await collectInboundFiles(channel, msg);
|
|
6291
|
+
body = weaveFileManifest(text, files);
|
|
6292
|
+
if (!body.trim()) {
|
|
6293
|
+
body = "\u7528\u6237\u53D1\u6765\u4E00\u4E2A\u9644\u4EF6\uFF0C\u4F46\u6865\u6CA1\u80FD\u4E0B\u8F7D\u5B83\uFF08\u53EF\u80FD\u8D85\u8FC7 50MB \u4E0A\u9650\u6216\u88AB\u98DE\u4E66\u62D2\u7EDD\uFF09\u3002\u8BF7\u544A\u8BC9\u7528\u6237\u9644\u4EF6\u6CA1\u8BFB\u5230\uFF0C\u53EF\u4EE5\u91CD\u53D1\uFF0C\u6216\u6539\u4E3A\u7C98\u8D34\u6587\u672C / \u53D1\u56FE\u7247\u3002";
|
|
6294
|
+
}
|
|
6295
|
+
}
|
|
6296
|
+
if (msg.replyToMessageId) {
|
|
6297
|
+
const quoted = await fetchQuotedMessage(channel, msg.replyToMessageId);
|
|
6298
|
+
body = weaveQuote(body, quoted);
|
|
6100
6299
|
}
|
|
6101
|
-
return
|
|
6300
|
+
return body;
|
|
6102
6301
|
}
|
|
6103
6302
|
async function handleTurn(msg, text, sessionKey, flat, project, perm) {
|
|
6104
6303
|
const existing = active.get(sessionKey);
|
|
6105
6304
|
if (existing) {
|
|
6106
6305
|
const images = messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0;
|
|
6107
|
-
const woven = await
|
|
6306
|
+
const woven = await ingestContext(msg, text);
|
|
6108
6307
|
const cur = active.get(sessionKey);
|
|
6109
6308
|
if (!cur) {
|
|
6110
6309
|
startReservedRun(msg, woven, sessionKey, flat, project, perm, images, true, text);
|
|
@@ -6141,8 +6340,15 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
|
|
|
6141
6340
|
const reaction = runReaction(msg.messageId, !sema.hasFree());
|
|
6142
6341
|
try {
|
|
6143
6342
|
const images = preloadedImages ?? (messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0);
|
|
6144
|
-
|
|
6145
|
-
|
|
6343
|
+
let firstText = preIngested ? text : await ingestContext(msg, text);
|
|
6344
|
+
const { thread: resolved, recreated } = await resolveThread(sessionKey, msg.chatId, {
|
|
6345
|
+
mode: perm.mode,
|
|
6346
|
+
network: perm.network
|
|
6347
|
+
});
|
|
6348
|
+
let thread = resolved;
|
|
6349
|
+
const neverSeen = !thread;
|
|
6350
|
+
const codexEmpty = neverSeen || recreated;
|
|
6351
|
+
const prior = neverSeen ? void 0 : await getSession(sessionKey);
|
|
6146
6352
|
if (!thread) {
|
|
6147
6353
|
const cwd = project?.cwd ?? fallbackCwd;
|
|
6148
6354
|
thread = await backend.startThread({ cwd, mode: perm.mode, network: perm.network });
|
|
@@ -6156,10 +6362,19 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
|
|
|
6156
6362
|
// `summaryText` (handleTurn's original) so the session label isn't
|
|
6157
6363
|
// manifest boilerplate + a temp path.
|
|
6158
6364
|
summary: stripFileTokens(summaryText2 ?? text).slice(0, 80),
|
|
6365
|
+
lastSeenAt: msg.createTime,
|
|
6159
6366
|
createdAt: Date.now(),
|
|
6160
6367
|
updatedAt: Date.now()
|
|
6161
6368
|
});
|
|
6162
6369
|
}
|
|
6370
|
+
if (msg.threadId && (codexEmpty || prior?.lastSeenAt !== void 0)) {
|
|
6371
|
+
const history = await fetchThreadContext(channel, msg.threadId, {
|
|
6372
|
+
sinceTime: codexEmpty ? 0 : prior?.lastSeenAt ?? 0,
|
|
6373
|
+
excludeMessageId: msg.messageId
|
|
6374
|
+
});
|
|
6375
|
+
firstText = weaveThreadHistory(firstText, history);
|
|
6376
|
+
}
|
|
6377
|
+
if (!neverSeen) void patchSession(sessionKey, { lastSeenAt: msg.createTime }).catch(() => void 0);
|
|
6163
6378
|
reserved.thread = thread;
|
|
6164
6379
|
await launchRun(
|
|
6165
6380
|
{
|
|
@@ -6185,9 +6400,9 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
|
|
|
6185
6400
|
}
|
|
6186
6401
|
async function resolveThread(threadId, chatId, perm) {
|
|
6187
6402
|
const live = sessions.get(threadId);
|
|
6188
|
-
if (live) return live;
|
|
6403
|
+
if (live) return { thread: live, recreated: false };
|
|
6189
6404
|
const rec = await getSession(threadId);
|
|
6190
|
-
if (!rec) return void 0;
|
|
6405
|
+
if (!rec) return { thread: void 0, recreated: false };
|
|
6191
6406
|
try {
|
|
6192
6407
|
const resumed = await backend.resumeThread({
|
|
6193
6408
|
cwd: rec.cwd,
|
|
@@ -6198,7 +6413,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
|
|
|
6198
6413
|
network: perm?.network
|
|
6199
6414
|
});
|
|
6200
6415
|
sessions.set(threadId, resumed);
|
|
6201
|
-
return resumed;
|
|
6416
|
+
return { thread: resumed, recreated: false };
|
|
6202
6417
|
} catch (err) {
|
|
6203
6418
|
log.fail("agent", err, { phase: "resume-on-turn", threadId });
|
|
6204
6419
|
const project = await getProjectByChatId(chatId);
|
|
@@ -6211,7 +6426,8 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
|
|
|
6211
6426
|
network: perm?.network ?? project?.network
|
|
6212
6427
|
});
|
|
6213
6428
|
sessions.set(threadId, fresh);
|
|
6214
|
-
|
|
6429
|
+
await patchSession(threadId, { codexThreadId: fresh.codexThreadId }).catch(() => void 0);
|
|
6430
|
+
return { thread: fresh, recreated: true };
|
|
6215
6431
|
}
|
|
6216
6432
|
}
|
|
6217
6433
|
async function evictLiveSessionsForChat(chatId) {
|
|
@@ -6243,7 +6459,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
|
|
|
6243
6459
|
return;
|
|
6244
6460
|
}
|
|
6245
6461
|
const images = messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0;
|
|
6246
|
-
const firstText = await
|
|
6462
|
+
const firstText = await ingestContext(msg, text) || "\u4F60\u597D\uFF0C\u6211\u4EEC\u5F00\u59CB\u5427\u3002";
|
|
6247
6463
|
log.info("card", "start", { project: project?.name ?? "(unregistered)", model, effort, images: images?.length ?? 0 });
|
|
6248
6464
|
await launchRun(
|
|
6249
6465
|
{
|