shellward 0.7.1 → 0.7.2

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/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  [![npm](https://img.shields.io/npm/v/shellward?color=cb0000&label=npm)](https://www.npmjs.com/package/shellward)
10
10
  [![license](https://img.shields.io/badge/license-Apache--2.0-blue)](./LICENSE)
11
- [![tests](https://img.shields.io/badge/tests-273%20passing-brightgreen)](#performance)
11
+ [![tests](https://img.shields.io/badge/tests-282%20passing-brightgreen)](#performance)
12
12
  [![deps](https://img.shields.io/badge/dependencies-0-brightgreen)](#performance)
13
13
 
14
14
  **🌐 官网: https://jnmetacode.github.io/shellward/**
@@ -36,6 +36,10 @@ export interface ComplianceReport {
36
36
  generatedAt: string;
37
37
  /** 项目实测风险造成的扣分(仅项目体检路径);0 表示纯控制项评分 */
38
38
  projectPenalty?: number;
39
+ /** 是否为静态扫描(未部署运行时):此时多数控制项不可验证,得分不代表完整合规 */
40
+ staticScan?: boolean;
41
+ /** 项目扫描的文件总数(静态扫描路径) */
42
+ filesScanned?: number;
39
43
  }
40
44
  /** 采集真实环境事实(运行时调用;测试可绕过直接注入 EnvFacts) */
41
45
  export declare function gatherEnvFacts(): EnvFacts;
@@ -127,6 +127,8 @@ export function runProjectComplianceAudit(config, root) {
127
127
  report.grade = gradeOf(report.score);
128
128
  report.projectPenalty = penalty;
129
129
  }
130
+ report.staticScan = true;
131
+ report.filesScanned = scan.filesScanned;
130
132
  return { report, scan };
131
133
  }
132
134
  const FINDING_PENALTY = { critical: 8, high: 4, medium: 1 };
@@ -40,23 +40,35 @@ export function renderHtmlReport(report, scan, locale, meta) {
40
40
  const when = report.generatedAt.slice(0, 19).replace('T', ' ');
41
41
  const S = [];
42
42
  // ===== 评分 Hero =====
43
+ // 诚实原则:静态扫描下多数控制项不可验证,不展示"优秀/A"式合规结论,
44
+ // 改以「风险发现数」为主指标,得分仅作"可观测项"的次要参考。
45
+ const findingsN = scan.findings.length;
46
+ const gradeLabel = report.staticScan ? t('可观测项', 'observable') : t(g.zh, g.en);
47
+ const verdict = report.staticScan
48
+ ? (findingsN === 0
49
+ ? { txt: t('未发现可观测风险', 'No observable risks'), c: '#16a34a', ic: '🟢' }
50
+ : { txt: t(`发现 ${findingsN} 项风险`, `${findingsN} risk(s) found`), c: '#dc2626', ic: '🔴' })
51
+ : { txt: t(g.zh, g.en), c: g.color, ic: '' };
43
52
  S.push(`
44
53
  <section class="hero">
45
54
  <div class="gauge" style="--p:${report.score};--c:${g.color}">
46
55
  <div class="gauge-in">
47
56
  <div class="gscore">${report.score}<small>/100</small></div>
48
- <div class="ggrade" style="color:${g.color}">${esc(report.grade)} · ${t(g.zh, g.en)}</div>
57
+ <div class="ggrade" style="color:${g.color}">${esc(report.grade)} · ${esc(gradeLabel)}</div>
49
58
  </div>
50
59
  </div>
51
60
  <div class="hero-side">
61
+ <div class="verdict" style="--vc:${verdict.c}">${verdict.ic} ${esc(verdict.txt)}</div>
52
62
  <div class="stat-row">
53
63
  ${stat('pass', '🟢', t('合规', 'Pass'), report.passed)}
54
64
  ${stat('warn', '🟡', t('部分', 'Partial'), report.warned)}
55
65
  ${stat('fail', '🔴', t('不合规', 'Fail'), report.failed)}
56
- ${stat('manual', '⚪', t('待确认', 'Review'), report.manual)}
66
+ ${stat('manual', '⚪', t('待核验', 'Review'), report.manual)}
57
67
  </div>
58
68
  ${report.projectPenalty ? `<div class="penalty">⚠ ${t('含项目实测风险扣分', 'Includes project-scan penalty')} <b>−${report.projectPenalty}</b></div>` : ''}
59
- <p class="hero-note">${t('得分基于本次可静态观测的项目风险。⚪ 待确认项需把 ShellWard 部署为运行时防护或人工核验。', 'Score reflects statically-observable project risk. ⚪ items need runtime deployment or manual review.')}</p>
69
+ <p class="hero-note">${report.staticScan
70
+ ? t(`⚠ 本次为静态扫描:已检查 ${report.filesScanned ?? scan.filesScanned} 个文件,仅评估可观测风险。<b>${report.manual} 项合规控制项未验证</b>(需部署 ShellWard 运行时或人工核验)——本报告不构成完整合规结论,得分仅供参考。`, `⚠ Static scan: checked ${report.filesScanned ?? scan.filesScanned} files for observable risk only. <b>${report.manual} controls unverified</b> — not a complete compliance verdict.`)
71
+ : t('得分基于已部署运行时的合规评估。', 'Score based on deployed-runtime assessment.')}</p>
60
72
  </div>
61
73
  </section>`);
62
74
  // ===== 项目实测风险 =====
@@ -225,6 +237,7 @@ section,.reg{padding:0 36px}
225
237
  .gscore small{font-size:15px;font-weight:500;color:var(--faint)}
226
238
  .ggrade{font-size:14px;font-weight:700;margin-top:6px}
227
239
  .hero-side{flex:1;min-width:0}
240
+ .verdict{font-size:17px;font-weight:800;color:var(--vc);margin:0 0 12px}
228
241
  .stat-row{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}
229
242
  .stat{background:#fff;border:1px solid var(--line);border-radius:10px;padding:10px 12px;text-align:center}
230
243
  .stat .sn{font-size:22px;font-weight:800;line-height:1}
Binary file
@@ -31,21 +31,39 @@ export function renderComplianceReport(report, locale) {
31
31
  const bar = scoreBar(report.score);
32
32
  L.push(zh ? '## 总评' : '## Overall');
33
33
  L.push('');
34
- L.push(`**${zh ? '合规得分' : 'Score'}: ${report.score}/100 ${gradeBadge(report.grade)}**`);
35
- L.push('');
36
- L.push('```');
37
- L.push(`${bar} ${report.score}/100 [${report.grade}]`);
38
- L.push('```');
34
+ // 诚实原则:静态扫描下不报"优秀/A"式合规结论,以风险发现数为主指标
35
+ if (report.staticScan) {
36
+ const fn = report.failed + (report.projectPenalty ? 1 : 0);
37
+ L.push(zh
38
+ ? `**项目实测风险: ${report.failed === 0 && !report.projectPenalty ? '🟢 未发现可观测风险' : '🔴 发现风险,详见下方'}**`
39
+ : `**Observable risk: ${report.failed === 0 && !report.projectPenalty ? '🟢 none found' : '🔴 see below'}**`);
40
+ L.push('');
41
+ L.push(`${bar} ${report.score}/100 ${zh ? '(可观测合规项,仅供参考)' : '(observable controls only)'}`);
42
+ void fn;
43
+ }
44
+ else {
45
+ L.push(`**${zh ? '合规得分' : 'Score'}: ${report.score}/100 ${gradeBadge(report.grade)}**`);
46
+ L.push('');
47
+ L.push('```');
48
+ L.push(`${bar} ${report.score}/100 [${report.grade}]`);
49
+ L.push('```');
50
+ }
39
51
  L.push('');
40
52
  L.push(zh
41
- ? `🟢 合规 ${report.passed} | 🟡 部分 ${report.warned} | 🔴 不合规 ${report.failed} | ⚪ 待确认 ${report.manual} (共 ${report.total} 项)`
42
- : `🟢 Pass ${report.passed} | 🟡 Partial ${report.warned} | 🔴 Fail ${report.failed} | ⚪ Manual ${report.manual} (${report.total} controls)`);
53
+ ? `🟢 合规 ${report.passed} | 🟡 部分 ${report.warned} | 🔴 不合规 ${report.failed} | ⚪ 待核验 ${report.manual} (共 ${report.total} 项)`
54
+ : `🟢 Pass ${report.passed} | 🟡 Partial ${report.warned} | 🔴 Fail ${report.failed} | ⚪ Review ${report.manual} (${report.total} controls)`);
43
55
  if (report.projectPenalty && report.projectPenalty > 0) {
44
56
  L.push('');
45
57
  L.push(zh
46
58
  ? `> 含项目实测风险扣分 **-${report.projectPenalty}**(见下方「项目实测风险」)`
47
59
  : `> Includes **-${report.projectPenalty}** from project scan findings (see Project Scan Findings)`);
48
60
  }
61
+ if (report.staticScan) {
62
+ L.push('');
63
+ L.push(zh
64
+ ? `> ⚠ 本次为静态扫描:已检查 ${report.filesScanned ?? '?'} 个文件,仅评估可观测风险;**${report.manual} 项合规控制项未验证**(需部署 ShellWard 运行时或人工核验)。本报告不构成完整合规结论。`
65
+ : `> ⚠ Static scan: ${report.filesScanned ?? '?'} files checked; **${report.manual} controls unverified**. Not a complete compliance verdict.`);
66
+ }
49
67
  L.push('');
50
68
  // ===== 优先整改(fail 项,按严重度) =====
51
69
  const fails = report.results
@@ -21,8 +21,8 @@ import { runProjectComplianceAudit } from '../compliance/audit.js';
21
21
  import { renderHtmlReport } from '../compliance/html-report.js';
22
22
  import { DEFAULT_CONFIG, resolveLocale } from '../types.js';
23
23
  const REPO_RE = /^https:\/\/(github\.com|gitlab\.com|gitee\.com|bitbucket\.org)\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+?(?:\.git)?\/?$/;
24
- const CLONE_TIMEOUT_MS = 30_000;
25
- const MAX_CONCURRENT = 2;
24
+ const CLONE_TIMEOUT_MS = 60_000;
25
+ const MAX_CONCURRENT = 4;
26
26
  /** 校验仓库 URL:仅允许白名单代码托管域名,拒绝带凭据/异常字符 */
27
27
  export function validateRepoUrl(input) {
28
28
  const url = (input || '').trim();
@@ -101,7 +101,9 @@ async function handleRepo(res, repo, locale, inc, dec) {
101
101
  send(res, 200, 'text/html', renderHtmlReport(report, scan, locale, { root: v.url }));
102
102
  }
103
103
  catch (e) {
104
- send(res, 502, 'text/html', errorPage('克隆/扫描失败:' + esc(e?.message || String(e)) + '。请确认是可公开访问的仓库。'));
104
+ const msg = esc(e?.message || String(e));
105
+ send(res, 502, 'text/html', errorPage(`克隆/扫描失败:${msg}。<br><br>可能原因:仓库过大(克隆超时 60s)、私有仓库、或地址有误。<br>` +
106
+ `<b>大仓库 / 私有代码请用本地客户端</b>(选文件夹、不上传):<code>npx shellward web --local</code>,或命令行 <code>npx shellward scan</code>。`));
105
107
  }
106
108
  finally {
107
109
  dec();
@@ -214,11 +216,11 @@ function send(res, code, type, body) {
214
216
  }
215
217
  function formPage(local) {
216
218
  const urlForm = `
217
- <form action="/scan" method="get">
219
+ <form action="/scan" method="get" onsubmit="var b=this.querySelector('button');b.disabled=true;b.textContent='扫描中…(大仓库需 10–60 秒,请勿重复点击)';">
218
220
  <label>${local ? '② ' : ''}公开仓库地址</label>
219
221
  <input name="repo" placeholder="https://github.com/owner/repo"${local ? '' : ' autofocus'}>
220
222
  <button type="submit">${local ? '体检该仓库 →' : '开始体检 →'}</button>
221
- <p class="hint">仅支持公开仓库(GitHub / GitLab / Gitee / Bitbucket)。${local ? '' : '<b>私有/敏感代码请用本地客户端或 CLI</b>:<code>npx shellward web --local</code> / <code>npx shellward scan</code>(不上传)。'}</p>
223
+ <p class="hint">仅支持公开仓库(GitHub / GitLab / Gitee / Bitbucket)。大仓库可能超时——${local ? '此时改用上方「选择文件夹」更稳。' : '<b>大仓库 / 私有代码请用本地客户端或 CLI</b>:<code>npx shellward web --local</code> / <code>npx shellward scan</code>(不上传)。'}</p>
222
224
  </form>`;
223
225
  const uploadForm = local ? `
224
226
  <form id="dirform">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shellward",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "mcpName": "io.github.jnMetaCode/shellward",
5
5
  "description": "AI agent security & MCP security middleware — prompt injection detection, AI firewall, runtime guardrails & data-loss prevention for LLM tool calls. 8-layer defense against data exfiltration & dangerous commands. Zero dependencies. SDK + OpenClaw plugin. Supports LangChain, AutoGPT, Claude Code, Cursor, OpenAI Agents, Hermes Agent.",
6
6
  "keywords": [
@@ -57,6 +57,10 @@ export interface ComplianceReport {
57
57
  generatedAt: string
58
58
  /** 项目实测风险造成的扣分(仅项目体检路径);0 表示纯控制项评分 */
59
59
  projectPenalty?: number
60
+ /** 是否为静态扫描(未部署运行时):此时多数控制项不可验证,得分不代表完整合规 */
61
+ staticScan?: boolean
62
+ /** 项目扫描的文件总数(静态扫描路径) */
63
+ filesScanned?: number
60
64
  }
61
65
 
62
66
  /** 层能力映射:控制项 id → 必须启用的层(全部启用才 pass,部分启用 warn,全关 fail) */
@@ -191,6 +195,8 @@ export function runProjectComplianceAudit(config: ShellWardConfig, root: string)
191
195
  report.grade = gradeOf(report.score)
192
196
  report.projectPenalty = penalty
193
197
  }
198
+ report.staticScan = true
199
+ report.filesScanned = scan.filesScanned
194
200
 
195
201
  return { report, scan }
196
202
  }
@@ -60,25 +60,37 @@ export function renderHtmlReport(
60
60
  const S: string[] = []
61
61
 
62
62
  // ===== 评分 Hero =====
63
+ // 诚实原则:静态扫描下多数控制项不可验证,不展示"优秀/A"式合规结论,
64
+ // 改以「风险发现数」为主指标,得分仅作"可观测项"的次要参考。
65
+ const findingsN = scan.findings.length
66
+ const gradeLabel = report.staticScan ? t('可观测项', 'observable') : t(g.zh, g.en)
67
+ const verdict = report.staticScan
68
+ ? (findingsN === 0
69
+ ? { txt: t('未发现可观测风险', 'No observable risks'), c: '#16a34a', ic: '🟢' }
70
+ : { txt: t(`发现 ${findingsN} 项风险`, `${findingsN} risk(s) found`), c: '#dc2626', ic: '🔴' })
71
+ : { txt: t(g.zh, g.en), c: g.color, ic: '' }
72
+
63
73
  S.push(`
64
74
  <section class="hero">
65
75
  <div class="gauge" style="--p:${report.score};--c:${g.color}">
66
76
  <div class="gauge-in">
67
77
  <div class="gscore">${report.score}<small>/100</small></div>
68
- <div class="ggrade" style="color:${g.color}">${esc(report.grade)} · ${t(g.zh, g.en)}</div>
78
+ <div class="ggrade" style="color:${g.color}">${esc(report.grade)} · ${esc(gradeLabel)}</div>
69
79
  </div>
70
80
  </div>
71
81
  <div class="hero-side">
82
+ <div class="verdict" style="--vc:${verdict.c}">${verdict.ic} ${esc(verdict.txt)}</div>
72
83
  <div class="stat-row">
73
84
  ${stat('pass', '🟢', t('合规', 'Pass'), report.passed)}
74
85
  ${stat('warn', '🟡', t('部分', 'Partial'), report.warned)}
75
86
  ${stat('fail', '🔴', t('不合规', 'Fail'), report.failed)}
76
- ${stat('manual', '⚪', t('待确认', 'Review'), report.manual)}
87
+ ${stat('manual', '⚪', t('待核验', 'Review'), report.manual)}
77
88
  </div>
78
89
  ${report.projectPenalty ? `<div class="penalty">⚠ ${t('含项目实测风险扣分', 'Includes project-scan penalty')} <b>−${report.projectPenalty}</b></div>` : ''}
79
- <p class="hero-note">${t(
80
- '得分基于本次可静态观测的项目风险。⚪ 待确认项需把 ShellWard 部署为运行时防护或人工核验。',
81
- 'Score reflects statically-observable project risk. items need runtime deployment or manual review.')}</p>
90
+ <p class="hero-note">${report.staticScan
91
+ ? t(`⚠ 本次为静态扫描:已检查 ${report.filesScanned ?? scan.filesScanned} 个文件,仅评估可观测风险。<b>${report.manual} 项合规控制项未验证</b>(需部署 ShellWard 运行时或人工核验)——本报告不构成完整合规结论,得分仅供参考。`,
92
+ `⚠ Static scan: checked ${report.filesScanned ?? scan.filesScanned} files for observable risk only. <b>${report.manual} controls unverified</b> — not a complete compliance verdict.`)
93
+ : t('得分基于已部署运行时的合规评估。', 'Score based on deployed-runtime assessment.')}</p>
82
94
  </div>
83
95
  </section>`)
84
96
 
@@ -263,6 +275,7 @@ section,.reg{padding:0 36px}
263
275
  .gscore small{font-size:15px;font-weight:500;color:var(--faint)}
264
276
  .ggrade{font-size:14px;font-weight:700;margin-top:6px}
265
277
  .hero-side{flex:1;min-width:0}
278
+ .verdict{font-size:17px;font-weight:800;color:var(--vc);margin:0 0 12px}
266
279
  .stat-row{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}
267
280
  .stat{background:#fff;border:1px solid var(--line);border-radius:10px;padding:10px 12px;text-align:center}
268
281
  .stat .sn{font-size:22px;font-weight:800;line-height:1}
Binary file
@@ -41,21 +41,38 @@ export function renderComplianceReport(report: ComplianceReport, locale: 'zh' |
41
41
  const bar = scoreBar(report.score)
42
42
  L.push(zh ? '## 总评' : '## Overall')
43
43
  L.push('')
44
- L.push(`**${zh ? '合规得分' : 'Score'}: ${report.score}/100 ${gradeBadge(report.grade)}**`)
45
- L.push('')
46
- L.push('```')
47
- L.push(`${bar} ${report.score}/100 [${report.grade}]`)
48
- L.push('```')
44
+ // 诚实原则:静态扫描下不报"优秀/A"式合规结论,以风险发现数为主指标
45
+ if (report.staticScan) {
46
+ const fn = report.failed + (report.projectPenalty ? 1 : 0)
47
+ L.push(zh
48
+ ? `**项目实测风险: ${report.failed === 0 && !report.projectPenalty ? '🟢 未发现可观测风险' : '🔴 发现风险,详见下方'}**`
49
+ : `**Observable risk: ${report.failed === 0 && !report.projectPenalty ? '🟢 none found' : '🔴 see below'}**`)
50
+ L.push('')
51
+ L.push(`${bar} ${report.score}/100 ${zh ? '(可观测合规项,仅供参考)' : '(observable controls only)'}`)
52
+ void fn
53
+ } else {
54
+ L.push(`**${zh ? '合规得分' : 'Score'}: ${report.score}/100 ${gradeBadge(report.grade)}**`)
55
+ L.push('')
56
+ L.push('```')
57
+ L.push(`${bar} ${report.score}/100 [${report.grade}]`)
58
+ L.push('```')
59
+ }
49
60
  L.push('')
50
61
  L.push(zh
51
- ? `🟢 合规 ${report.passed} | 🟡 部分 ${report.warned} | 🔴 不合规 ${report.failed} | ⚪ 待确认 ${report.manual} (共 ${report.total} 项)`
52
- : `🟢 Pass ${report.passed} | 🟡 Partial ${report.warned} | 🔴 Fail ${report.failed} | ⚪ Manual ${report.manual} (${report.total} controls)`)
62
+ ? `🟢 合规 ${report.passed} | 🟡 部分 ${report.warned} | 🔴 不合规 ${report.failed} | ⚪ 待核验 ${report.manual} (共 ${report.total} 项)`
63
+ : `🟢 Pass ${report.passed} | 🟡 Partial ${report.warned} | 🔴 Fail ${report.failed} | ⚪ Review ${report.manual} (${report.total} controls)`)
53
64
  if (report.projectPenalty && report.projectPenalty > 0) {
54
65
  L.push('')
55
66
  L.push(zh
56
67
  ? `> 含项目实测风险扣分 **-${report.projectPenalty}**(见下方「项目实测风险」)`
57
68
  : `> Includes **-${report.projectPenalty}** from project scan findings (see Project Scan Findings)`)
58
69
  }
70
+ if (report.staticScan) {
71
+ L.push('')
72
+ L.push(zh
73
+ ? `> ⚠ 本次为静态扫描:已检查 ${report.filesScanned ?? '?'} 个文件,仅评估可观测风险;**${report.manual} 项合规控制项未验证**(需部署 ShellWard 运行时或人工核验)。本报告不构成完整合规结论。`
74
+ : `> ⚠ Static scan: ${report.filesScanned ?? '?'} files checked; **${report.manual} controls unverified**. Not a complete compliance verdict.`)
75
+ }
59
76
  L.push('')
60
77
 
61
78
  // ===== 优先整改(fail 项,按严重度) =====
@@ -23,8 +23,8 @@ import { renderHtmlReport } from '../compliance/html-report.js'
23
23
  import { DEFAULT_CONFIG, resolveLocale } from '../types.js'
24
24
 
25
25
  const REPO_RE = /^https:\/\/(github\.com|gitlab\.com|gitee\.com|bitbucket\.org)\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+?(?:\.git)?\/?$/
26
- const CLONE_TIMEOUT_MS = 30_000
27
- const MAX_CONCURRENT = 2
26
+ const CLONE_TIMEOUT_MS = 60_000
27
+ const MAX_CONCURRENT = 4
28
28
 
29
29
  export interface WebServerOptions {
30
30
  port: number
@@ -105,7 +105,10 @@ async function handleRepo(res: any, repo: string, locale: 'zh' | 'en', inc: () =
105
105
  const { report, scan } = runProjectComplianceAudit(DEFAULT_CONFIG, dir)
106
106
  send(res, 200, 'text/html', renderHtmlReport(report, scan, locale, { root: v.url }))
107
107
  } catch (e: any) {
108
- send(res, 502, 'text/html', errorPage('克隆/扫描失败:' + esc(e?.message || String(e)) + '。请确认是可公开访问的仓库。'))
108
+ const msg = esc(e?.message || String(e))
109
+ send(res, 502, 'text/html', errorPage(
110
+ `克隆/扫描失败:${msg}。<br><br>可能原因:仓库过大(克隆超时 60s)、私有仓库、或地址有误。<br>` +
111
+ `<b>大仓库 / 私有代码请用本地客户端</b>(选文件夹、不上传):<code>npx shellward web --local</code>,或命令行 <code>npx shellward scan</code>。`))
109
112
  } finally {
110
113
  dec()
111
114
  try { rmSync(dir, { recursive: true, force: true }) } catch { /* ignore */ }
@@ -197,11 +200,11 @@ function send(res: any, code: number, type: string, body: string) {
197
200
 
198
201
  function formPage(local: boolean): string {
199
202
  const urlForm = `
200
- <form action="/scan" method="get">
203
+ <form action="/scan" method="get" onsubmit="var b=this.querySelector('button');b.disabled=true;b.textContent='扫描中…(大仓库需 10–60 秒,请勿重复点击)';">
201
204
  <label>${local ? '② ' : ''}公开仓库地址</label>
202
205
  <input name="repo" placeholder="https://github.com/owner/repo"${local ? '' : ' autofocus'}>
203
206
  <button type="submit">${local ? '体检该仓库 →' : '开始体检 →'}</button>
204
- <p class="hint">仅支持公开仓库(GitHub / GitLab / Gitee / Bitbucket)。${local ? '' : '<b>私有/敏感代码请用本地客户端或 CLI</b>:<code>npx shellward web --local</code> / <code>npx shellward scan</code>(不上传)。'}</p>
207
+ <p class="hint">仅支持公开仓库(GitHub / GitLab / Gitee / Bitbucket)。大仓库可能超时——${local ? '此时改用上方「选择文件夹」更稳。' : '<b>大仓库 / 私有代码请用本地客户端或 CLI</b>:<code>npx shellward web --local</code> / <code>npx shellward scan</code>(不上传)。'}</p>
205
208
  </form>`
206
209
 
207
210
  const uploadForm = local ? `