@linkaicli/linkai-cli 1.0.20 → 1.0.22
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 +2 -1
- package/GUIDE.md +6 -1
- package/README.md +2 -2
- package/THIRD_PARTY_NOTICES.txt +1 -1
- package/cli-manifest.json +20 -8
- package/clis/linkai/commands.js +3 -3
- package/clis/linkai/core.js +3 -3
- package/clis/linkai/deliverable.js +2 -2
- package/clis/linkai/dictionary.js +1 -1
- package/clis/linkai/oauth.js +6 -6
- package/dist/bundle/core.mjs +232 -231
- package/dist/bundle/skills.mjs +1 -1
- package/llms.txt +2 -2
- package/package.json +2 -2
- package/skills/agent-prompt.md +1 -1
- package/skills/linkai-ask/SKILL.md +52 -43
- package/skills/linkai-data-workflow/SKILL.md +2 -2
- package/skills/linkai-report-builder/SKILL.md +6 -4
- package/skills/linkai-usage/SKILL.md +7 -4
package/AGENTS.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
## 六条硬性规定(优先于本文其它内容)
|
|
7
7
|
|
|
8
|
-
1. **没有操作数据库的能力**:本 CLI 对用户开放,不能直连数据库、执行任意 SQL、管理数据源或改数仓。问数(`linkai ask
|
|
8
|
+
1. **没有操作数据库的能力**:本 CLI 对用户开放,不能直连数据库、执行任意 SQL、管理数据源或改数仓。问数(`linkai ask`)先把问题交给领谋服务端(小满问数);小满答不了时由 AI 按数据字典写单条只读 SQL 兜底,SQL 须先过数据集运行校验,再由服务端按用户的数据权限、在守卫和频率限制下执行。
|
|
9
9
|
领谋 BI 数据集只能是单条只读查询,且只能查 `linkai ds-tables` 列出的租户视图;需要新视图或改数据时,告诉用户联系领谋数据团队。
|
|
10
10
|
不要尝试绕过(命令已在本地拦截,也不要用 `api` 透传去调数据源 / SQL 类接口)。
|
|
11
11
|
2. **方案搭建完成必须交付完整方案知识库**:必须呈现方案地址、版本、方案内容与解决的问题,并按每张业务卡片分别描述卡片内容、数据集、数据来源、粒度和取数逻辑/规则。
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
- 所有命令输出 JSON 信封:成功 `{"ok":true,"data":…}`;失败退出码非 0,`{"ok":false,"error":{"code","message","hint?"}}`。日志走 stderr。
|
|
23
23
|
- 发现命令:`linkai list <关键字>`;看参数:`linkai <site> <command> --help -f json`。不要凭记忆拼参数。
|
|
24
|
+
- **工作流**:每个业务任务(搭建、修改、导入)的第一条命令是 `linkai flow-start --goal "<一句话目标>" --type create|modify|import|query [--plan <planId>]`(修改已有方案必带 `--plan`),结束时 `linkai flow-end --status delivered|abandoned|failed`。
|
|
24
25
|
- 写命令(help 中标 `[write]`)结果不确定时,先用只读命令确认现状,**不要重复执行来"修复"**。
|
|
25
26
|
- **登录只使用 OAuth**:首次登录由用户执行 `linkai login`,在领谋浏览器页面完成授权;CLI 用刷新令牌自动续期。不索要账号密码。同账号切换租户用 `linkai login --tenant <租户名>`;用户要求切换账号时,先问是否要切换领谋账号,确认后执行 `linkai login --switch-account`。
|
|
26
27
|
- 文件参数可用相对路径(相对当前目录)。
|
package/GUIDE.md
CHANGED
|
@@ -36,6 +36,10 @@ linkai --version
|
|
|
36
36
|
|
|
37
37
|
> Mac 若提示权限不足(`EACCES`),改用:`sudo npm install -g @linkaicli/linkai-cli`,按提示输入**电脑开机密码**。
|
|
38
38
|
|
|
39
|
+
> **一台电脑只装一份**:WorkBuddy 自带一套 Node.js,在 WorkBuddy 里让 AI 执行的 `npm install -g` 会装进它自己的目录,
|
|
40
|
+
> 和你在系统终端里装的是两份,各自升级、版本可能不同。用 WorkBuddy 的话,让 WorkBuddy 里的 AI 安装即可,系统终端里不用再装。
|
|
41
|
+
> 已经装了两份时,`linkai doctor` 会列出每份的位置和版本,用那份所在 Node 的 npm 执行 `npm uninstall -g @linkaicli/linkai-cli` 卸掉多余的。
|
|
42
|
+
|
|
39
43
|
## 第 3 步:登录领谋 BI(只需一次)
|
|
40
44
|
|
|
41
45
|
执行下面这行:
|
|
@@ -100,7 +104,7 @@ AI 会先给你看搭建计划(不会直接动手),你确认后才执行
|
|
|
100
104
|
- 「今天采购了多少、入库了多少?」
|
|
101
105
|
- 「本月各渠道的退货率是多少?」
|
|
102
106
|
|
|
103
|
-
AI
|
|
107
|
+
问题有歧义时 AI 会先问你(比如「入库」指物料入库还是成品入库),然后先交给领谋服务端(与领谋「小满问数」同一套)检索口径、查出数据;小满答不了时,AI 再按当前租户的数据字典写查询兜底(同样经领谋校验、按你的权限执行),回答时说明用了哪些数据、时间范围和「数据截至」时间。
|
|
104
108
|
查询按你在领谋里的数据权限执行,只能看到你本来就能看到的数据;有频率和数据量限制(明细最多 1000 行,没有导出权限时 50 行),需要大量明细请联系管理员导出。
|
|
105
109
|
|
|
106
110
|
## 升级
|
|
@@ -152,6 +156,7 @@ npm uninstall -g @linkaicli/linkai-cli
|
|
|
152
156
|
| AI 说「没有可用视图 / 不能操作数据库」 | 这是正常的安全限制:需要的数据视图请联系领谋数据团队开通 |
|
|
153
157
|
| 公共电脑 | 用完执行 `linkai logout`,撤销并删除本机 OAuth 会话 |
|
|
154
158
|
| 检查整体状态 | `linkai status` |
|
|
159
|
+
| 终端里 `linkai --version` 和 AI 用的版本不一样 | 装了两份(常见于 WorkBuddy 自带 Node)。`linkai doctor` 查看各份位置,只保留一份(见第 2 步) |
|
|
155
160
|
|
|
156
161
|
## 操作日志
|
|
157
162
|
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## 能做什么
|
|
6
6
|
|
|
7
|
-
-
|
|
7
|
+
- **问数**:直接问业务数据,例如「今天采购了多少、入库了多少」「本月各渠道退货率」。问题先交给领谋服务端(与领谋「小满问数」同一套)检索口径、查出数据;小满答不了时,AI 再按当前租户的数据字典写查询兜底(同样经领谋校验、按你的权限执行)。回答时说明用了哪些数据、时间范围和数据截至时间;问题有歧义时 AI 会先问你。
|
|
8
8
|
- **自动搭建看板 / 报表**:描述想看什么,AI 先给出搭建计划,你确认后自动完成数据集、卡片、看板、筛选器和报表,并逐项验收。完成后交付一份带版本的**方案知识库**:方案地址、方案内容、解决的问题,以及每张卡片的数据来源、粒度和取数规则。修改已有方案时,知识库同步升级版本。
|
|
9
9
|
- **数据导入与填报**:把 Excel 等数据按领谋的填报规则写入填报卡片,写前核对、写后回读。
|
|
10
10
|
- **复用已有成果**:建之前先查本租户里有没有同样的数据集、卡片,能复用就不重复建。
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
- **用你自己的领谋账号**:首次在领谋网页完成 OAuth 授权,之后自动续期;AI 不会索要你的密码。
|
|
17
17
|
- **只做你本来能做的事**:问数、取数按你在领谋里的数据权限执行,只能看到你本来就能看到的数据;租户之间严格隔离。
|
|
18
|
-
- **不能操作数据库**:不能直连数据库、执行任意 SQL
|
|
18
|
+
- **不能操作数据库**:不能直连数据库、执行任意 SQL、管理数据源或修改数仓。问数由领谋服务端按你的权限查询,并有频率和数据量限制(明细最多 1000 行,没有导出权限时 50 行)。
|
|
19
19
|
- **关键操作先确认**:正式发布、删除、覆盖等操作,AI 会先征得你同意。
|
|
20
20
|
- **全程留痕**:每次操作都会记录到领谋的操作日志,便于追溯。
|
|
21
21
|
|
package/THIRD_PARTY_NOTICES.txt
CHANGED
package/cli-manifest.json
CHANGED
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
{
|
|
69
69
|
"site": "linkai",
|
|
70
70
|
"name": "ask",
|
|
71
|
-
"description": "
|
|
71
|
+
"description": "问数:先把问题交给领谋服务端(与小满问数同一套)查询;小满答不了时,带 --sql 和 --fallback 由 AI 按数据字典写只读 SQL 兜底。按当前用户的数据权限执行,有频率和行数限制",
|
|
72
72
|
"access": "read",
|
|
73
73
|
"strategy": "public",
|
|
74
74
|
"browser": false,
|
|
@@ -77,32 +77,44 @@
|
|
|
77
77
|
"name": "question",
|
|
78
78
|
"type": "string",
|
|
79
79
|
"required": true,
|
|
80
|
-
"help": "
|
|
80
|
+
"help": "用户的问题(一次一个问题;有歧义先和用户确认,再把确认后的完整问题传入)"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "keywords",
|
|
84
|
+
"type": "string",
|
|
85
|
+
"required": false,
|
|
86
|
+
"help": "从问题里提炼的关键词,逗号分隔,如「采购数量,入库数量,今天」(用于统计)"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"name": "source",
|
|
90
|
+
"type": "string",
|
|
91
|
+
"required": false,
|
|
92
|
+
"help": "指定数据源 id(可选,仅问小满时;不传由服务端按问题自动选)"
|
|
81
93
|
},
|
|
82
94
|
{
|
|
83
95
|
"name": "sql",
|
|
84
96
|
"type": "string",
|
|
85
|
-
"required":
|
|
86
|
-
"help": "SQL
|
|
97
|
+
"required": false,
|
|
98
|
+
"help": "兜底:AI 写的 SQL 文件,1~3 个逗号分隔(必须同时给 --fallback)"
|
|
87
99
|
},
|
|
88
100
|
{
|
|
89
|
-
"name": "
|
|
101
|
+
"name": "fallback",
|
|
90
102
|
"type": "string",
|
|
91
103
|
"required": false,
|
|
92
|
-
"help": "
|
|
104
|
+
"help": "兜底:小满没答出来的那次 askId(refused / failed)"
|
|
93
105
|
},
|
|
94
106
|
{
|
|
95
107
|
"name": "template",
|
|
96
108
|
"type": "string",
|
|
97
109
|
"required": false,
|
|
98
|
-
"help": "
|
|
110
|
+
"help": "兜底:使用的指标模板编号(如 DP-PUR-002),没用不填"
|
|
99
111
|
},
|
|
100
112
|
{
|
|
101
113
|
"name": "rows",
|
|
102
114
|
"type": "int",
|
|
103
115
|
"default": 30,
|
|
104
116
|
"required": false,
|
|
105
|
-
"help": "
|
|
117
|
+
"help": "每个结果最多返回几行给 AI(1~200);更多的写到 --output 目录的文件里"
|
|
106
118
|
},
|
|
107
119
|
{
|
|
108
120
|
"name": "output",
|
package/clis/linkai/commands.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var te=Object.defineProperty;var r=(e,t)=>te(e,"name",{value:t,configurable:!0});import b from"node:fs";import w from"node:path";import ae from"node:os";import{fileURLToPath as ne}from"node:url";import{cli as Q,Strategy as se}from"@linkaicli/linkai-cli/registry";import{ArgumentError as h,AuthRequiredError as oe,CliError as k}from"@linkaicli/linkai-cli/errors";import*as a from"./core.js";import*as W from"./build.js";import*as U from"./deliverable.js";import*as G from"./fill-card.js";import*as S from"./collab.js";import*as _ from"./reuse.js";import*as C from"./dictionary.js";import*as f from"@linkaicli/linkai-cli/oplog";const l={site:a.SITE_ID,browser:!1,defaultFormat:"json"},re={site:a.SITE_ID,domain:"bi.linkmore.com",strategy:se.UI,browser:!0,siteSession:"persistent",navigateBefore:!1,defaultFormat:"json"},s=r((e,t,n=!0)=>({name:e,type:"string",required:n,help:t}),"str"),P=r((e,t)=>t?e.filter(n=>String(n.name).includes(t)):e,"filter"),x=r(e=>JSON.parse(a.readInputFile(e)),"json"),V=r(()=>new Date().toISOString().replace(/[-:]/g,"").replace("T","-").slice(0,15),"stamp"),R=r((e,...t)=>e||a.artifactsDir(...t),"outPath");function d(e){const t=e.func,n=r(async i=>{try{return await i()}catch(u){throw u instanceof a.LoginRequiredError?new oe("bi.linkmore.com",u.message,"请用户执行 linkai login,在浏览器完成领谋授权"):u instanceof a.TenantMismatchError?new k("TENANT_MISMATCH",u.message,"先用 linkai login --tenant <租户名> 切换到方案规范指定的租户,再用 linkai status 确认;无法切换就停止并告诉用户"):u instanceof a.NetworkError?new k("NETWORK_ERROR",u.message,"网络暂时不通(已自动重试):等 1~2 分钟或请用户检查网络 / 代理后再执行;只读命令可直接重试,写命令先用只读命令确认结果再决定是否重做",1):(u instanceof a.ApiError||u instanceof a.KindError)&&u.kind?new k(u.kind,u.message,ie[u.kind],1):u instanceof S.CollabConflictError?new k("COLLAB_CONFLICT",u.message,"别的执行者正在改这个对象:先 linkai collab-status --board <任务板> 看分工,改做别的任务,或等对方释放;不要绕过",1):u}},"guard"),o=r(i=>{e.access==="write"&&!e.name.startsWith("collab-")&&S.assertWritable(S.objectsForCommand(e.name,i||{},x))},"checkCollab"),c=e.browser?async(i,u)=>n(()=>(o(u),t(i,u))):async i=>n(()=>(o(i),t(i)));return Q({...e,func:c})}r(d,"site");const ie={SQL_INVALID:"SQL 里有视图中不存在的字段 / 表或语法错误:用 linkai kb-object / ds-tables 核对字段名后改 SQL,先 sql-check 通过再建或改数据集",IN_USE:"对象仍被引用:先用 linkai ds-refs --ds <数据集> 找到引用它的卡片,确认后先删卡片,再删数据集",NOT_FOUND:"对象不存在或已删除:用 plan-list / card-list / ds-list 重新确认精确 id,不要重复执行写命令",FORBIDDEN:"当前用户没有该对象的权限(如私有数据集):换用自己可见的对象,或请对象负责人共享",DUPLICATE_NAME:"同名对象已存在——很可能上一次已经创建成功:先用 card-list / ds-list / plan-list --search <名称> 确认并复用,不要换个名字或重复执行同样的创建",SQL_READONLY:"数据集只能是单条只读查询(SELECT 或 WITH … SELECT),不能改数据或建表;本 CLI 没有操作数据库的能力",SQL_TABLE_NOT_ALLOWED:"只能查询 linkai ds-tables 列出的租户视图:换用清单里的视图;确实需要新视图时停下,告诉用户联系领谋数据团队开放",READ_ONLY:"系统报表 / 系统方案(资源包下发到本租户的)只读:不能在本租户改,要到来源租户修改后重新下发;不要换个接口重复尝试,告诉用户",BAD_REQUEST:"请求体结构不对:先查 references/api-catalog.md,或回读一条现有数据照抄结构;优先用已封装的命令,不要反复试",KNOWLEDGE_INVALID:"方案知识文件不完整:按 message 补齐(每张线上业务卡片一段、cardId 必填、修改方案必须写 changeSummary),格式见 templates/report-spec/plan-knowledge.example.yaml",ASK_UNAVAILABLE:"问数服务还没有上线:告诉用户这个功能暂不可用;需要数据可以先搭看板或在领谋界面查看",CONFIG_INVALID:"配置违反领谋界面的保存规则(与在界面上点保存时的校验一致):按 message 逐条修正后再保存;规则见知识库 references/card.md、filters.md",FILE_NOT_FOUND:"文件路径不对:确认文件已写出、路径相对当前目录;不要原样重复执行"},Se=d({...l,name:"login",access:"write",description:"首次打开领谋 OAuth 授权页;同账号切换租户复用授权,切换账号需显式 --switch-account",args:[s("tenant","目标租户(名称、编码或 id);已有授权时无需再打开浏览器",!1),{name:"timeout",type:"int",default:300,help:"等待浏览器完成 OAuth 授权的秒数"},{name:"no-browser",type:"boolean",default:!1,help:"只打印授权地址,不自动打开系统浏览器"},{name:"switch-account",type:"boolean",default:!1,help:"用户确认切换领谋账号后,重新打开 OAuth 授权页"}],func:r(async e=>{const t=Number(e.timeout||300);if(!Number.isInteger(t)||t<1||t>1800)throw new h("timeout 必须是 1-1800 秒");if(!e["switch-account"]){let o=null;try{o=await a.ensureSession()}catch(c){if(!(c instanceof a.LoginRequiredError))throw c}if(o){const c=e.tenant?await a.switchTenant(o,e.tenant):{session:o,switched:!1};return c.switched&&(C.resetReads("switch-tenant"),a.clearShortCache()),{login:c.switched?"oauth_tenant_switch":"oauth_existing",autoRelogin:!0,...a.spaceOf(c.session)}}}C.resetReads("login"),a.clearShortCache();const n=await a.oauthLogin({tenant:e.tenant,timeoutSeconds:t,...e["no-browser"]?{browser:r(async()=>!1,"browser")}:{},switchAccount:e["switch-account"]});return{login:"oauth",autoRelogin:!0,...a.spaceOf(n)}},"func")}),Ie=Q({...l,name:"logout",access:"write",description:"退出登录:撤销 OAuth 会话并删除本机登录信息",args:[],func:r(async()=>{const e=await a.oauthLogout();return a.forgetCredentials(),a.clearSession(),{logout:!0,oauthRevoked:e}},"func")}),Ce=d({...l,name:"status",access:"read",description:"查看登录状态、当前租户、是否自动续登、数据源与可用表数量",args:[],func:r(async()=>{const e=await a.ensureSession(),t=a.tokenPayload(e.token),n=await a.ctx(),o=await a.allowedTables();return{loggedIn:!0,...a.spaceOf(e),autoRelogin:!0,authMode:"oauth",tokenHoursLeft:t.exp?Math.round((t.exp*1e3-Date.now())/36e4)/10:null,dataSourceId:n.dataSourceId,dataSources:n.dataSources,tables:o.size,...ce()}},"func")});function H(e){const t=e.status==="interrupted"&&e.reason==="build-incomplete"?"用同一个 spec 再执行 build --execute 会断点续跑;不再继续就清理试搭对象后 flow-end --status abandoned":e.status==="interrupted"&&e.reason==="outcome-unknown"?"上次写操作结果未知:先用只读命令回读确认现状,不要直接重复执行写命令":`继续做就在命令上加 --flow ${e.id};不再继续就 linkai flow-end --flow ${e.id} --status abandoned`;return{flow:e.id,goal:e.goal,type:e.type,plan:e.plan,source:e.source,status:e.status,reason:e.reason,lastActivity:new Date(e.lastActivity).toISOString(),lastCommand:e.lastCommand,lastErrorCode:e.lastErrorCode,build:e.build,next:t}}r(H,"describeFlow");function ce(){try{const e=f.unfinishedFlows().filter(t=>t.id!==f.currentFlow()?.id).slice(0,5).map(H);return e.length?{unfinishedFlows:e}:{}}catch{return{}}}r(ce,"unfinishedFlowsHint");const xe=d({...l,name:"tenants",access:"read",description:"查看可用租户、当前租户及下一步;用户完成 OAuth 授权后执行",args:[],func:r(async()=>{const e=await a.ensureSession(),t=await a.tenantList(e),n=t.find(i=>i.tenantId===e.tenantId),o=t.map(i=>({name:i.name,tenantId:i.tenantId,current:i.tenantId===e.tenantId}));let c;return t.length===1?c=`只有一个租户,当前租户是「${t[0].name}」。`:c=`当前租户是「${n?.name||""}」。同账号可直接执行 linkai login --tenant <租户名> 切换,无需重新授权。`,{tenantCount:t.length,single:t.length===1,current:n?.name||a.spaceOf(e).tenantName,tenants:o,next:c}},"func")}),Le=d({...l,name:"kinds",access:"read",defaultFormat:"table",description:"列出 74 种图表类型、字段建议与可用状态",args:[],func:r(async()=>a.kinds(),"func")}),le={answered:"回答用户:给出数字,并写明口径(用到的视图 / 字段见 caliber)、时间范围和「数据截至」(dataAsOf);结果被截断时说明只看了部分行",empty:"告诉用户「没有查到符合条件的数据」,不要说成 0;可以确认时间范围或条件是否太窄",rejected:"按 message 修改(换用可用视图、去掉分页偏移、改为汇总等);不要换个写法绕过安全规则",estimate_rejected:"预计扫描量过大:加上时间范围或筛选条件,或改为汇总后再查",failed:"按 message 里的数据库报错改 SQL(用 kb-object 核对字段名)后再查",timeout:"查询超时:缩小时间范围、加筛选条件或改为汇总后再查"},Ee=d({...l,name:"ask",access:"read",description:"问数:执行按数据字典口径写好的只读 SQL(1~3 条),返回数据、口径和数据截至时间;按当前用户的数据权限执行,有频率和行数限制",args:[s("question","用户的问题原文(原样传入,用于日志与统计)"),s("sql","SQL 文件路径,1~3 个,逗号分隔(一句话里有几个子问题就几条)"),s("keywords","从问题里提炼的关键词,逗号分隔,如「采购数量,入库数量,今天」",!1),s("template","使用的指标模板编号(如 DP-PUR-002),没用不填",!1),{name:"rows",type:"int",default:30,help:"每条结果最多返回几行(1~200);更多的写到 --output 目录的文件里"},s("output","完整结果保存目录(默认 ~/.linkai/artifacts/linkai/ask)",!1)],func:r(async e=>{const t=String(e.question||"").trim();if(!t)throw new h("--question 必填:传用户的问题原文");if(t.length>2e3)throw new h("--question 最长 2000 字");const n=String(e.sql||"").split(/[,,]/).map(m=>m.trim()).filter(Boolean);if(!n.length||n.length>3)throw new h("--sql 需要 1~3 个 SQL 文件,逗号分隔");const o=String(e.keywords||"").split(/[,,、]/).map(m=>m.trim()).filter(Boolean).slice(0,20),c=Math.min(200,Math.max(1,Number(e.rows)||30)),i=[];for(const[m,$]of n.entries()){const O=a.readInputFile($);if(a.hasOffset(O))throw new k("ASK_REJECTED",`第 ${m+1} 条 SQL 带分页偏移(OFFSET / LIMIT m,n):问数不支持翻页`,"改为汇总,或加时间范围 / 筛选条件缩小范围;需要完整明细请联系管理员导出",1);i.push(await a.localCheckSql(O))}await q({enforce:!0});const u=await a.ensureSession(),I=await a.ctx(),p=f.currentCaller(),y=f.currentFlow()?.id??null,g=await a.askQuery({question:t,dataSourceId:I.dataSourceId,queries:i.map(m=>({sql:m})),keywords:o,templateId:e.template||null,knowledgeRefs:C.recentRefs({tenantId:u.tenantId,flowId:y}),agent:{name:p.agent,version:p.toolVersion,model:p.model,sessionId:p.toolSessionId,flowId:y,runId:f.currentRunId(),cliVersion:p.cliVersion}}),A=g?.results||[];if(f.emit("ask",{requestId:g?.requestId??null,queries:i.length,rateLimited:!!g?.rateLimited,results:A.map(m=>({askId:m.askId,status:m.status,rows:m.rowCount,cached:m.cached}))}),g?.rateLimited)throw new k("RATE_LIMITED",g.message||"问数超过频率或数据量限制",`${g.retryAfterSeconds??60} 秒后再试,告诉用户原因;不要循环重试、换账号或拆分查询绕过。需要大量数据请改为汇总`,1);const D=R(e.output,"ask");return{requestId:g.requestId,results:A.map(m=>{const $=m.rows||[];let O=null;return $.length>c&&(b.mkdirSync(D,{recursive:!0}),O=a.writeJsonFile(w.join(D,`${m.askId}.json`),{question:t,columns:m.columns,rows:$})),{askId:m.askId,status:m.status,message:m.message??null,columns:m.columns,rows:$.slice(0,c),rowCount:m.rowCount,rowLimit:m.rowLimit,truncated:m.truncated,...O?{file:O,shownRows:c}:{},dataAsOf:m.dataAsOf??null,caliber:m.explain??null,cached:m.cached,next:le[m.status]??null}}),quota:g.quota??null}},"func")}),Ae=d({...l,name:"sql-check",access:"read",defaultFormat:"table",description:"校验数据集 SQL(只读、可用表)并列出字段;多个文件用逗号分隔一次校验(省去多次启动)",args:[s("file","SQL 文件路径;多个用逗号分隔,如 sql/a.sql,sql/b.sql")],func:r(async e=>{const t=String(e.file).split(/[,,]/).map(i=>i.trim()).filter(Boolean),n=r(i=>i.map(u=>({sort:u.sort_num,field:u.field_name,type:u.field_type,dbType:u.type_name})),"fields"),o=r(async()=>{try{await q({enforce:!1})}catch(i){if(i instanceof a.LoginRequiredError)throw i}},"dictNotice");if(t.length===1){de();const i=a.readInputFile(t[0]);return a.precheckSql(i),await o(),n(await a.checkSql(i))}await o();const c=[];for(const i of t)try{c.push({file:i,ok:!0,fields:n(await a.checkSql(a.readInputFile(i)))})}catch(u){if(u instanceof a.LoginRequiredError)throw u;c.push({file:i,ok:!1,error:String(u.message).slice(0,500)})}return{total:t.length,failed:c.filter(i=>!i.ok).length,results:c}},"func")});function de(){try{const e=w.join(ae.homedir(),".linkai","oplog","hints.json");let t={};try{t=JSON.parse(b.readFileSync(e,"utf8"))||{}}catch{}const n=Date.now(),o=[...(t.sqlCheck||[]).filter(c=>n-c<5*6e4),n];t.sqlCheck=o.slice(-10),o.length>=3&&n-(t.sqlCheckNoticeAt||0)>10*6e4&&(t.sqlCheckNoticeAt=n,f.addNotice("最近几分钟在逐个校验 SQL:可以一次校验多个文件,如 linkai sql-check --file sql/a.sql,sql/b.sql,sql/c.sql(每条命令都有启动开销,逐个跑更慢),结果按文件分别列出。","sql-check-batch")),b.mkdirSync(w.dirname(e),{recursive:!0}),b.writeFileSync(e,JSON.stringify(t),{mode:384})}catch{}}r(de,"nudgeBatchSqlCheck");const De=d({...l,name:"fmt",access:"read",defaultFormat:"plain",description:"把 SQL 格式化为易读的多行文本",args:[s("file","SQL 文件路径")],func:r(async e=>a.formatSql(a.readInputFile(e.file)),"func")}),Oe=d({...l,name:"ds-list",access:"read",defaultFormat:"table",description:"列出当前租户的数据集",args:[s("search","按名称关键字过滤(可选)",!1)],func:r(async e=>P(await a.dsList(),e.search),"func")}),Fe=d({...l,name:"ds-tables",access:"read",defaultFormat:"table",description:"列出数据集 SQL 可以使用的表(视图)",args:[s("search","按表名关键字过滤(可选)",!1)],func:r(async e=>[...await a.allowedTables()].sort().filter(t=>!e.search||t.includes(String(e.search).toLowerCase())).map(t=>({table:t})),"func")}),Te=d({...l,name:"ds-fields",access:"read",defaultFormat:"table",description:"列出数据集字段(列名就是卡片引用的映射键)",args:[s("ds","数据集的精确 id")],func:r(async e=>(await a.dsFields(e.ds)).map(t=>({sort:t.sortNum,column:t.tableFieldComment,type:t.tableFieldType,argumentType:t.argumentType})),"func")}),qe=d({...l,name:"ds-sql",access:"read",description:"把数据集当前保存的 SQL 读出到文件",args:[s("ds","数据集的精确 id"),s("output","输出 SQL 路径(默认 ~/.linkai/artifacts/linkai/sql/<dsId>.sql)",!1)],func:r(async e=>{const t=await a.dsDetail(e.ds),n=a.writeJsonFile(R(e.output,"sql",`${e.ds}.sql`),t.sql||"");return{name:t.name,lines:(t.sql||"").split(`
|
|
2
|
-
`).length,output:n}},"func")}),Ne=d({...l,name:"ds-refs",access:"read",defaultFormat:"table",description:"列出引用该数据集的卡片",args:[s("ds","数据集的精确 id")],func:r(async e=>a.dsRefs(e.ds),"func")}),$e=d({...l,name:"ds-match",access:"read",description:"复用查重:在当前租户里找与这段 SQL 完全一样或相似的数据集(建数据集前先查,不含 zz- 试搭对象)",args:[s("file","SQL 文件路径"),{name:"top",type:"int",default:3,help:"相似候选最多几个"}],func:r(async e=>{const t=_.matchDataset(a.readInputFile(e.file),await _.datasetIndex(),{top:Number(e.top)||3});return{...t,next:t.identical.length?`有完全一样的数据集,直接复用 ${t.identical[0].dsId}(build spec 的 datasets 写 id,或 spec 加 reuse: auto)`:t.similar.length?"有相似数据集:比较 columns 与口径后决定复用、在原数据集上调整(先 ds-refs 看影响),还是新建":"没有可复用的数据集,可以新建"}},"func")}),_e=d({...l,name:"ds-create",access:"write",description:"新建 SQL 数据集(先做只读与可用表校验,并格式化 SQL);当前租户已有完全一样的数据集时拒绝,除非 --allow-duplicate",args:[s("name","数据集名称(租户内唯一;试验用 zz- 前缀)"),s("file","SQL 文件路径"),{name:"allow-duplicate",type:"boolean",default:!1,help:"已有完全一样的数据集仍要新建(需说明理由,如要独立演进口径)"}],func:r(async e=>{const t=a.readInputFile(e.file);a.precheckSql(t),await q({enforce:!0});let n=null;try{n=_.matchDataset(t,await _.datasetIndex())}catch(i){if(i instanceof a.LoginRequiredError)throw i}if(n?.identical.length&&!e["allow-duplicate"])throw new k("DUPLICATE_DATASET",`当前租户已有完全一样的数据集:${n.identical.map(i=>`「${i.name}」(${i.dsId})`).join("、")}`,"直接复用该数据集(build spec 的 datasets 写 id,或 spec 加 reuse: auto);确需独立一份才加 --allow-duplicate",1);const o=await a.dsSave({name:e.name,sql:t}),c=await a.dsFields(o);return{dsId:o,name:e.name,fields:c.map(i=>i.tableFieldComment),...n?.similar.length?{similar:n.similar,next:"已有相似数据集:确认口径确实不同;如可合并,后续改为复用"}:{}}},"func")}),Re=d({...l,name:"ds-update",access:"write",description:"原地更新数据集(id 不变,卡片映射不断)",args:[s("ds","数据集的精确 id"),s("name","数据集名称"),s("file","SQL 文件路径")],func:r(async e=>{const t=a.readInputFile(e.file);a.precheckSql(t);const n=await a.dsDetail(e.ds);if(n?.name===e.name&&n?.sql&&_.sameSql(t,n.sql)){const i=await a.dsFields(e.ds);return{dsId:e.ds,sameId:!0,unchanged:!0,fields:i.map(u=>u.tableFieldComment),next:"SQL 和名称与线上一致,未重复保存;不要再次执行同样的更新"}}await q({enforce:!0});const o=await a.dsSave({id:e.ds,name:e.name,sql:t,current:n}),c=await a.dsFields(o);return{dsId:o,sameId:o===e.ds,fields:c.map(i=>i.tableFieldComment)}},"func")}),je=d({...l,name:"card-list",access:"read",defaultFormat:"table",description:"列出当前租户的卡片",args:[s("search","按名称关键字过滤(可选)",!1)],func:r(async e=>P(await a.cardList(),e.search),"func")}),Be=d({...l,name:"card-get",access:"read",description:"把卡片完整配置保存为 JSON 文件(修改前的基线,或 card-save 的输入)",args:[s("card","卡片的精确 id"),s("output","输出 JSON 路径(默认 ~/.linkai/artifacts/linkai/cards/<cardId>.json)",!1)],func:r(async e=>{const t=await a.cardDetail(e.card),n=a.writeJsonFile(R(e.output,"cards",`${e.card}.json`),t);return{name:t.name,fields:(t.indexBlockDataSettingDtos||[]).length,output:n}},"func")}),ve=d({...l,name:"card-save",access:"write",description:"保存完整的卡片配置 JSON(唯一能让配置生效的保存方式)",args:[s("file","修改后的卡片 JSON 文件路径")],func:r(async e=>{const t=x(e.file);if(!t.id)throw new Error("Card payload must contain id");const n=typeof t.chartSetting=="string"?JSON.parse(t.chartSetting||"{}"):t.chartSetting||{};t.chartSetting=JSON.stringify(n),t.chartType=t.chartType||n.id,t.chartParentType=t.chartParentType||n.parentId,await a.post("/index-block/update-new",t),String(t.chartType||n.id)==="agtable"&&n.config?.fitTableWidth!==!0&&f.addNotice("这张 AG 表格没有开启自适应宽度(chartSetting.config.fitTableWidth)。按 BI 操作规则应设为 true,列宽 agColumnWidths 只作为比例(如等分统一 50);用户明确要求固定宽度时除外。","ag-fit-width");const o=await a.cardDetail(t.id);return{cardId:t.id,name:o.name,fields:(o.indexBlockDataSettingDtos||[]).length}},"func")}),Pe=d({...l,name:"fill-write",access:"write",description:"写自由填报数据:一条记录一个元素、字段一次带齐(默认只校验并预览,加 --execute 才写入)",args:[s("card","填报卡片的精确 id(usedIndexBlockId)"),s("file",'记录 JSON 数组文件:[{"字段名":"值",…,"_id":"已有记录ID(可选)"}]'),s("key","填报块:fillfieldid(第一个块)、fillfieldid1(第二个块)…",!1),{name:"batch",type:"int",default:100,help:"每次请求的记录数(1-500)"},{name:"execute",type:"boolean",default:!1,help:"确认后真正写入"}],func:r(async e=>{const t=Number(e.batch||100);if(!Number.isInteger(t)||t<1||t>500)throw new h("batch 必须是 1-500");await a.assertCardInTenant(e.card);const n=await a.cardDetail(e.card);if(n?.isFreeFill===!1)throw new Error("该卡片不是自由填报卡片;fill-write 只支持自由填报");const o=G.fillBlockOf(n,e.key||"fillfieldid"),c=G.buildFillValues(o,x(e.file)),i=[];for(let p=0;p<c.length;p+=t)i.push(c.slice(p,p+t));const u={cardId:e.card,name:n.name,key:o.key,fields:o.fields,uniqueFields:o.uniqueFields,records:c.length,updates:c.filter(p=>p.value).length,batches:i.length,sample:c.slice(0,2)};if(!e.execute)return{...u,executed:!1,next:"核对字段与记录数后,用户确认再加 --execute"};const I=[];for(const[p,y]of i.entries())try{await a.post("/index-block/new-fill-field",{usedIndexBlockId:e.card,fillValues:y}),I.push(y.length)}catch(g){throw new Error(`第 ${p+1}/${i.length} 批写入失败(已成功 ${I.length} 批、${I.reduce((A,D)=>A+D,0)} 条):${g.message}。先回读填报表按依据字段核对,再只补未成功的部分,不要整批重跑`)}return{...u,executed:!0,written:I.reduce((p,y)=>p+y,0),next:"回读填报表本身:行数、依据字段无空值无重复、抽查整行字段完整;再核对看板"}},"func")}),Ke=d({...l,name:"card-chart",access:"write",description:"按图表类型新建或覆盖图表卡片(指定维度、数值、系列列)",args:[s("ds","数据集的精确 id"),s("name","卡片名称"),s("kind","图表类型,如 column/stacked(linkai kinds 查看全部)"),s("dim","维度列名,多个用逗号分隔",!1),s("meas","数值列名,多个用逗号分隔"),s("series","系列列名(可选)",!1),s("card","要覆盖的已有卡片 id(可选)",!1)],func:r(async e=>a.cardChart({dsId:e.ds,name:e.name,kind:e.kind,dim:e.dim,meas:e.meas,series:e.series,id:e.card}),"func")}),Qe=d({...l,name:"card-meter",access:"write",description:"新建或覆盖单数值指标卡(meter/list)",args:[s("ds","数据集的精确 id"),s("name","卡片名称"),s("col","数值列名"),s("subtitle","副标题",!1),s("color","数值颜色(十六进制,如 #E8611C)",!1),s("card","要覆盖的已有卡片 id(可选)",!1)],func:r(async e=>a.cardMeter({dsId:e.ds,name:e.name,col:e.col,subtitle:e.subtitle,color:e.color||void 0,id:e.card}),"func")}),Ue=d({...l,name:"card-table",access:"write",description:"按 spec JSON 新建或覆盖 AG 明细表(见 references/card.md)",args:[s("ds","数据集的精确 id"),s("spec","spec JSON 文件路径"),s("card","要覆盖的已有卡片 id(可选)",!1)],func:r(async e=>a.cardTable({dsId:e.ds,spec:x(e.spec),id:e.card}),"func")}),Je=d({...l,name:"card-gantt",access:"write",description:"按 spec JSON 新建或覆盖甘特图卡片(见 references/gantt.md)",args:[s("ds","数据集的精确 id"),s("spec","spec JSON 文件路径"),s("card","要覆盖的已有卡片 id(可选)",!1)],func:r(async e=>a.cardGantt({dsId:e.ds,spec:x(e.spec),id:e.card}),"func")}),ze=d({...l,name:"card-sort",access:"write",description:'设置卡片持久排序("列:asc,列:desc"、.json 文件或 clear),并回读确认排序生效',args:[s("card","卡片的精确 id"),s("spec",'"waxis3:desc,ID:asc" | sort.json | clear')],func:r(async e=>{const t=/\.json$/i.test(String(e.spec))?x(e.spec):e.spec;return a.cardSort(e.card,Array.isArray(t)?t:t.sortField||t)},"func")}),Me=d({...l,name:"query",access:"read",description:"按页面方式查询卡片数据;返回状态码、报错信息、行数和实际执行的 SQL",args:[s("card","卡片的精确 id"),{name:"page-size",type:"int",default:20,help:"取数行数"},s("filters","筛选条件 JSON 文件路径",!1),s("output","数据行保存路径(不在屏幕上输出数据)",!1)],func:r(async e=>{const t=await a.queryCard({cardId:e.card,pageSize:e["page-size"],filters:e.filters?x(e.filters):[]});return e.output&&a.writeJsonFile(e.output,t.data),delete t.data,t},"func")}),We=d({...l,name:"plan-list",access:"read",defaultFormat:"table",description:"列出分析方案(看板)",args:[s("search","按名称关键字过滤(可选)",!1)],func:r(async e=>P(await a.planList(),e.search),"func")}),Ge=d({...l,name:"plan-get",access:"read",description:"把方案详情保存为 JSON 文件(含租户模板筛选器)",args:[s("plan","方案的精确 id"),s("output","输出 JSON 路径(默认 ~/.linkai/artifacts/linkai/plans/<planId>.json)",!1),{name:"personal",type:"boolean",default:!1,help:"读取自己的个人筛选器副本"}],func:r(async e=>{const t=await a.planDetail(e.plan,!e.personal),n=a.writeJsonFile(R(e.output,"plans",`${e.plan}.json`),t);return{planId:e.plan,cards:(t.pageCards||[]).map(o=>({pageCardId:o.id,name:o.indexBlock?.name})),output:n}},"func")}),Ve=d({...l,name:"plan-create",access:"write",description:"新建空的分析方案(看板)",args:[s("name","方案名称")],func:r(async e=>{const t=await a.post("/page-setting/new-create-empty-plan",{name:e.name,isPrivate:!1,pageType:0});return f.addNotice(`手工搭建方案:完成后必须用 linkai plan-knowledge --plan ${t} --mode create --file <知识文件> 交付方案知识库(铁律 2)。新建看板优先用 linkai build(自动生成知识库并验收)。`,"plan-knowledge"),{planId:t,name:e.name}},"func")}),He=d({...l,name:"plan-add",access:"write",description:"把卡片放到方案的指定位置;返回 pageCardId(绑定筛选器时使用)",args:[s("plan","方案的精确 id"),s("card","卡片的精确 id"),{name:"x",type:"int",default:0,help:"横向位置(格,0–23)"},{name:"y",type:"int",default:0,help:"纵向位置(格)"},{name:"w",type:"int",default:24,help:"宽度(格,满宽 24)"},{name:"h",type:"int",default:20,help:"高度(格)"}],func:r(async e=>a.planAdd({planId:e.plan,cardId:e.card,x:e.x,y:e.y,w:e.w,h:e.h}),"func")}),Ye=d({...l,name:"plan-filter",access:"write",description:"保存方案筛选器为租户模板(全量覆盖),并重置自己的个人副本",args:[s("plan","方案的精确 id"),s("file","筛选器请求体 JSON 文件路径")],func:r(async e=>a.planFilter(e.plan,x(e.file)),"func")}),Xe=d({...l,name:"report-list",access:"read",defaultFormat:"table",description:"列出数据报表(每个报表对应一张卡片)",args:[s("search","按名称关键字过滤(可选)",!1)],func:r(async e=>P(await a.reportList(),e.search),"func")}),Ze=d({...l,name:"report-card",access:"read",description:"查询卡片对应报表的 userPageCardId(重置报表筛选器时使用)",args:[s("card","卡片的精确 id")],func:r(async e=>{const t=await a.reportDetail(e.card);return{cardId:e.card,userPageCardId:t.id}},"func")}),et=d({...l,name:"report-publish",access:"write",description:"把卡片发布为数据报表",args:[s("card","卡片的精确 id"),s("name","报表名称")],func:r(async e=>{await a.post("/index-block/operate-report",{indexBlockId:e.card,name:e.name,tag:"",isPrivate:!1});const t=(await a.reportList()).filter(n=>n.cardId===e.card);return{cardId:e.card,published:t.length===1,url:`https://bi.linkmore.com/view/table?indexBlockId=${e.card}&status=view`}},"func")}),tt=d({...l,name:"reset-filter",access:"write",description:"丢弃自己的个人筛选器副本,按模板重建(方案或报表卡片)",args:[s("plan","方案 id(方案筛选器)",!1),s("page-card","报表 userPageCardId 或方案 pageCardId(卡片筛选器)",!1)],func:r(async e=>{if(!!e.plan==!!e["page-card"])throw new Error("Pass exactly one of --plan or --page-card");const t=e.plan?{userPageSettingId:e.plan,filterSourceType:4}:{userPageCardId:e["page-card"],filterSourceType:2};return{filters:(await a.post("/page-setting/reset-user-filter",t)||[]).map(o=>o.labelName)}},"func")}),J=w.resolve(w.dirname(ne(import.meta.url)),"..","..");try{a.setClientVersion(JSON.parse(b.readFileSync(w.join(J,"package.json"),"utf8")).version)}catch{}const j={knowledge:w.join(J,"knowledge"),templates:w.join(J,"templates")};function K(e,t=e){return b.existsSync(e)?b.readdirSync(e,{withFileTypes:!0}).flatMap(n=>{const o=w.join(e,n.name);return n.isDirectory()?K(o,t):n.name.startsWith(".")?[]:[w.relative(t,o)]}):[]}r(K,"walk");function z(e,t){const n=j[e],o=w.resolve(n,t);if(o!==n&&!o.startsWith(n+w.sep))throw new h("路径必须在 knowledge/ 或 templates/ 之内");if(!b.existsSync(o)||b.statSync(o).isDirectory())throw new h(`文件不存在:${e}/${t}`,"执行 linkai kb 查看可用文件");return b.readFileSync(o,"utf8")}r(z,"readLocal");function M(e){return new k("KB_UNAVAILABLE",`知识库服务暂不可用:${e}`,"稍后重试;持续出现请联系领谋",1)}r(M,"kbUnavailable");function F(e){if(!(e instanceof a.AiKbError))return e;const t={400:"AI_KB_BAD_REQUEST",403:"AI_KB_FORBIDDEN",404:"AI_KB_NOT_FOUND",409:"AI_KB_CONFLICT",503:"AI_KB_UNAVAILABLE"},n={404:"确认 source、对象或文档属于当前租户;数据字典不会回退本地内容",409:"知识元数据未就绪时先联系知识库维护方;文档持续变化时稍后重试",503:"稍后重试;数据字典没有本地兜底,持续出现请联系领谋"};return new k(t[e.code]||"AI_KB_ERROR",e.message,n[e.code],1)}r(F,"aiKbFailure");function L(e,t,n,o,c){const i=e==null||e===""?t:Number(e);if(!Number.isInteger(i)||i<o||i>c)throw new h(`${n} 必须是 ${o}~${c} 的整数`);return i}r(L,"intOption");async function B(e){const t=await a.ctx();if(String(e).toLowerCase()!==String(t.dataSourceId).toLowerCase())throw new k("AI_KB_SOURCE_MISMATCH",`数据字典 source ${e} 与当前数据集 source ${t.dataSourceId} 不一致`,"不要跨 source 使用元数据;先确认服务端已为当前数据源完成探库,或确认 CLI 的数据源配置",1)}r(B,"requireDatasetSource");function Y(){return K(w.join(j.knowledge,"report-builder")).map(e=>({file:`knowledge/report-builder/${e}`,title:"",summary:"",library:"bi-builder",scope:"public",source:"bundled-fallback"}))}r(Y,"localBiDocs");function ue(e){return{file:`knowledge/${e.canonicalPath}`,title:"",library:e.library,scope:e.scope,source:"bundled-fallback",content:z("knowledge",e.canonicalPath)}}r(ue,"localBiDoc");function pe(e,t){const n=Array.isArray(t)?t:null,o=n?"bi-builder":t?.library||(String(t?.file||"").startsWith("templates/")?"templates":String(t?.file||"").includes("browser/")?"cli-docs":"bi-builder");return f.emit("kb.read",n?{library:o,op:e,docs:n.length,sources:[...new Set(n.map(c=>c?.source).filter(Boolean))]}:{library:o,op:e,file:t?.file??null,source:t?.source??null,chars:typeof t?.content=="string"?t.content.length:void 0}),t}r(pe,"logKb");function T(e,t,n,o={}){if(f.emit("kb.read",{library:"data-dictionary",op:e,source:t.source??null,...o,...e!=="sources"?{cached:a.kbCacheHit()}:{}}),e!=="sources"&&t.source){const c=a.readSession()?.tenantId,i=f.currentFlow()?.id??null;C.recordRead({tenantId:c,source:t.source,flowId:i,op:e}),e==="doc"&&t.doc&&C.recordRef({tenantId:c,flowId:i,ref:`doc:${t.doc}${o.title?` ${o.title}`:""}`}),e==="object"&&o.object&&C.recordRef({tenantId:c,flowId:i,ref:`object:${o.object}`})}return n}r(T,"logDict");async function q({enforce:e}){const t=await a.ensureSession(),n=await a.ctx(),o=await C.dictionaryStatus({tenantId:t.tenantId,source:n.dataSourceId,flowId:f.currentFlow()?.id??null,fetchSources:r(()=>a.dictSourcesCached(),"fetchSources"),fetchDocs:r(async()=>(await a.dictDocsHead(n.dataSourceId)).items,"fetchDocs")});if(a.logGuard("dictionary-fresh",o.ok,{tenantId:t.tenantId,source:n.dataSourceId,reason:o.reason??o.skipped??null,readAt:o.readAt??null,dictUpdatedAt:o.dictUpdatedAt??null,enforce:e}),o.skipped&&f.addNotice(`数据字典:${o.skipped}。`,"dictionary-skipped"),!o.ok){if(e)throw new k("DICTIONARY_REQUIRED",o.reason,C.readHint(n.dataSourceId),1);f.addNotice(`数据字典:${o.reason}。正式写入(ds-create / ds-update / build --execute)前会被拒绝。${C.readHint(n.dataSourceId)}`,"dictionary-stale")}return o}r(q,"checkDictionary");const X=r((e,t,n)=>Array.isArray(e)?e.slice(0,t).map(n):[],"top"),at=d({...l,name:"kb",access:"read",description:"读取文件型 BI 搭建知识库(服务器优先、本地兜底);数据字典请使用 kb-sources / kb-search 等结构化命令",args:[s("file","文件路径,如 knowledge/report-builder/build-spec.md 或 templates/report-spec/report.example.yaml",!1),s("search","按关键字过滤目录",!1),s("library","文件型知识库:bi-builder;data-dictionary 请使用 kb-sources 等结构化命令;另预留 integration-flow | user",!1)],func:r(async e=>pe(e.file?"doc":"catalog",await(async()=>{const t=String(e.file||"").replace(/^\.?\//,"");if(t.startsWith("templates/"))return{file:t,source:"bundled",content:z("templates",t.slice(10))};if(t==="knowledge/browser/README.md"||t==="browser/README.md")return{file:"knowledge/browser/README.md",source:"bundled-cli",content:z("knowledge","browser/README.md")};const n=e.search?String(e.search):"",o=e.library?String(e.library):"";if(o&&!a.KB_LIBRARIES[o])throw new h(`未知知识库:${o}`);if(o==="data-dictionary"||/^(?:knowledge\/)?data-dictionary(?:\/|$)/.test(t))throw new k("KB_STRUCTURED_API","数据字典不再按 Markdown 文件路径读取","先执行 linkai kb-sources,再使用 kb-search / kb-catalog / kb-object / kb-docs / kb-doc",1);if(o&&!a.KB_LIBRARIES[o].enabled)throw new k("KB_NOT_IMPLEMENTED",`知识库 ${o} 已预留,当前版本尚未启用`,void 0,1);if(!t){const i=o?[o]:["bi-builder"],u=[];for(const p of i)try{const y=await a.kbCatalog(p);p==="bi-builder"&&!(y.docs||[]).length?u.push(...Y()):u.push(...(y.docs||[]).map(g=>{const A=a.KB_LIBRARIES[p].pathPrefix,D=a.kbRequest(g.path,p).path;return{file:`knowledge/${A}/${D}`,title:g.title||"",summary:g.summary||"",library:p,scope:a.KB_LIBRARIES[p].scope,source:"server"}}))}catch(y){if(p==="bi-builder"&&y instanceof a.KbUnavailableError)u.push(...Y());else throw y instanceof a.KbUnavailableError?M(p):y}const I=!o||o==="bi-builder"?K(j.templates).map(p=>({file:`templates/${p}`,title:"报表模版",summary:"",source:"bundled"})):[];return[...u,...I].filter(p=>!n||p.file.includes(n)||p.title.includes(n))}const c=a.kbRequest(t,o||void 0);try{const i=await a.kbDoc(t,o||void 0);return{file:`knowledge/${i.path}`,title:i.title||"",library:i.library,scope:i.scope,source:"server",content:i.content||""}}catch(i){if(c.library==="bi-builder"&&(i instanceof a.KbUnavailableError||i instanceof a.KbDocumentNotFoundError)){if(!b.existsSync(w.join(j.knowledge,"report-builder")))throw M(c.library);return ue(c)}throw i instanceof a.KbUnavailableError?M(c.library):i}})()),"func")}),nt=d({...l,name:"kb-sources",access:"read",defaultFormat:"table",description:"列出当前租户可读的数据字典数据源,并标记当前数据集所用 source;后续命令必须显式传该 ID",args:[],func:r(async()=>{try{const[e,t]=await Promise.all([a.dictSourcesCached(),a.ctx()]),n=e.map(c=>({...c,selectedForDatasets:String(c.source).toLowerCase()===String(t.dataSourceId).toLowerCase()})),o=n.some(c=>c.selectedForDatasets);return T("sources",{},null,{sources:n.map(c=>({source:c.source,name:c.name})),aligned:o}),{datasetSourceId:t.dataSourceId,aligned:o,sources:n,...o?{}:{warning:"数据字典没有返回当前数据集 source;不要跨 source 使用元数据,请先确认服务端探库或数据源配置"}}}catch(e){throw F(e)}},"func")}),st=d({...l,name:"kb-catalog",access:"read",description:"分页列出指定 source 的可用视图/对象元数据",args:[s("source","数据源 ID(来自 kb-sources,必须与后续数据集 source 一致)"),s("search","表名、中文名或说明关键字",!1),s("page","页码,默认 1",!1),s("page-size","每页 1~100,默认 30",!1)],func:r(async e=>{try{await B(e.source);const t={source:e.source,keyword:e.search,pageIndex:L(e.page,1,"page",1,2147483647),pageSize:L(e["page-size"],30,"page-size",1,100)},n=await a.kbCached(e.source,`catalog|${t.keyword??""}|${t.pageIndex}|${t.pageSize}`,()=>a.aiKbCatalog(t));return T("catalog",e,n,{keyword:e.search??null,total:n?.totalCount??null})}catch(t){throw F(t)}},"func")}),ot=d({...l,name:"kb-object",access:"read",description:"读取指定 source 中一个可见对象的字段与关联关系",args:[s("source","数据源 ID(来自 kb-sources)"),s("db","数据库/schema 名"),s("object","对象/视图名")],func:r(async e=>{try{await B(e.source);const t=await a.kbCached(e.source,`object|${String(e.db).toLowerCase()}.${String(e.object).toLowerCase()}`,()=>a.aiKbObject({source:e.source,db:e.db,object:e.object}));return T("object",e,t,{object:`${e.db}.${e.object}`})}catch(t){throw F(t)}},"func")}),rt=d({...l,name:"kb-docs",access:"read",description:"分页列出指定 source 的业务口径、枚举、关系、数据点等知识文档",args:[s("source","数据源 ID(来自 kb-sources)"),s("category","caliber | enum | relation | datapoint | other",!1),s("search","标题或正文关键字",!1),s("page","页码,默认 1",!1),s("page-size","每页 1~100,默认 30",!1)],func:r(async e=>{const t=e.category?String(e.category):void 0;if(t&&!["caliber","enum","relation","datapoint","other"].includes(t))throw new h("category 必须是 caliber、enum、relation、datapoint、other 之一");try{await B(e.source);const n={source:e.source,category:t,keyword:e.search,pageIndex:L(e.page,1,"page",1,2147483647),pageSize:L(e["page-size"],30,"page-size",1,100)},o=await a.kbCached(e.source,`docs|${t??""}|${n.keyword??""}|${n.pageIndex}|${n.pageSize}`,()=>a.aiKbDocs(n));return T("docs",e,o,{category:t??null,keyword:e.search??null,total:o?.totalCount??null})}catch(n){throw F(n)}},"func")}),it=d({...l,name:"kb-doc",access:"read",description:"一致性读取一篇完整数据字典文档;自动按 nextOffset 分页,版本变化时从头重读",args:[s("source","数据源 ID(来自 kb-sources)"),s("doc","文档 ID(来自 kb-docs 或 kb-search)"),s("page-size","单次正文字符数,1~20000,默认 20000",!1)],func:r(async e=>{try{await B(e.source);const t=await a.kbCached(e.source,`doc|${String(e.doc).toLowerCase()}`,()=>a.aiKbDoc({source:e.source,doc:e.doc,limit:L(e["page-size"],2e4,"page-size",1,2e4)}));return T("doc",e,t,{doc:e.doc,title:t?.title??null,category:t?.category??null,chars:t?.totalChars??null})}catch(t){throw F(t)}},"func")}),ct=d({...l,name:"kb-search",access:"read",description:"按自然语言检索指定 source 的候选视图与业务知识;相关度不是正确率,使用前继续回读对象/文档核对",args:[s("source","数据源 ID(来自 kb-sources)"),s("question","业务问题,最长 2000 字"),s("top-tables","最多返回表数,1~10,默认 5",!1),s("top-docs","最多返回文档数,1~10,默认 3",!1)],func:r(async e=>{try{await B(e.source);const t={source:e.source,question:e.question,topTables:L(e["top-tables"],5,"top-tables",1,10),topDocs:L(e["top-docs"],3,"top-docs",1,10)},n=await a.kbCached(e.source,`search|${t.topTables}|${t.topDocs}|${a.normalizeQuestion(t.question)}`,()=>a.aiKbSearch(t));return T("search",e,n,{question:String(e.question||"").slice(0,200),hitTables:X(n?.tables,10,o=>`${o.db}.${o.object}`),hitDocs:X(n?.docs,10,o=>({doc:o.doc,title:o.title??null,category:o.category??null})),emptyReason:n?.emptyReason??null})}catch(t){throw F(t)}},"func")}),lt=Q({...l,name:"template",access:"write",description:"复制报表 spec 与示例文件;SQL 示例必须由 AI 按用户需求、租户数据字典和确认口径重写,CLI 不生成业务 SQL",args:[s("output","目标目录(默认 ./linkai-report),需不存在或为空",!1)],func:r(async e=>{const t=w.resolve(String(e.output||"linkai-report"));if(b.existsSync(t)&&b.readdirSync(t).length)throw new h(`目录已存在且不为空:${t}`,"换一个 --output 目录");return b.cpSync(w.join(j.templates,"report-spec"),t,{recursive:!0}),{output:t,files:K(t),next:`修改 ${w.join(t,"report.example.yaml")} 与 sql/ 下的 SQL,然后 linkai build --spec ${w.join(t,"report.example.yaml")}`}},"func")}),dt=d({...l,name:"build",access:"write",description:"严格按 BI 公共知识库执行 AI 提供的 SQL 与报表 spec(CLI 不生成业务 SQL)。默认只校验并给出执行计划;--execute 才写入;含正式名还需 --confirm",args:[s("spec","报表 spec 文件(.yaml/.json),模版用 linkai template 复制"),{name:"execute",type:"boolean",default:!1,help:"真正写入(默认只校验并输出执行计划)"},{name:"confirm",type:"boolean",default:!1,help:"用户已明确授权使用正式名(非 zz- 前缀)"},{name:"no-style",type:"boolean",default:!1,help:"跳过页面样式步骤(暖色背景、卡位透明)"}],func:r(async e=>{const t=W.loadSpec(e.spec),n=(t.spec.datasets||[]).some(i=>!i.id&&i.sql),o=r(async()=>{const i=await a.ensureSession(),u=a.spaceOf(i);if(t.spec.tenant&&![u.tenantId,u.tenantName,u.tenantCode].includes(String(t.spec.tenant)))throw new h(`当前登录的租户是「${u.tenantName}」,与 spec.tenant「${t.spec.tenant}」不一致`,"确认目标租户后执行 linkai login --tenant <租户名>,或修改 spec.tenant");n&&await q({enforce:!0})},"beforeWrite"),c=await W.runBuild(t,{execute:!!e.execute,confirm:!!e.confirm,style:!e["no-style"],beforeWrite:o});if(!e.execute&&n&&c.ok!==!1)try{c.dictionary=await q({enforce:!1})}catch(i){if(i instanceof a.LoginRequiredError)c.dictionary={ok:!1,reason:"未登录"};else throw i}if(c.ok===!1&&(c.stage==="validate"||c.stage==="authorize"))throw new h(`build ${c.stage} failed:
|
|
1
|
+
var ce=Object.defineProperty;var r=(e,t)=>ce(e,"name",{value:t,configurable:!0});import y from"node:fs";import w from"node:path";import le from"node:os";import{fileURLToPath as de}from"node:url";import{cli as W,Strategy as ue}from"@linkaicli/linkai-cli/registry";import{ArgumentError as h,AuthRequiredError as pe,CliError as k}from"@linkaicli/linkai-cli/errors";import*as a from"./core.js";import*as Z from"./build.js";import*as G from"./deliverable.js";import*as ee from"./fill-card.js";import*as C from"./collab.js";import*as v from"./reuse.js";import*as x from"./dictionary.js";import*as f from"@linkaicli/linkai-cli/oplog";const l={site:a.SITE_ID,browser:!1,defaultFormat:"json"},fe={site:a.SITE_ID,domain:"bi.linkmore.com",strategy:ue.UI,browser:!0,siteSession:"persistent",navigateBefore:!1,defaultFormat:"json"},n=r((e,t,s=!0)=>({name:e,type:"string",required:s,help:t}),"str"),M=r((e,t)=>t?e.filter(s=>String(s.name).includes(t)):e,"filter"),A=r(e=>JSON.parse(a.readInputFile(e)),"json"),te=r(()=>new Date().toISOString().replace(/[-:]/g,"").replace("T","-").slice(0,15),"stamp"),P=r((e,...t)=>e||a.artifactsDir(...t),"outPath");function d(e){const t=e.func,s=r(async i=>{try{return await i()}catch(u){throw u instanceof a.LoginRequiredError?new pe("bi.linkmore.com",u.message,"请用户执行 linkai login,在浏览器完成领谋授权"):u instanceof a.TenantMismatchError?new k("TENANT_MISMATCH",u.message,"先用 linkai login --tenant <租户名> 切换到方案规范指定的租户,再用 linkai status 确认;无法切换就停止并告诉用户"):u instanceof a.NetworkError?new k("NETWORK_ERROR",u.message,"网络暂时不通(已自动重试):等 1~2 分钟或请用户检查网络 / 代理后再执行;只读命令可直接重试,写命令先用只读命令确认结果再决定是否重做",1):(u instanceof a.ApiError||u instanceof a.KindError)&&u.kind?new k(u.kind,u.message,me[u.kind],1):u instanceof C.CollabConflictError?new k("COLLAB_CONFLICT",u.message,"别的执行者正在改这个对象:先 linkai collab-status --board <任务板> 看分工,改做别的任务,或等对方释放;不要绕过",1):u}},"guard"),o=r(i=>{e.access==="write"&&!e.name.startsWith("collab-")&&C.assertWritable(C.objectsForCommand(e.name,i||{},A))},"checkCollab"),c=e.browser?async(i,u)=>s(()=>(o(u),t(i,u))):async i=>s(()=>(o(i),t(i)));return W({...e,func:c})}r(d,"site");const me={SQL_INVALID:"SQL 里有视图中不存在的字段 / 表或语法错误:用 linkai kb-object / ds-tables 核对字段名后改 SQL,先 sql-check 通过再建或改数据集",IN_USE:"对象仍被引用:先用 linkai ds-refs --ds <数据集> 找到引用它的卡片,确认后先删卡片,再删数据集",NOT_FOUND:"对象不存在或已删除:用 plan-list / card-list / ds-list 重新确认精确 id,不要重复执行写命令",FORBIDDEN:"当前用户没有该对象的权限(如私有数据集):换用自己可见的对象,或请对象负责人共享",DUPLICATE_NAME:"同名对象已存在——很可能上一次已经创建成功:先用 card-list / ds-list / plan-list --search <名称> 确认并复用,不要换个名字或重复执行同样的创建",SQL_READONLY:"数据集只能是单条只读查询(SELECT 或 WITH … SELECT),不能改数据或建表;本 CLI 没有操作数据库的能力",SQL_TABLE_NOT_ALLOWED:"只能查询 linkai ds-tables 列出的租户视图:换用清单里的视图;确实需要新视图时停下,告诉用户联系领谋数据团队开放",READ_ONLY:"这是领谋下发的系统分析方案 / 系统报表(只读):不能通过 CLI 修改(卡片、筛选器、排序、删除都不行),不要重试或换个接口再试。停下来告诉用户:如需按本租户调整,请在领谋网页上把它另存为本租户的分析方案,再对新方案操作",SYSTEM_PLAN_READONLY:"这是领谋下发的系统分析方案(只读):可以读取方案知识,但不能写入或修改。停下来告诉用户:如需按本租户调整,请在领谋网页上把它另存为本租户的分析方案,再对新方案操作",CLI_FORBIDDEN:"LinkAI CLI 不能调用这个接口:把 message 原样告诉用户,不要重试、不要换路径再试;问数请用 linkai ask,口径补充或纠正请用户在领谋网页上维护",BAD_REQUEST:"请求体结构不对:先查 references/api-catalog.md,或回读一条现有数据照抄结构;优先用已封装的命令,不要反复试",KNOWLEDGE_INVALID:"方案知识文件不完整:按 message 补齐(每张线上业务卡片一段、cardId 必填、修改方案必须写 changeSummary),格式见 templates/report-spec/plan-knowledge.example.yaml",ASK_UNAVAILABLE:"问数服务还没有上线:告诉用户这个功能暂不可用;需要数据可以先搭看板或在领谋界面查看",CONFIG_INVALID:"配置违反领谋界面的保存规则(与在界面上点保存时的校验一致):按 message 逐条修正后再保存;规则见知识库 references/card.md、filters.md",FILE_NOT_FOUND:"文件路径不对:确认文件已写出、路径相对当前目录;不要原样重复执行"},De=d({...l,name:"login",access:"write",description:"首次打开领谋 OAuth 授权页;同账号切换租户复用授权,切换账号需显式 --switch-account",args:[n("tenant","目标租户(名称、编码或 id);已有授权时无需再打开浏览器",!1),{name:"timeout",type:"int",default:300,help:"等待浏览器完成 OAuth 授权的秒数"},{name:"no-browser",type:"boolean",default:!1,help:"只打印授权地址,不自动打开系统浏览器"},{name:"switch-account",type:"boolean",default:!1,help:"用户确认切换领谋账号后,重新打开 OAuth 授权页"}],func:r(async e=>{const t=Number(e.timeout||300);if(!Number.isInteger(t)||t<1||t>1800)throw new h("timeout 必须是 1-1800 秒");if(!e["switch-account"]){let o=null;try{o=await a.ensureSession()}catch(c){if(!(c instanceof a.LoginRequiredError))throw c}if(o){const c=e.tenant?await a.switchTenant(o,e.tenant):{session:o,switched:!1};return c.switched&&(x.resetReads("switch-tenant"),a.clearShortCache()),{login:c.switched?"oauth_tenant_switch":"oauth_existing",autoRelogin:!0,...a.spaceOf(c.session)}}}x.resetReads("login"),a.clearShortCache();const s=await a.oauthLogin({tenant:e.tenant,timeoutSeconds:t,...e["no-browser"]?{browser:r(async()=>!1,"browser")}:{},switchAccount:e["switch-account"]});return{login:"oauth",autoRelogin:!0,...a.spaceOf(s)}},"func")}),Oe=W({...l,name:"logout",access:"write",description:"退出登录:撤销 OAuth 会话并删除本机登录信息",args:[],func:r(async()=>{const e=await a.oauthLogout();return a.forgetCredentials(),a.clearSession(),{logout:!0,oauthRevoked:e}},"func")}),Te=d({...l,name:"status",access:"read",description:"查看登录状态、当前租户、是否自动续登、数据源与可用表数量",args:[],func:r(async()=>{const e=await a.ensureSession(),t=a.tokenPayload(e.token),s=await a.ctx(),o=await a.allowedTables();return{loggedIn:!0,...a.spaceOf(e),autoRelogin:!0,authMode:"oauth",tokenHoursLeft:t.exp?Math.round((t.exp*1e3-Date.now())/36e4)/10:null,dataSourceId:s.dataSourceId,dataSources:s.dataSources,tables:o.size,...we()}},"func")});function ae(e){const t=e.status==="interrupted"&&e.reason==="build-incomplete"?"用同一个 spec 再执行 build --execute 会断点续跑;不再继续就清理试搭对象后 flow-end --status abandoned":e.status==="interrupted"&&e.reason==="outcome-unknown"?"上次写操作结果未知:先用只读命令回读确认现状,不要直接重复执行写命令":`继续做就在命令上加 --flow ${e.id};不再继续就 linkai flow-end --flow ${e.id} --status abandoned`;return{flow:e.id,goal:e.goal,type:e.type,plan:e.plan,source:e.source,status:e.status,reason:e.reason,lastActivity:new Date(e.lastActivity).toISOString(),lastCommand:e.lastCommand,lastErrorCode:e.lastErrorCode,build:e.build,next:t}}r(ae,"describeFlow");function we(){try{const e=f.unfinishedFlows().filter(t=>t.id!==f.currentFlow()?.id).slice(0,5).map(ae);return e.length?{unfinishedFlows:e}:{}}catch{return{}}}r(we,"unfinishedFlowsHint");const Fe=d({...l,name:"tenants",access:"read",description:"查看可用租户、当前租户及下一步;用户完成 OAuth 授权后执行",args:[],func:r(async()=>{const e=await a.ensureSession(),t=await a.tenantList(e),s=t.find(i=>i.tenantId===e.tenantId),o=t.map(i=>({name:i.name,tenantId:i.tenantId,current:i.tenantId===e.tenantId}));let c;return t.length===1?c=`只有一个租户,当前租户是「${t[0].name}」。`:c=`当前租户是「${s?.name||""}」。同账号可直接执行 linkai login --tenant <租户名> 切换,无需重新授权。`,{tenantCount:t.length,single:t.length===1,current:s?.name||a.spaceOf(e).tenantName,tenants:o,next:c}},"func")}),Ne=d({...l,name:"kinds",access:"read",defaultFormat:"table",description:"列出 74 种图表类型、字段建议与可用状态",args:[],func:r(async()=>a.kinds(),"func")}),be={answered:"回答用户:给出数字或小表,写明口径(数据源 dataSource、用到的视图 / 字段中文名见 caliber)、时间范围和「数据截至」(dataAsOf);结果被截断时说明只看了部分行。直接在对话里回答,不要另生成文件",empty:"告诉用户「没有查到符合条件的数据」,不要说成 0;可以确认时间范围或条件是否太窄",refused:"小满答不了:按 linkai-ask 技能「兜底」读当前租户数据字典、写只读 SQL,再执行 linkai ask --question <同一问题> --sql <文件> --fallback <本次 askId>;字典里也没有相关数据就如实告诉用户",failed:"小满出错:按 linkai-ask 技能「兜底」读当前租户数据字典、写只读 SQL,再执行 linkai ask --question <同一问题> --sql <文件> --fallback <本次 askId>",rejected:"按 message 告诉用户原因(翻页取数、同类问题反复换条件被暂停、排队已满等);不要换个问法或改用兜底绕过"},he={answered:"回答用户:给出数字,写明「由 AI 按数据字典口径查询」和口径(用到的视图 / 字段见 caliber)、时间范围、「数据截至」(dataAsOf);结果被截断时说明只看了部分行",empty:"告诉用户「没有查到符合条件的数据」,不要说成 0;可以确认时间范围或条件是否太窄",rejected:"按 message 修改(换用可用视图、去掉分页偏移、改为汇总等);不要换个写法绕过安全规则",estimate_rejected:"预计扫描量过大:加上时间范围或筛选条件,或改为汇总后再查",failed:"按 message 里的数据库报错改 SQL(用 kb-object 核对字段名)后再查",timeout:"查询超时:缩小时间范围、加筛选条件或改为汇总后再查"},ne=new Set(["refused","failed"]),$e=d({...l,name:"ask",access:"read",description:"问数:先把问题交给领谋服务端(与小满问数同一套)查询;小满答不了时,带 --sql 和 --fallback 由 AI 按数据字典写只读 SQL 兜底。按当前用户的数据权限执行,有频率和行数限制",args:[n("question","用户的问题(一次一个问题;有歧义先和用户确认,再把确认后的完整问题传入)"),n("keywords","从问题里提炼的关键词,逗号分隔,如「采购数量,入库数量,今天」(用于统计)",!1),n("source","指定数据源 id(可选,仅问小满时;不传由服务端按问题自动选)",!1),n("sql","兜底:AI 写的 SQL 文件,1~3 个逗号分隔(必须同时给 --fallback)",!1),n("fallback","兜底:小满没答出来的那次 askId(refused / failed)",!1),n("template","兜底:使用的指标模板编号(如 DP-PUR-002),没用不填",!1),{name:"rows",type:"int",default:30,help:"每个结果最多返回几行给 AI(1~200);更多的写到 --output 目录的文件里"},n("output","完整结果保存目录(默认 ~/.linkai/artifacts/linkai/ask)",!1)],func:r(async e=>{const t=String(e.question||"").trim();if(!t)throw new h("--question 必填:传用户的问题");if(t.length>2e3)throw new h("--question 最长 2000 字");const s=String(e.keywords||"").split(/[,,、]/).map(m=>m.trim()).filter(Boolean).slice(0,20),o=Math.min(200,Math.max(1,Number(e.rows)||30)),c=await a.ensureSession(),i=f.currentCaller(),u=f.currentFlow()?.id??null,S={name:i.agent,version:i.toolVersion,model:i.model,sessionId:i.toolSessionId,flowId:u,runId:f.currentRunId(),cliVersion:i.cliVersion},p=P(e.output,"ask"),g=r((m,B)=>{const q=m.rows||[];let j=null;return q.length>o&&(y.mkdirSync(p,{recursive:!0}),j=a.writeJsonFile(w.join(p,`${m.askId}.json`),{question:t,columns:m.columns,rows:q})),{askId:m.askId??null,status:m.status??null,message:m.message??null,columns:m.columns||[],rows:q.slice(0,o),rowCount:m.rowCount??0,rowLimit:m.rowLimit??null,truncated:!!m.truncated,...j?{file:j,shownRows:o}:{},dataAsOf:m.dataAsOf??null,caliber:m.explain??null,cached:!!m.cached,next:B[m.status]??null}},"shape"),E=r(m=>{if(m?.rateLimited)throw new k("RATE_LIMITED",m.message||"问数超过频率或数据量限制",`${m.retryAfterSeconds??60} 秒后再试,告诉用户原因;不要循环重试、换账号或拆分问题绕过。需要大量数据请改为汇总`,1)},"rateLimited");if(e.sql||e.fallback){if(!e.sql||!e.fallback)throw new h("兜底需要同时给 --sql(AI 写的 SQL 文件)和 --fallback(小满没答出来的那次 askId)");const m=a.xiaomanAttempt(e.fallback);if(!m||!ne.has(m.status))throw new k("ASK_XIAOMAN_FIRST",m?`这次小满已经回答(${m.status}),不需要兜底`:"没有找到这次小满问数记录(或已超过 2 小时)",'先执行 linkai ask --question "<问题>"(不带 --sql)问小满;只有返回 refused / failed 时,才带上它的 askId 兜底',1);const B=String(e.sql).split(/[,,]/).map(I=>I.trim()).filter(Boolean);if(!B.length||B.length>3)throw new h("--sql 需要 1~3 个 SQL 文件,逗号分隔");const q=[];for(const[I,z]of B.entries()){const D=a.readInputFile(z);if(a.hasOffset(D))throw new k("ASK_REJECTED",`第 ${I+1} 条 SQL 带分页偏移(OFFSET / LIMIT m,n):问数不支持翻页`,"改为汇总,或加时间范围 / 筛选条件缩小范围;需要完整明细请联系管理员导出",1);q.push(await a.localCheckSql(D))}await _({enforce:!0});for(const[I,z]of q.entries())try{await a.checkSql(z)}catch(D){throw D instanceof a.LoginRequiredError||D instanceof a.NetworkError?D:new k("SQL_INVALID",`第 ${I+1} 条 SQL 没有通过数据集运行校验:${String(D.message).slice(0,300)}`,"按报错核对视图与字段(kb-object),改好后再执行;不要绕过",1)}const j=await a.ctx(),F=await a.askQuery({question:t,dataSourceId:j.dataSourceId,queries:q.map(I=>({sql:I})),keywords:s,templateId:e.template||null,knowledgeRefs:[`xiaoman-ask:${e.fallback}`,...x.recentRefs({tenantId:c.tenantId,flowId:u})].slice(0,50),agent:S}),X=F?.results||[];return f.emit("ask",{requestId:F?.requestId??null,mode:"fallback",fallbackOf:e.fallback,queries:q.length,rateLimited:!!F?.rateLimited,results:X.map(I=>({askId:I.askId,status:I.status,rows:I.rowCount,cached:I.cached}))}),E(F),{mode:"fallback",fallbackOf:e.fallback,requestId:F.requestId,results:X.map(I=>g(I,he)),quota:F.quota??null}}const L=await a.askQuestion({question:t,keywords:s,dataSourceId:e.source||null,agent:S}),b=L?.results?.[0]||{};return f.emit("ask",{requestId:L?.requestId??null,mode:"question",rateLimited:!!L?.rateLimited,results:[{askId:b.askId??null,status:b.status??null,rows:b.rowCount??0,cached:!!b.cached,ms:b.elapsedMs??null}]}),E(L),b.askId&&a.recordXiaomanAttempt({askId:b.askId,status:b.status,question:t}),{mode:"question",...g(b,be),dataSource:b.dataSourceName??null,sql:b.sql??null,...ne.has(b.status)?{fallback:{allowed:!0,askId:b.askId}}:{},quota:L?.quota??null}},"func")}),_e=d({...l,name:"sql-check",access:"read",defaultFormat:"table",description:"校验数据集 SQL(只读、可用表)并列出字段;多个文件用逗号分隔一次校验(省去多次启动)",args:[n("file","SQL 文件路径;多个用逗号分隔,如 sql/a.sql,sql/b.sql")],func:r(async e=>{const t=String(e.file).split(/[,,]/).map(i=>i.trim()).filter(Boolean),s=r(i=>i.map(u=>({sort:u.sort_num,field:u.field_name,type:u.field_type,dbType:u.type_name})),"fields"),o=r(async()=>{try{await _({enforce:!1})}catch(i){if(i instanceof a.LoginRequiredError)throw i}},"dictNotice");if(t.length===1){ye();const i=a.readInputFile(t[0]);return a.precheckSql(i),await o(),s(await a.checkSql(i))}await o();const c=[];for(const i of t)try{c.push({file:i,ok:!0,fields:s(await a.checkSql(a.readInputFile(i)))})}catch(u){if(u instanceof a.LoginRequiredError)throw u;c.push({file:i,ok:!1,error:String(u.message).slice(0,500)})}return{total:t.length,failed:c.filter(i=>!i.ok).length,results:c}},"func")});function ye(){try{const e=w.join(le.homedir(),".linkai","oplog","hints.json");let t={};try{t=JSON.parse(y.readFileSync(e,"utf8"))||{}}catch{}const s=Date.now(),o=[...(t.sqlCheck||[]).filter(c=>s-c<5*6e4),s];t.sqlCheck=o.slice(-10),o.length>=3&&s-(t.sqlCheckNoticeAt||0)>10*6e4&&(t.sqlCheckNoticeAt=s,f.addNotice("最近几分钟在逐个校验 SQL:可以一次校验多个文件,如 linkai sql-check --file sql/a.sql,sql/b.sql,sql/c.sql(每条命令都有启动开销,逐个跑更慢),结果按文件分别列出。","sql-check-batch")),y.mkdirSync(w.dirname(e),{recursive:!0}),y.writeFileSync(e,JSON.stringify(t),{mode:384})}catch{}}r(ye,"nudgeBatchSqlCheck");const Re=d({...l,name:"fmt",access:"read",defaultFormat:"plain",description:"把 SQL 格式化为易读的多行文本",args:[n("file","SQL 文件路径")],func:r(async e=>a.formatSql(a.readInputFile(e.file)),"func")}),Be=d({...l,name:"ds-list",access:"read",defaultFormat:"table",description:"列出当前租户的数据集",args:[n("search","按名称关键字过滤(可选)",!1)],func:r(async e=>M(await a.dsList(),e.search),"func")}),je=d({...l,name:"ds-tables",access:"read",defaultFormat:"table",description:"列出数据集 SQL 可以使用的表(视图)",args:[n("search","按表名关键字过滤(可选)",!1)],func:r(async e=>[...await a.allowedTables()].sort().filter(t=>!e.search||t.includes(String(e.search).toLowerCase())).map(t=>({table:t})),"func")}),ve=d({...l,name:"ds-fields",access:"read",defaultFormat:"table",description:"列出数据集字段(列名就是卡片引用的映射键)",args:[n("ds","数据集的精确 id")],func:r(async e=>(await a.dsFields(e.ds)).map(t=>({sort:t.sortNum,column:t.tableFieldComment,type:t.tableFieldType,argumentType:t.argumentType})),"func")}),Pe=d({...l,name:"ds-sql",access:"read",description:"把数据集当前保存的 SQL 读出到文件",args:[n("ds","数据集的精确 id"),n("output","输出 SQL 路径(默认 ~/.linkai/artifacts/linkai/sql/<dsId>.sql)",!1)],func:r(async e=>{const t=await a.dsDetail(e.ds),s=a.writeJsonFile(P(e.output,"sql",`${e.ds}.sql`),t.sql||"");return{name:t.name,lines:(t.sql||"").split(`
|
|
2
|
+
`).length,output:s}},"func")}),Qe=d({...l,name:"ds-refs",access:"read",defaultFormat:"table",description:"列出引用该数据集的卡片",args:[n("ds","数据集的精确 id")],func:r(async e=>a.dsRefs(e.ds),"func")}),Ke=d({...l,name:"ds-match",access:"read",description:"复用查重:在当前租户里找与这段 SQL 完全一样或相似的数据集(建数据集前先查,不含 zz- 试搭对象)",args:[n("file","SQL 文件路径"),{name:"top",type:"int",default:3,help:"相似候选最多几个"}],func:r(async e=>{const t=v.matchDataset(a.readInputFile(e.file),await v.datasetIndex(),{top:Number(e.top)||3});return{...t,next:t.identical.length?`有完全一样的数据集,直接复用 ${t.identical[0].dsId}(build spec 的 datasets 写 id,或 spec 加 reuse: auto)`:t.similar.length?"有相似数据集:比较 columns 与口径后决定复用、在原数据集上调整(先 ds-refs 看影响),还是新建":"没有可复用的数据集,可以新建"}},"func")}),Ue=d({...l,name:"ds-create",access:"write",description:"新建 SQL 数据集(先做只读与可用表校验,并格式化 SQL);当前租户已有完全一样的数据集时拒绝,除非 --allow-duplicate",args:[n("name","数据集名称(租户内唯一;试验用 zz- 前缀)"),n("file","SQL 文件路径"),{name:"allow-duplicate",type:"boolean",default:!1,help:"已有完全一样的数据集仍要新建(需说明理由,如要独立演进口径)"}],func:r(async e=>{const t=a.readInputFile(e.file);a.precheckSql(t),await _({enforce:!0});let s=null;try{s=v.matchDataset(t,await v.datasetIndex())}catch(i){if(i instanceof a.LoginRequiredError)throw i}if(s?.identical.length&&!e["allow-duplicate"])throw new k("DUPLICATE_DATASET",`当前租户已有完全一样的数据集:${s.identical.map(i=>`「${i.name}」(${i.dsId})`).join("、")}`,"直接复用该数据集(build spec 的 datasets 写 id,或 spec 加 reuse: auto);确需独立一份才加 --allow-duplicate",1);const o=await a.dsSave({name:e.name,sql:t}),c=await a.dsFields(o);return{dsId:o,name:e.name,fields:c.map(i=>i.tableFieldComment),...s?.similar.length?{similar:s.similar,next:"已有相似数据集:确认口径确实不同;如可合并,后续改为复用"}:{}}},"func")}),Me=d({...l,name:"ds-update",access:"write",description:"原地更新数据集(id 不变,卡片映射不断)",args:[n("ds","数据集的精确 id"),n("name","数据集名称"),n("file","SQL 文件路径")],func:r(async e=>{const t=a.readInputFile(e.file);a.precheckSql(t);const s=await a.dsDetail(e.ds);if(s?.name===e.name&&s?.sql&&v.sameSql(t,s.sql)){const i=await a.dsFields(e.ds);return{dsId:e.ds,sameId:!0,unchanged:!0,fields:i.map(u=>u.tableFieldComment),next:"SQL 和名称与线上一致,未重复保存;不要再次执行同样的更新"}}await _({enforce:!0});const o=await a.dsSave({id:e.ds,name:e.name,sql:t,current:s}),c=await a.dsFields(o);return{dsId:o,sameId:o===e.ds,fields:c.map(i=>i.tableFieldComment)}},"func")}),Je=d({...l,name:"card-list",access:"read",defaultFormat:"table",description:"列出当前租户的卡片",args:[n("search","按名称关键字过滤(可选)",!1)],func:r(async e=>M(await a.cardList(),e.search),"func")}),ze=d({...l,name:"card-get",access:"read",description:"把卡片完整配置保存为 JSON 文件(修改前的基线,或 card-save 的输入)",args:[n("card","卡片的精确 id"),n("output","输出 JSON 路径(默认 ~/.linkai/artifacts/linkai/cards/<cardId>.json)",!1)],func:r(async e=>{const t=await a.cardDetail(e.card),s=a.writeJsonFile(P(e.output,"cards",`${e.card}.json`),t);return{name:t.name,fields:(t.indexBlockDataSettingDtos||[]).length,output:s}},"func")}),We=d({...l,name:"card-save",access:"write",description:"保存完整的卡片配置 JSON(唯一能让配置生效的保存方式)",args:[n("file","修改后的卡片 JSON 文件路径")],func:r(async e=>{const t=A(e.file);if(!t.id)throw new Error("Card payload must contain id");const s=typeof t.chartSetting=="string"?JSON.parse(t.chartSetting||"{}"):t.chartSetting||{};t.chartSetting=JSON.stringify(s),t.chartType=t.chartType||s.id,t.chartParentType=t.chartParentType||s.parentId,await a.post("/index-block/update-new",t),String(t.chartType||s.id)==="agtable"&&s.config?.fitTableWidth!==!0&&f.addNotice("这张 AG 表格没有开启自适应宽度(chartSetting.config.fitTableWidth)。按 BI 操作规则应设为 true,列宽 agColumnWidths 只作为比例(如等分统一 50);用户明确要求固定宽度时除外。","ag-fit-width");const o=await a.cardDetail(t.id);return{cardId:t.id,name:o.name,fields:(o.indexBlockDataSettingDtos||[]).length}},"func")}),Ge=d({...l,name:"fill-write",access:"write",description:"写自由填报数据:一条记录一个元素、字段一次带齐(默认只校验并预览,加 --execute 才写入)",args:[n("card","填报卡片的精确 id(usedIndexBlockId)"),n("file",'记录 JSON 数组文件:[{"字段名":"值",…,"_id":"已有记录ID(可选)"}]'),n("key","填报块:fillfieldid(第一个块)、fillfieldid1(第二个块)…",!1),{name:"batch",type:"int",default:100,help:"每次请求的记录数(1-500)"},{name:"execute",type:"boolean",default:!1,help:"确认后真正写入"}],func:r(async e=>{const t=Number(e.batch||100);if(!Number.isInteger(t)||t<1||t>500)throw new h("batch 必须是 1-500");await a.assertCardInTenant(e.card);const s=await a.cardDetail(e.card);if(s?.isFreeFill===!1)throw new Error("该卡片不是自由填报卡片;fill-write 只支持自由填报");const o=ee.fillBlockOf(s,e.key||"fillfieldid"),c=ee.buildFillValues(o,A(e.file)),i=[];for(let p=0;p<c.length;p+=t)i.push(c.slice(p,p+t));const u={cardId:e.card,name:s.name,key:o.key,fields:o.fields,uniqueFields:o.uniqueFields,records:c.length,updates:c.filter(p=>p.value).length,batches:i.length,sample:c.slice(0,2)};if(!e.execute)return{...u,executed:!1,next:"核对字段与记录数后,用户确认再加 --execute"};const S=[];for(const[p,g]of i.entries())try{await a.post("/index-block/new-fill-field",{usedIndexBlockId:e.card,fillValues:g}),S.push(g.length)}catch(E){throw new Error(`第 ${p+1}/${i.length} 批写入失败(已成功 ${S.length} 批、${S.reduce((L,b)=>L+b,0)} 条):${E.message}。先回读填报表按依据字段核对,再只补未成功的部分,不要整批重跑`)}return{...u,executed:!0,written:S.reduce((p,g)=>p+g,0),next:"回读填报表本身:行数、依据字段无空值无重复、抽查整行字段完整;再核对看板"}},"func")}),Ve=d({...l,name:"card-chart",access:"write",description:"按图表类型新建或覆盖图表卡片(指定维度、数值、系列列)",args:[n("ds","数据集的精确 id"),n("name","卡片名称"),n("kind","图表类型,如 column/stacked(linkai kinds 查看全部)"),n("dim","维度列名,多个用逗号分隔",!1),n("meas","数值列名,多个用逗号分隔"),n("series","系列列名(可选)",!1),n("card","要覆盖的已有卡片 id(可选)",!1)],func:r(async e=>a.cardChart({dsId:e.ds,name:e.name,kind:e.kind,dim:e.dim,meas:e.meas,series:e.series,id:e.card}),"func")}),He=d({...l,name:"card-meter",access:"write",description:"新建或覆盖单数值指标卡(meter/list)",args:[n("ds","数据集的精确 id"),n("name","卡片名称"),n("col","数值列名"),n("subtitle","副标题",!1),n("color","数值颜色(十六进制,如 #E8611C)",!1),n("card","要覆盖的已有卡片 id(可选)",!1)],func:r(async e=>a.cardMeter({dsId:e.ds,name:e.name,col:e.col,subtitle:e.subtitle,color:e.color||void 0,id:e.card}),"func")}),Ye=d({...l,name:"card-table",access:"write",description:"按 spec JSON 新建或覆盖 AG 明细表(见 references/card.md)",args:[n("ds","数据集的精确 id"),n("spec","spec JSON 文件路径"),n("card","要覆盖的已有卡片 id(可选)",!1)],func:r(async e=>a.cardTable({dsId:e.ds,spec:A(e.spec),id:e.card}),"func")}),Xe=d({...l,name:"card-gantt",access:"write",description:"按 spec JSON 新建或覆盖甘特图卡片(见 references/gantt.md)",args:[n("ds","数据集的精确 id"),n("spec","spec JSON 文件路径"),n("card","要覆盖的已有卡片 id(可选)",!1)],func:r(async e=>a.cardGantt({dsId:e.ds,spec:A(e.spec),id:e.card}),"func")}),Ze=d({...l,name:"card-sort",access:"write",description:'设置卡片持久排序("列:asc,列:desc"、.json 文件或 clear),并回读确认排序生效',args:[n("card","卡片的精确 id"),n("spec",'"waxis3:desc,ID:asc" | sort.json | clear')],func:r(async e=>{const t=/\.json$/i.test(String(e.spec))?A(e.spec):e.spec;return a.cardSort(e.card,Array.isArray(t)?t:t.sortField||t)},"func")}),et=d({...l,name:"query",access:"read",description:"按页面方式查询卡片数据;返回状态码、报错信息、行数和实际执行的 SQL",args:[n("card","卡片的精确 id"),{name:"page-size",type:"int",default:20,help:"取数行数"},n("filters","筛选条件 JSON 文件路径",!1),n("output","数据行保存路径(不在屏幕上输出数据)",!1)],func:r(async e=>{const t=await a.queryCard({cardId:e.card,pageSize:e["page-size"],filters:e.filters?A(e.filters):[]});return e.output&&a.writeJsonFile(e.output,t.data),delete t.data,t},"func")}),tt=d({...l,name:"plan-list",access:"read",defaultFormat:"table",description:"列出分析方案(看板)",args:[n("search","按名称关键字过滤(可选)",!1)],func:r(async e=>M(await a.planList(),e.search),"func")}),at=d({...l,name:"plan-get",access:"read",description:"把方案详情保存为 JSON 文件(含租户模板筛选器)",args:[n("plan","方案的精确 id"),n("output","输出 JSON 路径(默认 ~/.linkai/artifacts/linkai/plans/<planId>.json)",!1),{name:"personal",type:"boolean",default:!1,help:"读取自己的个人筛选器副本"}],func:r(async e=>{const t=await a.planDetail(e.plan,!e.personal),s=a.writeJsonFile(P(e.output,"plans",`${e.plan}.json`),t);return{planId:e.plan,cards:(t.pageCards||[]).map(o=>({pageCardId:o.id,name:o.indexBlock?.name})),output:s}},"func")}),nt=d({...l,name:"plan-create",access:"write",description:"新建空的分析方案(看板)",args:[n("name","方案名称")],func:r(async e=>{const t=await a.post("/page-setting/new-create-empty-plan",{name:e.name,isPrivate:!1,pageType:0});return f.addNotice(`手工搭建方案:完成后必须用 linkai plan-knowledge --plan ${t} --mode create --file <知识文件> 交付方案知识库(铁律 2)。新建看板优先用 linkai build(自动生成知识库并验收)。`,"plan-knowledge"),{planId:t,name:e.name}},"func")}),st=d({...l,name:"plan-add",access:"write",description:"把卡片放到方案的指定位置;返回 pageCardId(绑定筛选器时使用)",args:[n("plan","方案的精确 id"),n("card","卡片的精确 id"),{name:"x",type:"int",default:0,help:"横向位置(格,0–23)"},{name:"y",type:"int",default:0,help:"纵向位置(格)"},{name:"w",type:"int",default:24,help:"宽度(格,满宽 24)"},{name:"h",type:"int",default:20,help:"高度(格)"}],func:r(async e=>a.planAdd({planId:e.plan,cardId:e.card,x:e.x,y:e.y,w:e.w,h:e.h}),"func")}),ot=d({...l,name:"plan-filter",access:"write",description:"保存方案筛选器为租户模板(全量覆盖),并重置自己的个人副本",args:[n("plan","方案的精确 id"),n("file","筛选器请求体 JSON 文件路径")],func:r(async e=>a.planFilter(e.plan,A(e.file)),"func")}),rt=d({...l,name:"report-list",access:"read",defaultFormat:"table",description:"列出数据报表(每个报表对应一张卡片)",args:[n("search","按名称关键字过滤(可选)",!1)],func:r(async e=>M(await a.reportList(),e.search),"func")}),it=d({...l,name:"report-card",access:"read",description:"查询卡片对应报表的 userPageCardId(重置报表筛选器时使用)",args:[n("card","卡片的精确 id")],func:r(async e=>{const t=await a.reportDetail(e.card);return{cardId:e.card,userPageCardId:t.id}},"func")}),ct=d({...l,name:"report-publish",access:"write",description:"把卡片发布为数据报表",args:[n("card","卡片的精确 id"),n("name","报表名称")],func:r(async e=>{await a.post("/index-block/operate-report",{indexBlockId:e.card,name:e.name,tag:"",isPrivate:!1});const t=(await a.reportList()).filter(s=>s.cardId===e.card);return{cardId:e.card,published:t.length===1,url:`https://bi.linkmore.com/view/table?indexBlockId=${e.card}&status=view`}},"func")}),lt=d({...l,name:"reset-filter",access:"write",description:"丢弃自己的个人筛选器副本,按模板重建(方案或报表卡片)",args:[n("plan","方案 id(方案筛选器)",!1),n("page-card","报表 userPageCardId 或方案 pageCardId(卡片筛选器)",!1)],func:r(async e=>{if(!!e.plan==!!e["page-card"])throw new Error("Pass exactly one of --plan or --page-card");const t=e.plan?{userPageSettingId:e.plan,filterSourceType:4}:{userPageCardId:e["page-card"],filterSourceType:2};return{filters:(await a.post("/page-setting/reset-user-filter",t)||[]).map(o=>o.labelName)}},"func")}),V=w.resolve(w.dirname(de(import.meta.url)),"..","..");try{a.setClientVersion(JSON.parse(y.readFileSync(w.join(V,"package.json"),"utf8")).version)}catch{}const Q={knowledge:w.join(V,"knowledge"),templates:w.join(V,"templates")};function J(e,t=e){return y.existsSync(e)?y.readdirSync(e,{withFileTypes:!0}).flatMap(s=>{const o=w.join(e,s.name);return s.isDirectory()?J(o,t):s.name.startsWith(".")?[]:[w.relative(t,o)]}):[]}r(J,"walk");function H(e,t){const s=Q[e],o=w.resolve(s,t);if(o!==s&&!o.startsWith(s+w.sep))throw new h("路径必须在 knowledge/ 或 templates/ 之内");if(!y.existsSync(o)||y.statSync(o).isDirectory())throw new h(`文件不存在:${e}/${t}`,"执行 linkai kb 查看可用文件");return y.readFileSync(o,"utf8")}r(H,"readLocal");function Y(e){return new k("KB_UNAVAILABLE",`知识库服务暂不可用:${e}`,"稍后重试;持续出现请联系领谋",1)}r(Y,"kbUnavailable");function N(e){if(!(e instanceof a.AiKbError))return e;const t={400:"AI_KB_BAD_REQUEST",403:"AI_KB_FORBIDDEN",404:"AI_KB_NOT_FOUND",409:"AI_KB_CONFLICT",503:"AI_KB_UNAVAILABLE"},s={404:"确认 source、对象或文档属于当前租户;数据字典不会回退本地内容",409:"知识元数据未就绪时先联系知识库维护方;文档持续变化时稍后重试",503:"稍后重试;数据字典没有本地兜底,持续出现请联系领谋"};return new k(t[e.code]||"AI_KB_ERROR",e.message,s[e.code],1)}r(N,"aiKbFailure");function O(e,t,s,o,c){const i=e==null||e===""?t:Number(e);if(!Number.isInteger(i)||i<o||i>c)throw new h(`${s} 必须是 ${o}~${c} 的整数`);return i}r(O,"intOption");async function K(e){const t=await a.ctx();if(String(e).toLowerCase()!==String(t.dataSourceId).toLowerCase())throw new k("AI_KB_SOURCE_MISMATCH",`数据字典 source ${e} 与当前数据集 source ${t.dataSourceId} 不一致`,"不要跨 source 使用元数据;先确认服务端已为当前数据源完成探库,或确认 CLI 的数据源配置",1)}r(K,"requireDatasetSource");function se(){return J(w.join(Q.knowledge,"report-builder")).map(e=>({file:`knowledge/report-builder/${e}`,title:"",summary:"",library:"bi-builder",scope:"public",source:"bundled-fallback"}))}r(se,"localBiDocs");function ke(e){return{file:`knowledge/${e.canonicalPath}`,title:"",library:e.library,scope:e.scope,source:"bundled-fallback",content:H("knowledge",e.canonicalPath)}}r(ke,"localBiDoc");function Se(e,t){const s=Array.isArray(t)?t:null,o=s?"bi-builder":t?.library||(String(t?.file||"").startsWith("templates/")?"templates":String(t?.file||"").includes("browser/")?"cli-docs":"bi-builder");return f.emit("kb.read",s?{library:o,op:e,docs:s.length,sources:[...new Set(s.map(c=>c?.source).filter(Boolean))]}:{library:o,op:e,file:t?.file??null,source:t?.source??null,chars:typeof t?.content=="string"?t.content.length:void 0}),t}r(Se,"logKb");async function $(e,t,s,o={}){if(f.emit("kb.read",{library:"data-dictionary",op:e,source:t.source??null,...o,...e!=="sources"?{cached:a.kbCacheHit()}:{}}),e!=="sources"&&t.source){const c=a.readSession()?.tenantId,i=f.currentFlow()?.id??null;let u=null;try{u=(await a.dictSourcesCached()).find(S=>String(S.source).toLowerCase()===String(t.source).toLowerCase())?.revision||null}catch{}x.recordRead({tenantId:c,source:t.source,flowId:i,op:e,revision:u}),e==="doc"&&t.doc&&x.recordRef({tenantId:c,flowId:i,ref:`doc:${t.doc}${o.title?` ${o.title}`:""}`}),e==="object"&&o.object&&x.recordRef({tenantId:c,flowId:i,ref:`object:${o.object}`})}return s}r($,"logDict");async function _({enforce:e}){const t=await a.ensureSession(),s=await a.ctx(),o=await x.dictionaryStatus({tenantId:t.tenantId,source:s.dataSourceId,flowId:f.currentFlow()?.id??null,fetchSources:r(()=>a.dictSourcesCached(),"fetchSources"),fetchDocs:r(async()=>(await a.dictDocsHead(s.dataSourceId)).items,"fetchDocs")});if(a.logGuard("dictionary-fresh",o.ok,{tenantId:t.tenantId,source:s.dataSourceId,reason:o.reason??o.skipped??null,readAt:o.readAt??null,dictUpdatedAt:o.dictUpdatedAt??null,enforce:e}),o.skipped&&f.addNotice(`数据字典:${o.skipped}。`,"dictionary-skipped"),!o.ok){if(e)throw new k("DICTIONARY_REQUIRED",o.reason,x.readHint(s.dataSourceId),1);f.addNotice(`数据字典:${o.reason}。正式写入(ds-create / ds-update / build --execute)前会被拒绝。${x.readHint(s.dataSourceId)}`,"dictionary-stale")}return o}r(_,"checkDictionary");const oe=r((e,t,s)=>Array.isArray(e)?e.slice(0,t).map(s):[],"top"),dt=d({...l,name:"kb",access:"read",description:"读取文件型 BI 搭建知识库(服务器优先、本地兜底);数据字典请使用 kb-sources / kb-search 等结构化命令",args:[n("file","文件路径,如 knowledge/report-builder/build-spec.md 或 templates/report-spec/report.example.yaml",!1),n("search","按关键字过滤目录",!1),n("library","文件型知识库:bi-builder;data-dictionary 请使用 kb-sources 等结构化命令;另预留 integration-flow | user",!1)],func:r(async e=>Se(e.file?"doc":"catalog",await(async()=>{const t=String(e.file||"").replace(/^\.?\//,"");if(t.startsWith("templates/"))return{file:t,source:"bundled",content:H("templates",t.slice(10))};if(t==="knowledge/browser/README.md"||t==="browser/README.md")return{file:"knowledge/browser/README.md",source:"bundled-cli",content:H("knowledge","browser/README.md")};const s=e.search?String(e.search):"",o=e.library?String(e.library):"";if(o&&!a.KB_LIBRARIES[o])throw new h(`未知知识库:${o}`);if(o==="data-dictionary"||/^(?:knowledge\/)?data-dictionary(?:\/|$)/.test(t))throw new k("KB_STRUCTURED_API","数据字典不再按 Markdown 文件路径读取","先执行 linkai kb-sources,再使用 kb-search / kb-catalog / kb-object / kb-docs / kb-doc",1);if(o&&!a.KB_LIBRARIES[o].enabled)throw new k("KB_NOT_IMPLEMENTED",`知识库 ${o} 已预留,当前版本尚未启用`,void 0,1);if(!t){const i=o?[o]:["bi-builder"],u=[];for(const p of i)try{const g=await a.kbCatalog(p);p==="bi-builder"&&!(g.docs||[]).length?u.push(...se()):u.push(...(g.docs||[]).map(E=>{const L=a.KB_LIBRARIES[p].pathPrefix,b=a.kbRequest(E.path,p).path;return{file:`knowledge/${L}/${b}`,title:E.title||"",summary:E.summary||"",library:p,scope:a.KB_LIBRARIES[p].scope,source:"server"}}))}catch(g){if(p==="bi-builder"&&g instanceof a.KbUnavailableError)u.push(...se());else throw g instanceof a.KbUnavailableError?Y(p):g}const S=!o||o==="bi-builder"?J(Q.templates).map(p=>({file:`templates/${p}`,title:"报表模版",summary:"",source:"bundled"})):[];return[...u,...S].filter(p=>!s||p.file.includes(s)||p.title.includes(s))}const c=a.kbRequest(t,o||void 0);try{const i=await a.kbDoc(t,o||void 0);return{file:`knowledge/${i.path}`,title:i.title||"",library:i.library,scope:i.scope,source:"server",content:i.content||""}}catch(i){if(c.library==="bi-builder"&&(i instanceof a.KbUnavailableError||i instanceof a.KbDocumentNotFoundError)){if(!y.existsSync(w.join(Q.knowledge,"report-builder")))throw Y(c.library);return ke(c)}throw i instanceof a.KbUnavailableError?Y(c.library):i}})()),"func")}),ut=d({...l,name:"kb-sources",access:"read",defaultFormat:"table",description:"列出当前租户可读的数据字典数据源,并标记当前数据集所用 source;后续命令必须显式传该 ID",args:[],func:r(async()=>{try{const[e,t]=await Promise.all([a.dictSourcesCached(),a.ctx()]),s=e.map(c=>({...c,selectedForDatasets:String(c.source).toLowerCase()===String(t.dataSourceId).toLowerCase()})),o=s.some(c=>c.selectedForDatasets);return $("sources",{},null,{sources:s.map(c=>({source:c.source,name:c.name})),aligned:o}),{datasetSourceId:t.dataSourceId,aligned:o,sources:s,...o?{}:{warning:"数据字典没有返回当前数据集 source;不要跨 source 使用元数据,请先确认服务端探库或数据源配置"}}}catch(e){throw N(e)}},"func")}),pt=d({...l,name:"kb-catalog",access:"read",description:"分页列出指定 source 的可用视图/对象元数据",args:[n("source","数据源 ID(来自 kb-sources,必须与后续数据集 source 一致)"),n("search","表名、中文名或说明关键字",!1),n("page","页码,默认 1",!1),n("page-size","每页 1~100,默认 30",!1)],func:r(async e=>{try{await K(e.source);const t={source:e.source,keyword:e.search,pageIndex:O(e.page,1,"page",1,2147483647),pageSize:O(e["page-size"],30,"page-size",1,100)},s=await a.kbCached(e.source,`catalog|${t.keyword??""}|${t.pageIndex}|${t.pageSize}`,()=>a.aiKbCatalog(t));return $("catalog",e,s,{keyword:e.search??null,total:s?.totalCount??null})}catch(t){throw N(t)}},"func")}),ft=d({...l,name:"kb-object",access:"read",description:"读取指定 source 中一个可见对象的字段与关联关系",args:[n("source","数据源 ID(来自 kb-sources)"),n("db","数据库/schema 名"),n("object","对象/视图名")],func:r(async e=>{try{await K(e.source);const t=await a.kbCached(e.source,`object|${String(e.db).toLowerCase()}.${String(e.object).toLowerCase()}`,()=>a.aiKbObject({source:e.source,db:e.db,object:e.object}));return $("object",e,t,{object:`${e.db}.${e.object}`})}catch(t){throw N(t)}},"func")}),mt=d({...l,name:"kb-docs",access:"read",description:"分页列出指定 source 的业务口径、枚举、关系、数据点等知识文档",args:[n("source","数据源 ID(来自 kb-sources)"),n("category","caliber | enum | relation | datapoint | other",!1),n("search","标题或正文关键字",!1),n("page","页码,默认 1",!1),n("page-size","每页 1~100,默认 30",!1)],func:r(async e=>{const t=e.category?String(e.category):void 0;if(t&&!["caliber","enum","relation","datapoint","other"].includes(t))throw new h("category 必须是 caliber、enum、relation、datapoint、other 之一");try{await K(e.source);const s={source:e.source,category:t,keyword:e.search,pageIndex:O(e.page,1,"page",1,2147483647),pageSize:O(e["page-size"],30,"page-size",1,100)},o=await a.kbCached(e.source,`docs|${t??""}|${s.keyword??""}|${s.pageIndex}|${s.pageSize}`,()=>a.aiKbDocs(s));return $("docs",e,o,{category:t??null,keyword:e.search??null,total:o?.totalCount??null})}catch(s){throw N(s)}},"func")}),wt=d({...l,name:"kb-doc",access:"read",description:"一致性读取一篇完整数据字典文档;自动按 nextOffset 分页,版本变化时从头重读",args:[n("source","数据源 ID(来自 kb-sources)"),n("doc","文档 ID(来自 kb-docs 或 kb-search)"),n("page-size","单次正文字符数,1~20000,默认 20000",!1)],func:r(async e=>{try{await K(e.source);const t=await a.kbCached(e.source,`doc|${String(e.doc).toLowerCase()}`,()=>a.aiKbDoc({source:e.source,doc:e.doc,limit:O(e["page-size"],2e4,"page-size",1,2e4)}));return $("doc",e,t,{doc:e.doc,title:t?.title??null,category:t?.category??null,chars:t?.totalChars??null})}catch(t){throw N(t)}},"func")}),bt=d({...l,name:"kb-search",access:"read",description:"按自然语言检索指定 source 的候选视图与业务知识;相关度不是正确率,使用前继续回读对象/文档核对",args:[n("source","数据源 ID(来自 kb-sources)"),n("question","业务问题,最长 2000 字"),n("top-tables","最多返回表数,1~10,默认 5",!1),n("top-docs","最多返回文档数,1~10,默认 3",!1)],func:r(async e=>{try{await K(e.source);const t={source:e.source,question:e.question,topTables:O(e["top-tables"],5,"top-tables",1,10),topDocs:O(e["top-docs"],3,"top-docs",1,10)},s=await a.kbCached(e.source,`search|${t.topTables}|${t.topDocs}|${a.normalizeQuestion(t.question)}`,()=>a.aiKbSearch(t));return $("search",e,s,{question:String(e.question||"").slice(0,200),hitTables:oe(s?.tables,10,o=>`${o.db}.${o.object}`),hitDocs:oe(s?.docs,10,o=>({doc:o.doc,title:o.title??null,category:o.category??null})),emptyReason:s?.emptyReason??null})}catch(t){throw N(t)}},"func")}),ht=W({...l,name:"template",access:"write",description:"复制报表 spec 与示例文件;SQL 示例必须由 AI 按用户需求、租户数据字典和确认口径重写,CLI 不生成业务 SQL",args:[n("output","目标目录(默认 ./linkai-report),需不存在或为空",!1)],func:r(async e=>{const t=w.resolve(String(e.output||"linkai-report"));if(y.existsSync(t)&&y.readdirSync(t).length)throw new h(`目录已存在且不为空:${t}`,"换一个 --output 目录");return y.cpSync(w.join(Q.templates,"report-spec"),t,{recursive:!0}),{output:t,files:J(t),next:`修改 ${w.join(t,"report.example.yaml")} 与 sql/ 下的 SQL,然后 linkai build --spec ${w.join(t,"report.example.yaml")}`}},"func")}),yt=d({...l,name:"build",access:"write",description:"严格按 BI 公共知识库执行 AI 提供的 SQL 与报表 spec(CLI 不生成业务 SQL)。默认只校验并给出执行计划;--execute 才写入;含正式名还需 --confirm",args:[n("spec","报表 spec 文件(.yaml/.json),模版用 linkai template 复制"),{name:"execute",type:"boolean",default:!1,help:"真正写入(默认只校验并输出执行计划)"},{name:"confirm",type:"boolean",default:!1,help:"用户已明确授权使用正式名(非 zz- 前缀)"},{name:"no-style",type:"boolean",default:!1,help:"跳过页面样式步骤(暖色背景、卡位透明)"}],func:r(async e=>{const t=Z.loadSpec(e.spec),s=(t.spec.datasets||[]).some(i=>!i.id&&i.sql),o=r(async()=>{const i=await a.ensureSession(),u=a.spaceOf(i);if(t.spec.tenant&&![u.tenantId,u.tenantName,u.tenantCode].includes(String(t.spec.tenant)))throw new h(`当前登录的租户是「${u.tenantName}」,与 spec.tenant「${t.spec.tenant}」不一致`,"确认目标租户后执行 linkai login --tenant <租户名>,或修改 spec.tenant");s&&await _({enforce:!0})},"beforeWrite"),c=await Z.runBuild(t,{execute:!!e.execute,confirm:!!e.confirm,style:!e["no-style"],beforeWrite:o});if(!e.execute&&s&&c.ok!==!1)try{c.dictionary=await _({enforce:!1})}catch(i){if(i instanceof a.LoginRequiredError)c.dictionary={ok:!1,reason:"未登录"};else throw i}if(c.ok===!1&&(c.stage==="validate"||c.stage==="authorize"))throw new h(`build ${c.stage} failed:
|
|
3
3
|
- ${c.errors.join(`
|
|
4
|
-
- `)}`,c.stage==="authorize"?"把正式名清单给用户确认,得到明确授权后再加 --confirm":"按错误逐条修改 spec 后重新 dry-run");return c},"func")});async function Z(e){const t=String(e.mode||"");if(!["create","update"].includes(t))throw new h("必须明确 --mode create 或 --mode update;用户未说明新建还是修改时,先主动询问用户");if(!e.file)throw new h("缺少 --file <方案知识 YAML/JSON>","文件必须描述方案内容、解决的问题,以及每张业务卡片的内容、数据来源、粒度和取数逻辑");const n=U.readKnowledgeFile(e.file);return U.publishPlanKnowledge({planId:e.plan,operation:t,authoring:n.knowledge||n})}r(Z,"publishPlanKnowledgeFromFile");const ut=d({...l,name:"plan-knowledge",access:"write",description:"生成、版本化并保存完整方案知识库;服务器不可用时内部留存本地。最终返回方案地址和完整知识内容,不返回 SQL 或上传信息",args:[s("plan","方案的精确 id"),s("mode","create(新方案)或 update(修改原方案)",!1),s("file","AI 编写的方案知识 YAML/JSON 文件",!1)],func:r(async e=>{const t=await Z(e);return f.emit("kb.write",{library:"plan-knowledge",op:e.mode||"create",plan:e.plan,version:t?.version??t?.planKnowledge?.version??null}),t},"func")}),pt=d({...l,name:"plan-knowledge-get",access:"read",description:"读取方案当前知识库;修改原方案前先读。服务器不可用时读取本地待同步版本,不展示存储信息",args:[s("plan","方案的精确 id")],func:r(async e=>{const t=await U.readCurrentPlanKnowledge(e.plan);return f.emit("kb.read",{library:"plan-knowledge",op:"doc",plan:e.plan,version:t?.version??null,exists:t?.exists??null,chars:typeof t?.content=="string"?t.content.length:null}),t},"func")}),mt=d({...l,name:"deliverable",access:"write",description:"兼容命令:等同 plan-knowledge;需要 --mode 与 --file,不再输出数据集 SQL",args:[s("plan","方案的精确 id"),s("mode","create 或 update",!1),s("file","方案知识 YAML/JSON 文件",!1)],func:Z}),ft=d({...l,name:"backup",access:"read",description:"修改已有对象前保存带时间戳的备份(卡片/方案 JSON 或数据集 SQL),存到 ~/.linkai/artifacts/linkai/backups/",args:[s("type","card(卡片)| plan(方案)| ds(数据集)"),s("id","对象的精确 id")],func:r(async e=>{const t=r(n=>a.artifactsDir("backups",`${e.type}-${e.id}-${V()}.${n}`),"at");if(e.type==="card")return{type:"card",output:a.writeJsonFile(t("json"),await a.cardDetail(e.id))};if(e.type==="plan")return{type:"plan",output:a.writeJsonFile(t("json"),await a.planDetail(e.id,!0))};if(e.type==="ds")return{type:"ds",output:a.writeJsonFile(t("sql"),(await a.dsDetail(e.id)).sql||"")};throw new Error("type must be card, plan or ds")},"func")}),wt=d({...l,name:"remove",access:"write",description:"按 folderId 删除数据集/卡片/方案/报表(id 来自各 *-list 命令)。需要用户明确授权",args:[s("folder","精确的 folderId,多个用逗号分隔")],func:r(async e=>{const t=String(e.folder).split(",").map(n=>n.trim()).filter(Boolean);return await a.post("/index-block/delete-folder-bussiness",t),{deleted:t.length,recycleBin:"POST /index-block/get-recycle-station-page-list"}},"func")}),bt=d({...l,name:"clean",access:"write",description:"按 zz- 前缀清理试验对象,按依赖顺序删除:报表 → 方案 → 卡片 → 数据集",args:[s("prefix","以 zz- 开头的前缀,如 zz-销售看板-")],func:r(async e=>a.cleanPrefix(e.prefix),"func")}),ht=d({...l,name:"api-get",access:"read",description:"调用未封装的读接口(GET,路径可省略 /api/app);先查 references/api-catalog.md。数据源、SQL 类接口不开放",args:[s("path","接口路径,如 /index-block/folders?type=1"),{name:"raw",type:"boolean",default:!1,help:"返回完整响应"}],func:r(async e=>(a.assertApiAllowed(e.path),a.api("GET",e.path,void 0,{raw:!!e.raw})),"func")}),me=/\/(get-|query|search)[\w-]*|\/[\w-]*-(list|detail|info)(?:[/?]|$)/i,yt=d({...l,name:"api",access:"write",accessOf:r(e=>me.test(String(e.path||""))?"read":"write","accessOf"),description:"调用未封装的写接口(POST/PUT/DELETE,可能新建或修改数据);先查 references/api-catalog.md。数据源、SQL 类接口不开放",args:[s("method","POST、PUT 或 DELETE"),s("path","接口路径"),s("body","请求体 JSON 文件路径",!1),{name:"raw",type:"boolean",default:!1,help:"返回完整响应(code/message/extendData)"}],func:r(async e=>{const t=String(e.method).toUpperCase();if(!["POST","PUT","DELETE"].includes(t))throw new Error("method must be POST, PUT or DELETE; use api-get for GET");return a.assertApiAllowed(e.path),a.api(t,e.path,e.body?x(e.body):t==="POST"?{}:void 0,{raw:!!e.raw})},"func")}),ee="create(新建方案)| modify(修改已有方案)| import(导入/更新数据)| query(只读查询分析)",gt=d({...l,name:"flow-start",access:"write",description:"开始一个工作流:接到业务任务时执行一次,之后的命令自动归入它;完成后用 flow-end 结束",args:[s("goal","这个任务要完成什么(用户原话概括)"),s("type",ee),s("plan","目标方案 id(修改已有方案时)",!1),s("model",'你(AI)当前使用的具体模型与版本,如 "Claude Opus 5.5"、"gpt-5.6";不确定就写你所知道的名称',!1)],func:r(async e=>{const t=f.startFlow({goal:e.goal,type:String(e.type||"").trim(),plan:e.plan||null,model:e.model||null});return{flow:t.id,goal:t.goal,type:t.type,plan:t.plan,model:f.currentModel(),next:"同一会话里的后续命令自动归入此工作流;并行开多个任务时,在命令上加 --flow <id> 指明。完成后 linkai flow-end --status delivered"}},"func")}),kt=d({...l,name:"flow-end",access:"write",description:"结束工作流:delivered(已交付)| abandoned(放弃)| failed(失败);默认结束当前工作流,或用 --flow <id> 指定",args:[s("status","delivered | abandoned | failed"),s("plan","交付的方案 id",!1),s("knowledge-version","方案知识库版本,如 v2",!1),s("note","结果说明或失败原因",!1)],func:r(async e=>{const t=Object.fromEntries(Object.entries({plan:e.plan,knowledgeVersion:e["knowledge-version"],note:e.note}).filter(([,o])=>o!=null&&o!=="")),n=f.endFlow({id:process.env.LINKAI_FLOW||void 0,status:String(e.status||"").trim(),result:t});return{flow:n.id,status:n.status,goal:n.goal,runs:n.runs,durationMinutes:Math.round(((n.endedAt??Date.now())-n.startedAt)/6e3)/10,result:n.result}},"func")}),St=d({...l,name:"flow-status",access:"read",description:"查看本机工作流:当前工作流、未完成或中断的工作流(含中断原因与下一步);--all 含已结束的",args:[{name:"all",type:"boolean",default:!1,help:"包括已结束和自动关闭的工作流(最近 7 天)"}],func:r(async e=>{const t=f.currentFlow(),n=(e.all?f.listFlows():f.unfinishedFlows()).slice(0,30).map(o=>({...H(o),runs:o.runs,startedAt:new Date(o.startedAt).toISOString(),endedAt:o.endedAt?new Date(o.endedAt).toISOString():null,result:o.result}));return{current:t,idleMinutes:f.flowIdleMinutes(),flows:n}},"func")}),N={name:"worker",type:"string",required:!1,help:"执行者名称(也可用全局参数 --worker);同一会话保持不变,如 主控、sql-1、card-2"},E=r(e=>String(e.worker||S.currentWorker()||""),"workerOf"),v=s("board","任务板 id(collab-start 返回,如 b-1a2b3c4d)"),It=d({...l,name:"collab-start",access:"write",description:"多会话协同:创建任务板(协调者执行一次),返回任务板 id 交给各执行者",args:[s("goal","这次协同要完成什么"),s("plan","目标方案 id(修改已有方案时)",!1),s("type",`工作流类型:${ee};默认有 --plan 为 modify,否则 create`,!1),N],func:r(async e=>{const t=S.startBoard({goal:e.goal,plan:e.plan,worker:E(e)});return f.startFlow({id:t.id,source:"board",goal:t.goal,type:e.type||(e.plan?"modify":"create"),plan:e.plan||null}),{board:t.id,coordinator:t.coordinator,goal:t.goal,next:"用 collab-task 拆分任务;各执行者写共享对象前先 collab-claim,成果用 collab-note 共享;读命令不用认领"}},"func")}),Ct=d({...l,name:"collab-task",access:"write",description:"多会话协同:新增任务(--title),或更新任务状态(--id --status todo|doing|done|blocked --result)",args:[v,s("title","新任务标题(新增时)",!1),{name:"id",type:"int",required:!1,help:"要更新的任务编号"},s("status","todo | doing | done | blocked",!1),s("result","结果或阻塞原因(文件路径、对象 id、结论)",!1),s("assignee","分配给哪个执行者",!1),s("object","任务涉及的共享对象,逗号分隔,如 card:<id>,ds:<id>",!1),N],func:r(async e=>e.id?S.updateTask({board:e.board,worker:E(e),id:e.id,status:e.status,result:e.result,assignee:e.assignee}):S.addTask({board:e.board,worker:E(e),title:e.title,assignee:e.assignee,objects:e.object}),"func")}),xt=d({...l,name:"collab-claim",access:"write",description:"多会话协同:写共享对象前认领(全部成功或全部不认领;重复认领即续期);被别人认领时返回 COLLAB_CONFLICT",args:[v,s("object","对象,逗号分隔:card:<id> ds:<id> plan:<id> knowledge:<planId> spec:<文件> report:<id> filter:<planId>"),{name:"ttl",type:"int",default:S.DEFAULT_TTL_SECONDS,help:"认领有效秒数(60-7200),到期自动释放"},s("note","在做什么",!1),N],func:r(async e=>S.claim({board:e.board,worker:E(e),objects:e.object,ttl:e.ttl,note:e.note}),"func")}),Lt=d({...l,name:"collab-release",access:"write",description:"多会话协同:释放认领(不指定对象则释放自己在该任务板上的全部认领)",args:[v,s("object","要释放的对象,逗号分隔",!1),N],func:r(async e=>S.release({board:e.board,worker:E(e),objects:e.object}),"func")}),Et=d({...l,name:"collab-note",access:"write",description:"多会话协同:共享成果或发现(已验证的 SQL 文件、建好的对象 id、口径结论、交接、阻塞),避免别人重复做",args:[v,s("text","内容"),s("kind","finding | result | handoff | blocker | decision(默认 finding)",!1),s("ref","相关文件路径或对象 id",!1),N],func:r(async e=>S.note({board:e.board,worker:E(e),text:e.text,kind:e.kind||"finding",ref:e.ref}),"func")}),At=d({...l,name:"collab-status",access:"read",description:"多会话协同:查看任务板(任务、有效认领、最近成果);不带 --board 列出全部进行中的任务板",args:[s("board","任务板 id",!1),{name:"notes",type:"int",default:30,help:"显示最近多少条成果记录"}],func:r(async e=>S.status({board:e.board,notes:e.notes}),"func")}),Dt=d({...l,name:"collab-close",access:"write",description:"多会话协同:关闭任务板并释放其上全部认领,同时结束对应工作流(协调者在交付后执行)",args:[v,s("status","工作流结束状态:delivered | abandoned | failed(默认 delivered)",!1),s("note","结果说明",!1),N],func:r(async e=>{const t=S.closeBoard({board:e.board,worker:E(e)});let n=null;try{n=f.endFlow({id:t.board,status:e.status||"delivered",result:e.note?{note:e.note}:null}).status}catch(o){n=`未结束:${o.message}`}return{...t,flow:n}},"func")}),Ot=d({...re,name:"shot",access:"write",description:"在浏览器中打开报表/方案页面并截图(需要 Chrome 与 LinkAI 扩展)",args:[s("url","页面地址,如 https://bi.linkmore.com/view/plan?plan=<planId>"),s("output","截图保存路径(默认 ~/.linkai/artifacts/linkai/shots/<时间>.png)",!1),{name:"wait",type:"int",default:12,help:"等待页面渲染的秒数"}],func:r(async(e,t)=>{const n=a.readConfig(),o=new URL(t.url);if(o.origin!==n.origin)throw new Error(`URL must be on ${n.origin}`);if(/\/login/i.test(o.pathname))throw new Error("Do not open /login: it clears localStorage");const c=await a.ensureSession();await e.goto(`${n.origin}/`),await e.evaluate(a.browserSessionScript(c)),await e.goto(o.href),await e.sleep(Number(t.wait));const i=await e.evaluate("(() => ({ title: document.title, onLogin: /login/i.test(location.pathname) }))()");if(i?.onLogin)throw new Error("Page redirected to /login: the synced session was rejected; run: linkai login");let u=null,I=null;try{const p=w.resolve(R(t.output,"shots",`${V()}.png`));b.mkdirSync(w.dirname(p),{recursive:!0}),u=await e.screenshot({path:p,fullPage:!1,format:"png"})||p}catch(p){I=String(p?.message||p).slice(0,200)}return{url:await e.getCurrentUrl(),title:i?.title,loggedIn:!0,output:u,screenshotError:I}},"func")});export{yt as apiCommand,ht as apiGetCommand,Ee as askCommand,ft as backupCommand,dt as buildCommand,Ke as cardChartCommand,Je as cardGanttCommand,Be as cardGetCommand,je as cardListCommand,Qe as cardMeterCommand,ve as cardSaveCommand,ze as cardSortCommand,Ue as cardTableCommand,bt as cleanCommand,xt as collabClaimCommand,Dt as collabCloseCommand,Et as collabNoteCommand,Lt as collabReleaseCommand,It as collabStartCommand,At as collabStatusCommand,Ct as collabTaskCommand,mt as deliverableCommand,Ce as doctorCommand,_e as dsCreateCommand,Te as dsFieldsCommand,Oe as dsListCommand,$e as dsMatchCommand,Ne as dsRefsCommand,qe as dsSqlCommand,Fe as dsTablesCommand,Re as dsUpdateCommand,Pe as fillWriteCommand,kt as flowEndCommand,gt as flowStartCommand,St as flowStatusCommand,De as fmtCommand,st as kbCatalogCommand,at as kbCommand,it as kbDocCommand,rt as kbDocsCommand,ot as kbObjectCommand,ct as kbSearchCommand,nt as kbSourcesCommand,Le as kindsCommand,Se as loginCommand,Ie as logoutCommand,He as planAddCommand,Ve as planCreateCommand,Ye as planFilterCommand,Ge as planGetCommand,ut as planKnowledgeCommand,pt as planKnowledgeGetCommand,We as planListCommand,Me as queryCommand,wt as removeCommand,Ze as reportCardCommand,Xe as reportListCommand,et as reportPublishCommand,tt as resetFilterCommand,Ot as shotCommand,Ae as sqlCheckCommand,lt as templateCommand,xe as tenantsCommand};
|
|
4
|
+
- `)}`,c.stage==="authorize"?"把正式名清单给用户确认,得到明确授权后再加 --confirm":"按错误逐条修改 spec 后重新 dry-run");return c},"func")});async function re(e){const t=String(e.mode||"");if(!["create","update"].includes(t))throw new h("必须明确 --mode create 或 --mode update;用户未说明新建还是修改时,先主动询问用户");if(!e.file)throw new h("缺少 --file <方案知识 YAML/JSON>","文件必须描述方案内容、解决的问题,以及每张业务卡片的内容、数据来源、粒度和取数逻辑");const s=G.readKnowledgeFile(e.file);return G.publishPlanKnowledge({planId:e.plan,operation:t,authoring:s.knowledge||s})}r(re,"publishPlanKnowledgeFromFile");const kt=d({...l,name:"plan-knowledge",access:"write",description:"生成、版本化并保存完整方案知识库;服务器不可用时内部留存本地。最终返回方案地址和完整知识内容,不返回 SQL 或上传信息",args:[n("plan","方案的精确 id"),n("mode","create(新方案)或 update(修改原方案)",!1),n("file","AI 编写的方案知识 YAML/JSON 文件",!1)],func:r(async e=>{const t=await re(e);return f.emit("kb.write",{library:"plan-knowledge",op:e.mode||"create",plan:e.plan,version:t?.version??t?.planKnowledge?.version??null}),t},"func")}),St=d({...l,name:"plan-knowledge-get",access:"read",description:"读取方案当前知识库;修改原方案前先读。服务器不可用时读取本地待同步版本,不展示存储信息",args:[n("plan","方案的精确 id")],func:r(async e=>{const t=await G.readCurrentPlanKnowledge(e.plan);return f.emit("kb.read",{library:"plan-knowledge",op:"doc",plan:e.plan,version:t?.version??null,exists:t?.exists??null,chars:typeof t?.content=="string"?t.content.length:null}),t},"func")}),gt=d({...l,name:"deliverable",access:"write",description:"兼容命令:等同 plan-knowledge;需要 --mode 与 --file,不再输出数据集 SQL",args:[n("plan","方案的精确 id"),n("mode","create 或 update",!1),n("file","方案知识 YAML/JSON 文件",!1)],func:re}),It=d({...l,name:"backup",access:"read",description:"修改已有对象前保存带时间戳的备份(卡片/方案 JSON 或数据集 SQL),存到 ~/.linkai/artifacts/linkai/backups/",args:[n("type","card(卡片)| plan(方案)| ds(数据集)"),n("id","对象的精确 id")],func:r(async e=>{const t=r(s=>a.artifactsDir("backups",`${e.type}-${e.id}-${te()}.${s}`),"at");if(e.type==="card")return{type:"card",output:a.writeJsonFile(t("json"),await a.cardDetail(e.id))};if(e.type==="plan")return{type:"plan",output:a.writeJsonFile(t("json"),await a.planDetail(e.id,!0))};if(e.type==="ds")return{type:"ds",output:a.writeJsonFile(t("sql"),(await a.dsDetail(e.id)).sql||"")};throw new Error("type must be card, plan or ds")},"func")}),Ct=d({...l,name:"remove",access:"write",description:"按 folderId 删除数据集/卡片/方案/报表(id 来自各 *-list 命令)。需要用户明确授权",args:[n("folder","精确的 folderId,多个用逗号分隔")],func:r(async e=>{const t=String(e.folder).split(",").map(s=>s.trim()).filter(Boolean);return await a.post("/index-block/delete-folder-bussiness",t),{deleted:t.length,recycleBin:"POST /index-block/get-recycle-station-page-list"}},"func")}),Lt=d({...l,name:"clean",access:"write",description:"按 zz- 前缀清理试验对象,按依赖顺序删除:报表 → 方案 → 卡片 → 数据集",args:[n("prefix","以 zz- 开头的前缀,如 zz-销售看板-")],func:r(async e=>a.cleanPrefix(e.prefix),"func")}),xt=d({...l,name:"api-get",access:"read",description:"调用未封装的读接口(GET,路径可省略 /api/app);先查 references/api-catalog.md。数据源、SQL 类接口不开放",args:[n("path","接口路径,如 /index-block/folders?type=1"),{name:"raw",type:"boolean",default:!1,help:"返回完整响应"}],func:r(async e=>(await a.assertApiAllowedByPolicy(e.path),a.api("GET",e.path,void 0,{raw:!!e.raw})),"func")}),ge=/\/(get-|query|search)[\w-]*|\/[\w-]*-(list|detail|info)(?:[/?]|$)/i,At=d({...l,name:"api",access:"write",accessOf:r(e=>ge.test(String(e.path||""))?"read":"write","accessOf"),description:"调用未封装的写接口(POST/PUT/DELETE,可能新建或修改数据);先查 references/api-catalog.md。数据源、SQL 类接口不开放",args:[n("method","POST、PUT 或 DELETE"),n("path","接口路径"),n("body","请求体 JSON 文件路径",!1),{name:"raw",type:"boolean",default:!1,help:"返回完整响应(code/message/extendData)"}],func:r(async e=>{const t=String(e.method).toUpperCase();if(!["POST","PUT","DELETE"].includes(t))throw new Error("method must be POST, PUT or DELETE; use api-get for GET");return await a.assertApiAllowedByPolicy(e.path),a.api(t,e.path,e.body?A(e.body):t==="POST"?{}:void 0,{raw:!!e.raw})},"func")}),ie="create(新建方案)| modify(修改已有方案)| import(导入/更新数据)| query(只读查询分析)",Et=d({...l,name:"flow-start",access:"write",description:"开始一个工作流:接到业务任务时执行一次,之后的命令自动归入它;完成后用 flow-end 结束",args:[n("goal","这个任务要完成什么(用户原话概括)"),n("type",ie),n("plan","目标方案 id(修改已有方案时)",!1),n("model",'你(AI)当前使用的具体模型与版本,如 "Claude Opus 5.5"、"gpt-5.6";不确定就写你所知道的名称',!1)],func:r(async e=>{const t=f.startFlow({goal:e.goal,type:String(e.type||"").trim(),plan:e.plan||null,model:e.model||null});return{flow:t.id,goal:t.goal,type:t.type,plan:t.plan,model:f.currentModel(),next:"同一会话里的后续命令自动归入此工作流;并行开多个任务时,在命令上加 --flow <id> 指明。完成后 linkai flow-end --status delivered"}},"func")}),qt=d({...l,name:"flow-end",access:"write",description:"结束工作流:delivered(已交付)| abandoned(放弃)| failed(失败);默认结束当前工作流,或用 --flow <id> 指定",args:[n("status","delivered | abandoned | failed"),n("plan","交付的方案 id",!1),n("knowledge-version","方案知识库版本,如 v2",!1),n("note","结果说明或失败原因",!1)],func:r(async e=>{const t=Object.fromEntries(Object.entries({plan:e.plan,knowledgeVersion:e["knowledge-version"],note:e.note}).filter(([,o])=>o!=null&&o!=="")),s=f.endFlow({id:process.env.LINKAI_FLOW||void 0,status:String(e.status||"").trim(),result:t});return{flow:s.id,status:s.status,goal:s.goal,runs:s.runs,durationMinutes:Math.round(((s.endedAt??Date.now())-s.startedAt)/6e3)/10,result:s.result}},"func")}),Dt=d({...l,name:"flow-status",access:"read",description:"查看本机工作流:当前工作流、未完成或中断的工作流(含中断原因与下一步);--all 含已结束的",args:[{name:"all",type:"boolean",default:!1,help:"包括已结束和自动关闭的工作流(最近 7 天)"}],func:r(async e=>{const t=f.currentFlow(),s=(e.all?f.listFlows():f.unfinishedFlows()).slice(0,30).map(o=>({...ae(o),runs:o.runs,startedAt:new Date(o.startedAt).toISOString(),endedAt:o.endedAt?new Date(o.endedAt).toISOString():null,result:o.result}));return{current:t,idleMinutes:f.flowIdleMinutes(),flows:s}},"func")}),R={name:"worker",type:"string",required:!1,help:"执行者名称(也可用全局参数 --worker);同一会话保持不变,如 主控、sql-1、card-2"},T=r(e=>String(e.worker||C.currentWorker()||""),"workerOf"),U=n("board","任务板 id(collab-start 返回,如 b-1a2b3c4d)"),Ot=d({...l,name:"collab-start",access:"write",description:"多会话协同:创建任务板(协调者执行一次),返回任务板 id 交给各执行者",args:[n("goal","这次协同要完成什么"),n("plan","目标方案 id(修改已有方案时)",!1),n("type",`工作流类型:${ie};默认有 --plan 为 modify,否则 create`,!1),R],func:r(async e=>{const t=C.startBoard({goal:e.goal,plan:e.plan,worker:T(e)});return f.startFlow({id:t.id,source:"board",goal:t.goal,type:e.type||(e.plan?"modify":"create"),plan:e.plan||null}),{board:t.id,coordinator:t.coordinator,goal:t.goal,next:"用 collab-task 拆分任务;各执行者写共享对象前先 collab-claim,成果用 collab-note 共享;读命令不用认领"}},"func")}),Tt=d({...l,name:"collab-task",access:"write",description:"多会话协同:新增任务(--title),或更新任务状态(--id --status todo|doing|done|blocked --result)",args:[U,n("title","新任务标题(新增时)",!1),{name:"id",type:"int",required:!1,help:"要更新的任务编号"},n("status","todo | doing | done | blocked",!1),n("result","结果或阻塞原因(文件路径、对象 id、结论)",!1),n("assignee","分配给哪个执行者",!1),n("object","任务涉及的共享对象,逗号分隔,如 card:<id>,ds:<id>",!1),R],func:r(async e=>e.id?C.updateTask({board:e.board,worker:T(e),id:e.id,status:e.status,result:e.result,assignee:e.assignee}):C.addTask({board:e.board,worker:T(e),title:e.title,assignee:e.assignee,objects:e.object}),"func")}),Ft=d({...l,name:"collab-claim",access:"write",description:"多会话协同:写共享对象前认领(全部成功或全部不认领;重复认领即续期);被别人认领时返回 COLLAB_CONFLICT",args:[U,n("object","对象,逗号分隔:card:<id> ds:<id> plan:<id> knowledge:<planId> spec:<文件> report:<id> filter:<planId>"),{name:"ttl",type:"int",default:C.DEFAULT_TTL_SECONDS,help:"认领有效秒数(60-7200),到期自动释放"},n("note","在做什么",!1),R],func:r(async e=>C.claim({board:e.board,worker:T(e),objects:e.object,ttl:e.ttl,note:e.note}),"func")}),Nt=d({...l,name:"collab-release",access:"write",description:"多会话协同:释放认领(不指定对象则释放自己在该任务板上的全部认领)",args:[U,n("object","要释放的对象,逗号分隔",!1),R],func:r(async e=>C.release({board:e.board,worker:T(e),objects:e.object}),"func")}),$t=d({...l,name:"collab-note",access:"write",description:"多会话协同:共享成果或发现(已验证的 SQL 文件、建好的对象 id、口径结论、交接、阻塞),避免别人重复做",args:[U,n("text","内容"),n("kind","finding | result | handoff | blocker | decision(默认 finding)",!1),n("ref","相关文件路径或对象 id",!1),R],func:r(async e=>C.note({board:e.board,worker:T(e),text:e.text,kind:e.kind||"finding",ref:e.ref}),"func")}),_t=d({...l,name:"collab-status",access:"read",description:"多会话协同:查看任务板(任务、有效认领、最近成果);不带 --board 列出全部进行中的任务板",args:[n("board","任务板 id",!1),{name:"notes",type:"int",default:30,help:"显示最近多少条成果记录"}],func:r(async e=>C.status({board:e.board,notes:e.notes}),"func")}),Rt=d({...l,name:"collab-close",access:"write",description:"多会话协同:关闭任务板并释放其上全部认领,同时结束对应工作流(协调者在交付后执行)",args:[U,n("status","工作流结束状态:delivered | abandoned | failed(默认 delivered)",!1),n("note","结果说明",!1),R],func:r(async e=>{const t=C.closeBoard({board:e.board,worker:T(e)});let s=null;try{s=f.endFlow({id:t.board,status:e.status||"delivered",result:e.note?{note:e.note}:null}).status}catch(o){s=`未结束:${o.message}`}return{...t,flow:s}},"func")}),Bt=d({...fe,name:"shot",access:"write",description:"在浏览器中打开报表/方案页面并截图(需要 Chrome 与 LinkAI 扩展)",args:[n("url","页面地址,如 https://bi.linkmore.com/view/plan?plan=<planId>"),n("output","截图保存路径(默认 ~/.linkai/artifacts/linkai/shots/<时间>.png)",!1),{name:"wait",type:"int",default:12,help:"等待页面渲染的秒数"}],func:r(async(e,t)=>{const s=a.readConfig(),o=new URL(t.url);if(o.origin!==s.origin)throw new Error(`URL must be on ${s.origin}`);if(/\/login/i.test(o.pathname))throw new Error("Do not open /login: it clears localStorage");const c=await a.ensureSession();await e.goto(`${s.origin}/`),await e.evaluate(a.browserSessionScript(c)),await e.goto(o.href),await e.sleep(Number(t.wait));const i=await e.evaluate("(() => ({ title: document.title, onLogin: /login/i.test(location.pathname) }))()");if(i?.onLogin)throw new Error("Page redirected to /login: the synced session was rejected; run: linkai login");let u=null,S=null;try{const p=w.resolve(P(t.output,"shots",`${te()}.png`));y.mkdirSync(w.dirname(p),{recursive:!0}),u=await e.screenshot({path:p,fullPage:!1,format:"png"})||p}catch(p){S=String(p?.message||p).slice(0,200)}return{url:await e.getCurrentUrl(),title:i?.title,loggedIn:!0,output:u,screenshotError:S}},"func")});export{At as apiCommand,xt as apiGetCommand,$e as askCommand,It as backupCommand,yt as buildCommand,Ve as cardChartCommand,Xe as cardGanttCommand,ze as cardGetCommand,Je as cardListCommand,He as cardMeterCommand,We as cardSaveCommand,Ze as cardSortCommand,Ye as cardTableCommand,Lt as cleanCommand,Ft as collabClaimCommand,Rt as collabCloseCommand,$t as collabNoteCommand,Nt as collabReleaseCommand,Ot as collabStartCommand,_t as collabStatusCommand,Tt as collabTaskCommand,gt as deliverableCommand,Te as doctorCommand,Ue as dsCreateCommand,ve as dsFieldsCommand,Be as dsListCommand,Ke as dsMatchCommand,Qe as dsRefsCommand,Pe as dsSqlCommand,je as dsTablesCommand,Me as dsUpdateCommand,Ge as fillWriteCommand,qt as flowEndCommand,Et as flowStartCommand,Dt as flowStatusCommand,Re as fmtCommand,pt as kbCatalogCommand,dt as kbCommand,wt as kbDocCommand,mt as kbDocsCommand,ft as kbObjectCommand,bt as kbSearchCommand,ut as kbSourcesCommand,Ne as kindsCommand,De as loginCommand,Oe as logoutCommand,st as planAddCommand,nt as planCreateCommand,ot as planFilterCommand,at as planGetCommand,kt as planKnowledgeCommand,St as planKnowledgeGetCommand,tt as planListCommand,et as queryCommand,Ct as removeCommand,it as reportCardCommand,rt as reportListCommand,ct as reportPublishCommand,lt as resetFilterCommand,Bt as shotCommand,_e as sqlCheckCommand,ht as templateCommand,Fe as tenantsCommand};
|