@leviyuan/lodestar 0.1.0 → 2.0.14
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 +76 -67
- package/cli.ts +176 -0
- package/config.ts +135 -0
- package/daemon.ts +1080 -144
- package/email-worker.ts +534 -0
- package/env-bootstrap.ts +7 -0
- package/feishu-mcp.ts +482 -0
- package/package.json +36 -37
- package/runtime-api.ts +569 -0
- package/scripts/runtime-thread.sh +91 -0
- package/status-dashboard.ts +733 -0
- package/src/cardkit.ts +0 -215
- package/src/cards.ts +0 -304
- package/src/claude-process.ts +0 -301
- package/src/config.ts +0 -83
- package/src/feishu.ts +0 -365
- package/src/instructions.ts +0 -22
- package/src/log.ts +0 -11
- package/src/paths.ts +0 -41
- package/src/session.ts +0 -447
package/src/cardkit.ts
DELETED
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Feishu Card Kit v1 wrapper.
|
|
3
|
-
*
|
|
4
|
-
* Endpoints used (base = https://open.feishu.cn/open-apis/cardkit/v1):
|
|
5
|
-
* POST /cards/id_convert message_id → card_id
|
|
6
|
-
* POST /cards create a card entity
|
|
7
|
-
* PUT /cards/:card_id/elements/:element_id/content stream text (typewriter)
|
|
8
|
-
* POST /cards/:card_id/elements add element
|
|
9
|
-
* PUT /cards/:card_id/elements/:element_id replace element
|
|
10
|
-
* DELETE /cards/:card_id/elements/:element_id remove element
|
|
11
|
-
* PATCH /cards/:card_id/settings toggle streaming_mode etc.
|
|
12
|
-
*
|
|
13
|
-
* Per-card invariants enforced here:
|
|
14
|
-
* - `sequence` is monotonically increasing per card_id
|
|
15
|
-
* - all writes for a card are serialized through a Promise queue
|
|
16
|
-
* - text-streaming PUTs are batched on a 120ms / 32-char heuristic to
|
|
17
|
-
* stay well under cardkit's per-card rate ceiling
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { getTenantToken } from './feishu'
|
|
21
|
-
import { log } from './log'
|
|
22
|
-
|
|
23
|
-
const BASE = 'https://open.feishu.cn/open-apis/cardkit/v1'
|
|
24
|
-
|
|
25
|
-
const FLUSH_INTERVAL_MS = 120
|
|
26
|
-
const FLUSH_MIN_DELTA = 32
|
|
27
|
-
|
|
28
|
-
interface CardState {
|
|
29
|
-
sequence: number
|
|
30
|
-
queue: Promise<void>
|
|
31
|
-
buffer: Map<string, string> // element_id → latest full text
|
|
32
|
-
lastSent: Map<string, string> // element_id → text last actually PUT
|
|
33
|
-
flushTimer: ReturnType<typeof setTimeout> | null
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const cards = new Map<string, CardState>()
|
|
37
|
-
|
|
38
|
-
function state(cardId: string): CardState {
|
|
39
|
-
let s = cards.get(cardId)
|
|
40
|
-
if (!s) {
|
|
41
|
-
s = {
|
|
42
|
-
sequence: 0,
|
|
43
|
-
queue: Promise.resolve(),
|
|
44
|
-
buffer: new Map(),
|
|
45
|
-
lastSent: new Map(),
|
|
46
|
-
flushTimer: null,
|
|
47
|
-
}
|
|
48
|
-
cards.set(cardId, s)
|
|
49
|
-
}
|
|
50
|
-
return s
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function nextSeq(cardId: string): number {
|
|
54
|
-
const s = state(cardId)
|
|
55
|
-
s.sequence += 1
|
|
56
|
-
return s.sequence
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function call(method: string, path: string, body?: object): Promise<any> {
|
|
60
|
-
const token = await getTenantToken()
|
|
61
|
-
const res = await fetch(`${BASE}${path}`, {
|
|
62
|
-
method,
|
|
63
|
-
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
64
|
-
...(body ? { body: JSON.stringify(body) } : {}),
|
|
65
|
-
})
|
|
66
|
-
const json = await res.json() as any
|
|
67
|
-
if (json?.code && json.code !== 0) {
|
|
68
|
-
throw new Error(`cardkit ${method} ${path}: code=${json.code} msg=${json.msg}`)
|
|
69
|
-
}
|
|
70
|
-
return json?.data
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Convert a sent interactive message into a card entity. */
|
|
74
|
-
export async function convertMessageToCard(messageId: string): Promise<string> {
|
|
75
|
-
const data = await call('POST', '/cards/id_convert', { message_id: messageId })
|
|
76
|
-
return data.card_id
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Create a card entity from raw schema-2.0 card JSON. */
|
|
80
|
-
export async function createCardEntity(card: object): Promise<string> {
|
|
81
|
-
const data = await call('POST', '/cards', {
|
|
82
|
-
type: 'card_json',
|
|
83
|
-
data: JSON.stringify(card),
|
|
84
|
-
})
|
|
85
|
-
return data.card_id
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** PUT element content (full text) — triggers typewriter on prefix-match.
|
|
89
|
-
*
|
|
90
|
-
* NOTE: CardKit rejects empty-string content with code 99992402 ("field
|
|
91
|
-
* validation failed"); we drop empty/whitespace-only writes here so callers
|
|
92
|
-
* can stream naively without per-call empty checks. */
|
|
93
|
-
export function streamText(cardId: string, elementId: string, content: string): Promise<void> {
|
|
94
|
-
if (!content || !content.trim()) return Promise.resolve()
|
|
95
|
-
const s = state(cardId)
|
|
96
|
-
const seq = nextSeq(cardId)
|
|
97
|
-
s.queue = s.queue.then(async () => {
|
|
98
|
-
try {
|
|
99
|
-
await call('PUT', `/cards/${cardId}/elements/${elementId}/content`, {
|
|
100
|
-
content, sequence: seq,
|
|
101
|
-
})
|
|
102
|
-
s.lastSent.set(elementId, content)
|
|
103
|
-
} catch (e) {
|
|
104
|
-
log(`cardkit streamText ${cardId}/${elementId}: ${e}`)
|
|
105
|
-
}
|
|
106
|
-
})
|
|
107
|
-
return s.queue
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** Throttled streaming: buffer + auto-flush every FLUSH_INTERVAL_MS or
|
|
111
|
-
* when the buffered delta crosses FLUSH_MIN_DELTA characters. */
|
|
112
|
-
export function streamTextThrottled(cardId: string, elementId: string, fullContent: string): void {
|
|
113
|
-
if (!fullContent || !fullContent.trim()) return
|
|
114
|
-
const s = state(cardId)
|
|
115
|
-
s.buffer.set(elementId, fullContent)
|
|
116
|
-
|
|
117
|
-
const last = s.lastSent.get(elementId) ?? ''
|
|
118
|
-
const delta = fullContent.length - last.length
|
|
119
|
-
if (delta >= FLUSH_MIN_DELTA) {
|
|
120
|
-
flush(cardId).catch(e => log(`cardkit flush(min-delta) ${cardId}: ${e}`))
|
|
121
|
-
return
|
|
122
|
-
}
|
|
123
|
-
if (!s.flushTimer) {
|
|
124
|
-
s.flushTimer = setTimeout(() => {
|
|
125
|
-
flush(cardId).catch(e => log(`cardkit flush(timer) ${cardId}: ${e}`))
|
|
126
|
-
}, FLUSH_INTERVAL_MS)
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Force an immediate flush of the buffered streams for a card. */
|
|
131
|
-
export async function flush(cardId: string): Promise<void> {
|
|
132
|
-
const s = cards.get(cardId)
|
|
133
|
-
if (!s) return
|
|
134
|
-
if (s.flushTimer) { clearTimeout(s.flushTimer); s.flushTimer = null }
|
|
135
|
-
const pending = [...s.buffer.entries()]
|
|
136
|
-
s.buffer.clear()
|
|
137
|
-
for (const [eid, text] of pending) {
|
|
138
|
-
if (s.lastSent.get(eid) === text) continue
|
|
139
|
-
await streamText(cardId, eid, text)
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** Add a new element to the card body or relative to a sibling. */
|
|
144
|
-
export function addElement(
|
|
145
|
-
cardId: string,
|
|
146
|
-
element: object,
|
|
147
|
-
opts: { type?: 'append' | 'insert_before' | 'insert_after'; targetElementId?: string } = {},
|
|
148
|
-
): Promise<void> {
|
|
149
|
-
const s = state(cardId)
|
|
150
|
-
const seq = nextSeq(cardId)
|
|
151
|
-
s.queue = s.queue.then(async () => {
|
|
152
|
-
try {
|
|
153
|
-
await call('POST', `/cards/${cardId}/elements`, {
|
|
154
|
-
type: opts.type ?? 'append',
|
|
155
|
-
...(opts.targetElementId ? { target_element_id: opts.targetElementId } : {}),
|
|
156
|
-
elements: JSON.stringify([element]),
|
|
157
|
-
sequence: seq,
|
|
158
|
-
})
|
|
159
|
-
} catch (e) { log(`cardkit addElement ${cardId}: ${e}`) }
|
|
160
|
-
})
|
|
161
|
-
return s.queue
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** Replace an entire element (used to swap a tool placeholder with its result). */
|
|
165
|
-
export function replaceElement(cardId: string, elementId: string, element: object): Promise<void> {
|
|
166
|
-
const s = state(cardId)
|
|
167
|
-
const seq = nextSeq(cardId)
|
|
168
|
-
s.queue = s.queue.then(async () => {
|
|
169
|
-
try {
|
|
170
|
-
await call('PUT', `/cards/${cardId}/elements/${elementId}`, {
|
|
171
|
-
element: JSON.stringify(element),
|
|
172
|
-
sequence: seq,
|
|
173
|
-
})
|
|
174
|
-
} catch (e) { log(`cardkit replaceElement ${cardId}/${elementId}: ${e}`) }
|
|
175
|
-
})
|
|
176
|
-
return s.queue
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** Delete an element by id. */
|
|
180
|
-
export function deleteElement(cardId: string, elementId: string): Promise<void> {
|
|
181
|
-
const s = state(cardId)
|
|
182
|
-
const seq = nextSeq(cardId)
|
|
183
|
-
s.queue = s.queue.then(async () => {
|
|
184
|
-
try {
|
|
185
|
-
await call('DELETE', `/cards/${cardId}/elements/${elementId}`, {
|
|
186
|
-
sequence: seq,
|
|
187
|
-
})
|
|
188
|
-
} catch (e) { log(`cardkit deleteElement ${cardId}/${elementId}: ${e}`) }
|
|
189
|
-
})
|
|
190
|
-
return s.queue
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** Patch settings — used to flip streaming_mode off when a turn finishes. */
|
|
194
|
-
export function patchSettings(cardId: string, settings: object): Promise<void> {
|
|
195
|
-
const s = state(cardId)
|
|
196
|
-
const seq = nextSeq(cardId)
|
|
197
|
-
s.queue = s.queue.then(async () => {
|
|
198
|
-
try {
|
|
199
|
-
await call('PATCH', `/cards/${cardId}/settings`, {
|
|
200
|
-
settings: JSON.stringify(settings),
|
|
201
|
-
sequence: seq,
|
|
202
|
-
})
|
|
203
|
-
} catch (e) { log(`cardkit patchSettings ${cardId}: ${e}`) }
|
|
204
|
-
})
|
|
205
|
-
return s.queue
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** Drop in-memory bookkeeping for a finished card. */
|
|
209
|
-
export async function dispose(cardId: string): Promise<void> {
|
|
210
|
-
const s = cards.get(cardId)
|
|
211
|
-
if (!s) return
|
|
212
|
-
await flush(cardId)
|
|
213
|
-
await s.queue
|
|
214
|
-
cards.delete(cardId)
|
|
215
|
-
}
|
package/src/cards.ts
DELETED
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Schema 2.0 Feishu card templates.
|
|
3
|
-
*
|
|
4
|
-
* Element-id convention (must be unique within a card):
|
|
5
|
-
* user_input — the collapsible "你说" panel
|
|
6
|
-
* thinking — the de-emphasized thinking stream
|
|
7
|
-
* tool_<i> — one collapsible per tool call, indexed from 0
|
|
8
|
-
* assistant — the main streaming assistant answer
|
|
9
|
-
* footer — runtime footer (timing / status)
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
export const ELEMENTS = {
|
|
13
|
-
thinking: 'thinking',
|
|
14
|
-
footer: 'footer',
|
|
15
|
-
tool: (i: number) => `tool_${i}`,
|
|
16
|
-
/** Assistant text is segmented: every tool call closes the running segment
|
|
17
|
-
* and the next assistant chunk opens a new one, so element order in the
|
|
18
|
-
* card matches Claude's emission order. */
|
|
19
|
-
assistant: (i: number) => `assistant_${i}`,
|
|
20
|
-
} as const
|
|
21
|
-
|
|
22
|
-
/** Single-line summary used as a collapsible-panel header for a tool call. */
|
|
23
|
-
export function summarizeToolInput(name: string, input: any): string {
|
|
24
|
-
if (!input || typeof input !== 'object') return ''
|
|
25
|
-
const truncate = (s: string, n: number) => s.length > n ? s.slice(0, n) + '…' : s
|
|
26
|
-
switch (name) {
|
|
27
|
-
case 'Bash': return truncate(String(input.command ?? ''), 80)
|
|
28
|
-
case 'Read':
|
|
29
|
-
case 'Write':
|
|
30
|
-
case 'Edit':
|
|
31
|
-
case 'NotebookEdit': return truncate(String(input.file_path ?? ''), 80)
|
|
32
|
-
case 'Glob': return truncate(String(input.pattern ?? ''), 80)
|
|
33
|
-
case 'Grep': return truncate(`${input.pattern ?? ''}${input.path ? ' in ' + input.path : ''}`, 80)
|
|
34
|
-
case 'WebFetch':
|
|
35
|
-
case 'WebSearch': return truncate(String(input.url ?? input.query ?? ''), 80)
|
|
36
|
-
case 'Agent':
|
|
37
|
-
case 'Task': return truncate(String(input.description ?? input.subject ?? ''), 80)
|
|
38
|
-
case 'Skill': return truncate(String(input.skill ?? ''), 80)
|
|
39
|
-
}
|
|
40
|
-
// generic fallback: first string-valued field
|
|
41
|
-
for (const v of Object.values(input)) {
|
|
42
|
-
if (typeof v === 'string' && v) return truncate(v, 80)
|
|
43
|
-
}
|
|
44
|
-
return ''
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
interface MainCardOpts {
|
|
48
|
-
sessionName: string
|
|
49
|
-
turn: number
|
|
50
|
-
model?: string
|
|
51
|
-
effort?: string
|
|
52
|
-
userText: string
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Initial card sent at the start of each turn. Streaming on. */
|
|
56
|
-
export function mainConversationCard(_opts: MainCardOpts): object {
|
|
57
|
-
return {
|
|
58
|
-
schema: '2.0',
|
|
59
|
-
config: {
|
|
60
|
-
streaming_mode: true,
|
|
61
|
-
summary: { content: '[Lodestar 正在生成…]' },
|
|
62
|
-
streaming_config: {
|
|
63
|
-
print_frequency_ms: { default: 60, android: 60, ios: 60, pc: 30 },
|
|
64
|
-
print_step: { default: 2, android: 2, ios: 2, pc: 4 },
|
|
65
|
-
print_strategy: 'fast',
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
body: {
|
|
69
|
-
// Initial body has just thinking + footer; assistant segments and tool
|
|
70
|
-
// panels are inserted between them in real time as Claude streams.
|
|
71
|
-
// Note: empty-string content is rejected by CardKit PUT so the
|
|
72
|
-
// thinking element starts with a single space placeholder; the first
|
|
73
|
-
// real append overwrites it.
|
|
74
|
-
elements: [
|
|
75
|
-
{ tag: 'markdown', element_id: ELEMENTS.thinking, content: ' ' },
|
|
76
|
-
{ tag: 'markdown', element_id: ELEMENTS.footer, content: '⏳ working…' },
|
|
77
|
-
],
|
|
78
|
-
},
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Empty assistant segment to be inserted just before the footer. */
|
|
83
|
-
export function assistantSegmentElement(i: number): object {
|
|
84
|
-
return { tag: 'markdown', element_id: ELEMENTS.assistant(i), content: ' ' }
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Final state for the thinking section once a turn closes — collapse the
|
|
88
|
-
* full thinking text into a panel so the card stays clean. Replaces the
|
|
89
|
-
* top-level `thinking` markdown element via PUT /elements/:id. */
|
|
90
|
-
export function thinkingCollapsedPanel(fullText: string): object {
|
|
91
|
-
const trimmed = fullText.trim()
|
|
92
|
-
return {
|
|
93
|
-
tag: 'collapsible_panel',
|
|
94
|
-
element_id: ELEMENTS.thinking,
|
|
95
|
-
header: { title: { tag: 'plain_text', content: `💭 思考过程 (${trimmed.length} 字)` } },
|
|
96
|
-
expanded: false,
|
|
97
|
-
elements: [
|
|
98
|
-
{ tag: 'markdown', content: trimmed.slice(0, 8000) || '_(空)_' },
|
|
99
|
-
],
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Element to insert for each tool call. expandable for big results.
|
|
104
|
-
*
|
|
105
|
-
* Header is a one-line summary: status + name + summarized input.
|
|
106
|
-
* Body holds the full input + (after completion) the full output. */
|
|
107
|
-
export function toolCallElement(
|
|
108
|
-
i: number,
|
|
109
|
-
name: string,
|
|
110
|
-
input: any,
|
|
111
|
-
output: string | null,
|
|
112
|
-
status: '⏳' | '✅' | '❌' = '⏳',
|
|
113
|
-
): object {
|
|
114
|
-
const summary = summarizeToolInput(name, input)
|
|
115
|
-
const headerText = summary
|
|
116
|
-
? `${status} 🔧 ${name}: ${summary}`
|
|
117
|
-
: `${status} 🔧 ${name}`
|
|
118
|
-
const inputBlock = '```\n' + JSON.stringify(input ?? {}, null, 2).slice(0, 2000) + '\n```'
|
|
119
|
-
const outputBlock = output != null
|
|
120
|
-
? '\n---\n**output:**\n```\n' + output.slice(0, 3000) + '\n```'
|
|
121
|
-
: ''
|
|
122
|
-
return {
|
|
123
|
-
tag: 'collapsible_panel',
|
|
124
|
-
element_id: ELEMENTS.tool(i),
|
|
125
|
-
header: { title: { tag: 'plain_text', content: headerText } },
|
|
126
|
-
expanded: false,
|
|
127
|
-
elements: [
|
|
128
|
-
{ tag: 'markdown', content: inputBlock + outputBlock },
|
|
129
|
-
],
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
interface PermissionOpts {
|
|
134
|
-
sessionName: string
|
|
135
|
-
toolName: string
|
|
136
|
-
description: string
|
|
137
|
-
inputPreview: string
|
|
138
|
-
requestId: string
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function permissionCard(opts: PermissionOpts): object {
|
|
142
|
-
const { sessionName, toolName, description, inputPreview, requestId } = opts
|
|
143
|
-
let pretty = inputPreview
|
|
144
|
-
try { pretty = JSON.stringify(JSON.parse(inputPreview), null, 2) } catch {}
|
|
145
|
-
return {
|
|
146
|
-
schema: '2.0',
|
|
147
|
-
config: { update_multi: true },
|
|
148
|
-
header: {
|
|
149
|
-
title: { tag: 'plain_text', content: `🔐 权限请求 · ${toolName}` },
|
|
150
|
-
subtitle: { tag: 'plain_text', content: sessionName },
|
|
151
|
-
template: 'orange',
|
|
152
|
-
},
|
|
153
|
-
body: {
|
|
154
|
-
elements: [
|
|
155
|
-
{ tag: 'markdown', content: description },
|
|
156
|
-
{ tag: 'markdown', content: '```\n' + pretty.slice(0, 2000) + '\n```' },
|
|
157
|
-
{
|
|
158
|
-
tag: 'column_set',
|
|
159
|
-
columns: [
|
|
160
|
-
permissionButtonColumn('✅ 允许', 'primary', requestId, 'allow'),
|
|
161
|
-
permissionButtonColumn('♾️ 始终允许', 'default', requestId, 'allow_always'),
|
|
162
|
-
permissionButtonColumn('❌ 拒绝', 'danger', requestId, 'deny'),
|
|
163
|
-
],
|
|
164
|
-
},
|
|
165
|
-
],
|
|
166
|
-
},
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function permissionButtonColumn(label: string, type: string, requestId: string, decision: string): object {
|
|
171
|
-
return {
|
|
172
|
-
tag: 'column', width: 'weighted', weight: 1,
|
|
173
|
-
elements: [{
|
|
174
|
-
tag: 'button',
|
|
175
|
-
text: { tag: 'plain_text', content: label },
|
|
176
|
-
type,
|
|
177
|
-
behaviors: [{ type: 'callback', value: { kind: 'permission', request_id: requestId, decision } }],
|
|
178
|
-
}],
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export function permissionResolvedCard(
|
|
183
|
-
toolName: string,
|
|
184
|
-
decision: 'allow' | 'allow_always' | 'deny',
|
|
185
|
-
user: string,
|
|
186
|
-
): object {
|
|
187
|
-
const ok = decision !== 'deny'
|
|
188
|
-
const label = decision === 'allow_always' ? '始终允许' : decision === 'allow' ? '已允许' : '已拒绝'
|
|
189
|
-
return {
|
|
190
|
-
schema: '2.0',
|
|
191
|
-
config: { update_multi: true },
|
|
192
|
-
header: {
|
|
193
|
-
title: { tag: 'plain_text', content: `🔐 权限请求 · ${toolName}` },
|
|
194
|
-
template: ok ? 'green' : 'red',
|
|
195
|
-
},
|
|
196
|
-
body: {
|
|
197
|
-
elements: [{
|
|
198
|
-
tag: 'markdown',
|
|
199
|
-
content: `${ok ? '✅' : '❌'} **${label}** by ${user || '匿名'} · ${new Date().toLocaleTimeString('zh-CN', { hour12: false })}`,
|
|
200
|
-
}],
|
|
201
|
-
},
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
interface ConsoleOpts {
|
|
206
|
-
sessionName: string
|
|
207
|
-
status: 'idle' | 'working' | 'awaiting_permission' | 'starting' | 'stopped'
|
|
208
|
-
model?: string
|
|
209
|
-
effort?: string
|
|
210
|
-
uptime?: string
|
|
211
|
-
lastActivity?: string
|
|
212
|
-
hasSession: boolean
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export function consoleCard(opts: ConsoleOpts): object {
|
|
216
|
-
const { sessionName, status, model, effort, uptime, lastActivity, hasSession } = opts
|
|
217
|
-
const statusEmoji = {
|
|
218
|
-
idle: '🟢 闲', working: '⚙️ 工作中', awaiting_permission: '🔐 等审批',
|
|
219
|
-
starting: '🚀 启动中', stopped: '⚪ 未运行',
|
|
220
|
-
}[status]
|
|
221
|
-
const meta = [
|
|
222
|
-
`状态: ${statusEmoji}`,
|
|
223
|
-
model ? `模型: ${model}${effort ? `/${effort}` : ''}` : null,
|
|
224
|
-
uptime ? `运行: ${uptime}` : null,
|
|
225
|
-
lastActivity ? `最近: ${lastActivity}` : null,
|
|
226
|
-
].filter(Boolean).join(' · ')
|
|
227
|
-
|
|
228
|
-
const buttons: [string, string, string][] = hasSession
|
|
229
|
-
? [
|
|
230
|
-
['⏸ 中断', 'interrupt', 'default'],
|
|
231
|
-
['🧹 /clear', 'clear', 'default'],
|
|
232
|
-
['⏹ 终止', 'stop', 'danger'],
|
|
233
|
-
['📁 ls', 'ls', 'default'],
|
|
234
|
-
]
|
|
235
|
-
: [
|
|
236
|
-
['🚀 启动', 'start', 'primary'],
|
|
237
|
-
['🔁 续聊', 'resume', 'default'],
|
|
238
|
-
['📁 ls', 'ls', 'default'],
|
|
239
|
-
]
|
|
240
|
-
|
|
241
|
-
const template = status === 'working' ? 'blue'
|
|
242
|
-
: status === 'awaiting_permission' ? 'orange'
|
|
243
|
-
: status === 'stopped' ? 'grey'
|
|
244
|
-
: 'green'
|
|
245
|
-
|
|
246
|
-
return {
|
|
247
|
-
schema: '2.0',
|
|
248
|
-
config: { update_multi: true },
|
|
249
|
-
header: {
|
|
250
|
-
title: { tag: 'plain_text', content: `🌟 Lodestar · ${sessionName}` },
|
|
251
|
-
template,
|
|
252
|
-
},
|
|
253
|
-
body: {
|
|
254
|
-
elements: [
|
|
255
|
-
{ tag: 'markdown', content: meta || '_(no state)_' },
|
|
256
|
-
{
|
|
257
|
-
tag: 'column_set',
|
|
258
|
-
columns: buttons.map(([label, action, kind]) => ({
|
|
259
|
-
tag: 'column', width: 'weighted', weight: 1,
|
|
260
|
-
elements: [{
|
|
261
|
-
tag: 'button',
|
|
262
|
-
text: { tag: 'plain_text', content: label },
|
|
263
|
-
type: kind,
|
|
264
|
-
behaviors: [{ type: 'callback', value: { kind: 'console', action } }],
|
|
265
|
-
}],
|
|
266
|
-
})),
|
|
267
|
-
},
|
|
268
|
-
],
|
|
269
|
-
},
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
interface MenuOpts {
|
|
274
|
-
question: string
|
|
275
|
-
options: string[]
|
|
276
|
-
requestId: string
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
export function menuCard(opts: MenuOpts): object {
|
|
280
|
-
const { question, options, requestId } = opts
|
|
281
|
-
return {
|
|
282
|
-
schema: '2.0',
|
|
283
|
-
config: { update_multi: true },
|
|
284
|
-
header: {
|
|
285
|
-
title: { tag: 'plain_text', content: '📋 等待选择' },
|
|
286
|
-
template: 'turquoise',
|
|
287
|
-
},
|
|
288
|
-
body: {
|
|
289
|
-
elements: [
|
|
290
|
-
{ tag: 'markdown', content: question || '_请选择一项:_' },
|
|
291
|
-
...options.map((opt, i) => ({
|
|
292
|
-
tag: 'button',
|
|
293
|
-
text: { tag: 'plain_text', content: opt },
|
|
294
|
-
type: 'default',
|
|
295
|
-
behaviors: [{ type: 'callback', value: { kind: 'menu', request_id: requestId, choice: i } }],
|
|
296
|
-
})),
|
|
297
|
-
],
|
|
298
|
-
},
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
export const STREAMING_OFF_SETTINGS = {
|
|
303
|
-
config: { streaming_mode: false, summary: { content: '✅ Lodestar 完成' } },
|
|
304
|
-
}
|