Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ cache
.history
public
resources
agent-market
app/view

# Research docs (working notes, not for commit)
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.

**项目**: dt-doraemon (哆啦A梦) — 开发者工具箱平台,包含代理服务、主机管理、配置中心、MCP 服务器注册中心、Skills 市场等模块。
**项目**: dt-doraemon (哆啦A梦) — 开发者工具箱平台,包含代理服务、主机管理、配置中心、MCP 服务器注册中心、Skill 市场、Agent 市场等模块。

**技术栈**: Egg.js 2.x + React 16 SSR + MySQL (Sequelize) + Webpack 4 + Redux + Socket.IO。

Expand Down
73 changes: 73 additions & 0 deletions app/controller/agents.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
const Controller = require('egg').Controller;
const fs = require('fs');

class AgentsController extends Controller {
async getAgentList() {
const data = await this.ctx.service.agents.queryAgentList(this.ctx.query);
this.ctx.body = this.app.utils.response(true, data);
}

async getAgentDetail() {
const data = await this.ctx.service.agents.getAgentDetail(this.ctx.query.name);
this.ctx.body = this.app.utils.response(true, data);
}

async getRelatedAgents() {
const { name, limit = 3 } = this.ctx.query;
const data = await this.ctx.service.agents.getRelatedAgents(name, limit);
this.ctx.body = this.app.utils.response(true, data);
}

async getAgentAsset() {
const { stream, mimeType, cacheControl } =
await this.ctx.service.agents.getAgentAssetStream(this.ctx.query);
this.ctx.set('Content-Type', mimeType);
this.ctx.set('Cache-Control', cacheControl);
this.ctx.body = stream;
}

async downloadAgentArchive() {
const { stream, fileName, mimeType } = await this.ctx.service.agents.getAgentArchiveStream(
this.ctx.query.name
);
this.ctx.set('Content-Type', mimeType);
this.ctx.set('Content-Disposition', `attachment; filename="${fileName}"`);
this.ctx.body = stream;
}

async importAgentFile() {
const files = this.ctx.request.files
? Array.isArray(this.ctx.request.files)
? this.ctx.request.files
: [this.ctx.request.files]
: [];
const file = files[0];

if (!file) {
this.ctx.throw(400, '缺少上传文件');
}

try {
const data = await this.ctx.service.agents.importAgentFile(
this.ctx.request.body || {},
file
);
this.ctx.body = this.app.utils.response(true, data);
} finally {
if (file?.filepath && fs.existsSync(file.filepath)) {
try {
fs.unlinkSync(file.filepath);
} catch (error) {
this.ctx.logger.warn(`[agents] 清理上传文件失败: ${error.message}`);
}
}
}
}

async deleteAgent() {
const data = await this.ctx.service.agents.deleteAgent(this.ctx.request.body || {});
this.ctx.body = this.app.utils.response(true, data);
}
}

