cli-calctool 1.0.0 → 1.0.2

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.
Files changed (40) hide show
  1. package/README.md +9 -161
  2. package/cli.mjs +238 -0
  3. package/package.json +12 -14
  4. package/{skills/calctool → skill}/SKILL.md +9 -5
  5. package/{skills/calctool → skill}/skill.json +2 -2
  6. package/LICENSE +0 -201
  7. package/install.mjs +0 -442
  8. package/skills/blueprint/SKILL.md +0 -47
  9. package/skills/blueprint/install-meta.json +0 -7
  10. package/skills/blueprint/skill.json +0 -10
  11. package/skills/calctool/install-meta.json +0 -7
  12. package/skills/calctool/platform-template/README.md +0 -74
  13. package/skills/calctool/platform-template/index.html +0 -12
  14. package/skills/calctool/platform-template/package.json +0 -26
  15. package/skills/calctool/platform-template/src/App.tsx +0 -233
  16. package/skills/calctool/platform-template/src/authz.ts +0 -103
  17. package/skills/calctool/platform-template/src/engine/evaluate.ts +0 -114
  18. package/skills/calctool/platform-template/src/engine-definition.json +0 -195
  19. package/skills/calctool/platform-template/src/main.tsx +0 -13
  20. package/skills/calctool/platform-template/src/pipeline.ts +0 -80
  21. package/skills/calctool/platform-template/src/store.ts +0 -53
  22. package/skills/calctool/platform-template/tsconfig.json +0 -15
  23. package/skills/calctool/platform-template/vite.config.ts +0 -7
  24. package/skills/calctool/references/declarative-pages.md +0 -69
  25. package/skills/calctool/references/engine-meta-model.md +0 -123
  26. package/skills/calctool/references/finance-example.md +0 -132
  27. package/skills/calctool/references/formula-dsl.md +0 -92
  28. package/skills/calctool/references/import-ocr.md +0 -72
  29. package/skills/calctool/templates/ecommerce-ops/README.md +0 -27
  30. package/skills/calctool/templates/ecommerce-ops/domain-reference.yaml +0 -172
  31. package/skills/swarm/SKILL.md +0 -120
  32. package/skills/swarm/install-meta.json +0 -7
  33. package/skills/swarm/references/ops-heartbeat.md +0 -45
  34. package/skills/swarm/references/org-chart.md +0 -50
  35. package/skills/swarm/references/security-guard.md +0 -56
  36. package/skills/swarm/references/task-lifecycle.md +0 -53
  37. package/skills/swarm/references/traffic-light.md +0 -52
  38. package/skills/swarm/skill.json +0 -10
  39. package/skills/swarm/swarm-runtime.mjs +0 -600
  40. package/sources.json +0 -25
