aims-workstation 0.1.14 → 0.1.15

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.14",
16
+ "version": "0.1.15",
17
17
  "description": "AIMS 工作站:本地智能体工作站——ARM 助手本地化(下载/离线/升级)、工作台对话式定制、多工作区隔离、分级自主与审计",
18
18
  "bin": {
19
19
  "aims-workstation": "bin/aims-workstation.js"
package/server.js CHANGED
@@ -396,6 +396,27 @@ async function downloadAgent(agentId) {
396
396
  return manifest;
397
397
  }
398
398
 
399
+ // 本地档案写入(离线创建 / 未发布档案编辑):与 downloadAgent 同构的物化结构,source 标记 local。
400
+ // 发布到 ARM(同 id,见 /publish)后即转正为远端档案,本地结构不变——身份全程稳定。
401
+ const bumpPatch = (v) => { const p = String(v || 'v1.0.0').replace(/^v/, '').split('.').map(n => +n || 0); p[2] = (p[2] || 0) + 1; return `v${p[0] || 1}.${p[1] || 0}.${p[2]}`; };
402
+ function writeLocalAgent(agentId, { name, description, prompt }, { bump = false } = {}) {
403
+ const cur = bump ? (localManifest(agentId) || {}) : {};
404
+ const version = bump ? bumpPatch(cur.version) : 'v1.0.0';
405
+ const verDir = path.join(ARM_AGENTS_DIR, agentId, 'versions', version);
406
+ fs.mkdirSync(path.join(verDir, 'knowledges'), { recursive: true });
407
+ const manifest = {
408
+ agent_id: agentId, name, version, description: String(description || '').slice(0, 512),
409
+ downloaded_at: new Date().toISOString(), source: 'local',
410
+ prompt: String(prompt || ''),
411
+ skills: cur.skills || [], knowledges: cur.knowledges || [], ui_bindings: cur.ui_bindings || [], // 未发布档案无 ARM 绑定;编辑时保留既有
412
+ };
413
+ fs.writeFileSync(path.join(verDir, 'prompt.md'), manifest.prompt);
414
+ fs.writeFileSync(path.join(verDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
415
+ commitDownload(manifest);
416
+ audit({ actor: 'user', agentId, action: bump ? 'update-local-agent' : 'create-local-agent', version });
417
+ return manifest;
418
+ }
419
+
399
420
  // 同步:比对远端目录与本地库,产出 new/update/delisted 事件;顺带冲账离线经验 outbox
400
421
  async function armSync() {
401
422
  const remote = await armListAgents();
@@ -1842,6 +1863,7 @@ const server = http.createServer(async (req, res) => {
1842
1863
  update_available: !!(e.remote_version && cmpVersion(e.remote_version, e.installed_version) > 0),
1843
1864
  delisted: !!e.delisted, downloaded_at: e.downloaded_at || null,
1844
1865
  created_by_id: null, created_by: null, // 属主身份以远端目录为准(离线/已下架不可编辑,无需属主)
1866
+ local_only: !!e.local_only, // 本地未发布档案(离线创建):大厅显示「未发布」徽章 + 发布入口
1845
1867
  });
1846
1868
  }
1847
1869
  if (online) { // 静默刷新远端版本与下架标记(显式事件统计走 /api/arm/sync)——仅在完整拉取时;
@@ -1908,24 +1930,36 @@ const server = http.createServer(async (req, res) => {
1908
1930
  const { name, description, prompt, knowledge_ids } = await body(req);
1909
1931
  const nm = String(name || '').trim(), pr = String(prompt || '').trim();
1910
1932
  if (!nm || !pr) return json(res, 400, { error: '名称与 System Prompt 必填' });
1933
+ // 身份本地生成并随创建上行(新版 ARM 保持同 id;旧版忽略后以其返回 id 为准,行为不变)
1934
+ const localId = crypto.randomUUID();
1935
+ let newId, bound = 0, local = false;
1911
1936
  try {
1912
- const created = await armRest('/api/v1/agents', { method: 'POST', body: JSON.stringify({ name: nm, description: String(description || '').trim().slice(0, 512), prompt: pr, status: 'active' }) });
1913
- const newId = created?.id || created?.agent?.id;
1937
+ const created = await armRest('/api/v1/agents', { method: 'POST', body: JSON.stringify({ id: localId, name: nm, description: String(description || '').trim().slice(0, 512), prompt: pr, status: 'active' }) });
1938
+ newId = created?.id || created?.agent?.id;
1914
1939
  if (!newId) throw new Error('ARM 未返回新助手 id');
1915
- let bound = 0;
1916
1940
  for (const kid of (Array.isArray(knowledge_ids) ? knowledge_ids : [])) { // 绑定幂等(重复绑定返回已有版本)
1917
1941
  try { await armRest(`/api/v1/agents/${newId}/knowledges`, { method: 'POST', body: JSON.stringify({ knowledgeId: kid, kind: 'experience' }) }); bound++; }
1918
1942
  catch { /* 单条绑定失败不阻断创建 */ }
1919
1943
  }
1920
- const manifest = await downloadAgent(newId); // 创建即物化本地(沿用下载链路)
1921
- const reg = loadRegistry(); const entry = reg.agents.find((x) => x.agent_id === newId); if (entry) entry.created_via_workstation = true; saveRegistry(reg); // 大厅据此显示编辑/删除入口
1922
- audit({ actor: 'user', action: 'create-agent', agentId: newId, name: nm, knowledges: bound });
1923
- return json(res, 200, { agent_id: newId, manifest, knowledges_bound: bound });
1924
- } catch (e) { return json(res, 502, { error: `创建失败: ${e.message}` }); }
1925
- }
1926
- // 编辑档案(工作站创建的助手):ARM PUT 后重新物化,本地 prompt 同步更新
1944
+ } catch (e) {
1945
+ // 离线降级:仅网络不可达(ARM 请求失败/超时)时本地创建(07 离线理念:出生不依赖 ARM);
1946
+ // 业务错误(如名称已存在)照常上抛——那种情况下静默转本地只会掩盖问题。
1947
+ if (!/^ARM 请求失败/.test(String(e.message))) return json(res, 502, { error: `创建失败: ${e.message}` });
1948
+ newId = localId; local = true;
1949
+ }
1950
+ const manifest = local
1951
+ ? writeLocalAgent(newId, { name: nm, description: String(description || '').trim(), prompt: pr })
1952
+ : await downloadAgent(newId); // 创建即物化本地(沿用下载链路)
1953
+ const reg = loadRegistry(); const entry = reg.agents.find((x) => x.agent_id === newId);
1954
+ if (entry) { entry.created_via_workstation = true; if (local) entry.local_only = true; } saveRegistry(reg);
1955
+ audit({ actor: 'user', action: local ? 'create-agent-local' : 'create-agent', agentId: newId, name: nm, knowledges: bound });
1956
+ return json(res, 200, { agent_id: newId, manifest, knowledges_bound: bound, local });
1957
+ }
1958
+ // 编辑档案(工作站创建的助手):ARM PUT 后重新物化,本地 prompt 同步更新;未发布档案仅写本地(版本前进)
1927
1959
  if (req.method === 'GET' && pathname.match(/^\/api\/agents\/[0-9a-f-]{36}\/profile$/)) {
1928
1960
  const id = pathname.split('/')[3];
1961
+ const le = loadRegistry().agents.find((x) => x.agent_id === id);
1962
+ if (le && le.local_only) { const m = localManifest(id) || {}; return json(res, 200, { name: m.name || le.name, description: String(m.description || ''), prompt: m.prompt || '' }); }
1929
1963
  try {
1930
1964
  const d = await armRest(`/api/v1/agents/${id}`);
1931
1965
  return json(res, 200, { name: d.name, description: String(d.description || ''), prompt: d.prompt || '' });
@@ -1939,6 +1973,12 @@ const server = http.createServer(async (req, res) => {
1939
1973
  if (description !== undefined) patch.description = String(description || '').trim().slice(0, 512);
1940
1974
  if (String(prompt || '').trim()) patch.prompt = String(prompt).trim();
1941
1975
  if (!Object.keys(patch).length) return json(res, 400, { error: '没有可更新字段' });
1976
+ const le = loadRegistry().agents.find((x) => x.agent_id === id);
1977
+ if (le && le.local_only) { // 未发布档案:仅写本地(版本前进可回滚),发布后编辑将同步 ARM
1978
+ const cur = localManifest(id) || {};
1979
+ const manifest = writeLocalAgent(id, { name: patch.name || cur.name, description: patch.description ?? cur.description, prompt: patch.prompt || cur.prompt }, { bump: true });
1980
+ return json(res, 200, { ok: true, manifest, local: true });
1981
+ }
1942
1982
  try {
1943
1983
  await armRest(`/api/v1/agents/${id}`, { method: 'PUT', body: JSON.stringify(patch) });
1944
1984
  let manifest = null;
@@ -1957,6 +1997,8 @@ const server = http.createServer(async (req, res) => {
1957
1997
  }
1958
1998
  let mBind = pathname.match(/^\/api\/agents\/([0-9a-f-]{36})\/bindings$/);
1959
1999
  if (mBind && req.method === 'GET') {
2000
+ const le = loadRegistry().agents.find((x) => x.agent_id === mBind[1]);
2001
+ if (le && le.local_only) return json(res, 200, { skills: [], knowledges: [], local_only: true }); // 未发布档案无 ARM 绑定
1960
2002
  try {
1961
2003
  const d = await armRest(`/api/v1/agents/${mBind[1]}`);
1962
2004
  return json(res, 200, {
@@ -2011,18 +2053,48 @@ const server = http.createServer(async (req, res) => {
2011
2053
  return json(res, 200, { agent_id: newId, name: nm, copied, manifest: { agent_id: manifest.agent_id, name: manifest.name, version: manifest.version } });
2012
2054
  } catch (e) { return json(res, 502, { error: `克隆失败: ${e.message}` }); }
2013
2055
  }
2014
- // 从 ARM 删除(危险操作,前端二次确认):可选同时卸载本地档案
2056
+ // 从 ARM 删除(危险操作,前端二次确认):可选同时卸载本地档案;未发布档案只存在于本地,直接卸载
2015
2057
  if (req.method === 'POST' && pathname.match(/^\/api\/agents\/[0-9a-f-]{36}\/arm-delete$/)) {
2016
2058
  const id = pathname.split('/')[3];
2017
2059
  const { also_uninstall, purge_workspaces } = await body(req);
2018
2060
  try {
2019
- await armRest(`/api/v1/agents/${id}`, { method: 'DELETE' });
2061
+ const le = loadRegistry().agents.find((x) => x.agent_id === id);
2062
+ if (!(le && le.local_only)) await armRest(`/api/v1/agents/${id}`, { method: 'DELETE' });
2020
2063
  let uninstalled = false;
2021
- if (also_uninstall && loadRegistry().agents.some((a) => a.agent_id === id)) uninstalled = uninstallLocalAgent(id, !!purge_workspaces);
2022
- audit({ actor: 'user', action: 'arm-delete-agent', agentId: id, also_uninstalled: uninstalled });
2064
+ if (also_uninstall && le) uninstalled = uninstallLocalAgent(id, !!purge_workspaces);
2065
+ audit({ actor: 'user', action: le && le.local_only ? 'delete-local-agent' : 'arm-delete-agent', agentId: id, also_uninstalled: uninstalled });
2023
2066
  return json(res, 200, { ok: true, uninstalled });
2024
2067
  } catch (e) { return json(res, 502, { error: `ARM 删除失败: ${e.message}` }); }
2025
2068
  }
2069
+ // 发布到 ARM(本地未发布档案转正):同 id 上行(新版 ARM),身份不变、本地 deployment/工作台/数据零迁移。
2070
+ // 幂等:同 id 已在 ARM(上次发布部分成功)→ 校验同名即续走转正;旧版 ARM 忽略 id 返回新 id → 明确报错提示升级。
2071
+ if (req.method === 'POST' && pathname.match(/^\/api\/agents\/[0-9a-f-]{36}\/publish$/)) {
2072
+ const id = pathname.match(/^\/api\/agents\/([0-9a-f-]{36})\/publish$/)[1];
2073
+ const reg = loadRegistry();
2074
+ const le = reg.agents.find((x) => x.agent_id === id);
2075
+ if (!le || !le.local_only) return json(res, 400, { error: '该助手不是本地未发布档案(可能已发布)' });
2076
+ const m = localManifest(id);
2077
+ if (!m) return json(res, 400, { error: '本地档案缺失,请先重新创建' });
2078
+ try {
2079
+ let created;
2080
+ try {
2081
+ created = await armRest('/api/v1/agents', { method: 'POST', body: JSON.stringify({ id, name: m.name, description: String(m.description || '').slice(0, 512), prompt: m.prompt || '', status: 'active' }) });
2082
+ } catch (e) {
2083
+ if (/该 id 已存在/.test(String(e.message))) { // 幂等续走:确认同 id 同名即视为已创建
2084
+ const d = await armRest(`/api/v1/agents/${id}`);
2085
+ if (!d || d.name !== m.name) throw e;
2086
+ created = d;
2087
+ } else throw e;
2088
+ }
2089
+ const finalId = created?.id || created?.agent?.id;
2090
+ if (finalId && finalId !== id) return json(res, 502, { error: '当前 ARM 版本不支持保持助手身份(忽略客户端 id)——请升级 ARM 后再发布' });
2091
+ const manifest = await downloadAgent(id); // 重新物化:source 转正、版本对齐 ARM
2092
+ const reg2 = loadRegistry(); const en = reg2.agents.find((x) => x.agent_id === id);
2093
+ if (en) { en.local_only = false; en.remote_version = manifest.version; } saveRegistry(reg2);
2094
+ audit({ actor: 'user', action: 'publish-agent', agentId: id, name: m.name, version: manifest.version });
2095
+ return json(res, 200, { ok: true, manifest });
2096
+ } catch (e) { return json(res, 502, { error: `发布失败: ${e.message}` }); }
2097
+ }
2026
2098
  // ── 本地助手库:下载 / 升级 / 版本历史 / 回滚 / 卸载 ──
2027
2099
  let mAgt = pathname.match(/^\/api\/agents\/([0-9a-f-]{36})\/(download|upgrade|versions|rollback)$/);
2028
2100
  if (mAgt && req.method === 'POST' && (mAgt[2] === 'download' || mAgt[2] === 'upgrade')) {