Skip to content

Commit 44e3435

Browse files
committed
feat(agents): simplify install script and prompts
1 parent b196f66 commit 44e3435

9 files changed

Lines changed: 190 additions & 35 deletions

File tree

‎app/controller/agents.js‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,18 @@ class AgentsController extends Controller {
8080
});
8181
this.ctx.body = this.app.utils.response(true, data);
8282
}
83+
84+
async getInstallScript() {
85+
const script = await this.ctx.service.agents.getInstallScript();
86+
this.ctx.set('Content-Type', 'text/plain; charset=utf-8');
87+
this.ctx.body = script;
88+
}
89+
90+
async getCreatePluginScript() {
91+
const script = await this.ctx.service.agents.getCreatePluginScript();
92+
this.ctx.set('Content-Type', 'text/plain; charset=utf-8');
93+
this.ctx.body = script;
94+
}
8395
}
8496

8597
module.exports = AgentsController;

‎app/public/install.sh‎

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@ set -euo pipefail
1717
# AGENT_MARKET_NAME marketplace 名,默认: agent-market
1818

1919
AGENT_NAME="${1:-}"
20-
AGENT_MARKET_BASE_URL="${AGENT_MARKET_BASE_URL:-http://172.16.100.225:7001/agent-market}"
20+
CUSTOM_BASE_URL="${2:-}"
21+
DEFAULT_MARKET_URL="__AGENT_MARKET_BASE_URL__"
22+
if [[ "$DEFAULT_MARKET_URL" == *"__"* ]]; then
23+
DEFAULT_MARKET_URL="http://172.16.100.225:7001/agent-market"
24+
fi
25+
AGENT_MARKET_BASE_URL="${CUSTOM_BASE_URL:-${AGENT_MARKET_BASE_URL:-$DEFAULT_MARKET_URL}}"
2126
AGENT_MARKET_BASE_URL="${AGENT_MARKET_BASE_URL%/}"
2227
AGENT_MARKET_LOCAL_DIR="${AGENT_MARKET_LOCAL_DIR:-$HOME/.agents/agent-market}"
2328
AGENT_MARKET_NAME="${AGENT_MARKET_NAME:-agent-market}"
@@ -106,14 +111,17 @@ except:
106111
107112
for folder in [".claude-plugin", ".codex-plugin"]:
108113
mf_path = f"{market_dir}/{folder}/marketplace.json"
109-
market = {"name": "agent-market", "plugins": []}
114+
market = {"name": "agent-market", "owner": {"name": "Doraemon"}, "plugins": []}
110115
if os.path.exists(mf_path):
111116
try:
112117
with open(mf_path, "r") as f:
113118
market = json.load(f)
114119
except:
115120
pass
116121
122+
if "owner" not in market:
123+
market["owner"] = {"name": "Doraemon"}
124+
117125
# Remove existing entry if any
118126
market["plugins"] = [p for p in market.get("plugins", []) if p.get("name") != agent_name]
119127
@@ -144,38 +152,37 @@ try:
144152
data = json.load(open(sys.argv[1], encoding="utf-8"))
145153
except Exception:
146154
sys.exit(0)
147-
for prompt in data.get("interface", {}).get("defaultPrompt", []):
148-
match = re.search(r'\$([a-z0-9][a-z0-9-]*)', str(prompt))
149-
if match:
150-
print(match.group(1))
151-
break
155+
156+
prompts = data.get("interface", {}).get("defaultPrompt", [])
157+
if isinstance(prompts, list):
158+
for p in prompts:
159+
m = re.search(r'\$([A-Za-z0-9_-]+)', p)
160+
if m:
161+
print(m.group(1))
162+
sys.exit(0)
152163
PY
153164
}
154165

155166
CODEX="$(resolve_codex || true)"
156167
CLAUDE="$(resolve_claude || true)"
157168

