Files
deepseek-harness/docs/subsystems/feedback.zh.md
T
creatixchu f2f9700de2 feat(feedback): open one feedback dialog from /feedback and Dislike, with categories and a toast
Dislike and a bare /feedback open the Session's feedback dialog, which
collects one of seven fixed categories and an optional description and
records through the messageFeedback put or the new sessionFeedback
Remote; Like records at once. A recorded rating shows the filled glyph
and every submission is acknowledged by a toast. feedback/record and
MessageFeedbackItem carry the optional category; the note popover, its
layout scenario, and the clearNote/clear verbs are removed. ui-commands
gains the action command kind the /feedback decoration runs as.

Closes #3515
2026-09-09 10:20:24 +08:00

18 KiB
Raw Blame History

消息反馈

English | 中文

@deepseek-ai/dsh-message-feedback拥有针对单条 assistant 消息的可编辑反馈。权威 Session 日志保存 feedback/message-putfeedback/message-delete;不可变的 Session 级备注仍使用 feedback/record,由 @deepseek-ai/dsh-command-feedback 连同两种反馈共用的 FeedbackCategory 分类表一起拥有。三者都是仅写日志的事件,绝不进入模型上下文。

来源:packages/feedback/message-feedback/src/types.ts

公开类型

/** Opaque compare-and-set token for one exact feedback item revision. */
type MessageFeedbackVersion = Branded<'MessageFeedbackVersion'>
/** The human's overall judgment of one assistant message. */
type MessageFeedbackRating = 'positive' | 'negative'
/** One current feedback value and its opaque mutation token. */
interface MessageFeedbackItem {
  /** Stable identity of the assistant message inside the owning Session. */
  readonly messageId: MessageId
  /** Overall positive or negative judgment. */
  readonly rating: MessageFeedbackRating
  /** Optional explanation, preserved verbatim after validation. */
  readonly note?: string
  /** Category the human filed a negative judgment under. */
  readonly category?: FeedbackCategory
  /** Equality-only token replaced by every material create or update. */
  readonly version: MessageFeedbackVersion
  /** Host-assigned creation time in Unix epoch milliseconds. */
  readonly createdAt: number
  /** Host-assigned time of the most recent material update. */
  readonly updatedAt: number
}
/** A material creation or edit, retaining its complete current value. */
interface MessageFeedbackPut {
  /** Owning Session; inherited feedback in a fork belongs to its parent. */
  readonly sessionId: SessionId
  /** Value after this mutation, including the original creation time. */
  readonly item: MessageFeedbackItem
}
/** A material deletion of one current feedback item. */
interface MessageFeedbackDelete {
  /** Session that owns the deleted feedback. */
  readonly sessionId: SessionId
  /** Message whose feedback was removed. */
  readonly messageId: MessageId
}
/** Read all message feedback belonging to one persisted Session lifecycle. */
interface MessageFeedbackListRequest {
  /** Session whose feedback events should be read. */
  readonly sessionId: SessionId
}
/** Current feedback values for one Session, in first-creation order. */
interface MessageFeedbackListValue {
  /** Fresh immutable item snapshots. */
  readonly items: readonly MessageFeedbackItem[]
}
/** Create or replace feedback for one assistant message. */
interface MessageFeedbackPutRequest {
  /** Persisted Session that owns the target message. */
  readonly sessionId: SessionId
  /** Target assistant-message identity. */
  readonly messageId: MessageId
  /** Desired overall judgment. */
  readonly rating: MessageFeedbackRating
  /** Optional non-blank explanation. */
  readonly note?: string
  /** Optional category; absent keeps the item uncategorized. */
  readonly category?: FeedbackCategory
  /** Observed item version, or `null` to require that no item exists. */
  readonly ifVersion: MessageFeedbackVersion | null
}
/** Delete feedback for one message after observing its current version. */
interface MessageFeedbackDeleteRequest {
  /** Session that owns the feedback. */
  readonly sessionId: SessionId
  /** Message whose feedback should be absent after this operation. */
  readonly messageId: MessageId
  /** Observed item version; ignored when the item is already absent. */
  readonly ifVersion: MessageFeedbackVersion
}
/** Idempotent deletion acknowledgement. */
interface MessageFeedbackDeleteValue {
  /** Stable postcondition shared by the first deletion and every retry. */
  readonly absent: true
}
/** No persisted Session header exists for the requested id. */
interface MessageFeedbackSessionNotFound {
  readonly code: 'session-not-found'
  readonly sessionId: SessionId
}
/** The id does not name a derived, append-origin assistant message. */
interface MessageFeedbackTargetNotFound {
  readonly code: 'target-not-found'
  readonly sessionId: SessionId
  readonly messageId: MessageId
}
/** A material mutation did not match the addressed item's current version. */
interface MessageFeedbackVersionConflict {
  readonly code: 'version-conflict'
  /** Authoritative current item, or `null` when it does not exist. */
  readonly current: MessageFeedbackItem | null
}
/** A supplied note contains no non-whitespace character. */
interface MessageFeedbackNoteBlank {
  readonly code: 'note-blank'
}
/** A supplied note exceeds the configured UTF-8 byte limit. */
interface MessageFeedbackNoteTooLarge {
  readonly code: 'note-too-large'
  readonly maxBytes: number
  readonly actualBytes: number
}
/** Failures shared by the public message-feedback operations. */
type MessageFeedbackFailure =
  | MessageFeedbackSessionNotFound
  | MessageFeedbackTargetNotFound
  | MessageFeedbackVersionConflict
  | MessageFeedbackNoteBlank
  | MessageFeedbackNoteTooLarge
