From 0a07212452f6d55f5a27b455b2be3ec8be9be4df Mon Sep 17 00:00:00 2001 From: Cea <61349137+ceastld@users.noreply.github.com> Date: Tue, 15 Sep 2026 17:36:18 +0800 Subject: [PATCH] =?UTF-8?q?fix(dsh):=20=E8=A1=A5=E9=BD=90=E6=B3=A8?= =?UTF-8?q?=E5=85=A5=E6=B6=88=E6=81=AF=E7=9A=84=20id=20=E4=B8=8E=20role?= =?UTF-8?q?=EF=BC=8C=E9=81=BF=E5=85=8D=E4=BC=9A=E8=AF=9D=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E5=9B=9E=E6=94=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - plugins/quicker-dsh:agent.inject 改为完整 UserMessage(id + role) - tests:用本机 DSH snapshotSessionEvent 复现 lacks an identified message,并校验修复后可回放 - scripts/install-client.ps1:归属标记写入包内真实版本 - 文档:DSH 插件记为 0.2.1;旧会话不会自动修好 --- README.md | 2 +- .../\345\205\274\345\256\271\346\200\247.md" | 4 +- plugins/quicker-dsh/README.md | 4 +- plugins/quicker-dsh/index.js | 36 ++++++- plugins/quicker-dsh/package.json | 2 +- scripts/install-client.ps1 | 8 +- tests/dsh_inject_reload.mjs | 102 ++++++++++++++++++ tests/test_dsh_inject.py | 55 ++++++++++ tests/test_install_clients.py | 2 + 9 files changed, 205 insertions(+), 10 deletions(-) create mode 100644 tests/dsh_inject_reload.mjs create mode 100644 tests/test_dsh_inject.py diff --git a/README.md b/README.md index ede5589..c92311c 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ | Cursor | 本地插件;CLI 真实写动作通过 | Windows;技能、草稿创建/保存/预览 | | Claude Code | 原生插件,v0.2.1 | v0.2.0 公开市场安装和真实 MCP 连接通过;模型写动作待验收 | | VS Code / Copilot、Gemini CLI | 配置安装器 | 默认 Windows 用户配置;尚未完成各客户端写动作验收 | -| DeepSeek Harness | DSH bundle 插件 | Windows;安装包与转接已实现,真实 DSH 会话写动作待验收 | +| DeepSeek Harness | DSH bundle 插件,v0.2.1 | Windows;安装包与转接已实现;0.2.1 修复注入消息缺 id/role 导致历史无法加载。真实 DSH 会话写动作待验收 | 需要使用设置 → Agent 中带「启用 MCP」入口、并包含默认技能包发现修复的 Quicker 新构建。Release 支持已实现,待包含这些变更的正式版发布;已发布旧版没有该入口时仍不可用。本次已验证 Debug 的草稿编写与预览,Release 配置内核测试和正式前端构建通过,正式安装包端到端仍待验收,详见[兼容性说明](docs/兼容性.md)。 diff --git "a/docs/\345\205\274\345\256\271\346\200\247.md" "b/docs/\345\205\274\345\256\271\346\200\247.md" index e03c6f4..aaf6989 100644 --- "a/docs/\345\205\274\345\256\271\346\200\247.md" +++ "b/docs/\345\205\274\345\256\271\346\200\247.md" @@ -20,13 +20,13 @@ | Cursor CLI | 2026-09-07:2026.08.11-e8db854;v0.2.0 本地安装包技能加载、实时知识发现、4 步动作创建/保存/预览通过。IDE 3.7.12 的插件设置界面重载尚未单独操作验收 | | Claude Code | 2026-09-07:2.1.263;v0.2.0 插件与市场通过官方 validator,隔离安装及真实用户配置下的公开市场安装成功;识别 write-action 技能与 MCP,mcp list 连接真实 Quicker 成功。模型调用因未登录退出,未验证 Claude 模型写动作 | | VS Code / Gemini CLI | v0.2.0 默认用户配置安装/更新/卸载和隔离传输通过;尚未完成实际客户端写动作验收 | -| DeepSeek Harness | v0.2.0 bundle 清单、安装器复制、`dsh` Client 转接与隔离传输通过;尚未完成真实 DSH 会话写动作验收 | +| DeepSeek Harness | v0.2.1 补齐 `agent.inject` 的 `id` / `role`,避免会话回放报 `lacks an identified message`;bundle 清单、安装器复制、`dsh` Client 转接与隔离传输通过。0.2.0 已写入的旧会话不会自动修好。尚未完成真实 DSH 会话写动作验收 | ## 版本记录 每个平台使用自己的插件 manifest 版本。发布记录应包含平台版本、Quicker 版本或构建、操作系统、插件版本与实测结果。尚未实测的最低版本写为未知,不猜测兼容范围。 -当前 Claude Code 安装包版本为 `0.2.1`,补齐安装、认证、诊断与共存指引,MCP 运行配置和共享转接与 `0.2.0` 相同;其他安装包仍为 `0.2.0`。各平台分发方式见[客户端安装](客户端安装.md)。旧个人市场原型使用的 `+codex.<时间戳>` 后缀只用于刷新开发缓存,不代表跨平台发布版本。 +当前 Claude Code 安装包版本为 `0.2.1`,补齐安装、认证、诊断与共存指引,MCP 运行配置和共享转接与 `0.2.0` 相同。DeepSeek Harness 安装包为 `0.2.1`,只修注入消息标识。Codex / Cursor / 通用 MCP 仍为 `0.2.0`。各平台分发方式见[客户端安装](客户端安装.md)。旧个人市场原型使用的 `+codex.<时间戳>` 后缀只用于刷新开发缓存,不代表跨平台发布版本。 `0.1.1` 修复 `0.1.0` 安装成功但 MCP 无法启动的问题:当前 Codex legacy MCP 加载器不会展开参数中的 `${PLUGIN_ROOT}`,现改用 `cwd: "."` 与包内相对脚本路径。旧版用户按 README 的市场升级与重新安装步骤更新。隔离 HTTP fixture 用于传输回归,真实 Quicker 验收证据见下。 diff --git a/plugins/quicker-dsh/README.md b/plugins/quicker-dsh/README.md index ec02ae2..fa3898a 100644 --- a/plugins/quicker-dsh/README.md +++ b/plugins/quicker-dsh/README.md @@ -1,6 +1,8 @@ # Quicker DeepSeek Harness 插件 -版本:0.2.0。需要 Windows PowerShell 5.1、已安装的 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh` CLI 或桌面端),以及设置 → Agent 中带「启用 MCP」入口的本机 Quicker。此目录是完整 DSH bundle:写动作引导、stdio 转接,以及把 `@deepseek-ai/dsh-mcp-client` 接到本机 Quicker 的入口。运行时不依赖开发检出。 +版本:0.2.1。需要 Windows PowerShell 5.1、已安装的 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh` CLI 或桌面端),以及设置 → Agent 中带「启用 MCP」入口的本机 Quicker。此目录是完整 DSH bundle:写动作引导、stdio 转接,以及把 `@deepseek-ai/dsh-mcp-client` 接到本机 Quicker 的入口。运行时不依赖开发检出。 + +0.2.1 补齐 `agent.inject` 的 `id` / `role`。0.2.0 写入的会话在 DSH 回放时会报 `lacks an identified message` 并整段无法加载;本版只阻止新会话再被写坏,不能修好已经落盘的旧日志。 ## 安装 diff --git a/plugins/quicker-dsh/index.js b/plugins/quicker-dsh/index.js index a06b190..ed6776a 100644 --- a/plugins/quicker-dsh/index.js +++ b/plugins/quicker-dsh/index.js @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto' import { readFileSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' @@ -57,12 +58,39 @@ function authoringPreamble() { ].join('\n') } +function assertIdentifiedUserMessage(message) { + if (typeof message !== 'object' || message === null + || typeof message.id !== 'string' || message.id === '') { + throw new Error('dsh-plugin-quicker inject requires an identified message') + } + if (message.role !== 'user') { + throw new Error('dsh-plugin-quicker inject requires role user') + } + if (typeof message.source !== 'object' || message.source === null + || typeof message.source.kind !== 'string' || message.source.kind === '') { + throw new Error('dsh-plugin-quicker inject requires a message source') + } + if (!Array.isArray(message.content)) { + throw new Error('dsh-plugin-quicker inject requires content') + } +} + +export function createAuthoringGuideMessage(text) { + const message = { + id: randomUUID(), + role: 'user', + content: [{ type: 'text', text: text ?? authoringPreamble() }], + source: { kind: 'plugin', plugin: 'dsh-plugin-quicker' }, + } + assertIdentifiedUserMessage(message) + return message +} + function injectAuthoringGuide(agent) { + const message = createAuthoringGuideMessage() try { - agent.inject({ - content: [{ type: 'text', text: authoringPreamble() }], - source: { kind: 'plugin', plugin: 'dsh-plugin-quicker' }, - }) + // DSH persist + replay require a complete UserMessage (id + role). + agent.inject(message) } catch { // A disposed agent must not fail plugin activation. } diff --git a/plugins/quicker-dsh/package.json b/plugins/quicker-dsh/package.json index f05da96..18b4a90 100644 --- a/plugins/quicker-dsh/package.json +++ b/plugins/quicker-dsh/package.json @@ -1,6 +1,6 @@ { "name": "dsh-plugin-quicker", - "version": "0.2.0", + "version": "0.2.1", "description": "通过本机 Quicker MCP 编写、保存和预览自动化动作。", "type": "module", "main": "index.js", diff --git a/scripts/install-client.ps1 b/scripts/install-client.ps1 index 3d143c8..974aff3 100644 --- a/scripts/install-client.ps1 +++ b/scripts/install-client.ps1 @@ -161,7 +161,13 @@ $files = @{} foreach ($file in Get-ChildItem -LiteralPath $staging -Recurse -File -Force) { $files[$file.FullName.Substring($staging.Length + 1).Replace('\', '/')] = Get-PackageHash $file.FullName } -$newMarker = @{ owner = 'QuickerOrg/quicker-agent-integrations'; client = $Client; version = '0.2.0'; files = $files } +$packageVersion = '0.2.0' +$packageManifest = Join-Path $source 'package.json' +if (Test-Path -LiteralPath $packageManifest) { + $packageJson = Read-Json $packageManifest + if ($packageJson['version']) { $packageVersion = [string]$packageJson['version'] } +} +$newMarker = @{ owner = 'QuickerOrg/quicker-agent-integrations'; client = $Client; version = $packageVersion; files = $files } if ($server) { $newMarker['server'] = $server } Write-Json (Join-Path $staging '.quicker-managed.json') $newMarker $backup = $null diff --git a/tests/dsh_inject_reload.mjs b/tests/dsh_inject_reload.mjs new file mode 100644 index 0000000..714e0eb --- /dev/null +++ b/tests/dsh_inject_reload.mjs @@ -0,0 +1,102 @@ +import { readFileSync } from 'node:fs' +import { pathToFileURL } from 'node:url' +import { apply, createAuthoringGuideMessage } from '../plugins/quicker-dsh/index.js' + +const sessionEntry = process.env.DSH_SESSION_ENTRY +if (!sessionEntry) { + throw new Error('DSH_SESSION_ENTRY must point at @deepseek-ai/dsh-session/lib/index.js') +} +const { snapshotSessionEvent } = await import(pathToFileURL(sessionEntry).href) + +function asUserMessageEvent(data, seq = 1) { + return { + type: 'user/message', + seq, + time: Date.now(), + data, + surfaceOp: 'append', + } +} + +function expectThrow(label, fn, needle) { + try { + fn() + } catch (error) { + const text = String(error?.message ?? error) + if (!text.includes(needle)) { + throw new Error(`${label}: expected ${JSON.stringify(needle)}, got ${JSON.stringify(text)}`) + } + return text + } + throw new Error(`${label}: expected DSH to reject the event`) +} + +const broken = { + content: [{ type: 'text', text: 'Quicker MCP tools are registered as mcp__quicker__' }], + source: { kind: 'plugin', plugin: 'dsh-plugin-quicker' }, +} +const brokenError = expectThrow( + '0.2.0 inject payload', + () => snapshotSessionEvent(asUserMessageEvent(broken, 421)), + 'lacks an identified message', +) + +const fixed = createAuthoringGuideMessage('guide') +const adopted = snapshotSessionEvent(asUserMessageEvent(fixed, 1)) +if (adopted.data.id !== fixed.id || adopted.data.role !== 'user') { + throw new Error('fixed inject lost id or role after DSH adopt') +} + +const injected = [] +const handlers = {} +await apply({ + loader: { + create: async () => 'mcp-1', + store: { 'mcp-1': {} }, + remove: async () => {}, + }, + effect: async (fn) => { + await fn() + return async () => {} + }, + on: (event, handler) => { + handlers[event] = handler + }, +}) +handlers['agent/session-start']({ agent: { inject: (message) => injected.push(message) } }) +if (injected.length !== 1) { + throw new Error(`session-start should inject one guide, got ${injected.length}`) +} +snapshotSessionEvent(asUserMessageEvent(injected[0], 10)) + +const sessionPath = process.env.QUICKER_DSH_SESSION_JSONL +const evidence = [] +if (sessionPath) { + for (const line of readFileSync(sessionPath, 'utf8').split(/\r?\n/)) { + if (!line.includes('dsh-plugin-quicker')) continue + let event + try { + event = JSON.parse(line) + } catch { + continue + } + if (event?.type !== 'user/message') continue + const plugin = event.data?.source?.plugin + if (plugin !== 'dsh-plugin-quicker') continue + try { + snapshotSessionEvent(structuredClone(event)) + evidence.push({ seq: event.seq, ok: true }) + } catch (error) { + evidence.push({ seq: event.seq, ok: false, error: String(error?.message ?? error) }) + } + } +} + +process.stdout.write(JSON.stringify({ + ok: true, + brokenError, + fixedId: adopted.data.id, + injectedId: injected[0].id, + evidence, + plugin: new URL('../plugins/quicker-dsh/index.js', import.meta.url).href, +}) + '\n') diff --git a/tests/test_dsh_inject.py b/tests/test_dsh_inject.py new file mode 100644 index 0000000..7f23901 --- /dev/null +++ b/tests/test_dsh_inject.py @@ -0,0 +1,55 @@ +"""Replay the DSH session-load failure against the local DeepSeek Harness validator.""" + +from __future__ import annotations + +import json +import os +from pathlib import Path +import shutil +import subprocess +import unittest + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = Path(__file__).resolve().parent / "dsh_inject_reload.mjs" +EVIDENCE = Path(r"C:\Users\ldy\Downloads\session.v3.jsonl") + + +def resolve_dsh_node_modules() -> Path | None: + override = os.environ.get("DSH_NODE_MODULES") + if override: + root = Path(override) + if (root / "@deepseek-ai" / "dsh-session").is_dir(): + return root + cache = Path(os.environ.get("LOCALAPPDATA", "")) / "npm-cache" / "_npx" + if cache.is_dir(): + matches = sorted(cache.glob("*/node_modules/@deepseek-ai/dsh-session"), key=lambda p: p.stat().st_mtime, reverse=True) + if matches: + return matches[0].parents[1] + return None + + +@unittest.skipUnless(shutil.which("node"), "Node is required to load the DSH plugin") +class DshInjectReloadTests(unittest.TestCase): + def test_broken_inject_fails_dsh_reload_and_fixed_inject_passes(self) -> None: + node_modules = resolve_dsh_node_modules() + if node_modules is None: + self.skipTest("local @deepseek-ai/dsh-session was not found") + env = os.environ.copy() + env["DSH_SESSION_ENTRY"] = str(node_modules / "@deepseek-ai" / "dsh-session" / "lib" / "index.js") + if EVIDENCE.is_file(): + env["QUICKER_DSH_SESSION_JSONL"] = str(EVIDENCE) + result = subprocess.run( + ["node", str(SCRIPT)], + cwd=str(ROOT), + env=env, + capture_output=True, + text=True, + timeout=30, + check=False, + ) + self.assertEqual(result.returncode, 0, result.stderr + result.stdout) + payload = json.loads(result.stdout) + self.assertTrue(payload["ok"]) + self.assertIn("lacks an identified message", payload["brokenError"]) + self.assertTrue(payload["fixedId"]) + self.assertTrue(payload["injectedId"]) diff --git a/tests/test_install_clients.py b/tests/test_install_clients.py index 4c2f88c..6cbc867 100644 --- a/tests/test_install_clients.py +++ b/tests/test_install_clients.py @@ -32,6 +32,8 @@ def test_shared_sources_and_manifests(self): self.assertIn('agent/session-start', index) self.assertIn('loader.create', index) self.assertIn("'loader'", index) + self.assertIn('randomUUID', index) + self.assertIn("role: 'user'", index) self.assertNotIn("ctx.plugin('@deepseek-ai/dsh-mcp-client'", index)