fix(workflow): 补充网络 Agent 审查与 TSX 规则
This commit is contained in:
parent
f9189d5e1e
commit
d585e8706d
@ -145,7 +145,7 @@ Windows 上的 stdio MCP 客户端通过 `wsl.exe` 启动 Debian Bash;Node、
|
||||
| `pnpm run obsidian-context` | 输出 Obsidian 索引上下文;可传 `--module Admin`、`--query QQBot`、`--max-documents 10`。 |
|
||||
| `pnpm run obsidian-validate` | 校验 KT Obsidian vault 结构和链接;默认 warning 不让脚本失败,需要严格模式时传 `--fail-on-warnings`。 |
|
||||
| `pnpm run obsidian-sync` | 审计 Obsidian 工作流入口是否连通,并联动执行 validate。 |
|
||||
| `pnpm run cleanup-history` | 预览 `.kt-workspace` 测试/验证历史产物清理;真实清理用 `pnpm run cleanup-history -- --execute`,只保留最近 3 轮并保留模板目录。 |
|
||||
| `pnpm run cleanup-history` | 预览 `.kt-workspace` 测试/验证历史产物清理;真实清理用 `pnpm run cleanup-history -- --execute`,只保留最近 3 轮,并保留模板、活动运行输入和 `.kt-workspace/toolchains`。 |
|
||||
| `pnpm run test:unit` | 只运行显式登记的 Node 22 单元测试,不触发完整 evidence、Obsidian 或多仓集成构造。 |
|
||||
| `pnpm run test:live2d` | 运行 Live2D analyzer、review schema、S192-S217 历史切片、完整调用目录、implementation-target、module migration、variable/identifier 与 runtime coverage 独立测试。 |
|
||||
| `pnpm run test:integration` | 只运行 workflow closeout、Obsidian、cleanup、doc-sync 和 synthetic evidence writer 集成测试。 |
|
||||
@ -166,6 +166,8 @@ Windows 上的 stdio MCP 客户端通过 `wsl.exe` 启动 Debian Bash;Node、
|
||||
| `root` | `/home/yemu2/KT` |
|
||||
| `mcp` | `mcp/ktWorkflow` |
|
||||
| `napcat` | `GitHub/NapCatQQ` |
|
||||
| `networkAgent` | `Go/kt-network-agent` |
|
||||
| `palworld` | `Go/palworld-direct-connect` |
|
||||
| `api` | `Node/kt-template-online-api` |
|
||||
| `admin` | `Vue/kt-template-admin` |
|
||||
| `blog` | `Vue/kt-blog-web` |
|
||||
@ -210,7 +212,7 @@ Windows 上的 stdio MCP 客户端通过 `wsl.exe` 启动 Debian Bash;Node、
|
||||
- Admin 页面测试前可先执行 `pnpm run admin-login -- --url <Admin登录页>` 固化登录态,输出的 `storageState` 可作为后续 Playwright 用例前置状态。
|
||||
- 接口改动后调用 `kt_api_test_plan`,并真实请求一次接口。
|
||||
- 验证启动过本地服务后调用 `kt_cleanup_process_plan`,用只读计划核对路径、端口和 PID,再只结束本轮启动的进程。
|
||||
- 每轮测试/验证结束后先调用 `kt_cleanup_history`,或运行 `pnpm run cleanup-history -- --dry-run`;如果预览里 `deleted` 非空,确认范围后执行 `pnpm run cleanup-history -- --execute`,再复跑 dry-run 确认 `deleted=0`,让 `.kt-workspace` 历史产物只保留最近 3 轮并保留模板目录。Pio Live2D 的 `runtime-publish`、`runtime-export-moc-driven`、`probe-source-wordpress-parity`、`moc-runtime` 等活跃发布/基准输入会被保留,不按历史轮次删除。`kt_workstream_closeout` / `kt_workflow_loop_audit` 只认可明确的 `deleted=0` / `deleted=[]` 文本证据或结构化 `cleanupFinalDeleted=0`。
|
||||
- 每轮测试/验证结束后先调用 `kt_cleanup_history`,或运行 `pnpm run cleanup-history -- --dry-run`;如果预览里 `deleted` 非空,确认范围后执行 `pnpm run cleanup-history -- --execute`,再复跑 dry-run 确认 `deleted=0`,让 `.kt-workspace` 历史产物只保留最近 3 轮并保留模板目录。Pio Live2D 的 `runtime-publish`、`runtime-export-moc-driven`、`probe-source-wordpress-parity`、`moc-runtime` 等活跃发布/基准输入和 `.kt-workspace/toolchains` 会被保留,不按历史轮次删除。`kt_workstream_closeout` / `kt_workflow_loop_audit` 只认可明确的 `deleted=0` / `deleted=[]` 文本证据或结构化 `cleanupFinalDeleted=0`。
|
||||
- 更新 Obsidian 图谱、模块页、文档矩阵或 `.obsidian` 配置后运行 `kt_obsidian_validate` / `pnpm run obsidian-validate`;整理工作流入口时再跑 `kt_obsidian_sync`。
|
||||
- 改完文件后用 `kt_append_task_record` 先 `dryRun` 预览记录,再决定是否写入。
|
||||
- 提交前调用 `kt_commit_checklist`,确认文件范围和提交信息。
|
||||
|
||||
@ -89,6 +89,10 @@ test("preserves cleanup templates, workspace roots, and active Live2D runtime pa
|
||||
workspaceHistoryRoot?.preservedNames.includes("live2d-pio"),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
workspaceHistoryRoot?.preservedNames.includes("toolchains"),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
workspaceHistoryRoot?.preservedNames.includes("external"),
|
||||
false,
|
||||
|
||||
@ -6,5 +6,6 @@ import "./unit/deployObservation.test.js";
|
||||
import "./unit/guardrailsAndVerification.test.js";
|
||||
import "./unit/napcatAutomation.test.js";
|
||||
import "./unit/reviewGovernance.test.js";
|
||||
import "./unit/vueTsxReview.test.js";
|
||||
import "./unit/blogLive2DReviewRules.test.js";
|
||||
import "./unit/qqbotReviewRules.test.js";
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { registeredToolNames } from "../../core/constants.js";
|
||||
import { projectAliases, registeredToolNames } from "../../core/constants.js";
|
||||
import { registerTools } from "../../registerTools.js";
|
||||
import { hasEntityLoadTimeSerializationMutation } from "../../tools/envRisk.js";
|
||||
import { buildGuardrails } from "../../tools/guardrails.js";
|
||||
import { buildGlobalCodeReview } from "../../tools/review.js";
|
||||
import { buildGlobalCodeReview, defaultReviewProjects } from "../../tools/review.js";
|
||||
import {
|
||||
buildBusinessTestPlan,
|
||||
buildNapcatDeviceProfileCheck,
|
||||
@ -13,6 +13,13 @@ import {
|
||||
import { buildVerificationPlan } from "../../tools/verification.js";
|
||||
import { buildDbSyncPlan } from "../../tools/workflow.js";
|
||||
|
||||
test("keeps both network runtimes inside project inspection and default global review", () => {
|
||||
assert.equal(projectAliases.networkAgent, "Go/kt-network-agent");
|
||||
assert.equal(defaultReviewProjects.includes("networkAgent"), true);
|
||||
assert.equal(projectAliases.palworld, "Go/palworld-direct-connect");
|
||||
assert.equal(defaultReviewProjects.includes("palworld"), true);
|
||||
});
|
||||
|
||||
test("adds refactor-specific before-edit and behavior-preservation guardrails", () => {
|
||||
const result = buildGuardrails({
|
||||
project: "mcp",
|
||||
|
||||
40
src/__tests__/unit/vueTsxReview.test.ts
Normal file
40
src/__tests__/unit/vueTsxReview.test.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { findContextDependentVueTsxRenderHelpers } from "../../core/vueTsxReview.js";
|
||||
|
||||
/** Verifies safe setup-local render functions are not forced into arrow syntax. */
|
||||
test("allows function-declared Vue TSX slot render helpers without dynamic this", () => {
|
||||
const source = `
|
||||
function renderAgentControls() {
|
||||
return <Space>{status.value}</Space>;
|
||||
}
|
||||
function renderBodyCell({ record }) {
|
||||
return <Tag>{record.name}</Tag>;
|
||||
}
|
||||
return () => <KtTable v-slots={{
|
||||
bodyCell: renderBodyCell,
|
||||
headerControls: renderAgentControls,
|
||||
}} />;
|
||||
`;
|
||||
|
||||
assert.deepEqual(findContextDependentVueTsxRenderHelpers(source), []);
|
||||
});
|
||||
|
||||
/** Verifies only a render declaration that actually depends on dynamic this is flagged. */
|
||||
test("finds function-declared Vue TSX render helpers that read dynamic this", () => {
|
||||
const source = `
|
||||
function renderSafe() {
|
||||
function nestedOwner() { return this.label; }
|
||||
return nestedOwner();
|
||||
}
|
||||
function renderUnsafe() {
|
||||
const read = () => this.label;
|
||||
return <span>{read()}</span>;
|
||||
}
|
||||
`;
|
||||
|
||||
assert.deepEqual(findContextDependentVueTsxRenderHelpers(source), [
|
||||
{ line: 6, name: "renderUnsafe" },
|
||||
]);
|
||||
});
|
||||
@ -7,6 +7,8 @@ export const projectAliases = {
|
||||
knife4j: 'Plugins/knife4j-swagger-vue3',
|
||||
mcp: 'mcp/ktWorkflow',
|
||||
napcat: 'GitHub/NapCatQQ',
|
||||
networkAgent: 'Go/kt-network-agent',
|
||||
palworld: 'Go/palworld-direct-connect',
|
||||
playground: 'Vue/kt-template-online-playground',
|
||||
root: '.',
|
||||
web: 'Vue/kt-template-online-web',
|
||||
@ -20,6 +22,8 @@ export const projectLabels = {
|
||||
knife4j: 'Knife4j Swagger Vue3 插件',
|
||||
mcp: 'KT Workflow MCP',
|
||||
napcat: 'NapCatQQ Fork',
|
||||
networkAgent: 'KT Network Agent',
|
||||
palworld: 'Palworld Direct Connect PoC',
|
||||
playground: 'Playground 编辑器',
|
||||
root: 'KT 协作根目录',
|
||||
web: 'Web 前台展示',
|
||||
|
||||
77
src/core/vueTsxReview.ts
Normal file
77
src/core/vueTsxReview.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import ts from "typescript";
|
||||
|
||||
export interface ContextDependentRenderHelper {
|
||||
line: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds function-declared render helpers whose own lexical scope reads dynamic `this`.
|
||||
* Plain function declarations are valid slot callbacks; only an actual context dependency
|
||||
* justifies recommending an arrow function.
|
||||
* @param source - TSX, JSX, or Vue script source to inspect.
|
||||
* @returns Render helper names and one-based declaration lines that depend on `this`.
|
||||
*/
|
||||
export function findContextDependentVueTsxRenderHelpers(
|
||||
source: string,
|
||||
): ContextDependentRenderHelper[] {
|
||||
const sourceFile = ts.createSourceFile(
|
||||
"review.tsx",
|
||||
source,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.TSX,
|
||||
);
|
||||
const findings: ContextDependentRenderHelper[] = [];
|
||||
|
||||
/** Visits declarations while keeping each render helper's `this` owner isolated. */
|
||||
function visit(node: ts.Node): void {
|
||||
if (
|
||||
ts.isFunctionDeclaration(node) &&
|
||||
node.name &&
|
||||
/^render[A-Z0-9_]/.test(node.name.text) &&
|
||||
functionReadsDynamicThis(node)
|
||||
) {
|
||||
const position = sourceFile.getLineAndCharacterOfPosition(
|
||||
node.getStart(sourceFile),
|
||||
);
|
||||
findings.push({ line: position.line + 1, name: node.name.text });
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
|
||||
visit(sourceFile);
|
||||
return findings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks `this` owned by one function declaration, including nested arrows but excluding
|
||||
* nested functions or methods that establish their own dynamic `this`.
|
||||
* @param declaration - Render helper declaration under review.
|
||||
* @returns True when the helper's own context is read.
|
||||
*/
|
||||
function functionReadsDynamicThis(
|
||||
declaration: ts.FunctionDeclaration,
|
||||
): boolean {
|
||||
let found = false;
|
||||
|
||||
/** Walks the helper body until a different dynamic-`this` owner is reached. */
|
||||
function visit(node: ts.Node): void {
|
||||
if (found) return;
|
||||
if (node.kind === ts.SyntaxKind.ThisKeyword) {
|
||||
found = true;
|
||||
return;
|
||||
}
|
||||
if (
|
||||
node !== declaration &&
|
||||
ts.isFunctionLike(node) &&
|
||||
!ts.isArrowFunction(node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
|
||||
if (declaration.body) visit(declaration.body);
|
||||
return found;
|
||||
}
|
||||
@ -61,6 +61,7 @@ const workspaceRootPreservedNames = new Set([
|
||||
'subagents',
|
||||
'test-artifacts',
|
||||
'test-logs',
|
||||
'toolchains',
|
||||
'tmp',
|
||||
'verify',
|
||||
]);
|
||||
|
||||
@ -5,6 +5,7 @@ import type { ProjectAlias } from "../types.js";
|
||||
import type { EnvPolicyInput, RiskScanInput } from "../types.js";
|
||||
import { tryExecFile } from "../core/exec.js";
|
||||
import { detectRepoType, parseGitStatusFiles } from "../core/project.js";
|
||||
import { findContextDependentVueTsxRenderHelpers } from "../core/vueTsxReview.js";
|
||||
import {
|
||||
readTextIfExists,
|
||||
resolveProject,
|
||||
@ -275,12 +276,12 @@ export async function scanTaskRisk(
|
||||
|
||||
if (/\.(tsx|jsx|vue)$/.test(normalized)) {
|
||||
const source = readSmallFileIfExists(path.join(project.path, normalized));
|
||||
if (/\bfunction\s+render[A-Z0-9_]/.test(source)) {
|
||||
if (findContextDependentVueTsxRenderHelpers(source).length > 0) {
|
||||
addRisk(
|
||||
"medium",
|
||||
file,
|
||||
"Vue TSX render helper 使用 function 声明,容易在插槽渲染时踩 this/上下文问题。",
|
||||
"改为箭头函数,并在嵌套组件插槽中显式传 default。",
|
||||
"Vue TSX render helper 读取动态 this,插槽调用时可能丢失上下文。",
|
||||
"改为箭头函数或移除动态 this 依赖;普通无 this 的函数声明无需改写。",
|
||||
);
|
||||
}
|
||||
if (
|
||||
|
||||
@ -9,6 +9,7 @@ import type {
|
||||
} from "../types.js";
|
||||
import { tryExecFile } from "../core/exec.js";
|
||||
import { detectRepoType, parseGitStatusFiles } from "../core/project.js";
|
||||
import { findContextDependentVueTsxRenderHelpers } from "../core/vueTsxReview.js";
|
||||
import { resolveProject, toPosix, workspaceRoot } from "../core/workspace.js";
|
||||
import { scanTaskRisk } from "./envRisk.js";
|
||||
export const defaultReviewProjects = [
|
||||
@ -19,6 +20,8 @@ export const defaultReviewProjects = [
|
||||
"playground",
|
||||
"mcp",
|
||||
"napcat",
|
||||
"networkAgent",
|
||||
"palworld",
|
||||
"knife4j",
|
||||
"fnosK8s",
|
||||
] as const;
|
||||
@ -1981,21 +1984,6 @@ function collectProjectContentFindings(
|
||||
}
|
||||
|
||||
if (includeContentScan) {
|
||||
if (
|
||||
/\.(tsx|jsx|vue)$/.test(file) &&
|
||||
/\bfunction\s+render[A-Z0-9_]/.test(line)
|
||||
) {
|
||||
findings.push({
|
||||
category: "vue-tsx-render-helper",
|
||||
file,
|
||||
line: index + 1,
|
||||
level: "P2",
|
||||
message: "Vue TSX render helper 使用 function 声明。",
|
||||
project: project.relativePath,
|
||||
suggestion: "改为箭头函数,并在嵌套组件插槽中显式传 default。",
|
||||
});
|
||||
}
|
||||
|
||||
const credentialMatch = line.match(credentialAssignmentPattern);
|
||||
if (
|
||||
credentialMatch &&
|
||||
@ -2018,6 +2006,21 @@ function collectProjectContentFindings(
|
||||
});
|
||||
|
||||
if (includeContentScan) {
|
||||
if (/\.(tsx|jsx|vue)$/.test(file)) {
|
||||
findings.push(
|
||||
...findContextDependentVueTsxRenderHelpers(lines.join("\n")).map(
|
||||
(helper) => ({
|
||||
category: "vue-tsx-render-helper",
|
||||
file,
|
||||
line: helper.line,
|
||||
level: "P2" as const,
|
||||
message: `Vue TSX render helper ${helper.name} 依赖动态 this。`,
|
||||
project: project.relativePath,
|
||||
suggestion: "改为箭头函数或移除动态 this 依赖。",
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
findings.push(
|
||||
...findNapcatImageGovernanceFindings(lines, {
|
||||
file,
|
||||
|
||||
@ -6,6 +6,8 @@ export type ProjectAlias =
|
||||
| 'knife4j'
|
||||
| 'mcp'
|
||||
| 'napcat'
|
||||
| 'networkAgent'
|
||||
| 'palworld'
|
||||
| 'playground'
|
||||
| 'root'
|
||||
| 'web';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user