kcode-pi 0.1.5 → 0.1.7

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 (50) hide show
  1. package/README.md +35 -2
  2. package/dist/cli/kcode.d.ts +1 -0
  3. package/dist/cli/kcode.js +27 -4
  4. package/package.json +1 -1
  5. package/src/cli/kcode.ts +29 -4
  6. package/src/official/kingdee-skills.ts +60 -13
  7. package/src/rules/checker.ts +143 -0
  8. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/SKILL.md +2 -2
  9. package/vendor/kingdee-skills/ok-cosmic/SKILL.md +52 -101
  10. package/vendor/kingdee-skills/ok-cosmic/agents/openai.yaml +4 -4
  11. package/vendor/kingdee-skills/ok-cosmic/manifest.json +21 -20
  12. package/vendor/kingdee-skills/ok-cosmic/ok-cosmic-intro.html +1 -1
  13. package/vendor/kingdee-skills/ok-cosmic/rules/a-layer-rules.json +1 -1
  14. package/vendor/kingdee-skills/ok-cosmic/rules/anti-patterns.md +2 -2
  15. package/vendor/kingdee-skills/ok-cosmic/rules/coding-preferences.md +4 -4
  16. package/vendor/kingdee-skills/ok-cosmic/rules/constraints.md +3 -3
  17. package/vendor/kingdee-skills/ok-cosmic/rules/decision-matrix.md +8 -8
  18. package/vendor/kingdee-skills/ok-cosmic/rules/intent-routing.md +1 -1
  19. package/vendor/kingdee-skills/ok-cosmic/rules/post-check.md +19 -18
  20. package/vendor/kingdee-skills/ok-ksql/SKILL.md +9 -9
  21. package/vendor/kingdee-skills/ok-ksql/manifest.json +2 -1
  22. package/vendor/kingdee-skills/ok-ksql/references/ksql-datafix.md +2 -2
  23. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/scripts/pattern-matcher.py +0 -336
  24. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/scripts/review-score-calculator.py +0 -121
  25. package/vendor/kingdee-skills/ok-cosmic/CHANGELOG.md +0 -295
  26. package/vendor/kingdee-skills/ok-cosmic/README.md +0 -460
  27. package/vendor/kingdee-skills/ok-cosmic/requirements.txt +0 -2
  28. package/vendor/kingdee-skills/ok-cosmic/scripts/config_loader.py +0 -204
  29. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-api-knowledge.py +0 -910
  30. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-basedata-query.py +0 -359
  31. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-config-check.py +0 -181
  32. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-extpoints-query.py +0 -389
  33. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-form-metadata.py +0 -856
  34. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-post-check.py +0 -262
  35. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-post-lint.py +0 -293
  36. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/__init__.py +0 -2
  37. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/base.py +0 -393
  38. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/resource_check.py +0 -176
  39. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/scene_check.py +0 -375
  40. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/style_check.py +0 -434
  41. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/verify_check.py +0 -36
  42. package/vendor/kingdee-skills/ok-cosmic/scripts/route_client.py +0 -186
  43. package/vendor/kingdee-skills/ok-cosmic/scripts/script_utils.py +0 -40
  44. package/vendor/kingdee-skills/ok-cosmic/scripts/sqlite_cache.py +0 -142
  45. package/vendor/kingdee-skills/ok-cosmic/setup/cuslib/kd-cd-cosmic-commons.jar +0 -0
  46. package/vendor/kingdee-skills/ok-cosmic/setup/cuslib/kd-cd-cosmic-features.jar +0 -0
  47. package/vendor/kingdee-skills/ok-cosmic/setup/setup-mac.sh +0 -18
  48. package/vendor/kingdee-skills/ok-cosmic/setup/setup-windows.bat +0 -53
  49. package/vendor/kingdee-skills/ok-cosmic/setup/setup.jar +0 -0
  50. package/vendor/kingdee-skills/ok-ksql/scripts/ksql_lint.py +0 -363
package/README.md CHANGED
@@ -61,6 +61,12 @@ kcode context --refresh
61
61
  kcode doctor