@@ -1,47 +0,0 @@
1
- ---
2
- name: blueprint
3
- description: '把一个目标编译为可执行、可验证、可追溯的工程蓝图(确定性检查节点/依赖/验收标准/合同)。Compile one goal into an executable, verifiable, traceable engineering blueprint with deterministic checks. Превращает цель в исполняемый, проверяемый инженерный план с узлами, зависимостями, критериями приёмки и валидацией.'
4
- ---
5
-
6
- # Blueprint Skill
7
-
8
- Endpoint: https://cli.tax/wvz6zmRWmX
9
- Request schema: blueprint.skill.request/1.0
10
- Response schema: blueprint.skill.response/1.0
11
-
12
- ## Request envelope
13
-
14
- POST JSON to the endpoint with an `input` wrapper:
15
-
16
- ```json
17
- {
18
- "input": {
19
- "schemaVersion": "blueprint.skill.request/1.0",
20
- "requestId": "<unique-id>",
21
- "operation": "<operation>",
22
- "input": {}
23
- }
24
- }
25
- ```
26
-
27
- ## Operations
28
-
29
- - `capabilities`: discover the operation list, per-operation inputs, and the recommended next step.
30
- - `help`: return the usage guide, operation catalog, and request examples.
31
- - `intake`: return the questions the IDE must ask the user before building the Blueprint.
32
- - `validate`: deterministically validate a Blueprint object conforming to `blueprint.ir/1.0`.
33
- - `compile-inline`: validate and compile a Blueprint, returning the generated artifacts inline.
34
-
35
- ## Required flow
36
-
37
- 1. Call `capabilities` first and read the returned `nextStep`.
38
- 2. Call `intake` and ask the user the returned questions one at a time, waiting for each answer.
39
- 3. Do not compile a Blueprint until all required questions are answered.
40
- 4. Build a Blueprint conforming to `blueprint.ir/1.0`, then call `validate`.
41
- 5. Fix every validation finding until the report is green, then call `compile-inline` and save the artifacts.
42
-
43
- ## Safety rules
44
-
45
- - Never send credentials, model keys, provider endpoints, or personal secrets inside the request envelope or `input`.
46
- - The response `status` must be `succeeded`; a `failed` response is an error, not a result.
47
- - Public responses never prove that code was developed, tested, or deployed.
@@ -1,7 +0,0 @@
1
- {
2
- "source": "wvz6zmRWmX",
3
- "slug": "blueprint",
4
- "version": "v0.4.0",
5
- "endpoint": "https://cli.tax/api/public/skills/wvz6zmRWmX",
6
- "installedAt": "2026-08-17T00:00:00.000Z"
7
- }
@@ -1,10 +0,0 @@
1
- {
2
- "name": "blueprint",
3
- "displayName": "Blueprint",
4
- "description": "Blueprint 是一个跨工具的工程规划 CLI,把明确目标编译为可执行、可追溯、可验收的全站工程蓝图,并通过确定性规则检查每个节点、分支、合同、任务与验收闭环。默认由当前 IDE 的多智能体协同推演,也可连接用户自行安装的本地 Hermes;平台不托管模型密钥。",
5
- "schemaVersion": "cli.request/1.0",
6
- "endpoint": "https://cli.tax/wvz6zmRWmX",
7
- "method": "POST",
8
- "version": "v0.4.0",
9
- "type": "Skill"
10
- }
@@ -1,7 +0,0 @@
1
- {
2
- "source": "KKyA6xljUX",
3
- "slug": "calctool",
4
- "version": "v0.4.0",
5
- "endpoint": "https://cli.tax/api/public/skills/KKyA6xljUX",
6
- "installedAt": "2026-08-17T00:00:00.000Z"
7
- }
@@ -1,74 +0,0 @@
1
- # calctool 平台模板
2
-
3
- 由 calctool 生成的**可运行计算工具**工程模板(配置驱动、框架不变)。
4
-
5
- ## 一键运行
6
-
7
- ```bash
8
- npm install # 或 pnpm install / yarn install(环境自动适配)
9
- npm run dev # 启动开发服务器 http://localhost:5173
10
- ```
11
-
12
- ## 结构
13
-
14
- ```
15
- ├── src/
16
- │ ├── main.tsx # 入口(Ant Design X ConfigProvider)
17
- │ ├── App.tsx # 应用壳(录入/指标卡/链路/报告/授权 五页)
18
- │ ├── engine-definition.json # ★ 引擎定义 + 链路(改这里 = 改工具,框架不动)
19
- │ ├── engine/
20
- │ │ ├── evaluate.ts # JSON AST + decimal.js 确定性求值
21
- │ │ └── (recompute.ts 依赖图增量重算,按需扩展)
22
- │ ├── pipeline.ts # ★ 链路编排(联通节点,非单页孤岛)
23
- │ ├── authz.ts # ★ 授权管理(首次使用同意 + 逐能力授权,一次授权持久化)
24
- │ └── store.ts # 存储层(localStorage,可换 sqlite)
25
- ├── vite.config.ts
26
- └── package.json
27
- ```
28
-
29
- ## 整套工具(联通节点,非单页孤岛)
30
-
31
- 工具由**联通节点**组成,数据流在节点间流动:
32
-
33
- ```
34
- 录入/导入 → 数据校验 → 确定性计算 → 数据存储 → 指标卡/报告
35
- ↘ 自动化(可选)
36
- ```
37
-
38
- - 改动指标/公式:只改 `engine-definition.json`,链路自动感知
39
- - 新增能力:在 `pipeline.ts` 加节点(如导入、导出、告警),页面无需改
40
- - 页面「链路」Tab 可视化每个节点及其上下游,改工具时定位影响范围
41
-
42
- ## 授权与消耗提示
43
-
44
- 1. **首次使用总提示**:打开工具时弹窗告知"可能调用其他技能辅助、可能增加消耗",同意后持久化不再提示(可按工具分别记录)
45
- 2. **逐能力授权**:实际调用外部能力(Blueprint/搜索/模型/存储/自动化)前询问,一次授权永久记住
46
- 3. **授权管理页**:随时查看已授权能力、撤销授权
47
-
48
-
49
- ## 改指标/公式(需求变更,框架不变)
50
-
51
- 编辑 `src/engine-definition.json`:
52
-
53
- ```json
54
- {
55
- "fields": [{ "key": "visitors", "label": "访客数", "type": "integer", "unit": "人" }],
56
- "formulas": [
57
- { "key": "conversionRate", "label": "转化率",
58
- "expression": { "op": "safeDivide", "args": [{ "ref": "orders" }, { "ref": "visitors" }] } }
59
- ]
60
- }
61
- ```
62
-
63
- 保存即热更新——**不用改任何代码**。
64
-
65
- ## 公式运算符(详见 formula-dsl.md)
66
-
67
- `add / sub / mul / div(除零报错)/ safeDivide(除零回退)/ percentOf / round / if / ref / lit`
68
-
69
- ## 环境适配
70
-
71
- - Node ≥18:全功能(可换 better-sqlite3 持久化)
72
- - Node 16:兼容(sql.js WASM)
73
- - Node <16:建议只用 L0 配置预览
74
- - 包管理器:npm/pnpm/yarn 命令自动适配
@@ -1,12 +0,0 @@
1
- <!doctype html>
2
- <html lang="zh-CN">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>calctool 计算工具</title>
7
- </head>
8
- <body>
9
- <div id="root"></div>
10
- <script type="module" src="/src/main.tsx"></script>
11
- </body>
12
- </html>
@@ -1,26 +0,0 @@
1
- {
2
- "name": "calctool-tool",
3
- "version": "1.0.0",
4
- "private": true,
5
- "type": "module",
6
- "description": "calctool 生成的可运行计算工具(配置驱动,Ant Design X + decimal.js)",
7
- "scripts": {
8
- "dev": "vite",
9
- "build": "tsc -b && vite build",
10
- "preview": "vite preview",
11
- "typecheck": "tsc --noEmit"
12
- },
13
- "dependencies": {
14
- "react": "^19.0.0",
15
- "react-dom": "^19.0.0",
16
- "antd": "^6.0.0",
17
- "@ant-design/x": "^2.9.0",
18
- "decimal.js": "^10.4.3",
19
- "lucide-react": "^0.469.0"
20
- },
21
- "devDependencies": {
22
- "@vitejs/plugin-react": "^4.3.4",
23
- "typescript": "^5.6.0",
24
- "vite": "^6.0.0"
25
- }
26
- }
@@ -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
- }