@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.
- package/THIRD_PARTY_NOTICES.txt +1 -1
- package/clis/_shared/common.js +1 -1
- package/clis/_shared/site-auth.js +1 -1
- package/clis/linkai/build.js +1 -1
- package/clis/linkai/card-rules.js +1 -1
- package/clis/linkai/collab.js +1 -1
- package/clis/linkai/commands.js +4 -4
- package/clis/linkai/core.js +6 -6
- package/clis/linkai/deliverable.js +2 -2
- package/clis/linkai/dictionary.js +1 -1
- package/clis/linkai/fill-card.js +1 -1
- package/clis/linkai/oauth.js +7 -7
- package/clis/linkai/reuse.js +1 -1
- package/clis/linkai/templates.js +1 -1
- package/dist/bundle/core.mjs +496 -496
- package/dist/bundle/daemon.mjs +12 -12
- package/package.json +2 -2
package/clis/linkai/core.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import fs from"node:fs";import path from"node:path";import os from"node:os";import crypto from"node:crypto";import{authorizeTenantSession,loginWithOauth,refreshOauthSession,revokeOauthSession}from"./oauth.js";import{validateFillCardPayload}from"./fill-card.js";import{checkCardPayload,checkFilterPayload}from"./card-rules.js";import*as oplog from"@linkaicli/linkai-cli/oplog";import*as collab from"./collab.js";import TEMPLATES from"./templates.js";const SITE_ID="linkai",Z="00000000-0000-0000-0000-000000000000",DEFAULT_SITE="https://bi.linkmore.com/",DEFAULT_API="https://bi-api.linkmore.com",RETRYABLE=/UND_ERR_CONNECT_TIMEOUT|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|Connect Timeout/,STALE_SOCKET=/EPIPE|ECONNRESET|socket hang up|UND_ERR_SOCKET|other side closed/i,IDEMPOTENT=new Set(["GET","HEAD"]);class NetworkError extends Error{constructor(message,options){super(message,options),this.name="NetworkError"}}async function fetchRetry(url,opts,tries=3){for(let i=1;;i++)try{return await fetch(url,opts)}catch(e){if(opts?.signal?.aborted)throw e;const why=`${e?.cause?.code||""} ${e?.cause?.message||""} ${e?.message||""}`,retryable=RETRYABLE.test(why)||IDEMPOTENT.has(String(opts?.method||"GET").toUpperCase())&&STALE_SOCKET.test(why);if(i>=tries||!retryable)throw new NetworkError(`无法连接领谋 BI 服务器,请检查网络后重试(${why.trim().replace(/\s+/g," ").slice(0,120)})`,{cause:e});await new Promise(r=>setTimeout(r,1500*i))}}function configDir(){const dir=process.env.LINKAI_SITE_CONFIG_DIR||path.join(os.homedir(),".linkai","sites");return fs.mkdirSync(dir,{recursive:!0,mode:448}),migrateLegacyFiles(dir),dir}let _migrated=!1;function migrateLegacyFiles(dir){if(!_migrated){_migrated=!0;for(const[from,to]of[["lmbi.json",`${SITE_ID}.json`],["lmbi.session.json",`${SITE_ID}.session.json`]]){const a=path.join(dir,from),b=path.join(dir,to);try{fs.existsSync(a)&&!fs.existsSync(b)&&fs.renameSync(a,b)}catch{}}if(!process.env.LINKAI_ARTIFACTS_DIR){const a=path.join(os.homedir(),".linkai","artifacts","lmbi"),b=artifactsDir();try{fs.existsSync(a)&&!fs.existsSync(b)&&fs.renameSync(a,b)}catch{}}}}const configFile=()=>path.join(configDir(),`${SITE_ID}.json`);function artifactsDir(...sub){return path.join(process.env.LINKAI_ARTIFACTS_DIR||path.join(os.homedir(),".linkai","artifacts",SITE_ID),...sub)}function readConfig(){const config=readConfigFile();if("username"in config||"password"in config||"tenantConfirmed"in config){const{username,password,tenantConfirmed,...safe}=config;return writeConfigFile(safe),configFrom(safe)}return configFrom(config)}function configFrom(config){const url=new URL(config.url||DEFAULT_SITE);if(!["http:","https:"].includes(url.protocol))throw new Error("LMBI url must use http:// or https://");return{...config,url:url.href,origin:url.origin,apiBase:(config.apiBase||DEFAULT_API).replace(/\/$/,"")}}const sessionFile=()=>path.join(configDir(),`${SITE_ID}.session.json`),LOGIN_HINT="请用户执行 linkai login,在浏览器完成领谋授权";function readSession(){try{const j=JSON.parse(fs.readFileSync(sessionFile(),"utf8"));return j.authMode==="oauth"&&j.token&&j.refreshToken&&j.tenantId?j:null}catch{return null}}const shortCacheFile=()=>path.join(configDir(),`${SITE_ID}.cache.json`);async function shortCache(key,ttlMs,fn){const tenantId=readSession()?.tenantId;if(!tenantId||process.env.LINKAI_NO_CACHE==="1")return fn();let all={};try{all=JSON.parse(fs.readFileSync(shortCacheFile(),"utf8"))||{}}catch{}all.tenantId!==tenantId&&(all={tenantId,entries:{}});const hit=all.entries?.[key];if(hit&&Date.now()-hit.at>=0&&Date.now()-hit.at<ttlMs)return hit.value;const value=await fn();try{try{all=JSON.parse(fs.readFileSync(shortCacheFile(),"utf8"))||{}}catch{all={}}all.tenantId!==tenantId&&(all={tenantId,entries:{}}),all.entries??={},all.entries[key]={at:Date.now(),value};const tmp=`${shortCacheFile()}.${process.pid}.tmp`;fs.writeFileSync(tmp,JSON.stringify(all),{mode:384}),fs.renameSync(tmp,shortCacheFile())}catch{}return value}function clearShortCache(){try{fs.rmSync(shortCacheFile(),{force:!0})}catch{}}function saveSession(session){if(session?.authMode!=="oauth"||!session.token||!session.refreshToken||!session.tenantId||!session.agentId)throw new Error("只允许保存完整的 OAuth 会话");const file=sessionFile(),tmp=`${file}.${process.pid}.tmp`,data={token:session.token,tenantId:session.tenantId,agentId:session.agentId||"",...session.authMode==="oauth"&&session.refreshToken?{authMode:"oauth",refreshToken:session.refreshToken}:{},savedAt:new Date().toISOString()};return fs.writeFileSync(tmp,JSON.stringify(data),{mode:384}),fs.renameSync(tmp,file),_session=data,_ctx=null,_allow=null,data}function clearSession(){try{fs.rmSync(sessionFile())}catch{}_session=null,_ctx=null,_allow=null}function tokenPayload(token){try{return JSON.parse(Buffer.from(String(token).split(".")[1],"base64").toString())}catch{return{}}}function tokenHoursLeft(token){const p=tokenPayload(token);return p.exp?(p.exp*1e3-Date.now())/36e5:-1}function spaceOf(session){const p=tokenPayload(session?.token);let info={};try{info=JSON.parse(p.TenantInfo||"[]")[0]||{}}catch{}return{tenantId:session?.tenantId,tenantName:info.tenantName||"",tenantCode:info.tenantCode||"",user:p.UserName||""}}class LoginRequiredError extends Error{constructor(reason){super(`${reason}。${LOGIN_HINT}`),this.name="LoginRequiredError"}}function readConfigFile(){try{return JSON.parse(fs.readFileSync(configFile(),"utf8"))||{}}catch{return{}}}function writeConfigFile(data){const file=configFile(),tmp=`${file}.${process.pid}.tmp`;fs.writeFileSync(tmp,JSON.stringify(data,null,2),{mode:384}),fs.renameSync(tmp,file)}function forgetCredentials(){const{username,password,tenantId,tenantConfirmed,...rest}=readConfigFile();if(Object.keys(rest).length)writeConfigFile(rest);else try{fs.rmSync(configFile())}catch{}}function oauthAgentId(){forgetCredentials();const c=readConfigFile();if(typeof c.oauthAgentId=="string"&&/^[A-Za-z0-9._:-]{8,128}$/.test(c.oauthAgentId))return c.oauthAgentId;const agentId=crypto.randomUUID().replace(/-/g,"");return writeConfigFile({...c,oauthAgentId:agentId}),agentId}async function oauthLogin({tenant,switchAccount=!1,timeoutSeconds=300,browser}={}){const old=readSession();return loginWithOauth(readConfig(),{agentId:oauthAgentId(),tenant,switchAccount,timeoutSeconds,browser,onAuthorized:async session=>{if(switchAccount&&old){const previousUser=tokenPayload(old.token).UserId,nextUser=tokenPayload(session.token).UserId;if(previousUser&&nextUser&&previousUser===nextUser)throw await revokeOauthSession(readConfig(),session).catch(()=>{}),new Error("授权后仍是原领谋账号;请在授权页切换账号后重试")}if(tenant)try{const list=await tenantList(session),w=String(tenant).trim(),hit=list.filter(t=>t.tenantId===w||t.code===w||t.name===w),fuzzy=hit.length?hit:list.filter(t=>t.name.includes(w));if(fuzzy.length!==1||fuzzy[0].tenantId!==session.tenantId)throw new Error(`授权的租户不是「${w}」,请在领谋授权页选择目标租户后重试`)}catch(error){throw await revokeOauthSession(readConfig(),session).catch(()=>{}),error}forgetCredentials();const saved=saveSession(session);return old?.refreshToken&&await revokeOauthSession(readConfig(),old).catch(()=>{}),{session:saved,tenantName:spaceOf(saved).tenantName||saved.tenantId}}})}async function oauthLogout(){const session=readSession();if(!session?.refreshToken)return!1;try{return await revokeOauthSession(readConfig(),session)}catch{return!1}}let _session=null,_oauthRefresh=null;async function withSessionLock(task){const lock=`${sessionFile()}.refresh.lock`,deadline=Date.now()+3e4;let fd;for(;fd===void 0;)try{fd=fs.openSync(lock,"wx",384),fs.writeFileSync(fd,String(process.pid))}catch(error){if(error.code!=="EEXIST")throw error;try{const owner=Number(fs.readFileSync(lock,"utf8"));if(Number.isInteger(owner)&&owner>0){let alive=!0;try{process.kill(owner,0)}catch(check){check.code==="ESRCH"&&(alive=!1)}if(!alive){fs.rmSync(lock);continue}}}catch{}if(Date.now()>=deadline)throw new Error("等待 OAuth 会话操作超时,请稍后重试");await new Promise(resolve=>setTimeout(resolve,150))}try{return await task()}finally{fs.closeSync(fd),fs.rmSync(lock)}}async function refreshOauthLocked(previous){return withSessionLock(async()=>{const current=readSession();if(!current?.refreshToken)throw new LoginRequiredError("OAuth 会话已在本机退出");return current.refreshToken!==previous.refreshToken&&tokenHoursLeft(current.token)>.05?(_session=current,current):saveSession(await refreshOauthSession(readConfig(),current))})}async function ensureSession({refresh=!1}={}){if(forgetCredentials(),_session&&!refresh&&tokenHoursLeft(_session.token)>.05)return _session;const saved=_session||readSession();if(saved&&!refresh&&tokenHoursLeft(saved.token)>.05)return _session=saved,saved;if(saved?.authMode==="oauth"&&saved.refreshToken)return _oauthRefresh||(_oauthRefresh=(async()=>{try{return await refreshOauthLocked(saved)}catch(error){if(["invalid_grant","invalid_client","oauth_exchange_unknown"].includes(error.code)){const newer=readSession();if(newer&&newer.refreshToken!==saved.refreshToken&&tokenHoursLeft(newer.token)>.05)return _session=newer,newer;throw clearSession(),new LoginRequiredError(error.code==="oauth_exchange_unknown"?"OAuth 续期结果无法确认":"OAuth 授权已失效")}throw error}})().finally(()=>{_oauthRefresh=null})),_oauthRefresh;throw new LoginRequiredError(saved?"登录已过期":"尚未登录领谋 BI")}async function tenantList(session){session=session||await ensureSession();const p=tokenPayload(session.token),j=await rawFetch(readConfig(),session,"GET",`/api/app/account/user-bind-tenant-list/${p.UserId||p.sub||p.userId}`);if(j.code!==200)throw new LoginRequiredError(`读取租户列表失败(${j.message||j.code})`);return(j.data||[]).map(x=>({tenantId:x.tenantId,name:x.spaceName||x.tenantName||"",code:x.tenantCode||""})).filter(t=>!isPlatformTenant(t))}function isPlatformTenant(t){return t.tenantId===Z||t.code==="0000"||t.name==="全域租户"||t.name==="全域"}async function switchTenant(session,wanted){if(session?.authMode!=="oauth")throw new LoginRequiredError("当前没有 OAuth 授权");return withSessionLock(async()=>{let current=readSession();if(!current)throw new LoginRequiredError("当前没有 OAuth 授权");if(tokenPayload(current.token).UserId!==tokenPayload(session.token).UserId)throw new Error("领谋账号已切换,请重新读取当前租户");tokenHoursLeft(current.token)<=.05&&(current=saveSession(await refreshOauthSession(readConfig(),current)));const list=await tenantList(current),w=String(wanted).trim(),hit=list.filter(t=>t.tenantId===w||t.code===w||t.name===w),fuzzy=hit.length?hit:list.filter(t=>t.name.includes(w));if(fuzzy.length!==1)throw new Error(`租户「${w}」${fuzzy.length?"匹配到多个":"不存在"};可选:${list.map(t=>`${t.name}(${t.tenantId})`).join("、")}`);const target=fuzzy[0];if(target.tenantId===current.tenantId)return{session:current,tenant:target,switched:!1};const next=await authorizeTenantSession(readConfig(),current,target.tenantId);return saveSession(next),_tenantCardIds=null,await revokeOauthSession(readConfig(),current).catch(()=>{}),{session:next,tenant:target,switched:!0}})}let _clientVersion="";function setClientVersion(version){_clientVersion=String(version||"")}function logPath(p){const[pathname,query2=""]=String(p).split("?"),keys=query2?[...new URLSearchParams(query2).keys()]:[];return{path:pathname,query:keys.length?keys:void 0}}function identityOf(session){if(!session)return null;const p=tokenPayload(session.token);return{userId:String(p.UserId||p.sub||p.userId||""),tenantId:session.tenantId,tenantCode:spaceOf(session).tenantCode}}function logGuard(check,pass,extra={}){oplog.emit("guard",{check,pass,...extra})}async function rawFetch(config,session,method,p,body,{log=!0,signal}={}){const started=Date.now(),reqText=body!==void 0?JSON.stringify(body):void 0;log&&oplog.setIdentity(identityOf(session));let r;try{r=await fetchRetry(config.apiBase+p,{method,headers:{"X-LinkAI-Run-Id":oplog.currentRunId(),..._clientVersion?{"X-LinkAI-Version":_clientVersion}:{},Authorization:"Bearer "+session.token,Tenantid:session.tenantId,Agentid:session.agentId||"","Content-Type":"application/json"},...reqText!==void 0?{body:reqText}:{},...signal?{signal}:{}})}catch(error){throw log&&oplog.emit("api.call",{method,...logPath(p),ok:!1,network:!0,ms:Date.now()-started,reqBytes:reqText?Buffer.byteLength(reqText):0,message:String(error.message).slice(0,300)}),error}const text=await r.text();let j;try{j=JSON.parse(text),j.__status=r.status}catch{j={code:r.status,message:`HTTP ${r.status} non-JSON: ${text.slice(0,200)}`,__status:r.status}}if(log){const ok=r.status<400&&j.code===200;oplog.emit("api.call",{method,...logPath(p),status:r.status,code:j.code??null,ok,ms:Date.now()-started,reqBytes:reqText?Buffer.byteLength(reqText):0,resBytes:Buffer.byteLength(text),...ok?{}:{message:String(j.message||"").slice(0,300)}})}return j}const OPLOG_API="/api/app/linkai-oplog/events";oplog.setTransport(async(events,signal)=>{if(!readSession())return!1;const expired=j=>j.__status===401||j.code===401||j.__status===406||j.code===406;try{let session=await ensureSession(),j=await rawFetch(readConfig(),session,"POST",OPLOG_API,{events},{log:!1,signal});return expired(j)&&(session=await ensureSession({refresh:!0}),j=await rawFetch(readConfig(),session,"POST",OPLOG_API,{events},{log:!1,signal})),j.__status<400&&j.code===200?(oplog.saveServerConfig(j.data||{}),!0):!1}catch{return!1}});const BLOCKED_API=[/data-?source/i,/sql/i,/table-field/i,/table-info/i,/er-table/i,/field-info-by/i,/data-info-by/i,/index-block-data-argument/i,/copy-index-block-data/i,/\/export\/import/i,/task-sync/i,/data-warehouse/i,/authority-variable/i,/\/test\//i,/package/i,/subscribe/i,/distribute/i];function assertApiAllowed(p){let decoded=String(p);try{for(let k=0;k<3;k++)decoded=decodeURIComponent(decoded)}catch{}const hit=BLOCKED_API.find(re=>re.test(decoded)||re.test(String(p)));if(hit)throw logGuard("api-blocklist",!1,{path:String(p).split("?")[0],rule:String(hit)}),new Error(`Rejected: ${p} touches data sources / SQL / database structure, which this CLI does not allow. Use ds-create / ds-update for datasets; database changes must go through the LinkMore data team.`)}async function api(method,p,body,{raw=!1}={}){const config=readConfig();p=String(p).trim(),p.startsWith("/")||(p="/"+p),p.startsWith("/api/")||(p="/api/app"+p);try{validateFillCardPayload(method,p,body)}catch(error){throw logGuard("fill-card-structure",!1,{path:p.split("?")[0],message:error.message.slice(0,300)}),error}try{let notes;try{notes=checkCardPayload(method,p,body)}catch(error){const id=/update-new/i.test(p)&&body?.id&&body.id!==Z?body.id:null,previous=id&&error.errors?await cardDetail(id).catch(()=>null):null;if(!previous)throw error;notes=checkCardPayload(method,p,body,{previous})}for(const note of notes)oplog.addNotice(`卡片保存:${note}。`,"card-rules");let filterNotes;try{filterNotes=checkFilterPayload(method,p,body)}catch(error){const planId=body?.userPageSettingId&&Number(body.filterSourceType)!==2?body.userPageSettingId:null,previous=planId&&error.errors?(await planDetail(planId).catch(()=>null))?.filters:null;if(!previous)throw error;filterNotes=checkFilterPayload(method,p,body,{previous})}for(const note of filterNotes)oplog.addNotice(`筛选条件:${note}。`,"filter-rules")}catch(error){throw logGuard(/filter/.test(p)?"filter-rules":"card-rules",!1,{path:p.split("?")[0],message:error.message.slice(0,300)}),new KindError("CONFIG_INVALID",error.message)}const guardTarget=tenantGuardTarget(method,p,body);if(guardTarget){collab.assertWritable([collab.normalizeObject(`card:${guardTarget}`)]);try{await assertCardInTenant(guardTarget),logGuard("tenant-ownership",!0,{target:guardTarget,path:p.split("?")[0]})}catch(error){throw error instanceof TenantMismatchError&&logGuard("tenant-ownership",!1,{target:guardTarget,path:p.split("?")[0]}),error}}let session=await ensureSession(),j=await rawFetch(config,session,method,p,body);const expired=x=>x.__status===401||x.code===401||x.__status===406||x.code===406;if(expired(j)){if(!session.refreshToken)throw new LoginRequiredError("OAuth 授权已失效");if(session=await ensureSession({refresh:!0}),j=await rawFetch(config,session,method,p,body),expired(j))throw new LoginRequiredError("自动续登后仍被拒绝")}if(delete j.__status,raw)return j;if(j.code!==200)throw new ApiError(method,p,j);return j.data}class KindError extends Error{constructor(kind,message){super(message),this.name="KindError",this.kind=kind}}const API_ERROR_KINDS=[["SQL_INVALID",/cannot be resolved|analyzing error|syntax|语法错误|Unknown column|doesn't exist/i],["IN_USE",/被.{0,40}引用.{0,20}请先删除/],["DUPLICATE_NAME",/名称不能重复|名称已存在|已存在同名/],["READ_ONLY",/系统报表不能更改|系统方案或大屏不能|系统.{0,6}(只读|不能(更改|修改|新增|删除))/],["NOT_FOUND",/已被删除|不存在|Id有误|未获取到该信息|未找到该数据/],["FORBIDDEN",/私有|暂无权限|没有权限/],["BAD_REQUEST",/ModelState is not valid|Cannot deserialize/]];function cleanApiMessage(text){return String(text||"").split(/\s*STACK TRACE:/)[0].replace(/\s*\w*Exception:\s*/g," ").replace(/\s+/g," ").trim()}class ApiError extends Error{constructor(method,p,j={}){const main=cleanApiMessage(j.message),detail=cleanApiMessage(j.error&&j.error.details||""),text=detail&&!main.includes(detail)&&!detail.includes(main)?`${main} ${detail}`:detail.length>main.length?detail:main;super(`LMBI API error [${method} ${p.split("?")[0]}]: ${text.slice(0,400)}`),this.name="ApiError",this.path=p.split("?")[0],this.status=j.code,this.kind=API_ERROR_KINDS.find(([,re])=>re.test(text))?.[0]||null}}const post=(p,b)=>api("POST",p,b),get=p=>api("GET",p);let _ctx=null;async function ctx(){if(_ctx)return _ctx;const config=readConfig();let dataSourceId=process.env.LM_DATASOURCE||config.dataSourceId||"",dataSources=[];if(!dataSourceId){if(dataSources=await shortCache("dataSources",30*6e4,async()=>{const ds=await get("/index-block/index-block-data-source");return(Array.isArray(ds)?ds:ds.items||[]).map(x=>({id:x.id,name:x.name}))}),!dataSources.length)throw new Error("当前租户没有可用的数据源,请联系领谋开通");dataSourceId=dataSources[0].id}const folders=await shortCache("rootFolders",30*6e4,async()=>{const[planF,cardF,dsF]=await Promise.all([get("/index-block/folders?type=0"),get("/index-block/folders?type=1"),get("/index-block/folders?type=3")]);return{planFolder:planF[0]?.id||Z,cardFolder:cardF[0]?.id||Z,dsFolder:dsF[0]?.id||Z}});return _ctx={dataSourceId,dataSources,...folders},_ctx}function sqlTables(sql){const s=sql.replace(/\/\*[\s\S]*?\*\//g," ").replace(/--[^\n]*/g," ").replace(/'(?:[^'\\]|\\.)*'/g,"''").replace(/`/g,""),ctes=new Set([...s.matchAll(/(?:\bWITH(?:\s+RECURSIVE)?|,)\s*([A-Za-z_]\w*)\s*(?:\([^()]*\))?\s*AS\s*(?:NOT\s+)?(?:MATERIALIZED\s*)?\(/gi)].map(m=>m[1].toLowerCase())),item="([A-Za-z_][\\w.]*)(?:\\s+(?:AS\\s+)?(?!(?:WHERE|GROUP|ORDER|HAVING|LIMIT|UNION|JOIN|LEFT|RIGHT|INNER|CROSS|FULL|ON)\\b)[A-Za-z_]\\w*)?",froms=[];for(const m of s.matchAll(new RegExp(`\\b(?:FROM|JOIN)\\s+${item}((?:\\s*,\\s*${item})*)`,"gi"))){froms.push(m[1]);for(const x of(m[2]||"").matchAll(new RegExp(`,\\s*${item}`,"gi")))froms.push(x[1])}return[...new Set(froms)].filter(t=>!ctes.has(t.toLowerCase()))}let _allow=null;async function allowedTables(){if(_allow)return _allow;const c=await ctx(),names=await shortCache(`tables:${c.dataSourceId}`,10*6e4,async()=>[...new Set((await post(`/index-block/get-data-source-table-field/${c.dataSourceId}`)).map(x=>(x.table_name||"").toLowerCase()))]);return _allow=new Set(names),_allow}function formatSql(sql){const lines=sql.split(`
|
|
2
|
-
`).filter(l=>l.trim()),
|
|
3
|
-
${
|
|
1
|
+
var ze=Object.defineProperty;var o=(t,e)=>ze(t,"name",{value:e,configurable:!0});import h from"node:fs";import x from"node:path";import le from"node:os";import U from"node:crypto";import{authorizeTenantSession as Ge,loginWithOauth as Ke,refreshOauthSession as Te,revokeOauthSession as H}from"./oauth.js";import{validateFillCardPayload as We}from"./fill-card.js";import{checkCardPayload as ke,checkFilterPayload as Ne}from"./card-rules.js";import*as k from"@linkaicli/linkai-cli/oplog";import*as Oe from"./collab.js";import _e from"./templates.js";const j="linkai",b="00000000-0000-0000-0000-000000000000",He="https://bi.linkmore.com/",Je="https://bi-api.linkmore.com",qe=/UND_ERR_CONNECT_TIMEOUT|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|Connect Timeout/,Ve=/EPIPE|ECONNRESET|socket hang up|UND_ERR_SOCKET|other side closed/i,Qe=new Set(["GET","HEAD"]);class Ye extends Error{static{o(this,"NetworkError")}constructor(e,n){super(e,n),this.name="NetworkError"}}async function Ze(t,e,n=3){for(let r=1;;r++)try{return await fetch(t,e)}catch(a){if(e?.signal?.aborted)throw a;const i=`${a?.cause?.code||""} ${a?.cause?.message||""} ${a?.message||""}`,l=qe.test(i)||Qe.has(String(e?.method||"GET").toUpperCase())&&Ve.test(i);if(r>=n||!l)throw new Ye(`无法连接领谋 BI 服务器,请检查网络后重试(${i.trim().replace(/\s+/g," ").slice(0,120)})`,{cause:a});await new Promise(s=>setTimeout(s,1500*r))}}o(Ze,"fetchRetry");function ce(){const t=process.env.LINKAI_SITE_CONFIG_DIR||x.join(le.homedir(),".linkai","sites");return h.mkdirSync(t,{recursive:!0,mode:448}),Xe(t),t}o(ce,"configDir");let Ce=!1;function Xe(t){if(!Ce){Ce=!0;for(const[e,n]of[["lmbi.json",`${j}.json`],["lmbi.session.json",`${j}.session.json`]]){const r=x.join(t,e),a=x.join(t,n);try{h.existsSync(r)&&!h.existsSync(a)&&h.renameSync(r,a)}catch{}}if(!process.env.LINKAI_ARTIFACTS_DIR){const e=x.join(le.homedir(),".linkai","artifacts","lmbi"),n=et();try{h.existsSync(e)&&!h.existsSync(n)&&h.renameSync(e,n)}catch{}}}}o(Xe,"migrateLegacyFiles");const de=o(()=>x.join(ce(),`${j}.json`),"configFile");function et(...t){return x.join(process.env.LINKAI_ARTIFACTS_DIR||x.join(le.homedir(),".linkai","artifacts",j),...t)}o(et,"artifactsDir");function I(){const t=pe();if("username"in t||"password"in t||"tenantConfirmed"in t){const{username:e,password:n,tenantConfirmed:r,...a}=t;return he(a),$e(a)}return $e(t)}o(I,"readConfig");function $e(t){const e=new URL(t.url||He);if(!["http:","https:"].includes(e.protocol))throw new Error("LMBI url must use http:// or https://");return{...t,url:e.href,origin:e.origin,apiBase:(t.apiBase||Je).replace(/\/$/,"")}}o($e,"configFrom");const Y=o(()=>x.join(ce(),`${j}.session.json`),"sessionFile"),tt="请用户执行 linkai login,在浏览器完成领谋授权";function L(){try{const t=JSON.parse(h.readFileSync(Y(),"utf8"));return t.authMode==="oauth"&&t.token&&t.refreshToken&&t.tenantId?t:null}catch{return null}}o(L,"readSession");const J=o(()=>x.join(ce(),`${j}.cache.json`),"shortCacheFile");async function ue(t,e,n){const r=L()?.tenantId;if(!r||process.env.LINKAI_NO_CACHE==="1")return n();let a={};try{a=JSON.parse(h.readFileSync(J(),"utf8"))||{}}catch{}a.tenantId!==r&&(a={tenantId:r,entries:{}});const i=a.entries?.[t];if(i&&Date.now()-i.at>=0&&Date.now()-i.at<e)return i.value;const l=await n();try{try{a=JSON.parse(h.readFileSync(J(),"utf8"))||{}}catch{a={}}a.tenantId!==r&&(a={tenantId:r,entries:{}}),a.entries??={},a.entries[t]={at:Date.now(),value:l};const s=`${J()}.${process.pid}.tmp`;h.writeFileSync(s,JSON.stringify(a),{mode:384}),h.renameSync(s,J())}catch{}return l}o(ue,"shortCache");function Ht(){try{h.rmSync(J(),{force:!0})}catch{}}o(Ht,"clearShortCache");function Z(t){if(t?.authMode!=="oauth"||!t.token||!t.refreshToken||!t.tenantId||!t.agentId)throw new Error("只允许保存完整的 OAuth 会话");const e=Y(),n=`${e}.${process.pid}.tmp`,r={token:t.token,tenantId:t.tenantId,agentId:t.agentId||"",...t.authMode==="oauth"&&t.refreshToken?{authMode:"oauth",refreshToken:t.refreshToken}:{},savedAt:new Date().toISOString()};return h.writeFileSync(n,JSON.stringify(r),{mode:384}),h.renameSync(n,e),O=r,z=null,G=null,r}o(Z,"saveSession");function nt(){try{h.rmSync(Y())}catch{}O=null,z=null,G=null}o(nt,"clearSession");function D(t){try{return JSON.parse(Buffer.from(String(t).split(".")[1],"base64").toString())}catch{return{}}}o(D,"tokenPayload");function q(t){const e=D(t);return e.exp?(e.exp*1e3-Date.now())/36e5:-1}o(q,"tokenHoursLeft");function fe(t){const e=D(t?.token);let n={};try{n=JSON.parse(e.TenantInfo||"[]")[0]||{}}catch{}return{tenantId:t?.tenantId,tenantName:n.tenantName||"",tenantCode:n.tenantCode||"",user:e.UserName||""}}o(fe,"spaceOf");class E extends Error{static{o(this,"LoginRequiredError")}constructor(e){super(`${e}。${tt}`),this.name="LoginRequiredError"}}function pe(){try{return JSON.parse(h.readFileSync(de(),"utf8"))||{}}catch{return{}}}o(pe,"readConfigFile");function he(t){const e=de(),n=`${e}.${process.pid}.tmp`;h.writeFileSync(n,JSON.stringify(t,null,2),{mode:384}),h.renameSync(n,e)}o(he,"writeConfigFile");function me(){const{username:t,password:e,tenantId:n,tenantConfirmed:r,...a}=pe();if(Object.keys(a).length)he(a);else try{h.rmSync(de())}catch{}}o(me,"forgetCredentials");function rt(){me();const t=pe();if(typeof t.oauthAgentId=="string"&&/^[A-Za-z0-9._:-]{8,128}$/.test(t.oauthAgentId))return t.oauthAgentId;const e=U.randomUUID().replace(/-/g,"");return he({...t,oauthAgentId:e}),e}o(rt,"oauthAgentId");async function Jt({tenant:t,switchAccount:e=!1,timeoutSeconds:n=300,browser:r}={}){const a=L();return Ke(I(),{agentId:rt(),tenant:t,switchAccount:e,timeoutSeconds:n,browser:r,onAuthorized:o(async i=>{if(e&&a){const s=D(a.token).UserId,u=D(i.token).UserId;if(s&&u&&s===u)throw await H(I(),i).catch(()=>{}),new Error("授权后仍是原领谋账号;请在授权页切换账号后重试")}if(t)try{const s=await Fe(i),u=String(t).trim(),c=s.filter(f=>f.tenantId===u||f.code===u||f.name===u),d=c.length?c:s.filter(f=>f.name.includes(u));if(d.length!==1||d[0].tenantId!==i.tenantId)throw new Error(`授权的租户不是「${u}」,请在领谋授权页选择目标租户后重试`)}catch(s){throw await H(I(),i).catch(()=>{}),s}me();const l=Z(i);return a?.refreshToken&&await H(I(),a).catch(()=>{}),{session:l,tenantName:fe(l).tenantName||l.tenantId}},"onAuthorized")})}o(Jt,"oauthLogin");async function qt(){const t=L();if(!t?.refreshToken)return!1;try{return await H(I(),t)}catch{return!1}}o(qt,"oauthLogout");let O=null,X=null;async function Ae(t){const e=`${Y()}.refresh.lock`,n=Date.now()+3e4;let r;for(;r===void 0;)try{r=h.openSync(e,"wx",384),h.writeFileSync(r,String(process.pid))}catch(a){if(a.code!=="EEXIST")throw a;try{const i=Number(h.readFileSync(e,"utf8"));if(Number.isInteger(i)&&i>0){let l=!0;try{process.kill(i,0)}catch(s){s.code==="ESRCH"&&(l=!1)}if(!l){h.rmSync(e);continue}}}catch{}if(Date.now()>=n)throw new Error("等待 OAuth 会话操作超时,请稍后重试");await new Promise(i=>setTimeout(i,150))}try{return await t()}finally{h.closeSync(r),h.rmSync(e)}}o(Ae,"withSessionLock");async function at(t){return Ae(async()=>{const e=L();if(!e?.refreshToken)throw new E("OAuth 会话已在本机退出");return e.refreshToken!==t.refreshToken&&q(e.token)>.05?(O=e,e):Z(await Te(I(),e))})}o(at,"refreshOauthLocked");async function M({refresh:t=!1}={}){if(me(),O&&!t&&q(O.token)>.05)return O;const e=O||L();if(e&&!t&&q(e.token)>.05)return O=e,e;if(e?.authMode==="oauth"&&e.refreshToken)return X||(X=(async()=>{try{return await at(e)}catch(n){if(["invalid_grant","invalid_client","oauth_exchange_unknown"].includes(n.code)){const r=L();if(r&&r.refreshToken!==e.refreshToken&&q(r.token)>.05)return O=r,r;throw nt(),new E(n.code==="oauth_exchange_unknown"?"OAuth 续期结果无法确认":"OAuth 授权已失效")}throw n}})().finally(()=>{X=null})),X;throw new E(e?"登录已过期":"尚未登录领谋 BI")}o(M,"ensureSession");async function Fe(t){t=t||await M();const e=D(t.token),n=await V(I(),t,"GET",`/api/app/account/user-bind-tenant-list/${e.UserId||e.sub||e.userId}`);if(n.code!==200)throw new E(`读取租户列表失败(${n.message||n.code})`);return(n.data||[]).map(r=>({tenantId:r.tenantId,name:r.spaceName||r.tenantName||"",code:r.tenantCode||""})).filter(r=>!ot(r))}o(Fe,"tenantList");function ot(t){return t.tenantId===b||t.code==="0000"||t.name==="全域租户"||t.name==="全域"}o(ot,"isPlatformTenant");async function Vt(t,e){if(t?.authMode!=="oauth")throw new E("当前没有 OAuth 授权");return Ae(async()=>{let n=L();if(!n)throw new E("当前没有 OAuth 授权");if(D(n.token).UserId!==D(t.token).UserId)throw new Error("领谋账号已切换,请重新读取当前租户");q(n.token)<=.05&&(n=Z(await Te(I(),n)));const r=await Fe(n),a=String(e).trim(),i=r.filter(c=>c.tenantId===a||c.code===a||c.name===a),l=i.length?i:r.filter(c=>c.name.includes(a));if(l.length!==1)throw new Error(`租户「${a}」${l.length?"匹配到多个":"不存在"};可选:${r.map(c=>`${c.name}(${c.tenantId})`).join("、")}`);const s=l[0];if(s.tenantId===n.tenantId)return{session:n,tenant:s,switched:!1};const u=await Ge(I(),n,s.tenantId);return Z(u),Q=null,await H(I(),n).catch(()=>{}),{session:u,tenant:s,switched:!0}})}o(Vt,"switchTenant");let we="";function Qt(t){we=String(t||"")}o(Qt,"setClientVersion");function Le(t){const[e,n=""]=String(t).split("?"),r=n?[...new URLSearchParams(n).keys()]:[];return{path:e,query:r.length?r:void 0}}o(Le,"logPath");function it(t){if(!t)return null;const e=D(t.token);return{userId:String(e.UserId||e.sub||e.userId||""),tenantId:t.tenantId,tenantCode:fe(t).tenantCode}}o(it,"identityOf");function B(t,e,n={}){k.emit("guard",{check:t,pass:e,...n})}o(B,"logGuard");async function V(t,e,n,r,a,{log:i=!0,signal:l}={}){const s=Date.now(),u=a!==void 0?JSON.stringify(a):void 0;i&&k.setIdentity(it(e));let c;try{c=await Ze(t.apiBase+r,{method:n,headers:{"X-LinkAI-Run-Id":k.currentRunId(),...we?{"X-LinkAI-Version":we}:{},Authorization:"Bearer "+e.token,Tenantid:e.tenantId,Agentid:e.agentId||"","Content-Type":"application/json"},...u!==void 0?{body:u}:{},...l?{signal:l}:{}})}catch(m){throw i&&k.emit("api.call",{method:n,...Le(r),ok:!1,network:!0,ms:Date.now()-s,reqBytes:u?Buffer.byteLength(u):0,message:String(m.message).slice(0,300)}),m}const d=await c.text();let f;try{f=JSON.parse(d),f.__status=c.status}catch{f={code:c.status,message:`HTTP ${c.status} non-JSON: ${d.slice(0,200)}`,__status:c.status}}if(i){const m=c.status<400&&f.code===200;k.emit("api.call",{method:n,...Le(r),status:c.status,code:f.code??null,ok:m,ms:Date.now()-s,reqBytes:u?Buffer.byteLength(u):0,resBytes:Buffer.byteLength(d),...m?{}:{message:String(f.message||"").slice(0,300)}})}return f}o(V,"rawFetch");const De="/api/app/linkai-oplog/events";k.setTransport(async(t,e)=>{if(!L())return!1;const n=o(r=>r.__status===401||r.code===401||r.__status===406||r.code===406,"expired");try{let r=await M(),a=await V(I(),r,"POST",De,{events:t},{log:!1,signal:e});return n(a)&&(r=await M({refresh:!0}),a=await V(I(),r,"POST",De,{events:t},{log:!1,signal:e})),a.__status<400&&a.code===200?(k.saveServerConfig(a.data||{}),!0):!1}catch{return!1}});const st=[/data-?source/i,/sql/i,/table-field/i,/table-info/i,/er-table/i,/field-info-by/i,/data-info-by/i,/index-block-data-argument/i,/copy-index-block-data/i,/\/export\/import/i,/task-sync/i,/data-warehouse/i,/authority-variable/i,/\/test\//i,/package/i,/subscribe/i,/distribute/i];function Yt(t){let e=String(t);try{for(let r=0;r<3;r++)e=decodeURIComponent(e)}catch{}const n=st.find(r=>r.test(e)||r.test(String(t)));if(n)throw B("api-blocklist",!1,{path:String(t).split("?")[0],rule:String(n)}),new Error(`Rejected: ${t} touches data sources / SQL / database structure, which this CLI does not allow. Use ds-create / ds-update for datasets; database changes must go through the LinkMore data team.`)}o(Yt,"assertApiAllowed");async function _(t,e,n,{raw:r=!1}={}){const a=I();e=String(e).trim(),e.startsWith("/")||(e="/"+e),e.startsWith("/api/")||(e="/api/app"+e);try{We(t,e,n)}catch(c){throw B("fill-card-structure",!1,{path:e.split("?")[0],message:c.message.slice(0,300)}),c}try{let c;try{c=ke(t,e,n)}catch(f){const m=/update-new/i.test(e)&&n?.id&&n.id!==b?n.id:null,g=m&&f.errors?await Se(m).catch(()=>null):null;if(!g)throw f;c=ke(t,e,n,{previous:g})}for(const f of c)k.addNotice(`卡片保存:${f}。`,"card-rules");let d;try{d=Ne(t,e,n)}catch(f){const m=n?.userPageSettingId&&Number(n.filterSourceType)!==2?n.userPageSettingId:null,g=m&&f.errors?(await Ct(m).catch(()=>null))?.filters:null;if(!g)throw f;d=Ne(t,e,n,{previous:g})}for(const f of d)k.addNotice(`筛选条件:${f}。`,"filter-rules")}catch(c){throw B(/filter/.test(e)?"filter-rules":"card-rules",!1,{path:e.split("?")[0],message:c.message.slice(0,300)}),new P("CONFIG_INVALID",c.message)}const i=kt(t,e,n);if(i){Oe.assertWritable([Oe.normalizeObject(`card:${i}`)]);try{await Nt(i),B("tenant-ownership",!0,{target:i,path:e.split("?")[0]})}catch(c){throw c instanceof Ue&&B("tenant-ownership",!1,{target:i,path:e.split("?")[0]}),c}}let l=await M(),s=await V(a,l,t,e,n);const u=o(c=>c.__status===401||c.code===401||c.__status===406||c.code===406,"expired");if(u(s)){if(!l.refreshToken)throw new E("OAuth 授权已失效");if(l=await M({refresh:!0}),s=await V(a,l,t,e,n),u(s))throw new E("自动续登后仍被拒绝")}if(delete s.__status,r)return s;if(s.code!==200)throw new ve(t,e,s);return s.data}o(_,"api");class P extends Error{static{o(this,"KindError")}constructor(e,n){super(n),this.name="KindError",this.kind=e}}const lt=[["SQL_INVALID",/cannot be resolved|analyzing error|syntax|语法错误|Unknown column|doesn't exist/i],["IN_USE",/被.{0,40}引用.{0,20}请先删除/],["DUPLICATE_NAME",/名称不能重复|名称已存在|已存在同名/],["READ_ONLY",/系统报表不能更改|系统方案或大屏不能|系统.{0,6}(只读|不能(更改|修改|新增|删除))/],["NOT_FOUND",/已被删除|不存在|Id有误|未获取到该信息|未找到该数据/],["FORBIDDEN",/私有|暂无权限|没有权限/],["BAD_REQUEST",/ModelState is not valid|Cannot deserialize/]];function Pe(t){return String(t||"").split(/\s*STACK TRACE:/)[0].replace(/\s*\w*Exception:\s*/g," ").replace(/\s+/g," ").trim()}o(Pe,"cleanApiMessage");class ve extends Error{static{o(this,"ApiError")}constructor(e,n,r={}){const a=Pe(r.message),i=Pe(r.error&&r.error.details||""),l=i&&!a.includes(i)&&!i.includes(a)?`${a} ${i}`:i.length>a.length?i:a;super(`LMBI API error [${e} ${n.split("?")[0]}]: ${l.slice(0,400)}`),this.name="ApiError",this.path=n.split("?")[0],this.status=r.code,this.kind=lt.find(([,s])=>s.test(l))?.[0]||null}}const S=o((t,e)=>_("POST",t,e),"post"),v=o(t=>_("GET",t),"get");let z=null;async function ee(){if(z)return z;const t=I();let e=process.env.LM_DATASOURCE||t.dataSourceId||"",n=[];if(!e){if(n=await ue("dataSources",30*6e4,async()=>{const a=await v("/index-block/index-block-data-source");return(Array.isArray(a)?a:a.items||[]).map(i=>({id:i.id,name:i.name}))}),!n.length)throw new Error("当前租户没有可用的数据源,请联系领谋开通");e=n[0].id}const r=await ue("rootFolders",30*6e4,async()=>{const[a,i,l]=await Promise.all([v("/index-block/folders?type=0"),v("/index-block/folders?type=1"),v("/index-block/folders?type=3")]);return{planFolder:a[0]?.id||b,cardFolder:i[0]?.id||b,dsFolder:l[0]?.id||b}});return z={dataSourceId:e,dataSources:n,...r},z}o(ee,"ctx");function ct(t){const e=t.replace(/\/\*[\s\S]*?\*\//g," ").replace(/--[^\n]*/g," ").replace(/'(?:[^'\\]|\\.)*'/g,"''").replace(/`/g,""),n=new Set([...e.matchAll(/(?:\bWITH(?:\s+RECURSIVE)?|,)\s*([A-Za-z_]\w*)\s*(?:\([^()]*\))?\s*AS\s*(?:NOT\s+)?(?:MATERIALIZED\s*)?\(/gi)].map(l=>l[1].toLowerCase())),a="([A-Za-z_][\\w.]*)(?:\\s+(?:AS\\s+)?(?!(?:WHERE|GROUP|ORDER|HAVING|LIMIT|UNION|JOIN|LEFT|RIGHT|INNER|CROSS|FULL|ON)\\b)[A-Za-z_]\\w*)?",i=[];for(const l of e.matchAll(new RegExp(`\\b(?:FROM|JOIN)\\s+${a}((?:\\s*,\\s*${a})*)`,"gi"))){i.push(l[1]);for(const s of(l[2]||"").matchAll(new RegExp(`,\\s*${a}`,"gi")))i.push(s[1])}return[...new Set(i)].filter(l=>!n.has(l.toLowerCase()))}o(ct,"sqlTables");let G=null;async function dt(){if(G)return G;const t=await ee(),e=await ue(`tables:${t.dataSourceId}`,10*6e4,async()=>[...new Set((await S(`/index-block/get-data-source-table-field/${t.dataSourceId}`)).map(n=>(n.table_name||"").toLowerCase()))]);return G=new Set(e),G}o(dt,"allowedTables");function ut(t){const e=t.split(`
|
|
2
|
+
`).filter(l=>l.trim()),n=t.length/Math.max(e.length,1);if(e.length>3&&n<100)return t;const r=[];let a=t.replace(/'(?:[^'\\]|\\.)*'/g,l=>`${r.push(l)-1}`);a=" "+a.replace(/\s+/g," ")+" ";const i=["SELECT","FROM","LEFT JOIN","RIGHT JOIN","INNER JOIN","JOIN","WHERE","GROUP BY","HAVING","ORDER BY","LIMIT","UNION ALL","UNION"];for(const l of i){const s=l.replace(" ","");a=a.replace(new RegExp(`\\s${l.replace(" ","\\s+")}\\s`,"gi"),`
|
|
3
|
+
${s} `)}return a=a.replace(/\u0002/g," "),a=a.replace(/\nSELECT ([^\n]*)/g,(l,s)=>{let u="",c=0;for(const d of s)d==="("&&c++,d===")"&&c--,u+=d,d===","&&c===0&&(u+=`
|
|
4
4
|
`);return`
|
|
5
5
|
SELECT
|
|
6
|
-
`+
|
|
7
|
-
`}function stripTrailingSemicolon(sql){const masked=sql.replace(/'(?:[^'\\]|\\.)*'/g,m=>" ".repeat(m.length)).replace(/\/\*[\s\S]*?\*\//g,m=>" ".repeat(m.length)).replace(/--[^\n]*/g,m=>" ".repeat(m.length)),idx=[...masked.matchAll(/;/g)].map(m=>m.index);if(!idx.length)return sql;const last=idx[idx.length-1];if(masked.slice(last+1).trim()||idx.length>1)throw new Error('Dataset SQL must be a single SELECT statement (found ";" inside)');return sql.slice(0,last)+sql.slice(last+1)}const SYSTEM_SCHEMAS=/^(information_schema|mysql|performance_schema|sys)\./i;function assertReadOnlySql(sql){const masked=String(sql).replace(/'(?:[^'\\]|\\.)*'/g,"''").replace(/\/\*[\s\S]*?\*\//g," ").replace(/--[^\n]*/g," ").trim();if(!/^(SELECT|WITH)\b/i.test(masked))throw new KindError("SQL_READONLY","Rejected: dataset SQL must be a single read-only SELECT (or WITH … SELECT) query");if(/\bINTO\s+(OUTFILE|DUMPFILE)\b/i.test(masked))throw new KindError("SQL_READONLY","Rejected: SELECT … INTO OUTFILE/DUMPFILE is not allowed");stripTrailingSemicolon(sql)}function precheckSql(sql){try{assertReadOnlySql(sql)}catch(error){throw logGuard("sql-readonly",!1,{sqlSha256:oplog.sha256(String(sql)),chars:String(sql).length}),error}}async function checkSql(sql){precheckSql(sql),sql=stripTrailingSemicolon(sql);const used=sqlTables(sql),masked=sql.replace(/'(?:[^'\\]|\\.)*'/g,"''").replace(/`/g,""),system=[...new Set([...used.filter(t=>SYSTEM_SCHEMAS.test(t)),...[...masked.matchAll(/\b(information_schema|mysql|performance_schema|sys)\s*\.\s*\w+/gi)].map(m=>m[0])])];if(system.length)throw new KindError("SQL_TABLE_NOT_ALLOWED",`Rejected: system schema access is not allowed (${system.join(", ")})`);if(used.length){const allow=await allowedTables(),bad=used.filter(t=>!allow.has(t.replace(/^[\w]+\./,"").toLowerCase()));if(bad.length)throw new KindError("SQL_TABLE_NOT_ALLOWED",`Rejected: not in the space's table list (${bad.join(", ")}). Only tenant views listed by \`linkai ds-tables\` can be used; to use another view, ask the LinkMore data team to expose it (this CLI cannot operate the database).`)}const c=await ctx();return post("/index-block/get-field-info-by-data-set-sql",{dataSourceId:c.dataSourceId,sql,mode:2})}const CALC_GROUP=99,CALC_SUM=2,fieldColumns=fields=>[...fields].sort((a,b)=>Number(a.sort_num)-Number(b.sort_num)).map((f,i)=>{const num=["int","decimal"].includes(f.field_type);return{id:crypto.randomUUID(),tableFieldName:f.field_name,aliasCnName:f.field_name,fieldComment:"",argumentType:num?0:2,calculationMode:num?CALC_SUM:CALC_GROUP,tableFieldType:num?"decimal":f.field_type,tableFieldTypeName:f.type_name,sortNum:i,sqlType:0,isCustomize:!1}});async function dsSave({id,name,sql,remark,noFormat=!1,current=null}){const c=await ctx(),keep=id&&id!==Z?current||await dsDetail(id):null;!noFormat&&process.env.LM_NO_FMT!=="1"&&(sql=formatSql(sql)),sql=stripTrailingSemicolon(sql);const fields=await checkSql(sql);if(!fields?.length)throw new KindError("SQL_INVALID","运行 SQL 没有返回任何字段,无法生成映射字段");const ts=Date.now(),tables=[{id:crypto.randomUUID(),tableName:`sql_name_${ts}`,tableComment:`sql_comment_${ts}`,sortNum:0,fieldColumns:fieldColumns(fields)}];try{await post("/index-block/get-data-set",{tables,joinOns:[],mode:2,dataSourceId:c.dataSourceId,isNewDataSet:!0,filters:[],sortFields:[],sql})}catch(error){throw error instanceof ApiError?new KindError("SQL_INVALID",`数据集取数验证失败,未保存:${error.message}`):error}return post("/index-block-data-argument/add-or-update",{indexBlockDataId:id||Z,isNewDataSet:!0,mode:2,dataSourceId:c.dataSourceId,sql,name,parentId:keep?.parentId||c.dsFolder,tag:keep?.tag??"",remark:remark??keep?.remark??"",isPrivate:keep?.isPrivate??!1,joinOns:[],tables})}async function dsList({withMeta=!1}={}){const rows=[];for(let pageIndex=1;pageIndex<=200;pageIndex++){const d=await post("/index-block/get-index-block-data-folder-bussiness-info",{page:{pageIndex,pageSize:500}}),items=d.items||[];rows.push(...items.map(x=>({name:x.name,dsId:x.objId,folderId:x.id,...withMeta?{editTime:x.editTime||null,addUser:x.addUser||null}:{}})));const total=Number(d.totalCount??d.total??0);if(items.length<500||total&&rows.length>=total)break}return rows}const dsDetail=dsId=>get(`/index-block-data-argument/data-set-detail/${dsId}`),dsFields=dsId=>get(`/index-block-data-argument/data-set-fields/${dsId}`);async function dsRefs(dsId){return(await get(`/index-block/data-set-quoted-infos/${dsId}`)||[]).map(x=>({name:x.name,cardId:x.indexBlockId}))}function kinds(){return Object.entries(TEMPLATES).sort(([a],[b])=>a.localeCompare(b)).map(([kind,v])=>({kind,name:v._name||"",status:v._disabled?"disabled":v._minimal?"minimal":"full",tip:v._tip||""}))}const ROLE={dim:{prefix:"waxis",axis:"",argumentType:2},meas:{prefix:"yaxis",axis:"y1",argumentType:1},series:{prefix:"waxis_type",axis:"",argumentType:3}};function baseDto({field,col,axis="",argumentType=2,tableFieldType="string",title,orderByType=0,i=0}){return{id:crypto.randomUUID(),format:"default",axis,field,title:title??col,sortNum:i,calculationMode:0,seniorMode:0,periods:0,periodType:0,sqlText:"",comment:col,argumentType,orderByType,tableFieldId:Z,tableFieldGroupId:Z,periodsWithTableFieldId:Z,decimal:0,thousandSeparator:!0,unit:"",tableFieldType,isCustomize:!1,isAggregation:!1,isUseSnow:!1,parent:null,nullZero:!1,isVirtual:!1,isFill:!1,uniqueId:crypto.randomUUID(),isCustomizeFill:!1,isInnerFill:!1,fieldDesc:""}}const orderField=d=>({fieldExpression:"",aliasName:d.title,sortNum:d.sortNum,argumentType:d.argumentType,orderByType:d.orderByType,sqlText:"",tableFieldId:Z,tableFieldName:null,tableFieldComment:d.comment,tableFieldType:d.tableFieldType,isCustomize:!1,fieldExpressionDesc:"",isAggregation:!1,isVirtual:!1,isFill:!1,uniqueId:d.uniqueId,isCustomizeFill:!1,isInnerFill:!1,isNeedTransferJson:!1,jsonFatherId:Z,children:null});async function saveCard({dsId,name,remark,chartParentType,chartType,dtos,chartSetting,limitCount=500,id}){const c=await ctx(),keep=id&&id!==Z?await cardDetail(id):null,payload={indexBlockDataId:dsId,isMulData:!1,indexBlockShowOrderFields:dtos.map(orderField),filters:[],isFreeFill:!1,id:id||Z,indexBlockType:0,name,tag:keep?.tag??"",remark:remark??keep?.remark??"",parentId:keep?.parentId||c.cardFolder,isPrivate:keep?.isPrivate??!1,chartParentType,chartType,limitCount,indexBlockDataSettingDtos:dtos,chartSetting:JSON.stringify(chartSetting)};let cid=id;return(!cid||cid===Z)&&(cid=await post("/index-block/new",payload)),payload.id=cid,await post("/index-block/update-new",payload),{cardId:cid,name,chartType:`${chartParentType}/${chartType}`}}async function datasetMeta(dsId){const fields=await dsFields(dsId);return{fields,meta:Object.fromEntries(fields.map(f=>[f.tableFieldComment,f]))}}async function cardChart({dsId,name,kind,dim,meas,series,id}){const tpl=TEMPLATES[kind];if(!tpl)throw new Error(`Chart kind "${kind}" is not in the v2 chart registry; run: linkai kinds`);if(tpl._disabled)throw new Error(`Chart kind "${kind}" (${tpl._name}) is disabled: ${tpl._disabled}`);const{fields,meta}=await datasetMeta(dsId),dto=(role,n,col,i)=>{const m=meta[col];if(!m)throw new Error(`Dataset has no column "${col}". Available: ${fields.map(f=>f.tableFieldComment).join(" / ")}`);const r=ROLE[role];return baseDto({field:r.prefix+n,col,axis:r.axis,argumentType:r.argumentType,tableFieldType:m.tableFieldType,i})},dims=String(dim||"").split(",").filter(Boolean),measures=String(meas||"").split(",").filter(Boolean);if(!measures.length)throw new Error("At least one measure column is required");const dtos=dims.map((d,i)=>dto("dim",i+1,d,i));return measures.forEach((m,i)=>dtos.push(dto("meas",i+1,m,dtos.length))),series&&dtos.push(dto("series",1,series,dtos.length)),saveCard({dsId,name,chartParentType:tpl.chartParentType,chartType:tpl.chartType,dtos,chartSetting:tpl.chartSetting,limitCount:0,id})}async function cardMeter({dsId,name,col,subtitle,color="#E8611C",id}){const dtos=[baseDto({field:"yaxis1",col,axis:"y1",argumentType:1,tableFieldType:"decimal",title:subtitle||col})];return saveCard({dsId,name,chartParentType:"meter",chartType:"list",dtos,chartSetting:{id:"list",parentId:"meter",config:{group:{type:"value",groups:[]},content:{type:"main",position:"left",bgColor:"lucency",icon:{show:!1,img:"",postion:"left"},labelText:{fontSize:12,fill:"rgb(153,153,153)"},valueText:{fontSize:14,fontWeight:800,fill:"rgba(0,0,0,0.85)"},labelTextMain:{fontSize:12,fill:"rgb(153,153,153)",fontStyle:"italic"},valueTextMain:{fontSize:30,fontWeight:800,fill:color},textPadding:0}}},id})}async function cardTable({dsId,spec,id}){const{meta}=await datasetMeta(dsId),fmap={},dtos=[];let n=0;const reg=col=>{if(fmap[col])return fmap[col];const m=meta[col];if(!m)throw new Error(`Dataset has no column "${col}". Available: ${Object.keys(meta).join(" / ")}`);const f=`waxis${++n}`;return dtos.push(baseDto({field:f,col,argumentType:m.argumentType,tableFieldType:m.tableFieldType,i:n-1})),fmap[col]=f,f},u=()=>crypto.randomUUID(),st=o=>({fontSize:12,fill:"#1F2329",...o}),STYLES={b:st({fontSize:15,fontWeight:600,lineHeight:22}),h:st({fontSize:13,fontWeight:600,lineHeight:20}),grey:st({fill:"#8F959E",lineHeight:18}),"":st({lineHeight:18})},emptyItem=()=>({id:u(),type:"labelValue",field:"",layout:"left",arrangement:"horizontal",fieldStyle:STYLES[""],valueStyle:STYLES[""],showLabel:!1,iconPosition:"left",labelValueGap:4,showColon:!1}),plainColumn=(f,header,hidden)=>({id:"column_"+u(),field:[f],fixed:!1,enableCustomRender:!1,areas:[{id:u(),width:null,rows:[{id:u(),itemGap:10,items:[emptyItem()]}]}],name:header,contentAlign:"left",spanRows:!1,hidden,isPivot:!1,childPivotFields:[],topValueField:!1}),columns=[];for(const c of spec.columns||[]){if(c.plain){columns.push(plainColumn(reg(c.plain),c.header,!1));continue}const usedFields=[],areas=[],mkItem=tok=>{const[col,style=""]=tok.split(":"),f=reg(col);usedFields.push(f);const isImg=style==="image",sty=STYLES[isImg?"":style]||STYLES[""],it={id:u(),type:isImg?"image":"labelValue",field:f,layout:"left",arrangement:"horizontal",fieldStyle:sty,valueStyle:sty,showLabel:!1,iconPosition:"left",labelValueGap:4,showColon:!1};return isImg&&(it.imgWidth=c.imgWidth||112),it};c.image&&areas.push({id:u(),width:(c.imgWidth||112)+16,rows:[{id:u(),itemGap:10,items:[mkItem(`${c.image}:image`)]}]}),areas.push({id:u(),width:null,rows:(c.rows||[]).map(r=>({id:u(),itemGap:10,items:(Array.isArray(r)?r:[r]).map(mkItem)}))}),columns.push({id:"column_"+u(),field:usedFields,fixed:!1,enableCustomRender:!0,areas,name:c.header,contentAlign:"left",spanRows:!1,hidden:!1,isPivot:!1,childPivotFields:[],topValueField:!1})}for(const col of spec.hidden||[])columns.push(plainColumn(reg(col),col,!0));const conds=kind=>(spec[kind]||[]).flatMap(r=>{const dep=fmap[r.dep]||reg(r.dep),tgt=fmap[r.col]||reg(r.col),list=[];for(const[val,color]of Object.entries(r.eq||{}))list.push({field:tgt,color,depField:dep,operator:"==",value:isNaN(+val)?val:+val});for(const[val,color]of Object.entries(r.gte||{}))list.push({field:tgt,color,depField:dep,operator:">=",value:+val});for(const[val,color]of Object.entries(r.ne||{}))list.push({field:tgt,color,depField:dep,operator:"!=",value:val});return list}),rowMark=spec.rowGrey?[{field:"",color:"#F7F8FA",depField:fmap[spec.rowGrey.dep]||reg(spec.rowGrey.dep),operator:">=",value:spec.rowGrey.gte}]:[];spec.order&®(spec.order);const chart={id:"agtable",parentId:"table",config:{sheetType:"table",showPagination:!0,pagination:{pageSize:spec.pageSize||20},fitTableWidth:spec.fitTableWidth!==!1,showHeader:!0,headerHeight:36,rowHeight:spec.rowHeight||150,imgHeight:spec.imgHeight||120,showLineNumber:!1,lineNumberTextAlign:"left",actionColTextAlign:"left",cellTextAlign:"left",cellBorder:{visible:!0,color:"#EDEFF2",width:1},total:{show:!1,labelColumn:"",labelText:"总计",rowtotalType:"all"},agColumnWidths:(spec.columns||[]).map(c=>({key:columns.find(x=>x.name===c.header).field[0],width:c.width||200})),conditionStyle:{value:{conditions:conds("pills")},arrow:{conditions:[]},fontColor:{conditions:conds("fontColor")},rowMark:{conditions:rowMark},cellMark:{conditions:[]}},bgMark:{visible:!1,fields:[]},mobileViewFields:[],report:{visible:!1,fillField:{fields:[],fillFieldProperties:[]}},syncReport:{visible:!1,syncConfig:{webHookUrl:"",webHookId:"",relations:[],fields:[]}},wd:{rows:[],values:[],columns}}};return saveCard({dsId,name:spec.name,remark:spec.remark,chartParentType:"table",chartType:"agtable",dtos,chartSetting:chart,limitCount:spec.limitCount||2e3,id})}async function cardGantt({dsId,spec,id}){const{meta}=await datasetMeta(dsId),fmap={},dtos=[];let n=0;const reg=col=>{if(!col)return"";if(fmap[col])return fmap[col];const m=meta[col];if(!m)throw new Error(`Dataset has no column "${col}". Available: ${Object.keys(meta).join(" / ")}`);const f=`waxis${++n}`;return dtos.push(baseDto({field:f,col,argumentType:m.argumentType,tableFieldType:m.tableFieldType,i:n-1})),fmap[col]=f,f},chart={id:"gantt",parentId:"table",config:{ganttRenderConfig:{showIcon:!0,style:{image:reg(spec.image),main:reg(spec.main),assist:reg(spec.assist),status:reg(spec.status),tags:(spec.tags||[]).map(reg),desc:(spec.desc||[]).map(reg),list:(spec.list||[]).map(reg),progress:reg(spec.progress),taskBar:{textFields:(spec.taskBarText||[]).map(reg)},fieldMap:{startDate:reg(spec.start),endDate:reg(spec.end),plannedStart:reg(spec.plannedStart),plannedEnd:reg(spec.plannedEnd)}},dynamic:{main:[],levels:{}}},conditionStyle:{value:{conditions:[]},arrow:{conditions:[]},fontColor:{conditions:[]}},ganttGridTitle:spec.title||"任务",ganttGridMinWidth:452,projectRowHeight:spec.rowHeight||202,defaultZoomLevel:spec.zoom||"day",pagination:{pageSize:spec.pageSize||20},showPagination:!1}};return spec.order&®(spec.order),saveCard({dsId,name:spec.name,remark:spec.remark,chartParentType:"table",chartType:"gantt",dtos,chartSetting:chart,id})}async function cardList(){const rows=[];for(let pageIndex=1;pageIndex<=200;pageIndex++){const d=await post("/index-block/get-index-block-folder-bussiness-info",{page:{pageIndex,pageSize:500}}),items=d.items||[];rows.push(...items.map(x=>({name:x.name,cardId:x.objId,folderId:x.id})));const total=Number(d.totalCount??d.total??0);if(items.length<500||total&&rows.length>=total)break}return rows}class TenantMismatchError extends Error{constructor(cardId,tenantName){super(`卡片 ${cardId} 不属于当前租户${tenantName?`「${tenantName}」`:""},已拒绝写入(写进去的数据会落在当前租户名下)`),this.name="TenantMismatchError",this.cardId=cardId}}const TENANT_GUARDED_WRITE=/\/index-block\/(?:update-new|new-fill-field|add-fill-field|fill-field|add-fill-field-by-s-c-m|new-import-fill-data|import-fill-data)(?:\?|$)/i;let _tenantCardIds=null;function tenantGuardTarget(method,p,body){if(String(method).toUpperCase()==="GET"||!TENANT_GUARDED_WRITE.test(String(p))||!body||typeof body!="object"||Array.isArray(body))return"";const pick=(...keys)=>{for(const k of keys){const hit=Object.keys(body).find(x=>x.toLowerCase()===k);if(hit&&body[hit])return String(body[hit])}return""};return/update-new/i.test(p)?pick("id"):pick("usedindexblockid")}async function assertCardInTenant(cardId){if(!cardId)return;const id=String(cardId).toLowerCase(),load=async()=>{_tenantCardIds=new Set((await cardList()).map(x=>String(x.cardId).toLowerCase()))};if(_tenantCardIds?_tenantCardIds.has(id)||await load():await load(),!_tenantCardIds.has(id))throw new TenantMismatchError(cardId,spaceOf(await ensureSession()).tenantName)}const cardDetail=cardId=>post("/index-block/get-new-index-block-detail",{id:cardId});async function cardSort(cardId,spec){let list;if(spec==="clear")list=[];else if(Array.isArray(spec))list=spec;else{const dtos=(await cardDetail(cardId)).indexBlockDataSettingDtos||[];list=String(spec).split(",").map(s=>s.trim()).filter(Boolean).map(s=>{const[col,dir="asc"]=s.split(":"),hit=dtos.find(x=>x.field===col)||dtos.find(x=>x.comment===col);if(!hit)throw new Error(`Card has no field "${col}". Available: ${dtos.map(x=>`${x.field}=${x.comment}`).join(" / ")}`);if(!["asc","desc"].includes(dir.toLowerCase()))throw new Error(`Sort direction must be asc or desc: ${s}`);return{field:hit.field,type:1,sort:dir.toLowerCase(),sortFields:[]}})}const sortField=list.map((x,i)=>({type:1,sortFields:[],...x,sortNum:i}));await api("PUT","/page-setting/page-card-index-block-sort-field",{usedIndexBlockId:cardId,sortField});const q=await queryCard({cardId,pageSize:1});return{cardId,requested:sortField.map(x=>`${x.field} ${x.type===2?"manual":x.sort}`),sqlHasOrderBy:/\bORDER\s+BY\s+(?:W\.)?(?:waxis|yaxis|waxis_type|order)\w*/i.test(q.sql||""),queryCode:q.code}}async function queryCard({cardId,pageSize=20,filters=[]}){const j=await api("POST","/page-setting/get-new-index-block-data",{usedIndexBlockId:cardId,filters,page:{pageIndex:1,pageSize:+pageSize}},{raw:!0}),rows=Array.isArray(j.data)?j.data:[];return{code:j.code,message:j.code===200?"":j.message,total:j.dataCount,rows:rows.length,columns:rows[0]?Object.keys(rows[0]):[],sql:j.extendData?.sql||"",data:rows}}async function planList(){return((await post("/index-block/get-page-setting-folder-bussiness-info",{isOnlySeeSelf:!1,page:{pageIndex:1,pageSize:500}})).items||[]).map(x=>({name:x.name,planId:x.objId,folderId:x.id}))}const planDetail=(planId,isEdit=!0)=>post(`/page-setting/get-new-page-plan/${planId}?isEdit=${isEdit}`,{});async function planAdd({planId,cardId,x=0,y=0,w=24,h=20}){const c=await post("/page-setting/new-page-card",{indexBlockId:cardId,sourceType:0,x:+x,y:+y,width:+w,height:+h,mobileX:0,mobileY:+y,mobileWidth:4,mobileHeight:+h,cardType:1,bindFilterIds:[],userPageSettingId:planId});return{pageCardId:c.id,x:c.x,y:c.y,width:c.width,height:c.height}}async function planFilter(planId,body){if(body.isTenantSystem!==!0)throw new Error("Plan filters must be saved with isTenantSystem:true (tenant template); see references/filters.md");await post("/page-setting/batch-add-or-update-filter",body);const after=await post("/page-setting/reset-user-filter",{userPageSettingId:planId,filterSourceType:4});return{planId,filters:(after||[]).map(f=>f.labelName)}}async function reportList(){return((await post("/index-block/get-index-block-report-folder-bussiness-info",{})).items||[]).map(x=>({name:x.name,cardId:x.indexBlockId,reportId:x.objId,folderId:x.id}))}const reportDetail=cardId=>post("/page-setting/get-new-page-card-report",{indexBlockId:cardId,isSubWarn:!1});async function cleanPrefix(prefix){if(!prefix||!prefix.startsWith("zz-"))throw new Error('clean only accepts prefixes starting with "zz-"');const removed={},del=async(kind,ids)=>{removed[kind]=ids.length,ids.length&&await post("/index-block/delete-folder-bussiness",ids)};return await del("reports",(await reportList()).filter(x=>x.name.startsWith(prefix)).map(x=>x.folderId)),await del("plans",(await planList()).filter(x=>x.name.startsWith(prefix)).map(x=>x.folderId)),await del("cards",(await cardList()).filter(x=>x.name.startsWith(prefix)).map(x=>x.folderId)),await del("datasets",(await dsList()).filter(x=>x.name.startsWith(prefix)).map(x=>x.folderId)),{prefix,removed}}function browserSessionScript(session){return`(() => { const s = ${JSON.stringify({t:session.token,c:session.tenantId,a:session.agentId})};
|
|
6
|
+
`+u.trim()}),a=a.replace(/\u0001(\d+)\u0001/g,(l,s)=>r[+s]),a.trim()+`
|
|
7
|
+
`}o(ut,"formatSql");function ge(t){const e=t.replace(/'(?:[^'\\]|\\.)*'/g,a=>" ".repeat(a.length)).replace(/\/\*[\s\S]*?\*\//g,a=>" ".repeat(a.length)).replace(/--[^\n]*/g,a=>" ".repeat(a.length)),n=[...e.matchAll(/;/g)].map(a=>a.index);if(!n.length)return t;const r=n[n.length-1];if(e.slice(r+1).trim()||n.length>1)throw new Error('Dataset SQL must be a single SELECT statement (found ";" inside)');return t.slice(0,r)+t.slice(r+1)}o(ge,"stripTrailingSemicolon");const ft=/^(information_schema|mysql|performance_schema|sys)\./i;function pt(t){const e=String(t).replace(/'(?:[^'\\]|\\.)*'/g,"''").replace(/\/\*[\s\S]*?\*\//g," ").replace(/--[^\n]*/g," ").trim();if(!/^(SELECT|WITH)\b/i.test(e))throw new P("SQL_READONLY","Rejected: dataset SQL must be a single read-only SELECT (or WITH … SELECT) query");if(/\bINTO\s+(OUTFILE|DUMPFILE)\b/i.test(e))throw new P("SQL_READONLY","Rejected: SELECT … INTO OUTFILE/DUMPFILE is not allowed");ge(t)}o(pt,"assertReadOnlySql");function ht(t){try{pt(t)}catch(e){throw B("sql-readonly",!1,{sqlSha256:k.sha256(String(t)),chars:String(t).length}),e}}o(ht,"precheckSql");async function mt(t){ht(t),t=ge(t);const e=ct(t),n=t.replace(/'(?:[^'\\]|\\.)*'/g,"''").replace(/`/g,""),r=[...new Set([...e.filter(i=>ft.test(i)),...[...n.matchAll(/\b(information_schema|mysql|performance_schema|sys)\s*\.\s*\w+/gi)].map(i=>i[0])])];if(r.length)throw new P("SQL_TABLE_NOT_ALLOWED",`Rejected: system schema access is not allowed (${r.join(", ")})`);if(e.length){const i=await dt(),l=e.filter(s=>!i.has(s.replace(/^[\w]+\./,"").toLowerCase()));if(l.length)throw new P("SQL_TABLE_NOT_ALLOWED",`Rejected: not in the space's table list (${l.join(", ")}). Only tenant views listed by \`linkai ds-tables\` can be used; to use another view, ask the LinkMore data team to expose it (this CLI cannot operate the database).`)}const a=await ee();return S("/index-block/get-field-info-by-data-set-sql",{dataSourceId:a.dataSourceId,sql:t,mode:2})}o(mt,"checkSql");const wt=99,gt=2,yt=o(t=>[...t].sort((e,n)=>Number(e.sort_num)-Number(n.sort_num)).map((e,n)=>{const r=["int","decimal"].includes(e.field_type);return{id:U.randomUUID(),tableFieldName:e.field_name,aliasCnName:e.field_name,fieldComment:"",argumentType:r?0:2,calculationMode:r?gt:wt,tableFieldType:r?"decimal":e.field_type,tableFieldTypeName:e.type_name,sortNum:n,sqlType:0,isCustomize:!1}}),"fieldColumns");async function Zt({id:t,name:e,sql:n,remark:r,noFormat:a=!1,current:i=null}){const l=await ee(),s=t&&t!==b?i||await bt(t):null;!a&&process.env.LM_NO_FMT!=="1"&&(n=ut(n)),n=ge(n);const u=await mt(n);if(!u?.length)throw new P("SQL_INVALID","运行 SQL 没有返回任何字段,无法生成映射字段");const c=Date.now(),d=[{id:U.randomUUID(),tableName:`sql_name_${c}`,tableComment:`sql_comment_${c}`,sortNum:0,fieldColumns:yt(u)}];try{await S("/index-block/get-data-set",{tables:d,joinOns:[],mode:2,dataSourceId:l.dataSourceId,isNewDataSet:!0,filters:[],sortFields:[],sql:n})}catch(f){throw f instanceof ve?new P("SQL_INVALID",`数据集取数验证失败,未保存:${f.message}`):f}return S("/index-block-data-argument/add-or-update",{indexBlockDataId:t||b,isNewDataSet:!0,mode:2,dataSourceId:l.dataSourceId,sql:n,name:e,parentId:s?.parentId||l.dsFolder,tag:s?.tag??"",remark:r??s?.remark??"",isPrivate:s?.isPrivate??!1,joinOns:[],tables:d})}o(Zt,"dsSave");async function St({withMeta:t=!1}={}){const n=[];for(let r=1;r<=200;r++){const a=await S("/index-block/get-index-block-data-folder-bussiness-info",{page:{pageIndex:r,pageSize:500}}),i=a.items||[];n.push(...i.map(s=>({name:s.name,dsId:s.objId,folderId:s.id,...t?{editTime:s.editTime||null,addUser:s.addUser||null}:{}})));const l=Number(a.totalCount??a.total??0);if(i.length<500||l&&n.length>=l)break}return n}o(St,"dsList");const bt=o(t=>v(`/index-block-data-argument/data-set-detail/${t}`),"dsDetail"),It=o(t=>v(`/index-block-data-argument/data-set-fields/${t}`),"dsFields");async function Xt(t){return(await v(`/index-block/data-set-quoted-infos/${t}`)||[]).map(n=>({name:n.name,cardId:n.indexBlockId}))}o(Xt,"dsRefs");function en(){return Object.entries(_e).sort(([t],[e])=>t.localeCompare(e)).map(([t,e])=>({kind:t,name:e._name||"",status:e._disabled?"disabled":e._minimal?"minimal":"full",tip:e._tip||""}))}o(en,"kinds");const xt={dim:{prefix:"waxis",axis:"",argumentType:2},meas:{prefix:"yaxis",axis:"y1",argumentType:1},series:{prefix:"waxis_type",axis:"",argumentType:3}};function te({field:t,col:e,axis:n="",argumentType:r=2,tableFieldType:a="string",title:i,orderByType:l=0,i:s=0}){return{id:U.randomUUID(),format:"default",axis:n,field:t,title:i??e,sortNum:s,calculationMode:0,seniorMode:0,periods:0,periodType:0,sqlText:"",comment:e,argumentType:r,orderByType:l,tableFieldId:b,tableFieldGroupId:b,periodsWithTableFieldId:b,decimal:0,thousandSeparator:!0,unit:"",tableFieldType:a,isCustomize:!1,isAggregation:!1,isUseSnow:!1,parent:null,nullZero:!1,isVirtual:!1,isFill:!1,uniqueId:U.randomUUID(),isCustomizeFill:!1,isInnerFill:!1,fieldDesc:""}}o(te,"baseDto");const Et=o(t=>({fieldExpression:"",aliasName:t.title,sortNum:t.sortNum,argumentType:t.argumentType,orderByType:t.orderByType,sqlText:"",tableFieldId:b,tableFieldName:null,tableFieldComment:t.comment,tableFieldType:t.tableFieldType,isCustomize:!1,fieldExpressionDesc:"",isAggregation:!1,isVirtual:!1,isFill:!1,uniqueId:t.uniqueId,isCustomizeFill:!1,isInnerFill:!1,isNeedTransferJson:!1,jsonFatherId:b,children:null}),"orderField");async function ne({dsId:t,name:e,remark:n,chartParentType:r,chartType:a,dtos:i,chartSetting:l,limitCount:s=500,id:u}){const c=await ee(),d=u&&u!==b?await Se(u):null,f={indexBlockDataId:t,isMulData:!1,indexBlockShowOrderFields:i.map(Et),filters:[],isFreeFill:!1,id:u||b,indexBlockType:0,name:e,tag:d?.tag??"",remark:n??d?.remark??"",parentId:d?.parentId||c.cardFolder,isPrivate:d?.isPrivate??!1,chartParentType:r,chartType:a,limitCount:s,indexBlockDataSettingDtos:i,chartSetting:JSON.stringify(l)};let m=u;return(!m||m===b)&&(m=await S("/index-block/new",f)),f.id=m,await S("/index-block/update-new",f),{cardId:m,name:e,chartType:`${r}/${a}`}}o(ne,"saveCard");async function ye(t){const e=await It(t);return{fields:e,meta:Object.fromEntries(e.map(n=>[n.tableFieldComment,n]))}}o(ye,"datasetMeta");async function tn({dsId:t,name:e,kind:n,dim:r,meas:a,series:i,id:l}){const s=_e[n];if(!s)throw new Error(`Chart kind "${n}" is not in the v2 chart registry; run: linkai kinds`);if(s._disabled)throw new Error(`Chart kind "${n}" (${s._name}) is disabled: ${s._disabled}`);const{fields:u,meta:c}=await ye(t),d=o((C,$,W,p)=>{const w=c[W];if(!w)throw new Error(`Dataset has no column "${W}". Available: ${u.map(A=>A.tableFieldComment).join(" / ")}`);const y=xt[C];return te({field:y.prefix+$,col:W,axis:y.axis,argumentType:y.argumentType,tableFieldType:w.tableFieldType,i:p})},"dto"),f=String(r||"").split(",").filter(Boolean),m=String(a||"").split(",").filter(Boolean);if(!m.length)throw new Error("At least one measure column is required");const g=f.map((C,$)=>d("dim",$+1,C,$));return m.forEach((C,$)=>g.push(d("meas",$+1,C,g.length))),i&&g.push(d("series",1,i,g.length)),ne({dsId:t,name:e,chartParentType:s.chartParentType,chartType:s.chartType,dtos:g,chartSetting:s.chartSetting,limitCount:0,id:l})}o(tn,"cardChart");async function nn({dsId:t,name:e,col:n,subtitle:r,color:a="#E8611C",id:i}){const l=[te({field:"yaxis1",col:n,axis:"y1",argumentType:1,tableFieldType:"decimal",title:r||n})];return ne({dsId:t,name:e,chartParentType:"meter",chartType:"list",dtos:l,chartSetting:{id:"list",parentId:"meter",config:{group:{type:"value",groups:[]},content:{type:"main",position:"left",bgColor:"lucency",icon:{show:!1,img:"",postion:"left"},labelText:{fontSize:12,fill:"rgb(153,153,153)"},valueText:{fontSize:14,fontWeight:800,fill:"rgba(0,0,0,0.85)"},labelTextMain:{fontSize:12,fill:"rgb(153,153,153)",fontStyle:"italic"},valueTextMain:{fontSize:30,fontWeight:800,fill:a},textPadding:0}}},id:i})}o(nn,"cardMeter");async function rn({dsId:t,spec:e,id:n}){const{meta:r}=await ye(t),a={},i=[];let l=0;const s=o(p=>{if(a[p])return a[p];const w=r[p];if(!w)throw new Error(`Dataset has no column "${p}". Available: ${Object.keys(r).join(" / ")}`);const y=`waxis${++l}`;return i.push(te({field:y,col:p,argumentType:w.argumentType,tableFieldType:w.tableFieldType,i:l-1})),a[p]=y,y},"reg"),u=o(()=>U.randomUUID(),"u"),c=o(p=>({fontSize:12,fill:"#1F2329",...p}),"st"),d={b:c({fontSize:15,fontWeight:600,lineHeight:22}),h:c({fontSize:13,fontWeight:600,lineHeight:20}),grey:c({fill:"#8F959E",lineHeight:18}),"":c({lineHeight:18})},f=o(()=>({id:u(),type:"labelValue",field:"",layout:"left",arrangement:"horizontal",fieldStyle:d[""],valueStyle:d[""],showLabel:!1,iconPosition:"left",labelValueGap:4,showColon:!1}),"emptyItem"),m=o((p,w,y)=>({id:"column_"+u(),field:[p],fixed:!1,enableCustomRender:!1,areas:[{id:u(),width:null,rows:[{id:u(),itemGap:10,items:[f()]}]}],name:w,contentAlign:"left",spanRows:!1,hidden:y,isPivot:!1,childPivotFields:[],topValueField:!1}),"plainColumn"),g=[];for(const p of e.columns||[]){if(p.plain){g.push(m(s(p.plain),p.header,!1));continue}const w=[],y=[],A=o(T=>{const[N,F=""]=T.split(":"),Ie=s(N);w.push(Ie);const se=F==="image",xe=d[se?"":F]||d[""],Ee={id:u(),type:se?"image":"labelValue",field:Ie,layout:"left",arrangement:"horizontal",fieldStyle:xe,valueStyle:xe,showLabel:!1,iconPosition:"left",labelValueGap:4,showColon:!1};return se&&(Ee.imgWidth=p.imgWidth||112),Ee},"mkItem");p.image&&y.push({id:u(),width:(p.imgWidth||112)+16,rows:[{id:u(),itemGap:10,items:[A(`${p.image}:image`)]}]}),y.push({id:u(),width:null,rows:(p.rows||[]).map(T=>({id:u(),itemGap:10,items:(Array.isArray(T)?T:[T]).map(A)}))}),g.push({id:"column_"+u(),field:w,fixed:!1,enableCustomRender:!0,areas:y,name:p.header,contentAlign:"left",spanRows:!1,hidden:!1,isPivot:!1,childPivotFields:[],topValueField:!1})}for(const p of e.hidden||[])g.push(m(s(p),p,!0));const C=o(p=>(e[p]||[]).flatMap(w=>{const y=a[w.dep]||s(w.dep),A=a[w.col]||s(w.col),T=[];for(const[N,F]of Object.entries(w.eq||{}))T.push({field:A,color:F,depField:y,operator:"==",value:isNaN(+N)?N:+N});for(const[N,F]of Object.entries(w.gte||{}))T.push({field:A,color:F,depField:y,operator:">=",value:+N});for(const[N,F]of Object.entries(w.ne||{}))T.push({field:A,color:F,depField:y,operator:"!=",value:N});return T}),"conds"),$=e.rowGrey?[{field:"",color:"#F7F8FA",depField:a[e.rowGrey.dep]||s(e.rowGrey.dep),operator:">=",value:e.rowGrey.gte}]:[];e.order&&s(e.order);const W={id:"agtable",parentId:"table",config:{sheetType:"table",showPagination:!0,pagination:{pageSize:e.pageSize||20},fitTableWidth:e.fitTableWidth!==!1,showHeader:!0,headerHeight:36,rowHeight:e.rowHeight||150,imgHeight:e.imgHeight||120,showLineNumber:!1,lineNumberTextAlign:"left",actionColTextAlign:"left",cellTextAlign:"left",cellBorder:{visible:!0,color:"#EDEFF2",width:1},total:{show:!1,labelColumn:"",labelText:"总计",rowtotalType:"all"},agColumnWidths:(e.columns||[]).map(p=>({key:g.find(w=>w.name===p.header).field[0],width:p.width||200})),conditionStyle:{value:{conditions:C("pills")},arrow:{conditions:[]},fontColor:{conditions:C("fontColor")},rowMark:{conditions:$},cellMark:{conditions:[]}},bgMark:{visible:!1,fields:[]},mobileViewFields:[],report:{visible:!1,fillField:{fields:[],fillFieldProperties:[]}},syncReport:{visible:!1,syncConfig:{webHookUrl:"",webHookId:"",relations:[],fields:[]}},wd:{rows:[],values:[],columns:g}}};return ne({dsId:t,name:e.name,remark:e.remark,chartParentType:"table",chartType:"agtable",dtos:i,chartSetting:W,limitCount:e.limitCount||2e3,id:n})}o(rn,"cardTable");async function an({dsId:t,spec:e,id:n}){const{meta:r}=await ye(t),a={},i=[];let l=0;const s=o(c=>{if(!c)return"";if(a[c])return a[c];const d=r[c];if(!d)throw new Error(`Dataset has no column "${c}". Available: ${Object.keys(r).join(" / ")}`);const f=`waxis${++l}`;return i.push(te({field:f,col:c,argumentType:d.argumentType,tableFieldType:d.tableFieldType,i:l-1})),a[c]=f,f},"reg"),u={id:"gantt",parentId:"table",config:{ganttRenderConfig:{showIcon:!0,style:{image:s(e.image),main:s(e.main),assist:s(e.assist),status:s(e.status),tags:(e.tags||[]).map(s),desc:(e.desc||[]).map(s),list:(e.list||[]).map(s),progress:s(e.progress),taskBar:{textFields:(e.taskBarText||[]).map(s)},fieldMap:{startDate:s(e.start),endDate:s(e.end),plannedStart:s(e.plannedStart),plannedEnd:s(e.plannedEnd)}},dynamic:{main:[],levels:{}}},conditionStyle:{value:{conditions:[]},arrow:{conditions:[]},fontColor:{conditions:[]}},ganttGridTitle:e.title||"任务",ganttGridMinWidth:452,projectRowHeight:e.rowHeight||202,defaultZoomLevel:e.zoom||"day",pagination:{pageSize:e.pageSize||20},showPagination:!1}};return e.order&&s(e.order),ne({dsId:t,name:e.name,remark:e.remark,chartParentType:"table",chartType:"gantt",dtos:i,chartSetting:u,id:n})}o(an,"cardGantt");async function Re(){const e=[];for(let n=1;n<=200;n++){const r=await S("/index-block/get-index-block-folder-bussiness-info",{page:{pageIndex:n,pageSize:500}}),a=r.items||[];e.push(...a.map(l=>({name:l.name,cardId:l.objId,folderId:l.id})));const i=Number(r.totalCount??r.total??0);if(a.length<500||i&&e.length>=i)break}return e}o(Re,"cardList");class Ue extends Error{static{o(this,"TenantMismatchError")}constructor(e,n){super(`卡片 ${e} 不属于当前租户${n?`「${n}」`:""},已拒绝写入(写进去的数据会落在当前租户名下)`),this.name="TenantMismatchError",this.cardId=e}}const Tt=/\/index-block\/(?:update-new|new-fill-field|add-fill-field|fill-field|add-fill-field-by-s-c-m|new-import-fill-data|import-fill-data)(?:\?|$)/i;let Q=null;function kt(t,e,n){if(String(t).toUpperCase()==="GET"||!Tt.test(String(e))||!n||typeof n!="object"||Array.isArray(n))return"";const r=o((...a)=>{for(const i of a){const l=Object.keys(n).find(s=>s.toLowerCase()===i);if(l&&n[l])return String(n[l])}return""},"pick");return/update-new/i.test(e)?r("id"):r("usedindexblockid")}o(kt,"tenantGuardTarget");async function Nt(t){if(!t)return;const e=String(t).toLowerCase(),n=o(async()=>{Q=new Set((await Re()).map(r=>String(r.cardId).toLowerCase()))},"load");if(Q?Q.has(e)||await n():await n(),!Q.has(e))throw new Ue(t,fe(await M()).tenantName)}o(Nt,"assertCardInTenant");const Se=o(t=>S("/index-block/get-new-index-block-detail",{id:t}),"cardDetail");async function on(t,e){let n;if(e==="clear")n=[];else if(Array.isArray(e))n=e;else{const i=(await Se(t)).indexBlockDataSettingDtos||[];n=String(e).split(",").map(l=>l.trim()).filter(Boolean).map(l=>{const[s,u="asc"]=l.split(":"),c=i.find(d=>d.field===s)||i.find(d=>d.comment===s);if(!c)throw new Error(`Card has no field "${s}". Available: ${i.map(d=>`${d.field}=${d.comment}`).join(" / ")}`);if(!["asc","desc"].includes(u.toLowerCase()))throw new Error(`Sort direction must be asc or desc: ${l}`);return{field:c.field,type:1,sort:u.toLowerCase(),sortFields:[]}})}const r=n.map((i,l)=>({type:1,sortFields:[],...i,sortNum:l}));await _("PUT","/page-setting/page-card-index-block-sort-field",{usedIndexBlockId:t,sortField:r});const a=await Ot({cardId:t,pageSize:1});return{cardId:t,requested:r.map(i=>`${i.field} ${i.type===2?"manual":i.sort}`),sqlHasOrderBy:/\bORDER\s+BY\s+(?:W\.)?(?:waxis|yaxis|waxis_type|order)\w*/i.test(a.sql||""),queryCode:a.code}}o(on,"cardSort");async function Ot({cardId:t,pageSize:e=20,filters:n=[]}){const r=await _("POST","/page-setting/get-new-index-block-data",{usedIndexBlockId:t,filters:n,page:{pageIndex:1,pageSize:+e}},{raw:!0}),a=Array.isArray(r.data)?r.data:[];return{code:r.code,message:r.code===200?"":r.message,total:r.dataCount,rows:a.length,columns:a[0]?Object.keys(a[0]):[],sql:r.extendData?.sql||"",data:a}}o(Ot,"queryCard");async function _t(){return((await S("/index-block/get-page-setting-folder-bussiness-info",{isOnlySeeSelf:!1,page:{pageIndex:1,pageSize:500}})).items||[]).map(e=>({name:e.name,planId:e.objId,folderId:e.id}))}o(_t,"planList");const Ct=o((t,e=!0)=>S(`/page-setting/get-new-page-plan/${t}?isEdit=${e}`,{}),"planDetail");async function sn({planId:t,cardId:e,x:n=0,y:r=0,w:a=24,h:i=20}){const l=await S("/page-setting/new-page-card",{indexBlockId:e,sourceType:0,x:+n,y:+r,width:+a,height:+i,mobileX:0,mobileY:+r,mobileWidth:4,mobileHeight:+i,cardType:1,bindFilterIds:[],userPageSettingId:t});return{pageCardId:l.id,x:l.x,y:l.y,width:l.width,height:l.height}}o(sn,"planAdd");async function ln(t,e){if(e.isTenantSystem!==!0)throw new Error("Plan filters must be saved with isTenantSystem:true (tenant template); see references/filters.md");await S("/page-setting/batch-add-or-update-filter",e);const n=await S("/page-setting/reset-user-filter",{userPageSettingId:t,filterSourceType:4});return{planId:t,filters:(n||[]).map(r=>r.labelName)}}o(ln,"planFilter");async function $t(){return((await S("/index-block/get-index-block-report-folder-bussiness-info",{})).items||[]).map(e=>({name:e.name,cardId:e.indexBlockId,reportId:e.objId,folderId:e.id}))}o($t,"reportList");const cn=o(t=>S("/page-setting/get-new-page-card-report",{indexBlockId:t,isSubWarn:!1}),"reportDetail");async function dn(t){if(!t||!t.startsWith("zz-"))throw new Error('clean only accepts prefixes starting with "zz-"');const e={},n=o(async(r,a)=>{e[r]=a.length,a.length&&await S("/index-block/delete-folder-bussiness",a)},"del");return await n("reports",(await $t()).filter(r=>r.name.startsWith(t)).map(r=>r.folderId)),await n("plans",(await _t()).filter(r=>r.name.startsWith(t)).map(r=>r.folderId)),await n("cards",(await Re()).filter(r=>r.name.startsWith(t)).map(r=>r.folderId)),await n("datasets",(await St()).filter(r=>r.name.startsWith(t)).map(r=>r.folderId)),{prefix:t,removed:e}}o(dn,"cleanPrefix");function un(t){return`(() => { const s = ${JSON.stringify({t:t.token,c:t.tenantId,a:t.agentId})};
|
|
8
8
|
localStorage['Access-Token'] = JSON.stringify(s.t);
|
|
9
9
|
localStorage['agentId'] = JSON.stringify(s.a);
|
|
10
10
|
localStorage['LM_COMPANY'] = JSON.stringify([{ id: s.c, name: '-' }]);
|
|
11
11
|
sessionStorage['LM_COMPANY_SELECT'] = JSON.stringify(s.c);
|
|
12
|
-
return true; })()`}function
|
|
12
|
+
return true; })()`}o(un,"browserSessionScript");function fn(t,e){return t=x.resolve(String(t)),h.mkdirSync(x.dirname(t),{recursive:!0}),h.writeFileSync(t,typeof e=="string"?e:JSON.stringify(e,null,1)),t}o(fn,"writeJsonFile");function pn(t){const e=x.resolve(String(t).trim());if(!h.existsSync(e))throw new P("FILE_NOT_FOUND",`文件不存在:${e}(相对路径按当前目录 ${process.cwd()} 解析)`);return h.readFileSync(e,"utf8")}o(pn,"readInputFile");const be="/linkai-kb",re=Object.freeze({"bi-builder":{pathPrefix:"report-builder",scope:"public",enabled:!0,fallback:"bundled"},"data-dictionary":{pathPrefix:"data-dictionary",scope:"tenant",enabled:!0,fallback:"none"},"integration-flow":{pathPrefix:"integration-flow",scope:"tenant",enabled:!1,fallback:"none"},user:{pathPrefix:"user",scope:"user",enabled:!1,fallback:"none"}});class je extends Error{static{o(this,"KbUnavailableError")}constructor(){super("知识库服务暂不可用"),this.name="KbUnavailableError"}}class At extends Error{static{o(this,"KbDocumentNotFoundError")}constructor(e,n){super(`知识库文档不存在:${e}/${n}`),this.name="KbDocumentNotFoundError"}}const ae=o(t=>(t.code===404||t.__status===404)&&/non-JSON/.test(String(t.message||"")),"endpointMissing");function Ft(t){const e=String(t||"").trim().replace(/\\/g,"/").replace(/^\.?\//,"").replace(/^knowledge\//,"");if(!e||e.split("/").some(n=>n===".."||n==="")||!e.endsWith(".md"))throw new Error(`知识库路径不合法:${t}(例:knowledge/report-builder/SOP.md)`);return e}o(Ft,"normalizeKbPath");function Lt(t,e){const n=Ft(t);let r=e?String(e):"",a=n;const i=Object.entries(re).find(([,s])=>n===s.pathPrefix||n.startsWith(`${s.pathPrefix}/`));r||(r=i?.[0]||"bi-builder",i&&(a=n.slice(i[1].pathPrefix.length).replace(/^\//,"")));const l=re[r];if(!l)throw new Error(`未知知识库:${r}(支持:${Object.keys(re).join(", ")})`);if(!l.enabled)throw new Error(`知识库 ${r} 已预留,当前版本尚未启用`);if(i&&i[0]!==r)throw new Error(`知识库路径 ${n} 不属于 ${r}`);if((n===l.pathPrefix||n.startsWith(`${l.pathPrefix}/`))&&(a=n.slice(l.pathPrefix.length).replace(/^\//,"")),!a||a.split("/").some(s=>s===".."||s==="")||!a.endsWith(".md"))throw new Error(`知识库路径不合法:${t}`);return{library:r,path:a,canonicalPath:`${l.pathPrefix}/${a}`,...l}}o(Lt,"kbRequest");async function hn(t="bi-builder"){const e=re[t];if(!e)throw new Error(`未知知识库:${t}`);if(!e.enabled)throw new Error(`知识库 ${t} 已预留,当前版本尚未启用`);if(t==="data-dictionary")throw new Error("数据字典使用结构化只读接口;请执行 linkai kb-sources,再按需使用 kb-search / kb-catalog / kb-object / kb-docs / kb-doc");const n=await _("GET",`${be}/catalog?library=${encodeURIComponent(t)}`,void 0,{raw:!0});if(ae(n))throw new je;if(n.code!==200)throw new Error(`读取知识库目录失败:${n.message||n.code}`);return{...n.data||{docs:[]},library:t,scope:e.scope}}o(hn,"kbCatalog");async function mn(t,e){const n=Lt(t,e);if(n.library==="data-dictionary")throw new Error("数据字典不再按 Markdown 文件路径读取;请使用 kb-search / kb-object / kb-docs / kb-doc");const r=await _("GET",`${be}/doc?library=${encodeURIComponent(n.library)}&path=${encodeURIComponent(n.path)}`,void 0,{raw:!0});if(ae(r))throw new je;if(r.code===404||r.__status===404)throw new At(n.library,n.path);if(r.code!==200)throw new Error(r.message||`读取知识库失败:${n.library}/${n.path}`);return{...r.data,library:n.library,scope:n.scope,path:n.canonicalPath}}o(mn,"kbDoc");const Dt="/ai-kb";class R extends Error{static{o(this,"AiKbError")}constructor(e,n){super(n||`数据字典服务返回错误:${e}`),this.name="AiKbError",this.code=Number(e)||500}}function oe(t){const e=new URLSearchParams;for(const[n,r]of Object.entries(t))r!=null&&r!==""&&e.set(n,String(r));return e.toString()}o(oe,"query");async function K(t,e,n){let r;try{r=await _(t,`${Dt}/${e}`,n,{raw:!0})}catch(a){throw a instanceof E?a:new R(503,`数据字典服务暂不可用:${a.message||a}`)}if(ae(r))throw new R(503,"数据字典接口尚未部署或当前网关不可用");if(r.code!==200)throw new R(r.code,r.message);return r.data}o(K,"aiKbRequest");const wn=o(()=>K("GET","sources"),"aiKbSources");function gn({source:t,keyword:e,pageIndex:n=1,pageSize:r=30}){return K("GET",`catalog?${oe({source:t,keyword:e,pageIndex:n,pageSize:r})}`)}o(gn,"aiKbCatalog");function yn({source:t,db:e,object:n}){return K("GET",`object?${oe({source:t,db:e,object:n})}`)}o(yn,"aiKbObject");function Sn({source:t,category:e,keyword:n,pageIndex:r=1,pageSize:a=30}){return K("GET",`docs?${oe({source:t,category:e,keyword:n,pageIndex:r,pageSize:a})}`)}o(Sn,"aiKbDocs");function Pt({source:t,doc:e,offset:n=0,limit:r=2e4,expectedVersion:a}){return K("GET",`doc?${oe({source:t,doc:e,offset:n,limit:r,expectedVersion:a})}`)}o(Pt,"aiKbDocPage");async function bn({source:t,doc:e,limit:n=2e4,maxRestarts:r=3}){for(let a=0;a<=r;a++){let i=0,l,s;const u=[];let c=0;try{for(;;){const d=await Pt({source:t,doc:e,offset:i,limit:n,expectedVersion:l});if(c++,s||=d,l||=d.contentVersion,d.contentVersion!==l)throw new R(409,"文档内容版本在分页读取过程中发生变化");if(u.push(d.content||""),d.nextOffset===null||d.nextOffset===void 0)return{...s,content:u.join(""),contentVersion:l,offset:0,nextOffset:null,totalChars:d.totalChars,truncated:!1,pages:c};const f=Number(d.nextOffset);if(!Number.isInteger(f)||f<=i)throw new R(503,"数据字典服务返回了无效的 nextOffset");i=f}}catch(d){if(!(d instanceof R)||d.code!==409||a===r)throw d}}throw new R(409,"文档持续变化,无法完成一致性读取")}o(bn,"aiKbDoc");function In({source:t,question:e,topTables:n=5,topDocs:r=3}){return K("POST","search",{source:t,question:e,topTables:n,topDocs:r})}o(In,"aiKbSearch");const Me=`${be}/plan-knowledge`;class ie extends Error{static{o(this,"PlanKnowledgeUnavailableError")}constructor(){super("方案知识库服务暂不可用"),this.name="PlanKnowledgeUnavailableError"}}class vt extends Error{static{o(this,"PlanKnowledgeConflictError")}constructor(e="方案知识库版本冲突,请重新读取当前版本后再更新"){super(e),this.name="PlanKnowledgeConflictError"}}const Be=o(t=>ae(t)||t.code===429||Number(t.code)>=500,"planKnowledgeUnavailable");async function xn(t){let e;try{e=await _("GET",`${Me}?planId=${encodeURIComponent(t)}`,void 0,{raw:!0})}catch(n){throw n instanceof E?n:new ie}if(Be(e))throw new ie;if(e.code===404)return null;if(e.code!==200)throw new Error(e.message||`读取方案知识库失败:${t}`);return e.data||null}o(xn,"planKnowledgeGet");async function En(t){let e;try{e=await _("POST",Me,t,{raw:!0})}catch(r){throw r instanceof E?r:new ie}if(Be(e))throw new ie;if(e.code===409)throw new vt(e.message||void 0);if(e.code!==200)throw new Error(e.message||`保存方案知识库失败:${t.planId}`);const n=e.data||{};if(Number(n.version)!==Number(t.version))throw new Error(`方案知识库版本异常:请求 v${t.version},服务器返回 v${n.version||"?"}`);return n}o(En,"planKnowledgePut");export{R as AiKbError,ve as ApiError,re as KB_LIBRARIES,At as KbDocumentNotFoundError,je as KbUnavailableError,P as KindError,E as LoginRequiredError,Ye as NetworkError,vt as PlanKnowledgeConflictError,ie as PlanKnowledgeUnavailableError,j as SITE_ID,Ue as TenantMismatchError,b as Z,gn as aiKbCatalog,bn as aiKbDoc,Pt as aiKbDocPage,Sn as aiKbDocs,yn as aiKbObject,In as aiKbSearch,wn as aiKbSources,dt as allowedTables,_ as api,et as artifactsDir,Yt as assertApiAllowed,Nt as assertCardInTenant,pt as assertReadOnlySql,un as browserSessionScript,tn as cardChart,Se as cardDetail,an as cardGantt,Re as cardList,nn as cardMeter,on as cardSort,rn as cardTable,mt as checkSql,Pe as cleanApiMessage,dn as cleanPrefix,nt as clearSession,Ht as clearShortCache,ce as configDir,de as configFile,ee as ctx,bt as dsDetail,It as dsFields,St as dsList,Xt as dsRefs,Zt as dsSave,M as ensureSession,yt as fieldColumns,me as forgetCredentials,ut as formatSql,v as get,ot as isPlatformTenant,hn as kbCatalog,mn as kbDoc,Lt as kbRequest,en as kinds,B as logGuard,Ft as normalizeKbPath,Jt as oauthLogin,qt as oauthLogout,sn as planAdd,Ct as planDetail,ln as planFilter,xn as planKnowledgeGet,En as planKnowledgePut,_t as planList,S as post,ht as precheckSql,Ot as queryCard,I as readConfig,pn as readInputFile,L as readSession,cn as reportDetail,$t as reportList,Z as saveSession,Qt as setClientVersion,ue as shortCache,fe as spaceOf,ct as sqlTables,ge as stripTrailingSemicolon,Vt as switchTenant,kt as tenantGuardTarget,Fe as tenantList,D as tokenPayload,fn as writeJsonFile};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
`)}function
|
|
1
|
+
var D=Object.defineProperty;var s=(e,r)=>D(e,"name",{value:r,configurable:!0});import I from"node:crypto";import u from"node:fs";import f from"node:path";import{parseYaml as L}from"@linkaicli/linkai-cli/utils";import*as c from"./core.js";const N=s(e=>e&&e!==c.Z,"isRealId"),$=s(e=>Array.isArray(e)?e.map(String).map(r=>r.trim()).filter(Boolean):e?[String(e).trim()]:[],"list"),K=s(e=>String(e||"").replace(/[\\/:*?"<>|\s]+/g,"_").slice(0,120)||"plan","safe"),E=s(e=>/```\s*sql|\bselect\b[\s\S]{0,2000}\bfrom\b/i.test(JSON.stringify(e||{})),"containsFullSql");function U(e){const r=f.resolve(String(e)),n=u.readFileSync(r,"utf8");return/\.json$/i.test(r)?JSON.parse(n):L(n)}s(U,"readKnowledgeFile");function j(e){const r=[],n=s((t,o)=>{String(t||"").trim()||r.push(o)},"need");n(e?.summary,"knowledge.summary(方案内容)必填"),n(e?.problem,"knowledge.problem(解决的问题)必填"),(!Array.isArray(e?.cards)||!e.cards.length)&&r.push("knowledge.cards 至少需要一张业务卡片");for(const[t,o]of(e?.cards||[]).entries())n(o.cardId,`knowledge.cards[${t}].cardId 必填`),n(o.content,`knowledge.cards[${t}].content(卡片内容)必填`),n(o.source,`knowledge.cards[${t}].source(数据来源)必填`),n(o.grain,`knowledge.cards[${t}].grain(数据粒度)必填`),n(o.dataLogic,`knowledge.cards[${t}].dataLogic(取数逻辑)必填`);return E(e)&&r.push("方案知识库不得包含完整 SQL;请改写为自然语言取数逻辑和规则"),r}s(j,"validateKnowledgeAuthoring");function q(e,r){return{summary:e.knowledge?.summary,problem:e.knowledge?.problem,rules:$(e.knowledge?.rules),changeSummary:e.knowledge?.changeSummary||"首次创建",cards:(e.cards||[]).map(n=>({cardId:r.cards[n.key],content:n.knowledge?.content,source:n.knowledge?.source,grain:n.knowledge?.grain,dataLogic:n.knowledge?.dataLogic,rules:$(n.knowledge?.rules)}))}}s(q,"knowledgeAuthoringFromBuild");async function F(e){const r=c.readConfig(),n=await c.planDetail(e,!0),t=[];for(const o of n.pageCards||[]){const i=o.indexBlockId||o.indexBlock?.id;if(!N(i))continue;const l=await c.cardDetail(i),g=l.indexBlockDataId,a=N(g)?await c.dsDetail(g):null;t.push({pageCardId:o.id,cardId:i,name:l.name,chartType:`${l.chartParentType||""}/${l.chartType||""}`.replace(/^\/$/,""),dataset:a?{datasetId:g,name:a.name}:null})}return{plan:{planId:e,name:n.name,url:`${r.origin}/view/plan?plan=${e}`},cards:t}}s(F,"readPlanFacts");function A(e,r,{operation:n,baseline:t=!1}={}){const o=new Map((r.cards||[]).map(a=>[String(a.cardId),a])),i=new Set(e.cards.map(a=>String(a.cardId))),l=e.cards.filter(a=>!o.has(String(a.cardId))).map(a=>`${a.name}(${a.cardId})`),g=[...o.keys()].filter(a=>!i.has(a));if(l.length||g.length){const a=[];throw l.length&&a.push(`以下线上业务卡片缺少知识段落:${l.join("、")}`),g.length&&a.push(`知识文件包含不在方案中的卡片:${g.join("、")}`),new c.KindError("KNOWLEDGE_INVALID",a.join(";"))}return{schemaVersion:1,name:`${e.plan.name}方案知识库`,plan:e.plan,operation:n,baseline:t,summary:String(r.summary).trim(),problem:String(r.problem).trim(),rules:$(r.rules),changeSummary:String(r.changeSummary||(n==="create"?"首次创建":"方案更新")).trim(),cards:e.cards.map((a,h)=>{const m=o.get(String(a.cardId));return{order:h+1,...a,content:String(m.content).trim(),source:String(m.source).trim(),grain:String(m.grain).trim(),dataLogic:String(m.dataLogic).trim(),rules:$(m.rules)}})}}s(A,"assembleDocument");function y(e,r){const n=[];if(n.push(`# ${e.name}`,""),n.push(`- 版本:v${r}`),n.push(`- 方案名称:${e.plan.name}`),n.push(`- 方案 ID:${e.plan.planId}`),n.push(`- 方案地址:${e.plan.url}`,""),n.push("## 方案说明","","### 方案内容","",e.summary,"","### 解决的问题","",e.problem,""),n.push("### 公共规则",""),e.rules.length)for(const t of e.rules)n.push(`- ${t}`);else n.push("(无额外公共规则)");n.push("","## 卡片与数据","");for(const t of e.cards){if(n.push(`## ${String(t.order).padStart(2,"0")} ${t.name}`,""),n.push("### 卡片内容","",`- 卡片 ID:${t.cardId}`,`- 图表类型:${t.chartType||"未标注"}`,"",t.content,""),n.push("### 数据集",""),n.push(`- 数据集名称:${t.dataset?.name||"(无独立数据集)"}`),n.push(`- 数据集 ID:${t.dataset?.datasetId||"(无)"}`),n.push(`- 数据来源:${t.source}`),n.push(`- 数据粒度:${t.grain}`,""),n.push("### 取数逻辑与规则","",t.dataLogic,""),t.rules.length)for(const o of t.rules)n.push(`- ${o}`);n.push("")}return n.push("## 版本记录","",`- v${r}:${e.baseline?"历史方案首次纳管;":""}${e.changeSummary}`,""),n.join(`
|
|
2
|
+
`)}s(y,"renderPlanKnowledge");function O(e){const{operation:r,baseline:n,...t}=e;return I.createHash("sha256").update(JSON.stringify(t)).digest("hex")}s(O,"contentHash");function b(e){const r=c.readSession()?.tenantId||"unknown-tenant";return c.artifactsDir("plan-knowledge",K(r),K(e))}s(b,"localPlanDir");function v(e){const r=b(e);return u.existsSync(r)?u.readdirSync(r).filter(n=>/^v\d+\.pending\.json$/.test(n)).sort((n,t)=>Number(n.slice(1))-Number(t.slice(1))).map(n=>f.join(r,n)):[]}s(v,"pendingFiles");function P(e){const r=b(e);return u.existsSync(r)?u.readdirSync(r).filter(n=>/^v\d+\.md$/.test(n)).sort((n,t)=>Number(n.slice(1))-Number(t.slice(1))).map(n=>f.join(r,n)):[]}s(P,"localMarkdownFiles");function C(e){const r=[...v(e),...P(e)].map(n=>Number(f.basename(n).match(/^v(\d+)/)?.[1]||0));return Math.max(0,...r)}s(C,"highestLocalVersion");function J(e){const r=v(e).at(-1);if(!r)return null;try{return{file:r,...JSON.parse(u.readFileSync(r,"utf8"))}}catch{return null}}s(J,"latestPending");async function W(e){try{const r=await c.planKnowledgeGet(e);return r?{planId:e,exists:!0,version:`v${Number(r.version)||1}`,content:r.markdown||y(r.document,Number(r.version)||1)}:{planId:e,exists:!1}}catch(r){if(!(r instanceof c.PlanKnowledgeUnavailableError))throw r;const n=P(e).at(-1);if(!n)return{planId:e,exists:!1,next:"当前没有可读取的方案知识库:照常完成修改,最后用 plan-knowledge --mode update 同步,CLI 会自动补建 v1 基线"};const t=Number(f.basename(n).match(/^v(\d+)/)?.[1]||1);return{planId:e,exists:!0,version:`v${t}`,content:u.readFileSync(n,"utf8")}}}s(W,"readCurrentPlanKnowledge");function V(e,r,n,t){const o=b(e);u.mkdirSync(o,{recursive:!0,mode:448});const i=f.join(o,`v${r}`);u.writeFileSync(`${i}.pending.json`,JSON.stringify({payload:n,createdAt:new Date().toISOString()},null,2),{mode:384}),u.writeFileSync(`${i}.md`,t,{mode:384})}s(V,"savePending");async function B(e){for(const r of v(e)){let n;try{n=JSON.parse(u.readFileSync(r,"utf8"))}catch{continue}try{await c.planKnowledgePut(n.payload),u.rmSync(r)}catch(t){if(t instanceof c.PlanKnowledgeUnavailableError)return;if(t instanceof c.PlanKnowledgeConflictError){u.renameSync(r,r.replace(/\.pending\.json$/,".conflict.json"));continue}return}}}s(B,"syncPending");async function Q({planId:e,operation:r,authoring:n}){if(!["create","update"].includes(r))throw new Error("operation 必须是 create 或 update;不明确时先询问用户");const t=j(n);if(r==="update"&&!String(n?.changeSummary||"").trim()&&t.push("修改方案时 knowledge.changeSummary(本次修改说明)必填"),t.length)throw new c.KindError("KNOWLEDGE_INVALID",t.join(";"));await B(e);const o=await F(e);let i=null,l=!1;try{i=await c.planKnowledgeGet(e)}catch(d){if(!(d instanceof c.PlanKnowledgeUnavailableError))throw d;l=!0}const a=A(o,n,{operation:r,baseline:r==="update"&&!i}),h=O(a);if(i?.contentHash===h){const d=Number(i.version)||1,w=i.document||a;return{plan:o.plan,planKnowledge:{name:w.name,version:`v${d}`,content:i.markdown||y(w,d)}}}const m=i?null:J(e);if(m?.payload?.contentHash===h){const d=Number(m.payload.version)||1,w=m.payload.document||a;return{plan:o.plan,planKnowledge:{name:w.name,version:`v${d}`,content:m.payload.markdown||y(w,d)}}}if(r==="create"&&i)throw new Error(`方案 ${e} 已有方案知识库,请使用 update`);const k=Math.max(Number(i?.version)||0,C(e));let p=k+1,S=y(a,p);const x={planId:e,operation:r,expectedVersion:k,version:p,contentHash:h,document:a,markdown:S};if(!l)try{const d=await c.planKnowledgePut(x);p=Number(d?.version)||p,S=y(a,p)}catch(d){if(!(d instanceof c.PlanKnowledgeUnavailableError))throw d;l=!0}return l&&V(e,p,x,S),{plan:o.plan,planKnowledge:{name:a.name,version:`v${p}`,content:S}}}s(Q,"publishPlanKnowledge");export{E as containsFullSql,q as knowledgeAuthoringFromBuild,Q as publishPlanKnowledge,W as readCurrentPlanKnowledge,U as readKnowledgeFile,y as renderPlanKnowledge,j as validateKnowledgeAuthoring};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
var x=Object.defineProperty;var r=(e,t)=>x(e,"name",{value:t,configurable:!0});import p from"node:fs";import y from"node:path";import D from"node:os";const L=2*3600*1e3,m=r(()=>y.join(D.homedir(),".linkai","cache","dict-reads.json"),"file");function g(){try{return JSON.parse(p.readFileSync(m(),"utf8"))||{}}catch{return{}}}r(g,"readState");function h(e){try{p.mkdirSync(y.dirname(m()),{recursive:!0,mode:448}),p.writeFileSync(m(),JSON.stringify(e),{mode:384})}catch{}}r(h,"writeState");function C({tenantId:e,source:t,flowId:u=null,op:l,now:f=Date.now()}){if(!e||!t)return;const a=g();a.reads??={},a.reads[e]??={},a.reads[e][String(t).toLowerCase()]={readAt:f,flowId:u,op:l},h(a)}r(C,"recordRead");function R(e,t=Date.now()){h({reads:{},resetAt:t,resetReason:e})}r(R,"resetReads");function b(e){if(!e)return null;const t=Date.parse(`${String(e).trim().replace(" ","T")}+08:00`);return Number.isFinite(t)?t:null}r(b,"parseServerTime");const S=r(e=>new Date(e+8*3600*1e3).toISOString().slice(0,19).replace("T"," "),"fmt");async function T({tenantId:e,source:t,flowId:u,fetchSources:l,fetchDocs:f,now:a=Date.now()}){let k;try{k=await l()}catch(n){return{ok:!0,skipped:`数据字典服务暂不可用(${String(n?.message||n).slice(0,80)}),本次未校验`}}const c=(k||[]).find(n=>String(n.source).toLowerCase()===String(t).toLowerCase());if(!c||c.metadataReady===!1)return{ok:!0,skipped:"当前数据集所用数据源没有可用的数据字典,本次未校验"};let w=[];try{w=await f()||[]}catch{}const A=[b(c.lastSuccessAt),...w.map(n=>b(n.updatedAt))].filter(Number.isFinite),o=A.length?Math.max(...A):null,s=g().reads?.[e]?.[String(t).toLowerCase()],d={source:t,sourceName:c.name??null,dictUpdatedAt:o?S(o):null};if(!s)return{...d,ok:!1,reason:"当前租户还没有读取数据字典(切换租户或重新登录后必须重新读取)"};const i=S(s.readAt);return o&&s.readAt<o?{...d,readAt:i,ok:!1,reason:`数据字典在 ${S(o)} 更新过,晚于上次读取(${i}),必须重新读取`}:s.flowId!==u&&a-s.readAt>L?{...d,readAt:i,ok:!1,reason:`上次读取数据字典是 ${i},不在当前任务内且已超过 2 小时,必须重新读取`}:{...d,readAt:i,ok:!0}}r(T,"dictionaryStatus");function _(e){return`先读取当前租户的最新数据字典:linkai kb-sources 确认数据源,再按需 linkai kb-search --source ${e} --question "<业务问题>"、kb-object / kb-docs / kb-doc 核对本次 SQL 用到的视图、字段与口径,然后重新执行。不能沿用其它租户或旧的数据字典结论。`}r(_,"readHint");export{L as MAX_AGE_OUTSIDE_FLOW_MS,T as dictionaryStatus,b as parseServerTime,_ as readHint,C as recordRead,R as resetReads};
|
package/clis/linkai/fill-card.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const
|
|
1
|
+
var q=Object.defineProperty;var p=(e,o)=>q(e,"name",{value:o,configurable:!0});const h=Object.freeze({varchar:"string",int:"int",decimal:"decimal",datetime:"datetime",date:"date",bool:"bool",img:"string",file:"string",btn:"string"});function F(e){return e?Array.isArray(e)?e.flatMap(F):typeof e!="object"?[]:Array.isArray(e.fillFieldProperties)?e.fillFieldProperties:Array.isArray(e.fillFields)?F(e.fillFields):[]:[]}p(F,"properties");function $(e){if(!e)return null;if(typeof e=="object")return e.config?.report;if(typeof e!="string")return null;try{return JSON.parse(e).config?.report}catch{return null}}p($,"chartReport");function I(e,o,l){if(String(e).toUpperCase()!=="POST"||!/(?:^|\/)index-block\/(?:new|update-new)(?:\?|$)/i.test(String(o))||!l||typeof l!="object"||Array.isArray(l))return;const d=$(l.chartSetting),s=F([l.fillFields,l.fillField]),w=F([d?.fillFields,d?.fillField]),u=[...s,...w],a=Array.isArray(l.indexBlockShowOrderFields)?l.indexBlockShowOrderFields:[],f=Array.isArray(l.indexBlockDataSettingDtos)?l.indexBlockDataSettingDtos:[];if(!u.length&&!a.some(i=>i?.isFill)&&!f.some(i=>i?.isFill))return;if(!s.length)throw new Error("填报卡片缺少顶层 fillFields[].fillFieldProperties 定义");const c=new Map;for(const i of u){const n=String(i?.fillField||"").trim(),t=i?.fillFieldType;if(!n||!Object.hasOwn(h,t))throw new Error(`填报字段「${n||"未命名"}」的 fillFieldType 无效;字符必须用 varchar,不能用 string`);if(i.tableFieldType&&i.tableFieldType!==h[t])throw new Error(`填报字段「${n}」的 tableFieldType 与 fillFieldType 不匹配`);if(!i.uniqueId)throw new Error(`填报字段「${n}」缺少 uniqueId`);const r=c.get(i.uniqueId);if(r&&(r.fillField!==n||r.fillFieldType!==t))throw new Error(`填报字段「${n}」在多个配置位置的名称或类型不一致`);c.set(i.uniqueId,{fillField:n,fillFieldType:t})}for(const i of a.filter(n=>n?.isFill)){const n=c.get(i.uniqueId);if(!n)throw new Error("左侧填报字段缺少同 uniqueId 的填报属性定义");const t=n.fillField,r=`{${t}}`;if(!t||i.argumentType!==4||i.isCustomize!==!0||i.fieldExpression!==r||i.fieldExpressionDesc!==r)throw new Error(`左侧填报字段「${t||"未命名"}」必须是 argumentType:4、isCustomize:true,表达式为 {字段名}`);if(i.tableFieldType!==h[n.fillFieldType])throw new Error(`左侧填报字段「${t}」的 tableFieldType 与填报类型不匹配`)}for(const[i,n]of c)if(!a.some(t=>t?.isFill&&t.uniqueId===i))throw new Error(`填报字段「${n.fillField}」缺少同 uniqueId 的左侧填报定义`);for(const i of f.filter(n=>n?.isFill)){if(i.argumentType!==2)throw new Error(`右侧绘制区的填报字段「${i.title||i.comment||""}」必须保持 argumentType:2`);if(!a.some(n=>n?.isFill&&n.uniqueId===i.uniqueId))throw new Error("右侧绘制区的填报字段缺少同 uniqueId 的左侧填报定义")}}p(I,"validateFillCardPayload");function m(e){return typeof e=="string"?e.trim():String(e?.fillField||e?.field||e?.title||e?.name||"").trim()}p(m,"nameOf");function O(e,o="fillfieldid"){if(!/^fillfieldid\d*$/.test(o))throw new Error(`key 只能是 fillfieldid、fillfieldid1…(选择填报块),收到:${o}`);const l=Number(o.slice(11)||0),d=Array.isArray(e?.fillFields)?e.fillFields:[],s=d.find(r=>Number(r?.sortNum)===l)||d[l],w=F(s).map(m).filter(Boolean);if(!w.length)throw new Error(`卡片没有 ${o} 对应的填报块,或填报块没有字段`);const u=$(e?.chartSetting),a=d.indexOf(s),f=p((r,g)=>{if(Array.isArray(r))return r;if(typeof r!="string"||!r.trim())return[];try{const y=JSON.parse(r);return Array.isArray(y)?y:[y]}catch{}return(g&&r.includes(";")?r.split(";")[a]??"":r).split(/[,,]/).map(y=>y.trim()).filter(Boolean)},"asList"),c=[f(s?.freeFillUniqueField,!1),f(s?.freeFillUniqueFields,!1),f(e?.freeFillUniqueFields,!0),f(u?.freeFillUniqueFields,!0)].find(r=>r.length)||[],i=new Map(F(s).map(r=>[String(r?.uniqueId||""),m(r)])),n=c.map(r=>typeof r=="object"&&r?.uniqueId&&i.get(String(r.uniqueId))||i.get(String(r).trim())||m(r)).filter(Boolean),t=n.filter(r=>!w.includes(r));if(t.length)throw new Error(`无法识别卡片的依据字段(${t.join("、")});请先回读页面配置确认,不要猜测`);return{key:o,fields:w,uniqueFields:n}}p(O,"fillBlockOf");const E=p(e=>e==null?"":typeof e=="object"?JSON.stringify(e):String(e),"cell");function j(e,o){if(!Array.isArray(o)||!o.length)throw new Error("records 必须是非空 JSON 数组,每个元素是一条业务记录");const l=new Set(e.fields),d=new Map;return o.map((s,w)=>{const u=`第 ${w+1} 条记录`;if(!s||typeof s!="object"||Array.isArray(s))throw new Error(`${u}不是对象`);const{_id:a,...f}=s,c=Object.keys(f).filter(t=>!l.has(t));if(c.length)throw new Error(`${u}含有填报块里没有的字段:${c.join("、")}(可用:${e.fields.join("、")})`);const i=e.fields.filter(t=>!Object.hasOwn(f,t));if(i.length)throw new Error(`${u}缺少字段:${i.join("、")}。一条记录必须一次带齐全部填报字段,禁止拆字段写`);if(!a&&!e.uniqueFields.length)throw new Error(`${u}没有 _id,且卡片未配置“填报内容依据的字段”:无法安全新增(不能自造记录ID)。请在页面上新增,或先配置依据字段`);if(e.uniqueFields.length){const t=e.uniqueFields.map(g=>E(f[g]).trim());if(t.some(g=>!g))throw new Error(`${u}的依据字段(${e.uniqueFields.join("、")})不能为空`);const r=JSON.stringify(t);if(d.has(r))throw new Error(`${u}与第 ${d.get(r)+1} 条的依据字段重复:${t.join(" / ")}`);d.set(r,w)}const n=JSON.stringify(Object.fromEntries(e.fields.map(t=>[t,E(f[t])])));return{key:e.key,...a?{value:String(a)}:{},fillFieldValue:n}})}p(j,"buildFillValues");export{j as buildFillValues,O as fillBlockOf,I as validateFillCardPayload};
|
package/clis/linkai/oauth.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
`),
|
|
1
|
+
var j=Object.defineProperty;var c=(e,n)=>j(e,"name",{value:n,configurable:!0});import E from"node:crypto";import{spawn as B}from"node:child_process";const m="linkai-cli",z="linkai offline_access",b=c(e=>Buffer.from(e).toString("base64url"),"base64url"),T=c(()=>b(E.randomBytes(32)),"randomValue");function y(e,n,o){let t;try{t=new URL(e)}catch{throw new Error(`OAuth 发现文档缺少有效的 ${o}`)}if(t.origin!==n||!["http:","https:"].includes(t.protocol)||t.username||t.password||t.hash)throw new Error(`OAuth 发现文档的 ${o} 地址与领谋配置不匹配`);return t}c(y,"endpoint");async function $(e){const n=new URL(e.apiBase).origin,o=await fetch(`${e.apiBase}/.well-known/oauth-authorization-server`,{headers:{Accept:"application/json"},signal:AbortSignal.timeout(15e3)});if(!o.ok)throw new Error(`领谋 OAuth 接口暂不可用(HTTP ${o.status}),请确认后端已部署`);const t=await o.json().catch(()=>null);if(!t||t.issuer!==e.apiBase||!Array.isArray(t.code_challenge_methods_supported)||!t.code_challenge_methods_supported.includes("S256")||!Array.isArray(t.grant_types_supported)||!t.grant_types_supported.includes("authorization_code")||!t.grant_types_supported.includes("refresh_token"))throw new Error("领谋 OAuth 发现文档不符合 CLI 的授权码与 PKCE 要求");const r=y(t.authorization_endpoint,e.origin,"authorization_endpoint"),s=y(t.token_endpoint,n,"token_endpoint"),u=y(t.revocation_endpoint,n,"revocation_endpoint"),a=t.cli_session_endpoint?y(t.cli_session_endpoint,n,"cli_session_endpoint"):null;if(s.pathname!=="/oauth/token"||u.pathname!=="/oauth/revoke")throw new Error("领谋 OAuth 发现文档中的令牌接口路径不符合预期");if(a&&(a.pathname!=="/oauth/cli/sessions"||a.search))throw new Error("领谋线上授权会话接口路径不符合预期");return{authorization:r,token:s,revocation:u,online:a}}c($,"oauthMetadata");function H(e){const[n,o]=process.platform==="darwin"?["open",[e]]:process.platform==="win32"?["rundll32.exe",["url.dll,FileProtocolHandler",e]]:["xdg-open",[e]];return new Promise(t=>{const r=B(n,o,{stdio:"ignore",detached:!0});r.once("error",()=>t(!1)),r.once("spawn",()=>{r.unref(),t(!0)})})}c(H,"openSystemBrowser");async function v(e,n){let o;try{o=await fetch(e,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"},body:new URLSearchParams(n),signal:AbortSignal.timeout(15e3)})}catch{const r=new Error("领谋 OAuth 请求结果无法确认,请重新授权");throw r.code="oauth_exchange_unknown",r}const t=await o.json().catch(()=>null);if(!o.ok){const r=typeof t?.error=="string"?t.error:`HTTP_${o.status}`,s=new Error(`领谋 OAuth 请求失败:${r}`);throw s.code=o.status>=500?"oauth_exchange_unknown":r,s}return t}c(v,"oauthForm");async function O(e,n,o=15e3){const t=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(n),signal:AbortSignal.timeout(o)}),r=await t.json().catch(()=>null);if(!t.ok){const s=new Error(`领谋线上授权请求失败:${r?.error||`HTTP_${t.status}`}`);throw s.code=r?.error||"oauth_online_error",s}return r}c(O,"onlineRequest");async function R(e,n){if(!e.ok||!e.body)throw new Error(`领谋线上授权事件连接失败(HTTP ${e.status})`);const o=e.body.getReader(),t=new TextDecoder;let r="";try{for(;;){const{value:s,done:u}=await o.read();if(u)throw new Error("领谋线上授权事件连接已断开,请重新执行 linkai login");if(r=(r+t.decode(s,{stream:!0})).replace(/\r\n/g,`
|
|
2
|
+
`),r.length>65536)throw new Error("领谋线上授权事件无效");let a;for(;(a=r.indexOf(`
|
|
3
3
|
|
|
4
|
-
`))!==-1;){const
|
|
5
|
-
`).some(
|
|
6
|
-
`).find(
|
|
7
|
-
请核对页面中的授权码:${
|
|
8
|
-
`),await
|
|
4
|
+
`))!==-1;){const h=r.slice(0,a);if(r=r.slice(a+2),!h.split(`
|
|
5
|
+
`).some(d=>d==="event: authorization"))continue;const p=h.split(`
|
|
6
|
+
`).find(d=>d.startsWith("data: "));let l;try{l=JSON.parse(p?.slice(6))}catch{throw new Error("领谋线上授权事件无效")}if(l?.state!==n)throw new Error("领谋线上授权事件 state 不匹配");if(l.status==="approved")return;throw l.status==="expired"?new Error("等待领谋 OAuth 授权超时,请重新执行 linkai login"):new Error("领谋线上授权未完成,请重新执行 linkai login")}}}finally{await o.cancel().catch(()=>{})}}c(R,"waitForOnlineApproval");function P(e,n){if(!e||typeof e.access_token!="string"||typeof e.refresh_token!="string"||typeof e.tenant_id!="string"||e.token_type?.toLowerCase()!=="bearer")throw new Error("领谋 OAuth 令牌响应缺少必要字段");return{token:e.access_token,refreshToken:e.refresh_token,tenantId:e.tenant_id,agentId:n,authMode:"oauth"}}c(P,"tokenSession");async function M(e,n){const{token:o}=await $(e),t=await v(o,{grant_type:"refresh_token",client_id:m,refresh_token:n.refreshToken});try{return P(t,n.agentId)}catch{const r=new Error("领谋 OAuth 续期响应不完整,请重新授权");throw r.code="oauth_exchange_unknown",r}}c(M,"refreshOauthSession");async function C(e,n){if(!n?.refreshToken)return!1;const{revocation:o}=await $(e);return await v(o,{client_id:m,token:n.refreshToken}),!0}c(C,"revokeOauthSession");async function W(e,n,o){const t=await $(e),r=T(),s=T(),u=b(E.createHash("sha256").update(s,"ascii").digest()),a="http://127.0.0.1:49152/callback",h=await fetch(new URL("/oauth/authorize",e.apiBase),{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json",Authorization:`Bearer ${n.token}`,Agentid:n.agentId,Tenantid:n.tenantId},body:JSON.stringify({response_type:"code",client_id:m,redirect_uri:a,scope:z,state:r,code_challenge:u,code_challenge_method:"S256",agent_id:n.agentId,tenant_id:o}),signal:AbortSignal.timeout(15e3)}),p=await h.json().catch(()=>null);if(!h.ok||typeof p?.redirect_uri!="string")throw new Error(`切换租户授权失败:${p?.error||`HTTP_${h.status}`}`);const l=new URL(p.redirect_uri),d=new URL(a);if(l.origin!==d.origin||l.pathname!==d.pathname||l.searchParams.getAll("state").length!==1||l.searchParams.get("state")!==r||l.searchParams.getAll("code").length!==1||!l.searchParams.get("code"))throw new Error("切换租户授权回调不符合预期");const i=await v(t.token,{grant_type:"authorization_code",client_id:m,code:l.searchParams.get("code"),redirect_uri:a,code_verifier:s}),w=P(i,n.agentId);if(w.tenantId.toLowerCase()!==String(o).toLowerCase())throw await C(e,w).catch(()=>{}),new Error("切换租户后令牌的租户与目标不一致");return w}c(W,"authorizeTenantSession");async function q(e,{agentId:n,tenant:o,switchAccount:t=!1,timeoutSeconds:r=300,browser:s=H,onAuthorized:u=c(async a=>({session:a,tenantName:a.tenantId}),"onAuthorized")}={}){const a=await $(e);if(!a.online)throw new Error("领谋线上授权会话接口尚未部署,请稍后重试");const h=T(),p=T(),l=b(E.createHash("sha256").update(p,"ascii").digest()),d=n||E.randomUUID().replace(/-/g,"");let i=null,w=null,k=!1,_=null,A=null;try{if(i=await O(a.online,{client_id:m,scope:z,state:h,code_challenge:l,code_challenge_method:"S256",agent_id:d,timeout_seconds:r}),!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(i?.session_id)||!/^[A-Za-z0-9_-]{43}$/.test(i?.client_secret)||!/^[0-9A-F]{4}-[0-9A-F]{4}$/.test(i?.verification_code))throw new Error("领谋线上授权会话响应不完整");const f=y(i.authorization_url,e.origin,"authorization_url");if(f.pathname!==a.authorization.pathname||f.searchParams.get("session_id")!==i.session_id)throw new Error("领谋线上授权页地址无效");o&&f.searchParams.set("tenant",o),t&&f.searchParams.set("prompt","login");const g=`${a.online.href}/${i.session_id}`;_=new AbortController,A=setTimeout(()=>_.abort(),r*1e3),A.unref();const L=await fetch(`${g}/events`,{headers:{Accept:"text/event-stream","X-Cli-Session":i.client_secret},signal:_.signal});process.stderr.write(`请在浏览器中完成领谋授权:${f.href}
|
|
7
|
+
请核对页面中的授权码:${i.verification_code}
|
|
8
|
+
`),await s(f.href),await R(L,h);const I=await O(`${g}/token`,{client_secret:i.client_secret,code_verifier:p});w=P(I,d);const U=await u(w);k=!0;let S=!1;for(let x=0;x<2&&!S;x++)try{S=(await O(`${g}/complete`,{client_secret:i.client_secret,success:!0},5e3))?.status==="completed"}catch{}if(!S)throw new Error("CLI 已保存授权,但网页确认通知失败;请执行 linkai status 核对登录状态");return U.session}catch(f){if(w&&!k&&await C(e,w).catch(()=>{}),i?.session_id&&i?.client_secret&&!k){const g=`${a.online.href}/${i.session_id}`;await O(`${g}/complete`,{client_secret:i.client_secret,success:!1}).catch(()=>{})}throw!k&&_?.signal.aborted?new Error("等待领谋 OAuth 授权超时,请重新执行 linkai login"):f}finally{A&&clearTimeout(A),_?.abort()}}c(q,"loginWithOauth");export{W as authorizeTenantSession,q as loginWithOauth,$ as oauthMetadata,H as openSystemBrowser,M as refreshOauthSession,C as revokeOauthSession};
|
package/clis/linkai/reuse.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
var $=Object.defineProperty;var l=(t,e)=>$(t,"name",{value:e,configurable:!0});import y from"node:fs";import w from"node:path";import q from"node:os";import U from"node:crypto";import*as u from"./core.js";const z=.75,I=/^zz-/i,x=l(t=>U.createHash("sha256").update(t).digest("hex"),"sha");function k(t){return String(t||"").replace(/\/\*[\s\S]*?\*\//g," ").replace(/--[^\n]*/g," ").split(/('(?:[^'\\]|\\.)*')/).map((r,a)=>a%2?r:r.replace(/`/g,"").toLowerCase()).join("").replace(/\s+/g," ").replace(/\s*([(),=<>!+\-*/;|])\s*/g,"$1").replace(/;+$/,"").trim()}l(k,"normalizeSql");function p(t){return x(k(t)).slice(0,32)}l(p,"sqlFingerprint");function D(t){return t.match(/'(?:[^'\\]|\\.)*'|[a-z_一-龥][\w一-龥.]*|\d+(?:\.\d+)?|[^\s\w]/g)||[]}l(D,"tokens");function T(t){const e=D(k(t)),s=new Set;for(let r=0;r<e.length-1;r++)s.add(x(`${e[r]} ${e[r+1]}`).slice(0,8));return[...s]}l(T,"sqlShingles");function E(t,e){const s=new Set(t),r=new Set(e);if(!s.size&&!r.size)return 1;let a=0;for(const o of s)r.has(o)&&a++;return a/(s.size+r.size-a)}l(E,"similarity");function K(t,e){const s=p(e);if(p(t)===s)return!0;try{return p(u.stripTrailingSemicolon(u.formatSql(t)))===s}catch{return!1}}l(K,"sameSql");const b=l(t=>[...new Set(u.sqlTables(u.stripTrailingSemicolon(String(t||""))).map(e=>e.replace(/^[\w]+\./,"").toLowerCase()))].sort(),"tableSet"),g=l((t,e)=>t.length===e.length&&t.every((s,r)=>s===e[r]),"sameSet");function j(t){const e=w.join(q.homedir(),".linkai","cache");return y.mkdirSync(e,{recursive:!0,mode:448}),w.join(e,`ds-index-${String(t).replace(/[^\w-]/g,"")}.json`)}l(j,"cacheFile");async function A(t,e,s){const r=new Array(t.length);let a=0;return await Promise.all(Array.from({length:Math.min(e,t.length)},async()=>{for(;a<t.length;){const o=a++;r[o]=await s(t[o],o)}})),r}l(A,"mapLimit");async function F({concurrency:t=12}={}){const e=await u.ensureSession(),s=j(e.tenantId);let r={};try{r=JSON.parse(y.readFileSync(s,"utf8"))||{}}catch{}const a=await u.dsList({withMeta:!0}),o=await A(a,t,async i=>{const m=r[i.dsId];if(m&&m.editTime===i.editTime)return{...m,name:i.name};let d;try{d=await u.dsDetail(i.dsId)}catch(n){if(n instanceof u.LoginRequiredError||n instanceof u.NetworkError)throw n;return{dsId:i.dsId,name:i.name,editTime:i.editTime,addUser:i.addUser||null,unreadable:!0}}const h=(d.tables?.[0]?.fieldColumns||[]).map(n=>n.aliasCnName||n.tableFieldName).filter(Boolean);return{dsId:i.dsId,name:i.name,editTime:i.editTime,addUser:i.addUser||null,hash:p(d.sql),tables:b(d.sql),shingles:T(d.sql),columns:h}}),c=Object.fromEntries(o.map(i=>[i.dsId,i]));try{y.writeFileSync(s,JSON.stringify(c),{mode:384})}catch{}return o}l(F,"datasetIndex");function N(t,e,{top:s=3}={}){const r=p(t),a=(()=>{try{return p(u.formatSql(t))}catch{return r}})(),o=b(t),c=T(t),i=e.filter(n=>!n.unreadable&&!I.test(n.name)),m=i.filter(n=>n.hash===r||n.hash===a).map(n=>({dsId:n.dsId,name:n.name,addUser:n.addUser,columns:n.columns})),d=new Set(m.map(n=>n.dsId)),h=i.filter(n=>!d.has(n.dsId)&&g(n.tables,o)).map(n=>({dsId:n.dsId,name:n.name,addUser:n.addUser,score:Math.min(.99,Math.round(E(c,n.shingles)*100)/100),columns:n.columns})).filter(n=>n.score>=z).sort((n,L)=>L.score-n.score).slice(0,s);return{tables:o,identical:m,similar:h}}l(N,"matchDataset");const S=l(t=>(Array.isArray(t)?t:String(t??"").split(",")).map(e=>String(e).trim()).filter(Boolean),"list"),f=l(t=>[...new Set(t)].sort(),"sorted");function R(t){const e=[];for(const s of t?.columns||[]){s.plain&&e.push(s.plain);for(const r of s.rows||[])for(const a of r)e.push(String(a).split(":")[0])}return f(e)}l(R,"tableColumns");function B(t){return t.type==="chart"?{kind:String(t.kind||"").toLowerCase(),dims:f(S(t.dim)),meas:f(S(t.meas)),series:f(S(t.series)),cols:null}:t.type==="meter"?{kind:"meter/list",dims:null,meas:null,series:null,cols:f(S(t.col))}:t.type==="table"&&t.spec&&typeof t.spec=="object"?{kind:"table/agtable",dims:null,meas:null,series:null,cols:R(t.spec)}:null}l(B,"cardSignatureFromSpec");function M(t){const e=typeof t.chartSetting=="string"?(()=>{try{return JSON.parse(t.chartSetting||"{}")}catch{return{}}})():t.chartSetting||{},s=`${e.parentId||t.chartParentType||""}/${e.id||t.chartType||""}`.toLowerCase(),r=t.indexBlockDataSettingDtos||[],a=l(o=>f(r.filter(c=>Number(c.argumentType)===o).map(c=>c.comment||c.title).filter(Boolean)),"byRole");return{kind:s,dims:a(2),meas:a(1),series:a(3),cols:f(r.map(o=>o.comment||o.title).filter(Boolean))}}l(M,"cardSignatureFromDetail");function O(t,e){return!t||!e||t.kind!==e.kind?!1:t.cols&&!t.dims?g(t.cols,e.cols):g(t.dims,e.dims)&&g(t.meas,e.meas)&&g(t.series,e.series)}l(O,"sameCard");async function H(t,e){const s=B(e);if(!s)return[];const r=(await u.dsRefs(t)).filter(o=>!I.test(o.name)),a=[];for(const o of r)try{const c=await u.cardDetail(o.cardId);O(s,M(c))&&a.push({cardId:o.cardId,name:o.name})}catch{}return a}l(H,"matchCards");const C=new Set(["suggest","auto","off"]);async function Q(t,e){const s=t.reuse??"suggest";if(!C.has(s))throw new Error(`reuse 只能是 ${[...C].join(" / ")}`);if(s==="off")return{mode:s,datasets:{},cards:{}};const r=await F(),a={};for(const c of t.datasets||[]){if(c.id||c.reuse===!1)continue;const i=N(e(c.sql),r),m=i.identical.length?s==="auto"?`复用 ${i.identical[0].dsId}`:"可复用(spec 加 reuse: auto 或把 id 写进 datasets)":i.similar.length?"有相似数据集,请比较后决定复用、在原数据集上调整,还是新建":"新建";a[c.key]={identical:i.identical,similar:i.similar,decision:m,reuseId:s==="auto"?i.identical[0]?.dsId??null:null}}const o={};for(const c of t.cards||[]){if(c.type==="existing"||c.reuse===!1)continue;const i=t.datasets?.find(h=>h.key===c.dataset),m=i?.id||a[c.dataset]?.reuseId||a[c.dataset]?.identical?.[0]?.dsId;if(!m)continue;const d=await H(m,c);d.length&&(o[c.key]={candidates:d,decision:s==="auto"&&(i?.id||a[c.dataset]?.reuseId)?`复用 ${d[0].cardId}`:"可复用已有卡片(type: existing + id)",reuseId:s==="auto"&&(i?.id||a[c.dataset]?.reuseId)?d[0].cardId:null})}return{mode:s,datasets:a,cards:o}}l(Q,"reviewSpec");export{C as REUSE_MODES,z as SIMILAR_THRESHOLD,M as cardSignatureFromDetail,B as cardSignatureFromSpec,F as datasetIndex,H as matchCards,N as matchDataset,k as normalizeSql,Q as reviewSpec,O as sameCard,K as sameSql,E as similarity,p as sqlFingerprint,T as sqlShingles};
|