From fc416a3d3c73429f8e3dfe90b6b0d7a9b9f8f256 Mon Sep 17 00:00:00 2001
From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
Date: Fri, 30 Oct 2026 11:05:08 +0800
Subject: [PATCH 01/16] test(dsh): expose bounded npm preparation timings
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
---
plugins/dsh/scripts/smoke-native-host.mjs | 53 ++++++++++++++++++++--
plugins/dsh/test/smoke-native-host.test.ts | 28 +++++++++++-
2 files changed, 76 insertions(+), 5 deletions(-)
diff --git a/plugins/dsh/scripts/smoke-native-host.mjs b/plugins/dsh/scripts/smoke-native-host.mjs
index e32d92911..716614a03 100644
--- a/plugins/dsh/scripts/smoke-native-host.mjs
+++ b/plugins/dsh/scripts/smoke-native-host.mjs
@@ -1,7 +1,7 @@
import { spawn } from 'node:child_process';
import { createHash, randomUUID } from 'node:crypto';
import { closeSync, openSync } from 'node:fs';
-import { access, chmod, lstat, mkdir, mkdtemp, readFile, readdir, realpath, stat, writeFile } from 'node:fs/promises';
+import { access, chmod, open, lstat, mkdir, mkdtemp, readFile, readdir, realpath, stat, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -206,6 +206,50 @@ export function safeProbeFailure(log, probe) {
return undefined;
}
+// npm emits completed timers incrementally with --timing, even when killed before
+// its final timing.json. Never expose arbitrary timer names, URLs or log lines.
+const npmPreparationLogLimit = 1024 * 1024;
+export function summarizeNpmPreparation(log, truncated = false) {
+ const phases = new Set(['idealTree', 'idealTree:init', 'idealTree:userRequests', 'idealTree:buildDeps',
+ 'idealTree:fixDepFlags', 'reify', 'reify:loadTrees', 'reify:diffTrees', 'reify:retireShallow',
+ 'reify:createSparse', 'reify:loadBundles', 'reify:unpack', 'reify:unretire', 'reify:build',
+ 'reify:trash', 'reify:save', 'command:install']);
+ const result = { completedTimingsMs: {}, lastCompletedPhase: null,
+ cacheObservations: { hit: 0, miss: 0, revalidated: 0 }, truncated: truncated || log.length > npmPreparationLogLimit };
+ let bounded = log.slice(-npmPreparationLogLimit);
+ if (result.truncated) {
+ const newline = bounded.indexOf('\n');
+ bounded = newline < 0 ? '' : bounded.slice(newline + 1);
+ }
+ for (const line of bounded.split('\n')) {
+ const timer = /^npm timing ([A-Za-z:]+) Completed in ([0-9]+)ms\r?$/.exec(line);
+ if (timer && phases.has(timer[1])) {
+ const elapsed = Number(timer[2]);
+ if (Number.isSafeInteger(elapsed) && elapsed <= 3600000) {
+ result.completedTimingsMs[timer[1]] = elapsed;
+ result.lastCompletedPhase = timer[1];
+ }
+ }
+ const request = /^npm http fetch GET [0-9]{3} \S+ [0-9]+ms \(cache (hit|miss|revalidated)\)\r?$/.exec(line);
+ if (request) result.cacheObservations[request[1]]++;
+ // npm also logs package-cache lookups separately from HTTP fetch responses.
+ if (/^npm http cache \S+ [0-9]+ms \(cache hit\)\r?$/.test(line)) result.cacheObservations.hit++;
+ }
+ return result;
+}
+
+async function readPreparationLog(path) {
+ let file;
+ try {
+ file = await open(path, 'r');
+ const { size } = await file.stat();
+ const buffer = Buffer.alloc(Math.min(size, npmPreparationLogLimit));
+ const { bytesRead } = await file.read(buffer, 0, buffer.length, Math.max(0, size - buffer.length));
+ return { text: buffer.subarray(0, bytesRead).toString('utf8'), truncated: size > buffer.length };
+ } catch { return { text: '', truncated: false }; }
+ finally { await file?.close(); }
+}
+
// Only npm's public download content is shared; every Host/config/profile stays fresh.
export async function prepareDownloadCache(path) {
if (!isAbsolute(path)) throw new Error('Download cache must be an absolute temporary directory.');
@@ -304,13 +348,14 @@ async function main() {
try {
const installStarted = Date.now();
try {
- await run('install-host', [npm, 'install', '--prefix', root, '--ignore-scripts', '--no-audit', '--no-fund',
+ await run('install-host', [npm, 'install', '--timing', '--loglevel=http', '--prefix', root, '--ignore-scripts', '--no-audit', '--no-fund',
'--registry=https://registry.npmjs.org', `@deepseek-ai/dsh@${dshVersion}`, `pnpm@${PNPM_VERSION}`], 600000);
} finally {
env.npm_config_cache = join(root, 'npm-cache');
summary.preparation.elapsedMs = Date.now() - installStarted;
- const installLog = await readFile(join(root, 'logs/install-host.log'), 'utf8').catch(() => '');
- summary.preparation.observedCodes = knownHostFailureCodes(installLog);
+ const installLog = await readPreparationLog(join(root, 'logs/install-host.log'));
+ summary.preparation.observedCodes = knownHostFailureCodes(installLog.text);
+ summary.preparation.npm = summarizeNpmPreparation(installLog.text, installLog.truncated);
}
const dsh = join(root, 'node_modules/@deepseek-ai/dsh/lib/bin.js');
await access(dsh);
diff --git a/plugins/dsh/test/smoke-native-host.test.ts b/plugins/dsh/test/smoke-native-host.test.ts
index 397e8f77d..e4274de67 100644
--- a/plugins/dsh/test/smoke-native-host.test.ts
+++ b/plugins/dsh/test/smoke-native-host.test.ts
@@ -4,7 +4,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
// Node-only CI entry; keep its helpers importable without starting a Host or downloading dependencies.
// @ts-expect-error The development-only Node entry has no TypeScript declarations.
-import { prepareDownloadCache, scenarioProbes, hostInstallManifest, selectDshVersion, supportedDshVersions, isolatedEnvironment, profileInstallManifest, retainedCordisFamily, runBounded, selectTarball } from '../scripts/smoke-native-host.mjs';
+import { summarizeNpmPreparation, prepareDownloadCache, scenarioProbes, hostInstallManifest, selectDshVersion, supportedDshVersions, isolatedEnvironment, profileInstallManifest, retainedCordisFamily, runBounded, selectTarball } from '../scripts/smoke-native-host.mjs';
const roots: string[] = [];
async function root() {
@@ -283,3 +283,29 @@ test('trusted temporary base aliases accept lexical and canonical children but n
}
}
});
+
+
+test('npm preparation diagnostics retain only bounded known timing and cache observations', () => {
+ const result = summarizeNpmPreparation([
+ 'npm timing idealTree:init Completed in 12ms',
+ 'npm timing idealTree:buildDeps Completed in 345ms',
+ 'npm timing reify:unpack Completed in 500ms',
+ 'npm timing secret/token Completed in 8ms',
+ 'npm http fetch GET 200 https://private.invalid/token 23ms (cache miss)',
+ 'npm http fetch GET 200 https://private.invalid/token 4ms (cache hit)',
+ 'npm http fetch GET 200 https://private.invalid/token 5ms (cache revalidated)',
+ 'npm timing reify Completed in 99999999999999999ms',
+ 'npm http cache diagnostic-fixture@http://private.invalid/package.tgz 0ms (cache hit)',
+ ].join('\n'));
+ expect(result).toEqual({ completedTimingsMs: { 'idealTree:init': 12, 'idealTree:buildDeps': 345, 'reify:unpack': 500 },
+ lastCompletedPhase: 'reify:unpack', cacheObservations: { hit: 2, miss: 1, revalidated: 1 }, truncated: false });
+ expect(JSON.stringify(result)).not.toMatch(/secret|token|private/);
+ expect(summarizeNpmPreparation('x'.repeat(1024 * 1024 + 1)).truncated).toBe(true);
+});
+
+
+test('truncated npm diagnostic tails discard the partial first line', () => {
+ const result = summarizeNpmPreparation('npm timing reify Completed in 1ms\nnpm timing idealTree Completed in 2ms', true);
+ expect(result.completedTimingsMs).toEqual({ idealTree: 2 });
+ expect(result.lastCompletedPhase).toBe('idealTree');
+});
From 74e6d93b7b8558fc728803d261cf25e9aa2d637f Mon Sep 17 00:00:00 2001
From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
Date: Fri, 30 Oct 2026 11:21:46 +0800
Subject: [PATCH 02/16] fix(dsh): prefer compatible dependency reuse in Host
smoke
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
---
.../changes/add-dsh-native-plugin/verification.md | 11 +++++++++++
plugins/dsh/scripts/smoke-native-host.mjs | 3 ++-
2 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/openspec/changes/add-dsh-native-plugin/verification.md b/openspec/changes/add-dsh-native-plugin/verification.md
index 0c390c5da..c25077b2c 100644
--- a/openspec/changes/add-dsh-native-plugin/verification.md
+++ b/openspec/changes/add-dsh-native-plugin/verification.md
@@ -560,3 +560,14 @@ Windows Node 24.2.0、DSH 0.2.0-rc.2、Chrome 156 的新隔离 Profile 已通过
兼容审查补充:日志搬迁改为先排他 link 原记录,持久化目标目录后 unlink 源记录并持久化源目录,再写扩展后继;双份需完整相等,原字节 SHA 备份不覆盖。独立定向日志/投影 15 测试、74 断言及 CLI typecheck 通过,不代表全套与真实旧包矩阵完成。无效入口投影为 unreadable,Host 不再默认 enabled,补充卡片与模型上下文测试通过。
旧消费者不必理解 activation 日志。中断启用后的合法标准入口仍可能被旧版正常管理;新恢复在写入前必须比较身份、版本、installedAt、指纹与归属,变化即 unknown 并保留现场。该有限保证替代“旧版必能阻止全部待恢复操作”的过度承诺;真实旧包在 moved 阶段写入后的恢复验收仍须单列。CLI 安全转换 v3 不扫描全机 Host 日志,保留原日志与技能文件;NativeRuntime 处理自己日志域的 pending 恢复。
+
+
+### Windows 官方 Host 准备超时诊断(2026-10-30)
+
+DSH 0.6.0 发布检查在 Windows / DSH 0.2.1-rc.2 的 `install-host` 阶段两次达到 600 秒上限,均未进入技能探针;不能将该失败解释为启停断言失败。公开下载缓存存在不证明依赖解析或解包已加速。诊断摘要新增有界、白名单 npm 阶段计时及缓存观察计数,原始日志仍私有;计数不是去重请求数或完整缓存命中率。
+
+本机诊断发现新版 Host 依赖树存在多份嵌套同版本 Zod。隔离 npm Registry 的最小相容范围实验中,SDK 范围 `^3.25.0 || ^4.0.0` 与十二个 exact `4.4.3` 消费方共存:默认解析为根 `4.6.7` 加十二份 `4.4.3`,`--prefer-dedupe` 则保留一份 `4.4.3`;两棵树均通过 npm 依赖约束检查。此前不相容 major 的实验不代表这个实际依赖形状。
+
+候选调整仅在官方 Host 准备命令增加 `--prefer-dedupe`,不添加 Zod override、直接依赖、忽略 peer 的选项,也不改变安装脚本禁用、空白 Profile、进程清理、600 秒准备上限或业务断言。它可能在原范围内选择较旧的兼容依赖,因此仍须验证实际 Host 依赖身份和完整探针。
+
+当前只确认最小图的重复减少;真实 Windows 冷安装耗时、两版 Host 启停/恢复、其余平台及最终发布包验收仍待完成,不宣称超时已解决。修改验收脚本不等于改变已发布 CLI 或 DSH 产品运行逻辑。
diff --git a/plugins/dsh/scripts/smoke-native-host.mjs b/plugins/dsh/scripts/smoke-native-host.mjs
index 716614a03..b9b3816b0 100644
--- a/plugins/dsh/scripts/smoke-native-host.mjs
+++ b/plugins/dsh/scripts/smoke-native-host.mjs
@@ -348,7 +348,8 @@ async function main() {
try {
const installStarted = Date.now();
try {
- await run('install-host', [npm, 'install', '--timing', '--loglevel=http', '--prefix', root, '--ignore-scripts', '--no-audit', '--no-fund',
+ // Prefer reuse within the original exact/range/peer constraints to reduce duplicate files.
+ await run('install-host', [npm, 'install', '--prefer-dedupe', '--timing', '--loglevel=http', '--prefix', root, '--ignore-scripts', '--no-audit', '--no-fund',
'--registry=https://registry.npmjs.org', `@deepseek-ai/dsh@${dshVersion}`, `pnpm@${PNPM_VERSION}`], 600000);
} finally {
env.npm_config_cache = join(root, 'npm-cache');
From 0708f64b9f8d20c126e106f8a87b4e46449fa4c4 Mon Sep 17 00:00:00 2001
From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
Date: Fri, 30 Oct 2026 13:51:18 +0800
Subject: [PATCH 03/16] test(dsh): retain safe install admission diagnostics
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
---
.../add-dsh-native-plugin/verification.md | 6 ++++--
plugins/dsh/scripts/smoke-native-host.mjs | 11 ++++++++++-
.../integration/skill-availability-fixture.mjs | 1 +
plugins/dsh/test/probe-diagnostics.test.ts | 18 ++++++++++++++++++
4 files changed, 33 insertions(+), 3 deletions(-)
diff --git a/openspec/changes/add-dsh-native-plugin/verification.md b/openspec/changes/add-dsh-native-plugin/verification.md
index c25077b2c..cd54c3174 100644
--- a/openspec/changes/add-dsh-native-plugin/verification.md
+++ b/openspec/changes/add-dsh-native-plugin/verification.md
@@ -566,8 +566,10 @@ Windows Node 24.2.0、DSH 0.2.0-rc.2、Chrome 156 的新隔离 Profile 已通过
DSH 0.6.0 发布检查在 Windows / DSH 0.2.1-rc.2 的 `install-host` 阶段两次达到 600 秒上限,均未进入技能探针;不能将该失败解释为启停断言失败。公开下载缓存存在不证明依赖解析或解包已加速。诊断摘要新增有界、白名单 npm 阶段计时及缓存观察计数,原始日志仍私有;计数不是去重请求数或完整缓存命中率。
-本机诊断发现新版 Host 依赖树存在多份嵌套同版本 Zod。隔离 npm Registry 的最小相容范围实验中,SDK 范围 `^3.25.0 || ^4.0.0` 与十二个 exact `4.4.3` 消费方共存:默认解析为根 `4.6.7` 加十二份 `4.4.3`,`--prefer-dedupe` 则保留一份 `4.4.3`;两棵树均通过 npm 依赖约束检查。此前不相容 major 的实验不代表这个实际依赖形状。
+本机诊断发现新版 Host 依赖树存在多份嵌套同版本 Zod。隔离 npm Registry 的最小相容范围实验中,SDK 范围 `^3.25.0 || ^4.0.0` 与十二个 exact `4.4.3` 消费方共存:默认解析为根 `4.6.7` 加十二份 `4.4.3`,`--prefer-dedupe` 则保留一份 `4.4.3`;两棵树均通过 npm 依赖约束检查。
候选调整仅在官方 Host 准备命令增加 `--prefer-dedupe`,不添加 Zod override、直接依赖、忽略 peer 的选项,也不改变安装脚本禁用、空白 Profile、进程清理、600 秒准备上限或业务断言。它可能在原范围内选择较旧的兼容依赖,因此仍须验证实际 Host 依赖身份和完整探针。
-当前只确认最小图的重复减少;真实 Windows 冷安装耗时、两版 Host 启停/恢复、其余平台及最终发布包验收仍待完成,不宣称超时已解决。修改验收脚本不等于改变已发布 CLI 或 DSH 产品运行逻辑。
+真实 Windows Node 24.2.0 / npm 11.3.0 的新版 Host lock-only 对照确认:默认 687 个安装位置、51 份 Zod,prefer-dedupe 为 636 个安装位置、1 份 Zod。本机两次完整冷安装仍达到 600 秒上限;减少重复依赖并不证明冷安装超时已解决。
+
+候选提交 `74e6d93b7` 的 [CI 40996963179](https://github.com/iflytek/skillhub/actions/runs/40996963179) 中,旧版 Host 的 Windows Node 24.2.0 完整探针通过,准备耗时 348647ms;Windows Node 24 的准备完成(459541ms),但随后技能可用性探针在 user-install 的 accepted 断言失败。Linux/macOS 基础检查通过,后续官方 Host 和启停矩阵因前置失败未执行,不能算通过。安装工具返回状态与固定失败码需要补充诊断;保留现有断言。最终发布包验收仍待完成。修改验收脚本不等于改变已发布 CLI 或 DSH 产品运行逻辑。
diff --git a/plugins/dsh/scripts/smoke-native-host.mjs b/plugins/dsh/scripts/smoke-native-host.mjs
index b9b3816b0..117aa2171 100644
--- a/plugins/dsh/scripts/smoke-native-host.mjs
+++ b/plugins/dsh/scripts/smoke-native-host.mjs
@@ -125,7 +125,16 @@ export function safeAvailabilityCheckpoint(value) {
const checkpoints = ['setup', 'install', 'install-poll', 'reference', 'before-digest', 'execute', 'assertion', 'after-digest', 'mutation'];
const assertions = ['none', 'isError', 'kind', 'schemaVersion', 'association', 'scope', 'integrity', 'discovery', 'name', 'reason', 'invocation', 'installation', 'namespace', 'slug', 'version', 'fingerprint', 'render', 'privacy', 'unchanged', 'accepted', 'completed', 'reference'];
if (!scenarios.includes(value.scenario) || !checkpoints.includes(value.checkpoint) || !assertions.includes(value.assertion)) return undefined;
- return { scenario: value.scenario, checkpoint: value.checkpoint, assertion: value.assertion };
+ const result = { scenario: value.scenario, checkpoint: value.checkpoint, assertion: value.assertion };
+ const statuses = ['accepted', 'not-confirmed', 'awaiting-confirmation', 'cancelled'];
+ // Mirror the public installationFailureCodes contract; never forward error messages or IDs.
+ const codes = ['CONTEXT_STALE', 'REQUEST_CONFLICT', 'SOURCE_NOT_CONFIGURED', 'SOURCE_DENIED',
+ 'SKILL_UNAVAILABLE', 'RATE_LIMITED', 'SOURCE_UNAVAILABLE', 'TARGET_UNAVAILABLE', 'UNCONFIRMED'];
+ if (statuses.includes(value.install?.status)) {
+ result.install = { status: value.install.status };
+ if (codes.includes(value.install.code)) result.install.code = value.install.code;
+ }
+ return result;
}
export function safeToggleCheckpoint(value) {
diff --git a/plugins/dsh/test/integration/skill-availability-fixture.mjs b/plugins/dsh/test/integration/skill-availability-fixture.mjs
index 803421514..6f455ad6c 100644
--- a/plugins/dsh/test/integration/skill-availability-fixture.mjs
+++ b/plugins/dsh/test/integration/skill-availability-fixture.mjs
@@ -97,6 +97,7 @@ async function run(ctx, root, signal, checkpoint) {
checkpoint.operation = { deadlineReached: false };
mark('install');
const result = await execute('astron_skillhub_install', { namespace, slug: 'document-helper', scope });
+ checkpoint.install = { status: result.value?.status, code: result.value?.code };
mark('assertion', 'isError');
assert.equal(result.isError, false);
mark('assertion', 'accepted');
diff --git a/plugins/dsh/test/probe-diagnostics.test.ts b/plugins/dsh/test/probe-diagnostics.test.ts
index 2585082a7..dcde01c28 100644
--- a/plugins/dsh/test/probe-diagnostics.test.ts
+++ b/plugins/dsh/test/probe-diagnostics.test.ts
@@ -1,4 +1,5 @@
import { expect, test } from 'bun:test';
+import { installationFailureCodes } from '../src/host/installation-failure';
// @ts-expect-error The development-only Node entry has no TypeScript declarations.
import { safeProbeFailure } from '../scripts/smoke-native-host.mjs';
@@ -80,3 +81,20 @@ test('operation observations and Host codes whitelist every field without reveal
expect(safeProbeFailure(JSON.stringify({ ...base, probe: 'runtime-baseline' }), 'runtime-baseline').operation).toBeUndefined();
}
});
+
+test('availability admission diagnostics expose only known install status and failure code', () => {
+ const checkpoint = { scenario: 'user-install', checkpoint: 'assertion', assertion: 'accepted',
+ install: { status: 'not-confirmed', code: 'TARGET_UNAVAILABLE', message: '/secret/token' } };
+ const record = { passed: false, probe: 'skill-availability', stage: 'host-assertion',
+ failureCode: 'HOST_ASSERTION_FAILED', diagnostics: { availability: checkpoint } };
+ const safe = safeProbeFailure(JSON.stringify(record), 'skill-availability');
+ expect(safe.availability.install).toEqual({ status: 'not-confirmed', code: 'TARGET_UNAVAILABLE' });
+ expect(JSON.stringify(safe)).not.toContain('secret');
+ for (const code of installationFailureCodes) {
+ checkpoint.install.code = code;
+ expect(safeProbeFailure(JSON.stringify(record), 'skill-availability').availability.install.code).toBe(code);
+ }
+ checkpoint.install.status = '/private/status';
+ checkpoint.install.code = '/private/code';
+ expect(safeProbeFailure(JSON.stringify(record), 'skill-availability').availability.install).toBeUndefined();
+});
From a431833b33bcf7523949537c50b9409c00c70ce3 Mon Sep 17 00:00:00 2001
From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
Date: Fri, 30 Oct 2026 14:02:20 +0800
Subject: [PATCH 04/16] test(dsh): use explicit ESM failure-code import
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
---
plugins/dsh/test/probe-diagnostics.test.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugins/dsh/test/probe-diagnostics.test.ts b/plugins/dsh/test/probe-diagnostics.test.ts
index dcde01c28..acfae96b9 100644
--- a/plugins/dsh/test/probe-diagnostics.test.ts
+++ b/plugins/dsh/test/probe-diagnostics.test.ts
@@ -1,5 +1,5 @@
import { expect, test } from 'bun:test';
-import { installationFailureCodes } from '../src/host/installation-failure';
+import { installationFailureCodes } from '../src/host/installation-failure.js';
// @ts-expect-error The development-only Node entry has no TypeScript declarations.
import { safeProbeFailure } from '../scripts/smoke-native-host.mjs';
From 55bd334a00754035d36b95ad7cae8d767dc324ee Mon Sep 17 00:00:00 2001
From: wanting12345 <2571039622@qq.com>
Date: Fri, 30 Oct 2026 14:07:00 +0800
Subject: [PATCH 05/16] fix(web): truncate pinned version column in suite
detail tables
The pinned version cells in the suite member table and the version
ledger member list rendered raw member version strings (60+ character
immutable snapshot versions) inside fixed-width grid columns without
any overflow handling, so the text bled into adjacent columns.
Clip them with the same truncate + title pattern already used for the
member coordinate in the same tables; the full version stays available
via hover.
Signed-off-by: wanting12345 <2571039622@qq.com>
Co-Authored-By: Claude Code
---
web/src/features/suite/suite-member-table.tsx | 2 +-
web/src/features/suite/suite-version-ledger.tsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/web/src/features/suite/suite-member-table.tsx b/web/src/features/suite/suite-member-table.tsx
index 72db6e0fd..aa6d75e06 100644
--- a/web/src/features/suite/suite-member-table.tsx
+++ b/web/src/features/suite/suite-member-table.tsx
@@ -77,7 +77,7 @@ export function SuiteMemberTable({ suite, returnTo, management = false, onEdit }
{identity}
) : identity}
- v{member.version}
+ {`v${member.version}`}
{member.summary || t(member.entry ? 'suite.entryMemberPurpose' : 'suite.memberPurposeFallback')}
diff --git a/web/src/features/suite/suite-version-ledger.tsx b/web/src/features/suite/suite-version-ledger.tsx
index 09f191f60..c2dd2b2af 100644
--- a/web/src/features/suite/suite-version-ledger.tsx
+++ b/web/src/features/suite/suite-version-ledger.tsx
@@ -227,7 +227,7 @@ function ExpandedVersion({
@{member.namespace}/{member.slug}
) : @{member.namespace}/{member.slug}}
- v{member.version}
+ {`v${member.version}`}
{member.entry ? t('suite.entrySkill') : t('suite.management.memberRole')}
Date: Fri, 30 Oct 2026 14:34:41 +0800
Subject: [PATCH 06/16] docs(weekly): mirror 2026-W44 report
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
---
weekly/site/reports.json | 9 +-
weekly/site/reports/2026-W44/index.html | 1435 +++++++++++++++++++++++
weekly/source.json | 2 +-
3 files changed, 1444 insertions(+), 2 deletions(-)
create mode 100644 weekly/site/reports/2026-W44/index.html
diff --git a/weekly/site/reports.json b/weekly/site/reports.json
index f37215afb..897e6e3e2 100644
--- a/weekly/site/reports.json
+++ b/weekly/site/reports.json
@@ -1,6 +1,13 @@
{
- "latest": "2026-W43",
+ "latest": "2026-W44",
"reports": [
+ {
+ "week": "2026-W44",
+ "title": "SkillHub 开源周报|2026 年第 44 周",
+ "period": "2026-10-23 00:00—2026-10-30 00:00",
+ "snapshot": "2026-10-30 14:23 Asia/Shanghai",
+ "path": "reports/2026-W44/"
+ },
{
"week": "2026-W43",
"title": "SkillHub 开源周报|2026 年第 43 周",
diff --git a/weekly/site/reports/2026-W44/index.html b/weekly/site/reports/2026-W44/index.html
new file mode 100644
index 000000000..4ec336fe8
--- /dev/null
+++ b/weekly/site/reports/2026-W44/index.html
@@ -0,0 +1,1435 @@
+
+
+
+
+
+
+ SkillHub 开源周报|2026 年第 44 周
+
+
+
+
+ 跳到报告正文
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
先看仓库规模和协作流转,再看已经交付与仍待验证的内容。
事件统计覆盖完整的 7 天周期;库存与开放队列取采集快照。
+
一、仓库关键信息
+ Issue 与 PR 事件流量均为净减少,Release 三条产品线均有交付;35 次失败仍需跟踪。
+
+
规模快照
+
Stars5,338较上次观测 +55
+
Forks815仓库库存字段
+
10 月 25 日 14:20 认证观测 5,283 Stars,本次于 10 月 30 日 14:23 观测 5,338,观测差 +55;两次观测不对齐周期边界,不能解释为周净增长。认证 Stargazer 完整分页与库存同为 5,338,其中 80 个当前仍可见到达落入本期,也不代表净增长。Fork 列表 1,167 条与库存字段 815 不一致,因此不展示 Fork 到达数。
+
协作流转
+
Issue 新增6
+
Issue 关闭7
+
PR 新增23
+
PR 合并23
+
PR 关闭未合并2
+
Issue 事件净减少 1,快照开放 26;main PR 事件净减少 2,快照开放 9。库存只用于当前快照,不用于倒推周期起点。
+
+ 运行判断:完整周期共有 623 条 Actions,成功 470、失败 35,有效成功率 93.1%。本期 GitHub Release 为应用、CLI、DSH 各 1 个;当前开放 Issue 26 个、main PR 9 个。
+ 主要风险:完整周期内有 35 次失败,其中 Check DSH Plugin 13 次、PR CLI 5 次;报告保留失败记录,不以随后成功覆盖历史失败。
+
+
+
二、功能迭代信息
+ 版本发布情况:周期内共发布 3 个 GitHub Release:应用、CLI、DSH 各 1 个;npm 同期发布 CLI 1 个版本、DSH 2 个版本。
+ 本期版本发布情况| 产品线 | GitHub Release | 包仓库状态 | 发布时间与边界 |
+ | SkillHub 应用 | v0.2.24 | 不适用 | 10 月 26 日 09:43 发布。标签祖先核验包含本期 23 个合并 PR 中的 9 个;其余 14 个本期合并 PR 不归入该版本。 |
+ | SkillHub CLI | 0.2.0 | @astron-team/skillhub 周期内发布 0.2.0;快照时 latest 为 0.3.0。 | GitHub Release 于 10 月 26 日 14:05 发布,npm 于 14:06 发布。0.3.0 于 10 月 30 日 10:17/10:20 发布,晚于周期截止,不计入本期。 |
+ | SkillHub DSH | 0.4.0 | @astron-team/skillhub-dsh 同期发布 0.4.0、0.5.0,latest 为 0.5.0。 | GitHub Release 于 10 月 26 日 14:30 发布;npm 0.4.0、0.5.0 分别于 14:23、17:36 发布。GitHub 与 npm 版本轨道分别说明,不把 0.5.0 写成 GitHub Release。 |
+
+ 版本后的功能进展
+
+
+
+
三、生态相关进展
+
+
5新增 Issue 作者
4新增 PR 作者
5合并 PR 作者
+
+ 作者、Fork 库存和 npm 下载是独立信号,不构成转化漏斗。CLI 在 10 月 23—28 日 6 个完整 UTC 日桶记录 1,522 次下载;Fork 到达数因来源不一致而省略。
+
+
+
+
+
+
+
+
健康判断只使用可观察执行与开放队列证据。
成功的安全工作流不等于漏洞为零。
+
项目健康状态
+
工程稳定性失败需收敛
93.1% 470/505
有效运行成功 470 次、失败 35 次。
共 623 条:另有跳过 91、取消 27;等待授权 0。
+
安全流水线存在失败
72 / 2 成功 / 失败
Security 成功 72、失败 2。
另有跳过 22;执行结果不等同于漏洞数量为 0。
+
自动化运营核心任务运行
28 Backlog Rescore 成功
Issue Triage 成功 13、失败 0。
Triage 另有跳过 26、取消 2;仅表示工作流执行结果。
+
Release 交付多轨发布
1 / 1 / 1 应用 / CLI / DSH
应用 v0.2.24、CLI 0.2.0、DSH GitHub Release 0.4.0。
CLI 0.3.0 在周期截止后发布;不同发布轨道不混算。
+
Issue 治理事件净减少
−1 周期事件净流量
新增 6、关闭 7;快照开放 26 个。
11 个带 triage/needs-info,占 42.3%;16 个已开放至少 30 天。
+
PR 治理事件净减少
−2 周期事件净流量
main:新增 23、合并 23、关闭未合并 2。
快照开放 9 个,其中草稿 2 个、3 个已开放至少 30 天。
+
+
+
CI 与自动化执行
+ 完整周期共 623 条 Actions:成功 470、失败 35、跳过 91、取消 27、等待授权 0。
+ Actions 结果构成
成功 470失败 35等待授权 0跳过 91取消 27
有效成功率 = 470 / (470 + 35) = 93.1%。失败分布:Check DSH Plugin 13、PR CLI 5、RISC-V Images 4、DeepWiki 3、PR Tests 3、PR E2E Tests 2、Security 2、backend-tests 2、Publish Images 1。
+ 重点工作流执行结果| 工作流 | 成功 / 失败 | 其他结果与判断 |
+ | Check DSH Plugin | 13 / 13 | 取消 8;为最大失败来源。 |
+ | PR CLI | 21 / 5 | 与 CLI 版本发布并行,失败记录不因发布成功而删除。 |
+ | PR Tests | 40 / 3 | 跳过 22、取消 7。 |
+ | Security | 72 / 2 | 跳过 22;不能据此推断私有告警为 0。 |
+ | backend-tests | 59 / 2 | 本期无等待授权记录。 |
+ | RISC-V Images | 23 / 4 | 本期无等待授权记录。 |
+
+
+
+
开放队列健康
以下为 10 月 30 日 14:23 后采集的认证 API 快照;队列可包含周期截止后的新状态。
+ Issue 健康
开放 Issue 年龄结构
< 7 天3
7—13 天5
14—29 天2
≥ 30 天16
triage/needs-info 占比
11 个 Issue 仍需补充信息。
应集中澄清、拆解或关闭。
+ PR 健康
< 7 天4
7—13 天2
14—29 天0
≥ 30 天3
长期开放 PR 为 #437、#797、#894;其中 #894 为草稿。快照时共有 2 个草稿 PR。
+
+
+
+
+
先看周期事件流转,再看需要优先判断的高价值输入。
价值级别是治理判断,不替代仓库标签。
+
本周 Issue 与 PR 流转
+ Issue +6 / 关闭 7重新打开 0 · 事件净减少 1 · 快照开放 26
PR +23 / 完成 25合并 23 + 关闭未合并 2 · 事件净减少 2 · 快照开放 9
+ 数据解读:7 个关闭 Issue 与 23 个合并 PR 都包含更早周期创建项。所有 PR 流量只保留目标分支为 main 的记录;事件区间完整覆盖 10 月 23—29 日。
+ 新增 Issue(按价值排序,6 / 6)
+ 关键 PR 流转
W44 截至快照关键 PR 流转| 状态 | 价值 | PR | 结果与判断 |
+ | 已进入 v0.2.24 | A | #1012—#1017 | 镜像、安全、发布校验、文档部署、字体与 CLI 下载修复已被应用标签包含。 |
+ | CLI 已发布 | A | #1019 / CLI 0.2.0 | 周期内 GitHub Release 与 npm 包均已验证;0.3.0 在截止后发布。 |
+ | 主分支待应用发布 | A | #1021 / #1027 / #1034 | 社区本地化、Scanner 兼容与授权修复均晚于 v0.2.24。 |
+ | 主分支持续迭代 | B | #1025 / #1030 / #1032 / #1033 | DSH 安装激活、更新概览、卡片与兼容修复已合并;不倒推为进入已发布 npm 版本。 |
+ | 推进中 | B | #1029 | 本期创建且截至快照仍开放;可合并字段、检查状态与产品验收分别判断。 |
+
+ 本期关闭未合并 PR(2 个)
#630、#906 均未合并关闭,只计入关闭未合并。#906 的 0.1.13 未发布;本期后续仅以独立 Release/npm 证据确认 CLI 0.2.0 发布,不把 #906 的关闭本身解释为交付。
+ 当前 A / B 判断队列
当前高价值 PR 队列| 价值 | 对象 | 快照状态与下一门槛 |
+ | A | #1003 创作平台运行时 | API 显示 clean;仍需完成分层安全验收,不能只凭 mergeable 判断完成。 |
+ | A | #1004 邮件通知 | API 显示 dirty;需要先解决冲突,再审查验邮、隐私、重试与 SMTP 运维边界。 |
+ | B | #1029 业务事件出口 | mergeable=true 但 mergeable_state=blocked;默认关闭、脱敏与投递责任仍是验收门槛。 |
+
+
+
+
+
+
这里说明时间边界、数据来源与缺失项。
未取得的维度不以 0 或上期数值替代。
+
统计口径
+ - 统计周期按“上周四收口至本周四收口”使用半开区间
[2026-10-23 00:00:00, 2026-10-30 00:00:00)(Asia/Shanghai),UTC 等价区间为 [2026-10-22 16:00:00, 2026-10-29 16:00:00),完整覆盖 7 天。
+ - 统计时点事件截止固定为 10 月 30 日 00:00:00;认证 API 于 14:23 后采集。库存和开放队列是采集时快照,事件总量按固定截止过滤;截止后的事件不进入本期流量与发布总数。
+ - Issue 与 PRIssue 剔除带
pull_request 字段的条目;PR 只保留目标为 main 的记录。新增、关闭、合并分别按 created_at、closed_at、merged_at 过滤;重开来自 events 端点。
+ - Star 证据上一期于 10 月 25 日 14:20 认证观测 5,283 Stars,本次于 10 月 30 日 14:23 认证观测 5,338,观测差 +55。两次观测不对齐周期边界,只能描述观测差。80 为完整
starred_at 分页中落入本期且查询时仍可见的到达数,不代表净增长。
+ - Release 与交付状态按
published_at 过滤,应用、CLI、DSH GitHub Release 各 1 个。v0.2.24 的 Git 祖先核验包含本期 23 个合并 PR 中的 9 个;其余 14 个只能描述为进入主分支。CLI 0.3.0 晚于截止时间,不计入本期;npm 包独立统计。
+ - Actions 成功率
success / (success + failure);等待授权、跳过和取消不进入分母,但单独展示。
+ - 生态日桶npm 与 Traffic 统一汇总 10 月 23—28 日 6 个完整 UTC 日桶。它们完全位于本期区间内,但未覆盖本地周期两端和最新延迟日桶,因此标记为部分覆盖,不与完整周期直接比较。
+
+
数据来源与限制
来源覆盖| 指标 | 取值 | 来源与覆盖 |
+ | Stars 库存 / 观测差 | 5,338 / +55 | GitHub REST 仓库快照;认证 Stargazer 完整分页共 5,338 条,与库存一致。观测差不是周净增长。 |
+ | Star 当前可见到达 | 80 | 按 starred_at 精确过滤,只表示查询时仍可见到达,不代表净增长。 |
+ | Forks 库存 | 815 | 仓库库存字段;Fork 列表 1,167 条与库存不一致,不展示到达数。 |
+ | Issue 新增 / 关闭 / 重开 | 6 / 7 / 0 | Issues 与 events 分页;剔除 PR;事件净减少 1,快照开放 26。 |
+ | PR 新增 / 合并 / 关闭未合并 / 重开 | 23 / 23 / 2 / 0 | Pulls 与 events 完整分页,仅含目标分支 main;事件净减少 2,快照开放 9。 |
+ | 应用 / CLI / DSH GitHub Release | 1 / 1 / 1 | Releases 端点按 published_at 过滤;CLI 0.3.0 在截止后发布,不计入本期。 |
+ | npm CLI / DSH latest | 0.3.0 / 0.5.0 | Registry 快照 latest;本期 CLI 发布 0.2.0,DSH 发布 0.4.0、0.5.0。CLI 0.3.0 是截止后状态。 |
+ | Actions 记录 | 623 | 按精确 created_at 过滤;成功 470、失败 35、等待授权 0、跳过 91、取消 27。 |
+ | CLI 下载 | 1,522 | npm Downloads API 的 6 个完整 UTC 日桶(10 月 23—28 日),部分覆盖。 |
+ | Views / Clones | 3,989 / 8,789 | GitHub Traffic API 的同 6 个完整 UTC 日桶,部分覆盖。 |
+ | 快照开放 Issue / main PR | 26 / 9 | 10 月 30 日 14:23 后认证 API 快照;仓库 open_issues_count 35 = Issue 26 + PR 9。 |
+
未取得:周期起止时刻严格对齐的 Star/Fork 库存快照、可信 Fork 到达数、精确对齐 Asia/Shanghai 的 npm/Traffic 完整汇总、生产实例可用率与延迟、生产扫描成功率、私有安全告警,以及文章、直播、社区分享和合作项目链接。以上缺口不以 0 或推断值替代。
+
+
+
+
+
+
+
diff --git a/weekly/source.json b/weekly/source.json
index befc85832..14f613550 100644
--- a/weekly/source.json
+++ b/weekly/source.json
@@ -1,4 +1,4 @@
{
"repository": "https://github.com/XiaoSeS/skillhub-weekly.git",
- "commit": "81761794ab97cb03e3d85a062e4bbd76c8463c26"
+ "commit": "6c1e530394585fa11ac9ccab370c6b1b28dc0563"
}
From 20fd579827edae76789a5e7ac6fe29a6fb40a381 Mon Sep 17 00:00:00 2001
From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
Date: Fri, 30 Oct 2026 14:44:42 +0800
Subject: [PATCH 07/16] chore(dsh): prepare version 0.6.1 publication
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
---
.../changes/add-dsh-native-plugin/release-guide.md | 12 ++++++------
.../changes/add-dsh-native-plugin/release-notes.md | 10 +++++-----
.../changes/add-dsh-native-plugin/verification.md | 5 +++++
plugins/dsh/README.en.md | 10 +++++-----
plugins/dsh/README.md | 10 +++++-----
plugins/dsh/package.json | 2 +-
6 files changed, 27 insertions(+), 22 deletions(-)
diff --git a/openspec/changes/add-dsh-native-plugin/release-guide.md b/openspec/changes/add-dsh-native-plugin/release-guide.md
index e4a2c3923..d1a3cbc07 100644
--- a/openspec/changes/add-dsh-native-plugin/release-guide.md
+++ b/openspec/changes/add-dsh-native-plugin/release-guide.md
@@ -12,14 +12,14 @@
### 1.1 当前候选与发布顺序
-本轮拟发布 CLI `0.3.0`(`cli-v0.3.0`)和 DSH `0.6.0`(`dsh-v0.6.0`),范围为技能启停、统一只读检查更新、紧凑已安装卡片,以及发布前库存兼容修复。不发布服务端镜像或部署 SaaS。
+CLI `0.3.0`(`cli-v0.3.0`)已发布,本轮继续发布 DSH `0.6.1`(`dsh-v0.6.1`),范围为技能启停、统一只读检查更新、紧凑已安装卡片,以及发布前库存兼容修复。不发布服务端镜像或部署 SaaS。
共享库存保持严格 v2,不写入 `installationState`;启停状态从可信的 `SKILL.md` / `SKILL.md.disabled` 入口派生,不要求 CLI 用户同步升级。已发布 CLI `0.2.0` 的正常查询和普通目标管理必须继续兼容;默认更新、同步及 Suite 不得无意重新启用停用目标。旧 CLI 显式 force 重装、更新、同步仍按原覆盖语义执行,可能恢复标准入口;显式 remove 仍可删除,新版重新读取须反映真实变化。
1. 使用真实旧公开 CLI `0.2.0`、DSH `0.5.0` 建立基线,在同一隔离 HOME 交替操作,验证普通/停用目标混存、来源与位置隔离、默认保护和显式覆盖语义。仅证明拒写或字节未损坏不算兼容通过。
2. 读 v1/v2 不迁移。未发布 v3 必须在锁内校验身份、入口与未完成事务,备份原始字节后安全转换 v2;普通操作日志保持旧 DSH 可读,启停日志隔离影响。并发及五阶段强杀恢复重新验收。
-3. 完成候选检查、双 RC、跨平台、真实浏览器及草稿保护;旧候选结果仅在输入等价时可复用。兼容修复正在进行,未通过前阻断发布。
-4. 通过后按专用工作流依次发布 CLI 和 DSH,核对公开字节、干净安装和升级保留。此顺序是交付安排,不是要求用户同步升级 CLI。当前不声明任何候选已发布。
+3. 完成候选检查、双 RC、跨平台、真实浏览器及草稿保护;旧候选结果仅在输入等价时可复用。兼容修复及真实旧包验收已通过;最终版本包和公开包仍按本流程单独验收。
+4. CLI 0.3.0 已完成公开字节及安装验收,不重复发布;DSH 0.6.1 通过后按专用工作流发布,核对公开字节、干净安装和升级保留。此顺序是交付安排,不是要求用户同步升级 CLI。保留已存在的 `dsh-v0.6.0` 标签,不移动到新提交;DSH 0.6.1 公开验收完成前不声明已发布。
旧消费者不承诺识别启停专用日志:中断启用已留下合法标准入口时,旧版仍可能执行合法管理。新恢复必须先核精确目标身份、版本、时间、指纹和归属,发现旧版变更即保留现场并返回 unknown,不能用旧意图覆盖新内容。CLI 的 v3 转换只核当前库存和入口并保留日志,不扫描全机 Host;NativeRuntime 在自己的日志域先处理恢复。
@@ -105,7 +105,7 @@ Host 准备允许通过显式 `--download-cache` 共享受校验临时目录中
### 2.3 隔离环境验证
-本次 `0.6.0` 候选分别验证 DSH `0.2.0-rc.2` 与 `0.2.1-rc.2`,Node `>=24.2.0 <25`。Host、app-boot、核心模块及注入 Client 模块须为同一精确发行版。新 RC 仅使用 `4.0.5 / 1.0.6 / 1.0.10 / 1.1.7`;旧 RC 的 Cordis、Loader、Include、Timer 须为已验证的完整组合:`4.0.4 / 1.0.5 / 1.0.9 / 1.1.6` 或 `4.0.5 / 1.0.6 / 1.0.10 / 1.1.7`,不得交叉混装;以实际 Host 解析结果为准。已发布 `0.1.0` 尚不包含后一组合支持及历史回执修复,`0.1.1` 起包含这些修复,`0.2.0` 延续此兼容基线;发布前使用本次 tarball 验证。在独立测试账户或容器中准备 Web Profile、工作区和测试技能,连接测试 Registry。仅隔离 Profile/DSH home 不足以隔离共享 inventory。
+本次 `0.6.1` 候选分别验证 DSH `0.2.0-rc.2` 与 `0.2.1-rc.2`,Node `>=24.2.0 <25`。Host、app-boot、核心模块及注入 Client 模块须为同一精确发行版。新 RC 仅使用 `4.0.5 / 1.0.6 / 1.0.10 / 1.1.7`;旧 RC 的 Cordis、Loader、Include、Timer 须为已验证的完整组合:`4.0.4 / 1.0.5 / 1.0.9 / 1.1.6` 或 `4.0.5 / 1.0.6 / 1.0.10 / 1.1.7`,不得交叉混装;以实际 Host 解析结果为准。已发布 `0.1.0` 尚不包含后一组合支持及历史回执修复,`0.1.1` 起包含这些修复,`0.2.0` 延续此兼容基线;发布前使用本次 tarball 验证。在独立测试账户或容器中准备 Web Profile、工作区和测试技能,连接测试 Registry。仅隔离 Profile/DSH home 不足以隔离共享 inventory。
首次创建测试 Web Profile,然后安装本地候选包:
@@ -190,7 +190,7 @@ SaaS 使用其现有流水线部署;公共接口契约、测试数据规则与
### 4.1 独立发布流程
-插件使用独立版本,tag 前缀固定为 `dsh-v`(本次为 `dsh-v0.6.0`),不复用 CLI 的 `cli-v*` 发布流程。专用工作流为 `.github/workflows/release-dsh-plugin.yml`;发布前仍须确认 npm 包名、兼容基线及候选包验收,本次候选为 `0.6.0`,manifest 已显式设为 `private: false`;缺省 Registry 为 `https://skill.xfyun.cn`,显式空串/空白仍断开,已有自定义来源保留。发布前分别验证缺省、断开和自定义配置,以及正式域名的固定版本安装、状态识别和本地管理。正式发布仍以最终提交、候选验收和审批结果为准。
+插件使用独立版本,tag 前缀固定为 `dsh-v`(本次为 `dsh-v0.6.1`),不复用 CLI 的 `cli-v*` 发布流程。专用工作流为 `.github/workflows/release-dsh-plugin.yml`;发布前仍须确认 npm 包名、兼容基线及候选包验收,本次候选为 `0.6.1`,manifest 已显式设为 `private: false`;缺省 Registry 为 `https://skill.xfyun.cn`,显式空串/空白仍断开,已有自定义来源保留。发布前分别验证缺省、断开和自定义配置,以及正式域名的固定版本安装、状态识别和本地管理。正式发布仍以最终提交、候选验收和审批结果为准。
发布流程依次完成:校验版本 → 测试与构建 → 生成并检查 tarball → 隔离 DSH 候选验收 → 发布审批 → 发布 npm → 校验已发布包。当前工作流自动执行版本校验、测试与构建、包检查、上传及完整性校验;隔离 DSH 的 UI、真实模型和平台验收按本次确认范围另行执行,未验项目须单独注明;CI 矩阵成功不等同于这些验收通过。工作流使用仓库的 `NPM_TOKEN` 发布凭证,源码及 npm 包不包含凭证;配置了环境保护规则时还须完成对应审批。
@@ -213,7 +213,7 @@ SaaS 使用其现有流水线部署;公共接口契约、测试数据规则与
### 4.3 发布说明
-`0.6.0` 的变更说明见 [release-notes.md](release-notes.md)。正式版本发布前按最终候选及验收结果更新,不沿用开发版本的支持声明。
+`0.6.1` 的变更说明见 [release-notes.md](release-notes.md)。正式版本发布前按最终候选及验收结果更新,不沿用开发版本的支持声明。
每个版本包含:
diff --git a/openspec/changes/add-dsh-native-plugin/release-notes.md b/openspec/changes/add-dsh-native-plugin/release-notes.md
index d3a9a0587..98ca6e7a3 100644
--- a/openspec/changes/add-dsh-native-plugin/release-notes.md
+++ b/openspec/changes/add-dsh-native-plugin/release-notes.md
@@ -1,15 +1,15 @@
# SkillHub DSH 原生插件发布说明
-## 0.6.0 候选:启停、检查更新与紧凑卡片
+## 0.6.1 候选:启停、检查更新与紧凑卡片
- **受管理技能启停**:[#1025](https://github.com/iflytek/skillhub/pull/1025) 按确认的位置将 `SKILL.md` 转换为 `SKILL.md.disabled`,启用恢复入口,保留目录和资源。更新停用安装后仍保持停用;文件修改、共享归属、目标被占用及不可信记录仍阻止写操作。停用不撤回已加载会话内容,也不停止任务。
- **统一检查、逐项更新**:[#1030](https://github.com/iflytek/skillhub/pull/1030) 主动只读检查当前来源库存,渐进显示结果和失败重试。一次可信快照、有界并发、同来源技能共用远端 resolve;取消及上下文变化不接纳迟到结果。真正更新仍进入详情按位置确认,不提供批量自动覆盖。
- **日常围绕使用技能**:[#1032](https://github.com/iflytek/skillhub/pull/1032) 已安装页采用桌面两列、窄屏一列的紧凑横向卡片,显示真实本地名称、一行摘要和实际安装版本;至多两个语义标签。整卡进入详情,保留使用技能主按钮,管理安装/检查更新收进更多菜单。单卡检查不清除其他卡结果,使用和菜单不误触详情。
-- **库存兼容**:共享库存保持严格 v2,不写入 `installationState`;启停状态从可信的 `SKILL.md` / `SKILL.md.disabled` 入口派生,不要求 CLI 用户同步升级。已发布 CLI `0.2.0` 的正常查询和普通目标管理必须继续兼容;默认更新、同步及 Suite 不得无意重新启用停用目标。旧 CLI 显式 force 重装、更新、同步仍按原覆盖语义执行,可能恢复标准入口;显式 remove 仍可删除,新版重新读取须反映真实变化。 未发布 v3 的安全转换、普通/启停日志兼容隔离及真实旧包交替验收正在进行;未通过前阻断发布。
+- **库存兼容**:共享库存保持严格 v2,不写入 `installationState`;启停状态从可信的 `SKILL.md` / `SKILL.md.disabled` 入口派生,不要求 CLI 用户同步升级。已发布 CLI `0.2.0` 的正常查询和普通目标管理必须继续兼容;默认更新、同步及 Suite 不得无意重新启用停用目标。旧 CLI 显式 force 重装、更新、同步仍按原覆盖语义执行,可能恢复标准入口;显式 remove 仍可删除,新版重新读取须反映真实变化。 [#1033](https://github.com/iflytek/skillhub/pull/1033) 已完成未发布 v3 的安全转换、普通/启停日志兼容隔离及真实旧包交替验收。
- **交付边界**:保留现有双 RC、Node/Cordis 与来源边界;详情官网链接仅使用 API 的安全 `webUrl`,缺失即隐藏。该增量不包含 SaaS 部署、场景助手或技能清单导入导出。
-上述包含已合并功能及正在实施的发布前兼容修复,不声明 `0.6.0`/CLI `0.3.0` 已发布,也不把历史功能验收等同本轮最终候选、跨平台恢复或公开包验收。发布顺序和限制见 [发布指南](release-guide.md),结果由 [验证记录](verification.md) 单独维护。
+上述功能及库存兼容修复已合并。CLI `0.3.0` 已发布;DSH `0.6.1` 仍是候选,不把历史功能验收等同最终版本包或公开包验收。发布顺序和限制见 [发布指南](release-guide.md),结果由 [验证记录](verification.md) 单独维护。
## 0.5.0 技能可用性诊断
@@ -20,7 +20,7 @@
## 版本与交付状态
-当前准备版本为 DSH `0.6.0` 和配套 CLI `0.3.0`,尚不据此声明已公开发布。历史版本说明保留;实际公开状态以对应 npm 版本和专用发布工作流为准。
+当前准备版本为 DSH `0.6.1`;CLI `0.3.0` 已发布并完成公开包验收,不重复发布。保留已存在的 `dsh-v0.6.0` 标签,DSH 候选使用新标签 `dsh-v0.6.1`,尚不据此声明 DSH 已公开发布。历史版本说明保留;实际公开状态以对应 npm 版本和专用发布工作流为准。
`@astron-team/skillhub-dsh@0.3.1` 已发布,独立标签为 `dsh-v0.3.1`。[发布准备 PR #995](https://github.com/iflytek/skillhub/pull/995) 已合并,[专用发布工作流](https://github.com/iflytek/skillhub/actions/runs/39556075587) 成功;[公开 npm 包](https://www.npmjs.com/package/@astron-team/skillhub-dsh/v/0.3.1) 与工作流产物逐字节一致,发布时 `latest` 指向 `0.3.1`。两版 Linux 的严格安装、原生探针及浏览器操作复验通过,测试工件与公开包字节一致,结果见 [验证摘要](verification.md)。历史已发布版本保持不变。
@@ -110,7 +110,7 @@ Node 保持 `>=24.2.0 <25`。精确兼容与验证边界见 [兼容设计](compa
## 安装、更新与维护
-`0.6.0` 公开可用后,可通过 `dsh plugin --profile skillhub-test add @astron-team/skillhub-dsh@0.6.0 --ignore-scripts` 安装到已有 Web Profile;发布前使用本地候选 tarball。发布前从源码执行 `bun run check` 后生成 `.tgz`,使用固定版本 DSH 的 `plugin ... add` 安装。具体准备、命令、配置和测试流程见 [中文指南](../../../plugins/dsh/README.md)、[英文指南](../../../plugins/dsh/README.en.md) 及 [发布指南](release-guide.md)。
+`0.6.1` 公开可用后,可通过 `dsh plugin --profile skillhub-test add @astron-team/skillhub-dsh@0.6.1 --ignore-scripts` 安装到已有 Web Profile;发布前使用本地候选 tarball。发布前从源码执行 `bun run check` 后生成 `.tgz`,使用固定版本 DSH 的 `plugin ... add` 安装。具体准备、命令、配置和测试流程见 [中文指南](../../../plugins/dsh/README.md)、[英文指南](../../../plugins/dsh/README.en.md) 及 [发布指南](release-guide.md)。
更新或回退前保留原包和校验值、停止新操作并处理待恢复事务,然后停止 Host、安装兼容包并重新启动。浏览器旧资源可强制刷新;这不能代替 Host 依赖重载。禁用或移除包不会卸载技能,不应删除技能目录、inventory 或操作日志。
diff --git a/openspec/changes/add-dsh-native-plugin/verification.md b/openspec/changes/add-dsh-native-plugin/verification.md
index cd54c3174..4d6a1179e 100644
--- a/openspec/changes/add-dsh-native-plugin/verification.md
+++ b/openspec/changes/add-dsh-native-plugin/verification.md
@@ -573,3 +573,8 @@ DSH 0.6.0 发布检查在 Windows / DSH 0.2.1-rc.2 的 `install-host` 阶段两
真实 Windows Node 24.2.0 / npm 11.3.0 的新版 Host lock-only 对照确认:默认 687 个安装位置、51 份 Zod,prefer-dedupe 为 636 个安装位置、1 份 Zod。本机两次完整冷安装仍达到 600 秒上限;减少重复依赖并不证明冷安装超时已解决。
候选提交 `74e6d93b7` 的 [CI 40996963179](https://github.com/iflytek/skillhub/actions/runs/40996963179) 中,旧版 Host 的 Windows Node 24.2.0 完整探针通过,准备耗时 348647ms;Windows Node 24 的准备完成(459541ms),但随后技能可用性探针在 user-install 的 accepted 断言失败。Linux/macOS 基础检查通过,后续官方 Host 和启停矩阵因前置失败未执行,不能算通过。安装工具返回状态与固定失败码需要补充诊断;保留现有断言。最终发布包验收仍待完成。修改验收脚本不等于改变已发布 CLI 或 DSH 产品运行逻辑。
+
+
+后续固定提交 `a431833b33bcf7523949537c50b9409c00c70ce3` 的 [CI 41011761831](https://github.com/iflytek/skillhub/actions/runs/41011761831) 已通过全部 14 项 DSH 检查。Windows / Host 0.2.1-rc.2 的启停与关闭监听场景准备分别耗时 348044ms、295943ms;Host 0.2.0-rc.2 分别为 291219ms、322063ms,均在全新 Profile 中通过真实探针。此前安装接受断言失败本轮未复现;新增诊断保留其固定状态和错误码,不修改断言。
+
+本机部分缓存复测仍在 600787ms 超时,因此上述结果证明该固定提交的 CI 矩阵通过,不保证所有 Windows 环境在 600 秒内完成。旧 `dsh-v0.6.0` 标签保持不变;后续发布候选调整为 DSH 0.6.1,使用独立新标签。CLI 0.3.0 已发布,不重复发布,也不发布 SkillHub 应用 Release。0.6.1 发布身份、安装包及公开 npm 验收按发布流程单独验证。
diff --git a/plugins/dsh/README.en.md b/plugins/dsh/README.en.md
index 173ac76c2..6170ce360 100644
--- a/plugins/dsh/README.en.md
+++ b/plugins/dsh/README.en.md
@@ -12,7 +12,7 @@ Version `0.4.0` uses the independent `dsh-v0.4.0` tag. It adds versioned example
`0.5.0` adds read-only skill availability diagnosis in installed cards and conversations, with separate integrity, discovery and invocation-policy results. It retains the compatibility and inventory format of `0.4.0`.
-`0.6.0` includes managed enable/disable, unified read-only update checks, and compact horizontal installed cards with a More menu. Shared inventory remains v2 without a coordinated CLI upgrade. Compatibility fixes and real historical-package acceptance are in progress; release remains blocked until they pass. Check npm for public availability.
+`0.6.1` includes managed enable/disable, unified read-only update checks, and compact horizontal installed cards with a More menu. Shared inventory remains v2 without a coordinated CLI upgrade. Compatibility acceptance with the actual CLI 0.2.0 package has passed. Check npm for public availability.
## Use an installed skill
@@ -78,8 +78,8 @@ Published `0.1.0` still requires the original Cordis `4.0.4` baseline and does n
Query the exact npm version first and install only after confirming availability. Check DSH, Node and a complete Cordis combination listed above, stop the target Web Profile Host, then install the plugin. A verified local tarball can also be installed using the next section:
```sh
-npm view @astron-team/skillhub-dsh@0.6.0 version
-dsh plugin --profile skillhub-test add @astron-team/skillhub-dsh@0.6.0 --ignore-scripts
+npm view @astron-team/skillhub-dsh@0.6.1 version
+dsh plugin --profile skillhub-test add @astron-team/skillhub-dsh@0.6.1 --ignore-scripts
dsh --profile skillhub-test --host 127.0.0.1 --port 0 --no-open
```
@@ -89,7 +89,7 @@ Replace `skillhub-test` with the intended Web Profile; initial creation is docum
Shared inventory remains strict v2 without a persisted `installationState`; activation is derived from trusted `SKILL.md` / `SKILL.md.disabled` entries. A coordinated CLI upgrade is not required. Published CLI `0.2.0` must retain normal queries and ordinary-target management; default update, sync and Suite paths must not unintentionally enable disabled targets. Historical explicit force install, upgrade and sync retain their overwrite semantics and may restore the standard entry; explicit remove retains deletion semantics. New readers must reflect these actual file changes.
-Reading v1/v2 does not migrate it. Any v3 inventory from unpublished builds requires identity, entry and pending-transaction validation, an original-byte backup under the same lock, and safe conversion to v2. Never restore a stale backup over newer records. Ordinary journals remain readable by historical DSH; toggle journals isolate compatibility impact. Compatibility implementation and real historical-package acceptance are in progress; release remains blocked until they pass.
+Reading v1/v2 does not migrate it. Any v3 inventory from unpublished builds requires identity, entry and pending-transaction validation, an original-byte backup under the same lock, and safe conversion to v2. Never restore a stale backup over newer records. Ordinary journals remain readable by historical DSH; toggle journals isolate compatibility impact. Compatibility acceptance using alternating operations with actual historical packages has passed; check npm for public availability.
Authors provide examples at the fixed `.skillhub/examples.json` path inside each skill package. The file keeps its location after installation and follows the skill's version and fingerprint; no separate global example copy is created. Skills without examples use a custom task; invalid declarations remain ordinary files without exposing invalid examples.
@@ -223,7 +223,7 @@ Choose “管理安装 → 检查可用性” on a conversational installed-skil
The result distinguishes local integrity, current-session discovery, and user/model invocation policies. Shadowing, local modifications and unknown Host state are separate outcomes. A name-only lookup reports Host observations without proving installation ownership or integrity. Diagnosis never modifies, removes or enables/disables a skill, edits a draft, or sends a message. A historical result is not a guarantee about a later invocation.
-## Managed skill enable/disable (0.6.0)
+## Managed skill enable/disable (0.6.1)
Open “管理安装” and choose enable or disable on the explicit location row. User and project copies are independent: disabling the user copy does not disable a project copy. UI and conversation mutations require real human confirmation. `astron_skillhub_disable` / `astron_skillhub_enable` accept fresh installation references, never arbitrary paths.
diff --git a/plugins/dsh/README.md b/plugins/dsh/README.md
index 435aec020..c4a659cf5 100644
--- a/plugins/dsh/README.md
+++ b/plugins/dsh/README.md
@@ -12,7 +12,7 @@ SkillHub 的 DSH 原生插件。在 DSH 侧栏打开 **技能中心**,匿名
`0.5.0` 新增技能卡片及对话中的只读可用性诊断,分别报告文件完整性、宿主发现和调用策略,保留 `0.4.0` 的兼容范围及库存格式。
-`0.6.0` 包含受管理技能启停、统一只读检查更新,以及横向已安装卡片和更多菜单。共享库存继续使用 v2,不要求用户同步升级 CLI;兼容修复及真实旧包验收进行中,未通过前阻断发布。公开可用状态以 npm 为准。
+`0.6.1` 包含受管理技能启停、统一只读检查更新,以及横向已安装卡片和更多菜单。共享库存继续使用 v2,不要求用户同步升级 CLI;已通过与 CLI 0.2.0 的真实旧包兼容验收。公开可用状态以 npm 为准。
## 使用已安装技能
@@ -78,8 +78,8 @@ npm pack --ignore-scripts --pack-destination /absolute/path/to/candidate-output
先查询 npm 上的精确版本,确认可用后安装。核对 DSH、Node 和上表完整 Cordis 组合,停止目标 Web Profile 的 Host,再执行插件安装;也可按下一节安装已验证的本地 tarball:
```sh
-npm view @astron-team/skillhub-dsh@0.6.0 version
-dsh plugin --profile skillhub-test add @astron-team/skillhub-dsh@0.6.0 --ignore-scripts
+npm view @astron-team/skillhub-dsh@0.6.1 version
+dsh plugin --profile skillhub-test add @astron-team/skillhub-dsh@0.6.1 --ignore-scripts
dsh --profile skillhub-test --host 127.0.0.1 --port 0 --no-open
```
@@ -89,7 +89,7 @@ dsh --profile skillhub-test --host 127.0.0.1 --port 0 --no-open
共享库存保持严格 v2,不写入 `installationState`;启停状态从可信的 `SKILL.md` / `SKILL.md.disabled` 入口派生,不要求 CLI 用户同步升级。已发布 CLI `0.2.0` 的正常查询和普通目标管理必须继续兼容;默认更新、同步及 Suite 不得无意重新启用停用目标。旧 CLI 显式 force 重装、更新、同步仍按原覆盖语义执行,可能恢复标准入口;显式 remove 仍可删除,新版重新读取须反映真实变化。
-读取 v1/v2 不迁移。未发布构建产生的 v3 必须在身份、入口与未完成事务校验后,同锁保存原字节备份并安全转换为 v2;不能恢复过时备份覆盖新记录。旧 DSH 普通管理日志保持兼容,启停日志隔离影响。兼容修复及真实旧包验收正在进行,未通过前阻断发布。
+读取 v1/v2 不迁移。未发布构建产生的 v3 必须在身份、入口与未完成事务校验后,同锁保存原字节备份并安全转换为 v2;不能恢复过时备份覆盖新记录。旧 DSH 普通管理日志保持兼容,启停日志隔离影响。已通过真实旧包交替操作的兼容验收;公开可用状态以 npm 为准。
案例由作者随技能包提供固定文件 `.skillhub/examples.json`,安装后保持相同位置,跟随技能版本和指纹,不创建另一个全局案例副本。无案例时使用自定义任务;无效声明按普通文件保留,不提供错误的案例入口。
@@ -228,7 +228,7 @@ dsh plugin --profile skillhub-test remove @astron-team/skillhub-dsh
诊断分别核验本地文件、当前会话加载结果,以及用户/模型调用策略。同名技能生效、本地修改和宿主无法核验会给出不同结论。按技能名查询只说明当前宿主观察到的事实,不证明安装来源或内容完整性。检查不会修改、卸载、启停技能或发送消息;历史结果不保证下一次调用仍然可用。
-## 受管理技能启停(0.6.0)
+## 受管理技能启停(0.6.1)
在“管理安装”中,对明确位置选择“停用”或“启用”。用户级和项目级副本独立管理;停用用户副本不会关闭项目副本。页面与对话均需真实用户确认,取消不写文件。对话工具 `astron_skillhub_disable` / `astron_skillhub_enable` 使用新查询中的位置引用,不能提交自由路径。
diff --git a/plugins/dsh/package.json b/plugins/dsh/package.json
index f0e270fb1..605158007 100644
--- a/plugins/dsh/package.json
+++ b/plugins/dsh/package.json
@@ -1,6 +1,6 @@
{
"name": "@astron-team/skillhub-dsh",
- "version": "0.6.0",
+ "version": "0.6.1",
"private": false,
"type": "module",
"license": "Apache-2.0",
From a77ff18d0731490263ec49a7cb4dae710fcd64ef Mon Sep 17 00:00:00 2001
From: FenjuFu <92919259+FenjuFu@users.noreply.github.com>
Date: Fri, 30 Oct 2026 17:15:59 +0000
Subject: [PATCH 08/16] docs(deploy): clarify device login and configuration
refresh
Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com>
---
docs/09-deployment.md | 23 ++++++++++++++++++++---
docs/skillhub/en/guide/deployment.md | 17 +++++++++++++++++
2 files changed, 37 insertions(+), 3 deletions(-)
diff --git a/docs/09-deployment.md b/docs/09-deployment.md
index af6c3462b..34c9e648b 100644
--- a/docs/09-deployment.md
+++ b/docs/09-deployment.md
@@ -4,8 +4,12 @@
### CLI 设备授权地址
-`skillhub login --no-open --registry https://skills.example.com` 使用设备码授权:
-CLI 请求 `/api/v1/auth/device/code`,用户打开返回的 `/device` 页面并登录、确认授权,
+`skillhub login --no-open --registry https://skills.example.com` 先按 `--token`、
+`SKILLHUB_TOKEN`、该 registry 已保存凭据的顺序解析 Token。仅在未解析到 Token 时
+进入设备码授权;已解析的 Token 若验证失败,不会自动回退到设备码流程。`--no-open`
+只禁止自动打开浏览器,不会跳过 Token 解析。
+
+设备码流程中,CLI 请求 `/api/v1/auth/device/code`,用户打开返回的 `/device` 页面并登录、确认授权,
CLI 再轮询兑换 Token。完成后用 `skillhub whoami --registry https://skills.example.com`
确认登录身份;不要分享设备码、用户码或 Token。
@@ -15,10 +19,23 @@ CLI 再轮询兑换 Token。完成后用 `skillhub whoami --registry https://ski
- 若公开地址也未设置,保留兼容的 `/device` 相对地址,由 CLI 按 registry 的源解析。
正式部署、子路径部署或 Web/API 分离部署应设置正确的 Web 公开地址。
- 显式指定有效的 HTTP(S) 地址或根相对路径时原样使用,支持自定义页面和查询参数。
- 非法地址会阻止服务启动,错误提示只包含配置项名称,不回显配置值。修改后需重启服务。
+ 非法地址会阻止服务启动,错误提示只包含配置项名称,不回显配置值。
+ 宿主机进程修改配置后需重启;Compose 部署修改 `.env.release` 后需重建受影响容器,
+ `docker compose restart` 不会更新容器环境变量。
- Compose 的空配置交由后端处理;Helm 的 `deviceAuthVerificationUri` 默认对应
`/device`,也可显式覆盖。
+对已运行的单机交付栈,仅修改设备授权地址时,在原部署目录保留相同的项目名和 override,执行:
+
+```bash
+make validate-release-config
+docker compose --env-file .env.release -f compose.release.yml up -d --no-deps --force-recreate server
+```
+
+以上命令用于更新已有后端容器;首次部署仍使用完整栈的 `up -d`。若同时修改公开地址等
+其他服务也使用的配置,应一并重建相应服务。重新执行 CLI 登录并核对返回的授权地址,
+不要仅凭配置文件内容判断已生效。
+
`/cli/auth` 是旧版浏览器 loopback 回调入口,需要客户端附带回调参数;它不是输入设备码的页面。
不要将设备授权地址设成这个旧入口。此次配置保护不改变旧客户端回调流程。
diff --git a/docs/skillhub/en/guide/deployment.md b/docs/skillhub/en/guide/deployment.md
index 275b3b92c..111cc383e 100644
--- a/docs/skillhub/en/guide/deployment.md
+++ b/docs/skillhub/en/guide/deployment.md
@@ -38,6 +38,23 @@ Resolve validation errors before starting. Backend startup runs Flyway database
Suite review writes are enabled by default in the single-server release Compose topology; Suite Bundle confirmation is disabled by default. See [Bundle import](suite-bundle.md) for enabling it. For rolling upgrades where old and new backends coexist, follow the [Suite deployment order](https://github.com/iflytek/skillhub/blob/main/docs/25-skill-suites.md).
+## CLI device authorization URL
+
+`skillhub login --no-open --registry https://skills.example.com` first resolves `--token`, then `SKILLHUB_TOKEN`, then saved credentials for that registry. Device authorization starts only when no token is resolved; an invalid token does not automatically fall back. `--no-open` only disables automatic browser launch. Verify the resulting identity with `skillhub whoami --registry https://skills.example.com`.
+
+- When `DEVICE_AUTH_VERIFICATION_URI` is unset, empty, or whitespace, the server appends `/device` to `SKILLHUB_PUBLIC_BASE_URL`, preserving sub-paths and trimming trailing slashes. For example, `https://skills.example.com/portal/` becomes `https://skills.example.com/portal/device`.
+- Without a public base URL, the compatibility fallback is `/device`, resolved by the CLI against the registry origin. Set the actual public Web URL for production, sub-path, or separate Web/API deployments.
+- An explicit valid HTTP(S) URL or root-relative path overrides the default. Invalid values prevent startup without echoing the configured value. `/cli/auth` is the legacy loopback callback, not the device-code entry page.
+
+After changing this setting in `.env.release`, recreate the backend in the existing deployment directory, preserving its Compose project name and overrides:
+
+```bash
+make validate-release-config
+docker compose --env-file .env.release -f compose.release.yml up -d --no-deps --force-recreate server
+```
+
+This updates an already running stack; use the full `up -d` for an initial deployment. A plain `restart` does not reload container environment variables. If other shared settings changed, recreate their affected services too. Retry device authorization and check the returned public URL without sharing codes or tokens. See the [operations reference](https://github.com/iflytek/skillhub/blob/main/docs/09-deployment.md) for the full configuration contract.
+
## Upgrade, stop, and troubleshoot
Before upgrading, record the current image version and configuration, and back up both the database and skill object storage. Set `SKILLHUB_VERSION` to the target release and repeat validation, `pull`, `up -d`, and workflow verification. Rolling back an image does not roll back the database; check migration compatibility first.
From c4c596e1a315aa0128916a32cc9da8394fed9aba Mon Sep 17 00:00:00 2001
From: PerryLink
Date: Sun, 1 Nov 2026 22:47:18 +0800
Subject: [PATCH 09/16] fix(web): clip the suite version cells that still
overflow
#1040 clipped the pinned version column in the suite member table and the
version ledger's member list. Two cells in the same feature kept rendering
the same 60+ character immutable snapshot versions unclipped:
- the version ledger's own version cell, in an 84px grid column;
- the bundle operation detail's member cell, in a 90px grid column.
Both bleed into the neighbouring status/visibility columns. Clip them with
the truncate + title pattern used elsewhere in these tables, and assert the
clipped cell in the existing operation-detail test.
Regenerate docs/frontend-analytics-manifest.json: the marker in
suite-bundle-operation-detail.tsx moved three lines down.
Signed-off-by: PerryLink
---
docs/frontend-analytics-manifest.json | 6 ++--
.../suite-bundle-operation-detail.test.tsx | 31 +++++++++++++++++++
.../suite/suite-bundle-operation-detail.tsx | 7 +++--
.../features/suite/suite-version-ledger.tsx | 8 ++---
4 files changed, 43 insertions(+), 9 deletions(-)
diff --git a/docs/frontend-analytics-manifest.json b/docs/frontend-analytics-manifest.json
index 3b230fb59..6e280c546 100644
--- a/docs/frontend-analytics-manifest.json
+++ b/docs/frontend-analytics-manifest.json
@@ -352,9 +352,9 @@
{"file":"web/src/features/suite/suite-bundle-operation-detail.tsx","line":131,"host":"Button","prop":"data-analytics-id","event":"click","ids":["suite.draft.open"]},
{"file":"web/src/features/suite/suite-bundle-operation-detail.tsx","line":137,"host":"Button","prop":"data-analytics-id","event":"click","ids":["suite.operation.restart"]},
{"file":"web/src/features/suite/suite-bundle-operation-detail.tsx","line":181,"host":"Button","prop":"data-analytics-id","event":"click","ids":["suite.operation.refresh"]},
- {"file":"web/src/features/suite/suite-bundle-operation-detail.tsx","line":280,"host":"Button","prop":"data-analytics-id","event":"click","ids":["skill.detail.open"]},
- {"file":"web/src/features/suite/suite-bundle-operation-detail.tsx","line":310,"host":"ConfirmDialog","prop":"confirmAnalyticsId","event":"click","ids":["suite.operation.cancel.confirm"]},
- {"file":"web/src/features/suite/suite-bundle-operation-detail.tsx","line":310,"host":"ConfirmDialog","prop":"cancelAnalyticsId","event":"click","ids":["suite.operation.cancel.cancel"]},
+ {"file":"web/src/features/suite/suite-bundle-operation-detail.tsx","line":283,"host":"Button","prop":"data-analytics-id","event":"click","ids":["skill.detail.open"]},
+ {"file":"web/src/features/suite/suite-bundle-operation-detail.tsx","line":313,"host":"ConfirmDialog","prop":"confirmAnalyticsId","event":"click","ids":["suite.operation.cancel.confirm"]},
+ {"file":"web/src/features/suite/suite-bundle-operation-detail.tsx","line":313,"host":"ConfirmDialog","prop":"cancelAnalyticsId","event":"click","ids":["suite.operation.cancel.cancel"]},
{"file":"web/src/features/suite/suite-management-actions.tsx","line":123,"host":"Button","prop":"data-analytics-id","event":"click","ids":["suite.draft.reopen"]},
{"file":"web/src/features/suite/suite-management-actions.tsx","line":128,"host":"Button","prop":"data-analytics-id","event":"click","ids":["suite.version.create"]},
{"file":"web/src/features/suite/suite-management-actions.tsx","line":131,"host":"Button","prop":"data-analytics-id","event":"click","ids":["suite.version.yank"]},
diff --git a/web/src/features/suite/suite-bundle-operation-detail.test.tsx b/web/src/features/suite/suite-bundle-operation-detail.test.tsx
index 7823be0f6..21c086c4e 100644
--- a/web/src/features/suite/suite-bundle-operation-detail.test.tsx
+++ b/web/src/features/suite/suite-bundle-operation-detail.test.tsx
@@ -93,6 +93,37 @@ describe('SuiteBundleOperationDetail', () => {
})
})
+ it('clips a long immutable member version so it cannot bleed into the status column', () => {
+ const version = `snapshot-${'a'.repeat(60)}`
+ mocks.operation.data = {
+ operationId: 'operation-1',
+ status: 'CANCELLED',
+ mode: 'CREATE',
+ targetCoordinate: '@global/care-suite',
+ targetVersion: '1.0.0',
+ updatedAt: '2026-09-14T04:00:00Z',
+ members: [{
+ position: 0,
+ redacted: false,
+ coordinate: '@global/member-under-review',
+ status: 'CANCELLED',
+ version,
+ sourceType: 'PACKAGE',
+ relationship: 'ADDED',
+ publishAction: 'CREATE_VERSION',
+ visibility: 'PUBLIC',
+ errors: [],
+ warnings: [],
+ }],
+ }
+
+ render()
+
+ const cell = screen.getByTitle(`v${version}`)
+ expect(cell.className).toContain('truncate')
+ expect(cell.textContent).toBe(`v${version}`)
+ })
+
it('explains when a new member Skill does not exist yet', () => {
mocks.operation.data = {
operationId: 'operation-planned',
diff --git a/web/src/features/suite/suite-bundle-operation-detail.tsx b/web/src/features/suite/suite-bundle-operation-detail.tsx
index 4780f4b44..b44ae730e 100644
--- a/web/src/features/suite/suite-bundle-operation-detail.tsx
+++ b/web/src/features/suite/suite-bundle-operation-detail.tsx
@@ -264,8 +264,11 @@ export function SuiteBundleOperationDetail({ operationId }: { operationId: strin
{member.publishAction ? ` · ${t(`suite.bundle.action.${member.publishAction}`)}` : null}
) : —}
-
- {member.visibility ?? '—'}
v{member.version ?? '—'}
+
+ {member.visibility ?? '—'}
+
+ v{member.version ?? '—'}
+
Date: Mon, 2 Nov 2026 15:28:27 +0800
Subject: [PATCH 10/16] docs(deploy): distinguish runtime and source
configuration updates
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
---
docs/09-deployment.md | 21 ++++++++++++++++-----
docs/skillhub/en/guide/deployment.md | 12 +++++++++---
2 files changed, 25 insertions(+), 8 deletions(-)
diff --git a/docs/09-deployment.md b/docs/09-deployment.md
index 34c9e648b..cf6a7a930 100644
--- a/docs/09-deployment.md
+++ b/docs/09-deployment.md
@@ -25,16 +25,27 @@ CLI 再轮询兑换 Token。完成后用 `skillhub whoami --registry https://ski
- Compose 的空配置交由后端处理;Helm 的 `deviceAuthVerificationUri` 默认对应
`/device`,也可显式覆盖。
-对已运行的单机交付栈,仅修改设备授权地址时,在原部署目录保留相同的项目名和 override,执行:
+对已运行的单机交付栈,仅修改设备授权地址时,先编辑原部署目录中的 `.env.release`:
+
+- 源码部署:在包含 `Makefile` 和 `scripts/validate-release-config.sh` 的仓库根目录,
+ 先执行 `make validate-release-config`,修正错误后再重建。
+- 一键脚本部署:进入脚本输出的 `Runtime dir`(即该实例的 `--home` 目录)。脚本仅下载
+ Compose 清单和环境模板,不提供上述 Make 校验入口,不要在该目录执行 `make`。
+
+以下重建命令适用于未自定义项目名、未使用 override 的标准栈:
```bash
-make validate-release-config
docker compose --env-file .env.release -f compose.release.yml up -d --no-deps --force-recreate server
```
-以上命令用于更新已有后端容器;首次部署仍使用完整栈的 `up -d`。若同时修改公开地址等
-其他服务也使用的配置,应一并重建相应服务。重新执行 CLI 登录并核对返回的授权地址,
-不要仅凭配置文件内容判断已生效。
+若原部署使用了 `-p`、`COMPOSE_PROJECT_NAME` 或额外的 `-f` 文件,重建时必须保留原项目名、
+环境文件及按原顺序排列的全部 `-f` 参数,不能直接照抄以上基础命令;否则可能操作另一实例
+或丢失 override 中的配置。此操作只重建已有后端容器,不重新下载部署文件。
+
+首次部署仍使用完整栈的 `up -d`。若同时修改公开地址等其他服务也使用的配置,
+应一并重建相应服务。验证设备授权地址时,使用未传 `--token`、未设置 `SKILLHUB_TOKEN`,
+且该 registry 没有保存凭据的 CLI 环境,再执行上述登录命令并核对返回地址;
+已有 Token 的登录成功不能证明设备授权地址已更新。不要仅凭配置文件内容判断已生效。
`/cli/auth` 是旧版浏览器 loopback 回调入口,需要客户端附带回调参数;它不是输入设备码的页面。
不要将设备授权地址设成这个旧入口。此次配置保护不改变旧客户端回调流程。
diff --git a/docs/skillhub/en/guide/deployment.md b/docs/skillhub/en/guide/deployment.md
index 111cc383e..a7201bac5 100644
--- a/docs/skillhub/en/guide/deployment.md
+++ b/docs/skillhub/en/guide/deployment.md
@@ -46,14 +46,20 @@ Suite review writes are enabled by default in the single-server release Compose
- Without a public base URL, the compatibility fallback is `/device`, resolved by the CLI against the registry origin. Set the actual public Web URL for production, sub-path, or separate Web/API deployments.
- An explicit valid HTTP(S) URL or root-relative path overrides the default. Invalid values prevent startup without echoing the configured value. `/cli/auth` is the legacy loopback callback, not the device-code entry page.
-After changing this setting in `.env.release`, recreate the backend in the existing deployment directory, preserving its Compose project name and overrides:
+Edit `.env.release` in the existing deployment directory before recreating the backend:
+
+- Source checkout: run `make validate-release-config` from the repository root containing `Makefile` and `scripts/validate-release-config.sh`, and resolve errors before recreation.
+- One-click script installation: use the `Runtime dir` printed by the script (the instance's `--home` directory). The script downloads the Compose file and environment template, not the Make validation tooling; do not run `make` in that directory.
+
+The following command applies to a standard stack without a custom project name or override files:
```bash
-make validate-release-config
docker compose --env-file .env.release -f compose.release.yml up -d --no-deps --force-recreate server
```
-This updates an already running stack; use the full `up -d` for an initial deployment. A plain `restart` does not reload container environment variables. If other shared settings changed, recreate their affected services too. Retry device authorization and check the returned public URL without sharing codes or tokens. See the [operations reference](https://github.com/iflytek/skillhub/blob/main/docs/09-deployment.md) for the full configuration contract.
+If the original deployment uses `-p`, `COMPOSE_PROJECT_NAME`, or additional `-f` files, retain its project name, environment file and every `-f` argument in the original order. Do not copy the base command unchanged: it could target another instance or omit override settings. This only recreates the existing backend; it does not download deployment files again.
+
+Use the full `up -d` for an initial deployment. A plain `restart` does not reload container environment variables. If other shared settings changed, recreate their affected services too. To verify the device URL, retry the login command without `--token`, `SKILLHUB_TOKEN`, or saved credentials for that registry, and check the returned public URL. Successful login with an existing token does not verify the device URL. Do not share codes or tokens. See the [operations reference](https://github.com/iflytek/skillhub/blob/main/docs/09-deployment.md) for the full configuration contract.
## Upgrade, stop, and troubleshoot
From 36824d33b75f3e84c994b966013d3b2c110305f1 Mon Sep 17 00:00:00 2001
From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
Date: Mon, 2 Nov 2026 15:38:30 +0800
Subject: [PATCH 11/16] docs: complete remaining deployment and navigation gaps
in issue 1042
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
---
README.md | 19 ++++++++++++++++-
README_zh.md | 16 +++++++++++++-
docs/06-api-design.md | 17 +++++++++++++++
docs/README.md | 1 +
docs/frontend-analytics-integration.md | 22 ++++++++++++++++++--
docs/security-scanning.md | 9 ++++++++
docs/skillhub/.vitepress/config.ts | 2 ++
docs/skillhub/en/guide/frontend-analytics.md | 10 +++++++++
docs/skillhub/en/guide/index.md | 1 +
docs/skillhub/guide/frontend-analytics.md | 10 +++++++++
docs/skillhub/guide/index.md | 1 +
weekly/scripts/validate_site.py | 11 ++++++++++
weekly/site/reports/2026-W44/index.html | 2 +-
weekly/source.json | 2 +-
14 files changed, 117 insertions(+), 6 deletions(-)
create mode 100644 docs/skillhub/en/guide/frontend-analytics.md
create mode 100644 docs/skillhub/guide/frontend-analytics.md
diff --git a/README.md b/README.md
index 73b300d97..13b973023 100644
--- a/README.md
+++ b/README.md
@@ -282,11 +282,28 @@ curl -fsSL https://imageless.oss-cn-beijing.aliyuncs.com/runtime.sh | sh -s -- u
| Parameter | Description | Example |
|-----------|-------------|---------|
| `--public-url ` | Public access URL (recommended) | `--public-url https://skill.example.com` |
-| `--version ` | Specific image tag | `--version v0.2.0` |
+| `--version ` | Specific image tag | `--version v0.2.25` |
+| `--ref ` | Git ref for deployment files (GitHub raw mode) | `--ref v0.2.25` |
| `--aliyun` | Use Aliyun mirror (China) | `--aliyun` |
| `--home ` | Runtime directory | `--home /opt/skillhub` |
| `--no-scanner` | Disable security scanner | `--no-scanner` |
+`--version` selects image tags; it does not pin the deployment files. By default,
+those files come from `main`. To pin the installer, Compose file, environment
+template and application image tag together, use a released Git ref and tag:
+
+```bash
+curl -fsSL https://raw.githubusercontent.com/iflytek/skillhub/v0.2.25/scripts/runtime.sh | sh -s -- up --ref v0.2.25 --version v0.2.25 --home /opt/skillhub --public-url https://skillhub.your-company.com
+```
+
+This example uses GitHub raw and requires access to it. `--aliyun` switches runtime
+file downloads to the OSS root, where `--ref` does not select a versioned directory.
+Likewise, a custom `SKILLHUB_RAW_BASE` takes precedence over the Git-ref URL.
+Neither should be described as pinning deployment files with `--ref`.
+The script preserves an existing `.env.release`; record its settings and explicit
+image overrides separately. Pinning the application tag does not freeze every
+dependency image or replace a database backup.
+
> **Important**: Configure `--public-url` for production deployments to ensure CLI install commands and Agent setup instructions display the correct URLs.
For sub-path deployments, keep the public URL and runtime base path aligned in
diff --git a/README_zh.md b/README_zh.md
index cf1305862..875de021b 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -249,11 +249,25 @@ curl -fsSL https://imageless.oss-cn-beijing.aliyuncs.com/runtime.sh | sh -s -- u
| 参数 | 说明 | 示例 |
|------|------|------|
| `--public-url ` | 公网访问地址(推荐配置) | `--public-url https://skill.example.com` |
-| `--version ` | 指定镜像版本 | `--version v0.2.0` |
+| `--version ` | 指定镜像版本 | `--version v0.2.25` |
+| `--ref ` | 指定部署文件的 Git ref(GitHub raw 模式) | `--ref v0.2.25` |
| `--aliyun` | 使用阿里云镜像(国内推荐) | `--aliyun` |
| `--home ` | 指定运行时目录 | `--home /opt/skillhub` |
| `--no-scanner` | 禁用安全扫描服务 | `--no-scanner` |
+`--version` 选择镜像标签,不固定部署文件;部署文件默认取自 `main`。
+如需同时固定安装脚本、Compose 清单、环境模板及应用镜像标签,可使用已发布的 ref 和标签:
+
+```bash
+curl -fsSL https://raw.githubusercontent.com/iflytek/skillhub/v0.2.25/scripts/runtime.sh | sh -s -- up --ref v0.2.25 --version v0.2.25 --home /opt/skillhub --public-url https://skillhub.your-company.com
+```
+
+此例需要能访问 GitHub raw。`--aliyun` 会改用 OSS 根目录下载部署文件,
+此时 `--ref` 不会选择版本子目录;自定义 `SKILLHUB_RAW_BASE` 也优先于按 Git ref 生成的地址,
+不能把这两种情况称为已通过 `--ref` 固定部署文件。
+脚本保留已有 `.env.release`,请另行记录其中配置及显式镜像覆盖值。
+固定应用镜像标签不等于冻结全部依赖镜像,也不能代替数据库备份。
+
> **重要**:生产环境请务必配置 `--public-url`,确保 CLI 安装命令和 Agent 设置指引显示正确的地址。
如果通过 `/skillhub/` 这类子路径对外发布,需要让公网地址和前端基础路径保持一致。
diff --git a/docs/06-api-design.md b/docs/06-api-design.md
index 92b48f7df..5ac2e4889 100644
--- a/docs/06-api-design.md
+++ b/docs/06-api-design.md
@@ -95,6 +95,23 @@ Public API 的可见性规则:
- 若 `latest_version_id = null`,即使 `visibility=PUBLIC`,skill 也不会对外公开,只有 owner 可访问
- `hidden=true` 时,普通访客不可访问;仅 owner 或该 namespace 的 `ADMIN` / `OWNER` 可访问
+### 技能详情的公共页面地址
+
+`GET /api/v1/skills/{namespace}/{slug}` 的 `data.webUrl` 是可空的 URI 字符串,
+指向该技能的 SkillHub 网页详情,而非作者自定义外站地址,也不是 CLI `resolve` 响应字段。
+它只取自部署配置 `skillhub.public.base-url`(环境变量 `SKILLHUB_PUBLIC_BASE_URL`),
+不根据请求的 Host 或转发头推测地址,不改变技能原有访问权限。
+
+- 有效配置须为带主机的 HTTP(S) 地址,不能包含用户凭据、查询参数或片段。
+- 配置缺省、空白或不符合上述校验时返回 `null`;客户端应隐藏该跳转入口,不拼造链接。
+- 保留公开地址中的子路径,去掉结尾斜杠,再追加 `/space/{namespace}/{slug}`。
+ 两个路径段分别按 UTF-8 URL 编码,空格为 `%20`,段内斜杠为 `%2F`。
+- 例如公开地址 `https://skills.example.com/portal/` 和技能 `team/demo`,
+ 返回 `https://skills.example.com/portal/space/team/demo`。
+
+实现与边界测试:[SkillWebUrls](../server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillWebUrls.java)、
+[SkillWebUrlsTest](../server/skillhub-app/src/test/java/com/iflytek/skillhub/service/SkillWebUrlsTest.java)。
+
`GET /api/v1/skills/{namespace}/{slug}/versions/{version}` 的 `data` 字段除版本基础信息外,还必须包含:
- `parsedMetadataJson`:`SKILL.md` frontmatter 的完整 JSON 序列化结果
diff --git a/docs/README.md b/docs/README.md
index e0c505c0f..c437494ca 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -18,6 +18,7 @@ Use the documentation site for user workflows, repository guides for engineering
- [Docker deployment and upgrades](skillhub/en/guide/deployment.md) / [中文](skillhub/guide/deployment.md)
- [Kubernetes](skillhub/en/guide/kubernetes.md) / [中文](skillhub/guide/kubernetes.md)
+- [Optional frontend analytics](skillhub/en/guide/frontend-analytics.md) / [前端动作统计](skillhub/guide/frontend-analytics.md)
- [Deployment reference](09-deployment.md), [object storage](object-storage.md), [SMTP and password reset](19-smtp-password-reset-email-setup.md)
- [Security scanning](security-scanning.md), [observability](observability-developer-guide.md), [private SSO integration](12-private-sso-integration-playbook.md)
diff --git a/docs/frontend-analytics-integration.md b/docs/frontend-analytics-integration.md
index ad1c5e942..183a39df9 100644
--- a/docs/frontend-analytics-integration.md
+++ b/docs/frontend-analytics-integration.md
@@ -160,13 +160,31 @@ window.skillhubAnalytics.stop()
stop 不能撤回已开始的 fetch。重新获准后才可调用 start();同一个 root 重复初始化保留第一次规则和回调,修改规则需要先 stop 再初始化。
-7. 将入口恢复 `enabled: false`,重新创建 edge。保持「Disable cache」勾选(或换一个新的无痕窗口),刷新页面后再搜索:不得加载 script.js、产生采集回调或 send。完全不注入时也应零采集。最后在隔离演示中停止 Umami,再打开新页面搜索和打开技能卡片,应仍正常,无统计依赖导致的页面错误:
+7. **关闭态独立验收**:将入口恢复 `enabled: false`,重新创建 edge:
+
+```sh
+docker compose -f docs/examples/analytics/compose.yaml up -d --no-deps --force-recreate edge
+```
+
+保持「Disable cache」勾选(或换一个新的无痕窗口),刷新页面后搜索:不得加载 script.js、产生采集回调或 send,搜索与技能卡片仍正常。完全不注入时也应零采集。这一步不验证启用态的故障隔离。
+
+8. **启用态依赖故障验收**:仅在上述隔离演示中,将入口重新设为 `enabled: true`,保留第 2 节创建的有效网站 UUID,并确认当前授权允许采集;按第 7 步重新创建 edge,禁用缓存并刷新。先重复第 3~5 步,确认一次测试动作确实入库,作为故障前对照。不要为了测试停止共享平台或生产服务。
+
+保持入口启用和授权有效,仅停止演示 Umami:
```sh
docker compose -f docs/examples/analytics/compose.yaml stop umami
```
-验收后如需继续演示,使用上面的 up 命令恢复。不要为了测试停止共享平台或生产服务。
+在已加载 SDK 的页面再次搜索,应观察到采集请求失败,同时搜索结果和技能卡片仍正常。再新开一个禁用缓存的页面:SDK 请求应失败,但业务仍可用,不能出现统计异常导致的未捕获页面错误。记录失败请求和业务结果;不要把关闭态的零请求当成故障隔离通过,也不要把失败请求当作成功入库。
+
+9. **恢复或清理**:继续演示时执行:
+
+```sh
+docker compose -f docs/examples/analytics/compose.yaml start umami
+```
+
+等待 `http://localhost:8300` 恢复可用后,禁用缓存并刷新 edge 页面,再执行一次新动作,确认请求成功且新事件入库。原页面的 SDK 初始化失败不会保证自动恢复,故障期间丢失的事件也不保证补发。结束演示则按第 7 节关闭采集、恢复入口文件,并仅清理该演示项目;不要删除现有 SkillHub 或共享平台的数据。
## 6. 故障排查
diff --git a/docs/security-scanning.md b/docs/security-scanning.md
index 536ce465a..ae3723c7e 100644
--- a/docs/security-scanning.md
+++ b/docs/security-scanning.md
@@ -81,6 +81,15 @@ Scanner-side optional environment variables:
- `SKILLHUB_SCANNER_HARD_TIMEOUT_SECONDS` (default `930`)
- `SKILLHUB_SCANNER_MAX_UPLOAD_SIZE_BYTES` (default `110100480`, or 105 MiB)
+For `SKILLHUB_SCANNER_MAX_UPLOAD_SIZE_BYTES`, prefer an integer byte value such as
+`110100480` (105 MiB). The scanner also accepts scientific notation when the value
+is an exact finite integer, for example `1.1010048e+08`; this accommodates Helm
+serialization. Fractional values, non-finite values and invalid text are rejected
+at scanner startup rather than silently rounded. Zero and negative integer values
+are clamped to `1` byte, not interpreted as unlimited. Scientific-notation values
+must also fit the platform's supported integer range. This scanner-side limit does
+not override separate proxy or server upload limits.
+
If the LLM variables are absent, the scanner should still run with non-LLM analyzers.
The default timeout ordering is server read timeout (900 seconds), scanner hard timeout
(930 seconds), then pending-message reclaim (960 seconds). A hard timeout exits the scanner process
diff --git a/docs/skillhub/.vitepress/config.ts b/docs/skillhub/.vitepress/config.ts
index 59bbd3cd4..169c1fae4 100644
--- a/docs/skillhub/.vitepress/config.ts
+++ b/docs/skillhub/.vitepress/config.ts
@@ -70,6 +70,7 @@ export default defineConfig({
items: [
{ text: 'Docker 部署', link: '/guide/deployment' },
{ text: 'Kubernetes 部署', link: '/guide/kubernetes' },
+ { text: '前端动作统计', link: '/guide/frontend-analytics' },
{ text: '技能头像与审核', link: '/guide/skill-avatars' },
],
},
@@ -144,6 +145,7 @@ export default defineConfig({
items: [
{ text: 'Deploy with Docker', link: '/en/guide/deployment' },
{ text: 'Deploy on Kubernetes', link: '/en/guide/kubernetes' },
+ { text: 'Frontend analytics', link: '/en/guide/frontend-analytics' },
{ text: 'Skill avatars and moderation', link: '/en/guide/skill-avatars' },
],
},
diff --git a/docs/skillhub/en/guide/frontend-analytics.md b/docs/skillhub/en/guide/frontend-analytics.md
new file mode 100644
index 000000000..3fb30cf95
--- /dev/null
+++ b/docs/skillhub/en/guide/frontend-analytics.md
@@ -0,0 +1,10 @@
+# Frontend analytics
+
+SkillHub does not collect frontend actions or load an analytics SDK by default. Pages expose passive semantic attributes; deployment owners choose an integration under their consent and privacy policies.
+
+The delivered example is a manual integration: the deployment owner maintains the collector entry, adapter, and proxy configuration. Configuration-only activation and lazy loading through Docker/Compose or Helm remain follow-up work, not current deployment instructions.
+
+- [English interface and privacy contract](https://github.com/iflytek/skillhub/blob/main/docs/frontend-analytics.md): event meaning, adapter boundaries, transport fields, and sub-path mapping.
+- [Step-by-step integration guide (Chinese)](https://github.com/iflytek/skillhub/blob/main/docs/frontend-analytics-integration.md): reuse a platform or run an isolated Umami demo, configure rules, verify stored events, separately test disabled collection and enabled dependency failure, then recover or clean up.
+
+A download click expresses intent, not successful download, installation, or execution. Keep administration secrets out of the browser. Platform retention, access control, and compliance belong to the deployment owner. These links follow repository main; pinned deployments should use documentation and examples from the matching tag.
diff --git a/docs/skillhub/en/guide/index.md b/docs/skillhub/en/guide/index.md
index 6b4dcdb35..753d1bc61 100644
--- a/docs/skillhub/en/guide/index.md
+++ b/docs/skillhub/en/guide/index.md
@@ -37,6 +37,7 @@ Read these guides when you need to run your own registry. Choose a deployment me
- [Deploy with Docker](./deployment.md): installation, configuration, upgrades, and backups.
- [Deploy on Kubernetes](./kubernetes.md): cluster and Helm deployment.
+- [Frontend analytics](./frontend-analytics.md): optional manual integration, consent, and failure acceptance.
- [Skill avatars and moderation](./skill-avatars.md): administrator configuration for image moderation providers.
## Develop and integrate
diff --git a/docs/skillhub/guide/frontend-analytics.md b/docs/skillhub/guide/frontend-analytics.md
new file mode 100644
index 000000000..d2e5949b2
--- /dev/null
+++ b/docs/skillhub/guide/frontend-analytics.md
@@ -0,0 +1,10 @@
+# 前端动作统计
+
+SkillHub 默认不采集前端动作、不加载统计 SDK。页面提供被动的语义标签,部署方可按自身授权和隐私策略选择接入。
+
+当前已交付的是手动接入示例:部署方维护采集入口、适配器及代理配置。Docker/Compose 或 Helm 配置化开启、按需加载的能力仍在后续 PR 中推进,不能将其作为当前部署步骤。
+
+- [中文接入指南](https://github.com/iflytek/skillhub/blob/main/docs/frontend-analytics-integration.md):复用已有平台或在隔离环境运行 Umami 演示,配置规则、检查真实入库、分别验证关闭态和启用态故障,最后恢复或清理。
+- [英文接口与隐私契约](https://github.com/iflytek/skillhub/blob/main/docs/frontend-analytics.md):事件含义、适配器边界、传输字段及子路径映射。
+
+点击下载只表示下载意图,不证明下载、安装或执行成功。不要把管理密钥放到浏览器;平台数据保留、访问控制和合规由部署方负责。指南对应仓库 main;固定版本部署应阅读相同 tag 的文档与示例。
diff --git a/docs/skillhub/guide/index.md b/docs/skillhub/guide/index.md
index b6ef57dd7..57247860c 100644
--- a/docs/skillhub/guide/index.md
+++ b/docs/skillhub/guide/index.md
@@ -37,6 +37,7 @@
- [Docker 部署](./deployment.md):安装、配置、升级和备份。
- [Kubernetes 部署](./kubernetes.md):集群和 Helm 部署。
+- [前端动作统计](./frontend-analytics.md):可选手动接入、授权与故障验收。
- [技能头像与审核](./skill-avatars.md):管理员配置图片审核服务。
## 开发与集成
diff --git a/weekly/scripts/validate_site.py b/weekly/scripts/validate_site.py
index 64e4d1dfe..aef060c9e 100755
--- a/weekly/scripts/validate_site.py
+++ b/weekly/scripts/validate_site.py
@@ -50,6 +50,8 @@ class PageParser(HTMLParser):
self.empty_table_count = 0
self.ids: set[str] = set()
self.duplicate_ids: set[str] = set()
+ self.skip_targets: set[str] = set()
+ self.main_ids: set[str] = set()
self.external_assets: list[str] = []
self.non_content_depth = 0
@@ -70,6 +72,12 @@ class PageParser(HTMLParser):
if element_id in self.ids:
self.duplicate_ids.add(element_id)
self.ids.add(element_id)
+ if tag == "main" and element_id:
+ self.main_ids.add(element_id)
+ if tag == "a" and "skip-link" in (values.get("class") or "").split():
+ href = values.get("href") or ""
+ if href.startswith("#"):
+ self.skip_targets.add(href[1:])
if tag == "h1":
self.h1_count += 1
if values.get("role") == "tab":
@@ -237,6 +245,9 @@ def validate(root: Path) -> list[str]:
)
if empty_panels:
errors.append(f"{index_path}: panels without modules: {empty_panels}")
+ for target in sorted(parser.skip_targets):
+ if target not in parser.main_ids:
+ errors.append(f"{index_path}: skip link target #{target} must identify main")
if parser.duplicate_ids:
errors.append(f"{index_path}: duplicate ids: {sorted(parser.duplicate_ids)}")
if parser.external_assets:
diff --git a/weekly/site/reports/2026-W44/index.html b/weekly/site/reports/2026-W44/index.html
index 4ec336fe8..5c4568e70 100644
--- a/weekly/site/reports/2026-W44/index.html
+++ b/weekly/site/reports/2026-W44/index.html
@@ -1246,7 +1246,7 @@ footer {
-
+
先看仓库规模和协作流转,再看已经交付与仍待验证的内容。
事件统计覆盖完整的 7 天周期;库存与开放队列取采集快照。
一、仓库关键信息
diff --git a/weekly/source.json b/weekly/source.json
index 14f613550..d5b2d87b5 100644
--- a/weekly/source.json
+++ b/weekly/source.json
@@ -1,4 +1,4 @@
{
"repository": "https://github.com/XiaoSeS/skillhub-weekly.git",
- "commit": "6c1e530394585fa11ac9ccab370c6b1b28dc0563"
+ "commit": "13a95bb287e341150e5bd95461a2284a7daf6576"
}
From 4334b34ec1994aad5563164131ac78a13bcd5d34 Mon Sep 17 00:00:00 2001
From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
Date: Mon, 2 Nov 2026 15:40:41 +0800
Subject: [PATCH 12/16] docs(scanner): clarify positive scientific notation
range
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
---
docs/security-scanning.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/security-scanning.md b/docs/security-scanning.md
index ae3723c7e..f96d4ffa7 100644
--- a/docs/security-scanning.md
+++ b/docs/security-scanning.md
@@ -86,8 +86,8 @@ For `SKILLHUB_SCANNER_MAX_UPLOAD_SIZE_BYTES`, prefer an integer byte value such
is an exact finite integer, for example `1.1010048e+08`; this accommodates Helm
serialization. Fractional values, non-finite values and invalid text are rejected
at scanner startup rather than silently rounded. Zero and negative integer values
-are clamped to `1` byte, not interpreted as unlimited. Scientific-notation values
-must also fit the platform's supported integer range. This scanner-side limit does
+are clamped to `1` byte, not interpreted as unlimited. Positive scientific-notation
+values must also fit the platform's supported integer range. This scanner-side limit does
not override separate proxy or server upload limits.
If the LLM variables are absent, the scanner should still run with non-LLM analyzers.
From 00a7b8679c4362910115906b8254fb7d7370bd2d Mon Sep 17 00:00:00 2001
From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
Date: Mon, 2 Nov 2026 15:58:35 +0800
Subject: [PATCH 13/16] docs(analytics): provide concise in-site integration
reading
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
---
docs/frontend-analytics-integration.md | 18 +++++++----
docs/frontend-analytics.md | 20 ++++++++-----
.../en/guide/frontend-analytics-guide.md | 12 ++++++++
.../en/guide/frontend-analytics-reference.md | 12 ++++++++
docs/skillhub/en/guide/frontend-analytics.md | 30 +++++++++++++++----
.../guide/frontend-analytics-guide.md | 12 ++++++++
.../guide/frontend-analytics-reference.md | 12 ++++++++
docs/skillhub/guide/frontend-analytics.md | 30 +++++++++++++++----
8 files changed, 122 insertions(+), 24 deletions(-)
create mode 100644 docs/skillhub/en/guide/frontend-analytics-guide.md
create mode 100644 docs/skillhub/en/guide/frontend-analytics-reference.md
create mode 100644 docs/skillhub/guide/frontend-analytics-guide.md
create mode 100644 docs/skillhub/guide/frontend-analytics-reference.md
diff --git a/docs/frontend-analytics-integration.md b/docs/frontend-analytics-integration.md
index 183a39df9..00c69021e 100644
--- a/docs/frontend-analytics-integration.md
+++ b/docs/frontend-analytics-integration.md
@@ -1,10 +1,11 @@
# 前端动作统计接入指南
+
## 1. 先了解边界
SkillHub 页面只提供被动的 `data-analytics-id` 标签,默认不采集、不加载统计 SDK、不发送事件。可选 helper 把允许的原生交互转换成 `{ id, event }`,再交给部署方选择的 adapter。点击下载只表示下载意图,不表示下载、安装成功;提交发布也不表示发布成功。部署方负责授权、隐私合规、保留期限和平台安全。
-本文对应 PR-1(#1010)的手工接入示例,不是产品内置开关。仅通过 Docker/Compose 或 Helm 配置开启、默认关闭和按需加载的能力计划在 PR-2 交付,见 [OpenSpec 变更](../openspec/changes/add-configurable-frontend-analytics/proposal.md)。Issue #989 要等 PR-2 完成并验证后才关闭。精确接口、事件边界和隐私约束以[英文契约](frontend-analytics.md)为准。
+本文对应 PR-1(#1010)的手工接入示例,不是产品内置开关。仅通过 Docker/Compose 或 Helm 配置开启、默认关闭和按需加载的能力计划在 PR-2 交付,见 [OpenSpec 变更](https://github.com/iflytek/skillhub/blob/main/openspec/changes/add-configurable-frontend-analytics/proposal.md)。Issue #989 要等 PR-2 完成并验证后才关闭。精确接口、事件边界和隐私约束以[英文契约][analytics-reference]为准。
## 2. 选择接入路径
@@ -12,8 +13,8 @@ SkillHub 页面只提供被动的 `data-analytics-id` 标签,默认不采集
先确认平台接收地址、公开站点标识、授权策略和 CSP。不要把平台管理密钥放到浏览器。平台不受支持时,需要部署方编写自己的 adapter;当前版本不能只填平台名字就接入任意平台。
-1. 复用 [helper](examples/analytics/observe/v1/skillhub-intents.mjs),不要修改业务页面或引入厂商 SDK。
-2. 参照 [Umami adapter](examples/analytics/observe/v1/umami-adapter.mjs)实现 `send(intent) -> Promise` 和 `stop()`。send 必须消化同步异常和异步拒绝,不向页面抛错;stop 后不能再派发请求,已经开始的 fetch 无法撤回。
+1. 复用 [helper](https://github.com/iflytek/skillhub/blob/main/docs/examples/analytics/observe/v1/skillhub-intents.mjs),不要修改业务页面或引入厂商 SDK。
+2. 参照 [Umami adapter](https://github.com/iflytek/skillhub/blob/main/docs/examples/analytics/observe/v1/umami-adapter.mjs)实现 `send(intent) -> Promise` 和 `stop()`。send 必须消化同步异常和异步拒绝,不向页面抛错;stop 后不能再派发请求,已经开始的 fetch 无法撤回。
3. 由 adapter 验证平台参数、清洗最终发送字段、管理 SDK 加载和超时。不要只清洗 helper 回调后就信任 SDK 自动附加的 URL、标题或身份字段。
4. 在部署方入口中连接 helper 与 adapter,只有策略允许时才初始化:
@@ -122,7 +123,7 @@ node web/scripts/analytics-manifest.mjs --write
node web/scripts/analytics-manifest.mjs
```
-审核生成的 [manifest](frontend-analytics-manifest.json) 差异和原生交互测试。部署方再加一条规则即可。ID 必须匹配 `^[a-z]+(?:\.[a-z]+){2,}$`:至少三段小写字母,不含数字、下划线或资源信息。已有 ID 含义冻结,不能把用户、技能名、版本、路径拼进去。manifest 是版本清单,不是运行时封闭枚举。
+审核生成的 [manifest](https://github.com/iflytek/skillhub/blob/main/docs/frontend-analytics-manifest.json) 差异和原生交互测试。部署方再加一条规则即可。ID 必须匹配 `^[a-z]+(?:\.[a-z]+){2,}$`:至少三段小写字母,不含数字、下划线或资源信息。已有 ID 含义冻结,不能把用户、技能名、版本、路径拼进去。manifest 是版本清单,不是运行时封闭枚举。
## 4. 部署、子路径和 CSP
@@ -132,7 +133,7 @@ edge 仅在 UI HTML 注入一个延迟执行的 module,不改变 API、登录
根路径演示的 CSP 需要允许同源外部 module 和同源连接。保留原有 CSP;nonce-only 或 strict-dynamic 策略不能只靠同源满足。需要部署方协调每响应 nonce 或已批准的 hash/SRI;做不到就不要启用。不要加 unsafe-inline、unsafe-eval 或通配符。还要保留原安全头,注意 Nginx add_header 的 location 继承规则。
-`/skillhub/` 等子路径不是只改一个 URL。先按正常部署方式配置 `SKILLHUB_WEB_BASE_PATH=/skillhub/` 和匹配的同源 API base,再同时调整 UI location、无尾斜线重定向、旁路正则、模块目录/注入 src、两个 Umami 精确端点和 adapter prefix。edge 到 Web 的 proxy_pass 不带尾斜线,保留原 URI,不能再次重写。逐项配置见[英文契约的子路径映射](frontend-analytics.md#supported-sub-path-mapping)。OAuth、公网 HTTPS 和资源地址仍遵守原部署要求。
+`/skillhub/` 等子路径不是只改一个 URL。先按正常部署方式配置 `SKILLHUB_WEB_BASE_PATH=/skillhub/` 和匹配的同源 API base,再同时调整 UI location、无尾斜线重定向、旁路正则、模块目录/注入 src、两个 Umami 精确端点和 adapter prefix。edge 到 Web 的 proxy_pass 不带尾斜线,保留原 URI,不能再次重写。逐项配置见[英文契约的子路径映射][analytics-subpaths]。OAuth、公网 HTTPS 和资源地址仍遵守原部署要求。
## 5. 验收:照这个顺序检查
@@ -215,4 +216,9 @@ docker compose -f docs/examples/analytics/compose.yaml down -v
- 尽力发送,无重试、无持久队列,可能少计。SDK 加载期最多在内存暂存 20 条,默认最多等三秒;超额、失败、超时或 stop 都丢弃。已就绪发送彼此独立,不因一个请求挂起而阻塞后续动作。
- 示例固定 Umami 3.4.1。stop 的派发保护依赖此版本在 fetch 前同步序列化的顺序,升级必须重跑适配器测试和真实浏览器验收;不要同时加载第二个 Umami tracker。
- JSON 字段少不等于匿名传输。IP、User-Agent、Origin 仍到平台;SDK 还带网站、实际 hostname 和后续会话 cache 头。edge 去掉采集代理的 Cookie、Authorization、Referer,并使用 no-referrer。Umami 仍可能据传输元数据推导浏览器、系统、设备、地域和会话。
-- SDK 在清洗前仍会在页面内读取标题、URL、referrer 和自身 localStorage 开关;通用 helper 不读取这些内容。平台日志、数据保留和权限需另审计,详见[隐私契约](frontend-analytics.md#privacy-and-integration-modes)。
+- SDK 在清洗前仍会在页面内读取标题、URL、referrer 和自身 localStorage 开关;通用 helper 不读取这些内容。平台日志、数据保留和权限需另审计,详见[隐私契约][analytics-privacy]。
+
+
+[analytics-reference]: frontend-analytics.md
+[analytics-subpaths]: frontend-analytics.md#supported-sub-path-mapping
+[analytics-privacy]: frontend-analytics.md#privacy-and-integration-modes
diff --git a/docs/frontend-analytics.md b/docs/frontend-analytics.md
index 621b6d0d6..6be1b6162 100644
--- a/docs/frontend-analytics.md
+++ b/docs/frontend-analytics.md
@@ -1,8 +1,9 @@
# Passive frontend analytics attributes
-For step-by-step setup, see the [Chinese integration guide](frontend-analytics-integration.md).
+
+For step-by-step setup, see the [Chinese integration guide][analytics-guide].
This document is the English contract reference. Configuration-only deployment is
-planned in [PR-2](../openspec/changes/add-configurable-frontend-analytics/proposal.md);
+planned in [PR-2](https://github.com/iflytek/skillhub/blob/main/openspec/changes/add-configurable-frontend-analytics/proposal.md);
the current examples remain deployment-owned and opt-in.
SkillHub exposes semantic HTML attributes for separately maintained, optional
@@ -57,7 +58,7 @@ The source inventory covers these domains:
- Notifications/preferences, account/profile/security/token flows, login,
registration/reset/device/CLI authorization, administration and global navigation.
-[The generated manifest](frontend-analytics-manifest.json) is the authoritative
+[The generated manifest](https://github.com/iflytek/skillhub/blob/main/docs/frontend-analytics-manifest.json) is the authoritative
source index: registered routes and their implementations, literal JSX sites,
conditional values, static navigation definitions and forwarding sites. Counts
are recomputed from the TypeScript AST; they are not quotas. Unique IDs are
@@ -166,7 +167,7 @@ incident. Vendor credentials and ingestion endpoints belong outside core.
## Configure rules and add tags
-[The vendor-neutral helper](examples/analytics/observe/v1/skillhub-intents.mjs)
+[The vendor-neutral helper](https://github.com/iflytek/skillhub/blob/main/docs/examples/analytics/observe/v1/skillhub-intents.mjs)
exports `observeSkillhubIntents({ root, rules, onIntent })`, frozen `DEFAULT_RULES`
(the original 12 categories), and pure `resolveRules(rules)`:
@@ -220,7 +221,7 @@ example is not present in the `web/` Docker build context.
## Connect a platform: local Umami v3.4.1 demo
-[The Compose example](examples/analytics/compose.yaml) runs Umami v3.4.1 (MIT),
+[The Compose example](https://github.com/iflytek/skillhub/blob/main/docs/examples/analytics/compose.yaml) runs Umami v3.4.1 (MIT),
Postgres 16 and an optional edge; SkillHub remains your existing web upstream.
From the repository root:
@@ -242,13 +243,13 @@ an isolated local Docker environment with available network pools. Open
`http://localhost:8300` for local setup only. Umami's default `admin` / `umami`
credentials, the public DB password and app secret are DEMO ONLY: change them.
Create a synthetic website in Umami, copy its UUID into
-[`observer.mjs`](examples/analytics/observe/v1/observer.mjs), then explicitly change
+[`observer.mjs`](https://github.com/iflytek/skillhub/blob/main/docs/examples/analytics/observe/v1/observer.mjs), then explicitly change
`enabled` and the consent hook for your local policy. Open SkillHub through
`http://localhost:8081` and exercise an enabled action; inspect the request to
`/_skillhub-umami/api/send` and Umami's event view. Tear down the local demo with
`docker compose -f docs/examples/analytics/compose.yaml down -v` (demo data is removed).
-[`umami-adapter.mjs`](examples/analytics/observe/v1/umami-adapter.mjs) is the only
+[`umami-adapter.mjs`](https://github.com/iflytek/skillhub/blob/main/docs/examples/analytics/observe/v1/umami-adapter.mjs) is the only
vendor-specific code. It loads `/script.js` with `data-auto-track="false"`, so
v3.4.1 installs no pageview, history hook, click listener or performance observer.
`data-host-url` is the same-origin `/_skillhub-umami` prefix, and
@@ -315,7 +316,7 @@ HTML is the deployment rollback; default SkillHub never loads any of this.
Prefer a separately maintained same-origin HTTPS module, optionally inserted by
Nginx `sub_filter`, not an app entrypoint modification. Start with
-[the opt-in edge template](examples/analytics/nginx.conf). It is not loaded by the
+[the opt-in edge template](https://github.com/iflytek/skillhub/blob/main/docs/examples/analytics/nginx.conf). It is not loaded by the
product. Compose substitutes only the two upstream host:port variables. Publish
its HTTP listener on loopback behind your HTTPS terminator. Serve the supplied
`observer.mjs`, helper and adapter outside the app bundle; only enable the entry
@@ -390,3 +391,6 @@ configuration, reload through the normal deployment procedure, and purge cached
HTML. Existing cached versioned JS can remain temporarily but must not be referenced
or initialized. Verify new HTML has no script, no callback/reporting occurs and
SkillHub remains usable. This document does not authorize production changes.
+
+
+[analytics-guide]: frontend-analytics-integration.md
diff --git a/docs/skillhub/en/guide/frontend-analytics-guide.md b/docs/skillhub/en/guide/frontend-analytics-guide.md
new file mode 100644
index 000000000..093e6aafa
--- /dev/null
+++ b/docs/skillhub/en/guide/frontend-analytics-guide.md
@@ -0,0 +1,12 @@
+# Detailed setup (Chinese)
+
+The detailed setup instructions below are currently available in Chinese. Validate the example in an isolated environment first.
+
+[← Back to overview](./frontend-analytics.md)
+
+
+
+[analytics-guide]: ./frontend-analytics-guide.md
+[analytics-reference]: ./frontend-analytics-reference.md
+[analytics-subpaths]: ./frontend-analytics-reference.md#supported-sub-path-mapping
+[analytics-privacy]: ./frontend-analytics-reference.md#privacy-and-integration-modes
diff --git a/docs/skillhub/en/guide/frontend-analytics-reference.md b/docs/skillhub/en/guide/frontend-analytics-reference.md
new file mode 100644
index 000000000..c2736f8ed
--- /dev/null
+++ b/docs/skillhub/en/guide/frontend-analytics-reference.md
@@ -0,0 +1,12 @@
+# Interface and privacy contract
+
+This contract is in English. Detailed setup instructions are currently available in Chinese.
+
+[← Back to overview](./frontend-analytics.md)
+
+
+
+[analytics-guide]: ./frontend-analytics-guide.md
+[analytics-reference]: ./frontend-analytics-reference.md
+[analytics-subpaths]: ./frontend-analytics-reference.md#supported-sub-path-mapping
+[analytics-privacy]: ./frontend-analytics-reference.md#privacy-and-integration-modes
diff --git a/docs/skillhub/en/guide/frontend-analytics.md b/docs/skillhub/en/guide/frontend-analytics.md
index 3fb30cf95..8e21802a3 100644
--- a/docs/skillhub/en/guide/frontend-analytics.md
+++ b/docs/skillhub/en/guide/frontend-analytics.md
@@ -1,10 +1,30 @@
# Frontend analytics
-SkillHub does not collect frontend actions or load an analytics SDK by default. Pages expose passive semantic attributes; deployment owners choose an integration under their consent and privacy policies.
+Collection and SDK loading are off by default. The delivered example is a **manual integration**; configuration-only activation through Docker/Compose or Helm has not been merged. Click events express intent, not successful downloads, publishing, or execution.
-The delivered example is a manual integration: the deployment owner maintains the collector entry, adapter, and proxy configuration. Configuration-only activation and lazy loading through Docker/Compose or Helm remain follow-up work, not current deployment instructions.
+## Connect a platform
-- [English interface and privacy contract](https://github.com/iflytek/skillhub/blob/main/docs/frontend-analytics.md): event meaning, adapter boundaries, transport fields, and sub-path mapping.
-- [Step-by-step integration guide (Chinese)](https://github.com/iflytek/skillhub/blob/main/docs/frontend-analytics-integration.md): reuse a platform or run an isolated Umami demo, configure rules, verify stored events, separately test disabled collection and enabled dependency failure, then recover or clean up.
+1. **Choose a platform**: reuse one or run the isolated Umami demo. Never use demo credentials in production.
+2. **Configure collection**: provide a valid website UUID, selected actions, and an explicit consent check. Deployment owners maintain the entry module, adapter, and proxy.
+3. **Verify before enabling access**: check outgoing fields and stored events, then confirm disabled collection and platform failure leave normal business flows working.
-A download click expresses intent, not successful download, installation, or execution. Keep administration secrets out of the browser. Platform retention, access control, and compliance belong to the deployment owner. These links follow repository main; pinned deployments should use documentation and examples from the matching tag.
+The [detailed setup guide (Chinese)](./frontend-analytics-guide.md) contains commands, rules, sub-paths, and recovery steps. The [interface and privacy contract (English)](./frontend-analytics-reference.md) defines event and adapter boundaries.
+
+## Acceptance checks
+
+| Scenario | Expected result |
+| --- | --- |
+| Disabled by default | No analytics SDK or events; business flows work |
+| Enabled with consent | One corresponding event per action, actually stored; no search terms or identity fields |
+| Platform fails while enabled | Collection fails, but search and skill pages still work |
+| Platform recovers | Refresh to verify new stored events; lost events are not guaranteed to replay |
+
+## Common questions
+
+- **No data?** Check the demo edge entry, consent, website UUID, rules, and cache. HTTP 200 alone does not prove storage.
+- **Can any platform work by setting its URL?** No. Other platforms need an adapter and reviewed proxy endpoints.
+- **Is collection anonymous?** A small JSON payload is not anonymous transport. IP and User-Agent may still reach the platform. Deployment owners handle access control, retention, and compliance.
+
+## Disable collection
+
+Use `window.skillhubAnalytics.stop()` to stop future dispatch in the current page; already dispatched requests cannot be recalled. For persistent disabling, restore `enabled: false`, recreate edge, and refresh with cache disabled. Remove the injection/edge and return to the original Web entry to remove the integration entirely. Follow the detailed guide to clean up only the isolated demo, never shared platform or business data.
diff --git a/docs/skillhub/guide/frontend-analytics-guide.md b/docs/skillhub/guide/frontend-analytics-guide.md
new file mode 100644
index 000000000..378c4e9ef
--- /dev/null
+++ b/docs/skillhub/guide/frontend-analytics-guide.md
@@ -0,0 +1,12 @@
+# 详细接入操作(中文)
+
+以下步骤适用于当前手动接入示例,请先在隔离环境验证。
+
+[← 返回接入概览](./frontend-analytics.md)
+
+
+
+[analytics-guide]: ./frontend-analytics-guide.md
+[analytics-reference]: ./frontend-analytics-reference.md
+[analytics-subpaths]: ./frontend-analytics-reference.md#supported-sub-path-mapping
+[analytics-privacy]: ./frontend-analytics-reference.md#privacy-and-integration-modes
diff --git a/docs/skillhub/guide/frontend-analytics-reference.md b/docs/skillhub/guide/frontend-analytics-reference.md
new file mode 100644
index 000000000..d8b2de725
--- /dev/null
+++ b/docs/skillhub/guide/frontend-analytics-reference.md
@@ -0,0 +1,12 @@
+# 接口与隐私契约(英文)
+
+本页契约正文为英文,详细操作步骤见中文指南。
+
+[← 返回接入概览](./frontend-analytics.md)
+
+
+
+[analytics-guide]: ./frontend-analytics-guide.md
+[analytics-reference]: ./frontend-analytics-reference.md
+[analytics-subpaths]: ./frontend-analytics-reference.md#supported-sub-path-mapping
+[analytics-privacy]: ./frontend-analytics-reference.md#privacy-and-integration-modes
diff --git a/docs/skillhub/guide/frontend-analytics.md b/docs/skillhub/guide/frontend-analytics.md
index d2e5949b2..d5337fcc2 100644
--- a/docs/skillhub/guide/frontend-analytics.md
+++ b/docs/skillhub/guide/frontend-analytics.md
@@ -1,10 +1,30 @@
# 前端动作统计
-SkillHub 默认不采集前端动作、不加载统计 SDK。页面提供被动的语义标签,部署方可按自身授权和隐私策略选择接入。
+默认不采集、不加载统计 SDK。当前提供**手动接入示例**;通过 Docker/Compose 或 Helm 配置直接开启的能力尚未合并。点击事件只表示操作意图,不证明下载、发布或执行成功。
-当前已交付的是手动接入示例:部署方维护采集入口、适配器及代理配置。Docker/Compose 或 Helm 配置化开启、按需加载的能力仍在后续 PR 中推进,不能将其作为当前部署步骤。
+## 如何接入
-- [中文接入指南](https://github.com/iflytek/skillhub/blob/main/docs/frontend-analytics-integration.md):复用已有平台或在隔离环境运行 Umami 演示,配置规则、检查真实入库、分别验证关闭态和启用态故障,最后恢复或清理。
-- [英文接口与隐私契约](https://github.com/iflytek/skillhub/blob/main/docs/frontend-analytics.md):事件含义、适配器边界、传输字段及子路径映射。
+1. **选平台**:已有平台可复用;没有平台可先运行隔离的 Umami 演示。不要把演示密码用于生产。
+2. **配置采集**:准备有效网站 UUID、需要采集的动作与明确授权判断;部署方维护采集入口、适配器和代理。
+3. **验收再开放**:检查请求字段和真实入库,并验证关闭与平台故障不影响正常业务。
-点击下载只表示下载意图,不证明下载、安装或执行成功。不要把管理密钥放到浏览器;平台数据保留、访问控制和合规由部署方负责。指南对应仓库 main;固定版本部署应阅读相同 tag 的文档与示例。
+[详细操作指南(中文)](./frontend-analytics-guide.md)提供命令、规则、子路径与恢复步骤;[接口与隐私契约(英文)](./frontend-analytics-reference.md)定义事件及适配器边界。
+
+## 验收看什么
+
+| 场景 | 应观察到什么 |
+| --- | --- |
+| 默认关闭 | 不加载统计 SDK、不发送事件,业务正常 |
+| 已启用且获得授权 | 一次动作一条对应事件,平台确实入库;不传搜索词或身份字段 |
+| 启用后平台故障 | 请求失败但搜索、技能页面仍可用 |
+| 平台恢复 | 刷新页面后新事件能入库,丢失事件不保证补发 |
+
+## 常见问题
+
+- **没有数据?** 核对是否走了示例 edge 入口、授权、网站 UUID、规则和缓存;HTTP 200 不等于已入库。
+- **任意平台都能只填地址吗?** 不能。其他平台还需适配器和经过审核的代理端点。
+- **算匿名采集吗?** 精简 JSON 不等于匿名;平台仍可能收到 IP、User-Agent 等传输信息。部署方负责访问控制、保留期限与合规。
+
+## 如何关闭
+
+当前页可用 `window.skillhubAnalytics.stop()` 停止后续派发,已发出的请求不能撤回。持续关闭需将入口恢复为 `enabled: false`、重新创建 edge 并禁用缓存刷新;完全撤掉接入则移除注入/edge,回到原 Web 入口。隔离演示的清理步骤见详细指南,不要清理共享平台或业务数据。
From 525f741914ea2f4270b1574ef14eccf41e28d725 Mon Sep 17 00:00:00 2001
From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
Date: Mon, 2 Nov 2026 16:07:35 +0800
Subject: [PATCH 14/16] docs(analytics): document actionable first-time local
setup
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
---
docs/frontend-analytics-integration.md | 22 +--
docs/frontend-analytics.md | 8 +-
docs/skillhub/en/guide/frontend-analytics.md | 133 ++++++++++++++++---
docs/skillhub/guide/frontend-analytics.md | 132 +++++++++++++++---
4 files changed, 240 insertions(+), 55 deletions(-)
diff --git a/docs/frontend-analytics-integration.md b/docs/frontend-analytics-integration.md
index 00c69021e..9d77b1ca2 100644
--- a/docs/frontend-analytics-integration.md
+++ b/docs/frontend-analytics-integration.md
@@ -34,18 +34,20 @@ function stop() { stopObserver(); adapter.stop() }
前提:已有能正常使用的 SkillHub Web;已安装 Docker 和 Compose;能使用示例指定的 Umami 3.4.1、Postgres 16、Nginx 1.30.6 镜像;本机 8300 和 8081 端口空闲。所有 shell 命令从仓库根目录运行。示例会创建独立演示网络和临时数据库,不用于生产持久化。请在隔离环境运行。
+启动前先执行 `docker compose ls --all`,确认专用项目名 `skillhub-analytics-demo` 未被其他项目占用。若已占用,选择一个未使用的新名字,并在本文所有命令中一致替换。所有演示命令显式使用 `-p`,防止继承终端已有的 `COMPOSE_PROJECT_NAME` 而误操作其他部署;换一个克隆目录本身不能隔离 Compose 项目。
+
先检查配置,再启动:
```sh
-docker compose -f docs/examples/analytics/compose.yaml config
-docker compose -f docs/examples/analytics/compose.yaml up -d
+docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml config
+docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml up -d
```
默认上游是 `host.docker.internal:80`,对应 release Compose 的 `WEB_PORT` 默认 80。这里是容器访问宿主机的 Web 入口,不是后端端口,也不是 edge 自己的 8081。若 Web 实际发布在其他端口(比如 18080),先在当前终端导出,再启动。后面所有 compose 命令都要在这个终端里执行,否则重建 edge 时会回到默认的 80:
```sh
export SKILLHUB_WEB_UPSTREAM=host.docker.internal:18080
-docker compose -f docs/examples/analytics/compose.yaml up -d
+docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml up -d
```
原生 Linux Docker 需要 `host.docker.internal` 映射,示例 edge 已包含 `extra_hosts: ["host.docker.internal:host-gateway"]`。Docker Desktop/OrbStack 已提供这个名字。宿主机 Web 必须监听容器可达的地址;如果防火墙或仅回环监听阻止访问,先调整可达性,或填写容器可以访问的其他 `host:port`。不要填写 `http://` 或 URL 路径。
@@ -62,9 +64,9 @@ docker compose -f docs/examples/analytics/compose.yaml up -d
然后重建 edge,使单文件挂载更新可靠生效:
```sh
-docker compose -f docs/examples/analytics/compose.yaml up -d --force-recreate edge
-docker compose -f docs/examples/analytics/compose.yaml exec edge nginx -t
-docker compose -f docs/examples/analytics/compose.yaml exec edge nginx -T
+docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml up -d --force-recreate edge
+docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml exec edge nginx -t
+docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml exec edge nginx -T
```
打开 `http://localhost:8081`,不要从原 Web 入口验收注入。
@@ -164,7 +166,7 @@ stop 不能撤回已开始的 fetch。重新获准后才可调用 start();同
7. **关闭态独立验收**:将入口恢复 `enabled: false`,重新创建 edge:
```sh
-docker compose -f docs/examples/analytics/compose.yaml up -d --no-deps --force-recreate edge
+docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml up -d --no-deps --force-recreate edge
```
保持「Disable cache」勾选(或换一个新的无痕窗口),刷新页面后搜索:不得加载 script.js、产生采集回调或 send,搜索与技能卡片仍正常。完全不注入时也应零采集。这一步不验证启用态的故障隔离。
@@ -174,7 +176,7 @@ docker compose -f docs/examples/analytics/compose.yaml up -d --no-deps --force-r
保持入口启用和授权有效,仅停止演示 Umami:
```sh
-docker compose -f docs/examples/analytics/compose.yaml stop umami
+docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml stop umami
```
在已加载 SDK 的页面再次搜索,应观察到采集请求失败,同时搜索结果和技能卡片仍正常。再新开一个禁用缓存的页面:SDK 请求应失败,但业务仍可用,不能出现统计异常导致的未捕获页面错误。记录失败请求和业务结果;不要把关闭态的零请求当成故障隔离通过,也不要把失败请求当作成功入库。
@@ -182,7 +184,7 @@ docker compose -f docs/examples/analytics/compose.yaml stop umami
9. **恢复或清理**:继续演示时执行:
```sh
-docker compose -f docs/examples/analytics/compose.yaml start umami
+docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml start umami
```
等待 `http://localhost:8300` 恢复可用后,禁用缓存并刷新 edge 页面,再执行一次新动作,确认请求成功且新事件入库。原页面的 SDK 初始化失败不会保证自动恢复,故障期间丢失的事件也不保证补发。结束演示则按第 7 节关闭采集、恢复入口文件,并仅清理该演示项目;不要删除现有 SkillHub 或共享平台的数据。
@@ -208,7 +210,7 @@ docker compose -f docs/examples/analytics/compose.yaml start umami
结束本地演示(会删除演示数据库,先确认无需要保留的数据):
```sh
-docker compose -f docs/examples/analytics/compose.yaml down -v
+docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml down -v
```
限制必须一起评估:
diff --git a/docs/frontend-analytics.md b/docs/frontend-analytics.md
index 6be1b6162..5156ac485 100644
--- a/docs/frontend-analytics.md
+++ b/docs/frontend-analytics.md
@@ -225,9 +225,11 @@ example is not present in the `web/` Docker build context.
Postgres 16 and an optional edge; SkillHub remains your existing web upstream.
From the repository root:
+Before starting, run `docker compose ls --all` and confirm the dedicated project name `skillhub-analytics-demo` is not used by another project. If occupied, choose an unused name and replace it consistently in every command. Explicit `-p` prevents inheriting an existing terminal `COMPOSE_PROJECT_NAME` and targeting another deployment; cloning into a different directory does not itself isolate the Compose project.
+
```sh
-docker compose -f docs/examples/analytics/compose.yaml config
-docker compose -f docs/examples/analytics/compose.yaml up -d
+docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml config
+docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml up -d
```
The default Web upstream is `host.docker.internal:80`, matching the release
@@ -247,7 +249,7 @@ Create a synthetic website in Umami, copy its UUID into
`enabled` and the consent hook for your local policy. Open SkillHub through
`http://localhost:8081` and exercise an enabled action; inspect the request to
`/_skillhub-umami/api/send` and Umami's event view. Tear down the local demo with
-`docker compose -f docs/examples/analytics/compose.yaml down -v` (demo data is removed).
+`docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml down -v` (demo data is removed).
[`umami-adapter.mjs`](https://github.com/iflytek/skillhub/blob/main/docs/examples/analytics/observe/v1/umami-adapter.mjs) is the only
vendor-specific code. It loads `/script.js` with `data-auto-track="false"`, so
diff --git a/docs/skillhub/en/guide/frontend-analytics.md b/docs/skillhub/en/guide/frontend-analytics.md
index 8e21802a3..e344cb864 100644
--- a/docs/skillhub/en/guide/frontend-analytics.md
+++ b/docs/skillhub/en/guide/frontend-analytics.md
@@ -1,30 +1,121 @@
-# Frontend analytics
+# Frontend analytics: first local setup
-Collection and SDK loading are off by default. The delivered example is a **manual integration**; configuration-only activation through Docker/Compose or Helm has not been merged. Click events express intent, not successful downloads, publishing, or execution.
+This walkthrough adds a local demo entry in front of an existing SkillHub Web and verifies events stored in Umami. Collection is off by default. This is a **manual integration example**; configuration-only activation through Docker/Compose or Helm has not been merged.
-## Connect a platform
+## 1. Prepare Web and the example files
-1. **Choose a platform**: reuse one or run the isolated Umami demo. Never use demo credentials in production.
-2. **Configure collection**: provide a valid website UUID, selected actions, and an explicit consent check. Deployment owners maintain the entry module, adapter, and proxy.
-3. **Verify before enabling access**: check outgoing fields and stored events, then confirm disabled collection and platform failure leave normal business flows working.
+Confirm your existing SkillHub Web opens and search works. Not deployed yet? Complete [Docker deployment](./deployment.md) first. This example assumes a local root-path deployment; use the [detailed guide](./frontend-analytics-guide.md) for sub-paths, production proxies, and CSP. Docker, Compose, Git, and unused local ports 8300 and 8081 are required.
-The [detailed setup guide (Chinese)](./frontend-analytics-guide.md) contains commands, rules, sub-paths, and recovery steps. The [interface and privacy contract (English)](./frontend-analytics-reference.md) defines event and adapter boundaries.
-
-## Acceptance checks
-
-| Scenario | Expected result |
+| Address / port | Purpose |
| --- | --- |
-| Disabled by default | No analytics SDK or events; business flows work |
-| Enabled with consent | One corresponding event per action, actually stored; no search terms or identity fields |
-| Platform fails while enabled | Collection fails, but search and skill pages still work |
-| Platform recovers | Refresh to verify new stored events; lost events are not guaranteed to replay |
+| Original SkillHub Web (default 80) | Existing application; this demo does not redeploy it |
+| `http://localhost:8300` | Umami administration: create a website and inspect events |
+| `http://localhost:8081` | New demo SkillHub entry: edge injects the collector script |
-## Common questions
+One-click installation directories do not contain the example sources. Get the demo files from the fixed `v0.2.25` tag in a separate working directory:
-- **No data?** Check the demo edge entry, consent, website UUID, rules, and cache. HTTP 200 alone does not prove storage.
-- **Can any platform work by setting its URL?** No. Other platforms need an adapter and reviewed proxy endpoints.
-- **Is collection anonymous?** A small JSON payload is not anonymous transport. IP and User-Agent may still reach the platform. Deployment owners handle access control, retention, and compliance.
+```sh
+git clone --depth 1 --branch v0.2.25 https://github.com/iflytek/skillhub.git skillhub-analytics-demo
+cd skillhub-analytics-demo
+```
-## Disable collection
+Use an unused destination if that directory exists. This fetches demo assets, not a replacement SkillHub deployment; do not reinstall your existing application. If using an existing checkout, confirm it contains `docs/examples/analytics/compose.yaml`. Run every following shell command from the **repository root in the same terminal**, not the runtime installation directory.
-Use `window.skillhubAnalytics.stop()` to stop future dispatch in the current page; already dispatched requests cannot be recalled. For persistent disabling, restore `enabled: false`, recreate edge, and refresh with cache disabled. Remove the injection/edge and return to the original Web entry to remove the integration entirely. Follow the detailed guide to clean up only the isolated demo, never shared platform or business data.
+The example connects to the original Web on host port 80 by default. If its actual port is 18080, first set:
+
+```sh
+export SKILLHUB_WEB_UPSTREAM=host.docker.internal:18080
+```
+
+Replace `18080` with the actual **Web** port, not the backend port. Do not include `http://` or a path. Web must listen on an address reachable by containers; loopback-only binding or firewall rules can block edge. The example includes the `host-gateway` mapping for native Linux.
+
+## 2. Start Umami and obtain a website UUID
+
+Before starting, run `docker compose ls --all` and confirm the dedicated project name `skillhub-analytics-demo` is not used by another project. If occupied, choose an unused name and replace it consistently in every command. Explicit `-p` prevents inheriting an existing terminal `COMPOSE_PROJECT_NAME` and targeting another deployment; cloning into a different directory does not itself isolate the Compose project.
+
+Check the example configuration, then start only its database and Umami:
+
+```sh
+docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml config
+docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml up -d postgres umami
+```
+
+Wait for `http://localhost:8300`, sign in with demo credentials **`admin` / `umami`**, and change the password immediately. Create a demo website in website management and copy its **UUID**, not its name, URL, or your password.
+
+This Compose uses public demo secrets and a temporary database. It does not provide a production persistence, backup, or access-control solution; do not deploy it unchanged in production.
+
+## 3. Configure the demo actions
+
+Open `docs/examples/analytics/observe/v1/observer.mjs` in the repository. Preserve the imports, `start()`, `stop()`, and other code; replace only `const config` and `hasConsent()` with:
+
+```js
+const config = Object.freeze({
+ enabled: true,
+ website: 'REPLACE_WITH_YOUR_WEBSITE_UUID',
+ prefix: '/_skillhub-umami',
+ rules: DEFAULT_RULES,
+})
+
+// Only for an isolated demo where you explicitly consent to collection.
+// Production integrations must read the actual consent state.
+function hasConsent() { return true }
+```
+
+`DEFAULT_RULES` is already imported and covers existing actions such as search submission and skill cards. The original `skill.example.open` has no corresponding product element, so it is omitted here. Never put administration secrets or personal data in this file, and do not use a fixed `true` for other users.
+
+## 4. Start the demo entry and verify stored events
+
+```sh
+docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml up -d --force-recreate edge
+docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml exec edge nginx -t
+```
+
+Expect a successful Nginx check. Open **`http://localhost:8081`**, rather than the original Web URL, to test collection.
+
+1. In developer tools Network, enable **Disable cache** and **Preserve log**, refresh, and filter `/_skillhub-umami/`. Expect `script.js` to load successfully, without an automatic pageview.
+2. Enter test text and click Search: expect one `api/send` POST. Search again with Enter: expect one additional event.
+3. Inspect the payload: `name` is `search.query.submit`, and `data.event` is `submit`. Search terms and identity fields must be absent. Open a skill card to check `skill.card.open`.
+4. In Umami on port 8300, open the website events view and select the current time range. Confirm matching events and counts. **HTTP 200 alone does not prove storage.** Click events express intent, not successful downloads, publishing, or execution.
+
+| Additional acceptance check | Expected result |
+| --- | --- |
+| Disable collection and refresh | No SDK / event requests; application still works |
+| Stop demo Umami while collection is enabled | Collection fails; search and skill pages still work |
+| Start Umami and refresh | New events are stored; lost events are not guaranteed to replay |
+
+The [detailed guide](./frontend-analytics-guide.md) provides fault-test controls and recovery steps. Operate only this demo, never stop a shared platform.
+
+## 5. Disable and clean up
+
+Run `window.skillhubAnalytics.stop()` in the current page to stop future dispatch; already dispatched requests cannot be recalled. For persistent disabling, restore `enabled: false` and make `hasConsent()` return `false`, then recreate edge and refresh with cache disabled.
+
+
+```sh
+docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml up -d --no-deps --force-recreate edge
+```
+
+To pause the demo while keeping its containers for continued use:
+
+```sh
+docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml stop
+```
+
+The demo database is not a production persistence solution. Back up required data first; stopping is not a durability guarantee. Only when demo data is no longer needed, run:
+
+```sh
+docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml down -v
+```
+
+This removes the example containers and volumes; treat demo data as deleted. Do not run cleanup against the existing SkillHub deployment. Return to the original Web URL afterward.
+
+## Common problems
+
+| Symptom | Check first |
+| --- | --- |
+| Compose file not found | Work from the cloned repository root, not the runtime installation directory |
+| Ports 8300 / 8081 cannot start | Local port conflicts; inspect `docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml logs` |
+| 8081 returns 502 | Web port, container-reachable binding, and firewall; do not use the backend port |
+| No collection requests | Visit 8081, verify enabled / consent, recreate after editing, and refresh with cache disabled |
+| Requests fail or no stored events | Correct UUID, Umami readiness, matching time range, and the actual Network response |
+
+Further reading: [complete setup guide (Chinese)](./frontend-analytics-guide.md) · [interface and privacy contract](./frontend-analytics-reference.md). Minimal fields do not mean anonymous transport: IP and User-Agent may still reach the platform. Deployment owners handle retention, access control, and compliance.
diff --git a/docs/skillhub/guide/frontend-analytics.md b/docs/skillhub/guide/frontend-analytics.md
index d5337fcc2..16d95e83d 100644
--- a/docs/skillhub/guide/frontend-analytics.md
+++ b/docs/skillhub/guide/frontend-analytics.md
@@ -1,30 +1,120 @@
-# 前端动作统计
+# 前端动作统计:首次本地接入
-默认不采集、不加载统计 SDK。当前提供**手动接入示例**;通过 Docker/Compose 或 Helm 配置直接开启的能力尚未合并。点击事件只表示操作意图,不证明下载、发布或执行成功。
+本文带你在已有 SkillHub 前增加一个本地演示入口,连接 Umami 并确认事件入库。默认不采集;当前是**手动接入示例**,通过 Docker/Compose 或 Helm 配置直接开启的方案尚未合并。
-## 如何接入
+## 1. 准备已有 Web 和示例文件
-1. **选平台**:已有平台可复用;没有平台可先运行隔离的 Umami 演示。不要把演示密码用于生产。
-2. **配置采集**:准备有效网站 UUID、需要采集的动作与明确授权判断;部署方维护采集入口、适配器和代理。
-3. **验收再开放**:检查请求字段和真实入库,并验证关闭与平台故障不影响正常业务。
+先确认 SkillHub Web 可以正常打开、搜索。尚未部署?先完成 [Docker 部署](./deployment.md),再回来继续。本例适用于本机根路径部署;子路径、生产代理与 CSP 请用[详细指南](./frontend-analytics-guide.md)。需要 Docker、Compose 和 Git,且 8300、8081 未被占用。
-[详细操作指南(中文)](./frontend-analytics-guide.md)提供命令、规则、子路径与恢复步骤;[接口与隐私契约(英文)](./frontend-analytics-reference.md)定义事件及适配器边界。
-
-## 验收看什么
-
-| 场景 | 应观察到什么 |
+| 地址/端口 | 用途 |
| --- | --- |
-| 默认关闭 | 不加载统计 SDK、不发送事件,业务正常 |
-| 已启用且获得授权 | 一次动作一条对应事件,平台确实入库;不传搜索词或身份字段 |
-| 启用后平台故障 | 请求失败但搜索、技能页面仍可用 |
-| 平台恢复 | 刷新页面后新事件能入库,丢失事件不保证补发 |
+| 原 SkillHub Web(默认 80) | 已有业务服务,本例不会重新部署它 |
+| `http://localhost:8300` | Umami 管理界面,创建网站和查看事件 |
+| `http://localhost:8081` | 本例新增的 SkillHub 访问入口,经过 edge 注入采集脚本 |
+
+一键安装目录只有部署文件,没有本例源码。从固定的 `v0.2.25` tag 获取演示配套,在另一个工作目录执行:
+
+```sh
+git clone --depth 1 --branch v0.2.25 https://github.com/iflytek/skillhub.git skillhub-analytics-demo
+cd skillhub-analytics-demo
+```
+
+目标目录已存在时换用空目录。这只是获取演示配套,不会替换现有 SkillHub,也不要求重装它。使用已有仓库时先确认包含 `docs/examples/analytics/compose.yaml`。以下所有 shell 命令都在**仓库根目录、同一终端**执行,不能在一键安装目录直接运行。
+
+示例默认连接原 Web 的宿主机 80 端口。如果实际是 18080,先设置:
+
+```sh
+export SKILLHUB_WEB_UPSTREAM=host.docker.internal:18080
+```
+
+将 `18080` 换成实际 **Web** 端口,不是后端端口;不要填写 `http://` 或路径。Web 必须监听容器可达的地址,仅回环监听或防火墙限制可能阻止 edge 访问。示例已为原生 Linux 配置 `host-gateway` 映射。
+
+## 2. 启动 Umami,取得网站 UUID
+
+启动前先执行 `docker compose ls --all`,确认专用项目名 `skillhub-analytics-demo` 未被其他项目占用。若已占用,选择一个未使用的新名字,并在本文所有命令中一致替换。所有演示命令显式使用 `-p`,防止继承终端已有的 `COMPOSE_PROJECT_NAME` 而误操作其他部署;换一个克隆目录本身不能隔离 Compose 项目。
+
+先检查示例,再只启动统计数据库和 Umami:
+
+```sh
+docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml config
+docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml up -d postgres umami
+```
+
+等待 `http://localhost:8300` 可访问,用演示账号 **`admin` / `umami`** 登录并立即修改密码。在网站管理中创建一个演示网站,复制它的 **UUID**(不是网站名称、网址或登录密码)。
+
+此 Compose 使用公开演示密钥和临时数据库,不具备生产持久化、备份与访问控制方案,不要直接用作生产部署。
+
+## 3. 配置本次演示要采集的动作
+
+打开仓库中的 `docs/examples/analytics/observe/v1/observer.mjs`,保留现有 imports、`start()`、`stop()` 等代码,仅将 `const config` 和 `hasConsent()` 替换为下列内容:
+
+```js
+const config = Object.freeze({
+ enabled: true,
+ website: '替换为上一步的网站 UUID',
+ prefix: '/_skillhub-umami',
+ rules: DEFAULT_RULES,
+})
+
+// 仅用于本人明确同意采集的隔离演示;正式接入必须读取真实授权状态。
+function hasConsent() { return true }
+```
+
+`DEFAULT_RULES` 已由文件开头导入,包含搜索提交、技能卡片等现有动作。示例原有的 `skill.example.open` 不对应产品元素,这里不使用它。不要将真实管理密钥或个人信息放进该文件,也不要对其他用户直接使用固定的 `true`。
+
+## 4. 启动演示入口,检查真实事件
+
+```sh
+docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml up -d --force-recreate edge
+docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml exec edge nginx -t
+```
+
+`nginx -t` 应成功。打开 **`http://localhost:8081`**,不要继续使用原 Web 地址验收采集。
+
+1. 开发者工具 Network 勾选 **Disable cache** 和 **Preserve log**,刷新页面,筛选 `/_skillhub-umami/`。应成功加载 `script.js`,但不自动发送 pageview。
+2. 搜索框输入测试文字并点击搜索,应有一条 `api/send` POST;再按 Enter 搜索一次,应再增加一条。
+3. 检查 Payload:事件名为 `search.query.submit`,`data.event` 为 `submit`;不得包含搜索词或身份字段。点击技能卡片可检查 `skill.card.open`。
+4. 在 8300 的 Umami 网站事件页选择当前时间范围,确认对应事件及次数。**HTTP 200 本身不能证明已入库**。点击事件只代表意图,不代表下载、发布或执行成功。
+
+| 还需验收 | 预期 |
+| --- | --- |
+| 关闭采集、刷新 | 无 SDK/事件请求,业务正常 |
+| 已启用时停止演示 Umami | 采集失败,搜索与技能页面仍可用 |
+| 启动 Umami、刷新 | 新事件可入库;不保证补发丢失事件 |
+
+上述故障测试的完整对照、恢复步骤见[详细指南](./frontend-analytics-guide.md)。只操作本例统计服务,不要停止共享平台。
+
+## 5. 关闭与清理
+
+当前页执行 `window.skillhubAnalytics.stop()` 可阻止后续派发,已发出的请求不能撤回。持续关闭时,将文件恢复为 `enabled: false`、`hasConsent()` 返回 `false`,再重建 edge 并禁用缓存刷新。
+
+
+```sh
+docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml up -d --no-deps --force-recreate edge
+```
+
+暂时停止演示、保留容器以便继续:
+
+```sh
+docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml stop
+```
+
+演示数据库不是生产持久化方案;需要数据请先备份,不要把停止等同持久化保证。确定不再需要演示数据时才执行:
+
+```sh
+docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml down -v
+```
+
+该命令删除本示例的容器和卷,演示数据应视为删除;不对现有 SkillHub 部署执行清理。关闭后回到原 Web 地址。
## 常见问题
-- **没有数据?** 核对是否走了示例 edge 入口、授权、网站 UUID、规则和缓存;HTTP 200 不等于已入库。
-- **任意平台都能只填地址吗?** 不能。其他平台还需适配器和经过审核的代理端点。
-- **算匿名采集吗?** 精简 JSON 不等于匿名;平台仍可能收到 IP、User-Agent 等传输信息。部署方负责访问控制、保留期限与合规。
+| 现象 | 先检查 |
+| --- | --- |
+| 找不到 Compose 文件 | 当前目录应是克隆仓库根目录,而非 runtime 安装目录 |
+| 8300/8081 无法启动 | 本机端口占用;看 `docker compose -p skillhub-analytics-demo -f docs/examples/analytics/compose.yaml logs` |
+| 8081 返回 502 | Web 端口、容器可达监听和防火墙;不要填后端端口 |
+| 没有采集请求 | 是否访问 8081,入口是否启用/获得授权,修改后是否重建及禁用缓存刷新 |
+| 请求失败或平台无数据 | UUID 是否正确、Umami 是否就绪、时间范围是否匹配;检查 Network 响应 |
-## 如何关闭
-
-当前页可用 `window.skillhubAnalytics.stop()` 停止后续派发,已发出的请求不能撤回。持续关闭需将入口恢复为 `enabled: false`、重新创建 edge 并禁用缓存刷新;完全撤掉接入则移除注入/edge,回到原 Web 入口。隔离演示的清理步骤见详细指南,不要清理共享平台或业务数据。
+进阶:[完整操作指南](./frontend-analytics-guide.md) · [接口与隐私契约(英文)](./frontend-analytics-reference.md)。字段精简不等于匿名传输:IP、User-Agent 等仍可能到达平台,保留期限、访问控制和合规由部署方负责。
From d8d20c403389ce92e5c5e659e586ebfb045a02d2 Mon Sep 17 00:00:00 2001
From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
Date: Mon, 2 Nov 2026 16:13:31 +0800
Subject: [PATCH 15/16] docs(analytics): explicitly state one-click production
deployment is pending
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
---
docs/frontend-analytics-integration.md | 2 ++
docs/frontend-analytics.md | 4 +++-
docs/skillhub/en/guide/frontend-analytics.md | 6 +++++-
docs/skillhub/guide/frontend-analytics.md | 6 +++++-
4 files changed, 15 insertions(+), 3 deletions(-)
diff --git a/docs/frontend-analytics-integration.md b/docs/frontend-analytics-integration.md
index 9d77b1ca2..594e624b0 100644
--- a/docs/frontend-analytics-integration.md
+++ b/docs/frontend-analytics-integration.md
@@ -3,6 +3,8 @@
## 1. 先了解边界
+当前是手动、本地演示接入,生产一键配置尚未交付;新方案合并并验证后再更新本指南。
+
SkillHub 页面只提供被动的 `data-analytics-id` 标签,默认不采集、不加载统计 SDK、不发送事件。可选 helper 把允许的原生交互转换成 `{ id, event }`,再交给部署方选择的 adapter。点击下载只表示下载意图,不表示下载、安装成功;提交发布也不表示发布成功。部署方负责授权、隐私合规、保留期限和平台安全。
本文对应 PR-1(#1010)的手工接入示例,不是产品内置开关。仅通过 Docker/Compose 或 Helm 配置开启、默认关闭和按需加载的能力计划在 PR-2 交付,见 [OpenSpec 变更](https://github.com/iflytek/skillhub/blob/main/openspec/changes/add-configurable-frontend-analytics/proposal.md)。Issue #989 要等 PR-2 完成并验证后才关闭。精确接口、事件边界和隐私约束以[英文契约][analytics-reference]为准。
diff --git a/docs/frontend-analytics.md b/docs/frontend-analytics.md
index 5156ac485..fdf4b321a 100644
--- a/docs/frontend-analytics.md
+++ b/docs/frontend-analytics.md
@@ -4,7 +4,9 @@
For step-by-step setup, see the [Chinese integration guide][analytics-guide].
This document is the English contract reference. Configuration-only deployment is
planned in [PR-2](https://github.com/iflytek/skillhub/blob/main/openspec/changes/add-configurable-frontend-analytics/proposal.md);
-the current examples remain deployment-owned and opt-in.
+the current examples remain deployment-owned and opt-in. They describe a manual,
+local demo integration; production one-click configuration has not been delivered.
+The guide will be updated after the new flow is merged and validated.
SkillHub exposes semantic HTML attributes for separately maintained, optional
collectors. It does not collect or send analytics by default. There is no SDK,
diff --git a/docs/skillhub/en/guide/frontend-analytics.md b/docs/skillhub/en/guide/frontend-analytics.md
index e344cb864..eaf48ebc4 100644
--- a/docs/skillhub/en/guide/frontend-analytics.md
+++ b/docs/skillhub/en/guide/frontend-analytics.md
@@ -1,6 +1,10 @@
# Frontend analytics: first local setup
-This walkthrough adds a local demo entry in front of an existing SkillHub Web and verifies events stored in Umami. Collection is off by default. This is a **manual integration example**; configuration-only activation through Docker/Compose or Helm has not been merged.
+This walkthrough adds a local demo entry in front of an existing SkillHub Web and verifies events stored in Umami. Collection is off by default.
+
+::: warning Current support scope
+This page covers the delivered manual, local demo integration, not a production one-click deployment. **Production one-click configuration has not been delivered.** The new Docker/Compose and Helm configuration flow is still in progress; this page will be updated after it is merged and validated.
+:::
## 1. Prepare Web and the example files
diff --git a/docs/skillhub/guide/frontend-analytics.md b/docs/skillhub/guide/frontend-analytics.md
index 16d95e83d..a7389dbcf 100644
--- a/docs/skillhub/guide/frontend-analytics.md
+++ b/docs/skillhub/guide/frontend-analytics.md
@@ -1,6 +1,10 @@
# 前端动作统计:首次本地接入
-本文带你在已有 SkillHub 前增加一个本地演示入口,连接 Umami 并确认事件入库。默认不采集;当前是**手动接入示例**,通过 Docker/Compose 或 Helm 配置直接开启的方案尚未合并。
+本文带你在已有 SkillHub 前增加一个本地演示入口,连接 Umami 并确认事件入库。SkillHub 默认不采集。
+
+::: warning 当前支持范围
+本文介绍已交付的手动、本地演示接入,不是生产一键部署指南。**生产一键配置接入尚未交付**;通过 Docker/Compose 或 Helm 配置开启的新方案仍在推进,合并并完成验证后再更新本页。
+:::
## 1. 准备已有 Web 和示例文件
From 250336d3e9a7446feed37e7c0790932bd9d1eef9 Mon Sep 17 00:00:00 2001
From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
Date: Mon, 2 Nov 2026 16:27:55 +0800
Subject: [PATCH 16/16] feat(dsh): isolate installed skills and updates across
registries (#1047)
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
---
cli/src/integrations/installed-projection.ts | 29 ++-
cli/src/integrations/native-contract.d.ts | 4 +
cli/src/integrations/native-runtime.ts | 3 +-
.../integrations/installed-projection.test.ts | 29 ++-
.../changes/add-dsh-native-plugin/README.md | 4 +
.../changes/add-dsh-native-plugin/proposal.md | 4 +
.../registry-switching-design.md | 84 +++++++++
.../specs/dsh-registry-connection/spec.md | 25 +++
.../changes/add-dsh-native-plugin/tasks.md | 8 +
plugins/dsh/README.en.md | 5 +
plugins/dsh/README.md | 5 +
plugins/dsh/src/client/catalog-page.tsx | 45 +++--
plugins/dsh/src/client/config-page.tsx | 28 +--
plugins/dsh/src/client/config-styles.tsx | 6 +-
.../dsh/src/client/conversation-detail.tsx | 2 +-
plugins/dsh/src/client/index.tsx | 20 +-
plugins/dsh/src/client/install-control.tsx | 6 +-
.../client/install-receipt-presentation.ts | 4 +-
plugins/dsh/src/client/install-tool-view.tsx | 4 +-
.../dsh/src/client/installation-progress.tsx | 2 +-
plugins/dsh/src/client/installed-skills.tsx | 19 +-
plugins/dsh/src/client/management-result.ts | 3 +-
.../dsh/src/client/management-tool-view.tsx | 22 +--
.../dsh/src/client/native-control-styles.tsx | 8 +-
plugins/dsh/src/client/registry-config.ts | 47 +++++
plugins/dsh/src/client/registry-selector.tsx | 56 ++++++
.../dsh/src/client/skill-primary-actions.tsx | 6 +-
plugins/dsh/src/client/use-skill.tsx | 4 +-
plugins/dsh/src/host/config.ts | 7 +-
plugins/dsh/src/host/index.ts | 43 +++--
plugins/dsh/src/host/local-runtime.ts | 5 +
plugins/dsh/src/host/management-context.ts | 6 +-
plugins/dsh/src/host/management-tool.ts | 2 +-
plugins/dsh/src/host/types.ts | 3 +-
.../dsh/test/install-control-state.test.tsx | 10 +
.../test/install-receipt-presentation.test.ts | 9 +-
plugins/dsh/test/installed-references.test.ts | 4 +-
plugins/dsh/test/integration/README.md | 24 +++
.../integration/registry-config-component.tsx | 45 +++++
.../integration/registry-config-probe.mjs | 45 +++++
.../registry-switching-browser.mjs | 132 +++++++++++++
.../registry-switching-fixture.mjs | 174 ++++++++++++++++++
.../integration/registry-switching-host.mjs | 67 +++++++
plugins/dsh/test/management-context.test.ts | 4 +-
plugins/dsh/test/management-view.test.tsx | 6 +-
plugins/dsh/test/prepare-skill-use.test.ts | 2 +-
plugins/dsh/test/receipt-context.test.ts | 2 +-
plugins/dsh/test/registry-config.test.ts | 56 ++++++
plugins/dsh/test/registry-isolation.test.ts | 47 +++++
plugins/dsh/test/runtime-execution.test.ts | 1 +
plugins/dsh/test/search-installation.test.ts | 6 +-
plugins/dsh/test/update-check.test.ts | 3 +-
plugins/dsh/test/use-skill.test.ts | 9 +
53 files changed, 1083 insertions(+), 111 deletions(-)
create mode 100644 openspec/changes/add-dsh-native-plugin/registry-switching-design.md
create mode 100644 plugins/dsh/src/client/registry-config.ts
create mode 100644 plugins/dsh/src/client/registry-selector.tsx
create mode 100644 plugins/dsh/test/integration/registry-config-component.tsx
create mode 100644 plugins/dsh/test/integration/registry-config-probe.mjs
create mode 100644 plugins/dsh/test/integration/registry-switching-browser.mjs
create mode 100644 plugins/dsh/test/integration/registry-switching-fixture.mjs
create mode 100644 plugins/dsh/test/integration/registry-switching-host.mjs
create mode 100644 plugins/dsh/test/registry-config.test.ts
create mode 100644 plugins/dsh/test/registry-isolation.test.ts
diff --git a/cli/src/integrations/installed-projection.ts b/cli/src/integrations/installed-projection.ts
index 99192908b..d607f18b1 100644
--- a/cli/src/integrations/installed-projection.ts
+++ b/cli/src/integrations/installed-projection.ts
@@ -6,7 +6,7 @@ import { z } from 'zod'
import { readInstallationState, type InventoryStore } from '../stores/inventory-store'
import { readInstalledSkillMetadata, sameInstalledSkillSource } from '../services/installed-skill-metadata'
-import type { InstalledProjection, InstalledProjectionItem, InstalledProjectionTarget } from './native-contract'
+import type { InstalledProjection, InstalledProjectionItem, InstalledProjectionTarget, AllInstalledProjection } from './native-contract'
export type { InstalledProjection, InstalledProjectionItem, InstalledProjectionTarget } from './native-contract'
const text = z.string().min(1)
@@ -25,6 +25,33 @@ export async function readInstalledProjection(inventory: Pick,
+ targets: readonly InstalledProjectionTarget[]): Promise {
+ let records: unknown[]
+ try {
+ records = z.object({ items: z.array(z.unknown()) }).parse(await inventory.read()).items
+ } catch { return { items: [], complete: false, issues: ['inventory-unreadable'] } }
+ const result: AllInstalledProjection = { items: [], complete: true, issues: [] }
+ const registries = new Map()
+ for (const record of records) {
+ const parsed = itemSchema.safeParse(record)
+ if (parsed.success) {
+ const registry = parsed.data.registry.replace(/\/+$/, '')
+ const bucket = registries.get(registry) ?? []
+ bucket.push(record); registries.set(registry, bucket)
+ }
+ else { result.complete = false; result.issues = ['invalid-record'] }
+ }
+ for (const [registry, sourceRecords] of registries) {
+ const projection = await readInstalledProjectionRecords(sourceRecords, registry, targets)
+ result.items.push(...projection.items.map(item => ({ ...item, registry })))
+ result.complete &&= projection.complete
+ result.issues = [...new Set([...result.issues, ...projection.issues])]
+ }
+ return result
+}
+
/** Internal projection primitive: callers must obtain records from a trusted inventory snapshot. */
export async function readInstalledProjectionRecords(records: readonly unknown[], registry: string,
targets: readonly InstalledProjectionTarget[]): Promise {
diff --git a/cli/src/integrations/native-contract.d.ts b/cli/src/integrations/native-contract.d.ts
index c533ae8c4..c55077224 100644
--- a/cli/src/integrations/native-contract.d.ts
+++ b/cli/src/integrations/native-contract.d.ts
@@ -29,6 +29,9 @@ export interface InstalledProjectionItem {
installationState?: InstallationState
state: 'present' | 'missing' | 'metadata-invalid' | 'mismatch' | 'unreadable'
}
+export interface AllInstalledProjection extends InstalledProjection {
+ items: (InstalledProjectionItem & { registry: string })[]
+}
export interface InstalledProjection {
items: InstalledProjectionItem[]
complete: boolean
@@ -82,6 +85,7 @@ export interface NativeRuntime {
inspectUpgrade(selected: UpgradeSelection, targets: readonly InstalledProjectionTarget[]): Promise
assessUpdate(installed: UpdateIdentity, candidate: UpdateIdentity): UpdateAssessment
openInstalledSnapshot(registry: string, targets: readonly InstalledProjectionTarget[]): Promise<{ projection: InstalledProjection; read(item: InstalledProjectionItem): Promise }>
+ listAllInstalled(targets: readonly InstalledProjectionTarget[]): Promise
listInstalled(registry: string, targets: readonly InstalledProjectionTarget[]): Promise
readonly hostInstanceId: string
listPending(): Promise
diff --git a/cli/src/integrations/native-runtime.ts b/cli/src/integrations/native-runtime.ts
index 16223bace..d88e8dbd5 100644
--- a/cli/src/integrations/native-runtime.ts
+++ b/cli/src/integrations/native-runtime.ts
@@ -5,7 +5,7 @@ import { isDeepStrictEqual } from 'node:util'
import { inspectToggle, executeNativeToggle, recoverToggle } from './native-toggle'
import { readSkillExamples } from '../services/skill-examples'
import { observeInstallationDiscovery } from './install-discovery'
-import { readInstalledProjection, openInstalledProjectionSnapshot } from './installed-projection'
+import { readInstalledProjection, readAllInstalledProjection, openInstalledProjectionSnapshot } from './installed-projection'
import { assessSkillUpdate } from './update-assessment'
import { inspectUpgradeBaseline } from './upgrade-baseline'
import { inspectRemovalBaseline } from './removal-baseline'
@@ -116,6 +116,7 @@ export async function openNativeRuntime(options: { stateDirectory: string; inven
assessUpdate: assessSkillUpdate,
inspectUpgrade: (selected, targets) => inspectUpgradeBaseline(inventory, { ...selected }, targets.map(target => ({ ...target }))),
openInstalledSnapshot: (registry, targets) => openInstalledProjectionSnapshot(inventory, registry, targets),
+ listAllInstalled: targets => readAllInstalledProjection(inventory, targets.map(target => ({ ...target }))),
listInstalled: (registry, targets) => readInstalledProjection(inventory, registry, targets.map(target => ({ ...target }))),
executeInstall: async request => {
await ensureCompatible()
diff --git a/cli/test/unit/integrations/installed-projection.test.ts b/cli/test/unit/integrations/installed-projection.test.ts
index b30732ad2..fafa99b27 100644
--- a/cli/test/unit/integrations/installed-projection.test.ts
+++ b/cli/test/unit/integrations/installed-projection.test.ts
@@ -2,7 +2,7 @@ import { expect, test } from 'bun:test'
import { realpath, mkdtemp, mkdir, rm, rename, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
-import { readInstalledProjection } from '../../../src/integrations/installed-projection'
+import { readInstalledProjection, readAllInstalledProjection } from '../../../src/integrations/installed-projection'
import type { InventoryItem } from '../../../src/stores/inventory-store'
const registry = 'https://registry.example'
@@ -84,3 +84,30 @@ test('entry projection rejects missing, ambiguous and unsafe entries without inv
}
} finally { await rm(root, { recursive: true, force: true }) }
})
+
+
+test('all-source projection keeps trusted source identity and default source filtering', async () => {
+ const root = await realpath(await mkdtemp(join(tmpdir(), 'skillhub-all-sources-')))
+ try {
+ const items: InventoryItem[] = []
+ for (const [scope, source] of [['user', registry], ['project', 'https://other.example']] as const) {
+ const rootDir = join(root, scope)
+ const target = { version: '1.0.0', agent: 'dsh', rootDir, installDir: join(rootDir, 'writer'), installedAt: 'today' }
+ const item = { registry: source, namespace: 'demo', slug: 'writer', targets: [target] }
+ items.push(item)
+ await mkdir(join(target.installDir, '.skillhub'), { recursive: true })
+ await writeFile(join(target.installDir, '.skillhub/metadata.json'), JSON.stringify({ ...item, ...target }))
+ await writeFile(join(target.installDir, 'SKILL.md'), 'content')
+ }
+ const inventory = { read: async () => ({ items }) }
+ const targets = [{ scope: 'user' as const, root: join(root, 'user') }, { scope: 'project' as const, root: join(root, 'project') }]
+ const all = await readAllInstalledProjection(inventory, targets)
+ expect(all.complete).toBe(true)
+ expect(all.items.map(item => [item.registry, item.scope, item.state])).toEqual([
+ [registry, 'user', 'present'], ['https://other.example', 'project', 'present'],
+ ])
+ expect((await readInstalledProjection(inventory, registry, targets)).items).toHaveLength(1)
+ expect(await readAllInstalledProjection({ read: async () => { throw new Error('unreadable') } }, targets))
+ .toEqual({ items: [], complete: false, issues: ['inventory-unreadable'] })
+ } finally { await rm(root, { recursive: true, force: true }) }
+})
diff --git a/openspec/changes/add-dsh-native-plugin/README.md b/openspec/changes/add-dsh-native-plugin/README.md
index a37c57305..b3cc9a4a7 100644
--- a/openspec/changes/add-dsh-native-plugin/README.md
+++ b/openspec/changes/add-dsh-native-plugin/README.md
@@ -63,3 +63,7 @@
- [使用优先页面与二级安装管理](skill-use-first-design.md):日常动作、只读安装选择及精确管理边界。
- [已安装技能统一检查更新](installed-update-check-design.md):用户主动检查、技能级汇总筛选与逐位置更新,第46节跟踪验收。
+
+## 后续增量
+
+- [多来源保存与当前来源更新检查](registry-switching-design.md):范围、设计与本地验证结果;版本发布另行安排。
diff --git a/openspec/changes/add-dsh-native-plugin/proposal.md b/openspec/changes/add-dsh-native-plugin/proposal.md
index 7ac8e3936..baca22657 100644
--- a/openspec/changes/add-dsh-native-plugin/proposal.md
+++ b/openspec/changes/add-dsh-native-plugin/proposal.md
@@ -50,3 +50,7 @@
## 安装级启停增量
为SkillHub可确认归属的DSH安装增加按位置停用与恢复,将入口重命名为 SKILL.md.disabled,保持目录、资源及正文内容不变。用户级与项目级互不隐式联动;会话级调用控制、外部技能管理与SaaS不纳入。扩展共享核心库存和操作日志,CLI兼容及迁移必须先通过门禁,不能将新字段伪装为现有v2。当前设计不代表已实现或已发布;见[启停设计](skill-toggle-design.md)及tasks.md第44节。
+
+## 后续增量:多来源切换
+
+允许保存多个具名来源,同一 Profile/插件实例共享一个当前来源;旧来源安装保留本地使用和管理,更新仅限当前来源。正式设计与验收见 [registry-switching-design.md](registry-switching-design.md),本地实现与验证完成,尚未合并发布。
diff --git a/openspec/changes/add-dsh-native-plugin/registry-switching-design.md b/openspec/changes/add-dsh-native-plugin/registry-switching-design.md
new file mode 100644
index 000000000..32f82b272
--- /dev/null
+++ b/openspec/changes/add-dsh-native-plugin/registry-switching-design.md
@@ -0,0 +1,84 @@
+# 多来源保存与当前来源更新检查(本地实现与验证完成)
+
+本增量由 2026-11-02 需求讨论提出,已实现并完成本地自动化及真实 Host/浏览器验证;版本发布另行安排。本文归属既有 DSH OpenSpec,不创建另一套独立产品协议。
+
+## 已确认范围
+
+- 保存多个具名 Registry,任一时刻仅选一个当前市场来源。首期不聚合多个市场,不新增登录或私有访问能力。
+- 切换市场不删除、移动、停用既有技能,不改写安装的来源身份;已安装技能保留使用及管理能力。
+- 更新检查、可更新筛选和可执行升级入口仅适用于当前来源。其他来源未检查不等于已是最新,不计入本轮成功或失败数量。
+- 切换后取消旧来源只读更新检查,丢弃迟到响应并隐藏旧更新提示。切回后重新检查,旧结果不能直接成为升级授权。
+- 绝不以新来源同名技能替换旧来源安装;新安装遇目标冲突沿用现有保护。
+- 删除保存的来源只删除连接配置,不删除其安装。变更URL是连接目标变化,不能自动迁移既有安装归属。
+- 已提交的文件事务遵守现有事务/恢复机制,切换来源不等于回滚或取消已接受的写入;旧确认引用必须失效。
+- 保持共享库存v2、原有安装记录及CLI兼容,不能为了多来源修改历史库存来源。
+- 升级前查看变化、场景助手、会话加载面板、套件接入均不在本增量范围。
+
+## 当前实现接缝(本地 origin/main 12f850c5a)
+
+- `plugins/dsh/src/host/config.ts`:仅 registryUrl 与 detailTabs;地址缺省为现有默认,显式空串断连。
+- `plugins/dsh/src/client/config-page.tsx`:通过宿主配置表单写入 registryUrl;配置可能只读。
+- `plugins/dsh/src/host/index.ts`:Host 服务持有单一 registry/sourceRevision;重配导致来源上下文变化。
+- `listInstalledForAgent` 传当前 registryUrl 到本地运行时;来源绑定引用贯穿可用性和管理方法。因此“保留其他来源安装管理”必须核对其投影和授权,不能只增加地址下拉框。
+
+## 共享作用域与交互
+
+用户已确认同一 DSH Profile/插件实例共享当前来源,不做每会话独立选择。
+
+- 技能中心顶部提供紧凑的来源选择,具名来源的新增、编辑、删除放在现有设置页;不新增管理中心。
+- 浏览页只展示当前市场。已安装页展示全部可信受管安装,仍以使用技能为主要操作;仅其他来源安装显示一个来源标识,管理在详情页二级区域进行。
+- 其他来源详情仅展示可信本地内容和本地管理,不向当前市场请求同名详情,也不提供错误的官方链接、远程版本或升级按钮。
+- 当前来源更新筛选和汇总只统计当前来源。没有匹配安装时显示“当前来源暂无可检查的技能”。
+- 来源只读或配置服务不可用时禁用修改;保存冲突提示重新读取,不覆盖另一个页面的选择。
+- 删除当前来源同时断开连接,不自动切往别处。断开后远程浏览/检查不可用,本地受管技能仍可使用、诊断、启停和卸载。
+
+## 配置与兼容设计
+
+新增可选 `savedRegistries: Array<{ name: string; url: string }>`,保留 `registryUrl` 为唯一当前地址,不新增 activeId 双重状态。列表缺省为空,旧自定义地址作为当前项可见,首次读取不强制回写。省略 registryUrl 的官方默认和显式空串断开语义保持不变。
+
+地址验证及来源比较复用现有逻辑,不扩大 URL 等价规则;相同来源去重,名称只用于展示,不能成为库存归属。地址不接受凭证、查询串和片段。添加或改名不自动切换;修改当前条目的 URL 时将列表和当前地址原子保存,并按来源切换处理。
+
+复用 Host configForms 的 revision CAS 和订阅:快速选择只写 registryUrl;删除当前项等复合编辑原子提交。其他页面通过配置通知和 Host sourceRevision 丢弃旧读取及确认。无额外数据库、localStorage 或库存迁移。
+
+## 全来源本地管理与更新隔离
+
+- `cli/src/integrations/installed-projection.ts:45` 当前过滤非当前来源;保留现有当前来源投影,新增明确的全部受管来源读取,共用库存/根目录校验。
+- 投影携带可信库存 registry。Host 的 itemKey、locationKey、卡片分组、缓存和导航均使用 registry + namespace + slug(位置键另含 scope/path),不能将两源同名技能合并。
+- `plugins/dsh/src/host/index.ts` 的 readInstalledItem、本地 use/diagnose/toggle/remove 及回执使用重读 item 的真实来源;保持根目录、指纹、修改检测、共享目标、sandbox、库存 CAS 与 sourceRevision 校验。
+- getInstalledDetail 对非当前来源采用本地路径;远程详情仅允许当前来源。显式断开也不短路本地能力。
+- 单项 checkUpdate/prepareUpgrade 在 resolve 前由 Host 拒绝来源不符;批量 scan 保持当前来源快照。UI 隐藏按钮不是授权边界。
+- 切换时清理读取缓存、失效未提交计划、终止旧检查;无法终止的请求通过 revision 丢弃结果。已受理事务保留原始身份和幂等键,继续可查询结果。
+
+## 实施顺序及风险
+
+1. 兼容配置与共享切换,覆盖只读、并发保存和重启。
+2. 全来源可信投影和来源身份贯穿本地管理,保持 CLI 默认查询语义及库存 v2。
+3. 后端当前来源更新限制、扫描统计及竞态处理。
+4. 页面来源选择、旧来源本地详情及精简状态展示。
+5. 回归、真实 Host/浏览器验收及文档;分别记录自动测试与人工预览证据。
+
+重点风险是跨源同名误分组、详情串源、断开时本地能力被旧逻辑短路,以及切换与已受理事务竞态。均需专项用例;不通过放宽既有确认或文件保护解决。回退不迁移库存,新字段可由旧版本忽略,原 registryUrl 仍可读取;旧插件仍按原行为只展示当前来源。
+
+## 验收标准
+
+1. A安装技能后切至B,A技能文件/启停状态不变,已安装仍可识别及使用;管理不得将来源冒认为B。
+2. B下检查更新只访问B,不产生A远程请求;B同名新版不触发A安装的更新提示。
+3. A慢请求与切B竞态:响应晚到不污染B结果;原更新确认不可执行。
+4. B下其他来源安装不计入已最新、检查失败、可更新数量;切回A发起新检查。
+5. A不可用与B不可用独立处理;删除A连接后安装保留。
+6. 同技能双范围安装分别判断版本,来源与安装范围两个维度不可混淆。
+7. 升级已提交时切换来源,真实文件事务最终结果仍可追踪,不重复提交、不伪造取消。
+8. 旧registryUrl配置、显式空串、只读配置、重启、多个页面均有明确兼容行为。
+
+9. 显式断开或删除当前连接后,本地管理仍可用且无远程请求;两源同名技能不合并,旧来源详情不串源。
+10. 真实 DSH 双页面切换和重启保持选择;浏览器验证紧凑布局、按钮行为及错误反馈,提供可访问的本地预览。
+
+
+## 本地验证结果
+
+- 构建、类型检查、lint 与 OpenSpec 严格校验通过;DSH 全量 677 项通过,最终配置序列化修复后补跑配置 10 项与安装 UI 8 项专项。
+- CLI 类型检查、lint、构建通过;全量首轮受环境库存和并发超时干扰,六个失败文件以隔离 HOME、原时限逐一复验通过,保留首次失败记录。
+- 两个支持的 Host(0.2.0-rc.2、0.2.1-rc.2)均运行真实双来源文件事务与浏览器流程。慢响应切换、已受理升级继续、幂等、跨页配置 CAS、断开管理、同名分组通过。
+- 新版同 Profile 真实进程重启后,保存的来源名称及当前选择保留;重启不重复运行配置/安装观察器。
+- 新增可复现入口见 `plugins/dsh/test/integration/README.md`“多来源切换与本地安装管理”。Registry 为本地 HTTP 夹具;未调用模型,不构成 SaaS、跨操作系统或冷安装验收。
+- 真实浏览器曾发现配置 schema 回调无法还原,已改为可序列化结构并新增跨进程序列化回归;不以 mock ConfigForm 通过替代 Host 兼容性。
diff --git a/openspec/changes/add-dsh-native-plugin/specs/dsh-registry-connection/spec.md b/openspec/changes/add-dsh-native-plugin/specs/dsh-registry-connection/spec.md
index 6ed5e71a5..9c2482ad2 100644
--- a/openspec/changes/add-dsh-native-plugin/specs/dsh-registry-connection/spec.md
+++ b/openspec/changes/add-dsh-native-plugin/specs/dsh-registry-connection/spec.md
@@ -94,3 +94,28 @@ The open-source versions endpoint SHALL accept optional `downloadableOnly`, defa
#### Scenario: Existing consumers omit the new parameter
- **WHEN** a management consumer omits downloadableOnly
- **THEN** its authorized lifecycle history remains available and no authentication policy changes
+
+### Requirement: Saved sources with one shared active source
+The plugin SHALL support named saved sources with one active registryUrl shared by a Profile/plugin instance. Configuration writes SHALL preserve revision CAS, omitted defaults and explicit disconnection. Removing a source SHALL NOT remove its installations.
+
+#### Scenario: Shared switch and conflicting edits
+- **WHEN** one page switches from A to B while another holds an old configuration revision
+- **THEN** all pages observe B and stale writes are rejected rather than overwriting the selection
+
+### Requirement: Local management independent of the active market
+The plugin SHALL retain trusted managed installations from other sources, including while disconnected. Local actions SHALL use their original inventory identity and preserve existing filesystem and confirmation protections. Different sources with equal coordinates SHALL NOT merge.
+
+#### Scenario: Old source installation after a switch
+- **WHEN** A has an installation and the active source becomes B or disconnected
+- **THEN** the installation remains locally usable and manageable and its detail does not request B's same-name skill
+
+### Requirement: Update checks restricted to the active source
+The Host SHALL restrict individual and bulk update checks and upgrade preparation to installations from the active source before remote requests. Other-source installations SHALL NOT count as latest, failed or updateable. Switching SHALL discard old results and invalidate unsubmitted plans without abandoning accepted transactions.
+
+#### Scenario: Slow response after switching
+- **WHEN** A's update check completes after switching to B
+- **THEN** its result is discarded, only B participates in current update counts, and switching back to A requires a fresh check before upgrade
+
+#### Scenario: Cross-source upgrade attempt
+- **WHEN** an old-source item is submitted for an update check or upgrade against the active source
+- **THEN** the Host rejects the request before resolve and does not replace its installation with a same-name package
diff --git a/openspec/changes/add-dsh-native-plugin/tasks.md b/openspec/changes/add-dsh-native-plugin/tasks.md
index e6c146691..bfe47ff0c 100644
--- a/openspec/changes/add-dsh-native-plugin/tasks.md
+++ b/openspec/changes/add-dsh-native-plugin/tasks.md
@@ -415,3 +415,11 @@
- [ ] 47.3 补旧对话草稿往返保护的真实浏览器证据,核对候选完整性、旧公开包升级和真实新旧包同 HOME 交替操作、停用默认保护及未发布 v3 安全转换;拒写不计兼容通过。
- [ ] 47.4 完成发布准备 PR、独立评审与 CI,先发布 CLI 0.3.0 并验证,再发布 DSH 0.6.0;核对公开字节、安装与保留原技能。
- [ ] 47.5 同步路线图 #998 的已完成、暂缓及后续范围,记录最终发布结果与证据边界。
+
+## 48. 多来源切换与当前来源更新检查
+
+- [x] 48.1 兼容保存具名来源,复用 Host 配置 CAS、订阅及共享切换。
+- [x] 48.2 全来源可信安装投影、来源身份贯穿本地操作和断开后的管理。
+- [x] 48.3 单项及批量更新只允许当前来源,处理切换竞态和已受理事务。
+- [x] 48.4 紧凑来源选择及其他来源本地详情,保持精简操作。
+- [x] 48.5 完成 registry-switching-design.md 十项验收、CLI 兼容回归和真实 DSH 预览,更新用户文档。
diff --git a/plugins/dsh/README.en.md b/plugins/dsh/README.en.md
index 6170ce360..1016ebf69 100644
--- a/plugins/dsh/README.en.md
+++ b/plugins/dsh/README.en.md
@@ -125,11 +125,16 @@ Open DSH **Plugins → @astron-team/skillhub-dsh → SkillHub 设置**.
3. Choose visibility for versions, security audit and reviews.
4. Select **保存配置** (Save). After the saved confirmation, open **技能中心** and search for a public test skill.
+Add named addresses under **保存的来源** and save, then switch from the Skill Center header. The active source is shared across pages and conversations of the same Profile/plugin instance. Configuration revision checks reject conflicting edits rather than overwriting another page.
+
+Switching preserves existing installations. Installed skills from other trusted sources remain locally usable and manageable, including availability checks, enable/disable and removal; only the current source participates in update checks and upgrades. Same-name skills from different sources remain distinct. Removing the active saved source disconnects without deleting installations; old-source details never borrow current-market content.
+
**已配置** means a nonempty address is saved, not that connectivity has been tested. An empty address disconnects the source. **重置修改** (Reset changes) restores the current Host snapshot without a network reload or save. Read-only connections cannot edit configuration. On a revision conflict, reset the draft before editing the current values.
| Field | Meaning |
| --- | --- |
| `registryUrl` | Registry base URL; defaults to `https://skill.xfyun.cn` when omitted; explicit empty/whitespace disconnects without fallback; custom values are preserved |
+| `savedRegistries` | Optional named sources `[{ name, url }]`; defaults to an empty list. `registryUrl` remains the sole active source |
| `detailTabs.versions` | Version history visibility |
| `detailTabs.securityAudit` | Security audit visibility |
| `detailTabs.reviews` | Read-only review visibility |
diff --git a/plugins/dsh/README.md b/plugins/dsh/README.md
index c4a659cf5..613d2548a 100644
--- a/plugins/dsh/README.md
+++ b/plugins/dsh/README.md
@@ -125,11 +125,16 @@ Loader entry ID 为 `astron-skillhub`;`skillhub` 是 Host/Remote 服务名。
3. 在“详情标签”设置版本、安全审核及评论的显示方式。
4. 点击“保存配置”,看到“配置已保存”后进入侧栏“技能中心”搜索公开测试技能。
+在“保存的来源”添加名称和地址后保存,可从技能中心顶部快速切换。当前来源由同一 Profile/插件实例的所有页面和会话共享;并发编辑通过配置版本校验,冲突时不会覆盖其他页面。
+
+切换来源不删除既有安装。“已安装”保留其他来源的受管技能,支持本地使用、诊断、启停与卸载;更新检查和升级仅适用于当前来源。不同来源同名技能分开处理。删除当前保存来源会断开连接,已安装技能保留;其他来源详情不借用当前市场内容。
+
“已配置”只表示 Host 已保存非空来源,不代表连通测试成功。空地址表示断开来源;“重置修改”恢复当前 Host 配置快照,不发起网络重载或保存。只读连接不可编辑。若保存发生 revision 冲突,先重置修改,再基于当前值编辑。
| 字段 | 行为 |
| --- | --- |
| `registryUrl` | Registry 基础地址;缺省为 `https://skill.xfyun.cn`;显式空串或仅空白断开,不回退;自定义值保留 |
+| `savedRegistries` | 可选具名来源列表 `[{ name, url }]`;缺省为空,`registryUrl` 仍是唯一当前来源 |
| `detailTabs.versions` | 版本历史显示策略 |
| `detailTabs.securityAudit` | 安全审核显示策略 |
| `detailTabs.reviews` | 只读评论显示策略 |
diff --git a/plugins/dsh/src/client/catalog-page.tsx b/plugins/dsh/src/client/catalog-page.tsx
index 7fb4ce62a..5be066a3d 100644
--- a/plugins/dsh/src/client/catalog-page.tsx
+++ b/plugins/dsh/src/client/catalog-page.tsx
@@ -1,3 +1,5 @@
+import { RegistrySelector, type RegistryConfiguration } from './registry-selector.js';
+import { sameRegistry } from './registry-config.js';
import { SkillWebsiteLink } from './skill-website-link.js';
import { UpgradeControl } from './upgrade-control.js';
import { RemovalControl } from './removal-control.js';
@@ -42,13 +44,13 @@ function unwrap(result: { ok: true; value: T } | { ok: false; error: { code:
if (result.ok) return result.value;
throw new Error(catalogErrorMessage(result.error));
}
-export function CatalogPage({ remote, navigation, client, onReturn, installation, installed, availability }: {
- remote: Remote; navigation: CatalogNavigation; client: QueryClient; onReturn: () => void; installation?: InstallationServices; installed?: InstalledServices; availability: UiAvailability;
+export function CatalogPage({ remote, navigation, client, onReturn, installation, installed, availability, registryConfiguration }: {
+ remote: Remote; navigation: CatalogNavigation; client: QueryClient; onReturn: () => void; installation?: InstallationServices; installed?: InstalledServices; availability: UiAvailability; registryConfiguration?: RegistryConfiguration;
}) {
- return ;
+ return ;
}
-function Catalog({ remote, navigation, onReturn, installation, installed, availability }: {
- remote: Remote; navigation: CatalogNavigation; onReturn: () => void; installation?: InstallationServices; installed?: InstalledServices; availability: UiAvailability;
+function Catalog({ remote, navigation, onReturn, installation, installed, availability, registryConfiguration }: {
+ remote: Remote; navigation: CatalogNavigation; onReturn: () => void; installation?: InstallationServices; installed?: InstalledServices; availability: UiAvailability; registryConfiguration?: RegistryConfiguration;
}) {
const queryClient = useQueryClient();
const entries = useSyncExternalStore(availability.subscribe, availability.getSnapshot);
@@ -70,6 +72,15 @@ function Catalog({ remote, navigation, onReturn, installation, installed, availa
const context = useQuery({ queryKey: ['skillhub-context'], queryFn: async () => unwrap(await remote.getContext()), retry: false });
const compatibility = useQuery({ queryKey: ['skillhub-compatibility'], queryFn: async () => unwrap(await remote.getCompatibility()), retry: false });
const revision = context.data?.sourceRevision;
+ const previousRevision = useRef(revision);
+ useLayoutEffect(() => {
+ if (previousRevision.current && revision && previousRevision.current !== revision) {
+ setInstallCandidate(null); setUpgradeCandidate(null); setRemovalCandidate(null);
+ navigation.selected = null; setSelected(null);
+ navigation.installedOrigin = undefined; setInstalledOrigin(undefined);
+ }
+ previousRevision.current = revision;
+ }, [revision, navigation]);
// Share the installed tab's cache and settled-operation invalidation, without blocking market reads.
const inventory = useQuery({ queryKey: ['skillhub-installed', installed?.sessionId, revision],
enabled: !!installed && !!revision && !!context.data?.configured && tab === 'browse', retry: false,
@@ -117,8 +128,8 @@ function Catalog({ remote, navigation, onReturn, installation, installed, availa
setSelected(item);
}
function manageSkill(item: NonNullable['items'][number], intent: 'remove' | 'upgrade') {
- if (intent === 'remove') { if (revision && context.data) setRemovalCandidate({ item, revision, source: context.data.registryUrl }); return; }
- if (revision && context.data) setUpgradeCandidate({ item, revision, source: context.data.registryUrl });
+ if (intent === 'remove') { if (revision && context.data) setRemovalCandidate({ item, revision, source: item.registry ?? context.data.registryUrl }); return; }
+ if (revision && context.data) setUpgradeCandidate({ item, revision, source: item.registry ?? context.data.registryUrl });
}
function error(message: string, retry: () => void) {
return ;
@@ -160,7 +171,7 @@ function Catalog({ remote, navigation, onReturn, installation, installed, availa
}} />}
SkillHub/{selected ? '技能详情' : '技能广场'}
-
+
{registryConfiguration && }
{(installation || installed) && }
{sourceOpen &&
当前来源:{context.data?.registryUrl || '尚未配置来源'}
}
@@ -168,8 +179,7 @@ function Catalog({ remote, navigation, onReturn, installation, installed, availa
{compatibility.isError ? '无法读取 DSH 依赖版本核验结果。' : `当前 DSH 依赖组合未通过核验,安装、升级、卸载和恢复写入已禁用。请使用 ${compatibility.data?.candidateDshVersion} 对应的完整发行依赖。`}
}
{context.isPending ? : context.isError ? error(context.error.message, () => { void context.refetch(); })
- : !context.data.configured ? 连接 SkillHub
{entries.configuration ? '在 DSH 的插件页面打开 SkillHub 配置,填写来源地址并保存。' : '尚未配置技能来源,且当前配置入口不可用。请先恢复 DSH 的插件配置功能,再连接 SkillHub。'}
- : selected ? <>
+ : selected && context.data.configured ? <>
{sourceMatches ? detail.data?.title || selected.displayName || selected.slug : selected.displayName || selected.slug}
@@ -181,7 +191,7 @@ function Catalog({ remote, navigation, onReturn, installation, installed, availa
: detail.isPending ?
: detail.isError ? error(detail.error.message, () => { void detail.refetch(); })
: <>
- {installation && installed &&
item.namespace === selected.namespace && item.slug === selected.slug) ?? []}
+ {installation && installed && sameRegistry(item.registry, context.data.registryUrl) && item.namespace === selected.namespace && item.slug === selected.slug) ?? []}
sourceRevision={revision!} services={installed} verified={!!inventory.data && !inventory.isError && inventory.data.complete && !inventory.data.unavailable.length}
onInstall={() => setInstallCandidate({ item: selected, revision: revision! })} />}
@@ -190,8 +200,8 @@ function Catalog({ remote, navigation, onReturn, installation, installed, availa
onContextStale={() => { void detail.refetch(); }}
onSelectVersion={version => { const next = { ...selected, latestVersion: version }; navigation.selected = next; setSelected(next); }} />
>}
> : <>
@@ -201,6 +211,7 @@ function Catalog({ remote, navigation, onReturn, installation, installed, availa
: installed ? setInstallCandidate({ item: { namespace: item.namespace, slug: item.slug, latestVersion: item.version, summary: '' }, revision: revision! })} onRemove={item => manageSkill(item, 'remove')} onUpgrade={item => manageSkill(item, 'upgrade')} navigation={installedNavigation!} source={context.data.registryUrl} sourceRevision={revision!} remote={remote} modes={context.data.detailTabs} scrollContainer={scroller} />
: 请先打开一个 DSH 会话,再查看该用户和项目的安装记录。
}
+ {!context.data.configured ? 连接 SkillHub
{entries.configuration ? '在 DSH 的插件页面配置来源,或从顶部选择已保存的来源。' : '来源配置暂不可用,已安装技能仍可管理。'}
: <>