@whyshu/argent-erp 0.2.2 → 0.2.3

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whyshu/argent-erp",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "description": "Argent ERP 会话视图(dsh conversation.view)+ 六禾园办公室助理 preset(只聊办公室、只读 ERP)+ 办公室聊一聊入口",
6
6
  "keywords": [
package/src/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * argent-erp · 宿主侧插件(v0.2.2)
2
+ * argent-erp · 宿主侧插件(v0.2.3)
3
3
  *
4
4
  * 职责只有两件:
5
5
  * 1. 注册本地只读端点 `/api/argent-erp/*`(带账号 token 去 whyshu.com,**token 不下发前端**);
@@ -36,28 +36,57 @@ export function apply(ctx) {
36
36
  try {
37
37
  let disposeMask = null
38
38
  let lastMask = ''
39
+ /**
40
+ * 重入闸(2026-11-08 修):`ctx.tools.restrict()` 与它的 disposer **都会同步 emit
41
+ * `tools/change`**(见 dsh-scope `ScopedLayers.effect` 的两处 onChange)。
42
+ * 第一次收敛之后才出现的全局工具(典型:MCP 网关在启动后几秒才注册
43
+ * `mcp__argent__erp_*`)会让本函数在「dispose 旧 mask → restrict 新 mask」中途被
44
+ * 同步回调重入;此时 `lastMask` 还没更新 ⇒ 每层都判定"集合变了" ⇒ 无限同步递归
45
+ * (主线程 100% CPU + 堆 OOM,dsh 端口 bound 但 HTTP 不再响应)。
46
+ * 判据:工具名集合没变就只允许一个 reconcile 在跑;本函数自己触发的变化只记 dirty,
47
+ * 等这一轮收敛完再补一轮(保证真有新工具时不会漏收敛)。
48
+ */
49
+ let reconciling = false
50
+ let dirty = false
51
+
52
+ /** 收敛一轮:按当前全局工具名重算并换 mask。 */
53
+ const applyMask = () => {
54
+ // 全局视图(不传 scope)⇒ 拿到的是"未收敛"的全量工具,不受本作用域 mask 影响
55
+ const all = typeof ctx.tools?.definitions === 'function' ? ctx.tools.definitions() : []
56
+ const names = all.map((d) => (d && d.name) || '')
57
+ const allow = filterErpTools(names)
58
+ // 有 ERP 工具 ⇒ 只放行它们;一个都没有(没装 @whyshu/argent / MCP 未起)⇒ **全部禁掉**,
59
+ // 宁可这个会话无工具,也不让 Agent Team 等全局工具漏进来(边界优先)。
60
+ const mask = allow.length ? { allow } : (names.length ? { deny: names } : null)
61
+ if (!mask) return
62
+ const key = JSON.stringify(mask)
63
+ if (key === lastMask) return
64
+ // 先记账再动 mask:restrict/它的 disposer 同步 emit 时,重入会被闸挡住且只标 dirty;
65
+ // 即便真漏进来一次,key 也已是最新值,不会再次判定"集合变了"。
66
+ lastMask = key
67
+ try { disposeMask?.() } catch { /* noop */ }
68
+ disposeMask = ctx.tools.restrict(mask)
69
+ console.log(allow.length
70
+ ? `[argent-office] 工具边界:仅放行 ${allow.length} 个 ERP 只读工具`
71
+ : '[argent-office] 工具边界:未发现 ERP 工具 ⇒ 全部工具禁用(检查 @whyshu/argent 是否安装)')
72
+ }
39
73
 
40
74
  const reconcile = () => {
75
+ if (reconciling) { dirty = true; return }
76
+ reconciling = true
41
77
  try {
42
- // 全局视图(不传 scope)⇒ 拿到的是"未收敛"的全量工具,不受本作用域 mask 影响
43
- const all = typeof ctx.tools?.definitions === 'function' ? ctx.tools.definitions() : []
44
- const names = all.map((d) => (d && d.name) || '')
45
- const allow = filterErpTools(names)
46
- // 有 ERP 工具 ⇒ 只放行它们;一个都没有(没装 @whyshu/argent / MCP 未起)⇒ **全部禁掉**,
47
- // 宁可这个会话无工具,也不让 Agent Team 等全局工具漏进来(边界优先)。
48
- const mask = allow.length ? { allow } : (names.length ? { deny: names } : null)
49
- if (!mask) return
50
- const key = JSON.stringify(mask)
51
- if (key === lastMask) return
52
- try { disposeMask?.() } catch { /* noop */ }
53
- disposeMask = ctx.tools.restrict(mask)
54
- lastMask = key
55
- console.log(allow.length
56
- ? `[argent-office] 工具边界:仅放行 ${allow.length} 个 ERP 只读工具`
57
- : '[argent-office] 工具边界:未发现 ERP 工具 ⇒ 全部工具禁用(检查 @whyshu/argent 是否安装)')
78
+ // 最多补 20 轮:只有"收敛过程中又出现了新工具"才会再来一轮,正常情况 1 轮结束
79
+ for (let round = 0; round < 20; round++) {
80
+ dirty = false
81
+ applyMask()
82
+ if (!dirty) break
83
+ }
58
84
  } catch (e) {
59
- // 收敛失败不阻塞会话:提示词仍然是第二道边界
85
+ // 收敛失败不阻塞会话:提示词仍然是第二道边界;清掉 key 让下一次工具变化时重试
86
+ lastMask = ''
60
87
  console.warn('[argent-office] 工具边界收敛失败(继续按提示词边界运行):', (e && e.message) || e)
88
+ } finally {
89
+ reconciling = false
61
90
  }
62
91
  }
63
92