@timqi/pier 0.1.0 → 0.1.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/README.md +58 -125
- package/dist/agent/config.js +6 -15
- package/dist/agent/credentials.js +11 -23
- package/dist/agent/events.js +42 -64
- package/dist/agent/listing.js +39 -92
- package/dist/agent/pi.js +103 -252
- package/dist/boards/boards.js +19 -29
- package/dist/channels/attach.js +14 -42
- package/dist/channels/chains.js +33 -37
- package/dist/channels/chunk.js +8 -28
- package/dist/channels/commands.js +3 -14
- package/dist/channels/config.js +33 -52
- package/dist/channels/control.js +4 -13
- package/dist/channels/conversations.js +8 -25
- package/dist/channels/dedup.js +8 -17
- package/dist/channels/gatekeeper.js +13 -23
- package/dist/channels/lark-api.js +23 -63
- package/dist/channels/lark-outbound.js +12 -44
- package/dist/channels/lark-panel.js +7 -23
- package/dist/channels/lark-render.js +18 -62
- package/dist/channels/lark.js +52 -141
- package/dist/channels/lines.js +13 -15
- package/dist/channels/panel.js +16 -36
- package/dist/channels/receipts.js +29 -52
- package/dist/channels/routes.js +3 -9
- package/dist/channels/runtime.js +12 -23
- package/dist/channels/slack-api.js +34 -86
- package/dist/channels/slack-directory.js +7 -23
- package/dist/channels/slack-outbound.js +12 -56
- package/dist/channels/slack-panel.js +4 -13
- package/dist/channels/slack-render.js +23 -91
- package/dist/channels/slack-tool.js +48 -171
- package/dist/channels/slack.js +73 -239
- package/dist/channels/telegram-api.js +8 -20
- package/dist/channels/telegram-panel.js +5 -21
- package/dist/channels/telegram-render.js +13 -40
- package/dist/channels/telegram.js +54 -146
- package/dist/channels/types.js +5 -16
- package/dist/cli.js +17 -41
- package/dist/config-sync.js +87 -4
- package/dist/core/hub.js +7 -20
- package/dist/core/identity.js +20 -59
- package/dist/core/inbound-file.js +15 -49
- package/dist/core/inbox.js +12 -34
- package/dist/core/queue.js +3 -5
- package/dist/core/reply.js +41 -142
- package/dist/core/router.js +209 -264
- package/dist/core/types.js +4 -0
- package/dist/db.js +88 -272
- package/dist/drain.js +57 -50
- package/dist/extensions/index.js +3 -11
- package/dist/extensions/web/anthropic.js +3 -9
- package/dist/extensions/web/artifacts.js +2 -5
- package/dist/extensions/web/content.js +4 -12
- package/dist/extensions/web/http.js +2 -6
- package/dist/extensions/web/language.js +8 -18
- package/dist/extensions/web/openai.js +1 -1
- package/dist/extensions/web/provider.js +5 -18
- package/dist/extensions/web/tools.js +19 -63
- package/dist/lock.js +98 -0
- package/dist/log.js +9 -26
- package/dist/main.js +84 -183
- package/dist/paths.js +10 -26
- package/dist/secrets.js +18 -45
- package/dist/service.js +31 -73
- package/dist/settings.js +19 -63
- package/dist/tasks/agent.js +24 -45
- package/dist/tasks/callbacks.js +8 -16
- package/dist/tasks/command.js +2 -6
- package/dist/tasks/definitions.js +39 -62
- package/dist/tasks/execution.js +41 -39
- package/dist/tasks/groups.js +8 -11
- package/dist/tasks/messages.js +88 -155
- package/dist/tasks/outbox.js +33 -54
- package/dist/tasks/routes.js +4 -7
- package/dist/tasks/runs.js +4 -9
- package/dist/tasks/service.js +29 -42
- package/dist/tasks/store.js +36 -27
- package/dist/tasks/tool.js +59 -60
- package/dist/tools-task.js +20 -60
- package/dist/tools.js +98 -325
- package/dist/update.js +20 -43
- package/dist/web/auth.js +118 -179
- package/dist/web/config-sync.js +2 -2
- package/dist/web/config.js +3 -7
- package/dist/web/explorer.js +10 -21
- package/dist/web/fs.js +20 -42
- package/dist/web/instance.js +35 -82
- package/dist/web/providers.js +5 -11
- package/dist/web/public/assets/{activity-Bl3vZukb.js → activity-B89_hH7q.js} +1 -1
- package/dist/web/public/assets/activity-B89_hH7q.js.br +0 -0
- package/dist/web/public/assets/activity-B89_hH7q.js.gz +0 -0
- package/dist/web/public/assets/{boards-DYuf4Mlj.js → boards-BeKW0ZXK.js} +1 -1
- package/dist/web/public/assets/boards-BeKW0ZXK.js.br +0 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js.gz +0 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js +4 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js.br +0 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js.gz +0 -0
- package/dist/web/public/assets/index-DzXDXra_.js +85 -0
- package/dist/web/public/assets/index-DzXDXra_.js.br +0 -0
- package/dist/web/public/assets/index-DzXDXra_.js.gz +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css +2 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css.br +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css.gz +0 -0
- package/dist/web/public/assets/{runs-BLJu7EXN.js → runs-Cwy0mN8i.js} +1 -1
- package/dist/web/public/assets/runs-Cwy0mN8i.js.br +0 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js.gz +0 -0
- package/dist/web/public/assets/{settings-BrdVh-Zi.js → settings-DzZLmujq.js} +1 -1
- package/dist/web/public/assets/settings-DzZLmujq.js.br +0 -0
- package/dist/web/public/assets/settings-DzZLmujq.js.gz +0 -0
- package/dist/web/public/assets/{task-runs-CeQS1rxa.js → task-runs-BCakxFk8.js} +1 -1
- package/dist/web/public/assets/task-runs-BCakxFk8.js.br +0 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js.gz +0 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js +4 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js.br +0 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js.gz +0 -0
- package/dist/web/public/index.html +30 -16
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/sw.js +14 -2
- package/dist/web/public/sw.js.br +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/push.js +55 -77
- package/dist/web/route.js +3 -7
- package/dist/web/server.js +109 -190
- package/dist/web/session-state.js +13 -53
- package/dist/web/types.js +2 -4
- package/dist/web/webpush.js +10 -25
- package/docs/deploy.md +115 -330
- package/package.json +1 -1
- package/skills/pier-boards/SKILL.md +81 -160
- package/skills/pier-help/SKILL.md +23 -20
- package/skills/pier-slack/SKILL.md +2 -2
- package/skills/pier-tasks/SKILL.md +23 -15
- package/dist/config-sync-fetch.js +0 -84
- package/dist/limits.js +0 -14
- package/dist/web/public/assets/activity-Bl3vZukb.js.br +0 -0
- package/dist/web/public/assets/activity-Bl3vZukb.js.gz +0 -0
- package/dist/web/public/assets/boards-DYuf4Mlj.js.br +0 -0
- package/dist/web/public/assets/boards-DYuf4Mlj.js.gz +0 -0
- package/dist/web/public/assets/explorer-qJH_9nTE.js +0 -4
- package/dist/web/public/assets/explorer-qJH_9nTE.js.br +0 -0
- package/dist/web/public/assets/explorer-qJH_9nTE.js.gz +0 -0
- package/dist/web/public/assets/index-Dqdb-Eqt.js +0 -85
- package/dist/web/public/assets/index-Dqdb-Eqt.js.br +0 -0
- package/dist/web/public/assets/index-Dqdb-Eqt.js.gz +0 -0
- package/dist/web/public/assets/index-DzmMzvi_.css +0 -2
- package/dist/web/public/assets/index-DzmMzvi_.css.br +0 -0
- package/dist/web/public/assets/index-DzmMzvi_.css.gz +0 -0
- package/dist/web/public/assets/runs-BLJu7EXN.js.br +0 -0
- package/dist/web/public/assets/runs-BLJu7EXN.js.gz +0 -0
- package/dist/web/public/assets/settings-BrdVh-Zi.js.br +0 -0
- package/dist/web/public/assets/settings-BrdVh-Zi.js.gz +0 -0
- package/dist/web/public/assets/task-runs-CeQS1rxa.js.br +0 -0
- package/dist/web/public/assets/task-runs-CeQS1rxa.js.gz +0 -0
- package/dist/web/public/assets/tasks-bcb3fYdK.js +0 -4
- package/dist/web/public/assets/tasks-bcb3fYdK.js.br +0 -0
- package/dist/web/public/assets/tasks-bcb3fYdK.js.gz +0 -0
package/dist/channels/lark.js
CHANGED
|
@@ -1,25 +1,11 @@
|
|
|
1
1
|
// Lark (Feishu) adapter: normalize long-connection events, render outbound
|
|
2
|
-
// turns as cards.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// `topicMode` toggle is meaningless here — there is no other behaviour.
|
|
10
|
-
//
|
|
11
|
-
// Four more things are Lark-specific and live only here:
|
|
12
|
-
// - Message bodies are JSON *strings* (`content` is double-encoded), and a
|
|
13
|
-
// mention is a `@_user_N` placeholder resolved through `mentions[]`.
|
|
14
|
-
// - Reactions are named keys: 👀 is `OnIt`, and removal is list-then-delete
|
|
15
|
-
// because the API deletes by reaction_id.
|
|
16
|
-
// - A card callback does not say which thread its message lives in, so every
|
|
17
|
-
// button's value carries the thread root (`LarkActionValue.root`).
|
|
18
|
-
// - Delivery is at-least-once and the transport acks only after the handler
|
|
19
|
-
// returns, so handlers queue work and return; `event_id` is deduplicated.
|
|
20
|
-
//
|
|
21
|
-
// Everything policy-shaped (mention/bind gates, per-chat overrides) is in
|
|
22
|
-
// config.ts, platform-blind and shared with Telegram and Slack.
|
|
2
|
+
// turns as cards. As on Slack, Pier never posts into a chat's main flow: a
|
|
3
|
+
// conversation is `<chatId>/<rootMessageId>` (`reply_in_thread`; DMs thread
|
|
4
|
+
// too). Lark-specific: `content` is a double-encoded JSON string and a mention
|
|
5
|
+
// is a `@_user_N` placeholder resolved through `mentions[]`; 👀 is the reaction
|
|
6
|
+
// key `OnIt`, removed by reaction_id; a card callback does not say which thread
|
|
7
|
+
// its message lives in, so every button value carries the root; delivery is
|
|
8
|
+
// at-least-once and acked when the handler returns, so handlers queue and return.
|
|
23
9
|
import { saveInboundAll } from "../core/inbox.js";
|
|
24
10
|
import { MAX_INBOUND_BYTES } from "../core/inbound-file.js";
|
|
25
11
|
import { awaitsTurn } from "../core/reply.js";
|
|
@@ -35,21 +21,14 @@ import { CWD_SUBMIT_PREFIX, LarkPanel } from "./lark-panel.js";
|
|
|
35
21
|
import { card, markdown, OFFER_PREFIX } from "./lark-render.js";
|
|
36
22
|
import { PANEL_PREFIX } from "./panel.js";
|
|
37
23
|
import { ReceiptLedger, Receipts } from "./receipts.js";
|
|
38
|
-
/** Lark wants a named key here; 👀 has none, `OnIt` is its "being handled". */
|
|
39
24
|
const WORKING = "OnIt";
|
|
40
|
-
//
|
|
41
|
-
// the event is already acked by the transport, so nothing slows the source.
|
|
25
|
+
// The event is already acked, so this bounds concurrency, not the backlog.
|
|
42
26
|
const MAX_ACTIVE_CHATS = 16;
|
|
43
27
|
const RECEIPT_STALE_MS = 30 * 60_000;
|
|
44
28
|
const DRAIN_TIMEOUT_MS = 5000;
|
|
45
|
-
/** How long a delivered event id is remembered, against redelivery. */
|
|
46
29
|
const DEDUP_TTL_MS = 5 * 60_000;
|
|
47
30
|
const DEDUP_MAX = 2000;
|
|
48
|
-
/**
|
|
49
|
-
* A Lark conversation is always `<chatId>/<rootMessageId>` — the thread is
|
|
50
|
-
* the session. This pair is the only definition of the format; control.ts
|
|
51
|
-
* decodes with it rather than splitting on "/" itself.
|
|
52
|
-
*/
|
|
31
|
+
/** The only definition of the conversation id format; control.ts decodes with it. */
|
|
53
32
|
const conversationId = (chatId, root) => `${chatId}/${root}`;
|
|
54
33
|
export const parseConversation = (id) => {
|
|
55
34
|
const at = id.indexOf("/");
|
|
@@ -57,31 +36,20 @@ export const parseConversation = (id) => {
|
|
|
57
36
|
? { chatId: id, root: "" }
|
|
58
37
|
: { chatId: id.slice(0, at), root: id.slice(at + 1) };
|
|
59
38
|
};
|
|
60
|
-
/**
|
|
61
|
-
* The thread a message belongs to. A message already in a topic keeps its
|
|
62
|
-
* root; one posted in the chat becomes the root of its own — which is what
|
|
63
|
-
* makes every request its own session without asking Lark for anything.
|
|
64
|
-
*/
|
|
39
|
+
/** A message posted in the chat becomes the root of its own topic. */
|
|
65
40
|
const threadOf = (msg) => msg.rootId || msg.messageId;
|
|
66
41
|
export class LarkChannel {
|
|
67
42
|
deps;
|
|
68
43
|
id = "lark";
|
|
69
44
|
api;
|
|
70
45
|
log;
|
|
71
|
-
/** 👀 lifecycle, durable; see receipts.ts for why it is not just a Map. */
|
|
72
46
|
receipts;
|
|
73
|
-
/** Ordering per chat, concurrency across them; see chains.ts. */
|
|
74
47
|
chains;
|
|
75
|
-
/** The inbound gate and the bind-hint throttle; see gatekeeper.ts. */
|
|
76
48
|
gate;
|
|
77
|
-
/** Event ids already handled, against at-least-once delivery. */
|
|
78
49
|
seen;
|
|
79
|
-
/**
|
|
50
|
+
/** Absent when no control was wired (tests). */
|
|
80
51
|
panel;
|
|
81
|
-
/** User names, cached for the process — one contact lookup per person. */
|
|
82
52
|
names = new Map();
|
|
83
|
-
/** Chats already reported to the store this process; a rename waits for a
|
|
84
|
-
* restart, which is soon enough for a Console display label. */
|
|
85
53
|
discovered = new Set();
|
|
86
54
|
me = "";
|
|
87
55
|
out;
|
|
@@ -98,7 +66,7 @@ export class LarkChannel {
|
|
|
98
66
|
this.api = deps.client ?? new LarkApi(config.token, config.appToken, this.log);
|
|
99
67
|
this.out = new LarkOutbound(this.api, this.log);
|
|
100
68
|
this.receipts = new Receipts(
|
|
101
|
-
//
|
|
69
|
+
// Removal needs the emoji key back.
|
|
102
70
|
{
|
|
103
71
|
setReaction: (_chatId, messageId, emoji) => emoji
|
|
104
72
|
? this.api.addReaction(messageId, emoji)
|
|
@@ -116,12 +84,10 @@ export class LarkChannel {
|
|
|
116
84
|
async start(onMessage) {
|
|
117
85
|
this.me = await this.api.botOpenId();
|
|
118
86
|
if (!this.me) {
|
|
119
|
-
//
|
|
120
|
-
// every chat with require-mention on goes silent. Loud, not a debug line.
|
|
87
|
+
// Every chat with require-mention on goes silent; loud, not a debug line.
|
|
121
88
|
this.log("bot info returned no open_id: mention detection is disabled");
|
|
122
89
|
}
|
|
123
90
|
this.running = true;
|
|
124
|
-
// Best-effort and off the critical path.
|
|
125
91
|
void this.receipts.sweep(true);
|
|
126
92
|
this.socket = await this.api.connect({
|
|
127
93
|
onMessage: (event) => this.onEvent(event, onMessage),
|
|
@@ -135,16 +101,12 @@ export class LarkChannel {
|
|
|
135
101
|
await this.chains.drain(DRAIN_TIMEOUT_MS);
|
|
136
102
|
}
|
|
137
103
|
// --- inbound ---------------------------------------------------------------
|
|
138
|
-
/**
|
|
139
|
-
*
|
|
140
|
-
* when this returns, so routing is synchronous and the work is queued.
|
|
141
|
-
*/
|
|
104
|
+
/** The SDK acks the frame when this returns, so routing is synchronous and
|
|
105
|
+
* the work is queued. */
|
|
142
106
|
onEvent(event, onMessage) {
|
|
143
107
|
if (!this.running)
|
|
144
108
|
return;
|
|
145
|
-
// Asked on every event, throttled inside receipts.ts.
|
|
146
109
|
void this.receipts.sweep();
|
|
147
|
-
// Our own echo or another app's message.
|
|
148
110
|
if (event.senderType === "app")
|
|
149
111
|
return;
|
|
150
112
|
if (this.seen.duplicate(event.eventId))
|
|
@@ -157,8 +119,6 @@ export class LarkChannel {
|
|
|
157
119
|
onCardEvent(action, onMessage) {
|
|
158
120
|
if (!this.running)
|
|
159
121
|
return;
|
|
160
|
-
// A card callback carries its own event id; the composed key is the
|
|
161
|
-
// fallback for a payload that arrives without one.
|
|
162
122
|
const dedupId = action.eventId ??
|
|
163
123
|
`card:${action.messageId}:${action.operatorId}:${action.value?.key ?? action.name ?? ""}`;
|
|
164
124
|
if (this.seen.duplicate(dedupId))
|
|
@@ -178,12 +138,12 @@ export class LarkChannel {
|
|
|
178
138
|
if (!raw && !attachments.length && !mentioned)
|
|
179
139
|
return;
|
|
180
140
|
const isDm = msg.chatType === "p2p";
|
|
181
|
-
if (!this.discovered.has(msg.chatId)) {
|
|
141
|
+
if (!this.discovered.has(msg.chatId) && this.gate.mayDiscover({ isDm, userId: senderId })) {
|
|
182
142
|
this.discovered.add(msg.chatId);
|
|
183
143
|
const name = isDm
|
|
184
144
|
? `DM · ${await this.userName(senderId)}`
|
|
185
145
|
: (await this.api.chatName(msg.chatId).catch((err) => {
|
|
186
|
-
//
|
|
146
|
+
// Usually a missing scope.
|
|
187
147
|
this.log(`chat lookup failed for ${msg.chatId}: ${String(err)}`);
|
|
188
148
|
return undefined;
|
|
189
149
|
})) ?? msg.chatId;
|
|
@@ -194,9 +154,8 @@ export class LarkChannel {
|
|
|
194
154
|
});
|
|
195
155
|
}
|
|
196
156
|
const text = raw.trim();
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
// could positively match, so any target means "not us".
|
|
157
|
+
// Lark gives Pier no @username a command target could match, so any target
|
|
158
|
+
// means "not us".
|
|
200
159
|
const parsed = parseCommand(text);
|
|
201
160
|
const command = parsed?.target ? undefined : parsed;
|
|
202
161
|
const root = threadOf(msg);
|
|
@@ -204,8 +163,7 @@ export class LarkChannel {
|
|
|
204
163
|
const bindRequest = command?.name === "bind" && isDm;
|
|
205
164
|
const admitted = this.gate.admit("message", msg.chatId, {
|
|
206
165
|
isDm,
|
|
207
|
-
// Mentioned, or continuing a topic Pier already owns —
|
|
208
|
-
// equivalent of Telegram's "replying to the bot", durable so it still
|
|
166
|
+
// Mentioned, or continuing a topic Pier already owns — durable, so it
|
|
209
167
|
// holds after a restart.
|
|
210
168
|
addressed: mentioned || (!!msg.rootId && !!this.deps.control?.knows(here)),
|
|
211
169
|
userId: senderId,
|
|
@@ -220,22 +178,18 @@ export class LarkChannel {
|
|
|
220
178
|
return this.bind(senderId, msg.messageId, command?.args ?? "");
|
|
221
179
|
if (command?.name === "stop")
|
|
222
180
|
return this.abortTurn(here, msg.messageId);
|
|
223
|
-
// `@bot`
|
|
224
|
-
// `/settings` are the same request: show me this conversation's settings.
|
|
181
|
+
// A bare `@bot` and `/settings` are the same request.
|
|
225
182
|
if (this.panel && (command?.name === "settings" || (!text && !attachments.length && mentioned))) {
|
|
226
183
|
return this.panel.open(here, msg.chatId, root);
|
|
227
184
|
}
|
|
228
|
-
// Downloading only past the gate: an unauthorized sender must not
|
|
229
|
-
//
|
|
185
|
+
// Downloading only past the gate: an unauthorized sender must not make the
|
|
186
|
+
// bot pull bytes on their behalf.
|
|
230
187
|
const markers = await this.saveAttachments(msg.messageId, attachments);
|
|
231
|
-
//
|
|
232
|
-
// turn can
|
|
233
|
-
// turn even starts — so the name is resolved first and the mark→dispatch
|
|
234
|
-
// pair stays synchronous.
|
|
188
|
+
// Resolved before the mark: any await between mark() and dispatch is a
|
|
189
|
+
// window in which a previous turn can settle and take this receipt with it.
|
|
235
190
|
const sender = { id: senderId, name: await this.userName(senderId) };
|
|
236
191
|
this.receipts.mark(here.conversationId, msg.chatId, msg.messageId);
|
|
237
|
-
//
|
|
238
|
-
// end is the wrong default when the human is watching a 👀 in a topic.
|
|
192
|
+
// Steer: a follow-up is the wrong default when the human is watching a 👀.
|
|
239
193
|
onMessage({
|
|
240
194
|
key: here,
|
|
241
195
|
senderId,
|
|
@@ -244,11 +198,8 @@ export class LarkChannel {
|
|
|
244
198
|
mode: "steer",
|
|
245
199
|
});
|
|
246
200
|
}
|
|
247
|
-
/**
|
|
248
|
-
*
|
|
249
|
-
* to fetch, and whether the bot was addressed. `content` is a JSON string;
|
|
250
|
-
* malformed or unreadable types are logged and dropped at this boundary.
|
|
251
|
-
*/
|
|
201
|
+
/** `content` is a JSON string; malformed or unreadable types are logged and
|
|
202
|
+
* dropped at this boundary. */
|
|
252
203
|
readContent(msg) {
|
|
253
204
|
let content = {};
|
|
254
205
|
try {
|
|
@@ -278,8 +229,7 @@ export class LarkChannel {
|
|
|
278
229
|
case "media":
|
|
279
230
|
case "audio":
|
|
280
231
|
if (content.file_key) {
|
|
281
|
-
// `file_size` is optional and sometimes a numeric string
|
|
282
|
-
// one is fine — download() enforces the cap mid-stream regardless.
|
|
232
|
+
// `file_size` is optional and sometimes a numeric string.
|
|
283
233
|
const size = Number(content.file_size);
|
|
284
234
|
attachments.push({
|
|
285
235
|
key: String(content.file_key),
|
|
@@ -292,9 +242,8 @@ export class LarkChannel {
|
|
|
292
242
|
default:
|
|
293
243
|
this.log(`ignored message type ${msg.messageType ?? "?"}`);
|
|
294
244
|
}
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
// name, so the agent sees who was meant.
|
|
245
|
+
// The bot's own placeholder is addressing and is removed; anyone else's
|
|
246
|
+
// becomes their name.
|
|
298
247
|
let mentioned = false;
|
|
299
248
|
for (const mention of msg.mentions ?? []) {
|
|
300
249
|
const isMe = !!this.me && mention.id?.open_id === this.me;
|
|
@@ -303,11 +252,8 @@ export class LarkChannel {
|
|
|
303
252
|
}
|
|
304
253
|
return { text, attachments, mentioned };
|
|
305
254
|
}
|
|
306
|
-
/** Rich text: the readable runs, and any images embedded in it. */
|
|
307
255
|
readPost(raw) {
|
|
308
|
-
// A post body may arrive wrapped in a locale (`{zh_cn: {title, content}}`)
|
|
309
|
-
// rather than flat — both shapes are real. Take the flat body when it is
|
|
310
|
-
// one, else the first locale entry that is an object.
|
|
256
|
+
// A post body may arrive flat or wrapped in a locale (`{zh_cn: {title, content}}`).
|
|
311
257
|
const content = Array.isArray(raw.content) || typeof raw.title === "string"
|
|
312
258
|
? raw
|
|
313
259
|
: (Object.values(raw).find((v) => !!v && typeof v === "object" && !Array.isArray(v)) ??
|
|
@@ -326,9 +272,8 @@ export class LarkChannel {
|
|
|
326
272
|
if (run.tag === "text" || run.tag === "a")
|
|
327
273
|
parts.push(String(run.text ?? ""));
|
|
328
274
|
else if (run.tag === "at") {
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
// not content; anyone else's becomes their name.
|
|
275
|
+
// Rich text carries the at run inline, not as a placeholder; the
|
|
276
|
+
// bot's own is detected via `mentions[]`.
|
|
332
277
|
if (run.user_id !== this.me)
|
|
333
278
|
parts.push(`@${run.user_name ?? run.user_id ?? "?"}`);
|
|
334
279
|
}
|
|
@@ -343,9 +288,8 @@ export class LarkChannel {
|
|
|
343
288
|
}
|
|
344
289
|
// --- card actions ------------------------------------------------------------
|
|
345
290
|
async onAction(action, onMessage) {
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
// button's name. Absent both, the payload is not one Pier minted.
|
|
291
|
+
// A form submit carries the root in the button's name instead of the
|
|
292
|
+
// value. Absent both, the payload is not one Pier minted.
|
|
349
293
|
const payload = action.value?.key ?? "";
|
|
350
294
|
const formRoot = action.name?.startsWith(CWD_SUBMIT_PREFIX)
|
|
351
295
|
? action.name.slice(CWD_SUBMIT_PREFIX.length)
|
|
@@ -370,18 +314,15 @@ export class LarkChannel {
|
|
|
370
314
|
await this.panel?.onCwdSubmit(key, action, root);
|
|
371
315
|
return;
|
|
372
316
|
}
|
|
373
|
-
// Panel clicks are namespaced `cfg:` and never reach the agent.
|
|
374
317
|
if (payload.startsWith(PANEL_PREFIX)) {
|
|
375
318
|
if (!(await this.panel?.onAction(action, key, payload, root))) {
|
|
376
319
|
this.log(`panel action ${payload} with no panel wired, dropped`);
|
|
377
320
|
}
|
|
378
321
|
return;
|
|
379
322
|
}
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
// state and survives a restart. A value without one is a stale card from
|
|
384
|
-
// before this convention, and the user clicked expecting something.
|
|
323
|
+
// The label travels in the echoed value (Lark cannot return a 2.0 card;
|
|
324
|
+
// see LarkActionValue), so a click survives a restart. A value without
|
|
325
|
+
// one is a stale card, and the user clicked expecting something.
|
|
385
326
|
const label = payload.startsWith(OFFER_PREFIX) && typeof action.value?.label === "string"
|
|
386
327
|
? action.value.label
|
|
387
328
|
: undefined;
|
|
@@ -391,10 +332,8 @@ export class LarkChannel {
|
|
|
391
332
|
.catch((err) => this.log(`stale-option notice failed: ${String(err)}`));
|
|
392
333
|
return;
|
|
393
334
|
}
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
// topic shows an answer to a request nobody can see being made, and there
|
|
397
|
-
// is nothing to carry the eyes.
|
|
335
|
+
// A bot cannot post as the user, so the pick is echoed: otherwise the
|
|
336
|
+
// topic shows an answer to a request nobody can see, with nothing to carry the eyes.
|
|
398
337
|
const sender = { id: action.operatorId, name: await this.userName(action.operatorId) };
|
|
399
338
|
await this.out.retire(action.messageId);
|
|
400
339
|
const echo = await this.api.replyCard(root, card([markdown(picked(label))]))
|
|
@@ -413,20 +352,14 @@ export class LarkChannel {
|
|
|
413
352
|
mode: "steer",
|
|
414
353
|
});
|
|
415
354
|
}
|
|
416
|
-
/**
|
|
417
|
-
* Stop the turn this conversation is running. The abort makes Pi end the
|
|
418
|
-
* turn, which reaches send() through the normal turn-end path and clears
|
|
419
|
-
* the 👀 receipts — so nothing here touches them.
|
|
420
|
-
*/
|
|
355
|
+
/** The abort ends the turn, which reaches send() and clears the receipts. */
|
|
421
356
|
async abortTurn(key, messageId) {
|
|
422
357
|
await this.deps.control?.abort(key);
|
|
423
358
|
await this.api.replyCard(messageId, card([markdown(STOPPED)]));
|
|
424
359
|
}
|
|
425
360
|
// --- bind ------------------------------------------------------------------
|
|
426
|
-
/**
|
|
427
|
-
*
|
|
428
|
-
* but a DM that swallows every message looks broken rather than locked.
|
|
429
|
-
*/
|
|
361
|
+
/** Groups stay silent, but a DM that swallows every message looks broken
|
|
362
|
+
* rather than locked. */
|
|
430
363
|
async hintBind(userId, messageId) {
|
|
431
364
|
if (!this.gate.mayHint(userId))
|
|
432
365
|
return;
|
|
@@ -435,8 +368,8 @@ export class LarkChannel {
|
|
|
435
368
|
}
|
|
436
369
|
async bind(userId, messageId, code) {
|
|
437
370
|
const name = await this.userName(userId);
|
|
438
|
-
const
|
|
439
|
-
await this.api.replyCard(messageId, card([markdown(bindResult(
|
|
371
|
+
const outcome = this.deps.store.redeemBindCode("lark", code, { id: userId, name });
|
|
372
|
+
await this.api.replyCard(messageId, card([markdown(bindResult(outcome, name))]));
|
|
440
373
|
}
|
|
441
374
|
// --- lookups ---------------------------------------------------------------
|
|
442
375
|
async userName(openId) {
|
|
@@ -444,17 +377,12 @@ export class LarkChannel {
|
|
|
444
377
|
if (hit)
|
|
445
378
|
return hit;
|
|
446
379
|
const name = await this.api.userName(openId).catch((err) => {
|
|
447
|
-
// The id is the honest fallback label; the reason still gets said.
|
|
448
380
|
this.log(`user lookup failed for ${openId}: ${String(err)}`);
|
|
449
381
|
return openId;
|
|
450
382
|
});
|
|
451
383
|
this.names.set(openId, name);
|
|
452
384
|
return name;
|
|
453
385
|
}
|
|
454
|
-
/** The message's attachments as the shared save loop wants them (the loop
|
|
455
|
-
* itself, size gate and lost markers included, is core/inbox.ts; the
|
|
456
|
-
* mid-stream refusal in download() names "too large" so the loop's marker
|
|
457
|
-
* stays honest when the metadata lied by omission). */
|
|
458
386
|
saveAttachments(messageId, files) {
|
|
459
387
|
return saveInboundAll(this.id, files.map((file) => ({
|
|
460
388
|
label: file.name ?? "attachment",
|
|
@@ -465,34 +393,20 @@ export class LarkChannel {
|
|
|
465
393
|
})), this.log);
|
|
466
394
|
}
|
|
467
395
|
// --- outbound --------------------------------------------------------------
|
|
468
|
-
/**
|
|
469
|
-
* Called on every turn-end, empty text included: the turn settled with
|
|
470
|
-
* nothing to say, and the 👀 receipts still have to come off.
|
|
471
|
-
*/
|
|
472
396
|
async send(conversation, reply) {
|
|
473
397
|
const { root } = parseConversation(conversation);
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
// turn in the chat's main flow — the one thing this adapter promises
|
|
477
|
-
// never to do — so it is refused loudly instead, and the receipts still
|
|
478
|
-
// come off so no 👀 is stranded.
|
|
398
|
+
// No root is a foreign id; posting it would put a turn in the chat's main
|
|
399
|
+
// flow. Refused loudly, receipts still cleared.
|
|
479
400
|
if (!root) {
|
|
480
401
|
this.log(`refusing to answer ${conversation}: no thread root in the conversation id`);
|
|
481
402
|
await this.receipts.settle(conversation);
|
|
482
403
|
return;
|
|
483
404
|
}
|
|
484
|
-
//
|
|
485
|
-
|
|
486
|
-
await this.receipts.settleAfter(conversation, () => this.out.reply(root, reply));
|
|
405
|
+
// The turn ended either way; a 👀 left up by a failed send looks like work.
|
|
406
|
+
await this.receipts.settleAfter(conversation, () => this.out.reply(root, reply), reply.meta);
|
|
487
407
|
}
|
|
488
|
-
/**
|
|
489
|
-
*
|
|
490
|
-
* no message of the user's to carry them — nobody typed one — so without this
|
|
491
|
-
* the topic shows nothing at all while the agent works. The turn-end `send`
|
|
492
|
-
* clears it like any other receipt; an error note is not marked, because no
|
|
493
|
-
* turn follows it (`awaitsTurn`) and the eyes would sit there until the stale
|
|
494
|
-
* sweep.
|
|
495
|
-
*/
|
|
408
|
+
/** The 👀 goes on the note itself: the turn it triggers has no message of
|
|
409
|
+
* the user's to carry them. */
|
|
496
410
|
async notify(conversation, note) {
|
|
497
411
|
const { chatId, root } = parseConversation(conversation);
|
|
498
412
|
if (!root) {
|
|
@@ -500,9 +414,6 @@ export class LarkChannel {
|
|
|
500
414
|
return;
|
|
501
415
|
}
|
|
502
416
|
const messageId = await this.out.note(root, note);
|
|
503
|
-
// The last card of a long note, so the eyes sit at the foot of the topic,
|
|
504
|
-
// where the reply will land. A reaction that fails is swallowed and logged
|
|
505
|
-
// by receipts.ts, so the note itself is never lost to one.
|
|
506
417
|
if (messageId && awaitsTurn(note.origin))
|
|
507
418
|
this.receipts.mark(conversation, chatId, messageId);
|
|
508
419
|
}
|
package/dist/channels/lines.js
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
// What the shared control moments say — one spelling for three platforms.
|
|
2
|
-
//
|
|
3
|
-
// Bind, stop and the option echo behave identically everywhere by contract,
|
|
4
|
-
// and their lines were copied per adapter until Lark made three of each; the
|
|
5
|
-
// same wording drifting apart is how the panels went (see panel.ts). How a
|
|
6
|
-
// line is *sent* stays with the adapter; the only legitimate variation is the
|
|
7
|
-
// spelling of the bind command, so it is the parameter.
|
|
8
|
-
/** DM-only, throttled by Gatekeeper.mayHint; groups stay silent by contract. */
|
|
1
|
+
// What the shared control moments say — one spelling for three platforms. The
|
|
2
|
+
// only legitimate variation is the spelling of the bind command, so it is the parameter.
|
|
9
3
|
export const bindHint = (command) => `You are not bound yet. Ask the operator for a bind code, then send ${command}.`;
|
|
10
|
-
/** The
|
|
11
|
-
export const bindResult = (
|
|
12
|
-
|
|
4
|
+
/** The name arrives escaped by the caller. */
|
|
5
|
+
export const bindResult = (outcome, name) => {
|
|
6
|
+
if (outcome === "bound")
|
|
7
|
+
return `Bound as ${name}.`;
|
|
8
|
+
return outcome === "voided"
|
|
9
|
+
? "That bind code is invalid or expired — and too many wrong tries have now" +
|
|
10
|
+
" voided it. Ask the operator for a new one."
|
|
11
|
+
: "That bind code is invalid or expired.";
|
|
12
|
+
};
|
|
13
13
|
export const STOPPED = "\u23f9 Stopped.";
|
|
14
|
-
/**
|
|
14
|
+
/** Echoed because a bot cannot post as the user. */
|
|
15
15
|
export const picked = (label) => `\u25b8 ${label}`;
|
|
16
|
-
/**
|
|
17
|
-
* conventions. Said in the chat: the person clicked and would otherwise see
|
|
18
|
-
* nothing happen, which reads as broken (5b). */
|
|
16
|
+
/** Said in the chat: the person clicked and would otherwise see nothing happen (§5). */
|
|
19
17
|
export const STALE_OPTION = "⚠ That option is no longer available — please type the choice instead.";
|
package/dist/channels/panel.js
CHANGED
|
@@ -1,21 +1,9 @@
|
|
|
1
|
-
// The in-chat settings panel, minus the platform
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// Every payload is namespaced `cfg:` and consumed here, so a panel tap can
|
|
5
|
-
// never be mistaken for one of the agent's next-step buttons (whose payload is
|
|
6
|
-
// the label itself) and never reaches the agent. Choices travel as an index
|
|
7
|
-
// rather than a name: Telegram's callback data caps at 64 bytes, and an index
|
|
8
|
-
// cannot be invalidated by a label someone rewrote.
|
|
9
|
-
//
|
|
10
|
-
// What is left to a platform is markup, how its one message is sent, edited
|
|
11
|
-
// and deleted, and how it asks for a single typed answer — Slack has modals,
|
|
12
|
-
// Telegram has a forced reply. Everything above that is the same panel, so it
|
|
13
|
-
// is written once here.
|
|
1
|
+
// The in-chat settings panel, minus the platform: one message edited in place,
|
|
2
|
+
// every payload namespaced `cfg:` so a tap never reaches the agent. Choices
|
|
3
|
+
// travel as an index: Telegram's callback data caps at 64 bytes.
|
|
14
4
|
import { compact, thinkingLabel } from "../core/reply.js";
|
|
15
5
|
export const PANEL_PREFIX = "cfg:";
|
|
16
6
|
const MODELS_PER_PAGE = 8;
|
|
17
|
-
/** The cwd prompt's one sentence and placeholder — each platform owns only
|
|
18
|
-
* its widget's lead-in ("Reply with…", a modal hint, a form label). */
|
|
19
7
|
export const CWD_TAIL = "A new session starts there; the current one stays in its own directory.";
|
|
20
8
|
export const CWD_PLACEHOLDER = "/path/to/project";
|
|
21
9
|
const onOff = (v) => (v ? "on" : "off");
|
|
@@ -40,7 +28,6 @@ export class ChatPanel {
|
|
|
40
28
|
return this.panels.get(key.conversationId);
|
|
41
29
|
}
|
|
42
30
|
// --- rendering ---------------------------------------------------------------
|
|
43
|
-
/** The panel proper: this session, this chat, and what can be done to them. */
|
|
44
31
|
async view(key, chatId) {
|
|
45
32
|
const status = await this.deps.control.status(key);
|
|
46
33
|
return {
|
|
@@ -51,8 +38,6 @@ export class ChatPanel {
|
|
|
51
38
|
? this.sessionLines(status)
|
|
52
39
|
: ["None yet — send a message to start one."],
|
|
53
40
|
},
|
|
54
|
-
// Slack calls it a channel, Telegram a chat; the panel says what the
|
|
55
|
-
// person reading it says.
|
|
56
41
|
{
|
|
57
42
|
title: this.platform === "slack" ? "Channel" : "Chat",
|
|
58
43
|
lines: this.chatLines(chatId),
|
|
@@ -90,7 +75,6 @@ export class ChatPanel {
|
|
|
90
75
|
: `mention ${onOff(policy.requireMention)} · bind ${onOff(policy.requireBind)}${this.gateExtras(chat, policy)}`;
|
|
91
76
|
return [`${this.esc(chat.name || chatId)} · ${chat.kind} · ${this.code(chatId)}`, gates];
|
|
92
77
|
}
|
|
93
|
-
/** Redraw the panel this conversation owns. */
|
|
94
78
|
async refresh(key, note) {
|
|
95
79
|
const state = this.state(key);
|
|
96
80
|
if (!state)
|
|
@@ -98,14 +82,8 @@ export class ChatPanel {
|
|
|
98
82
|
await this.draw(state, await this.view(key, state.chatId), note);
|
|
99
83
|
}
|
|
100
84
|
// --- actions -----------------------------------------------------------------
|
|
101
|
-
/**
|
|
102
|
-
*
|
|
103
|
-
* can treat it as one of the agent's next-step labels instead.
|
|
104
|
-
*
|
|
105
|
-
* `reopen` is how a panel left behind by a previous process recovers: its
|
|
106
|
-
* state died with that process, and redrawing from the platform's own copy
|
|
107
|
-
* of the message is the only honest answer. It costs one tap.
|
|
108
|
-
*/
|
|
85
|
+
/** Returns false when the payload is not ours. `reopen` recovers a panel
|
|
86
|
+
* whose state died with a previous process; it costs one tap. */
|
|
109
87
|
async dispatch(key, payload, ctx, reopen) {
|
|
110
88
|
if (!payload.startsWith(PANEL_PREFIX))
|
|
111
89
|
return false;
|
|
@@ -157,7 +135,14 @@ export class ChatPanel {
|
|
|
157
135
|
const state = this.state(key);
|
|
158
136
|
if (!state)
|
|
159
137
|
return;
|
|
160
|
-
|
|
138
|
+
// An empty list and a catalog that could not be read are two different
|
|
139
|
+
// answers; the second must not draw as the first.
|
|
140
|
+
let unavailable;
|
|
141
|
+
state.models = await this.deps.control.models().catch((err) => {
|
|
142
|
+
unavailable = `Could not list models: ${String(err)}`;
|
|
143
|
+
this.deps.log(unavailable);
|
|
144
|
+
return [];
|
|
145
|
+
});
|
|
161
146
|
const status = await this.deps.control.status(key);
|
|
162
147
|
const pages = Math.max(1, Math.ceil(state.models.length / MODELS_PER_PAGE));
|
|
163
148
|
const at = Math.min(Math.max(page, 0), pages - 1);
|
|
@@ -166,7 +151,7 @@ export class ChatPanel {
|
|
|
166
151
|
groups: [{
|
|
167
152
|
title: "Model",
|
|
168
153
|
suffix: ` · page ${at + 1}/${pages}`,
|
|
169
|
-
lines: state.models.length ? [] : ["No models with configured auth."],
|
|
154
|
+
lines: state.models.length ? [] : [unavailable ?? "No models with configured auth."],
|
|
170
155
|
}],
|
|
171
156
|
picks: slice.map((model, i) => {
|
|
172
157
|
const current = status?.model?.provider === model.provider && status.model.id === model.id;
|
|
@@ -210,15 +195,10 @@ export class ChatPanel {
|
|
|
210
195
|
await this.deps.control.setThinking(key, level);
|
|
211
196
|
await this.refresh(key, `Reasoning set to ${thinkingLabel(level)}.`);
|
|
212
197
|
}
|
|
213
|
-
/**
|
|
214
|
-
*
|
|
215
|
-
* creation, so "change the working directory" *is* "start a new session
|
|
216
|
-
* there". Shared because both platforms have to say so and handle the same
|
|
217
|
-
* two failures.
|
|
218
|
-
*/
|
|
198
|
+
/** Pi fixes cwd at session creation, so "change the working directory" *is*
|
|
199
|
+
* "start a new session there". */
|
|
219
200
|
async startSessionIn(key, path) {
|
|
220
201
|
if (!path.startsWith("/")) {
|
|
221
|
-
// A silent no-op here reads as "the button is broken".
|
|
222
202
|
const error = "That is not an absolute path — nothing changed.";
|
|
223
203
|
await this.refresh(key, error);
|
|
224
204
|
return { error };
|