@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miphamai/cli",
3
- "version": "0.46.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
+ }
@@ -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
 
@@ -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.46.0' as const
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