@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.
@@ -11,17 +11,17 @@ async function handleAuth(subcommand, args, flags, ctx) {
11
11
  const deviceEnv = getDeviceEnv();
12
12
  if (deviceEnv) {
13
13
  return ctx.output({ ok: true, authenticated: false, device_env: 'present', mcp_url: deviceEnv.SHIPLENS_MCP_URL, next: 'Use shiplens auth mcp-config, then complete OAuth in your Agent.' }, () => {
14
- console.log(`✅ shiplens.env detected; MCP: ${deviceEnv.SHIPLENS_MCP_URL}`);
15
- console.log('Complete OAuth login in your Agent before using MCP.');
14
+ console.log(`✅ 已发现 shiplens.envMCP: ${deviceEnv.SHIPLENS_MCP_URL}`);
15
+ console.log('请在 Agent 中完成 OAuth 登录后使用 MCP');
16
16
  });
17
17
  }
18
18
  if (!ctx.resolvedAuth.is_present) {
19
19
  return ctx.output({
20
20
  ok: false,
21
21
  authenticated: false,
22
- message: 'No credentials detected. Configure via shiplens auth set.',
22
+ message: '未检测到鉴权密钥,请使用 shiplens auth set 配置',
23
23
  }, () => {
24
- console.log('⚠️ No valid credentials configured. Run: shiplens auth set');
24
+ console.log('⚠️ 当前未配置有效凭证。请执行: shiplens auth set');
25
25
  });
26
26
  }
27
27
 
@@ -36,9 +36,9 @@ async function handleAuth(subcommand, args, flags, ctx) {
36
36
  email: meResp.email,
37
37
  last_verified_at: new Date().toISOString(),
38
38
  }, () => {
39
- console.log('✅ Authentication valid');
40
- console.log(`👤 User ID: ${meResp.user_id} | Email: ${meResp.email}`);
41
- console.log(`🔑 Secret Source: ${ctx.resolvedAuth.source} (${ctx.resolvedAuth.masked_secret})`);
39
+ console.log('✅ 鉴权状态有效');
40
+ console.log(`👤 用户 ID: ${meResp.user_id} | 邮箱: ${meResp.email}`);
41
+ console.log(`🔑 密钥来源: ${ctx.resolvedAuth.source} (${ctx.resolvedAuth.masked_secret})`);
42
42
  });
