@ycodium-ai/plugin-im 0.2.2570 → 0.2.2588
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 +31 -15
- package/dist/index.js +370 -17
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -30,18 +30,32 @@ is HTTP long-polling.
|
|
|
30
30
|
The default is one message per finished turn. A single turn can produce dozens
|
|
31
31
|
of assistant messages, mostly commentary between tool calls.
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
33
|
+
The reply to a thread this plugin opened is written as the agent works
|
|
34
|
+
(`thread.turn-progress` ticks, reply text read with `getThreadMessage`) and
|
|
35
|
+
settled when the turn ends; threads opened elsewhere still get the per-turn or
|
|
36
|
+
per-message text. How depends on the platform:
|
|
37
|
+
|
|
38
|
+
- **Lark** patches one card.
|
|
39
|
+
- **Telegram** edits one message (`editMessageText`, plain text) and, when the
|
|
40
|
+
reply outgrows a single message, freezes it and continues in the next one.
|
|
41
|
+
A `message is not modified` answer counts as success; a 429 waits the
|
|
42
|
+
`retry_after` it names, once.
|
|
43
|
+
- **WeCom** keeps sending frames of one `stream.id` (`finish: false`) on the
|
|
44
|
+
inbound message's `req_id` and closes with `finish: true`. With no inbound
|
|
45
|
+
message to reply to, or after the platform rejects the stream, the settled text
|
|
46
|
+
goes out as ordinary messages instead.
|
|
47
|
+
|
|
48
|
+
An adapter opts in with the optional `turnCards` surface (`src/adapter.ts`).
|
|
49
|
+
Approval messages are posted for every pending request on platforms with
|
|
50
|
+
buttons and replaced with a settled notice when the request is answered, here or
|
|
51
|
+
elsewhere (`thread.request-resolved`); WeCom, which answers by number, posts only
|
|
52
|
+
the first and just stops accepting its number. Cards that were still open when the
|
|
53
|
+
plugin restarted are marked interrupted. The pieces are pure and separate:
|
|
54
|
+
`turnView.ts` (ticks to a view model), `larkCards.ts` (view model to card JSON),
|
|
55
|
+
`larkMarkdown.ts`, `larkCardStream.ts` (throttle, one update in flight, newest
|
|
56
|
+
snapshot wins), `turnCards.ts` (per-thread queue and lifecycle),
|
|
57
|
+
`plainTurnText.ts` (view model to plain text), `telegramTurn.ts` (message
|
|
58
|
+
rollover).
|
|
45
59
|
|
|
46
60
|
A group keeps its place across restarts. Which thread a group is talking in, the
|
|
47
61
|
team it is pointed at, the model and plan mode set from the group, the threads
|
|
@@ -55,9 +69,11 @@ Chat commands: `/stop`, `/status`, `/new`, `/sessions`, `/resume`, `/model`,
|
|
|
55
69
|
switch threads this plugin opened, since the host lets a plugin send only into
|
|
56
70
|
its own threads.
|
|
57
71
|
|
|
58
|
-
On Lark, `/panel` sends a command panel
|
|
59
|
-
is a button, and the ones that take an argument (a thread, a team, a model) open
|
|
60
|
-
a second view on the same
|
|
72
|
+
On Lark and Telegram, `/panel` sends a command panel (`/help` stays text and ends with a pointer to it; on WeCom, which has no panel, it does not): every command above
|
|
73
|
+
is a button (a card on Lark, an inline keyboard on Telegram), and the ones that take an argument (a thread, a team, a model) open
|
|
74
|
+
a second view on the same message with a Back button. Telegram buttons carry only
|
|
75
|
+
a short `pn:<version>:<index>` token (`callback_data` is capped at 64 bytes);
|
|
76
|
+
the action stays in memory and a tap on an older keyboard version counts as expired. Each card keeps its own view
|
|
61
77
|
stack in memory (`src/panelController.ts`), so after a restart an old card only
|
|
62
78
|
answers that it has expired. A tap runs the same handler as the typed command and
|
|
63
79
|
waits in the same per-group queue as typed messages. Another platform opts in by
|
package/dist/index.js
CHANGED
|
@@ -133399,12 +133399,16 @@ var POLL_TIMEOUT_SECONDS = 45;
|
|
|
133399
133399
|
var TELEGRAM_TEXT_MAX_CHARS = 4e3;
|
|
133400
133400
|
var TelegramApiError = class extends Error {
|
|
133401
133401
|
code;
|
|
133402
|
-
|
|
133402
|
+
/** 限流(429)时平台给的等待秒数;别的错误没有。 */
|
|
133403
|
+
retryAfterSeconds;
|
|
133404
|
+
constructor(method, code2, description, retryAfterSeconds) {
|
|
133403
133405
|
super(`telegram ${method} failed (${String(code2 ?? "?")}): ${description}`);
|
|
133404
133406
|
this.name = "TelegramApiError";
|
|
133405
133407
|
this.code = code2;
|
|
133408
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
133406
133409
|
}
|
|
133407
133410
|
};
|
|
133411
|
+
var isNotModified = (error) => error instanceof TelegramApiError && error.code === 400 && error.message.toLowerCase().includes("message is not modified");
|
|
133408
133412
|
var PHOTO_NAME = "photo.jpg";
|
|
133409
133413
|
var PHOTO_MIME = "image/jpeg";
|
|
133410
133414
|
var mimeFromName2 = (name) => {
|
|
@@ -133463,10 +133467,20 @@ var makeTelegramApi = (input) => {
|
|
|
133463
133467
|
});
|
|
133464
133468
|
const envelope = await response.json();
|
|
133465
133469
|
if (envelope.ok !== true) {
|
|
133466
|
-
throw new TelegramApiError(
|
|
133470
|
+
throw new TelegramApiError(
|
|
133471
|
+
method,
|
|
133472
|
+
envelope.error_code,
|
|
133473
|
+
envelope.description ?? "no reason",
|
|
133474
|
+
envelope.parameters?.retry_after
|
|
133475
|
+
);
|
|
133467
133476
|
}
|
|
133468
133477
|
return envelope.result;
|
|
133469
133478
|
};
|
|
133479
|
+
const keyboard = (rows) => ({
|
|
133480
|
+
inline_keyboard: rows.map(
|
|
133481
|
+
(row) => row.map((button) => ({ text: button.text, callback_data: button.data }))
|
|
133482
|
+
)
|
|
133483
|
+
});
|
|
133470
133484
|
return {
|
|
133471
133485
|
/** 机器人自己是谁;群里的命令靠它的 `username` 认出是不是发给自己的。 */
|
|
133472
133486
|
getMe: () => call("getMe", {}),
|
|
@@ -133486,16 +133500,45 @@ var makeTelegramApi = (input) => {
|
|
|
133486
133500
|
await call("sendMessage", { chat_id: chatId, text: chunk });
|
|
133487
133501
|
}
|
|
133488
133502
|
},
|
|
133489
|
-
/**
|
|
133490
|
-
|
|
133491
|
-
|
|
133492
|
-
|
|
133493
|
-
|
|
133494
|
-
|
|
133495
|
-
|
|
133496
|
-
|
|
133503
|
+
/**
|
|
133504
|
+
* 发一条消息,回它的 id。调用方保证 `text` 不超过单条上限(边写边更新的回复要自己
|
|
133505
|
+
* 在上限处翻篇,这里不替它切)。带 `rows` 就挂一块内联键盘。
|
|
133506
|
+
*/
|
|
133507
|
+
sendOne: async (chatId, text2, rows) => {
|
|
133508
|
+
const sent = await call("sendMessage", {
|
|
133509
|
+
chat_id: chatId,
|
|
133510
|
+
text: text2,
|
|
133511
|
+
...rows === void 0 ? {} : { reply_markup: keyboard(rows) }
|
|
133512
|
+
});
|
|
133513
|
+
return sent?.message_id === void 0 ? null : String(sent.message_id);
|
|
133514
|
+
},
|
|
133515
|
+
/**
|
|
133516
|
+
* 改一条已发出的消息。内容没变(`message is not modified`)当成功。`rows` 不给就是
|
|
133517
|
+
* 去掉按钮:改了文字又要保留按钮的调用方得自己再传一次。
|
|
133518
|
+
*/
|
|
133519
|
+
editMessage: async (chatId, messageId, text2, rows = []) => {
|
|
133520
|
+
try {
|
|
133521
|
+
await call("editMessageText", {
|
|
133522
|
+
chat_id: chatId,
|
|
133523
|
+
message_id: Number(messageId),
|
|
133524
|
+
text: text2,
|
|
133525
|
+
reply_markup: keyboard(rows)
|
|
133526
|
+
});
|
|
133527
|
+
} catch (error) {
|
|
133528
|
+
if (!isNotModified(error)) throw error;
|
|
133497
133529
|
}
|
|
133498
|
-
}
|
|
133530
|
+
},
|
|
133531
|
+
/** 带按钮的审批消息,一行一个按钮;回消息 id,之后才改得了它(平台没给就是 null)。 */
|
|
133532
|
+
sendButtons: async (chatId, text2, buttons) => {
|
|
133533
|
+
const sent = await call("sendMessage", {
|
|
133534
|
+
chat_id: chatId,
|
|
133535
|
+
text: text2,
|
|
133536
|
+
reply_markup: keyboard(
|
|
133537
|
+
buttons.map((button) => [{ text: button.label, data: button.token }])
|
|
133538
|
+
)
|
|
133539
|
+
});
|
|
133540
|
+
return sent?.message_id === void 0 ? null : String(sent.message_id);
|
|
133541
|
+
},
|
|
133499
133542
|
/** 答完点击,不然客户端上那个按钮一直转圈。带 `text` 时点击者会看到一句提示。 */
|
|
133500
133543
|
answerCallback: (callbackQueryId, text2) => call("answerCallbackQuery", {
|
|
133501
133544
|
callback_query_id: callbackQueryId,
|
|
@@ -133536,16 +133579,168 @@ var makeTelegramApi = (input) => {
|
|
|
133536
133579
|
};
|
|
133537
133580
|
};
|
|
133538
133581
|
|
|
133582
|
+
// src/telegramPanel.ts
|
|
133583
|
+
var PANEL_DATA_PREFIX = "pn:";
|
|
133584
|
+
var renderPanel = (card, version) => {
|
|
133585
|
+
const actions = [];
|
|
133586
|
+
const rowOf = (buttons) => buttons.map((button) => {
|
|
133587
|
+
actions.push(button.action);
|
|
133588
|
+
return {
|
|
133589
|
+
text: button.label,
|
|
133590
|
+
data: `${PANEL_DATA_PREFIX}${String(version)}:${String(actions.length - 1)}`
|
|
133591
|
+
};
|
|
133592
|
+
});
|
|
133593
|
+
const rows = [];
|
|
133594
|
+
for (const group of card.groups) {
|
|
133595
|
+
if (group.buttons.length > 0) rows.push(rowOf(group.buttons));
|
|
133596
|
+
}
|
|
133597
|
+
if (card.nav.length > 0) rows.push(rowOf(card.nav));
|
|
133598
|
+
return {
|
|
133599
|
+
text: [card.title, card.text].filter((part) => part !== "").join("\n\n"),
|
|
133600
|
+
rows,
|
|
133601
|
+
actions
|
|
133602
|
+
};
|
|
133603
|
+
};
|
|
133604
|
+
var parsePanelData = (data) => {
|
|
133605
|
+
const match = /^pn:(\d+):(\d+)$/.exec(data);
|
|
133606
|
+
if (match === null) return null;
|
|
133607
|
+
return { version: Number(match[1]), index: Number(match[2]) };
|
|
133608
|
+
};
|
|
133609
|
+
|
|
133610
|
+
// src/plainTurnText.ts
|
|
133611
|
+
var RUNNING_LABEL_MAX_CHARS = 200;
|
|
133612
|
+
var clip2 = (text2, max) => {
|
|
133613
|
+
const chars = Array.from(text2);
|
|
133614
|
+
return chars.length <= max ? text2 : `${chars.slice(0, max - 1).join("")}\u2026`;
|
|
133615
|
+
};
|
|
133616
|
+
var turnTextParts = (view, t) => {
|
|
133617
|
+
const names = view.steps.map((kind) => t(`card.step.${kind}`));
|
|
133618
|
+
if (view.omitted > 0) names.unshift(t("card.step.omitted", { count: view.omitted }));
|
|
133619
|
+
const headLines = [view.title === "" ? t("card.untitled") : view.title];
|
|
133620
|
+
if (names.length > 0) headLines.push(names.join(" \u2192 "));
|
|
133621
|
+
if (view.running !== null) headLines.push(`\u{1F527} ${clip2(view.running, RUNNING_LABEL_MAX_CHARS)}`);
|
|
133622
|
+
const body = view.body.trim();
|
|
133623
|
+
const footLines = [];
|
|
133624
|
+
if (view.status === "working") {
|
|
133625
|
+
if (body === "" && names.length === 0) footLines.push(t("card.status.working"));
|
|
133626
|
+
} else {
|
|
133627
|
+
footLines.push(t(`card.status.${view.status}`));
|
|
133628
|
+
if (view.errorText !== null && view.errorText !== "") footLines.push(view.errorText);
|
|
133629
|
+
}
|
|
133630
|
+
return { head: headLines.join("\n"), body, foot: footLines.join("\n") };
|
|
133631
|
+
};
|
|
133632
|
+
var joinParts = (...parts) => parts.filter((part) => part !== "").join("\n\n");
|
|
133633
|
+
|
|
133634
|
+
// src/telegramTurn.ts
|
|
133635
|
+
var makeTurnWriter = (deps) => {
|
|
133636
|
+
let first = deps.resume ?? null;
|
|
133637
|
+
let live = deps.resume ?? null;
|
|
133638
|
+
let liveText = null;
|
|
133639
|
+
let frozen = 0;
|
|
133640
|
+
let consumed = 0;
|
|
133641
|
+
const put = async (text2) => {
|
|
133642
|
+
const bounded = text2.length <= deps.limit ? text2 : text2.slice(0, deps.limit);
|
|
133643
|
+
if (live === null) {
|
|
133644
|
+
const id = await deps.messages.send(bounded);
|
|
133645
|
+
if (id === null) throw new Error("telegram sent a message without an id");
|
|
133646
|
+
live = id;
|
|
133647
|
+
first ??= id;
|
|
133648
|
+
} else if (bounded === liveText) {
|
|
133649
|
+
return;
|
|
133650
|
+
} else {
|
|
133651
|
+
await deps.messages.edit(live, bounded);
|
|
133652
|
+
}
|
|
133653
|
+
liveText = bounded;
|
|
133654
|
+
};
|
|
133655
|
+
return {
|
|
133656
|
+
firstId: () => first,
|
|
133657
|
+
write: async (view) => {
|
|
133658
|
+
const { head, body, foot } = turnTextParts(view, deps.t);
|
|
133659
|
+
consumed = Math.min(consumed, body.length);
|
|
133660
|
+
for (; ; ) {
|
|
133661
|
+
const rest = body.slice(consumed);
|
|
133662
|
+
const prefix = frozen === 0 ? head.slice(0, Math.floor(deps.limit / 2)) : "";
|
|
133663
|
+
const whole = joinParts(prefix, rest, foot);
|
|
133664
|
+
if (whole.length <= deps.limit) {
|
|
133665
|
+
if (whole !== "") await put(whole);
|
|
133666
|
+
return;
|
|
133667
|
+
}
|
|
133668
|
+
const room = deps.limit - prefix.length - 2;
|
|
133669
|
+
const chunk = splitMessage(rest, room)[0] ?? "";
|
|
133670
|
+
if (chunk === "") {
|
|
133671
|
+
await put(whole);
|
|
133672
|
+
return;
|
|
133673
|
+
}
|
|
133674
|
+
await put(joinParts(prefix, chunk));
|
|
133675
|
+
frozen += 1;
|
|
133676
|
+
consumed += chunk.length;
|
|
133677
|
+
consumed += body.slice(consumed).length - body.slice(consumed).trimStart().length;
|
|
133678
|
+
live = null;
|
|
133679
|
+
liveText = null;
|
|
133680
|
+
}
|
|
133681
|
+
}
|
|
133682
|
+
};
|
|
133683
|
+
};
|
|
133684
|
+
|
|
133539
133685
|
// src/telegramAdapter.ts
|
|
133540
133686
|
var COLUMNS2 = [
|
|
133541
133687
|
{ key: "telegramToken", kind: "password", labelKey: "column.telegramToken.label" }
|
|
133542
133688
|
];
|
|
133543
133689
|
var BACKOFF_START_MS = 1e3;
|
|
133544
133690
|
var BACKOFF_MAX_MS = 6e4;
|
|
133691
|
+
var TELEGRAM_CARD_THROTTLE_MS = 1200;
|
|
133692
|
+
var FLOOD_WAIT_MAX_SECONDS = 30;
|
|
133693
|
+
var REMEMBERED_CARD_LIMIT = 200;
|
|
133545
133694
|
var tokenOf = (binding) => binding.values["telegramToken"] ?? "";
|
|
133546
133695
|
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
133547
133696
|
var makeTelegramAdapter = (deps) => {
|
|
133548
133697
|
const apis = /* @__PURE__ */ new Map();
|
|
133698
|
+
const sleep = deps.sleep ?? defaultSleep;
|
|
133699
|
+
const t = deps.translate;
|
|
133700
|
+
const withFloodWait = async (run) => {
|
|
133701
|
+
try {
|
|
133702
|
+
return await run();
|
|
133703
|
+
} catch (error) {
|
|
133704
|
+
if (error instanceof TelegramApiError && error.code === 429 && error.retryAfterSeconds !== void 0 && error.retryAfterSeconds <= FLOOD_WAIT_MAX_SECONDS) {
|
|
133705
|
+
await sleep(error.retryAfterSeconds * 1e3);
|
|
133706
|
+
return run();
|
|
133707
|
+
}
|
|
133708
|
+
throw error;
|
|
133709
|
+
}
|
|
133710
|
+
};
|
|
133711
|
+
const remembering = () => {
|
|
133712
|
+
const entries = /* @__PURE__ */ new Map();
|
|
133713
|
+
return {
|
|
133714
|
+
get: (key) => entries.get(key),
|
|
133715
|
+
set: (key, value) => {
|
|
133716
|
+
entries.delete(key);
|
|
133717
|
+
entries.set(key, value);
|
|
133718
|
+
if (entries.size > REMEMBERED_CARD_LIMIT) {
|
|
133719
|
+
const oldest = entries.keys().next().value;
|
|
133720
|
+
if (oldest !== void 0) entries.delete(oldest);
|
|
133721
|
+
}
|
|
133722
|
+
},
|
|
133723
|
+
delete: (key) => entries.delete(key)
|
|
133724
|
+
};
|
|
133725
|
+
};
|
|
133726
|
+
const turnWriters = remembering();
|
|
133727
|
+
const panelKeyboards = remembering();
|
|
133728
|
+
const panelClick = (callback, operatorId, handlers) => {
|
|
133729
|
+
const messageId = callback.message?.message_id;
|
|
133730
|
+
const chatId = callback.message?.chat.id;
|
|
133731
|
+
if (messageId === void 0 || chatId === void 0) return t("panel.expired");
|
|
133732
|
+
const keyboard = panelKeyboards.get(`${String(chatId)}:${String(messageId)}`);
|
|
133733
|
+
const parsed = parsePanelData(callback.data ?? "");
|
|
133734
|
+
const action = keyboard === void 0 || parsed === null || parsed.version !== keyboard.version ? void 0 : keyboard.actions[parsed.index];
|
|
133735
|
+
if (action === void 0) return t("panel.expired");
|
|
133736
|
+
return handlers.onPanelClick?.({
|
|
133737
|
+
chatId: String(chatId),
|
|
133738
|
+
cardId: String(messageId),
|
|
133739
|
+
operatorId,
|
|
133740
|
+
op: action.op,
|
|
133741
|
+
arg: action.arg ?? null
|
|
133742
|
+
}) ?? null;
|
|
133743
|
+
};
|
|
133549
133744
|
const apiFor = async (binding) => {
|
|
133550
133745
|
const existing = apis.get(binding.rowId);
|
|
133551
133746
|
if (existing !== void 0) return existing;
|
|
@@ -133554,6 +133749,15 @@ var makeTelegramAdapter = (deps) => {
|
|
|
133554
133749
|
apis.set(binding.rowId, api);
|
|
133555
133750
|
return api;
|
|
133556
133751
|
};
|
|
133752
|
+
const makeWriter = (api, chatId, resume) => makeTurnWriter({
|
|
133753
|
+
t,
|
|
133754
|
+
limit: TELEGRAM_TEXT_MAX_CHARS,
|
|
133755
|
+
...resume === void 0 ? {} : { resume },
|
|
133756
|
+
messages: {
|
|
133757
|
+
send: (text2) => withFloodWait(() => api.sendOne(chatId, text2)),
|
|
133758
|
+
edit: (messageId, text2) => withFloodWait(() => api.editMessage(chatId, messageId, text2))
|
|
133759
|
+
}
|
|
133760
|
+
});
|
|
133557
133761
|
return {
|
|
133558
133762
|
platform: "telegram",
|
|
133559
133763
|
icon: "telegram",
|
|
@@ -133581,10 +133785,8 @@ var makeTelegramAdapter = (deps) => {
|
|
|
133581
133785
|
for (const update of updates) {
|
|
133582
133786
|
const callback = update.callback_query;
|
|
133583
133787
|
if (callback?.data !== void 0) {
|
|
133584
|
-
const
|
|
133585
|
-
|
|
133586
|
-
callback.from === void 0 ? null : String(callback.from.id)
|
|
133587
|
-
);
|
|
133788
|
+
const operatorId = callback.from === void 0 ? null : String(callback.from.id);
|
|
133789
|
+
const notice = callback.data.startsWith(PANEL_DATA_PREFIX) ? panelClick(callback, operatorId, handlers) : handlers.onApproval(callback.data, operatorId);
|
|
133588
133790
|
await api.answerCallback(callback.id, notice ?? void 0).catch(() => void 0);
|
|
133589
133791
|
continue;
|
|
133590
133792
|
}
|
|
@@ -133637,8 +133839,72 @@ var makeTelegramAdapter = (deps) => {
|
|
|
133637
133839
|
},
|
|
133638
133840
|
askApproval: async (binding, request) => {
|
|
133639
133841
|
const api = await apiFor(binding);
|
|
133640
|
-
|
|
133641
|
-
|
|
133842
|
+
return api.sendButtons(binding.chatId, request.body, request.options);
|
|
133843
|
+
},
|
|
133844
|
+
settleApproval: async (binding, cardId, settlement) => {
|
|
133845
|
+
const api = await apiFor(binding);
|
|
133846
|
+
const line = settlement.kind === "chosen" ? t("card.approval.chosen", { label: settlement.label }) : t("card.approval.elsewhere");
|
|
133847
|
+
await withFloodWait(
|
|
133848
|
+
() => api.editMessage(
|
|
133849
|
+
binding.chatId,
|
|
133850
|
+
cardId,
|
|
133851
|
+
`${t("card.approval.doneTitle")}
|
|
133852
|
+
${line}`
|
|
133853
|
+
)
|
|
133854
|
+
);
|
|
133855
|
+
},
|
|
133856
|
+
turnCards: {
|
|
133857
|
+
throttleMs: TELEGRAM_CARD_THROTTLE_MS,
|
|
133858
|
+
open: async (binding, view) => {
|
|
133859
|
+
const api = await apiFor(binding);
|
|
133860
|
+
const writer = makeWriter(api, binding.chatId);
|
|
133861
|
+
try {
|
|
133862
|
+
await writer.write(view);
|
|
133863
|
+
} catch (error) {
|
|
133864
|
+
if (writer.firstId() === null) throw error;
|
|
133865
|
+
deps.log.warn("telegram could not finish the first card write", {
|
|
133866
|
+
error: error instanceof Error ? error.message : String(error)
|
|
133867
|
+
});
|
|
133868
|
+
}
|
|
133869
|
+
const cardId = writer.firstId();
|
|
133870
|
+
if (cardId !== null && view.status === "working") {
|
|
133871
|
+
turnWriters.set(`${binding.chatId}:${cardId}`, writer);
|
|
133872
|
+
}
|
|
133873
|
+
return cardId;
|
|
133874
|
+
},
|
|
133875
|
+
update: async (binding, cardId, view) => {
|
|
133876
|
+
const key = `${binding.chatId}:${cardId}`;
|
|
133877
|
+
const writer = turnWriters.get(key) ?? makeWriter(await apiFor(binding), binding.chatId, cardId);
|
|
133878
|
+
await writer.write(view);
|
|
133879
|
+
if (view.status !== "working") turnWriters.delete(key);
|
|
133880
|
+
else turnWriters.set(key, writer);
|
|
133881
|
+
}
|
|
133882
|
+
},
|
|
133883
|
+
panel: {
|
|
133884
|
+
open: async (binding, card) => {
|
|
133885
|
+
const api = await apiFor(binding);
|
|
133886
|
+
const rendered = renderPanel(card, 1);
|
|
133887
|
+
const cardId = await withFloodWait(
|
|
133888
|
+
() => api.sendOne(binding.chatId, rendered.text, rendered.rows)
|
|
133889
|
+
);
|
|
133890
|
+
if (cardId !== null) {
|
|
133891
|
+
panelKeyboards.set(`${binding.chatId}:${cardId}`, {
|
|
133892
|
+
version: 1,
|
|
133893
|
+
actions: rendered.actions
|
|
133894
|
+
});
|
|
133895
|
+
}
|
|
133896
|
+
return cardId;
|
|
133897
|
+
},
|
|
133898
|
+
update: async (binding, cardId, card) => {
|
|
133899
|
+
const api = await apiFor(binding);
|
|
133900
|
+
const key = `${binding.chatId}:${cardId}`;
|
|
133901
|
+
const version = (panelKeyboards.get(key)?.version ?? 0) + 1;
|
|
133902
|
+
const rendered = renderPanel(card, version);
|
|
133903
|
+
await withFloodWait(
|
|
133904
|
+
() => api.editMessage(binding.chatId, cardId, rendered.text, rendered.rows)
|
|
133905
|
+
);
|
|
133906
|
+
panelKeyboards.set(key, { version, actions: rendered.actions });
|
|
133907
|
+
}
|
|
133642
133908
|
},
|
|
133643
133909
|
fetchResource: async (binding, input) => {
|
|
133644
133910
|
const api = await apiFor(binding);
|
|
@@ -133908,6 +134174,7 @@ var runWecomClient = (input) => {
|
|
|
133908
134174
|
const nextRequestId = (kind) => `${kind}_${++sequence}`;
|
|
133909
134175
|
const pendingAcks = /* @__PURE__ */ new Map();
|
|
133910
134176
|
const pendingRequests = /* @__PURE__ */ new Map();
|
|
134177
|
+
const openStreams = /* @__PURE__ */ new Map();
|
|
133911
134178
|
const subscribeId = nextRequestId("aibot_subscribe");
|
|
133912
134179
|
let missedPong = 0;
|
|
133913
134180
|
let closed = false;
|
|
@@ -133937,6 +134204,8 @@ var runWecomClient = (input) => {
|
|
|
133937
134204
|
return;
|
|
133938
134205
|
}
|
|
133939
134206
|
const error = typeof frame.errcode === "number" && frame.errcode !== 0 ? new Error(`WeCom ack failed: ${String(frame.errcode)} ${frame.errmsg ?? ""}`) : null;
|
|
134207
|
+
const stream = openStreams.get(requestId);
|
|
134208
|
+
if (stream !== void 0 && error !== null) stream.failed = true;
|
|
133940
134209
|
settleAck(requestId, error);
|
|
133941
134210
|
return;
|
|
133942
134211
|
}
|
|
@@ -133948,6 +134217,7 @@ var runWecomClient = (input) => {
|
|
|
133948
134217
|
if (finished) return;
|
|
133949
134218
|
finished = true;
|
|
133950
134219
|
clearInterval(heartbeat);
|
|
134220
|
+
openStreams.clear();
|
|
133951
134221
|
void input.socket.close().catch(() => void 0);
|
|
133952
134222
|
for (const requestId of pendingAcks.keys()) {
|
|
133953
134223
|
settleAck(requestId, new Error("WeCom connection closed"));
|
|
@@ -134035,8 +134305,38 @@ var runWecomClient = (input) => {
|
|
|
134035
134305
|
}
|
|
134036
134306
|
}
|
|
134037
134307
|
};
|
|
134308
|
+
const openStream = (chatId) => {
|
|
134309
|
+
const requestId = pendingRequests.get(chatId);
|
|
134310
|
+
if (requestId === void 0) return null;
|
|
134311
|
+
pendingRequests.delete(chatId);
|
|
134312
|
+
const id = nextRequestId("stream");
|
|
134313
|
+
const state = { failed: false, finished: false };
|
|
134314
|
+
openStreams.set(requestId, state);
|
|
134315
|
+
return {
|
|
134316
|
+
id,
|
|
134317
|
+
send: async (content, finish2) => {
|
|
134318
|
+
if (state.failed) return false;
|
|
134319
|
+
if (state.finished) return true;
|
|
134320
|
+
if (finish2) state.finished = true;
|
|
134321
|
+
try {
|
|
134322
|
+
await send({
|
|
134323
|
+
cmd: "aibot_respond_msg",
|
|
134324
|
+
headers: { req_id: requestId },
|
|
134325
|
+
body: { msgtype: "stream", stream: { id, finish: finish2, content } }
|
|
134326
|
+
});
|
|
134327
|
+
} catch (error) {
|
|
134328
|
+
state.failed = true;
|
|
134329
|
+
openStreams.delete(requestId);
|
|
134330
|
+
throw error;
|
|
134331
|
+
}
|
|
134332
|
+
if (finish2) openStreams.delete(requestId);
|
|
134333
|
+
return true;
|
|
134334
|
+
}
|
|
134335
|
+
};
|
|
134336
|
+
};
|
|
134038
134337
|
return {
|
|
134039
134338
|
sendText,
|
|
134339
|
+
openStream,
|
|
134040
134340
|
rememberRequest: (chatId, requestId) => {
|
|
134041
134341
|
if (requestId !== null) pendingRequests.set(chatId, requestId);
|
|
134042
134342
|
},
|
|
@@ -134051,6 +134351,8 @@ var runWecomClient = (input) => {
|
|
|
134051
134351
|
};
|
|
134052
134352
|
|
|
134053
134353
|
// src/wecomAdapter.ts
|
|
134354
|
+
var WECOM_CARD_THROTTLE_MS = 400;
|
|
134355
|
+
var LIVE_STREAM_LIMIT = 200;
|
|
134054
134356
|
var BACKOFF_START_MS2 = 1e3;
|
|
134055
134357
|
var BACKOFF_MAX_MS2 = 6e4;
|
|
134056
134358
|
var COLUMNS3 = [
|
|
@@ -134071,6 +134373,36 @@ var optionIndexFrom = (text2, count) => {
|
|
|
134071
134373
|
};
|
|
134072
134374
|
var makeWecomAdapter = (deps) => {
|
|
134073
134375
|
const clients = /* @__PURE__ */ new Map();
|
|
134376
|
+
const t = deps.translate;
|
|
134377
|
+
const liveStreams = /* @__PURE__ */ new Map();
|
|
134378
|
+
const rememberStream = (key, entry) => {
|
|
134379
|
+
liveStreams.set(key, entry);
|
|
134380
|
+
if (liveStreams.size > LIVE_STREAM_LIMIT) {
|
|
134381
|
+
const oldest = liveStreams.keys().next().value;
|
|
134382
|
+
if (oldest !== void 0) liveStreams.delete(oldest);
|
|
134383
|
+
}
|
|
134384
|
+
};
|
|
134385
|
+
const emit = async (entry, view) => {
|
|
134386
|
+
const { head, body, foot } = turnTextParts(view, t);
|
|
134387
|
+
if (view.status === "working") {
|
|
134388
|
+
let shown = body;
|
|
134389
|
+
const whole = joinParts(head, body, foot);
|
|
134390
|
+
if (whole.length > WECOM_TEXT_MAX_CHARS) {
|
|
134391
|
+
const marker = t("card.truncated", { count: 9999999 });
|
|
134392
|
+
const room = WECOM_TEXT_MAX_CHARS - head.length - foot.length - marker.length - 6;
|
|
134393
|
+
const kept = room > 0 ? body.slice(Math.max(0, body.length - room)) : "";
|
|
134394
|
+
shown = `${t("card.truncated", { count: body.length - kept.length })}
|
|
134395
|
+
${kept}`;
|
|
134396
|
+
}
|
|
134397
|
+
await entry.stream.send(joinParts(head, shown, foot), false);
|
|
134398
|
+
return;
|
|
134399
|
+
}
|
|
134400
|
+
const chunks = splitForWecom(joinParts(head, body, foot));
|
|
134401
|
+
const delivered = await entry.stream.send(chunks[0] ?? "", true);
|
|
134402
|
+
for (const chunk of delivered ? chunks.slice(1) : chunks) {
|
|
134403
|
+
await entry.client.sendText(entry.chatId, chunk);
|
|
134404
|
+
}
|
|
134405
|
+
};
|
|
134074
134406
|
return {
|
|
134075
134407
|
platform: "wecom",
|
|
134076
134408
|
icon: "wecom",
|
|
@@ -134169,6 +134501,25 @@ var makeWecomAdapter = (deps) => {
|
|
|
134169
134501
|
await (clients.get(binding.rowId)?.sendText(binding.chatId, lines.join("\n")) ?? Promise.resolve());
|
|
134170
134502
|
return null;
|
|
134171
134503
|
},
|
|
134504
|
+
turnCards: {
|
|
134505
|
+
throttleMs: WECOM_CARD_THROTTLE_MS,
|
|
134506
|
+
open: async (binding, view) => {
|
|
134507
|
+
const client = clients.get(binding.rowId);
|
|
134508
|
+
const stream = client?.openStream(binding.chatId) ?? null;
|
|
134509
|
+
if (client === void 0 || stream === null) return null;
|
|
134510
|
+
const entry = { stream, client, chatId: binding.chatId };
|
|
134511
|
+
await emit(entry, view);
|
|
134512
|
+
if (view.status === "working") rememberStream(`${binding.rowId}:${stream.id}`, entry);
|
|
134513
|
+
return stream.id;
|
|
134514
|
+
},
|
|
134515
|
+
update: async (binding, cardId, view) => {
|
|
134516
|
+
const key = `${binding.rowId}:${cardId}`;
|
|
134517
|
+
const entry = liveStreams.get(key);
|
|
134518
|
+
if (entry === void 0) return;
|
|
134519
|
+
await emit(entry, view);
|
|
134520
|
+
if (view.status !== "working") liveStreams.delete(key);
|
|
134521
|
+
}
|
|
134522
|
+
},
|
|
134172
134523
|
answerByText: (_binding, text2, options) => {
|
|
134173
134524
|
const index = optionIndexFrom(text2, options.length);
|
|
134174
134525
|
return index === null ? null : options[index - 1]?.token ?? null;
|
|
@@ -134222,6 +134573,7 @@ var imPlugin = definePlugin({
|
|
|
134222
134573
|
// Telegram 在不少网络里要绕代理,而绕不绕、绕哪个是按行配的 —— 这是插件面上
|
|
134223
134574
|
// `ctx.proxies` 的第一个真实消费者。
|
|
134224
134575
|
fetchVia: (proxy) => ctx.proxies.fetch(proxy),
|
|
134576
|
+
translate: t,
|
|
134225
134577
|
log: {
|
|
134226
134578
|
info: (message, fields) => ctx.log.info(message, fields),
|
|
134227
134579
|
warn: (message, fields) => ctx.log.warn(message, fields)
|
|
@@ -134230,6 +134582,7 @@ var imPlugin = definePlugin({
|
|
|
134230
134582
|
makeWecomAdapter({
|
|
134231
134583
|
// 企微不需要厂商 SDK:全程 JSON over WebSocket,走插件面那条出网长连接。
|
|
134232
134584
|
connectSocket: (url) => ctx.socket.connect({ url }),
|
|
134585
|
+
translate: t,
|
|
134233
134586
|
log: {
|
|
134234
134587
|
info: (message, fields) => ctx.log.info(message, fields),
|
|
134235
134588
|
warn: (message, fields) => ctx.log.warn(message, fields)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ycodium-ai/plugin-im",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2588",
|
|
4
4
|
"description": "Talk to your agents from Telegram, WeCom or Lark.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"im",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"esbuild": "0.28.1",
|
|
28
28
|
"markdown-it": "^15.0.2",
|
|
29
29
|
"vite-plus": "0.2.2",
|
|
30
|
-
"@ycodium-ai/plugin-api": "0.2.
|
|
30
|
+
"@ycodium-ai/plugin-api": "0.2.2588"
|
|
31
31
|
},
|
|
32
32
|
"ycodium": {
|
|
33
33
|
"plugin": "./dist/index.js",
|