Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 31 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,9 @@ jobs:
strategy:
fail-fast: false
matrix:
include:
- node: 22
host-ref: dsh-v0.1.2-rc.1
- node: 24
host-ref: dsh-v0.1.6-alpha.1
node: [22, 24]
runs-on: ubuntu-latest
timeout-minutes: 40
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262

Expand All @@ -28,11 +24,37 @@ jobs:
with:
version: 11.7.0

- name: Clone pinned DSH host
- name: Install plugin dependencies
run: pnpm install --frozen-lockfile

- name: Check
run: pnpm run check

smoke:
needs: check
runs-on: ubuntu-latest
timeout-minutes: 40
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 24

- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1
with:
version: 11.7.0

- name: Resolve latest DSH tag
id: host
run: echo "ref=$(node scripts/resolve-dsh-tag.mjs)" >> "$GITHUB_OUTPUT"

- name: Clone latest DSH host
run: |
DSH_ROOT="$GITHUB_WORKSPACE/../../.dsh/source/current"
mkdir -p "$(dirname "$DSH_ROOT")"
git clone --depth 1 --branch "${{ matrix.host-ref }}" https://github.com/deepseek-ai/deepseek-harness.git "$DSH_ROOT"
git clone --depth 1 --branch "${{ steps.host.outputs.ref }}" https://github.com/deepseek-ai/deepseek-harness.git "$DSH_ROOT"
echo "Resolved DSH host ${{ steps.host.outputs.ref }}"

- name: Install and build DSH host
working-directory: ${{ github.workspace }}/../../.dsh/source/current
Expand All @@ -43,9 +65,6 @@ jobs:
- name: Install plugin dependencies
run: pnpm install --frozen-lockfile

- name: Check
run: pnpm run check

- name: Browser annotation interactions
run: |
node test/overlay-browser.mjs
Expand All @@ -58,7 +77,7 @@ jobs:
run: node scripts/verify-pack.mjs

- name: Smoke real DSH Web
run: node scripts/e2e.mjs --install tarball --tarball "${{ steps.pack.outputs.tarball }}" --tarball-sha256 "${{ steps.pack.outputs.sha256 }}" --port 3190 ${{ matrix.node == 24 && '--sidebar' || '' }}
run: node scripts/e2e.mjs --install tarball --tarball "${{ steps.pack.outputs.tarball }}" --tarball-sha256 "${{ steps.pack.outputs.sha256 }}" --port 3190 --sidebar
env:
DSH_ROOT: ${{ github.workspace }}/../../.dsh/source/current
DSH_BIN: ${{ github.workspace }}/../../.dsh/source/current/apps/cli/lib/bin.js
10 changes: 7 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ jobs:
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
PRERELEASE: ${{ github.event.release.prerelease }}
HOST_REF: dsh-v0.1.6-alpha.1

steps:
# actions/checkout@v4
Expand Down Expand Up @@ -86,13 +85,18 @@ jobs:
fi
fi

- name: Clone selected DSH host
- name: Resolve latest DSH tag
id: host
run: echo "ref=$(node scripts/resolve-dsh-tag.mjs)" >> "$GITHUB_OUTPUT"

- name: Clone latest DSH host
shell: bash
run: |
DSH_ROOT="$GITHUB_WORKSPACE/../../.dsh/source/current"
mkdir -p "$(dirname "$DSH_ROOT")"
git clone --depth 1 --branch "$HOST_REF" https://github.com/deepseek-ai/deepseek-harness.git "$DSH_ROOT"
git clone --depth 1 --branch "${{ steps.host.outputs.ref }}" https://github.com/deepseek-ai/deepseek-harness.git "$DSH_ROOT"
echo "DSH_ROOT=$DSH_ROOT" >> "$GITHUB_ENV"
echo "Resolved DSH host ${{ steps.host.outputs.ref }}"

- name: Install and build DSH host
working-directory: ${{ github.workspace }}/../../.dsh/source/current
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
- **第三种界面语言不再把批注界面退回中文**:本插件只有 zh / en 两套文案,而 DSH 的 locale 服务不止这两种。节点选定 `ru`(或任何 zh/en 之外的语言)时,`setLang` 以前把一切非 zh/en 都当成 zh,于是整套批注 UI —— 工具条、编辑窗口、提示、批注协议块 —— 全变成中文。现在 `zh`/`en`/`ru` 原样采用,其余一律英文;`test/locale-fallback.test.mjs` 固定这条规则(fr / de / zh-Hans 走英文)。
- **恢复 DSH 0.1.1(textarea 输入区)上的 Enter 批注拼稿**:1.4.6 适配 0.1.2 的 `div[data-composer-input]` 输入区时,移除了 Enter keydown 门控里的 textarea 分支,旧核心上 Enter 不再拼入批注块(发送按钮路径不受影响,但 Enter 发送会静默丢弃待发送批注)。门控现并行接受 `div[data-composer-input]` 与 textarea 两条分支;0.1.1-rc.2 实测恢复,div 分支行为不变。

