kcode-pi 0.1.6 → 0.1.8
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/README.md +40 -5
- package/docs/DEVELOPMENT.md +2 -0
- package/extensions/kingdee-harness.ts +139 -1
- package/extensions/kingdee-tools.ts +43 -1
- package/package.json +2 -1
- package/skills/kd-check/SKILL.md +1 -2
- package/skills/kd-cosmic-dev/SKILL.md +3 -3
- package/skills/kd-cosmic-review/SKILL.md +2 -2
- package/skills/kd-cosmic-unittest/SKILL.md +2 -2
- package/skills/kd-debug/SKILL.md +1 -2
- package/skills/kd-execute/SKILL.md +2 -2
- package/skills/kd-gen/SKILL.md +2 -1
- package/skills/kd-plan/SKILL.md +3 -3
- package/src/harness/gates.ts +14 -1
- package/src/harness/state.ts +44 -1
- package/src/harness/types.ts +14 -0
- package/src/official/kingdee-skills.ts +60 -13
- package/src/rules/checker.ts +143 -0
- package/src/tools/sdk-signature.ts +309 -0
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/SKILL.md +2 -2
- package/vendor/kingdee-skills/ok-cosmic/SKILL.md +52 -101
- package/vendor/kingdee-skills/ok-cosmic/agents/openai.yaml +4 -4
- package/vendor/kingdee-skills/ok-cosmic/manifest.json +21 -20
- package/vendor/kingdee-skills/ok-cosmic/ok-cosmic-intro.html +1 -1
- package/vendor/kingdee-skills/ok-cosmic/rules/a-layer-rules.json +1 -1
- package/vendor/kingdee-skills/ok-cosmic/rules/anti-patterns.md +2 -2
- package/vendor/kingdee-skills/ok-cosmic/rules/coding-preferences.md +4 -4
- package/vendor/kingdee-skills/ok-cosmic/rules/constraints.md +3 -3
- package/vendor/kingdee-skills/ok-cosmic/rules/decision-matrix.md +8 -8
- package/vendor/kingdee-skills/ok-cosmic/rules/intent-routing.md +1 -1
- package/vendor/kingdee-skills/ok-cosmic/rules/post-check.md +19 -18
- package/vendor/kingdee-skills/ok-ksql/SKILL.md +9 -9
- package/vendor/kingdee-skills/ok-ksql/manifest.json +2 -1
- package/vendor/kingdee-skills/ok-ksql/references/ksql-datafix.md +2 -2
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/scripts/pattern-matcher.py +0 -336
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/scripts/review-score-calculator.py +0 -121
- package/vendor/kingdee-skills/ok-cosmic/CHANGELOG.md +0 -295
- package/vendor/kingdee-skills/ok-cosmic/README.md +0 -460
- package/vendor/kingdee-skills/ok-cosmic/requirements.txt +0 -2
- package/vendor/kingdee-skills/ok-cosmic/scripts/config_loader.py +0 -204
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-api-knowledge.py +0 -910
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-basedata-query.py +0 -359
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-config-check.py +0 -181
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-extpoints-query.py +0 -389
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-form-metadata.py +0 -856
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-post-check.py +0 -262
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-post-lint.py +0 -293
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/__init__.py +0 -2
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/base.py +0 -393
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/resource_check.py +0 -176
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/scene_check.py +0 -375
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/style_check.py +0 -434
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/verify_check.py +0 -36
- package/vendor/kingdee-skills/ok-cosmic/scripts/route_client.py +0 -186
- package/vendor/kingdee-skills/ok-cosmic/scripts/script_utils.py +0 -40
- package/vendor/kingdee-skills/ok-cosmic/scripts/sqlite_cache.py +0 -142
- package/vendor/kingdee-skills/ok-cosmic/setup/cuslib/kd-cd-cosmic-commons.jar +0 -0
- package/vendor/kingdee-skills/ok-cosmic/setup/cuslib/kd-cd-cosmic-features.jar +0 -0
- package/vendor/kingdee-skills/ok-cosmic/setup/setup-mac.sh +0 -18
- package/vendor/kingdee-skills/ok-cosmic/setup/setup-windows.bat +0 -53
- package/vendor/kingdee-skills/ok-cosmic/setup/setup.jar +0 -0
- package/vendor/kingdee-skills/ok-ksql/scripts/ksql_lint.py +0 -363
package/src/harness/types.ts
CHANGED
|
@@ -9,6 +9,19 @@ export interface GateResult {
|
|
|
9
9
|
checkedAt: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export interface KdQuestion {
|
|
13
|
+
id: string;
|
|
14
|
+
phase: KdPhase;
|
|
15
|
+
question: string;
|
|
16
|
+
reason?: string;
|
|
17
|
+
choices?: string[];
|
|
18
|
+
blocking: boolean;
|
|
19
|
+
status: "open" | "answered";
|
|
20
|
+
answer?: string;
|
|
21
|
+
createdAt: string;
|
|
22
|
+
answeredAt?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
12
25
|
export interface ActiveRun {
|
|
13
26
|
id: string;
|
|
14
27
|
phase: KdPhase;
|
|
@@ -19,6 +32,7 @@ export interface ActiveRun {
|
|
|
19
32
|
risk?: KdRisk;
|
|
20
33
|
artifacts: Record<string, string>;
|
|
21
34
|
gate: GateResult;
|
|
35
|
+
questions?: KdQuestion[];
|
|
22
36
|
}
|
|
23
37
|
|
|
24
38
|
export const PHASE_ORDER: KdPhase[] = ["discuss", "spec", "plan", "execute", "verify", "ship"];
|
|
@@ -109,8 +109,8 @@ export async function runOfficialCommand(command: OfficialCommand): Promise<Comm
|
|
|
109
109
|
export async function cosmicConfigCommand(cwd: string, config?: string): Promise<OfficialCommand> {
|
|
110
110
|
await ensureOfficialSkillRoot(cwd, "ok-cosmic");
|
|
111
111
|
const configPath = resolveConfigPath(cwd, config);
|
|
112
|
-
const display = formatCommand("kcode-node:cosmic-config", config ? ["--config", configPath] : []);
|
|
113
|
-
return officialCommand(display, () => runCosmicConfig(cwd, configPath));
|
|
112
|
+
const display = formatCommand("kcode-node:cosmic-config", config && configPath ? ["--config", configPath] : []);
|
|
113
|
+
return officialCommand(display, () => runCosmicConfig(cwd, configPath, Boolean(config)));
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
export async function cosmicMetadataCommand(
|
|
@@ -133,7 +133,7 @@ export async function cosmicMetadataCommand(
|
|
|
133
133
|
if (params.fuzzy) args.push("--fuzzy", ...splitTerms(params.fuzzy));
|
|
134
134
|
if (params.typeFilter) args.push("--type", params.typeFilter);
|
|
135
135
|
const configPath = resolveConfigPath(cwd, params.config);
|
|
136
|
-
return officialCommand(formatCommand("kcode-node:cosmic-metadata", args), () => runCosmicMetadata(cwd, configPath, params));
|
|
136
|
+
return officialCommand(formatCommand("kcode-node:cosmic-metadata", args), () => runCosmicMetadata(cwd, configPath, Boolean(params.config), params));
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
export async function cosmicApiCommand(
|
|
@@ -219,8 +219,10 @@ function officialCommand(display: string, run: () => Promise<Omit<CommandResult,
|
|
|
219
219
|
};
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
function resolveConfigPath(cwd: string, config?: string): string {
|
|
223
|
-
|
|
222
|
+
function resolveConfigPath(cwd: string, config?: string): string | undefined {
|
|
223
|
+
if (config) return resolveWorkspacePath(cwd, config);
|
|
224
|
+
const projectConfig = resolve(cwd, "ok-cosmic.json");
|
|
225
|
+
return existsSync(projectConfig) ? projectConfig : undefined;
|
|
224
226
|
}
|
|
225
227
|
|
|
226
228
|
function readJsonObject(path: string): Record<string, unknown> {
|
|
@@ -231,20 +233,34 @@ function readJsonObject(path: string): Record<string, unknown> {
|
|
|
231
233
|
return data as Record<string, unknown>;
|
|
232
234
|
}
|
|
233
235
|
|
|
234
|
-
|
|
236
|
+
interface LoadedCosmicConfig {
|
|
237
|
+
config: Record<string, unknown>;
|
|
238
|
+
path?: string;
|
|
239
|
+
source: "project" | "explicit" | "bundled";
|
|
240
|
+
baseDir: string;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function runCosmicConfig(cwd: string, configPath: string | undefined, explicitConfig: boolean): Promise<Omit<CommandResult, "command">> {
|
|
235
244
|
const issues: Array<{ level: "OK" | "WARNING" | "ERROR"; key: string; message: string }> = [];
|
|
236
245
|
issues.push({ level: "OK", key: "runtime.node", message: `Node ${process.version}` });
|
|
237
246
|
|
|
238
|
-
let
|
|
247
|
+
let loaded: LoadedCosmicConfig | undefined;
|
|
239
248
|
try {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
249
|
+
loaded = loadCosmicConfig(cwd, configPath, explicitConfig);
|
|
250
|
+
if (loaded.path) {
|
|
251
|
+
issues.push({ level: "OK", key: "__file__", message: `已找到配置文件: ${loaded.path}` });
|
|
252
|
+
} else {
|
|
253
|
+
issues.push({
|
|
254
|
+
level: "WARNING",
|
|
255
|
+
key: "__file__",
|
|
256
|
+
message: "当前项目未提供 ok-cosmic.json,已使用 KCode 随包默认配置。项目根目录 cosmic.json 是苍穹工程配置,不是 KCode 官方能力配置。",
|
|
257
|
+
});
|
|
258
|
+
}
|
|
243
259
|
} catch (error) {
|
|
244
260
|
issues.push({ level: "ERROR", key: "__file__", message: error instanceof Error ? error.message : String(error) });
|
|
245
261
|
}
|
|
246
262
|
|
|
247
|
-
if (
|
|
263
|
+
if (loaded) issues.push(...validateCosmicConfig(loaded.config, loaded.baseDir));
|
|
248
264
|
const errors = issues.filter((issue) => issue.level === "ERROR").length;
|
|
249
265
|
const warnings = issues.filter((issue) => issue.level === "WARNING").length;
|
|
250
266
|
const lines = issues.map((issue) => `[${issue.level}] ${issue.key}: ${issue.message}`);
|
|
@@ -252,6 +268,36 @@ async function runCosmicConfig(_cwd: string, configPath: string): Promise<Omit<C
|
|
|
252
268
|
return { exitCode: errors ? 1 : 0, stdout: `${lines.join("\n")}\n`, stderr: "" };
|
|
253
269
|
}
|
|
254
270
|
|
|
271
|
+
function loadCosmicConfig(cwd: string, configPath: string | undefined, explicitConfig: boolean): LoadedCosmicConfig {
|
|
272
|
+
if (configPath) {
|
|
273
|
+
if (!existsSync(configPath)) throw new Error(`找不到配置文件: ${configPath}`);
|
|
274
|
+
return {
|
|
275
|
+
config: readJsonObject(configPath),
|
|
276
|
+
path: configPath,
|
|
277
|
+
source: explicitConfig ? "explicit" : "project",
|
|
278
|
+
baseDir: dirname(configPath),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
if (explicitConfig) throw new Error("指定的 ok-cosmic.json 配置文件不存在。");
|
|
282
|
+
return {
|
|
283
|
+
config: defaultCosmicConfig(),
|
|
284
|
+
source: "bundled",
|
|
285
|
+
baseDir: cwd,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function defaultCosmicConfig(): Record<string, unknown> {
|
|
290
|
+
return {
|
|
291
|
+
graph: {
|
|
292
|
+
dbPath: join(packageRoot, "vendor", "kingdee-skills", "ok-cosmic", "setup", "ok-cosmic-docs.db"),
|
|
293
|
+
},
|
|
294
|
+
route: {
|
|
295
|
+
apiUrl: process.env.COSMIC_ROUTE_API || process.env.COSMIC_RUNTIME_ROUTE_API || "",
|
|
296
|
+
timeoutSeconds: Number(process.env.COSMIC_ROUTE_TIMEOUT || 10),
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
255
301
|
function validateCosmicConfig(config: Record<string, unknown>, baseDir: string): Array<{ level: "OK" | "WARNING" | "ERROR"; key: string; message: string }> {
|
|
256
302
|
const issues: Array<{ level: "OK" | "WARNING" | "ERROR"; key: string; message: string }> = [];
|
|
257
303
|
const graph = objectValue(config.graph);
|
|
@@ -308,10 +354,11 @@ function validateCosmicConfig(config: Record<string, unknown>, baseDir: string):
|
|
|
308
354
|
|
|
309
355
|
async function runCosmicMetadata(
|
|
310
356
|
cwd: string,
|
|
311
|
-
configPath: string,
|
|
357
|
+
configPath: string | undefined,
|
|
358
|
+
explicitConfig: boolean,
|
|
312
359
|
params: { form: string; fuzzy?: string; typeFilter?: string; sql?: boolean; op?: boolean; showDetail?: boolean },
|
|
313
360
|
): Promise<Omit<CommandResult, "command">> {
|
|
314
|
-
const config =
|
|
361
|
+
const config = loadCosmicConfig(cwd, configPath, explicitConfig).config;
|
|
315
362
|
const cache = readMetadataCache(cwd);
|
|
316
363
|
const targets = params.form.split(/[,,]/).map((item) => item.trim()).filter(Boolean);
|
|
317
364
|
if (targets.length === 0) return { exitCode: 1, stdout: "", stderr: "必须提供 formId 或中文单据名。" };
|
package/src/rules/checker.ts
CHANGED
|
@@ -65,6 +65,7 @@ function checkCosmicReviewerRules(lines: string[]): CheckResult[] {
|
|
|
65
65
|
...checkSecurityPatterns(lines),
|
|
66
66
|
...checkThreadingPatterns(lines),
|
|
67
67
|
...checkUiPerformance(lines),
|
|
68
|
+
...checkViewInteractionOrder(lines),
|
|
68
69
|
...checkLoggingAndDiagnostics(lines),
|
|
69
70
|
];
|
|
70
71
|
}
|
|
@@ -134,6 +135,17 @@ function checkResourceHandling(lines: string[]): CheckResult[] {
|
|
|
134
135
|
results.push(makeResult(i, line, "DataSet", "resource", "error", "P0:DataSet 创建后未明显使用 try-with-resources 关闭,可能导致连接泄漏", "p0-dataset-not-closed"));
|
|
135
136
|
}
|
|
136
137
|
|
|
138
|
+
for (let i = 0; i < lines.length; i++) {
|
|
139
|
+
const line = lines[i];
|
|
140
|
+
if (isCommentLine(line)) continue;
|
|
141
|
+
if (/\.\s*getDynamicObject\s*\([^)]*\)\s*\.\s*get(?:String|Long|Int|Integer|Double|Date|PkValue|DynamicObject|DynamicObjectCollection)\s*\(/.test(line)) {
|
|
142
|
+
results.push(makeResult(i, line, "getDynamicObject", "resource", "warning", "P1:嵌套 DynamicObject 直接取值缺少空值保护,引用字段为空时可能 NPE;先取对象并判空或用安全取值工具", "p1-dynamicobject-chain-null-risk"));
|
|
143
|
+
}
|
|
144
|
+
if (/row\s*\.\s*get(?:BigDecimal|String|Long|Int|Integer|Date)\s*\(/.test(line) && !nearbyLineHas(lines, i, -2, /\brow\b\s*(?:!=|==)\s*null|Optional\.ofNullable\s*\(\s*row\s*\)|Objects\.nonNull\s*\(\s*row\s*\)/)) {
|
|
145
|
+
results.push(makeResult(i, line, "row", "resource", "warning", "P1:DataSet.Row 取值可能返回 null,参与运算或解包前应判空或给默认值", "p1-dataset-row-null-risk"));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
137
149
|
return results;
|
|
138
150
|
}
|
|
139
151
|
|
|
@@ -159,6 +171,20 @@ function checkSecurityPatterns(lines: string[]): CheckResult[] {
|
|
|
159
171
|
if (/(password|passwd|secret|token|ak|sk)\s*=\s*"[^"]{4,}"/i.test(line)) {
|
|
160
172
|
results.push(makeResult(i, line, "=", "security", "error", "P0:疑似敏感信息硬编码,请改为安全配置或密文存储", "p0-secret-hardcode"));
|
|
161
173
|
}
|
|
174
|
+
|
|
175
|
+
if (/\bcache\s*\.\s*put\s*\(/i.test(line) || /\bCacheFactory\b/.test(line)) {
|
|
176
|
+
const statement = collectStatement(lines, i, 5);
|
|
177
|
+
if (/\bput\s*\(/i.test(statement) && !/(account|acct|tenant|org|dcId|getAccountId|getOrgId|RequestContext|UserServiceHelper)/i.test(statement)) {
|
|
178
|
+
results.push(makeResult(i, line, "put", "security", "warning", "P1:缓存写入 key 未体现账套/租户/组织隔离,跨账套环境可能串数据", "p1-cache-key-without-tenant"));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (/\b(openConnection|HttpClient\s*\.|HttpClientBuilder|OkHttpClient|RestTemplate|WebClient)\b/.test(line)) {
|
|
183
|
+
const statement = collectStatement(lines, i, 8);
|
|
184
|
+
if (!/(timeout|setConnectTimeout|setReadTimeout|connectTimeout|readTimeout|callTimeout|responseTimeout)/i.test(statement)) {
|
|
185
|
+
results.push(makeResult(i, line, line.match(/\b(openConnection|HttpClient|HttpClientBuilder|OkHttpClient|RestTemplate|WebClient)\b/)?.[1] ?? "HTTP", "security", "warning", "P1:第三方 HTTP 调用未看到超时设置,生产环境可能阻塞业务线程", "p1-http-without-timeout"));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
162
188
|
}
|
|
163
189
|
|
|
164
190
|
return results;
|
|
@@ -197,6 +223,22 @@ function checkUiPerformance(lines: string[]): CheckResult[] {
|
|
|
197
223
|
if (isInsideLoop(lines, i) && /getFieldIndex\s*\(/.test(line)) {
|
|
198
224
|
results.push(makeResult(i, line, "getFieldIndex", "performance", "warning", "P1:循环内重复获取 FieldIndex 会造成性能损耗;应在循环外缓存", "p1-loop-field-index"));
|
|
199
225
|
}
|
|
226
|
+
|
|
227
|
+
if (/SerializationUtils\s*\.\s*toJsonString\s*\(/.test(line) && /(view|model|event|page|args|this|getView\s*\(|getModel\s*\(|\be\b)/i.test(line)) {
|
|
228
|
+
results.push(makeResult(i, line, "SerializationUtils", "performance", "warning", "P1:不要序列化页面、模型或事件大对象做日志,容易造成 CPU 和内存压力;只记录关键字段", "p1-heavy-json-serialization"));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const entryUiMatch = line.match(/\bgetView\s*\(\)\s*\.\s*(setEnable|setVisible)\s*\(/);
|
|
232
|
+
if (entryUiMatch) {
|
|
233
|
+
const statement = collectStatement(lines, i, 3);
|
|
234
|
+
const args = extractCallArgs(statement, /(setEnable|setVisible)\s*\(/);
|
|
235
|
+
if (args.length >= 2 && !looksLikeRowIndexedViewCall(args)) {
|
|
236
|
+
const fieldArgs = args.slice(1).join(",");
|
|
237
|
+
if (/(entry|entries|detail|row|qty|price|amount|material|item)/i.test(fieldArgs)) {
|
|
238
|
+
results.push(makeResult(i, line, entryUiMatch[1], "performance", "warning", "P1:疑似对分录字段使用单头 setEnable/setVisible;分录字段应使用带 rowIndex 的重载", "p1-entry-field-without-row-index"));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
200
242
|
}
|
|
201
243
|
|
|
202
244
|
if (beginInitCount > endInitCount) {
|
|
@@ -209,6 +251,45 @@ function checkUiPerformance(lines: string[]): CheckResult[] {
|
|
|
209
251
|
return results;
|
|
210
252
|
}
|
|
211
253
|
|
|
254
|
+
function checkViewInteractionOrder(lines: string[]): CheckResult[] {
|
|
255
|
+
const results: CheckResult[] = [];
|
|
256
|
+
|
|
257
|
+
for (let i = 0; i < lines.length; i++) {
|
|
258
|
+
const line = lines[i];
|
|
259
|
+
if (isCommentLine(line)) continue;
|
|
260
|
+
|
|
261
|
+
if (/setDataChanged\s*\(\s*false\s*\)/.test(line)) {
|
|
262
|
+
for (let j = i + 1; j < Math.min(lines.length, i + 25); j++) {
|
|
263
|
+
if (/^\s*}\s*$/.test(lines[j])) break;
|
|
264
|
+
if (!isCommentLine(lines[j]) && /(getModel\s*\(\)\s*\.\s*)?setValue\s*\(/.test(lines[j])) {
|
|
265
|
+
results.push(makeResult(i, line, "setDataChanged", "lifecycle", "warning", "P1:setDataChanged(false) 后仍继续 setValue,脏标记会被重新置回;应放到所有 setValue 之后", "p1-set-data-changed-before-setvalue"));
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (/showConfirm\s*\(/.test(line)) {
|
|
272
|
+
const statement = collectStatement(lines, i, 8);
|
|
273
|
+
const args = extractCallArgs(statement, /showConfirm\s*\(/);
|
|
274
|
+
if (args.length < 3 || /\bnull\b/.test(args[2] ?? "")) {
|
|
275
|
+
results.push(makeResult(i, line, "showConfirm", "lifecycle", "error", "P0:showConfirm 未提供有效回调监听器,用户选择不会触发业务处理", "p0-show-confirm-without-callback"));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (/getView\s*\(\)\s*\.\s*close\s*\(/.test(line)) {
|
|
280
|
+
for (let j = i + 1; j < Math.min(lines.length, i + 10); j++) {
|
|
281
|
+
if (/^\s*}\s*$/.test(lines[j])) break;
|
|
282
|
+
if (!isCommentLine(lines[j]) && /returnDataToParent\s*\(/.test(lines[j])) {
|
|
283
|
+
results.push(makeResult(i, line, "close", "lifecycle", "error", "P0:先 close 再 returnDataToParent 会导致返回数据不执行;应先 returnDataToParent 再 close", "p0-close-before-return-data"));
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return results;
|
|
291
|
+
}
|
|
292
|
+
|
|
212
293
|
function checkLoggingAndDiagnostics(lines: string[]): CheckResult[] {
|
|
213
294
|
const results: CheckResult[] = [];
|
|
214
295
|
|
|
@@ -548,6 +629,68 @@ function isInsideLoop(lines: string[], index: number): boolean {
|
|
|
548
629
|
return false;
|
|
549
630
|
}
|
|
550
631
|
|
|
632
|
+
function collectStatement(lines: string[], start: number, maxExtraLines: number): string {
|
|
633
|
+
const parts: string[] = [];
|
|
634
|
+
for (let i = start; i < Math.min(lines.length, start + maxExtraLines + 1); i++) {
|
|
635
|
+
parts.push(lines[i]);
|
|
636
|
+
if (/[;{}]\s*$/.test(lines[i].trim())) break;
|
|
637
|
+
}
|
|
638
|
+
return parts.join("\n");
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function extractCallArgs(statement: string, callPattern: RegExp): string[] {
|
|
642
|
+
const match = callPattern.exec(statement);
|
|
643
|
+
if (!match || match.index === undefined) return [];
|
|
644
|
+
const open = statement.indexOf("(", match.index);
|
|
645
|
+
if (open < 0) return [];
|
|
646
|
+
|
|
647
|
+
const args: string[] = [];
|
|
648
|
+
let depth = 0;
|
|
649
|
+
let current = "";
|
|
650
|
+
let quote: "'" | '"' | undefined;
|
|
651
|
+
for (let i = open + 1; i < statement.length; i++) {
|
|
652
|
+
const ch = statement[i];
|
|
653
|
+
const prev = statement[i - 1];
|
|
654
|
+
if (quote) {
|
|
655
|
+
current += ch;
|
|
656
|
+
if (ch === quote && prev !== "\\") quote = undefined;
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
if (ch === "'" || ch === '"') {
|
|
660
|
+
quote = ch;
|
|
661
|
+
current += ch;
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
if (ch === "(" || ch === "[" || ch === "{") {
|
|
665
|
+
depth++;
|
|
666
|
+
current += ch;
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
if (ch === ")" && depth === 0) {
|
|
670
|
+
if (current.trim()) args.push(current.trim());
|
|
671
|
+
return args;
|
|
672
|
+
}
|
|
673
|
+
if (ch === ")" || ch === "]" || ch === "}") {
|
|
674
|
+
depth--;
|
|
675
|
+
current += ch;
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
if (ch === "," && depth === 0) {
|
|
679
|
+
args.push(current.trim());
|
|
680
|
+
current = "";
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
current += ch;
|
|
684
|
+
}
|
|
685
|
+
return [];
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function looksLikeRowIndexedViewCall(args: string[]): boolean {
|
|
689
|
+
if (args.length < 3) return false;
|
|
690
|
+
const second = args[1].trim();
|
|
691
|
+
return /^(rowIndex|index|idx|i|row|entryIndex|\d+)$/.test(second) || /\bgetRowIndex\s*\(/.test(second);
|
|
692
|
+
}
|
|
693
|
+
|
|
551
694
|
function isCommentLine(line: string): boolean {
|
|
552
695
|
const trimmed = line.trim();
|
|
553
696
|
return trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*");
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { extname, join, relative } from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { resolveWorkspacePath } from "../platform/path.ts";
|
|
6
|
+
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
|
|
9
|
+
export type SdkSignatureLanguage = "java" | "csharp";
|
|
10
|
+
|
|
11
|
+
export interface SdkSignatureParams {
|
|
12
|
+
language: SdkSignatureLanguage;
|
|
13
|
+
query?: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
method?: string;
|
|
16
|
+
path?: string;
|
|
17
|
+
limit?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SdkSignatureResult {
|
|
21
|
+
language: SdkSignatureLanguage;
|
|
22
|
+
query: string;
|
|
23
|
+
exitCode: number;
|
|
24
|
+
stdout: string;
|
|
25
|
+
stderr: string;
|
|
26
|
+
sources: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const SKIP_DIRS = new Set([
|
|
30
|
+
".git",
|
|
31
|
+
".gradle",
|
|
32
|
+
".idea",
|
|
33
|
+
".pi",
|
|
34
|
+
".tmp",
|
|
35
|
+
"dist",
|
|
36
|
+
"node_modules",
|
|
37
|
+
"out",
|
|
38
|
+
"target",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
export async function inspectSdkSignature(cwd: string, params: SdkSignatureParams): Promise<SdkSignatureResult> {
|
|
42
|
+
const query = (params.className || params.query || "").trim();
|
|
43
|
+
if (!query) {
|
|
44
|
+
return {
|
|
45
|
+
language: params.language,
|
|
46
|
+
query,
|
|
47
|
+
exitCode: 2,
|
|
48
|
+
stdout: "",
|
|
49
|
+
stderr: "Provide query or className for kd_sdk_signature. method is only a filter within a matched class/type.",
|
|
50
|
+
sources: [],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (params.language === "java") return inspectJavaSignature(cwd, params, query);
|
|
55
|
+
return inspectCsharpSignature(cwd, params, query);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function formatSdkSignatureResult(result: SdkSignatureResult): string {
|
|
59
|
+
return [
|
|
60
|
+
`Language: ${result.language}`,
|
|
61
|
+
`Query: ${result.query}`,
|
|
62
|
+
`Exit: ${result.exitCode}`,
|
|
63
|
+
result.sources.length ? `Sources:\n${result.sources.map((source) => `- ${source}`).join("\n")}` : "Sources: none",
|
|
64
|
+
result.stdout.trim() ? `\nSTDOUT:\n${result.stdout.trim()}` : undefined,
|
|
65
|
+
result.stderr.trim() ? `\nSTDERR:\n${result.stderr.trim()}` : undefined,
|
|
66
|
+
"",
|
|
67
|
+
"Use this as local SDK evidence only when Exit is 0. If it fails, use build output or project SDK configuration before trusting bundled knowledge.",
|
|
68
|
+
]
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
.join("\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function inspectJavaSignature(cwd: string, params: SdkSignatureParams, query: string): Promise<SdkSignatureResult> {
|
|
74
|
+
const roots = scanRoots(cwd, params.path);
|
|
75
|
+
const jars = roots.flatMap((root) => findFiles(root, ".jar", params.limit ?? 200));
|
|
76
|
+
if (jars.length === 0) {
|
|
77
|
+
return {
|
|
78
|
+
language: "java",
|
|
79
|
+
query,
|
|
80
|
+
exitCode: 2,
|
|
81
|
+
stdout: "",
|
|
82
|
+
stderr: "No jar files found in the current project. Build/copy dependencies first or pass path=<sdk/lib directory>.",
|
|
83
|
+
sources: [],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const classNames = params.className ? [params.className] : await findJavaClasses(jars, query, params.limit ?? 20);
|
|
88
|
+
if (classNames.length === 0) {
|
|
89
|
+
return {
|
|
90
|
+
language: "java",
|
|
91
|
+
query,
|
|
92
|
+
exitCode: 1,
|
|
93
|
+
stdout: "",
|
|
94
|
+
stderr: `No class matching "${query}" found in project jars.`,
|
|
95
|
+
sources: jars.map((jar) => relativeOrSelf(cwd, jar)).slice(0, 20),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const classpath = jars.join(process.platform === "win32" ? ";" : ":");
|
|
100
|
+
const outputs: string[] = [];
|
|
101
|
+
const errors: string[] = [];
|
|
102
|
+
const inspectedSources = new Set<string>();
|
|
103
|
+
|
|
104
|
+
for (const className of classNames.slice(0, params.limit ?? 10)) {
|
|
105
|
+
try {
|
|
106
|
+
const result = await execFileAsync("javap", ["-classpath", classpath, "-public", className], {
|
|
107
|
+
cwd,
|
|
108
|
+
timeout: 60_000,
|
|
109
|
+
maxBuffer: 1024 * 1024 * 4,
|
|
110
|
+
});
|
|
111
|
+
const text = filterMethodLines(result.stdout || "", params.method);
|
|
112
|
+
if (text.trim()) outputs.push(`## ${className}\n${text.trim()}`);
|
|
113
|
+
for (const source of jarsContainingClass(jars, className)) inspectedSources.add(relativeOrSelf(cwd, source));
|
|
114
|
+
} catch (error) {
|
|
115
|
+
const err = error as { message?: string; stderr?: string };
|
|
116
|
+
errors.push(`${className}: ${err.stderr || err.message || String(error)}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
language: "java",
|
|
122
|
+
query,
|
|
123
|
+
exitCode: outputs.length ? 0 : 1,
|
|
124
|
+
stdout: outputs.join("\n\n"),
|
|
125
|
+
stderr: errors.join("\n").trim(),
|
|
126
|
+
sources: [...inspectedSources].sort(),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function inspectCsharpSignature(cwd: string, params: SdkSignatureParams, query: string): Promise<SdkSignatureResult> {
|
|
131
|
+
const roots = scanRoots(cwd, params.path);
|
|
132
|
+
const dlls = roots.flatMap((root) => findFiles(root, ".dll", params.limit ?? 200));
|
|
133
|
+
if (dlls.length === 0) {
|
|
134
|
+
return {
|
|
135
|
+
language: "csharp",
|
|
136
|
+
query,
|
|
137
|
+
exitCode: 2,
|
|
138
|
+
stdout: "",
|
|
139
|
+
stderr: "No dll files found in the current project. Build/restore references first or pass path=<sdk/bin directory>.",
|
|
140
|
+
sources: [],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const script = [
|
|
145
|
+
"$ErrorActionPreference = 'SilentlyContinue'",
|
|
146
|
+
"$query = $args[0]",
|
|
147
|
+
"$method = $args[1]",
|
|
148
|
+
"$dlls = $args[2..($args.Length-1)]",
|
|
149
|
+
"$hits = New-Object System.Collections.Generic.List[string]",
|
|
150
|
+
"foreach ($dll in $dlls) {",
|
|
151
|
+
" try { $asm = [System.Reflection.Assembly]::LoadFrom($dll) } catch { continue }",
|
|
152
|
+
" try { $types = $asm.GetExportedTypes() } catch { continue }",
|
|
153
|
+
" foreach ($type in $types) {",
|
|
154
|
+
" if ($type.FullName -notlike \"*$query*\" -and $type.Name -notlike \"*$query*\") { continue }",
|
|
155
|
+
" $hits.Add(\"## \" + $type.FullName + \"`nAssembly: \" + $dll)",
|
|
156
|
+
" foreach ($m in $type.GetMethods([System.Reflection.BindingFlags]'Public,Instance,Static,DeclaredOnly')) {",
|
|
157
|
+
" if ($method -and $m.Name -notlike \"*$method*\") { continue }",
|
|
158
|
+
" $params = ($m.GetParameters() | ForEach-Object { $_.ParameterType.Name + ' ' + $_.Name }) -join ', '",
|
|
159
|
+
" $hits.Add(' ' + $m.ReturnType.Name + ' ' + $m.Name + '(' + $params + ')')",
|
|
160
|
+
" }",
|
|
161
|
+
" foreach ($p in $type.GetProperties([System.Reflection.BindingFlags]'Public,Instance,Static,DeclaredOnly')) {",
|
|
162
|
+
" if ($method -and $p.Name -notlike \"*$method*\") { continue }",
|
|
163
|
+
" $hits.Add(' property ' + $p.PropertyType.Name + ' ' + $p.Name)",
|
|
164
|
+
" }",
|
|
165
|
+
" }",
|
|
166
|
+
"}",
|
|
167
|
+
"$hits | Select-Object -First 200",
|
|
168
|
+
].join("; ");
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const executable = process.platform === "win32" ? "powershell" : "pwsh";
|
|
172
|
+
const result = await execFileAsync(executable, ["-NoProfile", "-Command", script, query, params.method ?? "", ...dlls], {
|
|
173
|
+
cwd,
|
|
174
|
+
timeout: 60_000,
|
|
175
|
+
maxBuffer: 1024 * 1024 * 4,
|
|
176
|
+
});
|
|
177
|
+
const stdout = result.stdout || "";
|
|
178
|
+
return {
|
|
179
|
+
language: "csharp",
|
|
180
|
+
query,
|
|
181
|
+
exitCode: stdout.trim() ? 0 : 1,
|
|
182
|
+
stdout,
|
|
183
|
+
stderr: stdout.trim() ? "" : `No public type matching "${query}" found in project dlls.`,
|
|
184
|
+
sources: dlls.map((dll) => relativeOrSelf(cwd, dll)).slice(0, 20),
|
|
185
|
+
};
|
|
186
|
+
} catch (error) {
|
|
187
|
+
const err = error as { message?: string; stderr?: string };
|
|
188
|
+
return {
|
|
189
|
+
language: "csharp",
|
|
190
|
+
query,
|
|
191
|
+
exitCode: 1,
|
|
192
|
+
stdout: "",
|
|
193
|
+
stderr: err.stderr || err.message || String(error),
|
|
194
|
+
sources: dlls.map((dll) => relativeOrSelf(cwd, dll)).slice(0, 20),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function findJavaClasses(jars: string[], query: string, limit: number): Promise<string[]> {
|
|
200
|
+
const basenameQuery = query.includes(".") ? query.split(".").at(-1) || query : query;
|
|
201
|
+
const classPattern = `${basenameQuery}.class`;
|
|
202
|
+
const results = new Set<string>();
|
|
203
|
+
|
|
204
|
+
for (const jar of jars) {
|
|
205
|
+
try {
|
|
206
|
+
const output = await execFileAsync("jar", ["tf", jar], { timeout: 30_000, maxBuffer: 1024 * 1024 * 4 });
|
|
207
|
+
for (const line of (output.stdout || "").split(/\r?\n/)) {
|
|
208
|
+
if (!line.endsWith(".class") || line.includes("$")) continue;
|
|
209
|
+
if (!line.toLowerCase().includes(classPattern.toLowerCase())) continue;
|
|
210
|
+
results.add(line.replace(/\.class$/, "").replace(/\//g, "."));
|
|
211
|
+
if (results.size >= limit) return [...results];
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
const matched = findClassNamesInJarBytes(jar, basenameQuery, limit - results.size);
|
|
215
|
+
for (const className of matched) results.add(className);
|
|
216
|
+
if (results.size >= limit) return [...results];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return [...results];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function findClassNamesInJarBytes(jar: string, query: string, limit: number): string[] {
|
|
224
|
+
const data = readFileSync(jar).toString("latin1");
|
|
225
|
+
const pattern = `${query}.class`;
|
|
226
|
+
const results = new Set<string>();
|
|
227
|
+
let index = data.indexOf(pattern);
|
|
228
|
+
while (index >= 0 && results.size < limit) {
|
|
229
|
+
let start = index;
|
|
230
|
+
while (start > 0 && /[A-Za-z0-9_/$-]/.test(data[start - 1])) start--;
|
|
231
|
+
const entry = data.slice(start, index + pattern.length);
|
|
232
|
+
if (entry.includes("/") && !entry.includes("$")) {
|
|
233
|
+
results.add(entry.replace(/\.class$/, "").replace(/\//g, "."));
|
|
234
|
+
}
|
|
235
|
+
index = data.indexOf(pattern, index + pattern.length);
|
|
236
|
+
}
|
|
237
|
+
return [...results];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function jarsContainingClass(jars: string[], className: string): string[] {
|
|
241
|
+
const entry = `${className.replace(/\./g, "/")}.class`;
|
|
242
|
+
return jars.filter((jar) => {
|
|
243
|
+
try {
|
|
244
|
+
const data = readFileSync(jar).toString("latin1");
|
|
245
|
+
return data.includes(entry);
|
|
246
|
+
} catch {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function filterMethodLines(output: string, method?: string): string {
|
|
253
|
+
if (!method) return output;
|
|
254
|
+
const lines = output.split(/\r?\n/);
|
|
255
|
+
const kept = lines.filter((line) => line.includes(`${method}(`) || /^(Compiled from|public class|public interface)/.test(line.trim()));
|
|
256
|
+
return kept.join("\n");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function scanRoots(cwd: string, path?: string): string[] {
|
|
260
|
+
if (path) {
|
|
261
|
+
const resolved = resolveWorkspacePath(cwd, path);
|
|
262
|
+
return existsSync(resolved) ? [resolved] : [];
|
|
263
|
+
}
|
|
264
|
+
return [cwd];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function findFiles(root: string, extension: string, limit: number): string[] {
|
|
268
|
+
if (existsSync(root)) {
|
|
269
|
+
try {
|
|
270
|
+
const stat = statSync(root);
|
|
271
|
+
if (stat.isFile()) return extname(root).toLowerCase() === extension ? [root] : [];
|
|
272
|
+
} catch {
|
|
273
|
+
return [];
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const results: string[] = [];
|
|
277
|
+
walk(root, extension, results, limit, 0);
|
|
278
|
+
return results;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function walk(dir: string, extension: string, results: string[], limit: number, depth: number): void {
|
|
282
|
+
if (results.length >= limit || depth > 8 || !existsSync(dir)) return;
|
|
283
|
+
let entries: string[];
|
|
284
|
+
try {
|
|
285
|
+
entries = readdirSync(dir);
|
|
286
|
+
} catch {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
for (const entry of entries) {
|
|
290
|
+
if (results.length >= limit) return;
|
|
291
|
+
const full = join(dir, entry);
|
|
292
|
+
let stat;
|
|
293
|
+
try {
|
|
294
|
+
stat = statSync(full);
|
|
295
|
+
} catch {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (stat.isDirectory()) {
|
|
299
|
+
if (!SKIP_DIRS.has(entry)) walk(full, extension, results, limit, depth + 1);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (stat.isFile() && extname(entry).toLowerCase() === extension) results.push(full);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function relativeOrSelf(cwd: string, path: string): string {
|
|
307
|
+
const rel = relative(cwd, path);
|
|
308
|
+
return rel && !rel.startsWith("..") ? rel : path;
|
|
309
|
+
}
|
|
@@ -304,7 +304,7 @@ references/
|
|
|
304
304
|
|
|
305
305
|
### 📌 步骤3: 执行模式匹配扫描
|
|
306
306
|
|
|
307
|
-
**使用
|
|
307
|
+
**使用 KCode `kd_check` 或手动执行以下模式检查**:
|
|
308
308
|
|
|
309
309
|
```
|
|
310
310
|
📌 步骤3: 执行模式匹配扫描
|
|
@@ -671,4 +671,4 @@ D:\new_workspace\macc-dev\pm\report\代码审查报告_xxx.html ❌ 子模块
|
|
|
671
671
|
### 幂等性
|
|
672
672
|
- 重复执行是否产生副作用
|
|
673
673
|
- MQ 消费是否支持重复消费
|
|
674
|
-
- 操作是否可安全重试
|
|
674
|
+
- 操作是否可安全重试
|