@fieldwangai/agentflow 0.1.35 → 0.1.36

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/bin/lib/auth.mjs CHANGED
@@ -23,6 +23,10 @@ function sessionsPath() {
23
23
  return path.join(authRoot(), "sessions.json");
24
24
  }
25
25
 
26
+ function userAllowlistPath() {
27
+ return path.join(authRoot(), "user-allowlist.json");
28
+ }
29
+
26
30
  function readJsonObject(filePath) {
27
31
  try {
28
32
  if (!fs.existsSync(filePath)) return {};
@@ -82,6 +86,57 @@ export function authSetupRequired() {
82
86
  return Object.keys(readAuthUsers()).length === 0;
83
87
  }
84
88
 
89
+ function normalizeUserAllowlistInput(value) {
90
+ if (Array.isArray(value)) return value.map((item) => String(item || "").trim()).filter(Boolean);
91
+ if (typeof value === "string") {
92
+ return value
93
+ .split(/[\s,;]+/g)
94
+ .map((item) => item.trim())
95
+ .filter(Boolean);
96
+ }
97
+ return [];
98
+ }
99
+
100
+ export function readUserAllowlist() {
101
+ const fromEnv = normalizeUserAllowlistInput(process.env.AGENTFLOW_USER_WHITELIST || process.env.AGENTFLOW_ALLOWED_USERS || "");
102
+ let fromFile = [];
103
+ try {
104
+ const p = userAllowlistPath();
105
+ if (fs.existsSync(p)) {
106
+ const data = JSON.parse(fs.readFileSync(p, "utf-8"));
107
+ fromFile = normalizeUserAllowlistInput(Array.isArray(data) ? data : data?.users);
108
+ }
109
+ } catch {
110
+ fromFile = [];
111
+ }
112
+ const users = Array.from(new Set([...fromFile, ...fromEnv].map((item) => String(item || "").trim()).filter(Boolean)));
113
+ return { enabled: users.length > 0, users, path: userAllowlistPath() };
114
+ }
115
+
116
+ function userAllowlistMatchSet(users) {
117
+ const out = new Set();
118
+ for (const user of users) {
119
+ const raw = String(user || "").trim().toLowerCase();
120
+ if (raw) out.add(raw);
121
+ const safe = sanitizeAgentflowUserId(user);
122
+ if (safe) out.add(safe);
123
+ }
124
+ return out;
125
+ }
126
+
127
+ export function isAuthUserAllowed(user) {
128
+ const allowlist = readUserAllowlist();
129
+ if (!allowlist.enabled) return true;
130
+ const allowed = userAllowlistMatchSet(allowlist.users);
131
+ const candidates = [
132
+ String(user?.userId || "").trim().toLowerCase(),
133
+ String(user?.username || "").trim().toLowerCase(),
134
+ sanitizeAgentflowUserId(user?.userId),
135
+ sanitizeAgentflowUserId(user?.username),
136
+ ].filter(Boolean);
137
+ return candidates.some((candidate) => allowed.has(candidate));
138
+ }
139
+
85
140
  function listFlowDirs(root) {
86
141
  try {
87
142
  if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) return [];
@@ -184,6 +239,9 @@ export function loginOrCreateUser(username, password) {
184
239
  if (!userId) {
185
240
  return { ok: false, error: "用户名须以字母开头,仅可使用字母、数字、下划线与连字符,最多 64 字符" };
186
241
  }
242
+ if (!isAuthUserAllowed({ userId, username: String(username || "").trim() })) {
243
+ return { ok: false, forbidden: true, error: "用户不在白名单中,请联系管理员开通访问权限" };
244
+ }
187
245
  const pwd = String(password || "");
188
246
  if (pwd.length < 4) return { ok: false, error: "密码至少 4 位" };
189
247
 
@@ -257,11 +257,13 @@ export function listNodesJson(workspaceRoot, flowId, flowSource, opts = {}) {
257
257
  addFromDir(path.join(root, PROJECT_NODES_DIR), "project");
258
258
  for (const manifest of listMarketplaceNodes(root, marketplaceFlowData)) {
259
259
  let type = "agent";
260
- const runtimeType = String(manifest.runtime?.type || manifest.type || "").toLowerCase();
260
+ const runtimeType = String(manifest.baseDefinitionId || manifest.runtime?.type || manifest.type || "").toLowerCase();
261
261
  if (runtimeType.startsWith("control")) type = "control";
262
262
  else if (runtimeType.startsWith("provide")) type = "provide";
263
263
  byId.set(manifest.definitionId, {
264
264
  id: manifest.definitionId,
265
+ baseDefinitionId: manifest.baseDefinitionId || manifest.runtime?.type || "",
266
+ marketplaceDefinitionId: manifest.definitionId,
265
267
  packageId: manifest.id,
266
268
  version: manifest.version,
267
269
  type,
@@ -269,6 +269,8 @@ function buildAgentStepPrompt(step, flowContext) {
269
269
  parts.push(
270
270
  "## 上下文已就绪(禁止 forage)\n" +
271
271
  "- 节点定义见上方 schema 表,**禁止** Glob/Read `builtin/nodes/`、`.workspace/agentflow/nodes/`、历史 `runBuild/` 来推断节点结构。\n" +
272
+ "- 默认不要读取、搜索或 Glob 历史运行产物;除非用户明确要求分析历史 run/log,否则检索时必须排除 `**/runBuild/**`、`**/logs/**`、`.workspace/agentflow/**/runBuild/**`、`~/agentflow/runBuild/**`、`node_modules/**`、`dist/**`。\n" +
273
+ "- 不要从历史 runBuild/logs 中推断业务事实、指标资产或 skill 文档;优先读取当前 workspace 的源文件、analytics-docs、skills、配置与用户给出的上下文。\n" +
272
274
  "- 目标 instance 的当前 YAML 已附上(若 instanceId 已知);tool_nodejs 节点引用的 .mjs 脚本内容也已附上(若存在)。\n" +
273
275
  "- 如需查看整份 flow,仅在确实需要时读取一次。"
274
276
  );
@@ -165,8 +165,42 @@ export function listGitWorktrees(repoRoot) {
165
165
  }
166
166
 
167
167
  export function findRegisteredWorktree(repoRoot, worktreePath) {
168
- const target = path.resolve(worktreePath);
169
- return listGitWorktrees(repoRoot).find((entry) => path.resolve(entry.path) === target) || null;
168
+ const target = canonicalPathForCompare(worktreePath);
169
+ return listGitWorktrees(repoRoot).find((entry) => canonicalPathForCompare(entry.path) === target) || null;
170
+ }
171
+
172
+ function canonicalPathForCompare(rawPath) {
173
+ const abs = path.resolve(String(rawPath || ""));
174
+ try {
175
+ return fs.realpathSync.native(abs);
176
+ } catch {
177
+ /* missing path: canonicalize the nearest existing parent */
178
+ }
179
+ const missingParts = [];
180
+ let cursor = abs;
181
+ while (cursor && !fs.existsSync(cursor)) {
182
+ const parent = path.dirname(cursor);
183
+ if (parent === cursor) break;
184
+ missingParts.unshift(path.basename(cursor));
185
+ cursor = parent;
186
+ }
187
+ try {
188
+ return path.join(fs.realpathSync.native(cursor), ...missingParts);
189
+ } catch {
190
+ return abs;
191
+ }
192
+ }
193
+
194
+ function pruneGitWorktrees(repoRoot) {
195
+ const result = runGit(["worktree", "prune", "--expire", "now"], repoRoot);
196
+ if (result.status !== 0) {
197
+ throw new Error(`git worktree prune failed: ${result.stderr || result.stdout}`);
198
+ }
199
+ }
200
+
201
+ function isMissingRegisteredWorktreeError(result) {
202
+ const text = String(result?.stderr || result?.stdout || "");
203
+ return /missing but already registered worktree/i.test(text);
170
204
  }
171
205
 
172
206
  function branchExists(repoRoot, branch) {
@@ -187,7 +221,7 @@ function actualWorktreeBranch(worktreePath) {
187
221
  return branch === "HEAD" ? "DETACHED" : branch;
188
222
  }
189
223
 
190
- export function loadGitWorktree({ repoPath, branch = "", worktreePath = "", pipelineWorkspace }) {
224
+ export function loadGitWorktree({ repoPath, branch = "", worktreePath = "", pipelineWorkspace, force = false, pruneMissing = true }) {
191
225
  const repoRoot = resolveGitRepoRoot(repoPath);
192
226
  const wantedBranch = String(branch || "").trim();
193
227
  const target = path.resolve(worktreePath || defaultWorktreePath(pipelineWorkspace || repoRoot, repoRoot, wantedBranch));
@@ -196,7 +230,7 @@ export function loadGitWorktree({ repoPath, branch = "", worktreePath = "", pipe
196
230
  throw new Error(`branch does not exist in repoPath: ${wantedBranch}`);
197
231
  }
198
232
 
199
- const registered = findRegisteredWorktree(repoRoot, target);
233
+ let registered = findRegisteredWorktree(repoRoot, target);
200
234
  if (fs.existsSync(target)) {
201
235
  if (!registered) {
202
236
  throw new Error(`worktreePath exists but is not registered for repoPath: ${target}`);
@@ -205,11 +239,26 @@ export function loadGitWorktree({ repoPath, branch = "", worktreePath = "", pipe
205
239
  throw new Error(`worktreePath branch mismatch: expected ${wantedBranch}, got ${registered.branch || "DETACHED"}`);
206
240
  }
207
241
  } else {
242
+ if (registered && pruneMissing) {
243
+ pruneGitWorktrees(repoRoot);
244
+ registered = findRegisteredWorktree(repoRoot, target);
245
+ }
246
+ if (registered && !force) {
247
+ throw new Error(`worktreePath is missing but still registered: ${target}; set pruneMissing=true or force=true`);
248
+ }
208
249
  fs.mkdirSync(path.dirname(target), { recursive: true });
209
- const args = wantedBranch
210
- ? ["worktree", "add", target, wantedBranch]
211
- : ["worktree", "add", "--detach", target, "HEAD"];
212
- const result = runGit(args, repoRoot);
250
+ const args = ["worktree", "add"];
251
+ if (force) args.push("--force");
252
+ if (wantedBranch) {
253
+ args.push(target, wantedBranch);
254
+ } else {
255
+ args.push("--detach", target, "HEAD");
256
+ }
257
+ let result = runGit(args, repoRoot);
258
+ if (result.status !== 0 && pruneMissing && isMissingRegisteredWorktreeError(result)) {
259
+ pruneGitWorktrees(repoRoot);
260
+ result = runGit(args, repoRoot);
261
+ }
213
262
  if (result.status !== 0) {
214
263
  throw new Error(`git worktree add failed: ${result.stderr || result.stdout}`);
215
264
  }
@@ -329,6 +329,10 @@
329
329
  "displayName": "Image Display",
330
330
  "description": "Preview an image URL, data URL, or image path on the Workspace canvas and pass the source downstream"
331
331
  },
332
+ "display_chart": {
333
+ "displayName": "Chart Display",
334
+ "description": "Render ChartSpec/ECharts JSON on the Workspace canvas and pass the JSON downstream"
335
+ },
332
336
  "provide_str": {
333
337
  "displayName": "Text",
334
338
  "description": "Provide a text value directly, value will be passed to downstream as-is"
@@ -329,6 +329,10 @@
329
329
  "displayName": "图片展示",
330
330
  "description": "在 Workspace 画布中预览图片 URL、data URL 或图片路径,并将来源继续传给下游"
331
331
  },
332
+ "display_chart": {
333
+ "displayName": "图表展示",
334
+ "description": "在 Workspace 画布中渲染 ChartSpec/ECharts JSON,并将 JSON 继续传给下游"
335
+ },
332
336
  "provide_str": {
333
337
  "displayName": "文本",
334
338
  "description": "直接提供一段文本,value 会原样供下游引用"
@@ -85,12 +85,21 @@ function normalizeManifest(raw, packageDir, source = "workspace") {
85
85
  const version = raw.version != null ? String(raw.version).trim() : "";
86
86
  if (!id || !version) return null;
87
87
  const runtime = raw.runtime && typeof raw.runtime === "object" ? raw.runtime : {};
88
+ const baseDefinitionId =
89
+ raw.baseDefinitionId != null && String(raw.baseDefinitionId).trim() !== ""
90
+ ? String(raw.baseDefinitionId).trim()
91
+ : raw.sourceDefinitionId != null && String(raw.sourceDefinitionId).trim() !== ""
92
+ ? String(raw.sourceDefinitionId).trim()
93
+ : runtime.type != null && String(runtime.type).trim() !== ""
94
+ ? String(runtime.type).trim()
95
+ : "";
88
96
  return {
89
97
  ...raw,
90
98
  id,
91
99
  version,
92
100
  packageDir,
93
101
  definitionId: `marketplace:${id}@${version}`,
102
+ baseDefinitionId,
94
103
  displayName: raw.displayName != null ? String(raw.displayName) : raw.name != null ? String(raw.name) : id,
95
104
  description: raw.description != null ? String(raw.description) : "",
96
105
  input: normalizeSlotList(raw.input || raw.inputs),
@@ -176,8 +185,11 @@ function depMatchesNode(dep, id, version) {
176
185
  }
177
186
 
178
187
  function instanceMatchesNode(inst, id, version) {
179
- const parsed = parseMarketplaceDefinitionId(inst?.definitionId);
180
- return Boolean(parsed && parsed.id === id && (!parsed.version || parsed.version === version));
188
+ const parsed =
189
+ parseMarketplaceDefinitionId(inst?.definitionId) ||
190
+ parseMarketplaceDefinitionId(inst?.marketplaceRef);
191
+ if (parsed) return Boolean(parsed.id === id && (!parsed.version || parsed.version === version));
192
+ return inst?.marketplacePackageId === id && (inst?.marketplaceVersion == null || String(inst.marketplaceVersion) === version);
181
193
  }
182
194
 
183
195
  export function listMarketplaceNodeUsages(workspaceRoot, id, version, opts = {}) {
@@ -349,6 +361,7 @@ export function listMarketplacePackages(workspaceRoot, opts = {}) {
349
361
  id: n.id,
350
362
  version: n.version,
351
363
  definitionId: n.definitionId,
364
+ baseDefinitionId: n.baseDefinitionId,
352
365
  displayName: n.displayName,
353
366
  description: n.description,
354
367
  inputs: n.input,
@@ -440,7 +453,7 @@ export function writeFlowMarketplaceLock(workspaceRoot, flowDir, flowData) {
440
453
  if (!flowData || !flowData.instances || typeof flowData.instances !== "object") return null;
441
454
  const nodes = {};
442
455
  for (const inst of Object.values(flowData.instances)) {
443
- const defId = inst && inst.definitionId;
456
+ const defId = inst && (inst.marketplaceRef || inst.definitionId);
444
457
  const resolved = resolveMarketplaceNodePackage(workspaceRoot, flowDir, defId, flowData);
445
458
  if (!resolved) continue;
446
459
  nodes[resolved.id] = {
@@ -630,6 +643,7 @@ export function publishNodeFromInstance(workspaceRoot, payload = {}, options = {
630
643
  const label = String(payload.label || payload.instanceId || "node").trim();
631
644
  const id = safePackageId(payload.id || payload.packageId || label);
632
645
  const version = normalizeVersion(payload.version || "1.0.0");
646
+ const sourceDefinitionId = String(payload.definitionId || "").trim();
633
647
  if (!id) return { ok: false, error: "Invalid package id" };
634
648
 
635
649
  const inputs = normalizeSlotList(payload.inputs || payload.input).map((slot) => ({
@@ -667,7 +681,7 @@ export function publishNodeFromInstance(workspaceRoot, payload = {}, options = {
667
681
  command: script,
668
682
  }
669
683
  : {
670
- type: "agent_subAgent",
684
+ type: sourceDefinitionId || "agent_subAgent",
671
685
  }
672
686
  );
673
687
  const manifest = {
@@ -675,6 +689,7 @@ export function publishNodeFromInstance(workspaceRoot, payload = {}, options = {
675
689
  version,
676
690
  name: label,
677
691
  description,
692
+ baseDefinitionId: sourceDefinitionId || runtime.type || "agent_subAgent",
678
693
  runtime,
679
694
  inputs,
680
695
  outputs,
@@ -693,6 +708,8 @@ export function publishNodeFromInstance(workspaceRoot, payload = {}, options = {
693
708
  version,
694
709
  packageDir: dest,
695
710
  definitionId: `marketplace:${id}@${version}`,
711
+ baseDefinitionId: manifest.baseDefinitionId,
712
+ marketplaceDefinitionId: `marketplace:${id}@${version}`,
696
713
  packagedFiles: packagedScript?.packagedFiles || [],
697
714
  };
698
715
  }
@@ -1,4 +1,5 @@
1
1
  import path from "path";
2
+ import yaml from "js-yaml";
2
3
  import { getFlowDir } from "./workspace.mjs";
3
4
  import { PACKAGE_ROOT, PIPELINES_DIR } from "./paths.mjs";
4
5
  import { listSkills, listSkillsFromSources, workspaceSkillSources } from "./skill-registry.mjs";
@@ -118,6 +119,7 @@ function skillBodyFromRegistryItem(skill) {
118
119
  name: skill.name,
119
120
  key: skill.key,
120
121
  description: skill.description,
122
+ frontmatter: skill.frontmatter,
121
123
  source: skill.source,
122
124
  sourceLabel: skill.sourceLabel,
123
125
  path: skill.path,
@@ -201,6 +203,7 @@ export function buildSkillsContext({ workspaceContext, source = "current-workspa
201
203
  name: s.name,
202
204
  key: s.key,
203
205
  description: s.description,
206
+ frontmatter: s.frontmatter,
204
207
  sourceLabel: s.sourceLabel,
205
208
  path: s.path,
206
209
  body: s.body,
@@ -208,18 +211,33 @@ export function buildSkillsContext({ workspaceContext, source = "current-workspa
208
211
  };
209
212
  }
210
213
 
214
+ function renderSkillFrontmatter(frontmatter) {
215
+ if (!frontmatter || typeof frontmatter !== "object" || Array.isArray(frontmatter)) return "";
216
+ const extra = Object.fromEntries(
217
+ Object.entries(frontmatter).filter(([key, value]) => {
218
+ if (key === "name" || key === "description") return false;
219
+ if (value == null) return false;
220
+ if (typeof value === "string" && value.trim() === "") return false;
221
+ return true;
222
+ }),
223
+ );
224
+ if (Object.keys(extra).length === 0) return "";
225
+ return yaml.dump(extra, { lineWidth: 100, noRefs: true }).trim();
226
+ }
227
+
211
228
  export function renderSkillsContextForPrompt(skillsContext) {
212
229
  const ctx = normalizeSkillsContext(skillsContext);
213
230
  if (!ctx || !Array.isArray(ctx.skillBodies) || ctx.skillBodies.length === 0) return "";
214
231
  const blocks = ctx.skillBodies.slice(0, 20).map((skill) => {
215
232
  const body = String(skill.body || "").trim();
216
- return [
233
+ const frontmatter = renderSkillFrontmatter(skill.frontmatter);
234
+ const header = [
217
235
  `### ${skill.name}`,
218
236
  skill.description ? `说明:${skill.description}` : "",
219
237
  `来源:${skill.path || skill.sourceLabel || ""}`,
220
- "",
221
- body.slice(0, 16000),
238
+ frontmatter ? `元数据:\n${frontmatter}` : "",
222
239
  ].filter(Boolean).join("\n");
240
+ return [header, body.slice(0, 16000)].filter(Boolean).join("\n\n");
223
241
  });
224
- return ["## 已加载 Skills", "", ...blocks].join("\n\n");
242
+ return ["## 已加载 Skills", ...blocks].join("\n\n");
225
243
  }
@@ -38,7 +38,8 @@ function parseSkillFrontmatter(content) {
38
38
  const match = String(content || "").match(/^---\s*\r?\n([\s\S]*?)\r?\n---/);
39
39
  if (!match) return {};
40
40
  try {
41
- return yaml.load(match[1]) || {};
41
+ const parsed = yaml.load(match[1]) || {};
42
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
42
43
  } catch {
43
44
  return {};
44
45
  }
@@ -58,6 +59,7 @@ export function parseSkillFile(absPath, source = {}) {
58
59
  id: name,
59
60
  name,
60
61
  description: String(meta.description || "").trim(),
62
+ frontmatter: meta,
61
63
  source: sourceId,
62
64
  sourceLabel,
63
65
  path: absPath,