43
43
  } catch (err) {
44
44
  ctx.output({
@@ -48,7 +48,7 @@ async function handleAuth(subcommand, args, flags, ctx) {
48
48
  masked_secret: ctx.resolvedAuth.masked_secret,
49
49
  error: err.message,
50
50
  }, () => {
51
- console.log(`❌ Credential verification failed: ${err.message}`);
51
+ console.log(`❌ 凭证校验失败: ${err.message}`);
52
52
  });
53
53
  }
54
54
  break;
@@ -56,9 +56,9 @@ async function handleAuth(subcommand, args, flags, ctx) {
56
56
 
57
57
  case 'mcp-config': {
58
58
  const deviceEnv = getDeviceEnv();
59
- if (!deviceEnv) throw new Error('shiplens.env not found. Run shiplens auth bind and complete email activation first.');
59
+ if (!deviceEnv) throw new Error('未找到 shiplens.env,请先执行 shiplens auth bind 并完成邮件激活');
60
60
  const client = flags.client || 'manual';
61
- if (!CLIENTS.includes(client)) throw new Error(`Unsupported MCP client: ${client}. Supported: ${CLIENTS.join(', ')}`);
61
+ if (!CLIENTS.includes(client)) throw new Error(`不支持的 MCP 客户端: ${client}。支持 ${CLIENTS.join(', ')}`);
62
62
  const config = getMcpConfig(client, deviceEnv.SHIPLENS_MCP_URL);
63
63
  ctx.output({ ok: true, mcp_config: config, oauth_required: true }, () => console.log(JSON.stringify(config, null, 2)));
64
64
  break;
@@ -66,17 +66,17 @@ async function handleAuth(subcommand, args, flags, ctx) {
66
66
 
67
67
  case 'configure': {
68
68
  const deviceEnv = getDeviceEnv();
69
- if (!deviceEnv) throw new Error('shiplens.env not found. Run shiplens auth bind and complete email activation first.');
69
+ if (!deviceEnv) throw new Error('未找到 shiplens.env,请先执行 shiplens auth bind 并完成邮件激活');
70
70
  const client = flags.client || args[0];
71
- if (!CLIENTS.includes(client)) throw new Error(`Specify target MCP client via --client: ${CLIENTS.join(', ')}`);
71
+ if (!CLIENTS.includes(client)) throw new Error(`请通过 --client 指定 MCP 客户端:${CLIENTS.join(', ')}`);
72
72
  const result = configureMcpClient(client, deviceEnv.SHIPLENS_MCP_URL);
73
73
  ctx.output({ ok: true, client, oauth_required: true, ...result }, () => {
74
74
  if (client === 'manual') {
75
75
  console.log(JSON.stringify(result.config, null, 2));
76
76
  return;
77
77
  }
78
- console.log(result.written ? `✅ Shiplens MCP configuration written to ${client}.` : 'ℹ️ Shiplens MCP config already exists, unchanged.');
79
- console.log('Complete OAuth login in your Agent MCP panel.');
78
+ console.log(result.written ? `✅ 已写入 ${client} 的 Shiplens MCP 配置。` : 'ℹ️ Shiplens MCP 配置已存在,未覆盖。');
79
+ console.log('请在 Agent MCP 面板中完成 OAuth 登录。');
80
80
  });
81
81
  break;
82
82
  }
@@ -96,7 +96,7 @@ async function handleAuth(subcommand, args, flags, ctx) {
96
96
  } else {
97
97
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
98
98
  secret = await new Promise((resolve) => {
99
- rl.question('Enter Shiplens Access Secret: ', (ans) => {
99
+ rl.question('请输入 Shiplens Access Secret: ', (ans) => {
100
100
  rl.close();
101
101
  resolve(ans.trim());
102
102
  });
@@ -104,7 +104,7 @@ async function handleAuth(subcommand, args, flags, ctx) {
104
104
  }
105
105
 
106
106
  if (!secret) {
107
- throw new Error('Access Secret cannot be empty.');
107
+ throw new Error('密钥不能为空');
108
108
  }
109
109
 
110
110
  const tempClient = new APIClient(ctx.client.baseURL, secret);
@@ -123,17 +123,17 @@ async function handleAuth(subcommand, args, flags, ctx) {
123
123
  masked_secret: maskSecret(secret),
124
124
  email: email || undefined,
125
125
  user_id: userId || undefined,
126
- message: 'Access Secret saved securely to ~/.shiplens/config.json',
126
+ message: '鉴权密钥已安全保存至 ~/.shiplens/config.json',
127
127
  }, () => {
128
- console.log(`✅ Access Secret saved: ${maskSecret(secret)}`);
129
- if (email) console.log(`👤 Bound User: ${email} (${userId})`);
128
+ console.log(`✅ 鉴权密钥已保存: ${maskSecret(secret)}`);
129
+ if (email) console.log(`👤 绑定用户: ${email} (${userId})`);
130
130
  });
131
131
  break;
132
132
  }
133
133
 
134
134
  case 'secret': {
135
135
  const action = args[0] || 'list';
136
- if (!ctx.resolvedAuth.is_present) throw new Error('Run auth set first to configure an Access Secret with administrative rights.');
136
+ if (!ctx.resolvedAuth.is_present) throw new Error('请先用 auth set 配置可管理 Access Secret');
137
137
  if (action === 'list') {
138
138
  const response = await ctx.client.listAccessSecrets();
139
139
  return ctx.output({ ok: true, access_secrets: response.api_keys || [] }, () => console.log(JSON.stringify(response.api_keys || [], null, 2)));
@@ -143,25 +143,25 @@ async function handleAuth(subcommand, args, flags, ctx) {
143
143
  const response = await ctx.client.createAccessSecret({ app_id: flags['app-id'], scopes, expires_at: flags.expires });
144
144
  saveSecretToGlobal(response.api_key);
145
145
  return ctx.output({ ok: true, api_key_id: response.api_key_id, api_key_preview: response.api_key_preview, app_id: response.app_id, scopes: response.scopes, expires_at: response.expires_at, saved_to_global_config: true }, () => {
146
- console.log(`✅ Offline Access Secret created and saved (${response.api_key_preview}).`);
146
+ console.log(`✅ 离线 Access Secret 已创建并保存(${response.api_key_preview})。`);
147
147
  });
148
148
  }
149
149
  if (action === 'revoke') {
150
150
  const apiKeyId = flags['key-id'] || args[1];
151
- if (!apiKeyId) throw new Error('Provide --key-id <api_key_id>.');
151
+ if (!apiKeyId) throw new Error('请提供 --key-id <api_key_id>');
152
152
  const response = await ctx.client.revokeAccessSecret(apiKeyId);
153
- return ctx.output({ ok: true, ...response }, () => console.log(`✅ Revoked Access Secret: ${response.api_key_id}`));
153
+ return ctx.output({ ok: true, ...response }, () => console.log(`✅ 已撤销 Access Secret: ${response.api_key_id}`));
154
154
  }
155
- throw new Error('Supported auth secret subcommands: list, create, revoke');
155
+ throw new Error('支持的 auth secret 子命令: list, create, revoke');
156
156
  }
157
157
 
158
158
  case 'whoami': {
159
159
  const meResp = await ctx.client.me();
160
160
  ctx.output(meResp, () => {
161
- console.log(`👤 User ID: ${meResp.user_id}`);
162
- console.log(`📧 Email: ${meResp.email}`);
161
+ console.log(`👤 用户 ID: ${meResp.user_id}`);
162
+ console.log(`📧 邮箱: ${meResp.email}`);
163
163
  if (meResp.project_count) {
164
- console.log(`📦 Projects Count: ${meResp.project_count}`);
164
+ console.log(`📦 拥有项目数: ${meResp.project_count}`);
165
165
  }
166
166
  });
167
167
  break;
@@ -171,17 +171,21 @@ async function handleAuth(subcommand, args, flags, ctx) {
171
171
  clearGlobalSecret();
172
172
  ctx.output({
173
173
  ok: true,
174
- message: 'Logged out successfully. Local credentials cleared.',
174
+ message: '已成功退出登录并清除本地存储凭证',
175
175
  }, () => {
176
- console.log('🚪 Logged out successfully. Local credentials cleared.');
176
+ console.log('🚪 已退出登录并清除本地凭证。');
177
177
  });
178
178
  break;
179
179
  }
180
180
 
181
181
  case 'bind': {
182
+ // ── auth bind:零弹窗邮箱绑定 ──────────────────────────────────────────
183
+ // 调用方式:shiplens auth bind --email <email> --json
184
+ // 自动从 .shiplens.json 读取 app_id / project_name / api_url
185
+ // 调用 POST /api/auth/email/start 发送 Magic Link,最多重试 2 次
182
186
  const email = flags.email || args[0];
183
187
  if (!email || !email.includes('@')) {
184
- const err = new Error('Provide a valid email address, e.g.: shiplens auth bind --email you@company.com');
188
+ const err = new Error('请提供有效的邮箱地址,例如:shiplens auth bind --email you@company.com');
185
189
  err.code = 'INVALID_EMAIL';
186
190
  throw err;
187
191
  }
@@ -202,6 +206,7 @@ async function handleAuth(subcommand, args, flags, ctx) {
202
206
  device_name: require('os').hostname(),
203
207
  });
204
208
  } catch (err) {
209
+ // 网络 / 服务端失败时返回标准错误 JSON,不挂起
205
210
  ctx.output({
206
211
  ok: false,
207
212
  magic_link_sent: false,
@@ -210,7 +215,7 @@ async function handleAuth(subcommand, args, flags, ctx) {
210
215
  code: err.code || 'NETWORK_FAILED',
211
216
  message: err.message,
212
217
  }, () => {
213
- console.error(`❌ Failed to send email [${err.code || 'NETWORK_FAILED'}]: ${err.message}`);
218
+ console.error(`❌ 邮件发送失败 [${err.code || 'NETWORK_FAILED'}]: ${err.message}`);
214
219
  });
215
220
  process.exitCode = 1;
216
221
  return;
@@ -225,17 +230,17 @@ async function handleAuth(subcommand, args, flags, ctx) {
225
230
  app_id: appId || undefined,
226
231
  project_name: projectName || undefined,
227
232
  shiplens_env_written: Boolean(envPath),
228
- message: `Activation email sent to ${email}. Click the link to complete registration.`,
233
+ message: `激活邮件已发送至 ${email},请点击邮件中的链接完成注册与激活。`,
229
234
  }, () => {
230
- console.log(`📧 Activation email sent to: ${email}`);
231
- if (appId) console.log(`📦 Bound App ID: ${appId}`);
232
- console.log('👉 Click the link in your email to activate your 50,000 monthly free events quota.');
235
+ console.log(`📧 激活邮件已发送至: ${email}`);
236
+ if (appId) console.log(`📦 绑定项目 ID: ${appId}`);
237
+ console.log('👉 请点击邮件中的链接完成注册,每月可获得 50,000 事件免费统计额度。');
233
238
  });
234
239
  break;
235
240
  }
236
241
 
237
242
  default:
238
- throw new Error(`Unknown auth subcommand: ${subcommand}. Supported: status, set, secret, whoami, logout, bind, mcp-config, configure`);
243
+ throw new Error(`未知的 auth 子命令: ${subcommand}。支持 status, set, secret, whoami, logout, bind, mcp-config, configure`);
239
244
  }
240
245
  }
241
246
 
@@ -3,7 +3,7 @@ const path = require('path');
3
3
  const { getLocalConfig } = require('../config');
4
4
 
5
5
  /**
6
- * Get standard storage path for business context: .shiplens/contexts/<app_id>.md
6
+ * 获取指定 app_id 的业务上下文标准存储路径: .shiplens/contexts/<app_id>.md
7
7
  */
8
8
  function getContextFilePath(appId, customFile = null, dir = process.cwd()) {
9
9
  if (customFile) return path.resolve(dir, customFile);
@@ -11,7 +11,7 @@ function getContextFilePath(appId, customFile = null, dir = process.cwd()) {
11
11
  }
12
12
 
13
13
  /**
14
- * Extract declared app_id from Markdown header
14
+ * Markdown 内容中提取头部声明的 app_id
15
15
  */
16
16
  function extractAppIdFromMarkdown(content) {
17
17
  const match = content.match(/app_id[^\w\n]*[:=]\s*([a-zA-Z0-9_.\-]+)/i);
@@ -19,7 +19,7 @@ function extractAppIdFromMarkdown(content) {
19
19
  }
20
20
 
21
21
  /**
22
- * Network executor with up to 2 retries and graceful fallback
22
+ * 带有最多 2 次重试与优雅跳过的网络执行器 (非阻塞容错)
23
23
  */
24
24
  async function executeWithSafeFallback(fn, maxRetries = 2, baseMs = 500) {
25
25
  let lastErr = null;
@@ -37,7 +37,7 @@ async function executeWithSafeFallback(fn, maxRetries = 2, baseMs = 500) {
37
37
  }
38
38
 
39
39
  /**
40
- * Handle context command (push, pull, show)
40
+ * 处理 context 命令 (push, pull, show)
41
41
  */
42
42
  async function handleContext(subcommand, args, flags, ctx) {
43
43
  const targetSub = subcommand || 'show';
@@ -50,7 +50,7 @@ async function handleContext(subcommand, args, flags, ctx) {
50
50
  case 'push': {
51
51
  if (!fs.existsSync(filePath)) {
52
52
  const displayPath = path.relative(process.cwd(), filePath) || filePath;
53
- const err = new Error(`Local context file not found: ${displayPath}. Generate business context first.`);
53
+ const err = new Error(`未找到本地上下文文件: ${displayPath}。请先生成对应产品的业务上下文。`);
54
54
  err.code = 'FILE_NOT_FOUND';
55
55
  throw err;
56
56
  }
@@ -58,12 +58,14 @@ async function handleContext(subcommand, args, flags, ctx) {
58
58
  const content = fs.readFileSync(filePath, 'utf8');
59
59
  const declaredAppId = extractAppIdFromMarkdown(content);
60
60
 
61
+ // 多项目防冲突校验:若文件中显式声明了 app_id,必须与当前目标 app_id 一致
61
62
  if (declaredAppId && declaredAppId !== appId) {
62
- const err = new Error(`Context file app_id (${declaredAppId}) does not match current target app_id (${appId}). Push rejected.`);
63
+ const err = new Error(`上下文文件绑定的 app_id (${declaredAppId}) 与当前项目 app_id (${appId}) 不一致,禁止推送!请修正文件头部声明。`);
63
64
  err.code = 'PROJECT_ID_MISMATCH';
64
65
  throw err;
65
66
  }
66
67
 
68
+ // 最多重试 2 次,失败则优雅跳过,不阻断流程
67
69
  const syncResult = await executeWithSafeFallback(async () => {
68
70
  return ctx.client.uploadProjectContext(appId, {
69
71
  context_markdown: content,
@@ -78,7 +80,7 @@ async function handleContext(subcommand, args, flags, ctx) {
78
80
  action: 'push',
79
81
  app_id: appId,
80
82
  file: filePath,
81
- message: 'Server connection timed out (2 attempts). Skipped cloud sync without blocking.',
83
+ message: '连接服务器超时或未成功 (已尝试2),已自动跳过云端同步步骤,暂未上传业务上下文。',
82
84
  });
83
85
  return;
84
86
  }
@@ -89,7 +91,7 @@ async function handleContext(subcommand, args, flags, ctx) {
89
91
  app_id: appId,
90
92
  file: filePath,
91
93
  bytes: Buffer.byteLength(content, 'utf8'),
92
- message: `Project business context (${path.relative(process.cwd(), filePath)}) synced to cloud successfully.`,
94
+ message: `项目业务上下文 (${path.relative(process.cwd(), filePath)}) 已成功同步至云端。`,
93
95
  updated_at: syncResult.result.updated_at || new Date().toISOString(),
94
96
  });
95
97
  break;
@@ -98,11 +100,12 @@ async function handleContext(subcommand, args, flags, ctx) {
98
100
  case 'pull': {
99
101
  if (fs.existsSync(filePath) && !flags.force) {
100
102
  const displayPath = path.relative(process.cwd(), filePath) || filePath;
101
- const err = new Error(`Local file ${displayPath} already exists. Pass --force to overwrite.`);
103
+ const err = new Error(`本地已存在 ${displayPath}。若需强制从云端覆盖请追加 --force 参数。`);
102
104
  err.code = 'FILE_EXISTS';
103
105
  throw err;
104
106
  }
105
107
 
108
+ // 最多重试 2 次,失败则优雅跳过,不阻断流程
106
109
  const pullResult = await executeWithSafeFallback(async () => {
107
110
  return ctx.client.getProjectContext(appId);
108
111
  }, 2, 500);
@@ -113,7 +116,7 @@ async function handleContext(subcommand, args, flags, ctx) {
113
116
  skipped: true,
114
117
  action: 'pull',
115
118
  app_id: appId,
116
- message: 'Server connection timed out (2 attempts). Skipped cloud pull without blocking.',
119
+ message: '连接服务器超时或未成功 (已尝试2),已自动跳过云端拉取步骤,暂未下载业务上下文。',
117
120
  });
118
121
  return;
119
122
  }
@@ -127,7 +130,7 @@ async function handleContext(subcommand, args, flags, ctx) {
127
130
  app_id: appId,
128
131
  file: filePath,
129
132
  bytes: Buffer.byteLength(pullResult.result.context_markdown, 'utf8'),
130
- message: `Business context pulled and saved to ${path.relative(process.cwd(), filePath)}.`,
133
+ message: `已从云端成功拉取业务上下文并保存至 ${path.relative(process.cwd(), filePath)}。`,
131
134
  updated_at: pullResult.result.updated_at,
132
135
  });
133
136
  break;
@@ -137,10 +140,12 @@ async function handleContext(subcommand, args, flags, ctx) {
137
140
  let content = null;
138
141
  let source = 'local';
139
142
 
143
+ // 若未加 --cloud 且本地文件存在,优先读取本地
140
144
  if (!flags.cloud && fs.existsSync(filePath)) {
141
145
  content = fs.readFileSync(filePath, 'utf8');
142
146
  source = 'local';
143
147
  } else {
148
+ // 本地不存在或指定 --cloud 时,从云端拉取 (最多尝试 2 次)
144
149
  const fetchResult = await executeWithSafeFallback(async () => {
145
150
  return ctx.client.getProjectContext(appId);
146
151
  }, 2, 500);
@@ -157,7 +162,7 @@ async function handleContext(subcommand, args, flags, ctx) {
157
162
  ok: true,
158
163
  app_id: appId,
159
164
  source: 'none',
160
- message: `No context found locally (${displayPath}) or in cloud. Server lookup skipped after timeout.`,
165
+ message: `本地 (${displayPath}) 与云端暂无该产品的业务上下文。连接服务器超时或未成功 (已尝试2次),已跳过云端查询。`,
161
166
  });
162
167
  return;
163
168
  }
@@ -173,7 +178,7 @@ async function handleContext(subcommand, args, flags, ctx) {
173
178
  }
174
179
 
175
180
  default:
176
- throw new Error(`Unknown context subcommand: ${targetSub}. Supported: push, pull, show`);
181
+ throw new Error(`未知的 context 子命令: ${targetSub}。支持: push, pull, show`);
177
182
  }
178
183
  }
179
184
 
@@ -10,14 +10,14 @@ async function handleDashboards(subcommand, args, flags, ctx) {
10
10
  dashboards: list,
11
11
  }, () => {
12
12
  if (!list || list.length === 0) {
13
- console.log('No custom dashboards found. Create one using shiplens dashboards create.');
13
+ console.log('暂无自定义看板。可以使用 shiplens dashboards create 创建。');
14
14
  return;
15
15
  }
16
- console.log(`📊 Found ${list.length} dashboard(s):\n`);
16
+ console.log(`📊 当前项目共有 ${list.length} 个看板:\n`);
17
17
  list.forEach((d, i) => {
18
- const defTag = d.is_default ? ' [Default]' : '';
18
+ const defTag = d.is_default ? ' [默认]' : '';
19
19
  console.log(`[${i + 1}] ${d.title} (ID: ${d.dashboard_id})${defTag}`);
20
- if (d.url) console.log(` URL: ${d.url}`);
20
+ if (d.url) console.log(` 看板链接: ${d.url}`);
21
21
  console.log('');
22
22
  });
23
23
  });
@@ -25,16 +25,23 @@ async function handleDashboards(subcommand, args, flags, ctx) {
25
25
  }
26
26
 
27
27
  case 'create': {
28
+ // ── 支持两种模式 ──────────────────────────────────────────────────────
29
+ // 模式 A(默认):POST /api/apps/{app_id}/dashboards
30
+ // 适用:传入 widgets / query 结构体,支持 --file 与 --prompt
31
+ // 模式 B(--ai):POST /api/dashboard
32
+ // 适用:自然语言 Prompt 驱动,AI 引擎根据 prompt 自动生成看板
33
+ // 调用示例:shiplens dashboards create --ai --prompt "分析注册流失" --json
28
34
  const useAI = Boolean(flags.ai);
29
- const title = flags.title || 'AI Telemetry Dashboard';
35
+ const title = flags.title || 'AI 行为分析看板';
30
36
  const prompt = flags.prompt || '';
31
37
 
32
38
  if (useAI) {
39
+ // 模式 B:AI 自然语言看板
33
40
  let appId = '';
34
41
  try { appId = ctx.resolveAppId(); } catch (_) {}
35
42
 
36
43
  if (!prompt) {
37
- throw new Error('Provide natural language dashboard prompt via --prompt when using --ai mode.');
44
+ throw new Error('使用 --ai 模式时,请通过 --prompt 提供自然语言看板描述');
38
45
  }
39
46
 
40
47
  const reqData = { title, prompt };
@@ -42,38 +49,39 @@ async function handleDashboards(subcommand, args, flags, ctx) {
42
49
 
43
50
  const resp = await ctx.client.createAIDashboard(reqData);
44
51
  ctx.output(resp, () => {
45
- console.log('🎉 AI Dashboard created successfully!');
46
- console.log(`📌 Title: ${resp.title || title} (ID: ${resp.dashboard_id})`);
47
- if (resp.dashboard_url) console.log(`🔗 Dashboard URL: ${resp.dashboard_url}`);
52
+ console.log('🎉 AI 看板生成成功!');
53
+ console.log(`📌 看板标题: ${resp.title || title} (ID: ${resp.dashboard_id})`);
54
+ if (resp.dashboard_url) console.log(`🔗 访问链接: ${resp.dashboard_url}`);
48
55
  });
49
56
  } else {
57
+ // 模式 A:标准看板创建
50
58
  const appId = ctx.resolveAppId();
51
- const reqData = { title, prompt, app_id: appId };
59
+ const reqData = { title, prompt };
52
60
 
53
61
  if (flags.file) {
54
62
  if (!fs.existsSync(flags.file)) {
55
- throw new Error(`Dashboard config file not found: ${flags.file}`);
63
+ throw new Error(`看板配置文件不存在: ${flags.file}`);
56
64
  }
57
65
  const fileContent = JSON.parse(fs.readFileSync(flags.file, 'utf8'));
58
66
  Object.assign(reqData, fileContent);
59
67
  }
60
68
 
61
69
  if (!reqData.prompt && (!reqData.widgets || reqData.widgets.length === 0)) {
62
- throw new Error('Provide generation prompt via --prompt or configuration via --file.');
70
+ throw new Error('请通过 --prompt 提供生成提示词,或通过 --file 提供看板配置文件');
63
71
  }
64
72
 
65
73
  const resp = await ctx.client.createDashboard(appId, reqData);
66
74
  ctx.output(resp, () => {
67
- console.log('🎉 Dashboard created successfully!');
68
- console.log(`📌 Title: ${resp.title || reqData.title} (ID: ${resp.dashboard_id})`);
69
- if (resp.dashboard_url) console.log(`🔗 Dashboard URL: ${resp.dashboard_url}`);
75
+ console.log('🎉 看板生成成功!');
76
+ console.log(`📌 看板标题: ${resp.title || reqData.title} (ID: ${resp.dashboard_id})`);
77
+ if (resp.dashboard_url) console.log(`🔗 访问链接: ${resp.dashboard_url}`);
70
78
  });
71
79
  }
72
80
  break;
73
81
  }
74
82
 
75
83
  default:
76
- throw new Error(`Unknown dashboards subcommand: ${subcommand}. Supported: list, create`);
84
+ throw new Error(`未知的 dashboards 子命令: ${subcommand}。支持 list, create`);
77
85
  }
78
86
  }
79
87
 
@@ -9,7 +9,7 @@ async function handleDoctor(args, flags, ctx) {
9
9
  let hasFail = false;
10
10
  let hasWarn = false;
11
11
 
12
- // 1. local_config check
12
+ // 1. local_config 检查
13
13
  const localCfg = getLocalConfig(wd);
14
14
  if (localCfg && localCfg.app_id) {
15
15
  checks.local_config = {
@@ -20,11 +20,11 @@ async function handleDoctor(args, flags, ctx) {
20
20
  hasWarn = true;
21
21
  checks.local_config = {
22
22
  status: 'fail',
23
- message: 'No .shiplens.json or missing app_id in current directory',
23
+ message: '未在当前目录找到 .shiplens.json 或缺少 app_id',
24
24
  };
25
25
  }
26
26
 
27
- // 2. sdk_dependency check
27
+ // 2. sdk_dependency 检查
28
28
  const sdkCheck = { status: 'pass' };
29
29
  const pkgPath = path.join(wd, 'package.json');
30
30
  if (fs.existsSync(pkgPath)) {
@@ -35,27 +35,26 @@ async function handleDoctor(args, flags, ctx) {
35
35
  sdkCheck.version = deps['@shiplens/sdk'];
36
36
  } else {
37
37
  sdkCheck.status = 'warn';
38
- sdkCheck.message = '@shiplens/sdk not explicitly found in package.json';
38
+ sdkCheck.message = 'package.json 中未显式包含 @shiplens/sdk 依赖';
39
39
  hasWarn = true;
40
40
  }
41
41
  } catch (e) {
42
42
  sdkCheck.status = 'warn';
43
- sdkCheck.message = 'Unable to parse package.json';
43
+ sdkCheck.message = '无法解析 package.json';
44
44
  }
45
45
  } else {
46
46
  sdkCheck.status = 'warn';
47
- sdkCheck.message = 'package.json not found (static HTML or non-Node project)';
47
+ sdkCheck.message = '未发现 package.json (可能是纯静态或非 Node 项目)';
48
48
  }
49
49
  checks.sdk_dependency = sdkCheck;
50
50
 
51
- // 3. code_injection check
51
+ // 3. code_injection 检查
52
52
  const detected = detectProject(wd);
53
53
  let injectedFile = '';
54
54
  const candidates = [
55
55
  'src/app/layout.tsx', 'app/layout.tsx', 'src/app/layout.js', 'app/layout.js',
56
56
  'src/pages/_app.tsx', 'pages/_app.tsx', 'src/pages/_app.js', 'pages/_app.js',
57
57
  'src/main.tsx', 'src/main.ts', 'src/main.jsx', 'src/main.js',
58
- 'src/index.tsx', 'src/index.ts', 'src/index.jsx', 'src/index.js',
59
58
  'src/App.vue', 'index.html', 'public/index.html', 'shiplens.config.ts',
60
59
  'src/components/ShiplensTracker.tsx', 'components/ShiplensTracker.tsx',
61
60
  ];
@@ -79,34 +78,26 @@ async function handleDoctor(args, flags, ctx) {
79
78
  hasWarn = true;
80
79
  checks.code_injection = {
81
80
  status: 'warn',
82
- message: `No initShiplens call detected in entry files (Framework: ${detected.framework})`,
81
+ message: `未在常规入口检测到 initShiplens 调用 (框架探测: ${detected.framework})`,
83
82
  };
84
83
  }
85
84
 
86
- // 4. ingestion_connectivity check
85
+ // 4. ingestion_connectivity 检查
87
86
  try {
88
- const start = Date.now();
89
- await ctx.client.me();
87
+ const latency = await ctx.client.testConnectivity();
90
88
  checks.ingestion_connectivity = {
91
89
  status: 'pass',
92
- latency_ms: Date.now() - start,
90
+ latency_ms: latency,
93
91
  };
94
92
  } catch (err) {
95
- if (err.statusCode === 401 || err.statusCode === 403) {
96
- checks.ingestion_connectivity = {
97
- status: 'pass',
98
- latency_ms: 50,
99
- };
100
- } else {
101
- hasFail = true;
102
- checks.ingestion_connectivity = {
103
- status: 'fail',
104
- message: `API connection failed: ${err.message}`,
105
- };
106
- }
93
+ hasFail = true;
94
+ checks.ingestion_connectivity = {
95
+ status: 'fail',
96
+ message: `连接后端 API 失败: ${err.message}`,
97
+ };
107
98
  }
108
99
 
109
- // 5. auth_credential check
100
+ // 5. auth_credential 检查
110
101
  if (ctx.resolvedAuth.is_present) {
111
102
  checks.auth_credential = {
112
103
  status: 'pass',
@@ -115,11 +106,11 @@ async function handleDoctor(args, flags, ctx) {
115
106
  } else {
116
107
  checks.auth_credential = {
117
108
  status: 'warn',
118
- message: 'No Access Secret configured (login required for advanced features)',
109
+ message: '当前未配置 Access Secret (如需使用 projects/sql/dashboards 命令需登录)',
119
110
  };
120
111
  }
121
112
 
122
- // 6. email_binding check
113
+ // 6. email_binding 检查(读取本地状态机中的 bound_email 字段)
123
114
  if (localCfg && localCfg.bound_email) {
124
115
  checks.email_binding = {
125
116
  status: 'pass',
@@ -129,11 +120,11 @@ async function handleDoctor(args, flags, ctx) {
129
120
  hasWarn = true;
130
121
  checks.email_binding = {
131
122
  status: 'warn',
132
- message: 'Email not bound. Run: shiplens auth bind --email <you@example.com>',
123
+ message: '未检测到绑定邮箱,统计数据未激活。执行: shiplens auth bind --email <you@example.com>',
133
124
  };
134
125
  }
135
126
 
136
- // 7. schema_freshness check
127
+ // 7. schema_freshness 检查(本地 schema_last_modified_at 是否超过 7 天未同步)
137
128
  if (localCfg && localCfg.schema_last_modified_at) {
138
129
  const ageMs = Date.now() - localCfg.schema_last_modified_at * 1000;
139
130
  const ageDays = ageMs / (1000 * 60 * 60 * 24);
@@ -142,7 +133,7 @@ async function handleDoctor(args, flags, ctx) {
142
133
  checks.schema_freshness = {
143
134
  status: 'warn',
144
135
  age_days: Math.floor(ageDays),
145
- message: `Local schema is ${Math.floor(ageDays)} days old. Run shiplens projects list to refresh.`,
136
+ message: `本地 Schema ${Math.floor(ageDays)} 天未同步,建议执行 shiplens projects list 刷新`,
146
137
  };
147
138
  } else {
148
139
  checks.schema_freshness = {
@@ -154,7 +145,7 @@ async function handleDoctor(args, flags, ctx) {
154
145
  } else {
155
146
  checks.schema_freshness = {
156
147
  status: 'warn',
157
- message: 'No schema_last_modified_at recorded locally. Run shiplens init or projects list.',
148
+ message: '本地未记录 schema_last_modified_at,请重新执行 shiplens init projects list',
158
149
  };
159
150
  hasWarn = true;
160
151
  }
@@ -174,7 +165,7 @@ async function handleDoctor(args, flags, ctx) {
174
165
  };
175
166
 
176
167
  ctx.output(report, () => {
177
- console.log('🏥 Shiplens Environment & Telemetry Diagnostic Report');
168
+ console.log('🏥 Shiplens 项目环境与链路体检报告');
178
169
  console.log('='.repeat(50));
179
170
  for (const [name, item] of Object.entries(checks)) {
180
171
  let icon = '✅';
@@ -182,18 +173,18 @@ async function handleDoctor(args, flags, ctx) {
182
173
  else if (item.status === 'fail') icon = '❌';
183
174
 
184
175
  console.log(`${icon} [${name}]: ${item.status}`);
185
- if (item.app_id) console.log(` App ID: ${item.app_id}`);
186
- if (item.email) console.log(` Email: ${item.email}`);
187
- if (item.version) console.log(` Version: ${item.version}`);
188
- if (item.file) console.log(` File: ${item.file}`);
189
- if (item.latency_ms !== undefined) console.log(` Latency: ${item.latency_ms} ms`);
190
- if (item.source) console.log(` Credential Source: ${item.source}`);
191
- if (item.age_days !== undefined) console.log(` Schema Age: ${item.age_days} day(s)`);
192
- if (item.message) console.log(` Details: ${item.message}`);
176
+ if (item.app_id) console.log(` app_id: ${item.app_id}`);
177
+ if (item.email) console.log(` 邮箱: ${item.email}`);
178
+ if (item.version) console.log(` 版本: ${item.version}`);
179
+ if (item.file) console.log(` 文件: ${item.file}`);
180
+ if (item.latency_ms !== undefined) console.log(` 网络延迟: ${item.latency_ms} ms`);
181
+ if (item.source) console.log(` 凭据来源: ${item.source}`);
182
+ if (item.age_days !== undefined) console.log(` Schema 距上次同步: ${item.age_days} 天`);
183
+ if (item.message) console.log(` 说明: ${item.message}`);
193
184
  }
194
185
  console.log('-'.repeat(50));
195
- console.log(`🩺 Overall Status: ${overall}`);
196
- console.log(`⏱ Timestamp: ${report.doctor_ran_at}`);
186
+ console.log(`🩺 总体体检结果: ${overall}`);
187
+ console.log(`⏱ 体检时间: ${report.doctor_ran_at}`);
197
188
  });
198
189
  }
199
190