@leviyuan/lodestar 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/daemon.ts CHANGED
@@ -108,6 +108,18 @@ async function handleMessage(data: any): Promise<void> {
108
108
  if (await session.runCommand(text)) return
109
109
  }
110
110
 
111
+ // Pending AskUserQuestion: route the message as a custom answer
112
+ // instead of opening a new turn. This is how custom-text answers
113
+ // work in this version — Feishu schema 2.0 doesn't support form/
114
+ // input elements, so the chat box itself is the input. Only applies
115
+ // to text-only messages (an image attachment opens a new turn as
116
+ // usual). Bare-word commands have already been intercepted above.
117
+ if (msgType === 'text' && text && session.hasPendingAsk()) {
118
+ const userId = message.sender?.sender_id?.open_id ?? ''
119
+ await session.onAskMessageAnswer(text, userId)
120
+ return
121
+ }
122
+
111
123
  let filePath: string | undefined
112
124
  if (msgType === 'image' && contentObj.image_key) {
113
125
  filePath = await feishu.downloadAttachment(message.message_id, contentObj.image_key, 'image')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leviyuan/lodestar",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/cards.ts CHANGED
@@ -406,47 +406,17 @@ export function askUserQuestionElement(
406
406
  })
407
407
  }
408
408
  }
409
- // Custom-answer input (pending state only). Feishu form submit
410
- // packages the input value under `form_value` in the callback
411
- // payload; daemon.handleCardAction reads it as
412
- // `value.form_value.custom_answer`.
409
+ // Custom-answer hint (pending state only). Feishu schema 2.0
410
+ // doesn't actually have `form` / `input` elements `tag: form`
411
+ // gets rejected with code 300315. Instead we route custom
412
+ // answers through a plain chat message: daemon.handleMessage
413
+ // detects a pending ask and forwards the next inbound text as
414
+ // the answer, no new turn opened. This is also more Feishu-
415
+ // native: the chat input is right there, no extra widget.
413
416
  if (!isAnswered) {
414
- bodyElements.push({ tag: 'markdown', content: '_或自己输入:_' })
415
417
  bodyElements.push({
416
- tag: 'form',
417
- name: `ask_form_${toolUseId.slice(-12)}`,
418
- elements: [{
419
- tag: 'column_set',
420
- columns: [
421
- {
422
- tag: 'column', width: 'weighted', weight: 3, vertical_align: 'center',
423
- elements: [{
424
- tag: 'input',
425
- name: 'custom_answer',
426
- placeholder: { tag: 'plain_text', content: '自定义回答…' },
427
- required: false,
428
- }],
429
- },
430
- {
431
- tag: 'column', width: 'weighted', weight: 1, vertical_align: 'center',
432
- elements: [{
433
- tag: 'button',
434
- text: { tag: 'plain_text', content: '发送' },
435
- type: 'primary',
436
- form_action_type: 'submit',
437
- behaviors: [{
438
- type: 'callback',
439
- value: {
440
- kind: 'ask',
441
- tool_use_id: toolUseId,
442
- question_idx: 0,
443
- custom: true,
444
- },
445
- }],
446
- }],
447
- },
448
- ],
449
- }],
418
+ tag: 'markdown',
419
+ content: '_💬 也可以直接在群里回复你的答案(裸词命令 `hi`/`kill`/`restart`/`clear` 仍然优先)_',
450
420
  })
451
421
  } else if (answered?.customText) {
452
422
  bodyElements.push({
package/src/session.ts CHANGED
@@ -332,6 +332,30 @@ export class Session {
332
332
  }
333
333
  }
334
334
 
335
+ /** True iff there's at least one open AskUserQuestion awaiting an
336
+ * answer in this session. `daemon.handleMessage` uses this to
337
+ * decide whether an inbound chat message should be a custom answer
338
+ * (routed to onAskMessageAnswer) instead of opening a new turn. */
339
+ hasPendingAsk(): boolean {
340
+ return this.pendingAsks.size > 0
341
+ }
342
+
343
+ /** Funnel an arbitrary chat message into the oldest pending ask as
344
+ * a `customText` answer. Falls back to a normal turn if for some
345
+ * reason there's nothing pending (defensive — handleMessage's gate
346
+ * should prevent that). Picks the first entry by Map insertion
347
+ * order, which is the earliest unanswered ask. */
348
+ async onAskMessageAnswer(text: string, user: string): Promise<void> {
349
+ const firstEntry = this.pendingAsks.entries().next()
350
+ if (firstEntry.done) {
351
+ log(`session "${this.sessionName}": onAskMessageAnswer with no pending — falling back to onUserMessage`)
352
+ await this.onUserMessage(text)
353
+ return
354
+ }
355
+ const [toolUseId, _pending] = firstEntry.value
356
+ await this.onAskCustomAnswer(toolUseId, 0, text, user)
357
+ }
358
+
335
359
  /** Click handler for an AskUserQuestion option button. Dispatches
336
360
  * to `resolveAsk` if can_use_tool has already arrived, otherwise
337
361
  * parks the click on the pendingAsk record for renderPermission