aims-workstation 0.1.13 → 0.1.14

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/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.13",
16
+ "version": "0.1.14",
17
17
  "description": "AIMS 工作站:本地智能体工作站——ARM 助手本地化(下载/离线/升级)、工作台对话式定制、多工作区隔离、分级自主与审计",
18
18
  "bin": {
19
19
  "aims-workstation": "bin/aims-workstation.js"
package/server.js CHANGED
@@ -272,7 +272,21 @@ async function armListAgents(keyword = '') {
272
272
  out.push(...(d.agents || []));
273
273
  if (!out.length || out.length >= +(d.total || 0)) break;
274
274
  }
275
- 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
+ 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_id: a.createdBy || '', created_by: a.creatorName || a.createdBy || '' }));
276
+ }
277
+
278
+ // PAT 身份(带缓存):身份只随 (ARM 源, 令牌) 变化——key 命中直接复用,避免目录高频刷新
279
+ // (搜索防抖每 400ms 一次 /api/agents)时每次都打 /auth/me。失败不缓存:未配置令牌/不可达时
280
+ // 返回 null,恢复后下次自动重试。设置页改源/换令牌 → key 变化 → 自动重新获取。
281
+ const armMeCache = { key: '', me: null };
282
+ async function armMe() {
283
+ const key = `${armApiBase()}|${armApiToken()}`;
284
+ if (armMeCache.key === key && armMeCache.me) return armMeCache.me;
285
+ try {
286
+ const u = await armRest('/api/v1/auth/me');
287
+ if (u && u.id) { armMeCache.key = key; armMeCache.me = { id: u.id, name: u.name || '', role: u.role || 'USER' }; return armMeCache.me; }
288
+ } catch { }
289
+ return null;
276
290
  }
277
291
 
278
292
  // 知识检索(全局经验库):REST 混合检索(RAG 语义 ⊕ 关键词);不可达由调用方降级(本地知识关键词命中)
