@fieldwangai/agentflow 0.1.34 → 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/agent-runners.mjs +17 -0
- package/bin/lib/auth.mjs +58 -0
- package/bin/lib/catalog-flows.mjs +3 -1
- package/bin/lib/composer-agent.mjs +4 -0
- package/bin/lib/composer-skill-router.mjs +23 -4
- package/bin/lib/git-worktree.mjs +57 -8
- package/bin/lib/locales/en.json +4 -0
- package/bin/lib/locales/zh.json +4 -0
- package/bin/lib/marketplace.mjs +21 -4
- package/bin/lib/runtime-context.mjs +22 -4
- package/bin/lib/skill-registry.mjs +49 -3
- package/bin/lib/ui-server.mjs +1177 -29
- package/bin/pipeline/pre-process-node.mjs +4 -0
- package/builtin/nodes/agent_subAgent.md +4 -1
- package/builtin/nodes/display_chart.md +31 -0
- package/builtin/nodes/tool_git_worktree_load.md +10 -0
- package/builtin/nodes/tool_nodejs.md +3 -0
- package/builtin/web-ui/dist/assets/index-7-343AUn.js +214 -0
- package/builtin/web-ui/dist/assets/index-CPsrRISH.css +1 -0
- package/builtin/web-ui/dist/assets/index-DgQRkS4v.js +61 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-B1j_UaHw.js +0 -202
- package/builtin/web-ui/dist/assets/index-ChiTnW0H.css +0 -1
|
@@ -711,6 +711,14 @@ function truncateComposerLine(s) {
|
|
|
711
711
|
return t.slice(0, COMPOSER_STATUS_MAX - 1) + "…";
|
|
712
712
|
}
|
|
713
713
|
|
|
714
|
+
const RAW_TRACE_MAX_CHARS = 4096;
|
|
715
|
+
|
|
716
|
+
function rawTraceText(value) {
|
|
717
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
718
|
+
if (!text) return "";
|
|
719
|
+
return text.length > RAW_TRACE_MAX_CHARS ? text.slice(0, RAW_TRACE_MAX_CHARS) + "\n...[truncated]" : text;
|
|
720
|
+
}
|
|
721
|
+
|
|
714
722
|
function normalizeStreamTextChunk(t) {
|
|
715
723
|
if (!t || typeof t !== "string") return "";
|
|
716
724
|
return t.replace(/\\n/g, "\n").replace(/\\t/g, "\t");
|
|
@@ -728,6 +736,7 @@ function extractCursorStreamNlText(event) {
|
|
|
728
736
|
}
|
|
729
737
|
if (typeof event.text === "string" && event.text.trim()) return normalizeStreamTextChunk(event.text);
|
|
730
738
|
if (typeof event.thinking === "string" && event.thinking.trim()) return normalizeStreamTextChunk(event.thinking);
|
|
739
|
+
if (typeof event.delta === "string" && event.delta.trim()) return normalizeStreamTextChunk(event.delta);
|
|
731
740
|
return "";
|
|
732
741
|
}
|
|
733
742
|
|
|
@@ -864,6 +873,7 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
864
873
|
for (const line of lines) {
|
|
865
874
|
try {
|
|
866
875
|
const event = JSON.parse(line);
|
|
876
|
+
emit({ type: "raw", source: "cursor", stream: "stdout", eventType: event?.type || "unknown", text: rawTraceText(event) });
|
|
867
877
|
if (event.type === "assistant" && event.message?.content) {
|
|
868
878
|
const text = extractCursorStreamNlText(event);
|
|
869
879
|
if (text) {
|
|
@@ -903,6 +913,7 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
903
913
|
emit({ type: "status", line: `${t("runner.event_label")}: ${event.type ?? "unknown"}` });
|
|
904
914
|
}
|
|
905
915
|
} catch (_) {
|
|
916
|
+
emit({ type: "raw", source: "cursor", stream: "stdout", eventType: "line", text: rawTraceText(line) });
|
|
906
917
|
if (line.includes('"type":"tool_call"') || line.includes('"type": "tool_call"')) {
|
|
907
918
|
let subtype = "?";
|
|
908
919
|
try {
|
|
@@ -1017,6 +1028,7 @@ export function runOpenCodeAgentWithPrompt(cliWorkspace, promptText, options = {
|
|
|
1017
1028
|
const line = outBuf.slice(0, idx);
|
|
1018
1029
|
outBuf = outBuf.slice(idx + 1);
|
|
1019
1030
|
if (line) {
|
|
1031
|
+
emit({ type: "raw", source: "opencode", stream: "stdout", eventType: "line", text: rawTraceText(line) });
|
|
1020
1032
|
tryEmitOpenCodeLineAsNatural(line, emit);
|
|
1021
1033
|
emit({ type: "status", line: `[stdout] ${truncateComposerLine(line)}` });
|
|
1022
1034
|
}
|
|
@@ -1032,6 +1044,7 @@ export function runOpenCodeAgentWithPrompt(cliWorkspace, promptText, options = {
|
|
|
1032
1044
|
const line = errBuf.slice(0, idx);
|
|
1033
1045
|
errBuf = errBuf.slice(idx + 1);
|
|
1034
1046
|
if (line) {
|
|
1047
|
+
emit({ type: "raw", source: "opencode", stream: "stderr", eventType: "line", text: rawTraceText(line) });
|
|
1035
1048
|
tryEmitOpenCodeLineAsNatural(line, emit);
|
|
1036
1049
|
emit({ type: "status", line: `[stderr] ${truncateComposerLine(line)}` });
|
|
1037
1050
|
}
|
|
@@ -1051,10 +1064,12 @@ export function runOpenCodeAgentWithPrompt(cliWorkspace, promptText, options = {
|
|
|
1051
1064
|
child.stderr.removeAllListeners();
|
|
1052
1065
|
child.removeAllListeners();
|
|
1053
1066
|
if (outBuf.trim()) {
|
|
1067
|
+
emit({ type: "raw", source: "opencode", stream: "stdout", eventType: "tail", text: rawTraceText(outBuf.trim()) });
|
|
1054
1068
|
tryEmitOpenCodeLineAsNatural(outBuf.trim(), emit);
|
|
1055
1069
|
emit({ type: "status", line: truncateComposerLine(outBuf.trim()) });
|
|
1056
1070
|
}
|
|
1057
1071
|
if (errBuf.trim()) {
|
|
1072
|
+
emit({ type: "raw", source: "opencode", stream: "stderr", eventType: "tail", text: rawTraceText(errBuf.trim()) });
|
|
1058
1073
|
tryEmitOpenCodeLineAsNatural(errBuf.trim(), emit);
|
|
1059
1074
|
emit({ type: "status", line: `[opencode_stderr] ${truncateComposerLine(errBuf.trim())}` });
|
|
1060
1075
|
}
|
|
@@ -1151,6 +1166,7 @@ export function runClaudeCodeAgentWithPrompt(cliWorkspace, promptText, options =
|
|
|
1151
1166
|
for (const line of lines) {
|
|
1152
1167
|
try {
|
|
1153
1168
|
const event = JSON.parse(line);
|
|
1169
|
+
emit({ type: "raw", source: "claude-code", stream: "stdout", eventType: event?.type || "unknown", text: rawTraceText(event) });
|
|
1154
1170
|
if (event.type === "assistant" && event.message && Array.isArray(event.message.content)) {
|
|
1155
1171
|
for (const block of event.message.content) {
|
|
1156
1172
|
if (!block || typeof block !== "object") continue;
|
|
@@ -1196,6 +1212,7 @@ export function runClaudeCodeAgentWithPrompt(cliWorkspace, promptText, options =
|
|
|
1196
1212
|
emit({ type: "status", line: `${t("runner.event_label")}: ${event.type ?? "unknown"}` });
|
|
1197
1213
|
}
|
|
1198
1214
|
} catch (_) {
|
|
1215
|
+
emit({ type: "raw", source: "claude-code", stream: "stdout", eventType: "line", text: rawTraceText(line) });
|
|
1199
1216
|
emit({ type: "status", line: truncateComposerLine(line) });
|
|
1200
1217
|
}
|
|
1201
1218
|
}
|
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,
|
|
@@ -106,6 +106,7 @@ export function buildScriptContentBlockForInstances(flowYamlAbs, instanceIds) {
|
|
|
106
106
|
* @param {string} [opts.modelKey]
|
|
107
107
|
* @param {boolean} [opts.force]
|
|
108
108
|
* @param {(ev: object) => void} [opts.onStreamEvent]
|
|
109
|
+
* @param {(subtype: string, toolName: string) => void} [opts.onToolCall]
|
|
109
110
|
* @returns {{ child: import('child_process').ChildProcess, finished: Promise<void> }}
|
|
110
111
|
*/
|
|
111
112
|
export function startComposerAgent(opts) {
|
|
@@ -123,6 +124,7 @@ export function startComposerAgent(opts) {
|
|
|
123
124
|
|
|
124
125
|
const common = {
|
|
125
126
|
onStreamEvent: opts.onStreamEvent,
|
|
127
|
+
onToolCall: opts.onToolCall,
|
|
126
128
|
force: Boolean(opts.force),
|
|
127
129
|
env,
|
|
128
130
|
};
|
|
@@ -267,6 +269,8 @@ function buildAgentStepPrompt(step, flowContext) {
|
|
|
267
269
|
parts.push(
|
|
268
270
|
"## 上下文已就绪(禁止 forage)\n" +
|
|
269
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" +
|
|
270
274
|
"- 目标 instance 的当前 YAML 已附上(若 instanceId 已知);tool_nodejs 节点引用的 .mjs 脚本内容也已附上(若存在)。\n" +
|
|
271
275
|
"- 如需查看整份 flow,仅在确实需要时读取一次。"
|
|
272
276
|
);
|
|
@@ -13,7 +13,11 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import fs from "fs";
|
|
15
15
|
import path from "path";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
listSkills as registryListSkills,
|
|
18
|
+
listUniqueSkills as registryListUniqueSkills,
|
|
19
|
+
readSkillDetail as registryReadSkillDetail,
|
|
20
|
+
} from "./skill-registry.mjs";
|
|
17
21
|
|
|
18
22
|
// ─── 意图模式定义 ─────────────────────────────────────────────────────────
|
|
19
23
|
|
|
@@ -126,7 +130,7 @@ function readFileCached(absPath) {
|
|
|
126
130
|
}
|
|
127
131
|
|
|
128
132
|
export function listComposerSkills(packageRoot, workspaceRoot) {
|
|
129
|
-
return
|
|
133
|
+
return registryListUniqueSkills(packageRoot, workspaceRoot).map(({ body, content, ...skill }) => skill);
|
|
130
134
|
}
|
|
131
135
|
|
|
132
136
|
export function readComposerSkillDetail(packageRoot, workspaceRoot, keyOrName) {
|
|
@@ -140,9 +144,24 @@ export function loadResourcesForSkillKeys(skillKeys, packageRoot, workspaceRoot)
|
|
|
140
144
|
const wanted = new Set(skillKeys.map((x) => String(x || "").trim()).filter(Boolean));
|
|
141
145
|
if (wanted.size === 0) return { skills: [], references: [], skillsHint: "", hasContext: false };
|
|
142
146
|
|
|
143
|
-
const
|
|
144
|
-
|
|
147
|
+
const exactByKey = new Map(registryListSkills(packageRoot, workspaceRoot).map((item) => [item.key, item]));
|
|
148
|
+
const candidateItems = [];
|
|
149
|
+
const seenKeys = new Set();
|
|
150
|
+
for (const item of registryListUniqueSkills(packageRoot, workspaceRoot)) {
|
|
145
151
|
if (!wanted.has(item.key) && !wanted.has(item.name)) continue;
|
|
152
|
+
candidateItems.push(item);
|
|
153
|
+
seenKeys.add(item.key);
|
|
154
|
+
}
|
|
155
|
+
for (const key of wanted) {
|
|
156
|
+
const exact = exactByKey.get(key);
|
|
157
|
+
if (exact && !seenKeys.has(exact.key)) {
|
|
158
|
+
candidateItems.push(exact);
|
|
159
|
+
seenKeys.add(exact.key);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const skills = [];
|
|
164
|
+
for (const item of candidateItems) {
|
|
146
165
|
skills.push({
|
|
147
166
|
id: item.name,
|
|
148
167
|
content: item.body,
|
package/bin/lib/git-worktree.mjs
CHANGED
|
@@ -165,8 +165,42 @@ export function listGitWorktrees(repoRoot) {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
export function findRegisteredWorktree(repoRoot, worktreePath) {
|
|
168
|
-
const target =
|
|
169
|
-
return listGitWorktrees(repoRoot).find((entry) =>
|
|
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
|
-
|
|
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 =
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
}
|
package/bin/lib/locales/en.json
CHANGED
|
@@ -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"
|
package/bin/lib/locales/zh.json
CHANGED
|
@@ -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 会原样供下游引用"
|
package/bin/lib/marketplace.mjs
CHANGED
|
@@ -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 =
|
|
180
|
-
|
|
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
|
-
|
|
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",
|
|
242
|
+
return ["## 已加载 Skills", ...blocks].join("\n\n");
|
|
225
243
|
}
|
|
@@ -5,6 +5,15 @@ import yaml from "js-yaml";
|
|
|
5
5
|
|
|
6
6
|
const fileCache = new Map();
|
|
7
7
|
const CACHE_TTL_MS = 60_000;
|
|
8
|
+
const SOURCE_PRIORITY = new Map([
|
|
9
|
+
["workspace-agents", 100],
|
|
10
|
+
["workspace-codex", 95],
|
|
11
|
+
["workspace-cursor", 90],
|
|
12
|
+
["builtin", 80],
|
|
13
|
+
["global-agents", 70],
|
|
14
|
+
["global-codex", 65],
|
|
15
|
+
["global-cursor", 60],
|
|
16
|
+
]);
|
|
8
17
|
|
|
9
18
|
function readFileCached(absPath) {
|
|
10
19
|
const now = Date.now();
|
|
@@ -29,7 +38,8 @@ function parseSkillFrontmatter(content) {
|
|
|
29
38
|
const match = String(content || "").match(/^---\s*\r?\n([\s\S]*?)\r?\n---/);
|
|
30
39
|
if (!match) return {};
|
|
31
40
|
try {
|
|
32
|
-
|
|
41
|
+
const parsed = yaml.load(match[1]) || {};
|
|
42
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
33
43
|
} catch {
|
|
34
44
|
return {};
|
|
35
45
|
}
|
|
@@ -49,6 +59,7 @@ export function parseSkillFile(absPath, source = {}) {
|
|
|
49
59
|
id: name,
|
|
50
60
|
name,
|
|
51
61
|
description: String(meta.description || "").trim(),
|
|
62
|
+
frontmatter: meta,
|
|
52
63
|
source: sourceId,
|
|
53
64
|
sourceLabel,
|
|
54
65
|
path: absPath,
|
|
@@ -62,7 +73,15 @@ export function listSkillFiles(dir) {
|
|
|
62
73
|
try {
|
|
63
74
|
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return [];
|
|
64
75
|
return fs.readdirSync(dir, { withFileTypes: true })
|
|
65
|
-
.filter((entry) =>
|
|
76
|
+
.filter((entry) => {
|
|
77
|
+
if (entry.isDirectory()) return true;
|
|
78
|
+
if (!entry.isSymbolicLink()) return false;
|
|
79
|
+
try {
|
|
80
|
+
return fs.statSync(path.join(dir, entry.name)).isDirectory();
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
})
|
|
66
85
|
.map((entry) => path.join(dir, entry.name, "SKILL.md"))
|
|
67
86
|
.filter((skillPath) => fs.existsSync(skillPath));
|
|
68
87
|
} catch {
|
|
@@ -132,14 +151,41 @@ export function listSkillsFromSources(sources, opts = {}) {
|
|
|
132
151
|
return { skills, warnings };
|
|
133
152
|
}
|
|
134
153
|
|
|
154
|
+
function skillPriority(skill) {
|
|
155
|
+
return SOURCE_PRIORITY.get(String(skill?.source || "")) ?? 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function dedupeSkillsByName(skills) {
|
|
159
|
+
const byName = new Map();
|
|
160
|
+
for (const skill of Array.isArray(skills) ? skills : []) {
|
|
161
|
+
const name = String(skill?.name || skill?.id || "").trim();
|
|
162
|
+
if (!name) continue;
|
|
163
|
+
const existing = byName.get(name);
|
|
164
|
+
if (!existing || skillPriority(skill) > skillPriority(existing)) {
|
|
165
|
+
byName.set(name, skill);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return Array.from(byName.values()).sort((a, b) => {
|
|
169
|
+
const bySource = String(a.sourceLabel || "").localeCompare(String(b.sourceLabel || ""));
|
|
170
|
+
if (bySource !== 0) return bySource;
|
|
171
|
+
return String(a.name || "").localeCompare(String(b.name || ""));
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
135
175
|
export function listSkills(packageRoot, workspaceRoot, opts = {}) {
|
|
136
176
|
return listSkillsFromSources(defaultSkillSources(packageRoot, workspaceRoot), opts).skills;
|
|
137
177
|
}
|
|
138
178
|
|
|
179
|
+
export function listUniqueSkills(packageRoot, workspaceRoot, opts = {}) {
|
|
180
|
+
return dedupeSkillsByName(listSkills(packageRoot, workspaceRoot, opts));
|
|
181
|
+
}
|
|
182
|
+
|
|
139
183
|
export function readSkillDetail(packageRoot, workspaceRoot, keyOrName) {
|
|
140
184
|
const wanted = String(keyOrName || "").trim();
|
|
141
185
|
if (!wanted) return null;
|
|
142
|
-
const
|
|
186
|
+
const all = listSkills(packageRoot, workspaceRoot);
|
|
187
|
+
const item = all.find((skill) => skill.key === wanted)
|
|
188
|
+
|| dedupeSkillsByName(all).find((skill) => skill.name === wanted);
|
|
143
189
|
if (!item) return null;
|
|
144
190
|
return item;
|
|
145
191
|
}
|