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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)。

Expand Down
4 changes: 2 additions & 2 deletions docs/兼容性.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 验收证据见下。

Expand Down
4 changes: 3 additions & 1 deletion plugins/quicker-dsh/README.md
Original file line number Diff line number Diff line change
@@ -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` 并整段无法加载;本版只阻止新会话再被写坏,不能修好已经落盘的旧日志。

## 安装

Expand Down
36 changes: 32 additions & 4 deletions plugins/quicker-dsh/index.js
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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.
}
Expand Down
2 changes: 1 addition & 1 deletion plugins/quicker-dsh/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dsh-plugin-quicker",
"version": "0.2.0",
"version": "0.2.1",
"description": "通过本机 Quicker MCP 编写、保存和预览自动化动作。",
"type": "module",
"main": "index.js",
Expand Down
8 changes: 7 additions & 1 deletion scripts/install-client.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
102 changes: 102 additions & 0 deletions tests/dsh_inject_reload.mjs
Original file line number Diff line number Diff line change
@@ -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__<toolName>' }],
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')
55 changes: 55 additions & 0 deletions tests/test_dsh_inject.py
Original file line number Diff line number Diff line change
@@ -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"])
2 changes: 2 additions & 0 deletions tests/test_install_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
Loading