/** Successful public operation result. */
interface MessageFeedbackSuccess<T> {
  readonly ok: true
  readonly value: T
}
/** Rejected public operation result with a stable business failure. */
interface MessageFeedbackRejected<E extends MessageFeedbackFailure> {
  readonly ok: false
  readonly error: E
}
/** Result returned by the message-feedback `list` operation. */
type MessageFeedbackListResult =
  | MessageFeedbackSuccess<MessageFeedbackListValue>
  | MessageFeedbackRejected<MessageFeedbackSessionNotFound>
/** Result returned by the message-feedback `put` operation. */
type MessageFeedbackPutResult =
  | MessageFeedbackSuccess<MessageFeedbackItem>
  | MessageFeedbackRejected<
    | MessageFeedbackSessionNotFound
    | MessageFeedbackTargetNotFound
    | MessageFeedbackVersionConflict
    | MessageFeedbackNoteBlank
    | MessageFeedbackNoteTooLarge
  >
/** Result returned by the message-feedback `delete` operation. */
type MessageFeedbackDeleteResult =
  | MessageFeedbackSuccess<MessageFeedbackDeleteValue>
  | MessageFeedbackRejected<MessageFeedbackSessionNotFound | MessageFeedbackVersionConflict>

Session 反馈类型

来源:packages/feedback/command-feedback/src/types.ts

/** One of the fixed feedback categories; the ids are durable log vocabulary. */
type FeedbackCategory =
  | 'task-result'
  | 'instruction-following'
  | 'product-interaction'
  | 'service-stability'
  | 'resource-cost'
  | 'security-privacy-permission'
  | 'other'
/**
 * One recorded human remark about a Session. Both members are optional: a
 * submission with neither still records that the human asked for the
 * Session to be reviewed, which is what authorizes log delivery.
 */
interface FeedbackRecord {
  /** Free-text remark with surrounding whitespace removed; never empty when present. */
  readonly text?: string
  /** Category the human filed the remark under. */
  readonly category?: FeedbackCategory
}
/** Record one Session-level remark through the Host Remote. */
interface SessionFeedbackRecordRequest {
  /** Live Session the remark describes. */
  readonly sessionId: SessionId
  /** Free-text remark; blank text is recorded as absent. */
  readonly text?: string
  /** Category the human filed the remark under. */
  readonly category?: FeedbackCategory
}
/** Stable postcondition of a recorded remark. */
interface SessionFeedbackRecordValue {
  /** The remark is appended to the Session log; flushing follows the Session's own schedule. */
  readonly recorded: true
}
/** No live Session carries the requested id. */
interface SessionFeedbackSessionNotFound {
  readonly code: 'session-not-found'
  readonly sessionId: SessionId
}
/** Result returned by the `sessionFeedback.record` operation. */
type SessionFeedbackRecordResult =
  | { readonly ok: true; readonly value: SessionFeedbackRecordValue }
  | { readonly ok: false; readonly error: SessionFeedbackSessionNotFound }

数据与并发

