@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 +12 -0
- package/package.json +1 -1
- package/src/cards.ts +9 -39
- package/src/session.ts +24 -0
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
package/src/cards.ts
CHANGED
|
@@ -406,47 +406,17 @@ export function askUserQuestionElement(
|
|
|
406
406
|
})
|
|
407
407
|
}
|
|
408
408
|
}
|
|
409
|
-
// Custom-answer
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
//
|
|
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: '
|
|
417
|
-
|
|
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
|