pikiclaw 0.2.35 → 0.2.37
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 +36 -23
- package/dist/bot-feishu-render.js +8 -6
- package/dist/bot-feishu.js +61 -6
- package/dist/bot-telegram-render.js +8 -6
- package/dist/bot.js +34 -19
- package/dist/channel-feishu.js +26 -17
- package/dist/channel-telegram.js +11 -0
- package/dist/code-agent.js +57 -73
- package/dist/dashboard-ui.js +5 -5
- package/dist/dashboard.js +62 -1
- package/dist/mcp-bridge.js +65 -14
- package/dist/mcp-session-server.js +73 -187
- package/dist/run.js +2 -1
- package/dist/tools/gui.js +14 -0
- package/dist/tools/types.js +12 -0
- package/dist/tools/workspace.js +231 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,6 +40,19 @@ pikiclaw 的思路不同:**只挑最好的,然后把它们组合到极致。
|
|
|
40
40
|
└──────── 流式进度 / 文件 / 截图 ←──────────┘
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
pikiclaw 不是另一个终端包装器,也不是另一个云端 IDE。
|
|
44
|
+
|
|
45
|
+
它更像一个让官方 coding agent 变得**可远程调度、可持续运行、可回传结果**的本地执行中枢。
|
|
46
|
+
|
|
47
|
+
当你需要:
|
|
48
|
+
|
|
49
|
+
- 在手机上发一句话就能派活
|
|
50
|
+
- 任务必须跑在你自己的电脑、现有代码库和本地工具链里
|
|
51
|
+
- 想在 Claude Code / Codex CLI / Gemini CLI 之间自由切换
|
|
52
|
+
- 希望进度、截图、文件自动回到聊天
|
|
53
|
+
|
|
54
|
+
pikiclaw 会比“守在终端里”“SSH + tmux”或“单厂商云端 agent”更顺。
|
|
55
|
+
|
|
43
56
|
---
|
|
44
57
|
|
|
45
58
|
## Quick Start
|
|
@@ -48,7 +61,7 @@ pikiclaw 的思路不同:**只挑最好的,然后把它们组合到极致。
|
|
|
48
61
|
|
|
49
62
|
- Node.js 18+
|
|
50
63
|
- 本机已安装 [`claude`](https://docs.anthropic.com/en/docs/claude-code)、[`codex`](https://github.com/openai/codex) 或 [`gemini`](https://github.com/google-gemini/gemini-cli) 中的任意一个
|
|
51
|
-
- 一个 [Telegram Bot Token](https://t.me/BotFather) 或[飞书应用](https://open.feishu.cn)凭证
|
|
64
|
+
- 一个 [Telegram Bot Token](https://t.me/BotFather) 或 [飞书应用](https://open.feishu.cn) 凭证
|
|
52
65
|
|
|
53
66
|
### 一行启动
|
|
54
67
|
|
|
@@ -87,10 +100,10 @@ npx pikiclaw@latest --setup
|
|
|
87
100
|
|
|
88
101
|
### IM Channels
|
|
89
102
|
|
|
90
|
-
| 渠道 | 消息编辑 |
|
|
91
|
-
|
|
92
|
-
| **Telegram** | ✅ | ✅ | ✅ |
|
|
93
|
-
| **飞书** | ✅ | ✅ | ✅ | ✅ |
|
|
103
|
+
| 渠道 | 消息编辑 | 文件收发 | 回调按钮 | 命令菜单 | 场景 |
|
|
104
|
+
|------|---------|---------|---------|---------|------|
|
|
105
|
+
| **Telegram** | ✅ | ✅ | ✅ | ✅ | 全球 / 个人 |
|
|
106
|
+
| **飞书** | ✅ | ✅ | ✅ | ✅ | 国内 / 团队 |
|
|
94
107
|
|
|
95
108
|
两个渠道可以**同时启动**。
|
|
96
109
|
|
|
@@ -99,16 +112,15 @@ npx pikiclaw@latest --setup
|
|
|
99
112
|
| 能力 | 说明 |
|
|
100
113
|
|------|------|
|
|
101
114
|
| 实时流式输出 | Agent 工作时消息持续更新 |
|
|
102
|
-
| Thinking / Reasoning | 实时查看 Agent
|
|
115
|
+
| Thinking / Reasoning / Plan | 实时查看 Agent 的思考、推理和计划步骤 |
|
|
103
116
|
| Token 追踪 | 输入/输出/缓存统计,上下文使用率实时显示 |
|
|
104
|
-
| 产物回传 |
|
|
105
|
-
|
|
|
106
|
-
| 守护进程 | 崩溃自动重启,指数退避(3s → 60s) |
|
|
117
|
+
| 产物回传 | 截图、日志、生成文件自动发回聊天 |
|
|
118
|
+
| 长程任务保障 | 系统级防休眠 + 守护进程 + 异常自愈 |
|
|
107
119
|
| 长文本处理 | 超长输出自动拆分或打包为 `.md` |
|
|
108
120
|
| 多会话管理 | 随时切换、恢复历史会话 |
|
|
109
121
|
| 图片/文件输入 | 截图、PDF、文档直接发给 Agent |
|
|
110
122
|
| 项目 Skills | `.pikiclaw/skills/` 自定义技能,兼容 `.claude/commands/` |
|
|
111
|
-
| 安全模式 |
|
|
123
|
+
| 安全模式 | 白名单访问控制,支持切换更安全的 agent 权限模式 |
|
|
112
124
|
| Web Dashboard | 可视化配置、会话浏览、主机监控 |
|
|
113
125
|
|
|
114
126
|
---
|
|
@@ -134,24 +146,23 @@ npx pikiclaw@latest --setup
|
|
|
134
146
|
|
|
135
147
|
| | 终端直接跑 | SSH + tmux | 云端 Agent | **pikiclaw** |
|
|
136
148
|
|---|---|---|---|---|
|
|
137
|
-
| 执行环境 | ✅ 本地 | ✅ 本地 |
|
|
149
|
+
| 执行环境 | ✅ 本地 | ✅ 本地 | ⚠️ 通常是远端或沙盒 | ✅ 本地 |
|
|
138
150
|
| 走开后还能跑 | ❌ 合盖就断 | ⚠️ 要配 tmux | ✅ | ✅ 防休眠 + 守护进程 |
|
|
139
151
|
| 手机可控 | ❌ | ⚠️ 打字痛苦 | ✅ | ✅ IM 原生 |
|
|
140
|
-
| 实时看进度 | ✅ 终端 | ⚠️ 得连上去看 |
|
|
152
|
+
| 实时看进度 | ✅ 终端 | ⚠️ 得连上去看 | ⚠️ 依平台而定 | ✅ 流式推到聊天 |
|
|
141
153
|
| 结果自动回传 | ❌ | ❌ | ⚠️ 看平台 | ✅ 截图/文件/长文本 |
|
|
142
|
-
| 配置门槛 | 无 | SSH/穿透/tmux |
|
|
154
|
+
| 配置门槛 | 无 | SSH/穿透/tmux | 注册并适应平台工作流 | `npx` 一行 |
|
|
143
155
|
|
|
144
|
-
### pikiclaw vs.
|
|
156
|
+
### pikiclaw vs. OpenClaw / 官方入口
|
|
145
157
|
|
|
146
|
-
| | **pikiclaw** | OpenClaw |
|
|
158
|
+
| 维度 | **pikiclaw** | OpenClaw | 官方入口(Claude / Codex / Gemini) |
|
|
147
159
|
|---|---|---|---|
|
|
148
|
-
|
|
|
149
|
-
|
|
|
150
|
-
|
|
|
151
|
-
|
|
|
152
|
-
|
|
|
153
|
-
|
|
|
154
|
-
| **上手成本** | **一行 `npx`** | 需部署后端 | 需安装服务端 |
|
|
160
|
+
| 产品层 | IM 驱动的本地 agent 控制平面 | 通用个人 AI 助手 / 多渠道生态 | 单一厂商的原生 agent 入口 |
|
|
161
|
+
| Agent 策略 | 复用官方 CLI,吃到各家最新能力 | 自带 runtime / agent stack | 只服务自家模型 |
|
|
162
|
+
| 执行环境 | 你的电脑 | 个人设备网络 / 本地节点 | 本地 CLI 或厂商云 |
|
|
163
|
+
| 渠道策略 | Telegram + 飞书深度打磨 | 广覆盖 | Web / App / Slack / CLI 为主 |
|
|
164
|
+
| 锁定程度 | 低,可随时切换引擎 | 中 | 高 |
|
|
165
|
+
| 最强场景 | 远程 coding、长任务、本地自动化 | 全能个人助手 | 原生模型体验、企业集成 |
|
|
155
166
|
|
|
156
167
|
---
|
|
157
168
|
|
|
@@ -159,6 +170,8 @@ npx pikiclaw@latest --setup
|
|
|
159
170
|
|
|
160
171
|
pikiclaw 不限于编程。你的 Agent 能做什么,pikiclaw 就能远程调度什么。
|
|
161
172
|
|
|
173
|
+
它尤其适合那些**必须在你自己的环境里执行**、同时又希望**进度和结果直接回到 IM** 的任务。
|
|
174
|
+
|
|
162
175
|
**工程重构** — "把整个项目从 JS 迁移到 TS,跑测试直到全部通过。搞定告诉我。"
|
|
163
176
|
|
|
164
177
|
**文档处理** — "把 docs/ 下所有零散文档整理汇总,提取核心指标,输出一份报告。"
|
|
@@ -298,7 +311,7 @@ npx pikiclaw@latest --doctor # 检查环境
|
|
|
298
311
|
## Development
|
|
299
312
|
|
|
300
313
|
```bash
|
|
301
|
-
git clone https://github.com/
|
|
314
|
+
git clone https://github.com/xiaotonng/pikiclaw.git
|
|
302
315
|
cd pikiclaw
|
|
303
316
|
npm install
|
|
304
317
|
echo "TELEGRAM_BOT_TOKEN=your_token" > .env
|
|
@@ -176,7 +176,7 @@ export function buildStreamPreviewMarkdown(input) {
|
|
|
176
176
|
const thinkDisplay = formatThinkingForDisplay(input.thinking, maxBody);
|
|
177
177
|
const planDisplay = renderPlanForPreview(input.plan ?? null);
|
|
178
178
|
const activityDisplay = summarizeActivityForPreview(input.activity);
|
|
179
|
-
const maxActivity = !display && !thinkDisplay && !planDisplay ?
|
|
179
|
+
const maxActivity = !display && !thinkDisplay && !planDisplay ? 2400 : 1400;
|
|
180
180
|
const parts = [];
|
|
181
181
|
const label = thinkLabel(input.agent);
|
|
182
182
|
if (planDisplay) {
|
|
@@ -189,8 +189,10 @@ export function buildStreamPreviewMarkdown(input) {
|
|
|
189
189
|
parts.push(`**${label}**\n${thinkDisplay}`);
|
|
190
190
|
}
|
|
191
191
|
else if (display) {
|
|
192
|
-
if (rawThinking)
|
|
193
|
-
|
|
192
|
+
if (rawThinking) {
|
|
193
|
+
const thinkSnippet = formatThinkingForDisplay(input.thinking, 600);
|
|
194
|
+
parts.push(`**${label}**\n${thinkSnippet}`);
|
|
195
|
+
}
|
|
194
196
|
const preview = display.length > maxBody ? '(...truncated)\n' + display.slice(-maxBody) : display;
|
|
195
197
|
parts.push(preview);
|
|
196
198
|
}
|
|
@@ -211,8 +213,8 @@ export function buildFinalReplyRender(agent, result) {
|
|
|
211
213
|
const narrative = summary.narrative.join('\n');
|
|
212
214
|
if (narrative) {
|
|
213
215
|
let display = narrative;
|
|
214
|
-
if (display.length >
|
|
215
|
-
display = '...\n' + display.slice(-
|
|
216
|
+
if (display.length > 1600)
|
|
217
|
+
display = '...\n' + display.slice(-1600);
|
|
216
218
|
activityText = `**Activity**\n${display}\n\n`;
|
|
217
219
|
}
|
|
218
220
|
const commandSummary = formatActivityCommandSummary(summary.completedCommands, summary.activeCommands, summary.failedCommands);
|
|
@@ -221,7 +223,7 @@ export function buildFinalReplyRender(agent, result) {
|
|
|
221
223
|
}
|
|
222
224
|
let thinkingText = '';
|
|
223
225
|
if (result.thinking) {
|
|
224
|
-
thinkingText = `**${thinkLabel(agent)}**\n${formatThinkingForDisplay(result.thinking,
|
|
226
|
+
thinkingText = `**${thinkLabel(agent)}**\n${formatThinkingForDisplay(result.thinking, 1600)}\n\n`;
|
|
225
227
|
}
|
|
226
228
|
let statusText = '';
|
|
227
229
|
if (result.incomplete) {
|
package/dist/bot-feishu.js
CHANGED
|
@@ -437,14 +437,19 @@ export class FeishuBot extends Bot {
|
|
|
437
437
|
livePreview?.update(nextText, nextThinking, nextActivity, meta, plan);
|
|
438
438
|
}, undefined, mcpSendFile);
|
|
439
439
|
await livePreview?.settle();
|
|
440
|
-
// End streaming mode — finalize the card before sending final reply
|
|
441
|
-
if (placeholderId) {
|
|
442
|
-
const summary = result.message.slice(0, 80).replace(/\s+/g, ' ').trim() || 'Response complete.';
|
|
443
|
-
await this.channel.endStreaming(placeholderId, summary);
|
|
444
|
-
}
|
|
445
440
|
this.log(`[handleMessage] done agent=${session.agent} ok=${result.ok} elapsed=${result.elapsedS.toFixed(1)}s ` +
|
|
446
441
|
`tokens=in:${fmtTokens(result.inputTokens)}/out:${fmtTokens(result.outputTokens)}`);
|
|
447
|
-
|
|
442
|
+
// For streaming cards: push final content via CardKit before ending stream,
|
|
443
|
+
// then send any overflow chunks as new messages.
|
|
444
|
+
// Regular cards: edit placeholder with final content as before.
|
|
445
|
+
const wasStreaming = placeholderId && this.channel.isStreamingCard(placeholderId);
|
|
446
|
+
let finalReplyIds;
|
|
447
|
+
if (wasStreaming) {
|
|
448
|
+
finalReplyIds = await this.finalizeStreamingCard(ctx, placeholderId, session.agent, result);
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
finalReplyIds = await this.sendFinalReply(ctx, placeholderId, session.agent, result);
|
|
452
|
+
}
|
|
448
453
|
this.registerSessionMessages(ctx.chatId, finalReplyIds, session);
|
|
449
454
|
this.log(`[handleMessage] final reply sent to chat=${ctx.chatId}`);
|
|
450
455
|
}
|
|
@@ -455,6 +460,10 @@ export class FeishuBot extends Bot {
|
|
|
455
460
|
if (placeholderId) {
|
|
456
461
|
try {
|
|
457
462
|
await this.channel.editMessage(ctx.chatId, placeholderId, errorText);
|
|
463
|
+
// End streaming if this was a streaming card
|
|
464
|
+
if (this.channel.isStreamingCard(placeholderId)) {
|
|
465
|
+
await this.channel.endStreaming(placeholderId, 'Error');
|
|
466
|
+
}
|
|
458
467
|
}
|
|
459
468
|
catch {
|
|
460
469
|
await this.channel.send(ctx.chatId, errorText).catch(() => null);
|
|
@@ -474,6 +483,51 @@ export class FeishuBot extends Bot {
|
|
|
474
483
|
this.finishTask(taskId);
|
|
475
484
|
});
|
|
476
485
|
}
|
|
486
|
+
/**
|
|
487
|
+
* Finalize a streaming card: push final content via CardKit, end streaming,
|
|
488
|
+
* then send any overflow chunks as new messages.
|
|
489
|
+
* This avoids the "schemaV2 card can not change schemaV1" error that occurs
|
|
490
|
+
* when trying to PATCH a CardKit v2 card with a v1 interactive card.
|
|
491
|
+
*/
|
|
492
|
+
async finalizeStreamingCard(ctx, placeholderId, agent, result) {
|
|
493
|
+
const rendered = buildFinalReplyRender(agent, result);
|
|
494
|
+
const messageIds = [placeholderId];
|
|
495
|
+
const MAX_CARD = 25_000;
|
|
496
|
+
let firstText;
|
|
497
|
+
let remaining = '';
|
|
498
|
+
if (rendered.fullText.length <= MAX_CARD) {
|
|
499
|
+
firstText = rendered.fullText;
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
const maxFirst = MAX_CARD - rendered.headerText.length - rendered.footerText.length;
|
|
503
|
+
if (maxFirst > 200) {
|
|
504
|
+
let cut = rendered.bodyText.lastIndexOf('\n', maxFirst);
|
|
505
|
+
if (cut < maxFirst * 0.3)
|
|
506
|
+
cut = maxFirst;
|
|
507
|
+
firstText = `${rendered.headerText}${rendered.bodyText.slice(0, cut)}${rendered.footerText}`;
|
|
508
|
+
remaining = rendered.bodyText.slice(cut);
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
firstText = `${rendered.headerText}${rendered.footerText}`;
|
|
512
|
+
remaining = rendered.bodyText;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
// Push final content while card is still in streaming mode (CardKit v2 API)
|
|
516
|
+
await this.channel.editMessage(ctx.chatId, placeholderId, firstText);
|
|
517
|
+
// Finalize the streaming card
|
|
518
|
+
const summary = result.message.slice(0, 80).replace(/\s+/g, ' ').trim() || 'Response complete.';
|
|
519
|
+
await this.channel.endStreaming(placeholderId, summary);
|
|
520
|
+
// Send overflow chunks as new messages
|
|
521
|
+
if (remaining.trim()) {
|
|
522
|
+
const chunks = splitText(remaining, MAX_CARD);
|
|
523
|
+
for (const chunk of chunks) {
|
|
524
|
+
const sent = await this.channel.send(ctx.chatId, chunk);
|
|
525
|
+
if (sent)
|
|
526
|
+
messageIds.push(sent);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return messageIds;
|
|
530
|
+
}
|
|
477
531
|
async sendFinalReply(ctx, placeholderId, agent, result) {
|
|
478
532
|
const rendered = buildFinalReplyRender(agent, result);
|
|
479
533
|
const messageIds = [];
|
|
@@ -542,6 +596,7 @@ export class FeishuBot extends Bot {
|
|
|
542
596
|
try {
|
|
543
597
|
await this.channel.sendFile(ctx.chatId, filePath, {
|
|
544
598
|
caption: opts?.caption,
|
|
599
|
+
replyTo: ctx.messageId,
|
|
545
600
|
asPhoto: opts?.kind === 'photo',
|
|
546
601
|
});
|
|
547
602
|
return { ok: true };
|
|
@@ -298,7 +298,7 @@ export function buildStreamPreviewHtml(input) {
|
|
|
298
298
|
const thinkDisplay = formatThinkingForDisplay(input.thinking, maxBody);
|
|
299
299
|
const planDisplay = renderPlanForPreview(input.plan ?? null);
|
|
300
300
|
const activityDisplay = summarizeActivityForPreview(input.activity);
|
|
301
|
-
const maxActivity = !display && !thinkDisplay && !planDisplay ?
|
|
301
|
+
const maxActivity = !display && !thinkDisplay && !planDisplay ? 2400 : 1400;
|
|
302
302
|
const parts = [];
|
|
303
303
|
const label = thinkLabel(input.agent);
|
|
304
304
|
if (planDisplay) {
|
|
@@ -311,8 +311,10 @@ export function buildStreamPreviewHtml(input) {
|
|
|
311
311
|
parts.push(`<blockquote><b>${escapeHtml(label)}</b>\n${escapeHtml(thinkDisplay)}</blockquote>`);
|
|
312
312
|
}
|
|
313
313
|
else if (display) {
|
|
314
|
-
if (rawThinking)
|
|
315
|
-
|
|
314
|
+
if (rawThinking) {
|
|
315
|
+
const thinkSnippet = formatThinkingForDisplay(input.thinking, 600);
|
|
316
|
+
parts.push(`<blockquote><b>${escapeHtml(label)}</b>\n${escapeHtml(thinkSnippet)}</blockquote>`);
|
|
317
|
+
}
|
|
316
318
|
const preview = display.length > maxBody ? '(...truncated)\n' + display.slice(-maxBody) : display;
|
|
317
319
|
parts.push(mdToTgHtml(preview));
|
|
318
320
|
}
|
|
@@ -329,8 +331,8 @@ export function buildFinalReplyRender(agent, result) {
|
|
|
329
331
|
const narrative = summary.narrative.join('\n');
|
|
330
332
|
if (narrative) {
|
|
331
333
|
let display = narrative;
|
|
332
|
-
if (display.length >
|
|
333
|
-
display = '...\n' + display.slice(-
|
|
334
|
+
if (display.length > 1600)
|
|
335
|
+
display = '...\n' + display.slice(-1600);
|
|
334
336
|
activityHtml = `<blockquote><b>Activity</b>\n${escapeHtml(display)}</blockquote>\n\n`;
|
|
335
337
|
}
|
|
336
338
|
const commandSummary = formatActivityCommandSummary(summary.completedCommands, summary.activeCommands, summary.failedCommands);
|
|
@@ -339,7 +341,7 @@ export function buildFinalReplyRender(agent, result) {
|
|
|
339
341
|
}
|
|
340
342
|
let thinkingHtml = '';
|
|
341
343
|
if (result.thinking) {
|
|
342
|
-
thinkingHtml = `<blockquote><b>${thinkLabel(agent)}</b>\n${escapeHtml(formatThinkingForDisplay(result.thinking,
|
|
344
|
+
thinkingHtml = `<blockquote><b>${thinkLabel(agent)}</b>\n${escapeHtml(formatThinkingForDisplay(result.thinking, 1600))}</blockquote>\n\n`;
|
|
343
345
|
}
|
|
344
346
|
let statusHtml = '';
|
|
345
347
|
if (result.incomplete) {
|
package/dist/bot.js
CHANGED
|
@@ -8,20 +8,20 @@ import fs from 'node:fs';
|
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { execSync, spawn } from 'node:child_process';
|
|
10
10
|
import { getActiveUserConfig, onUserConfigChange, resolveUserWorkdir, setUserWorkdir } from './user-config.js';
|
|
11
|
-
import { doStream, getSessions, getSessionTail, getUsage, initializeProjectSkills, listAgents, listModels, listSkills, } from './code-agent.js';
|
|
11
|
+
import { doStream, getSessions, getSessionTail, getUsage, initializeProjectSkills, listAgents, listModels, listSkills, isPendingSessionId, } from './code-agent.js';
|
|
12
12
|
import { getDriver, hasDriver, allDriverIds } from './agent-driver.js';
|
|
13
13
|
import { terminateProcessTree } from './process-control.js';
|
|
14
|
-
export const VERSION = '0.2.
|
|
14
|
+
export const VERSION = '0.2.37';
|
|
15
15
|
const MACOS_USER_ACTIVITY_PULSE_INTERVAL_MS = 20_000;
|
|
16
16
|
const MACOS_USER_ACTIVITY_PULSE_TIMEOUT_S = 30;
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
18
|
// Helpers
|
|
19
19
|
// ---------------------------------------------------------------------------
|
|
20
20
|
/**
|
|
21
|
-
* If `dir` has a .gitignore,
|
|
22
|
-
*
|
|
21
|
+
* If `dir` has a .gitignore, ignore managed `.pikiclaw` state without hiding
|
|
22
|
+
* `.pikiclaw/skills`, which may be committed as project skills.
|
|
23
23
|
*/
|
|
24
|
-
function ensureGitignore(dir) {
|
|
24
|
+
export function ensureGitignore(dir) {
|
|
25
25
|
try {
|
|
26
26
|
const gi = path.join(dir, '.gitignore');
|
|
27
27
|
if (!fs.existsSync(gi))
|
|
@@ -30,10 +30,12 @@ function ensureGitignore(dir) {
|
|
|
30
30
|
'.pikiclaw/*',
|
|
31
31
|
'!.pikiclaw/skills/',
|
|
32
32
|
'!.pikiclaw/skills/**',
|
|
33
|
+
];
|
|
34
|
+
const legacyLines = new Set([
|
|
35
|
+
'.pikiclaw/',
|
|
33
36
|
'.claude/skills/',
|
|
34
37
|
'.agents/skills/',
|
|
35
|
-
];
|
|
36
|
-
const legacyLines = new Set(['.pikiclaw/']);
|
|
38
|
+
]);
|
|
37
39
|
const rawLines = fs.readFileSync(gi, 'utf8').split(/\r?\n/);
|
|
38
40
|
const normalized = rawLines.filter(line => {
|
|
39
41
|
const trimmed = line.trim();
|
|
@@ -171,25 +173,19 @@ export function thinkLabel(agent) {
|
|
|
171
173
|
return 'Thinking';
|
|
172
174
|
}
|
|
173
175
|
}
|
|
174
|
-
export function extractThinkingTail(text, maxLines =
|
|
176
|
+
export function extractThinkingTail(text, maxLines = 10) {
|
|
175
177
|
const normalized = text.replace(/\r\n?/g, '\n').trim();
|
|
176
178
|
if (!normalized)
|
|
177
179
|
return '';
|
|
178
|
-
const blocks = normalized
|
|
179
|
-
.split(/\n\s*\n+/)
|
|
180
|
-
.map(block => block.trim())
|
|
181
|
-
.filter(Boolean);
|
|
182
|
-
if (blocks.length > 1)
|
|
183
|
-
return blocks[blocks.length - 1];
|
|
184
180
|
const lines = normalized
|
|
185
181
|
.split('\n')
|
|
186
182
|
.map(line => line.trimEnd())
|
|
187
183
|
.filter(line => line.trim());
|
|
188
|
-
if (lines.length >
|
|
189
|
-
return lines.slice(-
|
|
184
|
+
if (lines.length > maxLines)
|
|
185
|
+
return lines.slice(-maxLines).join('\n').trim();
|
|
190
186
|
return normalized;
|
|
191
187
|
}
|
|
192
|
-
export function formatThinkingForDisplay(text, maxChars =
|
|
188
|
+
export function formatThinkingForDisplay(text, maxChars = 1600) {
|
|
193
189
|
let display = extractThinkingTail(text);
|
|
194
190
|
if (display.length > maxChars)
|
|
195
191
|
display = '...\n' + display.slice(-maxChars);
|
|
@@ -200,6 +196,21 @@ export function buildPrompt(text, files) {
|
|
|
200
196
|
return text;
|
|
201
197
|
return `${text || 'Please analyze this.'}\n\n[Files: ${files.map(f => path.basename(f)).join(', ')}]`;
|
|
202
198
|
}
|
|
199
|
+
function appendExtraPrompt(base, extra) {
|
|
200
|
+
const lhs = String(base || '').trim();
|
|
201
|
+
const rhs = String(extra || '').trim();
|
|
202
|
+
if (!lhs)
|
|
203
|
+
return rhs;
|
|
204
|
+
if (!rhs)
|
|
205
|
+
return lhs;
|
|
206
|
+
return `${lhs}\n\n${rhs}`;
|
|
207
|
+
}
|
|
208
|
+
function buildMcpDeliveryPrompt() {
|
|
209
|
+
return [
|
|
210
|
+
'[Artifact Return]',
|
|
211
|
+
'This is an IM conversation, so pay attention to the IM tools.',
|
|
212
|
+
].join('\n');
|
|
213
|
+
}
|
|
203
214
|
function configModelValue(config, agent) {
|
|
204
215
|
switch (agent) {
|
|
205
216
|
case 'claude': return String(config.claudeModel || process.env.CLAUDE_MODEL || 'claude-opus-4-6').trim();
|
|
@@ -791,6 +802,10 @@ export class Bot {
|
|
|
791
802
|
const extraArgs = agentConfig.extraArgs || [];
|
|
792
803
|
this.log(`[runStream] agent=${cs.agent} session=${cs.sessionId || '(new)'} workdir=${this.workdir} timeout=${this.runTimeout}s attachments=${attachments.length}`);
|
|
793
804
|
this.log(`[runStream] ${cs.agent} config: model=${resolvedModel} extraArgs=[${extraArgs.join(' ')}]`);
|
|
805
|
+
const isFirstTurnOfSession = !cs.sessionId || isPendingSessionId(cs.sessionId);
|
|
806
|
+
const effectiveSystemPrompt = isFirstTurnOfSession
|
|
807
|
+
? (mcpSendFile ? appendExtraPrompt(systemPrompt, buildMcpDeliveryPrompt()) : systemPrompt)
|
|
808
|
+
: undefined;
|
|
794
809
|
const opts = {
|
|
795
810
|
agent: cs.agent, prompt, workdir: this.workdir, timeout: this.runTimeout,
|
|
796
811
|
sessionId: cs.sessionId, model: null,
|
|
@@ -799,13 +814,13 @@ export class Bot {
|
|
|
799
814
|
// codex-specific
|
|
800
815
|
codexModel: cs.agent === 'codex' ? resolvedModel : this.codexModel,
|
|
801
816
|
codexFullAccess: this.codexFullAccess,
|
|
802
|
-
codexDeveloperInstructions:
|
|
817
|
+
codexDeveloperInstructions: effectiveSystemPrompt || undefined,
|
|
803
818
|
codexExtraArgs: this.codexExtraArgs.length ? this.codexExtraArgs : undefined,
|
|
804
819
|
codexPrevCumulative: cs.codexCumulative,
|
|
805
820
|
// claude-specific
|
|
806
821
|
claudeModel: cs.agent === 'claude' ? resolvedModel : this.claudeModel,
|
|
807
822
|
claudePermissionMode: this.claudePermissionMode,
|
|
808
|
-
claudeAppendSystemPrompt:
|
|
823
|
+
claudeAppendSystemPrompt: effectiveSystemPrompt || undefined,
|
|
809
824
|
claudeExtraArgs: this.claudeExtraArgs.length ? this.claudeExtraArgs : undefined,
|
|
810
825
|
// gemini-specific
|
|
811
826
|
geminiModel: cs.agent === 'gemini' ? resolvedModel : (this.agentConfigs.gemini?.model || ''),
|
package/dist/channel-feishu.js
CHANGED
|
@@ -15,6 +15,7 @@ import path from 'node:path';
|
|
|
15
15
|
import { Channel, DEFAULT_CHANNEL_CAPABILITIES, sleep, } from './channel-base.js';
|
|
16
16
|
export { FeishuChannel };
|
|
17
17
|
const FEISHU_CARD_MAX = 28_000; // card markdown budget (card JSON limit ~30KB)
|
|
18
|
+
const FILE_MAX_BYTES = 20 * 1024 * 1024; // 20MB max for file send/receive
|
|
18
19
|
const PHOTO_EXTS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif']);
|
|
19
20
|
const FEISHU_WS_START_RETRY_MAX_DELAY_MS = 60_000;
|
|
20
21
|
function describeError(err) {
|
|
@@ -647,7 +648,8 @@ class FeishuChannel extends Channel {
|
|
|
647
648
|
throw new Error('no card_id returned');
|
|
648
649
|
}
|
|
649
650
|
catch (e) {
|
|
650
|
-
|
|
651
|
+
const detail = e?.response?.data ? ` resp=${JSON.stringify(e.response.data)}` : '';
|
|
652
|
+
this._log(`[streaming] CardKit create failed: ${e?.message || e}${detail}, falling back to regular card`);
|
|
651
653
|
return this.send(chatId, initialContent);
|
|
652
654
|
}
|
|
653
655
|
// Step 2: Send card as message (reply to user's message if replyTo is set)
|
|
@@ -679,6 +681,10 @@ class FeishuChannel extends Channel {
|
|
|
679
681
|
return this.send(chatId, initialContent);
|
|
680
682
|
}
|
|
681
683
|
}
|
|
684
|
+
/** Check if a message is currently a streaming card (CardKit v2). */
|
|
685
|
+
isStreamingCard(messageId) {
|
|
686
|
+
return this.cardStates.has(messageId);
|
|
687
|
+
}
|
|
682
688
|
/**
|
|
683
689
|
* End streaming mode on a card and finalize it.
|
|
684
690
|
* After this, `editMessage()` falls through to the regular PATCH path.
|
|
@@ -759,20 +765,21 @@ class FeishuChannel extends Channel {
|
|
|
759
765
|
}
|
|
760
766
|
/** Upload and send a local file. */
|
|
761
767
|
async sendFile(chatId, filePath, opts = {}) {
|
|
768
|
+
const stat = fs.statSync(filePath);
|
|
769
|
+
if (stat.size > FILE_MAX_BYTES) {
|
|
770
|
+
throw new Error(`file too large (${(stat.size / 1024 / 1024).toFixed(1)}MB, max ${FILE_MAX_BYTES / 1024 / 1024}MB)`);
|
|
771
|
+
}
|
|
762
772
|
const content = fs.readFileSync(filePath);
|
|
763
773
|
const filename = path.basename(filePath);
|
|
764
774
|
const isPhoto = opts.asPhoto ?? PHOTO_EXTS.has(path.extname(filename).toLowerCase());
|
|
775
|
+
const replyTo = opts.replyTo ? String(opts.replyTo) : undefined;
|
|
765
776
|
if (isPhoto) {
|
|
766
777
|
try {
|
|
767
778
|
const imageKey = await this.uploadImage(content);
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
data: {
|
|
771
|
-
|
|
772
|
-
msg_type: 'image',
|
|
773
|
-
content: JSON.stringify({ image_key: imageKey }),
|
|
774
|
-
},
|
|
775
|
-
});
|
|
779
|
+
const msgContent = JSON.stringify({ image_key: imageKey });
|
|
780
|
+
const resp = replyTo
|
|
781
|
+
? await this.client.im.message.reply({ path: { message_id: replyTo }, data: { msg_type: 'image', content: msgContent } })
|
|
782
|
+
: await this.client.im.message.create({ params: { receive_id_type: 'chat_id' }, data: { receive_id: String(chatId), msg_type: 'image', content: msgContent } });
|
|
776
783
|
return resp?.data?.message_id ?? null;
|
|
777
784
|
}
|
|
778
785
|
catch (err) {
|
|
@@ -782,14 +789,10 @@ class FeishuChannel extends Channel {
|
|
|
782
789
|
}
|
|
783
790
|
}
|
|
784
791
|
const fileKey = await this.uploadFile(content, filename);
|
|
785
|
-
const
|
|
786
|
-
|
|
787
|
-
data: {
|
|
788
|
-
|
|
789
|
-
msg_type: 'file',
|
|
790
|
-
content: JSON.stringify({ file_key: fileKey }),
|
|
791
|
-
},
|
|
792
|
-
});
|
|
792
|
+
const msgContent = JSON.stringify({ file_key: fileKey });
|
|
793
|
+
const resp = replyTo
|
|
794
|
+
? await this.client.im.message.reply({ path: { message_id: replyTo }, data: { msg_type: 'file', content: msgContent } })
|
|
795
|
+
: await this.client.im.message.create({ params: { receive_id_type: 'chat_id' }, data: { receive_id: String(chatId), msg_type: 'file', content: msgContent } });
|
|
793
796
|
return resp?.data?.message_id ?? null;
|
|
794
797
|
}
|
|
795
798
|
// ========================================================================
|
|
@@ -805,6 +808,12 @@ class FeishuChannel extends Channel {
|
|
|
805
808
|
const localPath = path.join(this.workdir, `_feishu_${name}`);
|
|
806
809
|
fs.mkdirSync(this.workdir, { recursive: true });
|
|
807
810
|
await resp.writeFile(localPath);
|
|
811
|
+
// Check downloaded file size
|
|
812
|
+
const stat = fs.statSync(localPath);
|
|
813
|
+
if (stat.size > FILE_MAX_BYTES) {
|
|
814
|
+
fs.rmSync(localPath, { force: true });
|
|
815
|
+
throw new Error(`file too large (${(stat.size / 1024 / 1024).toFixed(1)}MB, max ${FILE_MAX_BYTES / 1024 / 1024}MB)`);
|
|
816
|
+
}
|
|
808
817
|
return localPath;
|
|
809
818
|
}
|
|
810
819
|
// ========================================================================
|
package/dist/channel-telegram.js
CHANGED
|
@@ -63,6 +63,7 @@ import { Channel, DEFAULT_CHANNEL_CAPABILITIES, splitText, sleep, } from './chan
|
|
|
63
63
|
setGlobalDispatcher(new EnvHttpProxyAgent());
|
|
64
64
|
export { TelegramChannel };
|
|
65
65
|
const TG_MAX = 4096;
|
|
66
|
+
const FILE_MAX_BYTES = 20 * 1024 * 1024; // 20MB max for file send/receive
|
|
66
67
|
const PHOTO_EXTS = new Set(['.jpg', '.jpeg', '.png', '.webp']);
|
|
67
68
|
function previewText(value, max = 280) {
|
|
68
69
|
const normalized = value.replace(/\s+/g, ' ').trim();
|
|
@@ -489,6 +490,10 @@ class TelegramChannel extends Channel {
|
|
|
489
490
|
return data?.result?.message_id ?? null;
|
|
490
491
|
}
|
|
491
492
|
async sendFile(chatId, filePath, opts = {}) {
|
|
493
|
+
const stat = fs.statSync(filePath);
|
|
494
|
+
if (stat.size > FILE_MAX_BYTES) {
|
|
495
|
+
throw new Error(`file too large (${(stat.size / 1024 / 1024).toFixed(1)}MB, max ${FILE_MAX_BYTES / 1024 / 1024}MB)`);
|
|
496
|
+
}
|
|
492
497
|
const content = fs.readFileSync(filePath);
|
|
493
498
|
const filename = path.basename(filePath);
|
|
494
499
|
const wantsPhoto = opts.asPhoto ?? PHOTO_EXTS.has(path.extname(filename).toLowerCase());
|
|
@@ -585,6 +590,12 @@ class TelegramChannel extends Channel {
|
|
|
585
590
|
const buf = Buffer.from(await resp.arrayBuffer());
|
|
586
591
|
fs.writeFileSync(localPath, buf);
|
|
587
592
|
}
|
|
593
|
+
// Check downloaded file size
|
|
594
|
+
const stat = fs.statSync(localPath);
|
|
595
|
+
if (stat.size > FILE_MAX_BYTES) {
|
|
596
|
+
fs.rmSync(localPath, { force: true });
|
|
597
|
+
throw new Error(`file too large (${(stat.size / 1024 / 1024).toFixed(1)}MB, max ${FILE_MAX_BYTES / 1024 / 1024}MB)`);
|
|
598
|
+
}
|
|
588
599
|
return localPath;
|
|
589
600
|
}
|
|
590
601
|
// ========================================================================
|