cli-calctool 0.3.4 → 1.0.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 +9 -161
- package/cli.mjs +238 -0
- package/package.json +20 -7
- package/{skills/calctool → skill}/skill.json +1 -1
- package/LICENSE +0 -201
- package/install.mjs +0 -442
- package/skills/blueprint/SKILL.md +0 -47
- package/skills/blueprint/install-meta.json +0 -7
- package/skills/blueprint/skill.json +0 -10
- package/skills/calctool/install-meta.json +0 -7
- package/skills/calctool/platform-template/README.md +0 -74
- package/skills/calctool/platform-template/index.html +0 -12
- package/skills/calctool/platform-template/package.json +0 -26
- package/skills/calctool/platform-template/src/App.tsx +0 -233
- package/skills/calctool/platform-template/src/authz.ts +0 -103
- package/skills/calctool/platform-template/src/engine/evaluate.ts +0 -114
- package/skills/calctool/platform-template/src/engine-definition.json +0 -195
- package/skills/calctool/platform-template/src/main.tsx +0 -13
- package/skills/calctool/platform-template/src/pipeline.ts +0 -80
- package/skills/calctool/platform-template/src/store.ts +0 -53
- package/skills/calctool/platform-template/tsconfig.json +0 -15
- package/skills/calctool/platform-template/vite.config.ts +0 -7
- package/skills/calctool/references/declarative-pages.md +0 -69
- package/skills/calctool/references/engine-meta-model.md +0 -123
- package/skills/calctool/references/finance-example.md +0 -132
- package/skills/calctool/references/formula-dsl.md +0 -92
- package/skills/calctool/references/import-ocr.md +0 -72
- package/skills/calctool/templates/ecommerce-ops/README.md +0 -27
- package/skills/calctool/templates/ecommerce-ops/domain-reference.yaml +0 -172
- package/skills/swarm/SKILL.md +0 -120
- package/skills/swarm/install-meta.json +0 -7
- package/skills/swarm/references/ops-heartbeat.md +0 -45
- package/skills/swarm/references/org-chart.md +0 -50
- package/skills/swarm/references/security-guard.md +0 -56
- package/skills/swarm/references/task-lifecycle.md +0 -53
- package/skills/swarm/references/traffic-light.md +0 -52
- package/skills/swarm/skill.json +0 -10
- package/skills/swarm/swarm-runtime.mjs +0 -600
- package/sources.json +0 -25
- package/{skills/calctool → skill}/SKILL.md +5 -5
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
// calctool 生成的可运行工具:整套工具(联通节点,非单页孤岛)
|
|
2
|
-
// 链路:录入 → 校验 → 计算 → 存储 → 输出(+ 自动化)
|
|
3
|
-
// 调用外部能力前必须授权(一次授权持久化,不再提示)
|
|
4
|
-
import { useEffect, useState } from 'react'
|
|
5
|
-
import { Alert, Button, Card, Form, Input, InputNumber, message, Modal, Segmented, Space, Statistic, Table, Tag, Typography } from 'antd'
|
|
6
|
-
import { evaluateEngine } from './engine/evaluate'
|
|
7
|
-
import { ToolStore } from './store'
|
|
8
|
-
import { authz, OnboardingConsent, type CapabilityKind } from './authz'
|
|
9
|
-
import { defaultPipeline, nodeNeighbors, type EngineDefinition } from './pipeline'
|
|
10
|
-
import engine from './engine-definition.json'
|
|
11
|
-
|
|
12
|
-
const { Title, Paragraph, Text } = Typography
|
|
13
|
-
|
|
14
|
-
const def = engine as EngineDefinition
|
|
15
|
-
const store = new ToolStore(def.engineId)
|
|
16
|
-
const pipeline = def.pipeline ?? defaultPipeline(def.engineId)
|
|
17
|
-
const onboarding = new OnboardingConsent(def.engineId)
|
|
18
|
-
|
|
19
|
-
export default function App() {
|
|
20
|
-
const [form] = Form.useForm()
|
|
21
|
-
const [results, setResults] = useState<Record<string, string>>({})
|
|
22
|
-
const [page, setPage] = useState<string>('input')
|
|
23
|
-
const [history, setHistory] = useState(store.list())
|
|
24
|
-
const [authzModal, setAuthzModal] = useState<{ capability: CapabilityKind; purpose: string } | null>(null)
|
|
25
|
-
const [showOnboarding, setShowOnboarding] = useState(!onboarding.accepted())
|
|
26
|
-
|
|
27
|
-
// 首次使用总提示(一次性同意,持久化不再弹)
|
|
28
|
-
useEffect(() => {
|
|
29
|
-
if (!onboarding.accepted()) setShowOnboarding(true)
|
|
30
|
-
}, [])
|
|
31
|
-
|
|
32
|
-
const acceptOnboarding = () => {
|
|
33
|
-
onboarding.accept()
|
|
34
|
-
setShowOnboarding(false)
|
|
35
|
-
message.success('已记录同意;如需调整可在「授权管理」页查看')
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// 授权确认(一次授权持久化;未授权则询问,授权后不再提示)
|
|
39
|
-
const ensureAuthz = (capability: CapabilityKind, purpose: string): boolean => {
|
|
40
|
-
if (authz.isGranted(capability)) return true
|
|
41
|
-
setAuthzModal({ capability, purpose })
|
|
42
|
-
return false
|
|
43
|
-
}
|
|
44
|
-
const confirmAuthz = () => {
|
|
45
|
-
if (authzModal) {
|
|
46
|
-
authz.grant(authzModal.capability, authzModal.purpose)
|
|
47
|
-
message.success(`已授权「${authz.label(authzModal.capability)}」,后续不再提示`)
|
|
48
|
-
setAuthzModal(null)
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const run = (values: Record<string, unknown>) => {
|
|
53
|
-
// 计算前需授权存储能力(写入历史)
|
|
54
|
-
if (!ensureAuthz('storage', '保存计算记录到本地历史')) return
|
|
55
|
-
try {
|
|
56
|
-
const inputs = Object.fromEntries(
|
|
57
|
-
Object.entries(values).map(([k, v]) => [k, v === undefined || v === '' ? null : Number(v)]),
|
|
58
|
-
)
|
|
59
|
-
// 链路:compute -> store -> output
|
|
60
|
-
const out = evaluateEngine(def.formulas, inputs)
|
|
61
|
-
setResults(out)
|
|
62
|
-
store.save(inputs, out)
|
|
63
|
-
setHistory(store.list())
|
|
64
|
-
message.success('计算完成')
|
|
65
|
-
setPage('dashboard')
|
|
66
|
-
} catch (error) {
|
|
67
|
-
message.error(error instanceof Error ? error.message : '计算失败,请检查输入')
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// 首次使用总提示弹窗(同意后持久化,不再弹出)
|
|
72
|
-
const renderOnboardingModal = () => (
|
|
73
|
-
<Modal
|
|
74
|
-
open={showOnboarding}
|
|
75
|
-
title={`欢迎使用「${def.name}」`}
|
|
76
|
-
okText="同意并开始使用"
|
|
77
|
-
cancelText="暂不使用"
|
|
78
|
-
onOk={acceptOnboarding}
|
|
79
|
-
onCancel={acceptOnboarding}
|
|
80
|
-
closable={false}
|
|
81
|
-
maskClosable={false}
|
|
82
|
-
>
|
|
83
|
-
<Paragraph>
|
|
84
|
-
<Text strong>关于能力调用与消耗:</Text>
|
|
85
|
-
</Paragraph>
|
|
86
|
-
<Paragraph>
|
|
87
|
-
本工具为整套计算工具(录入 → 校验 → 计算 → 存储 → 输出,联通节点)。
|
|
88
|
-
使用过程中<b>可能调用其他技能辅助</b>(如 Blueprint 开发流程编排、互联网情报搜索、AI 模型),
|
|
89
|
-
<b>可能增加调用消耗</b>。
|
|
90
|
-
</Paragraph>
|
|
91
|
-
<Paragraph type="secondary">
|
|
92
|
-
同意后不再提示;后续可在「授权管理」页查看已授权能力并随时撤销。
|
|
93
|
-
</Paragraph>
|
|
94
|
-
</Modal>
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
// 授权确认弹窗(用户可见,一次授权)
|
|
98
|
-
const renderAuthzModal = () => (
|
|
99
|
-
<Modal
|
|
100
|
-
open={authzModal !== null}
|
|
101
|
-
title="能力调用授权"
|
|
102
|
-
okText="授权"
|
|
103
|
-
cancelText="拒绝"
|
|
104
|
-
onOk={confirmAuthz}
|
|
105
|
-
onCancel={() => setAuthzModal(null)}
|
|
106
|
-
>
|
|
107
|
-
{authzModal ? (
|
|
108
|
-
<Paragraph>
|
|
109
|
-
本工具将调用 <Text strong>{authz.label(authzModal.capability)}</Text>:
|
|
110
|
-
<br />{authzModal.purpose}
|
|
111
|
-
<br />
|
|
112
|
-
<Text type="secondary">授权一次后永久记住,后续不再提示。可在「授权管理」页撤销。</Text>
|
|
113
|
-
</Paragraph>
|
|
114
|
-
) : null}
|
|
115
|
-
</Modal>
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
const renderInput = () => (
|
|
119
|
-
<Card title="录入" style={{ maxWidth: 720, margin: '0 auto' }}>
|
|
120
|
-
<Form form={form} layout="vertical" onFinish={run}>
|
|
121
|
-
{def.fields.map((f) => (
|
|
122
|
-
<Form.Item
|
|
123
|
-
key={f.key}
|
|
124
|
-
name={f.key}
|
|
125
|
-
label={`${f.label}${f.unit ? `(${f.unit})` : ''}`}
|
|
126
|
-
rules={[{ required: Boolean(f.required), message: `请输入${f.label}` }]}
|
|
127
|
-
>
|
|
128
|
-
{['integer', 'number', 'money', 'percent'].includes(f.type)
|
|
129
|
-
? <InputNumber style={{ width: '100%' }} placeholder={`请输入${f.label}`} />
|
|
130
|
-
: <Input placeholder={`请输入${f.label}`} />}
|
|
131
|
-
</Form.Item>
|
|
132
|
-
))}
|
|
133
|
-
<Button type="primary" htmlType="submit">计算</Button>
|
|
134
|
-
</Form>
|
|
135
|
-
</Card>
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
const renderDashboard = () => (
|
|
139
|
-
<Card title="指标卡" style={{ maxWidth: 720, margin: '0 auto' }}>
|
|
140
|
-
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
|
141
|
-
<Button onClick={() => setPage('input')}>返回修改</Button>
|
|
142
|
-
<Space wrap size="large">
|
|
143
|
-
{def.formulas.map((f) => (
|
|
144
|
-
<Statistic key={f.key} title={f.label} value={results[f.key] ?? '—'} precision={2} valueStyle={{ fontSize: 22 }} />
|
|
145
|
-
))}
|
|
146
|
-
</Space>
|
|
147
|
-
<Text type="secondary">计算基于确定性公式引擎(decimal.js),结果可复现。</Text>
|
|
148
|
-
</Space>
|
|
149
|
-
</Card>
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
const renderPipeline = () => (
|
|
153
|
-
<Card title="工具链路(整套工具 · 联通节点)" style={{ maxWidth: 720, margin: '0 auto' }}>
|
|
154
|
-
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
|
155
|
-
{pipeline.nodes.map((node) => {
|
|
156
|
-
const neighbors = nodeNeighbors(pipeline, node.id)
|
|
157
|
-
const downstream = neighbors?.downstream.map((n) => n.id).join(' → ') || '(终端)'
|
|
158
|
-
return (
|
|
159
|
-
<div key={node.id} style={{ border: '1px solid #d9d9d9', borderRadius: 8, padding: 12 }}>
|
|
160
|
-
<Space>
|
|
161
|
-
<Tag color="blue">{node.kind}</Tag>
|
|
162
|
-
<Text strong>{node.label}</Text>
|
|
163
|
-
<Text type="secondary">→ {downstream}</Text>
|
|
164
|
-
</Space>
|
|
165
|
-
</div>
|
|
166
|
-
)
|
|
167
|
-
})}
|
|
168
|
-
<Alert type="info" showIcon message="链路说明" description="工具由联通节点组成:录入 → 校验 → 计算 → 存储 → 输出。改动指标/公式只改引擎定义,链路自动感知;新增自动化节点可扩展。" />
|
|
169
|
-
</Space>
|
|
170
|
-
</Card>
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
const renderReport = () => (
|
|
174
|
-
<Card title="历史记录" style={{ maxWidth: 720, margin: '0 auto' }}>
|
|
175
|
-
<Table<Record<string, unknown>>
|
|
176
|
-
rowKey="id"
|
|
177
|
-
dataSource={history}
|
|
178
|
-
pagination={{ pageSize: 5 }}
|
|
179
|
-
columns={[
|
|
180
|
-
{ title: '时间', dataIndex: 'updatedAt', render: (v: string) => new Date(v).toLocaleString() },
|
|
181
|
-
{ title: '输入', dataIndex: 'inputs', render: (v: Record<string, unknown>) => JSON.stringify(v) },
|
|
182
|
-
{ title: '结果', dataIndex: 'results', render: (v: Record<string, unknown>) => JSON.stringify(v) },
|
|
183
|
-
]}
|
|
184
|
-
/>
|
|
185
|
-
<Button danger style={{ marginTop: 16 }} onClick={() => { if (ensureAuthz('storage', '清空本地历史记录')) { store.clear(); setHistory([]) } }}>清空历史</Button>
|
|
186
|
-
</Card>
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
const renderAuthz = () => (
|
|
190
|
-
<Card title="授权管理" style={{ maxWidth: 720, margin: '0 auto' }}>
|
|
191
|
-
<Table<ReturnType<typeof authz.list>[number]>
|
|
192
|
-
rowKey="capability"
|
|
193
|
-
dataSource={authz.list()}
|
|
194
|
-
pagination={false}
|
|
195
|
-
columns={[
|
|
196
|
-
{ title: '能力', dataIndex: 'capability', render: (v: CapabilityKind) => authz.label(v) },
|
|
197
|
-
{ title: '状态', dataIndex: 'granted', render: (v: boolean) => (v ? <Tag color="green">已授权</Tag> : <Tag>未授权</Tag>) },
|
|
198
|
-
{ title: '授权时间', dataIndex: 'grantedAt', render: (v: string) => new Date(v).toLocaleString() },
|
|
199
|
-
{ title: '操作', render: (_, r) => <Button size="small" danger onClick={() => { authz.revoke(r.capability); setPage('authz') }}>撤销</Button> },
|
|
200
|
-
]}
|
|
201
|
-
/>
|
|
202
|
-
</Card>
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
return (
|
|
206
|
-
<div style={{ maxWidth: 960, margin: '0 auto', padding: '32px 16px' }}>
|
|
207
|
-
<Title level={2} style={{ textAlign: 'center' }}>{def.name}</Title>
|
|
208
|
-
<Paragraph type="secondary" style={{ textAlign: 'center' }}>
|
|
209
|
-
引擎 ID:{def.engineId} · 整套工具({pipeline.nodes.length} 个联通节点)· 配置驱动
|
|
210
|
-
</Paragraph>
|
|
211
|
-
<div style={{ textAlign: 'center', marginBottom: 24 }}>
|
|
212
|
-
<Segmented
|
|
213
|
-
value={page}
|
|
214
|
-
onChange={(v) => setPage(String(v))}
|
|
215
|
-
options={[
|
|
216
|
-
{ label: '录入', value: 'input' },
|
|
217
|
-
{ label: '指标卡', value: 'dashboard' },
|
|
218
|
-
{ label: '链路', value: 'pipeline' },
|
|
219
|
-
{ label: '报告', value: 'report' },
|
|
220
|
-
{ label: '授权', value: 'authz' },
|
|
221
|
-
]}
|
|
222
|
-
/>
|
|
223
|
-
</div>
|
|
224
|
-
{page === 'input' && renderInput()}
|
|
225
|
-
{page === 'dashboard' && renderDashboard()}
|
|
226
|
-
{page === 'pipeline' && renderPipeline()}
|
|
227
|
-
{page === 'report' && renderReport()}
|
|
228
|
-
{page === 'authz' && renderAuthz()}
|
|
229
|
-
{renderOnboardingModal()}
|
|
230
|
-
{renderAuthzModal()}
|
|
231
|
-
</div>
|
|
232
|
-
)
|
|
233
|
-
}
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
// calctool 授权管理:调用外部能力前的透明授权 + 一次授权持久化
|
|
2
|
-
// 原则:调用任何外部能力(blueprint / 网络搜索 / 模型 / 外部 API)前必须告知用户,
|
|
3
|
-
// 用户授权一次后记住(localStorage 持久化),后续不再重复提示。
|
|
4
|
-
|
|
5
|
-
export type CapabilityKind =
|
|
6
|
-
| 'blueprint' // Blueprint 开发流程编排
|
|
7
|
-
| 'web-search' // 情报蜂群网络搜索
|
|
8
|
-
| 'model' // AI 模型调用
|
|
9
|
-
| 'external-api' // 外部 API(数据源/报表导出等)
|
|
10
|
-
| 'storage' // 本地/远端数据存储
|
|
11
|
-
| 'automation' // 自动化(定时/触发器)
|
|
12
|
-
|
|
13
|
-
export type Authorization = {
|
|
14
|
-
capability: CapabilityKind
|
|
15
|
-
granted: boolean
|
|
16
|
-
grantedAt: string
|
|
17
|
-
note?: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const AUTH_KEY = 'calctool:authz:v1'
|
|
21
|
-
const ONBOARDING_KEY = 'calctool:onboarding:v1'
|
|
22
|
-
const CAPABILITY_LABELS: Record<CapabilityKind, string> = {
|
|
23
|
-
'blueprint': 'Blueprint 开发流程编排',
|
|
24
|
-
'web-search': '互联网情报搜索',
|
|
25
|
-
'model': 'AI 模型调用',
|
|
26
|
-
'external-api': '外部 API 数据源',
|
|
27
|
-
'storage': '数据存储',
|
|
28
|
-
'automation': '自动化任务',
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** 首次使用总提示:告知工具可能调用其他技能/增加消耗,同意后持久化不再弹 */
|
|
32
|
-
export class OnboardingConsent {
|
|
33
|
-
constructor(private readonly engineId: string) {}
|
|
34
|
-
|
|
35
|
-
private key(): string { return `${ONBOARDING_KEY}:${this.engineId}` }
|
|
36
|
-
|
|
37
|
-
/** 是否已同意首次使用提示 */
|
|
38
|
-
accepted(): boolean {
|
|
39
|
-
if (typeof localStorage === 'undefined') return true
|
|
40
|
-
return localStorage.getItem(this.key()) === 'accepted'
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** 同意并持久化(一次同意,不再提示) */
|
|
44
|
-
accept(): void {
|
|
45
|
-
if (typeof localStorage !== 'undefined') {
|
|
46
|
-
localStorage.setItem(this.key(), 'accepted')
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** 首次使用提示文案(可自定义,默认含消耗提示) */
|
|
51
|
-
notice(extra?: string): string {
|
|
52
|
-
return extra ?? '本工具为整套计算工具,可能调用其他技能辅助使用(如 Blueprint 开发流程编排、互联网情报搜索、AI 模型),可能增加调用消耗。同意后不再提示,可随时在「授权管理」页查看与撤销。'
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export class AuthzManager {
|
|
57
|
-
private store: Record<string, Authorization> = {}
|
|
58
|
-
|
|
59
|
-
constructor() {
|
|
60
|
-
if (typeof localStorage === 'undefined') return
|
|
61
|
-
try {
|
|
62
|
-
this.store = JSON.parse(localStorage.getItem(AUTH_KEY) ?? '{}')
|
|
63
|
-
} catch { this.store = {} }
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** 是否已授权某能力 */
|
|
67
|
-
isGranted(capability: CapabilityKind): boolean {
|
|
68
|
-
return this.store[capability]?.granted === true
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** 需要授权时的提示文案 */
|
|
72
|
-
promptFor(capability: CapabilityKind, purpose: string): string {
|
|
73
|
-
return `调用「${CAPABILITY_LABELS[capability]}」:${purpose}。是否授权?授权一次后不再提示。`
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** 记录授权(一次授权,持久化) */
|
|
77
|
-
grant(capability: CapabilityKind, note?: string): void {
|
|
78
|
-
this.store[capability] = {
|
|
79
|
-
capability, granted: true, grantedAt: new Date().toISOString(), note,
|
|
80
|
-
}
|
|
81
|
-
if (typeof localStorage !== 'undefined') {
|
|
82
|
-
localStorage.setItem(AUTH_KEY, JSON.stringify(this.store))
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** 撤销授权 */
|
|
87
|
-
revoke(capability: CapabilityKind): void {
|
|
88
|
-
delete this.store[capability]
|
|
89
|
-
if (typeof localStorage !== 'undefined') {
|
|
90
|
-
localStorage.setItem(AUTH_KEY, JSON.stringify(this.store))
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
list(): Authorization[] {
|
|
95
|
-
return Object.values(this.store)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
static label(capability: CapabilityKind): string {
|
|
99
|
-
return CAPABILITY_LABELS[capability]
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export const authz = new AuthzManager()
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
// calctool 公式引擎:JSON AST + decimal.js 确定性求值
|
|
2
|
-
// 公式 DSL 规范见 references/formula-dsl.md
|
|
3
|
-
import Decimal from 'decimal.js'
|
|
4
|
-
|
|
5
|
-
export type FormulaNode =
|
|
6
|
-
| { ref: string }
|
|
7
|
-
| { lit: string | number }
|
|
8
|
-
| { op: string; args: FormulaNode[] }
|
|
9
|
-
|
|
10
|
-
export type CalcError =
|
|
11
|
-
| { code: 'DIV_ZERO'; nodeId: string }
|
|
12
|
-
| { code: 'MISSING_INPUT'; fieldId: string }
|
|
13
|
-
| { code: 'NON_FINITE'; nodeId: string }
|
|
14
|
-
|
|
15
|
-
export type EngineValues = Record<string, string | number | null>
|
|
16
|
-
|
|
17
|
-
/** 安全除法:除数为 0 用 fallback(默认 0),与 div 报错区分 */
|
|
18
|
-
function safeDiv(a: Decimal, b: Decimal, fallback = '0'): Decimal {
|
|
19
|
-
if (b.isZero()) return new Decimal(fallback)
|
|
20
|
-
return a.div(b)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** 求值一个公式 AST 节点(纯函数,无副作用)
|
|
24
|
-
* 节点形态:{ ref: 'field' } 引用 / { lit: 1 } 字面量 / { op, args } 运算
|
|
25
|
-
*/
|
|
26
|
-
export function evaluate(node: FormulaNode, values: EngineValues): Decimal {
|
|
27
|
-
// 引用节点(无 op,只有 ref)
|
|
28
|
-
if ('ref' in node && node.ref !== undefined) {
|
|
29
|
-
const raw = values[node.ref]
|
|
30
|
-
if (raw === null || raw === undefined || raw === '') {
|
|
31
|
-
throw { code: 'MISSING_INPUT', fieldId: node.ref } as CalcError
|
|
32
|
-
}
|
|
33
|
-
return new Decimal(String(raw))
|
|
34
|
-
}
|
|
35
|
-
// 字面量节点(无 op,只有 lit)
|
|
36
|
-
if ('lit' in node && node.lit !== undefined) {
|
|
37
|
-
return new Decimal(String(node.lit ?? 0))
|
|
38
|
-
}
|
|
39
|
-
switch (node.op) {
|
|
40
|
-
case 'add':
|
|
41
|
-
return node.args.reduce((acc, arg) => acc.plus(evaluate(arg, values)), new Decimal(0))
|
|
42
|
-
case 'sub': {
|
|
43
|
-
const [a, b] = node.args
|
|
44
|
-
return evaluate(a, values).minus(evaluate(b, values))
|
|
45
|
-
}
|
|
46
|
-
case 'mul':
|
|
47
|
-
return node.args.reduce((acc, arg) => acc.times(evaluate(arg, values)), new Decimal(1))
|
|
48
|
-
case 'div': {
|
|
49
|
-
const [a, b] = node.args
|
|
50
|
-
const divisor = evaluate(b, values)
|
|
51
|
-
if (divisor.isZero()) throw { code: 'DIV_ZERO', nodeId: 'div' } as CalcError
|
|
52
|
-
return evaluate(a, values).div(divisor)
|
|
53
|
-
}
|
|
54
|
-
case 'safeDivide': {
|
|
55
|
-
const [a, b] = node.args
|
|
56
|
-
return safeDiv(evaluate(a, values), evaluate(b, values))
|
|
57
|
-
}
|
|
58
|
-
case 'percentOf': {
|
|
59
|
-
const [a, b] = node.args
|
|
60
|
-
return evaluate(a, values).div(evaluate(b, values)).times(100)
|
|
61
|
-
}
|
|
62
|
-
case 'round': {
|
|
63
|
-
const [a] = node.args
|
|
64
|
-
return evaluate(a, values).toDecimalPlaces(2)
|
|
65
|
-
}
|
|
66
|
-
case 'if': {
|
|
67
|
-
const [cond, thenNode, elseNode] = node.args
|
|
68
|
-
const v = evaluate(cond, values)
|
|
69
|
-
return v.isZero() ? evaluate(elseNode, values) : evaluate(thenNode, values)
|
|
70
|
-
}
|
|
71
|
-
default:
|
|
72
|
-
throw new Error(`Unsupported operator: ${node.op}`)
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** 从 AST 提取引用的字段 key(依赖图构建用) */
|
|
77
|
-
export function extractRefs(node: FormulaNode | undefined): string[] {
|
|
78
|
-
if (!node) return []
|
|
79
|
-
if ('ref' in node && node.ref !== undefined) return [node.ref]
|
|
80
|
-
if ('lit' in node && node.lit !== undefined) return []
|
|
81
|
-
return (node.args ?? []).flatMap(extractRefs)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** 按公式定义计算全部结果(含跨公式引用,安全除零) */
|
|
85
|
-
export function evaluateEngine(
|
|
86
|
-
formulas: Array<{ key: string; expression: FormulaNode }>,
|
|
87
|
-
inputs: EngineValues,
|
|
88
|
-
): Record<string, string> {
|
|
89
|
-
const values: EngineValues = { ...inputs }
|
|
90
|
-
const results: Record<string, string> = {}
|
|
91
|
-
const visited = new Set<string>()
|
|
92
|
-
const stack: string[] = []
|
|
93
|
-
|
|
94
|
-
const compute = (key: string) => {
|
|
95
|
-
if (visited.has(key)) return
|
|
96
|
-
if (stack.includes(key)) throw new Error(`Cycle detected: ${[...stack, key].join(' → ')}`)
|
|
97
|
-
const formula = formulas.find((f) => f.key === key)
|
|
98
|
-
if (!formula) return
|
|
99
|
-
stack.push(key)
|
|
100
|
-
const refs = extractRefs(formula.expression)
|
|
101
|
-
for (const ref of refs) {
|
|
102
|
-
if (formulas.some((f) => f.key === ref)) compute(ref) // 跨公式引用先算
|
|
103
|
-
}
|
|
104
|
-
const result = evaluate(formula.expression, values)
|
|
105
|
-
const str = result.toFixed(4).replace(/\.?0+$/, '')
|
|
106
|
-
values[key] = str
|
|
107
|
-
results[key] = str
|
|
108
|
-
stack.pop()
|
|
109
|
-
visited.add(key)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
for (const f of formulas) compute(f.key)
|
|
113
|
-
return results
|
|
114
|
-
}
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"engineId": "ecommerce-ops-dashboard",
|
|
3
|
-
"name": "电商运营仪表盘",
|
|
4
|
-
"semanticVersion": "1.0.0",
|
|
5
|
-
"category": "ecommerce",
|
|
6
|
-
"decimalPolicy": "decimal-string",
|
|
7
|
-
"fields": [
|
|
8
|
-
{
|
|
9
|
-
"key": "visitors",
|
|
10
|
-
"label": "访客数",
|
|
11
|
-
"type": "integer",
|
|
12
|
-
"unit": "人",
|
|
13
|
-
"required": true
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
"key": "orders",
|
|
17
|
-
"label": "订单数",
|
|
18
|
-
"type": "integer",
|
|
19
|
-
"unit": "单",
|
|
20
|
-
"required": true
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
"key": "gmv",
|
|
24
|
-
"label": "GMV",
|
|
25
|
-
"type": "money",
|
|
26
|
-
"unit": "CNY",
|
|
27
|
-
"required": true
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
"key": "adSpend",
|
|
31
|
-
"label": "广告费",
|
|
32
|
-
"type": "money",
|
|
33
|
-
"unit": "CNY",
|
|
34
|
-
"required": true
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
"key": "customRate",
|
|
38
|
-
"label": "自定义转化率修正",
|
|
39
|
-
"type": "percent",
|
|
40
|
-
"unit": "%"
|
|
41
|
-
}
|
|
42
|
-
],
|
|
43
|
-
"formulas": [
|
|
44
|
-
{
|
|
45
|
-
"key": "conversionRate",
|
|
46
|
-
"label": "转化率",
|
|
47
|
-
"expression": {
|
|
48
|
-
"op": "safeDivide",
|
|
49
|
-
"args": [
|
|
50
|
-
{
|
|
51
|
-
"ref": "orders"
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
"ref": "visitors"
|
|
55
|
-
}
|
|
56
|
-
]
|
|
57
|
-
}
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
"key": "aov",
|
|
61
|
-
"label": "客单价",
|
|
62
|
-
"expression": {
|
|
63
|
-
"op": "safeDivide",
|
|
64
|
-
"args": [
|
|
65
|
-
{
|
|
66
|
-
"ref": "gmv"
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
"ref": "orders"
|
|
70
|
-
}
|
|
71
|
-
]
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
"key": "roas",
|
|
76
|
-
"label": "ROAS",
|
|
77
|
-
"expression": {
|
|
78
|
-
"op": "safeDivide",
|
|
79
|
-
"args": [
|
|
80
|
-
{
|
|
81
|
-
"ref": "gmv"
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
"ref": "adSpend"
|
|
85
|
-
}
|
|
86
|
-
]
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
"key": "gmvPerVisitor",
|
|
91
|
-
"label": "访客价值",
|
|
92
|
-
"expression": {
|
|
93
|
-
"op": "safeDivide",
|
|
94
|
-
"args": [
|
|
95
|
-
{
|
|
96
|
-
"ref": "gmv"
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
"ref": "visitors"
|
|
100
|
-
}
|
|
101
|
-
]
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
],
|
|
105
|
-
"rules": [],
|
|
106
|
-
"views": [],
|
|
107
|
-
"testSuites": [
|
|
108
|
-
{
|
|
109
|
-
"name": "基准样例",
|
|
110
|
-
"inputs": {
|
|
111
|
-
"visitors": 10000,
|
|
112
|
-
"orders": 300,
|
|
113
|
-
"gmv": 50000,
|
|
114
|
-
"adSpend": 12000
|
|
115
|
-
},
|
|
116
|
-
"expect": {
|
|
117
|
-
"conversionRate": "3",
|
|
118
|
-
"aov": "166.6667",
|
|
119
|
-
"roas": "4.1667"
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
],
|
|
123
|
-
"pipeline": {
|
|
124
|
-
"engineId": "ecommerce-ops-dashboard",
|
|
125
|
-
"nodes": [
|
|
126
|
-
{
|
|
127
|
-
"id": "input",
|
|
128
|
-
"kind": "input",
|
|
129
|
-
"label": "录入/导入",
|
|
130
|
-
"inputs": [],
|
|
131
|
-
"outputs": [
|
|
132
|
-
"validate"
|
|
133
|
-
]
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
"id": "validate",
|
|
137
|
-
"kind": "validate",
|
|
138
|
-
"label": "数据校验",
|
|
139
|
-
"inputs": [
|
|
140
|
-
"input"
|
|
141
|
-
],
|
|
142
|
-
"outputs": [
|
|
143
|
-
"compute"
|
|
144
|
-
]
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
"id": "compute",
|
|
148
|
-
"kind": "compute",
|
|
149
|
-
"label": "确定性计算",
|
|
150
|
-
"inputs": [
|
|
151
|
-
"validate"
|
|
152
|
-
],
|
|
153
|
-
"outputs": [
|
|
154
|
-
"store",
|
|
155
|
-
"output"
|
|
156
|
-
]
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
"id": "store",
|
|
160
|
-
"kind": "store",
|
|
161
|
-
"label": "数据存储",
|
|
162
|
-
"inputs": [
|
|
163
|
-
"compute"
|
|
164
|
-
],
|
|
165
|
-
"outputs": [
|
|
166
|
-
"output"
|
|
167
|
-
]
|
|
168
|
-
},
|
|
169
|
-
{
|
|
170
|
-
"id": "output",
|
|
171
|
-
"kind": "output",
|
|
172
|
-
"label": "指标卡/报告",
|
|
173
|
-
"inputs": [
|
|
174
|
-
"compute",
|
|
175
|
-
"store"
|
|
176
|
-
],
|
|
177
|
-
"outputs": []
|
|
178
|
-
},
|
|
179
|
-
{
|
|
180
|
-
"id": "automate",
|
|
181
|
-
"kind": "automate",
|
|
182
|
-
"label": "自动化(可选)",
|
|
183
|
-
"inputs": [
|
|
184
|
-
"store"
|
|
185
|
-
],
|
|
186
|
-
"outputs": [
|
|
187
|
-
"output"
|
|
188
|
-
],
|
|
189
|
-
"config": {
|
|
190
|
-
"enabled": false
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
]
|
|
194
|
-
}
|
|
195
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import ReactDOM from 'react-dom/client'
|
|
3
|
-
import { ConfigProvider } from 'antd'
|
|
4
|
-
import zhCN from 'antd/locale/zh_CN'
|
|
5
|
-
import App from './App'
|
|
6
|
-
|
|
7
|
-
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
8
|
-
<React.StrictMode>
|
|
9
|
-
<ConfigProvider locale={zhCN}>
|
|
10
|
-
<App />
|
|
11
|
-
</ConfigProvider>
|
|
12
|
-
</React.StrictMode>,
|
|
13
|
-
)
|