@miphamai/cli 0.46.0 → 0.47.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/package.json +2 -1
- package/src/daemon/feishu/adapter.ts +79 -0
- package/src/daemon/feishu/api.ts +23 -0
- package/src/daemon/feishu/env.ts +22 -0
- package/src/daemon/feishu/events.ts +53 -0
- package/src/daemon/feishu/types.ts +14 -0
- package/src/daemon/index.ts +19 -0
- package/src/daemon/server.ts +21 -0
- package/src/daemon/session-manager.ts +12 -0
- package/src/daemon/session-worker.ts +5 -0
- package/src/shared/package-info.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miphamai/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.0",
|
|
4
4
|
"description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"test": "vitest run"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
+
"@larksuiteoapi/node-sdk": "^1.73.0",
|
|
44
45
|
"commander": "^13.1.0",
|
|
45
46
|
"ink": "^5.2.1",
|
|
46
47
|
"ink-text-input": "^6.0.0",
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createFeishuApi } from './api.js'
|
|
2
|
+
import { createFeishuEventDispatcher } from './events.js'
|
|
3
|
+
import type { FeishuConfig, FeishuTextMessage } from './types.js'
|
|
4
|
+
import type { SessionManager } from '../session-manager'
|
|
5
|
+
import type { SessionWorker } from '../session-worker'
|
|
6
|
+
import type { RateLimiter } from '../rate-limiter'
|
|
7
|
+
|
|
8
|
+
export interface FeishuAdapterDeps {
|
|
9
|
+
sm: SessionManager
|
|
10
|
+
getOrCreateWorker: (sessionId: string) => SessionWorker | null
|
|
11
|
+
rateLimiter: RateLimiter
|
|
12
|
+
cwd: string
|
|
13
|
+
provider: string
|
|
14
|
+
model: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface FeishuAdapter {
|
|
18
|
+
handleEvent(request: Request): Promise<Response>
|
|
19
|
+
isAllowed(openId: string): boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createFeishuAdapter(config: FeishuConfig, deps: FeishuAdapterDeps): FeishuAdapter {
|
|
23
|
+
const api = createFeishuApi(config)
|
|
24
|
+
const allowed = new Set(config.allowedOpenIds)
|
|
25
|
+
|
|
26
|
+
const onMessage = async (msg: FeishuTextMessage) => {
|
|
27
|
+
try {
|
|
28
|
+
if (!allowed.has(msg.openId)) return
|
|
29
|
+
if (!deps.rateLimiter.check(`feishu:${msg.openId}`).allowed) return
|
|
30
|
+
|
|
31
|
+
const session = deps.sm.getOrCreateByFeishuOpenId(
|
|
32
|
+
msg.openId,
|
|
33
|
+
deps.cwd,
|
|
34
|
+
deps.provider,
|
|
35
|
+
deps.model,
|
|
36
|
+
)
|
|
37
|
+
const worker = deps.getOrCreateWorker(session.id)
|
|
38
|
+
if (!worker) {
|
|
39
|
+
await api.sendText(msg.openId, '(会话初始化失败,请稍后重试)')
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
await worker.processPrompt(msg.text)
|
|
43
|
+
const result = worker.getLastAssistantContent()
|
|
44
|
+
await api.sendText(msg.openId, result ? result.slice(0, 4000) : '(无回复)')
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error('[feishu] message handling failed:', err)
|
|
47
|
+
try {
|
|
48
|
+
await api.sendText(msg.openId, '(处理失败,请稍后重试)')
|
|
49
|
+
} catch {
|
|
50
|
+
/* 忽略回送失败,确保不 rethrow → Feishu 不重试 */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const dispatcher = createFeishuEventDispatcher(config, onMessage)
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
isAllowed: (openId) => allowed.has(openId),
|
|
59
|
+
async handleEvent(request: Request): Promise<Response> {
|
|
60
|
+
let body: unknown = {}
|
|
61
|
+
try {
|
|
62
|
+
body = await request.json()
|
|
63
|
+
} catch {
|
|
64
|
+
/* 非 JSON body 容忍 */
|
|
65
|
+
}
|
|
66
|
+
// URL 验证:回显 challenge(未加密)
|
|
67
|
+
if (body && typeof body === 'object' && 'challenge' in (body as object)) {
|
|
68
|
+
return Response.json({ challenge: (body as { challenge: string }).challenge })
|
|
69
|
+
}
|
|
70
|
+
const headers: Record<string, string> = {}
|
|
71
|
+
request.headers.forEach((v, k) => (headers[k] = v))
|
|
72
|
+
const result = await dispatcher.invoke(body, headers)
|
|
73
|
+
if (!result.ok) {
|
|
74
|
+
return Response.json({ code: 1, msg: result.reason }, { status: 400 })
|
|
75
|
+
}
|
|
76
|
+
return Response.json({ code: 0 })
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as lark from '@larksuiteoapi/node-sdk'
|
|
2
|
+
import type { FeishuConfig } from './types.js'
|
|
3
|
+
|
|
4
|
+
export interface FeishuApi {
|
|
5
|
+
sendText(openId: string, text: string): Promise<void>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createFeishuApi(config: FeishuConfig): FeishuApi {
|
|
9
|
+
const client = new lark.Client({
|
|
10
|
+
appId: config.appId,
|
|
11
|
+
appSecret: config.appSecret,
|
|
12
|
+
appType: lark.AppType.SelfBuild,
|
|
13
|
+
domain: lark.Domain.Feishu,
|
|
14
|
+
})
|
|
15
|
+
return {
|
|
16
|
+
async sendText(openId: string, text: string): Promise<void> {
|
|
17
|
+
await client.im.message.create({
|
|
18
|
+
params: { receive_id_type: 'open_id' },
|
|
19
|
+
data: { receive_id: openId, msg_type: 'text', content: JSON.stringify({ text }) },
|
|
20
|
+
})
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { FeishuConfig } from './types.js'
|
|
2
|
+
|
|
3
|
+
export function parseFeishuEnv(): FeishuConfig | null {
|
|
4
|
+
const appId = process.env.FEISHU_APP_ID
|
|
5
|
+
const appSecret = process.env.FEISHU_APP_SECRET
|
|
6
|
+
const encryptKey = process.env.FEISHU_ENCRYPT_KEY || ''
|
|
7
|
+
const verificationToken = process.env.FEISHU_VERIFICATION_TOKEN || ''
|
|
8
|
+
// Fail-closed:encryptKey 必填。lark SDK 对 im.message.receive_v1 事件流的签名验证
|
|
9
|
+
// 只依赖 encryptKey(checkIsEventValidated 在空 key 时静默返回 true);
|
|
10
|
+
// verificationToken 仅用于 URL challenge 握手,不保护事件流,故保持可选。
|
|
11
|
+
if (!appId || !appSecret || !encryptKey) return null
|
|
12
|
+
return {
|
|
13
|
+
appId,
|
|
14
|
+
appSecret,
|
|
15
|
+
encryptKey,
|
|
16
|
+
verificationToken,
|
|
17
|
+
allowedOpenIds: (process.env.FEISHU_ALLOWED_OPEN_IDS || '')
|
|
18
|
+
.split(',')
|
|
19
|
+
.map((s) => s.trim())
|
|
20
|
+
.filter(Boolean),
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as lark from '@larksuiteoapi/node-sdk'
|
|
2
|
+
import type { FeishuConfig, FeishuTextMessage } from './types.js'
|
|
3
|
+
|
|
4
|
+
export type OnFeishuMessage = (msg: FeishuTextMessage) => Promise<void>
|
|
5
|
+
|
|
6
|
+
export type FeishuInvokeResult =
|
|
7
|
+
{ ok: true } | { ok: false; reason: 'invalid_signature' | 'no_handler' | 'parse_failed' }
|
|
8
|
+
|
|
9
|
+
export interface FeishuEventDispatcher {
|
|
10
|
+
invoke(body: unknown, headers: Record<string, string>): Promise<FeishuInvokeResult>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createFeishuEventDispatcher(
|
|
14
|
+
config: FeishuConfig,
|
|
15
|
+
onMessage: OnFeishuMessage,
|
|
16
|
+
): FeishuEventDispatcher {
|
|
17
|
+
const dispatcher = new lark.EventDispatcher({
|
|
18
|
+
encryptKey: config.encryptKey,
|
|
19
|
+
verificationToken: config.verificationToken,
|
|
20
|
+
}).register({
|
|
21
|
+
'im.message.receive_v1': async (data: any): Promise<true> => {
|
|
22
|
+
const message = data?.message
|
|
23
|
+
if (message?.message_type !== 'text') return true
|
|
24
|
+
let text = ''
|
|
25
|
+
try {
|
|
26
|
+
text = JSON.parse(message.content).text ?? ''
|
|
27
|
+
} catch {
|
|
28
|
+
return true
|
|
29
|
+
}
|
|
30
|
+
const openId = data?.sender?.sender_id?.open_id
|
|
31
|
+
if (!openId || !text) return true
|
|
32
|
+
await onMessage({ chatId: message.chat_id, messageId: message.message_id, openId, text })
|
|
33
|
+
return true
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
async invoke(body, headers): Promise<FeishuInvokeResult> {
|
|
39
|
+
const assigned = Object.assign(Object.create({ headers }), body)
|
|
40
|
+
let raw: unknown
|
|
41
|
+
try {
|
|
42
|
+
raw = await dispatcher.invoke(assigned)
|
|
43
|
+
} catch {
|
|
44
|
+
return { ok: false, reason: 'parse_failed' }
|
|
45
|
+
}
|
|
46
|
+
if (raw === undefined) return { ok: false, reason: 'invalid_signature' }
|
|
47
|
+
if (typeof raw === 'string' && raw.startsWith('no ')) {
|
|
48
|
+
return { ok: false, reason: 'no_handler' }
|
|
49
|
+
}
|
|
50
|
+
return { ok: true }
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface FeishuConfig {
|
|
2
|
+
appId: string
|
|
3
|
+
appSecret: string
|
|
4
|
+
encryptKey: string
|
|
5
|
+
verificationToken: string
|
|
6
|
+
allowedOpenIds: string[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface FeishuTextMessage {
|
|
10
|
+
chatId: string
|
|
11
|
+
messageId: string
|
|
12
|
+
openId: string
|
|
13
|
+
text: string
|
|
14
|
+
}
|
package/src/daemon/index.ts
CHANGED
|
@@ -23,6 +23,9 @@ import { RateLimiter } from './rate-limiter'
|
|
|
23
23
|
import { PACKAGE_VERSION } from '../shared/package-info'
|
|
24
24
|
import type { DaemonStatus } from './types'
|
|
25
25
|
import { logger } from './logger'
|
|
26
|
+
import { parseFeishuEnv } from './feishu/env.js'
|
|
27
|
+
import type { FeishuConfig } from './feishu/types.js'
|
|
28
|
+
import { loadConfig } from '../config/loader'
|
|
26
29
|
|
|
27
30
|
const HOME = homedir()
|
|
28
31
|
const MIPHAM_HOME = join(HOME, '.mipham')
|
|
@@ -163,6 +166,21 @@ export async function startDaemon(): Promise<{ port: number; token: string }> {
|
|
|
163
166
|
const rateLimiter = new RateLimiter(100, 60_000)
|
|
164
167
|
activeRateLimiter = rateLimiter
|
|
165
168
|
|
|
169
|
+
// Feishu remote-control adapter(env 未配置时跳过;配置则解析 session 默认 provider/model)
|
|
170
|
+
const feishuConfig = parseFeishuEnv()
|
|
171
|
+
let feishu: { config: FeishuConfig; cwd: string; provider: string; model: string } | undefined
|
|
172
|
+
if (feishuConfig) {
|
|
173
|
+
const cfg = loadConfig()
|
|
174
|
+
// 取第一个非 upcoming(即非「待上线」)的 provider 及其首个 model
|
|
175
|
+
const provider = cfg.providers.find((p) => p.status !== 'upcoming') ?? cfg.providers[0]
|
|
176
|
+
feishu = {
|
|
177
|
+
config: feishuConfig,
|
|
178
|
+
cwd: process.env.FEISHU_CWD || process.cwd(),
|
|
179
|
+
provider: provider?.id ?? 'anthropic',
|
|
180
|
+
model: provider?.models?.[0]?.id ?? 'claude-sonnet-5',
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
166
184
|
// Start HTTP server (Bun.serve starts listening immediately)
|
|
167
185
|
const server = createServer({
|
|
168
186
|
db,
|
|
@@ -177,6 +195,7 @@ export async function startDaemon(): Promise<{ port: number; token: string }> {
|
|
|
177
195
|
goalManager,
|
|
178
196
|
scheduleManager,
|
|
179
197
|
rateLimiter,
|
|
198
|
+
feishu,
|
|
180
199
|
})
|
|
181
200
|
activeServer = server
|
|
182
201
|
|
package/src/daemon/server.ts
CHANGED
|
@@ -23,6 +23,8 @@ import { createToolRegistry } from '../tools'
|
|
|
23
23
|
import { PermissionSystem } from '../core/permission'
|
|
24
24
|
import type { ProviderRegistry } from '../providers/registry'
|
|
25
25
|
import type { ToolDefinition, PermissionMode } from '../shared/types'
|
|
26
|
+
import { createFeishuAdapter } from './feishu/adapter.js'
|
|
27
|
+
import type { FeishuConfig } from './feishu/types.js'
|
|
26
28
|
|
|
27
29
|
interface ServerConfig {
|
|
28
30
|
db: DaemonDatabase
|
|
@@ -37,6 +39,7 @@ interface ServerConfig {
|
|
|
37
39
|
goalManager: GoalManager
|
|
38
40
|
scheduleManager: ScheduleManager
|
|
39
41
|
rateLimiter: RateLimiter
|
|
42
|
+
feishu?: { config: FeishuConfig; cwd: string; provider: string; model: string }
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
interface WsData {
|
|
@@ -78,6 +81,7 @@ export function createServer(config: ServerConfig): Server<WsData> {
|
|
|
78
81
|
goalManager,
|
|
79
82
|
scheduleManager,
|
|
80
83
|
rateLimiter,
|
|
84
|
+
feishu,
|
|
81
85
|
} = config
|
|
82
86
|
|
|
83
87
|
const wsClients = new Map<string, Set<ServerWebSocket<WsData>>>()
|
|
@@ -198,6 +202,18 @@ export function createServer(config: ServerConfig): Server<WsData> {
|
|
|
198
202
|
}
|
|
199
203
|
}
|
|
200
204
|
|
|
205
|
+
// ── Feishu remote-control adapter(独立签名验证,复用同一 worker 池)──
|
|
206
|
+
const feishuAdapter = feishu
|
|
207
|
+
? createFeishuAdapter(feishu.config, {
|
|
208
|
+
sm,
|
|
209
|
+
getOrCreateWorker,
|
|
210
|
+
rateLimiter,
|
|
211
|
+
cwd: feishu.cwd,
|
|
212
|
+
provider: feishu.provider,
|
|
213
|
+
model: feishu.model,
|
|
214
|
+
})
|
|
215
|
+
: undefined
|
|
216
|
+
|
|
201
217
|
const server = Bun.serve<WsData>({
|
|
202
218
|
port,
|
|
203
219
|
hostname,
|
|
@@ -211,6 +227,11 @@ export function createServer(config: ServerConfig): Server<WsData> {
|
|
|
211
227
|
const method = req.method
|
|
212
228
|
logger.info('request', { method, path })
|
|
213
229
|
|
|
230
|
+
// ── Feishu event callback(独立签名验证,不经过 daemon Bearer 鉴权)──
|
|
231
|
+
if (feishuAdapter && method === 'POST' && path === '/feishu/event') {
|
|
232
|
+
return await feishuAdapter.handleEvent(req)
|
|
233
|
+
}
|
|
234
|
+
|
|
214
235
|
// ── Rate limiting (skip health endpoint) ──────────
|
|
215
236
|
if (path !== '/api/v1/health') {
|
|
216
237
|
const ip = server.requestIP(req)?.address || 'unknown'
|
|
@@ -23,6 +23,18 @@ export class SessionManager {
|
|
|
23
23
|
return this.db.listSessions(status)
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
getOrCreateByFeishuOpenId(
|
|
27
|
+
openId: string,
|
|
28
|
+
cwd: string,
|
|
29
|
+
provider: string,
|
|
30
|
+
model: string,
|
|
31
|
+
): DaemonSession {
|
|
32
|
+
const name = `feishu-${openId}`
|
|
33
|
+
const existing = this.db.listSessions().find((s) => s.name === name && s.status !== 'closed')
|
|
34
|
+
if (existing) return existing
|
|
35
|
+
return this.db.createSession({ name, cwd, provider, model })
|
|
36
|
+
}
|
|
37
|
+
|
|
26
38
|
closeSession(id: string): void {
|
|
27
39
|
this.db.closeSession(id)
|
|
28
40
|
for (const cb of this.closeCallbacks) {
|
|
@@ -253,6 +253,11 @@ export class SessionWorker {
|
|
|
253
253
|
return this.session.id
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
/** Get the last assistant text content from the underlying engine. */
|
|
257
|
+
getLastAssistantContent(): string | undefined {
|
|
258
|
+
return this.engine.getLastAssistantContent()
|
|
259
|
+
}
|
|
260
|
+
|
|
256
261
|
// ── Persistence ────────────────────────────────────────────────────────
|
|
257
262
|
|
|
258
263
|
/**
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
export const PACKAGE_NAME = '@miphamai/cli' as const
|
|
10
10
|
|
|
11
11
|
/** 当前发布版本 */
|
|
12
|
-
export const PACKAGE_VERSION = '0.
|
|
12
|
+
export const PACKAGE_VERSION = '0.47.0' as const
|
|
13
13
|
|
|
14
14
|
/** npm install 全局安装命令 */
|
|
15
15
|
export const NPM_INSTALL_COMMAND = `npm install -g ${PACKAGE_NAME}` as const
|