62
62
  ```
63
63
 
64
+ 查看当前 KCode 版本:
65
+
66
+ ```powershell
67
+ kcode version
68
+ ```
69
+
64
70
  启动工作环境:
65
71
 
66
72
  ```powershell
@@ -236,6 +242,16 @@ kcode context --refresh
236
242
  kcode doctor
237
243
  ```
238
244
 
245
+ ### `kcode version`
246
+
247
+ 显示当前 KCode package 版本、安装路径、随包 Pi CLI 版本和 Node 版本。
248
+
249
+ ```powershell
250
+ kcode version
251
+ kcode --version
252
+ kcode -v
253
+ ```
254
+
239
255
  ### `kcode start`
240
256
 
241
257
  启动 KCode 工作环境。
@@ -398,15 +414,32 @@ kd_debug 分析金蝶日志和堆栈
398
414
  这些工具不依赖本机 Python:
399
415
 
400
416
  - `kd_ksql_lint` 是内置 Node 静态检查器。
401
- - `kd_cosmic_config` 使用 Node 读取并校验 `ok-cosmic.json`。
402
- - `kd_cosmic_metadata` 使用 `ok-cosmic.json` 中的统一路由 API,并在当前项目 `.pi/kd/official-skills/` 下维护 JSON 缓存。
417
+ - `kd_cosmic_config` 使用 Node 校验 Cosmic 官方能力配置;项目没有 `ok-cosmic.json` 时会自动使用 KCode 随包默认配置。
418
+ - `kd_cosmic_metadata` 使用统一路由 API 查询真实单据/表单元数据,并在当前项目 `.pi/kd/official-skills/` 下维护 JSON 缓存。
403
419
  - `kd_cosmic_api` 查询随包金蝶知识库;需要精确方法签名时,仍要结合当前项目 SDK、编译输出或红绿证据确认。
404
420
 
421
+ `ok-cosmic.json` 是可选的 KCode 官方能力覆盖配置,不是苍穹工程模板里的 `cosmic.json`。业务项目里的 `cosmic.json` 通常只包含开发者标识、工程标识、MC 资源地址等运行环境信息,不能替代 `ok-cosmic.json`。
422
+
423
+ 只有需要指定企业统一路由 API 或覆盖知识库路径时,才需要在业务项目根目录创建 `ok-cosmic.json`:
424
+
425
+ ```json
426
+ {
427
+ "graph": {
428
+ "dbPath": "D:\\path\\to\\ok-cosmic-docs.db"
429
+ },
430
+ "route": {
431
+ "apiUrl": "https://your-runtime-route.example.com/api",
432
+ "timeoutSeconds": 10
433
+ }
434
+ }
435
+ ```
436
+
405
437
  ## 升级
406
438
 
407
439
  查看当前安装版本:
408
440
 
409
441
  ```powershell
442
+ kcode version
410
443
  npm ls -g kcode-pi --depth=0
