ysagc-agent 0.3.0 → 0.5.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.
Files changed (67) hide show
  1. package/README.md +132 -60
  2. package/dist/agent.d.ts +17 -0
  3. package/dist/agent.d.ts.map +1 -0
  4. package/dist/agent.js +331 -0
  5. package/dist/agent.js.map +1 -0
  6. package/dist/cli.d.ts +14 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +320 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/config.d.ts +58 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +152 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/executor.d.ts +48 -0
  15. package/dist/executor.d.ts.map +1 -0
  16. package/dist/executor.js +458 -0
  17. package/dist/executor.js.map +1 -0
  18. package/dist/index.d.ts +23 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +45 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/logger.d.ts +37 -0
  23. package/dist/logger.d.ts.map +1 -0
  24. package/dist/logger.js +121 -0
  25. package/dist/logger.js.map +1 -0
  26. package/dist/path-utils.d.ts +44 -0
  27. package/dist/path-utils.d.ts.map +1 -0
  28. package/dist/path-utils.js +206 -0
  29. package/dist/path-utils.js.map +1 -0
  30. package/dist/protocol.d.ts +92 -0
  31. package/dist/protocol.d.ts.map +1 -0
  32. package/dist/protocol.js +94 -0
  33. package/dist/protocol.js.map +1 -0
  34. package/dist/security.d.ts +32 -0
  35. package/dist/security.d.ts.map +1 -0
  36. package/dist/security.js +228 -0
  37. package/dist/security.js.map +1 -0
  38. package/dist/skills.d.ts +16 -0
  39. package/dist/skills.d.ts.map +1 -0
  40. package/dist/skills.js +105 -0
  41. package/dist/skills.js.map +1 -0
  42. package/dist/tokens.d.ts +26 -0
  43. package/dist/tokens.d.ts.map +1 -0
  44. package/dist/tokens.js +50 -0
  45. package/dist/tokens.js.map +1 -0
  46. package/dist/tools.d.ts +69 -0
  47. package/dist/tools.d.ts.map +1 -0
  48. package/dist/tools.js +694 -0
  49. package/dist/tools.js.map +1 -0
  50. package/package.json +36 -21
  51. package/src/config.js +0 -94
  52. package/src/http-fs.js +0 -178
  53. package/src/index.js +0 -442
  54. package/src/logger.js +0 -84
  55. package/src/methods/browse.js +0 -91
  56. package/src/methods/dialog.js +0 -84
  57. package/src/methods/fs.js +0 -674
  58. package/src/methods/git.js +0 -391
  59. package/src/methods/project.js +0 -57
  60. package/src/methods/scaffold.js +0 -131
  61. package/src/methods/skill.js +0 -472
  62. package/src/methods/system.js +0 -82
  63. package/src/methods/terminal.js +0 -234
  64. package/src/origin.js +0 -38
  65. package/src/pairing.js +0 -168
  66. package/src/rpc.js +0 -62
  67. package/src/sandbox.js +0 -178
