opencode-tbot 0.1.5 → 0.1.7

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 CHANGED
@@ -22,7 +22,7 @@ A Telegram plugin for driving [OpenCode](https://opencode.ai) from chat.
22
22
  Run:
23
23
 
24
24
  ```bash
25
- npx opencode-tbot@latest install
25
+ npm install opencode-tbot@latest
26
26
  ```
27
27
 
28
28
  The installer registers the plugin globally and writes the default runtime config.
package/README.zh-CN.md CHANGED
@@ -22,7 +22,7 @@
22
22
  执行:
23
23
 
24
24
  ```bash
25
- npx opencode-tbot@latest install
25
+ npm install opencode-tbot@latest
26
26
  ```
27
27
 
28
28
  安装器会注册全局插件并写入默认运行时配置。
package/dist/plugin.js CHANGED
@@ -214,6 +214,17 @@ function buildOpenCodeSdkConfig(options) {
214
214
  };
215
215
  }
216
216
  var EMPTY_RESPONSE_TEXT = "OpenCode returned empty response.";
217
+ var PROMPT_MESSAGE_POLL_DELAYS_MS = [
218
+ 0,
219
+ 100,
220
+ 250,
221
+ 500,
222
+ 1e3,
223
+ 2e3,
224
+ 4e3,
225
+ 8e3
226
+ ];
227
+ var PROMPT_MESSAGE_POLL_LIMIT = 20;
217
228
  var STRUCTURED_REPLY_SCHEMA = {
218
229
  type: "json_schema",
219
230
  retryCount: 2,
@@ -327,7 +338,8 @@ var OpenCodeClient = class {
327
338
  url: file.url
328
339
  }))];
329
340
  if (parts.length === 0) throw new Error("Prompt requires text or file attachments.");
