pikiclaw 0.2.49 → 0.2.51
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 +52 -56
- package/dist/bot-feishu-render.js +33 -0
- package/dist/bot-feishu.js +151 -2
- package/dist/bot-telegram-render.js +39 -0
- package/dist/bot-telegram.js +147 -2
- package/dist/bot.js +130 -1
- package/dist/cli.js +1 -1
- package/dist/code-agent.js +3 -2
- package/dist/dashboard-ui.js +8 -8
- package/dist/dashboard.js +210 -1
- package/dist/driver-claude.js +19 -0
- package/dist/driver-codex.js +110 -4
- package/dist/driver-gemini.js +10 -0
- package/dist/human-loop-codex.js +23 -0
- package/dist/human-loop.js +120 -0
- package/dist/mcp-bridge.js +122 -28
- package/dist/mcp-session-server.js +12 -2
- package/dist/tools/desktop.js +443 -0
- package/dist/user-config.js +6 -1
- package/package.json +2 -2
- package/dist/tools/gui.js +0 -14
package/README.md
CHANGED
|
@@ -2,13 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
# pikiclaw
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Put the world's smartest AI agents in your pocket. Command local Claude & Gemini via best IM.**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
*让最好用的 IM 变成你电脑上的顶级 Agent 控制台*
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
npx pikiclaw@latest
|
|
11
|
-
```
|
|
9
|
+
> npx pikiclaw@latest
|
|
12
10
|
|
|
13
11
|
<p align="center">
|
|
14
12
|
<a href="https://www.npmjs.com/package/pikiclaw"><img src="https://img.shields.io/npm/v/pikiclaw" alt="npm"></a>
|
|
@@ -92,48 +90,48 @@ npx pikiclaw@latest --doctor
|
|
|
92
90
|
|
|
93
91
|
---
|
|
94
92
|
|
|
95
|
-
##
|
|
96
|
-
|
|
97
|
-
### Channels
|
|
93
|
+
## Current Capabilities
|
|
98
94
|
|
|
99
|
-
|
|
100
|
-
- 飞书已可用
|
|
101
|
-
- 两个渠道可以同时启动
|
|
95
|
+
### Channels And Agents
|
|
102
96
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
-
|
|
106
|
-
- Codex CLI
|
|
107
|
-
- Gemini CLI
|
|
108
|
-
|
|
109
|
-
Agent 通过 driver registry 接入,模型列表、会话列表、usage 展示都走统一接口。
|
|
97
|
+
- Telegram、飞书都可用,也可以同时启动
|
|
98
|
+
- Claude Code、Codex CLI、Gemini CLI 都已接入
|
|
99
|
+
- agent 通过统一 driver registry 管理,模型列表、session 列表、usage 展示走同一套接口
|
|
110
100
|
|
|
111
101
|
### Runtime
|
|
112
102
|
|
|
113
103
|
- 流式预览和持续消息更新
|
|
114
104
|
- 会话切换、恢复和多轮续聊
|
|
115
105
|
- 工作目录浏览与切换
|
|
116
|
-
-
|
|
117
|
-
- watchdog 守护和自动重启
|
|
118
|
-
-
|
|
106
|
+
- 文件附件自动进入 session workspace
|
|
107
|
+
- 长任务防休眠、watchdog 守护和自动重启
|
|
108
|
+
- 长文本自动拆分,图片和文件可直接回传到 IM
|
|
109
|
+
- Dashboard 可查看运行状态、sessions、usage、主机状态和 macOS 权限状态
|
|
119
110
|
|
|
120
111
|
### Skills
|
|
121
112
|
|
|
122
|
-
-
|
|
113
|
+
- 项目级 skills 以 `.pikiclaw/skills/*/SKILL.md` 为 canonical 入口
|
|
123
114
|
- 兼容 `.claude/commands/*.md`
|
|
115
|
+
- 兼容 legacy `.claude/skills` / `.agents/skills`,并可合并回 `.pikiclaw/skills`
|
|
124
116
|
- IM 内可通过 `/skills` 和 `/sk_<name>` 触发
|
|
125
117
|
|
|
126
|
-
###
|
|
118
|
+
### Codex Human Loop
|
|
119
|
+
|
|
120
|
+
当 Codex 在运行过程中请求额外用户输入时,pikiclaw 会把问题转成 Telegram / 飞书里的交互提示,用户回复后再继续当前任务。
|
|
121
|
+
|
|
122
|
+
### MCP And GUI Automation
|
|
127
123
|
|
|
128
|
-
每次 Agent stream
|
|
124
|
+
每次 Agent stream 都会启动一个会话级 MCP bridge,把本地工具按本次任务注入给 Agent。
|
|
129
125
|
|
|
130
|
-
|
|
126
|
+
当前内置工具:
|
|
131
127
|
|
|
132
128
|
- `im_list_files`:列出 session workspace 文件
|
|
133
129
|
- `im_send_file`:把文件实时发回 IM
|
|
134
|
-
- `take_screenshot`:跨平台截图并返回路径
|
|
135
130
|
|
|
136
|
-
|
|
131
|
+
可选 GUI 能力:
|
|
132
|
+
|
|
133
|
+
- 浏览器自动化:通过 `@playwright/mcp` 补充接入,默认支持 Chrome extension mode,也可切到 headless / isolated 模式
|
|
134
|
+
- macOS 桌面自动化:通过 Appium Mac2 提供 `desktop_open_app`、`desktop_snapshot`、`desktop_click`、`desktop_type`、`desktop_screenshot` 等工具
|
|
137
135
|
|
|
138
136
|
---
|
|
139
137
|
|
|
@@ -156,42 +154,37 @@ Agent 通过 driver registry 接入,模型列表、会话列表、usage 展示
|
|
|
156
154
|
|
|
157
155
|
---
|
|
158
156
|
|
|
159
|
-
##
|
|
157
|
+
## Config And Setup Notes
|
|
160
158
|
|
|
161
|
-
|
|
159
|
+
- 持久化配置在 `~/.pikiclaw/setting.json`
|
|
160
|
+
- Dashboard 是主配置入口,环境变量仍然可用
|
|
161
|
+
- 浏览器 GUI 相关常用变量:
|
|
162
|
+
- `PIKICLAW_BROWSER_GUI`
|
|
163
|
+
- `PIKICLAW_BROWSER_USE_EXTENSION`
|
|
164
|
+
- `PIKICLAW_BROWSER_HEADLESS`
|
|
165
|
+
- `PIKICLAW_BROWSER_ISOLATED`
|
|
166
|
+
- `PLAYWRIGHT_MCP_EXTENSION_TOKEN`
|
|
167
|
+
- 桌面 GUI 相关常用变量:
|
|
168
|
+
- `PIKICLAW_DESKTOP_GUI`
|
|
169
|
+
- `PIKICLAW_DESKTOP_APPIUM_URL`
|
|
162
170
|
|
|
163
|
-
|
|
164
|
-
- MCP tools:偏“可执行工具能力”,目前是会话级 bridge,由 pikiclaw 在每次 stream 时注入
|
|
171
|
+
如果要启用 macOS 桌面自动化,需要先准备 Appium Mac2:
|
|
165
172
|
|
|
166
|
-
|
|
173
|
+
```bash
|
|
174
|
+
npm install -g appium
|
|
175
|
+
appium driver install mac2
|
|
176
|
+
appium
|
|
177
|
+
```
|
|
167
178
|
|
|
168
|
-
|
|
179
|
+
然后给运行 `pikiclaw` 的终端应用授予 macOS 的辅助功能权限。
|
|
169
180
|
|
|
170
|
-
|
|
181
|
+
---
|
|
171
182
|
|
|
172
|
-
|
|
183
|
+
## Roadmap
|
|
173
184
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
| 飞书渠道 | ✅ |
|
|
178
|
-
| Claude Code driver | ✅ |
|
|
179
|
-
| Codex CLI driver | ✅ |
|
|
180
|
-
| Gemini CLI driver | ✅ |
|
|
181
|
-
| Web Dashboard | ✅ |
|
|
182
|
-
| 项目级 Skills | ✅ |
|
|
183
|
-
| 会话级 MCP bridge | ✅ |
|
|
184
|
-
| 文件回传 / 截图回传 | ✅ |
|
|
185
|
-
| 守护重启 / 防休眠 | ✅ |
|
|
186
|
-
|
|
187
|
-
### 待办
|
|
188
|
-
|
|
189
|
-
| 项目 | 说明 |
|
|
190
|
-
|---|---|
|
|
191
|
-
| 顶级 Skills 接入 | 把 skills 从当前项目级入口提升为更统一的顶级接入能力 |
|
|
192
|
-
| 顶级 MCP 工具接入 | 把当前会话级 MCP bridge 扩展成更完整的顶级工具接入层 |
|
|
193
|
-
| GUI 自动化工具补全 | 在 `src/tools/gui.ts` 上接入点击、输入、聚焦、窗口控制等工具 |
|
|
194
|
-
| 更多渠道 | WhatsApp 仍在规划中 |
|
|
185
|
+
- 把当前会话级 MCP bridge 继续扩展成更完整的顶级工具接入层
|
|
186
|
+
- 继续完善 GUI 自动化能力,尤其是浏览器与桌面工具的协同链路
|
|
187
|
+
- 增加更多 IM 渠道,WhatsApp 仍在规划中
|
|
195
188
|
|
|
196
189
|
---
|
|
197
190
|
|
|
@@ -216,6 +209,9 @@ npx vitest run test/channel-feishu.unit.test.ts
|
|
|
216
209
|
npx pikiclaw@latest --doctor
|
|
217
210
|
```
|
|
218
211
|
|
|
212
|
+
`npm run dev` 只跑本地源码链路,会固定使用 `--no-daemon`,避免跳转到生产/自举用的 `npx pikiclaw@latest`。
|
|
213
|
+
同时会把本次启动的全部日志写到 `~/.pikiclaw/dev/dev.log`,并在每次启动时先清空旧日志。
|
|
214
|
+
|
|
219
215
|
更多实现细节见:
|
|
220
216
|
|
|
221
217
|
- [ARCHITECTURE.md](ARCHITECTURE.md)
|
|
@@ -9,6 +9,7 @@ import { fmtUptime, fmtTokens, fmtBytes, formatThinkingForDisplay, thinkLabel }
|
|
|
9
9
|
import { summarizePromptForStatus } from './bot-commands.js';
|
|
10
10
|
import { formatProviderUsageLines } from './bot-telegram-render.js';
|
|
11
11
|
import { formatActivityCommandSummary, parseActivitySummary, renderPlanForPreview, summarizeActivityForPreview } from './bot-streaming.js';
|
|
12
|
+
import { currentHumanLoopQuestion, humanLoopAnsweredCount, isHumanLoopAwaitingText, isHumanLoopQuestionAnswered, summarizeHumanLoopAnswer, } from './human-loop.js';
|
|
12
13
|
import path from 'node:path';
|
|
13
14
|
import { listSubdirs } from './bot.js';
|
|
14
15
|
// ---------------------------------------------------------------------------
|
|
@@ -163,6 +164,38 @@ export function renderSessionTurnMarkdown(userText, assistantText) {
|
|
|
163
164
|
parts.push('**Assistant**', assistant);
|
|
164
165
|
return parts.join('\n\n');
|
|
165
166
|
}
|
|
167
|
+
export function buildHumanLoopPromptMarkdown(prompt) {
|
|
168
|
+
const question = currentHumanLoopQuestion(prompt);
|
|
169
|
+
const lines = [`**${prompt.title}**`];
|
|
170
|
+
if (prompt.detail)
|
|
171
|
+
lines.push(`\`${prompt.detail}\``);
|
|
172
|
+
lines.push(`*${humanLoopAnsweredCount(prompt)}/${prompt.questions.length} answered*`);
|
|
173
|
+
if (!question)
|
|
174
|
+
return lines.join('\n\n');
|
|
175
|
+
if (question.header.trim())
|
|
176
|
+
lines.push(`**${question.header}**`);
|
|
177
|
+
lines.push(question.prompt);
|
|
178
|
+
const options = question.options || [];
|
|
179
|
+
if (options.length) {
|
|
180
|
+
lines.push(options.map((option, index) => {
|
|
181
|
+
const detail = option.description ? `\n ${option.description}` : '';
|
|
182
|
+
return `${index + 1}. ${option.label}${detail}`;
|
|
183
|
+
}).join('\n'));
|
|
184
|
+
}
|
|
185
|
+
if (isHumanLoopAwaitingText(prompt)) {
|
|
186
|
+
lines.push(`*${question.secret ? 'Reply in chat with the secret value.' : 'Reply in chat with text to answer.'}*`);
|
|
187
|
+
}
|
|
188
|
+
if (prompt.hint)
|
|
189
|
+
lines.push(`*${prompt.hint}*`);
|
|
190
|
+
if (prompt.questions.length > 1) {
|
|
191
|
+
lines.push(prompt.questions.map((item, index) => {
|
|
192
|
+
const summary = summarizeHumanLoopAnswer(prompt, item);
|
|
193
|
+
const answered = isHumanLoopQuestionAnswered(prompt, index);
|
|
194
|
+
return `${answered ? '●' : '○'} ${item.header || item.prompt}: ${summary.display}`;
|
|
195
|
+
}).join('\n'));
|
|
196
|
+
}
|
|
197
|
+
return lines.join('\n\n');
|
|
198
|
+
}
|
|
166
199
|
// ---------------------------------------------------------------------------
|
|
167
200
|
// LivePreview renderer — produces Markdown for Feishu card elements
|
|
168
201
|
// ---------------------------------------------------------------------------
|
package/dist/bot-feishu.js
CHANGED
|
@@ -18,7 +18,9 @@ import { getStartData, getSessionsPageData, getModelsListData, getSessionTurnPre
|
|
|
18
18
|
import { buildAgentsCommandView, buildModelsCommandView, buildSessionsCommandView, buildSkillsCommandView, decodeCommandAction, executeCommandAction, } from './bot-command-ui.js';
|
|
19
19
|
import { LivePreview } from './bot-telegram-live-preview.js';
|
|
20
20
|
import { formatActiveTaskRestartError, getActiveTaskCount, registerProcessRuntime, requestProcessRestart, } from './process-control.js';
|
|
21
|
-
import { feishuPreviewRenderer, feishuStreamingPreviewRenderer, buildInitialPreviewMarkdown, buildFinalReplyRender, renderCommandNotice, renderCommandSelectionCard, renderSessionTurnMarkdown, renderStart, renderStatus, renderHost, buildSwitchWorkdirCard, resolveFeishuRegisteredPath, } from './bot-feishu-render.js';
|
|
21
|
+
import { feishuPreviewRenderer, feishuStreamingPreviewRenderer, buildInitialPreviewMarkdown, buildHumanLoopPromptMarkdown, buildFinalReplyRender, renderCommandNotice, renderCommandSelectionCard, renderSessionTurnMarkdown, renderStart, renderStatus, renderHost, buildSwitchWorkdirCard, resolveFeishuRegisteredPath, } from './bot-feishu-render.js';
|
|
22
|
+
import { buildCodexHumanLoopPrompt } from './human-loop-codex.js';
|
|
23
|
+
import { currentHumanLoopQuestion, humanLoopOptionSelected } from './human-loop.js';
|
|
22
24
|
import { FeishuChannel } from './channel-feishu.js';
|
|
23
25
|
import { splitText, supportsChannelCapability } from './channel-base.js';
|
|
24
26
|
import { getActiveUserConfig } from './user-config.js';
|
|
@@ -376,11 +378,110 @@ export class FeishuBot extends Bot {
|
|
|
376
378
|
parts.push(`cancelled ${cancelledQueued} queued ${cancelledQueued === 1 ? 'task' : 'tasks'}`);
|
|
377
379
|
await ctx.reply(`Stopped current session: ${parts.join(', ')}.`);
|
|
378
380
|
}
|
|
381
|
+
buildHumanLoopKeyboard(promptId) {
|
|
382
|
+
const prompt = this.humanLoopPrompt(promptId);
|
|
383
|
+
const question = prompt ? currentHumanLoopQuestion(prompt) : null;
|
|
384
|
+
const rows = [];
|
|
385
|
+
for (let index = 0; index < (question?.options?.length || 0); index++) {
|
|
386
|
+
const option = question.options[index];
|
|
387
|
+
rows.push({
|
|
388
|
+
actions: [{
|
|
389
|
+
tag: 'button',
|
|
390
|
+
text: { tag: 'plain_text', content: `${humanLoopOptionSelected(prompt, option.value) ? '●' : '○'} ${option.label}`.slice(0, 32) },
|
|
391
|
+
value: { action: `hl:o:${promptId}:${index}` },
|
|
392
|
+
}],
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
if (question?.options?.length && question.allowFreeform) {
|
|
396
|
+
rows.push({
|
|
397
|
+
actions: [{
|
|
398
|
+
tag: 'button',
|
|
399
|
+
text: { tag: 'plain_text', content: 'Other...' },
|
|
400
|
+
value: { action: `hl:other:${promptId}` },
|
|
401
|
+
}],
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
if (question?.allowEmpty) {
|
|
405
|
+
rows.push({
|
|
406
|
+
actions: [{
|
|
407
|
+
tag: 'button',
|
|
408
|
+
text: { tag: 'plain_text', content: 'Skip' },
|
|
409
|
+
value: { action: `hl:skip:${promptId}` },
|
|
410
|
+
}],
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
rows.push({
|
|
414
|
+
actions: [{
|
|
415
|
+
tag: 'button',
|
|
416
|
+
text: { tag: 'plain_text', content: 'Cancel' },
|
|
417
|
+
value: { action: `hl:cancel:${promptId}` },
|
|
418
|
+
}],
|
|
419
|
+
});
|
|
420
|
+
return { rows };
|
|
421
|
+
}
|
|
422
|
+
async refreshHumanLoopPrompt(chatId, promptId, suffix) {
|
|
423
|
+
const prompt = this.humanLoopPrompt(promptId);
|
|
424
|
+
if (!prompt)
|
|
425
|
+
return;
|
|
426
|
+
const messageId = prompt.messageIds[0];
|
|
427
|
+
if (!messageId)
|
|
428
|
+
return;
|
|
429
|
+
const markdown = `${buildHumanLoopPromptMarkdown(prompt)}${suffix ? `\n\n*${suffix}*` : ''}`;
|
|
430
|
+
await this.channel.editMessage(chatId, String(messageId), markdown, {
|
|
431
|
+
keyboard: this.buildHumanLoopKeyboard(promptId),
|
|
432
|
+
}).catch(() => { });
|
|
433
|
+
}
|
|
434
|
+
async finalizeHumanLoopPrompt(prompt, suffix) {
|
|
435
|
+
if (!prompt)
|
|
436
|
+
return;
|
|
437
|
+
const messageId = prompt.messageIds[0];
|
|
438
|
+
if (!messageId)
|
|
439
|
+
return;
|
|
440
|
+
const markdown = `${buildHumanLoopPromptMarkdown(prompt)}\n\n*${suffix}*`;
|
|
441
|
+
await this.channel.editMessage(prompt.chatId, String(messageId), markdown, {
|
|
442
|
+
keyboard: { rows: [] },
|
|
443
|
+
}).catch(() => { });
|
|
444
|
+
}
|
|
445
|
+
createCodexHumanLoopHandler(ctx, taskId) {
|
|
446
|
+
return async (request) => {
|
|
447
|
+
const blueprint = buildCodexHumanLoopPrompt(request);
|
|
448
|
+
const active = this.beginHumanLoopPrompt({
|
|
449
|
+
taskId,
|
|
450
|
+
chatId: ctx.chatId,
|
|
451
|
+
...blueprint,
|
|
452
|
+
});
|
|
453
|
+
try {
|
|
454
|
+
const sent = await ctx.reply(buildHumanLoopPromptMarkdown(active.prompt), {
|
|
455
|
+
keyboard: this.buildHumanLoopKeyboard(active.prompt.promptId),
|
|
456
|
+
});
|
|
457
|
+
if (sent)
|
|
458
|
+
this.registerHumanLoopMessage(active.prompt.promptId, sent);
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
this.humanLoopCancel(active.prompt.promptId, error?.message || 'Failed to send prompt.');
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
return active.result;
|
|
465
|
+
};
|
|
466
|
+
}
|
|
379
467
|
// ---- streaming bridge -----------------------------------------------------
|
|
380
468
|
async handleMessage(msg, ctx) {
|
|
381
469
|
const text = msg.text.trim();
|
|
382
470
|
if (!text && !msg.files.length)
|
|
383
471
|
return;
|
|
472
|
+
const pendingPrompt = this.pendingHumanLoopPrompt(ctx.chatId);
|
|
473
|
+
if (pendingPrompt && text && !msg.files.length && !text.startsWith('/')) {
|
|
474
|
+
const result = this.humanLoopSubmitText(ctx.chatId, text);
|
|
475
|
+
if (!result) {
|
|
476
|
+
await ctx.reply('Please answer the active prompt using the buttons above.');
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (result.completed)
|
|
480
|
+
await this.finalizeHumanLoopPrompt(result.prompt, 'Answer submitted.');
|
|
481
|
+
else
|
|
482
|
+
await this.refreshHumanLoopPrompt(ctx.chatId, result.prompt.promptId);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
384
485
|
const session = this.resolveIncomingSession(ctx, text, msg.files);
|
|
385
486
|
const cs = this.chat(ctx.chatId);
|
|
386
487
|
this.applySessionSelection(cs, session);
|
|
@@ -484,7 +585,7 @@ export class FeishuBot extends Bot {
|
|
|
484
585
|
const mcpSendFile = this.createMcpSendFileCallback(ctx);
|
|
485
586
|
const result = await this.runStream(prompt, session, files, (nextText, nextThinking, nextActivity = '', meta, plan) => {
|
|
486
587
|
livePreview?.update(nextText, nextThinking, nextActivity, meta, plan);
|
|
487
|
-
}, undefined, mcpSendFile, abortController.signal);
|
|
588
|
+
}, undefined, mcpSendFile, abortController.signal, this.createCodexHumanLoopHandler(ctx, taskId));
|
|
488
589
|
await livePreview?.settle();
|
|
489
590
|
const finalReplyIds = await this.sendFinalReply(ctx, placeholderId, session.agent, result);
|
|
490
591
|
this.registerSessionMessages(ctx.chatId, finalReplyIds, session);
|
|
@@ -680,6 +781,8 @@ export class FeishuBot extends Bot {
|
|
|
680
781
|
// ---- callback handlers ----------------------------------------------------
|
|
681
782
|
async handleCallback(data, ctx) {
|
|
682
783
|
try {
|
|
784
|
+
if (await this.handleHumanLoopCallback(data, ctx))
|
|
785
|
+
return;
|
|
683
786
|
if (await this.handleTaskStopCallback(data, ctx))
|
|
684
787
|
return;
|
|
685
788
|
if (await this.handleSwitchNavigateCallback(data, ctx))
|
|
@@ -698,6 +801,52 @@ export class FeishuBot extends Bot {
|
|
|
698
801
|
this.log(`callback error: ${e}`);
|
|
699
802
|
}
|
|
700
803
|
}
|
|
804
|
+
async handleHumanLoopCallback(data, ctx) {
|
|
805
|
+
if (!data.startsWith('hl:'))
|
|
806
|
+
return false;
|
|
807
|
+
const [, action, promptId, rawIndex] = data.split(':');
|
|
808
|
+
const prompt = this.humanLoopPrompt(promptId);
|
|
809
|
+
if (!prompt)
|
|
810
|
+
return true;
|
|
811
|
+
if (action === 'cancel') {
|
|
812
|
+
const cancelled = this.humanLoopCancel(promptId, 'Prompt cancelled from Feishu.');
|
|
813
|
+
await this.finalizeHumanLoopPrompt(cancelled, 'Cancelled.');
|
|
814
|
+
return true;
|
|
815
|
+
}
|
|
816
|
+
if (action === 'skip') {
|
|
817
|
+
const result = this.humanLoopSkip(promptId);
|
|
818
|
+
if (!result)
|
|
819
|
+
return true;
|
|
820
|
+
if (result.completed)
|
|
821
|
+
await this.finalizeHumanLoopPrompt(result.prompt, 'Answer submitted.');
|
|
822
|
+
else
|
|
823
|
+
await this.refreshHumanLoopPrompt(ctx.chatId, promptId);
|
|
824
|
+
return true;
|
|
825
|
+
}
|
|
826
|
+
if (action === 'other') {
|
|
827
|
+
const result = this.humanLoopSelectOption(promptId, '__other__', { requestFreeform: true });
|
|
828
|
+
if (!result)
|
|
829
|
+
return true;
|
|
830
|
+
await this.refreshHumanLoopPrompt(ctx.chatId, promptId);
|
|
831
|
+
return true;
|
|
832
|
+
}
|
|
833
|
+
if (action === 'o') {
|
|
834
|
+
const index = Number.parseInt(rawIndex || '', 10);
|
|
835
|
+
const question = this.humanLoopCurrentQuestion(promptId);
|
|
836
|
+
const option = Number.isFinite(index) ? question?.options?.[index] : null;
|
|
837
|
+
if (!option)
|
|
838
|
+
return true;
|
|
839
|
+
const result = this.humanLoopSelectOption(promptId, option.value);
|
|
840
|
+
if (!result)
|
|
841
|
+
return true;
|
|
842
|
+
if (result.completed)
|
|
843
|
+
await this.finalizeHumanLoopPrompt(result.prompt, 'Answer submitted.');
|
|
844
|
+
else
|
|
845
|
+
await this.refreshHumanLoopPrompt(ctx.chatId, promptId);
|
|
846
|
+
return true;
|
|
847
|
+
}
|
|
848
|
+
return true;
|
|
849
|
+
}
|
|
701
850
|
async handleTaskStopCallback(data, ctx) {
|
|
702
851
|
if (!data.startsWith('tsk:stop:'))
|
|
703
852
|
return false;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { encodeCommandAction } from './bot-command-ui.js';
|
|
2
2
|
import { fmtUptime, formatThinkingForDisplay, thinkLabel } from './bot.js';
|
|
3
3
|
import { formatActivityCommandSummary, parseActivitySummary, renderPlanForPreview, summarizeActivityForPreview } from './bot-streaming.js';
|
|
4
|
+
import { currentHumanLoopQuestion, humanLoopAnsweredCount, isHumanLoopAwaitingText, isHumanLoopQuestionAnswered, summarizeHumanLoopAnswer, } from './human-loop.js';
|
|
4
5
|
export function escapeHtml(t) {
|
|
5
6
|
return t.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
6
7
|
}
|
|
@@ -91,6 +92,44 @@ export function renderCommandSelectionKeyboard(view) {
|
|
|
91
92
|
}))),
|
|
92
93
|
};
|
|
93
94
|
}
|
|
95
|
+
export function buildHumanLoopPromptHtml(prompt) {
|
|
96
|
+
const question = currentHumanLoopQuestion(prompt);
|
|
97
|
+
const lines = [`<b>${escapeHtml(prompt.title)}</b>`];
|
|
98
|
+
if (prompt.detail)
|
|
99
|
+
lines.push(compactCode(prompt.detail, 40));
|
|
100
|
+
lines.push(`<i>${humanLoopAnsweredCount(prompt)}/${prompt.questions.length} answered</i>`);
|
|
101
|
+
if (!question)
|
|
102
|
+
return lines.join('\n');
|
|
103
|
+
lines.push('');
|
|
104
|
+
if (question.header.trim())
|
|
105
|
+
lines.push(`<b>${escapeHtml(question.header)}</b>`);
|
|
106
|
+
lines.push(escapeHtml(question.prompt));
|
|
107
|
+
const options = question.options || [];
|
|
108
|
+
if (options.length) {
|
|
109
|
+
lines.push('');
|
|
110
|
+
for (let i = 0; i < options.length; i++) {
|
|
111
|
+
const option = options[i];
|
|
112
|
+
lines.push(`${i + 1}. ${escapeHtml(option.label)}`);
|
|
113
|
+
if (option.description)
|
|
114
|
+
lines.push(`<i>${escapeHtml(option.description)}</i>`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (isHumanLoopAwaitingText(prompt)) {
|
|
118
|
+
lines.push('', `<i>${question.secret ? 'Reply with the secret value in chat.' : 'Reply with text in chat to answer.'}</i>`);
|
|
119
|
+
}
|
|
120
|
+
if (prompt.hint)
|
|
121
|
+
lines.push('', `<i>${escapeHtml(prompt.hint)}</i>`);
|
|
122
|
+
if (prompt.questions.length > 1) {
|
|
123
|
+
lines.push('');
|
|
124
|
+
for (let i = 0; i < prompt.questions.length; i++) {
|
|
125
|
+
const item = prompt.questions[i];
|
|
126
|
+
const summary = summarizeHumanLoopAnswer(prompt, item);
|
|
127
|
+
const answered = isHumanLoopQuestionAnswered(prompt, i);
|
|
128
|
+
lines.push(`${answered ? '●' : '○'} ${escapeHtml(item.header || item.prompt)}: ${escapeHtml(summary.display)}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return lines.join('\n');
|
|
132
|
+
}
|
|
94
133
|
function mdInline(line) {
|
|
95
134
|
const parts = [];
|
|
96
135
|
let rest = line;
|
package/dist/bot-telegram.js
CHANGED
|
@@ -19,7 +19,9 @@ import { buildAgentsCommandView, buildModelsCommandView, buildSessionsCommandVie
|
|
|
19
19
|
import { buildSwitchWorkdirView, resolveRegisteredPath } from './bot-telegram-directory.js';
|
|
20
20
|
import { LivePreview } from './bot-telegram-live-preview.js';
|
|
21
21
|
import { formatActiveTaskRestartError, getActiveTaskCount, registerProcessRuntime, buildRestartCommand, requestProcessRestart, } from './process-control.js';
|
|
22
|
-
import { buildInitialPreviewHtml, buildStreamPreviewHtml, buildFinalReplyRender, escapeHtml, formatMenuLines, formatProviderUsageLines, renderCommandNoticeHtml, renderCommandSelectionHtml, renderCommandSelectionKeyboard, renderSessionTurnHtml, truncateMiddle, } from './bot-telegram-render.js';
|
|
22
|
+
import { buildInitialPreviewHtml, buildHumanLoopPromptHtml, buildStreamPreviewHtml, buildFinalReplyRender, escapeHtml, formatMenuLines, formatProviderUsageLines, renderCommandNoticeHtml, renderCommandSelectionHtml, renderCommandSelectionKeyboard, renderSessionTurnHtml, truncateMiddle, } from './bot-telegram-render.js';
|
|
23
|
+
import { buildCodexHumanLoopPrompt } from './human-loop-codex.js';
|
|
24
|
+
import { currentHumanLoopQuestion, humanLoopOptionSelected } from './human-loop.js';
|
|
23
25
|
import { TelegramChannel } from './channel-telegram.js';
|
|
24
26
|
import { splitText, supportsChannelCapability } from './channel-base.js';
|
|
25
27
|
import { getActiveUserConfig } from './user-config.js';
|
|
@@ -358,11 +360,91 @@ export class TelegramBot extends Bot {
|
|
|
358
360
|
parts.push(`cancelled ${cancelledQueued} queued ${cancelledQueued === 1 ? 'task' : 'tasks'}`);
|
|
359
361
|
await ctx.reply(`Stopped current session: ${parts.join(', ')}.`);
|
|
360
362
|
}
|
|
363
|
+
buildHumanLoopKeyboard(promptId) {
|
|
364
|
+
const prompt = this.humanLoopPrompt(promptId);
|
|
365
|
+
const question = prompt ? currentHumanLoopQuestion(prompt) : null;
|
|
366
|
+
const inline_keyboard = [];
|
|
367
|
+
const optionRows = (question?.options || []).map((option, index) => ([{
|
|
368
|
+
text: `${humanLoopOptionSelected(prompt, option.value) ? '●' : '○'} ${truncateMiddle(option.label, 28)}`,
|
|
369
|
+
callback_data: `hl:o:${promptId}:${index}`,
|
|
370
|
+
}]));
|
|
371
|
+
inline_keyboard.push(...optionRows);
|
|
372
|
+
if (question?.options?.length && question.allowFreeform) {
|
|
373
|
+
inline_keyboard.push([{ text: 'Other...', callback_data: `hl:other:${promptId}` }]);
|
|
374
|
+
}
|
|
375
|
+
if (question?.allowEmpty) {
|
|
376
|
+
inline_keyboard.push([{ text: 'Skip', callback_data: `hl:skip:${promptId}` }]);
|
|
377
|
+
}
|
|
378
|
+
inline_keyboard.push([{ text: 'Cancel', callback_data: `hl:cancel:${promptId}` }]);
|
|
379
|
+
return { inline_keyboard };
|
|
380
|
+
}
|
|
381
|
+
async refreshHumanLoopPrompt(chatId, promptId, opts = {}) {
|
|
382
|
+
const prompt = this.humanLoopPrompt(promptId);
|
|
383
|
+
if (!prompt)
|
|
384
|
+
return;
|
|
385
|
+
const messageId = prompt.messageIds[0];
|
|
386
|
+
if (typeof messageId !== 'number')
|
|
387
|
+
return;
|
|
388
|
+
const html = `${buildHumanLoopPromptHtml(prompt)}${opts.suffix ? `\n\n<i>${escapeHtml(opts.suffix)}</i>` : ''}`;
|
|
389
|
+
await this.channel.editMessage(chatId, messageId, html, {
|
|
390
|
+
parseMode: 'HTML',
|
|
391
|
+
keyboard: opts.submitted ? { inline_keyboard: [] } : this.buildHumanLoopKeyboard(promptId),
|
|
392
|
+
}).catch(() => { });
|
|
393
|
+
}
|
|
394
|
+
async finalizeHumanLoopPrompt(prompt, suffix) {
|
|
395
|
+
if (!prompt)
|
|
396
|
+
return;
|
|
397
|
+
const messageId = prompt.messageIds[0];
|
|
398
|
+
if (typeof messageId !== 'number')
|
|
399
|
+
return;
|
|
400
|
+
const html = `${buildHumanLoopPromptHtml(prompt)}\n\n<i>${escapeHtml(suffix)}</i>`;
|
|
401
|
+
await this.channel.editMessage(prompt.chatId, messageId, html, {
|
|
402
|
+
parseMode: 'HTML',
|
|
403
|
+
keyboard: { inline_keyboard: [] },
|
|
404
|
+
}).catch(() => { });
|
|
405
|
+
}
|
|
406
|
+
createCodexHumanLoopHandler(ctx, taskId, messageThreadId) {
|
|
407
|
+
return async (request) => {
|
|
408
|
+
const blueprint = buildCodexHumanLoopPrompt(request);
|
|
409
|
+
const active = this.beginHumanLoopPrompt({
|
|
410
|
+
taskId,
|
|
411
|
+
chatId: ctx.chatId,
|
|
412
|
+
...blueprint,
|
|
413
|
+
});
|
|
414
|
+
try {
|
|
415
|
+
const sent = await ctx.reply(buildHumanLoopPromptHtml(active.prompt), {
|
|
416
|
+
parseMode: 'HTML',
|
|
417
|
+
messageThreadId,
|
|
418
|
+
keyboard: this.buildHumanLoopKeyboard(active.prompt.promptId),
|
|
419
|
+
});
|
|
420
|
+
if (typeof sent === 'number')
|
|
421
|
+
this.registerHumanLoopMessage(active.prompt.promptId, sent);
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
this.humanLoopCancel(active.prompt.promptId, error?.message || 'Failed to send prompt.');
|
|
425
|
+
throw error;
|
|
426
|
+
}
|
|
427
|
+
return active.result;
|
|
428
|
+
};
|
|
429
|
+
}
|
|
361
430
|
// ---- streaming bridge -----------------------------------------------------
|
|
362
431
|
async handleMessage(msg, ctx) {
|
|
363
432
|
const text = msg.text.trim();
|
|
364
433
|
if (!text && !msg.files.length)
|
|
365
434
|
return;
|
|
435
|
+
const pendingPrompt = this.pendingHumanLoopPrompt(ctx.chatId);
|
|
436
|
+
if (pendingPrompt && text && !msg.files.length && !text.startsWith('/')) {
|
|
437
|
+
const result = this.humanLoopSubmitText(ctx.chatId, text);
|
|
438
|
+
if (!result) {
|
|
439
|
+
await ctx.reply('Please answer the active prompt using the buttons above.');
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (result.completed)
|
|
443
|
+
await this.finalizeHumanLoopPrompt(result.prompt, 'Answer submitted.');
|
|
444
|
+
else
|
|
445
|
+
await this.refreshHumanLoopPrompt(ctx.chatId, result.prompt.promptId);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
366
448
|
const session = this.resolveIncomingSession(ctx, text, msg.files);
|
|
367
449
|
const cs = this.chat(ctx.chatId);
|
|
368
450
|
this.applySessionSelection(cs, session);
|
|
@@ -472,7 +554,7 @@ export class TelegramBot extends Bot {
|
|
|
472
554
|
const mcpSendFile = this.createMcpSendFileCallback(ctx, messageThreadId);
|
|
473
555
|
const result = await this.runStream(prompt, session, files, (nextText, nextThinking, nextActivity = '', meta, plan) => {
|
|
474
556
|
livePreview?.update(nextText, nextThinking, nextActivity, meta, plan);
|
|
475
|
-
}, undefined, mcpSendFile, abortController.signal);
|
|
557
|
+
}, undefined, mcpSendFile, abortController.signal, this.createCodexHumanLoopHandler(ctx, taskId, messageThreadId));
|
|
476
558
|
await livePreview?.settle();
|
|
477
559
|
this.log(`[handleMessage] done agent=${session.agent} ok=${result.ok} session=${result.sessionId || '?'} elapsed=${result.elapsedS.toFixed(1)}s edits=${livePreview?.getEditCount() || 0} ` +
|
|
478
560
|
`tokens=in:${fmtTokens(result.inputTokens)}/cached:${fmtTokens(result.cachedInputTokens)}/out:${fmtTokens(result.outputTokens)}`);
|
|
@@ -659,7 +741,70 @@ export class TelegramBot extends Bot {
|
|
|
659
741
|
await ctx.answerCallback('Nothing to stop.');
|
|
660
742
|
return true;
|
|
661
743
|
}
|
|
744
|
+
async handleHumanLoopCallback(data, ctx) {
|
|
745
|
+
if (!data.startsWith('hl:'))
|
|
746
|
+
return false;
|
|
747
|
+
const [, action, promptId, rawIndex] = data.split(':');
|
|
748
|
+
const prompt = this.humanLoopPrompt(promptId);
|
|
749
|
+
if (!prompt) {
|
|
750
|
+
await ctx.answerCallback('This prompt is no longer active.');
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
753
|
+
if (action === 'cancel') {
|
|
754
|
+
const cancelled = this.humanLoopCancel(promptId, 'Prompt cancelled from Telegram.');
|
|
755
|
+
await this.finalizeHumanLoopPrompt(cancelled, 'Cancelled.');
|
|
756
|
+
await ctx.answerCallback('Cancelled.');
|
|
757
|
+
return true;
|
|
758
|
+
}
|
|
759
|
+
if (action === 'skip') {
|
|
760
|
+
const result = this.humanLoopSkip(promptId);
|
|
761
|
+
if (!result) {
|
|
762
|
+
await ctx.answerCallback('This prompt is no longer active.');
|
|
763
|
+
return true;
|
|
764
|
+
}
|
|
765
|
+
if (result.completed)
|
|
766
|
+
await this.finalizeHumanLoopPrompt(result.prompt, 'Answer submitted.');
|
|
767
|
+
else
|
|
768
|
+
await this.refreshHumanLoopPrompt(ctx.chatId, promptId);
|
|
769
|
+
await ctx.answerCallback(result.completed ? 'Submitted.' : 'Skipped.');
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
if (action === 'other') {
|
|
773
|
+
const result = this.humanLoopSelectOption(promptId, '__other__', { requestFreeform: true });
|
|
774
|
+
if (!result) {
|
|
775
|
+
await ctx.answerCallback('This prompt is no longer active.');
|
|
776
|
+
return true;
|
|
777
|
+
}
|
|
778
|
+
await this.refreshHumanLoopPrompt(ctx.chatId, promptId);
|
|
779
|
+
await ctx.answerCallback('Reply with text to continue.');
|
|
780
|
+
return true;
|
|
781
|
+
}
|
|
782
|
+
if (action === 'o') {
|
|
783
|
+
const index = Number.parseInt(rawIndex || '', 10);
|
|
784
|
+
const question = this.humanLoopCurrentQuestion(promptId);
|
|
785
|
+
const option = Number.isFinite(index) ? question?.options?.[index] : null;
|
|
786
|
+
if (!option) {
|
|
787
|
+
await ctx.answerCallback('Option expired.');
|
|
788
|
+
return true;
|
|
789
|
+
}
|
|
790
|
+
const result = this.humanLoopSelectOption(promptId, option.value);
|
|
791
|
+
if (!result) {
|
|
792
|
+
await ctx.answerCallback('This prompt is no longer active.');
|
|
793
|
+
return true;
|
|
794
|
+
}
|
|
795
|
+
if (result.completed)
|
|
796
|
+
await this.finalizeHumanLoopPrompt(result.prompt, 'Answer submitted.');
|
|
797
|
+
else
|
|
798
|
+
await this.refreshHumanLoopPrompt(ctx.chatId, promptId);
|
|
799
|
+
await ctx.answerCallback(result.completed ? 'Submitted.' : 'Recorded.');
|
|
800
|
+
return true;
|
|
801
|
+
}
|
|
802
|
+
await ctx.answerCallback();
|
|
803
|
+
return true;
|
|
804
|
+
}
|
|
662
805
|
async handleCallback(data, ctx) {
|
|
806
|
+
if (await this.handleHumanLoopCallback(data, ctx))
|
|
807
|
+
return;
|
|
663
808
|
if (await this.handleTaskStopCallback(data, ctx))
|
|
664
809
|
return;
|
|
665
810
|
if (await this.handleSwitchNavigateCallback(data, ctx))
|