cli-calctool 0.1.0

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 (30) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +147 -0
  3. package/install.mjs +436 -0
  4. package/package.json +27 -0
  5. package/skills/blueprint/SKILL.md +47 -0
  6. package/skills/blueprint/install-meta.json +7 -0
  7. package/skills/blueprint/skill.json +10 -0
  8. package/skills/calctool/SKILL.md +167 -0
  9. package/skills/calctool/install-meta.json +7 -0
  10. package/skills/calctool/platform-template/README.md +74 -0
  11. package/skills/calctool/platform-template/index.html +12 -0
  12. package/skills/calctool/platform-template/package.json +26 -0
  13. package/skills/calctool/platform-template/src/App.tsx +233 -0
  14. package/skills/calctool/platform-template/src/authz.ts +103 -0
  15. package/skills/calctool/platform-template/src/engine/evaluate.ts +114 -0
  16. package/skills/calctool/platform-template/src/engine-definition.json +195 -0
  17. package/skills/calctool/platform-template/src/main.tsx +13 -0
  18. package/skills/calctool/platform-template/src/pipeline.ts +80 -0
  19. package/skills/calctool/platform-template/src/store.ts +53 -0
  20. package/skills/calctool/platform-template/tsconfig.json +15 -0
  21. package/skills/calctool/platform-template/vite.config.ts +7 -0
  22. package/skills/calctool/references/declarative-pages.md +69 -0
  23. package/skills/calctool/references/engine-meta-model.md +123 -0
  24. package/skills/calctool/references/finance-example.md +132 -0
  25. package/skills/calctool/references/formula-dsl.md +92 -0
  26. package/skills/calctool/references/import-ocr.md +72 -0
  27. package/skills/calctool/skill.json +10 -0
  28. package/skills/calctool/templates/ecommerce-ops/README.md +27 -0
  29. package/skills/calctool/templates/ecommerce-ops/domain-reference.yaml +172 -0
  30. package/sources.json +18 -0
