@ats-cx/cx-cli 0.1.0
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/.env.example +8 -0
- package/README.md +119 -0
- package/bin/cx-cli.cjs +90 -0
- package/config/apm-provider.json +7 -0
- package/config/apm-provider.template.jsonc +48 -0
- package/config/diagnostic-rules.json +54 -0
- package/config/diagnostic-rules.template.jsonc +82 -0
- package/config/event-semantics.json +828 -0
- package/config/event-semantics.template.jsonc +13 -0
- package/config/toolkit.json +3 -0
- package/dist/apm-help.d.ts +5 -0
- package/dist/apm-help.js +108 -0
- package/dist/apm-output.d.ts +49 -0
- package/dist/apm-output.js +115 -0
- package/dist/budget.d.ts +14 -0
- package/dist/budget.js +16 -0
- package/dist/cli.d.ts +20 -0
- package/dist/cli.js +596 -0
- package/dist/context.d.ts +12 -0
- package/dist/context.js +20 -0
- package/dist/contract.d.ts +11 -0
- package/dist/contract.js +4 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +8 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +8 -0
- package/dist/init.d.ts +17 -0
- package/dist/init.js +194 -0
- package/dist/output.d.ts +7 -0
- package/dist/output.js +7 -0
- package/dist/project-pull.d.ts +32 -0
- package/dist/project-pull.js +81 -0
- package/dist/report.d.ts +3 -0
- package/dist/report.js +42 -0
- package/dist/skills.d.ts +12 -0
- package/dist/skills.js +430 -0
- package/dist/tools/apm-tools.d.ts +115 -0
- package/dist/tools/apm-tools.js +329 -0
- package/dist/tools/conclusion-tools.d.ts +24 -0
- package/dist/tools/conclusion-tools.js +61 -0
- package/dist/tools/create-run.d.ts +61 -0
- package/dist/tools/create-run.js +273 -0
- package/dist/tools/log-tools.d.ts +68 -0
- package/dist/tools/log-tools.js +116 -0
- package/dist/tools/project-tools.d.ts +82 -0
- package/dist/tools/project-tools.js +139 -0
- package/dist/workspace.d.ts +51 -0
- package/dist/workspace.js +104 -0
- package/package.json +37 -0
- package/skills/apm-query/SKILL.md +304 -0
- package/skills/apm-query/agents/openai.yaml +10 -0
- package/skills/cx-cli-setup/SKILL.md +120 -0
- package/skills/cx-cli-setup/agents/openai.yaml +11 -0
- package/skills/editor-diagnostic/SKILL.md +255 -0
- package/skills/editor-diagnostic/agents/openai.yaml +13 -0
- package/skills/semantics-curation/SKILL.md +151 -0
- package/skills/semantics-curation/agents/openai.yaml +12 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ConfigSource, ResolvedConfig, SourceRepoBinding } from "@ats-cx/cx-core";
|
|
2
|
+
/** 诊断工作区目录名,建在编辑器源码仓库根下。 */
|
|
3
|
+
export declare const WORKSPACE_DIR_NAME = ".cx-cli";
|
|
4
|
+
/** 工作区语义表文件名:工作区模式下的唯一语义来源(spec 组件 D)。 */
|
|
5
|
+
export declare const WORKSPACE_SEMANTICS_FILE = "event-semantics.json";
|
|
6
|
+
/**
|
|
7
|
+
* 工作区 config.json 契约。相对路径相对 .cx-cli/ 解析(与 config/ 目录同一约定)。
|
|
8
|
+
* toolkitConfigDir 可选:省略即运行中 cx-cli 自身的 config/;显式路径钉住一个完整配置组
|
|
9
|
+
* (ADR 0001 修订,见 ADR 0004)。
|
|
10
|
+
*/
|
|
11
|
+
export interface WorkspaceConfig {
|
|
12
|
+
toolkitConfigDir?: string;
|
|
13
|
+
runsDir: string;
|
|
14
|
+
projectHistoryDir: string;
|
|
15
|
+
outDir: string;
|
|
16
|
+
}
|
|
17
|
+
/** 发现并解析后的工作区:所有路径均为绝对路径。 */
|
|
18
|
+
export interface Workspace {
|
|
19
|
+
/** 编辑器源码仓库根(.cx-cli 所在目录)。 */
|
|
20
|
+
hostRoot: string;
|
|
21
|
+
workspaceDir: string;
|
|
22
|
+
/** null 表示使用运行中 cx-cli 自身的 config/。 */
|
|
23
|
+
toolkitConfigDir: string | null;
|
|
24
|
+
runsDir: string;
|
|
25
|
+
projectHistoryDir: string;
|
|
26
|
+
outDir: string;
|
|
27
|
+
/** 工作区 .env 路径(不保证存在;project pull 在其存在时才优先用)。 */
|
|
28
|
+
envPath: string;
|
|
29
|
+
/** 工作区语义表路径;工作区模式下语义只读它,不回落工具箱种子表。 */
|
|
30
|
+
semanticsPath: string;
|
|
31
|
+
/** 三仓绑定:按 HOST_SOURCE_BINDINGS 在宿主仓库根下合成,是运行时唯一来源(无配置层)。 */
|
|
32
|
+
sourceRepos: SourceRepoBinding[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 从 cwd 向上找 .cx-cli/ 工作区(类 .git 发现),到文件系统根为止。
|
|
36
|
+
* 命中但 config.json 缺失/损坏视为工作区损坏,直接报错——
|
|
37
|
+
* 静默回退会把产物写到别处,比报错更难排查。
|
|
38
|
+
*/
|
|
39
|
+
export declare function discoverWorkspace(cwd: string): Workspace | null;
|
|
40
|
+
/**
|
|
41
|
+
* 工作区模式即真实诊断路径(fixture 自测走显式 --config,那时 workspace 为 null)。
|
|
42
|
+
* 此时日志源仍指向本地文件,说明工具箱 config/ 被自测配置污染——logSource 是唯一
|
|
43
|
+
* 不被工作区覆盖、却能左右结论的键,静默下去整份诊断会建立在假数据上且看不出异常。
|
|
44
|
+
* 返回提示文本交由调用方输出(不阻塞:将来离线日志分析可能是正当用法)。
|
|
45
|
+
*/
|
|
46
|
+
export declare function workspaceLogSourceWarning(config: ResolvedConfig, workspace: Workspace | null, configSource?: ConfigSource): string | null;
|
|
47
|
+
/**
|
|
48
|
+
* 把工作区覆盖应用到共享配置:产物路径落工作区内。
|
|
49
|
+
* 源码绑定不经配置层,直接住在 Workspace.sourceRepos(ADR 0002)。
|
|
50
|
+
*/
|
|
51
|
+
export declare function applyWorkspace(config: ResolvedConfig, workspace: Workspace): ResolvedConfig;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { machineConfigDir } from "@ats-cx/cx-core";
|
|
4
|
+
import { UserFacingError } from "./errors.js";
|
|
5
|
+
/** 诊断工作区目录名,建在编辑器源码仓库根下。 */
|
|
6
|
+
export const WORKSPACE_DIR_NAME = ".cx-cli";
|
|
7
|
+
/** 工作区语义表文件名:工作区模式下的唯一语义来源(spec 组件 D)。 */
|
|
8
|
+
export const WORKSPACE_SEMANTICS_FILE = "event-semantics.json";
|
|
9
|
+
/** 工作区模式下自动合成的源码绑定:仓库 id → 宿主仓库内相对位置。 */
|
|
10
|
+
const HOST_SOURCE_BINDINGS = [
|
|
11
|
+
["cxeditor", "."],
|
|
12
|
+
["cxeditor-resource", "resource"],
|
|
13
|
+
["cxeditor-apps", "src/apps"],
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* 从 cwd 向上找 .cx-cli/ 工作区(类 .git 发现),到文件系统根为止。
|
|
17
|
+
* 命中但 config.json 缺失/损坏视为工作区损坏,直接报错——
|
|
18
|
+
* 静默回退会把产物写到别处,比报错更难排查。
|
|
19
|
+
*/
|
|
20
|
+
export function discoverWorkspace(cwd) {
|
|
21
|
+
for (let current = resolve(cwd);;) {
|
|
22
|
+
const workspaceDir = join(current, WORKSPACE_DIR_NAME);
|
|
23
|
+
if (existsSync(workspaceDir) && statSync(workspaceDir).isDirectory()) {
|
|
24
|
+
return loadWorkspace(current, workspaceDir);
|
|
25
|
+
}
|
|
26
|
+
const parent = dirname(current);
|
|
27
|
+
if (parent === current) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
current = parent;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function loadWorkspace(hostRoot, workspaceDir) {
|
|
34
|
+
const configPath = join(workspaceDir, "config.json");
|
|
35
|
+
const remedy = `请在 ${hostRoot} 重新运行 cx-cli init,或用 --config 显式指定配置目录。`;
|
|
36
|
+
if (!existsSync(configPath)) {
|
|
37
|
+
throw new UserFacingError(`错误: 发现工作区 ${workspaceDir},但缺少 config.json。\n${remedy}`);
|
|
38
|
+
}
|
|
39
|
+
let raw;
|
|
40
|
+
try {
|
|
41
|
+
raw = JSON.parse(readFileSync(configPath, "utf8"));
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
45
|
+
throw new UserFacingError(`错误: ${configPath} 不是合法 JSON(${reason})。\n${remedy}`);
|
|
46
|
+
}
|
|
47
|
+
const hasToolkitConfigDir = Object.hasOwn(raw, "toolkitConfigDir");
|
|
48
|
+
const toolkitConfigDir = hasToolkitConfigDir ? raw.toolkitConfigDir : null;
|
|
49
|
+
if (hasToolkitConfigDir && (typeof toolkitConfigDir !== "string" || toolkitConfigDir.length === 0)) {
|
|
50
|
+
throw new UserFacingError(`错误: ${configPath} 的 toolkitConfigDir 必须是路径字符串或省略。\n${remedy}`);
|
|
51
|
+
}
|
|
52
|
+
const resolveLocal = (value) => (isAbsolute(value) ? value : join(workspaceDir, value));
|
|
53
|
+
return {
|
|
54
|
+
hostRoot,
|
|
55
|
+
workspaceDir,
|
|
56
|
+
toolkitConfigDir: typeof toolkitConfigDir === "string" ? resolveLocal(toolkitConfigDir) : null,
|
|
57
|
+
runsDir: resolveLocal(String(raw.runsDir ?? "runs")),
|
|
58
|
+
projectHistoryDir: resolveLocal(String(raw.projectHistoryDir ?? "data")),
|
|
59
|
+
outDir: resolveLocal(String(raw.outDir ?? "out")),
|
|
60
|
+
envPath: join(workspaceDir, ".env"),
|
|
61
|
+
semanticsPath: join(workspaceDir, WORKSPACE_SEMANTICS_FILE),
|
|
62
|
+
sourceRepos: HOST_SOURCE_BINDINGS.map(([id, relPath]) => ({ id, path: resolve(hostRoot, relPath) })),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 工作区模式即真实诊断路径(fixture 自测走显式 --config,那时 workspace 为 null)。
|
|
67
|
+
* 此时日志源仍指向本地文件,说明工具箱 config/ 被自测配置污染——logSource 是唯一
|
|
68
|
+
* 不被工作区覆盖、却能左右结论的键,静默下去整份诊断会建立在假数据上且看不出异常。
|
|
69
|
+
* 返回提示文本交由调用方输出(不阻塞:将来离线日志分析可能是正当用法)。
|
|
70
|
+
*/
|
|
71
|
+
export function workspaceLogSourceWarning(config, workspace, configSource = "explicit") {
|
|
72
|
+
if (!workspace || config.logSource.type !== "file") {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const sameDirLocal = join(config.configDir, "toolkit.local.json");
|
|
76
|
+
const machineLocal = join(machineConfigDir(), "toolkit.local.json");
|
|
77
|
+
const sourcePath = definesLogSource(sameDirLocal)
|
|
78
|
+
? sameDirLocal
|
|
79
|
+
: configSource === "toolkit" && definesLogSource(machineLocal)
|
|
80
|
+
? machineLocal
|
|
81
|
+
: join(config.configDir, "toolkit.json");
|
|
82
|
+
return (`警告: 工作区 ${workspace.workspaceDir} 下日志源仍是本地文件 ${config.logSource.path},真实诊断会建立在假数据上。\n` +
|
|
83
|
+
`请删掉 ${sourcePath} 里的 logSource;fixture 自测改用 --config fixtures/config。`);
|
|
84
|
+
}
|
|
85
|
+
function definesLogSource(filePath) {
|
|
86
|
+
if (!existsSync(filePath)) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
const raw = JSON.parse(readFileSync(filePath, "utf8"));
|
|
90
|
+
return typeof raw === "object" && raw !== null && Object.hasOwn(raw, "logSource");
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 把工作区覆盖应用到共享配置:产物路径落工作区内。
|
|
94
|
+
* 源码绑定不经配置层,直接住在 Workspace.sourceRepos(ADR 0002)。
|
|
95
|
+
*/
|
|
96
|
+
export function applyWorkspace(config, workspace) {
|
|
97
|
+
return {
|
|
98
|
+
...config,
|
|
99
|
+
runsDir: workspace.runsDir,
|
|
100
|
+
outDir: workspace.outDir,
|
|
101
|
+
projectHistoryDir: workspace.projectHistoryDir,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=workspace.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ats-cx/cx-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "编辑器诊断工具箱命令行:串联埋点日志、project 快照与 APM,输出结构化诊断 run 供 Agent 消费;附带 Claude Code / Codex skills",
|
|
5
|
+
"author": "heyg",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=22.12"
|
|
10
|
+
},
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"bin": {
|
|
14
|
+
"cx-cli": "./bin/cx-cli.cjs"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"bin",
|
|
18
|
+
"dist",
|
|
19
|
+
"!dist/**/*.map",
|
|
20
|
+
"skills",
|
|
21
|
+
"config",
|
|
22
|
+
".env.example"
|
|
23
|
+
],
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"commander": "^15.0.0",
|
|
29
|
+
"zod": "^3.24.0",
|
|
30
|
+
"@ats-cx/cx-core": "0.1.0"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc -p tsconfig.json",
|
|
34
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
35
|
+
"typecheck:tests": "tsc -p tsconfig.test.json"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: apm-query
|
|
3
|
+
description: >-
|
|
4
|
+
用 cx-cli apm 命令组从 APM 网页端临时取数(不建 run)。
|
|
5
|
+
当用户要「查一下」某用户 / projectId / 订单的网络情况、接口报错、某个埋点有没有触发、现网 project JSON,
|
|
6
|
+
或「最近的日志查不到」时使用。诊断客诉、建 run 走 editor-diagnostic skill。
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# APM 临时取数(cx-cli)
|
|
10
|
+
|
|
11
|
+
要结论建 run,要数据走 apm-query。
|
|
12
|
+
|
|
13
|
+
## 0. 先路由
|
|
14
|
+
|
|
15
|
+
| 用户意图 | 处理 |
|
|
16
|
+
|---|---|
|
|
17
|
+
| 「保存丢了」「改完变回去」「刷新后丢失」「用户投诉」「为什么」等客诉或归因问题 | 交给 **editor-diagnostic skill** 建 run;不要在这里下诊断结论。 |
|
|
18
|
+
| 「先看数据,不用建 run」或只问网络、接口、埋点、现网 JSON | 留在本 skill,做临时取数。 |
|
|
19
|
+
| 已在诊断 run 中,需要补查 APM 数据 | 查询命令加 `--run <runId>`;证据用法见 **editor-diagnostic skill**。 |
|
|
20
|
+
|
|
21
|
+
## 1. 前提与产物
|
|
22
|
+
|
|
23
|
+
先检查:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
cx-cli --version
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
命令不可用时交给 **cx-cli-setup skill**。APM 命令组不要求诊断工作区,也不读取 `.env`:在工作区内,查询落盘默认写到 `.cx-cli/out/apm/`;否则写到 `~/.config/cx-cli/out/apm/`。命令 stdout 只给摘要和预览,完整原始行始终在落盘文件中。
|
|
30
|
+
|
|
31
|
+
## 2. 选择命令
|
|
32
|
+
|
|
33
|
+
先按意图选日志类型,再补身份与时间窗:
|
|
34
|
+
|
|
35
|
+
| 用户说 | 命令 | 口径 |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| 网络情况 / 网络质量 / 网差不差 | `cx-cli apm query --type network …` | 只查 network;不要默认附带 api,每多一张表约多一次查询成本。 |
|
|
38
|
+
| 接口报错 / 请求慢 / 请求失败 | `cx-cli apm query --type api …` | 用户点名状态码时才加 `--where data_http_status=500`;「非 200 / 失败率」从落盘聚合。 |
|
|
39
|
+
| 同时问网络和接口 | 分别执行 network 与 api 两条查询 | 两条互不依赖,可并行执行。 |
|
|
40
|
+
| 前端报错 / 页面报错 / 报 JS 错 | `cx-cli apm query --type error --project <id> …`;只有用户身份时改用 `--user <id>` | 看 `data_message` 与原始 `data_stack`;前 3 帧常是 APM SDK 的 fetch 包装,可跳过。第一方帧只能定位到 chunk;把消息中的接口路径带去 api 查询对照。回答把「sourcemap 未部署,无法还原到源码行」作为工具链能力边界声明,不作为本次故障的根因结论。 |
|
|
41
|
+
| 某个埋点 / 有没有触发 / 触发几次 | `cx-cli apm query --type user_behav --event <事件名> …` | `--event` 是模糊匹配。 |
|
|
42
|
+
| 页面性能 / 加载慢 | `cx-cli apm query --type performance [--sub-type navigation\|memory\|resource] …` | 按用户点名的性能类型选择子类型。 |
|
|
43
|
+
| 渲染 / 绘制 | `cx-cli apm query --type paint …` | 读取绘制与耗时字段。 |
|
|
44
|
+
| 服务端报错 / 轨迹 / 服务端接口 | `cx-cli apm query --type server_error …` / `--type footprint …` / `--type server_api …` | 按用户点名的对象选一张表。 |
|
|
45
|
+
| 现网 JSON / 线上版本 | `cx-cli apm project <projectId>` | 返回当前线上 project JSON,不是失败快照。 |
|
|
46
|
+
| stderr 出现「同步失败」警告 | `cx-cli apm flush` 成功后重查 | query 默认已自动同步;正常情况不用手动 flush。 |
|
|
47
|
+
|
|
48
|
+
### 身份解析
|
|
49
|
+
|
|
50
|
+
| 已知身份 | 查询参数 |
|
|
51
|
+
|---|---|
|
|
52
|
+
| projectId | `--project <projectId>` |
|
|
53
|
+
| 邮箱 | 先 `cx-cli apm resolve --email <邮箱>`,再用返回的 `userId` 加 `--user <userId>` |
|
|
54
|
+
| 手机号 | 先 `cx-cli apm resolve --phone <手机号>`,再用返回的 `userId` 加 `--user <userId>` |
|
|
55
|
+
| userId | `--user <userId>` |
|
|
56
|
+
| 订单号 | `--item oNNN` |
|
|
57
|
+
| sessionId | `--session <sessionId>` |
|
|
58
|
+
| 相册 ID | `--collection <collectionId>` |
|
|
59
|
+
|
|
60
|
+
### 时间窗
|
|
61
|
+
|
|
62
|
+
`apm query` 的 `--from` 必填:
|
|
63
|
+
|
|
64
|
+
- 用户给了时间窗:照用,并按需加 `--to`。
|
|
65
|
+
- 有 projectId、用户未给时间窗:查项目全史,用 `--from 2000-01-01`。
|
|
66
|
+
- 只有用户身份或没有 ID 过滤、用户未给时间窗:从执行日往前取最近 7 天。
|
|
67
|
+
- 最终回答必须声明实际使用的时间窗。`--from` / `--to` 过滤 `log_time`;它是客户端本地墙钟、不带时区,可能明显偏差甚至落到未来。时间序列按服务器钟 `received_time` 分桶,并在发现两钟偏差时指出。
|
|
68
|
+
- 两钟差值判读:差为整小时 ≈ 时区差(两个戳都不带时区),零碎差 ≈ 钟漂或上报缓冲。`received_time` 是批次到达时间:SDK 先存 IndexedDB,凑满 20 条或每 30s 传一批,关页遗留存量下次打开页面才排空——堆积批会整批落到之后的某天。同设备内的先后顺序以 `log_time` 为准。
|
|
69
|
+
|
|
70
|
+
### 规模
|
|
71
|
+
|
|
72
|
+
| 条件 | 做法 |
|
|
73
|
+
|---|---|
|
|
74
|
+
| 有 `--project` / `--session` / `--collection` | 直接拉取;不先花一次查询做 `--count`。 |
|
|
75
|
+
| 只有 `--user`,或没有 ID 过滤 | 先加 `--count` 探规模;超过 50,000 行时收窄时间窗再拉。 |
|
|
76
|
+
| 「最近 N 条」「最新一次」 | 加 `--limit N`。 |
|
|
77
|
+
|
|
78
|
+
## 3. 读取落盘并聚合
|
|
79
|
+
|
|
80
|
+
查询落盘形状是 `{ meta, result }`。以下片段都把落盘绝对路径作为第一个参数;`session_id` 为空时统一退到 `install_id`。按小时分桶使用 `slice(0, 13)`,改成 `slice(0, 10)` 即按日分桶。
|
|
81
|
+
|
|
82
|
+
### network:RTT、downlink、网络类型与时间桶
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
node --input-type=commonjs - "/absolute/path/query.network....json" <<'NODE'
|
|
86
|
+
const { readFileSync } = require("node:fs");
|
|
87
|
+
const dump = JSON.parse(readFileSync(process.argv[2], "utf8"));
|
|
88
|
+
const rows = dump.result;
|
|
89
|
+
const number = value => {
|
|
90
|
+
if (value === "" || value == null) return null;
|
|
91
|
+
const parsed = Number(value);
|
|
92
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
93
|
+
};
|
|
94
|
+
const stats = values => {
|
|
95
|
+
const sorted = values.map(number).filter(value => value !== null).sort((a, b) => a - b);
|
|
96
|
+
const pick = ratio => sorted.length ? sorted[Math.round((sorted.length - 1) * ratio)] : null;
|
|
97
|
+
return {
|
|
98
|
+
count: sorted.length,
|
|
99
|
+
avg: sorted.length ? sorted.reduce((sum, value) => sum + value, 0) / sorted.length : null,
|
|
100
|
+
p50: pick(0.5),
|
|
101
|
+
p95: pick(0.95),
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
const groupKey = row => row.session_id || row.install_id || "(unknown)";
|
|
105
|
+
const bucketKey = row => String(row.received_time || "(unknown)").replace("T", " ").slice(0, 13);
|
|
106
|
+
const group = (items, key) => {
|
|
107
|
+
const groups = new Map();
|
|
108
|
+
for (const item of items) {
|
|
109
|
+
const value = key(item);
|
|
110
|
+
const matched = groups.get(value);
|
|
111
|
+
matched ? matched.push(item) : groups.set(value, [item]);
|
|
112
|
+
}
|
|
113
|
+
return groups;
|
|
114
|
+
};
|
|
115
|
+
const summarize = items => ({
|
|
116
|
+
count: items.length,
|
|
117
|
+
rtt: stats(items.map(row => row.data_rtt)),
|
|
118
|
+
downlink: stats(items.map(row => row.data_downlink)),
|
|
119
|
+
effectiveType: Object.fromEntries(
|
|
120
|
+
[...group(items, row => row.data_effective_type || "(unknown)")].map(([key, values]) => [key, values.length])
|
|
121
|
+
),
|
|
122
|
+
});
|
|
123
|
+
console.log(JSON.stringify({
|
|
124
|
+
total: dump.meta.total,
|
|
125
|
+
fetched: dump.meta.fetched,
|
|
126
|
+
overall: summarize(rows),
|
|
127
|
+
byGroup: Object.fromEntries([...group(rows, groupKey)].map(([key, values]) => [key, summarize(values)])),
|
|
128
|
+
byReceivedHour: Object.fromEntries([...group(rows, bucketKey)].map(([key, values]) => [key, summarize(values)])),
|
|
129
|
+
}, null, 2));
|
|
130
|
+
NODE
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### api:URL 失败率与耗时分位
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
node --input-type=commonjs - "/absolute/path/query.api....json" <<'NODE'
|
|
137
|
+
const { readFileSync } = require("node:fs");
|
|
138
|
+
const dump = JSON.parse(readFileSync(process.argv[2], "utf8"));
|
|
139
|
+
const rows = dump.result;
|
|
140
|
+
const groupKey = row => row.session_id || row.install_id || "(unknown)";
|
|
141
|
+
const failed = row => String(row.data_http_status) !== "200" || String(row.data_biz_status) !== "200000";
|
|
142
|
+
const group = (items, key) => {
|
|
143
|
+
const groups = new Map();
|
|
144
|
+
for (const item of items) {
|
|
145
|
+
const value = key(item);
|
|
146
|
+
const matched = groups.get(value);
|
|
147
|
+
matched ? matched.push(item) : groups.set(value, [item]);
|
|
148
|
+
}
|
|
149
|
+
return groups;
|
|
150
|
+
};
|
|
151
|
+
const costs = items => {
|
|
152
|
+
const sorted = items
|
|
153
|
+
.map(row => row.data_cost === "" || row.data_cost == null ? null : Number(row.data_cost))
|
|
154
|
+
.filter(value => value !== null && Number.isFinite(value))
|
|
155
|
+
.sort((a, b) => a - b);
|
|
156
|
+
const pick = ratio => sorted.length ? sorted[Math.round((sorted.length - 1) * ratio)] : null;
|
|
157
|
+
return {
|
|
158
|
+
avg: sorted.length ? sorted.reduce((sum, value) => sum + value, 0) / sorted.length : null,
|
|
159
|
+
p50: pick(0.5),
|
|
160
|
+
p95: pick(0.95),
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
const summarize = items => {
|
|
164
|
+
const failureCount = items.filter(failed).length;
|
|
165
|
+
return {
|
|
166
|
+
total: items.length,
|
|
167
|
+
failed: failureCount,
|
|
168
|
+
failureRate: items.length ? failureCount / items.length : 0,
|
|
169
|
+
groups: new Set(items.map(groupKey)).size,
|
|
170
|
+
cost: costs(items),
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
const byUrl = [...group(rows, row => row.data_url || "(unknown)")]
|
|
174
|
+
.map(([url, items]) => ({ url, ...summarize(items) }))
|
|
175
|
+
.sort((left, right) => right.total - left.total);
|
|
176
|
+
const byReceivedHour = [...group(rows, row => String(row.received_time || "(unknown)").replace("T", " ").slice(0, 13))]
|
|
177
|
+
.map(([hour, items]) => ({ hour, ...summarize(items) }));
|
|
178
|
+
const byGroup = Object.fromEntries(
|
|
179
|
+
[...group(rows, groupKey)].map(([key, items]) => [key, summarize(items)])
|
|
180
|
+
);
|
|
181
|
+
console.log(JSON.stringify({ total: dump.meta.total, fetched: dump.meta.fetched, byUrl, byGroup, byReceivedHour }, null, 2));
|
|
182
|
+
NODE
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### user_behav:事件计数与会话分组
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
node --input-type=commonjs - "/absolute/path/query.user_behav....json" <<'NODE'
|
|
189
|
+
const { readFileSync } = require("node:fs");
|
|
190
|
+
const dump = JSON.parse(readFileSync(process.argv[2], "utf8"));
|
|
191
|
+
const rows = dump.result;
|
|
192
|
+
const groupKey = row => row.session_id || row.install_id || "(unknown)";
|
|
193
|
+
const group = (items, key) => {
|
|
194
|
+
const groups = new Map();
|
|
195
|
+
for (const item of items) {
|
|
196
|
+
const value = key(item);
|
|
197
|
+
const matched = groups.get(value);
|
|
198
|
+
matched ? matched.push(item) : groups.set(value, [item]);
|
|
199
|
+
}
|
|
200
|
+
return groups;
|
|
201
|
+
};
|
|
202
|
+
const countBy = (items, key) => Object.fromEntries(
|
|
203
|
+
[...group(items, key)].map(([value, matched]) => [value, matched.length])
|
|
204
|
+
);
|
|
205
|
+
console.log(JSON.stringify({
|
|
206
|
+
total: dump.meta.total,
|
|
207
|
+
fetched: dump.meta.fetched,
|
|
208
|
+
events: countBy(rows, row => row.data_name || "(unknown)"),
|
|
209
|
+
byGroup: Object.fromEntries(
|
|
210
|
+
[...group(rows, groupKey)].map(([key, items]) => [key, {
|
|
211
|
+
total: items.length,
|
|
212
|
+
events: countBy(items, row => row.data_name || "(unknown)"),
|
|
213
|
+
}])
|
|
214
|
+
),
|
|
215
|
+
byReceivedHour: countBy(rows, row => String(row.received_time || "(unknown)").replace("T", " ").slice(0, 13)),
|
|
216
|
+
}, null, 2));
|
|
217
|
+
NODE
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### user_behav:行为轨迹还原
|
|
221
|
+
|
|
222
|
+
用户要「行为轨迹 / 这个项目用户做了什么」时用这段:按 `received_time` 排序还原顺序,按日分桶给概览,关键事件逐条给明细,`switchSheet` 序列还原翻页路径。`received_time` 是批次到达时间,堆积批会错天——判读口径见「时间窗」。
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
node --input-type=commonjs - "/absolute/path/query.user_behav....json" <<'NODE'
|
|
226
|
+
const { readFileSync } = require("node:fs");
|
|
227
|
+
const dump = JSON.parse(readFileSync(process.argv[2], "utf8"));
|
|
228
|
+
// 按 case 增删;switchSheet / TabSync_Query 等高频噪音不进白名单,由 byDay 计数承载
|
|
229
|
+
const KEY_EVENTS = new Set([
|
|
230
|
+
"openProject", "enterPage", "closeOrLeavePage", "switchMainTab",
|
|
231
|
+
"saveProject", "TabSync_SaveSuccessBroadcast",
|
|
232
|
+
"startUploadFiles", "completeUploadFiles", "addToCart",
|
|
233
|
+
]);
|
|
234
|
+
const parseExtra = value => {
|
|
235
|
+
if (typeof value !== "string") return value;
|
|
236
|
+
try { return JSON.parse(value); } catch { return value; }
|
|
237
|
+
};
|
|
238
|
+
const rows = dump.result
|
|
239
|
+
.slice()
|
|
240
|
+
.sort((left, right) => String(left.received_time).localeCompare(String(right.received_time)));
|
|
241
|
+
const byDay = new Map();
|
|
242
|
+
for (const row of rows) {
|
|
243
|
+
const day = String(row.received_time || "(unknown)").slice(0, 10);
|
|
244
|
+
const bucket = byDay.get(day) || { first: row.received_time, last: row.received_time, count: 0, users: new Set(), sessions: new Set(), events: {}, sheetSequence: [] };
|
|
245
|
+
bucket.last = row.received_time;
|
|
246
|
+
bucket.count += 1;
|
|
247
|
+
bucket.users.add(row.user_id);
|
|
248
|
+
bucket.sessions.add(row.session_id || row.install_id || "(unknown)");
|
|
249
|
+
bucket.events[row.data_name] = (bucket.events[row.data_name] || 0) + 1;
|
|
250
|
+
if (row.data_name === "switchSheet") bucket.sheetSequence.push(parseExtra(row.data_extra)?.sheetIndex ?? null);
|
|
251
|
+
byDay.set(day, bucket);
|
|
252
|
+
}
|
|
253
|
+
console.log(JSON.stringify({
|
|
254
|
+
total: dump.meta.total,
|
|
255
|
+
fetched: dump.meta.fetched,
|
|
256
|
+
byDay: Object.fromEntries([...byDay].map(([day, bucket]) => [day, {
|
|
257
|
+
first: bucket.first,
|
|
258
|
+
last: bucket.last,
|
|
259
|
+
count: bucket.count,
|
|
260
|
+
users: [...bucket.users],
|
|
261
|
+
sessions: [...bucket.sessions],
|
|
262
|
+
events: bucket.events,
|
|
263
|
+
sheetSequence: bucket.sheetSequence,
|
|
264
|
+
}])),
|
|
265
|
+
keyEvents: rows
|
|
266
|
+
.filter(row => KEY_EVENTS.has(row.data_name))
|
|
267
|
+
.map(row => ({
|
|
268
|
+
received: row.received_time,
|
|
269
|
+
log: row.log_time,
|
|
270
|
+
user: row.user_id,
|
|
271
|
+
session: row.session_id || row.install_id || "(unknown)",
|
|
272
|
+
event: row.data_name,
|
|
273
|
+
extra: parseExtra(row.data_extra),
|
|
274
|
+
})),
|
|
275
|
+
}, null, 2));
|
|
276
|
+
NODE
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## 4. 回答纪律
|
|
280
|
+
|
|
281
|
+
每次回答都给出四项:
|
|
282
|
+
|
|
283
|
+
1. 实际时间窗;
|
|
284
|
+
2. 实际过滤条件;
|
|
285
|
+
3. 查询的 `total` 与 `fetched`;
|
|
286
|
+
4. 查询落盘绝对路径。
|
|
287
|
+
|
|
288
|
+
只陈述查到的数据和数据缺口,不把相关性写成根因。用户继续追问「为什么」或要求归因时,交给 **editor-diagnostic skill** 建 run。
|
|
289
|
+
|
|
290
|
+
## 5. 复用、清理与速查
|
|
291
|
+
|
|
292
|
+
- 同一会话中,相同条件不要重查:先读取已有落盘,核对 `meta.request.body` 与这次所需的类型、身份、时间窗和过滤条件;一致就直接复用。
|
|
293
|
+
- `<outDir>/apm/` 只是临时取数产物目录,可随时整目录删除;用户要求清理时,先确认解析后的精确目标确为本次 `<outDir>/apm/`,再执行删除。没有专用清理命令。
|
|
294
|
+
- 回答最后一行再附一次落盘绝对路径,方便继续读取。
|
|
295
|
+
|
|
296
|
+
| 子命令 | 速查 |
|
|
297
|
+
|---|---|
|
|
298
|
+
| `apm query` | `cx-cli apm query --type network --project 6478815 --from 2000-01-01 --json` |
|
|
299
|
+
| `apm flush` | `cx-cli apm flush --json` |
|
|
300
|
+
| `apm resolve` | `cx-cli apm resolve --email user@example.com --json` |
|
|
301
|
+
| `apm project` | `cx-cli apm project 6478815 --json` |
|
|
302
|
+
|
|
303
|
+
所有命令支持 `--json`。退出码:`0` 成功,`1` 业务失败,`2` 参数错误。exit 1 若返回
|
|
304
|
+
`kind:"unconfigured"`,转给 **cx-cli-setup skill** 填站点地址后再重试。
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Codex / ChatGPT 桌面端的可选元数据(Claude Code 忽略本文件)。
|
|
2
|
+
# 字段集见 https://learn.chatgpt.com/docs/build-skills 的 Optional metadata。
|
|
3
|
+
interface:
|
|
4
|
+
display_name: "APM 临时取数(cx-cli)"
|
|
5
|
+
short_description: "不建 run,直接从 APM 网页端查用户 / 项目的网络、接口、埋点数据并落盘聚合"
|
|
6
|
+
brand_color: "#2F6FEB"
|
|
7
|
+
default_prompt: "帮我查 <projectId 或邮箱> 在 <时间段> 的 <网络情况 / 接口报错 / 某个埋点>"
|
|
8
|
+
|
|
9
|
+
policy:
|
|
10
|
+
allow_implicit_invocation: true
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cx-cli-setup
|
|
3
|
+
description: >-
|
|
4
|
+
安装 / 修复编辑器诊断工具箱 cx-cli(npm 包 @ats-cx/cx-cli)并在当前仓库初始化 .cx-cli 诊断工作区。
|
|
5
|
+
当用户要「安装 cx-cli」「setup 诊断环境」「初始化诊断工作区」,cx-cli 命令不可用、skills 未链接 / 没链接上,
|
|
6
|
+
或 apm 报 unconfigured 要填站点地址时使用。日常诊断走 editor-diagnostic skill。
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# cx-cli Setup
|
|
10
|
+
|
|
11
|
+
两级安装的统一入口:**机器级**(全局命令 `cx-cli` 可用 + 包内 skills 链进本机 Agent + 机器级覆盖目录 `~/.config/cx-cli/`,一台机器一次)+ **项目级**(当前仓库建 `.cx-cli/` 诊断工作区,每个仓库一次)。全部幂等:重复触发只补缺的、修坏的,已有配置与语义表不动;`.cx-cli/.gitignore` 是唯一会按当前模板重写的文件。
|
|
12
|
+
|
|
13
|
+
本 skill 只调用现成命令并解读 `--json` 输出:机器级喊 `cx-cli skills status` / `cx-cli skills install`,项目级喊 `cx-cli init`。不要绕过它们手工建软链、手工建目录。本 skill 随 `@ats-cx/cx-cli` 包分发,**不需要知道包装在哪**——需要路径时全部从命令输出里取。
|
|
14
|
+
|
|
15
|
+
从别的 skill 带着 `kind:"unconfigured"` 的 apm 错误转交过来时,直接进 §4(文件路径取错误里的 `hint`),§1–3 可省。
|
|
16
|
+
|
|
17
|
+
## 1. 机器级:探针
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
cx-cli skills status --json
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
按结果三分支:
|
|
24
|
+
|
|
25
|
+
| 结果 | 含义 | 动作 |
|
|
26
|
+
|------|------|------|
|
|
27
|
+
| stdout 是 JSON | 命令可用 | 读 `ok`:`true` → 记下 `configDir.path` 与 `configDir.apmConfigured`,进 §3;`false`(exit 1)→ §2 |
|
|
28
|
+
| shell 报 command not found(exit 127) | 当前 shell 的 PATH 里没有 `cx-cli`——多半装在别的 node 版本下 | §1.1 |
|
|
29
|
+
| stderr 首行 `[cx-cli] 当前 node vX 低于要求(>= 22.12),且未找到可用的新版 node` | 自带的 node 启动器找不到达标 node | 转告用户按第二行提示做:`nvm install 24`,或(非 nvm 用户)`export CX_CLI_NODE=<node 可执行文件绝对路径>`。**不代装**;装好后重新触发本 skill |
|
|
30
|
+
|
|
31
|
+
### 1.1 命令不存在:先找已装副本,不装第二份
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
ls -d ~/.nvm/versions/node/*/lib/node_modules/@ats-cx/cx-cli 2>/dev/null
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- **找到**(多份取版本号最高的那个 `<v>`):本次会话用绝对路径 `~/.nvm/versions/node/<v>/bin/cx-cli` 代替 `cx-cli`(它自带 node 启动器,在当前 shell 的旧 node 下也能跑),回 §1 重跑探针。同时告诉用户:日常终端 `nvm use <v>` 就有命令;想让日常 node 里也有,按工具箱 README「nvm 多个 node 版本」迁移后重跑 `cx-cli skills install`。**不装第二份。**
|
|
38
|
+
- **没找到**(含 volta / brew 等非 nvm 环境——探测只认 `~/.nvm`):把下面这段原样给用户,**不代装**,用户装完重新触发本 skill:
|
|
39
|
+
|
|
40
|
+
> 在你日常默认的 node 下(不用先 `nvm use`)执行:
|
|
41
|
+
> `npm i -g @ats-cx/cx-cli && cx-cli skills install`
|
|
42
|
+
> 前提:机器上有一份 ≥ 22.12 的 node(`nvm install 24` 一次即可,不必改 default)。`nvm install` 会把当前终端切到 24,**开一个新终端**再装,否则包会落进 24 的目录、日常终端里找不到命令。`npm i -g` 打出的 `notsup` / `EBADENGINE` engines 警告是预期的,包已装好;只有 `~/.npmrc` 写了 `engine-strict=true` 才会拒装(临时加 `--no-engine-strict`)。
|
|
43
|
+
|
|
44
|
+
维护者(clone 工具箱仓库的开发通道)不走这句,走 `bash scripts/setup.sh`——见工具箱 README「安装」开发通道段。
|
|
45
|
+
|
|
46
|
+
## 2. 机器级:修复
|
|
47
|
+
|
|
48
|
+
`ok:false` 时不必分辨 `issues` 里是 `missing` / `stale` / `dangling` / `orphans`,一律:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cx-cli skills install --json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
逐字段解读并转述:
|
|
55
|
+
|
|
56
|
+
| 字段 | 含义与动作 |
|
|
57
|
+
|------|-----------|
|
|
58
|
+
| `created` | 新建的链(首次安装、包升级后新增的 skill) |
|
|
59
|
+
| `updated[]` | 重指的链。**逐条转述 `from → path`**,让用户看见「原来指向哪一份」(另一 node 版本下的旧包、旧 clone)。若 `from` 那份才是用户想用的,让他从那一份重跑 `cx-cli skills install` 即可指回,零成本 |
|
|
60
|
+
| `pruned` | 已删的孤儿链(更名 / 删除的旧 skill 留下的) |
|
|
61
|
+
| `skipped` | 已指向当前包,没动 |
|
|
62
|
+
| `warnings` | 如「跳过 …:已存在同名真实文件/目录(非软链),请手动处理」——转述路径,**不代删** |
|
|
63
|
+
| `configDir` | `path` 机器级覆盖目录(§4 写 apm 覆盖用它);`created` 本次是否新建;`apmConfigured` 记下,§4 处理 |
|
|
64
|
+
| `hint` | 可直接转述:Agent 无需重启;`~/.claude/` 不存在时会说「装好 Claude Code 后重跑」;Codex 读 `~/.agents/skills` 无需额外链 |
|
|
65
|
+
|
|
66
|
+
exit 1 时读 `kind`:`unsupported_install`(npx 缓存 / `pnpm add -g` 装的)→ 转述 `hint`(改用 `npm i -g @ats-cx/cx-cli`),不代装;`io` → 转述 `error`(多半是权限或只读目录),不代修。
|
|
67
|
+
|
|
68
|
+
## 3. 项目级:初始化诊断工作区
|
|
69
|
+
|
|
70
|
+
在当前会话仓库内执行(cwd 处于编辑器源码仓库中即可,命令自己向上找 git 根):
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
cx-cli init --json
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
逐字段解读输出:
|
|
77
|
+
|
|
78
|
+
| 字段 | 含义与动作 |
|
|
79
|
+
|------|-----------|
|
|
80
|
+
| `workspaceDir` | 工作区位置(宿主仓库根 `.cx-cli/`) |
|
|
81
|
+
| `created` / `skipped` | 本次建了什么 / 已存在没动什么。首次 init 从工具箱种子复制 `event-semantics.json`;已有语义表、`.env` 与 `config.json` 均不覆盖 |
|
|
82
|
+
| `rewritten` | 本次被 init 改写的已有文件:`.gitignore` 每次按当前模板重写;`config.json` 仅在其 `toolkitConfigDir` 指向不存在的目录时被修(删掉这一键,`warnings` 会说明) |
|
|
83
|
+
| `warnings` | 四类:「宿主不像编辑器仓库」→ 转述给用户确认是否跑错目录,不阻塞;「skills 未安装到本机 Agent(…)」→ 回 §2(§1–2 走完后不该出现);「apm 未配置:…」→ §4 处理;「config.json 的 toolkitConfigDir 指向不存在的 …,已移除」→ 转述即可,无需动作 |
|
|
84
|
+
| `missingEnvKeys` | `.cx-cli/.env` 里还空着的 DB 凭据键 |
|
|
85
|
+
| `hint` | 汇总提示,可直接转述;apm 未配置时它会说「填好 apm 后可运行 run new / apm query」 |
|
|
86
|
+
|
|
87
|
+
工作区的版本控制边界由该 `.gitignore` 固定:默认 `*` 全屏蔽——`.env`、`config.json`、`runs/`、`data/`、`out/` 以及任何后来落进工作区的文件都不进宿主仓库 git;只有 `event-semantics.json` 与 `curation/` 被反向放行,它们是宿主仓库的语义策展成果,应随宿主 git 留档。用户若要让工作区里的别的文件进 git,需自己改这份 `.gitignore`,且下次 `init` 会被重写掉。
|
|
88
|
+
|
|
89
|
+
exit 1(向上找不到 git 仓库根)→ 提示用户 cd 进编辑器源码仓库后重试。
|
|
90
|
+
|
|
91
|
+
## 4. 收尾
|
|
92
|
+
|
|
93
|
+
**apm 站点地址**(`apmConfigured:false`,或 `init` warnings / 别的 skill 转交的错误里有「apm 未配置」):
|
|
94
|
+
|
|
95
|
+
1. 把那句提示**原文转述**(形如「apm 未配置:请在 `<机器级文件绝对路径>` 填 baseUrl 与 defaults.host(值向团队获取)。」),然后问用户:团队文档里那行 JSON(`{ "baseUrl": "https://…", "defaults": { "host": "www.…" } }`)贴给我,我写进去;或者你自己填。
|
|
96
|
+
2. 收到值 → 读现有 `<configDir.path>/apm-provider.local.json`(`skills install` 已落骨架),**只改 `baseUrl` 与 `defaults.host`,其余键原样保留**,写回。站点地址不是凭据,可以经 Agent 之手;但**不猜值、不从记忆或历史对话里编**,用户没给就只留提示。Agent 沙箱不允许写 `$HOME`(Codex 默认)时,改为把整行 JSON 与文件绝对路径给用户自己贴。
|
|
97
|
+
3. 验证:重跑 `cx-cli skills status --json`,`configDir.apmConfigured` 变 `true` 即可;若出现 `configDir.apmError`,把它转述给用户(多半是贴坏了 JSON)。不发真实 apm 请求——那是 apm-query skill 的事。
|
|
98
|
+
|
|
99
|
+
**DB 凭据**(`missingEnvKeys` 非空):告知用户在 `<workspaceDir>/.env` 填入这些键(向团队获取)。**只提示、不代填、永不入库。** 凭据补齐后即可 `cx-cli project pull <projectId>`;诊断命令不碰 DB,不需要 `.env`。
|
|
100
|
+
|
|
101
|
+
日常诊断走 editor-diagnostic skill:工作区就绪后各命令自动从 cwd 发现工作区,无需 `--config`。
|
|
102
|
+
|
|
103
|
+
## 升级 / 卸载(只答不做)
|
|
104
|
+
|
|
105
|
+
用户问起时给命令,**不代跑**;两者都在装包时所用的那个 node 版本下执行:
|
|
106
|
+
|
|
107
|
+
- 升级(降级同形):`npm i -g @ats-cx/cx-cli@latest && cx-cli skills install`。不碰任何仓库的 `.cx-cli/` 与 `~/.config/cx-cli/`,无需重跑 `init`,Agent 无需重启。
|
|
108
|
+
- 卸载:`cx-cli skills uninstall && npm rm -g @ats-cx/cx-cli`(先解链再删包)。`~/.config/cx-cli/` 与各仓库 `.cx-cli/` 由用户自己决定删不删。
|
|
109
|
+
|
|
110
|
+
细节见工具箱 README「升级」「卸载」「nvm 多个 node 版本」。
|
|
111
|
+
|
|
112
|
+
## 冷启动(本 skill 尚未装到任何 Agent 时)
|
|
113
|
+
|
|
114
|
+
本 skill 随包分发,装包前触发不了。新机器第一次由人在终端执行一次(前提与注意事项同 §1.1):
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
npm i -g @ats-cx/cx-cli && cx-cli skills install
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
此后任何仓库的 Agent 会话都能直接触发本 skill。
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Codex / ChatGPT 桌面端的可选元数据(Claude Code 忽略本文件)。
|
|
2
|
+
# 字段集见 https://learn.chatgpt.com/docs/build-skills 的 Optional metadata。
|
|
3
|
+
interface:
|
|
4
|
+
display_name: "cx-cli 安装与工作区初始化"
|
|
5
|
+
short_description: "安装 / 修复诊断工具箱 cx-cli,链接 skills,并在当前仓库建 .cx-cli 诊断工作区(幂等)"
|
|
6
|
+
brand_color: "#2F6FEB"
|
|
7
|
+
default_prompt: "在当前仓库安装 cx-cli 并初始化 .cx-cli 诊断工作区"
|
|
8
|
+
|
|
9
|
+
policy:
|
|
10
|
+
# 默认即 true:cx-cli 不可用时需要能被隐式命中兜底
|
|
11
|
+
allow_implicit_invocation: true
|