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/README.md +50 -29
- package/bot-identity.js +246 -0
- package/client.js +2438 -627
- package/cordis.patch.yml +128 -0
- package/docs/README.md +39 -0
- package/docs/acceptance.md +136 -0
- package/docs/architecture.md +161 -0
- package/docs/bot-agents-spec.md +205 -0
- package/docs/bot-collaboration-spec.md +161 -0
- package/docs/data-contract.md +262 -0
- package/docs/mcp-inventory-analysis.md +58 -0
- package/docs/requirements.md +234 -0
- package/index.js +270 -15
- package/package.json +6 -2
- package/scripts/check-bot-identity.mjs +71 -0
- package/scripts/lint-factories.mjs +315 -0
- package/scripts/lint-scopes.mjs +179 -0
- package/scripts/scaffold-bots.mjs +76 -0
- package/scripts/test-manifest.mjs +72 -0
- package/scripts/verify.mjs +159 -0
- package/spec/README.md +299 -0
- package/spec/base-content-spec.md +129 -0
- package/spec/cai-content-spec.md +257 -0
- package/spec/shui-content-spec.md +252 -0
- package/spec/spec-CORE.md +155 -0
- package/spec/tui-content-spec.md +259 -0
- package/spec/zi-content-spec.md +256 -0
- package/src/assistant/index.js +389 -4
- package/src/bot-data.js +25 -13
- package/src/bundle.json +1 -0
- package/src/cards/index.js +18 -4
- package/src/dashboard/index.js +65 -25
- package/src/entry.js +7 -7
- package/src/i18n/index.js +80 -60
- package/src/mount.js +19 -2
- package/src/nav.js +7 -5
- package/src/pages-data.js +29 -262
- package/src/shell/index.js +57 -33
- package/src/styles.js +396 -78
- package/src/workbench/index.js +1297 -85
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-erpbot",
|
|
3
|
-
"version": "0.1.
|
|
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/
|
|
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
|
+
}
|