@@ -0,0 +1,167 @@
1
+ ---
2
+ name: calctool
3
+ description: 按需生成「万能计算工具」:用户输入一个领域需求(如"我是财务,想要一个经营健康诊断工具"),本技能通过提问明确指标、公式、输入方式与输出形式,生成一个可执行、可验证、可发布的在线计算工具——支持自定义指标、自定义公式逻辑、用户上传内容自动识别(Excel 映射 / 图片 OCR)、报告输出。当用户想"把某套计算逻辑/指标/公式做成在线工具"时使用。
4
+ ---
5
+
6
+ # calctool
7
+
8
+ 把「业务计算逻辑」编译为「可执行的在线计算工具」的生成器。
9
+
10
+ ## 全链路总流程(老板视角 → 可运行工具)
11
+
12
+ ```
13
+ 老板(任何 IDE / DSH):"我要搭电商运营计算工具"
14
+
15
+ 1. 顾问式对话(intake-round) —— 智能体主动给方案(基于领域参考包),
16
+ 老板逐轮确认指标/公式/口径,直到说"创建"
17
+
18
+ 2. 情报蜂群(research) —— 自动派情报智能体搜索行业标准/抓指定地址,
19
+ 产出可溯源参考包(老板说"电商"等关键词时自动触发)
20
+
21
+ 3. 蜂群执行(brain-invoke/complete)—— 需求自动拆解为 fields/formulas/rules/
22
+ imports/reports/pages 任务,依赖图驱动并行,独立复核,确定性合并
23
+
24
+ 4. 编译工具(compile-tool) —— 引擎定义 → 可运行工程文件清单
25
+ (App 壳/公式引擎/存储/构建,页面自动生成:录入/指标卡/报告)
26
+
27
+ 5. 环境适配(probe-env/adapt-config)—— 自动探测 Node 版本/包管理器/OS/架构,
28
+ Node≥18 全功能、16 兼容(sql.js)、<16 零构建预览;pnpm/yarn/npm 自动适配
29
+
30
+ 6. 完成前门禁(final-gate) —— 审计/测试/运维三智能体协调接管检测,
31
+ 全部符合通过(engine-valid / 基准样例全通过 / 环境就绪)才标记完成
32
+
33
+ 7. 交付运行 —— 按适配命令一键搭建(pnpm install → pnpm run dev)
34
+ → 老板得到可交互工具;后续对话继续改指标/公式(热更新)
35
+
36
+ 或直接走「全自动流水线」(auto-pipeline)——老板只需说需求 + 授权一次:
37
+ 授权(告知消耗) → 情报蜂群 → 蜂群生成 → 测试审计 → 编译 → 自动启动弹出页面
38
+ → 完成前门禁(审计/测试/运维三智能体协调接管,全部符合通过才标记完成)
39
+ 全程无需老板参与,热更新即时生效
40
+ ```
41
+
42
+ **关键**:老板一句话 → 对话确认 → 情报 → 蜂群 → 编译 → 环境适配 → **可运行工具**。全程框架不变,需求变更只改配置。
43
+
44
+ ## 何时使用
45
+
46
+ - 用户描述了一个领域场景(财务、运营、工程、教育、医疗……)并想要一个可交互的在线计算工具
47
+ - 用户已有指标、公式、评分或报告逻辑,想固化成工具
48
+ - 用户需要"自定义指标 + 自定义公式 + 上传识别"能力
49
+
50
+ 不要用于:纯展示型页面(无计算)、与计算无关的 CRUD 后台。
51
+
52
+ ## 核心原则
53
+
54
+ 1. **确定性计算**:正式数字由版本化确定性引擎(公式 AST + Decimal 运行时)产生;模型/OCR/AI 只产生候选、草稿与解释,绝不直接给出不可追溯的正式结果。
55
+ 2. **配置化而非代码化**:工具 = 一份可发布的引擎定义(字段目录 + 公式图 + 规则包 + 导入映射 + 视图 + 报告),不是散落的页面代码。
56
+ 3. **显式除零**:所有除法必须选 `div`(除零报错)或 `safeDivide`(除零回退),不静默吞错。
57
+ 4. **零虚构**:能力未接入时保持"未接入态"(planned/not_installed/disconnected),不虚构数据、状态或按钮。
58
+
59
+ ## 五步实施流程
60
+
61
+ ### 1. intake —— 收集需求(必须提问,一次一问)
62
+ 调用运行时 intake 或按下面问题逐条问用户:
63
+ - 目标:这个工具帮用户完成什么?什么必须发生、什么绝不允许发生?
64
+ - 输入指标:用户会输入哪些字段?(如收入、成本、人数、月份)
65
+ - 公式逻辑:哪些指标由公式算出?(如毛利 = 收入 - 成本)
66
+ - 输入方式:手工录入 / Excel 上传 / 图片 OCR / 三者都要?
67
+ - 输出形式:指标卡、表格、诊断报告、历史记录?
68
+ - 约束:单位、精度、语言、离线/在线、禁止项?
69
+
70
+ ### 2. 生成引擎定义(Engine Definition)
71
+ ```yaml
72
+ engineId: <kebab-case-引擎名>
73
+ name: <显示名>
74
+ category: <领域,如 finance/operations/education>
75
+ ownerType: platform-template
76
+ status: draft
77
+ semanticVersion: 1.0.0
78
+ compatibilityProfile: legacy-compatible
79
+ decimalPolicy: decimal-string
80
+ defaultLocale: zh-CN
81
+ ```
82
+ 包含(详见 references/engine-meta-model.md):
83
+ - field-catalog:字段目录(类型:amount/ratio/int/enum/dimension/date)
84
+ - formula-graph:公式图(节点 = 字段/公式,边 = 依赖)
85
+ - rule-packs:规则包(阈值、评分、分级)
86
+ - import-profiles:导入映射(Excel 列 / OCR 字段 → 字段目录)
87
+ - report-template:报告模板(指标卡 + 表格 + 诊断结论)
88
+
89
+ ### 3. 编译公式(详见 references/formula-dsl.md)
90
+ - 把自然语言/Excel 公式编译为 JSON AST(禁止 eval/new Function)
91
+ - 运算符走最小注册表(add/sub/mul/div/safeDivide/percentOf/if/case/sum/avg/lookup…)
92
+ - 数值用 Decimal 字符串,单位编译期推断,错误结构化传播
93
+ - 发布前跑依赖图检查:引用存在、无环、可见范围
94
+
95
+ ### 4. 生成在线工具(详见 references/declarative-pages.md)
96
+ - 表单页:录入字段(按 field-catalog 自动生成)
97
+ - 指标卡页:公式结果 StatGrid
98
+ - 报告页:结构化输出 + 可导出
99
+ - 页面用声明式规格(ApplicationPageSpec),不手写重复模板
100
+
101
+ ### 5. 验收与发布
102
+ - validate:确定性校验引擎定义(引用闭合、无环、单位一致、测试通过)
103
+ - 上传识别走导入 Profile(Excel 映射 + OCR 草稿确认,自动导入先进草稿)
104
+ - **完成前门禁(final-gate)**:每次项目完成之前,审计/测试/运维三智能体协调接管检测——
105
+ - **审计智能体**:引擎定义确定性校验 0 findings、公式仅走受控 AST(禁 eval)、引用闭合
106
+ - **测试智能体**:基准样例(testSuites)全部通过,一个不过都不放行
107
+ - **运维智能体**:环境探测成功、依赖分级适配、安装/启动命令可用、热更新就绪
108
+ - 三智能体全部符合通过(gate passed)才标记完成;任一未通过返回 findings,修复后重新接管检测
109
+ - 发布为版本化引擎,任何公式/字段/阈值变化都创建新版本,不原地修改
110
+ - 输出:可运行的在线工具 + 引擎定义包 + 验收报告 + 完成前门禁结论
111
+
112
+ ## 多智能体蜂群模式(大脑协同,推荐复杂工具用)
113
+
114
+ 单智能体 `compile-inline` 适合简单工具;复杂工具(多字段 + 多公式 + 规则 + 导入 + 报告)用**蜂群协同**,多智能体并行生成、独立复核、确定性合并——准确率更高、产出更强。
115
+
116
+ ### 大脑模式(brain-handshake)
117
+ - `ide`:用当前 IDE 的多智能体(subagent)执行蜂群(默认)
118
+ - `hermes_local`:用用户本地安装的 Hermes 执行蜂群
119
+
120
+ ### 蜂群流程(协议 calctool.coordinator.run-plan/1.0)
121
+
122
+ 1. **brain-handshake**:协商大脑模式(ide/hermes_local),返回蜂群能力
123
+ 2. **brain-invoke**:传需求 → **自动拆解**为蜂群任务(按需派发,需要几个派几个):
124
+ - `fields` 字段目录(入口,总是有)
125
+ - `formulas` 公式图(有公式才派)· 独立复核
126
+ - `rules` 规则包(有规则才派)· 独立复核
127
+ - `imports` 导入映射(有导入才派)
128
+ - `reports` 报告模板(有报告才派)
129
+ - `pages` 页面规格(总是有)
130
+ 依赖图驱动:fields 先 → formulas/imports 并行 → reports → pages;maxParallel 控制并发
131
+ 3. **brain-invoke 批量派发**:每个就绪 work item 派给一个独立智能体,收集产物
132
+ 4. **brain-events / brain-status**:流式收集事件、查询蜂群状态(就绪/运行/阻塞)
133
+ 5. **brain-complete**:提交全部产物 → **确定性合并**为引擎定义 → 校验 → 发布
134
+ 6. **brain-cancel**:随时取消蜂群
135
+
136
+ ### 准确率与用户控制
137
+ - **准确率**:合并后引擎定义必须通过确定性校验(引用闭合/公式链/单位/Decimal/无环),0 findings 才发布
138
+ - **用户控制**:每一步产物可查看、修改、重试(budget.maxAttempts);最终工具可测算、改公式、调阈值
139
+ - **编程能力**:蜂群产物是真实可编译的引擎定义(JSON AST + Decimal 运行时),不是伪代码
140
+
141
+ ## 输出产物
142
+
143
+ ```
144
+ <engine-id>/
145
+ ├── manifest.yaml # 引擎身份与版本
146
+ ├── field-catalog.json # 字段目录
147
+ ├── formula-graph.json # 公式图(AST)
148
+ ├── rule-packs.json # 规则包
149
+ ├── import-profiles.json # 导入映射
150
+ ├── report-template.json # 报告模板
151
+ └── tests/ # 确定性测试(样例 → 期望结果)
152
+ ```
153
+
154
+ ## 参考文档
155
+
156
+ - `references/engine-meta-model.md` —— 引擎元模型(字段/公式/规则/导入/报告)
157
+ - `references/formula-dsl.md` —— 公式 DSL 与运行时(AST/Decimal/依赖图/错误值)
158
+ - `references/declarative-pages.md` —— 声明式页面规格系统(表单/指标/报告渲染)
159
+ - `references/import-ocr.md` —— 导入与 OCR(Excel 映射/图片识别/草稿确认)
160
+ - `references/finance-example.md` —— 经营健康诊断完整范例(50 字段 → 10 指标 → 报告)
161
+
162
+ ## 安全规则
163
+
164
+ - 不执行任意 JavaScript;公式只走受控 AST
165
+ - 上传文件先验证类型/大小/指纹,OCR 结果进草稿不覆盖正式数据
166
+ - 不虚构后端数据;未接入能力显示真实状态
167
+ - 财务/税务输出需明确"经营估算模型,生产使用前由专业人员复核"
@@ -0,0 +1,7 @@
1
+ {
2
+ "source": "KKyA6xljUX",
3
+ "slug": "calctool",
4
+ "version": "v0.3.0",
5
+ "endpoint": "https://cli.tax/api/public/skills/KKyA6xljUX",
6
+ "installedAt": "2026-08-17T00:00:00.000Z"
7
+ }
@@ -0,0 +1,74 @@
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 命令自动适配
@@ -0,0 +1,12 @@
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>
@@ -0,0 +1,26 @@
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
+ }
@@ -0,0 +1,233 @@
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
+ }
@@ -0,0 +1,103 @@
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()