@linkaicli/linkai-cli 1.0.18 → 1.0.20

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/AGENTS.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  ## 六条硬性规定(优先于本文其它内容)
7
7
 
8
- 1. **没有操作数据库的能力**:本 CLI 对用户开放,不能直连数据库、执行任意 SQL、管理数据源或改数仓。
8
+ 1. **没有操作数据库的能力**:本 CLI 对用户开放,不能直连数据库、执行任意 SQL、管理数据源或改数仓。问数(`linkai ask`)也只是把单条只读查询交给服务端,按用户的数据权限、在守卫和频率限制下执行。
9
9
  领谋 BI 数据集只能是单条只读查询,且只能查 `linkai ds-tables` 列出的租户视图;需要新视图或改数据时,告诉用户联系领谋数据团队。
10
10
  不要尝试绕过(命令已在本地拦截,也不要用 `api` 透传去调数据源 / SQL 类接口)。
11
11
  2. **方案搭建完成必须交付完整方案知识库**:必须呈现方案地址、版本、方案内容与解决的问题,并按每张业务卡片分别描述卡片内容、数据集、数据来源、粒度和取数逻辑/规则。
package/GUIDE.md CHANGED
@@ -93,6 +93,16 @@ linkai skills install workbuddy --mode rules
93
93
  AI 会先给你看搭建计划(不会直接动手),你确认后才执行;正式发布、删除等操作也会先问你。
94
94
  搭建完成后,AI 会完整呈现:**方案知识库版本、方案地址、方案内容与解决的问题,以及每张业务卡片对应的数据集、数据来源、粒度和取数逻辑/规则**。完整 SQL 不作为交付物展示。
95
95
 
96
+ ### 问数:直接问业务数据
97
+
98
+ 不搭看板、只想知道一个数时,直接问,例如:
99
+
100
+ - 「今天采购了多少、入库了多少?」
101
+ - 「本月各渠道的退货率是多少?」
102
+
103
+ AI 会先按当前租户的数据字典确认口径(有歧义会先问你,比如「入库」指物料入库还是成品入库),再查出数据,回答时说明用了哪些数据、时间范围和「数据截至」时间。
104
+ 查询按你在领谋里的数据权限执行,只能看到你本来就能看到的数据;有频率和数据量限制(明细最多 1000 行,没有导出权限时 50 行),需要大量明细请联系管理员导出。
105
+
96
106
  ## 升级
97
107
 
98
108
  **自动升级**:LinkAI-CLI 自动保持最新版本,不需要手动操作。发布了必须升级的版本时,下一条命令会先自动升级(约 10 秒)再执行;自动升级失败时命令会提示手动升级的方法。技能随之自动更新。
package/README.md CHANGED
@@ -1,82 +1,75 @@
1
1
  # LinkAI-CLI
2
2
 
3
- LinkMoreBI.CLI 的浏览器与站点自动化底层能力(命令 `linkai`),面向 AI 代理调用(Codex、Claude Code、DeepSeek 等)。
3
+ **领谋 BI 的 AI 命令行(`linkai`)**:让你常用的 AI 助手——WorkBuddy、Claude Code、Codex、CodeBuddy、DeepSeek 等——直接在领谋 BI 里**问数据、搭看板、导数据**。你只需要用中文说需求,AI 按领谋的规则和你的账号权限去做。
4
4
 
5
- - **站点适配器**:把网站 / 接口封装成结构化命令,统一 JSON 输出,写命令带回读。当前内置 **领谋 BI**(`linkai build`、`linkai status` 等)。
6
- - **浏览器桥接**:通过本机守护进程 + Chrome 扩展,在独立标签页里打开页面、读取状态、截图(`linkai browser …`)。
7
- - **自动搭建报表**:搭建报表知识库 + 数据字典知识库 + 声明式 spec,`linkai build` 一条命令完成
8
- 数据集 → 卡片 → 看板 → 筛选器 → 报表 → 验收,并输出完整、版本化的方案知识库(方案地址、方案说明,以及每张卡片的数据与取数规则;不输出完整 SQL)。
9
- - **不具备操作数据库的能力**:面向用户开放,不能直连数据库、执行任意 SQL 或管理数据源;数据集只能是只读查询、只能查空间选表清单里的视图。
5
+ ## 能做什么
10
6
 
11
- **用户安装与使用:见 [`GUIDE.md`](GUIDE.md)。** 本地构建与安装:见 [`DEVELOPMENT.md`](DEVELOPMENT.md)。AI 代理请从 [`AGENTS.md`](AGENTS.md) 开始。发布流程见 [`release/README.md`](release/README.md)。
7
+ - **问数**:直接问业务数据,例如「今天采购了多少、入库了多少」「本月各渠道退货率」。AI 先按当前租户的数据字典确认口径,再查出数据,回答时说明用了哪些数据、时间范围和数据截至时间;口径有歧义会先问你。
8
+ - **自动搭建看板 / 报表**:描述想看什么,AI 先给出搭建计划,你确认后自动完成数据集、卡片、看板、筛选器和报表,并逐项验收。完成后交付一份带版本的**方案知识库**:方案地址、方案内容、解决的问题,以及每张卡片的数据来源、粒度和取数规则。修改已有方案时,知识库同步升级版本。
9
+ - **数据导入与填报**:把 Excel 等数据按领谋的填报规则写入填报卡片,写前核对、写后回读。
10
+ - **复用已有成果**:建之前先查本租户里有没有同样的数据集、卡片,能复用就不重复建。
11
+ - **多会话协同**:一个方案由多个 AI 会话并行推进时自动分工,避免互相覆盖。
12
+ - **浏览器操作(可选)**:打开领谋页面截图验收、做网页操作,需要另装浏览器扩展。
13
+
14
+ ## 权限与安全
15
+
16
+ - **用你自己的领谋账号**:首次在领谋网页完成 OAuth 授权,之后自动续期;AI 不会索要你的密码。
17
+ - **只做你本来能做的事**:问数、取数按你在领谋里的数据权限执行,只能看到你本来就能看到的数据;租户之间严格隔离。
18
+ - **不能操作数据库**:不能直连数据库、执行任意 SQL、管理数据源或修改数仓。问数只是把单条只读查询交给领谋服务端,由服务端校验后执行,并有频率和数据量限制(明细最多 1000 行,没有导出权限时 50 行)。
19
+ - **关键操作先确认**:正式发布、删除、覆盖等操作,AI 会先征得你同意。
20
+ - **全程留痕**:每次操作都会记录到领谋的操作日志,便于追溯。
12
21
 
13
22
  ## 安装
14
23
 
15
- 需要 Node.js >= 20.18.1。
24
+ 需要 Node.js 20.18.1 或更高版本。
16
25
 
17
26
  ```bash
18
- npm install -g @linkaicli/linkai-cli # 或在本仓库:npm ci && npm run build && npm link
27
+ npm install -g @linkaicli/linkai-cli
19
28
  linkai --version
20
29
  ```
21
30
 
22
- 浏览器桥接(仅 `shot`、`browser` 类命令需要;领谋 BI 搭建是纯接口,不需要浏览器):
23
-
24
- 1. 打开 `chrome://extensions`,开启「开发者模式」。
25
- 2. 「加载已解压的扩展程序」,选择本仓库的 `extension/` 目录(或发布包中的扩展目录)。
26
- 3. `linkai doctor` 检查守护进程与扩展连接。
31
+ Mac 提示权限不足(`EACCES`)时,在命令前加 `sudo`。
27
32
 
28
- ## 领谋 BI 快速开始
33
+ ## 首次登录
29
34
 
30
35
  ```bash
31
- linkai login # 首次登录:在领谋网页完成 OAuth 授权;之后自动续期
32
- linkai status # 当前租户
33
-
34
- linkai template --output my-report # 复制报表模版
35
- linkai build --spec my-report/report.example.yaml # dry-run:执行计划 + 布局
36
- linkai build --spec my-report/report.example.yaml --execute # 新建、自动验收并生成方案知识库
37
- linkai plan-knowledge --plan <planId> --mode update --file plan-knowledge.yaml # 修改原方案后同步方案知识库
36
+ linkai login # 打开领谋网页完成授权,选择租户;之后自动续期
37
+ linkai status # 查看当前账号和租户
38
38
  ```
39
39
 
40
- 全部命令:`linkai --help`;知识库:`linkai kb`;规则:`linkai skills read linkai-report-builder -f plain`。
40
+ 切换租户:`linkai login --tenant <租户名>`。
41
41
 
42
- ## 在 AI 工具中使用
42
+ ## 接入 AI 工具
43
43
 
44
44
  ```bash
45
- linkai skills install codex # 安装到 Codex(~/.codex/skills),装完新开一个会话
46
- linkai skills install claude # 安装到 Claude Code(~/.claude/skills)
45
+ linkai skills install workbuddy # 或 claude、codex、codebuddy
47
46
  ```
48
47
 
49
- 升级 linkai 后,第一次运行任何命令会自动更新本机已装的技能(`--dir` 自定义目录除外)。没有技能机制的模型(如 DeepSeek API 接入),把 `AGENTS.md` 里的「通用系统提示」放进系统提示。
50
- 在 Codex 里首次运行 `linkai` 时,如果弹出联网或写入 `~/.linkai` 的授权请求,选择允许(可选「以后都允许 linkai 开头的命令」)。
48
+ 装完新开一个 AI 会话,直接说需求即可,例如:
51
49
 
52
- ## 目录
50
+ - 「今天采购数量有多少,入库了多少?」
51
+ - 「帮我搭一个销售看板:本期销售金额、件数、按渠道的日趋势、商品明细,先给我看计划」
52
+ - 「把刚才那个方案的方案知识库给我」
53
53
 
54
- | 路径 | 内容 |
55
- |---|---|
56
- | `src/` | CLI 核心:命令注册、执行、输出、守护进程、浏览器桥接 |
57
- | `extension/` | Chrome 扩展(浏览器桥接) |
58
- | `clis/linkai/` | 领谋 BI 适配器:`commands.js`、`core.js`、`build.js`、`templates.js` |
59
- | `knowledge/report-builder/` | 搭建报表知识库(SOP、build-spec、references) |
60
- | `knowledge/data-dictionary/` | 数据字典知识库(预留) |
61
- | `knowledge/browser/` | 浏览器能力说明 |
62
- | `templates/report-spec/` | 自动搭建报表模版 |
63
- | `skills/` | AI 技能:`linkai-report-builder`、`linkai-data-workflow`、`linkai-collab`(多会话协同)、`linkai-usage`、`linkai-browser` |
54
+ 没有技能机制的 AI 工具(如通过 API 接入的 DeepSeek),执行 `linkai skills prompt`,把输出放进它的系统提示。
64
55
 
65
- ## 本地数据
56
+ ## 升级
66
57
 
67
- `~/.linkai/`:`sites/`(站点凭据与会话,0600)、`artifacts/`(基线、截图、取数结果)、守护进程状态。
68
- 可用 `LINKAI_SITE_CONFIG_DIR`、`LINKAI_ARTIFACTS_DIR` 改位置。版本检查默认关闭(设置 `LINKAI_UPDATE_URL` 启用)。
58
+ 自动进行:有新版本时,LinkAI-CLI 会自动下载、校验并安装,AI 工具里的技能随之更新,不需要手动操作。
69
59
 
70
- ## 开发
60
+ 从 1.0.16 及更早版本(旧包名 `@linkmore/linkai-cli`)升级需要手动一次:
71
61
 
72
62
  ```bash
73
- npm ci
74
- npm run build # tsc + 打包 dist/bundle(发布用单文件)+ 生成 cli-manifest.json
75
- npm run typecheck
76
- npm test # unit + extension + adapter
63
+ linkai daemon stop
64
+ npm uninstall -g @linkmore/linkai-cli
65
+ npm install -g @linkaicli/linkai-cli
77
66
  ```
78
67
 
79
- 本地安装(开发链接 / 按用户方式安装)、扩展加载、模拟用户全流程与常见问题见 [`DEVELOPMENT.md`](DEVELOPMENT.md)。
68
+ ## 更多
69
+
70
+ - 完整的安装与使用说明:安装包内的 `GUIDE.md`
71
+ - AI 代理调用约定:安装包内的 `AGENTS.md`
72
+ - 浏览器扩展(截图验收、网页操作才需要):请联系领谋获取
80
73
 
81
74
  ## 许可
82
75
 
@@ -1,4 +1,4 @@
1
- @linkaicli/linkai-cli 1.0.18 安装包内含以下第三方软件(已打包进 dist/bundle)。
1
+ @linkaicli/linkai-cli 1.0.20 安装包内含以下第三方软件(已打包进 dist/bundle)。
2
2
 
3
3
  ── @colors/colors@1.5.0(MIT)
4
4
 
package/cli-manifest.json CHANGED
@@ -65,6 +65,57 @@
65
65
  "modulePath": "linkai/commands.js",
66
66
  "sourceFile": "linkai/commands.js"
67
67
  },
