@dyyz1993/create-agent 1.9.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +23 -23
- package/src/lib/copy.ts +236 -243
- package/templates/agent/eslint-plugin-rpc/index.js +50 -0
- package/templates/agent/eslint-plugin-rpc/package.json +6 -0
- package/templates/agent/eslint-plugin-rpc/rules/module-file-naming.js +99 -0
- package/templates/agent/eslint-plugin-rpc/rules/no-bare-method.js +78 -0
- package/templates/agent/eslint-plugin-rpc/rules/no-direct-register.js +65 -0
- package/templates/agent/eslint-plugin-rpc/rules/no-hardcoded-strings.js +69 -0
- package/templates/agent/eslint-plugin-rpc/rules/require-api-client.js +58 -0
- package/templates/agent/eslint-plugin-rpc/rules/require-typed-register.js +78 -0
- package/templates/agent/eslint-plugin-rpc/rules/schema-merge-only.js +55 -0
- package/templates/agent/eslint.config.mjs +2 -1
- package/templates/agent/package.json +1 -2
- package/templates/agent/src/mainview/components/layout/AppLayout.tsx +3 -1
- package/templates/chat/eslint-plugin-rpc/index.js +50 -0
- package/templates/chat/eslint-plugin-rpc/package.json +6 -0
- package/templates/chat/eslint-plugin-rpc/rules/module-file-naming.js +99 -0
- package/templates/chat/eslint-plugin-rpc/rules/no-bare-method.js +78 -0
- package/templates/chat/eslint-plugin-rpc/rules/no-direct-register.js +65 -0
- package/templates/chat/eslint-plugin-rpc/rules/no-hardcoded-strings.js +69 -0
- package/templates/chat/eslint-plugin-rpc/rules/require-api-client.js +58 -0
- package/templates/chat/eslint-plugin-rpc/rules/require-typed-register.js +78 -0
- package/templates/chat/eslint-plugin-rpc/rules/schema-merge-only.js +55 -0
- package/templates/chat/eslint.config.mjs +2 -1
- package/templates/chat/package.json +1 -2
- package/templates/general/eslint-plugin-rpc/index.js +50 -0
- package/templates/general/eslint-plugin-rpc/package.json +6 -0
- package/templates/general/eslint-plugin-rpc/rules/module-file-naming.js +99 -0
- package/templates/general/eslint-plugin-rpc/rules/no-bare-method.js +78 -0
- package/templates/general/eslint-plugin-rpc/rules/no-direct-register.js +65 -0
- package/templates/general/eslint-plugin-rpc/rules/no-hardcoded-strings.js +69 -0
- package/templates/general/eslint-plugin-rpc/rules/require-api-client.js +58 -0
- package/templates/general/eslint-plugin-rpc/rules/require-typed-register.js +78 -0
- package/templates/general/eslint-plugin-rpc/rules/schema-merge-only.js +55 -0
- package/templates/general/eslint.config.mjs +2 -1
- package/templates/general/package.json +1 -2
- package/templates/agent/bun.lock +0 -1279
- package/templates/agent/package-lock.json +0 -3670
- package/templates/chat/bun.lock +0 -1203
- package/templates/chat/package-lock.json +0 -3670
- package/templates/general/bun.lock +0 -1266
- package/templates/general/package-lock.json +0 -3670
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview 强制 RPC 模块文件的命名和导出规范
|
|
3
|
+
*/
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
function getModuleName(filename) {
|
|
7
|
+
const match = filename.match(/\/modules\/([^/]+)\.[jt]sx?$/);
|
|
8
|
+
return match ? match[1] : null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
meta: {
|
|
13
|
+
type: "problem",
|
|
14
|
+
docs: {
|
|
15
|
+
description: "RPC 模块文件必须遵循命名和导出规范",
|
|
16
|
+
category: "RPC Conventions",
|
|
17
|
+
recommended: "error",
|
|
18
|
+
},
|
|
19
|
+
messages: {
|
|
20
|
+
missingMethodsExport:
|
|
21
|
+
'模块文件 "{{file}}" 必须导出 "{{ModuleName}}Methods" 接口。',
|
|
22
|
+
methodPrefixMismatch:
|
|
23
|
+
'方法 "{{method}}" 的前缀不匹配模块 "{{module}}"。必须以 "{{module}}." 开头。',
|
|
24
|
+
eventPrefixMismatch:
|
|
25
|
+
'事件 "{{event}}" 的前缀不匹配模块 "{{module}}"。必须以 "{{module}}." 开头。',
|
|
26
|
+
},
|
|
27
|
+
schema: [],
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
create(context) {
|
|
31
|
+
const filename = context.getFilename();
|
|
32
|
+
const moduleName = getModuleName(filename);
|
|
33
|
+
if (!moduleName) return {};
|
|
34
|
+
|
|
35
|
+
const moduleNamePascal = moduleName
|
|
36
|
+
.split(/[-_]/)
|
|
37
|
+
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
38
|
+
.join("");
|
|
39
|
+
|
|
40
|
+
let hasMethodsExport = false;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
TSInterfaceDeclaration(node) {
|
|
44
|
+
const name = node.id.name;
|
|
45
|
+
|
|
46
|
+
if (name === `${moduleNamePascal}Methods`) {
|
|
47
|
+
hasMethodsExport = true;
|
|
48
|
+
|
|
49
|
+
if (node.body && node.body.body) {
|
|
50
|
+
for (const member of node.body.body) {
|
|
51
|
+
if (member.type === "TSPropertySignature" && member.key) {
|
|
52
|
+
const key =
|
|
53
|
+
member.key.type === "Literal" ? String(member.key.value) : null;
|
|
54
|
+
if (key && !key.startsWith(`${moduleName}.`)) {
|
|
55
|
+
context.report({
|
|
56
|
+
node: member,
|
|
57
|
+
messageId: "methodPrefixMismatch",
|
|
58
|
+
data: { method: key, module: moduleName },
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (name === `${moduleNamePascal}Events`) {
|
|
67
|
+
if (node.body && node.body.body) {
|
|
68
|
+
for (const member of node.body.body) {
|
|
69
|
+
if (member.type === "TSPropertySignature" && member.key) {
|
|
70
|
+
const key =
|
|
71
|
+
member.key.type === "Literal" ? String(member.key.value) : null;
|
|
72
|
+
if (key && !key.startsWith(`${moduleName}.`)) {
|
|
73
|
+
context.report({
|
|
74
|
+
node: member,
|
|
75
|
+
messageId: "eventPrefixMismatch",
|
|
76
|
+
data: { event: key, module: moduleName },
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
"Program:exit"() {
|
|
86
|
+
if (!hasMethodsExport) {
|
|
87
|
+
context.report({
|
|
88
|
+
loc: { line: 1, column: 0 },
|
|
89
|
+
messageId: "missingMethodsExport",
|
|
90
|
+
data: {
|
|
91
|
+
file: filename.split("/").pop(),
|
|
92
|
+
ModuleName: moduleNamePascal,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview 禁止使用裸方法名(无模块前缀)
|
|
3
|
+
*/
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
const METHOD_CALL_PATTERNS = [
|
|
7
|
+
{ calleePattern: /^register$/, argIndex: 0 },
|
|
8
|
+
{ calleePattern: /\.call$/, argIndex: 0 },
|
|
9
|
+
{ calleePattern: /\.subscribe$/, argIndex: 0 },
|
|
10
|
+
{ calleePattern: /\.emitEvent$/, argIndex: 0 },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
function isModuleMethodName(name) {
|
|
14
|
+
const parts = name.split(".");
|
|
15
|
+
return parts.length === 2 && parts[0].length > 0 && parts[1].length > 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
meta: {
|
|
20
|
+
type: "problem",
|
|
21
|
+
docs: {
|
|
22
|
+
description: "RPC 方法名必须使用 module.action 格式(禁止裸方法名)",
|
|
23
|
+
category: "RPC Conventions",
|
|
24
|
+
recommended: "error",
|
|
25
|
+
},
|
|
26
|
+
messages: {
|
|
27
|
+
bareMethod:
|
|
28
|
+
'RPC 方法名 "{{name}}" 缺少模块前缀。必须使用 "module.action" 格式,如 "{{suggestion}}"。',
|
|
29
|
+
invalidFormat:
|
|
30
|
+
'RPC 方法名 "{{name}}" 格式错误。必须使用 "module.action" 格式(单一 "." 分隔)。',
|
|
31
|
+
},
|
|
32
|
+
schema: [],
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
create(context) {
|
|
36
|
+
const filename = context.getFilename();
|
|
37
|
+
|
|
38
|
+
if (!/\.[jt]sx?$/.test(filename)) return {};
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
CallExpression(node) {
|
|
42
|
+
const { callee, arguments: args } = node;
|
|
43
|
+
|
|
44
|
+
let calleeText = "";
|
|
45
|
+
if (callee.type === "Identifier") {
|
|
46
|
+
calleeText = callee.name;
|
|
47
|
+
} else if (callee.type === "MemberExpression" && callee.property.type === "Identifier") {
|
|
48
|
+
calleeText = `.${callee.property.name}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const pattern of METHOD_CALL_PATTERNS) {
|
|
52
|
+
if (!pattern.calleePattern.test(calleeText)) continue;
|
|
53
|
+
if (args.length <= pattern.argIndex) continue;
|
|
54
|
+
|
|
55
|
+
const arg = args[pattern.argIndex];
|
|
56
|
+
if (arg.type !== "Literal" || typeof arg.value !== "string") continue;
|
|
57
|
+
|
|
58
|
+
const methodName = arg.value;
|
|
59
|
+
|
|
60
|
+
if (!methodName.includes(".")) {
|
|
61
|
+
const suggestion = `module.${methodName}`;
|
|
62
|
+
context.report({
|
|
63
|
+
node: arg,
|
|
64
|
+
messageId: "bareMethod",
|
|
65
|
+
data: { name: methodName, suggestion },
|
|
66
|
+
});
|
|
67
|
+
} else if (!isModuleMethodName(methodName)) {
|
|
68
|
+
context.report({
|
|
69
|
+
node: arg,
|
|
70
|
+
messageId: "invalidFormat",
|
|
71
|
+
data: { name: methodName },
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview server.register() 只允许在 shared/handlers/ 目录内使用
|
|
3
|
+
*/
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
meta: {
|
|
8
|
+
type: "problem",
|
|
9
|
+
docs: {
|
|
10
|
+
description:
|
|
11
|
+
"server.register() 只允许在 shared/handlers/ 目录内使用,入口文件和其他文件禁止直接调用",
|
|
12
|
+
category: "RPC Conventions",
|
|
13
|
+
recommended: "error",
|
|
14
|
+
},
|
|
15
|
+
messages: {
|
|
16
|
+
noDirectRegisterInEntry:
|
|
17
|
+
"入口文件禁止直接调用 server.register()。请导入并使用 registerAllHandlers() 统一注册 handler。",
|
|
18
|
+
noDirectRegisterGeneral:
|
|
19
|
+
"禁止直接调用 server.register()。Handler 注册只能在 shared/handlers/ 目录内的文件中进行。",
|
|
20
|
+
},
|
|
21
|
+
schema: [],
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
create(context) {
|
|
25
|
+
const filename = context.getFilename();
|
|
26
|
+
|
|
27
|
+
if (!/src\/.*\.[jt]sx?$/.test(filename)) return {};
|
|
28
|
+
|
|
29
|
+
if (filename.includes("shared/handlers/")) return {};
|
|
30
|
+
|
|
31
|
+
const isEntryPoint =
|
|
32
|
+
filename.endsWith("bun/index.ts") || filename.endsWith("server.ts");
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
CallExpression(node) {
|
|
36
|
+
const { callee } = node;
|
|
37
|
+
|
|
38
|
+
if (
|
|
39
|
+
callee.type === "MemberExpression" &&
|
|
40
|
+
callee.property.type === "Identifier" &&
|
|
41
|
+
callee.property.name === "register" &&
|
|
42
|
+
callee.object.type === "Identifier"
|
|
43
|
+
) {
|
|
44
|
+
const objectName = callee.object.name;
|
|
45
|
+
|
|
46
|
+
const serverVarNames = [
|
|
47
|
+
"server",
|
|
48
|
+
"rpcServer",
|
|
49
|
+
"rpc",
|
|
50
|
+
"rpc_server",
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
if (serverVarNames.includes(objectName)) {
|
|
54
|
+
context.report({
|
|
55
|
+
node,
|
|
56
|
+
messageId: isEntryPoint
|
|
57
|
+
? "noDirectRegisterInEntry"
|
|
58
|
+
: "noDirectRegisterGeneral",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
meta: {
|
|
5
|
+
type: "suggestion",
|
|
6
|
+
docs: {
|
|
7
|
+
description: "disallow hardcoded strings in JSX (use i18n t() instead)",
|
|
8
|
+
category: "Best Practices",
|
|
9
|
+
recommended: false,
|
|
10
|
+
},
|
|
11
|
+
fixable: null,
|
|
12
|
+
schema: [
|
|
13
|
+
{
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
ignoreAttributes: {
|
|
17
|
+
type: "array",
|
|
18
|
+
items: { type: "string" },
|
|
19
|
+
},
|
|
20
|
+
ignoreComponents: {
|
|
21
|
+
type: "array",
|
|
22
|
+
items: { type: "string" },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
additionalProperties: false,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
messages: {
|
|
29
|
+
hardcodedString:
|
|
30
|
+
'Hardcoded string "{{text}}" should use i18n translation (t()).',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
create(context) {
|
|
35
|
+
const options = context.options[0] || {};
|
|
36
|
+
const ignoreAttributes = new Set(options.ignoreAttributes || ["className", "style", "id", "key", "ref", "type", "name", "data-testid", "viewBox", "d", "points", "x1", "x2", "y1", "y2", "strokeWidth", "strokeLinecap", "strokeLinejoin", "fill", "stroke", "xmlns"]);
|
|
37
|
+
new Set(options.ignoreComponents || []);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
JSXText(node) {
|
|
41
|
+
const text = node.value.trim();
|
|
42
|
+
if (text.length > 1 && /[a-zA-Z]{2,}/.test(text)) {
|
|
43
|
+
context.report({
|
|
44
|
+
node,
|
|
45
|
+
messageId: "hardcodedString",
|
|
46
|
+
data: { text },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
JSXAttribute(node) {
|
|
51
|
+
if (ignoreAttributes.has(node.name.name)) return;
|
|
52
|
+
if (
|
|
53
|
+
node.value &&
|
|
54
|
+
node.value.type === "Literal" &&
|
|
55
|
+
typeof node.value.value === "string"
|
|
56
|
+
) {
|
|
57
|
+
const text = node.value.value.trim();
|
|
58
|
+
if (text.length > 3 && /[a-zA-Z]{3,}/.test(text) && !text.startsWith("var(") && !text.startsWith("bg-") && !text.startsWith("text-") && !text.startsWith("border-")) {
|
|
59
|
+
context.report({
|
|
60
|
+
node,
|
|
61
|
+
messageId: "hardcodedString",
|
|
62
|
+
data: { text },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview 强制前端使用 apiClient 进行 RPC 调用
|
|
3
|
+
*/
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
meta: {
|
|
8
|
+
type: "problem",
|
|
9
|
+
docs: {
|
|
10
|
+
description: "前端 RPC 调用必须通过 apiClient",
|
|
11
|
+
category: "RPC Conventions",
|
|
12
|
+
recommended: "error",
|
|
13
|
+
},
|
|
14
|
+
messages: {
|
|
15
|
+
useApiClient:
|
|
16
|
+
"前端 RPC 调用必须通过 apiClient.call() / apiClient.subscribe()。禁止直接操作 WebSocket 或其他传输层。",
|
|
17
|
+
},
|
|
18
|
+
schema: [],
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
create(context) {
|
|
22
|
+
const filename = context.getFilename();
|
|
23
|
+
|
|
24
|
+
if (!/mainview\/.*\.[jt]sx?$/.test(filename)) return {};
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
NewExpression(node) {
|
|
28
|
+
if (
|
|
29
|
+
node.callee.type === "Identifier" &&
|
|
30
|
+
node.callee.name === "WebSocket"
|
|
31
|
+
) {
|
|
32
|
+
context.report({
|
|
33
|
+
node,
|
|
34
|
+
messageId: "useApiClient",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
CallExpression(node) {
|
|
40
|
+
if (
|
|
41
|
+
node.callee.type === "MemberExpression" &&
|
|
42
|
+
node.callee.property.type === "Identifier" &&
|
|
43
|
+
node.callee.property.name === "send"
|
|
44
|
+
) {
|
|
45
|
+
if (node.callee.object.type === "Identifier") {
|
|
46
|
+
const objName = node.callee.object.name.toLowerCase();
|
|
47
|
+
if (objName.includes("websocket") || objName.includes("ws")) {
|
|
48
|
+
context.report({
|
|
49
|
+
node,
|
|
50
|
+
messageId: "useApiClient",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview 入口文件必须导入 registerAllHandlers
|
|
3
|
+
*/
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
meta: {
|
|
8
|
+
type: "problem",
|
|
9
|
+
docs: {
|
|
10
|
+
description: "入口文件必须导入并调用 registerAllHandlers",
|
|
11
|
+
category: "RPC Conventions",
|
|
12
|
+
recommended: "error",
|
|
13
|
+
},
|
|
14
|
+
messages: {
|
|
15
|
+
missingRegisterAllHandlersImport:
|
|
16
|
+
'入口文件必须导入 registerAllHandlers。请添加:import { registerAllHandlers } from "./shared/handlers/register-all-handlers";',
|
|
17
|
+
missingRegisterAllHandlersCall:
|
|
18
|
+
"已导入 registerAllHandlers 但未调用。请在创建 server 后调用 registerAllHandlers(server)。",
|
|
19
|
+
},
|
|
20
|
+
schema: [],
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
create(context) {
|
|
24
|
+
const filename = context.getFilename();
|
|
25
|
+
|
|
26
|
+
const isEntryPoint =
|
|
27
|
+
filename.endsWith("bun/index.ts") || filename.endsWith("gateway/ws-handler.ts");
|
|
28
|
+
|
|
29
|
+
if (!isEntryPoint) return {};
|
|
30
|
+
|
|
31
|
+
let hasRegisterAllHandlersImport = false;
|
|
32
|
+
let hasRegisterAllHandlersCall = false;
|
|
33
|
+
let importNode = null;
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
ImportDeclaration(node) {
|
|
37
|
+
if (
|
|
38
|
+
node.source.type === "Literal" &&
|
|
39
|
+
typeof node.source.value === "string" &&
|
|
40
|
+
node.source.value.includes("register-all-handlers")
|
|
41
|
+
) {
|
|
42
|
+
for (const specifier of node.specifiers) {
|
|
43
|
+
if (
|
|
44
|
+
specifier.type === "ImportSpecifier" &&
|
|
45
|
+
specifier.imported.name === "registerAllHandlers"
|
|
46
|
+
) {
|
|
47
|
+
hasRegisterAllHandlersImport = true;
|
|
48
|
+
importNode = node;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
CallExpression(node) {
|
|
55
|
+
if (
|
|
56
|
+
node.callee.type === "Identifier" &&
|
|
57
|
+
node.callee.name === "registerAllHandlers"
|
|
58
|
+
) {
|
|
59
|
+
hasRegisterAllHandlersCall = true;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
"Program:exit"() {
|
|
64
|
+
if (!hasRegisterAllHandlersImport) {
|
|
65
|
+
context.report({
|
|
66
|
+
loc: { line: 1, column: 0 },
|
|
67
|
+
messageId: "missingRegisterAllHandlersImport",
|
|
68
|
+
});
|
|
69
|
+
} else if (!hasRegisterAllHandlersCall) {
|
|
70
|
+
context.report({
|
|
71
|
+
node: importNode,
|
|
72
|
+
messageId: "missingRegisterAllHandlersCall",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview rpc-schema.ts 只允许合并,禁止直接定义方法
|
|
3
|
+
*/
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
meta: {
|
|
8
|
+
type: "problem",
|
|
9
|
+
docs: {
|
|
10
|
+
description: "rpc-schema.ts 禁止直接定义方法,只允许 extends 合并",
|
|
11
|
+
category: "RPC Conventions",
|
|
12
|
+
recommended: "error",
|
|
13
|
+
},
|
|
14
|
+
messages: {
|
|
15
|
+
noDirectDefinition:
|
|
16
|
+
'rpc-schema.ts 中禁止直接定义方法 "{{key}}"。请在 src/shared/modules/ 下对应模块文件中定义,然后在此处 extends 合并。',
|
|
17
|
+
},
|
|
18
|
+
schema: [],
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
create(context) {
|
|
22
|
+
const filename = context.getFilename();
|
|
23
|
+
|
|
24
|
+
if (!filename.endsWith("rpc-schema.ts") && !filename.endsWith("rpc-schema.tsx")) return {};
|
|
25
|
+
|
|
26
|
+
const schemaInterfaces = new Set(["RPCMethods", "RPCEvents"]);
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
TSInterfaceDeclaration(node) {
|
|
30
|
+
if (!schemaInterfaces.has(node.id.name)) return;
|
|
31
|
+
|
|
32
|
+
if (node.body && node.body.body) {
|
|
33
|
+
for (const member of node.body.body) {
|
|
34
|
+
if (member.type === "TSPropertySignature" && member.key) {
|
|
35
|
+
const keyName =
|
|
36
|
+
member.key.type === "Identifier"
|
|
37
|
+
? member.key.name
|
|
38
|
+
: member.key.type === "Literal"
|
|
39
|
+
? String(member.key.value)
|
|
40
|
+
: null;
|
|
41
|
+
|
|
42
|
+
if (keyName) {
|
|
43
|
+
context.report({
|
|
44
|
+
node: member,
|
|
45
|
+
messageId: "noDirectDefinition",
|
|
46
|
+
data: { key: keyName },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import js from '@eslint/js';
|
|
2
2
|
import tseslint from 'typescript-eslint';
|
|
3
|
-
import rpcPlugin from '
|
|
3
|
+
import rpcPlugin from './eslint-plugin-rpc/index.js';
|
|
4
4
|
|
|
5
5
|
export default tseslint.config(
|
|
6
6
|
js.configs.recommended,
|
|
@@ -10,6 +10,7 @@ export default tseslint.config(
|
|
|
10
10
|
'node_modules/**',
|
|
11
11
|
'build/**',
|
|
12
12
|
'dist/**',
|
|
13
|
+
'eslint-plugin-rpc/**',
|
|
13
14
|
'postcss.config.js',
|
|
14
15
|
'tailwind.config.js',
|
|
15
16
|
],
|
|
@@ -33,8 +33,7 @@
|
|
|
33
33
|
"zustand": "^5.0.12"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
|
|
37
|
-
"@eslint/js": "^9.0.0",
|
|
36
|
+
"@eslint/js": "^9.0.0",
|
|
38
37
|
"@testing-library/jest-dom": "^6.9.1",
|
|
39
38
|
"@testing-library/react": "^16.3.2",
|
|
40
39
|
"@testing-library/user-event": "^14.6.1",
|
|
@@ -9,6 +9,7 @@ import { MobileTabBar } from "../activity-bar/MobileTabBar";
|
|
|
9
9
|
import { ExplorerSidebar } from "../explorer/ExplorerSidebar";
|
|
10
10
|
import { ChatPanel } from "../chat/ChatPanel";
|
|
11
11
|
import { ThemeToggle } from "../common/ThemeToggle";
|
|
12
|
+
import { LanguageSwitcher } from "../common/LanguageSwitcher";
|
|
12
13
|
|
|
13
14
|
const GitPanel = React.lazy(() => import("../git/GitPanel").then((m) => ({ default: m.GitPanel })));
|
|
14
15
|
const SearchPanel = React.lazy(() => import("../search/SearchPanel").then((m) => ({ default: m.SearchPanel })));
|
|
@@ -87,7 +88,8 @@ export function AppLayout({
|
|
|
87
88
|
{mode === "desktop" ? t("app.mode.desktop") : t("app.mode.web")}
|
|
88
89
|
</span>
|
|
89
90
|
<span className="ml-3 text-[var(--color-text-tertiary)]">{t("app.title")}</span>
|
|
90
|
-
<div className="ml-auto">
|
|
91
|
+
<div className="ml-auto flex items-center gap-1">
|
|
92
|
+
<LanguageSwitcher />
|
|
91
93
|
<ThemeToggle />
|
|
92
94
|
</div>
|
|
93
95
|
</div>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* eslint-plugin-rpc — RPC 模块化规范 ESLint 插件(单注册架构)
|
|
3
|
+
*
|
|
4
|
+
* 规则列表:
|
|
5
|
+
* - rpc/no-bare-method : 方法名必须使用 module.action 格式
|
|
6
|
+
* - rpc/no-direct-register : server.register() 只允许在 handlers/ 目录内使用
|
|
7
|
+
* - rpc/schema-merge-only : rpc-schema.ts 禁止直接定义方法
|
|
8
|
+
* - rpc/module-file-naming : 模块文件命名、导出、方法前缀强制规范
|
|
9
|
+
* - rpc/require-typed-register : 入口文件必须导入 registerAllHandlers
|
|
10
|
+
* - rpc/require-api-client : 前端必须通过 apiClient 调用 RPC
|
|
11
|
+
*/
|
|
12
|
+
"use strict";
|
|
13
|
+
|
|
14
|
+
const noBareMethod = require("./rules/no-bare-method");
|
|
15
|
+
const noDirectRegister = require("./rules/no-direct-register");
|
|
16
|
+
const schemaMergeOnly = require("./rules/schema-merge-only");
|
|
17
|
+
const moduleFileNaming = require("./rules/module-file-naming");
|
|
18
|
+
const requireTypedRegister = require("./rules/require-typed-register");
|
|
19
|
+
const requireApiClient = require("./rules/require-api-client");
|
|
20
|
+
const noHardcodedStrings = require("./rules/no-hardcoded-strings");
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
meta: {
|
|
24
|
+
name: "eslint-plugin-rpc",
|
|
25
|
+
version: "1.0.0",
|
|
26
|
+
},
|
|
27
|
+
rules: {
|
|
28
|
+
"no-bare-method": noBareMethod,
|
|
29
|
+
"no-direct-register": noDirectRegister,
|
|
30
|
+
"schema-merge-only": schemaMergeOnly,
|
|
31
|
+
"module-file-naming": moduleFileNaming,
|
|
32
|
+
"require-typed-register": requireTypedRegister,
|
|
33
|
+
"require-api-client": requireApiClient,
|
|
34
|
+
"no-hardcoded-strings": noHardcodedStrings,
|
|
35
|
+
},
|
|
36
|
+
configs: {
|
|
37
|
+
recommended: {
|
|
38
|
+
plugins: ["rpc"],
|
|
39
|
+
rules: {
|
|
40
|
+
"rpc/no-bare-method": "error",
|
|
41
|
+
"rpc/no-direct-register": "error",
|
|
42
|
+
"rpc/schema-merge-only": "error",
|
|
43
|
+
"rpc/module-file-naming": "error",
|
|
44
|
+
"rpc/require-typed-register": "error",
|
|
45
|
+
"rpc/require-api-client": "error",
|
|
46
|
+
"rpc/no-hardcoded-strings": "warn",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|