158-
if [[ -z "$CODEX" && -z "$CLAUDE" ]]; then
159-
log ""
160-
warn "未检测到 codex / claude CLI,无法自动安装;marketplace 源码已就绪于 $AGENT_MARKET_LOCAL_DIR"
161-
exit 1
162-
fi
163-
164169
# 注册 marketplace + 安装 plugin(读命令输出判断是否已注册/已安装,幂等可重复执行)。
165170
install_codex() {
166171
local cli="$1"
167172
log ""
168173
log "Codex 安装 $AGENT_NAME@$AGENT_MARKET_NAME ..."
169-
# 不用 grep -Fq:命中即关闭读端,CLI 输出超管道缓冲时被 SIGPIPE 杀死,pipefail 下会误判未注册。
170-
if "$cli" plugin marketplace list --json 2>/dev/null | grep -F "\"name\": \"$AGENT_MARKET_NAME\"" >/dev/null; then
174+
175+
# 注册 local marketplace(只在未注册时 add,避免重复报错)
176+
if "$cli" plugin marketplace list 2>/dev/null | grep -Fq "$AGENT_MARKET_NAME"; then
171177
ok "marketplace $AGENT_MARKET_NAME 已注册,跳过"
172178
else
173179
"$cli" plugin marketplace add "$AGENT_MARKET_LOCAL_DIR" \
174-
|| die "codex marketplace add 失败"
180+
|| die "codex plugin marketplace add 失败"
175181
ok "marketplace $AGENT_MARKET_NAME 已注册"
176182
fi
177-
# 不用 grep -Fq:命中即关闭读端,CLI 输出超管道缓冲时被 SIGPIPE 杀死,pipefail 下会误判未安装。
178-
if "$cli" plugin list --json 2>/dev/null | grep -F "\"pluginId\": \"$AGENT_NAME@$AGENT_MARKET_NAME\"" >/dev/null; then
183+
184+
local installed="$HOME/.codex/plugins/cache/$AGENT_MARKET_NAME/$AGENT_NAME"
185+
if [[ -d "$installed" ]]; then
179186
ok "plugin $AGENT_NAME 已安装,跳过"
180187
else
181188
"$cli" plugin add "$AGENT_NAME@$AGENT_MARKET_NAME" \
@@ -186,15 +193,16 @@ install_codex() {
186193

187194
install_claude() {
188195
local cli="$1"
189-
local known="$HOME/.claude/plugins/known_marketplaces.json"
190-
local installed="$HOME/.claude/plugins/installed_plugins.json"
191196
log ""
192197
log "Claude Code 安装 $AGENT_NAME@$AGENT_MARKET_NAME ..."
193-
if [[ -f "$known" ]] && grep -Fq "\"$AGENT_MARKET_NAME\"" "$known"; then
194-
ok "marketplace $AGENT_MARKET_NAME 已注册,跳过"
198+
199+
local installed="$HOME/.claude/plugins/installed_plugins.json"
200+
if "$cli" plugin marketplace list 2>/dev/null | grep -Fq "$AGENT_MARKET_NAME"; then
201+
ok "marketplace $AGENT_MARKET_NAME 已注册,更新索引"
202+
"$cli" plugin marketplace update "$AGENT_MARKET_NAME" >/dev/null 2>&1 || true
195203
else
196204
"$cli" plugin marketplace add "$AGENT_MARKET_LOCAL_DIR" \
197-
|| die "claude marketplace add 失败"
205+
|| die "claude plugin marketplace add 失败"
198206
ok "marketplace $AGENT_MARKET_NAME 已注册"
199207
fi
200208
if [[ -f "$installed" ]] && grep -Fq "\"$AGENT_NAME\"" "$installed"; then

‎app/router.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ module.exports = (app) => {
174174
app.post('/api/agents/sync-git', app.controller.agents.syncGitAgents);
175175
app.post('/api/agents/update-git-config', app.controller.agents.updateAgentGitConfig);
176176
app.post('/api/agents/delete', app.controller.agents.deleteAgent);
177+
app.get('/agent-market/install.sh', app.controller.agents.getInstallScript);
178+
app.get('/agent-market/create-plugin.sh', app.controller.agents.getCreatePluginScript);
177179

178180
/**
179181
* Skills Registry API (v1)

‎app/service/agents.js‎

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -257,11 +257,11 @@ class AgentsService extends Service {
257257
const category = this.mapCodexCategory(iface.category);
258258
const longDescription = String(iface.longDescription || description || '').trim();
259259
const defaultPrompt = Array.isArray(iface.defaultPrompt) ? iface.defaultPrompt : [];
260-
if (defaultPrompt.length > 3) {
261-
this.ctx.throw(400, 'interface.defaultPrompt 最多支持 3 条');
260+
if (defaultPrompt.length > 5) {
261+
this.ctx.throw(400, 'interface.defaultPrompt 最多支持 5 条');
262262
}
263-
if (defaultPrompt.some((item) => typeof item !== 'string' || item.length > 128)) {
264-
this.ctx.throw(400, 'interface.defaultPrompt 每条必须是 128 字符以内的字符串');
263+
if (defaultPrompt.some((item) => typeof item !== 'string' || item.length > 1024)) {
264+
this.ctx.throw(400, 'interface.defaultPrompt 每条必须是 1024 字符以内的字符串');
265265
}
266266

267267
const skills = this.normalizeManifestPath(manifest.skills, 'skills');
@@ -1463,6 +1463,23 @@ class AgentsService extends Service {
14631463
synced: false,
14641464
};
14651465
}
1466+
1467+
async getInstallScript() {
1468+
const filePath = path.join(this.app.baseDir, 'app/public/install.sh');
1469+
let content = await fs.promises.readFile(filePath, 'utf-8');
1470+
const host =
1471+
(this.ctx.get && this.ctx.get('x-forwarded-host')) || this.ctx.host || 'localhost:7001';
1472+
const protocol =
1473+
(this.ctx.get && this.ctx.get('x-forwarded-proto')) || this.ctx.protocol || 'http';
1474+
const currentBaseUrl = `${protocol}://${host}/agent-market`;
1475+
content = content.replace(/__AGENT_MARKET_BASE_URL__/g, currentBaseUrl);
1476+
return content;
1477+
}
1478+
1479+
async getCreatePluginScript() {
1480+
const filePath = path.join(this.app.baseDir, 'app/public/create-plugin.sh');
1481+
return await fs.promises.readFile(filePath, 'utf-8');
1482+
}
14661483
}
14671484

14681485
module.exports = AgentsService;

‎config/config.default.js‎

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,6 @@ module.exports = (app) => {
3131
prefix: '/resources/',
3232
dir: path.join(app.baseDir, 'resources'),
3333
},
34-
{
35-
prefix: '/agent-market/',
36-
dir: path.join(app.baseDir, 'app/public'),
37-
maxAge: 0, // maxAge 缓存,默认 1 年
38-
buffer: false, // 不读进内存,改文件立即生效
39-
dynamic: true, // 实时读取文件,支持热更新
40-
},
4134
],
4235
};
4336
exports.keys = '123456';

‎docs/docsify/zh-cn/guide/agent-market.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ bugfix-agent
338338
| `interface.longDescription` | string | 否 | 详情页“Agent 简介”展示的完整长描述,支持多段落 |
339339
| `interface.category` | string | 否 | 分类标识(如 `Coding` 映射为 `工程效率`) |
340340
| `interface.capabilities` | string[] / object[] | 否 | 能力项列表,在 Hero 区域作为功能项逗号分隔展示 |
341-
| `interface.defaultPrompt` | string[] | 否 | 开场问题列表,最多 3 条,每条 ≤ 128 字符,提供“快捷使用”入口 |
341+
| `interface.defaultPrompt` | string[] | 否 | 开场问题列表,最多 5 条,每条 ≤ 1024 字符(支持多行参数与代码块),提供“快捷使用”入口 |
342342
| `interface.logo` | string | 否 | Logo 相对路径,如 `./assets/logo.png`;缺省时自动探测 `assets/` 下图片 |
343343

344344
### 2. `.claude-plugin/plugin.json`

‎test/agent-market-controller.test.js‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,33 @@ test('downloadAgentArchive 返回 ZIP 文件流和下载响应头', async () =>
8080
assert.equal(headers['Content-Disposition'], 'attachment; filename="bugfix-agent.zip"');
8181
assert.equal(controller.ctx.body, stream);
8282
});
83+
84+
test('getInstallScript 设置 text/plain 响应头并返回脚本内容', async () => {
85+
const headers = {};
86+
const controller = buildController({
87+
getInstallScript: async () => '#!/usr/bin/env bash\necho install',
88+
});
89+
controller.ctx.set = (key, value) => {
90+
headers[key] = value;
91+
};
92+
93+
await controller.getInstallScript();
94+
95+
assert.equal(headers['Content-Type'], 'text/plain; charset=utf-8');
96+
assert.equal(controller.ctx.body, '#!/usr/bin/env bash\necho install');
97+
});
98+
99+
test('getCreatePluginScript 设置 text/plain 响应头并返回脚本内容', async () => {
100+
const headers = {};
101+
const controller = buildController({
102+
getCreatePluginScript: async () => '#!/usr/bin/env bash\necho create-plugin',
103+
});
104+
controller.ctx.set = (key, value) => {
105+
headers[key] = value;
106+
};
107+
108+
await controller.getCreatePluginScript();
109+
110+
assert.equal(headers['Content-Type'], 'text/plain; charset=utf-8');
111+
assert.equal(controller.ctx.body, '#!/usr/bin/env bash\necho create-plugin');
112+
});

‎test/agent-market-service.test.js‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,3 +473,39 @@ test('getGitAuthArgs 域名白名单与空 host 防护', () => {
473473
const auth4 = service.getGitAuthArgs('');
474474
assert.deepEqual(auth4, []);
475475
});
476+
477+
test('getInstallScript 动态替换 __AGENT_MARKET_BASE_URL__ 为当前请求来源', async () => {
478+
const service = createService();
479+
service.app.baseDir = path.resolve(__dirname, '..');
480+
service.ctx.host = 'localhost:7001';
481+
service.ctx.protocol = 'http';
482+
service.ctx.get = (header) => {
483+
if (header === 'x-forwarded-host') return 'doraemon.internal:8080';
484+
if (header === 'x-forwarded-proto') return 'https';
485+
return '';
486+
};
487+
488+
const script = await service.getInstallScript();
489+
assert.ok(script.includes('https://doraemon.internal:8080/agent-market'));
490+
assert.ok(!script.includes('__AGENT_MARKET_BASE_URL__'));
491+
});
492+
493+
test('getInstallScript 无代理头时使用 ctx.host 和 ctx.protocol', async () => {
494+
const service = createService();
495+
service.app.baseDir = path.resolve(__dirname, '..');
496+
service.ctx.host = '127.0.0.1:7001';
497+
service.ctx.protocol = 'http';
498+
service.ctx.get = () => '';
499+
500+
const script = await service.getInstallScript();
501+
assert.ok(script.includes('http://127.0.0.1:7001/agent-market'));
502+
assert.ok(!script.includes('__AGENT_MARKET_BASE_URL__'));
503+
});
504+
505+
test('getCreatePluginScript 读取并返回 create-plugin.sh 内容', async () => {
506+
const service = createService();
507+
service.app.baseDir = path.resolve(__dirname, '..');
508+
509+
const script = await service.getCreatePluginScript();
510+
assert.ok(script.includes('create-plugin.sh'));
511+
});

‎test/agent-plugin-contract.test.js‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,63 @@ test('validateCodexManifest 返回规范化的展示与配置字段', () => {
4747
assert.equal(validated.category, '工程效率');
4848
});
4949

50+
test('validateCodexManifest 支持最多 1024 字符的多行代码块 defaultPrompt 并拦截超长配置', () => {
51+
const service = createService();
52+
const baseManifest = {
53+
name: 'batch-task-agent',
54+
version: '1.0.0',
55+
description: '任务类型扩展 Agent',
56+
author: { name: 'DTStack' },
57+
skills: './skills/',
58+
interface: {
59+
displayName: 'Batch Task Agent',
60+
category: 'Coding',
61+
defaultPrompt: [],
62+
},
63+
};
64+
65+
// 1. 包含 >128 字符的多行 scope 代码块(用户实际场景)能够正常校验通过
66+
const longPrompt =
67+
'$batch-task-type-agent\n```scope\ntype: FOO_SQL\nid: 56\nreference: DORIS\ndisplayName: Foo SQL\nassociateFunction: true\nfunctionTypeId: 28\nscriptType: true\nscriptTypeId: 25\n```';
68+
const validManifest = {
69+
...baseManifest,
70+
interface: { ...baseManifest.interface, defaultPrompt: [longPrompt] },
71+
};
72+
const res = service.validateCodexManifest(validManifest);
73+
assert.deepEqual(res.defaultPrompt, [longPrompt]);
74+
75+
// 2. 超过 1024 字符的 prompt 应该被拦截
76+
const overLengthPrompt = 'a'.repeat(1025);
77+
const invalidManifest = {
78+
...baseManifest,
79+
interface: { ...baseManifest.interface, defaultPrompt: [overLengthPrompt] },
80+
};
81+
assert.throws(
82+
() => service.validateCodexManifest(invalidManifest),
83+
/interface\.defaultPrompt 每条必须是 1024 字符以内的字符串/
84+
);
85+
86+
// 3. 支持配置最多 5 条 prompt
87+
const fivePrompts = ['p1', 'p2', 'p3', 'p4', 'p5'];
88+
const fiveManifest = {
89+
...baseManifest,
90+
interface: { ...baseManifest.interface, defaultPrompt: fivePrompts },
91+
};
92+
const fiveRes = service.validateCodexManifest(fiveManifest);
93+
assert.deepEqual(fiveRes.defaultPrompt, fivePrompts);
94+
95+
// 4. 超过 5 条应该被拦截
96+
const sixPrompts = ['p1', 'p2', 'p3', 'p4', 'p5', 'p6'];
97+
const sixManifest = {
98+
...baseManifest,
99+
interface: { ...baseManifest.interface, defaultPrompt: sixPrompts },
100+
};
101+
assert.throws(
102+
() => service.validateCodexManifest(sixManifest),
103+
/interface\.defaultPrompt 最多支持 5 条/
104+
);
105+
});
106+
50107
test('validateClaudeManifest 校验 agents 配置有效性', () => {
51108
const service = createService();
52109
assert.throws(

0 commit comments

Comments
 (0)