@vibearound/plugin-channel-sdk 0.3.0 → 0.4.0
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 +62 -108
- package/dist/advanced.d.ts +19 -0
- package/dist/advanced.d.ts.map +1 -0
- package/dist/advanced.js +18 -0
- package/dist/advanced.js.map +1 -0
- package/dist/connection.d.ts +10 -4
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +10 -4
- package/dist/connection.js.map +1 -1
- package/dist/index.d.ts +27 -44
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -45
- package/dist/index.js.map +1 -1
- package/dist/plugin.d.ts +80 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/{run-plugin.js → plugin.js} +43 -44
- package/dist/plugin.js.map +1 -0
- package/dist/renderer.d.ts +52 -46
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +114 -90
- package/dist/renderer.js.map +1 -1
- package/dist/types.d.ts +19 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/advanced.ts +43 -0
- package/src/connection.ts +10 -4
- package/src/index.ts +41 -60
- package/src/{run-plugin.ts → plugin.ts} +70 -72
- package/src/renderer.ts +142 -97
- package/src/types.ts +24 -0
- package/dist/run-plugin.d.ts +0 -95
- package/dist/run-plugin.d.ts.map +0 -1
- package/dist/run-plugin.js.map +0 -1
package/src/renderer.ts
CHANGED
|
@@ -25,36 +25,30 @@
|
|
|
25
25
|
*
|
|
26
26
|
* ## Usage
|
|
27
27
|
*
|
|
28
|
+
* Subclass and implement `sendText` + `sendBlock` (+ optionally `editBlock`):
|
|
29
|
+
*
|
|
28
30
|
* ```ts
|
|
29
31
|
* class MyRenderer extends BlockRenderer<string> {
|
|
30
|
-
* protected async
|
|
31
|
-
*
|
|
32
|
+
* protected async sendText(chatId, text) {
|
|
33
|
+
* await myApi.sendMessage(chatId, text);
|
|
34
|
+
* }
|
|
35
|
+
* protected async sendBlock(chatId, kind, content) {
|
|
36
|
+
* const msg = await myApi.sendMessage(chatId, content);
|
|
32
37
|
* return msg.id;
|
|
33
38
|
* }
|
|
34
|
-
* protected async editBlock(
|
|
39
|
+
* protected async editBlock(chatId, ref, kind, content, sealed) {
|
|
35
40
|
* await myApi.editMessage(ref, content);
|
|
36
41
|
* }
|
|
37
42
|
* }
|
|
38
|
-
*
|
|
39
|
-
* // In main.ts:
|
|
40
|
-
* const renderer = new MyRenderer({ verbose: { showThinking: false } });
|
|
41
|
-
*
|
|
42
|
-
* // When user sends a message:
|
|
43
|
-
* renderer.onPromptSent(channelId);
|
|
44
|
-
* try {
|
|
45
|
-
* await agent.prompt({ sessionId, content });
|
|
46
|
-
* await renderer.onTurnEnd(channelId);
|
|
47
|
-
* } catch (e) {
|
|
48
|
-
* await renderer.onTurnError(channelId, String(e));
|
|
49
|
-
* }
|
|
50
|
-
*
|
|
51
|
-
* // In the ACP client's sessionUpdate handler:
|
|
52
|
-
* renderer.onSessionUpdate(notification);
|
|
53
43
|
* ```
|
|
44
|
+
*
|
|
45
|
+
* The SDK's `runChannelPlugin` wires all ACP events to this renderer
|
|
46
|
+
* automatically — plugins don't call onSessionUpdate/onPromptSent/etc
|
|
47
|
+
* directly.
|
|
54
48
|
*/
|
|
55
49
|
|
|
56
50
|
import type { SessionNotification } from "@agentclientprotocol/sdk";
|
|
57
|
-
import type { BlockKind, BlockRendererOptions, VerboseConfig } from "./types.js";
|
|
51
|
+
import type { BlockKind, BlockRendererOptions, CommandEntry, VerboseConfig } from "./types.js";
|
|
58
52
|
|
|
59
53
|
// ---------------------------------------------------------------------------
|
|
60
54
|
// Local ACP session-update narrowing
|
|
@@ -100,7 +94,7 @@ type ConsumedSessionUpdate =
|
|
|
100
94
|
|
|
101
95
|
interface ManagedBlock<TRef> {
|
|
102
96
|
/** Channel this block belongs to. Captured at creation time. */
|
|
103
|
-
|
|
97
|
+
chatId: string;
|
|
104
98
|
kind: BlockKind;
|
|
105
99
|
content: string;
|
|
106
100
|
/** Platform message reference set after the first successful send. */
|
|
@@ -139,13 +133,21 @@ const DEFAULT_MIN_EDIT_INTERVAL_MS = 1000;
|
|
|
139
133
|
* return type of `sendBlock` and the first argument of `editBlock`.
|
|
140
134
|
*/
|
|
141
135
|
export abstract class BlockRenderer<TRef = string> {
|
|
136
|
+
/** When true, blocks are sent and edited in real-time. When false, each
|
|
137
|
+
* block is held until complete, then sent once (send-only mode). */
|
|
138
|
+
protected readonly streaming: boolean;
|
|
142
139
|
protected readonly flushIntervalMs: number;
|
|
143
140
|
protected readonly minEditIntervalMs: number;
|
|
144
141
|
protected readonly verbose: VerboseConfig;
|
|
145
142
|
|
|
146
143
|
private states = new Map<string, ChannelState<TRef>>();
|
|
147
144
|
|
|
145
|
+
/** The chatId of the most recent prompt. Used as fallback target for
|
|
146
|
+
* notifications that arrive without an explicit chatId. */
|
|
147
|
+
private lastActiveChatId: string | null = null;
|
|
148
|
+
|
|
148
149
|
constructor(options: BlockRendererOptions = {}) {
|
|
150
|
+
this.streaming = options.streaming ?? true;
|
|
149
151
|
this.flushIntervalMs = options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
|
|
150
152
|
this.minEditIntervalMs = options.minEditIntervalMs ?? DEFAULT_MIN_EDIT_INTERVAL_MS;
|
|
151
153
|
this.verbose = {
|
|
@@ -155,21 +157,31 @@ export abstract class BlockRenderer<TRef = string> {
|
|
|
155
157
|
}
|
|
156
158
|
|
|
157
159
|
// ---------------------------------------------------------------------------
|
|
158
|
-
// Abstract
|
|
160
|
+
// Abstract — plugin MUST implement these
|
|
159
161
|
// ---------------------------------------------------------------------------
|
|
160
162
|
|
|
161
163
|
/**
|
|
162
|
-
* Send a
|
|
164
|
+
* Send a plain text message to the IM. Used for system text, agent ready
|
|
165
|
+
* notifications, session ready, and error messages.
|
|
166
|
+
*/
|
|
167
|
+
protected abstract sendText(chatId: string, text: string): Promise<void>;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Send a new streaming block message to the platform.
|
|
163
171
|
*
|
|
164
172
|
* Return the platform message reference that will be passed to future
|
|
165
173
|
* `editBlock` calls. Return `null` if editing is not supported.
|
|
166
174
|
*/
|
|
167
175
|
protected abstract sendBlock(
|
|
168
|
-
|
|
176
|
+
chatId: string,
|
|
169
177
|
kind: BlockKind,
|
|
170
178
|
content: string,
|
|
171
179
|
): Promise<TRef | null>;
|
|
172
180
|
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Optional overrides — plugin MAY implement these
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
173
185
|
/**
|
|
174
186
|
* Edit an existing block message in-place.
|
|
175
187
|
*
|
|
@@ -180,7 +192,7 @@ export abstract class BlockRenderer<TRef = string> {
|
|
|
180
192
|
* Use to switch from a "streaming" card format to a finalized one.
|
|
181
193
|
*/
|
|
182
194
|
protected editBlock?(
|
|
183
|
-
|
|
195
|
+
chatId: string,
|
|
184
196
|
ref: TRef,
|
|
185
197
|
kind: BlockKind,
|
|
186
198
|
content: string,
|
|
@@ -207,31 +219,18 @@ export abstract class BlockRenderer<TRef = string> {
|
|
|
207
219
|
|
|
208
220
|
/**
|
|
209
221
|
* Called after the last block has been flushed and the turn is complete.
|
|
210
|
-
* Override to perform cleanup (e.g. remove a "typing" indicator
|
|
211
|
-
* processing reaction, etc.).
|
|
212
|
-
*/
|
|
213
|
-
protected onAfterTurnEnd(_channelId: string): Promise<void> {
|
|
214
|
-
return Promise.resolve();
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Called after a turn error, once state has been cleaned up.
|
|
219
|
-
* Override to send an error message to the user.
|
|
222
|
+
* Override to perform cleanup (e.g. remove a "typing" indicator).
|
|
220
223
|
*/
|
|
221
|
-
protected
|
|
224
|
+
protected onAfterTurnEnd(_chatId: string): Promise<void> {
|
|
222
225
|
return Promise.resolve();
|
|
223
226
|
}
|
|
224
227
|
|
|
225
228
|
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
* Default: identity (sessionId === channelId).
|
|
229
|
-
*
|
|
230
|
-
* Override if your plugin namespaces channel IDs (e.g. Feishu uses
|
|
231
|
-
* `"feishu:<sessionId>"`, WeChat uses `"weixin-openclaw-bridge:<sessionId>"`).
|
|
229
|
+
* Called after a turn error. Default sends an error message via sendText.
|
|
230
|
+
* Override for platform-specific error rendering (e.g. error card).
|
|
232
231
|
*/
|
|
233
|
-
protected
|
|
234
|
-
|
|
232
|
+
protected async onAfterTurnError(chatId: string, error: string): Promise<void> {
|
|
233
|
+
await this.sendText(chatId, `❌ Error: ${error}`);
|
|
235
234
|
}
|
|
236
235
|
|
|
237
236
|
// ---------------------------------------------------------------------------
|
|
@@ -244,14 +243,12 @@ export abstract class BlockRenderer<TRef = string> {
|
|
|
244
243
|
* Routes the event to the correct block based on its variant, appending
|
|
245
244
|
* deltas to the current block or starting a new one when the kind changes.
|
|
246
245
|
*
|
|
247
|
-
*
|
|
246
|
+
* Called automatically by `runChannelPlugin` — plugins don't call this directly.
|
|
248
247
|
*/
|
|
249
248
|
onSessionUpdate(notification: SessionNotification): void {
|
|
250
|
-
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
// of importing the SDK's own union. Variants other than the four we
|
|
254
|
-
// handle are treated as no-ops.
|
|
249
|
+
// sessionId from ACP = chatId (the host replaces the real agent session
|
|
250
|
+
// ID with the chat ID before forwarding to the plugin).
|
|
251
|
+
const chatId = notification.sessionId;
|
|
255
252
|
const rawUpdate = notification.update as unknown as { sessionUpdate: string };
|
|
256
253
|
const variant = rawUpdate.sessionUpdate;
|
|
257
254
|
if (
|
|
@@ -263,47 +260,44 @@ export abstract class BlockRenderer<TRef = string> {
|
|
|
263
260
|
return;
|
|
264
261
|
}
|
|
265
262
|
const update = rawUpdate as ConsumedSessionUpdate;
|
|
266
|
-
const channelId = this.sessionIdToChannelId(sessionId);
|
|
267
263
|
|
|
268
264
|
switch (update.sessionUpdate) {
|
|
269
265
|
case "agent_message_chunk": {
|
|
270
266
|
const delta = update.content?.text ?? "";
|
|
271
|
-
if (delta) this.appendToBlock(
|
|
267
|
+
if (delta) this.appendToBlock(chatId, "text", delta);
|
|
272
268
|
break;
|
|
273
269
|
}
|
|
274
270
|
case "agent_thought_chunk": {
|
|
275
|
-
if (!this.verbose.showThinking) return;
|
|
271
|
+
if (!this.verbose.showThinking) return;
|
|
276
272
|
const delta = update.content?.text ?? "";
|
|
277
|
-
if (delta) this.appendToBlock(
|
|
273
|
+
if (delta) this.appendToBlock(chatId, "thinking", delta);
|
|
278
274
|
break;
|
|
279
275
|
}
|
|
280
276
|
case "tool_call": {
|
|
281
|
-
if (!this.verbose.showToolUse) return;
|
|
282
|
-
if (update.title) this.appendToBlock(
|
|
277
|
+
if (!this.verbose.showToolUse) return;
|
|
278
|
+
if (update.title) this.appendToBlock(chatId, "tool", `🔧 ${update.title}\n`);
|
|
283
279
|
break;
|
|
284
280
|
}
|
|
285
281
|
case "tool_call_update": {
|
|
286
|
-
if (!this.verbose.showToolUse) return;
|
|
282
|
+
if (!this.verbose.showToolUse) return;
|
|
287
283
|
const title = update.title ?? "tool";
|
|
288
284
|
if (update.status === "completed" || update.status === "error") {
|
|
289
|
-
this.appendToBlock(
|
|
285
|
+
this.appendToBlock(chatId, "tool", `✅ ${title}\n`);
|
|
290
286
|
}
|
|
291
287
|
break;
|
|
292
288
|
}
|
|
293
|
-
// Unknown / unconsumed variant — ignore.
|
|
294
289
|
}
|
|
295
290
|
}
|
|
296
291
|
|
|
297
292
|
/**
|
|
298
|
-
* Call
|
|
299
|
-
*
|
|
300
|
-
* Clears any leftover state from a previous turn so the new turn starts
|
|
301
|
-
* with a clean slate.
|
|
293
|
+
* Call before sending a prompt. Tracks the active chatId and clears
|
|
294
|
+
* leftover state from a previous turn.
|
|
302
295
|
*/
|
|
303
|
-
onPromptSent(
|
|
304
|
-
|
|
296
|
+
onPromptSent(chatId: string): void {
|
|
297
|
+
this.lastActiveChatId = chatId;
|
|
298
|
+
const old = this.states.get(chatId);
|
|
305
299
|
if (old?.flushTimer) clearTimeout(old.flushTimer);
|
|
306
|
-
this.states.set(
|
|
300
|
+
this.states.set(chatId, {
|
|
307
301
|
blocks: [],
|
|
308
302
|
flushTimer: null,
|
|
309
303
|
lastEditMs: 0,
|
|
@@ -311,14 +305,70 @@ export abstract class BlockRenderer<TRef = string> {
|
|
|
311
305
|
});
|
|
312
306
|
}
|
|
313
307
|
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
// Host notification handlers — built-in defaults, no per-plugin duplication
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
/** Handle `va/system_text` from host. */
|
|
313
|
+
onSystemText(chatId: string, text: string): void {
|
|
314
|
+
this.sendText(chatId, text).catch(() => {});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Handle `va/agent_ready` from host. */
|
|
318
|
+
onAgentReady(chatId: string, agent: string, version: string): void {
|
|
319
|
+
this.sendText(chatId, `🤖 Agent: ${agent} v${version}`).catch(() => {});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Handle `va/session_ready` from host. */
|
|
323
|
+
onSessionReady(chatId: string, sessionId: string): void {
|
|
324
|
+
this.sendText(chatId, `📋 Session: ${sessionId}`).catch(() => {});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Handle `va/command_menu` from host — display available commands.
|
|
329
|
+
*
|
|
330
|
+
* Default renders a plain-text list. Override for platform-specific
|
|
331
|
+
* rendering (e.g. Feishu interactive card, Slack Block Kit, Telegram
|
|
332
|
+
* inline keyboard).
|
|
333
|
+
*/
|
|
334
|
+
onCommandMenu(
|
|
335
|
+
chatId: string,
|
|
336
|
+
systemCommands: CommandEntry[],
|
|
337
|
+
agentCommands: CommandEntry[],
|
|
338
|
+
): void {
|
|
339
|
+
const lines: string[] = [];
|
|
340
|
+
|
|
341
|
+
lines.push("System commands:");
|
|
342
|
+
for (const cmd of systemCommands) {
|
|
343
|
+
const usage = cmd.args ? `/${cmd.name} ${cmd.args}` : `/${cmd.name}`;
|
|
344
|
+
lines.push(` ${usage} — ${cmd.description}`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (agentCommands.length > 0) {
|
|
348
|
+
lines.push("");
|
|
349
|
+
lines.push("Agent commands (use /agent <command>):");
|
|
350
|
+
for (const cmd of agentCommands) {
|
|
351
|
+
const desc = cmd.description.length > 80
|
|
352
|
+
? `${cmd.description.slice(0, 77)}...`
|
|
353
|
+
: cmd.description;
|
|
354
|
+
lines.push(` /${cmd.name} — ${desc}`);
|
|
355
|
+
}
|
|
356
|
+
} else {
|
|
357
|
+
lines.push("");
|
|
358
|
+
lines.push("Agent commands will appear after sending your first message.");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
this.sendText(chatId, lines.join("\n")).catch(() => {});
|
|
362
|
+
}
|
|
363
|
+
|
|
314
364
|
/**
|
|
315
365
|
* Call this after `agent.prompt()` resolves (turn complete).
|
|
316
366
|
*
|
|
317
367
|
* Seals and flushes the last block, then waits for all pending sends/edits
|
|
318
368
|
* to complete before calling `onAfterTurnEnd`.
|
|
319
369
|
*/
|
|
320
|
-
async onTurnEnd(
|
|
321
|
-
const state = this.states.get(
|
|
370
|
+
async onTurnEnd(chatId: string): Promise<void> {
|
|
371
|
+
const state = this.states.get(chatId);
|
|
322
372
|
if (!state) return;
|
|
323
373
|
|
|
324
374
|
if (state.flushTimer) {
|
|
@@ -332,35 +382,33 @@ export abstract class BlockRenderer<TRef = string> {
|
|
|
332
382
|
this.enqueueFlush(state, last);
|
|
333
383
|
}
|
|
334
384
|
|
|
335
|
-
// Wait for the entire chain to drain before cleanup
|
|
336
385
|
await state.sendChain;
|
|
337
|
-
this.states.delete(
|
|
338
|
-
await this.onAfterTurnEnd(
|
|
386
|
+
this.states.delete(chatId);
|
|
387
|
+
await this.onAfterTurnEnd(chatId);
|
|
339
388
|
}
|
|
340
389
|
|
|
341
390
|
/**
|
|
342
391
|
* Call this when `agent.prompt()` throws (turn error).
|
|
343
392
|
*
|
|
344
|
-
* Discards pending state and calls `onAfterTurnError
|
|
345
|
-
* send an error message to the user.
|
|
393
|
+
* Discards pending state and calls `onAfterTurnError`.
|
|
346
394
|
*/
|
|
347
|
-
async onTurnError(
|
|
348
|
-
const state = this.states.get(
|
|
395
|
+
async onTurnError(chatId: string, error: string): Promise<void> {
|
|
396
|
+
const state = this.states.get(chatId);
|
|
349
397
|
if (state?.flushTimer) clearTimeout(state.flushTimer);
|
|
350
|
-
this.states.delete(
|
|
351
|
-
await this.onAfterTurnError(
|
|
398
|
+
this.states.delete(chatId);
|
|
399
|
+
await this.onAfterTurnError(chatId, error);
|
|
352
400
|
}
|
|
353
401
|
|
|
354
402
|
// ---------------------------------------------------------------------------
|
|
355
403
|
// Internal — block management
|
|
356
404
|
// ---------------------------------------------------------------------------
|
|
357
405
|
|
|
358
|
-
private appendToBlock(
|
|
359
|
-
let state = this.states.get(
|
|
406
|
+
private appendToBlock(chatId: string, kind: BlockKind, delta: string): void {
|
|
407
|
+
let state = this.states.get(chatId);
|
|
360
408
|
if (!state) {
|
|
361
409
|
// Auto-create state if onPromptSent wasn't called (e.g. host-initiated turns)
|
|
362
410
|
state = { blocks: [], flushTimer: null, lastEditMs: 0, sendChain: Promise.resolve() };
|
|
363
|
-
this.states.set(
|
|
411
|
+
this.states.set(chatId, state);
|
|
364
412
|
}
|
|
365
413
|
|
|
366
414
|
const last = state.blocks.at(-1);
|
|
@@ -379,33 +427,30 @@ export abstract class BlockRenderer<TRef = string> {
|
|
|
379
427
|
}
|
|
380
428
|
this.enqueueFlush(state, last);
|
|
381
429
|
}
|
|
382
|
-
state.blocks.push({
|
|
430
|
+
state.blocks.push({ chatId, kind, content: delta, ref: null, creating: false, sealed: false });
|
|
383
431
|
}
|
|
384
432
|
|
|
385
|
-
this.scheduleFlush(
|
|
433
|
+
this.scheduleFlush(chatId, state);
|
|
386
434
|
}
|
|
387
435
|
|
|
388
|
-
private scheduleFlush(
|
|
436
|
+
private scheduleFlush(chatId: string, state: ChannelState<TRef>): void {
|
|
389
437
|
if (state.flushTimer) return; // already scheduled
|
|
390
438
|
|
|
391
439
|
state.flushTimer = setTimeout(() => {
|
|
392
440
|
state.flushTimer = null;
|
|
393
|
-
this.flush(
|
|
441
|
+
this.flush(chatId, state);
|
|
394
442
|
}, this.flushIntervalMs);
|
|
395
443
|
}
|
|
396
444
|
|
|
397
|
-
private flush(
|
|
445
|
+
private flush(chatId: string, state: ChannelState<TRef>): void {
|
|
398
446
|
const block = state.blocks.at(-1);
|
|
399
447
|
if (!block || block.sealed || !block.content) return;
|
|
400
448
|
|
|
401
|
-
// Send-only mode:
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
// boundary transitions inside `appendToBlock` (which seal the block
|
|
407
|
-
// first) will actually POST.
|
|
408
|
-
if (!this.editBlock) {
|
|
449
|
+
// Send-only mode (streaming=false): defer intermediate sends.
|
|
450
|
+
// Only sealed blocks (from onTurnEnd or block boundary transitions)
|
|
451
|
+
// will actually POST. This prevents the user seeing a partial chunk
|
|
452
|
+
// followed by the full message as two separate messages.
|
|
453
|
+
if (!this.streaming) {
|
|
409
454
|
return;
|
|
410
455
|
}
|
|
411
456
|
|
|
@@ -416,7 +461,7 @@ export abstract class BlockRenderer<TRef = string> {
|
|
|
416
461
|
if (!state.flushTimer) {
|
|
417
462
|
state.flushTimer = setTimeout(() => {
|
|
418
463
|
state.flushTimer = null;
|
|
419
|
-
this.flush(
|
|
464
|
+
this.flush(chatId, state);
|
|
420
465
|
}, delay);
|
|
421
466
|
}
|
|
422
467
|
return;
|
|
@@ -439,12 +484,12 @@ export abstract class BlockRenderer<TRef = string> {
|
|
|
439
484
|
if (block.ref === null && !block.creating) {
|
|
440
485
|
// First send — use sentinel to prevent concurrent creates
|
|
441
486
|
block.creating = true;
|
|
442
|
-
block.ref = await this.sendBlock(block.
|
|
487
|
+
block.ref = await this.sendBlock(block.chatId, block.kind, content);
|
|
443
488
|
block.creating = false;
|
|
444
489
|
state.lastEditMs = Date.now();
|
|
445
|
-
} else if (block.ref !== null && !block.creating && this.editBlock) {
|
|
446
|
-
// Subsequent update — edit in-place
|
|
447
|
-
await this.editBlock(block.
|
|
490
|
+
} else if (block.ref !== null && !block.creating && this.streaming && this.editBlock) {
|
|
491
|
+
// Subsequent update — edit in-place (streaming mode only)
|
|
492
|
+
await this.editBlock(block.chatId, block.ref, block.kind, content, block.sealed);
|
|
448
493
|
state.lastEditMs = Date.now();
|
|
449
494
|
}
|
|
450
495
|
// else: create is in-flight (creating === true) — skip
|
package/src/types.ts
CHANGED
|
@@ -43,6 +43,18 @@ export interface PluginManifest {
|
|
|
43
43
|
capabilities?: PluginCapabilities;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Command menu
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
/** A command entry from the host's command list. */
|
|
51
|
+
export interface CommandEntry {
|
|
52
|
+
name: string;
|
|
53
|
+
description: string;
|
|
54
|
+
args?: string;
|
|
55
|
+
aliases?: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
// ---------------------------------------------------------------------------
|
|
47
59
|
// Block rendering
|
|
48
60
|
// ---------------------------------------------------------------------------
|
|
@@ -58,6 +70,18 @@ export interface VerboseConfig {
|
|
|
58
70
|
}
|
|
59
71
|
|
|
60
72
|
export interface BlockRendererOptions {
|
|
73
|
+
/**
|
|
74
|
+
* Whether the IM platform supports message editing (streaming mode).
|
|
75
|
+
*
|
|
76
|
+
* - `true` (default): blocks stream in real-time — `sendBlock()` creates
|
|
77
|
+
* the message, `editBlock()` updates it as more content arrives.
|
|
78
|
+
* - `false`: each block is held until complete, then sent once via
|
|
79
|
+
* `sendBlock()`. `editBlock()` is never called.
|
|
80
|
+
*
|
|
81
|
+
* Set to `false` for platforms that don't support editing sent messages
|
|
82
|
+
* (e.g. QQ Bot, WhatsApp, WeChat, LINE).
|
|
83
|
+
*/
|
|
84
|
+
streaming?: boolean;
|
|
61
85
|
/**
|
|
62
86
|
* Debounce interval before flushing an unsealed block (ms).
|
|
63
87
|
* Controls how often in-progress blocks are sent to the platform.
|
package/dist/run-plugin.d.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* runChannelPlugin — shared main.ts boilerplate for every channel plugin.
|
|
3
|
-
*
|
|
4
|
-
* Each channel plugin used to have a ~120-line `main.ts` that was 85%
|
|
5
|
-
* identical across Slack, Telegram, Discord, DingTalk, WeCom, etc:
|
|
6
|
-
* connect to host, validate config keys, wire the standard sessionUpdate
|
|
7
|
-
* and extNotification handlers, create the stream handler, start the bot,
|
|
8
|
-
* wait for disconnect, stop. This helper absorbs that boilerplate so each
|
|
9
|
-
* plugin's `main.ts` reduces to ~20 lines — a factory for the bot and a
|
|
10
|
-
* factory for the stream handler.
|
|
11
|
-
*
|
|
12
|
-
* ## Usage
|
|
13
|
-
*
|
|
14
|
-
* ```ts
|
|
15
|
-
* import { runChannelPlugin } from "@vibearound/plugin-channel-sdk";
|
|
16
|
-
* import { SlackBot } from "./bot.js";
|
|
17
|
-
* import { AgentStreamHandler } from "./agent-stream.js";
|
|
18
|
-
*
|
|
19
|
-
* runChannelPlugin({
|
|
20
|
-
* name: "vibearound-slack",
|
|
21
|
-
* version: "0.1.0",
|
|
22
|
-
* requiredConfig: ["bot_token", "app_token"],
|
|
23
|
-
* createBot: ({ config, agent, log, cacheDir }) =>
|
|
24
|
-
* new SlackBot(
|
|
25
|
-
* { bot_token: config.bot_token as string, app_token: config.app_token as string },
|
|
26
|
-
* agent,
|
|
27
|
-
* log,
|
|
28
|
-
* cacheDir,
|
|
29
|
-
* ),
|
|
30
|
-
* createStreamHandler: (bot, log, verbose) =>
|
|
31
|
-
* new AgentStreamHandler(bot, log, verbose),
|
|
32
|
-
* });
|
|
33
|
-
* ```
|
|
34
|
-
*/
|
|
35
|
-
import type { Agent } from "@agentclientprotocol/sdk";
|
|
36
|
-
import type { SessionNotification } from "./types.js";
|
|
37
|
-
export type ChannelPluginLogger = (level: string, msg: string) => void;
|
|
38
|
-
export interface ChannelStreamHandler {
|
|
39
|
-
onSessionUpdate(params: SessionNotification): void;
|
|
40
|
-
onSystemText(text: string): void;
|
|
41
|
-
onAgentReady(agent: string, version: string): void;
|
|
42
|
-
onSessionReady(sessionId: string): void;
|
|
43
|
-
}
|
|
44
|
-
export interface ChannelBot<THandler extends ChannelStreamHandler = ChannelStreamHandler> {
|
|
45
|
-
setStreamHandler(handler: THandler): void;
|
|
46
|
-
start(): Promise<void> | void;
|
|
47
|
-
stop(): Promise<void> | void;
|
|
48
|
-
}
|
|
49
|
-
export interface CreateBotContext {
|
|
50
|
-
config: Record<string, unknown>;
|
|
51
|
-
agent: Agent;
|
|
52
|
-
log: ChannelPluginLogger;
|
|
53
|
-
cacheDir: string;
|
|
54
|
-
}
|
|
55
|
-
export interface VerboseOptions {
|
|
56
|
-
showThinking: boolean;
|
|
57
|
-
showToolUse: boolean;
|
|
58
|
-
}
|
|
59
|
-
export interface RunChannelPluginSpec<TBot extends ChannelBot<THandler>, THandler extends ChannelStreamHandler> {
|
|
60
|
-
/** Plugin name reported during ACP initialize (e.g. "vibearound-slack"). */
|
|
61
|
-
name: string;
|
|
62
|
-
/** Plugin version reported during ACP initialize. */
|
|
63
|
-
version: string;
|
|
64
|
-
/**
|
|
65
|
-
* Config keys that MUST be present on `meta.config`. Plugin startup
|
|
66
|
-
* fails with a clear error if any are missing. Keep to primitives
|
|
67
|
-
* (strings/booleans); deeper validation belongs in the bot constructor.
|
|
68
|
-
*/
|
|
69
|
-
requiredConfig?: string[];
|
|
70
|
-
/** Factory: build the platform bot from host-supplied config + agent. */
|
|
71
|
-
createBot: (ctx: CreateBotContext) => TBot | Promise<TBot>;
|
|
72
|
-
/**
|
|
73
|
-
* Factory: build the agent stream handler for this plugin. The handler
|
|
74
|
-
* is wired to the bot via `bot.setStreamHandler(handler)` before the
|
|
75
|
-
* bot is started.
|
|
76
|
-
*/
|
|
77
|
-
createStreamHandler: (bot: TBot, log: ChannelPluginLogger, verbose: VerboseOptions) => THandler;
|
|
78
|
-
/**
|
|
79
|
-
* Optional hook invoked after the bot has been constructed but before
|
|
80
|
-
* `start()` is called. Use this for one-off initialization that needs
|
|
81
|
-
* to log diagnostic info (e.g. Telegram's `probe()`).
|
|
82
|
-
*/
|
|
83
|
-
afterCreate?: (bot: TBot, log: ChannelPluginLogger) => Promise<void> | void;
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Run a channel plugin to completion.
|
|
87
|
-
*
|
|
88
|
-
* Performs the ACP initialize handshake, validates required config,
|
|
89
|
-
* constructs the bot + stream handler, starts the bot, waits for the host
|
|
90
|
-
* connection to close, then stops the bot and exits the process.
|
|
91
|
-
*
|
|
92
|
-
* Never returns under normal operation — the process exits at the end.
|
|
93
|
-
*/
|
|
94
|
-
export declare function runChannelPlugin<TBot extends ChannelBot<THandler>, THandler extends ChannelStreamHandler>(spec: RunChannelPluginSpec<TBot, THandler>): Promise<void>;
|
|
95
|
-
//# sourceMappingURL=run-plugin.d.ts.map
|
package/dist/run-plugin.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"run-plugin.d.ts","sourceRoot":"","sources":["../src/run-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAIH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAC;AAItD,OAAO,KAAK,EAGV,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAMpB,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;AAEvE,MAAM,WAAW,oBAAoB;IACnC,eAAe,CAAC,MAAM,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACnD,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACnD,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CACzC;AAED,MAAM,WAAW,UAAU,CAAC,QAAQ,SAAS,oBAAoB,GAAG,oBAAoB;IACtF,gBAAgB,CAAC,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC1C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC9B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,KAAK,EAAE,KAAK,CAAC;IACb,GAAG,EAAE,mBAAmB,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB,CACnC,IAAI,SAAS,UAAU,CAAC,QAAQ,CAAC,EACjC,QAAQ,SAAS,oBAAoB;IAErC,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IAEb,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAC;IAEhB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAE1B,yEAAyE;IACzE,SAAS,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3D;;;;OAIG;IACH,mBAAmB,EAAE,CACnB,GAAG,EAAE,IAAI,EACT,GAAG,EAAE,mBAAmB,EACxB,OAAO,EAAE,cAAc,KACpB,QAAQ,CAAC;IAEd;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,mBAAmB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAC7E;AAMD;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,SAAS,UAAU,CAAC,QAAQ,CAAC,EACjC,QAAQ,SAAS,oBAAoB,EACrC,IAAI,EAAE,oBAAoB,CAAC,IAAI,EAAE,QAAQ,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAY3D"}
|
package/dist/run-plugin.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"run-plugin.js","sourceRoot":"","sources":["../src/run-plugin.ts"],"names":[],"mappings":"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"}
|