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
5 changes: 5 additions & 0 deletions app/model/skills_item.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ module.exports = (app) => {
allowNull: true,
comment: '所属技能包的 slug',
},
contributor: {
type: STRING(50),
allowNull: true,
comment: '贡献者',
},
created_at: {
type: DATE,
allowNull: false,
Expand Down
75 changes: 73 additions & 2 deletions app/service/skills.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const GIT_COMMAND_TIMEOUT_MS = 120 * 1000;
const GITHUB_API_TIMEOUT_MS = 10 * 1000;
const MAX_PLATFORM_TAGS = 5;
const MAX_TAG_LENGTH = 20;
const MAX_CONTRIBUTOR_LENGTH = 50;
const DISCOVER_DEPTH_LIMIT = 2;
const SKILLS_ROOT_DISCOVER_DEPTH_LIMIT = 8;
const DISCOVER_MAX_DIR_COUNT = 3000;
Expand Down Expand Up @@ -125,6 +126,7 @@ class SkillsService extends Service {
await SkillsFile.sync();
await this.ensureSkillsItemVersionColumn();
await this.ensureSkillsItemPackageColumns();
await this.ensureSkillsItemContributorColumn();
this.storageReady = true;
})();

Expand Down Expand Up @@ -168,6 +170,18 @@ class SkillsService extends Service {
}
}

async ensureSkillsItemContributorColumn() {
const queryInterface = this.app.model.getQueryInterface();
const table = await queryInterface.describeTable('skills_items');
if (table.contributor) return;

await queryInterface.addColumn('skills_items', 'contributor', {
type: this.app.Sequelize.STRING(50),
allowNull: true,
comment: '贡献者',
});
}

parseJsonArray(value) {
if (!value) return [];
if (Array.isArray(value)) return value;
Expand Down Expand Up @@ -198,6 +212,7 @@ class SkillsService extends Service {
installCommand: skill.installCommand,
isPackage: skill.isPackage ? 1 : 0,
parentSlug: skill.parentSlug || null,
contributor: skill.contributor || '',
};
}

Expand Down Expand Up @@ -227,6 +242,7 @@ class SkillsService extends Service {
fileCount: Number(row.file_count) || 0,
isPackage: Number(row.is_package) || 0,
parentSlug: row.parent_slug || null,
contributor: this.parseContributor(row.contributor),
};
}

Expand Down Expand Up @@ -296,7 +312,8 @@ class SkillsService extends Service {
item.name.toLowerCase().includes(value) ||
item.description.toLowerCase().includes(value) ||
item.sourceRepo.toLowerCase().includes(value) ||
item.tags.some((tag) => tag.toLowerCase().includes(value))
item.tags.some((tag) => tag.toLowerCase().includes(value)) ||
item.contributor.toLowerCase().includes(value)
);
}

Expand Down Expand Up @@ -729,6 +746,38 @@ class SkillsService extends Service {
.filter(Boolean);
}

parseContributor(value) {
if (Array.isArray(value)) {
return String(value[0] || '').trim();
}

return String(value || '').trim();
}

validateContributor(value) {
const contributor = this.parseContributor(value);
if (contributor.length > MAX_CONTRIBUTOR_LENGTH) {
this.ctx.throw(400, `贡献者不能超过 ${MAX_CONTRIBUTOR_LENGTH} 个字符`);
}
return contributor;
}

applyContributorToSkillRecords(records, contributor, shouldOverride = true) {
if (!shouldOverride) return records;

return records.map((record) => ({
...record,
contributor,
}));
}

validateSkillRecordContributors(records) {
return records.map((record) => ({
...record,
contributor: this.validateContributor(record.contributor),
}));
}

