dsh-plugin-agentflow 0.0.0-stage → 0.2.1
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/README.md +88 -2
- package/cordis.patch.yml +195 -0
- package/index.js +448 -0
- package/lib/bridge.js +858 -0
- package/lib/client.js +4 -0
- package/package.json +42 -4
- package/runtime/boards.mjs +486 -0
- package/runtime/bridge.mjs +230 -0
- package/runtime/dsh-source.mjs +289 -0
- package/runtime/engine.mjs +352 -0
- package/runtime/index.html +3299 -0
- package/runtime/llm.mjs +252 -0
- package/runtime/mcp.mjs +823 -0
- package/runtime/preset-gen.mjs +1117 -0
- package/runtime/presets.mjs +399 -0
- package/runtime/run.mjs +76 -0
- package/runtime/server.mjs +1151 -0
- package/runtime/skills.mjs +131 -0
- package/runtime/store.mjs +103 -0
- package/runtime/verify.mjs +219 -0
- package/runtime/web/dist/assets/index-CkM6YPl4.js +348 -0
- package/runtime/web/dist/assets/index-DEZcpxgr.css +1 -0
- package/runtime/web/dist/index.html +13 -0
- package/scripts/build-client.mjs +36 -0
- package/scripts/sync-runtime.mjs +84 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({ id: "dsh-plugin-agentflow", factory: (require) => {
|
|
2
|
+
var module = { exports: {} }; var exports = module.exports;
|
|
3
|
+
var k=Object.defineProperty;var B=Object.getOwnPropertyDescriptor;var A=Object.getOwnPropertyNames;var F=Object.prototype.hasOwnProperty;var j=(e,n)=>{for(var o in n)k(e,o,{get:n[o],enumerable:!0})},L=(e,n,o,r)=>{if(n&&typeof n=="object"||typeof n=="function")for(let l of A(n))!F.call(e,l)&&l!==o&&k(e,l,{get:()=>n[l],enumerable:!(r=B(n,l))||r.enumerable});return e};var D=e=>L(k({},"__esModule",{value:!0}),e);var nt={};j(nt,{apply:()=>G,inject:()=>Y,name:()=>M});module.exports=D(nt);var t=require("react"),x="agentflow",H=30;function U(){let[e,n]=(0,t.useState)(w),o=(0,t.useRef)(!1);return(0,t.useEffect)(()=>(S.add(n),()=>{S.delete(n)}),[]),(0,t.createElement)("iframe",{src:E(e),onLoad:()=>{try{fetch("http://127.0.0.1:8791/api/plugin-alive?stage=iframe-load",{mode:"no-cors"}).catch(()=>{})}catch{}},onError:()=>{try{fetch("http://127.0.0.1:8791/api/plugin-alive?stage=iframe-error",{mode:"no-cors"}).catch(()=>{})}catch{}},title:"AgentFlow",style:{width:"100%",height:"100%",border:"none",display:"block",background:"#f7f8fa"}})}function W(e){let n=typeof e.size=="number"?e.size:16;return(0,t.createElement)("svg",{width:n,height:n,viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:1.4,strokeLinecap:"round",strokeLinejoin:"round"},(0,t.createElement)("rect",{x:1.5,y:6,width:4,height:4,rx:1}),(0,t.createElement)("rect",{x:10.5,y:1.5,width:4,height:4,rx:1}),(0,t.createElement)("rect",{x:10.5,y:10.5,width:4,height:4,rx:1}),(0,t.createElement)("path",{d:"M5.5 8h2.5V3.5h2.5M8 8v4.5h2.5"}))}var p="http://127.0.0.1:8791",b=null,w="",S=new Set;function N(e){w=String(e||"");for(let n of S)try{n(w)}catch{}}function _(){try{if(document.body&&document.body.hasAttribute("data-ds-dark-theme")||document.documentElement.classList.contains("dark"))return"dark"}catch{}try{if(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches)return"dark"}catch{}return"light"}function E(e){let n=new URLSearchParams;return e&&n.set("board",e),n.set("theme",_()),`${p}/?${n.toString()}`}function O(){try{if(b&&typeof b.selectPanel=="function")return b.selectPanel(x),!0}catch{}return!1}var M="agentflow",Y=["slots","layout"];function m(e,n){try{let o=`${e}: ${n?.stack||String(n)}`;fetch(`${p}/api/plugin-alive?stage=apply-error&detail=${encodeURIComponent(o.slice(0,800))}`,{mode:"no-cors"}).catch(()=>{})}catch{}}function G(e,n){try{J(e,n)}catch(o){m("apply",o),console.error("[agentflow] \u5BA2\u6237\u7AEF\u63D2\u4EF6\u521D\u59CB\u5316\u5931\u8D25\uFF1A",o)}}function J(e,n){n&&typeof n.agentflowUrl=="string"&&n.agentflowUrl&&(p=n.agentflowUrl);let o=null;try{o=e.layout??null}catch(r){m("layout",r),o=null}b=o,e.slots.inject("main",()=>e.slots.register({name:"main",key:x},U));try{e.slots.inject("settings.section",()=>e.slots.register({name:"settings.section",id:"agentflow",order:90,label:()=>"AgentFlow",inject:()=>({})},tt))}catch(r){m("settings.section",r)}try{e.slots.inject("plugins.add.actions",()=>e.slots.register({name:"plugins.add.actions",id:"agentflow-export",inject:()=>({})},et))}catch(r){m("plugins.add.actions",r)}e.slots.inject("sidebar.panellist",()=>e.slots.register({name:"sidebar.panellist",id:x,order:H,label:()=>"AgentFlow"},r=>(0,t.createElement)(W,r)))}var V={border:"1px solid rgba(128,130,120,0.28)",borderRadius:12,padding:"14px 16px",background:"rgba(128,130,120,0.06)",maxWidth:560},T={border:"none",borderRadius:8,padding:"8px 14px",fontSize:13,fontWeight:600,color:"#fff",background:"#24543F",cursor:"pointer"},q={...T,opacity:.55,cursor:"not-allowed"};function C(){let[e,n]=(0,t.useState)(!1),[o,r]=(0,t.useState)(null),[l,s]=(0,t.useState)(null);return{busy:e,result:o,error:l,run:()=>{e||(n(!0),s(null),r(null),fetch(`${p}/api/export/preset`,{method:"POST",headers:{"content-type":"application/json"},body:"{}"}).then(async c=>{let f=await c.json().catch(()=>({}));if(!c.ok)throw new Error(f.error||`HTTP ${c.status}`);return f}).then(c=>r(c)).catch(c=>s(String(c&&c.message||c))).finally(()=>n(!1)))}}}function K(){let[e,n]=(0,t.useState)(null),[o,r]=(0,t.useState)(null),l=()=>{r(null),fetch(`${p}/api/board`).then(async s=>{if(!s.ok)throw new Error(`HTTP ${s.status}`);return await s.json()}).then(n).catch(s=>r(String(s&&s.message||s)))};return(0,t.useEffect)(l,[]),{board:e,error:o,reload:l}}function Q({busy:e,result:n,error:o}){return o?(0,t.createElement)("div",{style:{marginTop:10,fontSize:12,color:"#B4451F",lineHeight:1.6}},`\u5BFC\u51FA\u5931\u8D25\uFF1A${o}`):e?(0,t.createElement)("div",{style:{marginTop:10,fontSize:12,opacity:.7}},"\u6B63\u5728\u5199\u5165 profile\u2026"):n?(0,t.createElement)("div",{style:{marginTop:10,fontSize:12,lineHeight:1.7}},(0,t.createElement)("div",null,`\u5DF2${n.replaced?"\u66F4\u65B0":"\u5199\u5165"}\u9884\u8BBE `,(0,t.createElement)("b",null,n.presetId||"")),(0,t.createElement)("div",{style:{opacity:.7,wordBreak:"break-all"}},n.file||""),(0,t.createElement)("div",{style:{marginTop:4,fontWeight:600}},"\u91CD\u542F DSH \u540E\u751F\u6548")):null}function X(){let[e,n]=(0,t.useState)(null),[o,r]=(0,t.useState)(null),[l,s]=(0,t.useState)(null),g=()=>{r(null),fetch(`${p}/api/presets`).then(async a=>{if(!a.ok)throw new Error(`HTTP ${a.status}`);let i=await a.json();if(i.error)throw new Error(i.error);n(i.presets||[])}).catch(a=>r(String(a&&a.message||a)))};return(0,t.useEffect)(g,[]),{presets:e,error:o,busyId:l,reload:g,copy:a=>{s(a.id),r(null),fetch(`${p}/api/presets/${encodeURIComponent(a.id)}/copy`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({name:`${a.name} \u526F\u672C`})}).then(async i=>{let u=await i.json().catch(()=>({}));if(!i.ok)throw new Error(u.error||`HTTP ${i.status}`);return u}).then(()=>g()).catch(i=>r(String(i&&i.message||i))).finally(()=>s(null))},remove:a=>{window.confirm(`\u5220\u9664\u9884\u8BBE\u300C${a.name}\u300D\uFF1F\u53EA\u5220 profile \u91CC\u7684\u8FD9\u4E00\u884C\uFF0C\u91CD\u542F DSH \u751F\u6548\u3002`)&&(s(a.id),r(null),fetch(`${p}/api/presets/${encodeURIComponent(a.id)}`,{method:"DELETE"}).then(async i=>{let u=await i.json().catch(()=>({}));if(!i.ok)throw new Error(u.error||`HTTP ${i.status}`);return u}).then(()=>g()).catch(i=>r(String(i&&i.message||i))).finally(()=>s(null)))}}}var Z={display:"inline-block",padding:"1px 7px",borderRadius:999,fontSize:11,border:"1px solid rgba(128,130,120,0.35)",opacity:.85};function h({children:e,onClick:n,disabled:o,danger:r}){return(0,t.createElement)("button",{type:"button",onClick:n,disabled:o,style:{border:`1px solid ${r?"rgba(180,69,31,0.45)":"rgba(128,130,120,0.35)"}`,borderRadius:7,padding:"4px 10px",fontSize:12,background:"transparent",color:r?"#B4451F":"inherit",cursor:o?"not-allowed":"pointer",opacity:o?.5:1}},e)}function tt(e){let[n,o]=(0,t.useState)(!1),r=d=>{if(N(d),O()){try{typeof e?.close=="function"&&e.close()}catch{}return}o(!0)},{board:l,error:s,reload:g}=K(),{busy:c,result:f,error:a,run:i}=C(),{presets:u,error:v,busyId:y,reload:$,copy:I,remove:P}=X(),R=l?.nodes?.length??0,z=l?.edges?.length??0;return(0,t.createElement)("div",{style:V},n?(0,t.createElement)("div",{style:{position:"fixed",inset:0,zIndex:9999,background:"rgba(0,0,0,0.45)",display:"flex",flexDirection:"column"}},(0,t.createElement)("div",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",padding:"8px 12px",background:"#17181B",color:"#EDEDE8",fontSize:13}},(0,t.createElement)("span",null,"AgentFlow \u753B\u5E03"),(0,t.createElement)("button",{type:"button",onClick:()=>o(!1),style:{border:"1px solid rgba(255,255,255,0.25)",borderRadius:7,padding:"4px 12px",fontSize:12,background:"transparent",color:"inherit",cursor:"pointer"}},"\u5173\u95ED")),(0,t.createElement)("iframe",{src:E(w),title:"AgentFlow \u753B\u5E03",style:{flex:1,width:"100%",border:"none",background:"#F4F5F2"}})):null,(0,t.createElement)("div",{style:{fontSize:14,fontWeight:600,marginBottom:6}},"AgentFlow \u753B\u5E03 \u2192 \u9884\u8BBE"),(0,t.createElement)("div",{style:{fontSize:12.5,lineHeight:1.7,opacity:.78}},s?`\u8BFB\u4E0D\u5230\u753B\u5E03\uFF1A${s}\uFF08\u786E\u8BA4 AgentFlow \u670D\u52A1\u5728 ${p} \u8FD0\u884C\uFF09`:l?`\u5F53\u524D\u753B\u5E03\u300C${l.name||"\u672A\u547D\u540D"}\u300D${R} \u4E2A\u8282\u70B9 / ${z} \u6761\u8FDE\u7EBF`:"\u6B63\u5728\u8BFB\u53D6\u753B\u5E03\u2026"),(0,t.createElement)("div",{style:{marginTop:12,display:"flex",gap:10,alignItems:"center"}},(0,t.createElement)("button",{type:"button",onClick:i,disabled:c||s!==null,style:c||s!==null?q:T},c?"\u5BFC\u51FA\u4E2D\u2026":"\u5BFC\u51FA\u5F53\u524D\u753B\u5E03\u4E3A\u9884\u8BBE"),(0,t.createElement)("button",{type:"button",onClick:g,style:{border:"1px solid rgba(128,130,120,0.35)",borderRadius:8,padding:"7px 12px",fontSize:12.5,background:"transparent",color:"inherit",cursor:"pointer"}},"\u5237\u65B0\u753B\u5E03\u4FE1\u606F")),(0,t.createElement)("div",{style:{marginTop:16,borderTop:"1px solid rgba(128,130,120,0.25)",paddingTop:12}},(0,t.createElement)("div",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:8}},(0,t.createElement)("div",{style:{fontSize:13,fontWeight:600}},"\u81EA\u5B9A\u4E49\u9884\u8BBE"),h({children:"\u5237\u65B0",onClick:$,disabled:y!==null})),v?(0,t.createElement)("div",{style:{fontSize:12,color:"#B4451F"}},`\u8BFB\u53D6\u5931\u8D25\uFF1A${v}`):null,u?u.length===0?(0,t.createElement)("div",{style:{fontSize:12,opacity:.7}},"\u8FD8\u6CA1\u6709\u81EA\u5B9A\u4E49\u9884\u8BBE"):(0,t.createElement)("div",{style:{display:"flex",flexDirection:"column",gap:8}},u.map(d=>(0,t.createElement)("div",{key:d.id,style:{display:"flex",alignItems:"center",gap:10,padding:"8px 10px",borderRadius:9,background:"rgba(128,130,120,0.08)"}},(0,t.createElement)("div",{style:{minWidth:0,flex:1}},(0,t.createElement)("div",{style:{fontSize:13,fontWeight:600}},d.name),(0,t.createElement)("div",{style:{fontSize:11,opacity:.65,display:"flex",gap:6,flexWrap:"wrap",marginTop:2}},(0,t.createElement)("span",null,d.id),(0,t.createElement)("span",{style:Z},d.source==="profile"?"profile":d.packageName||"\u63D2\u4EF6\u5305"))),h({children:y===d.id?"\u5904\u7406\u4E2D\u2026":"\u590D\u5236",onClick:()=>I(d),disabled:y!==null}),d.editable?h({children:"\u5220\u9664",onClick:()=>P(d),disabled:y!==null,danger:!0}):null,h({children:"\u7F16\u8F91",onClick:()=>r(d.id)})))):(0,t.createElement)("div",{style:{fontSize:12,opacity:.7}},"\u8BFB\u53D6\u4E2D\u2026"),(0,t.createElement)("div",{style:{fontSize:11,opacity:.6,marginTop:8,lineHeight:1.6}},"\u63D2\u4EF6\u5305\u81EA\u5E26\u7684\u9884\u8BBE\uFF08Sharp Agent / \u7FA4\u53CB\u8154\uFF09\u53EA\u80FD\u590D\u5236\u6210\u81EA\u5DF1\u7684\u518D\u6539\uFF1B\u5220\u9664\u4E0E\u7F16\u8F91\u53EA\u5BF9 profile \u91CC\u7684\u9884\u8BBE\u5F00\u653E\u3002\u6539\u5B8C\u8BB0\u5F97\u91CD\u542F DSH\u3002")),(0,t.createElement)(Q,{busy:c,result:f,error:a}))}function et(){let{busy:e,error:n,run:o}=C();return(0,t.createElement)("button",{type:"button",onClick:o,disabled:e,style:{display:"block",width:"100%",textAlign:"left",border:"none",background:"transparent",color:"inherit",padding:"8px 12px",fontSize:13,cursor:e?"not-allowed":"pointer",opacity:e?.6:1}},e?"\u6B63\u5728\u5BFC\u51FA AgentFlow \u9884\u8BBE\u2026":"\u4ECE AgentFlow \u753B\u5E03\u751F\u6210\u9884\u8BBE",n?(0,t.createElement)("span",{style:{display:"block",fontSize:11,color:"#B4451F"}},n):null)}
|
|
4
|
+
return module.exports; } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-agentflow",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "DeepSeek Harness tool plugin: run AgentFlow (material-canvas agent orchestrator) boards from the model",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"check": "node --check index.js && node --check lib/bridge.js && node --check scripts/sync-runtime.mjs",
|
|
8
|
+
"test": "node test/bridge.selftest.mjs",
|
|
9
|
+
"build": "node scripts/build-client.mjs",
|
|
10
|
+
"sync": "node scripts/sync-runtime.mjs"
|
|
11
|
+
},
|
|
12
|
+
"author": "",
|
|
13
|
+
"license": "ISC",
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@deepseek-ai/cordis": "^4.0.5",
|
|
16
|
+
"@deepseek-ai/dsh-tools": "^0.2.1-rc.2"
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"files": [
|
|
20
|
+
"index.js",
|
|
21
|
+
"lib/**/*.js",
|
|
22
|
+
"runtime/**",
|
|
23
|
+
"cordis.patch.yml",
|
|
24
|
+
"scripts/**/*.mjs",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"dsh": {
|
|
28
|
+
"bundle": {
|
|
29
|
+
"patch": "./cordis.patch.yml"
|
|
30
|
+
},
|
|
31
|
+
"client": {
|
|
32
|
+
"inject": [],
|
|
33
|
+
"platform": "web"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"esbuild": "^0.28.2"
|
|
38
|
+
},
|
|
39
|
+
"exports": {
|
|
40
|
+
".": "./index.js",
|
|
41
|
+
"./client": "./lib/client.js",
|
|
42
|
+
"./package.json": "./package.json"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
// boards.mjs —— 多画布存储层(每个自定义预设一张画布)。
|
|
2
|
+
//
|
|
3
|
+
// 索引:<DATA_DIR>/boards.json
|
|
4
|
+
// 结构见 docs/boards-contract.md:{ order: [id...], boards: { id: {id,name,presetId,createdAt,updatedAt} } }
|
|
5
|
+
// 画布:<DATA_DIR>/boards/<id>.json,内容就是 Board(id/name/nodes/edges)。
|
|
6
|
+
//
|
|
7
|
+
// 迁移:boards.json 不存在且 <DATA_DIR>/board.json 存在 → 复制成 flow-agentflow,原文件不动。
|
|
8
|
+
// 过渡:flow-agentflow 与旧 board.json 做 mtime 仲裁的兼容 ——
|
|
9
|
+
// 旧文件比画布文件新(说明有人在直接写 board.json)时,先导入再读写;
|
|
10
|
+
// 一旦发生过这种导入,后续保存会同步镜像回旧文件(否则旧测试/旧前端读不到结果)。
|
|
11
|
+
// 迁移后没人动旧文件时,旧文件永远保持原样。
|
|
12
|
+
|
|
13
|
+
import fs from 'node:fs/promises';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { dataDir, writeJsonAtomic, exclusive } from './store.mjs';
|
|
16
|
+
|
|
17
|
+
export const FLOW_BOARD_ID = 'flow-agentflow';
|
|
18
|
+
|
|
19
|
+
// id 会拼进文件路径:只允许字母数字开头,禁止 .. 之类
|
|
20
|
+
const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
21
|
+
|
|
22
|
+
/** 带 HTTP 状态码的错误,server.mjs 直接把它翻成响应码。 */
|
|
23
|
+
export function httpError(status, message) {
|
|
24
|
+
return Object.assign(new Error(message), { status });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function boardsDir() {
|
|
28
|
+
return path.join(dataDir(), 'boards');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function indexFile() {
|
|
32
|
+
return path.join(dataDir(), 'boards.json');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 旧单画布文件(迁移来源 / 过渡兼容目标) */
|
|
36
|
+
export function legacyBoardFile() {
|
|
37
|
+
return path.join(dataDir(), 'board.json');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function boardFile(id) {
|
|
41
|
+
return path.join(boardsDir(), `${id}.json`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 草稿目录:手动保存前的未落定修改,一张画布一份 */
|
|
45
|
+
export function draftsDir() {
|
|
46
|
+
return path.join(dataDir(), 'drafts');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function draftFile(id) {
|
|
50
|
+
return path.join(draftsDir(), `${id}.json`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isValidBoardId(id) {
|
|
54
|
+
return typeof id === 'string' && ID_RE.test(id) && !id.includes('..');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function exists(file) {
|
|
58
|
+
try {
|
|
59
|
+
await fs.access(file);
|
|
60
|
+
return true;
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function mtimeMs(file) {
|
|
67
|
+
try {
|
|
68
|
+
return (await fs.stat(file)).mtimeMs;
|
|
69
|
+
} catch {
|
|
70
|
+
return -1;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function readJson(file) {
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(await fs.readFile(file, 'utf8'));
|
|
77
|
+
} catch (err) {
|
|
78
|
+
if (err.code === 'ENOENT') return null;
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------- 索引 ----------
|
|
84
|
+
|
|
85
|
+
function emptyIndex() {
|
|
86
|
+
return { order: [], boards: {} };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 容错:剔除 order 里不存在的 id、补上 boards 里有但 order 漏了的 id。 */
|
|
90
|
+
function normalizeIndex(raw) {
|
|
91
|
+
const source = raw && typeof raw === 'object' ? raw : {};
|
|
92
|
+
const boards = source.boards && typeof source.boards === 'object' ? source.boards : {};
|
|
93
|
+
const seen = new Set();
|
|
94
|
+
const order = [];
|
|
95
|
+
for (const rawId of Array.isArray(source.order) ? source.order : []) {
|
|
96
|
+
const id = String(rawId);
|
|
97
|
+
if (!boards[id] || seen.has(id)) continue;
|
|
98
|
+
seen.add(id);
|
|
99
|
+
order.push(id);
|
|
100
|
+
}
|
|
101
|
+
const missing = Object.keys(boards)
|
|
102
|
+
.filter((id) => !seen.has(id))
|
|
103
|
+
.sort((a, b) => (boards[a]?.createdAt || 0) - (boards[b]?.createdAt || 0));
|
|
104
|
+
order.push(...missing);
|
|
105
|
+
return { order, boards };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function readIndex() {
|
|
109
|
+
return normalizeIndex(await readJson(indexFile()));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function writeIndex(index) {
|
|
113
|
+
const normalized = normalizeIndex(index);
|
|
114
|
+
await writeJsonAtomic(indexFile(), normalized);
|
|
115
|
+
return normalized;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------- 迁移 / 旧文件过渡 ----------
|
|
119
|
+
|
|
120
|
+
async function readLegacyBoard() {
|
|
121
|
+
return readJson(legacyBoardFile());
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 迁移只跑一次:索引存在就直接返回;原 board.json 一个字节都不改。 */
|
|
125
|
+
export async function migrateIfNeeded() {
|
|
126
|
+
return exclusive(async () => {
|
|
127
|
+
if (await exists(indexFile())) return { migrated: false };
|
|
128
|
+
const legacy = await readLegacyBoard();
|
|
129
|
+
if (!legacy) return { migrated: false };
|
|
130
|
+
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
const board = {
|
|
133
|
+
...legacy,
|
|
134
|
+
id: FLOW_BOARD_ID,
|
|
135
|
+
name: legacy.name || 'AgentFlow',
|
|
136
|
+
nodes: Array.isArray(legacy.nodes) ? legacy.nodes : [],
|
|
137
|
+
edges: Array.isArray(legacy.edges) ? legacy.edges : [],
|
|
138
|
+
};
|
|
139
|
+
const meta = {
|
|
140
|
+
id: FLOW_BOARD_ID,
|
|
141
|
+
name: board.name,
|
|
142
|
+
presetId: FLOW_BOARD_ID,
|
|
143
|
+
createdAt: now,
|
|
144
|
+
updatedAt: now,
|
|
145
|
+
};
|
|
146
|
+
await writeJsonAtomic(boardFile(FLOW_BOARD_ID), board);
|
|
147
|
+
await writeJsonAtomic(indexFile(), { order: [FLOW_BOARD_ID], boards: { [FLOW_BOARD_ID]: meta } });
|
|
148
|
+
return { migrated: true, board, meta };
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 发生过旧文件导入的 id:之后保存时把内容镜像回旧文件(仅过渡期用)
|
|
153
|
+
const legacyActive = new Set();
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 旧 board.json 比 flow-agentflow 画布文件新时,按旧文件为准导入一次。
|
|
157
|
+
* 只在 exclusive 内部调用,调用方不需要再上锁。
|
|
158
|
+
*/
|
|
159
|
+
async function importLegacyIfNewerUnlocked(id) {
|
|
160
|
+
if (id !== FLOW_BOARD_ID) return false;
|
|
161
|
+
const legacyMs = await mtimeMs(legacyBoardFile());
|
|
162
|
+
if (legacyMs < 0) return false;
|
|
163
|
+
if ((await mtimeMs(boardFile(id))) >= legacyMs) return false;
|
|
164
|
+
|
|
165
|
+
const legacy = await readLegacyBoard();
|
|
166
|
+
if (!legacy) return false;
|
|
167
|
+
|
|
168
|
+
const board = {
|
|
169
|
+
...legacy,
|
|
170
|
+
id,
|
|
171
|
+
name: legacy.name || 'AgentFlow',
|
|
172
|
+
nodes: Array.isArray(legacy.nodes) ? legacy.nodes : [],
|
|
173
|
+
edges: Array.isArray(legacy.edges) ? legacy.edges : [],
|
|
174
|
+
};
|
|
175
|
+
await writeJsonAtomic(boardFile(id), board);
|
|
176
|
+
|
|
177
|
+
const index = await readIndex();
|
|
178
|
+
const now = Date.now();
|
|
179
|
+
const meta = index.boards[id] || {
|
|
180
|
+
id,
|
|
181
|
+
name: board.name,
|
|
182
|
+
presetId: FLOW_BOARD_ID,
|
|
183
|
+
createdAt: now,
|
|
184
|
+
updatedAt: now,
|
|
185
|
+
};
|
|
186
|
+
meta.name = board.name;
|
|
187
|
+
meta.updatedAt = now;
|
|
188
|
+
index.boards[id] = meta;
|
|
189
|
+
if (!index.order.includes(id)) index.order.push(id);
|
|
190
|
+
await writeIndex(index);
|
|
191
|
+
|
|
192
|
+
legacyActive.add(id);
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---------- 画布 ----------
|
|
197
|
+
|
|
198
|
+
async function readBoardRaw(id) {
|
|
199
|
+
if (!isValidBoardId(id)) return null;
|
|
200
|
+
return readJson(boardFile(id));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** 读一张画布;不存在 / id 非法返回 null。 */
|
|
204
|
+
export async function getBoard(id) {
|
|
205
|
+
if (!isValidBoardId(id)) return null;
|
|
206
|
+
return exclusive(async () => {
|
|
207
|
+
await importLegacyIfNewerUnlocked(id);
|
|
208
|
+
return readBoardRaw(id);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** 列表:索引顺序 + 每张的 nodeCount / 是否有草稿。 */
|
|
213
|
+
export async function listBoards() {
|
|
214
|
+
const index = await readIndex();
|
|
215
|
+
const withDraft = await draftIdSet();
|
|
216
|
+
const out = [];
|
|
217
|
+
for (const id of index.order) {
|
|
218
|
+
const meta = index.boards[id];
|
|
219
|
+
const board = await readBoardRaw(id);
|
|
220
|
+
out.push({
|
|
221
|
+
id,
|
|
222
|
+
name: board?.name || meta?.name || id,
|
|
223
|
+
presetId: meta?.presetId ?? null,
|
|
224
|
+
nodeCount: Array.isArray(board?.nodes) ? board.nodes.length : 0,
|
|
225
|
+
updatedAt: meta?.updatedAt || meta?.createdAt || 0,
|
|
226
|
+
hasDraft: withDraft.has(id),
|
|
227
|
+
// 改了画布但还没点「保存」(预设没跟着更新)
|
|
228
|
+
presetOutdated: Boolean(meta?.presetOutdated),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** 默认画布 = 索引里的第一张。找不到返回 null。 */
|
|
235
|
+
export async function resolveBoard(id) {
|
|
236
|
+
const wanted = id ? String(id) : (await readIndex()).order[0];
|
|
237
|
+
if (!wanted) return null;
|
|
238
|
+
const board = await getBoard(wanted);
|
|
239
|
+
return board ? { id: wanted, board } : null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function slugify(name) {
|
|
243
|
+
const s = String(name || '')
|
|
244
|
+
.toLowerCase()
|
|
245
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
246
|
+
.replace(/^-+|-+$/g, '');
|
|
247
|
+
return s || 'board';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** 新建画布:presetId 有值就用它当 id(冲突报 409),否则由名字生成唯一 id。 */
|
|
251
|
+
export async function createBoard({ name, presetId } = {}) {
|
|
252
|
+
const safeName = String(name || '').trim() || '未命名画布';
|
|
253
|
+
const wantedPreset = presetId === undefined || presetId === null || presetId === ''
|
|
254
|
+
? null
|
|
255
|
+
: String(presetId);
|
|
256
|
+
if (wantedPreset && !isValidBoardId(wantedPreset)) {
|
|
257
|
+
throw httpError(400, `预设 id 不合法:${wantedPreset}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return exclusive(async () => {
|
|
261
|
+
const index = await readIndex();
|
|
262
|
+
let id;
|
|
263
|
+
if (wantedPreset) {
|
|
264
|
+
if (index.boards[wantedPreset] || (await exists(boardFile(wantedPreset)))) {
|
|
265
|
+
throw httpError(409, `画布已存在:${wantedPreset}`);
|
|
266
|
+
}
|
|
267
|
+
id = wantedPreset;
|
|
268
|
+
} else {
|
|
269
|
+
const base = slugify(safeName);
|
|
270
|
+
id = base;
|
|
271
|
+
let n = 2;
|
|
272
|
+
while (index.boards[id] || (await exists(boardFile(id)))) id = `${base}-${n++}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const now = Date.now();
|
|
276
|
+
const board = { id, name: safeName, nodes: [], edges: [] };
|
|
277
|
+
const meta = { id, name: safeName, presetId: wantedPreset, createdAt: now, updatedAt: now };
|
|
278
|
+
await writeJsonAtomic(boardFile(id), board);
|
|
279
|
+
await writeIndex({
|
|
280
|
+
order: [...index.order, id],
|
|
281
|
+
boards: { ...index.boards, [id]: meta },
|
|
282
|
+
});
|
|
283
|
+
return { board, meta };
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** 持久化一张画布(读-改-写用同一个锁)。opts.clearDraft:正式保存后清草稿。 */
|
|
288
|
+
async function persistUnlocked(id, board, opts = {}) {
|
|
289
|
+
const index = await readIndex();
|
|
290
|
+
const now = Date.now();
|
|
291
|
+
const prev = index.boards[id];
|
|
292
|
+
const meta = prev
|
|
293
|
+
? { ...prev }
|
|
294
|
+
: { id, name: board.name || id, presetId: id === FLOW_BOARD_ID ? FLOW_BOARD_ID : null, createdAt: now, updatedAt: now };
|
|
295
|
+
|
|
296
|
+
const next = { ...board, id };
|
|
297
|
+
if (legacyActive.has(id) && id === FLOW_BOARD_ID) {
|
|
298
|
+
// 先写旧文件、后写画布文件:保证画布文件 mtime 更新,不会触发下一次导入
|
|
299
|
+
await writeJsonAtomic(legacyBoardFile(), next);
|
|
300
|
+
}
|
|
301
|
+
await writeJsonAtomic(boardFile(id), next);
|
|
302
|
+
|
|
303
|
+
if (next.name) meta.name = next.name;
|
|
304
|
+
meta.updatedAt = now;
|
|
305
|
+
// 画布内容变了,预设就落后了:列表上要能提示「预设未更新」。
|
|
306
|
+
// 导出预设成功后由 markPresetSynced 清掉。
|
|
307
|
+
if (!opts.keepPresetSynced) meta.presetOutdated = true;
|
|
308
|
+
index.boards[id] = meta;
|
|
309
|
+
if (!index.order.includes(id)) index.order.push(id);
|
|
310
|
+
await writeIndex(index);
|
|
311
|
+
// 只有「正式保存」才让草稿失效:节点 patch / 跑图结果也走底层的 mutateBoardById,
|
|
312
|
+
// 那些不该顺手丢掉用户还没保存的草稿。
|
|
313
|
+
if (opts.clearDraft) await removeDraftUnlocked(id);
|
|
314
|
+
return { ok: true, meta, board: next };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** 读-改-写一张画布;画布不存在抛 404。opts.clearDraft 透传给 persistUnlocked。 */
|
|
318
|
+
export async function mutateBoardById(id, fn, opts = {}) {
|
|
319
|
+
if (!isValidBoardId(id)) throw httpError(404, `画布不存在:${id}`);
|
|
320
|
+
return exclusive(async () => {
|
|
321
|
+
await importLegacyIfNewerUnlocked(id);
|
|
322
|
+
const index = await readIndex();
|
|
323
|
+
const existing = await readBoardRaw(id);
|
|
324
|
+
if (!existing && !index.boards[id]) throw httpError(404, `画布不存在:${id}`);
|
|
325
|
+
const base = existing || { id, name: index.boards[id]?.name || id, nodes: [], edges: [] };
|
|
326
|
+
const next = await fn(base);
|
|
327
|
+
if (!next || typeof next !== 'object' || Array.isArray(next)) {
|
|
328
|
+
throw new Error('mutateBoardById 的回调必须返回画布对象');
|
|
329
|
+
}
|
|
330
|
+
return persistUnlocked(id, next, opts);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** 整体保存一张画布(PUT /api/boards/:id):正式保存,成功后草稿失效。 */
|
|
335
|
+
export async function saveBoard(id, board) {
|
|
336
|
+
if (!board || typeof board !== 'object' || Array.isArray(board)) {
|
|
337
|
+
throw httpError(400, '缺少画布数据');
|
|
338
|
+
}
|
|
339
|
+
const out = await mutateBoardById(id, () => ({ ...board, id }), { clearDraft: true });
|
|
340
|
+
return { ok: true, meta: out.meta };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** 导出预设成功后调用:画布与预设重新对齐,清掉「预设未更新」。 */
|
|
344
|
+
export async function markPresetSynced(id) {
|
|
345
|
+
if (!isValidBoardId(id)) return { ok: false };
|
|
346
|
+
return exclusive(async () => {
|
|
347
|
+
const index = await readIndex();
|
|
348
|
+
const meta = index.boards[id];
|
|
349
|
+
if (meta) {
|
|
350
|
+
meta.presetOutdated = false;
|
|
351
|
+
index.boards[id] = meta;
|
|
352
|
+
await writeIndex(index);
|
|
353
|
+
}
|
|
354
|
+
return { ok: true };
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** 改元信息(目前只有 name)。 */
|
|
359
|
+
export async function renameBoard(id, name) {
|
|
360
|
+
if (!isValidBoardId(id)) throw httpError(404, `画布不存在:${id}`);
|
|
361
|
+
const safeName = String(name ?? '').trim();
|
|
362
|
+
if (!safeName) throw httpError(400, '画布名不能为空');
|
|
363
|
+
|
|
364
|
+
return exclusive(async () => {
|
|
365
|
+
const index = await readIndex();
|
|
366
|
+
const meta = index.boards[id];
|
|
367
|
+
if (!meta) throw httpError(404, `画布不存在:${id}`);
|
|
368
|
+
const board = await readBoardRaw(id);
|
|
369
|
+
if (board) {
|
|
370
|
+
board.name = safeName;
|
|
371
|
+
await writeJsonAtomic(boardFile(id), board);
|
|
372
|
+
}
|
|
373
|
+
meta.name = safeName;
|
|
374
|
+
meta.updatedAt = Date.now();
|
|
375
|
+
index.boards[id] = meta;
|
|
376
|
+
await writeIndex(index);
|
|
377
|
+
return { ok: true, meta };
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** 删除画布;最后一张不许删。 */
|
|
382
|
+
export async function deleteBoard(id) {
|
|
383
|
+
if (!isValidBoardId(id)) throw httpError(404, `画布不存在:${id}`);
|
|
384
|
+
return exclusive(async () => {
|
|
385
|
+
const index = await readIndex();
|
|
386
|
+
if (!index.boards[id]) throw httpError(404, `画布不存在:${id}`);
|
|
387
|
+
if (index.order.length <= 1) throw httpError(400, '至少要保留一张画布');
|
|
388
|
+
|
|
389
|
+
const order = index.order.filter((x) => x !== id);
|
|
390
|
+
const boards = { ...index.boards };
|
|
391
|
+
delete boards[id];
|
|
392
|
+
// 先落索引再删文件:中途崩溃最多留下孤儿文件,不会出现索引指向缺失文件
|
|
393
|
+
await writeIndex({ order, boards });
|
|
394
|
+
await fs.rm(boardFile(id), { force: true });
|
|
395
|
+
await removeDraftUnlocked(id);
|
|
396
|
+
legacyActive.delete(id);
|
|
397
|
+
return { ok: true, id };
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ---------- 草稿(手动保存 + 未保存修改) ----------
|
|
402
|
+
//
|
|
403
|
+
// 画布定位收窄成「预设工作流配置器」后,编辑不再自动落盘:
|
|
404
|
+
// 前端把未保存的修改 PUT 到草稿,正式保存时草稿清掉。
|
|
405
|
+
// 文件:<DATA_DIR>/drafts/<id>.json,内容 { board, updatedAt }。
|
|
406
|
+
|
|
407
|
+
/** 扫目录拿有草稿的画布 id(只看文件名,不读内容)。 */
|
|
408
|
+
async function draftIdSet() {
|
|
409
|
+
try {
|
|
410
|
+
const entries = await fs.readdir(draftsDir());
|
|
411
|
+
return new Set(
|
|
412
|
+
entries
|
|
413
|
+
.filter((name) => name.endsWith('.json'))
|
|
414
|
+
.map((name) => name.slice(0, -'.json'.length)),
|
|
415
|
+
);
|
|
416
|
+
} catch (err) {
|
|
417
|
+
if (err.code === 'ENOENT') return new Set();
|
|
418
|
+
throw err;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** 是否有草稿;id 非法按没有处理。 */
|
|
423
|
+
export async function hasDraft(id) {
|
|
424
|
+
return isValidBoardId(id) && (await exists(draftFile(id)));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** 读草稿:返回 { board, updatedAt },没有 / id 非法返回 null。 */
|
|
428
|
+
export async function getDraft(id) {
|
|
429
|
+
if (!isValidBoardId(id)) return null;
|
|
430
|
+
const rec = await readJson(draftFile(id));
|
|
431
|
+
if (!rec || typeof rec !== 'object' || !rec.board) return null;
|
|
432
|
+
return { board: rec.board, updatedAt: Number(rec.updatedAt) || 0 };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** 写草稿(原子写)。画布必须存在,否则 404。 */
|
|
436
|
+
export async function saveDraft(id, board) {
|
|
437
|
+
if (!isValidBoardId(id)) throw httpError(404, `画布不存在:${id}`);
|
|
438
|
+
if (!board || typeof board !== 'object' || Array.isArray(board)) {
|
|
439
|
+
throw httpError(400, '缺少画布数据');
|
|
440
|
+
}
|
|
441
|
+
return exclusive(async () => {
|
|
442
|
+
const index = await readIndex();
|
|
443
|
+
if (!index.boards[id] && !(await exists(boardFile(id)))) {
|
|
444
|
+
throw httpError(404, `画布不存在:${id}`);
|
|
445
|
+
}
|
|
446
|
+
const updatedAt = Date.now();
|
|
447
|
+
await writeJsonAtomic(draftFile(id), { board: { ...board, id }, updatedAt });
|
|
448
|
+
return { ok: true, updatedAt };
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** 无锁删除草稿:只在 exclusive 内部调用(正式保存 / 删画布时顺手清)。 */
|
|
453
|
+
async function removeDraftUnlocked(id) {
|
|
454
|
+
if (!isValidBoardId(id)) return false;
|
|
455
|
+
const file = draftFile(id);
|
|
456
|
+
if (!(await exists(file))) return false;
|
|
457
|
+
await fs.rm(file, { force: true });
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** 删草稿(幂等)。 */
|
|
462
|
+
export async function deleteDraft(id) {
|
|
463
|
+
if (!isValidBoardId(id)) throw httpError(404, `画布不存在:${id}`);
|
|
464
|
+
return exclusive(async () => ({ ok: true, removed: await removeDraftUnlocked(id) }));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* 启动时调用:先迁移;一张画布都没有时用 makeBoard() 造默认的 flow-agentflow。
|
|
469
|
+
* @param {() => object} makeBoard 造默认画布内容的函数
|
|
470
|
+
*/
|
|
471
|
+
export async function ensureDefaultBoard(makeBoard) {
|
|
472
|
+
const migrated = await migrateIfNeeded();
|
|
473
|
+
const index = await readIndex();
|
|
474
|
+
if (index.order.length) {
|
|
475
|
+
return { created: false, migrated: migrated.migrated, id: index.order[0] };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const seed = typeof makeBoard === 'function' ? makeBoard() : { nodes: [], edges: [] };
|
|
479
|
+
const created = await createBoard({ name: 'AgentFlow', presetId: FLOW_BOARD_ID });
|
|
480
|
+
await saveBoard(FLOW_BOARD_ID, {
|
|
481
|
+
...seed,
|
|
482
|
+
id: FLOW_BOARD_ID,
|
|
483
|
+
name: created.board.name,
|
|
484
|
+
});
|
|
485
|
+
return { created: true, migrated: migrated.migrated, id: FLOW_BOARD_ID };
|
|
486
|
+
}
|