From 23d3807ae113232d765fd4c8ab09b3d09be657fc Mon Sep 17 00:00:00 2001 From: turtle1999 Date: Mon, 28 Sep 2026 19:12:16 +0800 Subject: [PATCH 01/10] docs: add upgrade guide structure, skill, and gate Introduce docs/upgrade-guide/v//guide.md for externally perceptible breaking changes. The dsh-create-upgrade-guide skill owns scope, placement, maintenance, and format; verify-upgrade-guides enforces layout, frontmatter, sections, and the 500-word ceiling. Guides are English-only and registered as the upgrade-guide document kind. --- .../skills/dsh-create-upgrade-guide/SKILL.md | 53 +++++++++ .agents/skills/dsh-doc/SKILL.md | 3 +- .../dsh-doc/references/metadata-links-i18n.md | 3 +- .../skills/dsh-doc/templates/upgrade-guide.md | 27 +++++ AGENTS.md | 2 + docs/AGENTS.md | 2 +- docs/i18n/README.i18n.yaml | 4 +- docs/i18n/README.md | 1 + docs/i18n/README.zh.md | 1 + package.json | 1 + scripts/doc-budgets.manifest.json | 2 +- scripts/doc-standard.spec.ts | 1 + scripts/run-gates.ts | 1 + scripts/translation-pairing.manifest.json | 3 +- scripts/verify-upgrade-guides.spec.ts | 112 ++++++++++++++++++ scripts/verify-upgrade-guides.ts | 78 ++++++++++++ 16 files changed, 287 insertions(+), 7 deletions(-) create mode 100644 .agents/skills/dsh-create-upgrade-guide/SKILL.md create mode 100644 .agents/skills/dsh-doc/templates/upgrade-guide.md create mode 100644 scripts/verify-upgrade-guides.spec.ts create mode 100644 scripts/verify-upgrade-guides.ts diff --git a/.agents/skills/dsh-create-upgrade-guide/SKILL.md b/.agents/skills/dsh-create-upgrade-guide/SKILL.md new file mode 100644 index 0000000000..cf3b46bfdd --- /dev/null +++ b/.agents/skills/dsh-create-upgrade-guide/SKILL.md @@ -0,0 +1,53 @@ +--- +name: dsh-create-upgrade-guide +description: Use when a deepseek-harness change breaks an externally perceptible surface (CLI, profiles, cordis.yml or settings keys, persisted user data, SDK or wire APIs, published package entry points), or when changing a surface that an unreleased upgrade guide already covers, to write or update docs/upgrade-guide/v//guide.md. +--- + +# DSH Create Upgrade Guide + +An upgrade guide tells a reader who runs release vT what to change to run the next release. Write it in the change that introduces the break, as soon as the break is recognized; do not defer it to release preparation. + +## Scope + +Write a guide only for breaks that someone outside the repository can observe after upgrading: + +- `dsh` commands, flags, and profile names; +- `cordis.yml`, patch, overlay, and settings keys or their accepted values; +- persisted user data: Session logs, SQLite stores, and user data directories; +- TypeScript and Python SDK exports, JSON-RPC, HTTP, and ACP messages; +- published package names and entry points. + +Pre-stable internal APIs whose consumers are all updated in the same change need no guide. A Session-format change still follows [type acknowledgements](../../../docs/cookbook/reviewing-persistence-type-changes.md); its guide states only the reader's actions and links that record. + +## Location + +`docs/upgrade-guide/v//guide.md` + +- `` is the `version` field of the root `package.json` when you write the guide, for example `v0.1.7-rc.2`. The guide describes the upgrade from that release to the next one. +- `` is a kebab-case name of the changed surface, for example `profile-flag-rename`. +- No other files belong in the tree: no index, no Chinese counterpart, no attachments. + +## Maintenance + +While the root version is unchanged, the current version directory describes the delta from vT to current `master`. When a later change touches a surface a guide there covers, update that guide in the same change; delete the guide when the break is reverted. After the version bump, guides in older version directories are frozen; a new break goes into the new version directory. Never write a guide spanning more than one release. + +## Format + +Start from the [upgrade-guide template](../dsh-doc/templates/upgrade-guide.md). `verify-upgrade-guides` enforces: + +- frontmatter with exactly `kind: upgrade-guide` and a one-sentence `description` naming what breaks; add a key only together with its gate check, then backfill every guide; +- one `#` title, then exactly the `## Change` and `## Migration` sections, in that order; +- at most 500 words, frontmatter included. + +`Change` states the old and new behavior of the surface and who is affected. `Migration` lists ordered steps that name exact files, keys, commands, or symbols, and states how to confirm the migration worked. + +When a guide needs more than 500 words, the extra text belongs elsewhere: link source files or symbols instead of restating code, move rationale into an [Agent Note](../../notes/README.md), and turn repeated mechanical steps into a script or `dsh` command that the guide invokes. + +Write English only, following [dsh-prose-standard](../dsh-prose-standard/SKILL.md). + +## Verify + +```sh +pnpm run verify-upgrade-guides +pnpm run verify-md-links +``` diff --git a/.agents/skills/dsh-doc/SKILL.md b/.agents/skills/dsh-doc/SKILL.md index 83eee5d526..0c97714672 100644 --- a/.agents/skills/dsh-doc/SKILL.md +++ b/.agents/skills/dsh-doc/SKILL.md @@ -57,6 +57,7 @@ The `kind` frontmatter field selects exactly one README template. Every kind in - `persistence-change` → [templates/persistence-change.md](templates/persistence-change.md): a dated record in `docs/persistence-changes/` — acknowledge a detected type transition with per-root predecessors and generated after schemas. - `persistence-release` → [templates/persistence-release.md](templates/persistence-release.md): a pinned tag in `docs/persistence-changes/releases/` — compare reconstructed historical types without claiming compatibility acknowledgement. - `persistence-format` → [templates/persistence-format.md](templates/persistence-format.md): a historical Session format checkpoint in `docs/persistence-changes/historical-formats/`, with complete schemas; the current writer uses the existing catalog. +- `upgrade-guide` → [templates/upgrade-guide.md](templates/upgrade-guide.md): an English-only `docs/upgrade-guide/v//guide.md` — migration steps for one externally perceptible break, owned by [dsh-create-upgrade-guide](../dsh-create-upgrade-guide/SKILL.md). Open the template before writing and follow its skeleton and rules; it states what the kind is, how the page is structured, and the fact checks each section owes. Add a new kind only together with a distinct template file, a documented repository position or declared owner, and a focused check that maps documents to it. @@ -67,7 +68,7 @@ These rules decide what a section may say. They apply to every authored human-fa - **Summary says what the subject does.** The opening `Summary` and the user-facing sections describe what a user or agent can DO with the subject — outcomes, benefits, when to choose it, main cost — never its role, type, or internal identity. In a package Summary, “what it is” means only its reader-visible capability, not its Cordis role, registrations, or internal components. Omit source identifiers unless the reader directly uses them in configuration, a command, or a public API. "The seam registers `ctx.x` and appends `x/event` records" is identity narration; "you can save a note per message and it survives restarts" is what it does. - **Developer sections explain, never enumerate.** Folded implementation content covers the overall design concept, architecture, and hand-waving dataflow — enough to understand how the package works — and links code for exact detail. No full API catalogs, exhaustive column lists, event-payload enumerations, or JSDoc restatement inside the folds. - **Dev Note is the only slop zone.** Partial ideas, scratches, undecided directions, measured artifacts, and working hypotheses live only in the final Dev Note, marked explicitly non-authoritative. Every other section is polished, current-state prose. -- **Current state only.** Ordinary documentation describes the current codebase. Dedicated `persistence-change`, `persistence-release`, and `persistence-format` records retain historical type evidence; release comparisons and format checkpoints describe observed history without asserting compatibility. Agent Notes and postmortems retain their own historical scope. +- **Current state only.** Ordinary documentation describes the current codebase. Dedicated `persistence-change`, `persistence-release`, and `persistence-format` records retain historical type evidence; release comparisons and format checkpoints describe observed history without asserting compatibility. An `upgrade-guide` describes the change from its version directory's release to the next one. Agent Notes and postmortems retain their own historical scope. - **Use controlled technical English.** Give each sentence an explicit actor and one main action when ambiguity can change behavior. Reuse one term per concept, prefer direct verbs, split stacked instructions and conditions, and preserve modality and exceptions. Apply the non-certified, ASD-STE100-inspired discipline in [the page-style reference](references/style.md#controlled-technical-english). Do not force a shorter sentence when precision would fall. ## Quality criteria diff --git a/.agents/skills/dsh-doc/references/metadata-links-i18n.md b/.agents/skills/dsh-doc/references/metadata-links-i18n.md index 19ee5dde40..34e4cc55bb 100644 --- a/.agents/skills/dsh-doc/references/metadata-links-i18n.md +++ b/.agents/skills/dsh-doc/references/metadata-links-i18n.md @@ -29,7 +29,7 @@ kind: "package-reference" ## The kind system -`kind` selects the document template directly; every kind maps to exactly one template that exists in this skill, and no template exists without a kind. Files named `docs/persistence-changes/YYYY-MM-DD-slug.md` use `persistence-change`; files named `docs/persistence-changes/releases/dsh-v*.md` use `persistence-release`. Files named `docs/persistence-changes/historical-formats/vN.md` use `persistence-format`. Their Chinese siblings use the same kind. Folder READMEs are indexes, not records. Derive package README kinds mechanically, in this order: +`kind` selects the document template directly; every kind maps to exactly one template that exists in this skill, and no template exists without a kind. Files named `docs/persistence-changes/YYYY-MM-DD-slug.md` use `persistence-change`; files named `docs/persistence-changes/releases/dsh-v*.md` use `persistence-release`. Files named `docs/persistence-changes/historical-formats/vN.md` use `persistence-format`. Their Chinese siblings use the same kind. Files named `docs/upgrade-guide/v//guide.md` use `upgrade-guide` and have no Chinese sibling. Folder READMEs are indexes, not records. Derive package README kinds mechanically, in this order: 1. The README is `packages/README.md` or `packages//README.md` → `package-group`. 2. The package manifest declares `dsh.bundle.patch` → `package-bundle`. @@ -45,6 +45,7 @@ kind: "package-reference" | `persistence-change` | `docs/persistence-changes/YYYY-MM-DD-slug.md` and its Chinese sibling | [persistence-change.md](../templates/persistence-change.md) | Historical acknowledgement: mechanically detected type changes, per-root predecessor references, compatibility decision, and generated after schemas. | | `persistence-release` | `docs/persistence-changes/releases/dsh-v*.md` and its Chinese sibling | [persistence-release.md](../templates/persistence-release.md) | Retrospective observation: pinned tag, adjacent before/after digests, actual version constants, and changed after schemas; no compatibility acknowledgement. | | `persistence-format` | `docs/persistence-changes/historical-formats/vN.md` and its Chinese sibling | [persistence-format.md](../templates/persistence-format.md) | Selected historical format: source evidence, complete root schemas and reachable types; the current writer uses the generated catalog. | +| `upgrade-guide` | `docs/upgrade-guide/v//guide.md`, English only | [upgrade-guide.md](../templates/upgrade-guide.md) | Upgrade path: one externally perceptible break from the directory's release to the next, with `Change` and `Migration` sections in at most 500 words. | Before assigning `package-library` or `package-bundle`, inspect the facts: read `package.json` for `dsh.bundle.patch` and `src/index.ts` for the entry shape (`apply` export or a default service export is a plugin; a plain module API is a library). `dsh plugin --profile add ` installs any npm dependency, but the profile reconcile activates a layer only for a package that declares `dsh.bundle`; never present that command as an install path for a library or a plain plugin. The documentation check derives the expected kind from these same facts, rejects another value, and rejects `name`, `audience`, `tags`, and README-local `i18n` metadata. Add a new kind only with a distinct template, an unambiguous repository position or declared owner, and a focused check that maps documents to it. diff --git a/.agents/skills/dsh-doc/templates/upgrade-guide.md b/.agents/skills/dsh-doc/templates/upgrade-guide.md new file mode 100644 index 0000000000..c5308115d3 --- /dev/null +++ b/.agents/skills/dsh-doc/templates/upgrade-guide.md @@ -0,0 +1,27 @@ +# Template: upgrade-guide + +Use this kind for `docs/upgrade-guide/v//guide.md`. The guides are English-only and have no folder index. [dsh-create-upgrade-guide](../../dsh-create-upgrade-guide/SKILL.md) owns scope, placement, maintenance, and length rules; `pnpm run verify-upgrade-guides` enforces them. + +## Frontmatter + +```yaml +--- +kind: upgrade-guide +description: "The externally perceptible surface that breaks and what replaces it." +--- +``` + +## Skeleton + +````markdown +# + +## Change + +State the old and new behavior of the surface, who observes it, and the pull request that introduced it. + +## Migration + +1. Name the exact file, key, command, or symbol to change, with a before/after snippet when it is shorter than prose. +2. State how to confirm the migration worked. +```` diff --git a/AGENTS.md b/AGENTS.md index 37cfd8d280..fc94452a0f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,6 +8,8 @@ Public APIs are pre-stable; update every consumer. Follow [version/status](docs/ Acknowledge [declared persistence-type changes](docs/cookbook/reviewing-persistence-type-changes.md). +Record each externally perceptible breaking change immediately in an [upgrade guide](.agents/skills/dsh-create-upgrade-guide/SKILL.md). + **Application launch.** Only `dsh` profiles launch supported Node apps; package bins, demos, and public SDK argv escapes are forbidden ([rule](docs/architecture.md#application-launch)). ## Repository layout diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 56485c7234..76de4d3632 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -55,7 +55,7 @@ When the gate goes red: 2. **Condense** content that belongs here but can be shorter. 3. **Raise** the ceiling only when the words need the space; justify the manifest diff in the PR. A too-low ceiling is a budget bug. -Ceilings are guardrails, not reduction targets. At or below target, retain at least 5% headroom; above target, freeze the ceiling until relocation or condensation brings the document under target. Lower a ceiling only when the document still has room. Targets: root `AGENTS.md` ≤ 1,950; `architecture.md` ≤ 2,400; subtree `AGENTS.md` ≤ 600, except `packages/AGENTS.md` ≤ 750 and this file ≤ 1,320; `packages/README.md` ≤ 994; plus `cordis-primer.md` 600, `defensive-patterns.md` 550, `testing.md` 1,300, `examples/AGENTS.md` 310. Review governs unbudgeted tiers. +Ceilings are guardrails, not reduction targets. At or below target, retain at least 5% headroom; above target, freeze the ceiling until relocation or condensation brings the document under target. Lower a ceiling only when the document still has room. Targets: root `AGENTS.md` ≤ 1,960; `architecture.md` ≤ 2,400; subtree `AGENTS.md` ≤ 600, except `packages/AGENTS.md` ≤ 750 and this file ≤ 1,320; `packages/README.md` ≤ 994; plus `cordis-primer.md` 600, `defensive-patterns.md` 550, `testing.md` 1,300, `examples/AGENTS.md` 310. Review governs unbudgeted tiers. ## The slop checklist diff --git a/docs/i18n/README.i18n.yaml b/docs/i18n/README.i18n.yaml index 4c794f49bd..ddb3521a91 100644 --- a/docs/i18n/README.i18n.yaml +++ b/docs/i18n/README.i18n.yaml @@ -12,8 +12,8 @@ en: 08386e96eebf06d9 zh: c6d2b41199b0c466 /bilingual-documentation/scope-and-exclusions: - en: db70e758813d339b - zh: 3dcea283a08c47cb + en: 093bd091c9d46eda + zh: ab6cd07f94c740e4 /bilingual-documentation/division-of-labor: en: 88097c97cb72f3ba zh: 78a18000626cc818 diff --git a/docs/i18n/README.md b/docs/i18n/README.md index 4c23fac67e..ca970d6b2d 100644 --- a/docs/i18n/README.md +++ b/docs/i18n/README.md @@ -60,6 +60,7 @@ Generated English sources omit the language switcher that ordinary authored sour - `docs/i18n/terminology.md` and [style-samples.md](style-samples.md) — both are bilingual by construction. - [translation-prompt.md](translation-prompt.md) — the automated pipeline's prompt template; its body is machine-consumed verbatim, so a paired translation would change pipeline behavior. - [review-ownership/README.md](../../.github/review-ownership/README.md) — repository-internal approval policy maintained in English only. +- `docs/upgrade-guide/` — agent-facing upgrade guides maintained in English only. - `.agents/notes/archived/` — frozen historical triplets. [`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) validates their completeness and content seals; translation maintenance must never rewrite them. **Universal requirement**: every current or future document in scope must merge as a complete bilingual pair. [scripts/translation-pairing.manifest.json](../../scripts/translation-pairing.manifest.json) contains only explicit exclusions; there is no per-file rollout list, date cutoff, or README-specific policy class. diff --git a/docs/i18n/README.zh.md b/docs/i18n/README.zh.md index a905a0bdae..edc6f972fc 100644 --- a/docs/i18n/README.zh.md +++ b/docs/i18n/README.zh.md @@ -62,6 +62,7 @@ - `docs/i18n/terminology.md` 与 [style-samples.md](style-samples.md):二者本身即为中英对照文档。 - [translation-prompt.md](translation-prompt.md):自动翻译流水线的提示词模板;正文逐字进入模型请求,配对翻译会改变流水线行为。 - [review-ownership/README.md](../../.github/review-ownership/README.md):仓库内部审批策略,只以英文维护。 +- `docs/upgrade-guide/`:面向 agent 的升级指南,只以英文维护。 - `.agents/notes/archived/`:冻结的历史三文件配对。[`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) 校验其完整性和内容封存记录;翻译维护绝不能重写这些文件。 **统一要求**:当前及今后纳入范围的每篇文档,合并时都必须构成完整的双语配对。[scripts/translation-pairing.manifest.json](../../scripts/translation-pairing.manifest.json) 只包含显式排除项;不存在逐文件推进清单、日期分界或 README 专用政策类别。 diff --git a/package.json b/package.json index 4317f7dc94..e254b86997 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ "verify-archived-agent-notes": "tsx scripts/verify-archived-agent-notes.ts", "verify-type-equiv": "tsx scripts/verify-type-equiv.ts", "verify-skill-invocation-metadata": "tsx scripts/verify-skill-invocation-metadata.ts", + "verify-upgrade-guides": "tsx scripts/verify-upgrade-guides.ts", "verify-translation-prompt": "tsx scripts/verify-translation-prompt.ts", "verify-translation-pairing": "tsx scripts/verify-translation-pairing.ts", "test:docs": "tsx scripts/run-gates.ts doc-quick", diff --git a/scripts/doc-budgets.manifest.json b/scripts/doc-budgets.manifest.json index d06682d7fd..2eba3f4c17 100644 --- a/scripts/doc-budgets.manifest.json +++ b/scripts/doc-budgets.manifest.json @@ -1,5 +1,5 @@ { - "AGENTS.md": 1950, + "AGENTS.md": 1960, "docs/AGENTS.md": 1320, "docs/architecture.md": 2410, "docs/cordis-primer.md": 600, diff --git a/scripts/doc-standard.spec.ts b/scripts/doc-standard.spec.ts index e8ed611e60..84dd6e1515 100644 --- a/scripts/doc-standard.spec.ts +++ b/scripts/doc-standard.spec.ts @@ -45,6 +45,7 @@ const KIND_TEMPLATES: Readonly> = { 'persistence-change': '.agents/skills/dsh-doc/templates/persistence-change.md', 'persistence-release': '.agents/skills/dsh-doc/templates/persistence-release.md', 'persistence-format': '.agents/skills/dsh-doc/templates/persistence-format.md', + 'upgrade-guide': '.agents/skills/dsh-doc/templates/upgrade-guide.md', } /** diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index e1e2de9c21..ade1057e24 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -820,6 +820,7 @@ function docSyncLeafGates(options: { pnpmScript('skill-invocation-metadata', 'verify-skill-invocation-metadata', { label: 'skill invocation metadata', quick: true }), pnpmScript('translation-prompt', 'verify-translation-prompt', { label: 'translation prompt', quick: true }), pnpmScript('doc-budgets', 'verify-doc-budgets', { label: 'doc budgets', quick: true }), + pnpmScript('upgrade-guides', 'verify-upgrade-guides', { label: 'upgrade guides', quick: true }), pnpmExec('doc-standard-tests', ['vitest', 'run', 'scripts/doc-standard.spec.ts'], { label: 'documentation standard tests', quick: true, diff --git a/scripts/translation-pairing.manifest.json b/scripts/translation-pairing.manifest.json index f9bb1549a2..fe517aa995 100644 --- a/scripts/translation-pairing.manifest.json +++ b/scripts/translation-pairing.manifest.json @@ -8,6 +8,7 @@ "docs/cordis-api/inherited.md", "docs/i18n/style-samples.md", "docs/i18n/terminology.md", - "docs/i18n/translation-prompt.md" + "docs/i18n/translation-prompt.md", + "docs/upgrade-guide/" ] } diff --git a/scripts/verify-upgrade-guides.spec.ts b/scripts/verify-upgrade-guides.spec.ts new file mode 100644 index 0000000000..07b1e516aa --- /dev/null +++ b/scripts/verify-upgrade-guides.spec.ts @@ -0,0 +1,112 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { collectUpgradeGuideViolations } from './verify-upgrade-guides.ts' + +const roots: string[] = [] + +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) +}) + +const VALID = [ + '---', + 'kind: upgrade-guide', + 'description: "The `--preset` flag is removed."', + '---', + '', + '# `--profile` replaces `--preset`', + '', + '## Change', + '', + 'Old and new behavior.', + '', + '### Detail', + '', + '```md', + '# not a heading', + '```', + '', + '## Migration', + '', + '1. Rename the flag.', + '', +].join('\n') + +function fixtureRoot(files: Record): string { + const root = mkdtempSync(join(tmpdir(), 'dsh-upgrade-guides-')) + roots.push(root) + for (const [file, source] of Object.entries(files)) { + const path = join(root, 'docs/upgrade-guide', file) + mkdirSync(dirname(path), { recursive: true }) + writeFileSync(path, source) + } + return root +} + +describe('upgrade guide gate', () => { + it('accepts an absent tree and conforming guides, including prerelease versions and CRLF files', () => { + expect(collectUpgradeGuideViolations(fixtureRoot({}))).toEqual([]) + expect(collectUpgradeGuideViolations(fixtureRoot({ + 'v0.1.7-rc.2/profile-flag/guide.md': VALID, + 'v1.0.0/crlf-item/guide.md': VALID.replaceAll('\n', '\r\n'), + }))).toEqual([]) + }) + + it('rejects files outside the version/item/guide.md layout', () => { + expect(collectUpgradeGuideViolations(fixtureRoot({ + 'README.md': VALID, + '0.1.7/item/guide.md': VALID, + 'v0.1/item/guide.md': VALID, + 'v0.1.7/Item_Name/guide.md': VALID, + 'v0.1.7/item/guide.zh.md': VALID, + 'v0.1.7/item/extra/guide.md': VALID, + }))).toEqual([ + 'docs/upgrade-guide/0.1.7/item/guide.md', + 'docs/upgrade-guide/README.md', + 'docs/upgrade-guide/v0.1.7/Item_Name/guide.md', + 'docs/upgrade-guide/v0.1.7/item/extra/guide.md', + 'docs/upgrade-guide/v0.1.7/item/guide.zh.md', + 'docs/upgrade-guide/v0.1/item/guide.md', + ].map(path => `${path}: only v//guide.md files belong in docs/upgrade-guide`)) + }) + + it('rejects missing, extra, or invalid frontmatter fields', () => { + const body = VALID.slice(VALID.indexOf('# ')) + expect(collectUpgradeGuideViolations(fixtureRoot({ + 'v0.1.7/none/guide.md': body, + 'v0.1.7/list/guide.md': `---\n- a\n---\n${body}`, + 'v0.1.7/fields/guide.md': `---\nkind: persistence-change\ndescription: ""\nversion: v0.1.7\n---\n${body}`, + }))).toEqual([ + 'docs/upgrade-guide/v0.1.7/fields/guide.md: frontmatter keys must be exactly description, kind; got description, kind, version', + 'docs/upgrade-guide/v0.1.7/fields/guide.md: kind must be upgrade-guide', + 'docs/upgrade-guide/v0.1.7/fields/guide.md: description must be a non-empty string', + 'docs/upgrade-guide/v0.1.7/list/guide.md: frontmatter must be a YAML object', + 'docs/upgrade-guide/v0.1.7/none/guide.md: must start with YAML frontmatter', + ]) + }) + + it('rejects a missing title and missing, renamed, extra, or reordered sections', () => { + const cases = { + 'no-title': VALID.replace('# `--profile` replaces `--preset`\n', ''), + 'two-titles': VALID.replace('## Migration', '# Migration'), + 'renamed': VALID.replace('## Change', '## Changes'), + 'extra': `${VALID}\n## References\n`, + 'reordered': VALID.replace('## Change', '## Placeholder').replace('## Migration', '## Change').replace('## Placeholder', '## Migration'), + } + const violations = collectUpgradeGuideViolations(fixtureRoot(Object.fromEntries( + Object.entries(cases).map(([item, source]) => [`v0.1.7/${item}/guide.md`, source]), + ))) + expect(violations.map(violation => violation.split('/')[3])).toEqual(['extra', 'no-title', 'renamed', 'reordered', 'two-titles']) + expect(violations[0]).toBe('docs/upgrade-guide/v0.1.7/extra/guide.md: headings must be one "#" title followed by "## Change", "## Migration"; got "# `--profile` replaces `--preset`", "## Change", "## Migration", "## References"') + }) + + it('rejects a guide over 500 words, counting frontmatter', () => { + const words = VALID.split(/\s+/u).filter(Boolean).length + expect(collectUpgradeGuideViolations(fixtureRoot({ + 'v0.1.7/at-limit/guide.md': `${VALID}${'word '.repeat(500 - words)}\n`, + 'v0.1.7/over-limit/guide.md': `${VALID}${'word '.repeat(501 - words)}\n`, + }))).toEqual(['docs/upgrade-guide/v0.1.7/over-limit/guide.md: 501 words exceeds the 500-word ceiling']) + }) +}) diff --git a/scripts/verify-upgrade-guides.ts b/scripts/verify-upgrade-guides.ts new file mode 100644 index 0000000000..9a776013af --- /dev/null +++ b/scripts/verify-upgrade-guides.ts @@ -0,0 +1,78 @@ +/** + * Enforce the `docs/upgrade-guide/v//guide.md` layout, metadata, + * sections, and word ceiling defined by the `dsh-create-upgrade-guide` skill. + * @module scripts/verify-upgrade-guides + */ + +import { existsSync, readFileSync, readdirSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { JSON_SCHEMA, load } from 'js-yaml' +import { fromMarkdown } from 'mdast-util-from-markdown' + +const ROOT = resolve(import.meta.dirname, '..') +const GUIDE_ROOT = 'docs/upgrade-guide' +const MAX_WORDS = 500 +const FRONTMATTER_KEYS = ['description', 'kind'] +const SECTIONS = ['Change', 'Migration'] +const SEMVER = String.raw`(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?` +const GUIDE_PATH = new RegExp(`^v${SEMVER}/[a-z0-9]+(?:-[a-z0-9]+)*/guide\\.md$`, 'u') + +/** Return one guide's frontmatter and structure violations. */ +function guideViolations(source: string): string[] { + const violations: string[] = [] + const words = source.split(/\s+/u).filter(Boolean).length + if (words > MAX_WORDS) violations.push(`${String(words)} words exceeds the ${String(MAX_WORDS)}-word ceiling`) + + const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---\r?\n/u.exec(source) + if (frontmatter === null) return [...violations, 'must start with YAML frontmatter'] + const metadata = load(frontmatter[1] as string, { schema: JSON_SCHEMA }) + if (typeof metadata !== 'object' || metadata === null || Array.isArray(metadata)) return [...violations, 'frontmatter must be a YAML object'] + const fields = metadata as Record + const keys = Object.keys(fields).sort() + if (keys.join() !== FRONTMATTER_KEYS.join()) violations.push(`frontmatter keys must be exactly ${FRONTMATTER_KEYS.join(', ')}; got ${keys.join(', ') || 'none'}`) + if (fields.kind !== 'upgrade-guide') violations.push('kind must be upgrade-guide') + if (typeof fields.description !== 'string' || fields.description.trim() === '') violations.push('description must be a non-empty string') + + const body = source.slice(frontmatter[0].length) + const outline = fromMarkdown(body).children + .flatMap(node => node.type === 'heading' && node.depth <= 2 ? [body.slice(node.position?.start.offset, node.position?.end.offset)] : []) + const expected = SECTIONS.map(section => `## ${section}`) + if (outline.length !== expected.length + 1 || !outline[0]?.startsWith('# ') || outline.slice(1).join('\n') !== expected.join('\n')) { + violations.push(`headings must be one "#" title followed by ${expected.map(heading => `"${heading}"`).join(', ')}; got ${outline.map(heading => `"${heading}"`).join(', ') || 'none'}`) + } + return violations +} + +/** + * Report upgrade-guide tree violations. + * @param root - Repository root containing `docs/upgrade-guide`. + * @returns diagnostics prefixed by the repository-relative file path; empty when the tree is absent. + */ +export function collectUpgradeGuideViolations(root: string): string[] { + const directory = join(root, GUIDE_ROOT) + if (!existsSync(directory)) return [] + const violations: string[] = [] + const files = readdirSync(directory, { recursive: true, withFileTypes: true }) + .filter(entry => !entry.isDirectory()) + .map(entry => join(entry.parentPath, entry.name).slice(directory.length + 1).replaceAll('\\', '/')) + .sort() + for (const file of files) { + const path = `${GUIDE_ROOT}/${file}` + if (!GUIDE_PATH.test(file)) { + violations.push(`${path}: only v//guide.md files belong in ${GUIDE_ROOT}`) + continue + } + for (const violation of guideViolations(readFileSync(join(directory, file), 'utf8'))) violations.push(`${path}: ${violation}`) + } + return violations +} + +if (process.argv[1] && import.meta.filename === resolve(process.argv[1])) { + const violations = collectUpgradeGuideViolations(ROOT) + if (violations.length > 0) { + process.stderr.write('verify-upgrade-guides: violations found (see .agents/skills/dsh-create-upgrade-guide/SKILL.md):\n') + for (const violation of violations) process.stderr.write(` ${violation}\n`) + process.exit(1) + } + process.stdout.write('verify-upgrade-guides: upgrade guides conform.\n') +} From 9126818b7fd09c18de6f97df1e5cdf518ead8406 Mon Sep 17 00:00:00 2001 From: turtle1999 Date: Tue, 29 Sep 2026 10:25:27 +0800 Subject: [PATCH 02/10] docs: backfill v0.1.7-rc.2 upgrade guides Record the three externally perceptible breaks between dsh-v0.1.7-rc.2 and dsh-v0.2.0-rc.1: Schedule rows moving into the Automation tasks optional bundle, the legacy work-details value reading as Detailed, and Session-log uploads requiring dsh-otel with a new collector host. The template no longer asks for pull request links, which verify-repository-references rejects. --- .../skills/dsh-doc/templates/upgrade-guide.md | 2 +- .../schedule-optional-bundle/guide.md | 20 +++++++++++++ .../session-log-otel-collector/guide.md | 30 +++++++++++++++++++ .../transcript-view-legacy-normal/guide.md | 23 ++++++++++++++ 4 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.md create mode 100644 docs/upgrade-guide/v0.1.7-rc.2/session-log-otel-collector/guide.md create mode 100644 docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.md diff --git a/.agents/skills/dsh-doc/templates/upgrade-guide.md b/.agents/skills/dsh-doc/templates/upgrade-guide.md index c5308115d3..091495b81d 100644 --- a/.agents/skills/dsh-doc/templates/upgrade-guide.md +++ b/.agents/skills/dsh-doc/templates/upgrade-guide.md @@ -18,7 +18,7 @@ description: "The externally perceptible surface that breaks and what replaces i ## Change -State the old and new behavior of the surface, who observes it, and the pull request that introduced it. +State the old and new behavior of the surface and who observes it. ## Migration diff --git a/docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.md b/docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.md new file mode 100644 index 0000000000..ccd08f683d --- /dev/null +++ b/docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.md @@ -0,0 +1,20 @@ +--- +kind: upgrade-guide +description: "The Web composition no longer carries the Schedule rows; the Automation tasks optional bundle inserts them." +--- + +# Schedule moves into the Automation tasks optional bundle + +## Change + +In v0.1.7-rc.2, `@deepseek-ai/dsh-web-app` carried the `time-context`, `schedule`, and `ui-schedule` rows with `disabled: true`. Turning Schedule on in the Plugins page, or by hand, wrote id-targeted overrides such as `- id: schedule` with `disabled: false` into `$DSH_HOME/profiles//cordis.patch.yml` or a `--patch` overlay. + +The next release removes the three rows from the Web composition. `@deepseek-ai/dsh-experimental-schedule-bundle`, shown as Automation tasks in the Plugins page's Official group, inserts them. Every installation ships the bundle switched off. + +Profiles that enabled Schedule by id lose it after upgrading: the loader warns `patch: entry schedule not found` (and the same for `time-context` and `ui-schedule`), the `schedule_*` tools and the Automation tasks page disappear, and stored reminders stop being delivered. Stored tasks and delivery records remain on disk. + +## Migration + +1. Open Plugins, find Automation tasks in the Official group, and switch it on. The switch appends `@deepseek-ai/dsh-experimental-schedule-bundle` to `dsh.profile.bundles` in `$DSH_HOME/profiles//package.json`; for a profile edited by hand, add that entry yourself. +2. Keep existing overrides that set other fields on `schedule`, `time-context`, or `ui-schedule`, such as `deliveryHistoryDays`; they apply again once the bundle inserts the rows. Overrides that only set `disabled: false` are redundant and can be deleted. +3. Confirm: restart `dsh web`, check that startup logs no `patch: entry ... not found` warning for the three ids, and that the sidebar shows Automation tasks with the previously stored reminders. diff --git a/docs/upgrade-guide/v0.1.7-rc.2/session-log-otel-collector/guide.md b/docs/upgrade-guide/v0.1.7-rc.2/session-log-otel-collector/guide.md new file mode 100644 index 0000000000..e96eaf087f --- /dev/null +++ b/docs/upgrade-guide/v0.1.7-rc.2/session-log-otel-collector/guide.md @@ -0,0 +1,30 @@ +--- +kind: upgrade-guide +description: "Feedback Session-log uploads go to a new collector host and require the @deepseek-ai/dsh-otel service." +--- + +# Session-log uploads use the shared OTel service and a new collector + +## Change + +In v0.1.7-rc.2, `@deepseek-ai/dsh-session-telemetry-otel` owned its own OTLP transport and sent feedback-authorized Session logs to `https://harness-telemetry.deepseeksvc.com/v1/logs`. + +From the next release: + +- The plugin injects the `otel` service from the new `@deepseek-ai/dsh-otel` plugin. `@deepseek-ai/dsh-base` mounts it as the `otel` row; a composition that mounts `session-telemetry-otel` without that row leaves the plugin inactive, and feedback uploads stop. +- The default collector is `https://dsh-otel-collector.deepseeksvc.com/v1/logs`. `DSH_TELEMETRY_OTLP_URL` still overrides it. +- Uploads are split into requests of at most 4,000,000 uncompressed bytes (`maxRequestBytes`). + +Profiles that extend `@deepseek-ai/dsh-base` and networks with unrestricted egress need no action. Persisted Session formats are unchanged. + +## Migration + +1. If a custom composition lists `session-telemetry-otel` without `@deepseek-ai/dsh-base`, add the row before it: + + ```yaml + - id: otel + name: '@deepseek-ai/dsh-otel' + ``` + +2. If an egress allowlist or proxy rule names `harness-telemetry.deepseeksvc.com`, add `dsh-otel-collector.deepseeksvc.com`. A deployment that points `DSH_TELEMETRY_OTLP_URL` at its own collector keeps its setting. +3. Confirm: send feedback with Session-log upload enabled and check that the configured collector receives the request; for a self-hosted collector, check its receive log. diff --git a/docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.md b/docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.md new file mode 100644 index 0000000000..b0af748cad --- /dev/null +++ b/docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.md @@ -0,0 +1,23 @@ +--- +kind: upgrade-guide +description: "The legacy ui-chat.transcriptView value normal, and an unset value in non-Desktop Web, now display Detailed instead of Standard." +--- + +# Legacy `normal` work details display as Detailed + +## Change + +The Host setting `ui-chat.transcriptView` selects Settings → General → Work details. In v0.1.7-rc.2 the client displayed the legacy saved value `normal` as `standard`, and every client used `standard` when the setting was missing, `null`, or invalid. + +From the next release: + +- A saved `normal` displays as `detailed` on Desktop and Web. The saved value on disk is not rewritten. +- A missing, `null`, or invalid value displays as `detailed` in non-Desktop Web (the npm `dsh web` install). Desktop still uses `standard`. +- Saved `compact`, `standard`, `detailed`, and `verbose` values are unchanged. + +Users who relied on the old reading see running Turns with process-group bodies expanded instead of collapsed summaries. + +## Migration + +1. To keep the previous presentation, open Settings → General → Work details and choose Standard. The client saves `standard` to `ui-chat.transcriptView`, which every later default change leaves untouched. +2. Confirm: start a Turn that calls tools and check that its process group shows a collapsed summary with live task detail. From 61d4ab0b6ccedd954cc1d00e8643d0f275b24fde Mon Sep 17 00:00:00 2001 From: turtle1999 Date: Tue, 29 Sep 2026 10:34:29 +0800 Subject: [PATCH 03/10] docs: make upgrade guides bilingual and drop the OTel guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upgrade guides now pair guide.md with guide.zh.md and guide.i18n.yaml under the ordinary translation-pairing gate. verify-upgrade-guides checks the Chinese sibling's metadata and its 变更/迁移 sections; the 500-word ceiling applies to the English source. Remove the session-log-otel-collector guide. --- .../skills/dsh-create-upgrade-guide/SKILL.md | 9 +++--- .agents/skills/dsh-doc/SKILL.md | 2 +- .../dsh-doc/references/metadata-links-i18n.md | 4 +-- .../skills/dsh-doc/templates/upgrade-guide.md | 4 ++- docs/i18n/README.i18n.yaml | 4 +-- docs/i18n/README.md | 1 - docs/i18n/README.zh.md | 1 - .../schedule-optional-bundle/guide.i18n.yaml | 16 ++++++++++ .../schedule-optional-bundle/guide.md | 2 ++ .../schedule-optional-bundle/guide.zh.md | 22 ++++++++++++++ .../session-log-otel-collector/guide.md | 30 ------------------- .../guide.i18n.yaml | 16 ++++++++++ .../transcript-view-legacy-normal/guide.md | 2 ++ .../transcript-view-legacy-normal/guide.zh.md | 25 ++++++++++++++++ scripts/translation-pairing.manifest.json | 3 +- scripts/verify-upgrade-guides.spec.ts | 30 +++++++++++++++---- scripts/verify-upgrade-guides.ts | 25 ++++++++++------ 17 files changed, 137 insertions(+), 59 deletions(-) create mode 100644 docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.i18n.yaml create mode 100644 docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.zh.md delete mode 100644 docs/upgrade-guide/v0.1.7-rc.2/session-log-otel-collector/guide.md create mode 100644 docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.i18n.yaml create mode 100644 docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.zh.md diff --git a/.agents/skills/dsh-create-upgrade-guide/SKILL.md b/.agents/skills/dsh-create-upgrade-guide/SKILL.md index cf3b46bfdd..9723537e22 100644 --- a/.agents/skills/dsh-create-upgrade-guide/SKILL.md +++ b/.agents/skills/dsh-create-upgrade-guide/SKILL.md @@ -25,7 +25,7 @@ Pre-stable internal APIs whose consumers are all updated in the same change need - `` is the `version` field of the root `package.json` when you write the guide, for example `v0.1.7-rc.2`. The guide describes the upgrade from that release to the next one. - `` is a kebab-case name of the changed surface, for example `profile-flag-rename`. -- No other files belong in the tree: no index, no Chinese counterpart, no attachments. +- Each item directory holds exactly `guide.md`, its Chinese counterpart `guide.zh.md`, and the pairing record `guide.i18n.yaml`: no index, no attachments. ## Maintenance @@ -36,18 +36,19 @@ While the root version is unchanged, the current version directory describes the Start from the [upgrade-guide template](../dsh-doc/templates/upgrade-guide.md). `verify-upgrade-guides` enforces: - frontmatter with exactly `kind: upgrade-guide` and a one-sentence `description` naming what breaks; add a key only together with its gate check, then backfill every guide; -- one `#` title, then exactly the `## Change` and `## Migration` sections, in that order; -- at most 500 words, frontmatter included. +- one `#` title, then exactly the `## Change` and `## Migration` sections in `guide.md`, and `## 变更` and `## 迁移` in `guide.zh.md`, in that order; +- at most 500 words in `guide.md`, frontmatter included. `Change` states the old and new behavior of the surface and who is affected. `Migration` lists ordered steps that name exact files, keys, commands, or symbols, and states how to confirm the migration worked. When a guide needs more than 500 words, the extra text belongs elsewhere: link source files or symbols instead of restating code, move rationale into an [Agent Note](../../notes/README.md), and turn repeated mechanical steps into a script or `dsh` command that the guide invokes. -Write English only, following [dsh-prose-standard](../dsh-prose-standard/SKILL.md). +Follow [dsh-prose-standard](../dsh-prose-standard/SKILL.md). Write the Chinese counterpart in the same change under the [bilingual pairing rules](../../../docs/AGENTS.md), then record the pair with `pnpm run verify-translation-pairing --write docs/upgrade-guide/v//guide.md`. ## Verify ```sh pnpm run verify-upgrade-guides +pnpm run verify-translation-pairing docs/upgrade-guide/v//guide.md pnpm run verify-md-links ``` diff --git a/.agents/skills/dsh-doc/SKILL.md b/.agents/skills/dsh-doc/SKILL.md index 0c97714672..5e32d4c07e 100644 --- a/.agents/skills/dsh-doc/SKILL.md +++ b/.agents/skills/dsh-doc/SKILL.md @@ -57,7 +57,7 @@ The `kind` frontmatter field selects exactly one README template. Every kind in - `persistence-change` → [templates/persistence-change.md](templates/persistence-change.md): a dated record in `docs/persistence-changes/` — acknowledge a detected type transition with per-root predecessors and generated after schemas. - `persistence-release` → [templates/persistence-release.md](templates/persistence-release.md): a pinned tag in `docs/persistence-changes/releases/` — compare reconstructed historical types without claiming compatibility acknowledgement. - `persistence-format` → [templates/persistence-format.md](templates/persistence-format.md): a historical Session format checkpoint in `docs/persistence-changes/historical-formats/`, with complete schemas; the current writer uses the existing catalog. -- `upgrade-guide` → [templates/upgrade-guide.md](templates/upgrade-guide.md): an English-only `docs/upgrade-guide/v//guide.md` — migration steps for one externally perceptible break, owned by [dsh-create-upgrade-guide](../dsh-create-upgrade-guide/SKILL.md). +- `upgrade-guide` → [templates/upgrade-guide.md](templates/upgrade-guide.md): `docs/upgrade-guide/v//guide.md` and its Chinese sibling — migration steps for one externally perceptible break, owned by [dsh-create-upgrade-guide](../dsh-create-upgrade-guide/SKILL.md). Open the template before writing and follow its skeleton and rules; it states what the kind is, how the page is structured, and the fact checks each section owes. Add a new kind only together with a distinct template file, a documented repository position or declared owner, and a focused check that maps documents to it. diff --git a/.agents/skills/dsh-doc/references/metadata-links-i18n.md b/.agents/skills/dsh-doc/references/metadata-links-i18n.md index 34e4cc55bb..4ea476085c 100644 --- a/.agents/skills/dsh-doc/references/metadata-links-i18n.md +++ b/.agents/skills/dsh-doc/references/metadata-links-i18n.md @@ -29,7 +29,7 @@ kind: "package-reference" ## The kind system -`kind` selects the document template directly; every kind maps to exactly one template that exists in this skill, and no template exists without a kind. Files named `docs/persistence-changes/YYYY-MM-DD-slug.md` use `persistence-change`; files named `docs/persistence-changes/releases/dsh-v*.md` use `persistence-release`. Files named `docs/persistence-changes/historical-formats/vN.md` use `persistence-format`. Their Chinese siblings use the same kind. Files named `docs/upgrade-guide/v//guide.md` use `upgrade-guide` and have no Chinese sibling. Folder READMEs are indexes, not records. Derive package README kinds mechanically, in this order: +`kind` selects the document template directly; every kind maps to exactly one template that exists in this skill, and no template exists without a kind. Files named `docs/persistence-changes/YYYY-MM-DD-slug.md` use `persistence-change`; files named `docs/persistence-changes/releases/dsh-v*.md` use `persistence-release`. Files named `docs/persistence-changes/historical-formats/vN.md` use `persistence-format`. Their Chinese siblings use the same kind. Files named `docs/upgrade-guide/v//guide.md` and their `guide.zh.md` siblings use `upgrade-guide`. Folder READMEs are indexes, not records. Derive package README kinds mechanically, in this order: 1. The README is `packages/README.md` or `packages//README.md` → `package-group`. 2. The package manifest declares `dsh.bundle.patch` → `package-bundle`. @@ -45,7 +45,7 @@ kind: "package-reference" | `persistence-change` | `docs/persistence-changes/YYYY-MM-DD-slug.md` and its Chinese sibling | [persistence-change.md](../templates/persistence-change.md) | Historical acknowledgement: mechanically detected type changes, per-root predecessor references, compatibility decision, and generated after schemas. | | `persistence-release` | `docs/persistence-changes/releases/dsh-v*.md` and its Chinese sibling | [persistence-release.md](../templates/persistence-release.md) | Retrospective observation: pinned tag, adjacent before/after digests, actual version constants, and changed after schemas; no compatibility acknowledgement. | | `persistence-format` | `docs/persistence-changes/historical-formats/vN.md` and its Chinese sibling | [persistence-format.md](../templates/persistence-format.md) | Selected historical format: source evidence, complete root schemas and reachable types; the current writer uses the generated catalog. | -| `upgrade-guide` | `docs/upgrade-guide/v//guide.md`, English only | [upgrade-guide.md](../templates/upgrade-guide.md) | Upgrade path: one externally perceptible break from the directory's release to the next, with `Change` and `Migration` sections in at most 500 words. | +| `upgrade-guide` | `docs/upgrade-guide/v//guide.md` and its Chinese sibling | [upgrade-guide.md](../templates/upgrade-guide.md) | Upgrade path: one externally perceptible break from the directory's release to the next, with `Change` and `Migration` sections in at most 500 English words. | Before assigning `package-library` or `package-bundle`, inspect the facts: read `package.json` for `dsh.bundle.patch` and `src/index.ts` for the entry shape (`apply` export or a default service export is a plugin; a plain module API is a library). `dsh plugin --profile add ` installs any npm dependency, but the profile reconcile activates a layer only for a package that declares `dsh.bundle`; never present that command as an install path for a library or a plain plugin. The documentation check derives the expected kind from these same facts, rejects another value, and rejects `name`, `audience`, `tags`, and README-local `i18n` metadata. Add a new kind only with a distinct template, an unambiguous repository position or declared owner, and a focused check that maps documents to it. diff --git a/.agents/skills/dsh-doc/templates/upgrade-guide.md b/.agents/skills/dsh-doc/templates/upgrade-guide.md index 091495b81d..d47f7dc4ae 100644 --- a/.agents/skills/dsh-doc/templates/upgrade-guide.md +++ b/.agents/skills/dsh-doc/templates/upgrade-guide.md @@ -1,6 +1,6 @@ # Template: upgrade-guide -Use this kind for `docs/upgrade-guide/v//guide.md`. The guides are English-only and have no folder index. [dsh-create-upgrade-guide](../../dsh-create-upgrade-guide/SKILL.md) owns scope, placement, maintenance, and length rules; `pnpm run verify-upgrade-guides` enforces them. +Use this kind for `docs/upgrade-guide/v//guide.md` and its Chinese sibling `guide.zh.md`, whose sections are `## 变更` and `## 迁移`. The tree has no folder index. [dsh-create-upgrade-guide](../../dsh-create-upgrade-guide/SKILL.md) owns scope, placement, maintenance, and length rules; `pnpm run verify-upgrade-guides` enforces them. ## Frontmatter @@ -16,6 +16,8 @@ description: "The externally perceptible surface that breaks and what replaces i ````markdown # +English | [中文](guide.zh.md) + ## Change State the old and new behavior of the surface and who observes it. diff --git a/docs/i18n/README.i18n.yaml b/docs/i18n/README.i18n.yaml index ddb3521a91..4c794f49bd 100644 --- a/docs/i18n/README.i18n.yaml +++ b/docs/i18n/README.i18n.yaml @@ -12,8 +12,8 @@ en: 08386e96eebf06d9 zh: c6d2b41199b0c466 /bilingual-documentation/scope-and-exclusions: - en: 093bd091c9d46eda - zh: ab6cd07f94c740e4 + en: db70e758813d339b + zh: 3dcea283a08c47cb /bilingual-documentation/division-of-labor: en: 88097c97cb72f3ba zh: 78a18000626cc818 diff --git a/docs/i18n/README.md b/docs/i18n/README.md index ca970d6b2d..4c23fac67e 100644 --- a/docs/i18n/README.md +++ b/docs/i18n/README.md @@ -60,7 +60,6 @@ Generated English sources omit the language switcher that ordinary authored sour - `docs/i18n/terminology.md` and [style-samples.md](style-samples.md) — both are bilingual by construction. - [translation-prompt.md](translation-prompt.md) — the automated pipeline's prompt template; its body is machine-consumed verbatim, so a paired translation would change pipeline behavior. - [review-ownership/README.md](../../.github/review-ownership/README.md) — repository-internal approval policy maintained in English only. -- `docs/upgrade-guide/` — agent-facing upgrade guides maintained in English only. - `.agents/notes/archived/` — frozen historical triplets. [`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) validates their completeness and content seals; translation maintenance must never rewrite them. **Universal requirement**: every current or future document in scope must merge as a complete bilingual pair. [scripts/translation-pairing.manifest.json](../../scripts/translation-pairing.manifest.json) contains only explicit exclusions; there is no per-file rollout list, date cutoff, or README-specific policy class. diff --git a/docs/i18n/README.zh.md b/docs/i18n/README.zh.md index edc6f972fc..a905a0bdae 100644 --- a/docs/i18n/README.zh.md +++ b/docs/i18n/README.zh.md @@ -62,7 +62,6 @@ - `docs/i18n/terminology.md` 与 [style-samples.md](style-samples.md):二者本身即为中英对照文档。 - [translation-prompt.md](translation-prompt.md):自动翻译流水线的提示词模板;正文逐字进入模型请求,配对翻译会改变流水线行为。 - [review-ownership/README.md](../../.github/review-ownership/README.md):仓库内部审批策略,只以英文维护。 -- `docs/upgrade-guide/`:面向 agent 的升级指南,只以英文维护。 - `.agents/notes/archived/`:冻结的历史三文件配对。[`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) 校验其完整性和内容封存记录;翻译维护绝不能重写这些文件。 **统一要求**:当前及今后纳入范围的每篇文档,合并时都必须构成完整的双语配对。[scripts/translation-pairing.manifest.json](../../scripts/translation-pairing.manifest.json) 只包含显式排除项;不存在逐文件推进清单、日期分界或 README 专用政策类别。 diff --git a/docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.i18n.yaml b/docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.i18n.yaml new file mode 100644 index 0000000000..f0046f5fc9 --- /dev/null +++ b/docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.md +/: + en: 224d57a22f38ba26 + zh: 1472851dde2bf9c3 +/schedule-moves-into-the-automation-tasks-optional-bundle: + en: 138f715e3a260eb6 + zh: eff22703b2c79915 +/schedule-moves-into-the-automation-tasks-optional-bundle/change: + en: b1b250e677c23b10 + zh: b71c1591ea4f9ce5 +/schedule-moves-into-the-automation-tasks-optional-bundle/migration: + en: 656fea5dc45f8310 + zh: 8dfe16d2fdacf71a diff --git a/docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.md b/docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.md index ccd08f683d..72ffb9f689 100644 --- a/docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.md +++ b/docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.md @@ -5,6 +5,8 @@ description: "The Web composition no longer carries the Schedule rows; the Autom # Schedule moves into the Automation tasks optional bundle +English | [中文](guide.zh.md) + ## Change In v0.1.7-rc.2, `@deepseek-ai/dsh-web-app` carried the `time-context`, `schedule`, and `ui-schedule` rows with `disabled: true`. Turning Schedule on in the Plugins page, or by hand, wrote id-targeted overrides such as `- id: schedule` with `disabled: false` into `$DSH_HOME/profiles//cordis.patch.yml` or a `--patch` overlay. diff --git a/docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.zh.md b/docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.zh.md new file mode 100644 index 0000000000..46483a1175 --- /dev/null +++ b/docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.zh.md @@ -0,0 +1,22 @@ +--- +kind: upgrade-guide +description: "Web 组合不再包含 Schedule 的三行;改由“自动化任务”可选 bundle 插入。" +--- + +# Schedule 移入“自动化任务”可选 bundle + +[English](guide.md) | 中文 + +## 变更 + +在 v0.1.7-rc.2 中,`@deepseek-ai/dsh-web-app` 以 `disabled: true` 包含 `time-context`、`schedule` 和 `ui-schedule` 三行。在插件管理页或手动开启 Schedule,会向 `$DSH_HOME/profiles//cordis.patch.yml` 或 `--patch` overlay 写入按 id 定位的覆盖项,例如 `- id: schedule` 加 `disabled: false`。 + +下一版本从 Web 组合中移除这三行。插件管理页“官方”分组中的“自动化任务”,即 `@deepseek-ai/dsh-experimental-schedule-bundle`,负责插入它们。每个安装都自带该 bundle,默认关闭。 + +按 id 开启 Schedule 的 profile 升级后会失去 Schedule:loader 警告 `patch: entry schedule not found`(`time-context` 和 `ui-schedule` 同理),`schedule_*` 工具和自动化任务页面消失,已存储的提醒不再投递。已存储的任务和投递记录仍保留在磁盘上。 + +## 迁移 + +1. 打开插件管理页,在“官方”分组中找到“自动化任务”并开启。开关会把 `@deepseek-ai/dsh-experimental-schedule-bundle` 追加到 `$DSH_HOME/profiles//package.json` 的 `dsh.profile.bundles`;手动维护的 profile 需自行添加该条目。 +2. 保留为 `schedule`、`time-context` 或 `ui-schedule` 设置其他字段的覆盖项,例如 `deliveryHistoryDays`;bundle 插入这些行后它们重新生效。只设置 `disabled: false` 的覆盖项已无作用,可以删除。 +3. 确认:重启 `dsh web`,检查启动日志中这三个 id 不再出现 `patch: entry ... not found` 警告,且侧栏显示“自动化任务”并列出之前存储的提醒。 diff --git a/docs/upgrade-guide/v0.1.7-rc.2/session-log-otel-collector/guide.md b/docs/upgrade-guide/v0.1.7-rc.2/session-log-otel-collector/guide.md deleted file mode 100644 index e96eaf087f..0000000000 --- a/docs/upgrade-guide/v0.1.7-rc.2/session-log-otel-collector/guide.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -kind: upgrade-guide -description: "Feedback Session-log uploads go to a new collector host and require the @deepseek-ai/dsh-otel service." ---- - -# Session-log uploads use the shared OTel service and a new collector - -## Change - -In v0.1.7-rc.2, `@deepseek-ai/dsh-session-telemetry-otel` owned its own OTLP transport and sent feedback-authorized Session logs to `https://harness-telemetry.deepseeksvc.com/v1/logs`. - -From the next release: - -- The plugin injects the `otel` service from the new `@deepseek-ai/dsh-otel` plugin. `@deepseek-ai/dsh-base` mounts it as the `otel` row; a composition that mounts `session-telemetry-otel` without that row leaves the plugin inactive, and feedback uploads stop. -- The default collector is `https://dsh-otel-collector.deepseeksvc.com/v1/logs`. `DSH_TELEMETRY_OTLP_URL` still overrides it. -- Uploads are split into requests of at most 4,000,000 uncompressed bytes (`maxRequestBytes`). - -Profiles that extend `@deepseek-ai/dsh-base` and networks with unrestricted egress need no action. Persisted Session formats are unchanged. - -## Migration - -1. If a custom composition lists `session-telemetry-otel` without `@deepseek-ai/dsh-base`, add the row before it: - - ```yaml - - id: otel - name: '@deepseek-ai/dsh-otel' - ``` - -2. If an egress allowlist or proxy rule names `harness-telemetry.deepseeksvc.com`, add `dsh-otel-collector.deepseeksvc.com`. A deployment that points `DSH_TELEMETRY_OTLP_URL` at its own collector keeps its setting. -3. Confirm: send feedback with Session-log upload enabled and check that the configured collector receives the request; for a self-hosted collector, check its receive log. diff --git a/docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.i18n.yaml b/docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.i18n.yaml new file mode 100644 index 0000000000..89d7925446 --- /dev/null +++ b/docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.md +/: + en: 7d7e527ed3c87db1 + zh: 4654dab85b66bcb0 +/legacy-normal-work-details-display-as-detailed: + en: 3c88b0b06094c162 + zh: 9ae8ff49975824a9 +/legacy-normal-work-details-display-as-detailed/change: + en: 0e2287aa768141f3 + zh: 035992a94129fd9e +/legacy-normal-work-details-display-as-detailed/migration: + en: 6078ccce1ceacfd6 + zh: 6a394cf5259c9965 diff --git a/docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.md b/docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.md index b0af748cad..cfb2be2793 100644 --- a/docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.md +++ b/docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.md @@ -5,6 +5,8 @@ description: "The legacy ui-chat.transcriptView value normal, and an unset value # Legacy `normal` work details display as Detailed +English | [中文](guide.zh.md) + ## Change The Host setting `ui-chat.transcriptView` selects Settings → General → Work details. In v0.1.7-rc.2 the client displayed the legacy saved value `normal` as `standard`, and every client used `standard` when the setting was missing, `null`, or invalid. diff --git a/docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.zh.md b/docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.zh.md new file mode 100644 index 0000000000..4bcc77d18e --- /dev/null +++ b/docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.zh.md @@ -0,0 +1,25 @@ +--- +kind: upgrade-guide +description: "旧值 ui-chat.transcriptView: normal,以及非 Desktop Web 中未设置的值,改为按“详细”而非“标准”展示。" +--- + +# 旧值 `normal` 的工作步骤展示改为“详细” + +[English](guide.md) | 中文 + +## 变更 + +Host 设置 `ui-chat.transcriptView` 对应设置 → 通用设置 → 工作步骤展示。在 v0.1.7-rc.2 中,客户端把旧保存值 `normal` 按 `standard` 展示;设置缺失、为 `null` 或无效时,所有客户端都使用 `standard`。 + +从下一版本起: + +- 保存的 `normal` 在 Desktop 和 Web 中均按 `detailed` 展示,磁盘中的保存值不会被改写。 +- 设置缺失、为 `null` 或无效时,非 Desktop Web(npm 安装的 `dsh web`)按 `detailed` 展示;Desktop 仍使用 `standard`。 +- 已保存的 `compact`、`standard`、`detailed` 和 `verbose` 保持不变。 + +依赖旧读取方式的用户会看到运行中轮次的过程组正文直接展开,而不是收起的摘要。 + +## 迁移 + +1. 如需保留原有展示,打开设置 → 通用设置 → 工作步骤展示并选择“标准”。客户端会把 `standard` 保存到 `ui-chat.transcriptView`,此后的默认值变化不影响该值。 +2. 确认:发起一个调用工具的轮次,检查其过程组显示收起的摘要和实时任务详情。 diff --git a/scripts/translation-pairing.manifest.json b/scripts/translation-pairing.manifest.json index fe517aa995..f9bb1549a2 100644 --- a/scripts/translation-pairing.manifest.json +++ b/scripts/translation-pairing.manifest.json @@ -8,7 +8,6 @@ "docs/cordis-api/inherited.md", "docs/i18n/style-samples.md", "docs/i18n/terminology.md", - "docs/i18n/translation-prompt.md", - "docs/upgrade-guide/" + "docs/i18n/translation-prompt.md" ] } diff --git a/scripts/verify-upgrade-guides.spec.ts b/scripts/verify-upgrade-guides.spec.ts index 07b1e516aa..6d92157e8a 100644 --- a/scripts/verify-upgrade-guides.spec.ts +++ b/scripts/verify-upgrade-guides.spec.ts @@ -34,6 +34,11 @@ const VALID = [ '', ].join('\n') +const VALID_ZH = VALID + .replace('description: "The `--preset` flag is removed."', 'description: "移除 `--preset` 参数。"') + .replace('## Change', '## 变更') + .replace('## Migration', '## 迁移') + function fixtureRoot(files: Record): string { const root = mkdtempSync(join(tmpdir(), 'dsh-upgrade-guides-')) roots.push(root) @@ -46,30 +51,32 @@ function fixtureRoot(files: Record): string { } describe('upgrade guide gate', () => { - it('accepts an absent tree and conforming guides, including prerelease versions and CRLF files', () => { + it('accepts an absent tree and conforming guide pairs, including prerelease versions and CRLF files', () => { expect(collectUpgradeGuideViolations(fixtureRoot({}))).toEqual([]) expect(collectUpgradeGuideViolations(fixtureRoot({ 'v0.1.7-rc.2/profile-flag/guide.md': VALID, + 'v0.1.7-rc.2/profile-flag/guide.zh.md': VALID_ZH, + 'v0.1.7-rc.2/profile-flag/guide.i18n.yaml': 'not parsed by this gate\n', 'v1.0.0/crlf-item/guide.md': VALID.replaceAll('\n', '\r\n'), }))).toEqual([]) }) - it('rejects files outside the version/item/guide.md layout', () => { + it('rejects files outside the version/item/guide pair layout', () => { expect(collectUpgradeGuideViolations(fixtureRoot({ 'README.md': VALID, '0.1.7/item/guide.md': VALID, 'v0.1/item/guide.md': VALID, 'v0.1.7/Item_Name/guide.md': VALID, - 'v0.1.7/item/guide.zh.md': VALID, + 'v0.1.7/item/notes.md': VALID, 'v0.1.7/item/extra/guide.md': VALID, }))).toEqual([ 'docs/upgrade-guide/0.1.7/item/guide.md', 'docs/upgrade-guide/README.md', 'docs/upgrade-guide/v0.1.7/Item_Name/guide.md', 'docs/upgrade-guide/v0.1.7/item/extra/guide.md', - 'docs/upgrade-guide/v0.1.7/item/guide.zh.md', + 'docs/upgrade-guide/v0.1.7/item/notes.md', 'docs/upgrade-guide/v0.1/item/guide.md', - ].map(path => `${path}: only v//guide.md files belong in docs/upgrade-guide`)) + ].map(path => `${path}: only v//guide.{md,zh.md,i18n.yaml} files belong in docs/upgrade-guide`)) }) it('rejects missing, extra, or invalid frontmatter fields', () => { @@ -102,11 +109,22 @@ describe('upgrade guide gate', () => { expect(violations[0]).toBe('docs/upgrade-guide/v0.1.7/extra/guide.md: headings must be one "#" title followed by "## Change", "## Migration"; got "# `--profile` replaces `--preset`", "## Change", "## Migration", "## References"') }) - it('rejects a guide over 500 words, counting frontmatter', () => { + it('checks the Chinese sibling against its own section names and metadata', () => { + expect(collectUpgradeGuideViolations(fixtureRoot({ + 'v0.1.7/english-headings/guide.zh.md': VALID, + 'v0.1.7/wrong-kind/guide.zh.md': VALID_ZH.replace('kind: upgrade-guide', 'kind: guide'), + }))).toEqual([ + 'docs/upgrade-guide/v0.1.7/english-headings/guide.zh.md: headings must be one "#" title followed by "## 变更", "## 迁移"; got "# `--profile` replaces `--preset`", "## Change", "## Migration"', + 'docs/upgrade-guide/v0.1.7/wrong-kind/guide.zh.md: kind must be upgrade-guide', + ]) + }) + + it('rejects an English guide over 500 words, counting frontmatter, and leaves the Chinese sibling uncounted', () => { const words = VALID.split(/\s+/u).filter(Boolean).length expect(collectUpgradeGuideViolations(fixtureRoot({ 'v0.1.7/at-limit/guide.md': `${VALID}${'word '.repeat(500 - words)}\n`, 'v0.1.7/over-limit/guide.md': `${VALID}${'word '.repeat(501 - words)}\n`, + 'v0.1.7/over-limit/guide.zh.md': `${VALID_ZH}${'词 '.repeat(600)}\n`, }))).toEqual(['docs/upgrade-guide/v0.1.7/over-limit/guide.md: 501 words exceeds the 500-word ceiling']) }) }) diff --git a/scripts/verify-upgrade-guides.ts b/scripts/verify-upgrade-guides.ts index 9a776013af..bb067cb41f 100644 --- a/scripts/verify-upgrade-guides.ts +++ b/scripts/verify-upgrade-guides.ts @@ -1,6 +1,7 @@ /** * Enforce the `docs/upgrade-guide/v//guide.md` layout, metadata, * sections, and word ceiling defined by the `dsh-create-upgrade-guide` skill. + * `verify-translation-pairing` owns the Chinese sibling's pairing record. * @module scripts/verify-upgrade-guides */ @@ -11,17 +12,20 @@ import { fromMarkdown } from 'mdast-util-from-markdown' const ROOT = resolve(import.meta.dirname, '..') const GUIDE_ROOT = 'docs/upgrade-guide' -const MAX_WORDS = 500 const FRONTMATTER_KEYS = ['description', 'kind'] -const SECTIONS = ['Change', 'Migration'] +/** Required `##` sections for each guide language; the word ceiling applies to the English source. */ +const LANGUAGES = { + 'guide.md': { sections: ['Change', 'Migration'], maxWords: 500 }, + 'guide.zh.md': { sections: ['变更', '迁移'], maxWords: undefined }, +} as const const SEMVER = String.raw`(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?` -const GUIDE_PATH = new RegExp(`^v${SEMVER}/[a-z0-9]+(?:-[a-z0-9]+)*/guide\\.md$`, 'u') +const GUIDE_PATH = new RegExp(`^v${SEMVER}/[a-z0-9]+(?:-[a-z0-9]+)*/(guide\\.md|guide\\.zh\\.md|guide\\.i18n\\.yaml)$`, 'u') /** Return one guide's frontmatter and structure violations. */ -function guideViolations(source: string): string[] { +function guideViolations(source: string, language: typeof LANGUAGES[keyof typeof LANGUAGES]): string[] { const violations: string[] = [] const words = source.split(/\s+/u).filter(Boolean).length - if (words > MAX_WORDS) violations.push(`${String(words)} words exceeds the ${String(MAX_WORDS)}-word ceiling`) + if (language.maxWords !== undefined && words > language.maxWords) violations.push(`${String(words)} words exceeds the ${String(language.maxWords)}-word ceiling`) const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---\r?\n/u.exec(source) if (frontmatter === null) return [...violations, 'must start with YAML frontmatter'] @@ -36,7 +40,7 @@ function guideViolations(source: string): string[] { const body = source.slice(frontmatter[0].length) const outline = fromMarkdown(body).children .flatMap(node => node.type === 'heading' && node.depth <= 2 ? [body.slice(node.position?.start.offset, node.position?.end.offset)] : []) - const expected = SECTIONS.map(section => `## ${section}`) + const expected = language.sections.map(section => `## ${section}`) if (outline.length !== expected.length + 1 || !outline[0]?.startsWith('# ') || outline.slice(1).join('\n') !== expected.join('\n')) { violations.push(`headings must be one "#" title followed by ${expected.map(heading => `"${heading}"`).join(', ')}; got ${outline.map(heading => `"${heading}"`).join(', ') || 'none'}`) } @@ -58,11 +62,14 @@ export function collectUpgradeGuideViolations(root: string): string[] { .sort() for (const file of files) { const path = `${GUIDE_ROOT}/${file}` - if (!GUIDE_PATH.test(file)) { - violations.push(`${path}: only v//guide.md files belong in ${GUIDE_ROOT}`) + const name = GUIDE_PATH.exec(file)?.[1] + if (name === undefined) { + violations.push(`${path}: only v//guide.{md,zh.md,i18n.yaml} files belong in ${GUIDE_ROOT}`) continue } - for (const violation of guideViolations(readFileSync(join(directory, file), 'utf8'))) violations.push(`${path}: ${violation}`) + if (name === 'guide.md' || name === 'guide.zh.md') { + for (const violation of guideViolations(readFileSync(join(directory, file), 'utf8'), LANGUAGES[name])) violations.push(`${path}: ${violation}`) + } } return violations } From 8671d305831e7508a69d8445ad25240acd4e8c06 Mon Sep 17 00:00:00 2001 From: lsdsjy <1356263+lsdsjy@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:00:58 +0800 Subject: [PATCH 04/10] =?UTF-8?q?fix(web):=20=E6=97=A0=E8=B5=A0=E9=87=91?= =?UTF-8?q?=E6=97=B6=E9=9A=90=E8=97=8F=E8=AE=BE=E7=BD=AE=E9=A1=B5=E8=B5=A0?= =?UTF-8?q?=E9=87=91=E8=A1=8C=E5=B9=B6=E4=BF=AE=E6=AD=A3=E5=BC=95=E5=AF=BC?= =?UTF-8?q?=E5=85=85=E5=80=BC=E6=96=87=E6=A1=88=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 余额读取成功但没有正额赠金时,账号设置页不再显示赠金余额行及其分隔线; 加载中与读取失败时仍保留该行。删除不再使用的 bonusEmpty 文案。 引导页跳过充值的英文提示改为指向 Settings → Accounts。 --- packages/client/ui-settings-account/README.i18n.yaml | 4 ++-- packages/client/ui-settings-account/README.md | 2 +- packages/client/ui-settings-account/README.zh.md | 2 +- .../ui-settings-account/src/client/AccountSection.tsx | 6 ++---- packages/client/ui-settings-account/src/client/locales.ts | 2 -- .../ui-settings-account/src/client/locales/onboarding.ts | 2 +- .../ui-settings-account/tests/account.client.spec.tsx | 6 ++---- .../ui-settings-account/tests/expected/details-en.txt | 2 +- .../ui-settings-account/tests/expected/details-zh.txt | 2 +- 9 files changed, 11 insertions(+), 17 deletions(-) diff --git a/packages/client/ui-settings-account/README.i18n.yaml b/packages/client/ui-settings-account/README.i18n.yaml index 5aba3a01d9..1b597f4d27 100644 --- a/packages/client/ui-settings-account/README.i18n.yaml +++ b/packages/client/ui-settings-account/README.i18n.yaml @@ -24,8 +24,8 @@ en: 6b3ad3b6860100cf zh: aeced4fb37642f15 /deepseek-ai-dsh-client-ui-settings-account/further-exploration: - en: c785923662681191 - zh: f24389c25e954035 + en: a28acdef21b1643a + zh: 5e2e357a6b716274 /deepseek-ai-dsh-client-ui-settings-account/model-experience: en: ee1df7ca3467ee3f zh: 84904a5148da3862 diff --git a/packages/client/ui-settings-account/README.md b/packages/client/ui-settings-account/README.md index 75f37f6789..18604436f7 100644 --- a/packages/client/ui-settings-account/README.md +++ b/packages/client/ui-settings-account/README.md @@ -77,7 +77,7 @@ Account login uses a dismissible dialog before the model onboarding credential e A terminal account-state stream failure appears in the sign-in dialog or Account settings. Plugin unload suppresses late failure reports. -The balance card shows recharge funds and bonus credit in separate rows. The bonus row is present while signed in; without positive bonus credit it states that no bonus is available. A failed wallet read shows the existing unavailable copy as a link to Platform — the same destination and behavior as the Usage action, so the embedded page opens on Desktop and a new tab elsewhere — while the loading and empty states stay plain text. Currencies retain their own amounts, and a notice date is rendered only inside the server’s own message. +The balance card shows recharge funds and bonus credit in separate rows. While signed in, the bonus row appears during loading, after a failed wallet read, and when a wallet holds positive bonus credit; a successful read without positive bonus credit omits the row. A failed wallet read shows the existing unavailable copy as a link to Platform — the same destination and behavior as the Usage action, so the embedded page opens on Desktop and a new tab elsewhere — while the loading state stays plain text. Currencies retain their own amounts, and a notice date is rendered only inside the server’s own message. A successful login selects the first available `deepseek-account` model as the default when no other provider has a configured API key, even if a previous default was saved. Configured keys preserve the existing default even when their provider has no available models. Session-specific selections remain unchanged. diff --git a/packages/client/ui-settings-account/README.zh.md b/packages/client/ui-settings-account/README.zh.md index 1b7b184587..ccc6daaeae 100644 --- a/packages/client/ui-settings-account/README.zh.md +++ b/packages/client/ui-settings-account/README.zh.md @@ -77,7 +77,7 @@ Desktop preload 标记在账号凭证已存储后启用引导。进度属于本 账户状态流的终止错误会显示在登录对话框或账户设置中。插件卸载后不再报告延迟到达的错误。 -余额卡片分行展示充值余额与赠金余额。赠金行在登录态下常驻;没有正额赠金时显示暂无可用赠金。余额或赠金读取失败时,沿用既有的不可用文案并改为指向 Platform 的链接,目标与行为同“查询用量”一致:Desktop 打开内嵌页面,其它环境新开标签页;加载中与暂无赠金仍为纯文本。不同币种分别显示;日期只在服务端自己的消息中出现。 +余额卡片分行展示充值余额与赠金余额。登录态下,赠金行在加载中、余额读取失败以及存在正额赠金时显示;读取成功但没有正额赠金时不显示该行。余额或赠金读取失败时,沿用既有的不可用文案并改为指向 Platform 的链接,目标与行为同“查询用量”一致:Desktop 打开内嵌页面,其它环境新开标签页;加载中仍为纯文本。不同币种分别显示;日期只在服务端自己的消息中出现。 登录成功时,若其他提供方均未配置 API key,则将 `deepseek-account` 的第一个可用模型设为默认值,即使此前保存过默认选择。其他提供方只要配置了 key,就保留原默认值,即使其没有可用模型。会话内的显式选择保持不变。 diff --git a/packages/client/ui-settings-account/src/client/AccountSection.tsx b/packages/client/ui-settings-account/src/client/AccountSection.tsx index d59b45a976..953b48cc53 100644 --- a/packages/client/ui-settings-account/src/client/AccountSection.tsx +++ b/packages/client/ui-settings-account/src/client/AccountSection.tsx @@ -183,7 +183,7 @@ export function AccountSection({ t, useAccount, useTheme, start, cancel, openPla ? {t(!signedIn ? 'balanceSignedOut' : 'loading')} : platformLink(t('balanceUnavailable'), css.unavailableLink)} - {signedIn && <> + {signedIn && (details?.balance?.status !== 'ready' || bonusWallets.length > 0) && <>
{t('bonusBalance')} @@ -194,9 +194,7 @@ export function AccountSection({ t, useAccount, useTheme, start, cancel, openPla )} : details?.balance === undefined ? {t('loading')} - : details.balance.status === 'failed' - ? platformLink(t('balanceUnavailable'), css.unavailableLink) - : {t('bonusEmpty')}} + : platformLink(t('balanceUnavailable'), css.unavailableLink)}
} diff --git a/packages/client/ui-settings-account/src/client/locales.ts b/packages/client/ui-settings-account/src/client/locales.ts index 9b6178bf38..e6f743a4a5 100644 --- a/packages/client/ui-settings-account/src/client/locales.ts +++ b/packages/client/ui-settings-account/src/client/locales.ts @@ -33,7 +33,6 @@ export const en = { quotaDescription: 'DeepSeek Harness cannot start a new task with this account when no balance is available. Would you like to top up? You can also top up later in Settings → Account.', quotaTopUp: 'Top up', bonusNoticeTitle: 'Bonus credited', - bonusEmpty: 'No bonus available', } as const /** Account locale keys. */ export type AccountKey = keyof typeof en @@ -69,5 +68,4 @@ export const zh: Record = { quotaDescription: '没有可用额度时,DeepSeek Harness 无法开始新的任务,是否前往充值?或者可以稍后前往 设置 → 账号与余额 进行充值。', quotaTopUp: '去充值', bonusNoticeTitle: '赠金已到账', - bonusEmpty: '暂无可用赠金', } diff --git a/packages/client/ui-settings-account/src/client/locales/onboarding.ts b/packages/client/ui-settings-account/src/client/locales/onboarding.ts index 5e3b71cf20..d3b1e22be8 100644 --- a/packages/client/ui-settings-account/src/client/locales/onboarding.ts +++ b/packages/client/ui-settings-account/src/client/locales/onboarding.ts @@ -76,7 +76,7 @@ export const onboardingEnglishCopy: Record onboardingSkipDescription: 'You can change how progress, performance, and usage are displayed, or enable Coding Tools, anytime in Profile → General Settings.', onboardingKeepSetting: 'Continue setup', onboardingNoCreditTitle: 'Skip adding credits?', - onboardingNoCreditDescription: 'DeepSeek Harness can’t start new tasks without credits. You can add them later in Profile → Accounts.', + onboardingNoCreditDescription: 'DeepSeek Harness can’t start new tasks without credits. You can add them later in Settings → Accounts.', onboardingUnderstood: 'Got it', onboardingGoTopUp: 'Add credits', onboardingSaveFailed: 'Could not save settings. Try again.', diff --git a/packages/client/ui-settings-account/tests/account.client.spec.tsx b/packages/client/ui-settings-account/tests/account.client.spec.tsx index 5294b9be22..577e213b5a 100644 --- a/packages/client/ui-settings-account/tests/account.client.spec.tsx +++ b/packages/client/ui-settings-account/tests/account.client.spec.tsx @@ -446,13 +446,11 @@ it.each([en, zh])('renders positive bonus wallets separately from recharge balan }) it.each([[], [{ currency: 'CNY' as const, balance: '0.00' }, { currency: 'USD' as const, balance: '-1.00' }]].map(bonusWallets => ({ bonusWallets })))( - 'keeps the bonus row without inventing credit for zero or negative wallets', ({ bonusWallets }) => { + 'omits the bonus row for zero or negative wallets', ({ bonusWallets }) => { mount({ status: 'credential-stored', attempt: null }, en, { balance: { status: 'ready', value: [{ currency: 'CNY', balance: '0' }], bonusWallets }, }) - // The row itself stays and states the absence. - expect(screen.getByText(en.bonusBalance)).toBeTruthy() - expect(screen.getByText(en.bonusEmpty)).toBeTruthy() + expect(screen.queryByText(en.bonusBalance)).toBeNull() expect(screen.getByText('¥0.00')).toBeTruthy() expect(screen.queryByText('¥-1.00')).toBeNull() }, diff --git a/packages/client/ui-settings-account/tests/expected/details-en.txt b/packages/client/ui-settings-account/tests/expected/details-en.txt index da68efdf2b..08cdd9dbe9 100644 --- a/packages/client/ui-settings-account/tests/expected/details-en.txt +++ b/packages/client/ui-settings-account/tests/expected/details-en.txt @@ -1 +1 @@ -Harness Mock (TEST ONLY)138****0000More account informationTopped-up balance¥1,234.56<$0.01Granted balanceNo bonus availableMoreView usageTop up +Harness Mock (TEST ONLY)138****0000More account informationTopped-up balance¥1,234.56<$0.01MoreView usageTop up diff --git a/packages/client/ui-settings-account/tests/expected/details-zh.txt b/packages/client/ui-settings-account/tests/expected/details-zh.txt index 75bc72e8f8..725935948e 100644 --- a/packages/client/ui-settings-account/tests/expected/details-zh.txt +++ b/packages/client/ui-settings-account/tests/expected/details-zh.txt @@ -1 +1 @@ -Harness Mock (TEST ONLY)138****0000更多账号信息充值余额¥1,234.56<$0.01赠金余额暂无可用赠金更多查询用量充值 +Harness Mock (TEST ONLY)138****0000更多账号信息充值余额¥1,234.56<$0.01更多查询用量充值 From 30a39133978da2f218d7b306bdde2585b7ae079a Mon Sep 17 00:00:00 2001 From: lsdsjy <1356263+lsdsjy@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:12:35 +0800 Subject: [PATCH 05/10] =?UTF-8?q?fix(web):=20=E7=BB=9F=E4=B8=80=E5=BC=95?= =?UTF-8?q?=E5=AF=BC=E9=A1=B5=E8=AE=BE=E7=BD=AE=E5=85=A5=E5=8F=A3=E6=96=87?= =?UTF-8?q?=E6=A1=88=E5=B9=B6=E8=A6=86=E7=9B=96=E8=B5=A0=E9=87=91=E8=A1=8C?= =?UTF-8?q?=E5=88=86=E9=9A=94=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 引导页两条确认提示改为指向实际入口:Settings → Account / Settings → General, 中文同步为 设置 → 账号与余额 / 设置 → 通用设置。赠金行可见性提为 showBonusRow, 并断言隐藏赠金行时其分隔线一并移除。 --- .../ui-settings-account/src/client/AccountSection.tsx | 3 ++- .../ui-settings-account/src/client/locales/onboarding.ts | 8 ++++---- .../ui-settings-account/tests/account.client.spec.tsx | 4 ++++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/client/ui-settings-account/src/client/AccountSection.tsx b/packages/client/ui-settings-account/src/client/AccountSection.tsx index 953b48cc53..6d020ad29a 100644 --- a/packages/client/ui-settings-account/src/client/AccountSection.tsx +++ b/packages/client/ui-settings-account/src/client/AccountSection.tsx @@ -93,6 +93,7 @@ export function AccountSection({ t, useAccount, useTheme, start, cancel, openPla const wallets = details?.balance?.status === 'ready' ? details.balance.value : undefined const bonusWallets = details?.balance?.status === 'ready' ? details.balance.bonusWallets.filter(wallet => new Big(wallet.balance).gt(0)) : [] + const showBonusRow = details?.balance?.status !== 'ready' || bonusWallets.length > 0 const attempt = state?.attempt const active = attempt !== null && attempt !== undefined && ['initializing', 'waiting-browser', 'exchanging', 'committing'].includes(attempt.phase) @@ -183,7 +184,7 @@ export function AccountSection({ t, useAccount, useTheme, start, cancel, openPla ? {t(!signedIn ? 'balanceSignedOut' : 'loading')} : platformLink(t('balanceUnavailable'), css.unavailableLink)}
- {signedIn && (details?.balance?.status !== 'ready' || bonusWallets.length > 0) && <> + {signedIn && showBonusRow && <>
{t('bonusBalance')} diff --git a/packages/client/ui-settings-account/src/client/locales/onboarding.ts b/packages/client/ui-settings-account/src/client/locales/onboarding.ts index d3b1e22be8..509986446e 100644 --- a/packages/client/ui-settings-account/src/client/locales/onboarding.ts +++ b/packages/client/ui-settings-account/src/client/locales/onboarding.ts @@ -30,10 +30,10 @@ export const onboardingCopy = { onboardingBack: '上一步', onboardingSkip: '跳过', onboardingSkipTitle: '跳过设置?', - onboardingSkipDescription: '你可以随时前往 个人中心 → 通用设置 中调整工作过程、性能与用量展示方式或开启代码工作工具。', + onboardingSkipDescription: '你可以随时前往 设置 → 通用设置 中调整工作过程、性能与用量展示方式或开启代码工作工具。', onboardingKeepSetting: '继续设置', onboardingNoCreditTitle: '暂时跳过充值?', - onboardingNoCreditDescription: '没有可用额度时,DeepSeek Harness 将无法开始新的任务。你可以稍后前往 个人中心 → 账号与余额 进行充值。', + onboardingNoCreditDescription: '没有可用额度时,DeepSeek Harness 将无法开始新的任务。你可以稍后前往 设置 → 账号与余额 进行充值。', onboardingUnderstood: '我知道了', onboardingGoTopUp: '前往充值', onboardingSaveFailed: '未能保存设置,请重试。', @@ -73,10 +73,10 @@ export const onboardingEnglishCopy: Record onboardingBack: 'Back', onboardingSkip: 'Skip', onboardingSkipTitle: 'Skip setup?', - onboardingSkipDescription: 'You can change how progress, performance, and usage are displayed, or enable Coding Tools, anytime in Profile → General Settings.', + onboardingSkipDescription: 'You can change how progress, performance, and usage are displayed, or enable Coding Tools, anytime in Settings → General.', onboardingKeepSetting: 'Continue setup', onboardingNoCreditTitle: 'Skip adding credits?', - onboardingNoCreditDescription: 'DeepSeek Harness can’t start new tasks without credits. You can add them later in Settings → Accounts.', + onboardingNoCreditDescription: 'DeepSeek Harness can’t start new tasks without credits. You can add them later in Settings → Account.', onboardingUnderstood: 'Got it', onboardingGoTopUp: 'Add credits', onboardingSaveFailed: 'Could not save settings. Try again.', diff --git a/packages/client/ui-settings-account/tests/account.client.spec.tsx b/packages/client/ui-settings-account/tests/account.client.spec.tsx index 577e213b5a..cae2d7fac0 100644 --- a/packages/client/ui-settings-account/tests/account.client.spec.tsx +++ b/packages/client/ui-settings-account/tests/account.client.spec.tsx @@ -13,6 +13,7 @@ import type { BonusNotice } from '../src/client/bonus-notices.ts' import type { AccountMenuProps } from '../src/client/AccountMenu.tsx' import type {} from '../src/client/index.ts' import { en, zh, type AccountKey } from '../src/client/locales.ts' +import css from '../src/client/AccountSection.module.css' afterEach(() => { cleanup(); vi.unstubAllGlobals() }) @@ -441,6 +442,7 @@ it.each([en, zh])('renders positive bonus wallets separately from recharge balan }) expect(screen.getByText(copy.balance).parentElement!.textContent).toBe(`${copy.balance}¥209.00$20.07`) expect(screen.getByText(copy.bonusBalance).parentElement!.textContent).toBe(`${copy.bonusBalance}¥5.00<$0.01`) + expect(screen.getByRole('region').querySelectorAll(`.${css.divider}`)).toHaveLength(2) await expect(`${screen.getByRole('region').textContent}\n`) .toMatchFileSnapshot(`./expected/bonus-${copy === en ? 'en' : 'zh'}.txt`) }) @@ -451,6 +453,8 @@ it.each([[], [{ currency: 'CNY' as const, balance: '0.00' }, { currency: 'USD' a balance: { status: 'ready', value: [{ currency: 'CNY', balance: '0' }], bonusWallets }, }) expect(screen.queryByText(en.bonusBalance)).toBeNull() + // The divider above the bonus row goes with it. + expect(screen.getByRole('region').querySelectorAll(`.${css.divider}`)).toHaveLength(1) expect(screen.getByText('¥0.00')).toBeTruthy() expect(screen.queryByText('¥-1.00')).toBeNull() }, From 7a0c6ebd9e002e02181bfc8809e0e085922e8e69 Mon Sep 17 00:00:00 2001 From: Yif <877193178@qq.com> Date: Tue, 29 Sep 2026 11:46:03 +0800 Subject: [PATCH 06/10] refine(web): show only the package-name example in the install guide The install guide listed the package name, a GitHub address, and a local directory as numbered steps; only the package name needs an example. Keep that entry and drop the index badge, give the item's hint and example the title's text color, and balance the container's vertical padding. The trust box above Install also states that installed plugins do not update automatically, that upgrading means uninstalling and installing the new version, and that later releases will keep improving that experience. --- .../cancelled.expected.md | 2 +- .../another-way.expected.md | 12 ++------ .../plugin-install-github/mirror.expected.md | 2 +- apps/web/tests/plugin-manager.e2e.ts | 11 +++++-- .../client/ui-plugin-manager/README.i18n.yaml | 4 +-- packages/client/ui-plugin-manager/README.md | 2 +- .../client/ui-plugin-manager/README.zh.md | 2 +- .../src/client/PluginManagerPage.module.css | 30 +++++++------------ .../src/client/PluginManagerPage.tsx | 12 ++++---- .../ui-plugin-manager/src/client/locales.ts | 18 +++-------- .../tests/components.client.spec.tsx | 15 +++++----- 11 files changed, 45 insertions(+), 65 deletions(-) diff --git a/apps/web/tests/expected/plugin-install-cancel/cancelled.expected.md b/apps/web/tests/expected/plugin-install-cancel/cancelled.expected.md index 80e2756b5a..9999ca0af5 100644 --- a/apps/web/tests/expected/plugin-install-cancel/cancelled.expected.md +++ b/apps/web/tests/expected/plugin-install-cancel/cancelled.expected.md @@ -7,5 +7,5 @@ - text: slow-package - button "插件安装引导和示例" - button "安装源 默认安装源" - - note: 请确认插件来源可信。插件在本机以你的权限运行,来源不明的插件可能损坏 DeepSeek Harness,或读取和泄露你的数据。 + - note: 请确认插件来源可信。插件在本机以你的权限运行,来源不明的插件可能损坏 DeepSeek Harness,或读取和泄露你的数据。 插件安装后,暂不支持自动更新。若需升级,请先卸载再安装新版,后续版本会持续改善升级体验。 - button "安装" diff --git a/apps/web/tests/expected/plugin-install-github/another-way.expected.md b/apps/web/tests/expected/plugin-install-github/another-way.expected.md index 0932737ec2..66a9943dd0 100644 --- a/apps/web/tests/expected/plugin-install-github/another-way.expected.md +++ b/apps/web/tests/expected/plugin-install-github/another-way.expected.md @@ -7,16 +7,8 @@ - button "安装源 中国大陆镜像源" - list: - listitem: - - text: 包名 插件包名即 npm 包名(如 dsh-xxx 或 @作者/插件名),社区插件的 README 安装命令中 dsh plugin add 或 pnpm add 之后的部分。 示例: + - text: 填入插件 npm 包名 插件包名即 npm 包名(如 dsh-xxx 或 @作者/插件名),社区插件的 README 安装命令中 dsh plugin add 或 pnpm add 之后的部分。 示例: - code: dsh-plugin-whale-pet - button "填入示例 dsh-plugin-whale-pet": 填入示例 - - listitem: - - text: GitHub 仓库地址 插件在 GitHub 上的开源仓库地址,也支持其他 Git 仓库。 示例: - - code: https://github.com/author/dsh-plugin - - button "填入示例 https://github.com/author/dsh-plugin": 填入示例 - - listitem: - - text: 本地插件目录 本机上插件目录的绝对路径,适用于自行开发或已下载的插件。 示例: - - code: /Users/name/my-plugin - - button "填入示例 /Users/name/my-plugin": 填入示例 - - note: 请确认插件来源可信。插件在本机以你的权限运行,来源不明的插件可能损坏 DeepSeek Harness,或读取和泄露你的数据。 + - note: 请确认插件来源可信。插件在本机以你的权限运行,来源不明的插件可能损坏 DeepSeek Harness,或读取和泄露你的数据。 插件安装后,暂不支持自动更新。若需升级,请先卸载再安装新版,后续版本会持续改善升级体验。 - button "安装" [disabled] diff --git a/apps/web/tests/expected/plugin-install-github/mirror.expected.md b/apps/web/tests/expected/plugin-install-github/mirror.expected.md index e414fac6ce..4f672b66d5 100644 --- a/apps/web/tests/expected/plugin-install-github/mirror.expected.md +++ b/apps/web/tests/expected/plugin-install-github/mirror.expected.md @@ -5,5 +5,5 @@ - /placeholder: 例如 dsh-plugin-whale-pet - button "插件安装引导和示例" - button "安装源 中国大陆镜像源" - - note: 请确认插件来源可信。插件在本机以你的权限运行,来源不明的插件可能损坏 DeepSeek Harness,或读取和泄露你的数据。 + - note: 请确认插件来源可信。插件在本机以你的权限运行,来源不明的插件可能损坏 DeepSeek Harness,或读取和泄露你的数据。 插件安装后,暂不支持自动更新。若需升级,请先卸载再安装新版,后续版本会持续改善升级体验。 - button "安装" [disabled] diff --git a/apps/web/tests/plugin-manager.e2e.ts b/apps/web/tests/plugin-manager.e2e.ts index b89279d173..03c1f43128 100644 --- a/apps/web/tests/plugin-manager.e2e.ts +++ b/apps/web/tests/plugin-manager.e2e.ts @@ -835,13 +835,18 @@ describe('web e2e: plugin manager', () => { const install = dialog.getByRole('button', { name: '安装', exact: true }) expect(await install.isDisabled()).toBe(true) expect(await dialog.getByRole('note').textContent()).toContain('请确认插件来源可信') + expect(await dialog.innerText()).toContain('暂不支持自动更新') await dialog.getByRole('button', { name: '插件安装引导和示例' }).click() + // The guide carries the package-name example only; Git and path specs stay valid without guide copy. + await expect.poll(() => dialog.getByRole('listitem').count()).toBe(1) + await dialog.getByRole('button', { name: '填入示例 dsh-plugin-whale-pet' }).click() + expect(await field.inputValue()).toBe('dsh-plugin-whale-pet') + expect(await dialog.getByRole('status').count()).toBe(0) for (const [example, hint] of [ ['https://github.com/author/dsh-plugin', '请替换为实际的 Git 仓库地址'], ['/Users/name/my-plugin', '请替换为本机插件目录的实际路径'], - ]) { - await dialog.getByRole('button', { name: `填入示例 ${example}` }).click() - expect(await field.inputValue()).toBe(example) + ] as const) { + await field.fill(example) expect(await dialog.getByRole('status').textContent()).toBe(hint) } await field.fill('/actual/plugin-directory') diff --git a/packages/client/ui-plugin-manager/README.i18n.yaml b/packages/client/ui-plugin-manager/README.i18n.yaml index 29acda2d0f..02c89f4673 100644 --- a/packages/client/ui-plugin-manager/README.i18n.yaml +++ b/packages/client/ui-plugin-manager/README.i18n.yaml @@ -18,8 +18,8 @@ en: b7d2396da72ecac4 zh: 4ec8336478dd39c7 /deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/installing-a-bundle: - en: 20e7310ab6867389 - zh: eed2957381abc0bd + en: f7c3980e7bf9aa63 + zh: 9a5b9633151690d7 /deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/switching-a-bundle: en: abc90531efa889d5 zh: 0562570f85e69c35 diff --git a/packages/client/ui-plugin-manager/README.md b/packages/client/ui-plugin-manager/README.md index c075e53cf0..79e0ba8275 100644 --- a/packages/client/ui-plugin-manager/README.md +++ b/packages/client/ui-plugin-manager/README.md @@ -41,7 +41,7 @@ On first use, when pnpm uses the unconfigured official npm registry and npmmirro The Host sends GET requests to `https://registry.npmjs.org/-/ping` and `https://registry.npmmirror.com/-/ping` through its normal fetch proxy. A 2xx response wins; redirects and failures do not. The remaining request is cancelled, both response bodies are discarded, and cleanup completes before the result returns. The default deadline is 1500 ms and results, including unavailable results, are cached for five minutes. Configure `registryProbeTimeoutMs` and `registryProbeCacheTtlMs` on `ui-plugin-manager`; `registryProbeEnabled: false` disables probing. If both requests fail or time out, the existing default remains selected. No IP-location service or Session content is involved. -**Add plugin** takes a package name with an optional version, a Git address, a tarball, or an absolute local path; the dialog says a package name is what follows `dsh plugin add` in a README. Enter in the package or custom-registry field submits the install request, but Enter used to confirm an input-method candidate does not. **Install guide and examples** under the field opens a guide that shows the three common forms with an example each; **Use example** drops one into the field. Git and local-path examples require the actual address or path; a hint appears while either example remains in the field. A trust warning stays above **Install**, including when the guide is collapsed. **Registry** beside it names the registry the install asks first and unfolds the choice: pnpm's own registry, named by the registry it actually names — the official npm registry, the mainland China mirror, or its host — which keeps the neutral default name until the Host reads it, is listed once, and carries its host unless that host is already its name; each mirror the Host configured (`pluginManager.registries`), npmmirror as the mainland China mirror; and a typed http(s) address. The options use an opaque selector panel, fill rows only on hover, and show hosts in tertiary text without parentheses. The custom-address area highlights on hover; clicking anywhere in it selects the custom registry and focuses its input, whose focus style matches the package field. Keyboard focus alone preserves the selected registry; activating the custom radio or editing the address selects the custom registry. Tab moves from the selected radio to the address, and Shift+Tab moves back; leaving either end closes the selector and returns focus to **Registry**. Switching to an offered registry retains the custom URL draft for switching back. Submitting an invalid custom address blocks installation, opens the registry selector, and focuses the address. Invalid package and custom-address fields keep a red error border and add the shared blue inset focus ring on focus. The options float over the dialog from the control, so unfolding them never lengthens the card, and the card's content scrolls when the guide makes it taller than the viewport. The initial choice follows the response comparison above; the choice is then remembered in this browser (`localStorage`) and starts the next dialog, and a remembered address the Host no longer offers stays as a typed one and remains the requested address when installation starts before the registry list returns. **Install** first asks the Host to read what the spec names (`pluginManager.inspect`) at the chosen registry: a name the list already shows, a name no registry has, a path without a package, a package without a bundle patch, or a spec pnpm would refuse comes back under the field as one sentence, with the spec kept for editing; when no registry could be reached, the sentence names every one asked. The install then starts at the registry that answered. An accepted spec opens the installing screen, which shows the package's name, one-liner, and version as the Host read them and folds pnpm's command and output behind **Show install details**. A finished install offers **Enable now**, which switches the new bundle on, closes the dialog, and scrolls the list to it; closing instead leaves it installed and off. While the Host moves on to another registry, the installing screen says which registry could not serve the package and which is asked now, and each pnpm run behind the details carries a badge naming its registry. A failed install says what went wrong in one line, as the Host attributed it — no registry could be reached, naming every one asked; the host a GitHub address or a tarball link is fetched from could not be reached, which no registry helps; the package was not found, the disk is full, the profile is not writable, pnpm blocked a build script — with pnpm's output behind the details and **Retry** at hand, and **Change registry** beside it when the Host laid the failure at the registry it asked, which returns to the spec with the registry options open; the Host has already put the profile files back. When pnpm blocked a dependency's install scripts, the failed screen lists the packages whose scripts wait for permission and offers **Allow these scripts and retry** in place of **Retry**; the Host saves the permission in the profile's `pnpm-workspace.yaml`, which a failed run leaves as pnpm wrote it, then runs pnpm again, and the installed screen names what was allowed. A successful installation does not certify that a module can activate. +**Add plugin** takes a package name with an optional version, a Git address, a tarball, or an absolute local path; the dialog says a package name is what follows `dsh plugin add` in a README. Enter in the package or custom-registry field submits the install request, but Enter used to confirm an input-method candidate does not. **Install guide and examples** under the field opens a guide that shows the package-name example; **Use example** drops it into the field. A field still holding the Git or local-path example keeps a hint asking for the actual value. A trust box above **Install** stays visible including when the guide is collapsed, and adds that installed plugins do not update automatically: upgrading means uninstalling and installing the new version, which later releases will keep improving. **Registry** beside it names the registry the install asks first and unfolds the choice: pnpm's own registry, named by the registry it actually names — the official npm registry, the mainland China mirror, or its host — which keeps the neutral default name until the Host reads it, is listed once, and carries its host unless that host is already its name; each mirror the Host configured (`pluginManager.registries`), npmmirror as the mainland China mirror; and a typed http(s) address. The options use an opaque selector panel, fill rows only on hover, and show hosts in tertiary text without parentheses. The custom-address area highlights on hover; clicking anywhere in it selects the custom registry and focuses its input, whose focus style matches the package field. Keyboard focus alone preserves the selected registry; activating the custom radio or editing the address selects the custom registry. Tab moves from the selected radio to the address, and Shift+Tab moves back; leaving either end closes the selector and returns focus to **Registry**. Switching to an offered registry retains the custom URL draft for switching back. Submitting an invalid custom address blocks installation, opens the registry selector, and focuses the address. Invalid package and custom-address fields keep a red error border and add the shared blue inset focus ring on focus. The options float over the dialog from the control, so unfolding them never lengthens the card, and the card's content scrolls when the guide makes it taller than the viewport. The initial choice follows the response comparison above; the choice is then remembered in this browser (`localStorage`) and starts the next dialog, and a remembered address the Host no longer offers stays as a typed one and remains the requested address when installation starts before the registry list returns. **Install** first asks the Host to read what the spec names (`pluginManager.inspect`) at the chosen registry: a name the list already shows, a name no registry has, a path without a package, a package without a bundle patch, or a spec pnpm would refuse comes back under the field as one sentence, with the spec kept for editing; when no registry could be reached, the sentence names every one asked. The install then starts at the registry that answered. An accepted spec opens the installing screen, which shows the package's name, one-liner, and version as the Host read them and folds pnpm's command and output behind **Show install details**. A finished install offers **Enable now**, which switches the new bundle on, closes the dialog, and scrolls the list to it; closing instead leaves it installed and off. While the Host moves on to another registry, the installing screen says which registry could not serve the package and which is asked now, and each pnpm run behind the details carries a badge naming its registry. A failed install says what went wrong in one line, as the Host attributed it — no registry could be reached, naming every one asked; the host a GitHub address or a tarball link is fetched from could not be reached, which no registry helps; the package was not found, the disk is full, the profile is not writable, pnpm blocked a build script — with pnpm's output behind the details and **Retry** at hand, and **Change registry** beside it when the Host laid the failure at the registry it asked, which returns to the spec with the registry options open; the Host has already put the profile files back. When pnpm blocked a dependency's install scripts, the failed screen lists the packages whose scripts wait for permission and offers **Allow these scripts and retry** in place of **Retry**; the Host saves the permission in the profile's `pnpm-workspace.yaml`, which a failed run leaves as pnpm wrote it, then runs pnpm again, and the installed screen names what was allowed. A successful installation does not certify that a module can activate. During preparation and download, **Cancel install** asks the Host to stop the run and waits for confirmation. Loading the bundle cannot be cancelled. Closing with ×, Escape, or the backdrop hides the dialog immediately and requests cancellation when possible. **View installation** reopens the same task with its output; another installation cannot start while its outcome is pending or unconfirmed. Confirmed cancellation returns to the spec and shows a toast; the manifest and lockfile are restored, while downloaded files can remain. A lost installation response triggers a request to recover the result; **Check installation status** and reconnect retry that request. If the Host has no active request, **Installation result unavailable** allows returning to editing after checking the plugin list. A cancellation sent before acceptance waits and retries automatically; a failed cancellation can be retried manually. Hidden tasks report their outcome through toasts without reopening the dialog. diff --git a/packages/client/ui-plugin-manager/README.zh.md b/packages/client/ui-plugin-manager/README.zh.md index 0d8e0f5afd..db44a60359 100644 --- a/packages/client/ui-plugin-manager/README.zh.md +++ b/packages/client/ui-plugin-manager/README.zh.md @@ -41,7 +41,7 @@ kind: "package-reference" Host 通过普通 fetch 代理向 `https://registry.npmjs.org/-/ping` 和 `https://registry.npmmirror.com/-/ping` 发送 GET 请求,以最先返回 2xx 的源为结果;重定向和失败不参与选择。随后取消另一条请求、丢弃两个响应体,并在清理完成后返回。默认时限为 1500 毫秒,包括不可用结果在内的探测结果均缓存五分钟。在 `ui-plugin-manager` 上配置 `registryProbeTimeoutMs` 和 `registryProbeCacheTtlMs`;`registryProbeEnabled: false` 关闭探测。两条请求都失败或超时时保留现有默认源。不使用 IP 地区服务,也不发送 Session 内容。 -**添加插件**接受包名(可带版本)、Git 地址、压缩包或本地绝对路径;对话框说明包名就是 README 里 `dsh plugin add` 后面的那一段。在包名或自定义安装源输入框中按 Enter 会提交安装请求,但用于确认输入法候选词的 Enter 不会触发安装。输入框下方的**插件安装引导和示例**展开一段引导,给出三种常见形式各一个示例;**填入示例**把示例填进输入框。Git 地址和本地路径示例需要替换为实际值;输入框中保留示例时会显示提醒。可信来源提示始终显示在**安装**按钮上方,引导收起后也可见。旁边的**安装源**写着安装首先询问的注册表,展开后可选:pnpm 自身的注册表,按它实际指向的源命名——npm 官方源、中国大陆镜像源,其余以主机名显示——Host 读不到时保留中性的默认名称,只列一次,选项标题带上主机名(主机名即名称时不重复显示);Host 配置的每个镜像(`pluginManager.registries`),npmmirror 显示为中国大陆镜像源;以及手动输入的 http(s) 地址。选项使用不透明选择面板,仅悬停行使用底色,主机名不加括号并使用三级文字色。自定义地址区域在悬停时高亮;点击其中任意位置会选中自定义源并聚焦输入框,焦点样式与包名输入框一致。仅通过键盘聚焦不会改变已选安装源;激活自定义单选项或编辑地址才会选中自定义源。Tab 从已选单选项移到地址框,Shift+Tab 反向返回;从任一端移出时关闭选择器,并将焦点交还**安装源**按钮。切到预设源时保留自定义地址草稿,切回后继续使用。提交无效的自定义地址时会阻止安装,展开安装源选择器并聚焦地址框。包名和自定义地址输入框无效时保留红色错误边框,聚焦时加上共享的蓝色内嵌焦点环。选项从控件上浮在对话框之上,展开不会拉长卡片;引导使卡片高过视口时,卡片内容可以滚动。初始选择遵循上文的响应比较规则;之后选择记在本浏览器(`localStorage`)里,下次打开对话框从它开始,Host 不再提供的已记住地址保留为手动输入的地址;在安装源列表返回前开始安装,也仍请求该地址。**安装**先让 Host 在所选注册表读出 spec 指向什么(`pluginManager.inspect`):列表中已有的名字、所有注册表都没有的名字、没有包的路径、没有组合包 patch 的包,或 pnpm 会拒绝的 spec,都以一句话回到输入框下方,spec 保留可继续编辑;所有注册表都连不上时,这句话列出问过的每一个。随后安装从作答的那个注册表开始。通过检查的 spec 打开安装中界面,展示 Host 读到的包名、一句话简介和版本,pnpm 的命令与输出折叠在**查看安装详情**之后。安装完成后提供**立即启用**:启用新组合包、关闭对话框并把列表滚动到它;直接关闭则让它保持已安装但关闭。Host 改问另一个注册表时,安装中界面会说明哪个源没能提供这个包、现在改问哪个,详情里每次 pnpm 运行都带一个写明所用源的标记。安装失败时按 Host 的归因用一行话说明原因——所有安装源都连不上,并列出问过的每一个;GitHub 地址或压缩包链接自身的主机连不上,换源无济于事;包不存在、磁盘已满、profile 不可写、pnpm 拦下了构建脚本——pnpm 输出在详情里,**重试**就在手边,Host 把失败归于所问注册表时还在旁边提供**更换安装源**,回到 spec 输入界面并展开安装源选项;Host 已经把 profile 文件放回原样。pnpm 拦下依赖的安装脚本时,失败界面列出等待允许的包,并以**允许这些脚本并重试**取代**重试**;Host 把授权写进 profile 的 `pnpm-workspace.yaml`(失败的运行保留 pnpm 写入的这个文件)再运行 pnpm,安装完成界面会说明允许了哪些脚本。安装成功不代表模块一定能够激活。 +**添加插件**接受包名(可带版本)、Git 地址、压缩包或本地绝对路径;对话框说明包名就是 README 里 `dsh plugin add` 后面的那一段。在包名或自定义安装源输入框中按 Enter 会提交安装请求,但用于确认输入法候选词的 Enter 不会触发安装。输入框下方的**插件安装引导和示例**展开一段引导,给出包名示例;**填入示例**把它填进输入框。输入框中仍是 Git 地址或本地路径示例时会显示提醒,要求替换为实际值。**安装**按钮上方的提示框在引导收起后也可见,并说明插件安装后暂不支持自动更新:升级需先卸载再安装新版,后续版本会持续改善升级体验。旁边的**安装源**写着安装首先询问的注册表,展开后可选:pnpm 自身的注册表,按它实际指向的源命名——npm 官方源、中国大陆镜像源,其余以主机名显示——Host 读不到时保留中性的默认名称,只列一次,选项标题带上主机名(主机名即名称时不重复显示);Host 配置的每个镜像(`pluginManager.registries`),npmmirror 显示为中国大陆镜像源;以及手动输入的 http(s) 地址。选项使用不透明选择面板,仅悬停行使用底色,主机名不加括号并使用三级文字色。自定义地址区域在悬停时高亮;点击其中任意位置会选中自定义源并聚焦输入框,焦点样式与包名输入框一致。仅通过键盘聚焦不会改变已选安装源;激活自定义单选项或编辑地址才会选中自定义源。Tab 从已选单选项移到地址框,Shift+Tab 反向返回;从任一端移出时关闭选择器,并将焦点交还**安装源**按钮。切到预设源时保留自定义地址草稿,切回后继续使用。提交无效的自定义地址时会阻止安装,展开安装源选择器并聚焦地址框。包名和自定义地址输入框无效时保留红色错误边框,聚焦时加上共享的蓝色内嵌焦点环。选项从控件上浮在对话框之上,展开不会拉长卡片;引导使卡片高过视口时,卡片内容可以滚动。初始选择遵循上文的响应比较规则;之后选择记在本浏览器(`localStorage`)里,下次打开对话框从它开始,Host 不再提供的已记住地址保留为手动输入的地址;在安装源列表返回前开始安装,也仍请求该地址。**安装**先让 Host 在所选注册表读出 spec 指向什么(`pluginManager.inspect`):列表中已有的名字、所有注册表都没有的名字、没有包的路径、没有组合包 patch 的包,或 pnpm 会拒绝的 spec,都以一句话回到输入框下方,spec 保留可继续编辑;所有注册表都连不上时,这句话列出问过的每一个。随后安装从作答的那个注册表开始。通过检查的 spec 打开安装中界面,展示 Host 读到的包名、一句话简介和版本,pnpm 的命令与输出折叠在**查看安装详情**之后。安装完成后提供**立即启用**:启用新组合包、关闭对话框并把列表滚动到它;直接关闭则让它保持已安装但关闭。Host 改问另一个注册表时,安装中界面会说明哪个源没能提供这个包、现在改问哪个,详情里每次 pnpm 运行都带一个写明所用源的标记。安装失败时按 Host 的归因用一行话说明原因——所有安装源都连不上,并列出问过的每一个;GitHub 地址或压缩包链接自身的主机连不上,换源无济于事;包不存在、磁盘已满、profile 不可写、pnpm 拦下了构建脚本——pnpm 输出在详情里,**重试**就在手边,Host 把失败归于所问注册表时还在旁边提供**更换安装源**,回到 spec 输入界面并展开安装源选项;Host 已经把 profile 文件放回原样。pnpm 拦下依赖的安装脚本时,失败界面列出等待允许的包,并以**允许这些脚本并重试**取代**重试**;Host 把授权写进 profile 的 `pnpm-workspace.yaml`(失败的运行保留 pnpm 写入的这个文件)再运行 pnpm,安装完成界面会说明允许了哪些脚本。安装成功不代表模块一定能够激活。 准备和下载期间,**取消安装**会请求 Host 停止运行并等待确认。加载组合包的阶段不可取消。点击 ×、按 Escape 或点击遮罩会立即隐藏对话框,并在可以取消时请求取消。**查看安装任务**会重新打开同一任务并保留输出;结果待定或尚未确认时不能发起另一项安装。确认取消后回到 spec 输入界面并显示 toast;manifest 与 lockfile 已恢复,已下载文件可能保留。安装响应丢失后会请求恢复结果;**核对安装状态**和重连会重试该请求。Host 已无活动请求时,**未能获取安装结果**允许检查插件列表后返回编辑。早于接收确认的取消请求会等待并自动重试;取消失败可手动重试。隐藏的任务通过 toast 通知结果,不会重新弹出对话框。 diff --git a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css index f2818ba19a..5c1240711b 100644 --- a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css +++ b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css @@ -567,20 +567,27 @@ display: flex; flex-direction: column; gap: 10px; - padding: 8px 14px 14px; + padding: 8px 14px; border: 0.5px solid var(--dsw-alias-border-l4); border-radius: var(--dsw-radius-lg); background: var(--dsw-alias-bg-layer-1); } .guideHint { - margin: 0; + margin: 6px 0 0; font-size: 12px; line-height: 18px; - color: var(--dsw-alias-label-secondary); + color: var(--dsw-alias-label-tertiary); } /* Trust guidance stays beside the install action, including while examples are collapsed. */ +.installSafetyText { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; +} + .installSafety { display: flex; align-items: flex-start; @@ -620,21 +627,6 @@ align-self: center; } -.guideIndex { - display: inline-flex; - flex: none; - align-items: center; - justify-content: center; - width: 20px; - height: 20px; - border-radius: 999px; - corner-shape: round; - background: var(--dsw-alias-bg-module-platform); - font-size: 11px; - font-weight: 500; - color: var(--dsw-alias-label-secondary); -} - .guideMain { display: flex; flex: 1; @@ -653,7 +645,7 @@ .guideExample { font-size: 12px; line-height: 20px; - color: var(--dsw-alias-label-secondary); + color: var(--dsw-alias-label-tertiary); overflow-wrap: anywhere; } diff --git a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx index 92d06906f0..f2e10bed0f 100644 --- a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx +++ b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx @@ -668,11 +668,9 @@ interface GuideExample { readonly hintKey: PluginManagerLocaleKey } -/** The spec forms the install guide shows, each with an example the person can drop into the field. */ +/** The spec form the install guide shows, with an example the person can drop into the field. */ const GUIDE_EXAMPLES = [ { key: 'id', titleKey: 'installGuideIdTitle', exampleKey: 'installGuideIdExample', hintKey: 'installGuideIdHint' }, - { key: 'git', titleKey: 'installGuideGitTitle', exampleKey: 'installGuideGitExample', hintKey: 'installGuideGitHint' }, - { key: 'path', titleKey: 'installGuidePathTitle', exampleKey: 'installGuidePathExample', hintKey: 'installGuidePathHint' }, ] as const satisfies readonly GuideExample[] /** The one-line reading of a classified pnpm failure. */ @@ -892,7 +890,10 @@ function InstallDialog({