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
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A'-2 校验器的负面测试:证明 verify.mjs 的 AST 顺序校验能拦住「加载期」违规。
|
|
3
|
+
*
|
|
4
|
+
* 关键:verifyOrder 只检查「直接子声明的初始化表达式」里的跨片段引用(加载期真约束)。
|
|
5
|
+
* 函数体内的引用是调用期求值,不构成加载期约束,正确不报——这不是漏报。
|
|
6
|
+
*
|
|
7
|
+
* 所以负面用例必须构造「加载期」违规:entry.js 的 `const x = createYyy()` 是直接子声明的
|
|
8
|
+
* 初始化表达式,它引用的工厂/横切小片如果排在 entry 之后,就是真违规。
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync } from 'node:fs'
|
|
11
|
+
import { dirname, join } from 'node:path'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
13
|
+
import { verifyOrder } from './verify.mjs'
|
|
14
|
+
|
|
15
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
16
|
+
const srcDir = join(root, 'src')
|
|
17
|
+
const manifest = JSON.parse(readFileSync(join(srcDir, 'bundle.json'), 'utf8'))
|
|
18
|
+
const entries = [...manifest.fragments].sort((a, b) => a.order - b.order)
|
|
19
|
+
const files = entries.map((f) => ({ ...f, text: readFileSync(join(srcDir, f.file), 'utf8') }))
|
|
20
|
+
|
|
21
|
+
const clone = () => files.map((f) => ({ ...f, text: f.text }))
|
|
22
|
+
let failures = 0
|
|
23
|
+
|
|
24
|
+
function run(label, filesArg, expectViolation) {
|
|
25
|
+
let result
|
|
26
|
+
try { result = verifyOrder(filesArg) } catch (e) { result = { ok: false, violations: [{ from: '?', to: '?', symbols: [e.message] }] } }
|
|
27
|
+
const has = !result.ok
|
|
28
|
+
const ok = has === expectViolation
|
|
29
|
+
if (!ok) failures += 1
|
|
30
|
+
const detail = result.violations.map((v) => `${v.from}→${v.to}:${v.symbols.join(',')}`).join(' | ') || '无违规'
|
|
31
|
+
console.log(`${ok ? 'ok ' : 'FAIL'} ${label.padEnd(46)} -> ${detail}`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 1. 基线:现行顺序必须无违规。
|
|
35
|
+
run('现行顺序(基线,应通过)', clone(), false)
|
|
36
|
+
|
|
37
|
+
// 2. 真违规:把 shell/index.js 移到 entry.js 之后。
|
|
38
|
+
// entry.js 里 `const shell = createShell(...)` 是加载期初始化表达式,createShell 声明在 shell/index.js。
|
|
39
|
+
// 若 shell/index.js 排在 entry.js 之后,加载到 entry 那行时 createShell 尚未定义 → 真违规。
|
|
40
|
+
{
|
|
41
|
+
const f = clone()
|
|
42
|
+
const shell = f.find((x) => x.file === 'shell/index.js')
|
|
43
|
+
const entry = f.find((x) => x.file === 'entry.js')
|
|
44
|
+
shell.order = entry.order + 1
|
|
45
|
+
f.sort((a, b) => a.order - b.order)
|
|
46
|
+
run('shell/index.js 移到 entry.js 之后(应报违规)', f, true)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 3. 真违规:把 i18n/index.js 移到 entry.js 之后(entry 的 createI18n() 会读到未声明符号)。
|
|
50
|
+
{
|
|
51
|
+
const f = clone()
|
|
52
|
+
const i18n = f.find((x) => x.file === 'i18n/index.js')
|
|
53
|
+
const entry = f.find((x) => x.file === 'entry.js')
|
|
54
|
+
i18n.order = entry.order + 1
|
|
55
|
+
f.sort((a, b) => a.order - b.order)
|
|
56
|
+
run('i18n/index.js 移到 entry.js 之后(应报违规)', f, true)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 4. 无误报:函数体引用(PAGES 在 kpi-detail 函数体内)不应报。
|
|
60
|
+
// 把 pages-data.js 移到 kpi-detail.js 之后,verifyOrder 应「不报」——因为 PAGES 是函数体引用。
|
|
61
|
+
{
|
|
62
|
+
const f = clone()
|
|
63
|
+
const pd = f.find((x) => x.file === 'pages-data.js')
|
|
64
|
+
const kd = f.find((x) => x.file === 'kpi-detail.js')
|
|
65
|
+
pd.order = kd.order + 1
|
|
66
|
+
f.sort((a, b) => a.order - b.order)
|
|
67
|
+
run('pages-data 移到 kpi-detail 后(函数体引用,应不报)', f, false)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log('')
|
|
71
|
+
console.log(failures === 0 ? '全部符合预期:AST 校验器正确识别加载期违规、无误报。' : `${failures} 项不符合预期`)
|
|
72
|
+
process.exit(failures === 0 ? 0 : 1)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A'-2 校验器:用 acorn AST 精确校验拼接产物的片段顺序。
|
|
3
|
+
*
|
|
4
|
+
* 只回答一个问题:factory 作用域顶层(直接子声明)里,有没有「片段 A 引用片段 B 声明的
|
|
5
|
+
* 符号,但 A 排在 B 之前」——即加载期会 ReferenceError 的顺序违规。
|
|
6
|
+
*
|
|
7
|
+
* 作用域处理(关键):
|
|
8
|
+
* - 只遍历 factory 函数的直接子声明(VariableDeclaration / FunctionDeclaration / ClassDeclaration),
|
|
9
|
+
* 以及这些声明「初始化表达式」里的标识符引用。
|
|
10
|
+
* - 不递归进函数体(FunctionDeclaration 的 body)——函数体内的标识符在调用时才求值,
|
|
11
|
+
* 是「函数体引用」,不构成加载期顺序约束。这正确排除了局部变量遮蔽的误报。
|
|
12
|
+
*
|
|
13
|
+
* 符号归属:
|
|
14
|
+
* - 每个片段声明的顶层符号(factory 直接子声明里的 const/let/function/class 名字)。
|
|
15
|
+
* - 跨片段引用 = 片段 A 的「直接子声明的初始化表达式」里,出现了片段 B 声明的符号名。
|
|
16
|
+
*
|
|
17
|
+
* 返回 { ok, violations: [{from, to, symbols}] }。
|
|
18
|
+
*/
|
|
19
|
+
import { readFileSync } from 'node:fs'
|
|
20
|
+
import { join, dirname } from 'node:path'
|
|
21
|
+
import { createRequire } from 'node:module'
|
|
22
|
+
import { fileURLToPath } from 'node:url'
|
|
23
|
+
|
|
24
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
25
|
+
const root = join(here, '..')
|
|
26
|
+
const require = createRequire(join(here, 'noop.js'))
|
|
27
|
+
|
|
28
|
+
let acorn
|
|
29
|
+
try {
|
|
30
|
+
acorn = require(join(root, 'node_modules', 'acorn'))
|
|
31
|
+
} catch {
|
|
32
|
+
throw new Error('scripts/verify.mjs: acorn not installed — run `npm install` first')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {Array<{file:string, order:number, text:string}>} files 按 order 升序的片段
|
|
37
|
+
* @returns {{ok:boolean, violations:Array<{from:string,to:string,symbols:string[]}>}}
|
|
38
|
+
*/
|
|
39
|
+
export function verifyOrder(files) {
|
|
40
|
+
const whole = files.map((f) => f.text).join('')
|
|
41
|
+
let ast
|
|
42
|
+
try {
|
|
43
|
+
ast = acorn.parse(whole, { ecmaVersion: 2022, sourceType: 'script', locations: true })
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw new Error(`verify: client bundle 解析失败 — ${error.message}`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 行号 -> 片段映射
|
|
49
|
+
const lineOffset = []
|
|
50
|
+
let cursor = 0
|
|
51
|
+
for (const f of files) {
|
|
52
|
+
const lines = f.text.split('\n').length - 1
|
|
53
|
+
lineOffset.push({ file: f.file, start: cursor, end: cursor + lines })
|
|
54
|
+
cursor += lines
|
|
55
|
+
}
|
|
56
|
+
function fileOf(line) {
|
|
57
|
+
for (const r of lineOffset) if (line - 1 >= r.start && line - 1 <= r.end) return r.file
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 找 factory 函数
|
|
62
|
+
function findFactory(node) {
|
|
63
|
+
if (!node || typeof node !== 'object') return null
|
|
64
|
+
if (node.type === 'FunctionExpression' && node.parent &&
|
|
65
|
+
node.parent.type === 'Property' && node.parent.key &&
|
|
66
|
+
(node.parent.key.name === 'factory' || node.parent.key.value === 'factory')) return node
|
|
67
|
+
for (const key of Object.keys(node)) {
|
|
68
|
+
if (['type', 'start', 'end', 'loc', 'range', 'parent'].includes(key)) continue
|
|
69
|
+
const v = node[key]
|
|
70
|
+
if (Array.isArray(v)) { for (const it of v) if (it && it.type) { it.parent = node; const r = findFactory(it); if (r) return r } }
|
|
71
|
+
else if (v && typeof v === 'object' && v.type) { v.parent = node; const r = findFactory(v); if (r) return r }
|
|
72
|
+
}
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
const factory = findFactory(ast)
|
|
76
|
+
if (!factory) throw new Error('verify: 找不到 factory 函数')
|
|
77
|
+
|
|
78
|
+
// factory 直接子声明 → 符号归属
|
|
79
|
+
const declared = new Map() // symbol -> file
|
|
80
|
+
const fileDeclared = new Map() // file -> Set<symbol>
|
|
81
|
+
for (const f of files) fileDeclared.set(f.file, new Set())
|
|
82
|
+
for (const stmt of factory.body.body) {
|
|
83
|
+
const file = fileOf(stmt.loc.start.line)
|
|
84
|
+
if (!file) continue
|
|
85
|
+
if (stmt.type === 'VariableDeclaration') {
|
|
86
|
+
for (const d of stmt.declarations) {
|
|
87
|
+
if (d.id && d.id.type === 'Identifier') {
|
|
88
|
+
declared.set(d.id.name, file)
|
|
89
|
+
fileDeclared.get(file).add(d.id.name)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} else if (stmt.type === 'FunctionDeclaration' && stmt.id) {
|
|
93
|
+
declared.set(stmt.id.name, file)
|
|
94
|
+
fileDeclared.get(file).add(stmt.id.name)
|
|
95
|
+
} else if (stmt.type === 'ClassDeclaration' && stmt.id) {
|
|
96
|
+
declared.set(stmt.id.name, file)
|
|
97
|
+
fileDeclared.get(file).add(stmt.id.name)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 片段顺序索引
|
|
102
|
+
const orderIdx = new Map(files.map((f, i) => [f.file, i]))
|
|
103
|
+
|
|
104
|
+
// 遍历 factory 直接子声明「初始化表达式」里的标识符,找跨片段引用
|
|
105
|
+
const violations = new Map() // `${from}->${to}` -> Set<symbol>
|
|
106
|
+
|
|
107
|
+
// 递归收集一个表达式的自由标识符(不进入嵌套函数体)
|
|
108
|
+
function collectIdents(node, out, depth = 0) {
|
|
109
|
+
if (!node || typeof node !== 'object') return
|
|
110
|
+
if (node.type === 'Identifier') {
|
|
111
|
+
out.add(node.name)
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
// 进入函数体就停(函数体内是调用期求值,不构成加载期约束)
|
|
115
|
+
if (depth > 0 && (node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration' ||
|
|
116
|
+
node.type === 'ArrowFunctionExpression' || node.type === 'ClassDeclaration' ||
|
|
117
|
+
node.type === 'ClassExpression')) {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
for (const key of Object.keys(node)) {
|
|
121
|
+
if (['type', 'start', 'end', 'loc', 'range', 'parent'].includes(key)) continue
|
|
122
|
+
const v = node[key]
|
|
123
|
+
if (key === 'key' && node.type === 'Property' && !node.computed) continue // 对象键 { chart: ... } 不是引用
|
|
124
|
+
if (key === 'key' && node.type === 'MemberExpression' && !node.computed) continue // obj.chart 的 chart 不是引用
|
|
125
|
+
if (Array.isArray(v)) { for (const it of v) if (it && typeof it === 'object') collectIdents(it, out, depth) }
|
|
126
|
+
else if (v && typeof v === 'object') collectIdents(v, out, depth)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (const stmt of factory.body.body) {
|
|
131
|
+
const file = fileOf(stmt.loc.start.line)
|
|
132
|
+
if (!file) continue
|
|
133
|
+
// 只处理「有初始化表达式的直接子声明」
|
|
134
|
+
let initExprs = []
|
|
135
|
+
if (stmt.type === 'VariableDeclaration') {
|
|
136
|
+
for (const d of stmt.declarations) if (d.init) initExprs.push(d.init)
|
|
137
|
+
}
|
|
138
|
+
for (const init of initExprs) {
|
|
139
|
+
const idents = new Set()
|
|
140
|
+
collectIdents(init, idents, 0)
|
|
141
|
+
for (const sym of idents) {
|
|
142
|
+
if (!declared.has(sym)) continue // 非片段声明(React/h/window/document 等平台全局)
|
|
143
|
+
const declFile = declared.get(sym)
|
|
144
|
+
if (declFile === file) continue // 同片段内
|
|
145
|
+
if (orderIdx.get(file) < orderIdx.get(declFile)) {
|
|
146
|
+
const key = `${file} -> ${declFile}`
|
|
147
|
+
if (!violations.has(key)) violations.set(key, new Set())
|
|
148
|
+
violations.get(key).add(sym)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const list = [...violations.entries()].map(([key, syms]) => {
|
|
155
|
+
const [from, to] = key.split(' -> ')
|
|
156
|
+
return { from, to, symbols: [...syms].sort() }
|
|
157
|
+
})
|
|
158
|
+
return { ok: list.length === 0, violations: list }
|
|
159
|
+
}
|
package/spec/README.md
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# Erp 四 Bot 页 · 总览与共享契约
|
|
2
|
+
|
|
3
|
+
> 文档集:本 README(共享契约)+ `cai` / `zi` / `tui` / `shui` 四份 Bot 规格 + `base-content-spec.md`(MCP 与数据单页)+ `spec-CORE`(速览)。
|
|
4
|
+
> 本版为 **bot 化设计 v2:对话主导 + 头像入口**。财 / 资 / 推 / 税 四页的**主区域是对话**;Bot 的身份与专属技能**不常驻**,点对话头部的 **Bot 头像**打开右侧栏即可。KPI 卡布局与下方卡片组件架构不变。
|
|
5
|
+
> **spec 不放业务数据**:页面数字、口径勾稽、数据集列定义都不写进 spec;那些由 payload 与画布渲染器承载。spec 只保留**Bot 身份定义逻辑、专属技能绑定逻辑、对话交互逻辑**。
|
|
6
|
+
> 本版不含评审记录、修订记录、归档说明与历史稿。
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 1. 产品定位
|
|
11
|
+
|
|
12
|
+
**Erp = Harness + ERP,四个业务 Bot 是 AI 执行主体的人格化入口。**
|
|
13
|
+
|
|
14
|
+
| 层 | 页面 | 形态 | 说明 |
|
|
15
|
+
|---|---|---|---|
|
|
16
|
+
| 业务 Bot | 财 `cai` · 资 `zi` · 推 `tui` · 税 `shui` | **对话式 Bot 页** | KPI 状态行 + 对话主区;头像打开身份与专属技能侧栏 |
|
|
17
|
+
| 能力基座 | 底座 `base` | **单页:MCP 与数据** | MCP 通道与服务 + 技能与场景 + 数据源 / 批次 / 主数据 / 口径 / 质量 + 回执与审计;一个导航入口,页内无 tab |
|
|
18
|
+
| 协作入口 | 总览 `overview` | 总览页(本轮布局不动) | 四 Bot 运行状态与跨页待办 |
|
|
19
|
+
|
|
20
|
+
一句话:**对话是 Bot 干活的地方;头像后面是它「是谁、会什么、记得你什么」的配置。**
|
|
21
|
+
|
|
22
|
+
设计约束:
|
|
23
|
+
|
|
24
|
+
1. **KPI 卡布局不动**:顶排 4 张卡、7 种变体、`MetricDetailModal` 弹层全部保留;spec 不写具体数值。
|
|
25
|
+
2. **下方组件架构不动**:仍是一张工作台大卡片(原 `SectionShell` 的卡片族),12 种渲染器与 payload 契约保留,用于画布;不再常驻左右两栏。
|
|
26
|
+
3. **对话占满主区**:KPI 行以下的空间全部留给 Chat 卡片。
|
|
27
|
+
4. **身份与技能不常驻**:左侧不再放 Bot 面板;点对话头部的 Bot 头像 → 右侧栏(身份 + 专属技能)。
|
|
28
|
+
5. **右上角 AI 助手入口移除**:TopBar 的 assistant 按钮、总览 AI 卡、Dock / Rail / Overlay 一并退役;对话下沉到每个导航目录内部。
|
|
29
|
+
6. **插件架构保留**:`sidebar.panellist` / `main` 槽位、Host 半 `/erp/workspace` 路由、官方会话嵌入(`conversation.content`,`variant: 'embedded'`)继续使用。
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 2. 页面骨架
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
┌────────────────────────────────────────────────────────────────────┐
|
|
37
|
+
│ TopBar:品牌 / 问候 / 搜索 / 通知 / 头像(右上角 AI 入口已移除) │
|
|
38
|
+
├────┬───────────────────────────────────────────────────────────────┤
|
|
39
|
+
│ R │ 4 × KPI 卡(布局与 7 种变体不变,点开仍是 MetricDetailModal) │
|
|
40
|
+
│ a ├───────────────────────────────────────────────────────────────┤
|
|
41
|
+
│ i │ Chat 卡片(占满下方剩余空间,= 核心板块) │
|
|
42
|
+
│ l │ ┌───────────────────────────────────────────────────────────┐ │
|
|
43
|
+
│ │ │ 头部:[Bot 头像▾] 账房 · 把账记对把账关好 ● 就绪 │ │
|
|
44
|
+
│ │ │ 会话标题 历史 新会话 画布 │ │
|
|
45
|
+
│ 7 │ ├───────────────────────────────────────────────────────────┤ │
|
|
46
|
+
│ 页 │ │ 官方会话(embedded chat):欢迎语 + 引导 chips + 消息流 │ │
|
|
47
|
+
│ │ ├───────────────────────────────────────────────────────────┤ │
|
|
48
|
+
│ │ │ 官方 composer(输入区) │ │
|
|
49
|
+
│ │ └───────────────────────────────────────────────────────────┘ │
|
|
50
|
+
└────┴───────────────────────────────────────────────────────────────┘
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 2.1 Chat 卡片
|
|
54
|
+
|
|
55
|
+
> 布局稿:`_erp-shots/bot-ui-chat.png`(默认对话页)、`bot-ui-identity.png`(头像 → 身份配置)、`bot-ui-skills.png`(技能配置)、`bot-ui-canvas.png`(技能画布)。四张均为静态设计稿,非实现。
|
|
56
|
+
|
|
57
|
+
- 外层沿用 `.card workbench` 卡片族与容器查询;Card 内只有对话,不再有常驻左栏。
|
|
58
|
+
- 头部左侧 = **Bot 头像按钮**(圆形、Aurora 强调色、带运行状态点),点击打开配置抽屉;旁边是 Bot 名称 + 一句话角色 + 状态(就绪 / 运行中 / 待交互 / 未读 / 错误)。
|
|
59
|
+
- 头部右侧 = 会话标题(可截断)+ 三个图标按钮:历史、新会话、画布。会话标题支持点击展开历史列表。
|
|
60
|
+
- 对话体 = 官方 `conversation.content` 嵌入;欢迎态显示开场白 + 3 个引导 chips(已加载技能的第一条触发语),点击预填 composer。
|
|
61
|
+
- 输入区 = 官方 composer,不做自绘输入框。
|
|
62
|
+
- 页面级 state 只有语言、主题、当前页、当前 Bot 会话;其余交给会话服务。
|
|
63
|
+
|
|
64
|
+
### 2.2 头像 → Bot 配置抽屉
|
|
65
|
+
|
|
66
|
+
- 入口有两个:头部头像点击;头部「配置」文字按钮(窄屏只留头像)。
|
|
67
|
+
- 呈现:右侧覆盖式抽屉(`aside`,宽 `420–480px`,覆盖在 Chat 之上,**不挤压、不销毁对话**);≤900px 时全宽;Escape / 点击遮罩关闭。
|
|
68
|
+
- 三个页签:
|
|
69
|
+
1. **身份**:身份卡(名称 / 角色 / 图标 / 强调色 / 版本)+ 角色定义(职责 / 语气 / 边界,系统值只读)+ 可编辑项(称呼、问候语偏好、回复语气偏好、时间与单位偏好)。
|
|
70
|
+
2. **技能**:已加载 / 可加载两组;每行 = 名称 + 来源(本地 / 平台)+ 版本 + 优先级 + 状态开关 + 「问这个」;顶部显示 `已加载 N / 12`;底部「从平台技能库加载」入口。灰度技能带「灰度」角标,停用技能置灰不可加载。
|
|
71
|
+
3. **专属技能**(在身份页内,单独一组):已绑定的 DSH 技能以小方块卡片平铺,末尾固定一个「+」卡片;点「+」列出 `~/.dsh/skills` 里尚未绑定的技能,选中即绑定。绑定结果写入该 Bot 的 `bot.json` 与 `AGENTS.md`。
|
|
72
|
+
- 抽屉打开时不新增路由、不刷新页面;关闭后对话滚动位置与会话不变。
|
|
73
|
+
|
|
74
|
+
### 2.3 技能画布
|
|
75
|
+
|
|
76
|
+
- 触发:对话回复中的「查看结果」、技能行的「看结果」、头部「画布」按钮(打开上次画布)。
|
|
77
|
+
- 呈现:右侧覆盖式画布抽屉(与配置抽屉同层规则),顶部「技能名 + 返回对话」;内容复用 12 种渲染器与 payload。
|
|
78
|
+
- 画布是临时视图:关闭即回对话;不改变会话,不改变技能加载状态。
|
|
79
|
+
- 图表类技能保留 payload 的 `alt`(图表 / 清单双视图)规则。
|
|
80
|
+
|
|
81
|
+
### 2.4 响应式与主题
|
|
82
|
+
|
|
83
|
+
- 宽度用 `.erp-app` 容器查询(`container-name: erp`):`≤1180px` KPI 行 2 列、Chat 头部动作收进「更多」菜单;`≤900px` 配置 / 画布抽屉全宽;`≤720px` 引导 chips 折行、头像与名称纵向排列;`≤560px` 隐藏角色副标。
|
|
84
|
+
- 高度用 `.erp-board` 容器查询(`container-name: board`):短高度下欢迎语缩短、chips 收成一行、输入区保持可见。
|
|
85
|
+
- 禁止 `@media` 判断面板尺寸;CSS 注释里禁止反引号(会提前终止模板字符串)。
|
|
86
|
+
- 明暗主题跟随 DSH(`useHostTheme`);抽屉与画布沿用同一套 `--erp-*` token。
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 3. Bot 契约
|
|
91
|
+
|
|
92
|
+
### 3.1 机器人清单
|
|
93
|
+
|
|
94
|
+
| id | 页面 | 名称(zh / en) | 一句话角色 | 本地技能数 | 会话工作区 |
|
|
95
|
+
|---|---|---|---|---|---|
|
|
96
|
+
| `cai` | 财 · 过去 | 账房 / The Ledger | 把账记对、把账关好、把证据留全 | 8 | `D:\erpBot\cai` |
|
|
97
|
+
| `zi` | 资 · 现在 | 司库 / The Treasurer | 守住可支付能力与安全垫 | 6 | `D:\erpBot\zi` |
|
|
98
|
+
| `tui` | 推 · 未来 | 军师 / The Planner | 把不确定性与假设讲清楚 | 8 | `D:\erpBot\tui` |
|
|
99
|
+
| `shui` | 税 · 税务 | 税官 / The Tax Officer | 不错报、不漏报、红线可见 | 6 | `D:\erpBot\shui` |
|
|
100
|
+
|
|
101
|
+
### 3.2 Bot 定义字段
|
|
102
|
+
|
|
103
|
+
```text
|
|
104
|
+
bot = {
|
|
105
|
+
id: 'cai' | 'zi' | 'tui' | 'shui',
|
|
106
|
+
name: { zh, en }, // 账房 / The Ledger
|
|
107
|
+
role: { zh, en }, // 一句话角色(头部副标)
|
|
108
|
+
icon: 'ledger' | 'wallet' | 'compass' | 'receipt',
|
|
109
|
+
accent: 'aurora-v1' | 'aurora-v2' | …,
|
|
110
|
+
version: 'S3', // 技能注册表版本,显示在身份卡
|
|
111
|
+
persona: {
|
|
112
|
+
duties: { zh, en }, // 职责
|
|
113
|
+
tone: { zh, en }, // 语气
|
|
114
|
+
boundaries: { zh, en }, // 能做 / 不能做
|
|
115
|
+
notApplicable: { zh, en }, // 明确不适用项
|
|
116
|
+
greeting: { zh, en } // 开场白(欢迎态第一行)
|
|
117
|
+
},
|
|
118
|
+
identityEditable: ['salutation', 'greetingStyle', 'replyTone', 'timeUnit'], // 用户可改的偏好
|
|
119
|
+
skills: [skill], // 见 §4
|
|
120
|
+
memoryKey: 'dsh-erpbot.bot.memory.v1.<id>',
|
|
121
|
+
sessionKey: 'dsh-erpbot.bot.session.v1.<id>',
|
|
122
|
+
workspaceDir: 'D:\\erpBot\\<id>', // 每 Bot 一个子文件夹,例如 D:\erpBot\cai
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 3.3 身份定义逻辑(写进每页 spec 的核心)
|
|
127
|
+
|
|
128
|
+
- **角色定义 = 三个边界**:职责(它负责解释什么)、语气(它怎么说话)、边界(它不能做什么)。三个都必须写,且必须互不重叠。
|
|
129
|
+
- **身份决定回答范围**:Bot 只回答本领域问题;越界时明确拒答并指向对应 Bot(财 → 资 / 推 / 税 / 底座)。
|
|
130
|
+
- **身份决定措辞**:语气与称谓写进 persona,对话首条与欢迎语必须与之匹配;不做人格化过度(不寒暄、不夸张)。
|
|
131
|
+
- **身份可微调不可改内核**:用户可改称呼、问候偏好、语气偏好、时间单位偏好;职责 / 边界 / 不适用项只读。
|
|
132
|
+
- **身份与技能解耦**:加载技能不改变身份;身份变化不自动加载技能。
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 4. 技能契约
|
|
137
|
+
|
|
138
|
+
### 4.1 技能来源
|
|
139
|
+
|
|
140
|
+
1. **Bot 本地技能(28 项)**:原四页左栏分类直接转为技能,id 形如 `cai.vouchers`。这是「分类与核心业务」的保留方式。
|
|
141
|
+
2. **平台技能(12 项)**:底座「技能与场景」的注册表(S3);Bot 只能加载,不能改写版本与策略。
|
|
142
|
+
|
|
143
|
+
### 4.2 技能字段
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
skill = {
|
|
147
|
+
id: 'cai.close', // botId.名字
|
|
148
|
+
botId: 'cai',
|
|
149
|
+
name: { zh, en }, // 期末关账 / Period close
|
|
150
|
+
desc: { zh, en }, // 一行能力说明(不说具体数字)
|
|
151
|
+
source: 'bot-local' | 'platform',
|
|
152
|
+
pillar: 'P0' | 'P1',
|
|
153
|
+
version: 'S3' | 'v3.0', // 平台技能带版本;本地技能写 null
|
|
154
|
+
state: 'loaded' | 'available' | 'disabled',
|
|
155
|
+
triggers: [{ zh, en }], // 2–3 条引导 prompt(问法,不是数据)
|
|
156
|
+
renderer: 'list'|'table'|'flow'|'cards'|'steps'|'checks'|'bars'|'chart'|'donut'|'timeline'|'scenarios',
|
|
157
|
+
writeScope: 'read' | 'controlled-write',
|
|
158
|
+
audit: 'none' | 'RC-<编号>' // 受控写必须有回执
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### 4.3 加载 / 卸载规则
|
|
163
|
+
|
|
164
|
+
- 默认已加载:本地 P0;本地 P1 与平台技能默认 `available`。
|
|
165
|
+
- 绑定写入该 Bot 的 `bot.json`(`{bot, skills[], updatedAt}`)与 `AGENTS.md`(技能清单一节);解绑只从这两处移除。
|
|
166
|
+
- 同一 Bot 已加载技能上限 **12**;达上限提示「先卸载一个再加载」,不自动替换。
|
|
167
|
+
- 灰度技能可加载但带「灰度」角标、默认只读;停用技能置灰不可加载。
|
|
168
|
+
- 技能不改变身份与口径:画布永远读 payload 当前值,前端不重算业务数。
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## 5. 工作区文件契约(bot.json + AGENTS.md)
|
|
173
|
+
|
|
174
|
+
### 5.1 两份文件
|
|
175
|
+
|
|
176
|
+
每个 Bot 的工作区只有两份配置,落在 `D:\erpBot\<bot>\`:
|
|
177
|
+
|
|
178
|
+
```text
|
|
179
|
+
D:\erpBot\<bot>\
|
|
180
|
+
├── bot.json # { bot, skills: ['<DSH 技能名>', ...], updatedAt }
|
|
181
|
+
└── AGENTS.md # 身份 + 场景提示 + 已绑定技能 + 用户偏好 + 约束
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
- **AGENTS.md 是身份的唯一载体**:身份(角色 / 职责 / 语气 / 边界)、场景提示、已绑定的 DSH 技能、用户偏好与约束都在这里;宿主的会话在这份文件所在的工作区里跑,因此每次对话自动带上这些上下文。
|
|
185
|
+
- **bot.json 只存机器可读的一小份**:绑定技能列表与更新时间,用于界面回读与幂等写入。
|
|
186
|
+
- 不再有独立的「记忆」页签,也不再有本地记忆模型;技能与偏好都只写这两份文件。
|
|
187
|
+
|
|
188
|
+
### 5.2 写入规则
|
|
189
|
+
|
|
190
|
+
| 项 | 规则 |
|
|
191
|
+
|---|---|
|
|
192
|
+
| 写入时机 | 绑定 / 解绑技能、修改称呼 / 回复语气 / 时间口径 |
|
|
193
|
+
| 写入路径 | `POST /erp/bot`(Host 半按白名单 bot id 定位目录,不接受请求传入路径) |
|
|
194
|
+
| 读取时机 | 打开右侧栏时读取 `GET /erp/bot?bot=<id>` |
|
|
195
|
+
| 离线降级 | 路由不可用时保留本地镜像(`localStorage['dsh-erpbot.bot.config.v1.<botId>']`),并显示保存状态 |
|
|
196
|
+
| 缺失 / 损坏 | `bot.json` 解析失败按空配置处理,页面不阻塞、不弹窗 |
|
|
197
|
+
| 隔离 | 四个 Bot 各自一份目录;底座不读 Bot 配置 |
|
|
198
|
+
| 隐私 | 只存身份文本与技能名;不存手机号、证件号、银行账号 |
|
|
199
|
+
|
|
200
|
+
### 5.3 技能来源
|
|
201
|
+
|
|
202
|
+
- 只使用 **DSH 标准技能库** `~/.dsh/skills/<name>/SKILL.md`,不内置任何自制技能。
|
|
203
|
+
- 宿主解析每个 `SKILL.md` 的 frontmatter(`name` / `description`)后由 `GET /erp/skills` 给到界面。
|
|
204
|
+
- 「+」卡片只列**尚未绑定**的技能;绑定即写入 `bot.json` 与 `AGENTS.md`,解绑从两处移除。
|
|
205
|
+
- 技能名在界面上原样显示(英文小写 kebab-case),便于与 DSH 技能目录一一对应。
|
|
206
|
+
## 6. 对话契约
|
|
207
|
+
|
|
208
|
+
### 6.1 复用与移除
|
|
209
|
+
|
|
210
|
+
- **复用**:`createAssistant` 的会话 / 工作区 / 预填能力(工作区创建、`sessions.create/retain`、`requestDraftInitialization`、`conversation.content` 嵌入、主题桥)。
|
|
211
|
+
- **移除**:TopBar assistant 入口、总览 `AssistantCard`、`AssistantDock` / `AssistantRail` / `AssistantOverlay`、呈现状态存储键。
|
|
212
|
+
- **保留**:槽位注册、Host 半 `/erp/workspace` 路由、官方会话、`dsh-erpbot.assistant.session.v1` 的一次性迁移。
|
|
213
|
+
|
|
214
|
+
### 6.2 工作区与会话
|
|
215
|
+
|
|
216
|
+
- **每 Bot 一个工作区**,默认根目录 **`D:\erpBot`**,子文件夹与 Bot 一一对应:`D:\erpBot\cai`、`D:\erpBot\zi`、`D:\erpBot\tui`、`D:\erpBot\shui`(按需创建)。
|
|
217
|
+
- Host 路由 `POST /erp/workspace` 接受 `{ bot }`(白名单 `cai/zi/tui/shui`);目录由插件按白名单固定映射,**不接受请求传入路径**;工作区标题 `erpBot · 财 Bot` / `erpBot · 资 Bot` / `erpBot · 推 Bot` / `erpBot · 税 Bot`。
|
|
218
|
+
- D 盘不可用(不存在 / 无写权限)时,降级到 `Documents\deepseek-harness\erpbot-<id>`,并在右侧栏显示实际工作区路径;不静默失败。
|
|
219
|
+
- 每 Bot 独立工作区与会话列表;会话 key `dsh-erpbot.bot.session.v1.<id>`。
|
|
220
|
+
- 切页不销毁会话(`retain` 持有引用),切回恢复;页面卸载释放引用并持久化 lastSessionId。
|
|
221
|
+
- 会话头状态点语义:就绪 / 运行中 / 待交互 / 未读 / 错误;与官方会话状态同源。
|
|
222
|
+
|
|
223
|
+
### 6.3 技能在对话中的三种入口
|
|
224
|
+
|
|
225
|
+
1. **欢迎 chips**:已加载技能中 P0 的前 3 条触发语,预填 composer。
|
|
226
|
+
2. **技能行「问这个」**:配置抽屉内点技能,预填该技能触发语。
|
|
227
|
+
3. **对话回复「查看结果」**:切换到画布抽屉。
|
|
228
|
+
|
|
229
|
+
### 6.4 降级
|
|
230
|
+
|
|
231
|
+
| 场景 | 表现 |
|
|
232
|
+
|---|---|
|
|
233
|
+
| `sessions` / `workspaces` 不可用 | Chat 区显示降级说明 + 「打开官方会话」链接,不白屏 |
|
|
234
|
+
| 工作区创建失败 | 显示错误 + 重试;配置抽屉与画布仍可用(本地数据) |
|
|
235
|
+
| 会话丢失 / 归档 | 自动新建会话;历史列表空态 |
|
|
236
|
+
| 无 `conversation.content` 槽位 | 显示 fallback 卡片,不吞错 |
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## 7. 交互主链路
|
|
241
|
+
|
|
242
|
+
1. 进入 `#/erp/cai` → 头部显示「账房」与状态点;无会话则创建,有则恢复。
|
|
243
|
+
2. 用户点引导 chip → composer 预填问题 → 发送 → 官方会话流式回答。
|
|
244
|
+
3. 用户点头像 → 右侧栏:看角色定义与已绑定技能,点「+」绑定新的 DSH 技能,或点卡片右上角解绑;改称呼 / 语气 / 时间口径会立即重写 AGENTS.md。
|
|
245
|
+
4. 用户点技能「问这个」→ composer 预填;点「看结果」→ 画布抽屉(用原渲染器)。
|
|
246
|
+
5. 关闭抽屉 → 回到对话,会话与滚动位置不变;切页 / 切语言 / 切主题 → 状态保持。
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## 8. 写作规范(spec 层面)
|
|
251
|
+
|
|
252
|
+
- **不写业务数据**:不写 KPI 数值、金额、比率、时点数字、勾稽算式、数据集列定义;这些属于 payload / 画布实现,不进本 spec 集。
|
|
253
|
+
- 可以写:结构数量(8 个技能、上限 12)、版本(S3 / v3.2)、状态(loaded / available / disabled)、键名(localStorage / 会话键 / 工作区目录)、渲染器种类、交互规则。
|
|
254
|
+
- 中文为主,字段 `{zh,en}` 成对;英文无汉字、不用撇号;无感叹号、无营销词、无 emoji。
|
|
255
|
+
- 状态三档 `ok / warn / off` 只用于 UI 语义;技能状态用 `loaded / available / disabled`。
|
|
256
|
+
- 禁止出现评审 / 修订记录 / 归档说明 / 未吸收 / review-archive 等历史内容。
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## 9. 落地阶段与验收
|
|
261
|
+
|
|
262
|
+
### 9.1 阶段
|
|
263
|
+
|
|
264
|
+
1. **本阶段(spec 定稿)**:四份 Bot 规格 + 本契约 + spec-CORE;不动代码。
|
|
265
|
+
2. **阶段 2(布局与对话)**:Chat 主区 + 头像配置抽屉 + 移除旧助手入口;四页接入。
|
|
266
|
+
3. **阶段 3(配置)**:身份偏好、DSH 技能绑定 / 解绑、bot.json 与 AGENTS.md 读写、i18n、预览与断言。
|
|
267
|
+
4. **阶段 4(铺开)**:总览 / 底座的 Chat 入口统一接入(本轮 spec 只给共享契约,不改其布局)。
|
|
268
|
+
|
|
269
|
+
> 实现状态(2026-11-12):阶段 2 / 3 已落地 —— `BotShell` + 头像配置抽屉 + 技能画布 + 每 Bot 工作区(`D:\erpBot\<bot>`,D 盘不可用时降级)+ 每 Bot 的 bot.json 与 AGENTS.md;原「数 / 管」两页合并为「底座 · MCP 与数据」单页(4 张 KPI + 8 个清单项,页内无 tab;`#/erp/shu` / `#/erp/guan` 作为深链别名落到同一页),导航从 7 项收敛为 6 项(总览 + 四 Bot + 底座),品牌改为 erpBot;`_erp-dev/check.cjs` 已覆盖 Bot 页、rail 与底座单页断言。总览与底座的 Chat 入口留在阶段 4。
|
|
270
|
+
|
|
271
|
+
### 9.2 验收清单(阶段 3 后逐条验证)
|
|
272
|
+
|
|
273
|
+
- [ ] 四页顶排 4 张 KPI 卡、7 种变体与弹窗不变;spec 中未出现具体数值。
|
|
274
|
+
- [ ] KPI 行以下是一张 Chat 大卡片,无常驻左栏、无常驻 Bot 面板。
|
|
275
|
+
- [ ] 对话头部有可点击的 Bot 头像;点击打开右侧栏(身份 + 专属技能,单页无页签)。
|
|
276
|
+
- [ ] 底座为单页:4 张 KPI + 8 个清单项、页内无 tab;`#/erp/shu` / `#/erp/guan` 都落到同一页,rail 仍高亮「底座」。
|
|
277
|
+
- [ ] 关闭抽屉后对话会话、滚动位置、输入草稿不丢。
|
|
278
|
+
- [ ] 身份:角色定义只读、偏好可改;专属技能:小方块卡片 + 「+」添加、卡片右上角解绑,绑定结果写入 bot.json 与 AGENTS.md。
|
|
279
|
+
- [ ] 技能「问这个」能预填 composer;「看结果」能打开画布抽屉并返回。
|
|
280
|
+
- [ ] 四个 Bot 各自独立工作区(`D:\erpBot\cai|zi|tui|shui`)与会话;切页 / 切语言 / 刷新保持。
|
|
281
|
+
- [ ] D 盘不可用时能降级到 `Documents` 并在配置抽屉显示实际路径。
|
|
282
|
+
- [ ] TopBar 与总览无 AI 助手入口;无 Dock / Rail / Overlay 残留。
|
|
283
|
+
- [ ] 中英双语无中文泄漏;暗色主题正常;≤1180 / ≤900 / ≤720 / ≤560 窄面板按容器查询降级。
|
|
284
|
+
- [ ] `node scripts/build.mjs --check` / `lint-factories` / `node --check client.js` 全绿(阶段 2 起)。
|
|
285
|
+
- [ ] 插件槽位与 `/erp/workspace` 路由仍工作,仅接受白名单 bot id。
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## 10. 文档索引
|
|
290
|
+
|
|
291
|
+
| 文件 | 内容 |
|
|
292
|
+
|---|---|
|
|
293
|
+
| `README.md` | 本文件:布局 / 头像右侧栏 / Bot / 专属技能 / 工作区文件 / 对话契约 / 规范 / 验收 |
|
|
294
|
+
| `spec-CORE.md` | 速览:四 Bot 身份、技能配置、平台技能库、记忆与对话要点 |
|
|
295
|
+
| `cai-content-spec.md` | 财 · 账房 Bot:身份定义 + 8 技能配置 + 记忆 + 对话交互 |
|
|
296
|
+
| `zi-content-spec.md` | 资 · 司库 Bot:身份定义 + 6 技能配置 + 记忆 + 对话交互 |
|
|
297
|
+
| `tui-content-spec.md` | 推 · 军师 Bot:身份定义 + 8 技能配置 + 记忆 + 对话交互 |
|
|
298
|
+
| `shui-content-spec.md` | 税 · 税官 Bot:身份定义 + 6 技能配置 + 记忆 + 对话交互 |
|
|
299
|
+
| `base-content-spec.md` | 底座(MCP 与数据单页):定位与无 tab 规则、4 KPI、8 清单项、技能注册表(S3)、与四 Bot 的关系、边界与验收 |
|