aims-workstation 0.1.13 → 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.13",
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
@@ -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 语义 ⊕ 关键词);不可达由调用方降级(本地知识关键词命中)
@@ -382,6 +396,27 @@ async function downloadAgent(agentId) {
382
396
  return manifest;
383
397
  }
384
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
+
385
420
  // 同步:比对远端目录与本地库,产出 new/update/delisted 事件;顺带冲账离线经验 outbox
386
421
  async function armSync() {
387
422
  const remote = await armListAgents();
@@ -1664,6 +1699,8 @@ const server = http.createServer(async (req, res) => {
1664
1699
  // ── 静态前端(web/dist 生产构建)──
1665
1700
  if (req.method === 'GET' && !pathname.startsWith('/api')) {
1666
1701
  let p = path.join(ROOT, 'web/dist', pathname === '/' ? 'index.html' : pathname);
1702
+ // 目录路径解析为其 index.html(否则 readFileSync 读目录返回空响应体)
1703
+ if (fs.existsSync(p) && fs.statSync(p).isDirectory()) p = path.join(p, 'index.html');
1667
1704
  if (!p.startsWith(path.join(ROOT, 'web/dist')) || !fs.existsSync(p)) p = path.join(ROOT, 'web/dist/index.html');
1668
1705
  if (fs.existsSync(p)) {
1669
1706
  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 +1862,8 @@ const server = http.createServer(async (req, res) => {
1825
1862
  installed_version: e.installed_version, remote_version: e.remote_version || null,
1826
1863
  update_available: !!(e.remote_version && cmpVersion(e.remote_version, e.installed_version) > 0),
1827
1864
  delisted: !!e.delisted, downloaded_at: e.downloaded_at || null,
1865
+ created_by_id: null, created_by: null, // 属主身份以远端目录为准(离线/已下架不可编辑,无需属主)
1866
+ local_only: !!e.local_only, // 本地未发布档案(离线创建):大厅显示「未发布」徽章 + 发布入口
1828
1867
  });
1829
1868
  }
1830
1869
  if (online) { // 静默刷新远端版本与下架标记(显式事件统计走 /api/arm/sync)——仅在完整拉取时;
@@ -1837,6 +1876,7 @@ const server = http.createServer(async (req, res) => {
1837
1876
  description: c.description || cur.description, remote_version: c.version,
1838
1877
  deployed: !!loadDeployment(c.agent_id),
1839
1878
  update_available: cur.installed ? cmpVersion(c.version, cur.installed_version) > 0 : false,
1879
+ created_by_id: c.created_by_id || null, created_by: c.created_by || null,
1840
1880
  });
1841
1881
  }
1842
1882
  if (!kw) {
@@ -1848,7 +1888,10 @@ const server = http.createServer(async (req, res) => {
1848
1888
  saveRegistry(reg);
1849
1889
  }
1850
1890
  }
1851
- return json(res, 200, { online, auth_error: authError, last_sync_at: reg.last_sync_at || null, agents: [...byId.values()] });
1891
+ // PAT 身份(编辑/删除门控依据:me.id vs 卡片 created_by_id,或 ADMIN):armMe 有缓存——
1892
+ // 身份只随 (ARM 源, 令牌) 变化,搜索防抖高频刷新目录时不重复打 /auth/me;失败降级 null 不阻断目录
1893
+ const me = online ? await armMe() : null;
1894
+ return json(res, 200, { online, auth_error: authError, me, last_sync_at: reg.last_sync_at || null, agents: [...byId.values()] });
1852
1895
  }
1853
1896
  // ── 新建助手:本地起草(AI 生成 prompt)→ ARM JSON 创建(创建即发布)→ 可选知识绑定(幂等)→ 下载物化 ──
1854
1897
  if (req.method === 'POST' && pathname === '/api/agents/draft-prompt') {
@@ -1887,24 +1930,36 @@ const server = http.createServer(async (req, res) => {
1887
1930
  const { name, description, prompt, knowledge_ids } = await body(req);
1888
1931
  const nm = String(name || '').trim(), pr = String(prompt || '').trim();
1889
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;
1890
1936
  try {
1891
- 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' }) });
1892
- 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;
1893
1939
  if (!newId) throw new Error('ARM 未返回新助手 id');
1894
- let bound = 0;
1895
1940
  for (const kid of (Array.isArray(knowledge_ids) ? knowledge_ids : [])) { // 绑定幂等(重复绑定返回已有版本)
1896
1941
  try { await armRest(`/api/v1/agents/${newId}/knowledges`, { method: 'POST', body: JSON.stringify({ knowledgeId: kid, kind: 'experience' }) }); bound++; }
1897
1942
  catch { /* 单条绑定失败不阻断创建 */ }
1898
1943
  }
1899
- const manifest = await downloadAgent(newId); // 创建即物化本地(沿用下载链路)
1900
- const reg = loadRegistry(); const entry = reg.agents.find((x) => x.agent_id === newId); if (entry) entry.created_via_workstation = true; saveRegistry(reg); // 大厅据此显示编辑/删除入口
1901
- audit({ actor: 'user', action: 'create-agent', agentId: newId, name: nm, knowledges: bound });
1902
- return json(res, 200, { agent_id: newId, manifest, knowledges_bound: bound });
1903
- } catch (e) { return json(res, 502, { error: `创建失败: ${e.message}` }); }
1904
- }
1905
- // 编辑档案(工作站创建的助手):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 同步更新;未发布档案仅写本地(版本前进)
1906
1959
  if (req.method === 'GET' && pathname.match(/^\/api\/agents\/[0-9a-f-]{36}\/profile$/)) {
1907
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 || '' }); }
1908
1963
  try {
1909
1964
  const d = await armRest(`/api/v1/agents/${id}`);
1910
1965
  return json(res, 200, { name: d.name, description: String(d.description || ''), prompt: d.prompt || '' });
@@ -1918,6 +1973,12 @@ const server = http.createServer(async (req, res) => {
1918
1973
  if (description !== undefined) patch.description = String(description || '').trim().slice(0, 512);
1919
1974
  if (String(prompt || '').trim()) patch.prompt = String(prompt).trim();
1920
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
+ }
1921
1982
  try {
1922
1983
  await armRest(`/api/v1/agents/${id}`, { method: 'PUT', body: JSON.stringify(patch) });
1923
1984
  let manifest = null;
@@ -1926,18 +1987,114 @@ const server = http.createServer(async (req, res) => {
1926
1987
  return json(res, 200, { ok: true, manifest });
1927
1988
  } catch (e) { return json(res, 502, { error: `更新失败: ${e.message}` }); }
1928
1989
  }
1929
- // 从 ARM 删除(危险操作,前端二次确认):可选同时卸载本地档案
1990
+ // ── 绑定管理(编辑档案对话框):skill/知识绑定增删代理 ARM,变更后重新物化本地(版本前进可回滚)──
1991
+ if (req.method === 'GET' && pathname === '/api/agents/skills') { // 绑定选择器的技能检索(对应知识检索 /api/agents/knowledges)
1992
+ const kwd = url.searchParams.get('keyword') || '';
1993
+ try {
1994
+ const d = await armRest(`/api/v1/skills?keyword=${encodeURIComponent(kwd)}&page=1&pageSize=20`);
1995
+ return json(res, 200, { items: (d.skills || []).map((s) => ({ id: s.id, name: s.name, description: String(s.description || '').slice(0, 80) })) });
1996
+ } catch (e) { return json(res, 200, { items: [], error: String(e.message).slice(0, 100) }); }
1997
+ }
1998
+ let mBind = pathname.match(/^\/api\/agents\/([0-9a-f-]{36})\/bindings$/);
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 绑定
2002
+ try {
2003
+ const d = await armRest(`/api/v1/agents/${mBind[1]}`);
2004
+ return json(res, 200, {
2005
+ 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) })),
2006
+ 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 })),
2007
+ });
2008
+ } catch (e) { return json(res, 502, { error: `绑定读取失败: ${e.message}` }); }
2009
+ }
2010
+ if (mBind && req.method === 'POST') {
2011
+ const { type, ref_id, kind } = await body(req);
2012
+ if (!['skill', 'knowledge'].includes(type) || !ref_id) return json(res, 400, { error: 'type 须为 skill|knowledge,且提供 ref_id' });
2013
+ const sub = type === 'skill' ? 'skills' : 'knowledges';
2014
+ const payload = type === 'skill' ? { skillId: ref_id } : { knowledgeId: ref_id, kind: kind === 'experience' ? 'experience' : 'essential' };
2015
+ try {
2016
+ const r = await armRest(`/api/v1/agents/${mBind[1]}/${sub}`, { method: 'POST', body: JSON.stringify(payload) });
2017
+ let manifest = null;
2018
+ try { manifest = await downloadAgent(mBind[1]); } catch { /* 物化失败不回滚 ARM 变更,提示用户重新下载 */ }
2019
+ audit({ actor: 'user', action: 'bind-agent-asset', agentId: mBind[1], type, ref_id, version: r?.version });
2020
+ return json(res, 200, { ok: true, version: r?.version || null, manifest });
2021
+ } catch (e) { return json(res, 502, { error: `绑定失败: ${e.message}` }); }
2022
+ }
2023
+ if (mBind && req.method === 'DELETE') {
2024
+ const { type, binding_id } = await body(req);
2025
+ if (!['skill', 'knowledge'].includes(type) || !binding_id) return json(res, 400, { error: 'type 须为 skill|knowledge,且提供 binding_id' });
2026
+ try {
2027
+ await armRest(`/api/v1/agents/${mBind[1]}/${type === 'skill' ? 'skills' : 'knowledges'}?bindingId=${encodeURIComponent(binding_id)}`, { method: 'DELETE' });
2028
+ let manifest = null;
2029
+ try { manifest = await downloadAgent(mBind[1]); } catch { }
2030
+ audit({ actor: 'user', action: 'unbind-agent-asset', agentId: mBind[1], type, binding_id });
2031
+ return json(res, 200, { ok: true, manifest });
2032
+ } catch (e) { return json(res, 502, { error: `解绑失败: ${e.message}` }); }
2033
+ }
2034
+ // 克隆助手(fork):复制档案 + 全部绑定,创建归当前 PAT 用户的新助手——非属主想改 / 想基于
2035
+ // 已有快速调教的正路。无需对源助手的编辑权限(源详情是公开读);新助手属主即当前用户,
2036
+ // 后续绑定守卫天然放行。绑定逐条 best-effort(单条失败计数不阻断),UI 资产跟随最新版本。
2037
+ if (req.method === 'POST' && pathname.match(/^\/api\/agents\/([0-9a-f-]{36})\/clone$/)) {
2038
+ const id = pathname.match(/^\/api\/agents\/([0-9a-f-]{36})\/clone$/)[1];
2039
+ const { name, description } = await body(req);
2040
+ try {
2041
+ const src = await armRest(`/api/v1/agents/${id}`);
2042
+ const nm = String(name || `${src.name}-副本`).trim();
2043
+ 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' }) });
2044
+ const newId = created?.id || created?.agent?.id;
2045
+ if (!newId) throw new Error('ARM 未返回新助手 id');
2046
+ const copied = { skills: 0, knowledges: 0, ui_assets: 0, failed: 0 };
2047
+ 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++; } }
2048
+ 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++; } }
2049
+ 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++; } }
2050
+ const manifest = await downloadAgent(newId); // 创建即物化(沿用新建链路)
2051
+ const reg = loadRegistry(); const entry = reg.agents.find((x) => x.agent_id === newId); if (entry) entry.created_via_workstation = true; saveRegistry(reg);
2052
+ audit({ actor: 'user', action: 'clone-agent', agentId: newId, source: id, name: nm, ...copied });
2053
+ return json(res, 200, { agent_id: newId, name: nm, copied, manifest: { agent_id: manifest.agent_id, name: manifest.name, version: manifest.version } });
2054
+ } catch (e) { return json(res, 502, { error: `克隆失败: ${e.message}` }); }
2055
+ }
2056
+ // 从 ARM 删除(危险操作,前端二次确认):可选同时卸载本地档案;未发布档案只存在于本地,直接卸载
1930
2057
  if (req.method === 'POST' && pathname.match(/^\/api\/agents\/[0-9a-f-]{36}\/arm-delete$/)) {
1931
2058
  const id = pathname.split('/')[3];
1932
2059
  const { also_uninstall, purge_workspaces } = await body(req);
1933
2060
  try {
1934
- 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' });
1935
2063
  let uninstalled = false;
1936
- if (also_uninstall && loadRegistry().agents.some((a) => a.agent_id === id)) uninstalled = uninstallLocalAgent(id, !!purge_workspaces);
1937
- 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 });
1938
2066
  return json(res, 200, { ok: true, uninstalled });