当前条目由 payload 中 sessionId 与所属 Session 匹配的权威反馈事件归约得到。每个条目携带好评或差评、可选备注、可选分类、Host 分配的 createdAt/updatedAt 时间戳及自己的 opaque version。version 只能用于相等比较,且只与目标消息比较;调用方不能排序或自行合成它。

put 采用严格乐观并发:已有条目的每次请求都必须匹配当前 ifVersion,即使请求不会改变目标值(重复已存评分、备注与分类的 put)。冲突会返回权威当前条目(不存在时为 null),因此调用方无需额外读取,即可协调丢失响应或并发编辑。删除已经不存在的条目同样成功。按 Session 划分的队列串行执行读取与变更;cold 变更在读取、比较、追加和 flush 期间持有持久化写句柄。匹配版本的无变更操作不追加事件。

目标与生命周期权威

live 持有者的内存日志直接提供目标 Session 的观测;cold 读取使用 SessionPersistence.open(id, 'read') 句柄,变更则使用写句柄。两条路径都不构造 Session 或 Agent。先由 stat(id) 预检明确不存在;stat 已确认存在的 Session 若读取失败,会按基础设施故障原样传播。put 只接受具有指定 MessageId 的非空、append-origin assistant/messagereplacement-origin、仅承载 usage 的空记录和非 assistant 记录都不是反馈目标。

fork 种子可以包含父 Session 的反馈事件,但 payload 保留父级 sessionId,因此不会成为子 Session 的当前反馈。删除条目会追加删除标记;早先的评分与备注仍保留在日志中。

持久化与 Remote 约定

成功的消息反馈变更会等待权威持久化完成:live 操作通过所属 Session 追加,并要求有 ctx.sessions.flush 监听器参与;cold 操作通过写句柄追加并 flush。持久化故障会原样传播,不会报告成功。maxNoteBytes 为必填项,按 UTF-8 字节限制备注文本;Web Host 组合将其设为 8192。该包通过 TypertRemoteService@Remote 发布 Host messageFeedback.listmessageFeedback.putmessageFeedback.delete 一元 Remote 约定;command-feedback 以同样方式发布面向 live Session 的 Session 级备注 sessionFeedback.record。下方生成的 Cordis API 是方法级权威。

插件释放会关闭操作接纳,并排空已进入各 Session 队列的工作。

显式启用后,session-log-deepseek 会在后续符合条件的 DeepSeek 请求中,把反馈作为普通 dsh_session_log 后缀的一部分传送。记录反馈不会触发 LLM 请求,也不会单独上传 dsh_feedback。对于非 DeepSeek 路由,OTel 后端可以将权威日志前缀释放至已记录的反馈。命令确认文本确认记录并标识 Session 与匿名用户,不报告遥测策略或投递结果。

Web 界面

@deepseek-ai/dsh-client-ui-message-feedback 是浏览器侧消费方。@deepseek-ai/dsh-api-remotes 挂载生成的 messageFeedbacksessionFeedback 贡献,因此该插件调用 ctx.remote.messageFeedbackctx.remote.sessionFeedback,不接触传输层。

控件是 conversation.chat.assistant-actions list slot 的 feedback 条目(order 10),该 slot 由 ui-conversation 声明,并渲染在已定稿助手消息的 IconActions 行内。AssistantMessageNode 携带来自 assistant/message 事件的可选 messageId。被中断冻结的部分输出没有该字段,渲染点在字段缺失时跳过该 slot。该操作栏每个 Turn 渲染一次,位于收尾的助手消息上:Host 接受每条 append-origin 步骤消息作为目标,但多步骤 Turn 中较早的步骤渲染的是工具行而非可评分正文,因此 UI 暴露的范围比 Host 约定允许的更窄。

每个 Session 一个 MessageFeedbackController,支撑该 Session 内所有消息的控件:一次 list 读取即填充整段对话,且延迟到首次 hover 或 focus 才发起,而非挂载时触发。每次变更把该 controller 最后观察到的版本作为 ifVersion 发送;version-conflict 响应携带权威条目,controller 据此对账而不重新拉取。变更按 Session 串行,排队操作与已提交版本比较。connection/reset 只刷新已读取过的 Session。

