@muyichengshayu/promptx 0.2.8 → 0.2.9

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.9
4
+
5
+ - 接入 `Kimi Code CLI` 执行引擎:工作台 Agent 选择器新增 Kimi Code 选项,支持本地会话发现、线程复用、TodoList 过程展示与停止控制,与现有 Codex / Claude Code / OpenCode 并列为第四类 Agent。
6
+ - 工作台 Agent 选择器与过滤状态持久化:切换任务后会自动恢复上次选中的 Agent 引擎和中栏过滤条件,不再每次回到默认值。
7
+ - runner 状态提示支持多语言:idle 进度提示(排队中、运行中、停止中)通过 `messageKey` 机制走前端 i18n 翻译,英文环境下显示对应英文文案。
8
+ - 修复图片上传地址在云主机部署下失效的问题:前端 `API_BASE` 改为运行时按 `window.location.origin` 计算,数据库中只存相对路径;同时兼容已存的老数据(`localhost` / `127.0.0.1` 绝对路径在显示时自动替换为当前 host),runner 收到的 prompt 仍会正确转换为本地地址。
9
+
3
10
  ## 0.2.8
4
11
 
5
12
  - 修复 `Claude Code` 的 `TodoWrite` 过程映射:待办列表会被归一为结构化 `todo_list` 事件,正确展示待办内容、进行中与已完成状态,不再只暴露原始工具输入。
