@shiplens/cli 1.3.0 → 1.4.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.
@@ -2,33 +2,28 @@ async function handleHeatmap(args, flags, ctx) {
2
2
  const appId = ctx.resolveAppId();
3
3
  const templateId = flags.template || args[0];
4
4
  if (!templateId) {
5
- throw new Error('Specify target page template ID via --template <id>');
5
+ throw new Error('请通过 --template <id> 指定目标页面模板 ID');
6
6
  }
7
7
 
8
8
  const env = flags.env || 'production';
9
9
  const domHash = flags['dom-hash'] || '';
10
10
 
11
- const resp = await ctx.client.getHeatmap({
12
- app_id: appId,
13
- template_id: templateId,
14
- env,
15
- dom_hash: domHash,
16
- });
11
+ const resp = await ctx.client.heatmap(appId, templateId, env, domHash);
17
12
 
18
13
  ctx.output(resp, () => {
19
- console.log(`🔥 Page Heatmap Analysis (Template: ${resp.template_id}, DOM Hash: ${resp.dom_hash || '-'}):`);
14
+ console.log(`🔥 页面热力图分析 (模板: ${resp.template_id}, DOM 哈希: ${resp.dom_hash || '-'}):`);
20
15
  if (resp.skeleton_svg_url) {
21
- console.log(`🖼️ Skeleton SVG Preview URL: ${resp.skeleton_svg_url}`);
16
+ console.log(`🖼️ 骨架图 SVG 预览地址: ${resp.skeleton_svg_url}`);
22
17
  }
23
- console.log(`👆 Total Clicks: ${resp.clicks_total || 0}\n`);
18
+ console.log(`👆 总点击数: ${resp.clicks_total || 0}\n`);
24
19
  if (resp.elements && resp.elements.length > 0) {
25
- console.log('Top Clicked Elements:');
20
+ console.log('高频点击元素分布:');
26
21
  resp.elements.forEach((el, i) => {
27
- console.log(`[${i + 1}] Element: ${el.label || '-'} (Selector: ${el.selector})`);
28
- console.log(` Clicks: ${el.clicks} | Click Rate: ${((el.click_rate || 0) * 100).toFixed(2)}% | Coordinates: (${el.x_pct}, ${el.y_pct})`);
22
+ console.log(`[${i + 1}] 元素: ${el.label || '-'} (选择器: ${el.selector})`);
23
+ console.log(` 点击数: ${el.clicks} | 点击转化率: ${((el.click_rate || 0) * 100).toFixed(2)}% | 坐标: (${el.x_pct}, ${el.y_pct})`);
29
24
  });
30
25
  } else {
31
- console.log('(No click data found for this template)');
26
+ console.log('(当前模板暂无点击元素数据)');
32
27
  }
33
28
  });
34
29
  }
@@ -1,5 +1,5 @@
1
1
  const readline = require('readline');
2
- const { detectProject, detectExistingApp, injectSDK, injectSkill, installSDKDependency, getGitEmail, autoGitCommit } = require('../injector');
2
+ const { detectProject, detectExistingApp, injectSDK, injectSkill, installSDKDependency, getGitEmail } = require('../injector');
3
3
  const { saveLocalConfig } = require('../config');
4
4
  const { saveDeviceEnv } = require('../device-env');
5
5
  const { resolveTaxonomyFromIDs, formatTaxonomySummary } = require('../taxonomy');