### 工程
- CI 与发布验收不再把宿主钉在过期标签上。单元检查(Node 22 / 24)只跑 `pnpm run check`,不克隆宿主;冒烟和发布前的真实 Web 验收改为 `scripts/resolve-dsh-tag.mjs` 在运行时选取最高的 `dsh-v*` 标签。冒烟探针按文档相对路径解析插件地址(0.2.0 起是 `plugins/??…`),不再把相对路径粘到端口后面。

## [1.4.11-preview.1] - 2026-09-15

- 更新 preview 预发布验收宿主至 DSH 0.1.6-alpha.1;发布任务验证同一安装包后发布至 npm preview。
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# dsh-annotation

Verified hosts: DSH 0.1.6-alpha.1 and the 0.1.2-rc.1 minimum. These are core versions, not Desktop shell versions.
CI runs unit checks on Node 22 and 24 without cloning a DSH host. Browser smoke and release acceptance resolve the highest `dsh-v*` tag on deepseek-ai/deepseek-harness at run time. These are core versions, not Desktop shell versions.

Sidebar file annotations (DSH 0.1.6-alpha.1): select plain text, Markdown, or code in a workspace file preview and use the same annotate, save, and Enter-to-send flow. The editor and model-facing quote include the file path. After reloading and reopening the original file, its markers are restored; identical passages in different files remain separate. PDF, images, and HTML inside iframes are not covered.

Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# dsh-annotation

验证宿主:DSH 0.1.6-alpha.1;最低支持线仍验证 DSH 0.1.2-rc.1。版本号指 DSH 内核,不是 Desktop 外壳。
CI 在 Node 22 与 24 上跑单元检查,不克隆 DSH 宿主。浏览器冒烟和发布验收在运行时解析 deepseek-ai/deepseek-harness 上最高的 `dsh-v*` 标签。版本号指 DSH 内核,不是 Desktop 外壳。

侧边栏文件批注(DSH 0.1.6-alpha.1):在工作区文件预览中选中文本、Markdown 或代码正文,即可沿用同一套批注、保存和回车发送流程。编辑器和发给模型的引用包含文件路径;刷新并重新打开原文件后恢复标记,同文不同文件可分别批注。当前不包含 PDF、图片或 iframe 内 HTML。

Expand Down
29 changes: 21 additions & 8 deletions scripts/e2e.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { createServer } from 'node:net'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { pluginClientUrl } from './plugin-client-url.mjs'

