@linkaicli/linkai-cli 1.0.18 → 1.0.19

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.
@@ -1,4 +1,4 @@
1
- import fs from"node:fs";import path from"node:path";import os from"node:os";import{fileURLToPath}from"node:url";import{cli,Strategy}from"@linkaicli/linkai-cli/registry";import{ArgumentError,AuthRequiredError,CliError}from"@linkaicli/linkai-cli/errors";import*as core from"./core.js";import*as build from"./build.js";import*as deliver from"./deliverable.js";import*as fillCard from"./fill-card.js";import*as collab from"./collab.js";import*as reuse from"./reuse.js";import*as dictionary from"./dictionary.js";import*as oplog from"@linkaicli/linkai-cli/oplog";const api={site:core.SITE_ID,browser:!1,defaultFormat:"json"},ui={site:core.SITE_ID,domain:"bi.linkmore.com",strategy:Strategy.UI,browser:!0,siteSession:"persistent",navigateBefore:!1,defaultFormat:"json"},str=(name,help,required=!0)=>({name,type:"string",required,help}),filter=(rows,search)=>search?rows.filter(r=>String(r.name).includes(search)):rows,json=file=>JSON.parse(core.readInputFile(file)),stamp=()=>new Date().toISOString().replace(/[-:]/g,"").replace("T","-").slice(0,15),outPath=(given,...fallback)=>given||core.artifactsDir(...fallback);function site(def){const func=def.func,guard=async run=>{try{return await run()}catch(e){throw e instanceof core.LoginRequiredError?new AuthRequiredError("bi.linkmore.com",e.message,"请用户执行 linkai login,在浏览器完成领谋授权"):e instanceof core.TenantMismatchError?new CliError("TENANT_MISMATCH",e.message,"先用 linkai login --tenant <租户名> 切换到方案规范指定的租户,再用 linkai status 确认;无法切换就停止并告诉用户"):e instanceof core.NetworkError?new CliError("NETWORK_ERROR",e.message,"网络暂时不通(已自动重试):等 1~2 分钟或请用户检查网络 / 代理后再执行;只读命令可直接重试,写命令先用只读命令确认结果再决定是否重做",1):(e instanceof core.ApiError||e instanceof core.KindError)&&e.kind?new CliError(e.kind,e.message,API_ERROR_HINTS[e.kind],1):e instanceof collab.CollabConflictError?new CliError("COLLAB_CONFLICT",e.message,"别的执行者正在改这个对象:先 linkai collab-status --board <任务板> 看分工,改做别的任务,或等对方释放;不要绕过",1):e}},checkCollab=a=>{def.access==="write"&&!def.name.startsWith("collab-")&&collab.assertWritable(collab.objectsForCommand(def.name,a||{},json))},wrapped=def.browser?async(page,a)=>guard(()=>(checkCollab(a),func(page,a))):async a=>guard(()=>(checkCollab(a),func(a)));return cli({...def,func:wrapped})}const API_ERROR_HINTS={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",CONFIG_INVALID:"配置违反领谋界面的保存规则(与在界面上点保存时的校验一致):按 message 逐条修正后再保存;规则见知识库 references/card.md、filters.md",FILE_NOT_FOUND:"文件路径不对:确认文件已写出、路径相对当前目录;不要原样重复执行"},loginCommand=site({...api,name:"login",access:"write",description:"首次打开领谋 OAuth 授权页;同账号切换租户复用授权,切换账号需显式 --switch-account",args:[str("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:async args=>{const timeoutSeconds=Number(args.timeout||300);if(!Number.isInteger(timeoutSeconds)||timeoutSeconds<1||timeoutSeconds>1800)throw new ArgumentError("timeout 必须是 1-1800 秒");if(!args["switch-account"]){let current=null;try{current=await core.ensureSession()}catch(error){if(!(error instanceof core.LoginRequiredError))throw error}if(current){const result=args.tenant?await core.switchTenant(current,args.tenant):{session:current,switched:!1};return result.switched&&(dictionary.resetReads("switch-tenant"),core.clearShortCache()),{login:result.switched?"oauth_tenant_switch":"oauth_existing",autoRelogin:!0,...core.spaceOf(result.session)}}}dictionary.resetReads("login"),core.clearShortCache();const session=await core.oauthLogin({tenant:args.tenant,timeoutSeconds,...args["no-browser"]?{browser:async()=>!1}:{},switchAccount:args["switch-account"]});return{login:"oauth",autoRelogin:!0,...core.spaceOf(session)}}}),logoutCommand=cli({...api,name:"logout",access:"write",description:"退出登录:撤销 OAuth 会话并删除本机登录信息",args:[],func:async()=>{const revoked=await core.oauthLogout();return core.forgetCredentials(),core.clearSession(),{logout:!0,oauthRevoked:revoked}}}),doctorCommand=site({...api,name:"status",access:"read",description:"查看登录状态、当前租户、是否自动续登、数据源与可用表数量",args:[],func:async()=>{const session=await core.ensureSession(),p=core.tokenPayload(session.token),ctx=await core.ctx(),allow=await core.allowedTables();return{loggedIn:!0,...core.spaceOf(session),autoRelogin:!0,authMode:"oauth",tokenHoursLeft:p.exp?Math.round((p.exp*1e3-Date.now())/36e4)/10:null,dataSourceId:ctx.dataSourceId,dataSources:ctx.dataSources,tables:allow.size,...unfinishedFlowsHint()}}});function describeFlow(f){const next=f.status==="interrupted"&&f.reason==="build-incomplete"?"用同一个 spec 再执行 build --execute 会断点续跑;不再继续就清理试搭对象后 flow-end --status abandoned":f.status==="interrupted"&&f.reason==="outcome-unknown"?"上次写操作结果未知:先用只读命令回读确认现状,不要直接重复执行写命令":`继续做就在命令上加 --flow ${f.id};不再继续就 linkai flow-end --flow ${f.id} --status abandoned`;return{flow:f.id,goal:f.goal,type:f.type,plan:f.plan,source:f.source,status:f.status,reason:f.reason,lastActivity:new Date(f.lastActivity).toISOString(),lastCommand:f.lastCommand,lastErrorCode:f.lastErrorCode,build:f.build,next}}function unfinishedFlowsHint(){try{const list=oplog.unfinishedFlows().filter(f=>f.id!==oplog.currentFlow()?.id).slice(0,5).map(describeFlow);return list.length?{unfinishedFlows:list}:{}}catch{return{}}}const tenantsCommand=site({...api,name:"tenants",access:"read",description:"查看可用租户、当前租户及下一步;用户完成 OAuth 授权后执行",args:[],func:async()=>{const s=await core.ensureSession(),list=await core.tenantList(s),current=list.find(t=>t.tenantId===s.tenantId),tenants=list.map(t=>({name:t.name,tenantId:t.tenantId,current:t.tenantId===s.tenantId}));let next;return list.length===1?next=`只有一个租户,当前租户是「${list[0].name}」。`:next=`当前租户是「${current?.name||""}」。同账号可直接执行 linkai login --tenant <租户名> 切换,无需重新授权。`,{tenantCount:list.length,single:list.length===1,current:current?.name||core.spaceOf(s).tenantName,tenants,next}}}),kindsCommand=site({...api,name:"kinds",access:"read",defaultFormat:"table",description:"列出 74 种图表类型、字段建议与可用状态",args:[],func:async()=>core.kinds()}),sqlCheckCommand=site({...api,name:"sql-check",access:"read",defaultFormat:"table",description:"校验数据集 SQL(只读、可用表)并列出字段;多个文件用逗号分隔一次校验(省去多次启动)",args:[str("file","SQL 文件路径;多个用逗号分隔,如 sql/a.sql,sql/b.sql")],func:async args=>{const files=String(args.file).split(/[,,]/).map(f=>f.trim()).filter(Boolean),fields=list=>list.map(f=>({sort:f.sort_num,field:f.field_name,type:f.field_type,dbType:f.type_name})),dictNotice=async()=>{try{await checkDictionary({enforce:!1})}catch(e){if(e instanceof core.LoginRequiredError)throw e}};if(files.length===1){nudgeBatchSqlCheck();const sql=core.readInputFile(files[0]);return core.precheckSql(sql),await dictNotice(),fields(await core.checkSql(sql))}await dictNotice();const results=[];for(const file of files)try{results.push({file,ok:!0,fields:fields(await core.checkSql(core.readInputFile(file)))})}catch(e){if(e instanceof core.LoginRequiredError)throw e;results.push({file,ok:!1,error:String(e.message).slice(0,500)})}return{total:files.length,failed:results.filter(r=>!r.ok).length,results}}});function nudgeBatchSqlCheck(){try{const file=path.join(os.homedir(),".linkai","oplog","hints.json");let state={};try{state=JSON.parse(fs.readFileSync(file,"utf8"))||{}}catch{}const now=Date.now(),recent=[...(state.sqlCheck||[]).filter(t=>now-t<5*6e4),now];state.sqlCheck=recent.slice(-10),recent.length>=3&&now-(state.sqlCheckNoticeAt||0)>10*6e4&&(state.sqlCheckNoticeAt=now,oplog.addNotice("最近几分钟在逐个校验 SQL:可以一次校验多个文件,如 linkai sql-check --file sql/a.sql,sql/b.sql,sql/c.sql(每条命令都有启动开销,逐个跑更慢),结果按文件分别列出。","sql-check-batch")),fs.mkdirSync(path.dirname(file),{recursive:!0}),fs.writeFileSync(file,JSON.stringify(state),{mode:384})}catch{}}const fmtCommand=site({...api,name:"fmt",access:"read",defaultFormat:"plain",description:"把 SQL 格式化为易读的多行文本",args:[str("file","SQL 文件路径")],func:async args=>core.formatSql(core.readInputFile(args.file))}),dsListCommand=site({...api,name:"ds-list",access:"read",defaultFormat:"table",description:"列出当前租户的数据集",args:[str("search","按名称关键字过滤(可选)",!1)],func:async args=>filter(await core.dsList(),args.search)}),dsTablesCommand=site({...api,name:"ds-tables",access:"read",defaultFormat:"table",description:"列出数据集 SQL 可以使用的表(视图)",args:[str("search","按表名关键字过滤(可选)",!1)],func:async args=>[...await core.allowedTables()].sort().filter(n=>!args.search||n.includes(String(args.search).toLowerCase())).map(table=>({table}))}),dsFieldsCommand=site({...api,name:"ds-fields",access:"read",defaultFormat:"table",description:"列出数据集字段(列名就是卡片引用的映射键)",args:[str("ds","数据集的精确 id")],func:async args=>(await core.dsFields(args.ds)).map(x=>({sort:x.sortNum,column:x.tableFieldComment,type:x.tableFieldType,argumentType:x.argumentType}))}),dsSqlCommand=site({...api,name:"ds-sql",access:"read",description:"把数据集当前保存的 SQL 读出到文件",args:[str("ds","数据集的精确 id"),str("output","输出 SQL 路径(默认 ~/.linkai/artifacts/linkai/sql/<dsId>.sql)",!1)],func:async args=>{const d=await core.dsDetail(args.ds),output=core.writeJsonFile(outPath(args.output,"sql",`${args.ds}.sql`),d.sql||"");return{name:d.name,lines:(d.sql||"").split(`
2
- `).length,output}}}),dsRefsCommand=site({...api,name:"ds-refs",access:"read",defaultFormat:"table",description:"列出引用该数据集的卡片",args:[str("ds","数据集的精确 id")],func:async args=>core.dsRefs(args.ds)}),dsMatchCommand=site({...api,name:"ds-match",access:"read",description:"复用查重:在当前租户里找与这段 SQL 完全一样或相似的数据集(建数据集前先查,不含 zz- 试搭对象)",args:[str("file","SQL 文件路径"),{name:"top",type:"int",default:3,help:"相似候选最多几个"}],func:async args=>{const m=reuse.matchDataset(core.readInputFile(args.file),await reuse.datasetIndex(),{top:Number(args.top)||3});return{...m,next:m.identical.length?`有完全一样的数据集,直接复用 ${m.identical[0].dsId}(build spec 的 datasets 写 id,或 spec 加 reuse: auto)`:m.similar.length?"有相似数据集:比较 columns 与口径后决定复用、在原数据集上调整(先 ds-refs 看影响),还是新建":"没有可复用的数据集,可以新建"}}}),dsCreateCommand=site({...api,name:"ds-create",access:"write",description:"新建 SQL 数据集(先做只读与可用表校验,并格式化 SQL);当前租户已有完全一样的数据集时拒绝,除非 --allow-duplicate",args:[str("name","数据集名称(租户内唯一;试验用 zz- 前缀)"),str("file","SQL 文件路径"),{name:"allow-duplicate",type:"boolean",default:!1,help:"已有完全一样的数据集仍要新建(需说明理由,如要独立演进口径)"}],func:async args=>{const sql=core.readInputFile(args.file);core.precheckSql(sql),await checkDictionary({enforce:!0});let match=null;try{match=reuse.matchDataset(sql,await reuse.datasetIndex())}catch(e){if(e instanceof core.LoginRequiredError)throw e}if(match?.identical.length&&!args["allow-duplicate"])throw new CliError("DUPLICATE_DATASET",`当前租户已有完全一样的数据集:${match.identical.map(d=>`「${d.name}」(${d.dsId})`).join("、")}`,"直接复用该数据集(build spec 的 datasets 写 id,或 spec 加 reuse: auto);确需独立一份才加 --allow-duplicate",1);const dsId=await core.dsSave({name:args.name,sql}),fields=await core.dsFields(dsId);return{dsId,name:args.name,fields:fields.map(f=>f.tableFieldComment),...match?.similar.length?{similar:match.similar,next:"已有相似数据集:确认口径确实不同;如可合并,后续改为复用"}:{}}}}),dsUpdateCommand=site({...api,name:"ds-update",access:"write",description:"原地更新数据集(id 不变,卡片映射不断)",args:[str("ds","数据集的精确 id"),str("name","数据集名称"),str("file","SQL 文件路径")],func:async args=>{const sql=core.readInputFile(args.file);core.precheckSql(sql);const current=await core.dsDetail(args.ds);if(current?.name===args.name&&current?.sql&&reuse.sameSql(sql,current.sql)){const fields2=await core.dsFields(args.ds);return{dsId:args.ds,sameId:!0,unchanged:!0,fields:fields2.map(f=>f.tableFieldComment),next:"SQL 和名称与线上一致,未重复保存;不要再次执行同样的更新"}}await checkDictionary({enforce:!0});const dsId=await core.dsSave({id:args.ds,name:args.name,sql,current}),fields=await core.dsFields(dsId);return{dsId,sameId:dsId===args.ds,fields:fields.map(f=>f.tableFieldComment)}}}),cardListCommand=site({...api,name:"card-list",access:"read",defaultFormat:"table",description:"列出当前租户的卡片",args:[str("search","按名称关键字过滤(可选)",!1)],func:async args=>filter(await core.cardList(),args.search)}),cardGetCommand=site({...api,name:"card-get",access:"read",description:"把卡片完整配置保存为 JSON 文件(修改前的基线,或 card-save 的输入)",args:[str("card","卡片的精确 id"),str("output","输出 JSON 路径(默认 ~/.linkai/artifacts/linkai/cards/<cardId>.json)",!1)],func:async args=>{const d=await core.cardDetail(args.card),output=core.writeJsonFile(outPath(args.output,"cards",`${args.card}.json`),d);return{name:d.name,fields:(d.indexBlockDataSettingDtos||[]).length,output}}}),cardSaveCommand=site({...api,name:"card-save",access:"write",description:"保存完整的卡片配置 JSON(唯一能让配置生效的保存方式)",args:[str("file","修改后的卡片 JSON 文件路径")],func:async args=>{const body=json(args.file);if(!body.id)throw new Error("Card payload must contain id");const cs=typeof body.chartSetting=="string"?JSON.parse(body.chartSetting||"{}"):body.chartSetting||{};body.chartSetting=JSON.stringify(cs),body.chartType=body.chartType||cs.id,body.chartParentType=body.chartParentType||cs.parentId,await core.post("/index-block/update-new",body),String(body.chartType||cs.id)==="agtable"&&cs.config?.fitTableWidth!==!0&&oplog.addNotice("这张 AG 表格没有开启自适应宽度(chartSetting.config.fitTableWidth)。按 BI 操作规则应设为 true,列宽 agColumnWidths 只作为比例(如等分统一 50);用户明确要求固定宽度时除外。","ag-fit-width");const after=await core.cardDetail(body.id);return{cardId:body.id,name:after.name,fields:(after.indexBlockDataSettingDtos||[]).length}}}),fillWriteCommand=site({...api,name:"fill-write",access:"write",description:"写自由填报数据:一条记录一个元素、字段一次带齐(默认只校验并预览,加 --execute 才写入)",args:[str("card","填报卡片的精确 id(usedIndexBlockId)"),str("file",'记录 JSON 数组文件:[{"字段名":"值",…,"_id":"已有记录ID(可选)"}]'),str("key","填报块:fillfieldid(第一个块)、fillfieldid1(第二个块)…",!1),{name:"batch",type:"int",default:100,help:"每次请求的记录数(1-500)"},{name:"execute",type:"boolean",default:!1,help:"确认后真正写入"}],func:async args=>{const batch=Number(args.batch||100);if(!Number.isInteger(batch)||batch<1||batch>500)throw new ArgumentError("batch 必须是 1-500");await core.assertCardInTenant(args.card);const detail=await core.cardDetail(args.card);if(detail?.isFreeFill===!1)throw new Error("该卡片不是自由填报卡片;fill-write 只支持自由填报");const block=fillCard.fillBlockOf(detail,args.key||"fillfieldid"),fillValues=fillCard.buildFillValues(block,json(args.file)),batches=[];for(let i=0;i<fillValues.length;i+=batch)batches.push(fillValues.slice(i,i+batch));const plan={cardId:args.card,name:detail.name,key:block.key,fields:block.fields,uniqueFields:block.uniqueFields,records:fillValues.length,updates:fillValues.filter(x=>x.value).length,batches:batches.length,sample:fillValues.slice(0,2)};if(!args.execute)return{...plan,executed:!1,next:"核对字段与记录数后,用户确认再加 --execute"};const written=[];for(const[i,part]of batches.entries())try{await core.post("/index-block/new-fill-field",{usedIndexBlockId:args.card,fillValues:part}),written.push(part.length)}catch(error){throw new Error(`第 ${i+1}/${batches.length} 批写入失败(已成功 ${written.length} 批、${written.reduce((a,b)=>a+b,0)} 条):${error.message}。先回读填报表按依据字段核对,再只补未成功的部分,不要整批重跑`)}return{...plan,executed:!0,written:written.reduce((a,b)=>a+b,0),next:"回读填报表本身:行数、依据字段无空值无重复、抽查整行字段完整;再核对看板"}}}),cardChartCommand=site({...api,name:"card-chart",access:"write",description:"按图表类型新建或覆盖图表卡片(指定维度、数值、系列列)",args:[str("ds","数据集的精确 id"),str("name","卡片名称"),str("kind","图表类型,如 column/stacked(linkai kinds 查看全部)"),str("dim","维度列名,多个用逗号分隔",!1),str("meas","数值列名,多个用逗号分隔"),str("series","系列列名(可选)",!1),str("card","要覆盖的已有卡片 id(可选)",!1)],func:async args=>core.cardChart({dsId:args.ds,name:args.name,kind:args.kind,dim:args.dim,meas:args.meas,series:args.series,id:args.card})}),cardMeterCommand=site({...api,name:"card-meter",access:"write",description:"新建或覆盖单数值指标卡(meter/list)",args:[str("ds","数据集的精确 id"),str("name","卡片名称"),str("col","数值列名"),str("subtitle","副标题",!1),str("color","数值颜色(十六进制,如 #E8611C)",!1),str("card","要覆盖的已有卡片 id(可选)",!1)],func:async args=>core.cardMeter({dsId:args.ds,name:args.name,col:args.col,subtitle:args.subtitle,color:args.color||void 0,id:args.card})}),cardTableCommand=site({...api,name:"card-table",access:"write",description:"按 spec JSON 新建或覆盖 AG 明细表(见 references/card.md)",args:[str("ds","数据集的精确 id"),str("spec","spec JSON 文件路径"),str("card","要覆盖的已有卡片 id(可选)",!1)],func:async args=>core.cardTable({dsId:args.ds,spec:json(args.spec),id:args.card})}),cardGanttCommand=site({...api,name:"card-gantt",access:"write",description:"按 spec JSON 新建或覆盖甘特图卡片(见 references/gantt.md)",args:[str("ds","数据集的精确 id"),str("spec","spec JSON 文件路径"),str("card","要覆盖的已有卡片 id(可选)",!1)],func:async args=>core.cardGantt({dsId:args.ds,spec:json(args.spec),id:args.card})}),cardSortCommand=site({...api,name:"card-sort",access:"write",description:'设置卡片持久排序("列:asc,列:desc"、.json 文件或 clear),并回读确认排序生效',args:[str("card","卡片的精确 id"),str("spec",'"waxis3:desc,ID:asc" | sort.json | clear')],func:async args=>{const spec=/\.json$/i.test(String(args.spec))?json(args.spec):args.spec;return core.cardSort(args.card,Array.isArray(spec)?spec:spec.sortField||spec)}}),queryCommand=site({...api,name:"query",access:"read",description:"按页面方式查询卡片数据;返回状态码、报错信息、行数和实际执行的 SQL",args:[str("card","卡片的精确 id"),{name:"page-size",type:"int",default:20,help:"取数行数"},str("filters","筛选条件 JSON 文件路径",!1),str("output","数据行保存路径(不在屏幕上输出数据)",!1)],func:async args=>{const r=await core.queryCard({cardId:args.card,pageSize:args["page-size"],filters:args.filters?json(args.filters):[]});return args.output&&core.writeJsonFile(args.output,r.data),delete r.data,r}}),planListCommand=site({...api,name:"plan-list",access:"read",defaultFormat:"table",description:"列出分析方案(看板)",args:[str("search","按名称关键字过滤(可选)",!1)],func:async args=>filter(await core.planList(),args.search)}),planGetCommand=site({...api,name:"plan-get",access:"read",description:"把方案详情保存为 JSON 文件(含租户模板筛选器)",args:[str("plan","方案的精确 id"),str("output","输出 JSON 路径(默认 ~/.linkai/artifacts/linkai/plans/<planId>.json)",!1),{name:"personal",type:"boolean",default:!1,help:"读取自己的个人筛选器副本"}],func:async args=>{const d=await core.planDetail(args.plan,!args.personal),output=core.writeJsonFile(outPath(args.output,"plans",`${args.plan}.json`),d);return{planId:args.plan,cards:(d.pageCards||[]).map(c=>({pageCardId:c.id,name:c.indexBlock?.name})),output}}}),planCreateCommand=site({...api,name:"plan-create",access:"write",description:"新建空的分析方案(看板)",args:[str("name","方案名称")],func:async args=>{const planId=await core.post("/page-setting/new-create-empty-plan",{name:args.name,isPrivate:!1,pageType:0});return oplog.addNotice(`手工搭建方案:完成后必须用 linkai plan-knowledge --plan ${planId} --mode create --file <知识文件> 交付方案知识库(铁律 2)。新建看板优先用 linkai build(自动生成知识库并验收)。`,"plan-knowledge"),{planId,name:args.name}}}),planAddCommand=site({...api,name:"plan-add",access:"write",description:"把卡片放到方案的指定位置;返回 pageCardId(绑定筛选器时使用)",args:[str("plan","方案的精确 id"),str("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:async args=>core.planAdd({planId:args.plan,cardId:args.card,x:args.x,y:args.y,w:args.w,h:args.h})}),planFilterCommand=site({...api,name:"plan-filter",access:"write",description:"保存方案筛选器为租户模板(全量覆盖),并重置自己的个人副本",args:[str("plan","方案的精确 id"),str("file","筛选器请求体 JSON 文件路径")],func:async args=>core.planFilter(args.plan,json(args.file))}),reportListCommand=site({...api,name:"report-list",access:"read",defaultFormat:"table",description:"列出数据报表(每个报表对应一张卡片)",args:[str("search","按名称关键字过滤(可选)",!1)],func:async args=>filter(await core.reportList(),args.search)}),reportCardCommand=site({...api,name:"report-card",access:"read",description:"查询卡片对应报表的 userPageCardId(重置报表筛选器时使用)",args:[str("card","卡片的精确 id")],func:async args=>{const d=await core.reportDetail(args.card);return{cardId:args.card,userPageCardId:d.id}}}),reportPublishCommand=site({...api,name:"report-publish",access:"write",description:"把卡片发布为数据报表",args:[str("card","卡片的精确 id"),str("name","报表名称")],func:async args=>{await core.post("/index-block/operate-report",{indexBlockId:args.card,name:args.name,tag:"",isPrivate:!1});const hit=(await core.reportList()).filter(r=>r.cardId===args.card);return{cardId:args.card,published:hit.length===1,url:`https://bi.linkmore.com/view/table?indexBlockId=${args.card}&status=view`}}}),resetFilterCommand=site({...api,name:"reset-filter",access:"write",description:"丢弃自己的个人筛选器副本,按模板重建(方案或报表卡片)",args:[str("plan","方案 id(方案筛选器)",!1),str("page-card","报表 userPageCardId 或方案 pageCardId(卡片筛选器)",!1)],func:async args=>{if(!!args.plan==!!args["page-card"])throw new Error("Pass exactly one of --plan or --page-card");const body=args.plan?{userPageSettingId:args.plan,filterSourceType:4}:{userPageCardId:args["page-card"],filterSourceType:2};return{filters:(await core.post("/page-setting/reset-user-filter",body)||[]).map(f=>f.labelName)}}}),PKG_ROOT=path.resolve(path.dirname(fileURLToPath(import.meta.url)),"..","..");try{core.setClientVersion(JSON.parse(fs.readFileSync(path.join(PKG_ROOT,"package.json"),"utf8")).version)}catch{}const KB_ROOTS={knowledge:path.join(PKG_ROOT,"knowledge"),templates:path.join(PKG_ROOT,"templates")};function walk(dir,base=dir){return fs.existsSync(dir)?fs.readdirSync(dir,{withFileTypes:!0}).flatMap(e=>{const p=path.join(dir,e.name);return e.isDirectory()?walk(p,base):e.name.startsWith(".")?[]:[path.relative(base,p)]}):[]}function readLocal(rootName,rel){const root=KB_ROOTS[rootName],target=path.resolve(root,rel);if(target!==root&&!target.startsWith(root+path.sep))throw new ArgumentError("路径必须在 knowledge/ 或 templates/ 之内");if(!fs.existsSync(target)||fs.statSync(target).isDirectory())throw new ArgumentError(`文件不存在:${rootName}/${rel}`,"执行 linkai kb 查看可用文件");return fs.readFileSync(target,"utf8")}function kbUnavailable(library){return new CliError("KB_UNAVAILABLE",`知识库服务暂不可用:${library}`,"稍后重试;持续出现请联系领谋",1)}function aiKbFailure(error){if(!(error instanceof core.AiKbError))return error;const codes={400:"AI_KB_BAD_REQUEST",403:"AI_KB_FORBIDDEN",404:"AI_KB_NOT_FOUND",409:"AI_KB_CONFLICT",503:"AI_KB_UNAVAILABLE"},hints={404:"确认 source、对象或文档属于当前租户;数据字典不会回退本地内容",409:"知识元数据未就绪时先联系知识库维护方;文档持续变化时稍后重试",503:"稍后重试;数据字典没有本地兜底,持续出现请联系领谋"};return new CliError(codes[error.code]||"AI_KB_ERROR",error.message,hints[error.code],1)}function intOption(value,fallback,name,min,max){const n=value==null||value===""?fallback:Number(value);if(!Number.isInteger(n)||n<min||n>max)throw new ArgumentError(`${name} 必须是 ${min}~${max} 的整数`);return n}async function requireDatasetSource(source){const context=await core.ctx();if(String(source).toLowerCase()!==String(context.dataSourceId).toLowerCase())throw new CliError("AI_KB_SOURCE_MISMATCH",`数据字典 source ${source} 与当前数据集 source ${context.dataSourceId} 不一致`,"不要跨 source 使用元数据;先确认服务端已为当前数据源完成探库,或确认 CLI 的数据源配置",1)}function localBiDocs(){return walk(path.join(KB_ROOTS.knowledge,"report-builder")).map(p=>({file:`knowledge/report-builder/${p}`,title:"",summary:"",library:"bi-builder",scope:"public",source:"bundled-fallback"}))}function localBiDoc(req){return{file:`knowledge/${req.canonicalPath}`,title:"",library:req.library,scope:req.scope,source:"bundled-fallback",content:readLocal("knowledge",req.canonicalPath)}}function logKb(op,result){const list=Array.isArray(result)?result:null,library=list?"bi-builder":result?.library||(String(result?.file||"").startsWith("templates/")?"templates":String(result?.file||"").includes("browser/")?"cli-docs":"bi-builder");return oplog.emit("kb.read",list?{library,op,docs:list.length,sources:[...new Set(list.map(x=>x?.source).filter(Boolean))]}:{library,op,file:result?.file??null,source:result?.source??null,chars:typeof result?.content=="string"?result.content.length:void 0}),result}function logDict(op,args,result,extra={}){return oplog.emit("kb.read",{library:"data-dictionary",op,source:args.source??null,...extra}),op!=="sources"&&args.source&&dictionary.recordRead({tenantId:core.readSession()?.tenantId,source:args.source,flowId:oplog.currentFlow()?.id??null,op}),result}const DICT_CACHE_MS=2*6e4;async function checkDictionary({enforce}){const session=await core.ensureSession(),c=await core.ctx(),st=await dictionary.dictionaryStatus({tenantId:session.tenantId,source:c.dataSourceId,flowId:oplog.currentFlow()?.id??null,fetchSources:()=>core.shortCache("dict:sources",DICT_CACHE_MS,async()=>(await core.aiKbSources()).sources||[]),fetchDocs:()=>core.shortCache(`dict:docs:${c.dataSourceId}`,DICT_CACHE_MS,async()=>((await core.aiKbDocs({source:c.dataSourceId,pageIndex:1,pageSize:20})).items||[]).map(d=>({updatedAt:d.updatedAt})))});if(core.logGuard("dictionary-fresh",st.ok,{tenantId:session.tenantId,source:c.dataSourceId,reason:st.reason??st.skipped??null,readAt:st.readAt??null,dictUpdatedAt:st.dictUpdatedAt??null,enforce}),st.skipped&&oplog.addNotice(`数据字典:${st.skipped}。`,"dictionary-skipped"),!st.ok){if(enforce)throw new CliError("DICTIONARY_REQUIRED",st.reason,dictionary.readHint(c.dataSourceId),1);oplog.addNotice(`数据字典:${st.reason}。正式写入(ds-create / ds-update / build --execute)前会被拒绝。${dictionary.readHint(c.dataSourceId)}`,"dictionary-stale")}return st}const top=(list,n,pick)=>Array.isArray(list)?list.slice(0,n).map(pick):[],kbCommand=site({...api,name:"kb",access:"read",description:"读取文件型 BI 搭建知识库(服务器优先、本地兜底);数据字典请使用 kb-sources / kb-search 等结构化命令",args:[str("file","文件路径,如 knowledge/report-builder/build-spec.md 或 templates/report-spec/report.example.yaml",!1),str("search","按关键字过滤目录",!1),str("library","文件型知识库:bi-builder;data-dictionary 请使用 kb-sources 等结构化命令;另预留 integration-flow | user",!1)],func:async args=>logKb(args.file?"doc":"catalog",await(async()=>{const raw=String(args.file||"").replace(/^\.?\//,"");if(raw.startsWith("templates/"))return{file:raw,source:"bundled",content:readLocal("templates",raw.slice(10))};if(raw==="knowledge/browser/README.md"||raw==="browser/README.md")return{file:"knowledge/browser/README.md",source:"bundled-cli",content:readLocal("knowledge","browser/README.md")};const keyword=args.search?String(args.search):"",wanted=args.library?String(args.library):"";if(wanted&&!core.KB_LIBRARIES[wanted])throw new ArgumentError(`未知知识库:${wanted}`);if(wanted==="data-dictionary"||/^(?:knowledge\/)?data-dictionary(?:\/|$)/.test(raw))throw new CliError("KB_STRUCTURED_API","数据字典不再按 Markdown 文件路径读取","先执行 linkai kb-sources,再使用 kb-search / kb-catalog / kb-object / kb-docs / kb-doc",1);if(wanted&&!core.KB_LIBRARIES[wanted].enabled)throw new CliError("KB_NOT_IMPLEMENTED",`知识库 ${wanted} 已预留,当前版本尚未启用`,void 0,1);if(!raw){const libraries=wanted?[wanted]:["bi-builder"],docs=[];for(const library of libraries)try{const catalog=await core.kbCatalog(library);library==="bi-builder"&&!(catalog.docs||[]).length?docs.push(...localBiDocs()):docs.push(...(catalog.docs||[]).map(d=>{const prefix=core.KB_LIBRARIES[library].pathPrefix,relative=core.kbRequest(d.path,library).path;return{file:`knowledge/${prefix}/${relative}`,title:d.title||"",summary:d.summary||"",library,scope:core.KB_LIBRARIES[library].scope,source:"server"}}))}catch(e){if(library==="bi-builder"&&e instanceof core.KbUnavailableError)docs.push(...localBiDocs());else throw e instanceof core.KbUnavailableError?kbUnavailable(library):e}const templates=!wanted||wanted==="bi-builder"?walk(KB_ROOTS.templates).map(t=>({file:`templates/${t}`,title:"报表模版",summary:"",source:"bundled"})):[];return[...docs,...templates].filter(d=>!keyword||d.file.includes(keyword)||d.title.includes(keyword))}const req=core.kbRequest(raw,wanted||void 0);try{const doc=await core.kbDoc(raw,wanted||void 0);return{file:`knowledge/${doc.path}`,title:doc.title||"",library:doc.library,scope:doc.scope,source:"server",content:doc.content||""}}catch(e){if(req.library==="bi-builder"&&(e instanceof core.KbUnavailableError||e instanceof core.KbDocumentNotFoundError)){if(!fs.existsSync(path.join(KB_ROOTS.knowledge,"report-builder")))throw kbUnavailable(req.library);return localBiDoc(req)}throw e instanceof core.KbUnavailableError?kbUnavailable(req.library):e}})())}),kbSourcesCommand=site({...api,name:"kb-sources",access:"read",defaultFormat:"table",description:"列出当前租户可读的数据字典数据源,并标记当前数据集所用 source;后续命令必须显式传该 ID",args:[],func:async()=>{try{const[{sources=[]},context]=await Promise.all([core.aiKbSources(),core.ctx()]),rows=sources.map(source=>({...source,selectedForDatasets:String(source.source).toLowerCase()===String(context.dataSourceId).toLowerCase()})),aligned=rows.some(source=>source.selectedForDatasets);return logDict("sources",{},null,{sources:rows.map(r=>({source:r.source,name:r.name})),aligned}),{datasetSourceId:context.dataSourceId,aligned,sources:rows,...aligned?{}:{warning:"数据字典没有返回当前数据集 source;不要跨 source 使用元数据,请先确认服务端探库或数据源配置"}}}catch(error){throw aiKbFailure(error)}}}),kbCatalogCommand=site({...api,name:"kb-catalog",access:"read",description:"分页列出指定 source 的可用视图/对象元数据",args:[str("source","数据源 ID(来自 kb-sources,必须与后续数据集 source 一致)"),str("search","表名、中文名或说明关键字",!1),str("page","页码,默认 1",!1),str("page-size","每页 1~100,默认 30",!1)],func:async args=>{try{await requireDatasetSource(args.source);const r=await core.aiKbCatalog({source:args.source,keyword:args.search,pageIndex:intOption(args.page,1,"page",1,2147483647),pageSize:intOption(args["page-size"],30,"page-size",1,100)});return logDict("catalog",args,r,{keyword:args.search??null,total:r?.totalCount??null})}catch(error){throw aiKbFailure(error)}}}),kbObjectCommand=site({...api,name:"kb-object",access:"read",description:"读取指定 source 中一个可见对象的字段与关联关系",args:[str("source","数据源 ID(来自 kb-sources)"),str("db","数据库/schema 名"),str("object","对象/视图名")],func:async args=>{try{await requireDatasetSource(args.source);const r=await core.aiKbObject({source:args.source,db:args.db,object:args.object});return logDict("object",args,r,{object:`${args.db}.${args.object}`})}catch(error){throw aiKbFailure(error)}}}),kbDocsCommand=site({...api,name:"kb-docs",access:"read",description:"分页列出指定 source 的业务口径、枚举、关系、数据点等知识文档",args:[str("source","数据源 ID(来自 kb-sources)"),str("category","caliber | enum | relation | datapoint | other",!1),str("search","标题或正文关键字",!1),str("page","页码,默认 1",!1),str("page-size","每页 1~100,默认 30",!1)],func:async args=>{const category=args.category?String(args.category):void 0;if(category&&!["caliber","enum","relation","datapoint","other"].includes(category))throw new ArgumentError("category 必须是 caliber、enum、relation、datapoint、other 之一");try{await requireDatasetSource(args.source);const r=await core.aiKbDocs({source:args.source,category,keyword:args.search,pageIndex:intOption(args.page,1,"page",1,2147483647),pageSize:intOption(args["page-size"],30,"page-size",1,100)});return logDict("docs",args,r,{category:category??null,keyword:args.search??null,total:r?.totalCount??null})}catch(error){throw aiKbFailure(error)}}}),kbDocCommand=site({...api,name:"kb-doc",access:"read",description:"一致性读取一篇完整数据字典文档;自动按 nextOffset 分页,版本变化时从头重读",args:[str("source","数据源 ID(来自 kb-sources)"),str("doc","文档 ID(来自 kb-docs 或 kb-search)"),str("page-size","单次正文字符数,1~20000,默认 20000",!1)],func:async args=>{try{await requireDatasetSource(args.source);const r=await core.aiKbDoc({source:args.source,doc:args.doc,limit:intOption(args["page-size"],2e4,"page-size",1,2e4)});return logDict("doc",args,r,{doc:args.doc,title:r?.title??null,category:r?.category??null,chars:r?.totalChars??null})}catch(error){throw aiKbFailure(error)}}}),kbSearchCommand=site({...api,name:"kb-search",access:"read",description:"按自然语言检索指定 source 的候选视图与业务知识;相关度不是正确率,使用前继续回读对象/文档核对",args:[str("source","数据源 ID(来自 kb-sources)"),str("question","业务问题,最长 2000 字"),str("top-tables","最多返回表数,1~10,默认 5",!1),str("top-docs","最多返回文档数,1~10,默认 3",!1)],func:async args=>{try{await requireDatasetSource(args.source);const r=await core.aiKbSearch({source:args.source,question:args.question,topTables:intOption(args["top-tables"],5,"top-tables",1,10),topDocs:intOption(args["top-docs"],3,"top-docs",1,10)});return logDict("search",args,r,{question:String(args.question||"").slice(0,200),hitTables:top(r?.tables,10,t=>`${t.db}.${t.object}`),hitDocs:top(r?.docs,10,d=>({doc:d.doc,title:d.title??null,category:d.category??null})),emptyReason:r?.emptyReason??null})}catch(error){throw aiKbFailure(error)}}}),templateCommand=cli({...api,name:"template",access:"write",description:"复制报表 spec 与示例文件;SQL 示例必须由 AI 按用户需求、租户数据字典和确认口径重写,CLI 不生成业务 SQL",args:[str("output","目标目录(默认 ./linkai-report),需不存在或为空",!1)],func:async args=>{const dest=path.resolve(String(args.output||"linkai-report"));if(fs.existsSync(dest)&&fs.readdirSync(dest).length)throw new ArgumentError(`目录已存在且不为空:${dest}`,"换一个 --output 目录");return fs.cpSync(path.join(KB_ROOTS.templates,"report-spec"),dest,{recursive:!0}),{output:dest,files:walk(dest),next:`修改 ${path.join(dest,"report.example.yaml")} 与 sql/ 下的 SQL,然后 linkai build --spec ${path.join(dest,"report.example.yaml")}`}}}),buildCommand=site({...api,name:"build",access:"write",description:"严格按 BI 公共知识库执行 AI 提供的 SQL 与报表 spec(CLI 不生成业务 SQL)。默认只校验并给出执行计划;--execute 才写入;含正式名还需 --confirm",args:[str("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:async args=>{const loaded=build.loadSpec(args.spec),writesSql=(loaded.spec.datasets||[]).some(d=>!d.id&&d.sql),beforeWrite=async()=>{const session=await core.ensureSession(),cur=core.spaceOf(session);if(loaded.spec.tenant&&![cur.tenantId,cur.tenantName,cur.tenantCode].includes(String(loaded.spec.tenant)))throw new ArgumentError(`当前登录的租户是「${cur.tenantName}」,与 spec.tenant「${loaded.spec.tenant}」不一致`,"确认目标租户后执行 linkai login --tenant <租户名>,或修改 spec.tenant");writesSql&&await checkDictionary({enforce:!0})},result=await build.runBuild(loaded,{execute:!!args.execute,confirm:!!args.confirm,style:!args["no-style"],beforeWrite});if(!args.execute&&writesSql&&result.ok!==!1)try{result.dictionary=await checkDictionary({enforce:!1})}catch(e){if(e instanceof core.LoginRequiredError)result.dictionary={ok:!1,reason:"未登录"};else throw e}if(result.ok===!1&&(result.stage==="validate"||result.stage==="authorize"))throw new ArgumentError(`build ${result.stage} failed:
3
- - ${result.errors.join(`
4
- - `)}`,result.stage==="authorize"?"把正式名清单给用户确认,得到明确授权后再加 --confirm":"按错误逐条修改 spec 后重新 dry-run");return result}});async function publishPlanKnowledgeFromFile(args){const mode=String(args.mode||"");if(!["create","update"].includes(mode))throw new ArgumentError("必须明确 --mode create 或 --mode update;用户未说明新建还是修改时,先主动询问用户");if(!args.file)throw new ArgumentError("缺少 --file <方案知识 YAML/JSON>","文件必须描述方案内容、解决的问题,以及每张业务卡片的内容、数据来源、粒度和取数逻辑");const input=deliver.readKnowledgeFile(args.file);return deliver.publishPlanKnowledge({planId:args.plan,operation:mode,authoring:input.knowledge||input})}const planKnowledgeCommand=site({...api,name:"plan-knowledge",access:"write",description:"生成、版本化并保存完整方案知识库;服务器不可用时内部留存本地。最终返回方案地址和完整知识内容,不返回 SQL 或上传信息",args:[str("plan","方案的精确 id"),str("mode","create(新方案)或 update(修改原方案)",!1),str("file","AI 编写的方案知识 YAML/JSON 文件",!1)],func:async args=>{const r=await publishPlanKnowledgeFromFile(args);return oplog.emit("kb.write",{library:"plan-knowledge",op:args.mode||"create",plan:args.plan,version:r?.version??r?.planKnowledge?.version??null}),r}}),planKnowledgeGetCommand=site({...api,name:"plan-knowledge-get",access:"read",description:"读取方案当前知识库;修改原方案前先读。服务器不可用时读取本地待同步版本,不展示存储信息",args:[str("plan","方案的精确 id")],func:async args=>{const r=await deliver.readCurrentPlanKnowledge(args.plan);return oplog.emit("kb.read",{library:"plan-knowledge",op:"doc",plan:args.plan,version:r?.version??null,exists:r?.exists??null,chars:typeof r?.content=="string"?r.content.length:null}),r}}),deliverableCommand=site({...api,name:"deliverable",access:"write",description:"兼容命令:等同 plan-knowledge;需要 --mode 与 --file,不再输出数据集 SQL",args:[str("plan","方案的精确 id"),str("mode","create 或 update",!1),str("file","方案知识 YAML/JSON 文件",!1)],func:publishPlanKnowledgeFromFile}),backupCommand=site({...api,name:"backup",access:"read",description:"修改已有对象前保存带时间戳的备份(卡片/方案 JSON 或数据集 SQL),存到 ~/.linkai/artifacts/linkai/backups/",args:[str("type","card(卡片)| plan(方案)| ds(数据集)"),str("id","对象的精确 id")],func:async args=>{const at=ext=>core.artifactsDir("backups",`${args.type}-${args.id}-${stamp()}.${ext}`);if(args.type==="card")return{type:"card",output:core.writeJsonFile(at("json"),await core.cardDetail(args.id))};if(args.type==="plan")return{type:"plan",output:core.writeJsonFile(at("json"),await core.planDetail(args.id,!0))};if(args.type==="ds")return{type:"ds",output:core.writeJsonFile(at("sql"),(await core.dsDetail(args.id)).sql||"")};throw new Error("type must be card, plan or ds")}}),removeCommand=site({...api,name:"remove",access:"write",description:"按 folderId 删除数据集/卡片/方案/报表(id 来自各 *-list 命令)。需要用户明确授权",args:[str("folder","精确的 folderId,多个用逗号分隔")],func:async args=>{const ids=String(args.folder).split(",").map(s=>s.trim()).filter(Boolean);return await core.post("/index-block/delete-folder-bussiness",ids),{deleted:ids.length,recycleBin:"POST /index-block/get-recycle-station-page-list"}}}),cleanCommand=site({...api,name:"clean",access:"write",description:"按 zz- 前缀清理试验对象,按依赖顺序删除:报表 → 方案 → 卡片 → 数据集",args:[str("prefix","以 zz- 开头的前缀,如 zz-销售看板-")],func:async args=>core.cleanPrefix(args.prefix)}),apiGetCommand=site({...api,name:"api-get",access:"read",description:"调用未封装的读接口(GET,路径可省略 /api/app);先查 references/api-catalog.md。数据源、SQL 类接口不开放",args:[str("path","接口路径,如 /index-block/folders?type=1"),{name:"raw",type:"boolean",default:!1,help:"返回完整响应"}],func:async args=>(core.assertApiAllowed(args.path),core.api("GET",args.path,void 0,{raw:!!args.raw}))}),READ_POST_API=/\/(get-|query|search)[\w-]*|\/[\w-]*-(list|detail|info)(?:[/?]|$)/i,apiCommand=site({...api,name:"api",access:"write",accessOf:args=>READ_POST_API.test(String(args.path||""))?"read":"write",description:"调用未封装的写接口(POST/PUT/DELETE,可能新建或修改数据);先查 references/api-catalog.md。数据源、SQL 类接口不开放",args:[str("method","POST、PUT 或 DELETE"),str("path","接口路径"),str("body","请求体 JSON 文件路径",!1),{name:"raw",type:"boolean",default:!1,help:"返回完整响应(code/message/extendData)"}],func:async args=>{const m=String(args.method).toUpperCase();if(!["POST","PUT","DELETE"].includes(m))throw new Error("method must be POST, PUT or DELETE; use api-get for GET");return core.assertApiAllowed(args.path),core.api(m,args.path,args.body?json(args.body):m==="POST"?{}:void 0,{raw:!!args.raw})}}),FLOW_TYPES_HELP="create(新建方案)| modify(修改已有方案)| import(导入/更新数据)| query(只读查询分析)",flowStartCommand=site({...api,name:"flow-start",access:"write",description:"开始一个工作流:接到业务任务时执行一次,之后的命令自动归入它;完成后用 flow-end 结束",args:[str("goal","这个任务要完成什么(用户原话概括)"),str("type",FLOW_TYPES_HELP),str("plan","目标方案 id(修改已有方案时)",!1),str("model",'你(AI)当前使用的具体模型与版本,如 "Claude Opus 5.5"、"gpt-5.6";不确定就写你所知道的名称',!1)],func:async a=>{const f=oplog.startFlow({goal:a.goal,type:String(a.type||"").trim(),plan:a.plan||null,model:a.model||null});return{flow:f.id,goal:f.goal,type:f.type,plan:f.plan,model:oplog.currentModel(),next:"同一会话里的后续命令自动归入此工作流;并行开多个任务时,在命令上加 --flow <id> 指明。完成后 linkai flow-end --status delivered"}}}),flowEndCommand=site({...api,name:"flow-end",access:"write",description:"结束工作流:delivered(已交付)| abandoned(放弃)| failed(失败);默认结束当前工作流,或用 --flow <id> 指定",args:[str("status","delivered | abandoned | failed"),str("plan","交付的方案 id",!1),str("knowledge-version","方案知识库版本,如 v2",!1),str("note","结果说明或失败原因",!1)],func:async a=>{const result=Object.fromEntries(Object.entries({plan:a.plan,knowledgeVersion:a["knowledge-version"],note:a.note}).filter(([,v])=>v!=null&&v!=="")),f=oplog.endFlow({id:process.env.LINKAI_FLOW||void 0,status:String(a.status||"").trim(),result});return{flow:f.id,status:f.status,goal:f.goal,runs:f.runs,durationMinutes:Math.round(((f.endedAt??Date.now())-f.startedAt)/6e3)/10,result:f.result}}}),flowStatusCommand=site({...api,name:"flow-status",access:"read",description:"查看本机工作流:当前工作流、未完成或中断的工作流(含中断原因与下一步);--all 含已结束的",args:[{name:"all",type:"boolean",default:!1,help:"包括已结束和自动关闭的工作流(最近 7 天)"}],func:async a=>{const current=oplog.currentFlow(),list=(a.all?oplog.listFlows():oplog.unfinishedFlows()).slice(0,30).map(f=>({...describeFlow(f),runs:f.runs,startedAt:new Date(f.startedAt).toISOString(),endedAt:f.endedAt?new Date(f.endedAt).toISOString():null,result:f.result}));return{current,idleMinutes:oplog.flowIdleMinutes(),flows:list}}}),workerArg={name:"worker",type:"string",required:!1,help:"执行者名称(也可用全局参数 --worker);同一会话保持不变,如 主控、sql-1、card-2"},workerOf=a=>String(a.worker||collab.currentWorker()||""),boardArg=str("board","任务板 id(collab-start 返回,如 b-1a2b3c4d)"),collabStartCommand=site({...api,name:"collab-start",access:"write",description:"多会话协同:创建任务板(协调者执行一次),返回任务板 id 交给各执行者",args:[str("goal","这次协同要完成什么"),str("plan","目标方案 id(修改已有方案时)",!1),str("type",`工作流类型:${FLOW_TYPES_HELP};默认有 --plan 为 modify,否则 create`,!1),workerArg],func:async a=>{const board=collab.startBoard({goal:a.goal,plan:a.plan,worker:workerOf(a)});return oplog.startFlow({id:board.id,source:"board",goal:board.goal,type:a.type||(a.plan?"modify":"create"),plan:a.plan||null}),{board:board.id,coordinator:board.coordinator,goal:board.goal,next:"用 collab-task 拆分任务;各执行者写共享对象前先 collab-claim,成果用 collab-note 共享;读命令不用认领"}}}),collabTaskCommand=site({...api,name:"collab-task",access:"write",description:"多会话协同:新增任务(--title),或更新任务状态(--id --status todo|doing|done|blocked --result)",args:[boardArg,str("title","新任务标题(新增时)",!1),{name:"id",type:"int",required:!1,help:"要更新的任务编号"},str("status","todo | doing | done | blocked",!1),str("result","结果或阻塞原因(文件路径、对象 id、结论)",!1),str("assignee","分配给哪个执行者",!1),str("object","任务涉及的共享对象,逗号分隔,如 card:<id>,ds:<id>",!1),workerArg],func:async a=>a.id?collab.updateTask({board:a.board,worker:workerOf(a),id:a.id,status:a.status,result:a.result,assignee:a.assignee}):collab.addTask({board:a.board,worker:workerOf(a),title:a.title,assignee:a.assignee,objects:a.object})}),collabClaimCommand=site({...api,name:"collab-claim",access:"write",description:"多会话协同:写共享对象前认领(全部成功或全部不认领;重复认领即续期);被别人认领时返回 COLLAB_CONFLICT",args:[boardArg,str("object","对象,逗号分隔:card:<id> ds:<id> plan:<id> knowledge:<planId> spec:<文件> report:<id> filter:<planId>"),{name:"ttl",type:"int",default:collab.DEFAULT_TTL_SECONDS,help:"认领有效秒数(60-7200),到期自动释放"},str("note","在做什么",!1),workerArg],func:async a=>collab.claim({board:a.board,worker:workerOf(a),objects:a.object,ttl:a.ttl,note:a.note})}),collabReleaseCommand=site({...api,name:"collab-release",access:"write",description:"多会话协同:释放认领(不指定对象则释放自己在该任务板上的全部认领)",args:[boardArg,str("object","要释放的对象,逗号分隔",!1),workerArg],func:async a=>collab.release({board:a.board,worker:workerOf(a),objects:a.object})}),collabNoteCommand=site({...api,name:"collab-note",access:"write",description:"多会话协同:共享成果或发现(已验证的 SQL 文件、建好的对象 id、口径结论、交接、阻塞),避免别人重复做",args:[boardArg,str("text","内容"),str("kind","finding | result | handoff | blocker | decision(默认 finding)",!1),str("ref","相关文件路径或对象 id",!1),workerArg],func:async a=>collab.note({board:a.board,worker:workerOf(a),text:a.text,kind:a.kind||"finding",ref:a.ref})}),collabStatusCommand=site({...api,name:"collab-status",access:"read",description:"多会话协同:查看任务板(任务、有效认领、最近成果);不带 --board 列出全部进行中的任务板",args:[str("board","任务板 id",!1),{name:"notes",type:"int",default:30,help:"显示最近多少条成果记录"}],func:async a=>collab.status({board:a.board,notes:a.notes})}),collabCloseCommand=site({...api,name:"collab-close",access:"write",description:"多会话协同:关闭任务板并释放其上全部认领,同时结束对应工作流(协调者在交付后执行)",args:[boardArg,str("status","工作流结束状态:delivered | abandoned | failed(默认 delivered)",!1),str("note","结果说明",!1),workerArg],func:async a=>{const closed=collab.closeBoard({board:a.board,worker:workerOf(a)});let flow=null;try{flow=oplog.endFlow({id:closed.board,status:a.status||"delivered",result:a.note?{note:a.note}:null}).status}catch(e){flow=`未结束:${e.message}`}return{...closed,flow}}}),shotCommand=site({...ui,name:"shot",access:"write",description:"在浏览器中打开报表/方案页面并截图(需要 Chrome 与 LinkAI 扩展)",args:[str("url","页面地址,如 https://bi.linkmore.com/view/plan?plan=<planId>"),str("output","截图保存路径(默认 ~/.linkai/artifacts/linkai/shots/<时间>.png)",!1),{name:"wait",type:"int",default:12,help:"等待页面渲染的秒数"}],func:async(page,args)=>{const config=core.readConfig(),target=new URL(args.url);if(target.origin!==config.origin)throw new Error(`URL must be on ${config.origin}`);if(/\/login/i.test(target.pathname))throw new Error("Do not open /login: it clears localStorage");const session=await core.ensureSession();await page.goto(`${config.origin}/`),await page.evaluate(core.browserSessionScript(session)),await page.goto(target.href),await page.sleep(Number(args.wait));const state=await page.evaluate("(() => ({ title: document.title, onLogin: /login/i.test(location.pathname) }))()");if(state?.onLogin)throw new Error("Page redirected to /login: the synced session was rejected; run: linkai login");let output=null,screenshotError=null;try{const file=path.resolve(outPath(args.output,"shots",`${stamp()}.png`));fs.mkdirSync(path.dirname(file),{recursive:!0}),output=await page.screenshot({path:file,fullPage:!1,format:"png"})||file}catch(e){screenshotError=String(e?.message||e).slice(0,200)}return{url:await page.getCurrentUrl(),title:state?.title,loggedIn:!0,output,screenshotError}}});export{apiCommand,apiGetCommand,backupCommand,buildCommand,cardChartCommand,cardGanttCommand,cardGetCommand,cardListCommand,cardMeterCommand,cardSaveCommand,cardSortCommand,cardTableCommand,cleanCommand,collabClaimCommand,collabCloseCommand,collabNoteCommand,collabReleaseCommand,collabStartCommand,collabStatusCommand,collabTaskCommand,deliverableCommand,doctorCommand,dsCreateCommand,dsFieldsCommand,dsListCommand,dsMatchCommand,dsRefsCommand,dsSqlCommand,dsTablesCommand,dsUpdateCommand,fillWriteCommand,flowEndCommand,flowStartCommand,flowStatusCommand,fmtCommand,kbCatalogCommand,kbCommand,kbDocCommand,kbDocsCommand,kbObjectCommand,kbSearchCommand,kbSourcesCommand,kindsCommand,loginCommand,logoutCommand,planAddCommand,planCreateCommand,planFilterCommand,planGetCommand,planKnowledgeCommand,planKnowledgeGetCommand,planListCommand,queryCommand,removeCommand,reportCardCommand,reportListCommand,reportPublishCommand,resetFilterCommand,shotCommand,sqlCheckCommand,templateCommand,tenantsCommand};
1
+ var Z=Object.defineProperty;var r=(e,t)=>Z(e,"name",{value:t,configurable:!0});import w from"node:fs";import f from"node:path";import ee from"node:os";import{fileURLToPath as te}from"node:url";import{cli as v,Strategy as ae}from"@linkaicli/linkai-cli/registry";import{ArgumentError as h,AuthRequiredError as ne,CliError as g}from"@linkaicli/linkai-cli/errors";import*as a from"./core.js";import*as J from"./build.js";import*as P from"./deliverable.js";import*as z from"./fill-card.js";import*as b from"./collab.js";import*as O from"./reuse.js";import*as x from"./dictionary.js";import*as m from"@linkaicli/linkai-cli/oplog";const l={site:a.SITE_ID,browser:!1,defaultFormat:"json"},se={site:a.SITE_ID,domain:"bi.linkmore.com",strategy:ae.UI,browser:!0,siteSession:"persistent",navigateBefore:!1,defaultFormat:"json"},s=r((e,t,n=!0)=>({name:e,type:"string",required:n,help:t}),"str"),q=r((e,t)=>t?e.filter(n=>String(n.name).includes(t)):e,"filter"),S=r(e=>JSON.parse(a.readInputFile(e)),"json"),M=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 ne("bi.linkmore.com",u.message,"请用户执行 linkai login,在浏览器完成领谋授权"):u instanceof a.TenantMismatchError?new g("TENANT_MISMATCH",u.message,"先用 linkai login --tenant <租户名> 切换到方案规范指定的租户,再用 linkai status 确认;无法切换就停止并告诉用户"):u instanceof a.NetworkError?new g("NETWORK_ERROR",u.message,"网络暂时不通(已自动重试):等 1~2 分钟或请用户检查网络 / 代理后再执行;只读命令可直接重试,写命令先用只读命令确认结果再决定是否重做",1):(u instanceof a.ApiError||u instanceof a.KindError)&&u.kind?new g(u.kind,u.message,oe[u.kind],1):u instanceof b.CollabConflictError?new g("COLLAB_CONFLICT",u.message,"别的执行者正在改这个对象:先 linkai collab-status --board <任务板> 看分工,改做别的任务,或等对方释放;不要绕过",1):u}},"guard"),o=r(i=>{e.access==="write"&&!e.name.startsWith("collab-")&&b.assertWritable(b.objectsForCommand(e.name,i||{},S))},"checkCollab"),c=e.browser?async(i,u)=>n(()=>(o(u),t(i,u))):async i=>n(()=>(o(i),t(i)));return v({...e,func:c})}r(d,"site");const oe={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",CONFIG_INVALID:"配置违反领谋界面的保存规则(与在界面上点保存时的校验一致):按 message 逐条修正后再保存;规则见知识库 references/card.md、filters.md",FILE_NOT_FOUND:"文件路径不对:确认文件已写出、路径相对当前目录;不要原样重复执行"},ye=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&&(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 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")}),ge=v({...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")}),ke=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,...re()}},"func")});function W(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(W,"describeFlow");function re(){try{const e=m.unfinishedFlows().filter(t=>t.id!==m.currentFlow()?.id).slice(0,5).map(W);return e.length?{unfinishedFlows:e}:{}}catch{return{}}}r(re,"unfinishedFlowsHint");const Se=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")}),Ie=d({...l,name:"kinds",access:"read",defaultFormat:"table",description:"列出 74 种图表类型、字段建议与可用状态",args:[],func:r(async()=>a.kinds(),"func")}),Ce=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 N({enforce:!1})}catch(i){if(i instanceof a.LoginRequiredError)throw i}},"dictNotice");if(t.length===1){ie();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 ie(){try{const e=f.join(ee.homedir(),".linkai","oplog","hints.json");let t={};try{t=JSON.parse(w.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,m.addNotice("最近几分钟在逐个校验 SQL:可以一次校验多个文件,如 linkai sql-check --file sql/a.sql,sql/b.sql,sql/c.sql(每条命令都有启动开销,逐个跑更慢),结果按文件分别列出。","sql-check-batch")),w.mkdirSync(f.dirname(e),{recursive:!0}),w.writeFileSync(e,JSON.stringify(t),{mode:384})}catch{}}r(ie,"nudgeBatchSqlCheck");const xe=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")}),Le=d({...l,name:"ds-list",access:"read",defaultFormat:"table",description:"列出当前租户的数据集",args:[s("search","按名称关键字过滤(可选)",!1)],func:r(async e=>q(await a.dsList(),e.search),"func")}),Ee=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")}),Ae=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")}),De=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")}),Oe=d({...l,name:"ds-refs",access:"read",defaultFormat:"table",description:"列出引用该数据集的卡片",args:[s("ds","数据集的精确 id")],func:r(async e=>a.dsRefs(e.ds),"func")}),Fe=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=O.matchDataset(a.readInputFile(e.file),await O.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")}),Te=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 N({enforce:!0});let n=null;try{n=O.matchDataset(t,await O.datasetIndex())}catch(i){if(i instanceof a.LoginRequiredError)throw i}if(n?.identical.length&&!e["allow-duplicate"])throw new g("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")}),Ne=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&&O.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 N({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")}),_e=d({...l,name:"card-list",access:"read",defaultFormat:"table",description:"列出当前租户的卡片",args:[s("search","按名称关键字过滤(可选)",!1)],func:r(async e=>q(await a.cardList(),e.search),"func")}),qe=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")}),Re=d({...l,name:"card-save",access:"write",description:"保存完整的卡片配置 JSON(唯一能让配置生效的保存方式)",args:[s("file","修改后的卡片 JSON 文件路径")],func:r(async e=>{const t=S(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&&m.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")}),$e=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=z.fillBlockOf(n,e.key||"fillfieldid"),c=z.buildFillValues(o,S(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 k=[];for(const[p,y]of i.entries())try{await a.post("/index-block/new-fill-field",{usedIndexBlockId:e.card,fillValues:y}),k.push(y.length)}catch(D){throw new Error(`第 ${p+1}/${i.length} 批写入失败(已成功 ${k.length} 批、${k.reduce((B,j)=>B+j,0)} 条):${D.message}。先回读填报表按依据字段核对,再只补未成功的部分,不要整批重跑`)}return{...u,executed:!0,written:k.reduce((p,y)=>p+y,0),next:"回读填报表本身:行数、依据字段无空值无重复、抽查整行字段完整;再核对看板"}},"func")}),Be=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")}),je=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")}),ve=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:S(e.spec),id:e.card}),"func")}),Pe=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:S(e.spec),id:e.card}),"func")}),Ke=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))?S(e.spec):e.spec;return a.cardSort(e.card,Array.isArray(t)?t:t.sortField||t)},"func")}),Qe=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?S(e.filters):[]});return e.output&&a.writeJsonFile(e.output,t.data),delete t.data,t},"func")}),Ue=d({...l,name:"plan-list",access:"read",defaultFormat:"table",description:"列出分析方案(看板)",args:[s("search","按名称关键字过滤(可选)",!1)],func:r(async e=>q(await a.planList(),e.search),"func")}),Je=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")}),ze=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 m.addNotice(`手工搭建方案:完成后必须用 linkai plan-knowledge --plan ${t} --mode create --file <知识文件> 交付方案知识库(铁律 2)。新建看板优先用 linkai build(自动生成知识库并验收)。`,"plan-knowledge"),{planId:t,name:e.name}},"func")}),Me=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")}),We=d({...l,name:"plan-filter",access:"write",description:"保存方案筛选器为租户模板(全量覆盖),并重置自己的个人副本",args:[s("plan","方案的精确 id"),s("file","筛选器请求体 JSON 文件路径")],func:r(async e=>a.planFilter(e.plan,S(e.file)),"func")}),Ge=d({...l,name:"report-list",access:"read",defaultFormat:"table",description:"列出数据报表(每个报表对应一张卡片)",args:[s("search","按名称关键字过滤(可选)",!1)],func:r(async e=>q(await a.reportList(),e.search),"func")}),He=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")}),Ve=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")}),Ye=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")}),K=f.resolve(f.dirname(te(import.meta.url)),"..","..");try{a.setClientVersion(JSON.parse(w.readFileSync(f.join(K,"package.json"),"utf8")).version)}catch{}const F={knowledge:f.join(K,"knowledge"),templates:f.join(K,"templates")};function $(e,t=e){return w.existsSync(e)?w.readdirSync(e,{withFileTypes:!0}).flatMap(n=>{const o=f.join(e,n.name);return n.isDirectory()?$(o,t):n.name.startsWith(".")?[]:[f.relative(t,o)]}):[]}r($,"walk");function Q(e,t){const n=F[e],o=f.resolve(n,t);if(o!==n&&!o.startsWith(n+f.sep))throw new h("路径必须在 knowledge/ 或 templates/ 之内");if(!w.existsSync(o)||w.statSync(o).isDirectory())throw new h(`文件不存在:${e}/${t}`,"执行 linkai kb 查看可用文件");return w.readFileSync(o,"utf8")}r(Q,"readLocal");function U(e){return new g("KB_UNAVAILABLE",`知识库服务暂不可用:${e}`,"稍后重试;持续出现请联系领谋",1)}r(U,"kbUnavailable");function L(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 g(t[e.code]||"AI_KB_ERROR",e.message,n[e.code],1)}r(L,"aiKbFailure");function I(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(I,"intOption");async function T(e){const t=await a.ctx();if(String(e).toLowerCase()!==String(t.dataSourceId).toLowerCase())throw new g("AI_KB_SOURCE_MISMATCH",`数据字典 source ${e} 与当前数据集 source ${t.dataSourceId} 不一致`,"不要跨 source 使用元数据;先确认服务端已为当前数据源完成探库,或确认 CLI 的数据源配置",1)}r(T,"requireDatasetSource");function G(){return $(f.join(F.knowledge,"report-builder")).map(e=>({file:`knowledge/report-builder/${e}`,title:"",summary:"",library:"bi-builder",scope:"public",source:"bundled-fallback"}))}r(G,"localBiDocs");function ce(e){return{file:`knowledge/${e.canonicalPath}`,title:"",library:e.library,scope:e.scope,source:"bundled-fallback",content:Q("knowledge",e.canonicalPath)}}r(ce,"localBiDoc");function le(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 m.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(le,"logKb");function E(e,t,n,o={}){return m.emit("kb.read",{library:"data-dictionary",op:e,source:t.source??null,...o}),e!=="sources"&&t.source&&x.recordRead({tenantId:a.readSession()?.tenantId,source:t.source,flowId:m.currentFlow()?.id??null,op:e}),n}r(E,"logDict");const H=2*6e4;async function N({enforce:e}){const t=await a.ensureSession(),n=await a.ctx(),o=await x.dictionaryStatus({tenantId:t.tenantId,source:n.dataSourceId,flowId:m.currentFlow()?.id??null,fetchSources:r(()=>a.shortCache("dict:sources",H,async()=>(await a.aiKbSources()).sources||[]),"fetchSources"),fetchDocs:r(()=>a.shortCache(`dict:docs:${n.dataSourceId}`,H,async()=>((await a.aiKbDocs({source:n.dataSourceId,pageIndex:1,pageSize:20})).items||[]).map(c=>({updatedAt:c.updatedAt}))),"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&&m.addNotice(`数据字典:${o.skipped}。`,"dictionary-skipped"),!o.ok){if(e)throw new g("DICTIONARY_REQUIRED",o.reason,x.readHint(n.dataSourceId),1);m.addNotice(`数据字典:${o.reason}。正式写入(ds-create / ds-update / build --execute)前会被拒绝。${x.readHint(n.dataSourceId)}`,"dictionary-stale")}return o}r(N,"checkDictionary");const V=r((e,t,n)=>Array.isArray(e)?e.slice(0,t).map(n):[],"top"),Xe=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=>le(e.file?"doc":"catalog",await(async()=>{const t=String(e.file||"").replace(/^\.?\//,"");if(t.startsWith("templates/"))return{file:t,source:"bundled",content:Q("templates",t.slice(10))};if(t==="knowledge/browser/README.md"||t==="browser/README.md")return{file:"knowledge/browser/README.md",source:"bundled-cli",content:Q("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 g("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 g("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(...G()):u.push(...(y.docs||[]).map(D=>{const B=a.KB_LIBRARIES[p].pathPrefix,j=a.kbRequest(D.path,p).path;return{file:`knowledge/${B}/${j}`,title:D.title||"",summary:D.summary||"",library:p,scope:a.KB_LIBRARIES[p].scope,source:"server"}}))}catch(y){if(p==="bi-builder"&&y instanceof a.KbUnavailableError)u.push(...G());else throw y instanceof a.KbUnavailableError?U(p):y}const k=!o||o==="bi-builder"?$(F.templates).map(p=>({file:`templates/${p}`,title:"报表模版",summary:"",source:"bundled"})):[];return[...u,...k].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(!w.existsSync(f.join(F.knowledge,"report-builder")))throw U(c.library);return ce(c)}throw i instanceof a.KbUnavailableError?U(c.library):i}})()),"func")}),Ze=d({...l,name:"kb-sources",access:"read",defaultFormat:"table",description:"列出当前租户可读的数据字典数据源,并标记当前数据集所用 source;后续命令必须显式传该 ID",args:[],func:r(async()=>{try{const[{sources:e=[]},t]=await Promise.all([a.aiKbSources(),a.ctx()]),n=e.map(c=>({...c,selectedForDatasets:String(c.source).toLowerCase()===String(t.dataSourceId).toLowerCase()})),o=n.some(c=>c.selectedForDatasets);return E("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 L(e)}},"func")}),et=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 T(e.source);const t=await a.aiKbCatalog({source:e.source,keyword:e.search,pageIndex:I(e.page,1,"page",1,2147483647),pageSize:I(e["page-size"],30,"page-size",1,100)});return E("catalog",e,t,{keyword:e.search??null,total:t?.totalCount??null})}catch(t){throw L(t)}},"func")}),tt=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 T(e.source);const t=await a.aiKbObject({source:e.source,db:e.db,object:e.object});return E("object",e,t,{object:`${e.db}.${e.object}`})}catch(t){throw L(t)}},"func")}),at=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 T(e.source);const n=await a.aiKbDocs({source:e.source,category:t,keyword:e.search,pageIndex:I(e.page,1,"page",1,2147483647),pageSize:I(e["page-size"],30,"page-size",1,100)});return E("docs",e,n,{category:t??null,keyword:e.search??null,total:n?.totalCount??null})}catch(n){throw L(n)}},"func")}),nt=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 T(e.source);const t=await a.aiKbDoc({source:e.source,doc:e.doc,limit:I(e["page-size"],2e4,"page-size",1,2e4)});return E("doc",e,t,{doc:e.doc,title:t?.title??null,category:t?.category??null,chars:t?.totalChars??null})}catch(t){throw L(t)}},"func")}),st=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 T(e.source);const t=await a.aiKbSearch({source:e.source,question:e.question,topTables:I(e["top-tables"],5,"top-tables",1,10),topDocs:I(e["top-docs"],3,"top-docs",1,10)});return E("search",e,t,{question:String(e.question||"").slice(0,200),hitTables:V(t?.tables,10,n=>`${n.db}.${n.object}`),hitDocs:V(t?.docs,10,n=>({doc:n.doc,title:n.title??null,category:n.category??null})),emptyReason:t?.emptyReason??null})}catch(t){throw L(t)}},"func")}),ot=v({...l,name:"template",access:"write",description:"复制报表 spec 与示例文件;SQL 示例必须由 AI 按用户需求、租户数据字典和确认口径重写,CLI 不生成业务 SQL",args:[s("output","目标目录(默认 ./linkai-report),需不存在或为空",!1)],func:r(async e=>{const t=f.resolve(String(e.output||"linkai-report"));if(w.existsSync(t)&&w.readdirSync(t).length)throw new h(`目录已存在且不为空:${t}`,"换一个 --output 目录");return w.cpSync(f.join(F.templates,"report-spec"),t,{recursive:!0}),{output:t,files:$(t),next:`修改 ${f.join(t,"report.example.yaml")} 与 sql/ 下的 SQL,然后 linkai build --spec ${f.join(t,"report.example.yaml")}`}},"func")}),rt=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=J.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 N({enforce:!0})},"beforeWrite"),c=await J.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 N({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
+ - ${c.errors.join(`
4
+ - `)}`,c.stage==="authorize"?"把正式名清单给用户确认,得到明确授权后再加 --confirm":"按错误逐条修改 spec 后重新 dry-run");return c},"func")});async function Y(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=P.readKnowledgeFile(e.file);return P.publishPlanKnowledge({planId:e.plan,operation:t,authoring:n.knowledge||n})}r(Y,"publishPlanKnowledgeFromFile");const it=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 Y(e);return m.emit("kb.write",{library:"plan-knowledge",op:e.mode||"create",plan:e.plan,version:t?.version??t?.planKnowledge?.version??null}),t},"func")}),ct=d({...l,name:"plan-knowledge-get",access:"read",description:"读取方案当前知识库;修改原方案前先读。服务器不可用时读取本地待同步版本,不展示存储信息",args:[s("plan","方案的精确 id")],func:r(async e=>{const t=await P.readCurrentPlanKnowledge(e.plan);return m.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")}),lt=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:Y}),dt=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}-${M()}.${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")}),ut=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")}),pt=d({...l,name:"clean",access:"write",description:"按 zz- 前缀清理试验对象,按依赖顺序删除:报表 → 方案 → 卡片 → 数据集",args:[s("prefix","以 zz- 开头的前缀,如 zz-销售看板-")],func:r(async e=>a.cleanPrefix(e.prefix),"func")}),mt=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")}),de=/\/(get-|query|search)[\w-]*|\/[\w-]*-(list|detail|info)(?:[/?]|$)/i,ft=d({...l,name:"api",access:"write",accessOf:r(e=>de.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?S(e.body):t==="POST"?{}:void 0,{raw:!!e.raw})},"func")}),X="create(新建方案)| modify(修改已有方案)| import(导入/更新数据)| query(只读查询分析)",wt=d({...l,name:"flow-start",access:"write",description:"开始一个工作流:接到业务任务时执行一次,之后的命令自动归入它;完成后用 flow-end 结束",args:[s("goal","这个任务要完成什么(用户原话概括)"),s("type",X),s("plan","目标方案 id(修改已有方案时)",!1),s("model",'你(AI)当前使用的具体模型与版本,如 "Claude Opus 5.5"、"gpt-5.6";不确定就写你所知道的名称',!1)],func:r(async e=>{const t=m.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:m.currentModel(),next:"同一会话里的后续命令自动归入此工作流;并行开多个任务时,在命令上加 --flow <id> 指明。完成后 linkai flow-end --status delivered"}},"func")}),bt=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=m.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")}),ht=d({...l,name:"flow-status",access:"read",description:"查看本机工作流:当前工作流、未完成或中断的工作流(含中断原因与下一步);--all 含已结束的",args:[{name:"all",type:"boolean",default:!1,help:"包括已结束和自动关闭的工作流(最近 7 天)"}],func:r(async e=>{const t=m.currentFlow(),n=(e.all?m.listFlows():m.unfinishedFlows()).slice(0,30).map(o=>({...W(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:m.flowIdleMinutes(),flows:n}},"func")}),A={name:"worker",type:"string",required:!1,help:"执行者名称(也可用全局参数 --worker);同一会话保持不变,如 主控、sql-1、card-2"},C=r(e=>String(e.worker||b.currentWorker()||""),"workerOf"),_=s("board","任务板 id(collab-start 返回,如 b-1a2b3c4d)"),yt=d({...l,name:"collab-start",access:"write",description:"多会话协同:创建任务板(协调者执行一次),返回任务板 id 交给各执行者",args:[s("goal","这次协同要完成什么"),s("plan","目标方案 id(修改已有方案时)",!1),s("type",`工作流类型:${X};默认有 --plan 为 modify,否则 create`,!1),A],func:r(async e=>{const t=b.startBoard({goal:e.goal,plan:e.plan,worker:C(e)});return m.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")}),gt=d({...l,name:"collab-task",access:"write",description:"多会话协同:新增任务(--title),或更新任务状态(--id --status todo|doing|done|blocked --result)",args:[_,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),A],func:r(async e=>e.id?b.updateTask({board:e.board,worker:C(e),id:e.id,status:e.status,result:e.result,assignee:e.assignee}):b.addTask({board:e.board,worker:C(e),title:e.title,assignee:e.assignee,objects:e.object}),"func")}),kt=d({...l,name:"collab-claim",access:"write",description:"多会话协同:写共享对象前认领(全部成功或全部不认领;重复认领即续期);被别人认领时返回 COLLAB_CONFLICT",args:[_,s("object","对象,逗号分隔:card:<id> ds:<id> plan:<id> knowledge:<planId> spec:<文件> report:<id> filter:<planId>"),{name:"ttl",type:"int",default:b.DEFAULT_TTL_SECONDS,help:"认领有效秒数(60-7200),到期自动释放"},s("note","在做什么",!1),A],func:r(async e=>b.claim({board:e.board,worker:C(e),objects:e.object,ttl:e.ttl,note:e.note}),"func")}),St=d({...l,name:"collab-release",access:"write",description:"多会话协同:释放认领(不指定对象则释放自己在该任务板上的全部认领)",args:[_,s("object","要释放的对象,逗号分隔",!1),A],func:r(async e=>b.release({board:e.board,worker:C(e),objects:e.object}),"func")}),It=d({...l,name:"collab-note",access:"write",description:"多会话协同:共享成果或发现(已验证的 SQL 文件、建好的对象 id、口径结论、交接、阻塞),避免别人重复做",args:[_,s("text","内容"),s("kind","finding | result | handoff | blocker | decision(默认 finding)",!1),s("ref","相关文件路径或对象 id",!1),A],func:r(async e=>b.note({board:e.board,worker:C(e),text:e.text,kind:e.kind||"finding",ref:e.ref}),"func")}),Ct=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=>b.status({board:e.board,notes:e.notes}),"func")}),xt=d({...l,name:"collab-close",access:"write",description:"多会话协同:关闭任务板并释放其上全部认领,同时结束对应工作流(协调者在交付后执行)",args:[_,s("status","工作流结束状态:delivered | abandoned | failed(默认 delivered)",!1),s("note","结果说明",!1),A],func:r(async e=>{const t=b.closeBoard({board:e.board,worker:C(e)});let n=null;try{n=m.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")}),Lt=d({...se,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,k=null;try{const p=f.resolve(R(t.output,"shots",`${M()}.png`));w.mkdirSync(f.dirname(p),{recursive:!0}),u=await e.screenshot({path:p,fullPage:!1,format:"png"})||p}catch(p){k=String(p?.message||p).slice(0,200)}return{url:await e.getCurrentUrl(),title:i?.title,loggedIn:!0,output:u,screenshotError:k}},"func")});export{ft as apiCommand,mt as apiGetCommand,dt as backupCommand,rt as buildCommand,Be as cardChartCommand,Pe as cardGanttCommand,qe as cardGetCommand,_e as cardListCommand,je as cardMeterCommand,Re as cardSaveCommand,Ke as cardSortCommand,ve as cardTableCommand,pt as cleanCommand,kt as collabClaimCommand,xt as collabCloseCommand,It as collabNoteCommand,St as collabReleaseCommand,yt as collabStartCommand,Ct as collabStatusCommand,gt as collabTaskCommand,lt as deliverableCommand,ke as doctorCommand,Te as dsCreateCommand,Ae as dsFieldsCommand,Le as dsListCommand,Fe as dsMatchCommand,Oe as dsRefsCommand,De as dsSqlCommand,Ee as dsTablesCommand,Ne as dsUpdateCommand,$e as fillWriteCommand,bt as flowEndCommand,wt as flowStartCommand,ht as flowStatusCommand,xe as fmtCommand,et as kbCatalogCommand,Xe as kbCommand,nt as kbDocCommand,at as kbDocsCommand,tt as kbObjectCommand,st as kbSearchCommand,Ze as kbSourcesCommand,Ie as kindsCommand,ye as loginCommand,ge as logoutCommand,Me as planAddCommand,ze as planCreateCommand,We as planFilterCommand,Je as planGetCommand,it as planKnowledgeCommand,ct as planKnowledgeGetCommand,Ue as planListCommand,Qe as queryCommand,ut as removeCommand,He as reportCardCommand,Ge as reportListCommand,Ve as reportPublishCommand,Ye as resetFilterCommand,Lt as shotCommand,Ce as sqlCheckCommand,ot as templateCommand,Se as tenantsCommand};