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.
@@ -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
- /* ========== 技能市场(插件市场):聚合 ClawHub + Agent-Foundry ========== */
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
- res.json({ ok: true, automations: loadJsonFile(automationsFile, []) });
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
- const rule = list.find((a) => a.id === req.params.id);
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
- // 任务已入队,runCount 持久化失败不阻塞响应(带 persistWarning 提示)
673
- const persistWarning = !saveJsonFile(automationsFile, list);
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
- res.json({ ok: true, builtin: BUILTIN_TEMPLATES, user });
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
- id: 'doc-summary',
788
- icon: '📄',
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 加密传输) ========== */