autosnippet 3.2.6 → 3.2.7
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 +16 -1
- package/lib/external/mcp/handlers/task.js +182 -10
- package/lib/http/HttpServer.js +4 -0
- package/lib/http/routes/remote.js +1138 -0
- package/lib/http/routes/task.js +1 -0
- package/lib/infrastructure/database/migrations/003_add_remote_commands.js +27 -0
- package/package.json +12 -1
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ That's it. After you approve some candidates, they become **Recipes** — struct
|
|
|
45
45
|
|
|
46
46
|
```
|
|
47
47
|
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
|
|
48
|
-
│ ① Setup │──→ │ ② Cold │──→ │ ③ Target │──→ │ ④
|
|
48
|
+
│ ① Setup │──→ │ ② Cold │──→ │ ③ Target │──→ │ ④ Review │──→ │ ⑤ IDE │
|
|
49
49
|
│ asd setup │ │ Start │ │ Scan │ │ Dashboard │ │ Delivery │
|
|
50
50
|
└────────────┘ └────────────┘ └────────────┘ └────────────┘ └─────┬──────┘
|
|
51
51
|
│
|
|
@@ -115,6 +115,7 @@ Access via CLI `asd task`, MCP tool `autosnippet_task`, or `#asd` in VS Code Age
|
|
|
115
115
|
| **Claude Code** | MCP + CLAUDE.md | `CLAUDE.md` + MCP tools; supports hooks |
|
|
116
116
|
| **Trae / Qoder** | MCP | Auto-generated by `asd setup` |
|
|
117
117
|
| **Xcode** | File watcher | `asd watch` + file directives + snippet sync |
|
|
118
|
+
| **Lark (Feishu)** | Bot + WebSocket | Send commands from phone → IDE executes via Copilot Agent Mode |
|
|
118
119
|
|
|
119
120
|
All configs generated by `asd setup`. Run `asd upgrade` to refresh after updates.
|
|
120
121
|
|
|
@@ -167,6 +168,20 @@ your-project/
|
|
|
167
168
|
|
|
168
169
|
Recipes are Markdown files. SQLite is a read cache. If the DB breaks, `asd sync` rebuilds it.
|
|
169
170
|
|
|
171
|
+
## Remote Programming via Lark
|
|
172
|
+
|
|
173
|
+
Code from your phone. Send messages in Lark (Feishu) → they get injected into VS Code Copilot Agent Mode → results sent back to Lark. Task notifications with IDE screenshots are pushed back automatically.
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
Phone (Lark) → Feishu Cloud (WSS) → Local API Server → VS Code → Copilot Agent Mode
|
|
177
|
+
↑ |
|
|
178
|
+
└─────────────────────── Result notification + Screenshot ────────────────┘
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
System commands: `/help` `/status` `/queue` `/cancel` `/clear` `/ping` `/screen`
|
|
182
|
+
|
|
183
|
+
See [Lark Integration Guide](docs/lark-integration.en.md) for setup instructions.
|
|
184
|
+
|
|
170
185
|
## Configuration
|
|
171
186
|
|
|
172
187
|
Put a `.env` in your project root, or use Dashboard → LLM Config:
|
|
@@ -19,27 +19,35 @@ import { envelope } from '../envelope.js';
|
|
|
19
19
|
export async function taskHandler(ctx, args) {
|
|
20
20
|
const taskService = ctx.container.get('taskGraphService');
|
|
21
21
|
|
|
22
|
+
let result;
|
|
22
23
|
switch (args.operation) {
|
|
23
24
|
// ── Session ──
|
|
24
25
|
case 'prime':
|
|
25
26
|
return _prime(taskService, args);
|
|
26
27
|
// ── Task CRUD ──
|
|
27
28
|
case 'create':
|
|
28
|
-
|
|
29
|
+
result = await _create(taskService, args);
|
|
30
|
+
break;
|
|
29
31
|
case 'ready':
|
|
30
32
|
return _ready(taskService, args);
|
|
31
33
|
case 'claim':
|
|
32
|
-
|
|
34
|
+
result = await _claim(taskService, args);
|
|
35
|
+
break;
|
|
33
36
|
case 'close':
|
|
34
|
-
|
|
37
|
+
result = await _close(ctx, taskService, args);
|
|
38
|
+
break;
|
|
35
39
|
case 'fail':
|
|
36
|
-
|
|
40
|
+
result = await _fail(taskService, args);
|
|
41
|
+
break;
|
|
37
42
|
case 'defer':
|
|
38
|
-
|
|
43
|
+
result = await _defer(taskService, args);
|
|
44
|
+
break;
|
|
39
45
|
case 'progress':
|
|
40
|
-
|
|
46
|
+
result = await _progress(taskService, args);
|
|
47
|
+
break;
|
|
41
48
|
case 'decompose':
|
|
42
|
-
|
|
49
|
+
result = await _decompose(taskService, args);
|
|
50
|
+
break;
|
|
43
51
|
case 'show':
|
|
44
52
|
return _show(taskService, args);
|
|
45
53
|
case 'list':
|
|
@@ -54,11 +62,14 @@ export async function taskHandler(ctx, args) {
|
|
|
54
62
|
return _stats(taskService);
|
|
55
63
|
// ── Decisions ──
|
|
56
64
|
case 'record_decision':
|
|
57
|
-
|
|
65
|
+
result = await _recordDecision(taskService, args);
|
|
66
|
+
break;
|
|
58
67
|
case 'revise_decision':
|
|
59
|
-
|
|
68
|
+
result = await _reviseDecision(taskService, args);
|
|
69
|
+
break;
|
|
60
70
|
case 'unpin_decision':
|
|
61
|
-
|
|
71
|
+
result = await _unpinDecision(taskService, args);
|
|
72
|
+
break;
|
|
62
73
|
case 'list_decisions':
|
|
63
74
|
return _listDecisions(taskService);
|
|
64
75
|
default:
|
|
@@ -68,6 +79,13 @@ export async function taskHandler(ctx, args) {
|
|
|
68
79
|
meta: { tool: 'autosnippet_task' },
|
|
69
80
|
});
|
|
70
81
|
}
|
|
82
|
+
|
|
83
|
+
// ── 飞书任务进度通知(异步非阻塞)──
|
|
84
|
+
_notifyTaskProgress(args.operation, args, result).catch((err) => {
|
|
85
|
+
process.stderr.write(`[MCP/Task] Notify error: ${err?.message}\n`);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return result;
|
|
71
89
|
}
|
|
72
90
|
|
|
73
91
|
// ── create ──
|
|
@@ -455,3 +473,157 @@ async function _listDecisions(svc) {
|
|
|
455
473
|
meta: { tool: 'autosnippet_task' },
|
|
456
474
|
});
|
|
457
475
|
}
|
|
476
|
+
|
|
477
|
+
// ═══ 飞书任务进度通知(通过 API Server 中转)═══════════════
|
|
478
|
+
//
|
|
479
|
+
// MCP Server 与 API Server 是独立进程。
|
|
480
|
+
// 飞书 WSClient 连接在 API Server 中,因此通知需 HTTP 中转。
|
|
481
|
+
// ═══════════════════════════════════════════════════════════
|
|
482
|
+
|
|
483
|
+
const PRIORITY_LABELS = ['P0 紧急', 'P1 高', 'P2 中', 'P3 低', 'P4 微'];
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* 通过 API Server 的 /api/v1/remote/notify 发送飞书通知
|
|
487
|
+
* @param {string} text
|
|
488
|
+
* @returns {Promise<boolean>}
|
|
489
|
+
*/
|
|
490
|
+
async function _sendLarkViaApi(text) {
|
|
491
|
+
try {
|
|
492
|
+
const port = process.env.PORT || 3000;
|
|
493
|
+
const resp = await fetch(`http://localhost:${port}/api/v1/remote/notify`, {
|
|
494
|
+
method: 'POST',
|
|
495
|
+
headers: { 'Content-Type': 'application/json' },
|
|
496
|
+
body: JSON.stringify({ text }),
|
|
497
|
+
signal: AbortSignal.timeout(5000),
|
|
498
|
+
});
|
|
499
|
+
if (!resp.ok) {
|
|
500
|
+
process.stderr.write(`[MCP/Task] Lark notify HTTP ${resp.status}\n`);
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
const body = await resp.json();
|
|
504
|
+
return body.success === true;
|
|
505
|
+
} catch (err) {
|
|
506
|
+
process.stderr.write(`[MCP/Task] Lark notify failed: ${err?.message}\n`);
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* 通过 API Server 截取 IDE 窗口截图并发送到飞书
|
|
513
|
+
* @param {string} [caption] — 可选文字说明
|
|
514
|
+
* @returns {Promise<boolean>}
|
|
515
|
+
*/
|
|
516
|
+
async function _sendScreenshotViaApi(caption = '') {
|
|
517
|
+
try {
|
|
518
|
+
const port = process.env.PORT || 3000;
|
|
519
|
+
const resp = await fetch(`http://localhost:${port}/api/v1/remote/screenshot`, {
|
|
520
|
+
method: 'POST',
|
|
521
|
+
headers: { 'Content-Type': 'application/json' },
|
|
522
|
+
body: JSON.stringify({ caption }),
|
|
523
|
+
signal: AbortSignal.timeout(15000),
|
|
524
|
+
});
|
|
525
|
+
if (!resp.ok) {
|
|
526
|
+
process.stderr.write(`[MCP/Task] Screenshot HTTP ${resp.status}\n`);
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
const body = await resp.json();
|
|
530
|
+
return body.success === true;
|
|
531
|
+
} catch (err) {
|
|
532
|
+
process.stderr.write(`[MCP/Task] Screenshot failed: ${err?.message}\n`);
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* 根据任务操作向飞书发送进度通知(异步非阻塞)
|
|
539
|
+
* result 是 envelope() 返回的 { success, data, message, meta }
|
|
540
|
+
*/
|
|
541
|
+
async function _notifyTaskProgress(operation, args, result) {
|
|
542
|
+
if (!result || result.success === false) return;
|
|
543
|
+
|
|
544
|
+
const data = result.data;
|
|
545
|
+
let text = '';
|
|
546
|
+
|
|
547
|
+
switch (operation) {
|
|
548
|
+
case 'create': {
|
|
549
|
+
const title = data?.title || args.title || '';
|
|
550
|
+
const id = data?.id || '';
|
|
551
|
+
const type = data?.taskType || args.taskType || 'task';
|
|
552
|
+
const pri = PRIORITY_LABELS[data?.priority ?? args.priority ?? 2] || 'P2';
|
|
553
|
+
const dup = result.message?.includes('Duplicate') || result.message?.startsWith('⚠') ? ' (重复)' : '';
|
|
554
|
+
text = `📋 新任务${dup}: ${id}\n${title}\n类型: ${type} | ${pri}`;
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
case 'claim': {
|
|
558
|
+
const id = data?.id || args.id;
|
|
559
|
+
const title = data?.title || '';
|
|
560
|
+
text = `🔨 开始执行: ${id}\n${title}`;
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
case 'close': {
|
|
564
|
+
const closed = data?.closed || data;
|
|
565
|
+
const title = closed?.title || '';
|
|
566
|
+
const id = closed?.id || args.id;
|
|
567
|
+
const reason = closed?.closeReason || args.reason || '';
|
|
568
|
+
const readyCount = data?.newlyReady?.length || 0;
|
|
569
|
+
const readyInfo = readyCount > 0 ? `\n→ ${readyCount} 个任务新就绪` : '';
|
|
570
|
+
text = `✅ 完成: ${id}\n${title}\n原因: ${reason}${readyInfo}`;
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
case 'fail': {
|
|
574
|
+
const title = data?.title || '';
|
|
575
|
+
const id = data?.id || args.id;
|
|
576
|
+
const reason = data?.lastFailReason || args.reason || '未知';
|
|
577
|
+
const count = data?.failCount || 0;
|
|
578
|
+
text = `❌ 失败: ${id}\n${title}\n原因: ${reason}${count > 1 ? ` (第${count}次)` : ''}`;
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
case 'defer': {
|
|
582
|
+
const id = data?.id || args.id;
|
|
583
|
+
const title = data?.title || '';
|
|
584
|
+
text = `⏸️ 暂缓: ${id} — ${title}`;
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
case 'progress': {
|
|
588
|
+
const id = data?.id || args.id;
|
|
589
|
+
const note = args.reason || args.description || '';
|
|
590
|
+
text = note
|
|
591
|
+
? `📝 进度: ${id}\n${note.slice(0, 200)}`
|
|
592
|
+
: `📝 进度: ${id}`;
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
case 'decompose': {
|
|
596
|
+
const epicId = args.id;
|
|
597
|
+
const count = Array.isArray(data) ? data.length : 0;
|
|
598
|
+
const subTitles = Array.isArray(data)
|
|
599
|
+
? data.slice(0, 5).map((t, i) => ` ${i + 1}. ${t.title || t.id}`).join('\n')
|
|
600
|
+
: '';
|
|
601
|
+
text = `📂 拆解: ${epicId} → ${count} 个子任务${subTitles ? '\n' + subTitles : ''}`;
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
case 'record_decision': {
|
|
605
|
+
const title = data?.title || args.title || '';
|
|
606
|
+
text = `📌 决策: ${title}`;
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
case 'revise_decision': {
|
|
610
|
+
const oldId = data?.superseded || args.id;
|
|
611
|
+
const newTitle = data?.newDecision?.title || args.title;
|
|
612
|
+
text = `🔄 决策更新: ${oldId} → ${newTitle}`;
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
case 'unpin_decision': {
|
|
616
|
+
const id = data?.id || args.id;
|
|
617
|
+
text = `🔓 决策取消: ${id}`;
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
default:
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (text) {
|
|
625
|
+
await _sendLarkViaApi(text);
|
|
626
|
+
// 发送文字通知后,附带 IDE 窗口截图
|
|
627
|
+
await _sendScreenshotViaApi();
|
|
628
|
+
}
|
|
629
|
+
}
|
package/lib/http/HttpServer.js
CHANGED
|
@@ -38,6 +38,7 @@ import skillsRouter from './routes/skills.js';
|
|
|
38
38
|
import snippetRouter from './routes/snippets.js';
|
|
39
39
|
import spmRouter from './routes/spm.js';
|
|
40
40
|
import taskRouter from './routes/task.js';
|
|
41
|
+
import remoteRouter from './routes/remote.js';
|
|
41
42
|
import violationsRouter from './routes/violations.js';
|
|
42
43
|
import wikiRouter from './routes/wiki.js';
|
|
43
44
|
|
|
@@ -302,6 +303,9 @@ export class HttpServer {
|
|
|
302
303
|
// Wiki 路由
|
|
303
304
|
this.app.use(`${apiPrefix}/wiki`, wikiRouter);
|
|
304
305
|
|
|
306
|
+
// Remote 路由(飞书 Bot → IDE 远程指令桥接)
|
|
307
|
+
this.app.use(`${apiPrefix}/remote`, remoteRouter);
|
|
308
|
+
|
|
305
309
|
// 根路径 — 返回 API 元信息(避免外部探测产生无意义 404)
|
|
306
310
|
this.app.all('/', (req, res) => {
|
|
307
311
|
res.json({
|