fix(feedback): report shared anonymous user id
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/feedback/command-feedback/README.md
|
||||
README.md: 96d2825f4b63c95ad6f45ca8b2e05d1fc5ae92aa
|
||||
README.zh.md: 5220afe68b1f0de50fd1368900758906ee6907c9
|
||||
README.md: 52b8fb6a423fca69f76397deec36ecd22a6a6023
|
||||
README.zh.md: ca74d53f2531a46c2c16aa1423cee52e89c8256f
|
||||
@@ -8,7 +8,7 @@ Trigger-independent session feedback plus human-facing `/feedback` capture. The
|
||||
|
||||
| Input | Result |
|
||||
|---|---|
|
||||
| `/feedback <text>` | Append `feedback/record` and acknowledge with `Feedback recorded for session {id}`. |
|
||||
| `/feedback <text>` | Append `feedback/record` and acknowledge with `Feedback recorded for session {sessionId}` followed by `User: {userId}`. |
|
||||
| `/feedback` | Return a direct usage error. Whitespace-only input is treated as empty. |
|
||||
|
||||
Surrounding whitespace is discarded, but feedback is otherwise unparsed: no truncation, case folding, or control words. Text that looks like another command, such as `/feedback /plan felt slow`, is feedback content. Repeated commands each produce their own event; nothing is replaced or merged.
|
||||
@@ -17,7 +17,7 @@ Surrounding whitespace is discarded, but feedback is otherwise unparsed: no trun
|
||||
|
||||
`recordFeedback(session, text)` is the command-independent write path. It rejects empty normalized text and appends `feedback/record { text }`; a different UI, hook, or host integration can call it without constructing a slash command. The `/feedback` handler uses that producer and starts no model work. The optional [`dsh-session-telemetry-otel`](../../session/session-telemetry-otel) consumer observes the event without changing its capture contract.
|
||||
|
||||
The feedback text appears in exactly one durable payload: `feedback/record`. [`dsh-commands`](../../interaction/commands/README.md) still appends its generic `command/run` / `command/done` pairing, but this definition sets `recordInput: false`, so `command/run` omits `args`; the paired `command/done` carries only the outcome. All three events are log-only and absent from the ordered surface, `deriveMessages()`, and model requests. These appends start persistence's ordinary eager drain, but neither producer forces `session/flush`, so acknowledgement means the feedback is in the log, not that it has reached disk. Rejected empty input leaves only the command pairing settled as `kind: 'error'`, with no `feedback/record`.
|
||||
The feedback text appears in exactly one durable payload: `feedback/record`. [`dsh-commands`](../../interaction/commands/README.md) still appends its generic `command/run` / `command/done` pairing, but this definition sets `recordInput: false`, so `command/run` omits `args`; the paired `command/done` carries only the outcome. All three events are log-only and absent from the ordered surface, `deriveMessages()`, and model requests. These appends start persistence's ordinary eager drain, but neither producer forces `session/flush`, so acknowledgement means the feedback is in the log, not that it has reached disk. The acknowledgement identifies both the receiving session and the [shared anonymous user](../../session/user-id/); the first accepted feedback for a harness home can create `$DSH_HOME/.userid`. Rejected empty input leaves only the command pairing settled as `kind: 'error'`, with no `feedback/record` and no user-id lookup.
|
||||
|
||||
The event is authoritative rather than the command record because feedback may arrive through a trigger other than `/feedback`. Keeping the payload out of `command/run` avoids two records carrying the same text.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
| 输入 | 结果 |
|
||||
|---|---|
|
||||
| `/feedback <text>` | 追加 `feedback/record`,并以 `Feedback recorded for session {id}` 确认。 |
|
||||
| `/feedback <text>` | 追加 `feedback/record`,并以 `Feedback recorded for session {sessionId}` 确认,随后显示 `User: {userId}`。 |
|
||||
| `/feedback` | 返回一个直接用法错误。仅含空白的输入视为空输入。 |
|
||||
|
||||
前后空白会被丢弃,但除此之外,反馈内容不会被解析:没有截断、大小写折叠或控制词。看起来像另一个命令的文本(例如 `/feedback /plan felt slow`)就是反馈内容。重复执行命令时,每次都会产生一个事件;不会发生替换或合并。
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
`recordFeedback(session, text)` 是不依赖命令的写入路径。它拒绝规范化后为空的文本,并追加 `feedback/record { text }`;其他 UI、钩子或 host 集成无需构造斜杠命令即可调用它。`/feedback` 处理器通过该生产方写入,且不启动任何模型工作。可选的 [`dsh-session-telemetry-otel`](../../session/session-telemetry-otel) 消费方会观察该事件,但不改变它的采集约定。
|
||||
|
||||
反馈文本只出现在一个持久载荷中:`feedback/record`。[`dsh-commands`](../../interaction/commands/README.md) 仍会追加通用的 `command/run` / `command/done` 配对,但此定义设置了 `recordInput: false`,因此 `command/run` 会省略 `args`;配对的 `command/done` 只携带结果。三个事件都仅写入日志,不出现在有序 surface、`deriveMessages()` 以及模型请求中。这些追加会启动持久化的常规即时排空,但两个生产方都不会强制 `session/flush`,因此确认文本表示反馈已进入日志,而不表示它已经落盘。被拒绝的空输入只会留下以 `kind: 'error'` 结算的命令配对,不会产生 `feedback/record`。
|
||||
反馈文本只出现在一个持久载荷中:`feedback/record`。[`dsh-commands`](../../interaction/commands/README.md) 仍会追加通用的 `command/run` / `command/done` 配对,但此定义设置了 `recordInput: false`,因此 `command/run` 会省略 `args`;配对的 `command/done` 只携带结果。三个事件都仅写入日志,不出现在有序 surface、`deriveMessages()` 以及模型请求中。这些追加会启动持久化的常规即时排空,但两个生产方都不会强制 `session/flush`,因此确认文本表示反馈已进入日志,而不表示它已经落盘。确认文本同时标明接收反馈的会话和[共享匿名用户](../../session/user-id/);对于某个 harness home,首次接受反馈时可能创建 `$DSH_HOME/.userid`。被拒绝的空输入只会留下以 `kind: 'error'` 结算的命令配对,不会产生 `feedback/record`,也不会查找用户 id。
|
||||
|
||||
权威记录是该事件,而不是命令记录,因为反馈可能来自 `/feedback` 之外的触发方式。让载荷不进入 `command/run`,可避免两条记录携带相同文本。
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"@deepseek-ai/dsh-commands": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session": "^0.0.1",
|
||||
"@deepseek-ai/dsh-user-id": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -38,6 +39,7 @@
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-user-id": "workspace:^",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
import type { Context } from 'cordis'
|
||||
import type { CommandInvocation, CommandResult } from '@deepseek-ai/dsh-commands'
|
||||
import type { Session } from '@deepseek-ai/dsh-session'
|
||||
import { getOrCreateAnonymousUserId } from '@deepseek-ai/dsh-user-id'
|
||||
|
||||
export const name = 'command-feedback'
|
||||
export const inject = ['commands']
|
||||
@@ -41,8 +42,8 @@ export function recordFeedback(session: Session, text: string): void {
|
||||
* Validate, record, and acknowledge one feedback entry. Returning an error
|
||||
* leaves no `feedback/record` event.
|
||||
* @param invocation - receiving agent, raw command input, and UI cancellation.
|
||||
* @returns an acknowledgement containing the receiving session id, or a usage error
|
||||
* when no feedback text was supplied.
|
||||
* @returns an acknowledgement containing the receiving session and anonymous
|
||||
* user ids, or a usage error when no feedback text was supplied.
|
||||
*/
|
||||
function executeFeedbackCommand(invocation: CommandInvocation): CommandResult {
|
||||
if (invocation.rawInput.trim().length === 0) {
|
||||
@@ -51,7 +52,7 @@ function executeFeedbackCommand(invocation: CommandInvocation): CommandResult {
|
||||
recordFeedback(invocation.agent.session, invocation.rawInput)
|
||||
return {
|
||||
kind: 'success',
|
||||
text: `Feedback recorded for session ${invocation.agent.session.id}`,
|
||||
text: `Feedback recorded for session ${invocation.agent.session.id}\nUser: ${getOrCreateAnonymousUserId()}`,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
|
||||
@@ -7,6 +7,17 @@ import CommandService from '@deepseek-ai/dsh-commands'
|
||||
import SessionStore, { foldSurface, Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import * as commandFeedback from '@deepseek-ai/dsh-command-feedback'
|
||||
|
||||
const { USER_ID, getOrCreateAnonymousUserId } = vi.hoisted(() => {
|
||||
const USER_ID = '01234567-89ab-4cde-8f01-23456789abcd'
|
||||
return { USER_ID, getOrCreateAnonymousUserId: vi.fn(() => USER_ID) }
|
||||
})
|
||||
|
||||
vi.mock('@deepseek-ai/dsh-user-id', () => ({
|
||||
getOrCreateAnonymousUserId,
|
||||
}))
|
||||
|
||||
beforeEach(() => getOrCreateAnonymousUserId.mockClear())
|
||||
|
||||
interface Harness {
|
||||
readonly ctx: Context
|
||||
readonly agent: Agent
|
||||
@@ -93,7 +104,7 @@ describe('/feedback human command', () => {
|
||||
const test = await harness()
|
||||
await expect(run(test, ' the diff view is unreadable')).resolves.toEqual({
|
||||
kind: 'success',
|
||||
text: `Feedback recorded for session ${test.session.id}`,
|
||||
text: `Feedback recorded for session ${test.session.id}\nUser: ${USER_ID}`,
|
||||
})
|
||||
expect(feedbackTexts(test.session)).toEqual(['the diff view is unreadable'])
|
||||
const commandRun = test.session.events.find(event => event.type === 'command/run')
|
||||
@@ -141,8 +152,8 @@ describe('/feedback human command', () => {
|
||||
test.ctx.commands.execute(test.agent, '/feedback second', signal),
|
||||
])
|
||||
expect(settled.map(item => item?.result)).toEqual([
|
||||
{ kind: 'success', text: `Feedback recorded for session ${test.session.id}` },
|
||||
{ kind: 'success', text: `Feedback recorded for session ${test.session.id}` },
|
||||
{ kind: 'success', text: `Feedback recorded for session ${test.session.id}\nUser: ${USER_ID}` },
|
||||
{ kind: 'success', text: `Feedback recorded for session ${test.session.id}\nUser: ${USER_ID}` },
|
||||
])
|
||||
expect(feedbackTexts(test.session)).toEqual(['first', 'second'])
|
||||
})
|
||||
@@ -167,6 +178,7 @@ describe('/feedback human command', () => {
|
||||
}
|
||||
await expect(run(test)).resolves.toEqual(expected)
|
||||
await expect(run(test, ' \n\t ')).resolves.toEqual(expected)
|
||||
expect(getOrCreateAnonymousUserId).not.toHaveBeenCalled()
|
||||
expect(feedbackTexts(test.session)).toEqual([])
|
||||
const done = test.session.events.filter(event => event.type === 'command/done')
|
||||
expect(done.map(event => event.data.kind)).toEqual(['error', 'error'])
|
||||
|
||||
@@ -2,7 +2,7 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import Include from '@cordisjs/plugin-include'
|
||||
@@ -11,6 +11,7 @@ import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent'
|
||||
import CommandService from '@deepseek-ai/dsh-commands'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import * as CommandFeedback from '@deepseek-ai/dsh-command-feedback'
|
||||
import { getOrCreateAnonymousUserId } from '@deepseek-ai/dsh-user-id'
|
||||
|
||||
let root: string | undefined
|
||||
let context: Context | undefined
|
||||
@@ -20,6 +21,7 @@ afterEach(async () => {
|
||||
context = undefined
|
||||
if (root !== undefined) await rm(root, { recursive: true, force: true })
|
||||
root = undefined
|
||||
vi.unstubAllEnvs()
|
||||
})
|
||||
|
||||
/** Register one idle agent over a store-owned session, as an app's spine does. */
|
||||
@@ -51,6 +53,7 @@ function agent(ctx: Context): Agent {
|
||||
describe('/feedback real Loader composition through cordis.yml', () => {
|
||||
it('boots cordis.yml and records feedback without model-visible output', async () => {
|
||||
root = await mkdtemp(join(tmpdir(), 'dsh-command-feedback-loader-'))
|
||||
vi.stubEnv('DSH_HOME', root)
|
||||
const configPath = join(root, 'cordis.yml')
|
||||
await writeFile(configPath, [
|
||||
"- name: '@deepseek-ai/dsh-agent'",
|
||||
@@ -87,9 +90,10 @@ describe('/feedback real Loader composition through cordis.yml', () => {
|
||||
expect(context.commands.list(owner).map(command => command.name)).toContain('feedback')
|
||||
|
||||
const accepted = await context.commands.execute(owner, '/feedback the diff view is unreadable', signal)
|
||||
const userId = getOrCreateAnonymousUserId({ env: { DSH_HOME: root } })
|
||||
expect(accepted?.result).toEqual({
|
||||
kind: 'success',
|
||||
text: 'Feedback recorded for session feedback-loader-agent',
|
||||
text: `Feedback recorded for session feedback-loader-agent\nUser: ${userId}`,
|
||||
})
|
||||
const rejected = await context.commands.execute(owner, '/feedback', signal)
|
||||
expect(rejected?.result).toEqual({
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
{
|
||||
"path": "../../core/session"
|
||||
},
|
||||
{
|
||||
"path": "../../session/user-id"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user