dsh-knj-workflow 0.1.94 → 0.1.112
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/lib/client.js +239 -82
- package/lib/graph.js +145 -34
- package/lib/graph.test.js +122 -5
- package/lib/index.js +73 -4
- package/lib/orchestrator.js +80 -34
- package/lib/orchestrator.test.js +176 -3
- package/lib/repro-zero-outedge.mjs +90 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -562,12 +562,15 @@ window.__ModuleLoader__.load({
|
|
|
562
562
|
this.scheduleCommit(); // 逐键输入防抖合并
|
|
563
563
|
const wf = this.state.wf;
|
|
564
564
|
if (wf.nodes.some((n) => n.id === newId && n.id !== oldId)) { toast('阶段编码已存在'); return; }
|
|
565
|
+
// 改 id 后同步所有引用:edges from/to、其他节点的 routes[].to / approveTo / rejectTo / displayFrom / inputs[].from
|
|
565
566
|
const nodes = wf.nodes.map((n) => {
|
|
566
|
-
if (n.id
|
|
567
|
-
const u = { ...n
|
|
567
|
+
if (n.id === oldId) return { ...n, id: newId };
|
|
568
|
+
const u = { ...n };
|
|
568
569
|
if (n.approveTo === oldId) u.approveTo = newId;
|
|
569
570
|
if (n.rejectTo === oldId) u.rejectTo = newId;
|
|
570
571
|
if (n.displayFrom === oldId) u.displayFrom = newId;
|
|
572
|
+
if (Array.isArray(n.routes)) u.routes = n.routes.map((r) => (r.to === oldId ? { ...r, to: newId } : r));
|
|
573
|
+
if (Array.isArray(n.inputs)) u.inputs = n.inputs.map((r) => (r.from === oldId ? { ...r, from: newId } : r));
|
|
571
574
|
return u;
|
|
572
575
|
});
|
|
573
576
|
const edges = wf.edges.map((e) => ({ ...e, from: e.from === oldId ? newId : e.from, to: e.to === oldId ? newId : e.to }));
|
|
@@ -581,8 +584,8 @@ window.__ModuleLoader__.load({
|
|
|
581
584
|
if (type === 'task') {
|
|
582
585
|
Object.assign(base, { inputs: [], body: { prompt: '', mode: 'single', output: {} }, prehook: [], posthook: [] });
|
|
583
586
|
} else if (type === 'human') {
|
|
584
|
-
//
|
|
585
|
-
Object.assign(base, { displayFrom: '',
|
|
587
|
+
// 人工节点不预设去向:通常在流程中间(如设计评审),去向由用户按需配置
|
|
588
|
+
Object.assign(base, { displayFrom: '', routes: [] });
|
|
586
589
|
}
|
|
587
590
|
wf.nodes.push(base);
|
|
588
591
|
this.pushNow(wf); // 新增节点后提交,撤销可移除
|
|
@@ -603,23 +606,61 @@ window.__ModuleLoader__.load({
|
|
|
603
606
|
schemaVersion: 2, inputs: wf.inputs || [],
|
|
604
607
|
nodes: wf.nodes.map((n) => {
|
|
605
608
|
const out = { ...n, inputs: (n.inputs || []).filter((r) => r.from && r.field) };
|
|
606
|
-
// 人工节点统一输出 routes(旧 approveTo/rejectTo 自动迁移为 通过/驳回
|
|
607
|
-
if (n.type === 'human'
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
609
|
+
// 人工节点统一输出 routes(旧 approveTo/rejectTo 自动迁移为 通过/驳回 两条去向),并清掉旧字段
|
|
610
|
+
if (n.type === 'human') {
|
|
611
|
+
if (!Array.isArray(n.routes)) {
|
|
612
|
+
const routes = [];
|
|
613
|
+
if (n.approveTo) routes.push({ label: '通过', to: n.approveTo, tone: 'success' });
|
|
614
|
+
if (n.rejectTo) routes.push({ label: '驳回', to: n.rejectTo, tone: 'danger' });
|
|
615
|
+
out.routes = routes;
|
|
616
|
+
}
|
|
617
|
+
delete out.approveTo;
|
|
618
|
+
delete out.rejectTo;
|
|
612
619
|
}
|
|
613
620
|
return out;
|
|
614
621
|
}),
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
622
|
+
// 边规范化:value 智能转换('true'→true、'5'→5;${} 引用保留字符串)、
|
|
623
|
+
// op=in 时 value 转数组、空 when 置 null、同 from→to 去重合并(自愈历史脏数据)
|
|
624
|
+
edges: (() => {
|
|
625
|
+
// UI 值输入框存的是字符串,而 subagent 输出的布尔/数字是真值——
|
|
626
|
+
// 不转换则 'true' === true 永远失配、条件静默走 default。
|
|
627
|
+
const coerce = (v) => {
|
|
628
|
+
if (typeof v !== 'string') return v;
|
|
629
|
+
if (v.startsWith('${')) return v; // 引用字符串(运行时解析),保留
|
|
630
|
+
if (v === 'true') return true;
|
|
631
|
+
if (v === 'false') return false;
|
|
632
|
+
if (v !== '' && !isNaN(Number(v))) return Number(v);
|
|
633
|
+
return v;
|
|
634
|
+
};
|
|
635
|
+
const norm = wf.edges.map((e) => {
|
|
636
|
+
if (e.when && e.when.op === 'in' && typeof e.when.value === 'string') {
|
|
637
|
+
return { ...e, when: { ...e.when, value: e.when.value.split(',').map((s) => s.trim()).filter(Boolean).map(coerce) } };
|
|
638
|
+
}
|
|
639
|
+
if (e.when && typeof e.when.value === 'string') {
|
|
640
|
+
return { ...e, when: { ...e.when, value: coerce(e.when.value) } };
|
|
641
|
+
}
|
|
642
|
+
if (e.when && !e.when.field) return { ...e, when: null };
|
|
643
|
+
return e;
|
|
644
|
+
});
|
|
645
|
+
const out = [];
|
|
646
|
+
const idxByPair = new Map();
|
|
647
|
+
for (const e of norm) {
|
|
648
|
+
const k = e.from + '->' + e.to;
|
|
649
|
+
const i = idxByPair.get(k);
|
|
650
|
+
if (i === undefined) { idxByPair.set(k, out.length); out.push(e); continue; }
|
|
651
|
+
const prev = out[i];
|
|
652
|
+
// 两条都有条件时不自动合并(静默丢条件会悄悄改变路由语义):
|
|
653
|
+
// 两条都保留,让服务端 validateWorkflow 报「重复边」给用户明确提示。
|
|
654
|
+
if (prev.when && e.when) { out.push(e); continue; }
|
|
655
|
+
// 一条有条件一条没有(如历史脏数据 when 边 + 纯 default 边)→ 合并:when 取有值者,default 任一真则保留
|
|
656
|
+
out[i] = {
|
|
657
|
+
...prev,
|
|
658
|
+
when: prev.when || e.when || null,
|
|
659
|
+
default: !!(prev.default || e.default),
|
|
660
|
+
};
|
|
619
661
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
}),
|
|
662
|
+
return out;
|
|
663
|
+
})(),
|
|
623
664
|
};
|
|
624
665
|
}
|
|
625
666
|
async save() {
|
|
@@ -630,9 +671,10 @@ window.__ModuleLoader__.load({
|
|
|
630
671
|
try {
|
|
631
672
|
await api('/workflows', { method: 'POST', body: graph });
|
|
632
673
|
toast('已保存');
|
|
633
|
-
// 保存成功 =
|
|
674
|
+
// 保存成功 = 新基线:规范化结果(去重边/迁移 routes)回写 state,UI 立即干净
|
|
634
675
|
this.flushPending();
|
|
635
|
-
this.
|
|
676
|
+
this.setState({ wf: graph });
|
|
677
|
+
this.history = [JSON.parse(JSON.stringify(graph))];
|
|
636
678
|
this.redoStack = [];
|
|
637
679
|
this.setState({ canUndo: false, canRedo: false });
|
|
638
680
|
this.props.onSaved();
|
|
@@ -765,7 +807,8 @@ window.__ModuleLoader__.load({
|
|
|
765
807
|
]),
|
|
766
808
|
sec('行为 prompt(task 节点)', [
|
|
767
809
|
'本节点让 AI 做什么,写清目标与约束。',
|
|
768
|
-
'可引用:${inputDescription}(任务需求描述)、${inputTitle}(标题)、${inputs.参数名}(任务输入)。',
|
|
810
|
+
'可引用:${inputDescription}(任务需求描述)、${inputTitle}(标题)、${storyCode}(用户故事编码)、${cwd}(任务工作目录)、${inputs.参数名}(任务输入)。',
|
|
811
|
+
'运行时每个 task 节点会自动声明任务工作目录(所有文件/命令操作都在其中)。',
|
|
769
812
|
'需要 skill 时在 prompt 里直接写「先调用 skill 工具加载 skill「xxx」」。',
|
|
770
813
|
'上游输出引用(${上游编码.字段}):',
|
|
771
814
|
...this.buildRefsText(this.state.wf.nodes.find((n) => n.id === this.state.selectedId)).split('\n').map((s) => ' ' + s),
|
|
@@ -1046,7 +1089,7 @@ window.__ModuleLoader__.load({
|
|
|
1046
1089
|
}
|
|
1047
1090
|
renderEdgeProps() {
|
|
1048
1091
|
const key = this.state.selectedEdge;
|
|
1049
|
-
//
|
|
1092
|
+
// 人工决策边(多去向):key 形如 human:<nodeId>:<index>
|
|
1050
1093
|
if (key.startsWith('human:')) return this.renderHumanEdgeProps(key);
|
|
1051
1094
|
const [from, to] = key.split('->');
|
|
1052
1095
|
const edge = this.state.wf.edges.find((e) => e.from === from && e.to === to);
|
|
@@ -1058,15 +1101,17 @@ window.__ModuleLoader__.load({
|
|
|
1058
1101
|
react.createElement('div', { className: 'knj-form-label' }, '连线'),
|
|
1059
1102
|
react.createElement('div', { style: { fontSize: 13 } }, `${from} → ${to}`),
|
|
1060
1103
|
needCondition ? react.createElement('div', null,
|
|
1061
|
-
react.createElement('div', { className: 'knj-form-label', style: { marginTop: 10 } }, '条件(when)'
|
|
1062
|
-
react.createElement('
|
|
1104
|
+
react.createElement('div', { className: 'knj-form-label', style: { marginTop: 10 } }, '条件(when)',
|
|
1105
|
+
react.createElement('span', { onMouseEnter: (e) => this.showTip(e, '字段三种写法:\n· 裸字段名(如 level)——相对作用域:网关取唯一上游输出、任务多出边取自身输出\n· ${节点id.字段}(如 ${verify.passed})——显式引用任意已执行节点,取原始值(布尔/数字直接比较)\n· 裸点号路径(如 verify.passed)——同上\n\n值:布尔/数字自动识别(true/false/5 存为真值);支持 ${inputs.参数名} 引用;in 用逗号分隔。'), onMouseLeave: () => this.hideTip(), style: { cursor: 'help', color: 'var(--dsw-alias-label-tertiary)', marginLeft: 6, fontSize: 12 } }, 'ⓘ'),
|
|
1106
|
+
),
|
|
1107
|
+
react.createElement('input', { className: 'knj-input', placeholder: '字段名(如 level;判断任意上游写 ${节点id.字段})', value: edge.when?.field || '', onChange: (e) => this.patchEdge(key, { when: { op: 'eq', value: '', ...edge.when, field: e.target.value } }) }),
|
|
1063
1108
|
react.createElement('div', { className: 'knj-row', style: { marginTop: 8 } },
|
|
1064
1109
|
react.createElement('select', { className: 'knj-input', style: { maxWidth: 120 }, value: edge.when?.op || 'eq', onChange: (e) => this.patchEdge(key, { when: { field: '', ...edge.when, op: e.target.value } }) },
|
|
1065
1110
|
react.createElement('option', { value: 'eq' }, '等于'),
|
|
1066
1111
|
react.createElement('option', { value: 'neq' }, '不等于'),
|
|
1067
1112
|
react.createElement('option', { value: 'in' }, '属于'),
|
|
1068
1113
|
),
|
|
1069
|
-
react.createElement('input', { className: 'knj-input', placeholder: '
|
|
1114
|
+
react.createElement('input', { className: 'knj-input', placeholder: '值(布尔/数字自动识别;in 用逗号分隔;支持 ${inputs.x})', value: this.valueToText(edge.when?.value), onChange: (e) => this.patchEdge(key, { when: { ...edge.when, value: e.target.value } }) }),
|
|
1070
1115
|
),
|
|
1071
1116
|
edge.when ? react.createElement('button', { className: 'knj-btn', style: { alignSelf: 'flex-start', marginTop: 6 }, onClick: () => this.patchEdge(key, { when: null }) }, '清除条件') : null,
|
|
1072
1117
|
) : react.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-tertiary)', marginTop: 8 } }, '顺序边,无需条件判断'),
|
|
@@ -1261,7 +1306,7 @@ window.__ModuleLoader__.load({
|
|
|
1261
1306
|
fieldLabel('节点编码(id)', '节点唯一标识:下游用 ${编码.字段} 引用(如 ${analyze.description}),网关用字段值做分支判断。修改后 prompt 里旧的 ${旧编码.字段} 引用需手动更新。', '下游用 ${编码.字段} 引用,如 ${analyze.description}'),
|
|
1262
1307
|
react.createElement('input', { className: 'knj-input', style: { marginBottom: 8 }, placeholder: '如:analyze', defaultValue: node.id || '', onBlur: (e) => this.patchNodeId(node.id, e.target.value.trim()) }),
|
|
1263
1308
|
isTask ? react.createElement('div', null,
|
|
1264
|
-
fieldLabel('行为 prompt(主体)', '本节点让 AI 做什么,写清目标与约束。\n可引用:\n${inputDescription}(任务需求描述)\n${inputTitle}(标题)\n${inputs.参数名}(任务输入)\n\n上游输出引用(${上游编码.字段}):\n' + this.buildRefsText(node) + '\n\n注意:XOR 分支未执行的节点引用为空;整节点输出用 ${节点id};${ctx.前缀写法仍兼容。', '让 AI 做什么;可引用 ${inputDescription}、${上游编码.字段}(ⓘ 看完整清单)'),
|
|
1309
|
+
fieldLabel('行为 prompt(主体)', '本节点让 AI 做什么,写清目标与约束。\n可引用:\n${inputDescription}(任务需求描述)\n${inputTitle}(标题)\n${storyCode}(用户故事编码,可选)\n${cwd}(任务工作目录;运行时还会自动向 subagent 声明该目录)\n${inputs.参数名}(任务输入)\n\n上游输出引用(${上游编码.字段}):\n' + this.buildRefsText(node) + '\n\n注意:XOR 分支未执行的节点引用为空;整节点输出用 ${节点id};${ctx.前缀写法仍兼容。', '让 AI 做什么;可引用 ${inputDescription}、${storyCode}、${cwd}、${上游编码.字段}(ⓘ 看完整清单)'),
|
|
1265
1310
|
react.createElement('textarea', { className: 'knj-textarea', style: { minHeight: 140 }, placeholder: '如:根据需求 ${inputDescription} 输出功能描述与验收标准', value: node.body?.prompt || '', onChange: (e) => this.patchNode(node.id, { body: { ...node.body, prompt: e.target.value } }) }),
|
|
1266
1311
|
fieldLabel('执行方式', 'single:一个 AI 完成本阶段,输出单个对象。\nparallel:派多个 AI 并行做同一件事,结果合并为数组(配合「最大执行次数」外的 parallelItems 使用,见输出说明)。', 'single:单个 AI 完成;parallel:多个 AI 并行,结果合并为数组'),
|
|
1267
1312
|
react.createElement('select', { className: 'knj-input', value: node.body?.mode || 'single', onChange: (e) => this.patchNode(node.id, { body: { ...node.body, mode: e.target.value } }) },
|
|
@@ -1435,26 +1480,35 @@ window.__ModuleLoader__.load({
|
|
|
1435
1480
|
if (typeof s !== 'string' || !s.trim()) return false;
|
|
1436
1481
|
const v = s.trim();
|
|
1437
1482
|
if (v.length > 220) return false;
|
|
1438
|
-
|
|
1483
|
+
if (v.includes(' ')) return false; // 路径不含空格:错误消息(含空格+中文)不会被误判
|
|
1484
|
+
// 绝对/相对路径开头(/ ./ ../ 盘符:\ ~/),或纯文件名带扩展名
|
|
1485
|
+
return /^(\/|\.{1,2}\/|[A-Za-z]:[\\/]|~\/)/.test(v) || /^\S+\.\w{1,12}$/.test(v);
|
|
1439
1486
|
}
|
|
1440
1487
|
/** 从产物(JSON)里收集看起来像文件路径的字符串(去重,限制数量) */
|
|
1488
|
+
/** 从产物(JSON)里收集文件路径:只认「文件类字段」的数组元素(changedFiles/affectedFiles/paths 等),
|
|
1489
|
+
* 错误消息/描述等文本字段不收集(避免把错误文案里的路径误判成文件)。去重,限制数量。 */
|
|
1441
1490
|
function collectFiles(value, out, depth) {
|
|
1442
1491
|
out = out || new Set();
|
|
1443
1492
|
depth = depth || 0;
|
|
1444
1493
|
if (depth > 4 || out.size >= 20) return out;
|
|
1445
|
-
if (value === null || value === undefined) return out;
|
|
1446
|
-
if (
|
|
1447
|
-
|
|
1494
|
+
if (value === null || value === undefined || typeof value !== 'object') return out;
|
|
1495
|
+
if (Array.isArray(value)) {
|
|
1496
|
+
// 仅当整体是文件列表数组时(由上层文件字段进入)才收元素;普通数组不递归收字符串
|
|
1497
|
+
for (const v of value) {
|
|
1498
|
+
if (typeof v === 'string') { if (looksLikeFile(v)) out.add(v.trim()); }
|
|
1499
|
+
else collectFiles(v, out, depth + 1);
|
|
1500
|
+
}
|
|
1448
1501
|
return out;
|
|
1449
1502
|
}
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1503
|
+
for (const k of Object.keys(value)) {
|
|
1504
|
+
// 字段名暗示文件列表(changedFiles/affectedFiles/files/paths/artifacts 等)→ 收集其数组元素
|
|
1505
|
+
if (Array.isArray(value[k]) && /(file|path|src|output|artifact)/i.test(k)) {
|
|
1506
|
+
for (const v of value[k]) {
|
|
1507
|
+
if (typeof v === 'string') { if (looksLikeFile(v)) out.add(v.trim()); }
|
|
1508
|
+
else collectFiles(v, out, depth + 1);
|
|
1509
|
+
}
|
|
1510
|
+
} else {
|
|
1511
|
+
collectFiles(value[k], out, depth + 1);
|
|
1458
1512
|
}
|
|
1459
1513
|
}
|
|
1460
1514
|
return out;
|
|
@@ -1471,53 +1525,90 @@ window.__ModuleLoader__.load({
|
|
|
1471
1525
|
// 新建任务模态(完整表单)+ 中央工作台(子 tab)+ 打开函数
|
|
1472
1526
|
// ---------------------------------------------------------------------------
|
|
1473
1527
|
let newTaskRoot = null;
|
|
1528
|
+
let newTaskRootHandle = null;
|
|
1474
1529
|
function openNewTaskModal() {
|
|
1475
1530
|
ensureStyle();
|
|
1476
1531
|
if (newTaskRoot) { newTaskRoot.style.display = 'flex'; return; }
|
|
1477
1532
|
newTaskRoot = document.createElement('div');
|
|
1478
1533
|
document.body.appendChild(newTaskRoot);
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1534
|
+
// 关闭 = 真正卸载:下次打开表单全新(不残留上次填的标题/描述/工作目录/参数)
|
|
1535
|
+
const close = () => {
|
|
1536
|
+
if (newTaskRootHandle) { newTaskRootHandle.unmount(); newTaskRootHandle = null; }
|
|
1537
|
+
if (newTaskRoot) { newTaskRoot.remove(); newTaskRoot = null; }
|
|
1538
|
+
};
|
|
1539
|
+
newTaskRootHandle = react_dom_client.createRoot(newTaskRoot);
|
|
1540
|
+
newTaskRootHandle.render(react.createElement(NewTaskModal, { onClose: close }));
|
|
1482
1541
|
}
|
|
1483
1542
|
|
|
1484
1543
|
class NewTaskModal extends react.Component {
|
|
1485
1544
|
constructor(props) {
|
|
1486
1545
|
super(props);
|
|
1487
|
-
this.state = { description: '', title: '', workflowId: '', cwd: '', cwdOptions: [], inputs: {}, workflows: [], busy: false, error: null };
|
|
1546
|
+
this.state = { description: '', title: '', storyCode: '', workflowId: '', cwd: '', cwdOptions: [], inputs: {}, workflows: [], busy: false, error: null, model: '', provider: '', models: [] };
|
|
1488
1547
|
}
|
|
1489
1548
|
componentDidMount() { this.load(); }
|
|
1549
|
+
/** 取当前会话的模型/provider 作为表单默认值(用户可改成其他模型,如 glm 额度用完时切 deepseek) */
|
|
1550
|
+
currentModelDefaults() {
|
|
1551
|
+
try {
|
|
1552
|
+
const ag = _ctx?.get?.('agents');
|
|
1553
|
+
const init = ag?.currentInitiator?.() ?? ag?.roots?.()?.[0];
|
|
1554
|
+
const opts = init?.options || {};
|
|
1555
|
+
return { model: opts.model || '', provider: opts.provider || '' };
|
|
1556
|
+
} catch { return { model: '', provider: '' }; }
|
|
1557
|
+
}
|
|
1558
|
+
/** 拉取可用模型列表:直接走 Host 插件 /models(本地解析配置文件,毫秒级)。
|
|
1559
|
+
* 不再尝试 connection.api.llm.models RPC——它会遍历所有 provider 并逐个远端 resolve,
|
|
1560
|
+
* 在 key 失效/网络慢时挂起很久,导致表单"加载半天才出来"。 */
|
|
1561
|
+
async loadModels() {
|
|
1562
|
+
const list = [];
|
|
1563
|
+
try {
|
|
1564
|
+
const r = await api('/models');
|
|
1565
|
+
for (const m of (r && r.models) || []) list.push(m);
|
|
1566
|
+
} catch { /* 失败则留空(仅显示"继承当前会话") */ }
|
|
1567
|
+
const seen = new Set();
|
|
1568
|
+
const uniq = list.filter((m) => {
|
|
1569
|
+
const k = (m.provider || '') + '::' + (m.model || '');
|
|
1570
|
+
if (!m.model || seen.has(k)) return false;
|
|
1571
|
+
seen.add(k);
|
|
1572
|
+
return true;
|
|
1573
|
+
});
|
|
1574
|
+
if (uniq.length === 0) console.warn('[knj-workflow] 模型列表为空:/models 未返回模型');
|
|
1575
|
+
this.setState({ models: uniq });
|
|
1576
|
+
}
|
|
1577
|
+
/** 工作目录下拉:异步加载,不阻塞表单渲染(refresh 可能慢/挂起,最多等 3 秒) */
|
|
1578
|
+
async loadCwdOptions() {
|
|
1579
|
+
try {
|
|
1580
|
+
const ws = _ctx?.get?.('workspaces') || _workspaces;
|
|
1581
|
+
if (ws && typeof ws.refresh === 'function' && !(ws.list?.getSnapshot?.()?.items || []).length) {
|
|
1582
|
+
await Promise.race([
|
|
1583
|
+
ws.refresh(),
|
|
1584
|
+
new Promise((resolve) => setTimeout(resolve, 3000)),
|
|
1585
|
+
]);
|
|
1586
|
+
}
|
|
1587
|
+
const items = ws?.list?.getSnapshot?.()?.items || [];
|
|
1588
|
+
const cwdOptions = [];
|
|
1589
|
+
for (const item of items) {
|
|
1590
|
+
const view = item?.getSnapshot?.()?.view || item || {};
|
|
1591
|
+
const path = view.path || view.cwd || '';
|
|
1592
|
+
const title = view.title || view.name || (path ? path.replace(/[\\/]+$/, '').split(/[\\/]/).pop() : '');
|
|
1593
|
+
if (path) cwdOptions.push({ path, title: title || path });
|
|
1594
|
+
}
|
|
1595
|
+
this.setState({ cwdOptions });
|
|
1596
|
+
} catch { /* 加载失败保持空 */ }
|
|
1597
|
+
}
|
|
1490
1598
|
async load() {
|
|
1491
1599
|
try {
|
|
1492
1600
|
const w = await api('/workflows');
|
|
1493
1601
|
const workflows = w.workflows || [];
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
if (ws && typeof ws.refresh === 'function' && !(ws.list?.getSnapshot?.()?.items || []).length) {
|
|
1500
|
-
await ws.refresh();
|
|
1501
|
-
}
|
|
1502
|
-
const items = ws?.list?.getSnapshot?.()?.items || [];
|
|
1503
|
-
console.log('[knj-workflow] workspaces:', ws ? 'ok' : 'null', 'items:', items.length, 'sample:', items[0] ? (items[0].getSnapshot ? JSON.stringify(items[0].getSnapshot().view) : JSON.stringify(items[0])) : null);
|
|
1504
|
-
for (const item of items) {
|
|
1505
|
-
// 防御两种结构:Workspace 实例(有 getSnapshot().view)或 raw workspace view(自身即带 path)
|
|
1506
|
-
const view = item?.getSnapshot?.()?.view || item || {};
|
|
1507
|
-
const path = view.path || view.cwd || '';
|
|
1508
|
-
const title = view.title || view.name || (path ? path.replace(/[\\/]+$/, '').split(/[\\/]/).pop() : '');
|
|
1509
|
-
console.log('[knj-workflow] cwd item:', 'hasGetSnapshot=' + (typeof item?.getSnapshot === 'function'), 'path=' + path, 'title=' + (title || ''));
|
|
1510
|
-
if (path) cwdOptions.push({ path, title: title || path });
|
|
1511
|
-
}
|
|
1512
|
-
} catch (e) { console.warn('[knj-workflow] 加载工作目录失败:', e); }
|
|
1513
|
-
console.log('[knj-workflow] cwdOptions built:', JSON.stringify(cwdOptions));
|
|
1514
|
-
this.setState({ workflows, workflowId: workflows[0]?.id || '', cwdOptions, error: null });
|
|
1602
|
+
const def = this.currentModelDefaults();
|
|
1603
|
+
// 先渲染表单主体(工作流/默认模型),工作区与模型列表异步填充,不等慢请求
|
|
1604
|
+
this.setState({ workflows, workflowId: workflows[0]?.id || '', model: def.model, provider: def.provider, error: null });
|
|
1605
|
+
this.loadCwdOptions();
|
|
1606
|
+
this.loadModels();
|
|
1515
1607
|
} catch (e) { this.setState({ error: e.message }); }
|
|
1516
1608
|
}
|
|
1517
1609
|
selectedWorkflow() { return this.state.workflows.find((w) => w.id === this.state.workflowId); }
|
|
1518
1610
|
render() {
|
|
1519
|
-
const { description, title, workflowId, cwd, cwdOptions, inputs, workflows, busy, error } = this.state;
|
|
1520
|
-
console.log('[knj-workflow] render cwdOptions:', cwdOptions.length);
|
|
1611
|
+
const { description, title, storyCode, workflowId, cwd, cwdOptions, inputs, workflows, busy, error, model, provider } = this.state;
|
|
1521
1612
|
const wf = this.selectedWorkflow();
|
|
1522
1613
|
const wfInputs = (wf && Array.isArray(wf.inputs)) ? wf.inputs : [];
|
|
1523
1614
|
return react.createElement('div', { className: 'knj-overlay' },
|
|
@@ -1536,6 +1627,10 @@ console.log('[knj-workflow] render cwdOptions:', cwdOptions.length);
|
|
|
1536
1627
|
react.createElement('div', { className: 'knj-form-label' }, '标题(创建后自动总结,可改)'),
|
|
1537
1628
|
react.createElement('input', { className: 'knj-input', placeholder: '留空则创建后自动总结', value: title, onChange: (e) => this.setState({ title: e.target.value }) }),
|
|
1538
1629
|
),
|
|
1630
|
+
react.createElement('div', { className: 'knj-form-section' },
|
|
1631
|
+
react.createElement('div', { className: 'knj-form-label', title: '用户故事编号(如 US-123 / STORY-45)。可选;填了之后节点 prompt 里可用 ${storyCode} 引用(如记录到提交信息/文档标题)。' }, '用户故事编码(可选) ⓘ'),
|
|
1632
|
+
react.createElement('input', { className: 'knj-input', placeholder: '如 US-123(可留空)', value: storyCode, onChange: (e) => this.setState({ storyCode: e.target.value }) }),
|
|
1633
|
+
),
|
|
1539
1634
|
react.createElement('div', { className: 'knj-form-section' },
|
|
1540
1635
|
react.createElement('div', { className: 'knj-form-label' }, '工作流'),
|
|
1541
1636
|
react.createElement('select', { className: 'knj-input', value: workflowId, onChange: (e) => this.setState({ workflowId: e.target.value }) },
|
|
@@ -1550,6 +1645,21 @@ console.log('[knj-workflow] render cwdOptions:', cwdOptions.length);
|
|
|
1550
1645
|
react.createElement('div', { className: 'knj-form-label' }, '任务输入参数'),
|
|
1551
1646
|
wfInputs.map((inp) => react.createElement('input', { key: inp.name, className: 'knj-input', style: { marginBottom: 8 }, placeholder: `${inp.label || inp.name}${inp.required ? '(必填)' : ''}`, value: inputs[inp.name] || '', onChange: (e) => this.setState({ inputs: { ...inputs, [inp.name]: e.target.value } }) })),
|
|
1552
1647
|
) : null,
|
|
1648
|
+
react.createElement('div', { className: 'knj-form-section' },
|
|
1649
|
+
react.createElement('div', { className: 'knj-form-label', title: '所有阶段 subagent 都用这个模型执行。默认继承你当前会话的模型(见下拉第一项)。当前模型额度不足时,可在这里换成其他可用模型避免任务失败。' }, '执行模型(可选,留空继承当前会话) ⓘ'),
|
|
1650
|
+
react.createElement('select', { className: 'knj-input', value: model ? provider + '::' + model : '', onChange: (e) => {
|
|
1651
|
+
const v = e.target.value;
|
|
1652
|
+
if (!v) { this.setState({ model: '', provider: '' }); return; }
|
|
1653
|
+
const sep = v.indexOf('::');
|
|
1654
|
+
this.setState({ provider: v.slice(0, sep), model: v.slice(sep + 2) });
|
|
1655
|
+
} },
|
|
1656
|
+
// 第一项 = 继承当前会话(显示当前模型名);列表排除与它重复的模型,避免出现两个一样的选项
|
|
1657
|
+
react.createElement('option', { value: '' }, model ? `继承当前会话(${provider}/${model})` : '继承当前会话(默认)'),
|
|
1658
|
+
this.state.models
|
|
1659
|
+
.filter((m) => m.provider + '::' + m.model !== (model ? provider + '::' + model : ''))
|
|
1660
|
+
.map((m) => react.createElement('option', { key: m.provider + '::' + m.model, value: m.provider + '::' + m.model }, `${m.provider}/${m.model}`)),
|
|
1661
|
+
),
|
|
1662
|
+
),
|
|
1553
1663
|
react.createElement('div', { className: 'knj-row', style: { justifyContent: 'flex-end' } },
|
|
1554
1664
|
react.createElement('button', { className: 'knj-btn', onClick: this.props.onClose }, '取消'),
|
|
1555
1665
|
react.createElement('button', { className: 'knj-btn primary', disabled: busy || !description.trim() || !workflowId, onClick: () => this.submit() }, '创建并启动'),
|
|
@@ -1559,14 +1669,18 @@ console.log('[knj-workflow] render cwdOptions:', cwdOptions.length);
|
|
|
1559
1669
|
);
|
|
1560
1670
|
}
|
|
1561
1671
|
async submit() {
|
|
1562
|
-
const { description, title, workflowId, cwd, inputs } = this.state;
|
|
1672
|
+
const { description, title, storyCode, workflowId, cwd, inputs, model, provider } = this.state;
|
|
1563
1673
|
const wf = this.selectedWorkflow();
|
|
1564
1674
|
const wfInputs = (wf && Array.isArray(wf.inputs)) ? wf.inputs : [];
|
|
1565
1675
|
this.setState({ busy: true });
|
|
1566
1676
|
try {
|
|
1567
1677
|
const body = { title: title.trim() || description.trim().slice(0, 50), workflowId };
|
|
1568
1678
|
body.description = description.trim();
|
|
1679
|
+
if (storyCode.trim()) body.storyCode = storyCode.trim();
|
|
1569
1680
|
if (cwd.trim()) body.cwd = cwd.trim();
|
|
1681
|
+
// 模型配置:填了就固定用(所有阶段 subagent),空则继承当前会话
|
|
1682
|
+
if (model.trim()) body.model = model.trim();
|
|
1683
|
+
if (provider.trim()) body.provider = provider.trim();
|
|
1570
1684
|
const filled = {};
|
|
1571
1685
|
wfInputs.forEach((inp) => { const v = inputs[inp.name]; if (v != null && v !== '') filled[inp.name] = v; });
|
|
1572
1686
|
if (Object.keys(filled).length) body.inputs = filled;
|
|
@@ -1602,14 +1716,21 @@ console.log('[knj-workflow] render cwdOptions:', cwdOptions.length);
|
|
|
1602
1716
|
}
|
|
1603
1717
|
|
|
1604
1718
|
let devBoardRoot = null;
|
|
1719
|
+
let devBoardRootHandle = null; // React root 句柄(关闭时需 unmount 清定时器,不能只 remove DOM)
|
|
1605
1720
|
let _closeDevBoard = null; // 收起开发任务弹窗(跳转子会话前先收起,避免盖住)
|
|
1606
1721
|
function openDevBoard() {
|
|
1607
1722
|
ensureStyle();
|
|
1608
1723
|
if (devBoardRoot) { devBoardRoot.style.display = 'flex'; return; }
|
|
1609
1724
|
devBoardRoot = document.createElement('div');
|
|
1610
1725
|
document.body.appendChild(devBoardRoot);
|
|
1611
|
-
_closeDevBoard = () => {
|
|
1612
|
-
|
|
1726
|
+
_closeDevBoard = () => {
|
|
1727
|
+
if (!devBoardRoot && !devBoardRootHandle) return;
|
|
1728
|
+
// 真正卸载而不是 display:none:否则再次打开会残留上次的详情视图/翻页/滚动位置
|
|
1729
|
+
if (devBoardRootHandle) { devBoardRootHandle.unmount(); devBoardRootHandle = null; }
|
|
1730
|
+
if (devBoardRoot) { devBoardRoot.remove(); devBoardRoot = null; }
|
|
1731
|
+
};
|
|
1732
|
+
devBoardRootHandle = react_dom_client.createRoot(devBoardRoot);
|
|
1733
|
+
devBoardRootHandle.render(react.createElement(DevBoardOverlay, {
|
|
1613
1734
|
onClose: _closeDevBoard,
|
|
1614
1735
|
}));
|
|
1615
1736
|
}
|
|
@@ -1834,14 +1955,25 @@ console.log('[knj-workflow] render cwdOptions:', cwdOptions.length);
|
|
|
1834
1955
|
await this.loadResults();
|
|
1835
1956
|
if (this.props.onChanged) this.props.onChanged();
|
|
1836
1957
|
}
|
|
1837
|
-
/** 某节点的产物(results.json 里按节点 id
|
|
1958
|
+
/** 某节点的产物(results.json 里按节点 id 存;展开时若缺失会从 stage 文件补,见 toggle) */
|
|
1838
1959
|
resultFor(stageId) {
|
|
1839
1960
|
const results = this.state.results;
|
|
1840
1961
|
if (!results || typeof results !== 'object') return null;
|
|
1841
|
-
return results[stageId];
|
|
1842
|
-
}
|
|
1843
|
-
toggle(stageId) {
|
|
1844
|
-
|
|
1962
|
+
return results[stageId] ?? null;
|
|
1963
|
+
}
|
|
1964
|
+
async toggle(stageId) {
|
|
1965
|
+
const willExpand = this.state.expanded !== stageId;
|
|
1966
|
+
this.setState((s) => ({ expanded: willExpand ? stageId : null }));
|
|
1967
|
+
// 展开时补产物:results.json 可能为空(任务中断/未 finalize),
|
|
1968
|
+
// 但每节点完成时 checkpoint 会实时落盘 stages/<id>.json——从那里补。
|
|
1969
|
+
if (!willExpand) return;
|
|
1970
|
+
if (this.resultFor(stageId) != null) return;
|
|
1971
|
+
try {
|
|
1972
|
+
const r = await api('/tasks/' + this.props.task.id + '/stages/' + stageId);
|
|
1973
|
+
if (r && r.data != null && typeof r.data === 'object') {
|
|
1974
|
+
this.setState((s) => ({ results: { ...(s.results || {}), [stageId]: r.data } }));
|
|
1975
|
+
}
|
|
1976
|
+
} catch { /* stage 文件也没有则保持无产物 */ }
|
|
1845
1977
|
}
|
|
1846
1978
|
duration(s) {
|
|
1847
1979
|
if (!s.startedAt || !s.finishedAt) return '';
|
|
@@ -1860,7 +1992,8 @@ console.log('[knj-workflow] render cwdOptions:', cwdOptions.length);
|
|
|
1860
1992
|
const result = this.resultFor(s.id);
|
|
1861
1993
|
const sessionIds = s.sessionIds || [];
|
|
1862
1994
|
const expanded = this.state.expanded === s.id;
|
|
1863
|
-
|
|
1995
|
+
// 展开条件放宽:done/failed 节点必有产物(可能在 results 里或 stage 文件里),点了再异步补数据
|
|
1996
|
+
const hasDetail = result != null || sessionIds.length > 0 || s.error || s.status === 'done' || s.status === 'failed';
|
|
1864
1997
|
const statusIcon = s.status === 'done' ? icon('check')
|
|
1865
1998
|
: isRunning ? icon('clock')
|
|
1866
1999
|
: s.status === 'skipped' ? '–'
|
|
@@ -1881,9 +2014,9 @@ console.log('[knj-workflow] render cwdOptions:', cwdOptions.length);
|
|
|
1881
2014
|
expanded ? react.createElement('div', { className: 'knj-node-body' },
|
|
1882
2015
|
s.error ? react.createElement('div', { style: { fontSize: 11, color: 'var(--dsw-alias-state-error-primary)', wordBreak: 'break-all' } }, s.error) : null,
|
|
1883
2016
|
result != null ? react.createElement('div', null,
|
|
1884
|
-
react.createElement('div', { className: 'knj-node-sec' }, '
|
|
2017
|
+
react.createElement('div', { className: 'knj-node-sec' }, '产物(输出字段)'),
|
|
2018
|
+
this.renderResultFields(result),
|
|
1885
2019
|
this.renderFiles(s.id, result),
|
|
1886
|
-
react.createElement('pre', null, JSON.stringify(result, null, 2)),
|
|
1887
2020
|
) : null,
|
|
1888
2021
|
this.state.fileView && this.state.fileView.stageId === s.id ? react.createElement('div', null,
|
|
1889
2022
|
react.createElement('div', { className: 'knj-node-sec', style: { display: 'flex', alignItems: 'center', gap: 8 } },
|
|
@@ -1893,24 +2026,47 @@ console.log('[knj-workflow] render cwdOptions:', cwdOptions.length);
|
|
|
1893
2026
|
react.createElement('pre', { style: { maxHeight: 320 } }, this.state.fileView.content),
|
|
1894
2027
|
) : null,
|
|
1895
2028
|
sessionIds.length > 0 ? react.createElement('div', null,
|
|
1896
|
-
react.createElement('div', { className: 'knj-node-sec' }, '
|
|
2029
|
+
react.createElement('div', { className: 'knj-node-sec' }, '执行记录(子会话)'),
|
|
1897
2030
|
sessionIds.map((sid) => react.createElement('div', { key: sid, className: 'knj-row', style: { marginTop: 6, gap: 8 } },
|
|
1898
|
-
react.createElement('code', { style: { fontSize: 11, color: 'var(--dsw-alias-label-secondary)', flex: 1, wordBreak: 'break-all' } }, sid),
|
|
1899
|
-
react.createElement('button', { className: 'knj-btn', style: { flexShrink: 0 }, onClick: () => this.viewSubagent(sid) }, '
|
|
2031
|
+
react.createElement('code', { style: { fontSize: 11, color: 'var(--dsw-alias-label-secondary)', flex: 1, wordBreak: 'break-all' } }, '👤 ' + sid),
|
|
2032
|
+
react.createElement('button', { className: 'knj-btn', style: { flexShrink: 0 }, title: '跳转到该子代理会话查看完整执行细节', onClick: () => this.viewSubagent(sid) }, '查看会话'),
|
|
1900
2033
|
)),
|
|
1901
2034
|
) : null,
|
|
1902
2035
|
) : null,
|
|
1903
2036
|
);
|
|
1904
2037
|
}
|
|
1905
|
-
/**
|
|
2038
|
+
/** 节点输出按字段展示:字段名(灰底标签)+ 值(缩进 + 左边框竖线),对象/数组折叠为 JSON */
|
|
2039
|
+
renderResultFields(result) {
|
|
2040
|
+
if (result && typeof result === 'object' && !Array.isArray(result)) {
|
|
2041
|
+
const keys = Object.keys(result);
|
|
2042
|
+
if (keys.length === 0) return react.createElement('pre', { style: { fontSize: 11 } }, '{}');
|
|
2043
|
+
const labelStyle = { display: 'inline-block', background: 'var(--dsw-alias-fill-2, #eef0f3)', padding: '1px 8px', borderRadius: 4, fontSize: 11, fontWeight: 600, fontFamily: 'monospace', color: 'var(--dsw-alias-label-secondary)' };
|
|
2044
|
+
const valueStyle = { fontSize: 12.5, color: 'var(--dsw-alias-label-primary)', wordBreak: 'break-all', whiteSpace: 'pre-wrap', marginTop: 4, paddingLeft: 10, borderLeft: '2px solid var(--dsw-alias-border-l2, #e2e6ec)' };
|
|
2045
|
+
return react.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 10 } },
|
|
2046
|
+
keys.map((k) => {
|
|
2047
|
+
const v = result[k];
|
|
2048
|
+
const simple = v == null || typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean';
|
|
2049
|
+
return react.createElement('div', { key: k },
|
|
2050
|
+
react.createElement('div', { style: labelStyle }, k),
|
|
2051
|
+
simple
|
|
2052
|
+
? react.createElement('div', { style: valueStyle }, v == null ? String(v) : String(v))
|
|
2053
|
+
: react.createElement('pre', { style: { ...valueStyle, fontSize: 11, maxHeight: 160, overflow: 'auto', background: 'var(--dsw-alias-fill-2, #f5f6f8)', padding: 6, borderRadius: 4, marginLeft: 10, borderLeft: '2px solid var(--dsw-alias-border-l2, #e2e6ec)' } }, JSON.stringify(v, null, 2)),
|
|
2054
|
+
);
|
|
2055
|
+
}),
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
// 数组/标量等非对象结果:保持 JSON 展示
|
|
2059
|
+
return react.createElement('pre', { style: { fontSize: 11 } }, JSON.stringify(result, null, 2));
|
|
2060
|
+
}
|
|
2061
|
+
/** 从产物里识别文件路径并渲染为可点击的「查看文件」列表 */
|
|
1906
2062
|
renderFiles(stageId, result) {
|
|
1907
2063
|
const files = [...collectFiles(result)];
|
|
1908
2064
|
if (!files.length) return null;
|
|
1909
2065
|
return react.createElement('div', null,
|
|
1910
|
-
react.createElement('div', { className: 'knj-node-sec' }, '
|
|
2066
|
+
react.createElement('div', { className: 'knj-node-sec' }, '文件(产物路径)'),
|
|
1911
2067
|
files.map((f) => react.createElement('div', { key: f, className: 'knj-row', style: { marginTop: 6, gap: 8 } },
|
|
1912
|
-
react.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: f }, f),
|
|
1913
|
-
react.createElement('button', { className: 'knj-btn', style: { flexShrink: 0 }, onClick: () => this.viewFile(stageId, f) }, '
|
|
2068
|
+
react.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: f }, '📄 ' + f),
|
|
2069
|
+
react.createElement('button', { className: 'knj-btn', style: { flexShrink: 0, borderColor: 'var(--dsw-static-blue-600)', color: 'var(--dsw-static-blue-600)' }, title: '读取并查看该文件内容', onClick: () => this.viewFile(stageId, f) }, '查看文件'),
|
|
1914
2070
|
)),
|
|
1915
2071
|
);
|
|
1916
2072
|
}
|
|
@@ -1965,6 +2121,7 @@ console.log('[knj-workflow] render cwdOptions:', cwdOptions.length);
|
|
|
1965
2121
|
),
|
|
1966
2122
|
react.createElement('button', { className: 'knj-btn', title: '刷新', onClick: () => this.refresh() }, icon('refresh'), '刷新'),
|
|
1967
2123
|
react.createElement('span', { className: 'knj-badge', style: { background: statusMeta.color } }, statusMeta.label),
|
|
2124
|
+
task.model ? react.createElement('span', { className: 'knj-badge', style: { background: '#5b6472' }, title: '该任务固定的执行模型(创建时指定)' }, task.model) : null,
|
|
1968
2125
|
),
|
|
1969
2126
|
// 整体进度条 + 摘要
|
|
1970
2127
|
react.createElement('div', { className: 'knj-progress', style: { width: '100%', marginTop: 12 } },
|