listpage_cli 0.0.310 → 0.0.312
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/bin/adapters/cli-interaction.js +5 -7
- package/bin/app/parse-args.js +16 -7
- package/bin/cli.js +6 -14
- package/bin/commands/lark/read-command.js +9 -1
- package/bin/commands/{build-project-command.js → project/build-project-command.js} +1 -1
- package/bin/commands/{deploy-project-command.js → project/deploy-project-command.js} +4 -4
- package/bin/commands/{release-project-command.js → project/release-project-command.js} +4 -4
- package/bin/commands/project-command.js +25 -0
- package/bin/services/build-project-service.js +40 -3
- package/package.json +1 -1
- package/templates/backend-template/package.json.tmpl +1 -1
- package/templates/backend-template/tsconfig.build.json +1 -0
- package/templates/backend-template/tsconfig.json +5 -2
- package/templates/frontend-template/package.json.tmpl +9 -13
- package/templates/frontend-template/src/api/index.ts +32 -4
- package/templates/frontend-template/src/api/request-config.ts +13 -0
- package/templates/frontend-template/src/api/user.ts +2 -13
- package/templates/rush-template/.gitignore.tmpl +3 -1
- package/templates/rush-template/listpage.config.json.tmpl +4 -0
- package/templates/skills-template/feishu-prd-reviewer/SKILL.md +124 -0
- package/templates/skills-template/feishu-prd-reviewer/example.md +36 -0
- package/templates/frontend-template/src/api/config.tsx +0 -26
- package/templates/rush-template/.github/workflows/ci.yml +0 -27
- package/templates/rush-template/.trae/rules/project_rules.md +0 -25
|
@@ -160,17 +160,15 @@ function printHelp() {
|
|
|
160
160
|
` 用法: ${helpColor("listpage_cli install-skill [skillName] [--project]", "dim")}`,
|
|
161
161
|
" 说明: 安装技能到 Cursor;默认 skillName 为 test,安装到当前命令执行目录的 .cursor/skills/",
|
|
162
162
|
"",
|
|
163
|
-
` ${helpColor("
|
|
164
|
-
` 用法: ${helpColor("listpage_cli build
|
|
165
|
-
" 说明: 非交互校验当前目录是否为有效项目根(需存在 listpage.config.json
|
|
163
|
+
` ${helpColor("project", "green")}`,
|
|
164
|
+
` 用法: ${helpColor("listpage_cli project build", "dim")}`,
|
|
165
|
+
" 说明: 非交互校验当前目录是否为有效项目根(需存在 listpage.config.json),构建并写入 .listpage/output",
|
|
166
166
|
"",
|
|
167
|
-
`
|
|
168
|
-
` 用法: ${helpColor("listpage_cli release-project [tag] [--profile dev] [--platform linux/amd64] [--env KEY=VALUE]", "dim")}`,
|
|
167
|
+
` 用法: ${helpColor("listpage_cli project release [tag] [--profile dev] [--platform linux/amd64] [--env KEY=VALUE]", "dim")}`,
|
|
169
168
|
" 说明: 先校验 .listpage/output 产物,再按 login/build/tag/push 执行 Docker 发布",
|
|
170
169
|
` 备注: ${helpColor("参数优先级为 CLI > profile > base", "yellow")}`,
|
|
171
170
|
"",
|
|
172
|
-
`
|
|
173
|
-
` 用法: ${helpColor("listpage_cli deploy-project [tag] [--profile dev] [--platform linux/amd64] [--env KEY=VALUE] [--skip-image]", "dim")}`,
|
|
171
|
+
` 用法: ${helpColor("listpage_cli project deploy [tag] [--profile dev] [--platform linux/amd64] [--env KEY=VALUE] [--skip-image]", "dim")}`,
|
|
174
172
|
" 说明: 使用 docker.remote + docker.container 执行部署,支持 ports[] 与 envFile/env 合并",
|
|
175
173
|
` 备注: ${helpColor("默认会清理并拉取镜像;传入 --skip-image 时只执行容器相关步骤(假定镜像已是最新且已存在)", "yellow")}`,
|
|
176
174
|
` ${helpColor("参数优先级为 CLI > profile > base", "yellow")}`,
|
package/bin/app/parse-args.js
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cliOptionArgStartIndex = cliOptionArgStartIndex;
|
|
4
|
+
exports.parseProjectTailPositionals = parseProjectTailPositionals;
|
|
3
5
|
exports.parseArgs = parseArgs;
|
|
4
6
|
exports.parseCommandOptions = parseCommandOptions;
|
|
5
7
|
exports.parseCommandPositionals = parseCommandPositionals;
|
|
6
8
|
const KNOWN_COMMANDS = new Set([
|
|
7
9
|
"init",
|
|
8
10
|
"install-skill",
|
|
9
|
-
"
|
|
10
|
-
"release-project",
|
|
11
|
-
"deploy-project",
|
|
11
|
+
"project",
|
|
12
12
|
"lark",
|
|
13
13
|
]);
|
|
14
|
+
function cliOptionArgStartIndex(command) {
|
|
15
|
+
return 1;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* `project <sub> [tag] ...` 中,子命令之后的位置参数(如 release/deploy 的镜像 tag)。
|
|
19
|
+
*/
|
|
20
|
+
function parseProjectTailPositionals(rawArgs) {
|
|
21
|
+
return parseCommandPositionals(rawArgs, "project").slice(1);
|
|
22
|
+
}
|
|
14
23
|
function parseArgs(argv) {
|
|
15
24
|
const rawArgs = [...argv];
|
|
16
25
|
const commandToken = rawArgs[0];
|
|
@@ -41,8 +50,8 @@ function parseArgs(argv) {
|
|
|
41
50
|
positionals,
|
|
42
51
|
};
|
|
43
52
|
}
|
|
44
|
-
function parseCommandOptions(rawArgs) {
|
|
45
|
-
const args = rawArgs.slice(
|
|
53
|
+
function parseCommandOptions(rawArgs, command) {
|
|
54
|
+
const args = rawArgs.slice(cliOptionArgStartIndex(command));
|
|
46
55
|
const profile = readSingleOption(args, "profile");
|
|
47
56
|
const platform = readSingleOption(args, "platform");
|
|
48
57
|
const envEntries = readMultiOption(args, "env");
|
|
@@ -55,8 +64,8 @@ function parseCommandOptions(rawArgs) {
|
|
|
55
64
|
skipImage: skipImage || undefined,
|
|
56
65
|
};
|
|
57
66
|
}
|
|
58
|
-
function parseCommandPositionals(rawArgs) {
|
|
59
|
-
const args = rawArgs.slice(
|
|
67
|
+
function parseCommandPositionals(rawArgs, command) {
|
|
68
|
+
const args = rawArgs.slice(cliOptionArgStartIndex(command));
|
|
60
69
|
const positionals = [];
|
|
61
70
|
const optionsWithValue = new Set(["--profile", "--platform", "--env"]);
|
|
62
71
|
for (let index = 0; index < args.length; index += 1) {
|
package/bin/cli.js
CHANGED
|
@@ -6,9 +6,7 @@ const execute_1 = require("./app/execute");
|
|
|
6
6
|
const command_result_1 = require("./domain/command-result");
|
|
7
7
|
const init_command_1 = require("./commands/init-command");
|
|
8
8
|
const install_skill_command_1 = require("./commands/install-skill-command");
|
|
9
|
-
const
|
|
10
|
-
const release_project_command_1 = require("./commands/release-project-command");
|
|
11
|
-
const deploy_project_command_1 = require("./commands/deploy-project-command");
|
|
9
|
+
const project_command_1 = require("./commands/project-command");
|
|
12
10
|
const lark_command_1 = require("./commands/lark-command");
|
|
13
11
|
const node_fs_adapter_1 = require("./adapters/node-fs-adapter");
|
|
14
12
|
const filesystem_capability_service_1 = require("./services/filesystem-capability-service");
|
|
@@ -33,14 +31,10 @@ const installSkillCommandHandler = (0, install_skill_command_1.createInstallSkil
|
|
|
33
31
|
defaultSkillName: "test",
|
|
34
32
|
},
|
|
35
33
|
});
|
|
36
|
-
const
|
|
37
|
-
fs: fsAdapter,
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
fs: fsAdapter,
|
|
41
|
-
});
|
|
42
|
-
const deployProjectCommandHandler = (0, deploy_project_command_1.createDeployProjectCommandHandler)({
|
|
43
|
-
fs: fsAdapter,
|
|
34
|
+
const projectCommandHandler = (0, project_command_1.createProjectCommandHandler)({
|
|
35
|
+
build: { fs: fsAdapter },
|
|
36
|
+
release: { fs: fsAdapter },
|
|
37
|
+
deploy: { fs: fsAdapter },
|
|
44
38
|
});
|
|
45
39
|
const larkCommandHandler = (0, lark_command_1.createLarkCommandHandler)({
|
|
46
40
|
fs: fsAdapter,
|
|
@@ -52,9 +46,7 @@ async function main() {
|
|
|
52
46
|
handlers: {
|
|
53
47
|
init: initCommandHandler,
|
|
54
48
|
"install-skill": installSkillCommandHandler,
|
|
55
|
-
|
|
56
|
-
"release-project": releaseProjectCommandHandler,
|
|
57
|
-
"deploy-project": deployProjectCommandHandler,
|
|
49
|
+
project: projectCommandHandler,
|
|
58
50
|
lark: larkCommandHandler,
|
|
59
51
|
},
|
|
60
52
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.handleReadCommand = handleReadCommand;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
4
5
|
const command_result_1 = require("../../domain/command-result");
|
|
5
6
|
const parse_doc_1 = require("./parse-doc");
|
|
6
7
|
async function handleReadCommand(input, client, fs) {
|
|
@@ -23,9 +24,16 @@ async function handleReadCommand(input, client, fs) {
|
|
|
23
24
|
const relativePath = `.listpage/lark/${docToken}/prd.md`;
|
|
24
25
|
const absolutePath = fs.resolve(fs.cwd(), relativePath);
|
|
25
26
|
const outputDir = fs.dirname(absolutePath);
|
|
26
|
-
|
|
27
|
+
try {
|
|
28
|
+
if ((0, node_fs_1.existsSync)(outputDir)) {
|
|
29
|
+
(0, node_fs_1.rmSync)(outputDir, { recursive: true, force: true });
|
|
30
|
+
}
|
|
27
31
|
fs.ensureDir(outputDir);
|
|
28
32
|
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
35
|
+
return (0, command_result_1.commandError)(`清理旧文档产物失败: ${message}`, "cleanup_lark_doc_dir_failed", 1);
|
|
36
|
+
}
|
|
29
37
|
const docContent = await (0, parse_doc_1.parseBlocksToMarkdown)(blocks, {
|
|
30
38
|
fs,
|
|
31
39
|
outputDir,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createBuildProjectCommandHandler = createBuildProjectCommandHandler;
|
|
4
|
-
const build_project_service_1 = require("
|
|
4
|
+
const build_project_service_1 = require("../../services/build-project-service");
|
|
5
5
|
function createBuildProjectCommandHandler(deps) {
|
|
6
6
|
return async (_input) => {
|
|
7
7
|
return (0, build_project_service_1.runBuildProjectFlow)(deps);
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createDeployProjectCommandHandler = createDeployProjectCommandHandler;
|
|
4
|
-
const parse_args_1 = require("
|
|
5
|
-
const deploy_project_service_1 = require("
|
|
4
|
+
const parse_args_1 = require("../../app/parse-args");
|
|
5
|
+
const deploy_project_service_1 = require("../../services/deploy-project-service");
|
|
6
6
|
function createDeployProjectCommandHandler(deps) {
|
|
7
7
|
return async (input) => {
|
|
8
|
-
const firstPositional = (0, parse_args_1.
|
|
8
|
+
const firstPositional = (0, parse_args_1.parseProjectTailPositionals)(input.rawArgs)[0];
|
|
9
9
|
const tag = typeof firstPositional === "string" && firstPositional.trim() !== ""
|
|
10
10
|
? firstPositional.trim()
|
|
11
11
|
: undefined;
|
|
12
|
-
const options = (0, parse_args_1.parseCommandOptions)(input.rawArgs);
|
|
12
|
+
const options = (0, parse_args_1.parseCommandOptions)(input.rawArgs, "project");
|
|
13
13
|
const cliOverrides = {
|
|
14
14
|
tag,
|
|
15
15
|
profile: options.profile,
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createReleaseProjectCommandHandler = createReleaseProjectCommandHandler;
|
|
4
|
-
const parse_args_1 = require("
|
|
5
|
-
const release_project_service_1 = require("
|
|
4
|
+
const parse_args_1 = require("../../app/parse-args");
|
|
5
|
+
const release_project_service_1 = require("../../services/release-project-service");
|
|
6
6
|
function createReleaseProjectCommandHandler(deps) {
|
|
7
7
|
return async (input) => {
|
|
8
|
-
const firstPositional = (0, parse_args_1.
|
|
8
|
+
const firstPositional = (0, parse_args_1.parseProjectTailPositionals)(input.rawArgs)[0];
|
|
9
9
|
const tag = typeof firstPositional === "string" && firstPositional.trim() !== ""
|
|
10
10
|
? firstPositional.trim()
|
|
11
11
|
: undefined;
|
|
12
|
-
const options = (0, parse_args_1.parseCommandOptions)(input.rawArgs);
|
|
12
|
+
const options = (0, parse_args_1.parseCommandOptions)(input.rawArgs, "project");
|
|
13
13
|
const cliOverrides = {
|
|
14
14
|
tag,
|
|
15
15
|
profile: options.profile,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createProjectCommandHandler = createProjectCommandHandler;
|
|
4
|
+
const command_result_1 = require("../domain/command-result");
|
|
5
|
+
const build_project_command_1 = require("./project/build-project-command");
|
|
6
|
+
const release_project_command_1 = require("./project/release-project-command");
|
|
7
|
+
const deploy_project_command_1 = require("./project/deploy-project-command");
|
|
8
|
+
function createProjectCommandHandler(deps) {
|
|
9
|
+
const build = (0, build_project_command_1.createBuildProjectCommandHandler)(deps.build);
|
|
10
|
+
const release = (0, release_project_command_1.createReleaseProjectCommandHandler)(deps.release);
|
|
11
|
+
const deploy = (0, deploy_project_command_1.createDeployProjectCommandHandler)(deps.deploy);
|
|
12
|
+
return async (input) => {
|
|
13
|
+
const subCommand = input.positionals[0];
|
|
14
|
+
switch (subCommand) {
|
|
15
|
+
case "build":
|
|
16
|
+
return build(input);
|
|
17
|
+
case "release":
|
|
18
|
+
return release(input);
|
|
19
|
+
case "deploy":
|
|
20
|
+
return deploy(input);
|
|
21
|
+
default:
|
|
22
|
+
return (0, command_result_1.commandError)("错误: 未知的子命令。目前仅支持: build, release, deploy", "invalid_subcommand", 1);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -31,6 +31,13 @@ async function runBuildProjectFlow(deps) {
|
|
|
31
31
|
const message = error instanceof Error ? error.message : String(error);
|
|
32
32
|
return (0, command_result_1.commandError)(message, command_result_1.COMMAND_ERROR_CODES.invalidPath, 1);
|
|
33
33
|
}
|
|
34
|
+
try {
|
|
35
|
+
removeOutputDirBeforeBuild(projectRoot);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
39
|
+
return (0, command_result_1.commandError)(`[${command_runner_1.COMMAND_RUNNER_STAGE}] 清理输出目录失败: ${message}`, command_result_1.COMMAND_ERROR_CODES.executionFailed, 1);
|
|
40
|
+
}
|
|
34
41
|
if (deps.executeBuild) {
|
|
35
42
|
try {
|
|
36
43
|
const result = await deps.executeBuild({
|
|
@@ -76,13 +83,36 @@ function getBuildTargets(projectRoot, config) {
|
|
|
76
83
|
projectDir: resolveBuildTarget(projectRoot, item.projectDir),
|
|
77
84
|
}));
|
|
78
85
|
}
|
|
86
|
+
function resolveArtifactsBuildSlice(config) {
|
|
87
|
+
const artifacts = config.artifacts;
|
|
88
|
+
if (artifacts && typeof artifacts === "object" && !Array.isArray(artifacts)) {
|
|
89
|
+
const a = artifacts;
|
|
90
|
+
return {
|
|
91
|
+
frontend: a.frontend ?? config.frontend,
|
|
92
|
+
backend: a.backend ?? config.backend,
|
|
93
|
+
copyFiles: a.copyFiles ?? config.copyFiles,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
frontend: config.frontend,
|
|
98
|
+
backend: config.backend,
|
|
99
|
+
copyFiles: config.copyFiles,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
79
102
|
function toBuildProjectConfig(config) {
|
|
80
|
-
const
|
|
81
|
-
const
|
|
103
|
+
const slice = resolveArtifactsBuildSlice(config);
|
|
104
|
+
const frontendRaw = slice.frontend;
|
|
105
|
+
if (!Array.isArray(frontendRaw) || frontendRaw.length === 0) {
|
|
106
|
+
throw new Error("配置字段无效: frontend (至少包含一个构建项)");
|
|
107
|
+
}
|
|
108
|
+
const backendRaw = slice.backend;
|
|
109
|
+
if (!backendRaw || typeof backendRaw !== "object" || Array.isArray(backendRaw)) {
|
|
110
|
+
throw new Error("配置字段无效: backend");
|
|
111
|
+
}
|
|
82
112
|
return {
|
|
83
113
|
frontend: frontendRaw.map((item) => toBuildTarget(item)),
|
|
84
114
|
backend: toBuildTarget(backendRaw),
|
|
85
|
-
copyFiles: toCopySpecs(
|
|
115
|
+
copyFiles: toCopySpecs(slice.copyFiles),
|
|
86
116
|
};
|
|
87
117
|
}
|
|
88
118
|
function toBuildTarget(item) {
|
|
@@ -115,6 +145,13 @@ function resolveBuildTarget(projectRoot, projectDir) {
|
|
|
115
145
|
? projectDir
|
|
116
146
|
: node_path_1.default.resolve(projectRoot, projectDir);
|
|
117
147
|
}
|
|
148
|
+
/** 构建前删除 `.listpage/output`,避免遗留旧产物。 */
|
|
149
|
+
function removeOutputDirBeforeBuild(projectRoot) {
|
|
150
|
+
const outputRoot = node_path_1.default.resolve(projectRoot, ".listpage", "output");
|
|
151
|
+
if ((0, node_fs_1.existsSync)(outputRoot)) {
|
|
152
|
+
(0, node_fs_1.rmSync)(outputRoot, { recursive: true, force: true });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
118
155
|
function toCopySpecs(value) {
|
|
119
156
|
if (!Array.isArray(value)) {
|
|
120
157
|
return [];
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"moduleResolution": "nodenext",
|
|
5
5
|
"resolvePackageJsonExports": true,
|
|
6
6
|
"esModuleInterop": true,
|
|
7
|
-
"isolatedModules":
|
|
7
|
+
"isolatedModules": false,
|
|
8
8
|
"declaration": true,
|
|
9
9
|
"removeComments": true,
|
|
10
10
|
"emitDecoratorMetadata": true,
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
"forceConsistentCasingInFileNames": true,
|
|
21
21
|
"noImplicitAny": false,
|
|
22
22
|
"strictBindCallApply": false,
|
|
23
|
-
"noFallthroughCasesInSwitch": false
|
|
23
|
+
"noFallthroughCasesInSwitch": false,
|
|
24
|
+
"paths": {
|
|
25
|
+
"exclude": ["node_modules", "dist"]
|
|
26
|
+
}
|
|
24
27
|
}
|
|
25
28
|
}
|
|
@@ -10,22 +10,18 @@
|
|
|
10
10
|
"preview": "rsbuild preview"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"react-router-dom": ">=6.0.0",
|
|
17
|
-
"@ant-design/v5-patch-for-react-19": "~1.0.3",
|
|
13
|
+
"listpage-http": "0.0.312",
|
|
14
|
+
"listpage-components": "0.0.312",
|
|
15
|
+
"antd": "6.3.1",
|
|
18
16
|
"ahooks": "^3.9.5",
|
|
19
|
-
"
|
|
20
|
-
"axios": "^1.12.2",
|
|
17
|
+
"@ant-design/icons": "~6.0.2",
|
|
21
18
|
"dayjs": "^1.11.18",
|
|
22
19
|
"lodash": "^4.17.21",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"mobx-react-lite": "~4.1.1"
|
|
20
|
+
"lucide-react": "~0.575.0",
|
|
21
|
+
"recharts": "^3.7.0",
|
|
22
|
+
"react": "^19.2.0",
|
|
23
|
+
"react-dom": "^19.2.0",
|
|
24
|
+
"react-router-dom": ">=6.0.0"
|
|
29
25
|
},
|
|
30
26
|
"devDependencies": {
|
|
31
27
|
"@rsbuild/core": "^1.6.0",
|
|
@@ -1,5 +1,33 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createApiClient } from 'listpage-http';
|
|
2
|
+
import { requestConfig, type RequestConfig } from './request-config';
|
|
3
|
+
import { TOKEN_STORAGE_KEY } from '@/constants/auth';
|
|
4
|
+
import { notification } from 'antd';
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
+
const isDev = process.env.NODE_ENV === 'development';
|
|
7
|
+
|
|
8
|
+
const api = createApiClient<RequestConfig>(requestConfig, {
|
|
9
|
+
baseURL: '/api/v1',
|
|
10
|
+
tokenKey: TOKEN_STORAGE_KEY,
|
|
11
|
+
defaultTimeout: 10_000,
|
|
12
|
+
successCodes: [20000, 200, 201],
|
|
13
|
+
unauthorizedCodes: [401],
|
|
14
|
+
unauthorizedRedirectPath: '/login',
|
|
15
|
+
server: {
|
|
16
|
+
protocol: undefined,
|
|
17
|
+
host: isDev ? 'localhost' : undefined,
|
|
18
|
+
port: isDev ? 3000 : undefined,
|
|
19
|
+
publicPath: '/api/v1',
|
|
20
|
+
},
|
|
21
|
+
onError: (code, msg) => {
|
|
22
|
+
// 统一错误处理
|
|
23
|
+
console.error('[API Error]', code, msg);
|
|
24
|
+
notification.open({
|
|
25
|
+
title: String(code),
|
|
26
|
+
description: msg || `请求失败 (${code})`,
|
|
27
|
+
placement: 'topRight',
|
|
28
|
+
type: 'error',
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export default api;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineEndpoint } from 'listpage-http';
|
|
2
|
+
import type { ReqLogin, ResLogin } from './user';
|
|
3
|
+
|
|
4
|
+
export const requestConfig = {
|
|
5
|
+
user: {
|
|
6
|
+
login: defineEndpoint<ReqLogin, ResLogin>({
|
|
7
|
+
method: 'POST',
|
|
8
|
+
path: '/auth/login',
|
|
9
|
+
}),
|
|
10
|
+
},
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
export type RequestConfig = typeof requestConfig;
|
|
@@ -1,19 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
type ReqLogin = {
|
|
1
|
+
export type ReqLogin = {
|
|
4
2
|
email: string;
|
|
5
3
|
password: string;
|
|
6
4
|
};
|
|
7
5
|
|
|
8
|
-
type ResLogin = {
|
|
6
|
+
export type ResLogin = {
|
|
9
7
|
token: string;
|
|
10
8
|
};
|
|
11
|
-
|
|
12
|
-
const login = async (values: ReqLogin) => {
|
|
13
|
-
const response = await apiClient.post<ResLogin>('/auth/login', values);
|
|
14
|
-
return response.data.data!;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export default {
|
|
18
|
-
login,
|
|
19
|
-
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: feishu-prd-reviewer
|
|
3
|
+
description: 评审飞书 PRD(支持评审后结合待办反向澄清需求)
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Feishu PRD Reviewer(产品视角)
|
|
7
|
+
|
|
8
|
+
## 触发条件
|
|
9
|
+
|
|
10
|
+
- 用户要求处理该飞书 PRD(评审或补齐待办),并明确提供文档地址(URL)或 `doc_token`
|
|
11
|
+
- 通过 `npx listpage_cli lark read <doc_token>` 能成功读取到文档内容
|
|
12
|
+
|
|
13
|
+
## 输入
|
|
14
|
+
|
|
15
|
+
- 飞书 PRD URL(用于解析 `doc_token`)
|
|
16
|
+
- 或用户直接提供 `doc_token`
|
|
17
|
+
|
|
18
|
+
## 输出
|
|
19
|
+
|
|
20
|
+
- PRD 含待办:结合“AI 评审章节 + 待办问题”逐条处理待办,更新需求清单描述并回写对应区块、将待办标记为 done;随后结束
|
|
21
|
+
- PRD 不含待办:生成产品经理易读的评审结论并回写区块;随后结束
|
|
22
|
+
|
|
23
|
+
## 核心原则
|
|
24
|
+
|
|
25
|
+
- **互斥执行**:一次仅执行一个模式。
|
|
26
|
+
- 有待办:只做“待办处理”,不做任何新的需求评审与结论生成(但可读取既有 AI 评审章节作为待办处理输入)。
|
|
27
|
+
- 无待办:只做“需求评审”,不进行待办处理流程(不创建 requirements.md)。
|
|
28
|
+
- **待办口径**:以“未完成待办”为准;若文档中的待办区块全部为 done(或没有待办条目),视为“无待办”。
|
|
29
|
+
- 产品视角优先:以“用户侧影响、业务闭环、风险与待确认点”为主,避免晦涩技术术语堆砌
|
|
30
|
+
- 与代码对齐:结合仓库现有实现推断隐含约束与异常场景,补齐开发需要的澄清点
|
|
31
|
+
- 约束强执行:待办处理阶段严格遵循标题层级与提示语移除规则(见“待办处理约束”)
|
|
32
|
+
|
|
33
|
+
## 执行流程
|
|
34
|
+
|
|
35
|
+
1. 获取 `doc_token`:从 URL 提取,或直接使用用户提供的 token
|
|
36
|
+
2. 读取需求文档:在项目根目录执行
|
|
37
|
+
- `npx listpage_cli lark read <doc_token>`
|
|
38
|
+
- 该命令会把内容写入 `.listpage/lark/<doc_token>/prd.md`,并输出文件路径
|
|
39
|
+
3. 判断待办是否存在
|
|
40
|
+
- 无待办:指“未完成待办”数量为 0(包括:文档没有待办、或所有待办均已标记 done)-> 进入步骤 6(代码评审 + 结论生成)
|
|
41
|
+
- 有待办:存在未完成待办 -> 进入步骤 4(待办逐条处理)
|
|
42
|
+
4. 待办逐条处理(有待办时不进入新的代码评审与结论生成)
|
|
43
|
+
- 创建新文档
|
|
44
|
+
- 文件路径示例:`.listpage/lark/<doc_token>/requirements.md`
|
|
45
|
+
- 标题约束:严禁使用 H1/H2,所有标题必须从 H3 开始
|
|
46
|
+
- 内容约束:移除提示语“下面部分由产品经理编写,内容为准备评审的需求点”
|
|
47
|
+
- 内容来源:以原文档中“需求清单”作为基底,并结合“AI 评审章节(如 robot_face 回写的评审结论)”与待办问题进行澄清改写;去掉“需求清单”标题与相关提示语
|
|
48
|
+
- 循环处理每条待办
|
|
49
|
+
1. 读取待办事项内容
|
|
50
|
+
2. 同步读取对应需求点的 AI 评审内容(需求描述/问题/可行性/风险点)
|
|
51
|
+
3. 以“待办问题 + AI 评审结论”共同作为输入,改写 `requirements.md` 中对应需求点,让需求描述更清晰、可执行
|
|
52
|
+
4. 如 AI 评审与待办口径冲突,以“待办明确的新口径”优先,并在需求描述中消除歧义
|
|
53
|
+
5. 回写时只更新需求清单内容,不改变既定回写命令和回写目标区块
|
|
54
|
+
6. 直到所有待办处理完成
|
|
55
|
+
- 回写飞书文档(待办全部完成后)
|
|
56
|
+
- 执行:`npx listpage_cli lark update-block <doc_token> woman "<markdown_file_path>"`
|
|
57
|
+
- 更新待办状态
|
|
58
|
+
- 执行:`npx listpage_cli lark check-todo <doc_token> <index>`
|
|
59
|
+
- 将待办标记为完成(done)
|
|
60
|
+
5. 流程结束:待办处理完成后停止,不再执行后续步骤
|
|
61
|
+
6. 代码评审与结论生成(无待办时执行)
|
|
62
|
+
|
|
63
|
+
- 目标:先把“需求点列表”固定到 `review.md`,再对每条需求点按序逐条评审并回写,避免一次性把所有需求点塞进上下文导致爆炸
|
|
64
|
+
- 阶段 A:需求点提炼并写入 `review.md`(仅拆条,不写评审)
|
|
65
|
+
- 从步骤 2 读取到的 PRD 内容中,提取“需求清单”区段的全部需求点(包含图片/补充信息的隐含诉求)
|
|
66
|
+
- 将需求拆成 N 条“可执行条目”,为每条生成稳定编号(例如 `i=1..N`)
|
|
67
|
+
- 在本地创建/覆盖 Markdown:`.listpage/lark/<doc_token>/review.md`
|
|
68
|
+
- 在 `review.md` 顶部添加一个“执行待办清单”(仅用于阶段 B 驱动逐条处理)
|
|
69
|
+
- 格式示例(必须是 Markdown checklist):`- [ ] 需求点 1`
|
|
70
|
+
- 对每条生成一行:`- [ ] 需求点 i`
|
|
71
|
+
- 阶段 A 不把任何条目标为已完成
|
|
72
|
+
- 每条使用固定区块边界,格式示例:`### 需求点 i`(注意:仅要求禁止 H1/H2,H3 允许)
|
|
73
|
+
- 仅在每个 `### 需求点 i` 区块内写“需求描述”,并严格复用 `example.md` 的字段与缩进/子 bullet 结构(产品视角)
|
|
74
|
+
- 阶段 A 不写“问题/可行性/风险点”,只负责把每个需求点的“需求描述”结构固定下来,避免一次性输出过多评审内容导致上下文爆炸
|
|
75
|
+
- 格式约束:严禁使用 H1/H2 标题或表格;允许使用列表与加粗字段
|
|
76
|
+
- 阶段 B:逐条评审并回写 `review.md`(按序、只处理一条)
|
|
77
|
+
- 对 i = 1..N 循环执行,直到全部条目完成
|
|
78
|
+
1. 只读取 `review.md` 中两处内容:
|
|
79
|
+
- 执行待办清单里第 i 行(`- [ ] 需求点 i` 或 `- [x] 需求点 i`)
|
|
80
|
+
- `### 需求点 i` 对应的区块(不要把其它条目的内容也纳入本轮处理)
|
|
81
|
+
2. 只基于 PRD 需求本身进行判断,不做“结合代码去猜口径/猜实现”的推断;并且收紧“问题”触发条件:
|
|
82
|
+
- 只有当 PRD 信息不足以形成“开发可执行的需求描述”(关键落点/口径/范围/汇总规则无法确定)时,才输出 `**问题:**`(不写 `可行性/风险点`)
|
|
83
|
+
- 如果基于该需求点本身已足够做出“可以实现”的判断(即使仍存在非关键细节,也不阻塞落地),则不要问问题:直接输出 `**可行性:**`(成本等级 + 精简的必要说明)
|
|
84
|
+
3. 将输出严格复用 `example.md` 的区块结构,并回写到对应 `### 需求点 i` 区块,保持其它区块不变:
|
|
85
|
+
- 不新增其它字段/标题/正文块
|
|
86
|
+
- 仅按需要填写 `需求描述` / `问题` / `可行性` / `风险点`(这些字段与缩进/子 bullet 结构必须与 `example.md` 一致)
|
|
87
|
+
- 不允许出现代码/组件/函数/文件名;不要根据代码猜口径(需求不清晰就只提问题)
|
|
88
|
+
4. 把执行待办清单里第 i 行从 `- [ ] 需求点 i` 更新为 `- [x] 需求点 i`(作为“本条已完成”的唯一状态)
|
|
89
|
+
- 阶段 C:最终回写飞书
|
|
90
|
+
- 等循环全部完成后,在本地生成“回写版”文件:`review_writeback.md`
|
|
91
|
+
- `review_writeback.md` 最顶部新增 `### 评审人` 标题,并下一行写 `<当前模型名称>`(格式与 `example.md` 一致)
|
|
92
|
+
- 该文件同时保留 `### 评审人` 区块与所有 `### 需求点 i` 区块
|
|
93
|
+
- 必须删除顶部的“执行待办清单”(`- [ ]/- [x] 需求点 i` 那一段及其所在行),确保飞书里不出现 checklist 待办格式
|
|
94
|
+
- 执行飞书回写时,仅使用 `review_writeback.md`:
|
|
95
|
+
- `npx listpage_cli lark update-block <doc_token> robot_face "review_writeback.md"`
|
|
96
|
+
|
|
97
|
+
## 评审方法(无待办时)
|
|
98
|
+
|
|
99
|
+
(无待办时执行第 6 步的“阶段 A/B/C”,本节仅保留总体输出原则)
|
|
100
|
+
|
|
101
|
+
- 需求点提炼:从“需求清单”区段理解全部需要落地的需求点(包括图片描述与补充信息中的隐含诉求),并拆成可逐条评审的 N 条条目
|
|
102
|
+
- 输出模板:每个 `### 需求点 i` 区块的呈现结构必须严格复用 `example.md`
|
|
103
|
+
- `- **需求描述:**:` 必须出现
|
|
104
|
+
- 仅当信息缺口会阻塞落地:才出现 `- **问题:**:`(此时不出现 `**可行性:**` 与 `**风险点:**`)
|
|
105
|
+
- 其余情况(能实现/不阻塞落地):出现 `- **可行性:**:`(可追加 `- **风险点:**:`,仅影响范围大时)
|
|
106
|
+
- 澄清优先(但不泛问):不阻塞落地时不提问;不做口径猜测
|
|
107
|
+
- 禁止代码猜测:不引用仓库代码/组件/函数/文件名;也不要根据代码推断不清晰的口径(应改为提问)
|
|
108
|
+
|
|
109
|
+
## 写回文档(无待办时)
|
|
110
|
+
|
|
111
|
+
- 本地写回:通过第 6 步阶段 C 生成 `review_writeback.md`
|
|
112
|
+
- 格式要求:严禁使用(H1-H2)标题或表格;只保留 `### 评审人` 与所有 `### 需求点 i` 区块
|
|
113
|
+
- 同步飞书:执行 `npx listpage_cli lark update-block <doc_token> robot_face "review_writeback.md"`
|
|
114
|
+
|
|
115
|
+
## 待办处理约束(强制)
|
|
116
|
+
|
|
117
|
+
- 新建 requirements.md 文档禁止 H1/H2,标题从 H3 开始
|
|
118
|
+
- 必须移除指定提示语,并去掉“需求清单”标题本身
|
|
119
|
+
- 必须把“AI 评审章节 + 待办问题”合并后再更新需求清单,不可仅依据待办原文机械改写
|
|
120
|
+
|
|
121
|
+
## 成功标准
|
|
122
|
+
|
|
123
|
+
- 有待办:所有待办均回写并标记 done;已基于“AI 评审章节 + 待办问题”完成需求清单澄清更新;仅完成待办处理相关的文档更新;不生成新的需求评审结论(也不创建新的 review.md)
|
|
124
|
+
- 无待办:生成的评审结论回写成功;且回写内容满足“无 H1-H2 标题,无表格”的格式要求
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
### 评审人
|
|
2
|
+
|
|
3
|
+
模型名称
|
|
4
|
+
|
|
5
|
+
### 需求点 1
|
|
6
|
+
|
|
7
|
+
- **需求描述:**: (一句话概括要做什么,产品视角)
|
|
8
|
+
- (关键点 1)
|
|
9
|
+
- (关键点 2)
|
|
10
|
+
|
|
11
|
+
### 需求点 2
|
|
12
|
+
|
|
13
|
+
- **需求描述:**: (一句话概括要做什么,产品视角)
|
|
14
|
+
|
|
15
|
+
- **问题:**
|
|
16
|
+
- (问题 1:必须澄清“页面/表/口径/范围/汇总规则”等)
|
|
17
|
+
- (问题 2)
|
|
18
|
+
- (问题 3)
|
|
19
|
+
|
|
20
|
+
### 需求点 3
|
|
21
|
+
|
|
22
|
+
- **需求描述:**: (一句话概括要做什么,产品视角)
|
|
23
|
+
|
|
24
|
+
- **可行性:**
|
|
25
|
+
- 成本:低/中/高
|
|
26
|
+
- (在不引入猜测的前提下,说明成本对应的业务/口径/环节调整)
|
|
27
|
+
|
|
28
|
+
### 需求点 4
|
|
29
|
+
|
|
30
|
+
- **需求描述:**: (一句话概括要做什么,产品视角)
|
|
31
|
+
- **可行性:**
|
|
32
|
+
- 成本:中/高
|
|
33
|
+
- (在不引入猜测的前提下,说明成本对应的业务/口径/环节调整)
|
|
34
|
+
- **风险点:**
|
|
35
|
+
- (风险 1:可能影响的不止是展示文案,例如统计口径/汇总逻辑/跨模块联动等)
|
|
36
|
+
- (风险 2)
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { HttpClient } from 'listpage-next';
|
|
2
|
-
|
|
3
|
-
const isDev = process.env.NODE_ENV === 'development';
|
|
4
|
-
|
|
5
|
-
// 创建axios实例
|
|
6
|
-
const apiClient = new HttpClient({
|
|
7
|
-
token: 'admin',
|
|
8
|
-
tokenKey: '',
|
|
9
|
-
successCodes: [20000, 200],
|
|
10
|
-
server: {
|
|
11
|
-
protocol: undefined,
|
|
12
|
-
host: isDev ? 'localhost' : undefined,
|
|
13
|
-
port: isDev ? '3000' : undefined,
|
|
14
|
-
publicPath: '/api/v1',
|
|
15
|
-
},
|
|
16
|
-
config: {
|
|
17
|
-
timeout: 10000,
|
|
18
|
-
headers: {
|
|
19
|
-
'Content-Type': 'application/json',
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
apiClient.setup();
|
|
25
|
-
|
|
26
|
-
export default apiClient;
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
on:
|
|
3
|
-
push:
|
|
4
|
-
branches: [ "main" ]
|
|
5
|
-
pull_request:
|
|
6
|
-
branches: [ "main" ]
|
|
7
|
-
jobs:
|
|
8
|
-
build:
|
|
9
|
-
runs-on: ubuntu-latest
|
|
10
|
-
steps:
|
|
11
|
-
- uses: actions/checkout@v3
|
|
12
|
-
with:
|
|
13
|
-
fetch-depth: 2
|
|
14
|
-
- name: Git config user
|
|
15
|
-
uses: snow-actions/git-config-user@v1.0.0
|
|
16
|
-
with:
|
|
17
|
-
name: # Service Account's Name
|
|
18
|
-
email: # Service Account's Email Address
|
|
19
|
-
- uses: actions/setup-node@v3
|
|
20
|
-
with:
|
|
21
|
-
node-version: 16
|
|
22
|
-
- name: Verify Change Logs
|
|
23
|
-
run: node common/scripts/install-run-rush.js change --verify
|
|
24
|
-
- name: Rush Install
|
|
25
|
-
run: node common/scripts/install-run-rush.js install
|
|
26
|
-
- name: Rush rebuild
|
|
27
|
-
run: node common/scripts/install-run-rush.js rebuild --verbose --production
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# 项目规则
|
|
2
|
-
|
|
3
|
-
## 必须遵守的规则
|
|
4
|
-
|
|
5
|
-
### 技术方案编写规则
|
|
6
|
-
|
|
7
|
-
1. 存放目录: .trae/documents/ 下面。
|
|
8
|
-
2. 文件名称: xxx 技术方案.md
|
|
9
|
-
|
|
10
|
-
### 前端开发的规则
|
|
11
|
-
|
|
12
|
-
1. 前端组件导出必须使用具名导出,禁止 export default 的写法!!!
|
|
13
|
-
2. 当使用 ListPage 组件时,必须遵守项目内容文档:[ListPage-AI 生成规范](docs/ListPage-AI生成规范.md)。
|
|
14
|
-
3. 禁止使用 React.FC 来定义组件,组件的类型应该是 `(props: Props) => JSX.Element`, 不用显式声明。
|
|
15
|
-
4. 前端接口维护在 `api` 目录下, 所有的接口从 `api/index.ts` 中导出,严格遵守现在`api`目录的结构,并在需要的使用的时候从 `api/index.ts` 中引入
|
|
16
|
-
|
|
17
|
-
### 后端开发的规则
|
|
18
|
-
|
|
19
|
-
1. 后端接口如无特殊要求均采用 POST 请求
|
|
20
|
-
2. 对于列表查询的,默认请求体一定包含 current, pageSize 两个参数
|
|
21
|
-
3. 后端接口返回的格式要求如下:
|
|
22
|
-
- 失败时,直接抛出异常,由拦截器自行处理,不用管
|
|
23
|
-
- 成功时:
|
|
24
|
-
- 常规数据的格式:{ code: 0, data: {} },控制器返回的数据直接就是 data,不需要再包一层
|
|
25
|
-
- 列表数据的格式:{ code: 0, data: { list: [], total: 0, current: 1, pageSize: 10 } },控制器返回的数据直接就是 data,不需要再包一层
|