aims-workstation 0.1.6 → 0.1.8
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/.env.example +0 -1
- package/package.json +1 -1
- package/server.js +421 -151
- package/web/dist/assets/index-CUF2TY1u.css +2 -0
- package/web/dist/assets/index-DtZ9K70Z.js +104 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-C9Ko3aMI.js +0 -104
- package/web/dist/assets/index-u4w4mrti.css +0 -2
package/server.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Agent 层基于 @earendil-works/pi-ai(统一 LLM API),前端为 web/ 预构建的 shadcn/ui 应用(web/dist)
|
|
4
4
|
'use strict';
|
|
5
5
|
// TLS 证书校验关闭(用户决策:内网私有部署自签证书常见,能访问优先)
|
|
6
|
-
// 覆盖所有出站 fetch(ARM REST
|
|
6
|
+
// 覆盖所有出站 fetch(ARM REST、LLM、面板代理)。本工具面向内网/本机使用。
|
|
7
7
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
8
8
|
const http = require('http');
|
|
9
9
|
const fs = require('fs');
|
|
@@ -76,7 +76,7 @@ const PORT = +(process.env.PORT || env.PORT || 5600);
|
|
|
76
76
|
const HOST = process.env.AIMS_HOST || '0.0.0.0'; // 默认局域网可达(团队共用);仅本机使用可设 AIMS_HOST=127.0.0.1
|
|
77
77
|
// ── 运行时配置(ARM 源与 LLM 均可私有化部署:在「系统设置」页切换,存 AIMS_HOME/config/platform.json,优先级 运行时 > 环境变量 > .env)──
|
|
78
78
|
const CFG_FILE = path.join(AIMS_HOME, 'config/platform.json');
|
|
79
|
-
const cfg = { arm_api_base: '',
|
|
79
|
+
const cfg = { arm_api_base: '', arm_token: '', llm_base_url: '', llm_api_key: '', llm_model: '', scene_profile: '', access_token: '' };
|
|
80
80
|
// 守护栏默认策略(可在「系统设置·守护栏」维护,存 platform.json,保存即热生效):deny=正则源串(命中即永久拒绝,任何级别不豁免);
|
|
81
81
|
// ro=只读白名单命令名(免确认直行);sudo_u_l2=true 时 `sudo -u <用户>` 降权形式不再永久拒绝、按写命令走 L2 人工确认(裸 sudo 仍拒绝)
|
|
82
82
|
const GUARD_DEFAULT = {
|
|
@@ -92,14 +92,14 @@ function normalizeGuard(g) {
|
|
|
92
92
|
sudo_u_l2: !!g.sudo_u_l2,
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
|
-
function loadCfg() { try { Object.assign(cfg, JSON.parse(fs.readFileSync(CFG_FILE, 'utf8'))); } catch { } cfg.guard = normalizeGuard(cfg.guard); }
|
|
95
|
+
function loadCfg() { try { Object.assign(cfg, JSON.parse(fs.readFileSync(CFG_FILE, 'utf8'))); } catch { } delete cfg.arm_mcp_url; /* MCP 通道已退役:剥离旧配置残留 */ cfg.guard = normalizeGuard(cfg.guard); }
|
|
96
96
|
loadCfg();
|
|
97
97
|
const guardNow = () => cfg.guard;
|
|
98
98
|
function saveCfg() { fs.writeFileSync(CFG_FILE, JSON.stringify(cfg, null, 2)); }
|
|
99
|
-
// ARM 站点地址:运行时配置 > ARM_API_BASE(env) >
|
|
99
|
+
// ARM 站点地址:运行时配置 > ARM_API_BASE(env) > 公司内网默认(内网可达即开箱可用)
|
|
100
|
+
//(2026-10-21 全面切换新版 ARM:仅 REST 通道,MCP 通道及旧版兼容回落已退役)
|
|
100
101
|
const ARM_DEFAULT_BASE = 'http://arm.agent-platform.dev.aimstek.cn';
|
|
101
|
-
const armApiBase = () => cfg.arm_api_base || process.env.ARM_API_BASE || env.ARM_API_BASE ||
|
|
102
|
-
const armMcpUrl = () => cfg.arm_mcp_url || `${armApiBase()}/api/mcp`;
|
|
102
|
+
const armApiBase = () => cfg.arm_api_base || process.env.ARM_API_BASE || env.ARM_API_BASE || ARM_DEFAULT_BASE;
|
|
103
103
|
const armApiToken = () => cfg.arm_token || env.ARM_API_TOKEN || ARM_PAT;
|
|
104
104
|
// LLM:OpenAI 兼容端点(baseUrl/模型/密钥),缺省公司内网 bifrost 网关(内网可达即可用,无需配置)
|
|
105
105
|
const LLM_DEFAULT_BASE = 'http://bifrost.infra.dev.aimstek.cn/v1';
|
|
@@ -186,26 +186,7 @@ const world = { devices: [], alarms: [], kpi: {}, work_orders: [], inspections:
|
|
|
186
186
|
function audit(entry) { entry.ts = new Date().toISOString(); fs.appendFileSync(AUDIT_LOG, JSON.stringify(entry) + '\n'); }
|
|
187
187
|
function metric(entry) { entry.ts = new Date().toISOString(); fs.appendFileSync(METRICS_LOG, JSON.stringify(entry) + '\n'); }
|
|
188
188
|
|
|
189
|
-
//
|
|
190
|
-
async function mcpCall(name, args, { pat = false, timeout_ms = 20000 } = {}) {
|
|
191
|
-
const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream' };
|
|
192
|
-
if (pat && ARM_PAT) headers['Authorization'] = `Bearer ${ARM_PAT}`;
|
|
193
|
-
const ctrl = new AbortController();
|
|
194
|
-
const timer = setTimeout(() => ctrl.abort(), timeout_ms); // 离线快速失败(降级本地库)
|
|
195
|
-
let data;
|
|
196
|
-
try {
|
|
197
|
-
const res = await fetch(armMcpUrl(), {
|
|
198
|
-
method: 'POST', headers, signal: ctrl.signal,
|
|
199
|
-
body: JSON.stringify({ jsonrpc: '2.0', id: crypto.randomUUID(), method: 'tools/call', params: { name, arguments: args } }),
|
|
200
|
-
});
|
|
201
|
-
data = await res.json();
|
|
202
|
-
} finally { clearTimeout(timer); }
|
|
203
|
-
if (data.error) throw new Error(`ARM MCP ${name}: ${data.error.message}`);
|
|
204
|
-
if ((data.result || {}).isError) throw new Error(`ARM MCP ${name} 拒绝: ${((data.result).content || []).map(c => c.text).join('').slice(0, 150)}`);
|
|
205
|
-
return ((data.result || {}).content || []).map(c => c.text || '').join('\n');
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// ARM REST 客户端(v2 开发版结构化通道;{ ok, data, msg } 包络,ok=false 抛错)
|
|
189
|
+
// ARM REST 客户端(v2 开发版结构化通道;{ ok, data, msg } 包络,ok=false 抛错)——目录/检索/下载/回流全链路唯一通道
|
|
209
190
|
async function armRest(pathname, init = {}, timeout_ms = 20000) {
|
|
210
191
|
const headers = { 'Content-Type': 'application/json', ...(init.headers || {}) };
|
|
211
192
|
const token = armApiToken();
|
|
@@ -232,6 +213,11 @@ async function armRest(pathname, init = {}, timeout_ms = 20000) {
|
|
|
232
213
|
throw e;
|
|
233
214
|
} finally { clearTimeout(timer); }
|
|
234
215
|
}
|
|
216
|
+
// 知识写入(经验回流链):REST v2,令牌走 armApiToken() 三级回落(设置页 > env > ~/.arm PAT)
|
|
217
|
+
// 返回 data: { id, name, shareUrl, … }——id 为 UUID,供指标留痕与回流记录定位
|
|
218
|
+
async function armCreateKnowledge({ name, description, content, tags }) {
|
|
219
|
+
return armRest('/api/v1/knowledges', { method: 'POST', body: JSON.stringify({ name, description, content, tags }) }, 25000);
|
|
220
|
+
}
|
|
235
221
|
const vVer = (v) => (String(v || '').startsWith('v') ? String(v) : `v${v || '0.0.0'}`); // REST 无 v 前缀,统一内部表示
|
|
236
222
|
|
|
237
223
|
const agentCache = new Map();
|
|
@@ -240,30 +226,14 @@ async function getArmAgent(agentId) {
|
|
|
240
226
|
if (lm) return { name: lm.name, version: lm.version, prompt: lm.prompt, knowledges: (lm.knowledges || []).map(k => k.ref) };
|
|
241
227
|
const hit = agentCache.get(agentId);
|
|
242
228
|
if (hit && Date.now() - hit.at < 5 * 60_000) return hit.data;
|
|
243
|
-
const
|
|
244
|
-
const
|
|
245
|
-
const
|
|
246
|
-
const prompt = (text.match(/## Prompt\n([\s\S]*?)(\n## |\n*$)/) || [])[1]?.trim() || '';
|
|
247
|
-
const knowledges = [...text.matchAll(/^- (.+?) v[\d.]+((\w+))/gm)].map(m => m[1]);
|
|
248
|
-
const data = { name, version, prompt, knowledges };
|
|
229
|
+
const d = await armRest(`/api/v1/agents/${agentId}`);
|
|
230
|
+
const knowledges = (d.knowledges || []).map(kb => (kb.knowledge && kb.knowledge.name) || kb.knowledgeId || '').filter(Boolean);
|
|
231
|
+
const data = { name: d.name || agentId, version: vVer(d.version), prompt: d.prompt || '', knowledges };
|
|
249
232
|
agentCache.set(agentId, { data, at: Date.now() });
|
|
250
233
|
return data;
|
|
251
234
|
}
|
|
252
235
|
|
|
253
|
-
//
|
|
254
|
-
function parseAgentCards(raw) {
|
|
255
|
-
const cards = [];
|
|
256
|
-
for (const block of raw.split(/\n(?=## )/)) {
|
|
257
|
-
const m = block.match(/^## (.+?) \((v[\d.]+)\)\nID: ([0-9a-f-]{36})/);
|
|
258
|
-
if (!m) continue;
|
|
259
|
-
const bind = block.match(/绑定:(\d+) Skill \/ (\d+) Knowledge/);
|
|
260
|
-
const desc = (block.split('\n').filter(l => l && !l.startsWith('#') && !l.startsWith('ID:') && !l.startsWith('绑定'))[0] || '').trim();
|
|
261
|
-
cards.push({ name: m[1], version: m[2], agent_id: m[3], skills: +(bind?.[1] || 0), knowledges: +(bind?.[2] || 0), description: desc.slice(0, 120) });
|
|
262
|
-
}
|
|
263
|
-
return cards;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// 助手目录:REST 分页结构化优先(可全量),旧版 ARM 回落 MCP markdown 解析(limit 上限 20)
|
|
236
|
+
// 助手目录:REST 分页结构化(可全量)
|
|
267
237
|
async function armListAgents(keyword = '') {
|
|
268
238
|
const out = [];
|
|
269
239
|
for (let page = 1; page <= 5; page++) {
|
|
@@ -274,26 +244,16 @@ async function armListAgents(keyword = '') {
|
|
|
274
244
|
return out.map(a => ({ agent_id: a.id, name: a.name, version: vVer(a.version), skills: a.skillsCount || 0, knowledges: a.knowledgesCount || 0, description: String(a.description || '').slice(0, 120), status: a.status || 'active', created_by: a.creatorName || a.createdBy || '' }));
|
|
275
245
|
}
|
|
276
246
|
|
|
277
|
-
// 知识检索(全局经验库):REST 混合检索(RAG 语义 ⊕
|
|
247
|
+
// 知识检索(全局经验库):REST 混合检索(RAG 语义 ⊕ 关键词);不可达由调用方降级(本地知识关键词命中)
|
|
278
248
|
async function searchKnowledgeItems(query, topK = 5) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
};
|
|
285
|
-
} catch {
|
|
286
|
-
const text = await mcpCall('search_knowledges', { query, limit: topK });
|
|
287
|
-
const items = text.split(/\n(?=## )/).filter(Boolean).map(b => {
|
|
288
|
-
const title = (b.match(/^## (.+?)(?:(相似度 (\d+)%))?/) || [])[1] || b.slice(0, 40);
|
|
289
|
-
return { title: title.trim(), similarity: +((b.match(/相似度 (\d+)%/) || [])[1] || 0) || null, snippet: b.replace(/^## .*\n?/, '').split('[完整内容')[0].slice(0, 320) };
|
|
290
|
-
});
|
|
291
|
-
return { via: 'mcp', items };
|
|
292
|
-
}
|
|
249
|
+
const d = await armRest('/api/v1/knowledges/search', { method: 'POST', body: JSON.stringify({ query, topK }) });
|
|
250
|
+
return {
|
|
251
|
+
via: `rest:${d.mode || 'hybrid'}${d.degraded ? ':degraded' : ''}`,
|
|
252
|
+
items: (d.items || []).map(it => ({ title: it.name, similarity: it.score != null ? Math.round(it.score * 100) : null, snippet: String(it.snippet || '').slice(0, 320), id: it.knowledgeId })),
|
|
253
|
+
};
|
|
293
254
|
}
|
|
294
255
|
|
|
295
256
|
// ── 本地助手库(07-助手本地化:下载物化 → 离线可用;按版本目录存档,升级原子切指针)──
|
|
296
|
-
const armSourceTag = (base) => { try { return `arm://${new URL(base).host}`; } catch { return 'arm://unknown'; } };
|
|
297
257
|
const regFile = () => path.join(ARM_AGENTS_DIR, 'registry.json');
|
|
298
258
|
function loadRegistry() {
|
|
299
259
|
try { return JSON.parse(fs.readFileSync(regFile(), 'utf8')); } catch { return { agents: [], last_sync_at: null }; }
|
|
@@ -319,13 +279,7 @@ function cmpVersion(a, b) { // 'v1.10.0' vs 'v1.9.3' 语义化比较
|
|
|
319
279
|
}
|
|
320
280
|
const slug = (s) => String(s).trim().replace(/[\\/:*?"<>|\s]+/g, '-').slice(0, 60) || 'knowledge';
|
|
321
281
|
|
|
322
|
-
//
|
|
323
|
-
async function downloadAgent(agentId) {
|
|
324
|
-
try { return await downloadAgentRest(agentId); }
|
|
325
|
-
catch { return await downloadAgentMcp(agentId); }
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// 注册表提交(两路下载共用):写版本目录后原子切指针
|
|
282
|
+
// 注册表提交:写版本目录后原子切指针
|
|
329
283
|
function commitDownload(manifest) {
|
|
330
284
|
const reg = loadRegistry();
|
|
331
285
|
let e = reg.agents.find(a => a.agent_id === manifest.agent_id);
|
|
@@ -349,7 +303,8 @@ function uninstallLocalAgent(agentId, purge_workspaces) {
|
|
|
349
303
|
return true;
|
|
350
304
|
}
|
|
351
305
|
|
|
352
|
-
|
|
306
|
+
// 下载物化:REST 结构化详情 + knowledgeId 直取全文
|
|
307
|
+
async function downloadAgent(agentId) {
|
|
353
308
|
const d = await armRest(`/api/v1/agents/${agentId}`);
|
|
354
309
|
const version = vVer(d.version);
|
|
355
310
|
const verDir = path.join(ARM_AGENTS_DIR, agentId, 'versions', version);
|
|
@@ -366,62 +321,39 @@ async function downloadAgentRest(agentId) {
|
|
|
366
321
|
} catch (e) { note = `知识拉取失败: ${String(e.message).slice(0, 80)}`; }
|
|
367
322
|
knowledges.push({ ref: name, version: vVer(kb.version), kind: kb.kind || '', file, note });
|
|
368
323
|
}
|
|
324
|
+
// UI 绑定物化(三资产模型:助手上岗自带工位)——面板过校验器入库、workbench 直接线上 v1(可回滚)
|
|
325
|
+
const ui_bindings = [];
|
|
326
|
+
for (const ub of d.uiBindings || []) {
|
|
327
|
+
const an = ub.asset && ub.asset.name;
|
|
328
|
+
const ver = ub.assetVersion || (ub.asset && ub.asset.latestVersion) || 'v1';
|
|
329
|
+
if (!an) continue;
|
|
330
|
+
let note = '';
|
|
331
|
+
try {
|
|
332
|
+
const { buf } = await armDownload(`/api/v1/ui-assets/${encodeURIComponent(an)}/versions/${encodeURIComponent(ver)}/download`);
|
|
333
|
+
fs.mkdirSync(path.join(verDir, 'ui'), { recursive: true });
|
|
334
|
+
fs.writeFileSync(path.join(verDir, 'ui', `${slug(an)}-${ver}.zip`), buf);
|
|
335
|
+
const r = installAssetFiles(unzipFiles(buf), { agentId, mode: 'live' });
|
|
336
|
+
note = `panels×${r.panels.length}${r.workbench ? ' + workbench' : ''}`;
|
|
337
|
+
} catch (e) { note = `UI 资产物化失败: ${String(e.message).slice(0, 80)}`; }
|
|
338
|
+
ui_bindings.push({ ref: an, version: ver, note });
|
|
339
|
+
}
|
|
369
340
|
const manifest = {
|
|
370
341
|
agent_id: agentId, name: d.name, version, description: d.description || '',
|
|
371
342
|
downloaded_at: new Date().toISOString(), source: `arm-rest://${(armApiBase() || '').replace(/^https?:\/\//, '').split('/')[0]}`,
|
|
372
343
|
prompt: d.prompt || '',
|
|
373
344
|
skills: (d.skills || []).map(s => ({ ref: (s.skill && s.skill.name) || s.skillId || s.id, version: vVer(s.version), kind: 'skill', description: String((s.skill && s.skill.description) || '').slice(0, 200) })),
|
|
374
|
-
knowledges,
|
|
345
|
+
knowledges, ui_bindings,
|
|
375
346
|
};
|
|
376
347
|
fs.writeFileSync(path.join(verDir, 'prompt.md'), manifest.prompt);
|
|
377
348
|
fs.writeFileSync(path.join(verDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
378
349
|
commitDownload(manifest);
|
|
379
|
-
audit({ actor: 'arm-library', agentId, action: 'download', via: 'rest', version, knowledges_local: knowledges.filter(k => k.file).length });
|
|
380
|
-
return manifest;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// 旧版 ARM 兼容:get_agent markdown 解析 + 知识 ID 经检索定位
|
|
384
|
-
async function downloadAgentMcp(agentId) {
|
|
385
|
-
const text = await mcpCall('get_agent', { id: agentId }, { timeout_ms: 25000 });
|
|
386
|
-
const section = (h) => (text.match(new RegExp(`## ${h}\\n([\\s\\S]*?)(\\n## |\\n*$)`)) || [])[1] || '';
|
|
387
|
-
const parseList = (s) => [...s.matchAll(/^- (.+?) (v[\d.]+)((\w+))/gm)].map(m => ({ ref: m[1].trim(), version: m[2], kind: m[3] }));
|
|
388
|
-
const name = (text.match(/^#\s+(.+?)\s+\(/m) || [])[1] || agentId;
|
|
389
|
-
const version = (text.match(/^#\s+.+?\s+\((v[\d.]+)\)/m) || [])[1] || 'v0.0.0';
|
|
390
|
-
const prompt = (text.match(/## Prompt\n([\s\S]*?)(\n## |\n*$)/) || [])[1]?.trim() || '';
|
|
391
|
-
const description = section('描述').trim();
|
|
392
|
-
const skills = parseList(section('Skills'));
|
|
393
|
-
const verDir = path.join(ARM_AGENTS_DIR, agentId, 'versions', version);
|
|
394
|
-
fs.mkdirSync(path.join(verDir, 'knowledges'), { recursive: true });
|
|
395
|
-
const knowledges = [];
|
|
396
|
-
for (const k of parseList(section('Knowledges'))) {
|
|
397
|
-
let file = null, note = '';
|
|
398
|
-
try { // 知识 ID 经检索定位(ARM 无按名直取接口),再拉全文。注意:知识正文自身含 "## " 内部标题,不可按标题切块——须按「标题→首个 [完整内容] 标记」成对匹配
|
|
399
|
-
const sres = await mcpCall('search_knowledges', { query: k.ref, limit: 5 }, { timeout_ms: 20000 });
|
|
400
|
-
const pairs = [...sres.matchAll(/^## (.+?)(?:(相似度 \d+%))?\n[\s\S]*?\[完整内容: get_knowledge id=([0-9a-f-]{36})\]/gm)];
|
|
401
|
-
const kid = pairs.find(m => m[1].trim() === k.ref)?.[2];
|
|
402
|
-
if (kid) {
|
|
403
|
-
const full = await mcpCall('get_knowledge', { id: kid }, { timeout_ms: 20000 });
|
|
404
|
-
file = `knowledges/${slug(k.ref)}.md`;
|
|
405
|
-
fs.writeFileSync(path.join(verDir, file), full);
|
|
406
|
-
} else note = 'ARM 检索未命中该知识,离线不可用';
|
|
407
|
-
} catch (e) { note = `知识拉取失败: ${String(e.message).slice(0, 80)}`; }
|
|
408
|
-
knowledges.push({ ...k, file, note });
|
|
409
|
-
}
|
|
410
|
-
const manifest = {
|
|
411
|
-
agent_id: agentId, name, version, description,
|
|
412
|
-
downloaded_at: new Date().toISOString(), source: armSourceTag(armMcpUrl()), prompt,
|
|
413
|
-
skills, knowledges,
|
|
414
|
-
};
|
|
415
|
-
fs.writeFileSync(path.join(verDir, 'prompt.md'), prompt);
|
|
416
|
-
fs.writeFileSync(path.join(verDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
417
|
-
commitDownload(manifest);
|
|
418
|
-
audit({ actor: 'arm-library', agentId, action: 'download', via: 'mcp', version, knowledges_local: knowledges.filter(k => k.file).length });
|
|
350
|
+
audit({ actor: 'arm-library', agentId, action: 'download', via: 'rest', version, knowledges_local: knowledges.filter(k => k.file).length, ui_bindings: ui_bindings.length });
|
|
419
351
|
return manifest;
|
|
420
352
|
}
|
|
421
353
|
|
|
422
354
|
// 同步:比对远端目录与本地库,产出 new/update/delisted 事件;顺带冲账离线经验 outbox
|
|
423
355
|
async function armSync() {
|
|
424
|
-
const remote = await armListAgents();
|
|
356
|
+
const remote = await armListAgents();
|
|
425
357
|
const reg = loadRegistry();
|
|
426
358
|
const remoteIds = new Set(remote.map(r => r.agent_id));
|
|
427
359
|
const events = { new: [], update: [], delisted: [] };
|
|
@@ -449,7 +381,7 @@ async function flushExperienceOutbox() {
|
|
|
449
381
|
const lines = fs.readFileSync(OUTBOX_FILE, 'utf8').split('\n').filter(Boolean);
|
|
450
382
|
const remain = []; let ok = 0;
|
|
451
383
|
for (const line of lines) {
|
|
452
|
-
try { const it = JSON.parse(line); await
|
|
384
|
+
try { const it = JSON.parse(line); await armCreateKnowledge(it.args); ok++; }
|
|
453
385
|
catch { remain.push(line); }
|
|
454
386
|
}
|
|
455
387
|
fs.writeFileSync(OUTBOX_FILE, remain.length ? remain.join('\n') + '\n' : '');
|
|
@@ -671,16 +603,15 @@ const TOOLS = {
|
|
|
671
603
|
const content = ['---', 'kind: experience', 'source: aims-platform-mvp', `agent: ${arm.name || agentId}@${arm.version || ''}`, 'scenario: 立库A区智能运维@mvp', '---', '', markdown].join('\n');
|
|
672
604
|
const args = { name: title, description: `MVP回流:${title}`, content, tags: ['MVP回流'] };
|
|
673
605
|
let r;
|
|
674
|
-
try { r = await
|
|
606
|
+
try { r = await armCreateKnowledge(args); }
|
|
675
607
|
catch (e) { // 离线:入 outbox 缓写,恢复连接后经「检查更新」自动回流
|
|
676
608
|
fs.appendFileSync(OUTBOX_FILE, JSON.stringify({ at: new Date().toISOString(), agentId, args }) + '\n');
|
|
677
609
|
audit({ actor: 'experience-reflow', target: 'outbox', title });
|
|
678
610
|
return `ARM 不可达(${String(e.message).slice(0, 60)}),经验已存本地待回流队列;恢复连接后在助手大厅点「检查更新」将自动回流。`;
|
|
679
611
|
}
|
|
680
|
-
|
|
681
|
-
metric({ kind: 'experience', agentId, title, knowledge_id: kid });
|
|
612
|
+
metric({ kind: 'experience', agentId, title, knowledge_id: r.id });
|
|
682
613
|
audit({ actor: 'experience-reflow', target: 'ARM', title });
|
|
683
|
-
return `经验已沉淀到 ARM
|
|
614
|
+
return `经验已沉淀到 ARM 知识库:${r.name}(id: ${r.id}${r.shareUrl ? `,${r.shareUrl}` : ''})`;
|
|
684
615
|
},
|
|
685
616
|
},
|
|
686
617
|
};
|
|
@@ -747,10 +678,15 @@ function openDb(file) {
|
|
|
747
678
|
const db = new DatabaseSync(path.join(DATA_DIR, file));
|
|
748
679
|
db.exec('CREATE TABLE IF NOT EXISTS docs (collection TEXT NOT NULL, id TEXT PRIMARY KEY, data TEXT NOT NULL, created TEXT, updated TEXT)');
|
|
749
680
|
db.exec('CREATE INDEX IF NOT EXISTS idx_docs_collection ON docs(collection)');
|
|
681
|
+
db.exec('CREATE TABLE IF NOT EXISTS _meta (k TEXT PRIMARY KEY, v TEXT NOT NULL)'); // 变更版本计数(面板订阅用,08 契约 §4)
|
|
750
682
|
_dbs.set(file, db);
|
|
751
683
|
}
|
|
752
684
|
return _dbs.get(file);
|
|
753
685
|
}
|
|
686
|
+
// 记录写入后自增 ver:<collection>(持久化,重启不归零)——REST 与助手 data_write 同源,面板 watch 据此联动
|
|
687
|
+
function bumpVer(db, coll) {
|
|
688
|
+
db.prepare("INSERT INTO _meta (k, v) VALUES (?, '1') ON CONFLICT(k) DO UPDATE SET v = CAST(v AS INTEGER) + 1").run(`ver:${coll}`);
|
|
689
|
+
}
|
|
754
690
|
const DB = openDb('store.db'); // 兼容旧引用(迁移后即空)
|
|
755
691
|
// 作用域语义:agent=助手私有(默认,存归属助手×工作区的独立库);shared=跨助手共享(存共享库)
|
|
756
692
|
const scopeOf = (m) => (m.scope === 'shared' || m.scope === 'global') ? 'shared' : 'agent'; // legacy global→shared, workbench→agent
|
|
@@ -800,6 +736,7 @@ function insertDoc(m, doc, wsId = 'default') {
|
|
|
800
736
|
const now = new Date().toISOString();
|
|
801
737
|
dbFor(m, wsId).prepare('INSERT OR REPLACE INTO docs (collection, id, data, created, updated) VALUES (?,?,?,?,?)')
|
|
802
738
|
.run(collKey(m), id, JSON.stringify(doc), now, now);
|
|
739
|
+
bumpVer(dbFor(m, wsId), collKey(m));
|
|
803
740
|
return { ...doc, _id: id, _created: now, _updated: now };
|
|
804
741
|
}
|
|
805
742
|
function updateDoc(m, id, patch, wsId = 'default') {
|
|
@@ -808,11 +745,13 @@ function updateDoc(m, id, patch, wsId = 'default') {
|
|
|
808
745
|
const merged = { ...JSON.parse(row.data), ...patch };
|
|
809
746
|
validateDoc(m, merged);
|
|
810
747
|
dbFor(m, wsId).prepare('UPDATE docs SET data = ?, updated = ? WHERE collection = ? AND id = ?').run(JSON.stringify(merged), new Date().toISOString(), collKey(m), id);
|
|
748
|
+
bumpVer(dbFor(m, wsId), collKey(m));
|
|
811
749
|
return merged;
|
|
812
750
|
}
|
|
813
751
|
function deleteDoc(m, id, wsId = 'default') {
|
|
814
752
|
const r = dbFor(m, wsId).prepare('DELETE FROM docs WHERE collection = ? AND id = ?').run(collKey(m), id);
|
|
815
753
|
if (!r.changes) throw new Error(`记录 ${id} 不存在`);
|
|
754
|
+
bumpVer(dbFor(m, wsId), collKey(m));
|
|
816
755
|
}
|
|
817
756
|
|
|
818
757
|
// ── 一次性迁移:旧单库 → 按助手分库(按归属切分记录)──
|
|
@@ -880,8 +819,8 @@ function deleteDoc(m, id, wsId = 'default') {
|
|
|
880
819
|
// 占位清理:旧类型残留保护
|
|
881
820
|
(function migrateBuiltinPanels() {
|
|
882
821
|
const MAP = {
|
|
883
|
-
realtime_list: { plugin: 'system-alarm-stream', slot: 'left', keep: ['
|
|
884
|
-
status_grid: { plugin: 'system-device-grid', slot: 'center', keep: ['
|
|
822
|
+
realtime_list: { plugin: 'system-alarm-stream', slot: 'left', keep: ['device_type'] },
|
|
823
|
+
status_grid: { plugin: 'system-device-grid', slot: 'center', keep: ['device_type', 'context_inject'] },
|
|
885
824
|
kpi_strip: { plugin: 'system-kpi-strip', slot: 'center', keep: [] },
|
|
886
825
|
inspection_report: { plugin: 'system-inspection-report', slot: 'left', keep: [] },
|
|
887
826
|
action_group: { plugin: 'system-action-group', slot: 'right', keep: ['actions'] },
|
|
@@ -1036,22 +975,233 @@ function pluginCenterData() {
|
|
|
1036
975
|
system: systemPluginStatus().map((s) => ({ ...s, usage: usage[`system:${s.id}`] || [] })),
|
|
1037
976
|
library: listPanelPlugins().map((pl) => {
|
|
1038
977
|
const full = loadPanelPlugin(pl.id);
|
|
1039
|
-
return { ...pl, status: full?.status || 'active', usage: usage[`plugin:${pl.id}`] || [], lastCheck: full?.lastCheck || null };
|
|
978
|
+
return { ...pl, status: full?.status || 'active', usage: usage[`plugin:${pl.id}`] || [], lastCheck: full?.lastCheck || null, arm_ref: full?.arm_ref || null };
|
|
1040
979
|
}),
|
|
1041
980
|
};
|
|
1042
981
|
}
|
|
1043
982
|
// 代理域名白名单:从在库插件代码中提取其声明访问的主机(含 deprecated,保守起见全量)
|
|
983
|
+
// 存主机名(剥离端口)——检查侧用 URL.hostname 比对;带端口声明的 host 此前永远匹配不上(45.36 实例排查)
|
|
1044
984
|
function proxyAllowedHosts() {
|
|
1045
985
|
const hosts = new Set(['localhost', '127.0.0.1']);
|
|
1046
986
|
for (const f of fs.readdirSync(PANELS_DIR).filter((f) => f.endsWith('.json'))) {
|
|
1047
987
|
try {
|
|
1048
988
|
const code = JSON.parse(fs.readFileSync(path.join(PANELS_DIR, f), 'utf8')).code || '';
|
|
1049
|
-
for (const m of code.matchAll(/https?:\/\/([a-z0-9.-]+(?::\d+)
|
|
989
|
+
for (const m of code.matchAll(/https?:\/\/([a-z0-9.-]+)(?::\d+)?/gi)) hosts.add(m[1].toLowerCase());
|
|
1050
990
|
} catch { }
|
|
1051
991
|
}
|
|
1052
992
|
return hosts;
|
|
1053
993
|
}
|
|
1054
994
|
|
|
995
|
+
// ── UI 资产分发(ARM 三资产模型:UIAsset = 面板/工作台的版本化分发,docs/08 §7、docs/09)──
|
|
996
|
+
const zlib = require('node:zlib');
|
|
997
|
+
// ZIP 读写器(零依赖:CRC32 查表 + local header + central directory + EOCD,deflate 走 zlib 内置)
|
|
998
|
+
const CRC_TABLE = (() => { const t = new Int32Array(256); for (let n = 0; n < 256; n++) { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? (0xEDB88320 ^ (c >>> 1)) : c >>> 1; t[n] = c; } return t; })();
|
|
999
|
+
function crc32(buf) { let c = -1; for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xFF] ^ (c >>> 8); return (c ^ -1) >>> 0; }
|
|
1000
|
+
const dosTime = (d) => ((d.getHours() << 11) | (d.getMinutes() << 5) | (d.getSeconds() >> 1)) & 0xFFFF;
|
|
1001
|
+
const dosDate = (d) => (((d.getFullYear() - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate()) & 0xFFFF;
|
|
1002
|
+
function zipFiles(files) { // files: [{path, data: Buffer|string}] → Buffer(兼容系统 unzip:deflate/stored 自适应 + UTF-8 名)
|
|
1003
|
+
const parts = [], central = []; let offset = 0; const now = new Date();
|
|
1004
|
+
for (const f of files) {
|
|
1005
|
+
const name = Buffer.from(f.path, 'utf8');
|
|
1006
|
+
const raw = Buffer.isBuffer(f.data) ? f.data : Buffer.from(String(f.data), 'utf8');
|
|
1007
|
+
const comp = zlib.deflateRawSync(raw, { level: 9 });
|
|
1008
|
+
const useDeflate = comp.length < raw.length;
|
|
1009
|
+
const data = useDeflate ? comp : raw;
|
|
1010
|
+
const crc = crc32(raw);
|
|
1011
|
+
const lh = Buffer.alloc(30);
|
|
1012
|
+
lh.writeUInt32LE(0x04034b50, 0); lh.writeUInt16LE(20, 4); lh.writeUInt16LE(0x0800, 6);
|
|
1013
|
+
lh.writeUInt16LE(useDeflate ? 8 : 0, 8); lh.writeUInt16LE(dosTime(now), 10); lh.writeUInt16LE(dosDate(now), 12);
|
|
1014
|
+
lh.writeUInt32LE(crc, 14); lh.writeUInt32LE(data.length, 18); lh.writeUInt32LE(raw.length, 22);
|
|
1015
|
+
lh.writeUInt16LE(name.length, 26); lh.writeUInt16LE(0, 28);
|
|
1016
|
+
parts.push(lh, name, data);
|
|
1017
|
+
const ch = Buffer.alloc(46);
|
|
1018
|
+
ch.writeUInt32LE(0x02014b50, 0); ch.writeUInt16LE(20, 4); ch.writeUInt16LE(20, 6); ch.writeUInt16LE(0x0800, 8);
|
|
1019
|
+
ch.writeUInt16LE(useDeflate ? 8 : 0, 10); ch.writeUInt16LE(dosTime(now), 12); ch.writeUInt16LE(dosDate(now), 14);
|
|
1020
|
+
ch.writeUInt32LE(crc, 16); ch.writeUInt32LE(data.length, 20); ch.writeUInt32LE(raw.length, 24);
|
|
1021
|
+
ch.writeUInt16LE(name.length, 28);
|
|
1022
|
+
ch.writeUInt32LE(offset, 42);
|
|
1023
|
+
central.push(Buffer.concat([ch, name]));
|
|
1024
|
+
offset += 30 + name.length + data.length;
|
|
1025
|
+
}
|
|
1026
|
+
const cd = Buffer.concat(central);
|
|
1027
|
+
const eocd = Buffer.alloc(22);
|
|
1028
|
+
eocd.writeUInt32LE(0x06054b50, 0); eocd.writeUInt16LE(files.length, 8); eocd.writeUInt16LE(files.length, 10);
|
|
1029
|
+
eocd.writeUInt32LE(cd.length, 12); eocd.writeUInt32LE(offset, 16);
|
|
1030
|
+
return Buffer.concat([...parts, cd, eocd]);
|
|
1031
|
+
}
|
|
1032
|
+
function unzipFiles(buf) { // → [{path, data: Buffer}](支持 stored/deflate)
|
|
1033
|
+
let eocd = -1;
|
|
1034
|
+
for (let i = buf.length - 22; i >= Math.max(0, buf.length - 22 - 65536); i--) {
|
|
1035
|
+
if (buf.readUInt32LE(i) === 0x06054b50) { eocd = i; break; }
|
|
1036
|
+
}
|
|
1037
|
+
if (eocd < 0) throw new Error('ZIP 解析失败:未找到目录尾');
|
|
1038
|
+
const count = buf.readUInt16LE(eocd + 10);
|
|
1039
|
+
let p = buf.readUInt32LE(eocd + 16);
|
|
1040
|
+
const out = [];
|
|
1041
|
+
for (let n = 0; n < count; n++) {
|
|
1042
|
+
if (buf.readUInt32LE(p) !== 0x02014b50) throw new Error('ZIP 解析失败:目录损坏');
|
|
1043
|
+
const method = buf.readUInt16LE(p + 10);
|
|
1044
|
+
const compSize = buf.readUInt32LE(p + 20);
|
|
1045
|
+
const nameLen = buf.readUInt16LE(p + 28), extraLen = buf.readUInt16LE(p + 30), commentLen = buf.readUInt16LE(p + 32);
|
|
1046
|
+
const lho = buf.readUInt32LE(p + 42);
|
|
1047
|
+
const zipPath = buf.slice(p + 46, p + 46 + nameLen).toString('utf8');
|
|
1048
|
+
const lnameLen = buf.readUInt16LE(lho + 26), lextraLen = buf.readUInt16LE(lho + 28);
|
|
1049
|
+
const dStart = lho + 30 + lnameLen + lextraLen;
|
|
1050
|
+
const comp = buf.slice(dStart, dStart + compSize);
|
|
1051
|
+
if (!zipPath.endsWith('/')) out.push({ path: zipPath, data: method === 8 ? zlib.inflateRawSync(comp) : Buffer.from(comp) });
|
|
1052
|
+
p += 46 + nameLen + extraLen + commentLen;
|
|
1053
|
+
}
|
|
1054
|
+
return out;
|
|
1055
|
+
}
|
|
1056
|
+
// ARM multipart 上传(FormData+Blob,零依赖 fetch 原生支持;复用 armRest 的 token 体系与错误提示)
|
|
1057
|
+
async function armUpload(pathname, formData, timeout_ms = 30000) {
|
|
1058
|
+
const token = armApiToken();
|
|
1059
|
+
const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), timeout_ms);
|
|
1060
|
+
try {
|
|
1061
|
+
const res = await fetch(armApiBase() + pathname, { method: 'POST', headers: token ? { Authorization: `Bearer ${token}` } : {}, body: formData, signal: ctrl.signal });
|
|
1062
|
+
const data = await res.json().catch(() => ({}));
|
|
1063
|
+
if (!res.ok || data.ok === false) {
|
|
1064
|
+
const noAuth = res.status === 401 || res.status === 403;
|
|
1065
|
+
throw new Error(`ARM 上传 ${pathname}: ${data.msg || res.status}${noAuth ? '——令牌未配置或无权限:到「系统设置 · ARM 源」配置访问令牌(arm_pat_…)' : ''}`);
|
|
1066
|
+
}
|
|
1067
|
+
return data.data;
|
|
1068
|
+
} finally { clearTimeout(t); }
|
|
1069
|
+
}
|
|
1070
|
+
// ARM 二进制下载(资产包 ZIP):返回 Buffer + X-File-Hash(完整性校验)
|
|
1071
|
+
async function armDownload(pathname, timeout_ms = 30000) {
|
|
1072
|
+
const token = armApiToken();
|
|
1073
|
+
const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), timeout_ms);
|
|
1074
|
+
try {
|
|
1075
|
+
const res = await fetch(armApiBase() + pathname, { headers: token ? { Authorization: `Bearer ${token}` } : {}, signal: ctrl.signal });
|
|
1076
|
+
if (!res.ok) {
|
|
1077
|
+
const j = await res.json().catch(() => ({}));
|
|
1078
|
+
const noAuth = res.status === 401 || res.status === 403;
|
|
1079
|
+
throw new Error(`ARM 下载 ${pathname.split('?')[0]}: ${j.msg || res.status}${noAuth ? '——令牌未配置或无权限:到「系统设置 · ARM 源」配置访问令牌(arm_pat_…)' : ''}`);
|
|
1080
|
+
}
|
|
1081
|
+
return { buf: Buffer.from(await res.arrayBuffer()), hash: res.headers.get('x-file-hash') || '' };
|
|
1082
|
+
} finally { clearTimeout(t); }
|
|
1083
|
+
}
|
|
1084
|
+
const assetSlug = (s) => String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40) || 'asset';
|
|
1085
|
+
// 组包:panel=单插件三件套;workbench=线上 DSL(无则草稿)+依赖插件+数据集 schema+requires 声明(08 契约 §7)
|
|
1086
|
+
function buildAssetBundle(type, key, nameOverride) {
|
|
1087
|
+
const files = [];
|
|
1088
|
+
const requires = { apis: [], actions: [], collections: [] };
|
|
1089
|
+
const plugins = []; const collections = [];
|
|
1090
|
+
const addPlugin = (id) => {
|
|
1091
|
+
if (plugins.some((p) => p.id === id)) return;
|
|
1092
|
+
const plugin = loadPanelPlugin(id);
|
|
1093
|
+
if (!plugin) return;
|
|
1094
|
+
plugins.push(plugin);
|
|
1095
|
+
files.push({ path: `panels/${plugin.id}.json`, data: JSON.stringify({ id: plugin.id, name: plugin.name, description: plugin.description, code: plugin.code }, null, 2) });
|
|
1096
|
+
for (const m of (plugin.code || '').matchAll(/https?:\/\/([a-z0-9.-]+)(?::\d+)?/gi)) { // 外部依赖声明:代码即事实(与 proxyAllowedHosts 同源正则),分享时提取
|
|
1097
|
+
const host = m[1].toLowerCase();
|
|
1098
|
+
if (host !== 'localhost' && host !== '127.0.0.1' && !requires.apis.some((x) => x.host === host)) requires.apis.push({ host, usage: `插件 ${plugin.id} 跨域取数`, via: 'proxy' });
|
|
1099
|
+
}
|
|
1100
|
+
for (const m of (plugin.code || '').matchAll(/store\.(?:list|create|update|remove|watch)\(\s*['"`]([a-z0-9_-]+)['"`]/g)) {
|
|
1101
|
+
const manifest = loadCollection(m[1]);
|
|
1102
|
+
if (manifest && !collections.some((c) => c.name === manifest.name)) collections.push(manifest);
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
let wbName = '';
|
|
1106
|
+
if (type === 'panel') {
|
|
1107
|
+
addPlugin(key);
|
|
1108
|
+
if (!plugins.length) throw new Error(`插件 ${key} 不存在`);
|
|
1109
|
+
} else {
|
|
1110
|
+
const dsl = loadWorkbench(key) || loadDraft(key);
|
|
1111
|
+
if (!dsl) throw new Error(`助手 ${key} 无工作台(线上与草稿均为空)`);
|
|
1112
|
+
wbName = dsl.workbench || '';
|
|
1113
|
+
for (const p of dsl.panels || []) {
|
|
1114
|
+
if (p.type === 'custom_panel' && p.panel) addPlugin(p.panel);
|
|
1115
|
+
for (const a of (p.params && p.params.actions) || []) { // 交互脚本全文明示(凭据风险人审,红线③)
|
|
1116
|
+
requires.actions.push({ panel: p.id, id: a.id, label: a.label, exec: String(a.exec), has_credentials: /pass|-p\s|pwd|token|secret|apikey|key=/i.test(String(a.exec)) });
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
for (const m of loadCollections()) { // 归属助手的数据集 schema(数据永不入包,红线②)
|
|
1120
|
+
if (m.owner_agent === key && !collections.some((c) => c.name === m.name)) collections.push(m);
|
|
1121
|
+
}
|
|
1122
|
+
const wb = JSON.parse(JSON.stringify(dsl));
|
|
1123
|
+
delete wb.owner_agent; wb.version = 'v1';
|
|
1124
|
+
files.push({ path: 'workbench.json', data: JSON.stringify(wb, null, 2) });
|
|
1125
|
+
}
|
|
1126
|
+
for (const m of collections) {
|
|
1127
|
+
files.push({ path: `collections/${m.name}.json`, data: JSON.stringify({ name: m.name, title: m.title, description: m.description, schema: m.schema, required: m.required || [], scope: m.scope === 'shared' ? 'shared' : 'agent' }, null, 2) });
|
|
1128
|
+
}
|
|
1129
|
+
const first = type === 'panel' ? plugins[0] : null;
|
|
1130
|
+
const dep = type === 'workbench' ? loadDeployment(key) : null;
|
|
1131
|
+
const wbSlug = assetSlug(dep?.name || wbName || '');
|
|
1132
|
+
// 中文名 slug 化后为空 → 以 agentId 前 8 位兜底(全局唯一锚)
|
|
1133
|
+
const name = nameOverride || (type === 'panel' ? `aims-panel-${first.id}` : `aims-workbench-${wbSlug !== 'asset' ? wbSlug : String(key).slice(0, 8)}`);
|
|
1134
|
+
const manifest = {
|
|
1135
|
+
name, description: (first ? first.description : `${dep?.name || wbName}的工作台模板`).slice(0, 500),
|
|
1136
|
+
asset_type: type, version: type === 'panel' ? first.version : 'v1', source: 'aims-workstation', shared_at: new Date().toISOString(),
|
|
1137
|
+
provides: { panels: plugins.map((p) => p.id), collections: collections.map((c) => c.name), workbench: type === 'workbench' },
|
|
1138
|
+
requires, compatibility: { 'aims-workstation': '>=0.1.5' },
|
|
1139
|
+
};
|
|
1140
|
+
files.push({ path: 'asset.json', data: JSON.stringify(manifest, null, 2) });
|
|
1141
|
+
files.push({
|
|
1142
|
+
path: 'SKILL.md',
|
|
1143
|
+
data: `---\nname: ${name}\ndescription: ${manifest.description.replace(/\n/g, ' ').slice(0, 200)}\n---\n\n# ${name}\n\nAIMS 工作台资产(${type === 'panel' ? '面板插件' : '工作台模板'})。\n\n## 包含\n\n- 面板插件:${plugins.map((p) => `${p.name}(${p.id}@${p.version})`).join('、') || '无'}\n- 数据集 schema:${collections.map((c) => c.name).join('、') || '无'}${type === 'workbench' ? '\n- 工作台 DSL:workbench.json' : ''}\n${requires.apis.length ? `\n## 外部 API 依赖(requires.apis)\n\n${requires.apis.map((a) => `- ${a.host}:${a.usage}`).join('\n')}` : ''}${requires.actions.length ? `\n## 交互脚本(requires.actions,含真实 shell 命令)\n\n${requires.actions.map((a) => `- [${a.panel}/${a.id}] ${a.label}:\`${a.exec.slice(0, 160)}\`${a.has_credentials ? ' ⚠️疑似含凭据' : ''}`).join('\n')}` : ''}\n\n## 安装\n\nAIMS 平台「插件中心 → 从 ARM 安装」按名搜索安装;面板代码过平台校验器后入库。\n`,
|
|
1144
|
+
});
|
|
1145
|
+
return { name, manifest, files };
|
|
1146
|
+
}
|
|
1147
|
+
// 解包安装:panels 过校验器入库;collections 同名保留本地仅新增缺失;workbench 按模式落草稿(人审)或直接线上(下载物化)
|
|
1148
|
+
function installAssetFiles(files, { agentId, mode = 'draft' } = {}) {
|
|
1149
|
+
const manifest = JSON.parse(String(files.find((f) => f.path === 'asset.json' || f.path.endsWith('/asset.json'))?.data || 'null'));
|
|
1150
|
+
if (!manifest || !manifest.name) throw new Error('资产包缺少 asset.json');
|
|
1151
|
+
const armRefOf = (hash) => ({ skill_name: manifest.name, version: manifest.version || null, file_hash: hash, shared_at: manifest.shared_at || null });
|
|
1152
|
+
const result = { name: manifest.name, asset_type: manifest.asset_type, panels: [], collections: [], workbench: null, skipped_collections: [] };
|
|
1153
|
+
for (const f of files.filter((x) => /^panels\/[a-z0-9-]+\.json$/.test(x.path))) {
|
|
1154
|
+
const p = JSON.parse(String(f.data));
|
|
1155
|
+
const saved = savePanelPlugin({ id: p.id, name: p.name, description: p.description, code: p.code, author: 'arm' });
|
|
1156
|
+
// savePanelPlugin 已写盘,补 arm_ref 后再写一次(不重复升版本)
|
|
1157
|
+
const full = loadPanelPlugin(p.id);
|
|
1158
|
+
Object.assign(full, { arm_ref: armRefOf(null) });
|
|
1159
|
+
fs.writeFileSync(path.join(PANELS_DIR, `${p.id}.json`), JSON.stringify(full, null, 2));
|
|
1160
|
+
result.panels.push({ id: saved.id, version: saved.version });
|
|
1161
|
+
}
|
|
1162
|
+
if (agentId) {
|
|
1163
|
+
for (const f of files.filter((x) => /^collections\/[a-z0-9_-]+\.json$/.test(x.path))) {
|
|
1164
|
+
const m = JSON.parse(String(f.data));
|
|
1165
|
+
if (loadCollection(m.name)) { result.skipped_collections.push(m.name); continue; } // 冲突保留本地(数据在现场,不静默换表)
|
|
1166
|
+
saveCollection({ name: m.name, title: m.title, description: m.description, schema: m.schema, required: m.required || [], scope: m.scope === 'shared' ? 'shared' : 'agent', owner_agent: m.scope === 'shared' ? undefined : agentId });
|
|
1167
|
+
result.collections.push(m.name);
|
|
1168
|
+
}
|
|
1169
|
+
const wbFile = files.find((x) => x.path === 'workbench.json');
|
|
1170
|
+
if (wbFile) {
|
|
1171
|
+
const dsl = JSON.parse(String(wbFile.data));
|
|
1172
|
+
dsl.owner_agent = agentId;
|
|
1173
|
+
if (mode === 'live') {
|
|
1174
|
+
dsl.version = `v${Math.max(0, ...workbenchVersions(agentId).map((x) => +String(x.version || '').replace(/^v/, '') || 0)) + 1}`;
|
|
1175
|
+
saveWorkbench(agentId, dsl); // 下载物化:直接线上(三道信任关后开箱即用,版本前进可回滚)
|
|
1176
|
+
} else {
|
|
1177
|
+
saveDraft(agentId, dsl); // 手动安装:进草稿,人审发布
|
|
1178
|
+
}
|
|
1179
|
+
const target = mode === 'live' ? path.join(WB_DIR, `${agentId}.json`) : draftPath(agentId);
|
|
1180
|
+
const cur = JSON.parse(fs.readFileSync(target, 'utf8'));
|
|
1181
|
+
cur.arm_ref = armRefOf(null);
|
|
1182
|
+
fs.writeFileSync(target, JSON.stringify(cur, null, 2));
|
|
1183
|
+
result.workbench = { mode, version: cur.version };
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
audit({ actor: 'asset-install', target: 'ARM', name: manifest.name, ...result });
|
|
1187
|
+
return result;
|
|
1188
|
+
}
|
|
1189
|
+
// 本地 arm_ref 索引:插件库 + 各工作台 DSL(列表页 installed/upgrade_available 判定)
|
|
1190
|
+
function localArmRefs() {
|
|
1191
|
+
const refs = {};
|
|
1192
|
+
for (const pl of listPanelPlugins()) {
|
|
1193
|
+
const full = loadPanelPlugin(pl.id);
|
|
1194
|
+
if (full?.arm_ref?.skill_name) (refs[full.arm_ref.skill_name] = refs[full.arm_ref.skill_name] || []).push({ kind: 'plugin', id: pl.id, file_hash: full.arm_ref.file_hash });
|
|
1195
|
+
}
|
|
1196
|
+
for (const f of fs.readdirSync(WB_DIR).filter((f) => f.endsWith('.json'))) {
|
|
1197
|
+
try {
|
|
1198
|
+
const dsl = JSON.parse(fs.readFileSync(path.join(WB_DIR, f), 'utf8'));
|
|
1199
|
+
if (dsl.arm_ref?.skill_name) (refs[dsl.arm_ref.skill_name] = refs[dsl.arm_ref.skill_name] || []).push({ kind: 'workbench', id: dsl.owner_agent || f.replace(/\.(draft\.)?json$/, ''), file_hash: dsl.arm_ref.file_hash });
|
|
1200
|
+
} catch { }
|
|
1201
|
+
}
|
|
1202
|
+
return refs;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1055
1205
|
// ── 工作台工程师 Agent(对话式开发工作台的 skill:工具即扩展点)──
|
|
1056
1206
|
const DEV_TOOLS = {
|
|
1057
1207
|
list_workbench: {
|
|
@@ -1154,6 +1304,25 @@ const DEV_TOOLS = {
|
|
|
1154
1304
|
const devCtx = { agentId: null };
|
|
1155
1305
|
const devSessions = new Map();
|
|
1156
1306
|
|
|
1307
|
+
// 定制会话上下文裁剪:前端 sessionId 固定为 dev-<agentId前缀>,会话跨请求长存——不裁剪会无限增长,
|
|
1308
|
+
// 最终超过模型上下文窗口被网关整单拒绝(2026-10-21 海立实例"发消息没反应"根因)。
|
|
1309
|
+
// 超预算从最旧轮次整轮丢弃;下刀点只选 user 消息边界,剩余前缀不会出现孤儿 toolResult(转换层要求其紧跟 assistant 的 toolCall);当前轮永不裁。
|
|
1310
|
+
const DEV_CTX_CHAR_BUDGET = 150_000; // 字符预算:混合中英/代码转录按 ~0.4 token/char 估 ≈ 60K token,对 128K 窗口留足余量
|
|
1311
|
+
function trimDevContext(ctx) {
|
|
1312
|
+
const msgs = ctx.messages;
|
|
1313
|
+
const size = () => msgs.reduce((n, m) => n + JSON.stringify(m.content ?? '').length, 0);
|
|
1314
|
+
if (size() <= DEV_CTX_CHAR_BUDGET) return 0;
|
|
1315
|
+
const lastUser = msgs.length - 1 - [...msgs].reverse().findIndex((m) => m.role === 'user');
|
|
1316
|
+
let dropped = 0;
|
|
1317
|
+
while (size() > DEV_CTX_CHAR_BUDGET) {
|
|
1318
|
+
const cut = msgs.slice(1, lastUser - dropped).findIndex((m) => m.role === 'user');
|
|
1319
|
+
if (cut < 0) break; // 当前轮之前再无完整轮次可丢(单轮就超预算只能放行,交由模型侧报错)
|
|
1320
|
+
dropped += cut + 1;
|
|
1321
|
+
msgs.splice(0, cut + 1);
|
|
1322
|
+
}
|
|
1323
|
+
return dropped;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1157
1326
|
function devSystemPrompt() {
|
|
1158
1327
|
const live = loadWorkbench(devCtx.agentId);
|
|
1159
1328
|
const plugins = listPanelPlugins();
|
|
@@ -1235,12 +1404,24 @@ async function devChatLoop(session, emit, signal = null) {
|
|
|
1235
1404
|
if (interrupted()) { emit('interrupted', { at: `第 ${round + 1} 轮` }); return { reply: '(已被用户中断)', events }; }
|
|
1236
1405
|
emit('status', { text: `第 ${round + 1} 轮` });
|
|
1237
1406
|
const stream = models.stream(model, session.context, signal ? { signal } : undefined);
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1407
|
+
let final = null;
|
|
1408
|
+
try {
|
|
1409
|
+
for await (const ev of stream) {
|
|
1410
|
+
if (interrupted()) break;
|
|
1411
|
+
if (ev.type === 'text_delta') emit('text_delta', { delta: ev.delta });
|
|
1412
|
+
else if (ev.type === 'toolcall_end') emit('status', { text: `调用工具 ${ev.toolCall?.name || ''}` });
|
|
1413
|
+
else if (ev.type === 'error') {
|
|
1414
|
+
if (interrupted()) break; // 中断引发的流错误按中断处理,不报错
|
|
1415
|
+
throw new Error(`LLM 流错误: ${ev.error?.errorMessage || JSON.stringify(ev.error).slice(0, 200)}`);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
final = await stream.result();
|
|
1419
|
+
// pi-ai 把认证/请求失败表现为 stopReason=error 而非异常(同 llmProbe 判定)——不检查会被吞成静默空回复
|
|
1420
|
+
if (final?.stopReason === 'error') throw new Error(`LLM 请求失败: ${final.error?.errorMessage || JSON.stringify(final.error || {}).slice(0, 200) || final.stopReason}`);
|
|
1421
|
+
} catch (e) {
|
|
1422
|
+
if (!interrupted()) throw e; // 真实错误上抛给路由 catch → SSE error → 前端 toast;中断路径用占位消息兜底
|
|
1242
1423
|
}
|
|
1243
|
-
|
|
1424
|
+
if (!final) final = { role: 'assistant', content: [], timestamp: Date.now() };
|
|
1244
1425
|
session.context.messages.push(final);
|
|
1245
1426
|
const calls = (final.content || []).filter(b => b.type === 'toolCall');
|
|
1246
1427
|
const text = (final.content || []).filter(b => b.type === 'text').map(b => b.text).join('');
|
|
@@ -1445,7 +1626,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1445
1626
|
if (pathname === '/api/health') return json(res, 200, { ok: true, auth_required: true }); // 前端据此弹登录门
|
|
1446
1627
|
return json(res, 401, { error: '需要访问令牌', code: 'AUTH_REQUIRED' });
|
|
1447
1628
|
}
|
|
1448
|
-
if (req.method === 'GET' && pathname === '/api/health') return json(res, 200, { ok: true, model: model?.id, arm:
|
|
1629
|
+
if (req.method === 'GET' && pathname === '/api/health') return json(res, 200, { ok: true, model: model?.id, arm: armApiBase(), pat: !!ARM_PAT, local_agents: loadRegistry().agents.length });
|
|
1449
1630
|
|
|
1450
1631
|
// ── ARM 连接与同步(07:大厅双视图 = 本地助手库 ∪ 远端目录)──
|
|
1451
1632
|
if (req.method === 'GET' && pathname === '/api/settings') {
|
|
@@ -1454,7 +1635,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
1454
1635
|
return json(res, 200, {
|
|
1455
1636
|
arm: {
|
|
1456
1637
|
api_base: armApiBase(), api_base_runtime: !!cfg.arm_api_base, default_base: ARM_DEFAULT_BASE,
|
|
1457
|
-
mcp_url: armMcpUrl(), mcp_explicit: !!cfg.arm_mcp_url,
|
|
1458
1638
|
token_set: !!t, token_masked: t ? `${t.slice(0, 10)}…${t.slice(-4)}` : '',
|
|
1459
1639
|
token_source: cfg.arm_token ? 'runtime' : (env.ARM_API_TOKEN ? 'env' : (ARM_PAT ? 'pat' : 'none')),
|
|
1460
1640
|
},
|
|
@@ -1470,7 +1650,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1470
1650
|
});
|
|
1471
1651
|
}
|
|
1472
1652
|
if (req.method === 'PUT' && pathname === '/api/settings') {
|
|
1473
|
-
const { arm_api_base,
|
|
1653
|
+
const { arm_api_base, arm_token, llm_base_url, llm_api_key, llm_model, scene_profile, access_token, guard } = await body(req);
|
|
1474
1654
|
// 守护栏维护:deny 正则可编译 / ro 须为单命令名;保存即热生效(classifyExec 每次调用现读配置);删减危险规则记入审计
|
|
1475
1655
|
let guardAudit = null;
|
|
1476
1656
|
if (guard !== undefined) {
|
|
@@ -1501,11 +1681,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
1501
1681
|
if (u && !/^https?:\/\/[^\s/]+/.test(u)) return json(res, 400, { error: `站点地址须为 http(s) URL:${u}` });
|
|
1502
1682
|
cfg.arm_api_base = u;
|
|
1503
1683
|
}
|
|
1504
|
-
if (arm_mcp_url !== undefined) {
|
|
1505
|
-
const u = normUrl(arm_mcp_url);
|
|
1506
|
-
if (u && !/^https?:\/\/[^\s/]+/.test(u)) return json(res, 400, { error: `MCP 端点须为 http(s) URL:${u}` });
|
|
1507
|
-
cfg.arm_mcp_url = u;
|
|
1508
|
-
}
|
|
1509
1684
|
if (arm_token !== undefined) cfg.arm_token = String(arm_token || '').trim(); // 置空 = 回落 env/PAT
|
|
1510
1685
|
if (llm_base_url !== undefined) {
|
|
1511
1686
|
const u = normUrl(llm_base_url);
|
|
@@ -1531,10 +1706,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
1531
1706
|
try { llm_probe = await llmProbe(); } catch (e) { llm_probe = { online: false, error: String(e.message).slice(0, 150) }; }
|
|
1532
1707
|
}
|
|
1533
1708
|
saveCfg();
|
|
1534
|
-
audit({ actor: 'settings', action: 'save', arm_api_base: cfg.arm_api_base,
|
|
1709
|
+
audit({ actor: 'settings', action: 'save', arm_api_base: cfg.arm_api_base, llm_model: cfg.llm_model, llm_base_changed: llm_base_url !== undefined, token_changed: arm_token !== undefined || llm_api_key !== undefined, scene_profile_changed: sceneChanged, scene_profile_len: sceneProfile().length, auth_changed: authChanged, guard_changed: guardAudit });
|
|
1535
1710
|
// ARM 保存后即时探测(失败不回滚:允许先配置后启动 ARM)
|
|
1536
1711
|
let probe = { online: false, error: '' };
|
|
1537
|
-
if ([arm_api_base,
|
|
1712
|
+
if ([arm_api_base, arm_token].some((v) => v !== undefined)) {
|
|
1538
1713
|
try {
|
|
1539
1714
|
const t0 = Date.now();
|
|
1540
1715
|
await armRest('/api/v1/agents?page=1&pageSize=1', {}, 6000);
|
|
@@ -1542,7 +1717,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1542
1717
|
} catch (e) { probe = { online: false, error: String(e.message).slice(0, 120) }; }
|
|
1543
1718
|
}
|
|
1544
1719
|
const notes = [];
|
|
1545
|
-
if (probe.error) notes.push('ARM
|
|
1720
|
+
if (probe.error) notes.push('ARM 当前源不可达(目录/检索将回落本地库,源恢复后自动可用)');
|
|
1546
1721
|
if (llm_probe && !llm_probe.online) notes.push('LLM 已保存但探针未通过(密钥/端点/模型可用性待确认,对话可能失败,可再试或改回)');
|
|
1547
1722
|
return json(res, 200, { saved: true, probe, llm_probe, note: notes.join(';') });
|
|
1548
1723
|
}
|
|
@@ -1552,14 +1727,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
1552
1727
|
try {
|
|
1553
1728
|
await armRest('/api/v1/agents?page=1&pageSize=1', {}, 6000);
|
|
1554
1729
|
online = true; latency_ms = Date.now() - t0;
|
|
1555
|
-
} catch {
|
|
1556
|
-
try { // 旧版 ARM 无 REST:回落 MCP 探测
|
|
1557
|
-
await mcpCall('search_agents', { keyword: '', limit: 1 }, { timeout_ms: 6000 });
|
|
1558
|
-
online = true; latency_ms = Date.now() - t0;
|
|
1559
|
-
} catch { }
|
|
1560
|
-
}
|
|
1730
|
+
} catch { }
|
|
1561
1731
|
const reg = loadRegistry();
|
|
1562
|
-
return json(res, 200, { online, latency_ms,
|
|
1732
|
+
return json(res, 200, { online, latency_ms, api: armApiBase(), pat: !!ARM_PAT, last_sync_at: reg.last_sync_at || null, local_count: reg.agents.length, outbox_pending: outboxCount() });
|
|
1563
1733
|
}
|
|
1564
1734
|
if (req.method === 'POST' && pathname === '/api/arm/sync') {
|
|
1565
1735
|
try { return json(res, 200, await armSync()); }
|
|
@@ -1571,7 +1741,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1571
1741
|
const kw = (url.searchParams.get('keyword') || '').trim().toLowerCase();
|
|
1572
1742
|
const reg = loadRegistry();
|
|
1573
1743
|
let online = true, remoteCards = [], authError = false;
|
|
1574
|
-
try { remoteCards = await armListAgents(url.searchParams.get('keyword') || ''); } // REST
|
|
1744
|
+
try { remoteCards = await armListAgents(url.searchParams.get('keyword') || ''); } // REST 全量
|
|
1575
1745
|
catch (e) { online = false; authError = /令牌未配置或无权限/.test(String(e.message)); } // 区分"未授权"与"不可达",前端引导配置令牌
|
|
1576
1746
|
const byId = new Map();
|
|
1577
1747
|
for (const e of reg.agents) {
|
|
@@ -1845,7 +2015,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1845
2015
|
if (!dep) return json(res, 400, { error: '助手未上岗' });
|
|
1846
2016
|
const arm = await getArmAgent(agent_id);
|
|
1847
2017
|
const toolsAvail = dep.toolset.map(t => t.tool).join(', ');
|
|
1848
|
-
const sys = '你是工业智能体平台的工作台设计师。根据助手档案生成工作台 DSL(JSON)。面板类型只允许: realtime_list(实时告警流)/status_grid(设备状态矩阵)/agent_dialog(诊断对话)/action_group(操作区)/kpi_strip(指标条)/inspection_report(自检报告)。结构: {"workbench":"名称","owner_agent":"agentId","version":"v0.1-draft","layout":[...],"panels":[{"id","type","title"
|
|
2018
|
+
const sys = '你是工业智能体平台的工作台设计师。根据助手档案生成工作台 DSL(JSON)。面板类型只允许: realtime_list(实时告警流)/status_grid(设备状态矩阵)/agent_dialog(诊断对话)/action_group(操作区)/kpi_strip(指标条)/inspection_report(自检报告)。结构: {"workbench":"名称","owner_agent":"agentId","version":"v0.1-draft","layout":[...],"panels":[{"id","type","title"}...]}。action_group 的 panels 含 "actions":[{"id","label","orchestration","autonomy","confirm":{"text","double_check"}}],orchestration 只能引用这些: logic://设备复位流程@mock / logic://告警派工@mock / logic://服务滚动重启@mock / logic://环境自检@mock。只输出 JSON,不要 markdown 代码围栏。';
|
|
1849
2019
|
const user = `助手: ${arm.name} ${arm.version}\n职责: ${arm.prompt}\n可用工具: ${toolsAvail}\n默认自主级别: ${dep.default_autonomy}\n请生成 4-6 个面板,含一个 agent_dialog 和一个 action_group。`;
|
|
1850
2020
|
const out = await llmComplete(sys, user);
|
|
1851
2021
|
const cleaned = out.replace(/```json|```/g, '').trim();
|
|
@@ -1950,6 +2120,17 @@ const server = http.createServer(async (req, res) => {
|
|
|
1950
2120
|
try { const m = saveCollection(await body(req)); return json(res, 200, { collection: m }); }
|
|
1951
2121
|
catch (e) { return json(res, 400, { error: e.message }); }
|
|
1952
2122
|
}
|
|
2123
|
+
if (req.method === 'GET' && pathname === '/api/store/version') { // 变更版本轻端点:面板 watch 轮询(08 契约 §4)
|
|
2124
|
+
const manifest = loadCollection(String(url.searchParams.get('name') || ''));
|
|
2125
|
+
if (!manifest) return json(res, 404, { error: '数据集不存在' });
|
|
2126
|
+
const wb = url.searchParams.get('wb') || undefined;
|
|
2127
|
+
const ws = url.searchParams.get('ws') || 'default';
|
|
2128
|
+
if (scopeOf(manifest) === 'agent' && wb && wb !== manifest.owner_agent) {
|
|
2129
|
+
return json(res, 403, { error: '数据集为助手私有' });
|
|
2130
|
+
}
|
|
2131
|
+
const row = dbFor(manifest, ws).prepare('SELECT v FROM _meta WHERE k = ?').get(`ver:${manifest.name}`);
|
|
2132
|
+
return json(res, 200, { version: +(row ? row.v : 0) });
|
|
2133
|
+
}
|
|
1953
2134
|
let mStore = pathname.match(/^\/api\/store\/([a-z0-9_-]+)\/records$/);
|
|
1954
2135
|
if (mStore) {
|
|
1955
2136
|
const manifest = loadCollection(mStore[1]);
|
|
@@ -2051,6 +2232,94 @@ const server = http.createServer(async (req, res) => {
|
|
|
2051
2232
|
return json(res, 400, { error: 'kind 须为 system|plugin' });
|
|
2052
2233
|
}
|
|
2053
2234
|
|
|
2235
|
+
// ── UI 资产分发(ARM 三资产模型):预览 / 发布 / 市场列表 / 安装 ──
|
|
2236
|
+
if (req.method === 'GET' && pathname === '/api/assets/preview') { // 分享确认框数据:组包预览(requires 全文明示,红线③)
|
|
2237
|
+
const type = url.searchParams.get('type');
|
|
2238
|
+
const key = type === 'panel' ? url.searchParams.get('id') : url.searchParams.get('agentId');
|
|
2239
|
+
if (!['panel', 'workbench'].includes(type) || !key) return json(res, 400, { error: 'type 须为 panel|workbench,且提供 id 或 agentId' });
|
|
2240
|
+
try {
|
|
2241
|
+
const bundle = buildAssetBundle(type, key);
|
|
2242
|
+
return json(res, 200, { name: bundle.name, manifest: bundle.manifest, files: bundle.files.map((f) => f.path) });
|
|
2243
|
+
} catch (e) { return json(res, 400, { error: String(e.message).slice(0, 300) }); }
|
|
2244
|
+
}
|
|
2245
|
+
if (req.method === 'POST' && pathname === '/api/assets/share') {
|
|
2246
|
+
const { type, id, agentId, name, bind_agent } = await body(req); // type: panel|workbench;panel 用 id=插件id,workbench 用 agentId
|
|
2247
|
+
const key = type === 'panel' ? id : agentId;
|
|
2248
|
+
if (!['panel', 'workbench'].includes(type) || !key) return json(res, 400, { error: 'type 须为 panel|workbench,且提供 id(插件)或 agentId(工作台)' });
|
|
2249
|
+
try {
|
|
2250
|
+
const bundle = buildAssetBundle(type, key, name);
|
|
2251
|
+
// 分享预检(红字提示用):requires 全文回传前端人审确认
|
|
2252
|
+
const fd = new FormData();
|
|
2253
|
+
fd.append('file', new Blob([zipFiles(bundle.files)], { type: 'application/zip' }), `${bundle.name}.zip`);
|
|
2254
|
+
const uploaded = await armUpload('/api/v1/ui-assets', fd);
|
|
2255
|
+
const armRef = { skill_name: bundle.name, version: uploaded.version, file_hash: uploaded.fileHash, shared_at: new Date().toISOString() };
|
|
2256
|
+
if (type === 'panel') {
|
|
2257
|
+
const full = loadPanelPlugin(id);
|
|
2258
|
+
full.arm_ref = armRef;
|
|
2259
|
+
fs.writeFileSync(path.join(PANELS_DIR, `${id}.json`), JSON.stringify(full, null, 2));
|
|
2260
|
+
} else {
|
|
2261
|
+
const target = loadDraft(agentId) ? draftPath(agentId) : path.join(WB_DIR, `${agentId}.json`);
|
|
2262
|
+
const dsl = JSON.parse(fs.readFileSync(target, 'utf8'));
|
|
2263
|
+
dsl.arm_ref = armRef;
|
|
2264
|
+
fs.writeFileSync(target, JSON.stringify(dsl, null, 2));
|
|
2265
|
+
}
|
|
2266
|
+
let bound = null;
|
|
2267
|
+
if (bind_agent) { // 发布后顺手绑定到当前助手(三资产组合:员工自带工位)
|
|
2268
|
+
const r = await armRest(`/api/v1/agents/${bind_agent}/ui-assets`, { method: 'POST', body: JSON.stringify({ name: bundle.name }) });
|
|
2269
|
+
bound = r && r.version;
|
|
2270
|
+
}
|
|
2271
|
+
metric({ kind: 'asset_share', type, name: bundle.name, version: uploaded.version });
|
|
2272
|
+
audit({ actor: 'asset-share', target: 'ARM', type, name: bundle.name, version: uploaded.version, bound });
|
|
2273
|
+
return json(res, 200, { name: bundle.name, version: uploaded.version, requires: bundle.manifest.requires, bound });
|
|
2274
|
+
} catch (e) { return json(res, 400, { error: String(e.message).slice(0, 400) }); }
|
|
2275
|
+
}
|
|
2276
|
+
if (req.method === 'GET' && pathname === '/api/assets/arm-library') {
|
|
2277
|
+
const keyword = url.searchParams.get('keyword') || '';
|
|
2278
|
+
const assetType = url.searchParams.get('asset_type') || '';
|
|
2279
|
+
try {
|
|
2280
|
+
const q = new URLSearchParams({ pageSize: '50', ...(keyword ? { keyword } : {}), ...(assetType ? { assetType } : {}) });
|
|
2281
|
+
const d = await armRest(`/api/v1/ui-assets?${q}`);
|
|
2282
|
+
const refs = localArmRefs();
|
|
2283
|
+
const items = (d.uiAssets || []).map((a) => {
|
|
2284
|
+
const local = refs[a.name] || [];
|
|
2285
|
+
return {
|
|
2286
|
+
name: a.name, description: a.description, asset_type: a.assetType, updated_at: a.updatedAt,
|
|
2287
|
+
download_count: a.downloadCount, latest_version: a.latestVersion ? a.latestVersion.version : null,
|
|
2288
|
+
publisher: a.creatorName,
|
|
2289
|
+
installed: local.length > 0,
|
|
2290
|
+
upgrade_available: !!(a.latestVersion && local.some((l) => l.file_hash && l.file_hash !== a.latestVersion.fileHash)),
|
|
2291
|
+
local,
|
|
2292
|
+
};
|
|
2293
|
+
});
|
|
2294
|
+
return json(res, 200, { items });
|
|
2295
|
+
} catch (e) { return json(res, 400, { error: String(e.message).slice(0, 300) }); }
|
|
2296
|
+
}
|
|
2297
|
+
if (req.method === 'POST' && pathname === '/api/assets/install') {
|
|
2298
|
+
const { name, version, agentId } = await body(req); // agentId:workbench 资产的目标助手(必填);panel 资产可省
|
|
2299
|
+
if (!name) return json(res, 400, { error: 'name 不能为空' });
|
|
2300
|
+
try {
|
|
2301
|
+
const detail = await armRest(`/api/v1/ui-assets/${encodeURIComponent(name)}`);
|
|
2302
|
+
if (detail.assetType === 'workbench' && !agentId) return json(res, 400, { error: '工作台资产须指定目标助手 agentId' });
|
|
2303
|
+
const ver = version || (detail.versions && detail.versions[0] && detail.versions[0].version) || 'v1';
|
|
2304
|
+
const { buf, hash } = await armDownload(`/api/v1/ui-assets/${encodeURIComponent(name)}/versions/${encodeURIComponent(ver)}/download`);
|
|
2305
|
+
const result = installAssetFiles(unzipFiles(buf), { agentId, mode: 'draft' }); // 手动安装进草稿,人审发布
|
|
2306
|
+
// 回填真实 file_hash(升级判断用)
|
|
2307
|
+
if (result.panels.length) {
|
|
2308
|
+
for (const p of result.panels) {
|
|
2309
|
+
const full = loadPanelPlugin(p.id);
|
|
2310
|
+
if (full?.arm_ref?.skill_name === name) { full.arm_ref.file_hash = hash; fs.writeFileSync(path.join(PANELS_DIR, `${p.id}.json`), JSON.stringify(full, null, 2)); }
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
if (agentId && result.workbench) {
|
|
2314
|
+
const target = draftPath(agentId);
|
|
2315
|
+
const cur = JSON.parse(fs.readFileSync(target, 'utf8'));
|
|
2316
|
+
if (cur.arm_ref?.skill_name === name) { cur.arm_ref.file_hash = hash; fs.writeFileSync(target, JSON.stringify(cur, null, 2)); }
|
|
2317
|
+
}
|
|
2318
|
+
metric({ kind: 'asset_install', name, version: ver });
|
|
2319
|
+
return json(res, 200, result);
|
|
2320
|
+
} catch (e) { return json(res, 400, { error: String(e.message).slice(0, 400) }); }
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2054
2323
|
// ── 草稿管理 ──
|
|
2055
2324
|
if (req.method === 'DELETE' && pathname === '/api/workbench/draft') {
|
|
2056
2325
|
const { agent_id } = await body(req);
|
|
@@ -2075,6 +2344,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
2075
2344
|
s.context.systemPrompt = devSystemPrompt();
|
|
2076
2345
|
}
|
|
2077
2346
|
s.context.messages.push({ role: 'user', content: message, timestamp: Date.now() });
|
|
2347
|
+
const dropped = trimDevContext(s.context);
|
|
2348
|
+
if (dropped > 0) emit('status', { text: `会话历史过长,已自动裁剪最早 ${dropped} 条消息` });
|
|
2078
2349
|
await devChatLoop(s, emit, ctl.signal);
|
|
2079
2350
|
emit('meta', { hasDraft: !!loadDraft(agentId) });
|
|
2080
2351
|
} catch (e) { emit('error', { message: e.message }); }
|
|
@@ -2206,11 +2477,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
2206
2477
|
const { agentId, name, description, content } = await body(req);
|
|
2207
2478
|
const arm = agentProfileNow(agentId);
|
|
2208
2479
|
const full = ['---', 'kind: experience', 'source: aims-platform-mvp', `agent: ${arm.name || agentId}@${arm.version || ''}`, 'scenario: 立库A区智能运维@mvp', '---', '', content].join('\n');
|
|
2209
|
-
const r = await
|
|
2210
|
-
|
|
2211
|
-
metric({ kind: 'experience', agentId, title: name, knowledge_id: kid });
|
|
2480
|
+
const r = await armCreateKnowledge({ name, description: description || `工作台人工审阅沉淀`, content: full, tags: ['MVP回流'] });
|
|
2481
|
+
metric({ kind: 'experience', agentId, title: name, knowledge_id: r.id });
|
|
2212
2482
|
audit({ actor: 'experience-reflow', target: 'ARM', name });
|
|
2213
|
-
return json(res, 200, { result: r });
|
|
2483
|
+
return json(res, 200, { result: `已创建 ${r.name}(id: ${r.id})${r.shareUrl ? `\n${r.shareUrl}` : ''}`, knowledge_id: r.id, share_url: r.shareUrl || '' });
|
|
2214
2484
|
}
|
|
2215
2485
|
// ── 自进化看板 ──
|
|
2216
2486
|
if (req.method === 'GET' && pathname === '/api/evolution') {
|
|
@@ -2289,7 +2559,7 @@ applyLLMConfig().then(() => {
|
|
|
2289
2559
|
server.listen(PORT, HOST, () => {
|
|
2290
2560
|
console.log(`[aims-workstation] http://localhost:${PORT}(绑定 ${HOST}) | 模型: ${model.id} @ ${model.baseUrl}`);
|
|
2291
2561
|
console.log(`[aims-workstation] 数据目录 AIMS_HOME: ${AIMS_HOME}`);
|
|
2292
|
-
|
|
2562
|
+
console.log(`[aims-workstation] ARM(REST): ${armApiBase()} | PAT: ${ARM_PAT ? '已加载' : '未配置'} | 已上岗助手: ${fs.readdirSync(DEPLOY_DIR).length} | 本地助手库: ${loadRegistry().agents.length}`);
|
|
2293
2563
|
});
|
|
2294
2564
|
// 已上岗助手后台物化为本地档案(best-effort;ARM 不可达时静默跳过,待手动同步)
|
|
2295
2565
|
(async () => {
|