@@ -1664,6 +1678,8 @@ const server = http.createServer(async (req, res) => {
1664
1678
  // ── 静态前端(web/dist 生产构建)──
1665
1679
  if (req.method === 'GET' && !pathname.startsWith('/api')) {
1666
1680
  let p = path.join(ROOT, 'web/dist', pathname === '/' ? 'index.html' : pathname);
1681
+ // 目录路径解析为其 index.html(否则 readFileSync 读目录返回空响应体)
1682
+ if (fs.existsSync(p) && fs.statSync(p).isDirectory()) p = path.join(p, 'index.html');
1667
1683
  if (!p.startsWith(path.join(ROOT, 'web/dist')) || !fs.existsSync(p)) p = path.join(ROOT, 'web/dist/index.html');
1668
1684
  if (fs.existsSync(p)) {
1669
1685
  const mime = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript', '.css': 'text/css', '.svg': 'image/svg+xml', '.png': 'image/png', '.ico': 'image/x-icon' }[path.extname(p)] || 'application/octet-stream';
@@ -1825,6 +1841,7 @@ const server = http.createServer(async (req, res) => {
1825
1841
  installed_version: e.installed_version, remote_version: e.remote_version || null,
1826
1842
  update_available: !!(e.remote_version && cmpVersion(e.remote_version, e.installed_version) > 0),
1827
1843
  delisted: !!e.delisted, downloaded_at: e.downloaded_at || null,
1844
+ created_by_id: null, created_by: null, // 属主身份以远端目录为准(离线/已下架不可编辑,无需属主)
1828
1845
  });
1829
1846
  }
1830
1847
  if (online) { // 静默刷新远端版本与下架标记(显式事件统计走 /api/arm/sync)——仅在完整拉取时;
@@ -1837,6 +1854,7 @@ const server = http.createServer(async (req, res) => {
1837
1854
  description: c.description || cur.description, remote_version: c.version,
1838
1855
  deployed: !!loadDeployment(c.agent_id),
1839
1856
  update_available: cur.installed ? cmpVersion(c.version, cur.installed_version) > 0 : false,
1857
+ created_by_id: c.created_by_id || null, created_by: c.created_by || null,
1840
1858
  });
1841
1859
  }
1842
1860
  if (!kw) {
@@ -1848,7 +1866,10 @@ const server = http.createServer(async (req, res) => {
1848
1866
  saveRegistry(reg);
1849
1867
  }
1850
1868
  }
1851
- return json(res, 200, { online, auth_error: authError, last_sync_at: reg.last_sync_at || null, agents: [...byId.values()] });
1869
+ // PAT 身份(编辑/删除门控依据:me.id vs 卡片 created_by_id,或 ADMIN):armMe 有缓存——
1870
+ // 身份只随 (ARM 源, 令牌) 变化,搜索防抖高频刷新目录时不重复打 /auth/me;失败降级 null 不阻断目录
1871
+ const me = online ? await armMe() : null;
1872
+ return json(res, 200, { online, auth_error: authError, me, last_sync_at: reg.last_sync_at || null, agents: [...byId.values()] });
1852
1873
  }
1853
1874
  // ── 新建助手:本地起草(AI 生成 prompt)→ ARM JSON 创建(创建即发布)→ 可选知识绑定(幂等)→ 下载物化 ──
1854
1875
  if (req.method === 'POST' && pathname === '/api/agents/draft-prompt') {
@@ -1926,6 +1947,70 @@ const server = http.createServer(async (req, res) => {
1926
1947
  return json(res, 200, { ok: true, manifest });
1927
1948
  } catch (e) { return json(res, 502, { error: `更新失败: ${e.message}` }); }
1928
1949
  }
1950
+ // ── 绑定管理(编辑档案对话框):skill/知识绑定增删代理 ARM,变更后重新物化本地(版本前进可回滚)──
1951
+ if (req.method === 'GET' && pathname === '/api/agents/skills') { // 绑定选择器的技能检索(对应知识检索 /api/agents/knowledges)
1952
+ const kwd = url.searchParams.get('keyword') || '';
1953
+ try {
1954
+ const d = await armRest(`/api/v1/skills?keyword=${encodeURIComponent(kwd)}&page=1&pageSize=20`);
1955
+ return json(res, 200, { items: (d.skills || []).map((s) => ({ id: s.id, name: s.name, description: String(s.description || '').slice(0, 80) })) });
1956
+ } catch (e) { return json(res, 200, { items: [], error: String(e.message).slice(0, 100) }); }
1957
+ }
1958
+ let mBind = pathname.match(/^\/api\/agents\/([0-9a-f-]{36})\/bindings$/);
1959
+ if (mBind && req.method === 'GET') {
1960
+ try {
1961
+ const d = await armRest(`/api/v1/agents/${mBind[1]}`);
1962
+ return json(res, 200, {
1963
+ skills: (d.skills || []).map((b) => ({ binding_id: b.id, ref_id: b.skillId, name: b.skill?.name || b.skillId, description: String(b.skill?.description || '').slice(0, 80) })),
1964
+ knowledges: (d.knowledges || []).map((b) => ({ binding_id: b.id, ref_id: b.knowledgeId, name: b.knowledge?.name || b.knowledgeId, description: String(b.knowledge?.description || '').slice(0, 80), kind: b.kind })),
1965
+ });
1966
+ } catch (e) { return json(res, 502, { error: `绑定读取失败: ${e.message}` }); }
1967
+ }
1968
+ if (mBind && req.method === 'POST') {
1969
+ const { type, ref_id, kind } = await body(req);
1970
+ if (!['skill', 'knowledge'].includes(type) || !ref_id) return json(res, 400, { error: 'type 须为 skill|knowledge,且提供 ref_id' });
1971
+ const sub = type === 'skill' ? 'skills' : 'knowledges';
1972
+ const payload = type === 'skill' ? { skillId: ref_id } : { knowledgeId: ref_id, kind: kind === 'experience' ? 'experience' : 'essential' };
1973
+ try {
1974
+ const r = await armRest(`/api/v1/agents/${mBind[1]}/${sub}`, { method: 'POST', body: JSON.stringify(payload) });
1975
+ let manifest = null;
1976
+ try { manifest = await downloadAgent(mBind[1]); } catch { /* 物化失败不回滚 ARM 变更,提示用户重新下载 */ }
1977
+ audit({ actor: 'user', action: 'bind-agent-asset', agentId: mBind[1], type, ref_id, version: r?.version });
1978
+ return json(res, 200, { ok: true, version: r?.version || null, manifest });
1979
+ } catch (e) { return json(res, 502, { error: `绑定失败: ${e.message}` }); }
1980
+ }
1981
+ if (mBind && req.method === 'DELETE') {
1982
+ const { type, binding_id } = await body(req);
1983
+ if (!['skill', 'knowledge'].includes(type) || !binding_id) return json(res, 400, { error: 'type 须为 skill|knowledge,且提供 binding_id' });
1984
+ try {
1985
+ await armRest(`/api/v1/agents/${mBind[1]}/${type === 'skill' ? 'skills' : 'knowledges'}?bindingId=${encodeURIComponent(binding_id)}`, { method: 'DELETE' });
1986
+ let manifest = null;
1987
+ try { manifest = await downloadAgent(mBind[1]); } catch { }
1988
+ audit({ actor: 'user', action: 'unbind-agent-asset', agentId: mBind[1], type, binding_id });
1989
+ return json(res, 200, { ok: true, manifest });
1990
+ } catch (e) { return json(res, 502, { error: `解绑失败: ${e.message}` }); }
1991
+ }
1992
+ // 克隆助手(fork):复制档案 + 全部绑定,创建归当前 PAT 用户的新助手——非属主想改 / 想基于
1993
+ // 已有快速调教的正路。无需对源助手的编辑权限(源详情是公开读);新助手属主即当前用户,
1994
+ // 后续绑定守卫天然放行。绑定逐条 best-effort(单条失败计数不阻断),UI 资产跟随最新版本。
1995
+ if (req.method === 'POST' && pathname.match(/^\/api\/agents\/([0-9a-f-]{36})\/clone$/)) {
1996
+ const id = pathname.match(/^\/api\/agents\/([0-9a-f-]{36})\/clone$/)[1];
1997
+ const { name, description } = await body(req);
1998
+ try {
1999
+ const src = await armRest(`/api/v1/agents/${id}`);
2000
+ const nm = String(name || `${src.name}-副本`).trim();
2001
+ const created = await armRest('/api/v1/agents', { method: 'POST', body: JSON.stringify({ name: nm, description: String(description ?? src.description ?? '').slice(0, 512), prompt: src.prompt || '', status: 'active' }) });
2002
+ const newId = created?.id || created?.agent?.id;
2003
+ if (!newId) throw new Error('ARM 未返回新助手 id');
2004
+ const copied = { skills: 0, knowledges: 0, ui_assets: 0, failed: 0 };
2005
+ for (const b of (src.skills || [])) { try { await armRest(`/api/v1/agents/${newId}/skills`, { method: 'POST', body: JSON.stringify({ skillId: b.skillId }) }); copied.skills++; } catch { copied.failed++; } }
2006
+ for (const b of (src.knowledges || [])) { try { await armRest(`/api/v1/agents/${newId}/knowledges`, { method: 'POST', body: JSON.stringify({ knowledgeId: b.knowledgeId, kind: b.kind || 'experience' }) }); copied.knowledges++; } catch { copied.failed++; } }
2007
+ for (const b of (src.uiBindings || [])) { try { await armRest(`/api/v1/agents/${newId}/ui-assets`, { method: 'POST', body: JSON.stringify({ name: b.asset?.name || b.uiAssetId }) }); copied.ui_assets++; } catch { copied.failed++; } }
2008
+ const manifest = await downloadAgent(newId); // 创建即物化(沿用新建链路)
2009
+ const reg = loadRegistry(); const entry = reg.agents.find((x) => x.agent_id === newId); if (entry) entry.created_via_workstation = true; saveRegistry(reg);
2010
+ audit({ actor: 'user', action: 'clone-agent', agentId: newId, source: id, name: nm, ...copied });
2011
+ return json(res, 200, { agent_id: newId, name: nm, copied, manifest: { agent_id: manifest.agent_id, name: manifest.name, version: manifest.version } });
2012
+ } catch (e) { return json(res, 502, { error: `克隆失败: ${e.message}` }); }
2013
+ }
1929
2014
  // 从 ARM 删除(危险操作,前端二次确认):可选同时卸载本地档案
1930
2015
  if (req.method === 'POST' && pathname.match(/^\/api\/agents\/[0-9a-f-]{36}\/arm-delete$/)) {
1931
2016
  const id = pathname.split('/')[3];
@@ -2379,14 +2464,16 @@ const server = http.createServer(async (req, res) => {
2379
2464
  dsl.arm_ref = armRef;
2380
2465
  fs.writeFileSync(target, JSON.stringify(dsl, null, 2));
2381
2466
  }
2382
- let bound = null;
2383
- if (bind_agent) { // 发布后顺手绑定到当前助手(三资产组合:员工自带工位)
2384
- const r = await armRest(`/api/v1/agents/${bind_agent}/ui-assets`, { method: 'POST', body: JSON.stringify({ name: bundle.name }) });
2385
- bound = r && r.version;
2467
+ let bound = null, bind_error = null;
2468
+ if (bind_agent) { // 发布后顺手绑定到当前助手(三资产组合:员工自带工位);失败不回滚发布(如非属主 403:仅助手创建者或管理员可绑定)
2469
+ try {
2470
+ const r = await armRest(`/api/v1/agents/${bind_agent}/ui-assets`, { method: 'POST', body: JSON.stringify({ name: bundle.name }) });
2471
+ bound = r && r.version;
2472
+ } catch (e) { bind_error = String(e.message).slice(0, 200); }
2386
2473
  }
2387
2474
  metric({ kind: 'asset_share', type, name: bundle.name, version: uploaded.version });
2388
- audit({ actor: 'asset-share', target: 'ARM', type, name: bundle.name, version: uploaded.version, bound });
2389
- return json(res, 200, { name: bundle.name, version: uploaded.version, requires: bundle.manifest.requires, bound });
2475
+ audit({ actor: 'asset-share', target: 'ARM', type, name: bundle.name, version: uploaded.version, bound, bind_error });
2476
+ return json(res, 200, { name: bundle.name, version: uploaded.version, requires: bundle.manifest.requires, bound, bind_error });
2390
2477
  } catch (e) { return json(res, 400, { error: String(e.message).slice(0, 400) }); }
2391
2478
  }
2392
2479
  if (req.method === 'GET' && pathname === '/api/assets/arm-library') {