@modelzen/feishu-codex-bridge 0.3.4 → 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/README.md +1 -0
- package/dist/cli.js +329 -18
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -165,6 +165,7 @@ feishu-codex-bridge bot rm <名> # 移除一个机器人配置
|
|
|
165
165
|
- **💬 单会话群**:整群就是**一条连续会话**(全程**免 @**、消息按序排队、无 `/resume`)。适合**个人单线深入**、像私聊一样直接聊。
|
|
166
166
|
- **干活**:在项目群里 **@机器人** 描述需求;机器人在该群绑定的目录里跑 Codex,流式卡片回结果。
|
|
167
167
|
- **话题 = 会话**:对某条消息开话题后,话题内可**免 @** 连续对话,是一条连贯的 Codex 会话。
|
|
168
|
+
- **发图 / 发附件**:直接在消息里**发图片**,Codex 能看到(多模态读图);**发文件附件**(日志 / PDF / 代码等),桥会把它下载到本地并把**绝对路径**告诉 Codex,让它用工具直接打开分析。⚠️ 附件落在桥的全局临时目录(`~/.feishu-codex-bridge/inbound`,1h 后自动清),**只有「完全访问」档**能读到——「项目内只读 / 读写」档的沙箱把读取锁在项目目录内,读不到该目录。单文件上限 50MB、单条消息最多 9 个;合并转发里的附件飞书官方不支持取,故不支持。
|
|
168
169
|
- **文档评论 @机器人**:在飞书文档评论里 @ 它就回(前提:已开通文档评论权限 + 订阅 `drive.notice.comment_add_v1`,且机器人对该文档有访问权限)。只支持 doc/docx/sheet/file;评论框不渲染 markdown,回复为纯文本,超长会截断。
|
|
169
170
|
- **终止**:卡片上的 **⏹** 随时终止当前轮;卡死超过 watchdog 阈值(默认 120s)自动中止并回收进程。
|
|
170
171
|
- **私聊控制台**:项目列表、设置(模型 / 推理强度 / 免 @ / watchdog / 管理员)、用量、诊断、重连,全在私聊菜单里。
|
package/dist/cli.js
CHANGED
|
@@ -68,7 +68,10 @@ var paths = {
|
|
|
68
68
|
* passes lark-cli's AssertSecurePath audit.
|
|
69
69
|
*/
|
|
70
70
|
secretsGetterScript: join(appDir, "secrets-getter"),
|
|
71
|
-
mediaDir: join(appDir, "media")
|
|
71
|
+
mediaDir: join(appDir, "media"),
|
|
72
|
+
/** Inbound file attachments downloaded from chat, handed to codex by absolute
|
|
73
|
+
* path (codex has no native file input). TTL-pruned like {@link mediaDir}. */
|
|
74
|
+
inboundDir: join(appDir, "inbound")
|
|
72
75
|
};
|
|
73
76
|
|
|
74
77
|
// src/config/bots.ts
|
|
@@ -5404,7 +5407,7 @@ async function collectInboundImages(channel, msg) {
|
|
|
5404
5407
|
return [];
|
|
5405
5408
|
}
|
|
5406
5409
|
if (refs.length === 0) return [];
|
|
5407
|
-
await pruneOldMedia();
|
|
5410
|
+
await pruneOldMedia(paths.mediaDir);
|
|
5408
5411
|
try {
|
|
5409
5412
|
await mkdir11(paths.mediaDir, { recursive: true });
|
|
5410
5413
|
} catch {
|
|
@@ -5507,16 +5510,16 @@ function readHeader(headers, name) {
|
|
|
5507
5510
|
function safeName(fileKey) {
|
|
5508
5511
|
return fileKey.replace(/[^a-zA-Z0-9_-]/g, "").slice(-40) || "img";
|
|
5509
5512
|
}
|
|
5510
|
-
async function pruneOldMedia() {
|
|
5513
|
+
async function pruneOldMedia(dir) {
|
|
5511
5514
|
let entries;
|
|
5512
5515
|
try {
|
|
5513
|
-
entries = await readdir2(
|
|
5516
|
+
entries = await readdir2(dir);
|
|
5514
5517
|
} catch {
|
|
5515
5518
|
return;
|
|
5516
5519
|
}
|
|
5517
5520
|
const cutoff = Date.now() - MEDIA_TTL_MS;
|
|
5518
5521
|
for (const name of entries) {
|
|
5519
|
-
const file = join14(
|
|
5522
|
+
const file = join14(dir, name);
|
|
5520
5523
|
try {
|
|
5521
5524
|
const st = await stat3(file);
|
|
5522
5525
|
if (st.mtimeMs < cutoff) await rm4(file, { force: true });
|
|
@@ -5524,6 +5527,277 @@ async function pruneOldMedia() {
|
|
|
5524
5527
|
}
|
|
5525
5528
|
}
|
|
5526
5529
|
}
|
|
5530
|
+
var MAX_FILES = 9;
|
|
5531
|
+
var MAX_FILE_BYTES = 50 * 1024 * 1024;
|
|
5532
|
+
function messageHasFiles(msg) {
|
|
5533
|
+
return (msg.resources ?? []).some((r) => r.type === "file");
|
|
5534
|
+
}
|
|
5535
|
+
async function collectInboundFiles(channel, msg) {
|
|
5536
|
+
const refs = [];
|
|
5537
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5538
|
+
for (const r of msg.resources ?? []) {
|
|
5539
|
+
if (r.type === "file" && r.fileKey && !seen.has(r.fileKey)) {
|
|
5540
|
+
seen.add(r.fileKey);
|
|
5541
|
+
refs.push({ messageId: msg.messageId, fileKey: r.fileKey, fileName: r.fileName });
|
|
5542
|
+
}
|
|
5543
|
+
}
|
|
5544
|
+
if (refs.length === 0) return [];
|
|
5545
|
+
await pruneOldMedia(paths.inboundDir);
|
|
5546
|
+
try {
|
|
5547
|
+
await mkdir11(paths.inboundDir, { recursive: true });
|
|
5548
|
+
} catch {
|
|
5549
|
+
}
|
|
5550
|
+
const out = [];
|
|
5551
|
+
for (const ref of refs.slice(0, MAX_FILES)) {
|
|
5552
|
+
const f = await downloadOneFile(channel, ref);
|
|
5553
|
+
if (f) out.push(f);
|
|
5554
|
+
}
|
|
5555
|
+
log.info("intake", "files", { found: refs.length, downloaded: out.length });
|
|
5556
|
+
return out;
|
|
5557
|
+
}
|
|
5558
|
+
async function downloadOneFile(channel, ref) {
|
|
5559
|
+
try {
|
|
5560
|
+
const res = await channel.rawClient.im.v1.messageResource.get({
|
|
5561
|
+
path: { message_id: ref.messageId, file_key: ref.fileKey },
|
|
5562
|
+
params: { type: "file" }
|
|
5563
|
+
});
|
|
5564
|
+
const declared = Number(readHeader(res.headers, "content-length"));
|
|
5565
|
+
if (Number.isFinite(declared) && declared > MAX_FILE_BYTES) {
|
|
5566
|
+
log.warn("intake", "file-too-large", { fileKey: ref.fileKey.slice(0, 24), bytes: declared });
|
|
5567
|
+
return void 0;
|
|
5568
|
+
}
|
|
5569
|
+
const name = cleanFileName(ref.fileName) || "attachment";
|
|
5570
|
+
const onDisk = `${safeName(ref.fileKey)}-${name}`;
|
|
5571
|
+
const file = join14(paths.inboundDir, onDisk);
|
|
5572
|
+
await res.writeFile(file);
|
|
5573
|
+
try {
|
|
5574
|
+
const st = await stat3(file);
|
|
5575
|
+
if (st.size > MAX_FILE_BYTES) {
|
|
5576
|
+
await rm4(file, { force: true });
|
|
5577
|
+
log.warn("intake", "file-too-large", { fileKey: ref.fileKey.slice(0, 24), bytes: st.size });
|
|
5578
|
+
return void 0;
|
|
5579
|
+
}
|
|
5580
|
+
} catch {
|
|
5581
|
+
}
|
|
5582
|
+
return { path: file, name };
|
|
5583
|
+
} catch (err) {
|
|
5584
|
+
log.warn("intake", "file-download-failed", { fileKey: ref.fileKey.slice(0, 24), err: String(err) });
|
|
5585
|
+
return void 0;
|
|
5586
|
+
}
|
|
5587
|
+
}
|
|
5588
|
+
function cleanFileName(name) {
|
|
5589
|
+
if (!name) return "";
|
|
5590
|
+
const base = name.split(/[/\\]/).pop() ?? name;
|
|
5591
|
+
const cleaned = base.replace(/[\x00-\x1f<>:"|?*]/g, "_").replace(/\s+/g, " ").trim().slice(0, 100);
|
|
5592
|
+
return cleaned === "." || cleaned === ".." ? "" : cleaned;
|
|
5593
|
+
}
|
|
5594
|
+
function stripFileTokens(text) {
|
|
5595
|
+
return text.replace(/<file\b[^<]*\/>/g, "").replace(/[ \t]+\n/g, "\n").trim();
|
|
5596
|
+
}
|
|
5597
|
+
function weaveFileManifest(text, files) {
|
|
5598
|
+
const stripped = stripFileTokens(text);
|
|
5599
|
+
if (files.length === 0) return stripped;
|
|
5600
|
+
const lines = files.map((f) => `- ${f.name} \u2192 ${f.path}`).join("\n");
|
|
5601
|
+
const head = stripped ? `${stripped}
|
|
5602
|
+
|
|
5603
|
+
` : "";
|
|
5604
|
+
return `${head}[\u7528\u6237\u4E0A\u4F20\u4E86 ${files.length} \u4E2A\u9644\u4EF6\uFF0C\u5DF2\u4FDD\u5B58\u5230\u672C\u5730\uFF0C\u53EF\u7528 shell / \u8BFB\u53D6\u5DE5\u5177\u6309\u4E0B\u9762\u7684\u7EDD\u5BF9\u8DEF\u5F84\u76F4\u63A5\u6253\u5F00\uFF1A
|
|
5605
|
+
${lines}
|
|
5606
|
+
]`;
|
|
5607
|
+
}
|
|
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
|
+
}
|
|
5527
5801
|
|
|
5528
5802
|
// src/bot/comments.ts
|
|
5529
5803
|
var SUPPORTED_FILE_TYPES = /* @__PURE__ */ new Set(["doc", "docx", "sheet", "file"]);
|
|
@@ -6010,20 +6284,36 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
|
|
|
6010
6284
|
const perm = turnPerm(project, senderId);
|
|
6011
6285
|
return { sessionKey: perm.roleSuffix ? `${baseKey}#${perm.roleSuffix}` : baseKey, ...perm };
|
|
6012
6286
|
}
|
|
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);
|
|
6299
|
+
}
|
|
6300
|
+
return body;
|
|
6301
|
+
}
|
|
6013
6302
|
async function handleTurn(msg, text, sessionKey, flat, project, perm) {
|
|
6014
6303
|
const existing = active.get(sessionKey);
|
|
6015
6304
|
if (existing) {
|
|
6016
6305
|
const images = messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0;
|
|
6306
|
+
const woven = await ingestContext(msg, text);
|
|
6017
6307
|
const cur = active.get(sessionKey);
|
|
6018
6308
|
if (!cur) {
|
|
6019
|
-
startReservedRun(msg,
|
|
6309
|
+
startReservedRun(msg, woven, sessionKey, flat, project, perm, images, true, text);
|
|
6020
6310
|
return;
|
|
6021
6311
|
}
|
|
6022
6312
|
if (getPendingPolicy(cfg) === "steer" && cur.run && cur.thread) {
|
|
6023
6313
|
const tid = cur.run.turnId();
|
|
6024
6314
|
if (tid) {
|
|
6025
6315
|
try {
|
|
6026
|
-
await cur.thread.steer({ text, images }, tid);
|
|
6316
|
+
await cur.thread.steer({ text: woven, images }, tid);
|
|
6027
6317
|
log.info("intake", "steer", { tid, images: images?.length ?? 0 });
|
|
6028
6318
|
return;
|
|
6029
6319
|
} catch (err) {
|
|
@@ -6031,13 +6321,13 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
|
|
|
6031
6321
|
}
|
|
6032
6322
|
}
|
|
6033
6323
|
}
|
|
6034
|
-
cur.queue.push({ text, images });
|
|
6324
|
+
cur.queue.push({ text: woven, images });
|
|
6035
6325
|
log.info("intake", "queued", { depth: cur.queue.length });
|
|
6036
6326
|
return;
|
|
6037
6327
|
}
|
|
6038
6328
|
startReservedRun(msg, text, sessionKey, flat, project, perm);
|
|
6039
6329
|
}
|
|
6040
|
-
function startReservedRun(msg, text, sessionKey, flat, project, perm, preloadedImages) {
|
|
6330
|
+
function startReservedRun(msg, text, sessionKey, flat, project, perm, preloadedImages, preIngested, summaryText2) {
|
|
6041
6331
|
const existing = active.get(sessionKey);
|
|
6042
6332
|
if (existing) {
|
|
6043
6333
|
existing.queue.push({ text, images: preloadedImages });
|
|
@@ -6050,7 +6340,15 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
|
|
|
6050
6340
|
const reaction = runReaction(msg.messageId, !sema.hasFree());
|
|
6051
6341
|
try {
|
|
6052
6342
|
const images = preloadedImages ?? (messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0);
|
|
6053
|
-
let
|
|
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);
|
|
6054
6352
|
if (!thread) {
|
|
6055
6353
|
const cwd = project?.cwd ?? fallbackCwd;
|
|
6056
6354
|
thread = await backend.startThread({ cwd, mode: perm.mode, network: perm.network });
|
|
@@ -6060,11 +6358,23 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
|
|
|
6060
6358
|
chatId: msg.chatId,
|
|
6061
6359
|
cwd,
|
|
6062
6360
|
codexThreadId: thread.codexThreadId,
|
|
6063
|
-
|
|
6361
|
+
// `text` is already file-woven when preIngested; use the raw
|
|
6362
|
+
// `summaryText` (handleTurn's original) so the session label isn't
|
|
6363
|
+
// manifest boilerplate + a temp path.
|
|
6364
|
+
summary: stripFileTokens(summaryText2 ?? text).slice(0, 80),
|
|
6365
|
+
lastSeenAt: msg.createTime,
|
|
6064
6366
|
createdAt: Date.now(),
|
|
6065
6367
|
updatedAt: Date.now()
|
|
6066
6368
|
});
|
|
6067
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);
|
|
6068
6378
|
reserved.thread = thread;
|
|
6069
6379
|
await launchRun(
|
|
6070
6380
|
{
|
|
@@ -6073,7 +6383,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
|
|
|
6073
6383
|
replyInThread: !flat,
|
|
6074
6384
|
flat,
|
|
6075
6385
|
thread,
|
|
6076
|
-
firstText
|
|
6386
|
+
firstText,
|
|
6077
6387
|
images,
|
|
6078
6388
|
knownThreadId: sessionKey,
|
|
6079
6389
|
requesterOpenId: msg.senderId
|
|
@@ -6090,9 +6400,9 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
|
|
|
6090
6400
|
}
|
|
6091
6401
|
async function resolveThread(threadId, chatId, perm) {
|
|
6092
6402
|
const live = sessions.get(threadId);
|
|
6093
|
-
if (live) return live;
|
|
6403
|
+
if (live) return { thread: live, recreated: false };
|
|
6094
6404
|
const rec = await getSession(threadId);
|
|
6095
|
-
if (!rec) return void 0;
|
|
6405
|
+
if (!rec) return { thread: void 0, recreated: false };
|
|
6096
6406
|
try {
|
|
6097
6407
|
const resumed = await backend.resumeThread({
|
|
6098
6408
|
cwd: rec.cwd,
|
|
@@ -6103,7 +6413,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
|
|
|
6103
6413
|
network: perm?.network
|
|
6104
6414
|
});
|
|
6105
6415
|
sessions.set(threadId, resumed);
|
|
6106
|
-
return resumed;
|
|
6416
|
+
return { thread: resumed, recreated: false };
|
|
6107
6417
|
} catch (err) {
|
|
6108
6418
|
log.fail("agent", err, { phase: "resume-on-turn", threadId });
|
|
6109
6419
|
const project = await getProjectByChatId(chatId);
|
|
@@ -6116,7 +6426,8 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
|
|
|
6116
6426
|
network: perm?.network ?? project?.network
|
|
6117
6427
|
});
|
|
6118
6428
|
sessions.set(threadId, fresh);
|
|
6119
|
-
|
|
6429
|
+
await patchSession(threadId, { codexThreadId: fresh.codexThreadId }).catch(() => void 0);
|
|
6430
|
+
return { thread: fresh, recreated: true };
|
|
6120
6431
|
}
|
|
6121
6432
|
}
|
|
6122
6433
|
async function evictLiveSessionsForChat(chatId) {
|
|
@@ -6147,8 +6458,8 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
|
|
|
6147
6458
|
await channel.send(msg.chatId, { markdown: `\u274C \u542F\u52A8\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}` }, { replyTo: msg.messageId }).catch(() => void 0);
|
|
6148
6459
|
return;
|
|
6149
6460
|
}
|
|
6150
|
-
const firstText = text || "\u4F60\u597D\uFF0C\u6211\u4EEC\u5F00\u59CB\u5427\u3002";
|
|
6151
6461
|
const images = messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0;
|
|
6462
|
+
const firstText = await ingestContext(msg, text) || "\u4F60\u597D\uFF0C\u6211\u4EEC\u5F00\u59CB\u5427\u3002";
|
|
6152
6463
|
log.info("card", "start", { project: project?.name ?? "(unregistered)", model, effort, images: images?.length ?? 0 });
|
|
6153
6464
|
await launchRun(
|
|
6154
6465
|
{
|
|
@@ -6161,7 +6472,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
|
|
|
6161
6472
|
model,
|
|
6162
6473
|
effort,
|
|
6163
6474
|
cwd,
|
|
6164
|
-
summary: text.slice(0, 80) || "(\u7A7A)",
|
|
6475
|
+
summary: stripFileTokens(text).slice(0, 80) || "(\u7A7A)",
|
|
6165
6476
|
requesterOpenId: msg.senderId,
|
|
6166
6477
|
roleSuffix: perm.roleSuffix
|
|
6167
6478
|
},
|
package/dist/index.d.ts
CHANGED
|
@@ -42,6 +42,9 @@ declare const paths: {
|
|
|
42
42
|
*/
|
|
43
43
|
secretsGetterScript: string;
|
|
44
44
|
mediaDir: string;
|
|
45
|
+
/** Inbound file attachments downloaded from chat, handed to codex by absolute
|
|
46
|
+
* path (codex has no native file input). TTL-pruned like {@link mediaDir}. */
|
|
47
|
+
inboundDir: string;
|
|
45
48
|
};
|
|
46
49
|
|
|
47
50
|
export { log, newTraceId, paths, withTrace };
|
package/dist/index.js
CHANGED
|
@@ -47,7 +47,10 @@ var paths = {
|
|
|
47
47
|
* passes lark-cli's AssertSecurePath audit.
|
|
48
48
|
*/
|
|
49
49
|
secretsGetterScript: join(appDir, "secrets-getter"),
|
|
50
|
-
mediaDir: join(appDir, "media")
|
|
50
|
+
mediaDir: join(appDir, "media"),
|
|
51
|
+
/** Inbound file attachments downloaded from chat, handed to codex by absolute
|
|
52
|
+
* path (codex has no native file input). TTL-pruned like {@link mediaDir}. */
|
|
53
|
+
inboundDir: join(appDir, "inbound")
|
|
51
54
|
};
|
|
52
55
|
|
|
53
56
|
// src/core/logger.ts
|