fix(workflow): 补充网络 Agent 审查与 TSX 规则

This commit is contained in:
sunlei 2026-07-23 00:44:29 +08:00
parent f9189d5e1e
commit d585e8706d
11 changed files with 164 additions and 22 deletions

View File

@ -145,7 +145,7 @@ Windows 上的 stdio MCP 客户端通过 `wsl.exe` 启动 Debian BashNode、
| `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 BashNode、
| `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 BashNode、
- 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`,确认文件范围和提交信息。

View File

@ -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,

View File

@ -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";

View File

@ -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",

View 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" },
]);
});

View File

@ -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
View 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;
}

View File

@ -61,6 +61,7 @@ const workspaceRootPreservedNames = new Set([
'subagents',
'test-artifacts',
'test-logs',
'toolchains',
'tmp',
'verify',
]);

View File

@ -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 (

View File

@ -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,

View File

@ -6,6 +6,8 @@ export type ProjectAlias =
| 'knife4j'
| 'mcp'
| 'napcat'
| 'networkAgent'
| 'palworld'
| 'playground'
| 'root'
| 'web';