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.
Files changed (62) hide show
  1. package/README.md +40 -5
  2. package/docs/DEVELOPMENT.md +2 -0
  3. package/extensions/kingdee-harness.ts +139 -1
  4. package/extensions/kingdee-tools.ts +43 -1
  5. package/package.json +2 -1
  6. package/skills/kd-check/SKILL.md +1 -2
  7. package/skills/kd-cosmic-dev/SKILL.md +3 -3
  8. package/skills/kd-cosmic-review/SKILL.md +2 -2
  9. package/skills/kd-cosmic-unittest/SKILL.md +2 -2
  10. package/skills/kd-debug/SKILL.md +1 -2
  11. package/skills/kd-execute/SKILL.md +2 -2
  12. package/skills/kd-gen/SKILL.md +2 -1
  13. package/skills/kd-plan/SKILL.md +3 -3
  14. package/src/harness/gates.ts +14 -1
  15. package/src/harness/state.ts +44 -1
  16. package/src/harness/types.ts +14 -0
  17. package/src/official/kingdee-skills.ts +60 -13
  18. package/src/rules/checker.ts +143 -0
  19. package/src/tools/sdk-signature.ts +309 -0
  20. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/SKILL.md +2 -2
  21. package/vendor/kingdee-skills/ok-cosmic/SKILL.md +52 -101
  22. package/vendor/kingdee-skills/ok-cosmic/agents/openai.yaml +4 -4
  23. package/vendor/kingdee-skills/ok-cosmic/manifest.json +21 -20
  24. package/vendor/kingdee-skills/ok-cosmic/ok-cosmic-intro.html +1 -1
  25. package/vendor/kingdee-skills/ok-cosmic/rules/a-layer-rules.json +1 -1
  26. package/vendor/kingdee-skills/ok-cosmic/rules/anti-patterns.md +2 -2
  27. package/vendor/kingdee-skills/ok-cosmic/rules/coding-preferences.md +4 -4
  28. package/vendor/kingdee-skills/ok-cosmic/rules/constraints.md +3 -3
  29. package/vendor/kingdee-skills/ok-cosmic/rules/decision-matrix.md +8 -8
  30. package/vendor/kingdee-skills/ok-cosmic/rules/intent-routing.md +1 -1
  31. package/vendor/kingdee-skills/ok-cosmic/rules/post-check.md +19 -18
  32. package/vendor/kingdee-skills/ok-ksql/SKILL.md +9 -9
  33. package/vendor/kingdee-skills/ok-ksql/manifest.json +2 -1
  34. package/vendor/kingdee-skills/ok-ksql/references/ksql-datafix.md +2 -2
  35. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/scripts/pattern-matcher.py +0 -336
  36. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/scripts/review-score-calculator.py +0 -121
  37. package/vendor/kingdee-skills/ok-cosmic/CHANGELOG.md +0 -295
  38. package/vendor/kingdee-skills/ok-cosmic/README.md +0 -460
  39. package/vendor/kingdee-skills/ok-cosmic/requirements.txt +0 -2
  40. package/vendor/kingdee-skills/ok-cosmic/scripts/config_loader.py +0 -204
  41. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-api-knowledge.py +0 -910
  42. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-basedata-query.py +0 -359
  43. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-config-check.py +0 -181
  44. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-extpoints-query.py +0 -389
  45. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-form-metadata.py +0 -856
  46. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-post-check.py +0 -262
  47. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-post-lint.py +0 -293
  48. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/__init__.py +0 -2
  49. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/base.py +0 -393
  50. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/resource_check.py +0 -176
  51. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/scene_check.py +0 -375
  52. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/style_check.py +0 -434
  53. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/verify_check.py +0 -36
  54. package/vendor/kingdee-skills/ok-cosmic/scripts/route_client.py +0 -186
  55. package/vendor/kingdee-skills/ok-cosmic/scripts/script_utils.py +0 -40
  56. package/vendor/kingdee-skills/ok-cosmic/scripts/sqlite_cache.py +0 -142
  57. package/vendor/kingdee-skills/ok-cosmic/setup/cuslib/kd-cd-cosmic-commons.jar +0 -0
  58. package/vendor/kingdee-skills/ok-cosmic/setup/cuslib/kd-cd-cosmic-features.jar +0 -0
  59. package/vendor/kingdee-skills/ok-cosmic/setup/setup-mac.sh +0 -18
  60. package/vendor/kingdee-skills/ok-cosmic/setup/setup-windows.bat +0 -53
  61. package/vendor/kingdee-skills/ok-cosmic/setup/setup.jar +0 -0
  62. package/vendor/kingdee-skills/ok-ksql/scripts/ksql_lint.py +0 -363
@@ -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
- return config ? resolveWorkspacePath(cwd, config) : resolve(cwd, "ok-cosmic.json");
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
- async function runCosmicConfig(_cwd: string, configPath: string): Promise<Omit<CommandResult, "command">> {
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 config: Record<string, unknown> | undefined;
247
+ let loaded: LoadedCosmicConfig | undefined;
239
248
  try {
240
- if (!existsSync(configPath)) throw new Error(`找不到配置文件: ${configPath}`);
241
- config = readJsonObject(configPath);
242
- issues.push({ level: "OK", key: "__file__", message: `已找到配置文件: ${configPath}` });
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 (config) issues.push(...validateCosmicConfig(config, dirname(configPath)));
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 = readJsonObject(configPath);
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 或中文单据名。" };
@@ -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
- **使用 pattern-matcher.py 或手动执行以下模式检查**:
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
+ - 操作是否可安全重试