const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const DSH_ROOT = process.env.DSH_ROOT ?? resolve(process.env.HOME ?? '', '.dsh/source/current')
Expand Down Expand Up @@ -102,7 +103,7 @@ try {
}
})
const clientUrl = state.clientUrl ?? '/plugins/@changfenhuang/dsh-annotation/client.js'
const response = await fetch(`http://127.0.0.1:${PORT}${clientUrl}`)
const response = await fetch(pluginClientUrl(readyUrl, clientUrl))
if (!response.ok) fail(`Annotation bundle 返回 ${response.status}`)
if (!state.mounted) fail('Annotation bundle 已加载,但页面没有挂载批注入口')
if (pageErrors.length > 0) fail(`页面异常: ${pageErrors.slice(0, 3).join(' | ')}`)
Expand Down Expand Up @@ -159,13 +160,25 @@ try {
const later = page.getByRole('button', { name: '稍后配置', exact: true })
if (await later.isVisible()) await later.click()
async function openFile() {
const expand = page.getByRole('button', { name: '打开右侧边栏', exact: true })
if (await expand.isVisible()) await expand.click()
if (!await page.locator('[data-sidebar-right-guide-entry="files"]').isVisible()) {
await page.getByRole('button', { name: '新标签页', exact: true }).click()
// Reload can leave the session list focused; the right sidebar belongs to the open session.
const session = page.getByText('文件批注验收', { exact: true }).first()
if (await session.isVisible()) await session.click()
const expand = page.locator('[data-sidebar-right-expand]')
const opened = page.locator('[data-sidebar-right-open]')
// The header control mounts after the session view. A collapsed panel stays
// in the DOM with visibility:hidden, so don't click that copy first.
await expand.or(opened).first().waitFor({ state: 'attached', timeout: 15000 })
if ((await opened.count()) === 0) {
if (await expand.isVisible()) await expand.click()
else await expand.first().evaluate(button => button.click())
}
await page.getByText('工作区文件', { exact: true }).click()
await page.locator('[data-files-entry="file"][data-files-path$="/README.zh-CN.md"] button').click()
await opened.first().waitFor({ state: 'attached' })
const files = page.locator('[data-sidebar-right-guide-entry="files"]')
if (!(await files.isVisible())) {
await page.locator('[data-dockkit-add-tab]').first().click()
}
await files.click()
await page.locator('[data-files-entry="file"][data-files-path$="/README.zh-CN.md"] > button').click()
await page.locator('[data-document-markdown] p').first().waitFor({ state: 'visible' })
}
await openFile()
Expand All @@ -188,7 +201,7 @@ try {
await page.locator('.dsh-ann-num').waitFor({ state: 'visible' })
await page.locator('.dsh-ann-num').click()
if (await page.locator('.dsh-ann-input').inputValue() !== '请解释这个文件段落') fail('文件批注刷新恢复失败')
await page.locator('.dsh-ann-card-head button').click()
await page.locator('.dsh-ann-card-head > .dsh-ann-icon').click()
if (pageErrors.length > 0) fail(`侧边栏异常: ${pageErrors.join(' | ')}`)
console.log('PASS 真实侧边栏打开工作区文件、选区、来源路径、保存、刷新后重新打开定位、重新编辑')
}
Expand Down
8 changes: 8 additions & 0 deletions scripts/plugin-client-url.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// 把 __DSH_BOOT__ 里的插件地址解析成可 fetch 的绝对 URL。
// 0.2.0 起 boot 条目是文档相对路径(plugins/??<id>/client.js&rev=…),
// 直接接到 origin 后面会变成非法端口。旧宿主的 /plugins/<id>/client.js 仍然适用。

export function pluginClientUrl(pageUrl, clientUrl) {
const origin = new URL(pageUrl).origin
return new URL(clientUrl, `${origin}/`).href
}
85 changes: 85 additions & 0 deletions scripts/resolve-dsh-tag.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/usr/bin/env node
// 从 deepseek-ai/deepseek-harness 的 dsh-v* 标签里选出最高语义化版本。
// CI 冒烟和发布验收都走这里,避免把宿主钉死在过期 tag 上。

import { execFileSync } from 'node:child_process'
import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'

const TAG = /^dsh-v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/
const REMOTE = 'https://github.com/deepseek-ai/deepseek-harness.git'

export function parseDshTag(name) {
const match = TAG.exec(name)
if (match === null) return null
return {
name,
major: Number(match[1]),
minor: Number(match[2]),
patch: Number(match[3]),
pre: match[4] === undefined ? null : match[4].split('.'),
}
}

function compareIdentifiers(left, right) {
const leftNum = /^[0-9]+$/.test(left)
const rightNum = /^[0-9]+$/.test(right)
if (leftNum && rightNum) return Number(left) - Number(right)
if (leftNum) return -1
if (rightNum) return 1
return left < right ? -1 : left > right ? 1 : 0
}

export function compareDshTags(leftName, rightName) {
const left = parseDshTag(leftName)
const right = parseDshTag(rightName)
if (left === null || right === null) {
throw new Error(`not a dsh tag: ${left === null ? leftName : rightName}`)
}
for (const key of ['major', 'minor', 'patch']) {
if (left[key] !== right[key]) return left[key] - right[key]
}
if (left.pre === null && right.pre === null) return 0
if (left.pre === null) return 1
if (right.pre === null) return -1
const length = Math.max(left.pre.length, right.pre.length)
for (let index = 0; index < length; index++) {
if (index >= left.pre.length) return -1
if (index >= right.pre.length) return 1
const order = compareIdentifiers(left.pre[index], right.pre[index])
if (order !== 0) return order
}
return 0
}

export function latestDshTag(names) {
const tags = names.filter(name => parseDshTag(name) !== null)
if (tags.length === 0) return null
return tags.reduce((best, name) => compareDshTags(name, best) > 0 ? name : best)
}

export function tagNamesFromLsRemote(output) {
const names = []
for (const line of output.split('\n')) {
const ref = line.split('\t')[1]
if (ref === undefined || !ref.startsWith('refs/tags/') || ref.endsWith('^{}')) continue
names.push(ref.slice('refs/tags/'.length))
}
return names
}

function invokedAsCli() {
const entry = process.argv[1]
if (entry === undefined) return false
return import.meta.url === pathToFileURL(resolve(entry)).href
}

if (invokedAsCli()) {
const output = execFileSync('git', ['ls-remote', '--tags', REMOTE], { encoding: 'utf8' })
const latest = latestDshTag(tagNamesFromLsRemote(output))
if (latest === null) {
console.error('deepseek-harness 上没有 dsh-v* 标签')
process.exit(1)
}
console.log(latest)
}
5 changes: 2 additions & 3 deletions test/layout-overlay.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ test('编辑窗口:标题右侧可删除该批注,且不再有取消按钮',
assert.match(editor, /removeQuote\(editingId\)/, '删除复用待发送批注移除逻辑')
assert.doesNotMatch(editor, /dsh-ann-cancel/, '编辑窗口不再有取消按钮')
assert.doesNotMatch(source, /dsh-ann-cancel/, '取消按钮的样式一并移除')
})

// Source slice for one `function name(...) { ... }` by brace matching.
function tryExtractFunction (name) {
const start = source.indexOf(`function ${name}(`)
Expand Down Expand Up @@ -231,6 +233,3 @@ test('编号胶囊的顶部下限抬到会话列上沿 + 4px,原文已滚入
assert.match(build, /anchor\.top >= chipFloor && chipTop < chipFloor/,
'clamp only while the passage itself starts below the floor — passages scrolled under the header keep the skip rule (#65)')
})


})
23 changes: 23 additions & 0 deletions test/plugin-client-url.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { pluginClientUrl } from '../scripts/plugin-client-url.mjs'

