feat(portal-shell): v2.0 P0 shadcn standardization + security + streaming + error handling
- shadcn/ui 标准化:废弃纸感令牌,统一 bg-background/text-foreground 等 - Tailwind v4 + @theme inline,移除 tailwind.config.js - React 19 use() + Suspense 流式渲染,首屏骨架秒出 - 三级错误边界:Route → Section → Widget 层层兜底 - 错误上报:useErrorReport → sendBeacon → /api/log mock 端点 - 三层安全边界:L1 角色门禁 / L2 权限点门禁 / L3 数据范围 - 权限位图 base36 压缩:67 权限点 → ~14 字符,JWT 体积减少 ≥ 99% - notify 统一 Toast 封装,禁止业务直接 import sonner - PluginBoundary 替代 PluginLoader(错误边界 + Suspense + Skeleton 三件套) 验证:typecheck 0 错误 / lint 0 错误 / build 6 路由生成成功
This commit is contained in:
@@ -113,6 +113,48 @@ function normalizeSchema(content: string): {
|
||||
return { staticDefs: staticDefs.join("\n"), queryFields };
|
||||
}
|
||||
|
||||
// Sanitize invalid input field types.
|
||||
//
|
||||
// Problem: services/ai subgraph declares `input ChatRequestInput { messages:
|
||||
// ChatMessage }` and `input ChatResponseInput { usage: Usage }` where
|
||||
// ChatMessage/Usage are OUTPUT types. GraphQL spec forbids input fields
|
||||
// referencing output types; graphql-codegen's typescript plugin rejects this.
|
||||
//
|
||||
// Solution: rewrite those offending input field types to `String` in the
|
||||
// combined schema. This is a codegen-only sanitize; the runtime apollo-router
|
||||
// uses the original subgraph schemas directly.
|
||||
//
|
||||
// Related: spec section 2.4
|
||||
const SANITIZE_INPUT_FIELD_REPLACEMENTS: Array<{
|
||||
inputName: string;
|
||||
fieldName: string;
|
||||
replacement: string;
|
||||
}> = [
|
||||
// services/ai: input ChatRequestInput { messages: ChatMessage }
|
||||
{
|
||||
inputName: "ChatRequestInput",
|
||||
fieldName: "messages",
|
||||
replacement: "String",
|
||||
},
|
||||
// services/ai: input ChatResponseInput { usage: Usage }
|
||||
{ inputName: "ChatResponseInput", fieldName: "usage", replacement: "String" },
|
||||
];
|
||||
|
||||
function sanitizeInputFields(content: string): string {
|
||||
let out = content;
|
||||
for (const r of SANITIZE_INPUT_FIELD_REPLACEMENTS) {
|
||||
// Match ` fieldName: OriginalType` lines within `input InputName { ... }`
|
||||
// blocks. We rely on the simple field-line format generated above.
|
||||
const inputBlockRe = new RegExp(
|
||||
`(input\\s+${r.inputName}\\s*\\{[^}]*?)` +
|
||||
`(\\s{2,}${r.fieldName}\\s*:\\s*)[A-Za-z_][A-Za-z0-9_\\[\\]!]*`,
|
||||
"g",
|
||||
);
|
||||
out = out.replace(inputBlockRe, `$1$2${r.replacement}`);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
const schemas = loadSchemas();
|
||||
const allStaticDefs: string[] = [];
|
||||
@@ -130,7 +172,7 @@ function main(): void {
|
||||
new Set(allQueryFields.map((f) => f.trim())),
|
||||
);
|
||||
|
||||
const combined = [
|
||||
let combined = [
|
||||
"# Combined normalized schema for graphql-codegen (federation stripped)",
|
||||
"# DO NOT EDIT - generated by scripts/normalize-schema.ts",
|
||||
"",
|
||||
@@ -142,6 +184,8 @@ function main(): void {
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
combined = sanitizeInputFields(combined);
|
||||
|
||||
const outDir = path.resolve(process.cwd(), "src/lib/api/__generated__");
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const outPath = path.join(outDir, "combined-schema.graphql");
|
||||
|
||||
Reference in New Issue
Block a user