68
+ {
69
+ "site": "linkai",
70
+ "name": "ask",
71
+ "description": "问数:执行按数据字典口径写好的只读 SQL(1~3 条),返回数据、口径和数据截至时间;按当前用户的数据权限执行,有频率和行数限制",
72
+ "access": "read",
73
+ "strategy": "public",
74
+ "browser": false,
75
+ "args": [
76
+ {
77
+ "name": "question",
78
+ "type": "string",
79
+ "required": true,
80
+ "help": "用户的问题原文(原样传入,用于日志与统计)"
81
+ },
82
+ {
83
+ "name": "sql",
84
+ "type": "string",
85
+ "required": true,
86
+ "help": "SQL 文件路径,1~3 个,逗号分隔(一句话里有几个子问题就几条)"
87
+ },
88
+ {
89
+ "name": "keywords",
90
+ "type": "string",
91
+ "required": false,
92
+ "help": "从问题里提炼的关键词,逗号分隔,如「采购数量,入库数量,今天」"
93
+ },
94
+ {
95
+ "name": "template",
96
+ "type": "string",
97
+ "required": false,
98
+ "help": "使用的指标模板编号(如 DP-PUR-002),没用不填"
99
+ },
100
+ {
101
+ "name": "rows",
102
+ "type": "int",
103
+ "default": 30,
104
+ "required": false,
105
+ "help": "每条结果最多返回几行(1~200);更多的写到 --output 目录的文件里"
106
+ },
107
+ {
108
+ "name": "output",
109
+ "type": "string",
110
+ "required": false,
111
+ "help": "完整结果保存目录(默认 ~/.linkai/artifacts/linkai/ask)"
112
+ }
113
+ ],
114
+ "defaultFormat": "json",
115
+ "type": "js",
116
+ "modulePath": "linkai/commands.js",
117
+ "sourceFile": "linkai/commands.js"
118
+ },
68
119
  {
69
120
  "site": "linkai",
70
121
  "name": "backup",
@@ -1 +1 @@
1
- import{ArgumentError}from"@linkaicli/linkai-cli/errors";function clamp(value,min,max){return Math.max(min,Math.min(value,max))}function clampInt(raw,fallback,min,max){const parsed=Number(raw);return Number.isFinite(parsed)?clamp(Math.floor(parsed),min,max):fallback}function normalizeNumericId(value,label,example){const normalized=String(value??"").trim();if(!/^\d+$/.test(normalized))throw new ArgumentError(`${label} must be a numeric ID`,`Pass a numeric ${label}, for example: ${example}`);return normalized}function requireNonEmptyQuery(value,label="query"){const normalized=String(value??"").trim();if(!normalized)throw new ArgumentError(`${label} cannot be empty`);return normalized}export{clamp,clampInt,normalizeNumericId,requireNonEmptyQuery};
1
+ var u=Object.defineProperty;var e=(t,r)=>u(t,"name",{value:r,configurable:!0});import{ArgumentError as m}from"@linkaicli/linkai-cli/errors";function a(t,r,n){return Math.max(r,Math.min(t,n))}e(a,"clamp");function f(t,r,n,o){const i=Number(t);return Number.isFinite(i)?a(Math.floor(i),n,o):r}e(f,"clampInt");function s(t,r,n){const o=String(t??"").trim();if(!/^\d+$/.test(o))throw new m(`${r} must be a numeric ID`,`Pass a numeric ${r}, for example: ${n}`);return o}e(s,"normalizeNumericId");function x(t,r="query"){const n=String(t??"").trim();if(!n)throw new m(`${r} cannot be empty`);return n}e(x,"requireNonEmptyQuery");export{a as clamp,f as clampInt,s as normalizeNumericId,x as requireNonEmptyQuery};
@@ -1 +1 @@
1
- import{AuthRequiredError,CommandExecutionError,TimeoutError,getErrorMessage}from"@linkaicli/linkai-cli/errors";import{cli,Strategy}from"@linkaicli/linkai-cli/registry";const DEFAULT_TIMEOUT_SECONDS=300,POLL_INTERVAL_MS=2e3;function normalizeIdentity(site,identity){if(!identity||typeof identity!="object"||Array.isArray(identity)||Object.keys(identity).length===0||"logged_in"in identity&&typeof identity.logged_in!="boolean")throw new CommandExecutionError(`${site} identity probe returned an invalid account result`);if(identity.logged_in===!1)throw new AuthRequiredError(site,"Identity probe reports a logged-out session");return{...identity,logged_in:!0,site}}function isAuthRequired(error){return error instanceof AuthRequiredError}async function tryProbe(config,page,phase){const probe=phase==="poll"&&config.poll?config.poll:config.verify;return normalizeIdentity(config.site,await probe(page,{phase}))}function authHint(config){return`Run \`linkai ${config.site} login\` to open the login page, then retry.`}function commandColumns(config){return["logged_in","site",...config.columns??["id","username","name"]]}function normalizeQuickCheck(result){return typeof result=="boolean"?{logged_in:result}:result&&typeof result=="object"&&!Array.isArray(result)?{...result,logged_in:typeof result.logged_in=="boolean"?result.logged_in:null}:{logged_in:null}}function normalizeRefreshResult(result){return result&&typeof result=="object"&&!Array.isArray(result)?result:{touched:!0}}function registerSiteAuthCommands(config){if(!config?.site||!config?.domain||!config?.loginUrl||typeof config.verify!="function")throw new Error("registerSiteAuthCommands requires site, domain, loginUrl, and verify(page)");config.registerWhoami!==!1&&cli({site:config.site,name:"whoami",access:"read",description:config.whoamiDescription??`Show the current logged-in ${config.site} account`,domain:config.domain,strategy:Strategy.COOKIE,browser:!0,navigateBefore:!1,siteSession:"persistent",args:[],columns:commandColumns(config),authStatus:{...typeof config.quickCheck=="function"?{quickCheck:async page=>normalizeQuickCheck(await config.quickCheck(page))}:{},...typeof config.refresh=="function"?{refresh:async(page,kwargs)=>normalizeRefreshResult(await config.refresh(page,kwargs))}:{}},func:async page=>tryProbe(config,page,"identity")}),cli({site:config.site,name:"login",access:"write",description:config.loginDescription??`Open ${config.site} login and wait until the browser session is authenticated`,domain:config.domain,strategy:Strategy.COOKIE,browser:!0,navigateBefore:!1,defaultWindowMode:"foreground",siteSession:"persistent",args:[{name:"timeout",type:"int",default:DEFAULT_TIMEOUT_SECONDS,help:"Maximum seconds to wait for the user to finish login"}],columns:["status",...commandColumns(config)],func:async(page,kwargs)=>{try{return{status:"already_logged_in",...await tryProbe(config,page,"identity")}}catch(error){if(!isAuthRequired(error))throw error}await page.goto(config.loginUrl);const timeoutSeconds=Number(kwargs.timeout??DEFAULT_TIMEOUT_SECONDS),deadline=Date.now()+timeoutSeconds*1e3;let lastAuthMessage="";for(;Date.now()<deadline;){await page.wait(Math.min(POLL_INTERVAL_MS/1e3,Math.max(.2,(deadline-Date.now())/1e3)));try{return{status:"login_complete",...await tryProbe(config,page,"poll")}}catch(error){if(!isAuthRequired(error))throw error;lastAuthMessage=getErrorMessage(error)}}throw new TimeoutError(`${config.site} login`,timeoutSeconds,lastAuthMessage?`${authHint(config)} Last auth check: ${lastAuthMessage}`:authHint(config))}})}export{registerSiteAuthCommands};
1
+ var p=Object.defineProperty;var r=(e,t)=>p(e,"name",{value:t,configurable:!0});import{AuthRequiredError as l,CommandExecutionError as f,TimeoutError as g,getErrorMessage as _}from"@linkaicli/linkai-cli/errors";import{cli as d,Strategy as m}from"@linkaicli/linkai-cli/registry";const h=300,b=2e3;function A(e,t){if(!t||typeof t!="object"||Array.isArray(t)||Object.keys(t).length===0||"logged_in"in t&&typeof t.logged_in!="boolean")throw new f(`${e} identity probe returned an invalid account result`);if(t.logged_in===!1)throw new l(e,"Identity probe reports a logged-out session");return{...t,logged_in:!0,site:e}}r(A,"normalizeIdentity");function c(e){return e instanceof l}r(c,"isAuthRequired");async function s(e,t,o){const i=o==="poll"&&e.poll?e.poll:e.verify;return A(e.site,await i(t,{phase:o}))}r(s,"tryProbe");function y(e){return`Run \`linkai ${e.site} login\` to open the login page, then retry.`}r(y,"authHint");function w(e){return["logged_in","site",...e.columns??["id","username","name"]]}r(w,"commandColumns");function C(e){return typeof e=="boolean"?{logged_in:e}:e&&typeof e=="object"&&!Array.isArray(e)?{...e,logged_in:typeof e.logged_in=="boolean"?e.logged_in:null}:{logged_in:null}}r(C,"normalizeQuickCheck");function E(e){return e&&typeof e=="object"&&!Array.isArray(e)?e:{touched:!0}}r(E,"normalizeRefreshResult");function M(e){if(!e?.site||!e?.domain||!e?.loginUrl||typeof e.verify!="function")throw new Error("registerSiteAuthCommands requires site, domain, loginUrl, and verify(page)");e.registerWhoami!==!1&&d({site:e.site,name:"whoami",access:"read",description:e.whoamiDescription??`Show the current logged-in ${e.site} account`,domain:e.domain,strategy:m.COOKIE,browser:!0,navigateBefore:!1,siteSession:"persistent",args:[],columns:w(e),authStatus:{...typeof e.quickCheck=="function"?{quickCheck:r(async t=>C(await e.quickCheck(t)),"quickCheck")}:{},...typeof e.refresh=="function"?{refresh:r(async(t,o)=>E(await e.refresh(t,o)),"refresh")}:{}},func:r(async t=>s(e,t,"identity"),"func")}),d({site:e.site,name:"login",access:"write",description:e.loginDescription??`Open ${e.site} login and wait until the browser session is authenticated`,domain:e.domain,strategy:m.COOKIE,browser:!0,navigateBefore:!1,defaultWindowMode:"foreground",siteSession:"persistent",args:[{name:"timeout",type:"int",default:h,help:"Maximum seconds to wait for the user to finish login"}],columns:["status",...w(e)],func:r(async(t,o)=>{try{return{status:"already_logged_in",...await s(e,t,"identity")}}catch(n){if(!c(n))throw n}await t.goto(e.loginUrl);const i=Number(o.timeout??h),u=Date.now()+i*1e3;let a="";for(;Date.now()<u;){await t.wait(Math.min(b/1e3,Math.max(.2,(u-Date.now())/1e3)));try{return{status:"login_complete",...await s(e,t,"poll")}}catch(n){if(!c(n))throw n;a=_(n)}}throw new g(`${e.site} login`,i,a?`${y(e)} Last auth check: ${a}`:y(e))},"func")})}r(M,"registerSiteAuthCommands");export{M as registerSiteAuthCommands};
@@ -1 +1 @@
1
- import fs from"node:fs";import path from"node:path";import{parseYaml}from"@linkaicli/linkai-cli/utils";import*as core from"./core.js";import*as oplog from"@linkaicli/linkai-cli/oplog";import*as reuse from"./reuse.js";import{containsFullSql,knowledgeAuthoringFromBuild,publishPlanKnowledge}from"./deliverable.js";const CARD_TYPES=new Set(["chart","meter","table","gantt","existing"]),TRANSPARENT_TYPES=new Set(["meter"]),DEFAULT_LAYOUT={contentX:2,contentWidth:20,pageBackgroundColor:"#F4F3EE",titleHeight:2},list=v=>(Array.isArray(v)?v:String(v??"").split(",")).map(s=>String(s).trim()).filter(Boolean);function currentTenantLabel(){try{const s=core.readSession();if(s){const t=core.spaceOf(s);return t.tenantName||t.tenantId}}catch{}return"(未登录:执行前请用户执行 linkai login 并在浏览器完成领谋授权)"}function loadSpec(file){const abs=path.resolve(String(file)),text=fs.readFileSync(abs,"utf8"),spec=/\.json$/i.test(abs)?JSON.parse(text):parseYaml(text),baseDir=path.dirname(abs);for(const c of spec?.cards||[])(c.type==="table"||c.type==="gantt")&&typeof c.spec=="string"&&(c.specFile=c.spec,c.spec=JSON.parse(fs.readFileSync(path.resolve(baseDir,c.spec),"utf8")));return{spec,specFile:abs,baseDir}}function validateSpec(spec){const errors=[],need=(cond,msg)=>{cond||errors.push(msg)};if(need(spec&&typeof spec=="object","spec 必须是对象"),errors.length)return errors;need(spec.version===1,"version 必须为 1"),need(spec.operation,"operation(create 或 update)必填;用户未说明时先主动询问"),need(spec.operation==="create","linkai build 当前用于新建方案,operation 必须为 create;修改原方案请定位 planId 后按知识库规则更新并执行 linkai plan-knowledge --mode update"),need(typeof spec.name=="string"&&spec.name.trim(),"name(方案名)必填"),need(spec.knowledge?.summary,"knowledge.summary(方案内容)必填"),need(spec.knowledge?.problem,"knowledge.problem(解决的问题)必填"),need(Array.isArray(spec.cards)&&spec.cards.length>0,"cards 至少需要一张业务卡片"),need(spec.reuse===void 0||reuse.REUSE_MODES.has(spec.reuse),`reuse 只能是 ${[...reuse.REUSE_MODES].join(" | ")}(默认 suggest)`);const dsKeys=new Set;for(const[i,d]of(spec.datasets||[]).entries())need(d.key,`datasets[${i}].key 必填`),need(!dsKeys.has(d.key),`datasets key 重复:${d.key}`),dsKeys.add(d.key),need(d.id||d.name&&d.sql,`datasets[${d.key}] 需要 id(复用已有数据集)或 name + sql`);const cardKeys=new Set;for(const[i,c]of(spec.cards||[]).entries()){if(need(c.key,`cards[${i}].key 必填`),need(!cardKeys.has(c.key),`cards key 重复:${c.key}`),cardKeys.add(c.key),need(CARD_TYPES.has(c.type),`cards[${c.key}].type 必须是 ${[...CARD_TYPES].join(" | ")}`),c.type==="existing"){need(c.id,`cards[${c.key}] type=existing 需要 id`),need(c.knowledge?.content,`cards[${c.key}].knowledge.content(卡片内容)必填`),need(c.knowledge?.source,`cards[${c.key}].knowledge.source(数据来源)必填`),need(c.knowledge?.grain,`cards[${c.key}].knowledge.grain(数据粒度)必填`),need(c.knowledge?.dataLogic,`cards[${c.key}].knowledge.dataLogic(取数逻辑)必填`);continue}need(dsKeys.has(c.dataset),`cards[${c.key}].dataset 引用了不存在的数据集 key:${c.dataset}`),c.type==="chart"&&(need(c.name,`cards[${c.key}].name 必填`),need(c.kind,`cards[${c.key}].kind 必填(如 column/stacked,见 linkai kinds)`),need(list(c.meas).length,`cards[${c.key}].meas 至少一个数值列`)),c.type==="meter"&&(need(c.name,`cards[${c.key}].name 必填`),need(c.col,`cards[${c.key}].col 必填`)),(c.type==="table"||c.type==="gantt")&&(need(c.spec&&typeof c.spec=="object",`cards[${c.key}].spec 必填(内联对象或 .json 路径)`),need(c.spec?.name,`cards[${c.key}].spec.name 必填`)),need(c.knowledge?.content,`cards[${c.key}].knowledge.content(卡片内容)必填`),need(c.knowledge?.source,`cards[${c.key}].knowledge.source(数据来源)必填`),need(c.knowledge?.grain,`cards[${c.key}].knowledge.grain(数据粒度)必填`),need(c.knowledge?.dataLogic,`cards[${c.key}].knowledge.dataLogic(取数逻辑)必填`)}const used=new Set;for(const[si,s]of(spec.sections||[]).entries())for(const[ri,r]of(s.rows||[]).entries()){need(Number(r.height)>0,`sections[${si}].rows[${ri}].height 必须为正整数(格)`);for(const p of r.cards||[])need(cardKeys.has(p.card),`sections[${si}].rows[${ri}] 引用了不存在的卡片 key:${p.card}`),need(!used.has(p.card),`卡片 ${p.card} 在布局中出现多次`),used.add(p.card)}for(const r of spec.publish?.reports||[])need(cardKeys.has(r.card),`publish.reports 引用了不存在的卡片 key:${r.card}`);return containsFullSql({knowledge:spec.knowledge,cards:(spec.cards||[]).map(c=>c.knowledge)})&&errors.push("方案知识库不得包含完整 SQL;cards[].knowledge.dataLogic 必须使用自然语言描述"),errors}function formalNames(spec){return[spec.name,...(spec.datasets||[]).filter(d=>!d.id).map(d=>d.name),...(spec.cards||[]).filter(c=>c.type!=="existing").map(c=>c.name||c.spec?.name),...(spec.publish?.reports||[]).map(r=>r.name)].filter(n=>n&&!String(n).startsWith("zz-"))}function planLayout(spec){const L={...DEFAULT_LAYOUT,...spec.layout||{}},right=L.contentX+L.contentWidth,blocks=[],errors=[];let y=0,mobileY=0;const push=b=>{blocks.push({...b,mobileX:0,mobileY,mobileWidth:4,mobileHeight:b.height}),mobileY+=b.height};for(const[si,s]of(spec.sections||[]).entries()){s.title&&(push({kind:"title",key:`title-${si}`,title:s.title,x:L.contentX,y,width:L.contentWidth,height:L.titleHeight}),y+=L.titleHeight);for(const[ri,r]of(s.rows||[]).entries()){const cards=r.cards||[],fixed=cards.reduce((n,p)=>n+(Number(p.width)||0),0),auto=cards.filter(p=>!p.width).length,autoW=auto?Math.floor((L.contentWidth-fixed)/auto):0;let x=L.contentX;for(const p of cards){const width=Number(p.width)||autoW;width<=0&&errors.push(`sections[${si}].rows[${ri}] 卡片 ${p.card} 宽度为 0(行内固定宽度已占满)`),push({kind:"card",key:p.card,x,y,width,height:Number(r.height)}),x+=width}x>right&&errors.push(`sections[${si}].rows[${ri}] 总宽 ${x-L.contentX} 超出内容区 ${L.contentWidth} 格`),y+=Number(r.height)}}return{layout:L,blocks,errors:[...errors,...overlaps(blocks,"pc"),...overlaps(blocks,"mobile")]}}function overlaps(blocks,mode){const rect=b=>mode==="pc"?[b.x,b.y,b.width,b.height]:[b.mobileX,b.mobileY,b.mobileWidth,b.mobileHeight],out=[];for(let i=0;i<blocks.length;i++)for(let j=i+1;j<blocks.length;j++){const[ax,ay,aw,ah]=rect(blocks[i]),[bx,by,bw,bh]=rect(blocks[j]);ax<bx+bw&&bx<ax+aw&&ay<by+bh&&by<ay+ah&&out.push(`${mode} 布局重叠:${blocks[i].key} 与 ${blocks[j].key}`)}return out}function titleContent(t){return JSON.stringify({type:"title-template",version:1,template:"section",config:{serial:String(t.serial??""),title:String(t.title??""),subtitle:String(t.subtitle??""),description:"",showDecorLine:!1}})}function describePlan(spec){const steps=[];for(const d of spec.datasets||[])steps.push(d.id?`复用数据集 ${d.key} = ${d.id}`:`建数据集 ${d.key}「${d.name}」← ${d.sql}`);for(const c of spec.cards||[]){if(c.type==="existing"){steps.push(`复用卡片 ${c.key} = ${c.id}`);continue}steps.push(`建卡片 ${c.key}(${c.type}${c.kind?` ${c.kind}`:""})「${c.name||c.spec?.name||""}」${c.sort?`,排序 ${c.sort}`:""}`)}steps.push(`建方案「${spec.name}」并按布局放入 ${(spec.sections||[]).length} 个分区`),steps.push("写页面样式(暖色背景、标题/指标卡卡位透明、隐藏外层标题)"),spec.filters&&steps.push(`写方案筛选器模板 ← ${spec.filters}`);for(const r of spec.publish?.reports||[])steps.push(`发布报表「${r.name}」← 卡片 ${r.card}`);return steps.push("验收:逐卡 query + 方案回读(卡片数、越界、重叠、背景色)"),steps.push("生成并保存版本化方案知识库(完整呈现给用户,不含 SQL)"),steps}function fillPlaceholders(text,state,dataSourceId=""){const pick=(map,kind)=>(_,k)=>{if(!map[k])throw new Error(`filters 引用了不存在或未创建的${kind} key:${k}`);return map[k]};return text.replaceAll("{{plan}}",state.planId).replaceAll("{{dataSource}}",dataSourceId).replace(/\{\{pageCard:([^}]+)\}\}/g,pick(state.blocks,"卡片")).replace(/\{\{dataset:([^}]+)\}\}/g,pick(state.datasets,"数据集"))}function readState(file){try{return JSON.parse(fs.readFileSync(file,"utf8"))}catch{return{datasets:{},cards:{},sorted:{},blocks:{},reports:{}}}}function writeState(file,state){fs.writeFileSync(file,JSON.stringify(state,null,2))}const resolveFile=(baseDir,p)=>path.resolve(baseDir,String(p));async function reviewReuse(spec,baseDir){try{return await reuse.reviewSpec(spec,file=>fs.readFileSync(resolveFile(baseDir,file),"utf8"))}catch(e){return{mode:spec.reuse??"suggest",skipped:core.LoginRequiredError&&e instanceof core.LoginRequiredError?"未登录,无法查重":`查重失败:${String(e.message).slice(0,200)}`,datasets:{},cards:{}}}}function reuseHint(review){const ds=Object.entries(review.datasets||{}).filter(([,v])=>v.identical.length||v.similar.length),cards=Object.entries(review.cards||{});return!ds.length&&!cards.length?null:`发现可复用对象:数据集 ${ds.map(([k])=>k).join("、")||"无"},卡片 ${cards.map(([k])=>k).join("、")||"无"}。`+(review.mode==="auto"?"完全一样的将直接复用;相似的请比较后决定。":"完全一样的可在 spec 加 reuse: auto 直接复用;相似的请比较 columns 后决定复用、在原数据集上调整,还是新建。")}async function runBuild({spec,specFile,baseDir},{execute=!1,confirm=!1,style=!0,beforeWrite}={}){const errors=validateSpec(spec),plan=errors.length?{blocks:[],errors:[]}:planLayout(spec);errors.push(...plan.errors);const formal=errors.length?[]:formalNames(spec);if(errors.length)return oplog.emit("build.stage",{stage:"validate",ok:!1,errors:errors.length}),{ok:!1,stage:"validate",errors};const summary={name:spec.name,tenant:currentTenantLabel(),steps:describePlan(spec),layout:plan.blocks.map(b=>({key:b.key,x:b.x,y:b.y,w:b.width,h:b.height}))};if(!execute){const review2=await reviewReuse(spec,baseDir),hint=reuseHint(review2);return{ok:!0,mode:"dry-run",...summary,formalNames:formal,reuse:review2,next:[hint,formal.length?"含正式名:取得用户授权后加 --execute --confirm":"确认无误后加 --execute"].filter(Boolean).join(" ")}}if(formal.length&&!confirm)return{ok:!1,stage:"authorize",errors:[`以下为正式名(非 zz- 前缀),按 SOP 需要用户明确授权后加 --confirm:${formal.join("、")}`]};beforeWrite&&await beforeWrite();const review=spec.reuse==="auto"?await reviewReuse(spec,baseDir):{mode:spec.reuse??"suggest",datasets:{},cards:{}},stateFile=`${specFile}.state.json`,state=readState(stateFile),save=()=>writeState(stateFile,state),log=[];let stepAt=Date.now();log.push=(...lines)=>{for(const line of lines){const[stage,rest=""]=String(line).split(/\s+(.*)/s);oplog.emit("build.stage",{stage,detail:rest.slice(0,200),ok:!0,ms:Date.now()-stepAt}),stepAt=Date.now()}return Array.prototype.push.apply(log,lines)},oplog.emit("build.stage",{stage:"start",ok:!0,resumed:!!(state.planId||Object.keys(state.datasets||{}).length),datasets:(spec.datasets||[]).length,cards:(spec.cards||[]).length,reports:(spec.publish?.reports||[]).length});for(const d of spec.datasets||[]){if(state.datasets[d.key])continue;const reused=!d.id&&review.datasets[d.key]?.reuseId;state.datasets[d.key]=d.id||reused||await core.dsSave({name:d.name,sql:fs.readFileSync(resolveFile(baseDir,d.sql),"utf8")}),save(),log.push(`dataset ${d.key} → ${state.datasets[d.key]}${reused?"(复用已有数据集)":""}`)}for(const c of spec.cards||[]){if(!state.cards[c.key]){const dsId=state.datasets[c.dataset];let r;c.type==="existing"?r={cardId:c.id}:review.cards[c.key]?.reuseId?r={cardId:review.cards[c.key].reuseId,reused:!0}:c.type==="chart"?r=await core.cardChart({dsId,name:c.name,kind:c.kind,dim:list(c.dim).join(","),meas:list(c.meas).join(","),series:c.series}):c.type==="meter"?r=await core.cardMeter({dsId,name:c.name,col:c.col,subtitle:c.subtitle,color:c.color||void 0}):c.type==="table"?r=await core.cardTable({dsId,spec:c.spec}):r=await core.cardGantt({dsId,spec:c.spec}),state.cards[c.key]=r.cardId,save(),log.push(`card ${c.key} → ${r.cardId}${r.reused?"(复用已有卡片)":""}`)}const sharedCard=review.cards[c.key]?.reuseId&&state.cards[c.key]===review.cards[c.key].reuseId;if(c.sort&&!state.sorted[c.key]&&!sharedCard){const r=await core.cardSort(state.cards[c.key],String(c.sort));state.sorted[c.key]=r.sqlHasOrderBy,save(),log.push(`sort ${c.key} → sqlHasOrderBy=${r.sqlHasOrderBy}`)}}state.planId||(state.planId=await core.post("/page-setting/new-create-empty-plan",{name:spec.name,isPrivate:!1,pageType:0}),save(),log.push(`plan → ${state.planId}`));for(const b of plan.blocks){if(state.blocks[b.key])continue;const pos={x:b.x,y:b.y,width:b.width,height:b.height,mobileX:b.mobileX,mobileY:b.mobileY,mobileWidth:b.mobileWidth,mobileHeight:b.mobileHeight},body=b.kind==="title"?{...pos,sourceType:0,cardType:5,contentText:titleContent(b.title),bindFilterIds:[],userPageSettingId:state.planId}:{...pos,indexBlockId:state.cards[b.key],sourceType:0,cardType:1,bindFilterIds:[],userPageSettingId:state.planId};state.blocks[b.key]=(await core.post("/page-setting/new-page-card",body)).id,save()}if(log.push(`placed ${plan.blocks.length} blocks`),style&&!state.styled&&(state.styled=await applyStyle(spec,plan,state),save(),log.push(`style → ${JSON.stringify(state.styled)}`)),spec.filters&&!state.filtered){const text=fillPlaceholders(fs.readFileSync(resolveFile(baseDir,spec.filters),"utf8"),state,(await core.ctx()).dataSourceId),body=JSON.parse(text);state.filterDatasetIds=[...new Set((body.filters||[]).map(f=>f.indexBlockDataId).filter(id=>id&&id!==core.Z))],state.filtered=await core.planFilter(state.planId,body),save(),log.push(`filters → ${state.filtered.filters?.length??0}`)}for(const r of spec.publish?.reports||[])state.reports[r.card]||(await core.post("/index-block/operate-report",{indexBlockId:state.cards[r.card],name:r.name,tag:"",isPrivate:!1}),state.reports[r.card]=r.name,save(),log.push(`report ${r.name}`));const reusedCards=new Set(Object.entries(review.cards||{}).filter(([k,v])=>v.reuseId&&state.cards[k]===v.reuseId).map(([k])=>k)),verify=await verifyBuild(spec,plan,state,reusedCards);return oplog.emit("build.stage",{stage:"verify",ok:!!verify.ok,checks:Array.isArray(verify.checks)?verify.checks.length:void 0,failed:Array.isArray(verify.checks)?verify.checks.filter(c=>c&&c.ok===!1).length:void 0}),verify.ok?{ok:!0,mode:"execute",...await publishPlanKnowledge({planId:state.planId,operation:"create",authoring:knowledgeAuthoringFromBuild(spec,state)}),verify,log,stateFile,...review.mode==="auto"?{reuse:review}:{}}:{ok:!1,mode:"execute",verify,log,stateFile}}async function applyStyle(spec,plan,state){const detail=await core.planDetail(state.planId,!0),cards=detail.pageCards||[],expected=new Set(Object.values(state.blocks)),got=new Set(cards.map(c=>c.id));if(![...expected].every(id=>got.has(id)))return{skipped:"方案回读缺卡,未写样式(避免 update-page-card 移除卡片)"};if(!detail.name)return{skipped:"方案详情缺 name 字段,未写样式(避免覆盖方案配置)"};const keyOf=Object.fromEntries(Object.entries(state.blocks).map(([k,id])=>[id,k])),typeOf=Object.fromEntries((spec.cards||[]).map(c=>[c.key,c.type])),hasTitles=plan.blocks.some(b=>b.kind==="title"),parse=s=>{try{return typeof s=="string"?JSON.parse(s||"{}"):s||{}}catch{return{}}},items=cards.map(c=>{const key=keyOf[c.id],ds=parse(c.dataScreenSetting);if(key){const isTitle=key.startsWith("title-"),transparent=isTitle||TRANSPARENT_TYPES.has(typeOf[key]);Object.assign(ds,{useCardConfig:!0},transparent?{backgroundColor:"transparent"}:{},isTitle||hasTitles?{cardTitle:{...ds.cardTitle||{},show:!1}}:{})}return{id:c.id,x:c.x,y:c.y,width:c.width,height:c.height,mobileX:c.mobileX,mobileY:c.mobileY,mobileWidth:c.mobileWidth,mobileHeight:c.mobileHeight,zIndex:c.zIndex??0,dataScreenSetting:JSON.stringify(ds),...c.dataScreenChartSetting!==void 0?{dataScreenChartSetting:c.dataScreenChartSetting}:{}}}),page={...parse(detail.dataScreenSetting),pageBackgroundColor:plan.layout.pageBackgroundColor};return await core.post("/page-setting/update-page-card",{list:items,userPageSettingId:state.planId,dataScreenSetting:JSON.stringify(page),name:detail.name,tag:detail.tag||"",remark:detail.remark||"",parentId:detail.parentId,isPrivate:!!detail.isPrivate}),{cards:items.length,pageBackgroundColor:page.pageBackgroundColor}}async function verifyBuild(spec,plan,state,reusedCards=new Set){const checks=[];for(const c of spec.cards||[]){const q=await core.queryCard({cardId:state.cards[c.key],pageSize:1});checks.push({check:`query ${c.key}`,ok:q.code===200,detail:q.code===200?`total=${q.total}`:q.message})}for(const c of(spec.cards||[]).filter(x=>x.type==="table"&&x.spec?.fitTableWidth!==!1)){const d=await core.cardDetail(state.cards[c.key]),fit=(typeof d.chartSetting=="string"?(()=>{try{return JSON.parse(d.chartSetting||"{}")}catch{return{}}})():d.chartSetting||{}).config?.fitTableWidth===!0,shared=reusedCards.has(c.key);checks.push({check:`AG 自适应宽度 ${c.key}`,ok:fit||shared,detail:fit?"":shared?"复用的共享表格未开启,未修改":"fitTableWidth 未开启"})}const detail=await core.planDetail(state.planId,!0),cards=detail.pageCards||[],ids=new Set(cards.map(c=>c.id));checks.push({check:"方案卡片齐全",ok:Object.values(state.blocks).every(id=>ids.has(id)),detail:`${cards.length} 张`});const L=plan.layout,outOfBounds=cards.filter(c=>c.x<L.contentX||c.x+c.width>L.contentX+L.contentWidth);checks.push({check:`PC 卡位在 [${L.contentX},${L.contentX+L.contentWidth}] 内`,ok:!outOfBounds.length,detail:outOfBounds.map(c=>c.id).join(",")});const rects=cards.map(c=>({key:c.id,x:c.x,y:c.y,width:c.width,height:c.height,mobileX:c.mobileX,mobileY:c.mobileY,mobileWidth:c.mobileWidth,mobileHeight:c.mobileHeight})),ov=[...overlaps(rects,"pc"),...overlaps(rects,"mobile")];if(checks.push({check:"PC/手机无重叠",ok:!ov.length,detail:ov.join("; ")}),state.styled&&!state.styled.skipped){let page={};try{page=JSON.parse(detail.dataScreenSetting||"{}")}catch{}checks.push({check:"页面背景色",ok:page.pageBackgroundColor===L.pageBackgroundColor,detail:page.pageBackgroundColor||"(未设置)"})}for(const[k,v]of Object.entries(state.sorted))checks.push({check:`排序生效 ${k}`,ok:v===!0,detail:""});return{ok:checks.every(c=>c.ok),checks}}export{describePlan,fillPlaceholders,formalNames,loadSpec,planLayout,runBuild,titleContent,validateSpec,verifyBuild};
1
+ var F=Object.defineProperty;var g=(t,s)=>F(t,"name",{value:s,configurable:!0});import O from"node:fs";import j from"node:path";import{parseYaml as E}from"@linkaicli/linkai-cli/utils";import*as f from"./core.js";import*as v from"@linkaicli/linkai-cli/oplog";import*as B from"./reuse.js";import{containsFullSql as X,knowledgeAuthoringFromBuild as L,publishPlanKnowledge as P}from"./deliverable.js";const W=new Set(["chart","meter","table","gantt","existing"]),q=new Set(["meter"]),H={contentX:2,contentWidth:20,pageBackgroundColor:"#F4F3EE",titleHeight:2},N=g(t=>(Array.isArray(t)?t:String(t??"").split(",")).map(s=>String(s).trim()).filter(Boolean),"list");function J(){try{const t=f.readSession();if(t){const s=f.spaceOf(t);return s.tenantName||s.tenantId}}catch{}return"(未登录:执行前请用户执行 linkai login 并在浏览器完成领谋授权)"}g(J,"currentTenantLabel");function se(t){const s=j.resolve(String(t)),e=O.readFileSync(s,"utf8"),d=/\.json$/i.test(s)?JSON.parse(e):E(e),i=j.dirname(s);for(const l of d?.cards||[])(l.type==="table"||l.type==="gantt")&&typeof l.spec=="string"&&(l.specFile=l.spec,l.spec=JSON.parse(O.readFileSync(j.resolve(i,l.spec),"utf8")));return{spec:d,specFile:s,baseDir:i}}g(se,"loadSpec");function Y(t){const s=[],e=g((c,n)=>{c||s.push(n)},"need");if(e(t&&typeof t=="object","spec 必须是对象"),s.length)return s;e(t.version===1,"version 必须为 1"),e(t.operation,"operation(create 或 update)必填;用户未说明时先主动询问"),e(t.operation==="create","linkai build 当前用于新建方案,operation 必须为 create;修改原方案请定位 planId 后按知识库规则更新并执行 linkai plan-knowledge --mode update"),e(typeof t.name=="string"&&t.name.trim(),"name(方案名)必填"),e(t.knowledge?.summary,"knowledge.summary(方案内容)必填"),e(t.knowledge?.problem,"knowledge.problem(解决的问题)必填"),e(Array.isArray(t.cards)&&t.cards.length>0,"cards 至少需要一张业务卡片"),e(t.reuse===void 0||B.REUSE_MODES.has(t.reuse),`reuse 只能是 ${[...B.REUSE_MODES].join(" | ")}(默认 suggest)`);const d=new Set;for(const[c,n]of(t.datasets||[]).entries())e(n.key,`datasets[${c}].key 必填`),e(!d.has(n.key),`datasets key 重复:${n.key}`),d.add(n.key),e(n.id||n.name&&n.sql,`datasets[${n.key}] 需要 id(复用已有数据集)或 name + sql`);const i=new Set;for(const[c,n]of(t.cards||[]).entries()){if(e(n.key,`cards[${c}].key 必填`),e(!i.has(n.key),`cards key 重复:${n.key}`),i.add(n.key),e(W.has(n.type),`cards[${n.key}].type 必须是 ${[...W].join(" | ")}`),n.type==="existing"){e(n.id,`cards[${n.key}] type=existing 需要 id`),e(n.knowledge?.content,`cards[${n.key}].knowledge.content(卡片内容)必填`),e(n.knowledge?.source,`cards[${n.key}].knowledge.source(数据来源)必填`),e(n.knowledge?.grain,`cards[${n.key}].knowledge.grain(数据粒度)必填`),e(n.knowledge?.dataLogic,`cards[${n.key}].knowledge.dataLogic(取数逻辑)必填`);continue}e(d.has(n.dataset),`cards[${n.key}].dataset 引用了不存在的数据集 key:${n.dataset}`),n.type==="chart"&&(e(n.name,`cards[${n.key}].name 必填`),e(n.kind,`cards[${n.key}].kind 必填(如 column/stacked,见 linkai kinds)`),e(N(n.meas).length,`cards[${n.key}].meas 至少一个数值列`)),n.type==="meter"&&(e(n.name,`cards[${n.key}].name 必填`),e(n.col,`cards[${n.key}].col 必填`)),(n.type==="table"||n.type==="gantt")&&(e(n.spec&&typeof n.spec=="object",`cards[${n.key}].spec 必填(内联对象或 .json 路径)`),e(n.spec?.name,`cards[${n.key}].spec.name 必填`)),e(n.knowledge?.content,`cards[${n.key}].knowledge.content(卡片内容)必填`),e(n.knowledge?.source,`cards[${n.key}].knowledge.source(数据来源)必填`),e(n.knowledge?.grain,`cards[${n.key}].knowledge.grain(数据粒度)必填`),e(n.knowledge?.dataLogic,`cards[${n.key}].knowledge.dataLogic(取数逻辑)必填`)}const l=new Set;for(const[c,n]of(t.sections||[]).entries())for(const[u,k]of(n.rows||[]).entries()){e(Number(k.height)>0,`sections[${c}].rows[${u}].height 必须为正整数(格)`);for(const y of k.cards||[])e(i.has(y.card),`sections[${c}].rows[${u}] 引用了不存在的卡片 key:${y.card}`),e(!l.has(y.card),`卡片 ${y.card} 在布局中出现多次`),l.add(y.card)}for(const c of t.publish?.reports||[])e(i.has(c.card),`publish.reports 引用了不存在的卡片 key:${c.card}`);return X({knowledge:t.knowledge,cards:(t.cards||[]).map(c=>c.knowledge)})&&s.push("方案知识库不得包含完整 SQL;cards[].knowledge.dataLogic 必须使用自然语言描述"),s}g(Y,"validateSpec");function D(t){return[t.name,...(t.datasets||[]).filter(e=>!e.id).map(e=>e.name),...(t.cards||[]).filter(e=>e.type!=="existing").map(e=>e.name||e.spec?.name),...(t.publish?.reports||[]).map(e=>e.name)].filter(e=>e&&!String(e).startsWith("zz-"))}g(D,"formalNames");function z(t){const s={...H,...t.layout||{}},e=s.contentX+s.contentWidth,d=[],i=[];let l=0,c=0;const n=g(u=>{d.push({...u,mobileX:0,mobileY:c,mobileWidth:4,mobileHeight:u.height}),c+=u.height},"push");for(const[u,k]of(t.sections||[]).entries()){k.title&&(n({kind:"title",key:`title-${u}`,title:k.title,x:s.contentX,y:l,width:s.contentWidth,height:s.titleHeight}),l+=s.titleHeight);for(const[y,h]of(k.rows||[]).entries()){const o=h.cards||[],r=o.reduce((x,b)=>x+(Number(b.width)||0),0),m=o.filter(x=>!x.width).length,p=m?Math.floor((s.contentWidth-r)/m):0;let S=s.contentX;for(const x of o){const b=Number(x.width)||p;b<=0&&i.push(`sections[${u}].rows[${y}] 卡片 ${x.card} 宽度为 0(行内固定宽度已占满)`),n({kind:"card",key:x.card,x:S,y:l,width:b,height:Number(h.height)}),S+=b}S>e&&i.push(`sections[${u}].rows[${y}] 总宽 ${S-s.contentX} 超出内容区 ${s.contentWidth} 格`),l+=Number(h.height)}}return{layout:s,blocks:d,errors:[...i,...C(d,"pc"),...C(d,"mobile")]}}g(z,"planLayout");function C(t,s){const e=g(i=>s==="pc"?[i.x,i.y,i.width,i.height]:[i.mobileX,i.mobileY,i.mobileWidth,i.mobileHeight],"rect"),d=[];for(let i=0;i<t.length;i++)for(let l=i+1;l<t.length;l++){const[c,n,u,k]=e(t[i]),[y,h,o,r]=e(t[l]);c<y+o&&y<c+u&&n<h+r&&h<n+k&&d.push(`${s} 布局重叠:${t[i].key} 与 ${t[l].key}`)}return d}g(C,"overlaps");function R(t){return JSON.stringify({type:"title-template",version:1,template:"section",config:{serial:String(t.serial??""),title:String(t.title??""),subtitle:String(t.subtitle??""),description:"",showDecorLine:!1}})}g(R,"titleContent");function _(t){const s=[];for(const e of t.datasets||[])s.push(e.id?`复用数据集 ${e.key} = ${e.id}`:`建数据集 ${e.key}「${e.name}」← ${e.sql}`);for(const e of t.cards||[]){if(e.type==="existing"){s.push(`复用卡片 ${e.key} = ${e.id}`);continue}s.push(`建卡片 ${e.key}(${e.type}${e.kind?` ${e.kind}`:""})「${e.name||e.spec?.name||""}」${e.sort?`,排序 ${e.sort}`:""}`)}s.push(`建方案「${t.name}」并按布局放入 ${(t.sections||[]).length} 个分区`),s.push("写页面样式(暖色背景、标题/指标卡卡位透明、隐藏外层标题)"),t.filters&&s.push(`写方案筛选器模板 ← ${t.filters}`);for(const e of t.publish?.reports||[])s.push(`发布报表「${e.name}」← 卡片 ${e.card}`);return s.push("验收:逐卡 query + 方案回读(卡片数、越界、重叠、背景色)"),s.push("生成并保存版本化方案知识库(完整呈现给用户,不含 SQL)"),s}g(_,"describePlan");function M(t,s,e=""){const d=g((i,l)=>(c,n)=>{if(!i[n])throw new Error(`filters 引用了不存在或未创建的${l} key:${n}`);return i[n]},"pick");return t.replaceAll("{{plan}}",s.planId).replaceAll("{{dataSource}}",e).replace(/\{\{pageCard:([^}]+)\}\}/g,d(s.blocks,"卡片")).replace(/\{\{dataset:([^}]+)\}\}/g,d(s.datasets,"数据集"))}g(M,"fillPlaceholders");function U(t){try{return JSON.parse(O.readFileSync(t,"utf8"))}catch{return{datasets:{},cards:{},sorted:{},blocks:{},reports:{}}}}g(U,"readState");function K(t,s){O.writeFileSync(t,JSON.stringify(s,null,2))}g(K,"writeState");const T=g((t,s)=>j.resolve(t,String(s)),"resolveFile");async function A(t,s){try{return await B.reviewSpec(t,e=>O.readFileSync(T(s,e),"utf8"))}catch(e){return{mode:t.reuse??"suggest",skipped:f.LoginRequiredError&&e instanceof f.LoginRequiredError?"未登录,无法查重":`查重失败:${String(e.message).slice(0,200)}`,datasets:{},cards:{}}}}g(A,"reviewReuse");function G(t){const s=Object.entries(t.datasets||{}).filter(([,d])=>d.identical.length||d.similar.length),e=Object.entries(t.cards||{});return!s.length&&!e.length?null:`发现可复用对象:数据集 ${s.map(([d])=>d).join("、")||"无"},卡片 ${e.map(([d])=>d).join("、")||"无"}。`+(t.mode==="auto"?"完全一样的将直接复用;相似的请比较后决定。":"完全一样的可在 spec 加 reuse: auto 直接复用;相似的请比较 columns 后决定复用、在原数据集上调整,还是新建。")}g(G,"reuseHint");async function oe({spec:t,specFile:s,baseDir:e},{execute:d=!1,confirm:i=!1,style:l=!0,beforeWrite:c}={}){const n=Y(t),u=n.length?{blocks:[],errors:[]}:z(t);n.push(...u.errors);const k=n.length?[]:D(t);if(n.length)return v.emit("build.stage",{stage:"validate",ok:!1,errors:n.length}),{ok:!1,stage:"validate",errors:n};const y={name:t.name,tenant:J(),steps:_(t),layout:u.blocks.map(a=>({key:a.key,x:a.x,y:a.y,w:a.width,h:a.height}))};if(!d){const a=await A(t,e),w=G(a);return{ok:!0,mode:"dry-run",...y,formalNames:k,reuse:a,next:[w,k.length?"含正式名:取得用户授权后加 --execute --confirm":"确认无误后加 --execute"].filter(Boolean).join(" ")}}if(k.length&&!i)return{ok:!1,stage:"authorize",errors:[`以下为正式名(非 zz- 前缀),按 SOP 需要用户明确授权后加 --confirm:${k.join("、")}`]};c&&await c();const h=t.reuse==="auto"?await A(t,e):{mode:t.reuse??"suggest",datasets:{},cards:{}},o=`${s}.state.json`,r=U(o),m=g(()=>K(o,r),"save"),p=[];let S=Date.now();p.push=(...a)=>{for(const w of a){const[$,I=""]=String(w).split(/\s+(.*)/s);v.emit("build.stage",{stage:$,detail:I.slice(0,200),ok:!0,ms:Date.now()-S}),S=Date.now()}return Array.prototype.push.apply(p,a)},v.emit("build.stage",{stage:"start",ok:!0,resumed:!!(r.planId||Object.keys(r.datasets||{}).length),datasets:(t.datasets||[]).length,cards:(t.cards||[]).length,reports:(t.publish?.reports||[]).length});for(const a of t.datasets||[]){if(r.datasets[a.key])continue;const w=!a.id&&h.datasets[a.key]?.reuseId;r.datasets[a.key]=a.id||w||await f.dsSave({name:a.name,sql:O.readFileSync(T(e,a.sql),"utf8")}),m(),p.push(`dataset ${a.key} → ${r.datasets[a.key]}${w?"(复用已有数据集)":""}`)}for(const a of t.cards||[]){if(!r.cards[a.key]){const $=r.datasets[a.dataset];let I;a.type==="existing"?I={cardId:a.id}:h.cards[a.key]?.reuseId?I={cardId:h.cards[a.key].reuseId,reused:!0}:a.type==="chart"?I=await f.cardChart({dsId:$,name:a.name,kind:a.kind,dim:N(a.dim).join(","),meas:N(a.meas).join(","),series:a.series}):a.type==="meter"?I=await f.cardMeter({dsId:$,name:a.name,col:a.col,subtitle:a.subtitle,color:a.color||void 0}):a.type==="table"?I=await f.cardTable({dsId:$,spec:a.spec}):I=await f.cardGantt({dsId:$,spec:a.spec}),r.cards[a.key]=I.cardId,m(),p.push(`card ${a.key} → ${I.cardId}${I.reused?"(复用已有卡片)":""}`)}const w=h.cards[a.key]?.reuseId&&r.cards[a.key]===h.cards[a.key].reuseId;if(a.sort&&!r.sorted[a.key]&&!w){const $=await f.cardSort(r.cards[a.key],String(a.sort));r.sorted[a.key]=$.sqlHasOrderBy,m(),p.push(`sort ${a.key} → sqlHasOrderBy=${$.sqlHasOrderBy}`)}}r.planId||(r.planId=await f.post("/page-setting/new-create-empty-plan",{name:t.name,isPrivate:!1,pageType:0}),m(),p.push(`plan → ${r.planId}`));for(const a of u.blocks){if(r.blocks[a.key])continue;const w={x:a.x,y:a.y,width:a.width,height:a.height,mobileX:a.mobileX,mobileY:a.mobileY,mobileWidth:a.mobileWidth,mobileHeight:a.mobileHeight},$=a.kind==="title"?{...w,sourceType:0,cardType:5,contentText:R(a.title),bindFilterIds:[],userPageSettingId:r.planId}:{...w,indexBlockId:r.cards[a.key],sourceType:0,cardType:1,bindFilterIds:[],userPageSettingId:r.planId};r.blocks[a.key]=(await f.post("/page-setting/new-page-card",$)).id,m()}if(p.push(`placed ${u.blocks.length} blocks`),l&&!r.styled&&(r.styled=await Q(t,u,r),m(),p.push(`style → ${JSON.stringify(r.styled)}`)),t.filters&&!r.filtered){const a=M(O.readFileSync(T(e,t.filters),"utf8"),r,(await f.ctx()).dataSourceId),w=JSON.parse(a);r.filterDatasetIds=[...new Set((w.filters||[]).map($=>$.indexBlockDataId).filter($=>$&&$!==f.Z))],r.filtered=await f.planFilter(r.planId,w),m(),p.push(`filters → ${r.filtered.filters?.length??0}`)}for(const a of t.publish?.reports||[])r.reports[a.card]||(await f.post("/index-block/operate-report",{indexBlockId:r.cards[a.card],name:a.name,tag:"",isPrivate:!1}),r.reports[a.card]=a.name,m(),p.push(`report ${a.name}`));const x=new Set(Object.entries(h.cards||{}).filter(([a,w])=>w.reuseId&&r.cards[a]===w.reuseId).map(([a])=>a)),b=await Z(t,u,r,x);return v.emit("build.stage",{stage:"verify",ok:!!b.ok,checks:Array.isArray(b.checks)?b.checks.length:void 0,failed:Array.isArray(b.checks)?b.checks.filter(a=>a&&a.ok===!1).length:void 0}),b.ok?{ok:!0,mode:"execute",...await P({planId:r.planId,operation:"create",authoring:L(t,r)}),verify:b,log:p,stateFile:o,...h.mode==="auto"?{reuse:h}:{}}:{ok:!1,mode:"execute",verify:b,log:p,stateFile:o}}g(oe,"runBuild");async function Q(t,s,e){const d=await f.planDetail(e.planId,!0),i=d.pageCards||[],l=new Set(Object.values(e.blocks)),c=new Set(i.map(r=>r.id));if(![...l].every(r=>c.has(r)))return{skipped:"方案回读缺卡,未写样式(避免 update-page-card 移除卡片)"};if(!d.name)return{skipped:"方案详情缺 name 字段,未写样式(避免覆盖方案配置)"};const n=Object.fromEntries(Object.entries(e.blocks).map(([r,m])=>[m,r])),u=Object.fromEntries((t.cards||[]).map(r=>[r.key,r.type])),k=s.blocks.some(r=>r.kind==="title"),y=g(r=>{try{return typeof r=="string"?JSON.parse(r||"{}"):r||{}}catch{return{}}},"parse"),h=i.map(r=>{const m=n[r.id],p=y(r.dataScreenSetting);if(m){const S=m.startsWith("title-"),x=S||q.has(u[m]);Object.assign(p,{useCardConfig:!0},x?{backgroundColor:"transparent"}:{},S||k?{cardTitle:{...p.cardTitle||{},show:!1}}:{})}return{id:r.id,x:r.x,y:r.y,width:r.width,height:r.height,mobileX:r.mobileX,mobileY:r.mobileY,mobileWidth:r.mobileWidth,mobileHeight:r.mobileHeight,zIndex:r.zIndex??0,dataScreenSetting:JSON.stringify(p),...r.dataScreenChartSetting!==void 0?{dataScreenChartSetting:r.dataScreenChartSetting}:{}}}),o={...y(d.dataScreenSetting),pageBackgroundColor:s.layout.pageBackgroundColor};return await f.post("/page-setting/update-page-card",{list:h,userPageSettingId:e.planId,dataScreenSetting:JSON.stringify(o),name:d.name,tag:d.tag||"",remark:d.remark||"",parentId:d.parentId,isPrivate:!!d.isPrivate}),{cards:h.length,pageBackgroundColor:o.pageBackgroundColor}}g(Q,"applyStyle");async function Z(t,s,e,d=new Set){const i=[];for(const o of t.cards||[]){const r=await f.queryCard({cardId:e.cards[o.key],pageSize:1});i.push({check:`query ${o.key}`,ok:r.code===200,detail:r.code===200?`total=${r.total}`:r.message})}for(const o of(t.cards||[]).filter(r=>r.type==="table"&&r.spec?.fitTableWidth!==!1)){const r=await f.cardDetail(e.cards[o.key]),p=(typeof r.chartSetting=="string"?(()=>{try{return JSON.parse(r.chartSetting||"{}")}catch{return{}}})():r.chartSetting||{}).config?.fitTableWidth===!0,S=d.has(o.key);i.push({check:`AG 自适应宽度 ${o.key}`,ok:p||S,detail:p?"":S?"复用的共享表格未开启,未修改":"fitTableWidth 未开启"})}const l=await f.planDetail(e.planId,!0),c=l.pageCards||[],n=new Set(c.map(o=>o.id));i.push({check:"方案卡片齐全",ok:Object.values(e.blocks).every(o=>n.has(o)),detail:`${c.length} 张`});const u=s.layout,k=c.filter(o=>o.x<u.contentX||o.x+o.width>u.contentX+u.contentWidth);i.push({check:`PC 卡位在 [${u.contentX},${u.contentX+u.contentWidth}] 内`,ok:!k.length,detail:k.map(o=>o.id).join(",")});const y=c.map(o=>({key:o.id,x:o.x,y:o.y,width:o.width,height:o.height,mobileX:o.mobileX,mobileY:o.mobileY,mobileWidth:o.mobileWidth,mobileHeight:o.mobileHeight})),h=[...C(y,"pc"),...C(y,"mobile")];if(i.push({check:"PC/手机无重叠",ok:!h.length,detail:h.join("; ")}),e.styled&&!e.styled.skipped){let o={};try{o=JSON.parse(l.dataScreenSetting||"{}")}catch{}i.push({check:"页面背景色",ok:o.pageBackgroundColor===u.pageBackgroundColor,detail:o.pageBackgroundColor||"(未设置)"})}for(const[o,r]of Object.entries(e.sorted))i.push({check:`排序生效 ${o}`,ok:r===!0,detail:""});return{ok:i.every(o=>o.ok),checks:i}}g(Z,"verifyBuild");export{_ as describePlan,M as fillPlaceholders,D as formalNames,se as loadSpec,z as planLayout,oe as runBuild,R as titleContent,Y as validateSpec,Z as verifyBuild};
@@ -1 +1 @@
1
- const NO_DATA_SETTING_CHARTS=new Set(["editor","iframe","clock","countdown"]),FRONTEND_DISABLED_CHARTS=new Set(["meter/commonMeter","meter/antdTable"]),PAGINATION_CHARTS=new Set(["agtable","gantt","multiFlowStep"]),FILTER_TYPE={select:1,time:2,condition:3},DEFAULT_TYPE={fixed:1,dynamic:4},parse=v=>{if(v&&typeof v=="object")return v;if(typeof v!="string"||!v.trim())return null;try{return JSON.parse(v)}catch{return}},filled=v=>!(v==null||v===""||Array.isArray(v)&&!v.length);function aliasError(title){if(!filled(title))return null;const s=String(title);return/^[0-9]/.test(s)?"不能以数字开头":/[~!@#$%^&*()+.`\-={}|;'",<>/?[\]]/.test(s)?"不能含特殊符号(可用中文括号)":null}const isCardSave=(method,p)=>String(method).toUpperCase()==="POST"&&/(?:^|\/)index-block\/(?:new|update-new)(?:\?|$)/i.test(String(p)),defaultDisabled=(parentId,id)=>FRONTEND_DISABLED_CHARTS.has(`${parentId}/${id}`);function cardErrors(body,{isDisabled=defaultDisabled}={}){if(!body||typeof body!="object"||Array.isArray(body))return[];const errors=[],chart=parse(body.chartSetting);if(!chart||!chart.id)return["请选择图表配置(chartSetting 缺失或不是合法 JSON,或没有图表 id)"];isDisabled(chart.parentId,chart.id)&&errors.push(`当前图表已禁用(${chart.parentId}/${chart.id}),请换用其它图表`);const dtos=Array.isArray(body.indexBlockDataSettingDtos)?body.indexBlockDataSettingDtos:[],drawn=dtos.filter(d=>[1,2,3].includes(Number(d?.argumentType)));!NO_DATA_SETTING_CHARTS.has(chart.id)&&!drawn.length&&errors.push("请选择绘制配置(维度 / 数值 / 系列至少一个字段)");for(const o of dtos.filter(d=>Number(d?.argumentType)===0))(!o?.title||!o?.orderByType)&&errors.push("指标排序不能为空(排序项要有字段 title 和排序方式 orderByType)");for(const d of drawn){const role=Number(d.argumentType);d.isAggregation&&d.isCustomize&&(role===2||role===3)&&errors.push(`聚合字段「${d.title||d.comment}」不可作为维度或系列`),d.isFill&&(role===1||role===3)&&errors.push(`填报字段「${d.title||d.comment}」不可作为数值或系列`)}for(const d of drawn)if(d.title&&d.comment&&d.title!==d.comment){const why=aliasError(d.title);why&&errors.push(`字段别名「${d.title}」${why}`)}if(chart.config?.report?.visible){const drawnIds=new Set(dtos.map(d=>d?.uniqueId).filter(Boolean));(Array.isArray(body.fillFields)?body.fillFields:[]).flatMap(b=>(b?.fields||[]).map(f=>f?.uniqueId)).filter(Boolean).some(id=>!drawnIds.has(id))&&errors.push("填报依据字段缺失,请重新选择(依据字段必须是卡片已绘制的字段)")}const fields=new Set(drawn.map(d=>d?.field).filter(Boolean));if((Array.isArray(body.filters)?body.filters:[]).forEach((f,i)=>{Number(f?.filterProperty)===1&&f?.propertyName&&!fields.has(f.propertyName)&&errors.push(`筛选条件配置错误:第 ${i+1} 个筛选(${f.labelName||f.propertyName})的结果集字段 ${f.propertyName} 不在卡片绘制字段里`),errors.push(...filterErrors(f,i,"card"))}),chart.id==="s2table"&&chart.config?.showPagination!==!0){const n=Number(body.limitCount);(!Number.isInteger(n)||n<1||n>5e3)&&errors.push("请输入数据条数,数据格式为整数,最大不能超过 5000(多维表未开分页)")}return[...new Set(errors)]}function normalizeCard(body){const chart=parse(body.chartSetting),notes=[];return PAGINATION_CHARTS.has(chart.id)&&chart.config?.showPagination===!0&&body.limitCount&&(delete body.limitCount,notes.push("已开启分页功能,数据条数已自动清空")),body.chartParentType=chart.parentId,body.chartType=chart.id,body.indexBlockType=chart.parentId==="table"?0:1,notes}function checkCardPayload(method,p,body,{isDisabled=defaultDisabled,previous=null}={}){if(!isCardSave(method,p)||!body||typeof body!="object"||Array.isArray(body))return[];const errors=cardErrors(body,{isDisabled}),before=previous?new Set(cardErrors(previous,{isDisabled})):new Set,introduced=errors.filter(e=>!before.has(e));if(introduced.length){const error=new Error(`卡片保存前校验未通过:${introduced.join(";")}`);throw error.errors=introduced,error}const kept=errors.filter(e=>before.has(e));return[...normalizeCard(body),...kept.length?[`卡片原有配置与界面保存规则不一致(本次未改动,未拦截,建议修正):${kept.join(";")}`]:[]]}function filterErrors(f,i=0,scope="plan"){const errors=[],name=`第 ${i+1} 个筛选(${f?.labelName||f?.fieldName||f?.propertyName||"未命名"})`;if(!f||typeof f!="object")return[`${name}不是对象`];const type=Number(f.filterType),property=Number(f.filterProperty);if(filled(f.filterOperationType)||errors.push(`${name}缺少类型选择 filterOperationType`),property===2){const ids=parse(f.variableIds);(Array.isArray(ids)?ids.filter(Boolean).length:filled(f.variableIds))||errors.push(`${name}是参数变量筛选,必须选择参数 variableIds`)}scope==="card"&&(property===1&&!filled(f.propertyName)&&errors.push(`${name}是结果集过滤,必须选择筛选维度 propertyName`),property===0&&!filled(f.fieldName)&&errors.push(`${name}是维度过滤,必须选择字段 fieldName`)),scope==="plan"&&property===1&&(Array.isArray(f.bindUserPageCards)?f.bindUserPageCards:[]).some(b=>!b?.pageCardId)&&errors.push(`${name}绑定的卡片缺少 pageCardId`);const valueType=Number(f.defaultValueType),value=parse(f.defaultValue)??f.defaultValue;return(type===FILTER_TYPE.select||type===FILTER_TYPE.time)&&!valueType&&errors.push(`${name}缺少默认值类型 defaultValueType`),type===FILTER_TYPE.time&&valueType===DEFAULT_TYPE.dynamic&&(filled(value)?Array.isArray(value)&&value.filter(v=>filled(v)).length!==2&&errors.push(`${name}动态日期区间要设置开始和结束两个值`):errors.push(`${name}动态日期默认值未设置`)),f.isRequired&&!filled(value)&&errors.push(`${name}是必填筛选,请设置默认值`),type===FILTER_TYPE.select&&(String(f.isSelfadd)==="true"?(f.userPageCardFilterValueRes||[]).filter(o=>filled(o?.value)).length||errors.push(`${name}手工选项为空,请添加选项`):filled(f.fieldName)||errors.push(`${name}选项来源缺少字段 fieldName`)),errors}const isFilterSave=(method,p)=>String(method).toUpperCase()==="POST"&&/(?:^|\/)page-setting\/batch-add-or-update-filter(?:\?|$)/i.test(String(p));function checkFilterPayload(method,p,body,{previous=null}={}){if(!isFilterSave(method,p)||!body||typeof body!="object")return[];const scope=Number(body.filterSourceType)===2?"card":"plan",keyed=list=>(Array.isArray(list)?list:[]).flatMap((f,i)=>filterErrors(f,i,scope).map(message=>({message,key:`${f?.labelName??i}|${message.replace(/^第 \d+ 个筛选/,"")}`}))),errors=keyed(body.filters),before=new Set(keyed(previous).map(e=>e.key)),introduced=errors.filter(e=>!before.has(e.key));if(introduced.length){const error=new Error(`筛选条件保存前校验未通过:${introduced.map(e=>e.message).join(";")}`);throw error.errors=introduced.map(e=>e.message),error}const kept=errors.filter(e=>before.has(e.key));return kept.length?[`筛选条件原有配置与界面保存规则不一致(本次未改动,未拦截,建议修正):${kept.map(e=>e.message).join(";")}`]:[]}export{FRONTEND_DISABLED_CHARTS,NO_DATA_SETTING_CHARTS,aliasError,cardErrors,checkCardPayload,checkFilterPayload,filterErrors,isCardSave,isFilterSave};
1
+ var A=Object.defineProperty;var c=(e,a)=>A(e,"name",{value:a,configurable:!0});const T=new Set(["editor","iframe","clock","countdown"]),w=new Set(["meter/commonMeter","meter/antdTable"]),I=new Set(["agtable","gantt","multiFlowStep"]),m={select:1,time:2,condition:3},C={fixed:1,dynamic:4},h=c(e=>{if(e&&typeof e=="object")return e;if(typeof e!="string"||!e.trim())return null;try{return JSON.parse(e)}catch{return}},"parse"),f=c(e=>!(e==null||e===""||Array.isArray(e)&&!e.length),"filled");function P(e){if(!f(e))return null;const a=String(e);return/^[0-9]/.test(a)?"不能以数字开头":/[~!@#$%^&*()+.`\-={}|;'",<>/?[\]]/.test(a)?"不能含特殊符号(可用中文括号)":null}c(P,"aliasError");const b=c((e,a)=>String(e).toUpperCase()==="POST"&&/(?:^|\/)index-block\/(?:new|update-new)(?:\?|$)/i.test(String(a)),"isCardSave"),S=c((e,a)=>w.has(`${e}/${a}`),"defaultDisabled");function $(e,{isDisabled:a=S}={}){if(!e||typeof e!="object"||Array.isArray(e))return[];const r=[],i=h(e.chartSetting);if(!i||!i.id)return["请选择图表配置(chartSetting 缺失或不是合法 JSON,或没有图表 id)"];a(i.parentId,i.id)&&r.push(`当前图表已禁用(${i.parentId}/${i.id}),请换用其它图表`);const o=Array.isArray(e.indexBlockDataSettingDtos)?e.indexBlockDataSettingDtos:[],p=o.filter(t=>[1,2,3].includes(Number(t?.argumentType)));!T.has(i.id)&&!p.length&&r.push("请选择绘制配置(维度 / 数值 / 系列至少一个字段)");for(const t of o.filter(n=>Number(n?.argumentType)===0))(!t?.title||!t?.orderByType)&&r.push("指标排序不能为空(排序项要有字段 title 和排序方式 orderByType)");for(const t of p){const n=Number(t.argumentType);t.isAggregation&&t.isCustomize&&(n===2||n===3)&&r.push(`聚合字段「${t.title||t.comment}」不可作为维度或系列`),t.isFill&&(n===1||n===3)&&r.push(`填报字段「${t.title||t.comment}」不可作为数值或系列`)}for(const t of p)if(t.title&&t.comment&&t.title!==t.comment){const n=P(t.title);n&&r.push(`字段别名「${t.title}」${n}`)}if(i.config?.report?.visible){const t=new Set(o.map(s=>s?.uniqueId).filter(Boolean));(Array.isArray(e.fillFields)?e.fillFields:[]).flatMap(s=>(s?.fields||[]).map(l=>l?.uniqueId)).filter(Boolean).some(s=>!t.has(s))&&r.push("填报依据字段缺失,请重新选择(依据字段必须是卡片已绘制的字段)")}const u=new Set(p.map(t=>t?.field).filter(Boolean));if((Array.isArray(e.filters)?e.filters:[]).forEach((t,n)=>{Number(t?.filterProperty)===1&&t?.propertyName&&!u.has(t.propertyName)&&r.push(`筛选条件配置错误:第 ${n+1} 个筛选(${t.labelName||t.propertyName})的结果集字段 ${t.propertyName} 不在卡片绘制字段里`),r.push(...N(t,n,"card"))}),i.id==="s2table"&&i.config?.showPagination!==!0){const t=Number(e.limitCount);(!Number.isInteger(t)||t<1||t>5e3)&&r.push("请输入数据条数,数据格式为整数,最大不能超过 5000(多维表未开分页)")}return[...new Set(r)]}c($,"cardErrors");function k(e){const a=h(e.chartSetting),r=[];return I.has(a.id)&&a.config?.showPagination===!0&&e.limitCount&&(delete e.limitCount,r.push("已开启分页功能,数据条数已自动清空")),e.chartParentType=a.parentId,e.chartType=a.id,e.indexBlockType=a.parentId==="table"?0:1,r}c(k,"normalizeCard");function B(e,a,r,{isDisabled:i=S,previous:o=null}={}){if(!b(e,a)||!r||typeof r!="object"||Array.isArray(r))return[];const p=$(r,{isDisabled:i}),u=o?new Set($(o,{isDisabled:i})):new Set,t=p.filter(s=>!u.has(s));if(t.length){const s=new Error(`卡片保存前校验未通过:${t.join(";")}`);throw s.errors=t,s}const n=p.filter(s=>u.has(s));return[...k(r),...n.length?[`卡片原有配置与界面保存规则不一致(本次未改动,未拦截,建议修正):${n.join(";")}`]:[]]}c(B,"checkCardPayload");function N(e,a=0,r="plan"){const i=[],o=`第 ${a+1} 个筛选(${e?.labelName||e?.fieldName||e?.propertyName||"未命名"})`;if(!e||typeof e!="object")return[`${o}不是对象`];const p=Number(e.filterType),u=Number(e.filterProperty);if(f(e.filterOperationType)||i.push(`${o}缺少类型选择 filterOperationType`),u===2){const s=h(e.variableIds);(Array.isArray(s)?s.filter(Boolean).length:f(e.variableIds))||i.push(`${o}是参数变量筛选,必须选择参数 variableIds`)}r==="card"&&(u===1&&!f(e.propertyName)&&i.push(`${o}是结果集过滤,必须选择筛选维度 propertyName`),u===0&&!f(e.fieldName)&&i.push(`${o}是维度过滤,必须选择字段 fieldName`)),r==="plan"&&u===1&&(Array.isArray(e.bindUserPageCards)?e.bindUserPageCards:[]).some(l=>!l?.pageCardId)&&i.push(`${o}绑定的卡片缺少 pageCardId`);const t=Number(e.defaultValueType),n=h(e.defaultValue)??e.defaultValue;return(p===m.select||p===m.time)&&!t&&i.push(`${o}缺少默认值类型 defaultValueType`),p===m.time&&t===C.dynamic&&(f(n)?Array.isArray(n)&&n.filter(s=>f(s)).length!==2&&i.push(`${o}动态日期区间要设置开始和结束两个值`):i.push(`${o}动态日期默认值未设置`)),e.isRequired&&!f(n)&&i.push(`${o}是必填筛选,请设置默认值`),p===m.select&&(String(e.isSelfadd)==="true"?(e.userPageCardFilterValueRes||[]).filter(l=>f(l?.value)).length||i.push(`${o}手工选项为空,请添加选项`):f(e.fieldName)||i.push(`${o}选项来源缺少字段 fieldName`)),i}c(N,"filterErrors");const x=c((e,a)=>String(e).toUpperCase()==="POST"&&/(?:^|\/)page-setting\/batch-add-or-update-filter(?:\?|$)/i.test(String(a)),"isFilterSave");function F(e,a,r,{previous:i=null}={}){if(!x(e,a)||!r||typeof r!="object")return[];const o=Number(r.filterSourceType)===2?"card":"plan",p=c(l=>(Array.isArray(l)?l:[]).flatMap((d,g)=>N(d,g,o).map(y=>({message:y,key:`${d?.labelName??g}|${y.replace(/^第 \d+ 个筛选/,"")}`}))),"keyed"),u=p(r.filters),t=new Set(p(i).map(l=>l.key)),n=u.filter(l=>!t.has(l.key));if(n.length){const l=new Error(`筛选条件保存前校验未通过:${n.map(d=>d.message).join(";")}`);throw l.errors=n.map(d=>d.message),l}const s=u.filter(l=>t.has(l.key));return s.length?[`筛选条件原有配置与界面保存规则不一致(本次未改动,未拦截,建议修正):${s.map(l=>l.message).join(";")}`]:[]}c(F,"checkFilterPayload");export{w as FRONTEND_DISABLED_CHARTS,T as NO_DATA_SETTING_CHARTS,P as aliasError,$ as cardErrors,B as checkCardPayload,F as checkFilterPayload,N as filterErrors,b as isCardSave,x as isFilterSave};
@@ -1 +1 @@
1
- import fs from"node:fs";import path from"node:path";import os from"node:os";import crypto from"node:crypto";const DEFAULT_TTL_SECONDS=900,MAX_TTL_SECONDS=7200,MAX_NOTES=500,KINDS=new Set(["plan","card","ds","knowledge","spec","report","filter"]);class CollabConflictError extends Error{constructor(message,holders){super(message),this.name="CollabConflictError",this.holders=holders}}function collabDir(){return path.join(os.homedir(),".linkai","collab")}const stateFile=()=>path.join(collabDir(),"state.json"),lockFile=()=>path.join(collabDir(),"state.lock");function currentWorker(){return String(process.env.LINKAI_COLLAB_WORKER||"").trim()}function emptyState(){return{boards:{},claims:{}}}function readState(){try{return{...emptyState(),...JSON.parse(fs.readFileSync(stateFile(),"utf8"))}}catch{return emptyState()}}function sleepSync(ms){Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,ms)}function update(fn){fs.mkdirSync(collabDir(),{recursive:!0,mode:448});const deadline=Date.now()+3e3;let fd=null,staleRemovalFailed=!1;for(;fd===null;){if(Date.now()>deadline)throw new Error("协同状态被占用超过 3 秒,请稍后重试");try{fd=fs.openSync(lockFile(),"wx",384)}catch(e){if(e.code!=="EEXIST")throw e;let stale=!1;if(!staleRemovalFailed)try{stale=Date.now()-fs.statSync(lockFile()).mtimeMs>5e3}catch{}if(stale)try{fs.rmSync(lockFile(),{force:!0});continue}catch{staleRemovalFailed=!0}sleepSync(10)}}try{const state=readState();pruneExpired(state);const result=fn(state),tmp=`${stateFile()}.${process.pid}.tmp`;return fs.writeFileSync(tmp,JSON.stringify(state,null,2),{mode:384}),fs.renameSync(tmp,stateFile()),result}finally{fs.closeSync(fd),fs.rmSync(lockFile(),{force:!0})}}function pruneExpired(state,now=Date.now()){for(const[key,claim2]of Object.entries(state.claims))claim2.expiresAt<=now&&delete state.claims[key]}const iso=ms=>new Date(ms).toISOString();function normalizeObject(raw){const text=String(raw||"").trim(),m=text.match(/^([a-z]+):(.+)$/i);if(!m||!KINDS.has(m[1].toLowerCase()))throw new Error(`对象写法不对:${text||"(空)"}。格式 <类型>:<id>,类型:${[...KINDS].join(" / ")},如 card:<cardId>`);const kind=m[1].toLowerCase(),id=kind==="spec"?path.resolve(m[2].trim()):m[2].trim().toLowerCase();return`${kind}:${id}`}function parseObjects(list){const items=(Array.isArray(list)?list:String(list||"").split(/[,,\s]+/)).filter(Boolean);if(!items.length)throw new Error("至少指定一个对象,如 --object card:<cardId>");return[...new Set(items.map(normalizeObject))]}function requireWorker(worker){const w=String(worker||"").trim();if(!w||w.length>40||!/^[\w一-龥.-]+$/.test(w))throw new Error("需要执行者名称:加 --worker <名称>(字母、数字、中文、. _ -,最长 40),如 --worker 主控 或 --worker sql-1");return w}function requireBoard(state,board){const b=state.boards[String(board||"")];if(!b)throw new Error(`任务板不存在:${board||"(空)"};先 linkai collab-start 创建,或 linkai collab-status 查看已有任务板`);return b}function addNote(board,worker,kind,text,ref){board.notes.push({at:iso(Date.now()),worker,kind,text:String(text).slice(0,2e3),...ref?{ref:String(ref).slice(0,500)}:{}}),board.notes.length>MAX_NOTES&&board.notes.splice(0,board.notes.length-MAX_NOTES)}function startBoard({goal,plan,worker}){const w=requireWorker(worker);if(!String(goal||"").trim())throw new Error("需要 --goal(这次协同要完成什么)");return update(state=>{const id=`b-${crypto.randomBytes(4).toString("hex")}`;return state.boards[id]={id,goal:String(goal).trim().slice(0,500),plan:plan?String(plan).toLowerCase():null,coordinator:w,createdAt:iso(Date.now()),closedAt:null,tasks:[],notes:[]},addNote(state.boards[id],w,"start",`创建任务板:${state.boards[id].goal}`),state.boards[id]})}function closeBoard({board,worker}){const w=requireWorker(worker);return update(state=>{const b=requireBoard(state,board),released=Object.entries(state.claims).filter(([,c])=>c.board===b.id).map(([k])=>k);for(const k of released)delete state.claims[k];return b.closedAt=iso(Date.now()),addNote(b,w,"close",`关闭任务板,释放 ${released.length} 个认领`),{board:b.id,closedAt:b.closedAt,released}})}function addTask({board,worker,title,assignee,objects}){const w=requireWorker(worker);if(!String(title||"").trim())throw new Error("需要 --title");return update(state=>{const b=requireBoard(state,board),task={id:b.tasks.length+1,title:String(title).trim().slice(0,300),assignee:assignee?requireWorker(assignee):null,objects:objects?parseObjects(objects):[],status:"todo",result:null,by:w,updatedAt:iso(Date.now())};return b.tasks.push(task),task})}const STATUSES=new Set(["todo","doing","done","blocked"]);function updateTask({board,worker,id,status:status2,result,assignee}){const w=requireWorker(worker);return update(state=>{const b=requireBoard(state,board),task=b.tasks.find(t=>t.id===Number(id));if(!task)throw new Error(`任务不存在:#${id}`);if(status2){if(!STATUSES.has(status2))throw new Error(`status 只能是 ${[...STATUSES].join(" / ")}`);task.status=status2}return assignee?task.assignee=requireWorker(assignee):status2==="doing"&&!task.assignee&&(task.assignee=w),result&&(task.result=String(result).slice(0,2e3)),task.updatedAt=iso(Date.now()),(status2==="done"||status2==="blocked")&&addNote(b,w,status2==="done"?"result":"blocker",`#${task.id} ${task.title}${result?`:${result}`:""}`),task})}function claim({board,worker,objects,ttl=DEFAULT_TTL_SECONDS,note:note2}){const w=requireWorker(worker),keys=parseObjects(objects),seconds=Math.min(Math.max(Number(ttl)||DEFAULT_TTL_SECONDS,60),MAX_TTL_SECONDS);return update(state=>{const b=requireBoard(state,board);if(b.closedAt)throw new Error(`任务板 ${b.id} 已关闭`);const now=Date.now(),held=keys.map(k=>[k,state.claims[k]]).filter(([,c])=>c&&c.worker!==w);if(held.length)throw new CollabConflictError(`对象已被其他执行者认领:${held.map(([k,c])=>`${k}(${c.worker},${Math.ceil((c.expiresAt-now)/6e4)} 分钟后过期)`).join(";")}`,held.map(([k,c])=>({object:k,...c,expiresAt:iso(c.expiresAt)})));for(const k of keys)state.claims[k]={worker:w,board:b.id,since:state.claims[k]?.since??now,expiresAt:now+seconds*1e3,note:note2?String(note2).slice(0,200):null};return addNote(b,w,"claim",`认领 ${keys.join("、")}${note2?`:${note2}`:""}`),{board:b.id,worker:w,objects:keys,expiresAt:iso(now+seconds*1e3)}})}function release({board,worker,objects}){const w=requireWorker(worker);return update(state=>{const b=requireBoard(state,board),wanted=objects?parseObjects(objects):Object.keys(state.claims).filter(k=>state.claims[k].board===b.id&&state.claims[k].worker===w),released=[];for(const k of wanted){const c=state.claims[k];if(c){if(c.worker!==w)throw new CollabConflictError(`不能释放别人的认领:${k}(${c.worker})`,[{object:k,...c,expiresAt:iso(c.expiresAt)}]);delete state.claims[k],released.push(k)}}return released.length&&addNote(b,w,"release",`释放 ${released.join("、")}`),{board:b.id,worker:w,released}})}function note({board,worker,text,kind="finding",ref}){const w=requireWorker(worker);if(!String(text||"").trim())throw new Error("需要 --text");const k=String(kind);if(!["finding","result","handoff","blocker","decision"].includes(k))throw new Error("kind 只能是 finding / result / handoff / blocker / decision");return update(state=>{const b=requireBoard(state,board);return addNote(b,w,k,text,ref),b.notes[b.notes.length-1]})}function status({board,notes=30}={}){const state=readState();pruneExpired(state);const now=Date.now(),claims=Object.entries(state.claims).map(([object,c])=>({object,worker:c.worker,board:c.board,note:c.note,expiresAt:iso(c.expiresAt),minutesLeft:Math.ceil((c.expiresAt-now)/6e4)}));if(!board)return{boards:Object.values(state.boards).filter(b2=>!b2.closedAt).map(b2=>({id:b2.id,goal:b2.goal,plan:b2.plan,coordinator:b2.coordinator,createdAt:b2.createdAt,tasks:b2.tasks.length,open:b2.tasks.filter(t=>t.status!=="done").length})),claims};const b=requireBoard(state,board);return{...b,notes:b.notes.slice(-Math.max(1,Number(notes)||30)),claims:claims.filter(c=>c.board===b.id)}}function assertWritable(objects,worker=currentWorker()){if(!objects.length||!fs.existsSync(stateFile()))return;const state=readState();pruneExpired(state);const now=Date.now(),held=objects.map(k=>[k,state.claims[k]]).filter(([,c])=>c&&c.worker!==worker);if(held.length)throw new CollabConflictError(`${held.map(([k,c])=>`${k} 正由「${c.worker}」处理(任务板 ${c.board},${Math.ceil((c.expiresAt-now)/6e4)} 分钟后过期)`).join(";")},${worker?`你是「${worker}」`:"当前命令没有 --worker"},已拒绝写入`,held.map(([k,c])=>({object:k,...c,expiresAt:iso(c.expiresAt)})))}function objectsForCommand(name,args,readJsonFile){const keys=[],add=(kind,id)=>{id&&keys.push(normalizeObject(`${kind}:${id}`))};if(add("card",args.card),add("ds",args.ds),args.plan&&(add("plan",args.plan),name==="plan-knowledge"&&add("knowledge",args.plan)),args.spec&&name==="build"&&add("spec",args.spec),name==="card-save"&&args.file)try{add("card",readJsonFile(args.file)?.id)}catch{}return[...new Set(keys)]}export{CollabConflictError,DEFAULT_TTL_SECONDS,addTask,assertWritable,claim,closeBoard,collabDir,currentWorker,normalizeObject,note,objectsForCommand,parseObjects,release,startBoard,status,updateTask};
1
+ var _=Object.defineProperty;var c=(r,e)=>_(r,"name",{value:e,configurable:!0});import u from"node:fs";import x from"node:path";import B from"node:os";import F from"node:crypto";const C=900,v=7200,L=500,N=new Set(["plan","card","ds","knowledge","spec","report","filter"]);class y extends Error{static{c(this,"CollabConflictError")}constructor(e,n){super(e),this.name="CollabConflictError",this.holders=n}}function E(){return x.join(B.homedir(),".linkai","collab")}c(E,"collabDir");const g=c(()=>x.join(E(),"state.json"),"stateFile"),$=c(()=>x.join(E(),"state.lock"),"lockFile");function W(){return String(process.env.LINKAI_COLLAB_WORKER||"").trim()}c(W,"currentWorker");function T(){return{boards:{},claims:{}}}c(T,"emptyState");function j(){try{return{...T(),...JSON.parse(u.readFileSync(g(),"utf8"))}}catch{return T()}}c(j,"readState");function q(r){Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,r)}c(q,"sleepSync");function m(r){u.mkdirSync(E(),{recursive:!0,mode:448});const e=Date.now()+3e3;let n=null,s=!1;for(;n===null;){if(Date.now()>e)throw new Error("协同状态被占用超过 3 秒,请稍后重试");try{n=u.openSync($(),"wx",384)}catch(t){if(t.code!=="EEXIST")throw t;let i=!1;if(!s)try{i=Date.now()-u.statSync($()).mtimeMs>5e3}catch{}if(i)try{u.rmSync($(),{force:!0});continue}catch{s=!0}q(10)}}try{const t=j();D(t);const i=r(t),o=`${g()}.${process.pid}.tmp`;return u.writeFileSync(o,JSON.stringify(t,null,2),{mode:384}),u.renameSync(o,g()),i}finally{u.closeSync(n),u.rmSync($(),{force:!0})}}c(m,"update");function D(r,e=Date.now()){for(const[n,s]of Object.entries(r.claims))s.expiresAt<=e&&delete r.claims[n]}c(D,"pruneExpired");const w=c(r=>new Date(r).toISOString(),"iso");function M(r){const e=String(r||"").trim(),n=e.match(/^([a-z]+):(.+)$/i);if(!n||!N.has(n[1].toLowerCase()))throw new Error(`对象写法不对:${e||"(空)"}。格式 <类型>:<id>,类型:${[...N].join(" / ")},如 card:<cardId>`);const s=n[1].toLowerCase(),t=s==="spec"?x.resolve(n[2].trim()):n[2].trim().toLowerCase();return`${s}:${t}`}c(M,"normalizeObject");function O(r){const e=(Array.isArray(r)?r:String(r||"").split(/[,,\s]+/)).filter(Boolean);if(!e.length)throw new Error("至少指定一个对象,如 --object card:<cardId>");return[...new Set(e.map(M))]}c(O,"parseObjects");function p(r){const e=String(r||"").trim();if(!e||e.length>40||!/^[\w一-龥.-]+$/.test(e))throw new Error("需要执行者名称:加 --worker <名称>(字母、数字、中文、. _ -,最长 40),如 --worker 主控 或 --worker sql-1");return e}c(p,"requireWorker");function h(r,e){const n=r.boards[String(e||"")];if(!n)throw new Error(`任务板不存在:${e||"(空)"};先 linkai collab-start 创建,或 linkai collab-status 查看已有任务板`);return n}c(h,"requireBoard");function k(r,e,n,s,t){r.notes.push({at:w(Date.now()),worker:e,kind:n,text:String(s).slice(0,2e3),...t?{ref:String(t).slice(0,500)}:{}}),r.notes.length>L&&r.notes.splice(0,r.notes.length-L)}c(k,"addNote");function U({goal:r,plan:e,worker:n}){const s=p(n);if(!String(r||"").trim())throw new Error("需要 --goal(这次协同要完成什么)");return m(t=>{const i=`b-${F.randomBytes(4).toString("hex")}`;return t.boards[i]={id:i,goal:String(r).trim().slice(0,500),plan:e?String(e).toLowerCase():null,coordinator:s,createdAt:w(Date.now()),closedAt:null,tasks:[],notes:[]},k(t.boards[i],s,"start",`创建任务板:${t.boards[i].goal}`),t.boards[i]})}c(U,"startBoard");function G({board:r,worker:e}){const n=p(e);return m(s=>{const t=h(s,r),i=Object.entries(s.claims).filter(([,o])=>o.board===t.id).map(([o])=>o);for(const o of i)delete s.claims[o];return t.closedAt=w(Date.now()),k(t,n,"close",`关闭任务板,释放 ${i.length} 个认领`),{board:t.id,closedAt:t.closedAt,released:i}})}c(G,"closeBoard");function H({board:r,worker:e,title:n,assignee:s,objects:t}){const i=p(e);if(!String(n||"").trim())throw new Error("需要 --title");return m(o=>{const l=h(o,r),a={id:l.tasks.length+1,title:String(n).trim().slice(0,300),assignee:s?p(s):null,objects:t?O(t):[],status:"todo",result:null,by:i,updatedAt:w(Date.now())};return l.tasks.push(a),a})}c(H,"addTask");const I=new Set(["todo","doing","done","blocked"]);function P({board:r,worker:e,id:n,status:s,result:t,assignee:i}){const o=p(e);return m(l=>{const a=h(l,r),d=a.tasks.find(S=>S.id===Number(n));if(!d)throw new Error(`任务不存在:#${n}`);if(s){if(!I.has(s))throw new Error(`status 只能是 ${[...I].join(" / ")}`);d.status=s}return i?d.assignee=p(i):s==="doing"&&!d.assignee&&(d.assignee=o),t&&(d.result=String(t).slice(0,2e3)),d.updatedAt=w(Date.now()),(s==="done"||s==="blocked")&&k(a,o,s==="done"?"result":"blocker",`#${d.id} ${d.title}${t?`:${t}`:""}`),d})}c(P,"updateTask");function Q({board:r,worker:e,objects:n,ttl:s=C,note:t}){const i=p(e),o=O(n),l=Math.min(Math.max(Number(s)||C,60),v);return m(a=>{const d=h(a,r);if(d.closedAt)throw new Error(`任务板 ${d.id} 已关闭`);const S=Date.now(),A=o.map(f=>[f,a.claims[f]]).filter(([,f])=>f&&f.worker!==i);if(A.length)throw new y(`对象已被其他执行者认领:${A.map(([f,b])=>`${f}(${b.worker},${Math.ceil((b.expiresAt-S)/6e4)} 分钟后过期)`).join(";")}`,A.map(([f,b])=>({object:f,...b,expiresAt:w(b.expiresAt)})));for(const f of o)a.claims[f]={worker:i,board:d.id,since:a.claims[f]?.since??S,expiresAt:S+l*1e3,note:t?String(t).slice(0,200):null};return k(d,i,"claim",`认领 ${o.join("、")}${t?`:${t}`:""}`),{board:d.id,worker:i,objects:o,expiresAt:w(S+l*1e3)}})}c(Q,"claim");function V({board:r,worker:e,objects:n}){const s=p(e);return m(t=>{const i=h(t,r),o=n?O(n):Object.keys(t.claims).filter(a=>t.claims[a].board===i.id&&t.claims[a].worker===s),l=[];for(const a of o){const d=t.claims[a];if(d){if(d.worker!==s)throw new y(`不能释放别人的认领:${a}(${d.worker})`,[{object:a,...d,expiresAt:w(d.expiresAt)}]);delete t.claims[a],l.push(a)}}return l.length&&k(i,s,"release",`释放 ${l.join("、")}`),{board:i.id,worker:s,released:l}})}c(V,"release");function Y({board:r,worker:e,text:n,kind:s="finding",ref:t}){const i=p(e);if(!String(n||"").trim())throw new Error("需要 --text");const o=String(s);if(!["finding","result","handoff","blocker","decision"].includes(o))throw new Error("kind 只能是 finding / result / handoff / blocker / decision");return m(l=>{const a=h(l,r);return k(a,i,o,n,t),a.notes[a.notes.length-1]})}c(Y,"note");function Z({board:r,notes:e=30}={}){const n=j();D(n);const s=Date.now(),t=Object.entries(n.claims).map(([o,l])=>({object:o,worker:l.worker,board:l.board,note:l.note,expiresAt:w(l.expiresAt),minutesLeft:Math.ceil((l.expiresAt-s)/6e4)}));if(!r)return{boards:Object.values(n.boards).filter(o=>!o.closedAt).map(o=>({id:o.id,goal:o.goal,plan:o.plan,coordinator:o.coordinator,createdAt:o.createdAt,tasks:o.tasks.length,open:o.tasks.filter(l=>l.status!=="done").length})),claims:t};const i=h(n,r);return{...i,notes:i.notes.slice(-Math.max(1,Number(e)||30)),claims:t.filter(o=>o.board===i.id)}}c(Z,"status");function tt(r,e=W()){if(!r.length||!u.existsSync(g()))return;const n=j();D(n);const s=Date.now(),t=r.map(i=>[i,n.claims[i]]).filter(([,i])=>i&&i.worker!==e);if(t.length)throw new y(`${t.map(([i,o])=>`${i} 正由「${o.worker}」处理(任务板 ${o.board},${Math.ceil((o.expiresAt-s)/6e4)} 分钟后过期)`).join(";")},${e?`你是「${e}」`:"当前命令没有 --worker"},已拒绝写入`,t.map(([i,o])=>({object:i,...o,expiresAt:w(o.expiresAt)})))}c(tt,"assertWritable");function et(r,e,n){const s=[],t=c((i,o)=>{o&&s.push(M(`${i}:${o}`))},"add");if(t("card",e.card),t("ds",e.ds),e.plan&&(t("plan",e.plan),r==="plan-knowledge"&&t("knowledge",e.plan)),e.spec&&r==="build"&&t("spec",e.spec),r==="card-save"&&e.file)try{t("card",n(e.file)?.id)}catch{}return[...new Set(s)]}c(et,"objectsForCommand");export{y as CollabConflictError,C as DEFAULT_TTL_SECONDS,H as addTask,tt as assertWritable,Q as claim,G as closeBoard,E as collabDir,W as currentWorker,M as normalizeObject,Y as note,et as objectsForCommand,O as parseObjects,V as release,U as startBoard,Z as status,P as updateTask};