点赞立即记录不带备注的好评并显示确认 toast。点踩打开该 Session 的反馈弹窗,即 conversation.input.overlayfeedback-dialog 条目:共用的 Modal 卡片,里面是七个分类标签和一个详情框。提交会 put 一条差评,带上所选分类与去除首尾空白的描述,两者也可都不带。不带文本的 /feedbackui-commandsaction 路由的一个装饰)为 Session 打开同一个弹窗,随后通过 sessionFeedback.record 记录;/feedback <text> 仍走宿主命令路径。再次点击已记录的评分会撤回它。

边界与限制

  • 操作队列仅在进程内生效;cold 写入排他性依赖所选持久化提供方。
  • 删除只移除当前条目,不会抹除 append-only 日志或已投递后缀中的早先备注。
  • 请求若恰好落在 live detach 之后、persistence catalog 物化 header 之前的极短窗口,可能收到 session-not-found;调用方应在 retirement materialization 后重试。
  • cold 请求读取完整日志;服务没有条目数或聚合字节上限。maxNoteBytes 只限制每条备注。
  • Host 约定不记录已认证的 actor 或审计身份,因此假设调用方边界可信。
  • Web 控件只出现在对话视图。trajectory 与 waterfall 视图不渲染反馈条目,尽管它们的助手节点携带相同的 messageId
  • Web 控制器不消费反馈日志事件,因此另一个标签页的评分要等到重连或下一次冲突响应才可见,不会立即出现。
  • 弹窗不预先校验 maxNoteBytes;超长描述在提交时以 note-too-large 失败,而不是在输入过程中。
  • sessionFeedback.record 只服务 live Session,否则回答 session-not-found;弹窗打开期间 Session 退役时,弹窗会报告该失败。

Cordis API

Generated from source by scripts/gen-cordis-catalog.ts (verified fresh by pnpm run verify-cordis-catalog in doc-sync; regenerate with pnpm run gen-cordis-catalog) — the language sides differ only in locale-specific paired document paths. Signature blocks use a ts cordis-catalog fence and keep the original source JSDoc; dispatch modes are defined in the primer, and the framework-inherited ctx API lives in cordis-api/inherited.md.

ctx.messageFeedbackMessageFeedbackService

Session-log service; cold operations never construct a Session or Agent.

/**
 * Read current feedback from the canonical log.
 * @param request - Session to inspect.
 * @returns immutable items or a definite persistence miss.
 */
@Remote('list') list(request: MessageFeedbackListRequest): Promise<MessageFeedbackListResult>

/**
 * Create or replace feedback after checking its current version.
 * Matching no-ops retain the version and append no event.
 * @param request - Target, desired value, and observed item version.
 * @returns the durable item or an explicit business failure.
 */
@Remote('put') put(request: MessageFeedbackPutRequest): Promise<MessageFeedbackPutResult>

/**
 * Delete one item after checking its version; absence succeeds without an event.
 * @param request - Session, message, and observed item version.
 * @returns the stable absent postcondition or an explicit failure.
 */
@Remote('delete') delete(request: MessageFeedbackDeleteRequest): Promise<MessageFeedbackDeleteResult>

Source: packages/feedback/message-feedback/src/index.ts

ctx.sessionFeedbackSessionFeedbackService

Host Remote through which a product surface records a Session-level remark.

/**
 * Record one remark on a live Session.
 * @param request - target Session plus the optional text and category.
 * @returns the recorded postcondition, or `session-not-found` when no live
 * Session carries the id.
 */
@Remote('record') record(request: SessionFeedbackRecordRequest): Promise<SessionFeedbackRecordResult>

Source: packages/feedback/command-feedback/src/index.ts

feedback/* events

feedback/committed — parallel

Observe a durable cold feedback mutation without publishing a live Session. Observers run before write ownership is released and must not await another message-feedback operation for this Session. The payload is borrowed read-only; deep-clone it before transferring ownership (for example, to Session.fromRestore).

/**
 * Observe a durable cold feedback mutation without publishing a live Session.
 * Observers run before write ownership is released and must not await
 * another message-feedback operation for this Session. The payload is borrowed
 * read-only; deep-clone it before transferring ownership (for example, to Session.fromRestore).
 * @param inspection - committed canonical prefix, including the feedback as its last event.
 * @mode parallel
 */
'feedback/committed'(inspection: SessionInspection): void

Types: SessionInspection

Source: packages/feedback/message-feedback/src/index.ts