Skip to content
This repository was archived by the owner on Aug 11, 2026. It is now read-only.
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
22 changes: 22 additions & 0 deletions packages/plugin-protocol/src/__tests__/manifest.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest';
import {
GHOST_MANIFEST_SUMMARY_MAX_CHARS,
GHOST_MANIFEST_SCHEMA_VERSION,
GHOST_OAUTH_SCOPES_MAX,
compareCindyVersions,
Expand Down Expand Up @@ -64,6 +65,27 @@ describe('Ghost manifest contract', () => {
expect(validateGhostManifest({ ...validManifest, schemaVersion: 1 }).ok).toBe(false);
});

it('shares the 300-character limit across description and whenToUse', () => {
expect(GHOST_MANIFEST_SUMMARY_MAX_CHARS).toBe(300);
for (const field of ['description', 'whenToUse'] as const) {
expect(
validateGhostManifest({
...validManifest,
[field]: 'x'.repeat(GHOST_MANIFEST_SUMMARY_MAX_CHARS),
}).ok,
).toBe(true);
expect(
validateGhostManifest({
...validManifest,
[field]: 'x'.repeat(GHOST_MANIFEST_SUMMARY_MAX_CHARS + 1),
}),
).toEqual({
ok: false,
reason: `${field} 必须是 1–${GHOST_MANIFEST_SUMMARY_MAX_CHARS} 字符的非空字符串`,
});
}
});

it('accepts an oidc-token secret without a settings page and normalizes its exact host', () => {
const result = validateGhostManifest({
...validManifest,
Expand Down
17 changes: 13 additions & 4 deletions packages/plugin-protocol/src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export const CINDY_FILE_EXT = '.cindy';
/** ghost.json 格式版本;与 Plugin HTTP API envelope 版本独立演进。 */
export const GHOST_MANIFEST_SCHEMA_VERSION = 2 as const;

/** ghost.json 的 description / whenToUse 字符上限。 */
export const GHOST_MANIFEST_SUMMARY_MAX_CHARS = 300;

/** Cindy host locales supported by Plugin manifest resources. */
export const GHOST_LOCALES = ['zh-CN', 'en', 'ja', 'ko'] as const;
export type GhostLocale = (typeof GHOST_LOCALES)[number];
Expand Down Expand Up @@ -1152,17 +1155,23 @@ export function validateGhostManifest(raw: unknown): ManifestValidation {
raw.description !== undefined &&
(typeof raw.description !== 'string' ||
raw.description.trim().length === 0 ||
raw.description.length > 300)
raw.description.length > GHOST_MANIFEST_SUMMARY_MAX_CHARS)
) {
return { ok: false, reason: 'description 必须是 1–300 字符的非空字符串' };
return {
ok: false,
reason: `description 必须是 1–${GHOST_MANIFEST_SUMMARY_MAX_CHARS} 字符的非空字符串`,
};
}
if (
raw.whenToUse !== undefined &&
(typeof raw.whenToUse !== 'string' ||
raw.whenToUse.trim().length === 0 ||
raw.whenToUse.length > 300)
raw.whenToUse.length > GHOST_MANIFEST_SUMMARY_MAX_CHARS)
) {
return { ok: false, reason: 'whenToUse 必须是 1–300 字符的非空字符串' };
return {
ok: false,
reason: `whenToUse 必须是 1–${GHOST_MANIFEST_SUMMARY_MAX_CHARS} 字符的非空字符串`,
};
}
if (raw.icon !== undefined) {
if (!isSafeGhostRelativePath(raw.icon)) {
Expand Down
Loading