@linkaicli/linkai-cli 1.0.17 → 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/GUIDE.md CHANGED
@@ -95,8 +95,7 @@ AI 会先给你看搭建计划(不会直接动手),你确认后才执行
95
95
 
96
96
  ## 升级
97
97
 
98
- **自动升级**:有新版本时,LinkAI-CLI 会在命令结束后于后台自动下载、校验并安装,下次运行即为新版本,并在结果里提示“已自动升级”;技能随之自动更新。不需要手动操作。
99
- 确实需要关闭时设置环境变量 `LINKAI_NO_AUTO_UPGRADE=1`。
98
+ **自动升级**:LinkAI-CLI 自动保持最新版本,不需要手动操作。发布了必须升级的版本时,下一条命令会先自动升级(约 10 秒)再执行;自动升级失败时命令会提示手动升级的方法。技能随之自动更新。
100
99
 
101
100
  手动升级(自动升级不可用时):
102
101
 
@@ -1,4 +1,4 @@
1
- @linkaicli/linkai-cli 1.0.17 安装包内含以下第三方软件(已打包进 dist/bundle)。
1
+ @linkaicli/linkai-cli 1.0.19 安装包内含以下第三方软件(已打包进 dist/bundle)。
2
2
 
3
3
  ── @colors/colors@1.5.0(MIT)
4
4
 