normalizePlatformTags(rawTags) {
const values = this.parseArrayLike(rawTags)
.map((item) => String(item || '').trim())
Expand Down Expand Up @@ -1406,6 +1455,12 @@ class SkillsService extends Service {
const allowedTools = this.parseArrayLike(
frontmatter['allowed-tools'] || frontmatter.allowedTools || frontmatter.allowed_tools
);
const contributor = this.parseContributor(
frontmatter.contributor ||
frontmatter.contributors ||
frontmatter.author ||
frontmatter.authors
);

const sourcePath = path.relative(repoDir, skillDir).split(path.sep).join('/');
const installCommand = this.getInstallCommand({
Expand All @@ -1423,6 +1478,7 @@ class SkillsService extends Service {
version,
tags,
allowedTools,
contributor,
updatedAt: stat.mtime,
sourceRepo: sourceMeta.sourceRepo,
sourcePath,
Expand Down Expand Up @@ -1633,6 +1689,8 @@ class SkillsService extends Service {
const packageName = String(params.packageName || '').trim();
const category = this.normalizeCategory(params.category);
const tags = this.normalizePlatformTags(params.tags);
const hasContributor = Object.prototype.hasOwnProperty.call(params, 'contributor');
const contributor = hasContributor ? this.validateContributor(params.contributor) : '';
const fileName = String((file && file.filename) || '').trim();
const filePath = String((file && file.filepath) || '').trim();

Expand Down Expand Up @@ -1675,7 +1733,7 @@ class SkillsService extends Service {
const identityKey = packageName || skillName;
const parsedSource = this.buildUploadSourceMeta(fileName, identityKey);

const skillRecords = discoveredSkillDirs.map((skillDir) => {
let skillRecords = discoveredSkillDirs.map((skillDir) => {
const record = this.prepareSkillRecord(
skillDir,
tempDir,
Expand All @@ -1688,6 +1746,11 @@ class SkillsService extends Service {
}
return record;
});
skillRecords = this.applyContributorToSkillRecords(
skillRecords,
contributor,
hasContributor
);

const tempUsedSlugs = new Set();
const excludeSlugs = skillRecords.map((record) =>
Expand Down Expand Up @@ -1825,6 +1888,8 @@ class SkillsService extends Service {

await this.ensureSkillCache();
const currentSkill = this.getSkillByIdentifier(slug);
const hasContributor = Object.prototype.hasOwnProperty.call(params, 'contributor');
const contributor = hasContributor ? this.validateContributor(params.contributor) : '';
await this.ensureStorageReady();

if (!name) {
Expand Down Expand Up @@ -1867,6 +1932,9 @@ class SkillsService extends Service {
version,
tags: JSON.stringify(tags || []),
};
if (hasContributor) {
payload.contributor = contributor || null;
}

if (!hasZipUpload) {
await itemRow.update(payload, { transaction });
Expand Down Expand Up @@ -2005,6 +2073,7 @@ class SkillsService extends Service {
skillRecords = [],
preferredPackageName = ''
) {
skillRecords = this.validateSkillRecordContributors(skillRecords);
const { SkillsItem, SkillsFile } = this.app.model;
const { Op } = this.app.Sequelize;
const repoStars = await this.fetchStarsBySourceRepo(sourceMeta.sourceRepo);
Expand Down Expand Up @@ -2062,6 +2131,7 @@ class SkillsService extends Service {
allowed_tools: JSON.stringify(
Array.from(new Set(skillRecords.flatMap((r) => r.allowedTools || [])))
),
contributor: skillRecords[0].contributor || null,
stars: resolvedStars,
updated_at_remote: new Date(),
source_repo: sourceMeta.sourceRepo || '',
Expand Down Expand Up @@ -2112,6 +2182,7 @@ class SkillsService extends Service {
version: record.version || '',
tags: JSON.stringify(record.tags || []),
allowed_tools: JSON.stringify(record.allowedTools || []),
contributor: record.contributor || null,
stars: resolvedStars,
updated_at_remote: record.updatedAt,
source_repo: record.sourceRepo,
Expand Down
9 changes: 3 additions & 6 deletions app/web/components/skills/SkillCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,9 @@ export const SkillCard: React.FC<SkillCardProps> = ({
{showMeta && (
<div className="card-meta">
<span className="meta-item">
<span className="meta-label">来源</span>
<span
className="meta-value"
title={skill.sourceRepo || skill.sourcePath || '-'}
>
{skill.sourceRepo || skill.sourcePath || '-'}
<span className="meta-label">贡献者</span>
<span className="meta-value" title={skill.contributor || '-'}>
{skill.contributor || '-'}
</span>
</span>
<span className="meta-separator">·</span>
Expand Down
1 change: 1 addition & 0 deletions app/web/pages/skills/detail/SkillDetailContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ const SkillDetailContent: React.FC<SkillDetailContentProps> = ({
downloadCommand={downloadCommand}
agentTerminalCommand={agentTerminalCommand}
manualDownloadUrl={manualDownloadUrl}
contributor={detail?.contributor || ''}
/>
) : null}
</div>
Expand Down
5 changes: 2 additions & 3 deletions app/web/pages/skills/detail/SkillSummaryModalContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,6 @@ const SkillSummaryModalContent: React.FC<SkillSummaryModalContentProps> = ({ slu
}

const detailTags = (detail.tags || []).filter((tag) => tag && tag !== detail.category);
const detailSource = detail.sourceRepo || detail.sourcePath || '-';
const detailUpdatedAt = detail.updatedAt
? new Date(detail.updatedAt).toLocaleString('zh-CN')
: '-';
Expand All @@ -207,8 +206,8 @@ const SkillSummaryModalContent: React.FC<SkillSummaryModalContentProps> = ({ slu
value: detailUpdatedAt,
},
{
label: '来源',
value: detailSource,
label: '贡献者',
value: detail.contributor || '-',
className: 'is-wide',
},
];
Expand Down
12 changes: 5 additions & 7 deletions app/web/pages/skills/detail/components/SkillInstallPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import { Button } from 'antd';
import agentIcon from '@/asset/images/skills-detail-figma/agent.svg';
import chevronDownIcon from '@/asset/images/skills-detail-figma/chevron-down.svg';
import chevronRightIcon from '@/asset/images/skills-detail-figma/chevron-right.svg';
import contributorOne from '@/asset/images/skills-detail-figma/contributor-1.png';
import contributorTwo from '@/asset/images/skills-detail-figma/contributor-2.png';
import copyDarkIcon from '@/asset/images/skills-detail-figma/copy-dark.svg';
import downloadIcon from '@/asset/images/skills-detail-figma/download.svg';
import emptyRelatedIcon from '@/asset/images/skills-detail-figma/empty-related.svg';
Expand Down Expand Up @@ -39,6 +37,7 @@ interface SkillInstallPanelProps {
downloadCommand: string;
agentTerminalCommand: string;
manualDownloadUrl: string;
contributor?: string;
}

const renderInlineCommand = (command: string, copyMessage: string, compact = false) => (
Expand Down Expand Up @@ -94,6 +93,7 @@ export const SkillInstallPanel: React.FC<SkillInstallPanelProps> = ({
downloadCommand,
agentTerminalCommand,
manualDownloadUrl,
contributor = '',
}) => (
<aside className="detail-right-sidebar">
<section className="install-panel">
Expand Down Expand Up @@ -286,11 +286,9 @@ export const SkillInstallPanel: React.FC<SkillInstallPanelProps> = ({
</div>
<div className="meta-row is-contributors">
<span>贡献者</span>
<div className="contributors-stack">
<img alt="contributor 1" src={contributorOne} />
<img alt="contributor 2" src={contributorTwo} />
<span>+3</span>
</div>
<span className="contributor-name" title={contributor || '-'}>
{contributor || '-'}
</span>
</div>
</section>
</aside>
Expand Down
29 changes: 6 additions & 23 deletions app/web/pages/skills/detail/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1059,29 +1059,12 @@
font-weight: 600;
}
}
.contributors-stack {
display: inline-flex;
align-items: center;
img,
span {
width: 16px;
height: 16px;
border-radius: 999px;
border: 1px solid #FFF;
margin-left: -4px;
}
img:first-child,
span:first-child {
margin-left: 0;
}
span {
background: #E2E8F0;
color: #566166;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 8px;
}
.contributor-name {
max-width: 120px;
overflow: hidden;
color: #475569;
text-overflow: ellipsis;
white-space: nowrap;
}

@media (max-width: 1200px) {
Expand Down
20 changes: 19 additions & 1 deletion app/web/pages/skills/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ const SkillsMarket: React.FC<any> = ({ history }) => {
category: skill.category || '通用',
tags: skill.tags || [],
version: skill.version || '',
contributor: skill.contributor || '',
});
};

Expand All @@ -191,11 +192,13 @@ const SkillsMarket: React.FC<any> = ({ history }) => {
return;
}
const values = importForm.getFieldsValue();
const contributor = String(values.contributor || '').trim();
const response = await API.importSkillFile({
file: targetFile,
skillName: values.skillName || '',
category: values.category,
tags: JSON.stringify(values.tags || []),
...(contributor ? { contributor } : {}),
});

if (!response.success) {
Expand Down Expand Up @@ -236,6 +239,7 @@ const SkillsMarket: React.FC<any> = ({ history }) => {
name: values.name,
category: values.category,
tags: JSON.stringify(values.tags || []),
contributor: values.contributor || '',
version: values.version || '',
file: targetFile,
});
Expand Down Expand Up @@ -293,7 +297,7 @@ const SkillsMarket: React.FC<any> = ({ history }) => {
allowClear
value={query.keyword}
className="keyword-search"
placeholder="搜索名称、描述、标签或来源..."
placeholder="搜索名称、描述、标签或贡献者..."
enterButton={<SearchOutlined />}
onChange={(e) => setQuery({ ...query, keyword: e.target.value })}
onSearch={(value) => updateQueryAndFetch({ keyword: value, pageNum: 1 })}
Expand Down Expand Up @@ -458,6 +462,13 @@ const SkillsMarket: React.FC<any> = ({ history }) => {
maxTagCount={5}
/>
</Form.Item>
<Form.Item
name="contributor"
label="贡献者(可选)"
rules={[{ max: 50, message: '贡献者不能超过 50 个字符' }]}
>
<Input placeholder="请输入贡献者名称" maxLength={50} />
</Form.Item>
</Form>
<Text type="secondary">提示:.zip 包内部应包含 `技能目录/SKILL.md`</Text>
</Modal>
Expand Down Expand Up @@ -542,6 +553,13 @@ const SkillsMarket: React.FC<any> = ({ history }) => {
<Form.Item name="version" label="版本号">
<Input placeholder="例如:V2.4.0-STABLE" maxLength={128} />
</Form.Item>
<Form.Item
name="contributor"
label="贡献者(可选)"
rules={[{ max: 50, message: '贡献者不能超过 50 个字符' }]}
>
<Input placeholder="请输入贡献者名称" maxLength={50} />
</Form.Item>
<Form.Item
label="重新上传 .zip(可选)"
extra="上传后会替换当前技能文件内容;要求 zip 中只包含一个技能目录"
Expand Down
1 change: 1 addition & 0 deletions app/web/pages/skills/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface SkillItem {
sourcePath: string;
isPackage?: number;
parentSlug?: string | null;
contributor: string;
}

export interface SkillListResponse {
Expand Down
Loading
Loading