330
- const data = unwrapSdkData(await this.client.session.prompt({
341
+ const knownMessageIds = await this.captureKnownMessageIds(input.sessionId);
342
+ const initialData = unwrapSdkData(await this.client.session.prompt({
331
343
  sessionID: input.sessionId,
332
344
  ...input.agent ? { agent: input.agent } : {},
333
345
  ...input.structured ? { format: STRUCTURED_REPLY_SCHEMA } : {},
@@ -335,19 +347,96 @@ var OpenCodeClient = class {
335
347
  ...input.variant ? { variant: input.variant } : {},
336
348
  parts
337
349
  }, SDK_OPTIONS));
338
- const finishedAt = Date.now();
339
- const bodyMd = input.structured ? extractStructuredMarkdown(data.info?.structured) : null;
340
- const responseParts = Array.isArray(data.parts) ? data.parts : [];
341
- const fallbackText = extractTextFromParts(responseParts) || bodyMd || EMPTY_RESPONSE_TEXT;
342
- return {
343
- assistantError: data.info?.error ?? null,
344
- bodyMd,
345
- fallbackText,
346
- info: data.info ?? null,
347
- metrics: extractPromptMetrics(data.info, startedAt, finishedAt),
348
- parts: responseParts,
349
- structured: data.info?.structured ?? null
350
- };
350
+ return buildPromptSessionResult(await this.resolvePromptResponse(input, initialData, knownMessageIds), {
351
+ emptyResponseText: EMPTY_RESPONSE_TEXT,
352
+ finishedAt: Date.now(),
353
+ startedAt,
354
+ structured: input.structured ?? false
355
+ });
356
+ }
357
+ async resolvePromptResponse(input, data, knownMessageIds) {
358
+ if (!shouldPollPromptMessage(data, input.structured ?? false)) return data;
359
+ const messageId = data.info?.id;
360
+ let bestCandidate = data;
361
+ if (!messageId) return await this.findLatestPromptResponse(input.sessionId, {
362
+ initialMessageId: null,
363
+ knownMessageIds,
364
+ structured: input.structured ?? false
365
+ }) ?? data;
366
+ for (const delayMs of PROMPT_MESSAGE_POLL_DELAYS_MS) {
367
+ if (delayMs > 0) await delay(delayMs);
368
+ const next = await this.fetchPromptMessage(input.sessionId, messageId);
369
+ if (!next) {
370
+ const latest = await this.findLatestPromptResponse(input.sessionId, {
371
+ initialMessageId: messageId,
372
+ knownMessageIds,
373
+ structured: input.structured ?? false
374
+ });
375
+ if (latest) {
376
+ bestCandidate = latest;
377
+ if (!shouldPollPromptMessage(bestCandidate, input.structured ?? false)) return bestCandidate;
378
+ }
379
+ continue;
380
+ }
381
+ data = next;
382
+ bestCandidate = next;
383
+ if (!shouldPollPromptMessage(data, input.structured ?? false)) return data;
384
+ const latest = await this.findLatestPromptResponse(input.sessionId, {
385
+ initialMessageId: messageId,
386
+ knownMessageIds,
387
+ structured: input.structured ?? false
388
+ });
389
+ if (latest) {
390
+ bestCandidate = latest;
391
+ if (!shouldPollPromptMessage(bestCandidate, input.structured ?? false)) return bestCandidate;
392
+ }
393
+ }
394
+ return bestCandidate;
395
+ }
396
+ async fetchPromptMessage(sessionId, messageId) {
397
+ if (typeof this.client.session.message !== "function") return null;
398
+ try {
399
+ return unwrapSdkData(await this.client.session.message({
400
+ sessionID: sessionId,
401
+ messageID: messageId
402
+ }, SDK_OPTIONS));
403
+ } catch {
404
+ return null;
405
+ }
406
+ }
407
+ async captureKnownMessageIds(sessionId) {
408
+ const messages = await this.fetchRecentPromptMessages(sessionId);
409
+ if (!messages) return /* @__PURE__ */ new Set();
410
+ return new Set(messages.map((message) => message.info?.id).filter((id) => typeof id === "string" && id.length > 0));
411
+ }
412
+ async fetchRecentPromptMessages(sessionId) {
413
+ if (typeof this.client.session.messages !== "function") return null;
414
+ try {
415
+ const data = unwrapSdkData(await this.client.session.messages({
416
+ sessionID: sessionId,
417
+ limit: PROMPT_MESSAGE_POLL_LIMIT
418
+ }, SDK_OPTIONS));
419
+ return Array.isArray(data) ? data : null;
420
+ } catch {
421
+ return null;
422
+ }
423
+ }
424
+ async findLatestPromptResponse(sessionId, options) {
425
+ const messages = await this.fetchRecentPromptMessages(sessionId);
426
+ if (!messages || messages.length === 0) return null;
427
+ const candidates = messages.filter((message) => toAssistantMessage(message.info) !== null).map((message) => {
428
+ const assistant = toAssistantMessage(message.info);
429
+ const id = assistant?.id ?? null;
430
+ return {
431
+ createdAt: typeof assistant?.time?.created === "number" && Number.isFinite(assistant.time.created) ? assistant.time.created : 0,
432
+ id,
433
+ isInitial: !!id && id === options.initialMessageId,
434
+ isNew: !!id && !options.knownMessageIds.has(id),
435
+ isUsable: !shouldPollPromptMessage(message, options.structured),
436
+ message
437
+ };
438
+ }).sort((left, right) => Number(right.isUsable) - Number(left.isUsable) || Number(right.isNew) - Number(left.isNew) || Number(right.isInitial) - Number(left.isInitial) || right.createdAt - left.createdAt);
439
+ return (candidates.find((candidate) => candidate.isUsable && (candidate.isNew || candidate.isInitial)) ?? candidates.find((candidate) => candidate.isNew || candidate.isInitial) ?? null)?.message ?? null;
351
440
  }
352
441
  async loadModels() {
353
442
  const [configResponse, providersResponse] = await Promise.all([this.client.config.get(void 0, SDK_OPTIONS), this.client.config.providers(void 0, SDK_OPTIONS)]);
@@ -450,6 +539,36 @@ function extractTextFromParts(parts) {
450
539
  if (!Array.isArray(parts)) return "";
451
540
  return parts.filter((part) => part.type === "text").map((part) => part.text).join("").trim();
452
541
  }
542
+ function buildPromptSessionResult(data, options) {
543
+ const assistantInfo = toAssistantMessage(data.info);
544
+ const bodyMd = options.structured ? extractStructuredMarkdown(assistantInfo?.structured) : null;
545
+ const responseParts = Array.isArray(data.parts) ? data.parts : [];
546
+ const fallbackText = extractTextFromParts(responseParts) || bodyMd || options.emptyResponseText;
547
+ return {
548
+ assistantError: assistantInfo?.error ?? null,
549
+ bodyMd,
550
+ fallbackText,
551
+ info: assistantInfo,
552
+ metrics: extractPromptMetrics(assistantInfo, options.startedAt, options.finishedAt),
553
+ parts: responseParts,
554
+ structured: assistantInfo?.structured ?? null
555
+ };
556
+ }
557
+ function shouldPollPromptMessage(data, structured) {
558
+ const assistantInfo = toAssistantMessage(data.info);
559
+ const bodyMd = structured ? extractStructuredMarkdown(assistantInfo?.structured) : null;
560
+ const hasText = extractTextFromParts(Array.isArray(data.parts) ? data.parts : []).length > 0;
561
+ const hasAssistantError = !!assistantInfo?.error;
562
+ const isCompleted = typeof assistantInfo?.time?.completed === "number" && Number.isFinite(assistantInfo.time.completed);
563
+ return !hasText && !bodyMd && !hasAssistantError && !isCompleted;
564
+ }
565
+ function toAssistantMessage(message) {
566
+ if (!message || typeof message !== "object") return null;
567
+ return !("role" in message) || message.role === "assistant" ? message : null;
568
+ }
569
+ function delay(ms) {
570
+ return new Promise((resolve) => setTimeout(resolve, ms));
571
+ }
453
572
  function extractStructuredMarkdown(structured) {
454
573
  const parsed = StructuredReplySchema.safeParse(structured);
455
574
  if (!parsed.success) return null;