const page = 'http://127.0.0.1:3190/?token=abc'

test('文档相对的 combo 路由解析到 /plugins/,不把路径粘进端口', () => {
assert.equal(
pluginClientUrl(page, 'plugins/??@changfenhuang/dsh-annotation/client.js&rev=99'),
'http://127.0.0.1:3190/plugins/??@changfenhuang/dsh-annotation/client.js&rev=99',
)
})

test('旧宿主的绝对路径和已经绝对的 URL 保持原样', () => {
assert.equal(
pluginClientUrl(page, '/plugins/@changfenhuang/dsh-annotation/client.js'),
'http://127.0.0.1:3190/plugins/@changfenhuang/dsh-annotation/client.js',
)
assert.equal(
pluginClientUrl(page, 'http://127.0.0.1:3190/plugins/@changfenhuang/dsh-annotation/client.js'),
'http://127.0.0.1:3190/plugins/@changfenhuang/dsh-annotation/client.js',
)
})
30 changes: 30 additions & 0 deletions test/resolve-dsh-tag.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { compareDshTags, latestDshTag, tagNamesFromLsRemote } from '../scripts/resolve-dsh-tag.mjs'

test('语义化比较:数字补丁、预发布顺序、正式版高于预发布', () => {
assert.ok(compareDshTags('dsh-v0.1.10', 'dsh-v0.1.9') > 0)
assert.ok(compareDshTags('dsh-v0.1.2-rc.1', 'dsh-v0.1.2-alpha.5') > 0)
assert.ok(compareDshTags('dsh-v0.1.0-rc.10', 'dsh-v0.1.0-rc.2') > 0)
assert.ok(compareDshTags('dsh-v0.2.0', 'dsh-v0.2.0-rc.2') > 0)
assert.ok(compareDshTags('dsh-v0.2.0-rc.2', 'dsh-v0.1.7-rc.2') > 0)
assert.equal(compareDshTags('dsh-v0.1.6-alpha.1', 'dsh-v0.1.6-alpha.1'), 0)
})

test('最高标签忽略非 dsh-v 引用和注解标签剥除行', () => {
const names = tagNamesFromLsRemote([
'abc\trefs/tags/dsh-v0.1.2-rc.1',
'def\trefs/tags/dsh-v0.2.0-rc.2',
'def\trefs/tags/dsh-v0.2.0-rc.2^{}',
'ghi\trefs/tags/dsh-v0.1.7-rc.2',
'jkl\trefs/tags/not-a-release',
].join('\n'))
assert.deepEqual(names, [
'dsh-v0.1.2-rc.1',
'dsh-v0.2.0-rc.2',
'dsh-v0.1.7-rc.2',
'not-a-release',
])
assert.equal(latestDshTag(names), 'dsh-v0.2.0-rc.2')
assert.equal(latestDshTag(['nightly', 'v1.0.0']), null)
})
Loading