mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
refactor(inventory): 优化技能与代理配置代码格式和检测逻辑
- 统一代码格式,增加多处代码块的换行和缩进保持一致 - 调整技能安装目标列表的格式,提升可读性 - 修复解压缩逻辑中异常抛出格式,增强异常信息规范 - 优化归一化文件名过滤条件表达式格式 - 修改配置文件检测逻辑,兼容环境变量和旧版配置方案 - 增强对 Bailian 相关模型提供者的检测逻辑支持 - 规范代理详情字段生成方法的代码风格 - 调整 MCP 写回相关函数的格式,提升可维护性 - 改进技能和代理详情函数参数格式,统一参数拆分显示 - 修复单元测试中路径和 JSON 写入格式,增加不同配置场景测试覆盖 - 确保软链接技能目录被正确识别为安装来源 - 增加多代理配置文件和技能安装的检测测试用例,提升测试精准度
This commit is contained in:
@@ -144,7 +144,10 @@ function skillRoots(home: string): Array<{ source: string; dir: string }> {
|
||||
{ source: "openclaw", dir: join(home, ".openclaw", "workspace", "skills") },
|
||||
{ source: "hermes", dir: join(home, ".hermes", "skills") },
|
||||
{ source: "gemini", dir: join(home, ".gemini", "skills") },
|
||||
{ source: "antigravity", dir: join(home, ".gemini", "antigravity", "skills") },
|
||||
{
|
||||
source: "antigravity",
|
||||
dir: join(home, ".gemini", "antigravity", "skills"),
|
||||
},
|
||||
{ source: "windsurf", dir: join(home, ".windsurf", "skills") },
|
||||
{ source: "windsurf", dir: join(home, ".codeium", "windsurf", "skills") },
|
||||
{ source: "qoderwork", dir: join(home, ".qoderwork", "skills") },
|
||||
@@ -228,21 +231,43 @@ export function getSkillDetail(id: string, home: string = homedir()): SkillDetai
|
||||
// ---- Skill install (upload a .zip and unpack it into a skill root) ----
|
||||
|
||||
/** Allow-listed skill install targets: source id -> label + path segments. */
|
||||
const SKILL_INSTALL_TARGETS: Array<{ source: string; label: string; sub: string[] }> = [
|
||||
{ source: "global", label: "All agents (~/.agents/skills)", sub: [".agents", "skills"] },
|
||||
const SKILL_INSTALL_TARGETS: Array<{
|
||||
source: string;
|
||||
label: string;
|
||||
sub: string[];
|
||||
}> = [
|
||||
{
|
||||
source: "global",
|
||||
label: "All agents (~/.agents/skills)",
|
||||
sub: [".agents", "skills"],
|
||||
},
|
||||
{ source: "claude-code", label: "Claude Code", sub: [".claude", "skills"] },
|
||||
{ source: "qwen-code", label: "Qwen Code", sub: [".qwen", "skills"] },
|
||||
{ source: "codex", label: "Codex", sub: [".codex", "skills"] },
|
||||
{ source: "opencode", label: "OpenCode", sub: [".config", "opencode", "skills"] },
|
||||
{
|
||||
source: "opencode",
|
||||
label: "OpenCode",
|
||||
sub: [".config", "opencode", "skills"],
|
||||
},
|
||||
{ source: "openclaw", label: "OpenClaw", sub: [".openclaw", "skills"] },
|
||||
{ source: "qoderwork", label: "QoderWork", sub: [".qoderwork", "skills"] },
|
||||
{ source: "windsurf", label: "Windsurf", sub: [".codeium", "windsurf", "skills"] },
|
||||
{
|
||||
source: "windsurf",
|
||||
label: "Windsurf",
|
||||
sub: [".codeium", "windsurf", "skills"],
|
||||
},
|
||||
{ source: "gemini", label: "Gemini", sub: [".gemini", "skills"] },
|
||||
];
|
||||
|
||||
/** The list of install targets exposed to the UI (source + human label). */
|
||||
export function skillInstallTargets(): Array<{ source: string; label: string }> {
|
||||
return SKILL_INSTALL_TARGETS.map((t) => ({ source: t.source, label: t.label }));
|
||||
export function skillInstallTargets(): Array<{
|
||||
source: string;
|
||||
label: string;
|
||||
}> {
|
||||
return SKILL_INSTALL_TARGETS.map((t) => ({
|
||||
source: t.source,
|
||||
label: t.label,
|
||||
}));
|
||||
}
|
||||
|
||||
function skillInstallRoot(source: string, home: string): string | null {
|
||||
@@ -511,7 +536,11 @@ function mcpWriteTarget(source: string, scope: string, home: string): McpWriteTa
|
||||
projectScoped: false,
|
||||
};
|
||||
if (source === "cursor")
|
||||
return { file: join(home, ".cursor", "mcp.json"), mapKey: "mcpServers", projectScoped: false };
|
||||
return {
|
||||
file: join(home, ".cursor", "mcp.json"),
|
||||
mapKey: "mcpServers",
|
||||
projectScoped: false,
|
||||
};
|
||||
if (source === "windsurf")
|
||||
return {
|
||||
file: join(home, ".codeium", "windsurf", "mcp_config.json"),
|
||||
@@ -537,7 +566,11 @@ function mcpWriteTarget(source: string, scope: string, home: string): McpWriteTa
|
||||
projectScoped: false,
|
||||
};
|
||||
if (source === "claude-desktop")
|
||||
return { file: claudeDesktopConfigPath(home), mapKey: "mcpServers", projectScoped: false };
|
||||
return {
|
||||
file: claudeDesktopConfigPath(home),
|
||||
mapKey: "mcpServers",
|
||||
projectScoped: false,
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -643,9 +676,14 @@ const AGENT_PROBES: AgentProbe[] = [
|
||||
{
|
||||
id: "claude-code",
|
||||
label: "Claude Code",
|
||||
paths: (h) => [join(h, ".claude", "settings.json"), join(h, ".claude.json")],
|
||||
// The agent writer honors CLAUDE_CONFIG_DIR, so probe the same location.
|
||||
paths: (h) => [
|
||||
join(process.env.CLAUDE_CONFIG_DIR || join(h, ".claude"), "settings.json"),
|
||||
join(h, ".claude.json"),
|
||||
],
|
||||
detect: (h) => {
|
||||
const env = asRecord(readJsonSafe(join(h, ".claude", "settings.json"))?.env);
|
||||
const configDir = process.env.CLAUDE_CONFIG_DIR || join(h, ".claude");
|
||||
const env = asRecord(readJsonSafe(join(configDir, "settings.json"))?.env);
|
||||
const baseUrl =
|
||||
env && typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : undefined;
|
||||
const model =
|
||||
@@ -660,9 +698,19 @@ const AGENT_PROBES: AgentProbe[] = [
|
||||
detect: (h) => {
|
||||
const settings = readJsonSafe(join(h, ".qwen", "settings.json"));
|
||||
const providers = asRecord(settings?.modelProviders);
|
||||
// The writer brands entries either "bailian-cli" (legacy) or with a
|
||||
// "[Bailian] <model>" display name.
|
||||
const hasBailian = providers
|
||||
? Object.values(providers).some(
|
||||
(list) => Array.isArray(list) && list.some((e) => asRecord(e)?.name === "bailian-cli"),
|
||||
(list) =>
|
||||
Array.isArray(list) &&
|
||||
list.some((entry) => {
|
||||
const name = asRecord(entry)?.name;
|
||||
return (
|
||||
typeof name === "string" &&
|
||||
(name === "bailian-cli" || name.startsWith("[Bailian]"))
|
||||
);
|
||||
}),
|
||||
)
|
||||
: false;
|
||||
const model = asRecord(settings?.model);
|
||||
@@ -718,10 +766,21 @@ const AGENT_PROBES: AgentProbe[] = [
|
||||
return { configured: false };
|
||||
}
|
||||
const providers = Array.isArray(config?.custom_providers) ? config.custom_providers : [];
|
||||
const configured = providers.some((p) => asRecord(p)?.name === "bailian-cli");
|
||||
const legacyConfigured = providers.some(
|
||||
(provider) => asRecord(provider)?.name === "bailian-cli",
|
||||
);
|
||||
// The writer now emits the official flat `model.*` block (provider
|
||||
// "custom" + a DashScope/Token Plan base_url); keep detecting legacy
|
||||
// custom_providers entries written by older CLI versions.
|
||||
const model = asRecord(config?.model);
|
||||
const flatConfigured = Boolean(
|
||||
model &&
|
||||
model.provider === "custom" &&
|
||||
typeof model.base_url === "string" &&
|
||||
model.base_url.includes("aliyuncs.com"),
|
||||
);
|
||||
return {
|
||||
configured,
|
||||
configured: legacyConfigured || flatConfigured,
|
||||
model: model && typeof model.default === "string" ? model.default : undefined,
|
||||
};
|
||||
},
|
||||
@@ -937,7 +996,11 @@ export function getAgentDetail(id: string, home: string = homedir()): AgentDetai
|
||||
const settings: AgentSettingsFile[] = installed
|
||||
? paths
|
||||
.filter((p) => existsSync(p))
|
||||
.map((p) => ({ path: p, lang: langForPath(p), text: readText(p) ?? "" }))
|
||||
.map((p) => ({
|
||||
path: p,
|
||||
lang: langForPath(p),
|
||||
text: readText(p) ?? "",
|
||||
}))
|
||||
.filter((s) => s.text.trim())
|
||||
: [];
|
||||
return {
|
||||
|
||||
@@ -110,15 +110,27 @@ test("listMcpServers 汇总 codex(toml) 与 claude(json) 的 MCP 定义", () =>
|
||||
".claude.json",
|
||||
JSON.stringify({
|
||||
mcpServers: { web: { url: "https://example.com/mcp", type: "sse" } },
|
||||
projects: { "/proj": { mcpServers: { local: { command: "python", args: ["s.py"] } } } },
|
||||
projects: {
|
||||
"/proj": {
|
||||
mcpServers: { local: { command: "python", args: ["s.py"] } },
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const servers = listMcpServers(home);
|
||||
const byName = Object.fromEntries(servers.map((s) => [s.name, s]));
|
||||
expect(byName.repl).toMatchObject({ source: "codex", transport: "stdio", origin: "local" });
|
||||
expect(byName.repl).toMatchObject({
|
||||
source: "codex",
|
||||
transport: "stdio",
|
||||
origin: "local",
|
||||
});
|
||||
expect(byName.repl.detail).toContain("node repl.js");
|
||||
expect(byName.web).toMatchObject({ source: "claude-code", transport: "sse", scope: "global" });
|
||||
expect(byName.web).toMatchObject({
|
||||
source: "claude-code",
|
||||
transport: "sse",
|
||||
scope: "global",
|
||||
});
|
||||
expect(byName.local).toMatchObject({
|
||||
source: "claude-code",
|
||||
transport: "stdio",
|
||||
@@ -139,10 +151,35 @@ test("listAgents 报告安装与已连接 bailian-cli 的状态", () => {
|
||||
write(
|
||||
home,
|
||||
".claude/settings.json",
|
||||
JSON.stringify({ env: { ANTHROPIC_BASE_URL: "https://x", ANTHROPIC_MODEL: "qwen3-max" } }),
|
||||
JSON.stringify({
|
||||
env: { ANTHROPIC_BASE_URL: "https://x", ANTHROPIC_MODEL: "qwen3-max" },
|
||||
}),
|
||||
);
|
||||
// Codex: installed but NOT configured (no bailian-cli provider).
|
||||
write(home, ".codex/config.toml", 'model = "gpt-5"\n');
|
||||
// Qwen Code: configured via the new "[Bailian] <model>" display name.
|
||||
write(
|
||||
home,
|
||||
".qwen/settings.json",
|
||||
JSON.stringify({
|
||||
modelProviders: {
|
||||
openai: [{ id: "qwen3-coder-plus", name: "[Bailian] qwen3-coder-plus" }],
|
||||
},
|
||||
model: { name: "qwen3-coder-plus" },
|
||||
}),
|
||||
);
|
||||
// Hermes: configured via the official flat model block (no custom_providers).
|
||||
write(
|
||||
home,
|
||||
".hermes/config.yaml",
|
||||
[
|
||||
"model:",
|
||||
" default: qwen3-max",
|
||||
" provider: custom",
|
||||
" base_url: https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
" api_key: sk-test",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
const agents = listAgents(home);
|
||||
const byId = Object.fromEntries(agents.map((a) => [a.id, a]));
|
||||
@@ -153,8 +190,25 @@ test("listAgents 报告安装与已连接 bailian-cli 的状态", () => {
|
||||
model: "qwen3-max",
|
||||
origin: "local",
|
||||
});
|
||||
expect(byId.codex).toMatchObject({ installed: true, configured: false, model: "gpt-5" });
|
||||
expect(byId.opencode).toMatchObject({ installed: false, configured: false });
|
||||
expect(byId.codex).toMatchObject({
|
||||
installed: true,
|
||||
configured: false,
|
||||
model: "gpt-5",
|
||||
});
|
||||
expect(byId["qwen-code"]).toMatchObject({
|
||||
installed: true,
|
||||
configured: true,
|
||||
model: "qwen3-coder-plus",
|
||||
});
|
||||
expect(byId.hermes).toMatchObject({
|
||||
installed: true,
|
||||
configured: true,
|
||||
model: "qwen3-max",
|
||||
});
|
||||
expect(byId.opencode).toMatchObject({
|
||||
installed: false,
|
||||
configured: false,
|
||||
});
|
||||
// Always reports all six known frameworks.
|
||||
expect(agents).toHaveLength(6);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user