@@ -13,7 +13,7 @@ async function handleInit(args, flags, ctx) {
13
13
  const description = flags.description || detected.description || '';
14
14
  const framework = flags.framework || detected.framework;
15
15
 
16
- // Taxonomy resolution
16
+ // 4 级分类判定:优先命令行显式参数,否则使用 detectProject 智能推断结果
17
17
  let taxonomy = detected.taxonomy;
18
18
  if (flags.genre || flags.subgenre || flags.tags) {
19
19
  const rawTags = flags.tags ? (Array.isArray(flags.tags) ? flags.tags : flags.tags.split(',').map((t) => t.trim())) : (taxonomy ? taxonomy.feature_tag_ids : []);
@@ -24,18 +24,20 @@ async function handleInit(args, flags, ctx) {
24
24
  const subgenreId = taxonomy?.subgenre?.id || 'browser_web_utility';
25
25
  const featureTagIds = taxonomy?.feature_tag_ids || [];
26
26
  if (featureTagIds.length > 10) {
27
- throw new Error('Maximum 10 feature tags allowed.');
27
+ throw new Error('最多只能提交 10 个四级特性标签');
28
28
  }
29
29
  const industry = flags.industry || subgenreId || 'saas';
30
30
 
31
- // 1. Safety check for existing project configuration
31
+ // 1. 检查本地是否已存在 SDK 或已有项目编号 (防意外覆盖分支)
32
32
  const existing = detectExistingApp(wd);
33
33
  if (existing.has_existing && !flags.force) {
34
34
  if (ctx.isJSON) {
35
35
  const result = {
36
36
  ok: false,
37
37
  code: 'PROJECT_EXISTS_LOCALLY',
38
- message: `Existing Shiplens configuration detected (App ID: ${existing.app_id}) in ${existing.source_file}. Use --force to overwrite.`,
38
+ message: `检测到本地已存在 Shiplens 统计配置 (项目 ID: ${existing.app_id}),位于 ${existing.source_file}。` +
39
+ `选项 1 [推荐]: 保持现有统计,保留原项目编号与历史数据;` +
40
+ `选项 2: 重新创建并覆盖 (追加 --force,向云端申请全新项目编号并覆盖本地,旧看板将停止接收新数据)。`,
39
41
  existing_app_id: existing.app_id,
40
42
  source_file: existing.source_file,
41
43
  project_name: existing.project_name || projectName,
@@ -45,43 +47,44 @@ async function handleInit(args, flags, ctx) {
45
47
  process.exitCode = 1;
46
48
  return;
47
49
  } else if (!process.stdin.isTTY) {
48
- console.log(`\n⚠️ Existing Shiplens project detected!`);
49
- console.log(`📌 App ID: ${existing.app_id} (Source: ${existing.source_file})`);
50
- console.log(`📊 Dashboard: ${ctx.client.baseURL}/dashboard/${existing.app_id}`);
51
- console.log(`🛑 Non-interactive terminal detected (CI/Agent). Existing configuration preserved.`);
52
- console.log(`💡 To overwrite and create a new project, run with --force:`);
53
- console.log(` npx @shiplens/cli init --force\n`);
50
+ console.log(`\n⚠️ 检测到当前项目已存在 Shiplens 配置!`);
51
+ console.log(`📌 本地已存在项目编号: ${existing.app_id} (来源: ${existing.source_file})`);
52
+ console.log(`📊 现有数据看板: ${ctx.client.baseURL}/dashboard/${existing.app_id}`);
53
+ console.log(`🛑 检测到非交互式终端环境 (CI/Agent),已自动保留现有配置。`);
54
+ console.log(`💡 选项 1 [推荐]: 保持现有统计,保留原项目 ID 与历史数据;`);
55
+ console.log(`💡 选项 2: 若要强制重新创建新项目并覆盖本地配置,请追加 --force 参数:`);
56
+ console.log(` npx.cmd --yes @shiplens/cli init --force\n`);
54
57
  return;
55
58
  } else {
56
- console.log(`\n⚠️ Existing Shiplens project detected!`);
57
- console.log(`📌 App ID: ${existing.app_id} (Source: ${existing.source_file})`);
58
- console.log(`📊 Dashboard: ${ctx.client.baseURL}/dashboard/${existing.app_id}`);
59
- console.log(`\n💡 Option 1 [Recommended]: Keep existing setup and reuse App ID.`);
60
- console.log(`💡 Option 2: Run with --force to overwrite and create a new project:`);
61
- console.log(` npx @shiplens/cli init --force\n`);
59
+ console.log(`\n⚠️ 检测到当前项目可能正在统计中!`);
60
+ console.log(`📌 本地已存在项目编号: ${existing.app_id} (来源: ${existing.source_file})`);
61
+ console.log(`📊 现有数据看板: ${ctx.client.baseURL}/dashboard/${existing.app_id}`);
62
+ console.log(`\n💡 选项 1 [推荐]: 保持现有统计,继续使用原项目 ID,历史数据和现有看板完整保留;`);
63
+ console.log(`💡 选项 2: 强制重新创建并覆盖 (追加 --force),申请全新空白项目 ID 覆盖本地 (注意:旧看板停止接收新数据,新旧数据无法合并):`);
64
+ console.log(` npx.cmd --yes @shiplens/cli init --force\n`);
62
65
 
63
66
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
64
67
  const confirmed = await new Promise((resolve) => {
65
- rl.question('Overwrite and create a new project? (y/N): ', (ans) => {
68
+ rl.question('是否确认覆盖并重新创建全新项目?(y/N): ', (ans) => {
66
69
  rl.close();
67
70
  resolve(ans.trim().toLowerCase() === 'y' || ans.trim().toLowerCase() === 'yes');
68
71
  });
69
72
  });
70
73
  if (!confirmed) {
71
- console.log('🛑 Operation cancelled. Existing configuration preserved.');
74
+ console.log('🛑 操作已取消,已保留原项目配置与统计。');
72
75
  return;
73
76
  }
74
77
  }
75
78
  }
76
79
 
77
- // 2. Inject AI Skill (.agents/skills/shiplens/SKILL.md & Cursor rules)
80
+ // 2. 自动释放/注入通用 AI Skill 技能书 (.agents/skills/shiplens/SKILL.md rules)
78
81
  const skillFile = injectSkill(wd);
79
82
 
80
- // 3. Register project on cloud
83
+ // 3. 上报/同步到服务端注册项目
81
84
  let appId = '';
82
85
  let dashboardUrl = '';
83
86
  let accountStatus = 'not_logged_in_unlinked';
84
- let accountStatusText = 'Not Logged In (Project unlinked)';
87
+ let accountStatusText = '未登录 (首次安装默认状态或本地不存在有效凭据)';
85
88
 
86
89
  try {
87
90
  const connResp = await ctx.client.connect({
@@ -99,24 +102,24 @@ async function handleInit(args, flags, ctx) {
99
102
  if (ctx.resolvedAuth && ctx.resolvedAuth.is_present) {
100
103
  if (connResp.account_linked !== false && connResp.linked !== false && !connResp.unlinked) {
101
104
  accountStatus = 'logged_in_linked';
102
- accountStatusText = 'Logged In (Project linked to account)';
105
+ accountStatusText = '已登录 (项目已与账号关联)';
103
106
  } else {
104
107
  accountStatus = 'logged_in_unlinked';
105
- accountStatusText = 'Logged In (Project not linked to account)';
108
+ accountStatusText = '已登录 (项目和账号未关联)';
106
109
  }
107
110
  } else {
108
111
  accountStatus = 'not_logged_in_unlinked';
109
- accountStatusText = 'Not Logged In (Project unlinked)';
112
+ accountStatusText = '未登录 (首次安装默认状态或本地不存在有效凭据)';
110
113
  }
111
114
  } catch (netErr) {
112
115
  netErr.code = netErr.code || 'CONNECT_FAILED';
113
116
  throw netErr;
114
117
  }
115
118
 
116
- // 4. Inject SDK code
119
+ // 4. 自动插桩注入 SDK 代码
117
120
  const injectedFile = injectSDK(wd, framework, appId);
118
121
 
119
- // 5. Save local configuration
122
+ // 5. 写入本地状态机 .shiplens.json
120
123
  saveLocalConfig(wd, {
121
124
  app_id: appId,
122
125
  project_name: projectName,
@@ -129,17 +132,17 @@ async function handleInit(args, flags, ctx) {
129
132
  last_synced_at: new Date().toISOString(),
130
133
  });
131
134
 
132
- // 6. Install SDK dependency
135
+ // 6. 安装依赖 (4-Tier 阶梯并发竞速)
133
136
  let installResult = { success: true, manager: 'skipped' };
134
137
  if (!flags['no-install'] && detected.package_manager) {
135
138
  installResult = await installSDKDependency(wd, detected.package_manager);
136
139
  if (!installResult.success) {
137
- const errMsg = `SDK installation failed: Unable to download @shiplens/sdk across all download sources (official npm registry, Alibaba Cloud npmmirror, GitHub mirror, and Shiplens CDN mirror).\n` +
138
- `Troubleshooting steps:\n` +
139
- ` 1. Check your local network connection or HTTP proxy / VPN settings;\n` +
140
- ` 2. Try running manually with Alibaba mirror: npm install @shiplens/sdk --registry=https://registry.npmmirror.com;\n` +
141
- ` 3. Or install via official registry: npm install @shiplens/sdk;\n` +
142
- ` 4. Visit https://shiplens.com for latest SDK status and installation guides.`;
140
+ const errMsg = `SDK 安装失败: 无法从 NPM 官方源、备用镜像源、GitHub 源码镜像或官方 CDN 镜像下载安装 @shiplens/sdk。\n` +
141
+ `排查步骤建议:\n` +
142
+ ` 1. 检查本地网络连接或 HTTP 代理 / VPN 状态;\n` +
143
+ ` 2. 尝试手动使用备用镜像安装: npm install @shiplens/sdk --registry=https://registry.npmmirror.com;\n` +
144
+ ` 3. 或手动通过官方源安装: npm install @shiplens/sdk;\n` +
145
+ ` 4. 访问 https://shiplens.dev 查看最新 SDK 状态与手动安装指南。`;
143
146
 
144
147
  if (ctx.isJSON) {
145
148
  ctx.output({
@@ -157,7 +160,7 @@ async function handleInit(args, flags, ctx) {
157
160
  }
158
161
  }
159
162
 
160
- // 7. Bind email if specified
163
+ // 7. 绑定邮箱 (若用户显式指定或传 auto)
161
164
  let targetEmail = flags.email;
162
165
  if (targetEmail === 'auto') {
163
166
  targetEmail = getGitEmail();
@@ -180,12 +183,6 @@ async function handleInit(args, flags, ctx) {
180
183
  }
181
184
  }
182
185
 
183
- // 8. Auto Git commit
184
- let gitCommitResult = { committed: false };
185
- if (!flags['no-commit']) {
186
- gitCommitResult = autoGitCommit(wd, 'feat: Integrate Shiplens SDK and configure dashboard');
187
- }
188
-
189
186
  const elapsed = Date.now() - start;
190
187
  const taxonomyFormatted = formatTaxonomySummary(taxonomy);
191
188
 
@@ -208,44 +205,39 @@ async function handleInit(args, flags, ctx) {
208
205
  auth_status: authStatus,
209
206
  shiplens_env_written: authStatus === 'magic_link_sent',
210
207
  activation_required: authStatus === 'pending',
211
- free_tier_info: '50,000 free events/month (~5,000 unique visitors)',
208
+ free_tier_info: '每月 50,000 事件 (约 5,000 独立访客) 永久免费统计额度',
212
209
  bound_email: targetEmail || undefined,
213
- git_committed: gitCommitResult.committed,
214
- git_commit_hash: gitCommitResult.hash || undefined,
215
210
  elapsed_ms: elapsed,
216
211
  atomic_completed: true,
217
212
  };
218
213
 
219
214
  ctx.output(result, () => {
220
215
  console.log(`\n==================================================`);
221
- console.log(`✅ Shiplens SDK successfully integrated (${elapsed} ms)\n`);
222
- console.log(`📦 Project & Dashboard Information`);
223
- console.log(`Project Name: ${projectName}`);
216
+ console.log(`✅ Shiplens 数据统计 SDK 已成功安装并完成代码插桩!(耗时 ${elapsed} ms)\n`);
217
+ console.log(`📦 项目与数据看板信息`);
218
+ console.log(`项目名称: ${projectName}`);
224
219
  if (description) {
225
- console.log(`Description: ${description}`);
220
+ console.log(`项目详情: ${description}`);
226
221
  }
227
222
  if (taxonomyFormatted) {
228
- console.log(`Taxonomy:`);
223
+ console.log(`项目类别:`);
229
224
  console.log(taxonomyFormatted);
230
225
  }
231
- console.log(`App ID: ${appId}`);
232
- console.log(`Code Injection: Automatically configured in ${injectedFile} (Framework: ${framework})`);
233
- console.log(`Live Dashboard / Activation URL: 🔗 ${dashboardUrl}`);
234
- console.log(`User Account: ${accountStatusText}`);
235
- if (gitCommitResult.committed) {
236
- console.log(`Git Commit: Automatically committed changes (Commit: ${gitCommitResult.hash})`);
237
- }
238
- console.log(`🧠 AI Skill Ready: ${skillFile}`);
239
- console.log(`🔒 [SHIPLENS_ATOMIC_COMPLETED] Setup completed atomically. No extra commands needed.`);
226
+ console.log(`应用 ID (App ID): ${appId}`);
227
+ console.log(`代码注入: 已自动在入口文件 ${injectedFile} 中配置集成 (框架: ${framework})`);
228
+ console.log(`实时数据看板 / 项目激活链接: 🔗 ${dashboardUrl}`);
229
+ console.log(`用户个人账号: ${accountStatusText}`);
230
+ console.log(`🧠 AI 技能就绪: ${skillFile}`);
231
+ console.log(`🔒 [SHIPLENS_ATOMIC_COMPLETED] 所有操作已原子化闭环,无需执行 git status / doctor 等额外检查。`);
240
232
  console.log(`==================================================\n`);
241
233
 
242
234
  if (authStatus === 'magic_link_sent' && targetEmail) {
243
- console.log(`📧 Activation email sent to: ${targetEmail}`);
244
- console.log(`👉 Click the link in your email to activate your 50,000 monthly free events quota and authorize this device.`);
235
+ console.log(`📧 激活邮件已发送至: ${targetEmail}`);
236
+ console.log(`👉 请点击邮件中的链接完成注册与设备授权,即可永久激活每月 50,000 事件免费统计额度。`);
245
237
  } else {
246
238
  console.log(`> [!IMPORTANT]`);
247
- console.log(`> **Project Pending Activation (Analytics not active yet)**`);
248
- console.log(`> **Please share your email**: Click the link in the email to complete registration, activate 50,000 free monthly events (~5,000 visitors), and bind your project to your account. Once activated, you can chat directly with me to query and analyze product data.`);
239
+ console.log(`> **项目待激活(数据统计尚未生效)**`);
240
+ console.log(`> **请告诉我您的邮箱**:点击邮件中的链接即可完成注册,激活每月 50,000 事件的免费额度(约5000人访问),同时绑定项目和账号;激活后您与我直接对话,我就有权限查询并分析产品数据。`);
249
241
  }
250
242
  });
251
243
  }
@@ -39,9 +39,9 @@ function emitResponse(result) {
39
39
  }
40
40
 
41
41
  async function handleMcp(subcommand) {
42
- if (subcommand !== 'serve') throw new Error('Supported mcp subcommand: serve');
42
+ if (subcommand !== 'serve') throw new Error('支持的 mcp 子命令: serve');
43
43
  const deviceEnv = getDeviceEnv();
44
- if (!deviceEnv) throw new Error('shiplens.env not found. Run shiplens auth bind and complete email activation first.');
44
+ if (!deviceEnv) throw new Error('未找到 shiplens.env,请先执行 shiplens auth bind 并完成邮件激活');
45
45
  let buffer = '';
46
46
  let sessionId = '';
47
47
  process.stdin.setEncoding('utf8');
@@ -4,22 +4,23 @@ async function handlePages(args, flags, ctx) {
4
4
  const env = flags.env || 'production';
5
5
  const limit = parseInt(flags.limit || '10', 10);
6
6
 
7
- const resp = await ctx.client.getPages({ app_id: appId, range, env, limit });
7
+ const resp = await ctx.client.pages(appId, range, env, limit);
8
8
 
9
9
  ctx.output(resp, () => {
10
- console.log(`📄 Page Telemetry Evidence (Range: ${range}, Top ${limit}):\n`);
10
+ console.log(`📄 页面证据分析 (时间范围: ${range}, ${limit}):\n`);
11
11
  const pages = Array.isArray(resp.pages) ? resp.pages : (Array.isArray(resp) ? resp : []);
12
12
  if (pages.length === 0) {
13
- console.log('(No page data found. Verify SDK reporting.)');
13
+ console.log('(暂无页面数据,请确认 SDK 已正常上报)');
14
14
  return;
15
15
  }
16
- console.log('# | Path / Title | PV | UV | Avg Dwell(s)');
17
- console.log('--- | ---------------------------------- | ------- | ------- | ------------');
16
+ // 表格化输出
17
+ console.log('序号 | 路径/标题 | PV | UV | 平均停留(s)');
18
+ console.log('--- | ---------------------------------- | ------- | ------- | ----------');
18
19
  pages.forEach((p, i) => {
19
20
  const path = (p.path || p.template_id || '-').padEnd(34);
20
21
  const pv = String(p.pv || p.pageviews || 0).padEnd(7);
21
22
  const uv = String(p.uv || p.unique_visitors || 0).padEnd(7);
22
- const dur = String(p.avg_duration_s || p.avg_session_duration_s || 0).padEnd(12);
23
+ const dur = String(p.avg_duration_s || p.avg_session_duration_s || 0).padEnd(10);
23
24
  console.log(`[${String(i + 1).padEnd(2)}] | ${path} | ${pv} | ${uv} | ${dur}`);
24
25
  });
25
26
  });
@@ -30,27 +31,30 @@ async function handlePaths(args, flags, ctx) {
30
31
  const range = flags.range || '7d';
31
32
  const env = flags.env || 'production';
32
33
 
33
- const resp = await ctx.client.queryPaths({ app_id: appId, range, env });
34
+ const resp = await ctx.client.paths(appId, range, env);
34
35
 
35
36
  ctx.output(resp, () => {
36
- console.log(`🔀 User Flow & Path Analysis (Range: ${range}):\n`);
37
+ console.log(`🔀 页面流转路径分析 (时间范围: ${range}):\n`);
37
38
 
39
+ // 入口页面
38
40
  if (resp.entry_pages && resp.entry_pages.length > 0) {
39
- console.log('▶ Top Entry Pages:');
41
+ console.log('▶ 主要入口页面:');
40
42
  resp.entry_pages.slice(0, 5).forEach((e, i) =>
41
- console.log(` [${i + 1}] ${e.path || e.page} - ${e.sessions || e.count || 0} sessions`)
43
+ console.log(` [${i + 1}] ${e.path || e.page} ${e.sessions || e.count || 0} 次会话`)
42
44
  );
43
45
  }
46
+ // 退出页面
44
47
  if (resp.exit_pages && resp.exit_pages.length > 0) {
45
- console.log('\n◀ Top Exit Pages:');
48
+ console.log('\n◀ 主要退出页面:');
46
49
  resp.exit_pages.slice(0, 5).forEach((e, i) =>
47
- console.log(` [${i + 1}] ${e.path || e.page} - Exit Rate ${((e.exit_rate || 0) * 100).toFixed(1)}%`)
50
+ console.log(` [${i + 1}] ${e.path || e.page} 退出率 ${((e.exit_rate || 0) * 100).toFixed(1)}%`)
48
51
  );
49
52
  }
53
+ // 路径流转
50
54
  if (resp.paths && resp.paths.length > 0) {
51
- console.log('\n↔ High-Frequency Paths:');
55
+ console.log('\n↔ 高频页面流转路径:');
52
56
  resp.paths.slice(0, 8).forEach((p, i) =>
53
- console.log(` [${i + 1}] ${p.from || p.source} → ${p.to || p.target} (${p.count || p.sessions || 0} times)`)
57
+ console.log(` [${i + 1}] ${p.from || p.source} → ${p.to || p.target} (${p.count || p.sessions || 0} )`)
54
58
  );
55
59
  } else {
56
60
  console.log(JSON.stringify(resp, null, 2));
@@ -63,18 +67,18 @@ async function handleCanvas(args, flags, ctx) {
63
67
  const range = flags.range || '7d';
64
68
  const env = flags.env || 'production';
65
69
 
66
- const resp = await ctx.client.queryPaths({ app_id: appId, range, env, mode: 'canvas' });
70
+ const resp = await ctx.client.behaviorCanvas(appId, range, env);
67
71
 
68
72
  ctx.output(resp, () => {
69
- console.log(`🎨 User Behavior Canvas Topology (Range: ${range}):\n`);
73
+ console.log(`🎨 用户行为画布拓扑 (时间范围: ${range}):\n`);
70
74
  const nodeCount = (resp.nodes || []).length;
71
75
  const edgeCount = (resp.edges || resp.links || []).length;
72
76
  if (nodeCount > 0 || edgeCount > 0) {
73
- console.log(`📍 ${nodeCount} nodes, ${edgeCount} edges`);
77
+ console.log(`📍 ${nodeCount} 个节点, ${edgeCount} 条边`);
74
78
  if (resp.nodes && resp.nodes.length > 0) {
75
- console.log('\nMain Nodes:');
79
+ console.log('\n主要节点:');
76
80
  resp.nodes.slice(0, 10).forEach((n, i) =>
77
- console.log(` [${i + 1}] ${n.label || n.path || n.id} (Visits: ${n.value || n.count || 0})`)
81
+ console.log(` [${i + 1}] ${n.label || n.path || n.id} (访问量: ${n.value || n.count || 0})`)
78
82
  );
79
83
  }
80
84
  } else {
@@ -9,19 +9,19 @@ async function handleProjects(subcommand, args, flags, ctx) {
9
9
  ctx.output(resp, () => {
10
10
  const projects = resp.projects || [];
11
11
  if (projects.length === 0) {
12
- console.log('No associated projects found. Use shiplens init to create a new project.');
12
+ console.log('暂无关联项目。可使用 shiplens init 初始化一个新项目。');
13
13
  return;
14
14
  }
15
- console.log(`📦 Found ${projects.length} project(s):\n`);
15
+ console.log(`📦 共有 ${projects.length} 个项目:\n`);
16
16
  projects.forEach((p, i) => {
17
17
  console.log(`[${i + 1}] ${p.project_name || p.app_name || 'Untitled'} (${p.app_id})`);
18
- if (p.description) console.log(` Description: ${p.description}`);
18
+ if (p.description) console.log(` 简介: ${p.description}`);
19
19
  if (p.taxonomy?.subgenre?.name) {
20
20
  const tags = (p.taxonomy.feature_tags || []).map((tag) => tag.name).join(', ');
21
- console.log(` Taxonomy: ${p.taxonomy.genre?.name || '-'} / ${p.taxonomy.subgenre.name}${tags ? ` - ${tags}` : ''}`);
21
+ console.log(` 分类: ${p.taxonomy.genre?.name || '-'} / ${p.taxonomy.subgenre.name}${tags ? ` · ${tags}` : ''}`);
22
22
  }
23
- console.log(` Industry: ${p.industry || '-'} | Status: ${p.status || 'active'} | Schema Timestamp: ${p.schema_last_modified_at || 0}`);
24
- if (p.created_at) console.log(` Created: ${p.created_at}`);
23
+ console.log(` 行业: ${p.industry || '-'} | 状态: ${p.status || 'active'} | Schema更新时间戳: ${p.schema_last_modified_at || 0}`);
24
+ if (p.created_at) console.log(` 创建时间: ${p.created_at}`);
25
25
  console.log('');
26
26
  });
27
27
  });
@@ -32,7 +32,7 @@ async function handleProjects(subcommand, args, flags, ctx) {
32
32
  const appId = ctx.resolveAppId();
33
33
  const res = await ctx.client.bindProject(appId, flags.name);
34
34
  ctx.output(res, () => {
35
- console.log(`✅ Project ${appId} bound to current account successfully.`);
35
+ console.log(`✅ 项目 ${appId} 已成功绑定至当前账号!`);
36
36
  });
37
37
  break;
38
38
  }
@@ -40,36 +40,36 @@ async function handleProjects(subcommand, args, flags, ctx) {
40
40
  case 'delete': {
41
41
  const appId = ctx.resolveAppId();
42
42
  if (!flags.force) {
43
- console.log(`⚠️ Warning: Deleting project ${appId} and all historical event data. This action cannot be undone!`);
43
+ console.log(`⚠️ 警告:正在删除项目 ${appId} 及其全部历史事件数据。该操作不可逆!`);
44
44
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
45
45
  const confirmed = await new Promise((resolve) => {
46
- rl.question(`Enter App ID or 'yes' to confirm deletion: `, (ans) => {
46
+ rl.question(`请输入项目 ID 'yes' 确认删除: `, (ans) => {
47
47
  rl.close();
48
48
  resolve(ans.trim() === 'yes' || ans.trim() === appId);
49
49
  });
50
50
  });
51
51
  if (!confirmed) {
52
- console.log('🛑 Operation cancelled.');
52
+ console.log('🛑 操作已取消。');
53
53
  return;
54
54
  }
55
55
  }
56
56
 
57
57
  const res = await ctx.client.deleteProject(appId);
58
58
 
59
- // Clean up local business context file
59
+ // 清理本地专属业务上下文文件
60
60
  const localContextPath = path.join(process.cwd(), '.shiplens', 'contexts', `${appId}.md`);
61
61
  if (fs.existsSync(localContextPath)) {
62
62
  try { fs.unlinkSync(localContextPath); } catch {}
63
63
  }
64
64
 
65
65
  ctx.output(res, () => {
66
- console.log(`🗑️ Project ${appId} deleted successfully.`);
66
+ console.log(`🗑️ 项目 ${appId} 已被成功删除。`);
67
67
  });
68
68
  break;
69
69
  }
70
70
 
71
71
  default:
72
- throw new Error(`Unknown projects subcommand: ${subcommand}. Supported: list, bind, delete`);
72
+ throw new Error(`未知的 projects 子命令: ${subcommand}。支持 list, bind, delete`);
73
73
  }
74
74
  }
75
75
 
@@ -11,28 +11,29 @@ async function handleQuery(args, flags, ctx) {
11
11
 
12
12
  let queryReq;
13
13
 
14
- // Support --file: read full AnalyticsQueryRequest from JSON file
14
+ // 支持 --file:从 JSON 文件读取完整 AnalyticsQueryRequest,最大灵活度
15
15
  if (flags.file) {
16
16
  if (!fs.existsSync(flags.file)) {
17
- const err = new Error(`Query config file not found: ${flags.file}`);
17
+ const err = new Error(`查询配置文件不存在: ${flags.file}`);
18
18
  err.code = 'INVALID_ARGS';
19
19
  throw err;
20
20
  }
21
21
  try {
22
22
  queryReq = JSON.parse(fs.readFileSync(flags.file, 'utf8'));
23
23
  } catch (e) {
24
- const err = new Error(`Failed to parse query config file (${flags.file}): ${e.message}`);
24
+ const err = new Error(`无法解析查询配置文件 (${flags.file}): ${e.message}`);
25
25
  err.code = 'INVALID_ARGS';
26
26
  throw err;
27
27
  }
28
+ // 允许用命令行 range / env 覆盖文件中的值
28
29
  if (!queryReq.range) queryReq.range = range;
29
30
  if (!queryReq.env) queryReq.env = env;
30
31
  } else {
31
- // Support --metrics (comma-separated) or --metric
32
+ // 支持 --metrics 多指标(逗号分隔)或单指标 --metric
32
33
  const metricsRaw = flags.metrics || flags.metric || 'pageviews';
33
34
  const metricList = String(metricsRaw).split(',').map((m) => m.trim()).filter(Boolean);
34
35
 
35
- // Parse --filter key=val
36
+ // 解析 --filter key=val(可多次指定)
36
37
  const filters = {};
37
38
  const filterList = Array.isArray(flags.filter)
38
39
  ? flags.filter
@@ -42,12 +43,13 @@ async function handleQuery(args, flags, ctx) {
42
43
  if (k && v.length > 0) filters[k.trim()] = v.join('=').trim();
43
44
  }
44
45
 
46
+ // 每个指标生成一个 panel
45
47
  queryReq = {
46
48
  env,
47
49
  range,
48
50
  panels: metricList.map((metric, idx) => ({
49
51
  panel_id: `panel_cli_${idx}`,
50
- title: `${metric} Trend`,
52
+ title: `${metric} 趋势`,
51
53
  metric,
52
54
  chart_type: chartType,
53
55
  time_grain: grain,
@@ -58,22 +60,22 @@ async function handleQuery(args, flags, ctx) {
58
60
  };
59
61
  }
60
62
 
61
- const resp = await ctx.client.query(appId, queryReq);
63
+ const resp = await ctx.client.queryAnalytics(appId, queryReq);
62
64
 
63
65
  ctx.output(resp, () => {
64
66
  const metricNames = (queryReq.panels || []).map((p) => p.metric).join(', ');
65
- console.log(`📊 Query Metrics: ${metricNames} | Range: ${queryReq.range}`);
67
+ console.log(`📊 查询指标: ${metricNames} | 时间范围: ${queryReq.range}`);
66
68
  if (resp.data && resp.data.length > 0) {
67
69
  resp.data.forEach((row) => console.log('•', JSON.stringify(row)));
68
70
  } else if (resp.panels && resp.panels.length > 0) {
69
71
  resp.panels.forEach((p) => {
70
- console.log(`\n📈 Panel: ${p.title || p.panel_id}`);
72
+ console.log(`\n📈 面板: ${p.title || p.panel_id}`);
71
73
  if (Array.isArray(p.data)) {
72
74
  p.data.forEach((row) => console.log(' •', JSON.stringify(row)));
73
75
  }
74
76
  });
75
77
  } else {
76
- console.log('(No matching data found)');
78
+ console.log('(暂无匹配数据)');
77
79
  }
78
80
  });
79
81
  }
@@ -2,7 +2,7 @@ async function handleSQL(args, flags, ctx) {
2
2
  const appId = ctx.resolveAppId();
3
3
  let queryStr = flags.query || flags.sql || args.join(' ');
4
4
 
5
- // Support --stdin for piped SQL execution
5
+ // 支持 --stdin:从标准输入读取 SQL(兼容管道,如 echo "SELECT..." | shiplens sql --stdin)
6
6
  if (flags.stdin || flags['read-stdin']) {
7
7
  const chunks = [];
8
8
  for await (const chunk of process.stdin) {
@@ -13,18 +13,19 @@ async function handleSQL(args, flags, ctx) {
13
13
  }
14
14
 
15
15
  if (!queryStr) {
16
- const err = new Error('SQL query string cannot be empty. Pass --query "SELECT ..." or pipe via --stdin.');
16
+ const err = new Error('SQL 查询语句不能为空,请使用 --query "SELECT ..." 或通过管道传入 --stdin');
17
17
  err.code = 'INVALID_SQL';
18
18
  throw err;
19
19
  }
20
20
 
21
- const resp = await ctx.client.executeSQL({ app_id: appId, query: queryStr });
21
+ const resp = await ctx.client.executeSQL(appId, queryStr);
22
22
 
23
23
  ctx.output(resp, () => {
24
24
  const rowCount = resp.row_count !== undefined ? resp.row_count : (resp.rows ? resp.rows.length : 0);
25
- console.log(`🔍 SQL executed successfully (${resp.elapsed_ms || 0} ms, ${rowCount} rows returned):\n`);
25
+ console.log(`🔍 SQL 执行成功 (耗时: ${resp.elapsed_ms || 0} ms, 返回 ${rowCount} ):\n`);
26
26
 
27
27
  if (resp.columns && resp.columns.length > 0) {
28
+ // 计算每列宽度,对齐输出
28
29
  const colWidths = resp.columns.map((col) => {
29
30
  let maxW = col.length;
30
31
  if (resp.rows) {
@@ -6,16 +6,16 @@ async function handleSummary(args, flags, ctx) {
6
6
  const resp = await ctx.client.summary(appId, range, env);
7
7
 
8
8
  ctx.output(resp, () => {
9
- console.log(`📈 Core Product Summary (Range: ${range}):`);
10
- console.log(`• Total Pageviews (PV): ${resp.total_pv || 0}`);
11
- console.log(`• Unique Visitors (UV): ${resp.total_uv || 0}`);
12
- console.log(`• Avg Session Duration: ${resp.avg_session_duration_s || 0} s`);
13
- console.log(`• Bounce Rate: ${((resp.bounce_rate || 0) * 100).toFixed(1)}%`);
9
+ console.log(`📈 产品核心数据摘要 (时间范围: ${range}):`);
10
+ console.log(`• 总浏览量 (PV): ${resp.total_pv || 0}`);
11
+ console.log(`• 独立访客 (UV): ${resp.total_uv || 0}`);
12
+ console.log(`• 平均会话时长: ${resp.avg_session_duration_s || 0} 秒`);
13
+ console.log(`• 跳出率: ${((resp.bounce_rate || 0) * 100).toFixed(1)}%`);
14
14
  if (resp.top_countries && resp.top_countries.length > 0) {
15
- console.log('• Top Countries:', resp.top_countries);
15
+ console.log('• Top 国家/地区:', resp.top_countries);
16
16
  }
17
17
  if (resp.top_devices && resp.top_devices.length > 0) {
18
- console.log('• Top Devices:', resp.top_devices);
18
+ console.log('• Top 访问设备:', resp.top_devices);
19
19
  }
20
20
  });
21
21
  }