module.exports = AgentsController;
3 changes: 2 additions & 1 deletion app/controller/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ class CommonController extends Controller {
}
async getLocalIp() {
const { app, ctx } = this;
const localIp = ctx.header['x-real-ip'] || ctx.ip;
const rawIp = ctx.header['x-real-ip'] || ctx.ip;
const localIp = rawIp === '::1' ? '127.0.0.1' : rawIp;
ctx.body = app.utils.response(true, {
localIp,
host: ctx.host,
Expand Down
144 changes: 144 additions & 0 deletions app/model/agent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
module.exports = (app) => {
const { INTEGER, STRING, TEXT, DATE, TINYINT } = app.Sequelize;

const Agent = app.model.define(
'agent',
{
id: {
type: INTEGER,
primaryKey: true,
autoIncrement: true,
},
name: {
type: STRING(100),
allowNull: false,
unique: true,
comment: 'Agent 唯一标识',
},
display_name: {
type: STRING(255),
allowNull: false,
comment: 'Agent 展示名称',
},
version: {
type: STRING(64),
allowNull: false,
defaultValue: '',
comment: 'Agent 版本号',
},
description: {
type: TEXT,
comment: '列表摘要',
},
profile: {
type: TEXT('long'),
comment: 'Agent 详细简介',
},
author_name: {
type: STRING(255),
comment: '作者',
},
category: {
type: STRING(64),
allowNull: false,
defaultValue: '通用',
comment: '分类',
},
tags: {
type: TEXT('long'),
comment: 'JSON 字符串数组',
},
prompts: {
type: TEXT('long'),
comment: 'JSON 字符串数组',
},
capabilities: {
type: TEXT('long'),
comment: 'JSON 字符串数组',
},
demo_images: {
type: TEXT('long'),
comment: 'JSON 字符串数组',
},
entrypoint_host: {
type: STRING(64),
comment: '入口宿主',
},
entrypoint_type: {
type: STRING(64),
comment: '入口类型',
},
entrypoint_name: {
type: STRING(255),
comment: '入口名称',
},
entrypoint_ref: {
type: STRING(1000),
comment: '入口路径',
},
logo_path: {
type: STRING(1000),
comment: 'Logo 相对路径',
},
logo_mime_type: {
type: STRING(100),
comment: 'Logo MIME',
},
logo_size: {
type: INTEGER,
allowNull: false,
defaultValue: 0,
comment: 'Logo 大小',
},
logo_hash: {
type: STRING(128),
comment: 'Logo 哈希',
},
content_hash: {
type: STRING(128),
allowNull: false,
comment: '内容哈希',
},
source_file_name: {
type: STRING(255),
comment: '上传文件名',
},
file_count: {
type: INTEGER,
allowNull: false,
defaultValue: 0,
comment: '文件数量',
},
is_delete: {
type: TINYINT,
allowNull: false,
defaultValue: 0,
comment: '是否删除',
},
created_at: {
type: DATE,
allowNull: false,
defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'),
},
updated_at: {
type: DATE,
allowNull: false,
defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'),
},
},
{
freezeTableName: true,
tableName: 'agents',
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
indexes: [
{ unique: true, fields: ['name'] },
{ fields: ['category'] },
{ fields: ['updated_at'] },
],
}
);

return Agent;
};
84 changes: 84 additions & 0 deletions app/model/agent_file.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
module.exports = (app) => {
const { INTEGER, STRING, TEXT, DATE, TINYINT } = app.Sequelize;

const AgentFile = app.model.define(
'agent_file',
{
id: {
type: INTEGER,
primaryKey: true,
autoIncrement: true,
},
agent_id: {
type: INTEGER,
allowNull: false,
comment: 'agents.id',
},
file_path: {
type: STRING(512),
allowNull: false,
comment: 'Agent 内相对路径',
},
mime_type: {
type: STRING(100),
comment: '文件 MIME',
},
size: {
type: INTEGER,
allowNull: false,
defaultValue: 0,
comment: '文件大小',
},
is_binary: {
type: TINYINT,
allowNull: false,
defaultValue: 0,
comment: '是否二进制',
},
encoding: {
type: STRING(20),
allowNull: false,
defaultValue: 'utf8',
comment: '内容编码',
},
mode: {
type: INTEGER,
allowNull: false,
defaultValue: 0,
comment: 'Unix 权限',
},
content: {
type: TEXT('long'),
comment: '文件内容',
},
is_delete: {
type: TINYINT,
allowNull: false,
defaultValue: 0,
},
created_at: {
type: DATE,
allowNull: false,
defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'),
},
updated_at: {
type: DATE,
allowNull: false,
defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'),
},
},
{
freezeTableName: true,
tableName: 'agent_files',
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
indexes: [
{ unique: true, fields: ['agent_id', 'file_path'] },
{ fields: ['agent_id'] },
],
}
);

return AgentFile;
};
64 changes: 64 additions & 0 deletions app/model/agent_skill.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
module.exports = (app) => {
const { INTEGER, STRING, DATE } = app.Sequelize;

const AgentSkill = app.model.define(
'agent_skill',
{
id: {
type: INTEGER,
primaryKey: true,
autoIncrement: true,
},
agent_id: {
type: INTEGER,
allowNull: false,
comment: 'agents.id',
},
skill_slug: {
type: STRING(255),
allowNull: false,
comment: 'Skill slug',
},
skill_id: {
type: INTEGER,
allowNull: true,
comment: 'skills_items.id',
},
relation_type: {
type: STRING(20),
allowNull: false,
comment: 'entrypoint 或 dependency',
},
sort_order: {
type: INTEGER,
allowNull: false,
defaultValue: 0,
comment: '展示顺序',
},
created_at: {
type: DATE,
allowNull: false,
defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'),
},
updated_at: {
type: DATE,
allowNull: false,
defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'),
},
},
{
freezeTableName: true,
tableName: 'agent_skills',
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
indexes: [
{ fields: ['agent_id'] },
{ fields: ['skill_slug'] },
{ fields: ['relation_type'] },
],
}
);

return AgentSkill;
};
Loading
Loading