411
444
  ```
412
445
 
@@ -12,5 +12,6 @@ export declare function runKcodeCli(args: string[], cwd?: string): KcodeCliResul
12
12
  export declare function initProject(cwd: string): KcodeCliResult;
13
13
  export declare function context(cwd: string, args: string[]): KcodeCliResult;
14
14
  export declare function doctor(cwd: string): KcodeCliResult;
15
+ export declare function version(): KcodeCliResult;
15
16
  export declare function start(cwd: string, piArgs: string[]): KcodeCliResult;
16
17
  export declare function resolvePiCliCommand(piArgs?: string[]): PiCliCommand | undefined;
package/dist/cli/kcode.js CHANGED
@@ -6,7 +6,9 @@ import { createRequire } from "node:module";
6
6
  import { ensureProjectContext, writeProjectContext } from "../context/project-context.js";
7
7
  const packageRoot = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
8
8
  const require = createRequire(import.meta.url);
9
- const packageName = readPackageName(packageRoot) ?? "kcode-pi";
9
+ const packageMetadata = readPackageMetadata(packageRoot);
10
+ const packageName = packageMetadata.name ?? "kcode-pi";
11
+ const packageVersion = packageMetadata.version ?? "unknown";
10
12
  export function runKcodeCli(args, cwd = process.cwd()) {
11
13
  const command = args[0] ?? "help";
12
14
  switch (command) {
@@ -16,6 +18,10 @@ export function runKcodeCli(args, cwd = process.cwd()) {
16
18
  return context(cwd, args.slice(1));
17
19
  case "doctor":
18
20
  return doctor(cwd);
21
+ case "version":
22
+ case "--version":
23
+ case "-v":
24
+ return version();
19
25
  case "start":
20
26
  return start(cwd, args.slice(1));
21
27
  case "help":
@@ -57,6 +63,7 @@ export function doctor(cwd) {
57
63
  const settingsPath = projectSettingsPath(cwd);
58
64
  lines.push(`Node:${node.status === 0 ? node.stdout.trim() : "未找到"}`);
59
65
  lines.push(`Pi CLI:${formatPiCliStatus(piCli, pi)}`);
66
+ lines.push(`KCode version:${packageName}@${packageVersion}`);
60
67
  lines.push(`KCode package:${packageRoot}`);
61
68
  lines.push(`项目配置:${existsSync(settingsPath) ? settingsPath : "未创建,请先运行 kcode init"}`);
62
69
  lines.push(`项目上下文:${existsSync(join(cwd, ".pi", "kd", "PROJECT_CONTEXT.md")) ? join(cwd, ".pi", "kd", "PROJECT_CONTEXT.md") : "未创建,请运行 kcode context"}`);
@@ -70,6 +77,19 @@ export function doctor(cwd) {
70
77
  output: lines.join("\n"),
71
78
  };
72
79
  }
80
+ export function version() {
81
+ const piCli = resolvePiCliCommand(["--version"]);
82
+ const pi = piCli ? spawnSync(piCli.command, piCli.args, { encoding: "utf8" }) : undefined;
83
+ return {
84
+ exitCode: 0,
85
+ output: [
86
+ `${packageName}@${packageVersion}`,
87
+ `KCode package:${packageRoot}`,
88
+ `Pi CLI:${formatPiCliStatus(piCli, pi)}`,
89
+ `Node:${process.version}`,
90
+ ].join("\n"),
91
+ };
92
+ }
73
93
  export function start(cwd, piArgs) {
74
94
  const init = initProject(cwd);
75
95
  const piCli = resolvePiCliCommand(piArgs);
@@ -127,12 +147,14 @@ function isSameKcodePackage(candidate, currentPackage) {
127
147
  return candidateName === packageName;
128
148
  }
129
149
  function readPackageName(packagePath) {
150
+ return readPackageMetadata(packagePath).name;
151
+ }
152
+ function readPackageMetadata(packagePath) {
130
153
  try {
131
- const packageJson = JSON.parse(readFileSync(join(packagePath, "package.json"), "utf8"));
132
- return packageJson.name;
154
+ return JSON.parse(readFileSync(join(packagePath, "package.json"), "utf8"));
133
155
  }
134
156
  catch {
135
- return undefined;
157
+ return {};
136
158
  }
137
159
  }
138
160
  function packageNameFromPath(path) {
@@ -182,6 +204,7 @@ function helpText() {
182
204
  " kcode init 初始化当前项目的 .pi/settings.json",
183
205
  " kcode context 生成或刷新 .pi/kd/PROJECT_CONTEXT.md",
184
206
  " kcode doctor 检查 Node、随包 Pi CLI、KCode package 和项目级配置",
207
+ " kcode version 显示 KCode、随包 Pi CLI 和 Node 版本",
185
208
  " kcode start 初始化项目配置后启动 KCode 工作环境",
186
209
  ].join("\n");
187
210
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kcode-pi",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Kingdee-specific package and harness for Pi Coding Agent",
5
5
  "type": "module",
6
6
  "private": false,
package/src/cli/kcode.ts CHANGED
@@ -17,7 +17,9 @@ interface PiSettings {
17
17
 
18
18
  const packageRoot = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
19
19
  const require = createRequire(import.meta.url);
20
- const packageName = readPackageName(packageRoot) ?? "kcode-pi";
20
+ const packageMetadata = readPackageMetadata(packageRoot);
21
+ const packageName = packageMetadata.name ?? "kcode-pi";
22
+ const packageVersion = packageMetadata.version ?? "unknown";
21
23
 
22
24
  export interface PiCliCommand {
23
25
  command: string;
@@ -36,6 +38,10 @@ export function runKcodeCli(args: string[], cwd = process.cwd()): KcodeCliResult
36
38
  return context(cwd, args.slice(1));
37
39
  case "doctor":
38
40
  return doctor(cwd);
41
+ case "version":
42
+ case "--version":
43
+ case "-v":
44
+ return version();
39
45
  case "start":
40
46
  return start(cwd, args.slice(1));
41
47
  case "help":
@@ -84,6 +90,7 @@ export function doctor(cwd: string): KcodeCliResult {
84
90
 
85
91
  lines.push(`Node:${node.status === 0 ? node.stdout.trim() : "未找到"}`);
86
92
  lines.push(`Pi CLI:${formatPiCliStatus(piCli, pi)}`);
93
+ lines.push(`KCode version:${packageName}@${packageVersion}`);
87
94
  lines.push(`KCode package:${packageRoot}`);
88
95
  lines.push(`项目配置:${existsSync(settingsPath) ? settingsPath : "未创建,请先运行 kcode init"}`);
89
96
  lines.push(`项目上下文:${existsSync(join(cwd, ".pi", "kd", "PROJECT_CONTEXT.md")) ? join(cwd, ".pi", "kd", "PROJECT_CONTEXT.md") : "未创建,请运行 kcode context"}`);
@@ -100,6 +107,20 @@ export function doctor(cwd: string): KcodeCliResult {
100
107
  };
101
108
  }
102
109
 
110
+ export function version(): KcodeCliResult {
111
+ const piCli = resolvePiCliCommand(["--version"]);
112
+ const pi = piCli ? spawnSync(piCli.command, piCli.args, { encoding: "utf8" }) : undefined;
113
+ return {
114
+ exitCode: 0,
115
+ output: [
116
+ `${packageName}@${packageVersion}`,
117
+ `KCode package:${packageRoot}`,
118
+ `Pi CLI:${formatPiCliStatus(piCli, pi)}`,
119
+ `Node:${process.version}`,
120
+ ].join("\n"),
121
+ };
122
+ }
123
+
103
124
  export function start(cwd: string, piArgs: string[]): KcodeCliResult {
104
125
  const init = initProject(cwd);
105
126
  const piCli = resolvePiCliCommand(piArgs);
@@ -168,11 +189,14 @@ function isSameKcodePackage(candidate: string, currentPackage: string): boolean
168
189
  }
169
190
 
170
191
  function readPackageName(packagePath: string): string | undefined {
192
+ return readPackageMetadata(packagePath).name;
193
+ }
194
+
195
+ function readPackageMetadata(packagePath: string): { name?: string; version?: string } {
171
196
  try {
172
- const packageJson = JSON.parse(readFileSync(join(packagePath, "package.json"), "utf8")) as { name?: string };
173
- return packageJson.name;
197
+ return JSON.parse(readFileSync(join(packagePath, "package.json"), "utf8")) as { name?: string; version?: string };
174
198
  } catch {
175
- return undefined;
199
+ return {};
176
200
  }
177
201
  }
178
202
 
@@ -229,6 +253,7 @@ function helpText(): string {
229
253
  " kcode init 初始化当前项目的 .pi/settings.json",
230
254
  " kcode context 生成或刷新 .pi/kd/PROJECT_CONTEXT.md",
231
255
  " kcode doctor 检查 Node、随包 Pi CLI、KCode package 和项目级配置",
256
+ " kcode version 显示 KCode、随包 Pi CLI 和 Node 版本",
232
257
  " kcode start 初始化项目配置后启动 KCode 工作环境",
233
258
  ].join("\n");
234
259
  }
@@ -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("/*");
@@ -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
+ - 操作是否可安全重试