@@ -1 +1 @@
1
- import{ArgumentError}from"@linkaicli/linkai-cli/errors";function clamp(value,min,max){return Math.max(min,Math.min(value,max))}function clampInt(raw,fallback,min,max){const parsed=Number(raw);return Number.isFinite(parsed)?clamp(Math.floor(parsed),min,max):fallback}function normalizeNumericId(value,label,example){const normalized=String(value??"").trim();if(!/^\d+$/.test(normalized))throw new ArgumentError(`${label} must be a numeric ID`,`Pass a numeric ${label}, for example: ${example}`);return normalized}function requireNonEmptyQuery(value,label="query"){const normalized=String(value??"").trim();if(!normalized)throw new ArgumentError(`${label} cannot be empty`);return normalized}export{clamp,clampInt,normalizeNumericId,requireNonEmptyQuery};
1
+ var u=Object.defineProperty;var e=(t,r)=>u(t,"name",{value:r,configurable:!0});import{ArgumentError as m}from"@linkaicli/linkai-cli/errors";function a(t,r,n){return Math.max(r,Math.min(t,n))}e(a,"clamp");function f(t,r,n,o){const i=Number(t);return Number.isFinite(i)?a(Math.floor(i),n,o):r}e(f,"clampInt");function s(t,r,n){const o=String(t??"").trim();if(!/^\d+$/.test(o))throw new m(`${r} must be a numeric ID`,`Pass a numeric ${r}, for example: ${n}`);return o}e(s,"normalizeNumericId");function x(t,r="query"){const n=String(t??"").trim();if(!n)throw new m(`${r} cannot be empty`);return n}e(x,"requireNonEmptyQuery");export{a as clamp,f as clampInt,s as normalizeNumericId,x as requireNonEmptyQuery};
@@ -1 +1 @@
1
- import{AuthRequiredError,CommandExecutionError,TimeoutError,getErrorMessage}from"@linkaicli/linkai-cli/errors";import{cli,Strategy}from"@linkaicli/linkai-cli/registry";const DEFAULT_TIMEOUT_SECONDS=300,POLL_INTERVAL_MS=2e3;function normalizeIdentity(site,identity){if(!identity||typeof identity!="object"||Array.isArray(identity)||Object.keys(identity).length===0||"logged_in"in identity&&typeof identity.logged_in!="boolean")throw new CommandExecutionError(`${site} identity probe returned an invalid account result`);if(identity.logged_in===!1)throw new AuthRequiredError(site,"Identity probe reports a logged-out session");return{...identity,logged_in:!0,site}}function isAuthRequired(error){return error instanceof AuthRequiredError}async function tryProbe(config,page,phase){const probe=phase==="poll"&&config.poll?config.poll:config.verify;return normalizeIdentity(config.site,await probe(page,{phase}))}function authHint(config){return`Run \`linkai ${config.site} login\` to open the login page, then retry.`}function commandColumns(config){return["logged_in","site",...config.columns??["id","username","name"]]}function normalizeQuickCheck(result){return typeof result=="boolean"?{logged_in:result}:result&&typeof result=="object"&&!Array.isArray(result)?{...result,logged_in:typeof result.logged_in=="boolean"?result.logged_in:null}:{logged_in:null}}function normalizeRefreshResult(result){return result&&typeof result=="object"&&!Array.isArray(result)?result:{touched:!0}}function registerSiteAuthCommands(config){if(!config?.site||!config?.domain||!config?.loginUrl||typeof config.verify!="function")throw new Error("registerSiteAuthCommands requires site, domain, loginUrl, and verify(page)");config.registerWhoami!==!1&&cli({site:config.site,name:"whoami",access:"read",description:config.whoamiDescription??`Show the current logged-in ${config.site} account`,domain:config.domain,strategy:Strategy.COOKIE,browser:!0,navigateBefore:!1,siteSession:"persistent",args:[],columns:commandColumns(config),authStatus:{...typeof config.quickCheck=="function"?{quickCheck:async page=>normalizeQuickCheck(await config.quickCheck(page))}:{},...typeof config.refresh=="function"?{refresh:async(page,kwargs)=>normalizeRefreshResult(await config.refresh(page,kwargs))}:{}},func:async page=>tryProbe(config,page,"identity")}),cli({site:config.site,name:"login",access:"write",description:config.loginDescription??`Open ${config.site} login and wait until the browser session is authenticated`,domain:config.domain,strategy:Strategy.COOKIE,browser:!0,navigateBefore:!1,defaultWindowMode:"foreground",siteSession:"persistent",args:[{name:"timeout",type:"int",default:DEFAULT_TIMEOUT_SECONDS,help:"Maximum seconds to wait for the user to finish login"}],columns:["status",...commandColumns(config)],func:async(page,kwargs)=>{try{return{status:"already_logged_in",...await tryProbe(config,page,"identity")}}catch(error){if(!isAuthRequired(error))throw error}await page.goto(config.loginUrl);const timeoutSeconds=Number(kwargs.timeout??DEFAULT_TIMEOUT_SECONDS),deadline=Date.now()+timeoutSeconds*1e3;let lastAuthMessage="";for(;Date.now()<deadline;){await page.wait(Math.min(POLL_INTERVAL_MS/1e3,Math.max(.2,(deadline-Date.now())/1e3)));try{return{status:"login_complete",...await tryProbe(config,page,"poll")}}catch(error){if(!isAuthRequired(error))throw error;lastAuthMessage=getErrorMessage(error)}}throw new TimeoutError(`${config.site} login`,timeoutSeconds,lastAuthMessage?`${authHint(config)} Last auth check: ${lastAuthMessage}`:authHint(config))}})}export{registerSiteAuthCommands};
1
+ var p=Object.defineProperty;var r=(e,t)=>p(e,"name",{value:t,configurable:!0});import{AuthRequiredError as l,CommandExecutionError as f,TimeoutError as g,getErrorMessage as _}from"@linkaicli/linkai-cli/errors";import{cli as d,Strategy as m}from"@linkaicli/linkai-cli/registry";const h=300,b=2e3;function A(e,t){if(!t||typeof t!="object"||Array.isArray(t)||Object.keys(t).length===0||"logged_in"in t&&typeof t.logged_in!="boolean")throw new f(`${e} identity probe returned an invalid account result`);if(t.logged_in===!1)throw new l(e,"Identity probe reports a logged-out session");return{...t,logged_in:!0,site:e}}r(A,"normalizeIdentity");function c(e){return e instanceof l}r(c,"isAuthRequired");async function s(e,t,o){const i=o==="poll"&&e.poll?e.poll:e.verify;return A(e.site,await i(t,{phase:o}))}r(s,"tryProbe");function y(e){return`Run \`linkai ${e.site} login\` to open the login page, then retry.`}r(y,"authHint");function w(e){return["logged_in","site",...e.columns??["id","username","name"]]}r(w,"commandColumns");function C(e){return typeof e=="boolean"?{logged_in:e}:e&&typeof e=="object"&&!Array.isArray(e)?{...e,logged_in:typeof e.logged_in=="boolean"?e.logged_in:null}:{logged_in:null}}r(C,"normalizeQuickCheck");function E(e){return e&&typeof e=="object"&&!Array.isArray(e)?e:{touched:!0}}r(E,"normalizeRefreshResult");function M(e){if(!e?.site||!e?.domain||!e?.loginUrl||typeof e.verify!="function")throw new Error("registerSiteAuthCommands requires site, domain, loginUrl, and verify(page)");e.registerWhoami!==!1&&d({site:e.site,name:"whoami",access:"read",description:e.whoamiDescription??`Show the current logged-in ${e.site} account`,domain:e.domain,strategy:m.COOKIE,browser:!0,navigateBefore:!1,siteSession:"persistent",args:[],columns:w(e),authStatus:{...typeof e.quickCheck=="function"?{quickCheck:r(async t=>C(await e.quickCheck(t)),"quickCheck")}:{},...typeof e.refresh=="function"?{refresh:r(async(t,o)=>E(await e.refresh(t,o)),"refresh")}:{}},func:r(async t=>s(e,t,"identity"),"func")}),d({site:e.site,name:"login",access:"write",description:e.loginDescription??`Open ${e.site} login and wait until the browser session is authenticated`,domain:e.domain,strategy:m.COOKIE,browser:!0,navigateBefore:!1,defaultWindowMode:"foreground",siteSession:"persistent",args:[{name:"timeout",type:"int",default:h,help:"Maximum seconds to wait for the user to finish login"}],columns:["status",...w(e)],func:r(async(t,o)=>{try{return{status:"already_logged_in",...await s(e,t,"identity")}}catch(n){if(!c(n))throw n}await t.goto(e.loginUrl);const i=Number(o.timeout??h),u=Date.now()+i*1e3;let a="";for(;Date.now()<u;){await t.wait(Math.min(b/1e3,Math.max(.2,(u-Date.now())/1e3)));try{return{status:"login_complete",...await s(e,t,"poll")}}catch(n){if(!c(n))throw n;a=_(n)}}throw new g(`${e.site} login`,i,a?`${y(e)} Last auth check: ${a}`:y(e))},"func")})}r(M,"registerSiteAuthCommands");export{M as registerSiteAuthCommands};
@@ -1 +1 @@
1
- import fs from"node:fs";import path from"node:path";import{parseYaml}from"@linkaicli/linkai-cli/utils";import*as core from"./core.js";import*as oplog from"@linkaicli/linkai-cli/oplog";import*as reuse from"./reuse.js";import{containsFullSql,knowledgeAuthoringFromBuild,publishPlanKnowledge}from"./deliverable.js";const CARD_TYPES=new Set(["chart","meter","table","gantt","existing"]),TRANSPARENT_TYPES=new Set(["meter"]),DEFAULT_LAYOUT={contentX:2,contentWidth:20,pageBackgroundColor:"#F4F3EE",titleHeight:2},list=v=>(Array.isArray(v)?v:String(v??"").split(",")).map(s=>String(s).trim()).filter(Boolean);function currentTenantLabel(){try{const s=core.readSession();if(s){const t=core.spaceOf(s);return t.tenantName||t.tenantId}}catch{}return"(未登录:执行前请用户执行 linkai login 并在浏览器完成领谋授权)"}function loadSpec(file){const abs=path.resolve(String(file)),text=fs.readFileSync(abs,"utf8"),spec=/\.json$/i.test(abs)?JSON.parse(text):parseYaml(text),baseDir=path.dirname(abs);for(const c of spec?.cards||[])(c.type==="table"||c.type==="gantt")&&typeof c.spec=="string"&&(c.specFile=c.spec,c.spec=JSON.parse(fs.readFileSync(path.resolve(baseDir,c.spec),"utf8")));return{spec,specFile:abs,baseDir}}function validateSpec(spec){const errors=[],need=(cond,msg)=>{cond||errors.push(msg)};if(need(spec&&typeof spec=="object","spec 必须是对象"),errors.length)return errors;need(spec.version===1,"version 必须为 1"),need(spec.operation,"operation(create 或 update)必填;用户未说明时先主动询问"),need(spec.operation==="create","linkai build 当前用于新建方案,operation 必须为 create;修改原方案请定位 planId 后按知识库规则更新并执行 linkai plan-knowledge --mode update"),need(typeof spec.name=="string"&&spec.name.trim(),"name(方案名)必填"),need(spec.knowledge?.summary,"knowledge.summary(方案内容)必填"),need(spec.knowledge?.problem,"knowledge.problem(解决的问题)必填"),need(Array.isArray(spec.cards)&&spec.cards.length>0,"cards 至少需要一张业务卡片"),need(spec.reuse===void 0||reuse.REUSE_MODES.has(spec.reuse),`reuse 只能是 ${[...reuse.REUSE_MODES].join(" | ")}(默认 suggest)`);const dsKeys=new Set;for(const[i,d]of(spec.datasets||[]).entries())need(d.key,`datasets[${i}].key 必填`),need(!dsKeys.has(d.key),`datasets key 重复:${d.key}`),dsKeys.add(d.key),need(d.id||d.name&&d.sql,`datasets[${d.key}] 需要 id(复用已有数据集)或 name + sql`);const cardKeys=new Set;for(const[i,c]of(spec.cards||[]).entries()){if(need(c.key,`cards[${i}].key 必填`),need(!cardKeys.has(c.key),`cards key 重复:${c.key}`),cardKeys.add(c.key),need(CARD_TYPES.has(c.type),`cards[${c.key}].type 必须是 ${[...CARD_TYPES].join(" | ")}`),c.type==="existing"){need(c.id,`cards[${c.key}] type=existing 需要 id`),need(c.knowledge?.content,`cards[${c.key}].knowledge.content(卡片内容)必填`),need(c.knowledge?.source,`cards[${c.key}].knowledge.source(数据来源)必填`),need(c.knowledge?.grain,`cards[${c.key}].knowledge.grain(数据粒度)必填`),need(c.knowledge?.dataLogic,`cards[${c.key}].knowledge.dataLogic(取数逻辑)必填`);continue}need(dsKeys.has(c.dataset),`cards[${c.key}].dataset 引用了不存在的数据集 key:${c.dataset}`),c.type==="chart"&&(need(c.name,`cards[${c.key}].name 必填`),need(c.kind,`cards[${c.key}].kind 必填(如 column/stacked,见 linkai kinds)`),need(list(c.meas).length,`cards[${c.key}].meas 至少一个数值列`)),c.type==="meter"&&(need(c.name,`cards[${c.key}].name 必填`),need(c.col,`cards[${c.key}].col 必填`)),(c.type==="table"||c.type==="gantt")&&(need(c.spec&&typeof c.spec=="object",`cards[${c.key}].spec 必填(内联对象或 .json 路径)`),need(c.spec?.name,`cards[${c.key}].spec.name 必填`)),need(c.knowledge?.content,`cards[${c.key}].knowledge.content(卡片内容)必填`),need(c.knowledge?.source,`cards[${c.key}].knowledge.source(数据来源)必填`),need(c.knowledge?.grain,`cards[${c.key}].knowledge.grain(数据粒度)必填`),need(c.knowledge?.dataLogic,`cards[${c.key}].knowledge.dataLogic(取数逻辑)必填`)}const used=new Set;for(const[si,s]of(spec.sections||[]).entries())for(const[ri,r]of(s.rows||[]).entries()){need(Number(r.height)>0,`sections[${si}].rows[${ri}].height 必须为正整数(格)`);for(const p of r.cards||[])need(cardKeys.has(p.card),`sections[${si}].rows[${ri}] 引用了不存在的卡片 key:${p.card}`),need(!used.has(p.card),`卡片 ${p.card} 在布局中出现多次`),used.add(p.card)}for(const r of spec.publish?.reports||[])need(cardKeys.has(r.card),`publish.reports 引用了不存在的卡片 key:${r.card}`);return containsFullSql({knowledge:spec.knowledge,cards:(spec.cards||[]).map(c=>c.knowledge)})&&errors.push("方案知识库不得包含完整 SQL;cards[].knowledge.dataLogic 必须使用自然语言描述"),errors}function formalNames(spec){return[spec.name,...(spec.datasets||[]).filter(d=>!d.id).map(d=>d.name),...(spec.cards||[]).filter(c=>c.type!=="existing").map(c=>c.name||c.spec?.name),...(spec.publish?.reports||[]).map(r=>r.name)].filter(n=>n&&!String(n).startsWith("zz-"))}function planLayout(spec){const L={...DEFAULT_LAYOUT,...spec.layout||{}},right=L.contentX+L.contentWidth,blocks=[],errors=[];let y=0,mobileY=0;const push=b=>{blocks.push({...b,mobileX:0,mobileY,mobileWidth:4,mobileHeight:b.height}),mobileY+=b.height};for(const[si,s]of(spec.sections||[]).entries()){s.title&&(push({kind:"title",key:`title-${si}`,title:s.title,x:L.contentX,y,width:L.contentWidth,height:L.titleHeight}),y+=L.titleHeight);for(const[ri,r]of(s.rows||[]).entries()){const cards=r.cards||[],fixed=cards.reduce((n,p)=>n+(Number(p.width)||0),0),auto=cards.filter(p=>!p.width).length,autoW=auto?Math.floor((L.contentWidth-fixed)/auto):0;let x=L.contentX;for(const p of cards){const width=Number(p.width)||autoW;width<=0&&errors.push(`sections[${si}].rows[${ri}] 卡片 ${p.card} 宽度为 0(行内固定宽度已占满)`),push({kind:"card",key:p.card,x,y,width,height:Number(r.height)}),x+=width}x>right&&errors.push(`sections[${si}].rows[${ri}] 总宽 ${x-L.contentX} 超出内容区 ${L.contentWidth} 格`),y+=Number(r.height)}}return{layout:L,blocks,errors:[...errors,...overlaps(blocks,"pc"),...overlaps(blocks,"mobile")]}}function overlaps(blocks,mode){const rect=b=>mode==="pc"?[b.x,b.y,b.width,b.height]:[b.mobileX,b.mobileY,b.mobileWidth,b.mobileHeight],out=[];for(let i=0;i<blocks.length;i++)for(let j=i+1;j<blocks.length;j++){const[ax,ay,aw,ah]=rect(blocks[i]),[bx,by,bw,bh]=rect(blocks[j]);ax<bx+bw&&bx<ax+aw&&ay<by+bh&&by<ay+ah&&out.push(`${mode} 布局重叠:${blocks[i].key} 与 ${blocks[j].key}`)}return out}function titleContent(t){return JSON.stringify({type:"title-template",version:1,template:"section",config:{serial:String(t.serial??""),title:String(t.title??""),subtitle:String(t.subtitle??""),description:"",showDecorLine:!1}})}function describePlan(spec){const steps=[];for(const d of spec.datasets||[])steps.push(d.id?`复用数据集 ${d.key} = ${d.id}`:`建数据集 ${d.key}「${d.name}」← ${d.sql}`);for(const c of spec.cards||[]){if(c.type==="existing"){steps.push(`复用卡片 ${c.key} = ${c.id}`);continue}steps.push(`建卡片 ${c.key}(${c.type}${c.kind?` ${c.kind}`:""})「${c.name||c.spec?.name||""}」${c.sort?`,排序 ${c.sort}`:""}`)}steps.push(`建方案「${spec.name}」并按布局放入 ${(spec.sections||[]).length} 个分区`),steps.push("写页面样式(暖色背景、标题/指标卡卡位透明、隐藏外层标题)"),spec.filters&&steps.push(`写方案筛选器模板 ← ${spec.filters}`);for(const r of spec.publish?.reports||[])steps.push(`发布报表「${r.name}」← 卡片 ${r.card}`);return steps.push("验收:逐卡 query + 方案回读(卡片数、越界、重叠、背景色)"),steps.push("生成并保存版本化方案知识库(完整呈现给用户,不含 SQL)"),steps}function fillPlaceholders(text,state,dataSourceId=""){const pick=(map,kind)=>(_,k)=>{if(!map[k])throw new Error(`filters 引用了不存在或未创建的${kind} key:${k}`);return map[k]};return text.replaceAll("{{plan}}",state.planId).replaceAll("{{dataSource}}",dataSourceId).replace(/\{\{pageCard:([^}]+)\}\}/g,pick(state.blocks,"卡片")).replace(/\{\{dataset:([^}]+)\}\}/g,pick(state.datasets,"数据集"))}function readState(file){try{return JSON.parse(fs.readFileSync(file,"utf8"))}catch{return{datasets:{},cards:{},sorted:{},blocks:{},reports:{}}}}function writeState(file,state){fs.writeFileSync(file,JSON.stringify(state,null,2))}const resolveFile=(baseDir,p)=>path.resolve(baseDir,String(p));async function reviewReuse(spec,baseDir){try{return await reuse.reviewSpec(spec,file=>fs.readFileSync(resolveFile(baseDir,file),"utf8"))}catch(e){return{mode:spec.reuse??"suggest",skipped:core.LoginRequiredError&&e instanceof core.LoginRequiredError?"未登录,无法查重":`查重失败:${String(e.message).slice(0,200)}`,datasets:{},cards:{}}}}function reuseHint(review){const ds=Object.entries(review.datasets||{}).filter(([,v])=>v.identical.length||v.similar.length),cards=Object.entries(review.cards||{});return!ds.length&&!cards.length?null:`发现可复用对象:数据集 ${ds.map(([k])=>k).join("、")||"无"},卡片 ${cards.map(([k])=>k).join("、")||"无"}。`+(review.mode==="auto"?"完全一样的将直接复用;相似的请比较后决定。":"完全一样的可在 spec 加 reuse: auto 直接复用;相似的请比较 columns 后决定复用、在原数据集上调整,还是新建。")}async function runBuild({spec,specFile,baseDir},{execute=!1,confirm=!1,style=!0,beforeWrite}={}){const errors=validateSpec(spec),plan=errors.length?{blocks:[],errors:[]}:planLayout(spec);errors.push(...plan.errors);const formal=errors.length?[]:formalNames(spec);if(errors.length)return oplog.emit("build.stage",{stage:"validate",ok:!1,errors:errors.length}),{ok:!1,stage:"validate",errors};const summary={name:spec.name,tenant:currentTenantLabel(),steps:describePlan(spec),layout:plan.blocks.map(b=>({key:b.key,x:b.x,y:b.y,w:b.width,h:b.height}))};if(!execute){const review2=await reviewReuse(spec,baseDir),hint=reuseHint(review2);return{ok:!0,mode:"dry-run",...summary,formalNames:formal,reuse:review2,next:[hint,formal.length?"含正式名:取得用户授权后加 --execute --confirm":"确认无误后加 --execute"].filter(Boolean).join(" ")}}if(formal.length&&!confirm)return{ok:!1,stage:"authorize",errors:[`以下为正式名(非 zz- 前缀),按 SOP 需要用户明确授权后加 --confirm:${formal.join("、")}`]};beforeWrite&&await beforeWrite();const review=spec.reuse==="auto"?await reviewReuse(spec,baseDir):{mode:spec.reuse??"suggest",datasets:{},cards:{}},stateFile=`${specFile}.state.json`,state=readState(stateFile),save=()=>writeState(stateFile,state),log=[];let stepAt=Date.now();log.push=(...lines)=>{for(const line of lines){const[stage,rest=""]=String(line).split(/\s+(.*)/s);oplog.emit("build.stage",{stage,detail:rest.slice(0,200),ok:!0,ms:Date.now()-stepAt}),stepAt=Date.now()}return Array.prototype.push.apply(log,lines)},oplog.emit("build.stage",{stage:"start",ok:!0,resumed:!!(state.planId||Object.keys(state.datasets||{}).length),datasets:(spec.datasets||[]).length,cards:(spec.cards||[]).length,reports:(spec.publish?.reports||[]).length});for(const d of spec.datasets||[]){if(state.datasets[d.key])continue;const reused=!d.id&&review.datasets[d.key]?.reuseId;state.datasets[d.key]=d.id||reused||await core.dsSave({name:d.name,sql:fs.readFileSync(resolveFile(baseDir,d.sql),"utf8")}),save(),log.push(`dataset ${d.key} → ${state.datasets[d.key]}${reused?"(复用已有数据集)":""}`)}for(const c of spec.cards||[]){if(!state.cards[c.key]){const dsId=state.datasets[c.dataset];let r;c.type==="existing"?r={cardId:c.id}:review.cards[c.key]?.reuseId?r={cardId:review.cards[c.key].reuseId,reused:!0}:c.type==="chart"?r=await core.cardChart({dsId,name:c.name,kind:c.kind,dim:list(c.dim).join(","),meas:list(c.meas).join(","),series:c.series}):c.type==="meter"?r=await core.cardMeter({dsId,name:c.name,col:c.col,subtitle:c.subtitle,color:c.color||void 0}):c.type==="table"?r=await core.cardTable({dsId,spec:c.spec}):r=await core.cardGantt({dsId,spec:c.spec}),state.cards[c.key]=r.cardId,save(),log.push(`card ${c.key} → ${r.cardId}${r.reused?"(复用已有卡片)":""}`)}const sharedCard=review.cards[c.key]?.reuseId&&state.cards[c.key]===review.cards[c.key].reuseId;if(c.sort&&!state.sorted[c.key]&&!sharedCard){const r=await core.cardSort(state.cards[c.key],String(c.sort));state.sorted[c.key]=r.sqlHasOrderBy,save(),log.push(`sort ${c.key} → sqlHasOrderBy=${r.sqlHasOrderBy}`)}}state.planId||(state.planId=await core.post("/page-setting/new-create-empty-plan",{name:spec.name,isPrivate:!1,pageType:0}),save(),log.push(`plan → ${state.planId}`));for(const b of plan.blocks){if(state.blocks[b.key])continue;const pos={x:b.x,y:b.y,width:b.width,height:b.height,mobileX:b.mobileX,mobileY:b.mobileY,mobileWidth:b.mobileWidth,mobileHeight:b.mobileHeight},body=b.kind==="title"?{...pos,sourceType:0,cardType:5,contentText:titleContent(b.title),bindFilterIds:[],userPageSettingId:state.planId}:{...pos,indexBlockId:state.cards[b.key],sourceType:0,cardType:1,bindFilterIds:[],userPageSettingId:state.planId};state.blocks[b.key]=(await core.post("/page-setting/new-page-card",body)).id,save()}if(log.push(`placed ${plan.blocks.length} blocks`),style&&!state.styled&&(state.styled=await applyStyle(spec,plan,state),save(),log.push(`style → ${JSON.stringify(state.styled)}`)),spec.filters&&!state.filtered){const text=fillPlaceholders(fs.readFileSync(resolveFile(baseDir,spec.filters),"utf8"),state,(await core.ctx()).dataSourceId),body=JSON.parse(text);state.filterDatasetIds=[...new Set((body.filters||[]).map(f=>f.indexBlockDataId).filter(id=>id&&id!==core.Z))],state.filtered=await core.planFilter(state.planId,body),save(),log.push(`filters → ${state.filtered.filters?.length??0}`)}for(const r of spec.publish?.reports||[])state.reports[r.card]||(await core.post("/index-block/operate-report",{indexBlockId:state.cards[r.card],name:r.name,tag:"",isPrivate:!1}),state.reports[r.card]=r.name,save(),log.push(`report ${r.name}`));const reusedCards=new Set(Object.entries(review.cards||{}).filter(([k,v])=>v.reuseId&&state.cards[k]===v.reuseId).map(([k])=>k)),verify=await verifyBuild(spec,plan,state,reusedCards);return oplog.emit("build.stage",{stage:"verify",ok:!!verify.ok,checks:Array.isArray(verify.checks)?verify.checks.length:void 0,failed:Array.isArray(verify.checks)?verify.checks.filter(c=>c&&c.ok===!1).length:void 0}),verify.ok?{ok:!0,mode:"execute",...await publishPlanKnowledge({planId:state.planId,operation:"create",authoring:knowledgeAuthoringFromBuild(spec,state)}),verify,log,stateFile,...review.mode==="auto"?{reuse:review}:{}}:{ok:!1,mode:"execute",verify,log,stateFile}}async function applyStyle(spec,plan,state){const detail=await core.planDetail(state.planId,!0),cards=detail.pageCards||[],expected=new Set(Object.values(state.blocks)),got=new Set(cards.map(c=>c.id));if(![...expected].every(id=>got.has(id)))return{skipped:"方案回读缺卡,未写样式(避免 update-page-card 移除卡片)"};if(!detail.name)return{skipped:"方案详情缺 name 字段,未写样式(避免覆盖方案配置)"};const keyOf=Object.fromEntries(Object.entries(state.blocks).map(([k,id])=>[id,k])),typeOf=Object.fromEntries((spec.cards||[]).map(c=>[c.key,c.type])),hasTitles=plan.blocks.some(b=>b.kind==="title"),parse=s=>{try{return typeof s=="string"?JSON.parse(s||"{}"):s||{}}catch{return{}}},items=cards.map(c=>{const key=keyOf[c.id],ds=parse(c.dataScreenSetting);if(key){const isTitle=key.startsWith("title-"),transparent=isTitle||TRANSPARENT_TYPES.has(typeOf[key]);Object.assign(ds,{useCardConfig:!0},transparent?{backgroundColor:"transparent"}:{},isTitle||hasTitles?{cardTitle:{...ds.cardTitle||{},show:!1}}:{})}return{id:c.id,x:c.x,y:c.y,width:c.width,height:c.height,mobileX:c.mobileX,mobileY:c.mobileY,mobileWidth:c.mobileWidth,mobileHeight:c.mobileHeight,zIndex:c.zIndex??0,dataScreenSetting:JSON.stringify(ds),...c.dataScreenChartSetting!==void 0?{dataScreenChartSetting:c.dataScreenChartSetting}:{}}}),page={...parse(detail.dataScreenSetting),pageBackgroundColor:plan.layout.pageBackgroundColor};return await core.post("/page-setting/update-page-card",{list:items,userPageSettingId:state.planId,dataScreenSetting:JSON.stringify(page),name:detail.name,tag:detail.tag||"",remark:detail.remark||"",parentId:detail.parentId,isPrivate:!!detail.isPrivate}),{cards:items.length,pageBackgroundColor:page.pageBackgroundColor}}async function verifyBuild(spec,plan,state,reusedCards=new Set){const checks=[];for(const c of spec.cards||[]){const q=await core.queryCard({cardId:state.cards[c.key],pageSize:1});checks.push({check:`query ${c.key}`,ok:q.code===200,detail:q.code===200?`total=${q.total}`:q.message})}for(const c of(spec.cards||[]).filter(x=>x.type==="table"&&x.spec?.fitTableWidth!==!1)){const d=await core.cardDetail(state.cards[c.key]),fit=(typeof d.chartSetting=="string"?(()=>{try{return JSON.parse(d.chartSetting||"{}")}catch{return{}}})():d.chartSetting||{}).config?.fitTableWidth===!0,shared=reusedCards.has(c.key);checks.push({check:`AG 自适应宽度 ${c.key}`,ok:fit||shared,detail:fit?"":shared?"复用的共享表格未开启,未修改":"fitTableWidth 未开启"})}const detail=await core.planDetail(state.planId,!0),cards=detail.pageCards||[],ids=new Set(cards.map(c=>c.id));checks.push({check:"方案卡片齐全",ok:Object.values(state.blocks).every(id=>ids.has(id)),detail:`${cards.length} 张`});const L=plan.layout,outOfBounds=cards.filter(c=>c.x<L.contentX||c.x+c.width>L.contentX+L.contentWidth);checks.push({check:`PC 卡位在 [${L.contentX},${L.contentX+L.contentWidth}] 内`,ok:!outOfBounds.length,detail:outOfBounds.map(c=>c.id).join(",")});const rects=cards.map(c=>({key:c.id,x:c.x,y:c.y,width:c.width,height:c.height,mobileX:c.mobileX,mobileY:c.mobileY,mobileWidth:c.mobileWidth,mobileHeight:c.mobileHeight})),ov=[...overlaps(rects,"pc"),...overlaps(rects,"mobile")];if(checks.push({check:"PC/手机无重叠",ok:!ov.length,detail:ov.join("; ")}),state.styled&&!state.styled.skipped){let page={};try{page=JSON.parse(detail.dataScreenSetting||"{}")}catch{}checks.push({check:"页面背景色",ok:page.pageBackgroundColor===L.pageBackgroundColor,detail:page.pageBackgroundColor||"(未设置)"})}for(const[k,v]of Object.entries(state.sorted))checks.push({check:`排序生效 ${k}`,ok:v===!0,detail:""});return{ok:checks.every(c=>c.ok),checks}}export{describePlan,fillPlaceholders,formalNames,loadSpec,planLayout,runBuild,titleContent,validateSpec,verifyBuild};
1
+ var F=Object.defineProperty;var g=(t,s)=>F(t,"name",{value:s,configurable:!0});import O from"node:fs";import j from"node:path";import{parseYaml as E}from"@linkaicli/linkai-cli/utils";import*as f from"./core.js";import*as v from"@linkaicli/linkai-cli/oplog";import*as B from"./reuse.js";import{containsFullSql as X,knowledgeAuthoringFromBuild as L,publishPlanKnowledge as P}from"./deliverable.js";const W=new Set(["chart","meter","table","gantt","existing"]),q=new Set(["meter"]),H={contentX:2,contentWidth:20,pageBackgroundColor:"#F4F3EE",titleHeight:2},N=g(t=>(Array.isArray(t)?t:String(t??"").split(",")).map(s=>String(s).trim()).filter(Boolean),"list");function J(){try{const t=f.readSession();if(t){const s=f.spaceOf(t);return s.tenantName||s.tenantId}}catch{}return"(未登录:执行前请用户执行 linkai login 并在浏览器完成领谋授权)"}g(J,"currentTenantLabel");function se(t){const s=j.resolve(String(t)),e=O.readFileSync(s,"utf8"),d=/\.json$/i.test(s)?JSON.parse(e):E(e),i=j.dirname(s);for(const l of d?.cards||[])(l.type==="table"||l.type==="gantt")&&typeof l.spec=="string"&&(l.specFile=l.spec,l.spec=JSON.parse(O.readFileSync(j.resolve(i,l.spec),"utf8")));return{spec:d,specFile:s,baseDir:i}}g(se,"loadSpec");function Y(t){const s=[],e=g((c,n)=>{c||s.push(n)},"need");if(e(t&&typeof t=="object","spec 必须是对象"),s.length)return s;e(t.version===1,"version 必须为 1"),e(t.operation,"operation(create 或 update)必填;用户未说明时先主动询问"),e(t.operation==="create","linkai build 当前用于新建方案,operation 必须为 create;修改原方案请定位 planId 后按知识库规则更新并执行 linkai plan-knowledge --mode update"),e(typeof t.name=="string"&&t.name.trim(),"name(方案名)必填"),e(t.knowledge?.summary,"knowledge.summary(方案内容)必填"),e(t.knowledge?.problem,"knowledge.problem(解决的问题)必填"),e(Array.isArray(t.cards)&&t.cards.length>0,"cards 至少需要一张业务卡片"),e(t.reuse===void 0||B.REUSE_MODES.has(t.reuse),`reuse 只能是 ${[...B.REUSE_MODES].join(" | ")}(默认 suggest)`);const d=new Set;for(const[c,n]of(t.datasets||[]).entries())e(n.key,`datasets[${c}].key 必填`),e(!d.has(n.key),`datasets key 重复:${n.key}`),d.add(n.key),e(n.id||n.name&&n.sql,`datasets[${n.key}] 需要 id(复用已有数据集)或 name + sql`);const i=new Set;for(const[c,n]of(t.cards||[]).entries()){if(e(n.key,`cards[${c}].key 必填`),e(!i.has(n.key),`cards key 重复:${n.key}`),i.add(n.key),e(W.has(n.type),`cards[${n.key}].type 必须是 ${[...W].join(" | ")}`),n.type==="existing"){e(n.id,`cards[${n.key}] type=existing 需要 id`),e(n.knowledge?.content,`cards[${n.key}].knowledge.content(卡片内容)必填`),e(n.knowledge?.source,`cards[${n.key}].knowledge.source(数据来源)必填`),e(n.knowledge?.grain,`cards[${n.key}].knowledge.grain(数据粒度)必填`),e(n.knowledge?.dataLogic,`cards[${n.key}].knowledge.dataLogic(取数逻辑)必填`);continue}e(d.has(n.dataset),`cards[${n.key}].dataset 引用了不存在的数据集 key:${n.dataset}`),n.type==="chart"&&(e(n.name,`cards[${n.key}].name 必填`),e(n.kind,`cards[${n.key}].kind 必填(如 column/stacked,见 linkai kinds)`),e(N(n.meas).length,`cards[${n.key}].meas 至少一个数值列`)),n.type==="meter"&&(e(n.name,`cards[${n.key}].name 必填`),e(n.col,`cards[${n.key}].col 必填`)),(n.type==="table"||n.type==="gantt")&&(e(n.spec&&typeof n.spec=="object",`cards[${n.key}].spec 必填(内联对象或 .json 路径)`),e(n.spec?.name,`cards[${n.key}].spec.name 必填`)),e(n.knowledge?.content,`cards[${n.key}].knowledge.content(卡片内容)必填`),e(n.knowledge?.source,`cards[${n.key}].knowledge.source(数据来源)必填`),e(n.knowledge?.grain,`cards[${n.key}].knowledge.grain(数据粒度)必填`),e(n.knowledge?.dataLogic,`cards[${n.key}].knowledge.dataLogic(取数逻辑)必填`)}const l=new Set;for(const[c,n]of(t.sections||[]).entries())for(const[u,k]of(n.rows||[]).entries()){e(Number(k.height)>0,`sections[${c}].rows[${u}].height 必须为正整数(格)`);for(const y of k.cards||[])e(i.has(y.card),`sections[${c}].rows[${u}] 引用了不存在的卡片 key:${y.card}`),e(!l.has(y.card),`卡片 ${y.card} 在布局中出现多次`),l.add(y.card)}for(const c of t.publish?.reports||[])e(i.has(c.card),`publish.reports 引用了不存在的卡片 key:${c.card}`);return X({knowledge:t.knowledge,cards:(t.cards||[]).map(c=>c.knowledge)})&&s.push("方案知识库不得包含完整 SQL;cards[].knowledge.dataLogic 必须使用自然语言描述"),s}g(Y,"validateSpec");function D(t){return[t.name,...(t.datasets||[]).filter(e=>!e.id).map(e=>e.name),...(t.cards||[]).filter(e=>e.type!=="existing").map(e=>e.name||e.spec?.name),...(t.publish?.reports||[]).map(e=>e.name)].filter(e=>e&&!String(e).startsWith("zz-"))}g(D,"formalNames");function z(t){const s={...H,...t.layout||{}},e=s.contentX+s.contentWidth,d=[],i=[];let l=0,c=0;const n=g(u=>{d.push({...u,mobileX:0,mobileY:c,mobileWidth:4,mobileHeight:u.height}),c+=u.height},"push");for(const[u,k]of(t.sections||[]).entries()){k.title&&(n({kind:"title",key:`title-${u}`,title:k.title,x:s.contentX,y:l,width:s.contentWidth,height:s.titleHeight}),l+=s.titleHeight);for(const[y,h]of(k.rows||[]).entries()){const o=h.cards||[],r=o.reduce((x,b)=>x+(Number(b.width)||0),0),m=o.filter(x=>!x.width).length,p=m?Math.floor((s.contentWidth-r)/m):0;let S=s.contentX;for(const x of o){const b=Number(x.width)||p;b<=0&&i.push(`sections[${u}].rows[${y}] 卡片 ${x.card} 宽度为 0(行内固定宽度已占满)`),n({kind:"card",key:x.card,x:S,y:l,width:b,height:Number(h.height)}),S+=b}S>e&&i.push(`sections[${u}].rows[${y}] 总宽 ${S-s.contentX} 超出内容区 ${s.contentWidth} 格`),l+=Number(h.height)}}return{layout:s,blocks:d,errors:[...i,...C(d,"pc"),...C(d,"mobile")]}}g(z,"planLayout");function C(t,s){const e=g(i=>s==="pc"?[i.x,i.y,i.width,i.height]:[i.mobileX,i.mobileY,i.mobileWidth,i.mobileHeight],"rect"),d=[];for(let i=0;i<t.length;i++)for(let l=i+1;l<t.length;l++){const[c,n,u,k]=e(t[i]),[y,h,o,r]=e(t[l]);c<y+o&&y<c+u&&n<h+r&&h<n+k&&d.push(`${s} 布局重叠:${t[i].key} 与 ${t[l].key}`)}return d}g(C,"overlaps");function R(t){return JSON.stringify({type:"title-template",version:1,template:"section",config:{serial:String(t.serial??""),title:String(t.title??""),subtitle:String(t.subtitle??""),description:"",showDecorLine:!1}})}g(R,"titleContent");function _(t){const s=[];for(const e of t.datasets||[])s.push(e.id?`复用数据集 ${e.key} = ${e.id}`:`建数据集 ${e.key}「${e.name}」← ${e.sql}`);for(const e of t.cards||[]){if(e.type==="existing"){s.push(`复用卡片 ${e.key} = ${e.id}`);continue}s.push(`建卡片 ${e.key}(${e.type}${e.kind?` ${e.kind}`:""})「${e.name||e.spec?.name||""}」${e.sort?`,排序 ${e.sort}`:""}`)}s.push(`建方案「${t.name}」并按布局放入 ${(t.sections||[]).length} 个分区`),s.push("写页面样式(暖色背景、标题/指标卡卡位透明、隐藏外层标题)"),t.filters&&s.push(`写方案筛选器模板 ← ${t.filters}`);for(const e of t.publish?.reports||[])s.push(`发布报表「${e.name}」← 卡片 ${e.card}`);return s.push("验收:逐卡 query + 方案回读(卡片数、越界、重叠、背景色)"),s.push("生成并保存版本化方案知识库(完整呈现给用户,不含 SQL)"),s}g(_,"describePlan");function M(t,s,e=""){const d=g((i,l)=>(c,n)=>{if(!i[n])throw new Error(`filters 引用了不存在或未创建的${l} key:${n}`);return i[n]},"pick");return t.replaceAll("{{plan}}",s.planId).replaceAll("{{dataSource}}",e).replace(/\{\{pageCard:([^}]+)\}\}/g,d(s.blocks,"卡片")).replace(/\{\{dataset:([^}]+)\}\}/g,d(s.datasets,"数据集"))}g(M,"fillPlaceholders");function U(t){try{return JSON.parse(O.readFileSync(t,"utf8"))}catch{return{datasets:{},cards:{},sorted:{},blocks:{},reports:{}}}}g(U,"readState");function K(t,s){O.writeFileSync(t,JSON.stringify(s,null,2))}g(K,"writeState");const T=g((t,s)=>j.resolve(t,String(s)),"resolveFile");async function A(t,s){try{return await B.reviewSpec(t,e=>O.readFileSync(T(s,e),"utf8"))}catch(e){return{mode:t.reuse??"suggest",skipped:f.LoginRequiredError&&e instanceof f.LoginRequiredError?"未登录,无法查重":`查重失败:${String(e.message).slice(0,200)}`,datasets:{},cards:{}}}}g(A,"reviewReuse");function G(t){const s=Object.entries(t.datasets||{}).filter(([,d])=>d.identical.length||d.similar.length),e=Object.entries(t.cards||{});return!s.length&&!e.length?null:`发现可复用对象:数据集 ${s.map(([d])=>d).join("、")||"无"},卡片 ${e.map(([d])=>d).join("、")||"无"}。`+(t.mode==="auto"?"完全一样的将直接复用;相似的请比较后决定。":"完全一样的可在 spec 加 reuse: auto 直接复用;相似的请比较 columns 后决定复用、在原数据集上调整,还是新建。")}g(G,"reuseHint");async function oe({spec:t,specFile:s,baseDir:e},{execute:d=!1,confirm:i=!1,style:l=!0,beforeWrite:c}={}){const n=Y(t),u=n.length?{blocks:[],errors:[]}:z(t);n.push(...u.errors);const k=n.length?[]:D(t);if(n.length)return v.emit("build.stage",{stage:"validate",ok:!1,errors:n.length}),{ok:!1,stage:"validate",errors:n};const y={name:t.name,tenant:J(),steps:_(t),layout:u.blocks.map(a=>({key:a.key,x:a.x,y:a.y,w:a.width,h:a.height}))};if(!d){const a=await A(t,e),w=G(a);return{ok:!0,mode:"dry-run",...y,formalNames:k,reuse:a,next:[w,k.length?"含正式名:取得用户授权后加 --execute --confirm":"确认无误后加 --execute"].filter(Boolean).join(" ")}}if(k.length&&!i)return{ok:!1,stage:"authorize",errors:[`以下为正式名(非 zz- 前缀),按 SOP 需要用户明确授权后加 --confirm:${k.join("、")}`]};c&&await c();const h=t.reuse==="auto"?await A(t,e):{mode:t.reuse??"suggest",datasets:{},cards:{}},o=`${s}.state.json`,r=U(o),m=g(()=>K(o,r),"save"),p=[];let S=Date.now();p.push=(...a)=>{for(const w of a){const[$,I=""]=String(w).split(/\s+(.*)/s);v.emit("build.stage",{stage:$,detail:I.slice(0,200),ok:!0,ms:Date.now()-S}),S=Date.now()}return Array.prototype.push.apply(p,a)},v.emit("build.stage",{stage:"start",ok:!0,resumed:!!(r.planId||Object.keys(r.datasets||{}).length),datasets:(t.datasets||[]).length,cards:(t.cards||[]).length,reports:(t.publish?.reports||[]).length});for(const a of t.datasets||[]){if(r.datasets[a.key])continue;const w=!a.id&&h.datasets[a.key]?.reuseId;r.datasets[a.key]=a.id||w||await f.dsSave({name:a.name,sql:O.readFileSync(T(e,a.sql),"utf8")}),m(),p.push(`dataset ${a.key} → ${r.datasets[a.key]}${w?"(复用已有数据集)":""}`)}for(const a of t.cards||[]){if(!r.cards[a.key]){const $=r.datasets[a.dataset];let I;a.type==="existing"?I={cardId:a.id}:h.cards[a.key]?.reuseId?I={cardId:h.cards[a.key].reuseId,reused:!0}:a.type==="chart"?I=await f.cardChart({dsId:$,name:a.name,kind:a.kind,dim:N(a.dim).join(","),meas:N(a.meas).join(","),series:a.series}):a.type==="meter"?I=await f.cardMeter({dsId:$,name:a.name,col:a.col,subtitle:a.subtitle,color:a.color||void 0}):a.type==="table"?I=await f.cardTable({dsId:$,spec:a.spec}):I=await f.cardGantt({dsId:$,spec:a.spec}),r.cards[a.key]=I.cardId,m(),p.push(`card ${a.key} → ${I.cardId}${I.reused?"(复用已有卡片)":""}`)}const w=h.cards[a.key]?.reuseId&&r.cards[a.key]===h.cards[a.key].reuseId;if(a.sort&&!r.sorted[a.key]&&!w){const $=await f.cardSort(r.cards[a.key],String(a.sort));r.sorted[a.key]=$.sqlHasOrderBy,m(),p.push(`sort ${a.key} → sqlHasOrderBy=${$.sqlHasOrderBy}`)}}r.planId||(r.planId=await f.post("/page-setting/new-create-empty-plan",{name:t.name,isPrivate:!1,pageType:0}),m(),p.push(`plan → ${r.planId}`));for(const a of u.blocks){if(r.blocks[a.key])continue;const w={x:a.x,y:a.y,width:a.width,height:a.height,mobileX:a.mobileX,mobileY:a.mobileY,mobileWidth:a.mobileWidth,mobileHeight:a.mobileHeight},$=a.kind==="title"?{...w,sourceType:0,cardType:5,contentText:R(a.title),bindFilterIds:[],userPageSettingId:r.planId}:{...w,indexBlockId:r.cards[a.key],sourceType:0,cardType:1,bindFilterIds:[],userPageSettingId:r.planId};r.blocks[a.key]=(await f.post("/page-setting/new-page-card",$)).id,m()}if(p.push(`placed ${u.blocks.length} blocks`),l&&!r.styled&&(r.styled=await Q(t,u,r),m(),p.push(`style → ${JSON.stringify(r.styled)}`)),t.filters&&!r.filtered){const a=M(O.readFileSync(T(e,t.filters),"utf8"),r,(await f.ctx()).dataSourceId),w=JSON.parse(a);r.filterDatasetIds=[...new Set((w.filters||[]).map($=>$.indexBlockDataId).filter($=>$&&$!==f.Z))],r.filtered=await f.planFilter(r.planId,w),m(),p.push(`filters → ${r.filtered.filters?.length??0}`)}for(const a of t.publish?.reports||[])r.reports[a.card]||(await f.post("/index-block/operate-report",{indexBlockId:r.cards[a.card],name:a.name,tag:"",isPrivate:!1}),r.reports[a.card]=a.name,m(),p.push(`report ${a.name}`));const x=new Set(Object.entries(h.cards||{}).filter(([a,w])=>w.reuseId&&r.cards[a]===w.reuseId).map(([a])=>a)),b=await Z(t,u,r,x);return v.emit("build.stage",{stage:"verify",ok:!!b.ok,checks:Array.isArray(b.checks)?b.checks.length:void 0,failed:Array.isArray(b.checks)?b.checks.filter(a=>a&&a.ok===!1).length:void 0}),b.ok?{ok:!0,mode:"execute",...await P({planId:r.planId,operation:"create",authoring:L(t,r)}),verify:b,log:p,stateFile:o,...h.mode==="auto"?{reuse:h}:{}}:{ok:!1,mode:"execute",verify:b,log:p,stateFile:o}}g(oe,"runBuild");async function Q(t,s,e){const d=await f.planDetail(e.planId,!0),i=d.pageCards||[],l=new Set(Object.values(e.blocks)),c=new Set(i.map(r=>r.id));if(![...l].every(r=>c.has(r)))return{skipped:"方案回读缺卡,未写样式(避免 update-page-card 移除卡片)"};if(!d.name)return{skipped:"方案详情缺 name 字段,未写样式(避免覆盖方案配置)"};const n=Object.fromEntries(Object.entries(e.blocks).map(([r,m])=>[m,r])),u=Object.fromEntries((t.cards||[]).map(r=>[r.key,r.type])),k=s.blocks.some(r=>r.kind==="title"),y=g(r=>{try{return typeof r=="string"?JSON.parse(r||"{}"):r||{}}catch{return{}}},"parse"),h=i.map(r=>{const m=n[r.id],p=y(r.dataScreenSetting);if(m){const S=m.startsWith("title-"),x=S||q.has(u[m]);Object.assign(p,{useCardConfig:!0},x?{backgroundColor:"transparent"}:{},S||k?{cardTitle:{...p.cardTitle||{},show:!1}}:{})}return{id:r.id,x:r.x,y:r.y,width:r.width,height:r.height,mobileX:r.mobileX,mobileY:r.mobileY,mobileWidth:r.mobileWidth,mobileHeight:r.mobileHeight,zIndex:r.zIndex??0,dataScreenSetting:JSON.stringify(p),...r.dataScreenChartSetting!==void 0?{dataScreenChartSetting:r.dataScreenChartSetting}:{}}}),o={...y(d.dataScreenSetting),pageBackgroundColor:s.layout.pageBackgroundColor};return await f.post("/page-setting/update-page-card",{list:h,userPageSettingId:e.planId,dataScreenSetting:JSON.stringify(o),name:d.name,tag:d.tag||"",remark:d.remark||"",parentId:d.parentId,isPrivate:!!d.isPrivate}),{cards:h.length,pageBackgroundColor:o.pageBackgroundColor}}g(Q,"applyStyle");async function Z(t,s,e,d=new Set){const i=[];for(const o of t.cards||[]){const r=await f.queryCard({cardId:e.cards[o.key],pageSize:1});i.push({check:`query ${o.key}`,ok:r.code===200,detail:r.code===200?`total=${r.total}`:r.message})}for(const o of(t.cards||[]).filter(r=>r.type==="table"&&r.spec?.fitTableWidth!==!1)){const r=await f.cardDetail(e.cards[o.key]),p=(typeof r.chartSetting=="string"?(()=>{try{return JSON.parse(r.chartSetting||"{}")}catch{return{}}})():r.chartSetting||{}).config?.fitTableWidth===!0,S=d.has(o.key);i.push({check:`AG 自适应宽度 ${o.key}`,ok:p||S,detail:p?"":S?"复用的共享表格未开启,未修改":"fitTableWidth 未开启"})}const l=await f.planDetail(e.planId,!0),c=l.pageCards||[],n=new Set(c.map(o=>o.id));i.push({check:"方案卡片齐全",ok:Object.values(e.blocks).every(o=>n.has(o)),detail:`${c.length} 张`});const u=s.layout,k=c.filter(o=>o.x<u.contentX||o.x+o.width>u.contentX+u.contentWidth);i.push({check:`PC 卡位在 [${u.contentX},${u.contentX+u.contentWidth}] 内`,ok:!k.length,detail:k.map(o=>o.id).join(",")});const y=c.map(o=>({key:o.id,x:o.x,y:o.y,width:o.width,height:o.height,mobileX:o.mobileX,mobileY:o.mobileY,mobileWidth:o.mobileWidth,mobileHeight:o.mobileHeight})),h=[...C(y,"pc"),...C(y,"mobile")];if(i.push({check:"PC/手机无重叠",ok:!h.length,detail:h.join("; ")}),e.styled&&!e.styled.skipped){let o={};try{o=JSON.parse(l.dataScreenSetting||"{}")}catch{}i.push({check:"页面背景色",ok:o.pageBackgroundColor===u.pageBackgroundColor,detail:o.pageBackgroundColor||"(未设置)"})}for(const[o,r]of Object.entries(e.sorted))i.push({check:`排序生效 ${o}`,ok:r===!0,detail:""});return{ok:i.every(o=>o.ok),checks:i}}g(Z,"verifyBuild");export{_ as describePlan,M as fillPlaceholders,D as formalNames,se as loadSpec,z as planLayout,oe as runBuild,R as titleContent,Y as validateSpec,Z as verifyBuild};
@@ -1 +1 @@
1
- const NO_DATA_SETTING_CHARTS=new Set(["editor","iframe","clock","countdown"]),FRONTEND_DISABLED_CHARTS=new Set(["meter/commonMeter","meter/antdTable"]),PAGINATION_CHARTS=new Set(["agtable","gantt","multiFlowStep"]),FILTER_TYPE={select:1,time:2,condition:3},DEFAULT_TYPE={fixed:1,dynamic:4},parse=v=>{if(v&&typeof v=="object")return v;if(typeof v!="string"||!v.trim())return null;try{return JSON.parse(v)}catch{return}},filled=v=>!(v==null||v===""||Array.isArray(v)&&!v.length);function aliasError(title){if(!filled(title))return null;const s=String(title);return/^[0-9]/.test(s)?"不能以数字开头":/[~!@#$%^&*()+.`\-={}|;'",<>/?[\]]/.test(s)?"不能含特殊符号(可用中文括号)":null}const isCardSave=(method,p)=>String(method).toUpperCase()==="POST"&&/(?:^|\/)index-block\/(?:new|update-new)(?:\?|$)/i.test(String(p)),defaultDisabled=(parentId,id)=>FRONTEND_DISABLED_CHARTS.has(`${parentId}/${id}`);function cardErrors(body,{isDisabled=defaultDisabled}={}){if(!body||typeof body!="object"||Array.isArray(body))return[];const errors=[],chart=parse(body.chartSetting);if(!chart||!chart.id)return["请选择图表配置(chartSetting 缺失或不是合法 JSON,或没有图表 id)"];isDisabled(chart.parentId,chart.id)&&errors.push(`当前图表已禁用(${chart.parentId}/${chart.id}),请换用其它图表`);const dtos=Array.isArray(body.indexBlockDataSettingDtos)?body.indexBlockDataSettingDtos:[],drawn=dtos.filter(d=>[1,2,3].includes(Number(d?.argumentType)));!NO_DATA_SETTING_CHARTS.has(chart.id)&&!drawn.length&&errors.push("请选择绘制配置(维度 / 数值 / 系列至少一个字段)");for(const o of dtos.filter(d=>Number(d?.argumentType)===0))(!o?.title||!o?.orderByType)&&errors.push("指标排序不能为空(排序项要有字段 title 和排序方式 orderByType)");for(const d of drawn){const role=Number(d.argumentType);d.isAggregation&&d.isCustomize&&(role===2||role===3)&&errors.push(`聚合字段「${d.title||d.comment}」不可作为维度或系列`),d.isFill&&(role===1||role===3)&&errors.push(`填报字段「${d.title||d.comment}」不可作为数值或系列`)}for(const d of drawn)if(d.title&&d.comment&&d.title!==d.comment){const why=aliasError(d.title);why&&errors.push(`字段别名「${d.title}」${why}`)}if(chart.config?.report?.visible){const drawnIds=new Set(dtos.map(d=>d?.uniqueId).filter(Boolean));(Array.isArray(body.fillFields)?body.fillFields:[]).flatMap(b=>(b?.fields||[]).map(f=>f?.uniqueId)).filter(Boolean).some(id=>!drawnIds.has(id))&&errors.push("填报依据字段缺失,请重新选择(依据字段必须是卡片已绘制的字段)")}const fields=new Set(drawn.map(d=>d?.field).filter(Boolean));if((Array.isArray(body.filters)?body.filters:[]).forEach((f,i)=>{Number(f?.filterProperty)===1&&f?.propertyName&&!fields.has(f.propertyName)&&errors.push(`筛选条件配置错误:第 ${i+1} 个筛选(${f.labelName||f.propertyName})的结果集字段 ${f.propertyName} 不在卡片绘制字段里`),errors.push(...filterErrors(f,i,"card"))}),chart.id==="s2table"&&chart.config?.showPagination!==!0){const n=Number(body.limitCount);(!Number.isInteger(n)||n<1||n>5e3)&&errors.push("请输入数据条数,数据格式为整数,最大不能超过 5000(多维表未开分页)")}return[...new Set(errors)]}function normalizeCard(body){const chart=parse(body.chartSetting),notes=[];return PAGINATION_CHARTS.has(chart.id)&&chart.config?.showPagination===!0&&body.limitCount&&(delete body.limitCount,notes.push("已开启分页功能,数据条数已自动清空")),body.chartParentType=chart.parentId,body.chartType=chart.id,body.indexBlockType=chart.parentId==="table"?0:1,notes}function checkCardPayload(method,p,body,{isDisabled=defaultDisabled,previous=null}={}){if(!isCardSave(method,p)||!body||typeof body!="object"||Array.isArray(body))return[];const errors=cardErrors(body,{isDisabled}),before=previous?new Set(cardErrors(previous,{isDisabled})):new Set,introduced=errors.filter(e=>!before.has(e));if(introduced.length){const error=new Error(`卡片保存前校验未通过:${introduced.join(";")}`);throw error.errors=introduced,error}const kept=errors.filter(e=>before.has(e));return[...normalizeCard(body),...kept.length?[`卡片原有配置与界面保存规则不一致(本次未改动,未拦截,建议修正):${kept.join(";")}`]:[]]}function filterErrors(f,i=0,scope="plan"){const errors=[],name=`第 ${i+1} 个筛选(${f?.labelName||f?.fieldName||f?.propertyName||"未命名"})`;if(!f||typeof f!="object")return[`${name}不是对象`];const type=Number(f.filterType),property=Number(f.filterProperty);if(filled(f.filterOperationType)||errors.push(`${name}缺少类型选择 filterOperationType`),property===2){const ids=parse(f.variableIds);(Array.isArray(ids)?ids.filter(Boolean).length:filled(f.variableIds))||errors.push(`${name}是参数变量筛选,必须选择参数 variableIds`)}scope==="card"&&(property===1&&!filled(f.propertyName)&&errors.push(`${name}是结果集过滤,必须选择筛选维度 propertyName`),property===0&&!filled(f.fieldName)&&errors.push(`${name}是维度过滤,必须选择字段 fieldName`)),scope==="plan"&&property===1&&(Array.isArray(f.bindUserPageCards)?f.bindUserPageCards:[]).some(b=>!b?.pageCardId)&&errors.push(`${name}绑定的卡片缺少 pageCardId`);const valueType=Number(f.defaultValueType),value=parse(f.defaultValue)??f.defaultValue;return(type===FILTER_TYPE.select||type===FILTER_TYPE.time)&&!valueType&&errors.push(`${name}缺少默认值类型 defaultValueType`),type===FILTER_TYPE.time&&valueType===DEFAULT_TYPE.dynamic&&(filled(value)?Array.isArray(value)&&value.filter(v=>filled(v)).length!==2&&errors.push(`${name}动态日期区间要设置开始和结束两个值`):errors.push(`${name}动态日期默认值未设置`)),f.isRequired&&!filled(value)&&errors.push(`${name}是必填筛选,请设置默认值`),type===FILTER_TYPE.select&&(String(f.isSelfadd)==="true"?(f.userPageCardFilterValueRes||[]).filter(o=>filled(o?.value)).length||errors.push(`${name}手工选项为空,请添加选项`):filled(f.fieldName)||errors.push(`${name}选项来源缺少字段 fieldName`)),errors}const isFilterSave=(method,p)=>String(method).toUpperCase()==="POST"&&/(?:^|\/)page-setting\/batch-add-or-update-filter(?:\?|$)/i.test(String(p));function checkFilterPayload(method,p,body,{previous=null}={}){if(!isFilterSave(method,p)||!body||typeof body!="object")return[];const scope=Number(body.filterSourceType)===2?"card":"plan",keyed=list=>(Array.isArray(list)?list:[]).flatMap((f,i)=>filterErrors(f,i,scope).map(message=>({message,key:`${f?.labelName??i}|${message.replace(/^第 \d+ 个筛选/,"")}`}))),errors=keyed(body.filters),before=new Set(keyed(previous).map(e=>e.key)),introduced=errors.filter(e=>!before.has(e.key));if(introduced.length){const error=new Error(`筛选条件保存前校验未通过:${introduced.map(e=>e.message).join(";")}`);throw error.errors=introduced.map(e=>e.message),error}const kept=errors.filter(e=>before.has(e.key));return kept.length?[`筛选条件原有配置与界面保存规则不一致(本次未改动,未拦截,建议修正):${kept.map(e=>e.message).join(";")}`]:[]}export{FRONTEND_DISABLED_CHARTS,NO_DATA_SETTING_CHARTS,aliasError,cardErrors,checkCardPayload,checkFilterPayload,filterErrors,isCardSave,isFilterSave};
1
+ var A=Object.defineProperty;var c=(e,a)=>A(e,"name",{value:a,configurable:!0});const T=new Set(["editor","iframe","clock","countdown"]),w=new Set(["meter/commonMeter","meter/antdTable"]),I=new Set(["agtable","gantt","multiFlowStep"]),m={select:1,time:2,condition:3},C={fixed:1,dynamic:4},h=c(e=>{if(e&&typeof e=="object")return e;if(typeof e!="string"||!e.trim())return null;try{return JSON.parse(e)}catch{return}},"parse"),f=c(e=>!(e==null||e===""||Array.isArray(e)&&!e.length),"filled");function P(e){if(!f(e))return null;const a=String(e);return/^[0-9]/.test(a)?"不能以数字开头":/[~!@#$%^&*()+.`\-={}|;'",<>/?[\]]/.test(a)?"不能含特殊符号(可用中文括号)":null}c(P,"aliasError");const b=c((e,a)=>String(e).toUpperCase()==="POST"&&/(?:^|\/)index-block\/(?:new|update-new)(?:\?|$)/i.test(String(a)),"isCardSave"),S=c((e,a)=>w.has(`${e}/${a}`),"defaultDisabled");function $(e,{isDisabled:a=S}={}){if(!e||typeof e!="object"||Array.isArray(e))return[];const r=[],i=h(e.chartSetting);if(!i||!i.id)return["请选择图表配置(chartSetting 缺失或不是合法 JSON,或没有图表 id)"];a(i.parentId,i.id)&&r.push(`当前图表已禁用(${i.parentId}/${i.id}),请换用其它图表`);const o=Array.isArray(e.indexBlockDataSettingDtos)?e.indexBlockDataSettingDtos:[],p=o.filter(t=>[1,2,3].includes(Number(t?.argumentType)));!T.has(i.id)&&!p.length&&r.push("请选择绘制配置(维度 / 数值 / 系列至少一个字段)");for(const t of o.filter(n=>Number(n?.argumentType)===0))(!t?.title||!t?.orderByType)&&r.push("指标排序不能为空(排序项要有字段 title 和排序方式 orderByType)");for(const t of p){const n=Number(t.argumentType);t.isAggregation&&t.isCustomize&&(n===2||n===3)&&r.push(`聚合字段「${t.title||t.comment}」不可作为维度或系列`),t.isFill&&(n===1||n===3)&&r.push(`填报字段「${t.title||t.comment}」不可作为数值或系列`)}for(const t of p)if(t.title&&t.comment&&t.title!==t.comment){const n=P(t.title);n&&r.push(`字段别名「${t.title}」${n}`)}if(i.config?.report?.visible){const t=new Set(o.map(s=>s?.uniqueId).filter(Boolean));(Array.isArray(e.fillFields)?e.fillFields:[]).flatMap(s=>(s?.fields||[]).map(l=>l?.uniqueId)).filter(Boolean).some(s=>!t.has(s))&&r.push("填报依据字段缺失,请重新选择(依据字段必须是卡片已绘制的字段)")}const u=new Set(p.map(t=>t?.field).filter(Boolean));if((Array.isArray(e.filters)?e.filters:[]).forEach((t,n)=>{Number(t?.filterProperty)===1&&t?.propertyName&&!u.has(t.propertyName)&&r.push(`筛选条件配置错误:第 ${n+1} 个筛选(${t.labelName||t.propertyName})的结果集字段 ${t.propertyName} 不在卡片绘制字段里`),r.push(...N(t,n,"card"))}),i.id==="s2table"&&i.config?.showPagination!==!0){const t=Number(e.limitCount);(!Number.isInteger(t)||t<1||t>5e3)&&r.push("请输入数据条数,数据格式为整数,最大不能超过 5000(多维表未开分页)")}return[...new Set(r)]}c($,"cardErrors");function k(e){const a=h(e.chartSetting),r=[];return I.has(a.id)&&a.config?.showPagination===!0&&e.limitCount&&(delete e.limitCount,r.push("已开启分页功能,数据条数已自动清空")),e.chartParentType=a.parentId,e.chartType=a.id,e.indexBlockType=a.parentId==="table"?0:1,r}c(k,"normalizeCard");function B(e,a,r,{isDisabled:i=S,previous:o=null}={}){if(!b(e,a)||!r||typeof r!="object"||Array.isArray(r))return[];const p=$(r,{isDisabled:i}),u=o?new Set($(o,{isDisabled:i})):new Set,t=p.filter(s=>!u.has(s));if(t.length){const s=new Error(`卡片保存前校验未通过:${t.join(";")}`);throw s.errors=t,s}const n=p.filter(s=>u.has(s));return[...k(r),...n.length?[`卡片原有配置与界面保存规则不一致(本次未改动,未拦截,建议修正):${n.join(";")}`]:[]]}c(B,"checkCardPayload");function N(e,a=0,r="plan"){const i=[],o=`第 ${a+1} 个筛选(${e?.labelName||e?.fieldName||e?.propertyName||"未命名"})`;if(!e||typeof e!="object")return[`${o}不是对象`];const p=Number(e.filterType),u=Number(e.filterProperty);if(f(e.filterOperationType)||i.push(`${o}缺少类型选择 filterOperationType`),u===2){const s=h(e.variableIds);(Array.isArray(s)?s.filter(Boolean).length:f(e.variableIds))||i.push(`${o}是参数变量筛选,必须选择参数 variableIds`)}r==="card"&&(u===1&&!f(e.propertyName)&&i.push(`${o}是结果集过滤,必须选择筛选维度 propertyName`),u===0&&!f(e.fieldName)&&i.push(`${o}是维度过滤,必须选择字段 fieldName`)),r==="plan"&&u===1&&(Array.isArray(e.bindUserPageCards)?e.bindUserPageCards:[]).some(l=>!l?.pageCardId)&&i.push(`${o}绑定的卡片缺少 pageCardId`);const t=Number(e.defaultValueType),n=h(e.defaultValue)??e.defaultValue;return(p===m.select||p===m.time)&&!t&&i.push(`${o}缺少默认值类型 defaultValueType`),p===m.time&&t===C.dynamic&&(f(n)?Array.isArray(n)&&n.filter(s=>f(s)).length!==2&&i.push(`${o}动态日期区间要设置开始和结束两个值`):i.push(`${o}动态日期默认值未设置`)),e.isRequired&&!f(n)&&i.push(`${o}是必填筛选,请设置默认值`),p===m.select&&(String(e.isSelfadd)==="true"?(e.userPageCardFilterValueRes||[]).filter(l=>f(l?.value)).length||i.push(`${o}手工选项为空,请添加选项`):f(e.fieldName)||i.push(`${o}选项来源缺少字段 fieldName`)),i}c(N,"filterErrors");const x=c((e,a)=>String(e).toUpperCase()==="POST"&&/(?:^|\/)page-setting\/batch-add-or-update-filter(?:\?|$)/i.test(String(a)),"isFilterSave");function F(e,a,r,{previous:i=null}={}){if(!x(e,a)||!r||typeof r!="object")return[];const o=Number(r.filterSourceType)===2?"card":"plan",p=c(l=>(Array.isArray(l)?l:[]).flatMap((d,g)=>N(d,g,o).map(y=>({message:y,key:`${d?.labelName??g}|${y.replace(/^第 \d+ 个筛选/,"")}`}))),"keyed"),u=p(r.filters),t=new Set(p(i).map(l=>l.key)),n=u.filter(l=>!t.has(l.key));if(n.length){const l=new Error(`筛选条件保存前校验未通过:${n.map(d=>d.message).join(";")}`);throw l.errors=n.map(d=>d.message),l}const s=u.filter(l=>t.has(l.key));return s.length?[`筛选条件原有配置与界面保存规则不一致(本次未改动,未拦截,建议修正):${s.map(l=>l.message).join(";")}`]:[]}c(F,"checkFilterPayload");export{w as FRONTEND_DISABLED_CHARTS,T as NO_DATA_SETTING_CHARTS,P as aliasError,$ as cardErrors,B as checkCardPayload,F as checkFilterPayload,N as filterErrors,b as isCardSave,x as isFilterSave};
@@ -1 +1 @@
1
- import fs from"node:fs";import path from"node:path";import os from"node:os";import crypto from"node:crypto";const DEFAULT_TTL_SECONDS=900,MAX_TTL_SECONDS=7200,MAX_NOTES=500,KINDS=new Set(["plan","card","ds","knowledge","spec","report","filter"]);class CollabConflictError extends Error{constructor(message,holders){super(message),this.name="CollabConflictError",this.holders=holders}}function collabDir(){return path.join(os.homedir(),".linkai","collab")}const stateFile=()=>path.join(collabDir(),"state.json"),lockFile=()=>path.join(collabDir(),"state.lock");function currentWorker(){return String(process.env.LINKAI_COLLAB_WORKER||"").trim()}function emptyState(){return{boards:{},claims:{}}}function readState(){try{return{...emptyState(),...JSON.parse(fs.readFileSync(stateFile(),"utf8"))}}catch{return emptyState()}}function sleepSync(ms){Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,ms)}function update(fn){fs.mkdirSync(collabDir(),{recursive:!0,mode:448});const deadline=Date.now()+3e3;let fd=null,staleRemovalFailed=!1;for(;fd===null;){if(Date.now()>deadline)throw new Error("协同状态被占用超过 3 秒,请稍后重试");try{fd=fs.openSync(lockFile(),"wx",384)}catch(e){if(e.code!=="EEXIST")throw e;let stale=!1;if(!staleRemovalFailed)try{stale=Date.now()-fs.statSync(lockFile()).mtimeMs>5e3}catch{}if(stale)try{fs.rmSync(lockFile(),{force:!0});continue}catch{staleRemovalFailed=!0}sleepSync(10)}}try{const state=readState();pruneExpired(state);const result=fn(state),tmp=`${stateFile()}.${process.pid}.tmp`;return fs.writeFileSync(tmp,JSON.stringify(state,null,2),{mode:384}),fs.renameSync(tmp,stateFile()),result}finally{fs.closeSync(fd),fs.rmSync(lockFile(),{force:!0})}}function pruneExpired(state,now=Date.now()){for(const[key,claim2]of Object.entries(state.claims))claim2.expiresAt<=now&&delete state.claims[key]}const iso=ms=>new Date(ms).toISOString();function normalizeObject(raw){const text=String(raw||"").trim(),m=text.match(/^([a-z]+):(.+)$/i);if(!m||!KINDS.has(m[1].toLowerCase()))throw new Error(`对象写法不对:${text||"(空)"}。格式 <类型>:<id>,类型:${[...KINDS].join(" / ")},如 card:<cardId>`);const kind=m[1].toLowerCase(),id=kind==="spec"?path.resolve(m[2].trim()):m[2].trim().toLowerCase();return`${kind}:${id}`}function parseObjects(list){const items=(Array.isArray(list)?list:String(list||"").split(/[,,\s]+/)).filter(Boolean);if(!items.length)throw new Error("至少指定一个对象,如 --object card:<cardId>");return[...new Set(items.map(normalizeObject))]}function requireWorker(worker){const w=String(worker||"").trim();if(!w||w.length>40||!/^[\w一-龥.-]+$/.test(w))throw new Error("需要执行者名称:加 --worker <名称>(字母、数字、中文、. _ -,最长 40),如 --worker 主控 或 --worker sql-1");return w}function requireBoard(state,board){const b=state.boards[String(board||"")];if(!b)throw new Error(`任务板不存在:${board||"(空)"};先 linkai collab-start 创建,或 linkai collab-status 查看已有任务板`);return b}function addNote(board,worker,kind,text,ref){board.notes.push({at:iso(Date.now()),worker,kind,text:String(text).slice(0,2e3),...ref?{ref:String(ref).slice(0,500)}:{}}),board.notes.length>MAX_NOTES&&board.notes.splice(0,board.notes.length-MAX_NOTES)}function startBoard({goal,plan,worker}){const w=requireWorker(worker);if(!String(goal||"").trim())throw new Error("需要 --goal(这次协同要完成什么)");return update(state=>{const id=`b-${crypto.randomBytes(4).toString("hex")}`;return state.boards[id]={id,goal:String(goal).trim().slice(0,500),plan:plan?String(plan).toLowerCase():null,coordinator:w,createdAt:iso(Date.now()),closedAt:null,tasks:[],notes:[]},addNote(state.boards[id],w,"start",`创建任务板:${state.boards[id].goal}`),state.boards[id]})}function closeBoard({board,worker}){const w=requireWorker(worker);return update(state=>{const b=requireBoard(state,board),released=Object.entries(state.claims).filter(([,c])=>c.board===b.id).map(([k])=>k);for(const k of released)delete state.claims[k];return b.closedAt=iso(Date.now()),addNote(b,w,"close",`关闭任务板,释放 ${released.length} 个认领`),{board:b.id,closedAt:b.closedAt,released}})}function addTask({board,worker,title,assignee,objects}){const w=requireWorker(worker);if(!String(title||"").trim())throw new Error("需要 --title");return update(state=>{const b=requireBoard(state,board),task={id:b.tasks.length+1,title:String(title).trim().slice(0,300),assignee:assignee?requireWorker(assignee):null,objects:objects?parseObjects(objects):[],status:"todo",result:null,by:w,updatedAt:iso(Date.now())};return b.tasks.push(task),task})}const STATUSES=new Set(["todo","doing","done","blocked"]);function updateTask({board,worker,id,status:status2,result,assignee}){const w=requireWorker(worker);return update(state=>{const b=requireBoard(state,board),task=b.tasks.find(t=>t.id===Number(id));if(!task)throw new Error(`任务不存在:#${id}`);if(status2){if(!STATUSES.has(status2))throw new Error(`status 只能是 ${[...STATUSES].join(" / ")}`);task.status=status2}return assignee?task.assignee=requireWorker(assignee):status2==="doing"&&!task.assignee&&(task.assignee=w),result&&(task.result=String(result).slice(0,2e3)),task.updatedAt=iso(Date.now()),(status2==="done"||status2==="blocked")&&addNote(b,w,status2==="done"?"result":"blocker",`#${task.id} ${task.title}${result?`:${result}`:""}`),task})}function claim({board,worker,objects,ttl=DEFAULT_TTL_SECONDS,note:note2}){const w=requireWorker(worker),keys=parseObjects(objects),seconds=Math.min(Math.max(Number(ttl)||DEFAULT_TTL_SECONDS,60),MAX_TTL_SECONDS);return update(state=>{const b=requireBoard(state,board);if(b.closedAt)throw new Error(`任务板 ${b.id} 已关闭`);const now=Date.now(),held=keys.map(k=>[k,state.claims[k]]).filter(([,c])=>c&&c.worker!==w);if(held.length)throw new CollabConflictError(`对象已被其他执行者认领:${held.map(([k,c])=>`${k}(${c.worker},${Math.ceil((c.expiresAt-now)/6e4)} 分钟后过期)`).join(";")}`,held.map(([k,c])=>({object:k,...c,expiresAt:iso(c.expiresAt)})));for(const k of keys)state.claims[k]={worker:w,board:b.id,since:state.claims[k]?.since??now,expiresAt:now+seconds*1e3,note:note2?String(note2).slice(0,200):null};return addNote(b,w,"claim",`认领 ${keys.join("、")}${note2?`:${note2}`:""}`),{board:b.id,worker:w,objects:keys,expiresAt:iso(now+seconds*1e3)}})}function release({board,worker,objects}){const w=requireWorker(worker);return update(state=>{const b=requireBoard(state,board),wanted=objects?parseObjects(objects):Object.keys(state.claims).filter(k=>state.claims[k].board===b.id&&state.claims[k].worker===w),released=[];for(const k of wanted){const c=state.claims[k];if(c){if(c.worker!==w)throw new CollabConflictError(`不能释放别人的认领:${k}(${c.worker})`,[{object:k,...c,expiresAt:iso(c.expiresAt)}]);delete state.claims[k],released.push(k)}}return released.length&&addNote(b,w,"release",`释放 ${released.join("、")}`),{board:b.id,worker:w,released}})}function note({board,worker,text,kind="finding",ref}){const w=requireWorker(worker);if(!String(text||"").trim())throw new Error("需要 --text");const k=String(kind);if(!["finding","result","handoff","blocker","decision"].includes(k))throw new Error("kind 只能是 finding / result / handoff / blocker / decision");return update(state=>{const b=requireBoard(state,board);return addNote(b,w,k,text,ref),b.notes[b.notes.length-1]})}function status({board,notes=30}={}){const state=readState();pruneExpired(state);const now=Date.now(),claims=Object.entries(state.claims).map(([object,c])=>({object,worker:c.worker,board:c.board,note:c.note,expiresAt:iso(c.expiresAt),minutesLeft:Math.ceil((c.expiresAt-now)/6e4)}));if(!board)return{boards:Object.values(state.boards).filter(b2=>!b2.closedAt).map(b2=>({id:b2.id,goal:b2.goal,plan:b2.plan,coordinator:b2.coordinator,createdAt:b2.createdAt,tasks:b2.tasks.length,open:b2.tasks.filter(t=>t.status!=="done").length})),claims};const b=requireBoard(state,board);return{...b,notes:b.notes.slice(-Math.max(1,Number(notes)||30)),claims:claims.filter(c=>c.board===b.id)}}function assertWritable(objects,worker=currentWorker()){if(!objects.length||!fs.existsSync(stateFile()))return;const state=readState();pruneExpired(state);const now=Date.now(),held=objects.map(k=>[k,state.claims[k]]).filter(([,c])=>c&&c.worker!==worker);if(held.length)throw new CollabConflictError(`${held.map(([k,c])=>`${k} 正由「${c.worker}」处理(任务板 ${c.board},${Math.ceil((c.expiresAt-now)/6e4)} 分钟后过期)`).join(";")},${worker?`你是「${worker}」`:"当前命令没有 --worker"},已拒绝写入`,held.map(([k,c])=>({object:k,...c,expiresAt:iso(c.expiresAt)})))}function objectsForCommand(name,args,readJsonFile){const keys=[],add=(kind,id)=>{id&&keys.push(normalizeObject(`${kind}:${id}`))};if(add("card",args.card),add("ds",args.ds),args.plan&&(add("plan",args.plan),name==="plan-knowledge"&&add("knowledge",args.plan)),args.spec&&name==="build"&&add("spec",args.spec),name==="card-save"&&args.file)try{add("card",readJsonFile(args.file)?.id)}catch{}return[...new Set(keys)]}export{CollabConflictError,DEFAULT_TTL_SECONDS,addTask,assertWritable,claim,closeBoard,collabDir,currentWorker,normalizeObject,note,objectsForCommand,parseObjects,release,startBoard,status,updateTask};
1
+ var _=Object.defineProperty;var c=(r,e)=>_(r,"name",{value:e,configurable:!0});import u from"node:fs";import x from"node:path";import B from"node:os";import F from"node:crypto";const C=900,v=7200,L=500,N=new Set(["plan","card","ds","knowledge","spec","report","filter"]);class y extends Error{static{c(this,"CollabConflictError")}constructor(e,n){super(e),this.name="CollabConflictError",this.holders=n}}function E(){return x.join(B.homedir(),".linkai","collab")}c(E,"collabDir");const g=c(()=>x.join(E(),"state.json"),"stateFile"),$=c(()=>x.join(E(),"state.lock"),"lockFile");function W(){return String(process.env.LINKAI_COLLAB_WORKER||"").trim()}c(W,"currentWorker");function T(){return{boards:{},claims:{}}}c(T,"emptyState");function j(){try{return{...T(),...JSON.parse(u.readFileSync(g(),"utf8"))}}catch{return T()}}c(j,"readState");function q(r){Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,r)}c(q,"sleepSync");function m(r){u.mkdirSync(E(),{recursive:!0,mode:448});const e=Date.now()+3e3;let n=null,s=!1;for(;n===null;){if(Date.now()>e)throw new Error("协同状态被占用超过 3 秒,请稍后重试");try{n=u.openSync($(),"wx",384)}catch(t){if(t.code!=="EEXIST")throw t;let i=!1;if(!s)try{i=Date.now()-u.statSync($()).mtimeMs>5e3}catch{}if(i)try{u.rmSync($(),{force:!0});continue}catch{s=!0}q(10)}}try{const t=j();D(t);const i=r(t),o=`${g()}.${process.pid}.tmp`;return u.writeFileSync(o,JSON.stringify(t,null,2),{mode:384}),u.renameSync(o,g()),i}finally{u.closeSync(n),u.rmSync($(),{force:!0})}}c(m,"update");function D(r,e=Date.now()){for(const[n,s]of Object.entries(r.claims))s.expiresAt<=e&&delete r.claims[n]}c(D,"pruneExpired");const w=c(r=>new Date(r).toISOString(),"iso");function M(r){const e=String(r||"").trim(),n=e.match(/^([a-z]+):(.+)$/i);if(!n||!N.has(n[1].toLowerCase()))throw new Error(`对象写法不对:${e||"(空)"}。格式 <类型>:<id>,类型:${[...N].join(" / ")},如 card:<cardId>`);const s=n[1].toLowerCase(),t=s==="spec"?x.resolve(n[2].trim()):n[2].trim().toLowerCase();return`${s}:${t}`}c(M,"normalizeObject");function O(r){const e=(Array.isArray(r)?r:String(r||"").split(/[,,\s]+/)).filter(Boolean);if(!e.length)throw new Error("至少指定一个对象,如 --object card:<cardId>");return[...new Set(e.map(M))]}c(O,"parseObjects");function p(r){const e=String(r||"").trim();if(!e||e.length>40||!/^[\w一-龥.-]+$/.test(e))throw new Error("需要执行者名称:加 --worker <名称>(字母、数字、中文、. _ -,最长 40),如 --worker 主控 或 --worker sql-1");return e}c(p,"requireWorker");function h(r,e){const n=r.boards[String(e||"")];if(!n)throw new Error(`任务板不存在:${e||"(空)"};先 linkai collab-start 创建,或 linkai collab-status 查看已有任务板`);return n}c(h,"requireBoard");function k(r,e,n,s,t){r.notes.push({at:w(Date.now()),worker:e,kind:n,text:String(s).slice(0,2e3),...t?{ref:String(t).slice(0,500)}:{}}),r.notes.length>L&&r.notes.splice(0,r.notes.length-L)}c(k,"addNote");function U({goal:r,plan:e,worker:n}){const s=p(n);if(!String(r||"").trim())throw new Error("需要 --goal(这次协同要完成什么)");return m(t=>{const i=`b-${F.randomBytes(4).toString("hex")}`;return t.boards[i]={id:i,goal:String(r).trim().slice(0,500),plan:e?String(e).toLowerCase():null,coordinator:s,createdAt:w(Date.now()),closedAt:null,tasks:[],notes:[]},k(t.boards[i],s,"start",`创建任务板:${t.boards[i].goal}`),t.boards[i]})}c(U,"startBoard");function G({board:r,worker:e}){const n=p(e);return m(s=>{const t=h(s,r),i=Object.entries(s.claims).filter(([,o])=>o.board===t.id).map(([o])=>o);for(const o of i)delete s.claims[o];return t.closedAt=w(Date.now()),k(t,n,"close",`关闭任务板,释放 ${i.length} 个认领`),{board:t.id,closedAt:t.closedAt,released:i}})}c(G,"closeBoard");function H({board:r,worker:e,title:n,assignee:s,objects:t}){const i=p(e);if(!String(n||"").trim())throw new Error("需要 --title");return m(o=>{const l=h(o,r),a={id:l.tasks.length+1,title:String(n).trim().slice(0,300),assignee:s?p(s):null,objects:t?O(t):[],status:"todo",result:null,by:i,updatedAt:w(Date.now())};return l.tasks.push(a),a})}c(H,"addTask");const I=new Set(["todo","doing","done","blocked"]);function P({board:r,worker:e,id:n,status:s,result:t,assignee:i}){const o=p(e);return m(l=>{const a=h(l,r),d=a.tasks.find(S=>S.id===Number(n));if(!d)throw new Error(`任务不存在:#${n}`);if(s){if(!I.has(s))throw new Error(`status 只能是 ${[...I].join(" / ")}`);d.status=s}return i?d.assignee=p(i):s==="doing"&&!d.assignee&&(d.assignee=o),t&&(d.result=String(t).slice(0,2e3)),d.updatedAt=w(Date.now()),(s==="done"||s==="blocked")&&k(a,o,s==="done"?"result":"blocker",`#${d.id} ${d.title}${t?`:${t}`:""}`),d})}c(P,"updateTask");function Q({board:r,worker:e,objects:n,ttl:s=C,note:t}){const i=p(e),o=O(n),l=Math.min(Math.max(Number(s)||C,60),v);return m(a=>{const d=h(a,r);if(d.closedAt)throw new Error(`任务板 ${d.id} 已关闭`);const S=Date.now(),A=o.map(f=>[f,a.claims[f]]).filter(([,f])=>f&&f.worker!==i);if(A.length)throw new y(`对象已被其他执行者认领:${A.map(([f,b])=>`${f}(${b.worker},${Math.ceil((b.expiresAt-S)/6e4)} 分钟后过期)`).join(";")}`,A.map(([f,b])=>({object:f,...b,expiresAt:w(b.expiresAt)})));for(const f of o)a.claims[f]={worker:i,board:d.id,since:a.claims[f]?.since??S,expiresAt:S+l*1e3,note:t?String(t).slice(0,200):null};return k(d,i,"claim",`认领 ${o.join("、")}${t?`:${t}`:""}`),{board:d.id,worker:i,objects:o,expiresAt:w(S+l*1e3)}})}c(Q,"claim");function V({board:r,worker:e,objects:n}){const s=p(e);return m(t=>{const i=h(t,r),o=n?O(n):Object.keys(t.claims).filter(a=>t.claims[a].board===i.id&&t.claims[a].worker===s),l=[];for(const a of o){const d=t.claims[a];if(d){if(d.worker!==s)throw new y(`不能释放别人的认领:${a}(${d.worker})`,[{object:a,...d,expiresAt:w(d.expiresAt)}]);delete t.claims[a],l.push(a)}}return l.length&&k(i,s,"release",`释放 ${l.join("、")}`),{board:i.id,worker:s,released:l}})}c(V,"release");function Y({board:r,worker:e,text:n,kind:s="finding",ref:t}){const i=p(e);if(!String(n||"").trim())throw new Error("需要 --text");const o=String(s);if(!["finding","result","handoff","blocker","decision"].includes(o))throw new Error("kind 只能是 finding / result / handoff / blocker / decision");return m(l=>{const a=h(l,r);return k(a,i,o,n,t),a.notes[a.notes.length-1]})}c(Y,"note");function Z({board:r,notes:e=30}={}){const n=j();D(n);const s=Date.now(),t=Object.entries(n.claims).map(([o,l])=>({object:o,worker:l.worker,board:l.board,note:l.note,expiresAt:w(l.expiresAt),minutesLeft:Math.ceil((l.expiresAt-s)/6e4)}));if(!r)return{boards:Object.values(n.boards).filter(o=>!o.closedAt).map(o=>({id:o.id,goal:o.goal,plan:o.plan,coordinator:o.coordinator,createdAt:o.createdAt,tasks:o.tasks.length,open:o.tasks.filter(l=>l.status!=="done").length})),claims:t};const i=h(n,r);return{...i,notes:i.notes.slice(-Math.max(1,Number(e)||30)),claims:t.filter(o=>o.board===i.id)}}c(Z,"status");function tt(r,e=W()){if(!r.length||!u.existsSync(g()))return;const n=j();D(n);const s=Date.now(),t=r.map(i=>[i,n.claims[i]]).filter(([,i])=>i&&i.worker!==e);if(t.length)throw new y(`${t.map(([i,o])=>`${i} 正由「${o.worker}」处理(任务板 ${o.board},${Math.ceil((o.expiresAt-s)/6e4)} 分钟后过期)`).join(";")},${e?`你是「${e}」`:"当前命令没有 --worker"},已拒绝写入`,t.map(([i,o])=>({object:i,...o,expiresAt:w(o.expiresAt)})))}c(tt,"assertWritable");function et(r,e,n){const s=[],t=c((i,o)=>{o&&s.push(M(`${i}:${o}`))},"add");if(t("card",e.card),t("ds",e.ds),e.plan&&(t("plan",e.plan),r==="plan-knowledge"&&t("knowledge",e.plan)),e.spec&&r==="build"&&t("spec",e.spec),r==="card-save"&&e.file)try{t("card",n(e.file)?.id)}catch{}return[...new Set(s)]}c(et,"objectsForCommand");export{y as CollabConflictError,C as DEFAULT_TTL_SECONDS,H as addTask,tt as assertWritable,Q as claim,G as closeBoard,E as collabDir,W as currentWorker,M as normalizeObject,Y as note,et as objectsForCommand,O as parseObjects,V as release,U as startBoard,Z as status,P as updateTask};