@@ -1,472 +0,0 @@
1
- /**
2
- * skill.* RPC 方法(T-M1-17,需求 5.22 Skills 技能安装)
3
- * - skill.list / skill.install / skill.uninstall / skill.setEnabled / skill.detail / skill.context
4
- * - 技能目录:<配置目录>/skills/<name>/SKILL.md(frontmatter + 正文)
5
- * - 启停状态:<配置目录>/skills/.state.json
6
- * - 市场安装:Agent 直连后端下载 raw(ticket 一次性票据)→ 校验 → 写入
7
- * - 本地导入:复制本机文件夹 → 校验
8
- * - 安全:SKILL.md 大小 ≤8K 字符、技能名白名单 [a-z0-9-]、安装/卸载/启停记审计
9
- */
10
- 'use strict';
11
-
12
- const fs = require('fs');
13
- const path = require('path');
14
- const http = require('http');
15
- const https = require('https');
16
- const { RpcError } = require('../rpc');
17
- const { CONFIG_DIR } = require('../config');
18
- const logger = require('../logger');
19
-
20
- const SKILLS_ROOT = path.join(CONFIG_DIR, 'skills');
21
- const STATE_FILE = path.join(SKILLS_ROOT, '.state.json');
22
- const NAME_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
23
- const MAX_BODY_CHARS = 8000; // SKILL.md 正文 ≤ 8K 字符
24
- const MAX_FILES = 50;
25
- const MAX_TOTAL_BYTES = 10 * 1024 * 1024; // 技能包 ≤ 10MB
26
-
27
- function ensureRoot() {
28
- fs.mkdirSync(SKILLS_ROOT, { recursive: true });
29
- return SKILLS_ROOT;
30
- }
31
-
32
- function stateFile() {
33
- try {
34
- return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
35
- } catch {
36
- return {};
37
- }
38
- }
39
-
40
- function saveState(state) {
41
- try {
42
- ensureRoot();
43
- fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
44
- } catch (e) {
45
- logger.warn(`[skill] 状态保存失败: ${e.message}`);
46
- }
47
- }
48
-
49
- /** 解析 SKILL.md:frontmatter + 正文 */
50
- function parseSkillDoc(content) {
51
- // 兼容带 BOM 的文件(常见于 Windows 编辑器/Compress-Archive 产物)
52
- let text = String(content || '');
53
- if (text.charCodeAt(0) === 0xfeff) text = text.slice(1);
54
- const m = text.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
55
- if (!m) return null;
56
- const meta = {};
57
- for (const line of m[1].split('\n')) {
58
- const kv = line.match(/^([a-zA-Z_]+):\s*(.*)$/);
59
- if (kv) {
60
- let v = kv[2].trim();
61
- if (v.startsWith('[') && v.endsWith(']')) {
62
- v = v.slice(1, -1).split(',').map((s) => s.trim()).filter(Boolean);
63
- }
64
- meta[kv[1]] = v;
65
- }
66
- }
67
- const name = String(meta.name || '').trim();
68
- if (!NAME_RE.test(name)) return null;
69
- const body = m[2].trim();
70
- if (body.length > MAX_BODY_CHARS) return null;
71
- return {
72
- name,
73
- description: String(meta.description || '').slice(0, 200),
74
- version: String(meta.version || '1.0.0'),
75
- author: String(meta.author || ''),
76
- tags: Array.isArray(meta.tags) ? meta.tags : [],
77
- body,
78
- };
79
- }
80
-
81
- function readSkillInfo(dir) {
82
- const p = path.join(dir, 'SKILL.md');
83
- if (!fs.existsSync(p)) return null;
84
- let content;
85
- try {
86
- content = fs.readFileSync(p, 'utf8');
87
- } catch {
88
- return null;
89
- }
90
- const parsed = parseSkillDoc(content);
91
- if (!parsed) return null;
92
- const st = stateFile();
93
- return {
94
- name: parsed.name,
95
- description: parsed.description,
96
- version: parsed.version,
97
- author: parsed.author,
98
- tags: parsed.tags,
99
- enabled: st[parsed.name] === undefined ? true : !!st[parsed.name],
100
- installedAt: (() => {
101
- try {
102
- return new Date(fs.statSync(dir).birthtimeMs).toISOString();
103
- } catch {
104
- return null;
105
- }
106
- })(),
107
- };
108
- }
109
-
110
- /** 校验技能目录:路径穿越、大小、文件数 */
111
- function validateSkillDir(root, name) {
112
- const errors = [];
113
- let fileCount = 0;
114
- let totalBytes = 0;
115
- const walk = (dir, relBase) => {
116
- let entries;
117
- try {
118
- entries = fs.readdirSync(dir, { withFileTypes: true });
119
- } catch {
120
- errors.push(`无法读取目录 ${relBase}`);
121
- return;
122
- }
123
- for (const e of entries) {
124
- const abs = path.join(dir, e.name);
125
- const rel = relBase ? `${relBase}/${e.name}` : e.name;
126
- if (rel.includes('..') || rel.startsWith('/')) {
127
- errors.push(`非法路径: ${rel}`);
128
- continue;
129
- }
130
- if (e.isDirectory()) walk(abs, rel);
131
- else if (e.isFile()) {
132
- fileCount++;
133
- totalBytes += fs.statSync(abs).size;
134
- }
135
- }
136
- };
137
- walk(root, '');
138
- if (fileCount > MAX_FILES) errors.push(`文件数超限(>${MAX_FILES})`);
139
- if (totalBytes > MAX_TOTAL_BYTES) errors.push(`体积超限(>10MB)`);
140
- return { ok: errors.length === 0, errors, fileCount, totalBytes };
141
- }
142
-
143
- /** 获取技能目录下的所有文件(相对路径 → 内容),供市场安装落地用 */
144
- function readSkillFiles(dir) {
145
- const out = [];
146
- const walk = (d, relBase) => {
147
- for (const e of fs.readdirSync(d, { withFileTypes: true })) {
148
- const abs = path.join(d, e.name);
149
- const rel = relBase ? `${relBase}/${e.name}` : e.name;
150
- if (e.isDirectory()) walk(abs, rel);
151
- else if (e.isFile()) out.push({ relPath: rel, content: fs.readFileSync(abs, 'utf8') });
152
- }
153
- };
154
- walk(dir, '');
155
- return out;
156
- }
157
-
158
- /** 写技能文件(含路径穿越校验) */
159
- function writeSkillFiles(name, files) {
160
- const root = path.join(SKILLS_ROOT, name);
161
- ensureRoot();
162
- if (fs.existsSync(root)) throw new RpcError('E_SKILL_EXISTS', `技能 ${name} 已安装`);
163
- for (const f of files) {
164
- const rel = String(f.relPath || '');
165
- if (!rel || rel.includes('..') || rel.startsWith('/') || rel.includes('\\')) {
166
- throw new RpcError('E_SKILL_INVALID', `非法文件路径: ${rel}`);
167
- }
168
- }
169
- fs.mkdirSync(root, { recursive: true });
170
- try {
171
- for (const f of files) {
172
- const abs = path.join(root, f.relPath);
173
- fs.mkdirSync(path.dirname(abs), { recursive: true });
174
- fs.writeFileSync(abs, String(f.content ?? ''));
175
- }
176
- } catch (e) {
177
- // 失败回滚
178
- try { fs.rmSync(root, { recursive: true, force: true }); } catch { /* ignore */ }
179
- throw new RpcError('E_SKILL_INVALID', `写入失败: ${e.message}`);
180
- }
181
- return root;
182
- }
183
-
184
- /** 市场下载:GET backendUrl/api/workspace/skills/market/:id/raw?ticket= */
185
- function fetchMarketRaw(backendUrl, skillId, ticket) {
186
- return new Promise((resolve, reject) => {
187
- let u;
188
- try {
189
- u = new URL(`${String(backendUrl).replace(/\/$/, '')}/api/workspace/skills/market/${encodeURIComponent(skillId)}/raw`);
190
- if (ticket) u.searchParams.set('ticket', ticket);
191
- } catch {
192
- return reject(new RpcError('E_SKILL_MARKET_OFFLINE', '技能市场地址无效'));
193
- }
194
- const lib = u.protocol === 'https:' ? https : http;
195
- const req = lib.get(u, { timeout: 15000 }, (res) => {
196
- if (res.statusCode !== 200) {
197
- let body = '';
198
- res.on('data', (c) => (body += c));
199
- res.on('end', () => {
200
- let msg = `下载失败(HTTP ${res.statusCode})`;
201
- try { msg = JSON.parse(body).msg || msg; } catch { /* ignore */ }
202
- reject(new RpcError('E_SKILL_MARKET_OFFLINE', msg));
203
- });
204
- return;
205
- }
206
- let body = '';
207
- res.on('data', (c) => (body += c));
208
- res.on('end', () => {
209
- try {
210
- const json = JSON.parse(body);
211
- if (json.code !== 0 || !Array.isArray(json.data.files)) {
212
- reject(new RpcError('E_SKILL_MARKET_OFFLINE', json.msg || '技能数据异常'));
213
- return;
214
- }
215
- resolve(json.data);
216
- } catch {
217
- reject(new RpcError('E_SKILL_MARKET_OFFLINE', '技能数据解析失败'));
218
- }
219
- });
220
- });
221
- req.on('error', (e) => reject(new RpcError('E_SKILL_MARKET_OFFLINE', `技能市场不可用: ${e.message}`)));
222
- req.on('timeout', () => { req.destroy(); reject(new RpcError('E_SKILL_MARKET_OFFLINE', '技能市场下载超时')); });
223
- });
224
- }
225
-
226
- /**
227
- * 从任意 http/https 链接下载技能(v0.3.0:skill.installFromUrl)
228
- * 支持两种形态:
229
- * - .zip / 压缩包(含 SKILL.md 的技能包,可带 scripts/resources)
230
- * - 单个 SKILL.md / .md 文本(视为单个技能)
231
- * 安全:仅 http/https;大小 ≤ MAX_TOTAL_BYTES;SKILL.md frontmatter + 名称白名单校验;
232
- * 解压文件防路径穿越;校验失败整体回滚。
233
- */
234
- function downloadUrl(url, maxBytes) {
235
- return new Promise((resolve, reject) => {
236
- let u;
237
- try {
238
- u = new URL(String(url || '').trim());
239
- } catch {
240
- return reject(new RpcError('E_VALIDATION', '链接无效'));
241
- }
242
- if (u.protocol !== 'http:' && u.protocol !== 'https:') {
243
- return reject(new RpcError('E_VALIDATION', '仅支持 http/https 链接'));
244
- }
245
- const lib = u.protocol === 'https:' ? https : http;
246
- const chunks = [];
247
- let total = 0;
248
- let settled = false;
249
- const fail = (err) => {
250
- if (settled) return;
251
- settled = true;
252
- reject(err);
253
- };
254
- const req = lib.get(u, { timeout: 20000, headers: { 'User-Agent': 'ysagc-agent/0.3' } }, (res) => {
255
- if (res.statusCode >= 400) {
256
- let body = '';
257
- res.on('data', (c) => (body += c));
258
- res.on('end', () => {
259
- let msg = `下载失败(HTTP ${res.statusCode})`;
260
- try { msg = JSON.parse(body).msg || msg; } catch { /* ignore */ }
261
- fail(new RpcError('E_SKILL_DOWNLOAD_FAILED', msg));
262
- });
263
- return;
264
- }
265
- res.on('data', (c) => {
266
- total += c.length;
267
- if (total > maxBytes) {
268
- req.destroy();
269
- fail(new RpcError('E_SKILL_DOWNLOAD_FAILED', `技能包过大(>${Math.floor(maxBytes / 1024 / 1024)}MB)`));
270
- return;
271
- }
272
- chunks.push(c);
273
- });
274
- res.on('end', () => {
275
- if (settled) return;
276
- settled = true;
277
- const base = path.basename(u.pathname || '').split('?')[0];
278
- resolve({ buffer: Buffer.concat(chunks), contentType: res.headers['content-type'] || '', fileName: base });
279
- });
280
- });
281
- req.on('error', (e) => fail(new RpcError('E_SKILL_DOWNLOAD_FAILED', `下载失败: ${e.message}`)));
282
- req.on('timeout', () => { req.destroy(); fail(new RpcError('E_SKILL_DOWNLOAD_FAILED', '下载超时')); });
283
- });
284
- }
285
-
286
- /** 解压 zip(防路径穿越),返回 [{relPath, content}] */
287
- function extractZipToFiles(buffer) {
288
- return new Promise((resolve, reject) => {
289
- let yauzl;
290
- try {
291
- yauzl = require('yauzl');
292
- } catch {
293
- return reject(new RpcError('E_SKILL_INVALID', '本机缺少解压组件,请更新 Agent'));
294
- }
295
- yauzl.fromBuffer(buffer, { lazyEntries: true, decodeStrings: true }, (err, zipfile) => {
296
- if (err || !zipfile) return reject(new RpcError('E_SKILL_INVALID', '压缩包解析失败'));
297
- const files = [];
298
- zipfile.on('entry', (entry) => {
299
- const name = entry.fileName;
300
- if (/\/$/.test(name)) { zipfile.readEntry(); return; }
301
- if (name.includes('..') || name.startsWith('/') || name.includes('\\')) { zipfile.readEntry(); return; }
302
- zipfile.openReadStream(entry, (e, stream) => {
303
- if (e || !stream) { zipfile.readEntry(); return; }
304
- const chunks = [];
305
- stream.on('data', (c) => chunks.push(c));
306
- stream.on('end', () => {
307
- files.push({ relPath: name, content: Buffer.concat(chunks).toString('utf8') });
308
- zipfile.readEntry();
309
- });
310
- stream.on('error', () => zipfile.readEntry());
311
- });
312
- });
313
- zipfile.on('end', () => resolve(files));
314
- zipfile.on('error', (e) => reject(new RpcError('E_SKILL_INVALID', `压缩包读取失败: ${e.message}`)));
315
- zipfile.readEntry();
316
- });
317
- });
318
- }
319
-
320
- function createSkillMethods(ctxRef) {
321
- const cfg = () => ctxRef().config;
322
-
323
- function audit(opType, target, result, errorCode) {
324
- try {
325
- const br = ctxRef().broadcast;
326
- if (br) br({ jsonrpc: '2.0', method: 'event.audit', params: { opType, target, result, errorCode } });
327
- } catch { /* ignore */ }
328
- }
329
-
330
- function register(reg) {
331
- // ---- 已安装列表 ----
332
- reg('skill.list', async () => {
333
- ensureRoot();
334
- const items = [];
335
- for (const entry of fs.readdirSync(SKILLS_ROOT, { withFileTypes: true })) {
336
- if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
337
- const info = readSkillInfo(path.join(SKILLS_ROOT, entry.name));
338
- if (info) items.push(info);
339
- }
340
- return { skills: items };
341
- });
342
-
343
- // ---- 详情 ----
344
- reg('skill.detail', async (params = {}) => {
345
- const name = String(params.name || '');
346
- const dir = path.join(SKILLS_ROOT, name);
347
- const p = path.join(dir, 'SKILL.md');
348
- if (!NAME_RE.test(name) || !fs.existsSync(p)) throw new RpcError('E_SKILL_NOT_FOUND', '技能不存在');
349
- const content = fs.readFileSync(p, 'utf8');
350
- const parsed = parseSkillDoc(content);
351
- if (!parsed) throw new RpcError('E_SKILL_INVALID', 'SKILL.md 格式非法');
352
- return { ...parsed, files: readSkillFiles(dir) };
353
- });
354
-
355
- // ---- 安装(market / local)----
356
- reg('skill.install', async (params = {}) => {
357
- const source = String(params.source || '');
358
- if (source !== 'market' && source !== 'local') throw new RpcError('E_VALIDATION', 'source 必须为 market 或 local');
359
- let name = '';
360
- if (source === 'market') {
361
- const skillId = String(params.ref || '');
362
- const ticket = String(params.ticket || '');
363
- const data = await fetchMarketRaw(cfg().backendUrl, skillId, ticket);
364
- const files = data.files || [];
365
- // 取 SKILL.md 里的 name
366
- const sm = files.find((f) => f.relPath === 'SKILL.md');
367
- if (!sm) throw new RpcError('E_SKILL_INVALID', '技能包缺少 SKILL.md');
368
- const parsed = parseSkillDoc(sm.content);
369
- if (!parsed) throw new RpcError('E_SKILL_INVALID', 'SKILL.md 格式非法');
370
- name = parsed.name;
371
- writeSkillFiles(name, files);
372
- } else {
373
- // local:本机路径
374
- const p = String(params.path || '');
375
- if (!p) throw new RpcError('E_VALIDATION', '缺少技能路径');
376
- const smPath = path.join(p, 'SKILL.md');
377
- if (!fs.existsSync(smPath)) throw new RpcError('E_SKILL_INVALID', '目录缺少 SKILL.md');
378
- const content = fs.readFileSync(smPath, 'utf8');
379
- const parsed = parseSkillDoc(content);
380
- if (!parsed) throw new RpcError('E_SKILL_INVALID', 'SKILL.md 格式非法');
381
- name = parsed.name;
382
- const files = readSkillFiles(p);
383
- writeSkillFiles(name, files);
384
- }
385
- const dir = path.join(SKILLS_ROOT, name);
386
- const check = validateSkillDir(dir, name);
387
- if (!check.ok) {
388
- try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
389
- throw new RpcError('E_SKILL_INVALID', `技能校验失败: ${check.errors.join('; ')}`);
390
- }
391
- audit('skill.install', name, 'ok');
392
- logger.info(`[skill] 已安装技能 ${name}(来源 ${source})`);
393
- return { ok: true, skill: readSkillInfo(dir) };
394
- });
395
-
396
- // ---- 从链接安装(v0.3.0)----
397
- reg('skill.installFromUrl', async (params = {}) => {
398
- const url = String(params.url || '').trim();
399
- if (!url) throw new RpcError('E_VALIDATION', '请输入技能链接');
400
- const { buffer, fileName } = await downloadUrl(url, MAX_TOTAL_BYTES);
401
- const isZip = /\.zip$/i.test(fileName) || (buffer.length >= 2 && buffer[0] === 0x50 && buffer[1] === 0x4b);
402
- let files = [];
403
- if (isZip) {
404
- files = await extractZipToFiles(buffer);
405
- } else {
406
- files = [{ relPath: 'SKILL.md', content: buffer.toString('utf8') }];
407
- }
408
- const sm = files.find((f) => f.relPath === 'SKILL.md');
409
- if (!sm) throw new RpcError('E_SKILL_INVALID', '技能包缺少 SKILL.md');
410
- const parsed = parseSkillDoc(sm.content);
411
- if (!parsed) throw new RpcError('E_SKILL_INVALID', 'SKILL.md 格式非法(需要 name/description frontmatter)');
412
- const name = parsed.name;
413
- const dir = writeSkillFiles(name, files);
414
- const check = validateSkillDir(dir, name);
415
- if (!check.ok) {
416
- try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
417
- throw new RpcError('E_SKILL_INVALID', `技能校验失败: ${check.errors.join('; ')}`);
418
- }
419
- audit('skill.install', name, 'ok');
420
- logger.info(`[skill] 已从链接安装技能 ${name}(${url})`);
421
- return { ok: true, skill: readSkillInfo(dir) };
422
- });
423
-
424
- // ---- 卸载(危险级:前端二次确认)----
425
- reg('skill.uninstall', async (params = {}) => {
426
- const name = String(params.name || '');
427
- const dir = path.join(SKILLS_ROOT, name);
428
- if (!NAME_RE.test(name) || !fs.existsSync(dir)) throw new RpcError('E_SKILL_NOT_FOUND', '技能不存在');
429
- fs.rmSync(dir, { recursive: true, force: true });
430
- const st = stateFile();
431
- delete st[name];
432
- saveState(st);
433
- audit('skill.uninstall', name, 'ok');
434
- return { ok: true };
435
- });
436
-
437
- // ---- 启用/禁用 ----
438
- reg('skill.setEnabled', async (params = {}) => {
439
- const name = String(params.name || '');
440
- const dir = path.join(SKILLS_ROOT, name);
441
- if (!NAME_RE.test(name) || !fs.existsSync(dir)) throw new RpcError('E_SKILL_NOT_FOUND', '技能不存在');
442
- const st = stateFile();
443
- st[name] = !!params.enabled;
444
- saveState(st);
445
- audit('skill.setEnabled', name, 'ok');
446
- return { ok: true, enabled: !!params.enabled };
447
- });
448
-
449
- // ---- AI 上下文注入(≤2K token,5.22.4)----
450
- reg('skill.context', async () => {
451
- ensureRoot();
452
- const st = stateFile();
453
- const parts = [];
454
- let budget = 2000; // 2K token ≈ 约 6000 字符(中文保守按 3 字/token)
455
- for (const entry of fs.readdirSync(SKILLS_ROOT, { withFileTypes: true })) {
456
- if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
457
- if (st[entry.name] === false) continue; // 已禁用
458
- const info = readSkillInfo(path.join(SKILLS_ROOT, entry.name));
459
- if (!info) continue;
460
- const snippet = `[技能 ${info.name}] ${info.description}(v${info.version})`;
461
- if (budget - snippet.length < 0 && parts.length > 0) break;
462
- parts.push(snippet);
463
- budget -= snippet.length;
464
- }
465
- return { skills: parts };
466
- });
467
- }
468
-
469
- return { register };
470
- }
471
-
472
- module.exports = { createSkillMethods, SKILLS_ROOT };
@@ -1,82 +0,0 @@
1
- /**
2
- * system.* RPC 方法:system.ping / system.info / system.check
3
- * (T-M0-14:系统信息 + git 检测 + 版本上报)
4
- */
5
- 'use strict';
6
-
7
- const os = require('os');
8
- const { execFile } = require('child_process');
9
- const path = require('path');
10
-
11
- const AGENT_VERSION = require('../../package.json').version;
12
- const PROTOCOL_VERSION = 1;
13
- const MIN_NODE_VERSION = 20;
14
-
15
- let startupAt = Date.now();
16
-
17
- function detectPlatform() {
18
- return `${process.platform} ${process.arch}`;
19
- }
20
-
21
- /**
22
- * git --version 检测(spawn 参数数组,绝不拼字符串命令)
23
- */
24
- function gitVersion(timeoutMs = 5000) {
25
- return new Promise((resolve) => {
26
- try {
27
- const child = execFile(
28
- 'git',
29
- ['--version'],
30
- { timeout: timeoutMs, windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] },
31
- (err, stdout) => {
32
- if (err) return resolve({ installed: false, version: '' });
33
- const m = String(stdout).match(/git version\s+([^\s]+)/i);
34
- resolve({ installed: true, version: m ? m[1] : String(stdout).trim() });
35
- }
36
- );
37
- child.on('error', () => resolve({ installed: false, version: '' }));
38
- } catch {
39
- resolve({ installed: false, version: '' });
40
- }
41
- });
42
- }
43
-
44
- /** 收集一次 gitVersion 结果并缓存 30s */
45
- let gitCache = null;
46
- let gitCacheAt = 0;
47
- async function gitVersionCached() {
48
- if (gitCache && Date.now() - gitCacheAt < 30000) return gitCache;
49
- gitCache = await gitVersion();
50
- gitCacheAt = Date.now();
51
- return gitCache;
52
- }
53
-
54
- function registerSystemMethods(register, ctxRef) {
55
- register('system.ping', async () => ({ pong: true, time: Date.now() }));
56
-
57
- register('system.info', async () => {
58
- const cfg = ctxRef().config;
59
- return {
60
- agentVersion: AGENT_VERSION,
61
- protocolVersion: PROTOCOL_VERSION,
62
- platform: detectPlatform(),
63
- nodeVersion: process.version,
64
- hostname: os.hostname(),
65
- uptimeSec: Math.floor((Date.now() - startupAt) / 1000),
66
- port: cfg.port,
67
- enableTerminal: !!cfg.enableTerminal,
68
- terminalPermission: cfg.terminalPermission || 'ask',
69
- trashEnabled: !!cfg.trashEnabled,
70
- };
71
- });
72
-
73
- register('system.check', async (params = {}) => {
74
- const checks = {};
75
- if (params.git !== false) checks.git = await gitVersionCached();
76
- checks.node = { installed: true, version: process.version, ok: Number(process.versions.node.split('.')[0]) >= MIN_NODE_VERSION };
77
- checks.agent = { version: AGENT_VERSION, protocolVersion: PROTOCOL_VERSION, ok: true };
78
- return { checks };
79
- });
80
- }
81
-
82
- module.exports = { registerSystemMethods, AGENT_VERSION, PROTOCOL_VERSION, gitVersion };