feihong-code 0.6.0 → 7.0.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/CHANGELOG.md +46 -446
- package/dist/agent/orchestrator.js +33 -6
- package/dist/agent/orchestrator.js.map +1 -1
- package/dist/agent/parallel-orchestrator.js +6 -3
- package/dist/agent/parallel-orchestrator.js.map +1 -1
- package/dist/agent/prompts.js +46 -16
- package/dist/agent/prompts.js.map +1 -1
- package/dist/agent/self-heal.js +2 -2
- package/dist/agent/self-heal.js.map +1 -1
- package/dist/cli/repl.js +10 -9
- package/dist/cli/repl.js.map +1 -1
- package/dist/cli/run.js +26 -23
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/version.js +1 -1
- package/dist/shared/concurrency.js +80 -0
- package/dist/shared/concurrency.js.map +1 -0
- package/dist/web/public/css/style.css +132 -7
- package/dist/web/public/index.html +193 -9
- package/dist/web/public/js/api.js +76 -3
- package/dist/web/public/js/app.js +858 -120
- package/dist/web/public/js/ui.js +367 -140
- package/dist/web/public/js/utils.js +44 -0
- package/dist/web/server.js +897 -53
- package/dist/web/server.js.map +1 -1
- package/dist/web/task-queue.js +14 -6
- package/dist/web/task-queue.js.map +1 -1
- package/docs/BENCHMARK_REPORT_en.md +329 -0
- package/docs/BENCHMARK_REPORT_zh.md +329 -0
- package/docs/TECHNICAL_SPEC_en.md +292 -0
- package/docs/TECHNICAL_SPEC_zh.md +292 -0
- package/docs/UPDATE_NOTES_en.md +159 -0
- package/docs/UPDATE_NOTES_zh.md +159 -0
- package/electron/main.js +404 -0
- package/package.json +62 -3
- package/tool-schema.json +2 -2
package/dist/web/server.js
CHANGED
|
@@ -129,10 +129,58 @@ function startWebServer(opts = {}) {
|
|
|
129
129
|
const content = (0, memory_1.readShortTerm)(memoryConfig, date ? new Date(date) : undefined);
|
|
130
130
|
res.json({ ok: true, content, date: date || new Date().toISOString().split('T')[0] });
|
|
131
131
|
});
|
|
132
|
+
// 手动添加一条短期记忆
|
|
133
|
+
app.post('/api/memory/short', (req, res) => {
|
|
134
|
+
const body = (req.body ?? {});
|
|
135
|
+
const type = ['task', 'fix', 'feature', 'error', 'note'].find((t) => t === body.type) || 'note';
|
|
136
|
+
const title = typeof body.title === 'string' && body.title.trim() ? body.title.trim() : '';
|
|
137
|
+
const content = typeof body.content === 'string' && body.content.trim() ? body.content.trim() : '';
|
|
138
|
+
if (!title || !content) {
|
|
139
|
+
res.status(400).json({ ok: false, error: '缺少 title 或 content 字段' });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const path = (0, memory_1.appendShortTerm)(memoryConfig, { type, title, content });
|
|
144
|
+
res.json({ ok: true, path, message: '已添加到短期记忆' });
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
res.status(500).json({ ok: false, error: '添加失败: ' + e.message });
|
|
148
|
+
}
|
|
149
|
+
});
|
|
132
150
|
app.get('/api/memory/long', (_req, res) => {
|
|
133
151
|
const content = (0, memory_1.readLongTerm)(memoryConfig);
|
|
134
152
|
res.json({ ok: true, content });
|
|
135
153
|
});
|
|
154
|
+
// 写入/编辑长期记忆(用户自定义需要记忆的内容)
|
|
155
|
+
app.post('/api/memory/long', (req, res) => {
|
|
156
|
+
const body = (req.body ?? {});
|
|
157
|
+
const content = typeof body.content === 'string' ? body.content : '';
|
|
158
|
+
try {
|
|
159
|
+
(0, memory_1.writeLongTerm)(memoryConfig, content);
|
|
160
|
+
res.json({ ok: true, message: '长期记忆已保存' });
|
|
161
|
+
}
|
|
162
|
+
catch (e) {
|
|
163
|
+
res.status(500).json({ ok: false, error: '保存失败: ' + e.message });
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
// 追加一条长期记忆
|
|
167
|
+
app.post('/api/memory/long/append', (req, res) => {
|
|
168
|
+
const body = (req.body ?? {});
|
|
169
|
+
const category = typeof body.category === 'string' && body.category.trim() ? body.category.trim() : '自定义';
|
|
170
|
+
const title = typeof body.title === 'string' && body.title.trim() ? body.title.trim() : '';
|
|
171
|
+
const content = typeof body.content === 'string' && body.content.trim() ? body.content.trim() : '';
|
|
172
|
+
if (!title || !content) {
|
|
173
|
+
res.status(400).json({ ok: false, error: '缺少 title 或 content 字段' });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
const id = (0, memory_1.appendLongTerm)(memoryConfig, { category, title, content, summarizedFrom: '手动添加' });
|
|
178
|
+
res.json({ ok: true, id, message: '已追加到长期记忆' });
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
res.status(500).json({ ok: false, error: '追加失败: ' + e.message });
|
|
182
|
+
}
|
|
183
|
+
});
|
|
136
184
|
app.get('/api/memory/stats', (_req, res) => {
|
|
137
185
|
const stats = (0, memory_1.getMemoryStats)(memoryConfig);
|
|
138
186
|
res.json({ ok: true, ...stats });
|
|
@@ -402,6 +450,70 @@ function startWebServer(opts = {}) {
|
|
|
402
450
|
res.status(500).json({ ok: false, error: '读取目录失败: ' + e.message });
|
|
403
451
|
}
|
|
404
452
|
});
|
|
453
|
+
// 新建文件夹
|
|
454
|
+
app.post('/api/workspace/mkdir', (req, res) => {
|
|
455
|
+
const body = (req.body ?? {});
|
|
456
|
+
const parent = typeof body?.parent === 'string' ? body.parent.trim() : '';
|
|
457
|
+
const name = typeof body?.name === 'string' ? body.name.trim() : '';
|
|
458
|
+
if (!parent || !name) {
|
|
459
|
+
res.status(400).json({ ok: false, error: '缺少 parent 或 name 字段' });
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
// 文件夹名安全校验:禁止路径分隔符和特殊字符
|
|
463
|
+
if (/[\\/:*?"<>|]/.test(name)) {
|
|
464
|
+
res.status(400).json({ ok: false, error: '文件夹名包含非法字符' });
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const parentDir = (0, path_1.resolve)(parent);
|
|
468
|
+
if (!assertPathAllowed(parentDir, res))
|
|
469
|
+
return;
|
|
470
|
+
const newDir = (0, path_1.join)(parentDir, name);
|
|
471
|
+
try {
|
|
472
|
+
if ((0, fs_1.existsSync)(newDir)) {
|
|
473
|
+
res.status(409).json({ ok: false, error: '文件夹已存在' });
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
(0, fs_1.mkdirSync)(newDir, { recursive: true });
|
|
477
|
+
res.json({ ok: true, path: newDir });
|
|
478
|
+
}
|
|
479
|
+
catch (e) {
|
|
480
|
+
res.status(500).json({ ok: false, error: '创建文件夹失败: ' + e.message });
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
// 重命名文件夹
|
|
484
|
+
app.post('/api/workspace/rename', (req, res) => {
|
|
485
|
+
const body = (req.body ?? {});
|
|
486
|
+
const path = typeof body?.path === 'string' ? body.path.trim() : '';
|
|
487
|
+
const newName = typeof body?.newName === 'string' ? body.newName.trim() : '';
|
|
488
|
+
if (!path || !newName) {
|
|
489
|
+
res.status(400).json({ ok: false, error: '缺少 path 或 newName 字段' });
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (/[\\/:*?"<>|]/.test(newName)) {
|
|
493
|
+
res.status(400).json({ ok: false, error: '文件夹名包含非法字符' });
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const oldPath = (0, path_1.resolve)(path);
|
|
497
|
+
if (!assertPathAllowed(oldPath, res))
|
|
498
|
+
return;
|
|
499
|
+
if (!(0, fs_1.existsSync)(oldPath) || !(0, fs_1.statSync)(oldPath).isDirectory()) {
|
|
500
|
+
res.status(400).json({ ok: false, error: '目标不是文件夹或不存在' });
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const parentDir = (0, path_1.dirname)(oldPath);
|
|
504
|
+
const newPath = (0, path_1.join)(parentDir, newName);
|
|
505
|
+
try {
|
|
506
|
+
if ((0, fs_1.existsSync)(newPath)) {
|
|
507
|
+
res.status(409).json({ ok: false, error: '同名文件夹已存在' });
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
(0, fs_1.renameSync)(oldPath, newPath);
|
|
511
|
+
res.json({ ok: true, path: newPath });
|
|
512
|
+
}
|
|
513
|
+
catch (e) {
|
|
514
|
+
res.status(500).json({ ok: false, error: '重命名失败: ' + e.message });
|
|
515
|
+
}
|
|
516
|
+
});
|
|
405
517
|
app.post('/api/files/read', (req, res) => {
|
|
406
518
|
const body = (req.body ?? {});
|
|
407
519
|
const file = typeof body?.path === 'string' ? body.path.trim() : '';
|
|
@@ -475,7 +587,514 @@ function startWebServer(opts = {}) {
|
|
|
475
587
|
res.status(500).json({ ok: false, error: '上传失败: ' + e.message });
|
|
476
588
|
}
|
|
477
589
|
});
|
|
478
|
-
/* ==========
|
|
590
|
+
/* ========== 系统截图(调用 Windows 截图工具,不弹浏览器分享框) ========== */
|
|
591
|
+
app.post('/api/screenshot', (_req, res) => {
|
|
592
|
+
try {
|
|
593
|
+
// Windows 10/11 内置截图工具(和 Win+Shift+S 效果一样)
|
|
594
|
+
// 调用后直接进入截图模式,用户截图后图片保存到剪贴板
|
|
595
|
+
if (process.platform === 'win32') {
|
|
596
|
+
(0, child_process_1.exec)('explorer.exe ms-screenclip:', (err) => {
|
|
597
|
+
if (err) {
|
|
598
|
+
res.status(500).json({ ok: false, error: '启动截图工具失败: ' + err.message });
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
res.json({ ok: true, message: '截图工具已启动,截图后按 Ctrl+V 粘贴到输入框' });
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
res.status(400).json({ ok: false, error: '仅支持 Windows 系统' });
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
catch (e) {
|
|
610
|
+
res.status(500).json({ ok: false, error: '启动截图工具失败: ' + e.message });
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
/* ========== 电脑操作(鼠标/键盘/截图,用语言控制电脑) ========== */
|
|
614
|
+
// 执行 PowerShell 命令的辅助函数
|
|
615
|
+
const runPowerShell = (script) => {
|
|
616
|
+
return new Promise((resolve, reject) => {
|
|
617
|
+
const ps = (0, child_process_1.spawn)('powershell.exe', ['-NoProfile', '-Command', script], {
|
|
618
|
+
windowsHide: true,
|
|
619
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
620
|
+
});
|
|
621
|
+
let stdout = '';
|
|
622
|
+
let stderr = '';
|
|
623
|
+
ps.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
624
|
+
ps.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
625
|
+
ps.on('close', (code) => {
|
|
626
|
+
if (code === 0)
|
|
627
|
+
resolve(stdout.trim());
|
|
628
|
+
else
|
|
629
|
+
reject(new Error(stderr || `PowerShell exited with code ${code}`));
|
|
630
|
+
});
|
|
631
|
+
ps.on('error', reject);
|
|
632
|
+
});
|
|
633
|
+
};
|
|
634
|
+
// 截图
|
|
635
|
+
app.post('/api/computer/screenshot', async (_req, res) => {
|
|
636
|
+
try {
|
|
637
|
+
if (process.platform !== 'win32') {
|
|
638
|
+
res.status(400).json({ ok: false, error: '仅支持 Windows 系统' });
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const script = `
|
|
642
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
643
|
+
Add-Type -AssemblyName System.Drawing
|
|
644
|
+
$screen = [System.Windows.Forms.Screen]::PrimaryScreen
|
|
645
|
+
$bounds = $screen.Bounds
|
|
646
|
+
$bitmap = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height
|
|
647
|
+
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
|
|
648
|
+
$graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)
|
|
649
|
+
$ms = New-Object System.IO.MemoryStream
|
|
650
|
+
$bitmap.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
651
|
+
$bytes = $ms.ToArray()
|
|
652
|
+
[Convert]::ToBase64String($bytes)
|
|
653
|
+
`;
|
|
654
|
+
const base64 = await runPowerShell(script);
|
|
655
|
+
res.json({ ok: true, image: 'data:image/png;base64,' + base64, width: 1920, height: 1080 });
|
|
656
|
+
}
|
|
657
|
+
catch (e) {
|
|
658
|
+
res.status(500).json({ ok: false, error: '截图失败: ' + e.message });
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
// 移动鼠标
|
|
662
|
+
app.post('/api/computer/mouse/move', async (req, res) => {
|
|
663
|
+
try {
|
|
664
|
+
const body = (req.body ?? {});
|
|
665
|
+
const x = parseInt(body?.x ?? '0');
|
|
666
|
+
const y = parseInt(body?.y ?? '0');
|
|
667
|
+
if (isNaN(x) || isNaN(y)) {
|
|
668
|
+
res.status(400).json({ ok: false, error: '缺少 x 或 y 坐标' });
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
const script = `
|
|
672
|
+
Add-Type @"
|
|
673
|
+
using System;
|
|
674
|
+
using System.Runtime.InteropServices;
|
|
675
|
+
public class MouseHelper {
|
|
676
|
+
[DllImport("user32.dll")]
|
|
677
|
+
public static extern bool SetCursorPos(int X, int Y);
|
|
678
|
+
}
|
|
679
|
+
"@
|
|
680
|
+
[MouseHelper]::SetCursorPos(${x}, ${y}) | Out-Null
|
|
681
|
+
Write-Output "ok"
|
|
682
|
+
`;
|
|
683
|
+
await runPowerShell(script);
|
|
684
|
+
res.json({ ok: true, x, y });
|
|
685
|
+
}
|
|
686
|
+
catch (e) {
|
|
687
|
+
res.status(500).json({ ok: false, error: '移动鼠标失败: ' + e.message });
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
// 点击鼠标
|
|
691
|
+
app.post('/api/computer/mouse/click', async (req, res) => {
|
|
692
|
+
try {
|
|
693
|
+
const body = (req.body ?? {});
|
|
694
|
+
const button = (body?.button ?? 'left');
|
|
695
|
+
const x = body?.x !== undefined ? parseInt(body.x) : null;
|
|
696
|
+
const y = body?.y !== undefined ? parseInt(body.y) : null;
|
|
697
|
+
const doubleClick = body?.double === true;
|
|
698
|
+
let clickFlag = '0x0002'; // left down
|
|
699
|
+
let upFlag = '0x0004'; // left up
|
|
700
|
+
if (button === 'right') {
|
|
701
|
+
clickFlag = '0x0008';
|
|
702
|
+
upFlag = '0x0010';
|
|
703
|
+
}
|
|
704
|
+
const movePart = (x !== null && y !== null) ? `[MouseHelper]::SetCursorPos(${x}, ${y}) | Out-Null; Start-Sleep -Milliseconds 100;` : '';
|
|
705
|
+
const clickPart = doubleClick
|
|
706
|
+
? `[MouseHelper]::mouse_event(${clickFlag}, 0, 0, 0, 0); [MouseHelper]::mouse_event(${upFlag}, 0, 0, 0, 0); Start-Sleep -Milliseconds 100; [MouseHelper]::mouse_event(${clickFlag}, 0, 0, 0, 0); [MouseHelper]::mouse_event(${upFlag}, 0, 0, 0, 0);`
|
|
707
|
+
: `[MouseHelper]::mouse_event(${clickFlag}, 0, 0, 0, 0); [MouseHelper]::mouse_event(${upFlag}, 0, 0, 0, 0);`;
|
|
708
|
+
const script = `
|
|
709
|
+
Add-Type @"
|
|
710
|
+
using System;
|
|
711
|
+
using System.Runtime.InteropServices;
|
|
712
|
+
public class MouseHelper {
|
|
713
|
+
[DllImport("user32.dll")]
|
|
714
|
+
public static extern bool SetCursorPos(int X, int Y);
|
|
715
|
+
[DllImport("user32.dll")]
|
|
716
|
+
public static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint cButtons, uint dwExtraInfo);
|
|
717
|
+
}
|
|
718
|
+
"@
|
|
719
|
+
${movePart}
|
|
720
|
+
${clickPart}
|
|
721
|
+
Write-Output "ok"
|
|
722
|
+
`;
|
|
723
|
+
await runPowerShell(script);
|
|
724
|
+
res.json({ ok: true, button, x, y, double: doubleClick });
|
|
725
|
+
}
|
|
726
|
+
catch (e) {
|
|
727
|
+
res.status(500).json({ ok: false, error: '点击鼠标失败: ' + e.message });
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
// 输入文字
|
|
731
|
+
app.post('/api/computer/keyboard/type', async (req, res) => {
|
|
732
|
+
try {
|
|
733
|
+
const body = (req.body ?? {});
|
|
734
|
+
const text = (body?.text ?? '');
|
|
735
|
+
if (!text) {
|
|
736
|
+
res.status(400).json({ ok: false, error: '缺少 text 字段' });
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
// SendKeys 需要转义特殊字符
|
|
740
|
+
const escaped = text.replace(/([+^%~(){}])/g, '{$1}');
|
|
741
|
+
const script = `
|
|
742
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
743
|
+
[System.Windows.Forms.SendKeys]::SendWait('${escaped.replace(/'/g, "''")}')
|
|
744
|
+
Write-Output "ok"
|
|
745
|
+
`;
|
|
746
|
+
await runPowerShell(script);
|
|
747
|
+
res.json({ ok: true, text });
|
|
748
|
+
}
|
|
749
|
+
catch (e) {
|
|
750
|
+
res.status(500).json({ ok: false, error: '输入文字失败: ' + e.message });
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
// 按键(快捷键)
|
|
754
|
+
app.post('/api/computer/keyboard/press', async (req, res) => {
|
|
755
|
+
try {
|
|
756
|
+
const body = (req.body ?? {});
|
|
757
|
+
const key = (body?.key ?? '');
|
|
758
|
+
if (!key) {
|
|
759
|
+
res.status(400).json({ ok: false, error: '缺少 key 字段' });
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
const script = `
|
|
763
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
764
|
+
[System.Windows.Forms.SendKeys]::SendWait('${key.replace(/'/g, "''")}')
|
|
765
|
+
Write-Output "ok"
|
|
766
|
+
`;
|
|
767
|
+
await runPowerShell(script);
|
|
768
|
+
res.json({ ok: true, key });
|
|
769
|
+
}
|
|
770
|
+
catch (e) {
|
|
771
|
+
res.status(500).json({ ok: false, error: '按键失败: ' + e.message });
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
const nodesFile = (0, path_1.join)(homeDir, 'nodes.json');
|
|
775
|
+
const sourcesFile = (0, path_1.join)(homeDir, 'sources.json');
|
|
776
|
+
const nodeDataDir = (0, path_1.join)(homeDir, 'node-data');
|
|
777
|
+
function loadNodes() {
|
|
778
|
+
const list = loadJsonFile(nodesFile, []);
|
|
779
|
+
return Array.isArray(list) ? list : [];
|
|
780
|
+
}
|
|
781
|
+
function saveNodes(list) {
|
|
782
|
+
return saveJsonFile(nodesFile, list);
|
|
783
|
+
}
|
|
784
|
+
function loadSources() {
|
|
785
|
+
const list = loadJsonFile(sourcesFile, []);
|
|
786
|
+
return Array.isArray(list) ? list : [];
|
|
787
|
+
}
|
|
788
|
+
function saveSources(list) {
|
|
789
|
+
return saveJsonFile(sourcesFile, list);
|
|
790
|
+
}
|
|
791
|
+
function loadNodeData(nodeId, type, fallback) {
|
|
792
|
+
const file = (0, path_1.join)(nodeDataDir, nodeId, `${type}.json`);
|
|
793
|
+
return loadJsonFile(file, fallback);
|
|
794
|
+
}
|
|
795
|
+
function saveNodeData(nodeId, type, data) {
|
|
796
|
+
const file = (0, path_1.join)(nodeDataDir, nodeId, `${type}.json`);
|
|
797
|
+
return saveJsonFile(file, data);
|
|
798
|
+
}
|
|
799
|
+
// 测试节点连接
|
|
800
|
+
async function testNodeConnection(node) {
|
|
801
|
+
try {
|
|
802
|
+
if (node.type === 'local') {
|
|
803
|
+
const dir = (0, path_1.resolve)(node.url);
|
|
804
|
+
if (!(0, fs_1.existsSync)(dir) || !(0, fs_1.statSync)(dir).isDirectory()) {
|
|
805
|
+
return { ok: false, error: '本地目录不存在' };
|
|
806
|
+
}
|
|
807
|
+
const caps = [];
|
|
808
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(dir, 'templates.json')))
|
|
809
|
+
caps.push('templates');
|
|
810
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(dir, 'skills.json')))
|
|
811
|
+
caps.push('skills');
|
|
812
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(dir, 'office.json')))
|
|
813
|
+
caps.push('office');
|
|
814
|
+
return { ok: true, capabilities: caps.length ? caps : node.capabilities };
|
|
815
|
+
}
|
|
816
|
+
// http / git 类型:请求节点的 /api/health 或 manifest
|
|
817
|
+
const url = node.type === 'http'
|
|
818
|
+
? node.url.replace(/\/$/, '') + '/api/health'
|
|
819
|
+
: node.url;
|
|
820
|
+
const headers = { 'User-Agent': 'fhcode-node/1.0' };
|
|
821
|
+
if (node.apiKey)
|
|
822
|
+
headers['Authorization'] = 'Bearer ' + node.apiKey;
|
|
823
|
+
const r = await fetchWithRetry(url, 10000, 0);
|
|
824
|
+
if (!r.ok)
|
|
825
|
+
return { ok: false, error: `HTTP ${r.status}` };
|
|
826
|
+
if (node.type === 'http') {
|
|
827
|
+
const data = await r.json().catch(() => ({}));
|
|
828
|
+
return { ok: true, capabilities: Array.isArray(data.capabilities) ? data.capabilities : node.capabilities };
|
|
829
|
+
}
|
|
830
|
+
return { ok: true, capabilities: node.capabilities };
|
|
831
|
+
}
|
|
832
|
+
catch (e) {
|
|
833
|
+
return { ok: false, error: e?.message || '连接失败' };
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
// 从节点同步数据
|
|
837
|
+
async function syncNodeData(node) {
|
|
838
|
+
const synced = [];
|
|
839
|
+
try {
|
|
840
|
+
if (node.type === 'local') {
|
|
841
|
+
const dir = (0, path_1.resolve)(node.url);
|
|
842
|
+
for (const cap of node.capabilities) {
|
|
843
|
+
const file = (0, path_1.join)(dir, `${cap}.json`);
|
|
844
|
+
if ((0, fs_1.existsSync)(file)) {
|
|
845
|
+
const data = JSON.parse((0, fs_1.readFileSync)(file, 'utf8'));
|
|
846
|
+
saveNodeData(node.id, cap, data);
|
|
847
|
+
synced.push(cap);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return { ok: true, synced };
|
|
851
|
+
}
|
|
852
|
+
// http 类型
|
|
853
|
+
if (node.type === 'http') {
|
|
854
|
+
const base = node.url.replace(/\/$/, '');
|
|
855
|
+
const headers = { 'User-Agent': 'fhcode-node/1.0' };
|
|
856
|
+
if (node.apiKey)
|
|
857
|
+
headers['Authorization'] = 'Bearer ' + node.apiKey;
|
|
858
|
+
for (const cap of node.capabilities) {
|
|
859
|
+
try {
|
|
860
|
+
const r = await fetch(`${base}/api/node/${cap}`, { headers, signal: AbortSignal.timeout(15000) });
|
|
861
|
+
if (r.ok) {
|
|
862
|
+
const data = await r.json();
|
|
863
|
+
const list = Array.isArray(data) ? data : (data.items || data.list || []);
|
|
864
|
+
saveNodeData(node.id, cap, list);
|
|
865
|
+
synced.push(cap);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
catch { /* skip */ }
|
|
869
|
+
}
|
|
870
|
+
return { ok: true, synced };
|
|
871
|
+
}
|
|
872
|
+
// git 类型:暂不支持自动 clone,标记为需手动同步
|
|
873
|
+
return { ok: false, error: 'Git 节点暂不支持自动同步,请使用本地目录方式', synced };
|
|
874
|
+
}
|
|
875
|
+
catch (e) {
|
|
876
|
+
return { ok: false, error: e?.message || '同步失败', synced };
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
// 节点 CRUD
|
|
880
|
+
app.get('/api/nodes', (_req, res) => {
|
|
881
|
+
const nodes = loadNodes().map((n) => {
|
|
882
|
+
const { apiKey, ...rest } = n;
|
|
883
|
+
return { ...rest, hasApiKey: !!apiKey };
|
|
884
|
+
});
|
|
885
|
+
res.json({ ok: true, nodes });
|
|
886
|
+
});
|
|
887
|
+
app.post('/api/nodes', async (req, res) => {
|
|
888
|
+
const body = (req.body ?? {});
|
|
889
|
+
const name = typeof body.name === 'string' ? body.name.trim() : '';
|
|
890
|
+
const type = ['http', 'git', 'local'].find((t) => t === body.type) || 'http';
|
|
891
|
+
const url = typeof body.url === 'string' ? body.url.trim() : '';
|
|
892
|
+
const apiKey = typeof body.apiKey === 'string' && body.apiKey.trim() ? body.apiKey.trim() : undefined;
|
|
893
|
+
const capabilities = Array.isArray(body.capabilities) ? body.capabilities.filter((c) => ['templates', 'skills', 'office'].includes(c)) : ['templates', 'skills', 'office'];
|
|
894
|
+
if (!name || !url) {
|
|
895
|
+
res.status(400).json({ ok: false, error: '缺少 name 或 url 字段' });
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
const node = {
|
|
899
|
+
id: `node_${Date.now()}_${(0, crypto_1.randomUUID)().slice(0, 4)}`,
|
|
900
|
+
name, type, url, apiKey, capabilities,
|
|
901
|
+
status: 'disconnected',
|
|
902
|
+
enabled: true,
|
|
903
|
+
createdAt: new Date().toISOString(),
|
|
904
|
+
};
|
|
905
|
+
const list = loadNodes();
|
|
906
|
+
list.push(node);
|
|
907
|
+
if (!saveNodes(list)) {
|
|
908
|
+
res.status(500).json({ ok: false, error: '节点保存失败' });
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
// 自动测试连接
|
|
912
|
+
const test = await testNodeConnection(node);
|
|
913
|
+
node.status = test.ok ? 'connected' : 'error';
|
|
914
|
+
node.lastError = test.error;
|
|
915
|
+
if (test.capabilities)
|
|
916
|
+
node.capabilities = test.capabilities;
|
|
917
|
+
saveNodes(list);
|
|
918
|
+
res.status(201).json({ ok: true, node: { ...node, apiKey: undefined, hasApiKey: !!apiKey }, connection: test });
|
|
919
|
+
});
|
|
920
|
+
app.put('/api/nodes/:id', async (req, res) => {
|
|
921
|
+
const list = loadNodes();
|
|
922
|
+
const idx = list.findIndex((n) => n.id === req.params.id);
|
|
923
|
+
if (idx < 0) {
|
|
924
|
+
res.status(404).json({ ok: false, error: '节点不存在' });
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
const body = (req.body ?? {});
|
|
928
|
+
const node = list[idx];
|
|
929
|
+
if (typeof body.name === 'string' && body.name.trim())
|
|
930
|
+
node.name = body.name.trim();
|
|
931
|
+
if (typeof body.url === 'string' && body.url.trim())
|
|
932
|
+
node.url = body.url.trim();
|
|
933
|
+
if (typeof body.type === 'string' && ['http', 'git', 'local'].includes(body.type))
|
|
934
|
+
node.type = body.type;
|
|
935
|
+
if (typeof body.apiKey === 'string')
|
|
936
|
+
node.apiKey = body.apiKey.trim() ? body.apiKey.trim() : undefined;
|
|
937
|
+
if (Array.isArray(body.capabilities))
|
|
938
|
+
node.capabilities = body.capabilities.filter((c) => ['templates', 'skills', 'office'].includes(c));
|
|
939
|
+
if (typeof body.enabled === 'boolean')
|
|
940
|
+
node.enabled = body.enabled;
|
|
941
|
+
if (!saveNodes(list)) {
|
|
942
|
+
res.status(500).json({ ok: false, error: '节点更新失败' });
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
res.json({ ok: true, node: { ...node, apiKey: undefined, hasApiKey: !!node.apiKey } });
|
|
946
|
+
});
|
|
947
|
+
app.delete('/api/nodes/:id', (req, res) => {
|
|
948
|
+
const list = loadNodes();
|
|
949
|
+
const next = list.filter((n) => n.id !== req.params.id);
|
|
950
|
+
if (next.length === list.length) {
|
|
951
|
+
res.status(404).json({ ok: false, error: '节点不存在' });
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
if (!saveNodes(next)) {
|
|
955
|
+
res.status(500).json({ ok: false, error: '节点删除失败' });
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
res.json({ ok: true });
|
|
959
|
+
});
|
|
960
|
+
app.post('/api/nodes/:id/test', async (req, res) => {
|
|
961
|
+
const node = loadNodes().find((n) => n.id === req.params.id);
|
|
962
|
+
if (!node) {
|
|
963
|
+
res.status(404).json({ ok: false, error: '节点不存在' });
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
const test = await testNodeConnection(node);
|
|
967
|
+
// 更新节点状态
|
|
968
|
+
const list = loadNodes();
|
|
969
|
+
const idx = list.findIndex((n) => n.id === node.id);
|
|
970
|
+
if (idx >= 0) {
|
|
971
|
+
list[idx].status = test.ok ? 'connected' : 'error';
|
|
972
|
+
list[idx].lastError = test.error;
|
|
973
|
+
if (test.capabilities)
|
|
974
|
+
list[idx].capabilities = test.capabilities;
|
|
975
|
+
saveNodes(list);
|
|
976
|
+
}
|
|
977
|
+
res.json({ ok: test.ok, error: test.error, capabilities: test.capabilities });
|
|
978
|
+
});
|
|
979
|
+
app.post('/api/nodes/:id/sync', async (req, res) => {
|
|
980
|
+
const node = loadNodes().find((n) => n.id === req.params.id);
|
|
981
|
+
if (!node) {
|
|
982
|
+
res.status(404).json({ ok: false, error: '节点不存在' });
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
const result = await syncNodeData(node);
|
|
986
|
+
const list = loadNodes();
|
|
987
|
+
const idx = list.findIndex((n) => n.id === node.id);
|
|
988
|
+
if (idx >= 0) {
|
|
989
|
+
list[idx].lastSyncAt = new Date().toISOString();
|
|
990
|
+
list[idx].status = result.ok ? 'connected' : 'error';
|
|
991
|
+
list[idx].lastError = result.error;
|
|
992
|
+
saveNodes(list);
|
|
993
|
+
}
|
|
994
|
+
res.json({ ok: result.ok, error: result.error, synced: result.synced });
|
|
995
|
+
});
|
|
996
|
+
// 自定义来源 CRUD
|
|
997
|
+
app.get('/api/sources', (_req, res) => {
|
|
998
|
+
res.json({ ok: true, sources: loadSources() });
|
|
999
|
+
});
|
|
1000
|
+
app.post('/api/sources', (req, res) => {
|
|
1001
|
+
const body = (req.body ?? {});
|
|
1002
|
+
const name = typeof body.name === 'string' ? body.name.trim() : '';
|
|
1003
|
+
const type = ['templates', 'skills', 'office'].find((t) => t === body.type);
|
|
1004
|
+
const url = typeof body.url === 'string' ? body.url.trim() : '';
|
|
1005
|
+
const nodeId = typeof body.nodeId === 'string' && body.nodeId.trim() ? body.nodeId.trim() : undefined;
|
|
1006
|
+
if (!name || !type || !url) {
|
|
1007
|
+
res.status(400).json({ ok: false, error: '缺少 name / type / url 字段' });
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
const source = {
|
|
1011
|
+
id: `src_${Date.now()}_${(0, crypto_1.randomUUID)().slice(0, 4)}`,
|
|
1012
|
+
name, type, url, nodeId,
|
|
1013
|
+
enabled: true,
|
|
1014
|
+
createdAt: new Date().toISOString(),
|
|
1015
|
+
};
|
|
1016
|
+
const list = loadSources();
|
|
1017
|
+
list.push(source);
|
|
1018
|
+
if (!saveSources(list)) {
|
|
1019
|
+
res.status(500).json({ ok: false, error: '来源保存失败' });
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
res.status(201).json({ ok: true, source });
|
|
1023
|
+
});
|
|
1024
|
+
app.put('/api/sources/:id', (req, res) => {
|
|
1025
|
+
const list = loadSources();
|
|
1026
|
+
const idx = list.findIndex((s) => s.id === req.params.id);
|
|
1027
|
+
if (idx < 0) {
|
|
1028
|
+
res.status(404).json({ ok: false, error: '来源不存在' });
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
const body = (req.body ?? {});
|
|
1032
|
+
const source = list[idx];
|
|
1033
|
+
if (typeof body.name === 'string' && body.name.trim())
|
|
1034
|
+
source.name = body.name.trim();
|
|
1035
|
+
if (typeof body.url === 'string' && body.url.trim())
|
|
1036
|
+
source.url = body.url.trim();
|
|
1037
|
+
if (typeof body.enabled === 'boolean')
|
|
1038
|
+
source.enabled = body.enabled;
|
|
1039
|
+
if (typeof body.nodeId === 'string')
|
|
1040
|
+
source.nodeId = body.nodeId.trim() || undefined;
|
|
1041
|
+
if (!saveSources(list)) {
|
|
1042
|
+
res.status(500).json({ ok: false, error: '来源更新失败' });
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
res.json({ ok: true, source });
|
|
1046
|
+
});
|
|
1047
|
+
app.delete('/api/sources/:id', (req, res) => {
|
|
1048
|
+
const list = loadSources();
|
|
1049
|
+
const next = list.filter((s) => s.id !== req.params.id);
|
|
1050
|
+
if (next.length === list.length) {
|
|
1051
|
+
res.status(404).json({ ok: false, error: '来源不存在' });
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
if (!saveSources(next)) {
|
|
1055
|
+
res.status(500).json({ ok: false, error: '来源删除失败' });
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
res.json({ ok: true });
|
|
1059
|
+
});
|
|
1060
|
+
// 从所有启用的节点收集数据
|
|
1061
|
+
function collectNodeTemplates() {
|
|
1062
|
+
const result = [];
|
|
1063
|
+
for (const node of loadNodes()) {
|
|
1064
|
+
if (!node.enabled || !node.capabilities.includes('templates'))
|
|
1065
|
+
continue;
|
|
1066
|
+
const data = loadNodeData(node.id, 'templates', []);
|
|
1067
|
+
if (Array.isArray(data)) {
|
|
1068
|
+
result.push(...data.map((t) => ({ ...t, source: node.name, nodeId: node.id })));
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return result;
|
|
1072
|
+
}
|
|
1073
|
+
function collectNodeSkills() {
|
|
1074
|
+
const result = [];
|
|
1075
|
+
for (const node of loadNodes()) {
|
|
1076
|
+
if (!node.enabled || !node.capabilities.includes('skills'))
|
|
1077
|
+
continue;
|
|
1078
|
+
const data = loadNodeData(node.id, 'skills', []);
|
|
1079
|
+
if (Array.isArray(data)) {
|
|
1080
|
+
result.push(...data.map((s) => ({ ...s, source: node.name, nodeId: node.id })));
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
return result;
|
|
1084
|
+
}
|
|
1085
|
+
function collectNodeOffice() {
|
|
1086
|
+
const result = [];
|
|
1087
|
+
for (const node of loadNodes()) {
|
|
1088
|
+
if (!node.enabled || !node.capabilities.includes('office'))
|
|
1089
|
+
continue;
|
|
1090
|
+
const data = loadNodeData(node.id, 'office', []);
|
|
1091
|
+
if (Array.isArray(data)) {
|
|
1092
|
+
result.push(...data.map((c) => ({ ...c, source: node.name, nodeId: node.id })));
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return result;
|
|
1096
|
+
}
|
|
1097
|
+
/* ========== 技能市场(插件市场):聚合 ClawHub + Agent-Foundry + 自定义节点 + 自定义来源 ========== */
|
|
479
1098
|
const CLAWHUB_API = 'https://clawhub.ai/api/v1/skills';
|
|
480
1099
|
const AGENT_FOUNDRY_CATALOG = 'https://raw.githubusercontent.com/hebertzhu/agent-foundry/main/catalog/skills-catalog.json';
|
|
481
1100
|
async function fetchWithRetry(url, timeoutMs = 15000, retries = 1) {
|
|
@@ -576,6 +1195,58 @@ function startWebServer(opts = {}) {
|
|
|
576
1195
|
results = results.concat(await fetchClawHubSkills(q, limit));
|
|
577
1196
|
if (source === 'agent-foundry' || source === 'all')
|
|
578
1197
|
results = results.concat(await fetchAgentFoundrySkills(q, limit));
|
|
1198
|
+
// 从节点同步的技能
|
|
1199
|
+
if (source === 'node' || source === 'all') {
|
|
1200
|
+
const nodeSkills = collectNodeSkills();
|
|
1201
|
+
results = results.concat(nodeSkills.map((s) => ({
|
|
1202
|
+
id: `node:${s.id}`,
|
|
1203
|
+
name: s.name,
|
|
1204
|
+
description: s.description,
|
|
1205
|
+
source: s.source,
|
|
1206
|
+
author: s.author,
|
|
1207
|
+
category: s.category,
|
|
1208
|
+
tags: s.tags,
|
|
1209
|
+
downloads: s.downloads,
|
|
1210
|
+
installHint: s.installHint,
|
|
1211
|
+
homepage: s.homepage,
|
|
1212
|
+
rawUrl: s.rawUrl,
|
|
1213
|
+
})));
|
|
1214
|
+
}
|
|
1215
|
+
// 从自定义来源拉取技能(HTTP URL 返回 JSON 数组)
|
|
1216
|
+
if (source === 'custom' || source === 'all') {
|
|
1217
|
+
for (const src of loadSources()) {
|
|
1218
|
+
if (!src.enabled || src.type !== 'skills')
|
|
1219
|
+
continue;
|
|
1220
|
+
try {
|
|
1221
|
+
const r = await fetchWithRetry(src.url, 10000, 0);
|
|
1222
|
+
if (r.ok) {
|
|
1223
|
+
const data = await r.json();
|
|
1224
|
+
const list = Array.isArray(data) ? data : (data.items || data.skills || []);
|
|
1225
|
+
results = results.concat(list.map((s) => ({
|
|
1226
|
+
id: `custom:${src.id}:${s.id || s.name}`,
|
|
1227
|
+
name: String(s.name || s.title || ''),
|
|
1228
|
+
description: String(s.description || s.desc || ''),
|
|
1229
|
+
source: src.name,
|
|
1230
|
+
author: s.author,
|
|
1231
|
+
category: String(s.category || 'custom'),
|
|
1232
|
+
tags: Array.isArray(s.tags) ? s.tags : [],
|
|
1233
|
+
downloads: Number(s.downloads || 0),
|
|
1234
|
+
installHint: String(s.installHint || ''),
|
|
1235
|
+
homepage: String(s.homepage || src.url),
|
|
1236
|
+
rawUrl: String(s.rawUrl || src.url),
|
|
1237
|
+
})));
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
catch { /* skip failed sources */ }
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
// 关键词过滤
|
|
1244
|
+
if (q) {
|
|
1245
|
+
const kw = q.toLowerCase();
|
|
1246
|
+
results = results.filter((s) => s.name.toLowerCase().includes(kw) ||
|
|
1247
|
+
s.description.toLowerCase().includes(kw) ||
|
|
1248
|
+
(s.tags || []).some((t) => t.toLowerCase().includes(kw)));
|
|
1249
|
+
}
|
|
579
1250
|
results.sort((a, b) => (b.downloads || 0) - (a.downloads || 0));
|
|
580
1251
|
res.json({ ok: true, total: results.length, skills: results.slice(0, limit) });
|
|
581
1252
|
});
|
|
@@ -611,9 +1282,93 @@ function startWebServer(opts = {}) {
|
|
|
611
1282
|
app.get('/api/skills/installed', (_req, res) => {
|
|
612
1283
|
res.json({ ok: true, skills: loadJsonFile(installedSkillsFile, []) });
|
|
613
1284
|
});
|
|
1285
|
+
// 预置常用快捷指令(用户可一键运行,也可保存为自己的指令后编辑)
|
|
1286
|
+
const BUILTIN_AUTOMATIONS = [
|
|
1287
|
+
{
|
|
1288
|
+
id: 'builtin-code-review',
|
|
1289
|
+
name: '代码审查',
|
|
1290
|
+
icon: '🔍',
|
|
1291
|
+
category: '质量',
|
|
1292
|
+
goal: '请对当前工作区的代码做一次全面审查,重点检查:1) 潜在的 Bug 和逻辑漏洞;2) 代码规范和可读性问题;3) 安全风险;4) 性能瓶颈。按问题严重程度排序,给出具体的修改建议。',
|
|
1293
|
+
builtin: true,
|
|
1294
|
+
},
|
|
1295
|
+
{
|
|
1296
|
+
id: 'builtin-gen-test',
|
|
1297
|
+
name: '生成测试',
|
|
1298
|
+
icon: '🧪',
|
|
1299
|
+
category: '测试',
|
|
1300
|
+
goal: '请为当前项目生成单元测试,优先覆盖核心业务逻辑和边界条件。先分析项目结构和已有测试框架,然后为关键模块编写测试用例,确保测试可以直接运行。',
|
|
1301
|
+
builtin: true,
|
|
1302
|
+
},
|
|
1303
|
+
{
|
|
1304
|
+
id: 'builtin-build-check',
|
|
1305
|
+
name: '构建检查',
|
|
1306
|
+
icon: '🏗️',
|
|
1307
|
+
category: '工程',
|
|
1308
|
+
goal: '请运行项目的构建命令(如 npm run build / tsc),检查是否有编译错误或类型错误。如果有错误,逐一分析原因并给出修复方案,修复后重新验证构建是否通过。',
|
|
1309
|
+
builtin: true,
|
|
1310
|
+
},
|
|
1311
|
+
{
|
|
1312
|
+
id: 'builtin-dep-check',
|
|
1313
|
+
name: '依赖检查',
|
|
1314
|
+
icon: '📦',
|
|
1315
|
+
category: '工程',
|
|
1316
|
+
goal: '请检查当前项目的 package.json 依赖,分析:1) 是否有过时的依赖需要升级;2) 是否有已知安全漏洞的依赖;3) 是否有未使用的冗余依赖。给出升级建议和风险提示。',
|
|
1317
|
+
builtin: true,
|
|
1318
|
+
},
|
|
1319
|
+
{
|
|
1320
|
+
id: 'builtin-format',
|
|
1321
|
+
name: '代码格式化',
|
|
1322
|
+
icon: '✨',
|
|
1323
|
+
category: '工程',
|
|
1324
|
+
goal: '请对当前项目的代码进行全面格式化。先检查项目是否配置了 ESLint / Prettier 等工具,如果有就按配置格式化;如果没有,就按通用规范统一缩进、引号、分号等风格。格式化后验证构建是否正常。',
|
|
1325
|
+
builtin: true,
|
|
1326
|
+
},
|
|
1327
|
+
{
|
|
1328
|
+
id: 'builtin-gen-doc',
|
|
1329
|
+
name: '生成文档',
|
|
1330
|
+
icon: '📝',
|
|
1331
|
+
category: '文档',
|
|
1332
|
+
goal: '请为当前项目生成一份完整的 README 文档,包含:项目简介、功能特性、技术栈、安装步骤、使用方法、目录结构说明、开发指南。如果项目有 API,也一并生成 API 文档。',
|
|
1333
|
+
builtin: true,
|
|
1334
|
+
},
|
|
1335
|
+
{
|
|
1336
|
+
id: 'builtin-refactor',
|
|
1337
|
+
name: '重构优化',
|
|
1338
|
+
icon: '♻️',
|
|
1339
|
+
category: '质量',
|
|
1340
|
+
goal: '请对当前项目的代码进行重构优化,重点关注:1) 重复代码提取;2) 过长函数拆分;3) 命名规范统一;4) 复杂逻辑简化。重构过程中确保不改变外部行为,重构后验证构建和测试通过。',
|
|
1341
|
+
builtin: true,
|
|
1342
|
+
},
|
|
1343
|
+
{
|
|
1344
|
+
id: 'builtin-release',
|
|
1345
|
+
name: '版本发布准备',
|
|
1346
|
+
icon: '🚀',
|
|
1347
|
+
category: '工程',
|
|
1348
|
+
goal: '请为当前项目准备一次版本发布,包括:1) 检查 package.json 版本号并建议升级;2) 整理 CHANGELOG 变更日志;3) 运行构建和测试确保通过;4) 检查是否有未提交的代码。给出完整的发布检查清单。',
|
|
1349
|
+
builtin: true,
|
|
1350
|
+
},
|
|
1351
|
+
{
|
|
1352
|
+
id: 'builtin-security',
|
|
1353
|
+
name: '安全扫描',
|
|
1354
|
+
icon: '🛡️',
|
|
1355
|
+
category: '安全',
|
|
1356
|
+
goal: '请对当前项目的代码进行安全扫描,重点检查:1) SQL 注入、XSS、CSRF 等常见漏洞;2) 硬编码的密钥或敏感信息;3) 不安全的依赖;4) 权限校验缺失。按风险等级排序,给出修复建议。',
|
|
1357
|
+
builtin: true,
|
|
1358
|
+
},
|
|
1359
|
+
{
|
|
1360
|
+
id: 'builtin-perf',
|
|
1361
|
+
name: '性能分析',
|
|
1362
|
+
icon: '⚡',
|
|
1363
|
+
category: '质量',
|
|
1364
|
+
goal: '请分析当前项目的性能瓶颈,重点关注:1) 慢查询和低效算法;2) 不必要的重复计算;3) 内存泄漏风险;4) 异步操作阻塞。给出具体的优化方案和预期收益,优化后验证效果。',
|
|
1365
|
+
builtin: true,
|
|
1366
|
+
},
|
|
1367
|
+
];
|
|
614
1368
|
const automationsFile = (0, path_1.join)(homeDir, 'automations.json');
|
|
615
1369
|
app.get('/api/automations', (_req, res) => {
|
|
616
|
-
|
|
1370
|
+
const user = loadJsonFile(automationsFile, []);
|
|
1371
|
+
res.json({ ok: true, builtin: BUILTIN_AUTOMATIONS, automations: user });
|
|
617
1372
|
});
|
|
618
1373
|
app.post('/api/automations', async (req, res) => {
|
|
619
1374
|
const body = (req.body ?? {});
|
|
@@ -658,7 +1413,16 @@ function startWebServer(opts = {}) {
|
|
|
658
1413
|
});
|
|
659
1414
|
app.post('/api/automations/:id/run', async (req, res) => {
|
|
660
1415
|
const list = loadJsonFile(automationsFile, []);
|
|
661
|
-
|
|
1416
|
+
// 先从用户自定义指令里找,找不到再从预置指令里找
|
|
1417
|
+
let rule = list.find((a) => a.id === req.params.id);
|
|
1418
|
+
let isBuiltin = false;
|
|
1419
|
+
if (!rule) {
|
|
1420
|
+
const builtin = BUILTIN_AUTOMATIONS.find((a) => a.id === req.params.id);
|
|
1421
|
+
if (builtin) {
|
|
1422
|
+
rule = builtin;
|
|
1423
|
+
isBuiltin = true;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
662
1426
|
if (!rule) {
|
|
663
1427
|
res.status(404).json({ ok: false, error: '指令不存在' });
|
|
664
1428
|
return;
|
|
@@ -669,12 +1433,16 @@ function startWebServer(opts = {}) {
|
|
|
669
1433
|
});
|
|
670
1434
|
rule.runCount += 1;
|
|
671
1435
|
rule.lastRunAt = new Date().toISOString();
|
|
672
|
-
//
|
|
673
|
-
|
|
1436
|
+
// 预置指令的 runCount 不持久化(只在内存中统计),用户自定义指令才持久化
|
|
1437
|
+
let persistWarning = false;
|
|
1438
|
+
if (!isBuiltin) {
|
|
1439
|
+
persistWarning = !saveJsonFile(automationsFile, list);
|
|
1440
|
+
}
|
|
674
1441
|
res.status(201).json({
|
|
675
1442
|
ok: true,
|
|
676
1443
|
task: publicTask(record, true),
|
|
677
1444
|
runCount: rule.runCount,
|
|
1445
|
+
builtin: isBuiltin || undefined,
|
|
678
1446
|
persistWarning: persistWarning || undefined,
|
|
679
1447
|
});
|
|
680
1448
|
});
|
|
@@ -737,9 +1505,43 @@ function startWebServer(opts = {}) {
|
|
|
737
1505
|
},
|
|
738
1506
|
];
|
|
739
1507
|
const templatesFile = (0, path_1.join)(homeDir, 'templates.json');
|
|
740
|
-
app.get('/api/templates', (_req, res) => {
|
|
1508
|
+
app.get('/api/templates', async (_req, res) => {
|
|
741
1509
|
const user = loadJsonFile(templatesFile, []);
|
|
742
|
-
|
|
1510
|
+
// 从节点同步的模板
|
|
1511
|
+
const nodeTemplates = collectNodeTemplates().map((t) => ({
|
|
1512
|
+
id: `node:${t.id}`,
|
|
1513
|
+
title: t.title,
|
|
1514
|
+
category: t.category,
|
|
1515
|
+
goal: t.goal,
|
|
1516
|
+
icon: t.icon,
|
|
1517
|
+
builtin: false,
|
|
1518
|
+
source: t.source,
|
|
1519
|
+
nodeId: t.nodeId,
|
|
1520
|
+
}));
|
|
1521
|
+
// 从自定义来源拉取模板
|
|
1522
|
+
const customTemplates = [];
|
|
1523
|
+
for (const src of loadSources()) {
|
|
1524
|
+
if (!src.enabled || src.type !== 'templates')
|
|
1525
|
+
continue;
|
|
1526
|
+
try {
|
|
1527
|
+
const r = await fetchWithRetry(src.url, 10000, 0);
|
|
1528
|
+
if (r.ok) {
|
|
1529
|
+
const data = await r.json();
|
|
1530
|
+
const list = Array.isArray(data) ? data : (data.items || data.templates || []);
|
|
1531
|
+
customTemplates.push(...list.map((t) => ({
|
|
1532
|
+
id: `custom:${src.id}:${t.id || t.title}`,
|
|
1533
|
+
title: String(t.title || t.name || ''),
|
|
1534
|
+
category: String(t.category || src.name),
|
|
1535
|
+
goal: String(t.goal || t.prompt || ''),
|
|
1536
|
+
icon: String(t.icon || '📄'),
|
|
1537
|
+
builtin: false,
|
|
1538
|
+
source: src.name,
|
|
1539
|
+
})));
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
catch { /* skip */ }
|
|
1543
|
+
}
|
|
1544
|
+
res.json({ ok: true, builtin: BUILTIN_TEMPLATES, user, node: nodeTemplates, custom: customTemplates });
|
|
743
1545
|
});
|
|
744
1546
|
app.post('/api/templates', async (req, res) => {
|
|
745
1547
|
const body = (req.body ?? {});
|
|
@@ -778,54 +1580,96 @@ function startWebServer(opts = {}) {
|
|
|
778
1580
|
}
|
|
779
1581
|
res.json({ ok: true });
|
|
780
1582
|
});
|
|
781
|
-
/* ========== 办公助理:内置能力清单 ========== */
|
|
782
|
-
app.get('/api/office/capabilities', (_req, res) => {
|
|
1583
|
+
/* ========== 办公助理:内置能力清单 + 节点能力 + 自定义来源 ========== */
|
|
1584
|
+
app.get('/api/office/capabilities', async (_req, res) => {
|
|
1585
|
+
const builtin = [
|
|
1586
|
+
{
|
|
1587
|
+
id: 'doc-summary',
|
|
1588
|
+
icon: '📄',
|
|
1589
|
+
title: '文档摘要',
|
|
1590
|
+
desc: '粘贴长文档,生成结构化摘要与要点',
|
|
1591
|
+
prompt: '请对以下文档做摘要,提取 3-5 个核心要点并列出待办:\n\n',
|
|
1592
|
+
source: '内置',
|
|
1593
|
+
},
|
|
1594
|
+
{
|
|
1595
|
+
id: 'doc-translate',
|
|
1596
|
+
icon: '🌐',
|
|
1597
|
+
title: '中英互译',
|
|
1598
|
+
desc: '技术文档/代码注释翻译',
|
|
1599
|
+
prompt: '请将以下内容准确翻译(保留代码与术语):\n\n',
|
|
1600
|
+
source: '内置',
|
|
1601
|
+
},
|
|
1602
|
+
{
|
|
1603
|
+
id: 'doc-rewrite',
|
|
1604
|
+
icon: '✍️',
|
|
1605
|
+
title: '润色改写',
|
|
1606
|
+
desc: '把草稿改写为正式/简洁风格',
|
|
1607
|
+
prompt: '请润色以下文本,使其更专业简洁:\n\n',
|
|
1608
|
+
source: '内置',
|
|
1609
|
+
},
|
|
1610
|
+
{
|
|
1611
|
+
id: 'meeting-notes',
|
|
1612
|
+
icon: '📋',
|
|
1613
|
+
title: '会议纪要',
|
|
1614
|
+
desc: '从聊天/录音转写生成行动项',
|
|
1615
|
+
prompt: '请从以下记录中提取:决策、负责人、截止时间、待办:\n\n',
|
|
1616
|
+
source: '内置',
|
|
1617
|
+
},
|
|
1618
|
+
{
|
|
1619
|
+
id: 'email-draft',
|
|
1620
|
+
icon: '✉️',
|
|
1621
|
+
title: '邮件起草',
|
|
1622
|
+
desc: '按要点生成工作邮件',
|
|
1623
|
+
prompt: '请起草一封邮件,主题/背景/诉求如下:\n\n',
|
|
1624
|
+
source: '内置',
|
|
1625
|
+
},
|
|
1626
|
+
{
|
|
1627
|
+
id: 'excel-formula',
|
|
1628
|
+
icon: '📊',
|
|
1629
|
+
title: '表格公式',
|
|
1630
|
+
desc: '描述需求生成 Excel/Sheets 公式',
|
|
1631
|
+
prompt: '请写出实现以下需求的表格公式并解释:\n\n',
|
|
1632
|
+
source: '内置',
|
|
1633
|
+
},
|
|
1634
|
+
];
|
|
1635
|
+
// 从节点同步的办公能力
|
|
1636
|
+
const nodeCaps = collectNodeOffice().map((c) => ({
|
|
1637
|
+
id: `node:${c.id}`,
|
|
1638
|
+
icon: c.icon,
|
|
1639
|
+
title: c.title,
|
|
1640
|
+
desc: c.desc,
|
|
1641
|
+
prompt: c.prompt,
|
|
1642
|
+
source: c.source,
|
|
1643
|
+
nodeId: c.nodeId,
|
|
1644
|
+
}));
|
|
1645
|
+
// 从自定义来源拉取办公能力
|
|
1646
|
+
const customCaps = [];
|
|
1647
|
+
for (const src of loadSources()) {
|
|
1648
|
+
if (!src.enabled || src.type !== 'office')
|
|
1649
|
+
continue;
|
|
1650
|
+
try {
|
|
1651
|
+
const r = await fetchWithRetry(src.url, 10000, 0);
|
|
1652
|
+
if (r.ok) {
|
|
1653
|
+
const data = await r.json();
|
|
1654
|
+
const list = Array.isArray(data) ? data : (data.items || data.capabilities || []);
|
|
1655
|
+
customCaps.push(...list.map((c) => ({
|
|
1656
|
+
id: `custom:${src.id}:${c.id || c.title}`,
|
|
1657
|
+
icon: String(c.icon || '🔧'),
|
|
1658
|
+
title: String(c.title || c.name || ''),
|
|
1659
|
+
desc: String(c.desc || c.description || ''),
|
|
1660
|
+
prompt: String(c.prompt || c.goal || ''),
|
|
1661
|
+
source: src.name,
|
|
1662
|
+
})));
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
catch { /* skip */ }
|
|
1666
|
+
}
|
|
783
1667
|
res.json({
|
|
784
1668
|
ok: true,
|
|
785
|
-
capabilities: [
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
title: '文档摘要',
|
|
790
|
-
desc: '粘贴长文档,生成结构化摘要与要点',
|
|
791
|
-
prompt: '请对以下文档做摘要,提取 3-5 个核心要点并列出待办:\n\n',
|
|
792
|
-
},
|
|
793
|
-
{
|
|
794
|
-
id: 'doc-translate',
|
|
795
|
-
icon: '🌐',
|
|
796
|
-
title: '中英互译',
|
|
797
|
-
desc: '技术文档/代码注释翻译',
|
|
798
|
-
prompt: '请将以下内容准确翻译(保留代码与术语):\n\n',
|
|
799
|
-
},
|
|
800
|
-
{
|
|
801
|
-
id: 'doc-rewrite',
|
|
802
|
-
icon: '✍️',
|
|
803
|
-
title: '润色改写',
|
|
804
|
-
desc: '把草稿改写为正式/简洁风格',
|
|
805
|
-
prompt: '请润色以下文本,使其更专业简洁:\n\n',
|
|
806
|
-
},
|
|
807
|
-
{
|
|
808
|
-
id: 'meeting-notes',
|
|
809
|
-
icon: '📋',
|
|
810
|
-
title: '会议纪要',
|
|
811
|
-
desc: '从聊天/录音转写生成行动项',
|
|
812
|
-
prompt: '请从以下记录中提取:决策、负责人、截止时间、待办:\n\n',
|
|
813
|
-
},
|
|
814
|
-
{
|
|
815
|
-
id: 'email-draft',
|
|
816
|
-
icon: '✉️',
|
|
817
|
-
title: '邮件起草',
|
|
818
|
-
desc: '按要点生成工作邮件',
|
|
819
|
-
prompt: '请起草一封邮件,主题/背景/诉求如下:\n\n',
|
|
820
|
-
},
|
|
821
|
-
{
|
|
822
|
-
id: 'excel-formula',
|
|
823
|
-
icon: '📊',
|
|
824
|
-
title: '表格公式',
|
|
825
|
-
desc: '描述需求生成 Excel/Sheets 公式',
|
|
826
|
-
prompt: '请写出实现以下需求的表格公式并解释:\n\n',
|
|
827
|
-
},
|
|
828
|
-
],
|
|
1669
|
+
capabilities: [...builtin, ...nodeCaps, ...customCaps],
|
|
1670
|
+
builtin,
|
|
1671
|
+
node: nodeCaps,
|
|
1672
|
+
custom: customCaps,
|
|
829
1673
|
});
|
|
830
1674
|
});
|
|
831
1675
|
/* ========== 大模型配置(三重加密:apiKey AES-256-GCM 落盘加密 + RSA 加密传输) ========== */
|