dsh-erpbot 0.1.0 → 0.1.1

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": "dsh-erpbot",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "description": "erpBot (Harness Erp): a bilingual finance/ERP panel with four business bots, a merged base page and an Aurora-style dashboard.",
6
6
  "keywords": [
@@ -30,6 +30,7 @@
30
30
  "main": "./index.js",
31
31
  "exports": {
32
32
  ".": "./index.js",
33
+ "./bot-identity": "./bot-identity.js",
33
34
  "./client": "./client.js",
34
35
  "./cordis.patch.yml": "./cordis.patch.yml",
35
36
  "./package.json": "./package.json",
@@ -39,11 +40,14 @@
39
40
  },
40
41
  "files": [
41
42
  "index.js",
43
+ "bot-identity.js",
42
44
  "client.js",
43
45
  "src/",
44
- "scripts/build.mjs",
46
+ "scripts/",
45
47
  "cordis.patch.yml",
46
48
  "locale/*.json",
49
+ "docs/",
50
+ "spec/",
47
51
  "icon.svg",
48
52
  "README.md",
49
53
  "LICENSE"
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Cross-check the two copies of the bot identity.
3
+ *
4
+ * The Host half owns bot-identity.js (used to scaffold bot.json / AGENTS.md /
5
+ * SKILLS.md at mount). The browser bundle owns src/bot-data.js (used to render).
6
+ * They must agree on everything that reaches a generated file, or a bot's
7
+ * on-disk identity would disagree with what its page shows.
8
+ *
9
+ * Usage: node scripts/check-bot-identity.mjs (exit 0 = consistent)
10
+ */
11
+ import { readFileSync } from 'node:fs'
12
+ import { dirname, join } from 'node:path'
13
+ import { fileURLToPath } from 'node:url'
14
+ import { pathToFileURL } from 'node:url'
15
+
16
+ const here = dirname(fileURLToPath(import.meta.url))
17
+ const root = join(here, '..')
18
+
19
+ const { BOT_IDENTITY } = await import(pathToFileURL(join(root, 'bot-identity.js')).href)
20
+
21
+ /* src/bot-data.js is a fragment (top-level `const`, no exports), so evaluate it
22
+ in an isolated scope and read the two bindings it declares. */
23
+ const fragment = readFileSync(join(root, 'src', 'bot-data.js'), 'utf8')
24
+ const sandbox = {}
25
+ new Function('out', fragment + '\n;out.BOTS = BOTS;out.BOT_IDS = BOT_IDS;')(sandbox)
26
+ const clientBots = sandbox.BOTS
27
+ const clientIds = sandbox.BOT_IDS
28
+
29
+ const FIELDS = ['name', 'role', 'duties', 'tone', 'boundaries', 'notApplicable', 'scene']
30
+ let failures = 0
31
+
32
+ const hostIds = Object.keys(BOT_IDENTITY)
33
+
34
+ if (hostIds.join(',') !== clientIds.join(',')) {
35
+ failures += 1
36
+ console.log(`FAIL ids: host=[${hostIds.join(',')}] client=[${clientIds.join(',')}]`)
37
+ } else {
38
+ console.log(`ok ids: ${hostIds.join(', ')}`)
39
+ }
40
+
41
+ for (const id of hostIds) {
42
+ const host = BOT_IDENTITY[id]
43
+ const client = clientBots[id]
44
+ if (client === undefined) {
45
+ failures += 1
46
+ console.log(`FAIL ${id}: missing in src/bot-data.js`)
47
+ continue
48
+ }
49
+ const drift = []
50
+ for (const field of FIELDS) {
51
+ const hostValue = host[field]
52
+ const clientValue = client[field]
53
+ if (hostValue === undefined || clientValue === undefined) {
54
+ drift.push(`${field} (missing on ${hostValue === undefined ? 'host' : 'client'})`)
55
+ continue
56
+ }
57
+ for (const lang of ['zh', 'en']) {
58
+ if (hostValue[lang] !== clientValue[lang]) drift.push(`${field}.${lang}`)
59
+ }
60
+ }
61
+ if (drift.length > 0) {
62
+ failures += 1
63
+ console.log(`FAIL ${id}: drifted fields → ${drift.join(', ')}`)
64
+ } else {
65
+ console.log(`ok ${id}: ${FIELDS.length} fields agree`)
66
+ }
67
+ }
68
+
69
+ console.log('')
70
+ console.log(failures === 0 ? 'bot identity consistent ✅' : `${failures} inconsistency group(s)`)
71
+ process.exit(failures === 0 ? 0 : 1)
@@ -0,0 +1,315 @@
1
+ /**
2
+ * 防回归静态检查:找出每个工厂(createXxx)函数体里「未注入、未在工厂任意
3
+ * 作用域声明、非平台全局」的自由标识符引用。
4
+ *
5
+ * 背景:0c41a01 工厂化重构把顶层符号(LANGS / SHORT_LOCALE / LANG_BADGE / readLang /
6
+ * setLang / drawTrend)挪进了 createI18n/createChart 的闭包,但其它工厂的函数体
7
+ * 仍按「顶层符号」引用它们。verify.mjs 只查加载期(直接子声明初始化表达式),
8
+ * 故意不递归函数体,所以这类调用期 ReferenceError 会漏网 —— 症状就是 HerpPanel
9
+ * 渲染时抛错、main slot 空白。
10
+ *
11
+ * 本检查器补齐这块:对每个 createXxx 工厂,收集其「整个函数体(含所有嵌套
12
+ * 函数)里出现的标识符」,减去三样东西后剩下的就是未注入自由引用:
13
+ * 1. deps 参数解构出来的注入符号;
14
+ * 2. 工厂任意层级作用域里声明的名字(含嵌套函数参数 / 局部变量 / 内部函数名 /
15
+ * catch 参数 / 函数表达式名);
16
+ * 3. 已知的平台 / 全局名字。
17
+ *
18
+ * 用法:node scripts/lint-factories.mjs (退出码 0 = 无未注入自由引用)
19
+ */
20
+ import { readFileSync } from 'node:fs'
21
+ import { dirname, join } from 'node:path'
22
+ import { fileURLToPath } from 'node:url'
23
+ import { createRequire } from 'node:module'
24
+
25
+ const here = dirname(fileURLToPath(import.meta.url))
26
+ const root = join(here, '..')
27
+ const require = createRequire(join(here, 'noop.js'))
28
+
29
+ let acorn
30
+ try { acorn = require(join(root, 'node_modules', 'acorn')) }
31
+ catch { throw new Error('scripts/lint-factories.mjs: acorn not installed — run `npm install` first') }
32
+
33
+ const fileArg = process.argv.find((a) => a.startsWith('--file='))
34
+ const clientPath = fileArg ? fileArg.slice('--file='.length) : join(root, 'client.js')
35
+ const code = readFileSync(clientPath, 'utf8')
36
+ const ast = acorn.parse(code, { ecmaVersion: 2022, sourceType: 'script' })
37
+
38
+ // 平台/全局允许名单
39
+ const GLOBALS = new Set([
40
+ 'React', 'h', 'window', 'document', 'console', 'fetch',
41
+ 'Math', 'Date', 'JSON', 'RegExp', 'Error', 'TypeError', 'String', 'Number', 'Array', 'Object',
42
+ 'Promise', 'Intl', 'Set', 'Map', 'Symbol', 'isFinite', 'parseFloat', 'parseInt', 'NaN', 'Infinity',
43
+ 'encodeURIComponent', 'decodeURIComponent', 'encodeURI',
44
+ 'undefined', 'arguments',
45
+ 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval',
46
+ 'requestAnimationFrame', 'cancelAnimationFrame', 'ResizeObserver',
47
+ 'localStorage', 'SVG_NS', 'PANEL_ID', 'NS',
48
+ ])
49
+
50
+ // 收集绑定标识符(声明/参数位置出现的名字)
51
+ function collectBinding(pat, out) {
52
+ if (!pat) return
53
+ switch (pat.type) {
54
+ case 'Identifier': out.add(pat.name); return
55
+ case 'ObjectPattern': for (const p of pat.properties) collectBinding(p.type === 'RestElement' ? p.argument : p.value, out); return
56
+ case 'ArrayPattern': for (const e of pat.elements) collectBinding(e, out); return
57
+ case 'AssignmentPattern': collectBinding(pat.left, out); return
58
+ case 'RestElement': collectBinding(pat.argument, out); return
59
+ default: return
60
+ }
61
+ }
62
+
63
+ // 收集一个函数作用域内的全部局部声明名(含参数、内部函数名、catch 参数),
64
+ // 并递归进嵌套函数。
65
+ function collectScope(node, out) {
66
+ for (const param of node.params || []) collectBinding(param, out)
67
+ const body = node.body
68
+ const stmts = body.type === 'BlockStatement' ? body.body : [body]
69
+ for (const stmt of stmts) {
70
+ if (stmt.type === 'VariableDeclaration') {
71
+ for (const d of stmt.declarations) {
72
+ collectBinding(d.id, out)
73
+ // 初始化表达式里的函数表达式/箭头函数也要递归(它们有自己的作用域)
74
+ if (d.init) collectNestedFunctions(d.init, out)
75
+ }
76
+ } else if ((stmt.type === 'FunctionDeclaration' || stmt.type === 'ClassDeclaration') && stmt.id) {
77
+ out.add(stmt.id.name)
78
+ if (stmt.type === 'FunctionDeclaration') collectScope(stmt, out)
79
+ } else {
80
+ collectNestedFunctions(stmt, out)
81
+ }
82
+ }
83
+ }
84
+
85
+ // 递归遍历节点,收集其中所有嵌套函数的作用域声明 + 块级变量声明
86
+ function collectNestedFunctions(node, out) {
87
+ if (!node || typeof node !== 'object') return
88
+ if (node.type === 'FunctionDeclaration') {
89
+ if (node.id) out.add(node.id.name)
90
+ collectScope(node, out)
91
+ return
92
+ }
93
+ if (node.type === 'FunctionExpression') {
94
+ if (node.id) out.add(node.id.name)
95
+ collectScope(node, out)
96
+ return
97
+ }
98
+ if (node.type === 'ArrowFunctionExpression') {
99
+ collectScope(node, out)
100
+ return
101
+ }
102
+ if (node.type === 'CatchClause') {
103
+ collectBinding(node.param, out)
104
+ collectNestedFunctions(node.body, out)
105
+ return
106
+ }
107
+ if (node.type === 'VariableDeclaration') {
108
+ for (const d of node.declarations) {
109
+ collectBinding(d.id, out)
110
+ if (d.init) collectNestedFunctions(d.init, out)
111
+ }
112
+ return
113
+ }
114
+ for (const key of Object.keys(node)) {
115
+ if (['type', 'start', 'end', 'loc', 'range'].includes(key)) continue
116
+ const v = node[key]
117
+ if (Array.isArray(v)) { for (const it of v) if (it && typeof it === 'object') collectNestedFunctions(it, out) }
118
+ else if (v && typeof v === 'object') collectNestedFunctions(v, out)
119
+ }
120
+ }
121
+
122
+ // 收集一个节点内出现的全部标识符(引用位置)
123
+ function collectIdents(node, out) {
124
+ if (!node || typeof node !== 'object') return
125
+ if (node.type === 'Identifier') { out.add(node.name); return }
126
+ // 属性键 / 成员名不是引用
127
+ if (node.type === 'Property' && !node.computed && node.key === node.value) return // 简写 {a}
128
+ for (const key of Object.keys(node)) {
129
+ if (['type', 'start', 'end', 'loc', 'range'].includes(key)) continue
130
+ if (key === 'key' && node.type === 'Property' && !node.computed) continue
131
+ if (key === 'key' && node.type === 'MethodDefinition' && !node.computed) continue
132
+ if (key === 'property' && node.type === 'MemberExpression' && !node.computed) continue
133
+ const v = node[key]
134
+ if (Array.isArray(v)) { for (const it of v) if (it && typeof it === 'object') collectIdents(it, out) }
135
+ else if (v && typeof v === 'object') collectIdents(v, out)
136
+ }
137
+ }
138
+
139
+ const factoryNodes = []
140
+ function findFactories(node) {
141
+ if (!node || typeof node !== 'object') return
142
+ if (node.type === 'FunctionDeclaration' && node.id && /^create[A-Z]/.test(node.id.name)) {
143
+ factoryNodes.push(node)
144
+ return // 不递归进工厂内部(避免误把内部函数当工厂)
145
+ }
146
+ for (const key of Object.keys(node)) {
147
+ if (['type', 'start', 'end', 'loc', 'range'].includes(key)) continue
148
+ const v = node[key]
149
+ if (Array.isArray(v)) { for (const it of v) if (it && it.type) findFactories(it) }
150
+ else if (v && typeof v === 'object' && v.type) findFactories(v)
151
+ }
152
+ }
153
+ findFactories(ast)
154
+
155
+ let failures = 0
156
+
157
+ for (const factory of factoryNodes) {
158
+ const name = factory.id.name
159
+ const params = factory.params.map((p) => p.name)
160
+
161
+ // 1) deps 解构注入符号
162
+ const injected = new Set()
163
+ for (const stmt of factory.body.body) {
164
+ if (stmt.type === 'VariableDeclaration') {
165
+ for (const d of stmt.declarations) {
166
+ if (d.init && d.init.type === 'Identifier' && params.includes(d.init.name)) {
167
+ collectBinding(d.id, injected)
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ // 2) 工厂全作用域局部声明(含参数、所有嵌套层)
174
+ const local = new Set(params)
175
+ for (const stmt of factory.body.body) {
176
+ if (stmt.type === 'VariableDeclaration') {
177
+ for (const d of stmt.declarations) {
178
+ collectBinding(d.id, local)
179
+ if (d.init) collectNestedFunctions(d.init, local)
180
+ }
181
+ } else if ((stmt.type === 'FunctionDeclaration' || stmt.type === 'ClassDeclaration') && stmt.id) {
182
+ local.add(stmt.id.name)
183
+ if (stmt.type === 'FunctionDeclaration') collectScope(stmt, local)
184
+ } else {
185
+ collectNestedFunctions(stmt, local)
186
+ }
187
+ }
188
+
189
+ // 3) 全函数体标识符
190
+ const used = new Set()
191
+ collectIdents(factory.body, used)
192
+
193
+ // 4) 自由引用
194
+ const free = [...used].filter((id) =>
195
+ !injected.has(id) && !local.has(id) && !GLOBALS.has(id))
196
+
197
+ if (free.length > 0) {
198
+ failures += 1
199
+ console.log(`FAIL ${name}: 未注入的自由引用 → ${free.sort().join(', ')}`)
200
+ } else {
201
+ console.log(`ok ${name}: 无未注入自由引用`)
202
+ }
203
+ }
204
+
205
+ /* =====================================================================
206
+ * 第二部分:apply() 的顶层符号检查。
207
+ *
208
+ * 工厂化把符号关进了各自闭包,而 mount.js 的 apply() 是「模块作用域」函数,
209
+ * 它只能引用 entry.js 解构回顶层的符号。entry.js 漏解构一个(例如
210
+ * refreshTeamChat),verify.mjs 不查函数体、上面的工厂检查也只看工厂内部,
211
+ * 于是症状同样落在运行期:apply 抛 ReferenceError,整个面板空白。
212
+ *
213
+ * 判据:apply() 函数体里出现的自由标识符,必须落在
214
+ * 1. 它自己的参数 / 局部声明;
215
+ * 2. 平台全局;
216
+ * 3. 模块作用域(loader factory 的直接子声明,含 entry.js 的解构)之内。
217
+ * ===================================================================== */
218
+
219
+ // 定位 __ModuleLoader__.load({ factory() { ... } }) 里的模块作用域函数
220
+ let moduleFn = null
221
+ function findModuleFn(node) {
222
+ if (moduleFn || !node || typeof node !== 'object') return
223
+ if (node.type === 'CallExpression' && node.arguments && node.arguments.length > 0) {
224
+ const first = node.arguments[0]
225
+ if (first && first.type === 'ObjectExpression') {
226
+ for (const prop of first.properties) {
227
+ const key = prop.key && (prop.key.name || prop.key.value)
228
+ const value = prop.value
229
+ if (key === 'factory' && value
230
+ && (value.type === 'FunctionExpression' || value.type === 'ArrowFunctionExpression')) {
231
+ moduleFn = value
232
+ return
233
+ }
234
+ }
235
+ }
236
+ }
237
+ for (const key of Object.keys(node)) {
238
+ if (['type', 'start', 'end', 'loc', 'range'].includes(key)) continue
239
+ const v = node[key]
240
+ if (Array.isArray(v)) { for (const it of v) if (it && it.type) findModuleFn(it) }
241
+ else if (v && typeof v === 'object' && v.type) findModuleFn(v)
242
+ }
243
+ }
244
+ findModuleFn(ast)
245
+
246
+ // 模块作用域的「直接子声明」——只有这些是 apply 能看见的顶层符号
247
+ const moduleScope = new Set()
248
+ if (moduleFn !== null) {
249
+ const body = moduleFn.body
250
+ const stmts = body.type === 'BlockStatement' ? body.body : []
251
+ for (const stmt of stmts) {
252
+ if (stmt.type === 'VariableDeclaration') {
253
+ for (const d of stmt.declarations) collectBinding(d.id, moduleScope)
254
+ } else if ((stmt.type === 'FunctionDeclaration' || stmt.type === 'ClassDeclaration') && stmt.id) {
255
+ moduleScope.add(stmt.id.name)
256
+ }
257
+ }
258
+ }
259
+
260
+ // 收集所有名为 apply 的函数值
261
+ const applyNodes = []
262
+ function findApply(node) {
263
+ if (!node || typeof node !== 'object') return
264
+ const key = node.key && (node.key.name || node.key.value)
265
+ if (key === 'apply' && node.value
266
+ && (node.value.type === 'FunctionExpression' || node.value.type === 'ArrowFunctionExpression')) {
267
+ applyNodes.push(node.value)
268
+ return
269
+ }
270
+ for (const k of Object.keys(node)) {
271
+ if (['type', 'start', 'end', 'loc', 'range'].includes(k)) continue
272
+ const v = node[k]
273
+ if (Array.isArray(v)) { for (const it of v) if (it && it.type) findApply(it) }
274
+ else if (v && typeof v === 'object' && v.type) findApply(v)
275
+ }
276
+ }
277
+ if (moduleFn !== null) findApply(moduleFn)
278
+
279
+ if (applyNodes.length === 0) {
280
+ console.log('FAIL apply(): 未找到模块作用域里的 apply(),检查器需要同步 DSH 插件形态')
281
+ failures += 1
282
+ } else if (moduleScope.size === 0) {
283
+ console.log('FAIL apply(): 未解析出模块作用域的直接子声明')
284
+ failures += 1
285
+ } else {
286
+ for (const applyFn of applyNodes) {
287
+ const local = new Set()
288
+ for (const param of applyFn.params || []) collectBinding(param, local)
289
+ const body = applyFn.body
290
+ const stmts = body.type === 'BlockStatement' ? body.body : [body]
291
+ for (const stmt of stmts) {
292
+ if (stmt.type === 'VariableDeclaration') {
293
+ for (const d of stmt.declarations) collectBinding(d.id, local)
294
+ } else if ((stmt.type === 'FunctionDeclaration' || stmt.type === 'ClassDeclaration') && stmt.id) {
295
+ local.add(stmt.id.name)
296
+ }
297
+ collectNestedFunctions(stmt, local)
298
+ }
299
+ const used = new Set()
300
+ collectIdents(applyFn.body, used)
301
+ const free = [...used].filter((id) =>
302
+ !local.has(id) && !GLOBALS.has(id) && !moduleScope.has(id))
303
+ if (free.length > 0) {
304
+ failures += 1
305
+ console.log(`FAIL apply(): 引用了未在模块作用域声明的符号 → ${free.sort().join(', ')}`)
306
+ console.log(' (多半是 entry.js 少解构了一个工厂导出;apply 只能看到顶层符号)')
307
+ } else {
308
+ console.log('ok apply(): 引用的顶层符号均已解构')
309
+ }
310
+ }
311
+ }
312
+
313
+ console.log('')
314
+ console.log(failures === 0 ? '全部工厂与 apply 均无未注入自由引用 ✅' : `${failures} 处存在未注入自由引用`)
315
+ process.exit(failures === 0 ? 0 : 1)
@@ -0,0 +1,179 @@
1
+ /**
2
+ * lint-scopes — catch cross-component undeclared references.
3
+ *
4
+ * The gap this closes: a helper declared inside ONE component but called from
5
+ * ANOTHER. Syntax is valid, so acorn/verify/build-check all pass; and
6
+ * lint-factories treats any declaration anywhere inside a factory as
7
+ * "declared", so it passes too. At runtime it is a ReferenceError that blanks
8
+ * the panel — which is exactly how `drawerHead` broke TeamShell.
9
+ *
10
+ * Method: for every function declared at factory scope, walk its body with a
11
+ * real lexical scope chain (params + declarations visible in each block,
12
+ * stopping at nested function boundaries), and report identifiers that resolve
13
+ * to nothing.
14
+ *
15
+ * Usage: node scripts/lint-scopes.mjs [file]
16
+ */
17
+ import { readFileSync } from 'node:fs'
18
+ import { join, dirname, resolve } from 'node:path'
19
+ import { fileURLToPath } from 'node:url'
20
+ import { createRequire } from 'node:module'
21
+
22
+ const here = dirname(fileURLToPath(import.meta.url))
23
+ const require = createRequire(join(here, '..', 'package.json'))
24
+ const acorn = require('acorn')
25
+
26
+ const target = process.argv[2]
27
+ ? resolve(process.argv[2])
28
+ : join(here, '..', 'src', 'workbench', 'index.js')
29
+
30
+ const src = readFileSync(target, 'utf8')
31
+ const ast = acorn.parse(src, { ecmaVersion: 'latest', sourceType: 'script', locations: true })
32
+
33
+ const GLOBALS = new Set([
34
+ 'React', 'h', 'window', 'document', 'console', 'fetch',
35
+ 'Math', 'Date', 'JSON', 'RegExp', 'Error', 'TypeError', 'String', 'Number', 'Array', 'Object',
36
+ 'Promise', 'Intl', 'Set', 'Map', 'Symbol', 'isFinite', 'parseFloat', 'parseInt', 'NaN', 'Infinity',
37
+ 'encodeURIComponent', 'decodeURIComponent', 'encodeURI', 'Boolean',
38
+ 'undefined', 'arguments', 'this',
39
+ 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval',
40
+ 'requestAnimationFrame', 'cancelAnimationFrame', 'ResizeObserver',
41
+ 'localStorage', 'SVG_NS', 'PANEL_ID', 'NS',
42
+ ])
43
+
44
+ function collectBinding(pat, out) {
45
+ if (!pat) return
46
+ switch (pat.type) {
47
+ case 'Identifier': out.add(pat.name); break
48
+ case 'ObjectPattern':
49
+ for (const p of pat.properties) collectBinding(p.type === 'RestElement' ? p.argument : p.value, out)
50
+ break
51
+ case 'ArrayPattern': for (const e of pat.elements) collectBinding(e, out); break
52
+ case 'RestElement': collectBinding(pat.argument, out); break
53
+ case 'AssignmentPattern': collectBinding(pat.left, out); break
54
+ default: break
55
+ }
56
+ }
57
+
58
+ /** Names visible in a scope: recurse through blocks, stop at function boundaries. */
59
+ function collectVisible(node, out) {
60
+ if (!node || typeof node !== 'object') return
61
+ switch (node.type) {
62
+ case 'VariableDeclaration':
63
+ for (const d of node.declarations) collectBinding(d.id, out)
64
+ return
65
+ case 'FunctionDeclaration': if (node.id) out.add(node.id.name); return
66
+ case 'FunctionExpression':
67
+ case 'ArrowFunctionExpression':
68
+ return
69
+ case 'ClassDeclaration': if (node.id) out.add(node.id.name); return
70
+ case 'CatchClause': if (node.param) collectBinding(node.param, out); break
71
+ default: break
72
+ }
73
+ for (const k of Object.keys(node)) {
74
+ if (k === 'loc' || k === 'start' || k === 'end') continue
75
+ const v = node[k]
76
+ if (Array.isArray(v)) { for (const it of v) collectVisible(it, out) }
77
+ else if (v && typeof v === 'object') collectVisible(v, out)
78
+ }
79
+ }
80
+
81
+ const problems = []
82
+
83
+ function analyze(fn, enclosing, label) {
84
+ const scope = new Set(enclosing)
85
+ for (const p of fn.params) collectBinding(p, scope)
86
+ const stmts = fn.body.type === 'BlockStatement' ? fn.body.body : []
87
+ for (const s of stmts) collectVisible(s, scope)
88
+
89
+ const visit = (node) => {
90
+ if (!node || typeof node !== 'object') return
91
+ switch (node.type) {
92
+ case 'FunctionDeclaration': {
93
+ const inner = node.id ? node.id.name : '(anonymous)'
94
+ analyze(node, scope, label + ' > ' + inner)
95
+ return
96
+ }
97
+ case 'FunctionExpression':
98
+ case 'ArrowFunctionExpression':
99
+ analyze(node, scope, label + ' > (anonymous)')
100
+ return
101
+ case 'Identifier':
102
+ if (!scope.has(node.name) && !GLOBALS.has(node.name)) {
103
+ problems.push({ label, name: node.name, line: node.loc.start.line })
104
+ }
105
+ return
106
+ case 'MemberExpression':
107
+ visit(node.object)
108
+ if (node.computed) visit(node.property)
109
+ return
110
+ case 'Property':
111
+ if (node.computed) visit(node.key)
112
+ visit(node.value)
113
+ return
114
+ case 'VariableDeclarator':
115
+ visit(node.init)
116
+ return
117
+ case 'LabeledStatement':
118
+ visit(node.body)
119
+ return
120
+ case 'BreakStatement':
121
+ case 'ContinueStatement':
122
+ return
123
+ default: break
124
+ }
125
+ for (const k of Object.keys(node)) {
126
+ if (k === 'loc' || k === 'start' || k === 'end') continue
127
+ const v = node[k]
128
+ if (Array.isArray(v)) { for (const it of v) visit(it) }
129
+ else if (v && typeof v === 'object') visit(v)
130
+ }
131
+ }
132
+
133
+ for (const s of stmts) visit(s)
134
+ }
135
+
136
+ let factory = null
137
+ ;(function find(node) {
138
+ if (!node || typeof node !== 'object' || factory) return
139
+ if (node.type === 'FunctionDeclaration' && node.id && node.id.name === 'createWorkbench') {
140
+ factory = node
141
+ return
142
+ }
143
+ for (const k of Object.keys(node)) {
144
+ const v = node[k]
145
+ if (Array.isArray(v)) { for (const it of v) find(it) }
146
+ else if (v && typeof v === 'object') find(v)
147
+ }
148
+ })(ast)
149
+
150
+ if (!factory) {
151
+ console.log('跳过:未找到 createWorkbench')
152
+ process.exit(0)
153
+ }
154
+
155
+ const factoryScope = new Set()
156
+ for (const p of factory.params) collectBinding(p, factoryScope)
157
+ for (const s of factory.body.body) collectVisible(s, factoryScope)
158
+
159
+ for (const stmt of factory.body.body) {
160
+ if (stmt && stmt.type === 'FunctionDeclaration' && stmt.id) {
161
+ analyze(stmt, factoryScope, stmt.id.name)
162
+ }
163
+ }
164
+
165
+ const seen = new Set()
166
+ const unique = problems.filter((p) => {
167
+ const k = p.label + '|' + p.name
168
+ if (seen.has(k)) return false
169
+ seen.add(k)
170
+ return true
171
+ })
172
+
173
+ if (unique.length) {
174
+ console.log(`FAIL 跨组件未声明引用 ${unique.length} 处:`)
175
+ for (const p of unique) console.log(` ${p.label} → ${p.name} (L${p.line})`)
176
+ process.exit(1)
177
+ }
178
+
179
+ console.log(`ok 作用域检查通过(${target.split(/[\\/]/).pop()})`)
@@ -0,0 +1,76 @@
1
+ /**
2
+ * One-shot run of the mount-time scaffolding, so the four bot workspaces can be
3
+ * verified without a DSH restart. Mirrors index.js scaffoldBotFiles exactly.
4
+ * Usage: node scripts/scaffold-bots.mjs [--root <dir>]
5
+ */
6
+ import { mkdir, readFile, writeFile } from 'node:fs/promises'
7
+ import { existsSync } from 'node:fs'
8
+ import { dirname, join } from 'node:path'
9
+ import { fileURLToPath, pathToFileURL } from 'node:url'
10
+ import { homedir } from 'node:os'
11
+
12
+ const here = dirname(fileURLToPath(import.meta.url))
13
+ const root = join(here, '..')
14
+ const { AGENTS_SCHEMA, BOT_IDENTITY, buildAgentsDoc } =
15
+ await import(pathToFileURL(join(root, 'bot-identity.js')).href)
16
+
17
+ const argRoot = process.argv.indexOf('--root')
18
+ const D_ROOT = argRoot >= 0 ? process.argv[argRoot + 1] : 'D:\\erpBot'
19
+ const FALLBACK_ROOT = join(homedir(), 'Documents', 'deepseek-harness')
20
+
21
+ async function ensureDirectory(bot) {
22
+ for (const [index, directory] of [join(D_ROOT, bot), join(FALLBACK_ROOT, 'erpbot-' + bot)].entries()) {
23
+ try {
24
+ await mkdir(directory, { recursive: true })
25
+ return directory
26
+ } catch (error) {
27
+ if (index === 0 && !existsSync('D:\\')) continue
28
+ throw error
29
+ }
30
+ }
31
+ throw new Error('workspace directory unavailable')
32
+ }
33
+
34
+ async function scaffoldBotFiles(bot) {
35
+ const directory = await ensureDirectory(bot)
36
+ const configPath = join(directory, 'bot.json')
37
+ const agentsPath = join(directory, 'AGENTS.md')
38
+ const created = []
39
+
40
+ let skills = []
41
+ let configExists = false
42
+ if (existsSync(configPath)) {
43
+ configExists = true
44
+ try {
45
+ const parsed = JSON.parse(await readFile(configPath, 'utf8'))
46
+ if (parsed !== null && typeof parsed === 'object' && Array.isArray(parsed.skills)) {
47
+ skills = parsed.skills.filter((name) => typeof name === 'string')
48
+ }
49
+ } catch (error) { /* rewritten below */ }
50
+ }
51
+ if (!configExists) {
52
+ await writeFile(configPath, JSON.stringify({ bot, skills, updatedAt: new Date().toISOString() }, null, 2) + '\n', 'utf8')
53
+ created.push('bot.json')
54
+ }
55
+
56
+ let agents = null
57
+ if (existsSync(agentsPath)) {
58
+ try { agents = await readFile(agentsPath, 'utf8') } catch (error) { agents = null }
59
+ }
60
+ let migrated = false
61
+ if (agents === null) {
62
+ await writeFile(agentsPath, buildAgentsDoc(bot, skills, {}) + '\n', 'utf8')
63
+ created.push('AGENTS.md')
64
+ } else if (agents.indexOf(AGENTS_SCHEMA) < 0) {
65
+ await writeFile(agentsPath, buildAgentsDoc(bot, skills, {}) + '\n', 'utf8')
66
+ migrated = true
67
+ }
68
+
69
+ return { bot, directory, created, migrated }
70
+ }
71
+
72
+ for (const bot of Object.keys(BOT_IDENTITY)) {
73
+ const r = await scaffoldBotFiles(bot)
74
+ console.log(`${r.bot.padEnd(9)} ${r.directory}`)
75
+ console.log(` created: ${r.created.length ? r.created.join(', ') : '(none)'}${r.migrated ? ' [AGENTS.md migrated]' : ''}`)
76
+ }