aims-workstation 0.1.11 → 0.1.13

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 CHANGED
@@ -16,4 +16,4 @@ ARM_API_TOKEN=<arm_pat_xxx-or-leave-empty>
16
16
 
17
17
  # npm 发版令牌(npmjs.com → Access Tokens 生成;供 scripts/publish-npm.sh 使用,发布免浏览器授权)
18
18
  # 选 Granular(限定本包、Read and write)或 Classic → Automation
19
- # NPM_TOKEN=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
19
+ # NPM_TOKEN=
package/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "@earendil-works/pi-ai": "^1.2.0"
14
14
  },
15
15
  "packageManager": "pnpm@11.21.0+sha512.521705bce689924eac72f5a3587122f362689ef6571e55ba80076fd637c11132ecffada26fad4ea79c485bfddbfd3d5a2a5b05805a77e893de71ec8a6cca3bb1",
16
- "version": "0.1.11",
16
+ "version": "0.1.13",
17
17
  "description": "AIMS 工作站:本地智能体工作站——ARM 助手本地化(下载/离线/升级)、工作台对话式定制、多工作区隔离、分级自主与审计",
18
18
  "bin": {
19
19
  "aims-workstation": "bin/aims-workstation.js"
package/server.js CHANGED
@@ -236,6 +236,19 @@ async function armRest(pathname, init = {}, timeout_ms = 20000) {
236
236
  async function armCreateKnowledge({ name, description, content, tags }) {
237
237
  return armRest('/api/v1/knowledges', { method: 'POST', body: JSON.stringify({ name, description, content, tags }) }, 25000);
238
238
  }
239
+
240
+ // 经验与助手关联:沉淀出的知识绑定到当前助手(助手检索范围=已绑定知识;缺这步=沉淀了但检索不到)。
241
+ // 绑定失败不阻断沉淀(老版 ARM 无端点时仅审计留痕)。
242
+ async function bindExperienceToAgent(agentId, knowledgeId) {
243
+ try {
244
+ await armRest(`/api/v1/agents/${agentId}/knowledges`, { method: 'POST', body: JSON.stringify({ knowledgeId, kind: 'experience' }) });
245
+ audit({ actor: 'experience-reflow', target: 'ARM', action: 'bind', agentId, knowledge_id: knowledgeId });
246
+ return true;
247
+ } catch (e) {
248
+ audit({ actor: 'experience-reflow', target: 'ARM', action: 'bind-failed', agentId, knowledge_id: knowledgeId, error: String(e.message || e).slice(0, 90) });
249
+ return false;
250
+ }
251
+ }
239
252
  const vVer = (v) => (String(v || '').startsWith('v') ? String(v) : `v${v || '0.0.0'}`); // REST 无 v 前缀,统一内部表示
240
253
 
241
254
  const agentCache = new Map();
@@ -399,7 +412,7 @@ async function flushExperienceOutbox() {
399
412
  const lines = fs.readFileSync(OUTBOX_FILE, 'utf8').split('\n').filter(Boolean);
400
413
  const remain = []; let ok = 0;
401
414
  for (const line of lines) {
402
- try { const it = JSON.parse(line); await armCreateKnowledge(it.args); ok++; }
415
+ try { const it = JSON.parse(line); const r = await armCreateKnowledge(it.args); if (it.agentId && r?.id) await bindExperienceToAgent(it.agentId, r.id); ok++; }
403
416
  catch { remain.push(line); }
404
417
  }
405
418
  fs.writeFileSync(OUTBOX_FILE, remain.length ? remain.join('\n') + '\n' : '');
@@ -618,7 +631,7 @@ const TOOLS = {
618
631
  desc: '把本次处置经验沉淀进企业知识库(ARM 经验库;离线时存本地待回流队列)',
619
632
  run: async ({ title, markdown }, agentId) => {
620
633
  const arm = agentProfileNow(agentId);
621
- const content = ['---', 'kind: experience', 'source: aims-platform-mvp', `agent: ${arm.name || agentId}@${arm.version || ''}`, 'scenario: 立库A区智能运维@mvp', '---', '', markdown].join('\n');
634
+ const content = ['---', 'kind: experience', 'source: aims-platform-mvp', `agent_id: ${agentId}`, `agent: ${arm.name || agentId}@${arm.version || ''}`, 'scenario: 立库A区智能运维@mvp', '---', '', markdown].join('\n');
622
635
  const args = { name: title, description: `MVP回流:${title}`, content, tags: ['MVP回流'] };
623
636
  let r;
624
637
  try { r = await armCreateKnowledge(args); }
@@ -627,9 +640,10 @@ const TOOLS = {
627
640
  audit({ actor: 'experience-reflow', target: 'outbox', title });
628
641
  return `ARM 不可达(${String(e.message).slice(0, 60)}),经验已存本地待回流队列;恢复连接后在助手大厅点「检查更新」将自动回流。`;
629
642
  }
643
+ const bound = await bindExperienceToAgent(agentId, r.id);
630
644
  metric({ kind: 'experience', agentId, title, knowledge_id: r.id });
631
- audit({ actor: 'experience-reflow', target: 'ARM', title });
632
- return `经验已沉淀到 ARM 知识库:${r.name}(id: ${r.id}${r.shareUrl ? `,${r.shareUrl}` : ''})`;
645
+ audit({ actor: 'experience-reflow', target: 'ARM', title, bound });
646
+ return `经验已沉淀到 ARM 知识库:${r.name}(id: ${r.id}${r.shareUrl ? `,${r.shareUrl}` : ''})${bound ? ',并已绑定到当前助手(检索即得)' : ';绑定助手未成功(已在审计留痕),可到 ARM 手动关联'}`;
633
647
  },
634
648
  },
635
649
  };
@@ -674,7 +688,23 @@ function loadWorkbench(agentId) {
674
688
  const p = path.join(WB_DIR, `${agentId}.json`);
675
689
  return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf8')) : null;
676
690
  }
691
+ // ── 多视图契约(DSL v2):views 为场景视图;panels 始终同步为各视图面板的扁平拼接(存量读取点零改动)──
692
+ function viewsOfSrv(dsl) {
693
+ if (Array.isArray(dsl?.views) && dsl.views.length) return dsl.views
694
+ return [{ id: 'main', name: '主视图', panels: Array.isArray(dsl?.panels) ? dsl.panels : [] }]
695
+ }
696
+ function normDsl(dsl) {
697
+ if (!dsl || typeof dsl !== 'object') return dsl
698
+ dsl.views = viewsOfSrv(dsl).map(v => ({
699
+ id: v.id || `view_${Math.random().toString(36).slice(2, 6)}`,
700
+ name: v.name || '视图', icon: v.icon,
701
+ panels: Array.isArray(v.panels) ? v.panels : [],
702
+ }))
703
+ dsl.panels = dsl.views.flatMap(v => v.panels)
704
+ return dsl
705
+ }
677
706
  function saveWorkbench(agentId, dsl) {
707
+ dsl = normDsl(dsl);
678
708
  fs.writeFileSync(path.join(WB_DIR, `${agentId}.json`), JSON.stringify(dsl, null, 2));
679
709
  const vp = path.join(WB_VER_DIR, `${agentId}.json`);
680
710
  const vers = fs.existsSync(vp) ? JSON.parse(fs.readFileSync(vp, 'utf8')) : [];
@@ -911,19 +941,22 @@ function loadDraft(agentId) {
911
941
  return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf8')) : null;
912
942
  }
913
943
  function saveDraft(agentId, dsl) {
914
- if (!Array.isArray(dsl.panels)) throw new Error('dsl.panels 必须为数组');
915
- const prev = loadDraft(agentId) || loadWorkbench(agentId) || { panels: [] };
944
+ dsl = normDsl(dsl); // v1/v2 统一:补 views、同步扁平 panels
945
+ const prevFlat = viewsOfSrv(loadDraft(agentId) || loadWorkbench(agentId) || {}).flatMap(v => v.panels);
916
946
  // 防护性合并:同 id 同类型的面板,Agent 遗漏的字段(如 action_group.actions)从上一版补全,防止全量重写丢配置
917
- dsl.panels = dsl.panels.map(p => {
918
- const old = prev.panels.find(x => x.id === p.id && x.type === p.type);
919
- return old ? { ...old, ...p } : p;
920
- });
921
- for (const p of dsl.panels) {
922
- if ((p.type === 'custom' || p.type === 'custom_panel') && !panelCodeOf(p)) throw new Error(`面板 ${p.id || p.title} 引用的代码/插件缺失`);
947
+ for (const v of dsl.views) {
948
+ v.panels = v.panels.map(p => {
949
+ const old = prevFlat.find(x => x.id === p.id && x.type === p.type);
950
+ return old ? { ...old, ...p } : p;
951
+ });
952
+ for (const p of v.panels) {
953
+ if ((p.type === 'custom' || p.type === 'custom_panel') && !panelCodeOf(p)) throw new Error(`面板 ${p.id || p.title} 引用的代码/插件缺失`);
954
+ }
923
955
  }
956
+ dsl.panels = dsl.views.flatMap(v => v.panels); // 合并后再次同步
924
957
  dsl.owner_agent = agentId;
925
958
  fs.writeFileSync(draftPath(agentId), JSON.stringify(dsl, null, 2));
926
- audit({ actor: 'dev-agent', agentId, action: 'draft', panels: dsl.panels.length });
959
+ audit({ actor: 'dev-agent', agentId, action: 'draft', panels: dsl.panels.length, views: dsl.views.length });
927
960
  }
928
961
 
929
962
  // ── Skill 装载(Agent Skills 规范:SKILL.md frontmatter 注入描述,正文按需读取)──
@@ -1311,7 +1344,7 @@ const DEV_TOOLS = {
1311
1344
  },
1312
1345
  update_workbench: {
1313
1346
  desc: '把完整的工作台 DSL 写入草稿(不会直接生效,需用户在编辑器审阅后发布)。基于当前 DSL 增量修改后整体提交',
1314
- schema: { dsl: { type: 'object', description: "完整工作台 DSL {workbench,version,panels:[...]}。panels 元素含 id/type/title;自定义面板用 type=custom_panel 且 panel=插件id,或 type=custom 且 code=面板代码" } },
1347
+ schema: { dsl: { type: 'object', description: "完整工作台 DSL {workbench,version,views:[{id,name,panels:[…]}],panels:[…]}。panels 元素含 id/type/title;自定义面板用 type=custom_panel 且 panel=插件id,或 type=custom 且 code=面板代码。多场景用 views 分视图(新场景新建视图);panels 扁平字段平台会自动同步,可只维护 views" } },
1315
1348
  required: ['dsl'],
1316
1349
  run: async ({ dsl }) => {
1317
1350
  saveDraft(devCtx.agentId, dsl);
@@ -1384,13 +1417,14 @@ function devSystemPrompt() {
1384
1417
  ' </div>`',
1385
1418
  '}',
1386
1419
  '',
1387
- '【当前工作台】', JSON.stringify(live ? { workbench: live.workbench, panels: live.panels.map(p => ({ id: p.id, type: p.type, title: p.title, panel: p.panel })) } : null),
1420
+ '【当前工作台】', JSON.stringify(live ? { workbench: live.workbench, views: viewsOfSrv(live).map(v => ({ id: v.id, name: v.name, panels: v.panels.map(p => ({ id: p.id, type: p.type, title: p.title, panel: p.panel })) })) } : null),
1388
1421
  '【插件库】', JSON.stringify(plugins.map(p => ({ id: p.id, name: p.name, version: p.version }))),
1389
1422
  '',
1390
1423
  '【修改已有插件】先 read_panel_plugin 看当前代码 → 需要持久数据就 create_collection → write_panel_plugin 同 id 覆盖(版本自增)→ 需要时 update_workbench 调整布局。同 id 写入即升级,工作台引用不受影响。',
1391
1424
  '【自探索工作流(必须遵守)】理解需求 → 探索 → 规划 → 实现 → 提交草稿:',
1392
1425
  '1. 探索:新需求涉及助手既有能力时,必须先 exec 读它的 AGENT.md(按名 grep 定位),必要时读其 skills/脚本与验证相关接口(curl 实测返回结构)。能推送通常就能搜索——从脚本/配置里找到真实的服务地址与 API。',
1393
1426
  '2. 规划:把探索结论映射为面板方案(数据从哪来、经什么能力取、展示成什么),方案要说清"复用了助手的哪个既有能力"。',
1427
+ '· 工作台支持多视图(DSL views:[{id,name,panels}],不同高频场景各建一块屏,如「日常监控/专项巡检/报表」):需求涉及新场景时优先新建视图,而不是往现有视图堆面板;提交的 DSL 带 views 即可,扁平 panels 由平台自动同步',
1394
1428
  '3. 实现:面板运行在浏览器,内网接口跨域受限——一律经平台代理取数:fetch("/api/proxy?url=" + encodeURIComponent(接口地址)),返回 {status, body}。慢接口(>8s 的重查询)追加 &timeout_ms=30000(上限 60s)与 &max_kb=512(默认 128KB 会截断大结果);但优先改用服务端过滤/分页参数(q=/limit/时间范围)减小单次拉取——不要靠放宽超时硬扛全量拉取。',
1395
1429
  '4. 提交:write_panel_plugin + update_workbench 草稿,明确告知用户审阅发布。',
1396
1430
  '【工作方式】改已有面板时基于当前 DSL 增量修改;单一职责,先小后大。',
@@ -2062,7 +2096,7 @@ const server = http.createServer(async (req, res) => {
2062
2096
  }
2063
2097
  if (req.method === 'PUT' && pathname === '/api/workbench') {
2064
2098
  const { agent_id, dsl } = await body(req);
2065
- if (!agent_id || !dsl?.panels) return json(res, 400, { error: 'agent_id 与 dsl.panels 必填' });
2099
+ if (!agent_id || !(dsl?.panels || dsl?.views?.length)) return json(res, 400, { error: 'agent_id 与 dsl(panels 或 views)必填' });
2066
2100
  dsl.version = nextVersion(dsl.version, agent_id);
2067
2101
  saveWorkbench(agent_id, dsl);
2068
2102
  try { fs.rmSync(draftPath(agent_id), { force: true }); } catch { }
@@ -2574,11 +2608,12 @@ const server = http.createServer(async (req, res) => {
2574
2608
  if (req.method === 'POST' && pathname === '/api/experience/commit') {
2575
2609
  const { agentId, name, description, content } = await body(req);
2576
2610
  const arm = agentProfileNow(agentId);
2577
- const full = ['---', 'kind: experience', 'source: aims-platform-mvp', `agent: ${arm.name || agentId}@${arm.version || ''}`, 'scenario: 立库A区智能运维@mvp', '---', '', content].join('\n');
2611
+ const full = ['---', 'kind: experience', 'source: aims-platform-mvp', `agent_id: ${agentId}`, `agent: ${arm.name || agentId}@${arm.version || ''}`, 'scenario: 立库A区智能运维@mvp', '---', '', content].join('\n');
2578
2612
  const r = await armCreateKnowledge({ name, description: description || `工作台人工审阅沉淀`, content: full, tags: ['MVP回流'] });
2613
+ const bound = await bindExperienceToAgent(agentId, r.id);
2579
2614
  metric({ kind: 'experience', agentId, title: name, knowledge_id: r.id });
2580
- audit({ actor: 'experience-reflow', target: 'ARM', name });
2581
- return json(res, 200, { result: `已创建 ${r.name}(id: ${r.id})${r.shareUrl ? `\n${r.shareUrl}` : ''}`, knowledge_id: r.id, share_url: r.shareUrl || '' });
2615
+ audit({ actor: 'experience-reflow', target: 'ARM', name, bound });
2616
+ return json(res, 200, { result: `已创建 ${r.name}(id: ${r.id})${r.shareUrl ? `\n${r.shareUrl}` : ''}${bound ? '\n已绑定到当前助手(检索即得)' : '\n绑定助手未成功(已在审计留痕),可到 ARM 手动关联'}`, knowledge_id: r.id, share_url: r.shareUrl || '' });
2582
2617
  }
2583
2618
  // ── 自进化看板 ──
2584
2619
  if (req.method === 'GET' && pathname === '/api/evolution') {