@@ -7,12 +7,14 @@ import {
7
7
  import { codexRunner } from './codexRunner.js'
8
8
  import { claudeCodeRunner } from './claudeCodeRunner.js'
9
9
  import { openCodeRunner } from './openCodeRunner.js'
10
+ import { kimiCodeRunner } from './kimiCodeRunner.js'
10
11
  import { shellRunner } from './shellRunner.js'
11
12
 
12
13
  const runnerRegistry = new Map([
13
14
  [codexRunner.engine, codexRunner],
14
15
  [claudeCodeRunner.engine, claudeCodeRunner],
15
16
  [openCodeRunner.engine, openCodeRunner],
17
+ [kimiCodeRunner.engine, kimiCodeRunner],
16
18
  [shellRunner.engine, shellRunner],
17
19
  ])
18
20
 
@@ -0,0 +1,561 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { execFileSync, spawn } from 'node:child_process'
4
+ import {
5
+ AGENT_ENGINES,
6
+ AGENT_RUN_ITEM_TYPES,
7
+ createAgentEventEnvelopeEvent,
8
+ createCompletedEnvelopeEvent,
9
+ createItemCompletedEvent,
10
+ createItemStartedEvent,
11
+ createStatusEnvelopeEvent,
12
+ createStderrEnvelopeEvent,
13
+ createStdoutEnvelopeEvent,
14
+ createThreadStartedEvent,
15
+ createTurnCompletedEvent,
16
+ getAgentEngineLabel,
17
+ } from '../../../../packages/shared/src/index.js'
18
+ import { createManagedSpawnOptions, forceStopChildProcess } from '../processControl.js'
19
+
20
+ const KIMI_CODE_BIN = process.env.KIMI_CODE_BIN || 'kimi'
21
+ const RESOLVED_KIMI_CODE_BIN = resolveKimiCodeBinary()
22
+
23
+ function resolveKimiCodeBinary() {
24
+ if (process.platform !== 'win32') {
25
+ return KIMI_CODE_BIN
26
+ }
27
+
28
+ if (path.extname(KIMI_CODE_BIN)) {
29
+ return KIMI_CODE_BIN
30
+ }
31
+
32
+ if (fs.existsSync(`${KIMI_CODE_BIN}.cmd`)) {
33
+ return `${KIMI_CODE_BIN}.cmd`
34
+ }
35
+
36
+ if (fs.existsSync(`${KIMI_CODE_BIN}.bat`)) {
37
+ return `${KIMI_CODE_BIN}.bat`
38
+ }
39
+
40
+ if (fs.existsSync(KIMI_CODE_BIN)) {
41
+ return KIMI_CODE_BIN
42
+ }
43
+
44
+ try {
45
+ const output = execFileSync('where.exe', [KIMI_CODE_BIN], {
46
+ encoding: 'utf8',
47
+ stdio: ['ignore', 'pipe', 'ignore'],
48
+ windowsHide: true,
49
+ }).trim()
50
+
51
+ if (!output) {
52
+ return KIMI_CODE_BIN
53
+ }
54
+
55
+ const candidates = output
56
+ .split(/\r?\n/g)
57
+ .map((line) => line.trim())
58
+ .filter(Boolean)
59
+
60
+ return candidates.find((item) => /\.(cmd|bat)$/i.test(item))
61
+ || candidates.find((item) => /\.(exe|com)$/i.test(item))
62
+ || candidates[0]
63
+ || KIMI_CODE_BIN
64
+ } catch {
65
+ return KIMI_CODE_BIN
66
+ }
67
+ }
68
+
69
+ function createKimiSpawn(commandArgs = [], cwd = '') {
70
+ const options = createManagedSpawnOptions({
71
+ cwd,
72
+ stdio: ['ignore', 'pipe', 'pipe'],
73
+ })
74
+
75
+ if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(RESOLVED_KIMI_CODE_BIN)) {
76
+ return spawn(
77
+ process.env.ComSpec || 'cmd.exe',
78
+ ['/d', '/s', '/c', RESOLVED_KIMI_CODE_BIN, ...commandArgs],
79
+ options
80
+ )
81
+ }
82
+
83
+ return spawn(RESOLVED_KIMI_CODE_BIN, commandArgs, options)
84
+ }
85
+
86
+ function normalizeSpawnError(error) {
87
+ if (error?.code === 'ENOENT') {
88
+ const attempted = RESOLVED_KIMI_CODE_BIN === KIMI_CODE_BIN
89
+ ? KIMI_CODE_BIN
90
+ : `${KIMI_CODE_BIN} -> ${RESOLVED_KIMI_CODE_BIN}`
91
+ return new Error(
92
+ `找不到 Kimi Code CLI(尝试执行:${attempted})。请先确认终端里可以运行 \`kimi --version\`,或设置环境变量 \`KIMI_CODE_BIN\`。`
93
+ )
94
+ }
95
+
96
+ return error
97
+ }
98
+
99
+ function parseJsonLine(line = '') {
100
+ const text = String(line || '').trim()
101
+ if (!text) {
102
+ return null
103
+ }
104
+
105
+ try {
106
+ return JSON.parse(text)
107
+ } catch {
108
+ return null
109
+ }
110
+ }
111
+
112
+ function splitBufferedLines(buffer = '') {
113
+ const text = String(buffer || '')
114
+ if (!text) {
115
+ return { lines: [], rest: '' }
116
+ }
117
+
118
+ const normalized = text.replace(/\r\n/g, '\n')
119
+ const parts = normalized.split('\n')
120
+ const rest = parts.pop() || ''
121
+
122
+ return {
123
+ lines: parts.map((line) => line.trim()).filter(Boolean),
124
+ rest,
125
+ }
126
+ }
127
+
128
+ function flushBufferedText(buffer = '') {
129
+ const { lines, rest } = splitBufferedLines(buffer)
130
+ const tail = String(rest || '').trim()
131
+ return tail ? [...lines, tail] : lines
132
+ }
133
+
134
+ function stringifyKimiToolResultContent(value) {
135
+ if (typeof value === 'string') {
136
+ return value.trim()
137
+ }
138
+
139
+ if (value == null) {
140
+ return ''
141
+ }
142
+
143
+ if (Array.isArray(value)) {
144
+ const parts = []
145
+ for (const item of value) {
146
+ if (item && typeof item === 'object') {
147
+ const text = String(item.text || '').trim()
148
+ if (text) parts.push(text)
149
+ } else if (typeof item === 'string') {
150
+ const text = item.trim()
151
+ if (text) parts.push(text)
152
+ }
153
+ }
154
+ return parts.join('\n').trim()
155
+ }
156
+
157
+ try {
158
+ const compact = JSON.stringify(value)
159
+ return compact.length <= 12000 ? compact : `${compact.slice(0, 11997)}...`
160
+ } catch {
161
+ return String(value || '').trim()
162
+ }
163
+ }
164
+
165
+ function buildKimiToolCommand(name = '', input = {}) {
166
+ const toolName = String(name || 'Kimi tool').trim() || 'Kimi tool'
167
+ if (!input || typeof input !== 'object') {
168
+ return toolName
169
+ }
170
+
171
+ const command = String(input.command || '').trim()
172
+ if (command) {
173
+ return `${toolName}: ${command}`
174
+ }
175
+
176
+ const singleValueKeys = ['file_path', 'path', 'pattern', 'query', 'url', 'description']
177
+ for (const key of singleValueKeys) {
178
+ const value = String(input[key] || '').trim()
179
+ if (value) {
180
+ return `${toolName}: ${value}`
181
+ }
182
+ }
183
+
184
+ try {
185
+ const compact = JSON.stringify(input)
186
+ return compact.length <= 240 ? `${toolName}: ${compact}` : `${toolName}: ${compact.slice(0, 237)}...`
187
+ } catch {
188
+ return toolName
189
+ }
190
+ }
191
+
192
+ function isKimiTodoToolName(name = '') {
193
+ return String(name || '').trim().toLowerCase() === 'settodolist'
194
+ }
195
+
196
+ function normalizeKimiTodoStatus(status = '') {
197
+ const normalized = String(status || '').trim().toLowerCase()
198
+ if (normalized === 'done') {
199
+ return 'completed'
200
+ }
201
+ if (normalized === 'in_progress') {
202
+ return 'in_progress'
203
+ }
204
+ return 'pending'
205
+ }
206
+
207
+ function normalizeKimiTodoItems(items = []) {
208
+ return (Array.isArray(items) ? items : [])
209
+ .map((entry) => {
210
+ if (!entry || typeof entry !== 'object') {
211
+ return null
212
+ }
213
+
214
+ const status = normalizeKimiTodoStatus(entry.status)
215
+ const text = String(entry.title || entry.text || '').trim()
216
+ if (!text) {
217
+ return null
218
+ }
219
+
220
+ return {
221
+ text,
222
+ status,
223
+ completed: status === 'completed',
224
+ }
225
+ })
226
+ .filter(Boolean)
227
+ }
228
+
229
+ function extractKimiSessionIdFromStderrLine(line = '') {
230
+ const match = String(line || '').match(/To resume this session:\s*kimi\s+(?:-r|--session|--resume)\s+([a-f0-9-]+)/i)
231
+ return match?.[1] ? String(match[1]).trim() : ''
232
+ }
233
+
234
+ export function isKimiInfoStderrLine(line = '') {
235
+ const text = String(line || '').trim()
236
+ if (!text) {
237
+ return true
238
+ }
239
+ if (/^To resume this session:/i.test(text)) {
240
+ return true
241
+ }
242
+ if (/^Shell cwd was reset to /i.test(text)) {
243
+ return true
244
+ }
245
+ return false
246
+ }
247
+
248
+ export function createKimiNormalizationState() {
249
+ return {
250
+ turnStarted: false,
251
+ toolUses: new Map(),
252
+ }
253
+ }
254
+
255
+ export function normalizeKimiEvents(event = {}, state = createKimiNormalizationState()) {
256
+ const role = String(event?.role || '').trim().toLowerCase()
257
+ const normalizedEvents = []
258
+
259
+ if (role === 'assistant') {
260
+ if (!state.turnStarted) {
261
+ state.turnStarted = true
262
+ normalizedEvents.push({ type: 'turn.started' })
263
+ }
264
+
265
+ const content = Array.isArray(event.content) ? event.content : []
266
+ content.forEach((block) => {
267
+ const blockType = String(block?.type || '').trim().toLowerCase()
268
+ if (blockType === 'think') {
269
+ const text = String(block?.think || '').trim()
270
+ if (text) {
271
+ normalizedEvents.push({
272
+ ...createItemStartedEvent({
273
+ type: AGENT_RUN_ITEM_TYPES.REASONING,
274
+ text,
275
+ }),
276
+ })
277
+ }
278
+ return
279
+ }
280
+
281
+ if (blockType === 'text') {
282
+ const text = String(block?.text || '').trim()
283
+ if (text) {
284
+ normalizedEvents.push({
285
+ ...createItemCompletedEvent({
286
+ type: AGENT_RUN_ITEM_TYPES.AGENT_MESSAGE,
287
+ text,
288
+ }),
289
+ })
290
+ }
291
+ return
292
+ }
293
+ })
294
+
295
+ const toolCalls = Array.isArray(event.tool_calls) ? event.tool_calls : []
296
+ toolCalls.forEach((toolCall) => {
297
+ const toolUseId = String(toolCall?.id || '').trim()
298
+ const name = String(toolCall?.function?.name || toolCall?.name || 'Kimi tool').trim() || 'Kimi tool'
299
+ const argsText = toolCall?.function?.arguments || toolCall?.arguments || '{}'
300
+ let parsedArgs = {}
301
+ try {
302
+ parsedArgs = JSON.parse(argsText)
303
+ } catch {
304
+ parsedArgs = {}
305
+ }
306
+ const command = buildKimiToolCommand(name, parsedArgs)
307
+ const isTodoTool = isKimiTodoToolName(name)
308
+ const todoItems = normalizeKimiTodoItems(parsedArgs.todos)
309
+
310
+ if (toolUseId) {
311
+ state.toolUses.set(toolUseId, {
312
+ name,
313
+ command,
314
+ kind: isTodoTool ? 'todo' : 'command',
315
+ todoItems,
316
+ })
317
+ }
318
+
319
+ if (isTodoTool) {
320
+ normalizedEvents.push({
321
+ ...createItemStartedEvent({
322
+ type: AGENT_RUN_ITEM_TYPES.TODO_LIST,
323
+ items: todoItems,
324
+ }),
325
+ })
326
+ return
327
+ }
328
+
329
+ normalizedEvents.push({
330
+ ...createItemStartedEvent({
331
+ type: AGENT_RUN_ITEM_TYPES.COMMAND_EXECUTION,
332
+ command,
333
+ status: 'in_progress',
334
+ }),
335
+ })
336
+ })
337
+
338
+ return normalizedEvents
339
+ }
340
+
341
+ if (role === 'tool') {
342
+ const toolCallId = String(event?.tool_call_id || '').trim()
343
+ const remembered = toolCallId ? state.toolUses.get(toolCallId) : null
344
+ const output = stringifyKimiToolResultContent(event?.content)
345
+ const todoTool = remembered?.kind === 'todo'
346
+
347
+ if (toolCallId) {
348
+ state.toolUses.delete(toolCallId)
349
+ }
350
+
351
+ if (todoTool) {
352
+ normalizedEvents.push({
353
+ ...createItemCompletedEvent({
354
+ type: AGENT_RUN_ITEM_TYPES.TODO_LIST,
355
+ items: Array.isArray(remembered?.todoItems) ? remembered.todoItems : [],
356
+ }),
357
+ })
358
+ return normalizedEvents
359
+ }
360
+
361
+ normalizedEvents.push({
362
+ ...createItemCompletedEvent({
363
+ type: AGENT_RUN_ITEM_TYPES.COMMAND_EXECUTION,
364
+ command: remembered?.command || remembered?.name || 'Kimi tool',
365
+ status: 'completed',
366
+ exit_code: 0,
367
+ aggregated_output: output,
368
+ }),
369
+ })
370
+
371
+ return normalizedEvents
372
+ }
373
+
374
+ return [{
375
+ type: `kimi.${role || 'event'}`,
376
+ detail: stringifyKimiToolResultContent(event?.content),
377
+ }]
378
+ }
379
+
380
+ export function normalizeKimiEvent(event = {}, state = createKimiNormalizationState()) {
381
+ return normalizeKimiEvents(event, state)[0] || null
382
+ }
383
+
384
+ function createExecArgs(session, prompt) {
385
+ const args = [
386
+ '--print',
387
+ '--output-format', 'stream-json',
388
+ ]
389
+
390
+ const sessionId = String(session?.engineSessionId || session?.engineThreadId || session?.codexThreadId || '').trim()
391
+ if (sessionId) {
392
+ args.push('--session', sessionId)
393
+ }
394
+
395
+ if (session?.cwd) {
396
+ args.push('--work-dir', session.cwd)
397
+ }
398
+
399
+ args.push('-p', String(prompt || ''))
400
+
401
+ return args
402
+ }
403
+
404
+ function createKimiRunStatusEvent(session = {}) {
405
+ const hasExistingThread = Boolean(
406
+ String(session?.engineSessionId || session?.engineThreadId || session?.codexThreadId || '').trim()
407
+ )
408
+
409
+ return createStatusEnvelopeEvent({
410
+ stage: hasExistingThread ? 'resuming' : 'starting',
411
+ message: hasExistingThread
412
+ ? '已连接 PromptX 项目,正在继续这轮执行。'
413
+ : '已创建 PromptX 项目,正在启动第一轮执行。',
414
+ })
415
+ }
416
+
417
+ export function streamPromptToKimiCodeSession(sessionInput, prompt, callbacks = {}) {
418
+ const session = sessionInput && typeof sessionInput === 'object' ? sessionInput : null
419
+ const normalizedPrompt = String(prompt || '').trim()
420
+
421
+ if (!session?.id || !session?.cwd) {
422
+ throw new Error('缺少 PromptX 项目。')
423
+ }
424
+ if (!normalizedPrompt) {
425
+ throw new Error('没有可发送的提示词。')
426
+ }
427
+
428
+ const onEvent = typeof callbacks.onEvent === 'function' ? callbacks.onEvent : () => {}
429
+ const onThreadStarted = typeof callbacks.onThreadStarted === 'function' ? callbacks.onThreadStarted : () => {}
430
+
431
+ const child = createKimiSpawn(createExecArgs(session, normalizedPrompt), session.cwd)
432
+ onEvent(createKimiRunStatusEvent(session))
433
+
434
+ let stdoutBuffer = ''
435
+ let stderrBuffer = ''
436
+ let lastStderrLine = ''
437
+ let finalMessage = ''
438
+ let finalSessionId = String(session.engineSessionId || session.engineThreadId || session.codexThreadId || '').trim()
439
+ const normalizationState = createKimiNormalizationState()
440
+
441
+ const rememberSessionId = (sessionId) => {
442
+ const value = String(sessionId || '').trim()
443
+ if (!value || value === finalSessionId) {
444
+ return
445
+ }
446
+
447
+ finalSessionId = value
448
+ onThreadStarted(value)
449
+ }
450
+
451
+ const emitKimiJsonLine = (line) => {
452
+ const event = parseJsonLine(line)
453
+ if (!event) {
454
+ onEvent(createStdoutEnvelopeEvent(line))
455
+ return
456
+ }
457
+
458
+ const normalizedEvents = normalizeKimiEvents(event, normalizationState)
459
+ normalizedEvents.forEach((normalizedEvent) => {
460
+ onEvent(createAgentEventEnvelopeEvent(normalizedEvent))
461
+ })
462
+
463
+ const role = String(event?.role || '').trim().toLowerCase()
464
+ if (role === 'assistant') {
465
+ const content = Array.isArray(event.content) ? event.content : []
466
+ const textBlocks = content
467
+ .filter((block) => String(block?.type || '').trim().toLowerCase() === 'text')
468
+ .map((block) => String(block?.text || '').trim())
469
+ .filter(Boolean)
470
+
471
+ if (textBlocks.length) {
472
+ const text = textBlocks.join('\n').trim()
473
+ finalMessage = `${finalMessage}${finalMessage ? '\n' : ''}${text}`
474
+ }
475
+ }
476
+ }
477
+
478
+ child.stdout.on('data', (chunk) => {
479
+ stdoutBuffer += chunk.toString()
480
+ const { lines, rest } = splitBufferedLines(stdoutBuffer)
481
+ stdoutBuffer = rest
482
+ lines.forEach(emitKimiJsonLine)
483
+ })
484
+
485
+ child.stderr.on('data', (chunk) => {
486
+ stderrBuffer += chunk.toString()
487
+ const { lines, rest } = splitBufferedLines(stderrBuffer)
488
+ stderrBuffer = rest
489
+ lines.forEach((line) => {
490
+ const sessionId = extractKimiSessionIdFromStderrLine(line)
491
+ if (sessionId) {
492
+ rememberSessionId(sessionId)
493
+ }
494
+
495
+ if (isKimiInfoStderrLine(line)) {
496
+ return
497
+ }
498
+
499
+ lastStderrLine = line
500
+ onEvent(createStderrEnvelopeEvent(line))
501
+ })
502
+ })
503
+
504
+ const result = new Promise((resolve, reject) => {
505
+ child.on('error', (error) => {
506
+ reject(normalizeSpawnError(error))
507
+ })
508
+
509
+ child.on('close', (code) => {
510
+ flushBufferedText(stdoutBuffer).forEach(emitKimiJsonLine)
511
+ flushBufferedText(stderrBuffer).forEach((line) => {
512
+ const sessionId = extractKimiSessionIdFromStderrLine(line)
513
+ if (sessionId) {
514
+ rememberSessionId(sessionId)
515
+ }
516
+
517
+ if (isKimiInfoStderrLine(line)) {
518
+ return
519
+ }
520
+
521
+ lastStderrLine = line
522
+ onEvent(createStderrEnvelopeEvent(line))
523
+ })
524
+
525
+ if (code !== 0) {
526
+ reject(new Error(lastStderrLine || 'Kimi Code 执行失败。'))
527
+ return
528
+ }
529
+
530
+ const message = finalMessage.trim()
531
+ onEvent(createAgentEventEnvelopeEvent(createTurnCompletedEvent()))
532
+ onEvent(createCompletedEnvelopeEvent(message))
533
+
534
+ resolve({
535
+ sessionId: session.id,
536
+ threadId: finalSessionId,
537
+ message,
538
+ })
539
+ })
540
+ })
541
+
542
+ return {
543
+ child,
544
+ result,
545
+ cancel(options = {}) {
546
+ forceStopChildProcess(child, options)
547
+ },
548
+ }
549
+ }
550
+
551
+ export const kimiCodeRunner = {
552
+ engine: AGENT_ENGINES.KIMI_CODE,
553
+ label: getAgentEngineLabel(AGENT_ENGINES.KIMI_CODE),
554
+ supportsWorkspaceHistory: false,
555
+ listKnownWorkspaces() {
556
+ return []
557
+ },
558
+ streamSessionPrompt(session, prompt, callbacks = {}) {
559
+ return streamPromptToKimiCodeSession(session, prompt, callbacks)
560
+ },
561
+ }
@@ -216,6 +216,7 @@ function createIdleProgressEvent(context = {}) {
216
216
  return createStatusEnvelopeEvent({
217
217
  stage: 'queued',
218
218
  message: '当前仍在排队,等待 runner 空闲后开始执行。',
219
+ messageKey: 'runner.status.queued',
219
220
  })
220
221
  }
221
222
 
@@ -223,12 +224,16 @@ function createIdleProgressEvent(context = {}) {
223
224
  return createStatusEnvelopeEvent({
224
225
  stage: 'stopping',
225
226
  message: '正在停止执行,等待引擎退出...',
227
+ messageKey: 'runner.status.stopping',
226
228
  })
227
229
  }
228
230
 
231
+ const agentLabel = getAgentEngineLabel(context.engine || 'codex')
229
232
  return createStatusEnvelopeEvent({
230
233
  stage: 'running',
231
- message: `${getAgentEngineLabel(context.engine || 'codex')} 仍在执行中,最近暂无新的过程输出。`,
234
+ message: `${agentLabel} 正在思考中...`,
235
+ messageKey: 'runner.status.thinking',
236
+ messageParams: { agentLabel },
232
237
  })
233
238
  }
234
239