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.
- package/README.md +35 -2
- package/dist/cli/kcode.d.ts +1 -0
- package/dist/cli/kcode.js +27 -4
- package/package.json +1 -1
- package/src/cli/kcode.ts +29 -4
- package/src/official/kingdee-skills.ts +60 -13
- package/src/rules/checker.ts +143 -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/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
|
|
402
|
-
- `kd_cosmic_metadata`
|
|
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
|
|
package/dist/cli/kcode.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
132
|
-
return packageJson.name;
|
|
154
|
+
return JSON.parse(readFileSync(join(packagePath, "package.json"), "utf8"));
|
|
133
155
|
}
|
|
134
156
|
catch {
|
|
135
|
-
return
|
|
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
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
|
|
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
|
-
|
|
173
|
-
return packageJson.name;
|
|
197
|
+
return JSON.parse(readFileSync(join(packagePath, "package.json"), "utf8")) as { name?: string; version?: string };
|
|
174
198
|
} catch {
|
|
175
|
-
return
|
|
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
|
-
|
|
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("/*");
|
|
@@ -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
|
+
- 操作是否可安全重试
|