diff --git a/PRIVACY.md b/PRIVACY.md index e4ddde2..581afa4 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -1,36 +1,48 @@ -# Zero-content privacy contract +# Task-agnostic feedback privacy contract -Plugin Lab v2 的隐私目标不是“先收集再脱敏”,而是让私密内容在能力层面无法进入反馈数据流。 +Plugin Lab v3 允许上传一个经过用户确认的体验大类,但不允许上传模型生成的自由文本摘要。目标是让开发者获得可聚合、可行动的方向,同时让当前任务、会话和日志在协议层面没有入口。 ## 不变量 -1. **内容非干涉**:在插件、健康状态、用户选择和单次事件 ID 相同时,替换任意 Prompt、回复、日志、异常、文件、路径或 Secret,序列化反馈字节必须保持不变。 -2. **有限字母表**:网络包只允许协议中列出的标量和公开插件坐标;未知字段直接拒绝。 -3. **宿主真源**:探活只读取 DSH Host 的 Loader/Fiber 状态,不调用目标插件,也不进行文件、网络或数据库 I/O。 -4. **Agent 零内容**:Agent 工具输入为空,输出只含 `health`、固定 `experience=unknown` 和确认布尔值。 -5. **主观状态不臆测**:只有用户点击产生 `good/mixed/bad`;健康状态不能自动转换为体验评价。 -6. **本地与上传解耦**:探活和确认体验都不产生网络请求;`/omdsh-join` 是独立同意动作。 -7. **无稳定身份**:没有用户、账号、设备、安装、Session 或跨报告匿名 ID。 -8. **无日志派生**:不接受日志摘要、错误类型、错误码、堆栈、指纹或哈希。 -9. **固定渲染**:Agent 状态和反馈字段使用有限枚举;服务端错误不回显原始输入。 -10. **聚合后通知**:GitHub 只接收达到阈值后的聚合枚举,不接收单条报告。 +1. **封闭影响范围**:私密内容最多影响 Agent 建议的一个大类;一旦用户确认大类,反馈序列化只由公开插件坐标、有限枚举和随机单次 ID 决定。 +2. **有限字母表**:网络包只允许协议列出的字段;未知字段直接拒绝,不做“接收后脱敏”。 +3. **没有自由摘要字段**:`summary`、`note`、`reason`、`message` 等字段全部拒绝。可读摘要只由固定模板渲染。 +4. **宿主真源**:探活只读取 DSH Host 的 Loader/Fiber 状态,不调用目标插件,也不进行文件、网络或数据库 I/O。 +5. **Agent 窄工具**:探活工具输入为空;预览工具只接受 `experience` 和 `category` 两个枚举,且没有存储或网络副作用。 +6. **主观状态不臆测**:只有用户确认才能产生 `source=user_confirmed` 的本地记录;健康状态不能自动转换为体验评价。 +7. **预览后提交**:体验和大类先写入本地草稿并显示完整预览;`/omdsh-join` 是独立的逐条发送动作。 +8. **无稳定身份**:没有用户、账号、设备、安装、Session 或跨报告匿名 ID。 +9. **无日志派生详情**:不接受日志摘要、错误类型、错误码、堆栈、指纹或哈希;崩溃只能成为 `health=error`。 +10. **聚合后通知**:GitHub 只接收达到阈值后的大类、状态、体验和报告数,不接收单条报告。 + +## 允许的大类 + +```text +installation | startup | invocation | compatibility +reliability | performance | result_quality | general +``` + +这些大类描述插件体验阶段,不描述用户正在执行什么任务。Agent 可以基于当前交互建议一个大类,但用户必须在预览中检查、修改或取消。 ## 必须通过的攻击测试 -- 把邮箱、Token、绝对路径、聊天文本分别放入会话和异常;反馈包不得变化或出现 canary。 -- 目标插件处于同一 Fiber 状态时,替换所有私密内容,反馈序列化结果必须一致。 -- 向 Agent 工具加入 `reason/log/prompt` 参数必须被拒绝。 -- 向上传包加入 `note/log/stack/signals/environment/participantId/occurredAt` 必须拒绝整个请求。 +- 向 Agent 预览工具加入 `summary/task/log/prompt` 参数必须拒绝整个调用。 +- 向上传包加入 `summary/note/log/stack/signals/environment/participantId/occurredAt` 必须拒绝整个请求。 +- 把邮箱、Token、绝对路径、聊天文本放入会话和异常;本地包与固定摘要不得出现 canary。 +- 相同公开插件、健康状态、体验和大类下,更换私密任务内容不能改变序列化包。 - `verbose/debug/include=logs` 不能改变探活结果。 -- 未配置 Loader、状态缺失或序列化失败只能得到 `unknown`,不能输出异常详情。 -- 用户只点击探活或体验选择时,抓包必须没有反馈请求。 -- 请求体超过 1 KiB 必须在 JSON 解析前拒绝。 +- 未配置 Loader、状态缺失或内部异常只能得到 `unknown`,不能输出异常详情。 +- 用户只点击探活、体验或大类时,抓包必须没有反馈请求。 +- 用户必须能在发送前看到插件、版本、状态、体验、大类和单次报告 ID。 +- 请求体超过 1 KiB 必须在接受字段前拒绝。 - 服务端 4xx/5xx 响应不得包含 canary、异常、路径或请求正文。 - 未达到聚合阈值时不能创建 GitHub Issue。 ## 明确限制 +- 受控大类仍是一条信息,例如 `result_quality` 会说明用户认为“结果质量”值得反馈;因此必须预览并由用户确认。 +- Agent 已经处在正常会话链路时可能看得到当前任务;本协议保证的是这些内容不能进入反馈工具参数或上传载荷,而不是声称 Agent 从未见过会话。 - `health=ok` 只表示宿主认为插件可运行,不代表用户满意,也不证明业务结果正确。 - 没有稳定身份后,聚合只能统计报告数,不能准确统计独立用户。 -- IP、连接时间等传输元数据仍会被网络基础设施瞬时观察;部署者必须通过日志配置和保留策略降低风险,不能据此宣称绝对匿名。 -- 旧版本可能已经创建本地崩溃或安装身份文件。v2 不读取、不补传,也不自动删除这些历史文件。 +- IP、连接时间等传输元数据仍会被网络基础设施瞬时观察;部署者必须通过日志配置和保留策略降低风险,不能宣称绝对匿名。 +- v3 不读取、不补传,也不自动删除历史 v1/v2 文件或表。 diff --git a/README.md b/README.md index 1c69f95..d51e752 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,43 @@ # Oh My DSH Plugin Lab -Plugin Lab 0.3 是 DeepSeek Harness rc.6 的零内容插件探活与体验反馈闭环。 +Plugin Lab 0.4 是面向 DeepSeek Harness rc.6 的隐私优先插件反馈闭环:Agent 可以探活并准备脱敏大类摘要,用户在上传前看到完整预览并逐次确认,后端只把达到阈值的聚合信号提交到 GitHub。 -它把两件事严格分开: +## 现在的体验 -- DSH Host 可以判断插件当前是否正常运行; -- “好用 / 一般 / 不好用”只能由用户确认,Agent 不得读取会话或日志代替用户推断。 - -## 体验流程 - -1. `/omdsh-start #` 选择本次试用的公开插件。 -2. 用户随时点击 Composer 左侧的“插件探活”,本地查看 `OK / 暂不可用 / 异常 / 未知`。 -3. Agent 可以调用零参数工具 `omdsh_analyze_plugin_experience`,但只会得到运行状态和 `experience=unknown`。 -4. 最新回复旁出现体验卡,由用户选择“好用 / 一般 / 不好用”。这一步只保存在本机。 -5. 用户再次点击加入跟进或运行 `/omdsh-join latest`,才会发送屏幕上已经说明的有限字段。 -6. 后端按插件、版本和状态聚合;达到阈值后创建 GitHub Issue,并通过回执邀请用户复测。 +1. `/omdsh-start #` 选择公开的插件坐标。 +2. 用户或 Agent 运行探活,只得到 `ok / unavailable / error / unknown`。 +3. Agent 可以建议“安装、启动、调用、兼容性、稳定性、性能、结果质量、整体体验”之一,但反馈工具不接受任务文本。 +4. 用户选择“好用 / 一般 / 不好用”和一个大类。 +5. Plugin Lab 在本地生成固定模板,并展示即将上传的全部可读信息;此时没有网络请求。 +6. 用户点击“确认并提交”或运行 `/omdsh-join latest` 后,有限枚举包才会发送。 +7. 后端按插件、版本、状态、体验和大类聚合;同类报告达到阈值后创建 GitHub Issue,并通过回执邀请复测。 ```mermaid flowchart LR - A["DSH Host Loader/Fiber"] --> B["本地状态枚举"] - B --> C["一键探活"] - B --> D["Agent 零参数工具"] - D --> E["experience = unknown"] - E --> F["用户确认好用 / 一般 / 不好用"] - F --> G["单独同意发送"] - G --> H["后端有限枚举聚合"] - H -->|"达到阈值"| I["GitHub 聚合 Issue"] - I --> J["修复与复测回执"] + A["插件试用"] --> B["Host 无日志探活"] + B --> C["Agent 建议有限大类"] + C --> D["用户选择体验和大类"] + D --> E["本地固定模板预览"] + E -->|"用户再次确认"| F["发送有限枚举"] + F --> G["后端聚合"] + G -->|"达到阈值"| H["GitHub 聚合 Issue"] + H --> I["修复与复测回执"] ``` +Agent 可能已经拥有当前任务的正常会话上下文,但 Plugin Lab 的工具参数和上传协议都没有承载这些内容的字段。Agent 只能从封闭枚举中提出建议;最终分类和主观体验由用户确认。 + ## 安装开发版本 ```sh pnpm install pnpm pack:release -dsh plugin --profile web add ./oh-my-dsh-plugin-lab-0.3.0.tgz +dsh plugin --profile web add ./oh-my-dsh-plugin-lab-0.4.0.tgz dsh --profile web ``` Plugin Lab 是标准 DSH Bundle:`package.json` 通过 `dsh.bundle.patch` 声明 Host 插件,通过 `dsh.client` 提供 Web 探活、结果卡和收件箱。 -版本 `0.3.0` 的 Peer 契约从 DSH `0.1.0-rc.6` 起。完整测试会执行真实的 rc.6 打包、安装、Host/Web 启动、Client Loader 注册和卸载。 +版本 `0.4.0` 的 Peer 契约从 DSH `0.1.0-rc.6` 起。完整测试会执行真实的 rc.6 打包、安装、Host/Web 启动、Client Loader 注册和卸载。 ## 命令与 Agent 工具 @@ -48,60 +45,69 @@ Plugin Lab 是标准 DSH Bundle:`package.json` 通过 `dsh.bundle.patch` 声 |---|---| | `/omdsh-start [#version]` | 开始单插件试用;不接受任务标签或备注 | | `/omdsh-probe` | 本地读取当前目标的 Host 生命周期状态 | -| `/omdsh-result ` | 用户确认体验,只保存本机 | -| `/omdsh-join ` | 单独同意发送有限字段 | +| `/omdsh-result ` | 用户确认体验和大类,生成本地预览 | +| `/omdsh-join ` | 用户确认发送已经展示的有限字段 | | `/omdsh-inbox [--peek]` | 查看聚合问题、修复版本与复测邀请 | | `/omdsh-retest [#version]` | 从单条回执开始复测 | | `/omdsh-status` | 查看本地试用、待发送和未读状态 | | `/omdsh-privacy` | 显示完整隐私边界 | -| `omdsh_analyze_plugin_experience({})` | Agent 查询运行状态;输入必须为空 | +| `omdsh_analyze_plugin_experience({})` | Agent 查询状态和允许的大类;输入必须为空 | +| `omdsh_preview_plugin_feedback({experience, category})` | 生成无副作用固定模板预览;不保存、不上传 | + +`verdict` 只能是 `good / mixed / bad`。`category` 只能是: + +```text +installation | startup | invocation | compatibility +reliability | performance | result_quality | general +``` -## 精确数据边界 +## 精确上传协议 唯一允许上传的数据包为: ```json { - "schemaVersion": 2, + "schemaVersion": 3, "type": "feedback.signal", "eventId": "随机单次 UUID", "plugin": { "moduleName": "marketplace-public-id", "version": "1.2.3" }, - "health": "ok", - "experience": "good", + "health": "error", + "experience": "bad", + "category": "reliability", "source": "user_confirmed" } ``` -复测时可以额外出现一个随机、单报告范围的 `retestOfReceiptId`。客户端和服务端都拒绝任何其他字段,而不是接收后脱敏。 +复测时可以额外出现一个随机、单报告范围的 `retestOfReceiptId`。客户端和服务端拒绝任何其他字段。协议没有 `summary` 自由文本字段;界面和 GitHub 中看到的中文摘要都由上述枚举通过固定模板生成。 不会创建、读取或发送: -- stdout、stderr、访问日志、应用日志; +- 当前任务、任务标签、Prompt、Assistant 回复或 Agent memory; +- stdout、stderr、访问日志、应用日志或 Tool 参数/结果; - exception、错误码、stack、frame、崩溃指纹; -- Prompt、Assistant 回复、Agent memory、Tool 参数和结果; - 文件、代码、路径、URL、环境变量、配置; - 用户、账号、设备、安装、Session 等稳定标识; -- 客户端时间、locale、OS、架构、DSH/Node 版本、任务标签、计数和时延; -- 备注、理由或任何自由文本反馈。 +- 客户端时间、locale、OS、架构、计数或时延; +- 备注、理由、模型自由摘要或其他自由文本。 -本地 v2 文件只有: +本地 v3 文件只有: ```text $DSH_HOME/omdsh-plugin-lab/ - feedback-v2.ndjson - share-requests-v2.ndjson - receipts-v2.ndjson - receipt-seen-v2.ndjson + feedback-v3.ndjson + share-requests-v3.ndjson + receipts-v3.ndjson + receipt-seen-v3.ndjson ``` -目录权限为 `0700`,文件权限为 `0600`。升级不会读取或补传旧版 `.install-id`、`events.ndjson` 或 `crashes.ndjson`;历史文件不会被自动删除。 +目录权限为 `0700`,文件权限为 `0600`。v3 不读取或补传旧版 `.install-id`、`events.ndjson`、`crashes.ndjson` 或 v2 队列;历史文件不会被自动删除。 -网络传输天然会让服务器或中间层观察 IP、时间等元数据,因此项目只承诺“载荷零内容、无身份字段”,不宣称绝对匿名。生产部署必须关闭代理、网关、WAF、应用和数据库的请求体日志,并不得把 IP/User-Agent 写入业务数据。 +网络传输仍会让服务器或中间层观察 IP、请求时间等元数据,因此项目不宣称绝对匿名。生产部署必须关闭代理、网关、WAF、应用和数据库的请求体日志,并不得把 IP/User-Agent 写入业务数据。 -更完整的可验证不变量和攻击测试见 [PRIVACY.md](./PRIVACY.md)。 +完整不变量和攻击测试见 [PRIVACY.md](./PRIVACY.md)。 ## 启用中央反馈 @@ -117,19 +123,19 @@ Bundle 默认关闭网络发送: retryIntervalMs: 30000 ``` -部署开启发送能力不等于用户同意。每条记录仍要由用户单独运行 `/omdsh-join`;拒绝发送不影响插件功能。 +部署开启发送能力不等于用户同意。每条记录仍要由用户单独确认 `/omdsh-join`;拒绝发送不影响插件功能。 ## 后端与 GitHub 飞轮 `server/` 提供 Node.js + PostgreSQL 接收器: -- 只接受 schema v2,未知字段 fail closed; +- 只接受 schema v3,未知字段 fail closed; - 请求体上限 1 KiB,错误响应不回显输入或异常; - 不存 IP、User-Agent、原始请求体或客户端时间; -- 不使用用户、安装或稳定匿名 ID,因此统计口径是“报告数”,不是“独立用户数”; -- 按公开插件、版本、health 和 experience 聚合; +- 不使用稳定用户 ID,统计口径是“报告数”而非“独立用户数”; +- 按公开插件、版本、health、experience 和 category 聚合; - 默认同类报告达到 5 条后才创建 GitHub 聚合 Issue; -- GitHub 不接收单条反馈或回执 ID; +- GitHub 只接收固定模板和聚合计数,不接收单条反馈、回执 ID或任务信息; - Follow Token 只关联一条报告,用于返回修复与复测状态。 生产环境需要: @@ -155,4 +161,4 @@ GITHUB_REPORT_THRESHOLD=5 pnpm test:all ``` -验证覆盖:闭合输入/输出 Schema、Loader 状态映射、Agent 零参数工具、内容非干涉金丝雀、无崩溃监听器、两阶段同意、Client 真实点击、后端未知字段拒绝、PostgreSQL v2 列审计、聚合阈值、回执/复测,以及真实 DSH rc.6 安装与启动生命周期。 +验证覆盖闭合 Schema、Agent 探活/预览工具、任务内容 canary、两阶段确认、Client 真实点击、服务端未知字段拒绝、PostgreSQL v3 列审计、GitHub 固定模板、聚合阈值、回执/复测,以及真实 DSH rc.6 安装与启动生命周期。 diff --git a/dist/agent-tool.d.ts b/dist/agent-tool.d.ts index b1e7c8f..ad4c54a 100644 --- a/dist/agent-tool.d.ts +++ b/dist/agent-tool.d.ts @@ -1,6 +1,12 @@ import type { Agent } from '@deepseek-ai/dsh-agent'; import type { ToolDefinition } from '@deepseek-ai/dsh-tools'; -import type { SafeExperienceAssessment } from './protocol.js'; +import { type ExperienceVerdict, type FeedbackCategory, type FeedbackPreview, type SafeExperienceAssessment } from './protocol.js'; export declare const AGENT_ASSESSMENT_TOOL = "omdsh_analyze_plugin_experience"; +export declare const AGENT_PREVIEW_TOOL = "omdsh_preview_plugin_feedback"; /** A raw ToolDefinition is used so the rc.6 input schema is closed as well as the output. */ export declare function createAgentAssessmentTool(assess: (agent: Agent | undefined) => SafeExperienceAssessment): ToolDefinition; +/** + * Pure preview tool: finite enum input, fixed-template output, and no storage or network side effect. + * The Agent can prepare the card, but only the user's separate confirmation can publish it. + */ +export declare function createAgentPreviewTool(preview: (agent: Agent | undefined, experience: ExperienceVerdict, category: FeedbackCategory) => FeedbackPreview): ToolDefinition; diff --git a/dist/agent-tool.js b/dist/agent-tool.js index 1c904e2..c792249 100644 --- a/dist/agent-tool.js +++ b/dist/agent-tool.js @@ -1,4 +1,6 @@ +import { FEEDBACK_CATEGORIES, } from './protocol.js'; export const AGENT_ASSESSMENT_TOOL = 'omdsh_analyze_plugin_experience'; +export const AGENT_PREVIEW_TOOL = 'omdsh_preview_plugin_feedback'; const PARAMETERS = { type: 'object', properties: {}, @@ -10,15 +12,70 @@ const OUTPUT = { properties: { health: { type: 'string', enum: ['ok', 'unavailable', 'error', 'unknown'] }, experience: { type: 'string', const: 'unknown' }, + feedbackCategories: { + type: 'array', + items: { type: 'string', enum: [...FEEDBACK_CATEGORIES] }, + }, + summaryIsTemplateOnly: { type: 'boolean', const: true }, + userConfirmationRequired: { type: 'boolean', const: true }, + }, + required: [ + 'health', 'experience', 'feedbackCategories', 'summaryIsTemplateOnly', 'userConfirmationRequired', + ], + additionalProperties: false, +}; +const PREVIEW_PARAMETERS = { + type: 'object', + properties: { + experience: { type: 'string', enum: ['good', 'mixed', 'bad'] }, + category: { type: 'string', enum: [...FEEDBACK_CATEGORIES] }, + }, + required: ['experience', 'category'], + additionalProperties: false, +}; +const PREVIEW_OUTPUT = { + type: 'object', + properties: { + plugin: { + type: 'object', + properties: { + moduleName: { type: 'string' }, + version: { type: 'string' }, + }, + required: ['moduleName'], + additionalProperties: false, + }, + health: { type: 'string', enum: ['ok', 'unavailable', 'error', 'unknown'] }, + experience: { type: 'string', enum: ['good', 'mixed', 'bad'] }, + category: { type: 'string', enum: [...FEEDBACK_CATEGORIES] }, + summary: { type: 'string' }, + willUpload: { type: 'boolean', const: false }, userConfirmationRequired: { type: 'boolean', const: true }, }, - required: ['health', 'experience', 'userConfirmationRequired'], + required: [ + 'plugin', 'health', 'experience', 'category', 'summary', 'willUpload', 'userConfirmationRequired', + ], additionalProperties: false, }; function noArguments(value) { return typeof value === 'object' && value !== null && !Array.isArray(value) && Object.keys(value).length === 0; } +function previewArguments(value) { + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + throw new TypeError('feedback preview accepts finite enums only'); + } + const row = value; + if (Object.keys(row).sort().join(',') !== 'category,experience' + || !['good', 'mixed', 'bad'].includes(row.experience) + || !FEEDBACK_CATEGORIES.includes(row.category)) { + throw new TypeError('feedback preview accepts finite enums only'); + } + return { + experience: row.experience, + category: row.category, + }; +} /** A raw ToolDefinition is used so the rc.6 input schema is closed as well as the output. */ export function createAgentAssessmentTool(assess) { return { @@ -26,6 +83,7 @@ export function createAgentAssessmentTool(assess) { description: [ 'Read only the current Plugin Lab trial lifecycle status from the DSH Host.', 'Call with an empty object. Never infer subjective quality from conversation content.', + 'You may suggest one task-agnostic category from feedbackCategories, but never include task details.', 'When experience is unknown, ask the user to confirm good, mixed, or bad.', ].join(' '), parameters: PARAMETERS, @@ -41,4 +99,28 @@ export function createAgentAssessmentTool(assess) { isConcurrencySafe: () => true, }; } +/** + * Pure preview tool: finite enum input, fixed-template output, and no storage or network side effect. + * The Agent can prepare the card, but only the user's separate confirmation can publish it. + */ +export function createAgentPreviewTool(preview) { + return { + name: AGENT_PREVIEW_TOOL, + description: [ + 'Prepare a task-agnostic feedback preview from two finite enums.', + 'Do not include or paraphrase the user task, conversation, logs, paths, errors, prompts, or outputs.', + 'This tool never stores or uploads anything. Show the preview and ask the user to confirm separately.', + ].join(' '), + parameters: PREVIEW_PARAMETERS, + output: { + schema: PREVIEW_OUTPUT, + render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }], + }, + async execute(args, exec) { + const parsed = previewArguments(args); + return preview(exec.agent, parsed.experience, parsed.category); + }, + isConcurrencySafe: () => true, + }; +} //# sourceMappingURL=agent-tool.js.map \ No newline at end of file diff --git a/dist/agent-tool.js.map b/dist/agent-tool.js.map index 4f82d1e..cfbe940 100644 --- a/dist/agent-tool.js.map +++ b/dist/agent-tool.js.map @@ -1 +1 @@ -{"version":3,"file":"agent-tool.js","sourceRoot":"","sources":["../src/agent-tool.ts"],"names":[],"mappings":"AAIA,MAAM,CAAC,MAAM,qBAAqB,GAAG,iCAAiC,CAAA;AAEtE,MAAM,UAAU,GAAG;IACjB,IAAI,EAAE,QAAQ;IACd,UAAU,EAAE,EAAE;IACd,QAAQ,EAAE,EAAE;IACZ,oBAAoB,EAAE,KAAK;CAC5B,CAAA;AAED,MAAM,MAAM,GAAmB;IAC7B,IAAI,EAAE,QAAQ;IACd,UAAU,EAAE;QACV,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE;QAC3E,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE;QAChD,wBAAwB,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE;KAC3D;IACD,QAAQ,EAAE,CAAC,QAAQ,EAAE,YAAY,EAAE,0BAA0B,CAAC;IAC9D,oBAAoB,EAAE,KAAK;CAC5B,CAAA;AAED,SAAS,WAAW,CAAC,KAAc;IACjC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;WACtE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,CAAA;AACtC,CAAC;AAED,6FAA6F;AAC7F,MAAM,UAAU,yBAAyB,CACvC,MAA8D;IAE9D,OAAO;QACL,IAAI,EAAE,qBAAqB;QAC3B,WAAW,EAAE;YACX,4EAA4E;YAC5E,sFAAsF;YACtF,0EAA0E;SAC3E,CAAC,IAAI,CAAC,GAAG,CAAC;QACX,UAAU,EAAE,UAAU;QACtB,MAAM,EAAE;YACN,MAAM,EAAE,MAAM;YACd,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;SAC1E;QACD,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI;YACtB,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC;gBAAE,MAAM,IAAI,SAAS,CAAC,4CAA4C,CAAC,CAAA;YACzF,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC3B,CAAC;QACD,iBAAiB,EAAE,GAAG,EAAE,CAAC,IAAI;KAC9B,CAAA;AACH,CAAC"} \ No newline at end of file +{"version":3,"file":"agent-tool.js","sourceRoot":"","sources":["../src/agent-tool.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/client.js b/dist/client.js index 36f0138..360ee10 100644 --- a/dist/client.js +++ b/dist/client.js @@ -54,22 +54,24 @@ var LabController = class { active }); } - async record(messageId, verdict) { + async record(messageId, verdict, category) { this.publish({ ...this.view, pending: { messageId, verdict, + category, phase: "saving" } }); - const settled = await this.execute(`/omdsh-result ${verdict}`); + const settled = await this.execute(`/omdsh-result ${verdict} ${category}`); if (settled.ok) this.publish({ ...this.view, active: false, pending: { messageId, verdict, + category, phase: "local", text: settled.text } @@ -79,6 +81,7 @@ var LabController = class { pending: { messageId, verdict, + category, phase: "error", text: settled.text } @@ -178,6 +181,40 @@ function shareLabel(verdict) { if (verdict === "mixed") return "查找相似反馈"; return "加入并等待修复"; } +const CATEGORY_CHOICES = [ + { + value: "installation", + label: "安装" + }, + { + value: "startup", + label: "启动" + }, + { + value: "invocation", + label: "调用" + }, + { + value: "compatibility", + label: "兼容性" + }, + { + value: "reliability", + label: "稳定性" + }, + { + value: "performance", + label: "性能" + }, + { + value: "result_quality", + label: "结果质量" + }, + { + value: "general", + label: "整体体验" + } +]; /** Latest durable assistant identity from the rc.6 conversation projection. */ function latestAssistantMessageId(nodes) { for (let index = nodes.length - 1; index >= 0; index -= 1) { @@ -192,6 +229,7 @@ function ExperienceResultCard({ messageId, useSession, usePluginLab, record, joi const view = usePluginLab((value) => value); const latestMessageId = useSession((snapshot) => latestAssistantMessageId(snapshot.nodes)); const [open, setOpen] = (0, react.useState)(false); + const [selectedVerdict, setSelectedVerdict] = (0, react.useState)(); if (!visible(view, messageId, latestMessageId)) return null; const pending = view.pending?.messageId === messageId ? view.pending : void 0; pending?.phase === "saving" || pending?.phase; @@ -217,7 +255,7 @@ function ExperienceResultCard({ messageId, useSession, usePluginLab, record, joi display: "block", marginBottom: 9 }, - children: "你觉得这个插件好用吗?" + children: selectedVerdict === void 0 ? "你觉得这个插件好用吗?" : "选择一个不涉及任务内容的大类" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { @@ -225,9 +263,9 @@ function ExperienceResultCard({ messageId, useSession, usePluginLab, record, joi marginBottom: 9, color: "var(--dsw-alias-label-secondary)" }, - children: "Agent 只知道运行状态,不会读取会话或日志替你判断。" + children: "Agent 可以建议有限大类,但不会把当前任务、会话或日志写进摘要。" }), - pending === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { + pending === void 0 && selectedVerdict === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)", @@ -238,7 +276,7 @@ function ExperienceResultCard({ messageId, useSession, usePluginLab, record, joi type: "button", style: choiceStyle, onClick: () => { - record(messageId, "good"); + setSelectedVerdict("good"); }, children: "好用" }), @@ -246,7 +284,7 @@ function ExperienceResultCard({ messageId, useSession, usePluginLab, record, joi type: "button", style: choiceStyle, onClick: () => { - record(messageId, "mixed"); + setSelectedVerdict("mixed"); }, children: "一般" }), @@ -254,12 +292,34 @@ function ExperienceResultCard({ messageId, useSession, usePluginLab, record, joi type: "button", style: choiceStyle, onClick: () => { - record(messageId, "bad"); + setSelectedVerdict("bad"); }, children: "不好用" }) ] }), + pending === void 0 && selectedVerdict !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { + style: { + display: "grid", + gridTemplateColumns: "repeat(2, 1fr)", + gap: 7 + }, + children: [CATEGORY_CHOICES.map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", { + type: "button", + style: choiceStyle, + onClick: () => { + record(messageId, selectedVerdict, choice.value); + }, + children: choice.label + }, choice.value)), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", { + type: "button", + style: choiceStyle, + onClick: () => { + setSelectedVerdict(void 0); + }, + children: "返回" + })] + }), pending !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { style: { display: "grid", @@ -273,7 +333,7 @@ function ExperienceResultCard({ messageId, useSession, usePluginLab, record, joi }, children: pending.phase === "saving" ? "正在只存到本机…" : pending.text }), - pending.phase === "local" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", { + pending.phase === "local" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", { type: "button", style: { ...choiceStyle, @@ -282,13 +342,14 @@ function ExperienceResultCard({ messageId, useSession, usePluginLab, record, joi onClick: () => { join(); }, - children: shareLabel(pending.verdict) + children: ["确认并提交:", shareLabel(pending.verdict)] }), (pending.phase === "joined" || pending.phase === "error") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", { type: "button", style: choiceStyle, onClick: () => { dismiss(); + setSelectedVerdict(void 0); setOpen(false); }, children: "完成" @@ -301,7 +362,7 @@ function ExperienceResultCard({ messageId, useSession, usePluginLab, record, joi marginTop: 10, color: "var(--dsw-alias-label-tertiary)" }, - children: "第一步只保存在本机;加入跟进只发送插件、版本、状态枚举和你的选择。网络传输并非绝对匿名。" + children: "提交前会显示完整脱敏预览;只发送插件、版本、状态、体验和大类枚举。网络传输并非绝对匿名。" }) ] })] @@ -449,7 +510,7 @@ function apply(ctx) { const controller = controllerFor(sessionId); return { hooks: { pluginLab: controller }, - record: (messageId, outcome) => controller.record(messageId, outcome), + record: (messageId, outcome, category) => controller.record(messageId, outcome, category), join: () => controller.join(), dismiss: () => controller.dismiss() }; diff --git a/dist/client.js.map b/dist/client.js.map index 0f34a04..35bcc90 100644 --- a/dist/client.js.map +++ b/dist/client.js.map @@ -1 +1 @@ -{"version":3,"file":"client.js","names":["INITIAL_VIEW: LabView","remote: CommandsRemote","sessionId: SessionId","error: unknown","triggerStyle: CSSProperties","panelStyle: CSSProperties","choiceStyle: CSSProperties","button: CSSProperties","button","button: CSSProperties"],"sources":["../src/client/controller.ts","../src/client/ExperienceResultCard.tsx","../src/client/InboxButton.tsx","../src/client/ProbeButton.tsx","../src/client/index.ts"],"sourcesContent":["import type { CommandResult } from '@deepseek-ai/dsh-commands/types'\nimport type { ClientRemote } from '@deepseek-ai/dsh-api-remotes/client'\nimport type { MessageId } from '@deepseek-ai/dsh-client-connection/client'\nimport type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots'\nimport type { ExperienceVerdict } from '../protocol.js'\n\n/** Exact rc.6 generated command Remote surface, kept narrow for testability. */\nexport type CommandsRemote = Pick\n\nexport interface PendingResult {\n readonly messageId: MessageId\n readonly verdict: ExperienceVerdict\n readonly phase: 'saving' | 'local' | 'joining' | 'joined' | 'error'\n readonly text?: string\n}\n\nexport interface LabView {\n readonly active: boolean\n readonly pending?: PendingResult\n}\n\nconst INITIAL_VIEW: LabView = Object.freeze({ active: false })\n\nfunction commandText(result: CommandResult | undefined): string {\n if (result === undefined) return '命令未被 DSH 接收。'\n return result.text ?? (result.kind === 'success' ? '完成。' : '操作失败。')\n}\n\nexport class LabController implements HostObservable {\n private view = INITIAL_VIEW\n private readonly listeners = new Set<() => void>()\n\n constructor(\n private readonly remote: CommandsRemote,\n private readonly sessionId: SessionId,\n ) {}\n\n getSnapshot = (): LabView => this.view\n\n subscribe = (listener: () => void): (() => void) => {\n this.listeners.add(listener)\n return () => { this.listeners.delete(listener) }\n }\n\n setTrialActive(active: boolean): void {\n this.publish({ ...this.view, active })\n }\n\n async record(messageId: MessageId, verdict: ExperienceVerdict): Promise {\n this.publish({ ...this.view, pending: { messageId, verdict, phase: 'saving' } })\n const settled = await this.execute(`/omdsh-result ${verdict}`)\n if (settled.ok) {\n this.publish({\n ...this.view,\n active: false,\n pending: { messageId, verdict, phase: 'local', text: settled.text },\n })\n } else {\n this.publish({ ...this.view, pending: { messageId, verdict, phase: 'error', text: settled.text } })\n }\n }\n\n async join(): Promise {\n const pending = this.view.pending\n if (pending === undefined || pending.phase !== 'local') return\n this.publish({ ...this.view, pending: { ...pending, phase: 'joining' } })\n const settled = await this.execute('/omdsh-join latest')\n this.publish({\n ...this.view,\n pending: {\n ...pending,\n phase: settled.ok ? 'joined' : 'error',\n text: settled.text,\n },\n })\n }\n\n async inbox(): Promise {\n return (await this.execute('/omdsh-inbox')).text\n }\n\n async probe(): Promise {\n return (await this.execute('/omdsh-probe')).text\n }\n\n dismiss(): void {\n const { pending: _pending, ...view } = this.view\n this.publish(view)\n }\n\n private async execute(line: string): Promise<{ ok: boolean; text: string }> {\n try {\n const result = await this.remote.execute(this.sessionId, line)\n if (!result.ok) return { ok: false, text: `${result.error.message} (${result.error.code})` }\n const command = result.value?.result\n return { ok: command?.kind === 'success', text: commandText(command) }\n } catch (error: unknown) {\n return { ok: false, text: error instanceof Error ? error.message : String(error) }\n }\n }\n\n private publish(view: LabView): void {\n this.view = Object.freeze(view)\n for (const listener of [...this.listeners]) listener()\n }\n}\n","import { useState, type CSSProperties } from 'react'\nimport type { MessageId } from '@deepseek-ai/dsh-client-connection/client'\nimport type { ConversationNode } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\nimport type { ExperienceVerdict } from '../protocol.js'\nimport type { LabController, LabView } from './controller.js'\n\nexport interface LabInjected {\n hooks: { pluginLab: LabController }\n record: (messageId: MessageId, verdict: ExperienceVerdict) => Promise\n join: () => Promise\n dismiss: () => void\n}\n\nexport type ExperienceResultCardProps =\n PropsRuntime<'conversation.chat.assistant-actions'>\n & InjectFace\n\nconst triggerStyle: CSSProperties = {\n border: 'none', borderRadius: 14, padding: '4px 9px', cursor: 'pointer',\n background: 'var(--dsw-alias-interactive-bg-hover)',\n color: 'var(--dsw-alias-label-secondary)', fontSize: 12,\n}\n\nconst panelStyle: CSSProperties = {\n position: 'absolute', zIndex: 30, right: 0, bottom: 34, width: 330,\n padding: 14, border: '1px solid var(--dsw-alias-border-secondary)', borderRadius: 12,\n background: 'var(--dsw-alias-bg-primary)', color: 'var(--dsw-alias-label-primary)',\n boxShadow: '0 12px 36px rgba(0,0,0,.18)', fontSize: 13, lineHeight: 1.5,\n}\n\nconst choiceStyle: CSSProperties = {\n border: '1px solid var(--dsw-alias-border-secondary)', borderRadius: 9,\n padding: '7px 9px', background: 'transparent', color: 'inherit', cursor: 'pointer',\n}\n\nfunction shareLabel(verdict: ExperienceVerdict): string {\n if (verdict === 'good') return '贡献聚合实测'\n if (verdict === 'mixed') return '查找相似反馈'\n return '加入并等待修复'\n}\n\n/** Latest durable assistant identity from the rc.6 conversation projection. */\nexport function latestAssistantMessageId(nodes: readonly ConversationNode[]): MessageId | undefined {\n for (let index = nodes.length - 1; index >= 0; index -= 1) {\n const node = nodes[index]\n if (node?.kind === 'assistant' && node.messageId !== undefined) return node.messageId\n }\n return undefined\n}\n\nfunction visible(view: LabView, messageId: MessageId, latestMessageId: MessageId | undefined): boolean {\n return view.pending?.messageId === messageId\n || (view.active && latestMessageId === messageId)\n}\n\nexport function ExperienceResultCard({\n messageId, useSession, usePluginLab, record, join, dismiss,\n}: ExperienceResultCardProps) {\n const view = usePluginLab(value => value)\n const latestMessageId = useSession(snapshot => latestAssistantMessageId(snapshot.nodes))\n const [open, setOpen] = useState(false)\n if (!visible(view, messageId, latestMessageId)) return null\n const pending = view.pending?.messageId === messageId ? view.pending : undefined\n const busy = pending?.phase === 'saving' || pending?.phase === 'joining'\n return (\n \n \n {open && (\n \n 你觉得这个插件好用吗?\n \n Agent 只知道运行状态,不会读取会话或日志替你判断。\n \n {pending === undefined && (\n \n \n \n \n \n )}\n {pending !== undefined && (\n \n \n {pending.phase === 'saving' ? '正在只存到本机…' : pending.text}\n \n {pending.phase === 'local' && (\n \n )}\n {(pending.phase === 'joined' || pending.phase === 'error') && (\n \n )}\n \n )}\n \n 第一步只保存在本机;加入跟进只发送插件、版本、状态枚举和你的选择。网络传输并非绝对匿名。\n \n \n )}\n \n )\n}\n","import { useState, type CSSProperties } from 'react'\nimport type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\n\nexport interface InboxInjected {\n checkInbox: () => Promise\n}\n\nexport type InboxButtonProps = PropsRuntime<'conversation.input.left'> & InjectFace\n\nconst button: CSSProperties = {\n height: 28, padding: '0 9px', border: 'none', borderRadius: 14, cursor: 'pointer',\n background: 'transparent', color: 'var(--dsw-alias-label-tertiary)', fontSize: 12,\n}\n\nexport function InboxButton({ checkInbox }: InboxButtonProps) {\n const [text, setText] = useState(null)\n const [busy, setBusy] = useState(false)\n return (\n \n {\n if (text !== null) return setText(null)\n setBusy(true)\n void checkInbox().then(setText).finally(() => { setBusy(false) })\n }}\n >\n {busy ? '检查中…' : '反馈进展'}\n \n {text !== null && (\n \n {text}\n \n )}\n \n )\n}\n","import { useState, type CSSProperties } from 'react'\nimport type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\n\nexport interface ProbeInjected {\n checkHealth: () => Promise\n}\n\nexport type ProbeButtonProps = PropsRuntime<'conversation.input.left'> & InjectFace\n\nconst button: CSSProperties = {\n height: 28, padding: '0 9px', border: 'none', borderRadius: 14, cursor: 'pointer',\n background: 'transparent', color: 'var(--dsw-alias-label-tertiary)', fontSize: 12,\n}\n\nexport function ProbeButton({ checkHealth }: ProbeButtonProps) {\n const [text, setText] = useState(null)\n const [busy, setBusy] = useState(false)\n return (\n \n {\n if (text !== null) return setText(null)\n setBusy(true)\n void checkHealth().then(setText).finally(() => { setBusy(false) })\n }}\n >\n {busy ? '探活中…' : '插件探活'}\n \n {text !== null && (\n \n {text}\n \n )}\n \n )\n}\n","import type { ClientContext, SessionId } from '@deepseek-ai/dsh-client-runtime/client'\nimport type {} from '@deepseek-ai/dsh-api-remotes/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-commands/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport { LabController, type CommandsRemote } from './controller.js'\nimport { ExperienceResultCard, type LabInjected } from './ExperienceResultCard.js'\nimport { InboxButton, type InboxInjected } from './InboxButton.js'\nimport { ProbeButton, type ProbeInjected } from './ProbeButton.js'\n\nexport { ExperienceResultCard } from './ExperienceResultCard.js'\nexport { InboxButton } from './InboxButton.js'\nexport { ProbeButton } from './ProbeButton.js'\nexport { LabController } from './controller.js'\n\nexport const inject = ['slots', 'remote', 'remote.commands']\n\nexport function apply(ctx: ClientContext): void {\n const controllers = new Map()\n ctx.effect(() => () => { controllers.clear() }, 'plugin-lab: client controller lifecycle')\n const controllerFor = (sessionId: SessionId): LabController => {\n let controller = controllers.get(sessionId)\n if (controller === undefined) {\n controller = new LabController(ctx.remote.commands as CommandsRemote, sessionId)\n controllers.set(sessionId, controller)\n }\n return controller\n }\n\n ctx.on('command/executed', (sessionId, name, result) => {\n if (result.kind !== 'success') return\n if (name === 'omdsh-start' || name === 'omdsh-retest') controllerFor(sessionId).setTrialActive(true)\n if (name === 'omdsh-result' || name === 'omdsh-feedback') controllerFor(sessionId).setTrialActive(false)\n })\n\n ctx.slots.inject('conversation.chat.assistant-actions', () => ctx.slots.register({\n name: 'conversation.chat.assistant-actions',\n id: 'omdsh-plugin-lab',\n order: 20,\n inject: (sessionId): LabInjected => {\n const controller = controllerFor(sessionId)\n return {\n hooks: { pluginLab: controller },\n record: (messageId, outcome) => controller.record(messageId, outcome),\n join: () => controller.join(),\n dismiss: () => controller.dismiss(),\n }\n },\n }, ExperienceResultCard))\n\n ctx.slots.inject('conversation.input.left', () => ctx.slots.register({\n name: 'conversation.input.left',\n id: 'omdsh-plugin-lab-inbox',\n order: 40,\n inject: (sessionId): InboxInjected => ({\n checkInbox: () => controllerFor(sessionId).inbox(),\n }),\n }, InboxButton))\n\n ctx.slots.inject('conversation.input.left', () => ctx.slots.register({\n name: 'conversation.input.left',\n id: 'omdsh-plugin-lab-probe',\n order: 39,\n inject: (sessionId): ProbeInjected => ({\n checkHealth: () => controllerFor(sessionId).probe(),\n }),\n }, ProbeButton))\n}\n"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"client.js","names":["INITIAL_VIEW: LabView","remote: CommandsRemote","sessionId: SessionId","error: unknown","triggerStyle: CSSProperties","panelStyle: CSSProperties","choiceStyle: CSSProperties","CATEGORY_CHOICES: ReadonlyArray<{ value: FeedbackCategory; label: string }>","button: CSSProperties","button","button: CSSProperties"],"sources":["../src/client/controller.ts","../src/client/ExperienceResultCard.tsx","../src/client/InboxButton.tsx","../src/client/ProbeButton.tsx","../src/client/index.ts"],"sourcesContent":["import type { CommandResult } from '@deepseek-ai/dsh-commands/types'\nimport type { ClientRemote } from '@deepseek-ai/dsh-api-remotes/client'\nimport type { MessageId } from '@deepseek-ai/dsh-client-connection/client'\nimport type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots'\nimport type { ExperienceVerdict, FeedbackCategory } from '../protocol.js'\n\n/** Exact rc.6 generated command Remote surface, kept narrow for testability. */\nexport type CommandsRemote = Pick\n\nexport interface PendingResult {\n readonly messageId: MessageId\n readonly verdict: ExperienceVerdict\n readonly category: FeedbackCategory\n readonly phase: 'saving' | 'local' | 'joining' | 'joined' | 'error'\n readonly text?: string\n}\n\nexport interface LabView {\n readonly active: boolean\n readonly pending?: PendingResult\n}\n\nconst INITIAL_VIEW: LabView = Object.freeze({ active: false })\n\nfunction commandText(result: CommandResult | undefined): string {\n if (result === undefined) return '命令未被 DSH 接收。'\n return result.text ?? (result.kind === 'success' ? '完成。' : '操作失败。')\n}\n\nexport class LabController implements HostObservable {\n private view = INITIAL_VIEW\n private readonly listeners = new Set<() => void>()\n\n constructor(\n private readonly remote: CommandsRemote,\n private readonly sessionId: SessionId,\n ) {}\n\n getSnapshot = (): LabView => this.view\n\n subscribe = (listener: () => void): (() => void) => {\n this.listeners.add(listener)\n return () => { this.listeners.delete(listener) }\n }\n\n setTrialActive(active: boolean): void {\n this.publish({ ...this.view, active })\n }\n\n async record(messageId: MessageId, verdict: ExperienceVerdict, category: FeedbackCategory): Promise {\n this.publish({ ...this.view, pending: { messageId, verdict, category, phase: 'saving' } })\n const settled = await this.execute(`/omdsh-result ${verdict} ${category}`)\n if (settled.ok) {\n this.publish({\n ...this.view,\n active: false,\n pending: { messageId, verdict, category, phase: 'local', text: settled.text },\n })\n } else {\n this.publish({ ...this.view, pending: { messageId, verdict, category, phase: 'error', text: settled.text } })\n }\n }\n\n async join(): Promise {\n const pending = this.view.pending\n if (pending === undefined || pending.phase !== 'local') return\n this.publish({ ...this.view, pending: { ...pending, phase: 'joining' } })\n const settled = await this.execute('/omdsh-join latest')\n this.publish({\n ...this.view,\n pending: {\n ...pending,\n phase: settled.ok ? 'joined' : 'error',\n text: settled.text,\n },\n })\n }\n\n async inbox(): Promise {\n return (await this.execute('/omdsh-inbox')).text\n }\n\n async probe(): Promise {\n return (await this.execute('/omdsh-probe')).text\n }\n\n dismiss(): void {\n const { pending: _pending, ...view } = this.view\n this.publish(view)\n }\n\n private async execute(line: string): Promise<{ ok: boolean; text: string }> {\n try {\n const result = await this.remote.execute(this.sessionId, line)\n if (!result.ok) return { ok: false, text: `${result.error.message} (${result.error.code})` }\n const command = result.value?.result\n return { ok: command?.kind === 'success', text: commandText(command) }\n } catch (error: unknown) {\n return { ok: false, text: error instanceof Error ? error.message : String(error) }\n }\n }\n\n private publish(view: LabView): void {\n this.view = Object.freeze(view)\n for (const listener of [...this.listeners]) listener()\n }\n}\n","import { useState, type CSSProperties } from 'react'\nimport type { MessageId } from '@deepseek-ai/dsh-client-connection/client'\nimport type { ConversationNode } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\nimport type { ExperienceVerdict, FeedbackCategory } from '../protocol.js'\nimport type { LabController, LabView } from './controller.js'\n\nexport interface LabInjected {\n hooks: { pluginLab: LabController }\n record: (\n messageId: MessageId,\n verdict: ExperienceVerdict,\n category: FeedbackCategory,\n ) => Promise\n join: () => Promise\n dismiss: () => void\n}\n\nexport type ExperienceResultCardProps =\n PropsRuntime<'conversation.chat.assistant-actions'>\n & InjectFace\n\nconst triggerStyle: CSSProperties = {\n border: 'none', borderRadius: 14, padding: '4px 9px', cursor: 'pointer',\n background: 'var(--dsw-alias-interactive-bg-hover)',\n color: 'var(--dsw-alias-label-secondary)', fontSize: 12,\n}\n\nconst panelStyle: CSSProperties = {\n position: 'absolute', zIndex: 30, right: 0, bottom: 34, width: 330,\n padding: 14, border: '1px solid var(--dsw-alias-border-secondary)', borderRadius: 12,\n background: 'var(--dsw-alias-bg-primary)', color: 'var(--dsw-alias-label-primary)',\n boxShadow: '0 12px 36px rgba(0,0,0,.18)', fontSize: 13, lineHeight: 1.5,\n}\n\nconst choiceStyle: CSSProperties = {\n border: '1px solid var(--dsw-alias-border-secondary)', borderRadius: 9,\n padding: '7px 9px', background: 'transparent', color: 'inherit', cursor: 'pointer',\n}\n\nfunction shareLabel(verdict: ExperienceVerdict): string {\n if (verdict === 'good') return '贡献聚合实测'\n if (verdict === 'mixed') return '查找相似反馈'\n return '加入并等待修复'\n}\n\nconst CATEGORY_CHOICES: ReadonlyArray<{ value: FeedbackCategory; label: string }> = [\n { value: 'installation', label: '安装' },\n { value: 'startup', label: '启动' },\n { value: 'invocation', label: '调用' },\n { value: 'compatibility', label: '兼容性' },\n { value: 'reliability', label: '稳定性' },\n { value: 'performance', label: '性能' },\n { value: 'result_quality', label: '结果质量' },\n { value: 'general', label: '整体体验' },\n]\n\n/** Latest durable assistant identity from the rc.6 conversation projection. */\nexport function latestAssistantMessageId(nodes: readonly ConversationNode[]): MessageId | undefined {\n for (let index = nodes.length - 1; index >= 0; index -= 1) {\n const node = nodes[index]\n if (node?.kind === 'assistant' && node.messageId !== undefined) return node.messageId\n }\n return undefined\n}\n\nfunction visible(view: LabView, messageId: MessageId, latestMessageId: MessageId | undefined): boolean {\n return view.pending?.messageId === messageId\n || (view.active && latestMessageId === messageId)\n}\n\nexport function ExperienceResultCard({\n messageId, useSession, usePluginLab, record, join, dismiss,\n}: ExperienceResultCardProps) {\n const view = usePluginLab(value => value)\n const latestMessageId = useSession(snapshot => latestAssistantMessageId(snapshot.nodes))\n const [open, setOpen] = useState(false)\n const [selectedVerdict, setSelectedVerdict] = useState()\n if (!visible(view, messageId, latestMessageId)) return null\n const pending = view.pending?.messageId === messageId ? view.pending : undefined\n const busy = pending?.phase === 'saving' || pending?.phase === 'joining'\n return (\n \n \n {open && (\n \n \n {selectedVerdict === undefined ? '你觉得这个插件好用吗?' : '选择一个不涉及任务内容的大类'}\n \n \n Agent 可以建议有限大类,但不会把当前任务、会话或日志写进摘要。\n \n {pending === undefined && selectedVerdict === undefined && (\n \n \n \n \n \n )}\n {pending === undefined && selectedVerdict !== undefined && (\n \n {CATEGORY_CHOICES.map(choice => (\n { void record(messageId, selectedVerdict, choice.value) }}\n >\n {choice.label}\n \n ))}\n \n \n )}\n {pending !== undefined && (\n \n \n {pending.phase === 'saving' ? '正在只存到本机…' : pending.text}\n \n {pending.phase === 'local' && (\n \n )}\n {(pending.phase === 'joined' || pending.phase === 'error') && (\n \n )}\n \n )}\n \n 提交前会显示完整脱敏预览;只发送插件、版本、状态、体验和大类枚举。网络传输并非绝对匿名。\n \n \n )}\n \n )\n}\n","import { useState, type CSSProperties } from 'react'\nimport type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\n\nexport interface InboxInjected {\n checkInbox: () => Promise\n}\n\nexport type InboxButtonProps = PropsRuntime<'conversation.input.left'> & InjectFace\n\nconst button: CSSProperties = {\n height: 28, padding: '0 9px', border: 'none', borderRadius: 14, cursor: 'pointer',\n background: 'transparent', color: 'var(--dsw-alias-label-tertiary)', fontSize: 12,\n}\n\nexport function InboxButton({ checkInbox }: InboxButtonProps) {\n const [text, setText] = useState(null)\n const [busy, setBusy] = useState(false)\n return (\n \n {\n if (text !== null) return setText(null)\n setBusy(true)\n void checkInbox().then(setText).finally(() => { setBusy(false) })\n }}\n >\n {busy ? '检查中…' : '反馈进展'}\n \n {text !== null && (\n \n {text}\n \n )}\n \n )\n}\n","import { useState, type CSSProperties } from 'react'\nimport type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\n\nexport interface ProbeInjected {\n checkHealth: () => Promise\n}\n\nexport type ProbeButtonProps = PropsRuntime<'conversation.input.left'> & InjectFace\n\nconst button: CSSProperties = {\n height: 28, padding: '0 9px', border: 'none', borderRadius: 14, cursor: 'pointer',\n background: 'transparent', color: 'var(--dsw-alias-label-tertiary)', fontSize: 12,\n}\n\nexport function ProbeButton({ checkHealth }: ProbeButtonProps) {\n const [text, setText] = useState(null)\n const [busy, setBusy] = useState(false)\n return (\n \n {\n if (text !== null) return setText(null)\n setBusy(true)\n void checkHealth().then(setText).finally(() => { setBusy(false) })\n }}\n >\n {busy ? '探活中…' : '插件探活'}\n \n {text !== null && (\n \n {text}\n \n )}\n \n )\n}\n","import type { ClientContext, SessionId } from '@deepseek-ai/dsh-client-runtime/client'\nimport type {} from '@deepseek-ai/dsh-api-remotes/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-commands/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport { LabController, type CommandsRemote } from './controller.js'\nimport { ExperienceResultCard, type LabInjected } from './ExperienceResultCard.js'\nimport { InboxButton, type InboxInjected } from './InboxButton.js'\nimport { ProbeButton, type ProbeInjected } from './ProbeButton.js'\n\nexport { ExperienceResultCard } from './ExperienceResultCard.js'\nexport { InboxButton } from './InboxButton.js'\nexport { ProbeButton } from './ProbeButton.js'\nexport { LabController } from './controller.js'\n\nexport const inject = ['slots', 'remote', 'remote.commands']\n\nexport function apply(ctx: ClientContext): void {\n const controllers = new Map()\n ctx.effect(() => () => { controllers.clear() }, 'plugin-lab: client controller lifecycle')\n const controllerFor = (sessionId: SessionId): LabController => {\n let controller = controllers.get(sessionId)\n if (controller === undefined) {\n controller = new LabController(ctx.remote.commands as CommandsRemote, sessionId)\n controllers.set(sessionId, controller)\n }\n return controller\n }\n\n ctx.on('command/executed', (sessionId, name, result) => {\n if (result.kind !== 'success') return\n if (name === 'omdsh-start' || name === 'omdsh-retest') controllerFor(sessionId).setTrialActive(true)\n if (name === 'omdsh-result' || name === 'omdsh-feedback') controllerFor(sessionId).setTrialActive(false)\n })\n\n ctx.slots.inject('conversation.chat.assistant-actions', () => ctx.slots.register({\n name: 'conversation.chat.assistant-actions',\n id: 'omdsh-plugin-lab',\n order: 20,\n inject: (sessionId): LabInjected => {\n const controller = controllerFor(sessionId)\n return {\n hooks: { pluginLab: controller },\n record: (messageId, outcome, category) => controller.record(messageId, outcome, category),\n join: () => controller.join(),\n dismiss: () => controller.dismiss(),\n }\n },\n }, ExperienceResultCard))\n\n ctx.slots.inject('conversation.input.left', () => ctx.slots.register({\n name: 'conversation.input.left',\n id: 'omdsh-plugin-lab-inbox',\n order: 40,\n inject: (sessionId): InboxInjected => ({\n checkInbox: () => controllerFor(sessionId).inbox(),\n }),\n }, InboxButton))\n\n ctx.slots.inject('conversation.input.left', () => ctx.slots.register({\n name: 'conversation.input.left',\n id: 'omdsh-plugin-lab-probe',\n order: 39,\n inject: (sessionId): ProbeInjected => ({\n checkHealth: () => controllerFor(sessionId).probe(),\n }),\n }, ProbeButton))\n}\n"],"mappings":"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"} \ No newline at end of file diff --git a/dist/index.d.ts b/dist/index.d.ts index 231de5e..fb92e41 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -1,4 +1,4 @@ -/** Zero-content plugin health and user-confirmed feedback loop for DeepSeek Harness. */ +/** Task-agnostic plugin health and user-confirmed feedback loop for DeepSeek Harness. */ import type { Context } from '@deepseek-ai/cordis'; import z from '@deepseek-ai/schemastery'; export declare const name = "omdsh-plugin-lab"; diff --git a/dist/index.js b/dist/index.js index 8e0b032..8d5cf2b 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,10 +1,11 @@ -/** Zero-content plugin health and user-confirmed feedback loop for DeepSeek Harness. */ +/** Task-agnostic plugin health and user-confirmed feedback loop for DeepSeek Harness. */ import z from '@deepseek-ai/schemastery'; -import { createAgentAssessmentTool } from './agent-tool.js'; +import { createAgentAssessmentTool, createAgentPreviewTool } from './agent-tool.js'; import { healthText, probeLoaderHealth } from './health.js'; -import { JOIN_USAGE, parseJoinTarget, parseReceiptId, parseStartInput, parseVerdict, RESULT_USAGE, RETEST_USAGE, START_USAGE, } from './input.js'; -import { FEEDBACK_SCHEMA_VERSION, } from './protocol.js'; +import { JOIN_USAGE, parseJoinTarget, parseReceiptId, parseResultInput, parseStartInput, RESULT_USAGE, RETEST_USAGE, START_USAGE, } from './input.js'; +import { FEEDBACK_SCHEMA_VERSION, FEEDBACK_CATEGORIES, } from './protocol.js'; import { FeedbackStore } from './storage.js'; +import { fixedSummary, renderUploadPreview } from './summary.js'; import { ExperienceUploader } from './uploader.js'; export const name = 'omdsh-plugin-lab'; export const inject = ['commands']; @@ -27,14 +28,13 @@ function health(ctx, plugin) { return probeLoaderHealth(ctx.get('loader'), plugin.moduleName); } function assessment(status) { - return { health: status, experience: 'unknown', userConfirmationRequired: true }; -} -function verdictText(verdict) { - if (verdict === 'good') - return '好用'; - if (verdict === 'mixed') - return '一般'; - return '不好用'; + return { + health: status, + experience: 'unknown', + feedbackCategories: FEEDBACK_CATEGORIES, + summaryIsTemplateOnly: true, + userConfirmationRequired: true, + }; } function renderReceipt(receipt) { const lines = ['已发送严格最小化反馈。']; @@ -93,10 +93,26 @@ export function apply(ctx, rawConfig) { const state = key === undefined ? undefined : trials.get(key); return assessment(state === undefined ? 'unknown' : health(ctx, state.plugin)); }; + const previewTrial = (key, experience, category) => { + const state = key === undefined ? undefined : trials.get(key); + if (state === undefined) + throw new TypeError('no active Plugin Lab trial'); + const status = health(ctx, state.plugin); + return { + plugin: state.plugin, + health: status, + experience, + category, + summary: fixedSummary(state.plugin, status, experience, category), + willUpload: false, + userConfirmationRequired: true, + }; + }; // Optional capability: headless command-only tests still work, while a normal // rc.6 Agent runtime receives the closed, zero-argument assessment tool. ctx.inject(['tools'], toolCtx => { toolCtx.tools.register(createAgentAssessmentTool(agent => assessTrial(agentSessionKey(agent)))); + toolCtx.tools.register(createAgentPreviewTool((agent, experience, category) => (previewTrial(agentSessionKey(agent), experience, category)))); }); const beginTrial = (invocation, input, retestOfReceiptId) => { const key = sessionKey(invocation); @@ -113,7 +129,7 @@ export function apply(ctx, rawConfig) { text: [ `试用已开始:${input.plugin.moduleName}${input.plugin.version === undefined ? '' : `#${input.plugin.version}`}`, `运行状态:${healthText(health(ctx, input.plugin))}`, - '完成后由你选择“好用 / 一般 / 不好用”;Agent 不会读取会话或日志替你判断。', + '完成后由你选择体验和一个脱敏大类;Agent 只能用有限枚举生成固定模板预览。', ].join('\n'), }; }; @@ -146,7 +162,7 @@ export function apply(ctx, rawConfig) { : [ `插件:${state.plugin.moduleName}`, `运行状态:${healthText(result.health)}`, - '主观体验:未确认。探活没有读取日志、会话、异常或文件。', + '主观体验:未确认。探活没有读取日志、会话、异常或文件。Agent 可建议大类,但不能提交任务摘要。', ].join('\n'), }; }; @@ -156,8 +172,11 @@ export function apply(ctx, rawConfig) { if (state === undefined) return { kind: 'error', text: `没有进行中的插件试用。先运行 ${START_USAGE}` }; let verdict; + let category; try { - verdict = parseVerdict(invocation.rawInput); + const parsed = parseResultInput(invocation.rawInput); + verdict = parsed.verdict; + category = parsed.category; } catch { return { kind: 'error', text: RESULT_USAGE }; @@ -169,6 +188,7 @@ export function apply(ctx, rawConfig) { plugin: state.plugin, health: health(ctx, state.plugin), experience: verdict, + category, source: 'user_confirmed', ...state.retestOfReceiptId === undefined ? {} : { retestOfReceiptId: state.retestOfReceiptId }, }; @@ -177,11 +197,9 @@ export function apply(ctx, rawConfig) { return { kind: 'success', text: [ - `运行状态:${healthText(event.health)}`, - `主观体验:${verdictText(verdict)}(由你确认)`, + ...renderUploadPreview(event), `已只保存到本机:${store.eventsPath}`, - '记录只有公开插件 ID/版本、状态枚举和你的选择;不含日志、会话、时间、环境或稳定身份。', - '运行 /omdsh-join latest 才会发送屏幕上这组有限字段。', + '请检查以上预览;只有运行 /omdsh-join latest 才会发送这组有限字段。', ].join('\n'), }; }; @@ -252,8 +270,8 @@ export function apply(ctx, rawConfig) { text: [ `结构化分享:${uploader === undefined ? '未启用' : '只能由用户逐次运行 /omdsh-join 触发'}`, '探活:仅本地读取 DSH Host 的 Loader/Fiber 生命周期枚举,不访问插件对象或网络。', - 'Agent:零参数工具只返回 health、experience=unknown、userConfirmationRequired=true。', - '可发送字段:schemaVersion、type、随机单次 eventId、公开插件 ID/版本、health、experience、source。', + 'Agent:探活工具零参数;预览工具只接受体验和大类枚举,固定模板预览不会存储或发送。', + '可发送字段:schemaVersion、type、随机单次 eventId、公开插件 ID/版本、health、experience、category、source。', '绝不读取或发送:日志、异常、堆栈、崩溃指纹、Prompt、回复、Tool 数据、文件、路径、环境、时间、用户/设备/安装/Session ID。', '网络仍会自然暴露传输元数据,因此本插件不宣称匿名;服务端必须禁止持久化 IP、User-Agent 和请求体日志。', `本地最小化数据:${store.dataDir}`, @@ -274,15 +292,15 @@ export function apply(ctx, rawConfig) { }); ctx.commands.register({ name: 'omdsh-result', - description: '由用户确认主观体验;只保存在本机', - input: { hint: '' }, + description: '由用户确认体验与脱敏大类,生成本地上传预览', + input: { hint: ` <${FEEDBACK_CATEGORIES.join('|')}>` }, recordInput: false, handler: submitResult, }); ctx.commands.register({ name: 'omdsh-feedback', - description: '兼容入口:由用户确认主观体验;只保存在本机', - input: { hint: '' }, + description: '兼容入口:由用户确认体验与脱敏大类,生成本地上传预览', + input: { hint: ` <${FEEDBACK_CATEGORIES.join('|')}>` }, recordInput: false, handler: submitResult, }); diff --git a/dist/index.js.map b/dist/index.js.map index 3d2ffc9..68b34c2 100644 --- a/dist/index.js.map +++ b/dist/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,wFAAwF;AAIxF,OAAO,CAAC,MAAM,0BAA0B,CAAA;AACxC,OAAO,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAA;AAC3D,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAmB,MAAM,aAAa,CAAA;AAC5E,OAAO,EACL,UAAU,EAAE,eAAe,EAAE,cAAc,EAAE,eAAe,EAAE,YAAY,EAC1E,YAAY,EAAE,YAAY,EAAE,WAAW,GACxC,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,uBAAuB,GAQxB,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAElD,MAAM,CAAC,MAAM,IAAI,GAAG,kBAAkB,CAAA;AACtC,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,UAAU,CAAC,CAAA;AAalC,MAAM,CAAC,MAAM,MAAM,GAAc,CAAC,CAAC,MAAM,CAAC;IACxC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;IACrB,UAAU,EAAE,CAAC,CAAC,OAAO,EAAE;IACvB,mBAAmB,EAAE,CAAC,CAAC,OAAO,EAAE;IAChC,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,wBAAwB,CAAC;IAC9D,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IAC3C,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC;CAC5C,CAAC,CAAA;AAgBF,SAAS,UAAU,CAAC,UAA4C;IAC9D,OAAO,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;AAC5C,CAAC;AAED,SAAS,eAAe,CAAC,KAAiE;IACxF,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;AACnE,CAAC;AAED,SAAS,MAAM,CAAC,GAAY,EAAE,MAAsB;IAClD,OAAO,iBAAiB,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAA2B,EAAE,MAAM,CAAC,UAAU,CAAC,CAAA;AAC1F,CAAC;AAED,SAAS,UAAU,CAAC,MAAoB;IACtC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,wBAAwB,EAAE,IAAI,EAAE,CAAA;AAClF,CAAC;AAED,SAAS,WAAW,CAAC,OAA0B;IAC7C,IAAI,OAAO,KAAK,MAAM;QAAE,OAAO,IAAI,CAAA;IACnC,IAAI,OAAO,KAAK,OAAO;QAAE,OAAO,IAAI,CAAA;IACpC,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,aAAa,CAAC,OAAsB;IAC3C,MAAM,KAAK,GAAG,CAAC,aAAa,CAAC,CAAA;IAC7B,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;IACtE,IAAI,OAAO,CAAC,cAAc,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,OAAO,CAAC,cAAc,KAAK,CAAC,CAAA;IACzF,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,MAAM,OAAO,CAAC,MAAM,GAAG,CAAC,CAAA;IACrE,IAAI,OAAO,CAAC,kBAAkB,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,OAAO,CAAC,kBAAkB,GAAG,CAAC,CAAA;IAC/F,IAAI,OAAO,CAAC,WAAW,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,OAAO,CAAC,WAAW,EAAE,CAAC,CAAA;IAChF,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,cAAc,CAAC,MAAc;IACpC,MAAM,gBAAgB,GAAG,MAAM,CAAC,gBAAgB,IAAI,KAAK,CAAA;IACzD,MAAM,eAAe,GAAG,MAAM,CAAC,eAAe,IAAI,MAAM,CAAA;IACxD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAC,IAAI,gBAAgB,IAAI,CAAC,EAAE,CAAC;QAChE,MAAM,IAAI,SAAS,CAAC,+DAA+D,CAAC,CAAA;IACtF,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAC,IAAI,eAAe,GAAG,KAAK,EAAE,CAAC;QACjE,MAAM,IAAI,SAAS,CAAC,mDAAmD,CAAC,CAAA;IAC1E,CAAC;IACD,MAAM,gBAAgB,GAAG,MAAM,CAAC,gBAAgB,IAAI,wBAAwB,CAAA;IAC5E,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,gBAAgB,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,SAAS,CAAC,6EAA6E,CAAC,CAAA;IACpG,CAAC;IACD,IAAI,MAAM,CAAC,SAAS,KAAK,SAAS,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClE,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;QACxC,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,KAAK,WAAW,IAAI,MAAM,CAAC,QAAQ,KAAK,WAAW,EAAE,CAAC;YACvG,MAAM,IAAI,SAAS,CAAC,mFAAmF,CAAC,CAAA;QAC1G,CAAC;IACH,CAAC;IACD,OAAO;QACL,GAAG,MAAM,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE;QAClE,GAAG,MAAM,CAAC,SAAS,KAAK,SAAS,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE;QACzG,UAAU,EAAE,MAAM,CAAC,UAAU,IAAI,MAAM,CAAC,mBAAmB,IAAI,KAAK;QACpE,gBAAgB;QAChB,gBAAgB;QAChB,eAAe;KAChB,CAAA;AACH,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,GAAY,EAAE,SAAiB;IACnD,MAAM,MAAM,GAAG,cAAc,CAAC,SAAS,CAAC,CAAA;IACxC,MAAM,KAAK,GAAG,IAAI,aAAa,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IAC/C,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,IAAI,MAAM,CAAC,SAAS,KAAK,SAAS;QAClE,CAAC,CAAC,IAAI,kBAAkB,CAAC,KAAK,EAAE;YAC9B,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,gBAAgB,EAAE,MAAM,CAAC,gBAAgB;YACzC,gBAAgB,EAAE,MAAM,CAAC,gBAAgB;SAC1C,CAAC;QACF,CAAC,CAAC,SAAS,CAAA;IACb,MAAM,MAAM,GAAG,IAAI,GAAG,EAAsB,CAAA;IAE5C,MAAM,WAAW,GAAG,CAAC,GAAuB,EAA4B,EAAE;QACxE,MAAM,KAAK,GAAG,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC7D,OAAO,UAAU,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAA;IAChF,CAAC,CAAA;IAED,8EAA8E;IAC9E,yEAAyE;IACzE,GAAG,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE;QAC9B,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,yBAAyB,CAAC,KAAK,CAAC,EAAE,CAAC,WAAW,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;IACjG,CAAC,CAAC,CAAA;IAEF,MAAM,UAAU,GAAG,CACjB,UAA6B,EAC7B,KAAyC,EACzC,iBAA0B,EACX,EAAE;QACjB,MAAM,GAAG,GAAG,UAAU,CAAC,UAAU,CAAC,CAAA;QAClC,IAAI,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACpB,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,0BAA0B,EAAE,CAAA;QAC5D,CAAC;QACD,MAAM,KAAK,GAAe;YACxB,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,GAAG,iBAAiB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE;SAChE,CAAA;QACD,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;QACtB,OAAO;YACL,IAAI,EAAE,SAAS;YACf,IAAI,EAAE;gBACJ,SAAS,KAAK,CAAC,MAAM,CAAC,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE;gBACzG,QAAQ,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE;gBAC/C,6CAA6C;aAC9C,CAAC,IAAI,CAAC,IAAI,CAAC;SACb,CAAA;IACH,CAAC,CAAA;IAED,MAAM,UAAU,GAAG,CAAC,UAA6B,EAAiB,EAAE;QAClE,IAAI,CAAC;YACH,OAAO,UAAU,CAAC,UAAU,EAAE,eAAe,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAA;QACrE,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAA;QAC7C,CAAC;IACH,CAAC,CAAA;IAED,MAAM,WAAW,GAAG,CAAC,UAA6B,EAAiB,EAAE;QACnE,MAAM,CAAC,SAAS,EAAE,GAAG,WAAW,CAAC,GAAG,UAAU,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QAC5F,IAAI,SAAS,KAAK,SAAS,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,CAAA;QACrG,IAAI,CAAC;YACH,OAAO,UAAU,CAAC,UAAU,EAAE,eAAe,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,cAAc,CAAC,SAAS,CAAC,CAAC,CAAA;QACjG,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,CAAA;QAC9C,CAAC;IACH,CAAC,CAAA;IAED,MAAM,KAAK,GAAG,CAAC,UAA6B,EAAiB,EAAE;QAC7D,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAA;QAChD,MAAM,MAAM,GAAG,WAAW,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAA;QAClD,OAAO;YACL,IAAI,EAAE,SAAS;YACf,IAAI,EAAE,KAAK,KAAK,SAAS;gBACvB,CAAC,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,eAAe;gBAC7C,CAAC,CAAC;oBACA,MAAM,KAAK,CAAC,MAAM,CAAC,UAAU,EAAE;oBAC/B,QAAQ,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE;oBACnC,6BAA6B;iBAC9B,CAAC,IAAI,CAAC,IAAI,CAAC;SACf,CAAA;IACH,CAAC,CAAA;IAED,MAAM,YAAY,GAAG,CAAC,UAA6B,EAAiB,EAAE;QACpE,MAAM,GAAG,GAAG,UAAU,CAAC,UAAU,CAAC,CAAA;QAClC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC7B,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,kBAAkB,WAAW,EAAE,EAAE,CAAA;QACxF,IAAI,OAA0B,CAAA;QAC9B,IAAI,CAAC;YACH,OAAO,GAAG,YAAY,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAA;QAC7C,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,CAAA;QAC9C,CAAC;QACD,MAAM,KAAK,GAAoB;YAC7B,aAAa,EAAE,uBAAuB;YACtC,IAAI,EAAE,iBAAiB;YACvB,OAAO,EAAE,MAAM,CAAC,UAAU,EAAE;YAC5B,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,MAAM,EAAE,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,CAAC;YACjC,UAAU,EAAE,OAAO;YACnB,MAAM,EAAE,gBAAgB;YACxB,GAAG,KAAK,CAAC,iBAAiB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,KAAK,CAAC,iBAAiB,EAAE;SAC/F,CAAA;QACD,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,CAAA;QAC9C,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAClB,OAAO;YACL,IAAI,EAAE,SAAS;YACf,IAAI,EAAE;gBACJ,QAAQ,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE;gBAClC,QAAQ,WAAW,CAAC,OAAO,CAAC,QAAQ;gBACpC,WAAW,KAAK,CAAC,UAAU,EAAE;gBAC7B,8CAA8C;gBAC9C,sCAAsC;aACvC,CAAC,IAAI,CAAC,IAAI,CAAC;SACb,CAAA;IACH,CAAC,CAAA;IAED,MAAM,YAAY,GAAG,KAAK,EAAE,UAA6B,EAA0B,EAAE;QACnF,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,wBAAwB,EAAE,CAAA;QAC1D,CAAC;QACD,IAAI,MAAc,CAAA;QAClB,IAAI,CAAC;YACH,MAAM,GAAG,eAAe,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAA;QAC/C,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,CAAA;QAC5C,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,iBAAiB,EAAE,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAA;QACvF,IAAI,OAAO,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,SAAS,EAAE,CAAC;YACjE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,uBAAuB,EAAE,CAAA;QACzD,CAAC;QACD,MAAM,QAAQ,GAAG,KAAK,CAAC,cAAc,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,OAAO,KAAK,OAAO,CAAC,CAAA;QACpF,IAAI,QAAQ,KAAK,SAAS;YAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAA;QAChG,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,CAAA;QAC3B,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,CAAC,MAAM,QAAQ,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YACnE,IAAI,OAAO,KAAK,SAAS;gBAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,CAAA;YACxE,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;YACvB,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAA;QACrE,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,6BAA6B,EAAE,CAAA;QACjE,CAAC;IACH,CAAC,CAAA;IAED,MAAM,KAAK,GAAG,KAAK,EAAE,UAA6B,EAA0B,EAAE;QAC5E,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,IAAI,CAAC;gBACH,MAAM,QAAQ,CAAC,eAAe,EAAE,CAAA;YAClC,CAAC;YAAC,MAAM,CAAC;gBACP,iFAAiF;YACnF,CAAC;QACH,CAAC;QACD,MAAM,MAAM,GAAG,KAAK,CAAC,cAAc,EAAE,CAAA;QACrC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,sBAAsB,EAAE,CAAA;QACjF,MAAM,KAAK,GAAG,CAAC,gBAAgB,MAAM,CAAC,MAAM,QAAQ,CAAC,CAAA;QACrD,KAAK,MAAM,OAAO,IAAI,MAAM,EAAE,CAAC;YAC7B,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC,CAAA;YACzC,IAAI,UAAU,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,QAAQ;gBAAE,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;QACtE,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAA;IACpD,CAAC,CAAA;IAED,MAAM,MAAM,GAAG,CAAC,UAA6B,EAAiB,EAAE;QAC9D,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAA;QAChD,MAAM,KAAK,GAAG,KAAK,KAAK,SAAS;YAC/B,CAAC,CAAC,CAAC,eAAe,CAAC;YACnB,CAAC,CAAC;gBACA,MAAM,KAAK,CAAC,MAAM,CAAC,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE;gBACtG,QAAQ,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE;gBAC/C,UAAU;aACX,CAAA;QACH,KAAK,CAAC,IAAI,CAAC,SAAS,KAAK,CAAC,OAAO,EAAE,CAAC,MAAM,KAAK,EAAE,UAAU,KAAK,CAAC,cAAc,EAAE,CAAC,MAAM,KAAK,CAAC,CAAA;QAC9F,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAA;IACpD,CAAC,CAAA;IAED,MAAM,OAAO,GAAG,GAAkB,EAAE,CAAC,CAAC;QACpC,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACJ,SAAS,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,0BAA0B,EAAE;YACtE,qDAAqD;YACrD,yEAAyE;YACzE,4EAA4E;YAC5E,0EAA0E;YAC1E,2DAA2D;YAC3D,WAAW,KAAK,CAAC,OAAO,EAAE;SAC3B,CAAC,IAAI,CAAC,IAAI,CAAC;KACb,CAAC,CAAA;IAEF,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC;QACpB,IAAI,EAAE,aAAa;QACnB,WAAW,EAAE,mBAAmB;QAChC,KAAK,EAAE,EAAE,IAAI,EAAE,2BAA2B,EAAE;QAC5C,WAAW,EAAE,KAAK;QAClB,OAAO,EAAE,UAAU;KACpB,CAAC,CAAA;IACF,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC;QACpB,IAAI,EAAE,aAAa;QACnB,WAAW,EAAE,kBAAkB;QAC/B,WAAW,EAAE,KAAK;QAClB,OAAO,EAAE,KAAK;KACf,CAAC,CAAA;IACF,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC;QACpB,IAAI,EAAE,cAAc;QACpB,WAAW,EAAE,kBAAkB;QAC/B,KAAK,EAAE,EAAE,IAAI,EAAE,kBAAkB,EAAE;QACnC,WAAW,EAAE,KAAK;QAClB,OAAO,EAAE,YAAY;KACtB,CAAC,CAAA;IACF,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC;QACpB,IAAI,EAAE,gBAAgB;QACtB,WAAW,EAAE,uBAAuB;QACpC,KAAK,EAAE,EAAE,IAAI,EAAE,kBAAkB,EAAE;QACnC,WAAW,EAAE,KAAK;QAClB,OAAO,EAAE,YAAY;KACtB,CAAC,CAAA;IACF,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC;QACpB,IAAI,EAAE,YAAY;QAClB,WAAW,EAAE,oBAAoB;QACjC,KAAK,EAAE,EAAE,IAAI,EAAE,mBAAmB,EAAE;QACpC,WAAW,EAAE,KAAK;QAClB,OAAO,EAAE,YAAY;KACtB,CAAC,CAAA;IACF,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC;QACpB,IAAI,EAAE,aAAa;QACnB,WAAW,EAAE,kBAAkB;QAC/B,KAAK,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE;QAC3B,WAAW,EAAE,KAAK;QAClB,OAAO,EAAE,KAAK;KACf,CAAC,CAAA;IACF,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC;QACpB,IAAI,EAAE,cAAc;QACpB,WAAW,EAAE,gBAAgB;QAC7B,KAAK,EAAE,EAAE,IAAI,EAAE,wCAAwC,EAAE;QACzD,WAAW,EAAE,KAAK;QAClB,OAAO,EAAE,WAAW;KACrB,CAAC,CAAA;IACF,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC;QACpB,IAAI,EAAE,cAAc;QACpB,WAAW,EAAE,eAAe;QAC5B,WAAW,EAAE,KAAK;QAClB,OAAO,EAAE,MAAM;KAChB,CAAC,CAAA;IACF,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC;QACpB,IAAI,EAAE,eAAe;QACrB,WAAW,EAAE,mBAAmB;QAChC,WAAW,EAAE,KAAK;QAClB,OAAO,EAAE,OAAO;KACjB,CAAC,CAAA;IAEF,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,GAAS,EAAE;YACvB,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,YAAY,EAAE,EAAE,QAAQ,CAAC,eAAe,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;gBACjF,uEAAuE;YACzE,CAAC,CAAC,CAAA;QACJ,CAAC,CAAA;QACD,KAAK,EAAE,CAAA;QACP,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE;YACd,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,eAAe,CAAC,CAAA;YACxD,OAAO,GAAG,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC,CAAA;QACnC,CAAC,EAAE,gCAAgC,CAAC,CAAA;IACtC,CAAC;AACH,CAAC;AAED,eAAe,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,CAAA"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/input.d.ts b/dist/input.d.ts index aee3ae4..a3fa487 100644 --- a/dist/input.d.ts +++ b/dist/input.d.ts @@ -1,12 +1,15 @@ -import type { ExperienceVerdict, TrialPluginRef } from './protocol.js'; +import { type ExperienceVerdict, type FeedbackCategory, type TrialPluginRef } from './protocol.js'; export interface StartInput { readonly plugin: TrialPluginRef; } export declare const START_USAGE = "Usage: /omdsh-start [#version]"; -export declare const RESULT_USAGE = "Usage: /omdsh-result "; +export declare const RESULT_USAGE: string; export declare const JOIN_USAGE = "Usage: /omdsh-join "; export declare const RETEST_USAGE = "Usage: /omdsh-retest [#version]"; export declare function parseStartInput(rawInput: string): StartInput; -export declare function parseVerdict(rawInput: string): ExperienceVerdict; +export declare function parseResultInput(rawInput: string): { + readonly verdict: ExperienceVerdict; + readonly category: FeedbackCategory; +}; export declare function parseJoinTarget(rawInput: string): string; export declare function parseReceiptId(value: string): string; diff --git a/dist/input.js b/dist/input.js index f956f73..3fa9c92 100644 --- a/dist/input.js +++ b/dist/input.js @@ -1,9 +1,11 @@ +import { FEEDBACK_CATEGORIES, } from './protocol.js'; const MODULE = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u; const VERSION = /^[0-9A-Za-z][0-9A-Za-z.+_-]{0,63}$/u; const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu; const VERDICTS = new Set(['good', 'mixed', 'bad']); +const CATEGORIES = new Set(FEEDBACK_CATEGORIES); export const START_USAGE = 'Usage: /omdsh-start [#version]'; -export const RESULT_USAGE = 'Usage: /omdsh-result '; +export const RESULT_USAGE = `Usage: /omdsh-result <${FEEDBACK_CATEGORIES.join('|')}>`; export const JOIN_USAGE = 'Usage: /omdsh-join '; export const RETEST_USAGE = 'Usage: /omdsh-retest [#version]'; export function parseStartInput(rawInput) { @@ -19,11 +21,15 @@ export function parseStartInput(rawInput) { } return { plugin: { moduleName, ...version === undefined ? {} : { version } } }; } -export function parseVerdict(rawInput) { - const value = rawInput.trim(); - if (!VERDICTS.has(value)) +export function parseResultInput(rawInput) { + const parts = rawInput.trim().split(/\s+/u).filter(Boolean); + if (parts.length !== 2) throw new TypeError(RESULT_USAGE); - return value; + const [verdict, category] = parts; + if (!VERDICTS.has(verdict) + || !CATEGORIES.has(category)) + throw new TypeError(RESULT_USAGE); + return { verdict: verdict, category: category }; } export function parseJoinTarget(rawInput) { const parts = rawInput.trim().split(/\s+/u).filter(Boolean); diff --git a/dist/input.js.map b/dist/input.js.map index 9ccaa6c..37434c1 100644 --- a/dist/input.js.map +++ b/dist/input.js.map @@ -1 +1 @@ -{"version":3,"file":"input.js","sourceRoot":"","sources":["../src/input.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,GAAG,qDAAqD,CAAA;AACpE,MAAM,OAAO,GAAG,qCAAqC,CAAA;AACrD,MAAM,IAAI,GAAG,yEAAyE,CAAA;AACtF,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAoB,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAA;AAErE,MAAM,CAAC,MAAM,WAAW,GAAG,oDAAoD,CAAA;AAC/E,MAAM,CAAC,MAAM,YAAY,GAAG,uCAAuC,CAAA;AACnE,MAAM,CAAC,MAAM,UAAU,GAAG,sCAAsC,CAAA;AAChE,MAAM,CAAC,MAAM,YAAY,GAAG,kEAAkE,CAAA;AAE9F,MAAM,UAAU,eAAe,CAAC,QAAgB;IAC9C,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IAC3D,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;IAC3B,IAAI,UAAU,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,SAAS,CAAC,WAAW,CAAC,CAAA;IACpF,MAAM,IAAI,GAAG,UAAU,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;IACxC,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,UAAU,CAAA;IACpE,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACjE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,KAAK,SAAS,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;QAClF,MAAM,IAAI,SAAS,CAAC,WAAW,CAAC,CAAA;IAClC,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,GAAG,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAA;AAChF,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,QAAgB;IAC3C,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAA;IAC7B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAA0B,CAAC;QAAE,MAAM,IAAI,SAAS,CAAC,YAAY,CAAC,CAAA;IAChF,OAAO,KAA0B,CAAA;AACnC,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,QAAgB;IAC9C,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IAC3D,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,SAAS;WAC3C,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,QAAQ,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAAE,MAAM,IAAI,SAAS,CAAC,UAAU,CAAC,CAAA;IACrF,OAAO,KAAK,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,MAAM,IAAI,SAAS,CAAC,YAAY,CAAC,CAAA;IACxD,OAAO,KAAK,CAAA;AACd,CAAC"} \ No newline at end of file +{"version":3,"file":"input.js","sourceRoot":"","sources":["../src/input.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/protocol.d.ts b/dist/protocol.d.ts index 39174cc..ba2dd1b 100644 --- a/dist/protocol.d.ts +++ b/dist/protocol.d.ts @@ -1,7 +1,9 @@ -/** Closed, zero-content protocol shared by the DSH plugin and ingest backend. */ -export declare const FEEDBACK_SCHEMA_VERSION: 2; +/** Closed, task-agnostic summary protocol shared by the DSH plugin and ingest backend. */ +export declare const FEEDBACK_SCHEMA_VERSION: 3; export type HealthStatus = 'ok' | 'unavailable' | 'error' | 'unknown'; export type ExperienceVerdict = 'good' | 'mixed' | 'bad'; +export declare const FEEDBACK_CATEGORIES: readonly ["installation", "startup", "invocation", "compatibility", "reliability", "performance", "result_quality", "general"]; +export type FeedbackCategory = typeof FEEDBACK_CATEGORIES[number]; export interface TrialPluginRef { /** Public DSH marketplace/package identifier; never a local path or user label. */ readonly moduleName: string; @@ -11,13 +13,25 @@ export interface TrialPluginRef { export interface SafeExperienceAssessment { readonly health: HealthStatus; readonly experience: 'unknown'; + readonly feedbackCategories: readonly FeedbackCategory[]; + readonly summaryIsTemplateOnly: true; + readonly userConfirmationRequired: true; +} +/** Preview generated only from public plugin coordinates and finite enums. */ +export interface FeedbackPreview { + readonly plugin: TrialPluginRef; + readonly health: HealthStatus; + readonly experience: ExperienceVerdict; + readonly category: FeedbackCategory; + readonly summary: string; + readonly willUpload: false; readonly userConfirmationRequired: true; } /** * Exact upload envelope. It intentionally contains no timestamp, stable user or * install identifier, task/session identifier, metrics, notes, error or log data. */ -export interface FeedbackEventV2 { +export interface FeedbackEventV3 { readonly schemaVersion: typeof FEEDBACK_SCHEMA_VERSION; readonly type: 'feedback.signal'; /** Random per-report id used only for idempotency. */ @@ -25,12 +39,14 @@ export interface FeedbackEventV2 { readonly plugin: TrialPluginRef; readonly health: HealthStatus; readonly experience: ExperienceVerdict; + /** Task-agnostic category selected from the closed vocabulary above. */ + readonly category: FeedbackCategory; readonly source: 'user_confirmed'; /** Optional, report-scoped link used only when the user explicitly starts a retest. */ readonly retestOfReceiptId?: string; } export interface LocalFeedbackRecord { - readonly event: FeedbackEventV2; + readonly event: FeedbackEventV3; readonly requestedShare: boolean; } export type ReceiptStatus = 'received' | 'clustered' | 'reported' | 'fix-released' | 'retest-requested' | 'verified' | 'confirmed' | 'closed'; @@ -56,4 +72,4 @@ export interface ReceiptSeen { readonly updatedAt?: number; } /** The local record is already the exact network packet; no projection can add fields. */ -export declare function uploadPayload(record: LocalFeedbackRecord): FeedbackEventV2; +export declare function uploadPayload(record: LocalFeedbackRecord): FeedbackEventV3; diff --git a/dist/protocol.js b/dist/protocol.js index 65797c8..46a53ed 100644 --- a/dist/protocol.js +++ b/dist/protocol.js @@ -1,5 +1,15 @@ -/** Closed, zero-content protocol shared by the DSH plugin and ingest backend. */ -export const FEEDBACK_SCHEMA_VERSION = 2; +/** Closed, task-agnostic summary protocol shared by the DSH plugin and ingest backend. */ +export const FEEDBACK_SCHEMA_VERSION = 3; +export const FEEDBACK_CATEGORIES = [ + 'installation', + 'startup', + 'invocation', + 'compatibility', + 'reliability', + 'performance', + 'result_quality', + 'general', +]; /** The local record is already the exact network packet; no projection can add fields. */ export function uploadPayload(record) { return record.event; diff --git a/dist/protocol.js.map b/dist/protocol.js.map index 4459289..67671c5 100644 --- a/dist/protocol.js.map +++ b/dist/protocol.js.map @@ -1 +1 @@ -{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA,iFAAiF;AAEjF,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAU,CAAA;AA0EjD,0FAA0F;AAC1F,MAAM,UAAU,aAAa,CAAC,MAA2B;IACvD,OAAO,MAAM,CAAC,KAAK,CAAA;AACrB,CAAC"} \ No newline at end of file +{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAE1F,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAU,CAAA;AAIjD,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,cAAc;IACd,SAAS;IACT,YAAY;IACZ,eAAe;IACf,aAAa;IACb,aAAa;IACb,gBAAgB;IAChB,SAAS;CACD,CAAA;AAuFV,0FAA0F;AAC1F,MAAM,UAAU,aAAa,CAAC,MAA2B;IACvD,OAAO,MAAM,CAAC,KAAK,CAAA;AACrB,CAAC"} \ No newline at end of file diff --git a/dist/storage.js b/dist/storage.js index 07e8475..f387e93 100644 --- a/dist/storage.js +++ b/dist/storage.js @@ -43,10 +43,10 @@ export class FeedbackStore { if (!isAbsolute(dataDir)) throw new TypeError('plugin-lab: dataDir must be an absolute path'); this.dataDir = dataDir; - this.eventsPath = join(dataDir, 'feedback-v2.ndjson'); - this.receiptsPath = join(dataDir, 'receipts-v2.ndjson'); - this.shareRequestsPath = join(dataDir, 'share-requests-v2.ndjson'); - this.receiptSeenPath = join(dataDir, 'receipt-seen-v2.ndjson'); + this.eventsPath = join(dataDir, 'feedback-v3.ndjson'); + this.receiptsPath = join(dataDir, 'receipts-v3.ndjson'); + this.shareRequestsPath = join(dataDir, 'share-requests-v3.ndjson'); + this.receiptSeenPath = join(dataDir, 'receipt-seen-v3.ndjson'); mkdirSync(dataDir, { recursive: true, mode: 0o700 }); chmodSync(dataDir, 0o700); } diff --git a/dist/summary.d.ts b/dist/summary.d.ts new file mode 100644 index 0000000..81a0cd9 --- /dev/null +++ b/dist/summary.d.ts @@ -0,0 +1,6 @@ +import type { ExperienceVerdict, FeedbackCategory, FeedbackEventV3, HealthStatus, TrialPluginRef } from './protocol.js'; +export declare function categoryText(category: FeedbackCategory): string; +export declare function verdictText(verdict: ExperienceVerdict): string; +export declare function fixedSummary(plugin: TrialPluginRef, health: HealthStatus, experience: ExperienceVerdict, category: FeedbackCategory): string; +/** Every readable preview line is derived from the exact closed upload packet. */ +export declare function renderUploadPreview(event: FeedbackEventV3): string[]; diff --git a/dist/summary.js b/dist/summary.js new file mode 100644 index 0000000..6738c79 --- /dev/null +++ b/dist/summary.js @@ -0,0 +1,47 @@ +const CATEGORY_TEXT = { + installation: '安装', + startup: '启动', + invocation: '调用', + compatibility: '兼容性', + reliability: '稳定性', + performance: '性能', + result_quality: '结果质量', + general: '整体体验', +}; +const HEALTH_TEXT = { + ok: '运行正常', + unavailable: '当前不可用', + error: '运行错误', + unknown: '状态未知', +}; +const VERDICT_TEXT = { + good: '好用', + mixed: '一般', + bad: '不好用', +}; +export function categoryText(category) { + return CATEGORY_TEXT[category]; +} +export function verdictText(verdict) { + return VERDICT_TEXT[verdict]; +} +export function fixedSummary(plugin, health, experience, category) { + const coordinate = `${plugin.moduleName}${plugin.version === undefined ? '' : `#${plugin.version}`}`; + return `${coordinate} 在“${CATEGORY_TEXT[category]}”方面:${HEALTH_TEXT[health]},用户体验为“${VERDICT_TEXT[experience]}”。`; +} +/** Every readable preview line is derived from the exact closed upload packet. */ +export function renderUploadPreview(event) { + return [ + '待确认的脱敏摘要(尚未发送):', + `摘要:${fixedSummary(event.plugin, event.health, event.experience, event.category)}`, + `插件:${event.plugin.moduleName}`, + `版本:${event.plugin.version ?? '未提供'}`, + `运行状态:${HEALTH_TEXT[event.health]}`, + `体验大类:${CATEGORY_TEXT[event.category]}(${event.category})`, + `主观体验:${VERDICT_TEXT[event.experience]}(由你确认)`, + `单次报告 ID:${event.eventId}`, + ...event.retestOfReceiptId === undefined ? [] : [`复测回执 ID:${event.retestOfReceiptId}`], + '不会上传:当前任务、对话、Prompt、回复、日志、报错详情、堆栈、文件、路径、环境或身份信息。', + ]; +} +//# sourceMappingURL=summary.js.map \ No newline at end of file diff --git a/dist/summary.js.map b/dist/summary.js.map new file mode 100644 index 0000000..5f370e3 --- /dev/null +++ b/dist/summary.js.map @@ -0,0 +1 @@ +{"version":3,"file":"summary.js","sourceRoot":"","sources":["../src/summary.ts"],"names":[],"mappings":"AAIA,MAAM,aAAa,GAAqC;IACtD,YAAY,EAAE,IAAI;IAClB,OAAO,EAAE,IAAI;IACb,UAAU,EAAE,IAAI;IAChB,aAAa,EAAE,KAAK;IACpB,WAAW,EAAE,KAAK;IAClB,WAAW,EAAE,IAAI;IACjB,cAAc,EAAE,MAAM;IACtB,OAAO,EAAE,MAAM;CAChB,CAAA;AAED,MAAM,WAAW,GAAiC;IAChD,EAAE,EAAE,MAAM;IACV,WAAW,EAAE,OAAO;IACpB,KAAK,EAAE,MAAM;IACb,OAAO,EAAE,MAAM;CAChB,CAAA;AAED,MAAM,YAAY,GAAsC;IACtD,IAAI,EAAE,IAAI;IACV,KAAK,EAAE,IAAI;IACX,GAAG,EAAE,KAAK;CACX,CAAA;AAED,MAAM,UAAU,YAAY,CAAC,QAA0B;IACrD,OAAO,aAAa,CAAC,QAAQ,CAAC,CAAA;AAChC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,OAA0B;IACpD,OAAO,YAAY,CAAC,OAAO,CAAC,CAAA;AAC9B,CAAC;AAED,MAAM,UAAU,YAAY,CAC1B,MAAsB,EACtB,MAAoB,EACpB,UAA6B,EAC7B,QAA0B;IAE1B,MAAM,UAAU,GAAG,GAAG,MAAM,CAAC,UAAU,GAAG,MAAM,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,EAAE,EAAE,CAAA;IACpG,OAAO,GAAG,UAAU,MAAM,aAAa,CAAC,QAAQ,CAAC,OAAO,WAAW,CAAC,MAAM,CAAC,UAAU,YAAY,CAAC,UAAU,CAAC,IAAI,CAAA;AACnH,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,mBAAmB,CAAC,KAAsB;IACxD,OAAO;QACL,iBAAiB;QACjB,MAAM,YAAY,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,QAAQ,CAAC,EAAE;QAClF,MAAM,KAAK,CAAC,MAAM,CAAC,UAAU,EAAE;QAC/B,MAAM,KAAK,CAAC,MAAM,CAAC,OAAO,IAAI,KAAK,EAAE;QACrC,QAAQ,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE;QACnC,QAAQ,aAAa,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,QAAQ,GAAG;QAC1D,QAAQ,YAAY,CAAC,KAAK,CAAC,UAAU,CAAC,QAAQ;QAC9C,WAAW,KAAK,CAAC,OAAO,EAAE;QAC1B,GAAG,KAAK,CAAC,iBAAiB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,WAAW,KAAK,CAAC,iBAAiB,EAAE,CAAC;QACtF,kDAAkD;KACnD,CAAA;AACH,CAAC"} \ No newline at end of file diff --git a/dist/types/client/ExperienceResultCard.d.ts b/dist/types/client/ExperienceResultCard.d.ts index 34e596a..ad7de82 100644 --- a/dist/types/client/ExperienceResultCard.d.ts +++ b/dist/types/client/ExperienceResultCard.d.ts @@ -1,13 +1,13 @@ import type { MessageId } from '@deepseek-ai/dsh-client-connection/client'; import type { ConversationNode } from '@deepseek-ai/dsh-client-runtime/client'; import type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'; -import type { ExperienceVerdict } from '../protocol.js'; +import type { ExperienceVerdict, FeedbackCategory } from '../protocol.js'; import type { LabController } from './controller.js'; export interface LabInjected { hooks: { pluginLab: LabController; }; - record: (messageId: MessageId, verdict: ExperienceVerdict) => Promise; + record: (messageId: MessageId, verdict: ExperienceVerdict, category: FeedbackCategory) => Promise; join: () => Promise; dismiss: () => void; } diff --git a/dist/types/client/controller.d.ts b/dist/types/client/controller.d.ts index 490b0ae..d6543f2 100644 --- a/dist/types/client/controller.d.ts +++ b/dist/types/client/controller.d.ts @@ -2,12 +2,13 @@ import type { ClientRemote } from '@deepseek-ai/dsh-api-remotes/client'; import type { MessageId } from '@deepseek-ai/dsh-client-connection/client'; import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'; import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots'; -import type { ExperienceVerdict } from '../protocol.js'; +import type { ExperienceVerdict, FeedbackCategory } from '../protocol.js'; /** Exact rc.6 generated command Remote surface, kept narrow for testability. */ export type CommandsRemote = Pick; export interface PendingResult { readonly messageId: MessageId; readonly verdict: ExperienceVerdict; + readonly category: FeedbackCategory; readonly phase: 'saving' | 'local' | 'joining' | 'joined' | 'error'; readonly text?: string; } @@ -24,7 +25,7 @@ export declare class LabController implements HostObservable { getSnapshot: () => LabView; subscribe: (listener: () => void) => (() => void); setTrialActive(active: boolean): void; - record(messageId: MessageId, verdict: ExperienceVerdict): Promise; + record(messageId: MessageId, verdict: ExperienceVerdict, category: FeedbackCategory): Promise; join(): Promise; inbox(): Promise; probe(): Promise; diff --git a/dist/types/protocol.d.ts b/dist/types/protocol.d.ts index 39174cc..ba2dd1b 100644 --- a/dist/types/protocol.d.ts +++ b/dist/types/protocol.d.ts @@ -1,7 +1,9 @@ -/** Closed, zero-content protocol shared by the DSH plugin and ingest backend. */ -export declare const FEEDBACK_SCHEMA_VERSION: 2; +/** Closed, task-agnostic summary protocol shared by the DSH plugin and ingest backend. */ +export declare const FEEDBACK_SCHEMA_VERSION: 3; export type HealthStatus = 'ok' | 'unavailable' | 'error' | 'unknown'; export type ExperienceVerdict = 'good' | 'mixed' | 'bad'; +export declare const FEEDBACK_CATEGORIES: readonly ["installation", "startup", "invocation", "compatibility", "reliability", "performance", "result_quality", "general"]; +export type FeedbackCategory = typeof FEEDBACK_CATEGORIES[number]; export interface TrialPluginRef { /** Public DSH marketplace/package identifier; never a local path or user label. */ readonly moduleName: string; @@ -11,13 +13,25 @@ export interface TrialPluginRef { export interface SafeExperienceAssessment { readonly health: HealthStatus; readonly experience: 'unknown'; + readonly feedbackCategories: readonly FeedbackCategory[]; + readonly summaryIsTemplateOnly: true; + readonly userConfirmationRequired: true; +} +/** Preview generated only from public plugin coordinates and finite enums. */ +export interface FeedbackPreview { + readonly plugin: TrialPluginRef; + readonly health: HealthStatus; + readonly experience: ExperienceVerdict; + readonly category: FeedbackCategory; + readonly summary: string; + readonly willUpload: false; readonly userConfirmationRequired: true; } /** * Exact upload envelope. It intentionally contains no timestamp, stable user or * install identifier, task/session identifier, metrics, notes, error or log data. */ -export interface FeedbackEventV2 { +export interface FeedbackEventV3 { readonly schemaVersion: typeof FEEDBACK_SCHEMA_VERSION; readonly type: 'feedback.signal'; /** Random per-report id used only for idempotency. */ @@ -25,12 +39,14 @@ export interface FeedbackEventV2 { readonly plugin: TrialPluginRef; readonly health: HealthStatus; readonly experience: ExperienceVerdict; + /** Task-agnostic category selected from the closed vocabulary above. */ + readonly category: FeedbackCategory; readonly source: 'user_confirmed'; /** Optional, report-scoped link used only when the user explicitly starts a retest. */ readonly retestOfReceiptId?: string; } export interface LocalFeedbackRecord { - readonly event: FeedbackEventV2; + readonly event: FeedbackEventV3; readonly requestedShare: boolean; } export type ReceiptStatus = 'received' | 'clustered' | 'reported' | 'fix-released' | 'retest-requested' | 'verified' | 'confirmed' | 'closed'; @@ -56,4 +72,4 @@ export interface ReceiptSeen { readonly updatedAt?: number; } /** The local record is already the exact network packet; no projection can add fields. */ -export declare function uploadPayload(record: LocalFeedbackRecord): FeedbackEventV2; +export declare function uploadPayload(record: LocalFeedbackRecord): FeedbackEventV3; diff --git a/examples/mock-ingest.mjs b/examples/mock-ingest.mjs index 72aad18..6c9897d 100644 --- a/examples/mock-ingest.mjs +++ b/examples/mock-ingest.mjs @@ -3,7 +3,12 @@ import { createServer } from 'node:http' const port = Number(process.env.PORT ?? 8787) const reports = new Map() const allowed = new Set([ - 'schemaVersion', 'type', 'eventId', 'plugin', 'health', 'experience', 'source', 'retestOfReceiptId', + 'schemaVersion', 'type', 'eventId', 'plugin', 'health', 'experience', 'category', + 'source', 'retestOfReceiptId', +]) +const categories = new Set([ + 'installation', 'startup', 'invocation', 'compatibility', + 'reliability', 'performance', 'result_quality', 'general', ]) const server = createServer((request, response) => { @@ -19,13 +24,14 @@ const server = createServer((request, response) => { request.on('end', () => { try { const event = JSON.parse(raw) - if (event?.schemaVersion !== 2 || event?.type !== 'feedback.signal') throw new TypeError() + if (event?.schemaVersion !== 3 || event?.type !== 'feedback.signal') throw new TypeError() if (Object.keys(event).some(key => !allowed.has(key))) throw new TypeError() if (event?.source !== 'user_confirmed') throw new TypeError() + if (!categories.has(event?.category)) throw new TypeError() const plugin = event?.plugin?.moduleName if (typeof plugin !== 'string' || plugin.length === 0) throw new TypeError() const version = typeof event.plugin.version === 'string' ? event.plugin.version : 'unknown' - const key = `${plugin}#${version}:${event.health}:${event.experience}` + const key = `${plugin}#${version}:${event.health}:${event.experience}:${event.category}` const similarReports = (reports.get(key) ?? 0) + 1 reports.set(key, similarReports) return json(response, 200, { diff --git a/package.json b/package.json index aafc23e..841d0c8 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,15 @@ { "name": "@oh-my-dsh/plugin-lab", - "version": "0.3.0", - "description": "DSH zero-content plugin health, user-confirmed feedback and aggregate fix flywheel", + "version": "0.4.0", + "description": "DSH privacy-first plugin health, confirmed category summaries and aggregate fix flywheel", "license": "MIT", "type": "module", "files": [ "dist", "cordis.patch.yml", "examples", - "README.md" + "README.md", + "PRIVACY.md" ], "exports": { ".": { diff --git a/scripts/plugin-e2e.mjs b/scripts/plugin-e2e.mjs index 6afda1c..46735fe 100644 --- a/scripts/plugin-e2e.mjs +++ b/scripts/plugin-e2e.mjs @@ -90,6 +90,9 @@ function assertStrictHostArtifact(indexSource, toolSource) { if (!toolSource.includes('omdsh_analyze_plugin_experience')) { throw new Error('installed Host artifact omitted the Agent safe assessment tool') } + if (!toolSource.includes('omdsh_preview_plugin_feedback')) { + throw new Error('installed Host artifact omitted the Agent finite-enum preview tool') + } for (const forbidden of ['uncaughtExceptionMonitor', 'participantId', 'crashes.ndjson', 'shareNote']) { if (source.includes(forbidden)) { throw new Error(`installed Host artifact retained forbidden v1 path: ${forbidden}`) diff --git a/server/migrations/001_initial.sql b/server/migrations/001_initial.sql index 35620e2..8153051 100644 --- a/server/migrations/001_initial.sql +++ b/server/migrations/001_initial.sql @@ -1,4 +1,4 @@ -- Historical v1 bootstrap intentionally retired. Existing installations keep --- their old tables untouched; new installations create only strict v2 tables +-- their old tables untouched; later migrations create the active strict tables -- in 003_zero_content_feedback.sql. SELECT 1; diff --git a/server/migrations/004_confirmed_category_summary.sql b/server/migrations/004_confirmed_category_summary.sql new file mode 100644 index 0000000..d0800d8 --- /dev/null +++ b/server/migrations/004_confirmed_category_summary.sql @@ -0,0 +1,47 @@ +-- v3 adds one task-agnostic category enum. Historical v1/v2 tables remain +-- untouched and are never read, reinterpreted, migrated or uploaded by v3. +CREATE TABLE IF NOT EXISTS feedback_clusters_v3 ( + id uuid PRIMARY KEY, + cluster_key text UNIQUE NOT NULL, + plugin_module text NOT NULL, + plugin_version text, + health text NOT NULL CHECK (health IN ('ok', 'unavailable', 'error', 'unknown')), + experience text NOT NULL CHECK (experience IN ('good', 'mixed', 'bad')), + category text NOT NULL CHECK (category IN ( + 'installation', 'startup', 'invocation', 'compatibility', + 'reliability', 'performance', 'result_quality', 'general' + )), + symptom text NOT NULL, + status text NOT NULL DEFAULT 'received', + report_count integer NOT NULL DEFAULT 0, + github_issue_url text, + recommended_version text, + updated_at bigint NOT NULL +); + +CREATE TABLE IF NOT EXISTS feedback_events_v3 ( + event_id uuid PRIMARY KEY, + plugin_module text NOT NULL, + plugin_version text, + health text NOT NULL CHECK (health IN ('ok', 'unavailable', 'error', 'unknown')), + experience text NOT NULL CHECK (experience IN ('good', 'mixed', 'bad')), + category text NOT NULL CHECK (category IN ( + 'installation', 'startup', 'invocation', 'compatibility', + 'reliability', 'performance', 'result_quality', 'general' + )), + source text NOT NULL CHECK (source = 'user_confirmed'), + retest_of_receipt_id uuid, + cluster_key text NOT NULL REFERENCES feedback_clusters_v3(cluster_key), + created_at bigint NOT NULL +); + +CREATE INDEX IF NOT EXISTS feedback_events_v3_plugin_idx + ON feedback_events_v3(plugin_module, created_at DESC); +CREATE INDEX IF NOT EXISTS feedback_events_v3_cluster_idx + ON feedback_events_v3(cluster_key); + +CREATE TABLE IF NOT EXISTS follow_receipts_v3 ( + receipt_id uuid PRIMARY KEY, + event_id uuid UNIQUE NOT NULL REFERENCES feedback_events_v3(event_id), + created_at bigint NOT NULL +); diff --git a/server/src/fingerprint.ts b/server/src/fingerprint.ts index 181263b..81fd3ed 100644 --- a/server/src/fingerprint.ts +++ b/server/src/fingerprint.ts @@ -2,8 +2,8 @@ import { createHash, createHmac } from 'node:crypto' import type { AcceptedEvent } from './types.js' export function symptomFor(event: AcceptedEvent): string { - if (event.health !== 'ok') return `health-${event.health}` - return `experience-${event.experience}` + if (event.health !== 'ok') return `${event.category}-health-${event.health}` + return `${event.category}-experience-${event.experience}` } export function clusterKey(event: AcceptedEvent): string { @@ -12,6 +12,7 @@ export function clusterKey(event: AcceptedEvent): string { event.pluginVersion ?? '*', event.health, event.experience, + event.category, ].join('\u001f')).digest('hex') } diff --git a/server/src/github.ts b/server/src/github.ts index 4b61100..e283662 100644 --- a/server/src/github.ts +++ b/server/src/github.ts @@ -4,6 +4,17 @@ export interface IssuePublisher { publish(cluster: ClusterRecord): Promise } +const CATEGORY_TEXT: Record = { + installation: '安装', + startup: '启动', + invocation: '调用', + compatibility: '兼容性', + reliability: '稳定性', + performance: '性能', + result_quality: '结果质量', + general: '整体体验', +} + export class GitHubIssuePublisher implements IssuePublisher { constructor( private readonly token: string, @@ -13,7 +24,7 @@ export class GitHubIssuePublisher implements IssuePublisher { async publish(cluster: ClusterRecord): Promise { const [owner, repo] = this.repository.split('/') if (owner === undefined || repo === undefined) throw new Error('GITHUB_REPOSITORY must be owner/repo') - const title = `[Plugin Lab] ${cluster.pluginModule}: ${cluster.symptom}` + const title = `[Plugin Lab] ${cluster.pluginModule}: ${CATEGORY_TEXT[cluster.category]}聚合反馈` const body = [ '## 聚合实测', '', @@ -21,9 +32,12 @@ export class GitHubIssuePublisher implements IssuePublisher { `- 版本:\`${cluster.pluginVersion ?? 'unknown'}\``, `- 运行状态:\`${cluster.health}\``, `- 用户确认体验:\`${cluster.experience}\``, + `- 脱敏大类:\`${cluster.category}\`(${CATEGORY_TEXT[cluster.category]})`, `- 聚合报告数:${cluster.similarReports}`, '', - '此 Issue 只包含达到阈值后的有限枚举聚合,不包含单条报告、日志、会话、环境、时间或身份字段。', + `固定摘要:该插件在“${CATEGORY_TEXT[cluster.category]}”方面收到 ${cluster.similarReports} 条同类体验反馈。`, + '', + '此 Issue 只包含达到阈值后的有限枚举聚合,不包含当前任务、单条报告、日志、会话、环境、时间或身份字段。', ].join('\n') const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/issues`, { method: 'POST', diff --git a/server/src/memory.ts b/server/src/memory.ts index 6a68e6e..e106db6 100644 --- a/server/src/memory.ts +++ b/server/src/memory.ts @@ -29,6 +29,7 @@ export class MemoryRepository implements ExperienceRepository { ...event.pluginVersion === undefined ? {} : { pluginVersion: event.pluginVersion }, health: event.health, experience: event.experience, + category: event.category, symptom, status: 'received', similarReports: 0, diff --git a/server/src/postgres.ts b/server/src/postgres.ts index 98bfa2a..bcdff1a 100644 --- a/server/src/postgres.ts +++ b/server/src/postgres.ts @@ -1,7 +1,7 @@ import type { Pool, PoolClient, QueryResultRow } from 'pg' import type { AcceptedEvent, ClusterRecord, ClusterStatus, ExperienceRepository, ExperienceVerdict, HealthStatus, - Ingested, PluginEvidence, ReleaseUpdate, StoredReceipt, + FeedbackCategory, Ingested, PluginEvidence, ReleaseUpdate, StoredReceipt, } from './types.js' interface ClusterRow extends QueryResultRow { @@ -11,6 +11,7 @@ interface ClusterRow extends QueryResultRow { plugin_version: string | null health: HealthStatus experience: ExperienceVerdict + category: FeedbackCategory symptom: string status: ClusterStatus report_count: number @@ -27,6 +28,7 @@ function cluster(row: ClusterRow): ClusterRecord { ...row.plugin_version === null ? {} : { pluginVersion: row.plugin_version }, health: row.health, experience: row.experience, + category: row.category, symptom: row.symptom, status: row.status, similarReports: row.report_count, @@ -51,22 +53,25 @@ export class PostgresRepository implements ExperienceRepository { const now = Date.now() const clusterId = crypto.randomUUID() await client.query( - `INSERT INTO feedback_clusters_v2 - (id, cluster_key, plugin_module, plugin_version, health, experience, symptom, status, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, 'received', $8) + `INSERT INTO feedback_clusters_v3 + (id, cluster_key, plugin_module, plugin_version, health, experience, category, symptom, status, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'received', $9) ON CONFLICT (cluster_key) DO NOTHING`, - [clusterId, key, event.pluginModule, event.pluginVersion ?? null, event.health, event.experience, symptom, now], + [ + clusterId, key, event.pluginModule, event.pluginVersion ?? null, + event.health, event.experience, event.category, symptom, now, + ], ) const inserted = await client.query( - `INSERT INTO feedback_events_v2 - (event_id, plugin_module, plugin_version, health, experience, source, + `INSERT INTO feedback_events_v3 + (event_id, plugin_module, plugin_version, health, experience, category, source, retest_of_receipt_id, cluster_key, created_at) - VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT (event_id) DO NOTHING RETURNING event_id`, [ event.eventId, event.pluginModule, event.pluginVersion ?? null, event.health, - event.experience, event.source, event.retestOfReceiptId ?? null, key, now, + event.experience, event.category, event.source, event.retestOfReceiptId ?? null, key, now, ], ) if (inserted.rowCount === 0) { @@ -77,16 +82,16 @@ export class PostgresRepository implements ExperienceRepository { } const receiptId = crypto.randomUUID() await client.query( - 'INSERT INTO follow_receipts_v2 (receipt_id, event_id, created_at) VALUES ($1, $2, $3)', + 'INSERT INTO follow_receipts_v3 (receipt_id, event_id, created_at) VALUES ($1, $2, $3)', [receiptId, event.eventId, now], ) const count = await client.query<{ count: string }>( - 'SELECT COUNT(*)::text AS count FROM feedback_events_v2 WHERE cluster_key = $1', + 'SELECT COUNT(*)::text AS count FROM feedback_events_v3 WHERE cluster_key = $1', [key], ) const reports = Number(count.rows[0]?.count ?? 1) const updated = await client.query( - `UPDATE feedback_clusters_v2 SET + `UPDATE feedback_clusters_v3 SET report_count = $2, status = CASE WHEN status = 'received' AND $2 > 1 THEN 'clustered' ELSE status END, updated_at = $3 @@ -108,9 +113,9 @@ export class PostgresRepository implements ExperienceRepository { async receipt(receiptId: string): Promise { const result = await this.pool.query( `SELECT c.*, r.event_id, r.receipt_id - FROM follow_receipts_v2 r - JOIN feedback_events_v2 e ON e.event_id = r.event_id - JOIN feedback_clusters_v2 c ON c.cluster_key = e.cluster_key + FROM follow_receipts_v3 r + JOIN feedback_events_v3 e ON e.event_id = r.event_id + JOIN feedback_clusters_v3 c ON c.cluster_key = e.cluster_key WHERE r.receipt_id = $1`, [receiptId], ) @@ -120,7 +125,7 @@ export class PostgresRepository implements ExperienceRepository { async markReported(clusterId: string, issueUrl: string): Promise { const result = await this.pool.query( - `UPDATE feedback_clusters_v2 SET status = 'reported', github_issue_url = $2, updated_at = $3 + `UPDATE feedback_clusters_v3 SET status = 'reported', github_issue_url = $2, updated_at = $3 WHERE id = $1 RETURNING *`, [clusterId, issueUrl, Date.now()], ) @@ -131,7 +136,7 @@ export class PostgresRepository implements ExperienceRepository { async release(clusterId: string, update: ReleaseUpdate): Promise { const result = await this.pool.query( - `UPDATE feedback_clusters_v2 SET status = 'retest-requested', recommended_version = $2, + `UPDATE feedback_clusters_v3 SET status = 'retest-requested', recommended_version = $2, github_issue_url = COALESCE($3, github_issue_url), updated_at = $4 WHERE id = $1 RETURNING *`, [clusterId, update.recommendedVersion, update.trackingUrl ?? null, Date.now()], @@ -155,7 +160,7 @@ export class PostgresRepository implements ExperienceRepository { SUM(CASE WHEN experience = 'bad' THEN 1 ELSE 0 END)::text AS bad, MAX(created_at)::text AS updated_at, MAX(plugin_version) FILTER (WHERE experience = 'good') AS version - FROM feedback_events_v2 WHERE plugin_module = $1 AND created_at >= $2`, + FROM feedback_events_v3 WHERE plugin_module = $1 AND created_at >= $2`, [pluginModule, since], ) const row = result.rows[0] @@ -173,10 +178,10 @@ export class PostgresRepository implements ExperienceRepository { async verifyRetest(receiptId: string, successful: boolean): Promise { const result = await this.pool.query( - `UPDATE feedback_clusters_v2 SET status = $2, updated_at = $3 + `UPDATE feedback_clusters_v3 SET status = $2, updated_at = $3 WHERE cluster_key = ( - SELECT e.cluster_key FROM follow_receipts_v2 r - JOIN feedback_events_v2 e ON e.event_id = r.event_id + SELECT e.cluster_key FROM follow_receipts_v3 r + JOIN feedback_events_v3 e ON e.event_id = r.event_id WHERE r.receipt_id = $1 ) RETURNING *`, @@ -189,9 +194,9 @@ export class PostgresRepository implements ExperienceRepository { private async receiptForEvent(client: PoolClient, eventId: string): Promise { const result = await client.query( `SELECT c.*, r.event_id, r.receipt_id - FROM follow_receipts_v2 r - JOIN feedback_events_v2 e ON e.event_id = r.event_id - JOIN feedback_clusters_v2 c ON c.cluster_key = e.cluster_key + FROM follow_receipts_v3 r + JOIN feedback_events_v3 e ON e.event_id = r.event_id + JOIN feedback_clusters_v3 c ON c.cluster_key = e.cluster_key WHERE r.event_id = $1`, [eventId], ) diff --git a/server/src/types.ts b/server/src/types.ts index d7873fd..487a447 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -1,5 +1,14 @@ export type HealthStatus = 'ok' | 'unavailable' | 'error' | 'unknown' export type ExperienceVerdict = 'good' | 'mixed' | 'bad' +export type FeedbackCategory = + | 'installation' + | 'startup' + | 'invocation' + | 'compatibility' + | 'reliability' + | 'performance' + | 'result_quality' + | 'general' export type ClusterStatus = | 'received' | 'clustered' @@ -10,13 +19,14 @@ export type ClusterStatus = | 'confirmed' | 'closed' -/** Exact accepted v2 packet after fail-closed validation. */ +/** Exact accepted v3 packet after fail-closed validation. */ export interface AcceptedEvent { readonly eventId: string readonly pluginModule: string readonly pluginVersion?: string readonly health: HealthStatus readonly experience: ExperienceVerdict + readonly category: FeedbackCategory readonly source: 'user_confirmed' readonly retestOfReceiptId?: string } @@ -28,9 +38,10 @@ export interface ClusterRecord { readonly pluginVersion?: string readonly health: HealthStatus readonly experience: ExperienceVerdict + readonly category: FeedbackCategory readonly symptom: string readonly status: ClusterStatus - /** Number of reports, not unique users: strict v2 has no stable participant id. */ + /** Number of reports, not unique users: strict v3 has no stable participant id. */ readonly similarReports: number readonly githubIssueUrl?: string readonly recommendedVersion?: string diff --git a/server/src/validation.ts b/server/src/validation.ts index 4646782..d0ce094 100644 --- a/server/src/validation.ts +++ b/server/src/validation.ts @@ -1,10 +1,16 @@ -import type { AcceptedEvent, ExperienceVerdict, HealthStatus } from './types.js' +import type { + AcceptedEvent, ExperienceVerdict, FeedbackCategory, HealthStatus, +} from './types.js' const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu const MODULE = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u const VERSION = /^[0-9A-Za-z][0-9A-Za-z.+_-]{0,63}$/u const HEALTH = new Set(['ok', 'unavailable', 'error', 'unknown']) const EXPERIENCE = new Set(['good', 'mixed', 'bad']) +const CATEGORY = new Set([ + 'installation', 'startup', 'invocation', 'compatibility', + 'reliability', 'performance', 'result_quality', 'general', +]) function row(value: unknown, name: string): Record { if (typeof value !== 'object' || value === null || Array.isArray(value)) { @@ -23,13 +29,14 @@ function string(value: unknown, name: string): string { return value } -/** Reject, rather than strip, every field outside the strict v2 alphabet. */ +/** Reject, rather than strip, every field outside the strict v3 alphabet. */ export function acceptEvent(value: unknown): AcceptedEvent { const root = row(value, 'event') exactKeys(root, [ - 'schemaVersion', 'type', 'eventId', 'plugin', 'health', 'experience', 'source', 'retestOfReceiptId', + 'schemaVersion', 'type', 'eventId', 'plugin', 'health', 'experience', 'category', + 'source', 'retestOfReceiptId', ], 'event') - if (root.schemaVersion !== 2 || root.type !== 'feedback.signal') { + if (root.schemaVersion !== 3 || root.type !== 'feedback.signal') { throw new TypeError('unsupported event schema') } const eventId = string(root.eventId, 'eventId') @@ -44,6 +51,7 @@ export function acceptEvent(value: unknown): AcceptedEvent { if (!HEALTH.has(root.health as HealthStatus)) throw new TypeError('health is invalid') if (!EXPERIENCE.has(root.experience as ExperienceVerdict)) throw new TypeError('experience is invalid') + if (!CATEGORY.has(root.category as FeedbackCategory)) throw new TypeError('category is invalid') if (root.source !== 'user_confirmed') throw new TypeError('source is invalid') const retestOfReceiptId = root.retestOfReceiptId === undefined ? undefined @@ -58,6 +66,7 @@ export function acceptEvent(value: unknown): AcceptedEvent { ...version === undefined ? {} : { pluginVersion: version }, health: root.health as HealthStatus, experience: root.experience as ExperienceVerdict, + category: root.category as FeedbackCategory, source: 'user_confirmed', ...retestOfReceiptId === undefined ? {} : { retestOfReceiptId }, } diff --git a/server/tests/github.spec.ts b/server/tests/github.spec.ts new file mode 100644 index 0000000..bf36390 --- /dev/null +++ b/server/tests/github.spec.ts @@ -0,0 +1,40 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { GitHubIssuePublisher } from '../src/github.js' +import type { ClusterRecord } from '../src/types.js' + +afterEach(() => { vi.unstubAllGlobals() }) + +describe('GitHub aggregate summary publisher', () => { + it('publishes only a fixed category summary after service-side aggregation', async () => { + let requestBody = '' + vi.stubGlobal('fetch', vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { + requestBody = String(init?.body ?? '') + return new Response(JSON.stringify({ html_url: 'https://github.example/issues/1' }), { + status: 201, + headers: { 'content-type': 'application/json' }, + }) + })) + const cluster: ClusterRecord = { + id: crypto.randomUUID(), + clusterKey: 'aggregate-key', + pluginModule: '@example/plugin', + pluginVersion: '1.0.0', + health: 'error', + experience: 'bad', + category: 'reliability', + symptom: 'reliability-health-error', + status: 'clustered', + similarReports: 5, + updatedAt: Date.now(), + } + await expect(new GitHubIssuePublisher('token', 'owner/repo').publish(cluster)) + .resolves.toBe('https://github.example/issues/1') + const posted = JSON.parse(requestBody) as { title: string; body: string } + expect(posted.title).toContain('稳定性聚合反馈') + expect(posted.body).toContain('脱敏大类:`reliability`(稳定性)') + expect(posted.body).toContain('聚合报告数:5') + for (const forbidden of [ + 'CANARY_PRIVATE_TASK', 'Prompt 内容', '用户日志内容', '/Users/alice', '单次报告 ID', + ]) expect(posted.body).not.toContain(forbidden) + }) +}) diff --git a/server/tests/http.spec.ts b/server/tests/http.spec.ts index 6359536..a830f8a 100644 --- a/server/tests/http.spec.ts +++ b/server/tests/http.spec.ts @@ -26,12 +26,13 @@ async function endpoint(): Promise { function packet() { return { - schemaVersion: 2, + schemaVersion: 3, type: 'feedback.signal', eventId: crypto.randomUUID(), plugin: { moduleName: '@example/plugin', version: '1.0.0' }, health: 'ok', experience: 'good', + category: 'general', source: 'user_confirmed', } } diff --git a/server/tests/postgres.spec.ts b/server/tests/postgres.spec.ts index 0d36d0a..310f60c 100644 --- a/server/tests/postgres.spec.ts +++ b/server/tests/postgres.spec.ts @@ -13,12 +13,13 @@ function accepted(eventId = crypto.randomUUID()): AcceptedEvent { pluginVersion: '1.0.0', health: 'error', experience: 'bad', + category: 'reliability', source: 'user_confirmed', } } -describe('PostgreSQL strict v2 repository', () => { - it('persists only finite v2 signals and an idempotent receipt lifecycle', async () => { +describe('PostgreSQL strict v3 repository', () => { + it('persists only finite v3 category summaries and an idempotent receipt lifecycle', async () => { const database = newDb() for (const file of readdirSync(resolve('server/migrations')).filter(file => file.endsWith('.sql')).sort()) { database.public.none(readFileSync(resolve('server/migrations', file), 'utf8')) @@ -32,7 +33,10 @@ describe('PostgreSQL strict v2 repository', () => { expect(duplicate.receipt.receiptId).toBe(first.receipt.receiptId) await repository.ingest(accepted(), 'cluster-key', 'health-error') await expect(repository.receipt(first.receipt.receiptId)).resolves.toMatchObject({ - cluster: { status: 'clustered', similarReports: 2, health: 'error', experience: 'bad' }, + cluster: { + status: 'clustered', similarReports: 2, health: 'error', + experience: 'bad', category: 'reliability', + }, }) const released = await repository.release(first.receipt.cluster.id, { recommendedVersion: '1.0.1' }) expect(released).toMatchObject({ status: 'retest-requested', recommendedVersion: '1.0.1' }) @@ -46,7 +50,7 @@ describe('PostgreSQL strict v2 repository', () => { const columns = database.public.many(` SELECT column_name FROM information_schema.columns - WHERE table_name = 'feedback_events_v2' + WHERE table_name = 'feedback_events_v3' `).map(row => row.column_name) expect(columns).not.toEqual(expect.arrayContaining([ 'participant_id', 'occurred_at', 'task_id', 'note', 'crash_signatures', 'environment', diff --git a/server/tests/service.spec.ts b/server/tests/service.spec.ts index 769ca1a..c7d8519 100644 --- a/server/tests/service.spec.ts +++ b/server/tests/service.spec.ts @@ -6,12 +6,13 @@ import { acceptEvent } from '../src/validation.js' function event(experience: 'good' | 'mixed' | 'bad' = 'bad', eventId = crypto.randomUUID()) { return { - schemaVersion: 2, + schemaVersion: 3, type: 'feedback.signal', eventId, plugin: { moduleName: '@example/search', version: '1.0.0' }, health: 'ok', experience, + category: 'reliability', source: 'user_confirmed', } as const } @@ -24,7 +25,7 @@ function service(repository = new MemoryRepository(), publisher?: { publish: (cl }, publisher) } -describe('zero-content feedback flywheel service', () => { +describe('task-agnostic summary feedback flywheel service', () => { it('is idempotent and returns a report-scoped follow receipt', async () => { const api = service() const input = event() @@ -57,11 +58,13 @@ describe('zero-content feedback flywheel service', () => { const accepted = acceptEvent(event('bad')) expect(clusterKey({ ...accepted, experience: 'good' })).not.toBe(clusterKey(accepted)) expect(clusterKey({ ...accepted, health: 'error' })).not.toBe(clusterKey(accepted)) + expect(clusterKey({ ...accepted, category: 'performance' })).not.toBe(clusterKey(accepted)) }) it('rejects old telemetry, log-derived fields, free text and unknown fields fail closed', () => { const attempts = [ { ...event(), note: 'CANARY_SECRET' }, + { ...event(), summary: 'CANARY_PRIVATE_TASK' }, { ...event(), log: 'CANARY_SECRET' }, { ...event(), stack: '/Users/alice/private.ts' }, { ...event(), participantId: crypto.randomUUID() }, @@ -70,7 +73,8 @@ describe('zero-content feedback flywheel service', () => { { ...event(), environment: { platform: 'darwin' } }, ] for (const attempt of attempts) expect(() => acceptEvent(attempt)).toThrow('unsupported fields') - expect(() => acceptEvent({ ...event(), schemaVersion: 1 })).toThrow('unsupported event schema') + expect(() => acceptEvent({ ...event(), schemaVersion: 2 })).toThrow('unsupported event schema') + expect(() => acceptEvent({ ...event(), category: 'private-task-summary' })).toThrow('category is invalid') expect(() => acceptEvent({ ...event(), source: 'agent_inferred' })).toThrow('source is invalid') }) diff --git a/src/agent-tool.ts b/src/agent-tool.ts index 0c25a5e..ffb17a4 100644 --- a/src/agent-tool.ts +++ b/src/agent-tool.ts @@ -1,8 +1,15 @@ import type { Agent } from '@deepseek-ai/dsh-agent' import type { JsonSchemaNode, ToolDefinition } from '@deepseek-ai/dsh-tools' -import type { SafeExperienceAssessment } from './protocol.js' +import { + FEEDBACK_CATEGORIES, + type ExperienceVerdict, + type FeedbackCategory, + type FeedbackPreview, + type SafeExperienceAssessment, +} from './protocol.js' export const AGENT_ASSESSMENT_TOOL = 'omdsh_analyze_plugin_experience' +export const AGENT_PREVIEW_TOOL = 'omdsh_preview_plugin_feedback' const PARAMETERS = { type: 'object', @@ -16,9 +23,51 @@ const OUTPUT: JsonSchemaNode = { properties: { health: { type: 'string', enum: ['ok', 'unavailable', 'error', 'unknown'] }, experience: { type: 'string', const: 'unknown' }, + feedbackCategories: { + type: 'array', + items: { type: 'string', enum: [...FEEDBACK_CATEGORIES] }, + }, + summaryIsTemplateOnly: { type: 'boolean', const: true }, + userConfirmationRequired: { type: 'boolean', const: true }, + }, + required: [ + 'health', 'experience', 'feedbackCategories', 'summaryIsTemplateOnly', 'userConfirmationRequired', + ], + additionalProperties: false, +} + +const PREVIEW_PARAMETERS = { + type: 'object', + properties: { + experience: { type: 'string', enum: ['good', 'mixed', 'bad'] }, + category: { type: 'string', enum: [...FEEDBACK_CATEGORIES] }, + }, + required: ['experience', 'category'], + additionalProperties: false, +} + +const PREVIEW_OUTPUT: JsonSchemaNode = { + type: 'object', + properties: { + plugin: { + type: 'object', + properties: { + moduleName: { type: 'string' }, + version: { type: 'string' }, + }, + required: ['moduleName'], + additionalProperties: false, + }, + health: { type: 'string', enum: ['ok', 'unavailable', 'error', 'unknown'] }, + experience: { type: 'string', enum: ['good', 'mixed', 'bad'] }, + category: { type: 'string', enum: [...FEEDBACK_CATEGORIES] }, + summary: { type: 'string' }, + willUpload: { type: 'boolean', const: false }, userConfirmationRequired: { type: 'boolean', const: true }, }, - required: ['health', 'experience', 'userConfirmationRequired'], + required: [ + 'plugin', 'health', 'experience', 'category', 'summary', 'willUpload', 'userConfirmationRequired', + ], additionalProperties: false, } @@ -27,6 +76,22 @@ function noArguments(value: unknown): boolean { && Object.keys(value).length === 0 } +function previewArguments(value: unknown): { experience: ExperienceVerdict; category: FeedbackCategory } { + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + throw new TypeError('feedback preview accepts finite enums only') + } + const row = value as Record + if (Object.keys(row).sort().join(',') !== 'category,experience' + || !['good', 'mixed', 'bad'].includes(row.experience as string) + || !FEEDBACK_CATEGORIES.includes(row.category as FeedbackCategory)) { + throw new TypeError('feedback preview accepts finite enums only') + } + return { + experience: row.experience as ExperienceVerdict, + category: row.category as FeedbackCategory, + } +} + /** A raw ToolDefinition is used so the rc.6 input schema is closed as well as the output. */ export function createAgentAssessmentTool( assess: (agent: Agent | undefined) => SafeExperienceAssessment, @@ -36,6 +101,7 @@ export function createAgentAssessmentTool( description: [ 'Read only the current Plugin Lab trial lifecycle status from the DSH Host.', 'Call with an empty object. Never infer subjective quality from conversation content.', + 'You may suggest one task-agnostic category from feedbackCategories, but never include task details.', 'When experience is unknown, ask the user to confirm good, mixed, or bad.', ].join(' '), parameters: PARAMETERS, @@ -50,3 +116,34 @@ export function createAgentAssessmentTool( isConcurrencySafe: () => true, } } + +/** + * Pure preview tool: finite enum input, fixed-template output, and no storage or network side effect. + * The Agent can prepare the card, but only the user's separate confirmation can publish it. + */ +export function createAgentPreviewTool( + preview: ( + agent: Agent | undefined, + experience: ExperienceVerdict, + category: FeedbackCategory, + ) => FeedbackPreview, +): ToolDefinition { + return { + name: AGENT_PREVIEW_TOOL, + description: [ + 'Prepare a task-agnostic feedback preview from two finite enums.', + 'Do not include or paraphrase the user task, conversation, logs, paths, errors, prompts, or outputs.', + 'This tool never stores or uploads anything. Show the preview and ask the user to confirm separately.', + ].join(' '), + parameters: PREVIEW_PARAMETERS, + output: { + schema: PREVIEW_OUTPUT, + render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }], + }, + async execute(args, exec) { + const parsed = previewArguments(args) + return preview(exec.agent, parsed.experience, parsed.category) + }, + isConcurrencySafe: () => true, + } +} diff --git a/src/client/ExperienceResultCard.tsx b/src/client/ExperienceResultCard.tsx index 3b00d30..66386c8 100644 --- a/src/client/ExperienceResultCard.tsx +++ b/src/client/ExperienceResultCard.tsx @@ -2,12 +2,16 @@ import { useState, type CSSProperties } from 'react' import type { MessageId } from '@deepseek-ai/dsh-client-connection/client' import type { ConversationNode } from '@deepseek-ai/dsh-client-runtime/client' import type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' -import type { ExperienceVerdict } from '../protocol.js' +import type { ExperienceVerdict, FeedbackCategory } from '../protocol.js' import type { LabController, LabView } from './controller.js' export interface LabInjected { hooks: { pluginLab: LabController } - record: (messageId: MessageId, verdict: ExperienceVerdict) => Promise + record: ( + messageId: MessageId, + verdict: ExperienceVerdict, + category: FeedbackCategory, + ) => Promise join: () => Promise dismiss: () => void } @@ -40,6 +44,17 @@ function shareLabel(verdict: ExperienceVerdict): string { return '加入并等待修复' } +const CATEGORY_CHOICES: ReadonlyArray<{ value: FeedbackCategory; label: string }> = [ + { value: 'installation', label: '安装' }, + { value: 'startup', label: '启动' }, + { value: 'invocation', label: '调用' }, + { value: 'compatibility', label: '兼容性' }, + { value: 'reliability', label: '稳定性' }, + { value: 'performance', label: '性能' }, + { value: 'result_quality', label: '结果质量' }, + { value: 'general', label: '整体体验' }, +] + /** Latest durable assistant identity from the rc.6 conversation projection. */ export function latestAssistantMessageId(nodes: readonly ConversationNode[]): MessageId | undefined { for (let index = nodes.length - 1; index >= 0; index -= 1) { @@ -60,6 +75,7 @@ export function ExperienceResultCard({ const view = usePluginLab(value => value) const latestMessageId = useSession(snapshot => latestAssistantMessageId(snapshot.nodes)) const [open, setOpen] = useState(false) + const [selectedVerdict, setSelectedVerdict] = useState() if (!visible(view, messageId, latestMessageId)) return null const pending = view.pending?.messageId === messageId ? view.pending : undefined const busy = pending?.phase === 'saving' || pending?.phase === 'joining' @@ -70,15 +86,32 @@ export function ExperienceResultCard({ {open && ( - 你觉得这个插件好用吗? + + {selectedVerdict === undefined ? '你觉得这个插件好用吗?' : '选择一个不涉及任务内容的大类'} + - Agent 只知道运行状态,不会读取会话或日志替你判断。 + Agent 可以建议有限大类,但不会把当前任务、会话或日志写进摘要。 - {pending === undefined && ( + {pending === undefined && selectedVerdict === undefined && ( - - - + + + + + )} + {pending === undefined && selectedVerdict !== undefined && ( + + {CATEGORY_CHOICES.map(choice => ( + + ))} + )} {pending !== undefined && ( @@ -88,16 +121,20 @@ export function ExperienceResultCard({ {pending.phase === 'local' && ( )} {(pending.phase === 'joined' || pending.phase === 'error') && ( - + )} )} - 第一步只保存在本机;加入跟进只发送插件、版本、状态枚举和你的选择。网络传输并非绝对匿名。 + 提交前会显示完整脱敏预览;只发送插件、版本、状态、体验和大类枚举。网络传输并非绝对匿名。 )} diff --git a/src/client/controller.ts b/src/client/controller.ts index a1b7bb9..8d8b7cb 100644 --- a/src/client/controller.ts +++ b/src/client/controller.ts @@ -3,7 +3,7 @@ import type { ClientRemote } from '@deepseek-ai/dsh-api-remotes/client' import type { MessageId } from '@deepseek-ai/dsh-client-connection/client' import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client' import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots' -import type { ExperienceVerdict } from '../protocol.js' +import type { ExperienceVerdict, FeedbackCategory } from '../protocol.js' /** Exact rc.6 generated command Remote surface, kept narrow for testability. */ export type CommandsRemote = Pick @@ -11,6 +11,7 @@ export type CommandsRemote = Pick export interface PendingResult { readonly messageId: MessageId readonly verdict: ExperienceVerdict + readonly category: FeedbackCategory readonly phase: 'saving' | 'local' | 'joining' | 'joined' | 'error' readonly text?: string } @@ -47,17 +48,17 @@ export class LabController implements HostObservable { this.publish({ ...this.view, active }) } - async record(messageId: MessageId, verdict: ExperienceVerdict): Promise { - this.publish({ ...this.view, pending: { messageId, verdict, phase: 'saving' } }) - const settled = await this.execute(`/omdsh-result ${verdict}`) + async record(messageId: MessageId, verdict: ExperienceVerdict, category: FeedbackCategory): Promise { + this.publish({ ...this.view, pending: { messageId, verdict, category, phase: 'saving' } }) + const settled = await this.execute(`/omdsh-result ${verdict} ${category}`) if (settled.ok) { this.publish({ ...this.view, active: false, - pending: { messageId, verdict, phase: 'local', text: settled.text }, + pending: { messageId, verdict, category, phase: 'local', text: settled.text }, }) } else { - this.publish({ ...this.view, pending: { messageId, verdict, phase: 'error', text: settled.text } }) + this.publish({ ...this.view, pending: { messageId, verdict, category, phase: 'error', text: settled.text } }) } } diff --git a/src/client/index.ts b/src/client/index.ts index e109246..b156518 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -40,7 +40,7 @@ export function apply(ctx: ClientContext): void { const controller = controllerFor(sessionId) return { hooks: { pluginLab: controller }, - record: (messageId, outcome) => controller.record(messageId, outcome), + record: (messageId, outcome, category) => controller.record(messageId, outcome, category), join: () => controller.join(), dismiss: () => controller.dismiss(), } diff --git a/src/index.ts b/src/index.ts index 0cf60c6..11555dd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,18 +1,21 @@ -/** Zero-content plugin health and user-confirmed feedback loop for DeepSeek Harness. */ +/** Task-agnostic plugin health and user-confirmed feedback loop for DeepSeek Harness. */ import type { Context } from '@deepseek-ai/cordis' import type { CommandInvocation, CommandResult } from '@deepseek-ai/dsh-commands' import z from '@deepseek-ai/schemastery' -import { createAgentAssessmentTool } from './agent-tool.js' +import { createAgentAssessmentTool, createAgentPreviewTool } from './agent-tool.js' import { healthText, probeLoaderHealth, type LoaderLike } from './health.js' import { - JOIN_USAGE, parseJoinTarget, parseReceiptId, parseStartInput, parseVerdict, + JOIN_USAGE, parseJoinTarget, parseReceiptId, parseResultInput, parseStartInput, RESULT_USAGE, RETEST_USAGE, START_USAGE, } from './input.js' import { FEEDBACK_SCHEMA_VERSION, type ExperienceVerdict, - type FeedbackEventV2, + FEEDBACK_CATEGORIES, + type FeedbackCategory, + type FeedbackEventV3, + type FeedbackPreview, type HealthStatus, type IngestReceipt, type LocalFeedbackRecord, @@ -20,6 +23,7 @@ import { type TrialPluginRef, } from './protocol.js' import { FeedbackStore } from './storage.js' +import { fixedSummary, renderUploadPreview } from './summary.js' import { ExperienceUploader } from './uploader.js' export const name = 'omdsh-plugin-lab' @@ -73,13 +77,13 @@ function health(ctx: Context, plugin: TrialPluginRef): HealthStatus { } function assessment(status: HealthStatus): SafeExperienceAssessment { - return { health: status, experience: 'unknown', userConfirmationRequired: true } -} - -function verdictText(verdict: ExperienceVerdict): string { - if (verdict === 'good') return '好用' - if (verdict === 'mixed') return '一般' - return '不好用' + return { + health: status, + experience: 'unknown', + feedbackCategories: FEEDBACK_CATEGORIES, + summaryIsTemplateOnly: true, + userConfirmationRequired: true, + } } function renderReceipt(receipt: IngestReceipt): string[] { @@ -138,10 +142,32 @@ export function apply(ctx: Context, rawConfig: Config): void { return assessment(state === undefined ? 'unknown' : health(ctx, state.plugin)) } + const previewTrial = ( + key: string | undefined, + experience: ExperienceVerdict, + category: FeedbackCategory, + ): FeedbackPreview => { + const state = key === undefined ? undefined : trials.get(key) + if (state === undefined) throw new TypeError('no active Plugin Lab trial') + const status = health(ctx, state.plugin) + return { + plugin: state.plugin, + health: status, + experience, + category, + summary: fixedSummary(state.plugin, status, experience, category), + willUpload: false, + userConfirmationRequired: true, + } + } + // Optional capability: headless command-only tests still work, while a normal // rc.6 Agent runtime receives the closed, zero-argument assessment tool. ctx.inject(['tools'], toolCtx => { toolCtx.tools.register(createAgentAssessmentTool(agent => assessTrial(agentSessionKey(agent)))) + toolCtx.tools.register(createAgentPreviewTool((agent, experience, category) => ( + previewTrial(agentSessionKey(agent), experience, category) + ))) }) const beginTrial = ( @@ -163,7 +189,7 @@ export function apply(ctx: Context, rawConfig: Config): void { text: [ `试用已开始:${input.plugin.moduleName}${input.plugin.version === undefined ? '' : `#${input.plugin.version}`}`, `运行状态:${healthText(health(ctx, input.plugin))}`, - '完成后由你选择“好用 / 一般 / 不好用”;Agent 不会读取会话或日志替你判断。', + '完成后由你选择体验和一个脱敏大类;Agent 只能用有限枚举生成固定模板预览。', ].join('\n'), } } @@ -196,7 +222,7 @@ export function apply(ctx: Context, rawConfig: Config): void { : [ `插件:${state.plugin.moduleName}`, `运行状态:${healthText(result.health)}`, - '主观体验:未确认。探活没有读取日志、会话、异常或文件。', + '主观体验:未确认。探活没有读取日志、会话、异常或文件。Agent 可建议大类,但不能提交任务摘要。', ].join('\n'), } } @@ -206,18 +232,22 @@ export function apply(ctx: Context, rawConfig: Config): void { const state = trials.get(key) if (state === undefined) return { kind: 'error', text: `没有进行中的插件试用。先运行 ${START_USAGE}` } let verdict: ExperienceVerdict + let category: FeedbackCategory try { - verdict = parseVerdict(invocation.rawInput) + const parsed = parseResultInput(invocation.rawInput) + verdict = parsed.verdict + category = parsed.category } catch { return { kind: 'error', text: RESULT_USAGE } } - const event: FeedbackEventV2 = { + const event: FeedbackEventV3 = { schemaVersion: FEEDBACK_SCHEMA_VERSION, type: 'feedback.signal', eventId: crypto.randomUUID(), plugin: state.plugin, health: health(ctx, state.plugin), experience: verdict, + category, source: 'user_confirmed', ...state.retestOfReceiptId === undefined ? {} : { retestOfReceiptId: state.retestOfReceiptId }, } @@ -226,11 +256,9 @@ export function apply(ctx: Context, rawConfig: Config): void { return { kind: 'success', text: [ - `运行状态:${healthText(event.health)}`, - `主观体验:${verdictText(verdict)}(由你确认)`, + ...renderUploadPreview(event), `已只保存到本机:${store.eventsPath}`, - '记录只有公开插件 ID/版本、状态枚举和你的选择;不含日志、会话、时间、环境或稳定身份。', - '运行 /omdsh-join latest 才会发送屏幕上这组有限字段。', + '请检查以上预览;只有运行 /omdsh-join latest 才会发送这组有限字段。', ].join('\n'), } } @@ -298,8 +326,8 @@ export function apply(ctx: Context, rawConfig: Config): void { text: [ `结构化分享:${uploader === undefined ? '未启用' : '只能由用户逐次运行 /omdsh-join 触发'}`, '探活:仅本地读取 DSH Host 的 Loader/Fiber 生命周期枚举,不访问插件对象或网络。', - 'Agent:零参数工具只返回 health、experience=unknown、userConfirmationRequired=true。', - '可发送字段:schemaVersion、type、随机单次 eventId、公开插件 ID/版本、health、experience、source。', + 'Agent:探活工具零参数;预览工具只接受体验和大类枚举,固定模板预览不会存储或发送。', + '可发送字段:schemaVersion、type、随机单次 eventId、公开插件 ID/版本、health、experience、category、source。', '绝不读取或发送:日志、异常、堆栈、崩溃指纹、Prompt、回复、Tool 数据、文件、路径、环境、时间、用户/设备/安装/Session ID。', '网络仍会自然暴露传输元数据,因此本插件不宣称匿名;服务端必须禁止持久化 IP、User-Agent 和请求体日志。', `本地最小化数据:${store.dataDir}`, @@ -321,15 +349,15 @@ export function apply(ctx: Context, rawConfig: Config): void { }) ctx.commands.register({ name: 'omdsh-result', - description: '由用户确认主观体验;只保存在本机', - input: { hint: '' }, + description: '由用户确认体验与脱敏大类,生成本地上传预览', + input: { hint: ` <${FEEDBACK_CATEGORIES.join('|')}>` }, recordInput: false, handler: submitResult, }) ctx.commands.register({ name: 'omdsh-feedback', - description: '兼容入口:由用户确认主观体验;只保存在本机', - input: { hint: '' }, + description: '兼容入口:由用户确认体验与脱敏大类,生成本地上传预览', + input: { hint: ` <${FEEDBACK_CATEGORIES.join('|')}>` }, recordInput: false, handler: submitResult, }) diff --git a/src/input.ts b/src/input.ts index 3a4e0bc..205a9c7 100644 --- a/src/input.ts +++ b/src/input.ts @@ -1,4 +1,9 @@ -import type { ExperienceVerdict, TrialPluginRef } from './protocol.js' +import { + FEEDBACK_CATEGORIES, + type ExperienceVerdict, + type FeedbackCategory, + type TrialPluginRef, +} from './protocol.js' export interface StartInput { readonly plugin: TrialPluginRef @@ -8,9 +13,10 @@ const MODULE = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u const VERSION = /^[0-9A-Za-z][0-9A-Za-z.+_-]{0,63}$/u const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu const VERDICTS = new Set(['good', 'mixed', 'bad']) +const CATEGORIES = new Set(FEEDBACK_CATEGORIES) export const START_USAGE = 'Usage: /omdsh-start [#version]' -export const RESULT_USAGE = 'Usage: /omdsh-result ' +export const RESULT_USAGE = `Usage: /omdsh-result <${FEEDBACK_CATEGORIES.join('|')}>` export const JOIN_USAGE = 'Usage: /omdsh-join ' export const RETEST_USAGE = 'Usage: /omdsh-retest [#version]' @@ -27,10 +33,16 @@ export function parseStartInput(rawInput: string): StartInput { return { plugin: { moduleName, ...version === undefined ? {} : { version } } } } -export function parseVerdict(rawInput: string): ExperienceVerdict { - const value = rawInput.trim() - if (!VERDICTS.has(value as ExperienceVerdict)) throw new TypeError(RESULT_USAGE) - return value as ExperienceVerdict +export function parseResultInput(rawInput: string): { + readonly verdict: ExperienceVerdict + readonly category: FeedbackCategory +} { + const parts = rawInput.trim().split(/\s+/u).filter(Boolean) + if (parts.length !== 2) throw new TypeError(RESULT_USAGE) + const [verdict, category] = parts + if (!VERDICTS.has(verdict as ExperienceVerdict) + || !CATEGORIES.has(category as FeedbackCategory)) throw new TypeError(RESULT_USAGE) + return { verdict: verdict as ExperienceVerdict, category: category as FeedbackCategory } } export function parseJoinTarget(rawInput: string): string { diff --git a/src/protocol.ts b/src/protocol.ts index 31e66b2..04af757 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -1,9 +1,20 @@ -/** Closed, zero-content protocol shared by the DSH plugin and ingest backend. */ +/** Closed, task-agnostic summary protocol shared by the DSH plugin and ingest backend. */ -export const FEEDBACK_SCHEMA_VERSION = 2 as const +export const FEEDBACK_SCHEMA_VERSION = 3 as const export type HealthStatus = 'ok' | 'unavailable' | 'error' | 'unknown' export type ExperienceVerdict = 'good' | 'mixed' | 'bad' +export const FEEDBACK_CATEGORIES = [ + 'installation', + 'startup', + 'invocation', + 'compatibility', + 'reliability', + 'performance', + 'result_quality', + 'general', +] as const +export type FeedbackCategory = typeof FEEDBACK_CATEGORIES[number] export interface TrialPluginRef { /** Public DSH marketplace/package identifier; never a local path or user label. */ @@ -15,6 +26,19 @@ export interface TrialPluginRef { export interface SafeExperienceAssessment { readonly health: HealthStatus readonly experience: 'unknown' + readonly feedbackCategories: readonly FeedbackCategory[] + readonly summaryIsTemplateOnly: true + readonly userConfirmationRequired: true +} + +/** Preview generated only from public plugin coordinates and finite enums. */ +export interface FeedbackPreview { + readonly plugin: TrialPluginRef + readonly health: HealthStatus + readonly experience: ExperienceVerdict + readonly category: FeedbackCategory + readonly summary: string + readonly willUpload: false readonly userConfirmationRequired: true } @@ -22,7 +46,7 @@ export interface SafeExperienceAssessment { * Exact upload envelope. It intentionally contains no timestamp, stable user or * install identifier, task/session identifier, metrics, notes, error or log data. */ -export interface FeedbackEventV2 { +export interface FeedbackEventV3 { readonly schemaVersion: typeof FEEDBACK_SCHEMA_VERSION readonly type: 'feedback.signal' /** Random per-report id used only for idempotency. */ @@ -30,13 +54,15 @@ export interface FeedbackEventV2 { readonly plugin: TrialPluginRef readonly health: HealthStatus readonly experience: ExperienceVerdict + /** Task-agnostic category selected from the closed vocabulary above. */ + readonly category: FeedbackCategory readonly source: 'user_confirmed' /** Optional, report-scoped link used only when the user explicitly starts a retest. */ readonly retestOfReceiptId?: string } export interface LocalFeedbackRecord { - readonly event: FeedbackEventV2 + readonly event: FeedbackEventV3 readonly requestedShare: boolean } @@ -75,6 +101,6 @@ export interface ReceiptSeen { } /** The local record is already the exact network packet; no projection can add fields. */ -export function uploadPayload(record: LocalFeedbackRecord): FeedbackEventV2 { +export function uploadPayload(record: LocalFeedbackRecord): FeedbackEventV3 { return record.event } diff --git a/src/storage.ts b/src/storage.ts index 8986c9a..b9a4af6 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -46,10 +46,10 @@ export class FeedbackStore { constructor(dataDir = defaultDataDir()) { if (!isAbsolute(dataDir)) throw new TypeError('plugin-lab: dataDir must be an absolute path') this.dataDir = dataDir - this.eventsPath = join(dataDir, 'feedback-v2.ndjson') - this.receiptsPath = join(dataDir, 'receipts-v2.ndjson') - this.shareRequestsPath = join(dataDir, 'share-requests-v2.ndjson') - this.receiptSeenPath = join(dataDir, 'receipt-seen-v2.ndjson') + this.eventsPath = join(dataDir, 'feedback-v3.ndjson') + this.receiptsPath = join(dataDir, 'receipts-v3.ndjson') + this.shareRequestsPath = join(dataDir, 'share-requests-v3.ndjson') + this.receiptSeenPath = join(dataDir, 'receipt-seen-v3.ndjson') mkdirSync(dataDir, { recursive: true, mode: 0o700 }) chmodSync(dataDir, 0o700) } diff --git a/src/summary.ts b/src/summary.ts new file mode 100644 index 0000000..90acdd6 --- /dev/null +++ b/src/summary.ts @@ -0,0 +1,61 @@ +import type { + ExperienceVerdict, FeedbackCategory, FeedbackEventV3, HealthStatus, TrialPluginRef, +} from './protocol.js' + +const CATEGORY_TEXT: Record = { + installation: '安装', + startup: '启动', + invocation: '调用', + compatibility: '兼容性', + reliability: '稳定性', + performance: '性能', + result_quality: '结果质量', + general: '整体体验', +} + +const HEALTH_TEXT: Record = { + ok: '运行正常', + unavailable: '当前不可用', + error: '运行错误', + unknown: '状态未知', +} + +const VERDICT_TEXT: Record = { + good: '好用', + mixed: '一般', + bad: '不好用', +} + +export function categoryText(category: FeedbackCategory): string { + return CATEGORY_TEXT[category] +} + +export function verdictText(verdict: ExperienceVerdict): string { + return VERDICT_TEXT[verdict] +} + +export function fixedSummary( + plugin: TrialPluginRef, + health: HealthStatus, + experience: ExperienceVerdict, + category: FeedbackCategory, +): string { + const coordinate = `${plugin.moduleName}${plugin.version === undefined ? '' : `#${plugin.version}`}` + return `${coordinate} 在“${CATEGORY_TEXT[category]}”方面:${HEALTH_TEXT[health]},用户体验为“${VERDICT_TEXT[experience]}”。` +} + +/** Every readable preview line is derived from the exact closed upload packet. */ +export function renderUploadPreview(event: FeedbackEventV3): string[] { + return [ + '待确认的脱敏摘要(尚未发送):', + `摘要:${fixedSummary(event.plugin, event.health, event.experience, event.category)}`, + `插件:${event.plugin.moduleName}`, + `版本:${event.plugin.version ?? '未提供'}`, + `运行状态:${HEALTH_TEXT[event.health]}`, + `体验大类:${CATEGORY_TEXT[event.category]}(${event.category})`, + `主观体验:${VERDICT_TEXT[event.experience]}(由你确认)`, + `单次报告 ID:${event.eventId}`, + ...event.retestOfReceiptId === undefined ? [] : [`复测回执 ID:${event.retestOfReceiptId}`], + '不会上传:当前任务、对话、Prompt、回复、日志、报错详情、堆栈、文件、路径、环境或身份信息。', + ] +} diff --git a/tests/client-components.spec.tsx b/tests/client-components.spec.tsx index df98d39..fd253f2 100644 --- a/tests/client-components.spec.tsx +++ b/tests/client-components.spec.tsx @@ -37,7 +37,7 @@ function propsFor(controller: LabController, messageId: MessageId): ExperienceRe sessionId: SESSION, useSession: sessionHook, usePluginLab, - record: (id, outcome) => controller.record(id, outcome), + record: (id, outcome, category) => controller.record(id, outcome, category), join: () => controller.join(), dismiss: () => { controller.dismiss() }, } as ExperienceResultCardProps @@ -68,10 +68,11 @@ describe('rc.6 client components', () => { rendered.rerender() fireEvent.click(screen.getByRole('button', { name: '体验结果' })) fireEvent.click(screen.getByRole('button', { name: '不好用' })) + fireEvent.click(screen.getByRole('button', { name: '稳定性' })) await screen.findByText('已只保存在本机') - expect(execute).toHaveBeenLastCalledWith(SESSION, '/omdsh-result bad') + expect(execute).toHaveBeenLastCalledWith(SESSION, '/omdsh-result bad reliability') - fireEvent.click(screen.getByRole('button', { name: '加入并等待修复' })) + fireEvent.click(screen.getByRole('button', { name: '确认并提交:加入并等待修复' })) await screen.findByText('问题回执:PL-RC6') expect(execute).toHaveBeenLastCalledWith(SESSION, '/omdsh-join latest') expect(screen.getByRole('button', { name: '完成' })).toBeDefined() diff --git a/tests/client-controller.spec.ts b/tests/client-controller.spec.ts index db5e120..4dd8e0a 100644 --- a/tests/client-controller.spec.ts +++ b/tests/client-controller.spec.ts @@ -19,11 +19,11 @@ describe('result-card controller', () => { controller.setTrialActive(true) expect(controller.getSnapshot()).toMatchObject({ active: true }) - await controller.record('latest' as MessageId, 'bad') - expect(execute).toHaveBeenLastCalledWith('session', '/omdsh-result bad') + await controller.record('latest' as MessageId, 'bad', 'reliability') + expect(execute).toHaveBeenLastCalledWith('session', '/omdsh-result bad reliability') expect(controller.getSnapshot()).toMatchObject({ active: false, - pending: { verdict: 'bad', phase: 'local' }, + pending: { verdict: 'bad', category: 'reliability', phase: 'local' }, }) await controller.join() @@ -40,7 +40,7 @@ describe('result-card controller', () => { })) const controller = new LabController({ execute: failed }, 'session' as SessionId) controller.setTrialActive(true) - await controller.record('message' as MessageId, 'mixed') + await controller.record('message' as MessageId, 'mixed', 'performance') expect(controller.getSnapshot().pending).toMatchObject({ phase: 'error', text: '连接已断开 (disconnected)', }) @@ -48,7 +48,7 @@ describe('result-card controller', () => { const absent: CommandsRemote['execute'] = vi.fn(async () => ({ ok: true as const, value: undefined })) const second = new LabController({ execute: absent }, 'session' as SessionId) second.setTrialActive(true) - await second.record('message' as MessageId, 'bad') + await second.record('message' as MessageId, 'bad', 'general') expect(second.getSnapshot().pending).toMatchObject({ phase: 'error', text: '命令未被 DSH 接收。', }) diff --git a/tests/input.spec.ts b/tests/input.spec.ts index b95743c..8cb7700 100644 --- a/tests/input.spec.ts +++ b/tests/input.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { parseJoinTarget, parseReceiptId, parseStartInput, parseVerdict } from '../src/input.js' +import { parseJoinTarget, parseReceiptId, parseResultInput, parseStartInput } from '../src/input.js' describe('closed command inputs', () => { it('accepts only a public module and optional bounded version', () => { @@ -16,12 +16,13 @@ describe('closed command inputs', () => { expect(() => parseStartInput(' ')).toThrow('/omdsh-start') }) - it('accepts only finite user-confirmed verdicts with no notes or flags', () => { - expect(parseVerdict('good')).toBe('good') - expect(parseVerdict('mixed')).toBe('mixed') - expect(parseVerdict('bad')).toBe('bad') - expect(() => parseVerdict('bad secret-note')).toThrow('/omdsh-result') - expect(() => parseVerdict('worked')).toThrow('/omdsh-result') + it('accepts only finite verdict and task-agnostic category alphabets', () => { + expect(parseResultInput('good installation')).toEqual({ verdict: 'good', category: 'installation' }) + expect(parseResultInput('mixed result_quality')).toEqual({ verdict: 'mixed', category: 'result_quality' }) + expect(parseResultInput('bad general')).toEqual({ verdict: 'bad', category: 'general' }) + expect(() => parseResultInput('bad secret-note')).toThrow('/omdsh-result') + expect(() => parseResultInput('bad reliability private-task')).toThrow('/omdsh-result') + expect(() => parseResultInput('worked general')).toThrow('/omdsh-result') }) it('accepts one join target and rejects extra arguments', () => { diff --git a/tests/plugin.spec.ts b/tests/plugin.spec.ts index 1df09e4..69f7f84 100644 --- a/tests/plugin.spec.ts +++ b/tests/plugin.spec.ts @@ -20,7 +20,7 @@ async function runtime(config: Parameters[1]) { } describe('DSH strict feedback integration', () => { - it('registers one-click health and records only the closed v2 packet', async () => { + it('registers one-click health and records only the closed v3 category packet', async () => { const root = mkdtempSync(join(tmpdir(), 'omdsh-plugin-lab-')) const dataDir = join(root, 'data') const { ctx, fiber, agent, signal } = await runtime({ dataDir }) @@ -37,24 +37,26 @@ describe('DSH strict feedback integration', () => { expect(health?.result.text).toContain('暂时无法判断') expect(health?.result.text).toContain('没有读取日志') - const feedback = await ctx.commands.execute(agent, '/omdsh-result bad', signal) + const feedback = await ctx.commands.execute(agent, '/omdsh-result bad reliability', signal) expect(feedback?.result.text).toContain('不好用(由你确认)') - expect(feedback?.result.text).toContain('不含日志') - const stored = JSON.parse(readFileSync(join(dataDir, 'feedback-v2.ndjson'), 'utf8').trim()) + expect(feedback?.result.text).toContain('不会上传:当前任务') + expect(feedback?.result.text).toContain('稳定性(reliability)') + const stored = JSON.parse(readFileSync(join(dataDir, 'feedback-v3.ndjson'), 'utf8').trim()) expect(stored).toEqual({ event: { - schemaVersion: 2, + schemaVersion: 3, type: 'feedback.signal', eventId: expect.any(String), plugin: { moduleName: '@example/plugin', version: '1.0.0' }, health: 'unknown', experience: 'bad', + category: 'reliability', source: 'user_confirmed', }, requestedShare: false, }) const forbidden = [ - 'participantId', 'occurredAt', 'taskId', 'environment', 'signals', 'note', + 'participantId', 'occurredAt', 'taskId', 'environment', 'signals', 'note', 'summary', 'message', 'stack', 'crash', 'session', 'locale', 'platform', 'path', ] for (const key of forbidden) expect(stored.event).not.toHaveProperty(key) @@ -70,7 +72,7 @@ describe('DSH strict feedback integration', () => { retryIntervalMs: 60_000, }) await ctx.commands.execute(agent, '/omdsh-start plugin', signal) - const feedback = await ctx.commands.execute(agent, '/omdsh-result good', signal) + const feedback = await ctx.commands.execute(agent, '/omdsh-result good general', signal) expect(feedback?.result.text).toContain('已只保存到本机') await fiber.dispose() }) @@ -98,21 +100,22 @@ describe('DSH strict feedback integration', () => { retryIntervalMs: 60_000, }) await ctx.commands.execute(agent, '/omdsh-start plugin#1.0.0', signal) - await ctx.commands.execute(agent, '/omdsh-result mixed', signal) + await ctx.commands.execute(agent, '/omdsh-result mixed compatibility', signal) expect(posts).toBe(0) const joined = await ctx.commands.execute(agent, '/omdsh-join latest', signal) expect(joined?.result.text).toContain('PL-STRICT') expect(posts).toBe(1) const packet = JSON.parse(body) as Record expect(Object.keys(packet).sort()).toEqual([ - 'eventId', 'experience', 'health', 'plugin', 'schemaVersion', 'source', 'type', + 'category', 'eventId', 'experience', 'health', 'plugin', 'schemaVersion', 'source', 'type', ]) expect(packet).toMatchObject({ - schemaVersion: 2, + schemaVersion: 3, type: 'feedback.signal', plugin: { moduleName: 'plugin', version: '1.0.0' }, health: 'unknown', experience: 'mixed', + category: 'compatibility', source: 'user_confirmed', }) await fiber.dispose() @@ -125,7 +128,7 @@ describe('DSH strict feedback integration', () => { const start = await ctx.commands.execute(agent, '/omdsh-start plugin private-task-label', signal) expect(start?.result.kind).toBe('error') await ctx.commands.execute(agent, '/omdsh-start plugin', signal) - const result = await ctx.commands.execute(agent, '/omdsh-result bad secret-note', signal) + const result = await ctx.commands.execute(agent, '/omdsh-result bad reliability secret-note', signal) expect(result?.result.kind).toBe('error') await fiber.dispose() }) @@ -141,8 +144,8 @@ describe('DSH strict feedback integration', () => { step: 1, message: { role: 'assistant', content: [{ type: 'text', text: 'CANARY_SECRET_TOKEN' }] } as never, }, { surfaceOp: 'append' }) - await ctx.commands.execute(agent, '/omdsh-result good', signal) - const stored = readFileSync(join(root, 'data', 'feedback-v2.ndjson'), 'utf8') + await ctx.commands.execute(agent, '/omdsh-result good result_quality', signal) + const stored = readFileSync(join(root, 'data', 'feedback-v3.ndjson'), 'utf8') expect(stored).not.toContain('CANARY_SECRET_TOKEN') await fiber.dispose() expect(process.listeners('uncaughtExceptionMonitor')).toEqual(before) diff --git a/tests/privacy-invariants.spec.ts b/tests/privacy-invariants.spec.ts index ec588a4..a23d10e 100644 --- a/tests/privacy-invariants.spec.ts +++ b/tests/privacy-invariants.spec.ts @@ -1,28 +1,32 @@ import type { Agent } from '@deepseek-ai/dsh-agent' import { describe, expect, it } from 'vitest' -import { createAgentAssessmentTool } from '../src/agent-tool.js' +import { createAgentAssessmentTool, createAgentPreviewTool } from '../src/agent-tool.js' import { probeLoaderHealth } from '../src/health.js' -import { uploadPayload, type FeedbackEventV2, type LocalFeedbackRecord } from '../src/protocol.js' +import { + FEEDBACK_CATEGORIES, uploadPayload, type FeedbackEventV3, type LocalFeedbackRecord, +} from '../src/protocol.js' +import { fixedSummary } from '../src/summary.js' -function event(eventId = crypto.randomUUID()): FeedbackEventV2 { +function event(eventId = crypto.randomUUID()): FeedbackEventV3 { return { - schemaVersion: 2, + schemaVersion: 3, type: 'feedback.signal', eventId, plugin: { moduleName: '@example/plugin', version: '1.0.0' }, health: 'ok', experience: 'good', + category: 'general', source: 'user_confirmed', } } -describe('zero-content privacy invariants', () => { +describe('task-agnostic summary privacy invariants', () => { it('uses the local packet byte-for-byte as the upload packet', () => { const packet = event() const record: LocalFeedbackRecord = { event: packet, requestedShare: true } expect(uploadPayload(record)).toBe(packet) expect(Object.keys(uploadPayload(record)).sort()).toEqual([ - 'eventId', 'experience', 'health', 'plugin', 'schemaVersion', 'source', 'type', + 'category', 'eventId', 'experience', 'health', 'plugin', 'schemaVersion', 'source', 'type', ]) }) @@ -41,7 +45,8 @@ describe('zero-content privacy invariants', () => { it('gives the Agent a closed zero-argument and closed enum-only contract', async () => { const tool = createAgentAssessmentTool(() => ({ - health: 'ok', experience: 'unknown', userConfirmationRequired: true, + health: 'ok', experience: 'unknown', feedbackCategories: FEEDBACK_CATEGORIES, + summaryIsTemplateOnly: true, userConfirmationRequired: true, })) expect(tool.parameters).toEqual({ type: 'object', properties: {}, required: [], additionalProperties: false, @@ -51,7 +56,10 @@ describe('zero-content privacy invariants', () => { agent: undefined, arguments: {}, } as never) - expect(result).toEqual({ health: 'ok', experience: 'unknown', userConfirmationRequired: true }) + expect(result).toEqual({ + health: 'ok', experience: 'unknown', feedbackCategories: FEEDBACK_CATEGORIES, + summaryIsTemplateOnly: true, userConfirmationRequired: true, + }) await expect(tool.execute({ reason: 'CANARY_SECRET' }, { agent: undefined, arguments: { reason: 'CANARY_SECRET' }, @@ -60,12 +68,35 @@ describe('zero-content privacy invariants', () => { it('cannot derive subjective experience when no user verdict exists', () => { const tool = createAgentAssessmentTool((_agent: Agent | undefined) => ({ - health: 'error', experience: 'unknown', userConfirmationRequired: true, + health: 'error', experience: 'unknown', feedbackCategories: FEEDBACK_CATEGORIES, + summaryIsTemplateOnly: true, userConfirmationRequired: true, })) expect(JSON.stringify(tool.output.schema)).not.toContain('good') expect(JSON.stringify(tool.output.schema)).not.toContain('bad') }) + it('lets the Agent preview finite categories without accepting or uploading task text', async () => { + const plugin = { moduleName: '@example/plugin', version: '1.0.0' } + const tool = createAgentPreviewTool((_agent, experience, category) => ({ + plugin, + health: 'error', + experience, + category, + summary: fixedSummary(plugin, 'error', experience, category), + willUpload: false, + userConfirmationRequired: true, + })) + const result = await tool.execute({ experience: 'bad', category: 'reliability' }, { + agent: undefined, arguments: {}, + } as never) + expect(result).toMatchObject({ + category: 'reliability', willUpload: false, userConfirmationRequired: true, + }) + await expect(tool.execute({ + experience: 'bad', category: 'reliability', summary: 'CANARY_PRIVATE_TASK', + }, { agent: undefined, arguments: {} } as never)).rejects.toThrow('finite enums only') + }) + it('satisfies content non-interference for arbitrary private canaries', () => { const firstPrivateWorld = 'prompt=ALICE_SECRET; stack=/Users/alice/private.ts' const secondPrivateWorld = 'prompt=BOB_SECRET; token=sk-private' diff --git a/tests/storage-uploader.spec.ts b/tests/storage-uploader.spec.ts index 33d088d..c84a4a2 100644 --- a/tests/storage-uploader.spec.ts +++ b/tests/storage-uploader.spec.ts @@ -3,7 +3,7 @@ import { mkdtempSync, readFileSync, statSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' -import type { FeedbackEventV2, LocalFeedbackRecord } from '../src/protocol.js' +import type { FeedbackEventV3, LocalFeedbackRecord } from '../src/protocol.js' import { FeedbackStore } from '../src/storage.js' import { ExperienceUploader } from '../src/uploader.js' @@ -14,13 +14,14 @@ afterEach(async () => { }) function record(): LocalFeedbackRecord { - const event: FeedbackEventV2 = { - schemaVersion: 2, + const event: FeedbackEventV3 = { + schemaVersion: 3, type: 'feedback.signal', eventId: crypto.randomUUID(), plugin: { moduleName: 'plugin', version: '1.0.0' }, health: 'error', experience: 'bad', + category: 'reliability', source: 'user_confirmed', } return { event, requestedShare: false } @@ -71,7 +72,7 @@ describe('strict local outbox and uploader', () => { response.end(JSON.stringify({ receiptId: 'receipt-1', status: 'clustered', updatedAt: 1, similarReports: 7, - recommendedVersion: '0.3.0', + recommendedVersion: '0.4.0', followToken: 'follow-secret', updatesUrl: `http://127.0.0.1:${address.port}/v1/receipts/receipt-1`, })) @@ -88,7 +89,7 @@ describe('strict local outbox and uploader', () => { }) const receipts = await uploader.flushPending(queued.event.eventId) expect(receipts.get(queued.event.eventId)).toMatchObject({ - status: 'clustered', similarReports: 7, recommendedVersion: '0.3.0', + status: 'clustered', similarReports: 7, recommendedVersion: '0.4.0', }) expect(JSON.parse(received)).toEqual(queued.event) expect(Buffer.byteLength(received)).toBeLessThan(512)