1939
2067
  } catch (e) { return json(res, 502, { error: `ARM 删除失败: ${e.message}` }); }
1940
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
+ }
1941
2098
  // ── 本地助手库:下载 / 升级 / 版本历史 / 回滚 / 卸载 ──
1942
2099
  let mAgt = pathname.match(/^\/api\/agents\/([0-9a-f-]{36})\/(download|upgrade|versions|rollback)$/);
1943
2100
  if (mAgt && req.method === 'POST' && (mAgt[2] === 'download' || mAgt[2] === 'upgrade')) {
@@ -2379,14 +2536,16 @@ const server = http.createServer(async (req, res) => {
2379
2536
  dsl.arm_ref = armRef;
2380
2537
  fs.writeFileSync(target, JSON.stringify(dsl, null, 2));
2381
2538
  }
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;
2539
+ let bound = null, bind_error = null;
2540
+ if (bind_agent) { // 发布后顺手绑定到当前助手(三资产组合:员工自带工位);失败不回滚发布(如非属主 403:仅助手创建者或管理员可绑定)
2541
+ try {
2542
+ const r = await armRest(`/api/v1/agents/${bind_agent}/ui-assets`, { method: 'POST', body: JSON.stringify({ name: bundle.name }) });
2543
+ bound = r && r.version;
2544
+ } catch (e) { bind_error = String(e.message).slice(0, 200); }
2386
2545
  }
2387
2546
  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 });
2547
+ audit({ actor: 'asset-share', target: 'ARM', type, name: bundle.name, version: uploaded.version, bound, bind_error });
2548
+ return json(res, 200, { name: bundle.name, version: uploaded.version, requires: bundle.manifest.requires, bound, bind_error });
2390
2549
  } catch (e) { return json(res, 400, { error: String(e.message).slice(0, 400) }); }
2391
2550
  }
2392
2551
  if (req.method === 'GET' && pathname === '/api/assets/arm-library') {